From bbcafbfc8d241f5e7c18baca41c6e8272f1d5f47 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 15 Mar 2024 10:08:39 +0900 Subject: [PATCH] Revert "Migrate v2 branch to develop (#3102)" This reverts commit 76f2313d043a7687ad6e1eca38bd377352dcf377. --- .ci/piptools-deps.in | 1 - .ci/piptools-deps.txt | 45 - .ci/publish-deps.in | 2 - .ci/publish-deps.txt | 320 - .ci/tox-deps.in | 1 - .ci/tox-deps.txt | 62 - .dockerignore | 22 +- .flake8 | 5 + .github/CODEOWNERS | 4 - .github/{codecov.yaml => codecov.yml} | 0 .github/dependabot.yaml | 26 - .github/dependabot.yml | 31 + .github/labeler.yml | 15 +- .github/pull_request_template.md | 2 +- .github/workflows/code_scan.yaml | 58 - .github/workflows/code_scan.yml | 66 + .github/workflows/codeql.yaml | 76 - .github/workflows/codeql.yml | 77 + .github/workflows/daily.yml | 41 + .github/workflows/docs.yaml | 18 +- .github/workflows/docs_stable.yaml | 71 - .github/workflows/docs_stable.yml | 75 + .github/workflows/labeler.yaml | 22 - .github/workflows/labeler.yml | 17 + .github/workflows/perf_benchmark.yaml | 61 +- .github/workflows/pre_merge.yaml | 112 - .github/workflows/pre_merge.yml | 143 + .github/workflows/publish.yaml | 85 - .github/workflows/publish.yml | 91 + .github/workflows/publish_internal.yaml | 91 - .github/workflows/publish_internal.yml | 100 + .github/workflows/run_tests_in_tox.yml | 75 + .github/workflows/run_tests_in_tox_custom.yml | 81 + .../{scorecard.yaml => scorecard.yml} | 0 .../{stale_marker.yaml => stale_marker.yml} | 0 .github/workflows/weekly.yml | 22 + .gitignore | 13 +- .../core/config/__init__.py => .gitmodules | 0 .isort.cfg | 4 + .pre-commit-config.yaml | 41 +- CODEOWNERS | 5 + MANIFEST.in | 3 +- NOTICE | 3 - docker/Dockerfile.cpu | 28 + docker/Dockerfile.cuda | 40 - docker/README.md | 24 - docker/build.sh | 59 +- docker/download_pretrained_weights.py | 52 - docker/requirements.in | 3 + docker/requirements.txt | 264 + docs/README.md | 21 - docs/source/conf.py | 34 +- .../additional_features/adaptive_training.rst | 22 +- .../auto_configuration.rst | 94 +- .../additional_features/config_input_size.rst | 71 + .../additional_features/fast_data_loading.rst | 53 +- .../explanation/additional_features/hpo.rst | 36 +- .../explanation/additional_features/index.rst | 2 + .../models_optimization.rst | 26 +- .../noisy_label_detection.rst | 30 + .../additional_features/tiling.rst | 185 +- .../explanation/additional_features/xai.rst | 15 - .../action/action_classification.rst | 9 +- .../algorithms/action/action_detection.rst | 15 +- .../explanation/algorithms/anomaly/index.rst | 18 +- .../hierarhical_classification.rst | 17 +- .../multi_class_classification.rst | 183 +- .../multi_label_classification.rst | 76 +- .../guide/explanation/algorithms/index.rst | 6 + .../object_detection/object_detection.rst | 162 +- .../segmentation/instance_segmentation.rst | 111 +- .../segmentation/semantic_segmentation.rst | 216 +- .../visual_prompting/fine_tuning.rst | 41 +- .../algorithms/visual_prompting/index.rst | 2 +- .../algorithms/visual_prompting/zero_shot.rst | 34 +- .../source/guide/get_started/api_tutorial.rst | 501 - .../source/guide/get_started/cli_commands.rst | 754 +- .../source/guide/get_started/installation.rst | 116 +- .../source/guide/get_started/introduction.rst | 80 +- docs/source/guide/index.rst | 1 - .../guide/tutorials/advanced/api_tutorial.rst | 307 + .../guide/tutorials/advanced/backbones.rst | 157 + .../tutorials/advanced/configuration.rst | 94 - .../guide/tutorials/advanced/hpo_tutorial.rst | 110 + .../source/guide/tutorials/advanced/index.rst | 7 +- .../guide/tutorials/advanced/self_sl.rst | 198 + .../guide/tutorials/advanced/semi_sl.rst | 169 + docs/source/guide/tutorials/base/demo.rst | 96 + docs/source/guide/tutorials/base/deploy.rst | 193 + docs/source/guide/tutorials/base/explain.rst | 24 +- .../how_to_train/action_classification.rst | 16 +- .../base/how_to_train/anomaly_detection.rst | 330 +- .../base/how_to_train/classification.rst | 75 +- .../tutorials/base/how_to_train/detection.rst | 611 +- .../tutorials/base/how_to_train/index.rst | 43 - .../how_to_train/instance_segmentation.rst | 420 +- .../how_to_train/semantic_segmentation.rst | 10 +- docs/source/guide/tutorials/base/index.rst | 16 +- .../multi_cls_selfsl_performance_CIFAR10.png | Bin 0 -> 63495 bytes .../multi_cls_selfsl_performance_CIFAR100.png | Bin 0 -> 60737 bytes .../multi_cls_selfsl_performance_Food-101.png | Bin 0 -> 56273 bytes for_developers/add_custom_model.ipynb | 286 - for_developers/cli_guide.md | 350 - for_developers/contribution_guide.md | 65 - for_developers/dir_structure.md | 36 - for_developers/engine_api_example.ipynb | 541 - for_developers/helpers.py | 86 - .../images/contribution_guide/ask_review.png | Bin 66353 -> 0 bytes .../images/contribution_guide/copy_link.png | Bin 105636 -> 0 bytes .../contribution_guide/design_proposal.png | Bin 58438 -> 0 bytes .../images/contribution_guide/design_req.png | Bin 100716 -> 0 bytes .../product_design/core_design_drawing.drawio | 1381 --- .../images/product_design/reuse_model.png | Bin 270631 -> 0 bytes .../support_various_data_format.png | Bin 427365 -> 0 bytes .../images/product_design/task_data_model.png | Bin 393940 -> 0 bytes for_developers/product_design.md | 116 - for_developers/regression_test/Dockerfile | 9 - for_developers/regression_test/README.md | 78 - for_developers/regression_test/build.sh | 9 - .../regression_test/docker-compose.yml | 19 - .../images/mlflow_dashboard.png | Bin 473442 -> 0 bytes .../images/mlflow_filtering.png | Bin 308868 -> 0 bytes .../images/mlflow_metrics_and_tags.png | Bin 156175 -> 0 bytes .../regression_test/requirements.in | 2 - .../regression_test/requirements.txt | 1062 -- for_developers/requirements-lock.txt | 101 - for_developers/setup_guide.md | 96 - for_developers/torchvision.ipynb | 95 - pyproject.toml | 294 +- requirements/action.txt | 6 + requirements/anomaly.txt | 4 + requirements/api.txt | 12 + requirements/base.txt | 14 + requirements/classification.txt | 7 + requirements/detection.txt | 10 + requirements/dev.txt | 14 + requirements/docs.txt | 10 + requirements/gh-actions.txt | 45 + requirements/openvino.txt | 8 + requirements/publish.txt | 2 + requirements/segmentation.txt | 9 + requirements/visual_prompting.txt | 5 + setup.py | 226 + src/otx/__init__.py | 17 +- src/otx/algo/__init__.py | 8 - .../algo/action_classification/__init__.py | 11 - .../backbones/__init__.py | 7 - .../backbones/movinet.py | 765 -- .../action_classification/heads/__init__.py | 7 - .../mmconfigs/movinet.yaml | 26 - .../action_classification/mmconfigs/x3d.yaml | 28 - src/otx/algo/action_classification/movinet.py | 20 - .../action_classification/openvino_model.py | 105 - .../recognizers/__init__.py | 9 - .../recognizers/movinet_recognizer.py | 46 - .../recognizers/recognizer.py | 29 - src/otx/algo/action_classification/x3d.py | 20 - src/otx/algo/action_detection/__init__.py | 4 - .../mmconfigs/x3d_fastrcnn.yaml | 51 - src/otx/algo/action_detection/x3d_fastrcnn.py | 22 - src/otx/algo/anomaly/__init__.py | 9 - src/otx/algo/anomaly/padim.py | 42 - src/otx/algo/anomaly/stfpm.py | 46 - src/otx/algo/callbacks/__init__.py | 4 - .../callbacks/adaptive_train_scheduling.py | 166 - src/otx/algo/classification/__init__.py | 9 - .../algo/classification/backbones/__init__.py | 10 - .../backbones/otx_efficientnet.py | 660 -- .../backbones/otx_efficientnet_v2.py | 104 - .../backbones/otx_mobilenet_v3.py | 368 - src/otx/algo/classification/deit_tiny.py | 179 - .../algo/classification/efficientnet_b0.py | 53 - .../algo/classification/efficientnet_v2.py | 53 - src/otx/algo/classification/heads/__init__.py | 16 - .../heads/custom_hlabel_linear_cls_head.py | 222 - .../custom_hlabel_non_linear_cls_head.py | 253 - .../custom_multilabel_linear_cls_head.py | 127 - .../custom_multilabel_non_linear_cls_head.py | 136 - .../algo/classification/losses/__init__.py | 8 - .../asymmetric_angular_loss_with_ignore.py | 151 - .../hlabel_classification/deit_tiny.yaml | 40 - .../efficientnet_b0_light.yaml | 32 - .../efficientnet_v2_light.yaml | 33 - .../mobilenet_v3_large_light.yaml | 30 - .../multiclass_classification/deit_tiny.yaml | 33 - .../efficientnet_b0.yaml | 39 - .../efficientnet_b0_light.yaml | 28 - .../efficientnet_v2.yaml | 38 - .../efficientnet_v2_light.yaml | 27 - .../mobilenet_v3_large.yaml | 37 - .../mobilenet_v3_large_light.yaml | 26 - .../multilabel_classification/deit_tiny.yaml | 32 - .../efficientnet_b0_light.yaml | 29 - .../efficientnet_v2_light.yaml | 28 - .../mobilenet_v3_large_light.yaml | 30 - .../algo/classification/mobilenet_v3_large.py | 78 - src/otx/algo/classification/otx_dino_v2.py | 148 - .../algo/classification/torchvision_model.py | 255 - src/otx/algo/detection/__init__.py | 9 - src/otx/algo/detection/atss.py | 60 - src/otx/algo/detection/backbones/__init__.py | 8 - .../backbones/pytorchcv_backbones.py | 179 - src/otx/algo/detection/heads/__init__.py | 9 - .../heads/class_incremental_mixin.py | 121 - .../heads/custom_anchor_generator.py | 87 - .../algo/detection/heads/custom_atss_head.py | 292 - .../algo/detection/heads/custom_ssd_head.py | 70 - src/otx/algo/detection/losses/__init__.py | 7 - .../algo/detection/losses/cross_focal_loss.py | 149 - .../detection/mmconfigs/atss_mobilenetv2.yaml | 95 - .../detection/mmconfigs/atss_r50_fpn.yaml | 101 - .../detection/mmconfigs/atss_resnext101.yaml | 104 - .../algo/detection/mmconfigs/rtmdet_tiny.yaml | 91 - .../detection/mmconfigs/ssd_mobilenetv2.yaml | 97 - src/otx/algo/detection/mmconfigs/yolox_l.yaml | 47 - src/otx/algo/detection/mmconfigs/yolox_s.yaml | 47 - .../algo/detection/mmconfigs/yolox_tiny.yaml | 47 - src/otx/algo/detection/mmconfigs/yolox_x.yaml | 47 - src/otx/algo/detection/mmdeploy/__init__.py | 4 - src/otx/algo/detection/mmdeploy/atss.py | 14 - .../algo/detection/mmdeploy/atss_r50_fpn.py | 14 - .../algo/detection/mmdeploy/base_detection.py | 54 - src/otx/algo/detection/mmdeploy/rtmdet.py | 14 - .../detection/mmdeploy/ssd_mobilenetv2.py | 14 - src/otx/algo/detection/mmdeploy/yolox.py | 14 - src/otx/algo/detection/mmdeploy/yolox_tiny.py | 14 - src/otx/algo/detection/rtmdet.py | 39 - src/otx/algo/detection/ssd.py | 266 - src/otx/algo/detection/yolox.py | 60 - src/otx/algo/hooks/__init__.py | 4 - src/otx/algo/hooks/recording_forward_hook.py | 454 - .../algo/instance_segmentation/__init__.py | 8 - .../instance_segmentation/heads/__init__.py | 8 - .../heads/custom_roi_head.py | 307 - .../algo/instance_segmentation/maskrcnn.py | 60 - .../mmconfigs/maskrcnn_efficientnetb2b.yaml | 199 - .../mmconfigs/maskrcnn_r50.yaml | 199 - .../mmconfigs/maskrcnn_swint.yaml | 212 - .../mmconfigs/rtmdet_inst_tiny.yaml | 99 - .../mmdeploy/__init__.py | 4 - .../mmdeploy/base_instance_segmentation.py | 41 - .../mmdeploy/maskrcnn.py | 10 - .../mmdeploy/maskrcnn_swint.py | 15 - .../mmdeploy/rtmdet_inst.py | 17 - .../otx_instseg_evaluation.py | 67 - .../algo/instance_segmentation/rtmdet_inst.py | 34 - src/otx/algo/samplers/__init__.py | 8 - src/otx/algo/samplers/balanced_sampler.py | 148 - .../samplers/class_incremental_sampler.py | 181 - src/otx/algo/schedulers/__init__.py | 8 - src/otx/algo/schedulers/warmup_schedulers.py | 29 - src/otx/algo/segmentation/__init__.py | 8 - .../algo/segmentation/backbones/__init__.py | 9 - src/otx/algo/segmentation/backbones/dinov2.py | 55 - .../algo/segmentation/backbones/litehrnet.py | 1602 --- src/otx/algo/segmentation/dino_v2_seg.py | 31 - src/otx/algo/segmentation/heads/__init__.py | 9 - .../segmentation/heads/custom_fcn_head.py | 167 - .../segmentation/heads/custom_ham_head.py | 65 - src/otx/algo/segmentation/litehrnet.py | 385 - src/otx/algo/segmentation/losses/__init__.py | 8 - .../losses/cross_entropy_loss_with_ignore.py | 94 - .../segmentation/mmconfigs/dino_v2_seg.yaml | 63 - .../segmentation/mmconfigs/litehrnet_18.yaml | 122 - .../segmentation/mmconfigs/litehrnet_s.yaml | 112 - .../segmentation/mmconfigs/litehrnet_x.yaml | 139 - .../segmentation/mmconfigs/segnext_b.yaml | 96 - .../segmentation/mmconfigs/segnext_s.yaml | 96 - .../segmentation/mmconfigs/segnext_t.yaml | 96 - src/otx/algo/segmentation/segnext.py | 40 - src/otx/algo/utils/__init__.py | 13 - src/otx/algo/utils/mmconfig.py | 38 - src/otx/algo/utils/segmentation.py | 397 - src/otx/algo/utils/support_otx_v1.py | 119 - src/otx/algo/utils/xai_utils.py | 165 - src/otx/algo/visual_prompting/__init__.py | 21 - .../visual_prompting/backbones/__init__.py | 9 - .../visual_prompting/backbones/tiny_vit.py | 639 -- .../algo/visual_prompting/backbones/vit.py | 417 - .../visual_prompting/decoders/__init__.py | 8 - .../visual_prompting/encoders/__init__.py | 9 - .../encoders/sam_image_encoder.py | 81 - .../encoders/sam_prompt_encoder.py | 274 - .../algo/visual_prompting/openvino_models.py | 150 - .../algo/visual_prompting/segment_anything.py | 541 - .../algo/visual_prompting/utils/__init__.py | 9 - .../visual_prompting/utils/layer_norm_2d.py | 27 - .../zero_shot_segment_anything.py | 834 -- src/otx/algorithms/__init__.py | 6 + src/otx/algorithms/action/__init__.py | 15 + .../algorithms/action/adapters/__init__.py | 19 + .../action/adapters/mmaction/__init__.py | 12 + .../action/adapters/mmaction/data/__init__.py | 21 + .../adapters/mmaction/data/cls_dataset.py | 138 + .../adapters/mmaction/data/det_dataset.py | 323 + .../mmaction/data/pipelines/__init__.py | 19 + .../mmaction/data/pipelines/loading.py | 57 + .../adapters/mmaction/models/__init__.py | 11 + .../mmaction/models/backbones/__init__.py | 17 + .../mmaction/models/backbones/movinet.py | 771 ++ .../mmaction/models/detectors/__init__.py | 9 + .../mmaction/models/detectors/fast_rcnn.py | 121 + .../mmaction/models/heads/__init__.py | 9 + .../mmaction/models}/heads/movinet_head.py | 27 +- .../mmaction/models/heads/roi_head.py | 30 + .../mmaction/models/recognizers/__init__.py | 8 + .../models/recognizers/movinet_recognizer.py | 43 + .../action/adapters/mmaction/task.py | 594 ++ .../adapters/mmaction/utils/__init__.py | 9 + .../adapters/mmaction/utils/det_eval_utils.py | 128 + .../adapters/mmaction/utils/export_utils.py | 148 + .../action/adapters/openvino/__init__.py | 20 + .../action/adapters/openvino/dataloader.py | 229 + .../openvino/model_wrappers/__init__.py | 19 + .../model_wrappers/openvino_models.py | 186 + .../action/adapters/openvino/task.py | 337 + src/otx/algorithms/action/configs/__init__.py | 15 + .../action/configs/base/__init__.py | 19 + .../action/configs/base/configuration.py | 82 + .../action/configs/classification/__init__.py | 5 + .../configs/classification/base/__init__.py | 4 + .../base/base_classification_dynamic.py | 10 + .../base/base_classification_static.py | 19 + .../configs/classification/base/supervised.py | 53 + .../configs/classification/configuration.yaml | 453 + .../classification/movinet/__init__.py | 15 + .../classification/movinet/data_pipeline.py | 77 + .../classification/movinet/deployment.py | 3 + .../configs/classification/movinet/model.py | 38 + .../classification/movinet/template.yaml | 63 + .../configs/classification/x3d/__init__.py | 15 + .../classification/x3d/data_pipeline.py | 78 + .../configs/classification/x3d/deployment.py | 3 + .../configs/classification/x3d/model.py | 38 + .../configs/classification/x3d/template.yaml | 63 + .../action/configs/detection/__init__.py | 15 + .../action/configs/detection/base/__init__.py | 15 + .../detection/base/ava_data_pipeline.py | 68 + .../detection/base/base_detection_dynamic.py | 11 + .../detection/base/base_detection_static.py | 32 + .../configs/detection/base/data_pipeline.py | 64 + .../detection/base/faster_rcnn_config.py | 106 + .../configs/detection/base/supervised.py | 45 + .../configs/detection/configuration.yaml | 453 + .../detection/x3d_fast_rcnn/__init__.py | 15 + .../detection/x3d_fast_rcnn/data_pipeline.py | 18 + .../detection/x3d_fast_rcnn/deployment.py | 3 + .../configs/detection/x3d_fast_rcnn/model.py | 47 + .../detection/x3d_fast_rcnn/template.yaml | 63 + src/otx/algorithms/action/task.py | 452 + src/otx/algorithms/action/tools/__init__.py | 5 + .../action/tools/sample_classification.py | 178 + .../action/tools/sample_detection.py | 176 + src/otx/algorithms/action/utils/__init__.py | 5 + .../utils/convert_public_data_to_cvat.py | 368 + src/otx/algorithms/action/utils/data.py | 211 + src/otx/algorithms/anomaly/__init__.py | 4 + .../algorithms/anomaly/adapters/__init__.py | 19 + .../anomaly/adapters/anomalib/__init__.py | 4 + .../anomalib/accelerators/__init__.py | 8 + .../adapters/anomalib/accelerators/xpu.py | 60 + .../adapters/anomalib/callbacks/__init__.py | 21 + .../adapters/anomalib/callbacks/inference.py | 169 + .../anomalib}/callbacks/iteration_timer.py | 30 +- .../adapters/anomalib/callbacks/progress.py | 121 + .../adapters/anomalib/config/__init__.py | 19 + .../anomalib/config/anomalib_config.py | 104 + .../adapters/anomalib/data/__init__.py | 19 + .../data/create_mvtec_ad_json_annotations.py | 275 + .../anomaly/adapters/anomalib/data/data.py | 277 + .../anomaly/adapters/anomalib/data/dataset.py | 323 + .../anomaly/adapters/anomalib/data/mvtec.py | 164 + .../adapters/anomalib/plugins/__init__.py | 7 + .../anomalib/plugins/xpu_precision.py | 109 + .../adapters/anomalib/strategies/__init__.py | 8 + .../anomalib/strategies/xpu_single.py | 60 + .../algorithms/anomaly/configs/__init__.py | 15 + .../anomaly/configs/base/__init__.py | 21 + .../anomaly/configs/base/configuration.py | 130 + .../configs/base/configuration_enums.py | 64 + .../anomaly/configs/base/draem/__init__.py | 9 + .../configs/base/draem/configuration.py | 96 + .../anomaly/configs/base/padim/__init__.py | 19 + .../configs/base/padim/configuration.py | 52 + .../anomaly/configs/base/stfpm/__init__.py | 19 + .../configs/base/stfpm/configuration.py | 115 + .../configs/classification/__init__.py | 15 + .../configs/classification/draem/__init__.py | 19 + .../draem/compression_config.json | 36 + .../classification/draem/configuration.py | 24 + .../classification/draem/configuration.yaml | 242 + .../draem/template_experimental.yaml | 31 + .../draem/transform_config.yaml | 26 + .../configs/classification/padim/__init__.py | 19 + .../padim/compression_config.json | 38 + .../classification/padim/configuration.py | 24 + .../classification/padim/configuration.yaml | 183 + .../padim/ptq_optimization_config.py | 25 + .../classification/padim/template.yaml | 35 + .../configs/classification/stfpm/__init__.py | 19 + .../stfpm/compression_config.json | 36 + .../classification/stfpm/configuration.py | 24 + .../classification/stfpm/configuration.yaml | 312 + .../classification/stfpm/hpo_config.yaml | 18 + .../classification/stfpm/template.yaml | 40 + .../anomaly/configs/detection/__init__.py | 15 + .../configs/detection/draem/__init__.py | 19 + .../detection/draem/compression_config.json | 36 + .../configs/detection/draem/configuration.py | 24 + .../detection/draem/configuration.yaml | 242 + .../draem/template_experimental.yaml | 31 + .../detection/draem/transform_config.yaml | 31 + .../configs/detection/padim/__init__.py | 19 + .../detection/padim/compression_config.json | 38 + .../configs/detection/padim/configuration.py | 24 + .../detection/padim/configuration.yaml | 183 + .../padim/ptq_optimization_config.py | 25 + .../configs/detection/padim/template.yaml | 35 + .../configs/detection/stfpm/__init__.py | 19 + .../detection/stfpm/compression_config.json | 36 + .../configs/detection/stfpm/configuration.py | 24 + .../detection/stfpm/configuration.yaml | 312 + .../configs/detection/stfpm/hpo_config.yaml | 18 + .../configs/detection/stfpm/template.yaml | 40 + .../anomaly/configs/segmentation/__init__.py | 15 + .../configs/segmentation/draem/__init__.py | 19 + .../draem/compression_config.json | 36 + .../segmentation/draem/configuration.py | 24 + .../segmentation/draem/configuration.yaml | 242 + .../draem/template_experimental.yaml | 31 + .../segmentation/draem/transform_config.yaml | 26 + .../configs/segmentation/padim/__init__.py | 19 + .../padim/compression_config.json | 38 + .../segmentation/padim/configuration.py | 24 + .../segmentation/padim/configuration.yaml | 183 + .../padim/ptq_optimization_config.py | 25 + .../configs/segmentation/padim/template.yaml | 35 + .../configs/segmentation/stfpm/__init__.py | 19 + .../stfpm/compression_config.json | 36 + .../segmentation/stfpm/configuration.py | 24 + .../segmentation/stfpm/configuration.yaml | 312 + .../segmentation/stfpm/hpo_config.yaml | 18 + .../configs/segmentation/stfpm/template.yaml | 40 + .../algorithms/anomaly/ote_tests_pytest.ini | 2 + src/otx/algorithms/anomaly/tasks/__init__.py | 22 + src/otx/algorithms/anomaly/tasks/inference.py | 501 + src/otx/algorithms/anomaly/tasks/nncf.py | 247 + src/otx/algorithms/anomaly/tasks/openvino.py | 544 + src/otx/algorithms/anomaly/tasks/train.py | 152 + src/otx/algorithms/anomaly/tools/README.md | 23 + src/otx/algorithms/anomaly/tools/__init__.py | 15 + src/otx/algorithms/anomaly/tools/sample.py | 393 + src/otx/algorithms/classification/__init__.py | 22 + .../classification/adapters/__init__.py | 15 + .../classification/adapters/mmcls/__init__.py | 34 + .../adapters/mmcls/apis/__init__.py | 8 + .../adapters/mmcls/apis/train.py | 162 + .../adapters/mmcls/configurer.py | 224 + .../adapters/mmcls/datasets/__init__.py | 43 + .../adapters/mmcls/datasets/otx_datasets.py | 493 + .../mmcls/datasets/pipelines/__init__.py | 36 + .../mmcls/datasets/pipelines/otx_pipelines.py | 187 + .../datasets/pipelines/transforms/__init__.py | 19 + .../datasets/pipelines/transforms/augmix.py | 236 + .../pipelines/transforms/otx_transforms.py | 79 + .../pipelines/transforms/random_augment.py | 201 + .../pipelines/transforms/twocrop_transform.py | 28 + .../adapters/mmcls/models/__init__.py | 56 + .../mmcls/models/backbones/__init__.py | 19 + .../mmcls/models/backbones/mmov_backbone.py | 43 + .../mmcls/models/classifiers/__init__.py | 13 + .../adapters/mmcls/models/classifiers/byol.py | 227 + .../classifiers/custom_image_classifier.py | 393 + .../mmcls/models/classifiers/mixin.py | 126 + .../models/classifiers/semisl_classifier.py | 53 + .../semisl_multilabel_classifier.py | 49 + .../models/classifiers/supcon_classifier.py | 33 + .../adapters/mmcls/models/heads/__init__.py | 53 + .../adapters/mmcls/models/heads/cls_head.py | 33 + .../mmcls/models/heads/contrastive_head.py | 54 + .../adapters/mmcls/models/heads/conv_head.py | 83 + .../mmcls/models/heads/custom_cls_head.py | 110 + .../custom_hierarchical_linear_cls_head.py | 182 + ...custom_hierarchical_non_linear_cls_head.py | 212 + .../custom_multi_label_linear_cls_head.py | 143 + .../custom_multi_label_non_linear_cls_head.py | 149 + .../heads/custom_vision_transformer_head.py | 40 + .../adapters/mmcls/models/heads/mixin.py | 16 + .../mmcls/models/heads/mmov_cls_head.py | 90 + .../mmcls/models/heads/non_linear_cls_head.py | 101 + .../mmcls/models/heads/semisl_cls_head.py | 228 + .../heads/semisl_multilabel_cls_head.py | 308 + .../mmcls/models/heads/supcon_cls_head.py | 100 + .../adapters/mmcls/models/losses/__init__.py | 29 + .../asymmetric_angular_loss_with_ignore.py | 129 + .../losses/asymmetric_loss_with_ignore.py | 110 + .../mmcls/models/losses/barlowtwins_loss.py | 55 + .../mmcls/models/losses/cross_entropy_loss.py | 53 + .../adapters/mmcls/models/losses/ib_loss.py | 66 + .../adapters/mmcls/models/necks/__init__.py | 20 + .../adapters/mmcls/models/necks/mmov_neck.py | 27 + .../adapters/mmcls/models/necks/selfsl_mlp.py | 106 + .../adapters/mmcls/nncf/__init__.py | 14 + .../adapters/mmcls/nncf/builder.py | 171 + .../adapters/mmcls/nncf/patches.py | 11 + .../adapters/mmcls/nncf/registers.py | 17 + .../adapters/mmcls/nncf/task.py | 115 + .../adapters/mmcls/optimizer/__init__.py | 19 + .../adapters/mmcls/optimizer/lars.py | 152 + .../classification/adapters/mmcls/task.py | 652 ++ .../adapters/mmcls/utils/__init__.py | 12 + .../adapters/mmcls/utils/builder.py | 48 + .../adapters/mmcls/utils/config_utils.py | 66 + .../adapters/mmcls/utils/exporter.py | 78 + .../adapters/openvino/__init__.py | 19 + .../classification/adapters/openvino/task.py | 447 + .../classification/configs/__init__.py | 19 + .../classification/configs/base/__init__.py | 21 + .../configs/base/configuration.py | 82 + .../configs/base/data/__init__.py | 14 + .../configs/base/data/data_pipeline.py | 69 + .../configs/base/data/selfsl/__init__.py | 14 + .../configs/base/data/selfsl/data_pipeline.py | 61 + .../configs/base/data/semisl/__init__.py | 14 + .../configs/base/data/semisl/data_pipeline.py | 83 + .../configs/base/data/supcon/__init__.py | 14 + .../configs/base/data/supcon/data_pipeline.py | 58 + .../configs/base/deployments/__init__.py | 5 + .../base_classification_dynamic.py | 17 + .../deployments/base_classification_static.py | 28 + .../configs/base/models/__init__.py | 15 + .../configs/base/models/deit.py | 18 + .../configs/base/models/efficientnet.py | 14 + .../configs/base/models/efficientnet_v2.py | 14 + .../configs/base/models/mobilenet_v3.py | 16 + .../classification/configs/configuration.yaml | 496 + .../configs/deit_tiny/__init__.py | 15 + .../configs/deit_tiny/data_pipeline.py | 18 + .../configs/deit_tiny/deployment.py | 11 + .../configs/deit_tiny/hpo_config.yaml | 15 + .../classification/configs/deit_tiny/model.py | 20 + .../configs/deit_tiny/model_hierarchical.py | 26 + .../configs/deit_tiny/model_multilabel.py | 24 + .../deit_tiny/ptq_optimization_config.py | 4 + .../configs/deit_tiny/semisl/__init__.py | 15 + .../configs/deit_tiny/semisl/data_pipeline.py | 18 + .../configs/deit_tiny/semisl/hparam.yaml | 23 + .../configs/deit_tiny/semisl/model.py | 21 + .../deit_tiny/semisl/model_multilabel.py | 29 + .../configs/deit_tiny/template.yaml | 49 + .../efficientnet_b0_cls_incr/__init__.py | 15 + .../compression_config.json | 75 + .../efficientnet_b0_cls_incr/data_pipeline.py | 18 + .../efficientnet_b0_cls_incr/deployment.py | 11 + .../efficientnet_b0_cls_incr/hpo_config.yaml | 15 + .../configs/efficientnet_b0_cls_incr/model.py | 25 + .../model_hierarchical.py | 24 + .../model_multilabel.py | 24 + .../selfsl/__init__.py | 15 + .../selfsl/data_pipeline.py | 18 + .../selfsl/hparam.yaml | 29 + .../efficientnet_b0_cls_incr/selfsl/model.py | 26 + .../semisl/__init__.py | 15 + .../semisl/data_pipeline.py | 18 + .../semisl/hparam.yaml | 25 + .../efficientnet_b0_cls_incr/semisl/model.py | 22 + .../semisl/model_multilabel.py | 30 + .../supcon/__init__.py | 15 + .../supcon/data_pipeline.py | 18 + .../supcon/hparam.yaml | 6 + .../efficientnet_b0_cls_incr/supcon/model.py | 26 + .../efficientnet_b0_cls_incr/template.yaml | 63 + .../efficientnet_v2_s_cls_incr/__init__.py | 15 + .../compression_config.json | 35 + .../data_pipeline.py | 18 + .../efficientnet_v2_s_cls_incr/deployment.py | 11 + .../hpo_config.yaml | 15 + .../efficientnet_v2_s_cls_incr/model.py | 19 + .../model_hierarchical.py | 22 + .../model_multilabel.py | 24 + .../selfsl/__init__.py | 15 + .../selfsl/data_pipeline.py | 18 + .../selfsl/hparam.yaml | 29 + .../selfsl/model.py | 26 + .../semisl/__init__.py | 15 + .../semisl/data_pipeline.py | 18 + .../semisl/hparam.yaml | 25 + .../semisl/model.py | 16 + .../semisl/model_multilabel.py | 30 + .../supcon/__init__.py | 15 + .../supcon/data_pipeline.py | 18 + .../supcon/hparam.yaml | 6 + .../supcon/model.py | 26 + .../efficientnet_v2_s_cls_incr/template.yaml | 62 + .../__init__.py | 15 + .../compression_config.json | 75 + .../data_pipeline.py | 18 + .../hpo_config.yaml | 15 + .../mobilenet_v3_large_075_cls_incr/model.py | 21 + .../model_hierarchical.py | 27 + .../model_multilabel.py | 27 + .../selfsl/__init__.py | 15 + .../selfsl/data_pipeline.py | 18 + .../selfsl/hparam.yaml | 29 + .../selfsl/model.py | 28 + .../supcon/__init__.py | 15 + .../supcon/data_pipeline.py | 18 + .../supcon/hparam.yaml | 6 + .../supcon/model.py | 25 + .../template_experiment.yaml | 50 + .../mobilenet_v3_large_1_cls_incr/__init__.py | 15 + .../compression_config.json | 75 + .../data_pipeline.py | 18 + .../deployment.py | 11 + .../hpo_config.yaml | 15 + .../mobilenet_v3_large_1_cls_incr/model.py | 25 + .../model_hierarchical.py | 24 + .../model_multilabel.py | 32 + .../selfsl/__init__.py | 15 + .../selfsl/data_pipeline.py | 18 + .../selfsl/hparam.yaml | 29 + .../selfsl/model.py | 26 + .../semisl/__init__.py | 15 + .../semisl/data_pipeline.py | 18 + .../semisl/hparam.yaml | 25 + .../semisl/model.py | 22 + .../semisl/model_multilabel.py | 29 + .../supcon/__init__.py | 15 + .../supcon/data_pipeline.py | 18 + .../supcon/hparam.yaml | 6 + .../supcon/model.py | 25 + .../template.yaml | 62 + .../mobilenet_v3_small_cls_incr/__init__.py | 15 + .../compression_config.json | 75 + .../data_pipeline.py | 18 + .../hpo_config.yaml | 16 + .../mobilenet_v3_small_cls_incr/model.py | 13 + .../model_hierarchical.py | 21 + .../model_multilabel.py | 21 + .../selfsl/__init__.py | 15 + .../selfsl/data_pipeline.py | 18 + .../selfsl/hparam.yaml | 29 + .../selfsl/model.py | 23 + .../supcon/__init__.py | 15 + .../supcon/data_pipeline.py | 18 + .../supcon/hparam.yaml | 6 + .../supcon/model.py | 21 + .../template_experiment.yaml | 50 + src/otx/algorithms/classification/task.py | 545 + .../classification/tools/__init__.py | 15 + .../tools/classification_sample.py | 369 + .../classification/utils/__init__.py | 21 + .../classification/utils/cls_utils.py | 144 + .../utils/convert_coco_to_multilabel.py | 109 + src/otx/algorithms/common/__init__.py | 4 + .../algorithms/common/adapters/__init__.py | 15 + .../common/adapters/mmcv/__init__.py | 66 + .../common/adapters/mmcv/clsincr_mixin.py | 51 + .../common/adapters/mmcv/configs/__init__.py | 15 + .../mmcv/configs/backbones/__init__.py | 15 + .../configs/backbones/efficientnet_b2b.yaml | 11 + .../mmcv/configs/backbones/lite_hrnet_18.py | 59 + .../mmcv/configs/backbones/lite_hrnet_s.py | 60 + .../mmcv/configs/backbones/lite_hrnet_x.py | 61 + .../configs/backbones/mobilenet_v2_w1.yaml | 7 + .../mmcv/configs/backbones/resnet18.yaml | 10 + .../mmcv/configs/backbones/resnet50.yaml | 13 + .../mmcv/configs/backbones/segnext.py | 36 + .../common/adapters/mmcv/configurer.py | 587 ++ .../common/adapters/mmcv/hooks/__init__.py | 107 + .../mmcv/hooks/adaptive_repeat_data_hook.py | 81 + .../mmcv/hooks/adaptive_training_hook.py | 138 + .../common/adapters/mmcv/hooks/cancel_hook.py | 86 + .../adapters/mmcv/hooks/checkpoint_hook.py | 173 + .../mmcv/hooks/composed_dataloaders_hook.py | 50 + .../mmcv/hooks/custom_model_ema_hook.py | 133 + .../mmcv/hooks/dual_model_ema_hook.py | 143 + .../mmcv/hooks/early_stopping_hook.py | 389 + .../common/adapters/mmcv/hooks/eval_hook.py | 139 + .../adapters/mmcv/hooks/force_train_hook.py | 38 + .../mmcv/hooks/fp16_sam_optimizer_hook.py | 103 + .../adapters/mmcv/hooks/hpu_optimizer_hook.py | 30 + .../adapters/mmcv/hooks/ib_loss_hook.py | 42 + .../common/adapters/mmcv/hooks/logger_hook.py | 106 + .../mmcv/hooks/loss_dynamics_tracking_hook.py | 90 + .../adapters/mmcv/hooks/mean_teacher_hook.py | 60 + .../adapters/mmcv/hooks/mem_cache_hook.py | 33 + .../adapters/mmcv/hooks/model_ema_v2_hook.py | 120 + .../adapters/mmcv/hooks/no_bias_decay_hook.py | 73 + .../adapters/mmcv/hooks/progress_hook.py | 91 + .../mmcv/hooks/recording_forward_hook.py | 379 + .../adapters/mmcv/hooks/sam_optimizer_hook.py | 99 + .../adapters/mmcv/hooks/semisl_cls_hook.py | 63 + .../adapters/mmcv/hooks/task_adapt_hook.py | 93 + .../mmcv/hooks/two_crop_transform_hook.py | 86 + .../adapters/mmcv/hooks/xpu_optimizer_hook.py | 38 + .../common/adapters/mmcv/models/__init__.py | 21 + .../mmcv/models/backbones/__init__.py | 22 + .../mmcv/models/backbones/efficientnet.py | 1313 +++ .../mmcv/models/backbones/efficientnetv2.py | 111 + .../mmcv/models/backbones/mobilenetv3.py | 387 + .../models/backbones/torchvision_backbones.py | 249 + .../common/adapters/mmcv/models/builder.py | 21 + .../common/adapters/mmcv/nncf/__init__.py | 14 + .../common/adapters/mmcv/nncf/hooks.py | 32 + .../common/adapters/mmcv/nncf/patches.py | 38 + .../common/adapters/mmcv/nncf/runners.py | 181 + .../common/adapters/mmcv/nncf/utils.py | 329 + .../common/adapters/mmcv/ops/__init__.py | 8 + .../multi_scale_deformable_attn_pytorch.py | 163 + .../adapters/mmcv/pipelines/__init__.py | 4 + .../pipelines/load_image_from_otx_dataset.py | 210 + .../mmcv/pipelines/transforms/__init__.py | 4 + .../mmcv/pipelines/transforms/augments.py | 216 + .../transforms/cython_augments/__init__.py | 3 + .../transforms/cython_augments/cv_augment.pyx | 146 + .../cython_augments/pil_augment.pyx | 500 + .../algorithms/common/adapters/mmcv/runner.py | 150 + .../common/adapters/mmcv/semisl_mixin.py | 62 + .../common/adapters/mmcv/tasks/__init__.py | 28 + .../common/adapters/mmcv/tasks/exporter.py | 110 + .../common/adapters/mmcv/tasks/registry.py | 8 + .../common/adapters/mmcv/tasks/version.py | 11 + .../common/adapters/mmcv/utils/__init__.py | 53 + .../utils/_builder_build_data_parallel.py | 164 + .../_config_utils_get_configs_by_keys.py | 78 + .../_config_utils_get_configs_by_pairs.py | 79 + .../adapters/mmcv/utils/automatic_bs.py | 188 + .../common/adapters/mmcv/utils/builder.py | 83 + .../adapters/mmcv/utils/config_utils.py | 990 ++ .../common/adapters/mmcv/utils/fp16_utils.py | 133 + .../adapters/mmcv/utils/hpu_optimizers.py | 31 + .../common/adapters/mmdeploy/__init__.py | 12 + .../common/adapters/mmdeploy/apis.py | 377 + .../common/adapters/mmdeploy/ops/__init__.py | 8 + .../adapters/mmdeploy/ops/custom_ops.py | 39 + .../adapters/mmdeploy/utils/__init__.py | 16 + .../adapters/mmdeploy/utils/mmdeploy.py | 73 + .../common/adapters/mmdeploy/utils/onnx.py | 123 + .../mmdeploy/utils/operations_domain.py | 11 + .../common/adapters/mmdeploy/utils/utils.py | 69 + .../common/adapters/nncf/__init__.py | 48 + .../common/adapters/nncf/compression.py | 90 + .../algorithms/common/adapters/nncf/config.py | 119 + .../common/adapters/nncf/patches.py | 44 + .../common/adapters/nncf/utils/__init__.py | 26 + .../common/adapters/nncf/utils/utils.py | 129 + .../common/adapters/torch/__init__.py | 4 + .../common/adapters/torch/amp/__init__.py | 9 + .../adapters/torch/amp/xpu_grad_scaler.py | 120 + .../adapters/torch/dataloaders/__init__.py | 10 + .../torch/dataloaders/composed_dataloader.py | 71 + .../torch/dataloaders/samplers/__init__.py | 12 + .../dataloaders/samplers/balanced_sampler.py | 139 + .../dataloaders/samplers/cls_incr_sampler.py | 150 + .../torch/dataloaders/samplers/otx_sampler.py | 124 + .../common/adapters/torch/utils/__init__.py | 9 + .../adapters/torch/utils/bs_search_algo.py | 235 + .../common/adapters/torch/utils/utils.py | 48 + src/otx/algorithms/common/configs/__init__.py | 20 + .../common/configs/configuration_enums.py | 76 + .../common/configs/training_base.py | 443 + src/otx/algorithms/common/tasks/__init__.py | 15 + src/otx/algorithms/common/tasks/base_task.py | 356 + src/otx/algorithms/common/tasks/nncf_task.py | 350 + src/otx/algorithms/common/tools/__init__.py | 15 + src/otx/algorithms/common/utils/__init__.py | 79 + src/otx/algorithms/common/utils/callback.py | 111 + src/otx/algorithms/common/utils/data.py | 332 + src/otx/algorithms/common/utils/dist_utils.py | 29 + src/otx/algorithms/common/utils/ext_loader.py | 21 + src/otx/algorithms/common/utils/ir.py | 39 + .../algorithms/common/utils/mask_to_bbox.py | 71 + src/otx/algorithms/common/utils/mo_wrapper.py | 171 + src/otx/algorithms/common/utils/task_adapt.py | 108 + src/otx/algorithms/common/utils/utils.py | 221 + src/otx/algorithms/detection/__init__.py | 11 + .../algorithms/detection/adapters/__init__.py | 15 + .../detection/adapters/mmdet/__init__.py | 19 + .../detection/adapters/mmdet/apis/__init__.py | 8 + .../detection/adapters/mmdet/apis/train.py | 202 + .../detection/adapters/mmdet/configurer.py | 228 + .../adapters/mmdet/datasets/__init__.py | 21 + .../adapters/mmdet/datasets/dataset.py | 428 + .../mmdet/datasets/pipelines/__init__.py | 38 + .../datasets/pipelines/load_pipelines.py | 133 + .../datasets/pipelines/torchvision2mmdet.py | 181 + .../mmdet/datasets/task_adapt_dataset.py | 85 + .../adapters/mmdet/datasets/tiling.py | 591 ++ .../adapters/mmdet/evaluation/__init__.py | 10 + .../adapters/mmdet/evaluation/evaluator.py | 403 + .../adapters/mmdet/hooks/__init__.py | 9 + .../hooks/det_class_probability_map_hook.py | 253 + .../mmdet/hooks/tile_sampling_hook.py | 24 + .../adapters/mmdet/models/__init__.py | 19 + .../mmdet/models/assigners/__init__.py | 8 + .../assigners/custom_max_iou_assigner.py | 104 + .../mmdet/models/backbones/__init__.py | 9 + .../mmdet/models/backbones/imgclsmob.py | 162 + .../mmdet/models/backbones/mmov_backbone.py | 29 + .../mmdet/models/dense_heads/__init__.py | 10 + .../mmdet/models/dense_heads/mmov_rpn_head.py | 94 + .../mmdet/models/dense_heads/mmov_ssd_head.py | 147 + .../models/dense_heads/mmov_yolov3_head.py | 84 + .../mmdet/models/detectors/__init__.py | 34 + .../models/detectors/custom_atss_detector.py | 141 + .../custom_deformable_detr_detector.py | 96 + .../models/detectors/custom_dino_detector.py | 170 + .../models/detectors/custom_lite_dino.py | 21 + .../detectors/custom_maskrcnn_detector.py | 144 + .../custom_maskrcnn_tile_optimized.py | 347 + .../detectors/custom_single_stage_detector.py | 205 + .../detectors/custom_two_stage_detector.py | 87 + .../models/detectors/custom_vfnet_detector.py | 79 + .../models/detectors/custom_yolox_detector.py | 192 + .../models/detectors/l2sp_detector_mixin.py | 28 + .../models/detectors/loss_dynamics_mixin.py | 131 + .../mmdet/models/detectors/mean_teacher.py | 341 + .../models/detectors/sam_detector_mixin.py | 15 + .../adapters/mmdet/models/heads/__init__.py | 34 + .../heads/cross_dataset_detector_head.py | 349 + .../models/heads/custom_anchor_generator.py | 62 + .../mmdet/models/heads/custom_atss_head.py | 514 + .../mmdet/models/heads/custom_dino_head.py | 709 ++ .../models/heads/custom_fcn_mask_head.py | 113 + .../mmdet/models/heads/custom_retina_head.py | 68 + .../mmdet/models/heads/custom_roi_head.py | 206 + .../mmdet/models/heads/custom_rpn_head.py | 25 + .../mmdet/models/heads/custom_ssd_head.py | 390 + .../mmdet/models/heads/custom_vfnet_head.py | 372 + .../mmdet/models/heads/custom_yolox_head.py | 304 + .../adapters/mmdet/models/heads/detr_head.py | 266 + .../adapters/mmdet/models/layers/__init__.py | 17 + .../adapters/mmdet/models/layers/dino.py | 191 + .../mmdet/models/layers/dino_layers.py | 616 ++ .../mmdet/models/layers/lite_detr_layers.py | 395 + .../adapters/mmdet/models/loss_dyns.py | 42 + .../adapters/mmdet/models/losses/__init__.py | 9 + .../mmdet/models/losses/cross_focal_loss.py | 153 + .../adapters/mmdet/models/losses/l2sp_loss.py | 96 + .../adapters/mmdet/models/necks/__init__.py | 14 + .../adapters/mmdet/models/necks/mmov_fpn.py | 87 + .../mmdet/models/necks/mmov_ssd_neck.py | 212 + .../mmdet/models/necks/mmov_yolov3_neck.py | 71 + .../adapters/mmdet/models/patch_mmdeploy.py | 73 + .../mmdet/models/roi_heads/__init__.py | 9 + .../models/roi_heads/bbox_heads/__init__.py | 8 + .../roi_heads/bbox_heads/mmov_bbox_head.py | 116 + .../models/roi_heads/mask_heads/__init__.py | 8 + .../roi_heads/mask_heads/mmov_mask_head.py | 71 + .../roi_heads/roi_extractors/__init__.py | 8 + .../single_level_roi_extractor.py | 65 + .../detection/adapters/mmdet/nncf/__init__.py | 12 + .../detection/adapters/mmdet/nncf/builder.py | 241 + .../detection/adapters/mmdet/nncf/patches.py | 167 + .../detection/adapters/mmdet/nncf/task.py | 115 + .../detection/adapters/mmdet/task.py | 720 ++ .../adapters/mmdet/utils/__init__.py | 28 + .../detection/adapters/mmdet/utils/builder.py | 50 + .../adapters/mmdet/utils/config_utils.py | 297 + .../adapters/mmdet/utils/exporter.py | 72 + .../detection/adapters/openvino/__init__.py | 4 + .../openvino/model_wrappers/__init__.py | 15 + .../detection/adapters/openvino/task.py | 760 ++ .../algorithms/detection/configs/__init__.py | 15 + .../detection/configs/base/__init__.py | 19 + .../detection/configs/base/configuration.py | 87 + .../detection/configs/base/data/__init__.py | 4 + .../configs/base/data/atss_data_pipeline.py | 106 + .../configs/base/data/detr_data_pipeline.py | 162 + .../data/iseg_efficientnet_data_pipeline.py | 107 + .../iseg_efficientnet_data_pipeline_xpu.py | 107 + .../base/data/iseg_resnet_data_pipeline.py | 107 + .../data/iseg_resnet_data_pipeline_xpu.py | 107 + .../configs/base/data/semisl/__init__.py | 4 + .../semisl/base_semisl_det_data_pipeline.py | 198 + .../semisl/semisl_is_eff_data_pipeline.py | 200 + .../semisl/semisl_is_res_data_pipeline.py | 191 + .../configs/base/data/tiling/__init__.py | 4 + .../base/data/tiling/atss_tile_pipeline.py | 99 + .../data/tiling/base_iseg_tile_pipeline.py | 95 + .../tiling/efficientnet_iseg_tile_pipeline.py | 95 + .../base/data/tiling/yolox_tile_pipeline.py | 102 + .../configs/base/data/yolox_data_pipeline.py | 120 + .../configs/base/deployments/__init__.py | 5 + .../deployments/base_detection_dynamic.py | 22 + .../base/deployments/base_detection_static.py | 41 + .../base_instance_segmentation_dynamic.py | 28 + .../base_instance_segmentation_static.py | 17 + .../detection/configs/base/models/__init__.py | 15 + .../detection/configs/base/models/detector.py | 23 + .../detection/configs/base/models/model.py | 25 + .../base/models/single_stage_detector.py | 42 + .../configs/base/models/unbiased_teacher.py | 25 + .../detection/configs/detection/__init__.py | 5 + .../configs/detection/configuration.yaml | 682 ++ .../detection/cspdarknet_yolox_l/__init__.py | 4 + .../compression_config.json | 33 + .../cspdarknet_yolox_l/data_pipeline.py | 9 + .../cspdarknet_yolox_l/deployment.py | 14 + .../cspdarknet_yolox_l/hpo_config.yaml | 16 + .../detection/cspdarknet_yolox_l/model.py | 24 + .../detection/cspdarknet_yolox_l/model_xpu.py | 26 + .../cspdarknet_yolox_l/template.yaml | 65 + .../cspdarknet_yolox_l/tile_pipeline.py | 7 + .../detection/cspdarknet_yolox_s/__init__.py | 4 + .../compression_config.json | 33 + .../cspdarknet_yolox_s/data_pipeline.py | 9 + .../cspdarknet_yolox_s/deployment.py | 13 + .../cspdarknet_yolox_s/hpo_config.yaml | 16 + .../detection/cspdarknet_yolox_s/model.py | 24 + .../detection/cspdarknet_yolox_s/model_xpu.py | 26 + .../cspdarknet_yolox_s/template.yaml | 65 + .../cspdarknet_yolox_s/tile_pipeline.py | 7 + .../cspdarknet_yolox_tiny/__init__.py | 15 + .../compression_config.json | 33 + .../cspdarknet_yolox_tiny/data_pipeline.py | 126 + .../cspdarknet_yolox_tiny/deployment.py | 11 + .../cspdarknet_yolox_tiny/hpo_config.yaml | 16 + .../detection/cspdarknet_yolox_tiny/model.py | 35 + .../cspdarknet_yolox_tiny/model_xpu.py | 37 + .../cspdarknet_yolox_tiny/template.yaml | 64 + .../cspdarknet_yolox_tiny/tile_pipeline.py | 118 + .../detection/cspdarknet_yolox_x/__init__.py | 4 + .../compression_config.json | 33 + .../cspdarknet_yolox_x/data_pipeline.py | 9 + .../cspdarknet_yolox_x/deployment.py | 13 + .../cspdarknet_yolox_x/hpo_config.yaml | 16 + .../detection/cspdarknet_yolox_x/model.py | 24 + .../detection/cspdarknet_yolox_x/model_xpy.py | 26 + .../cspdarknet_yolox_x/template.yaml | 65 + .../cspdarknet_yolox_x/tile_pipeline.py | 7 + .../detection/mobilenetv2_atss/__init__.py | 15 + .../mobilenetv2_atss/compression_config.json | 74 + .../mobilenetv2_atss/data_pipeline.py | 7 + .../detection/mobilenetv2_atss/deployment.py | 11 + .../mobilenetv2_atss/hpo_config.yaml | 16 + .../detection/mobilenetv2_atss/model.py | 91 + .../mobilenetv2_atss/semisl/__init__.py | 15 + .../semisl/compression_config.json | 74 + .../mobilenetv2_atss/semisl/data_pipeline.py | 7 + .../mobilenetv2_atss/semisl/hparam.yaml | 6 + .../mobilenetv2_atss/semisl/model.py | 90 + .../detection/mobilenetv2_atss/template.yaml | 65 + .../mobilenetv2_atss/tile_pipeline.py | 7 + .../detection/mobilenetv2_ssd/__init__.py | 15 + .../mobilenetv2_ssd/compression_config.json | 77 + .../mobilenetv2_ssd/data_pipeline.py | 101 + .../detection/mobilenetv2_ssd/deployment.py | 11 + .../detection/mobilenetv2_ssd/hpo_config.yaml | 16 + .../detection/mobilenetv2_ssd/model.py | 99 + .../mobilenetv2_ssd/semisl/__init__.py | 15 + .../semisl/compression_config.json | 77 + .../mobilenetv2_ssd/semisl/data_pipeline.py | 7 + .../mobilenetv2_ssd/semisl/hparam.yaml | 6 + .../detection/mobilenetv2_ssd/semisl/model.py | 104 + .../detection/mobilenetv2_ssd/template.yaml | 64 + .../mobilenetv2_ssd/tile_pipeline.py | 107 + .../resnet50_deformable_detr/__init__.py | 3 + .../resnet50_deformable_detr/data_pipeline.py | 4 + .../resnet50_deformable_detr/deployment.py | 12 + .../resnet50_deformable_detr/model.py | 92 + .../template_experimental.yaml | 66 + .../detection/resnet50_dino/__init__.py | 3 + .../detection/resnet50_dino/data_pipeline.py | 4 + .../detection/resnet50_dino/deployment.py | 12 + .../configs/detection/resnet50_dino/model.py | 100 + .../resnet50_dino/template_experimental.yaml | 66 + .../detection/resnet50_lite_dino/__init__.py | 3 + .../resnet50_lite_dino/data_pipeline.py | 4 + .../resnet50_lite_dino/deployment.py | 12 + .../detection/resnet50_lite_dino/model.py | 120 + .../template_experimental.yaml | 64 + .../detection/resnet50_vfnet/__init__.py | 15 + .../resnet50_vfnet/compression_config.json | 41 + .../detection/resnet50_vfnet/data_pipeline.py | 71 + .../detection/resnet50_vfnet/hpo_config.yaml | 16 + .../configs/detection/resnet50_vfnet/model.py | 74 + .../resnet50_vfnet/template_experimental.yaml | 54 + .../detection/resnext101_atss/__init__.py | 4 + .../resnext101_atss/compression_config.json | 74 + .../resnext101_atss/data_pipeline.py | 7 + .../detection/resnext101_atss/deployment.py | 14 + .../detection/resnext101_atss/hpo_config.yaml | 16 + .../detection/resnext101_atss/model.py | 83 + .../resnext101_atss/semisl/__init__.py | 4 + .../semisl/compression_config.json | 74 + .../resnext101_atss/semisl/data_pipeline.py | 7 + .../resnext101_atss/semisl/hparam.yaml | 6 + .../detection/resnext101_atss/semisl/model.py | 87 + .../detection/resnext101_atss/template.yaml | 65 + .../resnext101_atss/tile_pipeline.py | 7 + .../configs/instance_segmentation/__init__.py | 15 + .../instance_segmentation/configuration.yaml | 701 ++ .../convnext_maskrcnn/__init__.py | 4 + .../convnext_maskrcnn/compression_config.json | 46 + .../convnext_maskrcnn/data_pipeline.py | 80 + .../convnext_maskrcnn/deployment.py | 9 + .../deployment_tile_classifier.py | 22 + .../convnext_maskrcnn/hpo_config.yaml | 16 + .../convnext_maskrcnn/model.py | 131 + .../template_experimental.yaml | 63 + .../convnext_maskrcnn/tile_pipeline.py | 7 + .../efficientnetb2b_maskrcnn/__init__.py | 15 + .../compression_config.json | 46 + .../efficientnetb2b_maskrcnn/data_pipeline.py | 7 + .../data_pipeline_xpu.py | 7 + .../efficientnetb2b_maskrcnn/deployment.py | 9 + .../deployment_tile_classifier.py | 23 + .../efficientnetb2b_maskrcnn/hpo_config.yaml | 16 + .../efficientnetb2b_maskrcnn/model.py | 131 + .../semisl/__init__.py | 4 + .../semisl/compression_config.json | 41 + .../semisl/data_pipeline.py | 6 + .../semisl/hparam.yaml | 13 + .../efficientnetb2b_maskrcnn/semisl/model.py | 127 + .../efficientnetb2b_maskrcnn/template.yaml | 67 + .../efficientnetb2b_maskrcnn/tile_pipeline.py | 7 + .../maskrcnn_swin_t/__init__.py | 4 + .../maskrcnn_swin_t/compression_config.json | 46 + .../maskrcnn_swin_t/data_pipeline.py | 108 + .../maskrcnn_swin_t/data_pipeline_xpu.py | 108 + .../maskrcnn_swin_t/deployment.py | 14 + .../deployment_tile_classifier.py | 22 + .../maskrcnn_swin_t/hpo_config.yaml | 16 + .../maskrcnn_swin_t/model.py | 164 + .../maskrcnn_swin_t/template.yaml | 63 + .../maskrcnn_swin_t/tile_pipeline.py | 7 + .../resnet50_maskrcnn/__init__.py | 15 + .../resnet50_maskrcnn/compression_config.json | 46 + .../resnet50_maskrcnn/data_pipeline.py | 7 + .../resnet50_maskrcnn/data_pipeline_xpu.py | 7 + .../resnet50_maskrcnn/deployment.py | 9 + .../deployment_tile_classifier.py | 23 + .../resnet50_maskrcnn/hpo_config.yaml | 16 + .../resnet50_maskrcnn/model.py | 152 + .../resnet50_maskrcnn/semisl/__init__.py | 4 + .../semisl/compression_config.json | 41 + .../resnet50_maskrcnn/semisl/data_pipeline.py | 7 + .../resnet50_maskrcnn/semisl/hparam.yaml | 13 + .../resnet50_maskrcnn/semisl/model.py | 156 + .../resnet50_maskrcnn/template.yaml | 68 + .../resnet50_maskrcnn/tile_pipeline.py | 7 + .../configs/rotated_detection/__init__.py | 5 + .../rotated_detection/configuration.yaml | 686 ++ .../efficientnetb2b_maskrcnn/__init__.py | 5 + .../compression_config.json | 41 + .../efficientnetb2b_maskrcnn/data_pipeline.py | 108 + .../efficientnetb2b_maskrcnn/deployment.py | 12 + .../efficientnetb2b_maskrcnn/hpo_config.yaml | 16 + .../efficientnetb2b_maskrcnn/model.py | 121 + .../efficientnetb2b_maskrcnn/template.yaml | 65 + .../efficientnetb2b_maskrcnn/tile_pipeline.py | 7 + .../resnet50_maskrcnn/__init__.py | 5 + .../resnet50_maskrcnn/compression_config.json | 41 + .../resnet50_maskrcnn/data_pipeline.py | 106 + .../resnet50_maskrcnn/deployment.py | 12 + .../resnet50_maskrcnn/hpo_config.yaml | 16 + .../resnet50_maskrcnn/model.py | 153 + .../resnet50_maskrcnn/template.yaml | 66 + .../resnet50_maskrcnn/tile_pipeline.py | 7 + src/otx/algorithms/detection/task.py | 607 ++ .../algorithms/detection/tools/__init__.py | 15 + .../detection/tools/detection_sample.py | 358 + .../tools/detection_semisl_sample.py | 288 + .../tools/instance_segmentation_sample.py | 391 + .../algorithms/detection/utils/__init__.py | 34 + src/otx/algorithms/detection/utils/data.py | 494 + src/otx/algorithms/detection/utils/utils.py | 323 + src/otx/algorithms/segmentation/__init__.py | 11 + .../segmentation/adapters/__init__.py | 16 + .../segmentation/adapters/mmseg/__init__.py | 58 + .../adapters/mmseg/apis/__init__.py | 8 + .../segmentation/adapters/mmseg/apis/train.py | 170 + .../segmentation/adapters/mmseg/configurer.py | 199 + .../adapters/mmseg/datasets/__init__.py | 36 + .../adapters/mmseg/datasets/dataset.py | 281 + .../mmseg/datasets/pipelines/__init__.py | 28 + .../mmseg/datasets/pipelines/compose.py | 143 + .../mmseg/datasets/pipelines/loads.py | 98 + .../mmseg/datasets/pipelines/transforms.py | 416 + .../adapters/mmseg/models/__init__.py | 48 + .../mmseg/models/backbones/__init__.py | 22 + .../mmseg/models/backbones/litehrnet.py | 1506 +++ .../mmseg/models/backbones/mmov_backbone.py | 32 + .../adapters/mmseg/models/backbones/mscan.py | 471 + .../adapters/mmseg/models/heads/__init__.py | 22 + .../mmseg/models/heads/custom_otx_head.py | 198 + .../mmseg/models/heads/detcon_head.py | 72 + .../adapters/mmseg/models/heads/light_ham.py | 253 + .../mmseg/models/heads/mmov_decode_head.py | 89 + .../adapters/mmseg/models/heads/proto_head.py | 142 + .../adapters/mmseg/models/losses/__init__.py | 21 + .../losses/cross_entropy_loss_with_ignore.py | 99 + .../mmseg/models/losses/detcon_loss.py | 184 + .../models/losses/pixel_prototype_ce_loss.py | 101 + .../adapters/mmseg/models/necks/__init__.py | 19 + .../adapters/mmseg/models/necks/selfsl_mlp.py | 105 + .../mmseg/models/schedulers/__init__.py | 25 + .../adapters/mmseg/models/schedulers/base.py | 18 + .../mmseg/models/schedulers/constant.py | 28 + .../adapters/mmseg/models/schedulers/poly.py | 66 + .../adapters/mmseg/models/schedulers/step.py | 57 + .../mmseg/models/segmentors/__init__.py | 20 + .../mmseg/models/segmentors/detcon.py | 559 + .../segmentors/mean_teacher_segmentor.py | 246 + .../models/segmentors/otx_encoder_decoder.py | 118 + .../adapters/mmseg/models/utils/__init__.py | 33 + .../adapters/mmseg/models/utils/aggregator.py | 227 + .../mmseg/models/utils/angular_pw_conv.py | 36 + .../utils/asymmetric_position_attention.py | 102 + .../mmseg/models/utils/channel_shuffle.py | 35 + .../mmseg/models/utils/local_attention.py | 76 + .../mmseg/models/utils/loss_equalizer.py | 61 + .../adapters/mmseg/models/utils/normalize.py | 45 + .../mmseg/models/utils/proto_utils.py | 122 + .../adapters/mmseg/models/utils/psp_layer.py | 36 + .../adapters/mmseg/nncf/__init__.py | 21 + .../adapters/mmseg/nncf/builder.py | 183 + .../adapters/mmseg/nncf/patches.py | 12 + .../segmentation/adapters/mmseg/nncf/task.py | 116 + .../segmentation/adapters/mmseg/task.py | 565 + .../adapters/mmseg/utils/__init__.py | 27 + .../adapters/mmseg/utils/builder.py | 70 + .../adapters/mmseg/utils/data_utils.py | 242 + .../adapters/mmseg/utils/exporter.py | 73 + .../adapters/openvino/__init__.py | 14 + .../openvino/model_wrappers/__init__.py | 15 + .../segmentation/adapters/openvino/task.py | 392 + .../segmentation/configs/__init__.py | 4 + .../segmentation/configs/base/__init__.py | 19 + .../configs/base/configuration.py | 154 + .../configs/base/data/__init__.py | 14 + .../configs/base/data/data_pipeline.py | 91 + .../configs/base/data/selfsl/__init__.py | 14 + .../configs/base/data/selfsl/data_pipeline.py | 79 + .../configs/base/data/semisl/__init__.py | 4 + .../configs/base/data/semisl/data_pipeline.py | 128 + .../configs/base/data/supcon/__init__.py | 14 + .../configs/base/data/supcon/data_pipeline.py | 130 + .../configs/base/deployments/__init__.py | 4 + .../deployments/base_segmentation_dynamic.py | 18 + .../deployments/base_segmentation_static.py | 31 + .../segmentation/configs/configuration.yaml | 473 + .../configs/ham_segnext_b/__init__.py | 15 + .../ham_segnext_b/compression_config.json | 50 + .../configs/ham_segnext_b/data_pipeline.py | 18 + .../configs/ham_segnext_b/deployment.py | 11 + .../configs/ham_segnext_b/hpo_config.yaml | 15 + .../configs/ham_segnext_b/model.py | 53 + .../ham_segnext_b/ptq_optimization_config.py | 12 + .../configs/ham_segnext_b/selfsl/__init__.py | 4 + .../ham_segnext_b/selfsl/data_pipeline.py | 7 + .../configs/ham_segnext_b/selfsl/hparam.yaml | 17 + .../configs/ham_segnext_b/selfsl/model.py | 53 + .../configs/ham_segnext_b/semisl/__init__.py | 15 + .../ham_segnext_b/semisl/data_pipeline.py | 7 + .../configs/ham_segnext_b/semisl/hparam.yaml | 6 + .../configs/ham_segnext_b/semisl/model.py | 70 + .../configs/ham_segnext_b/template.yaml | 65 + .../configs/ham_segnext_s/__init__.py | 15 + .../ham_segnext_s/compression_config.json | 50 + .../configs/ham_segnext_s/data_pipeline.py | 18 + .../configs/ham_segnext_s/deployment.py | 11 + .../configs/ham_segnext_s/hpo_config.yaml | 15 + .../configs/ham_segnext_s/model.py | 52 + .../ham_segnext_s/ptq_optimization_config.py | 12 + .../configs/ham_segnext_s/selfsl/__init__.py | 4 + .../ham_segnext_s/selfsl/data_pipeline.py | 7 + .../configs/ham_segnext_s/selfsl/hparam.yaml | 17 + .../configs/ham_segnext_s/selfsl/model.py | 52 + .../configs/ham_segnext_s/semisl/__init__.py | 15 + .../ham_segnext_s/semisl/data_pipeline.py | 7 + .../configs/ham_segnext_s/semisl/hparam.yaml | 6 + .../configs/ham_segnext_s/semisl/model.py | 69 + .../configs/ham_segnext_s/template.yaml | 65 + .../configs/ham_segnext_t/__init__.py | 15 + .../ham_segnext_t/compression_config.json | 50 + .../configs/ham_segnext_t/data_pipeline.py | 18 + .../configs/ham_segnext_t/deployment.py | 11 + .../configs/ham_segnext_t/hpo_config.yaml | 15 + .../configs/ham_segnext_t/model.py | 44 + .../ham_segnext_t/ptq_optimization_config.py | 12 + .../configs/ham_segnext_t/selfsl/__init__.py | 4 + .../ham_segnext_t/selfsl/data_pipeline.py | 7 + .../configs/ham_segnext_t/selfsl/hparam.yaml | 17 + .../configs/ham_segnext_t/selfsl/model.py | 47 + .../configs/ham_segnext_t/semisl/__init__.py | 15 + .../ham_segnext_t/semisl/data_pipeline.py | 7 + .../configs/ham_segnext_t/semisl/hparam.yaml | 6 + .../configs/ham_segnext_t/semisl/model.py | 64 + .../configs/ham_segnext_t/template.yaml | 65 + .../configs/ocr_lite_hrnet_18/__init__.py | 15 + .../ocr_lite_hrnet_18/compression_config.json | 53 + .../ocr_lite_hrnet_18/data_pipeline.py | 7 + .../configs/ocr_lite_hrnet_18/deployment.py | 11 + .../configs/ocr_lite_hrnet_18/hpo_config.yaml | 15 + .../configs/ocr_lite_hrnet_18/model.py | 54 + .../ptq_optimization_config.py | 23 + .../ocr_lite_hrnet_18/selfsl/__init__.py | 15 + .../ocr_lite_hrnet_18/selfsl/data_pipeline.py | 7 + .../ocr_lite_hrnet_18/selfsl/hparam.yaml | 17 + .../configs/ocr_lite_hrnet_18/selfsl/model.py | 64 + .../ocr_lite_hrnet_18/semisl/__init__.py | 15 + .../ocr_lite_hrnet_18/semisl/data_pipeline.py | 7 + .../ocr_lite_hrnet_18/semisl/hparam.yaml | 6 + .../configs/ocr_lite_hrnet_18/semisl/model.py | 59 + .../ocr_lite_hrnet_18/supcon/__init__.py | 15 + .../ocr_lite_hrnet_18/supcon/data_pipeline.py | 7 + .../ocr_lite_hrnet_18/supcon/hparam.yaml | 8 + .../configs/ocr_lite_hrnet_18/supcon/model.py | 85 + .../configs/ocr_lite_hrnet_18/template.yaml | 49 + .../ocr_lite_hrnet_18_mod2/__init__.py | 15 + .../compression_config.json | 53 + .../ocr_lite_hrnet_18_mod2/data_pipeline.py | 7 + .../ocr_lite_hrnet_18_mod2/deployment.py | 11 + .../ocr_lite_hrnet_18_mod2/hpo_config.yaml | 15 + .../configs/ocr_lite_hrnet_18_mod2/model.py | 52 + .../ptq_optimization_config.py | 107 + .../ocr_lite_hrnet_18_mod2/selfsl/__init__.py | 15 + .../selfsl/data_pipeline.py | 7 + .../ocr_lite_hrnet_18_mod2/selfsl/hparam.yaml | 17 + .../ocr_lite_hrnet_18_mod2/selfsl/model.py | 64 + .../ocr_lite_hrnet_18_mod2/semisl/__init__.py | 15 + .../semisl/data_pipeline.py | 7 + .../ocr_lite_hrnet_18_mod2/semisl/hparam.yaml | 6 + .../ocr_lite_hrnet_18_mod2/semisl/model.py | 59 + .../ocr_lite_hrnet_18_mod2/supcon/__init__.py | 15 + .../supcon/data_pipeline.py | 7 + .../ocr_lite_hrnet_18_mod2/supcon/hparam.yaml | 8 + .../ocr_lite_hrnet_18_mod2/supcon/model.py | 84 + .../ocr_lite_hrnet_18_mod2/template.yaml | 62 + .../configs/ocr_lite_hrnet_s_mod2/__init__.py | 15 + .../compression_config.json | 53 + .../ocr_lite_hrnet_s_mod2/data_pipeline.py | 7 + .../ocr_lite_hrnet_s_mod2/deployment.py | 11 + .../ocr_lite_hrnet_s_mod2/hpo_config.yaml | 15 + .../configs/ocr_lite_hrnet_s_mod2/model.py | 54 + .../ptq_optimization_config.py | 86 + .../ocr_lite_hrnet_s_mod2/selfsl/__init__.py | 15 + .../selfsl/data_pipeline.py | 7 + .../ocr_lite_hrnet_s_mod2/selfsl/hparam.yaml | 17 + .../ocr_lite_hrnet_s_mod2/selfsl/model.py | 62 + .../ocr_lite_hrnet_s_mod2/semisl/__init__.py | 15 + .../semisl/data_pipeline.py | 7 + .../ocr_lite_hrnet_s_mod2/semisl/hparam.yaml | 6 + .../ocr_lite_hrnet_s_mod2/semisl/model.py | 65 + .../ocr_lite_hrnet_s_mod2/supcon/__init__.py | 15 + .../supcon/data_pipeline.py | 7 + .../ocr_lite_hrnet_s_mod2/supcon/hparam.yaml | 8 + .../ocr_lite_hrnet_s_mod2/supcon/model.py | 90 + .../ocr_lite_hrnet_s_mod2/template.yaml | 62 + .../configs/ocr_lite_hrnet_x_mod3/__init__.py | 15 + .../compression_config.json | 53 + .../ocr_lite_hrnet_x_mod3/data_pipeline.py | 7 + .../ocr_lite_hrnet_x_mod3/deployment.py | 12 + .../ocr_lite_hrnet_x_mod3/hpo_config.yaml | 15 + .../configs/ocr_lite_hrnet_x_mod3/model.py | 55 + .../ptq_optimization_config.py | 186 + .../ocr_lite_hrnet_x_mod3/selfsl/__init__.py | 15 + .../selfsl/data_pipeline.py | 7 + .../ocr_lite_hrnet_x_mod3/selfsl/hparam.yaml | 17 + .../ocr_lite_hrnet_x_mod3/selfsl/model.py | 62 + .../ocr_lite_hrnet_x_mod3/semisl/__init__.py | 15 + .../semisl/data_pipeline.py | 7 + .../ocr_lite_hrnet_x_mod3/semisl/hparam.yaml | 6 + .../ocr_lite_hrnet_x_mod3/semisl/model.py | 64 + .../ocr_lite_hrnet_x_mod3/supcon/__init__.py | 15 + .../supcon/data_pipeline.py | 7 + .../ocr_lite_hrnet_x_mod3/supcon/hparam.yaml | 8 + .../ocr_lite_hrnet_x_mod3/supcon/model.py | 87 + .../ocr_lite_hrnet_x_mod3/template.yaml | 62 + src/otx/algorithms/segmentation/task.py | 385 + .../algorithms/segmentation/tools/__init__.py | 15 + .../segmentation/tools/segmentation_sample.py | 407 + .../algorithms/segmentation/utils/__init__.py | 10 + .../algorithms/segmentation/utils/metadata.py | 28 + .../segmentation/utils/processing.py | 26 + .../algorithms/visual_prompting/__init__.py | 4 + .../visual_prompting/adapters/__init__.py | 4 + .../adapters/openvino/__init__.py | 17 + .../openvino/model_wrappers/__init__.py | 17 + .../model_wrappers/openvino_models.py | 155 + .../adapters/pytorch_lightning/__init__.py | 15 + .../pytorch_lightning/callbacks/__init__.py | 17 + .../pytorch_lightning/callbacks/inference.py | 135 + .../pytorch_lightning/config/__init__.py | 20 + .../config/visual_prompting_config.py | 123 + .../pytorch_lightning/datasets/__init__.py | 11 + .../pytorch_lightning/datasets/dataset.py | 472 + .../datasets/pipelines/__init__.py | 7 + .../datasets/pipelines/sam_transforms.py | 126 + .../datasets/pipelines/transforms.py | 111 + .../pytorch_lightning/models/__init__.py | 6 + .../models/backbones/__init__.py | 7 + .../models/backbones/tiny_vit.py | 744 ++ .../pytorch_lightning/models/backbones/vit.py | 472 + .../models/decoders/__init__.py | 6 + .../models}/decoders/sam_mask_decoder.py | 61 +- .../models/encoders/__init__.py | 7 + .../models/encoders/sam_image_encoder.py | 32 + .../models/encoders/sam_prompt_encoder.py | 271 + .../models/utils/__init__.py | 7 + .../models/utils/layer_norm.py | 42 + .../models}/utils/mlp_block.py | 12 +- .../models/visual_prompters/__init__.py | 7 + .../visual_prompters/segment_anything.py | 600 ++ .../zero_shot_segment_anything.py | 772 ++ .../visual_prompting/configs/__init__.py | 4 + .../visual_prompting/configs/base/__init__.py | 18 + .../configs/base/configuration.py | 143 + .../configs/base/configuration_enums.py | 25 + .../configs/configuration.yaml | 235 + .../configs/sam_tiny_vit/__init__.py | 6 + .../configs/sam_tiny_vit/config.yaml | 94 + .../configs/sam_tiny_vit/configuration.py | 14 + .../sam_tiny_vit/ptq_optimization_config.py | 22 + .../configs/sam_tiny_vit/template.yaml | 30 + .../configs/sam_vit_b/__init__.py | 6 + .../configs/sam_vit_b/config.yaml | 92 + .../configs/sam_vit_b/configuration.py | 14 + .../sam_vit_b/ptq_optimization_config.py | 22 + .../configs/sam_vit_b/template.yaml | 30 + .../zero_shot_sam_tiny_vit/__init__.py | 6 + .../zero_shot_sam_tiny_vit/config.yaml | 81 + .../zero_shot_sam_tiny_vit/configuration.py | 14 + .../zero_shot_sam_tiny_vit/configuration.yaml | 210 + .../ptq_optimization_config.py | 22 + .../zero_shot_sam_tiny_vit/template.yaml | 38 + .../configs/zero_shot_sam_vit_b/__init__.py | 6 + .../configs/zero_shot_sam_vit_b/config.yaml | 81 + .../zero_shot_sam_vit_b/configuration.py | 14 + .../zero_shot_sam_vit_b/configuration.yaml | 210 + .../ptq_optimization_config.py | 22 + .../configs/zero_shot_sam_vit_b/template.yaml | 38 + .../visual_prompting/tasks/__init__.py | 8 + .../visual_prompting/tasks/inference.py | 564 + .../visual_prompting/tasks/openvino.py | 1147 ++ .../visual_prompting/tasks/train.py | 110 + .../visual_prompting/utils/__init__.py | 19 + .../utils/visual_prompting_utils.py | 22 + src/otx/api/__init__.py | 4 + src/otx/api/configuration/__init__.py | 36 + .../configuration/configurable_parameters.py | 32 + .../configuration/default_model_parameters.py | 76 + .../api/configuration/elements/__init__.py | 30 + .../elements/configurable_enum.py | 60 + .../configuration/elements/metadata_keys.py | 90 + .../configuration/elements/parameter_group.py | 187 + .../elements/primitive_parameters.py | 534 + src/otx/api/configuration/elements/utils.py | 283 + src/otx/api/configuration/enums/__init__.py | 11 + .../api/configuration/enums/auto_hpo_state.py | 33 + .../enums/config_element_type.py | 69 + .../configuration/enums/model_lifecycle.py | 44 + src/otx/api/configuration/enums/utils.py | 20 + src/otx/api/configuration/helper/__init__.py | 26 + .../helper/config_element_mapping.py | 72 + src/otx/api/configuration/helper/convert.py | 139 + src/otx/api/configuration/helper/create.py | 380 + .../api/configuration/helper/substitute.py | 196 + src/otx/api/configuration/helper/utils.py | 249 + src/otx/api/configuration/helper/validate.py | 38 + src/otx/api/configuration/model_lifecycle.py | 14 + .../api/configuration/ui_rules/__init__.py | 17 + src/otx/api/configuration/ui_rules/rules.py | 103 + src/otx/api/configuration/ui_rules/types.py | 39 + src/otx/api/configuration/ui_rules/utils.py | 29 + src/otx/api/entities/__init__.py | 4 + src/otx/api/entities/annotation.py | 326 + src/otx/api/entities/color.py | 169 + src/otx/api/entities/coordinate.py | 42 + src/otx/api/entities/dataset_item.py | 550 + src/otx/api/entities/datasets.py | 449 + src/otx/api/entities/explain_parameters.py | 33 + src/otx/api/entities/graph.py | 146 + src/otx/api/entities/id.py | 56 + src/otx/api/entities/image.py | 154 + src/otx/api/entities/inference_parameters.py | 43 + src/otx/api/entities/interfaces/__init__.py | 4 + .../entities/interfaces/graph_interface.py | 75 + src/otx/api/entities/label.py | 215 + src/otx/api/entities/label_schema.py | 791 ++ src/otx/api/entities/media.py | 57 + src/otx/api/entities/metadata.py | 115 + src/otx/api/entities/metrics.py | 747 ++ src/otx/api/entities/model.py | 462 + src/otx/api/entities/model_template.py | 691 ++ .../api/entities/optimization_parameters.py | 34 + src/otx/api/entities/result_media.py | 121 + src/otx/api/entities/resultset.py | 185 + src/otx/api/entities/scored_label.py | 130 + src/otx/api/entities/shapes/__init__.py | 10 + src/otx/api/entities/shapes/ellipse.py | 283 + src/otx/api/entities/shapes/polygon.py | 228 + src/otx/api/entities/shapes/rectangle.py | 330 + src/otx/api/entities/shapes/shape.py | 189 + src/otx/api/entities/subset.py | 27 + src/otx/api/entities/task_environment.py | 133 + src/otx/api/entities/tensor.py | 53 + src/otx/api/entities/train_parameters.py | 61 + src/otx/api/py.typed | 3 + src/otx/api/serialization/__init__.py | 6 + src/otx/api/serialization/datetime_mapper.py | 32 + src/otx/api/serialization/id_mapper.py | 24 + src/otx/api/serialization/label_mapper.py | 195 + src/otx/api/usecases/__init__.py | 4 + src/otx/api/usecases/adapters/__init__.py | 7 + .../api/usecases/adapters/model_adapter.py | 56 + src/otx/api/usecases/evaluation/__init__.py | 31 + src/otx/api/usecases/evaluation/accuracy.py | 335 + .../usecases/evaluation/anomaly_metrics.py | 123 + src/otx/api/usecases/evaluation/averaging.py | 15 + .../usecases/evaluation/basic_operations.py | 153 + src/otx/api/usecases/evaluation/dice.py | 226 + src/otx/api/usecases/evaluation/f_measure.py | 871 ++ .../api/usecases/evaluation/metrics_helper.py | 110 + .../performance_provider_interface.py | 22 + .../api/usecases/exportable_code/__init__.py | 4 + .../api/usecases/exportable_code/demo/LICENSE | 201 + .../usecases/exportable_code/demo/README.md | 151 + .../usecases/exportable_code/demo/__init__.py | 4 + .../api/usecases/exportable_code/demo/demo.py | 133 + .../demo/demo_package/__init__.py | 18 + .../demo/demo_package/executors/__init__.py | 15 + .../demo_package/executors/asynchronous.py | 88 + .../demo_package/executors/sync_pipeline.py | 97 + .../demo_package/executors/synchronous.py | 52 + .../demo/demo_package/model_container.py | 164 + .../demo/demo_package/utils.py | 55 + .../exportable_code/demo/requirements.txt | 4 + .../usecases/exportable_code/demo/setup.py | 31 + .../exportable_code/inference/__init__.py | 13 + .../exportable_code/inference/inference.py | 33 + .../prediction_to_annotation_converter.py | 621 ++ .../exportable_code/streamer/__init__.py | 28 + .../exportable_code}/streamer/streamer.py | 122 +- .../exportable_code/visualizers/__init__.py | 9 + .../visualizers/anomaly_visualizer.py | 74 + .../exportable_code/visualizers/visualizer.py | 142 + src/otx/api/usecases/reporting/__init__.py | 9 + src/otx/api/usecases/reporting/callback.py | 79 + .../reporting/time_monitor_callback.py | 175 + src/otx/api/usecases/tasks/__init__.py | 4 + src/otx/api/usecases/tasks/exceptions.py | 28 + .../usecases/tasks/image_computer_vision.py | 23 + .../tasks/image_deep_learning_task.py | 25 + .../api/usecases/tasks/interfaces/__init__.py | 4 + .../tasks/interfaces/deployment_interface.py | 22 + .../tasks/interfaces/evaluate_interface.py | 31 + .../tasks/interfaces/explain_interface.py | 32 + .../tasks/interfaces/export_interface.py | 39 + .../tasks/interfaces/inference_interface.py | 61 + .../interfaces/optimization_interface.py | 44 + .../tasks/interfaces/training_interface.py | 61 + .../tasks/interfaces/unload_interface.py | 30 + src/otx/api/utils/__init__.py | 5 + src/otx/api/utils/anomaly_utils.py | 83 + src/otx/api/utils/argument_checks.py | 461 + src/otx/api/utils/async_pipeline.py | 49 + src/otx/api/utils/dataset_utils.py | 274 + src/otx/api/utils/detection_utils.py | 45 + src/otx/api/utils/importing.py | 28 + src/otx/api/utils/labels_utils.py | 20 + src/otx/api/utils/nms.py | 76 + src/otx/api/utils/segmentation_utils.py | 285 + src/otx/api/utils/shape_drawer.py | 690 ++ src/otx/api/utils/shape_factory.py | 195 + src/otx/api/utils/tiler.py | 419 + src/otx/api/utils/time_utils.py | 149 + src/otx/api/utils/vis_utils.py | 77 + src/otx/cli/__init__.py | 30 +- src/otx/cli/builder/__init__.py | 21 + src/otx/cli/builder/builder.py | 241 + .../builder/supported_backbone/__init__.py | 4 + .../cli/builder/supported_backbone/mmcls.json | 347 + .../cli/builder/supported_backbone/mmdet.json | 227 + .../cli/builder/supported_backbone/mmseg.json | 202 + .../builder/supported_backbone/omz.mmcls.json | 55 + .../cli/builder/supported_backbone/otx.json | 26 + .../supported_backbone/torchvision.json | 645 ++ src/otx/cli/cli.py | 580 - src/otx/cli/install.py | 142 - src/otx/cli/manager/__init__.py | 9 + src/otx/cli/manager/config_manager.py | 727 ++ src/otx/cli/patches/NOTICE | 1 - src/otx/cli/patches/mmaction2.patch | 1132 -- src/otx/cli/registry/__init__.py | 22 + src/otx/cli/registry/registry.py | 135 + src/otx/cli/tools/__init__.py | 15 + src/otx/cli/tools/build.py | 140 + src/otx/cli/tools/cli.py | 94 + src/otx/cli/tools/demo.py | 192 + src/otx/cli/tools/deploy.py | 96 + src/otx/cli/tools/eval.py | 171 + src/otx/cli/tools/explain.py | 213 + src/otx/cli/tools/export.py | 138 + src/otx/cli/tools/find.py | 139 + src/otx/cli/tools/optimize.py | 180 + src/otx/cli/tools/train.py | 345 + src/otx/cli/tools/utils/__init__.py | 15 + src/otx/cli/tools/utils/demo/__init__.py | 15 + .../cli/tools/utils/demo/images_capture.py | 217 + src/otx/cli/tools/utils/demo/visualization.py | 163 + src/otx/cli/utils/__init__.py | 24 +- src/otx/cli/utils/config.py | 66 + src/otx/cli/utils/errors.py | 30 + src/otx/cli/utils/experiment.py | 251 + src/otx/cli/utils/help_formatter.py | 247 - src/otx/cli/utils/hpo.py | 973 ++ src/otx/cli/utils/importing.py | 116 + src/otx/cli/utils/installation.py | 551 - src/otx/cli/utils/io.py | 281 + src/otx/cli/utils/jsonargparse.py | 422 - src/otx/cli/utils/multi_gpu.py | 360 + src/otx/cli/utils/nncf.py | 47 + src/otx/cli/utils/parser.py | 266 + src/otx/cli/utils/report.py | 152 + src/otx/cli/utils/telemetry.py | 88 + src/otx/cli/utils/workspace.py | 31 - src/otx/config/data/openvino.yaml | 21 - src/otx/core/__init__.py | 17 +- src/otx/core/config/__init__.py | 142 - src/otx/core/config/data.py | 127 - src/otx/core/config/device.py | 18 - src/otx/core/config/explain.py | 17 - src/otx/core/config/hpo.py | 32 - src/otx/core/data/__init__.py | 23 +- src/otx/core/data/adapter/__init__.py | 160 + .../data/adapter/action_dataset_adapter.py | 198 + .../data/adapter/anomaly_dataset_adapter.py | 237 + .../core/data/adapter/base_dataset_adapter.py | 439 + .../adapter/classification_dataset_adapter.py | 157 + .../data/adapter/detection_dataset_adapter.py | 70 + .../adapter/segmentation_dataset_adapter.py | 241 + .../visual_prompting_dataset_adapter.py | 89 + src/otx/core/data/caching/__init__.py | 9 + .../core/data/caching/mem_cache_handler.py | 223 + src/otx/core/data/caching/storage_cache.py | 117 + src/otx/core/data/dataset/__init__.py | 4 - .../data/dataset/action_classification.py | 44 - src/otx/core/data/dataset/action_detection.py | 103 - src/otx/core/data/dataset/anomaly/__init__.py | 8 - src/otx/core/data/dataset/anomaly/dataset.py | 150 - src/otx/core/data/dataset/base.py | 264 - src/otx/core/data/dataset/classification.py | 408 - src/otx/core/data/dataset/detection.py | 61 - .../data/dataset/instance_segmentation.py | 88 - src/otx/core/data/dataset/segmentation.py | 108 - src/otx/core/data/dataset/tile.py | 442 - src/otx/core/data/dataset/visual_prompting.py | 256 - src/otx/core/data/entity/__init__.py | 4 - .../core/data/entity/action_classification.py | 107 - src/otx/core/data/entity/action_detection.py | 100 - src/otx/core/data/entity/anomaly/__init__.py | 42 - .../data/entity/anomaly/classification.py | 80 - src/otx/core/data/entity/anomaly/detection.py | 89 - .../core/data/entity/anomaly/segmentation.py | 83 - src/otx/core/data/entity/base.py | 676 -- src/otx/core/data/entity/classification.py | 235 - src/otx/core/data/entity/detection.py | 114 - .../core/data/entity/instance_segmentation.py | 118 - src/otx/core/data/entity/segmentation.py | 93 - src/otx/core/data/entity/tile.py | 251 - src/otx/core/data/entity/utils.py | 73 - src/otx/core/data/entity/visual_prompting.py | 207 - src/otx/core/data/factory.py | 152 - src/otx/core/data/manager/__init__.py | 16 + src/otx/core/data/manager/dataset_manager.py | 151 + src/otx/core/data/mem_cache.py | 334 - src/otx/core/data/module.py | 239 - .../data/noisy_label_detection/__init__.py | 8 + .../core/data/noisy_label_detection/base.py | 72 + src/otx/core/data/pipelines/__init__.py | 3 + src/otx/core/data/pre_filtering.py | 86 - src/otx/core/data/tile_adaptor.py | 183 - src/otx/core/data/transform_libs/__init__.py | 4 - src/otx/core/data/transform_libs/mmaction.py | 249 - src/otx/core/data/transform_libs/mmcv.py | 76 - src/otx/core/data/transform_libs/mmdet.py | 300 - .../core/data/transform_libs/mmpretrain.py | 127 - src/otx/core/data/transform_libs/mmseg.py | 98 - .../core/data/transform_libs/torchvision.py | 329 - src/otx/core/exporter/__init__.py | 4 - src/otx/core/exporter/base.py | 275 - .../core/exporter/exportable_code/__init__.py | 4 - .../exporter/exportable_code/demo/README.md | 164 - .../exporter/exportable_code/demo/__init__.py | 4 - .../exporter/exportable_code/demo/demo.py | 107 - .../demo/demo_package/__init__.py | 15 - .../demo/demo_package/executors/__init__.py | 12 - .../demo_package/executors/asynchronous.py | 78 - .../demo_package/executors/synchronous.py | 49 - .../demo/demo_package/model_wrapper.py | 131 - .../demo/demo_package/streamer/__init__.py | 24 - .../demo/demo_package/utils.py | 61 - .../demo/demo_package/visualizers/__init__.py | 22 - .../demo_package/visualizers/vis_utils.py | 190 - .../demo_package/visualizers/visualizer.py | 398 - .../exportable_code/demo/requirements.txt | 3 - .../exporter/exportable_code/demo/setup.py | 30 - src/otx/core/exporter/mmdeploy.py | 265 - src/otx/core/exporter/native.py | 117 - src/otx/core/exporter/visual_prompting.py | 177 - src/otx/core/file.py | 14 + src/otx/core/metrics/__init__.py | 10 - src/otx/core/metrics/accuracy.py | 328 - src/otx/core/metrics/fmeasure.py | 755 -- src/otx/core/model/__init__.py | 4 - src/otx/core/model/entity/__init__.py | 4 - .../model/entity/action_classification.py | 204 - src/otx/core/model/entity/action_detection.py | 132 - src/otx/core/model/entity/base.py | 525 - src/otx/core/model/entity/classification.py | 884 -- src/otx/core/model/entity/detection.py | 523 - .../model/entity/instance_segmentation.py | 546 - .../core/model/entity/rotated_detection.py | 27 - src/otx/core/model/entity/segmentation.py | 223 - src/otx/core/model/entity/utils/__init__.py | 4 - src/otx/core/model/entity/utils/mmaction.py | 55 - src/otx/core/model/entity/utils/mmdet.py | 53 - src/otx/core/model/entity/utils/mmpretrain.py | 50 - src/otx/core/model/entity/utils/mmseg.py | 232 - src/otx/core/model/entity/visual_prompting.py | 993 -- src/otx/core/model/module/__init__.py | 4 - .../model/module/action_classification.py | 98 - src/otx/core/model/module/action_detection.py | 129 - src/otx/core/model/module/anomaly/__init__.py | 8 - .../model/module/anomaly/anomaly_lightning.py | 473 - src/otx/core/model/module/base.py | 274 - src/otx/core/model/module/classification.py | 356 - src/otx/core/model/module/detection.py | 152 - .../model/module/instance_segmentation.py | 204 - .../core/model/module/rotated_detection.py | 122 - src/otx/core/model/module/segmentation.py | 136 - src/otx/core/model/module/visual_prompting.py | 393 - src/otx/core/ov/__init__.py | 9 + src/otx/core/ov/graph/__init__.py | 9 + src/otx/core/ov/graph/graph.py | 645 ++ src/otx/core/ov/graph/parsers/__init__.py | 8 + src/otx/core/ov/graph/parsers/builder.py | 8 + src/otx/core/ov/graph/parsers/cls/__init__.py | 8 + .../ov/graph/parsers/cls/cls_base_parser.py | 105 + src/otx/core/ov/graph/parsers/parser.py | 25 + src/otx/core/ov/graph/utils.py | 290 + src/otx/core/ov/models/__init__.py | 14 + src/otx/core/ov/models/mmov_model.py | 68 + src/otx/core/ov/models/ov_model.py | 476 + src/otx/core/ov/models/parser_mixin.py | 62 + src/otx/core/ov/omz_wrapper.py | 400 + src/otx/core/ov/ops/__init__.py | 140 + src/otx/core/ov/ops/activations.py | 356 + src/otx/core/ov/ops/arithmetics.py | 147 + src/otx/core/ov/ops/builder.py | 57 + src/otx/core/ov/ops/convolutions.py | 129 + src/otx/core/ov/ops/generation.py | 40 + src/otx/core/ov/ops/image_processings.py | 142 + src/otx/core/ov/ops/infrastructures.py | 263 + src/otx/core/ov/ops/matmuls.py | 56 + src/otx/core/ov/ops/modules/__init__.py | 8 + src/otx/core/ov/ops/modules/op_module.py | 104 + src/otx/core/ov/ops/movements.py | 517 + src/otx/core/ov/ops/normalizations.py | 207 + src/otx/core/ov/ops/object_detections.py | 212 + src/otx/core/ov/ops/op.py | 100 + src/otx/core/ov/ops/poolings.py | 167 + src/otx/core/ov/ops/reductions.py | 132 + src/otx/core/ov/ops/shape_manipulations.py | 163 + src/otx/core/ov/ops/sorting_maximization.py | 110 + src/otx/core/ov/ops/type_conversions.py | 73 + src/otx/core/ov/ops/utils.py | 33 + src/otx/core/ov/registry.py | 56 + src/otx/core/ov/utils.py | 127 + src/otx/core/patcher.py | 211 + src/otx/core/types/__init__.py | 12 - src/otx/core/types/device.py | 21 - src/otx/core/types/explain.py | 22 - src/otx/core/types/export.py | 16 - src/otx/core/types/image.py | 45 - src/otx/core/types/precision.py | 13 - src/otx/core/types/task.py | 39 - src/otx/core/types/transformer_libs.py | 19 - src/otx/core/utils/__init__.py | 4 - src/otx/core/utils/build.py | 105 - src/otx/core/utils/cache.py | 89 - src/otx/core/utils/config.py | 96 - src/otx/core/utils/imports.py | 25 - src/otx/core/utils/instantiators.py | 120 - src/otx/core/utils/mask_util.py | 91 - src/otx/core/utils/pylogger.py | 48 - src/otx/core/utils/tile_merge.py | 294 - src/otx/core/utils/utils.py | 77 - src/otx/data/__init__.py | 8 - src/otx/data/anomaly/__init__.py | 9 - src/otx/data/anomaly/anomaly.py | 86 - src/otx/engine/__init__.py | 8 - src/otx/engine/engine.py | 841 -- src/otx/engine/hpo/__init__.py | 9 - src/otx/engine/hpo/hpo_api.py | 259 - src/otx/engine/hpo/hpo_trial.py | 140 - src/otx/engine/hpo/utils.py | 80 - src/otx/engine/utils/__init__.py | 4 - src/otx/engine/utils/api.py | 76 - src/otx/engine/utils/auto_configurator.py | 351 - src/otx/hpo/__init__.py | 17 +- src/otx/hpo/hpo_base.py | 191 +- src/otx/hpo/hpo_runner.py | 172 +- src/otx/hpo/hyperband.py | 351 +- src/otx/hpo/resource_manager.py | 205 +- src/otx/hpo/search_space.py | 274 +- src/otx/hpo/utils.py | 46 +- src/otx/recipe/__init__.py | 7 - src/otx/recipe/_base_/data/mmaction_base.yaml | 105 - src/otx/recipe/_base_/data/mmdet_base.yaml | 76 - .../recipe/_base_/data/mmpretrain_base.yaml | 52 - src/otx/recipe/_base_/data/mmseg_base.yaml | 74 - .../recipe/_base_/data/torchvision_base.yaml | 35 - src/otx/recipe/_base_/test.yaml | 26 - src/otx/recipe/_base_/train.yaml | 58 - .../action/action_classification/movinet.yaml | 32 - .../action_classification/openvino_model.yaml | 39 - .../action/action_classification/x3d.yaml | 32 - .../action/action_detection/x3d_fastrcnn.yaml | 102 - .../recipe/anomaly_classification/padim.yaml | 81 - .../recipe/anomaly_classification/stfpm.yaml | 86 - src/otx/recipe/anomaly_detection/padim.yaml | 81 - src/otx/recipe/anomaly_detection/stfpm.yaml | 86 - .../recipe/anomaly_segmentation/padim.yaml | 81 - .../recipe/anomaly_segmentation/stfpm.yaml | 86 - .../h_label_cls/efficientnet_b0_light.yaml | 43 - .../h_label_cls/efficientnet_v2_light.yaml | 55 - .../h_label_cls/mobilenet_v3_large_light.yaml | 58 - .../h_label_cls/openvino_model.yaml | 42 - .../h_label_cls/otx_deit_tiny.yaml | 47 - .../efficientnet_b0_light.yaml | 41 - .../efficientnet_v2_light.yaml | 52 - .../mobilenet_v3_large_light.yaml | 55 - .../multi_class_cls/openvino_model.yaml | 38 - .../multi_class_cls/otx_deit_tiny.yaml | 42 - .../multi_class_cls/otx_dino_v2.yaml | 106 - .../otx_dino_v2_linear_probe.yaml | 108 - .../multi_class_cls/otx_efficientnet_b0.yaml | 41 - .../multi_class_cls/otx_efficientnet_v2.yaml | 52 - .../otx_mobilenet_v3_large.yaml | 55 - .../multi_class_cls/tv_efficientnet_b0.yaml | 116 - .../multi_class_cls/tv_efficientnet_b1.yaml | 116 - .../multi_class_cls/tv_efficientnet_b3.yaml | 116 - .../multi_class_cls/tv_efficientnet_b4.yaml | 116 - .../multi_class_cls/tv_efficientnet_v2_l.yaml | 116 - .../tv_mobilenet_v3_small.yaml | 116 - .../multi_class_cls/tv_resnet_50.yaml | 116 - .../efficientnet_b0_light.yaml | 63 - .../efficientnet_v2_light.yaml | 68 - .../mobilenet_v3_large_light.yaml | 71 - .../multi_label_cls/openvino_model.yaml | 39 - .../multi_label_cls/otx_deit_tiny.yaml | 67 - .../recipe/detection/atss_mobilenetv2.yaml | 112 - .../detection/atss_mobilenetv2_tile.yaml | 115 - src/otx/recipe/detection/atss_r50_fpn.yaml | 93 - src/otx/recipe/detection/atss_resnext101.yaml | 112 - src/otx/recipe/detection/openvino_model.yaml | 38 - src/otx/recipe/detection/rtmdet_tiny.yaml | 146 - src/otx/recipe/detection/ssd_mobilenetv2.yaml | 118 - .../detection/ssd_mobilenetv2_tile.yaml | 121 - src/otx/recipe/detection/yolox_l.yaml | 143 - src/otx/recipe/detection/yolox_l_tile.yaml | 124 - src/otx/recipe/detection/yolox_s.yaml | 143 - src/otx/recipe/detection/yolox_s_tile.yaml | 124 - src/otx/recipe/detection/yolox_tiny.yaml | 135 - src/otx/recipe/detection/yolox_tiny_tile.yaml | 131 - src/otx/recipe/detection/yolox_x.yaml | 143 - src/otx/recipe/detection/yolox_x_tile.yaml | 124 - .../maskrcnn_efficientnetb2b.yaml | 105 - .../maskrcnn_efficientnetb2b_tile.yaml | 112 - .../instance_segmentation/maskrcnn_r50.yaml | 106 - .../maskrcnn_r50_tile.yaml | 112 - .../instance_segmentation/maskrcnn_swint.yaml | 103 - .../maskrcnn_swint_tile.yaml | 106 - .../instance_segmentation/openvino_model.yaml | 38 - .../rtmdet_inst_tiny.yaml | 146 - .../maskrcnn_efficientnetb2b.yaml | 99 - .../rotated_detection/maskrcnn_r50.yaml | 99 - .../recipe/semantic_segmentation/dino_v2.yaml | 80 - .../semantic_segmentation/litehrnet_18.yaml | 41 - .../semantic_segmentation/litehrnet_s.yaml | 41 - .../semantic_segmentation/litehrnet_x.yaml | 41 - .../semantic_segmentation/openvino_model.yaml | 33 - .../semantic_segmentation/segnext_b.yaml | 39 - .../semantic_segmentation/segnext_s.yaml | 39 - .../semantic_segmentation/segnext_t.yaml | 39 - .../visual_prompting/openvino_model.yaml | 37 - .../recipe/visual_prompting/sam_tiny_vit.yaml | 96 - .../recipe/visual_prompting/sam_vit_b.yaml | 96 - .../openvino_model.yaml | 30 - .../sam_tiny_vit.yaml | 45 - .../zero_shot_visual_prompting/sam_vit_b.yaml | 45 - src/otx/recipes/__init__.py | 3 + src/otx/recipes/stages/__init__.py | 3 + src/otx/recipes/stages/_base_/__init__.py | 3 + .../recipes/stages/_base_/data/__init__.py | 3 + src/otx/recipes/stages/_base_/data/coco.py | 34 + .../stages/_base_/data/coco_inst_seg.py | 34 + .../recipes/stages/_base_/data/coco_otx.py | 34 + .../recipes/stages/_base_/data/coco_ubt.py | 46 + .../recipes/stages/_base_/data/custom_seg.py | 32 + src/otx/recipes/stages/_base_/data/data.py | 3 + .../recipes/stages/_base_/data/data_seg.py | 29 + .../stages/_base_/data/pipelines/__init__.py | 3 + .../data/pipelines/coco_inst_seg_pipeline.py | 32 + .../data/pipelines/coco_otx_pipeline.py | 33 + .../data/pipelines/coco_resize_hflip_pad.py | 29 + .../stages/_base_/data/pipelines/incr_seg.py | 31 + .../_base_/data/pipelines/twocrop_pipeline.py | 27 + .../stages/_base_/data/pipelines/ubt.py | 126 + .../recipes/stages/_base_/data/seg_semisl.py | 12 + .../stages/_base_/data/selfsl_cls_data.py | 50 + .../stages/_base_/data/selfsl_seg_data.py | 40 + .../stages/_base_/data/twocrop_data.py | 16 + src/otx/recipes/stages/_base_/default.py | 10 + .../recipes/stages/_base_/dist/__init__.py | 3 + src/otx/recipes/stages/_base_/dist/dist.py | 1 + .../recipes/stages/_base_/logs/__init__.py | 3 + src/otx/recipes/stages/_base_/logs/log.py | 1 + .../stages/_base_/logs/tensorboard_logger.py | 7 + .../recipes/stages/_base_/logs/text_logger.py | 10 + .../recipes/stages/_base_/models/__init__.py | 3 + .../_base_/models/classifiers/__init__.py | 3 + .../_base_/models/classifiers/classifier.py | 11 + .../stages/_base_/models/cls_semisl.py | 10 + .../stages/_base_/models/cls_supcon.py | 23 + .../_base_/models/detectors/__init__.py | 3 + .../_base_/models/detectors/detector.py | 5 + src/otx/recipes/stages/_base_/models/model.py | 8 + .../_base_/models/segmentors/__init__.py | 3 + .../_base_/models/segmentors/segmentor.py | 5 + .../stages/_base_/optimizers/__init__.py | 3 + .../recipes/stages/_base_/optimizers/adam.py | 3 + .../recipes/stages/_base_/optimizers/lars.py | 3 + .../stages/_base_/optimizers/optimizer.py | 4 + .../recipes/stages/_base_/optimizers/sgd.py | 3 + .../recipes/stages/_base_/runners/__init__.py | 3 + .../stages/_base_/runners/epoch_runner.py | 5 + .../_base_/runners/epoch_runner_cancel.py | 3 + .../stages/_base_/runners/iter_runner.py | 5 + .../recipes/stages/_base_/runners/runner.py | 4 + .../recipes/stages/_base_/schedules/1cycle.py | 3 + .../stages/_base_/schedules/__init__.py | 3 + .../stages/_base_/schedules/cos_anneal.py | 3 + .../stages/_base_/schedules/plateau.py | 13 + .../stages/_base_/schedules/schedule.py | 2 + .../recipes/stages/classification/__init__.py | 3 + .../stages/classification/finetune.yaml | 25 + .../stages/classification/incremental.yaml | 52 + .../classification/multilabel/__init__.py | 5 + .../multilabel/incremental.yaml | 59 + .../classification/multilabel/semisl.yaml | 18 + .../classification/multilabel/train.yaml | 18 + .../recipes/stages/classification/selfsl.yaml | 45 + .../recipes/stages/classification/semisl.yaml | 22 + .../recipes/stages/classification/supcon.yaml | 25 + .../recipes/stages/classification/train.yaml | 31 + src/otx/recipes/stages/detection/__init__.py | 3 + src/otx/recipes/stages/detection/finetune.py | 20 + .../recipes/stages/detection/incremental.py | 48 + src/otx/recipes/stages/detection/semisl.py | 35 + src/otx/recipes/stages/detection/train.py | 43 + .../stages/instance_segmentation/__init__.py | 3 + .../instance_segmentation/incremental.py | 24 + .../stages/instance_segmentation/semisl.py | 26 + .../stages/instance_segmentation/train.py | 51 + .../recipes/stages/segmentation/__init__.py | 3 + .../recipes/stages/segmentation/finetune.py | 1 + .../stages/segmentation/incremental.py | 36 + .../stages/segmentation/incremental_poly.py | 22 + src/otx/recipes/stages/segmentation/selfsl.py | 33 + src/otx/recipes/stages/segmentation/semisl.py | 44 + .../stages/segmentation/semisl_poly.py | 22 + src/otx/recipes/stages/segmentation/supcon.py | 17 + src/otx/recipes/stages/segmentation/train.py | 46 + src/otx/tools/__init__.py | 4 - src/otx/tools/translate_mmrecipe.py | 57 - src/otx/utils/__init__.py | 21 +- src/otx/utils/logger.py | 154 + src/otx/utils/signal.py | 70 - src/otx/utils/utils.py | 120 +- tests/__init__.py | 20 +- .../action_classification_dataset/test.csv | 3 - .../action_classification_dataset/test/0.mp4 | Bin 10404 -> 0 bytes .../action_classification_dataset/test/1.mp4 | Bin 10241 -> 0 bytes .../action_classification_dataset/train.csv | 3 - .../action_classification_dataset/train/0.mp4 | Bin 10404 -> 0 bytes .../action_classification_dataset/train/1.mp4 | Bin 10241 -> 0 bytes .../action_classification_dataset/val.csv | 3 - .../action_classification_dataset/val/0.mp4 | Bin 10404 -> 0 bytes .../action_classification_dataset/val/1.mp4 | Bin 10241 -> 0 bytes .../annotations/ava_action_list_v2.2.pbtxt | 20 - .../annotations/ava_test.csv | 40 - .../annotations/ava_train.csv | 40 - .../annotations/ava_val.csv | 40 - .../annotations/test.pkl | Bin 3801 -> 0 bytes .../annotations/train.pkl | Bin 3801 -> 0 bytes .../annotations/val.pkl | Bin 3801 -> 0 bytes .../frames/train_video0/train_video0_0000.jpg | Bin 944 -> 0 bytes .../frames/train_video0/train_video0_0001.jpg | Bin 918 -> 0 bytes .../frames/train_video0/train_video0_0002.jpg | Bin 943 -> 0 bytes .../frames/train_video0/train_video0_0003.jpg | Bin 923 -> 0 bytes .../frames/train_video0/train_video0_0004.jpg | Bin 878 -> 0 bytes .../frames/train_video0/train_video0_0005.jpg | Bin 1038 -> 0 bytes .../frames/train_video0/train_video0_0006.jpg | Bin 1006 -> 0 bytes .../frames/train_video0/train_video0_0007.jpg | Bin 1035 -> 0 bytes .../frames/train_video0/train_video0_0008.jpg | Bin 1076 -> 0 bytes .../frames/train_video0/train_video0_0009.jpg | Bin 1038 -> 0 bytes .../frames/train_video1/train_video1_0000.jpg | Bin 945 -> 0 bytes .../frames/train_video1/train_video1_0001.jpg | Bin 924 -> 0 bytes .../frames/train_video1/train_video1_0002.jpg | Bin 944 -> 0 bytes .../frames/train_video1/train_video1_0003.jpg | Bin 928 -> 0 bytes .../frames/train_video1/train_video1_0004.jpg | Bin 878 -> 0 bytes .../frames/train_video1/train_video1_0005.jpg | Bin 1042 -> 0 bytes .../frames/train_video1/train_video1_0006.jpg | Bin 1010 -> 0 bytes .../frames/train_video1/train_video1_0007.jpg | Bin 1045 -> 0 bytes .../frames/train_video1/train_video1_0008.jpg | Bin 1079 -> 0 bytes .../frames/train_video1/train_video1_0009.jpg | Bin 1052 -> 0 bytes .../frames/train_video2/train_video2_0000.jpg | Bin 1074 -> 0 bytes .../frames/train_video2/train_video2_0001.jpg | Bin 1038 -> 0 bytes .../frames/train_video2/train_video2_0002.jpg | Bin 1076 -> 0 bytes .../frames/train_video2/train_video2_0003.jpg | Bin 1035 -> 0 bytes .../frames/train_video2/train_video2_0004.jpg | Bin 1006 -> 0 bytes .../frames/train_video2/train_video2_0005.jpg | Bin 1038 -> 0 bytes .../frames/train_video2/train_video2_0006.jpg | Bin 878 -> 0 bytes .../frames/train_video2/train_video2_0007.jpg | Bin 923 -> 0 bytes .../frames/train_video2/train_video2_0008.jpg | Bin 943 -> 0 bytes .../frames/train_video2/train_video2_0009.jpg | Bin 918 -> 0 bytes .../frames/train_video3/train_video3_0000.jpg | Bin 1079 -> 0 bytes .../frames/train_video3/train_video3_0001.jpg | Bin 1052 -> 0 bytes .../frames/train_video3/train_video3_0002.jpg | Bin 1079 -> 0 bytes .../frames/train_video3/train_video3_0003.jpg | Bin 1045 -> 0 bytes .../frames/train_video3/train_video3_0004.jpg | Bin 1010 -> 0 bytes .../frames/train_video3/train_video3_0005.jpg | Bin 1042 -> 0 bytes .../frames/train_video3/train_video3_0006.jpg | Bin 878 -> 0 bytes .../frames/train_video3/train_video3_0007.jpg | Bin 928 -> 0 bytes .../frames/train_video3/train_video3_0008.jpg | Bin 944 -> 0 bytes .../frames/train_video3/train_video3_0009.jpg | Bin 924 -> 0 bytes .../dataset/training/street/1.jpg | Bin 0 -> 631 bytes .../dataset/training/street/1_atr.txt | 3 + .../dataset/training/street/1_parts_1.png | Bin 0 -> 81 bytes .../dataset/training/street/1_seg.png | Bin 0 -> 85 bytes .../dataset/validation/2.jpg | Bin 0 -> 631 bytes .../dataset/validation/2_atr.txt | 4 + .../dataset/validation/2_parts_1.png | Bin 0 -> 89 bytes .../dataset/validation/2_parts_2.png | Bin 0 -> 88 bytes .../dataset/validation/2_seg.png | Bin 0 -> 85 bytes .../dataset_with_meta_file/dataset_meta.json | 3 + .../training/street/1.jpg | Bin 0 -> 631 bytes .../training/street/1_atr.txt | 3 + .../training/street/1_parts_1.png | Bin 0 -> 81 bytes .../training/street/1_seg.png | Bin 0 -> 85 bytes .../dataset/training/street/1.jpg | Bin 0 -> 631 bytes .../dataset/training/street/1.json | 102 + .../street/1/instance_000_ADE_train_1.png | Bin 0 -> 81 bytes .../street/1/instance_001_ADE_train_1.png | Bin 0 -> 83 bytes .../street/1/instance_002_ADE_train_1.png | Bin 0 -> 81 bytes .../dataset/training/street/1_parts_1.png | Bin 0 -> 93 bytes .../dataset/training/street/1_seg.png | Bin 0 -> 103 bytes .../dataset/validation/2.jpg | Bin 0 -> 631 bytes .../dataset/validation/2.json | 124 + .../validation/2/instance_000_ADE_val_2.png | Bin 0 -> 81 bytes .../validation/2/instance_001_ADE_val_2.png | Bin 0 -> 83 bytes .../validation/2/instance_002_ADE_val_2.png | Bin 0 -> 81 bytes .../validation/2/instance_003_ADE_val_2.png | Bin 0 -> 81 bytes .../dataset/validation/2_parts_1.png | Bin 0 -> 93 bytes .../dataset/validation/2_parts_2.png | Bin 0 -> 90 bytes .../dataset/validation/2_seg.png | Bin 0 -> 103 bytes .../dataset_with_meta_file/dataset_meta.json | 3 + .../training/street/1.jpg | Bin 0 -> 631 bytes .../training/street/1.json | 102 + .../street/1/instance_000_ADE_train_1.png | Bin 0 -> 81 bytes .../street/1/instance_001_ADE_train_1.png | Bin 0 -> 83 bytes .../street/1/instance_002_ADE_train_1.png | Bin 0 -> 81 bytes .../training/street/1_parts_1.png | Bin 0 -> 93 bytes .../training/street/1_seg.png | Bin 0 -> 103 bytes tests/assets/anomaly/classification/test.json | 41 + .../assets/anomaly/classification/train.json | 63 + tests/assets/anomaly/classification/val.json | 38 + tests/assets/anomaly/detection/test.json | 62 + tests/assets/anomaly/detection/train.json | 63 + tests/assets/anomaly/detection/val.json | 82 + .../hazelnut}/ground_truth/colour/00.png | Bin .../hazelnut}/ground_truth/colour/01.png | Bin .../hazelnut}/ground_truth/colour/02.png | Bin .../hazelnut}/ground_truth/colour/03.png | Bin .../hazelnut}/ground_truth/colour/04.png | Bin .../hazelnut}/ground_truth/colour/05.png | Bin .../hazelnut}/ground_truth/colour/06.png | Bin .../hazelnut}/ground_truth/colour/07.png | Bin .../hazelnut}/ground_truth/colour/08.png | Bin .../hazelnut}/ground_truth/colour/09.png | Bin .../hazelnut}/ground_truth/colour/10.png | Bin .../hazelnut}/ground_truth/colour/11.png | Bin .../hazelnut}/ground_truth/colour/12.png | Bin .../hazelnut}/ground_truth/colour/13.png | Bin .../hazelnut}/ground_truth/colour/14.png | Bin .../hazelnut}/ground_truth/colour/15.png | Bin .../hazelnut}/ground_truth/colour/16.png | Bin .../hazelnut}/test/colour/00.jpg | Bin .../hazelnut}/test/colour/01.jpg | Bin .../hazelnut}/test/colour/02.jpg | Bin .../hazelnut}/test/colour/03.jpg | Bin .../hazelnut}/test/colour/04.jpg | Bin .../hazelnut}/test/colour/05.jpg | Bin .../hazelnut}/test/colour/06.jpg | Bin .../hazelnut}/test/colour/07.jpg | Bin .../hazelnut}/test/colour/08.jpg | Bin .../hazelnut}/test/colour/09.jpg | Bin .../hazelnut}/test/colour/10.jpg | Bin .../hazelnut}/test/colour/11.jpg | Bin .../hazelnut}/test/colour/12.jpg | Bin .../hazelnut}/test/colour/13.jpg | Bin .../hazelnut}/test/colour/14.jpg | Bin .../hazelnut}/test/colour/15.jpg | Bin .../hazelnut}/test/colour/16.jpg | Bin .../hazelnut}/test/good/04.jpg | Bin .../hazelnut}/test/good/05.jpg | Bin .../hazelnut}/test/good/13.jpg | Bin .../hazelnut}/test/good/23.jpg | Bin .../hazelnut}/test/good/25.jpg | Bin .../hazelnut}/test/good/28.jpg | Bin .../hazelnut}/train/good/00.jpg | Bin .../hazelnut}/train/good/01.jpg | Bin .../hazelnut}/train/good/02.jpg | Bin .../hazelnut}/train/good/03.jpg | Bin .../hazelnut}/train/good/06.jpg | Bin .../hazelnut}/train/good/07.jpg | Bin .../hazelnut}/train/good/08.jpg | Bin .../hazelnut}/train/good/09.jpg | Bin .../hazelnut}/train/good/10.jpg | Bin .../hazelnut}/train/good/11.jpg | Bin .../hazelnut}/train/good/12.jpg | Bin .../hazelnut}/train/good/14.jpg | Bin .../hazelnut}/train/good/15.jpg | Bin .../hazelnut}/train/good/16.jpg | Bin .../hazelnut}/train/good/17.jpg | Bin .../hazelnut}/train/good/18.jpg | Bin .../hazelnut}/train/good/19.jpg | Bin .../hazelnut}/train/good/20.jpg | Bin .../hazelnut}/train/good/21.jpg | Bin .../hazelnut}/train/good/22.jpg | Bin .../hazelnut}/train/good/24.jpg | Bin .../hazelnut}/train/good/26.jpg | Bin .../hazelnut}/train/good/27.jpg | Bin .../hazelnut}/train/good/29.jpg | Bin .../hazelnut}/train/good/30.jpg | Bin .../hazelnut}/train/good/31.jpg | Bin .../hazelnut}/train/good/32.jpg | Bin .../hazelnut}/train/good/33.jpg | Bin tests/assets/anomaly/segmentation/test.json | 2833 +++++ tests/assets/anomaly/segmentation/train.json | 63 + tests/assets/anomaly/segmentation/val.json | 2431 +++++ .../annotations/instances_test.json | 88 - .../annotations/instances_train_5_imgs.json | 248 + .../annotations/instances_val.json | 12 +- .../annotations/instances_val_1_imgs.json | 86 + .../car_tree_bug/images/test/Slide20.PNG | Bin 6123 -> 0 bytes .../car_tree_bug/images/test/Slide3.PNG | Bin 21103 -> 0 bytes .../car_tree_bug/images/val/Slide20.PNG | Bin 6123 -> 0 bytes .../images/val}/Slide4.PNG | Bin .../assets/car_tree_bug/images/val/Slide5.PNG | Bin 0 -> 10729 bytes .../annotations/instances_test.json | 316 - .../images/test/Slide3.PNG | Bin 21103 -> 0 bytes .../images/test/Slide5.PNG | Bin 31177 -> 0 bytes .../images/test/Slide6.PNG | Bin 21277 -> 0 bytes .../images/test/Slide7.PNG | Bin 32317 -> 0 bytes .../images/test/Slide8.PNG | Bin 22874 -> 0 bytes .../images/test/Slide9.PNG | Bin 26796 -> 0 bytes ...tcity_000001_000031_gtFine_instanceIds.png | Bin 0 -> 76 bytes ...tcity_000001_000032_gtFine_instanceIds.png | Bin 0 -> 76 bytes ...tcity_000002_000045_gtFine_instanceIds.png | Bin 0 -> 76 bytes ...tcity_000001_000019_gtFine_instanceIds.png | Bin 0 -> 76 bytes .../defaultcity_000001_000031_leftImg8bit.png | Bin 0 -> 70 bytes .../defaultcity_000001_000032_leftImg8bit.png | Bin 0 -> 70 bytes .../defaultcity_000002_000045_leftImg8bit.png | Bin 0 -> 70 bytes .../defaultcity_000001_000019_leftImg8bit.png | Bin 0 -> 70 bytes ...tcity_000001_000019_gtFine_instanceIds.png | Bin 0 -> 76 bytes ...ity_000001_000019_gtFine_labelTrainIds.png | Bin 0 -> 69 bytes ...tcity_000001_000031_gtFine_instanceIds.png | Bin 0 -> 76 bytes ...ity_000001_000031_gtFine_labelTrainIds.png | Bin 0 -> 71 bytes ...tcity_000001_000032_gtFine_instanceIds.png | Bin 0 -> 76 bytes ...ity_000001_000032_gtFine_labelTrainIds.png | Bin 0 -> 71 bytes ...tcity_000002_000045_gtFine_instanceIds.png | Bin 0 -> 76 bytes ...ity_000002_000045_gtFine_labelTrainIds.png | Bin 0 -> 71 bytes .../defaultcity_000001_000019_leftImg8bit.png | Bin 0 -> 70 bytes .../defaultcity_000001_000031_leftImg8bit.png | Bin 0 -> 70 bytes .../defaultcity_000001_000032_leftImg8bit.png | Bin 0 -> 70 bytes .../defaultcity_000002_000045_leftImg8bit.png | Bin 0 -> 70 bytes .../{test => }/0/11.jpg | Bin .../{test => }/0/14.jpg | Bin .../{test => }/0/16.jpg | Bin .../{test => }/0/17.jpg | Bin .../{test => }/0/18.jpg | Bin .../{test => }/0/23.jpg | Bin .../{test => }/0/26.jpg | Bin .../classification_dataset/{test => }/0/3.jpg | Bin .../{test => }/0/30.jpg | Bin .../classification_dataset/{test => }/0/4.jpg | Bin .../classification_dataset/{test => }/0/5.jpg | Bin .../classification_dataset/{test => }/0/6.jpg | Bin .../classification_dataset/{test => }/0/7.jpg | Bin .../classification_dataset/{test => }/0/8.jpg | Bin .../classification_dataset/{test => }/1/0.jpg | Bin .../classification_dataset/{test => }/1/1.jpg | Bin .../{test => }/1/10.jpg | Bin .../{test => }/1/12.jpg | Bin .../{test => }/1/15.jpg | Bin .../{test => }/1/19.jpg | Bin .../classification_dataset/{test => }/1/2.jpg | Bin .../{test => }/1/21.jpg | Bin .../{test => }/1/27.jpg | Bin .../{test => }/1/28.jpg | Bin .../{test => }/1/29.jpg | Bin .../classification_dataset/val/0/11.jpg | Bin 730 -> 0 bytes .../classification_dataset/val/0/14.jpg | Bin 747 -> 0 bytes .../classification_dataset/val/0/16.jpg | Bin 732 -> 0 bytes .../classification_dataset/val/0/17.jpg | Bin 727 -> 0 bytes .../classification_dataset/val/0/18.jpg | Bin 756 -> 0 bytes .../classification_dataset/val/0/23.jpg | Bin 705 -> 0 bytes .../classification_dataset/val/0/26.jpg | Bin 759 -> 0 bytes .../assets/classification_dataset/val/0/3.jpg | Bin 782 -> 0 bytes .../classification_dataset/val/0/30.jpg | Bin 766 -> 0 bytes .../assets/classification_dataset/val/0/4.jpg | Bin 723 -> 0 bytes .../assets/classification_dataset/val/0/5.jpg | Bin 739 -> 0 bytes .../assets/classification_dataset/val/0/6.jpg | Bin 790 -> 0 bytes .../assets/classification_dataset/val/0/7.jpg | Bin 691 -> 0 bytes .../assets/classification_dataset/val/0/8.jpg | Bin 785 -> 0 bytes .../assets/classification_dataset/val/1/0.jpg | Bin 797 -> 0 bytes .../assets/classification_dataset/val/1/1.jpg | Bin 791 -> 0 bytes .../classification_dataset/val/1/10.jpg | Bin 825 -> 0 bytes .../classification_dataset/val/1/12.jpg | Bin 831 -> 0 bytes .../classification_dataset/val/1/15.jpg | Bin 803 -> 0 bytes .../classification_dataset/val/1/19.jpg | Bin 741 -> 0 bytes .../assets/classification_dataset/val/1/2.jpg | Bin 814 -> 0 bytes .../classification_dataset/val/1/21.jpg | Bin 815 -> 0 bytes .../classification_dataset/val/1/27.jpg | Bin 830 -> 0 bytes .../classification_dataset/val/1/28.jpg | Bin 785 -> 0 bytes .../classification_dataset/val/1/29.jpg | Bin 803 -> 0 bytes .../0/11.jpg | Bin .../0/14.jpg | Bin .../0/16.jpg | Bin .../0/17.jpg | Bin .../0/18.jpg | Bin .../0/23.jpg | Bin .../0/26.jpg | Bin .../0/3.jpg | Bin .../0/30.jpg | Bin .../0/4.jpg | Bin .../0/5.jpg | Bin .../0/6.jpg | Bin .../0/7.jpg | Bin .../0/8.jpg | Bin .../1/0.jpg | Bin .../1/1.jpg | Bin .../1/10.jpg | Bin .../1/12.jpg | Bin .../1/15.jpg | Bin .../1/19.jpg | Bin .../1/2.jpg | Bin .../1/21.jpg | Bin .../1/27.jpg | Bin .../1/28.jpg | Bin .../1/29.jpg | Bin .../2/13.jpg | Bin 0 -> 704 bytes .../2/20.jpg | Bin 0 -> 706 bytes .../2/22.jpg | Bin 0 -> 773 bytes .../2/24.jpg | Bin 0 -> 751 bytes .../2/25.jpg | Bin 0 -> 721 bytes .../2/31.jpg | Bin 0 -> 743 bytes .../2/9.jpg | Bin 0 -> 711 bytes .../3/.gitignore | 4 + .../supervised/val/dataset_meta.json | 5 - .../supervised/val/images/0001.png | Bin 319 -> 0 bytes .../supervised/val/images/0002.png | Bin 319 -> 0 bytes .../supervised/val/masks/0001.png | Bin 420 -> 0 bytes .../supervised/val/masks/0002.png | Bin 652 -> 0 bytes .../test => train}/dataset_meta.json | 0 .../{supervised => }/train/images/0001.png | Bin .../{supervised => }/train/images/0002.png | Bin .../{supervised => }/train/images/0003.png | Bin .../{supervised => }/train/masks/0001.png | Bin .../{supervised => }/train/masks/0002.png | Bin .../{supervised => }/train/masks/0003.png | Bin .../train => val}/dataset_meta.json | 0 .../{supervised/test => val}/images/0001.png | Bin .../{supervised/test => val}/images/0002.png | Bin .../{supervised/test => val}/masks/0001.png | Bin .../{supervised/test => val}/masks/0002.png | Bin .../train/0/annotations.xml | 61 + .../train/0/images/00020.jpg | Bin 0 -> 735 bytes .../train/0/images/00021.jpg | Bin 0 -> 728 bytes .../train/0/images/00022.jpg | Bin 0 -> 732 bytes .../train/0/images/00023.jpg | Bin 0 -> 727 bytes .../train/0/images/00024.jpg | Bin 0 -> 713 bytes .../train/0/images/00025.jpg | Bin 0 -> 739 bytes .../train/0/images/00026.jpg | Bin 0 -> 732 bytes .../train/0/images/00027.jpg | Bin 0 -> 743 bytes .../train/0/images/00028.jpg | Bin 0 -> 751 bytes .../train/0/images/00029.jpg | Bin 0 -> 748 bytes .../train/1/annotations.xml | 61 + .../train/1/images/00020.jpg | Bin 0 -> 748 bytes .../train/1/images/00021.jpg | Bin 0 -> 751 bytes .../train/1/images/00022.jpg | Bin 0 -> 743 bytes .../train/1/images/00023.jpg | Bin 0 -> 732 bytes .../train/1/images/00024.jpg | Bin 0 -> 739 bytes .../train/1/images/00025.jpg | Bin 0 -> 713 bytes .../train/1/images/00026.jpg | Bin 0 -> 727 bytes .../train/1/images/00027.jpg | Bin 0 -> 732 bytes .../train/1/images/00028.jpg | Bin 0 -> 728 bytes .../train/1/images/00029.jpg | Bin 0 -> 735 bytes .../cvat_dataset/action_detection/train.pkl | Bin 0 -> 3185 bytes .../action_detection/train/0/annotations.xml | 65 + .../train/0/images/frame_000000.png | Bin 0 -> 94 bytes .../train/0/images/frame_000001.png | Bin 0 -> 95 bytes .../train/0/images/frame_000002.png | Bin 0 -> 95 bytes .../train/0/images/frame_000003.png | Bin 0 -> 95 bytes .../train/0/images/frame_000004.png | Bin 0 -> 98 bytes .../train/0/images/frame_000005.png | Bin 0 -> 93 bytes .../train/0/images/frame_000006.png | Bin 0 -> 93 bytes .../train/0/images/frame_000007.png | Bin 0 -> 93 bytes .../train/0/images/frame_000008.png | Bin 0 -> 93 bytes .../train/0/images/frame_000009.png | Bin 0 -> 93 bytes .../action_detection/train/1/annotations.xml | 65 + .../train/1/images/frame_000000.png | Bin 0 -> 95 bytes .../train/1/images/frame_000001.png | Bin 0 -> 93 bytes .../train/1/images/frame_000002.png | Bin 0 -> 93 bytes .../train/1/images/frame_000003.png | Bin 0 -> 93 bytes .../train/1/images/frame_000004.png | Bin 0 -> 93 bytes .../train/1/images/frame_000005.png | Bin 0 -> 93 bytes .../train/1/images/frame_000006.png | Bin 0 -> 98 bytes .../train/1/images/frame_000007.png | Bin 0 -> 95 bytes .../train/1/images/frame_000008.png | Bin 0 -> 95 bytes .../train/1/images/frame_000009.png | Bin 0 -> 95 bytes .../action_detection/train/2/annotations.xml | 65 + .../train/2/images/frame_000000.png | Bin 0 -> 98 bytes .../train/2/images/frame_000001.png | Bin 0 -> 98 bytes .../train/2/images/frame_000002.png | Bin 0 -> 98 bytes .../train/2/images/frame_000003.png | Bin 0 -> 98 bytes .../train/2/images/frame_000004.png | Bin 0 -> 98 bytes .../train/2/images/frame_000005.png | Bin 0 -> 97 bytes .../train/2/images/frame_000006.png | Bin 0 -> 98 bytes .../train/2/images/frame_000007.png | Bin 0 -> 98 bytes .../train/2/images/frame_000008.png | Bin 0 -> 98 bytes .../train/2/images/frame_000009.png | Bin 0 -> 98 bytes .../action_detection/train/3/annotations.xml | 65 + .../train/3/images/frame_000000.png | Bin 0 -> 98 bytes .../train/3/images/frame_000001.png | Bin 0 -> 98 bytes .../train/3/images/frame_000002.png | Bin 0 -> 98 bytes .../train/3/images/frame_000003.png | Bin 0 -> 98 bytes .../train/3/images/frame_000004.png | Bin 0 -> 98 bytes .../train/3/images/frame_000005.png | Bin 0 -> 97 bytes .../train/3/images/frame_000006.png | Bin 0 -> 98 bytes .../train/3/images/frame_000007.png | Bin 0 -> 98 bytes .../train/3/images/frame_000008.png | Bin 0 -> 98 bytes .../train/3/images/frame_000009.png | Bin 0 -> 98 bytes .../datumaro_h-label/annotations/train.json | 489 + .../datumaro_h-label/annotations/valid.json | 489 + .../datumaro_h-label/images/train/00.jpg | Bin 0 -> 5285 bytes .../datumaro_h-label/images/train/01.jpg | Bin 0 -> 5431 bytes .../datumaro_h-label/images/train/02.jpg | Bin 0 -> 5341 bytes .../datumaro_h-label/images/train/03.jpg | Bin 0 -> 4541 bytes .../datumaro_h-label/images/train/04.jpg | Bin 0 -> 4335 bytes .../datumaro_h-label/images/train/05.jpg | Bin 0 -> 4496 bytes .../datumaro_h-label/images/train/06.jpg | Bin 0 -> 4697 bytes .../datumaro_h-label/images/train/07.jpg | Bin 0 -> 4530 bytes .../datumaro_h-label/images/train/08.jpg | Bin 0 -> 4822 bytes .../datumaro_h-label/images/train/09.jpg | Bin 0 -> 2963 bytes .../datumaro_h-label/images/train/10.jpg | Bin 0 -> 2881 bytes .../datumaro_h-label/images/train/11.jpg | Bin 0 -> 2882 bytes .../datumaro_h-label/images/valid/00.jpg | Bin 0 -> 5285 bytes .../datumaro_h-label/images/valid/01.jpg | Bin 0 -> 5431 bytes .../datumaro_h-label/images/valid/02.jpg | Bin 0 -> 5341 bytes .../datumaro_h-label/images/valid/03.jpg | Bin 0 -> 4541 bytes .../datumaro_h-label/images/valid/04.jpg | Bin 0 -> 4335 bytes .../datumaro_h-label/images/valid/05.jpg | Bin 0 -> 4496 bytes .../datumaro_h-label/images/valid/06.jpg | Bin 0 -> 4697 bytes .../datumaro_h-label/images/valid/07.jpg | Bin 0 -> 4530 bytes .../datumaro_h-label/images/valid/08.jpg | Bin 0 -> 4822 bytes .../datumaro_h-label/images/valid/09.jpg | Bin 0 -> 2963 bytes .../datumaro_h-label/images/valid/10.jpg | Bin 0 -> 2881 bytes .../datumaro_h-label/images/valid/11.jpg | Bin 0 -> 2882 bytes .../annotations/train.json | 430 + .../annotations/valid.json | 430 + .../images/train/00.jpg | Bin 0 -> 4679 bytes .../images/train/01.jpg | Bin 0 -> 4937 bytes .../images/train/02.jpg | Bin 0 -> 4765 bytes .../images/train/03.jpg | Bin 0 -> 4393 bytes .../images/train/04.jpg | Bin 0 -> 4380 bytes .../images/train/05.jpg | Bin 0 -> 4454 bytes .../images/train/06.jpg | Bin 0 -> 4131 bytes .../images/train/07.jpg | Bin 0 -> 4067 bytes .../images/train/08.jpg | Bin 0 -> 4041 bytes .../images/train/09.jpg | Bin 0 -> 2160 bytes .../images/train/10.jpg | Bin 0 -> 2417 bytes .../images/train/11.jpg | Bin 0 -> 2380 bytes .../images/valid/00.jpg | Bin 0 -> 4679 bytes .../images/valid/01.jpg | Bin 0 -> 4937 bytes .../images/valid/02.jpg | Bin 0 -> 4765 bytes .../images/valid/03.jpg | Bin 0 -> 4393 bytes .../images/valid/04.jpg | Bin 0 -> 4380 bytes .../images/valid/05.jpg | Bin 0 -> 4454 bytes .../images/valid/06.jpg | Bin 0 -> 4131 bytes .../images/valid/07.jpg | Bin 0 -> 4067 bytes .../images/valid/08.jpg | Bin 0 -> 4041 bytes .../images/valid/09.jpg | Bin 0 -> 2160 bytes .../images/valid/10.jpg | Bin 0 -> 2417 bytes .../images/valid/11.jpg | Bin 0 -> 2380 bytes .../annotations/train.json | 302 + .../annotations/valid.json | 302 + .../datumaro_multilabel/images/train/00.jpg | Bin 0 -> 3482 bytes .../datumaro_multilabel/images/train/01.jpg | Bin 0 -> 3338 bytes .../datumaro_multilabel/images/train/02.jpg | Bin 0 -> 3414 bytes .../datumaro_multilabel/images/train/03.jpg | Bin 0 -> 3044 bytes .../datumaro_multilabel/images/train/04.jpg | Bin 0 -> 3044 bytes .../datumaro_multilabel/images/train/05.jpg | Bin 0 -> 3074 bytes .../datumaro_multilabel/images/train/06.jpg | Bin 0 -> 2988 bytes .../datumaro_multilabel/images/train/07.jpg | Bin 0 -> 2923 bytes .../datumaro_multilabel/images/train/08.jpg | Bin 0 -> 3027 bytes .../datumaro_multilabel/images/train/09.jpg | Bin 0 -> 2242 bytes .../datumaro_multilabel/images/train/10.jpg | Bin 0 -> 1992 bytes .../datumaro_multilabel/images/train/11.jpg | Bin 0 -> 2138 bytes .../datumaro_multilabel/images/valid/00.jpg | Bin 0 -> 3482 bytes .../datumaro_multilabel/images/valid/01.jpg | Bin 0 -> 3338 bytes .../datumaro_multilabel/images/valid/02.jpg | Bin 0 -> 3414 bytes .../datumaro_multilabel/images/valid/03.jpg | Bin 0 -> 3044 bytes .../datumaro_multilabel/images/valid/04.jpg | Bin 0 -> 3044 bytes .../datumaro_multilabel/images/valid/05.jpg | Bin 0 -> 3074 bytes .../datumaro_multilabel/images/valid/06.jpg | Bin 0 -> 2988 bytes .../datumaro_multilabel/images/valid/07.jpg | Bin 0 -> 2923 bytes .../datumaro_multilabel/images/valid/08.jpg | Bin 0 -> 3027 bytes .../datumaro_multilabel/images/valid/09.jpg | Bin 0 -> 2242 bytes .../datumaro_multilabel/images/valid/10.jpg | Bin 0 -> 1992 bytes .../datumaro_multilabel/images/valid/11.jpg | Bin 0 -> 2138 bytes .../annotations/train.json | 238 + .../annotations/valid.json | 238 + .../images/train/00.jpg | Bin 0 -> 3078 bytes .../images/train/01.jpg | Bin 0 -> 3118 bytes .../images/train/02.jpg | Bin 0 -> 3009 bytes .../images/train/03.jpg | Bin 0 -> 3082 bytes .../images/train/04.jpg | Bin 0 -> 2886 bytes .../images/train/05.jpg | Bin 0 -> 3041 bytes .../images/train/06.jpg | Bin 0 -> 2697 bytes .../images/train/07.jpg | Bin 0 -> 2689 bytes .../images/train/08.jpg | Bin 0 -> 2697 bytes .../images/train/09.jpg | Bin 0 -> 1872 bytes .../images/train/10.jpg | Bin 0 -> 1706 bytes .../images/train/11.jpg | Bin 0 -> 1810 bytes .../images/valid/00.jpg | Bin 0 -> 3078 bytes .../images/valid/01.jpg | Bin 0 -> 3118 bytes .../images/valid/02.jpg | Bin 0 -> 3009 bytes .../images/valid/03.jpg | Bin 0 -> 3082 bytes .../images/valid/04.jpg | Bin 0 -> 2886 bytes .../images/valid/05.jpg | Bin 0 -> 3041 bytes .../images/valid/06.jpg | Bin 0 -> 2697 bytes .../images/valid/07.jpg | Bin 0 -> 2689 bytes .../images/valid/08.jpg | Bin 0 -> 2697 bytes .../images/valid/09.jpg | Bin 0 -> 1872 bytes .../images/valid/10.jpg | Bin 0 -> 1706 bytes .../images/valid/11.jpg | Bin 0 -> 1810 bytes .../annotations/test.json | 721 -- .../annotations/train.json | 700 -- .../annotations/val.json | 721 -- .../hlabel_classification/images/test/0.jpg | Bin 19539 -> 0 bytes .../hlabel_classification/images/test/1.jpg | Bin 29933 -> 0 bytes .../hlabel_classification/images/test/10.jpg | Bin 222211 -> 0 bytes .../hlabel_classification/images/test/11.jpg | Bin 360881 -> 0 bytes .../hlabel_classification/images/test/12.jpg | Bin 57832 -> 0 bytes .../hlabel_classification/images/test/13.jpg | Bin 65429 -> 0 bytes .../hlabel_classification/images/test/14.jpg | Bin 66226 -> 0 bytes .../hlabel_classification/images/test/15.jpg | Bin 370121 -> 0 bytes .../hlabel_classification/images/test/16.jpg | Bin 188698 -> 0 bytes .../hlabel_classification/images/test/17.jpg | Bin 175283 -> 0 bytes .../hlabel_classification/images/test/18.jpg | Bin 247356 -> 0 bytes .../hlabel_classification/images/test/19.jpg | Bin 44669 -> 0 bytes .../hlabel_classification/images/test/2.jpg | Bin 278748 -> 0 bytes .../hlabel_classification/images/test/20.jpg | Bin 157413 -> 0 bytes .../hlabel_classification/images/test/3.jpg | Bin 25473 -> 0 bytes .../hlabel_classification/images/test/4.jpg | Bin 47303 -> 0 bytes .../hlabel_classification/images/test/5.jpg | Bin 57425 -> 0 bytes .../hlabel_classification/images/test/6.jpg | Bin 143396 -> 0 bytes .../hlabel_classification/images/test/7.jpg | Bin 362909 -> 0 bytes .../hlabel_classification/images/test/8.jpg | Bin 178732 -> 0 bytes .../hlabel_classification/images/test/9.jpg | Bin 370567 -> 0 bytes .../hlabel_classification/images/train/0.jpg | Bin 19539 -> 0 bytes .../hlabel_classification/images/train/1.jpg | Bin 29933 -> 0 bytes .../hlabel_classification/images/train/10.jpg | Bin 222211 -> 0 bytes .../hlabel_classification/images/train/11.jpg | Bin 360881 -> 0 bytes .../hlabel_classification/images/train/12.jpg | Bin 57832 -> 0 bytes .../hlabel_classification/images/train/13.jpg | Bin 65429 -> 0 bytes .../hlabel_classification/images/train/14.jpg | Bin 66226 -> 0 bytes .../hlabel_classification/images/train/15.jpg | Bin 370121 -> 0 bytes .../hlabel_classification/images/train/16.jpg | Bin 188698 -> 0 bytes .../hlabel_classification/images/train/17.jpg | Bin 175283 -> 0 bytes .../hlabel_classification/images/train/18.jpg | Bin 247356 -> 0 bytes .../hlabel_classification/images/train/19.jpg | Bin 44669 -> 0 bytes .../hlabel_classification/images/train/2.jpg | Bin 278748 -> 0 bytes .../hlabel_classification/images/train/20.jpg | Bin 157413 -> 0 bytes .../hlabel_classification/images/train/3.jpg | Bin 25473 -> 0 bytes .../hlabel_classification/images/train/4.jpg | Bin 47303 -> 0 bytes .../hlabel_classification/images/train/5.jpg | Bin 57425 -> 0 bytes .../hlabel_classification/images/train/6.jpg | Bin 143396 -> 0 bytes .../hlabel_classification/images/train/7.jpg | Bin 362909 -> 0 bytes .../hlabel_classification/images/train/8.jpg | Bin 178732 -> 0 bytes .../hlabel_classification/images/train/9.jpg | Bin 370567 -> 0 bytes .../hlabel_classification/images/val/0.jpg | Bin 19539 -> 0 bytes .../hlabel_classification/images/val/1.jpg | Bin 29933 -> 0 bytes .../hlabel_classification/images/val/10.jpg | Bin 222211 -> 0 bytes .../hlabel_classification/images/val/11.jpg | Bin 360881 -> 0 bytes .../hlabel_classification/images/val/12.jpg | Bin 57832 -> 0 bytes .../hlabel_classification/images/val/13.jpg | Bin 65429 -> 0 bytes .../hlabel_classification/images/val/14.jpg | Bin 66226 -> 0 bytes .../hlabel_classification/images/val/15.jpg | Bin 370121 -> 0 bytes .../hlabel_classification/images/val/16.jpg | Bin 188698 -> 0 bytes .../hlabel_classification/images/val/17.jpg | Bin 175283 -> 0 bytes .../hlabel_classification/images/val/18.jpg | Bin 247356 -> 0 bytes .../hlabel_classification/images/val/19.jpg | Bin 44669 -> 0 bytes .../hlabel_classification/images/val/2.jpg | Bin 278748 -> 0 bytes .../hlabel_classification/images/val/20.jpg | Bin 157413 -> 0 bytes .../hlabel_classification/images/val/3.jpg | Bin 25473 -> 0 bytes .../hlabel_classification/images/val/4.jpg | Bin 47303 -> 0 bytes .../hlabel_classification/images/val/5.jpg | Bin 57425 -> 0 bytes .../hlabel_classification/images/val/6.jpg | Bin 143396 -> 0 bytes .../hlabel_classification/images/val/7.jpg | Bin 362909 -> 0 bytes .../hlabel_classification/images/val/8.jpg | Bin 178732 -> 0 bytes .../hlabel_classification/images/val/9.jpg | Bin 370567 -> 0 bytes .../rtmdet_tiny_8xb32-300e_coco.py | 547 - .../annotations/test.json | 121 - .../annotations/train.json | 270 - .../annotations/val.json | 270 - .../images/test/Slide12.jpg | Bin 80723 -> 0 bytes .../images/test/Slide6.jpg | Bin 75048 -> 0 bytes .../images/test/Slide8.jpg | Bin 65874 -> 0 bytes .../images/train/Slide1.jpg | Bin 37943 -> 0 bytes .../images/train/Slide10.jpg | Bin 57545 -> 0 bytes .../images/train/Slide11.jpg | Bin 87567 -> 0 bytes .../images/train/Slide2.jpg | Bin 39505 -> 0 bytes .../images/train/Slide3.jpg | Bin 64358 -> 0 bytes .../images/train/Slide4.jpg | Bin 66701 -> 0 bytes .../images/train/Slide5.jpg | Bin 58410 -> 0 bytes .../images/train/Slide7.jpg | Bin 75183 -> 0 bytes .../images/train/Slide9.jpg | Bin 84714 -> 0 bytes .../images/val/Slide1.jpg | Bin 37943 -> 0 bytes .../images/val/Slide10.jpg | Bin 57545 -> 0 bytes .../images/val/Slide11.jpg | Bin 87567 -> 0 bytes .../images/val/Slide2.jpg | Bin 39505 -> 0 bytes .../images/val/Slide3.jpg | Bin 64358 -> 0 bytes .../images/val/Slide4.jpg | Bin 66701 -> 0 bytes .../images/val/Slide5.jpg | Bin 58410 -> 0 bytes .../images/val/Slide7.jpg | Bin 75183 -> 0 bytes .../images/val/Slide9.jpg | Bin 84714 -> 0 bytes .../annotations/instances_test.json | 4631 ++++++++ .../annotations/instances_train.json | 9380 +++++++++++++++++ .../annotations/instances_val.json | 3323 ++++++ .../small_objects/images/test/sample_0.jpg | Bin 0 -> 283480 bytes .../small_objects/images/test/sample_1.jpg | Bin 0 -> 282541 bytes .../small_objects/images/train/sample_2.jpg | Bin 0 -> 244148 bytes .../small_objects/images/train/sample_3.jpg | Bin 0 -> 297205 bytes .../small_objects/images/train/sample_4.jpg | Bin 0 -> 199305 bytes .../small_objects/images/train/sample_6.jpg | Bin 0 -> 254231 bytes .../small_objects/images/train/sample_7.jpg | Bin 0 -> 274606 bytes .../small_objects/images/train/sample_9.jpg | Bin 0 -> 283614 bytes .../small_objects/images/val/sample_5.jpg | Bin 0 -> 210200 bytes .../small_objects/images/val/sample_8.jpg | Bin 0 -> 314233 bytes tests/assets/unlabeled_dataset/0.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/1.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/2.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/3.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/4.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/5.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/6.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/a/0.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/a/1.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/a/2.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/a/3.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/a/4.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/a/5.jpg | Bin 0 -> 631 bytes tests/assets/unlabeled_dataset/a/6.jpg | Bin 0 -> 631 bytes .../unlabeled_dataset/unlabeled_file_list.txt | 8 + .../voc_dataset1/Annotations/2007_000001.xml | 54 + .../voc_dataset1/ImageSets/Action/test.txt | 1 + .../voc_dataset1/ImageSets/Action/train.txt | 1 + .../voc_dataset1/ImageSets/Layout/test.txt | 1 + .../voc_dataset1/ImageSets/Layout/train.txt | 1 + .../ImageSets/Main/aeroplane_train.txt | 1 + .../ImageSets/Main/background_train.txt | 1 + .../ImageSets/Main/bicycle_train.txt | 1 + .../ImageSets/Main/bird_train.txt | 1 + .../ImageSets/Main/boat_train.txt | 1 + .../ImageSets/Main/bottle_train.txt | 1 + .../voc_dataset1/ImageSets/Main/bus_train.txt | 1 + .../voc_dataset1/ImageSets/Main/car_train.txt | 1 + .../voc_dataset1/ImageSets/Main/cat_train.txt | 1 + .../ImageSets/Main/chair_train.txt | 1 + .../voc_dataset1/ImageSets/Main/cow_train.txt | 1 + .../ImageSets/Main/diningtable_train.txt | 1 + .../voc_dataset1/ImageSets/Main/dog_train.txt | 1 + .../ImageSets/Main/horse_train.txt | 1 + .../ImageSets/Main/ignored_train.txt | 1 + .../ImageSets/Main/motorbike_train.txt | 1 + .../ImageSets/Main/person_train.txt | 1 + .../ImageSets/Main/pottedplant_train.txt | 1 + .../ImageSets/Main/sheep_train.txt | 1 + .../ImageSets/Main/sofa_train.txt | 1 + .../voc_dataset1/ImageSets/Main/test.txt | 1 + .../voc_dataset1/ImageSets/Main/train.txt | 1 + .../ImageSets/Main/train_train.txt | 1 + .../ImageSets/Main/tvmonitor_train.txt | 1 + .../ImageSets/Segmentation/test.txt | 1 + .../ImageSets/Segmentation/train.txt | 1 + .../ImageSets/Segmentation/val.txt | 1 + .../voc_dataset1/JPEGImages/2007_000001.jpg | Bin 0 -> 635 bytes .../voc_dataset1/JPEGImages/2007_000002.jpg | Bin 0 -> 635 bytes .../SegmentationClass/2007_000001.png | Bin 0 -> 97 bytes .../SegmentationObject/2007_000001.png | Bin 0 -> 94 bytes .../voc_dataset2/ImageSets/Main/train.txt | 3 + .../voc_dataset2/JPEGImages/2007_000001.jpg | Bin 0 -> 635 bytes tests/assets/yolo_dataset/obj.data | 4 + tests/assets/yolo_dataset/obj.names | 10 + .../assets/yolo_dataset/obj_train_data/1.jpg | Bin 0 -> 631 bytes .../assets/yolo_dataset/obj_train_data/1.txt | 2 + tests/assets/yolo_dataset/train.txt | 1 + tests/conftest.py | 137 +- tests/e2e/__init__.py | 3 + tests/e2e/cli/__init__.py | 3 + tests/e2e/cli/action/__init__.py | 3 + .../cli/action/test_action_classification.py | 135 + tests/e2e/cli/action/test_action_detection.py | 130 + tests/e2e/cli/anomaly/__init__.py | 3 + .../compressed_model.yml | 5 + .../nncf/nncf_quantization.dot | 240 + .../compressed_model.yml | 5 + .../nncf/nncf_quantization.dot | 430 + .../compressed_model.yml | 5 + .../nncf/nncf_quantization.dot | 240 + .../compressed_model.yml | 5 + .../nncf/nncf_quantization.dot | 430 + .../compressed_model.yml | 5 + .../nncf/nncf_quantization.dot | 240 + .../compressed_model.yml | 5 + .../nncf/nncf_quantization.dot | 430 + .../anomaly/test_anomaly_classification.py | 156 + .../e2e/cli/anomaly/test_anomaly_detection.py | 156 + .../cli/anomaly/test_anomaly_segmentation.py | 156 + tests/e2e/cli/classification/__init__.py | 3 + .../compressed_model.yml | 15 + .../compressed_model.yml | 15 + .../compressed_model.yml | 15 + .../test_api_xai_sanity_classification.py | 135 + .../cli/classification/test_classification.py | 712 ++ tests/e2e/cli/detection/__init__.py | 3 + .../compressed_model.yml | 10 + .../compressed_model.yml | 10 + .../compressed_model.yml | 10 + .../compressed_model.yml | 10 + .../compressed_model.yml | 10 + .../compressed_model.yml | 10 + .../compressed_model.yml | 10 + .../test_api_xai_sanity_detection.py | 171 + tests/e2e/cli/detection/test_detection.py | 356 + .../cli/detection/test_tiling_detection.py | 262 + .../e2e/cli/instance_segmentation/__init__.py | 3 + .../compressed_model.yml | 10 + .../compressed_model.yml | 14 + .../compressed_model.yml | 14 + .../compressed_model.yml | 10 + ...st_api_xai_sanity_instance_segmentation.py | 141 + .../test_instance_segmentation.py | 347 + .../test_tiling_instseg.py | 281 + .../e2e/cli/semantic_segmentation/__init__.py | 3 + .../compressed_model.yml | 5 + .../compressed_model.yml | 5 + .../compressed_model.yml | 5 + .../compressed_model.yml | 5 + .../compressed_model.yml | 5 + .../compressed_model.yml | 5 + .../compressed_model.yml | 5 + .../test_segmentation.py | 335 + tests/e2e/cli/test_cli.py | 102 + .../cli/visual_prompting}/__init__.py | 0 .../compressed_decoder.yml | 3 + .../compressed_image_encoder.yml | 3 + .../compressed_decoder.yml | 3 + .../compressed_image_encoder.yml | 3 + .../compressed_decoder.yml | 3 + .../compressed_image_encoder.yml | 3 + .../compressed_decoder.yml | 3 + .../compressed_image_encoder.yml | 3 + .../visual_prompting/test_visual_prompting.py | 156 + .../cli/visual_prompting/test_zero_shot.py | 129 + tests/e2e/test_api_xai_sanity.py | 440 + tests/fuzzing/cli_fuzzing.py | 1 + tests/fuzzing/helper.py | 2 +- tests/integration/__init__.py | 3 +- tests/integration/api/__init__.py | 3 +- tests/integration/api/action/__init__.py | 3 + .../action/test_api_action_classification.py | 228 + .../api/action/test_api_action_detection.py | 229 + .../api/classification/__init__.py | 3 + .../classification/test_api_classification.py | 294 + tests/integration/api/detection/__init__.py | 3 + .../api/detection/api_detection.py | 100 + .../api/detection/test_api_detection.py | 189 + .../integration/api/segmentation/__init__.py | 3 + .../api/segmentation/test_api_segmentation.py | 308 + .../api/test_auto_configuration.py | 71 - tests/integration/api/test_engine_api.py | 155 - tests/integration/api/test_xai.py | 162 - tests/integration/api/xai/__init__.py | 3 + tests/integration/cli/__init__.py | 3 +- tests/integration/cli/action/__init__.py | 3 + .../cli/action/test_action_classification.py | 143 + .../cli/action/test_action_detection.py | 109 + tests/integration/cli/anomaly/__init__.py | 3 + .../anomaly/test_anomaly_classification.py | 88 + .../cli/anomaly/test_anomaly_detection.py | 88 + .../cli/anomaly/test_anomaly_segmentation.py | 88 + .../cli/classification/__init__.py | 3 + .../cli/classification/test_classification.py | 535 + tests/integration/cli/detection/__init__.py | 3 + .../cli/detection/test_detection.py | 291 + .../cli/detection/test_tiling_detection.py | 129 + .../cli/instance_segmentation/__init__.py | 3 + .../test_instance_segmentation.py | 219 + .../test_rotated_detection.py | 45 + .../test_tiling_instseg.py | 167 + .../cli/semantic_segmentation/__init__.py | 3 + .../test_segmentation.py | 247 + .../cli/test_auto_configuration.py | 68 - tests/integration/cli/test_cli.py | 788 +- .../integration/cli/test_export_inference.py | 300 - tests/integration/cli/utils.py | 37 - .../cli/visual_prompting/__init__.py | 3 + .../visual_prompting/test_visual_prompting.py | 132 + .../cli/visual_prompting/test_zero_shot.py | 86 + tests/integration/conftest.py | 163 - tests/integration/detection/conftest.py | 67 - .../integration/detection/test_data_module.py | 11 - tests/integration/detection/test_model.py | 29 - tests/integration/test_tiling.py | 149 - tests/perf/__init__.py | 6 +- tests/perf/benchmark-reference.csv | 100 + tests/perf/benchmark.py | 391 +- tests/perf/conftest.py | 390 +- tests/perf/test_anomaly.py | 223 +- tests/perf/test_classification.py | 507 +- tests/perf/test_detection.py | 192 +- tests/perf/test_instance_segmentation.py | 361 +- tests/perf/test_semantic_segmentation.py | 184 +- tests/perf/test_visual_prompting.py | 151 +- tests/pytest.ini | 7 + tests/regression/__init__.py | 3 +- .../action/test_action_classification.py | 175 + .../action/test_action_detection.py | 115 + .../anomaly/test_anomaly_classificaiton.py | 274 + .../anomaly/test_anomaly_detection.py | 276 + .../anomaly/test_anomaly_segmentation.py | 276 + .../classification/test_classification.py | 1032 ++ tests/regression/conftest.py | 159 +- tests/regression/detection/test_detection.py | 374 + .../detection/test_tiling_detection.py | 248 + .../test_instance_segmentation.py | 308 + .../test_tiling_instance_segmentation.py | 254 + tests/regression/regression_command.py | 327 + tests/regression/regression_config.json | 1724 +++ tests/regression/regression_test_helpers.py | 240 + .../test_segmentation.py | 549 + tests/regression/summarize_test_results.py | 268 + tests/regression/test_regression.py | 847 -- tests/test_helpers.py | 436 + tests/test_suite/ARCHITECTURE.md | 1451 +++ tests/test_suite/QUICK_HOWTO.md | 185 + tests/test_suite/__init__.py | 0 tests/test_suite/e2e_test_system.py | 151 + tests/test_suite/fixtures.py | 395 + tests/test_suite/logging.py | 35 + tests/test_suite/pytest_insertions.py | 140 + tests/test_suite/run_test_command.py | 1272 +++ tests/test_suite/training_test_case.py | 120 + tests/test_suite/training_tests_actions.py | 799 ++ tests/test_suite/training_tests_common.py | 63 + tests/test_suite/training_tests_helper.py | 330 + tests/test_suite/training_tests_stage.py | 431 + tests/unit/__init__.py | 3 +- tests/unit/algo/__init__.py | 3 - .../algo/action_classification/__init__.py | 3 - .../backbones/__init__.py | 3 - .../backbones/test_movinet.py | 17 - .../action_classification/heads/__init__.py | 3 - .../heads/test_movinet_head.py | 25 - .../recognizers/__init__.py | 3 - .../recognizers/test_movinet_recognizer.py | 27 - .../test_adaptive_train_scheduling.py | 57 - .../algo/callbacks/test_iteration_timer.py | 84 - tests/unit/algo/classification/__init__.py | 2 - .../algo/classification/backbones/__init__.py | 2 - .../backbones/test_otx_efficientnet.py | 15 - .../backbones/test_otx_efficientnet_v2.py | 17 - .../backbones/test_otx_mobilenet_v3.py | 20 - tests/unit/algo/classification/conftest.py | 172 - .../algo/classification/heads/__init__.py | 2 - .../heads/test_custom_hlabel_cls_head.py | 122 - .../heads/test_custom_multilabel_cls_head.py | 87 - .../algo/classification/losses/__init__.py | 2 - .../losses/test_asymmetric_multilabel.py | 35 - .../algo/classification/test_otx_dino_v2.py | 37 - .../classification/test_torchvision_model.py | 70 - tests/unit/algo/detection/__init__.py | 3 - .../unit/algo/detection/backbones/__init__.py | 3 - .../backbones/test_pytorchcv_backbones.py | 103 - tests/unit/algo/detection/heads/__init__.py | 3 - .../heads/test_class_incremental_mixin.py | 79 - .../heads/test_custom_anchor_generator.py | 27 - .../detection/heads/test_custom_ssd_head.py | 24 - tests/unit/algo/detection/test_ssd.py | 26 - tests/unit/algo/hooks/__init__.py | 2 - .../algo/hooks/test_saliency_map_dumping.py | 64 - .../hooks/test_saliency_map_processing.py | 162 - tests/unit/algo/hooks/test_xai_hooks.py | 142 - .../algo/instance_segmentation/__init__.py | 3 - .../instance_segmentation/heads/__init__.py | 3 - .../heads/test_custom_roi_head.py | 95 - .../instance_segmentation/test_evaluation.py | 32 - tests/unit/algo/samplers/__init__.py | 2 - .../algo/samplers/test_balanced_sampler.py | 97 - .../test_class_incremental_sampler.py | 114 - tests/unit/algo/segmentation/__init__.py | 3 - .../algo/segmentation/backbones/__init__.py | 3 - .../algo/segmentation/backbones/litehrnet.py | 113 - .../unit/algo/segmentation/heads/__init__.py | 3 - .../heads/test_class_incremental_mixin.py | 40 - tests/unit/algo/utils/__init__.py | 4 - tests/unit/algo/utils/test_support_otx_v1.py | 229 - tests/unit/algo/visual_prompting/__init__.py | 3 - .../visual_prompting/backbones/__init__.py | 3 - .../backbones/test_tiny_vit.py | 174 - .../visual_prompting/backbones/test_vit.py | 162 - tests/unit/algo/visual_prompting/conftest.py | 122 - .../visual_prompting/decoders/__init__.py | 3 - .../decoders/test_sam_mask_decoder.py | 276 - .../visual_prompting/encoders/__init__.py | 3 - .../encoders/test_sam_image_encoder.py | 19 - .../encoders/test_sam_prompt_encoder.py | 147 - .../visual_prompting/test_openvino_models.py | 150 - .../visual_prompting/test_segment_anything.py | 352 - .../test_zero_shot_segment_anything.py | 750 -- .../algo/visual_prompting/utils/__init__.py | 3 - .../utils/test_layer_norm_2d.py | 22 - tests/unit/algorithms/__init__.py | 3 + tests/unit/algorithms/action/__init__.py | 13 + .../algorithms/action/adapters/__init__.py | 3 + .../action/adapters/mmaction/__init__.py | 3 + .../action/adapters/mmaction/data/__init__.py | 3 + .../mmaction/data/pipelines/__init__.py | 3 + .../data/pipelines/test_action_loading.py | 64 + .../mmaction/data/test_action_cls_dataset.py | 91 + .../mmaction/data/test_action_det_dataset.py | 142 + .../adapters/mmaction/models/__init__.py | 3 + .../mmaction/models/backbones/__init__.py | 3 + .../models/backbones/test_action_movinet.py | 203 + .../test_action_register_backbone.py | 21 + .../mmaction/models/detectors/__init__.py | 3 + .../models/detectors/test_action_fast_rcnn.py | 195 + .../mmaction/models/heads/__init__.py | 3 + .../models/heads/test_action_movinet_head.py | 32 + .../models/heads/test_action_roi_head.py | 72 + .../mmaction/models/recognizers/__init__.py | 3 + .../test_action_movinet_recognizer.py | 67 + .../action/adapters/mmaction/test_task.py | 450 + .../adapters/mmaction/utils/__init__.py | 3 + .../utils/test_action_det_eval_utils.py | 74 + .../utils/test_action_export_utils.py | 178 + .../action/adapters/openvino/__init__.py | 3 + .../openvino/test_action_dataloader.py | 335 + .../openvino/test_action_openvino_models.py | 224 + .../action/adapters/openvino/test_task.py | 395 + tests/unit/algorithms/action/test_helpers.py | 143 + .../unit/algorithms/action/tools/__init__.py | 3 + .../test_action_sample_classification.py | 120 + .../tools/test_action_sample_detection.py | 121 + .../unit/algorithms/action/utils/__init__.py | 3 + ...test_action_convert_public_data_to_cvat.py | 176 + .../action/utils/test_action_data.py | 142 + tests/unit/algorithms/anomaly/__init__.py | 3 + .../algorithms/anomaly/adapters/__init__.py | 3 + .../anomalib/accelerators/__init__.py | 5 + .../adapters/anomalib/accelerators/xpu.py | 59 + .../adapters/anomalib/plugins/__init__.py | 5 + .../anomalib/plugins/xpu_precision.py | 58 + .../adapters/anomalib/strategies/__init__.py | 5 + .../anomalib/strategies/test_xpu_single.py | 48 + .../anomaly/adapters/callbacks/__init__.py | 3 + .../callbacks/test_inference_callback.py | 35 + .../callbacks/test_progress_callback.py | 51 + .../anomaly/adapters/data/__init__.py | 3 + .../anomaly/adapters/data/test_dataset.py | 34 + .../algorithms/anomaly/config/__init__.py | 3 + .../anomaly/config/test_model_config_load.py | 65 + tests/unit/algorithms/anomaly/conftest.py | 40 + .../algorithms/anomaly/helpers/__init__.py | 4 + .../anomaly/helpers/dummy_dataset.py | 179 + .../algorithms/anomaly/helpers/dummy_model.py | 41 + .../unit/algorithms/anomaly/helpers/utils.py | 40 + .../unit/algorithms/anomaly/tasks/__init__.py | 4 + .../anomaly/tasks/test_inference.py | 86 + .../algorithms/anomaly/tasks/test_nncf.py | 52 + .../algorithms/anomaly/tasks/test_openvino.py | 143 + .../algorithms/anomaly/tasks/test_train.py | 50 + .../algorithms/classification/__init__.py | 13 + .../classification/adapters/__init__.py | 13 + .../classification/adapters/mmcls/__init__.py | 4 + .../adapters/mmcls/api/__init__.py | 5 + .../adapters/mmcls/api/test_train.py | 120 + .../adapters/mmcls/data/__init__.py | 4 + .../adapters/mmcls/data/test_datasets.py | 173 + .../adapters/mmcls/data/test_pipelines.py | 209 + .../adapters/mmcls/models/__init__.py | 3 + .../mmcls/models/classifiers/__init__.py | 4 + .../mmcls/models/classifiers/test_byol.py | 137 + .../test_custom_image_classifier.py | 224 + .../classifiers/test_semisl_classifier.py | 31 + .../classifiers/test_semisl_mlc_classifier.py | 32 + .../classifiers/test_supcon_classifier.py | 26 + .../adapters/mmcls/models/heads/__init__.py | 4 + .../models/heads/test_contrastive_head.py | 51 + .../models/heads/test_custom_cls_head.py | 58 + .../test_custom_hierarchical_cls_head.py | 104 + .../heads/test_custom_multilabel_cls_head.py | 67 + .../models/heads/test_multilabel_semisl.py | 76 + .../models/heads/test_semisl_cls_head.py | 119 + .../adapters/mmcls/models/heads/text_mixin.py | 21 + .../adapters/mmcls/models/losses/__init__.py | 4 + .../losses/test_asymmetric_multilabel.py | 70 + .../mmcls/models/losses/test_cross_entropy.py | 45 + .../adapters/mmcls/models/necks/__init__.py | 4 + .../mmcls/models/necks/test_selfsl_mlp.py | 153 + .../adapters/mmcls/nncf/__init__.py | 3 + .../mmcls/nncf/test_mmcls_nncf_builder.py | 58 + .../mmcls/nncf/test_mmcls_nncf_patches.py | 14 + .../mmcls/nncf/test_mmcls_nncf_registers.py | 16 + .../adapters/mmcls/optimizer/__init__.py | 4 + .../adapters/mmcls/optimizer/test_lars.py | 70 + .../adapters/mmcls/test_cls_config_builder.py | 39 + .../adapters/mmcls/test_configurer.py | 337 + .../adapters/mmcls/test_task.py | 530 + .../adapters/openvino/__init__.py | 3 + .../adapters/openvino/test_openvino_models.py | 6 + .../algorithms/classification/conftest.py | 36 + .../classification/tasks/__init__.py | 3 + .../tasks/test_classification_nncf.py | 76 + .../test_classification_openvino_task.py | 242 + .../algorithms/classification/test_helper.py | 188 + .../test_xai_classification_validity.py | 130 + .../classification/utils/__init__.py | 3 + .../classification/utils/test_utils.py | 99 + tests/unit/algorithms/common/__init__.py | 4 + .../algorithms/common/adapters/__init__.py | 4 + .../common/adapters/mmcv/__init__.py | 4 + .../hooks/test_adaptive_repeat_data_hook.py | 37 + .../hooks/test_adaptive_training_hooks.py | 92 + .../mmcv/hooks/test_cancel_interface_hook.py | 56 + .../mmcv/hooks/test_checkpoint_hook.py | 63 + .../hooks/test_composed_dataloader_hook.py | 20 + .../mmcv/hooks/test_early_stopping_hook.py | 275 + .../adapters/mmcv/hooks/test_ema_v2_hook.py | 32 + .../adapters/mmcv/hooks/test_eval_hook.py | 215 + .../hooks/test_fp16_sam_optimizer_hook.py | 20 + .../common/adapters/mmcv/hooks/test_hooks.py | 196 + .../adapters/mmcv/hooks/test_ib_loss_hook.py | 18 + .../mmcv/hooks/test_logger_replace_hook.py | 18 + .../mmcv/hooks/test_mean_teacher_hook.py | 20 + .../mmcv/hooks/test_model_ema_hook.py | 126 + .../mmcv/hooks/test_no_bias_decay_hook.py | 53 + .../hooks/test_recording_forward_hooks.py | 144 + .../hooks/test_save_initial_weight_hook.py | 18 + .../mmcv/hooks/test_semisl_cls_hook.py | 18 + .../mmcv/hooks/test_task_adapt_hook.py | 18 + .../mmcv/hooks/test_xpu_optimizer_hook.py | 18 + .../common/adapters/mmcv/nncf/__init__.py | 3 + .../common/adapters/mmcv/nncf/test_helpers.py | 310 + .../mmcv/nncf/test_mmcv_nncf_hooks.py | 53 + .../mmcv/nncf/test_mmcv_nncf_runners.py | 23 + .../mmcv/nncf/test_mmcv_nncf_utils.py | 173 + .../common/adapters/mmcv/ops/__init__.py | 4 + ...est_multi_scale_deformable_attn_pytorch.py | 20 + .../adapters/mmcv/pipelines/__init__.py | 4 + .../test_load_image_from_otx_dataset.py | 203 + .../mmcv/pipelines/transforms/__init__.py | 4 + .../pipelines/transforms/test_augments.py | 102 + .../mmcv/pipelines/transforms/test_augmix.py | 118 + .../transforms/test_otx_transforms.py | 56 + .../transforms/test_random_augment.py | 79 + .../transforms/test_twocrop_transform.py | 45 + .../adapters/mmcv/tasks/test_exporter.py | 76 + .../adapters/mmcv/tasks/test_helpers.py | 33 + .../adapters/mmcv/tasks/test_version.py | 9 + .../common/adapters/mmcv/test_configurer.py | 70 + .../common/adapters/mmcv/utils/__init__.py | 4 + .../adapters/mmcv/utils/test_automatic_bs.py | 160 + .../adapters/mmcv/utils/test_config_utils.py | 506 + .../adapters/mmcv/utils/test_fp16_utils.py | 98 + .../common/adapters/mmdeploy/__init__.py | 3 + .../common/adapters/mmdeploy/ops/__init__.py | 4 + .../adapters/mmdeploy/ops/test_custom_ops.py | 54 + .../adapters/mmdeploy/test_deploy_apis.py | 220 + .../common/adapters/mmdeploy/test_helpers.py | 87 + .../utils/test_deploy_utils_mmdeploy.py | 42 + .../mmdeploy/utils/test_deploy_utils_onnx.py | 67 + .../test_deploy_utils_operations_domain.py | 14 + .../mmdeploy/utils/test_deploy_utils_utils.py | 76 + .../common/adapters/nncf/__init__.py | 3 + .../adapters/nncf/test_nncf_compression.py | 96 + .../common/adapters/nncf/test_nncf_config.py | 116 + .../common/adapters/nncf/test_nncf_patches.py | 40 + .../common/adapters/nncf/test_nncf_utils.py | 100 + .../common/adapters/torch/__init__.py | 4 + .../torch/amp/test_xpu_grad_scaler.py | 39 + .../adapters/torch/dataloaders/__init__.py | 4 + .../samplers/test_balanced_sampler.py | 51 + .../samplers/test_cls_incr_sampler.py | 57 + .../dataloaders/samplers/test_otx_sampler.py | 31 + .../common/adapters/torch/utils/__init__.py | 4 + .../torch/utils/test_bs_search_algo.py | 241 + .../common/adapters/torch/utils/test_utils.py | 48 + .../common/configs}/__init__.py | 0 .../configs/test_configuration_enums.py | 38 + .../unit/algorithms/common/utils/__init__.py | 3 + .../unit/algorithms/common/utils/test_data.py | 107 + .../common/utils/test_dist_utils.py | 23 + .../algorithms/common/utils/test_utils.py | 28 + tests/unit/algorithms/detection/__init__.py | 13 + .../algorithms/detection/adapters/__init__.py | 3 + .../detection/adapters/mmdet/__init__.py | 3 + .../detection/adapters/mmdet/api/__init__.py | 5 + .../adapters/mmdet/api/test_train.py | 111 + .../adapters/mmdet/datasets/__init__.py | 3 + .../mmdet/datasets/pipelines/__init__.py | 3 + .../datasets/pipelines/test_load_pipelines.py | 127 + .../pipelines/test_torchvision2mmdet.py | 188 + .../mmdet/datasets/test_detection_dataset.py | 236 + .../adapters/mmdet/hooks/__init__.py | 3 + .../test_det_class_probability_map_hook.py | 100 + .../mmdet/hooks/test_tile_sampling_hook.py | 47 + .../adapters/mmdet/models/__init__.py | 3 + .../assigners/test_custom_max_iou_assigner.py | 57 + .../mmdet/models/backones/__init__.py | 3 + .../backones/test_ov_mmdet_mmov_backbone.py | 44 + .../mmdet/models/dense_heads/__init__.py | 3 + .../mmdet/models/dense_heads/conftest.py | 186 + .../test_loss_dynamics_tracking_heads.py | 71 + .../test_ov_mmdet_mmov_rpn_head.py | 68 + .../test_ov_mmdet_mmov_ssd_head.py | 85 + .../test_ov_mmdet_mmov_yolov3_head.py | 94 + .../mmdet/models/detectors/__init__.py | 3 + .../mmdet/models/detectors/conftest.py | 487 + .../detectors/test_custom_atss_detector.py | 74 + .../test_custom_deformable_detr_detector.py | 77 + .../detectors/test_custom_dino_detector.py | 57 + .../test_custom_single_stage_detector.py | 129 + .../test_custom_two_stage_detector.py | 261 + .../detectors/test_custom_vfnet_detector.py | 74 + .../detectors/test_loss_dynamics_tracking.py | 116 + .../models/detectors/test_mean_teacher.py | 114 + .../adapters/mmdet/models/heads/__init__.py | 4 + .../models/heads/test_custom_dino_head.py | 222 + .../heads/test_custom_lite_dino_head.py | 244 + .../adapters/mmdet/models/losses/__init__.py | 3 + .../models/losses/test_cross_focal_loss.py | 63 + .../mmdet/models/losses/test_l2sp_loss.py | 93 + .../adapters/mmdet/models/necks/__init__.py | 3 + .../necks/test_ov_mmdet_mmov_ssd_neck.py | 52 + .../mmdet/models/roi_heads/__init__.py | 3 + .../roi_heads/roi_extractors/__init__.py | 3 + ...est_ov_mmdet_single_level_roi_extractor.py | 20 + .../detection/adapters/mmdet/nncf/__init__.py | 3 + .../mmdet/nncf/test_mmdet_nncf_builder.py | 97 + .../mmdet/nncf/test_mmdet_nncf_patches.py | 14 + .../adapters/mmdet/nncf/test_task.py | 130 + .../adapters/mmdet/test_configurer.py | 426 + .../detection/adapters/mmdet/test_task.py | 557 + .../adapters/mmdet/utils/__init__.py | 3 + .../mmdet/utils/test_detection_builder.py | 24 + .../utils/test_detection_config_utils.py | 83 + .../adapters/mmdet/utils/test_exporter.py | 51 + .../detection/adapters/openvino/__init__.py | 3 + .../openvino/model_wrappers/__init__.py | 3 + .../detection/adapters/openvino/test_task.py | 244 + tests/unit/algorithms/detection/conftest.py | 23 + .../unit/algorithms/detection/test_helpers.py | 150 + .../detection/test_xai_detection_validity.py | 129 + .../algorithms/detection/tiling/__init__.py | 3 + .../detection/tiling/test_tiling_detection.py | 452 + .../tiling/test_tiling_tile_classifier.py | 213 + .../algorithms/detection/utils/__init__.py | 3 + .../detection/utils/test_detection_data.py | 86 + .../utils/test_detection_mask_to_bbox.py | 27 + .../detection/utils/test_detection_utils.py | 40 + .../unit/algorithms/segmentation/__init__.py | 13 + .../segmentation/adapters/__init__.py | 13 + .../segmentation/adapters/mmseg/__init__.py | 4 + .../adapters/mmseg/api/__init__.py | 5 + .../adapters/mmseg/api/test_train.py | 116 + .../adapters/mmseg/datasets/__init__.py | 4 + .../mmseg/datasets/pipelines/__init__.py | 4 + .../mmseg/datasets/pipelines/test_compose.py | 151 + .../mmseg/datasets/pipelines/test_loads.py | 98 + .../datasets/pipelines/test_transforms.py | 313 + .../adapters/mmseg/datasets/test_dataset.py | 102 + .../adapters/mmseg/models/__init__.py | 4 + .../mmseg/models/backbones/__init__.py | 4 + .../mmseg/models/backbones/test_litehrnet.py | 101 + .../backbones/test_mmseg_mmov_backbone.py | 48 + .../adapters/mmseg/models/heads/__init__.py | 4 + .../mmseg/models/heads/test_detcon_head.py | 64 + .../heads/test_mmseg_mmov_decode_head.py | 66 + .../mmseg/models/heads/test_prototype_head.py | 31 + .../adapters/mmseg/models/losses/__init__.py | 4 + .../test_cross_entropy_loss_with_ignore.py | 46 + .../mmseg/models/losses/test_detcon_loss.py | 63 + .../losses/test_pixel_prototype_ce_loss.py | 24 + .../adapters/mmseg/models/necks/__init__.py | 4 + .../mmseg/models/necks/test_selfsl_mlp.py | 153 + .../models/scalar_schedulers/__init__.py | 4 + .../scalar_schedulers/test_schedulers.py | 128 + .../mmseg/models/segmentors/__init__.py | 4 + .../mmseg/models/segmentors/test_detcon.py | 272 + .../segmentors/test_mean_teacher_segmentor.py | 51 + .../adapters/mmseg/models/utils/test_utils.py | 29 + .../adapters/mmseg/nncf/__init__.py | 3 + .../mmseg/nncf/test_mmseg_nncf_builder.py | 55 + .../mmseg/nncf/test_mmseg_nncf_patches.py | 14 + .../mmseg/nncf/test_mmseg_nncf_task.py | 64 + .../adapters/mmseg/test_mmseg_configurer.py | 339 + .../adapters/mmseg/utils/__init__.py | 4 + .../adapters/mmseg/utils/test_builder.py | 26 + .../adapters/mmseg/utils/test_data_utils.py | 89 + .../adapters/mmseg/utils/test_exporter.py | 36 + .../adapters/openvino/__init__.py | 3 + .../openvino/model_wrappers/__init__.py | 3 + .../adapters/openvino/test_openvino_task.py | 193 + .../adapters/test_otx_segmentation_task.py | 87 + .../unit/algorithms/segmentation/conftest.py | 15 + .../algorithms/segmentation/test_helpers.py | 105 + .../unit/algorithms/segmentation/test_task.py | 134 + .../algorithms/visual_prompting/__init__.py | 3 + .../openvino/model_wrappers/__init__.py | 3 + .../model_wrappers/test_openvino_models.py | 155 + .../pytorch_lightning/callbacks/__init__.py | 3 + .../callbacks/test_inference_callback.py | 179 + .../pytorch_lightning/config/__init__.py | 3 + .../config/test_visual_prompting_config.py | 82 + .../pytorch_lightning/datasets/__init__.py | 3 + .../datasets/pipelines/__init__.py | 3 + .../datasets/pipelines/test_sam_transforms.py | 97 + .../datasets/pipelines/test_transforms.py | 182 + .../datasets/test_dataset.py | 316 + .../pytorch_lightning/models/__init__.py | 3 + .../models/backbones/__init__.py | 3 + .../models/backbones/test_tiny_vit.py | 237 + .../models/backbones/test_vit.py | 198 + .../models/decoders/__init__.py | 3 + .../models/decoders/test_sam_mask_decoder.py | 263 + .../models/encoders/__init__.py | 3 + .../models/encoders/test_sam_image_encoder.py | 43 + .../encoders/test_sam_prompt_encoder.py | 176 + .../models/visual_prompters/__init__.py | 3 + .../visual_prompters/test_segment_anything.py | 567 + .../test_zero_shot_segment_anything.py | 536 + .../visual_prompting/tasks/__init__.py | 3 + .../visual_prompting/tasks/test_inference.py | 307 + .../visual_prompting/tasks/test_openvino.py | 916 ++ .../visual_prompting/tasks/test_train.py | 47 + .../visual_prompting/test_helpers.py | 217 + tests/unit/api/__init__.py | 3 + tests/unit/api/configuration/__init__.py | 0 .../configuration/dummy_broken_config.yaml | 28 + tests/unit/api/configuration/dummy_config.py | 185 + .../unit/api/configuration/dummy_config.yaml | 134 + .../elements/test_elements_utils.py | 399 + .../elements/test_metadata_keys.py | 136 + .../elements/test_primitive_parameters.py | 619 ++ .../enums/test_config_element_type.py | 100 + .../configuration/enums/test_enum_utils.py | 39 + .../enums/test_model_lifecycle.py | 36 + .../helper/test_config_element_mapping.py | 140 + .../api/configuration/helper/test_create.py | 640 ++ .../configuration/helper/test_helper_utils.py | 354 + .../test_configurable_parameters.py | 215 + .../test_configuration_helper.py | 464 + .../configuration/test_model_configuration.py | 31 + tests/unit/api/constants/__init__.py | 3 + tests/unit/api/constants/components.py | 25 + tests/unit/api/constants/requirements.py | 15 + tests/unit/api/entities/dummy_config.yaml | 148 + tests/unit/api/entities/dummy_template.yaml | 34 + .../interfaces/test_graph_interface.py | 52 + .../unit/api/entities/shapes/test_ellipse.py | 315 + .../unit/api/entities/shapes/test_polygon.py | 325 + .../api/entities/shapes/test_rectangle.py | 656 ++ tests/unit/api/entities/shapes/test_shape.py | 362 + tests/unit/api/entities/test_annotation.py | 598 ++ tests/unit/api/entities/test_color.py | 89 + tests/unit/api/entities/test_coordinate.py | 89 + tests/unit/api/entities/test_dataset_item.py | 994 ++ tests/unit/api/entities/test_datasets.py | 672 ++ tests/unit/api/entities/test_graph.py | 992 ++ tests/unit/api/entities/test_id.py | 48 + tests/unit/api/entities/test_image.py | 143 + .../api/entities/test_inference_parameters.py | 92 + tests/unit/api/entities/test_label.py | 182 + tests/unit/api/entities/test_label_schema.py | 1939 ++++ tests/unit/api/entities/test_media.py | 81 + tests/unit/api/entities/test_metadata.py | 237 + tests/unit/api/entities/test_metrics.py | 1061 ++ tests/unit/api/entities/test_model.py | 393 + .../unit/api/entities/test_model_template.py | 1187 +++ .../entities/test_optimization_parameters.py | 99 + tests/unit/api/entities/test_pickle.py | 41 + tests/unit/api/entities/test_result_media.py | 247 + tests/unit/api/entities/test_resultset.py | 129 + tests/unit/api/entities/test_scored_label.py | 75 + tests/unit/api/entities/test_subset.py | 126 + .../api/entities/test_task_environment.py | 409 + tests/unit/api/entities/test_tensor.py | 142 + .../api/entities/test_train_parameters.py | 51 + tests/unit/api/fixtures/__init__.py | 3 + tests/unit/api/fixtures/general.py | 20 + .../validation_helper.py | 85 + .../api/serialization/test_datetime_mapper.py | 34 + .../unit/api/serialization/test_id_mapper.py | 41 + .../api/serialization/test_label_mapper.py | 354 + .../usecases/adapters/test_model_adapter.py | 183 + .../api/usecases/evaluation/test_accuracy.py | 638 ++ .../evaluation/test_basic_operations.py | 221 + .../unit/api/usecases/evaluation/test_dice.py | 547 + .../api/usecases/evaluation/test_f_measure.py | 1739 +++ ...test_prediction_to_annotation_converter.py | 947 ++ .../usecases/exportable_code/test_streamer.py | 353 + .../exportable_code/test_visualization.py | 188 + .../api/usecases/reporting/test_callback.py | 76 + .../reporting/test_time_monitor_callback.py | 449 + .../tasks/interfaces/test_interfaces.py | 289 + .../unit/api/utils/test_segmentation_utils.py | 566 + tests/unit/api/utils/test_shape_drawer.py | 1315 +++ tests/unit/api/utils/test_shape_factory.py | 174 + tests/unit/cli/__init__.py | 2 - tests/unit/cli/builder/test_cli_builder.py | 297 + tests/unit/cli/conftest.py | 9 + tests/unit/cli/manager/test_config_manager.py | 677 ++ tests/unit/cli/registry/test_cli_registry.py | 108 + tests/unit/cli/test_cli.py | 185 - tests/unit/cli/test_install.py | 59 - tests/unit/cli/tools/test_build.py | 103 + tests/unit/cli/tools/test_cli.py | 44 + tests/unit/cli/tools/test_deploy.py | 103 + tests/unit/cli/tools/test_eval.py | 142 + tests/unit/cli/tools/test_export.py | 98 + tests/unit/cli/tools/test_find.py | 72 + tests/unit/cli/tools/test_optimize.py | 134 + tests/unit/cli/tools/test_train.py | 134 + tests/unit/cli/utils/__init__.py | 2 - tests/unit/cli/utils/test_config.py | 85 + tests/unit/cli/utils/test_experiment.py | 306 + tests/unit/cli/utils/test_help_formatter.py | 138 - tests/unit/cli/utils/test_hpo.py | 836 ++ tests/unit/cli/utils/test_importing.py | 60 + tests/unit/cli/utils/test_installation.py | 250 - tests/unit/cli/utils/test_io.py | 284 + tests/unit/cli/utils/test_jsonargparse.py | 248 - tests/unit/cli/utils/test_multi_gpu.py | 441 + tests/unit/cli/utils/test_nncf.py | 46 + tests/unit/cli/utils/test_parser.py | 253 + tests/unit/cli/utils/test_report.py | 86 + tests/unit/cli/utils/test_telemetry.py | 133 + tests/unit/conftest.py | 15 + tests/unit/core/__init__.py | 1 + tests/unit/core/config/test_resolver.py | 27 - tests/unit/core/conftest.py | 277 - tests/unit/core/data/__init__.py | 1 + .../core/data/adapter/test_action_adapter.py | 88 + .../core/data/adapter/test_anomaly_adapter.py | 124 + .../adapter/test_classification_adapter.py | 173 + .../data/adapter/test_detection_adapter.py | 173 + tests/unit/core/data/adapter/test_init.py | 153 + .../data/adapter/test_segmentation_adapter.py | 157 + .../adapter/test_visual_prompting_adapter.py | 62 + tests/unit/core/data/conftest.py | 90 - tests/unit/core/data/dataset/__init__.py | 3 - .../core/data/dataset/test_classification.py | 22 - .../data/dataset/test_visual_prompting.py | 115 - tests/unit/core/data/entity/__init__.py | 3 - tests/unit/core/data/entity/conftest.py | 41 - tests/unit/core/data/entity/test_base.py | 159 - tests/unit/core/data/entity/test_detection.py | 61 - .../core/data/entity/test_visual_prompting.py | 139 - .../core/data/manager/test_dataset_manager.py | 118 + tests/unit/core/data/test_caching.py | 91 + tests/unit/core/data/test_dataset.py | 73 - tests/unit/core/data/test_factory.py | 70 - tests/unit/core/data/test_helpers.py | 175 + tests/unit/core/data/test_mem_cache.py | 125 - tests/unit/core/data/test_module.py | 122 - tests/unit/core/data/test_pre_filtering.py | 60 - tests/unit/core/data/test_storage_caching.py | 132 + tests/unit/core/data/test_transform_libs.py | 213 - .../unit/core/data/transform_libs/__init__.py | 3 - .../core/data/transform_libs/test_mmcv.py | 49 - .../core/data/transform_libs/test_mmdet.py | 133 - .../core/data/transform_libs/test_mmseg.py | 39 - tests/unit/core/metrics/__init__.py | 2 - tests/unit/core/metrics/test_accuracy.py | 115 - tests/unit/core/metrics/test_fmeasure.py | 56 - tests/unit/core/model/__init__.py | 3 - tests/unit/core/model/entity/__init__.py | 3 - tests/unit/core/model/entity/test_base.py | 73 - .../core/model/entity/test_segmentation.py | 61 - .../model/entity/test_visual_prompting.py | 620 -- tests/unit/core/model/module/__init__.py | 3 - tests/unit/core/model/module/test_base.py | 70 - .../unit/core/model/module/test_detection.py | 92 - .../core/model/module/test_segmentation.py | 69 - tests/unit/core/ov/__init__.py | 3 + .../graph/parsers/test_ov_graph_cls_parser.py | 30 + .../ov/graph/parsers/test_ov_graph_parser.py | 29 + .../unit/core/ov/graph/test_ov_graph_grapy.py | 183 + .../unit/core/ov/graph/test_ov_graph_utils.py | 59 + tests/unit/core/ov/models/__init__.py | 13 + tests/unit/core/ov/models/mmcls/__init__.py | 13 + .../backbones/test_ov_mmcls_mmov_backbone.py | 43 + .../mmcls/heads/test_ov_mmcls_cls_head.py | 31 + .../mmcls/heads/test_ov_mmcls_conv_head.py | 40 + .../heads/test_ov_mmcls_mmcv_cls_head.py | 42 + .../mmcls/necks/test_ov_mmcls_mmov_neck.py | 22 + .../unit/core/ov/models/mmcls/test_helpers.py | 25 + .../core/ov/models/test_ov_models_ov_model.py | 45 + .../core/ov/ops/test_ov_ops_activations.py | 263 + .../core/ov/ops/test_ov_ops_arithmetics.py | 158 + tests/unit/core/ov/ops/test_ov_ops_builder.py | 54 + .../core/ov/ops/test_ov_ops_convolutions.py | 126 + .../core/ov/ops/test_ov_ops_generation.py | 30 + .../ov/ops/test_ov_ops_image_processings.py | 98 + .../ov/ops/test_ov_ops_infrastructures.py | 109 + tests/unit/core/ov/ops/test_ov_ops_matmuls.py | 54 + tests/unit/core/ov/ops/test_ov_ops_module.py | 48 + .../unit/core/ov/ops/test_ov_ops_movements.py | 295 + .../core/ov/ops/test_ov_ops_normalizations.py | 149 + .../ov/ops/test_ov_ops_object_detections.py | 133 + tests/unit/core/ov/ops/test_ov_ops_op.py | 24 + .../unit/core/ov/ops/test_ov_ops_poolings.py | 147 + .../core/ov/ops/test_ov_ops_reductions.py | 132 + .../ov/ops/test_ov_ops_shape_manipulations.py | 103 + .../ops/test_ov_ops_sorting_maximization.py | 47 + .../ov/ops/test_ov_ops_type_conversions.py | 55 + tests/unit/core/ov/ops/test_ov_ops_utils.py | 37 + tests/unit/core/ov/test_ov_omz_wrapper.py | 42 + tests/unit/core/ov/test_ov_registry.py | 33 + tests/unit/core/ov/test_ov_utils.py | 78 + tests/unit/core/test_core_patcher.py | 361 + tests/unit/core/utils/__init__.py | 3 - tests/unit/core/utils/test_mask_utils.py | 20 - tests/unit/core/utils/test_tile.py | 41 - tests/unit/core/utils/test_utils.py | 26 - tests/unit/engine/__init__.py | 2 - tests/unit/engine/utils/__init__.py | 2 - tests/unit/engine/utils/test_api.py | 55 - .../engine/utils/test_auto_configurator.py | 142 - tests/unit/hpo/__init__.py | 2 - tests/unit/hpo/test_hpo_base.py | 60 +- tests/unit/hpo/test_hyperband.py | 264 +- tests/unit/hpo/test_resource_manager.py | 145 +- tests/unit/hpo/test_search_space.py | 273 +- tests/unit/mpa/__init__.py | 3 + tests/unit/mpa/deploy/__init__.py | 3 + tests/unit/mpa/test_augments.py | 83 + tests/unit/utils/test_signal.py | 61 - tests/unit/utils/test_utils.py | 101 - tools/README.md | 138 + tools/experiment.py | 1060 ++ tox.ini | 140 +- 3283 files changed, 243285 insertions(+), 71634 deletions(-) delete mode 100644 .ci/piptools-deps.in delete mode 100644 .ci/piptools-deps.txt delete mode 100644 .ci/publish-deps.in delete mode 100644 .ci/publish-deps.txt delete mode 100644 .ci/tox-deps.in delete mode 100644 .ci/tox-deps.txt create mode 100644 .flake8 delete mode 100644 .github/CODEOWNERS rename .github/{codecov.yaml => codecov.yml} (100%) delete mode 100644 .github/dependabot.yaml create mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/code_scan.yaml create mode 100644 .github/workflows/code_scan.yml delete mode 100644 .github/workflows/codeql.yaml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/daily.yml delete mode 100644 .github/workflows/docs_stable.yaml create mode 100644 .github/workflows/docs_stable.yml delete mode 100644 .github/workflows/labeler.yaml create mode 100644 .github/workflows/labeler.yml delete mode 100644 .github/workflows/pre_merge.yaml create mode 100644 .github/workflows/pre_merge.yml delete mode 100644 .github/workflows/publish.yaml create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/publish_internal.yaml create mode 100644 .github/workflows/publish_internal.yml create mode 100644 .github/workflows/run_tests_in_tox.yml create mode 100644 .github/workflows/run_tests_in_tox_custom.yml rename .github/workflows/{scorecard.yaml => scorecard.yml} (100%) rename .github/workflows/{stale_marker.yaml => stale_marker.yml} (100%) create mode 100644 .github/workflows/weekly.yml rename tests/unit/core/config/__init__.py => .gitmodules (100%) create mode 100644 .isort.cfg create mode 100644 CODEOWNERS delete mode 100644 NOTICE create mode 100644 docker/Dockerfile.cpu delete mode 100644 docker/Dockerfile.cuda delete mode 100644 docker/README.md delete mode 100644 docker/download_pretrained_weights.py create mode 100644 docker/requirements.in create mode 100644 docker/requirements.txt delete mode 100644 docs/README.md create mode 100644 docs/source/guide/explanation/additional_features/config_input_size.rst create mode 100644 docs/source/guide/explanation/additional_features/noisy_label_detection.rst delete mode 100644 docs/source/guide/get_started/api_tutorial.rst create mode 100644 docs/source/guide/tutorials/advanced/api_tutorial.rst create mode 100644 docs/source/guide/tutorials/advanced/backbones.rst delete mode 100644 docs/source/guide/tutorials/advanced/configuration.rst create mode 100644 docs/source/guide/tutorials/advanced/hpo_tutorial.rst create mode 100644 docs/source/guide/tutorials/advanced/self_sl.rst create mode 100644 docs/source/guide/tutorials/advanced/semi_sl.rst create mode 100644 docs/source/guide/tutorials/base/demo.rst create mode 100644 docs/source/guide/tutorials/base/deploy.rst create mode 100644 docs/utils/images/multi_cls_selfsl_performance_CIFAR10.png create mode 100644 docs/utils/images/multi_cls_selfsl_performance_CIFAR100.png create mode 100644 docs/utils/images/multi_cls_selfsl_performance_Food-101.png delete mode 100644 for_developers/add_custom_model.ipynb delete mode 100644 for_developers/cli_guide.md delete mode 100644 for_developers/contribution_guide.md delete mode 100644 for_developers/dir_structure.md delete mode 100644 for_developers/engine_api_example.ipynb delete mode 100644 for_developers/helpers.py delete mode 100644 for_developers/images/contribution_guide/ask_review.png delete mode 100644 for_developers/images/contribution_guide/copy_link.png delete mode 100644 for_developers/images/contribution_guide/design_proposal.png delete mode 100644 for_developers/images/contribution_guide/design_req.png delete mode 100644 for_developers/images/product_design/core_design_drawing.drawio delete mode 100644 for_developers/images/product_design/reuse_model.png delete mode 100644 for_developers/images/product_design/support_various_data_format.png delete mode 100644 for_developers/images/product_design/task_data_model.png delete mode 100644 for_developers/product_design.md delete mode 100644 for_developers/regression_test/Dockerfile delete mode 100644 for_developers/regression_test/README.md delete mode 100644 for_developers/regression_test/build.sh delete mode 100644 for_developers/regression_test/docker-compose.yml delete mode 100644 for_developers/regression_test/images/mlflow_dashboard.png delete mode 100644 for_developers/regression_test/images/mlflow_filtering.png delete mode 100644 for_developers/regression_test/images/mlflow_metrics_and_tags.png delete mode 100644 for_developers/regression_test/requirements.in delete mode 100644 for_developers/regression_test/requirements.txt delete mode 100644 for_developers/requirements-lock.txt delete mode 100644 for_developers/setup_guide.md delete mode 100644 for_developers/torchvision.ipynb create mode 100644 requirements/action.txt create mode 100644 requirements/anomaly.txt create mode 100644 requirements/api.txt create mode 100644 requirements/base.txt create mode 100644 requirements/classification.txt create mode 100644 requirements/detection.txt create mode 100644 requirements/dev.txt create mode 100644 requirements/docs.txt create mode 100644 requirements/gh-actions.txt create mode 100644 requirements/openvino.txt create mode 100644 requirements/publish.txt create mode 100644 requirements/segmentation.txt create mode 100644 requirements/visual_prompting.txt create mode 100644 setup.py delete mode 100644 src/otx/algo/__init__.py delete mode 100644 src/otx/algo/action_classification/__init__.py delete mode 100644 src/otx/algo/action_classification/backbones/__init__.py delete mode 100644 src/otx/algo/action_classification/backbones/movinet.py delete mode 100644 src/otx/algo/action_classification/heads/__init__.py delete mode 100644 src/otx/algo/action_classification/mmconfigs/movinet.yaml delete mode 100644 src/otx/algo/action_classification/mmconfigs/x3d.yaml delete mode 100644 src/otx/algo/action_classification/movinet.py delete mode 100644 src/otx/algo/action_classification/openvino_model.py delete mode 100644 src/otx/algo/action_classification/recognizers/__init__.py delete mode 100644 src/otx/algo/action_classification/recognizers/movinet_recognizer.py delete mode 100644 src/otx/algo/action_classification/recognizers/recognizer.py delete mode 100644 src/otx/algo/action_classification/x3d.py delete mode 100644 src/otx/algo/action_detection/__init__.py delete mode 100644 src/otx/algo/action_detection/mmconfigs/x3d_fastrcnn.yaml delete mode 100644 src/otx/algo/action_detection/x3d_fastrcnn.py delete mode 100644 src/otx/algo/anomaly/__init__.py delete mode 100644 src/otx/algo/anomaly/padim.py delete mode 100644 src/otx/algo/anomaly/stfpm.py delete mode 100644 src/otx/algo/callbacks/__init__.py delete mode 100644 src/otx/algo/callbacks/adaptive_train_scheduling.py delete mode 100644 src/otx/algo/classification/__init__.py delete mode 100644 src/otx/algo/classification/backbones/__init__.py delete mode 100644 src/otx/algo/classification/backbones/otx_efficientnet.py delete mode 100644 src/otx/algo/classification/backbones/otx_efficientnet_v2.py delete mode 100644 src/otx/algo/classification/backbones/otx_mobilenet_v3.py delete mode 100644 src/otx/algo/classification/deit_tiny.py delete mode 100644 src/otx/algo/classification/efficientnet_b0.py delete mode 100644 src/otx/algo/classification/efficientnet_v2.py delete mode 100644 src/otx/algo/classification/heads/__init__.py delete mode 100644 src/otx/algo/classification/heads/custom_hlabel_linear_cls_head.py delete mode 100644 src/otx/algo/classification/heads/custom_hlabel_non_linear_cls_head.py delete mode 100644 src/otx/algo/classification/heads/custom_multilabel_linear_cls_head.py delete mode 100644 src/otx/algo/classification/heads/custom_multilabel_non_linear_cls_head.py delete mode 100644 src/otx/algo/classification/losses/__init__.py delete mode 100644 src/otx/algo/classification/losses/asymmetric_angular_loss_with_ignore.py delete mode 100644 src/otx/algo/classification/mmconfigs/hlabel_classification/deit_tiny.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/hlabel_classification/efficientnet_b0_light.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/hlabel_classification/efficientnet_v2_light.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/hlabel_classification/mobilenet_v3_large_light.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multiclass_classification/deit_tiny.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_b0.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_b0_light.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_v2.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_v2_light.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multiclass_classification/mobilenet_v3_large.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multiclass_classification/mobilenet_v3_large_light.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multilabel_classification/deit_tiny.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multilabel_classification/efficientnet_b0_light.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multilabel_classification/efficientnet_v2_light.yaml delete mode 100644 src/otx/algo/classification/mmconfigs/multilabel_classification/mobilenet_v3_large_light.yaml delete mode 100644 src/otx/algo/classification/mobilenet_v3_large.py delete mode 100644 src/otx/algo/classification/otx_dino_v2.py delete mode 100644 src/otx/algo/classification/torchvision_model.py delete mode 100644 src/otx/algo/detection/__init__.py delete mode 100644 src/otx/algo/detection/atss.py delete mode 100644 src/otx/algo/detection/backbones/__init__.py delete mode 100644 src/otx/algo/detection/backbones/pytorchcv_backbones.py delete mode 100644 src/otx/algo/detection/heads/__init__.py delete mode 100644 src/otx/algo/detection/heads/class_incremental_mixin.py delete mode 100644 src/otx/algo/detection/heads/custom_anchor_generator.py delete mode 100644 src/otx/algo/detection/heads/custom_atss_head.py delete mode 100644 src/otx/algo/detection/heads/custom_ssd_head.py delete mode 100644 src/otx/algo/detection/losses/__init__.py delete mode 100644 src/otx/algo/detection/losses/cross_focal_loss.py delete mode 100644 src/otx/algo/detection/mmconfigs/atss_mobilenetv2.yaml delete mode 100644 src/otx/algo/detection/mmconfigs/atss_r50_fpn.yaml delete mode 100644 src/otx/algo/detection/mmconfigs/atss_resnext101.yaml delete mode 100644 src/otx/algo/detection/mmconfigs/rtmdet_tiny.yaml delete mode 100644 src/otx/algo/detection/mmconfigs/ssd_mobilenetv2.yaml delete mode 100644 src/otx/algo/detection/mmconfigs/yolox_l.yaml delete mode 100644 src/otx/algo/detection/mmconfigs/yolox_s.yaml delete mode 100644 src/otx/algo/detection/mmconfigs/yolox_tiny.yaml delete mode 100644 src/otx/algo/detection/mmconfigs/yolox_x.yaml delete mode 100644 src/otx/algo/detection/mmdeploy/__init__.py delete mode 100644 src/otx/algo/detection/mmdeploy/atss.py delete mode 100644 src/otx/algo/detection/mmdeploy/atss_r50_fpn.py delete mode 100644 src/otx/algo/detection/mmdeploy/base_detection.py delete mode 100644 src/otx/algo/detection/mmdeploy/rtmdet.py delete mode 100644 src/otx/algo/detection/mmdeploy/ssd_mobilenetv2.py delete mode 100644 src/otx/algo/detection/mmdeploy/yolox.py delete mode 100644 src/otx/algo/detection/mmdeploy/yolox_tiny.py delete mode 100644 src/otx/algo/detection/rtmdet.py delete mode 100644 src/otx/algo/detection/ssd.py delete mode 100644 src/otx/algo/detection/yolox.py delete mode 100644 src/otx/algo/hooks/__init__.py delete mode 100644 src/otx/algo/hooks/recording_forward_hook.py delete mode 100644 src/otx/algo/instance_segmentation/__init__.py delete mode 100644 src/otx/algo/instance_segmentation/heads/__init__.py delete mode 100644 src/otx/algo/instance_segmentation/heads/custom_roi_head.py delete mode 100644 src/otx/algo/instance_segmentation/maskrcnn.py delete mode 100644 src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_efficientnetb2b.yaml delete mode 100644 src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_r50.yaml delete mode 100644 src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_swint.yaml delete mode 100644 src/otx/algo/instance_segmentation/mmconfigs/rtmdet_inst_tiny.yaml delete mode 100644 src/otx/algo/instance_segmentation/mmdeploy/__init__.py delete mode 100644 src/otx/algo/instance_segmentation/mmdeploy/base_instance_segmentation.py delete mode 100644 src/otx/algo/instance_segmentation/mmdeploy/maskrcnn.py delete mode 100644 src/otx/algo/instance_segmentation/mmdeploy/maskrcnn_swint.py delete mode 100644 src/otx/algo/instance_segmentation/mmdeploy/rtmdet_inst.py delete mode 100644 src/otx/algo/instance_segmentation/otx_instseg_evaluation.py delete mode 100644 src/otx/algo/instance_segmentation/rtmdet_inst.py delete mode 100644 src/otx/algo/samplers/__init__.py delete mode 100644 src/otx/algo/samplers/balanced_sampler.py delete mode 100644 src/otx/algo/samplers/class_incremental_sampler.py delete mode 100644 src/otx/algo/schedulers/__init__.py delete mode 100644 src/otx/algo/schedulers/warmup_schedulers.py delete mode 100644 src/otx/algo/segmentation/__init__.py delete mode 100644 src/otx/algo/segmentation/backbones/__init__.py delete mode 100644 src/otx/algo/segmentation/backbones/dinov2.py delete mode 100644 src/otx/algo/segmentation/backbones/litehrnet.py delete mode 100644 src/otx/algo/segmentation/dino_v2_seg.py delete mode 100644 src/otx/algo/segmentation/heads/__init__.py delete mode 100644 src/otx/algo/segmentation/heads/custom_fcn_head.py delete mode 100644 src/otx/algo/segmentation/heads/custom_ham_head.py delete mode 100644 src/otx/algo/segmentation/litehrnet.py delete mode 100644 src/otx/algo/segmentation/losses/__init__.py delete mode 100644 src/otx/algo/segmentation/losses/cross_entropy_loss_with_ignore.py delete mode 100644 src/otx/algo/segmentation/mmconfigs/dino_v2_seg.yaml delete mode 100644 src/otx/algo/segmentation/mmconfigs/litehrnet_18.yaml delete mode 100644 src/otx/algo/segmentation/mmconfigs/litehrnet_s.yaml delete mode 100644 src/otx/algo/segmentation/mmconfigs/litehrnet_x.yaml delete mode 100644 src/otx/algo/segmentation/mmconfigs/segnext_b.yaml delete mode 100644 src/otx/algo/segmentation/mmconfigs/segnext_s.yaml delete mode 100644 src/otx/algo/segmentation/mmconfigs/segnext_t.yaml delete mode 100644 src/otx/algo/segmentation/segnext.py delete mode 100644 src/otx/algo/utils/__init__.py delete mode 100644 src/otx/algo/utils/mmconfig.py delete mode 100644 src/otx/algo/utils/segmentation.py delete mode 100644 src/otx/algo/utils/support_otx_v1.py delete mode 100644 src/otx/algo/utils/xai_utils.py delete mode 100644 src/otx/algo/visual_prompting/__init__.py delete mode 100644 src/otx/algo/visual_prompting/backbones/__init__.py delete mode 100644 src/otx/algo/visual_prompting/backbones/tiny_vit.py delete mode 100644 src/otx/algo/visual_prompting/backbones/vit.py delete mode 100644 src/otx/algo/visual_prompting/decoders/__init__.py delete mode 100644 src/otx/algo/visual_prompting/encoders/__init__.py delete mode 100644 src/otx/algo/visual_prompting/encoders/sam_image_encoder.py delete mode 100644 src/otx/algo/visual_prompting/encoders/sam_prompt_encoder.py delete mode 100644 src/otx/algo/visual_prompting/openvino_models.py delete mode 100644 src/otx/algo/visual_prompting/segment_anything.py delete mode 100644 src/otx/algo/visual_prompting/utils/__init__.py delete mode 100644 src/otx/algo/visual_prompting/utils/layer_norm_2d.py delete mode 100644 src/otx/algo/visual_prompting/zero_shot_segment_anything.py create mode 100644 src/otx/algorithms/__init__.py create mode 100644 src/otx/algorithms/action/__init__.py create mode 100644 src/otx/algorithms/action/adapters/__init__.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/__init__.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/data/__init__.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/data/cls_dataset.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/data/det_dataset.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/data/pipelines/__init__.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/data/pipelines/loading.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/models/__init__.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/models/backbones/__init__.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/models/backbones/movinet.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/models/detectors/__init__.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/models/detectors/fast_rcnn.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/models/heads/__init__.py rename src/otx/{algo/action_classification => algorithms/action/adapters/mmaction/models}/heads/movinet_head.py (78%) create mode 100644 src/otx/algorithms/action/adapters/mmaction/models/heads/roi_head.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/models/recognizers/__init__.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/models/recognizers/movinet_recognizer.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/task.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/utils/__init__.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/utils/det_eval_utils.py create mode 100644 src/otx/algorithms/action/adapters/mmaction/utils/export_utils.py create mode 100644 src/otx/algorithms/action/adapters/openvino/__init__.py create mode 100644 src/otx/algorithms/action/adapters/openvino/dataloader.py create mode 100644 src/otx/algorithms/action/adapters/openvino/model_wrappers/__init__.py create mode 100644 src/otx/algorithms/action/adapters/openvino/model_wrappers/openvino_models.py create mode 100644 src/otx/algorithms/action/adapters/openvino/task.py create mode 100644 src/otx/algorithms/action/configs/__init__.py create mode 100644 src/otx/algorithms/action/configs/base/__init__.py create mode 100644 src/otx/algorithms/action/configs/base/configuration.py create mode 100644 src/otx/algorithms/action/configs/classification/__init__.py create mode 100644 src/otx/algorithms/action/configs/classification/base/__init__.py create mode 100644 src/otx/algorithms/action/configs/classification/base/base_classification_dynamic.py create mode 100644 src/otx/algorithms/action/configs/classification/base/base_classification_static.py create mode 100644 src/otx/algorithms/action/configs/classification/base/supervised.py create mode 100644 src/otx/algorithms/action/configs/classification/configuration.yaml create mode 100644 src/otx/algorithms/action/configs/classification/movinet/__init__.py create mode 100644 src/otx/algorithms/action/configs/classification/movinet/data_pipeline.py create mode 100644 src/otx/algorithms/action/configs/classification/movinet/deployment.py create mode 100644 src/otx/algorithms/action/configs/classification/movinet/model.py create mode 100644 src/otx/algorithms/action/configs/classification/movinet/template.yaml create mode 100644 src/otx/algorithms/action/configs/classification/x3d/__init__.py create mode 100644 src/otx/algorithms/action/configs/classification/x3d/data_pipeline.py create mode 100644 src/otx/algorithms/action/configs/classification/x3d/deployment.py create mode 100644 src/otx/algorithms/action/configs/classification/x3d/model.py create mode 100644 src/otx/algorithms/action/configs/classification/x3d/template.yaml create mode 100644 src/otx/algorithms/action/configs/detection/__init__.py create mode 100644 src/otx/algorithms/action/configs/detection/base/__init__.py create mode 100644 src/otx/algorithms/action/configs/detection/base/ava_data_pipeline.py create mode 100644 src/otx/algorithms/action/configs/detection/base/base_detection_dynamic.py create mode 100644 src/otx/algorithms/action/configs/detection/base/base_detection_static.py create mode 100644 src/otx/algorithms/action/configs/detection/base/data_pipeline.py create mode 100644 src/otx/algorithms/action/configs/detection/base/faster_rcnn_config.py create mode 100644 src/otx/algorithms/action/configs/detection/base/supervised.py create mode 100644 src/otx/algorithms/action/configs/detection/configuration.yaml create mode 100644 src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/__init__.py create mode 100644 src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/data_pipeline.py create mode 100644 src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/deployment.py create mode 100644 src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/model.py create mode 100644 src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/template.yaml create mode 100644 src/otx/algorithms/action/task.py create mode 100644 src/otx/algorithms/action/tools/__init__.py create mode 100644 src/otx/algorithms/action/tools/sample_classification.py create mode 100644 src/otx/algorithms/action/tools/sample_detection.py create mode 100644 src/otx/algorithms/action/utils/__init__.py create mode 100644 src/otx/algorithms/action/utils/convert_public_data_to_cvat.py create mode 100644 src/otx/algorithms/action/utils/data.py create mode 100644 src/otx/algorithms/anomaly/__init__.py create mode 100644 src/otx/algorithms/anomaly/adapters/__init__.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/__init__.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/accelerators/__init__.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/accelerators/xpu.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/callbacks/__init__.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/callbacks/inference.py rename src/otx/{algo => algorithms/anomaly/adapters/anomalib}/callbacks/iteration_timer.py (88%) create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/callbacks/progress.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/config/__init__.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/config/anomalib_config.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/data/__init__.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/data/create_mvtec_ad_json_annotations.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/data/data.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/data/dataset.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/data/mvtec.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/plugins/__init__.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/plugins/xpu_precision.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/strategies/__init__.py create mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/strategies/xpu_single.py create mode 100644 src/otx/algorithms/anomaly/configs/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/base/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/base/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/base/configuration_enums.py create mode 100644 src/otx/algorithms/anomaly/configs/base/draem/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/base/draem/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/base/padim/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/base/padim/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/base/stfpm/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/base/stfpm/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/classification/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/classification/draem/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/classification/draem/compression_config.json create mode 100644 src/otx/algorithms/anomaly/configs/classification/draem/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/classification/draem/configuration.yaml create mode 100644 src/otx/algorithms/anomaly/configs/classification/draem/template_experimental.yaml create mode 100644 src/otx/algorithms/anomaly/configs/classification/draem/transform_config.yaml create mode 100644 src/otx/algorithms/anomaly/configs/classification/padim/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/classification/padim/compression_config.json create mode 100644 src/otx/algorithms/anomaly/configs/classification/padim/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/classification/padim/configuration.yaml create mode 100644 src/otx/algorithms/anomaly/configs/classification/padim/ptq_optimization_config.py create mode 100644 src/otx/algorithms/anomaly/configs/classification/padim/template.yaml create mode 100644 src/otx/algorithms/anomaly/configs/classification/stfpm/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/classification/stfpm/compression_config.json create mode 100644 src/otx/algorithms/anomaly/configs/classification/stfpm/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/classification/stfpm/configuration.yaml create mode 100644 src/otx/algorithms/anomaly/configs/classification/stfpm/hpo_config.yaml create mode 100644 src/otx/algorithms/anomaly/configs/classification/stfpm/template.yaml create mode 100644 src/otx/algorithms/anomaly/configs/detection/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/detection/draem/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/detection/draem/compression_config.json create mode 100644 src/otx/algorithms/anomaly/configs/detection/draem/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/detection/draem/configuration.yaml create mode 100644 src/otx/algorithms/anomaly/configs/detection/draem/template_experimental.yaml create mode 100644 src/otx/algorithms/anomaly/configs/detection/draem/transform_config.yaml create mode 100644 src/otx/algorithms/anomaly/configs/detection/padim/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/detection/padim/compression_config.json create mode 100644 src/otx/algorithms/anomaly/configs/detection/padim/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/detection/padim/configuration.yaml create mode 100644 src/otx/algorithms/anomaly/configs/detection/padim/ptq_optimization_config.py create mode 100644 src/otx/algorithms/anomaly/configs/detection/padim/template.yaml create mode 100644 src/otx/algorithms/anomaly/configs/detection/stfpm/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/detection/stfpm/compression_config.json create mode 100644 src/otx/algorithms/anomaly/configs/detection/stfpm/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/detection/stfpm/configuration.yaml create mode 100644 src/otx/algorithms/anomaly/configs/detection/stfpm/hpo_config.yaml create mode 100644 src/otx/algorithms/anomaly/configs/detection/stfpm/template.yaml create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/draem/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/draem/compression_config.json create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/draem/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/draem/configuration.yaml create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/draem/template_experimental.yaml create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/draem/transform_config.yaml create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/padim/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/padim/compression_config.json create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/padim/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/padim/configuration.yaml create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/padim/ptq_optimization_config.py create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/padim/template.yaml create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/stfpm/__init__.py create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/stfpm/compression_config.json create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/stfpm/configuration.py create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/stfpm/configuration.yaml create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/stfpm/hpo_config.yaml create mode 100644 src/otx/algorithms/anomaly/configs/segmentation/stfpm/template.yaml create mode 100644 src/otx/algorithms/anomaly/ote_tests_pytest.ini create mode 100644 src/otx/algorithms/anomaly/tasks/__init__.py create mode 100644 src/otx/algorithms/anomaly/tasks/inference.py create mode 100644 src/otx/algorithms/anomaly/tasks/nncf.py create mode 100644 src/otx/algorithms/anomaly/tasks/openvino.py create mode 100644 src/otx/algorithms/anomaly/tasks/train.py create mode 100644 src/otx/algorithms/anomaly/tools/README.md create mode 100644 src/otx/algorithms/anomaly/tools/__init__.py create mode 100644 src/otx/algorithms/anomaly/tools/sample.py create mode 100644 src/otx/algorithms/classification/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/apis/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/apis/train.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/configurer.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/datasets/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/otx_pipelines.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/augmix.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/otx_transforms.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/random_augment.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/twocrop_transform.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/backbones/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/backbones/mmov_backbone.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/classifiers/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/classifiers/byol.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/classifiers/custom_image_classifier.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/classifiers/mixin.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/classifiers/semisl_classifier.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/classifiers/semisl_multilabel_classifier.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/classifiers/supcon_classifier.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/contrastive_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/conv_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_hierarchical_linear_cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_hierarchical_non_linear_cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_multi_label_linear_cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_multi_label_non_linear_cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_vision_transformer_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/mixin.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/mmov_cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/non_linear_cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/semisl_cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/semisl_multilabel_cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/heads/supcon_cls_head.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/losses/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/losses/asymmetric_angular_loss_with_ignore.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/losses/asymmetric_loss_with_ignore.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/losses/barlowtwins_loss.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/losses/cross_entropy_loss.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/losses/ib_loss.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/necks/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/necks/mmov_neck.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/models/necks/selfsl_mlp.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/nncf/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/nncf/builder.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/nncf/patches.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/nncf/registers.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/nncf/task.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/optimizer/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/optimizer/lars.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/task.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/utils/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/utils/builder.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py create mode 100644 src/otx/algorithms/classification/adapters/mmcls/utils/exporter.py create mode 100644 src/otx/algorithms/classification/adapters/openvino/__init__.py create mode 100644 src/otx/algorithms/classification/adapters/openvino/task.py create mode 100644 src/otx/algorithms/classification/configs/__init__.py create mode 100644 src/otx/algorithms/classification/configs/base/__init__.py create mode 100644 src/otx/algorithms/classification/configs/base/configuration.py create mode 100644 src/otx/algorithms/classification/configs/base/data/__init__.py create mode 100644 src/otx/algorithms/classification/configs/base/data/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/base/data/selfsl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/base/data/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/base/data/semisl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/base/data/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/base/data/supcon/__init__.py create mode 100644 src/otx/algorithms/classification/configs/base/data/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/base/deployments/__init__.py create mode 100644 src/otx/algorithms/classification/configs/base/deployments/base_classification_dynamic.py create mode 100644 src/otx/algorithms/classification/configs/base/deployments/base_classification_static.py create mode 100644 src/otx/algorithms/classification/configs/base/models/__init__.py create mode 100644 src/otx/algorithms/classification/configs/base/models/deit.py create mode 100644 src/otx/algorithms/classification/configs/base/models/efficientnet.py create mode 100644 src/otx/algorithms/classification/configs/base/models/efficientnet_v2.py create mode 100644 src/otx/algorithms/classification/configs/base/models/mobilenet_v3.py create mode 100644 src/otx/algorithms/classification/configs/configuration.yaml create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/__init__.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/deployment.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/hpo_config.yaml create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/model.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/model_hierarchical.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/model_multilabel.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/ptq_optimization_config.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/semisl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/semisl/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/semisl/model.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/semisl/model_multilabel.py create mode 100644 src/otx/algorithms/classification/configs/deit_tiny/template.yaml create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/__init__.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/compression_config.json create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/deployment.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/hpo_config.yaml create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model_hierarchical.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model_multilabel.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/model.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/model.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/model_multilabel.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/__init__.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/model.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/template.yaml create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/__init__.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/compression_config.json create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/deployment.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/hpo_config.yaml create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model_hierarchical.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model_multilabel.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/model.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/model.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/model_multilabel.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/__init__.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/model.py create mode 100644 src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/template.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/__init__.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/compression_config.json create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/hpo_config.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model_hierarchical.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model_multilabel.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/model.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/__init__.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/model.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/template_experiment.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/__init__.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/compression_config.json create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/deployment.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/hpo_config.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model_hierarchical.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model_multilabel.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/model.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/model.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/model_multilabel.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/__init__.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/model.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/template.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/__init__.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/compression_config.json create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/hpo_config.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model_hierarchical.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model_multilabel.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/__init__.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/model.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/__init__.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/hparam.yaml create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/model.py create mode 100644 src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/template_experiment.yaml create mode 100644 src/otx/algorithms/classification/task.py create mode 100644 src/otx/algorithms/classification/tools/__init__.py create mode 100644 src/otx/algorithms/classification/tools/classification_sample.py create mode 100644 src/otx/algorithms/classification/utils/__init__.py create mode 100644 src/otx/algorithms/classification/utils/cls_utils.py create mode 100644 src/otx/algorithms/classification/utils/convert_coco_to_multilabel.py create mode 100644 src/otx/algorithms/common/__init__.py create mode 100644 src/otx/algorithms/common/adapters/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/clsincr_mixin.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/configs/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/configs/backbones/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/configs/backbones/efficientnet_b2b.yaml create mode 100644 src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_18.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_s.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_x.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/configs/backbones/mobilenet_v2_w1.yaml create mode 100644 src/otx/algorithms/common/adapters/mmcv/configs/backbones/resnet18.yaml create mode 100644 src/otx/algorithms/common/adapters/mmcv/configs/backbones/resnet50.yaml create mode 100644 src/otx/algorithms/common/adapters/mmcv/configs/backbones/segnext.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/configurer.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_repeat_data_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_training_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/cancel_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/checkpoint_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/composed_dataloaders_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/custom_model_ema_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/dual_model_ema_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/early_stopping_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/eval_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/force_train_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/fp16_sam_optimizer_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/hpu_optimizer_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/ib_loss_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/loss_dynamics_tracking_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/mean_teacher_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/mem_cache_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/model_ema_v2_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/no_bias_decay_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/progress_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/recording_forward_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/sam_optimizer_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/semisl_cls_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/task_adapt_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/two_crop_transform_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/hooks/xpu_optimizer_hook.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/models/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/models/backbones/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnet.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnetv2.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/models/backbones/mobilenetv3.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/models/backbones/torchvision_backbones.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/models/builder.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/nncf/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/nncf/hooks.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/nncf/patches.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/nncf/runners.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/nncf/utils.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/ops/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/ops/multi_scale_deformable_attn_pytorch.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/pipelines/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/pipelines/load_image_from_otx_dataset.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/augments.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/cv_augment.pyx create mode 100644 src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/pil_augment.pyx create mode 100644 src/otx/algorithms/common/adapters/mmcv/runner.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/semisl_mixin.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/tasks/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/tasks/exporter.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/tasks/registry.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/tasks/version.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/utils/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/utils/_builder_build_data_parallel.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/utils/_config_utils_get_configs_by_keys.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/utils/_config_utils_get_configs_by_pairs.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/utils/builder.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/utils/fp16_utils.py create mode 100644 src/otx/algorithms/common/adapters/mmcv/utils/hpu_optimizers.py create mode 100644 src/otx/algorithms/common/adapters/mmdeploy/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmdeploy/apis.py create mode 100644 src/otx/algorithms/common/adapters/mmdeploy/ops/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmdeploy/ops/custom_ops.py create mode 100644 src/otx/algorithms/common/adapters/mmdeploy/utils/__init__.py create mode 100644 src/otx/algorithms/common/adapters/mmdeploy/utils/mmdeploy.py create mode 100644 src/otx/algorithms/common/adapters/mmdeploy/utils/onnx.py create mode 100644 src/otx/algorithms/common/adapters/mmdeploy/utils/operations_domain.py create mode 100644 src/otx/algorithms/common/adapters/mmdeploy/utils/utils.py create mode 100644 src/otx/algorithms/common/adapters/nncf/__init__.py create mode 100644 src/otx/algorithms/common/adapters/nncf/compression.py create mode 100644 src/otx/algorithms/common/adapters/nncf/config.py create mode 100644 src/otx/algorithms/common/adapters/nncf/patches.py create mode 100644 src/otx/algorithms/common/adapters/nncf/utils/__init__.py create mode 100644 src/otx/algorithms/common/adapters/nncf/utils/utils.py create mode 100644 src/otx/algorithms/common/adapters/torch/__init__.py create mode 100644 src/otx/algorithms/common/adapters/torch/amp/__init__.py create mode 100644 src/otx/algorithms/common/adapters/torch/amp/xpu_grad_scaler.py create mode 100644 src/otx/algorithms/common/adapters/torch/dataloaders/__init__.py create mode 100644 src/otx/algorithms/common/adapters/torch/dataloaders/composed_dataloader.py create mode 100644 src/otx/algorithms/common/adapters/torch/dataloaders/samplers/__init__.py create mode 100644 src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py create mode 100644 src/otx/algorithms/common/adapters/torch/dataloaders/samplers/cls_incr_sampler.py create mode 100644 src/otx/algorithms/common/adapters/torch/dataloaders/samplers/otx_sampler.py create mode 100644 src/otx/algorithms/common/adapters/torch/utils/__init__.py create mode 100644 src/otx/algorithms/common/adapters/torch/utils/bs_search_algo.py create mode 100644 src/otx/algorithms/common/adapters/torch/utils/utils.py create mode 100644 src/otx/algorithms/common/configs/__init__.py create mode 100644 src/otx/algorithms/common/configs/configuration_enums.py create mode 100644 src/otx/algorithms/common/configs/training_base.py create mode 100644 src/otx/algorithms/common/tasks/__init__.py create mode 100644 src/otx/algorithms/common/tasks/base_task.py create mode 100644 src/otx/algorithms/common/tasks/nncf_task.py create mode 100644 src/otx/algorithms/common/tools/__init__.py create mode 100644 src/otx/algorithms/common/utils/__init__.py create mode 100644 src/otx/algorithms/common/utils/callback.py create mode 100644 src/otx/algorithms/common/utils/data.py create mode 100644 src/otx/algorithms/common/utils/dist_utils.py create mode 100644 src/otx/algorithms/common/utils/ext_loader.py create mode 100644 src/otx/algorithms/common/utils/ir.py create mode 100644 src/otx/algorithms/common/utils/mask_to_bbox.py create mode 100644 src/otx/algorithms/common/utils/mo_wrapper.py create mode 100644 src/otx/algorithms/common/utils/task_adapt.py create mode 100644 src/otx/algorithms/common/utils/utils.py create mode 100644 src/otx/algorithms/detection/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/apis/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/apis/train.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/configurer.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/datasets/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/datasets/dataset.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/load_pipelines.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/torchvision2mmdet.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/datasets/task_adapt_dataset.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/evaluation/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/evaluation/evaluator.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/hooks/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/hooks/det_class_probability_map_hook.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/hooks/tile_sampling_hook.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/assigners/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/assigners/custom_max_iou_assigner.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/backbones/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/backbones/imgclsmob.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/backbones/mmov_backbone.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_rpn_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_ssd_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_yolov3_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_atss_detector.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_deformable_detr_detector.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_dino_detector.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_lite_dino.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_maskrcnn_detector.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_maskrcnn_tile_optimized.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_single_stage_detector.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_two_stage_detector.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_vfnet_detector.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_yolox_detector.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/l2sp_detector_mixin.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/loss_dynamics_mixin.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/mean_teacher.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/detectors/sam_detector_mixin.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/cross_dataset_detector_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_anchor_generator.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_atss_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_dino_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_fcn_mask_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_retina_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_roi_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_rpn_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_ssd_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_vfnet_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_yolox_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/heads/detr_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/layers/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/layers/dino.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/layers/dino_layers.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/layers/lite_detr_layers.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/loss_dyns.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/losses/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/losses/cross_focal_loss.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/losses/l2sp_loss.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/necks/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_fpn.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_ssd_neck.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_yolov3_neck.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/patch_mmdeploy.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/bbox_heads/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/bbox_heads/mmov_bbox_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/mask_heads/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/mask_heads/mmov_mask_head.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/single_level_roi_extractor.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/nncf/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/nncf/builder.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/nncf/patches.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/nncf/task.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/task.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/utils/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/utils/builder.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/utils/config_utils.py create mode 100644 src/otx/algorithms/detection/adapters/mmdet/utils/exporter.py create mode 100644 src/otx/algorithms/detection/adapters/openvino/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/openvino/model_wrappers/__init__.py create mode 100644 src/otx/algorithms/detection/adapters/openvino/task.py create mode 100644 src/otx/algorithms/detection/configs/__init__.py create mode 100644 src/otx/algorithms/detection/configs/base/__init__.py create mode 100644 src/otx/algorithms/detection/configs/base/configuration.py create mode 100644 src/otx/algorithms/detection/configs/base/data/__init__.py create mode 100644 src/otx/algorithms/detection/configs/base/data/atss_data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/detr_data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/iseg_efficientnet_data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/iseg_efficientnet_data_pipeline_xpu.py create mode 100644 src/otx/algorithms/detection/configs/base/data/iseg_resnet_data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/iseg_resnet_data_pipeline_xpu.py create mode 100644 src/otx/algorithms/detection/configs/base/data/semisl/__init__.py create mode 100644 src/otx/algorithms/detection/configs/base/data/semisl/base_semisl_det_data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/semisl/semisl_is_eff_data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/semisl/semisl_is_res_data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/tiling/__init__.py create mode 100644 src/otx/algorithms/detection/configs/base/data/tiling/atss_tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/tiling/base_iseg_tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/tiling/efficientnet_iseg_tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/tiling/yolox_tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/data/yolox_data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/base/deployments/__init__.py create mode 100644 src/otx/algorithms/detection/configs/base/deployments/base_detection_dynamic.py create mode 100644 src/otx/algorithms/detection/configs/base/deployments/base_detection_static.py create mode 100644 src/otx/algorithms/detection/configs/base/deployments/base_instance_segmentation_dynamic.py create mode 100644 src/otx/algorithms/detection/configs/base/deployments/base_instance_segmentation_static.py create mode 100644 src/otx/algorithms/detection/configs/base/models/__init__.py create mode 100644 src/otx/algorithms/detection/configs/base/models/detector.py create mode 100644 src/otx/algorithms/detection/configs/base/models/model.py create mode 100644 src/otx/algorithms/detection/configs/base/models/single_stage_detector.py create mode 100644 src/otx/algorithms/detection/configs/base/models/unbiased_teacher.py create mode 100644 src/otx/algorithms/detection/configs/detection/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/configuration.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/deployment.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/model_xpu.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/deployment.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/model_xpu.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/deployment.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/model_xpu.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/template.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/deployment.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/model_xpy.py create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/deployment.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/hparam.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/deployment.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/hparam.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/deployment.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/template_experimental.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_dino/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_dino/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_dino/deployment.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_dino/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_dino/template_experimental.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/deployment.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/template_experimental.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_vfnet/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_vfnet/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_vfnet/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_vfnet/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_vfnet/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnet50_vfnet/template_experimental.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/deployment.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/__init__.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/hparam.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/model.py create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml create mode 100644 src/otx/algorithms/detection/configs/detection/resnext101_atss/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/__init__.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/__init__.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/deployment.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/deployment_tile_classifier.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/model.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/template_experimental.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/__init__.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/data_pipeline_xpu.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/deployment.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/deployment_tile_classifier.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/model.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/__init__.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/hparam.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/model.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/template.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/__init__.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/data_pipeline_xpu.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/deployment.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/deployment_tile_classifier.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/model.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/template.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/__init__.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/data_pipeline_xpu.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/deployment.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/deployment_tile_classifier.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/model.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/__init__.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/hparam.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/model.py create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/template.yaml create mode 100644 src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/__init__.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/__init__.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/deployment.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/model.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/template.yaml create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/__init__.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/compression_config.json create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/data_pipeline.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/deployment.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/hpo_config.yaml create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/model.py create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/template.yaml create mode 100644 src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/tile_pipeline.py create mode 100644 src/otx/algorithms/detection/task.py create mode 100644 src/otx/algorithms/detection/tools/__init__.py create mode 100644 src/otx/algorithms/detection/tools/detection_sample.py create mode 100644 src/otx/algorithms/detection/tools/detection_semisl_sample.py create mode 100644 src/otx/algorithms/detection/tools/instance_segmentation_sample.py create mode 100644 src/otx/algorithms/detection/utils/__init__.py create mode 100644 src/otx/algorithms/detection/utils/data.py create mode 100644 src/otx/algorithms/detection/utils/utils.py create mode 100644 src/otx/algorithms/segmentation/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/apis/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/apis/train.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/configurer.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/datasets/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/datasets/dataset.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/compose.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/loads.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/transforms.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/mmov_backbone.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/mscan.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/heads/custom_otx_head.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/heads/detcon_head.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/heads/light_ham.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/heads/mmov_decode_head.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/heads/proto_head.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/losses/cross_entropy_loss_with_ignore.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/losses/detcon_loss.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/losses/pixel_prototype_ce_loss.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/necks/selfsl_mlp.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/base.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/constant.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/poly.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/step.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/mean_teacher_segmentor.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/otx_encoder_decoder.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/utils/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/utils/aggregator.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/utils/angular_pw_conv.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/utils/asymmetric_position_attention.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/utils/channel_shuffle.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/utils/local_attention.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/utils/loss_equalizer.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/utils/normalize.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/utils/proto_utils.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/models/utils/psp_layer.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/nncf/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/nncf/patches.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/task.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/utils/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/utils/builder.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py create mode 100644 src/otx/algorithms/segmentation/adapters/mmseg/utils/exporter.py create mode 100644 src/otx/algorithms/segmentation/adapters/openvino/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/openvino/model_wrappers/__init__.py create mode 100644 src/otx/algorithms/segmentation/adapters/openvino/task.py create mode 100644 src/otx/algorithms/segmentation/configs/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/base/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/base/configuration.py create mode 100644 src/otx/algorithms/segmentation/configs/base/data/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/base/data/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/base/data/selfsl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/base/data/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/base/data/semisl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/base/data/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/base/data/supcon/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/base/data/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/base/deployments/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/base/deployments/base_segmentation_dynamic.py create mode 100644 src/otx/algorithms/segmentation/configs/base/deployments/base_segmentation_static.py create mode 100644 src/otx/algorithms/segmentation/configs/configuration.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/compression_config.json create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/deployment.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/hpo_config.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/ptq_optimization_config.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/template.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/compression_config.json create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/deployment.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/hpo_config.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/ptq_optimization_config.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/template.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/compression_config.json create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/deployment.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/hpo_config.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/ptq_optimization_config.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/template.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/compression_config.json create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/deployment.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/hpo_config.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/ptq_optimization_config.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/template.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/compression_config.json create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/deployment.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/hpo_config.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/ptq_optimization_config.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/template.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/compression_config.json create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/deployment.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/hpo_config.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/ptq_optimization_config.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/template.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/compression_config.json create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/deployment.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/hpo_config.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/ptq_optimization_config.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/__init__.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/data_pipeline.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/hparam.yaml create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/model.py create mode 100644 src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/template.yaml create mode 100644 src/otx/algorithms/segmentation/task.py create mode 100644 src/otx/algorithms/segmentation/tools/__init__.py create mode 100644 src/otx/algorithms/segmentation/tools/segmentation_sample.py create mode 100644 src/otx/algorithms/segmentation/utils/__init__.py create mode 100644 src/otx/algorithms/segmentation/utils/metadata.py create mode 100644 src/otx/algorithms/segmentation/utils/processing.py create mode 100644 src/otx/algorithms/visual_prompting/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/openvino/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_models.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/inference.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/config/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/config/visual_prompting_config.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/dataset.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/sam_transforms.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/transforms.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/tiny_vit.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/vit.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/__init__.py rename src/otx/{algo/visual_prompting => algorithms/visual_prompting/adapters/pytorch_lightning/models}/decoders/sam_mask_decoder.py (93%) create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/sam_image_encoder.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/sam_prompt_encoder.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/layer_norm.py rename src/otx/{algo/visual_prompting => algorithms/visual_prompting/adapters/pytorch_lightning/models}/utils/mlp_block.py (80%) create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/segment_anything.py create mode 100644 src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/zero_shot_segment_anything.py create mode 100644 src/otx/algorithms/visual_prompting/configs/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/configs/base/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/configs/base/configuration.py create mode 100644 src/otx/algorithms/visual_prompting/configs/base/configuration_enums.py create mode 100644 src/otx/algorithms/visual_prompting/configs/configuration.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/config.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/configuration.py create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/ptq_optimization_config.py create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/template.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_vit_b/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_vit_b/config.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.py create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_vit_b/ptq_optimization_config.py create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_vit_b/template.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/config.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/configuration.py create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/configuration.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/ptq_optimization_config.py create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/template.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/config.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/configuration.py create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/configuration.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/ptq_optimization_config.py create mode 100644 src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/template.yaml create mode 100644 src/otx/algorithms/visual_prompting/tasks/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/tasks/inference.py create mode 100644 src/otx/algorithms/visual_prompting/tasks/openvino.py create mode 100644 src/otx/algorithms/visual_prompting/tasks/train.py create mode 100644 src/otx/algorithms/visual_prompting/utils/__init__.py create mode 100644 src/otx/algorithms/visual_prompting/utils/visual_prompting_utils.py create mode 100644 src/otx/api/__init__.py create mode 100644 src/otx/api/configuration/__init__.py create mode 100644 src/otx/api/configuration/configurable_parameters.py create mode 100644 src/otx/api/configuration/default_model_parameters.py create mode 100644 src/otx/api/configuration/elements/__init__.py create mode 100644 src/otx/api/configuration/elements/configurable_enum.py create mode 100644 src/otx/api/configuration/elements/metadata_keys.py create mode 100644 src/otx/api/configuration/elements/parameter_group.py create mode 100644 src/otx/api/configuration/elements/primitive_parameters.py create mode 100644 src/otx/api/configuration/elements/utils.py create mode 100644 src/otx/api/configuration/enums/__init__.py create mode 100644 src/otx/api/configuration/enums/auto_hpo_state.py create mode 100644 src/otx/api/configuration/enums/config_element_type.py create mode 100644 src/otx/api/configuration/enums/model_lifecycle.py create mode 100644 src/otx/api/configuration/enums/utils.py create mode 100644 src/otx/api/configuration/helper/__init__.py create mode 100644 src/otx/api/configuration/helper/config_element_mapping.py create mode 100644 src/otx/api/configuration/helper/convert.py create mode 100644 src/otx/api/configuration/helper/create.py create mode 100644 src/otx/api/configuration/helper/substitute.py create mode 100644 src/otx/api/configuration/helper/utils.py create mode 100644 src/otx/api/configuration/helper/validate.py create mode 100644 src/otx/api/configuration/model_lifecycle.py create mode 100644 src/otx/api/configuration/ui_rules/__init__.py create mode 100644 src/otx/api/configuration/ui_rules/rules.py create mode 100644 src/otx/api/configuration/ui_rules/types.py create mode 100644 src/otx/api/configuration/ui_rules/utils.py create mode 100644 src/otx/api/entities/__init__.py create mode 100644 src/otx/api/entities/annotation.py create mode 100644 src/otx/api/entities/color.py create mode 100644 src/otx/api/entities/coordinate.py create mode 100644 src/otx/api/entities/dataset_item.py create mode 100644 src/otx/api/entities/datasets.py create mode 100644 src/otx/api/entities/explain_parameters.py create mode 100644 src/otx/api/entities/graph.py create mode 100644 src/otx/api/entities/id.py create mode 100644 src/otx/api/entities/image.py create mode 100644 src/otx/api/entities/inference_parameters.py create mode 100644 src/otx/api/entities/interfaces/__init__.py create mode 100644 src/otx/api/entities/interfaces/graph_interface.py create mode 100644 src/otx/api/entities/label.py create mode 100644 src/otx/api/entities/label_schema.py create mode 100644 src/otx/api/entities/media.py create mode 100644 src/otx/api/entities/metadata.py create mode 100644 src/otx/api/entities/metrics.py create mode 100644 src/otx/api/entities/model.py create mode 100644 src/otx/api/entities/model_template.py create mode 100644 src/otx/api/entities/optimization_parameters.py create mode 100644 src/otx/api/entities/result_media.py create mode 100644 src/otx/api/entities/resultset.py create mode 100644 src/otx/api/entities/scored_label.py create mode 100644 src/otx/api/entities/shapes/__init__.py create mode 100644 src/otx/api/entities/shapes/ellipse.py create mode 100644 src/otx/api/entities/shapes/polygon.py create mode 100644 src/otx/api/entities/shapes/rectangle.py create mode 100644 src/otx/api/entities/shapes/shape.py create mode 100644 src/otx/api/entities/subset.py create mode 100644 src/otx/api/entities/task_environment.py create mode 100644 src/otx/api/entities/tensor.py create mode 100644 src/otx/api/entities/train_parameters.py create mode 100644 src/otx/api/py.typed create mode 100644 src/otx/api/serialization/__init__.py create mode 100644 src/otx/api/serialization/datetime_mapper.py create mode 100644 src/otx/api/serialization/id_mapper.py create mode 100644 src/otx/api/serialization/label_mapper.py create mode 100644 src/otx/api/usecases/__init__.py create mode 100644 src/otx/api/usecases/adapters/__init__.py create mode 100644 src/otx/api/usecases/adapters/model_adapter.py create mode 100644 src/otx/api/usecases/evaluation/__init__.py create mode 100644 src/otx/api/usecases/evaluation/accuracy.py create mode 100644 src/otx/api/usecases/evaluation/anomaly_metrics.py create mode 100644 src/otx/api/usecases/evaluation/averaging.py create mode 100644 src/otx/api/usecases/evaluation/basic_operations.py create mode 100644 src/otx/api/usecases/evaluation/dice.py create mode 100644 src/otx/api/usecases/evaluation/f_measure.py create mode 100644 src/otx/api/usecases/evaluation/metrics_helper.py create mode 100644 src/otx/api/usecases/evaluation/performance_provider_interface.py create mode 100644 src/otx/api/usecases/exportable_code/__init__.py create mode 100644 src/otx/api/usecases/exportable_code/demo/LICENSE create mode 100644 src/otx/api/usecases/exportable_code/demo/README.md create mode 100644 src/otx/api/usecases/exportable_code/demo/__init__.py create mode 100644 src/otx/api/usecases/exportable_code/demo/demo.py create mode 100644 src/otx/api/usecases/exportable_code/demo/demo_package/__init__.py create mode 100644 src/otx/api/usecases/exportable_code/demo/demo_package/executors/__init__.py create mode 100644 src/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py create mode 100644 src/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py create mode 100644 src/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py create mode 100644 src/otx/api/usecases/exportable_code/demo/demo_package/model_container.py create mode 100644 src/otx/api/usecases/exportable_code/demo/demo_package/utils.py create mode 100644 src/otx/api/usecases/exportable_code/demo/requirements.txt create mode 100644 src/otx/api/usecases/exportable_code/demo/setup.py create mode 100644 src/otx/api/usecases/exportable_code/inference/__init__.py create mode 100644 src/otx/api/usecases/exportable_code/inference/inference.py create mode 100644 src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py create mode 100644 src/otx/api/usecases/exportable_code/streamer/__init__.py rename src/otx/{core/exporter/exportable_code/demo/demo_package => api/usecases/exportable_code}/streamer/streamer.py (78%) create mode 100644 src/otx/api/usecases/exportable_code/visualizers/__init__.py create mode 100644 src/otx/api/usecases/exportable_code/visualizers/anomaly_visualizer.py create mode 100644 src/otx/api/usecases/exportable_code/visualizers/visualizer.py create mode 100644 src/otx/api/usecases/reporting/__init__.py create mode 100644 src/otx/api/usecases/reporting/callback.py create mode 100644 src/otx/api/usecases/reporting/time_monitor_callback.py create mode 100644 src/otx/api/usecases/tasks/__init__.py create mode 100644 src/otx/api/usecases/tasks/exceptions.py create mode 100644 src/otx/api/usecases/tasks/image_computer_vision.py create mode 100644 src/otx/api/usecases/tasks/image_deep_learning_task.py create mode 100644 src/otx/api/usecases/tasks/interfaces/__init__.py create mode 100644 src/otx/api/usecases/tasks/interfaces/deployment_interface.py create mode 100644 src/otx/api/usecases/tasks/interfaces/evaluate_interface.py create mode 100644 src/otx/api/usecases/tasks/interfaces/explain_interface.py create mode 100644 src/otx/api/usecases/tasks/interfaces/export_interface.py create mode 100644 src/otx/api/usecases/tasks/interfaces/inference_interface.py create mode 100644 src/otx/api/usecases/tasks/interfaces/optimization_interface.py create mode 100644 src/otx/api/usecases/tasks/interfaces/training_interface.py create mode 100644 src/otx/api/usecases/tasks/interfaces/unload_interface.py create mode 100644 src/otx/api/utils/__init__.py create mode 100644 src/otx/api/utils/anomaly_utils.py create mode 100644 src/otx/api/utils/argument_checks.py create mode 100644 src/otx/api/utils/async_pipeline.py create mode 100644 src/otx/api/utils/dataset_utils.py create mode 100644 src/otx/api/utils/detection_utils.py create mode 100644 src/otx/api/utils/importing.py create mode 100644 src/otx/api/utils/labels_utils.py create mode 100644 src/otx/api/utils/nms.py create mode 100644 src/otx/api/utils/segmentation_utils.py create mode 100644 src/otx/api/utils/shape_drawer.py create mode 100644 src/otx/api/utils/shape_factory.py create mode 100644 src/otx/api/utils/tiler.py create mode 100644 src/otx/api/utils/time_utils.py create mode 100644 src/otx/api/utils/vis_utils.py create mode 100644 src/otx/cli/builder/__init__.py create mode 100644 src/otx/cli/builder/builder.py create mode 100644 src/otx/cli/builder/supported_backbone/__init__.py create mode 100644 src/otx/cli/builder/supported_backbone/mmcls.json create mode 100644 src/otx/cli/builder/supported_backbone/mmdet.json create mode 100644 src/otx/cli/builder/supported_backbone/mmseg.json create mode 100644 src/otx/cli/builder/supported_backbone/omz.mmcls.json create mode 100644 src/otx/cli/builder/supported_backbone/otx.json create mode 100644 src/otx/cli/builder/supported_backbone/torchvision.json delete mode 100644 src/otx/cli/cli.py delete mode 100644 src/otx/cli/install.py create mode 100644 src/otx/cli/manager/__init__.py create mode 100644 src/otx/cli/manager/config_manager.py delete mode 100644 src/otx/cli/patches/NOTICE delete mode 100644 src/otx/cli/patches/mmaction2.patch create mode 100644 src/otx/cli/registry/__init__.py create mode 100644 src/otx/cli/registry/registry.py create mode 100644 src/otx/cli/tools/__init__.py create mode 100644 src/otx/cli/tools/build.py create mode 100644 src/otx/cli/tools/cli.py create mode 100644 src/otx/cli/tools/demo.py create mode 100644 src/otx/cli/tools/deploy.py create mode 100644 src/otx/cli/tools/eval.py create mode 100644 src/otx/cli/tools/explain.py create mode 100644 src/otx/cli/tools/export.py create mode 100644 src/otx/cli/tools/find.py create mode 100644 src/otx/cli/tools/optimize.py create mode 100644 src/otx/cli/tools/train.py create mode 100644 src/otx/cli/tools/utils/__init__.py create mode 100644 src/otx/cli/tools/utils/demo/__init__.py create mode 100644 src/otx/cli/tools/utils/demo/images_capture.py create mode 100644 src/otx/cli/tools/utils/demo/visualization.py create mode 100644 src/otx/cli/utils/config.py create mode 100644 src/otx/cli/utils/errors.py create mode 100644 src/otx/cli/utils/experiment.py delete mode 100644 src/otx/cli/utils/help_formatter.py create mode 100644 src/otx/cli/utils/hpo.py create mode 100644 src/otx/cli/utils/importing.py delete mode 100644 src/otx/cli/utils/installation.py create mode 100644 src/otx/cli/utils/io.py delete mode 100644 src/otx/cli/utils/jsonargparse.py create mode 100644 src/otx/cli/utils/multi_gpu.py create mode 100644 src/otx/cli/utils/nncf.py create mode 100644 src/otx/cli/utils/parser.py create mode 100644 src/otx/cli/utils/report.py create mode 100644 src/otx/cli/utils/telemetry.py delete mode 100644 src/otx/cli/utils/workspace.py delete mode 100644 src/otx/config/data/openvino.yaml delete mode 100644 src/otx/core/config/__init__.py delete mode 100644 src/otx/core/config/data.py delete mode 100644 src/otx/core/config/device.py delete mode 100644 src/otx/core/config/explain.py delete mode 100644 src/otx/core/config/hpo.py create mode 100644 src/otx/core/data/adapter/__init__.py create mode 100644 src/otx/core/data/adapter/action_dataset_adapter.py create mode 100644 src/otx/core/data/adapter/anomaly_dataset_adapter.py create mode 100644 src/otx/core/data/adapter/base_dataset_adapter.py create mode 100644 src/otx/core/data/adapter/classification_dataset_adapter.py create mode 100644 src/otx/core/data/adapter/detection_dataset_adapter.py create mode 100644 src/otx/core/data/adapter/segmentation_dataset_adapter.py create mode 100644 src/otx/core/data/adapter/visual_prompting_dataset_adapter.py create mode 100644 src/otx/core/data/caching/__init__.py create mode 100644 src/otx/core/data/caching/mem_cache_handler.py create mode 100644 src/otx/core/data/caching/storage_cache.py delete mode 100644 src/otx/core/data/dataset/__init__.py delete mode 100644 src/otx/core/data/dataset/action_classification.py delete mode 100644 src/otx/core/data/dataset/action_detection.py delete mode 100644 src/otx/core/data/dataset/anomaly/__init__.py delete mode 100644 src/otx/core/data/dataset/anomaly/dataset.py delete mode 100644 src/otx/core/data/dataset/base.py delete mode 100644 src/otx/core/data/dataset/classification.py delete mode 100644 src/otx/core/data/dataset/detection.py delete mode 100644 src/otx/core/data/dataset/instance_segmentation.py delete mode 100644 src/otx/core/data/dataset/segmentation.py delete mode 100644 src/otx/core/data/dataset/tile.py delete mode 100644 src/otx/core/data/dataset/visual_prompting.py delete mode 100644 src/otx/core/data/entity/__init__.py delete mode 100644 src/otx/core/data/entity/action_classification.py delete mode 100644 src/otx/core/data/entity/action_detection.py delete mode 100644 src/otx/core/data/entity/anomaly/__init__.py delete mode 100644 src/otx/core/data/entity/anomaly/classification.py delete mode 100644 src/otx/core/data/entity/anomaly/detection.py delete mode 100644 src/otx/core/data/entity/anomaly/segmentation.py delete mode 100644 src/otx/core/data/entity/base.py delete mode 100644 src/otx/core/data/entity/classification.py delete mode 100644 src/otx/core/data/entity/detection.py delete mode 100644 src/otx/core/data/entity/instance_segmentation.py delete mode 100644 src/otx/core/data/entity/segmentation.py delete mode 100644 src/otx/core/data/entity/tile.py delete mode 100644 src/otx/core/data/entity/utils.py delete mode 100644 src/otx/core/data/entity/visual_prompting.py delete mode 100644 src/otx/core/data/factory.py create mode 100644 src/otx/core/data/manager/__init__.py create mode 100644 src/otx/core/data/manager/dataset_manager.py delete mode 100644 src/otx/core/data/mem_cache.py delete mode 100644 src/otx/core/data/module.py create mode 100644 src/otx/core/data/noisy_label_detection/__init__.py create mode 100644 src/otx/core/data/noisy_label_detection/base.py create mode 100644 src/otx/core/data/pipelines/__init__.py delete mode 100644 src/otx/core/data/pre_filtering.py delete mode 100644 src/otx/core/data/tile_adaptor.py delete mode 100644 src/otx/core/data/transform_libs/__init__.py delete mode 100644 src/otx/core/data/transform_libs/mmaction.py delete mode 100644 src/otx/core/data/transform_libs/mmcv.py delete mode 100644 src/otx/core/data/transform_libs/mmdet.py delete mode 100644 src/otx/core/data/transform_libs/mmpretrain.py delete mode 100644 src/otx/core/data/transform_libs/mmseg.py delete mode 100644 src/otx/core/data/transform_libs/torchvision.py delete mode 100644 src/otx/core/exporter/__init__.py delete mode 100644 src/otx/core/exporter/base.py delete mode 100644 src/otx/core/exporter/exportable_code/__init__.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/README.md delete mode 100644 src/otx/core/exporter/exportable_code/demo/__init__.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo_package/__init__.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo_package/executors/__init__.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo_package/executors/asynchronous.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo_package/executors/synchronous.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo_package/model_wrapper.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo_package/streamer/__init__.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo_package/utils.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/__init__.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/vis_utils.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/visualizer.py delete mode 100644 src/otx/core/exporter/exportable_code/demo/requirements.txt delete mode 100644 src/otx/core/exporter/exportable_code/demo/setup.py delete mode 100644 src/otx/core/exporter/mmdeploy.py delete mode 100644 src/otx/core/exporter/native.py delete mode 100644 src/otx/core/exporter/visual_prompting.py create mode 100644 src/otx/core/file.py delete mode 100644 src/otx/core/metrics/__init__.py delete mode 100644 src/otx/core/metrics/accuracy.py delete mode 100644 src/otx/core/metrics/fmeasure.py delete mode 100644 src/otx/core/model/__init__.py delete mode 100644 src/otx/core/model/entity/__init__.py delete mode 100644 src/otx/core/model/entity/action_classification.py delete mode 100644 src/otx/core/model/entity/action_detection.py delete mode 100644 src/otx/core/model/entity/base.py delete mode 100644 src/otx/core/model/entity/classification.py delete mode 100644 src/otx/core/model/entity/detection.py delete mode 100644 src/otx/core/model/entity/instance_segmentation.py delete mode 100644 src/otx/core/model/entity/rotated_detection.py delete mode 100644 src/otx/core/model/entity/segmentation.py delete mode 100644 src/otx/core/model/entity/utils/__init__.py delete mode 100644 src/otx/core/model/entity/utils/mmaction.py delete mode 100644 src/otx/core/model/entity/utils/mmdet.py delete mode 100644 src/otx/core/model/entity/utils/mmpretrain.py delete mode 100644 src/otx/core/model/entity/utils/mmseg.py delete mode 100644 src/otx/core/model/entity/visual_prompting.py delete mode 100644 src/otx/core/model/module/__init__.py delete mode 100644 src/otx/core/model/module/action_classification.py delete mode 100644 src/otx/core/model/module/action_detection.py delete mode 100644 src/otx/core/model/module/anomaly/__init__.py delete mode 100644 src/otx/core/model/module/anomaly/anomaly_lightning.py delete mode 100644 src/otx/core/model/module/base.py delete mode 100644 src/otx/core/model/module/classification.py delete mode 100644 src/otx/core/model/module/detection.py delete mode 100644 src/otx/core/model/module/instance_segmentation.py delete mode 100644 src/otx/core/model/module/rotated_detection.py delete mode 100644 src/otx/core/model/module/segmentation.py delete mode 100644 src/otx/core/model/module/visual_prompting.py create mode 100644 src/otx/core/ov/__init__.py create mode 100644 src/otx/core/ov/graph/__init__.py create mode 100644 src/otx/core/ov/graph/graph.py create mode 100644 src/otx/core/ov/graph/parsers/__init__.py create mode 100644 src/otx/core/ov/graph/parsers/builder.py create mode 100644 src/otx/core/ov/graph/parsers/cls/__init__.py create mode 100644 src/otx/core/ov/graph/parsers/cls/cls_base_parser.py create mode 100644 src/otx/core/ov/graph/parsers/parser.py create mode 100644 src/otx/core/ov/graph/utils.py create mode 100644 src/otx/core/ov/models/__init__.py create mode 100644 src/otx/core/ov/models/mmov_model.py create mode 100644 src/otx/core/ov/models/ov_model.py create mode 100644 src/otx/core/ov/models/parser_mixin.py create mode 100644 src/otx/core/ov/omz_wrapper.py create mode 100644 src/otx/core/ov/ops/__init__.py create mode 100644 src/otx/core/ov/ops/activations.py create mode 100644 src/otx/core/ov/ops/arithmetics.py create mode 100644 src/otx/core/ov/ops/builder.py create mode 100644 src/otx/core/ov/ops/convolutions.py create mode 100644 src/otx/core/ov/ops/generation.py create mode 100644 src/otx/core/ov/ops/image_processings.py create mode 100644 src/otx/core/ov/ops/infrastructures.py create mode 100644 src/otx/core/ov/ops/matmuls.py create mode 100644 src/otx/core/ov/ops/modules/__init__.py create mode 100644 src/otx/core/ov/ops/modules/op_module.py create mode 100644 src/otx/core/ov/ops/movements.py create mode 100644 src/otx/core/ov/ops/normalizations.py create mode 100644 src/otx/core/ov/ops/object_detections.py create mode 100644 src/otx/core/ov/ops/op.py create mode 100644 src/otx/core/ov/ops/poolings.py create mode 100644 src/otx/core/ov/ops/reductions.py create mode 100644 src/otx/core/ov/ops/shape_manipulations.py create mode 100644 src/otx/core/ov/ops/sorting_maximization.py create mode 100644 src/otx/core/ov/ops/type_conversions.py create mode 100644 src/otx/core/ov/ops/utils.py create mode 100644 src/otx/core/ov/registry.py create mode 100644 src/otx/core/ov/utils.py create mode 100644 src/otx/core/patcher.py delete mode 100644 src/otx/core/types/__init__.py delete mode 100644 src/otx/core/types/device.py delete mode 100644 src/otx/core/types/explain.py delete mode 100644 src/otx/core/types/export.py delete mode 100644 src/otx/core/types/image.py delete mode 100644 src/otx/core/types/precision.py delete mode 100644 src/otx/core/types/task.py delete mode 100644 src/otx/core/types/transformer_libs.py delete mode 100644 src/otx/core/utils/__init__.py delete mode 100644 src/otx/core/utils/build.py delete mode 100644 src/otx/core/utils/cache.py delete mode 100644 src/otx/core/utils/config.py delete mode 100644 src/otx/core/utils/imports.py delete mode 100644 src/otx/core/utils/instantiators.py delete mode 100644 src/otx/core/utils/mask_util.py delete mode 100644 src/otx/core/utils/pylogger.py delete mode 100644 src/otx/core/utils/tile_merge.py delete mode 100644 src/otx/core/utils/utils.py delete mode 100644 src/otx/data/__init__.py delete mode 100644 src/otx/data/anomaly/__init__.py delete mode 100644 src/otx/data/anomaly/anomaly.py delete mode 100644 src/otx/engine/__init__.py delete mode 100644 src/otx/engine/engine.py delete mode 100644 src/otx/engine/hpo/__init__.py delete mode 100644 src/otx/engine/hpo/hpo_api.py delete mode 100644 src/otx/engine/hpo/hpo_trial.py delete mode 100644 src/otx/engine/hpo/utils.py delete mode 100644 src/otx/engine/utils/__init__.py delete mode 100644 src/otx/engine/utils/api.py delete mode 100644 src/otx/engine/utils/auto_configurator.py delete mode 100644 src/otx/recipe/__init__.py delete mode 100644 src/otx/recipe/_base_/data/mmaction_base.yaml delete mode 100644 src/otx/recipe/_base_/data/mmdet_base.yaml delete mode 100644 src/otx/recipe/_base_/data/mmpretrain_base.yaml delete mode 100644 src/otx/recipe/_base_/data/mmseg_base.yaml delete mode 100644 src/otx/recipe/_base_/data/torchvision_base.yaml delete mode 100644 src/otx/recipe/_base_/test.yaml delete mode 100644 src/otx/recipe/_base_/train.yaml delete mode 100644 src/otx/recipe/action/action_classification/movinet.yaml delete mode 100644 src/otx/recipe/action/action_classification/openvino_model.yaml delete mode 100644 src/otx/recipe/action/action_classification/x3d.yaml delete mode 100644 src/otx/recipe/action/action_detection/x3d_fastrcnn.yaml delete mode 100644 src/otx/recipe/anomaly_classification/padim.yaml delete mode 100644 src/otx/recipe/anomaly_classification/stfpm.yaml delete mode 100644 src/otx/recipe/anomaly_detection/padim.yaml delete mode 100644 src/otx/recipe/anomaly_detection/stfpm.yaml delete mode 100644 src/otx/recipe/anomaly_segmentation/padim.yaml delete mode 100644 src/otx/recipe/anomaly_segmentation/stfpm.yaml delete mode 100644 src/otx/recipe/classification/h_label_cls/efficientnet_b0_light.yaml delete mode 100644 src/otx/recipe/classification/h_label_cls/efficientnet_v2_light.yaml delete mode 100644 src/otx/recipe/classification/h_label_cls/mobilenet_v3_large_light.yaml delete mode 100644 src/otx/recipe/classification/h_label_cls/openvino_model.yaml delete mode 100644 src/otx/recipe/classification/h_label_cls/otx_deit_tiny.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/efficientnet_b0_light.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/efficientnet_v2_light.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/mobilenet_v3_large_light.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/openvino_model.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/otx_deit_tiny.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/otx_dino_v2.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/otx_dino_v2_linear_probe.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/otx_efficientnet_b0.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/otx_efficientnet_v2.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/otx_mobilenet_v3_large.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b0.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b1.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b3.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b4.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/tv_efficientnet_v2_l.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/tv_mobilenet_v3_small.yaml delete mode 100644 src/otx/recipe/classification/multi_class_cls/tv_resnet_50.yaml delete mode 100644 src/otx/recipe/classification/multi_label_cls/efficientnet_b0_light.yaml delete mode 100644 src/otx/recipe/classification/multi_label_cls/efficientnet_v2_light.yaml delete mode 100644 src/otx/recipe/classification/multi_label_cls/mobilenet_v3_large_light.yaml delete mode 100644 src/otx/recipe/classification/multi_label_cls/openvino_model.yaml delete mode 100644 src/otx/recipe/classification/multi_label_cls/otx_deit_tiny.yaml delete mode 100644 src/otx/recipe/detection/atss_mobilenetv2.yaml delete mode 100644 src/otx/recipe/detection/atss_mobilenetv2_tile.yaml delete mode 100644 src/otx/recipe/detection/atss_r50_fpn.yaml delete mode 100644 src/otx/recipe/detection/atss_resnext101.yaml delete mode 100644 src/otx/recipe/detection/openvino_model.yaml delete mode 100644 src/otx/recipe/detection/rtmdet_tiny.yaml delete mode 100644 src/otx/recipe/detection/ssd_mobilenetv2.yaml delete mode 100644 src/otx/recipe/detection/ssd_mobilenetv2_tile.yaml delete mode 100644 src/otx/recipe/detection/yolox_l.yaml delete mode 100644 src/otx/recipe/detection/yolox_l_tile.yaml delete mode 100644 src/otx/recipe/detection/yolox_s.yaml delete mode 100644 src/otx/recipe/detection/yolox_s_tile.yaml delete mode 100644 src/otx/recipe/detection/yolox_tiny.yaml delete mode 100644 src/otx/recipe/detection/yolox_tiny_tile.yaml delete mode 100644 src/otx/recipe/detection/yolox_x.yaml delete mode 100644 src/otx/recipe/detection/yolox_x_tile.yaml delete mode 100644 src/otx/recipe/instance_segmentation/maskrcnn_efficientnetb2b.yaml delete mode 100644 src/otx/recipe/instance_segmentation/maskrcnn_efficientnetb2b_tile.yaml delete mode 100644 src/otx/recipe/instance_segmentation/maskrcnn_r50.yaml delete mode 100644 src/otx/recipe/instance_segmentation/maskrcnn_r50_tile.yaml delete mode 100644 src/otx/recipe/instance_segmentation/maskrcnn_swint.yaml delete mode 100644 src/otx/recipe/instance_segmentation/maskrcnn_swint_tile.yaml delete mode 100644 src/otx/recipe/instance_segmentation/openvino_model.yaml delete mode 100644 src/otx/recipe/instance_segmentation/rtmdet_inst_tiny.yaml delete mode 100644 src/otx/recipe/rotated_detection/maskrcnn_efficientnetb2b.yaml delete mode 100644 src/otx/recipe/rotated_detection/maskrcnn_r50.yaml delete mode 100644 src/otx/recipe/semantic_segmentation/dino_v2.yaml delete mode 100644 src/otx/recipe/semantic_segmentation/litehrnet_18.yaml delete mode 100644 src/otx/recipe/semantic_segmentation/litehrnet_s.yaml delete mode 100644 src/otx/recipe/semantic_segmentation/litehrnet_x.yaml delete mode 100644 src/otx/recipe/semantic_segmentation/openvino_model.yaml delete mode 100644 src/otx/recipe/semantic_segmentation/segnext_b.yaml delete mode 100644 src/otx/recipe/semantic_segmentation/segnext_s.yaml delete mode 100644 src/otx/recipe/semantic_segmentation/segnext_t.yaml delete mode 100644 src/otx/recipe/visual_prompting/openvino_model.yaml delete mode 100644 src/otx/recipe/visual_prompting/sam_tiny_vit.yaml delete mode 100644 src/otx/recipe/visual_prompting/sam_vit_b.yaml delete mode 100644 src/otx/recipe/zero_shot_visual_prompting/openvino_model.yaml delete mode 100644 src/otx/recipe/zero_shot_visual_prompting/sam_tiny_vit.yaml delete mode 100644 src/otx/recipe/zero_shot_visual_prompting/sam_vit_b.yaml create mode 100644 src/otx/recipes/__init__.py create mode 100644 src/otx/recipes/stages/__init__.py create mode 100644 src/otx/recipes/stages/_base_/__init__.py create mode 100644 src/otx/recipes/stages/_base_/data/__init__.py create mode 100644 src/otx/recipes/stages/_base_/data/coco.py create mode 100644 src/otx/recipes/stages/_base_/data/coco_inst_seg.py create mode 100644 src/otx/recipes/stages/_base_/data/coco_otx.py create mode 100644 src/otx/recipes/stages/_base_/data/coco_ubt.py create mode 100644 src/otx/recipes/stages/_base_/data/custom_seg.py create mode 100644 src/otx/recipes/stages/_base_/data/data.py create mode 100644 src/otx/recipes/stages/_base_/data/data_seg.py create mode 100644 src/otx/recipes/stages/_base_/data/pipelines/__init__.py create mode 100644 src/otx/recipes/stages/_base_/data/pipelines/coco_inst_seg_pipeline.py create mode 100644 src/otx/recipes/stages/_base_/data/pipelines/coco_otx_pipeline.py create mode 100644 src/otx/recipes/stages/_base_/data/pipelines/coco_resize_hflip_pad.py create mode 100644 src/otx/recipes/stages/_base_/data/pipelines/incr_seg.py create mode 100644 src/otx/recipes/stages/_base_/data/pipelines/twocrop_pipeline.py create mode 100644 src/otx/recipes/stages/_base_/data/pipelines/ubt.py create mode 100644 src/otx/recipes/stages/_base_/data/seg_semisl.py create mode 100644 src/otx/recipes/stages/_base_/data/selfsl_cls_data.py create mode 100644 src/otx/recipes/stages/_base_/data/selfsl_seg_data.py create mode 100644 src/otx/recipes/stages/_base_/data/twocrop_data.py create mode 100644 src/otx/recipes/stages/_base_/default.py create mode 100644 src/otx/recipes/stages/_base_/dist/__init__.py create mode 100644 src/otx/recipes/stages/_base_/dist/dist.py create mode 100644 src/otx/recipes/stages/_base_/logs/__init__.py create mode 100644 src/otx/recipes/stages/_base_/logs/log.py create mode 100644 src/otx/recipes/stages/_base_/logs/tensorboard_logger.py create mode 100644 src/otx/recipes/stages/_base_/logs/text_logger.py create mode 100644 src/otx/recipes/stages/_base_/models/__init__.py create mode 100644 src/otx/recipes/stages/_base_/models/classifiers/__init__.py create mode 100644 src/otx/recipes/stages/_base_/models/classifiers/classifier.py create mode 100644 src/otx/recipes/stages/_base_/models/cls_semisl.py create mode 100644 src/otx/recipes/stages/_base_/models/cls_supcon.py create mode 100644 src/otx/recipes/stages/_base_/models/detectors/__init__.py create mode 100644 src/otx/recipes/stages/_base_/models/detectors/detector.py create mode 100644 src/otx/recipes/stages/_base_/models/model.py create mode 100644 src/otx/recipes/stages/_base_/models/segmentors/__init__.py create mode 100644 src/otx/recipes/stages/_base_/models/segmentors/segmentor.py create mode 100644 src/otx/recipes/stages/_base_/optimizers/__init__.py create mode 100644 src/otx/recipes/stages/_base_/optimizers/adam.py create mode 100644 src/otx/recipes/stages/_base_/optimizers/lars.py create mode 100644 src/otx/recipes/stages/_base_/optimizers/optimizer.py create mode 100644 src/otx/recipes/stages/_base_/optimizers/sgd.py create mode 100644 src/otx/recipes/stages/_base_/runners/__init__.py create mode 100644 src/otx/recipes/stages/_base_/runners/epoch_runner.py create mode 100644 src/otx/recipes/stages/_base_/runners/epoch_runner_cancel.py create mode 100644 src/otx/recipes/stages/_base_/runners/iter_runner.py create mode 100644 src/otx/recipes/stages/_base_/runners/runner.py create mode 100644 src/otx/recipes/stages/_base_/schedules/1cycle.py create mode 100644 src/otx/recipes/stages/_base_/schedules/__init__.py create mode 100644 src/otx/recipes/stages/_base_/schedules/cos_anneal.py create mode 100644 src/otx/recipes/stages/_base_/schedules/plateau.py create mode 100644 src/otx/recipes/stages/_base_/schedules/schedule.py create mode 100644 src/otx/recipes/stages/classification/__init__.py create mode 100644 src/otx/recipes/stages/classification/finetune.yaml create mode 100644 src/otx/recipes/stages/classification/incremental.yaml create mode 100644 src/otx/recipes/stages/classification/multilabel/__init__.py create mode 100644 src/otx/recipes/stages/classification/multilabel/incremental.yaml create mode 100644 src/otx/recipes/stages/classification/multilabel/semisl.yaml create mode 100644 src/otx/recipes/stages/classification/multilabel/train.yaml create mode 100644 src/otx/recipes/stages/classification/selfsl.yaml create mode 100644 src/otx/recipes/stages/classification/semisl.yaml create mode 100644 src/otx/recipes/stages/classification/supcon.yaml create mode 100644 src/otx/recipes/stages/classification/train.yaml create mode 100644 src/otx/recipes/stages/detection/__init__.py create mode 100644 src/otx/recipes/stages/detection/finetune.py create mode 100644 src/otx/recipes/stages/detection/incremental.py create mode 100644 src/otx/recipes/stages/detection/semisl.py create mode 100644 src/otx/recipes/stages/detection/train.py create mode 100644 src/otx/recipes/stages/instance_segmentation/__init__.py create mode 100644 src/otx/recipes/stages/instance_segmentation/incremental.py create mode 100644 src/otx/recipes/stages/instance_segmentation/semisl.py create mode 100644 src/otx/recipes/stages/instance_segmentation/train.py create mode 100644 src/otx/recipes/stages/segmentation/__init__.py create mode 100644 src/otx/recipes/stages/segmentation/finetune.py create mode 100644 src/otx/recipes/stages/segmentation/incremental.py create mode 100644 src/otx/recipes/stages/segmentation/incremental_poly.py create mode 100644 src/otx/recipes/stages/segmentation/selfsl.py create mode 100644 src/otx/recipes/stages/segmentation/semisl.py create mode 100644 src/otx/recipes/stages/segmentation/semisl_poly.py create mode 100644 src/otx/recipes/stages/segmentation/supcon.py create mode 100644 src/otx/recipes/stages/segmentation/train.py delete mode 100644 src/otx/tools/__init__.py delete mode 100644 src/otx/tools/translate_mmrecipe.py create mode 100644 src/otx/utils/logger.py delete mode 100644 src/otx/utils/signal.py delete mode 100644 tests/assets/action_classification_dataset/test.csv delete mode 100644 tests/assets/action_classification_dataset/test/0.mp4 delete mode 100644 tests/assets/action_classification_dataset/test/1.mp4 delete mode 100644 tests/assets/action_classification_dataset/train.csv delete mode 100644 tests/assets/action_classification_dataset/train/0.mp4 delete mode 100644 tests/assets/action_classification_dataset/train/1.mp4 delete mode 100644 tests/assets/action_classification_dataset/val.csv delete mode 100644 tests/assets/action_classification_dataset/val/0.mp4 delete mode 100644 tests/assets/action_classification_dataset/val/1.mp4 delete mode 100644 tests/assets/action_detection_dataset/annotations/ava_action_list_v2.2.pbtxt delete mode 100644 tests/assets/action_detection_dataset/annotations/ava_test.csv delete mode 100644 tests/assets/action_detection_dataset/annotations/ava_train.csv delete mode 100644 tests/assets/action_detection_dataset/annotations/ava_val.csv delete mode 100644 tests/assets/action_detection_dataset/annotations/test.pkl delete mode 100644 tests/assets/action_detection_dataset/annotations/train.pkl delete mode 100644 tests/assets/action_detection_dataset/annotations/val.pkl delete mode 100644 tests/assets/action_detection_dataset/frames/train_video0/train_video0_0000.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video0/train_video0_0001.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video0/train_video0_0002.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video0/train_video0_0003.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video0/train_video0_0004.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video0/train_video0_0005.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video0/train_video0_0006.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video0/train_video0_0007.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video0/train_video0_0008.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video0/train_video0_0009.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video1/train_video1_0000.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video1/train_video1_0001.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video1/train_video1_0002.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video1/train_video1_0003.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video1/train_video1_0004.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video1/train_video1_0005.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video1/train_video1_0006.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video1/train_video1_0007.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video1/train_video1_0008.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video1/train_video1_0009.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video2/train_video2_0000.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video2/train_video2_0001.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video2/train_video2_0002.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video2/train_video2_0003.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video2/train_video2_0004.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video2/train_video2_0005.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video2/train_video2_0006.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video2/train_video2_0007.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video2/train_video2_0008.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video2/train_video2_0009.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video3/train_video3_0000.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video3/train_video3_0001.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video3/train_video3_0002.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video3/train_video3_0003.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video3/train_video3_0004.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video3/train_video3_0005.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video3/train_video3_0006.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video3/train_video3_0007.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video3/train_video3_0008.jpg delete mode 100644 tests/assets/action_detection_dataset/frames/train_video3/train_video3_0009.jpg create mode 100644 tests/assets/ade20k2017_dataset/dataset/training/street/1.jpg create mode 100644 tests/assets/ade20k2017_dataset/dataset/training/street/1_atr.txt create mode 100644 tests/assets/ade20k2017_dataset/dataset/training/street/1_parts_1.png create mode 100644 tests/assets/ade20k2017_dataset/dataset/training/street/1_seg.png create mode 100644 tests/assets/ade20k2017_dataset/dataset/validation/2.jpg create mode 100644 tests/assets/ade20k2017_dataset/dataset/validation/2_atr.txt create mode 100644 tests/assets/ade20k2017_dataset/dataset/validation/2_parts_1.png create mode 100644 tests/assets/ade20k2017_dataset/dataset/validation/2_parts_2.png create mode 100644 tests/assets/ade20k2017_dataset/dataset/validation/2_seg.png create mode 100644 tests/assets/ade20k2017_dataset/dataset_with_meta_file/dataset_meta.json create mode 100644 tests/assets/ade20k2017_dataset/dataset_with_meta_file/training/street/1.jpg create mode 100644 tests/assets/ade20k2017_dataset/dataset_with_meta_file/training/street/1_atr.txt create mode 100644 tests/assets/ade20k2017_dataset/dataset_with_meta_file/training/street/1_parts_1.png create mode 100644 tests/assets/ade20k2017_dataset/dataset_with_meta_file/training/street/1_seg.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/training/street/1.jpg create mode 100644 tests/assets/ade20k2020_dataset/dataset/training/street/1.json create mode 100644 tests/assets/ade20k2020_dataset/dataset/training/street/1/instance_000_ADE_train_1.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/training/street/1/instance_001_ADE_train_1.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/training/street/1/instance_002_ADE_train_1.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/training/street/1_parts_1.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/training/street/1_seg.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/validation/2.jpg create mode 100644 tests/assets/ade20k2020_dataset/dataset/validation/2.json create mode 100644 tests/assets/ade20k2020_dataset/dataset/validation/2/instance_000_ADE_val_2.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/validation/2/instance_001_ADE_val_2.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/validation/2/instance_002_ADE_val_2.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/validation/2/instance_003_ADE_val_2.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/validation/2_parts_1.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/validation/2_parts_2.png create mode 100644 tests/assets/ade20k2020_dataset/dataset/validation/2_seg.png create mode 100644 tests/assets/ade20k2020_dataset/dataset_with_meta_file/dataset_meta.json create mode 100644 tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1.jpg create mode 100644 tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1.json create mode 100644 tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1/instance_000_ADE_train_1.png create mode 100644 tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1/instance_001_ADE_train_1.png create mode 100644 tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1/instance_002_ADE_train_1.png create mode 100644 tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1_parts_1.png create mode 100644 tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1_seg.png create mode 100644 tests/assets/anomaly/classification/test.json create mode 100644 tests/assets/anomaly/classification/train.json create mode 100644 tests/assets/anomaly/classification/val.json create mode 100644 tests/assets/anomaly/detection/test.json create mode 100644 tests/assets/anomaly/detection/train.json create mode 100644 tests/assets/anomaly/detection/val.json rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/00.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/01.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/02.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/03.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/04.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/05.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/06.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/07.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/08.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/09.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/10.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/11.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/12.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/13.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/14.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/15.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/ground_truth/colour/16.png (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/00.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/01.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/02.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/03.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/04.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/05.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/06.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/07.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/08.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/09.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/10.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/11.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/12.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/13.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/14.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/15.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/colour/16.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/good/04.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/good/05.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/good/13.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/good/23.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/good/25.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/test/good/28.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/00.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/01.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/02.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/03.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/06.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/07.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/08.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/09.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/10.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/11.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/12.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/14.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/15.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/16.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/17.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/18.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/19.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/20.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/21.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/22.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/24.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/26.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/27.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/29.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/30.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/31.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/32.jpg (100%) rename tests/assets/{anomaly_hazelnut => anomaly/hazelnut}/train/good/33.jpg (100%) create mode 100644 tests/assets/anomaly/segmentation/test.json create mode 100644 tests/assets/anomaly/segmentation/train.json create mode 100644 tests/assets/anomaly/segmentation/val.json delete mode 100644 tests/assets/car_tree_bug/annotations/instances_test.json create mode 100644 tests/assets/car_tree_bug/annotations/instances_train_5_imgs.json create mode 100644 tests/assets/car_tree_bug/annotations/instances_val_1_imgs.json delete mode 100644 tests/assets/car_tree_bug/images/test/Slide20.PNG delete mode 100644 tests/assets/car_tree_bug/images/test/Slide3.PNG delete mode 100644 tests/assets/car_tree_bug/images/val/Slide20.PNG rename tests/assets/{car_tree_bug_zero_shot/images/test => car_tree_bug/images/val}/Slide4.PNG (100%) create mode 100644 tests/assets/car_tree_bug/images/val/Slide5.PNG delete mode 100644 tests/assets/car_tree_bug_zero_shot/annotations/instances_test.json delete mode 100644 tests/assets/car_tree_bug_zero_shot/images/test/Slide3.PNG delete mode 100644 tests/assets/car_tree_bug_zero_shot/images/test/Slide5.PNG delete mode 100644 tests/assets/car_tree_bug_zero_shot/images/test/Slide6.PNG delete mode 100644 tests/assets/car_tree_bug_zero_shot/images/test/Slide7.PNG delete mode 100644 tests/assets/car_tree_bug_zero_shot/images/test/Slide8.PNG delete mode 100644 tests/assets/car_tree_bug_zero_shot/images/test/Slide9.PNG create mode 100644 tests/assets/cityscapes_dataset/dataset/gtFine/test/defaultcity/defaultcity_000001_000031_gtFine_instanceIds.png create mode 100644 tests/assets/cityscapes_dataset/dataset/gtFine/test/defaultcity/defaultcity_000001_000032_gtFine_instanceIds.png create mode 100644 tests/assets/cityscapes_dataset/dataset/gtFine/train/defaultcity/defaultcity_000002_000045_gtFine_instanceIds.png create mode 100644 tests/assets/cityscapes_dataset/dataset/gtFine/val/defaultcity/defaultcity_000001_000019_gtFine_instanceIds.png create mode 100644 tests/assets/cityscapes_dataset/dataset/leftImg8bit/test/defaultcity/defaultcity_000001_000031_leftImg8bit.png create mode 100644 tests/assets/cityscapes_dataset/dataset/leftImg8bit/test/defaultcity/defaultcity_000001_000032_leftImg8bit.png create mode 100644 tests/assets/cityscapes_dataset/dataset/leftImg8bit/train/defaultcity/defaultcity_000002_000045_leftImg8bit.png create mode 100644 tests/assets/cityscapes_dataset/dataset/leftImg8bit/val/defaultcity/defaultcity_000001_000019_leftImg8bit.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000019_gtFine_instanceIds.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000019_gtFine_labelTrainIds.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000031_gtFine_instanceIds.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000031_gtFine_labelTrainIds.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000032_gtFine_instanceIds.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000032_gtFine_labelTrainIds.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000002_000045_gtFine_instanceIds.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000002_000045_gtFine_labelTrainIds.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000001_000019_leftImg8bit.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000001_000031_leftImg8bit.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000001_000032_leftImg8bit.png create mode 100644 tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000002_000045_leftImg8bit.png rename tests/assets/classification_dataset/{test => }/0/11.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/14.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/16.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/17.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/18.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/23.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/26.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/3.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/30.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/4.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/5.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/6.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/7.jpg (100%) rename tests/assets/classification_dataset/{test => }/0/8.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/0.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/1.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/10.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/12.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/15.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/19.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/2.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/21.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/27.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/28.jpg (100%) rename tests/assets/classification_dataset/{test => }/1/29.jpg (100%) delete mode 100644 tests/assets/classification_dataset/val/0/11.jpg delete mode 100644 tests/assets/classification_dataset/val/0/14.jpg delete mode 100644 tests/assets/classification_dataset/val/0/16.jpg delete mode 100644 tests/assets/classification_dataset/val/0/17.jpg delete mode 100644 tests/assets/classification_dataset/val/0/18.jpg delete mode 100644 tests/assets/classification_dataset/val/0/23.jpg delete mode 100644 tests/assets/classification_dataset/val/0/26.jpg delete mode 100644 tests/assets/classification_dataset/val/0/3.jpg delete mode 100644 tests/assets/classification_dataset/val/0/30.jpg delete mode 100644 tests/assets/classification_dataset/val/0/4.jpg delete mode 100644 tests/assets/classification_dataset/val/0/5.jpg delete mode 100644 tests/assets/classification_dataset/val/0/6.jpg delete mode 100644 tests/assets/classification_dataset/val/0/7.jpg delete mode 100644 tests/assets/classification_dataset/val/0/8.jpg delete mode 100644 tests/assets/classification_dataset/val/1/0.jpg delete mode 100644 tests/assets/classification_dataset/val/1/1.jpg delete mode 100644 tests/assets/classification_dataset/val/1/10.jpg delete mode 100644 tests/assets/classification_dataset/val/1/12.jpg delete mode 100644 tests/assets/classification_dataset/val/1/15.jpg delete mode 100644 tests/assets/classification_dataset/val/1/19.jpg delete mode 100644 tests/assets/classification_dataset/val/1/2.jpg delete mode 100644 tests/assets/classification_dataset/val/1/21.jpg delete mode 100644 tests/assets/classification_dataset/val/1/27.jpg delete mode 100644 tests/assets/classification_dataset/val/1/28.jpg delete mode 100644 tests/assets/classification_dataset/val/1/29.jpg rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/11.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/14.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/16.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/17.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/18.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/23.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/26.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/3.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/30.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/4.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/5.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/6.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/7.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/0/8.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/0.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/1.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/10.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/12.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/15.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/19.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/2.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/21.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/27.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/28.jpg (100%) rename tests/assets/{classification_dataset/train => classification_dataset_class_incremental}/1/29.jpg (100%) create mode 100644 tests/assets/classification_dataset_class_incremental/2/13.jpg create mode 100644 tests/assets/classification_dataset_class_incremental/2/20.jpg create mode 100644 tests/assets/classification_dataset_class_incremental/2/22.jpg create mode 100644 tests/assets/classification_dataset_class_incremental/2/24.jpg create mode 100644 tests/assets/classification_dataset_class_incremental/2/25.jpg create mode 100644 tests/assets/classification_dataset_class_incremental/2/31.jpg create mode 100644 tests/assets/classification_dataset_class_incremental/2/9.jpg create mode 100644 tests/assets/classification_dataset_class_incremental/3/.gitignore delete mode 100644 tests/assets/common_semantic_segmentation_dataset/supervised/val/dataset_meta.json delete mode 100644 tests/assets/common_semantic_segmentation_dataset/supervised/val/images/0001.png delete mode 100644 tests/assets/common_semantic_segmentation_dataset/supervised/val/images/0002.png delete mode 100644 tests/assets/common_semantic_segmentation_dataset/supervised/val/masks/0001.png delete mode 100644 tests/assets/common_semantic_segmentation_dataset/supervised/val/masks/0002.png rename tests/assets/common_semantic_segmentation_dataset/{supervised/test => train}/dataset_meta.json (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised => }/train/images/0001.png (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised => }/train/images/0002.png (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised => }/train/images/0003.png (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised => }/train/masks/0001.png (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised => }/train/masks/0002.png (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised => }/train/masks/0003.png (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised/train => val}/dataset_meta.json (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised/test => val}/images/0001.png (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised/test => val}/images/0002.png (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised/test => val}/masks/0001.png (100%) rename tests/assets/common_semantic_segmentation_dataset/{supervised/test => val}/masks/0002.png (100%) create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/annotations.xml create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/images/00020.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/images/00021.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/images/00022.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/images/00023.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/images/00024.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/images/00025.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/images/00026.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/images/00027.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/images/00028.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/0/images/00029.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/annotations.xml create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/images/00020.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/images/00021.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/images/00022.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/images/00023.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/images/00024.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/images/00025.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/images/00026.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/images/00027.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/images/00028.jpg create mode 100644 tests/assets/cvat_dataset/action_classification/train/1/images/00029.jpg create mode 100644 tests/assets/cvat_dataset/action_detection/train.pkl create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/annotations.xml create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/images/frame_000000.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/images/frame_000001.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/images/frame_000002.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/images/frame_000003.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/images/frame_000004.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/images/frame_000005.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/images/frame_000006.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/images/frame_000007.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/images/frame_000008.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/0/images/frame_000009.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/annotations.xml create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/images/frame_000000.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/images/frame_000001.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/images/frame_000002.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/images/frame_000003.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/images/frame_000004.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/images/frame_000005.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/images/frame_000006.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/images/frame_000007.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/images/frame_000008.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/1/images/frame_000009.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/annotations.xml create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/images/frame_000000.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/images/frame_000001.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/images/frame_000002.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/images/frame_000003.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/images/frame_000004.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/images/frame_000005.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/images/frame_000006.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/images/frame_000007.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/images/frame_000008.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/2/images/frame_000009.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/annotations.xml create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/images/frame_000000.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/images/frame_000001.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/images/frame_000002.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/images/frame_000003.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/images/frame_000004.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/images/frame_000005.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/images/frame_000006.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/images/frame_000007.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/images/frame_000008.png create mode 100644 tests/assets/cvat_dataset/action_detection/train/3/images/frame_000009.png create mode 100644 tests/assets/datumaro_h-label/annotations/train.json create mode 100644 tests/assets/datumaro_h-label/annotations/valid.json create mode 100644 tests/assets/datumaro_h-label/images/train/00.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/01.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/02.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/03.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/04.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/05.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/06.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/07.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/08.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/09.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/10.jpg create mode 100644 tests/assets/datumaro_h-label/images/train/11.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/00.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/01.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/02.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/03.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/04.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/05.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/06.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/07.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/08.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/09.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/10.jpg create mode 100644 tests/assets/datumaro_h-label/images/valid/11.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/annotations/train.json create mode 100644 tests/assets/datumaro_h-label_class_decremental/annotations/valid.json create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/00.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/01.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/02.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/03.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/04.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/05.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/06.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/07.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/08.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/09.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/10.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/11.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/00.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/01.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/02.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/03.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/04.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/05.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/06.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/07.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/08.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/09.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/10.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/valid/11.jpg create mode 100644 tests/assets/datumaro_multilabel/annotations/train.json create mode 100644 tests/assets/datumaro_multilabel/annotations/valid.json create mode 100644 tests/assets/datumaro_multilabel/images/train/00.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/01.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/02.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/03.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/04.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/05.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/06.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/07.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/08.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/09.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/10.jpg create mode 100644 tests/assets/datumaro_multilabel/images/train/11.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/00.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/01.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/02.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/03.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/04.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/05.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/06.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/07.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/08.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/09.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/10.jpg create mode 100644 tests/assets/datumaro_multilabel/images/valid/11.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/annotations/train.json create mode 100644 tests/assets/datumaro_multilabel_class_decremental/annotations/valid.json create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/00.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/01.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/02.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/03.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/04.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/05.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/06.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/07.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/08.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/09.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/10.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/11.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/00.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/01.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/02.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/03.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/04.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/05.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/06.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/07.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/08.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/09.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/10.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/valid/11.jpg delete mode 100644 tests/assets/hlabel_classification/annotations/test.json delete mode 100644 tests/assets/hlabel_classification/annotations/train.json delete mode 100644 tests/assets/hlabel_classification/annotations/val.json delete mode 100644 tests/assets/hlabel_classification/images/test/0.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/1.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/10.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/11.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/12.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/13.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/14.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/15.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/16.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/17.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/18.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/19.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/2.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/20.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/3.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/4.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/5.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/6.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/7.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/8.jpg delete mode 100644 tests/assets/hlabel_classification/images/test/9.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/0.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/1.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/10.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/11.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/12.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/13.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/14.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/15.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/16.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/17.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/18.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/19.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/2.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/20.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/3.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/4.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/5.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/6.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/7.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/8.jpg delete mode 100644 tests/assets/hlabel_classification/images/train/9.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/0.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/1.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/10.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/11.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/12.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/13.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/14.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/15.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/16.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/17.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/18.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/19.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/2.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/20.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/3.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/4.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/5.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/6.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/7.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/8.jpg delete mode 100644 tests/assets/hlabel_classification/images/val/9.jpg delete mode 100644 tests/assets/mmdet_configs/rtmdet_tiny_8xb32-300e_coco.py delete mode 100644 tests/assets/multilabel_classification/annotations/test.json delete mode 100644 tests/assets/multilabel_classification/annotations/train.json delete mode 100644 tests/assets/multilabel_classification/annotations/val.json delete mode 100644 tests/assets/multilabel_classification/images/test/Slide12.jpg delete mode 100644 tests/assets/multilabel_classification/images/test/Slide6.jpg delete mode 100644 tests/assets/multilabel_classification/images/test/Slide8.jpg delete mode 100644 tests/assets/multilabel_classification/images/train/Slide1.jpg delete mode 100644 tests/assets/multilabel_classification/images/train/Slide10.jpg delete mode 100644 tests/assets/multilabel_classification/images/train/Slide11.jpg delete mode 100644 tests/assets/multilabel_classification/images/train/Slide2.jpg delete mode 100644 tests/assets/multilabel_classification/images/train/Slide3.jpg delete mode 100644 tests/assets/multilabel_classification/images/train/Slide4.jpg delete mode 100644 tests/assets/multilabel_classification/images/train/Slide5.jpg delete mode 100644 tests/assets/multilabel_classification/images/train/Slide7.jpg delete mode 100644 tests/assets/multilabel_classification/images/train/Slide9.jpg delete mode 100644 tests/assets/multilabel_classification/images/val/Slide1.jpg delete mode 100644 tests/assets/multilabel_classification/images/val/Slide10.jpg delete mode 100644 tests/assets/multilabel_classification/images/val/Slide11.jpg delete mode 100644 tests/assets/multilabel_classification/images/val/Slide2.jpg delete mode 100644 tests/assets/multilabel_classification/images/val/Slide3.jpg delete mode 100644 tests/assets/multilabel_classification/images/val/Slide4.jpg delete mode 100644 tests/assets/multilabel_classification/images/val/Slide5.jpg delete mode 100644 tests/assets/multilabel_classification/images/val/Slide7.jpg delete mode 100644 tests/assets/multilabel_classification/images/val/Slide9.jpg create mode 100644 tests/assets/small_objects/annotations/instances_test.json create mode 100644 tests/assets/small_objects/annotations/instances_train.json create mode 100644 tests/assets/small_objects/annotations/instances_val.json create mode 100644 tests/assets/small_objects/images/test/sample_0.jpg create mode 100644 tests/assets/small_objects/images/test/sample_1.jpg create mode 100644 tests/assets/small_objects/images/train/sample_2.jpg create mode 100644 tests/assets/small_objects/images/train/sample_3.jpg create mode 100644 tests/assets/small_objects/images/train/sample_4.jpg create mode 100644 tests/assets/small_objects/images/train/sample_6.jpg create mode 100644 tests/assets/small_objects/images/train/sample_7.jpg create mode 100644 tests/assets/small_objects/images/train/sample_9.jpg create mode 100644 tests/assets/small_objects/images/val/sample_5.jpg create mode 100644 tests/assets/small_objects/images/val/sample_8.jpg create mode 100644 tests/assets/unlabeled_dataset/0.jpg create mode 100644 tests/assets/unlabeled_dataset/1.jpg create mode 100644 tests/assets/unlabeled_dataset/2.jpg create mode 100644 tests/assets/unlabeled_dataset/3.jpg create mode 100644 tests/assets/unlabeled_dataset/4.jpg create mode 100644 tests/assets/unlabeled_dataset/5.jpg create mode 100644 tests/assets/unlabeled_dataset/6.jpg create mode 100644 tests/assets/unlabeled_dataset/a/0.jpg create mode 100644 tests/assets/unlabeled_dataset/a/1.jpg create mode 100644 tests/assets/unlabeled_dataset/a/2.jpg create mode 100644 tests/assets/unlabeled_dataset/a/3.jpg create mode 100644 tests/assets/unlabeled_dataset/a/4.jpg create mode 100644 tests/assets/unlabeled_dataset/a/5.jpg create mode 100644 tests/assets/unlabeled_dataset/a/6.jpg create mode 100644 tests/assets/unlabeled_dataset/unlabeled_file_list.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/Annotations/2007_000001.xml create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Action/test.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Action/train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Layout/test.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Layout/train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/aeroplane_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/background_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bicycle_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bird_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/boat_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bottle_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bus_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/car_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/cat_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/chair_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/cow_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/diningtable_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/dog_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/horse_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/ignored_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/motorbike_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/person_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/pottedplant_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/sheep_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/sofa_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/test.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/train_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/tvmonitor_train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/test.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/val.txt create mode 100644 tests/assets/voc_dataset/voc_dataset1/JPEGImages/2007_000001.jpg create mode 100644 tests/assets/voc_dataset/voc_dataset1/JPEGImages/2007_000002.jpg create mode 100644 tests/assets/voc_dataset/voc_dataset1/SegmentationClass/2007_000001.png create mode 100644 tests/assets/voc_dataset/voc_dataset1/SegmentationObject/2007_000001.png create mode 100644 tests/assets/voc_dataset/voc_dataset2/ImageSets/Main/train.txt create mode 100644 tests/assets/voc_dataset/voc_dataset2/JPEGImages/2007_000001.jpg create mode 100644 tests/assets/yolo_dataset/obj.data create mode 100644 tests/assets/yolo_dataset/obj.names create mode 100644 tests/assets/yolo_dataset/obj_train_data/1.jpg create mode 100644 tests/assets/yolo_dataset/obj_train_data/1.txt create mode 100644 tests/assets/yolo_dataset/train.txt create mode 100644 tests/e2e/__init__.py create mode 100644 tests/e2e/cli/__init__.py create mode 100644 tests/e2e/cli/action/__init__.py create mode 100644 tests/e2e/cli/action/test_action_classification.py create mode 100644 tests/e2e/cli/action/test_action_detection.py create mode 100644 tests/e2e/cli/anomaly/__init__.py create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_classification_padim/compressed_model.yml create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_classification_padim/nncf/nncf_quantization.dot create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_classification_stfpm/compressed_model.yml create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_classification_stfpm/nncf/nncf_quantization.dot create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_detection_padim/compressed_model.yml create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_detection_padim/nncf/nncf_quantization.dot create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_detection_stfpm/compressed_model.yml create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_detection_stfpm/nncf/nncf_quantization.dot create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_padim/compressed_model.yml create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_padim/nncf/nncf_quantization.dot create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_stfpm/compressed_model.yml create mode 100644 tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_stfpm/nncf/nncf_quantization.dot create mode 100644 tests/e2e/cli/anomaly/test_anomaly_classification.py create mode 100644 tests/e2e/cli/anomaly/test_anomaly_detection.py create mode 100644 tests/e2e/cli/anomaly/test_anomaly_segmentation.py create mode 100644 tests/e2e/cli/classification/__init__.py create mode 100644 tests/e2e/cli/classification/reference/Custom_Image_Classification_EfficientNet-V2-S/compressed_model.yml create mode 100644 tests/e2e/cli/classification/reference/Custom_Image_Classification_EfficinetNet-B0/compressed_model.yml create mode 100644 tests/e2e/cli/classification/reference/Custom_Image_Classification_MobileNet-V3-large-1x/compressed_model.yml create mode 100644 tests/e2e/cli/classification/test_api_xai_sanity_classification.py create mode 100644 tests/e2e/cli/classification/test_classification.py create mode 100644 tests/e2e/cli/detection/__init__.py create mode 100644 tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_ATSS/compressed_model.yml create mode 100644 tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_SSD/compressed_model.yml create mode 100644 tests/e2e/cli/detection/reference/Custom_Object_Detection_YOLOX/compressed_model.yml create mode 100644 tests/e2e/cli/detection/reference/Object_Detection_ResNeXt101_ATSS/compressed_model.yml create mode 100644 tests/e2e/cli/detection/reference/Object_Detection_YOLOX_L/compressed_model.yml create mode 100644 tests/e2e/cli/detection/reference/Object_Detection_YOLOX_S/compressed_model.yml create mode 100644 tests/e2e/cli/detection/reference/Object_Detection_YOLOX_X/compressed_model.yml create mode 100644 tests/e2e/cli/detection/test_api_xai_sanity_detection.py create mode 100644 tests/e2e/cli/detection/test_detection.py create mode 100644 tests/e2e/cli/detection/test_tiling_detection.py create mode 100644 tests/e2e/cli/instance_segmentation/__init__.py create mode 100644 tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_ConvNeXt/compressed_model.yml create mode 100644 tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B/compressed_model.yml create mode 100644 tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50/compressed_model.yml create mode 100644 tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_SwinT_FP16/compressed_model.yml create mode 100644 tests/e2e/cli/instance_segmentation/test_api_xai_sanity_instance_segmentation.py create mode 100644 tests/e2e/cli/instance_segmentation/test_instance_segmentation.py create mode 100644 tests/e2e/cli/instance_segmentation/test_tiling_instseg.py create mode 100644 tests/e2e/cli/semantic_segmentation/__init__.py create mode 100644 tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR/compressed_model.yml create mode 100644 tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-18_OCR/compressed_model.yml create mode 100644 tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-s-mod2_OCR/compressed_model.yml create mode 100644 tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-x-mod3_OCR/compressed_model.yml create mode 100644 tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_B/compressed_model.yml create mode 100644 tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_s/compressed_model.yml create mode 100644 tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_t/compressed_model.yml create mode 100644 tests/e2e/cli/semantic_segmentation/test_segmentation.py create mode 100644 tests/e2e/cli/test_cli.py rename tests/{integration/detection => e2e/cli/visual_prompting}/__init__.py (100%) create mode 100644 tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_Tiny_ViT/compressed_decoder.yml create mode 100644 tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_Tiny_ViT/compressed_image_encoder.yml create mode 100644 tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_decoder.yml create mode 100644 tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_image_encoder.yml create mode 100644 tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_Tiny_ViT/compressed_decoder.yml create mode 100644 tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_Tiny_ViT/compressed_image_encoder.yml create mode 100644 tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_ViT_B/compressed_decoder.yml create mode 100644 tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_ViT_B/compressed_image_encoder.yml create mode 100644 tests/e2e/cli/visual_prompting/test_visual_prompting.py create mode 100644 tests/e2e/cli/visual_prompting/test_zero_shot.py create mode 100644 tests/e2e/test_api_xai_sanity.py create mode 100644 tests/integration/api/action/__init__.py create mode 100644 tests/integration/api/action/test_api_action_classification.py create mode 100644 tests/integration/api/action/test_api_action_detection.py create mode 100644 tests/integration/api/classification/__init__.py create mode 100644 tests/integration/api/classification/test_api_classification.py create mode 100644 tests/integration/api/detection/__init__.py create mode 100644 tests/integration/api/detection/api_detection.py create mode 100644 tests/integration/api/detection/test_api_detection.py create mode 100644 tests/integration/api/segmentation/__init__.py create mode 100644 tests/integration/api/segmentation/test_api_segmentation.py delete mode 100644 tests/integration/api/test_auto_configuration.py delete mode 100644 tests/integration/api/test_engine_api.py delete mode 100644 tests/integration/api/test_xai.py create mode 100644 tests/integration/api/xai/__init__.py create mode 100644 tests/integration/cli/action/__init__.py create mode 100644 tests/integration/cli/action/test_action_classification.py create mode 100644 tests/integration/cli/action/test_action_detection.py create mode 100644 tests/integration/cli/anomaly/__init__.py create mode 100644 tests/integration/cli/anomaly/test_anomaly_classification.py create mode 100644 tests/integration/cli/anomaly/test_anomaly_detection.py create mode 100644 tests/integration/cli/anomaly/test_anomaly_segmentation.py create mode 100644 tests/integration/cli/classification/__init__.py create mode 100644 tests/integration/cli/classification/test_classification.py create mode 100644 tests/integration/cli/detection/__init__.py create mode 100644 tests/integration/cli/detection/test_detection.py create mode 100644 tests/integration/cli/detection/test_tiling_detection.py create mode 100644 tests/integration/cli/instance_segmentation/__init__.py create mode 100644 tests/integration/cli/instance_segmentation/test_instance_segmentation.py create mode 100644 tests/integration/cli/instance_segmentation/test_rotated_detection.py create mode 100644 tests/integration/cli/instance_segmentation/test_tiling_instseg.py create mode 100644 tests/integration/cli/semantic_segmentation/__init__.py create mode 100644 tests/integration/cli/semantic_segmentation/test_segmentation.py delete mode 100644 tests/integration/cli/test_auto_configuration.py delete mode 100644 tests/integration/cli/test_export_inference.py delete mode 100644 tests/integration/cli/utils.py create mode 100644 tests/integration/cli/visual_prompting/__init__.py create mode 100644 tests/integration/cli/visual_prompting/test_visual_prompting.py create mode 100644 tests/integration/cli/visual_prompting/test_zero_shot.py delete mode 100644 tests/integration/conftest.py delete mode 100644 tests/integration/detection/conftest.py delete mode 100644 tests/integration/detection/test_data_module.py delete mode 100644 tests/integration/detection/test_model.py delete mode 100644 tests/integration/test_tiling.py create mode 100644 tests/perf/benchmark-reference.csv create mode 100644 tests/pytest.ini create mode 100644 tests/regression/action/test_action_classification.py create mode 100644 tests/regression/action/test_action_detection.py create mode 100644 tests/regression/anomaly/test_anomaly_classificaiton.py create mode 100644 tests/regression/anomaly/test_anomaly_detection.py create mode 100644 tests/regression/anomaly/test_anomaly_segmentation.py create mode 100644 tests/regression/classification/test_classification.py create mode 100644 tests/regression/detection/test_detection.py create mode 100644 tests/regression/detection/test_tiling_detection.py create mode 100644 tests/regression/instance_segmentation/test_instance_segmentation.py create mode 100644 tests/regression/instance_segmentation/test_tiling_instance_segmentation.py create mode 100644 tests/regression/regression_command.py create mode 100644 tests/regression/regression_config.json create mode 100644 tests/regression/regression_test_helpers.py create mode 100644 tests/regression/semantic_segmentation/test_segmentation.py create mode 100644 tests/regression/summarize_test_results.py delete mode 100644 tests/regression/test_regression.py create mode 100644 tests/test_helpers.py create mode 100644 tests/test_suite/ARCHITECTURE.md create mode 100644 tests/test_suite/QUICK_HOWTO.md create mode 100644 tests/test_suite/__init__.py create mode 100644 tests/test_suite/e2e_test_system.py create mode 100644 tests/test_suite/fixtures.py create mode 100644 tests/test_suite/logging.py create mode 100644 tests/test_suite/pytest_insertions.py create mode 100644 tests/test_suite/run_test_command.py create mode 100644 tests/test_suite/training_test_case.py create mode 100644 tests/test_suite/training_tests_actions.py create mode 100644 tests/test_suite/training_tests_common.py create mode 100644 tests/test_suite/training_tests_helper.py create mode 100644 tests/test_suite/training_tests_stage.py delete mode 100644 tests/unit/algo/__init__.py delete mode 100644 tests/unit/algo/action_classification/__init__.py delete mode 100644 tests/unit/algo/action_classification/backbones/__init__.py delete mode 100644 tests/unit/algo/action_classification/backbones/test_movinet.py delete mode 100644 tests/unit/algo/action_classification/heads/__init__.py delete mode 100644 tests/unit/algo/action_classification/heads/test_movinet_head.py delete mode 100644 tests/unit/algo/action_classification/recognizers/__init__.py delete mode 100644 tests/unit/algo/action_classification/recognizers/test_movinet_recognizer.py delete mode 100644 tests/unit/algo/callbacks/test_adaptive_train_scheduling.py delete mode 100644 tests/unit/algo/callbacks/test_iteration_timer.py delete mode 100644 tests/unit/algo/classification/__init__.py delete mode 100644 tests/unit/algo/classification/backbones/__init__.py delete mode 100644 tests/unit/algo/classification/backbones/test_otx_efficientnet.py delete mode 100644 tests/unit/algo/classification/backbones/test_otx_efficientnet_v2.py delete mode 100644 tests/unit/algo/classification/backbones/test_otx_mobilenet_v3.py delete mode 100644 tests/unit/algo/classification/conftest.py delete mode 100644 tests/unit/algo/classification/heads/__init__.py delete mode 100644 tests/unit/algo/classification/heads/test_custom_hlabel_cls_head.py delete mode 100644 tests/unit/algo/classification/heads/test_custom_multilabel_cls_head.py delete mode 100644 tests/unit/algo/classification/losses/__init__.py delete mode 100644 tests/unit/algo/classification/losses/test_asymmetric_multilabel.py delete mode 100644 tests/unit/algo/classification/test_otx_dino_v2.py delete mode 100644 tests/unit/algo/classification/test_torchvision_model.py delete mode 100644 tests/unit/algo/detection/__init__.py delete mode 100644 tests/unit/algo/detection/backbones/__init__.py delete mode 100644 tests/unit/algo/detection/backbones/test_pytorchcv_backbones.py delete mode 100644 tests/unit/algo/detection/heads/__init__.py delete mode 100644 tests/unit/algo/detection/heads/test_class_incremental_mixin.py delete mode 100644 tests/unit/algo/detection/heads/test_custom_anchor_generator.py delete mode 100644 tests/unit/algo/detection/heads/test_custom_ssd_head.py delete mode 100644 tests/unit/algo/detection/test_ssd.py delete mode 100644 tests/unit/algo/hooks/__init__.py delete mode 100644 tests/unit/algo/hooks/test_saliency_map_dumping.py delete mode 100644 tests/unit/algo/hooks/test_saliency_map_processing.py delete mode 100644 tests/unit/algo/hooks/test_xai_hooks.py delete mode 100644 tests/unit/algo/instance_segmentation/__init__.py delete mode 100644 tests/unit/algo/instance_segmentation/heads/__init__.py delete mode 100644 tests/unit/algo/instance_segmentation/heads/test_custom_roi_head.py delete mode 100644 tests/unit/algo/instance_segmentation/test_evaluation.py delete mode 100644 tests/unit/algo/samplers/__init__.py delete mode 100644 tests/unit/algo/samplers/test_balanced_sampler.py delete mode 100644 tests/unit/algo/samplers/test_class_incremental_sampler.py delete mode 100644 tests/unit/algo/segmentation/__init__.py delete mode 100644 tests/unit/algo/segmentation/backbones/__init__.py delete mode 100644 tests/unit/algo/segmentation/backbones/litehrnet.py delete mode 100644 tests/unit/algo/segmentation/heads/__init__.py delete mode 100644 tests/unit/algo/segmentation/heads/test_class_incremental_mixin.py delete mode 100644 tests/unit/algo/utils/__init__.py delete mode 100644 tests/unit/algo/utils/test_support_otx_v1.py delete mode 100644 tests/unit/algo/visual_prompting/__init__.py delete mode 100644 tests/unit/algo/visual_prompting/backbones/__init__.py delete mode 100644 tests/unit/algo/visual_prompting/backbones/test_tiny_vit.py delete mode 100644 tests/unit/algo/visual_prompting/backbones/test_vit.py delete mode 100644 tests/unit/algo/visual_prompting/conftest.py delete mode 100644 tests/unit/algo/visual_prompting/decoders/__init__.py delete mode 100644 tests/unit/algo/visual_prompting/decoders/test_sam_mask_decoder.py delete mode 100644 tests/unit/algo/visual_prompting/encoders/__init__.py delete mode 100644 tests/unit/algo/visual_prompting/encoders/test_sam_image_encoder.py delete mode 100644 tests/unit/algo/visual_prompting/encoders/test_sam_prompt_encoder.py delete mode 100644 tests/unit/algo/visual_prompting/test_openvino_models.py delete mode 100644 tests/unit/algo/visual_prompting/test_segment_anything.py delete mode 100644 tests/unit/algo/visual_prompting/test_zero_shot_segment_anything.py delete mode 100644 tests/unit/algo/visual_prompting/utils/__init__.py delete mode 100644 tests/unit/algo/visual_prompting/utils/test_layer_norm_2d.py create mode 100644 tests/unit/algorithms/__init__.py create mode 100644 tests/unit/algorithms/action/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/data/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/data/pipelines/__init__.py create mode 100755 tests/unit/algorithms/action/adapters/mmaction/data/pipelines/test_action_loading.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/data/test_action_cls_dataset.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/data/test_action_det_dataset.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/backbones/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/backbones/test_action_movinet.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/backbones/test_action_register_backbone.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/detectors/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/detectors/test_action_fast_rcnn.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/heads/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/heads/test_action_movinet_head.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/heads/test_action_roi_head.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/recognizers/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/models/recognizers/test_action_movinet_recognizer.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/test_task.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/utils/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/mmaction/utils/test_action_det_eval_utils.py create mode 100755 tests/unit/algorithms/action/adapters/mmaction/utils/test_action_export_utils.py create mode 100644 tests/unit/algorithms/action/adapters/openvino/__init__.py create mode 100644 tests/unit/algorithms/action/adapters/openvino/test_action_dataloader.py create mode 100644 tests/unit/algorithms/action/adapters/openvino/test_action_openvino_models.py create mode 100644 tests/unit/algorithms/action/adapters/openvino/test_task.py create mode 100644 tests/unit/algorithms/action/test_helpers.py create mode 100644 tests/unit/algorithms/action/tools/__init__.py create mode 100644 tests/unit/algorithms/action/tools/test_action_sample_classification.py create mode 100644 tests/unit/algorithms/action/tools/test_action_sample_detection.py create mode 100644 tests/unit/algorithms/action/utils/__init__.py create mode 100644 tests/unit/algorithms/action/utils/test_action_convert_public_data_to_cvat.py create mode 100644 tests/unit/algorithms/action/utils/test_action_data.py create mode 100644 tests/unit/algorithms/anomaly/__init__.py create mode 100644 tests/unit/algorithms/anomaly/adapters/__init__.py create mode 100644 tests/unit/algorithms/anomaly/adapters/anomalib/accelerators/__init__.py create mode 100644 tests/unit/algorithms/anomaly/adapters/anomalib/accelerators/xpu.py create mode 100644 tests/unit/algorithms/anomaly/adapters/anomalib/plugins/__init__.py create mode 100644 tests/unit/algorithms/anomaly/adapters/anomalib/plugins/xpu_precision.py create mode 100644 tests/unit/algorithms/anomaly/adapters/anomalib/strategies/__init__.py create mode 100644 tests/unit/algorithms/anomaly/adapters/anomalib/strategies/test_xpu_single.py create mode 100644 tests/unit/algorithms/anomaly/adapters/callbacks/__init__.py create mode 100644 tests/unit/algorithms/anomaly/adapters/callbacks/test_inference_callback.py create mode 100644 tests/unit/algorithms/anomaly/adapters/callbacks/test_progress_callback.py create mode 100644 tests/unit/algorithms/anomaly/adapters/data/__init__.py create mode 100644 tests/unit/algorithms/anomaly/adapters/data/test_dataset.py create mode 100644 tests/unit/algorithms/anomaly/config/__init__.py create mode 100644 tests/unit/algorithms/anomaly/config/test_model_config_load.py create mode 100644 tests/unit/algorithms/anomaly/conftest.py create mode 100644 tests/unit/algorithms/anomaly/helpers/__init__.py create mode 100644 tests/unit/algorithms/anomaly/helpers/dummy_dataset.py create mode 100644 tests/unit/algorithms/anomaly/helpers/dummy_model.py create mode 100644 tests/unit/algorithms/anomaly/helpers/utils.py create mode 100644 tests/unit/algorithms/anomaly/tasks/__init__.py create mode 100644 tests/unit/algorithms/anomaly/tasks/test_inference.py create mode 100644 tests/unit/algorithms/anomaly/tasks/test_nncf.py create mode 100644 tests/unit/algorithms/anomaly/tasks/test_openvino.py create mode 100644 tests/unit/algorithms/anomaly/tasks/test_train.py create mode 100644 tests/unit/algorithms/classification/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/api/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/api/test_train.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/data/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/data/test_datasets.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/data/test_pipelines.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_byol.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_custom_image_classifier.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_semisl_classifier.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_semisl_mlc_classifier.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_supcon_classifier.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/heads/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_contrastive_head.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_cls_head.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_hierarchical_cls_head.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_multilabel_cls_head.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_multilabel_semisl.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_semisl_cls_head.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/heads/text_mixin.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/losses/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/losses/test_asymmetric_multilabel.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/losses/test_cross_entropy.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/necks/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/models/necks/test_selfsl_mlp.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/nncf/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_builder.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_patches.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_registers.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/optimizer/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/optimizer/test_lars.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/test_cls_config_builder.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py create mode 100644 tests/unit/algorithms/classification/adapters/mmcls/test_task.py create mode 100644 tests/unit/algorithms/classification/adapters/openvino/__init__.py create mode 100644 tests/unit/algorithms/classification/adapters/openvino/test_openvino_models.py create mode 100644 tests/unit/algorithms/classification/conftest.py create mode 100644 tests/unit/algorithms/classification/tasks/__init__.py create mode 100644 tests/unit/algorithms/classification/tasks/test_classification_nncf.py create mode 100644 tests/unit/algorithms/classification/tasks/test_classification_openvino_task.py create mode 100644 tests/unit/algorithms/classification/test_helper.py create mode 100644 tests/unit/algorithms/classification/test_xai_classification_validity.py create mode 100644 tests/unit/algorithms/classification/utils/__init__.py create mode 100644 tests/unit/algorithms/classification/utils/test_utils.py create mode 100644 tests/unit/algorithms/common/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_adaptive_repeat_data_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_adaptive_training_hooks.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_cancel_interface_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_checkpoint_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_composed_dataloader_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_early_stopping_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_ema_v2_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_eval_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_fp16_sam_optimizer_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_hooks.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_ib_loss_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_logger_replace_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_mean_teacher_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_model_ema_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_no_bias_decay_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_recording_forward_hooks.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_save_initial_weight_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_semisl_cls_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_task_adapt_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/hooks/test_xpu_optimizer_hook.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/nncf/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/nncf/test_helpers.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_hooks.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_runners.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_utils.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/ops/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/ops/test_multi_scale_deformable_attn_pytorch.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/pipelines/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/pipelines/test_load_image_from_otx_dataset.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_augments.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_augmix.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_otx_transforms.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_random_augment.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_twocrop_transform.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/tasks/test_exporter.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/tasks/test_helpers.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/tasks/test_version.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/test_configurer.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/utils/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/utils/test_automatic_bs.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/utils/test_config_utils.py create mode 100644 tests/unit/algorithms/common/adapters/mmcv/utils/test_fp16_utils.py create mode 100644 tests/unit/algorithms/common/adapters/mmdeploy/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/mmdeploy/ops/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/mmdeploy/ops/test_custom_ops.py create mode 100644 tests/unit/algorithms/common/adapters/mmdeploy/test_deploy_apis.py create mode 100644 tests/unit/algorithms/common/adapters/mmdeploy/test_helpers.py create mode 100644 tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_mmdeploy.py create mode 100644 tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_onnx.py create mode 100644 tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_operations_domain.py create mode 100644 tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_utils.py create mode 100644 tests/unit/algorithms/common/adapters/nncf/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/nncf/test_nncf_compression.py create mode 100644 tests/unit/algorithms/common/adapters/nncf/test_nncf_config.py create mode 100644 tests/unit/algorithms/common/adapters/nncf/test_nncf_patches.py create mode 100644 tests/unit/algorithms/common/adapters/nncf/test_nncf_utils.py create mode 100644 tests/unit/algorithms/common/adapters/torch/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/torch/amp/test_xpu_grad_scaler.py create mode 100644 tests/unit/algorithms/common/adapters/torch/dataloaders/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_balanced_sampler.py create mode 100644 tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_cls_incr_sampler.py create mode 100644 tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_otx_sampler.py create mode 100644 tests/unit/algorithms/common/adapters/torch/utils/__init__.py create mode 100644 tests/unit/algorithms/common/adapters/torch/utils/test_bs_search_algo.py create mode 100644 tests/unit/algorithms/common/adapters/torch/utils/test_utils.py rename tests/unit/{algo/callbacks => algorithms/common/configs}/__init__.py (100%) create mode 100644 tests/unit/algorithms/common/configs/test_configuration_enums.py create mode 100644 tests/unit/algorithms/common/utils/__init__.py create mode 100644 tests/unit/algorithms/common/utils/test_data.py create mode 100644 tests/unit/algorithms/common/utils/test_dist_utils.py create mode 100644 tests/unit/algorithms/common/utils/test_utils.py create mode 100644 tests/unit/algorithms/detection/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/api/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/api/test_train.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/datasets/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/test_load_pipelines.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/test_torchvision2mmdet.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/datasets/test_detection_dataset.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/hooks/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/hooks/test_det_class_probability_map_hook.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/hooks/test_tile_sampling_hook.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/assigners/test_custom_max_iou_assigner.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/backones/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/backones/test_ov_mmdet_mmov_backbone.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/conftest.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_loss_dynamics_tracking_heads.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_rpn_head.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_ssd_head.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_yolov3_head.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/detectors/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/detectors/conftest.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_atss_detector.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_deformable_detr_detector.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_dino_detector.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_single_stage_detector.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_two_stage_detector.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_vfnet_detector.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_loss_dynamics_tracking.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_mean_teacher.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/heads/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/heads/test_custom_dino_head.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/heads/test_custom_lite_dino_head.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/losses/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/losses/test_cross_focal_loss.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/losses/test_l2sp_loss.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/necks/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/necks/test_ov_mmdet_mmov_ssd_neck.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/test_ov_mmdet_single_level_roi_extractor.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/nncf/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/nncf/test_mmdet_nncf_builder.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/nncf/test_mmdet_nncf_patches.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/nncf/test_task.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/test_task.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/utils/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/utils/test_detection_builder.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/utils/test_detection_config_utils.py create mode 100644 tests/unit/algorithms/detection/adapters/mmdet/utils/test_exporter.py create mode 100644 tests/unit/algorithms/detection/adapters/openvino/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/openvino/model_wrappers/__init__.py create mode 100644 tests/unit/algorithms/detection/adapters/openvino/test_task.py create mode 100644 tests/unit/algorithms/detection/conftest.py create mode 100644 tests/unit/algorithms/detection/test_helpers.py create mode 100644 tests/unit/algorithms/detection/test_xai_detection_validity.py create mode 100644 tests/unit/algorithms/detection/tiling/__init__.py create mode 100644 tests/unit/algorithms/detection/tiling/test_tiling_detection.py create mode 100644 tests/unit/algorithms/detection/tiling/test_tiling_tile_classifier.py create mode 100644 tests/unit/algorithms/detection/utils/__init__.py create mode 100644 tests/unit/algorithms/detection/utils/test_detection_data.py create mode 100644 tests/unit/algorithms/detection/utils/test_detection_mask_to_bbox.py create mode 100644 tests/unit/algorithms/detection/utils/test_detection_utils.py create mode 100644 tests/unit/algorithms/segmentation/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/api/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/api/test_train.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/datasets/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_compose.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_loads.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_transforms.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/datasets/test_dataset.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_litehrnet.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_mmseg_mmov_backbone.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_detcon_head.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_mmseg_mmov_decode_head.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_prototype_head.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_cross_entropy_loss_with_ignore.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_detcon_loss.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_pixel_prototype_ce_loss.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/necks/test_selfsl_mlp.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/test_schedulers.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/test_detcon.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/test_mean_teacher_segmentor.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/models/utils/test_utils.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/nncf/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_builder.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_patches.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_task.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/utils/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_builder.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_data_utils.py create mode 100644 tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_exporter.py create mode 100644 tests/unit/algorithms/segmentation/adapters/openvino/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/openvino/model_wrappers/__init__.py create mode 100644 tests/unit/algorithms/segmentation/adapters/openvino/test_openvino_task.py create mode 100644 tests/unit/algorithms/segmentation/adapters/test_otx_segmentation_task.py create mode 100644 tests/unit/algorithms/segmentation/conftest.py create mode 100644 tests/unit/algorithms/segmentation/test_helpers.py create mode 100644 tests/unit/algorithms/segmentation/test_task.py create mode 100644 tests/unit/algorithms/visual_prompting/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/openvino/model_wrappers/test_openvino_models.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/test_inference_callback.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/config/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/config/test_visual_prompting_config.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/test_sam_transforms.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/test_transforms.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/test_dataset.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/test_tiny_vit.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/test_vit.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/test_sam_mask_decoder.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/test_sam_image_encoder.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/test_sam_prompt_encoder.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/test_segment_anything.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/test_zero_shot_segment_anything.py create mode 100644 tests/unit/algorithms/visual_prompting/tasks/__init__.py create mode 100644 tests/unit/algorithms/visual_prompting/tasks/test_inference.py create mode 100644 tests/unit/algorithms/visual_prompting/tasks/test_openvino.py create mode 100644 tests/unit/algorithms/visual_prompting/tasks/test_train.py create mode 100644 tests/unit/algorithms/visual_prompting/test_helpers.py create mode 100644 tests/unit/api/__init__.py create mode 100644 tests/unit/api/configuration/__init__.py create mode 100644 tests/unit/api/configuration/dummy_broken_config.yaml create mode 100644 tests/unit/api/configuration/dummy_config.py create mode 100644 tests/unit/api/configuration/dummy_config.yaml create mode 100644 tests/unit/api/configuration/elements/test_elements_utils.py create mode 100644 tests/unit/api/configuration/elements/test_metadata_keys.py create mode 100644 tests/unit/api/configuration/elements/test_primitive_parameters.py create mode 100644 tests/unit/api/configuration/enums/test_config_element_type.py create mode 100644 tests/unit/api/configuration/enums/test_enum_utils.py create mode 100644 tests/unit/api/configuration/enums/test_model_lifecycle.py create mode 100644 tests/unit/api/configuration/helper/test_config_element_mapping.py create mode 100644 tests/unit/api/configuration/helper/test_create.py create mode 100644 tests/unit/api/configuration/helper/test_helper_utils.py create mode 100644 tests/unit/api/configuration/test_configurable_parameters.py create mode 100644 tests/unit/api/configuration/test_configuration_helper.py create mode 100644 tests/unit/api/configuration/test_model_configuration.py create mode 100644 tests/unit/api/constants/__init__.py create mode 100644 tests/unit/api/constants/components.py create mode 100644 tests/unit/api/constants/requirements.py create mode 100644 tests/unit/api/entities/dummy_config.yaml create mode 100644 tests/unit/api/entities/dummy_template.yaml create mode 100644 tests/unit/api/entities/interfaces/test_graph_interface.py create mode 100644 tests/unit/api/entities/shapes/test_ellipse.py create mode 100644 tests/unit/api/entities/shapes/test_polygon.py create mode 100644 tests/unit/api/entities/shapes/test_rectangle.py create mode 100644 tests/unit/api/entities/shapes/test_shape.py create mode 100644 tests/unit/api/entities/test_annotation.py create mode 100644 tests/unit/api/entities/test_color.py create mode 100644 tests/unit/api/entities/test_coordinate.py create mode 100644 tests/unit/api/entities/test_dataset_item.py create mode 100644 tests/unit/api/entities/test_datasets.py create mode 100644 tests/unit/api/entities/test_graph.py create mode 100644 tests/unit/api/entities/test_id.py create mode 100644 tests/unit/api/entities/test_image.py create mode 100644 tests/unit/api/entities/test_inference_parameters.py create mode 100644 tests/unit/api/entities/test_label.py create mode 100644 tests/unit/api/entities/test_label_schema.py create mode 100644 tests/unit/api/entities/test_media.py create mode 100644 tests/unit/api/entities/test_metadata.py create mode 100644 tests/unit/api/entities/test_metrics.py create mode 100644 tests/unit/api/entities/test_model.py create mode 100644 tests/unit/api/entities/test_model_template.py create mode 100644 tests/unit/api/entities/test_optimization_parameters.py create mode 100644 tests/unit/api/entities/test_pickle.py create mode 100644 tests/unit/api/entities/test_result_media.py create mode 100644 tests/unit/api/entities/test_resultset.py create mode 100644 tests/unit/api/entities/test_scored_label.py create mode 100644 tests/unit/api/entities/test_subset.py create mode 100644 tests/unit/api/entities/test_task_environment.py create mode 100644 tests/unit/api/entities/test_tensor.py create mode 100644 tests/unit/api/entities/test_train_parameters.py create mode 100644 tests/unit/api/fixtures/__init__.py create mode 100644 tests/unit/api/fixtures/general.py create mode 100644 tests/unit/api/parameters_validation/validation_helper.py create mode 100644 tests/unit/api/serialization/test_datetime_mapper.py create mode 100644 tests/unit/api/serialization/test_id_mapper.py create mode 100644 tests/unit/api/serialization/test_label_mapper.py create mode 100644 tests/unit/api/usecases/adapters/test_model_adapter.py create mode 100644 tests/unit/api/usecases/evaluation/test_accuracy.py create mode 100644 tests/unit/api/usecases/evaluation/test_basic_operations.py create mode 100644 tests/unit/api/usecases/evaluation/test_dice.py create mode 100644 tests/unit/api/usecases/evaluation/test_f_measure.py create mode 100644 tests/unit/api/usecases/exportable_code/test_prediction_to_annotation_converter.py create mode 100644 tests/unit/api/usecases/exportable_code/test_streamer.py create mode 100644 tests/unit/api/usecases/exportable_code/test_visualization.py create mode 100644 tests/unit/api/usecases/reporting/test_callback.py create mode 100644 tests/unit/api/usecases/reporting/test_time_monitor_callback.py create mode 100644 tests/unit/api/usecases/tasks/interfaces/test_interfaces.py create mode 100644 tests/unit/api/utils/test_segmentation_utils.py create mode 100644 tests/unit/api/utils/test_shape_drawer.py create mode 100644 tests/unit/api/utils/test_shape_factory.py delete mode 100644 tests/unit/cli/__init__.py create mode 100644 tests/unit/cli/builder/test_cli_builder.py create mode 100644 tests/unit/cli/conftest.py create mode 100644 tests/unit/cli/manager/test_config_manager.py create mode 100644 tests/unit/cli/registry/test_cli_registry.py delete mode 100644 tests/unit/cli/test_cli.py delete mode 100644 tests/unit/cli/test_install.py create mode 100644 tests/unit/cli/tools/test_build.py create mode 100644 tests/unit/cli/tools/test_cli.py create mode 100644 tests/unit/cli/tools/test_deploy.py create mode 100644 tests/unit/cli/tools/test_eval.py create mode 100644 tests/unit/cli/tools/test_export.py create mode 100644 tests/unit/cli/tools/test_find.py create mode 100644 tests/unit/cli/tools/test_optimize.py create mode 100644 tests/unit/cli/tools/test_train.py delete mode 100644 tests/unit/cli/utils/__init__.py create mode 100644 tests/unit/cli/utils/test_config.py create mode 100644 tests/unit/cli/utils/test_experiment.py delete mode 100644 tests/unit/cli/utils/test_help_formatter.py create mode 100644 tests/unit/cli/utils/test_hpo.py create mode 100644 tests/unit/cli/utils/test_importing.py delete mode 100644 tests/unit/cli/utils/test_installation.py create mode 100644 tests/unit/cli/utils/test_io.py delete mode 100644 tests/unit/cli/utils/test_jsonargparse.py create mode 100644 tests/unit/cli/utils/test_multi_gpu.py create mode 100644 tests/unit/cli/utils/test_nncf.py create mode 100644 tests/unit/cli/utils/test_parser.py create mode 100644 tests/unit/cli/utils/test_report.py create mode 100644 tests/unit/cli/utils/test_telemetry.py create mode 100644 tests/unit/conftest.py delete mode 100644 tests/unit/core/config/test_resolver.py delete mode 100644 tests/unit/core/conftest.py create mode 100644 tests/unit/core/data/adapter/test_action_adapter.py create mode 100644 tests/unit/core/data/adapter/test_anomaly_adapter.py create mode 100644 tests/unit/core/data/adapter/test_classification_adapter.py create mode 100644 tests/unit/core/data/adapter/test_detection_adapter.py create mode 100644 tests/unit/core/data/adapter/test_init.py create mode 100644 tests/unit/core/data/adapter/test_segmentation_adapter.py create mode 100644 tests/unit/core/data/adapter/test_visual_prompting_adapter.py delete mode 100644 tests/unit/core/data/conftest.py delete mode 100644 tests/unit/core/data/dataset/__init__.py delete mode 100644 tests/unit/core/data/dataset/test_classification.py delete mode 100644 tests/unit/core/data/dataset/test_visual_prompting.py delete mode 100644 tests/unit/core/data/entity/__init__.py delete mode 100644 tests/unit/core/data/entity/conftest.py delete mode 100644 tests/unit/core/data/entity/test_base.py delete mode 100644 tests/unit/core/data/entity/test_detection.py delete mode 100644 tests/unit/core/data/entity/test_visual_prompting.py create mode 100644 tests/unit/core/data/manager/test_dataset_manager.py create mode 100644 tests/unit/core/data/test_caching.py delete mode 100644 tests/unit/core/data/test_dataset.py delete mode 100644 tests/unit/core/data/test_factory.py create mode 100644 tests/unit/core/data/test_helpers.py delete mode 100644 tests/unit/core/data/test_mem_cache.py delete mode 100644 tests/unit/core/data/test_module.py delete mode 100644 tests/unit/core/data/test_pre_filtering.py create mode 100644 tests/unit/core/data/test_storage_caching.py delete mode 100644 tests/unit/core/data/test_transform_libs.py delete mode 100644 tests/unit/core/data/transform_libs/__init__.py delete mode 100644 tests/unit/core/data/transform_libs/test_mmcv.py delete mode 100644 tests/unit/core/data/transform_libs/test_mmdet.py delete mode 100644 tests/unit/core/data/transform_libs/test_mmseg.py delete mode 100644 tests/unit/core/metrics/__init__.py delete mode 100644 tests/unit/core/metrics/test_accuracy.py delete mode 100644 tests/unit/core/metrics/test_fmeasure.py delete mode 100644 tests/unit/core/model/__init__.py delete mode 100644 tests/unit/core/model/entity/__init__.py delete mode 100644 tests/unit/core/model/entity/test_base.py delete mode 100644 tests/unit/core/model/entity/test_segmentation.py delete mode 100644 tests/unit/core/model/entity/test_visual_prompting.py delete mode 100644 tests/unit/core/model/module/__init__.py delete mode 100644 tests/unit/core/model/module/test_base.py delete mode 100644 tests/unit/core/model/module/test_detection.py delete mode 100644 tests/unit/core/model/module/test_segmentation.py create mode 100644 tests/unit/core/ov/__init__.py create mode 100644 tests/unit/core/ov/graph/parsers/test_ov_graph_cls_parser.py create mode 100644 tests/unit/core/ov/graph/parsers/test_ov_graph_parser.py create mode 100644 tests/unit/core/ov/graph/test_ov_graph_grapy.py create mode 100644 tests/unit/core/ov/graph/test_ov_graph_utils.py create mode 100644 tests/unit/core/ov/models/__init__.py create mode 100644 tests/unit/core/ov/models/mmcls/__init__.py create mode 100644 tests/unit/core/ov/models/mmcls/backbones/test_ov_mmcls_mmov_backbone.py create mode 100644 tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_cls_head.py create mode 100644 tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_conv_head.py create mode 100644 tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_mmcv_cls_head.py create mode 100644 tests/unit/core/ov/models/mmcls/necks/test_ov_mmcls_mmov_neck.py create mode 100644 tests/unit/core/ov/models/mmcls/test_helpers.py create mode 100644 tests/unit/core/ov/models/test_ov_models_ov_model.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_activations.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_arithmetics.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_builder.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_convolutions.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_generation.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_image_processings.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_infrastructures.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_matmuls.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_module.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_movements.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_normalizations.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_object_detections.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_op.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_poolings.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_reductions.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_shape_manipulations.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_sorting_maximization.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_type_conversions.py create mode 100644 tests/unit/core/ov/ops/test_ov_ops_utils.py create mode 100644 tests/unit/core/ov/test_ov_omz_wrapper.py create mode 100644 tests/unit/core/ov/test_ov_registry.py create mode 100644 tests/unit/core/ov/test_ov_utils.py create mode 100644 tests/unit/core/test_core_patcher.py delete mode 100644 tests/unit/core/utils/__init__.py delete mode 100644 tests/unit/core/utils/test_mask_utils.py delete mode 100644 tests/unit/core/utils/test_tile.py delete mode 100644 tests/unit/core/utils/test_utils.py delete mode 100644 tests/unit/engine/__init__.py delete mode 100644 tests/unit/engine/utils/__init__.py delete mode 100644 tests/unit/engine/utils/test_api.py delete mode 100644 tests/unit/engine/utils/test_auto_configurator.py delete mode 100644 tests/unit/hpo/__init__.py create mode 100644 tests/unit/mpa/__init__.py create mode 100644 tests/unit/mpa/deploy/__init__.py create mode 100644 tests/unit/mpa/test_augments.py delete mode 100644 tests/unit/utils/test_signal.py delete mode 100644 tests/unit/utils/test_utils.py create mode 100644 tools/README.md create mode 100644 tools/experiment.py diff --git a/.ci/piptools-deps.in b/.ci/piptools-deps.in deleted file mode 100644 index 776f187e890..00000000000 --- a/.ci/piptools-deps.in +++ /dev/null @@ -1 +0,0 @@ -pip-tools==7.4.1 diff --git a/.ci/piptools-deps.txt b/.ci/piptools-deps.txt deleted file mode 100644 index 879f7e91d78..00000000000 --- a/.ci/piptools-deps.txt +++ /dev/null @@ -1,45 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --generate-hashes --output-file=.ci/piptools-deps.txt .ci/piptools-deps.in -# -build==1.1.1 \ - --hash=sha256:8ed0851ee76e6e38adce47e4bee3b51c771d86c64cf578d0c2245567ee200e73 \ - --hash=sha256:8eea65bb45b1aac2e734ba2cc8dad3a6d97d97901a395bd0ed3e7b46953d2a31 - # via pip-tools -click==8.1.7 \ - --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ - --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de - # via pip-tools -packaging==23.2 \ - --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ - --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 - # via build -pip-tools==7.4.1 \ - --hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \ - --hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9 - # via -r .ci/piptools-deps.in -pyproject-hooks==1.0.0 \ - --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \ - --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5 - # via - # build - # pip-tools -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f - # via - # build - # pip-tools - # pyproject-hooks -wheel==0.42.0 \ - --hash=sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d \ - --hash=sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8 - # via pip-tools - -# WARNING: The following packages were not pinned, but pip requires them to be -# pinned when the requirements file includes hashes and the requirement is not -# satisfied by a package already installed. Consider using the --allow-unsafe flag. -# pip -# setuptools diff --git a/.ci/publish-deps.in b/.ci/publish-deps.in deleted file mode 100644 index 910e891a89c..00000000000 --- a/.ci/publish-deps.in +++ /dev/null @@ -1,2 +0,0 @@ -build==1.1.1 -twine==5.0.0 diff --git a/.ci/publish-deps.txt b/.ci/publish-deps.txt deleted file mode 100644 index 17f81e2013b..00000000000 --- a/.ci/publish-deps.txt +++ /dev/null @@ -1,320 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --generate-hashes --output-file=.ci/publish-deps.txt .ci/publish-deps.in -# -build==1.1.1 \ - --hash=sha256:8ed0851ee76e6e38adce47e4bee3b51c771d86c64cf578d0c2245567ee200e73 \ - --hash=sha256:8eea65bb45b1aac2e734ba2cc8dad3a6d97d97901a395bd0ed3e7b46953d2a31 - # via -r .ci/publish-deps.in -certifi==2024.2.2 \ - --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ - --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 - # via requests -cffi==1.16.0 \ - --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ - --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ - --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ - --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ - --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ - --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ - --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ - --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ - --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ - --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ - --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ - --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ - --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ - --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ - --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ - --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ - --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ - --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ - --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ - --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ - --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ - --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ - --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ - --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ - --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ - --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ - --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ - --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ - --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ - --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ - --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ - --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ - --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ - --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ - --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ - --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ - --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ - --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ - --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ - --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ - --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ - --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ - --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ - --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ - --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ - --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ - --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ - --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ - --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ - --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ - --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ - --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 - # via cryptography -charset-normalizer==3.3.2 \ - --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ - --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ - --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ - --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ - --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ - --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ - --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ - --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ - --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ - --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ - --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ - --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ - --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ - --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ - --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ - --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ - --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ - --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ - --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ - --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ - --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ - --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ - --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ - --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ - --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ - --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ - --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ - --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ - --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ - --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ - --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ - --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ - --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ - --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ - --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ - --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ - --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ - --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ - --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ - --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ - --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ - --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ - --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ - --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ - --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ - --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ - --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ - --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ - --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ - --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ - --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ - --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ - --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ - --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ - --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ - --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ - --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ - --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ - --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ - --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ - --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ - --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ - --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ - --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ - --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ - --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ - --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ - --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ - --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ - --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ - --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ - --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ - --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ - --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ - --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ - --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ - --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ - --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ - --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ - --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ - --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ - --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ - --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ - --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ - --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ - --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ - --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ - --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ - --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ - --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 - # via requests -cryptography==42.0.5 \ - --hash=sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee \ - --hash=sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576 \ - --hash=sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d \ - --hash=sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30 \ - --hash=sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413 \ - --hash=sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb \ - --hash=sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da \ - --hash=sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4 \ - --hash=sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd \ - --hash=sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc \ - --hash=sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8 \ - --hash=sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1 \ - --hash=sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc \ - --hash=sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e \ - --hash=sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8 \ - --hash=sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940 \ - --hash=sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400 \ - --hash=sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7 \ - --hash=sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16 \ - --hash=sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278 \ - --hash=sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74 \ - --hash=sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec \ - --hash=sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1 \ - --hash=sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2 \ - --hash=sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c \ - --hash=sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922 \ - --hash=sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a \ - --hash=sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6 \ - --hash=sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1 \ - --hash=sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e \ - --hash=sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac \ - --hash=sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7 - # via secretstorage -docutils==0.20.1 \ - --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \ - --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b - # via readme-renderer -idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f - # via requests -importlib-metadata==7.0.2 \ - --hash=sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792 \ - --hash=sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100 - # via - # keyring - # twine -jaraco-classes==3.3.1 \ - --hash=sha256:86b534de565381f6b3c1c830d13f931d7be1a75f0081c57dff615578676e2206 \ - --hash=sha256:cb28a5ebda8bc47d8c8015307d93163464f9f2b91ab4006e09ff0ce07e8bfb30 - # via keyring -jeepney==0.8.0 \ - --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \ - --hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755 - # via - # keyring - # secretstorage -keyring==24.3.1 \ - --hash=sha256:c3327b6ffafc0e8befbdb597cacdb4928ffe5c1212f7645f186e6d9957a898db \ - --hash=sha256:df38a4d7419a6a60fea5cef1e45a948a3e8430dd12ad88b0f423c5c143906218 - # via twine -markdown-it-py==3.0.0 \ - --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ - --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb - # via rich -mdurl==0.1.2 \ - --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ - --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba - # via markdown-it-py -more-itertools==10.2.0 \ - --hash=sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684 \ - --hash=sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1 - # via jaraco-classes -nh3==0.2.15 \ - --hash=sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770 \ - --hash=sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf \ - --hash=sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305 \ - --hash=sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601 \ - --hash=sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28 \ - --hash=sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7 \ - --hash=sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3 \ - --hash=sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911 \ - --hash=sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf \ - --hash=sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0 \ - --hash=sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5 \ - --hash=sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97 \ - --hash=sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d \ - --hash=sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e \ - --hash=sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3 \ - --hash=sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6 - # via readme-renderer -packaging==24.0 \ - --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ - --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 - # via build -pkginfo==1.10.0 \ - --hash=sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297 \ - --hash=sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097 - # via twine -pycparser==2.21 \ - --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ - --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 - # via cffi -pygments==2.17.2 \ - --hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \ - --hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367 - # via - # readme-renderer - # rich -pyproject-hooks==1.0.0 \ - --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \ - --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5 - # via build -readme-renderer==43.0 \ - --hash=sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311 \ - --hash=sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9 - # via twine -requests==2.31.0 \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 - # via - # requests-toolbelt - # twine -requests-toolbelt==1.0.0 \ - --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ - --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 - # via twine -rfc3986==2.0.0 \ - --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ - --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c - # via twine -rich==13.7.1 \ - --hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \ - --hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432 - # via twine -secretstorage==3.3.3 \ - --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \ - --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99 - # via keyring -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f - # via - # build - # pyproject-hooks -twine==5.0.0 \ - --hash=sha256:89b0cc7d370a4b66421cc6102f269aa910fe0f1861c124f573cf2ddedbc10cf4 \ - --hash=sha256:a262933de0b484c53408f9edae2e7821c1c45a3314ff2df9bdd343aa7ab8edc0 - # via -r .ci/publish-deps.in -urllib3==2.2.1 \ - --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \ - --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19 - # via - # requests - # twine -zipp==3.18.0 \ - --hash=sha256:c1bb803ed69d2cce2373152797064f7e79bc43f0a3748eb494096a867e0ebf79 \ - --hash=sha256:df8d042b02765029a09b157efd8e820451045890acc30f8e37dd2f94a060221f - # via importlib-metadata diff --git a/.ci/tox-deps.in b/.ci/tox-deps.in deleted file mode 100644 index ef0df6b3ae2..00000000000 --- a/.ci/tox-deps.in +++ /dev/null @@ -1 +0,0 @@ -tox==4.14.1 diff --git a/.ci/tox-deps.txt b/.ci/tox-deps.txt deleted file mode 100644 index 101b2e4e17a..00000000000 --- a/.ci/tox-deps.txt +++ /dev/null @@ -1,62 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --generate-hashes --output-file=.ci/tox-deps.txt .ci/tox-deps.in -# -cachetools==5.3.3 \ - --hash=sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945 \ - --hash=sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105 - # via tox -chardet==5.2.0 \ - --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ - --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 - # via tox -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via tox -distlib==0.3.8 \ - --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ - --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 - # via virtualenv -filelock==3.13.1 \ - --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \ - --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c - # via - # tox - # virtualenv -packaging==23.2 \ - --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ - --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 - # via - # pyproject-api - # tox -platformdirs==4.2.0 \ - --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ - --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 - # via - # tox - # virtualenv -pluggy==1.4.0 \ - --hash=sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 \ - --hash=sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be - # via tox -pyproject-api==1.6.1 \ - --hash=sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538 \ - --hash=sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675 - # via tox -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f - # via - # pyproject-api - # tox -tox==4.14.1 \ - --hash=sha256:b03754b6ee6dadc70f2611da82b4ed8f625fcafd247e15d1d0cb056f90a06d3b \ - --hash=sha256:f0ad758c3bbf7e237059c929d3595479363c3cdd5a06ac3e49d1dd020ffbee45 - # via -r .ci/tox-deps.in -virtualenv==20.25.1 \ - --hash=sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a \ - --hash=sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197 - # via tox diff --git a/.dockerignore b/.dockerignore index fd6f22b787e..82a92f43dae 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,15 @@ -* -!src/otx -!pyproject.toml -!README.md -!LICENSE -!MANIFEST.in -!docker/download_pretrained_weights.py +**/venv/** +.git +.github +.mypy_cache +.pytest_cache +.tox +.vscode +__pycache__ +build/ +data/ +docs/ +lightning_logs/ +misc/ +results/ +venv/ diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000000..ba57eead422 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +extend-ignore = E203 +exclude = .venv +max-complexity = 6 +max-line-length = 120 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 6b6c4b3225f..00000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,4 +0,0 @@ -# See help here: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners - -# These owners will be the default owners for everything in the repo. -* @samet-akcay @harimkang @vinnamkim @jaegukhyun @sungmanc @eugene123tw @kprokofi @chuneuny-emily @sovrasov diff --git a/.github/codecov.yaml b/.github/codecov.yml similarity index 100% rename from .github/codecov.yaml rename to .github/codecov.yml diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml deleted file mode 100644 index 25e6ae662fc..00000000000 --- a/.github/dependabot.yaml +++ /dev/null @@ -1,26 +0,0 @@ -version: 2 -updates: - - package-ecosystem: docker - directory: /.ci - schedule: - interval: weekly - - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly - - - package-ecosystem: docker - directory: /docker - schedule: - interval: weekly - - - package-ecosystem: pip - directory: /src/otx/core/exporter/exportable_code/demo - schedule: - interval: weekly - - - package-ecosystem: pip - directory: / - schedule: - interval: weekly diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..48d500edbf3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +version: 2 +updates: + - package-ecosystem: docker + directory: /.ci + schedule: + interval: weekly + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + + - package-ecosystem: docker + directory: /docker + schedule: + interval: weekly + + - package-ecosystem: pip + directory: /src/otx/api/usecases/exportable_code/demo + schedule: + interval: weekly + + - package-ecosystem: pip + directory: /requirements + schedule: + interval: weekly + + - package-ecosystem: pip + directory: /docker + schedule: + interval: weekly diff --git a/.github/labeler.yml b/.github/labeler.yml index 571910c0b31..1069c6b2486 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,7 +1,11 @@ # See help here: https://github.com/marketplace/actions/labeler -OTX 2.0: - - "src/otx/**/*" +CLI: + - "src/otx/cli/**/*" +API: + - "src/otx/api/**/*" +ALGO: + - "src/otx/algorithms/**/*" TEST: - "tests/**/*" DOC: @@ -9,16 +13,19 @@ DOC: - "**/*.md" - "LICENSE" - "third-party-programs.txt" - - "NOTICE" - - "for_developers/**/*" BUILD: - ".ci/**/*" - ".github/**/*" + - ".flake8" + - ".isort.cfg" + - ".mdlrc" - ".pre-commit-config.yaml" - "markdownlint.rb" + - "setup.py" - "pyproject.toml" - "tox.ini" - "MANIFEST.in" DEPENDENCY: - "requirements/*" + - "setup.py" - "pyproject.toml" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9ede2ac0116..23024ed7e7f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -34,6 +34,6 @@ not fully covered by unit tests or manual testing can be complicated. --> - [ ] I have updated the license header for each file (see an example below). ```python -# Copyright (C) 2024 Intel Corporation +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 ``` diff --git a/.github/workflows/code_scan.yaml b/.github/workflows/code_scan.yaml deleted file mode 100644 index cc3c5117123..00000000000 --- a/.github/workflows/code_scan.yaml +++ /dev/null @@ -1,58 +0,0 @@ -name: Code Scanning - -on: - workflow_dispatch: # run on request (no need for PR) - push: - branches: - - "releases/*" - schedule: - # every UTC 6PM from Mon to Fri - - cron: "0 18 * * 1-5" - -# Declare default permissions as read only. -permissions: read-all - -jobs: - Trivy-scan: - runs-on: ubuntu-20.04 - steps: - - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - with: - python-version: "3.10" - - name: Install dependencies - run: python -m pip install --require-hashes --no-deps -r .ci/tox-deps.txt - - name: Trivy Scanning - env: - TRIVY_DOWNLOAD_URL: ${{ vars.TRIVY_DOWNLOAD_URL }} - run: tox -vv -e trivy-scan - - name: Upload Trivy results artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 - with: - name: trivy-results - path: | - .tox/trivy-spdx-otx.json - .tox/trivy-results-otx.txt - .tox/trivy-results-otx.csv - Bandit: - runs-on: ubuntu-20.04 - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - with: - python-version: "3.10" - - name: Install dependencies - run: python -m pip install --require-hashes --no-deps -r .ci/tox-deps.txt - - name: Bandit Scanning - run: tox -e bandit-scan - - name: Upload Bandit artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 - with: - name: bandit-report - path: .tox/bandit-report.txt - # Use always() to always run this step to publish scan results when there are test failures - if: ${{ always() }} diff --git a/.github/workflows/code_scan.yml b/.github/workflows/code_scan.yml new file mode 100644 index 00000000000..b1877a5dd1a --- /dev/null +++ b/.github/workflows/code_scan.yml @@ -0,0 +1,66 @@ +name: Code Scanning + +on: + workflow_dispatch: # run on request (no need for PR) + push: + branches: + - "releases/*" + - "v2" + schedule: + # every UTC 6PM from Mon to Fri + - cron: "0 18 * * 1-5" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + Trivy-scan: + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set up Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.10" + - name: Install dependencies + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-dev-requirements.txt requirements/dev.txt + pip install --require-hashes --no-deps -r /tmp/otx-dev-requirements.txt + - name: Trivy Scanning + env: + TRIVY_DOWNLOAD_URL: ${{ vars.TRIVY_DOWNLOAD_URL }} + run: tox -vv -e trivy-scan + - name: Upload Trivy results artifact + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: trivy-results + path: | + .tox/trivy-spdx-otx.json + .tox/trivy-results-otx.txt + .tox/trivy-results-otx.csv + Bandit: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set up Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.10" + - name: Install dependencies + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-dev-requirements.txt requirements/dev.txt + pip install --require-hashes --no-deps -r /tmp/otx-dev-requirements.txt + rm /tmp/otx-dev-requirements.txt + - name: Bandit Scanning + run: tox -e bandit-scan + - name: Upload Bandit artifact + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: bandit-report + path: .tox/bandit-report.txt + # Use always() to always run this step to publish scan results when there are test failures + if: ${{ always() }} diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml deleted file mode 100644 index 3386eee9be8..00000000000 --- a/.github/workflows/codeql.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: - - develop - - releases - pull_request: - types: - - opened - - reopened - - synchronize - schedule: - - cron: "0 0 * * 0" - -permissions: - contents: read - -jobs: - analyze: - name: Analyze - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners - # Consider using larger runners for possible analysis time improvements. - runs-on: ubuntu-latest - timeout-minutes: 60 - permissions: - # required for all workflows - security-events: write - - # only required for workflows in private repositories - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - language: ["python"] - # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] - # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..9a2359d7e09 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,77 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: + - develop + - releases + - v2 + pull_request: + types: + - opened + - reopened + - synchronize + schedule: + - cron: "0 0 * * 0" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + # required for all workflows + security-events: write + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: ["python"] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml new file mode 100644 index 00000000000..2acb496760a --- /dev/null +++ b/.github/workflows/daily.yml @@ -0,0 +1,41 @@ +name: Daily Test + +on: + workflow_dispatch: # run on request (no need for PR) + schedule: + # every UTC 7PM from Mon to Fri + - cron: "0 19 * * 1-5" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + E2E-tests: + strategy: + fail-fast: false + matrix: + include: + - task: "act" + test_dir: "tests/e2e/cli/action" + - task: "ano" + test_dir: "tests/e2e/cli/anomaly" + - task: "cls" + test_dir: "tests/e2e/cli/classification" + - task: "det" + test_dir: "tests/e2e/cli/detection" + - task: "iseg" + test_dir: "tests/e2e/cli/instance_segmentation" + - task: "seg" + test_dir: "tests/e2e/cli/semantic_segmentation" + - task: "visprompt" + test_dir: "tests/e2e/cli/visual_prompting" + name: E2E-Test-py310-${{ matrix.task }} + uses: ./.github/workflows/run_tests_in_tox.yml + with: + python-version: "3.10" + toxenv-pyver: "py310" + toxenv-task: ${{ matrix.task }} + tests-dir: ${{ matrix.test_dir }} + timeout-minutes: 360 + upload-artifact: true + artifact-prefix: "daily-test-results" diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 80e38e9faea..b206511c30c 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -21,17 +21,17 @@ jobs: uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: python-version: "3.10" - - name: Install tox - run: python -m pip install --require-hashes --no-deps -r .ci/tox-deps.txt + - name: Install dependencies + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-dev-requirements.txt requirements/dev.txt + pip install --require-hashes --no-deps -r /tmp/otx-dev-requirements.txt + rm /tmp/otx-dev-requirements.txt - name: Build-Docs run: tox -e build-doc - name: Create gh-pages branch run: | - if [[ ${{github.event_name}} == 'workflow_dispatch' ]]; then - echo RELEASE_VERSION="test_build" >> $GITHUB_ENV - else - echo RELEASE_VERSION=${GITHUB_REF#refs/*/} >> $GITHUB_ENV - fi + echo RELEASE_VERSION=${GITHUB_REF#refs/*/} >> $GITHUB_ENV echo SOURCE_NAME=${GITHUB_REF#refs/*/} >> $GITHUB_OUTPUT echo SOURCE_BRANCH=${GITHUB_REF#refs/heads/} >> $GITHUB_OUTPUT echo SOURCE_TAG=${GITHUB_REF#refs/tags/} >> $GITHUB_OUTPUT @@ -63,12 +63,10 @@ jobs: echo '' > index.html mkdir -p ${{ env.RELEASE_VERSION }} cp -r /tmp/docs_build/* ./${{ env.RELEASE_VERSION }} + ln -sfn ${{ env.RELEASE_VERSION }} latest rm -rf /tmp/docs_build git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - if [[ ${{ env.RELEASE_VERSION }} != 'test_build' ]]; then - ln -sfn ${{ env.RELEASE_VERSION }} latest - fi git add ./latest ${{ env.RELEASE_VERSION }} git commit -m "Update documentation" -a || true - name: Push changes diff --git a/.github/workflows/docs_stable.yaml b/.github/workflows/docs_stable.yaml deleted file mode 100644 index be60899b206..00000000000 --- a/.github/workflows/docs_stable.yaml +++ /dev/null @@ -1,71 +0,0 @@ -name: Build Docs for releases - -on: - release: - types: [published] - -# Declare default permissions as read only. -permissions: read-all - -jobs: - Build-Docs: - runs-on: ubuntu-20.04 - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - fetch-depth: 0 # otherwise, you will failed to push refs to dest repo - - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - with: - python-version: "3.10" - - name: Install tox - run: python -m pip install --require-hashes --no-deps -r .ci/tox-deps.txt - - name: Build-Docs - run: tox -e build-doc - - name: Create gh-pages branch - run: | - echo RELEASE_VERSION=${GITHUB_REF#refs/*/} >> $GITHUB_ENV - echo SOURCE_NAME=${GITHUB_REF#refs/*/} >> $GITHUB_OUTPUT - echo SOURCE_BRANCH=${GITHUB_REF#refs/heads/} >> $GITHUB_OUTPUT - echo SOURCE_TAG=${GITHUB_REF#refs/tags/} >> $GITHUB_OUTPUT - existed_in_remote=$(git ls-remote --heads origin gh-pages) - - if [[ -z ${existed_in_remote} ]]; then - echo "Creating gh-pages branch" - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git checkout --orphan gh-pages - git reset --hard - touch .nojekyll - git add .nojekyll - git commit -m "Initializing gh-pages branch" - git push origin gh-pages - git checkout ${{steps.branch_name.outputs.SOURCE_NAME}} - echo "Created gh-pages branch" - else - echo "Branch gh-pages already exists" - fi - - name: Commit docs to gh-pages branch - run: | - git fetch - git checkout gh-pages - mkdir -p /tmp/docs_build - cp -r docs/build/html/* /tmp/docs_build/ - rm -rf ${{ env.RELEASE_VERSION }}/* - echo '' > index.html - mkdir -p ${{ env.RELEASE_VERSION }} - cp -r /tmp/docs_build/* ./${{ env.RELEASE_VERSION }} - ln -sfn ${{ env.RELEASE_VERSION }} stable - rm -rf /tmp/docs_build - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add ./stable ${{ env.RELEASE_VERSION }} - git commit -m "Update documentation" -a || true - - name: Push changes - uses: ad-m/github-push-action@fcea09907c44d7a7a3331c9c04080d55d87c95fe # master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: gh-pages diff --git a/.github/workflows/docs_stable.yml b/.github/workflows/docs_stable.yml new file mode 100644 index 00000000000..cbddeda016c --- /dev/null +++ b/.github/workflows/docs_stable.yml @@ -0,0 +1,75 @@ +name: Build Docs for releases + +on: + release: + types: [published] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + Build-Docs: + runs-on: ubuntu-20.04 + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 # otherwise, you will failed to push refs to dest repo + - name: Set up Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.10" + - name: Install dependencies + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-dev-requirements.txt requirements/dev.txt + pip install --require-hashes --no-deps -r /tmp/otx-dev-requirements.txt + rm /tmp/otx-dev-requirements.txt + - name: Build-Docs + run: tox -e build-doc + - name: Create gh-pages branch + run: | + echo RELEASE_VERSION=${GITHUB_REF#refs/*/} >> $GITHUB_ENV + echo SOURCE_NAME=${GITHUB_REF#refs/*/} >> $GITHUB_OUTPUT + echo SOURCE_BRANCH=${GITHUB_REF#refs/heads/} >> $GITHUB_OUTPUT + echo SOURCE_TAG=${GITHUB_REF#refs/tags/} >> $GITHUB_OUTPUT + existed_in_remote=$(git ls-remote --heads origin gh-pages) + + if [[ -z ${existed_in_remote} ]]; then + echo "Creating gh-pages branch" + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git checkout --orphan gh-pages + git reset --hard + touch .nojekyll + git add .nojekyll + git commit -m "Initializing gh-pages branch" + git push origin gh-pages + git checkout ${{steps.branch_name.outputs.SOURCE_NAME}} + echo "Created gh-pages branch" + else + echo "Branch gh-pages already exists" + fi + - name: Commit docs to gh-pages branch + run: | + git fetch + git checkout gh-pages + mkdir -p /tmp/docs_build + cp -r docs/build/html/* /tmp/docs_build/ + rm -rf ${{ env.RELEASE_VERSION }}/* + echo '' > index.html + mkdir -p ${{ env.RELEASE_VERSION }} + cp -r /tmp/docs_build/* ./${{ env.RELEASE_VERSION }} + ln -sfn ${{ env.RELEASE_VERSION }} stable + rm -rf /tmp/docs_build + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add ./stable ${{ env.RELEASE_VERSION }} + git commit -m "Update documentation" -a || true + - name: Push changes + uses: ad-m/github-push-action@fcea09907c44d7a7a3331c9c04080d55d87c95fe # master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: gh-pages diff --git a/.github/workflows/labeler.yaml b/.github/workflows/labeler.yaml deleted file mode 100644 index d3db416a20e..00000000000 --- a/.github/workflows/labeler.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: "Pull Request Labeler" -on: - - pull_request_target - -# Declare default permissions as read only. -permissions: read-all - -jobs: - labeler: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Uploads repository content to the runner - name: Checkout repository - with: - sparse-checkout: | - .github - - uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 00000000000..93e119c9336 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,17 @@ +name: "Pull Request Labeler" +on: + - pull_request_target + +# Declare default permissions as read only. +permissions: read-all + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/perf_benchmark.yaml b/.github/workflows/perf_benchmark.yaml index 543bf4c1ff3..1122e277aad 100644 --- a/.github/workflows/perf_benchmark.yaml +++ b/.github/workflows/perf_benchmark.yaml @@ -39,10 +39,48 @@ on: Additional perf-benchmark pytest arguments. "-k detection" -> detection task only "--dry-run" -> print command w/o execution. + dara-root: + type: string + description: Root directory containing validation data in CI server. + default: /home/validation/data/new/ + artifact-prefix: + type: string + default: perf-benchmark + workflow_call: + inputs: + model-category: + type: string + description: Model category to run benchmark [default, all] + default: default + data-size: + type: string + description: Dataset size to run benchmark [small, medium, large, all] + default: all + num-repeat: + type: number + description: Overrides default per-data-size number of repeat setting + default: 0 + num-epoch: + type: number + description: Overrides default per-model number of epoch setting + default: 0 + eval-upto: + type: string + description: The last operation to evaluate. 'optimize' means all. [train, export, optimize] + default: optimize + pytest-args: + type: string + description: | + Additional perf-benchmark pytest arguments. + "-k detection" -> detection task only + "--dry-run" -> print command w/o execution. data-root: type: string description: Root directory containing validation data in CI server. - default: "/home/validation/data/v2/" + default: /home/validation/data/new/ + artifact-prefix: + type: string + default: perf-benchmark # Declare default permissions as read only. permissions: read-all @@ -66,18 +104,27 @@ jobs: - task-short: "vsp" task: "visual_prompting" name: Perf-Benchmark-${{ matrix.task-short }} - runs-on: [self-hosted, linux, x64, dmount-v2, perf] + runs-on: [self-hosted, linux, x64, dmount] timeout-minutes: 8640 steps: - name: Checkout repository uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Install Python + - name: Set up Python uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: python-version: "3.10" - - name: Install tox - run: python -m pip install --require-hashes --no-deps -r .ci/tox-deps.txt - - name: Run Performance Test + - name: Install dependencies + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-dev-requirements.txt requirements/dev.txt + pip install --require-hashes --no-deps -r /tmp/otx-dev-requirements.txt + rm /tmp/otx-dev-requirements.txt + - name: Run Tests + env: + MLFLOW_TRACKING_SERVER_URI: ${{ vars.MLFLOW_TRACKING_SERVER_URI }} + BENCHMARK_RESULTS_CLEAR: ${{ vars.BENCHMARK_RESULTS_CLEAR }} + GH_CTX_REF_NAME: ${{ github.ref_name }} + GH_CTX_SHA: ${{ github.sha }} run: > tox -vv -e perf-benchmark -- tests/perf/test_${{ matrix.task }}.py ${{ inputs.pytest-args }} --model-category ${{ inputs.model-category }} @@ -92,7 +139,7 @@ jobs: - name: Upload test results uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: - name: perf-benchmark-${{ matrix.task-short }} + name: ${{ inputs.artifact-prefix }}-${{ matrix.task-short }} path: .tox/perf-*.csv # Use always() to always run this step to publish test results when there are test failures if: ${{ always() }} diff --git a/.github/workflows/pre_merge.yaml b/.github/workflows/pre_merge.yaml deleted file mode 100644 index b086a076a9a..00000000000 --- a/.github/workflows/pre_merge.yaml +++ /dev/null @@ -1,112 +0,0 @@ -name: PR Checks - -on: - push: - branches: - - develop - - releases/** - pull_request: - types: - - opened - - reopened - - synchronize - workflow_dispatch: # run on request (no need for PR) - -# Declare default permissions as read only. -permissions: read-all - -jobs: - Code-Quality-Checks: - # This is what will cancel the job concurrency - concurrency: - group: ${{ github.workflow }}-Linting-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - runs-on: ubuntu-20.04 - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - with: - python-version: "3.10" - - name: Install tox - run: python -m pip install --require-hashes --no-deps -r .ci/tox-deps.txt - - name: Code quality checks - run: tox -vv -e pre-commit - Unit-Test: - runs-on: ubuntu-20.04 - needs: Code-Quality-Checks - timeout-minutes: 120 - strategy: - fail-fast: false - matrix: - include: - - python-version: "3.10" - tox-env: "py310" - - python-version: "3.11" - tox-env: "py311" - name: Unit-Test-with-Python${{ matrix.python-version }} - # This is what will cancel the job concurrency - concurrency: - group: ${{ github.workflow }}-Unit-${{ github.event.pull_request.number || github.ref }}-${{ matrix.tox-env }} - cancel-in-progress: true - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Install Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - with: - python-version: ${{ matrix.python-version }} - - name: Install tox - run: python -m pip install --require-hashes --no-deps -r .ci/tox-deps.txt - - name: Run unit test - run: tox -vv -e unit-test-${{ matrix.tox-env }} - - name: Upload coverage reports to Codecov - run: | - # If the workflow is triggered from PR then it gets the commit id from the PR. - # else it uses the commit id of the latest commit. This is because the commit - # of the checked-out branch/commit does not exist in the tree as it is grafted. - # Also note: GitHub does not pass secrets to pipelines triggered from a fork. - # This means that upload will fail for PRs from forks. - if [ -n "${{ github.event.pull_request.head.sha }}" ] - then - COMMIT_ID=${{ github.event.pull_request.head.sha }} - else - COMMIT_ID=${{ github.sha }} - fi - # current version of codecov-action does not support uploading reports through the proxy - # so we use the latest version of codecov uploader binary - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ./codecov -t ${{ secrets.CODECOV_TOKEN }} --sha $COMMIT_ID -U $HTTP_PROXY -f .tox/coverage_unit-test-${{ matrix.tox-env }}.xml -F ${{ matrix.tox-env }} - - Integration-Test: - runs-on: [self-hosted, linux, x64, dev] - needs: Unit-Test - strategy: - fail-fast: false - matrix: - include: - - task: "action" - - task: "classification" - - task: "detection" - - task: "instance_segmentation" - - task: "semantic_segmentation" - - task: "visual_prompting" - - task: "anomaly" - name: Integration-Test-${{ matrix.task }}-py310 - # This is what will cancel the job concurrency - concurrency: - group: ${{ github.workflow }}-Integration-${{ github.event.pull_request.number || github.ref }}-${{ matrix.task }} - cancel-in-progress: true - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Install Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - with: - python-version: "3.10" - - name: Install tox - run: python -m pip install --require-hashes --no-deps -r .ci/tox-deps.txt - - name: Run Integration Test - run: tox -vv -e integration-test-${{ matrix.task }} diff --git a/.github/workflows/pre_merge.yml b/.github/workflows/pre_merge.yml new file mode 100644 index 00000000000..e9981fc5c30 --- /dev/null +++ b/.github/workflows/pre_merge.yml @@ -0,0 +1,143 @@ +name: PR Checks + +on: + push: + branches: + - develop + - releases/** + pull_request: + types: + - opened + - reopened + - synchronize + workflow_dispatch: # run on request (no need for PR) + +# Declare default permissions as read only. +permissions: read-all + +jobs: + Code-Quality-Checks: + # This is what will cancel the job concurrency + concurrency: + group: ${{ github.workflow }}-Linting-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + runs-on: ubuntu-20.04 + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set up Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.10" + - name: Install dependencies + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-dev-requirements.txt requirements/dev.txt + pip install --require-hashes --no-deps -r /tmp/otx-dev-requirements.txt + rm /tmp/otx-dev-requirements.txt + - name: Code quality checks + run: tox -vv -e pre-commit-all-py310-pt1 + Unit-Test: + needs: Code-Quality-Checks + strategy: + fail-fast: false + matrix: + include: + - python-version: "3.9" + tox-env: "py39" + - python-version: "3.10" + tox-env: "py310" + name: Unit-Test-with-Python${{ matrix.python-version }} + # This is what will cancel the job concurrency + concurrency: + group: ${{ github.workflow }}-Unit-${{ github.event.pull_request.number || github.ref }}-${{ matrix.tox-env }} + cancel-in-progress: true + uses: ./.github/workflows/run_tests_in_tox.yml + with: + python-version: ${{ matrix.python-version }} + toxenv-pyver: ${{ matrix.tox-env }} + toxenv-task: all + tests-dir: tests/unit + timeout-minutes: 120 + upload-artifact: true + artifact-prefix: "unit-test-results" + Coverage-Test: + needs: Code-Quality-Checks + concurrency: + group: ${{ github.workflow }}-Coverage-${{ github.event.pull_request.number || github.ref }}} + cancel-in-progress: true + runs-on: [self-hosted, linux, x64, dev] + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Install Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.8" + - name: Install dependencies + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-dev-requirements.txt requirements/dev.txt + pip install --require-hashes --no-deps -r /tmp/otx-dev-requirements.txt + rm /tmp/otx-dev-requirements.txt + - name: Run unit test + run: tox -vv -e unittest-all-py38-pt1 + - name: Upload coverage artifact + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: coverage + path: .tox/coverage.xml + - name: Upload coverage reports to Codecov + run: | + # If the workflow is triggered from PR then it gets the commit id from the PR. + # else it uses the commit id of the latest commit. This is because the commit + # of the checked-out branch/commit does not exist in the tree as it is grafted. + # Also note: GitHub does not pass secrets to pipelines triggered from a fork. + # This means that upload will fail for PRs from forks. + if [ -n "${{ github.event.pull_request.head.sha }}" ] + then + COMMIT_ID=${{ github.event.pull_request.head.sha }} + else + COMMIT_ID=${{ github.sha }} + fi + # current version of codecov-action does not support uploading reports through the proxy + # so we use the latest version of codecov uploader binary + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov -t ${{ secrets.CODECOV_TOKEN }} --sha $COMMIT_ID -U $HTTP_PROXY -f .tox/coverage.xml -F ${{ matrix.tox-env }} + Integration-Test: + needs: [Unit-Test, Coverage-Test] + strategy: + fail-fast: false + matrix: + include: + - task: "all" + test_dir: "tests/integration/cli/test_cli.py" + - task: "cls" + test_dir: "tests/integration/cli/classification" + - task: "det" + test_dir: "tests/integration/cli/detection" + - task: "iseg" + test_dir: "tests/integration/cli/instance_segmentation" + - task: "seg" + test_dir: "tests/integration/cli/semantic_segmentation" + - task: "act" + test_dir: "tests/integration/cli/action" + - task: "ano" + test_dir: "tests/integration/cli/anomaly" + - task: "visprompt" + test_dir: "tests/integration/cli/visual_prompting" + name: Integration-Test-py310-${{ matrix.task }} + # This is what will cancel the job concurrency + concurrency: + group: ${{ github.workflow }}-Integration-${{ github.event.pull_request.number || github.ref }}-${{ matrix.task }} + cancel-in-progress: true + uses: ./.github/workflows/run_tests_in_tox.yml + with: + python-version: "3.10" + toxenv-pyver: "py310" + toxenv-task: ${{ matrix.task }} + tests-dir: ${{ matrix.test_dir }} + timeout-minutes: 120 + upload-artifact: true + artifact-prefix: "intg-test-results" diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml deleted file mode 100644 index 39978050161..00000000000 --- a/.github/workflows/publish.yaml +++ /dev/null @@ -1,85 +0,0 @@ -name: Build and upload to PyPI - -on: - workflow_dispatch: # run on request (no need for PR) - release: - types: [published] - -# Declare default permissions as read only. -permissions: read-all - -jobs: - build_wheels: - name: Build wheels - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Build wheels - uses: pypa/cibuildwheel@0ecddd92b62987d7a2ae8911f4bb8ec9e2e4496a # v2.13.1 - - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 - with: - name: artifact-wheels - path: ./wheelhouse/*.whl - - build_sdist: - name: Build source distribution - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Set up Python 3.10 - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - with: - python-version: "3.10" - - name: Install pypa/build - run: python -m pip install --require-hashes --no-deps -r .ci/publish-deps.txt - - name: Build sdist - run: python -m build --sdist - - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 - with: - name: artifact-sdist - path: dist/*.tar.gz - - publish_package: - name: Publish package - needs: [build_wheels, build_sdist] - environment: pypi - runs-on: ubuntu-latest - permissions: - packages: write - steps: - - name: Download artifacts - uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3 - with: - path: dist - pattern: artifact-* - merge-multiple: true - # to determine where to publish the package distribution to PyPI or TestPyPI - - name: Check tag - id: check-tag - uses: actions-ecosystem/action-regex-match@9e6c4fb3d5e898f505be7a1fb6e7b0a278f6665b # v2.0.2 - with: - text: ${{ github.ref }} - regex: '^refs/tags/[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+rc[0-9]+|rc[0-9]+)?$' - - name: Upload package distributions to github - if: ${{ steps.check-tag.outputs.match != '' }} - uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: dist/* - tag: ${{ github.ref }} - overwrite: true - file_glob: true - - name: Publish package distributions to PyPI - if: ${{ steps.check-tag.outputs.match != '' }} - uses: pypa/gh-action-pypi-publish@e53eb8b103ffcb59469888563dc324e3c8ba6f06 # v1.8.12 - with: - password: ${{ secrets.PYPI_API_TOKEN }} - - name: Publish package distributions to TestPyPI - if: ${{ steps.check-tag.outputs.match == '' }} - uses: pypa/gh-action-pypi-publish@e53eb8b103ffcb59469888563dc324e3c8ba6f06 # v1.8.12 - with: - password: ${{ secrets.TESTPYPI_API_TOKEN }} - repository-url: https://test.pypi.org/legacy/ - verbose: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000000..0007547cd70 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,91 @@ +name: Build and upload to PyPI + +on: + workflow_dispatch: # run on request (no need for PR) + release: + types: [published] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + build_wheels: + name: Build wheels + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Build wheels + uses: pypa/cibuildwheel@0ecddd92b62987d7a2ae8911f4bb8ec9e2e4496a # v2.13.1 + - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: artifact-wheels + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set up Python 3.10 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.10" + - name: Install pypa/build + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-publish-requirements.txt requirements/publish.txt + pip install --require-hashes --no-deps -r /tmp/otx-publish-requirements.txt + rm /tmp/otx-publish-requirements.txt + - name: Build sdist + run: python -m build --sdist + - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: artifact-sdist + path: dist/*.tar.gz + + publish_package: + name: Publish package + needs: [build_wheels, build_sdist] + environment: pypi + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Download artifacts + uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3 + with: + # unpacks default artifact into dist/ + # if `name: artifact` is omitted, the action will create extra parent dir + path: dist + pattern: artifact-* + merge-multiple: true + # to determine where to publish the source distribution to PyPI or TestPyPI + - name: Check tag + id: check-tag + uses: actions-ecosystem/action-regex-match@9e6c4fb3d5e898f505be7a1fb6e7b0a278f6665b # v2.0.2 + with: + text: ${{ github.ref }} + regex: '^refs/tags/[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+rc[0-9]+|rc[0-9]+)?$' + - name: Upload package distributions to github + if: ${{ steps.check-tag.outputs.match != '' }} + uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: dist/* + tag: ${{ github.ref }} + overwrite: true + file_glob: true + - name: Publish package distributions to PyPI + if: ${{ steps.check-tag.outputs.match != '' }} + uses: pypa/gh-action-pypi-publish@e53eb8b103ffcb59469888563dc324e3c8ba6f06 # v1.8.12 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + - name: Publish package distributions to TestPyPI + if: ${{ steps.check-tag.outputs.match == '' }} + uses: pypa/gh-action-pypi-publish@e53eb8b103ffcb59469888563dc324e3c8ba6f06 # v1.8.12 + with: + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository-url: https://test.pypi.org/legacy/ + verbose: true diff --git a/.github/workflows/publish_internal.yaml b/.github/workflows/publish_internal.yaml deleted file mode 100644 index f487b175daf..00000000000 --- a/.github/workflows/publish_internal.yaml +++ /dev/null @@ -1,91 +0,0 @@ -name: Build and upload to internal PyPI - -on: - workflow_dispatch: # run on request (no need for PR) - -# Declare default permissions as read only. -permissions: read-all - -jobs: - build_wheels: - name: Build wheels - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Build wheels - uses: pypa/cibuildwheel@0ecddd92b62987d7a2ae8911f4bb8ec9e2e4496a # v2.13.1 - - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 - with: - name: artifact-wheels - path: ./wheelhouse/*.whl - - build_sdist: - name: Build source distribution - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Set up Python 3.10 - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - with: - python-version: "3.10" - - name: Install pypa/build - run: python -m pip install --require-hashes --no-deps -r .ci/publish-deps.txt - - name: Build sdist - run: python -m build --sdist - - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 - with: - name: artifact-sdist - path: dist/*.tar.gz - - publish_package: - name: Publish package - needs: [build_wheels, build_sdist] - environment: pypi - runs-on: [self-hosted, linux, x64, dev] - permissions: - packages: write - steps: - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - with: - python-version: "3.10" - - name: Install dependencies - run: python -m pip install --require-hashes --no-deps -r .ci/publish-deps.txt - - name: Download artifacts - uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3 - with: - path: dist - pattern: artifact-* - merge-multiple: true - # to determine where to publish the package distribution to PyPI or TestPyPI - - name: Check tag - id: check-tag - uses: actions-ecosystem/action-regex-match@9e6c4fb3d5e898f505be7a1fb6e7b0a278f6665b # v2.0.2 - with: - text: ${{ github.ref }} - regex: '^refs/heads/releases/[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+rc[0-9]+|rc[0-9]+)?$' - - name: Check dist contents - run: twine check dist/* - - name: Publish package dist to internal PyPI - if: ${{ steps.check-tag.outputs.match != '' }} - run: | - export no_proxy=${{ secrets.PYPI_HOST }} - export REPOSITORY_URL=http://${{ secrets.PYPI_HOST }}:${{ secrets.PYPI_PORT }} - twine upload --verbose --repository-url $REPOSITORY_URL dist/* -u ${{ secrets.PYPI_USER }} -p ${{ secrets.PYPI_PASSWORD }} - - name: Publish package distributions to TestPyPI - if: ${{ steps.check-tag.outputs.match == '' }} - run: | - export REPOSITORY_URL=https://test.pypi.org/legacy/ - twine upload --verbose --repository-url $REPOSITORY_URL dist/* -u __token__ -p ${{ secrets.TEST_PYPI_API_TOKEN }} - - name: Clean up dist - if: ${{ always() }} - run: | - if OUTPUT=$(ls | grep -c dist) - then - echo "Cleaning up dist directory" - rm -r dist - fi diff --git a/.github/workflows/publish_internal.yml b/.github/workflows/publish_internal.yml new file mode 100644 index 00000000000..3ea6d80b826 --- /dev/null +++ b/.github/workflows/publish_internal.yml @@ -0,0 +1,100 @@ +name: Build and upload to internal PyPI + +on: + workflow_dispatch: # run on request (no need for PR) + +# Declare default permissions as read only. +permissions: read-all + +jobs: + build_wheels: + name: Build wheels + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Build wheels + uses: pypa/cibuildwheel@0ecddd92b62987d7a2ae8911f4bb8ec9e2e4496a # v2.13.1 + - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: artifact-wheels + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set up Python 3.10 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.10" + - name: Install pypa/build + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-publish-requirements.txt requirements/publish.txt + pip install --require-hashes --no-deps -r /tmp/otx-publish-requirements.txt + rm /tmp/otx-publish-requirements.txt + - name: Build sdist + run: python -m build --sdist + - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: artifact-sdist + path: dist/*.tar.gz + + publish_package: + name: Publish package + needs: [build_wheels, build_sdist] + environment: pypi + runs-on: [self-hosted, linux, x64, dev] + permissions: + packages: write + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set up Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.10" + - name: Install dependencies + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-publish-requirements.txt requirements/publish.txt + pip install --require-hashes --no-deps -r /tmp/otx-publish-requirements.txt + rm /tmp/otx-publish-requirements.txt + - name: Download artifacts + uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3 + with: + # unpacks default artifact into dist/ + # if `name: artifact` is omitted, the action will create extra parent dir + path: dist + pattern: artifact-* + merge-multiple: true + - name: Check tag + id: check-tag + uses: actions-ecosystem/action-regex-match@9e6c4fb3d5e898f505be7a1fb6e7b0a278f6665b # v2.0.2 + with: + text: ${{ github.ref }} + regex: '^refs/heads/releases/[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+rc[0-9]+|rc[0-9]+)?$' + - name: Check dist contents + run: twine check dist/* + - name: Publish package dist to internal PyPI + if: ${{ steps.check-tag.outputs.match != '' }} + run: | + export no_proxy=${{ secrets.PYPI_HOST }} + export REPOSITORY_URL=http://${{ secrets.PYPI_HOST }}:${{ secrets.PYPI_PORT }} + twine upload --verbose --repository-url $REPOSITORY_URL dist/* -u ${{ secrets.PYPI_USER }} -p ${{ secrets.PYPI_PASSWORD }} + - name: Publish package distributions to TestPyPI + if: ${{ steps.check-tag.outputs.match == '' }} + run: | + export REPOSITORY_URL=https://test.pypi.org/legacy/ + twine upload --verbose --repository-url $REPOSITORY_URL dist/* -u __token__ -p ${{ secrets.TEST_PYPI_API_TOKEN }} + - name: Clean up dist + if: ${{ always() }} + run: | + if OUTPUT=$(ls | grep -c dist) + then + echo "Cleaning up dist directory" + rm -r dist + fi diff --git a/.github/workflows/run_tests_in_tox.yml b/.github/workflows/run_tests_in_tox.yml new file mode 100644 index 00000000000..2334f67eb71 --- /dev/null +++ b/.github/workflows/run_tests_in_tox.yml @@ -0,0 +1,75 @@ +on: + workflow_call: + inputs: + python-version: + type: string + default: "3.10" + toxenv-pyver: + description: "[py38, py39, py310]" + type: string + default: "py310" + toxenv-task: + description: "[all, act, ano, cls, det, seg, iseg]" + type: string + default: "all" + tests-dir: + type: string + default: "" + timeout-minutes: + type: number + default: 720 + upload-artifact: + type: boolean + default: false + runs-on: + type: string + default: "['self-hosted', 'Linux', 'X64', 'dev']" + task: + type: string + default: "undefined" + artifact-prefix: + type: string + default: "test-results" + toxenv-ptver: + type: string + default: "pt1" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + run_tests_in_tox: + # tricky workaround to pass list from the string input type + # https://github.com/orgs/community/discussions/11692 + runs-on: ${{ fromJson(inputs.runs-on) }} + timeout-minutes: ${{ inputs.timeout-minutes }} + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set up Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: ${{ inputs.python-version }} + - name: Install dependencies + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-dev-requirements.txt requirements/dev.txt + pip install --require-hashes --no-deps -r /tmp/otx-dev-requirements.txt + rm /tmp/otx-dev-requirements.txt + - name: Run Tests + env: + MLFLOW_TRACKING_SERVER_URI: ${{ vars.MLFLOW_TRACKING_SERVER_URI }} + BENCHMARK_RESULTS_CLEAR: ${{ vars.BENCHMARK_RESULTS_CLEAR }} + GH_CTX_REF_NAME: ${{ github.ref_name }} + GH_CTX_SHA: ${{ github.sha }} + run: tox -vv -e tests-${{ inputs.toxenv-task }}-${{ inputs.toxenv-pyver }}-${{ inputs.toxenv-ptver }} -- ${{ inputs.tests-dir }} + - name: Upload test results + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: ${{ inputs.artifact-prefix }}-${{ inputs.toxenv-task }}-${{ inputs.toxenv-pyver }}-${{ inputs.toxenv-ptver }} + path: | + .tox/tests-${{ inputs.toxenv-task }}-${{ inputs.toxenv-pyver }}-${{ inputs.toxenv-ptver }}.csv + .tox/tests-reg_${{ inputs.task }}*.csv + .tox/perf-*.csv + # Use always() to always run this step to publish test results when there are test failures + if: ${{ inputs.upload-artifact && always() }} diff --git a/.github/workflows/run_tests_in_tox_custom.yml b/.github/workflows/run_tests_in_tox_custom.yml new file mode 100644 index 00000000000..a524d4c7c74 --- /dev/null +++ b/.github/workflows/run_tests_in_tox_custom.yml @@ -0,0 +1,81 @@ +on: + workflow_call: + inputs: + python-version: + type: string + default: "3.10" + toxenv-pyver: + description: "[py38, py39, py310]" + type: string + default: "py310" + toxenv-task: + description: "[all, act, ano, cls, det, seg, iseg]" + type: string + default: "all" + tests-dir: + type: string + default: "" + timeout-minutes: + type: number + default: 720 + upload-artifact: + type: boolean + default: false + runs-on: + type: string + default: "['self-hosted', 'Linux', 'X64', 'dev']" + task: + type: string + default: "undefined" + artifact-prefix: + type: string + default: "test-results" + toxenv-ptver: + type: string + default: "pt1" + container-options: + type: string + default: "--runtime=nvidia --env-file=/home/runner/.nvidia.env --shm-size=24g" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + run_tests_on_custom: + # tricky workaround to pass list from the string input type + # https://github.com/orgs/community/discussions/11692 + runs-on: ${{ fromJson(inputs.runs-on) }} + container: + image: 219678651685.dkr.ecr.eu-central-1.amazonaws.com/ote-ci:11.7.1.1-devel-ubuntu20.04 + options: ${{ inputs.container-options }} + timeout-minutes: ${{ inputs.timeout-minutes }} + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set up Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: ${{ inputs.python-version }} + - name: Install dependencies + run: | + pip install --require-hashes --no-deps -r requirements/gh-actions.txt + pip-compile --generate-hashes -o /tmp/otx-dev-requirements.txt requirements/dev.txt + pip install --require-hashes --no-deps -r /tmp/otx-dev-requirements.txt + rm /tmp/otx-dev-requirements.txt + - name: Run Tests + env: + MLFLOW_TRACKING_SERVER_URI: ${{ vars.MLFLOW_TRACKING_SERVER_URI }} + BENCHMARK_RESULTS_CLEAR: ${{ vars.BENCHMARK_RESULTS_CLEAR }} + GH_CTX_REF_NAME: ${{ github.ref_name }} + GH_CTX_SHA: ${{ github.sha }} + run: tox -vv -e tests-${{ inputs.toxenv-task }}-${{ inputs.toxenv-pyver }}-${{ inputs.toxenv-ptver }} -- ${{ inputs.tests-dir }} + - name: Upload test results + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: ${{ inputs.artifact-prefix }}-${{ inputs.toxenv-task }}-${{ inputs.toxenv-pyver }}-${{ inputs.toxenv-ptver }} + path: | + .tox/tests-${{ inputs.toxenv-task }}-${{ inputs.toxenv-pyver }}-${{ inputs.toxenv-ptver }}.csv + .tox/tests-reg_${{ inputs.task }}*.csv + .tox/perf-*.csv + # Use always() to always run this step to publish test results when there are test failures + if: ${{ inputs.upload-artifact && always() }} diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yml similarity index 100% rename from .github/workflows/scorecard.yaml rename to .github/workflows/scorecard.yml diff --git a/.github/workflows/stale_marker.yaml b/.github/workflows/stale_marker.yml similarity index 100% rename from .github/workflows/stale_marker.yaml rename to .github/workflows/stale_marker.yml diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml new file mode 100644 index 00000000000..2a2bd8202ef --- /dev/null +++ b/.github/workflows/weekly.yml @@ -0,0 +1,22 @@ +name: Weekly Test + +on: + workflow_dispatch: # run on request (no need for PR) + schedule: + # every 12AM on Sunday + - cron: "0 0 * * 0" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + Weekly-Perf-Benchmark: + name: Weekly-Perf-Benchmark + uses: ./.github/workflows/perf_benchmark.yaml + with: + model-category: default + data-size: all + num-repeat: 0 + num-epoch: 0 + eval-upto: optimize + artifact-prefix: weekly-perf-benchmark diff --git a/.gitignore b/.gitignore index 6854e052ac4..d7ed0a6bbd6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,11 +9,16 @@ env otx-workspace* .env .tox +results/ +data/* .coverage +results/ build/ dist/ +!src/otx/recipes/** +src/otx/recipes/**/__pycache__ *egg-info *.pth @@ -38,5 +43,9 @@ src/**/*.c src/**/*.html src/**/*.so -# For regression test data storage -for_developers/regression_test/postgres_data/**/* + +# Dataset made by unit-test +tests/**/detcon_mask/* + +# sphinx-autosummary generated files +docs/**/_autosummary/ diff --git a/tests/unit/core/config/__init__.py b/.gitmodules similarity index 100% rename from tests/unit/core/config/__init__.py rename to .gitmodules diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000000..1def7865e02 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,4 @@ +[settings] +profile = black +known_third_party= + ote_sdk, diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 513a3086c94..229767045ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,56 +1,71 @@ default_language_version: + python: python3.10 node: 16.15.0 ruby: 2.7.2 repos: + - repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black + args: [--line-length, "120"] + files: '^(src|tests)/.*\.py' + # yaml formatting - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v2.7.1 hooks: - id: prettier + exclude: "src/otx/recipes" - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.6.1" + rev: "v1.1.1" hooks: - id: mypy - files: '^(src/otx)/.*\.py' - additional_dependencies: - [ + files: '^(src)/.*\.py' + additional_dependencies: [ + # numpy==1.19.5, types-PyYAML, attrs==21.2.*, types-requests, types-Deprecated, types-docutils, types_futures, - types-setuptools, types-python-dateutil, tokenize-rt==3.2.0, ] - args: [--disallow-untyped-calls] + args: [--no-strict-optional, --ignore-missing-imports] + exclude: "^src/otx/recipes" - repo: https://github.com/AleksaC/hadolint-py - rev: v2.12.0.3 + rev: v2.10.0 hooks: - id: hadolint name: Lint Dockerfiles description: Runs hadolint to lint Dockerfiles - repo: https://github.com/shellcheck-py/shellcheck-py - rev: v0.9.0.6 + rev: v0.9.0.5 hooks: - id: shellcheck # TODO remove this when all shell scripts have been removed from otx # markdown linting - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.37.0 + rev: v0.33.0 hooks: - id: markdownlint args: [--config=.markdownlint.yaml] # Ruff - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: "v0.1.2" + rev: "v0.0.258" hooks: - id: ruff - args: [--fix, --diff] - - id: ruff-format + exclude: "tests|src/otx/recipes" + args: [--fix] + + # # Git conflict marker + # - repo: https://github.com/jumanjihouse/pre-commit-hooks + # rev: 3.0.0 + # hooks: + # - id: git-check # Configure in .gitattributes diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000000..719d5c7bfa5 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,5 @@ +# See help here: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + +* @openvinotoolkit/ote-maintainers + +CODEOWNERS @openvinotoolkit/ote-admins @openvinotoolkit/ote-maintainers diff --git a/MANIFEST.in b/MANIFEST.in index 7a07397ba97..1a08c0b05ce 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,8 @@ -recursive-include src/otx *.patch +recursive-include requirements * recursive-include src/otx *.pyx recursive-include src/otx *.yaml recursive-include src/otx *.json recursive-include src/otx requirements.txt -recursive-include src/otx README.md recursive-exclude src/otx *.c graft tests global-exclude *.py[cod] diff --git a/NOTICE b/NOTICE deleted file mode 100644 index 9490a1ddedb..00000000000 --- a/NOTICE +++ /dev/null @@ -1,3 +0,0 @@ -This repository is under Apache License Version 2.0 licence. However the following files are borrowed from the external sources under different licenses. - -- MIT License: `src/otx/core/engine/**/*.py` diff --git a/docker/Dockerfile.cpu b/docker/Dockerfile.cpu new file mode 100644 index 00000000000..d4065fd8f33 --- /dev/null +++ b/docker/Dockerfile.cpu @@ -0,0 +1,28 @@ +FROM ubuntu:20.04@sha256:bb1c41682308d7040f74d103022816d41c50d7b0c89e9d706a74b4e548636e54 + +ARG PYTHON_VER=3.9 +ARG SOURCE=https://download.pytorch.org/whl/cpu +ENV DEBIAN_FRONTEND=noninteractive + +# hadolint ignore=DL3008 +RUN apt-get update && \ + apt-get install --no-install-recommends -y ca-certificates && \ + apt-get install --no-install-recommends -y curl python${PYTHON_VER} python${PYTHON_VER}-dev python${PYTHON_VER}-distutils g++ ffmpeg libsm6 libxext6 libgl1-mesa-glx && \ + curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ + python${PYTHON_VER} get-pip.py && \ + rm get-pip.py && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN mkdir /mnt/shared + +WORKDIR /training_extensions +COPY . /training_extensions + +RUN ln -s /mnt/shared ./shared + +# hadolint ignore=SC2102 +RUN pip install --no-cache-dir --require-hashes --no-deps -r docker/requirements.txt && \ + pip install --no-cache-dir -e .[full] + +CMD ["/bin/bash"] diff --git a/docker/Dockerfile.cuda b/docker/Dockerfile.cuda deleted file mode 100644 index bd7348c0db0..00000000000 --- a/docker/Dockerfile.cuda +++ /dev/null @@ -1,40 +0,0 @@ -FROM pytorch/pytorch:2.1.2-cuda11.8-cudnn8-runtime@sha256:971fbeae82c0a5a7a970a264a8b8ce1c3426aa79df7111004ad2bc2640f7d89c AS base - -ARG http_proxy -ARG https_proxy -ARG no_proxy -ARG NON_ROOT_HOME=/home/non-root - -RUN apt-get update && apt-get install -y --no-install-recommends \ - libsm6=2:1.2.3-1 \ - libxext6=2:1.3.4-0ubuntu1 \ - ffmpeg=7:4.2.7-0ubuntu0.1 \ - libfontconfig1=2.13.1-2ubuntu3 \ - libxrender1=1:0.9.10-1 \ - libgl1-mesa-glx=21.2.6-0ubuntu0.1~20.04.2 \ - && rm -rf /var/lib/apt/lists/* - -RUN useradd -l -u 10001 non-root \ - && mkdir -p ${NON_ROOT_HOME} - -WORKDIR ${NON_ROOT_HOME} -COPY . src_dir -RUN chown -R non-root:non-root ${NON_ROOT_HOME} - -USER non-root - -ENV PATH=${PATH}:${NON_ROOT_HOME}/.local/bin - -RUN pip install --no-cache-dir --require-hashes --no-deps -r src_dir/.ci/piptools-deps.txt && \ - pip-compile --generate-hashes -o /tmp/requirements.txt src_dir/pyproject.toml && \ - pip install --no-cache-dir --no-deps -e src_dir/ && \ - pip install --no-cache-dir --require-hashes --no-deps -r /tmp/requirements.txt && \ - otx install --do-not-install-torch && \ - rm /tmp/requirements.txt - -FROM base AS cuda - - -FROM base AS cuda_pretrained_ready -COPY docker/download_pretrained_weights.py download_pretrained_weights.py -RUN python download_pretrained_weights.py diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index 52a6942f5ef..00000000000 --- a/docker/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# How to build cuda and cuda-pretrained-ready Docker images - -1. By executing the following commands, it will build two Docker images: `otx:${OTX_VERSION}-cuda` and `otx:${OTX_VERSION}-cuda-pretrained-ready`. - - ```console - git clone https://github.com/openvinotoolkit/training_extensions.git - cd docker - ./build.sh - ``` - -2. After that, you can check whether the images are built correctly such as - - ```console - docker image ls | grep otx - ``` - - Example: - - ```console - otx 2.0.0-cuda-pretrained-ready 4f3b5f98f97c 3 minutes ago 14.5GB - otx 2.0.0-cuda 8d14caccb29a 8 minutes ago 10.4GB - ``` - -`otx:${OTX_VERSION}-cuda` is a minimal Docker image where OTX is installed with CUDA supports. On the other hand, `otx:${OTX_VERSION}-cuda-pretrained-ready` includes all the model pre-trained weights that OTX provides in addition to `otx:${OTX_VERSION}-cuda`. diff --git a/docker/build.sh b/docker/build.sh index 5888516d481..5ce1978c472 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -1,23 +1,44 @@ #!/bin/bash -# shellcheck disable=SC2154 -OTX_VERSION=$(python -c 'import otx; print(otx.__version__)') -THIS_DIR=$(dirname "$0") +PYTHON_VER="3.9" -echo "Build OTX ${OTX_VERSION} CUDA Docker image..." -docker build \ - --build-arg http_proxy="${http_proxy}" \ - --build-arg https_proxy="${https_proxy}" \ - --build-arg no_proxy="${no_proxy}" \ - --target cuda \ - -t "otx:${OTX_VERSION}-cuda" \ - -f "${THIS_DIR}/Dockerfile.cuda" "${THIS_DIR}"/.. +while [[ $# -gt 0 ]]; do + key="$1" -echo "Build OTX ${OTX_VERSION} CUDA pretrained-ready Docker image..." -docker build \ - --build-arg http_proxy="${http_proxy}" \ - --build-arg https_proxy="${https_proxy}" \ - --build-arg no_proxy="${no_proxy}" \ - --target cuda_pretrained_ready \ - -t "otx:${OTX_VERSION}-cuda-pretrained-ready" \ - -f "${THIS_DIR}/Dockerfile.cuda" "${THIS_DIR}"/.. + case $key in + -p|--python) + PYTHON_VER="$2" + shift # past argument + shift # past value + ;; + -h|--help) + DEFAULT="yes" + break + shift # past argument + ;; + esac +done + +if [ "$DEFAULT" == "yes" ]; then +cat << EndofMessage + USAGE: $0 [Options] + Options + -p|--python Specify Python version + -h|--help Print this message +EndofMessage +exit 0 +fi + +docker build -f docker/Dockerfile.cpu \ +--build-arg HTTP_PROXY="${http_proxy:?}" \ +--build-arg HTTPS_PROXY="${https_proxy:?}" \ +--build-arg NO_PROXY="${no_proxy:?}" \ +--build-arg python_ver="$PYTHON_VER" \ +--tag otx/cpu/python"$PYTHON_VER":latest .; RET=$? + +if [ $RET -ne 0 ]; then + echo "failed to build a 'otx/cpu/python$PYTHON_VER:latest' image. $RET" + exit 1 +fi + +echo "Successfully built docker image." diff --git a/docker/download_pretrained_weights.py b/docker/download_pretrained_weights.py deleted file mode 100644 index 7fee845239e..00000000000 --- a/docker/download_pretrained_weights.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Helper script to download all the model pre-trained weights.""" - -import logging -from pathlib import Path - -from importlib_resources import files -from omegaconf import OmegaConf -from otx.core.utils.instantiators import partial_instantiate_class - -logging.basicConfig( - level=logging.INFO, - filename="download_pretrained_weights.log", - filemode="w", -) - -logger = logging.getLogger() - - -def download_all() -> None: - """Download pre-trained weights of all models.""" - recipe_dir = Path(files("otx") / "recipe") - - for config_path in recipe_dir.glob("**/*.yaml"): - if "_base_" in str(config_path): - msg = f"Skip {config_path} since it is a base config." - logger.warning(msg) - continue - if config_path.name == "openvino_model.yaml": - msg = f"Skip {config_path} since it is not a PyTorch config." - logger.warning(msg) - continue - if "anomaly_" in str(config_path) or "otx_dino_v2" in str(config_path) or "h_label_cls" in str(config_path): - msg = f"Skip {config_path} since those models show errors on instantiation." - logger.warning(msg) - continue - - config = OmegaConf.load(config_path) - init_model = next(iter(partial_instantiate_class(config.model))) - try: - model = init_model() - msg = f"Downloaded pre-trained model weight of {model!s}" - logger.info(msg) - except Exception: - msg = f"Error on instiating {config_path}" - logger.exception(msg) - - -if __name__ == "__main__": - download_all() diff --git a/docker/requirements.in b/docker/requirements.in new file mode 100644 index 00000000000..1d09cc74f23 --- /dev/null +++ b/docker/requirements.in @@ -0,0 +1,3 @@ +torch==1.13.1 +torchvision==0.14.1 +torchaudio==0.13.1 diff --git a/docker/requirements.txt b/docker/requirements.txt new file mode 100644 index 00000000000..4364b52e0e2 --- /dev/null +++ b/docker/requirements.txt @@ -0,0 +1,264 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --extra-index-url=https://download.pytorch.org/whl/cpu --generate-hashes --output-file=docker/requirements.txt docker/requirements.in +# +--extra-index-url https://download.pytorch.org/whl/cpu + +certifi==2024.2.2 \ + --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ + --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 + # via requests +charset-normalizer==3.3.2 \ + --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ + --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ + --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ + --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ + --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ + --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ + --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ + --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ + --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ + --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ + --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ + --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ + --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ + --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ + --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ + --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ + --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ + --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ + --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ + --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ + --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ + --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ + --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ + --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ + --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ + --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ + --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ + --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ + --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ + --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ + --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ + --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ + --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ + --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ + --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ + --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ + --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ + --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ + --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ + --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ + --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ + --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ + --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ + --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ + --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ + --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ + --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ + --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ + --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ + --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ + --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ + --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ + --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ + --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ + --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ + --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ + --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ + --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ + --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ + --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ + --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ + --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ + --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ + --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ + --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ + --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ + --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ + --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ + --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ + --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ + --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ + --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ + --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ + --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ + --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ + --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ + --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ + --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ + --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ + --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ + --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ + --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ + --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ + --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ + --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ + --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ + --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ + --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ + --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ + --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 + # via requests +idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via requests +numpy==1.26.4 \ + --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ + --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ + --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ + --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ + --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ + --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ + --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ + --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ + --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ + --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ + --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ + --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ + --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ + --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ + --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ + --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ + --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ + --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ + --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ + --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ + --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ + --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ + --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ + --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ + --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ + --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ + --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ + --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ + --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ + --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ + --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ + --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ + --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ + --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ + --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ + --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f + # via torchvision +pillow==10.2.0 \ + --hash=sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8 \ + --hash=sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39 \ + --hash=sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac \ + --hash=sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869 \ + --hash=sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e \ + --hash=sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04 \ + --hash=sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9 \ + --hash=sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e \ + --hash=sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe \ + --hash=sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef \ + --hash=sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56 \ + --hash=sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa \ + --hash=sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f \ + --hash=sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f \ + --hash=sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e \ + --hash=sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a \ + --hash=sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2 \ + --hash=sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2 \ + --hash=sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5 \ + --hash=sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a \ + --hash=sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2 \ + --hash=sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213 \ + --hash=sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563 \ + --hash=sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591 \ + --hash=sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c \ + --hash=sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2 \ + --hash=sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb \ + --hash=sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757 \ + --hash=sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0 \ + --hash=sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452 \ + --hash=sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad \ + --hash=sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01 \ + --hash=sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f \ + --hash=sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5 \ + --hash=sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61 \ + --hash=sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e \ + --hash=sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b \ + --hash=sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068 \ + --hash=sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9 \ + --hash=sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588 \ + --hash=sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483 \ + --hash=sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f \ + --hash=sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67 \ + --hash=sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7 \ + --hash=sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311 \ + --hash=sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6 \ + --hash=sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72 \ + --hash=sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6 \ + --hash=sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129 \ + --hash=sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13 \ + --hash=sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67 \ + --hash=sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c \ + --hash=sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516 \ + --hash=sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e \ + --hash=sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e \ + --hash=sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364 \ + --hash=sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023 \ + --hash=sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1 \ + --hash=sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04 \ + --hash=sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d \ + --hash=sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a \ + --hash=sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7 \ + --hash=sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb \ + --hash=sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4 \ + --hash=sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e \ + --hash=sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1 \ + --hash=sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48 \ + --hash=sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868 + # via torchvision +requests==2.31.0 \ + --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ + --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 + # via torchvision +torch==1.13.1+cpu \ + --hash=sha256:02e387834a3489396f1871f83f7a795610d09ef3da01aa431493b08eac5c6666 \ + --hash=sha256:11692523b87c45b79ddfb5148b12a713d85235d399915490d94e079521f7e014 \ + --hash=sha256:207ab3700cd9c4349f4fd1892597eb3d385eb78221c0f2974ec54b8ea903aa00 \ + --hash=sha256:2dd7e5f584c7ea5d8f038fd1fa40465785f797df0c7d06c432f5e72d9817115a \ + --hash=sha256:43394e66487543c112044194e9bdecc6f48c869d692a9d0c755b95d642b29535 \ + --hash=sha256:4a8b84834eb12b3428c24e9f264c9bd6a2cf449fffc191374e7dbb2b950fc6d7 \ + --hash=sha256:71636a5c21927236f4974d2355fb3f66a0b707c28219b0135ff65ed0f0e61287 \ + --hash=sha256:988ee77c0975b4c3f570dfc62277b1e300bbbe7cc000ce2720e2e8c730fb9ce5 \ + --hash=sha256:dc185f2fdbb1f84855929d3ba7b36c74f218789d26a0e0268cb0586d466c8c24 + # via + # -r docker/requirements.in + # torchaudio + # torchvision +torchaudio==0.13.1+cpu \ + --hash=sha256:0f89fb3f6ecf894e4a287eb07fe41b07a5d2f8450d2c2ca62b994dedba63fefd \ + --hash=sha256:51e59ed350ce626a711ea3034c31de032d36ece3edca9311a58d78decc527a2f \ + --hash=sha256:56d7fd1a4b8ff89d8b37d43456d4607e8e2c641a57476c3d5bdb25ce02d4c7f3 \ + --hash=sha256:5c639c979b18d2bc595b440138c15c88d9b34ef56fc27d347a0ee73712cce4b8 \ + --hash=sha256:61e9f7490ee81f0796f9b109f4dd46a873192af5a0e39d2084d10c9010f846d4 \ + --hash=sha256:8d3f85add34a6c856a29c521cd907275704a867ef811476af88c935e44e36415 \ + --hash=sha256:dc464a0dc014d5b87a27921a63aedf358abbc8eed8e70d4ca3d773f02666a297 \ + --hash=sha256:fd6f069e1185394a5ec62b85df169d4c50020a13839f6f6a845fbc36120ff48a + # via -r docker/requirements.in +torchvision==0.14.1+cpu \ + --hash=sha256:2b58261a4270b173d43ff23344589f3a3a4087d33818bc991ff86e3df4a83d24 \ + --hash=sha256:46ff03ec16b49a6031714e98b1321a18ce789d8440a5073981e83b470a0484d1 \ + --hash=sha256:5d2b67028e2782e60fd667a8e95f071bf612282411cba7ae88720213c58a651a \ + --hash=sha256:6b40d371b80f16809ad5ced2fd8fd327c7a403a079eaa654c9da50bfaef73f78 \ + --hash=sha256:9191ce52974a6be4f2f75a23d9c96371ac3025448dfd48ee0d2a553316944c4c \ + --hash=sha256:a49c36369b48720304cda4085bba4a05b42ff295400feae89e0ee8683051f923 \ + --hash=sha256:bbc7c59733beb0236e5b489d34ba7d080d8c483be9e84bd6f5020c7d038ac751 \ + --hash=sha256:cc1466848e58873135afdda4f1c6432a5fd4bf4fb2a00886e9fb496a3004674b + # via -r docker/requirements.in +typing-extensions==4.9.0 \ + --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ + --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd + # via + # torch + # torchvision +urllib3==2.2.1 \ + --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \ + --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19 + # via requests diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index ff358cc7182..00000000000 --- a/docs/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# OTX 2.0 Documentation - -## Introduction - -This is the source code for the OTX documentation. It is built using sphinx-design and myst parser. - -## Installation - -To install the dependencies, run the following command: - -```bash -otx install --option docs -``` - -## Build - -To build the documentation, run the following command: - -```bash -sphinx-build -b html source build -``` diff --git a/docs/source/conf.py b/docs/source/conf.py index c2d7fe05f22..02e674cf0cf 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,9 +22,9 @@ # -- Project information ----------------------------------------------------- # -project = "OpenVINO™ Training Extensions" -copyright = "2024, OpenVINO™ Training Extensions Contributors" -author = "OpenVINO™ Training Extensions Contributors" +project = 'OpenVINO™ Training Extensions' +copyright = '2023, OpenVINO™ Training Extensions Contributors' +author = 'OpenVINO™ Training Extensions Contributors' release = __version__ # -- General configuration --------------------------------------------------- # @@ -34,19 +34,17 @@ # ones. extensions = [ "sphinx.ext.napoleon", # Support for NumPy and Google style docstrings - "sphinx.ext.autodoc", - "sphinx_copybutton", + 'sphinx.ext.autodoc', + 'sphinx_copybutton', "sphinx.ext.autosummary", # Create neat summary tables "sphinx.ext.viewcode", # Find the source files "sphinx.ext.autosectionlabel", # Refer sections its title "sphinx.ext.intersphinx", # Generate links to the documentation - "sphinx_tabs.tabs", - "sphinx_design", ] source_suffix = { ".rst": "restructuredtext", - ".md": "markdown", + ".md": "markdown", } suppress_warnings = [ @@ -55,7 +53,7 @@ ] # Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] +templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -69,14 +67,14 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_theme = "pydata_sphinx_theme" -html_static_path = ["_static"] +html_theme = 'pydata_sphinx_theme' +html_static_path = ['_static'] html_theme_options = { - "navbar_center": [], - "logo": { - "image_light": "logos/otx-logo.png", - "image_dark": "logos/otx-logo.png", + "navbar_center": [], + "logo": { + "image_light": 'logos/otx-logo.png', + "image_dark": 'logos/otx-logo.png', }, "icon_links": [ { @@ -88,7 +86,7 @@ ], } html_css_files = [ - "css/custom.css", + 'css/custom.css', ] # -- Extension configuration ------------------------------------------------- @@ -109,6 +107,6 @@ autoclass_content = "both" -autosummary_generate = True # Turn on sphinx.ext.autosummary -autosummary_ignore_module_all = False # Summary list in __all__ no others +autosummary_generate = True # Turn on sphinx.ext.autosummary +autosummary_ignore_module_all = False # Summary list in __all__ no others # autosummary_imported_members = True # document classes and functions imported in modules diff --git a/docs/source/guide/explanation/additional_features/adaptive_training.rst b/docs/source/guide/explanation/additional_features/adaptive_training.rst index 0bc03fe6b56..c0e989d722c 100644 --- a/docs/source/guide/explanation/additional_features/adaptive_training.rst +++ b/docs/source/guide/explanation/additional_features/adaptive_training.rst @@ -3,24 +3,8 @@ Adaptive Training Adaptive-training focuses to adjust the number of iterations or interval for the validation to achieve the fast training. In the small data regime, we don't need to validate the model at every epoch since there are a few iterations at a single epoch. -To handle this, we have implemented module named ``AdaptiveTrainScheduling``. This callback controls the interval of the validation to do faster training. +To handle this, we have implemented two modules named ``AdaptiveTrainingHook`` and ``AdaptiveRepeatDataHook``. Both of them controls the interval of the validation to do faster training. .. note:: - ``AdaptiveTrainScheduling`` changes the interval of the validation, evaluation and updating learning rate by checking the number of dataset. - - -.. tab-set:: - - .. tab-item:: API - - .. code-block:: python - - from otx.algo.callbacks.adaptive_train_scheduling import AdaptiveTrainScheduling - - engine.train(callbacks=[AdaptiveTrainScheduling()]) - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx train ... --callbacks otx.algo.callbacks.adaptive_train_scheduling.AdaptiveTrainScheduling + 1. ``AdaptiveTrainingHook`` changes the interval of the validation, evaluation and updating learning rate by checking the number of dataset. + 2. ``AdaptiveRepeatDataHook`` changes the repeats of the dataset by pathcing the sampler. diff --git a/docs/source/guide/explanation/additional_features/auto_configuration.rst b/docs/source/guide/explanation/additional_features/auto_configuration.rst index 8f4a125d187..5e91f239753 100644 --- a/docs/source/guide/explanation/additional_features/auto_configuration.rst +++ b/docs/source/guide/explanation/additional_features/auto_configuration.rst @@ -2,28 +2,29 @@ Auto-configuration ================== Auto-configuration for a deep learning framework means the automatic finding of the most appropriate settings for the training parameters, based on the dataset and the specific task at hand. -Auto-configuration can help to save time, it eases the process of interaction with OpenVINO™ Training Extensions and gives a better baseline for the given dataset. +Auto-configuration can help to save time, it eases the process of interaction with OpenVINO™ Training Extensions CLI and gives a better baseline for the given dataset. At this end, we developed a simple auto-configuration functionality to ease the process of training and validation utilizing our framework. Basically, to start the training and obtain a good baseline with the best trade-off between accuracy and speed we need to pass only a dataset in the right format without specifying anything else: -.. tab-set:: +.. code-block:: - .. tab-item:: API + $ otx train --train-data-roots - .. code-block:: python +.. note:: - from otx.engine import Engine + OpenVINO™ Training Extensions supports also ``otx build`` mode with the auto-configuration feature. We can build OpenVINO™ Training Extensions workspace with the following CLI command: - engine = Engine(data_root="") - engine.train() + .. code-block:: - .. tab-item:: CLI + $ otx build --train-data-roots - .. code-block:: bash +Moreover, our dataset can have no train/val splits at all. The Datumaro manager integrated into OpenVINO™ Training Extensions will handle it on its own. +It will recognize the task by analyzing the dataset and if there is no splits for the validation - Datumaro will do a random auto-split, saving this split to the workspace. It could be used with ``otx optimize`` or ``otx train``. - (otx) ...$ otx train ... --data_root +.. note:: + Currently, Datumaro auto-split feature supports 3 formats: `Imagenet `_ (multi-class classification), `COCO `_ (detection) and `Cityscapes `_ (semantic segmentation). After dataset preparation, the training will be started with the middle-sized template to achieve competitive accuracy preserving fast inference. @@ -40,26 +41,21 @@ Supported dataset formats for each task: - anomaly segmentation: `MVTec `_ - instance segmentation: `COCO `_, `Pascal-VOC `_ -If we have a dataset format occluded with other tasks, for example ``COCO`` format, we should directly emphasize the task type. If not, OpenVINO™ Training Extensions automatically chooses the task type that you might not intend: +If we have a dataset format occluded with other tasks, for example ``COCO`` format, we should directly emphasize the task type and use ``otx build`` first with an additional CLI option. If not, OpenVINO™ Training Extensions automatically chooses the task type that you might not intend: -.. tab-set:: +.. code-block:: - .. tab-item:: API + $ otx build --train-data-roots + --task {CLASSIFICATION, DETECTION, SEGMENTATION, ACTION_CLASSIFICATION, ACTION_DETECTION, ANOMALY_CLASSIFICATION, ANOMALY_DETECTION, ANOMALY_SEGMENTATION, INSTANCE_SEGMENTATION} - .. code-block:: python +It will create a task-specific workspace folder with configured template and auto dataset split if supported. - from otx.engine import Engine +Move to this folder and simply run without any options to start training: - engine = Engine(data_root="", task="") - engine.train() +.. code-block:: - .. tab-item:: CLI + $ otx train - .. code-block:: bash - - (otx) ...$ otx train --data_root - --task {MULTI_CLASS_CLS, MULTI_LABEL_CLS, H_LABEL_CLS, DETECTION, INSTANCE_SEGMENTATION, SEMANTIC_SEGMENTATION, ACTION_CLASSIFICATION, ACTION_DETECTION, ACTION_SEGMENTATION, ANOMALY_CLASSIFICATION, ANOMALY_DETECTION, ANOMALY_SEGMENTATION, VISUAL_PROMPTING} - ... Auto-adapt batch size --------------------- @@ -78,19 +74,9 @@ The learning rate is also adjusted based on the updated batch size accordingly. To use this feature, add the following parameter: -.. tab-set:: - - .. tab-item:: API - - .. code-block:: python - - Need to update! +.. code-block:: - .. tab-item:: CLI - - .. code-block:: bash - - Need to update! + $ otx train params --learning_parameters.auto_adapt_batch_size Safe 2. Find the maximum executable batch size (`Full` mode) @@ -101,19 +87,9 @@ Similar to the previous method, the learning rate is adjusted according to the u To use this feature, add the following parameter: -.. tab-set:: - - .. tab-item:: API +.. code-block:: - .. code-block:: python - - Need to update! - - .. tab-item:: CLI - - .. code-block:: bash - - Need to update! + $ otx train params --learning_parameters.auto_adapt_batch_size Full .. Warning:: @@ -132,20 +108,22 @@ To simplify the process of setting ``num_workers`` manually, this feature automa To use this feature, add the following parameter: -.. tab-set:: - - .. tab-item:: API +.. code-block:: - .. code-block:: python + $ otx train params --learning_parameters.auto_num_workers True - from otx.core.config.data import DataModuleConfig - from otx.core.data.module import OTXDataModule +Auto-detect training type +------------------------- - data_config = DataModuleConfig(..., auto_num_workers=True) - datamodule = OTXDataModule(..., config=data_config) +OpenVINO™ Training Extensions also support automatic detection of training types such as Semi-SL, Self-SL and Incremental. For Semi-SL usage only is a path to unlabeled data via `--unlabeled-data-roots` option needed for the command line. To use Self-SL learning just a folder with images in the `--train-data-roots` option without validation data is required to automatically start Self-SL pretraining. +OpenVINO™ Training Extensions will automatically recognize these types of tasks and if the task supports this training type the training will be started. - .. tab-item:: CLI +.. note:: + To use auto template configuration with Self-SL training type `--task` option is required since it is impossible to recognize task type by folder with only images. - .. code-block:: shell +Auto-adapt input size +--------------------- - (otx) ...$ otx train ... --data.config.auto_num_workers True +"Auto" input size feature tries to automatically select the right model input size +based on given dataset statictics. +See :ref:`adaptive-input-size`. diff --git a/docs/source/guide/explanation/additional_features/config_input_size.rst b/docs/source/guide/explanation/additional_features/config_input_size.rst new file mode 100644 index 00000000000..715831b07e5 --- /dev/null +++ b/docs/source/guide/explanation/additional_features/config_input_size.rst @@ -0,0 +1,71 @@ +Configurable Input Size +======================= + +This feature makes OTX use smaller input resolutions to expedite training and inference times, +or opt for larger input size to enhance overall model capabilities. + +Rather than manually modifying values within the data pipeline, a streamlined approach is provided. +You can change the model's input size by simply adding an argument during `train`, `eval`, or `export`. +Furthermore, when using a model weight trained on an input size other than the default, +OTX automatically aligns data pipelines to input size during `eval`` and `export` processes. + +You can use this feature using the following command: + +.. code-block:: + + $ otx train \ + ... \ + params --learning_parameters.input_size input_size + +The available input sizes are currently as follows: + +- 64x64 (only for classification) +- 128x128 (only for classification) +- 224x224 (only for classification) +- 256x256 +- 384x384 +- 512x512 +- 768x768 +- 1024x1024 +- Default (per-model default input size) +- Auto (adaptive to dataset statistics) + +.. _adaptive-input-size: + +Adaptive Input Size +------------------- + +"Auto" mode tries to automatically select the right size +based on given dataset statictics. + +1. OTX analyzes the input dataset to get robust statistics. + +2. Input size is initially set to typical large image size. + +.. code-block:: + + input_size = large_image_size + +3. (Optionally) Input size is adjusted by object sizes in the dataset, if any. + The input size from image size is rescaled accoridng to the ratio of + minimum recongnizable object size of models, which is typically 16x16 ~ 32x32, + and the typical small object size in the dataset. + In short, if objects are 64x64 in general in 512x512 image, + it will be down-scaled to 256x256 as 32x32 objects are enough to be detected. + +.. code-block:: + + input_size = input_size * MIN_RECOGNIZABLE_OBJECT_SIZE / small_object_size + +4. Select the closest size from standard preset sizes + +5. Restrict scale-up + +.. code-block:: + + input_size = min(input_size, default_model_input_size) + + +.. Note:: + Using smaller input size with datasets having lower image resolutions or larger objects can yield a speed advantage with minimal impact on model performance. + But note that the choice of small input size can potentially influence model performance based on the task, model architecture, and dataset characteristics. diff --git a/docs/source/guide/explanation/additional_features/fast_data_loading.rst b/docs/source/guide/explanation/additional_features/fast_data_loading.rst index 111412ac969..c219a5ba456 100644 --- a/docs/source/guide/explanation/additional_features/fast_data_loading.rst +++ b/docs/source/guide/explanation/additional_features/fast_data_loading.rst @@ -5,6 +5,21 @@ OpenVINO™ Training Extensions provides several ways to boost model training sp one of which is fast data loading. +=================== +Faster Augmentation +=================== + + +****** +AugMix +****** +AugMix [1]_ is a simple yet powerful augmentation technique +to improve robustness and uncertainty estimates of image classification task. +OpenVINO™ Training Extensions implemented it in `Cython `_ for faster augmentation. +Users do not need to configure anything as cythonized AugMix is used by default. + + + ======= Caching ======= @@ -21,20 +36,38 @@ One can enable in-memory caching for maximizing GPU utilization and reducing mod training time in those cases. -.. tab-set:: +.. code-block:: + + $ otx train --mem-cache-size=8GB .. + + + +*************** +Storage Caching +*************** + +OpenVINO™ Training Extensions uses `Datumaro `_ +under the hood for dataset managements. +Since Datumaro `supports `_ +`Apache Arrow `_, OpenVINO™ Training Extensions +can exploit fast data loading using memory-mapped arrow file at the expanse of storage consumtion. + + +.. code-block:: + + $ otx train .. params --algo_backend.storage_cache_scheme JPEG/75 + - .. tab-item:: API +The cache would be saved in ``$HOME/.cache/otx`` by default. +One could change it by modifying ``OTX_CACHE`` environment variable. - .. code-block:: python - from otx.core.config.data import DataModuleConfig - from otx.core.data.module import OTXDataModule +.. code-block:: - data_config = DataModuleConfig(..., mem_cache_size="8GB") - datamodule = OTXDataModule(..., config=data_config) + $ OTX_CACHE=/path/to/cache otx train .. params --algo_backend.storage_cache_scheme JPEG/75 - .. tab-item:: CLI - .. code-block:: shell +Please refere `Datumaro document `_ +for available schemes to choose but we recommend ``JPEG/75`` for fast data loaidng. - (otx) ...$ otx train ... --data.config.mem_cache_size 8GB +.. [1] Dan Hendrycks, Norman Mu, Ekin D. Cubuk, Barret Zoph, Justin Gilmer, and Balaji Lakshminarayanan. "AugMix: A Simple Data Processing Method to Improve Robustness and Uncertainty" International Conference on Learning Representations. 2020. diff --git a/docs/source/guide/explanation/additional_features/hpo.rst b/docs/source/guide/explanation/additional_features/hpo.rst index effb70ac0c0..6c471604a1c 100644 --- a/docs/source/guide/explanation/additional_features/hpo.rst +++ b/docs/source/guide/explanation/additional_features/hpo.rst @@ -17,23 +17,11 @@ Key features of OpenVINO™ Training Extensions include: You can run HPO by just adding **--enable-hpo** argument as below: -.. tab-set:: - - .. tab-item:: API - - .. code-block:: python - - from otx.engine import Engine - - engine = Engine(data_root="") - engine.train(run_hpo=True) - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx train ... --run_hpo True +.. code-block:: + $ otx train \ + ... \ + --enable-hpo ========= Algorithm @@ -59,7 +47,21 @@ You can configure HPO by modifying the ``hpo_config.yaml`` file. This file conta .. code-block:: - Need to Update! + metric: accuracy + search_algorithm: asha + hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0007 + - 0.07 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 32 + - 128 + - 2 As you can see, there are a few attributes required to run HPO. Fortunately, there are not many attributes, so it's not difficult to write your own ``hpo_config.yaml`` file. The more detailed description is as follows: diff --git a/docs/source/guide/explanation/additional_features/index.rst b/docs/source/guide/explanation/additional_features/index.rst index d7fa4855d47..0551f916191 100644 --- a/docs/source/guide/explanation/additional_features/index.rst +++ b/docs/source/guide/explanation/additional_features/index.rst @@ -11,5 +11,7 @@ Additional Features auto_configuration adaptive_training xai + noisy_label_detection fast_data_loading tiling + config_input_size diff --git a/docs/source/guide/explanation/additional_features/models_optimization.rst b/docs/source/guide/explanation/additional_features/models_optimization.rst index 89438badf21..08715fb3165 100644 --- a/docs/source/guide/explanation/additional_features/models_optimization.rst +++ b/docs/source/guide/explanation/additional_features/models_optimization.rst @@ -1,7 +1,7 @@ Models Optimization =================== -OpenVINO™ Training Extensions provides optimization algorithm: `Post-Training Quantization tool (PTQ) `_. +OpenVINO™ Training Extensions provides two types of optimization algorithms: `Post-Training Quantization tool (PTQ) `_ and `Neural Network Compression Framework (NNCF) `_. ******************************* Post-Training Quantization Tool @@ -11,21 +11,23 @@ PTQ is designed to optimize the inference of models by applying post-training me To run Post-training quantization it is required to convert the model to OpenVINO™ intermediate representation (IR) first. To perform fast and accurate quantization we use ``DefaultQuantization Algorithm`` for each task. Please, refer to the `Tune quantization Parameters `_ for further information about configuring the optimization. -Please, refer to our :doc:`dedicated tutorials <../../tutorials/base/how_to_train/index>` on how to optimize your model using PTQ. +PTQ parameters can be found and configured in ``template.yaml`` and ``configuration.yaml`` for each task. For Anomaly and Semantic Segmentation tasks, we have separate configuration files for PTQ, that can be found in the same directory with ``template.yaml``, for example for `PaDiM `_, `OCR-Lite-HRNe-18-mod2 `_ model. +************************************ +Neural Network Compression Framework +************************************ -.. tab-set:: +NNCF utilizes Training-time Optimization algorithms. It is a set of advanced algorithms for training-time model optimization within the Deep Learning framework such as Pytorch. +The process of optimization is controlled by the NNCF configuration file. A JSON configuration file is used for easier setup of the parameters of the compression algorithm. See `configuration file description `_. - .. tab-item:: API +You can refer to configuration files for default templates for each task accordingly: `Classification `_, `Object Detection `_, `Semantic segmentation `_, `Instance segmentation `_, `Anomaly classification `_, `Anomaly Detection `_, `Anomaly segmentation `_. Configs for other templates can be found in the same directory. - .. code-block:: python - from otx.engine import Engine - ... - engine.optimize(checkpoint="") +NNCF tends to provide better quality in terms of preserving accuracy as it uses training compression approaches. +Compression results achievable with the NNCF can be found `here `_ . +Meanwhile, the PTQ is faster but can degrade accuracy more than the training-enabled approach. - .. tab-item:: CLI +.. note:: + The main recommendation is to start with post-training compression and use NNCF compression during training if you are not satisfied with the results. - .. code-block:: shell - - (otx) ...$ otx optimize ... --checkpoint +Please, refer to our :doc:`dedicated tutorials <../../tutorials/base/how_to_train/index>` on how to optimize your model using PTQ or NNCF. \ No newline at end of file diff --git a/docs/source/guide/explanation/additional_features/noisy_label_detection.rst b/docs/source/guide/explanation/additional_features/noisy_label_detection.rst new file mode 100644 index 00000000000..7573124d943 --- /dev/null +++ b/docs/source/guide/explanation/additional_features/noisy_label_detection.rst @@ -0,0 +1,30 @@ +Noisy Label Detection +===================== + +OpenVINO™ Training Extensions provide a feature for detecting noisy labels during model training. +With this feature, you can identify noisy labeled samples in your training dataset. +Our algorithm accumulates the training loss dynamics during the model training +and exports it to `Datumaro `_. +The training loss dynamics are then post-processed by exponential moving average (EMA), +a strong criterion for detecting noisy label samples [1]_. +Finally, Datumaro ranks the top-k samples, which can be considered as noisy labeled candidates. +We provide an end-to-end example written in a Jupyter notebook, which you can find at the link in the note below. + +In OpenVINO™ Training Extensions CLI, you can enable this feature +by adding the argument ``--algo_backend.enable_noisy_label_detection true`` as follows. + +.. code-block:: + + $ otx train ... params --algo_backend.enable_noisy_label_detection true + +.. note:: + Currently, it supports both multi-class classification task and detection task with single GPU training. + +.. note:: **Important!** + The post-processing step to analyze the training loss dynamics requires `Datumaro `_. + Please see the following Jupyter notebook examples: + + 1. `Multi-class classification task `_. + 2. `Detection task `_. + +.. [1] Zhou, Tianyi, Shengjie Wang, and Jeff Bilmes. "Robust curriculum learning: from clean label detection to noisy label self-correction." International Conference on Learning Representations. 2021. diff --git a/docs/source/guide/explanation/additional_features/tiling.rst b/docs/source/guide/explanation/additional_features/tiling.rst index fe233057903..6bfd9adb0db 100644 --- a/docs/source/guide/explanation/additional_features/tiling.rst +++ b/docs/source/guide/explanation/additional_features/tiling.rst @@ -26,87 +26,93 @@ During testing, each tile is processed and predicted separately. The tiles are t The tiling strategy is implemented in the OpenVINO Training Extensions through the following steps: -.. note:: +.. code-block:: * Training: Create an ImageTilingDataset with annotated tiles -> Train with annotated tile images -> Evaluate on annotated tiles * Testing: Create an ImageTilingDataset including all tiles -> Test with all tile images -> Stitching -> Merge tile-level predictions -> Full Image Prediction .. note:: - While running `ote test` on models trained with tiling enabled, the evaluation will be performed on all tiles, this process includes merging all the tile-level prediction. + While running `ote eval` on models trained with tiling enabled, the evaluation will be performed on all tiles, this process includes merging all the tile-level prediction. The below context will be provided during evaluation: - .. code-block:: shell + .. code-block:: [>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] 650/650, 17.2 task/s, elapsed: 38s, ETA: 0s ==== merge: 7.326097726821899 sec ==== -Enable Tiling via OTX Training +Enable Tiling via OTX Training CLI ================================== Currently, tiling is supported for both detection and instance segmentation models. Please refer to :doc:`../algorithms/object_detection/object_detection` and :doc:`../algorithms/segmentation/instance_segmentation` for more details. -To enable tiling in OTX training, set ``data.config.tile_config.enable_tiler`` parameter to 1. Here's an example of enabling tiling: +To enable tiling in OTX training, set ``tiling_parameters.enable_tiling`` parameter to 1. Here's an example of enabling tiling for the SSD model template: -.. tab-set:: +.. code-block:: - .. tab-item:: API + otx train Custom_Object_Detection_Gen3_SSD --train-data-roots tests/assets/small_objects --val-data-roots tests/assets/small_objects params --tiling_parameters.enable_tiling 1 - .. code-block:: python +.. note:: - from otx.core.config.data import DataModuleConfig, TileConfig - from otx.core.data.module import OTXDataModule + To learn how to deploy the trained model and run the exported demo, refer to :doc:`../../tutorials/base/deploy`. - data_config = DataModuleConfig(..., tile_config=TileConfig(enable_tiler=True)) - datamodule = OTXDataModule(..., config=data_config) + To learn how to run the demo in CLI and visualize results, refer to :doc:`../../tutorials/base/demo`. - .. tab-item:: CLI +Enable Tiling via OTX Build +=========================== +Here's another way of enabling tiling for the SSD model template using the workspace: - .. code-block:: shell +.. code-block:: - (otx) ...$ otx train ... --data.config.tile_config.enable_tiler True + otx build Custom_Object_Detection_Gen3_SSD --train-data-roots tests/assets/small_objects --val-data-roots tests/assets/small_objects -.. note:: +The above command will create a workspace folder with the necessary files for training under ``otx-workspace-DETECTION``. - To learn how to deploy the trained model and run the exported demo, refer to :doc:`../../tutorials/base/deploy`. +You can then train the model with tiling enabled using the following command without specifying any data-related paths: - To learn how to run the demo in CLI and visualize results, refer to :doc:`../../tutorials/base/demo`. +.. code-block:: + cd otx-workspace-DETECTION + otx train params --tiling_parameters.enable_tiling 1 -Tile Size and Tile Overlap Optimization ------------------------------------------ -By default, the OpenVINO Training Extensions automatically optimize tile size and tile overlap to ensure efficient execution without compromising accuracy. - -To strike a balance between patch size and computational efficiency, the OpenVINO Training Extensions incorporate adaptive tiling parameter optimization. These features enable the proper tuning of tile size and other tiling-related parameters to ensure efficient execution without compromising accuracy. +Alternatively, you can update the ``tiling_parameters`` in ``configuration.yaml`` file under the workspace folder to configure tiling parameters: -Adaptive tiling parameter optimization works by finding the average object size in the training dataset and using that to determine the tile size. Currently, the average object size to tile size ratio is set to 3%. For example, if the average object size is 100x100 pixels, the tile size will be around 577x577 pixels. +.. code-block:: -This computation is performed by dividing the average object size by the desired object size ratio (default: 3%) and then taking the square root. This ensures that the objects are large enough to be detected by the model. The object size to tile size ratio can also be configured with ``tiling_parameters.object_tile_ratio`` parameter. + hyper_parameters: + parameter_overrides: + tiling_parameters: + enable_tiling: + default_value: true -Here's an example of setting the object size ratio to 5%: +And then train the model with tiling enabled using the following command: -.. tab-set:: +.. code-block:: - .. tab-item:: API + otx train - .. code-block:: python - from otx.core.config.data import DataModuleConfig, TileConfig - from otx.core.data.module import OTXDataModule +Tile Size and Tile Overlap Optimization +----------------------------------------- +By default, the OpenVINO Training Extensions automatically optimize tile size and tile overlap to ensure efficient execution without compromising accuracy. - tile_config = TileConfig(enable_tiler=True, enable_adaptive_tiling=True, object_tile_ratio=0.05) - data_config = DataModuleConfig(..., tile_config=tile_config) - datamodule = OTXDataModule(..., config=data_config) +To strike a balance between patch size and computational efficiency, the OpenVINO Training Extensions incorporate adaptive tiling parameter optimization. These features enable the proper tuning of tile size and other tiling-related parameters to ensure efficient execution without compromising accuracy. - .. tab-item:: CLI +Adaptive tiling parameter optimization works by finding the average object size in the training dataset and using that to determine the tile size. Currently, the average object size to tile size ratio is set to 3%. For example, if the average object size is 100x100 pixels, the tile size will be around 577x577 pixels. - .. code-block:: shell +This computation is performed by dividing the average object size by the desired object size ratio (default: 3%) and then taking the square root. This ensures that the objects are large enough to be detected by the model. The object size to tile size ratio can also be configured with ``tiling_parameters.object_tile_ratio`` parameter. - (otx) ...$ otx train ... --data.config.tile_config.enable_tiler True \ # enable tiling - --data.config.tile_config.enable_adaptive_tiling True \ # enable automatic tiling parameter optimization - --data.config.tile_config.object_tile_ratio 0.05 # set the object size ratio to 5% +Here's an example of setting the object size ratio to 5%: +.. code-block:: + + otx train Custom_Object_Detection_Gen3_SSD + --train-data-roots tests/assets/small_objects \ + --val-data-roots tests/assets/small_objects \ + params --tiling_parameters.enable_tiling 1 \ # enable tiling + --tiling_parameters.enable_adaptive_params 1 \ # enable automatic tiling parameter optimization + --tiling_parameters.object_tile_ratio 0.05 \ # set the object size ratio to 5% After determining the tile size, the tile overlap is computed by dividing the largest object size in the training dataset by the adaptive tile size. This calculation ensures that the largest object on the border of a tile is not split into two tiles and is covered by adjacent tiles. @@ -122,28 +128,16 @@ Since training and validation on all tiles from a high-resolution image dataset It's important to note that sampling is applied to the training and validation datasets, not the test dataset. -This can be configured with ``data.config.tile_config.enable_adaptive_tiling`` parameter. Here's an example: - -.. tab-set:: +This can be configured with ``tiling_parameters.tile_sampling_ratio`` parameter. Here's an example of setting the tile sampling ratio to 50%: - .. tab-item:: API - - .. code-block:: python - - from otx.core.config.data import DataModuleConfig, TileConfig - from otx.core.data.module import OTXDataModule - - tile_config = TileConfig(enable_tiler=True, enable_adaptive_tiling=True, sampling_ratio=0.5) - data_config = DataModuleConfig(..., tile_config=tile_config) - datamodule = OTXDataModule(..., config=data_config) - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx train ... --data.config.tile_config.enable_tiler True - --data.config.tile_config.enable_adaptive_tiling True - --data.config.tile_config.sampling_ratio 0.5 +.. code-block:: + + otx train Custom_Object_Detection_Gen3_SSD + --train-data-roots tests/assets/small_objects \ + --val-data-roots tests/assets/small_objects \ + params --tiling_parameters.enable_tiling 1 \ # enable tiling + --tiling_parameters.enable_adaptive_params 1 \ # enable automatic tiling parameter optimization + --tiling_parameters.tile_sampling_ratio 0.5 \ # set the tile sampling ratio to 50% Manual Tiling Parameter Configuration @@ -151,38 +145,26 @@ Manual Tiling Parameter Configuration Users can disable adaptive tiling and customize the tiling process by setting the following parameters: -.. tab-set:: - - .. tab-item:: API - - .. code-block:: python - - from otx.core.config.data import DataModuleConfig, TileConfig - from otx.core.data.module import OTXDataModule - - tile_config = TileConfig(enable_tiler=True, enable_adaptive_tiling=False, tile_size=(512,512), tile_overlap=0.2) - data_config = DataModuleConfig(..., tile_config=tile_config) - datamodule = OTXDataModule(..., config=data_config) - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx train ... --data.config.tile_config.enable_tiler True - --data.config.tile_config.enable_adaptive_tiling False - --data.config.tile_config.tile_size '[512,512]' - --data.config.tile_config.tile_overlap 0.2 +.. code-block:: + + otx train Custom_Object_Detection_Gen3_SSD + --train-data-roots tests/assets/small_objects \ + --val-data-roots tests/assets/small_objects \ + params --tiling_parameters.enable_tiling 1 \ # enable tiling + --tiling_parameters.enable_adaptive_params 0 \ # disable automatic tiling parameter optimization + --tiling_parameters.tile_size 512 \ # tile size configured to 512x512 + --tiling_parameters.tile_overlap 0.1 \ # 10% overlap between tiles By specifying these parameters, automatic tiling parameter optimization is disabled, and the tile size is configured to 512x512 pixels with a 10% overlap between tiles. The following parameters can be configured to customize the tiling process: -- ``tile_config.enable_tiling``: Enable or disable tiling (0 or 1) -- ``tile_config.enable_adaptive_params``: Enable or disable adaptive tiling parameter optimization (0 or 1) -- ``tile_config.object_tile_ratio``: Ratio of average object size to tile size (float between 0.0 and 1.0) -- ``tile_config.tile_size``: Tile edge length in pixels (integer between 100 and 4096) -- ``tile_config.overlap``: The overlap between adjacent tiles as a percentage (float between 0.0 and 1.0) -- ``tile_config.sampling_ratio``: The percentage of tiles to sample from the dataset (float between 0.0 and 1.0) +- ``tiling_parameters.enable_tiling``: Enable or disable tiling (0 or 1) +- ``tiling_parameters.enable_adaptive_params``: Enable or disable adaptive tiling parameter optimization (0 or 1) +- ``tiling_parameters.object_tile_ratio``: Ratio of average object size to tile size (float between 0.0 and 1.0) +- ``tiling_parameters.tile_size``: Tile edge length in pixels (integer between 100 and 4096) +- ``tiling_parameters.tile_overlap``: The overlap between adjacent tiles as a percentage (float between 0.0 and 1.0) +- ``tiling_parameters.tile_sampling_ratio``: The percentage of tiles to sample from the dataset (float between 0.0 and 1.0) Run Tiling on OpenVINO Exported Model @@ -190,38 +172,19 @@ Run Tiling on OpenVINO Exported Model After training a model with tiling enabled, you can export the model to OpenVINO IR format using the following command: -.. tab-set:: - - .. tab-item:: API - - .. code-block:: python - - engine.export(checkpoint="") - - .. tab-item:: CLI +.. code-block:: - .. code-block:: shell + otx export Custom_Object_Detection_Gen3_SSD --load-weights /weights.pth --output - (otx) ...$ otx export ... --checkpoint After exporting the model, you can run inference on the exported model using the following command: -.. tab-set:: - - .. tab-item:: API - - .. code-block:: python - - engine.test(checkpoint="") - - .. tab-item:: CLI - - .. code-block:: shell +.. code-block:: - (otx) ...$ otx test ... --checkpoint + ote eval Custom_Object_Detection_Gen3_SSD --test-data-roots tests/assets/small_objects --load-weights /openvino.xml .. warning:: When tiling is enabled, there is a trade-off between speed and accuracy as it increases the number of images to be processed. As a result, longer training and inference times are expected. If you encounter GPU out of memory errors, you can mitigate the issue by reducing the number of batches through the command-line interface (CLI) or - by adjusting the batch size value. + by adjusting the batch size value in ``template.yaml`` file located in the workspace. \ No newline at end of file diff --git a/docs/source/guide/explanation/additional_features/xai.rst b/docs/source/guide/explanation/additional_features/xai.rst index ad3f7a00cfe..3c91c2c71e1 100644 --- a/docs/source/guide/explanation/additional_features/xai.rst +++ b/docs/source/guide/explanation/additional_features/xai.rst @@ -93,18 +93,3 @@ Below we show the comparison of described algorithms. ``Access to the model inte +-------------------------------------------+----------------------------+--------------------------------------------+ | Execution speed | Fast | Fast | +-------------------------------------------+----------------------------+--------------------------------------------+ - - -.. tab-set:: - - .. tab-item:: API - - .. code-block:: python - - engine.explain(checkpoint="") - - .. tab-item:: CLI - - .. code-block:: bash - - (otx) ...$ otx explain ... --checkpoint diff --git a/docs/source/guide/explanation/algorithms/action/action_classification.rst b/docs/source/guide/explanation/algorithms/action/action_classification.rst index f6626de07a0..be04c27e2b7 100644 --- a/docs/source/guide/explanation/algorithms/action/action_classification.rst +++ b/docs/source/guide/explanation/algorithms/action/action_classification.rst @@ -32,16 +32,11 @@ Currently OpenVINO™ Training Extensions supports `X3D `_ | X3D | 2.49 | 3.79 | +| `Custom_Action_Classification_X3D `_ | X3D | 2.49 | 3.79 | +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------------------+-------------------------+ -| `Custom_Action_Classificaiton_MoViNet `_ | MoViNet | 2.71 | 3.10 | +| `Custom_Action_Classificaiton_MoViNet `_ | MoViNet | 2.71 | 3.10 | +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------------------+-------------------------+ -To see which models are available for the task, the following command can be executed: - -.. code-block:: shell - - (otx) ...$ otx find --task ACTION_CLASSIFICATION In the table below the **top-1 accuracy** on some academic datasets are presented. Each model is trained with single NVIDIA GeForce RTX 3090. diff --git a/docs/source/guide/explanation/algorithms/action/action_detection.rst b/docs/source/guide/explanation/algorithms/action/action_detection.rst index 726acbd5e5a..faf07cdc128 100644 --- a/docs/source/guide/explanation/algorithms/action/action_detection.rst +++ b/docs/source/guide/explanation/algorithms/action/action_detection.rst @@ -20,6 +20,13 @@ Dataset Format For the dataset handling inside OpenVINO™ Training Extensions, we use `Dataset Management Framework (Datumaro) `_. Since current Datumaro does not support `AVA dataset `_ format, therefore conversion to `CVAT dataset format `_ is needed. Currently, we offer conversion code from the AVA dataset format to the CVAT dataset format. Please refer `this script `_ +If you have your dataset in those formats, then you can simply run using one line of code: + +.. code-block:: + + $ otx train src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/template.yaml \ + --train-data-roots \ + --val-data-roots ****** Models @@ -30,15 +37,9 @@ We support the following ready-to-use model templates for transfer learning: +-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+---------------------+-------------------------+ | Template ID | Name | Complexity (GFLOPs) | Model size (MB) | +=========================================================================================================================================================================================+===============+=====================+=========================+ -| `Custom_Action_Detection_X3D_FAST_RCNN `_ | x3d_fast_rcnn | 13.04 | 8.32 | +| `Custom_Action_Detection_X3D_FAST_RCNN `_ | x3d_fast_rcnn | 13.04 | 8.32 | +-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+---------------------+-------------------------+ -To see which models are available for the task, the following command can be executed: - -.. code-block:: shell - - (otx) ...$ otx find --task ACTION_DETECTION - In the table below the **mAP** on some academic datasets are presented. Each model is trained using `Kinetics-400 `_ pre-trained weight with single Nvidia GeForce RTX3090. +----------------+-------+-----------+ diff --git a/docs/source/guide/explanation/algorithms/anomaly/index.rst b/docs/source/guide/explanation/algorithms/anomaly/index.rst index 74aa80b6909..806b235a668 100644 --- a/docs/source/guide/explanation/algorithms/anomaly/index.rst +++ b/docs/source/guide/explanation/algorithms/anomaly/index.rst @@ -77,13 +77,15 @@ Models ****** As mentioned above, the goal of visual anomaly detection is to learn a representation of normal behaviour in the data and then identify instances that deviate from this normal behaviour. OpenVINO Training Extensions supports several deep learning approaches to this task, including the following: -+-------+----------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+---------------------+-----------------+ -| Name | Classification | Detection | Segmentation | Complexity (GFLOPs) | Model size (MB) | -+=======+==============================================================================================================================================+==================================================================================================================================================+============================================================================================================================================+=====================+=================+ -| PADIM | `padim `_ | `padim `_ | `padim `_ | 3.9 | 168.4 | -+-------+----------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+---------------------+-----------------+ -| STFPM | `stfpm `_ | `stfpm `_ | `stfpm `_ | 5.6 | 21.1 | -+-------+----------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+---------------------+-----------------+ ++-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+-----------------+ +| Name | Template ID (Classification) | Template ID (Detection) | Template ID (Segmentation) | Complexity (GFLOPs) | Model size (MB) | ++=======+==============================================================================================================================================================================================+=============================================================================================================================================================================================+==========================================================================================================================================================================================+=====================+=================+ +| PADIM | `ote_anomaly_classification_padim `_ | `ote_anomaly_detection_padim `_ | `ote_anomaly_segmentation_padim `_ | 3.9 | 168.4 | ++-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+-----------------+ +| STFPM | `ote_anomaly_classification_stfpm `_ | `ote_anomaly_detection_stfpm `_ | `ote_anomaly_segmentation_stfpm `_ | 5.6 | 21.1 | ++-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+-----------------+ +| DRAEM | `ote_anomaly_classification_draem `_ | `ote_anomaly_detection_draem `_ | `ote_anomaly_segmentation_draem `_ | N/A | 1183.3 | ++-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+-----------------+ Clustering-based Models @@ -163,4 +165,4 @@ Training Parameters - ``Optimizer``: Both the reconstructive subnetwork and the discriminative subnetwork are trained using the Adam optimizer. - ``Loss``: The reconstructive subnetwork is trained using reconstruction loss which consists of a combination of L2 loss and Structural Similarity (SSIM) loss between the reconstructions and the original images. The discriminative subnetwork is trained using focal loss, computed between the pixel-level predictions and the ground truth masks of the augmented images. - ``Additional Training Techniques``: - - ``Early Stopping``: Early stopping is used to prevent overfitting. The early stopping patience can be configured by the user. By default, early stopping is enabled with a patience of 20 epochs. + - ``Early Stopping``: Early stopping is used to prevent overfitting. The early stopping patience can be configured by the user. By default, early stopping is enabled with a patience of 20 epochs. \ No newline at end of file diff --git a/docs/source/guide/explanation/algorithms/classification/hierarhical_classification.rst b/docs/source/guide/explanation/algorithms/classification/hierarhical_classification.rst index f6c50a2297a..ca1267f7bb3 100644 --- a/docs/source/guide/explanation/algorithms/classification/hierarhical_classification.rst +++ b/docs/source/guide/explanation/algorithms/classification/hierarhical_classification.rst @@ -41,6 +41,17 @@ Dataset Format For hierarchical image classification, we created our custom dataset format that is supported by `Datumaro `_. An example of the annotations format and dataset structure can be found in our `sample `_. +To use OpenVINO™ Training Extensions with this format, it is required to pass dataset root paths directly to the CLI command: + +.. code-block:: + + $ otx {train, optimize} --template \ + --train-data-roots \ + --val-data-roots + $ otx eval --template \ + --test-data-roots \ + --load-weights + .. note:: Please, refer to our :doc:`dedicated tutorial <../../../tutorials/base/how_to_train/classification>` for more information how to train, validate and optimize classification models. @@ -51,12 +62,6 @@ Models We use the same model templates as for Multi-class Classification. Please, refer: :ref:`Classification Models `. -To see which models are available for the task, the following command can be executed: - -.. code-block:: shell - - (otx) ...$ otx find --task H_LABEL_CLS - .. ******************** .. Incremental Learning .. ******************** diff --git a/docs/source/guide/explanation/algorithms/classification/multi_class_classification.rst b/docs/source/guide/explanation/algorithms/classification/multi_class_classification.rst index 93ec1fbce23..76fe3683d6e 100644 --- a/docs/source/guide/explanation/algorithms/classification/multi_class_classification.rst +++ b/docs/source/guide/explanation/algorithms/classification/multi_class_classification.rst @@ -16,7 +16,14 @@ For the supervised training we use the following algorithms components: - ``Additional training techniques`` - `No Bias Decay (NBD) `_: To add adaptability to the training pipeline and prevent overfitting. - - ``Early stopping``: To add adaptability to the training pipeline and prevent overfitting. + - ``Early stopping``: To add adaptability to the training pipeline and prevent overfitting. You can use early stopping like the below command. + + .. code-block:: + + $ otx train {TEMPLATE} ... \ + params \ + --learning_parameters.enable_early_stopping=True + - `Balanced Sampler `_: To create an efficient batch that consists of balanced samples over classes, reducing the iteration size as well. ************** @@ -63,21 +70,24 @@ We support the following ready-to-use model templates: +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------+---------------------+-----------------+ | Template ID | Name | Complexity (GFLOPs) | Model size (MB) | +==================================================================================================================================================================================================================+=======================+=====================+=================+ -| `Custom_Image_Classification_MobileNet-V3-large-1x `_ | MobileNet-V3-large-1x | 0.44 | 4.29 | +| `Custom_Image_Classification_MobileNet-V3-large-1x `_ | MobileNet-V3-large-1x | 0.44 | 4.29 | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------+---------------------+-----------------+ -| `Custom_Image_Classification_EfficinetNet-B0 `_ | EfficientNet-B0 | 0.81 | 4.09 | +| `Custom_Image_Classification_EfficinetNet-B0 `_ | EfficientNet-B0 | 0.81 | 4.09 | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------+---------------------+-----------------+ -| `Custom_Image_Classification_EfficientNet-V2-S `_ | EfficientNet-V2-S | 5.76 | 20.23 | +| `Custom_Image_Classification_EfficientNet-V2-S `_ | EfficientNet-V2-S | 5.76 | 20.23 | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------+---------------------+-----------------+ `EfficientNet-V2-S `_ has more parameters and Flops and needs more time to train, meanwhile providing superior classification performance. `MobileNet-V3-large-1x `_ is the best choice when training time and computational cost are in priority, nevertheless, this template provides competitive accuracy as well. `EfficientNet-B0 `_ consumes more Flops compared to MobileNet, providing better performance on large datasets, but may be not so stable in case of a small amount of training data. -To see which models are available for the task, the following command can be executed: +Besides this, we support public backbones from `torchvision `_, `pytorchcv `_, `mmcls `_ and `OpenVino Model Zoo `_. +Please, refer to the :doc:`tutorial <../../../tutorials/advanced/backbones>` how to customize models and run public backbones. + +To see which public backbones are available for the task, the following command can be executed: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx find --task MULTI_CLASS_CLS + $ otx find --backbone {torchvision, pytorchcv, mmcls, omz.mmcls} In the table below the top-1 accuracy on some academic datasets using our :ref:`supervised pipeline ` is presented. The results were obtained on our templates without any changes. We use 224x224 image resolution, for other hyperparameters, please, refer to the related template. We trained each model with single Nvidia GeForce RTX3090. @@ -90,5 +100,162 @@ In the table below the top-1 accuracy on some academic datasets using our :ref:` +-----------------------+-----------------+-----------+-----------+-----------+ | EfficientNet-V2-S | 96.13 | 90.36 | 97.68 | 86.74 | +-----------------------+-----------------+-----------+-----------+-----------+ - \* These datasets were splitted with auto-split (80% train, 20% test). + +************************ +Semi-supervised Learning +************************ + +Semi-SL (Semi-supervised Learning) is a type of machine learning algorithm that uses both labeled and unlabeled data to improve the performance of the model. This is particularly useful when labeled data is limited, expensive or time-consuming to obtain. + +We use `FixMatch `_ as a core algorithm for Semi-SL task solving. It is a specific implementation of Semi-SL that has been shown to be effective in various applications. FixMatch introduces pseudo-labeling, which is the process of generating labels for the unlabeled data and treating them as if they were labeled data. Pseudo-labeling is based on the idea that the model's prediction for the unlabeled data is likely to be correct, which can improve the model's accuracy and reduce the need for labeled data. + +In Semi-SL, the pseudo-labeling process is combined with a consistency loss that ensures that the predictions of the model are consistent across augmented versions of the same data. This helps to reduce the impact of noisy or incorrect labels that may arise from the pseudo-labeling process. Additionally, our algorithm uses a combination of strong data augmentations and a specific optimizer called Sharpness-Aware Minimization (SAM) to further improve the accuracy of the model. + +Overall, OpenVINO™ Training Extensions utilizes powerful techniques for improving the performance of Semi-SL algorithm with limited labeled data. They can be particularly useful in domains where labeled data is expensive or difficult to obtain, and can help to reduce the time and cost associated with collecting labeled data. + +.. _mcl_cls_semi_supervised_pipeline: + +- ``Pseudo-labeling (FixMatch)``: A specific implementation of Semi-SL that combines the use of pseudo-labeling with a consistency loss, strong data augmentations, and a specific optimizer called Sharpness-Aware Minimization (SAM) to improve the performance of the model. + +- ``Adaptable Threshold``: A novel addition to our solution that calculates a class-wise threshold for pseudo-labeling, which can solve the issue of imbalanced data and produce high-quality pseudo-labels that improve the overall score. + +- ``Unlabeled Warm-Up Loss``: A technique for preventing the initial unstable learning of pseudo-labeling by increasing the coefficient of the unlabeled loss from 0 to 1. + +- ``Exponential Moving Average (EMA)``: A technique for maintaining a moving average of the model's parameters, which can improve the generalization performance of the model. + +- ``Additional techniques``: Other than that, we use several solutions that apply to supervised learning (No bias Decay, Augmentations, Early-Stopping, etc.) + +Please, refer to the :doc:`tutorial <../../../tutorials/advanced/semi_sl>` on how to train semi-supervised learning. +Training time depends on the number of images and can be up to several times longer than conventional supervised learning. + +In the table below the top-1 accuracy on some academic datasets using our pipeline is presented. Same as the supervised setting except for an image for unlabeled and additional batch size. + +- 4 labeled images per class including unlabeled dataset for Semi-SL + ++-----------------------+---------+---------+-------+---------+--------+---------+ +| Dataset | CIFAR10 | | SVHN | | FMNIST | | ++=======================+=========+=========+=======+=========+========+=========+ +| | SL | Semi-SL | SL | Semi-SL | SL | Semi-SL | ++-----------------------+---------+---------+-------+---------+--------+---------+ +| MobileNet-V3-large-1x | 40.75 | 43.13 | 23.32 | 27.85 | 68.2 | 71.84 | ++-----------------------+---------+---------+-------+---------+--------+---------+ +| EfficientNet-B0 | 42.24 | 44.23 | 28.09 | 32.96 | 68.58 | 70.79 | ++-----------------------+---------+---------+-------+---------+--------+---------+ +| EfficientNet-V2-S | 36.03 | 39.66 | 16.81 | 20.28 | 65.99 | 69.61 | ++-----------------------+---------+---------+-------+---------+--------+---------+ + + +- 10 labeled images per class including unlabeled dataset for Semi-SL + ++-----------------------+---------+---------+-------+---------+--------+---------+ +| Dataset | CIFAR10 | | SVHN | | FMNIST | | ++=======================+=========+=========+=======+=========+========+=========+ +| | SL | Semi-SL | SL | Semi-SL | SL | Semi-SL | ++-----------------------+---------+---------+-------+---------+--------+---------+ +| MobileNet-V3-large-1x | 50.77 | 52.16 | 38.73 | 48.36 | 73.33 | 77.04 | ++-----------------------+---------+---------+-------+---------+--------+---------+ +| EfficientNet-B0 | 52.69 | 58.35 | 46.04 | 61.79 | 74.56 | 80.14 | ++-----------------------+---------+---------+-------+---------+--------+---------+ +| EfficientNet-V2-S | 48.84 | 55 | 26.16 | 47.99 | 74.6 | 80.92 | ++-----------------------+---------+---------+-------+---------+--------+---------+ + +.. note:: + This result can vary greatly depending on the image selected for each class. Also, since there are few labeled settings for the Semi-SL algorithm. Some models may require larger datasets for better results. + +************************ +Self-supervised Learning +************************ +.. _selfsl_multi_class_classification: + +Self-supervised learning can be one of the solutions if the user has a small data set, but label information is not yet available. +General self-supervised Learning in academia is commonly used to obtain well-pretrained weights from a source dataset without label information. +However, in real-world industries, it is difficult to apply because of small datasets, limited resources, or training in minutes. + +For these cases, OpenVINO™ Training Extensions provides improved self-supervised learning recipes that can be applied to the above harsh environments. +We adapted `BYOL `_ as our self-supervised method. +This algorithm will require some additional training time, meanwhile, improved performance is expected, especially in low-data regimes. +OpenVINO™ Training Extensions allows to perform a pre-training phase on any images to further use obtained weights on the target dataset. + +Below is graphs of performance improvement for three baseline datasets: CIFAR10, CIFAR100, and Food-101. +The graphs below show how much performance improvement over baseline was achieved using our self-supervised learning recipes. +We created subset datasets by sampling images to check performance from small to large datasets. +In particular, the smaller the data, the greater the performance improvement can be expected. +To get the below performance, we had two steps: + +- Train the models using only images without label information to get pretrained weights for a few epochs. +- Fine-tune the models with pretrained weights using subset datasets and get performance. + +We additionally obtained baseline performance from supervised learning using subset datasets for comparison. +Each subset dataset has 500, 1000, 5000, 10000, and the whole images, respectively. + +.. image:: ../../../../../utils/images/multi_cls_selfsl_performance_CIFAR10.png + :width: 600 + +.. image:: ../../../../../utils/images/multi_cls_selfsl_performance_CIFAR100.png + :width: 600 + +.. image:: ../../../../../utils/images/multi_cls_selfsl_performance_Food-101.png + :width: 600 + +To enable self-supervised training, the command below can be executed. The folder with images for pre-training is needed to be passed in ``--train-data-root`` folder. +Unlike other tasks, ``--val-data-root`` is not needed. + +.. code-block:: + + $ otx train EfficientNet-V2-S \ + --train-data-root path/to/folder/with/images + +.. note:: + It is also possible to pass a full imagenet dataset format to ``--train-data-root`` instead of just a folder with images. + However, it will be required to add ``--train-type Selfsupervised`` option into the command line. Otherwise, ordinary supervised training will be started with auto-split functionality. + +After self-supervised training, pretrained weights can be use for supervised (incremental) learning like the below command: + +.. code-block:: + + $ otx train EfficientNet-V2-S \ + --train-data-roots path/to/train/subset \ + --val-data-roots path/to/val/subset \ + --load-weights={PATH/PRETRAINED/WEIGHTS} + +******************************* +Supervised Contrastive Learning +******************************* + +To enhance the performance of the algorithm in the case when we have a small number of data, `Supervised Contrastive Learning (SupCon) `_ can be used. +More specifically, we train a model with two heads: classification head with Influence-Balanced Loss and contrastive head with `Barlow Twins loss `_. +The below table shows how much performance SupCon improved compared with baseline performance on three baseline datasets with 10 samples per class: CIFAR10, Eurosat-10, and Food-101. + ++-----------------------+---------+---------+------------+---------+----------+---------+ +| Model name | CIFAR10 | | Eurosat-10 | | Food-101 | | ++=======================+=========+=========+============+=========+==========+=========+ +| | SL | SupCon | SL | SupCon | SL | SupCon | ++-----------------------+---------+---------+------------+---------+----------+---------+ +| MobileNet-V3-large-1x | 55.06 | 58.88 | 77.60 | 78.70 | 34.83 | 34.38 | ++-----------------------+---------+---------+------------+---------+----------+---------+ +| EfficientNet-B0 | 42.81 | 46.35 | 66.87 | 70.23 | 37.26 | 39.17 | ++-----------------------+---------+---------+------------+---------+----------+---------+ +| EfficientNet-V2-S | 59.78 | 63.13 | 81.84 | 83.12 | 51.32 | 54.84 | ++-----------------------+---------+---------+------------+---------+----------+---------+ + +The SupCon training can be launched by adding additional option to template parameters like the below. +It can be launched only with supervised (incremental) training type. + +.. code-block:: + + $ otx train src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/template.yaml \ + --train-data-roots=tests/assets/imagenet_dataset_class_incremental \ + --val-data-roots=tests/assets/imagenet_dataset_class_incremental \ + params \ + --learning_parameters.enable_supcon=True + +.. note:: + SL stands for Supervised Learning. + + +.. ******************** +.. Incremental Learning +.. ******************** + +.. To be added soon \ No newline at end of file diff --git a/docs/source/guide/explanation/algorithms/classification/multi_label_classification.rst b/docs/source/guide/explanation/algorithms/classification/multi_label_classification.rst index 47f651492ad..eed5090426e 100644 --- a/docs/source/guide/explanation/algorithms/classification/multi_label_classification.rst +++ b/docs/source/guide/explanation/algorithms/classification/multi_label_classification.rst @@ -24,7 +24,12 @@ Dataset Format ************** As it is a common practice to use object detection datasets in the academic area, we support the most popular object detection format: `COCO `_. -Specifically, this format should be converted in our `internal representation `_. +Specifically, this format should be converted in our `internal representation `_ first. We provided a `script ` to help with conversion. +To convert the COCO data format to our internal one, run this script in similar way: + +.. code-block:: + + python convert_coco_to_multilabel.py --ann_file_path --data_root_dir --output .. note:: Names of the annotations files and overall dataset structure should be the same as the original `COCO `_. You need to convert train and validation sets separately. @@ -39,12 +44,6 @@ Models ****** We use the same models as for Multi-class classification. Please, refer: :ref:`Classification Models `. -To see which models are available for the task, the following command can be executed: - -.. code-block:: shell - - (otx) ...$ otx find --task MULTI_LABEL_CLS - In the table below the `mAP `_ metrics on some academic datasets using our :ref:`supervised pipeline ` are presented. The results were obtained on our templates without any changes (including input resolution, which is 224x224 for all templates). We trained each model with single Nvidia GeForce RTX3090. +-----------------------+-----------------+-----------+------------------+-----------+ @@ -56,3 +55,66 @@ In the table below the `mAP `_ as an auxiliary loss for Semi-SL task solving. BarlowTwins enforces consistency across augmented versions of the same data (both labeled and unlabeled): each sample is augmented first with `Augmix `_, then strongly augmented sample is generated by applying a pre-defined `RandAugment `_ strategy on top of the basic augmentation. + +.. _mlc_cls_semi_supervised_pipeline: + +- ``BarlowTwins loss``: A specific implementation of Semi-SL that combines the use of a consistency loss with strong data augmentations, and a specific optimizer called Sharpness-Aware Minimization (`SAM `_) to improve the performance of the model. + +- ``Adaptive loss auxiliary loss weighting``: A technique for assigning such a weight for an auxiliary loss that the resulting value is a predefined fraction of the EMA-smoothed main loss value. This method allows aligning contribution of the losses during different training phases. + +- ``Exponential Moving Average (EMA)``: A technique for maintaining a moving average of the model's parameters, which can improve the generalization performance of the model. + +- ``Additional techniques``: Other than that, we use several solutions that apply to supervised learning (No bias Decay, Augmentations, Early-Stopping, etc.) + +Please, refer to the :doc:`tutorial <../../../tutorials/advanced/semi_sl>` on how to train semi-supervised learning. +Training time depends on the number of images and can be up to several times longer than conventional supervised learning. + +In the table below the mAP metric on some public datasets using our pipeline is presented. + ++-----------------------+---------+----------------------+----------------+---------+----------------+---------+ +| Dataset | AerialMaritime 3 cls | | VOC 2007 3 cls | | COCO 14 5 cls | | ++=======================+======================+=========+================+=========+================+=========+ +| | SL | Semi-SL | SL | Semi-SL | SL | Semi-SL | ++-----------------------+----------------------+---------+----------------+---------+----------------+---------+ +| MobileNet-V3-large-1x | 74.28 | 74.41 | 96.34 | 97.29 | 82.39 | 83.77 | ++-----------------------+----------------------+---------+----------------+---------+----------------+---------+ +| EfficientNet-B0 | 79.59 | 80.91 | 97.75 | 98.59 | 83.24 | 84.19 | ++-----------------------+----------------------+---------+----------------+---------+----------------+---------+ +| EfficientNet-V2-S | 75.91 | 81.91 | 95.65 | 96.43 | 85.19 | 84.24 | ++-----------------------+----------------------+---------+----------------+---------+----------------+---------+ + +AerialMaritime was sampled with 5 images per class. VOC was sampled with 10 images per class and COCO was sampled with 20 images per class. +Additionel information abount the datasets can be found in the table below. + ++-----------------------+----------------+----------------------+ +| Dataset | Labeled images | Unlabeled images | ++=======================+================+======================+ +| AerialMaritime 3 cls | 10 | 42 | ++-----------------------+----------------+----------------------+ +| VOC 2007 3 cls | 30 | 798 | ++-----------------------+----------------+----------------------+ +| COCO 14 5 cls | 95 | 10142 | ++-----------------------+----------------+----------------------+ + +.. note:: + This result can vary depending on the image selected for each class. Also, since there are few labeled settings for the Semi-SL algorithm. Some models may require larger datasets for better results. + +.. ************************ +.. Self-supervised Learning +.. ************************ + +.. To be added soon + +.. ******************** +.. Incremental Learning +.. ******************** + +.. To be added soon \ No newline at end of file diff --git a/docs/source/guide/explanation/algorithms/index.rst b/docs/source/guide/explanation/algorithms/index.rst index 6092f6a355e..03351d37705 100644 --- a/docs/source/guide/explanation/algorithms/index.rst +++ b/docs/source/guide/explanation/algorithms/index.rst @@ -12,6 +12,12 @@ To this end, we support: - **Incremental learning**. This learning approach lets the model train on new data as it becomes available, rather than retraining the entire model on the whole dataset every time new data is added. OpenVINO™ Training Extensions supports also the class incremental approach for all tasks. In this approach, the model is first trained on a set of classes, and then incrementally updated with new classes of data, while keeping the previously learned classes' knowledge. The class incremental approach is particularly useful in situations where the number of classes is not fixed and new classes may be added over time. +.. _semi_sl_explanation: + +- **Semi-supervised learning**. This is a type of machine learning in which the model is trained on a dataset that contains a combination of labeled and unlabeled examples. The labeled examples are used to train the model, while the unlabeled examples are used to improve the model's performance by providing additional information about the underlying distribution of the data. This approach is often used when there is a limited amount of labeled data available, but a large amount of unlabeled data. This can make it more cost-effective and efficient to train models compared to traditional supervised learning, where the model is trained only on labeled data. + +- **Self-supervised learning**. This is a type of machine learning where the model is trained on a dataset that contains only unlabeled examples. The model is trained to learn useful representations of the data by solving a task that can be inferred from the input itself, without human-provided labels. The objective is to learn good representations of the input data that can then be used for downstream tasks such as classification, detection, generation or clustering. + ******** Contents diff --git a/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst b/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst index 64e926b8928..e8022cc9ea1 100644 --- a/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst +++ b/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst @@ -21,8 +21,16 @@ For the supervised training we use the following algorithms components: - ``Loss function``: We use `Generalized IoU Loss `_ for localization loss to train the ability of the model to find the coordinates of the objects. For the classification head, we use a standard `FocalLoss `_. - ``Additional training techniques`` - - ``Early stopping``: To add adaptability to the training pipeline and prevent overfitting. + - ``Early stopping``: To add adaptability to the training pipeline and prevent overfitting. You can use early stopping like the below command. + + .. code-block:: + + $ otx train {TEMPLATE} ... \ + params \ + --learning_parameters.enable_early_stopping=True + - `Anchor clustering for SSD `_: This model highly relies on predefined anchor boxes hyperparameter that impacts the size of objects, which can be detected. So before training, we collect object statistics within dataset, cluster them and modify anchor boxes sizes to fit the most for objects the model is going to detect. + - ``Backbone pretraining``: we pretrained MobileNetV2 backbone on large `ImageNet21k `_ dataset to improve feature extractor and learn better and faster. @@ -46,6 +54,13 @@ Learn more about the formats by following the links above. Here is an example of ├── val └── test +If you have your dataset in those formats, then you can simply run using one line of code: + +.. code-block:: + + $ otx train --train-data-roots \ + --val-data-roots + .. note:: Please, refer to our :doc:`dedicated tutorial <../../../tutorials/base/how_to_train/detection>` for more information how to train, validate and optimize detection models. @@ -56,29 +71,29 @@ Models We support the following ready-to-use model templates: -+------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ -| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | -+============================================================================================================================================================+=====================+=====================+=================+ -| `Custom_Object_Detection_YOLOX `_ | YOLOX-TINY | 6.5 | 20.4 | -+------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ -| `Object_Detection_YOLOX_S `_ | YOLOX_S | 33.51 | 46.0 | -+------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ -| `Object_Detection_YOLOX_L `_ | YOLOX_L | 194.57 | 207.0 | -+------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ -| `Object_Detection_YOLOX_X `_ | YOLOX_X | 352.42 | 378.0 | -+------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ -| `Custom_Object_Detection_Gen3_SSD `_ | SSD | 9.4 | 7.6 | -+------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ -| `Custom_Object_Detection_Gen3_ATSS `_ | MobileNetV2-ATSS | 20.6 | 9.1 | -+------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ -| `Object_Detection_ResNeXt101_ATSS `_ | ResNeXt101-ATSS | 434.75 | 344.0 | -+------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | ++===========================================================================================================================================================================================+=====================+=====================+=================+ +| `Custom_Object_Detection_YOLOX `_ | YOLOX-TINY | 6.5 | 20.4 | ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Object_Detection_YOLOX_S `_ | YOLOX_S | 33.51 | 46.0 | ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Object_Detection_YOLOX_L `_ | YOLOX_L | 194.57 | 207.0 | ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Object_Detection_YOLOX_X `_ | YOLOX_X | 352.42 | 378.0 | ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Custom_Object_Detection_Gen3_SSD `_ | SSD | 9.4 | 7.6 | ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Custom_Object_Detection_Gen3_ATSS `_ | MobileNetV2-ATSS | 20.6 | 9.1 | ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Object_Detection_ResNeXt101_ATSS `_ | ResNeXt101-ATSS | 434.75 | 344.0 | ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ Above table can be found using the following command -.. code-block:: shell +.. code-block:: - (otx) ...$ otx find --task DETECTION + $ otx find --task detection `MobileNetV2-ATSS `_ is a good medium-range model that works well and fast in most cases. `SSD `_ and `YOLOX `_ are light models, that a perfect for the fastest inference on low-power hardware. @@ -87,6 +102,55 @@ So if you have resources for a long training, you can pick the YOLOX model. ATSS still shows good performance among `RetinaNet `_ based models. Therfore, We added ATSS with large scale backbone, ResNeXt101-ATSS. We integrated large ResNeXt101 backbone to our Custom ATSS head, and it shows good transfer learning performance. In addition, we added a YOLOX variants to support users' diverse situations. +In addition to these models, we supports experimental models for object detection. These experimental models will be changed to official models within a few releases. + ++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | ++===========================================================================================================================================================================================================================+=====================+=====================+=================+ +| `Object_Detection_Deformable_DETR `_ | Deformable_DETR | 165 | 157.0 | ++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Object_Detection_DINO `_ | DINO | 235 | 182.0 | ++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Object_Detection_Lite_DINO `_ | Lite-DINO | 140 | 190.0 | ++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ + + +`Deformable_DETR `_ is `DETR `_ based model, and it solves slow convergence problem of DETR. `DINO `_ improves Deformable DETR based methods via denoising anchor boxes. Current SOTA models for object detection are based on DINO. +`Lite-DINO `_ is efficient structure for DINO. It reduces FLOPS of transformer's encoder which takes the highest computational costs. + +.. note:: + + For using experimental templates, you should specify full path of experimental template. Ex) otx build src/otx/algorithms/detection/configs/detection/resnet50_dino/template_experimental.yaml --task detection + +In addition to these models, we supports experimental models for object detection. These experimental models will be changed to official models within a few releases. + ++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | ++===========================================================================================================================================================================================================================+=====================+=====================+=================+ +| `Custom_Object_Detection_Gen3_Deformable_DETR `_ | Deformable_DETR | 165 | 157.0 | ++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Custom_Object_Detection_Gen3_DINO `_ | DINO | 235 | 182.0 | ++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Custom_Object_Detection_Gen3_ResNeXt101_ATSS `_ | ResNeXt101-ATSS | 434.75 | 344.0 | ++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ + +`Deformable_DETR `_ is `DETR `_ based model, and it solves slow convergence problem of DETR. `DINO `_ improves Deformable DETR based methods via denoising anchor boxes. Current SOTA models for object detection are based on DINO. +Although transformer based models show notable performance on various object detection benchmark, CNN based model still show good performance with proper latency. +Therefore, we added a new experimental CNN based method, ResNeXt101-ATSS. ATSS still shows good performance among `RetinaNet `_ based models. We integrated large ResNeXt101 backbone to our Custom ATSS head, and it shows good transfer learning performance. + +.. note:: + + For using experimental templates, you should specify full path of experimental template. Ex) otx build src/otx/algorithms/detection/configs/detection/resnet50_dino/template_experimental.yaml --task detection + +Besides this, we support public backbones from `torchvision `_, `pytorchcv `_, `mmcls `_ and `OpenVino Model Zoo `_. +Please, refer to the :doc:`tutorial <../../../tutorials/advanced/backbones>` how to customize models and run public backbones. + +To see which public backbones are available for the task, the following command can be executed: + +.. code-block:: + + $ otx find --backbone {torchvision, pytorchcv, mmcls, omz.mmcls} + In the table below the test mAP on some academic datasets using our :ref:`supervised pipeline ` is presented. For `COCO `__ dataset the accuracy of pretrained weights is shown, and we report official COCO mAP with AP50. @@ -123,3 +187,61 @@ We trained each model with a single Nvidia GeForce RTX3090. +----------------------------+------------------+-----------+-----------+-----------+-----------+--------------+ | YOLOX-X | 50.9 (68.4) | 44.2 | 96.3 | 56.2 | 91.5 | 98.9 | +----------------------------+------------------+-----------+-----------+-----------+-----------+--------------+ + +************************ +Semi-supervised Learning +************************ + +For Semi-SL task solving we use the `Unbiased Teacher model `_, which is a specific implementation of Semi-SL for object detection. The unbiased teacher detaches the student model and the teacher model to prevent the teacher from being polluted by noisy pseudo-labels. In the early stage, the teacher model is trained by supervised loss. This stage is called a burn-in stage. After the burn-in, the student model is trained using both pseudo-labeled data from the teacher model and labeled data. And the teacher model is updated using +EMA. + +In Semi-SL, the pseudo-labeling process is combined with a consistency loss that ensures that the predictions of the model are consistent across augmented versions of the same data. This helps to reduce the impact of noisy or incorrect labels that may arise from the pseudo-labeling process. Additionally, our algorithm uses a combination of strong data augmentations and a specific optimizer called Sharpness-Aware Minimization (SAM) to further improve the accuracy of the model. + +Overall, OpenVINO™ Training Extensions utilizes powerful techniques for improving the performance of Semi-SL algorithm with limited labeled data. They can be particularly useful in domains where labeled data is expensive or difficult to obtain, and can help to reduce the time and cost associated with collecting labeled data. + +.. _od_semi_supervised_pipeline: + +- ``Pseudo-labeling``: A specific implementation of Semi-SL that combines the use of pseudo-labeling with a consistency loss, strong data augmentations, and a specific optimizer called Sharpness-Aware Minimization (SAM) to improve the performance of the model. + +- ``Weak & Strong augmentation``: For teacher model weak augmentations(random flip) are applied to input image. For the student model strong augmentations(colorjtter, grayscale, goussian blur, random erasing) are applied. + +- ``Additional training techniques``: Other than that, we use several solutions that apply to supervised learning (No bias Decay, Augmentations, Early stopping, LR conditioning.). + +Please, refer to the :doc:`tutorial <../../../tutorials/advanced/semi_sl>` how to train semi supervised learning. + +In the table below the mAP on toy data sample from `COCO `__ dataset using our pipeline is presented. + +We sample 400 images that contain one of [person, car, bus] for labeled train images. And 4000 images for unlabeled images. For validation 100 images are selected from val2017. + ++---------------------+--------------------------------------------+ +| Dataset | Sampled COCO dataset | ++=====================+=====================+======================+ +| | SL | Semi-SL | ++---------------------+---------------------+----------------------+ +| MobileNetV2-ATSS | | Person: 69.70 | | Person: 69.44 | +| | | Car: 65.00 | | Car: 65.84 | +| | | Bus: 42.96 | | Bus: 50.7 | +| | | Mean: 59.20 | | Mean: 61.98 | ++---------------------+---------------------+----------------------+ +| SSD | | Person: 39.24 | | Person: 38.52 | +| | | Car: 19.24 | | Car: 28.02 | +| | | Bus: 21.34 | | Bus: 26.28 | +| | | Mean: 26.60 | | Mean: 30.96 | ++---------------------+---------------------+----------------------+ +| YOLOX | | Person: 65.64 | | Person: 69.00 | +| | | Car: 64.44 | | Car: 65.66 | +| | | Bus: 60.68 | | Bus: 65.12 | +| | | Mean: 63.6 | | Mean: 66.58 | ++---------------------+---------------------+----------------------+ + +.. ************************ +.. Self-supervised Learning +.. ************************ + +.. To be added soon + +.. ******************** +.. Incremental Learning +.. ******************** + +.. To be added soon diff --git a/docs/source/guide/explanation/algorithms/segmentation/instance_segmentation.rst b/docs/source/guide/explanation/algorithms/segmentation/instance_segmentation.rst index 69f80931290..0c7b0c6191c 100644 --- a/docs/source/guide/explanation/algorithms/segmentation/instance_segmentation.rst +++ b/docs/source/guide/explanation/algorithms/segmentation/instance_segmentation.rst @@ -41,6 +41,12 @@ Dataset Format ************** For the dataset handling inside OpenVINO™ Training Extensions, we use `Dataset Management Framework (Datumaro) `_. For instance segmentation we support `COCO `_ dataset format. +If you have your dataset in those formats, then you can simply run using one line of code: + +.. code-block:: + + $ otx train --train-data-roots \ + --val-data-roots .. note:: @@ -52,19 +58,15 @@ Models We support the following ready-to-use model templates: -+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+---------------------+-----------------+ -| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | -+===============================================================================================================================================================================================================+============================+=====================+=================+ -| `Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B `_ | MaskRCNN-EfficientNetB2B | 68.48 | 13.27 | -+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+---------------------+-----------------+ -| `Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50 `_ | MaskRCNN-ResNet50 | 533.80 | 177.90 | -+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+---------------------+-----------------+ - -Above table can be found using the following command - -.. code-block:: shell - - (otx) ...$ otx find --task INSTANCE_SEGMENTATION ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+---------------------+-----------------+ +| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | ++============================================================================================================================================================================================================================================+============================+=====================+=================+ +| `Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B `_ | MaskRCNN-EfficientNetB2B | 68.48 | 13.27 | ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+---------------------+-----------------+ +| `Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50 `_ | MaskRCNN-ResNet50 | 533.80 | 177.90 | ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+---------------------+-----------------+ +| `Custom_Counting_Instance_Segmentation_MaskRCNN_ConvNeXt `_ | MaskRCNN-ConvNeXt | 266.78 | 192.4 | ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+---------------------+-----------------+ MaskRCNN-ResNet50 utilizes the `ResNet-50 `_ architecture as the backbone network for extracting image features. This choice of backbone network results in a higher number of parameters and FLOPs, which consequently requires more training time. However, the model offers superior performance in terms of accuracy. @@ -72,12 +74,79 @@ On the other hand, MaskRCNN-EfficientNetB2B employs the `EfficientNet-B2 `_. Through our experiments, we have observed that this variant achieves better accuracy compared to MaskRCNN-ResNet50 while utilizing less GPU memory. However, it is important to note that the training time and inference duration may slightly increase. If minimizing training time is a significant concern, we recommend considering a switch to MaskRCNN-EfficientNetB2B. -In the table below the `mAP `_ metric on some academic datasets using our :ref:`supervised pipeline ` is presented. The results were obtained on our templates without any changes. We use 1024x1024 image resolution, for other hyperparameters, please, refer to the related template. We trained each model with single Nvidia GeForce RTX3090. +.. In the table below the `mAP `_ metric on some academic datasets using our :ref:`supervised pipeline ` is presented. The results were obtained on our templates without any changes. We use 1024x1024 image resolution, for other hyperparameters, please, refer to the related template. We trained each model with single Nvidia GeForce RTX3090. + +.. +---------------------------+--------------+------------+-----------------+ +.. | Model name | ADE20k | Cityscapes | Pascal-VOC 2007 | +.. +===========================+==============+============+=================+ +.. | MaskRCNN-EfficientNetB2B | N/A | N/A | N/A | +.. +---------------------------+--------------+------------+-----------------+ +.. | MaskRCNN-ResNet50 | N/A | N/A | N/A | +.. +---------------------------+--------------+------------+-----------------+ +.. | MaskRCNN-ConvNeXt | N/A | N/A | N/A | +.. +---------------------------+--------------+------------+-----------------+ + +.. ******************* +.. Tiling Pipeline +.. ******************* + +.. To be added soon + +************************ +Semi-supervised Learning +************************ + +We employ the modified `Unbiased Teacher framework `_ to tackle the problem of :ref:`Semi-supervised learning ` in instance segmentation. +This framework leverages two models during training: a "student" model, which serves as the primary model being trained, and a "teacher" model, which acts as a guiding reference for the student model. + +During training, the student model is updated using both ground truth annotations (for labeled data) and pseudo-labels (for unlabeled data). +These pseudo-labels are generated by the teacher model's predictions. Notably, the teacher model's parameters are updated based on the moving average of the student model's parameters. +This means that backward loss propagation is not utilized for updating the teacher model. Once training is complete, only the student model is used for making predictions in the instance segmentation task. + +We also use the warmup stage for the teacher model during the first epochs to avoid utilizing too misleading pseudo labeling. We use constant thresholding for pseudo bounding boxes and, unlike in Unbiased Teacher work, we utilize all unlabeled losses including regression one and mask loss. +There are some key differences in the augmentation pipelines used for labeled and unlabeled data. +Basic augmentations such as random flip, random rotate, and random crop are employed for the teacher model's input. +On the other hand, stronger augmentations like color distortion, RGB to gray conversion, Gaussian blur and `Random Erasing `_ are applied to the student model. +This discrepancy helps improve generalization and prevents unnecessary overfitting on the pseudo-labels generated by the teacher model. +In the same way as for the supervised pipeline we utilize EMA smoothing for the student model throughout the whole training process. + +.. note:: + + To obtain a better performance after fine-tuning on small labeled datasets used in Semi-SL tasks we adopt a repeat dataset which brings metric improvement in our experiments. However, the training time also increases noticeably. + If the training time is important or the Semi-SL dataset has a sufficient number of labeled images, dataset repetition times can be decreased or switched off + in the corresponding `data_pipeline.py `_ config. + +The table below presents the mAP metric achieved by our templates on various datasets. +We provide these scores for comparison purposes, alongside the supervised baseline trained solely on labeled data. + +* `Cityscapes `_ : 8 classes, 267 labeled images, 2708 unlabeled and 500 images for validation +* `TrashCan `_ : 22 classes, 606 labeled images, 5459 unlabeled and 1147 images for validation +* `WGISD `_ : 5 classes , 11 labeled images, 599 unlabeled and 27 images for validation +* `Pascal-tiny `_ : 20 classes, 337 labeled images, 709 unlabeled and 303 images for validation + ++-------------------------------------+------------+------------+-------+-------------+ +| Model name | Cityscapes | TrashCan | WGISD | Pascal-tiny | ++=====================================+============+============+=======+=============+ +| MaskRCNN-ResNet50-supervised | 33.79 | 25.63 | 23.21 | 22.23 | ++-------------------------------------+------------+------------+-------+-------------+ +| MaskRCNN-ResNet50-semisl | 40.08 | 36.78 | 41.12 | 24.84 | ++-------------------------------------+------------+------------+-------+-------------+ +| MaskRCNN-EfficientNetB2B-supervised | 25.81 | 23.12 | 32.6 | 15.00 | ++-------------------------------------+------------+------------+-------+-------------+ +| MaskRCNN-EfficientNetB2B-semisl | 28.73 | 26.45 | 33.5 | 16.24 | ++-------------------------------------+------------+------------+-------+-------------+ + + + + +.. ************************ +.. Self-supervised Learning +.. ************************ + +.. To be added soon + +.. ******************** +.. Incremental Learning +.. ******************** -+---------------------------+--------------+------------+-----------------+ -| Model name | ADE20k | Cityscapes | Pascal-VOC 2007 | -+===========================+==============+============+=================+ -| MaskRCNN-EfficientNetB2B | N/A | N/A | N/A | -+---------------------------+--------------+------------+-----------------+ -| MaskRCNN-ResNet50 | N/A | N/A | N/A | -+---------------------------+--------------+------------+-----------------+ +.. To be added soon \ No newline at end of file diff --git a/docs/source/guide/explanation/algorithms/segmentation/semantic_segmentation.rst b/docs/source/guide/explanation/algorithms/segmentation/semantic_segmentation.rst index 020c09488bb..80c04464f41 100644 --- a/docs/source/guide/explanation/algorithms/segmentation/semantic_segmentation.rst +++ b/docs/source/guide/explanation/algorithms/segmentation/semantic_segmentation.rst @@ -28,7 +28,13 @@ For the supervised training we use the following algorithms components: - ``Loss function``: We use standard `Cross Entropy Loss `_ to train a model. - ``Additional training techniques`` - - ``Early stopping``: To add adaptability to the training pipeline and prevent overfitting. + - ``Early stopping``: To add adaptability to the training pipeline and prevent overfitting. You can use early stopping like the below command. + + .. code-block:: + + $ otx train {TEMPLATE} ... \ + params \ + --learning_parameters.enable_early_stopping=True ************** Dataset Format @@ -39,6 +45,11 @@ For the dataset handling inside OpenVINO™ Training Extensions, we use `Dataset At this end we support `Common Semantic Segmentation `_ data format. If you organized supported dataset format, starting training will be very simple. We just need to pass a path to the root folder and desired model template to start training: +.. code-block:: + + $ otx train --train-data-roots \ + --val-data-roots + .. note:: Due to some internal limitations, the dataset should always consist of a "background" label. If your dataset doesn't have a background label, rename the first label to "background" in the ``meta.json`` file. @@ -55,26 +66,32 @@ Models We support the following ready-to-use model templates: -+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ -| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | -+======================================================================================================================================================================================+========================+=====================+=================+ -| `Custom_Semantic_Segmentation_Lite-HRNet-s-mod2_OCR `_ | Lite-HRNet-s-mod2 | 1.44 | 3.2 | -+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ -| `Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR `_ | Lite-HRNet-18-mod2 | 2.82 | 4.3 | -+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ -| `Custom_Semantic_Segmentation_Lite-HRNet-x-mod3_OCR `_ | Lite-HRNet-x-mod3 | 9.20 | 5.7 | -+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ -| `Custom_Semantic_Segmentation_SegNext_T `_ | SegNext-t | 6.07 | 4.23 | -+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ -| `Custom_Semantic_Segmentation_SegNext_S `_ | SegNext-s | 15.35 | 13.9 | -+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ -| `Custom_Semantic_Segmentation_SegNext_B `_ | SegNext-b | 32.08 | 27.56 | -+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ ++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ +| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | ++==============================================================================================================================================================================================================================+========================+=====================+=================+ +| `Custom_Semantic_Segmentation_Lite-HRNet-s-mod2_OCR `_ | Lite-HRNet-s-mod2 | 1.44 | 3.2 | ++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ +| `Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR `_ | Lite-HRNet-18-mod2 | 2.82 | 4.3 | ++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ +| `Custom_Semantic_Segmentation_Lite-HRNet-x-mod3_OCR `_ | Lite-HRNet-x-mod3 | 9.20 | 5.7 | ++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ All of these models are members of the same `Lite-HRNet `_ backbones family. They differ in the trade-off between accuracy and inference/training speed. ``Lite-HRNet-x-mod3`` is the template with heavy-size architecture for accurate predictions but it requires long training. Whereas the ``Lite-HRNet-s-mod2`` is the lightweight architecture for fast inference and training. It is the best choice for the scenario of a limited amount of data. The ``Lite-HRNet-18-mod2`` model is the middle-sized architecture for the balance between fast inference and training time. -Use `SegNext `_ model which can achieve superior perfomance while preserving fast inference and fast training. +Besides this, we added new templates in experimental phase. For now, they support only supervised incremental training type. To run training with new templates we should use direct path to ``template_experimental.yaml``. + ++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ +| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | ++==============================================================================================================================================================================================================================+========================+=====================+=================+ +| `Custom_Semantic_Segmentation_SegNext_T `_ | SegNext-t | 6.07 | 4.23 | ++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ +| `Custom_Semantic_Segmentation_SegNext_S `_ | SegNext-s | 15.35 | 13.9 | ++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ +| `Custom_Semantic_Segmentation_SegNext_B `_ | SegNext-b | 32.08 | 27.56 | ++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ + +New templates use `SegNext `_ model which can achieve superior perfomance while preserving fast inference and fast training. In the table below the `Dice score `_ on some academic datasets using our :ref:`supervised pipeline ` is presented. We use 512x512 image crop resolution, for other hyperparameters, please, refer to the related template. We trained each model with single Nvidia GeForce RTX3090. @@ -97,3 +114,168 @@ In the table below the `Dice score ` for more information on how to train, validate and optimize the semantic segmentation model. + +************************ +Semi-supervised Learning +************************ + +We employ the `Mean Teacher framework `_ to tackle the problem of :ref:`Semi-supervised learning ` in semantic segmentation. +This framework leverages two models during training: a "student" model, which serves as the primary model being trained, and a "teacher" model, which acts as a guiding reference for the student model. + +During training, the student model is updated using both ground truth annotations (for labeled data) and pseudo-labels (for unlabeled data). +These pseudo-labels are generated by the teacher model's predictions. Notably, the teacher model's parameters are updated based on the moving average of the student model's parameters. +This means that backward loss propagation is not utilized for updating the teacher model. Once training is complete, only the student model is used for making predictions in the semantic segmentation task. + +The Mean Teacher framework utilizes the same core algorithm components as the :ref:`supervised pipeline ` for semantic segmentation. +However, there are some key differences in the augmentation pipelines used for labeled and unlabeled data. +Basic augmentations such as random flip, random rotate, and random crop are employed for the teacher model's input. +On the other hand, stronger augmentations like color distortion, RGB to gray conversion, and `CutOut `_ are applied to the student model. +This discrepancy helps improve generalization and prevents unnecessary overfitting on the pseudo-labels generated by the teacher model. +Additionally, pixels with high entropy, which are deemed unreliable by the teacher model, are filtered out using a schedule that depends on the training iterations. + +For new experimental templates (SegNext family) we also adopted the prototype view approach which is based on two research works: `Rethinking Semantic Segmentation: A Prototype View `_ by Tianfei Zhou et al. and `Semi-supervised Semantic Segmentation with Prototype-based Consistency Regularization `_ by Hai-Ming Xu et al. +We implemented a prototype network and incorporated it into the base Mean Teacher framework. We set weights for losses empirically after extensive experiments on the datasets presented below. + +The table below presents the `Dice score `_ achieved by our templates on various datasets. +We provide these scores for comparison purposes, alongside the supervised baseline trained solely on labeled data. +We use 512x512 image resolution, for other hyperparameters, please, refer to the related templates. When training the new SegNext templates, we disabled early stopping and trained them for the full number of epochs. We trained each model with a single Nvidia GeForce RTX3090. +For `Cityscapes `_ and `Pascal-VOC `_ we use splits with 1/16 ratio of labeled to unlabeled data like `here `_. +For other datasets, we prepared different numbers of classes and used the random split of the train data to obtain labeled and unlabeled sets. + +* **VOC_12**: 2 classes (person, car) were selected, 12 labeled images, 500 unlabeled and 150 images for validation +* **KITTI_54**: 3 classes (vehicle, human, construction) were selected, 54 labeled images, 200 unlabeled and 50 images for validation +* **City_4**: 4 classes (fence, vegetation, car, truck) were selected, 53 labeled images, 800 unlabeled and 500 images for validation +* **DIS5K 1/4**: 1 class (objects), 242 labeled images, 728 unlabeled and 281 images for validation + +Other classes for these datasets are marked as background labels. + ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| Model name | Cityscapes | Pascal-VOC | DIS5K | VOC_12 | KITTI_54 | City_4 | Mean mDice | ++=======================+============+============+=======+========+==========+========+============+ +| Lite-HRNet-s-mod2 | 40.80 | 43.05 | 81.00 | 60.12 | 61.83 | 66.72 | 58.92 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| Lite-HRNet-18-mod2 | 42.71 | 44.42 | 81.18 | 63.24 | 61.4 | 67.97 | 60.15 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| Lite-HRNet-x-mod3 | 49.20 | 43.87 | 81.48 | 63.96 | 59.76 | 68.08 | 61.06 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| Lite-HRNet-s-mod2-SSL | 45.05 | 44.01 | 81.46 | 64.78 | 61.90 | 67.03 | 60.71 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| Lite-HRNet-18-mod2-SSL| 48.65 | 46.24 | 81.52 | 65.64 | 65.25 | 68.11 | 62.57 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| Lite-HRNet-x-mod3-SSL | 50.00 | 46.10 | 82.00 | 66.10 | 66.50 | 68.41 | 63.19 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| SegNext-t | 55.93 | 73.82 | 86.87 | 68.00 | 62.35 | 68.30 | 69.21 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| SegNext-s | 63.75 | 77.24 | 87.88 | 76.30 | 66.45 | 69.34 | 73.49 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| SegNext-b | 66.39 | 80.52 | 89.62 | 78.65 | 70.45 | 69.68 | 75.89 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| SegNext-t-SSL | 60.2 | 77.44 | 87.60 | 70.72 | 67.43 | 69.21 | 72.10 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| SegNext-s-SSL | 68.06 | 80.55 | 88.72 | 77.00 | 68.70 | 69.73 | 75.46 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ +| SegNext-b-SSL | 71.80 | 82.43 | 90.32 | 80.68 | 73.73 | 70.02 | 78.16 | ++-----------------------+------------+------------+-------+--------+----------+--------+------------+ + +************************ +Self-supervised Learning +************************ +.. _selfsl_semantic_segmentation: + +Self-supervised learning can be one of the solutions if the user has a small data set, but label information is not yet available. +General self-supervised Learning in academia is commonly used to obtain well-pretrained weights from a source dataset without label information. +However, in real-world industries, it is difficult to apply because of small datasets, limited resources, or training in minutes. + +For these cases, OpenVINO™ Training Extensions provides improved self-supervised learning recipes that can be applied to the above harsh environments. +OpenVINO™ Training Extensions allows to perform a pre-training phase on any images to further use obtained weights on the target dataset. +We adapted `DetCon `_ as our self-supervised method. +It takes some time to use these self-supervised learning recipes, but you can expect improved performance, especially in small-data regimes. + +The below table shows how much performance (mDice) self-supervised methods improved compared with baseline performance on the subsets of Pascal VOC 2012 with three classes (person, car, bicycle). +To get the below performance, we had two steps: + +- Train the models using only images containing at less one class of the three classes without label information to get pretrained weights for a few epochs. +- Fine-tune the models with pretrained weights using subset datasets and get performance. + +We additionally obtained baseline performance from supervised learning using subset datasets for comparison. +Each subset dataset has 8, 16, and 24 images, respectively. + ++--------------------+-------+---------+-------+---------+-------+---------+ +| Model name | #8 | | #16 | | #24 | | ++====================+=======+=========+=======+=========+=======+=========+ +| | SL | Self-SL | SL | Self-SL | SL | Self-SL | ++--------------------+-------+---------+-------+---------+-------+---------+ +| Lite-HRNet-s-mod2 | 48.30 | 53.55 | 57.08 | 58.96 | 62.40 | 63.46 | ++--------------------+-------+---------+-------+---------+-------+---------+ +| Lite-HRNet-18-mod2 | 53.47 | 49.20 | 56.69 | 58.72 | 62.81 | 63.63 | ++--------------------+-------+---------+-------+---------+-------+---------+ +| Lite-HRNet-x-mod3 | 50.23 | 50.93 | 60.09 | 61.61 | 62.66 | 64.87 | ++--------------------+-------+---------+-------+---------+-------+---------+ + +Unlike other tasks, two things are considered to use self-supervised learning: + +- ``--train-data-roots`` must be set to a directory only containing images, not ground truths. + DetCon uses pseudo masks created in ``detcon_mask`` directory for training. If they are not created yet, they will be created first. +- ``--val-data-roots`` is not needed. + +To enable self-supervised training, the command below can be executed: + +.. code-block:: + + $ otx train Lite-HRNet-18-mod2 \ + --train-data-roots path/to/images \ + +After self-supervised training, pretrained weights can be use for supervised (incremental) learning like the below command: + +.. code-block:: + + $ otx train Lite-HRNet-18-mod2 \ + --train-data-roots path/to/train/subset \ + --val-data-roots path/to/validation/subset \ + --load-weights={PATH/PRETRAINED/WEIGHTS} + +.. note:: + SL stands for Supervised Learning. + +******************************* +Supervised Contrastive Learning +******************************* + +To enhance the performance of the algorithm in case when we have a small number of data, `Supervised Contrastive Learning (SupCon) `_ can be used. + +More specifically, we train a model with two heads: segmentation head with Cross Entropy Loss and contrastive head with `DetCon loss `_. +As of using this advanced approach, we can expect improved performance and reduced training time rather than supervised learning. +The below table shows how much performance (mDice) SupCon improved compared with baseline performance on the subsets of Pascal VOC 2012 with three classes (person, car, bicycle). +Each subset dataset has 8, 16, and 24 images, respectively. + ++--------------------+-------+--------+-------+-------+--------+-------+-------+--------+-------+ +| Model name | #8 | | | #16 | | | #24 | | | ++====================+=======+========+=======+=======+========+=======+========+=======+=======+ +| | SL | SupCon | TR | SL | SupCon | TR | SL | SupCon | TR | ++--------------------+-------+--------+-------+-------+--------+-------+-------+--------+-------+ +| Lite-HRNet-s-mod2 | 52.30 | 54.24 | 0.83x | 59.58 | 61.44 | 0.93x | 62.86 | 64.30 | 1.03x | ++--------------------+-------+--------+-------+-------+--------+-------+-------+--------+-------+ +| Lite-HRNet-18-mod2 | 53.00 | 56.16 | 0.71x | 61.44 | 60.08 | 0.91x | 64.26 | 64.82 | 0.91x | ++--------------------+-------+--------+-------+-------+--------+-------+-------+--------+-------+ +| Lite-HRNet-x-mod3 | 53.71 | 58.67 | 0.83x | 58.43 | 61.52 | 0.73x | 64.72 | 65.83 | 0.73x | ++--------------------+-------+--------+-------+-------+--------+-------+-------+--------+-------+ + +The SupCon training can be launched by adding additional option to template parameters like the below. +It can be launched only with supervised (incremental) training type. + +.. code-block:: + + $ otx train Lite-HRNet-18-mod2 \ + --train-data-roots path/to/train/subset \ + --val-data-roots path/to/validation/subset \ + params \ + --learning_parameters.enable_supcon=True + +.. note:: + SL : Supervised Learning / TR : Training Time Ratio of SupCon compared with supervised learning + +.. ******************** +.. Incremental Learning +.. ******************** + +.. To be added soon diff --git a/docs/source/guide/explanation/algorithms/visual_prompting/fine_tuning.rst b/docs/source/guide/explanation/algorithms/visual_prompting/fine_tuning.rst index f2838a6ece5..67bc77ff4f8 100644 --- a/docs/source/guide/explanation/algorithms/visual_prompting/fine_tuning.rst +++ b/docs/source/guide/explanation/algorithms/visual_prompting/fine_tuning.rst @@ -1,5 +1,5 @@ Visual Prompting (Fine-tuning) -================================== +================= Visual prompting is a computer vision task that uses a combination of an image and prompts, such as texts, bounding boxes, points, and so on to troubleshoot problems. Using these useful prompts, the main purpose of this task is to obtain labels from unlabeled datasets, and to use generated label information on particular domains or to develop a new model with the generated information. @@ -47,11 +47,20 @@ We support three dataset formats for visual prompting: - `Pascal VOC `_ for instance segmentation and semantic segmentation + +If you organized supported dataset format, starting training will be very simple. We just need to pass a path to the root folder and desired model template to start training: + +.. code-block:: + + $ otx train \ + --train-data-roots \ + --val-data-roots + .. note:: During training, mDice for binary mask without label information is used for train/validation metric. - After training, if using ``otx test`` to evaluate performance, mDice for binary or multi-class masks with label information will be used. - As you can expect, performance will be different between ``otx train`` and ``otx test``, but if unlabeled mask performance is high, labeld mask performance is high as well. + After training, if using ``otx eval`` to evaluate performance, mDice for binary or multi-class masks with label information will be used. + As you can expect, performance will be different between ``otx train`` and ``otx eval``, but if unlabeled mask performance is high, labeld mask performance is high as well. ****** @@ -61,13 +70,13 @@ Models We support the following model templates in experimental phase: -+------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------+---------------------+-----------------+ -| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | -+============================================================================================================================================================+==============+=====================+=================+ -| `Visual_Prompting_SAM_Tiny_ViT `_ | SAM_Tiny_ViT | 38.95 | 47 | -+------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------+---------------------+-----------------+ -| `Visual_Prompting_SAM_ViT_B `_ | SAM_ViT_B | 483.71 | 362 | -+------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------+---------------------+-----------------+ ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------+---------------------+-----------------+ +| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | ++============================================================================================================================================================================================+==============+=====================+=================+ +| `Visual_Prompting_SAM_Tiny_ViT `_ | SAM_Tiny_ViT | 38.95 | 47 | ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------+---------------------+-----------------+ +| `Visual_Prompting_SAM_ViT_B `_ | SAM_ViT_B | 483.71 | 362 | ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------+---------------------+-----------------+ To check feasibility of `SAM `_, we did experiments using three public datasets with each other domains: `WGISD `_, `Trashcan `_, and `FLARE22 `_, and checked `Dice score `_. We used sampled training data from `Trashcan `_ and `FLARE22 `_, and full training data (=110) from `WGISD `_. @@ -94,9 +103,11 @@ The below table shows performance improvement after fine-tuning. According to datasets, ``learning rate`` and ``batch size`` can be adjusted like below: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx train --config \ - --data_root \ - --data.config.train_subset.batch_size \ - --optimizer.lr + $ otx train \ + --train-data-roots \ + --val-data-roots \ + params \ + --learning_parameters.dataset.train_batch_size \ + --learning_parameters.optimizer.lr diff --git a/docs/source/guide/explanation/algorithms/visual_prompting/index.rst b/docs/source/guide/explanation/algorithms/visual_prompting/index.rst index 8910b1101b5..c9d6abac31b 100644 --- a/docs/source/guide/explanation/algorithms/visual_prompting/index.rst +++ b/docs/source/guide/explanation/algorithms/visual_prompting/index.rst @@ -1,5 +1,5 @@ Visual Prompting -================ +============ .. toctree:: :maxdepth: 1 diff --git a/docs/source/guide/explanation/algorithms/visual_prompting/zero_shot.rst b/docs/source/guide/explanation/algorithms/visual_prompting/zero_shot.rst index daacbef51b4..4923a2b73c6 100644 --- a/docs/source/guide/explanation/algorithms/visual_prompting/zero_shot.rst +++ b/docs/source/guide/explanation/algorithms/visual_prompting/zero_shot.rst @@ -1,5 +1,5 @@ Visual Prompting (Zero-shot learning) -===================================== +================= Visual prompting is a computer vision task that uses a combination of an image and prompts, such as texts, bounding boxes, points, and so on to troubleshoot problems. Using these useful prompts, the main purpose of this task is to obtain labels from unlabeled datasets, and to use generated label information on particular domains or to develop a new model with the generated information. @@ -41,6 +41,15 @@ We support three dataset formats for visual prompting: - `Pascal VOC `_ for instance segmentation and semantic segmentation +If you organized supported dataset format, starting training will be very simple. We just need to pass a path to the root folder and desired model template to start training: + +.. code-block:: + + $ otx train \ + --train-data-roots \ + --val-data-roots + + ****** Models ****** @@ -48,11 +57,11 @@ Models We support the following model templates in experimental phase: -+---------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ -| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | -+===============================================================================================================================================================+========================+=====================+=================+ -| `Zero_Shot_SAM_Tiny_ViT `_ | Zero_Shot_SAM_Tiny_ViT | 38.18 | 25 | -+---------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ ++-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ +| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | ++===============================================================================================================================================================================================+========================+=====================+=================+ +| `Zero_Shot_SAM_Tiny_ViT `_ | Zero_Shot_SAM_Tiny_ViT | 38.18 | 25 | ++-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+---------------------+-----------------+ *************** Simple tutorial @@ -64,18 +73,19 @@ There are two steps for zero-shot inference: ``learn`` and ``infer``. Extracted reference features will be saved in the model checkpoint (such as `weight.pth`) with the model. You can do ``learn`` with the following source code: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx train --config \ - --data_root + $ otx train \ + --train-data-roots \ + --val-data-roots ``Infer`` is to get predicted masks on given target images. Unlike ``learn``, this stage doesn't need any prompt information. .. code-block:: - (otx) ...$ otx test --config \ - --data_root \ - --checkpoint + $ otx eval \ + --load-weights + --test-data-roots For example, when the positive (green) and the negative (red) points were given with the reference image for ``learn`` stage, you can get basic `SAM `_ prediction result (left). diff --git a/docs/source/guide/get_started/api_tutorial.rst b/docs/source/guide/get_started/api_tutorial.rst deleted file mode 100644 index 83fa6f52ca5..00000000000 --- a/docs/source/guide/get_started/api_tutorial.rst +++ /dev/null @@ -1,501 +0,0 @@ -OpenVINO™ Training Extensions API Quick-Start -============================================== - -Besides CLI functionality, The OpenVINO™ Training Extension provides APIs that help developers to integrate OpenVINO™ Training Extensions models into their projects. -This tutorial intends to show how to create a dataset, model and use all of the CLI functionality through APIs. - -For demonstration purposes we will use the Object Detection SSD model with `WGISD `_ public dataset as we did for the :doc:`CLI tutorial <../tutorials/base/how_to_train/detection>`. - -.. note:: - - To start with we need to `install OpenVINO™ Training Extensions `_. - -******************* -Dataset preparation -******************* - -1. Clone a repository -with `WGISD dataset `_. - -.. code-block:: shell - - cd data - git clone https://github.com/thsant/wgisd.git - cd wgisd - git checkout 6910edc5ae3aae8c20062941b1641821f0c30127 - -2. We need to rename annotations to -be distinguished by OpenVINO™ Training Extensions Datumaro manager: - -.. code-block:: shell - - mv data images && mv coco_annotations annotations && mv annotations/train_bbox_instances.json instances_train.json && mv annotations/test_bbox_instances.json instances_val.json - -Now it is all set to use this dataset inside OpenVINO™ Training Extensions - -************************************ -Quick Start with auto-configuration -************************************ - -Once the dataset is ready, we can immediately start training with the model and data pipeline recommended by OTX through auto-configuration. -The following code snippet demonstrates how to use the auto-configuration feature: - -.. code-block:: python - - from otx.engine import Engine - - engine = Engine(data_root="data/wgisd") - engine.train() - - -.. note:: - - If dataset supports multiple Task types, this will default to the Task type detected by OTX. - If you want to specify a specific Task type, you need to specify it like below: - - .. code-block:: python - - from otx.engine import Engine - - engine = Engine(data_root="data/wgisd", task="INSTANCE_SEGMENTATION") - engine.train() - - -********************************** -Check Available Model Recipes -********************************** - -If you want to use other models offered by OTX besides the ones provided by Auto-Configuration, you can get a list of available models in OTX as shown below. - -.. code-block:: python - - from otx.engine.utils.api import list_models - - model_lists = list_models(task="DETECTION") - print(model_lists) - - ''' - [ - 'yolox_tiny_tile', - 'yolox_x', - 'yolox_l_tile', - 'yolox_x_tile', 'yolox_l', - 'atss_r50_fpn', - 'ssd_mobilenetv2', - 'yolox_s', - 'yolox_tiny', - 'openvino_model', - 'atss_mobilenetv2', - 'yolox_s_tile', - 'rtmdet_tiny', - 'atss_mobilenetv2_tile', - 'atss_resnext101', - 'ssd_mobilenetv2_tile', - ] - ''' - - -.. note:: - - If you're looking for a specific name, use the pattern argument. - - .. code-block:: python - - from otx.engine.utils.api import list_models - - model_lists = list_models(task="DETECTION", pattern="tile") - print(model_lists) - ''' - [ - 'yolox_tiny_tile', - 'ssd_mobilenetv2_tile', - 'yolox_l_tile', - 'yolox_s_tile', - 'yolox_x_tile', - 'atss_mobilenetv2_tile', - ] - ''' - - -You can also find the available model recipes in YAML form in the folder ``otx/recipe``. - -********* -Engine -********* - -The ``otx.engine.Engine`` class is the main entry point for using OpenVINO™ Training Extensions APIs. - -1. Setting ``task`` - -Specify ``task``. This is the task type for that ``Engine`` usage. -You can set the task by referencing the ``OTXTaskType`` in ``otx.core.types.task``. -If no task is specified, the task is detected and used via ``datamodule`` or ``data_root``. - -.. code-block:: python - - from otx.core.types.task import OTXTaskType - from otx.engine import Engine - - engine = Engine(task=OTXTaskType.DETECTION) - # or - engine = Engine(task="DETECTION") - -2. Setting ``work_dir`` - -Specify ``work_dir``. This is the workspace for that ``Engine``, and where output is stored. -The default value is currently ``./otx-workspace``. - -.. code-block:: python - - from otx.engine import Engine - - engine = Engine(work_dir="work_dir") - - -3. Setting device - -You can set the device by referencing the ``DeviceType`` in ``otx.core.types.device``. -The current default setting is ``auto``. - -.. code-block:: python - - from otx.core.types.device import DeviceType - from otx.engine import Engine - - engine = Engine(device=DeviceType.gpu) - # or - engine = Engine(device="gpu") - - -In addition, the ``Engine`` constructor can be associated with the Trainer's constructor arguments to control the Trainer's functionality. -Refer `lightning.Trainer `_. - -4. Using the OTX configuration we can configure the Engine. - -.. code-block:: python - - from otx.engine import Engine - - recipe = "src/otx/recipe/detection/atss_mobilenetv2.yaml" - engine = Engine.from_config( - config_path=recipe, - data_root="data/wgisd", - work_dir="./otx-workspace", - ) - - -********* -Training -********* - -Create an output model and start actual training: - -1. Below is an example using the ``atss_mobilenetv2`` model provided by OTX. - -.. code-block:: python - - from otx.engine import Engine - - engine = Engine(data_root="data/wgisd", model="atss_mobilenetv2") - engine.train() - -2. Alternatively, we can use the configuration file. - -.. code-block:: python - - from otx.engine import Engine - - config = "src/otx/recipe/detection/atss_mobilenetv2.yaml" - - engine = Engine.from_config(config_path=config, data_root="data/wgisd") - engine.train() - -.. note:: - - This can use callbacks provided by OTX and several training techniques. - However, in this case, no arguments are specified for train. - -3. If you want to specify the model, you can do so as shown below: - -The model used by the Engine is of type ``otx.core.model.entity.base.OTXModel``. - -.. tab-set:: - - .. tab-item:: Custom Model - - .. code-block:: python - - from otx.algo.detection.atss import ATSS - from otx.engine import Engine - - model = ATSS(num_classes=5, variant="mobilenetv2") - - engine = Engine(data_root="data/wgisd", model=model) - engine.train() - - .. tab-item:: Custom Model with checkpoint - - .. code-block:: python - - from otx.algo.detection.atss import ATSS - from otx.engine import Engine - - model = ATSS(num_classes=5, variant="mobilenetv2") - - engine = Engine(data_root="data/wgisd", model=model, checkpoint="") - engine.train() - - .. tab-item:: Custom Optimizer & Scheduler - - .. code-block:: python - - from torch.optim import SGD - from torch.optim.lr_scheduler import CosineAnnealingLR - from otx.algo.detection.atss import ATSS - from otx.engine import Engine - - model = ATSS(num_classes=5, variant="mobilenetv2") - optimizer = SGD(model.parameters(), lr=0.01, weight_decay=1e-4, momentum=0.9) - scheduler = CosineAnnealingLR(optimizer, T_max=10000, eta_min=0) - - engine = Engine( - ..., - model=model, - optimizer=optimizer, - scheduler=scheduler, - ) - engine.train() - -4. If you want to specify the datamodule, you can do so as shown below: - -The datamodule used by the Engine is of type ``otx.core.data.module.OTXDataModule``. - -.. code-block:: python - - from otx.core.data.module import OTXDataModule - from otx.engine import Engine - - datamodule = OTXDataModule(data_root="data/wgisd") - - engine = Engine(datamodule=datamodule) - engine.train() - -.. note:: - - If both ``data_root`` and ``datamodule`` enter ``Engine`` as input, ``Engine`` uses datamodule as the base. - - -5. You can use train-specific arguments with ``train()`` function. - -.. tab-set:: - - .. tab-item:: Change Max Epochs - - .. code-block:: python - - engine.train(max_epochs=10) - - .. tab-item:: Fix Training Seed & Set Deterministic - - .. code-block:: python - - engine.train(seed=1234, deterministic=True) - - .. tab-item:: Use Mixed Precision - - .. code-block:: python - - engine.train(precision="16") - - .. note:: - - This uses lightning's precision value. You can use the values below: - - "64", "32", "16", "bf16", - - 64, 32, 16 - - .. tab-item:: Change Validation Metric - - .. code-block:: python - - from otx.core.metrics.fmeasure import FMeasure - - metric = FMeasue(num_classes=5) - engine.train(metric=metric) - - .. tab-item:: Set Callbacks & Logger - - .. code-block:: python - - from lightning.pytorch.callbacks import EarlyStopping - from lightning.pytorch.loggers.tensorboard import TensorBoardLogger - - engine.train(callbacks=[EarlyStopping()], loggers=[TensorBoardLogger()]) - -In addition, the ``train()`` function can be associated with the Trainer's constructor arguments to control the Trainer's functionality. -Refer `lightning.Trainer `_. - -For example, if you want to use the ``limit_val_batches`` feature provided by Trainer, you can use it like this: - -.. code-block:: python - - # disable validation - engine.train(limit_val_batches=0) - -6. It's also easy to use HPOs. - -.. code-block:: python - - engine.train(run_hpo=True) - - -*********** -Evaluation -*********** - -If the training is already in place, we just need to use the code below: - -.. tab-set:: - - .. tab-item:: Evaluate Model - - .. code-block:: python - - engine.test() - - .. tab-item:: Evaluate Model with different checkpoint - - .. code-block:: python - - engine.test(checkpoint="") - - .. note:: - - The format that can enter the checkpoint is of type torch (.ckpt) or exported model (.onnx, .xml). - - .. tab-item:: Evaluate Model with different datamodule or dataloader - - .. code-block:: python - - from otx.core.data.module import OTXDataModule - - datamodule = OTXDataModule(data_root="data/wgisd") - engine.test(datamodule=datamodule) - - .. tab-item:: Evaluate Model with different metrics - - .. code-block:: python - - from otx.core.metrics.fmeasure import FMeasure - - metric = FMeasue(num_classes=5) - engine.test(metric=metric) - - -*********** -Exporting -*********** - -To export our model to OpenVINO™ IR format we need to create output model and run exporting task. -If the engine is trained, you can proceed with the export using the current engine's checkpoint: - -The default value for ``export_format`` is ``OPENVINO``. -The default value for ``export_precision`` is ``FP32``. - -.. tab-set:: - - .. tab-item:: Export OpenVINO™ IR - - .. code-block:: python - - engine.export() - - .. tab-item:: Export ONNX - - .. code-block:: python - - engine.export(export_format="ONNX") - - .. tab-item:: Export with explain features - - .. code-block:: python - - engine.export(explain=True) - - .. note:: - - This ensures that it is exported with a ``saliency_map`` and ``feature_vector`` that will be used in the XAI. - - .. tab-item:: Export with different checkpoint - - .. code-block:: python - - engine.export(checkpoint="") - - .. tab-item:: Export with FP16 - - .. code-block:: python - - engine.export(precision="FP16") - - -**** -XAI -**** - -To run the XAI with the OpenVINO™ IR model, we need to create an output model and run the XAI procedure: - -.. tab-set:: - - .. tab-item:: Run XAI - - .. code-block:: python - - engine.explain(checkpoint="") - - .. tab-item:: Evaluate Model with different datamodule or dataloader - - .. code-block:: python - - from otx.core.data.module import OTXDataModule - - datamodule = OTXDataModule(data_root="data/wgisd") - engine.explain(..., datamodule=datamodule) - - .. tab-item:: Dump saliency_map - - .. code-block:: python - - engine.explain(..., dump=True) - - -************ -Optimization -************ - -To run the optimization with PTQ on the OpenVINO™ IR model, we need to create an output model and run the optimization procedure: - -.. tab-set:: - - .. tab-item:: Run PTQ Optimization - - .. code-block:: python - - engine.optimize(checkpoint="") - - .. tab-item:: Evaluate Model with different datamodule or dataloader - - .. code-block:: python - - from otx.core.data.module import OTXDataModule - - datamodule = OTXDataModule(data_root="data/wgisd") - engine.optimize(..., datamodule=datamodule) - - -You can validate the optimized model as the usual model. For example for the NNCF model it will look like this: - -.. code-block:: python - - engine.test(checkpoint="") - -That's it. Now, we can use OpenVINO™ Training Extensions APIs to create, train, and deploy deep learning models using the OpenVINO™ Training Extension. diff --git a/docs/source/guide/get_started/cli_commands.rst b/docs/source/guide/get_started/cli_commands.rst index 7779e49b6f3..9ef1e7b3ebe 100644 --- a/docs/source/guide/get_started/cli_commands.rst +++ b/docs/source/guide/get_started/cli_commands.rst @@ -1,4 +1,4 @@ -OpenVINO™ Training Extensions CLI Usage +OpenVINO™ Training Extensions CLI commands ========================================== All possible OpenVINO™ Training Extensions CLI commands are presented below along with some general examples of how to run specific functionality. There are :doc:`dedicated tutorials <../tutorials/base/how_to_train/index>` in our documentation with life-practical examples on specific datasets for each task. @@ -6,264 +6,110 @@ All possible OpenVINO™ Training Extensions CLI commands are presented below al .. note:: To run CLI commands you need to prepare a dataset. Each task requires specific data formats. To know more about which formats are supported by each task, refer to :doc:`explanation section <../explanation/algorithms/index>` in the documentation. - Also, by default, the OTX CLI is written using jsonargparse, see jsonargparse or LightningCLI. - `Jsonargparse Documentation _` ***** -Help +Find ***** -``otx --help`` show available sub-commands. - -.. code-block:: shell - - (otx) ...$ otx --help - ╭─ Arguments ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ Usage: otx [-h] [-v] {install,find,train,test,predict,export,optimize,explain} ... │ - │ │ - │ │ - │ OpenVINO Training-Extension command line tool │ - │ │ - │ │ - │ Options: │ - │ -h, --help Show this help message and exit. │ - │ -v, --version Display OTX version number. │ - │ │ - │ Subcommands: │ - │ For more details of each subcommand, add it as an argument followed by --help. │ - │ │ - │ │ - │ Available subcommands: │ - │ install Install OTX requirements. │ - │ find This shows the model provided by OTX. │ - │ train Trains the model using the provided LightningModule and OTXDataModule. │ - │ test Run the testing phase of the engine. │ - │ predict Run predictions using the specified model and data. │ - │ export Export the trained model to OpenVINO Intermediate Representation (IR) or ONNX formats. │ - │ optimize Applies NNCF.PTQ to the underlying models (now works only for OV models). │ - │ explain Run XAI using the specified model and data (test subset). │ - │ │ - ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - -.. note:: - - After installing the package, if torch is not installed properly, this will only show the ``install`` subcommand. You can refer to this :doc:`installation section `. - - -The subcommand can get help output in the following way. -For basic subcommand help, the Verbosity Level is 0. In this case, the CLI provides a Quick-Guide in markdown. - -.. code-block:: shell - - (otx) ...$ otx train --help - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ OpenVINO™ Training Extensions CLI Guide ┃ - ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - Github Repository: - https://github.com/openvinotoolkit/training_extensions. - - A better guide is provided by the documentation. - ╭─ Quick-Start ─────────────────────────────────────────────────────────╮ - │ │ - │ 1 you can train with data_root only. then OTX will provide default │ - │ model. │ - │ │ - │ │ - │ otx train --data_root │ - │ │ - │ │ - │ 2 you can pick a model or datamodule as Config file or Class. │ - │ │ - │ │ - │ otx train │ - │ --data_root │ - │ --model --data │ - │ │ - │ │ - │ 3 Of course, you can override the various values with commands. │ - │ │ - │ │ - │ otx train │ - │ --data_root │ - │ --max_epochs --checkpoint │ - │ │ - │ │ - │ 4 If you have a complete configuration file, run it like this. │ - │ │ - │ │ - │ otx train --data_root --config │ - │ │ - │ │ - │ To get more overridable argument information, run the command below. │ - │ │ - │ │ - │ # Verbosity Level 1 │ - │ otx train [optional_arguments] -h -v │ - │ # Verbosity Level 2 │ - │ otx train [optional_arguments] -h -vv │ - │ │ - ╰───────────────────────────────────────────────────────────────────────╯ - -For Verbosity Level 1, it shows Quick-Guide & the essential arguments. - -.. code-block:: shell - - (otx) ...$ otx train --help -v - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ OpenVINO™ Training Extensions CLI Guide ┃ - ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - Github Repository: - https://github.com/openvinotoolkit/training_extensions. - - A better guide is provided by the documentation. - ╭─ Quick-Start ─────────────────────────────────────────────────────────╮ - │ ... │ - ╰───────────────────────────────────────────────────────────────────────╯ - ╭─ Arguments ─────────────────────────────────────────────────────────────────────────────────────╮ - │ Usage: otx [options] train [-h] [-c CONFIG] [--print_config [=flags]] │ - │ [--data_root DATA_ROOT] [--task TASK] │ - │ [--engine CONFIG] │ - │ [--work_dir WORK_DIR] │ - │ [--engine.checkpoint CHECKPOINT] │ - │ [--engine.device {auto,gpu,cpu,tpu,ipu,hpu,mps}] │ - │ [--model.help CLASS_PATH_OR_NAME] │ - │ [--model CONFIG | CLASS_PATH_OR_NAME | .INIT_ARG_NAME VALUE] │ - │ [--data CONFIG] │ - │ [--optimizer CONFIG | CLASS_PATH_OR_NAME | .INIT_ARG_NAME VALUE] │ - │ [--scheduler CONFIG | CLASS_PATH_OR_NAME | .INIT_ARG_NAME VALUE] │ - │ │ - ... - -For Verbosity Level 2, it shows all available arguments. - -.. code-block:: shell - - (otx) ...$ otx train --help -vv - - -************ -print_config -************ - -Preview all configuration values that will be executed through that command line. - -.. code-block:: shell +``otx find`` lists model templates and backbones available for the given task. Specify the task name with ``--task`` option. Use ``--backbone BACKBONE`` to find the backbone from supported frameworks. - (otx) ...$ otx train --config --print_config +.. code-block:: + (otx) ...$ otx find --help + usage: otx find [-h] [--task TASK] [--template] [--backbone BACKBONE [BACKBONE ...]] -.. code-block:: yaml + optional arguments: + -h, --help show this help message and exit + --task TASK The currently supported options: ('CLASSIFICATION', 'DETECTION', 'ROTATED_DETECTION', 'INSTANCE_SEGMENTATION', 'SEGMENTATION', 'ACTION_CLASSIFICATION', 'ACTION_DETECTION', + 'ANOMALY_CLASSIFICATION', 'ANOMALY_DETECTION', 'ANOMALY_SEGMENTATION'). + --template Shows a list of templates that can be used immediately. + --backbone BACKBONE [BACKBONE ...] + The currently supported options: ['otx', 'mmcls', 'mmdet', 'mmseg', 'torchvision', 'pytorchcv', 'omz.mmcls']. - data_root: tests/assets/car_tree_bug - callback_monitor: val/map_50 - engine: - task: DETECTION - work_dir: ./otx-workspace - device: auto - model: - class_path: otx.algo.detection.atss.ATSS - init_args: - num_classes: 1000 - variant: mobilenetv2 - optimizer: ... - scheduler: ... - data: - task: DETECTION - config: - data_format: coco_instances - train_subset: ... - val_subset: ... - test_subset: ... - mem_cache_size: 1GB - mem_cache_img_max_size: null - image_color_channel: RGB - include_polygons: false - max_epochs: 2 - deterministic: false - precision: 16 - callbacks: ... - logger: ... -Users can also pre-generate a config file with an example like the one below. +Example to find ready-to-use templates for the detection task: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx train --config --print_config > config.yaml + (otx) ...$ otx find --task detection + +-----------+-----------------------------------+------------------+------------------------------------------------------------------------------------+ + | TASK | ID | NAME | BASE PATH | + +-----------+-----------------------------------+------------------+------------------------------------------------------------------------------------+ + | DETECTION | Custom_Object_Detection_Gen3_ATSS | MobileNetV2-ATSS | src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml | + | DETECTION | Object_Detection_ResNeXt101_ATSS | ResNeXt101-ATSS | src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml | + | DETECTION | Custom_Object_Detection_Gen3_SSD | SSD | src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml | + | DETECTION | Object_Detection_YOLOX_L | YOLOX-L | src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml | + | DETECTION | Object_Detection_YOLOX_S | YOLOX-S | src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml | + | DETECTION | Custom_Object_Detection_YOLOX | YOLOX-TINY | src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/template.yaml | + | DETECTION | Object_Detection_YOLOX_X | YOLOX-X | src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml | + +-----------+-----------------------------------+------------------+------------------------------------------------------------------------------------+ + +Example to find supported torchvision backbones for the detection task: +.. code-block:: -***** -Find -***** + (otx) ...$ otx find --task detection --backbone torchvision + +-------+--------------------------------+---------------+---------+ + | Index | Backbone Type | Required-Args | Options | + +-------+--------------------------------+---------------+---------+ + | 1 | torchvision.alexnet | | | + | 2 | torchvision.resnet18 | | | + | 3 | torchvision.resnet34 | | | + | 4 | torchvision.resnet50 | | | + ... + | 33 | torchvision.shufflenet_v2_x1_0 | | | + | 34 | torchvision.shufflenet_v2_x1_5 | | | + | 35 | torchvision.shufflenet_v2_x2_0 | | | + +-------+--------------------------------+---------------+---------+ -``otx find`` lists model templates and backbones available for the given task. Specify the task name with ``--task`` option. Use ``--pattern`` to find the model name from OTX. -.. code-block:: shell - (otx) ...$ otx find --help - ╭─ Arguments ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ Usage: otx [options] find [-h] │ - │ [--task {ACTION_CLASSIFICATION,ACTION_DETECTION,ANOMALY_CLASSIFICATION,ANOMALY_DETECTION,ANOMALY_SEGMENTATION,MULTI_CLASS_CLS,MULTI_LABEL_CLS,H_LABEL_CLS,DETEC │ - │ [--pattern PATTERN] │ - │ │ - │ │ - │ Options: │ - │ -h, --help Show this help message and exit. │ - │ --task {ACTION_CLASSIFICATION,ACTION_DETECTION,ANOMALY_CLASSIFICATION,ANOMALY_DETECTION,ANOMALY_SEGMENTATION,MULTI_CLASS_CLS,MULTI_LABEL_CLS,H_LABEL_CLS,DETECTION,ROTATED_DETECTION,DE │ - │ Value for filtering by task. Default is None, which shows all recipes. (type: None, default: None) │ - │ --pattern PATTERN This allows you to filter the model name of the recipe. For example, if you want to find all models that contain the word 'efficient', you can use '--pattern │ - │ efficient' (type: None, default: None) │ - │ │ - ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +************************* +Building workspace folder +************************* +``otx build`` creates a workspace with a particular model template and all the necessary components for training, evaluation, optimization, etc. This option is also used for modifying the backbone of the model. -Example to find ready-to-use templates for the detection task: +.. code-block:: -.. code-block:: shell - - (otx) ...$ otx find --task DETECTION - ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Task ┃ Model Name ┃ Recipe Path ┃ - ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ DETECTION │ yolox_tiny │ recipe/detection/yolox_tiny.yaml │ - │ DETECTION │ atss_mobilenetv2_tile │ recipe/detection/atss_mobilenetv2_tile.yaml │ - │ DETECTION │ openvino_model │ recipe/detection/openvino_model.yaml │ - │ DETECTION │ atss_mobilenetv2 │ recipe/detection/atss_mobilenetv2.yaml │ - │ DETECTION │ atss_resnext101 │ recipe/detection/atss_resnext101.yaml │ - │ DETECTION │ yolox_l_tile │ recipe/detection/yolox_l_tile.yaml │ - │ DETECTION │ ssd_mobilenetv2_tile │ recipe/detection/ssd_mobilenetv2_tile.yaml │ - │ DETECTION │ atss_r50_fpn │ recipe/detection/atss_r50_fpn.yaml │ - │ DETECTION │ yolox_tiny_tile │ recipe/detection/yolox_tiny_tile.yaml │ - │ DETECTION │ yolox_s │ recipe/detection/yolox_s.yaml │ - │ DETECTION │ yolox_s_tile │ recipe/detection/yolox_s_tile.yaml │ - │ DETECTION │ rtmdet_tiny │ recipe/detection/rtmdet_tiny.yaml │ - │ DETECTION │ yolox_x │ recipe/detection/yolox_x.yaml │ - │ DETECTION │ yolox_x_tile │ recipe/detection/yolox_x_tile.yaml │ - │ DETECTION │ ssd_mobilenetv2 │ recipe/detection/ssd_mobilenetv2.yaml │ - │ DETECTION │ yolox_l │ recipe/detection/yolox_l.yaml │ - └───────────┴───────────────────────┴─────────────────────────────────────────────┘ - -Example to find yolo named model for the detection task: - -.. code-block:: shell - - (otx) ...$ otx find --task DETECTION --pattern 'yolo*' - ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Task ┃ Model Name ┃ Recipe Path ┃ - ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ DETECTION │ yolox_tiny │ recipe/detection/yolox_tiny.yaml │ - │ DETECTION │ yolox_x │ recipe/detection/yolox_x.yaml │ - │ DETECTION │ yolox_l_tile │ recipe/detection/yolox_l_tile.yaml │ - │ DETECTION │ yolox_s │ recipe/detection/yolox_s.yaml │ - │ DETECTION │ yolox_l │ recipe/detection/yolox_l.yaml │ - │ DETECTION │ yolox_x_tile │ recipe/detection/yolox_x_tile.yaml │ - │ DETECTION │ yolox_s_tile │ recipe/detection/yolox_s_tile.yaml │ - │ DETECTION │ yolox_tiny_tile │ recipe/detection/yolox_tiny_tile.yaml │ - └───────────┴─────────────────┴───────────────────────────────────────┘ + (otx) ...$ otx build --help + usage: otx build [-h] [--train-data-roots TRAIN_DATA_ROOTS] [--val-data-roots VAL_DATA_ROOTS] [--test-data-roots TEST_DATA_ROOTS] [--unlabeled-data-roots UNLABELED_DATA_ROOTS] + [--unlabeled-file-list UNLABELED_FILE_LIST] [--task TASK] [--train-type TRAIN_TYPE] [--workspace WORKSPACE] [--model MODEL] [--backbone BACKBONE] + [template] + + positional arguments: + template Enter the path or ID or name of the template file. + This can be omitted if you have train-data-roots or run inside a workspace. + + optional arguments: + -h, --help show this help message and exit + --train-data-roots TRAIN_DATA_ROOTS + Comma-separated paths to training data folders. + --val-data-roots VAL_DATA_ROOTS + Comma-separated paths to validation data folders. + --test-data-roots TEST_DATA_ROOTS + Comma-separated paths to test data folders. + --unlabeled-data-roots UNLABELED_DATA_ROOTS + Comma-separated paths to unlabeled data folders + --unlabeled-file-list UNLABELED_FILE_LIST + Comma-separated paths to unlabeled file list + --task TASK The currently supported options: ('CLASSIFICATION', 'DETECTION', 'INSTANCE_SEGMENTATION', 'SEGMENTATION', 'ACTION_CLASSIFICATION', 'ACTION_DETECTION', 'ANOMALY_CLASSIFICATION', 'ANOMALY_DETECTION', 'ANOMALY_SEGMENTATION'). + --train-type TRAIN_TYPE + The currently supported options: dict_keys(['Incremental', 'Semisupervised', 'Selfsupervised']). + --workspace WORKSPACE Location where the workspace. + --model MODEL Enter the name of the model you want to use. (Ex. EfficientNet-B0). + --backbone BACKBONE Available Backbone Type can be found using 'otx find --backbone {framework}'. + If there is an already created backbone configuration yaml file, enter the corresponding path. + --deterministic Set deterministic to True, default=False. + --seed SEED Set seed for configuration. + + +For example, the following command line will create an object detection ``Custom_Object_Detection_Gen3_ATSS`` model template with ResNet backbone from `mmdetection `_: +To learn more about backbone replacement, please refer to the :doc:`following advanced tutorial <../tutorials/advanced/backbones>`. +.. code-block:: + (otx) ...$ otx build Custom_Object_Detection_Gen3_ATSS --backbone mmdet.ResNet --train-data-roots --val-data-roots ---------------- Dataset handling @@ -290,57 +136,164 @@ Then pass the path to ``coco_data_root`` to both root options: .. code-block:: - --data_root coco_data_root + --train-data-roots coco_data_root --val-data-roots coco_data_root +However, if you store your training set and validation separately - provide paths to both accordingly. +OpenVINO™ Training Extensions supports also auto-split functionality. If you don't have a prepared validation set - the Datumaro manager will run a random auto-split and will save the final dataset to ``splitted_dataset`` folder inside the workspace folder. This split can be further used for training. + +.. note:: + + Not all of the tasks support the auto-split feature. If the task isn't supported - unexpected behavior or errors may appear. Please, refer to :doc:`auto-configuration <../explanation/additional_features/auto_configuration>` documentation. + +If you have multiple annotation files like below, add additional argument (``--train-ann-files``). Then, you could use the annotation what you selected. +OpenVINO™ Training Extensions could randomly selects the train annotation file if you do not use additional argument (``--train-ann-files``) + +.. code-block:: + + coco_data_root + |---- annotations + |---- instances_train.json + |---- instances_train_1percent.json + |---- instances_train_10percent.json + |---- instances_val.json + |---- images + |---- train + |---- 000.jpg + .... + |---- val + |---- 000.jpg + .... + +.. code-block:: + + --train-data-roots coco_data_root --train-ann-files coco_data_root/annotations/instances_train_10percent.json + +.. note:: + + For now, only COCO format data could be used for direct annotation input ********* Training ********* -``otx train`` trains a model (a particular model template) on a dataset: - -The results will be saved in ``./otx-workspace/`` folder by default. The output folder can be modified by ``--work_dir`` option. These files are used by other commands: ``export``, ``test``, ``demo``, etc. +``otx train`` trains a model (a particular model template) on a dataset and saves results in two files: -``otx train`` receives ``--config`` as a argument. ``config`` can be a path to the specific ``*.yaml`` file. Also, the path to data root should be passed to the CLI to start training. +- ``weights.pth`` - a model snapshot +- ``label_schema.json`` - a label schema used in training, created from a dataset +The results will be saved in ``./outputs/`` folder by default. The output folder can be modified by ``--output`` option. These files are used by other commands: ``export``, ``eval``, ``demo``, etc. +``otx train`` receives ``template`` as a positional argument. ``template`` can be a path to the specific ``template.yaml`` file, template name or template ID. Also, the path to train and val data root should be passed to the CLI to start training. -Example of the command line to start training using Auto-Configuration: +However, if you created a workspace with ``otx build``, the training process can be started (in the workspace directory) just with ``otx train`` command without any additional options. OpenVINO™ Training Extensions will fetch everything else automatically. -.. code-block:: shell +.. code-block:: - (otx) ...$ otx train --data_root --task + otx train --help + usage: otx train [-h] [--train-data-roots TRAIN_DATA_ROOTS] [--val-data-roots VAL_DATA_ROOTS] [--unlabeled-data-roots UNLABELED_DATA_ROOTS] [--unlabeled-file-list UNLABELED_FILE_LIST] + [--load-weights LOAD_WEIGHTS] [--resume-from RESUME_FROM] [-o OUTPUT] [--workspace WORKSPACE] [--enable-hpo] [--hpo-time-ratio HPO_TIME_RATIO] [--gpus GPUS] + [--rdzv-endpoint RDZV_ENDPOINT] [--base-rank BASE_RANK] [--world-size WORLD_SIZE] [--mem-cache-size PARAMS.ALGO_BACKEND.MEM_CACHE_SIZE] [--data DATA] + [template] {params} ... + + positional arguments: + template Enter the path or ID or name of the template file. + This can be omitted if you have train-data-roots or run inside a workspace. + {params} sub-command help + params Hyper parameters defined in template file. + + optional arguments: + -h, --help show this help message and exit + --train-data-roots TRAIN_DATA_ROOTS + Comma-separated paths to training data folders. + --val-data-roots VAL_DATA_ROOTS + Comma-separated paths to validation data folders. + --unlabeled-data-roots UNLABELED_DATA_ROOTS + Comma-separated paths to unlabeled data folders + --unlabeled-file-list UNLABELED_FILE_LIST + Comma-separated paths to unlabeled file list + --train-type TRAIN_TYPE + The currently supported options: dict_keys(['Incremental', 'Semisupervised', 'Selfsupervised']). + --load-weights LOAD_WEIGHTS + Load model weights from previously saved checkpoint. + --resume-from RESUME_FROM + Resume training from previously saved checkpoint + -o OUTPUT, --output OUTPUT + Location where trained model will be stored. + --workspace WORKSPACE Location where the intermediate output of the training will be stored. + --enable-hpo Execute hyper parameters optimization (HPO) before training. + --hpo-time-ratio HPO_TIME_RATIO + Expected ratio of total time to run HPO to time taken for full fine-tuning. + --gpus GPUS Comma-separated indices of GPU. If there are more than one available GPU, then model is trained with multi GPUs. + --rdzv-endpoint RDZV_ENDPOINT + Rendezvous endpoint for multi-node training. + --base-rank BASE_RANK + Base rank of the current node workers. + --world-size WORLD_SIZE + Total number of workers in a worker group. + --mem-cache-size PARAMS.ALGO_BACKEND.MEM_CACHE_SIZE + Size of memory pool for caching decoded data to load data faster. For example, you can use digits for bytes size (e.g. 1024) or a string with size units (e.g. 7KiB = 7 * 2^10, 3MB = 3 * 10^6, and 2G = 2 * 2^30). + --deterministic Set deterministic to True, default=False. + --seed SEED Change seed for training. + --data DATA The data.yaml path want to use in train task. + + + +Example of the command line to start object detection training: -You can use the recipe configuration provided by OTX. The corresponding configuration file can be found via ``otx find``. +.. code-block:: -.. code-block:: shell + (otx) ...$ otx train SSD --train-data-roots --val-data-roots - (otx) ...$ otx train --config --data_root .. note:: - You also can visualize the training using ``Tensorboard`` as these logs are located in ``/tensorboard``. + You also can visualize the training using ``Tensorboard`` as these logs are located in ``/tf_logs``. .. note:: - ``--data.config.mem_cache_size`` provides in-memory caching for decoded images in main memory. + ``--mem-cache-size`` provides in-memory caching for decoded images in main memory. If the batch size is large, such as for classification tasks, or if your dataset contains high-resolution images, image decoding can account for a non-negligible overhead in data pre-processing. This option can be useful for maximizing GPU utilization and reducing model training time in those cases. If your machine has enough main memory, we recommend increasing this value as much as possible. - For example, you can cache approximately 10,000 of ``500x375~500x439`` sized images with ``--data.config.mem_cache_size 8GB``. + For example, you can cache approximately 10,000 of ``500x375~500x439`` sized images with ``--mem-cache-size=8GB``. It is also possible to start training by omitting the template and just passing the paths to dataset roots, then the :doc:`auto-configuration <../explanation/additional_features/auto_configuration>` will be enabled. Based on the dataset, OpenVINO™ Training Extensions will choose the task type and template with the best accuracy/speed trade-off. -You can override the configurable arguments. -For example, that is how you can change the max epochs and the batch size for the training: +You also can modify model template-specific parameters through the command line. To print all the available parameters the following command can be executed: + +.. code-block:: + + (otx) ...$ otx train TEMPLATE params --help + + + +For example, that is how you can change the learning rate and the batch size for the SSD model: + +.. code-block:: + + (otx) ...$ otx train SSD --train-data-roots \ + --val-data-roots \ + params \ + --learning_parameters.batch_size 16 \ + --learning_parameters.learning_rate 0.001 + +You could also enable storage caching to boost data loading at the expanse of storage: + +.. code-block:: + + (otx) ...$ otx train SSD --train-data-roots \ + --val-data-roots \ + params \ + --algo_backend.storage_cache_scheme JPEG/75 + +.. note:: + Not all templates support stroage cache. We are working on extending supported templates. -.. code-block:: shell - (otx) ...$ otx train ... --data.config.train_subset.batch_size --max_epochs +As can be seen from the parameters list, the model can be trained using multiple GPUs. To do so, you simply need to specify a comma-separated list of GPU indices after the ``--gpus`` argument. It will start the distributed data-parallel training with the GPUs you have specified. .. note:: - ``train``, ``test`` works based on ``lightning.Tranier``. You can change the Trainer component with the arguments of train and test. You can find more arguments in this documentation. - `Trainer _` + Multi-GPU training is currently supported for all tasks except for action tasks. We'll add support for them in the near future. ********** Exporting @@ -348,90 +301,142 @@ Exporting ``otx export`` exports a trained model to the OpenVINO™ IR format to efficiently run it on Intel hardware. -The command below performs exporting to the ``{work_dir}/`` path. +With the ``--help`` command, you can list additional information, such as its parameters common to all model templates: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx export ... --checkpoint + (otx) ...$ otx export --help + usage: otx export [-h] [--load-weights LOAD_WEIGHTS] [-o OUTPUT] [--workspace WORKSPACE] [--dump-features] [--half-precision] [template] -The command results in ``exported_model.xml``, ``exported_model.bin``. + positional arguments: + template Enter the path or ID or name of the template file. + This can be omitted if you have train-data-roots or run inside a workspace. -To use the exported model as an input for ``otx explain``, please dump additional outputs with internal information, using ``--explain``: + optional arguments: + -h, --help show this help message and exit + --load-weights LOAD_WEIGHTS + Load model weights from previously saved checkpoint. + -o OUTPUT, --output OUTPUT + Location where exported model will be stored. + --workspace WORKSPACE Location where the intermediate output of the export will be stored. + --dump-features Whether to return feature vector and saliency map for explanation purposes. + --half-precision This flag indicated if model is exported in half precision (FP16). -.. code-block:: shell - (otx) ...$ otx export ... --checkpoint --explain True +The command below performs exporting to the ``outputs/openvino`` path. +.. code-block:: -.. note:: - If ``.latest`` exists in work_dir, you can omit checkpoint and config. - You can also omit ``--work_dir`` if you run from the root of the workspace that contains ``.latest``. + (otx) ...$ otx export Custom_Object_Detection_Gen3_SSD --load-weights --output outputs/openvino + +The command results in ``openvino.xml``, ``openvino.bin`` and ``label_schema.json`` - .. code-block:: shell +To use the exported model as an input for ``otx explain``, please dump additional outputs with internal information, using ``--dump-features``: - (otx) ...$ otx export --work_dir +.. code-block:: - # OR if you are in the workspace root - (otx) ...$ otx export + (otx) ...$ otx export Custom_Object_Detection_Gen3_SSD --load-weights --output outputs/openvino/with_features --dump-features ************ Optimization ************ -``otx optimize`` optimizes a model using `PTQ `_ depending on the model and transforms it to ``INT8`` format. +``otx optimize`` optimizes a model using `NNCF `_ or `PTQ `_ depending on the model and transforms it to ``INT8`` format. +- NNCF optimization used for trained snapshots in a framework-specific format such as checkpoint (.pth) file from Pytorch - PTQ optimization used for models exported in the OpenVINO™ IR format -Command example for optimizing OpenVINO™ model (.xml) with OpenVINO™ PTQ: +With the ``--help`` command, you can list additional information: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx optimize ... --checkpoint \ - --data_root \ + usage: otx optimize [-h] [--train-data-roots TRAIN_DATA_ROOTS] [--val-data-roots VAL_DATA_ROOTS] [--load-weights LOAD_WEIGHTS] [-o OUTPUT] + [--workspace WORKSPACE] + [template] {params} ... + + positional arguments: + template Enter the path or ID or name of the template file. + This can be omitted if you have train-data-roots or run inside a workspace. + {params} sub-command help + params Hyper parameters defined in template file. + + optional arguments: + -h, --help show this help message and exit + --train-data-roots TRAIN_DATA_ROOTS + Comma-separated paths to training data folders. + --val-data-roots VAL_DATA_ROOTS + Comma-separated paths to validation data folders. + --load-weights LOAD_WEIGHTS + Load weights of trained model + -o OUTPUT, --output OUTPUT + Location where optimized model will be stored. + --workspace WORKSPACE Location where the intermediate output of the task will be stored. + +Command example for optimizing a PyTorch model (.pth) with OpenVINO™ NNCF: +.. code-block:: -Thus, to use PTQ pass the path to exported IR (.xml) model. + (otx) ...$ otx optimize SSD --load-weights \ + --train-data-roots \ + --val-data-roots \ + --output outputs/nncf -.. note:: - If ``.latest`` exists in work_dir, you can omit checkpoint and config. - You can also omit ``--work_dir`` if you run from the root of the workspace that contains ``.latest``. - .. code-block:: shell +Command example for optimizing OpenVINO™ model (.xml) with OpenVINO™ PTQ: + +.. code-block:: + + (otx) ...$ otx optimize SSD --load-weights \ + --val-data-roots \ + --output outputs/ptq - (otx) ...$ otx optimize --work_dir - # OR if you are in the workspace root - (otx) ...$ otx optimize +Thus, to use PTQ pass the path to exported IR (.xml) model, to use NNCF pass the path to the PyTorch (.pth) weights. + *********** Evaluation *********** -``otx test`` runs the evaluation of a model on the specific dataset. +``otx eval`` runs the evaluation of a model on the specific dataset. -The command below will evaluate the trained model on the provided dataset: +With the ``--help`` command, you can list additional information, such as its parameters common to all model templates: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx test ... --data_root \ - --checkpoint + (otx) ...$ otx eval --help + usage: otx eval [-h] [--test-data-roots TEST_DATA_ROOTS] [--load-weights LOAD_WEIGHTS] [-o OUTPUT] [--workspace WORKSPACE] [template] {params} ... -.. note:: + positional arguments: + template Enter the path or ID or name of the template file. + This can be omitted if you have train-data-roots or run inside a workspace. + {params} sub-command help + params Hyper parameters defined in template file. - It is possible to pass both PyTorch weights ``.pth`` or OpenVINO™ IR ``openvino.xml`` to ``--checkpoint`` option. + optional arguments: + -h, --help show this help message and exit + --test-data-roots TEST_DATA_ROOTS + Comma-separated paths to test data folders. + --load-weights LOAD_WEIGHTS + Load model weights from previously saved checkpoint. It could be a trained/optimized model (with PTQ only) or exported model. + -o OUTPUT, --output OUTPUT + Location where the intermediate output of the task will be stored. + --workspace WORKSPACE Path to the workspace where the command will run. -.. note:: - If ``.latest`` exists in work_dir, you can omit checkpoint and config. - You can also omit ``--work_dir`` if you run from the root of the workspace that contains ``.latest``. +The command below will evaluate the trained model on the provided dataset: - .. code-block:: shell +.. code-block:: - (otx) ...$ otx test --work_dir + (otx) ...$ otx eval SSD --test-data-roots \ + --load-weights \ + --output + +.. note:: + + It is possible to pass both PyTorch weights ``.pth`` or OpenVINO™ IR ``openvino.xml`` to ``--load-weights`` option. - # OR if you are in the workspace root - (otx) ...$ otx test *********** Explanation @@ -439,62 +444,149 @@ Explanation ``otx explain`` runs the explainable AI (XAI) algorithm on a specific model-dataset pair. It helps explain the model's decision-making process in a way that is easily understood by humans. +With the ``--help`` command, you can list additional information, such as its parameters common to all model templates: + +.. code-block:: + + (otx) ...$ otx explain --help + usage: otx explain [-h] --input INPUT [--output OUTPUT] --load-weights LOAD_WEIGHTS [--explain-algorithm EXPLAIN_ALGORITHM] [--overlay-weight OVERLAY_WEIGHT] [template] {params} ... + + positional arguments: + template Enter the path or ID or name of the template file. + This can be omitted if you have train-data-roots or run inside a workspace. + {params} sub-command help + params Hyper parameters defined in template file. + + optional arguments: + -h, --help show this help message and exit + -i INPUT, --input INPUT + Comma-separated paths to explain data folders. + -o OUTPUT, --output OUTPUT + Output path for explanation images. + --load-weights LOAD_WEIGHTS + Load model weights from previously saved checkpoint. + --explain-algorithm EXPLAIN_ALGORITHM + Explain algorithm name, currently support ['activationmap', 'eigencam', 'classwisesaliencymap']. For Openvino task, default method will be selected. + --process-saliency-maps PROCESS_SALIENCY_MAPS + Processing of saliency map includes (1) resizing to input image resolution and (2) applying a colormap. Depending on the number of targets to explain, this might take significant time. + --explain-all-classes EXPLAIN_ALL_CLASSES + Provides explanations for all classes. Otherwise, explains only predicted classes. This feature is supported by algorithms that can generate explanations per each class. + --overlay-weight OVERLAY_WEIGHT + Weight of the saliency map when overlaying the input image with saliency map. + The command below will generate saliency maps (heatmaps with red colored areas of focus) of the trained model on the provided dataset and save the resulting images to ``output`` path: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx explain --config \ - --checkpoint + (otx) ...$ otx explain SSD --input \ + --load-weights \ + --output \ + --explain-algorithm classwisesaliencymap \ + --overlay-weight 0.5 .. note:: It is possible to pass both PyTorch weights ``.pth`` or OpenVINO™ IR ``openvino.xml`` to ``--load-weights`` option. -By default, the model is exported to the OpenVINO™ IR format without extra feature information needed for the ``explain`` function. To use OpenVINO™ IR model in ``otx explain``, please first export it with ``--explain`` parameter: +By default, the model is exported to the OpenVINO™ IR format without extra feature information needed for the ``explain`` function. To use OpenVINO™ IR model in ``otx explain``, please first export it with ``--dump-features`` parameter: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx export ... --checkpoint \ - --explain True - (otx) ...$ otx explain ... --checkpoint outputs/openvino/with_features \ + (otx) ...$ otx export SSD --load-weights \ + --output outputs/openvino/with_features \ + --dump-features + (otx) ...$ otx explain SSD --input \ + --load-weights outputs/openvino/with_features \ + --output \ + --explain-algorithm classwisesaliencymap \ + --overlay-weight 0.5 -*********** -Workspace -*********** -If we run a typical Training example, will have a folder like the one below as output. +************* +Demonstration +************* -.. code-block:: bash +``otx demo`` runs model inference on images, videos, or webcam streams to show how it works with the user's data. - otx-workspace/ - .latest/ # Gather the most recent information. - train/ # Link to the output_dir where the most recent train was performed. - export/ # Link to the output_dir where the most recent export was performed. - .../ - 20240000_000000/ # Deliverables from OTX CLI - 20240000_000001/ # Deliverables from OTX CLI Second-Trial +.. note:: + + ``otx demo`` command requires GUI backend to your system for displaying inference results. + Only the OpenVINO™ IR model can be used for the ``otx demo`` command. -OTX considers the folder with ``.latest`` to be the root of the entire Workspace. -``.latest`` soft-links to the most recently trained output folder. +.. code-block:: -Case 1: If a user specifies an output ``work_dir`` (An already existing workspace) + (otx) ...$ otx demo --help + usage: otx demo [-h] -i INPUT --load-weights LOAD_WEIGHTS [--fit-to-size FIT_TO_SIZE FIT_TO_SIZE] [--loop] [--delay DELAY] [--display-perf] [--output OUTPUT] [template] {params} ... + + positional arguments: + template Enter the path or ID or name of the template file. + This can be omitted if you have train-data-roots or run inside a workspace. + {params} sub-command help + params Hyper parameters defined in template file. + + optional arguments: + -h, --help show this help message and exit + -i INPUT, --input INPUT + Source of input data: images folder, image, webcam and video. + --load-weights LOAD_WEIGHTS + Load model weights from previously saved checkpoint.It could be a trained/optimized model (with PTQ only) or exported model. + --fit-to-size FIT_TO_SIZE FIT_TO_SIZE + Width and Height space-separated values. Fits displayed images to window with specified Width and Height. This options applies to result visualisation only. + --loop Enable reading the input in a loop. + --delay DELAY Frame visualization time in ms. + --display-perf This option enables writing performance metrics on displayed frame. These metrics take into account not only model inference time, but also frame reading, pre-processing and post-processing. + --output OUTPUT + Output path to save input data with predictions. + +Command example of the demonstration: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx train --work_dir otx-workspace + (otx) ...$ otx demo SSD --input INPUT \ + --load-weights \ + --display-perf \ + --delay 1000 -This will then use the ``.latest`` in the otx-workspace for training. +Input can be a folder with images, a single image, a webcam ID or a video. The inference results of a model will be displayed to the GUI window with a 1-second interval. + +.. note:: + + If you execute this command from the remote environment (e.g., using text-only SSH via terminal) without having remote GUI client software, you can meet some error messages from this command. -Case 2: if a user executes a command from within the otx-workspace -.. code-block:: shell +*********** +Deployment +*********** + +``otx deploy`` creates ``openvino.zip`` with a self-contained python package, a demo application, and an exported model. As follows from the zip archive name, the ``deploy`` can be used only with the OpenVINO™ IR model. + +With the ``--help`` command, you can list additional information, such as its parameters common to all model templates: + +.. code-block:: + + (otx) ...$ otx deploy --help + usage: otx deploy [-h] [--load-weights LOAD_WEIGHTS] [-o OUTPUT] [template] + + positional arguments: + template Enter the path or ID or name of the template file. + This can be omitted if you have train-data-roots or run inside a workspace. + + optional arguments: + -h, --help show this help message and exit + --load-weights LOAD_WEIGHTS + Load model weights from previously saved checkpoint. + -o OUTPUT, --output OUTPUT + Location where openvino.zip will be stored. + + +Command example: + +.. code-block:: - cd otx-workspace + (otx) ...$ otx deploy SSD --load-weights \ + --output outputs/deploy - (otx) ...$ otx train # Behave in the same way as the first training - (otx) ...$ otx test # Perform a test with the config and checkpoint from the last training baseline. - (otx) ...$ otx export # Perform a export with the config and checkpoint from the last training baseline. diff --git a/docs/source/guide/get_started/installation.rst b/docs/source/guide/get_started/installation.rst index fd4ce0d08f6..61c730fc9c0 100644 --- a/docs/source/guide/get_started/installation.rst +++ b/docs/source/guide/get_started/installation.rst @@ -8,8 +8,14 @@ Prerequisites The current version of OpenVINO™ Training Extensions was tested in the following environment: - Ubuntu 20.04 -- Python >= 3.10 +- Python 3.8 ~ 3.10 +- (Optional) To use the NVidia GPU for the training: `CUDA Toolkit 11.7 `_ +.. note:: + + If using CUDA, make sure you are using a proper driver version. To do so, use ``ls -la /usr/local | grep cuda``. + + If necessary, `install CUDA 11.7 `_ (requires 'sudo' permission) and select it with ``export CUDA_HOME=/usr/local/cuda-11.7``. *********************************************** Install OpenVINO™ Training Extensions for users @@ -18,7 +24,7 @@ Install OpenVINO™ Training Extensions for users 1. Clone the training_extensions repository with the following command: -.. code-block:: shell +.. code-block:: git clone https://github.com/openvinotoolkit/training_extensions.git cd training_extensions @@ -27,7 +33,7 @@ repository with the following command: 2. Set up a virtual environment. -.. code-block:: shell +.. code-block:: # Create virtual env. python -m venv .otx @@ -35,32 +41,38 @@ virtual environment. # Activate virtual env. source .otx/bin/activate -3. Install OpenVINO™ Training Extensions package from either: +3. Install PyTorch according to your system environment. +Refer to the `official installation guide `_ -* A local source in development mode +.. note:: -.. code-block:: shell + Currently, only torch==1.13.1 ~ 2.0.1 have been fully validated. + (Older versions are not supported due to the security issues. Newer versions might not work correctly) - pip install -e . +.. code-block:: -* PyPI + # Install command for torch==2.0.1 for CUDA 11.7: + pip install torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/cu117 -.. code-block:: shell + # Or, install command for torch==1.13.1 for CUDA 11.7: + pip install torch==1.13.1 torchvision==0.14.1 --extra-index-url https://download.pytorch.org/whl/cu117 - pip install otx + # On CPU only systems: + pip install torch==1.13.1 torchvision==0.14.1 --extra-index-url https://download.pytorch.org/whl/cpu -4. Install PyTorch & Requirements for training according to your system environment. +4. Install OpenVINO™ Training Extensions package from either: -.. code-block:: shell +* A local source in development mode - otx install -v +.. code-block:: -[Optional] Refer to the `official installation guide `_ + pip install -e .[full] -.. note:: +* PyPI - Currently, only torch==2.1.1 was fully validated. (older versions are not supported due to security issues). +.. code-block:: + pip install otx[full] 5. Once the package is installed in the virtual environment, you can use full OpenVINO™ Training Extensions command line functionality. @@ -71,11 +83,11 @@ Install OpenVINO™ Training Extensions for developers Install ``tox`` and create a development environment: -.. code-block:: shell +.. code-block:: pip install tox # -- need to replace '310' below if another python version needed - tox devenv venv/otx -e unit-test-py310 + tox devenv venv/otx -e tests-all-py310 source venv/otx/bin/activate Then you may change code, and all fixes will be directly applied to the editable package. @@ -84,29 +96,46 @@ Then you may change code, and all fixes will be directly applied to the editable Install OpenVINO™ Training Extensions by using Docker ***************************************************** -1. By executing the following commands, it will build two Docker images: ``otx:${OTX_VERSION}-cuda`` and ``otx:${OTX_VERSION}-cuda-pretrained-ready``. +To build a docker image with Python 3.9, run a command below from the working copy of the OpenVINO training extensions. -.. code-block:: shell +.. code-block:: - git clone https://github.com/openvinotoolkit/training_extensions.git - cd docker - ./build.sh + # build a docker image (otx/cpu/python3.9:latest) with Python 3.9 (default) + training_extensions$ ./docker/build.sh + # or, with other version of Python e.g., 3.10 + training_extensions$ ./docker/build.sh --python 3.10 -2. After that, you can check whether the images are built correctly such as +.. note:: -.. code-block:: shell + When the docker image build script completed successfully, the image will be named and tagged as `otx/cpu/python:latest`. + You can check it using the command `docker images` on the terminal. - docker image ls | grep otx +To start the OpenVINO training extensions container using the image built in above, run a command below. -Example: +.. code-block:: -.. code-block:: shell + # start a container from `otx/cpu/python3.9:latest' image. + $ docker run \ + -it \ # enter interactive terminal + --rm \ # remove container after use + -v "$(pwd):/mnt/shared:rw" \ # mount current folder on host machine to the container + --shm-size=4g \ # increase mounted shared memory + otx/cpu/python3.9:latest # name of the docker image to be used to create container - otx 2.0.0-cuda-pretrained-ready 4f3b5f98f97c 3 minutes ago 14.5GB - otx 2.0.0-cuda 8d14caccb29a 8 minutes ago 10.4GB +Enjoy OpenVINO training extensions! +.. code-block:: -``otx:${OTX_VERSION}-cuda`` is a minimal Docker image where OTX is installed with CUDA supports. On the other hand, ``otx:${OTX_VERSION}-cuda-pretrained-ready`` includes all the model pre-trained weights that OTX provides in addition to ``otx:${OTX_VERSION}-cuda``. + # find all templates for the classification task + root@fc01132c3753:/training_extensions# otx find --task classification + +----------------+---------------------------------------------------+-----------------------+---------------------------------------------------------------------------------------+ + | TASK | ID | NAME | BASE PATH | + +----------------+---------------------------------------------------+-----------------------+---------------------------------------------------------------------------------------+ + | CLASSIFICATION | Custom_Image_Classification_DeiT-Tiny | DeiT-Tiny | src/otx/algorithms/classification/configs/deit_tiny/template.yaml | + | CLASSIFICATION | Custom_Image_Classification_EfficinetNet-B0 | EfficientNet-B0 | src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/template.yaml | + | CLASSIFICATION | Custom_Image_Classification_EfficientNet-V2-S | EfficientNet-V2-S | src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/template.yaml | + | CLASSIFICATION | Custom_Image_Classification_MobileNet-V3-large-1x | MobileNet-V3-large-1x | src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/template.yaml | + +----------------+---------------------------------------------------+-----------------------+---------------------------------------------------------------------------------------+ ********* Run tests @@ -115,9 +144,9 @@ Run tests To run some tests, need to have development environment on your host. The development requirements file (requirements/dev.txt) would be used to setup them. -.. code-block:: shell +.. code-block:: - $ otx install --option dev + $ pip install -r requirements/dev.txt $ pytest tests/ Another option to run the tests is using the testing automation tool `tox `_. Following commands will install @@ -126,7 +155,7 @@ the tool ``tox`` to your host and run all test codes inside of ``tests/`` folder .. code-block:: $ pip install tox - $ tox -e tests-all-py310 -- tests/ + $ tox -e tests-all-py310-pt1 -- tests/ .. note:: @@ -147,11 +176,24 @@ please update pip version by following command: 2. If you're facing a problem with ``torch`` or ``mmcv`` installation, please check that your CUDA version is compatible with torch version. Consider updating CUDA and CUDA drivers if needed. -Check the `command example `_ to install CUDA 11.8 with drivers on Ubuntu 20.04. +Check the `command example `_ to install CUDA 11.7 with drivers on Ubuntu 20.04. -3. If you have access to the Internet through the proxy server only, +3. If you use Anaconda environment, you should consider that OpenVINO has limited `Conda support `_ for Python 3.6 and 3.7 versions only. +So to use these python versions, please use other tools to create the environment (like ``venv`` or ``virtualenv``) and use ``pip`` as a package manager. + +4. If you have access to the Internet through the proxy server only, please use pip with proxy call as demonstrated by command below: -.. code-block:: shell +.. code-block:: python -m pip install --proxy http://:@: + +5. If you get ``mmcv`` kernel compilation error message, e.g. ModuleNotFoundEffor: no module named 'mmcv._ext', +please try to delete the pre-compiled MMCV wheel from the cache directory, and then try again. +Then the kernels would be compiled on your environment. + +.. code-block:: + + find ~/.cache/pip/wheels/ -name "mmcv*" -delete + pip uninstall mmcv-full + pip install otx[full] # pip install -e .[full] diff --git a/docs/source/guide/get_started/introduction.rst b/docs/source/guide/get_started/introduction.rst index 073e9f83f2d..048eb99f80d 100644 --- a/docs/source/guide/get_started/introduction.rst +++ b/docs/source/guide/get_started/introduction.rst @@ -31,6 +31,8 @@ OpenVINO™ Training Extensions supports the following computer vision tasks: OpenVINO™ Training Extensions supports the :doc:`following learning methods <../explanation/algorithms/index>`: - **Supervised**, incremental training, which includes class incremental scenario and contrastive learning for classification and semantic segmentation tasks +- **Semi-supervised learning** +- **Self-supervised learning** OpenVINO™ Training Extensions will provide the :doc:`following features <../explanation/additional_features/index>` in coming releases: @@ -44,74 +46,16 @@ OpenVINO™ Training Extensions will provide the :doc:`following features <../ex Documentation content ********************* -1. :octicon:`light-bulb` **Quick start guide**: - - .. grid:: - :gutter: 1 - - .. grid-item-card:: Installation Guide - :link: installation - :link-type: doc - :text-align: center - - .. grid-item-card:: API Quick-Start - :link: api_tutorial - :link-type: doc - :text-align: center - - .. grid-item-card:: CLI Commands - :link: cli_commands - :link-type: doc - :text-align: center - -2. :octicon:`book` **Tutorials**: - - .. grid:: 1 2 2 3 - :margin: 1 1 0 0 - :gutter: 1 - - .. grid-item-card:: Classification - :link: ../tutorials/base/how_to_train/classification - :link-type: doc - :text-align: center - - .. grid-item-card:: Detection - :link: ../tutorials/base/how_to_train/detection - :link-type: doc - :text-align: center - - .. grid-item-card:: Instance Segmentation - :link: ../tutorials/base/how_to_train/instance_segmentation - :link-type: doc - :text-align: center - - .. grid-item-card:: Semantic Segmentation - :link: ../tutorials/base/how_to_train/semantic_segmentation - :link-type: doc - :text-align: center - - .. grid-item-card:: Anomaly Task - :link: ../tutorials/base/how_to_train/anomaly_detection - :link-type: doc - :text-align: center - - .. grid-item-card:: Action Classification - :link: ../tutorials/base/how_to_train/action_classification - :link-type: doc - :text-align: center - - .. grid-item-card:: Action Detection - :link: ../tutorials/base/how_to_train/action_detection - :link-type: doc - :text-align: center - - .. grid-item-card:: Visual Prompting - :text-align: center - - .. grid-item-card:: Advanced - :link: ../tutorials/advanced/index - :link-type: doc - :text-align: center +1. **Quick start guide**: + + 1. Installation + 2. All possible OpenVINO™ Training Extensions CLI commands + +2. **Tutorials**: + + This section reveals tutorials on how to use CLI for every supported task and training type. + It provides the end-to-end solution from installation to model deployment and demo visualization on specific examples for each of the supported tasks. + In the advanced section tutorial on how to use APIs instead of CLI is presented. 3. **Explanation section**: diff --git a/docs/source/guide/index.rst b/docs/source/guide/index.rst index 73ca70741f2..504684ce8e0 100644 --- a/docs/source/guide/index.rst +++ b/docs/source/guide/index.rst @@ -10,7 +10,6 @@ Guide get_started/introduction get_started/installation get_started/cli_commands - get_started/api_tutorial .. toctree:: diff --git a/docs/source/guide/tutorials/advanced/api_tutorial.rst b/docs/source/guide/tutorials/advanced/api_tutorial.rst new file mode 100644 index 00000000000..984bdd885b0 --- /dev/null +++ b/docs/source/guide/tutorials/advanced/api_tutorial.rst @@ -0,0 +1,307 @@ +Utilize OpenVINO™ Training Extensions APIs in your project +========================================================== + +Besides CLI functionality, The OpenVINO™ Training Extension provides APIs that help developers to integrate OpenVINO™ Training Extensions models into their projects. +This tutorial intends to show how to create a dataset, model and use all of the CLI functionality through APIs. + +For demonstration purposes we will use the Object Detection SSD model with `WGISD `_ public dataset as we did for the :doc:`CLI tutorial <../base/how_to_train/detection>`. + +.. note:: + + To start with we need to `install OpenVINO™ Training Extensions `_. + +******************* +Dataset preparation +******************* + +1. Clone a repository +with `WGISD dataset `_. + +.. code-block:: + + cd data + git clone https://github.com/thsant/wgisd.git + cd wgisd + git checkout 6910edc5ae3aae8c20062941b1641821f0c30127 + +2. We need to rename annotations to +be distinguished by OpenVINO™ Training Extensions Datumaro manager: + +.. code-block:: + + mv data images && mv coco_annotations annotations && mv annotations/train_bbox_instances.json instances_train.json && mv annotations/test_bbox_instances.json instances_val.json + +Now it is all set to use this dataset inside OpenVINO™ Training Extensions + +********************************** +Model template and dataset loading +********************************** + +Let's import the necessary modules: + +.. code-block:: + + import cv2 + import numpy as np + + from otx.api.configuration.helper import create as create_parameters_from_parameters_schema + from otx.api.entities.inference_parameters import InferenceParameters + from otx.api.entities.model import ModelEntity + from otx.api.entities.resultset import ResultSetEntity + from otx.api.entities.subset import Subset + from otx.api.entities.task_environment import TaskEnvironment + from otx.api.usecases.tasks.interfaces.export_interface import ExportType + from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType + from otx.api.entities.optimization_parameters import OptimizationParameters + from otx.core.data.adapter import get_dataset_adapter + from otx.cli.registry import Registry + from otx.cli.utils.importing import get_impl_class + from otx.cli.utils.io import read_label_schema, read_model + from otx.cli.tools.utils.demo.visualization import draw_predictions + +We will use the SSD object detection model in that tutorial. Let's initiate the model template: + +.. code-block:: + + templates_dir = 'src/otx/algorithms' + registry = Registry(templates_dir) + model_template = registry.get('Custom_Object_Detection_Gen3_SSD') + +Derive hyperparameters from the model template. We can change essential hyperparameters for your configuration: + +.. code-block:: + + hyper_parameters = model_template.hyper_parameters.data + hyper_parameters = create_parameters_from_parameters_schema(hyper_parameters) + # print hyperparameters to see which one is available to modify + for p in hyper_parameters.learning_parameters.parameters: + print(f'{p}: {getattr(hyper_parameters.learning_parameters, p)}') + + # Let's modify the batch size and number of epochs to train + hyper_parameters.learning_parameters.batch_size = 8 + hyper_parameters.learning_parameters.num_iters = 5 + +The next step is to set up a dataset: + +.. code-block:: + + dataset_adapter = get_dataset_adapter(task_type = model_template.task_type, + # set a path to the root folder of the wgisd repository + train_data_roots="./wgisd", + val_data_roots="./wgisd", + test_data_roots="./wgisd") + dataset, labels_schema = dataset_adapter.get_otx_dataset(), dataset_adapter.get_label_schema() + + +*********************************** +Set up the environment and the task +*********************************** + +.. code-block:: + + Task = get_impl_class(model_template.entrypoints.base) + + environment = TaskEnvironment( + model=None, + hyper_parameters=hyper_parameters, + label_schema=labels_schema, + model_template=model_template) + + task = Task(task_environment=environment) + +***************************** +Training, Validation, Export +***************************** + +Create an output model and start actual training: + +.. code-block:: + + output_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + + task.train(dataset, output_model) + +To perform validation we need to infer our model on the validation dataset, create ``ResultSetEntity`` and save to this entity inference results: + +.. code-block:: + + validation_dataset = dataset.get_subset(Subset.VALIDATION) + + predicted_validation_dataset = task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True)) + + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + + task.evaluate(resultset) + + # print or save validation results + print(resultset.performance) + +To export our model to OpenVINO™ IR format we need to create output model and run exporting task. +To validate the OpenVINO™ IR model, we need to create an openvino task first and then run the evaluation procedure: + +.. code-block:: + + exported_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.export(ExportType.OPENVINO, exported_model) + + # substitute the model in the environment with exported_model + environment.model = exported_model + + # create an openvino task + ov_task = get_impl_class(model_template.entrypoints.openvino)(environment) + + # validation + predicted_validation_dataset = ov_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True)) + + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + ov_task.evaluate(resultset) + + # print or save the result + print(resultset.performance) + +************ +Optimization +************ + +To run the optimization with POT on the OpenVINO™ IR model, we need to create an output model and run the optimization procedure: + +.. code-block:: + + optimized_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + + ov_task.optimize( + OptimizationType.POT, + dataset, + optimized_model, + OptimizationParameters()) + +To run the NNCF accuracy-aware training, return model in the environment back, create NNCF task, output model and run optimization procedure: + +.. code-block:: + + # return PyTorch model back + environment.model = output_model + + # create an NNCF task based on our environment + nncf_task = get_impl_class(model_template.entrypoints.nncf)(environment) + + # create output model + optimized_nncf_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + + nncf_task.optimize( + OptimizationType.NNCF, + dataset, + optimized_nncf_model, + OptimizationParameters()) + +You can validate the optimized model as the usual model. For example for the NNCF model it will look like this: + +.. code-block:: + + # NNCF task inference + predicted_validation_dataset = nncf_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True)) + + # ResultSetEntity creating with optimized_nncf_model + resultset = ResultSetEntity( + model=optimized_nncf_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + + # evaluation + nncf_task.evaluate(resultset) + + # print or save the result + print(resultset.performance) + + +************************************** +Load the model and use it for any data +************************************** + +Let's assume, that we have already trained the model and we want to use it in our project. The simple steps on how to load the model and infer it on custom images are presented below. + +.. code-block:: + + # path to the trained OpenVINO™ Training Extensions weights, can be PyTorch .pth or OpenVINO™ IR .xml + weights_path = "path/to/trained/weights" + + # create new environment + environment = TaskEnvironment( + model=None, + hyper_parameters=hyper_parameters, + label_schema=read_label_schema(weights_path), + model_template=template, + ) + + # read the model and assign it to our environment + environment.model = read_model(environment.get_model_configuration(), weights_path, None) + + # create task + task_class = (get_impl_class(template.entrypoints.openvino) + if weights_path.endswith(".xml") + else get_impl_class(template.entrypoints.base)) + + task = task_class(task_environment=environment) + +Open some images, convert them to a small dataset, infer and get the annotations from our model. +We can convert these steps to function and use it in a loop with multiple images/frames from video: + +.. code-block:: + + def get_predictions(task, frame): + """Returns list of predictions made by task on a frame.""" + + empty_annotation = AnnotationSceneEntity(annotations=[], kind=AnnotationSceneKind.PREDICTION) + + item = DatasetItemEntity( + media=Image(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)), + annotation_scene=empty_annotation, + ) + + dataset = DatasetEntity(items=[item]) + + start_time = time.perf_counter() + predicted_validation_dataset = task.infer( + dataset, + InferenceParameters(is_evaluation=True), + ) + elapsed_time = time.perf_counter() - start_time + item = predicted_validation_dataset[0] + return item.get_annotations(), elapsed_time + + for img in images_list: + # use our function to get predictions + predictions = get_predictions(task, img) + + # we also can draw predictions on the image and visualize the result + img = draw_predictions(template.task_type, predictions, img, args.fit_to_size) + + +That's it. Now, we can use OpenVINO™ Training Extensions APIs to create, train, and deploy deep learning models using the OpenVINO™ Training Extension. \ No newline at end of file diff --git a/docs/source/guide/tutorials/advanced/backbones.rst b/docs/source/guide/tutorials/advanced/backbones.rst new file mode 100644 index 00000000000..f5a3bb93f4c --- /dev/null +++ b/docs/source/guide/tutorials/advanced/backbones.rst @@ -0,0 +1,157 @@ +Backbone Replacement +================================ + +This tutorial describes an example of how to find an available backbone and how it can be replaced in OpenVINO™ Training Extensions. + +The process has been tested on the following configuration. + +- Ubuntu 20.04 +- NVIDIA GeForce RTX 3090 +- Intel(R) Core(TM) i9-11900 +- CUDA Toolkit 11.1, python 3.9 + +***************************** +Currently supported backbones +***************************** + +The following libraries are currently available for backbone replacement. + ++-----------------------+-------+-------+-------------+-----------+-----------+ +| Task | mmdet | mmseg | torchvision | pytorchcv | omz.mmcls | ++=======================+=======+=======+=============+===========+===========+ +| Classification | O | O | O | O | O | ++-----------------------+-------+-------+-------------+-----------+-----------+ +| Detection | O | O | O | O | | ++-----------------------+-------+-------+-------------+-----------+-----------+ +| Segmentation | O | O | O | O | | ++-----------------------+-------+-------+-------------+-----------+-----------+ +| Instance-Segmentation | O | O | O | O | | ++-----------------------+-------+-------+-------------+-----------+-----------+ + +************************* +Setup virtual environment +************************* + +1. You can follow the installation process from a :doc:`quick start guide <../../get_started/installation>` +to create a universal virtual environment for OpenVINO™ Training Extensions. + +2. Activate your virtual +environment: + +.. code-block:: + + .otx/bin/activate + # or by this line, if you created an environment, using tox + . venv/otx/bin/activate + +***************************** +Backbone replacement tutorial +***************************** + +1. First, we need to configure the workspace +for the backbone replacement: + +.. note:: + + You can use the OpenVINO™ Training Extensions workspace to swap out backbones, train, set up configurations, and more. + Workspaces are created automatically on ``otx build`` or ``otx train``. + +.. code-block:: + + (otx) ...$ otx build --task classification + + [*] Workspace Path: otx-workspace-CLASSIFICATION + [*] Load Model Template ID: Custom_Image_Classification_EfficinetNet-B0 + [*] Load Model Name: EfficientNet-B0 + [*] - Updated: otx-workspace-CLASSIFICATION/model.py + [*] - Updated: otx-workspace-CLASSIFICATION/data_pipeline.py + [*] - Updated: otx-workspace-CLASSIFICATION/deployment.py + [*] - Updated: otx-workspace-CLASSIFICATION/hpo_config.yaml + [*] - Updated: otx-workspace-CLASSIFICATION/model_hierarchical.py + [*] - Updated: otx-workspace-CLASSIFICATION/model_multilabel.py + [*] - Updated: otx-workspace-CLASSIFICATION/compression_config.json + [*] Update data configuration file to: otx-workspace-CLASSIFICATION/data.yaml + + (otx) ...$ cd otx-workspace-CLASSIFICATION + +2. Next, we can find the backbone +we want to replace via ``otx find``: + +.. note:: + + We can use ``otx find`` to find templates and available backbones. + Each backbone may have a required argument. If the backbone has options for required arguments, ``otx build`` provide the first option as default. + +.. code-block:: + + (otx) ...$ otx find --backbone mmdet + + +-------+-------------------------+---------------+-------------------------------+ + | Index | Backbone Type | Required-Args | Options | + +-------+-------------------------+---------------+-------------------------------+ + | 1 | mmdet.RegNet | arch | regnetx_400mf, regnetx_800mf, | + | | | | regnetx_1.6gf, regnetx_3.2gf, | + | | | | regnetx_4.0gf, regnetx_6.4gf, | + | | | | regnetx_8.0gf, regnetx_12gf | + | 2 | mmdet.ResNet | depth | 18, 34, 50, 101, 152 | + | 3 | mmdet.ResNetV1d | depth | 18, 34, 50, 101, 152 | + | 4 | mmdet.ResNeXt | depth | 50, 101, 152 | + | 5 | mmdet.SSDVGG | input_size | 300, 512 | + | | | depth | 11, 16, 19 | + | 6 | mmdet.HRNet | extra | | + | 7 | mmdet.Res2Net | depth | 50, 101, 152 | + | 8 | mmdet.DetectoRS_ResNet | depth | 50, 101, 152 | + | 9 | mmdet.DetectoRS_ResNeXt | depth | 50, 101, 152 | + | 10 | mmdet.Darknet | | | + | 11 | mmdet.ResNeSt | depth | 50, 101, 152, 200 | + | 12 | mmdet.CSPDarknet | | | + +-------+-------------------------+---------------+-------------------------------+ + +3. We need to run the command below to replace +the backbone: + +In this example, we'll replace the classification model using the default EfficientNet with ``mmdet.ResNet``. +You can use the ``Backbone Type`` in the table output from ``otx find --backbone`` to use a different backbone. + +.. code-block:: + + (otx) ...$ otx build --backbone mmdet.RegNet + + [*] Backbone Config: mmdet.RegNet + [*] mmdet.RegNet requires the argument : ['arch'] + [*] Please refer to /venv/lib/python3.9/site-packages/mmdet/models/backbones/regnet.py + [*] 'arch' can choose between: ['regnetx_400mf', 'regnetx_800mf', 'regnetx_1.6gf', 'regnetx_3.2gf', 'regnetx_4.0gf', 'regnetx_6.4gf', 'regnetx_8.0gf', 'regnetx_12gf'] + [*] 'arch' default value: regnetx_400mf + [*] Save backbone configuration: otx-workspace-CLASSIFICATION/backbone.yaml + [*] Update model.py with backbone.yaml + Target Model: SAMImageClassifier + Target Backbone: mmdet.RegNet + Backbone config: {'arch': 'regnetx_400mf', 'avg_down': False, 'base_channels': 32, 'conv_cfg': None, 'dcn': None, 'deep_stem': False, 'dilations': (1, 1, 1, 1), 'frozen_stages': -1, 'in_channels': 3, 'init_cfg': None, 'norm_cfg': {'requires_grad': True, 'type': 'BN'}, 'norm_eval': True, 'out_indices': (0, 1, 2, 3), 'plugins': None, 'pretrained': None, 'stage_with_dcn': (False, False, False, False), 'stem_channels': 32, 'strides': (2, 2, 2, 2), 'style': 'pytorch', 'type': 'mmdet.RegNet', 'with_cp': False, 'zero_init_residual': True} + [*] Save model configuration: model.py + +Then we get ``model.py``, which has been changed to ``mmdet.ResNet``. + +.. note:: + + If you get a log like this, follow the steps below: + + .. code-block:: + + [!] mmseg.HRNet backbone has inputs that the user must enter. + [!] Edit backbone.yaml and run 'otx build --backbone backbone.yaml'. + + Please modify the available configuration file directly (``backbone.yaml``). + + You can then update the model with the command below: + + .. code-block:: + + (otx) ...$ otx build --backbone backbone.yaml + +4. After that, you can use any other OpenVINO™ Training Extensions command with the +new model: :doc:`quick start guide <../../get_started/installation>` + +You can use the backbones provided by ``mmdet``, ``mmseg``, ``torchvision``, and ``omz.mmcls`` in the same way as above. + +.. warning:: + Depending on your backbone, your data may require multiple hyperparameter optimizations. Custom models, except for TEMPLATE, are not yet guaranteed to be accurate. diff --git a/docs/source/guide/tutorials/advanced/configuration.rst b/docs/source/guide/tutorials/advanced/configuration.rst deleted file mode 100644 index e4aad3a5fac..00000000000 --- a/docs/source/guide/tutorials/advanced/configuration.rst +++ /dev/null @@ -1,94 +0,0 @@ -How to write OTX Configuration (recipe) -========================================== - -*************** -Configuration -*************** - -Example of ``recipe/classification/multi_class_cls`` - -.. code-block:: yaml - - model: - class_path: otx.algo.classification.mobilenet_v3_large.MobileNetV3ForMulticlassCls - init_args: - num_classes: 1000 - light: True - - optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0058 - momentum: 0.9 - weight_decay: 0.0001 - - scheduler: - class_path: otx.algo.schedulers.WarmupReduceLROnPlateau - init_args: - warmup_steps: 10 - mode: max - factor: 0.5 - patience: 1 - monitor: val/accuracy - - engine: - task: MULTI_CLASS_CLS - device: auto - - callback_monitor: val/accuracy - data: ../../_base_/data/mmpretrain_base.yaml - -We can use the ``~.yaml`` with the above values configured. - -- ``engine`` -- ``model``, ``optimizer``, ``scheduler`` -- ``data`` -- ``callback_monitor`` - -The basic configuration is the same as the configuration configuration format for jsonargparse. -`Jsonargparse Documentation _` - -### Configuration overrides - -Here we provide a feature called ``overrides``. - -.. code-block:: yaml - - ... - - overrides: - data: - config: - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - direction: horizontal - prob: 0.5 - type: RandomFlip - - type: PackInputs - ... - -This feature allows you to override the values need from the default configuration. -You can see the final configuration with the command below. - -.. code-block:: shell - - $ otx train --config --print_config - -### Callbacks & Logger overrides - -``callbacks`` and ``logger`` can currently be provided as a list of different callbacks and loggers. The way to override this is as follows. - -For example, if you want to change the patience of EarlyStopping, you can configure the overrides like this - -.. code-block:: yaml - - overrides: - ... - callbacks: - - class_path: ligthning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 diff --git a/docs/source/guide/tutorials/advanced/hpo_tutorial.rst b/docs/source/guide/tutorials/advanced/hpo_tutorial.rst new file mode 100644 index 00000000000..2227659c9b8 --- /dev/null +++ b/docs/source/guide/tutorials/advanced/hpo_tutorial.rst @@ -0,0 +1,110 @@ +Simple HPO Tutorial +============================ + +This tutorial provides a step-by-step guide on how to use Hyper-Parameter Optimization (HPO) for classification tasks. +In this tutorial, we will optimize the learning rate and batch size using HPO. + +************************* +Setup virtual environment +************************* + +1. You can follow the installation process from a :doc:`quick start guide <../../get_started/installation>` +to create a universal virtual environment for OpenVINO™ Training Extensions. + +2. Activate your virtual +environment: + +.. code-block:: + + .otx/bin/activate + # or by this line, if you created an environment, using tox + . venv/otx/bin/activate + + +************************* +Build workspace +************************* + +First, let's build a workspace. You can do this by running the following command: + +.. code-block:: + + (otx) ...$ otx build --train-data-roots data/flower_photos --model MobileNet-V3-large-1x + + [*] Load Model Template ID: Custom_Image_Classification_MobileNet-V3-large-1x + [*] Load Model Name: MobileNet-V3-large-1x + [*] Saving data configuration file to: ./otx-workspace-CLASSIFICATION/data.yaml + + (otx) ...$ cd ./otx-workspace-CLASSIFICATION + +.. note:: + + This is copied from :doc:`../../tutorials/base/how_to_train/classification`. + You can find more detail explanation from it. + +************************* +Set hpo_config.yaml +************************* + +Before running HPO, you can configure HPO using the ``hpo_config.yaml`` file. +This file contains all the information that the HPO module needs, including the hyperparameters that you want to optimize. +The file is located in the workspace you have made and comes with default values. + +Here's the default hpo_config.yaml: + +.. code-block:: + + metric: accuracy + search_algorithm: asha + hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0007 + - 0.07 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 32 + - 128 + - 2 + +Although this default configuration can be used for HPO, the search space for the learning rate is too wide. +Therefore, we will modify the configuration file slightly to make the search space more reasonable. You can easily modify the configuration file to optimize different hyperparameters. + +Here's the updated ``hpo_config.yaml``: + +.. code-block:: + + ... + ... + ... + learning_parameters.learning_rate: + param_type: quniform + range: + - 0.001 + - 0.01 + - 0.001 + ... + ... + ... + +By modifying the ``hpo_config.yaml`` file, you can easily change the search space or hyperparameters that will be optimized during the HPO process. + +************************************* +Run OpenVINO™ Training Extensions +************************************* + +Now it's time to run OpenVINO™ Training Extensions. You can enable HPO by adding the argument **--enable-hpo**. By default, HPO will use four times the time allocated to training. However, if you are short on time, you can reduce the time for HPO as training by adding the argument **--hpo-time-ratio** and setting it to 2. This means that HPO will use twice the time allocated to training. + +Here's an tutorial command: + +.. code-block:: + + $ otx train \ + ... \ + --enable-hpo \ + --hpo-time-ratio 2 + +With this command, HPO is automatically set to use twice the time allocated for training. You can easily adjust the HPO time allocation by modifying the value of the **--hpo-time-ratio** argument. diff --git a/docs/source/guide/tutorials/advanced/index.rst b/docs/source/guide/tutorials/advanced/index.rst index dd781303b4a..2aa25c0e0f7 100644 --- a/docs/source/guide/tutorials/advanced/index.rst +++ b/docs/source/guide/tutorials/advanced/index.rst @@ -3,8 +3,11 @@ Advanced Tutorials .. toctree:: :maxdepth: 1 - :hidden: - configuration + semi_sl + self_sl + backbones + api_tutorial + hpo_tutorial .. Once we have enough material, we might need to categorize these into `data`, `model learning` sections. \ No newline at end of file diff --git a/docs/source/guide/tutorials/advanced/self_sl.rst b/docs/source/guide/tutorials/advanced/self_sl.rst new file mode 100644 index 00000000000..3e4244c02bc --- /dev/null +++ b/docs/source/guide/tutorials/advanced/self_sl.rst @@ -0,0 +1,198 @@ +############################ +Use Self-Supervised Learning +############################ + +This tutorial introduces how to train a model using self-supervised learning and how to fine-tune the model with pre-trained weights. +OpenVINO™ Training Extensions provides self-supervised learning methods for :doc:`multi-classification <../../explanation/algorithms/classification/multi_class_classification>` and :doc:`semantic segmentation <../../explanation/algorithms/segmentation/semantic_segmentation>`. + +The process has been tested on the following configuration: + +- Ubuntu 20.04 +- NVIDIA GeForce RTX 3090 +- Intel(R) Core(TM) i9-10980XE +- CUDA Toolkit 11.7 + +.. note:: + + This example demonstrates how to work with :ref:`self-supervised learning for classification `. + There are some differences between classfication and semantic segmentation, so there will be some notes for :ref:`self-supervised learning for semantic segmentation `. + +************************* +Setup virtual environment +************************* + +1. You can follow the installation process from a :doc:`quick start guide <../../get_started/installation>` +to create a universal virtual environment for OpenVINO™ Training Extensions. + +2. Activate your virtual +environment: + +.. code-block:: + + .otx/bin/activate + # or by this line, if you created an environment, using tox + . venv/otx/bin/activate + +************ +Pre-training +************ + +1. In this self-supervised learning tutorial, images from `flowers dataset `_ +and :ref:`MobileNet-V3-large-1x ` model is utilized. + +2. Prepare OpenVINO™ Training Extensions workspace for **supervised learning** by running +the following command: + +.. code-block:: + + (otx) ...$ otx build --train-data-roots data/flower_photos --model MobileNet-V3-large-1x + + [*] Workspace Path: otx-workspace-CLASSIFICATION + [*] Load Model Template ID: Custom_Image_Classification_MobileNet-V3-large-1x + [*] Load Model Name: MobileNet-V3-large-1x + [*] - Updated: otx-workspace-CLASSIFICATION/model.py + [*] - Updated: otx-workspace-CLASSIFICATION/data_pipeline.py + [*] - Updated: otx-workspace-CLASSIFICATION/deployment.py + [*] - Updated: otx-workspace-CLASSIFICATION/hpo_config.yaml + [*] - Updated: otx-workspace-CLASSIFICATION/model_hierarchical.py + [*] - Updated: otx-workspace-CLASSIFICATION/model_multilabel.py + [*] - Updated: otx-workspace-CLASSIFICATION/compression_config.json + [*] Update data configuration file to: otx-workspace-CLASSIFICATION/data.yaml + +3. Prepare an OpenVINO™ Training Extensions workspace +for **self-supervised learning** by running the following command: + +.. code-block:: + + (otx) ...$ otx build --train-data-roots data/flower_photos --model MobileNet-V3-large-1x --train-type Selfsupervised --workspace otx-workspace-CLASSIFICATION-Selfsupervised + + [*] Workspace Path: otx-workspace-CLASSIFICATION-Selfsupervised + [*] Load Model Template ID: Custom_Image_Classification_MobileNet-V3-large-1x + [*] Load Model Name: MobileNet-V3-large-1x[*] - Updated: otx-workspace-CLASSIFICATION-Selfsupervised/selfsl/model.py + [*] - Updated: otx-workspace-CLASSIFICATION-Selfsupervised/selfsl/data_pipeline.py + [*] - Updated: otx-workspace-CLASSIFICATION-Selfsupervised/deployment.py + [*] - Updated: otx-workspace-CLASSIFICATION-Selfsupervised/hpo_config.yaml + [*] - Updated: otx-workspace-CLASSIFICATION-Selfsupervised/model_hierarchical.py + [*] - Updated: otx-workspace-CLASSIFICATION-Selfsupervised/model_multilabel.py + [*] Update data configuration file to: otx-workspace-CLASSIFICATION-Selfsupervised/data.yaml + +.. note:: + + One important thing must be considered to set the workspace for self-supervised learning: + + 1. It is also possible to pass just a directory with any images to ``--train-data-roots`` then ``--train-type Selfsupervised`` is not needed. OpenVINO™ Training Extensions will recognize this training type automatically. + However, if you passed a full imagenet data format (with different sub-folders inside), this option is mandatory since it is hard to distinguish between supervised training. + +After the workspace creation, the workspace structure is as follows: + +.. code-block:: + + otx-workspace-CLASSIFICATION + ├── compression_config.json + ├── configuration.yaml + ├── data_pipeline.py + ├── data.yaml + ├── deployment.py + ├── hpo_config.yaml + ├── model_hierarchical.py + ├── model_multilabel.py + ├── model.py + ├── splitted_dataset + │   ├── train + │   └── val + └── template.yaml + otx-workspace-CLASSIFICATION-Selfsupervised + ├── configuration.yaml + ├── data.yaml + ├── deployment.py + ├── hpo_config.yaml + ├── model_hierarchical.py + ├── model_multilabel.py + ├── selfsl + │   ├── data_pipeline.py + │   └── model.py + └── template.yaml + +.. note:: + + For :ref:`semantic segmentation `, ``--train-data-root`` must be set to a directory including **only images**, like below. + + For `VOC2012 dataset `_ used in :doc:`semantic segmentation tutorial <../base/how_to_train/semantic_segmentation>`, for example, the path ``data/VOCdevkit/VOC2012/JPEGImages`` must be set instead of ``data/VOCdevkit/VOC2012``. + + Please refer to :ref:`Explanation of Self-Supervised Learning for Semantic Segmentation `. Option ``--train-type`` is not needed. + + .. code-block:: + + (otx) ...$ otx build --train-data-roots data/VOCdevkit/VOC2012/JPEGImages \ + --model Lite-HRNet-18-mod2 + +4. To start training we need to call ``otx train`` +command in **self-supervised learning** workspace: + +.. code-block:: + + (otx) ...$ cd otx-workspace-CLASSIFICATION-Selfsupervised + (otx) ...$ otx train --data ../otx-workspace-CLASSIFICATION/data.yaml + + ... + + 2023-02-23 19:41:36,879 | INFO : Iter [4970/5000] lr: 8.768e-05, eta: 0:00:29, time: 1.128, data_time: 0.963, memory: 7522, current_iters: 4969, loss: 0.2788 + 2023-02-23 19:41:46,371 | INFO : Iter [4980/5000] lr: 6.458e-05, eta: 0:00:19, time: 0.949, data_time: 0.782, memory: 7522, current_iters: 4979, loss: 0.2666 + 2023-02-23 19:41:55,806 | INFO : Iter [4990/5000] lr: 5.037e-05, eta: 0:00:09, time: 0.943, data_time: 0.777, memory: 7522, current_iters: 4989, loss: 0.2793 + 2023-02-23 19:42:05,105 | INFO : Saving checkpoint at 5000 iterations + 2023-02-23 19:42:05,107 | INFO : ----------------- BYOL.state_dict_hook() called + 2023-02-23 19:42:05,314 | WARNING : training progress 100% + 2023-02-23 19:42:05,315 | INFO : Iter [5000/5000] lr: 4.504e-05, eta: 0:00:00, time: 0.951, data_time: 0.764, memory: 7522, current_iters: 4999, loss: 0.2787 + 2023-02-23 19:42:05,319 | INFO : run task done. + 2023-02-23 19:42:05,323 | INFO : called save_model + 2023-02-23 19:42:05,498 | INFO : Final model performance: Performance(score: -1, dashboard: (6 metric groups)) + 2023-02-23 19:42:05,499 | INFO : train done. + [*] Save Model to: models + +.. note:: + To use the same splitted train dataset, set ``--data ../otx-workspace-CLASSIFICATION/data.yaml`` insead of using ``data.yaml`` in self-supervised learning workspace. + +The training will return artifacts: ``weights.pth`` and ``label_schema.json`` and we can use the weights to fine-tune the model using the target dataset. +The final model performance will be set to -1, but it doesn't matter because self-supervised learning doesn't use accuracy. +Let's see how to fine-tune the model using pre-trained weights below. + +*********** +Fine-tuning +*********** + +After pre-training progress, start fine-tuning by calling the below command with adding ``--load-weights`` argument in **supervised learning workspace**. + +.. code-block:: + + (otx) ...$ cd ../otx-workspace-CLASSIFICATION + (otx) ...$ otx train --load-weights ../otx-workspace-CLASSIFICATION-Selfsupervised/models/weights.pth + + ... + + 2023-02-23 20:56:24,307 | INFO : run task done. + 2023-02-23 20:56:28,883 | INFO : called evaluate() + 2023-02-23 20:56:28,895 | INFO : Accuracy after evaluation: 0.9604904632152589 + 2023-02-23 20:56:28,896 | INFO : Evaluation completed + Performance(score: 0.9604904632152589, dashboard: (3 metric groups)) + +For comparison, we can also obtain the performance without pre-trained weights as below: + +.. code-block:: + + (otx) ...$ otx train + + ... + + 2023-02-23 18:24:34,453 | INFO : run task done. + 2023-02-23 18:24:39,043 | INFO : called evaluate() + 2023-02-23 18:24:39,056 | INFO : Accuracy after evaluation: 0.9550408719346049 + 2023-02-23 18:24:39,056 | INFO : Evaluation completed + Performance(score: 0.9550408719346049, dashboard: (3 metric groups)) + +With self-supervised learning, we can obtain well-adaptive weights and train the model more accurately. +This example showed a little improvement (0.955 → 0.960), but if we use only a few samples that are *too difficult to train a model on*, then +self-supervised learning can be the solution to improve the model perfomance more significantly. +You can check performance improvement examples in :ref:`self-supervised learning for classification ` documentation. + +.. note:: + Then we obtain the new model after fine-tuning, we can proceed with optimization and exporting as described in :doc:`classification tutorial <../base/how_to_train/classification>`. diff --git a/docs/source/guide/tutorials/advanced/semi_sl.rst b/docs/source/guide/tutorials/advanced/semi_sl.rst new file mode 100644 index 00000000000..b4ce1142627 --- /dev/null +++ b/docs/source/guide/tutorials/advanced/semi_sl.rst @@ -0,0 +1,169 @@ +############################ +Use Semi-Supervised Learning +############################ + +This tutorial provides an example of how to use semi-supervised learning with OpenVINO™ Training Extensions on the specific dataset. + +OpenVINO™ Training Extensions now offers semi-supervised learning, which combines labeled and unlabeled data during training to improve model accuracy in case when we have a small amount of annotated data. Currently, this type of training is available for multi-class classification, object detection, and semantic segmentation. + +Semi-supervised learning will soon be available for multi-label classification and instance segmentation as well. + +If you want to learn more about the algorithms used in semi-supervised learning, please refer to the explanation section below: + +- `Multi-class Classification <../../explanation/algorithms/classification/multi_class_classification.html#semi-supervised-learning>`__ +- `Object Detection <../../explanation/algorithms/object_detection/object_detection.html#semi-supervised-learning>`__ +- `Semantic Segmentation <../../explanation/algorithms/segmentation/semantic_segmentation.html#semi-supervised-learning>`__ + +In this tutorial, we use the MobileNet-V3-large-1x model for multi-class classification to cite an example of semi-supervised learning. + +The process has been tested on the following configuration: + +- Ubuntu 20.04 +- NVIDIA GeForce RTX 3090 +- Intel(R) Core(TM) i9-10980XE +- CUDA Toolkit 11.1 + + +.. note:: + + To learn how to export the trained model, refer to `classification export <../base/how_to_train/classification.html#export>`__. + + To learn how to optimize the trained model (.xml) with OpenVINO™ PTQ, refer to `classification optimization <../base/how_to_train/classification.html#optimization>`__. + + Currently, OpenVINO™ NNCF optimization doesn't support a full Semi-SL training algorithm. The accuracy-aware optimization will be executed on labeled data only. + So, the performance drop may be more noticeable than after ordinary supervised training. + + To learn how to deploy the trained model, refer to :doc:`deploy <../base/deploy>`. + + To learn how to run the demo and visualize results, refer to :doc:`demo <../base/demo>`. + +This tutorial explains how to train a model in semi-supervised learning mode and how to evaluate the resulting model. + +************************* +Setup virtual environment +************************* + +1. You can follow the installation process from a :doc:`quick start guide <../../get_started/installation>` +to create a universal virtual environment for OpenVINO™ Training Extensions. + +2. Activate your virtual +environment: + +.. code-block:: + + .otx/bin/activate + # or by this line, if you created an environment, using tox + . venv/otx/bin/activate + +*************************** +Dataset preparation +*************************** + +We use the same dataset, `flowers dataset `_, as we do in :doc:`classification tutorial <../base/how_to_train/classification>`. + +Since it is assumed that we have additional unlabeled images, +we make a use of ``tests/assets/imagenet_dataset`` for this purpose as an example. + + +*************************** +Enable via ``otx build`` +*************************** + +1. To enable semi-supervsied learning via ``otx build``, we need to add arguments ``--unlabeled-data-roots``. +OpenVINO™ Training Extensions receives the root path where unlabeled images are by ``--unlabeled-data-roots``. + +We should put the path where unlabeled data are contained. It is all we need to change to run semisupervised training. OpenVINO™ Training Extensions will recognize this training type automatically. + +.. note:: + + OpenVINO™ Training Extensions automatically searches for all image files with JPG, JPEG, and PNG formats in the root path specified using the ``--unlabeled-data-roots`` option, even if there are other file formats present. + The image files which are located in sub-folders (if threy presented) will be also collected for building unlabeled dataset. + + In this tutorial, we make use of auto-split functionality for the multi-class classification, which makes train/validation splits for the given dataset. + + For the details about auto-split, please refer to :doc:`auto-configuration <../../explanation/additional_features/auto_configuration>`. + +.. code-block:: + + (otx) ...$ otx build --train-data-roots data/flower_photos --unlabeled-data-roots tests/assets/imagenet_dataset --model MobileNet-V3-large-1x + + + [*] Workspace Path: otx-workspace-CLASSIFICATION + [*] Load Model Template ID: Custom_Image_Classification_MobileNet-V3-large-1x + [*] Load Model Name: MobileNet-V3-large-1x + [*] - Updated: otx-workspace-CLASSIFICATION/semisl/model.py + [*] - Updated: otx-workspace-CLASSIFICATION/semisl/data_pipeline.py + ... + [*] Update data configuration file to: otx-workspace-CLASSIFICATION/data.yaml + + + (otx) ...$ cd ./otx-workspace-CLASSIFICATION + + +2. To start training we need to call ``otx train`` +command in our workspace: + +.. code-block:: + + (otx) ...$ otx train + +In the train log, you can check that the train type is set to **Semisupervised** and related configurations are properly loaded as following: + +.. code-block:: + + ... + 2023-02-22 06:21:54,492 | INFO : called _init_recipe() + 2023-02-22 06:21:54,492 | INFO : train type = Semisupervised + 2023-02-22 06:21:54,492 | INFO : train type = Semisupervised - loading training_extensions/src/otx/recipes/stages/classification/semisl.yaml + 2023-02-22 06:21:54,500 | INFO : Replacing runner from EpochRunnerWithCancel to EpochRunnerWithCancel. + 2023-02-22 06:21:54,503 | INFO : initialized recipe = training_extensions/src/otx/recipes/stages/classification/semisl.yaml + ... + + +After training ends, a trained model is saved in the ``models`` sub-directory in the workspace ``otx-workspace-CLASSIFICATION``. + + +*************************** +Enable via ``otx train`` +*************************** + +1. To enable semi-supervised learning directly via ``otx train``, we also need to add the argument ``--unlabeled-data-roots`` +specifying a path to unlabeled images. + +.. code-block:: + + (otx) ...$ otx train src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/template.yaml \ + --train-data-roots data/flower_photos \ + --unlabeled-data-roots tests/assets/imagenet_dataset + +In the train log, you can check that the train type is set to **Semisupervised** and related configurations are properly loaded as following: + +.. code-block:: + + ... + 2023-02-22 06:21:54,492 | INFO : called _init_recipe() + 2023-02-22 06:21:54,492 | INFO : train type = Semisupervised + 2023-02-22 06:21:54,492 | INFO : train type = Semisupervised - loading training_extensions/src/otx/recipes/stages/classification/semisl.yaml + 2023-02-22 06:21:54,500 | INFO : Replacing runner from EpochRunnerWithCancel to EpochRunnerWithCancel. + 2023-02-22 06:21:54,503 | INFO : initialized recipe = training_extensions/src/otx/recipes/stages/classification/semisl.yaml + ... + + +After training ends, a trained model is saved in the ``latest_trained_model`` sub-directory in the workspace named ``otx-workspace-CLASSIFICATION`` by default. + + +*************************** +Validation +*************************** + +In the same manner with `the normal validation <../base/how_to_train/classification.html#validation>`__, +we can evaluate the trained model with auto-splitted validation dataset in the workspace and +save results to ``outputs/performance.json`` by the following command: + + +.. code-block:: + + (otx) ...$ otx eval src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/template.yaml \ + --test-data-roots splitted_dataset/val \ + --load-weights models/weights.pth \ + --output outputs diff --git a/docs/source/guide/tutorials/base/demo.rst b/docs/source/guide/tutorials/base/demo.rst new file mode 100644 index 00000000000..ad700d52fae --- /dev/null +++ b/docs/source/guide/tutorials/base/demo.rst @@ -0,0 +1,96 @@ +How to run the demonstration mode with OpenVINO™ Training Extensions CLI +======================================================================== + +This tutorial shows how to run :doc:`trained ` model inside OTX repository in demonstration mode. +It allows you to apply the model on the custom data or the online footage from a web camera and see how it will work in the real-life scenario. + +.. note:: + + This tutorial uses an object detection model for example, however for other tasks the functionality remains the same - you just need to replace the input dataset with your own. + +For visualization you use images from WGISD dataset from the :doc:`object detection tutorial `. + +1. Activate the virtual environment +created in the previous step. + +.. code-block:: + + source .otx/bin/activate + +2. As an ``input`` we can use a single image, +a folder of images, a video file, or a web camera id. We can run the demo on PyTorch (.pth) model and IR (.xml) model. + +The following command will run the demo on your input source, using PyTorch ``outputs/weights.pth``. + +.. code-block:: + + (demo) ...$ otx demo --input docs/utils/images/wgisd_dataset_sample.jpg \ + --load-weights outputs/weights.pth + +But if we'll provide a single image the demo processes and renders it quickly, then exits. To continuously visualize inference results on the screen, apply the ``loop`` option, which enforces the processing a single image in a loop. + +.. code-block:: + + (demo) ...$ otx demo --input docs/utils/images/wgisd_dataset_sample.jpg \ + --load-weights outputs/weights.pth --loop + +In this case, you can stop the demo by pressing `Q` button or killing the process in the terminal (``Ctrl+C`` for Linux). + +3. If we want to pass an images folder, it's better to specify the delay parameter, that defines, how much millisecond pause will be held between showing the next image. +For example ``--delay 100`` will make this pause 0.1 ms. +If you want to skip showing the resulting image and instead see the number of predictions and time spent on each image inference, specify ``--delay 0``. + + +4. In WGISD dataset we have high-resolution images, +so the ``--fit-to-size`` parameter would be quite useful. It resizes the resulting image to a specified: + +.. code-block:: + + (demo) ...$ otx demo --input docs/utils/images/wgisd_dataset_sample.jpg \ + --load-weights outputs/weights.pth --loop --fit-to-size 800 600 + + +5. To save inferenced results with predictions on it, we can specify the folder path, using ``--output``. +It works for images, videos, image folders and web cameras. To prevent issues, do not specify it together with a ``--loop`` parameter. + +.. code-block:: + + (demo) ...$ otx demo --input docs/utils/images/wgisd_dataset_sample.jpg \ + --load-weights outputs/weights.pth \ + --output resulted_images + +6. If we want to show inference speed right on images, +we can run the following line: + +.. code-block:: + + (demo) ...$ otx demo --input docs/utils/images/wgisd_dataset_sample.jpg \ + --load-weights outputs/weights.pth --loop \ + --fit-to-size 800 600 --display-perf + + +6. To run a demo on a web camera, you need to know its ID. +You can check a list of camera devices by running the command line below on Linux system: + +.. code-block:: + + (demo) ...$ sudo apt-get install v4l-utils + (demo) ...$ v4l2-ctl --list-devices + +The output will look like this: + +.. code-block:: + + Integrated Camera (usb-0000:00:1a.0-1.6): + /dev/video0 + +After that, you can use this ``/dev/video0`` as a camera ID for ``--input``. + +Congratulations! Now you have learned how to use base OpenVINO™ Training Extensions functionality. For the advanced features, refer to the next section called :doc:`../advanced/index`. + +*************** +Troubleshooting +*************** + +If you use Anaconda environment, keep in mind that OpenVINO has limited `Conda support `_ for Python 3.6 and 3.7 versions only. The demo package requires python 3.8, though. +Therefore, use other tools to create the environment (like ``venv`` or ``virtualenv``) and use ``pip`` as a package manager. \ No newline at end of file diff --git a/docs/source/guide/tutorials/base/deploy.rst b/docs/source/guide/tutorials/base/deploy.rst new file mode 100644 index 00000000000..367597d414c --- /dev/null +++ b/docs/source/guide/tutorials/base/deploy.rst @@ -0,0 +1,193 @@ +How to deploy the model and use demo in exportable code +======================================================= + +This guide explains how to deploy a model trained in the :doc:`previous stage ` and visualize it outside of this repository. +As a result of this step, you'll get the exported model together with the self-contained python package and a demo application to visualize results in other environment without long installation process. + +.. NOTE:: + To learn how to use demonstration mode inside this repository utilizing OTX CLI , refer to :doc:`demo`. + +To be specific, this tutorial uses the object detection ATSS model trained and exported in the previuos step, which is located in ``outputs/openvino``. +Nevertheless, it can be run for any task in the same manner. + +********** +Deployment +********** + +1. Activate the virtual environment +created in the previous step. + +.. code-block:: + + source .otx/bin/activate + +2. ``otx deploy`` returns an ``openvino.zip`` +archive with the following files: + +- model + + - ``model.xml`` and ``model.bin`` - model exported to the OpenVINO™ format + - ``config.json`` - file containing the post-processing info and meta information about labels in the dataset + +- python + + - model_wrappers (Optional) + - ``README.md`` + - ``LICENSE`` + - ``demo.py``- simple demo to visualize results of model inference + - ``requirements.txt`` - minimal packages required to run the demo + + +3. You can deploy the model exported to IR, +using the command below: + +.. code-block:: + + (otx) ...$ otx deploy src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml \ + --load-weights outputs/openvino/openvino.xml \ + --output outputs/deploy + + 2023-01-20 09:30:40,938 | INFO : Loading OpenVINO OTXDetectionTask + 2023-01-20 09:30:41,736 | INFO : OpenVINO task initialization completed + 2023-01-20 09:30:41,737 | INFO : Deploying the model + 2023-01-20 09:30:41,753 | INFO : Deploying completed + +You can also deploy the quantized model, that was optimized with NNCF or PTQ, passing the path to this model in IR format to ``--load-weights`` parameter. + +After that, you can use the resulting ``openvino.zip`` archive in other application. + +************* +Demonstrarion +************* + +Using the exported demo, we're able to run the model in the demonstration mode outside of this repository, using only ported ``.zip`` archive with minimum required packages. +The demo allows to apply our model on the custom data or the online footage from a web camera and see how it will work in the real-life scenario. + +1. Unzip the ``openvino.zip`` +archive. + +.. code-block:: + + unzip outputs/deploy/openvino.zip -d outputs/deploy/ + +2. To run the demo in exportable code, we can use a brand-new virtual environment, +where we need to install a minimalistic set of packages required for inference only. + +.. code-block:: + + python3 -m venv demo_venv --prompt="demo" + source demo_venv/bin/activate + python -m pip install -r outputs/deploy/requirements.txt + + +3. The following line will run the demo on your input source, +using the model in the ``model`` folder. You can pass as ``input`` a single image, a folder of images, a video file, or a web camera id. + +.. code-block:: + + (demo) ...$ python outputs/deploy/python/demo.py --input docs/utils/images/wgisd_dataset_sample.jpg \ + --models outputs/deploy/model + +You can press ``Q`` to stop inference during the demo running. + +For example, the model inference on image from WGISD dataset, which we used for :ref:`object detection training `, will look like this: + +.. image:: ../../../../utils/images/wgisd_pr_sample.jpg + :width: 600 + :alt: this image shows the inference results on the WGISD dataset + +.. note:: + + If you provide a single image as input, the demo processes and renders it quickly, then exits. To continuously + visualize inference results on the screen, apply the ``loop`` option, which enforces processing a single image in a loop. + In this case, you can stop the demo by pressing `Q` button or killing the process in the terminal (``Ctrl+C`` for Linux). + +To learn how to run the demo on Windows and MacOS, please refer to the ``outputs/deploy/python/README.md`` file in exportable code. + +4. To save inferenced results with predictions on it, we can specify the folder path, using ``--output``. +It works for images, videos, image folders and web cameras. To prevent issues, do not specify it together with a ``--loop`` parameter. + +.. code-block:: + + (demo) ...$ python outputs/deploy/python/demo.py --input docs/utils/images/wgisd_dataset_sample.jpg \ + --models outputs/deploy/model \ + --output resulted_images + +5. To run a demo on a web camera, we need to know its ID. +We can check a list of camera devices by running this command line on Linux system: + +.. code-block:: + + sudo apt-get install v4l-utils + v4l2-ctl --list-devices + +The output will look like this: + +.. code-block:: + + Integrated Camera (usb-0000:00:1a.0-1.6): + /dev/video0 + +After that, we can use this ``/dev/video0`` as a camera ID for ``--input``. + +6. We can also change ``config.json`` that specifies the confidence threshold and +color for each class visualization, but any changes should be made with caution. + +For example, in our image of the winery we see, that a lot of objects weren't detected. +The original confidence threshold was chosen based on the validation split results to maximize the final F1 metric, balancing precision and recall values. So, visual results can be not suitable enough for a user. +To overcome this problem, we can decrease ``confidence_threshold`` in ``config.json file`` from **0.4** to **0.3**. + +.. code-block:: + + "model_parameters": { + "result_based_confidence_threshold": true, + "confidence_threshold": 0.3000000059604645, + +For visual purposes, we can also update the color of ``Chardonnay`` class from yellow to lilac to make it more distinguishable. + +.. code-block:: + + "all_labels": { + "0": { + "_id": "0", + "name": "Chardonnay", + "color": { + "red": 230, + "green": 106, + "blue": 226, + +The result will be the following: + +.. image:: ../../../../utils/images/wgisd_pr2_sample.jpg + :width: 600 + :alt: this image shows the inference results on the WGISD dataset + +| + +.. note:: + + Although this is example for object detection demo, other tasks can have their own tunable parameters that we can check in ``config.json`` file + +Congratulations! Now you have learned how to use base OTX functionality. For the advanced features, please refer to the next section called :doc:`../advanced/index`. + +*************** +Troubleshooting +*************** + +1. If you have access to the Internet through the proxy server only, +please use pip with a proxy call as demonstrated by the command below: + +.. code-block:: + + python -m pip install --proxy http://:@: + + +2. If you use Anaconda environment, you should consider that OpenVINO has limited `Conda support `_ for Python 3.6 and 3.7 versions only. But the demo package requires python 3.8. +So please use other tools to create the environment (like ``venv`` or ``virtualenv``) and use ``pip`` as a package manager. + +3. If you have problems when you try to use ``pip install`` command, +please update the pip version by the following command: + +.. code-block:: + + python -m pip install --upgrade pip diff --git a/docs/source/guide/tutorials/base/explain.rst b/docs/source/guide/tutorials/base/explain.rst index 20bee0eb974..fa03aea9bea 100644 --- a/docs/source/guide/tutorials/base/explain.rst +++ b/docs/source/guide/tutorials/base/explain.rst @@ -4,7 +4,7 @@ How to explain the model behavior This guide explains the model behavior, which is trained through :doc:`previous stage `. It allows displaying the saliency maps, which provide the locality where the model gave an attention to predict a specific category. -To be specific, this tutorial uses as an example of the ATSS model trained through ``otx train`` and saved as ``otx-workspace/.latest/train/checkpoints/epoch_*.pth``. +To be specific, this tutorial uses as an example of the ATSS model trained through ``otx train`` and saved as ``outputs/weights.pth``. .. note:: @@ -15,28 +15,20 @@ For visualization we use images from WGISD dataset from the :doc:`object detecti 1. Activate the virtual environment created in the previous step. -.. code-block:: shell +.. code-block:: .otx/bin/activate # or by this line, if you created an environment, using tox . venv/otx/bin/activate -2. ``otx explain`` returns saliency maps (heatmaps with red colored areas of focus) +2. ``otx explain`` returns saliency maps (heatmaps with red colored areas of focus) +at the path specified by ``--output``. -.. tab-set:: - - .. tab-item:: CLI - - .. code-block:: shell - - Need to update! - - .. tab-item:: API - - .. code-block:: python - - Need to update! +.. code-block:: + otx explain --input otx-workspace-DETECTION/splitted_dataset/val/ \ + --output outputs/explanation \ + --load-weights outputs/weights.pth 3. To specify the algorithm of saliency map creation for classification, we can define the ``--explain-algorithm`` parameter. diff --git a/docs/source/guide/tutorials/base/how_to_train/action_classification.rst b/docs/source/guide/tutorials/base/how_to_train/action_classification.rst index e25ba9b2f9d..7304660dc15 100644 --- a/docs/source/guide/tutorials/base/how_to_train/action_classification.rst +++ b/docs/source/guide/tutorials/base/how_to_train/action_classification.rst @@ -7,6 +7,10 @@ To learn more about Action Classification task, refer to :doc:`../../../explanat .. note:: To learn more about managing the training process of the model including additional parameters and modification, refer to :doc:`./detection`. + To learn how to deploy the trained model, refer to: :doc:`../deploy`. + + To learn how to run the demo and visualize results, refer to: :doc:`../demo`. + The process has been tested on the following configuration. - Ubuntu 20.04 @@ -29,7 +33,7 @@ to create a universal virtual environment for OpenVINO™ Training Extensions. 2. Activate your virtual environment: -.. code-block:: shell +.. code-block:: .otx/bin/activate # or by this line, if you created an environment, using tox @@ -74,9 +78,14 @@ According to the `documentation `_ format using the following command: -.. code-block:: shell +.. code-block:: - Need to update! + (otx) ...$ python3 src/otx/algorithms/action/utils/convert_public_data_to_cvat.py \ + --task action_classification \ + --src_path ./data/hmdb51/rawframes \ + --dst_path ./data/hmdb51/CVAT/train \ + --ann_file ./data/hmdb51/hmdb51_train_split_1_rawframes.txt \ + --label_map ./data/hmdb51/label_map.txt The resulting folder structure will be as follows: @@ -267,4 +276,5 @@ Keep in mind that PTQ will take some time (generally less than NNCF optimization 3. Now, you have fully trained, optimized and exported an efficient model representation ready-to-use action classification model. +The following tutorials provide further steps on how to :doc:`deploy <../deploy>` and use your model in the :doc:`demonstration mode <../demo>` and visualize results. The examples are provided with an object detection model, but it is easy to apply them for action classification by substituting the object detection model with classification one. diff --git a/docs/source/guide/tutorials/base/how_to_train/anomaly_detection.rst b/docs/source/guide/tutorials/base/how_to_train/anomaly_detection.rst index 7f15cbdf329..42058ffdf8a 100644 --- a/docs/source/guide/tutorials/base/how_to_train/anomaly_detection.rst +++ b/docs/source/guide/tutorials/base/how_to_train/anomaly_detection.rst @@ -5,14 +5,18 @@ This tutorial demonstrates how to train, evaluate, and deploy a classification, Read :doc:`../../../explanation/algorithms/anomaly/index` for more information about the Anomaly tasks. .. note:: - To learn more about managing the training process of the model including additional parameters and its modification, refer to :doc:`./detection`. + To learn more about managing the training process of the model including additional parameters and its modification, refer to :doc:`./detection`. + + To learn how to deploy the trained model, refer to: :doc:`../deploy`. + + To learn how to run the demo and visualize results, refer to: :doc:`../demo`. The process has been tested with the following configuration: - Ubuntu 20.04 - NVIDIA GeForce RTX 3090 -- Intel(R) Core(TM) i9-11900 -- CUDA Toolkit 11.8 +- Intel(R) Core(TM) i9-10980XE +- CUDA Toolkit 11.1 ***************************** @@ -22,14 +26,21 @@ Setup the Virtual environment 1. To create a universal virtual environment for OpenVINO™ Training Extensions, please follow the installation process in the :doc:`quick start guide <../../../get_started/installation>`. -2. Activate your virtual +2. Alternatively, if you want to only train anomaly models, then you can create a task specific environment. +Then also follow the installation process in the guide above, but substitute ``pip install -e .[anomaly]`` with the following command: + +.. code-block:: + + pip install -e .[anomaly] + +3. Activate your virtual environment: -.. code-block:: shell +.. code-block:: - .otx/bin/activate - # or by this line, if you created an environment, using tox - . venv/otx/bin/activate + .otx/bin/activate + # or by this line, if you created an environment, using tox + . venv/otx/bin/activate ************************** Dataset Preparation @@ -90,121 +101,31 @@ Training 1. For this example let's look at the anomaly detection tasks -.. tab-set:: - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx find --task ANOMALY_DETECTION - ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Task ┃ Model Name ┃ Recipe Path ┃ - ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ ANOMALY_DETECTION │ stfpm │ src/otx/recipe/anomaly_detection/stfpm.yaml │ - │ ANOMALY_DETECTION │ padim │ src/otx/recipe/anomaly_detection/padim.yaml │ - └───────────────────┴────────────┴─────────────────────────────────────────────┘ +.. code-block:: bash - .. tab-item:: API + (otx) ...$ otx find --task anomaly_detection - .. code-block:: python +:: - from otx.engine.utils.api import list_models - - model_lists = list_models(task="ANOMALY_DETECTION") - print(model_lists) - ''' - ['stfpm', 'padim'] - ''' + +-------------------+-----------------------------+-------+------------------------------------------------------------------+ + | TASK | ID | NAME | BASE PATH | + +-------------------+-----------------------------+-------+------------------------------------------------------------------+ + | ANOMALY_DETECTION | ote_anomaly_detection_stfpm | STFPM | src/otx/algorithms/anomaly/configs/detection/stfpm/template.yaml | + | ANOMALY_DETECTION | ote_anomaly_detection_padim | PADIM | src/otx/algorithms/anomaly/configs/detection/padim/template.yaml | + +-------------------+-----------------------------+-------+------------------------------------------------------------------+ You can see two anomaly detection models, STFPM and PADIM. For more detail on each model, refer to Anomalib's `STFPM `_ and `PADIM `_ documentation. 2. Let's proceed with PADIM for this example. -.. tab-set:: - - .. tab-item:: CLI (auto-config) - - .. code-block:: shell - - (otx) ...$ otx train --data_root datasets/MVTec/bottle \ - --task ANOMALY_DETECTION - - .. tab-item:: CLI (with config) - - .. code-block:: shell - - (otx) ...$ otx train --config src/otx/recipe/anomaly_detection/padim.yaml \ - --data_root datasets/MVTec/bottle - - .. tab-item:: API (from_config) - - .. code-block:: python - - from otx.engine import Engine - - data_root = "datasets/MVTec/bottle" - recipe = "src/otx/recipe/anomaly_detection/padim.yaml" - - engine = Engine.from_config( - config_path=recipe, - data_root=data_root, - work_dir="otx-workspace", - ) - - engine.train(...) - - .. tab-item:: API - - .. code-block:: python - - from otx.engine import Engine - - data_root = "datasets/MVTec/bottle" - - engine = Engine( - model="padim", - data_root=data_root, - task="ANOMALY_DETECTION", - work_dir="otx-workspace", - ) - - engine.train(...) - - -3. ``(Optional)`` Additionally, we can tune training parameters such as batch size, learning rate, patience epochs. -Learn more about specific parameters using ``otx train --help -v`` or ``otx train --help -vv``. - -For example, to decrease the batch size to 4, fix the number of epochs to 100, extend the command line above with the following line. - -.. tab-set:: - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx train ... --data.config.train_subset.batch_size 4 \ - --max_epochs 100 - - .. tab-item:: API - - .. code-block:: python - - from otx.core.config.data import DataModuleConfig, SubsetConfig - from otx.core.data.module import OTXDataModule - from otx.engine import Engine - - data_config = DataModuleConfig(..., train_subset=SubsetConfig(..., batch_size=4)) - datamodule = OTXDataModule(..., config=data_config) +.. code-block:: bash - engine = Engine(..., datamodule=datamodule) + (otx) ...$ otx train ote_anomaly_detection_padim \ + --train-data-roots datasets/MVTec/bottle/train \ + --val-data-roots datasets/MVTec/bottle/test - engine.train(max_epochs=100) - -4. The training result ``checkpoints/*.ckpt`` file is located in ``{work_dir}`` folder, -while training logs can be found in the ``{work_dir}/{timestamp}`` dir. - -This will start training and generate artifacts for commands such as ``export`` and ``optimize``. You will notice the ``otx-workspace`` directory in your current working directory. This is where all the artifacts are stored. +This will start training and generate artifacts for commands such as ``export`` and ``optimize``. You will notice the ``otx-workspace-ANOMALY_DETECTION`` directory in your current working directory. This is where all the artifacts are stored. ************** Evaluation @@ -212,47 +133,16 @@ Evaluation Now we have trained the model, let's see how it performs on a specific dataset. In this example, we will use the same dataset to generate evaluation metrics. To perform evaluation you need to run the following commands: -.. tab-set:: - - .. tab-item:: CLI (with work_dir) - - .. code-block:: shell - - (otx) ...$ otx test --work_dir otx-workspace - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Test metric ┃ DataLoader 0 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ image_AUROC │ 0.8 │ - │ image_F1Score │ 0.8 │ - │ pixel_AUROC │ 0.8 │ - │ pixel_F1Score │ 0.8 │ - │ test/data_time │ 0.6517705321311951 │ - │ test/iter_time │ 0.6630784869194031 │ - └───────────────────────────┴───────────────────────────┘ +.. code-block:: bash - .. tab-item:: CLI (with config) + (otx) ...$ otx eval ote_anomaly_detection_padim \ + --test-data-roots datasets/MVTec/bottle/test \ + --load-weights otx-workspace-ANOMALY_DETECTION/models/weights.pth \ + --output otx-workspace-ANOMALY_DETECTION/outputs - .. code-block:: shell +You should see an output similar to the following:: - (otx) ...$ otx test --config src/otx/recipe/anomaly_detection/padim.yaml \ - --data_root datasets/MVTec/bottle \ - --checkpoint otx-workspace/20240313_042421/checkpoints/epoch_010.ckpt - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Test metric ┃ DataLoader 0 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ image_AUROC │ 0.8 │ - │ image_F1Score │ 0.8 │ - │ pixel_AUROC │ 0.8 │ - │ pixel_F1Score │ 0.8 │ - │ test/data_time │ 0.6517705321311951 │ - │ test/iter_time │ 0.6630784869194031 │ - └───────────────────────────┴───────────────────────────┘ - - .. tab-item:: API - - .. code-block:: python - - engine.test() + MultiScorePerformance(score: 0.6356589147286821, primary_metric: ScoreMetric(name=`f-measure`, score=`0.6356589147286821`), additional_metrics: (1 metrics), dashboard: (2 metric groups)) The primary metric here is the f-measure computed against the ground-truth bounding boxes. It is also called the local score. In addition, f-measure is also used to compute the global score. The global score is computed based on the global label of the image. That is, the image is anomalous if it contains at least one anomaly. This global score is stored as an additional metric. @@ -261,70 +151,50 @@ The primary metric here is the f-measure computed against the ground-truth bound All task types report Image-level F-measure as the primary metric. In addition, both localization tasks (anomaly detection and anomaly segmentation) also report localization performance (F-measure for anomaly detection and Dice-coefficient for anomaly segmentation). -******* +****** Export -******* +****** 1. ``otx export`` exports a trained Pytorch `.pth` model to the OpenVINO™ Intermediate Representation (IR) format. -It allows running the model on the Intel hardware much more efficient, especially on the CPU. Also, the resulting IR model is required to run PTQ optimization. IR model consists of 2 files: ``exported_model.xml`` for weights and ``exported_model.bin`` for architecture. +It allows running the model on the Intel hardware much more efficient, especially on the CPU. Also, the resulting IR model is required to run PTQ optimization. IR model consists of 2 files: ``openvino.xml`` for weights and ``openvino.bin`` for architecture. 2. We can run the below command line to export the trained model and save the exported model to the ``openvino`` folder: -.. tab-set:: - - .. tab-item:: CLI (with work_dir) - - .. code-block:: shell - - (otx) ...$ otx export --work_dir otx-workspace - ... - Elapsed time: 0:00:06.588245 - - .. tab-item:: CLI (with config) - - .. code-block:: shell - - (otx) ...$ otx export ... --checkpoint otx-workspace/20240313_042421/checkpoints/epoch_010.ckpt - ... - Elapsed time: 0:00:06.588245 - - .. tab-item:: API - - .. code-block:: python - - engine.export() - -Now that we have the exported model, let's check its performance using ``otx test``: - -.. tab-set:: +.. code-block:: - .. tab-item:: CLI (with work_dir) + otx export ote_anomaly_detection_padim \ + --load-weights otx-workspace-ANOMALY_DETECTION/models/weights.pth \ + --output otx-workspace-ANOMALY_DETECTION/openvino - .. code-block:: shell +You will see the outputs similar to the following: - (otx) ...$ otx test --work_dir otx-workspace \ - --checkpoint otx-workspace/20240313_052847/exported_model.xml \ - --engine.device cpu - ... +.. code-block:: - .. tab-item:: CLI (with config) + [INFO] 2023-02-21 16:42:43,207 - otx.algorithms.anomaly.tasks.inference - Initializing the task environment. + [INFO] 2023-02-21 16:42:43,632 - otx.algorithms.anomaly.tasks.train - Loaded model weights from Task Environment + [WARNING] 2023-02-21 16:42:43,639 - otx.algorithms.anomaly.tasks.inference - Ommitting feature dumping is not implemented.The saliency maps and representation vector outputs will be dumped in the exported model. + [INFO] 2023-02-21 16:42:43,640 - otx.algorithms.anomaly.tasks.inference - Exporting the OpenVINO model. + [ INFO ] The model was converted to IR v11, the latest model format that corresponds to the source DL framework input/output format. While IR v11 is backwards compatible with OpenVINO Inference Engine API v1.0, please use API v2.0 (as of 2022.1) to take advantage of the latest improvements in IR v11. + Find more information about API v2.0 and IR v11 at https://docs.openvino.ai/latest/openvino_2_0_transition_guide.html + [ SUCCESS ] Generated IR version 11 model. + [ SUCCESS ] XML file: /tmp/otx-anomaliba3imqkmo/onnx_model.xml + [ SUCCESS ] BIN file: /tmp/otx-anomaliba3imqkmo/onnx_model.bin - .. code-block:: shell +Now that we have the exported model, let's check its performance using ``otx eval``: - (otx) ...$ otx test --config src/otx/recipe/anomaly_detection/padim.yamll \ - --data_root data/wgisd \ - --checkpoint otx-workspace/20240312_052847/exported_model.xml \ - --engine.device cpu - ... +.. code-block:: bash - .. tab-item:: API + otx eval ote_anomaly_detection_padim \ + --test-data-roots datasets/MVTec/bottle/test \ + --load-weights otx-workspace-ANOMALY_DETECTION/openvino/openvino.xml \ + --output otx-workspace-ANOMALY_DETECTION/openvino - .. code-block:: python +This gives the following results: - exported_model = engine.export() - engine.test(checkpoint=exported_model) +.. code-block:: + MultiScorePerformance(score: 0.6511627906976744, primary_metric: ScoreMetric(name=`f-measure`, score=`0.6511627906976744`), additional_metrics: (1 metrics), dashboard: (2 metric groups)) ************ Optimization @@ -337,51 +207,41 @@ For more information refer to the :doc:`optimization explanation <../../../expla 1. Let's start with PTQ optimization. -.. tab-set:: - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx optimize --work_dir otx-workspace \ - --checkpoint otx-workspace/20240312_052847/exported_model.xml - - ... - Statistics collection ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 30/30 • 0:00:14 • 0:00:00 - Applying Fast Bias correction ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 58/58 • 0:00:02 • 0:00:00 - Elapsed time: 0:00:24.958733 - - .. tab-item:: API - - .. code-block:: python - - ckpt_path = "otx-workspace/20240312_052847/exported_model.xml" - engine.optimize(checkpoint=ckpt_path) - -Please note, that PTQ will take some time without logging to optimize the model. - -3. Finally, we can also evaluate the optimized model by passing -it to the ``otx test`` function. +.. code-block:: -.. tab-set:: + otx optimize ote_anomaly_detection_padim \ + --train-data-roots datasets/MVTec/bottle/train \ + --load-weights otx-workspace-ANOMALY_DETECTION/openvino/openvino.xml \ + --output otx-workspace-ANOMALY_DETECTION/ptq_model - .. tab-item:: CLI +This command generates the following files that can be used to run :doc:`otx demo <../demo>`: - .. code-block:: shell +- image_threshold +- pixel_threshold +- label_schema.json +- max +- min +- openvino.bin +- openvino.xml - (otx) ...$ otx test --work_dir otx-workspace \ - --checkpoint otx-workspace/20240313_055042/optimized_model.xml \ - --engine.device cpu +2. To perform NNCF optimization, pass the torch ``pth`` +weights to the ``opitmize`` command: - ... - Elapsed time: 0:00:10.260521 +.. code-block:: - .. tab-item:: API + otx optimize ote_anomaly_detection_padim \ + --train-data-roots datasets/MVTec/bottle/train \ + --load-weights otx-workspace-ANOMALY_DETECTION/models/weights.pth \ + --output otx-workspace-ANOMALY_DETECTION/nncf_model - .. code-block:: python +Similar to PTQ optimization, it generates the following files: - ckpt_path = "otx-workspace/20240313_055042/optimized_model.xml" - engine.test(checkpoint=ckpt_path) +- image_threshold +- pixel_threshold +- label_schema.json +- max +- min +- weights.pth ******************************* @@ -389,7 +249,7 @@ Segmentation and Classification ******************************* While the above example shows Anomaly Detection, you can also train Anomaly Segmentation and Classification models. -To see what tasks are available, you can pass ``ANOMALY_SEGMENTATION`` and ``ANOMALY_CLASSIFICATION`` to ``otx find`` mentioned in the `Training`_ section. You can then use the same commands to train, evaluate, export and optimize the models. +To see what tasks are available, you can pass ``anomaly_segmentation`` and ``anomaly_classification`` to ``otx find`` mentioned in the `Training`_ section. You can then use the same commands to train, evaluate, export and optimize the models. .. note:: diff --git a/docs/source/guide/tutorials/base/how_to_train/classification.rst b/docs/source/guide/tutorials/base/how_to_train/classification.rst index 1316c903826..6569194cd3a 100644 --- a/docs/source/guide/tutorials/base/how_to_train/classification.rst +++ b/docs/source/guide/tutorials/base/how_to_train/classification.rst @@ -8,12 +8,16 @@ To learn more about Classification task, refer to :doc:`../../../explanation/alg To learn deeper how to manage training process of the model including additional parameters and its modification, refer to :doc:`./detection`. + To learn how to deploy the trained model, refer to: :doc:`../deploy`. + + To learn how to run the demo and visualize results, refer to: :doc:`../demo`. + The process has been tested on the following configuration. - Ubuntu 20.04 - NVIDIA GeForce RTX 3090 - Intel(R) Core(TM) i9-10980XE -- CUDA Toolkit 11.8 +- CUDA Toolkit 11.1 .. note:: @@ -31,7 +35,7 @@ to create a universal virtual environment for OpenVINO™ Training Extensions. 2. Activate your virtual environment: -.. code-block:: shell +.. code-block:: .otx/bin/activate # or by this line, if you created an environment, using tox @@ -44,7 +48,7 @@ Dataset preparation Download and prepare a `flowers dataset `_ with the following command: -.. code-block:: shell +.. code-block:: cd data wget http://download.tensorflow.org/example_images/flower_photos.tgz @@ -83,61 +87,48 @@ The list of supported templates for classification is available with the command You also can modify the architecture of supported models with various backbones. To do that, please refer to the :doc:`advanced tutorial for model customization <../../advanced/backbones>`. -.. code-block:: shell - - (otx) ...$ otx find --task MULTI_CLSS_CLS - ┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Task ┃ Model Name ┃ Recipe Path ┃ - ┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ MULTI_CLASS_CLS │ openvino_model │ src/otx/recipe/classification/multi_class_cls/openvino_model.yaml │ - │ MULTI_CLASS_CLS │ tv_efficientnet_b0 │ src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b0.yaml │ - │ MULTI_CLASS_CLS │ tv_resnet_50 │ src/otx/recipe/classification/multi_class_cls/tv_resnet_50.yaml │ - │ MULTI_CLASS_CLS │ efficientnet_v2_light │ src/otx/recipe/classification/multi_class_cls/efficientnet_v2_light.yaml │ - │ MULTI_CLASS_CLS │ tv_efficientnet_b3 │ src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b3.yaml │ - │ MULTI_CLASS_CLS │ efficientnet_b0_light │ src/otx/recipe/classification/multi_class_cls/efficientnet_b0_light.yaml │ - │ MULTI_CLASS_CLS │ tv_efficientnet_v2_l │ src/otx/recipe/classification/multi_class_cls/tv_efficientnet_v2_l.yaml │ - │ MULTI_CLASS_CLS │ tv_efficientnet_b1 │ src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b1.yaml │ - │ MULTI_CLASS_CLS │ tv_mobilenet_v3_small │ src/otx/recipe/classification/multi_class_cls/tv_mobilenet_v3_small.yaml │ - │ MULTI_CLASS_CLS │ otx_mobilenet_v3_large │ src/otx/recipe/classification/multi_class_cls/otx_mobilenet_v3_large.yaml │ - │ MULTI_CLASS_CLS │ otx_deit_tiny │ src/otx/recipe/classification/multi_class_cls/otx_deit_tiny.yaml │ - │ MULTI_CLASS_CLS │ tv_efficientnet_b4 │ src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b4.yaml │ - │ MULTI_CLASS_CLS │ otx_efficientnet_v2 │ src/otx/recipe/classification/multi_class_cls/otx_efficientnet_v2.yaml │ - │ MULTI_CLASS_CLS │ mobilenet_v3_large_light │ src/otx/recipe/classification/multi_class_cls/mobilenet_v3_large_light.yaml │ - │ MULTI_CLASS_CLS │ otx_efficientnet_b0 │ src/otx/recipe/classification/multi_class_cls/otx_efficientnet_b0.yaml │ - │ MULTI_CLASS_CLS │ otx_dino_v2 │ src/otx/recipe/classification/multi_class_cls/otx_dino_v2.yaml │ - │ MULTI_CLASS_CLS │ otx_dino_v2_linear_probe │ src/otx/recipe/classification/multi_class_cls/otx_dino_v2_linear_probe.yaml │ - └─────────────────┴──────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘ - -To have a specific example in this tutorial, all commands will be run on the :ref:`otx_mobilenet_v3_large ` model. It's a light model, that achieves competitive accuracy while keeping the inference fast. +.. code-block:: + + (otx) ...$ otx find --task classification + + +----------------+---------------------------------------------------+-----------------------+---------------------------------------------------------------------------------------+ + | TASK | ID | NAME | PATH | + +----------------+---------------------------------------------------+-----------------------+---------------------------------------------------------------------------------------+ + | CLASSIFICATION | Custom_Image_Classification_MobileNet-V3-large-1x | MobileNet-V3-large-1x | src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/template.yaml | + | CLASSIFICATION | Custom_Image_Classification_EfficinetNet-B0 | EfficientNet-B0 | src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/template.yaml | + | CLASSIFICATION | Custom_Image_Classification_EfficientNet-V2-S | EfficientNet-V2-S | src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/template.yaml | + +----------------+---------------------------------------------------+-----------------------+---------------------------------------------------------------------------------------+ + +To have a specific example in this tutorial, all commands will be run on the :ref:`MobileNet-V3-large-1x ` model. It's a light model, that achieves competitive accuracy while keeping the inference fast. 2. Next, you need to create train/validation sets. OpenVINO™ Training Extensions supports auto-split functionality for the multi-class classification. For other classification types you need to prepare splits in advance. + .. note:: Currently, OpenVINO™ Training Extensions supports auto-split only for multi-class classification. For the multi-label and hierarchical tasks you need to prepare data splits in advance. Let's prepare an OpenVINO™ Training Extensions classification workspace running the following command: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx train --config src/otx/recipe/classification/multi_class_cls/otx_mobilenet_v3_large.yaml --data_root data/flower_photos --print_config + (otx) ...$ otx build --train-data-roots data/flower_photos --model MobileNet-V3-large-1x - data_root: data/flower_photos - work_dir: otx-regression - callback_monitor: val/accuracy - disable_infer_num_classes: false - engine: - task: MULTI_CLASS_CLS - device: auto - data: - ... + [*] Load Model Template ID: Custom_Image_Classification_MobileNet-V3-large-1x + [*] Load Model Name: MobileNet-V3-large-1x + [*] Saving data configuration file to: ./otx-workspace-CLASSIFICATION/data.yaml + + (otx) ...$ cd ./otx-workspace-CLASSIFICATION + +It will create **otx-workspace-CLASSIFICATION** with all necessary configs for MobileNet-V3-large-1x, prepared ``data.yaml`` to simplify CLI commands launch and splitted dataset named ``splitted_dataset``. 3. To start training you need to call ``otx train`` +command in our workspace: -.. code-block:: shell +.. code-block:: - (otx) ...$ otx train --config src/otx/recipe/classification/multi_class_cls/otx_mobilenet_v3_large.yaml --data_root data/flower_photos + (otx) ...$ otx train That's it! The training will return artifacts: ``weights.pth`` and ``label_schema.json``, which are needed as input for the further commands: ``export``, ``eval``, ``optimize``, etc. diff --git a/docs/source/guide/tutorials/base/how_to_train/detection.rst b/docs/source/guide/tutorials/base/how_to_train/detection.rst index 5be5dc518fa..b2434830c45 100644 --- a/docs/source/guide/tutorials/base/how_to_train/detection.rst +++ b/docs/source/guide/tutorials/base/how_to_train/detection.rst @@ -9,12 +9,18 @@ On this page, we show how to train, validate, export and optimize ATSS model on To have a specific example in this tutorial, all commands will be run on the ATSS model. It's a medium model, that achieves relatively high accuracy while keeping the inference fast. +.. note:: + + To learn how to deploy the trained model and run the exported demo, refer to :doc:`../deploy`. + + To learn how to run the demo in CLI and visualize results, refer to :doc:`../demo`. + The process has been tested on the following configuration. - Ubuntu 20.04 - NVIDIA GeForce RTX 3090 -- Intel(R) Core(TM) i9-11900 -- CUDA Toolkit 11.8 +- Intel(R) Core(TM) i9-10980XE CPU +- CUDA Toolkit 11.1 @@ -28,11 +34,11 @@ to create a universal virtual environment for OpenVINO™ Training Extensions. 2. Activate your virtual environment: -.. code-block:: shell +.. code-block:: - .otx/bin/activate - # or by this line, if you created an environment, using tox - . venv/otx/bin/activate + .otx/bin/activate + # or by this line, if you created an environment, using tox + . venv/otx/bin/activate .. _wgisd_dataset_descpiption: @@ -43,21 +49,21 @@ Dataset preparation .. note:: - Currently, we support the following object detection dataset formats: + Currently, we support the following object detection dataset formats: - - `COCO `_ - - `Pascal-VOC `_ - - `YOLO `_ + - `COCO `_ + - `Pascal-VOC `_ + - `YOLO `_ 1. Clone a repository with `WGISD dataset `_. -.. code-block:: shell +.. code-block:: - mkdir data ; cd data - git clone https://github.com/thsant/wgisd.git - cd wgisd - git checkout 6910edc5ae3aae8c20062941b1641821f0c30127 + mkdir data ; cd data + git clone https://github.com/thsant/wgisd.git + cd wgisd + git checkout 6910edc5ae3aae8c20062941b1641821f0c30127 This dataset contains images of grapevines with the annotation for different varieties of grapes. @@ -81,37 +87,36 @@ It's a great example to start with. The model achieves high accuracy right from 2. To run the training using :doc:`auto-configuration feature <../../../explanation/additional_features/auto_configuration>`, we need to reformat the dataset according to this structure: -.. code-block:: shell - - wgisd - ├── annotations/ - ├── instances_train.json - ├── instances_val.json - (Optional) - └── instances_test.json - ├──images/ - (The split on folders is optional) - ├── train - ├── val - └── test - (There may be more extra unrelated folders) +.. code-block:: + + wgisd + ├── annotations/ + ├── instances_train.json + ├── instances_val.json + (Optional) + └── instances_test.json + ├──images/ + (The split on folders is optional) + ├── train + ├── val + └── test + (There may be more extra unrelated folders) We can do that by running these commands: -.. code-block:: shell +.. code-block:: - # format images folder - mv data images + # format images folder + mv data images - # format annotations folder - mv coco_annotations annotations + # format annotations folder + mv coco_annotations annotations - # rename annotations to meet *_train.json pattern - mv annotations/train_bbox_instances.json annotations/instances_train.json - mv annotations/test_bbox_instances.json annotations/instances_val.json - cp annotations/instances_val.json annotations/instances_test.json + # rename annotations to meet *_train.json pattern + mv annotations/train_bbox_instances.json annotations/instances_train.json + mv annotations/test_bbox_instances.json annotations/instances_val.json - cd ../.. + cd ../.. ********* Training @@ -122,246 +127,202 @@ The list of supported templates for object detection is available with the comma .. note:: - The characteristics and detailed comparison of the models could be found in :doc:`Explanation section <../../../explanation/algorithms/object_detection/object_detection>`. - - -.. tab-set:: + The characteristics and detailed comparison of the models could be found in :doc:`Explanation section <../../../explanation/algorithms/object_detection/object_detection>`. - .. tab-item:: CLI + To modify the architecture of supported models with various backbones, please refer to the :doc:`advanced tutorial for backbone replacement <../../advanced/backbones>`. - .. code-block:: shell - - (otx) ...$ otx find --task DETECTION --pattern atss - ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Task ┃ Model Name ┃ Recipe Path ┃ - ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ DETECTION │ atss_mobilenetv2_tile │ src/otx/recipe/detection/atss_mobilenetv2_tile.yaml │ - │ DETECTION │ atss_r50_fpn │ src/otx/recipe/detection/atss_r50_fpn.yaml │ - │ DETECTION │ atss_resnext101 │ src/otx/recipe/detection/atss_resnext101.yaml │ - │ DETECTION │ atss_mobilenetv2 │ src/otx/recipe/detection/atss_mobilenetv2.yaml │ - └───────────┴───────────────────────┴────────────────────────────────────────────────────────────────┘ - - .. tab-item:: API - - .. code-block:: python - - from otx.engine.utils.api import list_models +.. code-block:: - model_lists = list_models(task="DETECTION", pattern="atss") - print(model_lists) - ''' - [ - 'atss_r50_fpn', - 'atss_mobilenetv2', - 'atss_mobilenetv2_tile', - 'atss_resnext101', - ] - ''' + (otx) ...$ otx find --template --task DETECTION + +-----------+-----------------------------------+------------------+------------------------------------------------------------------------------------+ + | TASK | ID | NAME | BASE PATH | + +-----------+-----------------------------------+------------------+------------------------------------------------------------------------------------+ + | DETECTION | Custom_Object_Detection_Gen3_ATSS | MobileNetV2-ATSS | src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml | + | DETECTION | Object_Detection_ResNeXt101_ATSS | ResNeXt101-ATSS | src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml | + | DETECTION | Custom_Object_Detection_Gen3_SSD | SSD | src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml | + | DETECTION | Object_Detection_YOLOX_L | YOLOX-L | src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml | + | DETECTION | Object_Detection_YOLOX_S | YOLOX-S | src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml | + | DETECTION | Custom_Object_Detection_YOLOX | YOLOX-TINY | src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/template.yaml | + | DETECTION | Object_Detection_YOLOX_X | YOLOX-X | src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml | + +-----------+-----------------------------------+------------------+------------------------------------------------------------------------------------+ .. _detection_workspace: -2. On this step we will configure configuration +2. On this step we will create **otx-workspace-Detection** with: -- all necessary configs for atss_mobilenetv2 +- all necessary configs for Custom_Object_Detection_Gen3_ATSS +- prepared ``data.yaml`` to simplify CLI commands launch - train/validation sets, based on provided annotation. -It may be counterintuitive, but for ``--data_root`` we need to pass the path to the dataset folder root (in our case it's ``data/wgisd``) instead of the folder with validation images. +It may be counterintuitive, but for ``--train-data-roots`` we need to pass the path to the dataset folder root (in our case it's ``data/wgisd``) instead of the folder with validation images. This is because the function automatically detects annotations and images according to the expected folder structure we achieved above. +So, if you'd like to add ``--val-data-roots``, please note, that it should also be a path to a dataset folder root. -Let's check the object detection configuration running the following command: - -.. code-block:: shell - - # or its config path - (otx) ...$ otx train --config src/otx/recipe/detection/atss_mobilenetv2.yaml --data_root data/wgisd --print_config - - ... - data_root: data/wgisd - work_dir: otx-workspace - callback_monitor: val/map_50 - disable_infer_num_classes: false - engine: - task: DETECTION - device: auto - data: - ... - -.. note:: +On contrary, if we omit adding ``--val-data-roots``, the function will find images for validation according to validation annotation and create ``splitted_dataset`` folder inside the workplace with the desired split. - If you want to get configuration as yaml file, please use ``--print_config`` parameter and ``> configs.yaml``. +Let's prepare the object detection workspace running the following command: - .. code-block:: shell - - (otx) ...$ otx train --config src/otx/recipe/detection/atss_mobilenetv2.yaml --data_root data/wgisd --print_config > configs.yaml - # Update configs.yaml & Train configs.yaml - (otx) ...$ otx train --config configs.yaml - - -3. ``otx train`` trains a model (a particular model template) -on a dataset and results: - -Here are the main outputs can expect with CLI: -- ``{work_dir}/{timestamp}/checkpoints/epoch_*.ckpt`` - a model checkpoint file. -- ``{work_dir}/{timestamp}/configs.yaml`` - The configuration file used in the training can be reused to reproduce the training. -- ``{work_dir}/.latest`` - The results of each of the most recently executed subcommands are soft-linked. This allows you to skip checkpoints and config file entry as a workspace. - -.. tab-set:: +.. code-block:: - .. tab-item:: CLI (auto-config) + # we can specify the template by its ID + (otx) ...$ otx build Custom_Object_Detection_Gen3_ATSS --train-data-roots data/wgisd - .. code-block:: shell + # or its name + (otx) ...$ otx build MobileNetV2-ATSS --train-data-roots data/wgisd - (otx) ...$ otx train --data_root data/wgisd + # or its path + (otx) ...$ otx build src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml --train-data-roots data/wgisd - .. tab-item:: CLI (with config) + ... + [*] Workspace Path: otx-workspace-DETECTION + [*] Load Model Template ID: Custom_Object_Detection_Gen3_ATSS + [*] Load Model Name: MobileNetV2-ATSS + [*] - Updated: otx-workspace-DETECTION/model.py + [*] - Updated: otx-workspace-DETECTION/data_pipeline.py + [*] - Updated: otx-workspace-DETECTION/tile_pipeline.py + [*] - Updated: otx-workspace-DETECTION/deployment.py + [*] - Updated: otx-workspace-DETECTION/hpo_config.yaml + [*] - Updated: otx-workspace-DETECTION/compression_config.json + [*] Found validation data in your dataset. It'll be used as validation data. + [*] Update data configuration file to: otx-workspace-DETECTION/data.yaml - .. code-block:: shell - (otx) ...$ otx train --config src/otx/recipe/detection/atss_mobilenetv2.yaml --data_root data/wgisd - .. tab-item:: API (from_config) +.. warning:: - .. code-block:: python + If you want to rebuild your current workspace by running ``otx build`` with other parameters, it's better to delete the original workplace before that to prevent mistakes. - from otx.engine import Engine +Check ``otx-workspace-DETECTION/data.yaml`` to ensure, which data subsets will be used for training and validation, and update it if necessary. - data_root = "data/wgisd" - recipe = "src/otx/recipe/detection/atss_mobilenetv2.yaml" +.. code-block:: - engine = Engine.from_config( - config_path=recipe, - data_root=data_root, - work_dir="otx-workspace", - ) + data: + train: + ann-files: null + data-roots: /otx-workspace-DETECTION/splitted_dataset/train + val: + ann-files: null + data-roots: /otx-workspace-DETECTION/splitted_dataset/val + test: + ann-files: null + data-roots: null + unlabeled: + file-list: null + data-roots: null + + +We also can modify the backbone of the model, by adding ``--backbone`` parameter. +We can find the available backbone by running ``otx find`` with the framework parameter. +Learn more about modified backbones in :doc:`advanced tutorial for backbone replacement <../../advanced/backbones>`. - engine.train(...) +3. ``otx train`` trains a model (a particular model template) +on a dataset and results in two files: - .. tab-item:: API +- ``weights.pth`` - a model snapshot +- ``label_schema.json`` - a label schema used in training, created from a dataset - .. code-block:: python +These are needed as inputs for the further commands: ``export``, ``eval``, ``optimize``, ``deploy`` and ``demo``. - from otx.engine import Engine - data_root = "data/wgisd" +4. The following command line starts training of the medium object +detection model on the first GPU on WGISD dataset: - engine = Engine( - model="atss_mobilenetv2, - data_root=data_root, - work_dir="otx-workspace", - ) +.. code-block:: - engine.train(...) + (otx) ...$ cd otx-workspace-DETECTION/ + (otx) ...$ otx train --output ../outputs --workspace ../outputs/logs --gpus 1 +To start multi-gpu training, list the indexes of GPUs you want to train on or omit `gpus` parameter, so training will run on all available GPUs. 4. ``(Optional)`` Additionally, we can tune training parameters such as batch size, learning rate, patience epochs or warm-up iterations. -Learn more about specific parameters using ``otx train --help -v`` or ``otx train --help -vv``. +Learn more about template-specific parameters using ``otx train params --help``. -For example, to decrease the batch size to 4, fix the number of epochs to 100, extend the command line above with the following line. +It can be done by manually updating parameters in the ``template.yaml`` file in your workplace or via the command line. -.. tab-set:: +For example, to decrease the batch size to 4, fix the number of epochs to 100 and disable early stopping, extend the command line above with the following line. - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx train ... --data.config.train_subset.batch_size 4 \ - --max_epochs 100 - - .. tab-item:: API - - .. code-block:: python - - from otx.core.config.data import DataModuleConfig, SubsetConfig - from otx.core.data.module import OTXDataModule - from otx.engine import Engine - - data_config = DataModuleConfig(..., train_subset=SubsetConfig(..., batch_size=4)) - datamodule = OTXDataModule(..., config=data_config) - - engine = Engine(..., datamodule=datamodule) +.. code-block:: - engine.train(max_epochs=100) + params --learning_parameters.batch_size 4 + --learning_parameters.num_iters 100 \ + --learning_parameters.enable_early_stopping false -5. The training result ``checkpoints/*.ckpt`` file is located in ``{work_dir}`` folder, -while training logs can be found in the ``{work_dir}/{timestamp}`` dir. +5. The training results are ``weights.pth`` and ``label_schema.json`` files that located in ``outputs`` folder, +while training logs can be found in the ``outputs/logs`` dir. .. note:: - We also can visualize the training using ``Tensorboard`` as these logs are located in ``{work_dir}/{timestamp}/tensorboard``. + We also can visualize the training using ``Tensorboard`` as these logs are located in ``outputs/logs/tf_logs``. .. code-block:: - otx-workspace - ├── outputs/ - ├── 20240403_134256/ - ├── csv/ - ├── checkpoints/ - | └── epoch_*.pth - ├── tensorboard/ - └── configs.yaml - └── .latest - └── train/ - ... + ... + 2023-01-10 05:40:21,520 | INFO : Update Lr patience: 3 + 2023-01-10 05:40:21,520 | INFO : Update Validation Interval: 2 + 2023-01-10 05:40:21,520 | INFO : Update Early-Stop patience: 5 + 2023-01-10 05:40:23,140 | INFO : Epoch [1][1/31] lr: 1.333e-03, eta: 11 days, 14:44:47, time: 1.619, data_time: 0.961, memory: 4673, current_iters: 0, loss_cls: 1.1261, loss_bbox: 0.6514, loss_centerness: 0.6337, loss: 2.4112, grad_norm: 18.5789 -The training time highly relies on the hardware characteristics, for example on 1 NVIDIA GeForce RTX 3090 the training took about 3 minutes. + ... + 2023-01-10 05:52:33,985 | INFO : run task done. + 2023-01-10 05:52:35,682 | INFO : Inference completed + 2023-01-10 05:52:35,683 | INFO : called evaluate() + 2023-01-10 05:52:35,907 | INFO : F-measure after evaluation: 0.5487693710118504 + 2023-01-10 05:52:35,907 | INFO : Evaluation completed + Performance(score: 0.5487693710118504, dashboard: (1 metric groups)) + +The training time highly relies on the hardware characteristics, for example on 1 NVIDIA GeForce RTX 3090 the training took about 15 minutes. After that, we have the PyTorch object detection model trained with OpenVINO™ Training Extensions, which we can use for evaliation, export, optimization and deployment. *********** -Evaluation +Validation *********** -1. ``otx test`` runs evaluation of a +1. ``otx eval`` runs evaluation of a trained model on a particular dataset. -Test function receives test annotation information and model snapshot, trained in previous step. +Eval function receives test annotation information and model snapshot, trained in previous step. +Please note, ``label_schema.json`` file contains meta information about the dataset and it should be located in the same folder as the model snapshot. -The default metric is mAP_50 measure. +The default metric is F1 measure. -2. That's how we can evaluate the snapshot in ``otx-workspace`` -folder on WGISD dataset and save results to ``otx-workspace``: +2. That's how we can evaluate the snapshot in ``outputs`` +folder on WGISD dataset and save results to ``outputs/performance``: -.. tab-set:: +.. code-block:: - .. tab-item:: CLI (with work_dir) + (otx) ...$ otx eval --test-data-roots splitted_dataset/val \ + --load-weights ../outputs/weights.pth \ + --output ../outputs/ - .. code-block:: shell - (otx) ...$ otx test --work_dir otx-workspace - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Test metric ┃ DataLoader 0 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ test/data_time │ 0.025369757786393166 │ - │ test/map_50 │ 0.8693901896476746 │ - │ test/iter_time │ 0.08180806040763855 │ - └───────────────────────────┴───────────────────────────┘ +3. The output of ``../outputs/performance.json`` consists of +a dict with target metric name and its value. - .. tab-item:: CLI (with config) +.. code-block:: - .. code-block:: shell + {"f-measure": 0.5487693710118504} - (otx) ...$ otx test --config src/otx/recipe/detection/atss_mobilenetv2.yaml \ - --data_root data/wgisd \ - --checkpoint otx-workspace/20240312_051135/checkpoints/epoch_033.ckpt - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Test metric ┃ DataLoader 0 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ test/data_time │ 0.025369757786393166 │ - │ test/map_50 │ 0.8693901896476746 │ - │ test/iter_time │ 0.08180806040763855 │ - └───────────────────────────┴───────────────────────────┘ +4. ``Optional`` Additionally, we can tune evaluation parameters such as confidence threshold via the command line. +Learn more about template-specific parameters using ``otx eval params --help``. - .. tab-item:: API +For example, if there are too many False-Positive predictions (there we have a prediction, but don't have annotated object for it), we can suppress its number by increasing the confidence threshold as it is shown below. - .. code-block:: python +Please note, by default, the optimal confidence threshold is detected based on validation results to maximize the final F1 metric. To set a custom confidence threshold, please disable ``result_based_confidence_threshold`` option. - engine.test() +.. code-block:: + (otx) ...$ otx eval --test-data-roots splitted_dataset/val \ + --load-weights ../outputs/weights.pth \ + --output ../outputs + params \ + --postprocessing.confidence_threshold 0.5 \ + --postprocessing.result_based_confidence_threshold false -3. The output of ``{work_dir}/{timestamp}/csv/version_0/metrics.csv`` consists of -a dict with target metric name and its value. + ... + 2023-01-10 06:21:04,254 | INFO : F-measure after evaluation: 0.514346439957492 ********* Export @@ -369,138 +330,53 @@ Export 1. ``otx export`` exports a trained Pytorch `.pth` model to the OpenVINO™ Intermediate Representation (IR) format. It allows to efficiently run it on Intel hardware, especially on CPU, using OpenVINO™ runtime. -Also, the resulting IR model is required to run PTQ optimization in the section below. IR model contains 2 files: ``exported_model.xml`` for weights and ``exported_model.bin`` for architecture. - -2. That's how we can export the trained model ``{work_dir}/{timestamp}/checkpoints/epoch_*.ckpt`` -from the previous section and save the exported model to the ``{work_dir}/{timestamp}/`` folder. - -.. tab-set:: - - .. tab-item:: CLI (with work_dir) - - .. code-block:: shell - - (otx) ...$ otx export --work_dir otx-workspace - ... - Elapsed time: 0:00:06.588245 - - .. tab-item:: CLI (with config) +Also, the resulting IR model is required to run PTQ optimization in the section below. IR model contains 2 files: ``openvino.xml`` for weights and ``openvino.bin`` for architecture. - .. code-block:: shell +2. That's how we can export the trained model ``../outputs/weights.pth`` +from the previous section and save the exported model to the ``../outputs/openvino/`` folder. - (otx) ...$ otx export ... --checkpoint otx-workspace/20240312_051135/checkpoints/epoch_033.ckpt - ... - Elapsed time: 0:00:06.588245 +.. code-block:: - .. tab-item:: API + (otx) ...$ otx export --load-weights ../outputs/weights.pth \ + --output ../outputs/openvino/ - .. code-block:: python + ... - engine.export() + 2023-01-10 06:23:41,621 | INFO : run task done. + 2023-01-10 06:23:41,630 | INFO : Exporting completed 3. We can check the accuracy of the IR model and the consistency between the exported model and the PyTorch model, -using ``otx test`` and passing the IR model path to the ``--checkpoint`` parameter. - -.. tab-set:: - - .. tab-item:: CLI (with work_dir) - - .. code-block:: shell - - (otx) ...$ otx test --work_dir otx-workspace \ - --checkpoint otx-workspace/20240312_052847/exported_model.xml \ - --engine.device cpu - ... - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Test metric ┃ DataLoader 0 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ test/map │ 0.5444773435592651 │ - │ test/map_50 │ 0.8693901896476746 │ - │ test/map_75 │ 0.5761404037475586 │ - │ test/map_large │ 0.561242401599884 │ - │ test/map_medium │ 0.2926788330078125 │ - │ test/map_per_class │ -1.0 │ - │ test/map_small │ -1.0 │ - │ test/mar_1 │ 0.055956535041332245 │ - │ test/mar_10 │ 0.45759353041648865 │ - │ test/mar_100 │ 0.6809769868850708 │ - │ test/mar_100_per_class │ -1.0 │ - │ test/mar_large │ 0.6932432055473328 │ - │ test/mar_medium │ 0.46584922075271606 │ - │ test/mar_small │ -1.0 │ - └───────────────────────────┴───────────────────────────┘ - - .. tab-item:: CLI (with config) - - .. code-block:: shell - - (otx) ...$ otx test --config src/otx/recipe/detection/atss_mobilenetv2.yaml \ - --data_root data/wgisd \ - --checkpoint otx-workspace/20240312_052847/exported_model.xml \ - --engine.device cpu - ... - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Test metric ┃ DataLoader 0 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ test/map │ 0.5444773435592651 │ - │ test/map_50 │ 0.8693901896476746 │ - │ test/map_75 │ 0.5761404037475586 │ - │ test/map_large │ 0.561242401599884 │ - │ test/map_medium │ 0.2926788330078125 │ - │ test/map_per_class │ -1.0 │ - │ test/map_small │ -1.0 │ - │ test/mar_1 │ 0.055956535041332245 │ - │ test/mar_10 │ 0.45759353041648865 │ - │ test/mar_100 │ 0.6809769868850708 │ - │ test/mar_100_per_class │ -1.0 │ - │ test/mar_large │ 0.6932432055473328 │ - │ test/mar_medium │ 0.46584922075271606 │ - │ test/mar_small │ -1.0 │ - └───────────────────────────┴───────────────────────────┘ - - .. tab-item:: API - - .. code-block:: python - - exported_model = engine.export() - engine.test(checkpoint=exported_model) - - -4. ``Optional`` Additionally, we can tune confidence threshold via the command line. -Learn more about template-specific parameters using ``otx export --help``. - -For example, If you want to get the ONNX model format you can run it like below. - -.. tab-set:: - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx export ... --checkpoint otx-workspace/20240312_051135/checkpoints/epoch_033.ckpt --export_format ONNX - - .. tab-item:: API +using ``otx eval`` and passing the IR model path to the ``--load-weights`` parameter. - .. code-block:: python - - engine.export(..., export_format="ONNX") +.. code-block:: -If you also want to export ``saliency_map``, a feature related to explain, and ``feature_vector`` information for XAI, you can do the following. + (otx) ...$ otx eval --test-data-roots splitted_dataset/val \ + --load-weights ../outputs/openvino/openvino.xml \ + --output ../outputs -.. tab-set:: + ... + 2023-01-10 06:24:50,382 | INFO : Start OpenVINO inference + 2023-01-10 06:24:54,943 | INFO : OpenVINO inference completed + 2023-01-10 06:24:54,944 | INFO : Start OpenVINO metric evaluation + 2023-01-10 06:24:55,117 | INFO : OpenVINO metric evaluation completed + Performance(score: 0.5487693710118504, dashboard: (1 metric groups)) - .. tab-item:: CLI - .. code-block:: shell +4. ``Optional`` Additionally, we can tune confidence threshold via the command line. +Learn more about template-specific parameters using ``otx export params --help``. - (otx) ...$ otx export ... --checkpoint otx-workspace/20240312_051135/checkpoints/epoch_033.ckpt --explain True +For example, if there are too many False-Positive predictions (there we have a prediction, but don't have annotated object for it), we can suppress its number by increasing the confidence threshold as it is shown below. - .. tab-item:: API +Please note, by default, the optimal confidence threshold is detected based on validation results to maximize the final F1 metric. To set a custom confidence threshold, please disable ``result_based_confidence_threshold`` option. - .. code-block:: python +.. code-block:: - engine.export(..., explain=True) + (otx) ...$ otx export --load-weights ../outputs/weights.pth \ + --output ../outputs \ + params \ + --postprocessing.confidence_threshold 0.5 \ + --postprocessing.result_based_confidence_threshold false ************* @@ -508,66 +384,69 @@ Optimization ************* 1. We can further optimize the model with ``otx optimize``. -It uses PTQ depending on the model and transforms it to ``INT8`` format. - -``PTQ`` optimization is used for models exported in the OpenVINO™ IR format. It decreases the floating-point precision to integer precision of the exported model by performing the post-training optimization. +It uses NNCF or PTQ depending on the model and transforms it to ``INT8`` format. -To learn more about optimization, refer to `NNCF repository `_. +``NNCF`` optimization is used for trained snapshots in a framework-specific format such as checkpoint (.pth) file from Pytorch. It starts accuracy-aware quantization based on the obtained weights from the training stage. Generally, we will see the same output as during training. -2. Command example for optimizing OpenVINO™ model (.xml) -with OpenVINO™ PTQ. - -.. tab-set:: +``PTQ`` optimization is used for models exported in the OpenVINO™ IR format. It decreases the floating-point precision to integer precision of the exported model by performing the post-training optimization. - .. tab-item:: CLI +The function results with the following files, which could be used to run :doc:`otx demo <../demo>` as well with PyTorch (`.pth`) and IR model (`.xml`): - .. code-block:: shell +- ``confidence_threshold`` +- ``config.json`` +- ``label_schema.json`` +- ``openvino.bin`` +- ``openvino.xml`` - (otx) ...$ otx optimize --work_dir otx-workspace \ - --checkpoint otx-workspace/20240312_052847/exported_model.xml +To learn more about optimization, refer to `NNCF repository `_. - ... - Statistics collection ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 30/30 • 0:00:14 • 0:00:00 - Applying Fast Bias correction ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 58/58 • 0:00:02 • 0:00:00 - Elapsed time: 0:00:24.958733 +2. Command example for optimizing a PyTorch model (`.pth`) +with OpenVINO NNCF. - .. tab-item:: API +.. code-block:: - .. code-block:: python + (otx) ...$ otx optimize --load-weights ../outputs/weights.pth \ + --output ../outputs/nncf \ + --output ../outputs/nncf - ckpt_path = "otx-workspace/20240312_052847/exported_model.xml" - engine.optimize(checkpoint=ckpt_path) + ... + 2023-01-17 06:46:08,208 | INFO : run task done. + 2023-01-17 06:46:08,618 | INFO : Inference completed + 2023-01-17 06:46:08,618 | INFO : called evaluate() + 2023-01-17 06:46:08,829 | INFO : F-measure after evaluation: 0.5446735395189003 + 2023-01-17 06:46:08,829 | INFO : Evaluation completed + Performance(score: 0.5446735395189003, dashboard: (1 metric groups)) -The optimization time highly relies on the hardware characteristics, for example on 1 NVIDIA GeForce RTX 3090 it took about 10 minutes. -Please note, that PTQ will take some time without logging to optimize the model. -3. Finally, we can also evaluate the optimized model by passing -it to the ``otx test`` function. +3. Command example for optimizing OpenVINO™ model (.xml) +with OpenVINO™ PTQ. -.. tab-set:: +.. code-block:: - .. tab-item:: CLI + (otx) ...$ otx optimize --load-weights ../outputs/openvino/openvino.xml \ + --output ../outputs/ptq \ + --output ../outputs/ptq - .. code-block:: shell + ... - (otx) ...$ otx test --work_dir otx-workspace \ - --checkpoint otx-workspace/20240312_055042/optimized_model.xml \ - --engine.device cpu + 2023-01-10 06:29:46,751 | INFO : Loading OpenVINO OTXDetectionTask + 2023-01-10 06:29:47,685 | INFO : OpenVINO task initialization completed + 2023-01-10 06:29:47,685 | INFO : Start PTQ optimization + 2023-01-10 06:34:29,304 | INFO : PTQ optimization completed + 2023-01-10 06:34:29,419 | INFO : Start OpenVINO inference + 2023-01-10 06:34:33,275 | INFO : OpenVINO inference completed + 2023-01-10 06:34:33,275 | INFO : Start OpenVINO metric evaluation + 2023-01-10 06:34:33,451 | INFO : OpenVINO metric evaluation completed + Performance(score: 0.5389435989256938, dashboard: (1 metric groups)) - ... - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Test metric ┃ DataLoader 0 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ test/map_50 │ 0.8693901896476746 │ - └───────────────────────────┴───────────────────────────┘ - Elapsed time: 0:00:10.260521 +The optimization time highly relies on the hardware characteristics, for example on 1 NVIDIA GeForce RTX 3090 it took about 10 minutes. +Please note, that PTQ will take some time without logging to optimize the model. - .. tab-item:: API +4. Finally, we can also evaluate the optimized model by passing +it to the ``otx eval`` function. - .. code-block:: python +Now we have fully trained, optimized and exported an efficient model representation ready-to-use object detection model. - ckpt_path = "otx-workspace/20240312_055042/optimized_model.xml" - engine.test(checkpoint=ckpt_path) +The following tutorials provide further steps how to :doc:`deploy <../deploy>` and use your model in the :doc:`demonstration mode <../demo>` and visualize results. -Now we have fully trained, optimized and exported an efficient model representation ready-to-use object detection model. diff --git a/docs/source/guide/tutorials/base/how_to_train/index.rst b/docs/source/guide/tutorials/base/how_to_train/index.rst index edf5816d514..35403321d73 100644 --- a/docs/source/guide/tutorials/base/how_to_train/index.rst +++ b/docs/source/guide/tutorials/base/how_to_train/index.rst @@ -1,51 +1,8 @@ How to train, validate, export and optimize the model ================================================================ -.. grid:: 1 2 2 3 - :margin: 1 1 0 0 - :gutter: 1 - - .. grid-item-card:: Classification - :link: classification - :link-type: doc - :text-align: center - - .. grid-item-card:: Detection - :link: detection - :link-type: doc - :text-align: center - - .. grid-item-card:: Instance Segmentation - :link: instance_segmentation - :link-type: doc - :text-align: center - - .. grid-item-card:: Semantic Segmentation - :link: semantic_segmentation - :link-type: doc - :text-align: center - - .. grid-item-card:: Anomaly Task - :link: anomaly_detection - :link-type: doc - :text-align: center - - .. grid-item-card:: Action Classification - :link: action_classification - :link-type: doc - :text-align: center - - .. grid-item-card:: Action Detection - :link: action_detection - :link-type: doc - :text-align: center - - .. grid-item-card:: Visual Prompting - :text-align: center - .. toctree:: :maxdepth: 1 - :hidden: classification detection diff --git a/docs/source/guide/tutorials/base/how_to_train/instance_segmentation.rst b/docs/source/guide/tutorials/base/how_to_train/instance_segmentation.rst index 69e699ac2ae..3d9526d92ab 100644 --- a/docs/source/guide/tutorials/base/how_to_train/instance_segmentation.rst +++ b/docs/source/guide/tutorials/base/how_to_train/instance_segmentation.rst @@ -11,12 +11,16 @@ To learn more about Instance Segmentation task, refer to :doc:`../../../explanat To learn deeper how to manage training process of the model including additional parameters and its modification. + To learn how to deploy the trained model, refer to: :doc:`../deploy`. + + To learn how to run the demo and visualize results, refer to: :doc:`../demo`. + The process has been tested on the following configuration. - Ubuntu 20.04 - NVIDIA GeForce RTX 3090 - Intel(R) Core(TM) i9-11900 -- CUDA Toolkit 11.8 +- CUDA Toolkit 11.7 ************************* Setup virtual environment @@ -28,7 +32,7 @@ to create a universal virtual environment for OpenVINO™ Training Extensions. 2. Activate your virtual environment: -.. code-block:: shell +.. code-block:: .otx/bin/activate # or by this line, if you created an environment, using tox @@ -47,13 +51,14 @@ Dataset preparation 1. Clone a repository with -`car-seg dataset `_. +`WGISD dataset `_. -.. code-block:: shell +.. code-block:: mkdir data ; cd data - wget https://ultralytics.com/assets/carparts-seg.zip - unzip carparts-seg.zip + git clone https://github.com/thsant/wgisd.git + cd wgisd + git checkout 6910edc5ae3aae8c20062941b1641821f0c30127 This dataset contains images of grapevines with the annotation for different varieties of grapes. @@ -75,7 +80,7 @@ This dataset contains images of grapevines with the annotation for different var 2. Check the file structure of downloaded dataset, we will need the following file structure: -.. code-block:: shell +.. code-block:: wgisd ├── annotations/ @@ -92,7 +97,7 @@ we will need the following file structure: We can do that by running these commands: -.. code-block:: shell +.. code-block:: # format images folder mv data images @@ -101,9 +106,8 @@ We can do that by running these commands: mv coco_annotations annotations # rename annotations to meet *_train.json pattern - mv annotations/train_polygons_instances.json annotations/instances_train.json - mv annotations/test_polygons_instances.json annotations/instances_val.json - cp annotations/instances_val.json annotations/instances_test.json + mv annotations/train_bbox_instances.json annotations/instances_train.json + mv annotations/test_bbox_instances.json annotations/instances_val.json cd ../.. @@ -121,146 +125,113 @@ The list of supported templates for instance segmentation is available with the The characteristics and detailed comparison of the models could be found in :doc:`Explanation section <../../../explanation/algorithms/segmentation/instance_segmentation>`. + To modify the architecture of supported models with various backbones, please refer to the :doc:`advanced tutorial for backbone replacement <../../advanced/backbones>`. -.. tab-set:: - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx find --task INSTANCE_SEGMENTATION - - ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Task ┃ Model Name ┃ Recipe Path ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ INSTANCE_SEGMENTATION │ openvino_model │ src/otx/recipe/instance_segmentation/openvino_model.yaml │ - │ INSTANCE_SEGMENTATION │ maskrcnn_r50 │ src/otx/recipe/instance_segmentation/maskrcnn_r50.yaml │ - │ INSTANCE_SEGMENTATION │ maskrcnn_r50_tile │ src/otx/recipe/instance_segmentation/maskrcnn_r50_tile.yaml │ - │ INSTANCE_SEGMENTATION │ maskrcnn_swint │ src/otx/recipe/instance_segmentation/maskrcnn_swint.yaml │ - │ INSTANCE_SEGMENTATION │ maskrcnn_efficientnetb2b │ src/otx/recipe/instance_segmentation/maskrcnn_efficientnetb2b.yaml │ - │ INSTANCE_SEGMENTATION │ rtmdet_inst_tiny │ src/otx/recipe/instance_segmentation/rtmdet_inst_tiny.yaml │ - │ INSTANCE_SEGMENTATION │ maskrcnn_efficientnetb2b_tile │ src/otx/recipe/instance_segmentation/maskrcnn_efficientnetb2b_tile.yaml │ - │ INSTANCE_SEGMENTATION │ maskrcnn_swint_tile │ src/otx/recipe/instance_segmentation/maskrcnn_swint_tile.yaml │ - └───────────────────────┴───────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────┘ - - .. tab-item:: API - - .. code-block:: python - - from otx.engine.utils.api import list_models +.. code-block:: - model_lists = list_models(task="INSTANCE_SEGMENTATION") - print(model_lists) - ''' - [ - 'maskrcnn_swint', - 'maskrcnn_r50', - 'maskrcnn_r50_tile', - 'rtmdet_inst_tiny', - 'maskrcnn_swint_tile', - 'maskrcnn_efficientnetb2b_tile', - 'openvino_model', - 'maskrcnn_efficientnetb2b', - ] - ''' + (otx) ...$ otx find --template --task instance_segmentation -2. On this step we will configure configuration -with: + +-----------------------+----------------------------------------------------------------+--------------------------+---------------------------------------------------------------------------------------------------+ + | TASK | ID | NAME | BASE PATH | + +-----------------------+----------------------------------------------------------------+--------------------------+---------------------------------------------------------------------------------------------------+ + | INSTANCE_SEGMENTATION | Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50 | MaskRCNN-ResNet50 | src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/template.yaml | + | INSTANCE_SEGMENTATION | Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B | MaskRCNN-EfficientNetB2B | src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/template.yaml | + | INSTANCE_SEGMENTATION | Custom_Counting_Instance_Segmentation_MaskRCNN_ConvNeXt | MaskRCNN-ConvNeXt | src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/template.yaml | + +-----------------------+----------------------------------------------------------------+--------------------------+---------------------------------------------------------------------------------------------------+ -- all necessary configs for maskrcnn_r50 -- train/validation sets, based on provided annotation. +2. We need to create +OpenVINO™ Training Extensions workspace first. -It may be counterintuitive, but for ``--data_root`` we need to pass the path to the dataset folder root (in our case it's ``data/wgisd``) instead of the folder with validation images. -This is because the function automatically detects annotations and images according to the expected folder structure we achieved above. +Let's prepare an OpenVINO™ Training Extensions instance segmentation workspace running the following command: -Let's check the object detection configuration running the following command: +.. code-block:: -.. code-block:: shell + (otx) ...$ otx build --task instance_segmentation - # or its config path - (otx) ...$ otx train --config src/otx/recipe/instance_segmentation/maskrcnn_r50.yaml --data_root data/wgisd --print_config + [*] Workspace Path: otx-workspace-INSTANCE_SEGMENTATION + [*] Load Model Template ID: Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50 + [*] Load Model Name: MaskRCNN-ResNet50 + [*] - Updated: otx-workspace-INSTANCE_SEGMENTATION/model.py + [*] - Updated: otx-workspace-INSTANCE_SEGMENTATION/data_pipeline.py + [*] - Updated: otx-workspace-INSTANCE_SEGMENTATION/tile_pipeline.py + [*] - Updated: otx-workspace-INSTANCE_SEGMENTATION/deployment.py + [*] - Updated: otx-workspace-INSTANCE_SEGMENTATION/hpo_config.yaml + [*] - Updated: otx-workspace-INSTANCE_SEGMENTATION/compression_config.json + [*] Update data configuration file to: otx-workspace-INSTANCE_SEGMENTATION/data.yaml - ... - data_root: data/wgisd - work_dir: otx-workspace - callback_monitor: val/map_50 - disable_infer_num_classes: false - engine: - task: INSTANCE_SEGMENTATION - device: auto - data: - ... + (otx) ...$ cd ./otx-workspace-INSTANCE_SEGMENTATION .. note:: + The default model for instance segmentation is MaskRCNN-ResNet50. + If you want to use a different model, use the commands below. - If you want to get configuration as yaml file, please use ``--print_config`` parameter and ``> configs.yaml``. + .. code-block:: - .. code-block:: shell + (otx) ...$ otx build --task instance_segmentation --model - (otx) ...$ otx train --config src/otx/recipe/instance_segmentation/maskrcnn_r50.yaml --data_root data/wgisd --print_config > configs.yaml - # Update configs.yaml & Train configs.yaml - (otx) ...$ otx train --config configs.yaml +It will create **otx-workspace-INSTANCE_SEGMENTATION** with all necessary configs for MaskRCNN-ResNet50, prepared ``data.yaml`` to simplify CLI commands launch and splitted dataset. -3. To start training we need to call ``otx train`` - -Here are the main outputs can expect with CLI: -- ``{work_dir}/{timestamp}/checkpoints/epoch_*.ckpt`` - a model checkpoint file. -- ``{work_dir}/{timestamp}/configs.yaml`` - The configuration file used in the training can be reused to reproduce the training. -- ``{work_dir}/.latest`` - The results of each of the most recently executed subcommands are soft-linked. This allows you to skip checkpoints and config file entry as a workspace. - -.. tab-set:: - - .. tab-item:: CLI (auto-config) - - .. code-block:: shell - - (otx) ...$ otx train --data_root data/wgisd --task INSTANCE_SEGMENTATION - - .. tab-item:: CLI (with config) - - .. code-block:: shell - - (otx) ...$ otx train --config src/otx/recipe/instance_segmentation/maskrcnn_r50.yaml --data_root data/wgisd +.. note:: + Using ``otx train`` with TEMPLATE allows you to run the training directly without ``otx build``. - .. tab-item:: API (from_config) + However, this requires ``--train-data-roots`` and ``--val-data-roots`` in the command. - .. code-block:: python + .. code-block:: - from otx.engine import Engine + (otx) ...$ otx train Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50 \ + --train-data-roots /wgisd \ + --val-data-roots /wgisd \ + params --learning_parameters.num_iters 8 - data_root = "data/wgisd" - recipe = "src/otx/recipe/instance_segmentation/maskrcnn_r50.yaml" + The command above also creates an ``otx-workspace-INSTANCE_SEGMENTATION``, just like running build. This also updates ``data.yaml`` with data-specific commands. - engine = Engine.from_config( - config_path=recipe, - data_root=data_root, - work_dir="otx-workspace", - ) +.. warning:: + Note, that we can't run CLI commands for instance segmentation via model name, since the same models are utilized for different algorithm and the behavior can be unpredictable. + Please, use the template path or template ID instead. - engine.train(...) +To simplify the command line functions calling, we may create a ``data.yaml`` file with annotations info and pass it as a ``--data`` parameter. +The content of the ``otx-workspace-INSTANCE_SEGMENTATION/data.yaml`` for dataset should have absolute paths and will be similar to that: - .. tab-item:: API +Check ``otx-workspace-INSTANCE_SEGMENTATION/data.yaml`` to ensure, which data subsets will be used for training and validation, and update it if necessary. - .. code-block:: python +.. code-block:: - from otx.engine import Engine + data: + train: + ann-files: null + data-roots: /wgisd + val: + ann-files: null + data-roots: /wgisd + test: + ann-files: null + data-roots: null + unlabeled: + file-list: null + data-roots: null - data_root = "data/wgisd" +3. To start training we need to call ``otx train`` +command in our workspace: - engine = Engine( - model="maskrcnn_r50", - task="INSTANCE_SEGMENTATION", - data_root=data_root, - work_dir="otx-workspace", - ) +.. code-block:: - engine.train(...) + (otx) .../otx-workspace-INSTANCE_SEGMENTATION$ otx train -.. note:: + ... + 2023-04-26 10:55:29,312 | INFO : Update LrUpdaterHook patience: 3 -> 3 + 2023-04-26 10:55:29,312 | INFO : Update CheckpointHook interval: 1 -> 2 + 2023-04-26 10:55:29,312 | INFO : Update EvalHook interval: 1 -> 2 + 2023-04-26 10:55:29,312 | INFO : Update EarlyStoppingHook patience: 10 -> 5 + 2023-04-26 10:55:46,681 | INFO : Epoch [1][28/28] lr: 5.133e-04, eta: 2:54:03, time: 1.055, data_time: 0.658, memory: 7521, current_iters: 27, loss_rpn_cls: 0.2227, loss_rpn_bbox: 0.1252, loss_cls: 1.0220, acc: 77.4606, loss_bbox: 0.7682, loss_mask: 1.1534, loss: 3.2915, grad_norm: 14.0078 - Because the dataset structure is mostly the same as detection, INSTANCE_SEGMENTATION requires the task type to be specified to enable auto-configuration. + ... + 2023-04-26 11:32:36,162 | INFO : called evaluate() + 2023-04-26 11:32:36,511 | INFO : F-measure after evaluation: 0.5576271186440678 + 2023-04-26 11:32:36,511 | INFO : Evaluation completed + Performance(score: 0.5576271186440678, dashboard: (1 metric groups)) + otx train time elapsed: 0:20:23.541362 -The training time highly relies on the hardware characteristics, for example on 1 NVIDIA GeForce RTX 3090 the training took about 10 minutes with full dataset. +The training time highly relies on the hardware characteristics, for example on 1 NVIDIA GeForce RTX 3090 the training took about 20 minutes with full dataset. 4. ``(Optional)`` Additionally, we can tune training parameters such as batch size, learning rate, patience epochs or warm-up iterations. Learn more about template-specific parameters using ``otx train params --help``. @@ -269,49 +240,37 @@ It can be done by manually updating parameters in the ``template.yaml`` file in For example, to decrease the batch size to 4, fix the number of epochs to 100 and disable early stopping, extend the command line above with the following line. -.. tab-set:: - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx train ... --data.config.train_subset.batch_size 4 \ - --max_epochs 100 - - .. tab-item:: API - - .. code-block:: python - - from otx.core.config.data import DataModuleConfig, SubsetConfig - from otx.core.data.module import OTXDataModule - from otx.engine import Engine - - data_config = DataModuleConfig(..., train_subset=SubsetConfig(..., batch_size=4)) - datamodule = OTXDataModule(..., config=data_config) +.. code-block:: - engine = Engine(..., datamodule=datamodule) + otx train params --learning_parameters.batch_size 4 \ + --learning_parameters.num_iters 100 \ + --learning_parameters.enable_early_stopping false - engine.train(max_epochs=100) +5. The training results are ``weights.pth`` and ``label_schema.json`` files located in ``outputs/**_train/models`` folder, +while training logs can be found in the ``outputs/**_train/logs`` dir. +- ``weights.pth`` - a model snapshot +- ``label_schema.json`` - a label schema used in training, created from a dataset -5. The training result ``checkpoints/*.ckpt`` file is located in ``{work_dir}`` folder, -while training logs can be found in the ``{work_dir}/{timestamp}`` dir. +These are needed as inputs for the further commands: ``export``, ``eval``, ``optimize``, ``deploy`` and ``demo``. .. note:: - We also can visualize the training using ``Tensorboard`` as these logs are located in ``{work_dir}/{timestamp}/tensorboard``. + We also can visualize the training using ``Tensorboard`` as these logs are located in ``outputs/**/logs/**/tf_logs``. .. code-block:: - otx-workspace - └── outputs/ - ├── 20240403_134256/ - | ├── csv/ - | ├── checkpoints/ - | | └── epoch_*.pth - | ├── tensorboard/ - | └── configs.yaml - └── .latest - └── train/ + otx-workspace-INSTANCE_SEGMENTATION + ├── outputs/ + ├── 20230403_134256_train/ + ├── logs/ + ├── models/ + ├── weights.pth + └── label_schema.json + └── cli_report.log + ├── latest_trained_model + ├── logs/ + ├── models/ + └── cli_report.log ... After that, we have the PyTorch instance segmentation model trained with OpenVINO™ Training Extensions, which we can use for evaluation, export, optimization and deployment. @@ -320,60 +279,46 @@ After that, we have the PyTorch instance segmentation model trained with OpenVIN Validation *********** -1. ``otx test`` runs evaluation of a trained +1. ``otx eval`` runs evaluation of a trained model on a specific dataset. -The test function receives test annotation information and model snapshot, trained in the previous step. +The eval function receives test annotation information and model snapshot, trained in the previous step. +Please note, ``label_schema.json`` file contains meta information about the dataset and it should be located in the same folder as the model snapshot. -``otx test`` will output a mAP_50 for instance segmentation. +``otx eval`` will output a F-measure for instance segmentation. 2. The command below will run validation on our dataset -and save performance results in ``otx-workspace``: +and save performance results in ``outputs/**_eval/performance.json`` file: -.. tab-set:: +.. code-block:: - .. tab-item:: CLI (with work_dir) + (otx) ...$ otx eval --test-data-roots /wgisd - .. code-block:: shell +We will get a similar to this validation output: - (otx) ...$ otx test --work_dir otx-workspace - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Test metric ┃ DataLoader 0 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ test/data_time │ 0.0007903117220848799 │ - │ test/iter_time │ 0.062202490866184235 │ - │ test/map │ 0.33679962158203125 │ - │ test/map_50 │ 0.5482384562492371 │ - │ test/map_75 │ 0.37118086218833923 │ - └───────────────────────────┴───────────────────────────┘ +.. code-block:: - .. tab-item:: CLI (with config) + ... - .. code-block:: shell + 2023-04-26 12:46:27,856 | INFO : Inference completed + 2023-04-26 12:46:27,856 | INFO : called evaluate() + 2023-04-26 12:46:28,453 | INFO : F-measure after evaluation: 0.5576271186440678 + 2023-04-26 12:46:28,453 | INFO : Evaluation completed + Performance(score: 0.5576271186440678, dashboard: (1 metric groups)) - (otx) ...$ otx test --config src/otx/recipe/instance_segmentation/maskrcnn_r50.yaml \ - --data_root data/wgisd \ - --checkpoint otx-workspace/20240312_051135/checkpoints/epoch_059.ckpt - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Test metric ┃ DataLoader 0 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ test/data_time │ 0.0007903117220848799 │ - │ test/iter_time │ 0.062202490866184235 │ - │ test/map │ 0.33679962158203125 │ - │ test/map_50 │ 0.5482384562492371 │ - │ test/map_75 │ 0.37118086218833923 │ - └───────────────────────────┴───────────────────────────┘ +.. note:: - .. tab-item:: API + You can omit ``--test-data-roots`` if you are currently inside a workspace and have test-data stuff written in ``data.yaml``. - .. code-block:: python + Also, if you're inside a workspace and ``weights.pth`` exists in ``outputs/latest_train_model/models`` dir, + you can omit ``--load-weights`` as well, assuming those weights are the default as ``latest_train_model/models/weights.pth``. - engine.test() +The output of ``./outputs/**_eval/performance.json`` consists of a dict with target metric name and its value. -3. The output of ``{work_dir}/{timestamp}/csv/version_0/metrics.csv`` consists of -a dict with target metric name and its value. +.. code-block:: + {"f-measure": 0.5576271186440678} ********* Export @@ -382,35 +327,27 @@ Export 1. ``otx export`` exports a trained Pytorch `.pth` model to the OpenVINO™ Intermediate Representation (IR) format. -It allows running the model on the Intel hardware much more efficient, especially on the CPU. Also, the resulting IR model is required to run PTQ optimization. IR model consists of 2 files: ``exported_model.xml`` for weights and ``exported_model.bin`` for architecture. +It allows running the model on the Intel hardware much more efficient, especially on the CPU. Also, the resulting IR model is required to run PTQ optimization. IR model consists of 2 files: ``openvino.xml`` for weights and ``openvino.bin`` for architecture. 2. We can run the below command line to export the trained model -and save the exported model to the ``{work_dir}/{timestamp}/`` folder. - -.. tab-set:: - - .. tab-item:: CLI (with work_dir) +and save the exported model to the ``outputs/**_export/openvino`` folder. - .. code-block:: shell - - (otx) ...$ otx export --work_dir otx-workspace - ... - Elapsed time: 0:00:06.588245 - - .. tab-item:: CLI (with config) - - .. code-block:: shell +.. note:: - (otx) ...$ otx export ... --checkpoint otx-workspace/20240312_051135/checkpoints/epoch_033.ckpt - ... - Elapsed time: 0:00:06.588245 + if you're inside a workspace and ``weights.pth`` exists in ``outputs/latest_train_model/models`` dir, + you can omit ``--load-weights`` as well, assuming those weights are the default as ``latest_train_model/models/weights.pth``. - .. tab-item:: API +.. code-block:: - .. code-block:: python + (otx) ...$ otx export - engine.export() + ... + [ SUCCESS ] Generated IR version 11 model. + [ SUCCESS ] XML file: otx-workspace-INSTANCE_SEGMENTATION/outputs/20230426_124738_export/logs/model.xml + [ SUCCESS ] BIN file: otx-workspace-INSTANCE_SEGMENTATION/outputs/20230426_124738_export/logs/model.bin + 2023-04-26 12:47:48,293 - mmdeploy - INFO - Successfully exported OpenVINO model: outputs/20230426_124738_export/logs/model_ready.xml + 2023-04-26 12:47:48,670 | INFO : Exporting completed ************* Optimization @@ -421,59 +358,28 @@ It uses NNCF or PTQ depending on the model and transforms it to ``INT8`` format. Please, refer to :doc:`optimization explanation <../../../explanation/additional_features/models_optimization>` section to get the intuition of what we use under the hood for optimization purposes. -2. Command example for optimizing -OpenVINO™ model (.xml) with OpenVINO™ PTQ. +2. Command example for optimizing +a PyTorch model (`.pth`) with OpenVINO™ `NNCF `_. -.. tab-set:: - - .. tab-item:: CLI +.. note:: - .. code-block:: shell + if you're inside a workspace and ``weights.pth`` exists in ``outputs/latest_train_model/models`` dir, + you can omit ``--load-weights`` as well (nncf only), assuming those weights are the default as ``latest_train_model/models/weights.pth``. - (otx) ...$ otx optimize --work_dir otx-workspace \ - --checkpoint otx-workspace/20240312_052847/exported_model.xml +.. code-block:: - ... - Statistics collection ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 30/30 • 0:00:14 • 0:00:00 - Applying Fast Bias correction ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 58/58 • 0:00:02 • 0:00:00 - Elapsed time: 0:00:24.958733 + (otx) ...$ otx optimize - .. tab-item:: API +3. Command example for optimizing +OpenVINO™ model (.xml) with OpenVINO™ PTQ. - .. code-block:: python +.. code-block:: - ckpt_path = "otx-workspace/20240312_052847/exported_model.xml" - engine.optimize(checkpoint=ckpt_path) + (otx) ...$ otx optimize --load-weights openvino_model/openvino.xml Please note, that PTQ will take some time (generally less than NNCF optimization) without logging to optimize the model. -3. Finally, we can also evaluate the optimized model by passing -it to the ``otx test`` function. - -.. tab-set:: - - .. tab-item:: CLI - - .. code-block:: shell - - (otx) ...$ otx test --work_dir otx-workspace \ - --checkpoint otx-workspace/20240312_055042/optimized_model.xml \ - --engine.device cpu - - ... - ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Test metric ┃ DataLoader 0 ┃ - ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ test/map_50 │ 0.5482384562492371 │ - └───────────────────────────┴───────────────────────────┘ - Elapsed time: 0:00:10.260521 - - .. tab-item:: API - - .. code-block:: python - - ckpt_path = "otx-workspace/20240312_055042/optimized_model.xml" - engine.test(checkpoint=ckpt_path) - -3. Now we have fully trained, optimized and exported an +4. Now we have fully trained, optimized and exported an efficient model representation ready-to-use instance segmentation model. + +The following tutorials provide further steps on how to :doc:`deploy <../deploy>` and use your model in the :doc:`demonstration mode <../demo>` and visualize results. diff --git a/docs/source/guide/tutorials/base/how_to_train/semantic_segmentation.rst b/docs/source/guide/tutorials/base/how_to_train/semantic_segmentation.rst index 07a9731940b..0ee749ed63b 100644 --- a/docs/source/guide/tutorials/base/how_to_train/semantic_segmentation.rst +++ b/docs/source/guide/tutorials/base/how_to_train/semantic_segmentation.rst @@ -9,12 +9,16 @@ To learn more about Segmentation task, refer to :doc:`../../../explanation/algor .. note:: To learn more about managing the training process of the model including additional parameters and its modification, refer to :doc:`./detection`. + To learn how to deploy the trained model, refer to: :doc:`../deploy`. + + To learn how to run the demo and visualize results, refer to: :doc:`../demo`. + The process has been tested on the following configuration. - Ubuntu 20.04 - NVIDIA GeForce RTX 3090 -- Intel(R) Core(TM) i9-11900 -- CUDA Toolkit 11.8 +- Intel(R) Core(TM) i9-10980XE +- CUDA Toolkit 11.1 ************************* Setup virtual environment @@ -26,7 +30,7 @@ to create a universal virtual environment for OpenVINO™ Training Extensions. 2. Activate your virtual environment: -.. code-block:: shell +.. code-block:: .otx/bin/activate # or by this line, if you created an environment, using tox diff --git a/docs/source/guide/tutorials/base/index.rst b/docs/source/guide/tutorials/base/index.rst index a852f3f6517..1574f464f83 100644 --- a/docs/source/guide/tutorials/base/index.rst +++ b/docs/source/guide/tutorials/base/index.rst @@ -1,22 +1,10 @@ Base Tutorials ============== -.. grid:: - :gutter: 1 - - .. grid-item-card:: Train to Export Model - :link: how_to_train/index - :link-type: doc - :text-align: center - - .. grid-item-card:: Explain Model - :link: explain - :link-type: doc - :text-align: center - .. toctree:: :maxdepth: 2 - :hidden: how_to_train/index + demo + deploy explain diff --git a/docs/utils/images/multi_cls_selfsl_performance_CIFAR10.png b/docs/utils/images/multi_cls_selfsl_performance_CIFAR10.png new file mode 100644 index 0000000000000000000000000000000000000000..7a5f38f3b0690e2bc4433bc865732d4911f7ba1e GIT binary patch literal 63495 zcmeFZWmHvd+ct__gn~*6iiiP%gdlC8fFLL+f|N8!cY{ePNC?s@WgrMB9V$qNlz?*5<}$*2acfhi!E&tqjdfc{t8; z@UR~?u(me066EAG`S%Mr%q;aetJt#Y@Fu&=C6%p6NGLRk-<#fwB^Z*BELcm4U$|)> zG1_kD{A^&OWU8r*k>!ebtoUB1;;75DZx4LAqw&S?owCMN4>jx8cP=*?7YLp>5EFOr z^S$Kj?~feO`XS4Fsef(ds8~WBOOdcwZnR&?+FPskCYSc;mDz~Gf&8SEY#Db=8qa_I zx~!}$NV4yrUpF3dF#r7(aQegr>VN*OYa=^|Gboi{`$Y)mhAt3TjqfO_lvM+_pV*tUz*fobjkwGojcdn z({u6BcCwdejSgB^SdiE|In~Es3+F8fm5C?ir=alTox`m$H#h&P!)>v;I7O;G*!bag z7~kmF*hz(W(%#}{y}v%H3=O+&tS#hDm$LiU3rR`IE`F5g`rfgftX_aE9iM$X^mHGL3cBM}w_e)D9 zH*enjQrLm}$dM!LD(URk+Ow<%EW5sLy4U#r#}}ifamQzQj&nU8Os;nqC%bX6VaKF{ zf4;xPmTua9wa}ex!ZFd)Q_@tMo3!wg^5X^t$J*N4Te7V5gFD!WCA@g44JV1ctB^vBMNly8 z$sX!>>FQ0+&dxM^#?6-^kFv8r9c#^)nXcd@ep@Zi4zVBq!5Zl0PDZaqb0s@AUnxaP zNx6L`dau~(TBN@FtlusnA#wG^sUNAj6{*hEKYv~) znI5RwI6rkr(Atw*w;a1KAU@Z2>^I5Q?c3!Ol~Q)xQxO;MGmK?K8Sp>Oe(IF>%s`Do zmgOIhd-~nqpX@(%^F-DBSVm{q0;ftkshgYIjMWfl-YjKB2{J^^FIYB{6CZ_15)%iLT ziu!cika|g?N|sXf_w+o(Ig{{XbYUN(cmI~r=_)NFLwexUP05Un=|2_X5)y&OKK0uuIYqB3Y=?J@q(5_nn475D(hnc%O_Q^;&aZ5-r=k=cx8w4Q zHvj%?cu7)nXO8WdW1q>!e5)C8Wm18xDOx|gd0SVPX1ty~)14b`v@;>zDR-)vK2a?v zxIM>C>^ei^`&+$5UR3=@)-uLg(n|hgS&BAyIL};*h>RR+N@fb-P`jET#=`PgIZaRG zdTWM9h9BKoZ*2a!mgR*BxuLo^W!y!PR$6YIowBmBxf|;%6#FF-WFsYIW%brqm-M9b39u^rQda_qJEKc2u^|j1EU1+`!)6-om;#>0M-G zy4ZdTE+(ihazb-NKU9*$!V;Sem(G0n@cj#qwsYxK(p-ein6NlWP9E-r*ydvmzb;zqV$gx;v!G z%gbk)cW-u>>Hnd_tt`m!x3j10Ej*LyFvA)Z6{V#7Zh}wf`opLS)lBmT*h#i z##$XEB8`)B`jGppts+fj;ryJ>mycOnSfrTq5|_xVb@IlI8~fQ5D{dDt}UMv5b)>J$R`~cZO!n*Ilv!8QJ(yw zqnSJ?nDUKsy1TorCpr#MP*7l#zEjT=WV@ZjJJFeLLeCm_ve2Y6-^Fj5@-K>Ep|0Z@ z3^;w(u}+OM)>IXL=g&B4>iLOIzvN`jxsj#;6fi?yS|MBZaDKCR zT(DnXHa?xvqpwegl8Oq8^{OL5uPPK0 zj9XIovnxGEKbl6x2#koR{QhLmNsWBrj*gB{#@hw1F8LelPIod)ujM;0Ddjs0ppU#k zQK9{%k)}u1l%y78H__oYa1lF{v2Lsnr|{763I~5mQ0JIe0-s9eIcA0)zX}K;@=)_OG!!jR#xW4 zlql3z;0uzu)*>RKgDudS*m}!dqLXcDD%Ot!NHwhahNRZ?3&CM7|sV4 zNu{4bKcDU^)9y$&>#V-NX-i$ulP6Df-+j!?Jcphs4M5eL=fp2$JNgRX0PP|yGc!}h zj~;gp`+IeHBzd&6AbQ`?E5!X>U!AG3wyB8}Dk*+OKh&Bru|*>d)vMQfCDaPvcaH=! zbFlRa|M{8geWb$|LX0)P-rrw8)(3KwAr8hkl@P-VG)tEhF|=h&X)`fPN1B8 zFE1}w4Hk2A#T~O>`V&%rh>DW(`)Es=faC0m18j<0UjY&scjiadh;2mRgvPxqu%FV} zeUP1~MdK@Xi0aQmW<*`qK1=^R?d=Y=)F+#_?e6OB)$U+8bO>u9r(m1WJ!I$o?Aed< z@>5lQwPQ_az@dy*IUSLSA3xR~(x%z&L(LhFmK0jOlM_d=rEpAFeuD2tN5C=ZI`(!! z&F@>^-}*4$tX+|0xiaUY6hLWRX7T;|_noAqEWj+d3!ArW*@Zs{3ew#_c>0ccZ!y_+ zGP)w{Tmjornmnh4;^Ja=pw-Rh>#GjsFW5>t@|-rExShlxDk_>g|J!J^JtyS(^S$wM z(bagB0gh|fj9E+Sllla?`ecoQUn2EWk_kx9B$7?>DA;8Md4FXn?;jwkf@GyM%S)Q2&yNJ5U=+G&YHH%drRi1Ck>G55goIF& zlam7|J|1t+vBJq`F9}p5J0uuHclO?0GqdFIUDG#Frv$1qEqZafbWsG*oqI<{d~nKF z7AAJc2acinSoW3f1E`XSx$=xCqV#;mXKK_#U%r&Qe*JmYK-;fh!DtL#7oY5|O*c__ zfAih<+ZrxE&|nPFZ^LFdRWqLhoGB!!W*v9xCcdU5diCOX&O}`$lhw$t)A>vN^trjY zj6+$ber--lfP)sj#Uc+q$CJ!D^H&*B7J#f|PMaFI>1lfGRAj zpv~}Xv$iv3jnKkSy$71>P)f8!pOpMVj)PQ;RMj0mPP_be$z_`^KujQtGz;WY@RV z3AYXD`uT#m*w`~Bzj?0)op=xvbM)2+#Xq19B!_u<8-#yzx~;E<`tEm5PQ=h z!8NW1vxj!Ci@GddM=9^=?cD_O^tGuegi*v%XDRzK`-NTEN=5GjKiIE(3=FMEYea2y zJ19n~+Dtsc;umRgTq#cB={rSzlt*LVpBp3f2^2W7d1?d12v8!bV zJ=#Hm4|s&$*p#dxr(gzpCKD|hiSH6BQK@@YN-f=(7T8$3Jm@5Ym}|s4g#>N@kzi1) zcy-i6gZg+f?c%3ppFbMb$4@oh*5ESh5cWMJRE$;n#+ID>-K=2k3bv?HmgO-LY%m3QNpDH0(3v35bgT??7^D{Vm9^gtHq(DL&%Y1~s#+6x;S8*|-IY_6a{ zR3%HlIHl`6Q^jwn6(Q`9dLvm~WX9<0B2MKq?Gj&FkicR9TT>c$P2a{dqy)!z4-5=U zu^Oo6HJKZ0Z5l+S3$r(C%d}`(7)0N4fAr`P@hMiySo>#Smhm4}`oqoWfYi6&ZTP6t zl<7D(tk$}|Ft?#%X_@NJZ>FvdhSSgel}6OrKFrZ(xPc3`Jt#gA6?kE-L)0J<;G7q* z=vj((2^}9FU-A4{Yg$8pJ!?$o!`9SGriTp;L(e+&e1G#@lB=#JUUcoNbw5MYC7+(t zlbnC#uK9&2h4PkO0{6P%B`p*GJ@Xkw;}!2s+FN|N__~^14lOjXh((N!PeeyO*?r&* zib8)$E1<};@bGX4H&7ifFE3t$pI=q=jEtmlp#rX}_F?I$$NKA54>oT*p^|Q#El7(O z9RXt@N=$*9o0S7Fc~Vl+(D3cI*B{^rp3$p(Y3*0EZO0BOdU|O{7`Gfm^!Y7ip-YIYusS%!$33Gvn6I%Qe zJC}DL-v!9T#qd`mEqJtXd-efrc|p7Jh;e&JDkz&LAUy$*B|x}9Ne#yv?W3VFNE(2e z(7gn)q740$P_#g1OFBBD@bCW5pFfX}N=vhfZ+h;J3cBYY`x&|Y_5~HK@v*U=&>a9- zz{2yBwJ)<@-jywE!C1}sG?_ojD~B~eEB3B#s+M!!RPNvmvqTc(k;CWilO^z*i!KXW zQPk*;cV{)Qt5Uf!xkYiV3(Ka<2Ip^uc)?Is`zx$~b*a6HtJ^=rK(v=%{;6AeqRixo zXR&>zU|UlblQ;w=DHLoS?(TsQzIi}JO?~|m6vY#08P5fs7O}DZ9*^uCy9vIUbyol& zSuEa=ysq6T)yy-XP!WPQ-lpx@x+nWVpSbKN?{>JZm_YH+(qz1*H$J&a< zQ-}ZR&mYW!LF4CLYL+AN9D=nRe7d_wH=S?1c8ARN1+6*WIQkYNG|8 zqt`;_d2DKK5{tPmy54X?_0pv-pwaP1Wjfgtn*GQ2{L#GQQJ$MOe4@<#!I!e42J#mt zluy*;QMhi!O{}_9yV0&aHac2~%1(Rs-u`X7_kXRZ@F8RrPzIp$@c8jqslXFTpAG~0 z_ea`we53RN$Te?@-br(3Yvh+ z@@I#WpK={$xN%@Q?0d;j!VQ`~9r5?~A8k(ALQPHWI6o?dlf@M(17*+`eW-hI@TDHj za-<9^6>&O>pVFdqJ__a0k8df^uVKW;OF%{{ixv}ua{2U-P%t(_9PP9lCvZ^zNOA$e z_QWq==DOE#0`Pmnq3!vROM{&SVrPwh#e&H?Lw;otuw<#K9HE%*xH#Oqxwf|Uv4;m0 zHMKtDuIsNaZW|pPJ!d_*mk?|LVP2ui0*^e>EOaM!hm5`}b=g7owY|B*$^sV5o<6nI z&p7>@4L_?l3RcBhx{58*T9H?ARksW45ADUhNR7<7Mml$xL%fJi=ut8qHM16zl#`v6 zQ$S{Ao50i^Rn-VCozlI9RaI4=GBWt-cn$6XiZ`h)%*PrTH6@)rrI={Y+|ojeQ_4rq zBx;;wl`PdmT6hNHm)^|}idCP|(s;nb&4gD*)BYm7m8A`-o^f7~t}SOu?$NxXnsb>h zJAMVtpA1wt2H(#;Iy!o))vO?NL?(>4Y;tn)j0~$nm^P0kc5NZ3&|8#>Y!?Y%2CgtZ zlY9G5T>na3S#Dn5vrSvJ(+pI%uXW09=c%y6G!1Sdmpl2PduVO^*CL(rGH4CkGE+9iw#r6}niT!`K?~*WjJnHQBX7xP zqo(S&8U^|OK|$6{>Ct>)EFAh=wC4Cr+K(ef#!p;0hj>>JvHKq>l=3lcx#V zpQXRZzc+BPCYAY%$i3c)UOB7ee!9(IQJdScZG1pr6yAvMVqi!W-LKYXaj*8xHbzE9 zXB7Eh@?%Yzt!5WwtI47i>3TQ>+{<*{ggKpkpLJW?daTx%yjAABIf)a;xTe{n`n>yu zu6Nb`)!enog{g#WCmR{oz_`MLz&vFyw6wKD`OO$zU0uO#W}vuEV;80x*0V}`um+f% zybyx=R_xDrto3KS4n4;e4V|%fU$$=DvDcR8DbpjjzLGAclffA@V^_Zv76gf1p5C8g zU5ehooaJ4n(~}VGss)%xd=o+!w)}H7jNi<3=%;w|LA` z>9^EZ(ZA!EQPf!yeecP8!=1UnWnbVv?L2Xz#*nv2E$S*N_2<)#!Rc|oEsrd3HoEw0 z?_P$lS%X%I!dU??j^FNi(X!bQ#Ug^iiX>^Dp6}aP4sYykIDY!g!W=1N@4jIW`!;CeR|-J<;n|;3qa< z8l`PgdIz6-j>)A%cxy-N4|?JngN$R2OcKk>Jl$;Q>BJGE$sQ`rC4bI*h~&eg+i63U ztTTJ<*#ZyDc{dbdLE?Xe_1ulodE=29%KdrSs5gjcwI0_ zf)2f_+C{~B{o|c4C6vjYl8PrUP?z0L`^du??CO8)bjW1Aj~LghVG`j;k*rYrzyrq> zC(GrMyO_FazKc;?3e2a*6&~2P&*43k**$?}L8eA1$0R)Z)qPOF;1pN_4arn+sOOgM z;mmy=XXVQv5KtA)FIG3Al&a&kr1vc{jMwlXc3RFvzO=%6+41PEu`w5ofzsh?o*) zI>HvSvs;j<5Ed3j;i;)xf_iibQWR6o51p^yzCEz(bO|XcDmrdk4a{N%TL^We3M%0u zv?7OzyoJgYxoFXA=+cJ2Kc9T?-~lOG**7a5LD{Fk-@fG3I9pej&Gv(2LtH|_T7Tqi zi`&WSw80maJN^Ld*M2d&y?g(O`zXjLi#i}F=Zi13QL`(ZhuCLfZvIL&%d#?*rzDq? zmv=w-E9kfZE)b~y=EplP)N*V$y%VVgm1O1Su4}Urf#NeWg}*(<%>0nEV0Aia;?nu^ zB-kz`SOTa*+!j6J0RaIdm3d>F`HReORHE`G{*e_Q0pHZdU!z}LTNALKeC4o#_Im>0 z1*I^-d1)G^%}eO*T)-+h>1@n?zIqna4kQkv)00|5)D9`LK94UyAx>P_^Rtgj9?!V^ zTySQZ6^T=o4%L6a*>|(F?g(^v;fT^1bCIpPgElwmvmY!5#sL!E!NI{%8FqFL)LRI; zbuZRbtgU&U(ew9*>PKlpskOJaH=pdf00_V>T!V5(!*9xvre9MG77ihz7QBEkI9k#Y zvO0t&atlkC++-k2It)bGfkUwY-x6M+oaoYH+{%6X_iv4zuj_=H^PPAxaLQmy+HuGM zLt4%GEGa}`-%-y?_v$gcyfDKQHnW_ZJR?2G z$-;76Nl+E-ql$D3rPjDx>&h`_`CY;3*VZOFI4268tuQFVXcffX57Y}6v^fRp^}VBm z5lf460WGBJ(`Y9lOyOg{<3xgl9D$SvvGWNaysrH^ls_JqWotp}!Sh~}#|D4C-eQ(J zwOzw`>T;OlQ3>Bzu%n=p3IvR^?60JS+LMx&2HtxhuuO+>lezvw@<|-0-mb2%Pic9$ zpAwu3|KlDd;v}D{BX#Gq!8cr36gqZVwDC?Uh1o1` zF4)1%-u@iWqpXz-^gS< z(NK3e4++`k8s$KN?SmC_wje#FcH{f#_i~-fA~lW;Gl?Ztg8WOzW~mi-B(qLlJ=|Jj zroXZ@mf4*3^SwCR@0HI97r*U(DT6Ky87xami(O{UIg{_{{zXHbVOz0|d=lFsCoi7* z&xQ*9)sf-xiRT||Ua+ecLe1JCvIJa`57 zu`(H^uL;3E4%;y2fnGrGmoYa#bM(s7S8%b1SU*thtGHpi@G^Ju`yRW51cPm+^mm0pvy`pg*p zL?~H!O(`Ka;T%>H;em15X?XIivJo#e_2u-8TAdsnDZwROT?WW@!StAMx|43KY}%14 z1wVo)#FJA~-wF#Kfpff8*l%lEInGQWyUUT!e5mdi5e&fJPbe>7Gub;j_F(HE zbMTVSgic*uUGDHzhwB|l9i$&cgDx~~PZ{W8RP;#HlWsov@aV%O`;C%H;WL5mHM|6F zVZ9uF*T}?a;tJvMJpQO~Pi}tAGn*v3Np?)2ph}1A(fs`im)YXh_=z!>073zNxNT1+ znu3T^?H+25r?7+Jj=q6OK;bMaBqS6N7&vjKq`cf40$lCuOCHvFM3m$Bi4%iK{HGwa z)JGH+7qeFI^O<%UXNoxIf$Oc#*2@_V8>6Dvmyn3})haxUA|tdw)LuP%yIEPNHk!D1 zK`^aRe2D4|jk^dr4H$N2?j=PfB?-!%+^0?r`xkR4>zn9aThkv_*a%$~KPH~SWqowh zw@nQBmM=T2*NV*sT#rV(h~9Z&@B}#L&z_*Bz4ibApJn1#==8|%s~8#x?fFH%t|s(%)L-ahp_-coab-a3-XA`& zLH}S86^&Y3S+D||zVejDKhDb0$%*RFp(O|3oO#z0-=bwYoX+~vd5lKtG_}Y zzWGjmo0ymwoZ#cY81Tv*;!9-0`N%*WW)u;dkT{etOhPS0Ef_LXxp8OB@BaLz*^dp> zb`o~XJx3S&C(P^(+oi2lTm6%LU4DGYGzun1`oS`1Smf?v^iM0bVYd$p_M-`HpES3i z3P`1$HEy1K(3+N()*GVX`mMb^5)Af6mZfe7Vm>nZq7$&|A)wp^!@ZfL8VdO@O{a_! zBDwz`D{TL0P^!r~Y0evOuOAUzoh+0YafLUHn!$E06!I}Z4Dq(F@t$B$A>hFBx%L)T zkywv3@C&H=?#am$YQ014IsCYq*9Nt3h$@V~pN1+HKEQhlZGWUGSs(HRfij4nyZdeP z>gsB$a~1Qnxs3ZkBiB*jZL#&mqu33wAXetb2rfxvOupP}TrthTXY-nMMBpUoW{Sly z&H`NQqowuo@u_f$mSc?W3(~RGXsa6TYB(~v>*PD0{MMJo21=igj7fhPXn9j}k6;rz zWgk^C&bch}9=jTpsek;wr*Pi*yvp7K2cCTR@WJwtvG}F7)5hoy(8+rr1k9kwT)cR3 z5Drzm4X_s~tlP`Ie|Dv$r^bKdAv2hi*}~j=wCYlqpQ_0B^t9GyTU+|2v6=d89ivs3 z)swf+OOHZXCbVVVqBSlrUtiWUXMPI%^yAAD4jDUprRs5nWzbfB!r3BiHGp;o6v9`) z2vwk9ZJz%1>(_8}0;x5e_n{VM+m0aPl=v4XIU7mf<0DSP>Q z%q9zZH)3XfS6kny_e$Pa4jaixQXx7!emQIndL_mRqpGB-scDt{r{}(-gpCC~qhAn- zghODqVRt}UVpbNI1%IXDA@h85oPs9xJ3h^Z9mQFlh3Sa|+vWA1jjMPEG4FnSgr59(fUX{`scoLaNu3q&kO-1Ue z)aI|nRg6a4ASGGSIM+#hA zlZw@J%TkWzX7*psP8unagHVEp1*25sK-m0cxx5tqMKd{Sg}%bsl-`)KnFV7{oIt0Ni81IYr z9IRX5_vEgO=Kw;A{+fYuC4r9a;%mcOT^7ZR`VN&E6 zt)TDr$AzfBfr&%7-&=N&pV2K3Dsfd~mSZEf=lzdgPS-eylqA%+lqW~J{i(WRCeP|d zOQ}HF0wtq~zo2?XOt|Lh7M^nlEXwP;vCkB$fzXK{s0u<>Q}gqB@B^La8gB!H0+hed zq>Y_W6SW&>0IiDy+5`|&NYlGEKiaZIN4UrZ6c;8rBfMC`p9i6>TtTdf&_(Zj@rEhi z)7K{sKdJ>3g#<=1k@yh5awSZHMCRq0Z_du>=_q?$6`TdRBP)CN?mdq%4M97RT_s$E z*w_a8{tTy9f&oD9bI+SV`UQS*By8*0)(luGupKTT;e{Kji*`XU2~afo3{y3vG^>DZ zckbC!!J7{uvhLM|EgOjjxMPG=ib z%%@)(ov}DJzT3x-qgd=~wCPArim*YJ`&S>&KFV&c?$Nx=kjaw`oxh&sPSb|VmQoU|s*xP==%wFGGUoa^d2B6J8@dC2ii zr^CRZE4~a;ckdp?O??e1mF3v68_3a!F7;8Vs;X8VCBZ)%idO&l@x-pf!2?;EeNAki5WKRekpL)QOBYI_RHs>h>!JRF3X~v+ETT1FrfurT5aK zTdVy)NSw2r+sQP#c7Gr@Dke|PY53qfpD!=I#9cbL&tHbd{er4|n+sD0s}5QHyv)%j z`x8I)d=xaRO=@WO5Zwrmd3F>TtBSd%k17&T!VYyod292nx%l1O+S)rf=;`6%;h=Fv z*KQduW2WVw-DvSK*nT97i;Fsr|3MT<2&PymWeMq`YK~Yuz>iHY5sE^qWI1>4C1Q}> z!^2P|%X#BL$Rgo6b7>coB0v{zx`EgtEsx&QC1*q?)))0y06yQotsR za$9%EwiyZjQ?f_Aj|qWz7HQ*;CWeZ+v{l~xz4zK~{ffR*gL=MIVr$wF>n+fPyT%7) zTjfn4G$uSY+Vz+GY*2>mmxweQ7QRV;GWLva&Hnh&P)m)Hv85Wu2;<09)dHZL%19yp zYb|3%%NPD{nBe0NAB@7XZ@s_CwULDsCy8plb10UleMT5Q5&>e5Tyj5S-u2abg#zFg zHdUoaKgoZx!McKIWMOW;z5Z==dH&in#1bZ9?@uBON%*};@Q4poM-tQ^n-4V@D##ur zLYCY>2suBwtk{weVQCaKb6Z-@LE2rw4Q~5n+kFve>kz0Fi{Q!*$?l=ufxz6bZQ!dBC z);8aWVqwkF+Pc!R9iWWlA9GU5=~Cfu!7)iL_z5%7W`281jhmgXs5|^QAiBP&-2tX( zUGp3|1rL~n73X>+w@PbAeSP59ujgk*TKQSjU6*tTBPbXlv$Pt~3M>u?-<|AUv*QDV zdy9r<=vYJOW~!>gJD&|JFdXcLTrnQt8w2K|ks(13^S8)^y4w8!$Zg-(sz zd|MM&h!$t9tBsTV;_;!++{!7x-OfIV+}+ufb^09)LgeM|14MVGQRkU=e+N&s9Bw#? zy*U4M;B#8qQ2~LaBpamB3EGORwq%nJ=%CHRhY!acW4{0T#aAU@Y;4S(7>)Z1W402z zpO6(0T7jK?;#y>D*-}~H@s?~Gqip4|XovpL9Q6tl#Qu=D)Lk~t^VE?=Y+0Dn_LuGF z6CDo^{!lSLHt+4Q(^*nyr=SG4SgxMsrl&uoq4_-fuc`!fjlAj=mHV6tXE$uU*w zGx_WGtm9#l-u@3?lzC6h6^qSFgd(4^i$YH5NaV9;WQfpHG^qbioKOmkysj}CWq=3^ zH8#FLr$(~6W_@iX%c6HXM2pfo0mLu>wDpi60nIpY{CEPXhchCIVF*4={-D;lo;fMT zFM>iX5zc2aIX(Rbg<45j*#cULFQZ5~wv4`L2{@(U&sUppfck2p#p>;ebRZI4L|u=O z@RLpB=KZCBFff6r<+*&^SM?e%Sr%DlydF4fTH4|T(T>tKBhdD>Z<()8yZo%`$X6pWpb+9 zgGuks0+F=A_cJU9OH;})Jp}D{517~+Sl6)D0(*XJWR;0P+2b?$EshWn_(YzOFs>pN z8B*AuAliPsE2cH3ZsTnNkk*3xT{fN|3lxsczc!OU zJl<$HQ6Dvs7hTfwtL5PhZ_hrNe_KggMM7#ruBU!j?6XgIH_=6}wN-1@W!yYr&?^ax zknoQXeIYddfXnD7plSoWumPD{9_D=!IW7G;R@W<7)jO-`w}R2Go#cmkPX&ER_?0@} za0gC|;Xu8wZQSo0?8yX(!DZH878%TXIvyY;t3|W@KY|ZDQ_)8z&+cg=L@H1=w@&Hp zUY>p1|9z1uZDb#`r@M!VclrKu)Et+l)<)dg({`?4){5}SYNm-# zoX*h>O3t=?z#_v<$$o)>RM0~zKH;ce%|N`}&dwwQ?Gq8LZs=&(oM6yG)Euhzq_+=4 z31Rm0!N6GAJ_ch3gIlQwG(Imr)DT?z{%N@I#JGF}jJaQr6JOU9i+;Y-=$-YzO2BR= z;2h4iuHCz}0C>@henL## zk-M2-z$sfr9&s=W_ZIhsvfNNM7QsY(zQJ=O)Xlrt=dJ*f7R6+Z*dHnP7 zjqepJg@pa~)V8d&j1q$$&P$fW;Y2#c0XFl}Xj;vxfN;#TD+$G9z}UTqLuD_vF17 zw}&*c#5_2dz?5wcE?U(NEDBFriBhaa2r}Bpdwiuf^No4ddkI*l;T^_28dn85ID9c= z!h_t4Rf~q(+LMrw+rhsPvrb&9$p-J4k8H1N>Je=h%!~!2%Bb zeEle^vc7)iL2?Xgzf6>HC`MNx3h{g#==hur~ds%E86ccQ2KY z<*JX_M3v2ua(Qtj3TAmn(L2`lm+)1JaYrNUG4WySe-C%-Y0q>mT-vf_3os@>kn$!kny?}#!Fc!*t(kKnj9}VGKg=H!*T@ z4IBr+j}s|{)d8`MZk#L<#2f42QxF4Te*f5x{WG16!+`i^DCv>bCbuE(Ve5rza008~ zCt%#1$0XQG_`o8`JSR_jeS5T{7uX#O2D*EVXP~sbX9tAZ?YOT*CIkUUd8COk6J*>$ zq?fX-hc05$-~`$$#=P+HdUQ1f9+{Xt=)&kKr^v&wD!VDf6Oigq< zVgeARc5we~r_9ZGaHdDfJ{L`m1L_B}b9$>{#;9&frYU8?6j_qkk!(FHh zTX*c>u^U&7myIk(SgF#4&?7)6ERfYFfjm$M8647OAM9f^`6qz(^6zh+oQv|2q;uEY zOGTyaKjwOTSQUeHz(U}^k8stfz2B^AL_Gcp%IhOBMf`i!&UiNryAcG9;L$KOsF0qO z{qBQ&I2LNt&fU9va5P}zvZ>KyZ)i_+o{&ec618rdvNw}FlNYSSf<>5mh=!hn%J~CnC+9@y+qh0$&^+Rj;T--%900!Y z_deev=!jrLqw?zXo%Gd>JWt(UvnrC<{?$B;<+^Pk49Z z)%i_@U?Ujb<%1z}7sHI>njMt;%DVqA!R^Zu68ekuvn=VD+GQ{4$h6QdY-$3H#s@va zhl*ZnX-U&3`-<(w&BSlq{7I&@J;N+|6Aiyv2QO)FbZ0%$ON2He$3Pz%Ut;QNX->r~ zPZxvGC9Bz>f5n92!_fg1t$mITr#-rVA{a=P9?8}k%T=UO&Jt`;vmLkYHxW1vn}@|o z1`?``y8`CC9@#fB5j|uB1kq;y=f&;HWMnrl0A+Mp3J<7?>Kn(vaMsjI8IE=;@$F(H#Zk7~)Q^x@JS{eE)#M$r!ZUCkpTRDq zBXWNsn~D+0F>-ZTaaw}7M(^&wyaY4nF%izy3}0Ars2I%X;`_6`#V-m9^%0w$x$pv! z0gcFU@cnWGfOS`w^Q?nnlxkUdAlsDfR59=;5@;lQ4<2w2@u6m{Hf&<+#57F&fO4u% znOf6%4Er2|;g@Tah;}7aPlcYYiy>0W37`G@$}Ze%cc zvxA)B?)S&Lh+=|U_>yS@RW1f6Q`t8Tz8w3V&!xg!*YJgPi#+#{0A%c?q`Ziw8|o}5 zSbB*)j{NAvZ=3|1`brmhBZRj&A9s(tiM{^OzA%X~~Ihk4QC<{d4vN}|FMiN#dP8gxJ z-bcOf;8_Q5QTTH27Q!c0x@2^`hmRe50t|y`bJEy#wXj;N0Xy}LNSg`voS78D&Hg_o z$aQ}wA^C#_*YyrX{78JXLHYoC23LNEJz~Y?AqOvur|tkd=dI}w z9kS~56QVkGAHoiDEfqVMoS!F|5|s8uJLB)sfq@dJzb9_K-3yrt8Rhn-y=M0bP04ZA zfSgfSQdN}!GZufKpL3UNn*{ZZcT#Ns{d+mT+Nxlx05TQyYxihOUEw(fwTa3+#4s94 zvJ3)25cVYl`#KcgUVqJAGBv?ctHT{?wqN+Y)hZDeO3s_V^OCl{FF~;sc>V`&Xj!eol1;;1OhBLdJRQG8P(Zxn>%58Jh;KrBWaY_+Rs- zaSqkP>5m_8h4dl=B|`}VVrs2~GeC%UVjrCNF+vdQwy_S7{9+)=$rlBbmns-jX8+Ut zX$wOEyH~8fZt9z0iPQHS8hXYA&4@rx;<=QEV#(`7(K!3f#%PUP)>2ie=3&SFUXc-2U24LhVb{gbYe*B&F$FNKQl5nXJfT@gL zLkQ=tt}Y?=Gi9&2wf+-we~pcMewfA4F1+;2QLLmU6iOH|@hb~|3Y|Mx^nbEgV9&#L z;P0?MS~dCRDOA-EIQ>wS9cuUj0|M~81sW!o*?n8KlfJ^)fDn45s)z@kIX^L~gg^}e z?%>gKBeTV!mR$rY!nmBx;$)R>)BmH6lRI~L(t1Vy>Hg{t;g!Be=tq}{9flJ&grpcC z#ZkldQ6I*!eN-0w)VFdE*?2Lw2f`>{Nrkk`DW3pTTqf~0WIOY zj)Sv@k!E5rmjsUlsj7|?qQ%S`f{d6x3b=p&zBN^j?%6A_iIAKxLlZZ{X7i>SA)>I0nc}{ zeQ-MX)0!HT64Ueax4=3CTKk=mMTW!Hx++9oYOVEG&DesZ0KQ!r5=?9*fYF z<<$4`pmm-vkU6%!K`>4PbGpP#1!Qw!O#cv~8X8a_F@?U7#EHrTVL}sz752^pkmYFf zzjN0j^`jjA&L*H_qJTR$ZT$p>q?%*<3?VElW(O4v2NIAUH!>dg9FHD(3B%(08*#53 zbp>Lk9ES}x7UF;>0{>HBCVB#{NCb5_j+`cPLj+L!^5raH-alWK{D#3VUktHA_I(8s zIy=@Hg5?%TWt_t;1%ebN&O&)YAH7JTTIo1K*I#>3`4*#r~bWXKFQ1kmxqO)MloFG1Wp33b_SRM@*6e zH|gOshzHw%3qAvE?wF&UB@Pq1M$ex=KM)@Th|~jk$M6g%u>j!FB=1I&0iUA~P2KX& zP6|sP32lk#hpsev%5dMQf63OnsJ6coi&3DLje|aT&{tNRrSZR&MnrH@t|rCDJeK2kFuyCX374{bVddBVj$)J@t#700p`sB1=3O+_WsmA~rDGLif+mFU&9caa0p(H8yWq3Y2#6Nv? zsiEd~^C9gPTifu<+El;>{t?o7ef|EXG8prYe|hkqG&Htc^&tDC6=M8)CK9?;S_idG4G4;y&EiY z`>r(7U2Y99k>U%WU22t__eh8?k|44fed@eEIZr+tw*+9%;dJdnpvQh?EW-hm92wi& z?$Ru(d5$m8>Z}8m_j9PeL^l+wpFvLR84{_Y(~C@BusdFQ_+J{HrW_w1r{HxO06(pt zHJAYNgIh+JU^q)Jy14)+zG83e?;EGmZ3Ivu|&vRDmY$as#_RVY@LNOJwo zFsnfwZ_Y4dg7D=OJ3tIKVX&3gOuwdoH8j5PKgaN`C|1ToZ)0NgGH!Zh>)0+HY#B3G zMK=)XAhz4pxvODN70~tWf=?0$Ve7VSTwu2(z$c{VAyE*b3}%3P7N>fNM>>H`Yiny? zf*~J0+j_X=DIz~iXV*P3X|Okvrs}@Nj=glW_l7M0y1eSK{eremVcT-wA3<=4_*S}YVGAAu$h{j6V+MXV#6XBoj zlWCy6niZ@iRdKA(s#G7V!V;1tVwIMWvEY(i$j&V1hi6V8QgPPDiTrQ)SWxh{@GM;y z$Uwhl3zpz1o}z;S3rVCLX4KTeLP<%<4rB?f=r`13EWluh0!xHCK^O>00sv{SJ!c^> zKi6Y^E&mK%Zo!ic=PZxy6K35jF3RDr)mV<77*u+`bh^KHyiXQ}9mq>$bCD9U3|W<9 zX9Kmh=T?pegtxDgWv+!EZV6}ga*`o%V1HI0o_Im>GfiI(+2jg{8cGmvP#Iu85>$xC zVOj?VwyFeT`#;$+!b1X*J%d|{H03)C+T$q*lKd8`wsu_Hz8Q*ZQsoxKl*o*Wsg3m zm%M|=cN|Z89L}T2S}8J^F(zvBEc@7=!SQ_ITc@bsS0=`%*}Ke)5L~A$=Qt1ejZ5|A zJ}*3d0%6vz7$4!Kit&~lziVXlA!>%<3cMK^+bn6~CUvOW1nCrh-P*Rk*Y=%qP}vxp zrdbRVr9<{Ais84QhV2_G+XgBH#E?Mt*=Gk{xEMoBihG92PcG7Lc-HuZaCX>XswEQv} zL@#*?fwzMSMUDQBal3m$YrOKwq(5canM?~df7+-dTh*WFO5SrLnEXbc|M1Ots=^Nx zJMJz&7+STTmZ;c{hr!{=Al`WN2OnL?$)e#4WGA}&E0;Xa30Ak4xyN(|lS|{rRI)hp z*y1^!ozraE$`{>Y-0k_dIOO1IfGt}sKS+k3;KIaX==y5Mt#eF^M`h=D0jrRVtUU+VAw{77}RIXP%t zQaf|wb9mmPN1FV2Md?vZGn1Oc0lgb3cP}HnYj0EkLG@|A!9V4&=pymiw{yfdZQjoR zDndd%=}O7WGzmw5 zTsBw4n90l5kJ~Rr&uqH8K9Mk(Va;?SgYq;(Xldb$`TSj1pQXNxnwqJR654E=6H{eV zxP8Q(iaLysj!{w$rI~V879AP(dGzss%A=&8n$@xwlZ%xEdHp>Uzkk_krBeB$xlsQfPXES@DV)9NaCJKW8i`0#*)Gx54tL97v1t79r||rG?whr2c=KU(IN}9 zwu-jKA~`R12_47Cnv&q(Z|$%dOXhnk>n)oRmh4W7vv~2bI_+BZ{ND$>H3ze-%UTG& z8&k}L5qBb?Ikb{6VnsY-Bw)`8OgUDy|9CK={!w~Ir@o{t7Y()fQ#(bbV@IS!d;`)% z2Id&3M&!13eJIG+>aKlq(3dz9;*9+Gvb_S$cX;xoZ*@4nW?-dqPCNc|e9wZp2kkdGHI2qSV+>ytF;_{TJDkfdkp*n@erd^*m~+ls@_jTi~>qJ+(iHdOkL5cfMvj6%>YRu{1bVyRwbal=X#X+{1cVsx!KYF)bBZSyg zm*DU{uGSUQzB~DAKBL*eHQ{d^*ymq@r)&z_T&1qA7;d7!UU2h*7G;3L+{l`9{#m6! zV<}%&M>Oj$ zhchSxPag^^-GX$p-kn0jhJ{2qluG1?1&^CWe7061&iwRF43Q=TfBbSFaFuOJedTJi zQs~Z|xnHF_8r7?HlsVDFEzS#T2C|td_t;*_OTZ(ZXzH-C=V(Tm8 zs@l6h2>}5Gq(MSNQVygyQe}6b2<&3Vw`J9&unRFTnvoj+y4`dDdU3@%pCu~(zZo0-! zi>)fGw%cA$cu9nVUw)zC{m(nETeaeI^EBDj4k}l;6MUGhPyr zX!-u(&9Ed76;Jik4IEeNEr<2Z+xJfeLP6O3ugD;VxW8$UUNzSHC0xyHJdf);ngaDh z^pmGz<6^5AEWtv%stp>|gUkUl5GHILm5tF<7;|yl5B&Gl@gPanA#1l9m3v&|RytS2 zHsASh@aL)TNLscg{O%*Q&vxgZ+>G)GYVqGm>qX>6yywKrnULMk1-n5f3-(VY&G389 zd%9&8Vt!mD+|9K@(ZH8;7>nmVLP5l;Gj9Wk-K=SJba0uw!*s-5iFM2-9^@8CC8bOLndsYdt0Io zvszp$l4C8sH9W|g;Npvtf0rTdug0SF!Q{$yqoK1zdLsXhe=rA!Ucd0Ne=;A&~OoA?K8L$)oBH=oqM}_XvM&Q#`T>>S$Qk<3zk>O zz7fTzEk#t6d2Atju!R(q0uq+HG^Se-GlN-{mE{YMuQe}zIFB3v%RibrF--*76yr@m zRf)(2KYlE1vx2VvTH6ZX;!1!&BeZG&5q;SXjTi+S1tZ z%9VTeHz=EsR0((}O`+j(c13-2;B2`I!WtU4^xkDC@=XA62j~gPFrOBa8*A_u>BRO8 z!nlCH_d4E?hDIpC97!=!hVmQSDx~2yNPZjmUU5 z&rsP?;jO7e+?yQQueINBoI9N*;q$`3%yMt9?8?#A=n{62zhHq0h)7?!{o>H*rR&1K z;UGkWJZROIUI9R5{DOc#Hm^~DbFRbM027E?pQ!pj6BiIzK1f(DAbE(1>t{?klS)CK-q)v5Nr~aC(T1w z(wdl!YomqFvkd(Ia=X|!k+(QAPu6k*pPXaI4W!Mn?aqFFv9 z3WSG{r85ib!YJgB)PV(-rfmr)syXhPk(qU4lX1knkPF+KO#`y0-M`N21w}o4Vq|Pu z50|3+2;XD5vw`r-O|YX4mx;hbiYz-sN&&4eVs%?aoSDqxV=L3gfBEN`>V%^o5j>2b zykLT?w=NN|c~XK5khdl{Fi<7*^sLiY<&UH`9hJK~n5RTFZ_vy1?Mr&TTBA5zaM9`V zKmUT+MXNCNb8O7y;LS|wV8xBX!_Ox{e}Ipp&k$aLpmFkhu;@VXv2$zX$xWGI`0(A# z4e(BuuQ`AI{25Um0PCb#?V9-J5<>{#1^aEZg=F)!yM~t5lU2K)o@b~vg`K=HVQcdZ z=X2dN!MNW;cP}JR9MTWrmx^)xXLSK7$2YPnC>-+7Bs6vSzfD5#p!B}zH7C$1g+U7&|8o})e1nZ>qaA0UUBgk!1Y6>4;=$R9LDP%mtf{1&g*Pz= zNa^!gN0Y`EZiU%8ehntRzptWACI^kLu%r#oSX~b9OSPi%Km0N}e)JSA4Jy6A_rix^ zIs8N8d~dWDH2E7lJ2(c=!9tgR1A0V+a|Z592+Rl|-$8+eN*#ie!U&am`5YQ%#KqAG>0Oy~QMAGP6^?B?On${Wu5F#6Xw zbsofXVUGA4cs=&*;M%&8Id zTBA^#4v&EX)>ML-7Ke(_-k<#2DT7PdPhR?B~7pGavs_{+$ z(gzYG8XY?j{^^XOLpBiAU@T*RG|migErM+Zk$)m_eh~c#%s+r~_d&ef^^kpEYUc7W zZ!QCB+HtzOhk6j|^)W>q*Wn1J&00t*kk6F-z}gu7{(Wfed+s~?ON077*Cawn) zJu3d_9hlG@PkNp7Mz%S@IP#yz@JnPKjVyG~lq+axXhhTWU;=b$nv@>zg%C`zfM@#< zr4xX)AaJYGnO#^o=xPw7 zU%XWYt6M3r>4`qqn0K(Mx>00b6|RlzG9wD-jhLU9vuh-@lT`xSmq9A3E?Cvt z87|kd_I?by8066D>Gt&sK=5$jc^x4Y8M#qGyMn}Xf?%NzBIi0M#Cwng35eEkb$s@$ zVPr)7VN}r1sfvW@l@%l!98a`*5!)*TXzlHa!0c-dHG8P-?l ze`01cF6oZvt^3+-fBYVU@5^8uB!~d??2eGdfixEICjUOT>5MODPKe+InnGSD5EPU^ zbRgaVh{AgacMk})^PfH6L_^MKyJaintH$)jz)3042kBqnseHe_ZU-Web|;&VKq2o_ zF{qIcxzH^OEOF2eN}Oz~dhJi(0T4+8S*{4P2bBYk0g#hGYs7#G3MzCrg7E%DaUXuN_hA0aV!wMuK zF|)Gb0`vz0-7g@Nz|MHUnw*82>aECld}pzL+H3Jh%% zc9It1;4s`-I{)<1U{Gm;3GVUQr34#^jFzb@+MH22R~%zdjV(=Y#Hsdp5BGjwNxcFc z1yTc~sbuTpohSg?I|H;0LQsMB3~I6{eNX}TzrcI z*8%TNT8(Dm)J=@6t5g$H9gpn`3ySSbE-$Q9m&WvYd&WU~gs=~OZe#$J{apkMQpK(3 z&p`MNElT2u3$(mgh>IHWKf&qI1z0`Y6Hg}Q<8R>Ayj0|@>nzSdpG=XJQsAJ`@-8+8 zfbEnPQ|$l4cFVmo2YPa(MoB0Csi>1>gPpamc=41^@$X|k>?61~JuFmgOy0kX$*dj4 zq0?FOE^v_<$#fX?O!G|Yqgmg<%x=5_5MDg=nu+%}V$rT`Ss>1p0zoO_{V(?f%$H5S z+5<^*hh_34@)9xR)B-)94<|f?>PQ(FjEkd$0To744Zv&(Ww0rT41t3H-C6K6Cm@p) zK}0QyKITK1zU}+B??b$3<9^%}=JU@@7q>II+1i?+5PPs2%I$zHbEhqHBnezvvy$4H znuq{|@ctHg1w*L415w6I$z_!iJfl{lmkBcT?o_#Mn|InRU8yd?8Ggt;f*P9;ToCaV zR26;nMKvf{QH;{H5~%F6wMc4A))vH}V%g2KQowh1KnXB2Gi#RGF^zd_okxl9(P1F0 z4;+R_R3H@7(MwR&AiO;Y1)6~X_88l6VIT4NvHt+lOzPUP`Rt1vQ;nm7_eO%@hV*$$u2O~C4YV!S?Erh zy_*f@h2v2?r0)Hp%W=Er4$?`8EGR?P>;h1uh>)2RkLYuUfKGx>ae={#9dD>=*j91E zt-8>Rqb*9WTxT-dkq-^hsSj8AhF0Yd$U{J7g zUDosJ3`$>VbR=IR^0K<&MsA-iS6a*%-2FQ4b>sK8IMOVMtImn zT{$^8l{vJ&**PVHkfv~x@Ye*teT!V1kTTS-vkQ9WiJ2MGH|rSBfiy#I)f`}mnavT@ zp?FmznKZfo*%Pw*Vj|;-Ey4_)^i5`SWE8{|K^5r2pzyo_MiCOV@F! z63g*(aYJ87dbdqF360Ke?HzPh7q=WnuZCJ646m0U(mMXP;5;vG46;k0!i9sTb30kT z{23xH!vMnlJ4EBQKt>`)?aIOJk>TOzkWbW(Y8oT9ylOdWyDg!Nue@AV8sHOyxB0j& zhdnB1x?cYy^0Ka_!1h4qD1df)q1DsV^N*Dm7&FKuSe6N374|9O7NU|LVYus{;*T1T zzNcf_7+mu@xYGX{N4WnzYmS*vzlAY;uVXYS={-5#Z+jB@Qv>^cq2Xra=dhg$5WR2f zZN8?Icq>|lm7X3+9tQ$z?oDhBXao8O2lF9%L&RA=E|(kbH_)#v_Q=sYS3{)S|87-I{k6BKvAMSAHCa=tapM86 zULlfMC`b^+Do+vwZ{7jNE5=zUNJK=U|5K1aO$XY4dN2&9frJ5A+C8Y_E@8NWCC}0Ry;NZ~eAR(*{7+%YM#vI^I!KxEDVOfM|Z z#Q0B6JOD=o0^ZBCsLHZ@YPCvka8&x`qBVv1MO%u}=Soj`?{ zrrbI+s($Bvw<&f(mkt3XB`u9C>by9@8iQ}j#Ad5Zs9)$WLDBNU!FO32-|gCGo{6rB z>C~9*BMV8h?8Zqev!Cq|UpGAN5z7Va*!>CA$|u)rs0u@WObH07H~G{8JLL3WtLKtX zIRUTAQc2vW@*6%F>wQHv^96atP!J>04}VaRR7`Ey@dH8s^+{9r<*|ILg@1Isw~UnM z-8{&z98DqrVkK62ly~FYgLf5jIiB}r(Dd3mXHu?6jJG`GwZkLN-apI3ZZp2ZFG^lScK54vcaApJBuzYISwJ zgWo^Q+-gEAzK>WvIC@=Zs($KEcCnMu)|$9F=hleNxZ2^HfCPH$^Fyr&gf;MB_S0t0 z`jad{$F=iUV3ArqK%94fTyX0Pg@8Rb;n6X3G$vXot5?tKkrAtl?;UB}jSo=dOjURt z5!LCXwC2SATS!|`WA92PzGl6scO`Z$$rw|m*39F-tV;=p;192xqe{~pzp@nma0$o% z7ndL&r;{M#IXk?K{!A|4`)gNqzKpW95+iba`u=_^<1D}59kg$QHs$$&PzZ}`jwu>F zGhT2fVdG?M>@O(&VOdUm9v%AeV*^O!z$QB(i9R3(m%t1_Am9{<>|^g}c^0v}K>K5dsa&=I#CubtsPlFF)n z-#l_-L#y8`K*IR+!$BnP#zX;QXZ~F|xd335LYI)=2RG^jY#Btm0%s)nWD)HjVrqw& zXV^e|pcRq}*#EW7;&C`q`RT{SF}%&CjW&#PRw{wYsEEc^P8Z@LugOW9+PWMAl*en4 z2(2g9c%Ld#h`dpzWUVy*bsxo5_a)}tf%l9&8MdMo>#F?&Jx*Kv2bDrZV_;x`DzO$s z2|B0Ho*+jz;eFOnCs%r_sIe;696=78+F@&^%EKM6b0^N@)fU2OF$bf)k(9?tyZ6qyR(_GCz{N*~Trk2)i@0$41Avjr(%;ttc7Y1lW zE&p=E`8X`|F3%xm?qE7hu|4M@D;c8xfV2WY4K%Oueh5&?(vavqXY=4wa`y1+hbc$j z#kqzYn>r0=Yz``GZ3*7(6KZ*398{_HX`_u%qviXlXlBw~KD$9L z=?FC<%;*_^V$(_*XfrSF1gKPZH8u#|MvTBaDZ_|2}?{Fua5*J7A}fhP{{Hc&WfOuN>C- ziXSCaOzL!D*AxGUD)rg`u0aIV*S!}aIk!(I!-Li?5R^0A1&a$pbkr$$Na0rx)KfFYARc^rnL(> ze+QZeeK9ocoT7qTAtVgB70`!e^`I!c4gR{$PcUBZ1DsEquvDAV{vT3H|CsXS>VPSq zvw~6qab-1L`3Xj9yqs?G%08{eFln_FfwbB!hvZadPj+~E?N4uDLqrZ@>Nj`)vQU6T zq?QGfZi@o&teW!R!?PK{zY%v9qrWPz;UuI4M!G-G z{>yQ@Cd28M1T+FTvY2%bK*0>s)XJRV{}X=sk2Fggu%*TYsWXBcgLkkKbchO2h@qUJ zy>+atszQQi0KEqv+g|k~>4Bi=#j9VcHQu~g;WV-~HR?&)dUoO<81dOgQ{-mLJ9pQo zdzyAb{s5;eUa^Y3-`O2XElPwlmj$s(V^YEk12a$~`;y zbF?mD?lSQ9!$AcXZ+el93K6W#NlmoArzSk2`o2t?n@Jcuh%0QB{=CmY*R^VJ38S)R z&$HJ*v*<5@s_4)9roz{d8i~|Iatff$zyQ1-2|$5|>OSraC(^%2Ho=5u2+u&n1iU{} z4WZNmS#vwI^2nqjJ}Yp4igbT{{1)t0*zZF_rJx*#p;Kp(@gXW)(PyPrXSRE&0Tht_ z@rjzwDhqDP$AfBBH5aZJG5KRA4H@>)3FTq#qZ9VNe@a}vmEMhSMbAZRqz2a%-jRgC zz=o+tOy%B86EJ-)5BLWdh8{S+`}BzdG_0s?>K9<=1avR}DEb)j_!DIY zq=seX4S$pbL~h7IU0fa!u@WV(G^x3da^aoE=w2i)R zPL`yCr$W`iT{~A?qwLyZ^9)o5-no}@&2R_lYDQ&7b{miey%k`E60j}?Z)!82oB?ppoyg68;U&w|fBcxr(~)W~SZ=rTaeE+Ax&Wx@AB2wO>;^ zZ=;_D5qy)Sp zqv}YN$nQ75D~lKSJMPBi{gqgRyb4Oi{-FS$-;4~Ef%nJ(oD+lz=fyVH*JC5|GEy~r zS0D`x!sfZ_jX^b>TUW{ zt~9IIya(~)5k0uF6@9QmzNquUo%+|_>(l*a;;-em9*_>8pCA@T9862~$XFe)8Nji6 zlWhY7&|^MSKM~AKIE!TKAgB%G_RgK__WXjF3WL2mL1`p@U=VDlXh3JP9yOX9{GPd89%-)Cv84EJKuCXH#Y@Q zNOpI55%53njT^wT(0?!>*eN_WNtk+us7pcON!-&5WjEM9&C53-=@*QFr0439L$Mt) z;Q$3hW<){r2qQ(_m1VS$!$j zNB>_c4;KAqx{lDt=yXP1d=9jvyLa`Zk{QN+H5O6$$%&A|%-IvK1!<69p{Ts$i4#sp zmaOcyZUG#{rVOu|9iA7YT&%B_^uL(qfo2BI07^ejjLB_wPt?irx(zn)^&Op=9i>AuPmHeP|sJ1AFBFGS*BXfRe z;MhOQ=AehbJ<>8rbM*KtIZ{4A!ORMQhY$`H-G&5ifnCCQfrJr|#Fj7;2bO3MyEqhTS2u-VrQZQ0ybL1v-Ver{f-(pr7rgPyD?4;qKThnN@=fh? zw6@KauwF#f#2c&&2E3Mg05sEPKU~xK*N^AisXZ_(cCfY6y7hKm+~zH~Y~{|FoFlfW zIs|mG;S_PkigHY312wJ@Tr$We4*5>+@E}FH5z+3JPA1n)W918)PT!TvkNljzd zEx!rYo1$kzA%5|E7vu)0+~P3x8OOEcv4HD6NOnJOLGJBP;2|gQA@-~{PKrdD*J-Wf z3`@+dk=x^4beRl4tENCm+_g0q?|1)|3P^S3w6C$$_)0j!kNkt6K+w>QL()K&xQYvM zHK|7jqQ|Vxy=w9+N>K{@fzXfVovD8Tjr1Ng(%jS>88bGfi5(c}gA~iL42L&imRje@ z+$tni-EDkHp*dedVx~P8VDo1}b0Siiy*%ZC&>v(hIFe8flvv7uAP{ea_jcyz<=De1 z$r2h~&-hr0Ipe=L4U*-ZeYL5fEnYoP#S&9tkGtCu;H!Vz&_u~+(UQKyGaRHy)p z(?`=BZYRTU;lZWDs}ue?XJDJ(_5hWih@@b2W zwi7a#d_R)=Dtd|6rD?15{jgCfCP{GKX>6?P1rg`Hk-(R=rn&Cd?#9Y+7v%c{(kG5C zjV~P4K4h$Ldna-6h$M!*&9O1w*ez_KUt>TlxfVHnSpS_qNbCy|q1>x&Yq;||-lyGW zo8{xO$;b!7cRKcwp#1vCIoPr-`4 zNj=TSDzc(cRyrR)NPe}xYHZJfuFAj<*?<$zH`nj#;Y@9Rso*gz0~3_53* zjKPUeU0Qqg**67jM#xxa8EAZUC*#dw-sAV7AGN~AMB#F_7^$5&^=cLfuzu1|d?AIJ zWBY6W+Y9{n*=48zOqPQSZR?pcnPej1#`#8#1abjPq2C@|TWpQ4JD;w`#rVPg&9+8i zM5^eW-&J=oRRW4!j!FH|_~)flBx1an$pzlY=zg)TAId=Gnkj<}Up`$B^94(Dc<%gs zax+;8Qfy)?LBS5yE#wWH2Y(uPDQUEa3Q_ePk=N^yqqZ=lyhKZD9@gS- zuj7JNnOo^ejV9Jhpsv*yjnaCO|I)!aar6?q8eaY{=IZT!6^YZD-Bqz`U)|7C`T zE)10T2-irmO0(X&?n4uc+S(ZZU>1i;60UWx9@{!$(7>qezNUKM^4VM6tVilBlb>d& zm4Vll#E4CBtZpHLIw5cZa&=nIk(QXYQIe^t^G~yq&&n&4R}b6A3tv=7V*iy9&z2xL zN~Fl9ts24F9;AS0Cb*DhE?ToPd-$UGFmZJIc5eAjqVHQH$~^c@-JJYe?j2??p4{5$ z8M;#+T`a@DXPrCqh?D13pG){vWA~2T_!8R?*(}eR zy0TCA)FI^#v!s+vl2@E%qE}$C^XHwB$+jeRg0&Ub5OmxrW3PoJ<({s5--~R1^%4@P zvxY{Y=i5WhY^%WN4kY#g8A|QM(R1HMN?AKr=I89fC%gxx;Q?V;6pTc+KVLjK{c=q% zvLN=G-T1|RG{v-DPfmfoUvJ3mw7J>}!>Bv5&sZvpr}za}ts3eEeRA+T9?B(z^Xrr% zpG5oblOWvnAp|bKfZx0fxeNgZvcSPb_yqZh1V%rg739*0N`! zJ7HWtIHwXaqBHjN(lalH#hsgO3RUSTuQy_v$>QjOV&X2gF^6~k`fL?R1#fDV|KG>> zpQHH>Vh<$y4;fX7`{oGZU>-iw?D-I*)GaVbO3&vdS=ajXD%oz><`!dLQjBTt4>A@Gbv~nyr>{n&CzI%y_=W)aNDF@x z>@8-@s*jKqHtcW&4828q=pwUVQhJu!%0q+2oMw(*j>62PrU=Et=CPln-KK8_FRySd zpU@fA;919e#f9-adrmuu#*6j*!T!Tw+y8fLo0xR29Pd@!tlFWvp+YA@CO?v%2l+k+ zA6}eBOimghVU&=K@}dS^brkz;*e_Y&t%OU^oO+|3xZ>NKxAgkK7K%%kTvQ`iEW#?E z36)~)df^X5>?~z2C(bbcI&f8qtD-LQB9Cz7A)BRX61*@rZnR`H82ySw>b1q?f!gB- zw|E+V|Na1hNsx|Xw5akm2tK1&t}?v2>*}7P2tO#l`M1y`p?KPI>L&I9vW0e&>aX?A zMG%kb=+_A1XE8kK&ft;Zd61T6t6e>h#@X%d)V_0;si8_5euHvMIfi2=NH4&sX|j03 zkEDcTdFF>A$|+T!Og}KtsdD=cm!n{;R9YB{wh@Y~-;%x(&VA(SQ?isaF#kDeAT7Gl=hLdJ; zao4BX45#B_7fjo)IoRvpcg{7H2?Sh2-I=OO3|}~fC(^C=XFoh6g!Ndeo@zq5-d=(y zP6-;hD=9?Gy|;W0+#r|^!?1H$7;J_=KbKc@`Xn5$s*rF;JU z)!vSf@d$#t1_%(vgtm=?r)rxN0!(=AO&{N}k+SEx`YU{W5FMPmo9lI$$tDu8zIyU7 zFE4b?zi~vHt|F2pAR-ZK)?}J*|M2k%uBiG`QJ^@n88(yN!_phPgRJlxEx9-E-lf33 zfkZCB4(*S>&(hj_!pYY$##UvTg6PTs&@cexa$hoA5+8h&*9Z)1R*l>q({Okzdv?QNdP#W&e<#f5$K>ifIlb-i z0qzn{X7gxkJkJq#(!*Z<{sQU_+ln${1JU_ov74SslBqGs$>;xX$v`@?KO$ zAYP4zAE%pMM{63lpx!S z`ET=SO3dk~3a-7>=X-8!wCOd2bb#x!FVAYWOs!_DHxvAeFg| z4<%L)bOUmeApd{@lL*mx0TZ0H+1K^-X8NPAH}oNgPO9t(QWYxOs{zY-tQSGDN6KR+ zTJ8Roj9CQtGdr*Q35VXn1~1MaDO{Zv*YPazBo?;tlNgdxX3{p~yZLl}9$EepGGjdJ zjm^r?h37o>B=M*3iGF?B%EZxxomYie+BI1EuWzKhb>|37My9t)?2lt#)9($zq9-A# z1raZzn^w+v^!U&X!Y@)&rk^}Ova@0Eh#4sQ0RD~`@P;VJN|-VZ3Tc|&j7OZu$DCuP z>g9Q7&9R3|mHznk)oev>37#m9m z%55aQtqzZI8oc8rVz!2cOb`(OrK(20&G{ z{x%wmN}I|kr)hI@H{y5r)3&hzQS#FwC3LLYSjIRhMwz<75Z8(0Voh*BHyOV>rptrT z8d#opck=dMtWHhU1H^>kN!9;#wMkDCGFh0InSCM7M7>lIc0Htr+wMcQ$N~JwUklF= zO%z~}kDs?g6rR1KBcBHj4$k8{J5t@(b}HlcXI|O(3GOD{`eytcO>Z>OB=oHO>w*$F z149S2Ky4$tFbHg;flLvG3{xY+XrRk`zMR)=<64c_{6T=TKH@TFzd0>~B)-7I)2wjL z`t1eVghg0*V4WYjeaWFq~$Zw0Ho93N<;QZzV{N7=YgNrLAH}_X=?59sKwc!0T;|o~qzd71R>;idQ zV(~5|M==sIng@l&?dZk{0!9GOF5;jztF)9l=zcvk%&vn&lvSTsmYM;@H<~@LdvA{^mcRt(lz#KSKAjQ2q`#EFYf76Hebsg_Ti*Bxd3O)dOd**}VTuwhz|lj1UiR-k zeK3ar5;+W030dzg!ddi)^jfcpmXoLKTCH7wcCjt3;L1|_OIeRdPalM@nr34wUFsoPjTMwdR+Fl;$ z%0(H6h~I|^8JL)upyBibja?jQBP>qrlt0=kRoynBc(QRix_2=7gQ|3>Z0mVf^Hpyw z;`8TXQxm?f{Bp^4!8f^5o4{C`9g=jQ1UKWE!t880_K3f~ls56~J!Nv3!+sa|er5PF zx>Gq9>F6>mAsx!JSR8gyE6A!1HR2^a%SmZyGC&Ti02)kX2G4Ef#?etdNF92fs?Wp6 zr;7Qpy}g|?L;tnAOy`c6>AJM&$M=2S>a^8m3I~SzQmMf)aEQW83zL17Z#{u=adE%P zfd(^$S4WWO0r!EwF(Cl~IV2950xd!TW|HFrS%|dy2thNPz+bB&gB%(M1~fr~^mRQF z*rcm3V1n&Ht2g%DPge_mF@;y*i z$n3GLA>2HrkUHDOg4EfBhx*+X{16|4k}0sP$ebw}-M%DAKLhXH_Pou@ zqQ{MjXTb6oEY0BBkShigk-^u81VP!xi(YyGZX*opkpkEj$e2U$1UjJN5)x1`w$&|Q z2{W2EMH0y%<6INsi@-K=b*~h-KTJDFYlV4QN-b+I(q&PF9{%sXpQ{!gq7{_4R=|cv`3r7Dhy#4^hOcBNf<(bqv6I!2!Xs zH>{OKnWbjAZAd?#_Nsft$Zrkw_ZWB5N%<91Rj!u-i9*PCuySl$S$^_3d|O+}9leV_ zPVuoe?C<-97VqCoWFr79 zPaz^6@@f4gwN6i)-;zv@eo@JIo@dLR5MuS#H;BSJUdH%`S%r`b|6F#*(&Az(1hPbe zwiPUaSKxa?L>$ozcmVym`WhP4;9q$=5eeaG*Wr;NpM5bCm5{Jo;_e>awSRAHe zKt$^uh)4oiyD1pJU=N2utvUMm=j3YoV~Z~?F$&K|>M&?kzExzj zl*_pdWDQnGxgVAj9&tD!Ndmx@Rq!EX)&&y;~RMJnLd@($LA7lV68ZgYE+uP5e zl0YU3!7Yqk+uIusTObc=HU~#XB;gxWP{?Qz^zr1qPkG8iyfu9uP1dy1@)qZ&1cHnk zYbl-%-*oYI&}N%ozgc)h92?ZB*yU^}8oU1(P0_C)nW)o+SNHXp;gXBgzhWSN-}M)F zP`eX*L!4Y2hskisb-D+o!CE-BL^j{YDNh^p+djvakU0(0$?GPk<3Jtk%DKs*@Fn3& zEyoKAU$ybIY5g6Zj}GT{J&2^e{X#FFi1gj^?Avozd|9t=D8h%|iSjYSwj<0u6hUwt z@jp1u;TZB}z}sdI4V1*;JTtgJG;08$tgE|e+1uVdGs0hn|7zci;H}(qv!BRVp+4-23CpkAEX+2%Yt190Rc?A*a|CqzxVzrx%hY$4 ztmu+<9@u4IYmedG!{g*1Mt?XxaDqyJ@*dJ!nf;O*fI+3)T7?E+n>mJ9m+9&G1VM3+ zeO^RJZSN#?RyDt%*l0C3yt2FRc{pAimhGv>yXkfh<&SuK`SEIr3ML`%A3p54!JTc3^r8pyh^_hlQe@ zKSmT+-~AnwJ>OH;q$i0p4uL6I^hw^ z$jR~Dz9)6oQ57|WU-WhPPzD_|Qw8C<+-bS9Z%K<3u=j91M)w@)GU@xnDgy*hJ$y-Z zWp=95WpQ@uCq8^Sv{~_vD8-Wv{8oYsk5Otk6dE0H*U2a-Tq`(*bMy>uD~5>F+4QN| z+T|qfT$KgYaxI*ls<>Z^YVMmgb9=$-ABw;-2yOV@wzeU zoP~)7b;+$0zB$gTQ#;yY0X+3gUg|r2myqtoq2-c&Dv|_?z`$S#>4HdWq&Bgz;6yTs zVPEexIL_Z7@gsT6EXSM*{^Qaw*+Av-I{vnN# zS*!1%_gL<3EbglR<`9XuI3*_=dYBnS)gj;MV(CKc&v((V?MUf;(pawt^>zZ&=L$lm z262TyxRai&vhoEaK^!cGX(=gZ;04Oc$|6NAa{Q>N6`@0{U`oLK0M=}}D{1=LnhJ-B;$}_XE1wchcLQCs*#jRy{NU-pb!CgYn}+*S{H6PS zx}#1!F1XlE_`Kp0tv**I`}4aV;ljnvzMHSu3FvgsUmWv^B|jGgRgQso zx~?w_6V_M{hxt__*xTDvoWrPTMabiljW>f^mLHBF{@61>yB5W3hy|h0n3q61fJlKL#eDfpX^6*`Af0d|`XO;ND=aAPrf^wb?J+ zi-W@b>;7K6!Wd?f^Js>`=V!V%HERHWQu~L;g5ec0hR|F$^80;=KB_?*1ChVipfm44 zMqucso(}^{X1K=Io;1aMB?^T)#EM>FX0E^a z>M;|%Szbn}uC0B$E^{y8kABJXbB`E1zg#0bdPO?D@N{w0@6C*bqpQfKIgph837;9q zhz`R=I-KNnDaom%wvGDpFbd0F5DW&ttX_K=R+90JOzNyX-anKFY_OP+*9{ShPYX|b z)b&{;B@-a5FD5sh!s&B` zQ(oss<%_1r^?|pqt{3jzbkcOwj9^;SJuz^=*tPj}#QhXauAv!IEC6THr+8i&$`B+ zUgZu8pO>1&xo=DT(CxMe{jun}YYi%|AfL}kQ*c4{(i35;D9nStouz1rGuWeedzVzz zSm8vV{QHP0fu3Npz`Lj1|7??h@bc7axgJIHnH8&MKZ)}+zpqnkTha&*b<)*1$EICQ zxU3!{x0X1{bx!|v(O|noZi`$%7T-jzVB&o3Af{X8IbHoT7hQD#oo)~M1LKc;|M8Ih z?utsX>i){QDdqh**6ZTLFUlE+BXJD(7Und7BryCRUp+0^h+dMR^ha|H%8sRiYBK^hvZrD$%+veaNOXj=axviDCA3wUVe&uIh3}dG4s!A$x#pPF?@e)6} zYP;rk@kPX7bwd|-yny|^5~WCNQL68&iis&_yL*3`(`~!aFVE}&AR8I_e;!Cf71Os{ zZGEC0TRR1jV=&TFw3*I zU$r{r%ed9^J4Y$WNH53rT=%(~PBJAftONp;n0+0K5!gfZd7n;Ld^ zsUVB#^w!1~zRcjV*9yNw>URvjVX*Gl-hCYrmeEGmb61)C$#PWPV%N7kHSK~@luWEcO1Ksi6> zK+iM08Ts-h77WeA0T&^3_D`VBP0_0$8e#jskFE1jcI%XmFu|2@&o0&!6T-q z2G^`^0McJ%r{KUkO*4_w@Hdu;;_9bvE-w9s(XaW9X5?{wsowc@yL>3;lE0eE6}}t0 z5kC^)GG(#Uy6Mw~zjO9PdX9jY*bFT2z$uou3jlVbswm#rpJ#l22~0J14qrq>f043Enz+3^Y=&V#U1WR&HL zfB-yH6@SR?8snwujgW<*_K3M5_Tw_otH*ozrSVGKeMspR$&5b_r!?`B1}dy)syx@~ z8D4a^JxfJy!tY;IOzK-Xn^xTg06afFSOU2( z9t33|au~3Ab6m^buaA$6q%PFrhW3~*lMv=@z|=y>H=l~@QTyd}E&^Q@oDk-*Kw)&A zt?&C}aIQ7c)2(mET1ctt$c(9r+{|!djv7{H#{7y?ykD3EjYpru+sKV0{6nt(Cht8q zmE*p%>&ecJ1&&j1PxVRW=^hw4U!F>C`tbOX6Uw0m)GNp9^~6>q<)6$FJi~$M+!(aX zd0Yx%2m~;06C$`TX6v|AEtE8tct>j3h)`2YILAb%}q_XP%GA#!<<+WIy#-%ek9u! z`L2KTPA(9pyJQv$IjNWO-Ib|p2S z2^YuwGq0Qk@3_?^mW>Zh2UoK}jnn#H+n+>`{Hff&jUrZpwWu_3%NRegbX@t9uTKPd zC}mtp?|WjuVE=A_*Wb!oKzwP}M0xFAf4X(+azuQ*f}2~pp)1U=MeOJ>hn=?9{3h+^ zTCaLgNt%yV3WNGN86q`Ii(z`fE9mVV0GRo#w*CG+34-K8`fY2LC&Zc{c}`F$Lsv!5 z-ufzz@jF0YDI|~7el2Lu?}!MMMz|Bfnk@NK4^p`>`Q<9@3aE*Ve3lT^Tl$&e6=YXj zFuOm))xRjt9%tWj@W%6r~x45n;yb| zsxK{B7G_u45y8liw^~^cgHbZP0@G(MAR05%MhYYsbN*X7fph601PC=giE{l9bn`a zU=|>pO%PvA2N4^0KL4hsn$RR>(8UQspVLcvAAxJhR@>(}(dpxT?1@yr?XS)}BSRvK zI6t+{Uj^rDcyp#+RWx9$kZ5ZDI~m6tf-%lNzUr5ICSIYqQn(ahFa30nmqiHAob_xchFy*jNIna|d!qfcAhm^FV;8x*+NX z!ekn-iN$Kc;i_kU{ru5)#|p#RBLIsdh!^i1M$a*c?l9^mAlCrVG|k^%hCUiY_ZOOn z8OA~SE~ryyR%_sJMB>_ky*TKrIcM{$ZM? z`~HnUEHw)X;*xJ|Z$0ne+wpkUUN@|+mOKzI*!ziVAluBbjlX)wNbPc6cHO?{K}>0R z#rrsdn6x{~oG1BNgnl(i-uFkoF7xfd1Go9R6=RSrdyRr8=KjtN zG|IQI;GQyt)-s>4@XIyjN1df=Q_pkjd@<=VZ$fS;$1C!V1qHfMg zH`#CJ)J+Z3TR)em#Qa?T^nW$?-QigF|KFk_MJgjZO(P?PGBPqkDGFt#GD7yqZlEHx zkjzMwjFN0Zq{!ZTWQXkR=XL7-{=R*G&-451IiBNq?&IjVt1H)ao#*-ayg%>p8W@WU zT6^_4%VXNmYe>tf@}Kw zI~M%XbBdQnOooR&X$s9xZqAzL^5~#d9mU||>TgFV{0!B--T@o4CDd=?Gj-?GXGNhh zLm2A^$8@-=kUSrVEPXytc8{ z^-&tH%T8s|chO3j>T%B(&xf6o{Q7nJiJ||@hdJ5aH`2YYw*F+&bi0u}qQ;=j16YJ- ztF6^tY1^pwCn+mJ87X8A<#(FK4L)r#5ng|u`2z zQi(w{SN+|OPaUp}iHkXBCtn$5ru;$YTc)h5k@%*In?4)2FIj52Yk3WvOQ>6abi>ZH zkDb5MlVkWz12;*gsL~$Iq~G0nyWDrd{yE#iK<)DLkR)P3lGWU{0DLU;J`O$T(-6T3 z7GRe{M!|SIz|S85M~CkzNod!C1gf_2{tlUEItJF8R?|fuok@>Cf=qE!z=J`Lwdz^k zX;)Q6v)E?Eucx~&&&u7_xy>!)V+|* z0gb?5J2N?_l#xA6X%s%2r3^Q)MtFCe`&1CqKa4PP_)i5X@<>zPEf5|8kvPrx^z+KW zqJb`VAMPQ1n+2e-1AYNj;t&ij2rqa9xw~+uS~H?X+VS;>nG8khW9o3SIha$!hey1js3PW+$yRGBjwYd< zVhN_tIl7!l&0V!S-%xiAZgA`L57ql&q%whmbwHDyv>mCZS58xIdB9?6M^vR293=BO>p}IqG>chciqQe2{f_ll3 zwmo&BU$^;PyV!^h$9<-ldT{n^n`zpV9YgXTJH{6Fo{y)nq86YWWzwYLu;8e(esfMp zx4-hb*G%#WtuXgY6|YLuiDClr+{wT1b?wj;=K}><`Ch zt~EAJbZQCi&WNXXY7yW5e*2bN632NWJWnm>+kQBP$CvO?$S!2nr7ZjIGOL3+e|2GA z@}*XD&hnaMvw6TwVI&@<&KFE40X2Rn&~rN2)c^RN#=n27%^XPZ>mKV&s1Cbdr6 zcyNZv&YAk{cN05jpnv{=1lKS`G=)!9;*6%S$#)0DkzTqId--Y{jox584STSubN_|5 z!#?)Kdk6M2OANTjw7M!;eB%vB>^(ow-!>Mj)kzjcnvyl~(@Aa1(xHp1`XtG)tmgBa zP3Y%A9b5bNjqBe9$P-HYCMOL^`(Sh_R)>oUvc0NPc==GrTCmX z#|UY&Tr=)l%twxom7l8&7mDROm$5&C{RxS$0uE# zYd+F1SF+9J5E+T4i&=@6$R#^XDxm3K@v5}#pq1l@5xC>|c0J!!J%M4-$Y+5kA0@>X zUeP_@U_v=bQEj@3tKr4#PWIDZ1~$!_KZx-Ye~ufr{8yvGGgFn;D}KjoqDmV&zua}{ zIG@9*!P2BELuvyiAJJ=ykTzjwXx8fKvx^bpdsmrFqkd}L8$JfT{VCSS6 z3x`n0+9>OKnl6@`XYU-NZY`sav7%;->U(#vyDZ!6qlJM&W1{1 z(7i*G^Pr-%^*V-B{*`+5719X?$43vhpSb#tWn|Pvo=UrQm2~MSve0Vcb^5dRB)?Me&3{it>LB7J0^zP40%kS@yPdkz8wyM7D zk7L~UhDRcQv9GOckXF|#Zrb|Sp)n)Y?UAM^ZA&`O606UHo zURg@&6K{a8zkfgEwl1UOfNWAVtj2cl*<)>0b*5be6cZ#V3g+eqQAb_9dbRFTJ%y^! zU(Z$c`je34z7bgI*$r%%^id)iZ29Di&PA@7t`drNZHx9${D?A?u?w&HHFMbYWunX33FRM{KUb%Yn2-w_;Q5_tRb- zZcMweb0MERDcvFj9Ux9$&+_%XTsWn(mU)&o0+5hz%M4s7Z z=1pp^tuWZ$$9^#v4f%E=ku0Q%|0xZ>lfP!20FT+=^uZ(S8$&zq+~MgSG(9>)KJ7}E zl|$0tkS^|(kjTU9s#R4O=T+@2r<(EDu{geL$**DVW!TG9E?vI}g9ViQ{E$b+w zX;GG((`$_7gLtp{0&l!uO>ON(XsdxUnkW#apK0TTZl)ln<$$kzgDxdeq5vWY*VJ5Z z9(tJzltfL$VH!v@f|j%CDyPjq-Bpdd=y)WAm`1dAmu(rR`HVDr!ZuCBKBTbbs8bOc z`-#OvzrL7RU1K+opuU^ub;~wN^)qXvTC7_4z>m9;PuRnU?F3t1@mAY#4LnxWoM&D= zU3R8dt3U{GySFRwj{KL`>ZYXOWPaJ@tM!v(BZIzL_LgVNe#3Z)P}q7L9ISxCa~@8V zJZ*F6PLL2(b@(4uVNpY?N)aU%q5KD|fspz~n!?*B85}q7X}^F9?|DqH;Kh_f{-bfp zq?asX`oAVImCTsmy!+$>E#;1?p{~IIni;+yFNYhwsH@2BlQ-6{OdgXvw)g6Rr&Qx1 z6F=w%)Nh342IjuVUh2#)&u-3CcWGb!{Ptq%Ga<7egJY7C?C6HSO|<+d9S*z>Y^q3V zYHCiNIdg!6;}(+O-v0g)qS6^lT_Q3I$lt=9t9Vcd!gF8YG5rMvjNfc9-&nNp-v$2M zo7{oqL;QmN&pby+GMX+p1 zf+}r|Qh=a7_b2^td9v@+bh_WMIk5FPglx5;Dy$f8(EXio&e>UXXh?*-nHlv0{I#w? zLe<-z$*+VnrPB(ACXl3Q%5@QeXdG+PIEE<%Z~{EmAk?#MDefq7E@*3~n$~m2Eboi* zZCQhC=i?sBwCxbBpX>2J-$`hUa%#^RTft+#TccG{T^+^wr!7jagx`LL+#PG>aZ~1} zn~YTsPu)Mv*@hZ4?mjxHzr4$am(81r#Y-29rFZa7K+INj%y(X|9sZzGk!emb7MoqJ z|24aCrf@_j-#NS~;a$mqtlUjosSwqEAq%FZ@TG*@vK({?pjhW^2p=y@Mz1xcbYBms zADX^%nx39MC|ftCny8d?bi%3^PBBR1Ab? z2p|-oUq=I0`1ZoP+lYu0`K!y7R%4La#dgPR1VklI0`vi-$h}nC?{iNvt+Lo&MS#3* z>re`V7I4`0I zyyRkqQ5dhS3UH!mGxZkTy3}|H)95!VOf0PPK(rB;k(rrEFxqdCkx#B3#s;DQA>I3(Cuu;a=3mMlkM zZTZ9)^8#_>-JU730NLiXKKay>Xq*`lw;}4{O&(%kknreCCYkIe)kH!5A@WCJvrpWc zJ6O>r8Z8-&vqklu9ec~eFNn1G-__U37d+PR04pb$bU-S3#Dc*DW zYog$pa#aNVCin(6cS?U`ih;X}*s^=)?^ zVK_23CfsA%Ae7bkec zRVY*s{iz$gK>MKJ*d>vEuQEN{P|l82u6W^Aw6n-;cm2JRBbx;(hhBb`c>8ka<)DI~ zxfi=m^ah?DV3O-xmtyTMM*e&T}VXlqtWoXd{rkAH{S=*tKAj3Q)99<{g>`4k{UZbXWS=9(h}3v#HtO63etJ)lc5Jxf@Crlt^;7 z|JZ)sh}QA?{h+}gCcAt0G(XPpV!7^b5Ssr)l1ygWAlJ)rb4Y>o8rWHZH0#gG;Kt$L z5TVOGk?zY=wsHbyzs3myl(whEV6kG)w5 zaPiP|(fi5#t!Q-Q@|AtvC+nhWAEk6tos>-0Pioz^vGUGM;a75dN;SE?2+O!v$aPV zNuBBZ9lg4TJu;vUm`84H(UID4T|v6jvEXZKU4106l9+I-FxGqdLgB;?4KCT4zbmt7(;xKo zspgFvfJTT|YbSW!5CG#Ne8eRL<$&t;XoKaeCy%|+pS@0br(Y&Fg1A%P@F~kR4#GN3 zpFo$O9rD@I%9J4_@ZoKvW3pu-w^zLA-;d*b;)9&sLFST`lkE(gRl@^DV*optZ8P`~ zKVsA~>{(nDC{=FLBuF-yJkluSMzJwx>zMSbw{IK1aeXsrjlC{gqVS&Q8iQ7NLke%* zo~QR{+%7a`Tsx)tGKB)NngnAU!*D_*MM{crqY6vCY946jsdp(PkkWAPB7z z*g|`Hc`?p5!(7vCVY=Y^cd9@oIpAK1<^>M2H}{oy^gX<3VX!x9GZ5_=fYl$+u9);# z+|Jc7a@WtRO7T}B z6P^dq?U=`kYiy+lEakP*OpDPYyjB&p4qqz@T|*LYb1Tqth=db_6_cj3CAtC|LYU=& zTZ@+|(DOgY((%0Xz64`c-PUl@ge!d2#svm-#=zGI+Vu#ZG= zdv|_7QTmB4(=goZdLY+!ZWs03w;Yr*^C692H4^sAq25 zLCKF{Kf1kIaSfz>7C88D6FW&GU$1cXMPGdwBF=bF-n^$M+smY&6*jup_Z+ zjwP6HULfvTV-o9T`h%CyfJ<+iORYTKo7Ske2iA~;d9+2lC~(1-n)0Ae3<=fST8WnF zR8&;3=6DPb@$5XdiOQ2w=>kNCRd~c9{PY#Qt1NfHHEoDLCZq_k^2W!!gd|~-QTnk$bCYz7O3|;z{^|n zLr#bT{dkm057Cduj2k+_2Y}guGbv!SgkZRpl@-|78^T2GeNf8wcJ1obcy@a|ZaR?~ zpTfZ*9C5Pmj-Pdy%J+4jz7+d2EFQTE)TVjjzPPc3q|p9OFyQVR@U=C#DLNpAP=8wN?Etvsh$X4PbPNz4yzVzq7V3Es*}u0diuF zg0WfvN9f9;1m3m zVrAlL105+<$x)H_a{0F z4vg+{KJ?VY(k<_+nD8BWC|P4Bqo%T5bwbtrUgxI?voBO5iRU%!-qha5?sL1d*kg3y&O&<` zH`87Vd=f;)UbS7Zkj+H*9g2Tn&_}~+9YFI(e0jPSJOhHx0QNlLtqy+zK1{@fS@L0w z0nYaA&`{<-)EfbG;DBV)x!%ZSahfzM1m4mI(Y;`iaDEDgQE+x8-p^@aC=NbAvT?Q5 zAFpczb07znC^x}Gd+~yK^B|csSWLtLBXA;QUMsJ~I>ORHcUN-!`kP5qInP+Cp9vSR z?N6dMstEgvfie!I;)aHgYV11)e*E}V!fcs>uY`C&#NZp?hcJp*gjKskw=rj?K^vf4 z1|A+Bt#pgiP|n(-cK(UoKC3}(@OXYK*MHCYx-4*M|Bcdn6+(R&(+HPX?iD;v^q+9w z)En%m2TytKzuh67yYFqIQHbJ0cWQCwo7z!|%8yFKz$9Nl zEewz>ip=!Zuk*Y1b6hPk*)8FkJ0@a2+1X;je7ctr83HK>E5)a_z!RbyGUoCz)hX7! z@`RU)`^vmA@inAY7q7#p34IwajEC3(=O8$3ceWnC7ap!lr~e`&acLyQUijxK)6?xK z9~&L-jtg%yXGJk1r&y{a;5m~qBJzd%y@qxzZxr*k=M-RwSiei`h};}a$*Qv{KX<(k z^?B2b)jcL+->&wZloA2|`THjVfCR~zX5#Xtv&FR%36B**whFc@s$<93;6-VTu_I2d zABMI_vgUZItP7-MGi5CWdV*s1O+?iB5T_gCKOQCgPi!>8|MnEnp*%~v`%mqiIf`Cw z&uSfi_6F(Cr%Wbqu6*Y-t@A41j1-TkS(+cDde49yo3+8=@Q1fz%|}C0lni85ck#0d z^2_w^J1McTwO{J>wYjqw3qHr}H&YD0Y2p6XGT%2|1jb^i9&;JyYx!#i_YWDFWR7lK!uLVLa(B<{F|PwsGQeQKOnjlpmc8FNd&{rfgiqA8L zT_Ag8IdY;TASO!h42D*&q0h2peAOxw*?P77uIMCKhm{>wfCLtEkBAk=6lp7;#g*3L z>rHR252>b;iD z_K6lSC|vJ8-^YM&N$jAp-z@v~?ZfV^6WAU@;Ec|l&^JEL-0Y9;V@UoXf7ZC<$@U0Y zqTO;J+w?|~CK=}1r*(}o3EEYJQ$OQyLn2{>j^F^S#5aLm!!Uhcx4h>fc0&}=5F-1Uq?05Q25bd@Mg%~jeSpqt6@c8=mFhrzPSkljO<`lEKM5)TIS65BGvmOmx(9d#}U z;r#X?%B3MWNSP+>iJi5`BiLs|ppO{=LSntiUO5P?7Tr7wp#E<7mLPK?`vr`3HC8iD zwv+Pm8^Mt=My`TjScToIl$!*yB`ENgmGfrh0T1@W(`(|Te^3f$B$%)yD7 z2b7~u!~Wu#prMZ{E6S1|d5dF*m#ArtdYr`xn5KIpznJrh{MS_r*1yT81byGca8kx4 zM+$#s$ z3;UVGE^m+3nLZ|phFIrRHtg}!b?IRVZ@X6HCbwh>>|WO{d!9}_JI%aVu*lW}=5HoP zn80kNCYo3TixXK|0eXC(j!pm@1}ExJrTiqIThX{WOY468RHl&T1-; z4MJnm_T=7nK6Li*LyKGODXCfH>|S+&QYxm$bSsp32Q-cRo#-;^4pFyxZ0(|Eb3DHP zgwf+X?QCPWCyv*HF`Rknn|=R$zwIL^K{4`rK=H-fcovQ03&N-ZBLyT2ndc{4ACg?>?0|MLyfVYq}1%v3>7pcnbxH6i>EWsy1!tb9-id5(6UuaV&n3wTfUL6 zc|Mo`qxeei^tIvor}wWPuvu!cK5^kp#c88uubH>`DQpLr4>*(07)2TF(k0EhWM&u< z@a?JULqnE~)W<6yT^a3)(~EVR7jmbEsrD<}e>guW#xcRlJ10icTf4+u~J$ zU$+@dZ<$H@wJm|!Lg;%7=7JaBU6u=(`P0m0sk(csCcq5bSpis6z;Wm#rWe@x;#1{^ z0kZoV9|W9n%8iN3s3NX`ga~=mC#r2`p~~c40bi}=quiT$Xg7Yp<-L4LJD4+ArP}(( z@HV2wv@`z^&%EZAR-Nm*L(FZ#Nby)l>^{ru${FaYM(v7O;u{djd;es{*0a+eP5H6@ zSh5UX$#|Z*_<((XPjd?euQHg-FH~-JT^?|PfCkARptp^?nQHZV6>W@;Gt}6;9vGEn z6(QfD$uq_%8lJb$t@cQ%a&5AiYoz;%U8Pv$JNddT;nBTSfhUM#IFLq8%l)}QXm)ru z$6Lo=<2Tb5=vl3NeODUVm$qMBPMpc+9GKnk`a1VctI?8tvVw|O$|Rk?yNU)EYixag zn~z-77*mXjztG7|*FP?!V=C-JYD!nm)^fX>^|qz$evUCI_l@R}2_EvgTAdeK)-*ekS!9P9a!%L2EqvUKy@?4TP{Q=2|Ei zom+24goJ#>ki?Rp8e@A9SZ_k$g*dm5T9Mq0j@}(}k%ojI_6u8flw&Yzv3TS?!mm%*G~9K8Itb_^W|~L0)hnn@ zlYNoP+YoY}z*wO=`7M-C6P%?kmH*;>?fM{s@Pi@HBQU#f-zl3Fg~S1b69tir;A@AA z9rsA99@;f$+U%64EV&x)G7}WXxpB6+HMP3k_w;PT)i)OM6zt3l3yY5XqgV1xSlc$I zi7)D!tqrSvz!IY{kcRY*Ok!a?oo?do#yy3c8niVwl$k;2KBws{d(_%cDpvB!#7W6V z1U(Sv9Z2f$n6)=gtXm+lr0ucXxOPdaTH}4@+CxsQa=b6qq-)8i?-Y-&>~wIanD8ik zJXY0xjUtlNKLRn6v9!Eg78Ad*F}<-_kkD;LTZB;SolEti>l+?uNHVDyf4i5HD{I-w zoP~#FqOB#7Pgj?0$n^RbZBgET_u6%Mzx6Nrs=9g{qSRN=-{`qz7ornnx#HDprsVI5 z0Xs)8PrwQ(COVhz=gdzXQlj0!6XEN<>W)3V;=wzhpZxj$ z`7X7%9p&Y?_Tx# z!AQk`Gi4`wM=$inWH6X0uTYMNXm=;~(LV~(HDQ@tHI3!x8-MXzJ4G$wm)(2bskb@7 zsl3NUg2FA2E015|Nt`koS`xmnDykz9tDjU$x$*nmE|tLJ#sB+s+hMx|4Bb>6r#-e; zrD^eEbleNgV)NFIiEa5WU@|5P`2<2>8!seGXL6MaQn$&ICF^>=EsIL{d{^%yU)PFe zmjl&7W}K1JwvC!G(T{gK>>J&U?q=eB?XNr)hs`%zQ4)_%HkabLvv6RF#s0qsK{Wl~ zw7j46RV>SG7K{u!cT(#$i3e|7X^O;QeU~rE-6m#+N4moCcslqzMx0k0owq75zcm!d zd44Y13(7eS&1avz)^D^qIvq-#WN+bu9%NSauIvB3^R!`BjA{_I7~}*zk-?@y`wr}t zZ|#;?m#&jv4*;ZMaAiu9hL5YEazwW}tTQCoLT@fH{_XU-?C4|p?ZdZowPTawUeVGr zq>N1R?dF$#KUyOUcm`GLuxR5xnH>e6oXM{JtiDi~wMlVJ3G2^1h{H4$l&QukpN!|a zCT&WQW^FSoS}NN#Z7HeFm8l0QZD8{+Nt`l_2aGbrg{x_rYZ_mRS$%t+H`e@_4l z)i`}*R1Ds|5OZtMPn-~mjO0Jk;vkWHPi#4%v_ebw&4#?3b-$*&JH8ZCZ&lDXPmHx+ z33`0FE1+?gqNE>V1l4p*?%mnsy6$w!!9QtFDvH1U%E(Hf#y3e}CBcex<1XwY)E9@Z zmePEf&s=gDJi#LF`m9sZl~FQOyfkym#jiJlbP9y$1ZjsN(>q-Ap1F7STTgB{P?)ni zd(x|=zdx+gJ82+Rn>aB0BHi*$Ux;646MO8Ca>#v;7~d$UmsKf=5!w=NBEKI}FgYJN zVh|X#Ce(#d1OeU#^FmW6y4N|P!DPKl@3v7(@iWQ#6+GOTy^R3taUH&$`(niH_0UL8 z_)jbQl&S`&aBBZ064i4Yb3X36F}G+AZO&n8o2=5_Fbz4G)XT*_f+OjCk#aY#Y%R^X zXWDfz$j_p1&}iRheWh0&3eA0a6%7YiH#IKRM~@B`Us)~V;5cW{7`^(Gs!Js=Drl?G zwNE&NqQYjMSmn>qOnt*vcW?sU^#Eu3{qalG&T@Cc9gpdg!b zPIPCGyGm!_82yFW3)iJ_viPlpuaC{Ts9E|gX^DQExh(0{rI8CWjTX0Td~BL9CL~5+ zSp6TeLVHhpI(st7-&va){gC)0@%OL$Y8s&u_b?<`XxP-_igVTnwRNlVjf+2Ti$%Vv zXs&39Pgwr`dgwu1!W54F_G52OzVkG4e#Y(7gucZ8XgZ>40N_?&59 z#pn<1iKkaAq`Q8&eW~@opB=3JwD_ZMYm=1$S#@v>mxe{hR)YuwZ;M~jNz$))CfWYu zfTrnBPRa0`R=d-#KK*maFDjWgGgWe+j(g?#o20O9qh(G(9bYRxlRf344AUYXIWQ8( zny`iWi+1JwDANrEvZ_tBr3cHeD)-Ru-m$yc#VJ|!f+ea6WR@5_oo$Rw+LZ7%_dmi} zPe(K-<;4xfvGOVWLc2OTF_`Y7ef-FA;9UK&rVET&wJu`KK_O;&({}a|{vYc_)Z0Ti zFU*X;9|@(p)x}q0&{j|sKjZQr85@s~Pj)otsc_Lif^6~N!4@>nN>H||n3$M^xPiPy zz}zA9(hpw$2aEOvKECr$55(-c^ZkD4{RaV8^}iigdK%g*_2HL=0mX=Yn5Cp##{{j&PNwWCWpy{2;~ZDmj4(1JkdS$8PbweA5%$1rxm;Cnod8R!esm>)j_KfmI)3vd}ipt9Vw>3*d zTupP7kwe{uiu#NB+q&ObExjYA9<6S4avaI=FWWNvSC2C+r?tr-U9RGSrexWSIIl5} zYCvLOi;1jD49)m-dV|ve4{DJ}rPe*Q?=*%uHR@Q*C-_*)m^sHRU!94h9vqNP-hxs0 z2a|1pM+rk}Xcq}INpeNNPFVSK-)`TsnAhKEG1LBbCQ}8|eJtn4YXSxa(MI)MIt8o=OFi zy4RmamQJ>h7jLeJGZ`F_(Z~zpY2JX+#X9dv(7cUKhSe?D1fdscd|?1yyAJe}gybEV1y*)Xx3!-xXe|g;C}QiYPEp8w?id*F$%$I(*p0qjcavCpP{NeAiv)jk zzfNV)4o%NG!_G$-?+{k@xIf-ni3I)Ab-G#sCLx`(7vxk418Zhs^DgLxb-%dFCTQ+5 zn$}42YhvOB`1+qqN;GQe?tOcFlhC=N;CSO1s5PV6XfMVzpV{WHN_t)K^+3O}gqVK8 z%^SrYr`3aRGjNIW7dahocUH+ie$3a2x##!6YkT}ZYW&V@>FvFIH!?vYkIw;rus%2; z3Q+}HP1|4g?-B$`A@xAgH85cOp`)_0GUc=?t$YC6cL>y&bmmE5#JxJWxc`km@Mr-- zP7l637ne7+c*F7bi=N-U{X*W)>UQ@33_anJKMOF#Aaff7KY4O%WX8s};cb3!F@GEu zcIy0jT4WQ@O(leKM%S0milZ!A{ldPnST3;t2>YyQ|u7Njcta z9ZTS(G*+|S`Ma9h;uKdTvUk=)^45=!U9Nn;9e3`K!NhDi%faUbw z^mtFObjjldO7$g^Bj50@fMFU@d^Gs*f!<%y@?APVU;ZE2@y_*Ufj`*?JTfqV~g2S;e98=0vDYq4%|E&`9Q=B`n~A zUW@z+A5o5qcpR6%AKlepBv7m8?6asfO!lqx(H=&S$L_4m++5Ao8tM0c~ z6Ic9Vs;irt8P7B};0w$9vwZcT<1y_I-D8p9Z_65CKD+mfu*+lMXaAXB^fvmZia7D3 zw3u9pm>W7Dw}VWGY-mRtIlyb@f~lnJ?2aIGd0SBv{YL0o0jX>VQKCM!;NajdkPs_@ zmC(doaKWVM<~0B} zoK0F!^^oR!O2zNoyJnI}JobS7>^$k~dqAm1^;g0fJ=IMEkCPrdtQ_^685jv?;Ix?7 z>F&oS7?!>FKsb+No^sQbajQf{v;9k*97{Ssf7z!UHjSOLP2*3O)2n+RH6T@TguUol z{}rIKAHi%E7yJXQJqP`c?_*`zPW%fJqU3f&S0}RTct*=}3(qNp1Xnjd@{H7vA4_U$ zYky_PIblE{3xznUJ1jme((Zx?NJMKNvSW8t1(F~94LkfSuu}u+TlI~RNNH+di@{>f znUBwtv#}^KBX~6d5v-oRXGm;~PQ{hxQ=ijkDVo`R@;hY}`O;6)BqKVTWu@&TLmCl3 zu3oS`*lXi8(`NE$&i-9dV|@o_>0(wB^9rwYp7Vyx^%Hi!aZXvqwa^UOsnyV%^yM8= zBC#h9f)dW6?W9@TUfhpzJZl+t-BMCO+pzqv$vF<3tr%R0ycM(V`79|q2bWijT{qvQ zX3ZKAmHYl!OmFS#+NKDMVGQ5E^_CCoF8xu?B%}F9<0-OH&&3ZY20hpAV;E!&cI1kQ zeUz<+5xb3kuWZ}J=e)Nvi?q7v>q@J3M}js_F^R|fHB+u}A>a0WTFn_>P43dg%we*e z_49A&dYB5Eg03*O+8i7>T*+LxfO(90{)h0<;Ez`lKE`TC9e;b5+^hF$?{t8^Wm|@| z_+;rG?>^=LleL*S->zLG9v&V^I+>U4%&n~t4Gj&^Ip}ucf`FJVYHx2hS%$hEnrq~c z99#Ti?>ii;Mk{y7ejC6{hK7VeJBDsaNv}rU>>*D338a4JozLjzJq!vm`j7;nMB+eb^H^PmBmCBk5UP$Fy;L9(^4Go% zS(kYNI8_3nBCX=kP?wteDq=2CTiWmKl zyR@{#_hvMK2OPmMC)SS9pv7%h)1k4M?LQJcm11`TK6N(WY)Lv><$ zr*y9nLgIS%F8}JQr+>^JY19K&AImci4zls}ri7#&@Ym)|N%f}peSP=v^N;&iEW*Q( zGzAleuKpNywD+ZAs@v?_dnyzO*t>4q+RZyyd;N~928ZVR@fU-H{EK42Pfj(6*%Pqfv)n&-miwjZru-O?uWf5|E>E&T^1>FrIOTg;dc{C5>e z9PdOGS#3L^N%bP{Ay4!6A9QlvCtJ1^iI2Q?dTltXnouW8?R(^+a@K$QIn$cXJ4=E2 zq^?7|u)(A(R25?@o%WP6+sE9?m&V z8k+{cJ{1t*uTsqzA)S0`ZuZ|PlF`oM<(Pouh0U2g=F5yz~N{C!$}0PT{-4Ww05) zBd;bD^8hd?N5Wljhg@@E<|VW5+G0&v-?w4DE??qsx0ESMIS1h5An4a5OLLQr{tv{O5O{xiPF*Kgl0Bcu^}p9e3bMAdZeP)!5ArOIP< zQJ+v4gfrV93KyhJmuH6SG1%!I8PPUc#oV_x)%Xn5_P^(^%puwDY&6`uh1)O`G52w|qG%V3NsYun?TxEBWgs)#0-J^1GR_!L>`Ol_BQ|3YMDP# zx+j2egNM|}O1IHZXey@KKv`j8OH5me zMANhg9yw|OzBxkjL(hHQFvcF{J?IU-5>-y52ir9`*q9KwUyIYUbX^1Sg_YH8OQSq( zCRNhUaeeKAQOLWW4(Xa*zwQUx29QB?Y*)<8 ze6zE&tDPk!C5sN>b6`A3ar5!x?T}EoF`hd|0)uM#XWo!HfaqNe^yp0 z?%C6)Pi^Ls2TRs-{#)VKov7XJv($|BLQPQe*8G0dH@c_FO<;H8VL!b_Qi=`9aq>yfGvY-+ZKIk zX+gg`f1>@X{uaU8TRramurfc0q1^||&WO1Vmw=}HAf&~cC@4Nb3IXNePFNc2oWc-m zWu-5GWPE)53`FOEbdn!AckY~@f98b2Cwv`TUoS+b*Ao+4^&7E0DqyVNhx+anLqj=J z(>K)%`JG2%=NA_{t5-;n(H|P7j|>bnq~~adr2#Oh=Ys!SuB@NZ*YX41T5JZ=9qTj1 zm>R^^0c`G??WkzA2+~JV}~$A9m<9cq?LX0)E{3n<{Zv@DB3r zT1q4MtWXh4BS?LF+XJfB*Kt(@_twDJSTitIb5o#_*OMnuE&6jOqveIHY@(%`Z#DAN z{f}p}yww>crD&Xv#MBd!qz4=;LgNQefCpGla7ctm*)4RXs{lwS%9Mm2rIe)F5=3=|rH}Eh5q=@MaOI9L1I`SCPGv5K8F;vXamg z{ue78`OUAsVEyNl|7-!8dMKf)6Q;MoX<)?=l?>qmVvwzou#b8ESmuM38)lEM!;nCl*CuQrBis#9v9hzjf@f z>Ywz0*tmyx5g5-uTUdZ>jU4pnkN?kJXn4gZRd-NMm8A6BH$m%tYhM$(oLP@HSzKLj z;0(z0DO9Q?$Qkx0$^Q8+2Gs9U*EGuQ8A;D_OPVL$Cd5>+$5U9EhnW2SCqAT zR(^B6oq-?(hS7DS1KR0a@b1s75c2-(Elzdh_h?*wc=WHI-~6YYgWGSg@n5AsUN(~Y z*OB#~FKqI z-~z}Q<%w8v3|lu77(^>OyUzek;D|YggQ+%n_rXutHyJpni8~Uj_6|_Mm{I6%KpkEU zsy;X#1{;&v;Q@Q7<$#1lIQkj5wFIc{jKmD>v17;N7YU_oV2CeSTAo2Dz+O)nQ{F)z z^~$}&qV=O6P*InSjTI1VF6Oy9oz{uS`0}i_vK2a?JLu`Xb8{u2(n?rr#ZQjV1zm%l zkfA$Y!g=nB2nn;{8d=`m>vYd&0`G``X8yY+(>Bhal>pCR8hrtazBS#FKwGXAKe=GJ z5jzsYTf#gJ6+R*Ag-cH2;*)FZg~%=e4Zzaib6kW4Hlfi-WcsMxi@$z_D;xzSCFPby z0#}BNdj}m|F+ffgxHmWYLyWhyroMl_Q{WN_KnKLRh|P$5UV@(0#nHwCV{Y}!Q?npR z5~&NkB||YdCsdMvy8&kMs-Iq5{Bn8Uz$j#IpbEL3xN#^jd(iD#`-PID-f^NZ8b^QC z;D8OXD5N*g2aFb01uh?@9(ccE2jz|&sMoa z4IUhy-fo1vba8#XIwbDjHneydl0AhyPjSlI+96t$hKA7k_O?gBCuG4VJrKWrqB*~T zT1P7d(qXLA#@T*DvJlp58p8s}Qg!~p4 z;j$?@2cO_TSYdJJF&QYHnxC3|q`;<%<#Rm78Is5^#TS33syq7?3QE8d{2RcO7hca` z*#5W%B0@zr$$Gg@;Wqd+g=PEpXu|)gUM)_*5~#;FZ3xjwyoke(cqnKqkU%P_$}|n) zCzUCd(1?bnu&jat8N_W)BUb;KnmYV*$F^Y(mQgw^~S3#rxs+Jj~5bdXM|u6XGfq+}V8Jpg@tu=BLuPAFh*5)QI8qsdm>&Y>=g+v^{{moq$B8 zB3w)%T-4qKoC{%LVe^ih<`fACg5!yUT|p`Z^zKn89ar;r$H#N(=;~gBES+B?|3OWF zEq~3;i8$vQ85+KG9M_APGXvld{;u9`|ITB)-5h>48BF^g<>vOmx!>E}t$IRp&p~PX zr5u>=`c@ya_-WhAB6HsB`H~aQ_%#DcBz$n*WKQ(;^AkFKsBkI7`KhS2rBh#^=2N6O zTzJT50oaShDm*hWI}65nW;CZy^aExQl}*&t8u;L7U`V8pCix;$Vknv@E-qF=Pz;2b z9w$=5Z-~gW?Ck7@;4Qq$+H1YFtfE2#4pA>zMD0$ldkW|lH-dv2{HU=12+ep-tfFtp z`73V_XJws!6_5uy&kS=RY4x^6#5jwJ#qT{X-ViMwB@>f)K|qI#W{Fr)S$T%wFFl2l z7#AwYJ3WJg@kAq5$UV2GLk;V!GEy@6sHiA6?5{N7G5-3XUe1Mg@87Eln>U^9ZR9Z( zv7L9yHf!W7ZfVhiH2y6c_;?rsO5@|_=VyaR{e02ro{NTDOonAsiZo1Sz9Hs51*e*a z2)Z~WxMMPWZ4{*#)J?$`SHcsf)z;SDCm?W^@wjUS90e{G%|bT44?svwadB~41qB77 zi>hQ}^S=66Pj4@g194#)`=qC*Pk!S`=G033_=;cd!cF*ua!Z9IqnE9wO1oSqIi54+ z4_EOxImG|{ z3u6YTgrY&1feBMB27gNI1kVlV*@Q$0G(kgZ-&&$4K!#=jSO7t2Tte0nF=6li{T(w- z_+g(XPjuP$kE2??zK zypgF0=)dEn+p&H7?ciWdy7S!l(0ju;$MM4gWdBGXko;`Rto=2vjr=VK1yWt2HUl09 zNCw}rJ+`!}bJJlP6R*Ud=f+8^G3JExn^DM=wxXhfpok;=jw(M6H7VZF*7PsM#TyW+ z*j#=d!fqs$LVE*Ied8zCY=a_?cIP?*`iefmm-cp6$Xc$EL- zM26Ai#+$I*z?7_nCpkw3r#ObMrkMjF;!c7@ngZc#L+JhkDB}l(CZ^Gtdeab;m>9!i zDG7c4@`YLE2GS?V<>?(Hxcdk}gzxjZg+9@=(&T8Dz_0s1D`A%k{V*~_!gB&L~T z2}JODz1F=ssEUp+V&!g}9XQ~K9|;Q!BMJtrQ}7mdA{)%f%`KBS4xl!J-i=$Z*R4%S zLUgbjthxo^W->xSgUxyH1*LW+tcqYL*poe;*V}!Ued8`n1x=F9S~`YS*n^#iZe#o6IP$?e)>#vKZ{K@i`*p-`Voe*~ zGQt}JOes52RxBu51_mU<1V0t&9mpnab%u{aLzQJ^*W=;7)k>p8xJLSn95E_BelX|@ zJbsqpkNpdp*V|}lP~gTmP4%ZssNlU*WY-8AGS5m;rAdP#y0Wt33KTX5nKkgtfuo!( z{*usP^!k#Om9-t@2Cx_&G>o^T`4a0q%NvcfOINNGK?Qo4i^+;m8ijIlAWod!OBITIlsf+tb~oe5$W!rKunA;K~yFsY!qIV3K^*#{4z zx3AADIhmj6ixnmJ?K$JClnn{msM`X_cyjcUJjeUq+N7%6A z@HhccPRpDL`M!K<;$e6_f=(UO_&kXa&IJ&NDK#cG!024 zicu1YY?fvNekVtSS_1#r>!oGpW#o3+%g4sko^;s8%iY<{%lV8g|9N}Qb7$OKWhLb# zWySd&y}aDdDN0GX{QCza-8>zn-ieibz=zPf>l{BvA~D(!f0JdZK0ZStjpZEFR5$j$ zJJ#!C#@0bjHTB}qWQC|nHqCrz8FN_!x14Bbph=$KAvncFwH4(*OPsQ0Ok)Kn*G{qxUIPF85~>b9^3NZ7NXgY2{m+kHCs8Q=^OK1z)@nQc z`AI`^`q6)VzHacpKa}?Wf9U_snzo%_*}OSAnsrBQTO0H3+qYXgI<)WXamhRVj{Dp9 z@8qPh@$t4YAGea#?JNgxNgXv-nOeXUpy1jhnmdH$H#<+;Vmrx6$sK9Wy~rjl!Q%ItylJ}_6ICj#Bs~%=IG&CCsy66R)se#jCU8= z{rHe!UzlGA3shv@&#GHxhfPD7gskw-aY&?$Eu3ABgbO@%=dTG-`-r~ zOnH!yz!Z5mO55xQS?`i1iHCs}YJ%;LNU%GT3d&l4%5hF6h{ z)5FPD0{iyy1g*|>&?TsAY8qcVh*QAUODiCdep_5ok=xSJGV?@{WIUhhCNVLwl5yOl zg6|anqa*h=`uqFuc5Ee4Z{lGpI<%eT;%u9?S=sqb5dt&E%iQoveb)qdU`r9FOR86*>{TM{qs|Zv8Yzq1_o;<3+rYQRhh=|ZEO!S6TR;rM!u?AeK zf&13hmfgI5eI5@VIf$Dv@`zEwvtVByij}&OloZYvxOm@Kfts{q#}2)FigN|=%v-j^ z$~wFsSY7CET%N2-7#(#iIl3ai5>4sST^PMh4q-4 zcFV~P{`wV5$0<2b6Glx6j*Fw`+3QKkcX29&>phwEr%Z#^_V)T5i!#wq4#Y}E8TFtr zWOt?pt$3zK2S@tx7pFbvKDD>E zWf}~u)0&CfMss9KXr;N5{Gf=)%Lp?F*Cd-i`bA?Ud$$1GeqiCY=@j^4P{CQ_~L9pH>$yUcB{! zLia7NvfuX4{*^0L-iLk?2jBpYI?7$P#^%kNNio`GR@zm8t5evzO-UDgeMuzOzVhf3 zMXuZU`PF*%ICn~mTUGJ|t*^@a&HWTAvO0f$Pjhqgx3^t|F0pt5>H>-7hU=c^&egZA zjGV#V=uTaCyWoAO5LLMSz~5qOl6mT_&*{_O&lVUJxmtLQcNH|dsIITbVjTxYM^TJv zNcA=yeSN&$MQ&2;t#Wd5(<7-tmVdq?}*9 zeY^4FWBo?#r|4KtsY*jtZaJq*d3n+vxVeid^!pJiMxbC!PFHo!b8S>G2s|mt;M+RMe;q4BXonxN)fN z-n*9{usok!IP+#lsv4@E*s-U+8G-cNyJ@tvv=-NvC+nZDtXy>NEoGN5e;JGuL#nQ> zCRvo7uWwOZ7ndJjf1DbqaqJk!21ec$-9}BCy z4)gx?dE55wloJyZq+oo;*V!K*Nx=^vZa?MVK-5!GFzV(5+zKg}L&Ds8YM|z(yc;hI zE9>C5Z&d?%aU$aLvl9>YM{c?)Z9`7%7LzB(?qlbU;xT2YQWK{- zRFG)rZt8d>9y8w$6YCMQGEJ?rG%DD9Pl;1TM#ikbiM{gA_dB{p7tjtKW@OygPgNQI z{Nv+e#*K7z`RFpqh4OX{^g0=a7uWub*nXM*+L&)$LoRNXN5amGijHRP=flHsUFQp0 zVgC5&@GE`Q)k`~#9;KzF{VlxvFGdR=zCAG7_Mr8{hg#h3G)md4jt$_JYvk=OXgR0YS(5QeM)6zWu5ITT0W6~aSxj&6XmJX zr)#^qxD9tUx+alP_({x+wh4B2c8Zj4`HOuXZs*CL_}m1Qd3jgb>`XR<>!aoxWUoW+nay--RAc6^ZKsuG-`c#kdV+m76zdNO{kn z8@eZC3UIX^+(|TorerD3{*mwBhfwuAjXq5FRnVO{(XCtHzdY|U`>TU7FfefR^RsTc zU+-_q>|f~zHaTi)+E^FO%FM(>Vqjn>DfM5N&^mou_&@@$Mb;YjNQDF{l&ylijEsSo zPm~VX%a<=n#L1GDmml$3Z~_XyUisY%7| zgXEQKOJ^ymsRczvH=;c@66K?Yny>oxYn_S36NSzpNlEN}v!m1VM{Z-&TE2O6ZO67m z9n0dQme$t%^b8F33$Yv$8V?R!&MtR*n_ZZynkXQXW>oOPeNbb!YY!Km*6X)#HNG}n zZY%b%J6Yk!P9i{*;Le@2#Nm1R^l4qkOp92{n>Xr?j+qLp`het0=f~6{H*Fml91OvN z*kH2~?PWM#;@TS~E-o5AKE5N&IH=hIIG%wGzve;yQRZzx~d7>1X%Aaqn#bss5B@$RS9NHnhs>*=n+RvXkz$LIB zYhNb{PNSS|T+lv#_|`6}!*}+CM?`F>(o5qGC##6Ls}jH+urwP|P#}kIL^s*tDkdUQ zeRuD$zzZiV9cF`Bf+bE0Sc_4ML84>ZPa-9|$WB11+xe0z5d3QFRQ8xcJ{CwnUV zIRSLV3KZ@di`L^LT3+bdhnj8@hn{a4!Ak$7o5T*f8hEyTV&rkB;?KY)MQ^oHwn10k`J8%!fmOXVo{Q;&sdYYbJm^qEdyB`nU)z#GukT6RTty3m&?TBRr%!AfHf$Jey-$Hvku=p7NVHyEni~aR0VJ)f ziOV>YT#NSM=jTTtukY{gop7ov6G$Bo3Yd8(zc~6J0kGBV`#i8gZBr8+a1V*#jl^Pr zMjD5=+|gm?YJH-Uz^k0+^~11yq0c{~a5ERz0QN1{u2VE8EiBre+aG#rQN}?~LucnP zx^5E_lkkj;jKy7Ya?i4jGI!B_p`EH4V7$doL3&NOc|~gRtHJ5HR97v6G1WgF8M=6C z*U-mvR+fJlHY=SMFLdn{t?YkpPX~Y{XzGARNgy%s--urwxv$>5q3!GI(>ZuB@rnG| z&*D$b3pSeOoD8{jYctNS(cqO*v;y&y#Y|YU{W>}{H>9kHonqZx=;Aro_3}&@&=j$V z!-ecjOu+=m*|NvE`ksG2njn$KSh2*^L%Aqh|y zYrjK3Mbpb`cSHQv6W>*K?%WB+q-0*?${;6Ku<@P}X}ayXLrY3Mj?m!PSR6qbWo3vV8iCrHRBI!DI9z zLZ?FMD;DM=o+OH$jI$W9ogK4Jvq?)!VF4Z!#||q>%lxoBUOCScxIC#QOfsY$=Xu7e?kPoXRXRU_5<-e`5GOX<>YF>=_H+`p6y5-C1DUcrA}e2;5S)TXV9QThck z^7280{o&9O8rRnrR0+gBB(jWkZ7Xy+0mk#X{hqADrq4MRv3KwCymbD;0=dBV$6nRk zb0hCPp@6LULm?UZlw~y9TUK;3HD#TA7<4BbXwa+>ePjiXWT>`MZCIuIEkUWlC*ttM zY5Rx>5PaN)vDd|d>?mibyMZjcc*bSfMPWX=rVAd1_&yG1K6I`#?1r z8r0~|pD}ZU^yS={cmLB`0-tIFAf3nWZwI#v3#Vsh((K*4H{*2Rp0l6zCITECMO0R% z_Ln^O>AOJPn7~JUG{2<~yF=mJcY0Da1RtLXy)RkE z;HEp&WkhV-Eyp&PH5ks|ii7C9N@OR4S(Bvc|Cnsl# zR%oi*FK9ZMMvvor3fCp|mdgPT-fk^BWrbf}KR!^Q-70ttQ37g9`%VoQF5iLBg_;jh z#@)v!5`9?GvOMb8GxKdCB05h^`_$S1CZNu-qI7LIfBt-HYpWVg(iDngi67|qaLWGU zqQAdD7CC-WByZQp-yuWGt2pA+?Ck8=X~s@g0$Mg|rfFB*1J~CAcvJ(+KNz_Jdkqc^ z{jRlA02+7|w^_Ds0dfoI&V^x>s+|;fsnZI)&+dDDk+em5i`h>zh@uQ*ZSN}Ym`Tl? z5f=C$z;v5MG-6@ullwQe@BG_^D7WqW-GA|p(=87DYx15UFA`->#+@8+9H5a)db_GY z(Dmq-pN!po@_jPq59+k+?`VI_&zh2SP^h|X^H%FKX~(nds$wR3%f5mu0hCS6-#Tb#+n7CmL%iogK^b>ht>(vv9~No}ROi4F z{qV!b=B@9%n5C;(QzuF;3#bRjXoe0`a96kKEetDF5wvs2cy>MB?APe1_3v+Q2yGc) z&;}ihP`CabT~6#sE@5F|pyFzW)PM~}Mn^z?o?-O@eg_Ii{m=F%iMMaZMMG5YTy#>yW4X64;8-F9RP8;rcwqa;x4t z(6RMP65{TqOO!AGVr0pFcH}amqCxZ&5`EoD**+HQxpU z&Y!Zke{#3}xvwsH^C-BJz^in0L;^XA^Yil;-<<&yBv3>2$5LV~ zR05WWf(IZLnwrW-)B_aIqM*=?+mm!`9yn0o?0Mz?61XwK_Kkm^M7cuV<9%CPTEt(o z?O&ORle=7RFn`s+M|;1vW2VRmCpXS|%&wOu5?y6Cw+Zj1o`2Mpx6Ai}g8wzk-A(TG zG3-~~oZ{N+xqX+cY}?c0vMbo&5QIfd+JPt77C%l70T?)iAM_8hiQ|Wd(Pp^-UBtG4 zD<*TgnTycl04|1k8yg#`0Vk%WrbOfppM=f=jUoiM{rtrXLfXXkv0GdDlf+&9XE2hF zpv%*3K49~W01{TE{UFv%?CjwX@&!Oni1H7kLei>I{PTsRmcmn2Mvsp~Pac{W1m%su z-XWUN;^JZ&v@%jKEvxVV3Nmr?M^aTRCdO$sV}zM0)qO9>Wj}>i5Yv)>u@5yiA9PDW zLE(ByKS* zA3yGhF%hRySG#z=yhq31QCGxP_E7G2Mv^ECd*`q2rrwST+i&D_E_CsbpA8DGCMQS2 zF2Gl+8NC=XYKzYPP&Qdf2NP!n>l-tn`)oSBpQ=D&i!n8idaeKvTU~7Ba^~v=0qBCB zlRjge)1|%H-e8eWy`R}UJ9{~P&(`|iJ+&t_Gqj@VJ|s~t#68{|rI(<2%5VEx%%4L~ z-O_CM<+b|iUsKJ||586RHy+PVh+rn1IOGt!D9S4;?x&}R0v55OA=d*5vh~UdWYh;A zDY$wgn`57z@_^0HZ8vB`gX`{U%H$>MB_45KI@|MF^Mk|Vsk%B` zq;8CG4#z$iZ>ci;ZOC+uIqYT}*UGtdzWmCZ+c`a=zjGRL-1m4z6y7crI1&ryr?U-; zjQi}UftF8Lrn!yU4nJB3c820R#jkq5eR*I;sa}1pe21slo5)8I0;0+rk5e5lPVwE^ z!J6AtVE4d5u!#R&Y$Dyy(e@Byfq?GExMKX$jUu}@$_`|6)9F_2%lMt<9cLtNaI&%U zD+-#s3jG0i*gh;gsQ#S-9WvN1zp-wqE4yn*$Nx>!m)D#9erBY4>;C;5xh)imZCf5b zN*CVXvTq^k-Su*|OOGQ2cMpGA9Zx)y_6| z4R}K!I}2uTAGzIOyO()RAsokAQuNJ5#5A1`Jnc#MvdOZ64xo1O7|+o2A@|=k2lFc* z#R%V}zxR!I{X2FU1w&X-kjCGum~D_rW8E1=w1|j!_uXEajFG&y7yJTCv6@2b@5J7| z8sAamvHZrRgi z@iA>;hjetR!Bh!xKQk+98>)Nru4v8B3jc+4=v-1R=b*}V7P;|-hK6RBuWM=!V@qT= z%zm0{?JSo;JLPElrc^wG^xJQ=bV97z9w})#>v`6ZevJFk}bul%)?XTG-8ziOM zc86#R7}TAC2PiS}^=laVR+8w#{Coq1(CKzlt8FSOyinYvr9~`bGoC&N!Q|S!*<=(H z;(gtY)Qi8rA0a0Q#G}J^B3{1SUGh7As}ey~7Uw#wrXfqk4ig-N5Yg)D>Wmk;*jHJG z{setAF*de#ak&ith0q${{|*(RPT7J!7oQv11kG4FXze2En{lzK=Byufi4#|r=0>!O z&e1Pvu@;>9`yTUC51twHObDjaXV5;a{)lTR)#2$G@%ZP-`^tLTTA8^;x!0AqV?DQY zRQUVAcQMI%x?KnTH2uuSNABm(a}*_+oIZ2r_~6$cKYwbNnl8xsTX;a@^pFA($CT8#iub;^B#tv1?G%)}}smL!6AHsjdA0Pyp&03qg(1#BI<`(Hwz? z7Eb&SG;rBL#3M$>TaSG_u>VB_*Vrzu)PfcIZa?Q&B{m~d3)NNAE+cN!2fykMxJ)wm z2l%73Jp7h1V7MZ*;CX7P> zz^Lx9urL|ei!dJ44jrO{A!PRA^t(IzVv;pqT<|Fz8N1(L<{cR z*#gSD!!66;nE*KtS6M#;Gx7(tT4Z_@>ZZC`~}9tvcR{;nd`%c4|#ZM zoPnDN@96gKGvtR4A68IQtnKf&T9L1Tb4emV2dvPj$tF8TM;de(+*KN+>Cui{K0;)2 z?MM0N;o-@1Y8Qq0^_76mKrvI;;T3rn0)FQdLXQ~7qLQ2aGJj>>!M)W*F=n_Vyd+9D zmWA^n-64kkXm-0&PV?_lW=Z^!xG1(Hm0uYW5z_OZA4K42OCIk_;{rDKV=*0QtJt{L z_Xw!E;+*32Y`tHfqIX41-iZ(%ytMu(|7g-pqPI68hl-+x|rAcsJ2ZnG{B7qE^|}C z6(VSrxw*OT((IY6=j(5Q#~~jdeSBmqd{;hPOmX0<;;CK7Z#8~_FvKBwG79<*A*KN& zZ2-$=gk&4Xt8D&s;fq7+V3SnUr57(`RhGwO>9_2mN7ZxeE&Y4I2?^`|{re<-c@0Pt z2`wti^zdC9fIIF#cwm&x=g_ zFNq4PsLcK2&yV7(?o_5MIZ<;NEIXa&oQE~ap5%)?^?K?wS24b}wnmDXJS5%(i9HO` z&JZpUz+VjjG4IqH7T_^zY`xIZQopPKa3E%0-gu-U$Y7iirf~aQ^A@WXBWvGcb9v*WS*f&T3ihp zs17kSdat@VYjknx$78VFU_68#w)y98Qx>qi{1E>MeK6l++UW4zyLaCzR{}+XE{Bs* zAVY>t?7lEz;oMVfAeq*3cP}ewB>B-5rM_3MDDX7g&z%bk2_ZwWrzBtrIu~p`?x&U& z+;kk`A_cwu{d}OC>a z0Y1M%xpqV`%?i-d(;I;FLTH{)`9nZ&p{mh9=-%zp^|Hq<#FaG^3eo=sjWl7(3U4$j%1d@+lRP3mDs!C6x_c6InHJ3^rySLM`T%anVB#RdCE8C#1jWqpw}4uFnBHcx^}Ma7jT?K&Q^525~?3WmC?@K z^zdp|f$@P04>*Oo-(ukjSO}uZ2dVrYPplDbyUC(r5%N?g;#!+{_br~*4~`mTXlrY` zFi7KLZ*qE8UQCRRgn}yk(y>j$wL9o>qH0z3i8u85zaPCz9wt=OWUIhvu+ST&k^Ato zI4?sYYco)7o19y^y38jGJX(Ub*_=7EGw@MPPR{G*=DIwaSA^8Auy=3GhYu{!$8MaR zL>)u$Y-DtA+lgB26SupX0wGy9cIy4QneKh>v+V_S1G044^xpjhVmMxL9eSPJS>FW5i=O>uKlak9+_sPq1a!OfVzoQt469E6?*qdo8^=fUt z07~o$7)(T&>x5|wT|G?9G$%?>Q3$!6%Ymy)HPCZ$Zl7FLVNSei_)(JgO-@{#cD2tW zTf*JsE14ud3-Vbp8SdPHTZTC2#Ag^98d@sGxZ2o|A={>@rDanW#hiOl53CtawIVMb zX)HB0(gu32qfTrU2d@d!FfarRaVpf^Yo;eeZ)oMygiaUMXCuk5)!2=@Z!$*%a ztgQG*d9J;RL<|e$?iC1Zed&L(Z*6zzXv#%u8fKmOP9hmOq_uDlw44b(YRvO~`J%;1 z%kbYbGl$;s7{V>d%K9NzApfs;((3YpHdX_ErTf7rNMH3 z-O|#CBnzQ(z;QJmy*WQW4{4|thoUvlWKw!jvg`sFx>Xqdi2gr3F>I>?p@8G%qj~_l2KZtE%_L* z6<6(YO|v<=6zo>I-q7IrKn8mbW^_o<= zJF<+|&Xcl?Mdjc9{tr`I!beq*x$ThLW~xuW$RBEwHN?BwvoF9aNWM+>4YdzTS7GS? z^XJb+wEn58^;Je?|9MRtn{AMA5+*B`bDEdnOoSB{?jdr3fPCsesK~D+nYWcA>O;|jgF0;kG~W603@hZ5 zu$wn;dM=}kagmrfbI$fxD#N7OWYRvQGXQr4CaWz@9MB`f&(F_dk;R|H?~(pZ*&ni(1Al8J z9FM(xSxZ-U}AuIFc8q>p78CB?_yIYmb5l_v&y{ zG6AS~l+M#5E~)P6Nf`O92wD=zgs2R-86pvd%8*82PV{g^p9y-b!qbx_hGPjR=*1hQ z;Woh{iGs`$7Ph}+L5$Vl-2zmK<_&Knk!3P|#^uPBR$_l{cagK?4fZ!NQ~Wa_J)vIz zU1XnY&-j_?U^e+hXSwRZM8j>9s`NjwnDbq__e=-8$sU71ssH#zbLV}UHF#1EOR19NFLiChoTX-8>{rzvJ$c~FO&Ce zc(2}1<|OKr@l*KJ4*orY9ED4Td7m!)dEpQp)wd@e!Mkea*`E=_&D;IR`gK45g5B;t zLGxqoxA^y;Ja!ix8&o~&U7Qwb_*Ktq?h|c$^DF-)*8&7Q4!4^tXum?c3;brW;P)%i z$cKdLf9O9w&R@34Lm;$SXX^(JVUp)9>VDr1<+=1Yyh)i{a-ST}Evj3#QBtvbS8CQn zRn1Qneb@OL8L!~LpA;YgKsY*nW@3299Ya7s6(vj8hsq){GzsT;o5sahPbn~YlPVc5 zg0=LW?;x0dW&{y57mY+GD#($8sMOZ>NHBQFzojB*A%cEaN$J^~+?wgmuQz+#cXfR} z)0Fl%wVos#aDiPwK-4D7j<)J^;1%7Npa5MV!VS7e&OLz zBd759n|6UvXc`~$UhJwXdrV46Qtv;W2;( zA&|~#i`(-HBYynJA8{UDUoOF436~;bHb8-qnSQMqr?QWmlh?xfD1PcN5-)`H32;ua zfB!4brhmd3ruX-7N|;{)buc*?6dffRgCu0;M#x0A=V!hwo~^c8tX`#nf8-%Jy+N4g z7Af_WN-kT%Nl6`lOQHFJI6(n$^_R z$%w?czP>(UY1N3)0k_v8d|=rXNoXW6simc*gQ3R5hL8qM)V)W|%uXa!vo%h4oxVBq zp6Bm6y7Q5FXDR0FuxwlmYIrr0$0%8EWs8Lo-4C)0GgN9}2^t0&3BIIb*UOyMMru# z5Z{98gl3Zl>ACgu=L0A<1kE8;1ELecH`L%*SrS5`+b2{31CZRbfn6xI!n3ycm2vl( z50p?-+`YUmRaL1X4o0TN;}SGkvB00G^Oh&T)X3-+3O6$smzh&HqKU2PT68FCL?l)_ zo~P!@w*3U|$N#WJTDkcAI5%vqrty9v%S-qasN(M4-r=#abW019L>HV0AJNjZCmyn2 zY;(aoncVt)JlyUyuX-ZX4`)Z5O~|zw>$j7cpUkKKsrm3F%X3!uy=%Mh54NrZq8p&e z62TB6p9e!_SJN7D{B&Ctz0^*g7*c-^#pO|p4 z_&uy~{NwB)GjsDH;0m<0^CjL=MXvPkGj2tGu1MSa80hc`QHd6z!>Pk$O&|6tgj z{Yh{KBaZ?JTfgKvVzz8>qITjmq-A7KVe@&MIBwsvjCqMG*RNl~x>k2~a*+nUeKVez zUR_xsq!;7{NhDwf{)lg|#A4?NlL#4pG7@5*gGfRliw+1<2K zv=^#bGC|DoJ~SJ4CqX37P_w4y=IR%xzt;b*tN zyu@)#V#?q_0`&5HVxeb06ZvNMOJ&btw1}SkPxN@(dUym-VUIsC6A_f$%p~I0b@4di zyoD3RrCd>czi|5Y~#IX9_e|31Z!V#)=S{gqP9w!T2Rh38jsqqlI?jumI;4{I4 z(1z}~70Y{g%L&jPnl7ifK_M+Esh5>bQFS9_n&$Gt3Xa5s9DLED5{3dt_1`{i8u{d zsblY=nc7a+XPgbEdd5-R@j@YU@^WYFq3LAzoog)S#|vsBFRN$@&Kw{Lf{BrNwt4aH z6~m1A3kZ&^F3d&Jw`e>z7O1ZbT(-XNVvuDg28WMxk6T~d_3PJ@EZ$B!d1CS->L zNaTBP<7k3AOdoH*8yOk-6}u(lSV6AQv;^e(JG>wyh1oQLS zHR5J?A}8Gf6I2D(s>2uQikX7Qo*}^K`RWbYhVdDcB*Tx*WYsm*WP#6C2ZCz&cByZG zEm3tW&|TU+Gu@J-sij3*BXlLyD?2+IYI{J&JYrT6Xb}(K+|ba_1mpO*_7oF}!b_+2 zY~ug~uJn+gCDM?v89=T*=6;%hs}oKLyW%6r7DytV)z2#1P3hP;YJIDF%Q1h=w+M{q}Q9TM$@6z(FWw zBS50pXe$}~d3X2BMNZadCyE*fG7V!kxMcC1CZU(uNp3IPk_r?0D}Qv^U33nTb6k zfiU_#yT=f4O;1mk1}8;RyN`oQ@KuP;$i;VX&PPV+UZV-+7b#_xyQe9Tcw+kmL_4C7 z5aVK!!sLH5U~o#%(+?FOZ!HL0je4XdHF^1l4|H1v@1NHf>08!C&rDA z#A{L4KfY>??DRUzx)!xap`|f>yDimtluER7}H+1VJbR`%+e8ZyH6I6rpN>;{$$ zCQ;G_Mz*8-B%Z+7@mzj=Y8N)@)1H8+%S8dC+RL#^eb{C$YkKp5u zPlyk=r1Zw;FW`lHynVy0=vAazhv+JE_UGiRz8;(s@@df;v3rE3&>o&bs zvoJlKpWrb<8%A(Y1MeD>o+qy%X|=(|LcMoN1qV3*EA(_r3ZXC)6E6fXz&^pw75A~y zfs+F}p?(2Upq8N_3*odNkh9zA6AfH6$pUvC=5`fuZ?n(mHw*h7u3nI@Y7EJwP?(!y zJEFWVJl;#)`1}-RgYna`$`R^8n`&oSKW_f?H_J{;ej7*(IgUn2 zbp?e~tj?!L26!A*<@RR_TjRO3d4~hPxSZZUpPF4j42(P7_luaD(u$PIdpybORW!MY zcDsV-I?aX&zNqzkD}1#^uh_wY(sh4z<_qH4KiWMmvcx6z_3Kx`g(xfg(T~5NbMl%O zI@7{9#DeRz^=`}lJ6;58+L~*nicwrb9)nB!6g1*Pg!I5%goc~c6rvm-6WP@HtFj1;@1Y!;>emRS8)*k>6F{i zQJ-k27Oq5P{=B>I0t;+SYzog|WP@seU)etHq{^lp86O#;hRX&*ND$+giA@F>GHKTg zf8uUHk(+uxW84+Pt3v>E1R3Jq;~YgyLT=l3zh_=RQ1EY#xTItdap&R-Gr~Yf{0o`U zSO;79*9g(p^argoV6GU*me8($d=xZPe1G7*S-S`O`GwxgKI47iB(k8!mCIzy)YhM~ zjz{Um8hdd@XL9&M);pn`lAkXFoY(hLCQ02}W%6oj;?=4@V`F31wzj{`U6^;0Qz)8< zq3R=KMTDXThli;g92}mSC9AL-9 zM-zbwNbO*gO>-_7enV7)XCz~QUQ#4Vy|LWon^VL{KC0Q;o*aClf;O%!>k>#wA+P*qFpRX zJgGpxCD9>l5RpXeoEUKN91%{`$-|flF?s>%AOeOlXtb0mzEz1(^l@$X0r(CZMSPT?3M3PU&6 zU6kAN758q3-R|!0j^WzF?DrTFe|yTEXAko;?apE1AIKf8YF$rQyLrxUQ~C!~p=?2Q zrYraEJ-gB|Q}aRqO<)7?1YzwFG{W4Rn-E%I7Jda9SGc$+jp^UGZTlmxcOL*;A|l&R z1n|9#$f?=V6y@jFAl*ks0_T9@)rb^?ZUF!=&B@A($0d1sOU_ZtyY}Kx=`%DAi1{mON#g-*e^sb zcwugS9wB{RgcZYJ>8hKWazb{?{X{dJZStfQ`A4823fR`$g!NP6;E{BwG$02+aPX6O zW~Wb|9wi2BH*O@cC4|BQ+6oQ24$6H#*wkHYELg7jC%0Gy{t+UI%@%oA;xce^K;zT| zX(M7mkS#PZjgC;A&8bra-ybOI4}68aai{>}p)y1@gT|xaJ`ij+F)|W~fvT_A9K^si zmO=*1od_l%6@YgtP@)-%d~Si02ru4ae(dQ*TtX>%lZ012sFC=waxu$??=L8OqS{5NYi|gLl~5blRT_elE`G-lB2!b!z6%0)3j?i^&jcG@iGi7; zaD-A`kNbRY-V7cRquK)EuK^5pr~vVs{qSJ*FRnwkqC*VJQ#A+`jqRaf7Hm8Iz)gr2 zXaojUn(aWCn#7OX&~fo)PsBZb6~A@UF`6rrS(J(#g7O)F6e0__H*WNWPsb2Vf3Y~@GGPhhU zqAEnZu%tx{=>+IDwy6ljCnhH^d(?(qF|j9~T6$*2e~yl-VT{Ci2h!$5ce{PtJZr7| zeajXT#=ozPA?e-Teb6%R;l6RD~HgU@}azy-(vLw=uft*mk&b#KMUTauV8i~@vZ z(3y#WKm;3YK%}|w&W$_!EYh8tdh53Kd#!EoN$sg$8kke~Gq8P==GN-_qx(n4JGZcw z3s)cMKLRYC?g{t0sn>tGFu`JT3$-YT7eNuCpO!_b*TIQ^H_A;I z4*^y?%5w?){4#3a1ITs6h)5;og9$u};)W665{1m^M-}g7Fef4sG5+b3F#u6bZ?A;4X+fESX-IVzX-XU6r7nUf{BFzh&O z51$wtEua|xRj8eK^wMhLa+{BpRkNxRALA}w5wLdS{ltrkFtAFP7{n+Fq1ZyrY`7_7 z_m;jBRf%|~0GfR)z6&A3s!y_&=oRRc#Jn7UVTJqo8XV9(yT(n#v|~nSB1|A?pG5YL zUtF9X2(%W)2=Oum&J-h*;M2`FLoippvh*u=5r04i{{6;#Zn(K$~sgUW9HB)~|5k_jb%bhk`^`=2@}NDK-+BEs%l;9En#L%6RnX(P)2_D#;pH zi3na2Se{FxA+ zfprK9h^(D0l9bZBcZ(n#4UrHj68;@KwCS%30USl*d=rp+YIar=9g2AO05x?aXUa!}Jg=ab5#x`1c?`DDZ;O4Bk>K&hN zTfY%6qGCtVM@vJ4oY>|b2f}cAz}Ld@HXmYM6;fyeK(&pF%c^d`3Sdm))vKXEigtJ% z*64=}I*isX>yE7K!h35d42_K1@K%mEp1mwakMy!;`>%p>AZ)4tp_Ryr5Tz8l5R3Ll ztGSWO+0@be#=mcI8b8CEp$K;pVA5nyZ&o^nznSmdWuP|NYPQ*U;*OSpp{x8gSKELo zzq^;kEJxH$(qk%*1jzKA+xPQlyNUZ`lv5-mMuf&+K?#HG4UW$oJT#K#^YasJR#p}s zhOw8AQ#j|UbG(U0R#gPHd~)X*V(?0TVmf!)joKt{f`oq_+fx; z!TiAHNBMUX^ctoPC%^5@c_a{Wt%SGd+3eANUA~F+$~bRb)jL4o;zf|X7%{Wx2$g#w2YbHfIvFVZG(L~+8wS!D;VrNeE(e(Q)^UAZhSzEL!o{X!C z>VFm3SQ!<-HL#oXbzh@QCSDh1@T{JB*8ITnN}=uxLLC3gY@oH{xf5tcky$0h%Wi5? zf%XD>gdR7a-`)Puy`YsftgI|arRV8vT5i^{(*Xj8k{!HOuYdrG*{t?GIbf0<6Y`!y z=KVPy%-yp0D5d|rjBE_0J7q|o}0!F7g&|X_@A~U(_r1j%#s>X zHeJ8YaQL;RtNM?T6mm5S!Dk}}x??HIFDLGK$m6oZP}h_-huA_e1L;XeuVvc=&yY5F zr*|CvtzAMyB2r)JY0805NaAG-c)9X4vr0g z_((K}lqzXHk32m7UEV5EiZ*@ z?a~x-SXa_g*-t-p@YWyu&+@H?VRtf85W#xSQQB&3wDW}ubH#;F{wVcTzaekjXF%RB z>%~1&;kOEJh5G9}T%lA~Lmk0L0b4J-fFToJatP!6#vdvRmv`h*bKj~@r3C=J+ID9MspCGD^SWiroZI$Gy7S;x6Z z-%!7oNsbkTO_Db13cC&c=a`fE^2|+!>x_8W)2rC2;oo$zOo=y5e)i3Oy8C=RPo~X) zlfwU?Z?3~5I&YI7Cnok9yhA2fvcamZG|ihvcO&_fc;>R-Qmt{5asF0_t=Xsl{km^HpZB<~^E%JtJdWdZKeEFt_U=2!_ym>C zOQ|~`hFR@o%sHFgasBz$qnlRi`xbjTD2{B3V5X(0*ZN$(={pbG*9)`+&py3IjD06J zp7I^muJF*sH*1Z8B-GSH>T31~WfplC-D#C*8POYh_fCsd&~=IZ^h-$OHUx=u=68-; zOAHBkcE`v!IP84CmyL=^IQ1NHG~y+MxxY{I>?{hbvIhO!;JNz?$ID;!KGZ#bSbM!r z^#kqRfVirBEYyQ)qr&qM@h*6v1Kg37xaXWOeU64Ktt?S|tgEam8^Mh~zPlFV%2t1M zi{WkmkT4z3`r9=gaaF7%rApN;hR-2JiBE{0*V4h`D= z$}Q310^_C$G@Nve*{ycGecRWrsx6OvM=o)?cO$v+8VfdD;e{7hUi|ZkH2?X;WUeo^ zQ6GKgZqweDt1bO{NH-ymaoq*8WMS4HZ3vr{rvidJV$Y=%#K^Z=NFTj%*A(=l=jVc& z5B?I3`7U0tHtBUK@0RBeGNbj56-k%uFzJ7+d;Yv8i{+W$E2+G%eSV*zwtidh{p#GW zx>Ei`KRjj9r!c5@g1n59;&PI?an7Rk-Bg9Gcb?wyAKk{l5PVCNykz?|8QIyD;4r9# zdLUz5KzghiN<&S35p8r^6L8)?RPVkaW3TJX@J+T#_t(oa!9glDhm5w=d1(IK1$We`~L2cJjO6di>(_uq&=a`6<3x( zy(wn#V=w4Rg;Qd}C4Q_NG`wwHQwJXsi3UXMy zjwkfm$?gh*>=W=8Im%ze^EW_SlAVF$;zQWI$SaW0yhC@hT!tuV6Ex#`=fb6Jc#&D2 z(vCZA3?G{l18o+;1fep%hm1T1n3c&`NOW}2(vn$#FG-Q!zrVxEQ+R*dTIVNE zce6GfW)!zxId&p!cAwVEiXe|nS4s8uPpaJ1=gh5g2R=Rn#RKd#D4kCxSfI2ckU+ZY zFI)Su)-BMR5P~ic6%ET9Fcd98fu9aY8EUj1(7@jLaJ%~wavBJ9-C_p%`h-noqhMiS zVK@-F4b;!gH)ydW`M@Eih>3gULqF`{iJua}=-juFDsKvg5sW}ENFjrd)9rEt77B-l~0EEQwux|fj^>=v9P#^EH=AzbRXPbQHfSS!C#(%j?8mZnSbf)HRwrpff(k_ z6n84AYW0@Db_M_0cO#(aQc~1welPp*n{V>wA`Sjk)0!T^uN`FBzSzXUjhn2A$%UK? zGg?Rq4ernz07Dc*Kk#FUgjKsN_V8iX^WwKo%m~`=)B4;{%ceehw*nww$?`*v+={_N z-4YCsAMGv*yd%4LWF$U`Iq0o|6kdu3z0B5qE8jl@iC}>D8Q2m?@zG(TBzO^!|Ibj% z+0I{u){7A0i1iYf9>iJb>{+&=-^06eT?n)d?-^P^i3GbMZIHKb9krv++~W&XfwcyE z-VcY0;6>#$9%m{TY@~@?=5)_nWIEc&?TwxEH^L9zOZw%(phE=@NpE7i1u{g!GLBMW z-iit+vsDJsi4gwVL>NQW1Vc;SY0)4r92~l`{ZZTi^EQ4(`8PiDwMuhedG&+T%VwMQ zrl%^cYT9zO+S}1-w_Jl*L*nTmj{txFRi|V#P0)l&Lm(u0IS8nvpl@o*saY;sp3@^u z5wPdI6B7krXzT*z@HVztfWsYn?u|f4pl|3wc|mM+KxEmyT){1eTkNtJC#|#!2%X;D ziJ^loz1&H$qiT(VfrG&jr%=$3&DWl=(Gd+hPEA8YLx`BL9lYNn33>o2wkRnnDLldb zjs=0$+Sc|Enh=E14?<5DO8I!`H@yQZuL!+RAvw?vvz2`RrOYc+ek2F_{$0yEJoHa- z(g)U{y!^9+ikj+u!t+ZN#7PPbg@AD+cF$hDqSkIS>$r)=AuI8FBK5gV&rx!x2Pb71 zaJLm8jEKPrS-fQhC15%PLWJc6T3i{FjAB?@g!+cyAZ!;6c0eo|b-1U4ke{`8Hh(ib zlEKT7>#>?_I>Ew)>dkHrZg=O{?sZv~k*06_4kU3TA8Xc!^C*Wk+An`{=%O__ zm5v(nh$zQ(+S>b{)e)rR_+;M~)j|%ZewVH5*)5_0@wW0>@g2Qe0`P#8m+3WFwr9>% zNYF+Vo9^4Yml)teDk|Pm3;7{w_W@M$&%A~bbra-bz|u2XJz<5lf#SkJ4==43f`eoA zoZ~+V1B#9RyuTC9zA2LHo|Tnr0QdMG-_Ntk(F^*sHajYUjU0 zItYa;!%44I+w4@en4S!q$E0LZ|BwmyH~Hm##8;)}0s9qt)Me%;D0vw(ksEp|ctvL! zCC3_aGzmwmhd7CU_s%nKRQU4=mWGxtd87mV=@UFz`E_1{L4iGS5NHCVy&Gbj(<`4m zVQXq`4sCWsw+eBsw3Uy0;h`PuHsWKfY@O&n05kRlo^Nl8hvpb$_fDR8iD z9s#sQB;W)BL06ERZZ%K$ZGc*X{4tzi6F^HYVy9Ag0_0x8x+Lu%=mM?bCQC?&X!QC# ze*E}QZIlMl_q(w!tmK|_dQooF9k!+s zJyr$k+P_D$w!)7hxZuPmCSC+k^x@Mdc%KVfLtO+I+0OxSAvh{Zq^Kj^m?ij9RR>}5O z-^DHSle4q01H4?QxC_RA%m1aI6f(2=C7I-GTHUxAIgF9{SY`=V^v(M?raT^Kn!3?x zUzzOb5Q=VV88dxJPbHisK2jMsz$>$#9U(JKygu=x<}>qGr_T5u{_K+`=FE3dBH;UG z(QkY21o_b4$=hgxWR&;ie^YL)7Lz_!-n~3jPSN?!*rFQ|rm9V7eJz%-^wFcrdrs~?KGaRCqgI&R zA(kk){?eKSiK&yW9@96(;$a7Mk=)Bq@h8wf__lBTPv+0>;&$M5dG}Yj?9=>f@2h(L zKJzg#Yqr%CU-6}P?Q|6rY(vpVjGmbPd+Y@ zBl9NGl-DNC42*?aCWmr;kJa8ROpr{beX|K*f>n@DPkqYj^b<}&-7$t8Prq*seH{}| z;i2^BCgCtH{B)+MyY(9dcm1i)r$`DB|LP(hmG(;gdEVz+KYTp6A!RtscSBjO-jH#d z?<;B96i%1x=M;G)4@FdR;o#ojj zwQje_>w|S|a+7Kw_I%j$QO912k^-VWbX9F%gNj#R9gQ7J7yyL-0N(-99D^NNv12}h z<30vu6msrrM`s+dyBaMS))bTxiSR3m`nJ z_EkIVs=SU5g=BGvTCkH$6kuF!EA z+)t+^>k%55)KF3+J0y#>UzPpcI6||JB}+9c8x;f$8TDPG!^1a#xE-G;pAUD7w*0iL zEa9wiusVt7{9jwE2MXh)mH=;xfs>OCIWMyRF8oR7fk-Fl#(#^?C#Ybx&iwvwa8Sur z+HPVr>bLf1{-jX6leJh`Lu&2ii&tMX)dp+qUN;Prj-$4g;uJA~mYigv0tJ31kaG6a z$z#V9RaN~L+(8H;aUR0qZI7B;TB_H~F*=R4g1Jj)J>D)yT1FGnAX9-R;r;9x4@?Fo ziRcqz9l=aX6D*bII+cJya#T1-O%@?-L+ts{hqY}1g(5!25MwnuFTVp_gY3}Fxgln!~R%N1fgvn^B=+^s)f@_=nocp@7lG%BR3ZhWJ} zuaiH^eYG1pm``M{6Y?me;_F~4ZW*G&53h;5RX67QC-ua6`!)N-W=FS^f~T&G_odO&~*{bBG5+k-UZx`cgb0K0j7M>AEEec2k{E>!Y%7!PV`j^ z*MMN8YBT_`KqfyIFsA+ZS^!o#rWg>Qo>Mx&7Wtl*?9 zf&mgRvdH&Ry z310q0!;8_qOY9BY&nG%!+TT~L*38f8{*&US{reZVT|mPNQzhJraAI z!(70RX6pZ;uZ}1zKn_aWz<0@lwSMN0a^C1CD{Q%Gpu^RD<@BU6Z%4Y+`!`qv+Chqu zs%jrjc+Oi|eYkBF#1eY&)}4MjhN!K9KX3iiScI))>DLA($WzI$S@q$=n%dJAj^Q}k zDgJx3Hy#iAVw?Ww62GJ2^=;I%{c}g3{HR!C5y94QtUYQ%?jIN8kbIg#l{2>oXD$=D zY3GXz}J~qw?^6q$f=nJrudqrP{4_ z>us$Esv^5fqU<+Fg;+tqnyV#;;?DW$_0sA$(r<1&btyeq79)5P;w?ia+*d*l@0kNIv& zUe)8uE!WR_h%*)&1|IoStXPx|Kw?=*5;EfPnk*{SYGo#$cu9)m+eJRmY5sFD8yBM4}sUJr^CoyL0^ zypC2)lfA%t1%sTR)?2(3o@SBVG3^zRijnO_aN28C&D$<=$Z4R8vv+g_BJk z{KsLet#{jy<)wOZF)?VL$7_lEMq>{K9!SPC$TW${7QB5T{eYT;1=*ohuH#X?0+x8p-U?>dd>iN=SNGQ;E{Z1b+bv<}rg?+-H{G-exKm|IIZ z`ox9BIiI{?(3ZE$WZabesQVRwiB^ko>4)rGa!Z&aBLZZJj#|hb^PeLkn z|EzuG?YThQbk-*=-qw8s~QwgSgBHrFw;H$Jsv&8g7DQn)0;-H+5h6!&2ZmmOji z5Kz~K7u&F;)q1?hFT6^s!VP}BT~~e!rAdieP{=vR9f@2I7c;fS-wZRLE62j&0XvzR za8S{+-wZ}x8Eg#6pa~I1Wu+_))j7>;GECmwb(Px|uT1*pp_7R%a47rtqx-5`8jf8j zT~A&5zEw9=RM)OInc7f)Y}?J)o3RV}j(arsz}1dM`$`J;>unhG@lSY-LMt7eYd~Ib zH6?+1aK75E*4a^7kP-8wjAGA7POMPS0FsC*mY*83}5vQn?B|C!MX;k(d zQGqQqpn~u^C8HKQ#WsMBSafAAUW$L?=Dt z!gg$ZDtQ~fAnj$TH%425^hgl19v?yo0K-K;Ys!r@3S;x%K9N@%_;AvVudV2|l|FB0ghPCi?BjJsBm}FZ@fu za!1EeM@7d37g*NX23=3G(%^oY`1J42Zv6L{FuQc=p{@wYtDjLi%qHqGIR4k6Pf*JP zgsu;@weN3I(UfN(`Fsw!5FFDE|9ch6=W2@;m-T7ISLlXQH@hsn2w-xNOuKk0&iv&+ z0UvJUM_wE`DDa_oI6+mb>sqhAtnixeme?ARS;wh1o3hkti>+ihRjS1)?%V341$AtylhgjvevN^XyGVzCS7ZQ}7w;fa2TF%mW+ zWh6Ehcv-}gl{m2C`d7euMyd|(Ckr+=Y*&@R_=Tqrom}CU-8G3xN#w7BvG?RG z-Fwg{=;Mk@zR^aB1nIdHuUs63UhNU%>fFnh`dMfk;yk=u_E0+{Qj$Kf-j2!Ue-R4_2R5(un%k)h%ICMu}0?ltGv(s(JLV70MQPCf?E!KYwt%;G`+z)A$pC4 zp}6R@Yt0#$bT*fV#xk+#hAxOS02=0W=ms%N=y^7eTsDPX9j>GK@+D+!BVj`qR9Vet z<}yrA#ch2FBp|IJty{Zih0~-s+NM}b4i=vcQVDwk({3O&(Wrc&+2f~jMKor2^tl!N zV3-X&zE1B1YW!bC{y{tgjA_$*xeOi&(N2au~!x#yY=Ed79)${v?Rj&#$= zvf_M1T;IzsoX9o|d3a{`C-+i;f6;lgo6hFErm!YjA#(`NhCyxS^>#OxT$1n6bD_ryVMl{fZ`7{J}c$ z*1DXPYmEyp!q>ta^4YePYmSgM6mJBYC-3K+UY3*&>m zR_z=V)@VAk!rHnRwkj_3WB1Sy;R90)(*WI#D8g){~6oH4Sp3Mwf%XcG+uHytsq zIDVWK99S6YoR1;ASQ(K$hj=w^CodhkcHq41tlVrzjHT7{dqD>d#432nG+y-E@0Sy5 zk4qZEw*MepBdq-z)$XLc<4K8=Iqdm|4PKgatx;3TqWN!@8_Wyzu)A`DSd;y4-BZ~T zpWiD-Mobv?$ec1h6?ZNbDCCDXDmJ&5&wLS9dZqj*F-}0+D?B~C)wq3l_*>s9dnK)1 z|4qx3K5jcuyh*{JAZYxgGSBwKT@)o~!Erm;mflbYW*znT|8=%DXw2T-d$)Jh4XUry z^_qK@2hZ?6XZ+uf<0l`t!rl;#ooR75YvQ!>4%%zy9)rk1OOVxXhjUA~9x&cI6t=X= zo>7I^zT-}@4IoO35lo4spB%#M5|qWn`3dYzpgJr_z4oIKT-~+HY*e!N=2bPo`4Ktw4Sc04Yc|HT@7AD*uLdgxj)Tfn_IzTgOLj)oC7 z^RAxS6KqW{L>jcwS25%--pM_$F~*|sSZ+vw1Yi9#LveMN6U>o<#x96iy`t;7(!UO% z*_xRgbNl}t+686nAuzk4e!$c{X#&O^oIF&}H}oxqo%n4!j-G=kt{Vhn;$e-lf%F5Z zc5WPmkp+>K=R`w%0zrTK&ngCOk}CZ}qTuH>c?9-I%bvE%JDR6eYCqDDM)Pp*ZAEol zu3*oxu@JMM>uN=QEmhgU2KNmTqh1ES#9MAvZ+-SM@ueqMaS`j9nUx>dl*HNtM-lun zLv@uQZ-GL&5_*yfaiCgc5*(Qg#K<}`ul|(wX+Wik(Y(40&%eg@aZPVV zKQ?SzNK%~OY46NnyUE1Kl~fL^I4d3D%`r)r4+7QsiypDE`oLf8k=|=`E(xv={nA=6 zi^!BnJg>DIH{J;eVZy~m!7vd1O-(<3TttWciM*SPmiEbF#uOFNYC=`mRgV80lut&h%hBVR- z3ZRnm7fWU}ad2`$(n1W!ViFVAqo+FUywD~7Tp8-?-Fz{Exw?wN3*15FM1Q7WnkRoLW5}xNT97P@Ye0b;Un6)U@Dt?F5 z-O3BSDInmK?8EcvT1>+Gm=iMs$W?FuT#dwd7pjB*Iih#SKW?k8*?@r~r$QddX%{5^ z?_jh;05&aWe*c5_t)bxGv4and7mT>_0SB3umKGG{o3>^&u2WSz$gD2_lpE1~Bx0`f zZnG>vec&uqCqCbdX+8ocB7DKF2N}NLUFLQdLf|4W3?YnyMhx{gR@M&c8FbqqE8vFM z@&AgC#LR+Qygv)3C`L{m<>ld{r?`-Co}((KfZ#Pw@3R$(#+3k_y(G>@CL?$JJZ z_y`ryfwHd+nuziml^LpW@8e#)K)>jSs{#Zjv>ei#sHkZ-RU^1pM_B%WjSpIuokT4T zReHiGSjYh8N=aqX)y4A&nF$N1!hN+F1@PP;Lv>+qg9K{U;Y3jUN6@^Y&w1G0JuYw< zJ`D*|;3pqC8C17%JFkYoQ*%MKwjW%F!&@$VZ=^Zv^jtcAakSXWVF#aR?qRutl|W%H z;`WWu?Bhp+#6T3(CyH&td+4$sSosCiw*ixYA$v}+Gf5157oVDk`N#ew@W(&puvM}Z zbOW#o-rNC~LJSDhFHl%Iaw((x#yDIB>o!ts+xSK0HtTeo?<2cPIALeGkcez*r&ALRu3i;Ua!twY`RE z>&JYWWi-W!JuPXiBD9k^uc&RDmlnh@l8FksGk8-yLo1vZa7@-77$;LFkuvze>47K+ zz^5eRNQt0;P>^7x&<&;)AN^B_5f(*jd zQIMHm@apkI&P#?pQxL$J4A?=6a0HZgSksEy^&g*1zxw_4`AWja!8+rCw+1i(GKv74 z{&$OY3FrPIH1aVR;-lLFOH#}r35IB&3=aboUjHRAs*b{q!SFWbD)+H&?wY?!pFXC< z-~GjwlHyg~)+Y_`82HXLDXO6=Qwe6YW3<^lwE0#@fX=;Q(C=`QYK=*;dIP!_q^57@ zrypq0Zm1Z=>@MF=zTPQC!XW5&^zYi#xbuGw`>c0g4{dQg@8OFEL(~Q^(wmC7j=p*0 z{njPr{4H;lUH|{+fP|wL@QA7P(`h?g=~ttr_hRB|pAWiFA1_Dx8vaGwetUAk?*eAB zcR3H7Np*z=3j+m9(eL;d@bongiSri!nfqtc)ssq?Gmsf0|E!Qt{_>T|m=iB>$v=}A7<-cs zonG(k!uj1D#V54b{{{!{dNfTK7#UZArII@Zg$D_4;N!0!)+Jz-QkJe_FzpSia--47 zwdJpv6%JLMRp#tz*?&=MH>hwXk6}+f-qwD9oryyK@gvV~J^Cj6x%wHaOpD{4z^Ao( zV?3v4OK~^tH4?&!Vwhb9(QT^MFL=|y5I)6|3sh9?)0)8-RV3Is7SGcpbM&q`z+~~_ zl0wCR&-k;^fy&7?zqOb8Sc?jHcs16lajL;nkcqomAXG&xv-x<9r93;`@vSi`J4=tHz#tr6V@|i|H^$wvw@jNE}U^*v!;9|}x-1>uLx0ApbgxiGq_7o-HG!Z+grE!lH z;uyo+S|%0H@qmFrPV+2(8Mk84#OS1Tx$G{k(^3s*n3Nl4_ck)4b1bNK$Z-{{Yd!Bp z-D@oz5E~McW_DXk6T6XDQsRt|RK4gyseH@9zNQWLKCu_452P|_JNX-gn8 zMfxqx=CH}fcq89_Dcx4nm&V>)8|CUDpn_d6bCVixh4DW~_G2PRgQo!ujyj>aL)bdv zl%R?NnOE?95Wa`_|M7RG*SqJ<4FRj|x>Uw)$Whq0b)>36b(JMjM7Maqx zdvdgIkew&S7L>|HxI1sfGwMFxx`p{eR9kp^dBHlD{d1-Hj^m{&UsY}v^1Iyl_q4^t zFBFe!h@J<&%pWdICEYrDyQnuCCZ-UZK2{IGrw$SWbxQ)+AAU<&9 z8E`a|aiI>`rzQ64p4|Jl4xZ26=7F`Y^m@0rFe7m zNeuk=S&^0!6oSw>^V;1nR-Xi^<0aw{sCO|(}SBFX)84Ivk8T#al;-_@SM=Q(}X*`H#-cl}#{>eeG7QL&;( zzPl(b%ZPo5HtXn>PbP^{y>Y}!m)$l>xV$v_9jtvhq#l=l2v_s`EmVCJZx zP@LTmlKponH3s7yth;`9pK_Vad`C=b3i*Pc_yVfq_WQ_QcL~v$*7zGe!DkuqtD|-R zaC})Oj?+bEPRS>aGbSN1p~R3e^j~z+u!U@}L>Fw-4{eoa?Swd-xa2kr^XWAk9Xg&(^fx{ZfBD-`J%aej^JpY~C??55XU6`gAI#t|NaXZ|J$}9aVdi6+-nw4ea!(@QA;fcx+N}!-3<2oonsn+T&EU zG+CHgI$LAyq?sMog7tmS@N`tpCnT0668RjafAXieaB*YMbNxl>KW#Ir5bLetf9A!k z=yGuT+FP3x8Q5Kv7&fd|o!r^Ed)wKAitpM3X=c`L7GhR>;q~PP)1e)k7|(6Hxmsgj z**Jtw&7U&lr@-Q({&z!D)2OgQ8Yjuz0r&pvvPn&%sy2H+()|o7+cAi4Ko`X95Ug*7 zU>RaPQ(g30WMFA{Wn`1uLyt))jAr_ zMKpq6UV4!xC+%1(p5J15PQGyR+SS_c21rt%b$!_hE~& z>#vMCw-57f?5ueyAtaOG`^#q_vu9>wwTsjaDVaMC{CSRhv$cxXNSUdB-?{xH`K}BH zinnKCkJ7;rfzoatnFCINfddRq^IvWVSeM>j_r|h~+%$5zT#)&}K-gs)jiqD^hfG5* zuHQZCN-qk)Iab=Yw*RqN%XDLp#bt)Eljlq&znd$^WJ>1&H&u{Q&kt*~i%vnUi z%!D7BH_9nIRU^pMf4#TN{Q>Ukmc5rZ!MzI;HR>VAKM^H+rP&?!5Mwz#8^PPXlb?S8 z#1^;@J^@if>-yYx%|#pt8$$VRa$;1VRRR%$@^LHv&Nq(gaq9c0ncBhlb|k`5^kiB&{5} zVxld@AVEMpp)kGUolnwAU<5M#=sW(Bn($F5O4tQL`~qja7KXy+maGo4pF650ASV$% zbT!a=Gq5-}N~V>g#d{n5g-0_9eocj<9imZ|b9?Q^o|e%& z{V9pP$F;pl;>m^~)`cr8Cuz^_^=$Ddh;))HEQ`v#VLcI;#>?`c%*m$s2Uy0sAorre zk41)WGFOC|x&qSDPtZQF9ZAXF++>PxqUDr%h=q54eleaM!>N|Bz#D05r`o@`U6Zxl zNFimmgXGb?7EgzECZWngX| z!_0MX*U(OVZ{XBc0f-^sTJ)nwM{R908bKrb8D4d%yWd57p=(IRRQ~fL$$eBRbszgj zvXqAw=GGgA6^1Yk>}i%cjxLA)QehUv4C3PA(5D8N7{H}*9X-7n?taafU3d99GK-nW zW4b3*D|F7%Z}Oa(v4>1YKI(dZ*hS82QQEPhMN7Etl%u>L2C)!4Qr-hGqh_`RneM;Wqv`g`9$dvVw`XY{bkQmdDT@B*vEd`<7qmzmqS zynG*Wvg*-XpvSz}cK|P|(6|3k`_?@*PUw|xb#1LF5FuFU-NZX#{*em0(h^)?`tVZt zckS{+7xpXqZ4fQ1LoTqs)eIrxF1u9fn?u=tBe_)Wlif!!|FH^P!2Otvn`PeEjdoo2 z@S$I5@?>J<3ciz!y90tTkdod094xGt&XT;m-V$NQ-@MlI{vdNRS;?3VQme^y{_?btE?i#70}Ahri=ih zMS|UhdLHe_kd>tYg_jI(rPErAeb%dWzJBYm;*PSa5+0F6PeFmvR_l0pcw84pvt=jC znI64GkEI7DuEgUQ{fkG=KXY3n+6Fm3ht)Df=H7R|xn22jTIa+nN&agW7KcW!DHKL# zOBJc-n~(aYvZxPC)H>(7gr1Y-)tKTax`nxj_PDMJPf996k%Dd~y#OHJIRP=+t$Tez zv7dy4EK9S9xcCFG(1`yM7Q+WLLyp5u8$R&5&lWLu(Er5?{_3Oxdsd;U1DzBY8DNI> zPU;CfXCE>H6uN&5&%S!)E6gpRJ$VoGf!GF%(R$%7;hZ;n@}#m-FSi(Z%Eqbj9*dU_ z{Ak>`jj#H```>4u3kVPNop-cd(jRucuEuQ=K^3|)U z`or{-pOv{$ZfclL{>uCHCEw7hXz=$MwK0QIwUqPEZ7vvR+!Z=gnbO~W* zeQL?FZy(%EzAGM}u9O;Grc)2_BJ_NCLiJf!qp~VU%lYsCZy7 z5|uxeS8bY3eSOWOO02T;i_d%x( z;(qCtV$+^3ba4^RYfphbqAUMi z<$}$$&F?qyz&0J$(Ju|u#qw+z2~y7 zH9r7i8T{}e8SoC*a}2C~gf&8mUbr|04?VGp@J67st`^QtyIA2r;y&*2@sjxG7phE8 zwXT$INHM%{xRkn|`)S>mrkCY$<GQzx|n9* zA7KqnZU$_kpGsQzCnY5{&Jhb*t@e2&6qIT9xwW{fz10npV5^>K^g}akvNz=9gZuXn z!iW0`?7*?ppP@nflF?Y#1tIb}euS$rt@a|O0wI5F${54IHC|cSrM$qEcQo8&P7Y!x zPW9Djl3fIAL@VjcCv$r+L*1W%;1UTZa5$^gAe`2r`;ii*z{HTQoN|?V;uDiavps+C z{G+jfpk=cK_pf5^t+o+2EE8)xTZS1K2Hn~!&m)J#9AIK-@{6nLV`Tb1K0b_CVbjo@ zPMjwq&Z;UhI`=+O3se*fFbolH0aW~}J75o}4s0-xgjmf6FYMv3dv?2`@dA@g8FMmd zRF1(xn5u@V5--^AyWtHwPc$9-cRz%u1$dXQ9{##}@Q96#?K8Kmxi_!BMSTv7)xGYz zeZxlK3)-!MbI~nz4R)Ggc_xCwxAZm{9C(__U@$p9k?qjLyRoWkK+yw!(-^=&OSg4z z3=Xd|UyYv}7XYEfv}{l{s<74pP`Ap(i_cARV-9p8a=x+8yxm>5!2=9kuuBLB2UbyU zlRdiqb`ZM_xE2dp7bC3^)Jd)J_%qE7yUiPsW&*qy*~5dPdJ11I>SopH4>>xDe_nhw zOgpCa_^Eb*-^sl4E{R>K0(17pXfNkaP)LsAK?76*s-#C;fRPy z+}?3dkbz*P8$7Oi3|8>2Uxy8Lu>OMk4EOe9iZuY$LUw`^nx7T|6<})8Dg{ zu`kec-%^v~PuXT_>97l>a_ZM(;Iey)an~N$K$q#9cQer~!udskNS6Cj*%%e{v17*` zN#fpUx#c4uEo0oeRkhfCnN7~cKJ2~0)1}tsoa{e^t$Hhlj2et*ot=3|foaukc!ukI z)yJOQllGG@?3~STLtjPSaEhGR7%lJnP|)XfjL+@c6X53%qJW-{hQP;agNz47R9q}9 z6-d(;aEyVbe*qgxcDhD_sJO{*Qhx%5jzZvPWM(c0{U!`N1kfemgDXb}Ma{VZz7OxD zqz^cI47oq5j1aNTPYPLQX2x{lRV%CXe%(C>286;>|Lo7cr&;T+vlNjp+##>S*-~|> zw7IHa^kejNp!&^p-O-TDPY*NOe{+0r647}p(Klu_D=!ccS<^DL95LPz-#MQlYd<`9V4~IcP0uPz_Rlt6jnhk3qYOvu znk_~|du966dN<@yUUZ1<^$*`z`^)m-6Dygs?nT4>itTipM#9(C>a>^V{4QLd`}sm{ zv+b<9cenKZG;S0baBglsdZMMkF}HHx zo`H%-p_$k8m+gv{KMgqby)AG{_1|UI61vFj%+=g`z)^9)e)jqY1tzY}V{I4Cmr@nP zZy7wI#A3y}sv!P!^9CtapJ?qIrAwF0(#FbV`njX(GMY^EKNehSc061{r}RCrKk%v3 zsmA<7y4Z(TO>}Z^QmEDsf$0<65~m)1=4kLky8PU=*{*bU(KNK8`xWcvV~!}aRPgb3 zA1vx%tFBfU=dBjCmJD;AjuLR8ZhJq~W}owlovyc);zP5k17F2&fuolBKX+RSo^A8Z z9X3@SQ};=-YSQ2GwNdKY-CGw02TfHS4LRPkjB8%t*gt-w|Ke7?i*>Av9gS718+YGe zWav~^iecSHRk*Hh*~*hO%Ch$5yh7G0wNEN073nNB3bN7rY_5LTD^4kD^xQ$^s!PN0 zyU~4CaSy^7IFLuD`7e_##&_xm%_h(gPv^Q3T>RoMB zMK&|Y_Km^G%!&QzF7Nb!)`Q8oz>S@KeHExUpb<`U*lALV3oJ|OOj^}Xo$`a(rI5Z+ z@1fgq5Y;q9n6qhojo2cJ?!OOCdN9{iuC+1xSXXIZ4R6Pek{03D)vgy#OmPjWe>%8l zRYBX{gXbu_Mcz-gbag!OxGYao%bk`yZ!?>IcXfxr3%c49!?_ps{N$C|w0mQ8jPj8q zVyIbd#dJ)Lr3D0}#jO7wy0wEUQu(kaH3!n8;H5uT=r@&#N=H9^_gTqq%DwlvpfZ!D z5X*uD>(_wRj>@*aM3L$1$6U3~)8EUXFXw+xD!k1ch6fL+-ZF7xsS|8HJ-u-(*=@(Y zRqR7YeeN|MvD|isLtb$OX#%N6P}avHXGNN>XHh))p5203cPK49z`FXosMSt3-3zvN zYK=P^xSA&AE_jCQe&fO+Q?$BW`xK=w&RF5Sxq}@eA`OK#BY6cu*72L-db&t?ik9S{s ze$?con$E9@h?S$OpDIVpTczJ6ZWH>)-W-CP*$H;LZlyLFwE+^Sj;NjiATfTF)ew>d)E z!;jBQ(@!q^;_TAm=W?wt5*xVR)Lyz}?jF@Y(4OS1OZ~cfRAzkCF#XS;V^u0fx%bpshp%ZU@B>lxdrL~uOV|f>oy|QMiNa@dOP0JEOob&^B zmM>O+^Aa(B))m>`aS3adx9_0iTg5|DeY^8&;uyo9OfM}qGi!Va7|YVFs9a8@9I0vR z=UY`cF>kv&HghMZnMh<8qH<(*zg}-=QU}Ldn47-q7j*Q3+1TW!5u=bO~mkMoNE_j`r{rL|Pcb z?TX=-4{cfJHPzoNRNoAO%cIr1^v#b3FD_osGi)@G-{VI=WwS?zDxFhJ{{2$fbQt!I z@`IqW-xg7G?|Z)cb5`sX-K^~=1S(w~6~B2{-5PVtZecaDTaP%*WCSrX0{K4$@x?`n zS&Q~)&hlJ~1&2|U;CI%pKK`@I^2y6<#rK4L-{&i-)8MtWNm7z*FpUTHyS$nru5_nz z6pVdnW<=1h7h2;3G@!33qy)Er9^w2~v zO-EGDe0Ce~X*xlvojOw6eyZoz(h2UwNo|!kBmg+}J!tZ$0Qo|BWGj; z%a1(apx-S}@@*w7&Rk-DShRZ3)-|DrEI1*n^;HQwre3#~Udu@!u6l{3t}q%4iT8bS z*{5c|G>VgI6>?TTq^-=g=E@sS9F3^^SbOnnmAa60foFyCW;>pqTPE1bKXAej+^}IH z#i(p&b=>-3b}dl^oEy&0#Fh~SfmxP2XdkW4J@@pB^k=lT_Nja4tvy*;^1x=3L`T8T z?sdUlHL*oE9!}f+^Xk5cXG*(|ePL;wFX&xv%GDCjSbvf#BJw!4cbe<~7sLIKa-B&5 z*%C*-+BlP!lLM0qVII!%yMt2rI{^1Vse-3ra~mmz>}|Jje)n^kK8v)s18*;V1o^WlU-PN9p;bMd+teD+a#Rz zVtlj%H|FE*OJKW}VQ%H1M0Q#qX?OO6Z-rbxmf{Jwx_9@rP=X=;|sts>ikEr!u zZ!p;V1P|NyLt{n;-HC|ehZ3a)`>!b23ON19VHPoCmhYB5h&NwtH`afpr)Hh2a|~;! ztbTPbhscZb7pgvQC#N9YB)h0++`d`?Gv1^ljL6qoN-fK(8xkv4;mZ!=G?G2FMyhA+ZH~MUzOl1Z<}vmY}GPU}O_1toQbQL_`gW zFu%Y+Q_Nr7?(VK3i<^ZxZ4}BSqShnc(odolJ%E3+$-7FUa(`=cdwbF$fkBEpls7!~ z=`_@hNxe?eXR>6cyElGwRewI~-*W{!vB7&UZcayG-o4A#G#tl0cV4uw^= z#&y#rt>ImpxePztD%;TH=9q0#7#FbPw9#p$@BXjcX6JHbLO*@-8aaLTRuKlpVd7;u z*q2wp-lss_L`DE{D7Z(U2xD<6K%yyurpc;oz%n#3F~d7Q@8Fb9d&X(jZv0)*m~?1K zjDt_e#!Q@*B;clRs1zDmZd)EYlgW;EQ+Uo%`d#D)>o;% zq@ypM-~C{Ebfjxo$@!q=<6i|=dd#U^n1%;#s)^C0B=zY8D1M@uyq0O!>|n}@@@pWw ztT@WtObCEl!T7?GA}AL^oYKG#LEMaz`G7b++7m(Nsa+IJp{l9pkg?v z1GODktk=|M4jmHiVGeamtpf-BO?fiU*l?VhZx zGvH0~u>-A`dtbFdvpUm5gQZDqbOCBlP*O_e=@wiIJ7E6D3sJtsmK@aEL3?uUkh{Nrbl! z0L^Bq_ZaH?Yn*4otaDb&>2&j){k`?4WQSOJGv^Q4b=d_JhAdYfo+Nv8N`P;7c#hp; zA%nix5t1U3VxcoNr8FrLVKGmm<4j6q?7sghh%RhRK0tTAwxGtpXZKm_%ak7F%ZjFs z?Gm%XNpD}^0I*MLrc-b#);MrpG^i%($&<^v?r>B?CzO7^)h@`y0He)*0f>H`6tLKc zfzmL$G6F_#Kk|+4Lp9ypSeqJLV=?fMAX8xX-b4f9aedgwEb9*qrsj{8UJ^GF z55o|TU2iNvO4)K$;H5x}WUCb25BfXE&pvRc%^jYe9+sl_d3(|;DJ~_J88>I6l3IJB zA-P-qlVH22LzmEjmTf%0rdSO6HD9ykh-BSpdi4EugT>4ecB6Sa3-J23b=c>7hoq`+{1vctm-#+#+(3Pg~D@-_$Qrbef?75YRDEQXJ&c< zrfnAzx(n%te&v>9$E-iBmni{Mv){Ut0@4ZMR0~xPYttB>g?zy}goyn7Z z5m)%cSaocL`aECWdmO+TS$2Tz!ne;XY-|K0w+?iuXF6gP$uG}nR`0zE2lb5OnVVTz zl@X-Ks|2h~2lCD(Oc8`-wihaB$Dd8aCmAU2oyOL|*M)OiL5Vg3_9S%fjW1BRdziv- zGk?4&U#s@?pQti{!3#w(fTwK!j*)Y9y~cSW?9?rH@1H^_1B7l+JqQZ_zDDYf$m>Ie zEuWTOBHdMb(W&acc}XBy`XQB)y6AV@<0XCk(|J#`pD4S&ZvXN&otz#j2`cJ!Le_~nd}bObW!Bj}y42ab zCE)7IlpL)eej#e@owl{Q+VR88$23xo%o;_R#D^wNr+X@}tv(bmo)N(eyFYJ2zdDGd-e=i{H zSanVJLav&Q>`2tduFPW`IccGGui)$cv$Xf&cQ^SF4F~ZD;lW|8v~kxw5*G(Ho-;e~ z!;U7o#acR8%+4{`!ZT!@Km5Z9hR^0$^WfAE z==(K=*6H+*tFI@K&VdB3i1)PxCKIR~cC{%zyD5L+-!wS zUjt3|wxzO|`5l|~uYFXoo@R5|ojRkI8FM#`Q^$&Re-y)q&z>;Jk`_x*dG^MB6&Ip^=3&fOip-_Q5+xvuy19Rd zZM#{>F}u`_jz%e*-I}iS;Tfe-cT7+5{w0zg&MBTvrxxtCMbuyJWrUGm#S_p13UDkX zW2{lK)k;Bf;-TUFw7((p{5H?18%`;v!N$(mKHr+s{V|iHgO?EaS#!RN;axvPTvp8E z=-{!Qh5EfZ>EkF@CKR+QCpxYUt2fr3r1yF0SNS)GNaok2`hy{f)rz@udT7*wGyti3 z$B%oWSMb#J(uQ1f`FDbC3h;I);P-oMxpq$k%UOf}SF*La0_cM@kQN)@Bo4C|6(-xF z2v#z(1uBOy0N2ku_;uJ!(>cqW%!}IaC4=eld209hWhXxtq;KPQ7m1tQBTLYp z0qa0qcM~;W)!jI~2$r7=*I~pNS(JYjUC2BPdrJ`p5g{WnX3LtW)i?+#LH?=d@JZVX z)+;pB0%3ecZ~)p@h6K@KFCO?38TNAcT==e#L-|*CQq?<{bNop>?mx);yI#j?RjG-| z0TxHE1%f%!uX4%^+kQ`QR6w$Mfe5f-I4C5f_}E&>_i)CLhHYI^ z2k;uNLqfHRwc86;iQR{G7lK4WWvYb^iqjQ&;39tj9>Q!(aQ&KHyDnAHP@8!TTU=A! z8YW30C9zD$-usz}8QW9Dfr2X-sXHMxCBuKUTsKLmFycVG3mPbMMg`Eg1o8tYf3hvd z@YlC{xVd{iXTWzWAMh74D+h(1n9Y+!7xSx!HJASqh0nyLb(L4!_gx>q644r5YqmMH zHce1R{F>=KEfJNjr1k69gMpKx-rLauLl+8w1H6ZY;aH61z5+N7<+w+6acTu9bt}#( zNGiuoDvE8%xjl<3=|@=s7hqkmEXYV5Ii~=WW1t539{iq51mu+gg|L!Oi9N38XBiA4 zL+S+k^x|LA2){8i#kQd@;$KZN>{WbOc}R6F@-)|ABF%$=TrVa}duy!#7(b7QFLa$Sl0x!1$x8T0jbOV4U8578yI(4V!J3v`1phi_ljGxMx3IRxnq}bpky-0tb=1FG^zJS)L=lij)^HC!8JiUn@mw3hBZ>6F90nawKf}8 zm?vN?`}|AmA_B$V2zfZGDpSlfh#0EDEGaTT!IqGaAlBYcs@y%XDQaa7XQd5~L?->_ z7TV)NrZbycEGSQY$nW0%>ocyi&isjstTA@Qa=87C+9e7aMn3dxa?rk~3wN7BwV5^f z)ntHrZ(a9e=c!RF#Tkp##n(SRN39+>a053|G1XiPLMXIYi+k%{X)xmpcvnpQC1CZG zvc(9W5H42LxCP5vji4!q1Gj*uf#wkq6#$(iziCW1T1$ao(0y09Wp_4XkUlyz&8@8w z0LQruVo)dt=+?=JY!FwkK&Y}D%w$4h!cp)HPk(0kez!WTl=1t9;NzM$X9OT82_=7k zHwiEfQ(S#yqCKBwbee0W2~BWNTZKb=c2DBug`!i*c3xP`OvQIt1dzZcWdXIc@O?l0 z0N!KfL)3%mnjVLRowBt(2=GrwjjGQ4$B|w4xZjI=)pEX~{m$ExXC~ZaN#)RFMS}ph z9mY9C7KVWK05hmO;CTfzet<6CY?vj8hpNb%HLJEtNQA)(gOb2hhqJ~{Q{V(33k-WV z?2v0lq=H(!2OyZhdu;^-Y%3_1{-1~!6~<0I0GkTqsM}LB<%U|p39uA&{-wyJ;czT0 zDY?A}1FID&?&yaQ?203X?&!4(?0@<2g^at*`ty%jda@Mhj(JBuVL`hG( zZg8pJY+vg6&2(uwD02Q7s40mNJ0B&-L_uJQHN(Jd6mHIU{*65WB`pkfdsm|yJktqW z5mh)+6&*{v8l_l8dyr#}pfc^pL>@r3Hn^(xpa@Sjttcwmx^t%wJf8G9#xaU}^J7!I zPVONbHUJ$}VG*tO)wxyCPIL35f`SpI(vUz52OKxss0mK%; zKk%c}hcR3sq)-WH=T;bB@^)-xA4rNUi}3#nCq$1~0J=2CZIm$xkBsWJO8}^7f#z&b z7;ITeZalbWxMrQ%;8x_#JaGV02IEhIu@)?yunZPkha1tn%r&!_uvO)%8w8*a&G|;L z)ky+q{PfbTUAqhRBzZj!ujQ(|Xc<8<4SQ8UDsU8xxlfm~5pN8FaGISOp?LyJ76OOd z_1N+6FD=P(0X(?xI@LCcfQ{A7Y1W`1K+h=65WK(!>uv7>0Y!o6+~2PX67Rm>mN>R= z+9buPp<#{s#P!LFYpU(%GDwnqFOt!6c%4%G8n+r#w!1}e+FP%<4q@#h2Eo{?B{mh3 zC*UJW0gR*}x*C9RA8?zSR&>$AvVEKVt zs?ouF1I0AVYId+V--H(Jy~e1InfvDkO+=EI&~7h?)g9~|t(&qe;lCulPE*p1Eeetu{jZfLEd2|7}`Y;b6&(WVT> zIXMSsO}|^ROSow|=i|qzQOFgi=3p3OaPeZ`z!fR95D-a1Ac}-e%VDfnwKU78hFjAB zYYPh-yZFweHAfN#cU=Lh+U?l3&+)KJp>kRODZQT4pLW2`Lo7AFoI|%^@V@Qpp4Fa$ z97lsxpSlU#h-0aoxNk+7SZGIZgHK` zik%?QC>?x2Sf%)pZfeHKi@gVU3qJ6bo=es`WTDOcnbMqNt>9RzERn09QY?R$G~q-$ zbMw{xaJ}XaWi-ZbeA+%FE^M<|`({tgB`VjPtHpvPdrkdr9ma=0>$%WmLyW=!82KEp zB~Ag4dIcyDAwCw=)YMc>4s6ensFFE1MQG&sI2|hsusPNCjZUqJmtHn5T9S^u?@~kPn>1cjbJN@EkUxyuqM>;rHzBJ46lQiPrqGi^iGQKfw z@R)M7%&j9!4|;j?=RU5zFHR5p>_~XGAk$+g%yDUPi!?p814pROPA)EeKYNA_q3#`B zABBz{zh%d|OZBDwIqjqDwHvdj78_|QYE>k(4b$kE;r|Bq$ytbfet#_H z`E5-q+=COe8P`6`oTuDGJtxQ-cu#ndbJ@D{%woPCtj?E7DZtID|G5g zpPZYX=qJb1N7-$UHK-&dC+ijT_82?&?648c9(24klNQs@rFzy}*ZtuW@l9G@OtKfn zOlRm+PI-M1=&`HO2u97_kWWfp*_ohV`tOae_H;!XB^Voc#@v0C89RHuVSAHHIr~A? z>I-#`f=rs}vJZI`Go;)-`TpK+;Y2C9O13+^@91_9EsLAUldy0ewsD&n;jehg?s853 z7|~npvVJfV{Jn|JscC0XmTB{chGvN&BRryKf{s5Mw>RmZ71WB9{rBE2%d}jta@wm* zCpcnJK=jD?^{oSjqp{|LebUnJTecT!eb#bi3u{`TIbrnFs5{HAreGWA8lpkxO43&EO6kEmsSVCd)cMr4oKv`GP2a{^dLHkuJ6m4cPHrt_c{nwjq;g_hc4VQQ z+&t=sto0l_`sj}{KfShpb$x2(+_&cb>>j&?cFV>~m#i%cqi1BPxFB~o$on9^py?$p*ODfgL(2r-iW^y;~T>l*b*RY9#Sx2&mVpDl0B@8hyP zqZXX#x^u5PBk%jrI`5{H*lS{`4W!gf7s4*}?YSGrp2>cC`9@7*yc}Yh6^QMzJwKi z8OLtc+|rLvuQVo~nEsBd|K7aw zq7KUgv+2c+f`h`FORp6*|KxI9QGf6t1H*Nz?{7r8OXZ6?Q;*p{w_}}>TUWc2k29fm zSyPrNqlHXhxLsj#e^7pz@QaXzM=|}1|8Z&Dl{d{24fbwP*VDF7ST4NQJEfv;?O_JL z2L_w&JMMlF>wMZ_BAI3^b^H}IgP&8)_qOA0U7C~M#bN|#bjy=1O|zZS?O%i$J3lh( zT+gld&+0yN%T_m9^sGYtW$npV)ZxdEBwx>KC?tFD;R!XCb-CNfD+fq)+b-DW-;KlO z^N!XxF~O}HW;{Qst}U3|I9S{yM2U$z{;9r%hZr8Fk3eMyjP`qu!&l-6$^)F z`9BYMcVS>KHR~c(?9Tisn;yINq4vq|>uSG}3$4H1HJuedkYrN!>=kQ6t(nA}&4ce@ zUJ`Fa)12$M)YG+No}XM1)s(le`-lU7#>(yxEwQz+FG3diKV_L}I~?Qdlh_#Z<*gy6 z;)Z*~Sl@p0wiAzfZ`s+uCHus^gnw)Qd2Q7RocIF?b3<7%n)@@Hw|lj`91;2C zAHKpw>VRr>a#d;dL;EXK=FW(0+|2*3`G>m;uIBdSReitjSeeE9M97w%>h5qY$1W~? zsn^M+w9^5!PS%W%lpcJ!J$tu|?^_4jX2>lJQQ~t@|1>?tWr98mcBGHOy!l z>KHY5PEf0u^fiB-SG%KlhCF9{{cA>!9lzzIm2KW5`XyHAH9oFlepHy?+K#}``w{ij z4J@^0O)gvG)_8Sq?-W$`2u+oL7eE)6XZ&-vO|?12>gEQLh zRRq84i^)a$2>YuI*GjkL-@egYTvSB#0Eqn>47|}cISxQ0^iidgeBntTjX@r4G~TMa z104ZXCV33E0(g`=kYx6v?y6&csdu=8b3AUnO~kde8U>MGN1Kl&_E6dAKMbcl#95gp zZ0pYYSbN2`d+)a89{#fOW%Joj=@0Kc`8hg0(xYuqK5VH=>(WG@GFLIzz{J4q$%8oV z@G0_CxF|)*htP;~G?w45rYIDY735d9K7V>s)(vfqrYj@a5;I-?2Tf}40B*b=j^4o0 z)Wd|lbvPHs3d6aJZam5{(qIrj60_r>Ku?Fj0mzS|ZB0qa9D)@@NoV;pQ}kW(0ZzT^ z6XTuSFUC?=89NqUH6JP+_lq@2s;}zbgi|J+DyBADvbVt&G565FOET$ZXK3lR91blr z?a9&W8jEXQMJ!icOR^oAMo2?p)ZGq8W#~sRe$&G!Hkvnc5 zm~QlOOxWhJ{M0q^7O$SQgVYb|!*F1A>JL&#zo-0iX@zgub=Ta22deEq*w6oOdpm^~1la{Q()TTgpDaDp{$_K0I+mRHtS`L+Qy#TVR)&S7EgJx?p2h?ly3kZ^1d zJJQo)h?+$X_0-cF^$a`uxD?Yv}3^Ch!+uBmTKn3H>>o73i6mzAlxbarB~AzSrN z@ap80!XKj#mL|}uASDUP5x8Z1SeBX!zdH ze;vn`qJeh5p5eg^zg^vGXQ&)q7~ z*H2LT#u+?kw4GU*tnK>9snzYKZ^Bx)w-z09Ey{0J+P&kRU#9CBBVXp5{bOdm&ybIv z_|ppJO6RLi8&~e=Rb&v!d=%TSkZ?fZL`a}P{PT(|jt94;Ha)wcqzU9>exE2~l6dsZ z(U0PZ?KVYn!U-nUe75Xs0!+RhJm`087-|J7+oFLGVpC91IBD(HSILQi-*H`C-KQTP zNnR0xo5lwWieB^X*|Q4I*`K!RgG#=rc-iZrQ^rEP@$A!eX2*(F#X7cs%E=NjU+*{_ zQ2y7g`q;Qr=R;>rot?{*`x&3iAdfy7{BOdmpIXl5-EEO~kAtE>f4u{DVS}Bw)R~1eP6VJgV>ewC}_~GCb?hn;=kd#K6{#tBomq! zNixCab2!r|zpMm@sgfm_p(HL~6yS}%YyiTTdddozvG#NK1h)q%zYlbtRk4`K_ci08kDg2RD{_SIy)yJk>fWD!Z!xUaoU(C z?|t@CrdYP`?r0I;Xp!m4bcU)2dJQI{3-XCGd&W8Tb1CwG&qZjZ5Q}|*!4_Ih>e?nB zn&~a-ycE9LSh}w^qBj-V)w|PZr5U|hgvV@-C$oO3fbf^>HR^}xj!fhmV5aXwT8gaZ zX&yj=kGn5Uxr2n#Q2S!9K1Y~VTVQ*SQFZKFeyPwyvn-Ehu0ED$w&IU@(wK*ZG=pGH zbkx9lMUjzZ z{5bx&@(RDfpba!PX*i$IPb&*2oRmk2R(3ADFkWEo*GG0WWfxQ}R+@y@8D=~0v7F+b zotEK&jATE%o8@)njAFkYv%P**-b+&`p-CLJY4x=s>foGZ8dUATBn-lJ;o6?9>$-$ zV*OaYBDgtHJ2lLx`j{DOkkam78IkyRj8qcAalwc|=~5_gTEqrcZxc+nE0c{Bj>Dih zNVUD9`*~E9UtplHv$?hP0nnc_Rz|sGge`YJ?QIuvxM#gT~WsV#g2Q& zQQ8co66PSOh#Us>D;R?pv~CTa4iPn1G=pENyxI&4cyeOn!1DomJiXFY+jT-2SdJiz zY~G{Xbq^FCy@+ZMD$t}nh(PDIPH?@;F;lj1+5S&y5B)Dic-^KIUCbWq4O@Kv_*3*f zoolBP4#zIEX+_!UGy4rX*a>DYuvzvhDOY!MobxnCtsC?NWR@{t>6sb`vjBb$m9Mv8 zE)PA!n_wABEzVR3X19XgMOaR_@F!$0m2!547SEe=SM>TtZm8*M+fR5Fl{mKLj z4L9|)aH^_gB*Z)S?(Dw&SD{q=r%z3i&8a-oU0E*2@=)4FajMS!6c#P040o#d`}U1_ z?w27g^JZz^DRE&L+(%MTFQ2Rg-a{11F@9#ob@6vYLs=D^C$x~@OK$Gn)v%dWb;NjZPl_K{rG`0@Bws= zYZ2LIXKHW|LWKLTHjw!?D=RBPIwX7-kU87FX%=yul{ug7EQoHK01modaC!GGUDmq; ztLp_SG|x(1$;*nj-q$Cwd2{Qt0ndkFrEg+0f_2pT4+R9dya*H8l~>q39Hcrlxh830 zBtYuQbU3Ic%m}E>j$eFfLi6DPK{3Mo=q0^j8^6jSzF=)-9|d&xD+x#XO<_ z@Fcwk*hn-2V*C=!?#z3640;mCexl3SuS!8{uCb*N@1+*6p03QP7=e=>Q54zPMJ5B}? z5F8%LCG`3%vL^y-E(lnMucoTv-O%)QZon?6LZ`q(zKzXRIchd0#$i}YwW3N)oFV$< z7$R`!mG_qGF7FeIAl40$Cvo56*5Xjn*SUfFiDAWISeU%tK*rZcaG*2p}MT zABAO_>(3wUXf1M&Ps@A@N#65@T{oU9ZoWt&=-bU#qsj^|uZTAtLW_;JX5BztLIoP9 zvfzf3HX-O?PjNbI5wW}qsWGmpW@LGU5$utzPx`+FYo$&BQv^kZte*q1M5O?{U>#My zE!Xdd12cS^{mj19RnEtazxxh;vsZqnC0o-dE_?35P1*kFuPYy)e{fFTebwYVHOH&6 z=;y5o#u>7rk#uNYX&tZZ z`J@X(VzL%~4kBbW#Q=2%@WfxpLOPXyanqZ~&cq&TO$k zQ}XMDQ6`32YwY|ZYtw9QAsBBbH#c=cCQ^T#*0b+Z6G4_R!HH}8^;v=36Ra=%= z+Pqel19f%=lZfw_J7TaWlfs4pGJL^7i1$XGn_ZNG)Ef$scQH_6*r*~Tyt!EVt9LHP znIS|)xh$V+A$fcF-Mt%ym%Dhr^0yRDP!tyga@o#KQ1YUO)4CdV|0n&f8PU5HPwzg$ z%hO|+X1wIr6p#mrviAq zEBNNIE(ttY$h_%%mlZrIE@(UV?*QZmg4C3^nfX#mSL>y&rcz`iPiQKag&VQR--L5p zFpb8p_Ld_(Cpa+95HnIun~<@YaG4ZXm&@knw%&eGpS{01HRjc}4H!@G4$~iRJ}=F` z>&`55!Gk2r0lsmuT>icv?mb46bfFIOLt4BxCni%^xo8wX~ zD)$^}%^wrqDq5ZuZ_L;?#`9%JF-}Sswv&X0x(|R~)sNKzi(p#6oX058 z1v4BHEdVJ2o4g#TdNmvkSA^oW)(Z%@d(RGRmzWB>RZtT$UPO=soz9izN1PlPEj zo&xr<1c(K~D_psHbtG~=qAV5IwWST=pG@vYsaP5;MQQcx^1YW!(2sQbHdH_QS!)uO z=@=Z76W^p>eOJ(xbJ|^)d2x&69_B-Kk1S+c#HMmbn1ER7F0HBgk)8YQb4}%i*E2qE zP;&{r_!Y+dOztN#ZuIrOcCxI*Jf~7`rSzdyLFLCfj;wr1GaoC9=*7DyVV@^fx+VhR;p8{d!#N6IPe)9e(PVWRUv@0+*M0rRfO2LVmob*x`Sg` zH*T!dW$&N&b|YDkxE4sWrfM_utEyxlX_LnuRlSV!kb&SS*c7W`~g~YpEJ9+Pc_~%`sbPVf$xhoUtwVh0K#^?G9*$TDL$d|a;N|_viF<-^=)=I{|UtCqV zZ`S!>{WBCfFFkp=tR?4Sj=j=&7d!BiCv2AB!H8j`P;kY(&;MkdqvUDE4+OhahjO11 zy&PA8uZ73AS#6ygnufBC8D%9`Ns%^uI99&zs7Aim~^%6Lg*8L-5#e?f08;JMj+V|24Ov8$!Mmovma0 zQ|B5pLKf<_;1lUP{Jv({erv1i<QN1cVmWEhW!Zc6ZUNv9)rPtCS&kIP#m8mVE03qf67qg%Fd$ zB5Bj(v}FE``Ja!nU^ZuDm=6a#9_yCC-&dUUYG7Ymzp~}HCBJC5a`jYcvYi2!q1>Ne zdVE{lmc;~5WkF}OlR{Shjh!u*A&Qv0U*gml{XZE@gos$_`^{?RxCEoXXV|m}u48LS z4u-bqe1ojd*+lO3=l{NBa%OG!R8&QQ7}S7}yMR~G~fQ%Ol8vZa$>j(I<2e zO;9*Q5lF7v-JOoaH1yw5a;43SY-VGzV?IWYiq5SE~NH$LY?8gH-kM*1xQx zP&}PKwH6D5>A~$+RF+D>VMy##f3tP#)~lfE^TRF%hq@C$8mmkr?%%(P8U|i@DuYM> zh?3gV5r0S#sH=w%EJM0<$I>R^J|~DeR%btDZI(s+a}IS{$rncpE|G1V4`{82M#j~Mh=^{cjT=?|{`l*a1>)H) zXGJz8iX?-o1k`>)(n|a)lBCOM6e;-%2zg#N6PTLt;#S-EL7) z7AYyIZfbjJt)`RcBdvqBMcvyG;IbgI%q1d{mN$fU;QN}I0Pq^BAnZbfa^aS}0Yb5n11uQQ{q=&e5QdxeK9;Hh}kFV~OR+AYKyaMty z0Qg-SG|z};F0ka&b-4>ND0e7t+`jz`7Y=R!B?Ybtrg3@Dq#}G5a+&ws{nhJirD&Dre7LTz?Yqpn^J{q@ds%RJMMU_V z>?Pd+zdwPakg;|%^#bKkM1_;ky!iA>w)b-v5hv)Pen%aiovIaxHzT4oKbG4F&IfrQ zo@puKlptSB8s|gtO`EHfFJ@ca_0{C6|-@%{Zor(U13%8qk zfg>hxWK%G=vLAtK!;JcDa}eLM(HL5bvoPI&-NYa@9%GN zn;nAC!#KcZicr0|$1Zs**InCGLE`&zpmjUk7iT#Dnd}7ZhX`fQp}9Z(^Jie6XpxlB zuRX13|G?iL<=Zw$i2+8{AV$d93=FeDkLgOZuiJ4@QBSK}(;hjN-FWibK6aPUBM@|& z0nAVJn=7Gd5WNpn0#ng3#Gxv9nr=Fbv#s z=7QOa7VFNi@`mxNR`;&v@iVGUPEHPn2@OO?UjQ(PPXs8Y26|0eU2YigV0S;j*>U!* zv2w`4%kS`M{(=ZL1p={wp<#FIyDQGlW0194=uC_1`m-ds@7<8yl(zY+;eS=pf2E0>Kh2aQd=JAoNoFG1(9>f}8+Nef> z@jP(3YWvA$ZWzC@gS&!0vp2k*aD+Sol8f+saM$(-Ckl}CpL}V?-k1bvrO_y@*4P;c zU1-LJcGp123`j+=n*EeB(IS3YbhZgD33#`h9ikP)DZ>MTFcbq{qKQFtIviTj+`9mn zBswW)u#^VHEx46`??&nP>!HlH5i94J4|l8I-$Ens^UMq_WhX(|qZ58VJlyQ90z@>?m{Bpy&d#P?v!)yFG;`=dH`&!`)j3ru zhWy#YbBqi_?ZJXGvHdi+7EON=!XyEULlh|=uSH~0f`u*EAsrSMC6A#cdQ}e%XM&9$ zbAo;Ylq4}$f~0x{R?Md-N3QKPb6cEufLUh|w9(X{1Qaf2M-=aV!1_V@w3ghTjK|NN zCpBcDelE-Sr?9JTc^g-3e>x_gWdvvm&XhbL!7~-^3kA@Ne2<2n>19S%RujQA4K$a4 z02XHzHt#}5uM1p3BTN#YXSNzJ5Be$nJ9h*vROImRz{i3#@>#&U_4>f&) zXQYV632u8?CcvgLmx%IQhPx$K1m3x}CPFPi_>a)Wdv8);)BQ^LWG`Ed`>hdj z-^?frOWBxP3-N`9J?&K*clO)Dn?iq>s2fjypF+~_@uR68ushZTwBE(6DJ`jT9+}SA zTMiLRU6$j&jw7xpnd-QP!&rnjeSK5ag)#aKx zx`JY?0#-{Kn{C^-zkeO(=SRToj>GA_0Bp((4|aDOzD-!#W+<`-I?QRHR8<0(-6eo) zN;vG{lw*{hA|oOqfq)WoHNw)vFD%S-g2*@EYxWsk#uAp5xV@mYy8CtWv9*$T>*Wa4 zm@Ts1*|gsi8wWC!y+@~l`Sq5;YHCma{iV1f9qARkx`5x5O>EvrGAsY$2}m4iZ9Z zD)GPRoN1lgy_pHe`qPWoBj?icY!R$;eoW zKO!kjVTT1ZW}hz4UcO?*3Wg0El)g>+=G$Yii``t)Y0Cia(}=Erp0 zW_BPmtOyFb<}~mY52R%G(Erp*#;vxsw*J|e-&d?2B^O}2(7oLn6W@Tb(-C^y7343a z+J|yu!Wx-LH322eqp3A<`ms)D!A0R>a%Q zhT4%?mH*|UUEQK~la9gC*L!+WqAz{v)BK5?H|i;WSD3$E?bn)JgOMb3S;u(=&!yGr7LwdSmOlt^??nZ z1yWBB0OQ*Q1d`(vWVYmC+S=EUybDXXz~I1s3}j+4?B>5%y6G3wG!n_jx7OU&7m?yFGYkY zx{b7qYvWje7C!6INKWe(2<4J22*_Sv9N-u(b&hqH1PYN)h{EXN_m0BfT9~)MrYn9e zDJmLFw7M7=h(=W~YDvstc#ya_MMp#^xkSyoUYQ=%L-N%BC7KbUrxzgnN)%AZ(P-%W zSaG4DWNIix&;;NJK9UfEszvm5$sgDq#r`<0y``AVp-FEddkY7L0$|ssY=IAV`N+~@ zU>Nrqw`*ByxRiSf_~3$|DGp^6(fLl+`-CvIL7jqjOxbLRKDyV4?d?^U5LFvVBth_Pj1@u zy?A(4Fz&^xj?0ZHufBt^7Stz-#|vQlT?l5xo*%rvnnfSJTAyDxC0#gi0Vi!> zH+p#&AZ;GC{zbj8a%I%}q1N08P$bSGfU$#If=r#V{@=>ID&?QbU7Z5A;Uo_BXb6`^ z$>Ta?PoKVs^wsbRS4ZV9j+JLkZw<5?5xdk~wdd#OAqo)?B_G7Ah1;f&A$g!sYK!w8 z1c^W)EQM{D$0K(Q_}m~Xqx`iM;Fb8dV#m$lt9Ij|*Xy~Q8Y6Y-Z{h20xzA!sM%Fdq z)nU@>g{hGZgc~hEUcs}#NmS6<+G-quEe1_Rio-{bde(5@Jx6R_zyMt}TxqlavQ>2{8VjPy4JAgZPz?zp$4 zrKJTYT(J+sUrspC9`b-2My6#*%D|T=5+6{i)_5xlooG-pC@@9uVl@%&}NyaxPkNNAvRg>(2$Yrh32qJeBm2pwb3vUzs?0 z!@RJo3&Ke%Lm)z#Zg^TRzN1euhF3eQ2wZFAkx{=?N?(J3Q8I~`jpu#y&Yk63Q9qE; z!2Eh`Bz=fI#0b=oJy#^2%%!lNC!FN*wDAa4BIh#6>drK+czH3~H@ z-!@rM`|QhKg=6nl@on0p6336#WQy_4SE4_C79^%l2rgike1P=AT`+_Qb?gH)gYV#_ zw;ec8_1gX7#pj|TJ| zG>Bi1e;Pqm;5d%3H4Y9A&#-lm6He#;^2d@UKZY9Xz`xs%S1>@a&g zli_S{u9TqN3=>FwweY`{bjh&=&&g-N@jx&=ltiBk5L5W);KKQ!3um8J>}>&v0GgIY zNacPh*VuAWk2VAA0D8)Iu-iAIiu!JHZSo+Claf>(FO&~W`a@}(n7`&oWF=m_?AZwy~?Ll6y)^o?)@8FO#v`g zjlaAkc|lpIFp=L59D@2k{!YGwH^$Lw?C}xZ-={jcZou=c!$?kiB+Eac47499Y<%y8 z1U>{Oi6526!o=+8u41WFMxWb70aesHP?LvlK9w$Bwh6M{kilRCAOafXpYSjex+9L` zPUtOFDoK8zEzQl(f1jG_#h|$xb^?gk5rqAQvY>Nhqyqn<(YFA~BRgrnB9Xd?ET7P> zF(*s{WR3#;-^lhQK$(+>8Fs7?n$CplYTB9; zj@P&8Dq=Xfo*k3y8#^d1v#Z?<`Iq z;w$~}=bqe6oOch0uUrAY9ROnpCk!|f&!m)E4qgy|3kDq z0v!;%3TdCaXX2sU*rrHI{P)KnynbB8HV3c}UJIF5pWuLO`G^Z6#yx7?T7bQCLu zpifDM6?26*rfccw2#dEgMDRACLcEs`nw(f)MHDRNDBY|O*`Yi24oWI|Hsb9|KE@1EmHNCyfDBmm O*IzQHrQ?t4dj21|Q7}pX literal 0 HcmV?d00001 diff --git a/docs/utils/images/multi_cls_selfsl_performance_Food-101.png b/docs/utils/images/multi_cls_selfsl_performance_Food-101.png new file mode 100644 index 0000000000000000000000000000000000000000..e16276064cfe1a573520ca19d2382e0f23834cff GIT binary patch literal 56273 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 None:\n", - " super().__init__()\n", - " self.num_classes = num_classes\n", - " net = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)\n", - " net.fc = nn.Linear(\n", - " in_features=net.fc.in_features, out_features=self.num_classes\n", - " )\n", - " self.net = net\n", - " self.softmax = nn.Softmax(dim=-1)\n", - " self.criterion = nn.CrossEntropyLoss()\n", - "\n", - " def forward(self, images: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:\n", - " logits = self.net(images)\n", - "\n", - " if self.training:\n", - " return self.criterion(logits, labels)\n", - "\n", - " return self.softmax(logits)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The next thing is that we need to develop the one derived from `OTXModel`.\n", - "However, in this example, we want to add the multi class classification model.\n", - "We should implement the class derived from `OTXClassificationModel`.\n", - "For another OTX task, such as `OTXTaskType.DETECTION`, we might be able to make a custom model by deriving from `OTXDetectionModel`.\n", - "\n", - "Since every `OTXModel` is an abstract class, it is designed to require a developer to implement three abstract functions:\n", - "\n", - "1) `_create_model()`\n", - "2) `_customize_inputs()`\n", - "3) `_customize_outputs()`\n", - "\n", - "You can see that the following example is exactly implementing those three functions.\n", - "Let's see together." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "class OTXResNet50(OTXMulticlassClsModel):\n", - " def __init__(self, num_classes: int) -> None:\n", - " super().__init__(num_classes=num_classes)\n", - " self.register_buffer(\n", - " \"mean\",\n", - " torch.FloatTensor([123.675, 116.28, 103.53]).view(-1, 1, 1),\n", - " False,\n", - " )\n", - " self.register_buffer(\n", - " \"std\",\n", - " torch.FloatTensor([58.395, 57.12, 57.375]).view(-1, 1, 1),\n", - " False,\n", - " )\n", - "\n", - " def _create_model(self) -> nn.Module:\n", - " # ResNet50_Weights.IMAGENET1K_V2 is a really powerful pretrained model equipped with the modern training scheme:\n", - " # ImageNet-1K acc@1: 80.858, acc@5\": 95.434.\n", - " return ResNet50WithLossComputation(num_classes=self.num_classes)\n", - "\n", - " def _customize_inputs(self, inputs: MulticlassClsBatchDataEntity) -> dict[str, Any]:\n", - " images = inputs.images.to(dtype=torch.float32)\n", - " images = (images - self.mean) / self.std\n", - " return {\n", - " \"images\": images,\n", - " \"labels\": torch.cat(inputs.labels, dim=0),\n", - " }\n", - "\n", - " def _customize_outputs(\n", - " self, outputs: Any, inputs: MulticlassClsBatchDataEntity\n", - " ) -> MulticlassClsBatchPredEntity | OTXBatchLossEntity:\n", - " if self.training:\n", - " return {\"loss\": outputs}\n", - "\n", - " # To list, batch-wise\n", - " scores = torch.unbind(outputs, 0)\n", - "\n", - " return MulticlassClsBatchPredEntity(\n", - " batch_size=inputs.batch_size,\n", - " images=inputs.images,\n", - " imgs_info=inputs.imgs_info,\n", - " scores=scores,\n", - " labels=inputs.labels,\n", - " )\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we have our own custom model which can be used for the OTX training process.\n", - "However, there are many things and configurations for the model training.\n", - "**Setting these things from scratch is so sick.**\n", - "Therefore, we will **borrow the configurations from the similar model for the multi class classification task OTX provided: `classification/otx_efficientnet_b0`.**\n", - "We just override our custom model on top of that.\n", - "\n", - "It means that we can compose **1) Data transform from MMPretrain and 2) Custom model from TorchVision**.\n", - "As you know, our design let them independent each other.\n", - "Therefore, any composition, not just this example, such as 1) Data transform from TorchVision and 2) Custom model from Detectron, is possible.\n", - "\n", - "Please see the following how we do that." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING:root:The corresponding keys in config are not used.: ['verbose', 'data_root', 'task', 'seed', 'callback_monitor', 'resume', 'disable_infer_num_classes']\n", - "WARNING:root:Set Default Optimizer: {'class_path': 'torch.optim.SGD', 'init_args': {'lr': 0.0049, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0.0001, 'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False}}\n", - "WARNING:root:Set Default Scheduler: {'class_path': 'lightning.pytorch.cli.ReduceLROnPlateau', 'init_args': {'monitor': 'train/loss', 'mode': 'min', 'factor': 0.5, 'patience': 1, 'threshold': 0.0001, 'threshold_mode': 'rel', 'cooldown': 0, 'min_lr': 0, 'eps': 1e-08, 'verbose': False}}\n", - "INFO:pytorch_lightning.utilities.rank_zero:GPU available: True (cuda), used: True\n", - "INFO:pytorch_lightning.utilities.rank_zero:TPU available: False, using: 0 TPU cores\n", - "INFO:pytorch_lightning.utilities.rank_zero:IPU available: False, using: 0 IPUs\n", - "INFO:pytorch_lightning.utilities.rank_zero:HPU available: False, using: 0 HPUs\n", - "INFO:pytorch_lightning.utilities.rank_zero:You are using a CUDA device ('NVIDIA GeForce RTX 3090') that has Tensor Cores. To properly utilize them, you should set `torch.set_float32_matmul_precision('medium' | 'high')` which will trade-off precision for performance. For more details, read https://pytorch.org/docs/stable/generated/torch.set_float32_matmul_precision.html#torch.set_float32_matmul_precision\n", - "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1]\n", - "\n", - " | Name | Type | Params\n", - "---------------------------------------------------\n", - "0 | model | OTXResNet50 | 23.5 M\n", - "1 | val_metric | MulticlassAccuracy | 0 \n", - "2 | test_metric | MulticlassAccuracy | 0 \n", - "---------------------------------------------------\n", - "23.5 M Trainable params\n", - "0 Non-trainable params\n", - "23.5 M Total params\n", - "94.049 Total estimated model params size (MB)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " \r" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/harimkan/workspace/repo/otx-regression/venv/lib/python3.10/site-packages/lightning/pytorch/loops/fit_loop.py:293: The number of training batches (1) is smaller than the logging interval Trainer(log_every_n_steps=50). Set a lower value for log_every_n_steps if you want to see logs for the training epoch.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 9: 100%|██████████| 1/1 [00:00<00:00, 8.83it/s, v_num=9, train/loss=0.563, val/accuracy=1.000]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:pytorch_lightning.utilities.rank_zero:`Trainer.fit` stopped: `max_epochs=10` reached.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 9: 100%|██████████| 1/1 [00:00<00:00, 2.92it/s, v_num=9, train/loss=0.563, val/accuracy=1.000]\n" - ] - }, - { - "data": { - "text/plain": [ - "{'train/loss': tensor(0.5628), 'val/accuracy': tensor(1.)}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from otx.engine import Engine\n", - "\n", - "data_dir = \"../tests/assets/classification_dataset\"\n", - "otx_model = OTXResNet50(num_classes=2)\n", - "\n", - "engine = Engine(\n", - " data_root=data_dir,\n", - " model=otx_model,\n", - " device=\"gpu\",\n", - " work_dir=\"otx-workspace\",\n", - ")\n", - "\n", - "engine.train(max_epochs=10)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Saying again. This is not the end image of the OTX training API. We will continue to strive to improve it so that users can use it conveniently. And, I believe that it is not difficult since we already have a solid core design and it is just an entrypoint.**" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "otx-v2", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/for_developers/cli_guide.md b/for_developers/cli_guide.md deleted file mode 100644 index a389fea9ebf..00000000000 --- a/for_developers/cli_guide.md +++ /dev/null @@ -1,350 +0,0 @@ -# How to use OTX CLI - -## Installation - -Please see [setup_guide.md](setup_guide.md). - -## otx help - -```console -otx --help -``` - -```powershell -╭─ Arguments ─────────────────────────────────────────────────────────────────────────────────────╮ -│ Usage: otx [-h] [-v] {install,train,test,predict,export} ... │ -│ │ -│ │ -│ OpenVINO Training-Extension command line tool │ -│ │ -│ │ -│ Options: │ -│ -h, --help Show this help message and exit. │ -│ -v, --version Display OTX version number. │ -│ │ -│ Subcommands: │ -│ For more details of each subcommand, add it as an argument followed by --help. │ -│ │ -│ │ -│ Available subcommands: │ -│ install Install OTX requirements. │ -│ train Trains the model using the provided LightningModule and OTXDataModule. │ -│ test Run the testing phase of the engine. │ -│ predict Run predictions using the specified model and data. │ -│ export Export the trained model to OpenVINO Intermediate Representation (IR) o │ -│ ONNX formats. │ -│ │ -╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ -``` - -The subcommand can get help output in the following way. -For basic subcommand help, the Verbosity Level is 0. In this case, the CLI provides a Quick-Guide in markdown. - -```console -# otx {subcommand} --help -otx train --help -``` - -```powershell -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ OpenVINO™ Training Extensions CLI Guide ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -Github Repository: -https://github.com/openvinotoolkit/training_extensions. - -A better guide is provided by the documentation. -╭─ Quick-Start ─────────────────────────────────────────────────────────╮ -│ │ -│ 1 you can train with data_root only. then OTX will provide default │ -│ model. │ -│ │ -│ │ -│ otx train --data_root │ -│ │ -│ │ -│ 2 you can pick a model or datamodule as Config file or Class. │ -│ │ -│ │ -│ otx train │ -│ --data_root │ -│ --model --data │ -│ │ -│ │ -│ 3 Of course, you can override the various values with commands. │ -│ │ -│ │ -│ otx train │ -│ --data_root │ -│ --max_epochs --checkpoint │ -│ │ -│ │ -│ 4 If you have a complete configuration file, run it like this. │ -│ │ -│ │ -│ otx train --data_root --config │ -│ │ -│ │ -│ To get more overridable argument information, run the command below. │ -│ │ -│ │ -│ # Verbosity Level 1 │ -│ otx train [optional_arguments] -h -v │ -│ # Verbosity Level 2 │ -│ otx train [optional_arguments] -h -vv │ -│ │ -╰───────────────────────────────────────────────────────────────────────╯ -``` - -For Verbosity Level 1, it shows Quick-Guide & the essential arguments. - -```console -otx train --help -v -``` - -```powershell -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ OpenVINO™ Training Extensions CLI Guide ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -Github Repository: -https://github.com/openvinotoolkit/training_extensions. - -A better guide is provided by the documentation. -╭─ Quick-Start ─────────────────────────────────────────────────────────╮ -│ ... │ -╰───────────────────────────────────────────────────────────────────────╯ -╭─ Arguments ─────────────────────────────────────────────────────────────────────────────────────╮ -│ Usage: otx [options] train [-h] [-c CONFIG] [--print_config [=flags]] │ -│ [--data_root DATA_ROOT] [--task TASK] │ -│ [--engine CONFIG] │ -│ [--work_dir WORK_DIR] │ -│ [--engine.checkpoint CHECKPOINT] │ -│ [--engine.device {auto,gpu,cpu,tpu,ipu,hpu,mps}] │ -│ [--model.help CLASS_PATH_OR_NAME] │ -│ [--model CONFIG | CLASS_PATH_OR_NAME | .INIT_ARG_NAME VALUE] │ -│ [--data CONFIG] │ -│ [--optimizer CONFIG | CLASS_PATH_OR_NAME | .INIT_ARG_NAME VALUE] │ -│ [--scheduler CONFIG | CLASS_PATH_OR_NAME | .INIT_ARG_NAME VALUE] │ -│ │ -... -``` - -For Verbosity Level 2, it shows all available arguments. - -```console -otx train --help -vv -``` - -## otx {subcommand} --print_config - -Preview all configuration values that will be executed through that command line. - -```console -otx train --config --print_config -``` - -```yaml -data_root: tests/assets/car_tree_bug -callback_monitor: val/map_50 -engine: - task: DETECTION - work_dir: ./otx-workspace - device: auto -model: - class_path: otx.algo.detection.atss.ATSS - init_args: - num_classes: 1000 - variant: mobilenetv2 -optimizer: ... -scheduler: ... -data: - task: DETECTION - config: - data_format: coco_instances - train_subset: ... - val_subset: ... - test_subset: ... - mem_cache_size: 1GB - mem_cache_img_max_size: null - image_color_channel: RGB - include_polygons: false -max_epochs: 2 -deterministic: false -precision: 16 -callbacks: ... -logger: ... -``` - -Users can also pre-generate a config file with an example like the one below. - -```console -otx train --config --print_config > config.yaml -``` - -## otx {subcommand} - -Use Auto-Configuration - -```console -otx train --data_root --task -``` - -Use Configuration file - -```console -otx train --config --data_root -``` - -Override Parameters - -```console -otx train ... --model.num_classes --max_epochs -``` - -Testing with checkpoint - -```console -otx test ... --checkpoint -``` - -Export to OpenVINO IR model or ONNX (Default="OPENVINO") - -```console -otx export ... --checkpoint --export_format -``` - -Testing with Exported model output - -```console -otx test ... --checkpoint -``` - -## How to write OTX Configuration (recipe) - -### Configuration - -Example of `recipe/classification/multi_class_cls` - -```yaml -model: - class_path: otx.algo.classification.mobilenet_v3_large.MobileNetV3ForMulticlassCls - init_args: - num_classes: 1000 - light: True - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0058 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: otx.algo.schedulers.WarmupReduceLROnPlateau - init_args: - warmup_steps: 10 - mode: max - factor: 0.5 - patience: 1 - monitor: val/accuracy - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy -data: ../../_base_/data/mmpretrain_base.yaml -``` - -We can use the `~.yaml` with the above values configured. - -- `engine` -- `model`, `optimizer`, `scheduler` -- `data` -- `callback_monitor` - The basic configuration is the same as the configuration configuration format for jsonargparse. - [Jsonargparse Documentation](https://jsonargparse.readthedocs.io/en/v4.27.4/#configuration-files) - -### Configuration overrides - -Here we provide a feature called `overrides`. - -```yaml -... - -overrides: - data: - config: - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - direction: horizontal - prob: 0.5 - type: RandomFlip - - type: PackInputs - ... -``` - -This feature allows you to override the values need from the default configuration. -You can see the final configuration with the command below. - -```console -otx train --config --print_config -``` - -### Callbacks & Logger overrides - -`callbacks` and `logger` can currently be provided as a list of different callbacks and loggers. The way to override this is as follows. - -For example, if you want to change the patience of EarlyStopping, you can configure the overrides like this - -```yaml -... - -overrides: - ... - callbacks: - - class_path: ligthning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 -``` - -## How to use OTX Workspace - -If we run a typical Training example, we'll have a folder like the one below as output. - -```console -otx-workspace/ - .latest/ # Gather the most recent information. - train/ # Link to the output_dir where the most recent train was performed. - export/ # Link to the output_dir where the most recent export was performed. - .../ - 20240000_000000/ # Deliverables from OTX CLI - 20240000_000001/ # Deliverables from OTX CLI Second-Trial -``` - -OTX considers the folder with .latest to be the root of the entire Workspace. -`.latest` soft-links to the most recently trained output folder. - -Case 1: If a user specifies an output `work_dir` (An already existing workspace) - -```console -otx train --work_dir otx-workspace -``` - -This will then use the .latest in the otx-workspace for training. - -Case 2: if a user executes a command from within the otx-workspace - -```console -cd otx-workspace - -otx train # Behave in the same way as the first training -otx test # Perform a test with the config and checkpoint from the last training baseline. -otx export # Perform a export with the config and checkpoint from the last training baseline. -``` diff --git a/for_developers/contribution_guide.md b/for_developers/contribution_guide.md deleted file mode 100644 index 8e6c37d4dc7..00000000000 --- a/for_developers/contribution_guide.md +++ /dev/null @@ -1,65 +0,0 @@ -# Contribution Guide - -## Design Change Proposal Process - -### Who Needs It? - -If you believe your work on OTX 2.0 could have a global impact on the product or involve significant changes to its structure, follow the design change proposal process. -We don't want this process to be a gatekeeper hindering our development. -Instead, we hope that it will be a systematic and fair framework for everyone to get agreement on major changes. -Given OTX 2.0's early development phase, the decision to follow this process is at the individual's discretion. -Besides, we encourage a flexible document format according to the taste of each developer, but there is with only one requirement outlined in the next section. -Developers should carefully consider this purpose before contributing to OTX 2.0. - -### How to Proceed - -For Intel internal developers, we have the "OTX2.0" channel in Teams. -This channel includes a file repository for viewing and uploading files. If you lack access, please contact the owner (mark.byun@intel.com). -This channel serves as the public space for suggesting and reviewing design change proposals. - -### Proposing Your Design Change - -1. **Prepare your proposal and upload it to the channel** - - While we don't enforce a strict document format, there is one essential requirement: establish design requirements. - These serve as a standard for reviewers evaluating the proposal in the next step. - For instance, here are the design requirements for the tiling feature: - - |![design_req](images/contribution_guide/design_req.png) - |:-:| - | Example for the design requirement | - - **Ensure your proposal aligns with [our product missions](./product_design.md#our-product-mission).** - - After that, you can write your proposal. In your document, address how your proposal meets the design requirements you've established, as shown in the following example: - - | ![design_proposal](images/contribution_guide/design_proposal.png) | - | :---------------------------------------------------------------: | - | Example for the design proposal | - - We expect that this "set design requirement -> resolve design requirement" process can make our design change decision clear. - -2. **Request a review on the channel** - - Copy the document link from the Teams UI: - - | ![copy_link](images/contribution_guide/copy_link.png) | - | :---------------------------------------------------: | - | Copy link | - - Post a review request on the channel to ask feedback from others: - - | ![ask_review](images/contribution_guide/ask_review.png) | - | :-----------------------------------------------------: | - | Submit a post to the channel | - -### Reviewing Other Members' Proposals - -Follow these two steps in the reviewing process: - -1. **Review the design requirements**: Ensure alignment with [our product missions](./product_design.md#our-product-mission). -2. **Review the proposal content against the design requirements**: Thoroughly check that the proposed changes address the design requirements. - -## Docstrings - -We follow Google style docstrings. For the detail guide of this, please see [this link](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). diff --git a/for_developers/dir_structure.md b/for_developers/dir_structure.md deleted file mode 100644 index 18e5033e9ec..00000000000 --- a/for_developers/dir_structure.md +++ /dev/null @@ -1,36 +0,0 @@ -```console -root/ - algo/ # Custom algo (e.g., hierarchical_cls_head) - cli/ # CLI entrypoints - engine/ # OTX Engine with Entry Point - core/ - config/ # Structured data type object for configurations - data/ # Data related things - dataset/ # OTXDataset - base.py - detection.py - ... - entity/ # OTXDataEntity - base.py - detection.py - ... - transform_libs/ # To support transform libraries (e.g., MMCV) - factory.py # Factory to instantiate data related objects - module.py # OTXDataModule - model/ # Model related things - entity/ # OTXModel - base.py - detection.py - ... - module/ # OTXLitModule - base.py - detection.py - ... - types/ # Enum definitions (e.g. OTXTaskType) - utils/ # Utility functions - recipe/ # Recipe YAML config for each model we support - _base_/ # Default YAML config files - detection/ # (e.g., rtmdet_tiny) - ... - tools/ # Python runnable scripts for some TBD use cases -``` diff --git a/for_developers/engine_api_example.ipynb b/for_developers/engine_api_example.ipynb deleted file mode 100644 index e4bb524fe69..00000000000 --- a/for_developers/engine_api_example.ipynb +++ /dev/null @@ -1,541 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# How to use OTX Engine\n", - "\n", - "## Installation\n", - "\n", - "Please see [setup_guide.md](setup_guide.md).\n", - "\n", - "## Engine Usage" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/harimkan/workspace/repo/otx-regression/venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - } - ], - "source": [ - "from otx.engine import Engine" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Training with dataset path (Auto-Configuration)\n", - "\n", - "- Auto-Configurator detect task from data_root\n", - "- Auto-Configurator select default model, data-transform, optimizer, scheduler" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING:root:The corresponding keys in config are not used.: ['verbose', 'data_root', 'task', 'seed', 'callback_monitor', 'resume', 'disable_infer_num_classes']\n", - "WARNING:root:Set Default Model: {'class_path': 'otx.algo.classification.efficientnet_b0.EfficientNetB0ForMulticlassCls', 'init_args': {'num_classes': 2, 'light': False}}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "init weight - https://github.com/osmr/imgclsmob/releases/download/v0.0.364/efficientnet_b0-0752-0e386130.pth.zip\n", - "init weight - https://github.com/osmr/imgclsmob/releases/download/v0.0.364/efficientnet_b0-0752-0e386130.pth.zip\n", - "init weight - https://github.com/osmr/imgclsmob/releases/download/v0.0.364/efficientnet_b0-0752-0e386130.pth.zip\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING:root:Set Default Optimizer: {'class_path': 'torch.optim.SGD', 'init_args': {'lr': 0.0049, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0.0001, 'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False}}\n", - "WARNING:root:Set Default Scheduler: {'class_path': 'lightning.pytorch.cli.ReduceLROnPlateau', 'init_args': {'monitor': 'train/loss', 'mode': 'min', 'factor': 0.5, 'patience': 1, 'threshold': 0.0001, 'threshold_mode': 'rel', 'cooldown': 0, 'min_lr': 0, 'eps': 1e-08, 'verbose': False}}\n", - "INFO:pytorch_lightning.utilities.rank_zero:GPU available: True (cuda), used: True\n", - "INFO:pytorch_lightning.utilities.rank_zero:TPU available: False, using: 0 TPU cores\n", - "INFO:pytorch_lightning.utilities.rank_zero:IPU available: False, using: 0 IPUs\n", - "INFO:pytorch_lightning.utilities.rank_zero:HPU available: False, using: 0 HPUs\n", - "INFO:pytorch_lightning.utilities.rank_zero:You are using a CUDA device ('NVIDIA GeForce RTX 3090') that has Tensor Cores. To properly utilize them, you should set `torch.set_float32_matmul_precision('medium' | 'high')` which will trade-off precision for performance. For more details, read https://pytorch.org/docs/stable/generated/torch.set_float32_matmul_precision.html#torch.set_float32_matmul_precision\n", - "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1]\n", - "\n", - " | Name | Type | Params\n", - "---------------------------------------------------------------\n", - "0 | model | EfficientNetB0ForMulticlassCls | 5.6 M \n", - "1 | val_metric | MulticlassAccuracy | 0 \n", - "2 | test_metric | MulticlassAccuracy | 0 \n", - "---------------------------------------------------------------\n", - "5.6 M Trainable params\n", - "0 Non-trainable params\n", - "5.6 M Total params\n", - "22.599 Total estimated model params size (MB)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " \r" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/harimkan/workspace/repo/otx-regression/venv/lib/python3.10/site-packages/lightning/pytorch/loops/fit_loop.py:293: The number of training batches (1) is smaller than the logging interval Trainer(log_every_n_steps=50). Set a lower value for log_every_n_steps if you want to see logs for the training epoch.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 1: 100%|██████████| 1/1 [00:00<00:00, 9.97it/s, v_num=10, train/loss=0.692, val/accuracy=0.680]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:pytorch_lightning.utilities.rank_zero:`Trainer.fit` stopped: `max_epochs=2` reached.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 1: 100%|██████████| 1/1 [00:00<00:00, 5.66it/s, v_num=10, train/loss=0.692, val/accuracy=0.680]\n" - ] - }, - { - "data": { - "text/plain": [ - "{'train/loss': tensor(0.6917), 'val/accuracy': tensor(0.6800)}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data_root = \"../tests/assets/classification_dataset\"\n", - "\n", - "engine = Engine(data_root=data_root)\n", - "\n", - "engine.train(max_epochs=2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Training with Custom OTXModel\n", - "\n", - "Please see [add_custom_model.ipynb](add_custom_model.ipynb).\n", - "\n", - "```python\n", - "# Inherited Class from otx.core.model.entity.base.OTXModel\n", - "custom_model = CustomOTXModel(...)\n", - "\n", - "engine = Engine(data_root=data_root, model=custom_model)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Training with OTX Model (model_name: str)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['otx_efficientnet_v2',\n", - " 'openvino_model',\n", - " 'otx_dino_v2_linear_probe',\n", - " 'mobilenet_v3_large_light',\n", - " 'efficientnet_b0_light',\n", - " 'otx_mobilenet_v3_large',\n", - " 'efficientnet_v2_light',\n", - " 'otx_dino_v2',\n", - " 'otx_efficientnet_b0',\n", - " 'otx_deit_tiny']" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from otx.engine.utils.api import list_models\n", - "\n", - "list_models(task=\"MULTI_CLASS_CLS\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING:root:The corresponding keys in config are not used.: ['verbose', 'data_root', 'task', 'seed', 'callback_monitor', 'resume', 'disable_infer_num_classes']\n", - "WARNING:root:Set Default Model: {'class_path': 'otx.algo.classification.mobilenet_v3_large.MobileNetV3ForMulticlassCls', 'init_args': {'num_classes': 2, 'light': True}}\n", - "WARNING:root:Set Default Optimizer: {'class_path': 'torch.optim.SGD', 'init_args': {'lr': 0.0058, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0.0001, 'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False}}\n", - "WARNING:root:Set Default Scheduler: {'class_path': 'otx.algo.schedulers.WarmupReduceLROnPlateau', 'init_args': {'warmup_steps': 10, 'monitor': 'val/accuracy', 'mode': 'max', 'factor': 0.5, 'patience': 1, 'threshold': 0.0001, 'threshold_mode': 'rel', 'cooldown': 0, 'min_lr': 0, 'eps': 1e-08, 'verbose': False}}\n", - "INFO:pytorch_lightning.utilities.rank_zero:GPU available: True (cuda), used: True\n", - "INFO:pytorch_lightning.utilities.rank_zero:TPU available: False, using: 0 TPU cores\n", - "INFO:pytorch_lightning.utilities.rank_zero:IPU available: False, using: 0 IPUs\n", - "INFO:pytorch_lightning.utilities.rank_zero:HPU available: False, using: 0 HPUs\n", - "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1]\n", - "\n", - " | Name | Type | Params\n", - "------------------------------------------------------------\n", - "0 | model | MobileNetV3ForMulticlassCls | 3.0 M \n", - "1 | val_metric | MulticlassAccuracy | 0 \n", - "2 | test_metric | MulticlassAccuracy | 0 \n", - "------------------------------------------------------------\n", - "3.0 M Trainable params\n", - "0 Non-trainable params\n", - "3.0 M Total params\n", - "11.895 Total estimated model params size (MB)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loads checkpoint by http backend from path: https://github.com/d-li14/mobilenetv3.pytorch/blob/master/pretrained/mobilenetv3-large-1cd25616.pth?raw=true\n", - "The model and loaded state dict do not match exactly\n", - "\n", - "unexpected key in source state_dict: classifier.0.weight, classifier.0.bias, classifier.3.weight, classifier.3.bias\n", - "\n", - "init weight - https://github.com/d-li14/mobilenetv3.pytorch/blob/master/pretrained/mobilenetv3-large-1cd25616.pth?raw=true\n", - "Loads checkpoint by http backend from path: https://github.com/d-li14/mobilenetv3.pytorch/blob/master/pretrained/mobilenetv3-large-1cd25616.pth?raw=true\n", - "The model and loaded state dict do not match exactly\n", - "\n", - "unexpected key in source state_dict: classifier.0.weight, classifier.0.bias, classifier.3.weight, classifier.3.bias\n", - "\n", - "init weight - https://github.com/d-li14/mobilenetv3.pytorch/blob/master/pretrained/mobilenetv3-large-1cd25616.pth?raw=true\n", - "Loads checkpoint by http backend from path: https://github.com/d-li14/mobilenetv3.pytorch/blob/master/pretrained/mobilenetv3-large-1cd25616.pth?raw=true\n", - "The model and loaded state dict do not match exactly\n", - "\n", - "unexpected key in source state_dict: classifier.0.weight, classifier.0.bias, classifier.3.weight, classifier.3.bias\n", - "\n", - "init weight - https://github.com/d-li14/mobilenetv3.pytorch/blob/master/pretrained/mobilenetv3-large-1cd25616.pth?raw=true\n", - "Epoch 1: 100%|██████████| 1/1 [00:00<00:00, 12.68it/s, v_num=11, train/loss=0.809, val/accuracy=0.520]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:pytorch_lightning.utilities.rank_zero:`Trainer.fit` stopped: `max_epochs=2` reached.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 1: 100%|██████████| 1/1 [00:00<00:00, 8.37it/s, v_num=11, train/loss=0.809, val/accuracy=0.520]\n" - ] - }, - { - "data": { - "text/plain": [ - "{'train/loss': tensor(0.8091), 'val/accuracy': tensor(0.5200)}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "engine = Engine(data_root=data_root, model=\"mobilenet_v3_large_light\")\n", - "\n", - "engine.train(max_epochs=2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Training with OTX configuration file\n", - "- Users can override configuration values when creating an Engin.from_config.\n", - "- Or Users can also modify the configuration file directly." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING:root:The corresponding keys in config are not used.: ['verbose', 'data_root', 'task', 'seed', 'callback_monitor', 'resume', 'disable_infer_num_classes']\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "init weight - https://github.com/osmr/imgclsmob/releases/download/v0.0.364/efficientnet_b0-0752-0e386130.pth.zip\n", - "init weight - https://github.com/osmr/imgclsmob/releases/download/v0.0.364/efficientnet_b0-0752-0e386130.pth.zip\n", - "init weight - https://github.com/osmr/imgclsmob/releases/download/v0.0.364/efficientnet_b0-0752-0e386130.pth.zip\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:pytorch_lightning.utilities.rank_zero:GPU available: True (cuda), used: True\n", - "INFO:pytorch_lightning.utilities.rank_zero:TPU available: False, using: 0 TPU cores\n", - "INFO:pytorch_lightning.utilities.rank_zero:IPU available: False, using: 0 IPUs\n", - "INFO:pytorch_lightning.utilities.rank_zero:HPU available: False, using: 0 HPUs\n", - "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1]\n", - "\n", - " | Name | Type | Params\n", - "---------------------------------------------------------------\n", - "0 | model | EfficientNetB0ForMulticlassCls | 5.6 M \n", - "1 | val_metric | MulticlassAccuracy | 0 \n", - "2 | test_metric | MulticlassAccuracy | 0 \n", - "---------------------------------------------------------------\n", - "5.6 M Trainable params\n", - "0 Non-trainable params\n", - "5.6 M Total params\n", - "22.599 Total estimated model params size (MB)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 1: 100%|██████████| 1/1 [00:00<00:00, 9.87it/s, v_num=12, train/loss=0.697, val/accuracy=0.440]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:pytorch_lightning.utilities.rank_zero:`Trainer.fit` stopped: `max_epochs=2` reached.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 1: 100%|██████████| 1/1 [00:00<00:00, 5.55it/s, v_num=12, train/loss=0.697, val/accuracy=0.440]\n" - ] - }, - { - "data": { - "text/plain": [ - "{'train/loss': tensor(0.6972), 'val/accuracy': tensor(0.4400)}" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from otx.engine import Engine\n", - "\n", - "config = \"../src/otx/recipe/classification/multi_class_cls/otx_efficientnet_b0.yaml\"\n", - "\n", - "engine = Engine.from_config(\n", - " config_path=config,\n", - " data_root=data_root,\n", - ")\n", - "\n", - "engine.train(max_epochs=2)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1]\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Testing DataLoader 0: 100%|██████████| 1/1 [00:00<00:00, 61.30it/s]\n" - ] - }, - { - "data": { - "text/html": [ - "
    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
    -       "┃        Test metric               DataLoader 0        ┃\n",
    -       "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
    -       "│       test/accuracy           0.4000000059604645     │\n",
    -       "└───────────────────────────┴───────────────────────────┘\n",
    -       "
    \n" - ], - "text/plain": [ - "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", - "┃\u001b[1m \u001b[0m\u001b[1m Test metric \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m DataLoader 0 \u001b[0m\u001b[1m \u001b[0m┃\n", - "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", - "│\u001b[36m \u001b[0m\u001b[36m test/accuracy \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.4000000059604645 \u001b[0m\u001b[35m \u001b[0m│\n", - "└───────────────────────────┴───────────────────────────┘\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "{'test/accuracy': tensor(0.4000)}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "engine.test()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "PosixPath('otx-workspace/exported_model.xml')" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "exported_model_path = engine.export() # export_format=\"OPENVINO\" is default\n", - "exported_model_path" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING:root:The corresponding keys in config are not used.: ['verbose', 'data_root', 'task', 'seed', 'callback_monitor', 'resume', 'disable_infer_num_classes']\n", - "/home/harimkan/workspace/repo/otx-regression/src/otx/core/utils/build.py:52: UserWarning: Set the default number of OpenVINO inference requests to 8.\n", - " You can specify the value in config.\n", - " warnings.warn(msg, stacklevel=1)\n", - "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1]\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Testing DataLoader 0: 100%|██████████| 1/1 [00:00<00:00, 11.92it/s]\n" - ] - }, - { - "data": { - "text/html": [ - "
    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
    -       "┃        Test metric               DataLoader 0        ┃\n",
    -       "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
    -       "│       test/accuracy           0.4000000059604645     │\n",
    -       "└───────────────────────────┴───────────────────────────┘\n",
    -       "
    \n" - ], - "text/plain": [ - "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", - "┃\u001b[1m \u001b[0m\u001b[1m Test metric \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m DataLoader 0 \u001b[0m\u001b[1m \u001b[0m┃\n", - "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", - "│\u001b[36m \u001b[0m\u001b[36m test/accuracy \u001b[0m\u001b[36m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m 0.4000000059604645 \u001b[0m\u001b[35m \u001b[0m│\n", - "└───────────────────────────┴───────────────────────────┘\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "{'test/accuracy': tensor(0.4000)}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Using Exsting Engine for OV Testing\n", - "engine.test(checkpoint=exported_model_path)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/for_developers/helpers.py b/for_developers/helpers.py deleted file mode 100644 index 8baac750648..00000000000 --- a/for_developers/helpers.py +++ /dev/null @@ -1,86 +0,0 @@ -# This source code is borrowed from https://github.com/pytorch/vision -# BSD 3-Clause License - -# Copyright (c) Soumith Chintala 2016, -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: - -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. - -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. - -# * Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import matplotlib.pyplot as plt -import torch -from torchvision import tv_tensors -from torchvision.transforms.v2 import functional as F -from torchvision.utils import draw_bounding_boxes, draw_segmentation_masks - - -def plot(imgs, row_title=None, **imshow_kwargs): - if not isinstance(imgs[0], list): - # Make a 2d grid even if there's just 1 row - imgs = [imgs] - - num_rows = len(imgs) - num_cols = len(imgs[0]) - _, axs = plt.subplots(nrows=num_rows, ncols=num_cols, squeeze=False) - for row_idx, row in enumerate(imgs): - for col_idx, img in enumerate(row): - boxes = None - masks = None - if isinstance(img, tuple): - img, target = img - if isinstance(target, dict): - boxes = target.get("boxes") - masks = target.get("masks") - elif isinstance(target, tv_tensors.BoundingBoxes): - boxes = target - else: - raise ValueError(f"Unexpected target type: {type(target)}") - img = F.to_image(img) - if img.dtype.is_floating_point and img.min() < 0: - # Poor man's re-normalization for the colors to be OK-ish. This - # is useful for images coming out of Normalize() - img -= img.min() - img /= img.max() - - img = F.to_dtype(img, torch.uint8, scale=True) - if boxes is not None: - img = draw_bounding_boxes(img, boxes, colors="yellow", width=3) - if masks is not None: - img = draw_segmentation_masks( - img, - masks.to(torch.bool), - colors=["green"] * masks.shape[0], - alpha=0.65, - ) - - ax = axs[row_idx, col_idx] - ax.imshow(img.permute(1, 2, 0).numpy(), **imshow_kwargs) - ax.set(xticklabels=[], yticklabels=[], xticks=[], yticks=[]) - - if row_title is not None: - for row_idx in range(num_rows): - axs[row_idx, 0].set(ylabel=row_title[row_idx]) - - plt.tight_layout() diff --git a/for_developers/images/contribution_guide/ask_review.png b/for_developers/images/contribution_guide/ask_review.png deleted file mode 100644 index ef707577f04fd8d157e939c823ee124ee1a37608..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66353 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 Boss|m diff --git a/for_developers/images/contribution_guide/copy_link.png b/for_developers/images/contribution_guide/copy_link.png deleted file mode 100644 index a5697672c545dc54dd351a761558675775374064..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105636 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 diff --git a/for_developers/images/contribution_guide/design_proposal.png b/for_developers/images/contribution_guide/design_proposal.png deleted file mode 100644 index e1dc85f7f3b2ae8249d02c1086f7fc572fe76292..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58438 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 diff --git a/for_developers/images/contribution_guide/design_req.png b/for_developers/images/contribution_guide/design_req.png deleted file mode 100644 index 7bb1091f72c6ec144c89d7b91f72c16f46b8b129..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 100716 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 z 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

    =)E=9`i;+z$m!fJ8C5n)Uh``$s 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

    &J6fhu!8F*ub*JIz=mA;W6wbzS3yX%DRc;_+ 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ℑ{Hk3s$tq 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 diff --git a/for_developers/images/product_design/task_data_model.png b/for_developers/images/product_design/task_data_model.png deleted file mode 100644 index 9fb79737eafc14f7ec41295003593259753d9da1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 393940 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 z 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

    ehg6ufP*(W$Pi> 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 z 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. - - ```console - for_developers/regression_test$ export POSTGRES_PASSWORD= - for_developers/regression_test$ USER=$(id -u) docker compose up -d - ``` - -## How to execute regression test - -1. Prerequisite - - You must [deploy the MLFlow Tracking server](#how-to-deploy-mlflow-tracking-server-with-postgres-database) beforehand executing the regression test. - After the deployment, you can access to your MLFlow Tracking server instance with `http://:5000` (e.g., if you launched the server instance in your local machine, `http://localhost:5000`). - You might see the following screen in your web browser: - - | ![Dashboard](images/mlflow_dashboard.png) | - | :---------------------------------------: | - | MLFlow Dashboard | - - By using MLFlow Tracking server, we can save, load, and visualize the regression test results easily. - Additionaly, our MLFlow Tracking server stores the data in PostgreSQL database backend. - This means that we can have our own web front-end for the regression tests in the future. - - | ![Metric and Tags](images/mlflow_metrics_and_tags.png) | - | :-------------------------------------------------------------------: | - | Test results and environment will be recorded in the metrics and tags | - - | ![Filtering MLFlow Run](images/mlflow_filtering.png) | - | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | - | One of MLFlow Tracking server UI provides filtering functionality. In this example, we filter the regression results. Test results and environment will be recorded in the metrics and tags | - -2. Launch the regression tests - - To launch the regression tests, you should use the PyTest framework. - We developed the regression tests workflow in `tests/regression`. - - ```console - pytest tests/regression --mlflow-tracking-uri http://:5000 --dataset-root-dir --user-name '' - ``` - - There are three CLI arguments you should put in the testing commands: - - - `--mlflow-tracking-uri http://:5000`: This is the MLFlow Tracking server URI where the integration test results will be stored in. - - - `--dataset-root-dir `: This is the local directory path where the integration test should look as the input dataset. - - ```console - - ├── classification - │ └── multiclass_CUB_small - │ ├── 1 - │ │ ├── train - │ │ ├── val - │ │ └── test - | ... - ├── detection - ... - ``` - - - `--user-name `: This is the user name who launched the integration test. diff --git a/for_developers/regression_test/build.sh b/for_developers/regression_test/build.sh deleted file mode 100644 index 88066eb2a8c..00000000000 --- a/for_developers/regression_test/build.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -http_proxy=${http_proxy:-} -https_proxy=${https_proxy:-} -no_proxy=${no_proxy:-} - -docker build -t mlflow-tracker:v2.8.1 \ - --build-arg http_proxy="$http_proxy" \ - --build-arg https_proxy="$https_proxy" \ - --build-arg no_proxy="$no_proxy" . diff --git a/for_developers/regression_test/docker-compose.yml b/for_developers/regression_test/docker-compose.yml deleted file mode 100644 index 837797d6d5a..00000000000 --- a/for_developers/regression_test/docker-compose.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: "3.9" - -services: - postgres-db: - image: postgres - restart: always - user: $USER - environment: - - POSTGRES_USER=admin - - POSTGRES_PASSWORD=$POSTGRES_PASSWORD - volumes: - - ./postgres_data:/var/lib/postgresql/data - - mlflow: - image: mlflow-tracker:v2.8.1 - restart: always - ports: - - 5000:5000 - command: "mlflow server --host 0.0.0.0 --backend-store-uri postgresql+psycopg2://admin:$POSTGRES_PASSWORD@postgres-db:5432" diff --git a/for_developers/regression_test/images/mlflow_dashboard.png b/for_developers/regression_test/images/mlflow_dashboard.png deleted file mode 100644 index 48925089e489e53108bf49e352e6af51066f94da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 473442 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�fW}pw6B4yG);MHg`sNUJx*~8*H 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

    d7se4a=~ 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

    $rNbO$vujf1Z( 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≪L1d+$nf&K=%{Rw~S#CPAW>LvO+BH1ze*gwymOK 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 W$o(QCqv8p2DaxtJR=zR$@_zs#Ec{yl diff --git a/for_developers/regression_test/images/mlflow_filtering.png b/for_developers/regression_test/images/mlflow_filtering.png deleted file mode 100644 index d079184b38b78866b44cd79bf301419437983b3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 308868 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

    L<})G}%jAf1FDuzQ*L=U>k`rol*+fE-I=jUM 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%0AZLp2;Oaa)uKaU=9UHyk_CvZ!|F*;_y$& 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 beD6@j4AshXOa|8wfTbX#B3&kF>i>TLe7cq) diff --git a/for_developers/regression_test/images/mlflow_metrics_and_tags.png b/for_developers/regression_test/images/mlflow_metrics_and_tags.png deleted file mode 100644 index 91f65f38bc9dd4158e641ace99a3c83df0c13ad7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156175 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�M?AmY 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 diff --git a/for_developers/regression_test/requirements.in b/for_developers/regression_test/requirements.in deleted file mode 100644 index 0b9b042ab94..00000000000 --- a/for_developers/regression_test/requirements.in +++ /dev/null @@ -1,2 +0,0 @@ -mlflow==2.8.1 -psycopg2-binary==2.9.9 diff --git a/for_developers/regression_test/requirements.txt b/for_developers/regression_test/requirements.txt deleted file mode 100644 index 4199f327dc1..00000000000 --- a/for_developers/regression_test/requirements.txt +++ /dev/null @@ -1,1062 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --generate-hashes --output-file=for_developers/regression_test/requirements.txt for_developers/regression_test/requirements.in -# -alembic==1.13.1 \ - --hash=sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43 \ - --hash=sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595 - # via mlflow -blinker==1.7.0 \ - --hash=sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9 \ - --hash=sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182 - # via flask -certifi==2024.2.2 \ - --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ - --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 - # via requests -charset-normalizer==3.3.2 \ - --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ - --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ - --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ - --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ - --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ - --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ - --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ - --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ - --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ - --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ - --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ - --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ - --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ - --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ - --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ - --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ - --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ - --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ - --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ - --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ - --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ - --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ - --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ - --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ - --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ - --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ - --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ - --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ - --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ - --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ - --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ - --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ - --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ - --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ - --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ - --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ - --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ - --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ - --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ - --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ - --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ - --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ - --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ - --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ - --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ - --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ - --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ - --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ - --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ - --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ - --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ - --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ - --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ - --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ - --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ - --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ - --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ - --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ - --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ - --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ - --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ - --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ - --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ - --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ - --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ - --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ - --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ - --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ - --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ - --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ - --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ - --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ - --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ - --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ - --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ - --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ - --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ - --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ - --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ - --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ - --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ - --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ - --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ - --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ - --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ - --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ - --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ - --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ - --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ - --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 - # via requests -click==8.1.7 \ - --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ - --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de - # via - # databricks-cli - # flask - # mlflow -cloudpickle==2.2.1 \ - --hash=sha256:61f594d1f4c295fa5cd9014ceb3a1fc4a70b0de1164b94fbc2d854ccba056f9f \ - --hash=sha256:d89684b8de9e34a2a43b3460fbca07d09d6e25ce858df4d5a44240403b6178f5 - # via mlflow -contourpy==1.2.0 \ - --hash=sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8 \ - --hash=sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956 \ - --hash=sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5 \ - --hash=sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063 \ - --hash=sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286 \ - --hash=sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a \ - --hash=sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686 \ - --hash=sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9 \ - --hash=sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f \ - --hash=sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4 \ - --hash=sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e \ - --hash=sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0 \ - --hash=sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e \ - --hash=sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488 \ - --hash=sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399 \ - --hash=sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431 \ - --hash=sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779 \ - --hash=sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9 \ - --hash=sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab \ - --hash=sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0 \ - --hash=sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd \ - --hash=sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e \ - --hash=sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc \ - --hash=sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6 \ - --hash=sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316 \ - --hash=sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808 \ - --hash=sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0 \ - --hash=sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f \ - --hash=sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843 \ - --hash=sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9 \ - --hash=sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95 \ - --hash=sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9 \ - --hash=sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de \ - --hash=sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4 \ - --hash=sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4 \ - --hash=sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa \ - --hash=sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8 \ - --hash=sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776 \ - --hash=sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41 \ - --hash=sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108 \ - --hash=sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e \ - --hash=sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8 \ - --hash=sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727 \ - --hash=sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a - # via matplotlib -cycler==0.12.1 \ - --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ - --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c - # via matplotlib -databricks-cli==0.18.0 \ - --hash=sha256:1176a5f42d3e8af4abfc915446fb23abc44513e325c436725f5898cbb9e3384b \ - --hash=sha256:87569709eda9af3e9db8047b691e420b5e980c62ef01675575c0d2b9b4211eb7 - # via mlflow -docker==6.1.3 \ - --hash=sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20 \ - --hash=sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9 - # via mlflow -entrypoints==0.4 \ - --hash=sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4 \ - --hash=sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f - # via mlflow -flask==3.0.2 \ - --hash=sha256:3232e0e9c850d781933cf0207523d1ece087eb8d87b23777ae38456e2fbe7c6e \ - --hash=sha256:822c03f4b799204250a7ee84b1eddc40665395333973dfb9deebfe425fefcb7d - # via mlflow -fonttools==4.49.0 \ - --hash=sha256:0404faea044577a01bb82d47a8fa4bc7a54067fa7e324785dd65d200d6dd1133 \ - --hash=sha256:07bc5ea02bb7bc3aa40a1eb0481ce20e8d9b9642a9536cde0218290dd6085828 \ - --hash=sha256:08877e355d3dde1c11973bb58d4acad1981e6d1140711230a4bfb40b2b937ccc \ - --hash=sha256:0af65c720520710cc01c293f9c70bd69684365c6015cc3671db2b7d807fe51f2 \ - --hash=sha256:0ba0e00620ca28d4ca11fc700806fd69144b463aa3275e1b36e56c7c09915559 \ - --hash=sha256:1f255ce8ed7556658f6d23f6afd22a6d9bbc3edb9b96c96682124dc487e1bf42 \ - --hash=sha256:1fac1b7eebfce75ea663e860e7c5b4a8831b858c17acd68263bc156125201abf \ - --hash=sha256:263832fae27481d48dfafcc43174644b6706639661e242902ceb30553557e16c \ - --hash=sha256:29e89d0e1a7f18bc30f197cfadcbef5a13d99806447c7e245f5667579a808036 \ - --hash=sha256:33037d9e56e2562c710c8954d0f20d25b8386b397250d65581e544edc9d6b942 \ - --hash=sha256:33c584c0ef7dc54f5dd4f84082eabd8d09d1871a3d8ca2986b0c0c98165f8e86 \ - --hash=sha256:36c8865bdb5cfeec88f5028e7e592370a0657b676c6f1d84a2108e0564f90e22 \ - --hash=sha256:4145f91531fd43c50f9eb893faa08399816bb0b13c425667c48475c9f3a2b9b5 \ - --hash=sha256:4d418b1fee41a1d14931f7ab4b92dc0bc323b490e41d7a333eec82c9f1780c75 \ - --hash=sha256:768947008b4dc552d02772e5ebd49e71430a466e2373008ce905f953afea755a \ - --hash=sha256:7c7125068e04a70739dad11857a4d47626f2b0bd54de39e8622e89701836eabd \ - --hash=sha256:83a0d9336de2cba86d886507dd6e0153df333ac787377325a39a2797ec529814 \ - --hash=sha256:86eef6aab7fd7c6c8545f3ebd00fd1d6729ca1f63b0cb4d621bccb7d1d1c852b \ - --hash=sha256:8fb022d799b96df3eaa27263e9eea306bd3d437cc9aa981820850281a02b6c9a \ - --hash=sha256:9d95fa0d22bf4f12d2fb7b07a46070cdfc19ef5a7b1c98bc172bfab5bf0d6844 \ - --hash=sha256:a974c49a981e187381b9cc2c07c6b902d0079b88ff01aed34695ec5360767034 \ - --hash=sha256:ac9a745b7609f489faa65e1dc842168c18530874a5f5b742ac3dd79e26bca8bc \ - --hash=sha256:af20acbe198a8a790618ee42db192eb128afcdcc4e96d99993aca0b60d1faeb4 \ - --hash=sha256:af281525e5dd7fa0b39fb1667b8d5ca0e2a9079967e14c4bfe90fd1cd13e0f18 \ - --hash=sha256:b050d362df50fc6e38ae3954d8c29bf2da52be384649ee8245fdb5186b620836 \ - --hash=sha256:b44a52b8e6244b6548851b03b2b377a9702b88ddc21dcaf56a15a0393d425cb9 \ - --hash=sha256:b607ea1e96768d13be26d2b400d10d3ebd1456343eb5eaddd2f47d1c4bd00880 \ - --hash=sha256:b85ec0bdd7bdaa5c1946398cbb541e90a6dfc51df76dfa88e0aaa41b335940cb \ - --hash=sha256:bebd91041dda0d511b0d303180ed36e31f4f54b106b1259b69fade68413aa7ff \ - --hash=sha256:c076a9e548521ecc13d944b1d261ff3d7825048c338722a4bd126d22316087b7 \ - --hash=sha256:cbe61b158deb09cffdd8540dc4a948d6e8f4d5b4f3bf5cd7db09bd6a61fee64e \ - --hash=sha256:cdee3ab220283057e7840d5fb768ad4c2ebe65bdba6f75d5d7bf47f4e0ed7d29 \ - --hash=sha256:ce7033cb61f2bb65d8849658d3786188afd80f53dad8366a7232654804529532 \ - --hash=sha256:d00af0884c0e65f60dfaf9340e26658836b935052fdd0439952ae42e44fdd2be \ - --hash=sha256:d647a0e697e5daa98c87993726da8281c7233d9d4ffe410812a4896c7c57c075 \ - --hash=sha256:d970ecca0aac90d399e458f0b7a8a597e08f95de021f17785fb68e2dc0b99717 \ - --hash=sha256:ea329dafb9670ffbdf4dbc3b0e5c264104abcd8441d56de77f06967f032943cb \ - --hash=sha256:ebf46e7f01b7af7861310417d7c49591a85d99146fc23a5ba82fdb28af156321 \ - --hash=sha256:edc0cce355984bb3c1d1e89d6a661934d39586bb32191ebff98c600f8957c63e \ - --hash=sha256:f3bbe672df03563d1f3a691ae531f2e31f84061724c319652039e5a70927167e \ - --hash=sha256:fc11e5114f3f978d0cea7e9853627935b30d451742eeb4239a81a677bdee6bf6 \ - --hash=sha256:fdb54b076f25d6b0f0298dc706acee5052de20c83530fa165b60d1f2e9cbe3cb - # via matplotlib -gitdb==4.0.11 \ - --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ - --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b - # via gitpython -gitpython==3.1.42 \ - --hash=sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd \ - --hash=sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb - # via mlflow -greenlet==3.0.3 \ - --hash=sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67 \ - --hash=sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6 \ - --hash=sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257 \ - --hash=sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4 \ - --hash=sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676 \ - --hash=sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61 \ - --hash=sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc \ - --hash=sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca \ - --hash=sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7 \ - --hash=sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728 \ - --hash=sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305 \ - --hash=sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6 \ - --hash=sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379 \ - --hash=sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414 \ - --hash=sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04 \ - --hash=sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a \ - --hash=sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf \ - --hash=sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491 \ - --hash=sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559 \ - --hash=sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e \ - --hash=sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274 \ - --hash=sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb \ - --hash=sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b \ - --hash=sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9 \ - --hash=sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b \ - --hash=sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be \ - --hash=sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506 \ - --hash=sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405 \ - --hash=sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113 \ - --hash=sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f \ - --hash=sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5 \ - --hash=sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230 \ - --hash=sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d \ - --hash=sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f \ - --hash=sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a \ - --hash=sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e \ - --hash=sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61 \ - --hash=sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6 \ - --hash=sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d \ - --hash=sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71 \ - --hash=sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22 \ - --hash=sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2 \ - --hash=sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3 \ - --hash=sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067 \ - --hash=sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc \ - --hash=sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881 \ - --hash=sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3 \ - --hash=sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e \ - --hash=sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac \ - --hash=sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53 \ - --hash=sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0 \ - --hash=sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b \ - --hash=sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83 \ - --hash=sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41 \ - --hash=sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c \ - --hash=sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf \ - --hash=sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da \ - --hash=sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33 - # via sqlalchemy -gunicorn==21.2.0 \ - --hash=sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0 \ - --hash=sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033 - # via mlflow -idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f - # via requests -importlib-metadata==6.11.0 \ - --hash=sha256:1231cf92d825c9e03cfc4da076a16de6422c863558229ea0b22b675657463443 \ - --hash=sha256:f0afba6205ad8f8947c7d338b5342d5db2afbfd82f9cbef7879a9539cc12eb9b - # via mlflow -itsdangerous==2.1.2 \ - --hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \ - --hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a - # via flask -jinja2==3.1.3 \ - --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ - --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 - # via - # flask - # mlflow -joblib==1.3.2 \ - --hash=sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1 \ - --hash=sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9 - # via scikit-learn -kiwisolver==1.4.5 \ - --hash=sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf \ - --hash=sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e \ - --hash=sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af \ - --hash=sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f \ - --hash=sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046 \ - --hash=sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3 \ - --hash=sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5 \ - --hash=sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71 \ - --hash=sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee \ - --hash=sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3 \ - --hash=sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9 \ - --hash=sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b \ - --hash=sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985 \ - --hash=sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea \ - --hash=sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16 \ - --hash=sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89 \ - --hash=sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c \ - --hash=sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9 \ - --hash=sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712 \ - --hash=sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342 \ - --hash=sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a \ - --hash=sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958 \ - --hash=sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d \ - --hash=sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a \ - --hash=sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130 \ - --hash=sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff \ - --hash=sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898 \ - --hash=sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b \ - --hash=sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f \ - --hash=sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265 \ - --hash=sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93 \ - --hash=sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929 \ - --hash=sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635 \ - --hash=sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709 \ - --hash=sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b \ - --hash=sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb \ - --hash=sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a \ - --hash=sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920 \ - --hash=sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e \ - --hash=sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544 \ - --hash=sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45 \ - --hash=sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390 \ - --hash=sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77 \ - --hash=sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355 \ - --hash=sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff \ - --hash=sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4 \ - --hash=sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7 \ - --hash=sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20 \ - --hash=sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c \ - --hash=sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162 \ - --hash=sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228 \ - --hash=sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437 \ - --hash=sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc \ - --hash=sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a \ - --hash=sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901 \ - --hash=sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4 \ - --hash=sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770 \ - --hash=sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525 \ - --hash=sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad \ - --hash=sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a \ - --hash=sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29 \ - --hash=sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90 \ - --hash=sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250 \ - --hash=sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d \ - --hash=sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3 \ - --hash=sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54 \ - --hash=sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f \ - --hash=sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1 \ - --hash=sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da \ - --hash=sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238 \ - --hash=sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa \ - --hash=sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523 \ - --hash=sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0 \ - --hash=sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205 \ - --hash=sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3 \ - --hash=sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4 \ - --hash=sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac \ - --hash=sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9 \ - --hash=sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb \ - --hash=sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced \ - --hash=sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd \ - --hash=sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0 \ - --hash=sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da \ - --hash=sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18 \ - --hash=sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9 \ - --hash=sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276 \ - --hash=sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333 \ - --hash=sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b \ - --hash=sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db \ - --hash=sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126 \ - --hash=sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9 \ - --hash=sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09 \ - --hash=sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0 \ - --hash=sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec \ - --hash=sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7 \ - --hash=sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff \ - --hash=sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9 \ - --hash=sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192 \ - --hash=sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8 \ - --hash=sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d \ - --hash=sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6 \ - --hash=sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797 \ - --hash=sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892 \ - --hash=sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f - # via matplotlib -mako==1.3.2 \ - --hash=sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e \ - --hash=sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c - # via alembic -markdown==3.5.2 \ - --hash=sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd \ - --hash=sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8 - # via mlflow -markupsafe==2.1.5 \ - --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ - --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ - --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ - --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ - --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ - --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ - --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ - --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ - --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ - --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ - --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ - --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ - --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ - --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ - --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ - --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ - --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ - --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ - --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ - --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ - --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ - --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ - --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ - --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ - --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ - --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ - --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ - --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ - --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ - --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ - --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ - --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ - --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ - --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ - --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ - --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ - --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ - --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ - --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ - --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ - --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ - --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ - --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ - --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ - --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ - --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ - --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ - --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ - --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ - --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ - --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ - --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ - --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ - --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ - --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ - --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ - --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ - --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ - --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ - --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 - # via - # jinja2 - # mako - # werkzeug -matplotlib==3.8.3 \ - --hash=sha256:04b36ad07eac9740fc76c2aa16edf94e50b297d6eb4c081e3add863de4bb19a7 \ - --hash=sha256:09074f8057917d17ab52c242fdf4916f30e99959c1908958b1fc6032e2d0f6d4 \ - --hash=sha256:1c5c8290074ba31a41db1dc332dc2b62def469ff33766cbe325d32a3ee291aea \ - --hash=sha256:242489efdb75b690c9c2e70bb5c6550727058c8a614e4c7716f363c27e10bba1 \ - --hash=sha256:40321634e3a05ed02abf7c7b47a50be50b53ef3eaa3a573847431a545585b407 \ - --hash=sha256:4c6e00a65d017d26009bac6808f637b75ceade3e1ff91a138576f6b3065eeeba \ - --hash=sha256:5184e07c7e1d6d1481862ee361905b7059f7fe065fc837f7c3dc11eeb3f2f900 \ - --hash=sha256:5745f6d0fb5acfabbb2790318db03809a253096e98c91b9a31969df28ee604aa \ - --hash=sha256:5e431a09e6fab4012b01fc155db0ce6dccacdbabe8198197f523a4ef4805eb26 \ - --hash=sha256:5f557156f7116be3340cdeef7f128fa99b0d5d287d5f41a16e169819dcf22357 \ - --hash=sha256:6728dde0a3997396b053602dbd907a9bd64ec7d5cf99e728b404083698d3ca01 \ - --hash=sha256:7b416239e9ae38be54b028abbf9048aff5054a9aba5416bef0bd17f9162ce161 \ - --hash=sha256:7c42dae72a62f14982f1474f7e5c9959fc4bc70c9de11cc5244c6e766200ba65 \ - --hash=sha256:813925d08fb86aba139f2d31864928d67511f64e5945ca909ad5bc09a96189bb \ - --hash=sha256:83c0653c64b73926730bd9ea14aa0f50f202ba187c307a881673bad4985967b7 \ - --hash=sha256:83e0f72e2c116ca7e571c57aa29b0fe697d4c6425c4e87c6e994159e0c008635 \ - --hash=sha256:b3c5f96f57b0369c288bf6f9b5274ba45787f7e0589a34d24bdbaf6d3344632f \ - --hash=sha256:b97653d869a71721b639714b42d87cda4cfee0ee74b47c569e4874c7590c55c5 \ - --hash=sha256:bf5932eee0d428192c40b7eac1399d608f5d995f975cdb9d1e6b48539a5ad8d0 \ - --hash=sha256:c4af3f7317f8a1009bbb2d0bf23dfaba859eb7dd4ccbd604eba146dccaaaf0a4 \ - --hash=sha256:cd3a0c2be76f4e7be03d34a14d49ded6acf22ef61f88da600a18a5cd8b3c5f3c \ - --hash=sha256:cf60138ccc8004f117ab2a2bad513cc4d122e55864b4fe7adf4db20ca68a078f \ - --hash=sha256:d7e7e0993d0758933b1a241a432b42c2db22dfa37d4108342ab4afb9557cbe3e \ - --hash=sha256:e7b49ab49a3bea17802df6872f8d44f664ba8f9be0632a60c99b20b6db2165b7 \ - --hash=sha256:e9764df0e8778f06414b9d281a75235c1e85071f64bb5d71564b97c1306a2afc \ - --hash=sha256:ef6c1025a570354297d6c15f7d0f296d95f88bd3850066b7f1e7b4f2f4c13a39 \ - --hash=sha256:f386cf162b059809ecfac3bcc491a9ea17da69fa35c8ded8ad154cd4b933d5ec \ - --hash=sha256:fa93695d5c08544f4a0dfd0965f378e7afc410d8672816aff1e81be1f45dbf2e - # via mlflow -mlflow==2.8.1 \ - --hash=sha256:c14716f2b328cc4a649394013035f8326d4408b9a7c8f378b8f0cb3e0e741af9 \ - --hash=sha256:e4e5bdd2d9efb0b386ecbce2df7e43f04c46a32080208414dc53b5fd71559678 - # via -r for_developers/regression_test/requirements.in -numpy==1.26.4 \ - --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ - --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ - --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ - --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ - --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ - --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ - --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ - --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ - --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ - --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ - --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ - --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ - --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ - --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ - --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ - --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ - --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ - --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ - --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ - --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ - --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ - --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ - --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ - --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ - --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ - --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ - --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ - --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ - --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ - --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ - --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ - --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ - --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ - --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ - --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ - --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f - # via - # contourpy - # matplotlib - # mlflow - # pandas - # pyarrow - # scikit-learn - # scipy -oauthlib==3.2.2 \ - --hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \ - --hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918 - # via databricks-cli -packaging==23.2 \ - --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ - --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 - # via - # docker - # gunicorn - # matplotlib - # mlflow -pandas==2.2.1 \ - --hash=sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee \ - --hash=sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e \ - --hash=sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572 \ - --hash=sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944 \ - --hash=sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403 \ - --hash=sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89 \ - --hash=sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab \ - --hash=sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6 \ - --hash=sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb \ - --hash=sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9 \ - --hash=sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019 \ - --hash=sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be \ - --hash=sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd \ - --hash=sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c \ - --hash=sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88 \ - --hash=sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0 \ - --hash=sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397 \ - --hash=sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc \ - --hash=sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2 \ - --hash=sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7 \ - --hash=sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06 \ - --hash=sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51 \ - --hash=sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0 \ - --hash=sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a \ - --hash=sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16 \ - --hash=sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02 \ - --hash=sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359 \ - --hash=sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b \ - --hash=sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df - # via mlflow -pillow==10.2.0 \ - --hash=sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8 \ - --hash=sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39 \ - --hash=sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac \ - --hash=sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869 \ - --hash=sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e \ - --hash=sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04 \ - --hash=sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9 \ - --hash=sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e \ - --hash=sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe \ - --hash=sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef \ - --hash=sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56 \ - --hash=sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa \ - --hash=sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f \ - --hash=sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f \ - --hash=sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e \ - --hash=sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a \ - --hash=sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2 \ - --hash=sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2 \ - --hash=sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5 \ - --hash=sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a \ - --hash=sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2 \ - --hash=sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213 \ - --hash=sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563 \ - --hash=sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591 \ - --hash=sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c \ - --hash=sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2 \ - --hash=sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb \ - --hash=sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757 \ - --hash=sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0 \ - --hash=sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452 \ - --hash=sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad \ - --hash=sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01 \ - --hash=sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f \ - --hash=sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5 \ - --hash=sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61 \ - --hash=sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e \ - --hash=sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b \ - --hash=sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068 \ - --hash=sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9 \ - --hash=sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588 \ - --hash=sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483 \ - --hash=sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f \ - --hash=sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67 \ - --hash=sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7 \ - --hash=sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311 \ - --hash=sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6 \ - --hash=sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72 \ - --hash=sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6 \ - --hash=sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129 \ - --hash=sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13 \ - --hash=sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67 \ - --hash=sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c \ - --hash=sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516 \ - --hash=sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e \ - --hash=sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e \ - --hash=sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364 \ - --hash=sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023 \ - --hash=sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1 \ - --hash=sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04 \ - --hash=sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d \ - --hash=sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a \ - --hash=sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7 \ - --hash=sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb \ - --hash=sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4 \ - --hash=sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e \ - --hash=sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1 \ - --hash=sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48 \ - --hash=sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868 - # via matplotlib -protobuf==4.25.3 \ - --hash=sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4 \ - --hash=sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8 \ - --hash=sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c \ - --hash=sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d \ - --hash=sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4 \ - --hash=sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa \ - --hash=sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c \ - --hash=sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019 \ - --hash=sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9 \ - --hash=sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c \ - --hash=sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2 - # via mlflow -psycopg2-binary==2.9.9 \ - --hash=sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9 \ - --hash=sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77 \ - --hash=sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e \ - --hash=sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84 \ - --hash=sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3 \ - --hash=sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2 \ - --hash=sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67 \ - --hash=sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876 \ - --hash=sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152 \ - --hash=sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f \ - --hash=sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a \ - --hash=sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6 \ - --hash=sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503 \ - --hash=sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f \ - --hash=sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493 \ - --hash=sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996 \ - --hash=sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f \ - --hash=sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e \ - --hash=sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59 \ - --hash=sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94 \ - --hash=sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7 \ - --hash=sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682 \ - --hash=sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420 \ - --hash=sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae \ - --hash=sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291 \ - --hash=sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe \ - --hash=sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980 \ - --hash=sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93 \ - --hash=sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692 \ - --hash=sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119 \ - --hash=sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716 \ - --hash=sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472 \ - --hash=sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b \ - --hash=sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2 \ - --hash=sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc \ - --hash=sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c \ - --hash=sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5 \ - --hash=sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab \ - --hash=sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984 \ - --hash=sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9 \ - --hash=sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf \ - --hash=sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0 \ - --hash=sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f \ - --hash=sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212 \ - --hash=sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb \ - --hash=sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be \ - --hash=sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90 \ - --hash=sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041 \ - --hash=sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7 \ - --hash=sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860 \ - --hash=sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d \ - --hash=sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245 \ - --hash=sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27 \ - --hash=sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417 \ - --hash=sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359 \ - --hash=sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202 \ - --hash=sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0 \ - --hash=sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7 \ - --hash=sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba \ - --hash=sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1 \ - --hash=sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd \ - --hash=sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07 \ - --hash=sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98 \ - --hash=sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55 \ - --hash=sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d \ - --hash=sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972 \ - --hash=sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f \ - --hash=sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e \ - --hash=sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26 \ - --hash=sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957 \ - --hash=sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53 \ - --hash=sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52 - # via -r for_developers/regression_test/requirements.in -pyarrow==14.0.2 \ - --hash=sha256:059bd8f12a70519e46cd64e1ba40e97eae55e0cbe1695edd95384653d7626b23 \ - --hash=sha256:06ff1264fe4448e8d02073f5ce45a9f934c0f3db0a04460d0b01ff28befc3696 \ - --hash=sha256:1e6987c5274fb87d66bb36816afb6f65707546b3c45c44c28e3c4133c010a881 \ - --hash=sha256:209bac546942b0d8edc8debda248364f7f668e4aad4741bae58e67d40e5fcf75 \ - --hash=sha256:20e003a23a13da963f43e2b432483fdd8c38dc8882cd145f09f21792e1cf22a1 \ - --hash=sha256:22a768987a16bb46220cef490c56c671993fbee8fd0475febac0b3e16b00a10e \ - --hash=sha256:2cc61593c8e66194c7cdfae594503e91b926a228fba40b5cf25cc593563bcd07 \ - --hash=sha256:2dbba05e98f247f17e64303eb876f4a80fcd32f73c7e9ad975a83834d81f3fda \ - --hash=sha256:32356bfb58b36059773f49e4e214996888eeea3a08893e7dbde44753799b2a02 \ - --hash=sha256:36cef6ba12b499d864d1def3e990f97949e0b79400d08b7cf74504ffbd3eb025 \ - --hash=sha256:37c233ddbce0c67a76c0985612fef27c0c92aef9413cf5aa56952f359fcb7379 \ - --hash=sha256:3c0fa3bfdb0305ffe09810f9d3e2e50a2787e3a07063001dcd7adae0cee3601a \ - --hash=sha256:3f16111f9ab27e60b391c5f6d197510e3ad6654e73857b4e394861fc79c37200 \ - --hash=sha256:52809ee69d4dbf2241c0e4366d949ba035cbcf48409bf404f071f624ed313a2b \ - --hash=sha256:5c1da70d668af5620b8ba0a23f229030a4cd6c5f24a616a146f30d2386fec422 \ - --hash=sha256:63ac901baec9369d6aae1cbe6cca11178fb018a8d45068aaf5bb54f94804a866 \ - --hash=sha256:64df2bf1ef2ef14cee531e2dfe03dd924017650ffaa6f9513d7a1bb291e59c15 \ - --hash=sha256:66e986dc859712acb0bd45601229021f3ffcdfc49044b64c6d071aaf4fa49e98 \ - --hash=sha256:6dd4f4b472ccf4042f1eab77e6c8bce574543f54d2135c7e396f413046397d5a \ - --hash=sha256:75ee0efe7a87a687ae303d63037d08a48ef9ea0127064df18267252cfe2e9541 \ - --hash=sha256:76fc257559404ea5f1306ea9a3ff0541bf996ff3f7b9209fc517b5e83811fa8e \ - --hash=sha256:78ea56f62fb7c0ae8ecb9afdd7893e3a7dbeb0b04106f5c08dbb23f9c0157591 \ - --hash=sha256:87482af32e5a0c0cce2d12eb3c039dd1d853bd905b04f3f953f147c7a196915b \ - --hash=sha256:87e879323f256cb04267bb365add7208f302df942eb943c93a9dfeb8f44840b1 \ - --hash=sha256:a01d0052d2a294a5f56cc1862933014e696aa08cc7b620e8c0cce5a5d362e976 \ - --hash=sha256:a25eb2421a58e861f6ca91f43339d215476f4fe159eca603c55950c14f378cc5 \ - --hash=sha256:a51fee3a7db4d37f8cda3ea96f32530620d43b0489d169b285d774da48ca9785 \ - --hash=sha256:a898d134d00b1eca04998e9d286e19653f9d0fcb99587310cd10270907452a6b \ - --hash=sha256:b0c4a18e00f3a32398a7f31da47fefcd7a927545b396e1f15d0c85c2f2c778cd \ - --hash=sha256:ba9fe808596c5dbd08b3aeffe901e5f81095baaa28e7d5118e01354c64f22807 \ - --hash=sha256:c65bf4fd06584f058420238bc47a316e80dda01ec0dfb3044594128a6c2db794 \ - --hash=sha256:c87824a5ac52be210d32906c715f4ed7053d0180c1060ae3ff9b7e560f53f944 \ - --hash=sha256:e354fba8490de258be7687f341bc04aba181fc8aa1f71e4584f9890d9cb2dec2 \ - --hash=sha256:e4b123ad0f6add92de898214d404e488167b87b5dd86e9a434126bc2b7a5578d \ - --hash=sha256:f7d029f20ef56673a9730766023459ece397a05001f4e4d13805111d7c2108c0 \ - --hash=sha256:fc0de7575e841f1595ac07e5bc631084fd06ca8b03c0f2ecece733d23cd5102a - # via mlflow -pyjwt==2.8.0 \ - --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ - --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 - # via databricks-cli -pyparsing==3.1.2 \ - --hash=sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad \ - --hash=sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742 - # via matplotlib -python-dateutil==2.9.0.post0 \ - --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ - --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 - # via - # matplotlib - # pandas -pytz==2023.4 \ - --hash=sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40 \ - --hash=sha256:f90ef520d95e7c46951105338d918664ebfd6f1d995bd7d153127ce90efafa6a - # via - # mlflow - # pandas -pyyaml==6.0.1 \ - --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ - --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ - --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f - # via mlflow -querystring-parser==1.2.4 \ - --hash=sha256:644fce1cffe0530453b43a83a38094dbe422ccba8c9b2f2a1c00280e14ca8a62 \ - --hash=sha256:d2fa90765eaf0de96c8b087872991a10238e89ba015ae59fedfed6bd61c242a0 - # via mlflow -requests==2.31.0 \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 - # via - # databricks-cli - # docker - # mlflow -scikit-learn==1.4.1.post1 \ - --hash=sha256:0df87de9ce1c0140f2818beef310fb2e2afdc1e66fc9ad587965577f17733649 \ - --hash=sha256:14e4c88436ac96bf69eb6d746ac76a574c314a23c6961b7d344b38877f20fee1 \ - --hash=sha256:1754b0c2409d6ed5a3380512d0adcf182a01363c669033a2b55cca429ed86a81 \ - --hash=sha256:1afed6951bc9d2053c6ee9a518a466cbc9b07c6a3f9d43bfe734192b6125d508 \ - --hash=sha256:1d491ef66e37f4e812db7e6c8286520c2c3fc61b34bf5e59b67b4ce528de93af \ - --hash=sha256:234b6bda70fdcae9e4abbbe028582ce99c280458665a155eed0b820599377d25 \ - --hash=sha256:2a3ee19211ded1a52ee37b0a7b373a8bfc66f95353af058a210b692bd4cda0dd \ - --hash=sha256:4310bff71aa98b45b46cd26fa641309deb73a5d1c0461d181587ad4f30ea3c36 \ - --hash=sha256:4ba516fcdc73d60e7f48cbb0bccb9acbdb21807de3651531208aac73c758e3ab \ - --hash=sha256:6145dfd9605b0b50ae72cdf72b61a2acd87501369a763b0d73d004710ebb76b5 \ - --hash=sha256:629e09f772ad42f657ca60a1a52342eef786218dd20cf1369a3b8d085e55ef8f \ - --hash=sha256:712c1c69c45b58ef21635360b3d0a680ff7d83ac95b6f9b82cf9294070cda710 \ - --hash=sha256:78cd27b4669513b50db4f683ef41ea35b5dddc797bd2bbd990d49897fd1c8a46 \ - --hash=sha256:93d3d496ff1965470f9977d05e5ec3376fb1e63b10e4fda5e39d23c2d8969a30 \ - --hash=sha256:9f43dd527dabff5521af2786a2f8de5ba381e182ec7292663508901cf6ceaf6e \ - --hash=sha256:a1e289f33f613cefe6707dead50db31930530dc386b6ccff176c786335a7b01c \ - --hash=sha256:aa0029b78ef59af22cfbd833e8ace8526e4df90212db7ceccbea582ebb5d6794 \ - --hash=sha256:c02e27d65b0c7dc32f2c5eb601aaf5530b7a02bfbe92438188624524878336f2 \ - --hash=sha256:c540aaf44729ab5cd4bd5e394f2b375e65ceaea9cdd8c195788e70433d91bbc5 \ - --hash=sha256:ce03506ccf5f96b7e9030fea7eb148999b254c44c10182ac55857bc9b5d4815f \ - --hash=sha256:d7cd3a77c32879311f2aa93466d3c288c955ef71d191503cf0677c3340ae8ae0 - # via mlflow -scipy==1.12.0 \ - --hash=sha256:196ebad3a4882081f62a5bf4aeb7326aa34b110e533aab23e4374fcccb0890dc \ - --hash=sha256:408c68423f9de16cb9e602528be4ce0d6312b05001f3de61fe9ec8b1263cad08 \ - --hash=sha256:4bf5abab8a36d20193c698b0f1fc282c1d083c94723902c447e5d2f1780936a3 \ - --hash=sha256:4c1020cad92772bf44b8e4cdabc1df5d87376cb219742549ef69fc9fd86282dd \ - --hash=sha256:5adfad5dbf0163397beb4aca679187d24aec085343755fcdbdeb32b3679f254c \ - --hash=sha256:5e32847e08da8d895ce09d108a494d9eb78974cf6de23063f93306a3e419960c \ - --hash=sha256:6546dc2c11a9df6926afcbdd8a3edec28566e4e785b915e849348c6dd9f3f490 \ - --hash=sha256:730badef9b827b368f351eacae2e82da414e13cf8bd5051b4bdfd720271a5371 \ - --hash=sha256:75ea2a144096b5e39402e2ff53a36fecfd3b960d786b7efd3c180e29c39e53f2 \ - --hash=sha256:78e4402e140879387187f7f25d91cc592b3501a2e51dfb320f48dfb73565f10b \ - --hash=sha256:8b8066bce124ee5531d12a74b617d9ac0ea59245246410e19bca549656d9a40a \ - --hash=sha256:8bee4993817e204d761dba10dbab0774ba5a8612e57e81319ea04d84945375ba \ - --hash=sha256:913d6e7956c3a671de3b05ccb66b11bc293f56bfdef040583a7221d9e22a2e35 \ - --hash=sha256:95e5c750d55cf518c398a8240571b0e0782c2d5a703250872f36eaf737751338 \ - --hash=sha256:9c39f92041f490422924dfdb782527a4abddf4707616e07b021de33467f917bc \ - --hash=sha256:a24024d45ce9a675c1fb8494e8e5244efea1c7a09c60beb1eeb80373d0fecc70 \ - --hash=sha256:a7ebda398f86e56178c2fa94cad15bf457a218a54a35c2a7b4490b9f9cb2676c \ - --hash=sha256:b360f1b6b2f742781299514e99ff560d1fe9bd1bff2712894b52abe528d1fd1e \ - --hash=sha256:bba1b0c7256ad75401c73e4b3cf09d1f176e9bd4248f0d3112170fb2ec4db067 \ - --hash=sha256:c3003652496f6e7c387b1cf63f4bb720951cfa18907e998ea551e6de51a04467 \ - --hash=sha256:e53958531a7c695ff66c2e7bb7b79560ffdc562e2051644c5576c39ff8efb563 \ - --hash=sha256:e646d8571804a304e1da01040d21577685ce8e2db08ac58e543eaca063453e1c \ - --hash=sha256:e7e76cc48638228212c747ada851ef355c2bb5e7f939e10952bc504c11f4e372 \ - --hash=sha256:f5f00ebaf8de24d14b8449981a2842d404152774c1a1d880c901bf454cb8e2a1 \ - --hash=sha256:f7ce148dffcd64ade37b2df9315541f9adad6efcaa86866ee7dd5db0c8f041c3 - # via - # mlflow - # scikit-learn -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 - # via - # databricks-cli - # python-dateutil - # querystring-parser -smmap==5.0.1 \ - --hash=sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62 \ - --hash=sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da - # via gitdb -sqlalchemy==2.0.28 \ - --hash=sha256:0315d9125a38026227f559488fe7f7cee1bd2fbc19f9fd637739dc50bb6380b2 \ - --hash=sha256:0d3dd67b5d69794cfe82862c002512683b3db038b99002171f624712fa71aeaa \ - --hash=sha256:124202b4e0edea7f08a4db8c81cc7859012f90a0d14ba2bf07c099aff6e96462 \ - --hash=sha256:1ee8bd6d68578e517943f5ebff3afbd93fc65f7ef8f23becab9fa8fb315afb1d \ - --hash=sha256:243feb6882b06a2af68ecf4bec8813d99452a1b62ba2be917ce6283852cf701b \ - --hash=sha256:2858bbab1681ee5406650202950dc8f00e83b06a198741b7c656e63818633526 \ - --hash=sha256:2f60843068e432311c886c5f03c4664acaef507cf716f6c60d5fde7265be9d7b \ - --hash=sha256:328529f7c7f90adcd65aed06a161851f83f475c2f664a898af574893f55d9e53 \ - --hash=sha256:33157920b233bc542ce497a81a2e1452e685a11834c5763933b440fedd1d8e2d \ - --hash=sha256:3eba73ef2c30695cb7eabcdb33bb3d0b878595737479e152468f3ba97a9c22a4 \ - --hash=sha256:426f2fa71331a64f5132369ede5171c52fd1df1bd9727ce621f38b5b24f48750 \ - --hash=sha256:45c7b78dfc7278329f27be02c44abc0d69fe235495bb8e16ec7ef1b1a17952db \ - --hash=sha256:46a3d4e7a472bfff2d28db838669fc437964e8af8df8ee1e4548e92710929adc \ - --hash=sha256:4a5adf383c73f2d49ad15ff363a8748319ff84c371eed59ffd0127355d6ea1da \ - --hash=sha256:4b6303bfd78fb3221847723104d152e5972c22367ff66edf09120fcde5ddc2e2 \ - --hash=sha256:56856b871146bfead25fbcaed098269d90b744eea5cb32a952df00d542cdd368 \ - --hash=sha256:5da98815f82dce0cb31fd1e873a0cb30934971d15b74e0d78cf21f9e1b05953f \ - --hash=sha256:5df5d1dafb8eee89384fb7a1f79128118bc0ba50ce0db27a40750f6f91aa99d5 \ - --hash=sha256:68722e6a550f5de2e3cfe9da6afb9a7dd15ef7032afa5651b0f0c6b3adb8815d \ - --hash=sha256:78bb7e8da0183a8301352d569900d9d3594c48ac21dc1c2ec6b3121ed8b6c986 \ - --hash=sha256:81ba314a08c7ab701e621b7ad079c0c933c58cdef88593c59b90b996e8b58fa5 \ - --hash=sha256:843a882cadebecc655a68bd9a5b8aa39b3c52f4a9a5572a3036fb1bb2ccdc197 \ - --hash=sha256:87724e7ed2a936fdda2c05dbd99d395c91ea3c96f029a033a4a20e008dd876bf \ - --hash=sha256:8c7f10720fc34d14abad5b647bc8202202f4948498927d9f1b4df0fb1cf391b7 \ - --hash=sha256:8e91b5e341f8c7f1e5020db8e5602f3ed045a29f8e27f7f565e0bdee3338f2c7 \ - --hash=sha256:943aa74a11f5806ab68278284a4ddd282d3fb348a0e96db9b42cb81bf731acdc \ - --hash=sha256:9461802f2e965de5cff80c5a13bc945abea7edaa1d29360b485c3d2b56cdb075 \ - --hash=sha256:9b66fcd38659cab5d29e8de5409cdf91e9986817703e1078b2fdaad731ea66f5 \ - --hash=sha256:a6bec1c010a6d65b3ed88c863d56b9ea5eeefdf62b5e39cafd08c65f5ce5198b \ - --hash=sha256:a921002be69ac3ab2cf0c3017c4e6a3377f800f1fca7f254c13b5f1a2f10022c \ - --hash=sha256:aca7b6d99a4541b2ebab4494f6c8c2f947e0df4ac859ced575238e1d6ca5716b \ - --hash=sha256:ad7acbe95bac70e4e687a4dc9ae3f7a2f467aa6597049eeb6d4a662ecd990bb6 \ - --hash=sha256:af8ce2d31679006e7b747d30a89cd3ac1ec304c3d4c20973f0f4ad58e2d1c4c9 \ - --hash=sha256:b4a2cf92995635b64876dc141af0ef089c6eea7e05898d8d8865e71a326c0385 \ - --hash=sha256:bbda76961eb8f27e6ad3c84d1dc56d5bc61ba8f02bd20fcf3450bd421c2fcc9c \ - --hash=sha256:bd7e4baf9161d076b9a7e432fce06217b9bd90cfb8f1d543d6e8c4595627edb9 \ - --hash=sha256:bea30da1e76cb1acc5b72e204a920a3a7678d9d52f688f087dc08e54e2754c67 \ - --hash=sha256:c61e2e41656a673b777e2f0cbbe545323dbe0d32312f590b1bc09da1de6c2a02 \ - --hash=sha256:c6c4da4843e0dabde41b8f2e8147438330924114f541949e6318358a56d1875a \ - --hash=sha256:d3499008ddec83127ab286c6f6ec82a34f39c9817f020f75eca96155f9765097 \ - --hash=sha256:dbb990612c36163c6072723523d2be7c3eb1517bbdd63fe50449f56afafd1133 \ - --hash=sha256:dd53b6c4e6d960600fd6532b79ee28e2da489322fcf6648738134587faf767b6 \ - --hash=sha256:df40c16a7e8be7413b885c9bf900d402918cc848be08a59b022478804ea076b8 \ - --hash=sha256:e0a5354cb4de9b64bccb6ea33162cb83e03dbefa0d892db88a672f5aad638a75 \ - --hash=sha256:e0b148ab0438f72ad21cb004ce3bdaafd28465c4276af66df3b9ecd2037bf252 \ - --hash=sha256:e23b88c69497a6322b5796c0781400692eca1ae5532821b39ce81a48c395aae9 \ - --hash=sha256:fc4974d3684f28b61b9a90fcb4c41fb340fd4b6a50c04365704a4da5a9603b05 \ - --hash=sha256:feea693c452d85ea0015ebe3bb9cd15b6f49acc1a31c28b3c50f4db0f8fb1e71 \ - --hash=sha256:fffcc8edc508801ed2e6a4e7b0d150a62196fd28b4e16ab9f65192e8186102b6 - # via - # alembic - # mlflow -sqlparse==0.4.4 \ - --hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \ - --hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c - # via mlflow -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via databricks-cli -threadpoolctl==3.3.0 \ - --hash=sha256:5dac632b4fa2d43f42130267929af3ba01399ef4bd1882918e92dbc30365d30c \ - --hash=sha256:6155be1f4a39f31a18ea70f94a77e0ccd57dced08122ea61109e7da89883781e - # via scikit-learn -typing-extensions==4.10.0 \ - --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ - --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb - # via - # alembic - # sqlalchemy -tzdata==2024.1 \ - --hash=sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd \ - --hash=sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252 - # via pandas -urllib3==2.2.1 \ - --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \ - --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19 - # via - # databricks-cli - # docker - # requests -websocket-client==1.7.0 \ - --hash=sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6 \ - --hash=sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588 - # via docker -werkzeug==3.0.1 \ - --hash=sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc \ - --hash=sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10 - # via flask -zipp==3.17.0 \ - --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ - --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 - # via importlib-metadata diff --git a/for_developers/requirements-lock.txt b/for_developers/requirements-lock.txt deleted file mode 100644 index 1c9b8db00e0..00000000000 --- a/for_developers/requirements-lock.txt +++ /dev/null @@ -1,101 +0,0 @@ -apturl==0.5.2 -attrs==23.1.0 -bandit==1.7.4 -black==23.9.1 -blinker==1.4 -Brlapi==0.8.3 -cachetools==5.3.0 -certifi==2020.6.20 -cfgv==3.4.0 -chardet==5.1.0 -click==8.0.4 -colorama==0.4.6 -command-not-found==0.3 -coverage==7.3.2 -cryptography==3.4.8 -cupshelpers==1.0 -dbus-python==1.2.18 -defer==1.0.6 -distlib==0.3.6 -distro==1.7.0 -distro-info===1.1build1 -exceptiongroup==1.1.1 -filelock==3.10.7 -gitdb==4.0.11 -GitPython==3.1.40 -httplib2==0.20.2 -identify==2.5.31 -idna==3.3 -importlib-metadata==4.6.4 -iniconfig==2.0.0 -jeepney==0.7.1 -keyring==23.5.0 -language-selector==0.1 -launchpadlib==1.10.16 -lazr.restfulclient==0.14.4 -lazr.uri==1.0.6 -louis==3.20.0 -macaroonbakery==1.3.1 -maturin==1.1.0 -more-itertools==8.10.0 -mypy==1.6.0 -mypy-extensions==1.0.0 -netifaces==0.11.0 -nodeenv==1.8.0 -oauthlib==3.2.0 -olefile==0.46 -packaging==23.0 -pathspec==0.11.2 -pbr==5.11.1 -pexpect==4.8.0 -Pillow==9.0.1 -platformdirs==2.5.1 -pluggy==1.0.0 -pre-commit==2.15.0 -protobuf==3.12.4 -ptyprocess==0.7.0 -py==1.10.0 -pycairo==1.20.1 -pycups==2.0.1 -PyGObject==3.42.1 -PyJWT==2.3.0 -pymacaroons==0.13.0 -PyNaCl==1.5.0 -pyparsing==2.4.7 -pyproject_api==1.5.1 -pyRFC3339==1.1 -pytest==6.2.5 -pytest-cov==2.11.1 -pytest-html==3.2.0 -pytest-metadata==2.0.4 -python-apt==2.3.0+ubuntu2.1 -python-dateutil==2.8.1 -python-debian===0.1.43ubuntu1 -pytz==2022.1 -pyxdg==0.27 -PyYAML==5.4.1 -reportlab==3.6.8 -requests==2.25.1 -ruff==0.0.292 -screen-resolution-extra==0.0.0 -SecretStorage==3.3.1 -six==1.16.0 -smmap==5.0.1 -ssh-import-id==5.11 -stevedore==5.1.0 -systemd-python==234 -testfixtures==7.0.0 -toml==0.10.2 -tomli==2.0.1 -tox==4.4.8 -typing_extensions==4.8.0 -ubuntu-advantage-tools==27.9 -ubuntu-drivers-common==0.0.0 -ufw==0.36.1 -unattended-upgrades==0.1 -urllib3==1.26.5 -virtualenv==20.21.0 -wadllib==1.3.6 -xdg==5 -xkit==0.0.0 -zipp==1.0.0 diff --git a/for_developers/setup_guide.md b/for_developers/setup_guide.md deleted file mode 100644 index e457f6dbefe..00000000000 --- a/for_developers/setup_guide.md +++ /dev/null @@ -1,96 +0,0 @@ -# How to setup dev env - -## Installation - -### Requirements - -CUDA >= 11.8 -torchvision >= 0.16 -python >= 3.9 -pip >= 23.3.2 - -### With Conda - -```console -# Create venv from conda -conda create -n otx-v2 python=3.11 -conda activate otx-v2 - -# Install PyTorch and TorchVision -conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia - -# Install otx with core requirements -pip install -e . - -# otx install (install mmX) -otx install -v -``` - -In case of the error: `File "setup.py" not found. Directory cannot be installed in editable mode:`, uprade pip: - -``` -pip install --upgrade pip -``` - -### With PIP & 'otx install' - -```console -# Create venv -python -m venv venv -source venv/bin/activate - -# Install this package -pip install -e . - -# OTX --help -otx --help - -# Upgrade pip -pip install --upgrade pip - -# Install torch & lightning base on user environments -otx install -v -# or 'otx install' (Not verbose mode) -``` - -Please see [requirements-lock.txt](requirements-lock.txt). This is what I got after the above installation steps by `pip freeze`. - -## Launch training with demo template - -- Auto-Configuration from dataset & task (Default Model: ATSS-MobilenetV2) - - ```console - otx train --data_root tests/assets/car_tree_bug --model.num_classes 3 --engine.device gpu --work_dir ./otx-workspace - ``` - -- Launch detection task ATSS-MobilenetV2 template - - ```console - otx train --config src/otx/recipe/detection/atss_mobilenetv2.yaml --data_root tests/assets/car_tree_bug --model.num_classes 3 --max_epochs 50 --check_val_every_n_epoch 10 --engine.device gpu --work_dir ./otx-workspace - ``` - -- Change subset names, e.g., "train" -> "train_16" (for training) - - ```console - otx train ... --data.config.train_subset.subset_name --data.config.val_subset.subset_name --data.config.test_subset.subset_name - ``` - -- Do train with the existing model checkpoint for resume - - ```console - otx train ... --engine.checkpoint - ``` - -- Do experiment with deterministic operations and the fixed seed - - ```console - otx train ... --deterministic True --seed - ``` - -- Do test with the existing model checkpoint - - ```console - otx test ... --checkpoint= - ``` - - `--deterministic True` might affect to the model performance. Please see [this link](https://lightning.ai/docs/pytorch/stable/common/trainer.html#deterministic). Therefore, it is not recommended to turn on this option for the model performance comparison. diff --git a/for_developers/torchvision.ipynb b/for_developers/torchvision.ipynb deleted file mode 100644 index 48a88e494f6..00000000000 --- a/for_developers/torchvision.ipynb +++ /dev/null @@ -1,95 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from datumaro import Dataset\n", - "from otx.core.data.dataset.detection import OTXDetectionDataset\n", - "import torch\n", - "from torchvision.transforms import v2\n", - "\n", - "torch.manual_seed(3003)\n", - "\n", - "transforms = v2.Compose(\n", - " [\n", - " v2.RandomResizedCrop(size=(224, 224), antialias=True),\n", - " v2.RandomHorizontalFlip(p=0.5),\n", - " v2.SanitizeBoundingBoxes(min_size=40),\n", - " v2.ToDtype(torch.float32, scale=True),\n", - " v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n", - " ]\n", - ")\n", - "\n", - "dm_dataset = Dataset.import_from(\"../tests/assets/car_tree_bug\")\n", - "dataset = OTXDetectionDataset(\n", - " dm_subset=dm_dataset.get_subset(\"train\"), transforms=transforms\n", - ")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/vinnamki/otx/training_extensions/src/otx/core/data/mem_cache.py:231: UserWarning: Before calling MemCacheHandlerSingleton.get(), you should call MemCacheHandlerSingleton.create() first.\n", - " warnings.warn(message=msg, stacklevel=1)\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "

    " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from helpers import plot\n", - "\n", - "\n", - "def to_plot_format(item):\n", - " return item.image, {\"boxes\": item.bboxes}\n", - "\n", - "\n", - "# Plot 2x2 same dataset items but augmented randomly\n", - "plot(\n", - " [\n", - " [to_plot_format(dataset[0]), to_plot_format(dataset[0])],\n", - " [to_plot_format(dataset[0]), to_plot_format(dataset[0])],\n", - " ]\n", - ")\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "otxv2", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/pyproject.toml b/pyproject.toml index 39f1b15d527..53433fcebbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,246 +1,85 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # SETUP CONFIGURATION. # [build-system] +# FIXME: specified torch wheel to build torch cpp extension should be fixed in some +# different way instead of using static link url requires = [ - "setuptools>=61", + "setuptools>=42", "wheel", "Cython~=0.29.32", + "numpy~=1.23.4" ] build-backend = "setuptools.build_meta" -[project] -name = "otx" -dynamic = ["version"] -description = "OpenVINO Training Extensions: Train, Evaluate, Optimize, Deploy Computer Vision Models via OpenVINO" -readme = "README.md" -requires-python = ">=3.10" -license = {file = "LICENSE"} -authors = [ - { name = "OpenVINO Training Extensions Contributors" }, -] -classifiers = [ - "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Cython", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", -] -dependencies = [ - "datumaro==1.6.0rc1", - "omegaconf==2.3.0", - "rich==13.7.1", - "jsonargparse==4.27.1", - "psutil==5.9.8", # Mem cache needs system checks - "ftfy==6.1.3", - "regex==2023.12.25", - "importlib_resources==6.1.3", - "docstring_parser==0.15", # CLI help-formatter - "rich_argparse==1.4.0", # CLI help-formatter -] - -[project.optional-dependencies] -dev = [ - "tox==4.4.5", - "pre-commit==2.20.0", - "pylint", - "pytest", - "coverage", - "pytest-timeout", - "pytest-mock", - "pytest-csv", - "pytest-cov", - "mlflow==2.11.1", # For regression test - "py-cpuinfo==9.0.0", # For regression test -] -docs = [ - "furo", - "myst-parser", - "sphinx==5.3.0", - "pydata-sphinx-theme==0.12.0", - "sphinx-tabs", - "sphinx-panels", - "sphinx-design", - "sphinx-copybutton==0.5.0", - "sphinx-autoapi", - "sphinxemoji", - "nbsphinx", -] -base = [ - "torch==2.1.1", - "lightning==2.1.2", - "pytorchcv", - "timm", - "openvino==2023.3.0", - "openvino-dev==2023.3.0", - "openvino-model-api==0.1.9", - "onnx==1.15.0", - "onnxconverter-common==1.14.0", - "nncf==2.8.1", -] -mmlab = [ - "mmdet==3.2.0", - "mmpretrain==1.1.1", - "mmsegmentation==1.2.1", - "mmaction2==1.2.0", - "mmdeploy==1.3.*", - # Without the pip cache, oss2 will sometimes install to a lower version. This is related to the installation of the mmlab library. - # This causes an error when training the mm model, so fix the version first. - "oss2==2.17.0", -] -anomaly = [ - # [FIXME] @ashwinvaidya17: Install using a temporary hot-fix commit due to a torchmetrics version conflict. - "anomalib @ git+https://github.com/openvinotoolkit/anomalib.git@e78091883a620229c277a79674a904d9f785f8d5", - # This is a dependency to avoid conflicts with installing the anomalib[core] option. - "av>=10.0.0", - "einops>=0.3.2", - "freia>=0.2", - "imgaug==0.4.0", - "kornia>=0.6.6,<0.6.10", - "matplotlib>=3.4.3", - "opencv-python>=4.5.3.56", - "pandas>=1.1.0", - "open-clip-torch>=2.23.0", -] - -[project.scripts] -otx = "otx.cli:main" - -[project.urls] -Documentation = "https://openvinotoolkit.github.io/training_extensions/" -Repository = "https://github.com/openvinotoolkit/training_extensions/" - -[tool.setuptools.dynamic] -version = {attr = "otx.__version__"} - -[tool.setuptools] -include-package-data = true - -[tool.setuptools.packages.find] -where = ["src"] -include = ["otx*"] # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # CIBUILDWHEEL CONFIGURATION. # [tool.cibuildwheel] -build = "cp39-manylinux_x86_64 cp310-manylinux_x86_64 cp311-manylinux_x86_64" +build = "cp38-manylinux_x86_64 cp39-manylinux_x86_64 cp310-manylinux_x86_64" # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # COVERAGE CONFIGURATION. # -[tool.coverage.paths] +[tool.coverage.run] source = [ - "src", + "src/otx/", ] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", +omit = [ + "src/otx/algorithms/*/configs/**/*pipeline*.py", + "src/otx/algorithms/*/configs/**/model*.py", + "src/otx/algorithms/*/configs/**/deployment.py", + "src/otx/algorithms/**/configs/**/backbones/*", + "src/otx/algorithms/**/*sample*.py", + "**/__init__.py", + "src/otx/recipes/*", ] # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # MYPY CONFIGURATION. # [tool.mypy] -python_version = "3.10" +python_version = 3.8 ignore_missing_imports = true show_error_codes = true +# TODO: Need to be edited +follow_imports = "skip" # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # RUFF CONFIGURATION # [tool.ruff] # Enable rules -select = [ - "F", # Pyflakes (`F`) - - # Enable all `pydocstyle` rules, limiting to those that adhere to the - # Google convention via `convention = "google"`, below. - "D", # pydocstyle (`D`) - "E", # pycodestyle error (`E`) - "W", # pycodestyle warning (`W`) - - "I", # isort (`I`) - "PL", # pylint (`PL`) - - "C90", # mccabe (`C90`) - "N", # pep8-naming (`N`) - "UP", # pyupgrade (`UP`) - - "YTT", # flake8-2020 (`YTT`) - "ANN", # flake8-annotations (`ANN`) - "S", # flake8-bandit (`S`) - # "BLE", # flake8-blind-except (`BLE`) -> Need to discuss new exception structure - "B", # flake8-bugbear (`B`) - "A", # flake8-builtins (`A`) - "COM", # flake8-commas (`COM`) - # "CPY", # flake8-copyright (`CPY`) -> Rules included in the preview version of RUFF. It may be added in the future, but for now, disable it. - "C4", # flake8-comprehensions (`C4`) - "DTZ", # flake8-datatimez (`DTZ`) - "T10", # flake8-debugger (`T10`) - "EM", # flake8-errmsg (`EM`) - "FA", # flake8-future-annotations (`FA`) - "ISC", # flake8-implicit-str-concat (`ISC`) - "ICN", # flake8-import-conventions (`ICN`) - "PIE", # flake8-pie (`PIE`) - "PT", # flake8-pytest-style (`PT`) - "RSE", # flake8-raise (`RSE`) - "RET", # flake8-return (`RET`) - "SLF", # flake8-self (`SLF`) - "SIM", # flake8-simplify (`SIM`) - "TID", # flake8-tidy-imports (`TID`) - "TCH", # flake8-type-checking (`TCH`) - "INT", # flake8-gettext (`INT`) - "ARG", # flake8-unsused-arguments (`ARG`) - "PTH", # flake8-use-pathlib (`PTH`) - "TD", # flake8-todos (`TD`) - "FIX", # flake8-fixme (`FIX`) - # "LOG", # flake8-logging (`LOG`) -> Rules included in the preview version of RUFF. It may be added in the future, but for now, disable it. - - "ERA", # eradicate (`ERA`) - "PD", # pandas-vet (`PD`) - "PGH", # pygrep-hooks (`PGH`) - "TRY", # tryceratos (`TRY`) - "FLY", # flynt (`FLY`) - "NPY", # NumPy-specific rules (`NPY`) - "PERF", # Perflint (`PERF`) - # "FURB", # refurb (`FURB`) -> Rules included in the preview version of RUFF. It may be added in the future, but for now, disable it. - "RUF", # Ruff-specific rules (`RUF`) -] +# pydocstyle (`D`) +# pycodestyle error (`E`) +# pycodestyle warning (`W`) +# Pyflakes (`F`) +# isort (`I`) +# pylint (`PL`) +select = ["D", "E", "F", "I", "W", "PL"] ignore = [ # pydocstyle - # On top of the Google convention, disable `D417`, which requires - # documentation for every function parameter. - "D417", # Missing argument descriptions in the docstring - - "D107", # Missing docstring in `__init__` - "D105", # Missing docstring in magic method - - # flake8-annotations - "ANN101", # Missing-type-self - "ANN002", # Missing type annotation for *args - "ANN003", # Missing type annotation for **kwargs + "D107", # Missing docstring in __init__ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D401", # First line should be in imperative mood; try rephrasing + "D404", # First word of the docstring should not be This + "D406", # Section name should end with a newline + "D407", # Missing dashed underline after section + "D413", # Missing blank line after last section + "D418", # Methods decorated with @overload shouldn't contain a docstring - "ANN102", # Missing type annotation for `cls` in classmethod - "ANN204", # Missing return type annotation for special method `__init__` - - "ARG002", # Unused method argument -> some function cannot use argument - - # flake8-type-checking - "TCH001", # typing-only-first-party-import, Sometimes this causes an incorrect error. - # flake8-fixme - "FIX002", # line-contains-todo + # pylint + "PLW2901" # Redefine loop name ] # Allow autofix for all enabled rules (when `--fix`) is provided. -fixable = ["ALL"] -# fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"] -# unfixable = [ -# "F401", # disable autofix for unused imports -# ] +fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"] +unfixable = [ + "F401", # disable autofix for unused imports +] # Exclude a variety of commonly ignored directories. exclude = [ @@ -264,16 +103,11 @@ exclude = [ "dist", "node_modules", "venv", - "tests/assets", - "src/otx/algo/**/mmdeploy/", - - # it will be cleaned up later - "src/otx/algo/classification/backbones/*", - "for_developers/helpers.py", +] - # Ruff complains it but don't know how to fix since it literally showed no useful logs. - # https://github.com/openvinotoolkit/training_extensions/actions/runs/7176557723/job/19541622452?pr=2718#step:5:170 - "tests/regression/*.py", +# Extended exclude for the project specific +extend-exclude = [ + "src/otx/api" ] # Same as Black. @@ -285,43 +119,15 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # minimum target version target-version = "py38" +format = "grouped" + [tool.ruff.mccabe] # Unlike Flake8, default to a complexity level of 10. -max-complexity = 20 +max-complexity = 10 [tool.ruff.pylint] allow-magic-value-types = ["str", "bytes", "int", "float"] max-args = 20 -max-branches = 50 -max-statements = 150 +max-branches = 25 +max-statements = 120 max-returns = 10 - -[tool.ruff.per-file-ignores] -# Declare an additional exclude rule for test code -"tests/**/*.py" = [ - "S101", # pytest-style allows `assert` statements in tests. - "SLF001", # We sometimes need to inspect private functions for testing. - "TCH003", # It doesn't seem necessary to use TYPE_CHECKING in tests. - "PT004", # fixture ignore type returning. - "E501", # Test skips lines that are too long. - "ANN001", # Skip annotation type hint in test codes - "ANN201", # Skip return type hint in test codes - "D", # Test skips missing docstring argument with magic (fixture) methods. -] -"src/otx/**/*.py" = [ - "ERA001", -] -# See https://github.com/openvinotoolkit/training_extensions/actions/runs/7109500350/job/19354528819?pr=2700 -"src/otx/core/config/**/*.py" = [ - "UP007" -] - -[tool.ruff.pydocstyle] -convention = "google" - -[tool.pytest.ini_options] -# TODO: Add cpu when OTX can run integration test parallelly for each task. -markers = [ - "gpu: mark tests which require NVIDIA GPU device", - # "cpu: mark tests which require CPU device", -] diff --git a/requirements/action.txt b/requirements/action.txt new file mode 100644 index 00000000000..070d2c8ff49 --- /dev/null +++ b/requirements/action.txt @@ -0,0 +1,6 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Action Requirements. +mmcv-full==1.7.0 +mmaction2==0.24.1 +mmdet==2.28.1 +mmdeploy==0.14.0 diff --git a/requirements/anomaly.txt b/requirements/anomaly.txt new file mode 100644 index 00000000000..4444d49f95b --- /dev/null +++ b/requirements/anomaly.txt @@ -0,0 +1,4 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Anomaly Requirements. +anomalib==0.5.1 +kornia==0.6.9 diff --git a/requirements/api.txt b/requirements/api.txt new file mode 100644 index 00000000000..0274aacd43e --- /dev/null +++ b/requirements/api.txt @@ -0,0 +1,12 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# API Requirements. # +attrs==23.1.* +networkx>=2.6,<=2.8.0 +numpy>=1.21.0,<=1.23.4 # np.bool was removed in 1.24.0 which was used in openvino runtime +omegaconf>=2.1.1 +opencv-python>=4.5 +pymongo==4.6.* +scikit-learn==1.3.* +Shapely>=1.7.1,<=1.8.0 +imagesize==1.4.1 +dill==0.3.* diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 00000000000..9a4099a6162 --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,14 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Base Algo Requirements. # +natsort==8.1.* +prettytable==3.9.* +protobuf==3.20.* +pyyaml +datumaro==1.6.0rc1 +psutil==5.9.* +scipy==1.10.* +bayesian-optimization==1.4.* +tensorboard==2.15.*; python_version >= '3.9' +tensorboard==2.14.*; python_version < '3.9' +multiprocess==0.70.* +pynvml==11.* diff --git a/requirements/classification.txt b/requirements/classification.txt new file mode 100644 index 00000000000..2256a3e755a --- /dev/null +++ b/requirements/classification.txt @@ -0,0 +1,7 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Classification Requirements. +mmcv-full==1.7.0 +mmcls==0.25.0 +timm==0.6.12 +mmdeploy==0.14.0 +pytorchcv==0.0.67 diff --git a/requirements/detection.txt b/requirements/detection.txt new file mode 100644 index 00000000000..a2d59b4d6a3 --- /dev/null +++ b/requirements/detection.txt @@ -0,0 +1,10 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Detection Requirements. +mmcv-full==1.7.0 +mmdet==2.28.1 +pytorchcv==0.0.67 +mmcls==0.25.0 +timm==0.6.12 +mmdeploy==0.14.0 +mmengine==0.7.4 +scikit-image # specifying different version w.r.t python_version is not effect diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 00000000000..dc269216630 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,14 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Development Requirements. # +pre-commit==2.20.0 +pylint==3.0.* +pytest==7.4.* +coverage==7.4.* +pytest-timeout==2.2.* +pytest-mock==3.12.* +onnx==1.15.0 +onnxruntime==1.14.1 +pytest-csv==3.0.* +tox==4.11.* +mlflow==2.10.2 +py-cpuinfo==9.0.0 diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 00000000000..a67fe82475a --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,10 @@ +furo==2023.3.* +myst-parser==2.0.* +sphinx==6.2.1 +pydata-sphinx-theme==0.15.2 +sphinx-tabs==3.4.* +sphinx-panels==0.4.* +sphinx-copybutton==0.5.0 +sphinx-autoapi==2.1.* +sphinxemoji==0.2.* +nbsphinx==0.9.* diff --git a/requirements/gh-actions.txt b/requirements/gh-actions.txt new file mode 100644 index 00000000000..33029eb1409 --- /dev/null +++ b/requirements/gh-actions.txt @@ -0,0 +1,45 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --generate-hashes --output-file=requirements.txt requirements/gh-actions.txt +# +build==1.0.3 \ + --hash=sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b \ + --hash=sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f + # via pip-tools +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de + # via pip-tools +packaging==23.2 \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 + # via build +pip-tools==7.4.0 \ + --hash=sha256:a92a6ddfa86ff389fe6ace381d463bc436e2c705bd71d52117c25af5ce867bb7 \ + --hash=sha256:b67432fd0759ed834c5367f9e0ce8c95441acecfec9c8e24b41aca166757adf0 + # via -r requirements/gh-actions.txt +pyproject-hooks==1.0.0 \ + --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \ + --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5 + # via + # build + # pip-tools +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via + # build + # pip-tools + # pyproject-hooks +wheel==0.42.0 \ + --hash=sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d \ + --hash=sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8 + # via pip-tools + +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes and the requirement is not +# satisfied by a package already installed. Consider using the --allow-unsafe flag. +# pip +# setuptools \ No newline at end of file diff --git a/requirements/openvino.txt b/requirements/openvino.txt new file mode 100644 index 00000000000..59da5fbdbec --- /dev/null +++ b/requirements/openvino.txt @@ -0,0 +1,8 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# OpenVINO Requirements. # +nncf==2.7.0 +onnx==1.15.0 +openvino-model-api==0.1.9 +openvino==2023.3.0 +openvino-dev==2023.3.0 +openvino-telemetry==2023.2.* diff --git a/requirements/publish.txt b/requirements/publish.txt new file mode 100644 index 00000000000..b65c65c1d86 --- /dev/null +++ b/requirements/publish.txt @@ -0,0 +1,2 @@ +build==1.0.3 +twine==5.0.0 diff --git a/requirements/segmentation.txt b/requirements/segmentation.txt new file mode 100644 index 00000000000..48ccce21381 --- /dev/null +++ b/requirements/segmentation.txt @@ -0,0 +1,9 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Segmentation Requirements. +mmcv-full==1.7.0 +mmsegmentation==0.30.0 +scikit-image # specifying different version w.r.t python_version is not effect +mmdeploy==0.14.0 +timm==0.6.12 +pytorchcv==0.0.67 +einops==0.6.1 diff --git a/requirements/visual_prompting.txt b/requirements/visual_prompting.txt new file mode 100644 index 00000000000..ba53e936a7a --- /dev/null +++ b/requirements/visual_prompting.txt @@ -0,0 +1,5 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Visual Prompting Requirements. +scikit-image # specifying different version w.r.t python_version is not effect +pytorch-lightning>=1.7.0,<1.10.0 +timm==0.6.12 diff --git a/setup.py b/setup.py new file mode 100644 index 00000000000..c0b280e507d --- /dev/null +++ b/setup.py @@ -0,0 +1,226 @@ +"""Setup file for OTX.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# ruff: noqa + +import os +import platform +from collections import defaultdict +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path +from typing import List, Union + +import numpy +from Cython.Build import cythonize +from Cython.Distutils import build_ext +from distutils.extension import Extension +from pkg_resources import Requirement +from setuptools import find_packages, setup + +try: + from torch.utils.cpp_extension import BuildExtension + + cmd_class = {"build_ext_torch_cpp_ext": BuildExtension} +except ModuleNotFoundError: + cmd_class = {} + print("Skip building ext ops due to the absence of torch.") + +cmd_class["build_ext_cythonized"] = build_ext + +def readme(): + with open('README.md', encoding='utf-8') as f: + content = f.read() + return content + + +def load_module(name: str = "src/otx/__init__.py"): + """Load Python Module. + + Args: + name (str, optional): Name of the module to load. + Defaults to "src/otx/__init__.py". + """ + location = str(Path(__file__).parent / name) + spec = spec_from_file_location(name=name, location=location) + module = module_from_spec(spec) # type: ignore + spec.loader.exec_module(module) # type: ignore + return module + + +def get_otx_version() -> str: + """Get version from `otx.__init__`. + + Version is stored in the main __init__ module in `otx`. + The varible storing the version is `__version__`. This function + reads `__init__` file, checks `__version__ variable and return + the value assigned to it. + + Example: + >>> # Assume that __version__ = "0.2.6" + >>> get_otx_version() + "0.2.6" + + Returns: + str: `otx` version. + """ + otx = load_module(name="src/otx/__init__.py") + return otx.__version__ + + +def get_requirements(requirement_files: Union[str, List[str]]) -> List[str]: + """Get packages from requirements.txt file. + + This function returns list of required packages from requirement files. + + Args: + requirement_files (List[str]): txt files that contains list of required + packages. + + Example: + >>> get_requirements(requirement_files=["openvino"]) + ['onnx>=1.8.1', 'networkx~=2.5', 'openvino-dev==2021.4.1', ...] + + Returns: + List[str]: List of required packages + """ + if isinstance(requirement_files, str): + requirement_files = [requirement_files] + + requirements: List[str] = [] + for requirement_file in requirement_files: + with open(f"requirements/{requirement_file}.txt", "r", encoding="UTF-8") as file: + for line in file: + package = line.strip() + if package and not package.startswith(("#", "-f")): + Requirement.parse(package) + requirements.append(package) + + return requirements + + +def get_extensions(): + if platform.system() == "Windows": + return [] + + def _cython_modules(): + cython_files = [ + "src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/pil_augment.pyx", + "src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/cv_augment.pyx" + ] + + ext_modules = [ + Extension( + cython_file.rstrip(".pyx").lstrip("src/").replace("/", "."), + [cython_file], + include_dirs=[numpy.get_include()], + extra_compile_args=["-O3"], + ) + for cython_file in cython_files + ] + + return cythonize(ext_modules) + + extensions = [] + extensions.extend(_cython_modules()) + return extensions + + +REQUIRED_PACKAGES = get_requirements(requirement_files="api") +EXTRAS_REQUIRE = { + "action": get_requirements(requirement_files=[ + "base", "openvino", "action", + ] + ), + "anomaly": get_requirements(requirement_files=[ + "base", "openvino", "anomaly", + ] + ), + "classification": get_requirements(requirement_files=[ + "base", "openvino", "classification", + ] + ), + "detection": get_requirements(requirement_files=[ + "base", "openvino", "detection", + ] + ), + "segmentation": get_requirements(requirement_files=[ + "base", "openvino", "segmentation", + ] + ), + "visual_prompting": get_requirements(requirement_files=[ + "base", "openvino", "visual_prompting", + ] + ), + "full": get_requirements(requirement_files=[ + "base", + "openvino", + "anomaly", + "classification", + "detection", + "segmentation", + "visual_prompting", + "action", + ] + ), +} + + +def find_yaml_recipes(): + """Find YAML recipe files in the package.""" + results = defaultdict(list) + + for root, _, files in os.walk("src/otx"): + module = ".".join(root.split(os.sep)) + for file in files: + _, ext = os.path.splitext(file) + if ext in [".yaml", ".json"]: + results[module] += [file] + + return results + + +package_data = {"": ["README.md", "LICENSE", "py.typed"]} +package_data.update(find_yaml_recipes()) + +setup( + name="otx", + version=get_otx_version(), + description="OpenVINO™ Training Extensions: " + "Train, Evaluate, Optimize, Deploy Computer Vision Models via OpenVINO™", + long_description=readme(), + long_description_content_type="text/markdown", + author="OpenVINO™ Training Extensions Contributors", + url="https://github.com/openvinotoolkit/training_extensions", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Cython", + ], + license="Apache License 2.0", + packages=find_packages(where="src", include=["otx*"]), + package_dir={"": "src"}, + package_data=package_data, + include_package_data=True, + ext_modules=get_extensions(), + cmdclass=cmd_class, + install_requires=REQUIRED_PACKAGES, + extras_require=EXTRAS_REQUIRE, + entry_points={ + "console_scripts": [ + "otx=otx.cli.tools.cli:main", + "otx_demo=otx.cli.tools.demo:main", + "otx_eval=otx.cli.tools.eval:main", + "otx_export=otx.cli.tools.export:main", + "otx_find=otx.cli.tools.find:main", + "otx_train=otx.cli.tools.train:main", + "otx_optimize=otx.cli.tools.optimize:main", + "otx_build=otx.cli.tools.build:main", + ] + }, +) diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 02217c39745..ce673d0b5d0 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -1,18 +1,7 @@ """OpenVINO Training Extensions.""" -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "2.0.0" - - -OTX_LOGO: str = """ - - ██████╗ ████████╗ ██╗ ██╗ -██╔═══██╗ ╚══██╔══╝ ╚██╗██╔╝ -██║ ██║ ██║ ╚███╔╝ -██║ ██║ ██║ ██╔██╗ -╚██████╔╝ ██║ ██╔╝ ██╗ - ╚═════╝ ╚═╝ ╚═╝ ╚═╝ - -""" +__version__ = "1.6.0dev" +# NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/algo/__init__.py b/src/otx/algo/__init__.py deleted file mode 100644 index 968d579e5f7..00000000000 --- a/src/otx/algo/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX custom algorithms, e.g., model, losses, hook, etc...""" - -from . import action_classification, classification, detection, segmentation, visual_prompting - -__all__ = ["action_classification", "classification", "detection", "segmentation", "visual_prompting"] diff --git a/src/otx/algo/action_classification/__init__.py b/src/otx/algo/action_classification/__init__.py deleted file mode 100644 index d9f3b9c7159..00000000000 --- a/src/otx/algo/action_classification/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX action classification models.""" - -from .backbones import OTXMoViNet -from .heads import MoViNetHead -from .openvino_model import OTXOVActionCls -from .recognizers import MoViNetRecognizer, OTXRecognizer3D - -__all__ = ["OTXOVActionCls", "OTXRecognizer3D", "OTXMoViNet", "MoViNetHead", "MoViNetRecognizer"] diff --git a/src/otx/algo/action_classification/backbones/__init__.py b/src/otx/algo/action_classification/backbones/__init__.py deleted file mode 100644 index 044d3165fa4..00000000000 --- a/src/otx/algo/action_classification/backbones/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Custom backbones for action classification.""" - -from .movinet import OTXMoViNet - -__all__ = ["OTXMoViNet"] diff --git a/src/otx/algo/action_classification/backbones/movinet.py b/src/otx/algo/action_classification/backbones/movinet.py deleted file mode 100644 index 70b0a2975a1..00000000000 --- a/src/otx/algo/action_classification/backbones/movinet.py +++ /dev/null @@ -1,765 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Code modified from: https://github.com/Atze00/MoViNet-pytorch/blob/main/movinets/models.py.""" - -from __future__ import annotations - -from collections import OrderedDict -from typing import Callable - -import torch -import torch.nn.functional as F # noqa: N812 -from einops import rearrange -from mmaction.models import MODELS -from mmengine.config import Config -from torch import Tensor, nn -from torch.nn.modules.utils import _pair, _triple - - -class Conv2dBNActivation(nn.Sequential): - """A base module that applies a 2D Conv-BN-Activation. - - Args: - in_planes (int): Number of input channels. - out_planes (int): Number of output channels. - kernel_size (Union[int, tuple[int, int]]): Size of the convolution kernel. - padding (Union[int, tuple[int, int]]): Size of the padding applied to the input. - stride (Union[int, tuple[int, int]], optional): Stride of the convolution. Default: 1. - groups (int, optional): Number of groups in the convolution. Default: 1. - norm_layer (Optional[Callable[..., nn.Module]], optional): Normalization layer to use. - If None, identity is used. Default: None. - activation_layer (Optional[Callable[..., nn.Module]], optional): Activation layer to use. - If None, identity is used. Default: None. - **kwargs (Any): Additional keyword arguments passed to nn.Conv2d. - - Attributes: - kernel_size (tuple[int, int]): Size of the convolution kernel. - stride (tuple[int, int]): Stride of the convolution. - out_channels (int): Number of output channels. - - """ - - def __init__( - self, - in_planes: int, - out_planes: int, - *, - kernel_size: int | tuple[int, int], - padding: int | tuple[int, int], - stride: int | tuple[int, int] = 1, - groups: int = 1, - norm_layer: Callable[..., nn.Module] | None = None, - activation_layer: Callable[..., nn.Module] | None = None, - **kwargs, - ) -> None: - kernel_size = _pair(kernel_size) - stride = _pair(stride) - padding = _pair(padding) - if norm_layer is None: - norm_layer = nn.Identity - if activation_layer is None: - activation_layer = nn.Identity - self.kernel_size = kernel_size - self.stride = stride - dict_layers = OrderedDict( - { - "conv2d": nn.Conv2d( - in_planes, - out_planes, - kernel_size=kernel_size, - stride=stride, - padding=padding, - groups=groups, - **kwargs, - ), - "norm": norm_layer(out_planes, eps=0.001), - "act": activation_layer(), - }, - ) - - self.out_channels = out_planes - super().__init__(dict_layers) - - -class Conv3DBNActivation(nn.Sequential): - """A base module that applies a 3D Conv-BN-Activation. - - Args: - in_planes (int): Number of input channels. - out_planes (int): Number of output channels. - kernel_size (Union[int, tuple[int, int, int]]): Size of the convolution kernel. - padding (Union[int, tuple[int, int, int]]): Size of the padding applied to the input. - stride (Union[int, tuple[int, int, int]], optional): Stride of the convolution. Default: 1. - groups (int, optional): Number of groups in the convolution. Default: 1. - norm_layer (Optional[Callable[..., nn.Module]], optional): Normalization layer to use. - If None, identity is used. Default: None. - activation_layer (Optional[Callable[..., nn.Module]], optional): Activation layer to use. - If None, identity is used. Default: None. - **kwargs (Any): Additional keyword arguments passed to nn.Conv3d. - - Attributes: - kernel_size (tuple[int, int, int]): Size of the convolution kernel. - stride (tuple[int, int, int]): Stride of the convolution. - out_channels (int): Number of output channels. - - """ - - def __init__( - self, - in_planes: int, - out_planes: int, - *, - kernel_size: int | tuple[int, int, int], - padding: int | tuple[int, int, int], - stride: int | tuple[int, int, int] = 1, - groups: int = 1, - norm_layer: Callable[..., nn.Module] | None = None, - activation_layer: Callable[..., nn.Module] | None = None, - **kwargs, - ) -> None: - kernel_size = _triple(kernel_size) - stride = _triple(stride) - padding = _triple(padding) - if norm_layer is None: - norm_layer = nn.Identity - if activation_layer is None: - activation_layer = nn.Identity - self.kernel_size = kernel_size - self.stride = stride - - dict_layers = OrderedDict( - { - "conv3d": nn.Conv3d( - in_planes, - out_planes, - kernel_size=kernel_size, - stride=stride, - padding=padding, - groups=groups, - **kwargs, - ), - "norm": norm_layer(out_planes, eps=0.001), - "act": activation_layer(), - }, - ) - - self.out_channels = out_planes - super().__init__(dict_layers) - - -class ConvBlock3D(nn.Module): - """A module that applies a 2+1D or 3D Conv-BN-activation sequential. - - Args: - in_planes (int): Number of input channels. - out_planes (int): Number of output channels. - kernel_size (tuple[int, int, int]): Size of the convolution kernel. - tf_like (bool): Whether to use TensorFlow-like padding and convolution. - conv_type (str): Type of 3D convolution to use. Must be "2plus1d" or "3d". - padding (tuple[int, int, int], optional): Size of the padding applied to the input. - Default: (0, 0, 0). - stride (tuple[int, int, int], optional): Stride of the convolution. Default: (1, 1, 1). - norm_layer (Optional[Callable[..., nn.Module]], optional): Normalization layer to use. - If None, identity is used. Default: None. - activation_layer (Optional[Callable[..., nn.Module]], optional): Activation layer to use. - If None, identity is used. Default: None. - bias (bool, optional): Whether to use bias in the convolution. Default: False. - **kwargs (Any): Additional keyword arguments passed to nn.Conv2d or nn.Conv3d. - - Attributes: - conv_1 (Union[Conv2dBNActivation, Conv3DBNActivation]): Convolutional layer. - conv_2 (Optional[Conv2dBNActivation]): Convolutional layer for 2+1D convolution. - padding (tuple[int, int, int]): Size of the padding applied to the input. - kernel_size (tuple[int, int, int]): Size of the convolution kernel. - dim_pad (int): Padding along the temporal dimension. - stride (tuple[int, int, int]): Stride of the convolution. - conv_type (str): Type of 3D convolution used. - tf_like (bool): Whether to use TensorFlow-like padding and convolution. - - """ - - def __init__( - self, - in_planes: int, - out_planes: int, - kernel_size: tuple[int, int, int], - tf_like: bool, - conv_type: str, - padding: tuple[int, int, int] = (0, 0, 0), - stride: tuple[int, int, int] = (1, 1, 1), - norm_layer: Callable[..., nn.Module] | None = None, - activation_layer: Callable[..., nn.Module] | None = None, - bias: bool = False, - **kwargs, - ) -> None: - super().__init__() - self.conv_2 = None - if tf_like: - # We need odd kernel to have even padding - # and stride == 1 to precompute padding, - if kernel_size[0] % 2 == 0: - raise ValueError("tf_like supports only odd" + " kernels for temporal dimension") - padding = ((kernel_size[0] - 1) // 2, 0, 0) - if stride[0] != 1: - raise ValueError("illegal stride value, tf like supports" + " only stride == 1 for temporal dimension") - if stride[1] > kernel_size[1] or stride[2] > kernel_size[2]: - # these values are not tested so should be avoided - raise ValueError("tf_like supports only" + " stride <= of the kernel size") - - if conv_type not in ["2plus1d", "3d"]: - raise ValueError("only 2plus2d or 3d are " + "allowed as 3d convolutions") - - if conv_type == "2plus1d": - self.conv_1 = Conv2dBNActivation( - in_planes, - out_planes, - kernel_size=(kernel_size[1], kernel_size[2]), - padding=(padding[1], padding[2]), - stride=(stride[1], stride[2]), - activation_layer=activation_layer, - norm_layer=norm_layer, - bias=bias, - **kwargs, - ) - if kernel_size[0] > 1: - self.conv_2 = Conv2dBNActivation( - in_planes, - out_planes, - kernel_size=(kernel_size[0], 1), - padding=(padding[0], 0), - stride=(stride[0], 1), - activation_layer=activation_layer, - norm_layer=norm_layer, - bias=bias, - **kwargs, - ) - elif conv_type == "3d": - self.conv_1 = Conv3DBNActivation( - in_planes, - out_planes, - kernel_size=kernel_size, - padding=padding, - activation_layer=activation_layer, - norm_layer=norm_layer, - stride=stride, - bias=bias, - **kwargs, - ) - self.padding = padding - self.kernel_size = kernel_size - self.dim_pad = self.kernel_size[0] - 1 - self.stride = stride - self.conv_type = conv_type - self.tf_like = tf_like - - def _forward(self, x: Tensor) -> Tensor: - shape_with_buffer = x.shape - if self.conv_type == "2plus1d": - x = rearrange(x, "b c t h w -> (b t) c h w") - x = self.conv_1(x) - if self.conv_type == "2plus1d": - x = rearrange(x, "(b t) c h w -> b c t h w", t=shape_with_buffer[2]) - if self.conv_2 is not None: - w = x.shape[-1] - x = rearrange(x, "b c t h w -> b c t (h w)") - x = self.conv_2(x) - x = rearrange(x, "b c t (h w) -> b c t h w", w=w) - return x - - def forward(self, x: Tensor) -> Tensor: - """Forward function of ConvBlock3D.""" - if self.tf_like: - x = same_padding( - x, - x.shape[-2], - x.shape[-1], - self.stride[-2], - self.stride[-1], - self.kernel_size[-2], - self.kernel_size[-1], - ) - return self._forward(x) - - -class SqueezeExcitation(nn.Module): - """Implements the Squeeze-and-Excitation (SE) block. - - Args: - input_channels (int): Number of input channels. - activation_2 (nn.Module): Activation function applied after the second convolutional block. - activation_1 (nn.Module): Activation function applied after the first convolutional block. - conv_type (str): Convolutional block type ("2plus1d" or "3d"). - squeeze_factor (int, optional): The reduction factor for the number of channels (default: 4). - bias (bool, optional): Whether to add a bias term to the convolutional blocks (default: True). - """ - - def __init__( - self, - input_channels: int, - activation_2: nn.Module, - activation_1: nn.Module, - conv_type: str, - squeeze_factor: int = 4, - bias: bool = True, - ) -> None: - super().__init__() - se_multiplier = 1 - squeeze_channels = _make_divisible(input_channels // squeeze_factor * se_multiplier, 8) - self.fc1 = ConvBlock3D( - input_channels * se_multiplier, - squeeze_channels, - kernel_size=(1, 1, 1), - padding=(0, 0, 0), - tf_like=False, - conv_type=conv_type, - bias=bias, - ) - self.activation_1 = activation_1() - self.activation_2 = activation_2() - self.fc2 = ConvBlock3D( - squeeze_channels, - input_channels, - kernel_size=(1, 1, 1), - padding=(0, 0, 0), - tf_like=False, - conv_type=conv_type, - bias=bias, - ) - - def _scale(self, x: Tensor) -> Tensor: - """Computes the scaling factor for the input tensor. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, channels, time, height, width). - - Returns: - torch.Tensor: Scaling factor for the input tensor of shape (batch_size, channels, 1, 1, 1). - """ - scale = F.adaptive_avg_pool3d(x, 1) - scale = self.fc1(scale) - scale = self.activation_1(scale) - scale = self.fc2(scale) - return self.activation_2(scale) - - def forward(self, x: Tensor) -> Tensor: - """Forward function of SqueezeExcitation.""" - scale = self._scale(x) - return scale * x - - -def _make_divisible(value: float, divisor: int, min_value: int | None = None) -> int: - if min_value is None: - min_value = divisor - new_v = max(min_value, int(value + divisor / 2) // divisor * divisor) - # Make sure that round down does not go down by more than 10%. - if new_v < 0.9 * value: - new_v += divisor - return new_v - - -def same_padding( - x: Tensor, - in_height: int, - in_width: int, - stride_h: int, - stride_w: int, - filter_height: int, - filter_width: int, -) -> Tensor: - """Applies padding to the input tensor to ensure that the output tensor size is the same as the input tensor size. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, channels, time, height, width). - in_height (int): Height of the input tensor. - in_width (int): Width of the input tensor. - stride_h (int): Stride in the height dimension. - stride_w (int): Stride in the width dimension. - filter_height (int): Height of the filter (kernel). - filter_width (int): Width of the filter (kernel). - - Returns: - torch.Tensor: Padded tensor of shape (batch_size, channels, time, height + pad_h, width + pad_w), where - pad_h and pad_w are the heights and widths of the top, bottom, left, and right padding applied to the tensor. - - """ - if in_height % stride_h == 0: - pad_along_height = max(filter_height - stride_h, 0) - else: - pad_along_height = max(filter_height - (in_height % stride_h), 0) - if in_width % stride_w == 0: - pad_along_width = max(filter_width - stride_w, 0) - else: - pad_along_width = max(filter_width - (in_width % stride_w), 0) - pad_top = pad_along_height // 2 - pad_bottom = pad_along_height - pad_top - pad_left = pad_along_width // 2 - pad_right = pad_along_width - pad_left - padding_pad = (pad_left, pad_right, pad_top, pad_bottom) - return torch.nn.functional.pad(x, padding_pad) - - -class TFAvgPool3D(nn.Module): - """3D average pooling layer with padding.""" - - def __init__(self) -> None: - super().__init__() - self.avgf = nn.AvgPool3d((1, 3, 3), stride=(1, 2, 2)) - - def forward(self, x: Tensor) -> Tensor: - """Applies 3D average pooling with padding to the input tensor. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, channels, time, height, width). - - Returns: - torch.Tensor: Pooled tensor of shape (batch_size, channels, time, height', width'), where - height' and width' are the heights and widths of the pooled tensor after padding is applied. - - """ - use_padding = x.shape[-1] % 2 != 0 - padding_pad = (0, 0, 0, 0) if use_padding else (0, 1, 0, 1) - x = torch.nn.functional.pad(x, padding_pad) - if use_padding: - x = torch.nn.functional.avg_pool3d( - x, - (1, 3, 3), - stride=(1, 2, 2), - count_include_pad=False, - padding=(0, 1, 1), - ) - else: - x = self.avgf(x) - x[..., -1] = x[..., -1] * 9 / 6 - x[..., -1, :] = x[..., -1, :] * 9 / 6 - return x - - -class BasicBneck(nn.Module): - """Basic bottleneck block of MoViNet network. - - Args: - cfg (Config): Configuration object containing block's hyperparameters. - tf_like (bool): A boolean indicating whether to use TensorFlow like convolution - padding or not. - conv_type (str): A string indicating the type of convolutional layer to use. - Can be "2d" or "3d". - norm_layer (Callable[..., nn.Module], optional): A callable normalization layer - to use. Defaults to None. - activation_layer (Callable[..., nn.Module], optional): A callable activation - layer to use. Defaults to None. - - Attributes: - expand (ConvBlock3D, optional): An optional expansion convolutional block. - deep (ConvBlock3D): A convolutional block with kernel size, stride, padding, - and groups as specified in the configuration object. - squeeze_excitation (SqueezeExcitation): A squeeze-and-excitation block. - project (ConvBlock3D): A projection convolutional block. - res (nn.Sequential, optional): An optional residual convolutional block. - alpha (nn.Parameter): A learnable parameter used in the ReZero operation. - """ - - def __init__( - self, - cfg: Config, - tf_like: bool, - conv_type: str, - norm_layer: Callable[..., nn.Module] | None = None, - activation_layer: Callable[..., nn.Module] | None = None, - ) -> None: - super().__init__() - self.res = None - - layers = [] - if cfg.expanded_channels != cfg.out_channels: - self.expand = ConvBlock3D( - in_planes=cfg.input_channels, - out_planes=cfg.expanded_channels, - kernel_size=(1, 1, 1), - padding=(0, 0, 0), - conv_type=conv_type, - tf_like=tf_like, - norm_layer=norm_layer, - activation_layer=activation_layer, - ) - self.deep = ConvBlock3D( - in_planes=cfg.expanded_channels, - out_planes=cfg.expanded_channels, - kernel_size=cfg.kernel_size, - padding=cfg.padding, - stride=cfg.stride, - groups=cfg.expanded_channels, - conv_type=conv_type, - tf_like=tf_like, - norm_layer=norm_layer, - activation_layer=activation_layer, - ) - self.se = SqueezeExcitation( - cfg.expanded_channels, - activation_1=activation_layer, - activation_2=(nn.Sigmoid if conv_type == "3d" else nn.Hardsigmoid), - conv_type=conv_type, - ) - self.project = ConvBlock3D( - cfg.expanded_channels, - cfg.out_channels, - kernel_size=(1, 1, 1), - padding=(0, 0, 0), - conv_type=conv_type, - tf_like=tf_like, - norm_layer=norm_layer, - activation_layer=nn.Identity, - ) - - if not (cfg.stride == (1, 1, 1) and cfg.input_channels == cfg.out_channels): - if cfg.stride != (1, 1, 1): - if tf_like: - layers.append(TFAvgPool3D()) - else: - layers.append(nn.AvgPool3d((1, 3, 3), stride=cfg.stride, padding=cfg.padding_avg)) - layers.append( - ConvBlock3D( - in_planes=cfg.input_channels, - out_planes=cfg.out_channels, - kernel_size=(1, 1, 1), - padding=(0, 0, 0), - norm_layer=norm_layer, - activation_layer=nn.Identity, - conv_type=conv_type, - tf_like=tf_like, - ), - ) - self.res = nn.Sequential(*layers) - # ReZero - self.alpha = nn.Parameter(torch.tensor(0.0), requires_grad=True) - - def forward(self, x: Tensor) -> Tensor: - """Forward function of BasicBneck.""" - residual = self.res(x) if self.res is not None else x - if hasattr(self, "expand"): - x = self.expand(x) - x = self.deep(x) - x = self.se(x) - x = self.project(x) - return residual + self.alpha * x - - -class MoViNet(nn.Module): - """MoViNet class used for video classification. - - Args: - cfg (Config): Configuration object containing network's hyperparameters. - conv_type (str, optional): A string indicating the type of convolutional layer - to use. Can be "2d" or "3d". Defaults to "3d". - tf_like (bool, optional): A boolean indicating whether to use TensorFlow like - convolution padding or not. Defaults to False. - - Attributes: - conv1 (ConvBlock3D): A convolutional block for the first layer. - blocks (nn.Sequential): A sequence of basic bottleneck blocks. - conv7 (ConvBlock3D): A convolutional block for the final layer. - - Methods: - avg(x: Tensor) -> Tensor: A static method that returns the adaptive average pool - of the input tensor. - _init_weights(module): A private method that initializes the weights of the network's - convolutional, batch normalization, and linear layers. - forward(x: Tensor) -> Tensor: The forward pass of the network. - - """ - - def __init__( - self, - cfg: Config, - conv_type: str = "3d", - tf_like: bool = False, - ) -> None: - super().__init__() - tf_like = True - blocks_dic = OrderedDict() - - norm_layer = nn.BatchNorm3d if conv_type == "3d" else nn.BatchNorm2d - activation_layer = nn.SiLU if conv_type == "3d" else nn.Hardswish - - self.conv1 = ConvBlock3D( - in_planes=cfg.conv1.input_channels, - out_planes=cfg.conv1.out_channels, - kernel_size=cfg.conv1.kernel_size, - stride=cfg.conv1.stride, - padding=cfg.conv1.padding, - conv_type=conv_type, - tf_like=tf_like, - norm_layer=norm_layer, - activation_layer=activation_layer, - ) - for i, block in enumerate(cfg.blocks): - for j, basicblock in enumerate(block): - blocks_dic[f"b{i}_l{j}"] = BasicBneck( - basicblock, - conv_type=conv_type, - tf_like=tf_like, - norm_layer=norm_layer, - activation_layer=activation_layer, - ) - self.blocks = nn.Sequential(blocks_dic) - self.conv7 = ConvBlock3D( - in_planes=cfg.conv7.input_channels, - out_planes=cfg.conv7.out_channels, - kernel_size=cfg.conv7.kernel_size, - stride=cfg.conv7.stride, - padding=cfg.conv7.padding, - conv_type=conv_type, - tf_like=tf_like, - norm_layer=norm_layer, - activation_layer=activation_layer, - ) - - def avg(self, x: Tensor) -> Tensor: - """Returns the adaptive average pool of the input tensor. - - Args: - x (Tensor): A tensor to be averaged. - - Returns: - Tensor: A tensor with the averaged values. - - """ - return F.adaptive_avg_pool3d(x, 1) - - @staticmethod - def _init_weights(module: nn.Module) -> None: - if isinstance(module, nn.Conv3d): - nn.init.kaiming_normal_(module.weight, mode="fan_out") - if module.bias is not None: - nn.init.zeros_(module.bias) - elif isinstance(module, (nn.BatchNorm3d, nn.BatchNorm2d, nn.GroupNorm)): - nn.init.ones_(module.weight) - nn.init.zeros_(module.bias) - elif isinstance(module, nn.Linear): - nn.init.normal_(module.weight, 0, 0.01) - nn.init.zeros_(module.bias) - - def forward(self, x: Tensor) -> Tensor: - """Forward function of MoViNet.""" - x = self.conv1(x) - x = self.blocks(x) - x = self.conv7(x) - return self.avg(x) - - def init_weights(self) -> None: - """Initializes the weights of network.""" - self.apply(self._init_weights) - - -@MODELS.register_module() -class OTXMoViNet(MoViNet): - """MoViNet wrapper class for OTX.""" - - def __init__(self, **kwargs): - cfg = Config() - cfg.name = "A0" - cfg.conv1 = Config() - OTXMoViNet.fill_conv(cfg.conv1, 3, 8, (1, 3, 3), (1, 2, 2), (0, 1, 1)) - - cfg.blocks = [ - [Config()], - [Config() for _ in range(3)], - [Config() for _ in range(3)], - [Config() for _ in range(4)], - [Config() for _ in range(4)], - ] - - # block 2 - OTXMoViNet.fill_se_config(cfg.blocks[0][0], 8, 8, 24, (1, 5, 5), (1, 2, 2), (0, 2, 2), (0, 1, 1)) - - # block 3 - OTXMoViNet.fill_se_config(cfg.blocks[1][0], 8, 32, 80, (3, 3, 3), (1, 2, 2), (1, 0, 0), (0, 0, 0)) - OTXMoViNet.fill_se_config(cfg.blocks[1][1], 32, 32, 80, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) - OTXMoViNet.fill_se_config(cfg.blocks[1][2], 32, 32, 80, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) - - # block 4 - OTXMoViNet.fill_se_config(cfg.blocks[2][0], 32, 56, 184, (5, 3, 3), (1, 2, 2), (2, 0, 0), (0, 0, 0)) - OTXMoViNet.fill_se_config(cfg.blocks[2][1], 56, 56, 112, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) - OTXMoViNet.fill_se_config(cfg.blocks[2][2], 56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) - - # block 5 - OTXMoViNet.fill_se_config(cfg.blocks[3][0], 56, 56, 184, (5, 3, 3), (1, 1, 1), (2, 1, 1), (0, 1, 1)) - OTXMoViNet.fill_se_config(cfg.blocks[3][1], 56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) - OTXMoViNet.fill_se_config(cfg.blocks[3][2], 56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) - OTXMoViNet.fill_se_config(cfg.blocks[3][3], 56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) - - # block 6 - OTXMoViNet.fill_se_config(cfg.blocks[4][0], 56, 104, 384, (5, 3, 3), (1, 2, 2), (2, 1, 1), (0, 1, 1)) - OTXMoViNet.fill_se_config(cfg.blocks[4][1], 104, 104, 280, (1, 5, 5), (1, 1, 1), (0, 2, 2), (0, 1, 1)) - OTXMoViNet.fill_se_config(cfg.blocks[4][2], 104, 104, 280, (1, 5, 5), (1, 1, 1), (0, 2, 2), (0, 1, 1)) - OTXMoViNet.fill_se_config(cfg.blocks[4][3], 104, 104, 344, (1, 5, 5), (1, 1, 1), (0, 2, 2), (0, 1, 1)) - - cfg.conv7 = Config() - OTXMoViNet.fill_conv(cfg.conv7, 104, 480, (1, 1, 1), (1, 1, 1), (0, 0, 0)) - - cfg.dense9 = Config({"hidden_dim": 2048}) - super().__init__(cfg) - - @staticmethod - def fill_se_config( - conf: Config, - input_channels: int, - out_channels: int, - expanded_channels: int, - kernel_size: tuple[int, int], - stride: tuple[int, int], - padding: tuple[int, int], - padding_avg: tuple[int, int], - ) -> None: - """Set the values of a given Config object to SE module. - - Args: - conf (Config): The Config object to be updated. - input_channels (int): The number of input channels. - out_channels (int): The number of output channels. - expanded_channels (int): The number of channels after expansion in the basic block. - kernel_size (tuple[int]): The size of the kernel. - stride (tuple[int]): The stride of the kernel. - padding (tuple[int]): The padding of the kernel. - padding_avg (tuple[int]): The padding for the average pooling operation. - - Returns: - None. - """ - conf.expanded_channels = expanded_channels - conf.padding_avg = padding_avg - OTXMoViNet.fill_conv( - conf, - input_channels, - out_channels, - kernel_size, - stride, - padding, - ) - - @staticmethod - def fill_conv( - conf: Config, - input_channels: int, - out_channels: int, - kernel_size: tuple[int, int], - stride: tuple[int, int], - padding: tuple[int, int], - ) -> None: - """Set the values of a given Config object to conv layer. - - Args: - conf (Config): The Config object to be updated. - input_channels (int): The number of input channels. - out_channels (int): The number of output channels. - kernel_size (tuple[int]): The size of the kernel. - stride (tuple[int]): The stride of the kernel. - padding (tuple[int]): The padding of the kernel. - - Returns: - None. - """ - conf.input_channels = input_channels - conf.out_channels = out_channels - conf.kernel_size = kernel_size - conf.stride = stride - conf.padding = padding diff --git a/src/otx/algo/action_classification/heads/__init__.py b/src/otx/algo/action_classification/heads/__init__.py deleted file mode 100644 index dee22c34408..00000000000 --- a/src/otx/algo/action_classification/heads/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Custom heads for action classification.""" - -from .movinet_head import MoViNetHead - -__all__ = ["MoViNetHead"] diff --git a/src/otx/algo/action_classification/mmconfigs/movinet.yaml b/src/otx/algo/action_classification/mmconfigs/movinet.yaml deleted file mode 100644 index da082719f11..00000000000 --- a/src/otx/algo/action_classification/mmconfigs/movinet.yaml +++ /dev/null @@ -1,26 +0,0 @@ -load_from: https://github.com/Atze00/MoViNet-pytorch/blob/main/weights/modelA0_statedict_v3?raw=true -backbone: - type: OTXMoViNet -cls_head: - average_clips: prob - in_channels: 480 - hidden_dim: 2048 - num_classes: 400 - loss_cls: - type: CrossEntropyLoss - loss_weight: 1.0 - type: MoViNetHead -data_preprocessor: - format_shape: NCTHW - mean: - - 0.0 - - 0.0 - - 0.0 - std: - - 255.0 - - 255.0 - - 255.0 - type: ActionDataPreprocessor -test_cfg: null -train_cfg: null -type: MoViNetRecognizer diff --git a/src/otx/algo/action_classification/mmconfigs/x3d.yaml b/src/otx/algo/action_classification/mmconfigs/x3d.yaml deleted file mode 100644 index 9535ae539a6..00000000000 --- a/src/otx/algo/action_classification/mmconfigs/x3d.yaml +++ /dev/null @@ -1,28 +0,0 @@ -load_from: https://download.openmmlab.com/mmaction/recognition/x3d/facebook/x3d_m_facebook_16x5x1_kinetics400_rgb_20201027-3f42382a.pth -backbone: - gamma_b: 2.25 - gamma_d: 2.2 - gamma_w: 1 - type: X3D -cls_head: - average_clips: prob - dropout_ratio: 0.5 - fc1_bias: false - in_channels: 432 - num_classes: 400 - spatial_type: avg - type: X3DHead -data_preprocessor: - format_shape: NCTHW - mean: - - 114.75 - - 114.75 - - 114.75 - std: - - 57.38 - - 57.38 - - 57.38 - type: ActionDataPreprocessor -test_cfg: null -train_cfg: null -type: OTXRecognizer3D diff --git a/src/otx/algo/action_classification/movinet.py b/src/otx/algo/action_classification/movinet.py deleted file mode 100644 index 766e543ca07..00000000000 --- a/src/otx/algo/action_classification/movinet.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""X3D model implementation.""" - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.action_classification import MMActionCompatibleModel - - -class MoViNet(MMActionCompatibleModel): - """MoViNet Model.""" - - def __init__(self, num_classes: int) -> None: - config = read_mmconfig("movinet") - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_action_ckpt(state_dict, add_prefix) diff --git a/src/otx/algo/action_classification/openvino_model.py b/src/otx/algo/action_classification/openvino_model.py deleted file mode 100644 index 348c3a7fb58..00000000000 --- a/src/otx/algo/action_classification/openvino_model.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Custom OpenVINO model wrappers for video recognition.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Callable - -import numpy as np -from openvino.model_api.adapters.utils import RESIZE_TYPES, InputTransform -from openvino.model_api.models.model import Model -from openvino.model_api.models.utils import ( - ClassificationResult, -) - -if TYPE_CHECKING: - from openvino.model_api.adapters import OpenvinoAdapter - - -def get_multiclass_predictions(logits: np.ndarray) -> ClassificationResult: - """Get multiclass predictions.""" - index = np.argmax(logits) - return ClassificationResult([(index, index, logits[index])], np.ndarray(0), np.ndarray(0), np.ndarray(0)) - - -class OTXOVActionCls(Model): - """OTX Action Classification model for openvino task.""" - - __model__ = "Action Classification" - - def __init__(self, model_adapter: OpenvinoAdapter, configuration: dict | None = None, preload: bool = False): - """Image model constructor. - - Calls the `Model` constructor first - """ - super().__init__(model_adapter, configuration, preload) - self.image_blob_names, self.image_info_blob_names = self._get_inputs() - self.image_blob_name = self.image_blob_names[0] - self.out_layer_name = self._get_outputs() - - _, self.n, self.c, self.t, self.h, self.w = self.inputs[self.image_blob_name].shape - self.resize = RESIZE_TYPES["standard"] - self.normalize = self._get_normalize_layer(model_adapter) - self.input_transform = InputTransform(False, None, None) - - self.interval = 4 - - def _get_inputs(self) -> tuple[list, list]: - image_blob_names, image_info_blob_names = [], [] - for name, metadata in self.inputs.items(): - if len(metadata.shape) == 6: - image_blob_names.append(name) - elif len(metadata.shape) == 4: - image_info_blob_names.append(name) - return image_blob_names, image_info_blob_names - - def _get_outputs(self) -> str: - layer_name = "logits" - for name, meta in self.outputs.items(): - if "logits" in meta.names: - layer_name = name - return layer_name - - @staticmethod - def _get_normalize_layer(model_adapter: OpenvinoAdapter) -> Callable | None: - model_info = None - for key in model_adapter.model.rt_info: - if key == "model_info": - model_info = model_adapter.model.rt_info["model_info"] - if model_info is None: - return None - scale_values = np.array( - model_adapter.model.rt_info["model_info"]["scale_values"].value.split(), - dtype=np.float64, - ) - mean_values = np.array( - model_adapter.model.rt_info["model_info"]["mean_values"].value.split(), - dtype=np.float64, - ) - return lambda x: (x - mean_values) / scale_values - - def preprocess(self, inputs: np.ndarray) -> tuple[dict, dict]: - """Pre-process.""" - meta = {"original_shape": inputs[0].shape} - frames = [] - for frame in inputs: - resized_frame = self.resize(frame, (self.w, self.h)) - if self.normalize: - resized_frame = self.normalize(resized_frame) - frames.append(resized_frame) - np_frames = self._reshape(frames) - dict_inputs = {self.image_blob_name: np_frames} - meta.update({"resized_shape": np_frames[0].shape}) - return dict_inputs, meta - - @staticmethod - def _reshape(inputs: list[np.ndarray]) -> np.ndarray: - """Reshape(expand, transpose, permute) the input np.ndarray.""" - np_inputs = np.expand_dims(inputs, axis=(0, 1)) # [1, 1, T, H, W, C] - return np_inputs.transpose(0, 1, -1, 2, 3, 4) # [1, 1, C, T, H, W] - - def postprocess(self, outputs: dict[str, np.ndarray], meta: dict[str, Any]) -> np.ndarray: - """Post-process.""" - logits = next(iter(outputs.values())).squeeze() - return get_multiclass_predictions(logits) diff --git a/src/otx/algo/action_classification/recognizers/__init__.py b/src/otx/algo/action_classification/recognizers/__init__.py deleted file mode 100644 index 196fb947b20..00000000000 --- a/src/otx/algo/action_classification/recognizers/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom 3D recognizers for OTX.""" - -from .movinet_recognizer import MoViNetRecognizer -from .recognizer import OTXRecognizer3D - -__all__ = ["OTXRecognizer3D", "MoViNetRecognizer"] diff --git a/src/otx/algo/action_classification/recognizers/movinet_recognizer.py b/src/otx/algo/action_classification/recognizers/movinet_recognizer.py deleted file mode 100644 index 2e0aaa3b3da..00000000000 --- a/src/otx/algo/action_classification/recognizers/movinet_recognizer.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""MoViNet Recognizer for OTX compatibility.""" - -import functools - -from mmaction.models import MODELS -from mmaction.models.recognizers.recognizer3d import Recognizer3D -from torch import nn - - -@MODELS.register_module() -class MoViNetRecognizer(Recognizer3D): - """MoViNet recognizer model framework for OTX compatibility.""" - - def __init__(self, **kwargs): - super().__init__(**kwargs) - # Hooks for redirect state_dict load/save - self._register_state_dict_hook(self.state_dict_hook) - self._register_load_state_dict_pre_hook(functools.partial(self.load_state_dict_pre_hook, self)) - - @staticmethod - def state_dict_hook(module: nn.Module, state_dict: dict, *args, **kwargs) -> None: # noqa: ARG004 - """Redirect model as output state_dict for OTX MoviNet compatibility.""" - for key in list(state_dict.keys()): - val = state_dict.pop(key) - new_key = key.replace("cls_head.", "") if "cls_head" in key else key.replace("backbone.", "") - state_dict[new_key] = val - - @staticmethod - def load_state_dict_pre_hook( - module: nn.Module, # noqa: ARG004 - state_dict: dict, - prefix: str, - *args, # noqa: ARG004 - **kwargs, # noqa: ARG004 - ) -> None: - """Redirect input state_dict to model for OTX model compatibility.""" - for key in list(state_dict.keys()): - val = state_dict.pop(key) - new_key = ( - key.replace("classifier", "cls_head.classifier") - if "classifier" in key - else prefix + "backbone." + key[len(prefix) :] - ) - state_dict[new_key] = val diff --git a/src/otx/algo/action_classification/recognizers/recognizer.py b/src/otx/algo/action_classification/recognizers/recognizer.py deleted file mode 100644 index 386752a6f15..00000000000 --- a/src/otx/algo/action_classification/recognizers/recognizer.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom 3D recognizer for OTX.""" - -import torch -from mmaction.models import MODELS -from mmaction.models.recognizers import Recognizer3D - - -@MODELS.register_module() -class OTXRecognizer3D(Recognizer3D): - """Custom 3d recognizer class for OTX. - - This is for patching forward function during export procedure. - """ - - def _forward(self, inputs: torch.Tensor, stage: str = "backbone", **kwargs) -> torch.Tensor: - """Network forward process for export procedure. - - Args: - inputs (torch.Tensor): Raw Inputs of the recognizer. - stage (str): Which stage to output the features. - - """ - feats, predict_kwargs = self.extract_feat(inputs, test_mode=True) - cls_scores = self.cls_head(feats, **predict_kwargs) - num_segs = cls_scores.shape[0] // inputs.shape[1] - return self.cls_head.average_clip(cls_scores, num_segs=num_segs) diff --git a/src/otx/algo/action_classification/x3d.py b/src/otx/algo/action_classification/x3d.py deleted file mode 100644 index 4cb19a04f05..00000000000 --- a/src/otx/algo/action_classification/x3d.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""X3D model implementation.""" - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.action_classification import MMActionCompatibleModel - - -class X3D(MMActionCompatibleModel): - """X3D Model.""" - - def __init__(self, num_classes: int) -> None: - config = read_mmconfig("x3d") - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_action_ckpt(state_dict, add_prefix) diff --git a/src/otx/algo/action_detection/__init__.py b/src/otx/algo/action_detection/__init__.py deleted file mode 100644 index 0510329eab3..00000000000 --- a/src/otx/algo/action_detection/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX action detection models.""" diff --git a/src/otx/algo/action_detection/mmconfigs/x3d_fastrcnn.yaml b/src/otx/algo/action_detection/mmconfigs/x3d_fastrcnn.yaml deleted file mode 100644 index a9e49c244c1..00000000000 --- a/src/otx/algo/action_detection/mmconfigs/x3d_fastrcnn.yaml +++ /dev/null @@ -1,51 +0,0 @@ -type: FastRCNN -_scope_: mmdet -init_cfg: - type: Pretrained - checkpoint: https://download.openmmlab.com/mmaction/recognition/x3d/facebook/x3d_m_facebook_16x5x1_kinetics400_rgb_20201027-3f42382a.pth -backbone: - type: mmaction.X3D - gamma_b: 2.25 - gamma_d: 2.2 - gamma_w: 1 -roi_head: - type: AVARoIHead - bbox_roi_extractor: - type: SingleRoIExtractor3D - roi_layer_type: RoIAlign - output_size: 8 - with_temporal_pool: true - bbox_head: - type: BBoxHeadAVA - background_class: true - in_channels: 432 - num_classes: 81 - multilabel: false - dropout_ratio: 0.5 -data_preprocessor: - type: ActionDataPreprocessor - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - format_shape: NCTHW -train_cfg: - rcnn: - assigner: - type: MaxIoUAssignerAVA - pos_iou_thr: 0.9 - neg_iou_thr: 0.9 - min_pos_iou: 0.9 - sampler: - type: RandomSampler - num: 32 - pos_fraction: 1 - neg_pos_ub: -1 - add_gt_as_proposals: true - pos_weight: 1.0 -test_cfg: - rcnn: null diff --git a/src/otx/algo/action_detection/x3d_fastrcnn.py b/src/otx/algo/action_detection/x3d_fastrcnn.py deleted file mode 100644 index e516f2912e6..00000000000 --- a/src/otx/algo/action_detection/x3d_fastrcnn.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""X3DFastRCNN model implementation.""" -from __future__ import annotations - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.action_detection import MMActionCompatibleModel - - -class X3DFastRCNN(MMActionCompatibleModel): - """X3D Model.""" - - def __init__(self, num_classes: int, topk: int | tuple[int]): - config = read_mmconfig("x3d_fastrcnn") - config.roi_head.bbox_head.topk = topk - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_action_ckpt(state_dict, add_prefix) diff --git a/src/otx/algo/anomaly/__init__.py b/src/otx/algo/anomaly/__init__.py deleted file mode 100644 index 6182b285077..00000000000 --- a/src/otx/algo/anomaly/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Module for anomaly OTX Models.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from .padim import Padim -from .stfpm import Stfpm - -__all__ = ["Padim", "Stfpm"] diff --git a/src/otx/algo/anomaly/padim.py b/src/otx/algo/anomaly/padim.py deleted file mode 100644 index 4d70e025423..00000000000 --- a/src/otx/algo/anomaly/padim.py +++ /dev/null @@ -1,42 +0,0 @@ -"""OTX Padim model.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from anomalib.models.image import Padim as AnomalibPadim - -from otx.core.model.entity.base import OTXModel -from otx.core.model.module.anomaly import OTXAnomaly - - -class Padim(OTXAnomaly, OTXModel, AnomalibPadim): - """OTX Padim model. - - Args: - backbone (str, optional): Feature extractor backbone. Defaults to "resnet18". - layers (list[str], optional): Feature extractor layers. Defaults to ["layer1", "layer2", "layer3"]. - pre_trained (bool, optional): Pretrained backbone. Defaults to True. - n_features (int | None, optional): Number of features. Defaults to None. - num_classes (int, optional): Anoamly don't use num_classes , - but OTXModel always receives num_classes, so need this. - """ - - def __init__( - self, - backbone: str = "resnet18", - layers: list[str] = ["layer1", "layer2", "layer3"], # noqa: B006 - pre_trained: bool = True, - n_features: int | None = None, - num_classes: int = 2, - ) -> None: - OTXAnomaly.__init__(self) - OTXModel.__init__(self, num_classes=num_classes) - AnomalibPadim.__init__( - self, - backbone=backbone, - layers=layers, - pre_trained=pre_trained, - n_features=n_features, - ) diff --git a/src/otx/algo/anomaly/stfpm.py b/src/otx/algo/anomaly/stfpm.py deleted file mode 100644 index f77b70c4dd0..00000000000 --- a/src/otx/algo/anomaly/stfpm.py +++ /dev/null @@ -1,46 +0,0 @@ -"""OTX STFPM model.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from anomalib.models.image.stfpm import Stfpm as AnomalibStfpm - -from otx.core.model.entity.base import OTXModel -from otx.core.model.module.anomaly import OTXAnomaly - -if TYPE_CHECKING: - from collections.abc import Sequence - - -class Stfpm(OTXAnomaly, OTXModel, AnomalibStfpm): - """OTX STFPM model. - - Args: - layers (Sequence[str]): Feature extractor layers. - backbone (str, optional): Feature extractor backbone. Defaults to "resnet18". - num_classes (int, optional): Anoamly don't use num_classes , - but OTXModel always receives num_classes, so need this. - """ - - def __init__( - self, - layers: Sequence[str] = ["layer1", "layer2", "layer3"], - backbone: str = "resnet18", - num_classes: int = 2, - ) -> None: - OTXAnomaly.__init__(self) - OTXModel.__init__(self, num_classes=num_classes) - AnomalibStfpm.__init__( - self, - backbone=backbone, - layers=layers, - ) - - @property - def trainable_model(self) -> str: - """Used by configure optimizer.""" - return "student_model" diff --git a/src/otx/algo/callbacks/__init__.py b/src/otx/algo/callbacks/__init__.py deleted file mode 100644 index 81294c38a2f..00000000000 --- a/src/otx/algo/callbacks/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX custom callbacks.""" diff --git a/src/otx/algo/callbacks/adaptive_train_scheduling.py b/src/otx/algo/callbacks/adaptive_train_scheduling.py deleted file mode 100644 index 2d40347123b..00000000000 --- a/src/otx/algo/callbacks/adaptive_train_scheduling.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""Callback to reschedule the validation interval adaptively.""" - -from __future__ import annotations - -import logging as log -import math -from functools import partial -from typing import TYPE_CHECKING - -from lightning import Callback - -if TYPE_CHECKING: - from lightning import LightningModule, Trainer - from lightning.pytorch.utilities.types import LRSchedulerConfig, LRSchedulerTypeUnion - - -class AdaptiveTrainScheduling(Callback): - """Adaptive Training Scheduling Hook. - - Depending on the size of iteration per epoch, adaptively update the validation interval and related values. - - Args: - max_interval: Maximum value of validation interval. - Defaults to 5. - decay: Parameter to control the interval. This value is set by manual manner. - Defaults to -0.025. - """ - - def __init__(self, max_interval: int = 5, decay: float = -0.025): - self.max_interval = max_interval - self.decay = decay - self.min_earlystop_interval = 3 - self.min_lrschedule_patience = 2 - self._saved_check_val_every_n_epoch: int | None = None - self._saved_log_every_n_steps: int | None = None - self._revert_lr_frequency: list = [] - self._revert_lr_patience: list = [] - self._revert_es_patience: list = [] - - def on_train_start(self, trainer: Trainer, pl_module: LightningModule) -> None: - """Execute this function at starting the train stage.""" - max_interval = min(self.max_interval, trainer.max_epochs) - iter_per_epoch = len(trainer.train_dataloader) - - adaptive_check_val_every_n_epoch = self._get_adaptive_interval( - iter_per_epoch=iter_per_epoch, - max_interval=max_interval, - ) - - if adaptive_check_val_every_n_epoch != trainer.check_val_every_n_epoch: - msg = ( - "You are using AdaptiveTrainScheduling hook. " - "This hook will temporarily update Trainer.check_val_every_n_epoch adaptively: " - f"{trainer.check_val_every_n_epoch} => {adaptive_check_val_every_n_epoch}" - ) - log.warning(msg) - - self._saved_check_val_every_n_epoch = trainer.check_val_every_n_epoch - trainer.check_val_every_n_epoch = adaptive_check_val_every_n_epoch - self._change_early_stopping_patience(trainer.callbacks, adaptive_check_val_every_n_epoch) - self._change_lr_scheduler_frequency(trainer.lr_scheduler_configs, adaptive_check_val_every_n_epoch) - - if iter_per_epoch < trainer.log_every_n_steps: - msg = ( - "Trainer.log_every_n_steps is higher than the number of iterations in a training epoch. " - "To ensure logging at the last batch, temporarily update Trainer.log_every_n_steps: " - f"{trainer.log_every_n_steps} => {iter_per_epoch}" - ) - log.warning(msg) - - self._saved_log_every_n_steps = trainer.log_every_n_steps - trainer.log_every_n_steps = iter_per_epoch - - def on_train_end(self, trainer: Trainer, pl_module: LightningModule) -> None: - """Execute this function at terminating the train stage.""" - if self._saved_check_val_every_n_epoch: - trainer.check_val_every_n_epoch = self._saved_check_val_every_n_epoch - self._saved_check_val_every_n_epoch = None - - if self._saved_log_every_n_steps: - trainer.log_every_n_steps = self._saved_log_every_n_steps - self._saved_log_every_n_steps = None - - if len(self._revert_lr_frequency) > 0 and len(self._revert_lr_patience) > 0: - for revert_f, revert_p in zip(self._revert_lr_frequency, self._revert_lr_patience): - revert_f() - revert_p() - - if len(self._revert_es_patience) > 0: - for revert_es in self._revert_es_patience: - revert_es() - - def _get_adaptive_interval(self, iter_per_epoch: int, max_interval: int) -> int: - """Get adaptive interval.""" - return max(round(math.exp(self.decay * iter_per_epoch) * max_interval), 1) - - def _change_lr_scheduler_frequency(self, lr_configs: list[LRSchedulerConfig], adaptive_interval: int) -> None: - """Change the frequency of LRscheduler. - - Since adaptive interval changes the validation interval, the frequency of LRscheduler also - should be changed according to the adaptive interval. - """ - - def _revert_frequency(config: LRSchedulerConfig, saved_frequency: int) -> None: - config.frequency = saved_frequency - - def _revert_patience(lr_scheduler: LRSchedulerTypeUnion, saved_patience: int) -> None: - lr_scheduler.patience = saved_patience - - for config in lr_configs: - if hasattr(config, "frequency") and hasattr(config, "interval") and config.interval == "epoch": - saved_frequency = config.frequency - config.frequency = adaptive_interval - msg = ( - "The frequency of LRscheduler will be changed due to the effect of adaptive interval: " - f"{saved_frequency} --> {adaptive_interval}." - ) - log.warning(msg) - self._revert_lr_frequency += [partial(_revert_frequency, config, saved_frequency)] - - if hasattr(config, "scheduler") and hasattr(config.scheduler, "patience"): - saved_patience = config.scheduler.patience - adjusted_patience = ( - max( - int((config.scheduler.patience + 1) / adaptive_interval), - self.min_lrschedule_patience, - ) - - 1 - ) - config.scheduler.patience = adjusted_patience - - msg = ( - "The patience of LRscheduler will be changed due to the effect of adaptive interval: " - f"{saved_patience} --> {adjusted_patience}." - ) - log.warning(msg) - self._revert_lr_patience += [partial(_revert_patience, config.scheduler, saved_patience)] - - def _change_early_stopping_patience(self, callbacks: list[Callback], adaptive_interval: int) -> None: - """Change the EarlyStopping patience to change the patience. - - Since adaptive interval changes the validation interval, the patience of early stopping also - should be changed according to the adaptive interval. - """ - - def _revert_func(callback: Callback, saved_patience: int) -> None: - callback.patience = saved_patience - - from lightning.pytorch.callbacks.early_stopping import EarlyStopping - - for callback in callbacks: - if isinstance(callback, EarlyStopping): - adjusted_patience = max(int(callback.patience / adaptive_interval), self.min_earlystop_interval) - msg = ( - "The patience of early stopping will be changed due to the effect of adaptive interval: " - f"{callback.patience} --> {adjusted_patience}." - ) - log.warning(msg) - - saved_patience = callback.patience - callback.patience = adjusted_patience - - self._revert_es_patience += [partial(_revert_func, callback, saved_patience)] diff --git a/src/otx/algo/classification/__init__.py b/src/otx/algo/classification/__init__.py deleted file mode 100644 index a33eaa5651c..00000000000 --- a/src/otx/algo/classification/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX classification models.""" - -from . import backbones, heads, losses -from .otx_dino_v2 import DINOv2, DINOv2RegisterClassifier - -__all__ = ["backbones", "heads", "losses", "DINOv2", "DINOv2RegisterClassifier"] diff --git a/src/otx/algo/classification/backbones/__init__.py b/src/otx/algo/classification/backbones/__init__.py deleted file mode 100644 index 3d18ab27083..00000000000 --- a/src/otx/algo/classification/backbones/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Backbone modules for OTX custom model.""" - -from .otx_efficientnet import OTXEfficientNet -from .otx_efficientnet_v2 import OTXEfficientNetV2 -from .otx_mobilenet_v3 import OTXMobileNetV3 - -__all__ = ["OTXEfficientNet", "OTXEfficientNetV2", "OTXMobileNetV3"] diff --git a/src/otx/algo/classification/backbones/otx_efficientnet.py b/src/otx/algo/classification/backbones/otx_efficientnet.py deleted file mode 100644 index 86bba072ae5..00000000000 --- a/src/otx/algo/classification/backbones/otx_efficientnet.py +++ /dev/null @@ -1,660 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""EfficientNet Module.""" - -import math -import os - -import torch.nn.functional as F -from mmcv.cnn import build_activation_layer -from mmcv.cnn.bricks import ConvModule -from mmengine.runner import load_checkpoint -from mmpretrain.registry import MODELS -from pytorchcv.models.model_store import download_model -from torch import nn -from torch.nn import init - -PRETRAINED_ROOT = "https://github.com/osmr/imgclsmob/releases/download/v0.0.364/" -pretrained_urls = { - "efficientnet_b0": PRETRAINED_ROOT + "efficientnet_b0-0752-0e386130.pth.zip", -} - - -def conv1x1_block( - in_channels, - out_channels, - stride=1, - padding=0, - groups=1, - bias=False, - use_bn=True, - bn_eps=1e-5, - activation="ReLU", -): - """Conv block.""" - return ConvModule( - in_channels=in_channels, - out_channels=out_channels, - kernel_size=1, - stride=stride, - padding=padding, - groups=groups, - bias=bias, - norm_cfg=(dict(type="BN", eps=bn_eps) if use_bn else None), - act_cfg=(dict(type=activation) if activation else None), - ) - - -def conv3x3_block( - in_channels, - out_channels, - stride=1, - padding=1, - dilation=1, - groups=1, - bias=False, - use_bn=True, - bn_eps=1e-5, - activation="ReLU", - IN_conv=False, -): - """Conv block.""" - return ConvModule( - in_channels=in_channels, - out_channels=out_channels, - kernel_size=3, - stride=stride, - padding=padding, - dilation=dilation, - groups=groups, - bias=bias, - norm_cfg=(dict(type="BN", eps=bn_eps) if use_bn else None), - act_cfg=(dict(type=activation) if activation else None), - ) - - -def dwconv3x3_block( - in_channels, - out_channels, - stride=1, - padding=1, - dilation=1, - bias=False, - use_bn=True, - bn_eps=1e-5, - activation="ReLU", -): - """Conv block.""" - return ConvModule( - in_channels=in_channels, - out_channels=out_channels, - kernel_size=3, - stride=stride, - padding=padding, - dilation=dilation, - groups=out_channels, - bias=bias, - norm_cfg=(dict(type="BN", eps=bn_eps) if use_bn else None), - act_cfg=(dict(type=activation) if activation else None), - ) - - -def dwconv5x5_block( - in_channels, - out_channels, - stride=1, - padding=2, - dilation=1, - bias=False, - use_bn=True, - bn_eps=1e-5, - activation="ReLU", -): - """Conv block.""" - return ConvModule( - in_channels=in_channels, - out_channels=out_channels, - kernel_size=5, - stride=stride, - padding=padding, - dilation=dilation, - groups=out_channels, - bias=bias, - norm_cfg=(dict(type="BN", eps=bn_eps) if use_bn else None), - act_cfg=(dict(type=activation) if activation else None), - ) - - -def round_channels(channels, divisor=8): - """Round weighted channel number (make divisible operation). - - Args: - channels : int or float. Original number of channels. - divisor : int, default 8. Alignment value. - """ - rounded_channels = max(int(channels + divisor / 2.0) // divisor * divisor, divisor) - if float(rounded_channels) < 0.9 * channels: - rounded_channels += divisor - return rounded_channels - - -def calc_tf_padding(x, kernel_size, stride=1, dilation=1): - """Calculate TF-same like padding size. - - Args: - x : tensor. Input tensor. - kernel_size : int. Convolution window size. - stride : int, default 1. Strides of the convolution. - dilation : int, default 1. Dilation value for convolution layer. - """ - height, width = x.size()[2:] - oh = math.ceil(height / stride) - ow = math.ceil(width / stride) - pad_h = max((oh - 1) * stride + (kernel_size - 1) * dilation + 1 - height, 0) - pad_w = max((ow - 1) * stride + (kernel_size - 1) * dilation + 1 - width, 0) - return pad_h // 2, pad_h - pad_h // 2, pad_w // 2, pad_w - pad_w // 2 - - -class SEBlock(nn.Module): - """Squeeze-and-Excitation block from 'Squeeze-and-Excitation Networks,'. - - https://arxiv.org/abs/1709.01507. - - Args: - channels : int. Number of channels. - reduction : int, default 16. Squeeze reduction value. - mid_channels : int or None, default None. Number of middle channels. - round_mid : bool, default False. Whether to round middle channel number (make divisible by 8). - use_conv : bool, default True. Whether to convolutional layers instead of fully-connected ones. - activation : function, or str, or nn.Module, default 'relu'. Activation function after the first convolution. - out_activation : function, or str, or nn.Module, Activation function after the last convolution. - """ - - def __init__( - self, - channels, - reduction=16, - mid_channels=None, - round_mid=False, - use_conv=True, - mid_activation="ReLU", - out_activation="Sigmoid", - ): - super().__init__() - self.use_conv = use_conv - if mid_channels is None: - mid_channels = channels // reduction if not round_mid else round_channels(float(channels) / reduction) - - self.pool = nn.AdaptiveAvgPool2d(output_size=1) - if use_conv: - self.conv1 = nn.Conv2d( - in_channels=channels, - out_channels=mid_channels, - kernel_size=1, - stride=1, - groups=1, - bias=True, - ) - else: - self.fc1 = nn.Linear(in_features=channels, out_features=mid_channels) - self.activ = build_activation_layer(dict(type=mid_activation)) - if use_conv: - self.conv2 = nn.Conv2d( - in_channels=mid_channels, - out_channels=channels, - kernel_size=1, - stride=1, - groups=1, - bias=True, - ) - else: - self.fc2 = nn.Linear(in_features=mid_channels, out_features=channels) - self.sigmoid = build_activation_layer(dict(type=out_activation)) - - def forward(self, x): - """Forward.""" - w = self.pool(x) - if not self.use_conv: - w = w.view(x.size(0), -1) - w = self.conv1(w) if self.use_conv else self.fc1(w) - w = self.activ(w) - w = self.conv2(w) if self.use_conv else self.fc2(w) - w = self.sigmoid(w) - if not self.use_conv: - w = w.unsqueeze(2).unsqueeze(3) - x = x * w - return x - - -class EffiDwsConvUnit(nn.Module): - """EfficientNet specific depthwise separable conv block/unit with BatchNorms and activations at each conv. - - Args: - in_channels : int. Number of input channels. - out_channels : int. Number of output channels. - stride : int or tuple/list of 2 int. Strides of the second convolution layer. - bn_eps : float. Small float added to variance in Batch norm. - activation : str. Name of activation function. - tf_mode : bool. Whether to use TF-like mode. - """ - - def __init__(self, in_channels, out_channels, stride, bn_eps, activation, tf_mode): - super().__init__() - self.tf_mode = tf_mode - self.residual = (in_channels == out_channels) and (stride == 1) - - self.dw_conv = dwconv3x3_block( - in_channels=in_channels, - out_channels=in_channels, - padding=(0 if tf_mode else 1), - bn_eps=bn_eps, - activation=activation, - ) - self.se = SEBlock(channels=in_channels, reduction=4, mid_activation=activation) - self.pw_conv = conv1x1_block( - in_channels=in_channels, - out_channels=out_channels, - bn_eps=bn_eps, - activation=None, - ) - - def forward(self, x): - """Forward.""" - if self.residual: - identity = x - if self.tf_mode: - x = F.pad(x, pad=calc_tf_padding(x, kernel_size=3)) - x = self.dw_conv(x) - x = self.se(x) - x = self.pw_conv(x) - if self.residual: - x = x + identity - return x - - -class EffiInvResUnit(nn.Module): - """EfficientNet inverted residual unit. - - Args: - in_channels : int. Number of input channels. - out_channels : int. Number of output channels. - kernel_size : int or tuple/list of 2 int. Convolution window size. - stride : int or tuple/list of 2 int. Strides of the second convolution layer. - exp_factor : int. Factor for expansion of channels. - se_factor : int. SE reduction factor for each unit. - bn_eps : float. Small float added to variance in Batch norm. - activation : str. Name of activation function. - tf_mode : bool. Whether to use TF-like mode. - """ - - def __init__( - self, - in_channels, - out_channels, - kernel_size, - stride, - exp_factor, - se_factor, - bn_eps, - activation, - tf_mode, - ): - super().__init__() - self.kernel_size = kernel_size - self.stride = stride - self.tf_mode = tf_mode - self.residual = (in_channels == out_channels) and (stride == 1) - self.use_se = se_factor > 0 - mid_channels = in_channels * exp_factor - dwconv_block_fn = dwconv3x3_block if kernel_size == 3 else (dwconv5x5_block if kernel_size == 5 else None) - - self.conv1 = conv1x1_block( - in_channels=in_channels, - out_channels=mid_channels, - bn_eps=bn_eps, - activation=activation, - ) - self.conv2 = dwconv_block_fn( - in_channels=mid_channels, - out_channels=mid_channels, - stride=stride, - padding=(0 if tf_mode else kernel_size // 2), - bn_eps=bn_eps, - activation=activation, - ) - if self.use_se: - self.se = SEBlock( - channels=mid_channels, - reduction=(exp_factor * se_factor), - mid_activation=activation, - ) - self.conv3 = conv1x1_block( - in_channels=mid_channels, - out_channels=out_channels, - bn_eps=bn_eps, - activation=None, - ) - - def forward(self, x): - """Forward.""" - if self.residual: - identity = x - x = self.conv1(x) - if self.tf_mode: - x = F.pad( - x, - pad=calc_tf_padding(x, kernel_size=self.kernel_size, stride=self.stride), - ) - x = self.conv2(x) - if self.use_se: - x = self.se(x) - x = self.conv3(x) - if self.residual: - x = x + identity - return x - - -class EffiInitBlock(nn.Module): - """EfficientNet specific initial block. - - Args: - in_channels : int. Number of input channels. - out_channels : int. Number of output channels. - bn_eps : float. Small float added to variance in Batch norm. - activation : str. Name of activation function. - tf_mode : bool. Whether to use TF-like mode. - """ - - def __init__(self, in_channels, out_channels, bn_eps, activation, tf_mode, IN_conv1): - super().__init__() - self.tf_mode = tf_mode - - self.conv = conv3x3_block( - in_channels=in_channels, - out_channels=out_channels, - stride=2, - padding=(0 if tf_mode else 1), - bn_eps=bn_eps, - activation=activation, - IN_conv=IN_conv1, - ) - - def forward(self, x): - """Forward.""" - if self.tf_mode: - x = F.pad(x, pad=calc_tf_padding(x, kernel_size=3, stride=2)) - x = self.conv(x) - return x - - -class EfficientNet(nn.Module): - """EfficientNet. - - Args: - channels : list of list of int. Number of output channels for each unit. - init_block_channels : int. Number of output channels for initial unit. - final_block_channels : int. Number of output channels for the final block of the feature extractor. - kernel_sizes : list of list of int. Number of kernel sizes for each unit. - strides_per_stage : list int. Stride value for the first unit of each stage. - expansion_factors : list of list of int. Number of expansion factors for each unit. - dropout_rate : float, default 0.2. Fraction of the input units to drop. Must be a number between 0 and 1. - tf_mode : bool, default False. Whether to use TF-like mode. - bn_eps : float, default 1e-5. Small float added to variance in Batch norm. - in_channels : int, default 3. Number of input channels. - in_size : tuple of two ints, default (224, 224). Spatial size of the expected input image. - num_classes : int, default 1000. Number of classification classes. - """ - - def __init__( - self, - channels, - init_block_channels, - final_block_channels, - kernel_sizes, - strides_per_stage, - expansion_factors, - tf_mode=False, - bn_eps=1e-5, - in_channels=3, - in_size=(224, 224), - dropout_cls=None, - pooling_type="avg", - bn_eval=False, - bn_frozen=False, - IN_first=False, - IN_conv1=False, - pretrained=False, - **kwargs, - ): - - super().__init__(**kwargs) - self.num_classes = 1000 - self.pretrained = pretrained - self.in_size = in_size - self.input_IN = nn.InstanceNorm2d(3, affine=True) if IN_first else None - self.bn_eval = bn_eval - self.bn_frozen = bn_frozen - self.pooling_type = pooling_type - self.num_features = self.num_head_features = final_block_channels - activation = "Swish" - self.features = nn.Sequential() - self.features.add_module( - "init_block", - EffiInitBlock( - in_channels=in_channels, - out_channels=init_block_channels, - bn_eps=bn_eps, - activation=activation, - tf_mode=tf_mode, - IN_conv1=IN_conv1, - ), - ) - in_channels = init_block_channels - for i, channels_per_stage in enumerate(channels): - kernel_sizes_per_stage = kernel_sizes[i] - expansion_factors_per_stage = expansion_factors[i] - stage = nn.Sequential() - for j, out_channels in enumerate(channels_per_stage): - kernel_size = kernel_sizes_per_stage[j] - expansion_factor = expansion_factors_per_stage[j] - stride = strides_per_stage[i] if (j == 0) else 1 - if i == 0: - stage.add_module( - f"unit{j + 1}", - EffiDwsConvUnit( - in_channels=in_channels, - out_channels=out_channels, - stride=stride, - bn_eps=bn_eps, - activation=activation, - tf_mode=tf_mode, - ), - ) - else: - stage.add_module( - f"unit{j + 1}", - EffiInvResUnit( - in_channels=in_channels, - out_channels=out_channels, - kernel_size=kernel_size, - stride=stride, - exp_factor=expansion_factor, - se_factor=4, - bn_eps=bn_eps, - activation=activation, - tf_mode=tf_mode, - ), - ) - in_channels = out_channels - self.features.add_module(f"stage{i+1}", stage) - # activation = activation if self.loss == 'softmax': else lambda: nn.PReLU(init=0.25) - self.features.add_module( - "final_block", - conv1x1_block( - in_channels=in_channels, out_channels=final_block_channels, bn_eps=bn_eps, activation=activation, - ), - ) - self._init_params() - - def _init_params(self): - for module in self.named_modules(): - if isinstance(module, nn.Conv2d): - init.kaiming_uniform_(module.weight) - if module.bias is not None: - init.constant_(module.bias, 0) - - def forward(self, x, return_featuremaps=False, get_embeddings=False): - """Forward.""" - if self.input_IN is not None: - x = self.input_IN(x) - - y = self.features(x) - if return_featuremaps: - return (y,) - - glob_features = self._glob_feature_vector(y, self.pooling_type, reduce_dims=False) - - logits = self.output(glob_features.view(x.shape[0], -1)) - - if not self.training and self.classification: - return [logits] - - if get_embeddings: - out_data = [logits, glob_features.view(x.shape[0], -1)] - elif self.loss in ["softmax", "am_softmax"]: - if self.lr_finder.enable and self.lr_finder.mode == "automatic": - out_data = logits - else: - out_data = [logits] - - elif self.loss in ["triplet"]: - out_data = [logits, glob_features] - else: - raise KeyError(f"Unsupported loss: {self.loss}") - - if self.lr_finder.enable and self.lr_finder.mode == "automatic": - return out_data - return tuple(out_data) - - -@MODELS.register_module() -class OTXEfficientNet(EfficientNet): - """Create EfficientNet model with specific parameters. - - Args: - version : str. Version of EfficientNet ('b0'...'b8'). - in_size : tuple of two ints. Spatial size of the expected input image. - """ - - def __init__(self, version, **kwargs): - self.model_name = "efficientnet_" + version - - if version == "b0": - in_size = (224, 224) - depth_factor = 1.0 - width_factor = 1.0 - elif version == "b1": - in_size = (240, 240) - depth_factor = 1.1 - width_factor = 1.0 - elif version == "b2": - in_size = (260, 260) - depth_factor = 1.2 - width_factor = 1.1 - elif version == "b3": - in_size = (300, 300) - depth_factor = 1.4 - width_factor = 1.2 - elif version == "b4": - in_size = (380, 380) - depth_factor = 1.8 - width_factor = 1.4 - elif version == "b5": - in_size = (456, 456) - depth_factor = 2.2 - width_factor = 1.6 - elif version == "b6": - in_size = (528, 528) - depth_factor = 2.6 - width_factor = 1.8 - elif version == "b7": - in_size = (600, 600) - depth_factor = 3.1 - width_factor = 2.0 - elif version == "b8": - in_size = (672, 672) - depth_factor = 3.6 - width_factor = 2.2 - else: - raise ValueError(f"Unsupported EfficientNet version {version}") - - init_block_channels = 32 - layers = [1, 2, 2, 3, 3, 4, 1] - downsample = [1, 1, 1, 1, 0, 1, 0] - channels_per_layers = [16, 24, 40, 80, 112, 192, 320] - expansion_factors_per_layers = [1, 6, 6, 6, 6, 6, 6] - kernel_sizes_per_layers = [3, 3, 5, 3, 5, 5, 3] - strides_per_stage = [1, 2, 2, 2, 1, 2, 1] - final_block_channels = 1280 - - layers = [int(math.ceil(li * depth_factor)) for li in layers] - channels_per_layers = [round_channels(ci * width_factor) for ci in channels_per_layers] - - from functools import reduce - - channels = reduce( - lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], - zip(channels_per_layers, layers, downsample), - [], - ) - kernel_sizes = reduce( - lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], - zip(kernel_sizes_per_layers, layers, downsample), - [], - ) - expansion_factors = reduce( - lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], - zip(expansion_factors_per_layers, layers, downsample), - [], - ) - strides_per_stage = reduce( - lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], - zip(strides_per_stage, layers, downsample), - [], - ) - strides_per_stage = [si[0] for si in strides_per_stage] - - init_block_channels = round_channels(init_block_channels * width_factor) - - if width_factor > 1.0: - assert int(final_block_channels * width_factor) == round_channels(final_block_channels * width_factor) - final_block_channels = round_channels(final_block_channels * width_factor) - - super().__init__( - channels=channels, - init_block_channels=init_block_channels, - final_block_channels=final_block_channels, - kernel_sizes=kernel_sizes, - strides_per_stage=strides_per_stage, - expansion_factors=expansion_factors, - dropout_cls=dict(dist="none"), - tf_mode=False, - bn_eps=1e-5, - in_size=in_size, - **kwargs, - ) - self.init_weights(self.pretrained) - - def forward(self, x): - """Forward.""" - return super().forward(x, return_featuremaps=True) - - def init_weights(self, pretrained=None): - """Initialize weights.""" - if isinstance(pretrained, str) and os.path.exists(pretrained): - load_checkpoint(self, pretrained) - print(f"init weight - {pretrained}") - elif pretrained is not None: - download_model(net=self, model_name=self.model_name) - print(f"init weight - {pretrained_urls[self.model_name]}") diff --git a/src/otx/algo/classification/backbones/otx_efficientnet_v2.py b/src/otx/algo/classification/backbones/otx_efficientnet_v2.py deleted file mode 100644 index 27fc7912e96..00000000000 --- a/src/otx/algo/classification/backbones/otx_efficientnet_v2.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""EfficientNetV2 model. - -Original papers: -- 'EfficientNetV2: Smaller Models and Faster Training,' https://arxiv.org/abs/2104.00298, -- 'Adversarial Examples Improve Image Recognition,' https://arxiv.org/abs/1911.09665. -""" - - -import os - -import timm -from mmengine.runner import load_checkpoint -from mmpretrain.registry import MODELS -from torch import nn - -PRETRAINED_ROOT = "https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-effv2-weights/" -pretrained_urls = { - "efficientnetv2_s_21k": PRETRAINED_ROOT + "tf_efficientnetv2_s_21k-6337ad01.pth", - "efficientnetv2_s_1k": PRETRAINED_ROOT + "tf_efficientnetv2_s_21ft1k-d7dafa41.pth", -} - -NAME_DICT = { - "mobilenetv3_large_21k": "mobilenetv3_large_100_miil_in21k", - "mobilenetv3_large_1k": "mobilenetv3_large_100_miil", - "tresnet": "tresnet_m", - "efficientnetv2_s_21k": "tf_efficientnetv2_s_in21k", - "efficientnetv2_s_1k": "tf_efficientnetv2_s_in21ft1k", - "efficientnetv2_m_21k": "tf_efficientnetv2_m_in21k", - "efficientnetv2_m_1k": "tf_efficientnetv2_m_in21ft1k", - "efficientnetv2_b0": "tf_efficientnetv2_b0", -} - - -class TimmModelsWrapper(nn.Module): - """Timm model wrapper.""" - - def __init__(self, model_name, pretrained=False, pooling_type="avg", **kwargs): - super().__init__(**kwargs) - self.model_name = model_name - self.pretrained = pretrained - if model_name in ["mobilenetv3_large_100_miil_in21k", "mobilenetv3_large_100_miil"]: - self.is_mobilenet = True - else: - self.is_mobilenet = False - - self.model = timm.create_model(NAME_DICT[self.model_name], pretrained=pretrained, num_classes=1000) - if self.pretrained: - print(f"init weight - {pretrained_urls[self.model_name]}") - self.model.classifier = None # Detach classifier. Only use 'backbone' part in otx. - self.num_head_features = self.model.num_features - self.num_features = self.model.conv_head.in_channels if self.is_mobilenet else self.model.num_features - self.pooling_type = pooling_type - - def forward(self, x, **kwargs): - """Forward.""" - y = self.extract_features(x) - return (y,) - - def extract_features(self, x): - """Extract features.""" - if self.is_mobilenet: - x = self.model.conv_stem(x) - x = self.model.bn1(x) - x = self.model.act1(x) - y = self.model.blocks(x) - return y - return self.model.forward_features(x) - - def get_config_optim(self, lrs): - """Get optimizer configs.""" - parameters = [ - {"params": self.model.named_parameters()}, - ] - if isinstance(lrs, list): - assert len(lrs) == len(parameters) - for lr, param_dict in zip(lrs, parameters): - param_dict["lr"] = lr - else: - assert isinstance(lrs, float) - for param_dict in parameters: - param_dict["lr"] = lrs - - return parameters - - -@MODELS.register_module() -class OTXEfficientNetV2(TimmModelsWrapper): - """EfficientNetV2 for OTX.""" - - def __init__(self, version="s_21k", **kwargs): - self.model_name = "efficientnetv2_" + version - super().__init__(model_name=self.model_name, **kwargs) - - def init_weights(self, pretrained=None): - """Initialize weights.""" - if isinstance(pretrained, str) and os.path.exists(pretrained): - load_checkpoint(self, pretrained) - print(f"init weight - {pretrained}") - elif pretrained is not None: - load_checkpoint(self, pretrained_urls[self.model_name]) - print(f"init weight - {pretrained_urls[self.model_name]}") diff --git a/src/otx/algo/classification/backbones/otx_mobilenet_v3.py b/src/otx/algo/classification/backbones/otx_mobilenet_v3.py deleted file mode 100644 index df2fa085708..00000000000 --- a/src/otx/algo/classification/backbones/otx_mobilenet_v3.py +++ /dev/null @@ -1,368 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Implementation of MobileNetV3. - -Original papers: -- 'Searching for MobileNetV3,' https://arxiv.org/abs/1905.02244. -""" - -import math -import os - -import torch.nn.functional as F -from mmengine.runner import load_checkpoint -from mmpretrain.models.utils import make_divisible -from mmpretrain.registry import MODELS -from torch import nn - -pretrained_root = "https://github.com/d-li14/mobilenetv3.pytorch/blob/master/pretrained/" -pretrained_urls = { - "mobilenetv3_small": pretrained_root + "mobilenetv3-small-55df8e1f.pth?raw=true", - "mobilenetv3_large": pretrained_root + "mobilenetv3-large-1cd25616.pth?raw=true", - "mobilenetv3_large_075": pretrained_root + "mobilenetv3-large-0.75-9632d2a8.pth?raw=true", -} - - -class ModelInterface(nn.Module): - """Model Interface.""" - - def __init__( - self, - classification=False, - contrastive=False, - pretrained=False, - loss="softmax", - **kwargs, - ): - super().__init__() - - self.classification = classification - self.contrastive = contrastive - self.pretrained = pretrained - self.classification_classes = {} - self.loss = loss - self.is_ie_model = False - if loss == "am_softmax": - self.use_angle_simple_linear = True - else: - self.use_angle_simple_linear = False - - @staticmethod - def _glob_feature_vector(x, mode, reduce_dims=True): - if mode == "avg": - out = F.adaptive_avg_pool2d(x, 1) - elif mode == "max": - out = F.adaptive_max_pool2d(x, 1) - elif mode == "avg+max": - avg_pool = F.adaptive_avg_pool2d(x, 1) - max_pool = F.adaptive_max_pool2d(x, 1) - out = avg_pool + max_pool - else: - raise ValueError(f"Unknown pooling mode: {mode}") - - if reduce_dims: - return out.view(x.size(0), -1) - return out - - -class HSigmoid(nn.Module): - """Approximated sigmoid function, so-called hard-version of sigmoid from 'Searching for MobileNetV3,'. - - https://arxiv.org/abs/1905.02244. - """ - - def forward(self, x): - """Forward.""" - return F.relu6(x + 3.0, inplace=True) / 6.0 - - -class HSwish(nn.Module): - """H-Swish activation function from 'Searching for MobileNetV3,'. - - https://arxiv.org/abs/1905.02244. - - Parameters: - inplace : bool, Whether to use inplace version of the module. - """ - - def __init__(self, inplace=False): - super().__init__() - self.inplace = inplace - - def forward(self, x): - """Forward.""" - return x * F.relu6(x + 3.0, inplace=self.inplace) / 6.0 - - -class SELayer(nn.Module): - """SE layer.""" - - def __init__(self, channel, reduction=4): - super().__init__() - self.avg_pool = nn.AdaptiveAvgPool2d(1) - self.fc = nn.Sequential( - nn.Linear(channel, make_divisible(channel // reduction, 8)), - nn.ReLU(inplace=True), - nn.Linear(make_divisible(channel // reduction, 8), channel), - HSigmoid(), - ) - - def forward(self, x): - """Forward.""" - # with no_nncf_se_layer_context(): - b, c, _, _ = x.size() - y = self.avg_pool(x).view(b, c) - y = self.fc(y).view(b, c, 1, 1) - return x * y - - -def conv_3x3_bn(inp, oup, stride, IN_conv1=False): - """Conv 3x3 layer with batch-norm.""" - return nn.Sequential( - nn.Conv2d(inp, oup, 3, stride, 1, bias=False), - nn.BatchNorm2d(oup) if not IN_conv1 else nn.InstanceNorm2d(oup, affine=True), - HSwish(), - ) - - -def conv_1x1_bn(inp, oup, loss="softmax"): - """Conv 1x1 layer with batch-norm.""" - return nn.Sequential( - nn.Conv2d(inp, oup, 1, 1, 0, bias=False), - nn.BatchNorm2d(oup), - HSwish() if loss == "softmax" else nn.PReLU(), - ) - - -class InvertedResidual(nn.Module): - """Inverted residual.""" - - def __init__(self, inp, hidden_dim, oup, kernel_size, stride, use_se, use_hs): - super().__init__() - assert stride in [1, 2] - - self.identity = stride == 1 and inp == oup - - if inp == hidden_dim: - self.conv = nn.Sequential( - # dw - nn.Conv2d( - hidden_dim, - hidden_dim, - kernel_size, - stride, - (kernel_size - 1) // 2, - groups=hidden_dim, - bias=False, - ), - nn.BatchNorm2d(hidden_dim), - HSwish() if use_hs else nn.ReLU(inplace=True), - # Squeeze-and-Excite - SELayer(hidden_dim) if use_se else nn.Identity(), - # pw-linear - nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False), - nn.BatchNorm2d(oup), - ) - else: - self.conv = nn.Sequential( - # pw - nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False), - nn.BatchNorm2d(hidden_dim), - HSwish() if use_hs else nn.ReLU(inplace=True), - # dw - nn.Conv2d( - hidden_dim, - hidden_dim, - kernel_size, - stride, - (kernel_size - 1) // 2, - groups=hidden_dim, - bias=False, - ), - nn.BatchNorm2d(hidden_dim), - # Squeeze-and-Excite - SELayer(hidden_dim) if use_se else nn.Identity(), - HSwish() if use_hs else nn.ReLU(inplace=True), - # pw-linear - nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False), - nn.BatchNorm2d(oup), - ) - - def forward(self, x): - """Forward.""" - if self.identity: - return x + self.conv(x) - return self.conv(x) - - -class MobileNetV3Base(ModelInterface): - """Base model of MobileNetV3.""" - - def __init__( - self, - num_classes=1000, - width_mult=1.0, - in_channels=3, - input_size=(224, 224), - dropout_cls=None, - pooling_type="avg", - feature_dim=1280, - IN_first=False, - self_challenging_cfg=False, - lr_finder=None, - **kwargs, - ): - - super().__init__(**kwargs) - self.in_size = input_size - self.num_classes = num_classes - self.input_IN = nn.InstanceNorm2d(in_channels, affine=True) if IN_first else None - self.pooling_type = pooling_type - self.self_challenging_cfg = self_challenging_cfg - self.width_mult = width_mult - self.dropout_cls = dropout_cls - self.lr_finder = lr_finder - self.feature_dim = feature_dim - - def infer_head(self, x, skip_pool=False): - """Inference head.""" - raise NotImplementedError - - def extract_features(self, x): - """Extract features.""" - raise NotImplementedError - - def forward(self, x, return_featuremaps=False, get_embeddings=False, gt_labels=None): - """Forward.""" - if self.input_IN is not None: - x = self.input_IN(x) - - y = self.extract_features(x) - if return_featuremaps: - return y - return y - - -class MobileNetV3(MobileNetV3Base): - """MobileNetV3.""" - - def __init__(self, cfgs, mode, IN_conv1=False, **kwargs): - super().__init__(**kwargs) - # setting of inverted residual blocks - self.cfgs = cfgs - assert mode in ["large", "small"] - # building first layer - input_channel = make_divisible(16 * self.width_mult, 8) - stride = 1 if self.in_size[0] < 100 else 2 - layers = [conv_3x3_bn(3, input_channel, stride, IN_conv1)] - # building inverted residual blocks - block = InvertedResidual - flag = True - for k, t, c, use_se, use_hs, s in self.cfgs: - if (self.in_size[0] < 100) and (s == 2) and flag: - s = 1 - flag = False - output_channel = make_divisible(c * self.width_mult, 8) - exp_size = make_divisible(input_channel * t, 8) - layers.append(block(input_channel, exp_size, output_channel, k, s, use_se, use_hs)) - input_channel = output_channel - self.features = nn.Sequential(*layers) - # building last several layers - self.conv = conv_1x1_bn(input_channel, exp_size, self.loss) - output_channel = {"large": 1280, "small": 1024} - output_channel = ( - make_divisible(output_channel[mode] * self.width_mult, 8) if self.width_mult > 1.0 else output_channel[mode] - ) - self._initialize_weights() - - def extract_features(self, x): - """Extract features.""" - y = self.conv(self.features(x)) - return (y, ) - - def infer_head(self, x, skip_pool=False): - """Inference head.""" - if not skip_pool: - glob_features = self._glob_feature_vector(x, self.pooling_type, reduce_dims=False) - else: - glob_features = x - - logits = self.classifier(glob_features.view(x.shape[0], -1)) - return glob_features, logits - - def _initialize_weights(self): - """Initialize weights.""" - for m in self.modules(): - if isinstance(m, nn.Conv2d): - n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels - m.weight.data.normal_(0, math.sqrt(2.0 / n)) - if m.bias is not None: - m.bias.data.zero_() - elif isinstance(m, nn.BatchNorm2d): - m.weight.data.fill_(1) - m.bias.data.zero_() - elif isinstance(m, nn.Linear): - n = m.weight.size(1) - m.weight.data.normal_(0, 0.01) - m.bias.data.zero_() - - -@MODELS.register_module() -class OTXMobileNetV3(MobileNetV3): - """MobileNetV3 model for OTX.""" - - cfgs = dict( - small=[ - # k, t, c, SE, HS, s - [3, 1, 16, 1, 0, 2], - [3, 4.5, 24, 0, 0, 2], - [3, 3.67, 24, 0, 0, 1], - [5, 4, 40, 1, 1, 2], - [5, 6, 40, 1, 1, 1], - [5, 6, 40, 1, 1, 1], - [5, 3, 48, 1, 1, 1], - [5, 3, 48, 1, 1, 1], - [5, 6, 96, 1, 1, 2], - [5, 6, 96, 1, 1, 1], - [5, 6, 96, 1, 1, 1], - ], - large=[ - # k, t, c, SE, HS, s - [3, 1, 16, 0, 0, 1], - [3, 4, 24, 0, 0, 2], - [3, 3, 24, 0, 0, 1], - [5, 3, 40, 1, 0, 2], - [5, 3, 40, 1, 0, 1], - [5, 3, 40, 1, 0, 1], - [3, 6, 80, 0, 1, 2], - [3, 2.5, 80, 0, 1, 1], - [3, 2.3, 80, 0, 1, 1], - [3, 2.3, 80, 0, 1, 1], - [3, 6, 112, 1, 1, 1], - [3, 6, 112, 1, 1, 1], - [5, 6, 160, 1, 1, 2], - [5, 6, 160, 1, 1, 1], - [5, 6, 160, 1, 1, 1], - ], - ) - - def __init__(self, mode="large", width_mult=1.0, **kwargs): - super().__init__(self.cfgs[mode], mode=mode, width_mult=width_mult, **kwargs) - self.key = "mobilenetv3_" + mode - if width_mult != 1.0: - self.key = self.key + f"_{int(width_mult * 100):03d}" # pylint: disable=consider-using-f-string - self.init_weights(self.pretrained) - - def forward(self, x): - """Forward.""" - return super().forward(x, return_featuremaps=True) - - def init_weights(self, pretrained=None): - """Initialize weights.""" - if isinstance(pretrained, str) and os.path.exists(pretrained): - load_checkpoint(self, pretrained) - print(f"init weight - {pretrained}") - elif pretrained is not None: - load_checkpoint(self, pretrained_urls[self.key]) - print(f"init weight - {pretrained_urls[self.key]}") diff --git a/src/otx/algo/classification/deit_tiny.py b/src/otx/algo/classification/deit_tiny.py deleted file mode 100644 index e2b03baf89b..00000000000 --- a/src/otx/algo/classification/deit_tiny.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""DeitTiny model implementation.""" -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Callable - -import numpy as np -import torch -from mmpretrain.models.utils import resize_pos_embed - -from otx.algo.hooks.recording_forward_hook import ViTReciproCAMHook -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.classification import ( - ExplainableOTXClsModel, - MMPretrainHlabelClsModel, - MMPretrainMulticlassClsModel, - MMPretrainMultilabelClsModel, -) - -if TYPE_CHECKING: - from mmpretrain.models import ImageClassifier - from mmpretrain.structures import DataSample - - -class ExplainableDeit(ExplainableOTXClsModel): - """Deit model which can attach a XAI hook.""" - - @torch.no_grad() - def head_forward_fn(self, x: torch.Tensor) -> torch.Tensor: - """Performs model's neck and head forward.""" - if not hasattr(self.model.backbone, "layers"): - raise ValueError - if not hasattr(self.model.backbone, "final_norm"): - raise ValueError - if not hasattr(self.model, "with_neck"): - raise ValueError - - # Part of the last transformer_encoder block (except first LayerNorm) - target_layer = self.model.backbone.layers[-1] - x = x + target_layer.attn(x) - x = target_layer.ffn(target_layer.norm2(x), identity=x) - - # Final LayerNorm and neck - if self.model.backbone.final_norm: - x = self.model.backbone.norm1(x) - if self.model.with_neck: - x = self.model.neck(x) - - # Head - cls_token = x[:, 0] - layer_output = [None, cls_token] - logit = self.model.head.forward(layer_output) - if isinstance(logit, list): - logit = torch.from_numpy(np.array(logit)) - return logit - - @staticmethod - def _forward_explain_image_classifier( - self: ImageClassifier, - inputs: torch.Tensor, - data_samples: list[DataSample] | None = None, - mode: str = "tensor", - ) -> dict: - """Forward func of the ImageClassifier instance, which located in is in OTXModel().model.""" - backbone = self.backbone - - ### Start of backbone forward - batch_size = inputs.shape[0] - x, patch_resolution = backbone.patch_embed(inputs) - - if backbone.cls_token is not None: - cls_token = backbone.cls_token.expand(batch_size, -1, -1) - x = torch.cat((cls_token, x), dim=1) - - x = x + resize_pos_embed( - backbone.pos_embed, - backbone.patch_resolution, - patch_resolution, - mode=backbone.interpolate_mode, - num_extra_tokens=backbone.num_extra_tokens, - ) - x = backbone.drop_after_pos(x) - - x = backbone.pre_norm(x) - - outs = [] - layernorm_feat = None - for i, layer in enumerate(backbone.layers): - if i == len(backbone.layers) - 1: - layernorm_feat = layer.norm1(x) - - x = layer(x) - - if i == len(backbone.layers) - 1 and backbone.final_norm: - x = backbone.ln1(x) - - if i in backbone.out_indices: - outs.append(backbone._format_output(x, patch_resolution)) # noqa: SLF001 - - x = tuple(outs) - ### End of backbone forward - - saliency_map = self.explain_fn(layernorm_feat) - - if self.with_neck: - x = self.neck(x) - - feature_vector = x[-1] - - if mode == "tensor": - logits = self.head(x) if self.with_head else x - elif mode == "predict": - logits = self.head.predict(x, data_samples) - else: - msg = f'Invalid mode "{mode}".' - raise RuntimeError(msg) - - return { - "logits": logits, - "feature_vector": feature_vector, - "saliency_map": saliency_map, - } - - def get_explain_fn(self) -> Callable: - """Returns explain function.""" - explainer = ViTReciproCAMHook( - self.head_forward_fn, - num_classes=self.num_classes, - ) - return explainer.func - - @property - def _optimization_config(self) -> dict[str, Any]: - """PTQ config for DeitTinyForMultilabelCls.""" - return {"model_type": "transformer"} - - -class DeitTinyForHLabelCls(ExplainableDeit, MMPretrainHlabelClsModel): - """DeitTiny Model for hierarchical label classification task.""" - - def __init__(self, num_classes: int, num_multiclass_heads: int, num_multilabel_classes: int) -> None: - self.num_multiclass_heads = num_multiclass_heads - self.num_multilabel_classes = num_multilabel_classes - - config = read_mmconfig(model_name="deit_tiny", subdir_name="hlabel_classification") - config.head.num_multiclass_heads = num_multiclass_heads - config.head.num_multilabel_classes = num_multilabel_classes - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_effnet_b0_ckpt(state_dict, "multiclass", add_prefix) - - -class DeitTinyForMulticlassCls(ExplainableDeit, MMPretrainMulticlassClsModel): - """DeitTiny Model for multi-label classification task.""" - - def __init__(self, num_classes: int) -> None: - config = read_mmconfig("deit_tiny", subdir_name="multiclass_classification") - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_effnet_b0_ckpt(state_dict, "multiclass", add_prefix) - - -class DeitTinyForMultilabelCls(ExplainableDeit, MMPretrainMultilabelClsModel): - """DeitTiny Model for multi-class classification task.""" - - def __init__(self, num_classes: int) -> None: - config = read_mmconfig("deit_tiny", subdir_name="multilabel_classification") - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_effnet_b0_ckpt(state_dict, "multiclass", add_prefix) diff --git a/src/otx/algo/classification/efficientnet_b0.py b/src/otx/algo/classification/efficientnet_b0.py deleted file mode 100644 index 43ba2c6047b..00000000000 --- a/src/otx/algo/classification/efficientnet_b0.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""EfficientNetB0 model implementation.""" -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.classification import ( - MMPretrainHlabelClsModel, - MMPretrainMulticlassClsModel, - MMPretrainMultilabelClsModel, -) - - -class EfficientNetB0ForHLabelCls(MMPretrainHlabelClsModel): - """EfficientNetB0 Model for hierarchical label classification task.""" - - def __init__(self, num_classes: int, num_multiclass_heads: int, num_multilabel_classes: int) -> None: - self.num_multiclass_heads = num_multiclass_heads - self.num_multilabel_classes = num_multilabel_classes - - config = read_mmconfig(model_name="efficientnet_b0_light", subdir_name="hlabel_classification") - config.head.num_multiclass_heads = num_multiclass_heads - config.head.num_multilabel_classes = num_multilabel_classes - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_effnet_b0_ckpt(state_dict, "hlabel", add_prefix) - - -class EfficientNetB0ForMulticlassCls(MMPretrainMulticlassClsModel): - """EfficientNetB0 Model for multi-label classification task.""" - - def __init__(self, num_classes: int, light: bool = False) -> None: - model_name = "efficientnet_b0_light" if light else "efficientnet_b0" - config = read_mmconfig(model_name=model_name, subdir_name="multiclass_classification") - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_effnet_b0_ckpt(state_dict, "multiclass", add_prefix) - - -class EfficientNetB0ForMultilabelCls(MMPretrainMultilabelClsModel): - """EfficientNetB0 Model for multi-class classification task.""" - - def __init__(self, num_classes: int) -> None: - config = read_mmconfig(model_name="efficientnet_b0_light", subdir_name="multilabel_classification") - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_effnet_b0_ckpt(state_dict, "multilabel", add_prefix) diff --git a/src/otx/algo/classification/efficientnet_v2.py b/src/otx/algo/classification/efficientnet_v2.py deleted file mode 100644 index 3fb2eac9559..00000000000 --- a/src/otx/algo/classification/efficientnet_v2.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""EfficientNetV2 model implementation.""" -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.classification import ( - MMPretrainHlabelClsModel, - MMPretrainMulticlassClsModel, - MMPretrainMultilabelClsModel, -) - - -class EfficientNetV2ForHLabelCls(MMPretrainHlabelClsModel): - """EfficientNetV2 Model for hierarchical label classification task.""" - - def __init__(self, num_classes: int, num_multiclass_heads: int, num_multilabel_classes: int) -> None: - self.num_multiclass_heads = num_multiclass_heads - self.num_multilabel_classes = num_multilabel_classes - - config = read_mmconfig("efficientnet_v2_light", subdir_name="hlabel_classification") - config.head.num_multiclass_heads = num_multiclass_heads - config.head.num_multilabel_classes = num_multilabel_classes - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_effnet_v2_ckpt(state_dict, "hlabel", add_prefix) - - -class EfficientNetV2ForMulticlassCls(MMPretrainMulticlassClsModel): - """EfficientNetV2 Model for multi-label classification task.""" - - def __init__(self, num_classes: int, light: bool = False) -> None: - model_name = "efficientnet_v2_light" if light else "efficientnet_v2" - config = read_mmconfig(model_name=model_name, subdir_name="multiclass_classification") - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_effnet_v2_ckpt(state_dict, "multiclass", add_prefix) - - -class EfficientNetV2ForMultilabelCls(MMPretrainMultilabelClsModel): - """EfficientNetV2 Model for multi-class classification task.""" - - def __init__(self, num_classes: int) -> None: - config = read_mmconfig("efficientnet_v2_light", subdir_name="multilabel_classification") - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_effnet_v2_ckpt(state_dict, "multilabel", add_prefix) diff --git a/src/otx/algo/classification/heads/__init__.py b/src/otx/algo/classification/heads/__init__.py deleted file mode 100644 index b76f56c44b8..00000000000 --- a/src/otx/algo/classification/heads/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Head modules for OTX custom model.""" - -from .custom_hlabel_linear_cls_head import CustomHierarchicalLinearClsHead -from .custom_hlabel_non_linear_cls_head import CustomHierarchicalNonLinearClsHead -from .custom_multilabel_linear_cls_head import CustomMultiLabelLinearClsHead -from .custom_multilabel_non_linear_cls_head import CustomMultiLabelNonLinearClsHead - -__all__ = [ - "CustomMultiLabelLinearClsHead", - "CustomMultiLabelNonLinearClsHead", - "CustomHierarchicalLinearClsHead", - "CustomHierarchicalNonLinearClsHead", -] diff --git a/src/otx/algo/classification/heads/custom_hlabel_linear_cls_head.py b/src/otx/algo/classification/heads/custom_hlabel_linear_cls_head.py deleted file mode 100644 index eecf41566cf..00000000000 --- a/src/otx/algo/classification/heads/custom_hlabel_linear_cls_head.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for defining h-label linear classification head.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import torch -from mmengine.model import BaseModule, normal_init -from mmpretrain.registry import MODELS -from mmpretrain.structures import DataSample -from torch import nn - -if TYPE_CHECKING: - from otx.core.data.dataset.classification import HLabelInfo - - -@MODELS.register_module() -class CustomHierarchicalLinearClsHead(BaseModule): - """Custom classification linear head for hierarchical classification task. - - Args: - num_multiclass_heads (int): Number of multi-class heads. - num_multilabel_classes (int): Number of multi-label classes. - in_channels (int): Number of channels in the input feature map. - num_classes (int): Number of total classes. - multiclass_loss (dict | None): Config of multi-class loss. - multilabel_loss (dict | None): Config of multi-label loss. - thr (float | None): Predictions with scores under the thresholds are considered - as negative. Defaults to 0.5. - """ - - def __init__( - self, - num_multiclass_heads: int, - num_multilabel_classes: int, - in_channels: int, - num_classes: int, - multiclass_loss_cfg: dict | None = None, - multilabel_loss_cfg: dict | None = None, - thr: float = 0.5, - **kwargs, - ): - super().__init__() - self.num_multiclass_heads = num_multiclass_heads - self.num_multilabel_classes = num_multilabel_classes - self.in_channels = in_channels - self.num_classes = num_classes - self.thr = thr - - if self.num_multiclass_heads == 0: - msg = "num_multiclass_head should be larger than 0" - raise ValueError(msg) - - self.multiclass_loss = MODELS.build(multiclass_loss_cfg) - if num_multilabel_classes > 0: - self.multilabel_loss = MODELS.build(multilabel_loss_cfg) - - self.fc = nn.Linear(self.in_channels, self.num_classes) - self._init_layers() - - def _init_layers(self) -> None: - """Initialize weights of the layers.""" - normal_init(self.fc, mean=0, std=0.01, bias=0) - - def pre_logits(self, feats: tuple[torch.Tensor]) -> torch.Tensor: - """The process before the final classification head.""" - return feats[-1] - - def forward(self, feats: tuple[torch.Tensor]) -> torch.Tensor: - """The forward process.""" - pre_logits = self.pre_logits(feats) - return self.fc(pre_logits) - - def set_hlabel_info(self, hlabel_info: HLabelInfo) -> None: - """Set hlabel information.""" - self.hlabel_info = hlabel_info - - def _get_gt_label(self, data_samples: list[DataSample]) -> torch.Tensor: - """Get gt labels from data samples.""" - return torch.stack([data_sample.gt_label for data_sample in data_samples]) - - def _get_head_idx_to_logits_range(self, hlabel_info: HLabelInfo, idx: int) -> tuple[int, int]: - """Get head_idx_to_logits_range information from hlabel information.""" - return ( - hlabel_info.head_idx_to_logits_range[str(idx)][0], - hlabel_info.head_idx_to_logits_range[str(idx)][1], - ) - - def loss(self, feats: tuple[torch.Tensor], data_samples: list[DataSample], **kwargs) -> dict: - """Calculate losses from the classification score. - - Args: - feats (tuple[Tensor]): The features extracted from the backbone. - Multiple stage inputs are acceptable but only the last stage - will be used to classify. The shape of every item should be - ``(num_samples, num_classes)``. - data_samples (List[DataSample]): The annotation data of - every samples. - **kwargs: Other keyword arguments to forward the loss module. - - Returns: - dict[str, Tensor]: a dictionary of loss components - """ - losses = {} - cls_scores = self(feats) - gt_labels = self._get_gt_label(data_samples) - - losses = {"loss": 0.0} - - # Multiclass loss - num_effective_heads_in_batch = 0 # consider the label removal case - for i in range(self.num_multiclass_heads): - if i not in self.hlabel_info.empty_multiclass_head_indices: - head_gt = gt_labels[:, i] - logit_range = self._get_head_idx_to_logits_range(self.hlabel_info, i) - head_logits = cls_scores[:, logit_range[0] : logit_range[1]] - valid_mask = head_gt >= 0 - - head_gt = head_gt[valid_mask] - if len(head_gt) > 0: - head_logits = head_logits[valid_mask, :] - losses["loss"] += self.multiclass_loss(head_logits, head_gt) - num_effective_heads_in_batch += 1 - - if num_effective_heads_in_batch > 0: - losses["loss"] /= num_effective_heads_in_batch - - # Multilabel loss - if self.num_multilabel_classes > 0: - head_gt = gt_labels[:, self.hlabel_info.num_multiclass_heads :] - head_logits = cls_scores[:, self.hlabel_info.num_single_label_classes :] - valid_mask = head_gt > 0 - head_gt = head_gt[valid_mask] - if len(head_gt) > 0: - img_metas = [data_sample.metainfo for data_sample in data_samples] - head_logits = head_logits[valid_mask] - valid_label_mask = self.get_valid_label_mask(img_metas).to(head_logits.device) - valid_label_mask = valid_label_mask[:, self.hlabel_info.num_single_label_classes :] - valid_label_mask = valid_label_mask[valid_mask] - losses["loss"] += self.multilabel_loss(head_logits, head_gt, valid_label_mask=valid_label_mask) - - return losses - - def predict( - self, - feats: tuple[torch.Tensor], - data_samples: list[DataSample] | None = None, - ) -> list[DataSample]: - """Inference without augmentation. - - Args: - feats (tuple[Tensor]): The features extracted from the backbone. - Multiple stage inputs are acceptable but only the last stage - will be used to classify. The shape of every item should be - ``(num_samples, num_classes)``. - data_samples (List[DataSample], optional): The annotation - data of every samples. If not None, set ``pred_label`` of - the input data samples. Defaults to None. - - Returns: - List[DataSample]: A list of data samples which contains the - predicted results. - """ - cls_scores = self(feats) - return self._get_predictions(cls_scores, data_samples) - - def _get_predictions( - self, - cls_scores: torch.Tensor, - data_samples: list[DataSample] | None = None, - ) -> list[DataSample]: - """Post-process the output of head. - - Including softmax and set ``pred_label`` of data samples. - """ - if data_samples is None: - data_samples = [DataSample() for _ in range(cls_scores.size(0))] - - # Multiclass - multiclass_pred_scores = [] - multiclass_pred_labels = [] - for i in range(self.num_multiclass_heads): - logit_range = self._get_head_idx_to_logits_range(self.hlabel_info, i) - multiclass_logit = cls_scores[:, logit_range[0] : logit_range[1]] - multiclass_pred = torch.softmax(multiclass_logit, dim=1) - multiclass_pred_score, multiclass_pred_label = torch.max(multiclass_pred, dim=1) - - multiclass_pred_scores.append(multiclass_pred_score.view(-1, 1)) - multiclass_pred_labels.append(multiclass_pred_label.view(-1, 1)) - - multiclass_pred_scores = torch.cat(multiclass_pred_scores, dim=1) - multiclass_pred_labels = torch.cat(multiclass_pred_labels, dim=1) - - if self.num_multilabel_classes > 0: - multilabel_logits = cls_scores[:, self.hlabel_info.num_single_label_classes :] - - multilabel_pred_scores = torch.sigmoid(multilabel_logits) - multilabel_pred_labels = (multilabel_pred_scores >= self.thr).int() - - pred_scores = torch.cat([multiclass_pred_scores, multilabel_pred_scores], axis=1) - pred_labels = torch.cat([multiclass_pred_labels, multilabel_pred_labels], axis=1) - else: - pred_scores = multiclass_pred_scores - pred_labels = multiclass_pred_labels - - for data_sample, score, label in zip(data_samples, pred_scores, pred_labels): - data_sample.set_pred_score(score).set_pred_label(label) - - return data_samples - - def get_valid_label_mask(self, img_metas: list[dict]) -> torch.Tensor: - """Get valid label mask using ignored_label.""" - valid_label_mask = [] - for meta in img_metas: - mask = torch.Tensor([1 for _ in range(self.num_classes)]) - if "ignored_labels" in meta and meta["ignored_labels"]: - mask[meta["ignored_labels"]] = 0 - valid_label_mask.append(mask) - return torch.stack(valid_label_mask, dim=0) diff --git a/src/otx/algo/classification/heads/custom_hlabel_non_linear_cls_head.py b/src/otx/algo/classification/heads/custom_hlabel_non_linear_cls_head.py deleted file mode 100644 index 6eb480dec85..00000000000 --- a/src/otx/algo/classification/heads/custom_hlabel_non_linear_cls_head.py +++ /dev/null @@ -1,253 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for defining h-label nonlinear classification head.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import torch -from mmengine.model import BaseModule, constant_init, normal_init -from mmpretrain.registry import MODELS -from mmpretrain.structures import DataSample -from torch import nn - -if TYPE_CHECKING: - from otx.core.data.dataset.classification import HLabelInfo - - -@MODELS.register_module() -class CustomHierarchicalNonLinearClsHead(BaseModule): - """Custom classification non-linear head for hierarchical classification task. - - Args: - num_multiclass_heads (int): Number of multi-class heads. - num_multilabel_classes (int): Number of multi-label classes. - in_channels (int): Number of channels in the input feature map. - num_classes (int): Number of total classes. - multiclass_loss (dict | None): Config of multi-class loss. - multilabel_loss (dict | None): Config of multi-label loss. - thr (float | None): Predictions with scores under the thresholds are considered - as negative. Defaults to 0.5. - hid_cahnnels (int): Number of channels in the hidden feature map at the classifier. - acivation_Cfg (dict | None): Config of activation layer at the classifier. - dropout (bool): Flag for the enabling the dropout at the classifier. - - """ - - def __init__( - self, - num_multiclass_heads: int, - num_multilabel_classes: int, - in_channels: int, - num_classes: int, - multiclass_loss_cfg: dict | None = None, - multilabel_loss_cfg: dict | None = None, - thr: float = 0.5, - hid_channels: int = 1280, - activation_cfg: dict | None = None, - dropout: bool = False, - **kwargs, - ): - super().__init__() - self.num_multiclass_heads = num_multiclass_heads - self.num_multilabel_classes = num_multilabel_classes - self.in_channels = in_channels - self.num_classes = num_classes - self.thr = thr - - self.hid_channels = hid_channels - self.dropout = dropout - - if self.num_multiclass_heads == 0: - msg = "num_multiclass_head should be larger than 0" - raise ValueError(msg) - - self.multiclass_loss = MODELS.build(multiclass_loss_cfg) - if num_multilabel_classes > 0: - self.multilabel_loss = MODELS.build(multilabel_loss_cfg) - - if not activation_cfg: - activation_cfg = {"type": "ReLU"} - - self.activation = MODELS.build(activation_cfg) - - classifier_modules = [ - nn.Linear(in_channels, hid_channels), - nn.BatchNorm1d(hid_channels), - self.activation, - ] - - if self.dropout: - classifier_modules.append(nn.Dropout(p=0.2)) - - classifier_modules.append(nn.Linear(hid_channels, num_classes)) - - self.classifier = nn.Sequential(*classifier_modules) - - self._init_layers() - - def _init_layers(self) -> None: - """Iniitialize weights of classification head.""" - for module in self.classifier: - if isinstance(module, nn.Linear): - normal_init(module, mean=0, std=0.01, bias=0) - elif isinstance(module, nn.BatchNorm1d): - constant_init(module, 1) - - def pre_logits(self, feats: tuple[torch.Tensor]) -> torch.Tensor: - """The process before the final classification head.""" - return feats[-1] - - def forward(self, feats: tuple[torch.Tensor]) -> torch.Tensor: - """The forward process.""" - pre_logits = self.pre_logits(feats) - return self.classifier(pre_logits) - - def set_hlabel_info(self, hlabel_info: HLabelInfo) -> None: - """Set hlabel information.""" - self.hlabel_info = hlabel_info - - def _get_gt_label(self, data_samples: list[DataSample]) -> torch.Tensor: - """Get gt labels from data samples.""" - return torch.stack([data_sample.gt_label for data_sample in data_samples]) - - def _get_head_idx_to_logits_range(self, hlabel_info: HLabelInfo, idx: int) -> tuple[int, int]: - """Get head_idx_to_logits_range information from hlabel information.""" - return ( - hlabel_info.head_idx_to_logits_range[str(idx)][0], - hlabel_info.head_idx_to_logits_range[str(idx)][1], - ) - - def loss(self, feats: tuple[torch.Tensor], data_samples: list[DataSample], **kwargs) -> dict: - """Calculate losses from the classification score. - - Args: - feats (tuple[Tensor]): The features extracted from the backbone. - Multiple stage inputs are acceptable but only the last stage - will be used to classify. The shape of every item should be - ``(num_samples, num_classes)``. - data_samples (List[DataSample]): The annotation data of - every samples. - **kwargs: Other keyword arguments to forward the loss module. - - Returns: - dict[str, Tensor]: a dictionary of loss components - """ - losses = {} - cls_scores = self(feats) - gt_labels = self._get_gt_label(data_samples) - - losses = {"loss": 0.0} - - # Multiclass loss - num_effective_heads_in_batch = 0 # consider the label removal case - for i in range(self.num_multiclass_heads): - if i not in self.hlabel_info.empty_multiclass_head_indices: - head_gt = gt_labels[:, i] - logit_range = self._get_head_idx_to_logits_range(self.hlabel_info, i) - head_logits = cls_scores[:, logit_range[0] : logit_range[1]] - valid_mask = head_gt >= 0 - - head_gt = head_gt[valid_mask] - if len(head_gt) > 0: - head_logits = head_logits[valid_mask, :] - losses["loss"] += self.multiclass_loss(head_logits, head_gt) - num_effective_heads_in_batch += 1 - - if num_effective_heads_in_batch > 0: - losses["loss"] /= num_effective_heads_in_batch - - # Multilabel loss - if self.num_multilabel_classes > 0: - head_gt = gt_labels[:, self.hlabel_info.num_multiclass_heads :] - head_logits = cls_scores[:, self.hlabel_info.num_single_label_classes :] - valid_mask = head_gt > 0 - head_gt = head_gt[valid_mask] - if len(head_gt) > 0: - img_metas = [data_sample.metainfo for data_sample in data_samples] - head_logits = head_logits[valid_mask] - valid_label_mask = self.get_valid_label_mask(img_metas).to(head_logits.device) - valid_label_mask = valid_label_mask[:, self.hlabel_info.num_single_label_classes :] - valid_label_mask = valid_label_mask[valid_mask] - losses["loss"] += self.multilabel_loss(head_logits, head_gt, valid_label_mask=valid_label_mask) - - return losses - - def predict( - self, - feats: tuple[torch.Tensor], - data_samples: list[DataSample] | None = None, - ) -> list[DataSample]: - """Inference without augmentation. - - Args: - feats (tuple[Tensor]): The features extracted from the backbone. - Multiple stage inputs are acceptable but only the last stage - will be used to classify. The shape of every item should be - ``(num_samples, num_classes)``. - data_samples (List[DataSample], optional): The annotation - data of every samples. If not None, set ``pred_label`` of - the input data samples. Defaults to None. - - Returns: - List[DataSample]: A list of data samples which contains the - predicted results. - """ - cls_scores = self(feats) - return self._get_predictions(cls_scores, data_samples) - - def _get_predictions( - self, - cls_scores: torch.Tensor, - data_samples: list[DataSample] | None = None, - ) -> list[DataSample]: - """Post-process the output of head. - - Including softmax and set ``pred_label`` of data samples. - """ - if data_samples is None: - data_samples = [DataSample() for _ in range(cls_scores.size(0))] - - # Multiclass - multiclass_pred_scores = [] - multiclass_pred_labels = [] - for i in range(self.num_multiclass_heads): - logit_range = self._get_head_idx_to_logits_range(self.hlabel_info, i) - multiclass_logit = cls_scores[:, logit_range[0] : logit_range[1]] - multiclass_pred = torch.softmax(multiclass_logit, dim=1) - multiclass_pred_score, multiclass_pred_label = torch.max(multiclass_pred, dim=1) - - multiclass_pred_scores.append(multiclass_pred_score.view(-1, 1)) - multiclass_pred_labels.append(multiclass_pred_label.view(-1, 1)) - - multiclass_pred_scores = torch.cat(multiclass_pred_scores, dim=1) - multiclass_pred_labels = torch.cat(multiclass_pred_labels, dim=1) - - if self.num_multilabel_classes > 0: - multilabel_logits = cls_scores[:, self.hlabel_info.num_single_label_classes :] - - multilabel_pred_scores = torch.sigmoid(multilabel_logits) - multilabel_pred_labels = (multilabel_pred_scores >= self.thr).int() - - pred_scores = torch.cat([multiclass_pred_scores, multilabel_pred_scores], axis=1) - pred_labels = torch.cat([multiclass_pred_labels, multilabel_pred_labels], axis=1) - else: - pred_scores = multiclass_pred_scores - pred_labels = multiclass_pred_labels - - for data_sample, score, label in zip(data_samples, pred_scores, pred_labels): - data_sample.set_pred_score(score).set_pred_label(label) - - return data_samples - - def get_valid_label_mask(self, img_metas: list[dict]) -> torch.Tensor: - """Get valid label mask using ignored_label.""" - valid_label_mask = [] - for meta in img_metas: - mask = torch.Tensor([1 for _ in range(self.num_classes)]) - if "ignored_labels" in meta and meta["ignored_labels"]: - mask[meta["ignored_labels"]] = 0 - valid_label_mask.append(mask) - return torch.stack(valid_label_mask, dim=0) diff --git a/src/otx/algo/classification/heads/custom_multilabel_linear_cls_head.py b/src/otx/algo/classification/heads/custom_multilabel_linear_cls_head.py deleted file mode 100644 index b37d1f25b78..00000000000 --- a/src/otx/algo/classification/heads/custom_multilabel_linear_cls_head.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for defining multi-label linear classification head.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import torch -from mmengine.model import normal_init -from mmpretrain.models.heads import MultiLabelLinearClsHead -from mmpretrain.registry import MODELS -from torch import nn -from torch.nn import functional - -if TYPE_CHECKING: - from mmpretrain.structures import DataSample - - -@MODELS.register_module() -class CustomMultiLabelLinearClsHead(MultiLabelLinearClsHead): - """Custom Linear classification head for multilabel task. - - Args: - num_classes (int): Number of categories. - in_channels (int): Number of channels in the input feature map. - normalized (bool): Normalize input features and weights. - scale (float): positive scale parameter. - loss (dict): Config of classification loss. - """ - - def __init__( - self, - num_classes: int, - in_channels: int, - normalized: bool = False, - scale: float = 1.0, - loss: dict | None = None, - ): - loss = ( - loss - if loss - else { - "type": "CrossEntropyLoss", - "use_sigmoid": True, - "reduction": "mean", - "loss_weight": 1.0, - } - ) - super().__init__( - loss=loss, - num_classes=num_classes, - in_channels=in_channels, - ) - self.num_classes = num_classes - self.normalized = normalized - self.scale = scale - self._init_layers() - - def _init_layers(self) -> None: - if self.normalized: - self.fc = AnglularLinear(self.in_channels, self.num_classes) - else: - self.fc = nn.Linear(self.in_channels, self.num_classes) - self._init_weights() - - def _init_weights(self) -> None: - """Initialize weights of head.""" - if isinstance(self.fc, nn.Linear): - normal_init(self.fc, mean=0, std=0.01, bias=0) - - def loss(self, feats: tuple[torch.Tensor], data_samples: list[DataSample], **kwargs) -> dict: - """Calculate losses from the classification score. - - Args: - feats (tuple[Tensor]): The features extracted from the backbone. - Multiple stage inputs are acceptable but only the last stage - will be used to classify. The shape of every item should be - ``(num_samples, num_classes)``. - data_samples (List[DataSample]): The annotation data of - every samples. - **kwargs: Other keyword arguments to forward the loss module. - - Returns: - dict[str, Tensor]: a dictionary of loss components - """ - img_metas = [data_sample.metainfo for data_sample in data_samples] - cls_score = self(feats) * self.scale - valid_label_mask = self.get_valid_label_mask(img_metas).to(cls_score.device) - losses = super()._get_loss(cls_score, data_samples, valid_label_mask=valid_label_mask, **kwargs) - losses["loss"] = losses["loss"] / self.scale - return losses - - def get_valid_label_mask(self, img_metas: list[dict]) -> torch.Tensor: - """Get valid label mask using ignored_label.""" - valid_label_mask = [] - for meta in img_metas: - mask = torch.Tensor([1 for _ in range(self.num_classes)]) - if "ignored_labels" in meta and meta["ignored_labels"]: - mask[meta["ignored_labels"]] = 0 - valid_label_mask.append(mask) - return torch.stack(valid_label_mask, dim=0) - - -class AnglularLinear(nn.Module): - """Computes cos of angles between input vectors and weights vectors. - - Args: - in_features (int): Number of input features. - out_features (int): Number of output cosine logits. - """ - - def __init__(self, in_features: int, out_features: int) -> None: - """Init fuction of AngularLinear class.""" - super().__init__() - self.in_features = in_features - self.out_features = out_features - self.weight = nn.Parameter(torch.Tensor(self.out_features, self.in_features)) - self.weight.data.normal_().renorm_(2, 0, 1e-5).mul_(1e5) - - def forward(self, x: torch.tensor) -> torch.tensor: - """Forward fuction of AngularLinear class.""" - cos_theta = functional.normalize(x.view(x.shape[0], -1), dim=1).mm( - functional.normalize(self.weight.t(), p=2, dim=0), - ) - return cos_theta.clamp(-1, 1) diff --git a/src/otx/algo/classification/heads/custom_multilabel_non_linear_cls_head.py b/src/otx/algo/classification/heads/custom_multilabel_non_linear_cls_head.py deleted file mode 100644 index dda7800a3fc..00000000000 --- a/src/otx/algo/classification/heads/custom_multilabel_non_linear_cls_head.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""This module contains the CustomMultiLabelNonLinearClsHead implementation for MMClassification.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import torch -from mmcv.cnn import build_activation_layer -from mmengine.model import constant_init, normal_init -from mmpretrain.models.heads import MultiLabelClsHead -from mmpretrain.registry import MODELS -from torch import nn - -from .custom_multilabel_linear_cls_head import AnglularLinear - -if TYPE_CHECKING: - from mmpretrain.structures import DataSample - - -@MODELS.register_module() -class CustomMultiLabelNonLinearClsHead(MultiLabelClsHead): - """Non-linear classification head for multilabel task. - - Args: - num_classes (int): Number of categories. - in_channels (int): Number of channels in the input feature map. - hid_channels (int): Number of channels in the hidden feature map. - act_cfg (dict | optional): The configuration of the activation function. - scale (float): Positive scale parameter. - loss (dict): Config of classification loss. - dropout (bool): Whether use the dropout or not. - normalized (bool): Normalize input features and weights in the last linar layer. - """ - - def __init__( - self, - num_classes: int, - in_channels: int, - hid_channels: int = 1280, - act_cfg: dict | None = None, - scale: float = 1.0, - loss: dict | None = None, - dropout: bool = False, - normalized: bool = False, - ): - act_cfg = act_cfg if act_cfg else {"type": "ReLU"} - loss = ( - loss - if loss - else { - "type": "CrossEntropyLoss", - "use_sigmoid": True, - "reduction": "mean", - "loss_weight": 1.0, - } - ) - super().__init__(loss=loss) - - self.in_channels = in_channels - self.num_classes = num_classes - self.hid_channels = hid_channels - self.dropout = dropout - self.normalized = normalized - self.scale = scale - - if self.num_classes <= 0: - msg = f"num_classes={num_classes} must be a positive integer" - raise ValueError(msg) - - self._init_layers(act_cfg) - - def _init_layers(self, act_cfg: dict) -> None: - """Initialize the layers.""" - modules = [ - nn.Linear(self.in_channels, self.hid_channels), - nn.BatchNorm1d(self.hid_channels), - build_activation_layer(act_cfg), - ] - if self.dropout: - modules.append(nn.Dropout(p=0.2)) - if self.normalized: - modules.append(AnglularLinear(self.hid_channels, self.num_classes)) - else: - modules.append(nn.Linear(self.hid_channels, self.num_classes)) - - self.classifier = nn.Sequential(*modules) - self._init_weights() - - def _init_weights(self) -> None: - """Iniitalize weights of model.""" - for module in self.classifier: - if isinstance(module, nn.Linear): - normal_init(module, mean=0, std=0.01, bias=0) - elif isinstance(module, nn.BatchNorm1d): - constant_init(module, 1) - - def forward(self, feats: tuple[torch.Tensor]) -> torch.Tensor: - """The forward process.""" - pre_logits = self.pre_logits(feats) - return self.classifier(pre_logits) - - def loss(self, feats: tuple[torch.Tensor], data_samples: list[DataSample], **kwargs) -> dict: - """Calculate losses from the classification score. - - Args: - feats (tuple[Tensor]): The features extracted from the backbone. - Multiple stage inputs are acceptable but only the last stage - will be used to classify. The shape of every item should be - ``(num_samples, num_classes)``. - data_samples (List[DataSample]): The annotation data of - every samples. - **kwargs: Other keyword arguments to forward the loss module. - - Returns: - dict[str, Tensor]: a dictionary of loss components - """ - img_metas = [data_sample.metainfo for data_sample in data_samples] - cls_score = self(feats) * self.scale - valid_label_mask = self.get_valid_label_mask(img_metas).to(cls_score.device) - - losses = super()._get_loss(cls_score, data_samples, valid_label_mask=valid_label_mask, **kwargs) - losses["loss"] = losses["loss"] / self.scale - return losses - - def get_valid_label_mask(self, img_metas: list[dict]) -> torch.Tensor: - """Get valid label mask using ignored_label.""" - valid_label_mask = [] - for meta in img_metas: - mask = torch.Tensor([1 for _ in range(self.num_classes)]) - if "ignored_labels" in meta and meta["ignored_labels"]: - mask[meta["ignored_labels"]] = 0 - valid_label_mask.append(mask) - return torch.stack(valid_label_mask, dim=0) diff --git a/src/otx/algo/classification/losses/__init__.py b/src/otx/algo/classification/losses/__init__.py deleted file mode 100644 index 31fa26ede6d..00000000000 --- a/src/otx/algo/classification/losses/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Backbone modules for OTX custom model.""" - -from .asymmetric_angular_loss_with_ignore import AsymmetricAngularLossWithIgnore - -__all__ = ["AsymmetricAngularLossWithIgnore"] diff --git a/src/otx/algo/classification/losses/asymmetric_angular_loss_with_ignore.py b/src/otx/algo/classification/losses/asymmetric_angular_loss_with_ignore.py deleted file mode 100644 index 7fd54e140b4..00000000000 --- a/src/otx/algo/classification/losses/asymmetric_angular_loss_with_ignore.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for defining AsymmetricAngularLossWithIgnore.""" - -from __future__ import annotations - -import torch -from mmpretrain.models.losses.utils import weight_reduce_loss -from mmpretrain.registry import MODELS -from torch import nn - - -def asymmetric_angular_loss_with_ignore( - pred: torch.tensor, - target: torch.tensor, - valid_label_mask: torch.tensor | None = None, - weight: torch.tensor | None = None, - gamma_pos: float = 0.0, - gamma_neg: float = 1.0, - clip: float = 0.05, - k: float = 0.8, - reduction: str = "mean", - avg_factor: int | None = None, -) -> nn.Module: - """Asymmetric angular loss. - - Args: - pred (torch.Tensor): The prediction with shape (N, *). - target (torch.Tensor): The ground truth label of the prediction with - shape (N, *). - valid_label_mask (torch.Tensor, optional): Label mask for consideration - of ignored label. - weight (torch.Tensor, optional): Sample-wise loss weight with shape - (N, ). Dafaults to None. - gamma_pos (float): positive focusing parameter. Defaults to 0.0. - gamma_neg (float): Negative focusing parameter. We usually set - gamma_neg > gamma_pos. Defaults to 1.0. - k (float): positive balance parameter. Defaults to 0.8. - clip (float, optional): Probability margin. Defaults to 0.05. - reduction (str): The method used to reduce the loss. - Options are "none", "mean" and "sum". If reduction is 'none' , loss - is same shape as pred and label. Defaults to 'mean'. - avg_factor (int, optional): Average factor that is used to average - the loss. Defaults to None. - - Returns: - torch.Tensor: Loss. - """ - if pred.shape != target.shape: - msg = "pred and target should be in the same shape." - raise ValueError(msg) - - eps = 1e-8 - target = target.type_as(pred) - anti_target = 1 - target - - xs_pos = torch.sigmoid(pred) - xs_neg = torch.sigmoid(-pred) - - if clip > 0: - xs_neg = (xs_neg + clip).clamp(max=1) - - asymmetric_focus = gamma_pos > 0 or gamma_neg > 0 - if asymmetric_focus: - pos_target0 = xs_neg * target - pos_target1 = xs_pos * anti_target - pos_target = pos_target0 + pos_target1 - one_sided_gamma = gamma_pos * target + gamma_neg * anti_target - one_sided_w = torch.pow(pos_target, one_sided_gamma) - - loss = -k * target * torch.log(xs_pos.clamp(min=eps)) - (1 - k) * anti_target * torch.log(xs_neg.clamp(min=eps)) - - if asymmetric_focus: - loss *= one_sided_w - - if valid_label_mask is not None: - loss = loss * valid_label_mask - - if weight is not None: - if weight.dim() != 1: - raise ValueError - weight = weight.float() - if pred.dim() > 1: - weight = weight.reshape(-1, 1) - if reduction != "mean": - avg_factor = None - return weight_reduce_loss(loss, weight, reduction, avg_factor) - - -@MODELS.register_module() -class AsymmetricAngularLossWithIgnore(nn.Module): - """Asymmetric angular loss. - - Args: - gamma_pos (float): positive focusing parameter. - Defaults to 0.0. - gamma_neg (float): Negative focusing parameter. We - usually set gamma_neg > gamma_pos. Defaults to 1.0. - k (float): positive balance parameter. Defaults to 0.8. - clip (float): Probability margin. Defaults to 0.05. - reduction (str): The method used to reduce the loss into - a scalar. - loss_weight (float): Weight of loss. Defaults to 1.0. - """ - - def __init__( - self, - gamma_pos: float = 0.0, - gamma_neg: float = 1.0, - k: float = 0.8, - clip: float = 0.05, - reduction: str = "mean", - loss_weight: float = 1.0, - ): - """Init fuction of AsymmetricAngularLossWithIgnore class.""" - super().__init__() - self.gamma_pos = gamma_pos - self.gamma_neg = gamma_neg - self.k = k - self.clip = clip - self.reduction = reduction - self.loss_weight = loss_weight - - def forward( - self, - pred: torch.tensor, - target: torch.tensor, - valid_label_mask: torch.tensor | None = None, - weight: torch.tensor | None = None, - avg_factor: int | None = None, - reduction_override: str | None = None, - ) -> torch.tensor: - """Asymmetric angular loss.""" - if reduction_override not in (None, "none", "mean", "sum"): - msg = f"reduction_override should be none / mean / sum / None, {reduction_override}" - raise ValueError(msg) - reduction = reduction_override if reduction_override else self.reduction - - return self.loss_weight * asymmetric_angular_loss_with_ignore( - pred, - target, - valid_label_mask, - weight, - gamma_pos=self.gamma_pos, - gamma_neg=self.gamma_neg, - k=self.k, - clip=self.clip, - reduction=reduction, - avg_factor=avg_factor, - ) diff --git a/src/otx/algo/classification/mmconfigs/hlabel_classification/deit_tiny.yaml b/src/otx/algo/classification/mmconfigs/hlabel_classification/deit_tiny.yaml deleted file mode 100644 index 1624c0982fe..00000000000 --- a/src/otx/algo/classification/mmconfigs/hlabel_classification/deit_tiny.yaml +++ /dev/null @@ -1,40 +0,0 @@ -load_from: https://download.openmmlab.com/mmclassification/v0/deit/deit-tiny_pt-4xb256_in1k_20220218-13b382a0.pth -backbone: - arch: deit-tiny - type: VisionTransformer - img_size: 224 - patch_size: 16 -head: - num_multiclass_heads: 0 - num_multilabel_classes: 0 - in_channels: 192 - num_classes: 1000 - multiclass_loss_cfg: - loss_weight: 1.0 - type: CrossEntropyLoss - multilabel_loss_cfg: - reduction: sum - gamma_neg: 1.0 - gamma_pos: 0.0 - type: AsymmetricAngularLossWithIgnore - type: CustomHierarchicalLinearClsHead -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -init_cfg: - - std: 0.2 - layer: Linear - type: TruncNormal - - bias: 0. - val: 1. - layer: LayerNorm - type: Constant -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/hlabel_classification/efficientnet_b0_light.yaml b/src/otx/algo/classification/mmconfigs/hlabel_classification/efficientnet_b0_light.yaml deleted file mode 100644 index 0ea798683c1..00000000000 --- a/src/otx/algo/classification/mmconfigs/hlabel_classification/efficientnet_b0_light.yaml +++ /dev/null @@ -1,32 +0,0 @@ -backbone: - version: b0 - pretrained: true - type: OTXEfficientNet -head: - num_multiclass_heads: 0 - num_multilabel_classes: 0 - in_channels: 1280 - num_classes: 1000 - multiclass_loss_cfg: - loss_weight: 1.0 - type: CrossEntropyLoss - multilabel_loss_cfg: - reduction: sum - gamma_neg: 1.0 - gamma_pos: 0.0 - type: AsymmetricAngularLossWithIgnore - type: CustomHierarchicalLinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/hlabel_classification/efficientnet_v2_light.yaml b/src/otx/algo/classification/mmconfigs/hlabel_classification/efficientnet_v2_light.yaml deleted file mode 100644 index 49250229bc4..00000000000 --- a/src/otx/algo/classification/mmconfigs/hlabel_classification/efficientnet_v2_light.yaml +++ /dev/null @@ -1,33 +0,0 @@ -backbone: - pretrained: true - type: OTXEfficientNetV2 -head: - num_multiclass_heads: 0 - num_multilabel_classes: 0 - in_channels: 1280 - num_classes: 1000 - multiclass_loss_cfg: - loss_weight: 1.0 - type: CrossEntropyLoss - multilabel_loss_cfg: - reduction: sum - gamma_neg: 1.0 - gamma_pos: 0.0 - type: AsymmetricAngularLossWithIgnore - normalized: true - scale: 7.0 - type: CustomHierarchicalLinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/hlabel_classification/mobilenet_v3_large_light.yaml b/src/otx/algo/classification/mmconfigs/hlabel_classification/mobilenet_v3_large_light.yaml deleted file mode 100644 index b8a97af3183..00000000000 --- a/src/otx/algo/classification/mmconfigs/hlabel_classification/mobilenet_v3_large_light.yaml +++ /dev/null @@ -1,30 +0,0 @@ -backbone: - type: OTXMobileNetV3 -head: - num_multiclass_heads: 0 - num_multilabel_classes: 0 - in_channels: 960 - num_classes: 1000 - multiclass_loss_cfg: - loss_weight: 1.0 - type: CrossEntropyLoss - multilabel_loss_cfg: - reduction: sum - gamma_neg: 1.0 - gamma_pos: 0.0 - type: AsymmetricAngularLossWithIgnore - type: CustomHierarchicalNonLinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multiclass_classification/deit_tiny.yaml b/src/otx/algo/classification/mmconfigs/multiclass_classification/deit_tiny.yaml deleted file mode 100644 index 3832334c0d0..00000000000 --- a/src/otx/algo/classification/mmconfigs/multiclass_classification/deit_tiny.yaml +++ /dev/null @@ -1,33 +0,0 @@ -load_from: https://download.openmmlab.com/mmclassification/v0/deit/deit-tiny_pt-4xb256_in1k_20220218-13b382a0.pth -backbone: - arch: deit-tiny - type: VisionTransformer - img_size: 224 - patch_size: 16 -head: - loss: - loss_weight: 1.0 - type: CrossEntropyLoss - in_channels: 192 - num_classes: 1000 - type: VisionTransformerClsHead -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -init_cfg: - - std: 0.2 - layer: Linear - type: TruncNormal - - bias: 0. - val: 1. - layer: LayerNorm - type: Constant -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_b0.yaml b/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_b0.yaml deleted file mode 100644 index 4a6496424a7..00000000000 --- a/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_b0.yaml +++ /dev/null @@ -1,39 +0,0 @@ -backbone: - version: b0 - pretrained: true - type: OTXEfficientNet -head: - act_cfg: - type: HSwish - dropout_rate: 0.2 - in_channels: 1280 - init_cfg: - bias: 0.0 - layer: Linear - mean: 0.0 - std: 0.01 - type: Normal - loss: - loss_weight: 1.0 - type: CrossEntropyLoss - mid_channels: - - 1280 - num_classes: 1000 - topk: - - 1 - - 5 - type: StackedLinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_b0_light.yaml b/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_b0_light.yaml deleted file mode 100644 index 38b5e8c373b..00000000000 --- a/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_b0_light.yaml +++ /dev/null @@ -1,28 +0,0 @@ -backbone: - version: b0 - pretrained: true - type: OTXEfficientNet -head: - in_channels: 1280 - loss: - loss_weight: 1.0 - type: CrossEntropyLoss - num_classes: 1000 - topk: - - 1 - - 5 - type: LinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_v2.yaml b/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_v2.yaml deleted file mode 100644 index 077d00d3a6c..00000000000 --- a/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_v2.yaml +++ /dev/null @@ -1,38 +0,0 @@ -backbone: - pretrained: true - type: OTXEfficientNetV2 -head: - act_cfg: - type: HSwish - dropout_rate: 0.2 - in_channels: 1280 - init_cfg: - bias: 0.0 - layer: Linear - mean: 0.0 - std: 0.01 - type: Normal - loss: - loss_weight: 1.0 - type: CrossEntropyLoss - mid_channels: - - 1280 - num_classes: 1000 - topk: - - 1 - - 5 - type: StackedLinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_v2_light.yaml b/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_v2_light.yaml deleted file mode 100644 index c2599fa9605..00000000000 --- a/src/otx/algo/classification/mmconfigs/multiclass_classification/efficientnet_v2_light.yaml +++ /dev/null @@ -1,27 +0,0 @@ -backbone: - pretrained: true - type: OTXEfficientNetV2 -head: - in_channels: 1280 - loss: - loss_weight: 1.0 - type: CrossEntropyLoss - num_classes: 1000 - topk: - - 1 - - 5 - type: LinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multiclass_classification/mobilenet_v3_large.yaml b/src/otx/algo/classification/mmconfigs/multiclass_classification/mobilenet_v3_large.yaml deleted file mode 100644 index 73e539b12fa..00000000000 --- a/src/otx/algo/classification/mmconfigs/multiclass_classification/mobilenet_v3_large.yaml +++ /dev/null @@ -1,37 +0,0 @@ -backbone: - type: OTXMobileNetV3 -head: - act_cfg: - type: HSwish - dropout_rate: 0.2 - in_channels: 960 - init_cfg: - bias: 0.0 - layer: Linear - mean: 0.0 - std: 0.01 - type: Normal - loss: - loss_weight: 1.0 - type: CrossEntropyLoss - mid_channels: - - 1280 - num_classes: 1000 - topk: - - 1 - - 5 - type: StackedLinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multiclass_classification/mobilenet_v3_large_light.yaml b/src/otx/algo/classification/mmconfigs/multiclass_classification/mobilenet_v3_large_light.yaml deleted file mode 100644 index 5da80351d46..00000000000 --- a/src/otx/algo/classification/mmconfigs/multiclass_classification/mobilenet_v3_large_light.yaml +++ /dev/null @@ -1,26 +0,0 @@ -backbone: - type: OTXMobileNetV3 -head: - in_channels: 960 - loss: - loss_weight: 1.0 - type: CrossEntropyLoss - num_classes: 1000 - topk: - - 1 - - 5 - type: LinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multilabel_classification/deit_tiny.yaml b/src/otx/algo/classification/mmconfigs/multilabel_classification/deit_tiny.yaml deleted file mode 100644 index 7074faf4632..00000000000 --- a/src/otx/algo/classification/mmconfigs/multilabel_classification/deit_tiny.yaml +++ /dev/null @@ -1,32 +0,0 @@ -load_from: https://download.openmmlab.com/mmclassification/v0/deit/deit-tiny_pt-4xb256_in1k_20220218-13b382a0.pth -backbone: - arch: deit-tiny - type: VisionTransformer - img_size: 224 - patch_size: 16 -head: - in_channels: 192 - num_classes: 1000 - loss: - type: AsymmetricAngularLossWithIgnore - type: CustomMultiLabelLinearClsHead -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -init_cfg: - - std: 0.2 - layer: Linear - type: TruncNormal - - bias: 0. - val: 1. - layer: LayerNorm - type: Constant -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multilabel_classification/efficientnet_b0_light.yaml b/src/otx/algo/classification/mmconfigs/multilabel_classification/efficientnet_b0_light.yaml deleted file mode 100644 index 8e4cc2af946..00000000000 --- a/src/otx/algo/classification/mmconfigs/multilabel_classification/efficientnet_b0_light.yaml +++ /dev/null @@ -1,29 +0,0 @@ -backbone: - version: b0 - pretrained: true - type: OTXEfficientNet -head: - num_classes: 1000 - in_channels: 1280 - loss: - reduction: sum - gamma_neg: 1.0 - gamma_pos: 0.0 - type: AsymmetricAngularLossWithIgnore - normalized: true - scale: 7.0 - type: CustomMultiLabelLinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multilabel_classification/efficientnet_v2_light.yaml b/src/otx/algo/classification/mmconfigs/multilabel_classification/efficientnet_v2_light.yaml deleted file mode 100644 index 29eb048563c..00000000000 --- a/src/otx/algo/classification/mmconfigs/multilabel_classification/efficientnet_v2_light.yaml +++ /dev/null @@ -1,28 +0,0 @@ -backbone: - pretrained: true - type: OTXEfficientNetV2 -head: - in_channels: 1280 - num_classes: 1000 - loss: - reduction: sum - gamma_neg: 1.0 - gamma_pos: 0.0 - type: AsymmetricAngularLossWithIgnore - normalized: true - scale: 7.0 - type: CustomMultiLabelLinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mmconfigs/multilabel_classification/mobilenet_v3_large_light.yaml b/src/otx/algo/classification/mmconfigs/multilabel_classification/mobilenet_v3_large_light.yaml deleted file mode 100644 index b183d39773c..00000000000 --- a/src/otx/algo/classification/mmconfigs/multilabel_classification/mobilenet_v3_large_light.yaml +++ /dev/null @@ -1,30 +0,0 @@ -backbone: - type: OTXMobileNetV3 -head: - num_classes: 1000 - in_channels: 960 - hid_channels: 1280 - loss: - reduction: sum - gamma_neg: 1.0 - gamma_pos: 0.0 - type: AsymmetricAngularLossWithIgnore - normalized: true - scale: 7.0 - act_cfg: - type: PReLU - type: CustomMultiLabelNonLinearClsHead -neck: - type: GlobalAveragePooling -data_preprocessor: - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: False - type: ClsDataPreprocessor -type: ImageClassifier diff --git a/src/otx/algo/classification/mobilenet_v3_large.py b/src/otx/algo/classification/mobilenet_v3_large.py deleted file mode 100644 index e187291fc48..00000000000 --- a/src/otx/algo/classification/mobilenet_v3_large.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""MobileNetV3 model implementation.""" -from __future__ import annotations - -from typing import Any - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.classification import ( - MMPretrainHlabelClsModel, - MMPretrainMulticlassClsModel, - MMPretrainMultilabelClsModel, -) - - -class MobileNetV3ForHLabelCls(MMPretrainHlabelClsModel): - """MobileNetV3 Model for hierarchical label classification task.""" - - def __init__(self, num_classes: int, num_multiclass_heads: int, num_multilabel_classes: int) -> None: - self.num_multiclass_heads = num_multiclass_heads - self.num_multilabel_classes = num_multilabel_classes - - config = read_mmconfig(model_name="mobilenet_v3_large_light", subdir_name="hlabel_classification") - config.head.num_multiclass_heads = num_multiclass_heads - config.head.num_multilabel_classes = num_multilabel_classes - super().__init__(num_classes=num_classes, config=config) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parent_parameters = super()._export_parameters - parent_parameters.update({"via_onnx": True}) - return parent_parameters - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_mobilenet_v3_ckpt(state_dict, "hlabel", add_prefix) - - -class MobileNetV3ForMulticlassCls(MMPretrainMulticlassClsModel): - """MobileNetV3 Model for multi-label classification task.""" - - def __init__(self, num_classes: int, light: bool = False) -> None: - model_name = "mobilenet_v3_large_light" if light else "mobilenet_v3_large" - config = read_mmconfig(model_name=model_name, subdir_name="multiclass_classification") - super().__init__(num_classes=num_classes, config=config) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parent_parameters = super()._export_parameters - parent_parameters.update({"via_onnx": True}) - return parent_parameters - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_mobilenet_v3_ckpt(state_dict, "multiclass", add_prefix) - - -class MobileNetV3ForMultilabelCls(MMPretrainMultilabelClsModel): - """MobileNetV3 Model for multi-class classification task.""" - - def __init__(self, num_classes: int) -> None: - config = read_mmconfig("mobilenet_v3_large_light", subdir_name="multilabel_classification") - super().__init__(num_classes=num_classes, config=config) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parent_parameters = super()._export_parameters - parent_parameters.update({"via_onnx": True}) - return parent_parameters - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_cls_mobilenet_v3_ckpt(state_dict, "multilabel", add_prefix) diff --git a/src/otx/algo/classification/otx_dino_v2.py b/src/otx/algo/classification/otx_dino_v2.py deleted file mode 100644 index bd4d8483350..00000000000 --- a/src/otx/algo/classification/otx_dino_v2.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""DINO-V2 model for the OTX classification.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -import torch -from torch import nn - -from otx.core.data.entity.base import OTXBatchLossEntity -from otx.core.data.entity.classification import ( - MulticlassClsBatchDataEntity, - MulticlassClsBatchPredEntity, -) -from otx.core.exporter.base import OTXModelExporter -from otx.core.exporter.native import OTXNativeModelExporter -from otx.core.model.entity.classification import OTXMulticlassClsModel -from otx.core.utils.config import inplace_num_classes - -if TYPE_CHECKING: - from omegaconf import DictConfig - - -class DINOv2(nn.Module): - """DINO-v2 Model.""" - - def __init__( - self, - backbone_name: str, - freeze_backbone: bool, - head_in_channels: int, - num_classes: int, - ): - super().__init__() - self.backbone = torch.hub.load( - repo_or_dir="facebookresearch/dinov2", - model=backbone_name, - ) - - if freeze_backbone: - self._freeze_backbone(self.backbone) - - self.head = nn.Linear( - head_in_channels, - num_classes, - ) - - self.loss = nn.CrossEntropyLoss() - self.softmax = nn.Softmax() - - def _freeze_backbone(self, backbone: nn.Module) -> None: - """Freeze the backbone.""" - for _, v in backbone.named_parameters(): - v.requires_grad = False - - def forward(self, imgs: torch.Tensor, labels: torch.Tensor = None) -> torch.Tensor: - """Forward function.""" - feats = self.backbone(imgs) - logits = self.head(feats) - if self.training: - return self.loss(logits, labels) - return self.softmax(logits) - - -class DINOv2RegisterClassifier(OTXMulticlassClsModel): - """DINO-v2 Classification Model with register.""" - - def __init__(self, num_classes: int, config: DictConfig) -> None: - config = inplace_num_classes(cfg=config, num_classes=num_classes) - self.config = config - super().__init__(num_classes=num_classes) # create the model - - def _create_model(self) -> nn.Module: - """Create the model.""" - return DINOv2( - backbone_name=self.config.backbone.name, - freeze_backbone=self.config.backbone.frozen, - head_in_channels=self.config.head.in_channels, - num_classes=self.config.head.num_classes, - ) - - def _customize_inputs(self, entity: MulticlassClsBatchDataEntity) -> dict[str, Any]: - """Customize the inputs for the model.""" - return { - "imgs": entity.stacked_images, - "labels": torch.cat(entity.labels), - } - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: MulticlassClsBatchDataEntity, - ) -> MulticlassClsBatchPredEntity | OTXBatchLossEntity: - """Customize the outputs for the model.""" - if self.training: - if not isinstance(outputs, torch.Tensor): - raise TypeError(outputs) - - losses = OTXBatchLossEntity() - losses["loss"] = outputs - return losses - - max_pred_elements, max_pred_idxs = torch.max(outputs, dim=1) - pred_scores = max_pred_elements - pred_labels = max_pred_idxs - - scores = torch.unbind(pred_scores, dim=0) - labels = torch.unbind(pred_labels, dim=0) - - return MulticlassClsBatchPredEntity( - batch_size=pred_labels.shape[0], - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - labels=labels, - ) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - export_params: dict[str, Any] = {} - - export_params["resize_mode"] = "standard" - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - export_params["via_onnx"] = False - export_params["input_size"] = (1, 3, 224, 224) - export_params["onnx_export_configuration"] = None - export_params["mean"] = [123.675, 116.28, 103.53] - export_params["std"] = [58.395, 57.12, 57.375] - - parent_parameters = super()._export_parameters - parent_parameters.update(export_params) - - return parent_parameters - - @property - def _exporter(self) -> OTXModelExporter: - """Creates OTXModelExporter object that can export the model.""" - return OTXNativeModelExporter(**self._export_parameters) - - @property - def _optimization_config(self) -> dict[str, Any]: - """PTQ config for DinoV2Cls.""" - return {"model_type": "transformer"} diff --git a/src/otx/algo/classification/torchvision_model.py b/src/otx/algo/classification/torchvision_model.py deleted file mode 100644 index a93b42bb4c7..00000000000 --- a/src/otx/algo/classification/torchvision_model.py +++ /dev/null @@ -1,255 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""Torchvision model for the OTX classification.""" - -from __future__ import annotations - -from typing import Any, Literal - -import torch -from torch import nn -from torchvision import models, tv_tensors - -from otx.core.data.entity.base import OTXBatchLossEntity -from otx.core.data.entity.classification import ( - MulticlassClsBatchDataEntity, - MulticlassClsBatchPredEntity, - MulticlassClsBatchPredEntityWithXAI, -) -from otx.core.model.entity.classification import OTXMulticlassClsModel - -TV_WEIGHTS = { - "resnet50": models.ResNet50_Weights.IMAGENET1K_V2, - "efficientnet_b0": models.EfficientNet_B0_Weights.IMAGENET1K_V1, - "efficientnet_b1": models.EfficientNet_B1_Weights.IMAGENET1K_V2, - "efficientnet_b3": models.EfficientNet_B3_Weights.IMAGENET1K_V1, # Balanced - "efficientnet_b4": models.EfficientNet_B4_Weights.IMAGENET1K_V1, - "efficientnet_v2_l": models.EfficientNet_V2_L_Weights.IMAGENET1K_V1, # Accuracy - "mobilenet_v3_small": models.MobileNet_V3_Small_Weights.IMAGENET1K_V1, # Speed -} - - -class TVModelWithLossComputation(nn.Module): - """TorchVision Model with Loss Computation. - - This class represents a TorchVision model with loss computation for classification tasks. - It takes a backbone model, number of classes, and an optional loss function as input. - - Args: - backbone ( - Literal["resnet50", "efficientnet_b0", "efficientnet_b1", "efficientnet_b3", - "efficientnet_b4", "efficientnet_v2_l", "mobilenet_v3_small"]): - The backbone model to use for feature extraction. - num_classes (int): The number of classes for the classification task. - loss (nn.Module | None, optional): The loss function to use. - - Methods: - forward(images: torch.Tensor, labels: torch.Tensor) -> torch.Tensor: - Performs forward pass of the model. - - """ - - def __init__( - self, - backbone: Literal[ - "resnet50", - "efficientnet_b0", - "efficientnet_b1", - "efficientnet_b3", - "efficientnet_b4", - "efficientnet_v2_l", - "mobilenet_v3_small", - ], - num_classes: int, - loss: nn.Module | None = None, - ) -> None: - super().__init__() - self.num_classes = num_classes - net = getattr(models, backbone)(weights=TV_WEIGHTS[backbone]) - - self.backbone = nn.Sequential(*list(net.children())[:-1]) - - last_layer = list(net.children())[-1] - classifier_len = len(list(last_layer.children())) - if classifier_len >= 1: - feature_channel = list(last_layer.children())[-1].in_features - layers = list(last_layer.children())[:-1] - self.head = nn.Sequential(*layers, nn.Linear(feature_channel, num_classes)) - else: - feature_channel = last_layer.in_features - self.head = nn.Linear(feature_channel, num_classes) - - self.softmax = nn.Softmax(dim=-1) - self.loss = nn.CrossEntropyLoss() if loss is None else loss - - def forward( - self, - images: torch.Tensor, - labels: torch.Tensor | None = None, - mode: str = "tensor", - ) -> torch.Tensor: - """Performs forward pass of the model. - - Args: - images (torch.Tensor): The input images. - labels (torch.Tensor): The ground truth labels. - mode (str, optional): The mode of the forward pass. Defaults to "tensor". - - Returns: - torch.Tensor: The output logits or loss, depending on the training mode. - """ - feats = self.backbone(images) - if len(feats.shape) == 4: # If feats is a 4D tensor: (batch_size, channels, height, width) - feats = feats.view(feats.size(0), -1) # Flatten the output of the backbone: (batch_size, features) - logits = self.head(feats) - if mode == "tensor": - return logits - if mode == "loss": - return self.loss(logits, labels) - return self.softmax(logits) - - -class OTXTVModel(OTXMulticlassClsModel): - """OTXTVModel is that represents a TorchVision model for multiclass classification. - - Args: - backbone ( - Literal["resnet50", "efficientnet_b0", "efficientnet_b1", "efficientnet_b3", "efficientnet_b4", - "efficientnet_v2_l", "mobilenet_v3_small"]): - The backbone architecture of the model. - num_classes (int): The number of classes for classification. - loss (nn.Module | None, optional): The loss function to be used. Defaults to None. - """ - - def __init__( - self, - backbone: Literal[ - "resnet50", - "efficientnet_b0", - "efficientnet_b1", - "efficientnet_b3", - "efficientnet_b4", - "efficientnet_v2_l", - "mobilenet_v3_small", - ], - num_classes: int, - loss: nn.Module | None = None, - ) -> None: - self.backbone = backbone - self.loss = loss - - super().__init__(num_classes=num_classes) - - def _create_model(self) -> nn.Module: - return TVModelWithLossComputation( - backbone=self.backbone, - num_classes=self.num_classes, - loss=self.loss, - ) - - def _customize_inputs(self, inputs: MulticlassClsBatchDataEntity) -> dict[str, Any]: - if isinstance(inputs.images, list): - images = tv_tensors.wrap(torch.stack(inputs.images, dim=0), like=inputs.images[0]) - else: - images = inputs.images - return { - "images": images, - "labels": torch.cat(inputs.labels, dim=0), - "mode": "loss" if self.training else "predict", - } - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: MulticlassClsBatchDataEntity, - ) -> MulticlassClsBatchPredEntity | MulticlassClsBatchPredEntityWithXAI | OTXBatchLossEntity: - if self.training: - return OTXBatchLossEntity(loss=outputs) - - # To list, batch-wise - logits = outputs if isinstance(outputs, torch.Tensor) else outputs["logits"] - scores = torch.unbind(logits, 0) - preds = logits.argmax(-1, keepdim=True).unbind(0) - - if self.explain_mode: - if not isinstance(outputs, dict) or "saliency_map" not in outputs: - msg = "No saliency maps in the model output." - raise ValueError(msg) - - saliency_maps = outputs["saliency_map"].detach().cpu().numpy() - - return MulticlassClsBatchPredEntityWithXAI( - batch_size=len(preds), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - labels=preds, - saliency_maps=list(saliency_maps), - feature_vectors=[], - ) - - return MulticlassClsBatchPredEntity( - batch_size=inputs.batch_size, - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - labels=preds, - ) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - export_params: dict[str, Any] = {} - export_params["input_size"] = (1, 3, 224, 224) - export_params["resize_mode"] = "standard" - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - export_params["via_onnx"] = False - export_params["onnx_export_configuration"] = None - export_params["mean"] = [0.485, 0.456, 0.406] - export_params["std"] = [0.229, 0.224, 0.225] - - parameters = super()._export_parameters - parameters.update(export_params) - return parameters - - @staticmethod - def _forward_explain_image_classifier( - self: TVModelWithLossComputation, - images: torch.Tensor, - labels: torch.Tensor | None = None, # noqa: ARG004 - mode: str = "tensor", - ) -> dict: - """Forward func of the TVModelWithLossComputation instance.""" - x = self.backbone(images) - backbone_feat = x - - saliency_map = self.explain_fn(backbone_feat) - - if len(x.shape) == 4: - x = x.view(x.size(0), -1) - - feature_vector = x - if len(feature_vector.shape) == 1: - feature_vector = feature_vector.unsqueeze(0) - - logits = self.head(x) - if mode == "predict": - logits = self.softmax(logits) - - return { - "logits": logits, - "feature_vector": feature_vector, - "saliency_map": saliency_map, - } - - @torch.no_grad() - def head_forward_fn(self, x: torch.Tensor) -> torch.Tensor: - """Performs model's neck and head forward. Can be redefined at the model's level.""" - if (head := getattr(self.model, "head", None)) is None: - raise ValueError - - if len(x.shape) == 4: - x = x.view(x.size(0), -1) - return head(x) diff --git a/src/otx/algo/detection/__init__.py b/src/otx/algo/detection/__init__.py deleted file mode 100644 index ad8951366c0..00000000000 --- a/src/otx/algo/detection/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom model implementations for detection task.""" - -from . import backbones, heads, losses -from .ssd import SSD - -__all__ = ["backbones", "heads", "losses", "SSD"] diff --git a/src/otx/algo/detection/atss.py b/src/otx/algo/detection/atss.py deleted file mode 100644 index 58eb1631780..00000000000 --- a/src/otx/algo/detection/atss.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""ATSS model implementations.""" - -from __future__ import annotations - -from typing import Any, Literal - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.detection import MMDetCompatibleModel - - -class ATSS(MMDetCompatibleModel): - """ATSS Model.""" - - def __init__(self, num_classes: int, variant: Literal["mobilenetv2", "r50_fpn", "resnext101"]) -> None: - model_name = f"atss_{variant}" - config = read_mmconfig(model_name=model_name) - super().__init__(num_classes=num_classes, config=config) - self.image_size = (1, 3, 736, 992) - self.tile_image_size = self.image_size - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - export_params = super()._export_parameters - export_params["deploy_cfg"] = "otx.algo.detection.mmdeploy.atss" - export_params["input_size"] = self.image_size - export_params["resize_mode"] = "standard" - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - - return export_params - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_det_ckpt(state_dict, add_prefix) - - -class ATSSR50FPN(ATSS): - """ATSSR50FPN Model.""" - - def __init__(self, num_classes: int) -> None: - super().__init__(num_classes=num_classes, variant="r50_fpn") - self.image_size = (1, 3, 800, 1333) - self.tile_image_size = self.image_size - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - export_params = super()._export_parameters - export_params["deploy_cfg"] = "otx.algo.detection.mmdeploy.atss_r50_fpn" - export_params["input_size"] = self.image_size - export_params["resize_mode"] = "standard" # [TODO](@Eunwoo): need to revert it to fit_to_window after resolving - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - - return export_params diff --git a/src/otx/algo/detection/backbones/__init__.py b/src/otx/algo/detection/backbones/__init__.py deleted file mode 100644 index 0872bf09da9..00000000000 --- a/src/otx/algo/detection/backbones/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom backbone implementations for detection task.""" - -from . import pytorchcv_backbones - -__all__ = ["pytorchcv_backbones"] diff --git a/src/otx/algo/detection/backbones/pytorchcv_backbones.py b/src/otx/algo/detection/backbones/pytorchcv_backbones.py deleted file mode 100644 index fd10250dc00..00000000000 --- a/src/otx/algo/detection/backbones/pytorchcv_backbones.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Backbone of pytorchcv for mmdetection backbones.""" - -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING - -import torch -from mmcv.cnn import build_activation_layer, build_norm_layer -from mmdet.registry import MODELS -from mmengine.dist import get_dist_info -from pytorchcv.model_provider import _models -from pytorchcv.models.model_store import download_model -from torch import distributed, nn -from torch.nn.modules.batchnorm import _BatchNorm - -if TYPE_CHECKING: - from mmdet.registry import Registry - from mmengine.config import Config, ConfigDict - -# ruff: noqa: SLF001 - - -def replace_activation(model: nn.Module, activation_cfg: dict) -> nn.Module: - """Replace activate funtion.""" - for name, module in model._modules.items(): - if len(list(module.children())) > 0: - model._modules[name] = replace_activation(module, activation_cfg) - if "activ" in name: - if activation_cfg["type"] == "torch_swish": - model._modules[name] = nn.SiLU() - else: - model._modules[name] = build_activation_layer(activation_cfg) - return model - - -def replace_norm(model: nn.Module, cfg: dict) -> nn.Module: - """Replace norm funtion.""" - for name, module in model._modules.items(): - if len(list(module.children())) > 0: - model._modules[name] = replace_norm(module, cfg) - if "bn" in name: - model._modules[name] = build_norm_layer(cfg, num_features=module.num_features)[1] - return model - - -def multioutput_forward(self: nn.Module, x: torch.Tensor) -> list[torch.Tensor]: - """Multioutput forward function for new model (copy from mmdet older).""" - outputs: list[torch.Tensor] = [] - - last_stage = max(self.out_indices) - for i, stage in enumerate(self.features): - x = stage(x) - s_verbose = str(i) + " " + str(x.shape) - if i in self.out_indices: - outputs.append(x) - s_verbose += "*" - if self.verbose: - print(s_verbose) - if i == last_stage: - break - - return outputs - - -def train(self: nn.Module, mode: bool = True) -> None: - """Train forward function for new model (copy from mmdet older).""" - super(self.__class__, self).train(mode) - - for i in range(self.frozen_stages + 1): - feature = self.features[i] - feature.eval() - for param in feature.parameters(): - param.requires_grad = False - - if mode and self.norm_eval: - for module in self.modules(): - # trick: eval have effect on BatchNorm only - if isinstance(module, _BatchNorm): - module.eval() - - -def init_weights(self: nn.Module, pretrained: bool = True) -> None: - """Init weights function for new model (copy from mmdet).""" - if pretrained: - rank, world_size = get_dist_info() - if rank == 0: - # Make sure that model is fetched to the local storage. - download_model(net=self, model_name=self.model_name, local_model_store_dir_path=self.models_cache_root) - if world_size > 1: - distributed.barrier() - else: - # Wait for model to be in the local storage, then load it. - distributed.barrier() - download_model(net=self, model_name=self.model_name, local_model_store_dir_path=self.models_cache_root) - - -ori_build_func = MODELS.build_func - - -def _pytorchcv_model_reduce(self) -> nn.Module: # noqa: ANN001 - return (_build_model_including_pytorchcv, (self.otx_cfg,)) - - -def _build_model_including_pytorchcv( - cfg: dict | ConfigDict | Config, - registry: Registry = MODELS, - default_args: dict | ConfigDict | Config | None = None, -) -> nn.Module: - """Try to build model from mmdet first and build from pytorchcv.""" - try: - model = ori_build_func(cfg, registry, default_args) - except KeyError: # build from pytorchcv - args = cfg.copy() - if default_args is not None: - for name, value in default_args.items(): - args.setdefault(name, value) - - model = _build_pytorchcv_model(**args) - - # support pickle - model.otx_cfg = args - model.__class__.__reduce__ = _pytorchcv_model_reduce.__get__(model, model.__class__) - - return model - - -def _build_pytorchcv_model( - type: str, # noqa: A002 - out_indices: list[int], - frozen_stages: int = 0, - norm_eval: bool = False, - verbose: bool = False, - activation_cfg: dict | None = None, - norm_cfg: dict | None = None, - **kwargs, -) -> nn.Module: - """Build pytorchcv model.""" - models_cache_root = kwargs.get("root", Path.home() / ".torch" / "models") - is_pretrained = kwargs.get("pretrained", False) - print( - f"Init model {type}, pretrained={is_pretrained}, models cache {models_cache_root}", - ) - model = _models[type](**kwargs) - if activation_cfg: - model = replace_activation(model, activation_cfg) - if norm_cfg: - model = replace_norm(model, norm_cfg) - model.out_indices = out_indices - model.frozen_stages = frozen_stages - model.norm_eval = norm_eval - model.verbose = verbose - model.model_name = type - model.models_cache_root = models_cache_root - if hasattr(model, "features") and isinstance(model.features, nn.Sequential): - # Save original forward, just in case. - model.forward_single_output = model.forward - model.forward = multioutput_forward.__get__(model) - model.init_weights = init_weights.__get__(model) - model.train = train.__get__(model) - - model.output = None - for i, _ in enumerate(model.features): - if i > max(out_indices): - model.features[i] = None - else: - print( - "Failed to automatically wrap backbone network. " - f"Object of type {model.__class__} has no valid attribute called " - "'features'.", - ) - - return model - - -MODELS.build_func = _build_model_including_pytorchcv diff --git a/src/otx/algo/detection/heads/__init__.py b/src/otx/algo/detection/heads/__init__.py deleted file mode 100644 index eeea4baae58..00000000000 --- a/src/otx/algo/detection/heads/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Custom head implementations for detection task.""" - -from .custom_anchor_generator import SSDAnchorGeneratorClustered -from .custom_atss_head import CustomATSSHead -from .custom_ssd_head import CustomSSDHead - -__all__ = ["SSDAnchorGeneratorClustered", "CustomATSSHead", "CustomSSDHead"] diff --git a/src/otx/algo/detection/heads/class_incremental_mixin.py b/src/otx/algo/detection/heads/class_incremental_mixin.py deleted file mode 100644 index cbe0c9a82d7..00000000000 --- a/src/otx/algo/detection/heads/class_incremental_mixin.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Cross Dataset Detector head for Ignore labels.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import torch -from mmdet.models.utils.misc import images_to_levels, multi_apply -from mmdet.registry import MODELS -from torch import Tensor - -if TYPE_CHECKING: - from mmdet.utils import InstanceList, OptInstanceList - - -@MODELS.register_module() -class ClassIncrementalMixin: - """Head class for Ignore labels.""" - - def get_atss_targets( - self, - anchor_list: list, - valid_flag_list: list[list[Tensor]], - batch_gt_instances: InstanceList, - batch_img_metas: list[dict], - batch_gt_instances_ignore: OptInstanceList = None, - unmap_outputs: bool = True, - ) -> tuple: - """Get targets for ATSS head. - - This method is almost the same as `AnchorHead.get_targets()`. Besides - returning the targets as the parent method does, it also returns the - anchors as the first element of the returned tuple. - """ - num_imgs = len(batch_img_metas) - if not len(anchor_list) == len(valid_flag_list) == num_imgs: - msg = f"Invalid inputs, anchor_list: {len(anchor_list)}, \ - valid_flag_list: {len(valid_flag_list)}, num_imgs: {num_imgs}" - raise ValueError(msg) - - # anchor number of multi levels - num_level_anchors = [anchors.size(0) for anchors in anchor_list[0]] - num_level_anchors_list = [num_level_anchors] * num_imgs - - # concat all level anchors and flags to a single tensor - for i in range(num_imgs): - if len(anchor_list[i]) != len(valid_flag_list[i]): - msg = "anchor_list and valid_flag_list have different shape" - raise ValueError(msg) - anchor_list[i] = torch.cat(anchor_list[i]) - valid_flag_list[i] = torch.cat(valid_flag_list[i]) - - # compute targets for each image - if batch_gt_instances_ignore is None: - batch_gt_instances_ignore = [None] * num_imgs - ( - all_anchors, - all_labels, - all_label_weights, - all_bbox_targets, - all_bbox_weights, - pos_inds_list, - neg_inds_list, - sampling_results_list, - ) = multi_apply( - self._get_targets_single, # type: ignore[attr-defined] - anchor_list, - valid_flag_list, - num_level_anchors_list, - batch_gt_instances, - batch_img_metas, - batch_gt_instances_ignore, - unmap_outputs=unmap_outputs, - ) - # Get `avg_factor` of all images, which calculate in `SamplingResult`. - # When using sampling method, avg_factor is usually the sum of - # positive and negative priors. When using `PseudoSampler`, - # `avg_factor` is usually equal to the number of positive priors. - avg_factor = sum([results.avg_factor for results in sampling_results_list]) - # split targets to a list w.r.t. multiple levels - anchors_list = images_to_levels(all_anchors, num_level_anchors) - labels_list = images_to_levels(all_labels, num_level_anchors) - label_weights_list = images_to_levels(all_label_weights, num_level_anchors) - bbox_targets_list = images_to_levels(all_bbox_targets, num_level_anchors) - bbox_weights_list = images_to_levels(all_bbox_weights, num_level_anchors) - # Changed part from mmdet - valid_label_mask = self.get_valid_label_mask(img_metas=batch_img_metas, all_labels=all_labels) - valid_label_mask = [i.to(anchor_list[0].device) for i in valid_label_mask] - if len(valid_label_mask) > 0: - valid_label_mask = images_to_levels(valid_label_mask, num_level_anchors) - return ( - anchors_list, - labels_list, - label_weights_list, - bbox_targets_list, - bbox_weights_list, - avg_factor, - valid_label_mask, - ) - - def get_valid_label_mask( - self, - img_metas: list[dict], - all_labels: list[Tensor], - use_bg: bool = False, - ) -> list[Tensor]: - """Calcualte valid label mask with ignored labels.""" - num_classes = self.num_classes + 1 if use_bg else self.num_classes # type: ignore[attr-defined] - valid_label_mask = [] - for i, meta in enumerate(img_metas): - mask = torch.Tensor([1 for _ in range(num_classes)]) - if "ignored_labels" in meta and meta["ignored_labels"]: - mask[meta["ignored_labels"]] = 0 - if use_bg: - mask[self.num_classes] = 0 # type: ignore[attr-defined] - mask = mask.repeat(len(all_labels[i]), 1) - valid_label_mask.append(mask) - return valid_label_mask diff --git a/src/otx/algo/detection/heads/custom_anchor_generator.py b/src/otx/algo/detection/heads/custom_anchor_generator.py deleted file mode 100644 index 0637991847f..00000000000 --- a/src/otx/algo/detection/heads/custom_anchor_generator.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom Anchor Generator for SSD.""" - -from __future__ import annotations - -import torch -from mmdet.models.task_modules.builder import PRIOR_GENERATORS -from mmdet.models.task_modules.prior_generators import AnchorGenerator -from torch.nn.modules.utils import _pair - - -@PRIOR_GENERATORS.register_module() -class SSDAnchorGeneratorClustered(AnchorGenerator): - """Custom Anchor Generator for SSD.""" - - def __init__( - self, - strides: tuple[int], - widths: list[list[int]], - heights: list[list[int]], - ) -> None: - """Initialize SSDAnchorGeneratorClustered. - - Args: - strides (Tuple[int]): Anchor's strides. - widths (List[List[int]]): Anchor's widths. - heights (List[List[int]]): Anchor's height. - """ - self.strides = [_pair(stride) for stride in strides] - self.widths = widths - self.heights = heights - self.centers = [(stride / 2.0, stride / 2.0) for stride in strides] - - self.center_offset = 0 - self.gen_base_anchors() - self.use_box_type = False - - def gen_base_anchors(self) -> None: - """Generate base anchor for SSD.""" - multi_level_base_anchors = [] - for widths, heights, centers in zip(self.widths, self.heights, self.centers): - base_anchors = self.gen_single_level_base_anchors( - widths=torch.Tensor(widths), - heights=torch.Tensor(heights), - center=torch.Tensor(centers), - ) - multi_level_base_anchors.append(base_anchors) - self.base_anchors = multi_level_base_anchors - - def gen_single_level_base_anchors( - self, - widths: torch.Tensor, - heights: torch.Tensor, - center: torch.Tensor, - ) -> torch.Tensor: - """Generate single_level_base_anchors for SSD. - - Args: - widths (torch.Tensor): Widths of base anchors. - heights (torch.Tensor): Heights of base anchors. - center (torch.Tensor): Centers of base anchors. - """ - x_center, y_center = center - - # use float anchor and the anchor's center is aligned with the - # pixel center - base_anchors = [ - x_center - 0.5 * widths, - y_center - 0.5 * heights, - x_center + 0.5 * widths, - y_center + 0.5 * heights, - ] - return torch.stack(base_anchors, dim=-1) - - def __repr__(self) -> str: - """Str: a string that describes the module.""" - indent_str = " " - repr_str = self.__class__.__name__ + "(\n" - repr_str += f"{indent_str}strides={self.strides},\n" - repr_str += f"{indent_str}widths={self.widths},\n" - repr_str += f"{indent_str}heights={self.heights},\n" - repr_str += f"{indent_str}num_levels={self.num_levels}\n" - repr_str += f"{indent_str}centers={self.centers},\n" - repr_str += f"{indent_str}center_offset={self.center_offset})" - return repr_str diff --git a/src/otx/algo/detection/heads/custom_atss_head.py b/src/otx/algo/detection/heads/custom_atss_head.py deleted file mode 100644 index 819590795fe..00000000000 --- a/src/otx/algo/detection/heads/custom_atss_head.py +++ /dev/null @@ -1,292 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom ATSS head for OTX template.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import torch -from mmdet.models.dense_heads.atss_head import ATSSHead -from mmdet.models.utils.misc import multi_apply -from mmdet.registry import MODELS -from mmdet.structures.bbox.bbox_overlaps import bbox_overlaps -from mmdet.utils.dist_utils import reduce_mean -from torch import Tensor - -from otx.algo.detection.heads.class_incremental_mixin import ( - ClassIncrementalMixin, -) -from otx.algo.detection.losses.cross_focal_loss import ( - CrossSigmoidFocalLoss, -) - -if TYPE_CHECKING: - from mmdet.utils import InstanceList, OptInstanceList - - -EPS = 1e-12 - - -@MODELS.register_module() -class CustomATSSHead(ClassIncrementalMixin, ATSSHead): - """CustomATSSHead for OTX template.""" - - def __init__( - self, - *args, - bg_loss_weight: float = -1.0, - use_qfl: bool = False, - qfl_cfg: dict | None = None, - **kwargs, - ): - if use_qfl: - kwargs["loss_cls"] = ( - qfl_cfg - if qfl_cfg - else { - "type": "QualityFocalLoss", - "use_sigmoid": True, - "beta": 2.0, - "loss_weight": 1.0, - } - ) - super().__init__(*args, **kwargs) - self.bg_loss_weight = bg_loss_weight - self.use_qfl = use_qfl - - def loss_by_feat( - self, - cls_scores: list[Tensor], - bbox_preds: list[Tensor], - centernesses: list[Tensor], - batch_gt_instances: InstanceList, - batch_img_metas: InstanceList, - batch_gt_instances_ignore: InstanceList | None = None, - ) -> dict[str, Tensor]: - """Compute losses of the head. - - Args: - cls_scores (list[Tensor]): Box scores for each scale level - Has shape (N, num_anchors * num_classes, H, W) - bbox_preds (list[Tensor]): Box energies / deltas for each scale - level with shape (N, num_anchors * 4, H, W) - centernesses (list[Tensor]): Centerness for each scale - level with shape (N, num_anchors * 1, H, W) - batch_gt_instances (list[:obj:`InstanceData`]): Batch of - gt_instance. It usually includes ``bboxes`` and ``labels`` - attributes. - batch_img_metas (list[dict]): Meta information of each image, e.g., - image size, scaling factor, etc. - batch_gt_instances_ignore (list[:obj:`InstanceData`], Optional): - Batch of gt_instances_ignore. It includes ``bboxes`` attribute - data that is ignored during training and testing. - Defaults to None. - - Returns: - dict[str, Tensor]: A dictionary of loss components. - """ - featmap_sizes = [featmap.size()[-2:] for featmap in cls_scores] - if len(featmap_sizes) != self.prior_generator.num_levels: - msg = "featmap_sizes and self.prior_generator.num_levels have different levels." - raise ValueError(msg) - - device = cls_scores[0].device - anchor_list, valid_flag_list = self.get_anchors(featmap_sizes, batch_img_metas, device=device) - - cls_reg_targets = self.get_targets( - anchor_list, - valid_flag_list, - batch_gt_instances, - batch_img_metas, - batch_gt_instances_ignore=batch_gt_instances_ignore, - ) - - ( - anchor_list, - labels_list, - label_weights_list, - bbox_targets_list, - bbox_weights_list, - avg_factor, - valid_label_mask, - ) = cls_reg_targets - avg_factor = reduce_mean(torch.tensor(avg_factor, dtype=torch.float, device=device)).item() - - losses_cls, losses_bbox, loss_centerness, bbox_avg_factor = multi_apply( - self.loss_by_feat_single, - anchor_list, - cls_scores, - bbox_preds, - centernesses, - labels_list, - label_weights_list, - bbox_targets_list, - valid_label_mask, - avg_factor=avg_factor, - ) - - bbox_avg_factor = sum(bbox_avg_factor) - bbox_avg_factor = reduce_mean(bbox_avg_factor).clamp_(min=1).item() - losses_bbox = [loss_bbox / bbox_avg_factor for loss_bbox in losses_bbox] - return {"loss_cls": losses_cls, "loss_bbox": losses_bbox, "loss_centerness": loss_centerness} - - def loss_by_feat_single( - self, - anchors: Tensor, - cls_score: Tensor, - bbox_pred: Tensor, - centerness: Tensor, - labels: Tensor, - label_weights: Tensor, - bbox_targets: Tensor, - valid_label_mask: Tensor, - avg_factor: float, - ) -> tuple: - """Compute loss of a single scale level. - - Args: - cls_score (Tensor): Box scores for each scale level - Has shape (N, num_anchors * num_classes, H, W). - bbox_pred (Tensor): Box energies / deltas for each scale - level with shape (N, num_anchors * 4, H, W). - centerness(Tensor): Centerness scores for each scale level. - anchors (Tensor): Box reference for each scale level with shape - (N, num_total_anchors, 4). - labels (Tensor): Labels of each anchors with shape - (N, num_total_anchors). - label_weights (Tensor): Label weights of each anchor with shape - (N, num_total_anchors) - bbox_targets (Tensor): BBox regression targets of each anchor with - shape (N, num_total_anchors, 4). - avg_factor (float): Average factor that is used to average - the loss. When using sampling method, avg_factor is usually - the sum of positive and negative priors. When using - `PseudoSampler`, `avg_factor` is usually equal to the number - of positive priors. - valid_label_mask (Tensor): Label mask for consideration of ignored - label with shape (N, num_total_anchors, 1). - - Returns: - tuple[Tensor]: A tuple of loss components. - """ - anchors = anchors.reshape(-1, 4) - cls_score = cls_score.permute(0, 2, 3, 1).reshape(-1, self.cls_out_channels).contiguous() - bbox_pred = bbox_pred.permute(0, 2, 3, 1).reshape(-1, 4) - centerness = centerness.permute(0, 2, 3, 1).reshape(-1) - bbox_targets = bbox_targets.reshape(-1, 4) - labels = labels.reshape(-1) - label_weights = label_weights.reshape(-1) - valid_label_mask = valid_label_mask.reshape(-1, self.cls_out_channels) - - # FG cat_id: [0, num_classes -1], BG cat_id: num_classes - pos_inds = self._get_pos_inds(labels) - - if self.use_qfl: - quality = label_weights.new_zeros(labels.shape) - - if len(pos_inds) > 0: - pos_bbox_targets = bbox_targets[pos_inds] - pos_bbox_pred = bbox_pred[pos_inds] - pos_anchors = anchors[pos_inds] - pos_centerness = centerness[pos_inds] - - centerness_targets = self.centerness_target(pos_anchors, pos_bbox_targets) - if self.reg_decoded_bbox: - pos_bbox_pred = self.bbox_coder.decode(pos_anchors, pos_bbox_pred) - - if self.use_qfl: - quality[pos_inds] = bbox_overlaps(pos_bbox_pred.detach(), pos_bbox_targets, is_aligned=True).clamp( - min=1e-6, - ) - - # regression loss - loss_bbox = self._get_loss_bbox(pos_bbox_targets, pos_bbox_pred, centerness_targets) - - # centerness loss - loss_centerness = self._get_loss_centerness(avg_factor, pos_centerness, centerness_targets) - - else: - loss_bbox = bbox_pred.sum() * 0 - loss_centerness = centerness.sum() * 0 - centerness_targets = bbox_targets.new_tensor(0.0) - - # Re-weigting BG loss - if self.bg_loss_weight >= 0.0: - neg_indices = labels == self.num_classes - label_weights[neg_indices] = self.bg_loss_weight - - if self.use_qfl: - labels = (labels, quality) # For quality focal loss arg spec - - # classification loss - loss_cls = self._get_loss_cls(cls_score, labels, label_weights, valid_label_mask, avg_factor) - - return loss_cls, loss_bbox, loss_centerness, centerness_targets.sum() - - def _get_pos_inds(self, labels: Tensor) -> Tensor: - bg_class_ind = self.num_classes - return ((labels >= 0) & (labels < bg_class_ind)).nonzero().squeeze(1) - - def _get_loss_cls( - self, - cls_score: Tensor, - labels: Tensor, - label_weights: Tensor, - valid_label_mask: Tensor, - avg_factor: Tensor, - ) -> Tensor: - if isinstance(self.loss_cls, CrossSigmoidFocalLoss): - loss_cls = self.loss_cls( - cls_score, - labels, - label_weights, - avg_factor=avg_factor, - valid_label_mask=valid_label_mask, - ) - else: - loss_cls = self.loss_cls(cls_score, labels, label_weights, avg_factor=avg_factor) - return loss_cls - - def _get_loss_centerness( - self, - avg_factor: Tensor, - pos_centerness: Tensor, - centerness_targets: Tensor, - ) -> Tensor: - return self.loss_centerness(pos_centerness, centerness_targets, avg_factor=avg_factor) - - def _get_loss_bbox( - self, - pos_bbox_targets: Tensor, - pos_bbox_pred: Tensor, - centerness_targets: Tensor, - ) -> Tensor: - return self.loss_bbox(pos_bbox_pred, pos_bbox_targets, weight=centerness_targets, avg_factor=1.0) - - def get_targets( - self, - anchor_list: list[list[Tensor]], - valid_flag_list: list[list[Tensor]], - batch_gt_instances: InstanceList, - batch_img_metas: list[dict], - batch_gt_instances_ignore: OptInstanceList = None, - unmap_outputs: bool = True, - ) -> tuple: - """Get targets for Detection head. - - This method is almost the same as `AnchorHead.get_targets()`. Besides - returning the targets as the parent method does, it also returns the - anchors as the first element of the returned tuple. - However, if the detector's head loss uses CrossSigmoidFocalLoss, - the labels_weights_list consists of (binarized label schema * weights) of batch images - """ - return self.get_atss_targets( - anchor_list, - valid_flag_list, - batch_gt_instances, - batch_img_metas, - batch_gt_instances_ignore, - unmap_outputs, - ) diff --git a/src/otx/algo/detection/heads/custom_ssd_head.py b/src/otx/algo/detection/heads/custom_ssd_head.py deleted file mode 100644 index 258a1065b1c..00000000000 --- a/src/otx/algo/detection/heads/custom_ssd_head.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Custom SSD head for OTX template.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from mmdet.models.dense_heads.ssd_head import SSDHead -from mmdet.registry import MODELS -from torch import nn - -if TYPE_CHECKING: - from mmengine.config import Config - - -@MODELS.register_module() -class CustomSSDHead(SSDHead): - """CustomSSDHead class for OTX. - - This is workaround for bug in mmdet3.2.0 - """ - - def __init__(self, *args, loss_cls: Config | dict | None = None, **kwargs) -> None: - """Initialize CustomSSDHead.""" - super().__init__(*args, **kwargs) - if loss_cls is None: - loss_cls = { - "type": "CrossEntropyLoss", - "use_sigmoid": False, - "reduction": "none", - "loss_weight": 1.0, - } - self.loss_cls = MODELS.build(loss_cls) - - def _init_layers(self) -> None: - """Initialize layers of the head. - - This modificaiton is needed for smart weight loading - """ - self.cls_convs = nn.ModuleList() - self.reg_convs = nn.ModuleList() - - activation_config = self.act_cfg.copy() - activation_config.setdefault("inplace", True) - for in_channel, num_base_priors in zip(self.in_channels, self.num_base_priors): - if self.use_depthwise: - activation_layer = MODELS.build(activation_config) - - self.reg_convs.append( - nn.Sequential( - nn.Conv2d(in_channel, in_channel, kernel_size=3, padding=1, groups=in_channel), - nn.BatchNorm2d(in_channel), - activation_layer, - nn.Conv2d(in_channel, num_base_priors * 4, kernel_size=1, padding=0), - ), - ) - self.cls_convs.append( - nn.Sequential( - nn.Conv2d(in_channel, in_channel, kernel_size=3, padding=1, groups=in_channel), - nn.BatchNorm2d(in_channel), - activation_layer, - nn.Conv2d(in_channel, num_base_priors * self.cls_out_channels, kernel_size=1, padding=0), - ), - ) - else: - self.reg_convs.append(nn.Conv2d(in_channel, num_base_priors * 4, kernel_size=3, padding=1)) - self.cls_convs.append( - nn.Conv2d(in_channel, num_base_priors * self.cls_out_channels, kernel_size=3, padding=1), - ) diff --git a/src/otx/algo/detection/losses/__init__.py b/src/otx/algo/detection/losses/__init__.py deleted file mode 100644 index e5b9e173df0..00000000000 --- a/src/otx/algo/detection/losses/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom OTX Losses for Object Detection.""" - - -__all__ = ["CrossSigmoidFocalLoss, OrdinaryFocalLoss"] diff --git a/src/otx/algo/detection/losses/cross_focal_loss.py b/src/otx/algo/detection/losses/cross_focal_loss.py deleted file mode 100644 index abb0668109a..00000000000 --- a/src/otx/algo/detection/losses/cross_focal_loss.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Cross Focal Loss for ignore labels.""" - -from __future__ import annotations - -import torch -import torch.nn.functional as F # noqa: N812 -from mmdet.models.losses.focal_loss import py_sigmoid_focal_loss, sigmoid_focal_loss -from mmdet.registry import MODELS -from torch import Tensor, nn -from torch.cuda.amp import custom_fwd - - -def cross_sigmoid_focal_loss( - inputs: Tensor, - targets: Tensor, - weight: Tensor | None = None, - alpha: float = 0.25, - gamma: float = 2.0, - reduction: str = "mean", - avg_factor: float | None = None, - valid_label_mask: Tensor | None = None, -) -> Tensor: - """Cross Focal Loss for ignore labels. - - Args: - inputs: inputs Tensor (N * C). - targets: targets Tensor (N). - weight: weight Tensor (N), consists of (binarized label schema * weight). - alpha: focal loss alpha. - gamma: focal loss gamma. - reduction: default = mean. - avg_factor: average factors. - valid_label_mask: ignore label mask. - """ - if torch.cuda.is_available() and inputs.is_cuda: - calculate_loss_func = sigmoid_focal_loss - else: - inputs_size = inputs.size(1) - targets = F.one_hot(targets, num_classes=inputs_size + 1) - targets = targets[:, :inputs_size] - calculate_loss_func = py_sigmoid_focal_loss - - loss = calculate_loss_func( - inputs, - targets, - weight=weight, - gamma=gamma, - alpha=alpha, - reduction="none", - avg_factor=None, - ) - - loss = loss * valid_label_mask if valid_label_mask is not None else loss - - if reduction == "mean": - loss = loss.mean() if avg_factor is None else loss.sum() / avg_factor - elif reduction == "sum": - loss = loss.sum() - return loss - - -@MODELS.register_module() -class CrossSigmoidFocalLoss(nn.Module): - """CrossSigmoidFocalLoss class for ignore labels with sigmoid.""" - - def __init__( - self, - use_sigmoid: bool = True, - gamma: float = 2.0, - alpha: float = 0.25, - reduction: str = "mean", - loss_weight: float = 1.0, - ): - super().__init__() - self.reduction = reduction - self.loss_weight = loss_weight - self.gamma = gamma - self.alpha = alpha - self.use_sigmoid = use_sigmoid - - self.cls_criterion = cross_sigmoid_focal_loss - - @custom_fwd(cast_inputs=torch.float32) - def forward( - self, - pred: Tensor, - targets: Tensor, - weight: Tensor | None = None, - reduction_override: str | None = None, - avg_factor: float | None = None, - valid_label_mask: Tensor | None = None, - **kwargs, - ) -> Tensor: - """Forward funtion of CrossSigmoidFocalLoss.""" - if reduction_override not in (None, "none", "mean", "sum"): - msg = f"{reduction_override} is not in (None, none, mean, sum)" - raise ValueError(msg) - reduction = reduction_override if reduction_override else self.reduction - return self.loss_weight * self.cls_criterion( - pred, - targets, - weight=weight, - alpha=self.alpha, - gamma=self.gamma, - reduction=reduction, - avg_factor=avg_factor, - valid_label_mask=valid_label_mask, - ) - - -@MODELS.register_module() -class OrdinaryFocalLoss(nn.Module): - """Focal loss without balancing.""" - - def __init__(self, gamma: float = 1.5, **kwargs): - super().__init__() - if gamma < 0: - msg = f"{gamma} is not valid number for gamma." - raise ValueError(msg) - self.gamma = gamma - - def forward( - self, - inputs: Tensor, - targets: Tensor, - label_weights: Tensor | None = None, - avg_factor: float | None = None, - reduction: str = "mean", - **kwargs, - ) -> Tensor: - """Forward function for focal loss.""" - if targets.numel() == 0: - return 0.0 * inputs.sum() - - cross_entropy_value = F.cross_entropy(inputs, targets, reduction="none") - p = torch.exp(-cross_entropy_value) - loss = (1 - p) ** self.gamma * cross_entropy_value - if label_weights is not None: - loss = loss * label_weights - if avg_factor is None: - avg_factor = targets.shape[0] - if reduction == "sum": - return loss.sum() - if reduction == "mean": - return loss.sum() / avg_factor - return loss diff --git a/src/otx/algo/detection/mmconfigs/atss_mobilenetv2.yaml b/src/otx/algo/detection/mmconfigs/atss_mobilenetv2.yaml deleted file mode 100644 index 22fdb92bf97..00000000000 --- a/src/otx/algo/detection/mmconfigs/atss_mobilenetv2.yaml +++ /dev/null @@ -1,95 +0,0 @@ -load_from: https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/models/object_detection/v2/mobilenet_v2-atss.pth -train_cfg: - assigner: - type: ATSSAssigner - topk: 9 - allowed_border: -1 - pos_weight: -1 - debug: false -test_cfg: - nms_pre: 1000 - min_bbox_size: 0 - score_thr: 0.05 - nms: - type: nms - iou_threshold: 0.6 - max_per_img: 100 -backbone: - type: mobilenetv2_w1 - out_indices: - - 2 - - 3 - - 4 - - 5 - frozen_stages: -1 - norm_eval: false - pretrained: true -data_preprocessor: - type: DetDataPreprocessor - mean: - - 0 - - 0 - - 0 - std: - - 255 - - 255 - - 255 - bgr_to_rgb: false - pad_size_divisor: 32 - non_blocking: true -type: ATSS -neck: - type: FPN - in_channels: - - 24 - - 32 - - 96 - - 320 - out_channels: 64 - start_level: 1 - add_extra_convs: on_output - num_outs: 5 - relu_before_extra_convs: true -bbox_head: - type: CustomATSSHead - num_classes: 2 - in_channels: 64 - stacked_convs: 4 - feat_channels: 64 - anchor_generator: - type: AnchorGenerator - ratios: - - 1.0 - octave_base_scale: 8 - scales_per_octave: 1 - strides: - - 8 - - 16 - - 32 - - 64 - - 128 - bbox_coder: - type: DeltaXYWHBBoxCoder - target_means: - - 0.0 - - 0.0 - - 0.0 - - 0.0 - target_stds: - - 0.1 - - 0.1 - - 0.2 - - 0.2 - loss_cls: - type: CrossSigmoidFocalLoss - use_sigmoid: true - gamma: 2.0 - alpha: 0.25 - loss_weight: 1.0 - loss_bbox: - type: GIoULoss - loss_weight: 2.0 - loss_centerness: - type: CrossEntropyLoss - use_sigmoid: true - loss_weight: 1.0 diff --git a/src/otx/algo/detection/mmconfigs/atss_r50_fpn.yaml b/src/otx/algo/detection/mmconfigs/atss_r50_fpn.yaml deleted file mode 100644 index 0a09b000567..00000000000 --- a/src/otx/algo/detection/mmconfigs/atss_r50_fpn.yaml +++ /dev/null @@ -1,101 +0,0 @@ -backbone: - depth: 50 - frozen_stages: 1 - init_cfg: - checkpoint: torchvision://resnet50 - type: Pretrained - norm_cfg: - requires_grad: true - type: BN - norm_eval: true - num_stages: 4 - out_indices: - - 0 - - 1 - - 2 - - 3 - style: pytorch - type: ResNet -bbox_head: - anchor_generator: - octave_base_scale: 8 - ratios: - - 1.0 - scales_per_octave: 1 - strides: - - 8 - - 16 - - 32 - - 64 - - 128 - type: AnchorGenerator - bbox_coder: - target_means: - - 0.0 - - 0.0 - - 0.0 - - 0.0 - target_stds: - - 0.1 - - 0.1 - - 0.2 - - 0.2 - type: DeltaXYWHBBoxCoder - feat_channels: 256 - in_channels: 256 - loss_bbox: - loss_weight: 2.0 - type: GIoULoss - loss_centerness: - loss_weight: 1.0 - type: CrossEntropyLoss - use_sigmoid: true - loss_cls: - alpha: 0.25 - gamma: 2.0 - loss_weight: 1.0 - type: FocalLoss - use_sigmoid: true - num_classes: 80 - stacked_convs: 4 - type: ATSSHead -data_preprocessor: - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_size_divisor: 32 - std: - - 58.395 - - 57.12 - - 57.375 - type: DetDataPreprocessor - non_blocking: true -neck: - add_extra_convs: on_output - in_channels: - - 256 - - 512 - - 1024 - - 2048 - num_outs: 5 - out_channels: 256 - start_level: 1 - type: FPN -test_cfg: - max_per_img: 100 - min_bbox_size: 0 - nms: - iou_threshold: 0.6 - type: nms - nms_pre: 1000 - score_thr: 0.05 -train_cfg: - allowed_border: -1 - assigner: - topk: 9 - type: ATSSAssigner - debug: false - pos_weight: -1 -type: ATSS diff --git a/src/otx/algo/detection/mmconfigs/atss_resnext101.yaml b/src/otx/algo/detection/mmconfigs/atss_resnext101.yaml deleted file mode 100644 index eb60688cd6a..00000000000 --- a/src/otx/algo/detection/mmconfigs/atss_resnext101.yaml +++ /dev/null @@ -1,104 +0,0 @@ -load_from: https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/models/object_detection/v2/resnext101_atss_070623.pth -train_cfg: - assigner: - type: ATSSAssigner - topk: 9 - allowed_border: -1 - pos_weight: -1 - debug: false -test_cfg: - nms_pre: 1000 - min_bbox_size: 0 - score_thr: 0.05 - nms: - type: nms - iou_threshold: 0.6 - max_per_img: 100 -type: ATSS -data_preprocessor: - type: DetDataPreprocessor - non_blocking: true - mean: - - 0 - - 0 - - 0 - std: - - 255 - - 255 - - 255 - bgr_to_rgb: false - pad_size_divisor: 32 -backbone: - type: ResNeXt - depth: 101 - groups: 64 - base_width: 4 - num_stages: 4 - out_indices: - - 0 - - 1 - - 2 - - 3 - frozen_stages: 1 - norm_cfg: - type: BN - requires_grad: true - style: pytorch - init_cfg: - type: Pretrained - checkpoint: open-mmlab://resnext101_64x4d -neck: - type: FPN - in_channels: - - 256 - - 512 - - 1024 - - 2048 - out_channels: 256 - start_level: 1 - add_extra_convs: on_output - num_outs: 5 - relu_before_extra_convs: true -bbox_head: - type: CustomATSSHead - num_classes: 2 - in_channels: 256 - stacked_convs: 4 - feat_channels: 256 - anchor_generator: - type: AnchorGenerator - ratios: - - 1.0 - octave_base_scale: 8 - scales_per_octave: 1 - strides: - - 8 - - 16 - - 32 - - 64 - - 128 - bbox_coder: - type: DeltaXYWHBBoxCoder - target_means: - - 0.0 - - 0.0 - - 0.0 - - 0.0 - target_stds: - - 0.1 - - 0.1 - - 0.2 - - 0.2 - loss_cls: - type: CrossSigmoidFocalLoss - use_sigmoid: true - gamma: 2.0 - alpha: 0.25 - loss_weight: 1.0 - loss_bbox: - type: GIoULoss - loss_weight: 2.0 - loss_centerness: - type: CrossEntropyLoss - use_sigmoid: true - loss_weight: 1.0 diff --git a/src/otx/algo/detection/mmconfigs/rtmdet_tiny.yaml b/src/otx/algo/detection/mmconfigs/rtmdet_tiny.yaml deleted file mode 100644 index 297fd98d1cd..00000000000 --- a/src/otx/algo/detection/mmconfigs/rtmdet_tiny.yaml +++ /dev/null @@ -1,91 +0,0 @@ -backbone: - act_cfg: - inplace: true - type: SiLU - arch: P5 - channel_attention: true - deepen_factor: 0.167 - expand_ratio: 0.5 - init_cfg: - checkpoint: https://download.openmmlab.com/mmdetection/v3.0/rtmdet/cspnext_rsb_pretrain/cspnext-tiny_imagenet_600e.pth - prefix: backbone. - type: Pretrained - norm_cfg: - type: SyncBN - type: CSPNeXt - widen_factor: 0.375 -bbox_head: - act_cfg: - inplace: true - type: SiLU - anchor_generator: - offset: 0 - strides: - - 8 - - 16 - - 32 - type: MlvlPointGenerator - bbox_coder: - type: DistancePointBBoxCoder - exp_on_reg: false - feat_channels: 96 - in_channels: 96 - loss_bbox: - loss_weight: 2.0 - type: GIoULoss - loss_cls: - beta: 2.0 - loss_weight: 1.0 - type: QualityFocalLoss - use_sigmoid: true - norm_cfg: - type: SyncBN - num_classes: 80 - pred_kernel_size: 1 - share_conv: true - stacked_convs: 2 - type: RTMDetSepBNHead - with_objectness: false -data_preprocessor: - batch_augments: null - bgr_to_rgb: false - mean: - - 103.53 - - 116.28 - - 123.675 - std: - - 57.375 - - 57.12 - - 58.395 - type: DetDataPreprocessor - non_blocking: true -neck: - act_cfg: - inplace: true - type: SiLU - expand_ratio: 0.5 - in_channels: - - 96 - - 192 - - 384 - norm_cfg: - type: SyncBN - num_csp_blocks: 1 - out_channels: 96 - type: CSPNeXtPAFPN -test_cfg: - max_per_img: 300 - min_bbox_size: 0 - nms: - iou_threshold: 0.65 - type: nms - nms_pre: 30000 - score_thr: 0.001 -train_cfg: - allowed_border: -1 - assigner: - topk: 13 - type: DynamicSoftLabelAssigner - debug: false - pos_weight: -1 -type: RTMDet diff --git a/src/otx/algo/detection/mmconfigs/ssd_mobilenetv2.yaml b/src/otx/algo/detection/mmconfigs/ssd_mobilenetv2.yaml deleted file mode 100644 index 598970b2ee3..00000000000 --- a/src/otx/algo/detection/mmconfigs/ssd_mobilenetv2.yaml +++ /dev/null @@ -1,97 +0,0 @@ -load_from: https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/models/object_detection/v2/mobilenet_v2-2s_ssd-992x736.pth -train_cfg: - assigner: - type: MaxIoUAssigner - min_pos_iou: 0.0 - ignore_iof_thr: -1 - gt_max_assign_all: false - pos_iou_thr: 0.4 - neg_iou_thr: 0.4 - smoothl1_beta: 1.0 - allowed_border: -1 - pos_weight: -1 - neg_pos_ratio: 3 - debug: false - use_giou: false - use_focal: false -test_cfg: - nms: - type: nms - iou_threshold: 0.45 - min_bbox_size: 0 - score_thr: 0.02 - max_per_img: 200 -type: SingleStageDetector -backbone: - type: mobilenetv2_w1 - out_indices: - - 4 - - 5 - frozen_stages: -1 - norm_eval: false - pretrained: true -data_preprocessor: - type: DetDataPreprocessor - mean: - - 0 - - 0 - - 0 - std: - - 255 - - 255 - - 255 - bgr_to_rgb: false - pad_size_divisor: 32 - non_blocking: true -bbox_head: - type: CustomSSDHead - num_classes: 80 - in_channels: - - 96 - - 320 - use_depthwise: true - norm_cfg: - type: BN - act_cfg: - type: ReLU - init_cfg: - type: Xavier - layer: Conv2d - distribution: uniform - anchor_generator: - type: SSDAnchorGeneratorClustered - strides: - - 16 - - 32 - widths: - - - 38.641007923271076 - - 92.49516032784699 - - 271.4234764938237 - - 141.53469410876247 - - - 206.04136086566515 - - 386.6542727907841 - - 716.9892752215089 - - 453.75609561761405 - - 788.4629155558277 - heights: - - - 48.9243877087132 - - 147.73088476194903 - - 158.23569788707474 - - 324.14510379107367 - - - 587.6216059488938 - - 381.60024152086544 - - 323.5988913027747 - - 702.7486097568518 - - 741.4865860938451 - bbox_coder: - type: DeltaXYWHBBoxCoder - target_means: - - 0.0 - - 0.0 - - 0.0 - - 0.0 - target_stds: - - 0.1 - - 0.1 - - 0.2 - - 0.2 diff --git a/src/otx/algo/detection/mmconfigs/yolox_l.yaml b/src/otx/algo/detection/mmconfigs/yolox_l.yaml deleted file mode 100644 index cfe8e3ae30b..00000000000 --- a/src/otx/algo/detection/mmconfigs/yolox_l.yaml +++ /dev/null @@ -1,47 +0,0 @@ -load_from: https://download.openmmlab.com/mmdetection/v2.0/yolox/yolox_l_8x8_300e_coco/yolox_l_8x8_300e_coco_20211126_140236-d3bd2b23.pth -train_cfg: - assigner: - type: SimOTAAssigner - center_radius: 2.5 -test_cfg: - score_thr: 0.01 - nms: - type: nms - iou_threshold: 0.65 - max_per_img: 100 -type: YOLOX -data_preprocessor: - type: DetDataPreprocessor - non_blocking: true - mean: - - 0.0 - - 0.0 - - 0.0 - std: - - 1.0 - - 1.0 - - 1.0 - pad_value: 114 - bgr_to_rgb: false - pad_size_divisor: 32 -backbone: - type: CSPDarknet - deepen_factor: 1.0 - widen_factor: 1.0 - out_indices: - - 2 - - 3 - - 4 -neck: - type: YOLOXPAFPN - in_channels: - - 256 - - 512 - - 1024 - out_channels: 256 - num_csp_blocks: 3 -bbox_head: - type: YOLOXHead - num_classes: 80 - in_channels: 256 - feat_channels: 256 diff --git a/src/otx/algo/detection/mmconfigs/yolox_s.yaml b/src/otx/algo/detection/mmconfigs/yolox_s.yaml deleted file mode 100644 index bd3545614ca..00000000000 --- a/src/otx/algo/detection/mmconfigs/yolox_s.yaml +++ /dev/null @@ -1,47 +0,0 @@ -load_from: https://download.openmmlab.com/mmdetection/v2.0/yolox/yolox_s_8x8_300e_coco/yolox_s_8x8_300e_coco_20211121_095711-4592a793.pth -train_cfg: - assigner: - type: SimOTAAssigner - center_radius: 2.5 -test_cfg: - score_thr: 0.01 - nms: - type: nms - iou_threshold: 0.65 - max_per_img: 100 -type: YOLOX -data_preprocessor: - type: DetDataPreprocessor - non_blocking: true - mean: - - 0.0 - - 0.0 - - 0.0 - std: - - 1.0 - - 1.0 - - 1.0 - pad_value: 114 - bgr_to_rgb: false - pad_size_divisor: 32 -backbone: - type: CSPDarknet - deepen_factor: 0.33 - widen_factor: 0.5 - out_indices: - - 2 - - 3 - - 4 -neck: - type: YOLOXPAFPN - in_channels: - - 128 - - 256 - - 512 - out_channels: 128 - num_csp_blocks: 4 -bbox_head: - type: YOLOXHead - num_classes: 80 - in_channels: 128 - feat_channels: 128 diff --git a/src/otx/algo/detection/mmconfigs/yolox_tiny.yaml b/src/otx/algo/detection/mmconfigs/yolox_tiny.yaml deleted file mode 100644 index 8f714e424e3..00000000000 --- a/src/otx/algo/detection/mmconfigs/yolox_tiny.yaml +++ /dev/null @@ -1,47 +0,0 @@ -load_from: https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/models/object_detection/v2/yolox_tiny_8x8.pth -train_cfg: - assigner: - type: SimOTAAssigner - center_radius: 2.5 -test_cfg: - score_thr: 0.01 - nms: - type: nms - iou_threshold: 0.65 - max_per_img: 100 -type: YOLOX -data_preprocessor: - type: DetDataPreprocessor - non_blocking: true - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - pad_value: 114 - bgr_to_rgb: false - pad_size_divisor: 32 -backbone: - type: CSPDarknet - deepen_factor: 0.33 - widen_factor: 0.375 - out_indices: - - 2 - - 3 - - 4 -neck: - type: YOLOXPAFPN - in_channels: - - 96 - - 192 - - 384 - out_channels: 96 - num_csp_blocks: 1 -bbox_head: - type: YOLOXHead - num_classes: 80 - in_channels: 96 - feat_channels: 96 diff --git a/src/otx/algo/detection/mmconfigs/yolox_x.yaml b/src/otx/algo/detection/mmconfigs/yolox_x.yaml deleted file mode 100644 index e1efeaba3e7..00000000000 --- a/src/otx/algo/detection/mmconfigs/yolox_x.yaml +++ /dev/null @@ -1,47 +0,0 @@ -load_from: https://download.openmmlab.com/mmdetection/v2.0/yolox/yolox_x_8x8_300e_coco/yolox_x_8x8_300e_coco_20211126_140254-1ef88d67.pth -train_cfg: - assigner: - type: SimOTAAssigner - center_radius: 2.5 -test_cfg: - score_thr: 0.01 - nms: - type: nms - iou_threshold: 0.65 - max_per_img: 100 -type: YOLOX -data_preprocessor: - type: DetDataPreprocessor - non_blocking: true - mean: - - 0.0 - - 0.0 - - 0.0 - std: - - 1.0 - - 1.0 - - 1.0 - pad_value: 114 - bgr_to_rgb: false - pad_size_divisor: 32 -backbone: - type: CSPDarknet - deepen_factor: 1.33 - widen_factor: 1.25 - out_indices: - - 2 - - 3 - - 4 -neck: - type: YOLOXPAFPN - in_channels: - - 320 - - 640 - - 1280 - out_channels: 320 - num_csp_blocks: 4 -bbox_head: - type: YOLOXHead - num_classes: 80 - in_channels: 320 - feat_channels: 320 diff --git a/src/otx/algo/detection/mmdeploy/__init__.py b/src/otx/algo/detection/mmdeploy/__init__.py deleted file mode 100644 index 9d16da15b65..00000000000 --- a/src/otx/algo/detection/mmdeploy/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""MMDeploy config for detection models.""" diff --git a/src/otx/algo/detection/mmdeploy/atss.py b/src/otx/algo/detection/mmdeploy/atss.py deleted file mode 100644 index ddc7d654514..00000000000 --- a/src/otx/algo/detection/mmdeploy/atss.py +++ /dev/null @@ -1,14 +0,0 @@ -"""MMDeploy config of ATSS model for Detection Task. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -_base_ = ["./base_detection.py"] - -ir_config = dict( - output_names=["boxes", "labels"], -) - -backend_config = dict( - model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 736, 992]))], -) diff --git a/src/otx/algo/detection/mmdeploy/atss_r50_fpn.py b/src/otx/algo/detection/mmdeploy/atss_r50_fpn.py deleted file mode 100644 index 62bbd1b75fc..00000000000 --- a/src/otx/algo/detection/mmdeploy/atss_r50_fpn.py +++ /dev/null @@ -1,14 +0,0 @@ -"""MMDeploy config of ATSS model for Detection Task. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -_base_ = ["./base_detection.py"] - -ir_config = dict( - output_names=["boxes", "labels"], -) - -backend_config = dict( - model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 800, 1333]))], -) diff --git a/src/otx/algo/detection/mmdeploy/base_detection.py b/src/otx/algo/detection/mmdeploy/base_detection.py deleted file mode 100644 index 1d4c1993d80..00000000000 --- a/src/otx/algo/detection/mmdeploy/base_detection.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Detection models base deploy config. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -ir_config = dict( - type="onnx", - export_params=True, - keep_initializers_as_inputs=False, - opset_version=11, - save_file="end2end.onnx", - input_names=["image"], - output_names=["boxes", "labels"], - input_shape=None, - # TODO - # optimizing onnx graph mess up NNCF graph at some point - # where we need to look into - optimize=False, - dynamic_axes={ - "image": { - 0: "batch", - 2: "height", - 3: "width", - }, - "boxes": { - 0: "batch", - 1: "num_dets", - }, - "labels": { - 0: "batch", - 1: "num_dets", - }, - }, -) - -codebase_config = dict( - type="mmdet", - task="ObjectDetection", - model_type="end2end", - post_processing=dict( - score_threshold=0.05, - confidence_threshold=0.005, # for YOLOv3 - iou_threshold=0.5, - max_output_boxes_per_class=200, - pre_top_k=5000, - keep_top_k=100, - background_label_id=-1, - ), -) - -backend_config = dict( - type="openvino", - mo_options=None, -) diff --git a/src/otx/algo/detection/mmdeploy/rtmdet.py b/src/otx/algo/detection/mmdeploy/rtmdet.py deleted file mode 100644 index 194aafa5e6d..00000000000 --- a/src/otx/algo/detection/mmdeploy/rtmdet.py +++ /dev/null @@ -1,14 +0,0 @@ -"""MMDeploy config of RTMdet model for Detection Task. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -_base_ = ["./base_detection.py"] - -ir_config = dict( - output_names=["boxes", "labels"], -) - -backend_config = dict( - model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 640, 640]))], -) diff --git a/src/otx/algo/detection/mmdeploy/ssd_mobilenetv2.py b/src/otx/algo/detection/mmdeploy/ssd_mobilenetv2.py deleted file mode 100644 index d0e152a5eb6..00000000000 --- a/src/otx/algo/detection/mmdeploy/ssd_mobilenetv2.py +++ /dev/null @@ -1,14 +0,0 @@ -"""MMDeploy config of SSD model for Detection Task. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -_base_ = ["./base_detection.py"] - -ir_config = dict( - output_names=["boxes", "labels"], -) - -backend_config = dict( - model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 864, 864]))], -) diff --git a/src/otx/algo/detection/mmdeploy/yolox.py b/src/otx/algo/detection/mmdeploy/yolox.py deleted file mode 100644 index a8877781ce4..00000000000 --- a/src/otx/algo/detection/mmdeploy/yolox.py +++ /dev/null @@ -1,14 +0,0 @@ -"""MMDeploy config of YOLOX models except YOLOX_tiny for Detection Task. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -_base_ = ["./base_detection.py"] - -ir_config = dict( - output_names=["boxes", "labels"], -) - -backend_config = dict( - model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 640, 640]))], -) diff --git a/src/otx/algo/detection/mmdeploy/yolox_tiny.py b/src/otx/algo/detection/mmdeploy/yolox_tiny.py deleted file mode 100644 index 2cad9bf91e1..00000000000 --- a/src/otx/algo/detection/mmdeploy/yolox_tiny.py +++ /dev/null @@ -1,14 +0,0 @@ -"""MMDeploy config of YOLOX Tiny model for Detection Task. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -_base_ = ["./base_detection.py"] - -ir_config = dict( - output_names=["boxes", "labels"], -) - -backend_config = dict( - model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 416, 416]))], -) diff --git a/src/otx/algo/detection/rtmdet.py b/src/otx/algo/detection/rtmdet.py deleted file mode 100644 index f47be33127c..00000000000 --- a/src/otx/algo/detection/rtmdet.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""RTMDetTiny model implementations.""" - -from __future__ import annotations - -from typing import Any, Literal - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.detection import MMDetCompatibleModel - - -class RTMDet(MMDetCompatibleModel): - """RTMDet Model.""" - - def __init__(self, num_classes: int, variant: Literal["tiny"]) -> None: - model_name = f"rtmdet_{variant}" - config = read_mmconfig(model_name=model_name) - super().__init__(num_classes=num_classes, config=config) - self.image_size = (1, 3, 640, 640) - self.tile_image_size = self.image_size - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - export_params = super()._export_parameters - export_params["deploy_cfg"] = "otx.algo.detection.mmdeploy.rtmdet" - export_params["input_size"] = self.image_size - export_params["resize_mode"] = "fit_to_window_letterbox" - export_params["pad_value"] = 114 - export_params["swap_rgb"] = False - - return export_params - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_det_ckpt(state_dict, add_prefix) diff --git a/src/otx/algo/detection/ssd.py b/src/otx/algo/detection/ssd.py deleted file mode 100644 index ba53ff4c9de..00000000000 --- a/src/otx/algo/detection/ssd.py +++ /dev/null @@ -1,266 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""SSD object detector for the OTX detection.""" - -from __future__ import annotations - -import logging -from copy import deepcopy -from typing import TYPE_CHECKING, Any, Literal - -import numpy as np -from datumaro.components.annotation import Bbox - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.detection import MMDetCompatibleModel -from otx.core.utils.build import build_mm_model, modify_num_classes - -if TYPE_CHECKING: - import torch - from lightning import Trainer - from mmdet.models.task_modules.prior_generators.anchor_generator import AnchorGenerator - from mmengine.registry import Registry - from omegaconf import DictConfig - from torch import device, nn - - from otx.core.data.dataset.base import OTXDataset - - -logger = logging.getLogger() - - -class SSD(MMDetCompatibleModel): - """Detecion model class for SSD.""" - - def __init__(self, num_classes: int, variant: Literal["mobilenetv2"]) -> None: - model_name = f"ssd_{variant}" - config = read_mmconfig(model_name=model_name) - super().__init__(num_classes=num_classes, config=config) - self.image_size = (1, 3, 864, 864) - self.tile_image_size = self.image_size - self._register_load_state_dict_pre_hook(self._set_anchors_hook) - - def _create_model(self) -> nn.Module: - from mmdet.models.data_preprocessors import ( - DetDataPreprocessor as _DetDataPreprocessor, - ) - from mmdet.registry import MODELS - from mmengine.registry import MODELS as MMENGINE_MODELS - - # NOTE: For the history of this monkey patching, please see - # https://github.com/openvinotoolkit/training_extensions/issues/2743 - @MMENGINE_MODELS.register_module(force=True) - class DetDataPreprocessor(_DetDataPreprocessor): - @property - def device(self) -> device: - try: - buf = next(self.buffers()) - except StopIteration: - return super().device - else: - return buf.device - - self.classification_layers = self.get_classification_layers(self.config, MODELS, "model.") - return build_mm_model(self.config, MODELS, self.load_from) - - def setup_callback(self, trainer: Trainer) -> None: - """Callback for setup OTX Model. - - OTXSSD requires auto anchor generating w.r.t. training dataset for better accuracy. - This callback will provide training dataset to model's anchor generator. - - Args: - trainer(Trainer): Lightning trainer contains OTXLitModule and OTXDatamodule. - """ - if trainer.training: - anchor_generator = self.model.bbox_head.anchor_generator - dataset = trainer.datamodule.train_dataloader().dataset - new_anchors = self._get_new_anchors(dataset, anchor_generator) - if new_anchors is not None: - logger.warning("Anchor will be updated by Dataset's statistics") - logger.warning(f"{anchor_generator.widths} -> {new_anchors[0]}") - logger.warning(f"{anchor_generator.heights} -> {new_anchors[1]}") - anchor_generator.widths = new_anchors[0] - anchor_generator.heights = new_anchors[1] - anchor_generator.gen_base_anchors() - - def _get_new_anchors(self, dataset: OTXDataset, anchor_generator: AnchorGenerator) -> tuple | None: - """Get new anchors for SSD from OTXDataset.""" - from mmdet.datasets.transforms import Resize - - target_wh = None - if isinstance(dataset.transforms, list): - for transform in dataset.transforms: - if isinstance(transform, Resize): - target_wh = transform.scale - if target_wh is None: - target_wh = (864, 864) - msg = f"Cannot get target_wh from the dataset. Assign it with the default value: {target_wh}" - logger.warning(msg) - group_as = [len(width) for width in anchor_generator.widths] - wh_stats = self._get_sizes_from_dataset_entity(dataset, list(target_wh)) - - if len(wh_stats) < sum(group_as): - logger.warning( - f"There are not enough objects to cluster: {len(wh_stats)} were detected, while it should be " - f"at least {sum(group_as)}. Anchor box clustering was skipped.", - ) - return None - - return self._get_anchor_boxes(wh_stats, group_as) - - @staticmethod - def _get_sizes_from_dataset_entity(dataset: OTXDataset, target_wh: list[int]) -> list[tuple[int, int]]: - """Function to get width and height size of items in OTXDataset. - - Args: - dataset(OTXDataset): OTXDataset in which to get statistics - target_wh(list[int]): target width and height of the dataset - Return - list[tuple[int, int]]: tuples with width and height of each instance - """ - wh_stats: list[tuple[int, int]] = [] - for item in dataset.dm_subset: - for ann in item.annotations: - if isinstance(ann, Bbox): - x1, y1, x2, y2 = ann.points - x1 = x1 / item.media.size[1] * target_wh[0] - y1 = y1 / item.media.size[0] * target_wh[1] - x2 = x2 / item.media.size[1] * target_wh[0] - y2 = y2 / item.media.size[0] * target_wh[1] - wh_stats.append((x2 - x1, y2 - y1)) - return wh_stats - - @staticmethod - def _get_anchor_boxes(wh_stats: list[tuple[int, int]], group_as: list[int]) -> tuple: - """Get new anchor box widths & heights using KMeans.""" - from sklearn.cluster import KMeans - - kmeans = KMeans(init="k-means++", n_clusters=sum(group_as), random_state=0).fit(wh_stats) - centers = kmeans.cluster_centers_ - - areas = np.sqrt(np.prod(centers, axis=1)) - idx = np.argsort(areas) - - widths = centers[idx, 0] - heights = centers[idx, 1] - - group_as = np.cumsum(group_as[:-1]) - widths, heights = np.split(widths, group_as), np.split(heights, group_as) - widths = [width.tolist() for width in widths] - heights = [height.tolist() for height in heights] - return widths, heights - - @staticmethod - def get_classification_layers( - config: DictConfig, - model_registry: Registry, - prefix: str, - ) -> dict[str, dict[str, bool | int]]: - """Return classification layer names by comparing two different number of classes models. - - Args: - config (DictConfig): Config for building model. - model_registry (Registry): Registry for building model. - prefix (str): Prefix of model param name. - Normally it is "model." since OTXModel set it's nn.Module model as self.model - - Return: - dict[str, dict[str, int]] - A dictionary contain classification layer's name and information. - `use_bg` means whether SSD use background class. It if True if SSD use softmax loss, and - it is False if SSD use cross entropy loss. - `num_anchors` means number of anchors of layer. SSD have classification per each anchor, - so we have to update every anchors. - """ - sample_config = deepcopy(config) - modify_num_classes(sample_config, 3) - sample_model_dict = build_mm_model(sample_config, model_registry, None).state_dict() - - modify_num_classes(sample_config, 4) - incremental_model_dict = build_mm_model(sample_config, model_registry, None).state_dict() - - classification_layers = {} - for key in sample_model_dict: - if sample_model_dict[key].shape != incremental_model_dict[key].shape: - sample_model_dim = sample_model_dict[key].shape[0] - if sample_model_dim % 3 != 0: - use_bg = True - num_anchors = int(sample_model_dim / 4) - classification_layers[prefix + key] = {"use_bg": use_bg, "num_anchors": num_anchors} - else: - use_bg = False - num_anchors = int(sample_model_dim / 3) - classification_layers[prefix + key] = {"use_bg": use_bg, "num_anchors": num_anchors} - return classification_layers - - def state_dict(self, *args, **kwargs) -> dict[str, Any]: - """Return state dictionary of model entity with anchor information. - - Returns: - A dictionary containing SSD state. - - """ - state_dict = super().state_dict(*args, **kwargs) - anchor_generator = self.model.bbox_head.anchor_generator - anchors = {"heights": anchor_generator.heights, "widths": anchor_generator.widths} - state_dict["model.model.anchors"] = anchors - return state_dict - - def load_state_dict_pre_hook(self, state_dict: dict[str, torch.Tensor], prefix: str, *args, **kwargs) -> None: - """Modify input state_dict according to class name matching before weight loading.""" - model2ckpt = self.map_class_names(self.model_classes, self.ckpt_classes) - - for param_name, info in self.classification_layers.items(): - model_param = self.state_dict()[param_name].clone() - ckpt_param = state_dict[prefix + param_name] - use_bg = info["use_bg"] - num_anchors = info["num_anchors"] - if use_bg: - num_ckpt_classes = len(self.ckpt_classes) + 1 - num_model_classes = len(self.model_classes) + 1 - else: - num_ckpt_classes = len(self.ckpt_classes) - num_model_classes = len(self.model_classes) - - for anchor_idx in range(num_anchors): - for model_dst, ckpt_dst in enumerate(model2ckpt): - if ckpt_dst >= 0: - # Copying only matched weight rows - model_param[anchor_idx * num_model_classes + model_dst].copy_( - ckpt_param[anchor_idx * num_ckpt_classes + ckpt_dst], - ) - if use_bg: - model_param[anchor_idx * num_model_classes + num_model_classes - 1].copy_( - ckpt_param[anchor_idx * num_ckpt_classes + num_ckpt_classes - 1], - ) - - # Replace checkpoint weight by mixed weights - state_dict[prefix + param_name] = model_param - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - export_params = super()._export_parameters - export_params["deploy_cfg"] = "otx.algo.detection.mmdeploy.ssd_mobilenetv2" - export_params["input_size"] = self.image_size - export_params["resize_mode"] = "standard" - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - - return export_params - - def _set_anchors_hook(self, state_dict: dict[str, Any], *args, **kwargs) -> None: - """Pre hook for pop anchor statistics from checkpoint state_dict.""" - anchors = state_dict.pop("model.model.anchors", None) - if anchors is not None: - anchor_generator = self.model.bbox_head.anchor_generator - anchor_generator.widths = anchors["widths"] - anchor_generator.heights = anchors["heights"] - anchor_generator.gen_base_anchors() - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_ssd_ckpt(state_dict, add_prefix) diff --git a/src/otx/algo/detection/yolox.py b/src/otx/algo/detection/yolox.py deleted file mode 100644 index 54be7be5b1f..00000000000 --- a/src/otx/algo/detection/yolox.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""YOLOX model implementations.""" - -from __future__ import annotations - -from typing import Any, Literal - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.detection import MMDetCompatibleModel - - -class YoloX(MMDetCompatibleModel): - """YoloX Model.""" - - def __init__(self, num_classes: int, variant: Literal["l", "s", "tiny", "x"]) -> None: - model_name = f"yolox_{variant}" - config = read_mmconfig(model_name=model_name) - super().__init__(num_classes=num_classes, config=config) - self.image_size = (1, 3, 640, 640) - self.tile_image_size = self.image_size - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - export_params = super()._export_parameters - export_params["resize_mode"] = "fit_to_window_letterbox" - export_params["pad_value"] = 114 - export_params["swap_rgb"] = True - export_params["input_size"] = self.image_size - export_params["deploy_cfg"] = "otx.algo.detection.mmdeploy.yolox" - - return export_params - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_det_ckpt(state_dict, add_prefix) - - -class YoloXTiny(YoloX): - """YoloX tiny Model.""" - - def __init__(self, num_classes: int) -> None: - super().__init__(num_classes=num_classes, variant="tiny") - self.image_size = (1, 3, 416, 416) - self.tile_image_size = self.image_size - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - export_params = super()._export_parameters - export_params["resize_mode"] = "fit_to_window_letterbox" - export_params["pad_value"] = 114 - export_params["swap_rgb"] = False - export_params["input_size"] = self.image_size - export_params["deploy_cfg"] = "otx.algo.detection.mmdeploy.yolox_tiny" - - return export_params diff --git a/src/otx/algo/hooks/__init__.py b/src/otx/algo/hooks/__init__.py deleted file mode 100644 index de1331bdce2..00000000000 --- a/src/otx/algo/hooks/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX custom hooks.""" diff --git a/src/otx/algo/hooks/recording_forward_hook.py b/src/otx/algo/hooks/recording_forward_hook.py deleted file mode 100644 index e8a825bddd1..00000000000 --- a/src/otx/algo/hooks/recording_forward_hook.py +++ /dev/null @@ -1,454 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Hooks for recording/updating model internal activations.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Callable, Sequence - -import numpy as np -import torch - -if TYPE_CHECKING: - from mmengine.structures.instance_data import InstanceData - from torch.utils.hooks import RemovableHandle - - -def feature_vector_fn(feature_map: torch.Tensor | Sequence[torch.Tensor]) -> torch.Tensor: - """Generate the feature vector by average pooling feature maps.""" - if isinstance(feature_map, (list, tuple)): - # aggregate feature maps from Feature Pyramid Network - feature_vectors = [ - # Spatially pooling and flatten, B x C x H x W => B x C' - torch.nn.functional.adaptive_avg_pool2d(f, (1, 1)).flatten(start_dim=1) - for f in feature_map - ] - if len(feature_vectors) > 1: - return torch.cat(feature_vectors, 1) - return feature_vectors[0] - - return torch.nn.functional.adaptive_avg_pool2d(feature_map, (1, 1)).flatten(start_dim=1) - - -class BaseRecordingForwardHook: - """While registered with the designated PyTorch module, this class caches feature vector during forward pass. - - Args: - normalize (bool): Whether to normalize the resulting saliency maps. - """ - - def __init__(self, head_forward_fn: Callable | None = None, normalize: bool = True) -> None: - self._head_forward_fn = head_forward_fn - self.handle: RemovableHandle | None = None - self._records: list[torch.Tensor] = [] - self._norm_saliency_maps = normalize - - @property - def records(self) -> list[torch.Tensor]: - """Return records.""" - return self._records - - def reset(self) -> None: - """Clear all history of records.""" - self._records.clear() - - def func(self, feature_map: torch.Tensor, fpn_idx: int = -1) -> torch.Tensor: - """This method get the feature vector or saliency map from the output of the module. - - Args: - feature_map (torch.Tensor): Feature map from the backbone module - fpn_idx (int, optional): The layer index to be processed if the model is a FPN. - Defaults to 0 which uses the largest feature map from FPN. - - Returns: - torch.Tensor (torch.Tensor): Saliency map for feature vector - """ - raise NotImplementedError - - def recording_forward( - self, - _: torch.nn.Module, - x: torch.Tensor, - output: torch.Tensor, - ) -> None: # pylint: disable=unused-argument - """Record the XAI result during executing model forward function.""" - tensors = self.func(output) - if isinstance(tensors, torch.Tensor): - tensors_np = tensors.detach().cpu().numpy() - elif isinstance(tensors, np.ndarray): - tensors_np = tensors - else: - self._torch_to_numpy_from_list(tensors) - tensors_np = tensors - - for tensor in tensors_np: - self._records.append(tensor) - - def _predict_from_feature_map(self, x: torch.Tensor) -> torch.Tensor: - with torch.no_grad(): - if self._head_forward_fn: - x = self._head_forward_fn(x) - if not isinstance(x, torch.Tensor): - x = torch.tensor(x) - return x - - def _torch_to_numpy_from_list(self, tensor_list: list[torch.Tensor | None]) -> None: - for i in range(len(tensor_list)): - tensor = tensor_list[i] - if isinstance(tensor, list): - self._torch_to_numpy_from_list(tensor) - elif isinstance(tensor, torch.Tensor): - tensor_list[i] = tensor.detach().cpu().numpy() - - @staticmethod - def _normalize_map(saliency_maps: torch.Tensor) -> torch.Tensor: - """Normalize saliency maps.""" - max_values, _ = torch.max(saliency_maps, -1) - min_values, _ = torch.min(saliency_maps, -1) - if len(saliency_maps.shape) == 2: - saliency_maps = 255 * (saliency_maps - min_values[:, None]) / (max_values - min_values + 1e-12)[:, None] - else: - saliency_maps = ( - 255 * (saliency_maps - min_values[:, :, None]) / (max_values - min_values + 1e-12)[:, :, None] - ) - return saliency_maps.to(torch.uint8) - - -class ActivationMapHook(BaseRecordingForwardHook): - """ActivationMapHook. Mean of the feature map along the channel dimension.""" - - @classmethod - def create_and_register_hook( - cls, - backbone: torch.nn.Module, - ) -> BaseRecordingForwardHook: - """Create this object and register it to the module forward hook.""" - hook = cls() - hook.handle = backbone.register_forward_hook(hook.recording_forward) - return hook - - def func(self, feature_map: torch.Tensor | Sequence[torch.Tensor], fpn_idx: int = -1) -> torch.Tensor: - """Generate the saliency map by average feature maps then normalizing to (0, 255).""" - if isinstance(feature_map, (list, tuple)): - feature_map = feature_map[fpn_idx] - - batch_size, _, h, w = feature_map.size() - activation_map = torch.mean(feature_map, dim=1) - - if self._norm_saliency_maps: - activation_map = activation_map.reshape((batch_size, h * w)) - activation_map = self._normalize_map(activation_map) - - return activation_map.reshape((batch_size, h, w)) - - -class ReciproCAMHook(BaseRecordingForwardHook): - """Implementation of Recipro-CAM for class-wise saliency map. - - Recipro-CAM: gradient-free reciprocal class activation map (https://arxiv.org/pdf/2209.14074.pdf) - """ - - def __init__( - self, - head_forward_fn: Callable, - num_classes: int, - normalize: bool = True, - optimize_gap: bool = False, - ) -> None: - super().__init__(head_forward_fn, normalize) - self._num_classes = num_classes - self._optimize_gap = optimize_gap - - @classmethod - def create_and_register_hook( - cls, - backbone: torch.nn.Module, - head_forward_fn: Callable, - num_classes: int, - optimize_gap: bool, - ) -> BaseRecordingForwardHook: - """Create this object and register it to the module forward hook.""" - hook = cls( - head_forward_fn, - num_classes=num_classes, - optimize_gap=optimize_gap, - ) - hook.handle = backbone.register_forward_hook(hook.recording_forward) - return hook - - def func(self, feature_map: torch.Tensor | Sequence[torch.Tensor], fpn_idx: int = -1) -> torch.Tensor: - """Generate the class-wise saliency maps using Recipro-CAM and then normalizing to (0, 255). - - Args: - feature_map (Union[torch.Tensor, List[torch.Tensor]]): feature maps from backbone or list of feature maps - from FPN. - fpn_idx (int, optional): The layer index to be processed if the model is a FPN. - Defaults to 0 which uses the largest feature map from FPN. - - Returns: - torch.Tensor: Class-wise Saliency Maps. One saliency map per each class - [batch, class_id, H, W] - """ - if isinstance(feature_map, (list, tuple)): - feature_map = feature_map[fpn_idx] - - batch_size, channel, h, w = feature_map.size() - saliency_maps = torch.empty(batch_size, self._num_classes, h, w) - for f in range(batch_size): - mosaic_feature_map = self._get_mosaic_feature_map(feature_map[f], channel, h, w) - mosaic_prediction = self._predict_from_feature_map(mosaic_feature_map) - saliency_maps[f] = mosaic_prediction.transpose(0, 1).reshape((self._num_classes, h, w)) - - if self._norm_saliency_maps: - saliency_maps = saliency_maps.reshape((batch_size, self._num_classes, h * w)) - saliency_maps = self._normalize_map(saliency_maps) - - return saliency_maps.reshape((batch_size, self._num_classes, h, w)) - - def _get_mosaic_feature_map(self, feature_map: torch.Tensor, c: int, h: int, w: int) -> torch.Tensor: - if self._optimize_gap: - # if isinstance(model_neck, GlobalAveragePooling): - # Optimization workaround for the GAP case (simulate GAP with more simple compute graph) - # Possible due to static sparsity of mosaic_feature_map - # Makes the downstream GAP operation to be dummy - feature_map_transposed = torch.flatten(feature_map, start_dim=1).transpose(0, 1)[:, :, None, None] - mosaic_feature_map = feature_map_transposed / (h * w) - else: - feature_map_repeated = feature_map.repeat(h * w, 1, 1, 1) - mosaic_feature_map_mask = torch.zeros(h * w, c, h, w).to(feature_map.device) - spacial_order = torch.arange(h * w).reshape(h, w) - for i in range(h): - for j in range(w): - k = spacial_order[i, j] - mosaic_feature_map_mask[k, :, i, j] = torch.ones(c).to(feature_map.device) - mosaic_feature_map = feature_map_repeated * mosaic_feature_map_mask - return mosaic_feature_map - - -class ViTReciproCAMHook(BaseRecordingForwardHook): - """Implementation of ViTRecipro-CAM for class-wise saliency map for transformer-based classifiers. - - Args: - head_forward_fn (callable): Forward pass function for the top of the model. - num_classes (int): Number of classes. - use_gaussian (bool): Defines kernel type for mosaic feature map generation. - If True, use gaussian 3x3 kernel. If False, use 1x1 kernel. - cls_token (bool): If True, includes classification token into the mosaic feature map. - normalize (bool): If True, Normalizes saliency maps. - """ - - def __init__( - self, - head_forward_fn: Callable, - num_classes: int, - use_gaussian: bool = True, - cls_token: bool = True, - normalize: bool = True, - ) -> None: - super().__init__(head_forward_fn, normalize) - self._num_classes = num_classes - self._use_gaussian = use_gaussian - self._cls_token = cls_token - - @classmethod - def create_and_register_hook( - cls, - target_layernorm: torch.nn.Module, - head_forward_fn: Callable, - num_classes: int, - ) -> BaseRecordingForwardHook: - """Create this object and register it to the module forward hook.""" - hook = cls( - head_forward_fn, - num_classes=num_classes, - ) - hook.handle = target_layernorm.register_forward_hook(hook.recording_forward) - return hook - - def func(self, feature_map: torch.Tensor, _: int = -1) -> torch.Tensor: - """Generate the class-wise saliency maps using ViTRecipro-CAM and then normalizing to (0, 255). - - Args: - feature_map (torch.Tensor): feature maps from target layernorm layer. - - Returns: - torch.Tensor: Class-wise Saliency Maps. One saliency map per each class - [batch, class_id, H, W] - """ - batch_size, token_number, _ = feature_map.size() - h = w = int((token_number - 1) ** 0.5) - saliency_maps = torch.empty(batch_size, self._num_classes, h, w) - for i in range(batch_size): - mosaic_feature_map = self._get_mosaic_feature_map(feature_map[i]) - mosaic_prediction = self._predict_from_feature_map(mosaic_feature_map) - saliency_maps[i] = mosaic_prediction.transpose(1, 0).reshape((self._num_classes, h, w)) - - if self._norm_saliency_maps: - saliency_maps = saliency_maps.reshape((batch_size, self._num_classes, h * w)) - saliency_maps = self._normalize_map(saliency_maps) - return saliency_maps.reshape((batch_size, self._num_classes, h, w)) - - def _get_mosaic_feature_map(self, feature_map: torch.Tensor) -> torch.Tensor: - token_number, dim = feature_map.size() - mosaic_feature_map = torch.zeros(token_number - 1, token_number, dim).to(feature_map.device) - h = w = int((token_number - 1) ** 0.5) - - if self._use_gaussian: - if self._cls_token: - mosaic_feature_map[:, 0, :] = feature_map[0, :] - feature_map_spacial = feature_map[1:, :].reshape(1, h, w, dim) - feature_map_spacial_repeated = feature_map_spacial.repeat(h * w, 1, 1, 1) # 196, 14, 14, 192 - - spacial_order = torch.arange(h * w).reshape(h, w) - gaussian = torch.tensor( - [[1 / 16.0, 1 / 8.0, 1 / 16.0], [1 / 8.0, 1 / 4.0, 1 / 8.0], [1 / 16.0, 1 / 8.0, 1 / 16.0]], - ).to(feature_map.device) - mosaic_feature_map_mask_padded = torch.zeros(h * w, h + 2, w + 2).to(feature_map.device) - for i in range(h): - for j in range(w): - k = spacial_order[i, j] - i_pad = i + 1 - j_pad = j + 1 - mosaic_feature_map_mask_padded[k, i_pad - 1 : i_pad + 2, j_pad - 1 : j_pad + 2] = gaussian - mosaic_feature_map_mask = mosaic_feature_map_mask_padded[:, 1:-1, 1:-1] - mosaic_feature_map_mask = torch.tensor(mosaic_feature_map_mask.unsqueeze(3).repeat(1, 1, 1, dim)) - - mosaic_fm_wo_cls_token = feature_map_spacial_repeated * mosaic_feature_map_mask - mosaic_feature_map[:, 1:, :] = mosaic_fm_wo_cls_token.reshape(h * w, h * w, dim) - else: - feature_map_repeated = feature_map.unsqueeze(0).repeat(h * w, 1, 1) - mosaic_feature_map_mask = torch.zeros(h * w, token_number).to(feature_map.device) - for i in range(h * w): - mosaic_feature_map_mask[i, i + 1] = torch.ones(1).to(feature_map.device) - if self._cls_token: - mosaic_feature_map_mask[:, 0] = torch.ones(1).to(feature_map.device) - mosaic_feature_map_mask = torch.tensor(mosaic_feature_map_mask.unsqueeze(2).repeat(1, 1, dim)) - mosaic_feature_map = feature_map_repeated * mosaic_feature_map_mask - - return mosaic_feature_map - - -class DetClassProbabilityMapHook(BaseRecordingForwardHook): - """Saliency map hook for object detection models.""" - - def __init__( - self, - num_classes: int, - num_anchors: list[int], - normalize: bool = True, - use_cls_softmax: bool = True, - ) -> None: - super().__init__(head_forward_fn=None, normalize=normalize) - # SSD-like heads also have background class - self._num_classes = num_classes - self._num_anchors = num_anchors - # Should be switched off for tiling - self.use_cls_softmax = use_cls_softmax - - def func( - self, - cls_scores: torch.Tensor | Sequence[torch.Tensor], - _: int = -1, - ) -> torch.Tensor: - """Generate the saliency map from raw classification head output, then normalizing to (0, 255). - - Args: - cls_scores (torch.Tensor | Sequence[torch.Tensor]): Classification scores from cls_head. - - Returns: - torch.Tensor: Class-wise Saliency Maps. One saliency map per each class - [batch, class_id, H, W] - """ - middle_idx = len(cls_scores) // 2 - # Resize to the middle feature map - batch_size, _, height, width = cls_scores[middle_idx].size() - saliency_maps = torch.empty(batch_size, self._num_classes, height, width) - for batch_idx in range(batch_size): - cls_scores_anchorless = [] - for scale_idx, cls_scores_per_scale in enumerate(cls_scores): - cls_scores_anchor_grouped = cls_scores_per_scale[batch_idx].reshape( - self._num_anchors[scale_idx], - (self._num_classes), - *cls_scores_per_scale.shape[-2:], - ) - cls_scores_out, _ = cls_scores_anchor_grouped.max(dim=0) - cls_scores_anchorless.append(cls_scores_out.unsqueeze(0)) - - cls_scores_anchorless_resized = [ - torch.nn.functional.interpolate(cls_scores_anchorless_per_level, (height, width), mode="bilinear") - for cls_scores_anchorless_per_level in cls_scores_anchorless - ] - - saliency_maps[batch_idx] = torch.cat(cls_scores_anchorless_resized, dim=0).mean(dim=0) - - # Don't use softmax for tiles in tiling detection, if the tile doesn't contain objects, - # it would highlight one of the class maps as a background class - if self.use_cls_softmax: - saliency_maps[0] = torch.stack([torch.softmax(t, dim=1) for t in saliency_maps[0]]) - - if self._norm_saliency_maps: - saliency_maps = saliency_maps.reshape((batch_size, self._num_classes, -1)) - saliency_maps = self._normalize_map(saliency_maps) - - return saliency_maps.reshape((batch_size, self._num_classes, height, width)) - - -class MaskRCNNRecordingForwardHook(BaseRecordingForwardHook): - """Dummy saliency map hook for Mask R-CNN model.""" - - def __init__(self, num_classes: int) -> None: - super().__init__() - self.num_classes = num_classes - - def func( - self, - predictions: list[InstanceData], - _: int = -1, - ) -> list[np.array]: - """Generate saliency maps from predicted masks by averaging and normalizing them per-class. - - Args: - predictions (list[InstanceData]): Predictions of Instance Segmentation model. - - Returns: - torch.Tensor: Class-wise Saliency Maps. One saliency map per each class - [batch, class_id, H, W] - """ - # TODO(gzalessk): Add unit tests # noqa: TD003 - batch_saliency_maps = [] - for prediction in predictions: - class_averaged_masks = self.average_and_normalize(prediction, self.num_classes) - batch_saliency_maps.append(class_averaged_masks) - return torch.stack(batch_saliency_maps) - - @classmethod - def average_and_normalize( - cls, - pred: InstanceData, - num_classes: int, - ) -> np.array: - """Average and normalize masks in prediction per-class. - - Args: - preds (InstanceData): Predictions of Instance Segmentation model. - num_classes (int): Num classes that model can predict. - - Returns: - np.array: Class-wise Saliency Maps. One saliency map per each class - [class_id, H, W] - """ - masks, scores, labels = (pred.masks, pred.scores, pred.labels) - _, height, width = masks.shape - - saliency_maps = torch.zeros((num_classes, height, width), dtype=torch.float32, device=labels.device) - class_objects = [0 for _ in range(num_classes)] - - for confidence, class_ind, raw_mask in zip(scores, labels, masks): - weighted_mask = raw_mask * confidence - saliency_maps[class_ind] += weighted_mask - class_objects[class_ind] += 1 - - for class_ind in range(num_classes): - # Normalize by number of objects of the certain class - saliency_maps[class_ind] /= max(class_objects[class_ind], 1) - - saliency_maps = saliency_maps.reshape((num_classes, -1)) - saliency_maps = cls._normalize_map(saliency_maps) - - return saliency_maps.reshape(num_classes, height, width) diff --git a/src/otx/algo/instance_segmentation/__init__.py b/src/otx/algo/instance_segmentation/__init__.py deleted file mode 100644 index 5fd14c6c6f6..00000000000 --- a/src/otx/algo/instance_segmentation/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX instance segmentation models.""" - -from . import heads - -__all__ = ["heads"] diff --git a/src/otx/algo/instance_segmentation/heads/__init__.py b/src/otx/algo/instance_segmentation/heads/__init__.py deleted file mode 100644 index 91cafe6b4aa..00000000000 --- a/src/otx/algo/instance_segmentation/heads/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom head architecture for OTX instance segmentation models.""" - -from .custom_roi_head import CustomConvFCBBoxHead, CustomRoIHead - -__all__ = ["CustomRoIHead", "CustomConvFCBBoxHead"] diff --git a/src/otx/algo/instance_segmentation/heads/custom_roi_head.py b/src/otx/algo/instance_segmentation/heads/custom_roi_head.py deleted file mode 100644 index a5699c6737f..00000000000 --- a/src/otx/algo/instance_segmentation/heads/custom_roi_head.py +++ /dev/null @@ -1,307 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom ROI head for OTX template.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import torch -from mmdet.models.losses import accuracy -from mmdet.models.roi_heads.bbox_heads.convfc_bbox_head import Shared2FCBBoxHead -from mmdet.models.roi_heads.standard_roi_head import StandardRoIHead -from mmdet.models.utils import multi_apply, unpack_gt_instances -from mmdet.registry import MODELS -from mmdet.structures.bbox import bbox2roi -from torch import Tensor - -from otx.algo.detection.heads.class_incremental_mixin import ( - ClassIncrementalMixin, -) -from otx.algo.detection.losses.cross_focal_loss import ( - CrossSigmoidFocalLoss, -) - -if TYPE_CHECKING: - from mmdet.models.task_modules.samplers import SamplingResult - from mmdet.structures import DetDataSample - from mmdet.utils import InstanceList - from mmengine.config import ConfigDict - - -@MODELS.register_module() -class CustomRoIHead(StandardRoIHead): - """CustomRoIHead class for OTX.""" - - def loss(self, x: tuple[Tensor], rpn_results_list: InstanceList, batch_data_samples: list[DetDataSample]) -> dict: - """Perform forward propagation and loss calculation of the detection roi on the features. - - Args: - x (tuple[Tensor]): list of multi-level img features. - rpn_results_list (list[:obj:`InstanceData`]): list of region - proposals. - batch_data_samples (list[:obj:`DetDataSample`]): The batch - data samples. It usually includes information such - as `gt_instance` or `gt_panoptic_seg` or `gt_sem_seg`. - - Returns: - dict[str, Tensor]: A dictionary of loss components - """ - outputs = unpack_gt_instances(batch_data_samples) - batch_gt_instances, batch_gt_instances_ignore, batch_img_metas = outputs - - # assign gts and sample proposals - num_imgs = len(batch_data_samples) - sampling_results = [] - for i in range(num_imgs): - # rename rpn_results.bboxes to rpn_results.priors - rpn_results = rpn_results_list[i] - rpn_results.priors = rpn_results.pop("bboxes") - - assign_result = self.bbox_assigner.assign(rpn_results, batch_gt_instances[i], batch_gt_instances_ignore[i]) - sampling_result = self.bbox_sampler.sample( - assign_result, - rpn_results, - batch_gt_instances[i], - feats=[lvl_feat[i][None] for lvl_feat in x], - ) - sampling_results.append(sampling_result) - - losses = {} - # bbox head loss - if self.with_bbox: - bbox_results = self.bbox_loss(x, sampling_results, batch_img_metas) - losses.update(bbox_results["loss_bbox"]) - - # mask head forward and loss - if self.with_mask: - mask_results = self.mask_loss(x, sampling_results, bbox_results["bbox_feats"], batch_gt_instances) - losses.update(mask_results["loss_mask"]) - - return losses - - def bbox_loss(self, x: tuple[Tensor], sampling_results: list[SamplingResult], batch_img_metas: list[dict]) -> dict: - """Perform forward propagation and loss calculation of the bbox head on the features of the upstream network. - - Args: - x (tuple[Tensor]): list of multi-level img features. - sampling_results (list["obj:`SamplingResult`]): Sampling results. - batch_img_metas (list[Dict]): Meta information of each image, e.g., image size, scaling factor, etc. - - Returns: - dict[str, Tensor]: Usually returns a dictionary with keys: - - - `cls_score` (Tensor): Classification scores. - - `bbox_pred` (Tensor): Box energies / deltas. - - `bbox_feats` (Tensor): Extract bbox RoI features. - - `loss_bbox` (dict): A dictionary of bbox loss components. - """ - rois = bbox2roi([res.bboxes for res in sampling_results]) - bbox_results = self._bbox_forward(x, rois) - - bbox_loss_and_target = self.bbox_head.loss_and_target( - cls_score=bbox_results["cls_score"], - bbox_pred=bbox_results["bbox_pred"], - rois=rois, - sampling_results=sampling_results, - rcnn_train_cfg=self.train_cfg, - batch_img_metas=batch_img_metas, - ) - bbox_results.update(loss_bbox=bbox_loss_and_target["loss_bbox"]) - - return bbox_results - - -@MODELS.register_module() -class CustomConvFCBBoxHead(Shared2FCBBoxHead, ClassIncrementalMixin): - """CustomConvFCBBoxHead class for OTX.""" - - def loss_and_target( - self, - cls_score: Tensor, - bbox_pred: Tensor, - rois: Tensor, - sampling_results: list[SamplingResult], - rcnn_train_cfg: ConfigDict, - batch_img_metas: list[dict], - concat: bool = True, - reduction_override: str | None = None, - ) -> dict: - """Calculate the loss based on the features extracted by the bbox head. - - Args: - cls_score (Tensor): Classification prediction - results of all class, has shape - (batch_size * num_proposals_single_image, num_classes) - bbox_pred (Tensor): Regression prediction results, - has shape - (batch_size * num_proposals_single_image, 4), the last - dimension 4 represents [tl_x, tl_y, br_x, br_y]. - rois (Tensor): RoIs with the shape - (batch_size * num_proposals_single_image, 5) where the first - column indicates batch id of each RoI. - sampling_results (list[obj:SamplingResult]): Assign results of - all images in a batch after sampling. - rcnn_train_cfg (obj:ConfigDict): `train_cfg` of RCNN. - batch_img_metas (list[Dict]): Meta information of each image, e.g., image size, scaling factor, etc. - concat (bool): Whether to concatenate the results of all - the images in a single batch. Defaults to True. - reduction_override (str, optional): The reduction - method used to override the original reduction - method of the loss. Options are "none", - "mean" and "sum". Defaults to None, - - Returns: - dict: A dictionary of loss and targets components. - The targets are only used for cascade rcnn. - """ - cls_reg_targets = self.get_targets( - sampling_results, - rcnn_train_cfg, - concat=concat, - batch_img_metas=batch_img_metas, - ) - losses = self.loss( - cls_score, - bbox_pred, - rois, - *cls_reg_targets, - reduction_override=reduction_override, # type: ignore[misc] - ) - - # cls_reg_targets is only for cascade rcnn - return {"loss_bbox": losses, "bbox_targets": cls_reg_targets} - - def get_targets( - self, - sampling_results: list[SamplingResult], - rcnn_train_cfg: ConfigDict, - batch_img_metas: list[dict], - concat: bool = True, - ) -> tuple: - """Calculate the ground truth for all samples in a batch according to the sampling_results. - - Almost the same as the implementation in bbox_head, we passed - additional parameters pos_inds_list and neg_inds_list to - `_get_targets_single` function. - - Args: - sampling_results (list[obj:SamplingResult]): Assign results of - all images in a batch after sampling. - rcnn_train_cfg (obj:ConfigDict): `train_cfg` of RCNN. - batch_img_metas (list[Dict]): Meta information of each image, e.g., image size, scaling factor, etc. - concat (bool): Whether to concatenate the results of all - the images in a single batch. - - Returns: - tuple[Tensor]: Ground truth for proposals in a single image. - Containing the following list of Tensors: - - - labels (list[Tensor],Tensor): Gt_labels for all - proposals in a batch, each tensor in list has - shape (num_proposals,) when `concat=False`, otherwise - just a single tensor has shape (num_all_proposals,). - - label_weights (list[Tensor]): Labels_weights for - all proposals in a batch, each tensor in list has - shape (num_proposals,) when `concat=False`, otherwise - just a single tensor has shape (num_all_proposals,). - - bbox_targets (list[Tensor],Tensor): Regression target - for all proposals in a batch, each tensor in list - has shape (num_proposals, 4) when `concat=False`, - otherwise just a single tensor has shape - (num_all_proposals, 4), the last dimension 4 represents - [tl_x, tl_y, br_x, br_y]. - - bbox_weights (list[tensor],Tensor): Regression weights for - all proposals in a batch, each tensor in list has shape - (num_proposals, 4) when `concat=False`, otherwise just a - single tensor has shape (num_all_proposals, 4). - """ - pos_priors_list = [res.pos_priors for res in sampling_results] - neg_priors_list = [res.neg_priors for res in sampling_results] - pos_gt_bboxes_list = [res.pos_gt_bboxes for res in sampling_results] - pos_gt_labels_list = [res.pos_gt_labels for res in sampling_results] - labels, label_weights, bbox_targets, bbox_weights = multi_apply( - self._get_targets_single, - pos_priors_list, - neg_priors_list, - pos_gt_bboxes_list, - pos_gt_labels_list, - cfg=rcnn_train_cfg, - ) - - valid_label_mask = self.get_valid_label_mask(img_metas=batch_img_metas, all_labels=labels, use_bg=True) - valid_label_mask = [i.to(labels[0].device) for i in valid_label_mask] - - if concat: - labels = torch.cat(labels, 0) - label_weights = torch.cat(label_weights, 0) - bbox_targets = torch.cat(bbox_targets, 0) - bbox_weights = torch.cat(bbox_weights, 0) - valid_label_mask = torch.cat(valid_label_mask, 0) - return labels, label_weights, bbox_targets, bbox_weights, valid_label_mask - - def loss( - self, - cls_score: Tensor, - bbox_pred: Tensor, - rois: Tensor, - labels: Tensor, - label_weights: Tensor, - bbox_targets: Tensor, - bbox_weights: Tensor, - valid_label_mask: Tensor | None = None, - reduction_override: str | None = None, - ) -> dict: - """Loss function for CustomConvFCBBoxHead.""" - losses = {} - if cls_score is not None and cls_score.numel() > 0: - avg_factor = max(torch.sum(label_weights > 0).float().item(), 1.0) - - if isinstance(self.loss_cls, CrossSigmoidFocalLoss): - losses["loss_cls"] = self.loss_cls( - cls_score, - labels, - label_weights, - avg_factor=avg_factor, - reduction_override=reduction_override, - valid_label_mask=valid_label_mask, - ) - else: - losses["loss_cls"] = self.loss_cls( - cls_score, - labels, - label_weights, - avg_factor=avg_factor, - reduction_override=reduction_override, - ) - losses["acc"] = accuracy(cls_score, labels) - if bbox_pred is not None: - bg_class_ind = self.num_classes - # 0~self.num_classes-1 are FG, self.num_classes is BG - pos_inds = (labels >= 0) & (labels < bg_class_ind) - # do not perform bounding box regression for BG anymore. - if pos_inds.any(): - if self.reg_decoded_bbox: - # When the regression loss (e.g. `IouLoss`, - # `GIouLoss`, `DIouLoss`) is applied directly on - # the decoded bounding boxes, it decodes the - # already encoded coordinates to absolute format. - bbox_pred = self.bbox_coder.decode(rois[:, 1:], bbox_pred) - if self.reg_class_agnostic: - pos_bbox_pred = bbox_pred.view(bbox_pred.size(0), 4)[pos_inds.type(torch.bool)] - else: - pos_bbox_pred = bbox_pred.view(bbox_pred.size(0), -1, 4)[ - pos_inds.type(torch.bool), - labels[pos_inds.type(torch.bool)], - ] - losses["loss_bbox"] = self.loss_bbox( - pos_bbox_pred, - bbox_targets[pos_inds.type(torch.bool)], - bbox_weights[pos_inds.type(torch.bool)], - avg_factor=bbox_targets.size(0), - reduction_override=reduction_override, - ) - else: - losses["loss_bbox"] = bbox_pred[pos_inds].sum() - return losses diff --git a/src/otx/algo/instance_segmentation/maskrcnn.py b/src/otx/algo/instance_segmentation/maskrcnn.py deleted file mode 100644 index 73e61498e25..00000000000 --- a/src/otx/algo/instance_segmentation/maskrcnn.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""MaskRCNN model implementations.""" - -from __future__ import annotations - -from typing import Any, Literal - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.instance_segmentation import MMDetInstanceSegCompatibleModel - - -class MaskRCNN(MMDetInstanceSegCompatibleModel): - """MaskRCNN Model.""" - - def __init__(self, num_classes: int, variant: Literal["efficientnetb2b", "r50", "swint"]) -> None: - model_name = f"maskrcnn_{variant}" - config = read_mmconfig(model_name=model_name) - super().__init__(num_classes=num_classes, config=config) - self.image_size = (1, 3, 1024, 1024) - self.tile_image_size = (1, 3, 512, 512) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - export_params = super()._export_parameters - export_params["deploy_cfg"] = "otx.algo.instance_segmentation.mmdeploy.maskrcnn" - export_params["input_size"] = self.image_size - export_params["resize_mode"] = "standard" # [TODO](@Eunwoo): need to revert it to fit_to_window after resolving - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - - return export_params - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_iseg_ckpt(state_dict, add_prefix) - - -class MaskRCNNSwinT(MaskRCNN): - """MaskRCNNSwinT Model.""" - - def __init__(self, num_classes: int) -> None: - super().__init__(num_classes=num_classes, variant="swint") - self.image_size = (1, 3, 1344, 1344) - self.tile_image_size = (1, 3, 512, 512) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - export_params = super()._export_parameters - export_params["deploy_cfg"] = "otx.algo.instance_segmentation.mmdeploy.maskrcnn_swint" - export_params["input_size"] = self.image_size - export_params["resize_mode"] = "standard" # [TODO](@Eunwoo): need to revert it to fit_to_window after resolving - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - - return export_params diff --git a/src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_efficientnetb2b.yaml b/src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_efficientnetb2b.yaml deleted file mode 100644 index 13ce4962ebf..00000000000 --- a/src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_efficientnetb2b.yaml +++ /dev/null @@ -1,199 +0,0 @@ -load_from: https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/models/instance_segmentation/v2/efficientnet_b2b-mask_rcnn-576x576.pth -data_preprocessor: - type: "DetDataPreprocessor" - non_blocking: true - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_mask: true - pad_size_divisor: 32 - std: - - 1.0 - - 1.0 - - 1.0 -type: MaskRCNN -backbone: - type: efficientnet_b2b - out_indices: - - 2 - - 3 - - 4 - - 5 - frozen_stages: -1 - pretrained: true - activation_cfg: - type: torch_swish - norm_cfg: - type: BN - requires_grad: true -neck: - type: FPN - in_channels: - - 24 - - 48 - - 120 - - 352 - out_channels: 80 - num_outs: 5 -rpn_head: - type: RPNHead - in_channels: 80 - feat_channels: 80 - anchor_generator: - type: AnchorGenerator - scales: - - 8 - ratios: - - 0.5 - - 1.0 - - 2.0 - strides: - - 4 - - 8 - - 16 - - 32 - - 64 - bbox_coder: - type: DeltaXYWHBBoxCoder - target_means: - - 0.0 - - 0.0 - - 0.0 - - 0.0 - target_stds: - - 1.0 - - 1.0 - - 1.0 - - 1.0 - loss_cls: - type: CrossSigmoidFocalLoss - use_sigmoid: true - loss_weight: 1.0 - loss_bbox: - type: L1Loss - loss_weight: 1.0 -roi_head: - type: CustomRoIHead - bbox_roi_extractor: - type: SingleRoIExtractor - roi_layer: - type: RoIAlign - output_size: 7 - sampling_ratio: 0 - out_channels: 80 - featmap_strides: - - 4 - - 8 - - 16 - - 32 - bbox_head: - type: CustomConvFCBBoxHead - in_channels: 80 - fc_out_channels: 1024 - roi_feat_size: 7 - num_classes: 80 - bbox_coder: - type: DeltaXYWHBBoxCoder - target_means: - - 0.0 - - 0.0 - - 0.0 - - 0.0 - target_stds: - - 0.1 - - 0.1 - - 0.2 - - 0.2 - reg_class_agnostic: false - loss_cls: - type: CrossEntropyLoss - use_sigmoid: false - loss_weight: 1.0 - loss_bbox: - type: L1Loss - loss_weight: 1.0 - mask_roi_extractor: - type: SingleRoIExtractor - roi_layer: - type: RoIAlign - output_size: 14 - sampling_ratio: 0 - out_channels: 80 - featmap_strides: - - 4 - - 8 - - 16 - - 32 - mask_head: - type: FCNMaskHead - num_convs: 4 - in_channels: 80 - conv_out_channels: 80 - num_classes: 80 - loss_mask: - type: CrossEntropyLoss - use_mask: true - loss_weight: 1.0 -train_cfg: - rpn: - assigner: - type: MaxIoUAssigner - pos_iou_thr: 0.7 - neg_iou_thr: 0.3 - min_pos_iou: 0.3 - match_low_quality: true - ignore_iof_thr: -1 - gpu_assign_thr: 300 - sampler: - type: RandomSampler - num: 256 - pos_fraction: 0.5 - neg_pos_ub: -1 - add_gt_as_proposals: false - allowed_border: -1 - pos_weight: -1 - debug: false - rpn_proposal: - nms_across_levels: false - nms_pre: 2000 - max_per_img: 1000 - nms: - type: nms - iou_threshold: 0.8 - min_bbox_size: 0 - rcnn: - assigner: - type: MaxIoUAssigner - pos_iou_thr: 0.5 - neg_iou_thr: 0.5 - min_pos_iou: 0.5 - match_low_quality: true - ignore_iof_thr: -1 - gpu_assign_thr: 300 - sampler: - type: RandomSampler - num: 256 - pos_fraction: 0.25 - neg_pos_ub: -1 - add_gt_as_proposals: true - mask_size: 28 - pos_weight: -1 - debug: false -test_cfg: - rpn: - nms_across_levels: false - nms_pre: 800 - max_per_img: 500 - nms: - type: nms - iou_threshold: 0.8 - min_bbox_size: 0 - rcnn: - score_thr: 0.05 - nms: - type: nms - iou_threshold: 0.7 - max_per_img: 500 - mask_thr_binary: 0.5 diff --git a/src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_r50.yaml b/src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_r50.yaml deleted file mode 100644 index aee1178ba7c..00000000000 --- a/src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_r50.yaml +++ /dev/null @@ -1,199 +0,0 @@ -load_from: https://download.openmmlab.com/mmdetection/v2.0/mask_rcnn/mask_rcnn_r50_fpn_mstrain-poly_3x_coco/mask_rcnn_r50_fpn_mstrain-poly_3x_coco_20210524_201154-21b550bb.pth -backbone: - depth: 50 - frozen_stages: 1 - init_cfg: - checkpoint: "torchvision://resnet50" - type: "Pretrained" - norm_cfg: - requires_grad: true - type: "BN" - norm_eval: true - num_stages: 4 - out_indices: - - 0 - - 1 - - 2 - - 3 - style: "pytorch" - type: "ResNet" -data_preprocessor: - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_mask: true - pad_size_divisor: 32 - std: - - 58.395 - - 57.12 - - 57.375 - type: "DetDataPreprocessor" - non_blocking: true -neck: - in_channels: - - 256 - - 512 - - 1024 - - 2048 - num_outs: 5 - out_channels: 256 - type: "FPN" -roi_head: - bbox_head: - bbox_coder: - target_means: - - 0.0 - - 0.0 - - 0.0 - - 0.0 - target_stds: - - 0.1 - - 0.1 - - 0.2 - - 0.2 - type: "DeltaXYWHBBoxCoder" - fc_out_channels: 1024 - in_channels: 256 - loss_bbox: - loss_weight: 1.0 - type: "L1Loss" - loss_cls: - loss_weight: 1.0 - type: "CrossSigmoidFocalLoss" - use_sigmoid: false - num_classes: 5 - reg_class_agnostic: false - roi_feat_size: 7 - type: "CustomConvFCBBoxHead" - bbox_roi_extractor: - featmap_strides: - - 4 - - 8 - - 16 - - 32 - out_channels: 256 - roi_layer: - output_size: 7 - sampling_ratio: 0 - type: "RoIAlign" - type: "SingleRoIExtractor" - mask_head: - conv_out_channels: 256 - in_channels: 256 - loss_mask: - loss_weight: 1.0 - type: "CrossEntropyLoss" - use_mask: true - num_classes: 5 - num_convs: 4 - type: "FCNMaskHead" - mask_roi_extractor: - featmap_strides: - - 4 - - 8 - - 16 - - 32 - out_channels: 256 - roi_layer: - output_size: 14 - sampling_ratio: 0 - type: "RoIAlign" - type: "SingleRoIExtractor" - type: "CustomRoIHead" -rpn_head: - anchor_generator: - ratios: - - 0.5 - - 1.0 - - 2.0 - scales: - - 8 - strides: - - 4 - - 8 - - 16 - - 32 - - 64 - type: "AnchorGenerator" - bbox_coder: - target_means: - - 0.0 - - 0.0 - - 0.0 - - 0.0 - target_stds: - - 1.0 - - 1.0 - - 1.0 - - 1.0 - type: "DeltaXYWHBBoxCoder" - feat_channels: 256 - in_channels: 256 - loss_bbox: - loss_weight: 1.0 - type: "L1Loss" - loss_cls: - loss_weight: 1.0 - type: "CrossEntropyLoss" - use_sigmoid: true - type: "RPNHead" -test_cfg: - rcnn: - mask_thr_binary: 0.5 - max_per_img: 100 - nms: - iou_threshold: 0.5 - type: "nms" - score_thr: 0.05 - rpn: - max_per_img: 1000 - min_bbox_size: 0 - nms: - iou_threshold: 0.7 - type: "nms" - nms_pre: 1000 -train_cfg: - rcnn: - assigner: - ignore_iof_thr: -1 - match_low_quality: true - min_pos_iou: 0.5 - neg_iou_thr: 0.5 - pos_iou_thr: 0.5 - type: "MaxIoUAssigner" - debug: false - mask_size: 28 - pos_weight: -1 - sampler: - add_gt_as_proposals: true - neg_pos_ub: -1 - num: 512 - pos_fraction: 0.25 - type: "RandomSampler" - rpn: - allowed_border: -1 - assigner: - ignore_iof_thr: -1 - match_low_quality: true - min_pos_iou: 0.3 - neg_iou_thr: 0.3 - pos_iou_thr: 0.7 - type: "MaxIoUAssigner" - debug: false - pos_weight: -1 - sampler: - add_gt_as_proposals: false - neg_pos_ub: -1 - num: 256 - pos_fraction: 0.5 - type: "RandomSampler" - rpn_proposal: - max_per_img: 1000 - min_bbox_size: 0 - nms: - iou_threshold: 0.7 - type: "nms" - nms_pre: 2000 -type: "MaskRCNN" diff --git a/src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_swint.yaml b/src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_swint.yaml deleted file mode 100644 index caed1cb7af4..00000000000 --- a/src/otx/algo/instance_segmentation/mmconfigs/maskrcnn_swint.yaml +++ /dev/null @@ -1,212 +0,0 @@ -load_from: https://download.openmmlab.com/mmdetection/v2.0/swin/mask_rcnn_swin-t-p4-w7_fpn_fp16_ms-crop-3x_coco/mask_rcnn_swin-t-p4-w7_fpn_fp16_ms-crop-3x_coco_20210908_165006-90a4008c.pth -backbone: - attn_drop_rate: 0.0 - convert_weights: true - depths: - - 2 - - 2 - - 6 - - 2 - drop_path_rate: 0.2 - drop_rate: 0.0 - embed_dims: 96 - init_cfg: - checkpoint: https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_tiny_patch4_window7_224.pth - type: Pretrained - mlp_ratio: 4 - num_heads: - - 3 - - 6 - - 12 - - 24 - out_indices: - - 0 - - 1 - - 2 - - 3 - patch_norm: true - qk_scale: null - qkv_bias: true - type: SwinTransformer - window_size: 7 - with_cp: false -data_preprocessor: - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_mask: true - pad_size_divisor: 32 - std: - - 58.395 - - 57.12 - - 57.375 - type: DetDataPreprocessor - non_blocking: true -neck: - in_channels: - - 96 - - 192 - - 384 - - 768 - num_outs: 5 - out_channels: 256 - type: FPN -roi_head: - bbox_head: - bbox_coder: - target_means: - - 0.0 - - 0.0 - - 0.0 - - 0.0 - target_stds: - - 0.1 - - 0.1 - - 0.2 - - 0.2 - type: DeltaXYWHBBoxCoder - fc_out_channels: 1024 - in_channels: 256 - loss_bbox: - loss_weight: 1.0 - type: L1Loss - loss_cls: - loss_weight: 1.0 - type: CrossEntropyLoss - use_sigmoid: false - num_classes: 80 - reg_class_agnostic: false - roi_feat_size: 7 - type: CustomConvFCBBoxHead - bbox_roi_extractor: - featmap_strides: - - 4 - - 8 - - 16 - - 32 - out_channels: 256 - roi_layer: - output_size: 7 - sampling_ratio: 0 - type: RoIAlign - type: SingleRoIExtractor - mask_head: - conv_out_channels: 256 - in_channels: 256 - loss_mask: - loss_weight: 1.0 - type: CrossEntropyLoss - use_mask: true - num_classes: 80 - num_convs: 4 - type: FCNMaskHead - mask_roi_extractor: - featmap_strides: - - 4 - - 8 - - 16 - - 32 - out_channels: 256 - roi_layer: - output_size: 14 - sampling_ratio: 0 - type: RoIAlign - type: SingleRoIExtractor - type: CustomRoIHead -rpn_head: - anchor_generator: - ratios: - - 0.5 - - 1.0 - - 2.0 - scales: - - 8 - strides: - - 4 - - 8 - - 16 - - 32 - - 64 - type: AnchorGenerator - bbox_coder: - target_means: - - 0.0 - - 0.0 - - 0.0 - - 0.0 - target_stds: - - 1.0 - - 1.0 - - 1.0 - - 1.0 - type: DeltaXYWHBBoxCoder - feat_channels: 256 - in_channels: 256 - loss_bbox: - loss_weight: 1.0 - type: L1Loss - loss_cls: - loss_weight: 1.0 - type: CrossSigmoidFocalLoss - use_sigmoid: true - type: RPNHead -test_cfg: - rcnn: - mask_thr_binary: 0.5 - max_per_img: 100 - nms: - iou_threshold: 0.5 - type: nms - score_thr: 0.05 - rpn: - max_per_img: 1000 - min_bbox_size: 0 - nms: - iou_threshold: 0.7 - type: nms - nms_pre: 1000 -train_cfg: - rcnn: - assigner: - ignore_iof_thr: -1 - match_low_quality: true - min_pos_iou: 0.5 - neg_iou_thr: 0.5 - pos_iou_thr: 0.5 - type: MaxIoUAssigner - debug: false - mask_size: 28 - pos_weight: -1 - sampler: - add_gt_as_proposals: true - neg_pos_ub: -1 - num: 512 - pos_fraction: 0.25 - type: RandomSampler - rpn: - allowed_border: -1 - assigner: - ignore_iof_thr: -1 - match_low_quality: true - min_pos_iou: 0.3 - neg_iou_thr: 0.3 - pos_iou_thr: 0.7 - type: MaxIoUAssigner - debug: false - pos_weight: -1 - sampler: - add_gt_as_proposals: false - neg_pos_ub: -1 - num: 256 - pos_fraction: 0.5 - type: RandomSampler - rpn_proposal: - max_per_img: 1000 - min_bbox_size: 0 - nms: - iou_threshold: 0.7 - type: nms - nms_pre: 2000 -type: MaskRCNN diff --git a/src/otx/algo/instance_segmentation/mmconfigs/rtmdet_inst_tiny.yaml b/src/otx/algo/instance_segmentation/mmconfigs/rtmdet_inst_tiny.yaml deleted file mode 100644 index fecd05202d1..00000000000 --- a/src/otx/algo/instance_segmentation/mmconfigs/rtmdet_inst_tiny.yaml +++ /dev/null @@ -1,99 +0,0 @@ -load_from: https://download.openmmlab.com/mmdetection/v3.0/rtmdet/rtmdet-ins_tiny_8xb32-300e_coco/rtmdet-ins_tiny_8xb32-300e_coco_20221130_151727-ec670f7e.pth -type: RTMDet -data_preprocessor: - type: DetDataPreprocessor - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - pad_value: 114 - bgr_to_rgb: false - pad_mask: true - pad_size_divisor: 32 - non_blocking: true -backbone: - type: CSPNeXt - arch: P5 - expand_ratio: 0.5 - deepen_factor: 0.167 - widen_factor: 0.375 - channel_attention: true - norm_cfg: - type: BN - act_cfg: - type: SiLU - inplace: true - init_cfg: - type: Pretrained - prefix: backbone. - checkpoint: https://download.openmmlab.com/mmdetection/v3.0/rtmdet/cspnext_rsb_pretrain/cspnext-tiny_imagenet_600e.pth -neck: - type: CSPNeXtPAFPN - in_channels: - - 96 - - 192 - - 384 - out_channels: 96 - num_csp_blocks: 1 - expand_ratio: 0.5 - norm_cfg: - type: BN - act_cfg: - type: SiLU - inplace: true -bbox_head: - type: RTMDetInsSepBNHead - num_classes: 80 - in_channels: 96 - stacked_convs: 2 - share_conv: true - pred_kernel_size: 1 - feat_channels: 96 - act_cfg: - type: SiLU - inplace: true - norm_cfg: - type: BN - requires_grad: true - anchor_generator: - type: MlvlPointGenerator - offset: 0 - strides: - - 8 - - 16 - - 32 - bbox_coder: - type: DistancePointBBoxCoder - loss_cls: - type: QualityFocalLoss - use_sigmoid: true - beta: 2.0 - loss_weight: 1.0 - loss_bbox: - type: GIoULoss - loss_weight: 2.0 - loss_mask: - type: DiceLoss - loss_weight: 2.0 - eps: 5.0e-06 - reduction: mean -train_cfg: - assigner: - type: DynamicSoftLabelAssigner - topk: 13 - allowed_border: -1 - pos_weight: -1 - debug: false -test_cfg: - nms_pre: 300 - min_bbox_size: 0 - score_thr: 0.05 - nms: - type: nms - iou_threshold: 0.6 - max_per_img: 100 - mask_thr_binary: 0.5 diff --git a/src/otx/algo/instance_segmentation/mmdeploy/__init__.py b/src/otx/algo/instance_segmentation/mmdeploy/__init__.py deleted file mode 100644 index cf021c10364..00000000000 --- a/src/otx/algo/instance_segmentation/mmdeploy/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""MMDeploy config for instance segmentation models.""" diff --git a/src/otx/algo/instance_segmentation/mmdeploy/base_instance_segmentation.py b/src/otx/algo/instance_segmentation/mmdeploy/base_instance_segmentation.py deleted file mode 100644 index fa778e82e7c..00000000000 --- a/src/otx/algo/instance_segmentation/mmdeploy/base_instance_segmentation.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Instance segmentation models based deploy config. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -_base_ = ["../../detection/mmdeploy/base_detection.py"] - -ir_config = dict( - output_names=[ - "boxes", - "labels", - "masks", - ], - dynamic_axes={ - "image": { - 0: "batch", - 2: "height", - 3: "width", - }, - "boxes": { - 0: "batch", - 1: "num_dets", - }, - "labels": { - 0: "batch", - 1: "num_dets", - }, - "masks": { - 0: "batch", - 1: "num_dets", - 2: "height", - 3: "width", - }, - }, -) - -codebase_config = dict( - post_processing=dict( - export_postprocess_mask=False, - ), -) diff --git a/src/otx/algo/instance_segmentation/mmdeploy/maskrcnn.py b/src/otx/algo/instance_segmentation/mmdeploy/maskrcnn.py deleted file mode 100644 index 75b0ed5bc69..00000000000 --- a/src/otx/algo/instance_segmentation/mmdeploy/maskrcnn.py +++ /dev/null @@ -1,10 +0,0 @@ -"""MMDeploy config of MaskRCNN models for Instance-Seg Task. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -_base_ = ["./base_instance_segmentation.py"] - -ir_config = dict( - output_names=["boxes", "labels", "masks"], -) diff --git a/src/otx/algo/instance_segmentation/mmdeploy/maskrcnn_swint.py b/src/otx/algo/instance_segmentation/mmdeploy/maskrcnn_swint.py deleted file mode 100644 index dbd4a43131d..00000000000 --- a/src/otx/algo/instance_segmentation/mmdeploy/maskrcnn_swint.py +++ /dev/null @@ -1,15 +0,0 @@ -"""MMDployment config of MaskRCNN-SwinT-FP16 model for Instance-Seg Task. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -_base_ = ["./base_instance_segmentation.py"] - - -# NOTE: Its necessary to use opset11 as squeeze>=opset13 does not work in -# mmdeploy::mmcv::ops::roi_align::roi_align_default. -# Refer to src/otx/algorithms/common/adapters/mmdeploy/ops/custom_ops.py::squeeze__default for future rewrite. -ir_config = dict( - output_names=["boxes", "labels", "masks"], - opset_version=11, -) diff --git a/src/otx/algo/instance_segmentation/mmdeploy/rtmdet_inst.py b/src/otx/algo/instance_segmentation/mmdeploy/rtmdet_inst.py deleted file mode 100644 index 6e3977045e5..00000000000 --- a/src/otx/algo/instance_segmentation/mmdeploy/rtmdet_inst.py +++ /dev/null @@ -1,17 +0,0 @@ -"""MMDeploy config of MaskRCNN models for Instance-Seg Task. - -reference: https://github.com/open-mmlab/mmdeploy/ -""" - -_base_ = ["./base_instance_segmentation.py"] - -ir_config = dict( - output_names=["boxes", "labels", "masks"], -) - -codebase_config = dict( - post_processing=dict( - max_output_boxes_per_class=100, - pre_top_k=300, - ) -) diff --git a/src/otx/algo/instance_segmentation/otx_instseg_evaluation.py b/src/otx/algo/instance_segmentation/otx_instseg_evaluation.py deleted file mode 100644 index 36c68c00ad1..00000000000 --- a/src/otx/algo/instance_segmentation/otx_instseg_evaluation.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Customised MAP metric for instance segmentation.""" - -from __future__ import annotations - -from typing import Any - -import pycocotools.mask as mask_utils -import torch -from torchmetrics.detection.mean_ap import MeanAveragePrecision - - -class OTXMaskRLEMeanAveragePrecision(MeanAveragePrecision): - """Customised MAP metric for instance segmentation. - - This metric computes RLE directly to accelerate the computation. - """ - - def update(self, preds: list[dict], target: list[dict]) -> None: - """Update the metric with the given predictions and targets. - - Args: - preds (list[dict]): list of RLE encoded masks - target (list[dict]): list of RLE encoded masks - """ - for item in preds: - bbox_detection, mask_detection = self._get_safe_item_values(item, warn=self.warn_on_many_detections) - if bbox_detection is not None: - self.detection_box.append(bbox_detection) - if mask_detection is not None: - self.detection_mask.append(mask_detection) - self.detection_labels.append(item["labels"]) - self.detection_scores.append(item["scores"]) - - for item in target: - bbox_groundtruth, mask_groundtruth = self._get_safe_item_values(item) - if bbox_groundtruth is not None: - self.groundtruth_box.append(bbox_groundtruth) - if mask_groundtruth is not None: - self.groundtruth_mask.append(mask_groundtruth) - self.groundtruth_labels.append(item["labels"]) - self.groundtruth_crowds.append(item.get("iscrowd", torch.zeros_like(item["labels"]))) - self.groundtruth_area.append(item.get("area", torch.zeros_like(item["labels"]))) - - def _get_safe_item_values( - self, - item: dict[str, Any], - warn: bool = False, - ) -> tuple: - """Convert masks to RLE format. - - Args: - item: input dictionary containing the boxes or masks - warn: whether to warn if the number of boxes or masks exceeds the max_detection_thresholds - - Returns: - RLE encoded masks - """ - if "segm" in self.iou_type: - masks = [] - for rle in item["masks"]: - if isinstance(rle["counts"], list): - rle["counts"] = mask_utils.frPyObjects(rle, *rle["size"])["counts"] - masks.append((tuple(rle["size"]), rle["counts"])) - return None, tuple(masks) diff --git a/src/otx/algo/instance_segmentation/rtmdet_inst.py b/src/otx/algo/instance_segmentation/rtmdet_inst.py deleted file mode 100644 index 2e53cfb2d7c..00000000000 --- a/src/otx/algo/instance_segmentation/rtmdet_inst.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""RTMDetInst model implementations.""" - -from __future__ import annotations - -from typing import Any, Literal - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.core.model.entity.instance_segmentation import MMDetInstanceSegCompatibleModel - - -class RTMDetInst(MMDetInstanceSegCompatibleModel): - """RTMDetInst Model.""" - - def __init__(self, num_classes: int, variant: Literal["tiny"]) -> None: - model_name = f"rtmdet_inst_{variant}" - config = read_mmconfig(model_name=model_name) - super().__init__(num_classes=num_classes, config=config) - self.image_size = (1, 3, 640, 640) - self.tile_image_size = self.image_size - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - export_params = super()._export_parameters - export_params["deploy_cfg"] = "otx.algo.instance_segmentation.mmdeploy.rtmdet_inst" - export_params["input_size"] = self.image_size - export_params["resize_mode"] = "fit_to_window_letterbox" - export_params["pad_value"] = 114 - export_params["swap_rgb"] = False - - return export_params diff --git a/src/otx/algo/samplers/__init__.py b/src/otx/algo/samplers/__init__.py deleted file mode 100644 index cff7a58d840..00000000000 --- a/src/otx/algo/samplers/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""Custom samplers for the OTX2.0.""" - -from .balanced_sampler import BalancedSampler - -__all__ = ["BalancedSampler"] diff --git a/src/otx/algo/samplers/balanced_sampler.py b/src/otx/algo/samplers/balanced_sampler.py deleted file mode 100644 index 287bbf1dcf4..00000000000 --- a/src/otx/algo/samplers/balanced_sampler.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""Balanced sampler for imbalanced data.""" - -from __future__ import annotations - -import math -from typing import TYPE_CHECKING - -import torch -from torch.utils.data import Sampler - -from otx.core.utils.utils import get_idx_list_per_classes - -if TYPE_CHECKING: - from otx.core.data.dataset.base import OTXDataset - - -class BalancedSampler(Sampler): - """Balanced sampler for imbalanced data for class-incremental task. - - This sampler is a sampler that creates an effective batch - In reduce mode, - reduce the iteration size by estimating the trials - that all samples in the tail class are selected more than once with probability 0.999 - - Args: - dataset (OTXDataset): A built-up dataset - efficient_mode (bool): Flag about using efficient mode - num_replicas (int, optional): Number of processes participating in - distributed training. By default, :attr:`world_size` is retrieved from the - current distributed group. - rank (int, optional): Rank of the current process within :attr:`num_replicas`. - By default, :attr:`rank` is retrieved from the current distributed - group. - drop_last (bool, optional): if ``True``, then the sampler will drop the - tail of the data to make it evenly divisible across the number of - replicas. If ``False``, the sampler will add extra indices to make - the data evenly divisible across the replicas. Default: ``False``. - n_repeats (int, optional) : number of iterations for manual setting - """ - - def __init__( - self, - dataset: OTXDataset, - efficient_mode: bool = False, - num_replicas: int = 1, - rank: int = 0, - drop_last: bool = False, - n_repeats: int = 1, - generator: torch.Generator | None = None, - ): - self.dataset = dataset - self.num_replicas = num_replicas - self.rank = rank - self.drop_last = drop_last - self.generator = generator - self.repeat = n_repeats - - super().__init__(dataset) - - # img_indices: dict[label: list[idx]] - ann_stats = get_idx_list_per_classes(dataset.dm_subset) - self.img_indices = {k: torch.tensor(v, dtype=torch.int64) for k, v in ann_stats.items() if len(v) > 0} - self.num_cls = len(self.img_indices.keys()) - self.data_length = len(self.dataset) - self.num_trials = int(self.data_length / self.num_cls) - - if efficient_mode: - # Reduce the # of sampling (sampling data for a single epoch) - num_tail = min(len(cls_indices) for cls_indices in self.img_indices.values()) - if num_tail > 1: - base = 1 - (1 / num_tail) - num_reduced_trials = int(math.log(0.001, base)) - self.num_trials = min(num_reduced_trials, self.num_trials) - - self.num_samples = self._calculate_num_samples() - - def _calculate_num_samples(self) -> int: - num_samples = self.num_trials * self.num_cls * self.repeat - - if self.num_replicas > 1: - # If the dataset length is evenly divisible by # of replicas, then there - # is no need to drop any data, since the dataset will be split equally. - if self.drop_last and num_samples % self.num_replicas != 0: - # Split to nearest available length that is evenly divisible. - # This is to ensure each rank receives the same amount of data when - # using this Sampler. - num_samples = math.ceil( - # `type:ignore` is required because Dataset cannot provide a default __len__ - # see NOTE in pytorch/torch/utils/data/sampler.py - (num_samples - self.num_replicas) / self.num_replicas, - ) - else: - num_samples = math.ceil(num_samples / self.num_replicas) - self.total_size = num_samples * self.num_replicas - - return num_samples - - def __iter__(self): - """Iter.""" - if self.generator is None: - seed = int(torch.empty((), dtype=torch.int64).random_().item()) - generator = torch.Generator() - generator.manual_seed(seed) - else: - generator = self.generator - - indices = [] - for _ in range(self.repeat): - for _ in range(self.num_trials): - index = torch.cat( - [ - self.img_indices[cls_indices][ - torch.randint(0, len(self.img_indices[cls_indices]), (1,), generator=self.generator) - ] - for cls_indices in self.img_indices - ], - ) - indices.append(index) - - indices = torch.cat(indices) - indices = indices.tolist() - - if self.num_replicas > 1: - if not self.drop_last: - # add extra samples to make it evenly divisible - padding_size = self.total_size - len(indices) - if padding_size <= len(indices): - indices += indices[:padding_size] - else: - indices += (indices * math.ceil(padding_size / len(indices)))[:padding_size] - else: - # remove tail of data to make it evenly divisible. - indices = indices[: self.total_size] - - # split and distribute indices - len_indices = len(indices) - indices = indices[ - self.rank * len_indices // self.num_replicas : (self.rank + 1) * len_indices // self.num_replicas - ] - - return iter(indices) - - def __len__(self): - """Return length of selected samples.""" - return self.num_samples diff --git a/src/otx/algo/samplers/class_incremental_sampler.py b/src/otx/algo/samplers/class_incremental_sampler.py deleted file mode 100644 index 5348c1d704a..00000000000 --- a/src/otx/algo/samplers/class_incremental_sampler.py +++ /dev/null @@ -1,181 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""Class incremental sampler for cls-incremental learning.""" - -from __future__ import annotations - -import math - -import numpy as np -import torch -from torch.utils.data import Sampler - -from otx.core.data.dataset.base import OTXDataset -from otx.core.utils.utils import get_idx_list_per_classes - - -class ClassIncrementalSampler(Sampler): - """Sampler for Class-Incremental Task. - - This sampler is a sampler that creates an effective batch - For default setting, - the square root of (number of old data/number of new data) is used as the ratio of old data - In effective mode, - the ratio of old and new data is used as 1:1 - - Args: - dataset (OTXDataset): A built-up dataset - batch_size (int): batch size of Sampling - efficient_mode (bool): Flag about using efficient mode - num_replicas (int, optional): Number of processes participating in - distributed training. By default, :attr:`world_size` is retrieved from the - current distributed group. - rank (int, optional): Rank of the current process within :attr:`num_replicas`. - By default, :attr:`rank` is retrieved from the current distributed - group. - drop_last (bool, optional): if ``True``, then the sampler will drop the - tail of the data to make it evenly divisible across the number of - replicas. If ``False``, the sampler will add extra indices to make - the data evenly divisible across the replicas. Default: ``False``. - n_repeats (Union[float, int, str], optional) : number of iterations for manual setting - """ - - def __init__( - self, - dataset: OTXDataset, - batch_size: int, - old_classes: list[str], - new_classes: list[str], - efficient_mode: bool = False, - num_replicas: int = 1, - rank: int = 0, - drop_last: bool = False, - n_repeats: int = 1, - generator: torch.Generator | None = None, - ): - self.dataset = dataset - self.batch_size = batch_size - self.num_replicas = num_replicas - self.rank = rank - self.drop_last = drop_last - self.generator = generator - self.repeat = n_repeats - - super().__init__(dataset) - - # Need to split new classes dataset indices & old classses dataset indices - ann_stats = get_idx_list_per_classes(dataset.dm_subset, True) - new_indices, old_indices = [], [] - for cls in new_classes: - new_indices.extend(ann_stats[cls]) - self.new_indices = torch.tensor(new_indices, dtype=torch.int64) - for cls in old_classes: - old_indices.extend(ann_stats[cls]) - self.old_indices = torch.tensor(old_indices, dtype=torch.int64) - - if not len(self.new_indices) > 0: - self.new_indices = self.old_indices - self.old_indices = torch.tensor([], dtype=torch.int64) - - old_new_ratio = np.sqrt(len(self.old_indices) / len(self.new_indices)) - - if efficient_mode: - self.data_length = int(len(self.new_indices) * (1 + old_new_ratio)) - self.old_new_ratio = 1 - else: - self.data_length = len(self.dataset) - self.old_new_ratio = int(old_new_ratio) - - self.num_samples = self._calcuate_num_samples() - - def _calcuate_num_samples(self) -> int: - num_samples = self.repeat * (1 + self.old_new_ratio) * int(self.data_length / (1 + self.old_new_ratio)) - - if not self.drop_last: - num_samples += ( - int(np.ceil(self.data_length * self.repeat / self.batch_size)) * self.batch_size - num_samples - ) - - if self.num_replicas > 1: - # If the dataset length is evenly divisible by # of replicas, then there - # is no need to drop any data, since the dataset will be split equally. - if self.drop_last and num_samples % self.num_replicas != 0: - # Split to nearest available length that is evenly divisible. - # This is to ensure each rank receives the same amount of data when - # using this Sampler. - num_samples = math.ceil( - # `type:ignore` is required because Dataset cannot provide a default __len__ - # see NOTE in pytorch/torch/utils/data/sampler.py - (num_samples - self.num_replicas) / self.num_replicas, - ) - else: - num_samples = math.ceil(num_samples / self.num_replicas) - self.total_size = num_samples * self.num_replicas - - return num_samples - - def __iter__(self): - """Iter.""" - if self.generator is None: - seed = int(torch.empty((), dtype=torch.int64).random_().item()) - generator = torch.Generator() - generator.manual_seed(seed) - else: - generator = self.generator - - indices = [] - for _ in range(self.repeat): - num_batches = self.data_length // self.batch_size - for _ in range(num_batches): - num_new_per_batch = self.batch_size // (1 + self.old_new_ratio) - - new_indices_random = torch.randint(0, len(self.new_indices), (num_new_per_batch,), generator=generator) - old_indices_random = torch.randint( - 0, - len(self.old_indices), - (self.batch_size - num_new_per_batch,), - generator=generator, - ) - - new_samples = self.new_indices[new_indices_random] - old_samples = self.old_indices[old_indices_random] - - indices.append(torch.cat([new_samples, old_samples])) - - indices = torch.cat(indices) - if not self.drop_last: - num_extra = int( - np.ceil(self.data_length * self.repeat / self.batch_size), - ) * self.batch_size - len(indices) - indices = torch.cat( - [ - indices, - indices[torch.randint(0, len(indices), (num_extra,), generator=generator)], - ], - ) - - if self.num_replicas > 1: - if not self.drop_last: - # add extra samples to make it evenly divisible - padding_size = self.total_size - len(indices) - # add extra samples to make it evenly divisible - if padding_size <= len(indices): - indices = torch.cat([indices, indices[:padding_size]]) - else: - indices = torch.cat([indices, (indices * math.ceil(padding_size / len(indices)))[:padding_size]]) - else: - # remove tail of data to make it evenly divisible. - indices = indices[: self.total_size] - - # shuffle before distributing indices - indices = indices[torch.randperm(len(indices))] - - # subsample - indices = indices[self.rank : self.total_size : self.num_replicas] - - return iter(indices) - - def __len__(self): - """Return length of selected samples.""" - return self.num_samples diff --git a/src/otx/algo/schedulers/__init__.py b/src/otx/algo/schedulers/__init__.py deleted file mode 100644 index 9ff8f508750..00000000000 --- a/src/otx/algo/schedulers/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom schedulers for the OTX2.0.""" - -from .warmup_schedulers import LinearWarmupScheduler - -__all__ = ["LinearWarmupScheduler"] diff --git a/src/otx/algo/schedulers/warmup_schedulers.py b/src/otx/algo/schedulers/warmup_schedulers.py deleted file mode 100644 index a74a88253c8..00000000000 --- a/src/otx/algo/schedulers/warmup_schedulers.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Warm-up schedulers for the OTX2.0.""" -from __future__ import annotations - -import torch - - -class LinearWarmupScheduler(torch.optim.lr_scheduler.LambdaLR): - """Linear Warmup scheduler.""" - - def __init__( - self, - optimizer: torch.optim.Optimizer, - num_warmup_steps: int = 1000, - interval: str = "step", - ): - if not num_warmup_steps > 0: - msg = f"num_warmup_steps should be > 0, got {num_warmup_steps}" - raise ValueError(msg) - self.num_warmup_steps = num_warmup_steps - self.interval = interval - super().__init__(optimizer, lambda step: min((step + 1.0) / self.num_warmup_steps, 1.0)) - - def step(self, epoch: int | None = None) -> None: - """Overriding the step to disable the warmup scheduler after n_steps.""" - if self._step_count <= self.num_warmup_steps: - super().step(epoch) diff --git a/src/otx/algo/segmentation/__init__.py b/src/otx/algo/segmentation/__init__.py deleted file mode 100644 index db37ae66fa0..00000000000 --- a/src/otx/algo/segmentation/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX segmentation models, hooks, utils, etc.""" - -from . import backbones, heads, losses - -__all__ = ["backbones", "heads", "losses"] diff --git a/src/otx/algo/segmentation/backbones/__init__.py b/src/otx/algo/segmentation/backbones/__init__.py deleted file mode 100644 index 2ed11f7459e..00000000000 --- a/src/otx/algo/segmentation/backbones/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Backbone modules for OTX segmentation model.""" - -from .dinov2 import DinoVisionTransformer -from .litehrnet import LiteHRNet - -__all__ = ["LiteHRNet", "DinoVisionTransformer"] diff --git a/src/otx/algo/segmentation/backbones/dinov2.py b/src/otx/algo/segmentation/backbones/dinov2.py deleted file mode 100644 index 880621d88d2..00000000000 --- a/src/otx/algo/segmentation/backbones/dinov2.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""DINO-V2 model for the OTX classification.""" - -from __future__ import annotations - -from functools import partial - -import torch -from mmengine.model import BaseModule -from mmseg.models.builder import BACKBONES -from torch import nn - - -@BACKBONES.register_module() -class DinoVisionTransformer(BaseModule): - """DINO-v2 Model.""" - - def __init__( - self, - name: str, - freeze_backbone: bool, - out_index: list[int], - init_cfg: dict | None = None, - ): - super().__init__(init_cfg) - torch.hub._validate_not_a_forked_repo = lambda a, b, c: True # noqa: SLF001, ARG005 - self.backbone = torch.hub.load(repo_or_dir="facebookresearch/dinov2", model=name) - if freeze_backbone: - self._freeze_backbone(self.backbone) - - # take intermediate layers to preserve spatial dimension - self.backbone.forward = partial( - self.backbone.get_intermediate_layers, - n=out_index, - reshape=True, - ) - - def _freeze_backbone(self, backbone: nn.Module) -> None: - """Freeze the backbone.""" - for _, v in backbone.named_parameters(): - v.requires_grad = False - - def init_weights(self) -> None: - """Initialize the weights.""" - # restrict rewriting backbone pretrained weights from torch.hub - # unless weights passed explicitly in config - if self.init_cfg: - return super().init_weights() - return None - - def forward(self, imgs: torch.Tensor) -> torch.Tensor: - """Forward function.""" - return self.backbone(imgs) diff --git a/src/otx/algo/segmentation/backbones/litehrnet.py b/src/otx/algo/segmentation/backbones/litehrnet.py deleted file mode 100644 index b503f5a65c3..00000000000 --- a/src/otx/algo/segmentation/backbones/litehrnet.py +++ /dev/null @@ -1,1602 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""HRNet network modules for base backbone. - -Modified from: -- https://github.com/HRNet/Lite-HRNet -""" - - -from __future__ import annotations - -import torch -import torch.utils.checkpoint as cp -from mmcv.cnn import ConvModule, build_conv_layer, build_norm_layer -from mmengine.model import BaseModule -from mmengine.utils import is_tuple_of -from mmseg.registry import MODELS -from torch import nn -from torch.nn import functional - -from otx.algo.utils.segmentation import ( - AsymmetricPositionAttentionModule, - IterativeAggregator, - LocalAttentionModule, - channel_shuffle, -) - - -class NeighbourSupport(nn.Module): - """Neighbour support module.""" - - def __init__( - self, - channels: int, - kernel_size: int = 3, - key_ratio: int = 8, - value_ratio: int = 8, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - ) -> None: - """Neighbour support module. - - Args: - channels (int): Number of input channels. - kernel_size (int): Kernel size for convolutional layers. Default is 3. - key_ratio (int): Ratio of input channels to key channels. Default is 8. - value_ratio (int): Ratio of input channels to value channels. Default is 8. - conv_cfg (dict | None): Config for convolutional layers. Default is None. - norm_cfg (dict | None): Config for normalization layers. Default is None. - """ - super().__init__() - - self.in_channels = channels - self.key_channels = int(channels / key_ratio) - self.value_channels = int(channels / value_ratio) - self.kernel_size = kernel_size - - self.key = nn.Sequential( - ConvModule( - in_channels=self.in_channels, - out_channels=self.key_channels, - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - ), - ConvModule( - self.key_channels, - self.key_channels, - kernel_size=self.kernel_size, - stride=1, - padding=(self.kernel_size - 1) // 2, - groups=self.key_channels, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ), - ConvModule( - in_channels=self.key_channels, - out_channels=self.kernel_size * self.kernel_size, - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ), - ) - self.value = nn.Sequential( - ConvModule( - in_channels=self.in_channels, - out_channels=self.value_channels, - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ), - nn.Unfold(kernel_size=self.kernel_size, stride=1, padding=1), - ) - self.out_conv = ConvModule( - in_channels=self.value_channels, - out_channels=self.in_channels, - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Forward.""" - h, w = (int(_) for _ in x.size()[-2:]) - - key = self.key(x).view(-1, 1, self.kernel_size**2, h, w) - weights = torch.softmax(key, dim=2) - - value = self.value(x).view(-1, self.value_channels, self.kernel_size**2, h, w) - y = torch.sum(weights * value, dim=2) - y = self.out_conv(y) - - return x + y - - -class CrossResolutionWeighting(nn.Module): - """Cross resolution weighting.""" - - def __init__( - self, - channels: list[int], - ratio: int = 16, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - act_cfg: dict | tuple[dict, dict] = ({"type": "ReLU"}, {"type": "Sigmoid"}), - ) -> None: - """Cross resolution weighting. - - Args: - channels (list[int]): Number of channels for each stage. - ratio (int): Reduction ratio of the bottleneck block. - conv_cfg (dict | None): Config dict for convolution layer. Default: None - norm_cfg (dict | None): Config dict for normalization layer. Default: None - act_cfg (dict | tuple[dict, dict]): Config dict or a tuple of config dicts for activation layer(s). - Default: ({"type": "ReLU"}, {"type": "Sigmoid"}). - """ - super().__init__() - - if isinstance(act_cfg, dict): - act_cfg = (act_cfg, act_cfg) - if len(act_cfg) != 2: - msg = "act_cfg must be a dict or a tuple of dicts of length 2." - raise ValueError(msg) - if not is_tuple_of(act_cfg, dict): - msg = "act_cfg must be a dict or a tuple of dicts." - raise TypeError(msg) - - self.channels = channels - total_channel = sum(channels) - - self.conv1 = ConvModule( - in_channels=total_channel, - out_channels=int(total_channel / ratio), - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=act_cfg[0], - ) - self.conv2 = ConvModule( - in_channels=int(total_channel / ratio), - out_channels=total_channel, - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=act_cfg[1], - ) - - def forward(self, x: torch.Tensor) -> list[torch.Tensor]: - """Forward.""" - min_size = [int(_) for _ in x[-1].size()[-2:]] - - out = [functional.adaptive_avg_pool2d(s, min_size) for s in x[:-1]] + [x[-1]] - out = torch.cat(out, dim=1) - out = self.conv1(out) - out = self.conv2(out) - out = torch.split(out, self.channels, dim=1) - - return [s * functional.interpolate(a, size=s.size()[-2:], mode="nearest") for s, a in zip(x, out)] - - -class SpatialWeighting(nn.Module): - """Spatial weighting.""" - - def __init__( - self, - channels: int, - ratio: int = 16, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - act_cfg: dict | tuple[dict, dict] = ({"type": "ReLU"}, {"type": "Sigmoid"}), - enable_norm: bool = False, - ) -> None: - """Spatial weighting. - - Args: - channels (int): Number of input channels. - ratio (int): Reduction ratio for the bottleneck block. Default: 16. - conv_cfg (dict | None): Configuration dict for convolutional layers. - Default: None. - act_cfg (dict | tuple[dict]): Configuration dict or tuple of dicts for - activation layers. If a single dict is provided, it will be used for - both activation layers. Default: ({"type": "ReLU"}, {"type": "Sigmoid"}). - - Raises: - ValueError: act_cfg must be a dict or a tuple of dicts of length 2. - TypeError: If act_cfg is not a dict or a tuple of dicts. - """ - super().__init__() - - if isinstance(act_cfg, dict): - act_cfg = (act_cfg, act_cfg) - if len(act_cfg) != 2: - msg = "act_cfg must be a dict or a tuple of dicts of length 2." - raise ValueError(msg) - if not is_tuple_of(act_cfg, dict): - msg = "act_cfg must be a dict or a tuple of dicts." - raise TypeError(msg) - - self.global_avgpool = nn.AdaptiveAvgPool2d(1) - self.conv1 = ConvModule( - in_channels=channels, - out_channels=int(channels / ratio), - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - act_cfg=act_cfg[0], - ) - self.conv2 = ConvModule( - in_channels=int(channels / ratio), - out_channels=channels, - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - act_cfg=act_cfg[1], - ) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Forward.""" - out = self.global_avgpool(x) - out = self.conv1(out) - out = self.conv2(out) - - return x * out - - -class SpatialWeightingV2(nn.Module): - """The original repo: https://github.com/DeLightCMU/PSA.""" - - def __init__( - self, - channels: int, - ratio: int = 16, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - enable_norm: bool = False, - ) -> None: - """SpatialWeightingV2. - - Args: - channels (int): Number of input channels. - ratio (int): Reduction ratio of internal channels. - conv_cfg (dict | None): Config dict for convolution layer. - norm_cfg (dict | None): Config dict for normalization layer. - enable_norm (bool): Whether to enable normalization layers. - """ - super().__init__() - - self.in_channels = channels - self.internal_channels = int(channels / ratio) - - # channel-only branch - self.v_channel = ConvModule( - in_channels=self.in_channels, - out_channels=self.internal_channels, - kernel_size=1, - stride=1, - bias=False, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg if enable_norm else None, - act_cfg=None, - ) - self.q_channel = ConvModule( - in_channels=self.in_channels, - out_channels=1, - kernel_size=1, - stride=1, - bias=False, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg if enable_norm else None, - act_cfg=None, - ) - self.out_channel = ConvModule( - in_channels=self.internal_channels, - out_channels=self.in_channels, - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "Sigmoid"}, - ) - - # spatial-only branch - self.v_spatial = ConvModule( - in_channels=self.in_channels, - out_channels=self.internal_channels, - kernel_size=1, - stride=1, - bias=False, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg if enable_norm else None, - act_cfg=None, - ) - self.q_spatial = ConvModule( - in_channels=self.in_channels, - out_channels=self.internal_channels, - kernel_size=1, - stride=1, - bias=False, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg if enable_norm else None, - act_cfg=None, - ) - self.global_avgpool = nn.AdaptiveAvgPool2d(1) - - def _channel_weighting(self, x: torch.Tensor) -> torch.Tensor: - """_channel_weighting. - - Args: - x (torch.Tensor): input tensor. - - Returns: - torch.Tensor: output tensor. - """ - h, w = (int(_) for _ in x.size()[-2:]) - - v = self.v_channel(x).view(-1, self.internal_channels, h * w) - - q = self.q_channel(x).view(-1, h * w, 1) - q = torch.softmax(q, dim=1) - - y = torch.matmul(v, q) - y = y.view(-1, self.internal_channels, 1, 1) - y = self.out_channel(y) - - return x * y - - def _spatial_weighting(self, x: torch.Tensor) -> torch.Tensor: - """_spatial_weighting. - - Args: - x (torch.Tensor): input tensor. - - Returns: - torch.Tensor: output tensor. - """ - h, w = (int(_) for _ in x.size()[-2:]) - - v = self.v_spatial(x) - v = v.view(-1, self.internal_channels, h * w) - - q = self.q_spatial(x) - q = self.global_avgpool(q) - q = torch.softmax(q, dim=1) - q = q.view(-1, 1, self.internal_channels) - - y = torch.matmul(q, v) - y = y.view(-1, 1, h, w) - y = torch.sigmoid(y) - - return x * y - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Forward.""" - y_channel = self._channel_weighting(x) - y_spatial = self._spatial_weighting(x) - - return y_channel + y_spatial - - -class ConditionalChannelWeighting(nn.Module): - """Conditional channel weighting module.""" - - def __init__( - self, - in_channels: list[int], - stride: int, - reduce_ratio: int, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - with_cp: bool = False, - dropout: float | None = None, - weighting_module_version: str = "v1", - neighbour_weighting: bool = False, - dw_ksize: int = 3, - ) -> None: - """Conditional channel weighting module. - - Args: - in_channels (list[int]): Number of input channels for each input feature map. - stride (int): Stride used in the first convolutional layer. - reduce_ratio (int): Reduction ratio used in the cross-resolution weighting module. - conv_cfg (dict | None): Dictionary to construct and configure the convolutional layers. - norm_cfg (dict | None): Dictionary to construct and configure the normalization layers. - with_cp (bool): Whether to use checkpointing to save memory. - dropout (float | None): Dropout probability used in the depthwise convolutional layers. - weighting_module_version (str): Version of the spatial weighting module to use. - neighbour_weighting (bool): Whether to use the neighbour support module. - dw_ksize (int): Kernel size used in the depthwise convolutional layers. - - Raises: - ValueError: If stride is not 1 or 2. - """ - super().__init__() - - if norm_cfg is None: - norm_cfg = {"type": "BN"} - - self.with_cp = with_cp - self.stride = stride - if stride not in [1, 2]: - msg = "stride must be 1 or 2." - raise ValueError(msg) - - spatial_weighting_module = SpatialWeighting if weighting_module_version == "v1" else SpatialWeightingV2 - branch_channels = [channel // 2 for channel in in_channels] - - self.cross_resolution_weighting = CrossResolutionWeighting( - branch_channels, - ratio=reduce_ratio, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - ) - self.depthwise_convs = nn.ModuleList( - [ - ConvModule( - channel, - channel, - kernel_size=dw_ksize, - stride=self.stride, - padding=dw_ksize // 2, - groups=channel, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ) - for channel in branch_channels - ], - ) - self.spatial_weighting = nn.ModuleList( - [ - spatial_weighting_module( - channels=channel, - ratio=4, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - enable_norm=True, - ) - for channel in branch_channels - ], - ) - - self.neighbour_weighting = None - if neighbour_weighting: - self.neighbour_weighting = nn.ModuleList( - [ - NeighbourSupport( - channel, - kernel_size=3, - key_ratio=8, - value_ratio=4, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - ) - for channel in branch_channels - ], - ) - - self.dropout = None - if dropout is not None and dropout > 0.0: - self.dropout = nn.ModuleList([nn.Dropout(p=dropout) for _ in branch_channels]) - - def _inner_forward(self, x: torch.Tensor) -> list[torch.Tensor]: - """_inner_forward. - - Args: - x (torch.Tensor): Input tensor. - - Returns: - list[torch.Tensor]: Output tensor. - """ - x = [s.chunk(2, dim=1) for s in x] - x1 = [s[0] for s in x] - x2 = [s[1] for s in x] - - x2 = self.cross_resolution_weighting(x2) - x2 = [dw(s) for s, dw in zip(x2, self.depthwise_convs)] - - if self.neighbour_weighting is not None: - x2 = [nw(s) for s, nw in zip(x2, self.neighbour_weighting)] - - x2 = [sw(s) for s, sw in zip(x2, self.spatial_weighting)] - - if self.dropout is not None: - x2 = [dropout(s) for s, dropout in zip(x2, self.dropout)] - - out = [torch.cat([s1, s2], dim=1) for s1, s2 in zip(x1, x2)] - - return [channel_shuffle(s, 2) for s in out] - - def forward(self, x: torch.Tensor) -> list[torch.Tensor]: - """Forward.""" - return cp.checkpoint(self._inner_forward, x) if self.with_cp and x.requires_grad else self._inner_forward(x) - - -class Stem(nn.Module): - """Stem.""" - - def __init__( - self, - in_channels: int, - stem_channels: int, - out_channels: int, - expand_ratio: int, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - with_cp: bool = False, - strides: tuple[int, int] = (2, 2), - extra_stride: bool = False, - input_norm: bool = False, - ) -> None: - """Stem initialization. - - Args: - in_channels (int): Number of input image channels. Typically 3. - stem_channels (int): Number of output channels of the stem layer. - out_channels (int): Number of output channels of the backbone network. - expand_ratio (int): Expansion ratio of the internal channels. - conv_cfg (dict | None): Dictionary to construct and configure convolution layers. - norm_cfg (dict | None): Dictionary to construct and configure normalization layers. - with_cp (bool): Use checkpointing to save memory during forward pass. - num_stages (int): Number of stages in the backbone network. - strides (tuple[int, int]): Strides of the first and subsequent stages. - extra_stride (bool): Use an extra stride in the second stage. - input_norm (bool): Use instance normalization on the input image. - - Raises: - TypeError: If strides is not a tuple or list. - ValueError: If len(strides) is not equal to num_stages + 1. - """ - super().__init__() - - if norm_cfg is None: - norm_cfg = {"type": "BN"} - - if not isinstance(strides, (tuple, list)): - msg = "strides must be tuple or list." - raise TypeError(msg) - if len(strides) != 2: - msg = "len(strides) must equal to 2." - raise ValueError(msg) - - self.in_channels = in_channels - self.out_channels = out_channels - self.conv_cfg = conv_cfg - self.norm_cfg = norm_cfg - self.with_cp = with_cp - - self.input_norm = None - if input_norm: - self.input_norm = nn.InstanceNorm2d(in_channels) - - self.conv1 = ConvModule( - in_channels=in_channels, - out_channels=stem_channels, - kernel_size=3, - stride=strides[0], - padding=1, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - ) - - self.conv2 = None - if extra_stride: - self.conv2 = ConvModule( - in_channels=stem_channels, - out_channels=stem_channels, - kernel_size=3, - stride=2, - padding=1, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - ) - - mid_channels = int(round(stem_channels * expand_ratio)) - branch_channels = stem_channels // 2 - if stem_channels == self.out_channels: - inc_channels = self.out_channels - branch_channels - else: - inc_channels = self.out_channels - stem_channels - - self.branch1 = nn.Sequential( - ConvModule( - branch_channels, - branch_channels, - kernel_size=3, - stride=strides[1], - padding=1, - groups=branch_channels, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ), - ConvModule( - branch_channels, - inc_channels, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - ), - ) - - self.expand_conv = ConvModule( - branch_channels, - mid_channels, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - ) - self.depthwise_conv = ConvModule( - mid_channels, - mid_channels, - kernel_size=3, - stride=strides[1], - padding=1, - groups=mid_channels, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ) - self.linear_conv = ConvModule( - mid_channels, - branch_channels if stem_channels == self.out_channels else stem_channels, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - ) - - def _inner_forward(self, x: torch.Tensor) -> torch.Tensor: - """_inner_forward. - - Args: - x (torch.Tensor): Input tensor. - - Returns: - torch.Tensor: Output tensor. - """ - if self.input_norm is not None: - x = self.input_norm(x) - - x = self.conv1(x) - if self.conv2 is not None: - x = self.conv2(x) - - x1, x2 = x.chunk(2, dim=1) - - x1 = self.branch1(x1) - - x2 = self.expand_conv(x2) - x2 = self.depthwise_conv(x2) - x2 = self.linear_conv(x2) - - out = torch.cat((x1, x2), dim=1) - - return channel_shuffle(out, 2) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Forward.""" - return cp.checkpoint(self._inner_forward, x) if self.with_cp and x.requires_grad else self._inner_forward(x) - - -class StemV2(nn.Module): - """StemV2.""" - - def __init__( - self, - in_channels: int, - stem_channels: int, - out_channels: int, - expand_ratio: int, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - with_cp: bool = False, - num_stages: int = 1, - strides: tuple[int, int] = (2, 2), - extra_stride: bool = False, - input_norm: bool = False, - ) -> None: - """StemV2 initialization. - - Args: - in_channels (int): Number of input image channels. Typically 3. - stem_channels (int): Number of output channels of the stem layer. - out_channels (int): Number of output channels of the backbone network. - expand_ratio (int): Expansion ratio of the internal channels. - conv_cfg (dict | None): Dictionary to construct and configure convolution layers. - norm_cfg (dict | None): Dictionary to construct and configure normalization layers. - with_cp (bool): Use checkpointing to save memory during forward pass. - num_stages (int): Number of stages in the backbone network. - strides (tuple[int, int]): Strides of the first and subsequent stages. - extra_stride (bool): Use an extra stride in the second stage. - input_norm (bool): Use instance normalization on the input image. - - Raises: - ValueError: If num_stages is less than 1. - TypeError: If strides is not a tuple or list. - ValueError: If len(strides) is not equal to num_stages + 1. - """ - super().__init__() - - if norm_cfg is None: - norm_cfg = {"type": "BN"} - if num_stages < 1: - msg = "num_stages must be greater than 0." - raise ValueError(msg) - if not isinstance(strides, (tuple, list)): - msg = "strides must be tuple or list." - raise TypeError(msg) - - if len(strides) != 1 + num_stages: - msg = "len(strides) must equal to num_stages + 1." - raise ValueError(msg) - - self.in_channels = in_channels - self.out_channels = out_channels - self.conv_cfg = conv_cfg - self.norm_cfg = norm_cfg - self.with_cp = with_cp - self.num_stages = num_stages - - self.input_norm = None - if input_norm: - self.input_norm = nn.InstanceNorm2d(in_channels) - - self.conv1 = ConvModule( - in_channels=in_channels, - out_channels=stem_channels, - kernel_size=3, - stride=strides[0], - padding=1, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - ) - - self.conv2 = None - if extra_stride: - self.conv2 = ConvModule( - in_channels=stem_channels, - out_channels=stem_channels, - kernel_size=3, - stride=2, - padding=1, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - ) - - mid_channels = int(round(stem_channels * expand_ratio)) - internal_branch_channels = stem_channels // 2 - out_branch_channels = self.out_channels // 2 - - self.branch1, self.branch2 = nn.ModuleList(), nn.ModuleList() - for stage in range(1, num_stages + 1): - self.branch1.append( - nn.Sequential( - ConvModule( - internal_branch_channels, - internal_branch_channels, - kernel_size=3, - stride=strides[stage], - padding=1, - groups=internal_branch_channels, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ), - ConvModule( - internal_branch_channels, - out_branch_channels if stage == num_stages else internal_branch_channels, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - ), - ), - ) - - self.branch2.append( - nn.Sequential( - ConvModule( - internal_branch_channels, - mid_channels, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - ), - ConvModule( - mid_channels, - mid_channels, - kernel_size=3, - stride=strides[stage], - padding=1, - groups=mid_channels, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ), - ConvModule( - mid_channels, - out_branch_channels if stage == num_stages else internal_branch_channels, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - ), - ), - ) - - def _inner_forward(self, x: torch.Tensor) -> list[torch.Tensor]: - """Forward pass of Stem module. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, channels, height, width). - - Returns: - list[torch.Tensor]: List of output tensors at each stage of the backbone. - """ - if self.input_norm is not None: - x = self.input_norm(x) - - y = self.conv1(x) - if self.conv2 is not None: - y = self.conv2(y) - - out_list = [y] - for stage in range(self.num_stages): - y1, y2 = y.chunk(2, dim=1) - - y1 = self.branch1[stage](y1) - y2 = self.branch2[stage](y2) - - y = torch.cat((y1, y2), dim=1) - y = channel_shuffle(y, 2) - out_list.append(y) - - return out_list - - def forward(self, x: torch.Tensor) -> list[torch.Tensor]: - """Forward.""" - return cp.checkpoint(self._inner_forward, x) if self.with_cp and x.requires_grad else self._inner_forward(x) - - -class ShuffleUnit(nn.Module): - """InvertedResidual block for ShuffleNetV2 backbone.""" - - def __init__( - self, - in_channels: int, - out_channels: int, - stride: int = 1, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - act_cfg: dict | None = None, - with_cp: bool = False, - ) -> None: - """InvertedResidual block for ShuffleNetV2 backbone. - - Args: - in_channels (int): The input channels of the block. - out_channels (int): The output channels of the block. - stride (int): Stride of the 3x3 convolution layer. Default: 1 - conv_cfg (dict): Config dict for convolution layer. - Default: None, which means using conv2d. - norm_cfg (dict): Config dict for normalization layer. - Default: dict(type='BN'). - act_cfg (dict): Config dict for activation layer. - Default: dict(type='ReLU'). - with_cp (bool): Use checkpoint or not. Using checkpoint will save some - memory while slowing down the training speed. Default: False. - - """ - super().__init__() - - if norm_cfg is None: - norm_cfg = {"type": "BN"} - if act_cfg is None: - act_cfg = {"type": "ReLU"} - - self.stride = stride - self.with_cp = with_cp - - branch_features = out_channels // 2 - if self.stride == 1 and in_channels != branch_features * 2: - msg = ( - f"in_channels ({in_channels}) should equal to " - f"branch_features * 2 ({branch_features * 2}) " - "when stride is 1" - ) - raise ValueError(msg) - - if self.stride > 1: - self.branch1 = nn.Sequential( - ConvModule( - in_channels, - in_channels, - kernel_size=3, - stride=self.stride, - padding=1, - groups=in_channels, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ), - ConvModule( - in_channels, - branch_features, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=act_cfg, - ), - ) - - self.branch2 = nn.Sequential( - ConvModule( - in_channels if (self.stride > 1) else branch_features, - branch_features, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=act_cfg, - ), - ConvModule( - branch_features, - branch_features, - kernel_size=3, - stride=self.stride, - padding=1, - groups=branch_features, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ), - ConvModule( - branch_features, - branch_features, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=act_cfg, - ), - ) - - def _inner_forward(self, x: torch.Tensor) -> torch.Tensor: - """_inner_forward.""" - if self.stride > 1: - out = torch.cat((self.branch1(x), self.branch2(x)), dim=1) - else: - x1, x2 = x.chunk(2, dim=1) - out = torch.cat((x1, self.branch2(x2)), dim=1) - - return channel_shuffle(out, 2) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Forward.""" - return cp.checkpoint(self._inner_forward, x) if self.with_cp and x.requires_grad else self._inner_forward(x) - - -class LiteHRModule(nn.Module): - """LiteHR module.""" - - def __init__( - self, - num_branches: int, - num_blocks: int, - in_channels: list[int], - reduce_ratio: int, - module_type: str, - multiscale_output: bool = False, - with_fuse: bool = True, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - with_cp: bool = False, - dropout: float | None = None, - weighting_module_version: str = "v1", - neighbour_weighting: bool = False, - ) -> None: - """LiteHR module. - - Args: - num_branches (int): Number of branches in the network. - num_blocks (int): Number of blocks in each branch. - in_channels (list[int]): List of input channels for each branch. - reduce_ratio (int): Reduction ratio for the weighting module. - module_type (str): Type of module to use for the network. Can be "LITE" or "NAIVE". - multiscale_output (bool, optional): Whether to output features from all branches. Defaults to False. - with_fuse (bool, optional): Whether to use the fuse layer. Defaults to True. - conv_cfg (dict, optional): Configuration for the convolutional layers. Defaults to None. - norm_cfg (dict, optional): Configuration for the normalization layers. Defaults to None. - with_cp (bool, optional): Whether to use checkpointing. Defaults to False. - dropout (float, optional): Dropout rate. Defaults to None. - weighting_module_version (str, optional): Version of the weighting module to use. Defaults to "v1". - neighbour_weighting (bool, optional): Whether to use neighbour weighting. Defaults to False. - """ - super().__init__() - - if norm_cfg is None: - norm_cfg = {"type": "BN"} - self._check_branches(num_branches, in_channels) - - self.in_channels = in_channels - self.num_branches = num_branches - - self.module_type = module_type - self.multiscale_output = multiscale_output - self.with_fuse = with_fuse - self.norm_cfg = norm_cfg - self.conv_cfg = conv_cfg - self.with_cp = with_cp - self.weighting_module_version = weighting_module_version - self.neighbour_weighting = neighbour_weighting - - if self.module_type == "LITE": - self.layers = self._make_weighting_blocks(num_blocks, reduce_ratio, dropout=dropout) - elif self.module_type == "NAIVE": - self.layers = self._make_naive_branches(num_branches, num_blocks) - - if self.with_fuse: - self.fuse_layers = self._make_fuse_layers() - self.relu = nn.ReLU() - - @staticmethod - def _check_branches(num_branches: int, in_channels: list[int]) -> None: - """Check input to avoid ValueError.""" - if num_branches != len(in_channels): - error_msg = f"NUM_BRANCHES({num_branches}) != NUM_INCHANNELS({len(in_channels)})" - raise ValueError(error_msg) - - def _make_weighting_blocks( - self, - num_blocks: int, - reduce_ratio: int, - stride: int = 1, - dropout: float | None = None, - ) -> nn.Sequential: - layers = [ - ConditionalChannelWeighting( - self.in_channels, - stride=stride, - reduce_ratio=reduce_ratio, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - with_cp=self.with_cp, - dropout=dropout, - weighting_module_version=self.weighting_module_version, - neighbour_weighting=self.neighbour_weighting, - ) - for _ in range(num_blocks) - ] - - return nn.Sequential(*layers) - - def _make_one_branch(self, branch_index: int, num_blocks: int, stride: int = 1) -> nn.Sequential: - """Make one branch.""" - layers = [ - ShuffleUnit( - self.in_channels[branch_index], - self.in_channels[branch_index], - stride=stride, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - with_cp=self.with_cp, - ), - ] + [ - ShuffleUnit( - self.in_channels[branch_index], - self.in_channels[branch_index], - stride=1, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - with_cp=self.with_cp, - ) - for _ in range(1, num_blocks) - ] - - return nn.Sequential(*layers) - - def _make_naive_branches(self, num_branches: int, num_blocks: int) -> nn.ModuleList: - """Make branches.""" - branches = [self._make_one_branch(i, num_blocks) for i in range(num_branches)] - return nn.ModuleList(branches) - - def _make_fuse_layers(self) -> nn.ModuleList: - """Make fuse layer.""" - if self.num_branches == 1: - return None - - num_branches = self.num_branches - in_channels = self.in_channels - num_out_branches = num_branches if self.multiscale_output else 1 - - fuse_layers = [] - for i in range(num_out_branches): - fuse_layer = [] - for j in range(num_branches): - if j > i: - fuse_layer.append( - nn.Sequential( - build_conv_layer( - self.conv_cfg, - in_channels[j], - in_channels[i], - kernel_size=1, - stride=1, - padding=0, - bias=False, - ), - build_norm_layer(self.norm_cfg, in_channels[i])[1], - ), - ) - elif j == i: - fuse_layer.append(None) - else: - conv_downsamples = [] - for k in range(i - j): - if k == i - j - 1: - conv_downsamples.append( - nn.Sequential( - build_conv_layer( - self.conv_cfg, - in_channels[j], - in_channels[j], - kernel_size=3, - stride=2, - padding=1, - groups=in_channels[j], - bias=False, - ), - build_norm_layer(self.norm_cfg, in_channels[j])[1], - build_conv_layer( - self.conv_cfg, - in_channels[j], - in_channels[i], - kernel_size=1, - stride=1, - padding=0, - bias=False, - ), - build_norm_layer(self.norm_cfg, in_channels[i])[1], - ), - ) - else: - conv_downsamples.append( - nn.Sequential( - build_conv_layer( - self.conv_cfg, - in_channels[j], - in_channels[j], - kernel_size=3, - stride=2, - padding=1, - groups=in_channels[j], - bias=False, - ), - build_norm_layer(self.norm_cfg, in_channels[j])[1], - build_conv_layer( - self.conv_cfg, - in_channels[j], - in_channels[j], - kernel_size=1, - stride=1, - padding=0, - bias=False, - ), - build_norm_layer(self.norm_cfg, in_channels[j])[1], - nn.ReLU(inplace=True), - ), - ) - fuse_layer.append(nn.Sequential(*conv_downsamples)) - fuse_layers.append(nn.ModuleList(fuse_layer)) - - return nn.ModuleList(fuse_layers) - - def forward(self, x: torch.Tensor) -> list[torch.Tensor]: - """Forward function.""" - if self.num_branches == 1: - return [self.layers[0](x[0])] - - if self.module_type == "LITE": - out = self.layers(x) - elif self.module_type == "NAIVE": - for i in range(self.num_branches): - x[i] = self.layers[i](x[i]) - out = x - - if self.with_fuse: - out_fuse = [] - for i in range(len(self.fuse_layers)): - y = out[0] if i == 0 else self.fuse_layers[i][0](out[0]) - for j in range(self.num_branches): - fuse_y = out[j] if i == j else self.fuse_layers[i][j](out[j]) - if fuse_y.size()[-2:] != y.size()[-2:]: - fuse_y = functional.interpolate(fuse_y, size=y.size()[-2:], mode="nearest") - - y += fuse_y - - out_fuse.append(self.relu(y)) - - out = out_fuse - elif not self.multiscale_output: - out = [out[0]] - - return out - - -@MODELS.register_module() -class LiteHRNet(BaseModule): - """Lite-HRNet backbone. - - `High-Resolution Representations for Labeling Pixels and Regions - `_ - - Args: - extra (dict): detailed configuration for each stage of HRNet. - in_channels (int): Number of input image channels. Default: 3. - conv_cfg (dict): dictionary to construct and config conv layer. - norm_cfg (dict): dictionary to construct and config norm layer. - norm_eval (bool): Whether to set norm layers to eval mode, namely, - freeze running stats (mean and var). Note: Effect on Batch Norm - and its variants only. Default: False - with_cp (bool): Use checkpoint or not. Using checkpoint will save some - memory while slowing down the training speed. - zero_init_residual (bool): whether to use zero init for last norm layer - in resblocks to let them behave as identity. - """ - - def __init__( - self, - extra: dict, - in_channels: int = 3, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - norm_eval: bool = False, - with_cp: bool = False, - zero_init_residual: bool = False, - dropout: float | None = None, - init_cfg: dict | None = None, - ) -> None: - """Init.""" - super().__init__(init_cfg=init_cfg) - - if norm_cfg is None: - norm_cfg = {"type": "BN"} - - self.extra = extra - self.conv_cfg = conv_cfg - self.norm_cfg = norm_cfg - self.norm_eval = norm_eval - self.with_cp = with_cp - self.zero_init_residual = zero_init_residual - - self.stem = Stem( - in_channels, - input_norm=self.extra["stem"]["input_norm"], - stem_channels=self.extra["stem"]["stem_channels"], - out_channels=self.extra["stem"]["out_channels"], - expand_ratio=self.extra["stem"]["expand_ratio"], - strides=self.extra["stem"]["strides"], - extra_stride=self.extra["stem"]["extra_stride"], - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - ) - - self.enable_stem_pool = self.extra["stem"].get("out_pool", False) - if self.enable_stem_pool: - self.stem_pool = nn.AvgPool2d(kernel_size=3, stride=2) - - self.num_stages = self.extra["num_stages"] - self.stages_spec = self.extra["stages_spec"] - - num_channels_last = [ - self.stem.out_channels, - ] - for i in range(self.num_stages): - num_channels = self.stages_spec["num_channels"][i] - num_channels = [num_channels[i] for i in range(len(num_channels))] - - setattr( - self, - f"transition{i}", - self._make_transition_layer(num_channels_last, num_channels), - ) - - stage, num_channels_last = self._make_stage( - self.stages_spec, - i, - num_channels, - multiscale_output=True, - dropout=dropout, - ) - setattr(self, f"stage{i}", stage) - - self.out_modules = None - if self.extra.get("out_modules") is not None: - out_modules = [] - in_modules_channels, out_modules_channels = num_channels_last[-1], None - if self.extra["out_modules"]["conv"]["enable"]: - out_modules_channels = self.extra["out_modules"]["conv"]["channels"] - out_modules.append( - ConvModule( - in_channels=in_modules_channels, - out_channels=out_modules_channels, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - ), - ) - in_modules_channels = out_modules_channels - if self.extra["out_modules"]["position_att"]["enable"]: - out_modules.append( - AsymmetricPositionAttentionModule( - in_channels=in_modules_channels, - key_channels=self.extra["out_modules"]["position_att"]["key_channels"], - value_channels=self.extra["out_modules"]["position_att"]["value_channels"], - psp_size=self.extra["out_modules"]["position_att"]["psp_size"], - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - ), - ) - if self.extra["out_modules"]["local_att"]["enable"]: - out_modules.append( - LocalAttentionModule( - num_channels=in_modules_channels, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - ), - ) - - if len(out_modules) > 0: - self.out_modules = nn.Sequential(*out_modules) - num_channels_last.append(in_modules_channels) - - self.add_stem_features = self.extra.get("add_stem_features", False) - if self.add_stem_features: - self.stem_transition = nn.Sequential( - ConvModule( - self.stem.out_channels, - self.stem.out_channels, - kernel_size=3, - stride=1, - padding=1, - groups=self.stem.out_channels, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg=None, - ), - ConvModule( - self.stem.out_channels, - num_channels_last[0], - kernel_size=1, - stride=1, - padding=0, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - ), - ) - - num_channels_last = [num_channels_last[0], *num_channels_last] - - self.with_aggregator = self.extra.get("out_aggregator") and self.extra["out_aggregator"]["enable"] - if self.with_aggregator: - self.aggregator = IterativeAggregator( - in_channels=num_channels_last, - min_channels=self.extra["out_aggregator"].get("min_channels", None), - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - ) - - def _make_transition_layer( - self, - num_channels_pre_layer: list[int], - num_channels_cur_layer: list[int], - ) -> nn.ModuleList: - """Make transition layer.""" - num_branches_cur = len(num_channels_cur_layer) - num_branches_pre = len(num_channels_pre_layer) - - transition_layers = [] - for i in range(num_branches_cur): - if i < num_branches_pre: - if num_channels_cur_layer[i] != num_channels_pre_layer[i]: - transition_layers.append( - nn.Sequential( - build_conv_layer( - self.conv_cfg, - num_channels_pre_layer[i], - num_channels_pre_layer[i], - kernel_size=3, - stride=1, - padding=1, - groups=num_channels_pre_layer[i], - bias=False, - ), - build_norm_layer(self.norm_cfg, num_channels_pre_layer[i])[1], - build_conv_layer( - self.conv_cfg, - num_channels_pre_layer[i], - num_channels_cur_layer[i], - kernel_size=1, - stride=1, - padding=0, - bias=False, - ), - build_norm_layer(self.norm_cfg, num_channels_cur_layer[i])[1], - nn.ReLU(), - ), - ) - else: - transition_layers.append(None) - else: - conv_downsamples = [] - for j in range(i + 1 - num_branches_pre): - in_channels = num_channels_pre_layer[-1] - out_channels = num_channels_cur_layer[i] if j == i - num_branches_pre else in_channels - conv_downsamples.append( - nn.Sequential( - build_conv_layer( - self.conv_cfg, - in_channels, - in_channels, - kernel_size=3, - stride=2, - padding=1, - groups=in_channels, - bias=False, - ), - build_norm_layer(self.norm_cfg, in_channels)[1], - build_conv_layer( - self.conv_cfg, - in_channels, - out_channels, - kernel_size=1, - stride=1, - padding=0, - bias=False, - ), - build_norm_layer(self.norm_cfg, out_channels)[1], - nn.ReLU(), - ), - ) - transition_layers.append(nn.Sequential(*conv_downsamples)) - - return nn.ModuleList(transition_layers) - - def _make_stage( - self, - stages_spec: dict, - stage_index: int, - in_channels: list[int], - multiscale_output: bool = True, - dropout: float | None = None, - ) -> tuple[nn.Module, list[int]]: - """Create a stage of the LiteHRNet backbone. - - Args: - stages_spec (dict): Specification of the stages of the backbone. - stage_index (int): Index of the current stage. - in_channels (list[int]): List of input channels for each branch. - multiscale_output (bool, optional): Whether to output features from all branches. Defaults to True. - dropout (float | None, optional): Dropout probability. Defaults to None. - - Returns: - tuple[nn.Module, list[int]]: A tuple containing the stage module and the output channels for each branch. - """ - num_modules = stages_spec["num_modules"][stage_index] - num_branches = stages_spec["num_branches"][stage_index] - num_blocks = stages_spec["num_blocks"][stage_index] - reduce_ratio = stages_spec["reduce_ratios"][stage_index] - with_fuse = stages_spec["with_fuse"][stage_index] - module_type = stages_spec["module_type"][stage_index] - weighting_module_version = stages_spec.get("weighting_module_version", "v1") - neighbour_weighting = stages_spec.get("neighbour_weighting", False) - - modules = [] - for i in range(num_modules): - # multi_scale_output is only used last module - reset_multiscale_output = not ((not multiscale_output) and i == num_modules - 1) - - modules.append( - LiteHRModule( - num_branches, - num_blocks, - in_channels, - reduce_ratio, - module_type, - multiscale_output=reset_multiscale_output, - with_fuse=with_fuse, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - with_cp=self.with_cp, - dropout=dropout, - weighting_module_version=weighting_module_version, - neighbour_weighting=neighbour_weighting, - ), - ) - in_channels = modules[-1].in_channels - - return nn.Sequential(*modules), in_channels - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Forward function.""" - stem_outputs = self.stem(x) - y_x2 = y_x4 = stem_outputs - y = y_x4 - - if self.enable_stem_pool: - y = self.stem_pool(y) - - y_list = [y] - for i in range(self.num_stages): - transition_modules = getattr(self, f"transition{i}") - - stage_inputs = [] - for j in range(self.stages_spec["num_branches"][i]): - if transition_modules[j]: - if j >= len(y_list): - stage_inputs.append(transition_modules[j](y_list[-1])) - else: - stage_inputs.append(transition_modules[j](y_list[j])) - else: - stage_inputs.append(y_list[j]) - - stage_module = getattr(self, f"stage{i}") - y_list = stage_module(stage_inputs) - - if self.out_modules is not None: - y_list.append(self.out_modules(y_list[-1])) - - if self.add_stem_features: - y_stem = self.stem_transition(y_x2) - y_list = [y_stem, *y_list] - - out = y_list - if self.with_aggregator: - out = self.aggregator(out) - - if self.extra.get("add_input", False): - out = [x, *out] - - return out diff --git a/src/otx/algo/segmentation/dino_v2_seg.py b/src/otx/algo/segmentation/dino_v2_seg.py deleted file mode 100644 index 4886616e4d9..00000000000 --- a/src/otx/algo/segmentation/dino_v2_seg.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""DinoV2Seg model implementations.""" -from __future__ import annotations - -from typing import Any - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.core.model.entity.segmentation import MMSegCompatibleModel - - -class DinoV2Seg(MMSegCompatibleModel): - """DinoV2Seg Model.""" - - def __init__(self, num_classes: int) -> None: - model_name = "dino_v2_seg" - config = read_mmconfig(model_name=model_name) - super().__init__(num_classes=num_classes, config=config) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parent_parameters = super()._export_parameters - parent_parameters["input_size"] = (1, 3, 560, 560) - return parent_parameters - - @property - def _optimization_config(self) -> dict[str, Any]: - """PTQ config for DinoV2Seg.""" - return {"model_type": "transformer"} diff --git a/src/otx/algo/segmentation/heads/__init__.py b/src/otx/algo/segmentation/heads/__init__.py deleted file mode 100644 index 19aa68e95cc..00000000000 --- a/src/otx/algo/segmentation/heads/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Head modules for OTX segmentation model.""" - -from .custom_fcn_head import CustomFCNHead -from .custom_ham_head import CustomLightHamHead - -__all__ = ["CustomFCNHead", "CustomLightHamHead"] diff --git a/src/otx/algo/segmentation/heads/custom_fcn_head.py b/src/otx/algo/segmentation/heads/custom_fcn_head.py deleted file mode 100644 index 8326916481b..00000000000 --- a/src/otx/algo/segmentation/heads/custom_fcn_head.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom FCNHead modules for OTX segmentation model.""" - -from __future__ import annotations - -import typing -from typing import TYPE_CHECKING - -import torch -from mmseg.models.decode_heads.fcn_head import FCNHead -from mmseg.models.losses import accuracy -from mmseg.models.utils import resize -from mmseg.registry import MODELS -from torch import Tensor, nn - -from otx.algo.utils import IterativeAggregator - -if TYPE_CHECKING: - from mmseg.utils import SampleList - - -class ClassIncrementalMixin: - """Mixin for class incremental learning.""" - - @typing.no_type_check - def loss_by_feat( - self, - seg_logits: Tensor, - batch_data_samples: SampleList, - ) -> dict: - """Compute segmentation loss. - - Args: - seg_logits (Tensor): The output from decode head forward function. - batch_data_samples (List[:obj:`SegDataSample`]): The seg - data samples. It usually includes information such - as `metainfo` and `gt_sem_seg`. - - Returns: - dict[str, Tensor]: a dictionary of loss components - """ - img_metas = [data_sample.metainfo for data_sample in batch_data_samples] - valid_label_mask = self.get_valid_label_mask(img_metas) - seg_label = self._stack_batch_gt(batch_data_samples) - loss = {} - seg_logits = resize( - input=seg_logits, - size=seg_label.shape[2:], - mode="bilinear", - align_corners=self.align_corners, - ) - seg_weight = self.sampler.sample(seg_logits, seg_label) if self.sampler is not None else None - seg_label = seg_label.squeeze(1) - - losses_decode = [self.loss_decode] if not isinstance(self.loss_decode, nn.ModuleList) else self.loss_decode - for loss_decode in losses_decode: - valid_label_mask_cfg = {} - if loss_decode.loss_name == "loss_ce_ignore": - valid_label_mask_cfg["valid_label_mask"] = valid_label_mask - if loss_decode.loss_name not in loss: - loss[loss_decode.loss_name] = loss_decode( - seg_logits, - seg_label, - weight=seg_weight, - ignore_index=self.ignore_index, - **valid_label_mask_cfg, - ) - else: - loss[loss_decode.loss_name] += loss_decode( - seg_logits, - seg_label, - weight=seg_weight, - ignore_index=self.ignore_index, - valid_label_mask=valid_label_mask, - **valid_label_mask_cfg, - ) - - loss["acc_seg"] = accuracy(seg_logits, seg_label, ignore_index=self.ignore_index) - return loss - - @typing.no_type_check - def get_valid_label_mask(self, img_metas: list[dict]) -> list[torch.Tensor]: - """Get valid label mask removing ignored classes to zero mask in a batch.""" - valid_label_mask = [] - for meta in img_metas: - mask = torch.Tensor([1 for _ in range(self.num_classes)]) - if "ignored_labels" in meta and meta["ignored_labels"]: - mask[meta["ignored_labels"]] = 0 - valid_label_mask.append(mask) - return valid_label_mask - - -@MODELS.register_module() -class CustomFCNHead(ClassIncrementalMixin, FCNHead): - """Custom FCNHead implementation. - - Custom FCNHead supports ignored label for class incremental learning cases. - """ - - def __init__( - self, - enable_aggregator: bool = False, - aggregator_min_channels: int = 0, - aggregator_merge_norm: str | None = None, - aggregator_use_concat: bool = False, - in_channels: list[int] | int | None = None, - in_index: list[int] | int | None = None, - norm_cfg: dict | None = None, - conv_cfg: dict | None = None, - input_transform: list | None = None, - *args, - **kwargs, - ): - if enable_aggregator: # Lite-HRNet aggregator - if in_channels is None or isinstance(in_channels, int): - msg = "'in_channels' should be List[int]." - raise ValueError(msg) - aggregator = IterativeAggregator( - in_channels=in_channels, - min_channels=aggregator_min_channels, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - merge_norm=aggregator_merge_norm, - use_concat=aggregator_use_concat, - ) - - aggregator_min_channels = aggregator_min_channels if aggregator_min_channels is not None else 0 - # change arguments temporarily - in_channels = max(in_channels[0], aggregator_min_channels) - input_transform = None - if isinstance(in_index, list): - in_index = in_index[0] - else: - aggregator = None - - super().__init__( - *args, - in_index=in_index, - norm_cfg=norm_cfg, - conv_cfg=conv_cfg, - input_transform=input_transform, - in_channels=in_channels, - **kwargs, - ) - - self.aggregator = aggregator - # re-define variables - self.in_channels = in_channels - self.input_transform = input_transform - self.in_index = in_index - - if self.act_cfg: - self.convs[-1].with_activation = False - delattr(self.convs[-1], "activate") - - def _transform_inputs(self, inputs: list[Tensor]) -> Tensor | list: - """Transform inputs for decoder. - - Args: - inputs (list[Tensor]): List of multi-level img features. - - Returns: - Tensor: The transformed inputs - """ - return self.aggregator(inputs)[0] if self.aggregator is not None else super()._transform_inputs(inputs) diff --git a/src/otx/algo/segmentation/heads/custom_ham_head.py b/src/otx/algo/segmentation/heads/custom_ham_head.py deleted file mode 100644 index cbcb4018efa..00000000000 --- a/src/otx/algo/segmentation/heads/custom_ham_head.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Implementation of HamburgerNet head.""" - -import torch -import torch.nn.functional as f -from mmseg.models.decode_heads.ham_head import NMF2D, LightHamHead -from mmseg.registry import MODELS - -from .custom_fcn_head import ClassIncrementalMixin - - -class CustomNMF2D(NMF2D): - """Non-negative Matrix Factorization (NMF) module. - - It is modified version from mmsegmentation to avoid randomness in inference. - """ - - def __init__(self, ham_channels: int = 512, **kwargs): - super().__init__(kwargs) - bases = f.normalize(torch.rand((self.S, ham_channels // self.S, self.R))) - self.bases = torch.nn.parameter.Parameter(bases, requires_grad=False) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Forward Function.""" - batch, channels, height, width = x.shape - - # (B, C, H, W) -> (B * S, D, N) - scale = channels // self.S - x = x.view(batch * self.S, scale, height * width) - - # (S, D, R) -> (B * S, D, R) - if self.training: - bases = self._build_bases(batch, self.S, scale, self.R, device=x.device) - else: - bases = self.bases.repeat(batch, 1, 1) - - bases, coef = self.local_inference(x, bases) - - # (B * S, N, R) - coef = self.compute_coef(x, bases, coef) - - # (B * S, D, R) @ (B * S, N, R)^T -> (B * S, D, N) - x = torch.bmm(bases, coef.transpose(1, 2)) - - # (B * S, D, N) -> (B, C, H, W) - return x.view(batch, channels, height, width) - - -@MODELS.register_module() -class CustomLightHamHead(ClassIncrementalMixin, LightHamHead): - """SegNeXt decode head. - - It is modified LightHamHead from mmsegmentation. - - Args: - ham_channels (int): input channels for Hamburger. - ham_kwargs (dict): kwagrs for Ham. - """ - - def __init__(self, ham_channels: int, ham_kwargs: dict, **kwargs): - super().__init__(ham_channels=ham_channels, ham_kwargs=ham_kwargs, **kwargs) - - self.hamburger.ham = CustomNMF2D(ham_channels=ham_channels, **ham_kwargs) diff --git a/src/otx/algo/segmentation/litehrnet.py b/src/otx/algo/segmentation/litehrnet.py deleted file mode 100644 index eb29257060e..00000000000 --- a/src/otx/algo/segmentation/litehrnet.py +++ /dev/null @@ -1,385 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""LiteHRNet model implementations.""" - -from __future__ import annotations - -from typing import Any, Literal - -from torch.onnx import OperatorExportTypes - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.segmentation import MMSegCompatibleModel - - -class LiteHRNet(MMSegCompatibleModel): - """LiteHRNet Model.""" - - def __init__(self, num_classes: int, variant: Literal["18", 18, "s", "x"]) -> None: - self.model_name = f"litehrnet_{variant}" - config = read_mmconfig(model_name=self.model_name) - super().__init__(num_classes=num_classes, config=config) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parent_parameters = super()._export_parameters - parent_parameters.update( - { - "onnx_export_configuration": {"operator_export_type": OperatorExportTypes.ONNX_ATEN_FALLBACK}, - "via_onnx": True, - }, - ) - - return parent_parameters - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_seg_lite_hrnet_ckpt(state_dict, add_prefix) - - @property - def _optimization_config(self) -> dict[str, Any]: - """PTQ config for LiteHRNet.""" - # TODO(Kirill): check PTQ without adding the whole backbone to ignored_scope #noqa: TD003 - ignored_scope = self._obtain_ignored_scope() - optim_config = { - "advanced_parameters": { - "activations_range_estimator_params": { - "min": {"statistics_type": "QUANTILE", "aggregator_type": "MIN", "quantile_outlier_prob": 1e-4}, - "max": {"statistics_type": "QUANTILE", "aggregator_type": "MAX", "quantile_outlier_prob": 1e-4}, - }, - }, - } - optim_config.update(ignored_scope) - return optim_config - - def _obtain_ignored_scope(self) -> dict[str, Any]: - """Returns the ignored scope for the model based on the litehrnet version.""" - if self.model_name == "litehrnet_18": - ignored_scope_names = [ - "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.0/Add_1", - "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.1/Add_1", - "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.0/Add_1", - "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.0/Add_2", - "/backbone/stage1/stage1.0/Add_5", - "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.1/Add_1", - "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.1/Add_2", - "/backbone/stage1/stage1.1/Add_5", - "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.2/Add_1", - "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.2/Add_2", - "/backbone/stage1/stage1.2/Add_5", - "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.3/Add_1", - "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.3/Add_2", - "/backbone/stage1/stage1.3/Add_5", - "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.0/Add_1", - "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.0/Add_2", - "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.0/Add_3", - "/backbone/stage2/stage2.0/Add_6", - "/backbone/stage2/stage2.0/Add_7", - "/backbone/stage2/stage2.0/Add_11", - "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.1/Add_1", - "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.1/Add_2", - "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.1/Add_3", - "/backbone/stage2/stage2.1/Add_6", - "/backbone/stage2/stage2.1/Add_7", - "/backbone/stage2/stage2.1/Add_11", - "/aggregator/Add", - "/aggregator/Add_1", - "/aggregator/Add_2", - "/backbone/stage2/stage2.1/Add", - ] - - return { - "ignored_scope": { - "patterns": ["/backbone/*"], - "names": ignored_scope_names, - }, - "preset": "mixed", - } - - if self.model_name == "litehrnet_s": - ignored_scope_names = [ - "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.0/Add_1", - "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.1/Add_1", - "/backbone/stage0/stage0.2/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.2/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.2/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.2/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.2/Add_1", - "/backbone/stage0/stage0.3/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.3/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.3/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.3/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.3/Add_1", - "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.0/Add_1", - "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.0/Add_2", - "/backbone/stage1/stage1.0/Add_5", - "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.1/Add_1", - "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.1/Add_2", - "/backbone/stage1/stage1.1/Add_5", - "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.2/Add_1", - "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.2/Add_2", - "/backbone/stage1/stage1.2/Add_5", - "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.3/Add_1", - "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.3/Add_2", - "/backbone/stage1/stage1.3/Add_5", - "/aggregator/Add", - "/aggregator/Add_1", - ] - - return { - "ignored_scope": { - "names": ignored_scope_names, - }, - "preset": "mixed", - } - - if self.model_name == "litehrnet_x": - ignored_scope_names = [ - "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.0/Add_1", - "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage0/stage0.1/Add_1", - "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.0/Add_1", - "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.0/Add_2", - "/backbone/stage1/stage1.0/Add_5", - "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.1/Add_1", - "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.1/Add_2", - "/backbone/stage1/stage1.1/Add_5", - "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.2/Add_1", - "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.2/Add_2", - "/backbone/stage1/stage1.2/Add_5", - "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage1/stage1.3/Add_1", - "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage1/stage1.3/Add_2", - "/backbone/stage1/stage1.3/Add_5", - "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.0/Add_1", - "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.0/Add_2", - "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.0/Add_3", - "/backbone/stage2/stage2.0/Add_6", - "/backbone/stage2/stage2.0/Add_7", - "/backbone/stage2/stage2.0/Add_11", - "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.1/Add_1", - "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.1/Add_2", - "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.1/Add_3", - "/backbone/stage2/stage2.1/Add_6", - "/backbone/stage2/stage2.1/Add_7", - "/backbone/stage2/stage2.1/Add_11", - "/backbone/stage2/stage2.2/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.2/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.2/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.2/layers/layers.0/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.2/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.2/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.2/Add_1", - "/backbone/stage2/stage2.2/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.2/Add_2", - "/backbone/stage2/stage2.2/layers/layers.1/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.2/Add_3", - "/backbone/stage2/stage2.2/Add_6", - "/backbone/stage2/stage2.2/Add_7", - "/backbone/stage2/stage2.2/Add_11", - "/backbone/stage2/stage2.3/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.3/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.3/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.3/layers/layers.0/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.3/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage2/stage2.3/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage2/stage2.3/Add_1", - "/backbone/stage2/stage2.3/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage2/stage2.3/Add_2", - "/backbone/stage2/stage2.3/layers/layers.1/cross_resolution_weighting/Mul_3", - "/backbone/stage2/stage2.3/Add_3", - "/backbone/stage2/stage2.3/Add_6", - "/backbone/stage2/stage2.3/Add_7", - "/backbone/stage2/stage2.3/Add_11", - "/backbone/stage3/stage3.0/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage3/stage3.0/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage3/stage3.0/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage3/stage3.0/layers/layers.0/cross_resolution_weighting/Mul_3", - "/backbone/stage3/stage3.0/layers/layers.0/cross_resolution_weighting/Mul_4", - "/backbone/stage3/stage3.0/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage3/stage3.0/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage3/stage3.0/Add_1", - "/backbone/stage3/stage3.0/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage3/stage3.0/Add_2", - "/backbone/stage3/stage3.0/layers/layers.1/cross_resolution_weighting/Mul_3", - "/backbone/stage3/stage3.0/Add_3", - "/backbone/stage3/stage3.0/layers/layers.1/cross_resolution_weighting/Mul_4", - "/backbone/stage3/stage3.0/Add_4", - "/backbone/stage3/stage3.0/Add_7", - "/backbone/stage3/stage3.0/Add_8", - "/backbone/stage3/stage3.0/Add_9", - "/backbone/stage3/stage3.0/Add_13", - "/backbone/stage3/stage3.0/Add_14", - "/backbone/stage3/stage3.0/Add_19", - "/backbone/stage3/stage3.1/layers/layers.0/cross_resolution_weighting/Mul", - "/backbone/stage3/stage3.1/layers/layers.0/cross_resolution_weighting/Mul_1", - "/backbone/stage3/stage3.1/layers/layers.0/cross_resolution_weighting/Mul_2", - "/backbone/stage3/stage3.1/layers/layers.0/cross_resolution_weighting/Mul_3", - "/backbone/stage3/stage3.1/layers/layers.0/cross_resolution_weighting/Mul_4", - "/backbone/stage3/stage3.1/layers/layers.1/cross_resolution_weighting/Mul", - "/backbone/stage3/stage3.1/layers/layers.1/cross_resolution_weighting/Mul_1", - "/backbone/stage3/stage3.1/Add_1", - "/backbone/stage3/stage3.1/layers/layers.1/cross_resolution_weighting/Mul_2", - "/backbone/stage3/stage3.1/Add_2", - "/backbone/stage3/stage3.1/layers/layers.1/cross_resolution_weighting/Mul_3", - "/backbone/stage3/stage3.1/Add_3", - "/backbone/stage3/stage3.1/layers/layers.1/cross_resolution_weighting/Mul_4", - "/backbone/stage3/stage3.1/Add_4", - "/backbone/stage3/stage3.1/Add_7", - "/backbone/stage3/stage3.1/Add_8", - "/backbone/stage3/stage3.1/Add_9", - "/backbone/stage3/stage3.1/Add_13", - "/backbone/stage3/stage3.1/Add_14", - "/backbone/stage3/stage3.1/Add_19", - "/backbone/stage0/stage0.0/Add", - "/backbone/stage0/stage0.1/Add", - "/backbone/stage1/stage1.0/Add", - "/backbone/stage1/stage1.1/Add", - "/backbone/stage1/stage1.2/Add", - "/backbone/stage1/stage1.3/Add", - "/backbone/stage2/stage2.0/Add", - "/backbone/stage2/stage2.1/Add", - "/backbone/stage2/stage2.2/Add", - "/backbone/stage2/stage2.3/Add", - "/backbone/stage3/stage3.0/Add", - "/backbone/stage3/stage3.1/Add", - ] - - return { - "ignored_scope": { - "patterns": ["/aggregator/*"], - "names": ignored_scope_names, - }, - "preset": "performance", - } - - return {} diff --git a/src/otx/algo/segmentation/losses/__init__.py b/src/otx/algo/segmentation/losses/__init__.py deleted file mode 100644 index 801a5863ac3..00000000000 --- a/src/otx/algo/segmentation/losses/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Custom Losses for OTX segmentation model.""" - -from .cross_entropy_loss_with_ignore import CrossEntropyLossWithIgnore - -__all__ = ["CrossEntropyLossWithIgnore"] diff --git a/src/otx/algo/segmentation/losses/cross_entropy_loss_with_ignore.py b/src/otx/algo/segmentation/losses/cross_entropy_loss_with_ignore.py deleted file mode 100644 index f0792f5ec32..00000000000 --- a/src/otx/algo/segmentation/losses/cross_entropy_loss_with_ignore.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Cross entropy loss for ignored mode in class-incremental learning.""" - -from __future__ import annotations - -import torch -import torch.nn.functional as F # noqa: N812 -from mmseg.models.losses import CrossEntropyLoss -from mmseg.models.losses.utils import weight_reduce_loss -from mmseg.registry import MODELS - - -@MODELS.register_module() -class CrossEntropyLossWithIgnore(CrossEntropyLoss): - """CrossEntropyLossWithIgnore with Ignore Mode Support for Class Incremental Learning. - - When new classes are added through continual training cycles, images from previous cycles - may become partially annotated if they are not revisited. - To prevent the model from predicting these new classes for such images, - CrossEntropyLossWithIgnore can be used to ignore the unseen classes. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._loss_name = "loss_ce_ignore" - - def forward( - self, - cls_score: torch.Tensor, - label: torch.Tensor, - weight: torch.Tensor | None = None, - avg_factor: int | None = None, - reduction_override: str = "mean", - ignore_index: int = 255, - valid_label_mask: torch.Tensor | None = None, - **kwargs, - ) -> torch.Tensor: - """Forward. - - Args: - cls_score (torch.Tensor, optional): The prediction with shape (N, 1). - label (torch.Tensor, optional): The learning label of the prediction. - weight (torch.Tensor, optional): Sample-wise loss weight. - Default: None. - class_weight (list[float], optional): The weight for each class. - Default: None. - avg_factor (int, optional): Average factor that is used to average - the loss. Default: None. - reduction_override (str, optional): The method used to reduce the loss. - Options are 'none', 'mean' and 'sum'. Default: 'mean'. - ignore_index (int): Specifies a target value that is ignored and - does not contribute to the input gradients. When - ``avg_non_ignore `` is ``True``, and the ``reduction`` is - ``''mean''``, the loss is averaged over non-ignored targets. - Defaults: 255. - valid_label_mask (torch.Tensor, optional): The valid labels with - shape (N, num_classes). - If the value in the valid_label_mask is 0, mask label of the - the mask label of the class corresponding to its index will be - ignored like ignore_index. - **kwargs (Any): Additional keyword arguments. - """ - if valid_label_mask is None: - return super().forward(cls_score, label, weight, avg_factor, reduction_override, ignore_index, **kwargs) - reduction = reduction_override if reduction_override else self.reduction - batch_size = label.shape[0] - for i in range(batch_size): - invalid_labels = (valid_label_mask[i] == 0).nonzero(as_tuple=False) - - for inv_l in invalid_labels: - cls_score = torch.cat((cls_score[:, :inv_l], cls_score[:, inv_l + 1 :]), dim=1) - - losses = F.cross_entropy(cls_score, label, reduction="none", ignore_index=ignore_index) - - if weight is not None: - weight = weight.float() - return weight_reduce_loss(losses, weight=weight, reduction=reduction, avg_factor=avg_factor) - - @property - def loss_name(self) -> str: - """Loss Name. - - This function must be implemented and will return the name of this - loss function. This name will be used to combine different loss items - by simple sum operation. In addition, if you want this loss item to be - included into the backward graph, `loss_` must be the prefix of the - name. - - Returns: - str: The name of this loss item. - """ - return self._loss_name diff --git a/src/otx/algo/segmentation/mmconfigs/dino_v2_seg.yaml b/src/otx/algo/segmentation/mmconfigs/dino_v2_seg.yaml deleted file mode 100644 index a90f5a39372..00000000000 --- a/src/otx/algo/segmentation/mmconfigs/dino_v2_seg.yaml +++ /dev/null @@ -1,63 +0,0 @@ -backbone: - name: dinov2_vits14_reg - freeze_backbone: true - out_index: - - 8 - - 9 - - 10 - - 11 - type: DinoVisionTransformer -data_preprocessor: - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_val: 0 - seg_pad_val: 255 - size: - - 560 - - 560 - std: - - 58.395 - - 57.12 - - 57.375 - test_cfg: - size_divisor: 14 - type: SegDataPreProcessor -decode_head: - init_cfg: - checkpoint: https://dl.fbaipublicfiles.com/dinov2/dinov2_vits14/dinov2_vits14_ade20k_linear_head.pth - type: Pretrained - prefix: decode_head - type: CustomFCNHead - in_channels: - - 384 - - 384 - - 384 - - 384 - in_index: - - 0 - - 1 - - 2 - - 3 - input_transform: resize_concat - channels: 1536 - kernel_size: 1 - num_convs: 1 - concat_input: false - dropout_ratio: -1 - num_classes: 2 - norm_cfg: - type: SyncBN - requires_grad: true - align_corners: false - loss_decode: - type: CrossEntropyLossWithIgnore - use_sigmoid: false - loss_weight: 1.0 -pretrained: null -test_cfg: - mode: whole -train_cfg: {} -type: EncoderDecoder diff --git a/src/otx/algo/segmentation/mmconfigs/litehrnet_18.yaml b/src/otx/algo/segmentation/mmconfigs/litehrnet_18.yaml deleted file mode 100644 index af8aacf5b4c..00000000000 --- a/src/otx/algo/segmentation/mmconfigs/litehrnet_18.yaml +++ /dev/null @@ -1,122 +0,0 @@ -load_from: "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/models/custom_semantic_segmentation/litehrnet18_imagenet1k_rsc.pth" -backbone: - type: LiteHRNet - norm_cfg: - type: BN - requires_grad: true - norm_eval: false - extra: - stem: - stem_channels: 32 - out_channels: 32 - expand_ratio: 1 - strides: - - 2 - - 2 - extra_stride: false - input_norm: false - num_stages: 3 - stages_spec: - num_modules: - - 2 - - 4 - - 2 - num_branches: - - 2 - - 3 - - 4 - num_blocks: - - 2 - - 2 - - 2 - module_type: - - LITE - - LITE - - LITE - with_fuse: - - true - - true - - true - reduce_ratios: - - 8 - - 8 - - 8 - num_channels: - - - 40 - - 80 - - - 40 - - 80 - - 160 - - - 40 - - 80 - - 160 - - 320 - out_modules: - conv: - enable: false - channels: 320 - position_att: - enable: false - key_channels: 128 - value_channels: 320 - psp_size: - - 1 - - 3 - - 6 - - 8 - local_att: - enable: false - out_aggregator: - enable: false - add_input: false -data_preprocessor: - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_val: 0 - seg_pad_val: 255 - size: - - 512 - - 512 - std: - - 58.395 - - 57.12 - - 57.375 - test_cfg: - size_divisor: 32 - type: SegDataPreProcessor -decode_head: - type: CustomFCNHead - in_channels: - - 40 - - 80 - - 160 - - 320 - in_index: - - 0 - - 1 - - 2 - - 3 - input_transform: "multiple_select" - channels: 40 - enable_aggregator: True - kernel_size: 1 - num_convs: 1 - concat_input: false - dropout_ratio: -1 - num_classes: 2 - norm_cfg: - type: BN - requires_grad: true - align_corners: false - loss_decode: - type: CrossEntropyLossWithIgnore - use_sigmoid: false - loss_weight: 1.0 -pretrained: null -test_cfg: - mode: whole -train_cfg: {} -type: EncoderDecoder diff --git a/src/otx/algo/segmentation/mmconfigs/litehrnet_s.yaml b/src/otx/algo/segmentation/mmconfigs/litehrnet_s.yaml deleted file mode 100644 index 8feb1820e04..00000000000 --- a/src/otx/algo/segmentation/mmconfigs/litehrnet_s.yaml +++ /dev/null @@ -1,112 +0,0 @@ -load_from: "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/models/custom_semantic_segmentation/litehrnetsv2_imagenet1k_rsc.pth" -backbone: - type: LiteHRNet - norm_cfg: - type: BN - requires_grad: true - norm_eval: false - extra: - stem: - stem_channels: 32 - out_channels: 32 - expand_ratio: 1 - strides: - - 2 - - 2 - extra_stride: true - input_norm: false - num_stages: 2 - stages_spec: - num_modules: - - 4 - - 4 - num_branches: - - 2 - - 3 - num_blocks: - - 2 - - 2 - module_type: - - LITE - - LITE - with_fuse: - - true - - true - reduce_ratios: - - 8 - - 8 - num_channels: - - - 60 - - 120 - - - 60 - - 120 - - 240 - out_modules: - conv: - enable: false - channels: 160 - position_att: - enable: false - key_channels: 64 - value_channels: 240 - psp_size: - - 1 - - 3 - - 6 - - 8 - local_att: - enable: false - out_aggregator: - enable: false - add_input: false -data_preprocessor: - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_val: 0 - seg_pad_val: 255 - size: - - 512 - - 512 - std: - - 58.395 - - 57.12 - - 57.375 - test_cfg: - size_divisor: 32 - type: SegDataPreProcessor -decode_head: - type: CustomFCNHead - in_channels: - - 60 - - 120 - - 240 - in_index: - - 0 - - 1 - - 2 - input_transform: "multiple_select" - channels: 60 - kernel_size: 1 - num_convs: 1 - concat_input: false - enable_aggregator: True - aggregator_merge_norm: None - aggregator_use_concat: False - dropout_ratio: -1 - num_classes: 2 - norm_cfg: - type: BN - requires_grad: true - align_corners: false - loss_decode: - type: CrossEntropyLossWithIgnore - use_sigmoid: false - loss_weight: 1.0 -pretrained: null -test_cfg: - mode: whole -train_cfg: {} -type: EncoderDecoder diff --git a/src/otx/algo/segmentation/mmconfigs/litehrnet_x.yaml b/src/otx/algo/segmentation/mmconfigs/litehrnet_x.yaml deleted file mode 100644 index 805e2e96ab8..00000000000 --- a/src/otx/algo/segmentation/mmconfigs/litehrnet_x.yaml +++ /dev/null @@ -1,139 +0,0 @@ -load_from: "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/models/custom_semantic_segmentation/litehrnetxv3_imagenet1k_rsc.pth" -backbone: - type: LiteHRNet - norm_cfg: - type: BN - requires_grad: true - norm_eval: false - extra: - stem: - stem_channels: 60 - out_channels: 60 - expand_ratio: 1 - strides: - - 2 - - 1 - extra_stride: false - input_norm: false - num_stages: 4 - stages_spec: - weighting_module_version: v1 - num_modules: - - 2 - - 4 - - 4 - - 2 - num_branches: - - 2 - - 3 - - 4 - - 5 - num_blocks: - - 2 - - 2 - - 2 - - 2 - module_type: - - LITE - - LITE - - LITE - - LITE - with_fuse: - - true - - true - - true - - true - reduce_ratios: - - 2 - - 4 - - 8 - - 8 - num_channels: - - - 18 - - 60 - - - 18 - - 60 - - 80 - - - 18 - - 60 - - 80 - - 160 - - - 18 - - 60 - - 80 - - 160 - - 320 - out_modules: - conv: - enable: false - channels: 320 - position_att: - enable: false - key_channels: 128 - value_channels: 320 - psp_size: - - 1 - - 3 - - 6 - - 8 - local_att: - enable: false - out_aggregator: - enable: false - add_input: false -data_preprocessor: - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_val: 0 - seg_pad_val: 255 - size: - - 512 - - 512 - std: - - 58.395 - - 57.12 - - 57.375 - test_cfg: - size_divisor: 32 - type: SegDataPreProcessor -decode_head: - type: CustomFCNHead - in_channels: - - 18 - - 60 - - 80 - - 160 - - 320 - in_index: - - 0 - - 1 - - 2 - - 3 - - 4 - input_transform: "multiple_select" - channels: 60 - kernel_size: 1 - num_convs: 1 - concat_input: false - dropout_ratio: -1 - num_classes: 2 - enable_aggregator: True - aggregator_min_channels: 60 - aggregator_merge_norm: None - aggregator_use_concat: False - norm_cfg: - type: BN - requires_grad: true - align_corners: false - loss_decode: - type: CrossEntropyLossWithIgnore - use_sigmoid: false - loss_weight: 1.0 -pretrained: null -test_cfg: - mode: whole -train_cfg: {} -type: EncoderDecoder diff --git a/src/otx/algo/segmentation/mmconfigs/segnext_b.yaml b/src/otx/algo/segmentation/mmconfigs/segnext_b.yaml deleted file mode 100644 index 35325dfe5cc..00000000000 --- a/src/otx/algo/segmentation/mmconfigs/segnext_b.yaml +++ /dev/null @@ -1,96 +0,0 @@ -backbone: - init_cfg: - checkpoint: https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_b_20230227-3ab7d230.pth - type: Pretrained - act_cfg: - type: GELU - attention_kernel_paddings: - - 2 - - - 0 - - 3 - - - 0 - - 5 - - - 0 - - 10 - attention_kernel_sizes: - - 5 - - - 1 - - 7 - - - 1 - - 11 - - - 1 - - 21 - depths: - - 3 - - 3 - - 12 - - 3 - drop_path_rate: 0.1 - drop_rate: 0.0 - embed_dims: - - 64 - - 128 - - 320 - - 512 - mlp_ratios: - - 8 - - 8 - - 4 - - 4 - norm_cfg: - requires_grad: true - type: BN - type: MSCAN -data_preprocessor: - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_val: 0 - seg_pad_val: 255 - size: - - 512 - - 512 - std: - - 58.395 - - 57.12 - - 57.375 - test_cfg: - size_divisor: 32 - type: SegDataPreProcessor -decode_head: - align_corners: false - channels: 512 - dropout_ratio: 0.1 - ham_channels: 512 - ham_kwargs: - MD_R: 16 - MD_S: 1 - eval_steps: 7 - inv_t: 100 - rand_init: true - train_steps: 6 - in_channels: - - 128 - - 320 - - 512 - in_index: - - 1 - - 2 - - 3 - loss_decode: - loss_weight: 1.0 - type: CrossEntropyLossWithIgnore - use_sigmoid: false - norm_cfg: - num_groups: 32 - requires_grad: true - type: GN - num_classes: 150 - type: CustomLightHamHead -pretrained: null -test_cfg: - mode: whole -train_cfg: {} -type: EncoderDecoder diff --git a/src/otx/algo/segmentation/mmconfigs/segnext_s.yaml b/src/otx/algo/segmentation/mmconfigs/segnext_s.yaml deleted file mode 100644 index 0a5bcbfd87c..00000000000 --- a/src/otx/algo/segmentation/mmconfigs/segnext_s.yaml +++ /dev/null @@ -1,96 +0,0 @@ -backbone: - init_cfg: - checkpoint: https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_s_20230227-f33ccdf2.pth - type: Pretrained - act_cfg: - type: GELU - attention_kernel_paddings: - - 2 - - - 0 - - 3 - - - 0 - - 5 - - - 0 - - 10 - attention_kernel_sizes: - - 5 - - - 1 - - 7 - - - 1 - - 11 - - - 1 - - 21 - depths: - - 2 - - 2 - - 4 - - 2 - drop_path_rate: 0.1 - drop_rate: 0.0 - embed_dims: - - 64 - - 128 - - 320 - - 512 - mlp_ratios: - - 8 - - 8 - - 4 - - 4 - norm_cfg: - requires_grad: true - type: BN - type: MSCAN -data_preprocessor: - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_val: 0 - seg_pad_val: 255 - size: - - 512 - - 512 - std: - - 58.395 - - 57.12 - - 57.375 - test_cfg: - size_divisor: 32 - type: SegDataPreProcessor -decode_head: - align_corners: false - channels: 256 - dropout_ratio: 0.1 - ham_channels: 256 - ham_kwargs: - MD_R: 16 - MD_S: 1 - eval_steps: 7 - inv_t: 100 - rand_init: true - train_steps: 6 - in_channels: - - 128 - - 320 - - 512 - in_index: - - 1 - - 2 - - 3 - loss_decode: - loss_weight: 1.0 - type: CrossEntropyLossWithIgnore - use_sigmoid: false - norm_cfg: - num_groups: 32 - requires_grad: true - type: GN - num_classes: 4 - type: CustomLightHamHead -pretrained: null -test_cfg: - mode: whole -train_cfg: {} -type: EncoderDecoder diff --git a/src/otx/algo/segmentation/mmconfigs/segnext_t.yaml b/src/otx/algo/segmentation/mmconfigs/segnext_t.yaml deleted file mode 100644 index 56e8481286c..00000000000 --- a/src/otx/algo/segmentation/mmconfigs/segnext_t.yaml +++ /dev/null @@ -1,96 +0,0 @@ -backbone: - init_cfg: - checkpoint: https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_t_20230227-119e8c9f.pth - type: Pretrained - act_cfg: - type: GELU - attention_kernel_paddings: - - 2 - - - 0 - - 3 - - - 0 - - 5 - - - 0 - - 10 - attention_kernel_sizes: - - 5 - - - 1 - - 7 - - - 1 - - 11 - - - 1 - - 21 - depths: - - 3 - - 3 - - 5 - - 2 - drop_path_rate: 0.1 - drop_rate: 0.0 - embed_dims: - - 32 - - 64 - - 160 - - 256 - mlp_ratios: - - 8 - - 8 - - 4 - - 4 - norm_cfg: - requires_grad: true - type: BN - type: MSCAN -data_preprocessor: - bgr_to_rgb: false - mean: - - 123.675 - - 116.28 - - 103.53 - pad_val: 0 - seg_pad_val: 255 - size: - - 512 - - 512 - std: - - 58.395 - - 57.12 - - 57.375 - test_cfg: - size_divisor: 32 - type: SegDataPreProcessor -decode_head: - align_corners: false - channels: 256 - dropout_ratio: 0.1 - ham_channels: 256 - ham_kwargs: - MD_R: 16 - MD_S: 1 - eval_steps: 7 - inv_t: 100 - rand_init: true - train_steps: 6 - in_channels: - - 64 - - 160 - - 256 - in_index: - - 1 - - 2 - - 3 - loss_decode: - loss_weight: 1.0 - type: CrossEntropyLossWithIgnore - use_sigmoid: false - norm_cfg: - num_groups: 32 - requires_grad: true - type: GN - num_classes: 150 - type: CustomLightHamHead -pretrained: null -test_cfg: - mode: whole -train_cfg: {} -type: EncoderDecoder diff --git a/src/otx/algo/segmentation/segnext.py b/src/otx/algo/segmentation/segnext.py deleted file mode 100644 index ec0d0ea0fc1..00000000000 --- a/src/otx/algo/segmentation/segnext.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""SegNext model implementations.""" -from __future__ import annotations - -from typing import Any, Literal - -from otx.algo.utils.mmconfig import read_mmconfig -from otx.algo.utils.support_otx_v1 import OTXv1Helper -from otx.core.model.entity.segmentation import MMSegCompatibleModel - - -class SegNext(MMSegCompatibleModel): - """SegNext Model.""" - - def __init__(self, num_classes: int, variant: Literal["b", "s", "t"]) -> None: - model_name = f"segnext_{variant}" - config = read_mmconfig(model_name=model_name) - super().__init__(num_classes=num_classes, config=config) - - def load_from_otx_v1_ckpt(self, state_dict: dict, add_prefix: str = "model.model.") -> dict: - """Load the previous OTX ckpt according to OTX2.0.""" - return OTXv1Helper.load_seg_segnext_ckpt(state_dict, add_prefix) - - @property - def _optimization_config(self) -> dict[str, Any]: - """PTQ config for SegNext.""" - # TODO(Kirill): check PTQ removing hamburger from ignored_scope #noqa: TD003 - return { - "ignored_scope": { - "patterns": ["__module.decode_head.hamburger*"], - "types": [ - "Add", - "MVN", - "Divide", - "Multiply", - ], - }, - } diff --git a/src/otx/algo/utils/__init__.py b/src/otx/algo/utils/__init__.py deleted file mode 100644 index 4df593632da..00000000000 --- a/src/otx/algo/utils/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utils used for OTX models.""" - -from .segmentation import AsymmetricPositionAttentionModule, IterativeAggregator, LocalAttentionModule, channel_shuffle - -__all__ = [ - "IterativeAggregator", - "channel_shuffle", - "LocalAttentionModule", - "AsymmetricPositionAttentionModule", -] diff --git a/src/otx/algo/utils/mmconfig.py b/src/otx/algo/utils/mmconfig.py deleted file mode 100644 index 55dc5cda5a9..00000000000 --- a/src/otx/algo/utils/mmconfig.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utils used for MMConfigs.""" - -import inspect -from pathlib import Path - -from omegaconf import DictConfig, OmegaConf - - -def read_mmconfig(model_name: str, subdir_name: str = ".") -> DictConfig: - """Read MMConfig. - - It try to read MMConfig from the yaml file which exists in - `/mmconfigs//.yaml` - - For example, if this function is called in `otx/algo/action_classification/x3d.py`, - `otx/algo/action_classification/mmconfigs/x3d.yaml` will be read. - """ - frame = inspect.stack()[1] - module = inspect.getmodule(frame[0]) - - if module is None or (module_file_path := module.__file__) is None: - msg = ( - "Cannot get Cannot get a valid module from Python function stack. " - "Please refer to this function docstring to see how to use correctly." - ) - raise RuntimeError(msg) - - root_dir = Path(module_file_path).parent / "mmconfigs" / subdir_name - yaml_fpath = root_dir / f"{model_name}.yaml" - - if not yaml_fpath.exists(): - msg = f"mmconfig file for {model_name} is not found in {yaml_fpath}" - raise FileNotFoundError(msg) - - return OmegaConf.load(yaml_fpath) diff --git a/src/otx/algo/utils/segmentation.py b/src/otx/algo/utils/segmentation.py deleted file mode 100644 index af40ce316dd..00000000000 --- a/src/otx/algo/utils/segmentation.py +++ /dev/null @@ -1,397 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utils used for LiteHRNet model.""" - -from __future__ import annotations - -from typing import Callable, ClassVar - -import torch -import torch.nn.functional as f -from mmcv.cnn import ConvModule, DepthwiseSeparableConvModule -from torch import nn -from torch.nn import AdaptiveAvgPool2d, AdaptiveMaxPool2d - - -def channel_shuffle(x: torch.Tensor, groups: int) -> torch.Tensor: - """Channel Shuffle operation. - - This function enables cross-group information flow for multiple groups - convolution layers. - - Args: - x (Tensor): The input tensor. - groups (int): The number of groups to divide the input tensor in the channel dimension. - - Returns: - Tensor: The output tensor after channel shuffle operation. - """ - batch_size, num_channels, height, width = x.size() - channels_per_group = num_channels // groups - - x = x.view(batch_size, groups, channels_per_group, height, width) - x = torch.transpose(x, 1, 2).contiguous() - - return x.view(batch_size, -1, height, width) - - -class PSPModule(nn.Module): - """PSP module. - - Reference: https://github.com/MendelXu/ANN. - """ - - methods: ClassVar[dict[str, AdaptiveMaxPool2d | AdaptiveAvgPool2d]] = { - "max": AdaptiveMaxPool2d, - "avg": AdaptiveAvgPool2d, - } - - def __init__(self, sizes: tuple = (1, 3, 6, 8), method: str = "max"): - super().__init__() - - pool_block = self.methods[method] - - self.stages = nn.ModuleList([pool_block(output_size=(size, size)) for size in sizes]) - - def forward(self, feats: torch.Tensor) -> torch.Tensor: - """Forward.""" - batch_size, c, _, _ = feats.size() - - priors = [stage(feats).view(batch_size, c, -1) for stage in self.stages] - - return torch.cat(priors, -1) - - -class AsymmetricPositionAttentionModule(nn.Module): - """AsymmetricPositionAttentionModule. - - Reference: https://github.com/MendelXu/ANN. - """ - - def __init__( - self, - in_channels: int, - key_channels: int, - value_channels: int | None = None, - psp_size: tuple | None = None, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - ): - super().__init__() - - self.in_channels = in_channels - self.key_channels = key_channels - self.value_channels = value_channels if value_channels is not None else in_channels - self.conv_cfg = conv_cfg - if norm_cfg is None: - norm_cfg = {"type": "BN"} - if psp_size is None: - psp_size = (1, 3, 6, 8) - self.norm_cfg = norm_cfg - self.query_key = ConvModule( - in_channels=self.in_channels, - out_channels=self.key_channels, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - ) - self.key_psp = PSPModule(psp_size, method="max") - - self.value = ConvModule( - in_channels=self.in_channels, - out_channels=self.value_channels, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - ) - self.value_psp = PSPModule(psp_size, method="max") - - self.out_conv = ConvModule( - in_channels=self.value_channels, - out_channels=self.in_channels, - kernel_size=1, - stride=1, - padding=0, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg=None, - ) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Forward.""" - batch_size, _, _ = x.size(0), x.size(2), x.size(3) - - query_key = self.query_key(x) - - key = self.key_psp(query_key) - value = self.value_psp(self.value(x)).permute(0, 2, 1) - query = query_key.view(batch_size, self.key_channels, -1).permute(0, 2, 1) - - similarity_scores = torch.matmul(query, key) - similarity_scores = (self.key_channels**-0.5) * similarity_scores - similarity_scores = f.softmax(similarity_scores, dim=-1) - - y = torch.matmul(similarity_scores, value) - y = y.permute(0, 2, 1).contiguous() - y = y.view(batch_size, self.value_channels, *x.size()[2:]) - y = self.out_conv(y) - - return x + y - - -class OnnxLpNormalization(torch.autograd.Function): - """OnnxLpNormalization.""" - - @staticmethod - def forward( - ctx: Callable, - x: torch.Tensor, - axis: int = 0, - p: int = 2, - eps: float = 1e-12, - ) -> torch.Tensor: - """Forward.""" - del ctx, p # These args are not used. - denom = x.norm(2, axis, True).clamp_min(eps).expand_as(x) - return x / denom - - @staticmethod - def symbolic( - g: torch.Graph, - x: torch.Tensor, - axis: int = 0, - p: int = 2, - eps: float = 1e-12, - ) -> torch.Graph: - """Symbolic onnxLpNormalization.""" - del eps # These args are not used. - return g.op("LpNormalization", x, axis_i=int(axis), p_i=int(p)) - - -def normalize(x: torch.Tensor, dim: int, p: int = 2, eps: float = 1e-12) -> torch.Tensor: - """Normalize method.""" - if torch.onnx.is_in_onnx_export(): - return OnnxLpNormalization.apply(x, dim, p, eps) - return torch.nn.functional.normalize(x, dim=dim, p=p, eps=eps) - - -class IterativeAggregator(nn.Module): - """IterativeAggregator. - - Based on: https://github.com/HRNet/Lite-HRNet. - """ - - def __init__( - self, - in_channels: list[int], - min_channels: int | None = None, - conv_cfg: dict | None = None, - norm_cfg: dict | None = None, - merge_norm: str | None = None, - use_concat: bool = False, - ) -> None: - """IterativeAggregator for LiteHRNet. - - Args: - in_channels (list[int]): List of input channels for each branch. - min_channels (int | None): Minimum number of channels. Defaults to None. - conv_cfg (dict | None): Config for convolution layers. Defaults to None. - norm_cfg (dict | None): Config for normalization layers. Defaults to None. - merge_norm (str | None): Whether to merge normalization layers. Defaults to None. - use_concat (bool): Whether to use concatenation. Defaults to False. - - Returns: - None - """ - if norm_cfg is None: - norm_cfg = {"type": "BN"} - super().__init__() - - self.use_concat = use_concat - - num_branches = len(in_channels) - self.in_channels = in_channels[::-1] - - min_channels = min_channels if min_channels is not None else 0 - - projects, expanders = [], [] - fuse_layers: list[ConvModule] = [] - for i in range(num_branches): - if not self.use_concat or i == 0: - fuse_layers.append(None) - else: - out_channels = self.in_channels[i + 1] - fuse_layers.append( - ConvModule( - in_channels=2 * out_channels, - out_channels=out_channels, - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - ), - ) - - if i != num_branches - 1: - out_channels = max(self.in_channels[i + 1], min_channels) - else: - out_channels = max(self.in_channels[i], min_channels) - - projects.append( - DepthwiseSeparableConvModule( - in_channels=max(self.in_channels[i], min_channels), - out_channels=out_channels, - kernel_size=3, - stride=1, - padding=1, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - dw_act_cfg=None, - pw_act_cfg={"type": "ReLU"}, - ), - ) - - if self.in_channels[i] < min_channels: - expanders.append( - ConvModule( - in_channels=self.in_channels[i], - out_channels=min_channels, - kernel_size=1, - stride=1, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - act_cfg={"type": "ReLU"}, - ), - ) - else: - expanders.append(None) - - self.projects = nn.ModuleList(projects) - self.expanders = nn.ModuleList(expanders) - self.fuse_layers = nn.ModuleList(fuse_layers) - - self.merge_norm = merge_norm - - @staticmethod - def _norm(x: torch.Tensor, mode: str | None = None) -> torch.Tensor: - """Normalize.""" - if mode is None or mode == "none": - out = x - elif mode == "channel": - out = normalize(x, dim=1, p=2) - else: - _, c, h, w = x.size() - y = x.view(-1, c, h * w) - y = normalize(y, dim=2, p=2) - out = y.view(-1, c, h, w) - - return out - - def forward(self, x: torch.Tensor) -> list[torch.Tensor]: - """Perform forward pass through the network. - - Args: - - x (List[Tensor]): Input tensor. - - Returns: - - List[Tensor]: Output tensor list. - """ - x = x[::-1] - - y_list = [] - last_x = None - for i, s_in in enumerate(x): - s = s_in - if self.expanders[i] is not None: - s = self.expanders[i](s) - - if last_x is not None: - last_x = f.interpolate(last_x, size=s.size()[-2:], mode="bilinear", align_corners=True) - - norm_s = self._norm(s, self.merge_norm) - norm_x = self._norm(last_x, self.merge_norm) - - if self.use_concat: - concat_s = torch.cat([norm_s, norm_x], dim=1) - s = self.fuse_layers[i](concat_s) - else: - s = norm_s + norm_x - - s = self.projects[i](s) - last_x = s - - y_list.append(s) - - return y_list[::-1] - - -class LocalAttentionModule(nn.Module): - """LocalAttentionModule. - - Reference: https://github.com/lxtGH/GALD-DGCNet. - """ - - def __init__(self, num_channels: int, conv_cfg: dict | None = None, norm_cfg: dict | None = None): - if norm_cfg is None: - norm_cfg = {"type": "BN"} - super().__init__() - - self.num_channels = num_channels - self.conv_cfg = conv_cfg - self.norm_cfg = norm_cfg - - self.dwconv1 = ConvModule( - in_channels=self.num_channels, - out_channels=self.num_channels, - kernel_size=3, - stride=2, - padding=1, - groups=self.num_channels, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - ) - self.dwconv2 = ConvModule( - in_channels=self.num_channels, - out_channels=self.num_channels, - kernel_size=3, - stride=2, - padding=1, - groups=self.num_channels, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - ) - self.dwconv3 = ConvModule( - in_channels=self.num_channels, - out_channels=self.num_channels, - kernel_size=3, - stride=2, - padding=1, - groups=self.num_channels, - conv_cfg=self.conv_cfg, - norm_cfg=self.norm_cfg, - act_cfg={"type": "ReLU"}, - ) - self.sigmoid_spatial = nn.Sigmoid() - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Forward.""" - _, _, h, w = x.size() - - y = self.dwconv1(x) - y = self.dwconv2(y) - y = self.dwconv3(y) - y = f.interpolate(y, size=(h, w), mode="bilinear", align_corners=True) - mask = self.sigmoid_spatial(y) - - return x + x * mask diff --git a/src/otx/algo/utils/support_otx_v1.py b/src/otx/algo/utils/support_otx_v1.py deleted file mode 100644 index 60f8475054b..00000000000 --- a/src/otx/algo/utils/support_otx_v1.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility functions to guarantee the OTX1.x models.""" -from __future__ import annotations - - -class OTXv1Helper: - """Helper class to support the backward compatibility of OTX v1.""" - - @staticmethod - def load_common_ckpt(state_dict: dict, add_prefix: str = "") -> dict: - """Load the OTX1.x model checkpoints that don't need special handling.""" - state_dict = state_dict["model"]["state_dict"] - for key in list(state_dict.keys()): - val = state_dict.pop(key) - state_dict[add_prefix + key] = val - return state_dict - - @staticmethod - def load_cls_effnet_b0_ckpt(state_dict: dict, label_type: str, add_prefix: str = "") -> dict: - """Load the OTX1.x efficientnet b0 classification checkpoints.""" - state_dict = state_dict["model"]["state_dict"] - for key in list(state_dict.keys()): - val = state_dict.pop(key) - if key.startswith("features."): - new_key = "backbone." + key if "activ" not in key else key - elif key.startswith("output."): - new_key = key.replace("output", "head") - if label_type != "hlabel": - new_key = new_key.replace("asl", "fc") - val = val.t() - state_dict[add_prefix + new_key] = val - return state_dict - - @staticmethod - def load_cls_effnet_v2_ckpt(state_dict: dict, label_type: str, add_prefix: str = "") -> dict: - """Load the OTX1.x efficientnet v2 classification checkpoints.""" - state_dict = state_dict["model"]["state_dict"] - for key in list(state_dict.keys()): - val = state_dict.pop(key) - if key.startswith("model.classifier."): - new_key = key.replace("model.classifier", "head.fc") - if label_type != "hlabel": - val = val.t() - elif key.startswith("model"): - new_key = "backbone." + key - state_dict[add_prefix + new_key] = val - return state_dict - - @staticmethod - def load_cls_mobilenet_v3_ckpt(state_dict: dict, label_type: str, add_prefix: str = "") -> dict: - """Load the OTX1.x mobilenet v3 classification checkpoints.""" - state_dict = state_dict["model"]["state_dict"] - for key in list(state_dict.keys()): - val = state_dict.pop(key) - if key.startswith("classifier."): - if "4" in key: - new_key = "head." + key.replace("4", "3") - if label_type == "multilabel": - val = val.t() - else: - new_key = "head." + key - elif key.startswith("act"): - new_key = "head." + key - elif not key.startswith("backbone."): - new_key = "backbone." + key - state_dict[add_prefix + new_key] = val - return state_dict - - @staticmethod - def load_cls_deit_ckpt(state_dict: dict, add_prefix: str = "") -> dict: - """Load the OTX1.x deit-tiny classification checkpoints.""" - return OTXv1Helper.load_common_ckpt(state_dict, add_prefix) - - @staticmethod - def load_det_ckpt(state_dict: dict, add_prefix: str = "") -> dict: - """Load the OTX1.x detection model checkpoints.""" - state_dict = state_dict["model"]["state_dict"] - for key in list(state_dict.keys()): - val = state_dict.pop(key) - if not key.startswith("ema_"): - state_dict[add_prefix + key] = val - return state_dict - - @staticmethod - def load_ssd_ckpt(state_dict: dict, add_prefix: str = "") -> dict: - """Load OTX1.x SSD model checkpoints.""" - state_dict["model"]["state_dict"]["anchors"] = state_dict.pop("anchors", None) - return OTXv1Helper.load_det_ckpt(state_dict, add_prefix) - - @staticmethod - def load_iseg_ckpt(state_dict: dict, add_prefix: str = "") -> dict: - """Load the instance segmentation model checkpoints.""" - return OTXv1Helper.load_det_ckpt(state_dict, add_prefix) - - @staticmethod - def load_seg_segnext_ckpt(state_dict: dict, add_prefix: str = "") -> dict: - """Load the OTX1.x segnext segmentation checkpoints.""" - state_dict = state_dict["model"]["state_dict"] - for key in list(state_dict.keys()): - val = state_dict.pop(key) - if "ham.bases" not in key: - state_dict[add_prefix + key] = val - return state_dict - - @staticmethod - def load_seg_lite_hrnet_ckpt(state_dict: dict, add_prefix: str = "") -> dict: - """Load the OTX1.x lite hrnet segmentation checkpoints.""" - state_dict = state_dict["model"]["state_dict"] - for key in list(state_dict.keys()): - val = state_dict.pop(key) - state_dict[add_prefix + key] = val - return state_dict - - @staticmethod - def load_action_ckpt(state_dict: dict, add_prefix: str = "") -> dict: - """Load the OTX1.x action cls/det model checkpoints.""" - return OTXv1Helper.load_common_ckpt(state_dict, add_prefix) diff --git a/src/otx/algo/utils/xai_utils.py b/src/otx/algo/utils/xai_utils.py deleted file mode 100644 index c37ff209f51..00000000000 --- a/src/otx/algo/utils/xai_utils.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Utils used for XAI.""" - -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING, Any - -import cv2 -import numpy as np -from datumaro import Image - -from otx.core.config.explain import ExplainConfig -from otx.core.data.entity.base import OTXBatchPredEntityWithXAI -from otx.core.data.entity.instance_segmentation import InstanceSegBatchPredEntityWithXAI -from otx.core.types.explain import TargetExplainGroup - -if TYPE_CHECKING: - from lightning.pytorch.utilities.types import EVAL_DATALOADERS - - from otx.core.data.module import OTXDataModule - - -def process_saliency_maps_in_pred_entity( - predict_result: list[OTXBatchPredEntityWithXAI | InstanceSegBatchPredEntityWithXAI | Any], - explain_config: ExplainConfig, -) -> list[Any] | list[OTXBatchPredEntityWithXAI | InstanceSegBatchPredEntityWithXAI]: - """Process saliency maps in PredEntity.""" - for predict_result_per_batch in predict_result: - saliency_maps = predict_result_per_batch.saliency_maps - imgs_info = predict_result_per_batch.imgs_info - ori_img_shapes = [img_info.ori_shape for img_info in imgs_info] - pred_labels = predict_result_per_batch.labels # type: ignore[union-attr] - if pred_labels: - pred_labels = [pred.tolist() for pred in pred_labels] - - processed_saliency_maps = process_saliency_maps(saliency_maps, explain_config, pred_labels, ori_img_shapes) - - predict_result_per_batch.saliency_maps = processed_saliency_maps - return predict_result - - -def process_saliency_maps( - saliency_maps: list, - explain_config: ExplainConfig, - pred_labels: list | None, - ori_img_shapes: list, -) -> list[dict[Any, Any]]: - """Perform saliency map convertion to dict and post-processing.""" - if explain_config.target_explain_group == TargetExplainGroup.ALL: - processed_saliency_maps = convert_maps_to_dict_all(saliency_maps) - elif explain_config.target_explain_group == TargetExplainGroup.PREDICTIONS: - processed_saliency_maps = convert_maps_to_dict_predictions(saliency_maps, pred_labels) - elif explain_config.target_explain_group == TargetExplainGroup.IMAGE: - processed_saliency_maps = convert_maps_to_dict_image(saliency_maps) - else: - msg = f"Target explain group {explain_config.target_explain_group} is not supported." - raise ValueError(msg) - - if explain_config.postprocess: - for i in range(len(processed_saliency_maps)): - processed_saliency_maps[i] = { - key: postprocess(s_map, ori_img_shapes[i]) for key, s_map in processed_saliency_maps[i].items() - } - - return processed_saliency_maps - - -def convert_maps_to_dict_all(saliency_maps: np.array) -> list[dict[Any, np.array]]: - """Convert salincy maps to dict for TargetExplainGroup.ALL.""" - if saliency_maps[0].ndim != 3: - msg = "Shape mismatch." - raise ValueError(msg) - - processed_saliency_maps = [] - for maps_per_image in saliency_maps: - explain_target_to_sal_map = dict(enumerate(maps_per_image)) - processed_saliency_maps.append(explain_target_to_sal_map) - return processed_saliency_maps - - -def convert_maps_to_dict_predictions(saliency_maps: np.array, pred_labels: list | None) -> list[dict[Any, np.array]]: - """Convert salincy maps to dict for TargetExplainGroup.PREDICTIONS.""" - if saliency_maps[0].ndim != 3: - msg = "Shape mismatch." - raise ValueError(msg) - if not pred_labels: - return [] - - processed_saliency_maps = [] - for i, maps_per_image in enumerate(saliency_maps): - explain_target_to_sal_map = {label: maps_per_image[label] for label in pred_labels[i] if pred_labels[i]} - processed_saliency_maps.append(explain_target_to_sal_map) - return processed_saliency_maps - - -def convert_maps_to_dict_image(saliency_maps: np.array) -> list[dict[Any, np.array]]: - """Convert salincy maps to dict for TargetExplainGroup.IMAGE.""" - if saliency_maps[0].ndim != 2: - msg = "Shape mismatch." - raise ValueError(msg) - return [{"map_per_image": map_per_image} for map_per_image in saliency_maps] - - -def postprocess(saliency_map: np.ndarray, output_size: tuple[int, int] | None) -> np.ndarray: - """Postprocess single saliency map.""" - if saliency_map.ndim != 2: - msg = "Shape mismatch." - raise ValueError(msg) - - if output_size: - h, w = output_size - saliency_map = cv2.resize(saliency_map, (w, h)) - return cv2.applyColorMap(saliency_map, cv2.COLORMAP_JET) - - -def dump_saliency_maps( - predict_result: list[OTXBatchPredEntityWithXAI | InstanceSegBatchPredEntityWithXAI | Any], - explain_config: ExplainConfig, - datamodule: EVAL_DATALOADERS | OTXDataModule, - output_dir: Path, - weight: float = 0.3, -) -> None: - """Sumps saliency maps (raw and with overlay).""" - output_dir = output_dir / "saliency_maps" - output_dir.mkdir(parents=True, exist_ok=True) - - for predict_result_per_batch in predict_result: - saliency_maps = predict_result_per_batch.saliency_maps - imgs_info = predict_result_per_batch.imgs_info - for pred_index in range(len(saliency_maps)): - img_id = imgs_info[pred_index].img_idx - img_data, image_save_name = _get_image_data_name(datamodule, img_id) - - for class_id, s_map in saliency_maps[pred_index].items(): - file_name_map = Path(image_save_name + "_class_" + str(class_id) + "_saliency_map.png") - save_path_map = output_dir / file_name_map - cv2.imwrite(str(save_path_map), s_map) - - if explain_config.postprocess: - file_name_overlay = Path(image_save_name + "_class_" + str(class_id) + "_overlay.png") - save_path_overlay = output_dir / file_name_overlay - overlay = _get_overlay(img_data, s_map, weight) - cv2.imwrite(str(save_path_overlay), overlay) - - -def _get_image_data_name( - datamodule: EVAL_DATALOADERS | OTXDataModule, - img_id: int, - subset_name: str = "test", -) -> tuple[np.array, str]: - subset = datamodule.subsets[subset_name] - image_name = subset.ids[img_id] - item = subset.dm_subset.get(id=image_name, subset=subset_name) - img = item.media_as(Image) - img_data, _ = subset._get_img_data_and_shape(img) # noqa: SLF001 - image_save_name = "".join([char if char.isalnum() else "_" for char in image_name]) - return img_data, image_save_name - - -def _get_overlay(img: np.ndarray, s_map: np.ndarray, weight: float = 0.3) -> np.ndarray: - overlay = img * weight + s_map * (1 - weight) - overlay[overlay > 255] = 255 - return overlay.astype(np.uint8) diff --git a/src/otx/algo/visual_prompting/__init__.py b/src/otx/algo/visual_prompting/__init__.py deleted file mode 100644 index 49cbe9e32d6..00000000000 --- a/src/otx/algo/visual_prompting/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX visual prompting models.""" - -from . import backbones, decoders, encoders -from .openvino_models import VisualPromptingDecoder, VisualPromptingImageEncoder -from .segment_anything import OTXSegmentAnything, SegmentAnything -from .zero_shot_segment_anything import OTXZeroShotSegmentAnything, ZeroShotSegmentAnything - -__all__ = [ - "backbones", - "encoders", - "decoders", - "OTXSegmentAnything", - "SegmentAnything", - "OTXZeroShotSegmentAnything", - "ZeroShotSegmentAnything", - "VisualPromptingImageEncoder", - "VisualPromptingDecoder", -] diff --git a/src/otx/algo/visual_prompting/backbones/__init__.py b/src/otx/algo/visual_prompting/backbones/__init__.py deleted file mode 100644 index a5eb223dd05..00000000000 --- a/src/otx/algo/visual_prompting/backbones/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Backbone modules for OTX visual prompting model.""" - -from .tiny_vit import TinyViT -from .vit import ViT - -__all__ = ["TinyViT", "ViT"] diff --git a/src/otx/algo/visual_prompting/backbones/tiny_vit.py b/src/otx/algo/visual_prompting/backbones/tiny_vit.py deleted file mode 100644 index 61e061c9143..00000000000 --- a/src/otx/algo/visual_prompting/backbones/tiny_vit.py +++ /dev/null @@ -1,639 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""TinyViT model for the OTX visual prompting.""" - -from __future__ import annotations - -import itertools - -import torch -import torch.nn.functional as F # noqa: N812 -from timm.layers import DropPath, to_2tuple, trunc_normal_ -from torch import Tensor, nn - -from otx.algo.visual_prompting.utils.layer_norm_2d import LayerNorm2d - - -class Conv2d_BN(nn.Sequential): # noqa: N801 - """Conv2d_BN for TinyViT.""" - - def __init__( - self, - a: int, - b: int, - ks: int = 1, - stride: int = 1, - pad: int = 0, - dilation: int = 1, - groups: int = 1, - bn_weight_init: float = 1.0, - ) -> None: - super().__init__() - self.add_module("c", nn.Conv2d(a, b, ks, stride, pad, dilation, groups, bias=False)) - bn = nn.BatchNorm2d(b) - nn.init.constant_(bn.weight, bn_weight_init) - nn.init.constant_(bn.bias, 0) - self.add_module("bn", bn) - - @torch.no_grad() - def fuse(self) -> nn.Module: - """Fuse weights and biases.""" - c, bn = self._modules.values() - w = bn.weight / (bn.running_var + bn.eps) ** 0.5 - w = c.weight * w[:, None, None, None] - b = bn.bias - bn.running_mean * bn.weight / (bn.running_var + bn.eps) ** 0.5 - m = nn.Conv2d( - w.size(1) * self.c.groups, - w.size(0), - w.shape[2:], - stride=self.c.stride, - padding=self.c.padding, - dilation=self.c.dilation, - groups=self.c.groups, - ) - m.weight.data.copy_(w) - m.bias.data.copy_(b) - return m - - -class PatchEmbed(nn.Module): - """PatchEmbed for TinyViT.""" - - def __init__(self, in_chans: int, embed_dim: int, resolution: int, activation: nn.Module) -> None: - super().__init__() - img_size: tuple[int, int] = to_2tuple(resolution) - self.patches_resolution = (img_size[0] // 4, img_size[1] // 4) - self.num_patches = self.patches_resolution[0] * self.patches_resolution[1] - self.in_chans = in_chans - self.embed_dim = embed_dim - n = embed_dim - self.seq = nn.Sequential( - Conv2d_BN(in_chans, n // 2, 3, 2, 1), - activation(), - Conv2d_BN(n // 2, n, 3, 2, 1), - ) - - def forward(self, x: Tensor) -> Tensor: - """Forward.""" - return self.seq(x) - - -class MBConv(nn.Module): - """MBConv for TinyViT.""" - - def __init__( - self, - in_chans: int, - out_chans: int, - expand_ratio: float, - activation: nn.Module, - drop_path: float, - ) -> None: - super().__init__() - self.in_chans = in_chans - self.hidden_chans = int(in_chans * expand_ratio) - self.out_chans = out_chans - - self.conv1 = Conv2d_BN(in_chans, self.hidden_chans, ks=1) - self.act1 = activation() - - self.conv2 = Conv2d_BN(self.hidden_chans, self.hidden_chans, ks=3, stride=1, pad=1, groups=self.hidden_chans) - self.act2 = activation() - - self.conv3 = Conv2d_BN(self.hidden_chans, out_chans, ks=1, bn_weight_init=0.0) - self.act3 = activation() - - self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() - - def forward(self, x: Tensor) -> Tensor: - """Forward.""" - shortcut = x - - x = self.conv1(x) - x = self.act1(x) - - x = self.conv2(x) - x = self.act2(x) - - x = self.conv3(x) - - x = self.drop_path(x) - - x += shortcut - return self.act3(x) - - -class PatchMerging(nn.Module): - """PatchMerging for TinyViT.""" - - def __init__(self, input_resolution: tuple[int, int], dim: int, out_dim: int, activation: nn.Module) -> None: - super().__init__() - - self.input_resolution = input_resolution - self.dim = dim - self.out_dim = out_dim - self.act = activation() - self.conv1 = Conv2d_BN(dim, out_dim, 1, 1, 0) - stride_c = 2 - if out_dim in (320, 448, 576): - stride_c = 1 - self.conv2 = Conv2d_BN(out_dim, out_dim, 3, stride_c, 1, groups=out_dim) - self.conv3 = Conv2d_BN(out_dim, out_dim, 1, 1, 0) - - def forward(self, x: Tensor) -> Tensor: - """Forward.""" - if x.ndim == 3: - height, width = self.input_resolution - batch = len(x) - x = x.view(batch, height, width, -1).permute(0, 3, 1, 2) - - x = self.conv1(x) - x = self.act(x) - - x = self.conv2(x) - x = self.act(x) - x = self.conv3(x) - return x.flatten(2).transpose(1, 2) - - -class ConvLayer(nn.Module): - """ConvLayer for TinyViT.""" - - def __init__( - self, - dim: int, - input_resolution: int, - depth: int, - activation: nn.Module, - drop_path: list[float] | float = 0.0, - downsample: nn.Module | None = None, - out_dim: int | None = None, - conv_expand_ratio: float = 4.0, - ) -> None: - super().__init__() - self.dim = dim - self.input_resolution = input_resolution - self.depth = depth - - # build blocks - self.blocks = nn.ModuleList( - [ - MBConv( - dim, - dim, - conv_expand_ratio, - activation, - drop_path[i] if isinstance(drop_path, list) else drop_path, - ) - for i in range(depth) - ], - ) - - # patch merging layer - if downsample is not None: - self.downsample = downsample(input_resolution, dim=dim, out_dim=out_dim, activation=activation) - else: - self.downsample = None - - def forward(self, x: Tensor) -> Tensor: - """Forward.""" - for blk in self.blocks: - x = blk(x) - if self.downsample is not None: - x = self.downsample(x) - return x - - -class Mlp(nn.Module): - """MLP for TinyViT.""" - - def __init__( - self, - in_features: int, - hidden_features: int | None = None, - out_features: int | None = None, - act_layer: nn.Module = nn.GELU, - drop: float = 0.0, - ) -> None: - super().__init__() - out_features = out_features or in_features - hidden_features = hidden_features or in_features - self.norm = nn.LayerNorm(in_features) - self.fc1 = nn.Linear(in_features, hidden_features) - self.fc2 = nn.Linear(hidden_features, out_features) - self.act = act_layer() - self.drop = nn.Dropout(drop) - - def forward(self, x: Tensor) -> Tensor: - """Forward.""" - x = self.norm(x) - - x = self.fc1(x) - x = self.act(x) - x = self.drop(x) - x = self.fc2(x) - return self.drop(x) - - -class Attention(nn.Module): - """Attention block for TinyViT.""" - - def __init__( - self, - dim: int, - key_dim: int, - num_heads: int = 8, - attn_ratio: int = 4, - resolution: tuple[int, int] = (14, 14), - ) -> None: - super().__init__() - assert isinstance(resolution, tuple) # noqa: S101 - assert len(resolution) == 2 # noqa: S101 - self.num_heads = num_heads - self.scale = key_dim**-0.5 - self.key_dim = key_dim - self.nh_kd = nh_kd = key_dim * num_heads - self.d = int(attn_ratio * key_dim) - self.dh = int(attn_ratio * key_dim) * num_heads - self.attn_ratio = attn_ratio - h = self.dh + nh_kd * 2 - - self.norm = nn.LayerNorm(dim) - self.qkv = nn.Linear(dim, h) - self.proj = nn.Linear(self.dh, dim) - - points = list(itertools.product(range(resolution[0]), range(resolution[1]))) - n = len(points) - attention_offsets: dict[tuple[int, int], int] = {} - idxs = [] - for p1 in points: - for p2 in points: - offset = (abs(p1[0] - p2[0]), abs(p1[1] - p2[1])) - if offset not in attention_offsets: - attention_offsets[offset] = len(attention_offsets) - idxs.append(attention_offsets[offset]) - self.attention_biases = nn.Parameter(torch.zeros(num_heads, len(attention_offsets))) - self.register_buffer("attention_bias_idxs", torch.LongTensor(idxs).view(n, n), persistent=False) - - @torch.no_grad() - def train(self, mode: bool = True) -> None: # noqa: D102 - super().train(mode) - if mode and hasattr(self, "ab"): - del self.ab - else: - self.register_buffer("ab", self.attention_biases[:, self.attention_bias_idxs], persistent=False) - - def forward(self, x: Tensor) -> Tensor: # x (B,N,C) - """Forward.""" - b, n, _ = x.shape - - # normalization - x = self.norm(x) - - qkv = self.qkv(x) - # (b, n, num_heads, d) - q, k, v = qkv.view(b, n, self.num_heads, -1).split([self.key_dim, self.key_dim, self.d], dim=3) - # (b, num_heads, n, d) - q = q.permute(0, 2, 1, 3) - k = k.permute(0, 2, 1, 3) - v = v.permute(0, 2, 1, 3) - - attn = (q @ k.transpose(-2, -1)) * self.scale + ( - self.attention_biases[:, self.attention_bias_idxs] if self.training else self.ab - ) - attn = attn.softmax(dim=-1) - x = (attn @ v).transpose(1, 2).reshape(b, n, self.dh) - return self.proj(x) - - -class TinyViTBlock(nn.Module): - """TinyViT Block. - - Args: - dim (int): Number of input channels. - input_resolution (tuple[int, int]): Input resolution. - num_heads (int): Number of attention heads. - window_size (int): Window size. - mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. - drop (float, optional): Dropout rate. Default: 0.0 - drop_path (float, optional): Stochastic depth rate. Default: 0.0 - local_conv_size (int): the kernel size of the convolution between - Attention and MLP. Default: 3 - activation: the activation function. Default: nn.GELU - """ - - def __init__( - self, - dim: int, - input_resolution: tuple[int, int], - num_heads: int, - window_size: int = 7, - mlp_ratio: float = 4.0, - drop: float = 0.0, - drop_path: float = 0.0, - local_conv_size: int = 3, - activation: nn.Module = nn.GELU, - ) -> None: - super().__init__() - self.dim = dim - self.input_resolution = input_resolution - self.num_heads = num_heads - assert window_size > 0, "window_size must be greater than 0" # noqa: S101 - self.window_size = window_size - self.mlp_ratio = mlp_ratio - - self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() - - assert dim % num_heads == 0, "dim must be divisible by num_heads" # noqa: S101 - head_dim = dim // num_heads - - window_resolution = (window_size, window_size) - self.attn = Attention(dim, head_dim, num_heads, attn_ratio=1, resolution=window_resolution) - - mlp_hidden_dim = int(dim * mlp_ratio) - mlp_activation = activation - self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=mlp_activation, drop=drop) - - pad = local_conv_size // 2 - self.local_conv = Conv2d_BN(dim, dim, ks=local_conv_size, stride=1, pad=pad, groups=dim) - - def forward(self, x: Tensor) -> Tensor: - """Forward.""" - h, w = self.input_resolution - b, l, c = x.shape # noqa: E741 - assert h * w == l, "input feature has wrong size" # noqa: S101 - res_x = x - if self.window_size == h and self.window_size == w: - x = self.attn(x) - else: - x = x.view(b, h, w, c) - pad_b = (self.window_size - h % self.window_size) % self.window_size - pad_r = (self.window_size - w % self.window_size) % self.window_size - padding = pad_b > 0 or pad_r > 0 - - if padding: - x = F.pad(x, (0, 0, 0, pad_r, 0, pad_b)) - - ph, pw = h + pad_b, w + pad_r - nh = ph // self.window_size - hw = pw // self.window_size - # window partition - x = ( - x.view(b, nh, self.window_size, hw, self.window_size, c) - .transpose(2, 3) - .reshape(b * nh * hw, self.window_size * self.window_size, c) - ) - x = self.attn(x) - # window reverse - x = x.view(b, nh, hw, self.window_size, self.window_size, c).transpose(2, 3).reshape(b, ph, pw, c) - - if padding: - x = x[:, :h, :w].contiguous() - - x = x.view(b, l, c) - - x = res_x + self.drop_path(x) - - x = x.transpose(1, 2).reshape(b, c, h, w) - x = self.local_conv(x) - x = x.view(b, c, l).transpose(1, 2) - - return x + self.drop_path(self.mlp(x)) - - def extra_repr(self) -> str: # noqa: D102 - return ( - f"dim={self.dim}, input_resolution={self.input_resolution}, num_heads={self.num_heads}, " - f"window_size={self.window_size}, mlp_ratio={self.mlp_ratio}" - ) - - -class BasicLayer(nn.Module): - """A basic TinyViT layer for one stage. - - Args: - dim (int): Number of input channels. - input_resolution (tuple[int]): Input resolution. - depth (int): Number of blocks. - num_heads (int): Number of attention heads. - window_size (int): Local window size. - mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. - drop (float, optional): Dropout rate. Default: 0.0 - drop_path (float | tuple[float], optional): Stochastic depth rate. Default: 0.0 - downsample (nn.Module | None, optional): Downsample layer at the end of the layer. Default: None - local_conv_size: the kernel size of the depthwise convolution between attention and MLP. Default: 3 - activation: the activation function. Default: nn.GELU - out_dim: the output dimension of the layer. Default: dim - """ - - def __init__( - self, - dim: int, - input_resolution: tuple[int, int], - depth: int, - num_heads: int, - window_size: int, - mlp_ratio: float = 4.0, - drop: float = 0.0, - drop_path: list[float] | float = 0.0, - downsample: nn.Module | None = None, - local_conv_size: int = 3, - activation: nn.Module = nn.GELU, - out_dim: int | None = None, - ) -> None: - super().__init__() - self.dim = dim - self.input_resolution = input_resolution - self.depth = depth - - # build blocks - self.blocks = nn.ModuleList( - [ - TinyViTBlock( - dim=dim, - input_resolution=input_resolution, - num_heads=num_heads, - window_size=window_size, - mlp_ratio=mlp_ratio, - drop=drop, - drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path, - local_conv_size=local_conv_size, - activation=activation, - ) - for i in range(depth) - ], - ) - - # patch merging layer - if downsample is not None: - self.downsample = downsample(input_resolution, dim=dim, out_dim=out_dim, activation=activation) - else: - self.downsample = None - - def forward(self, x: Tensor) -> Tensor: - """Forward.""" - for blk in self.blocks: - x = blk(x) - if self.downsample is not None: - x = self.downsample(x) - return x - - def extra_repr(self) -> str: # noqa: D102 - return f"dim={self.dim}, input_resolution={self.input_resolution}, depth={self.depth}" - - -class TinyViT(nn.Module): - """TinyViT for MobileSAM.""" - - def __init__( - self, - img_size: int = 224, - in_chans: int = 3, - embed_dims: list[int] | None = None, - depths: list[int] | None = None, - num_heads: list[int] | None = None, - window_sizes: list[int] | None = None, - mlp_ratio: float = 4.0, - drop_rate: float = 0.0, - drop_path_rate: float = 0.1, - mbconv_expand_ratio: float = 4.0, - local_conv_size: int = 3, - layer_lr_decay: float = 1.0, - ) -> None: - super().__init__() - embed_dims = embed_dims or [96, 192, 384, 768] - depths = depths or [2, 2, 6, 2] - num_heads = num_heads or [3, 6, 12, 24] - window_sizes = window_sizes or [7, 7, 14, 7] - - self.img_size = img_size - self.depths = depths - self.num_layers = len(depths) - self.mlp_ratio = mlp_ratio - - activation = nn.GELU - - self.patch_embed = PatchEmbed( - in_chans=in_chans, - embed_dim=embed_dims[0], - resolution=img_size, - activation=activation, - ) - - patches_resolution = self.patch_embed.patches_resolution - self.patches_resolution = patches_resolution - - # stochastic depth - dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule - - # build layers - self.layers = nn.ModuleList() - for i_layer in range(self.num_layers): - kwargs = { - "dim": embed_dims[i_layer], - "input_resolution": ( - patches_resolution[0] // (2 ** (i_layer - 1 if i_layer == 3 else i_layer)), - patches_resolution[1] // (2 ** (i_layer - 1 if i_layer == 3 else i_layer)), - ), - "depth": depths[i_layer], - "drop_path": dpr[sum(depths[:i_layer]) : sum(depths[: i_layer + 1])], - "downsample": PatchMerging if (i_layer < self.num_layers - 1) else None, - "out_dim": embed_dims[min(i_layer + 1, len(embed_dims) - 1)], - "activation": activation, - } - if i_layer == 0: - layer = ConvLayer( - conv_expand_ratio=mbconv_expand_ratio, - **kwargs, - ) - else: - layer = BasicLayer( - num_heads=num_heads[i_layer], - window_size=window_sizes[i_layer], - mlp_ratio=self.mlp_ratio, - drop=drop_rate, - local_conv_size=local_conv_size, - **kwargs, - ) - self.layers.append(layer) - - # init weights - self.apply(self._init_weights) - self.set_layer_lr_decay(layer_lr_decay) - self.neck = nn.Sequential( - nn.Conv2d( - embed_dims[-1], - 256, - kernel_size=1, - bias=False, - ), - LayerNorm2d(256), - nn.Conv2d( - 256, - 256, - kernel_size=3, - padding=1, - bias=False, - ), - LayerNorm2d(256), - ) - - def set_layer_lr_decay(self, layer_lr_decay: float) -> None: - """Set layer lr decay.""" - decay_rate = layer_lr_decay - - depth = sum(self.depths) - lr_scales = [decay_rate ** (depth - i - 1) for i in range(depth)] - - def _set_lr_scale(m: nn.Module, scale: float) -> None: - for p in m.parameters(): - p.lr_scale = scale - - self.patch_embed.apply(lambda x: _set_lr_scale(x, lr_scales[0])) - i = 0 - for layer in self.layers: - for block in layer.blocks: - block.apply(lambda x: _set_lr_scale(x, lr_scales[i])) # noqa: B023 - i += 1 - if layer.downsample is not None: - layer.downsample.apply(lambda x: _set_lr_scale(x, lr_scales[i - 1])) # noqa: B023 - assert i == depth # noqa: S101 - - for k, p in self.named_parameters(): - p.param_name = k - - def _check_lr_scale(m: nn.Module) -> None: - for p in m.parameters(): - assert hasattr(p, "lr_scale"), p.param_name # noqa: S101 - - self.apply(_check_lr_scale) - - def _init_weights(self, m: nn.Module) -> None: - """Initialize weights.""" - if isinstance(m, nn.Linear): - trunc_normal_(m.weight, std=0.02) - if isinstance(m, nn.Linear) and m.bias is not None: - nn.init.constant_(m.bias, 0) - elif isinstance(m, nn.LayerNorm): - nn.init.constant_(m.bias, 0) - nn.init.constant_(m.weight, 1.0) - - @torch.jit.ignore - def no_weight_decay_keywords(self) -> set[str]: - """Keyworkds for no weight decay.""" - return {"attention_biases"} - - def forward(self, x: Tensor) -> Tensor: - """Forward.""" - x = self.patch_embed(x) - - x = self.layers[0](x) - start_i = 1 - - for i in range(start_i, len(self.layers)): - layer = self.layers[i] - x = layer(x) - batch, _, channel = x.size() - x = x.view(batch, 64, 64, channel) - x = x.permute(0, 3, 1, 2) - return self.neck(x) diff --git a/src/otx/algo/visual_prompting/backbones/vit.py b/src/otx/algo/visual_prompting/backbones/vit.py deleted file mode 100644 index aaca0435d2d..00000000000 --- a/src/otx/algo/visual_prompting/backbones/vit.py +++ /dev/null @@ -1,417 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""ViT models for the OTX visual prompting.""" - -from __future__ import annotations - -import torch -import torch.nn.functional as F # noqa: N812 -from torch import Tensor, nn - -from otx.algo.visual_prompting.utils import LayerNorm2d, MLPBlock - - -# This class and its supporting functions below lightly adapted from the ViTDet backbone available at: https://github.com/facebookresearch/detectron2/blob/main/detectron2/modeling/backbone/vit.py -class ViT(nn.Module): - """Vision Transformer for visual prompting task. - - Args: - img_size (int): Input image size. - patch_size (int): Patch size. - in_chans (int): Number of input image channels. - embed_dim (int): Patch embedding dimension. - depth (int): Depth of ViT. - num_heads (int): Number of attention heads in each ViT block. - mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. - out_chans (int): Number of output channels. - qkv_bias (bool): If True, add a learnable bias to query, key, value. - norm_layer (nn.Module): Normalization layer. - act_layer (nn.Module): Activation layer. - use_abs_pos (bool): If True, use absolute positional embeddings. - use_rel_pos (bool): If True, add relative positional embeddings to the attention map. - rel_pos_zero_init (bool): If True, zero initialize relative positional parameters. - window_size (int): Window size for window attention blocks. - global_attn_indexes (list): Indexes for blocks using global attention. - """ - - def __init__( - self, - img_size: int = 1024, - patch_size: int = 16, - in_chans: int = 3, - embed_dim: int = 768, - depth: int = 12, - num_heads: int = 12, - mlp_ratio: float = 4.0, - out_chans: int = 256, - qkv_bias: bool = True, - norm_layer: nn.Module = nn.LayerNorm, - act_layer: nn.Module = nn.GELU, - use_abs_pos: bool = True, - use_rel_pos: bool = False, - rel_pos_zero_init: bool = True, - window_size: int = 0, - global_attn_indexes: tuple[int, ...] = (), - ) -> None: - super().__init__() - self.img_size = img_size - - self.patch_embed = PatchEmbed( - kernel_size=(patch_size, patch_size), - stride=(patch_size, patch_size), - in_chans=in_chans, - embed_dim=embed_dim, - ) - - self.pos_embed: nn.Parameter | None = None - if use_abs_pos: - # Initialize absolute positional embedding with pretrain image size. - self.pos_embed = nn.Parameter(torch.zeros(1, img_size // patch_size, img_size // patch_size, embed_dim)) - - self.blocks = nn.ModuleList() - for i in range(depth): - block = Block( - dim=embed_dim, - num_heads=num_heads, - mlp_ratio=mlp_ratio, - qkv_bias=qkv_bias, - norm_layer=norm_layer, - act_layer=act_layer, - use_rel_pos=use_rel_pos, - rel_pos_zero_init=rel_pos_zero_init, - window_size=window_size if i not in global_attn_indexes else 0, - input_size=(img_size // patch_size, img_size // patch_size), - ) - self.blocks.append(block) - - self.neck = nn.Sequential( - nn.Conv2d( - embed_dim, - out_chans, - kernel_size=1, - bias=False, - ), - LayerNorm2d(out_chans), - nn.Conv2d( - out_chans, - out_chans, - kernel_size=3, - padding=1, - bias=False, - ), - LayerNorm2d(out_chans), - ) - - def forward(self, x: Tensor) -> Tensor: - """Forward function. - - Args: - x (Tensor): Input tensor of shape (B, C, H, W). - - Returns: - Tensor: Output tensor of shape (B, out_chans, H, W). - """ - x = self.patch_embed(x) - if self.pos_embed is not None: - x = x + self.pos_embed - - for blk in self.blocks: - x = blk(x) - - return self.neck(x.permute(0, 3, 1, 2)) - - -class Block(nn.Module): - """Transformer blocks with support of window attention and residual propagation blocks. - - Args: - dim (int): Number of input channels. - num_heads (int): Number of attention heads in each ViT block. - mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. - qkv_bias (bool): If True, add a learnable bias to query, key, value. - norm_layer (nn.Module): Normalization layer. - act_layer (nn.Module): Activation layer. - use_rel_pos (bool): If True, add relative positional embeddings to the attention map. - rel_pos_zero_init (bool): If True, zero initialize relative positional parameters. - window_size (int): Window size for window attention blocks. If it equals 0, then - use global attention. - input_size (tuple(int, int) or None): Input resolution for calculating the relative - positional parameter size. - """ - - def __init__( - self, - dim: int, - num_heads: int, - mlp_ratio: float = 4.0, - qkv_bias: bool = True, - norm_layer: type[nn.Module] = nn.LayerNorm, - act_layer: type[nn.Module] = nn.GELU, - use_rel_pos: bool = False, - rel_pos_zero_init: bool = True, - window_size: int = 0, - input_size: tuple[int, int] | None = None, - ) -> None: - super().__init__() - self.norm1 = norm_layer(dim) - self.attn = Attention( - dim, - num_heads=num_heads, - qkv_bias=qkv_bias, - use_rel_pos=use_rel_pos, - rel_pos_zero_init=rel_pos_zero_init, - input_size=input_size if window_size == 0 else (window_size, window_size), - ) - - self.norm2 = norm_layer(dim) - self.mlp = MLPBlock(embedding_dim=dim, mlp_dim=int(dim * mlp_ratio), act=act_layer) - - self.window_size = window_size - - def forward(self, x: Tensor) -> Tensor: - """Forward function. - - Args: - x (Tensor): Input tensor of shape (B, H, W, C). - - Returns: - Tensor: Output tensor of shape (B, H, W, C). - """ - shortcut = x - x = self.norm1(x) - # Window partition - if self.window_size > 0: - height, width = x.shape[1], x.shape[2] - x, pad_hw = window_partition(x, self.window_size) - - x = self.attn(x) - # Reverse window partition - if self.window_size > 0: - x = window_unpartition(x, self.window_size, pad_hw, (height, width)) - - x = shortcut + x - return x + self.mlp(self.norm2(x)) - - -class Attention(nn.Module): - """Multi-head Attention block with relative position embeddings. - - Args: - dim (int): Number of input channels. - num_heads (int): Number of attention heads. - qkv_bias (bool): If True, add a learnable bias to query, key, value. - use_rel_pos (bool): If True, add relative positional embeddings to the attention map. - rel_pos_zero_init (bool): If True, zero initialize relative positional parameters. - input_size (tuple(int, int) or None): Input resolution for calculating the relative - positional parameter size. - """ - - def __init__( - self, - dim: int, - num_heads: int = 8, - qkv_bias: bool = True, - use_rel_pos: bool = False, - rel_pos_zero_init: bool = True, - input_size: tuple[int, int] | None = None, - ) -> None: - super().__init__() - self.num_heads = num_heads - head_dim = dim // num_heads - self.scale = head_dim**-0.5 - - self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) - self.proj = nn.Linear(dim, dim) - - self.use_rel_pos = use_rel_pos - if self.use_rel_pos: - assert input_size is not None, "Input size must be provided if using relative positional encoding." # noqa: S101 - # initialize relative positional embeddings - self.rel_pos_h = nn.Parameter(torch.zeros(2 * input_size[0] - 1, head_dim)) - self.rel_pos_w = nn.Parameter(torch.zeros(2 * input_size[1] - 1, head_dim)) - - def forward(self, x: Tensor) -> Tensor: - """Forward function. - - Args: - x (Tensor): Input tensor of shape (batch, height, width, C). - - Returns: - Tensor: Output tensor of shape (batch, height, width, C). - """ - batch, height, width, _ = x.shape - # qkv with shape (3, batch, nHead, height * width, C) - qkv = self.qkv(x).reshape(batch, height * width, 3, self.num_heads, -1).permute(2, 0, 3, 1, 4) - # q, k, v with shape (batch * nHead, height * width, C) - q, k, v = qkv.reshape(3, batch * self.num_heads, height * width, -1).unbind(0) - - attn = (q * self.scale) @ k.transpose(-2, -1) - - if self.use_rel_pos: - attn = add_decomposed_rel_pos(attn, q, self.rel_pos_h, self.rel_pos_w, (height, width), (height, width)) - - attn = attn.softmax(dim=-1) - x = ( - (attn @ v) - .view(batch, self.num_heads, height, width, -1) - .permute(0, 2, 3, 1, 4) - .reshape(batch, height, width, -1) - ) - return self.proj(x) - - -def window_partition(x: Tensor, window_size: int) -> tuple[Tensor, tuple[int, int]]: - """Partition into non-overlapping windows with padding if needed. - - Args: - x (Tensor): Input tokens with [batch, height, width, channel]. - window_size (int): Window size. - - Returns: - windows (Tensor): windows after partition with [batch * num_windows, window_size, window_size, channel]. - (hp, wp) (Tuple[int, int]): padded height and width before partition - """ - batch, height, width, channel = x.shape - - pad_h = (window_size - height % window_size) % window_size - pad_w = (window_size - width % window_size) % window_size - if pad_h > 0 or pad_w > 0: - x = F.pad(x, (0, 0, 0, pad_w, 0, pad_h)) - hp, wp = height + pad_h, width + pad_w - - x = x.view(batch, hp // window_size, window_size, wp // window_size, window_size, channel) - windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, channel) - return windows, (hp, wp) - - -def window_unpartition(windows: Tensor, window_size: int, pad_hw: tuple[int, int], hw: tuple[int, int]) -> Tensor: - """Window unpartition into original sequences and removing padding. - - Args: - windows (Tensor): input tokens with [batch * num_windows, window_size, window_size, C]. - window_size (int): window size. - pad_hw (Tuple): padded height and width (hp, wp). - hw (Tuple): original height and width (h, w) before padding. - - Returns: - x (Tensor): unpartitioned sequences with [B, H, W, C]. - """ - hp, wp = pad_hw - h, w = hw - batch = windows.shape[0] // (hp * wp // window_size // window_size) - x = windows.view(batch, hp // window_size, wp // window_size, window_size, window_size, -1) - x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(batch, hp, wp, -1) - - if hp > h or wp > w: - x = x[:, :h, :w, :].contiguous() - return x - - -def get_rel_pos(q_size: int, k_size: int, rel_pos: Tensor) -> Tensor: - """Get relative positional embeddings according to the relative positions of query and key sizes. - - Args: - q_size (int): size of query q. - k_size (int): size of key k. - rel_pos (Tensor): relative position embeddings (L, C). - - Returns: - Tensor: Extracted positional embeddings according to relative positions. - """ - max_rel_dist = int(2 * max(q_size, k_size) - 1) - # Interpolate rel pos if needed. - if rel_pos.shape[0] != max_rel_dist: - # Interpolate rel pos. - rel_pos_resized = F.interpolate( - rel_pos.reshape(1, rel_pos.shape[0], -1).permute(0, 2, 1), - size=max_rel_dist, - mode="linear", - ) - rel_pos_resized = rel_pos_resized.reshape(-1, max_rel_dist).permute(1, 0) - else: - rel_pos_resized = rel_pos - - # Scale the coords with short length if shapes for q and k are different. - q_coords = torch.arange(q_size)[:, None] * max(k_size / q_size, 1.0) - k_coords = torch.arange(k_size)[None, :] * max(q_size / k_size, 1.0) - relative_coords = (q_coords - k_coords) + (k_size - 1) * max(q_size / k_size, 1.0) - - return rel_pos_resized[relative_coords.long()] - - -def add_decomposed_rel_pos( - attn: Tensor, - q: Tensor, - rel_pos_h: Tensor, - rel_pos_w: Tensor, - q_size: tuple[int, int], - k_size: tuple[int, int], -) -> Tensor: - """Calculate decomposed Relative Positional Embeddings from `mvitv2`. - - https://github.com/facebookresearch/mvit/blob/19786631e330df9f3622e5402b4a419a263a2c80/mvit/models/attention.py - - Args: - attn (Tensor): attention map. - q (Tensor): query q in the attention layer with shape (batch, q_h * q_w, C). - rel_pos_h (Tensor): relative position embeddings (Lh, C) for height axis. - rel_pos_w (Tensor): relative position embeddings (Lw, C) for width axis. - q_size (Tuple): spatial sequence size of query q with (q_h, q_w). - k_size (Tuple): spatial sequence size of key k with (k_h, k_w). - - Returns: - attn (Tensor): attention map with added relative positional embeddings. - """ - q_h, q_w = q_size - k_h, k_w = k_size - rh = get_rel_pos(q_h, k_h, rel_pos_h) - rw = get_rel_pos(q_w, k_w, rel_pos_w) - - batch, _, dim = q.shape - r_q = q.reshape(batch, q_h, q_w, dim) - rel_h = torch.einsum("bhwc,hkc->bhwk", r_q, rh) - rel_w = torch.einsum("bhwc,wkc->bhwk", r_q, rw) - - return (attn.view(batch, q_h, q_w, k_h, k_w) + rel_h[:, :, :, :, None] + rel_w[:, :, :, None, :]).view( - batch, - q_h * q_w, - k_h * k_w, - ) - - -class PatchEmbed(nn.Module): - """Image to Patch Embedding. - - Args: - kernel_size (Tuple): kernel size of the projection layer. - stride (Tuple): stride of the projection layer. - padding (Tuple): padding size of the projection layer. - in_chans (int): Number of input image channels. - embed_dim (int): Patch embedding dimension. - """ - - def __init__( - self, - kernel_size: tuple[int, int] = (16, 16), - stride: tuple[int, int] = (16, 16), - padding: tuple[int, int] = (0, 0), - in_chans: int = 3, - embed_dim: int = 768, - ) -> None: - super().__init__() - - self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=kernel_size, stride=stride, padding=padding) - - def forward(self, x: Tensor) -> Tensor: - """Forward call. - - Args: - x (Tensor): input image tensor with shape (B, C, H, W). - - Returns: - Tensor: output tensor with shape (B, H', W', C'). - """ - x = self.proj(x) - # B C H W -> B H W C - return x.permute(0, 2, 3, 1) diff --git a/src/otx/algo/visual_prompting/decoders/__init__.py b/src/otx/algo/visual_prompting/decoders/__init__.py deleted file mode 100644 index f8c2047f622..00000000000 --- a/src/otx/algo/visual_prompting/decoders/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Decoder modules for OTX visual prompting model.""" - -from .sam_mask_decoder import SAMMaskDecoder - -__all__ = ["SAMMaskDecoder"] diff --git a/src/otx/algo/visual_prompting/encoders/__init__.py b/src/otx/algo/visual_prompting/encoders/__init__.py deleted file mode 100644 index 91772d67692..00000000000 --- a/src/otx/algo/visual_prompting/encoders/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Encoder modules for OTX visual prompting model.""" - -from .sam_image_encoder import SAMImageEncoder -from .sam_prompt_encoder import SAMPromptEncoder - -__all__ = ["SAMImageEncoder", "SAMPromptEncoder"] diff --git a/src/otx/algo/visual_prompting/encoders/sam_image_encoder.py b/src/otx/algo/visual_prompting/encoders/sam_image_encoder.py deleted file mode 100644 index 6143fd139c0..00000000000 --- a/src/otx/algo/visual_prompting/encoders/sam_image_encoder.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""SAM image encoder model for the OTX visual prompting.""" - -from functools import partial -from typing import ClassVar - -from torch import nn - - -class SAMImageEncoder(nn.Module): - """Image encoder model of Segment Anything for visual prompting model.""" - - backbone_configs: ClassVar[dict] = { - "tiny_vit": { - "img_size": 1024, - "embed_dims": [64, 128, 160, 320], - "depths": [2, 2, 6, 2], - "num_heads": [2, 4, 5, 10], - "window_sizes": [7, 7, 14, 7], - "drop_path_rate": 0.0, - }, - "vit_b": { - "img_size": 1024, - "norm_layer": partial(nn.LayerNorm, eps=1e-6), - "out_chans": 256, - "patch_size": 16, - "mlp_ratio": 4, - "qkv_bias": True, - "use_rel_pos": True, - "window_size": 14, - "embed_dim": 768, - "depth": 12, - "num_heads": 12, - "global_attn_indexes": [2, 5, 8, 11], - }, - "vit_l": { - "img_size": 1024, - "norm_layer": partial(nn.LayerNorm, eps=1e-6), - "out_chans": 256, - "patch_size": 16, - "mlp_ratio": 4, - "qkv_bias": True, - "use_rel_pos": True, - "window_size": 14, - "embed_dim": 1024, - "depth": 24, - "num_heads": 16, - "global_attn_indexes": [5, 11, 17, 23], - }, - "vit_h": { - "img_size": 1024, - "norm_layer": partial(nn.LayerNorm, eps=1e-6), - "out_chans": 256, - "patch_size": 16, - "mlp_ratio": 4, - "qkv_bias": True, - "use_rel_pos": True, - "window_size": 14, - "embed_dim": 1280, - "depth": 32, - "num_heads": 16, - "global_attn_indexes": [7, 15, 23, 31], - }, - } - - def __new__(cls, backbone: str, *args, **kwargs): # noqa: ARG003 - """Initialize image encoder to target backbone.""" - if backbone.lower() == "tiny_vit": - from otx.algo.visual_prompting.backbones.tiny_vit import TinyViT - - return TinyViT(**cls.backbone_configs.get(backbone.lower())) # type: ignore[arg-type] - elif backbone.lower() in ["vit_b", "vit_l", "vit_h"]: # noqa: RET505 - from otx.algo.visual_prompting.backbones.vit import ViT - - return ViT(**cls.backbone_configs.get(backbone.lower())) # type: ignore[arg-type] - - else: - error_log = f"{backbone} is not supported for SAMImageEncoder. Set among tiny_vit and vit_b." - raise ValueError(error_log) diff --git a/src/otx/algo/visual_prompting/encoders/sam_prompt_encoder.py b/src/otx/algo/visual_prompting/encoders/sam_prompt_encoder.py deleted file mode 100644 index 8ed67abcdaf..00000000000 --- a/src/otx/algo/visual_prompting/encoders/sam_prompt_encoder.py +++ /dev/null @@ -1,274 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""SAM prompt encoder model for the OTX visual prompting.""" - -from __future__ import annotations - -from typing import Any - -import numpy as np -import torch -from torch import nn - -from otx.algo.visual_prompting.utils.layer_norm_2d import LayerNorm2d - - -class SAMPromptEncoder(nn.Module): - """Encodes prompts for input to SAM's mask decoder. - - Reference: https://github.com/facebookresearch/segment-anything - - Args: - embed_dim (int): The prompts' embedding dimension. - image_embedding_size (tuple(int, int)): The spatial size of the image embedding, as (H, W). - input_image_size (int): The padded size of the image as input to the image encoder, as (H, W). - mask_in_chans (int): The number of hidden channels used for encoding input masks. - activation (nn.Module): The activation to use when encoding input masks. - """ - - def __init__( - self, - embed_dim: int, - image_embedding_size: tuple[int, int], - input_image_size: tuple[int, int], - mask_in_chans: int, - activation: type[nn.Module] = nn.GELU, - ) -> None: - super().__init__() - self.embed_dim = embed_dim - self.input_image_size = input_image_size - self.image_embedding_size = image_embedding_size - self.pe_layer = PositionEmbeddingRandom(embed_dim // 2) - - self.num_point_embeddings: int = 4 # pos/neg point + 2 box corners - point_embeddings = [nn.Embedding(1, embed_dim) for i in range(self.num_point_embeddings)] - self.point_embeddings = nn.ModuleList(point_embeddings) - self.not_a_point_embed = nn.Embedding(1, embed_dim) - - self.mask_input_size = (4 * image_embedding_size[0], 4 * image_embedding_size[1]) - self.mask_downscaling = nn.Sequential( - nn.Conv2d(1, mask_in_chans // 4, kernel_size=2, stride=2), - LayerNorm2d(mask_in_chans // 4), - activation(), - nn.Conv2d(mask_in_chans // 4, mask_in_chans, kernel_size=2, stride=2), - LayerNorm2d(mask_in_chans), - activation(), - nn.Conv2d(mask_in_chans, embed_dim, kernel_size=1), - ) - self.no_mask_embed = nn.Embedding(1, embed_dim) - - def get_dense_pe(self) -> torch.Tensor: - """Returns the positional encoding. - - It used to encode point prompts, applied to a dense set of points the shape of the image encoding. - - Returns: - torch.Tensor: Positional encoding with shape 1x(embed_dim)x(embedding_h)x(embedding_w). - """ - return self.pe_layer(self.image_embedding_size).unsqueeze(0) - - def _embed_points(self, points: torch.Tensor, labels: torch.Tensor, pad: bool) -> torch.Tensor: - """Embeds point prompts. - - Args: - points (torch.Tensor): A BxNx2 array of point prompts to the model. - Each point is in (X,Y) in pixels. - labels (torch.Tensor): A BxN array of labels for the point prompts. - 1 indicates a foreground point and 0 indicates a background point. - pad (bool): Whether to pad the points with a zero point. - - Returns: - torch.Tensor: The embedded points, as (N, embed_dim). - """ - points = points + 0.5 # Shift to center of pixel - if pad: - padding_point = torch.zeros((points.shape[0], 1, 2), device=points.device) - padding_label = -torch.ones((labels.shape[0], 1), device=labels.device) - points = torch.cat([points, padding_point], dim=1) - labels = torch.cat([labels, padding_label], dim=1) - point_embedding = self.pe_layer.forward_with_coords(points, self.input_image_size) - point_embedding[labels == -1] = 0.0 - point_embedding[labels == -1] += self.not_a_point_embed.weight - point_embedding[labels == 0] += self.point_embeddings[0].weight - point_embedding[labels == 1] += self.point_embeddings[1].weight - return point_embedding - - def _embed_boxes(self, boxes: torch.Tensor) -> torch.Tensor: - """Embeds box prompts. - - Args: - boxes (torch.Tensor): A Bx4 array given a box prompt to the model, in XYXY format. - - Returns: - torch.Tensor: The embedded boxes, as (N, embed_dim). - """ - boxes = boxes + 0.5 # Shift to center of pixel - coords = boxes.reshape(-1, 2, 2) - corner_embedding = self.pe_layer.forward_with_coords(coords, self.input_image_size) - corner_embedding[:, 0, :] += self.point_embeddings[2].weight - corner_embedding[:, 1, :] += self.point_embeddings[3].weight - return corner_embedding - - def _embed_masks(self, masks: torch.Tensor) -> torch.Tensor: - """Embeds mask inputs. - - Args: - masks (torch.Tensor): A low resolution mask input to the model, typically - coming from a previous prediction iteration. Has form Bx1xHxW, where - for SAM, H=W=256. Masks returned by a previous iteration of the - predict method do not need further transformation. - - Returns: - torch.Tensor: The embedded masks, as (N, embed_dim). - """ - return self.mask_downscaling(masks) - - def _get_batch_size( - self, - points: tuple[torch.Tensor, torch.Tensor] | None, - boxes: torch.Tensor | None, - masks: torch.Tensor | None, - ) -> int: - """Gets the batch size of the output given the batch size of the input prompts. - - Args: - points (tuple(torch.Tensor, torch.Tensor) or none): point coordinates and labels to embed. - boxes (torch.Tensor or none): boxes to embed. - masks (torch.Tensor or none): masks to embed. - - Returns: - int: The batch size of the output. - """ - if points is not None: - return points[0].shape[0] - elif boxes is not None: # noqa: RET505 - return boxes.shape[0] - elif masks is not None: - return masks.shape[0] - else: - return 1 - - def _get_device(self) -> torch.device: - """Gets the device of the embeddings. - - Returns: - torch.device: The device of the embeddings. - """ - return self.point_embeddings[0].weight.device - - def forward( - self, - points: tuple[torch.Tensor, torch.Tensor] | None, - boxes: torch.Tensor | None, - masks: torch.Tensor | None, - ) -> tuple[torch.Tensor, torch.Tensor]: - """Embeds different types of prompts, returning both sparse and dense embeddings. - - Args: - points (tuple(torch.Tensor, torch.Tensor) or none): Point coordinates and labels to embed. - Point coordinates are BxNx2 arrays of point prompts to the model. - Each point is in (X,Y) in pixels. Labels are BxN arrays of labels for the point prompts. - 1 indicates a foreground point and 0 indicates a background point. - boxes (torch.Tensor or none): A Bx4 array given a box prompt to the model, in XYXY format. - masks (torch.Tensor or none): A low resolution mask input to the model, typically - coming from a previous prediction iteration. Has form Bx1xHxW, where - for SAM, H=W=256. Masks returned by a previous iteration of the - predict method do not need further transformation. - - Returns: - sparse_embeddings (torch.Tensor): sparse embeddings for the points and boxes, with shape Nx1x(embed_dim), - where N is determined by the number of input points and boxes. - dense_embeddings (torch.Tensor): dense embeddings for the masks, - in the shape Nx(embed_dim)x(embed_H)x(embed_W). - """ - bs = self._get_batch_size(points, boxes, masks) - sparse_embeddings = torch.empty((bs, 0, self.embed_dim), device=self._get_device()) - if points is not None: - coords, labels = points - point_embeddings = self._embed_points(coords, labels, pad=(boxes is None)) - sparse_embeddings = torch.cat([sparse_embeddings, point_embeddings], dim=1) - if boxes is not None: - box_embeddings = self._embed_boxes(boxes) - sparse_embeddings = torch.cat([sparse_embeddings, box_embeddings], dim=1) - - if masks is not None: - dense_embeddings = self._embed_masks(masks) - else: - dense_embeddings = self.no_mask_embed.weight.reshape(1, -1, 1, 1).expand( - bs, - -1, - self.image_embedding_size[0], - self.image_embedding_size[1], - ) - - return sparse_embeddings, dense_embeddings - - -class PositionEmbeddingRandom(nn.Module): - """Positional encoding using random spatial frequencies. - - Args: - num_pos_feats (int): The number of positional frequencies. - scale (float): The scale of the positional encoding. - """ - - def __init__(self, num_pos_feats: int = 64, scale: float | None = None) -> None: - super().__init__() - if scale is None or scale <= 0.0: - scale = 1.0 - self.register_buffer( - "positional_encoding_gaussian_matrix", - scale * torch.randn((2, num_pos_feats)), - ) - - def _pe_encoding(self, coords: torch.Tensor) -> torch.Tensor: - """Positionally encode points that are normalized to [0,1]. - - Args: - coords (torch.Tensor): Stacked x-y grids, as (H, W, 2). - - Returns: - torch.Tensor: The positional encoding, as (H, W, num_pos_feats * 2). - """ - # assuming coords are in [0, 1]^2 square and have d_1 x ... x d_n x 2 shape - coords = 2 * coords - 1 - coords = coords @ self.positional_encoding_gaussian_matrix - coords = 2 * np.pi * coords - # outputs d_1 x ... x d_n x C shape - return torch.cat([torch.sin(coords), torch.cos(coords)], dim=-1) - - def forward(self, size: tuple[int, int]) -> torch.Tensor: - """Generate positional encoding for a grid of the specified size. - - Args: - size (tuple(int, int)): The size of the grid to generate the encoding for. - - Returns: - torch.Tensor: The positional encoding, as (num_pos_feats * 2, H, W). - """ - h, w = size - device: Any = self.positional_encoding_gaussian_matrix.device - grid = torch.ones((h, w), device=device, dtype=torch.float32) - y_embed = grid.cumsum(dim=0) - 0.5 - x_embed = grid.cumsum(dim=1) - 0.5 - y_embed = y_embed / h - x_embed = x_embed / w - - pe = self._pe_encoding(torch.stack([x_embed, y_embed], dim=-1)) - return pe.permute(2, 0, 1) # C x H x W - - def forward_with_coords(self, coords_input: torch.Tensor, image_size: tuple[int, int]) -> torch.Tensor: - """Positionally encode points that are not normalized to [0,1]. - - Args: - coords_input (torch.Tensor): The coordinates to encode, as (N, 1, 2). - image_size (tuple(int, int)): The size of the image the coordinates are from. - - Returns: - torch.Tensor: The positional encoding, as (N, 1, num_pos_feats * 2). - """ - coords = coords_input.clone() - coords[:, :, 0] = coords[:, :, 0] / image_size[1] - coords[:, :, 1] = coords[:, :, 1] / image_size[0] - return self._pe_encoding(coords.to(torch.float)) # B x N x C diff --git a/src/otx/algo/visual_prompting/openvino_models.py b/src/otx/algo/visual_prompting/openvino_models.py deleted file mode 100644 index 55a97f99d20..00000000000 --- a/src/otx/algo/visual_prompting/openvino_models.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""Openvino Model Wrappers for the OTX visual prompting.""" - -from __future__ import annotations - -from copy import deepcopy -from typing import TYPE_CHECKING, Any - -import numpy as np -from openvino.model_api.models import ImageModel, SegmentationModel -from openvino.model_api.models.types import BooleanValue, NumericalValue, StringValue - -if TYPE_CHECKING: - from openvino.model_api.adapters.inference_adapter import InferenceAdapter - - -class VisualPromptingImageEncoder(ImageModel): - """Image Encoder class of OTX Visual Prompting model for openvino task.""" - - __model__ = "image_encoder" - - def __init__( - self, - inference_adapter: InferenceAdapter, - configuration: dict[str, Any] | None = None, - preload: bool = False, - ): - super().__init__(inference_adapter, configuration, preload) - - @classmethod - def parameters(cls) -> dict[str, Any]: # noqa: D102 - parameters = super().parameters() - parameters.update( - { - "resize_type": StringValue(default_value="fit_to_window"), - "image_size": NumericalValue(value_type=int, default_value=1024, min=0, max=2048), - }, - ) - return parameters - - def preprocess(self, inputs: np.ndarray) -> tuple[dict[str, np.ndarray], dict[str, Any]]: - """Update meta for image encoder.""" - dict_inputs, meta = super().preprocess(inputs) - meta["resize_type"] = self.resize_type - return dict_inputs, meta - - -class VisualPromptingDecoder(SegmentationModel): - """Decoder class of OTX Visual Prompting model for openvino task.""" - - __model__ = "decoder" - - def __init__( - self, - model_adapter: InferenceAdapter, - configuration: dict | None = None, - preload: bool = False, - ): - super().__init__(model_adapter, configuration, preload) - - self.mask_input = np.zeros((1, 1, 256, 256), dtype=np.float32) - self.has_mask_input = np.zeros((1, 1), dtype=np.float32) - - @classmethod - def parameters(cls) -> dict[str, Any]: # noqa: D102 - parameters = super().parameters() - parameters.update({"image_size": NumericalValue(value_type=int, default_value=1024, min=0, max=2048)}) - parameters.update({"mask_threshold": NumericalValue(value_type=float, default_value=0.0, min=0, max=1)}) - parameters.update({"embed_dim": NumericalValue(value_type=int, default_value=256, min=0, max=512)}) - parameters.update({"embedded_processing": BooleanValue(default_value=True)}) - return parameters - - def _get_outputs(self) -> str: - return "upscaled_masks" - - def preprocess(self, inputs: dict[str, Any]) -> list[dict[str, Any]]: - """Preprocess prompts.""" - processed_prompts: list[dict[str, Any]] = [] - for prompt_name in ["bboxes", "points"]: - if (prompts := inputs.get(prompt_name, None)) is None or ( - labels := inputs["labels"].get(prompt_name, None) - ) is None: - continue - - for prompt, label in zip(prompts, labels): - if prompt_name == "bboxes": - point_coords = self.apply_coords(prompt.reshape(-1, 2, 2), inputs["orig_size"]) - point_labels = np.array([2, 3], dtype=np.float32).reshape(-1, 2) - else: - point_coords = self.apply_coords(prompt.reshape(-1, 1, 2), inputs["orig_size"]) - point_labels = np.array([1], dtype=np.float32).reshape(-1, 1) - - processed_prompts.append( - { - "point_coords": point_coords, - "point_labels": point_labels, - "mask_input": self.mask_input, - "has_mask_input": self.has_mask_input, - "orig_size": np.array(inputs["orig_size"], dtype=np.int64).reshape(-1, 2), - "label": label, - }, - ) - return processed_prompts - - def apply_coords(self, coords: np.ndarray, orig_size: np.ndarray | list[int] | tuple[int, int]) -> np.ndarray: - """Process coords according to preprocessed image size using image meta.""" - old_h, old_w = orig_size - new_h, new_w = self._get_preprocess_shape(old_h, old_w, self.image_size) - coords = deepcopy(coords).astype(np.float32) - coords[..., 0] = coords[..., 0] * (new_w / old_w) - coords[..., 1] = coords[..., 1] * (new_h / old_h) - return coords - - def _get_preprocess_shape(self, old_h: int, old_w: int, image_size: int) -> tuple[int, int]: - """Compute the output size given input size and target image size.""" - scale = image_size / max(old_h, old_w) - new_h, new_w = old_h * scale, old_w * scale - new_w = int(new_w + 0.5) - new_h = int(new_h + 0.5) - return (new_h, new_w) - - def _check_io_number(self, number_of_inputs: int | tuple[int], number_of_outputs: int | tuple[int]) -> None: - pass - - def _get_inputs(self) -> tuple[list[str], list[str]]: - """Get input layer name and shape.""" - image_blob_names = list(self.inputs.keys()) - image_info_blob_names: list = [] - return image_blob_names, image_info_blob_names - - def postprocess(self, outputs: dict[str, np.ndarray], meta: dict[str, Any]) -> dict[str, np.ndarray]: - """Postprocess to convert soft prediction to hard prediction. - - Args: - outputs (dict[str, np.ndarray]): The output of the model. - meta (dict[str, Any]): Contain label and original size. - - Returns: - (dict[str, np.ndarray]): The postprocessed output of the model. - """ - probability = max(min(float(outputs["scores"]), 1.0), 0.0) - hard_prediction = outputs[self.output_blob_name].squeeze(1) > self.mask_threshold - soft_prediction = hard_prediction * probability - - outputs["hard_prediction"] = hard_prediction - outputs["soft_prediction"] = soft_prediction - - return outputs diff --git a/src/otx/algo/visual_prompting/segment_anything.py b/src/otx/algo/visual_prompting/segment_anything.py deleted file mode 100644 index 40d69c6d34e..00000000000 --- a/src/otx/algo/visual_prompting/segment_anything.py +++ /dev/null @@ -1,541 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""Segment Anything model for the OTX visual prompting.""" - -from __future__ import annotations - -import logging as log -from typing import Any, Literal - -import torch -from torch import Tensor, nn -from torch.nn import functional as F # noqa: N812 -from torchvision import tv_tensors - -from otx.algo.visual_prompting.decoders import SAMMaskDecoder -from otx.algo.visual_prompting.encoders import SAMImageEncoder, SAMPromptEncoder -from otx.core.data.entity.base import OTXBatchLossEntity, Points -from otx.core.data.entity.visual_prompting import VisualPromptingBatchDataEntity, VisualPromptingBatchPredEntity -from otx.core.model.entity.visual_prompting import OTXVisualPromptingModel - -DEFAULT_CONFIG_SEGMENT_ANYTHING: dict[str, dict[str, Any]] = { - "tiny_vit": { - "load_from": "https://github.com/ChaoningZhang/MobileSAM/raw/master/weights/mobile_sam.pt", - }, - "vit_b": { - "load_from": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth", - }, - "vit_l": { - "load_from": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth", - }, - "vit_h": { - "load_from": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth", - }, -} - - -class SegmentAnything(nn.Module): - """Visual prompting model class for Segment Anything.""" - - def __init__( - self, - backbone: str, - load_from: str | None = None, - mask_threshold: float = 0.0, - image_size: int = 1024, - image_embedding_size: int = 64, - embed_dim: int = 256, - mask_in_chans: int = 16, - num_multimask_outputs: int = 3, - transformer_cfg: dict[str, int] | None = None, - transformer_dim: int = 256, - iou_head_depth: int = 3, - iou_head_hidden_dim: int = 256, - freeze_image_encoder: bool = True, - freeze_prompt_encoder: bool = True, - freeze_mask_decoder: bool = False, - use_stability_score: bool = False, - return_single_mask: bool = False, - return_extra_metrics: bool = False, - stability_score_offset: float = 1.0, - ) -> None: - super().__init__() - if transformer_cfg is None: - transformer_cfg = {"depth": 2, "embedding_dim": 256, "mlp_dim": 2048, "num_heads": 8} - - self.mask_threshold = mask_threshold - self.image_size = image_size - self.embed_dim = embed_dim - self.image_embedding_size = image_embedding_size - self.use_stability_score = use_stability_score - self.return_single_mask = return_single_mask - self.return_extra_metrics = return_extra_metrics - self.stability_score_offset = stability_score_offset - - self.image_encoder = SAMImageEncoder(backbone=backbone) - self.prompt_encoder = SAMPromptEncoder( - embed_dim=embed_dim, - image_embedding_size=(image_embedding_size, image_embedding_size), - input_image_size=(image_size, image_size), - mask_in_chans=mask_in_chans, - ) - self.mask_decoder = SAMMaskDecoder( - num_multimask_outputs=num_multimask_outputs, - transformer_cfg=transformer_cfg, - transformer_dim=transformer_dim, - iou_head_depth=iou_head_depth, - iou_head_hidden_dim=iou_head_hidden_dim, - ) - - self.load_checkpoint(load_from=load_from) - self.freeze_networks(freeze_image_encoder, freeze_prompt_encoder, freeze_mask_decoder) - - def freeze_networks( - self, - freeze_image_encoder: bool, - freeze_prompt_encoder: bool, - freeze_mask_decoder: bool, - ) -> None: - """Freeze networks depending on config.""" - if freeze_image_encoder: - for param in self.image_encoder.parameters(): - param.requires_grad = False - - if freeze_prompt_encoder: - for param in self.prompt_encoder.parameters(): - param.requires_grad = False - - if freeze_mask_decoder: - for param in self.mask_decoder.parameters(): - param.requires_grad = False - - def load_checkpoint( - self, - load_from: str | None, - ) -> None: - """Load checkpoint for SAM. - - Args: - load_from (Optional[str], optional): Checkpoint path for SAM. Defaults to None. - """ - try: - state_dict = torch.hub.load_state_dict_from_url(str(load_from)) - for key in [ - "image_encoder.norm_head.weight", - "image_encoder.norm_head.bias", - "image_encoder.head.weight", - "image_encoder.head.bias", - ]: - if key in state_dict: - state_dict.pop(key) - self.load_state_dict(state_dict) - except ValueError as e: - log.info( - f"{e}: {load_from} is not desirable format for torch.hub.load_state_dict_from_url. " - f"To manually load {load_from}, try to set it to trainer.checkpoint.", - ) - - def forward(self, *args, mode: str = "infer", **kwargs) -> Any: # noqa: ANN401 - """Forward method for visual prompting task.""" - assert mode in ["finetuning", "learn", "infer"] # noqa: S101 - if mode == "finetuning": - return self.forward_train(*args, **kwargs) - return self.forward_inference(*args, **kwargs) - - @torch.no_grad() - def forward_inference( - self, - image_embeddings: Tensor, - point_coords: Tensor, - point_labels: Tensor, - mask_input: Tensor, - has_mask_input: Tensor, - ori_shape: Tensor, - ) -> tuple[Tensor, ...]: - """Forward method for SAM inference (export/deploy). - - Args: - image_embeddings (Tensor): The image embedding with a batch index of length 1. - If it is a zero tensor, the image embedding will be computed from the image. - point_coords (Tensor): Coordinates of sparse input prompts, - corresponding to both point inputs and box inputs. - Boxes are encoded using two points, one for the top-left corner and one for the bottom-right corner. - Coordinates must already be transformed to long-side 1024. Has a batch index of length 1. - point_labels (Tensor): Labels for the sparse input prompts. - 0 is a negative input point, 1 is a positive input point, - 2 is a top-left box corner, 3 is a bottom-right box corner, and -1 is a padding point. - If there is no box input, a single padding point with label -1 and - coordinates (0.0, 0.0) should be concatenated. - mask_input (Tensor): A mask input to the model with shape 1x1x256x256. - This must be supplied even if there is no mask input. In this case, it can just be zeros. - has_mask_input (Tensor): An indicator for the mask input. - 1 indicates a mask input, 0 indicates no mask input. - This input has 1x1 shape due to supporting openvino input layout. - ori_shape (Tensor): The size of the input image in (H,W) format, before any transformation. - This input has 1x2 shape due to supporting openvino input layout. - """ - sparse_embedding = self._embed_points(point_coords, point_labels) - dense_embedding = self._embed_masks(mask_input, has_mask_input) - - masks, scores = self.mask_decoder.predict_masks( - image_embeddings=image_embeddings, - image_pe=self.prompt_encoder.get_dense_pe(), - sparse_prompt_embeddings=sparse_embedding, - dense_prompt_embeddings=dense_embedding, - ) - - if self.use_stability_score: - scores = self.calculate_stability_score( - masks, - self.mask_threshold, - self.stability_score_offset, - ) - - if self.return_single_mask: - masks, scores = self.select_masks(masks, scores, point_coords.shape[1]) - - upscaled_masks = self.postprocess_masks(masks, self.image_size, ori_shape) - - if self.return_extra_metrics: - stability_scores = self.calculate_stability_score( - upscaled_masks, - self.mask_threshold, - self.stability_score_offset, - ) - areas = (upscaled_masks > self.mask_threshold).sum(-1).sum(-1) - return upscaled_masks, scores, stability_scores, areas, masks - - return upscaled_masks, scores, masks - - def forward_train( - self, - images: tv_tensors.Image, - ori_shapes: list[Tensor], - bboxes: list[tv_tensors.BoundingBoxes | None], - points: list[tuple[Points, Tensor] | None], - gt_masks: list[tv_tensors.Mask] | None = None, - ) -> Tensor | tuple[list[Tensor], list[Tensor]]: - """Forward method for SAM training/validation/prediction. - - Args: - images (tv_tensors.Image): Images with shape (B, C, H, W). - ori_shapes (List[Tensor]): List of original shapes per image. - bboxes (List[tv_tensors.BoundingBoxes], optional): A Nx4 array given a box prompt to the model, - in XYXY format. - points (List[Tuple[Points, Tensor]], optional): Point coordinates and labels to embed. - Point coordinates are BxNx2 arrays of point prompts to the model. - Each point is in (X,Y) in pixels. Labels are BxN arrays of labels for the point prompts. - 1 indicates a foreground point and 0 indicates a background point. - gt_masks (List[tv_tensors.Mask], optional): Ground truth masks for loss calculation. - - Returns: - (Tensor): Calculated loss values. - (Tuple[List[Tensor], List[Tensor]]): Tuple of list with predicted masks with shape (B, 1, H, W) - and List with IoU predictions with shape (N, 1). - """ - image_embeddings = self.image_encoder(images) - pred_masks = [] - ious = [] - for idx, embedding in enumerate(image_embeddings): - low_res_masks, iou_predictions = [], [] - for prompt in [bboxes[idx], points[idx]]: - if prompt is None: - continue - - sparse_embeddings, dense_embeddings = self.prompt_encoder( - points=prompt if isinstance(prompt[0], Points) else None, - boxes=prompt if isinstance(prompt, tv_tensors.BoundingBoxes) else None, - masks=None, - ) - _low_res_masks, _iou_predictions = self.mask_decoder( - image_embeddings=embedding.unsqueeze(0), - image_pe=self.prompt_encoder.get_dense_pe(), - sparse_prompt_embeddings=sparse_embeddings, - dense_prompt_embeddings=dense_embeddings, - multimask_output=False, # when given multiple prompts. if there is single prompt True would be better. # noqa: E501 - ) - low_res_masks.append(_low_res_masks) - iou_predictions.append(_iou_predictions) - - pred_masks.append(torch.cat(low_res_masks, dim=0)) - ious.append(torch.cat(iou_predictions, dim=0)) - - if self.training: - loss_dice = 0.0 - loss_focal = 0.0 - loss_iou = 0.0 - - num_masks = sum(len(pred_mask) for pred_mask in pred_masks) - for pred_mask, gt_mask, iou, ori_shape in zip(pred_masks, gt_masks, ious, ori_shapes): # type: ignore[arg-type] - post_processed_pred_mask = self.postprocess_masks(pred_mask, self.image_size, ori_shape) - post_processed_pred_mask = post_processed_pred_mask.sigmoid() - post_processed_pred_mask = post_processed_pred_mask.flatten(1) - flatten_gt_mask = gt_mask.flatten(1).float() - - # calculate losses - loss_dice += self.calculate_dice_loss(post_processed_pred_mask, flatten_gt_mask, num_masks) - loss_focal += self.calculate_sigmoid_ce_focal_loss(post_processed_pred_mask, flatten_gt_mask, num_masks) - batch_iou = self.calculate_iou(post_processed_pred_mask, flatten_gt_mask) - loss_iou += F.mse_loss(iou, batch_iou.unsqueeze(1), reduction="sum") / num_masks - - loss = 20.0 * loss_focal + loss_dice + loss_iou - - return {"loss": loss, "loss_focal": loss_focal, "loss_dice": loss_dice, "loss_iou": loss_iou} - - post_processed_pred_masks: list[Tensor] = [] - for pred_mask, ori_shape in zip(pred_masks, ori_shapes): - post_processed_pred_mask = self.postprocess_masks(pred_mask, self.image_size, ori_shape) - post_processed_pred_masks.append(post_processed_pred_mask.squeeze(1).sigmoid()) - return post_processed_pred_masks, ious - - def _embed_points(self, point_coords: Tensor, point_labels: Tensor) -> Tensor: - """Embed sparse input prompts. - - Args: - point_coords (Tensor): Coordinates of sparse input prompts, - corresponding to both point inputs and box inputs. Boxes are encoded using two points, - one for the top-left corner and one for the bottom-right corner. - Coordinates must already be transformed to long-side 1024. Has a batch index of length 1. - point_labels (Tensor): Labels for the sparse input prompts. - 0 is a negative input point, 1 is a positive input point, - 2 is a top-left box corner, 3 is a bottom-right box corner, and -1 is a padding point. - If there is no box input, a single padding point with label -1 and - coordinates (0.0, 0.0) should be concatenated. - - Returns: - point_embedding (Tensor): The embedded sparse input prompts. - """ - point_coords = point_coords + 0.5 - point_coords = point_coords / self.image_size - point_embedding = self.prompt_encoder.pe_layer._pe_encoding(point_coords) # noqa: SLF001 - point_labels = point_labels.unsqueeze(-1).expand_as(point_embedding) - - point_embedding = point_embedding * (point_labels != -1) - point_embedding = point_embedding + self.prompt_encoder.not_a_point_embed.weight * (point_labels == -1) - - for i in range(self.prompt_encoder.num_point_embeddings): - point_embedding = point_embedding + self.prompt_encoder.point_embeddings[i].weight * (point_labels == i) - - return point_embedding - - def _embed_masks(self, input_mask: Tensor, has_mask_input: Tensor) -> Tensor: - """Embed the mask input. - - Args: - input_mask (Tensor): A mask input to the model with shape 1x1x256x256. - This must be supplied even if there is no mask input. In this case, it can just be zeros. - has_mask_input (Tensor): An indicator for the mask input. - 1 indicates a mask input, 0 indicates no mask input. - - Returns: - mask_embedding (Tensor): The embedded mask input. - """ - mask_embedding = has_mask_input * self.prompt_encoder.mask_downscaling(input_mask) - return mask_embedding + (1 - has_mask_input) * self.prompt_encoder.no_mask_embed.weight.reshape( - 1, - -1, - 1, - 1, - ) - - def calculate_dice_loss(self, inputs: Tensor, targets: Tensor, num_masks: int) -> Tensor: - """Compute the DICE loss, similar to generalized IOU for masks. - - Args: - inputs (Tensor): A tensor representing a mask. - targets (Tensor): A tensor with the same shape as inputs. Stores the binary classification labels - for each element in inputs (0 for the negative class and 1 for the positive class). - num_masks (int): The number of masks present in the current batch, used for normalization. - - Returns: - Tensor: The DICE loss. - """ - numerator = 2 * (inputs * targets).sum(-1) - denominator = inputs.sum(-1) + targets.sum(-1) - loss = 1 - (numerator + 1) / (denominator + 1) - return loss.sum() / num_masks - - def calculate_sigmoid_ce_focal_loss( - self, - inputs: Tensor, - targets: Tensor, - num_masks: int, - alpha: float = 0.25, - gamma: float = 2, - ) -> Tensor: - r"""Loss used in RetinaNet for dense detection: https://arxiv.org/abs/1708.02002. # noqa: D301. - - Args: - inputs (Tensor): A float tensor of arbitrary shape. - targets (Tensor): A tensor with the same shape as inputs. Stores the binary classification labels - for each element in inputs (0 for the negative class and 1 for the positive class). - num_masks (int): The number of masks present in the current batch, used for normalization. - alpha (float, *optional*, defaults to 0.25): Weighting factor in range (0,1) - to balance positive vs negative examples. - gamma (float, *optional*, defaults to 2.0): Exponent of the modulating factor \\(1 - p_t\\) - to balance easy vs hard examples. - - Returns: - Tensor: The focal loss. - """ - loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none") - p_t = inputs * targets + (1 - inputs) * (1 - targets) - loss = loss * ((1 - p_t) ** gamma) - if alpha >= 0: - alpha_t = alpha * targets + (1 - alpha) * (1 - targets) - loss = alpha_t * loss - return loss.mean(1).sum() / num_masks - - def calculate_iou(self, inputs: Tensor, targets: Tensor, epsilon: float = 1e-7) -> Tensor: - """Calculate the intersection over union (IOU) between the predicted mask and the ground truth mask. - - Args: - inputs (Tensor): A tensor representing a mask. - targets (Tensor): A tensor with the same shape as inputs. Stores the binary classification labels - for each element in inputs (0 for the negative class and 1 for the positive class). - epsilon (float, *optional*, defaults to 1e-7): A small value to prevent division by zero. - - Returns: - Tensor: The IOU between the predicted mask and the ground truth mask. - """ - pred_mask = (inputs >= 0.5).float() - intersection = torch.sum(torch.mul(pred_mask, targets), dim=1) - union = torch.sum(pred_mask, dim=1) + torch.sum(targets, dim=1) - intersection - return intersection / (union + epsilon) - - @classmethod - def postprocess_masks(cls, masks: Tensor, input_size: int, orig_size: Tensor) -> Tensor: - """Postprocess the predicted masks. - - Args: - masks (Tensor): A batch of predicted masks with shape Bx1xHxW. - input_size (int): The size of the image input to the model, in (H, W) format. - Used to remove padding. - orig_size (Tensor): The original image size with shape Bx2. - - Returns: - masks (Tensor): The postprocessed masks with shape Bx1xHxW. - """ - orig_size = orig_size.squeeze() - masks = F.interpolate(masks, size=(input_size, input_size), mode="bilinear", align_corners=False) - - prepadded_size = cls.get_prepadded_size(cls, orig_size, input_size) # type: ignore[arg-type] - masks = masks[..., : prepadded_size[0], : prepadded_size[1]] - - orig_size = orig_size.to(torch.int64) - h, w = orig_size[0], orig_size[1] - return F.interpolate(masks, size=(h, w), mode="bilinear", align_corners=False) - - def get_prepadded_size(self, input_image_size: Tensor, longest_side: int) -> Tensor: - """Get pre-padded size.""" - scale = longest_side / torch.max(input_image_size) - transformed_size = scale * input_image_size - return torch.floor(transformed_size + 0.5).to(torch.int64) - - def calculate_stability_score(self, masks: Tensor, mask_threshold: float, threshold_offset: float = 1.0) -> Tensor: - """Computes the stability score for a batch of masks. - - The stability score is the IoU between the binary masks obtained - by thresholding the predicted mask logits at high and low values. - - Args: - masks (Tensor): A batch of predicted masks with shape BxHxW. - mask_threshold (float): The threshold used to binarize the masks. - threshold_offset (float, optional): The offset used to compute the stability score. - - Returns: - stability_scores (Tensor): The stability scores for the batch of masks. - """ - # One mask is always contained inside the other. - # Save memory by preventing unnecessary cast to torch.int64 - intersections = ( - (masks > (mask_threshold + threshold_offset)).sum(-1, dtype=torch.int16).sum(-1, dtype=torch.int32) - ) - unions = (masks > (mask_threshold - threshold_offset)).sum(-1, dtype=torch.int16).sum(-1, dtype=torch.int32) - return intersections / unions - - def select_masks(self, masks: Tensor, iou_preds: Tensor, num_points: int) -> tuple[Tensor, Tensor]: - """Selects the best mask from a batch of masks. - - Args: - masks (Tensor): A batch of predicted masks with shape BxMxHxW. - iou_preds (Tensor): A batch of predicted IoU scores with shape BxM. - num_points (int): The number of points in the input. - - Returns: - masks (Tensor): The selected masks with shape Bx1xHxW. - iou_preds (Tensor): The selected IoU scores with shape Bx1. - """ - # Determine if we should return the multiclick mask or not from the number of points. - # The reweighting is used to avoid control flow. - score_reweight = torch.tensor([[1000] + [0] * (self.mask_decoder.num_mask_tokens - 1)]).to(iou_preds.device) - score = iou_preds + (num_points - 2.5) * score_reweight - best_idx = torch.argmax(score, dim=1) - masks = masks[torch.arange(masks.shape[0]), best_idx, :, :].unsqueeze(1) - iou_preds = iou_preds[torch.arange(masks.shape[0]), best_idx].unsqueeze(1) - - return masks, iou_preds - - -class OTXSegmentAnything(OTXVisualPromptingModel): - """Visual Prompting model.""" - - def __init__(self, backbone: Literal["tiny_vit", "vit_b"], num_classes: int = 0, **kwargs): - self.config = {"backbone": backbone, **DEFAULT_CONFIG_SEGMENT_ANYTHING[backbone], **kwargs} - super().__init__(num_classes=num_classes) - - def _create_model(self) -> nn.Module: - """Create a PyTorch model for this class.""" - return SegmentAnything(**self.config) - - def _customize_inputs(self, inputs: VisualPromptingBatchDataEntity) -> dict[str, Any]: # type: ignore[override] - """Customize the inputs for the model.""" - images = tv_tensors.wrap(torch.stack(inputs.images, dim=0).to(dtype=torch.float32), like=inputs.images[0]) - return { - "mode": "finetuning", - "images": images, - "ori_shapes": [torch.tensor(info.ori_shape) for info in inputs.imgs_info], - "gt_masks": inputs.masks, - "bboxes": self._inspect_prompts(inputs.bboxes), - "points": [ - (tv_tensors.wrap(point.unsqueeze(1), like=point), torch.ones(len(point), 1, device=point.device)) - if point is not None - else None - for point in self._inspect_prompts(inputs.points) - ], - } - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: VisualPromptingBatchDataEntity, # type: ignore[override] - ) -> VisualPromptingBatchPredEntity | OTXBatchLossEntity: - """Customize OTX output batch data entity if needed for model.""" - if self.training: - return outputs - - masks: list[tv_tensors.Mask] = [] - scores: list[torch.Tensor] = [] - for mask, score in zip(*outputs): - masks.append(tv_tensors.Mask(mask, dtype=torch.float32)) - scores.append(score) - - return VisualPromptingBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - masks=masks, - polygons=[], - points=[], - bboxes=[], - labels=[torch.cat(list(labels.values())) for labels in inputs.labels], - ) - - def _inspect_prompts(self, prompts: list[tv_tensors.TVTensor]) -> list[tv_tensors.TVTensor | None]: - """Inspect if given prompts are empty. - - If there are empty prompts (shape=0), they will be converted to None. - """ - return [None if p is None else None if p.shape[0] == 0 else p for p in prompts] diff --git a/src/otx/algo/visual_prompting/utils/__init__.py b/src/otx/algo/visual_prompting/utils/__init__.py deleted file mode 100644 index aedead59689..00000000000 --- a/src/otx/algo/visual_prompting/utils/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utils for OTX visual prompting model.""" - -from .layer_norm_2d import LayerNorm2d -from .mlp_block import MLPBlock - -__all__ = ["LayerNorm2d", "MLPBlock"] diff --git a/src/otx/algo/visual_prompting/utils/layer_norm_2d.py b/src/otx/algo/visual_prompting/utils/layer_norm_2d.py deleted file mode 100644 index ab7ab581ecf..00000000000 --- a/src/otx/algo/visual_prompting/utils/layer_norm_2d.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""2D layer normalization module for the OTX visual prompting.""" - -import torch -from torch import nn - - -class LayerNorm2d(nn.Module): - """2D-Layer Normalization module for ViT models. - - Reference: https://github.com/facebookresearch/segment-anything - """ - - def __init__(self, num_channels: int, eps: float = 1e-6) -> None: - super().__init__() - self.weight = nn.Parameter(torch.ones(num_channels)) - self.bias = nn.Parameter(torch.zeros(num_channels)) - self.eps = eps - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Forward.""" - u = x.mean(1, keepdim=True) - s = (x - u).pow(2).mean(1, keepdim=True) - x = (x - u) / torch.sqrt(s + self.eps) - return self.weight[:, None, None] * x + self.bias[:, None, None] diff --git a/src/otx/algo/visual_prompting/zero_shot_segment_anything.py b/src/otx/algo/visual_prompting/zero_shot_segment_anything.py deleted file mode 100644 index 8e64aea639b..00000000000 --- a/src/otx/algo/visual_prompting/zero_shot_segment_anything.py +++ /dev/null @@ -1,834 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""Segment Anything model for the OTX zero-shot visual prompting.""" - -from __future__ import annotations - -import logging as log -import os -from collections import defaultdict -from copy import deepcopy -from itertools import product -from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal - -import torch -import torchvision.transforms.v2 as tvt_v2 -from datumaro import Polygon as dmPolygon -from torch import LongTensor, Tensor, nn -from torch.nn import functional as F # noqa: N812 -from torchvision import tv_tensors -from torchvision.tv_tensors import BoundingBoxes, Image - -from otx.algo.visual_prompting.segment_anything import ( - DEFAULT_CONFIG_SEGMENT_ANYTHING, - SegmentAnything, -) -from otx.core.data.entity.base import OTXBatchLossEntity, Points -from otx.core.data.entity.visual_prompting import ( - ZeroShotVisualPromptingBatchDataEntity, - ZeroShotVisualPromptingBatchPredEntity, -) -from otx.core.model.entity.visual_prompting import OTXVisualPromptingModel - -if TYPE_CHECKING: - import numpy as np - - -class PromptGetter(nn.Module): - """Prompt getter for zero-shot learning.""" - - default_threshold_reference = 0.3 - default_threshold_target = 0.65 - - def __init__(self, image_size: int, downsizing: int = 64) -> None: - super().__init__() - self.image_size = image_size - self.downsizing = downsizing - - self.zero_tensor = torch.tensor(0) - - def set_default_thresholds(self, default_threshold_reference: float, default_threshold_target: float) -> None: - """Set default thresholds.""" - self.default_threshold_reference = default_threshold_reference - self.default_threshold_target = default_threshold_target - - def get_prompt_candidates( - self, - image_embeddings: Tensor, - reference_feats: Tensor, - used_indices: Tensor, - ori_shape: Tensor, - threshold: float = 0.0, - num_bg_points: int = 1, - ) -> tuple[dict[int, Tensor], dict[int, Tensor]]: - """Get prompt candidates.""" - total_points_scores: dict[int, Tensor] = {} - total_bg_coords: dict[int, Tensor] = {} - for label in map(int, used_indices): - points_scores, bg_coords = self( - image_embeddings=image_embeddings, - reference_feat=reference_feats[label], - ori_shape=ori_shape, - threshold=threshold, - num_bg_points=num_bg_points, - ) - - total_points_scores[label] = points_scores - total_bg_coords[label] = bg_coords - - return total_points_scores, total_bg_coords - - def forward( - self, - image_embeddings: Tensor, - reference_feat: Tensor, - ori_shape: Tensor, - threshold: float = 0.0, - num_bg_points: int = 1, - ) -> tuple[Tensor, Tensor]: - """Get prompt candidates from given reference and target features.""" - target_feat = image_embeddings.squeeze() # (256, 64, 64) - c_feat, h_feat, w_feat = target_feat.shape - target_feat = target_feat / target_feat.norm(dim=0, keepdim=True) - target_feat = target_feat.reshape(c_feat, h_feat * w_feat) - - sim = reference_feat @ target_feat - sim = sim.reshape(1, 1, h_feat, w_feat) - sim = ZeroShotSegmentAnything.postprocess_masks(sim, self.image_size, ori_shape) - - threshold = (threshold == 0) * self.default_threshold_target + threshold - points_scores, bg_coords = self._point_selection( - mask_sim=sim[0, 0], - ori_shape=ori_shape, - threshold=threshold, - num_bg_points=num_bg_points, - ) - - return points_scores, bg_coords - - def _point_selection( - self, - mask_sim: Tensor, - ori_shape: Tensor, - threshold: float = 0.0, - num_bg_points: int = 1, - ) -> tuple[Tensor, Tensor]: - """Select point used as point prompts.""" - _, w_sim = mask_sim.shape - - # Top-last point selection - bg_indices = mask_sim.flatten().topk(num_bg_points, largest=False)[1] - bg_x = (bg_indices // w_sim).unsqueeze(0) - bg_y = bg_indices - bg_x * w_sim - bg_coords = torch.cat((bg_y, bg_x), dim=0).permute(1, 0) - bg_coords = bg_coords.to(torch.float32) - - point_coords = torch.where(mask_sim > threshold) - fg_coords_scores = torch.stack(point_coords[::-1] + (mask_sim[point_coords],), dim=0).T - - # to handle empty tensor - len_fg_coords_scores = len(fg_coords_scores) - fg_coords_scores = F.pad(fg_coords_scores, (0, 0, 0, max(0, 1 - len_fg_coords_scores)), value=-1) - - ratio = self.image_size / ori_shape.max() - width = (ori_shape[1] * ratio).to(torch.int64) - n_w = width // self.downsizing - - # get grid numbers - idx_grid = ( - fg_coords_scores[:, 1] * ratio // self.downsizing * n_w + fg_coords_scores[:, 0] * ratio // self.downsizing - ) - idx_grid_unique = torch.unique( - idx_grid.to(torch.int64), - ) # unique op only supports INT64, INT8, FLOAT, STRING in ORT - - # get matched indices - matched_matrix = idx_grid.unsqueeze(-1) == idx_grid_unique # (totalN, uniqueN) - - # sample fg_coords_scores matched by matched_matrix - matched_grid = fg_coords_scores.unsqueeze(1) * matched_matrix.unsqueeze(-1) - - matched_indices = matched_grid[..., -1].topk(k=1, dim=0, largest=True)[1][0].to(torch.int64) - points_scores = matched_grid[matched_indices].diagonal().T - - # sort by the highest score - sorted_points_scores_indices = torch.argsort(points_scores[:, -1], descending=True).to(torch.int64) - points_scores = points_scores[sorted_points_scores_indices] - - return points_scores, bg_coords - - -class ZeroShotSegmentAnything(SegmentAnything): - """Zero-shot learning module using Segment Anything.""" - - def __init__( - self, - default_threshold_reference: float = 0.3, - default_threshold_target: float = 0.65, - *args, - **kwargs, - ) -> None: - msg = "" - if len(kwargs) == 0: - msg += "There isn't any given argument. Default setting will be used." - elif len(kwargs) == 1 and "backbone" in kwargs: - msg += ( - f"There is only backbone (={kwargs.get('backbone')}) argument. " - f"Other parameters will be set along with backbone (={kwargs.get('backbone')})." - ) - elif "backbone" not in kwargs: - msg += "There isn't a backbone argument, it will be reset with backbone=tiny_vit." - if len(msg) > 0: - log.info(msg) - kwargs = self.set_default_config(**kwargs) - - # check freeze conditions - for condition in ["freeze_image_encoder", "freeze_prompt_encoder", "freeze_mask_decoder"]: - if not kwargs.get(condition, False): - log.warning(f"{condition}(=False) must be set to True, changed.") - kwargs[condition] = True - - super().__init__(*args, **kwargs) - - self.prompt_getter = PromptGetter(image_size=self.image_size) - self.prompt_getter.set_default_thresholds( - default_threshold_reference=default_threshold_reference, - default_threshold_target=default_threshold_target, - ) - - self.point_labels_box = torch.tensor([[2, 3]], dtype=torch.float32) - self.has_mask_inputs = [torch.tensor([[0.0]]), torch.tensor([[1.0]])] - - def set_default_config(self, **kwargs) -> dict[str, Any]: - """Set default config when using independently.""" - backbone = kwargs.get("backbone", "tiny_vit") - kwargs.update( - { - "backbone": backbone, - "load_from": kwargs.get("load_from", DEFAULT_CONFIG_SEGMENT_ANYTHING[backbone]["load_from"]), - "freeze_image_encoder": kwargs.get("freeze_image_encoder", True), - "freeze_mask_decoder": kwargs.get("freeze_mask_decoder", True), - "freeze_prompt_encoder": kwargs.get("freeze_prompt_encoder", True), - }, - ) - return kwargs - - def expand_reference_info(self, reference_feats: Tensor, new_largest_label: int) -> Tensor: - """Expand reference info dimensions if newly given processed prompts have more lables.""" - if new_largest_label > (cur_largest_label := len(reference_feats) - 1): - diff = new_largest_label - cur_largest_label - reference_feats = F.pad(reference_feats, (0, 0, 0, 0, 0, diff), value=0.0) - return reference_feats - - @torch.no_grad() - def learn( - self, - images: list[tv_tensors.Image], - processed_prompts: list[dict[int, list[tv_tensors.TVTensor]]], - reference_feats: Tensor, - used_indices: Tensor, - ori_shapes: list[Tensor], - ) -> tuple[dict[str, Tensor], list[Tensor]] | None: - """Get reference features. - - Using given images, get reference features. - These reference features will be used for `infer` to get target results. - Currently, single batch is only supported. - - Args: - images (list[tv_tensors.Image]): List of given images for reference features. - processed_prompts (dict[int, list[tv_tensors.TVTensor]]): The class-wise prompts - processed at OTXZeroShotSegmentAnything._gather_prompts_with_labels. - reference_feats (Tensor): Reference features for target prediction. - used_indices (Tensor): To check which indices of reference features are validate. - ori_shapes (List[Tensor]): List of original shapes per image. - """ - # initialize tensors to contain reference features and prompts - largest_label = max(sum([[int(p) for p in prompt] for prompt in processed_prompts], [])) - reference_feats = self.expand_reference_info(reference_feats, largest_label) - new_used_indices: list[Tensor] = [] - # TODO (sungchul): consider how to handle multiple reference features, currently replace it # noqa: TD003 - - reference_masks: list[Tensor] = [] - for image, prompts, ori_shape in zip(images, processed_prompts, ori_shapes): - image_embeddings = self.image_encoder(image) - processed_embedding = image_embeddings.squeeze().permute(1, 2, 0) - - ref_masks = torch.zeros(largest_label + 1, *map(int, ori_shape)) - for label, input_prompts in prompts.items(): - # TODO (sungchul): how to skip background class # noqa: TD003 - # TODO (sungchul): ensemble multi reference features (current : use merged masks) # noqa: TD003 - ref_mask = torch.zeros(*map(int, ori_shape), dtype=torch.uint8, device=image.device) - for input_prompt in input_prompts: - if isinstance(input_prompt, tv_tensors.Mask): - # directly use annotation information as a mask - ref_mask[ - input_prompt == 1 - ] += 1 # TODO (sungchul): check if the mask is bool or int # noqa: TD003 - else: - if isinstance(input_prompt, BoundingBoxes): - point_coords = input_prompt.reshape(1, 2, 2) - point_labels = torch.tensor([[2, 3]], device=point_coords.device) - elif isinstance(input_prompt, Points): - point_coords = input_prompt.reshape(1, 1, 2) - point_labels = torch.tensor([[1]], device=point_coords.device) - elif isinstance( - input_prompt, - dmPolygon, - ): # TODO (sungchul): add other polygon types # noqa: TD003 - # TODO (sungchul): convert polygon to mask # noqa: TD003 - continue - else: - log.info(f"Current input prompt ({input_prompt.__class__.__name__}) is not supported.") - continue - - masks = self._predict_masks( - mode="learn", - image_embeddings=image_embeddings, - point_coords=point_coords, - point_labels=point_labels, - ori_shape=ori_shape, - is_cascade=False, - ) - ref_mask[masks] += 1 - ref_mask = torch.clip(ref_mask, 0, 1).to(torch.float32) - - ref_feat: Tensor | None = None - default_threshold_reference = deepcopy(self.prompt_getter.default_threshold_reference) - while ref_feat is None: - log.info(f"[*] default_threshold_reference : {default_threshold_reference:.4f}") - ref_feat = self._generate_masked_features( - processed_embedding, - ref_mask, - default_threshold_reference, - ) - default_threshold_reference -= 0.05 - - reference_feats[label] = ref_feat.detach().cpu() - new_used_indices.append(torch.tensor([label])) - ref_masks[label] = ref_mask.detach().cpu() - reference_masks.append(ref_masks) - used_indices = torch.cat((used_indices, *new_used_indices), dim=0).unique() - return {"reference_feats": reference_feats, "used_indices": used_indices}, reference_masks - - @torch.no_grad() - def infer( - self, - images: list[tv_tensors.Image], - reference_feats: Tensor, - used_indices: Tensor, - ori_shapes: list[Tensor], - threshold: float = 0.0, - num_bg_points: int = 1, - is_cascade: bool = False, - ) -> list[list[defaultdict[int, list[Tensor]]]]: - """Zero-shot inference with reference features. - - Get target results by using reference features and target images' features. - - Args: - images (list[tv_tensors.Image]): Given images for target results. - reference_feats (Tensor): Reference features for target prediction. - used_indices (Tensor): To check which indices of reference features are validate. - ori_shapes (list[Tensor]): Original image size. - threshold (float): Threshold to control masked region. Defaults to 0.0. - num_bg_points (1): Number of background points. Defaults to 1. - is_cascade (bool): Whether use cascade inference. Defaults to False. - - Returns: - (list[list[defaultdict[int, list[Tensor]]]]): List of predicted masks and used points. - """ - total_results = [] - for image, ori_shape in zip(images, ori_shapes): - if image.ndim == 3: - image = image.unsqueeze(0) # noqa: PLW2901 - - # get image embeddings - image_embeddings = self.image_encoder(image) - - total_points_scores, total_bg_coords = self.prompt_getter.get_prompt_candidates( - image_embeddings=image_embeddings, - reference_feats=reference_feats, - used_indices=used_indices, - ori_shape=ori_shape, - threshold=threshold, - num_bg_points=num_bg_points, - ) - predicted_masks: defaultdict = defaultdict(list) - used_points: defaultdict = defaultdict(list) - for label in total_points_scores: - points_scores, bg_coords = total_points_scores[label], total_bg_coords[label] - for point_score in points_scores: - x, y = point_score[:2] - is_done = False - for pm in predicted_masks.get(label, []): - # check if that point is already assigned - if pm[int(y), int(x)] > 0: - is_done = True - break - if is_done: - continue - - point_coords = torch.cat((point_score[:2].unsqueeze(0), bg_coords), dim=0).unsqueeze(0) - point_coords = self._preprocess_coords(point_coords, ori_shape, self.image_size) - point_labels = torch.tensor( - [1] + [0] * len(bg_coords), - dtype=torch.float32, - device=point_coords.device, - ).unsqueeze(0) - mask = self._predict_masks( - mode="infer", - image_embeddings=image_embeddings, - point_coords=point_coords, - point_labels=point_labels, - ori_shape=ori_shape, - is_cascade=is_cascade, - ) - predicted_masks[label].append(mask * point_score[2]) - used_points[label].append(point_score) - - # check overlapping area between different label masks - self._inspect_overlapping_areas(predicted_masks, used_points) - total_results.append([predicted_masks, used_points]) - return total_results - - def _inspect_overlapping_areas( - self, - predicted_masks: dict[int, list[Tensor]], - used_points: dict[int, list[Tensor]], - threshold_iou: float = 0.8, - ) -> None: - def _calculate_mask_iou(mask1: Tensor, mask2: Tensor) -> tuple[float, Tensor | None]: - if (union := torch.logical_or(mask1, mask2).sum().item()) == 0: - # Avoid division by zero - return 0.0, None - intersection = torch.logical_and(mask1, mask2) - return intersection.sum().item() / union, intersection - - for (label, masks), (other_label, other_masks) in product(predicted_masks.items(), predicted_masks.items()): - if other_label <= label: - continue - - overlapped_label = [] - overlapped_other_label = [] - for (im, mask), (jm, other_mask) in product(enumerate(masks), enumerate(other_masks)): - _mask_iou, _intersection = _calculate_mask_iou(mask, other_mask) - if _mask_iou > threshold_iou: - # compare overlapped regions between different labels and filter out the lower score - if used_points[label][im][2] > used_points[other_label][jm][2]: - overlapped_other_label.append(jm) - else: - overlapped_label.append(im) - elif _mask_iou > 0: - # refine the slightly overlapping region - overlapped_coords = torch.where(_intersection) - if used_points[label][im][2] > used_points[other_label][jm][2]: - other_mask[overlapped_coords] = 0.0 - else: - mask[overlapped_coords] = 0.0 - - for im in sorted(list(set(overlapped_label)), reverse=True): # noqa: C414 - masks.pop(im) - used_points[label].pop(im) - - for jm in sorted(list(set(overlapped_other_label)), reverse=True): # noqa: C414 - other_masks.pop(jm) - used_points[other_label].pop(jm) - - def _predict_masks( - self, - mode: str, - image_embeddings: Tensor, - point_coords: Tensor, - point_labels: Tensor, - ori_shape: Tensor, - is_cascade: bool = True, - ) -> Tensor: - """Predict target masks.""" - masks: Tensor - logits: Tensor - scores: Tensor - num_iter = 3 if is_cascade else 1 - for i in range(num_iter): - if i == 0: - # First-step prediction - mask_input = torch.zeros( - 1, - 1, - *(x * 4 for x in image_embeddings.shape[2:]), - device=image_embeddings.device, - ) - has_mask_input = self.has_mask_inputs[0].to(mask_input.device) - - elif i == 1: - # Cascaded Post-refinement-1 - mask_input, best_masks = self._decide_cascade_results(masks, logits, scores, is_single=True) # noqa: F821 - if best_masks.sum() == 0: - return best_masks - - has_mask_input = self.has_mask_inputs[1].to(mask_input.device) - - elif i == 2: - # Cascaded Post-refinement-2 - mask_input, best_masks = self._decide_cascade_results(masks, logits, scores) # noqa: F821 - if best_masks.sum() == 0: - return best_masks - - has_mask_input = self.has_mask_inputs[1].to(mask_input.device) - coords = torch.nonzero(best_masks) - y, x = coords[:, 0], coords[:, 1] - box_coords = self._preprocess_coords( - torch.tensor([[[x.min(), y.min()], [x.max(), y.max()]]], dtype=torch.float32, device=coords.device), - ori_shape, - self.image_size, - ) - point_coords = torch.cat((point_coords, box_coords), dim=1) - point_labels = torch.cat((point_labels, self.point_labels_box.to(point_labels.device)), dim=1) - - high_res_masks, scores, logits = self( - mode=mode, - image_embeddings=image_embeddings, - point_coords=point_coords, - point_labels=point_labels, - mask_input=mask_input, - has_mask_input=has_mask_input, - ori_shape=ori_shape, - ) - masks = high_res_masks > self.mask_threshold - _, best_masks = self._decide_cascade_results(masks, logits, scores) - return best_masks - - def _preprocess_coords( - self, - coords: Tensor, - ori_shape: list[int] | tuple[int, int] | Tensor, - target_length: int, - ) -> Tensor: - """Expects a torch tensor of length 2 in the final dimension. - - Requires the original image size in (H, W) format. - - Args: - coords (Tensor): Coordinates tensor. - ori_shape (List[int] | Tuple[int, int] | Tensor]): Original size of image. - target_length (int): The length of the longest side of the image. - - Returns: - (Tensor): Resized coordinates. - """ - old_h, old_w = ori_shape - new_h, new_w = self.get_prepadded_size(ori_shape, target_length) - coords[..., 0] = coords[..., 0] * (new_w / old_w) - coords[..., 1] = coords[..., 1] * (new_h / old_h) - return coords - - def _generate_masked_features( - self, - feats: Tensor, - masks: Tensor, - threshold_mask: float, - ) -> tuple[Tensor, ...] | None: - """Generate masked features. - - Args: - feats (Tensor): Raw reference features. It will be filtered with masks. - masks (Tensor): Reference masks used to filter features. - threshold_mask (float): Threshold to control masked region. - - Returns: - (Tensor): Masked features. - """ - scale_factor = self.image_size / max(masks.shape) - - # Post-process masks - masks = F.interpolate(masks.unsqueeze(0).unsqueeze(0), scale_factor=scale_factor, mode="bilinear").squeeze() - masks = self.pad_to_square(masks) - masks = F.interpolate(masks.unsqueeze(0).unsqueeze(0), size=feats.shape[0:2], mode="bilinear").squeeze() - - # Target feature extraction - if (masks > threshold_mask).sum() == 0: - # (for stability) there is no area to be extracted - return None - - masked_feat = feats[masks > threshold_mask] - masked_feat = masked_feat.mean(0).unsqueeze(0) - return masked_feat / masked_feat.norm(dim=-1, keepdim=True) - - def pad_to_square(self, x: Tensor) -> Tensor: - """Pad to a square input. - - Args: - x (Tensor): Mask to be padded. - - Returns: - (Tensor): Padded mask. - """ - h, w = x.shape[-2:] - padh = self.image_size - h - padw = self.image_size - w - return F.pad(x, (0, padw, 0, padh)) - - def _decide_cascade_results( - self, - masks: Tensor, - logits: Tensor, - scores: Tensor, - is_single: bool = False, - ) -> tuple[Tensor, Tensor]: - """Post-process masks for cascaded post-refinements.""" - if is_single: - best_idx = 0 - else: - # skip the first index components - scores, masks, logits = (x[:, 1:] for x in (scores, masks, logits)) - - # filter zero masks - while len(scores[0]) > 0 and masks[0, (best_idx := torch.argmax(scores[0]))].sum() == 0: - scores, masks, logits = ( - torch.cat((x[:, :best_idx], x[:, best_idx + 1 :]), dim=1) for x in (scores, masks, logits) - ) - - if len(scores[0]) == 0: - # all predicted masks were zero masks, ignore them. - return None, torch.zeros(masks.shape[-2:], device="cpu") - - best_idx = torch.argmax(scores[0]) - return logits[:, best_idx], masks[0, best_idx] - - -class OTXZeroShotSegmentAnything(OTXVisualPromptingModel): - """Zero-Shot Visual Prompting model.""" - - def __init__( - self, - backbone: Literal["tiny_vit", "vit_b"], - num_classes: int = 0, - root_reference_info: Path | str = "vpm_zsl_reference_infos", - save_outputs: bool = True, - is_cascade: bool = False, - pixel_mean: list[float] | None = [123.675, 116.28, 103.53], # noqa: B006 - pixel_std: list[float] | None = [58.395, 57.12, 57.375], # noqa: B006 - **kwargs, - ): - self.config = {"backbone": backbone, **DEFAULT_CONFIG_SEGMENT_ANYTHING[backbone], **kwargs} - super().__init__(num_classes=num_classes) - - self.save_outputs = save_outputs - self.root_reference_info: Path = Path(root_reference_info) - self.is_cascade = is_cascade - - self.register_buffer("pixel_mean", Tensor(pixel_mean).view(-1, 1, 1), False) - self.register_buffer("pixel_std", Tensor(pixel_std).view(-1, 1, 1), False) - - def _create_model(self) -> nn.Module: - """Create a PyTorch model for this class.""" - return ZeroShotSegmentAnything(**self.config) - - def forward( # type: ignore[override] - self, - inputs: ZeroShotVisualPromptingBatchDataEntity, # type: ignore[override] - ) -> ZeroShotVisualPromptingBatchPredEntity | OTXBatchLossEntity: - """Model forward function.""" - forward_fn = self.learn if self.training else self.infer - return forward_fn(inputs) # type: ignore[operator] - - def learn( - self, - inputs: ZeroShotVisualPromptingBatchDataEntity, - reset_feat: bool = False, - ) -> ZeroShotVisualPromptingBatchPredEntity | OTXBatchLossEntity: - """Learn to directly connect to the model.""" - if reset_feat: - self.initialize_reference_info() - - outputs = self.model.learn(**self._customize_inputs(inputs)) - return self._customize_outputs(outputs, inputs) - - def infer( - self, - inputs: ZeroShotVisualPromptingBatchDataEntity, - ) -> ZeroShotVisualPromptingBatchPredEntity | OTXBatchLossEntity: - """Infer to directly connect to the model.""" - outputs = self.model.infer(**self._customize_inputs(inputs)) - return self._customize_outputs(outputs, inputs) - - def _customize_inputs(self, inputs: ZeroShotVisualPromptingBatchDataEntity) -> dict[str, Any]: # type: ignore[override] - """Customize the inputs for the model.""" - inputs = self.transforms(inputs) - forward_inputs = { - "images": [tv_tensors.wrap(image.unsqueeze(0), like=image) for image in inputs.images], - "reference_feats": self.reference_feats, - "used_indices": self.used_indices, - "ori_shapes": [torch.tensor(info.ori_shape) for info in inputs.imgs_info], - } - if self.training: - # learn - forward_inputs.update( - {"processed_prompts": self._gather_prompts_with_labels(inputs.prompts, inputs.labels)}, - ) - else: - # infer - forward_inputs.update({"is_cascade": self.is_cascade}) - - return forward_inputs - - def _customize_outputs( # type: ignore[override] - self, - outputs: Any, # noqa: ANN401 - inputs: ZeroShotVisualPromptingBatchDataEntity, # type: ignore[override] - ) -> ZeroShotVisualPromptingBatchPredEntity | OTXBatchLossEntity: - """Customize OTX output batch data entity if needed for you model.""" - if self.training: - self.reference_feats = outputs[0].get("reference_feats") - self.used_indices = outputs[0].get("used_indices") - return outputs - - masks: list[tv_tensors.Mask] = [] - prompts: list[Points] = [] - scores: list[Tensor] = [] - labels: list[LongTensor] = [] - for predicted_masks, used_points in outputs: - for label, predicted_mask in predicted_masks.items(): - if len(predicted_mask) == 0: - continue - masks.append(tv_tensors.Mask(torch.stack(predicted_mask, dim=0), dtype=torch.float32)) - prompts.append( - Points( - torch.stack([p[:2] for p in used_points[label]], dim=0), - canvas_size=inputs.imgs_info[0].ori_shape, - dtype=torch.float32, - ), - ) - scores.append(torch.stack([p[2] for p in used_points[label]], dim=0)) - labels.append(torch.stack([LongTensor([label]) for _ in range(scores[-1].shape[0])], dim=0)) - - return ZeroShotVisualPromptingBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - prompts=prompts, - masks=masks, - polygons=[], - labels=labels, - ) - - def _gather_prompts_with_labels( - self, - prompts: list[list[tv_tensors.TVTensor]], - labels: list[Tensor], - ) -> list[dict[int, list[tv_tensors.TVTensor]]]: - """Gather prompts according to labels.""" - total_processed_prompts: list[dict[int, list[tv_tensors.TVTensor]]] = [] - for prompt, label in zip(prompts, labels): - processed_prompts = defaultdict(list) - for _prompt, _label in zip(prompt, label): - processed_prompts[int(_label)].append(_prompt) - sorted_processed_prompts = dict(sorted(processed_prompts.items(), key=lambda x: x)) - total_processed_prompts.append(sorted_processed_prompts) - return total_processed_prompts - - def apply_image(self, image: tv_tensors.Image | np.ndarray, target_length: int = 1024) -> tv_tensors.Image: - """Preprocess image to be used in the model.""" - h, w = image.shape[-2:] - target_size = self.get_preprocess_shape(h, w, target_length) - return tvt_v2.functional.resize(tvt_v2.functional.to_image(image), target_size, antialias=True) - - def apply_coords(self, coords: Tensor, ori_shape: tuple[int, ...], target_length: int = 1024) -> Tensor: - """Preprocess points to be used in the model.""" - old_h, old_w = ori_shape - new_h, new_w = self.get_preprocess_shape(ori_shape[0], ori_shape[1], target_length) - coords = deepcopy(coords).to(torch.float32) - coords[..., 0] = coords[..., 0] * (new_w / old_w) - coords[..., 1] = coords[..., 1] * (new_h / old_h) - return coords - - def apply_points(self, points: Points, ori_shape: tuple[int, ...], target_length: int = 1024) -> Points: - """Preprocess points to be used in the model.""" - return Points(self.apply_coords(points, ori_shape, target_length), canvas_size=(target_length, target_length)) - - def apply_boxes(self, boxes: BoundingBoxes, ori_shape: tuple[int, ...], target_length: int = 1024) -> BoundingBoxes: - """Preprocess boxes to be used in the model.""" - return BoundingBoxes( - self.apply_coords(boxes.reshape(-1, 2, 2), ori_shape, target_length).reshape(-1, 4), - format=boxes.format, - canvas_size=(target_length, target_length), - ) - - def apply_prompts( - self, - prompts: list[Points | BoundingBoxes], - ori_shape: tuple[int, ...], - target_length: int = 1024, - ) -> list[Points | BoundingBoxes]: - """Preprocess prompts to be used in the model.""" - transformed_prompts: list[Points | BoundingBoxes] = [] - for prompt in prompts: - if isinstance(prompt, Points): - transformed_prompts.append(self.apply_points(prompt, ori_shape, target_length)) - elif isinstance(prompt, BoundingBoxes): - transformed_prompts.append(self.apply_boxes(prompt, ori_shape, target_length)) - else: - log.info(f"Current prompt ({prompt.__class__.__name__}) is not supported, saved as it is.") - transformed_prompts.append(prompt) - return transformed_prompts - - def get_preprocess_shape(self, oldh: int, oldw: int, target_length: int) -> tuple[int, int]: - """Get preprocess shape.""" - scale = target_length * 1.0 / max(oldh, oldw) - newh, neww = oldh * scale, oldw * scale - neww = int(neww + 0.5) - newh = int(newh + 0.5) - return (newh, neww) - - def preprocess(self, x: Image) -> Image: - """Normalize pixel values and pad to a square input.""" - # Normalize colors - x = (x - self.pixel_mean) / self.pixel_std - - # Pad - x = self.model.pad_to_square(x) - return Image(x) - - def transforms(self, entity: ZeroShotVisualPromptingBatchDataEntity) -> ZeroShotVisualPromptingBatchDataEntity: - """Transforms for ZeroShotVisualPromptingBatchDataEntity.""" - entity.images = [self.preprocess(self.apply_image(image)) for image in entity.images] - entity.prompts = [ - self.apply_prompts(prompt, info.ori_shape, self.model.image_size) - for prompt, info in zip(entity.prompts, entity.imgs_info) - ] - return entity - - def initialize_reference_info(self) -> None: - """Initialize reference information.""" - self.register_buffer("reference_feats", torch.zeros(0, 1, self.model.embed_dim), False) - self.register_buffer("used_indices", torch.tensor([], dtype=torch.int64), False) - - def _find_latest_reference_info(self, root: Path) -> str | None: - """Find latest reference info to be used.""" - if not Path.is_dir(root): - return None - if len(stamps := sorted(os.listdir(root), reverse=True)) > 0: - return stamps[0] - return None - - def load_latest_reference_info(self, device: str | torch.device = "cpu") -> bool: - """Load latest reference info to be used.""" - if (latest_stamp := self._find_latest_reference_info(self.root_reference_info)) is not None: - latest_reference_info = self.root_reference_info / latest_stamp / "reference_info.pt" - reference_info = torch.load(latest_reference_info) - self.register_buffer( - "reference_feats", - reference_info.get("reference_feats", torch.zeros(0, 1, self.model.embed_dim)).to(device), - False, - ) - self.register_buffer( - "used_indices", - reference_info.get("used_indices", torch.tensor([], dtype=torch.int64)).to(device), - False, - ) - log.info(f"reference info saved at {latest_reference_info} was successfully loaded.") - return True - return False diff --git a/src/otx/algorithms/__init__.py b/src/otx/algorithms/__init__.py new file mode 100644 index 00000000000..daf814e52b2 --- /dev/null +++ b/src/otx/algorithms/__init__.py @@ -0,0 +1,6 @@ +"""OTX Algorithms.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +TRANSFORMER_BACKBONES = ["VisionTransformer", "T2T_ViT", "Conformer"] diff --git a/src/otx/algorithms/action/__init__.py b/src/otx/algorithms/action/__init__.py new file mode 100644 index 00000000000..3968aaa2d92 --- /dev/null +++ b/src/otx/algorithms/action/__init__.py @@ -0,0 +1,15 @@ +"""OTX Algorithms - Action Recognition.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import os + +os.environ["FEATURE_FLAGS_OTX_ACTION_TASKS"] = "1" + +MMACTION_AVAILABLE = True + +try: + import mmaction # noqa: F401 +except ImportError: + MMACTION_AVAILABLE = False diff --git a/src/otx/algorithms/action/adapters/__init__.py b/src/otx/algorithms/action/adapters/__init__.py new file mode 100644 index 00000000000..88fd0c69d7c --- /dev/null +++ b/src/otx/algorithms/action/adapters/__init__.py @@ -0,0 +1,19 @@ +"""Adapters for Action Recognition.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .openvino import OTXOVActionCls, OTXOVActionDet + +__all__ = ["OTXOVActionCls", "OTXOVActionDet"] diff --git a/src/otx/algorithms/action/adapters/mmaction/__init__.py b/src/otx/algorithms/action/adapters/mmaction/__init__.py new file mode 100644 index 00000000000..79b7026756b --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/__init__.py @@ -0,0 +1,12 @@ +"""OTX Adapters - mmaction2.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .data import OTXActionClsDataset, OTXActionDetDataset +from .models import register_action_backbones +from .utils import Exporter + +__all__ = ["OTXActionClsDataset", "OTXActionDetDataset", "Exporter"] + +register_action_backbones() diff --git a/src/otx/algorithms/action/adapters/mmaction/data/__init__.py b/src/otx/algorithms/action/adapters/mmaction/data/__init__.py new file mode 100644 index 00000000000..ad37d4a0867 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/data/__init__.py @@ -0,0 +1,21 @@ +"""OTX Algorithms - Action Classification Dataset.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .cls_dataset import OTXActionClsDataset +from .det_dataset import OTXActionDetDataset +from .pipelines import RawFrameDecode + +__all__ = ["OTXActionClsDataset", "OTXActionDetDataset", "RawFrameDecode"] diff --git a/src/otx/algorithms/action/adapters/mmaction/data/cls_dataset.py b/src/otx/algorithms/action/adapters/mmaction/data/cls_dataset.py new file mode 100644 index 00000000000..903dad76036 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/data/cls_dataset.py @@ -0,0 +1,138 @@ +"""Base MMDataset for Action Recognition Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from copy import copy +from typing import Any, Dict, List, Sequence + +import numpy as np +from mmaction.datasets.builder import DATASETS +from mmaction.datasets.pipelines import Compose +from mmaction.datasets.rawframe_dataset import RawframeDataset + +from otx.algorithms.action.adapters.mmaction.data.pipelines import RawFrameDecode +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import LabelEntity + + +# pylint: disable=too-many-instance-attributes +@DATASETS.register_module() +class OTXActionClsDataset(RawframeDataset): + """Wrapper that allows using a OTX dataset to train mmaction models. + + This wrapper is not based on the filesystem, + but instead loads the items here directly from the OTX DatasetEntity object. + """ + + class _DataInfoProxy: + def __init__(self, otx_dataset: DatasetEntity, labels: List[LabelEntity], modality: str): + self.otx_dataset = otx_dataset + self.labels = labels + self.label_idx = {label.id: i for i, label in enumerate(labels)} + self.modality = modality + self.video_info: Dict[str, Any] = {} + self._update_meta_data() + + def __len__(self) -> int: + return len(self.video_info) + + def _update_meta_data(self): + """Update video metadata of each item in self.otx_dataset. + + This function assumes that DatasetItemEntities in DatasetEntity are sorted by video id and frame idx + During iterating DatasetsetEntitiy, this fucnction generates video_info(dictionary) + video_info records metadata for each video, and it contains + - total_frame: Total frame number of the video, this value will be used to sample frames for training + - start_index: Offset for the video, this value will be added to sampled frame indices for the video + - label: Action category of the video + - modality = Modality of data, 'RGB' or 'Flow(Optical Flow)' + """ + video_info = {} + start_index = 0 + for idx, item in enumerate(self.otx_dataset): + metadata = item.get_metadata()[0].data + if metadata.video_id in video_info: + video_info[metadata.video_id]["total_frames"] += 1 + video_info[metadata.video_id]["start_index"] = start_index + else: + if len(item.get_annotations()) > 0: + label = int(item.get_roi_labels(self.labels)[0].id) + else: + label = None + ignored_labels = np.array([self.label_idx[label.id] for label in item.ignored_labels]) + video_info[metadata.video_id] = { + "total_frames": 1, + "start_index": idx, + "label": label, + "ignored_labels": ignored_labels, + "modality": self.modality, + } + start_index = idx + + self.video_info.update(video_info) + + def __getitem__(self, index: int) -> Dict[str, Any]: + """Prepare training data item. + + Action classification needs video for training, therefore this function generate item from video_info + """ + + item = self.video_info[list(self.video_info.keys())[index]] + return item + + # pylint: disable=too-many-arguments, invalid-name, super-init-not-called + # TODO Check need for additional params such as multi_class, with_offset + def __init__( + self, + otx_dataset: DatasetEntity, + labels: List[LabelEntity], + pipeline: Sequence[dict], + test_mode: bool = False, + modality: str = "RGB", # [RGB, FLOW(Optical flow)] + ): + self.otx_dataset = otx_dataset + self.labels = labels + self.CLASSES = [label.name for label in labels] + self.test_mode = test_mode + self.modality = modality + + self.video_infos = OTXActionClsDataset._DataInfoProxy(otx_dataset, labels, modality) + + self.pipeline = Compose(pipeline) + for transform in self.pipeline.transforms: + if isinstance(transform, RawFrameDecode): + transform.otx_dataset = self.otx_dataset + + def __len__(self) -> int: + """Return length of dataset.""" + return len(self.video_infos) + + def prepare_train_frames(self, idx: int) -> Dict[str, Any]: + """Get training data and annotations after pipeline. + + Args: + idx (int): Index of data + """ + item = copy(self.video_infos[idx]) # Copying dict(), not contents + return self.pipeline(item) + + def prepare_test_frames(self, idx: int) -> Dict[str, Any]: + """Get testing data after pipeline. + + Args: + idx (int): Index of data + """ + item = copy(self.video_infos[idx]) # Copying dict(), not contents + return self.pipeline(item) diff --git a/src/otx/algorithms/action/adapters/mmaction/data/det_dataset.py b/src/otx/algorithms/action/adapters/mmaction/data/det_dataset.py new file mode 100644 index 00000000000..038e296e938 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/data/det_dataset.py @@ -0,0 +1,323 @@ +"""Adapt AVADataset in mmaction2 into OTXDataset.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import os +from collections import defaultdict +from copy import copy, deepcopy +from logging import Logger +from typing import Any, Dict, List, Sequence, Tuple + +import mmcv +import numpy as np +from mmaction.core.evaluation.ava_utils import det2csv +from mmaction.datasets.ava_dataset import AVADataset +from mmaction.datasets.builder import DATASETS +from mmaction.datasets.pipelines import Compose +from mmcv.utils import print_log + +from otx.algorithms.action.adapters.mmaction.data.pipelines import RawFrameDecode +from otx.algorithms.action.adapters.mmaction.utils import det_eval +from otx.api.entities.annotation import Annotation +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import LabelEntity +from otx.api.entities.metadata import VideoMetadata +from otx.api.utils.shape_factory import ShapeFactory +from otx.utils.logger import get_logger + +root_logger = get_logger() + + +# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-locals, super-init-not-called +@DATASETS.register_module() +class OTXActionDetDataset(AVADataset): + """Wrapper that allows using a OTX dataset to train action detection models. + + This wrapper is not based on the filesystem, + but instead loads the items here directly from the OTX DatasetEntity object. + It is adapted from AVADataset of mmaction, but it supports other dataset such as UCF and JHMDB. + """ + + class _DataInfoProxy: + def __init__( + self, + otx_dataset: DatasetEntity, + labels: List[LabelEntity], + person_det_score_thr: float = 0.9, + num_max_proposals: int = 1000, + modality: str = "RGB", + fps: int = 30, + ): + self.otx_dataset = deepcopy(otx_dataset) + self.labels = labels + self.label_idx = {label.id: i for i, label in enumerate(labels)} + self.person_det_score_thr = person_det_score_thr + self.num_max_proposals = num_max_proposals + self.modality = modality + self.fps = fps + self.data_root = "/" + os.path.join(*os.path.abspath(str(self.otx_dataset[0].media.path)).split("/")[:-4]) + self.proposal_file_name = os.path.abspath(str(self.otx_dataset[0].media.path)).split("/")[-4] + self.proposal_file = os.path.join(self.data_root, f"{self.proposal_file_name}.pkl") + self.video_info: Dict[str, Any] = {} + + if os.path.exists(self.proposal_file): + self.proposals = mmcv.load(self.proposal_file) + self._patch_proposals() + else: + self.proposals = None + + self._update_meta_data() + + def __len__(self) -> int: + return len(self.otx_dataset) + + def _update_meta_data(self): + """Update video metadata of each item in self.otx_dataset. + + During iterating DatasetEntity, this function generate video_info(dictionary) to record metadata of video + After that, this function update metadata of each DatasetItemEntity of DatasetEntity + - start_index: Offset for the video, this value will be added to sampled frame indices + - timestamp: Timestamp of the DatasetItemEntity + - timestamp_start: Start timestamp of the video, this will be used to generate shot_info + - timestamp_end: End timestamp of the video, this will be used to generate shot_info + - shot_info = (0, (timestamp_end - timestamp_start)) * self.fps: + Range of frame indices, this is used to sample frame indices + - img_key = "video_id,frame_idx": key of pre-proposal dictionary + - modality = Modality of data, 'RGB' or 'Flow(Optical Flow)' + This function removes empty frames(frame with no action), since they are only used for background of clips + """ + + video_info = {} + start_index = 0 + for idx, item in enumerate(self.otx_dataset): + metadata = item.get_metadata()[0].data + if metadata.video_id in video_info: + video_info[metadata.video_id]["start_index"] = start_index + if metadata.frame_idx < video_info[metadata.video_id]["timestamp_start"]: + video_info[metadata.video_id]["timestamp_start"] = metadata.frame_idx + if metadata.frame_idx > video_info[metadata.video_id]["timestamp_end"]: + video_info[metadata.video_id]["timestamp_end"] = metadata.frame_idx + else: + video_info[metadata.video_id] = { + "start_index": idx, + "timestamp_start": metadata.frame_idx, + "timestamp_end": metadata.frame_idx, + } + start_index = idx + + remove_indices = [] + for idx, item in enumerate(self.otx_dataset): + metadata = item.get_metadata()[0].data + if metadata.is_empty_frame: + remove_indices.append(idx) + continue + + for key, value in video_info[metadata.video_id].items(): + metadata.update(key, value) + + shot_info = (0, (metadata.timestamp_end - metadata.timestamp_start)) * self.fps + img_key = f"{metadata.video_id},{metadata.frame_idx}" + ignored_labels = np.array([self.label_idx[label.id] for label in item.ignored_labels]) + metadata.update("shot_info", shot_info) + metadata.update("img_key", img_key) + metadata.update("timestamp", metadata.frame_idx) + metadata.update("ignored_labels", ignored_labels) + metadata.update("modality", self.modality) + + anns = item.get_annotations() + self._update_annotations(metadata, anns) + + self.otx_dataset.remove_at_indices(remove_indices) + self.video_info.update(video_info) + + def __getitem__(self, index: int) -> Dict[str, Any]: + """Prepare a dict 'data_info' that is expected by the mmaction pipeline to handle images and annotations. + + :return data_info: dictionary that contains the image and image metadata, as well as the labels of + the objects in the image + This iterates self.otx_dataset, which removes empty frames + """ + + item = self.otx_dataset[index] + metadata = item.get_metadata()[0].data + + data_info = dict( + **metadata.metadata, + ann_info=dict(label_list=self.labels), + fps=self.fps, + ) + + return data_info + + def _patch_proposals(self): + """Remove fixed string format. + + AVA dataset pre-proposals have fixed string format. + Fixed string format have scalability issues so here we remove it + """ + # FIXME This may consume lots of time depends on size of proposals + root_logger.info("Patching pre proposals...") + for img_key in list(self.proposals): + proposal = self.proposals.pop(img_key) + new_img_key = img_key.split(",")[0] + "," + str(int(img_key.split(",")[1])) + self.proposals[f"{new_img_key}"] = proposal + root_logger.info("Done.") + + def _update_annotations(self, metadata: VideoMetadata, anns: List[Annotation]): + """Update annotation information to item's metadata.""" + if len(anns) > 0: + bboxes, labels = [], [] + for ann in anns: + rectangle = ShapeFactory.shape_as_rectangle(ann.shape) + bbox = np.asarray([rectangle.x1, rectangle.y1, rectangle.x2, rectangle.y2]) + valid_labels = np.array([int(label.id) for label in ann.get_labels()], dtype=int) + label = np.zeros(len(self.labels) + 1, dtype=np.float32) + label[valid_labels] = 1.0 + bboxes.append(bbox) + labels.append(label) + metadata.update("gt_bboxes", np.stack(bboxes)) + metadata.update("gt_labels", np.stack(labels)) + else: + # Insert dummy gt bboxes for data pipeline in mmaction + metadata.update("gt_bboxes", np.zeros((1, 4))) + + if self.proposals is not None: + if metadata.img_key in self.proposals: # type: ignore[attr-defined] + proposals = self.proposals[metadata.img_key] # type: ignore[attr-defined] + else: + proposals = np.array([[0, 0, 1, 1, 1]]) + thr = min(self.person_det_score_thr, max(proposals[:, 4])) + positive_inds = proposals[:, 4] >= thr + proposals = proposals[positive_inds] + proposals = proposals[: self.num_max_proposals] + metadata.update("proposals", proposals[:, :4]) + metadata.update("scores", proposals[:, 4]) + + # TODO Remove duplicated codes with mmaction's AVADataset + def __init__( + self, + otx_dataset: DatasetEntity, + labels: List[LabelEntity], + pipeline: Sequence[dict], + test_mode: bool = False, + person_det_score_thr: float = 0.9, + num_max_proposals: int = 1000, + modality: str = "RGB", + fps: int = 30, + ): + self.otx_dataset = otx_dataset + self.labels = labels + self.CLASSES = [label.name for label in labels] + self.test_mode = test_mode + self.modality = modality + self._FPS = fps + self.person_det_score_thr = person_det_score_thr + self.num_max_proposals = num_max_proposals + + # OTX does not support custom_classes + self.custom_classes = None + + self.video_infos = OTXActionDetDataset._DataInfoProxy( + otx_dataset, labels, person_det_score_thr, num_max_proposals, modality, fps + ) + + self.pipeline = Compose(pipeline) + for transform in self.pipeline.transforms: + if isinstance(transform, RawFrameDecode): + transform.otx_dataset = self.otx_dataset + + # TODO. Handle exclude file for AVA dataset + self.exclude_file = None + + def prepare_train_frames(self, idx: int) -> Dict[str, Any]: + """Get training data and annotations after pipeline. + + Args: + idx (int): Index of data + """ + item = copy(self.video_infos[idx]) # Copying dict(), not contents + return self.pipeline(item) + + def prepare_test_frames(self, idx: int) -> Dict[str, Any]: + """Get testing data after pipeline. + + Args: + idx (int): Index of data + """ + item = copy(self.video_infos[idx]) # Copying dict(), not contents + return self.pipeline(item) + + # pylint: disable=too-many-locals, unused-argument + def evaluate( + self, + results: List[List[np.ndarray]], + metrics: Tuple[str] = ("mAP",), + logger: Logger = None, + **kwargs, + ): + """Evaluate the prediction results and report mAP.""" + assert len(metrics) == 1 and metrics[0] == "mAP", ( + 'For evaluation on AVADataset, you need to use metrics "mAP" ' + "See https://github.com/open-mmlab/mmaction2/pull/567 " + "for more info." + ) + csv_results = det2csv(self, results, self.custom_classes) + predictions = self._get_predictions(csv_results) + + ret = {} + for metric in metrics: + msg = f"Evaluating {metric} ..." + if logger is None: + msg = "\n" + msg + print_log(msg, logger=logger) + + eval_result = det_eval( + predictions, + metric, + self.labels, + self.video_infos, + self.exclude_file, + custom_classes=self.custom_classes, + ) + log_msg = [] + for key, value in eval_result.items(): + log_msg.append(f"\n{key}\t{value: .4f}") + str_log_msg = "".join(log_msg) + print_log(str_log_msg, logger=logger) + ret.update(eval_result) + return ret + + @staticmethod + def _get_predictions(csv_results: List[Tuple]): + """Convert model's inference results to predictions.""" + np_csv_results = np.array(csv_results) + _img_keys = np_csv_results[:, :2] + _boxes = np_csv_results[:, 2:6] + _labels = np_csv_results[:, 6] + _scores = np_csv_results[:, 7] + + boxes = defaultdict(list) + labels = defaultdict(list) + scores = defaultdict(list) + + for _img_key, _box, _label, _score in zip(_img_keys, _boxes, _labels, _scores): + img_key = _img_key[0] + "," + _img_key[1] + box = _box.astype("float") + label = _label.astype("int") + score = _score.astype("float") + boxes[img_key].append(box) + labels[img_key].append(label) + scores[img_key].append(score) + return (boxes, labels, scores) diff --git a/src/otx/algorithms/action/adapters/mmaction/data/pipelines/__init__.py b/src/otx/algorithms/action/adapters/mmaction/data/pipelines/__init__.py new file mode 100644 index 00000000000..aaca86692ae --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/data/pipelines/__init__.py @@ -0,0 +1,19 @@ +"""Collection of data pipelines for OTX Action Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .loading import RawFrameDecode + +__all__ = ["RawFrameDecode"] diff --git a/src/otx/algorithms/action/adapters/mmaction/data/pipelines/loading.py b/src/otx/algorithms/action/adapters/mmaction/data/pipelines/loading.py new file mode 100644 index 00000000000..4db0a1f2be1 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/data/pipelines/loading.py @@ -0,0 +1,57 @@ +"""Collection of video loading data pipelines for OTX Action Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from typing import Any, Dict + +import numpy as np +from mmaction.datasets.builder import PIPELINES + +from otx.api.entities.datasets import DatasetEntity + + +@PIPELINES.register_module(force=True) +class RawFrameDecode: + """Load and decode frames with given indices.""" + + otx_dataset: DatasetEntity + + def __call__(self, results: Dict[str, Any]) -> Dict[str, Any]: + """Call function of RawFrameDecode.""" + results = self._decode_from_list(results) + return results + + def _decode_from_list(self, results: Dict[str, Any]): + """Generate numpy array list from list of DatasetItemEntity.""" + imgs = [] + for index in results["frame_inds"]: + imgs.append(self.otx_dataset[int(index)].media.numpy) + results["imgs"] = imgs + results["original_shape"] = imgs[0].shape[:2] + results["img_shape"] = imgs[0].shape[:2] + + # we resize the gt_bboxes and proposals to their real scale + if "gt_bboxes" in results: + height, width = results["img_shape"] + scale_factor = np.array([width, height, width, height]) + gt_bboxes = results["gt_bboxes"] + gt_bboxes = (gt_bboxes * scale_factor).astype(np.float32) + results["gt_bboxes"] = gt_bboxes + if "proposals" in results and results["proposals"] is not None: + proposals = results["proposals"] + proposals = (proposals * scale_factor).astype(np.float32) + results["proposals"] = proposals + + return results diff --git a/src/otx/algorithms/action/adapters/mmaction/models/__init__.py b/src/otx/algorithms/action/adapters/mmaction/models/__init__.py new file mode 100644 index 00000000000..62a536df1e5 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/models/__init__.py @@ -0,0 +1,11 @@ +"""OTX Adapters for action recognition models - mmaction2.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .backbones import OTXMoViNet, register_action_backbones +from .detectors import AVAFastRCNN +from .heads import AVARoIHead, MoViNetHead +from .recognizers import MoViNetRecognizer + +__all__ = ["register_action_backbones", "AVAFastRCNN", "OTXMoViNet", "MoViNetHead", "MoViNetRecognizer", "AVARoIHead"] diff --git a/src/otx/algorithms/action/adapters/mmaction/models/backbones/__init__.py b/src/otx/algorithms/action/adapters/mmaction/models/backbones/__init__.py new file mode 100644 index 00000000000..43ecc488014 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/models/backbones/__init__.py @@ -0,0 +1,17 @@ +"""OTX Adapters for action recognition backbones - mmaction2.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from mmaction.models.backbones.x3d import X3D +from mmdet.models import BACKBONES as MMDET_BACKBONES + +from .movinet import OTXMoViNet + + +def register_action_backbones(): + """Register action backbone to mmdetection backbones.""" + MMDET_BACKBONES.register_module()(X3D) + + +__all__ = ["OTXMoViNet"] diff --git a/src/otx/algorithms/action/adapters/mmaction/models/backbones/movinet.py b/src/otx/algorithms/action/adapters/mmaction/models/backbones/movinet.py new file mode 100644 index 00000000000..2803d02cf56 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/models/backbones/movinet.py @@ -0,0 +1,771 @@ +"""Code modified from: https://github.com/Atze00/MoViNet-pytorch/blob/main/movinets/models.py.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +from collections import OrderedDict +from typing import Any, Callable, Optional, Tuple, Union + +import torch +import torch.nn.functional as F +from einops import rearrange +from mmaction.models.builder import BACKBONES +from mmcv.utils import Config +from torch import Tensor, nn +from torch.nn.modules.utils import _pair, _triple + + +class Conv2dBNActivation(nn.Sequential): + """A base module that applies a 2D Conv-BN-Activation. + + Args: + in_planes (int): Number of input channels. + out_planes (int): Number of output channels. + kernel_size (Union[int, Tuple[int, int]]): Size of the convolution kernel. + padding (Union[int, Tuple[int, int]]): Size of the padding applied to the input. + stride (Union[int, Tuple[int, int]], optional): Stride of the convolution. Default: 1. + groups (int, optional): Number of groups in the convolution. Default: 1. + norm_layer (Optional[Callable[..., nn.Module]], optional): Normalization layer to use. + If None, identity is used. Default: None. + activation_layer (Optional[Callable[..., nn.Module]], optional): Activation layer to use. + If None, identity is used. Default: None. + **kwargs (Any): Additional keyword arguments passed to nn.Conv2d. + + Attributes: + kernel_size (Tuple[int, int]): Size of the convolution kernel. + stride (Tuple[int, int]): Stride of the convolution. + out_channels (int): Number of output channels. + + """ + + def __init__( + self, + in_planes: int, + out_planes: int, + *, + kernel_size: Union[int, Tuple[int, int]], + padding: Union[int, Tuple[int, int]], + stride: Union[int, Tuple[int, int]] = 1, + groups: int = 1, + norm_layer: Optional[Callable[..., nn.Module]] = None, + activation_layer: Optional[Callable[..., nn.Module]] = None, + **kwargs: Any, + ) -> None: + kernel_size = _pair(kernel_size) + stride = _pair(stride) + padding = _pair(padding) + if norm_layer is None: + norm_layer = nn.Identity + if activation_layer is None: + activation_layer = nn.Identity + self.kernel_size = kernel_size + self.stride = stride + dict_layers = OrderedDict( + { + "conv2d": nn.Conv2d( + in_planes, + out_planes, + kernel_size=kernel_size, + stride=stride, + padding=padding, + groups=groups, + **kwargs, + ), + "norm": norm_layer(out_planes, eps=0.001), + "act": activation_layer(), + } + ) + + self.out_channels = out_planes + super().__init__(dict_layers) + + +class Conv3DBNActivation(nn.Sequential): + """A base module that applies a 3D Conv-BN-Activation. + + Args: + in_planes (int): Number of input channels. + out_planes (int): Number of output channels. + kernel_size (Union[int, Tuple[int, int, int]]): Size of the convolution kernel. + padding (Union[int, Tuple[int, int, int]]): Size of the padding applied to the input. + stride (Union[int, Tuple[int, int, int]], optional): Stride of the convolution. Default: 1. + groups (int, optional): Number of groups in the convolution. Default: 1. + norm_layer (Optional[Callable[..., nn.Module]], optional): Normalization layer to use. + If None, identity is used. Default: None. + activation_layer (Optional[Callable[..., nn.Module]], optional): Activation layer to use. + If None, identity is used. Default: None. + **kwargs (Any): Additional keyword arguments passed to nn.Conv3d. + + Attributes: + kernel_size (Tuple[int, int, int]): Size of the convolution kernel. + stride (Tuple[int, int, int]): Stride of the convolution. + out_channels (int): Number of output channels. + + """ + + def __init__( + self, + in_planes: int, + out_planes: int, + *, + kernel_size: Union[int, Tuple[int, int, int]], + padding: Union[int, Tuple[int, int, int]], + stride: Union[int, Tuple[int, int, int]] = 1, + groups: int = 1, + norm_layer: Optional[Callable[..., nn.Module]] = None, + activation_layer: Optional[Callable[..., nn.Module]] = None, + **kwargs: Any, + ) -> None: + kernel_size = _triple(kernel_size) + stride = _triple(stride) + padding = _triple(padding) + if norm_layer is None: + norm_layer = nn.Identity + if activation_layer is None: + activation_layer = nn.Identity + self.kernel_size = kernel_size + self.stride = stride + + dict_layers = OrderedDict( + { + "conv3d": nn.Conv3d( + in_planes, + out_planes, + kernel_size=kernel_size, + stride=stride, + padding=padding, + groups=groups, + **kwargs, + ), + "norm": norm_layer(out_planes, eps=0.001), + "act": activation_layer(), + } + ) + + self.out_channels = out_planes + super().__init__(dict_layers) + + +class ConvBlock3D(nn.Module): + """A module that applies a 2+1D or 3D Conv-BN-activation sequential. + + Args: + in_planes (int): Number of input channels. + out_planes (int): Number of output channels. + kernel_size (Tuple[int, int, int]): Size of the convolution kernel. + tf_like (bool): Whether to use TensorFlow-like padding and convolution. + conv_type (str): Type of 3D convolution to use. Must be "2plus1d" or "3d". + padding (Tuple[int, int, int], optional): Size of the padding applied to the input. + Default: (0, 0, 0). + stride (Tuple[int, int, int], optional): Stride of the convolution. Default: (1, 1, 1). + norm_layer (Optional[Callable[..., nn.Module]], optional): Normalization layer to use. + If None, identity is used. Default: None. + activation_layer (Optional[Callable[..., nn.Module]], optional): Activation layer to use. + If None, identity is used. Default: None. + bias (bool, optional): Whether to use bias in the convolution. Default: False. + **kwargs (Any): Additional keyword arguments passed to nn.Conv2d or nn.Conv3d. + + Attributes: + conv_1 (Union[Conv2dBNActivation, Conv3DBNActivation]): Convolutional layer. + conv_2 (Optional[Conv2dBNActivation]): Convolutional layer for 2+1D convolution. + padding (Tuple[int, int, int]): Size of the padding applied to the input. + kernel_size (Tuple[int, int, int]): Size of the convolution kernel. + dim_pad (int): Padding along the temporal dimension. + stride (Tuple[int, int, int]): Stride of the convolution. + conv_type (str): Type of 3D convolution used. + tf_like (bool): Whether to use TensorFlow-like padding and convolution. + + """ + + # pylint: disable=too-many-instance-attributes, too-many-arguments + def __init__( + self, + in_planes: int, + out_planes: int, + kernel_size: Tuple[int, int, int], + tf_like: bool, + conv_type: str, + padding: Tuple[int, int, int] = (0, 0, 0), + stride: Tuple[int, int, int] = (1, 1, 1), + norm_layer: Optional[Callable[..., nn.Module]] = None, + activation_layer: Optional[Callable[..., nn.Module]] = None, + bias: bool = False, + **kwargs: Any, + ) -> None: + super().__init__() + self.conv_2 = None + if tf_like: + # We need odd kernel to have even padding + # and stride == 1 to precompute padding, + if kernel_size[0] % 2 == 0: + raise ValueError("tf_like supports only odd" + " kernels for temporal dimension") + padding = ((kernel_size[0] - 1) // 2, 0, 0) + if stride[0] != 1: + raise ValueError("illegal stride value, tf like supports" + " only stride == 1 for temporal dimension") + if stride[1] > kernel_size[1] or stride[2] > kernel_size[2]: + # these values are not tested so should be avoided + raise ValueError("tf_like supports only" + " stride <= of the kernel size") + + if conv_type not in ["2plus1d", "3d"]: + raise ValueError("only 2plus2d or 3d are " + "allowed as 3d convolutions") + + if conv_type == "2plus1d": + self.conv_1 = Conv2dBNActivation( + in_planes, + out_planes, + kernel_size=(kernel_size[1], kernel_size[2]), + padding=(padding[1], padding[2]), + stride=(stride[1], stride[2]), + activation_layer=activation_layer, + norm_layer=norm_layer, + bias=bias, + **kwargs, + ) + if kernel_size[0] > 1: + self.conv_2 = Conv2dBNActivation( + in_planes, + out_planes, + kernel_size=(kernel_size[0], 1), + padding=(padding[0], 0), + stride=(stride[0], 1), + activation_layer=activation_layer, + norm_layer=norm_layer, + bias=bias, + **kwargs, + ) + elif conv_type == "3d": + self.conv_1 = Conv3DBNActivation( + in_planes, + out_planes, + kernel_size=kernel_size, + padding=padding, + activation_layer=activation_layer, + norm_layer=norm_layer, + stride=stride, + bias=bias, + **kwargs, + ) + self.padding = padding + self.kernel_size = kernel_size + self.dim_pad = self.kernel_size[0] - 1 + self.stride = stride + self.conv_type = conv_type + self.tf_like = tf_like + + def _forward(self, x: Tensor) -> Tensor: + shape_with_buffer = x.shape + if self.conv_type == "2plus1d": + x = rearrange(x, "b c t h w -> (b t) c h w") + x = self.conv_1(x) + if self.conv_type == "2plus1d": + x = rearrange(x, "(b t) c h w -> b c t h w", t=shape_with_buffer[2]) + if self.conv_2 is not None: + w = x.shape[-1] + x = rearrange(x, "b c t h w -> b c t (h w)") + x = self.conv_2(x) + x = rearrange(x, "b c t (h w) -> b c t h w", w=w) + return x + + def forward(self, x: Tensor) -> Tensor: + """Forward function of ConvBlock3D.""" + if self.tf_like: + x = same_padding( + x, + x.shape[-2], + x.shape[-1], + self.stride[-2], + self.stride[-1], + self.kernel_size[-2], + self.kernel_size[-1], + ) + x = self._forward(x) + return x + + +class SqueezeExcitation(nn.Module): + """Implements the Squeeze-and-Excitation (SE) block. + + Args: + input_channels (int): Number of input channels. + activation_2 (nn.Module): Activation function applied after the second convolutional block. + activation_1 (nn.Module): Activation function applied after the first convolutional block. + conv_type (str): Convolutional block type ("2plus1d" or "3d"). + squeeze_factor (int, optional): The reduction factor for the number of channels (default: 4). + bias (bool, optional): Whether to add a bias term to the convolutional blocks (default: True). + """ + + def __init__( + self, + input_channels: int, + activation_2: nn.Module, + activation_1: nn.Module, + conv_type: str, + squeeze_factor: int = 4, + bias: bool = True, + ) -> None: + super().__init__() + se_multiplier = 1 + squeeze_channels = _make_divisible(input_channels // squeeze_factor * se_multiplier, 8) + self.fc1 = ConvBlock3D( + input_channels * se_multiplier, + squeeze_channels, + kernel_size=(1, 1, 1), + padding=(0, 0, 0), + tf_like=False, + conv_type=conv_type, + bias=bias, + ) + self.activation_1 = activation_1() + self.activation_2 = activation_2() + self.fc2 = ConvBlock3D( + squeeze_channels, + input_channels, + kernel_size=(1, 1, 1), + padding=(0, 0, 0), + tf_like=False, + conv_type=conv_type, + bias=bias, + ) + + def _scale(self, x: Tensor) -> Tensor: + """Computes the scaling factor for the input tensor. + + Args: + x (torch.Tensor): Input tensor of shape (batch_size, channels, time, height, width). + + Returns: + torch.Tensor: Scaling factor for the input tensor of shape (batch_size, channels, 1, 1, 1). + """ + scale = F.adaptive_avg_pool3d(x, 1) + scale = self.fc1(scale) + scale = self.activation_1(scale) + scale = self.fc2(scale) + return self.activation_2(scale) + + def forward(self, x: Tensor) -> Tensor: + """Forward function of SqueezeExcitation.""" + scale = self._scale(x) + return scale * x + + +def _make_divisible(value: float, divisor: int, min_value: Optional[int] = None) -> int: + if min_value is None: + min_value = divisor + new_v = max(min_value, int(value + divisor / 2) // divisor * divisor) + # Make sure that round down does not go down by more than 10%. + if new_v < 0.9 * value: + new_v += divisor + return new_v + + +def same_padding( + x: Tensor, in_height: int, in_width: int, stride_h: int, stride_w: int, filter_height: int, filter_width: int +) -> Tensor: + """Applies padding to the input tensor to ensure that the output tensor size is the same as the input tensor size. + + Args: + x (torch.Tensor): Input tensor of shape (batch_size, channels, time, height, width). + in_height (int): Height of the input tensor. + in_width (int): Width of the input tensor. + stride_h (int): Stride in the height dimension. + stride_w (int): Stride in the width dimension. + filter_height (int): Height of the filter (kernel). + filter_width (int): Width of the filter (kernel). + + Returns: + torch.Tensor: Padded tensor of shape (batch_size, channels, time, height + pad_h, width + pad_w), where + pad_h and pad_w are the heights and widths of the top, bottom, left, and right padding applied to the tensor. + + """ + if in_height % stride_h == 0: + pad_along_height = max(filter_height - stride_h, 0) + else: + pad_along_height = max(filter_height - (in_height % stride_h), 0) + if in_width % stride_w == 0: + pad_along_width = max(filter_width - stride_w, 0) + else: + pad_along_width = max(filter_width - (in_width % stride_w), 0) + pad_top = pad_along_height // 2 + pad_bottom = pad_along_height - pad_top + pad_left = pad_along_width // 2 + pad_right = pad_along_width - pad_left + padding_pad = (pad_left, pad_right, pad_top, pad_bottom) + return torch.nn.functional.pad(x, padding_pad) + + +class TFAvgPool3D(nn.Module): + """3D average pooling layer with padding.""" + + def __init__(self) -> None: + super().__init__() + self.avgf = nn.AvgPool3d((1, 3, 3), stride=(1, 2, 2)) + + def forward(self, x: Tensor) -> Tensor: + """Applies 3D average pooling with padding to the input tensor. + + Args: + x (torch.Tensor): Input tensor of shape (batch_size, channels, time, height, width). + + Returns: + torch.Tensor: Pooled tensor of shape (batch_size, channels, time, height', width'), where + height' and width' are the heights and widths of the pooled tensor after padding is applied. + + """ + use_padding = x.shape[-1] % 2 != 0 + if use_padding: + padding_pad = (0, 0, 0, 0) + else: + padding_pad = (0, 1, 0, 1) + x = torch.nn.functional.pad(x, padding_pad) + if use_padding: + x = torch.nn.functional.avg_pool3d( + x, (1, 3, 3), stride=(1, 2, 2), count_include_pad=False, padding=(0, 1, 1) + ) + else: + x = self.avgf(x) + x[..., -1] = x[..., -1] * 9 / 6 + x[..., -1, :] = x[..., -1, :] * 9 / 6 + return x + + +class BasicBneck(nn.Module): + """Basic bottleneck block of MoViNet network. + + Args: + cfg (Config): Configuration object containing block's hyperparameters. + tf_like (bool): A boolean indicating whether to use TensorFlow like convolution + padding or not. + conv_type (str): A string indicating the type of convolutional layer to use. + Can be "2d" or "3d". + norm_layer (Callable[..., nn.Module], optional): A callable normalization layer + to use. Defaults to None. + activation_layer (Callable[..., nn.Module], optional): A callable activation + layer to use. Defaults to None. + + Attributes: + expand (ConvBlock3D, optional): An optional expansion convolutional block. + deep (ConvBlock3D): A convolutional block with kernel size, stride, padding, + and groups as specified in the configuration object. + squeeze_excitation (SqueezeExcitation): A squeeze-and-excitation block. + project (ConvBlock3D): A projection convolutional block. + res (nn.Sequential, optional): An optional residual convolutional block. + alpha (nn.Parameter): A learnable parameter used in the ReZero operation. + + Raises: + AssertionError: If the stride in configuration is not a tuple. + + """ + + def __init__( + self, + cfg: "Config", + tf_like: bool, + conv_type: str, + norm_layer: Optional[Callable[..., nn.Module]] = None, + activation_layer: Optional[Callable[..., nn.Module]] = None, + ) -> None: + super().__init__() + assert isinstance(cfg.stride, tuple) + self.res = None + + layers = [] + if cfg.expanded_channels != cfg.out_channels: + self.expand = ConvBlock3D( + in_planes=cfg.input_channels, + out_planes=cfg.expanded_channels, + kernel_size=(1, 1, 1), + padding=(0, 0, 0), + conv_type=conv_type, + tf_like=tf_like, + norm_layer=norm_layer, + activation_layer=activation_layer, + ) + self.deep = ConvBlock3D( + in_planes=cfg.expanded_channels, + out_planes=cfg.expanded_channels, + kernel_size=cfg.kernel_size, + padding=cfg.padding, + stride=cfg.stride, # type: ignore + groups=cfg.expanded_channels, + conv_type=conv_type, + tf_like=tf_like, + norm_layer=norm_layer, + activation_layer=activation_layer, + ) + # pylint: disable=invalid-name + self.se = SqueezeExcitation( + cfg.expanded_channels, + activation_1=activation_layer, + activation_2=(nn.Sigmoid if conv_type == "3d" else nn.Hardsigmoid), + conv_type=conv_type, + ) + self.project = ConvBlock3D( + cfg.expanded_channels, + cfg.out_channels, + kernel_size=(1, 1, 1), + padding=(0, 0, 0), + conv_type=conv_type, + tf_like=tf_like, + norm_layer=norm_layer, + activation_layer=nn.Identity, + ) + + if not (cfg.stride == (1, 1, 1) and cfg.input_channels == cfg.out_channels): + if cfg.stride != (1, 1, 1): + if tf_like: + layers.append(TFAvgPool3D()) + else: + layers.append(nn.AvgPool3d((1, 3, 3), stride=cfg.stride, padding=cfg.padding_avg)) + layers.append( + ConvBlock3D( + in_planes=cfg.input_channels, + out_planes=cfg.out_channels, + kernel_size=(1, 1, 1), + padding=(0, 0, 0), + norm_layer=norm_layer, + activation_layer=nn.Identity, + conv_type=conv_type, + tf_like=tf_like, + ) + ) + self.res = nn.Sequential(*layers) + # ReZero + self.alpha = nn.Parameter(torch.tensor(0.0), requires_grad=True) + + def forward(self, x: Tensor) -> Tensor: + """Forward function of BasicBneck.""" + if self.res is not None: + residual = self.res(x) + else: + residual = x + if hasattr(self, "expand"): + x = self.expand(x) + x = self.deep(x) + x = self.se(x) + x = self.project(x) + result = residual + self.alpha * x + return result + + +class MoViNet(nn.Module): + """MoViNet class used for video classification. + + Args: + cfg (Config): Configuration object containing network's hyperparameters. + conv_type (str, optional): A string indicating the type of convolutional layer + to use. Can be "2d" or "3d". Defaults to "3d". + tf_like (bool, optional): A boolean indicating whether to use TensorFlow like + convolution padding or not. Defaults to False. + + Attributes: + conv1 (ConvBlock3D): A convolutional block for the first layer. + blocks (nn.Sequential): A sequence of basic bottleneck blocks. + conv7 (ConvBlock3D): A convolutional block for the final layer. + + Methods: + avg(x: Tensor) -> Tensor: A static method that returns the adaptive average pool + of the input tensor. + _init_weights(module): A private method that initializes the weights of the network's + convolutional, batch normalization, and linear layers. + forward(x: Tensor) -> Tensor: The forward pass of the network. + + """ + + def __init__( + self, + cfg: "Config", + conv_type: str = "3d", + tf_like: bool = False, + ) -> None: + super().__init__() + tf_like = True + blocks_dic = OrderedDict() + + norm_layer = nn.BatchNorm3d if conv_type == "3d" else nn.BatchNorm2d + activation_layer = nn.SiLU if conv_type == "3d" else nn.Hardswish + + self.conv1 = ConvBlock3D( + in_planes=cfg.conv1.input_channels, + out_planes=cfg.conv1.out_channels, + kernel_size=cfg.conv1.kernel_size, + stride=cfg.conv1.stride, + padding=cfg.conv1.padding, + conv_type=conv_type, + tf_like=tf_like, + norm_layer=norm_layer, + activation_layer=activation_layer, + ) + for i, block in enumerate(cfg.blocks): + for j, basicblock in enumerate(block): + blocks_dic[f"b{i}_l{j}"] = BasicBneck( + basicblock, + conv_type=conv_type, + tf_like=tf_like, + norm_layer=norm_layer, + activation_layer=activation_layer, + ) + self.blocks = nn.Sequential(blocks_dic) + self.conv7 = ConvBlock3D( + in_planes=cfg.conv7.input_channels, + out_planes=cfg.conv7.out_channels, + kernel_size=cfg.conv7.kernel_size, + stride=cfg.conv7.stride, + padding=cfg.conv7.padding, + conv_type=conv_type, + tf_like=tf_like, + norm_layer=norm_layer, + activation_layer=activation_layer, + ) + + def avg(self, x: Tensor) -> Tensor: + """Returns the adaptive average pool of the input tensor. + + Args: + x (Tensor): A tensor to be averaged. + + Returns: + Tensor: A tensor with the averaged values. + + """ + return F.adaptive_avg_pool3d(x, 1) + + @staticmethod + def _init_weights(module): + if isinstance(module, nn.Conv3d): + nn.init.kaiming_normal_(module.weight, mode="fan_out") + if module.bias is not None: + nn.init.zeros_(module.bias) + elif isinstance(module, (nn.BatchNorm3d, nn.BatchNorm2d, nn.GroupNorm)): + nn.init.ones_(module.weight) + nn.init.zeros_(module.bias) + elif isinstance(module, nn.Linear): + nn.init.normal_(module.weight, 0, 0.01) + nn.init.zeros_(module.bias) + + def forward(self, x: Tensor) -> Tensor: + """Forward function of MoViNet.""" + x = self.conv1(x) + x = self.blocks(x) + x = self.conv7(x) + x = self.avg(x) + return x + + def init_weights(self): + """Initializes the weights of network.""" + self.apply(self._init_weights) + + +@BACKBONES.register_module() +class OTXMoViNet(MoViNet): + """MoViNet wrapper class for OTX.""" + + # pylint: disable=unused-argument + def __init__(self, **kwargs): + cfg = Config() + cfg.name = "A0" + cfg.conv1 = Config() + OTXMoViNet.fill_conv(cfg.conv1, 3, 8, (1, 3, 3), (1, 2, 2), (0, 1, 1)) + + cfg.blocks = [ + [Config()], + [Config() for _ in range(3)], + [Config() for _ in range(3)], + [Config() for _ in range(4)], + [Config() for _ in range(4)], + ] + + # block 2 + OTXMoViNet.fill_se_config(cfg.blocks[0][0], 8, 8, 24, (1, 5, 5), (1, 2, 2), (0, 2, 2), (0, 1, 1)) + + # block 3 + OTXMoViNet.fill_se_config(cfg.blocks[1][0], 8, 32, 80, (3, 3, 3), (1, 2, 2), (1, 0, 0), (0, 0, 0)) + OTXMoViNet.fill_se_config(cfg.blocks[1][1], 32, 32, 80, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) + OTXMoViNet.fill_se_config(cfg.blocks[1][2], 32, 32, 80, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) + + # block 4 + OTXMoViNet.fill_se_config(cfg.blocks[2][0], 32, 56, 184, (5, 3, 3), (1, 2, 2), (2, 0, 0), (0, 0, 0)) + OTXMoViNet.fill_se_config(cfg.blocks[2][1], 56, 56, 112, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) + OTXMoViNet.fill_se_config(cfg.blocks[2][2], 56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) + + # block 5 + OTXMoViNet.fill_se_config(cfg.blocks[3][0], 56, 56, 184, (5, 3, 3), (1, 1, 1), (2, 1, 1), (0, 1, 1)) + OTXMoViNet.fill_se_config(cfg.blocks[3][1], 56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) + OTXMoViNet.fill_se_config(cfg.blocks[3][2], 56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) + OTXMoViNet.fill_se_config(cfg.blocks[3][3], 56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)) + + # block 6 + OTXMoViNet.fill_se_config(cfg.blocks[4][0], 56, 104, 384, (5, 3, 3), (1, 2, 2), (2, 1, 1), (0, 1, 1)) + OTXMoViNet.fill_se_config(cfg.blocks[4][1], 104, 104, 280, (1, 5, 5), (1, 1, 1), (0, 2, 2), (0, 1, 1)) + OTXMoViNet.fill_se_config(cfg.blocks[4][2], 104, 104, 280, (1, 5, 5), (1, 1, 1), (0, 2, 2), (0, 1, 1)) + OTXMoViNet.fill_se_config(cfg.blocks[4][3], 104, 104, 344, (1, 5, 5), (1, 1, 1), (0, 2, 2), (0, 1, 1)) + + cfg.conv7 = Config() + OTXMoViNet.fill_conv(cfg.conv7, 104, 480, (1, 1, 1), (1, 1, 1), (0, 0, 0)) + + cfg.dense9 = Config(dict(hidden_dim=2048)) + super().__init__(cfg) + + @staticmethod + # pylint: disable=too-many-arguments + def fill_se_config( + conf, + input_channels, + out_channels, + expanded_channels, + kernel_size, + stride, + padding, + padding_avg, + ): + """Set the values of a given Config object to SE module. + + Args: + conf (Config): The Config object to be updated. + input_channels (int): The number of input channels. + out_channels (int): The number of output channels. + expanded_channels (int): The number of channels after expansion in the basic block. + kernel_size (tuple[int]): The size of the kernel. + stride (tuple[int]): The stride of the kernel. + padding (tuple[int]): The padding of the kernel. + padding_avg (tuple[int]): The padding for the average pooling operation. + + Returns: + None. + """ + conf.expanded_channels = expanded_channels + conf.padding_avg = padding_avg + OTXMoViNet.fill_conv( + conf, + input_channels, + out_channels, + kernel_size, + stride, + padding, + ) + + @staticmethod + def fill_conv( + conf, + input_channels, + out_channels, + kernel_size, + stride, + padding, + ): + """Set the values of a given Config object to conv layer. + + Args: + conf (Config): The Config object to be updated. + input_channels (int): The number of input channels. + out_channels (int): The number of output channels. + kernel_size (tuple[int]): The size of the kernel. + stride (tuple[int]): The stride of the kernel. + padding (tuple[int]): The padding of the kernel. + + Returns: + None. + """ + conf.input_channels = input_channels + conf.out_channels = out_channels + conf.kernel_size = kernel_size + conf.stride = stride + conf.padding = padding diff --git a/src/otx/algorithms/action/adapters/mmaction/models/detectors/__init__.py b/src/otx/algorithms/action/adapters/mmaction/models/detectors/__init__.py new file mode 100644 index 00000000000..6708273a881 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/models/detectors/__init__.py @@ -0,0 +1,9 @@ +"""OTX Adapters for action recognition detectors - mmaction2.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from .fast_rcnn import AVAFastRCNN + +__all__ = ["AVAFastRCNN"] diff --git a/src/otx/algorithms/action/adapters/mmaction/models/detectors/fast_rcnn.py b/src/otx/algorithms/action/adapters/mmaction/models/detectors/fast_rcnn.py new file mode 100644 index 00000000000..20850be772a --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/models/detectors/fast_rcnn.py @@ -0,0 +1,121 @@ +"""Fast RCNN for Action Detection.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import torch +from mmcv.runner import load_checkpoint +from mmcv.utils import ConfigDict +from mmdeploy.core import FUNCTION_REWRITER +from mmdet.models import DETECTORS +from mmdet.models.builder import build_detector +from mmdet.models.detectors import FastRCNN +from torch import nn + +from otx.algorithms.action.configs.detection.base.faster_rcnn_config import ( + faster_rcnn, + faster_rcnn_pretrained, +) + + +class ONNXPool3D(nn.Module): + """3D pooling method for onnx export. + + ONNX dose not support dynamic pooling, therefore pooling operation should be changed into static way + """ + + def __init__(self, dim, pool_type): + # TODO This is tempral solution to export two-stage action detection model. + # This should be re-visited after fixing CVS-104657 + super().__init__() + self.dim = dim + if pool_type == "avg": + self.pool = torch.mean + else: + self.pool = torch.max + self.pool_type = pool_type + + def forward(self, x): + """Forward method.""" + if self.dim == "temporal": + if self.pool_type == "avg": + return self.pool(x, 2, keepdim=True) + return self.pool(x, 2, keepdim=True)[0] + if self.pool_type == "avg": + return self.pool(x, (3, 4), keepdim=True) + return self.pool(self.pool(x, 3, keepdim=True)[0], 4, keepdim=True)[0] + + +@DETECTORS.register_module() +class AVAFastRCNN(FastRCNN): + """Implementation of `Fast R-CNN` for Action Detection. + + Add forward_infer function for inference without pre-proposals + """ + + # pylint: disable=too-many-arguments, too-many-locals + def __init__(self, backbone, roi_head, train_cfg, test_cfg, neck=None, pretrained=None): + super(FastRCNN, self).__init__( + backbone=backbone, + neck=neck, + roi_head=roi_head, + train_cfg=train_cfg, + test_cfg=test_cfg, + pretrained=pretrained, + ) + self.detector = None + + def patch_for_export(self): + """Patch mmdetection's FastRCNN for exporting to onnx.""" + self._add_detector() + self._patch_pools() + + def _add_detector(self): + """Add Person Detector for inference. + + Action classification backbone + Fast RCNN structure use pre-proposals instead outputs from person detector + This saves training and evaluation time. However, this method doesn't support inference for the dataset + without pre-proposals. Therefore, when the model infers to dataset without pre-proposal, person detector + should be added to action detector. + """ + detector = ConfigDict(faster_rcnn) + self.detector = build_detector(detector) + ckpt = load_checkpoint(self.detector, faster_rcnn_pretrained, map_location="cpu") + self.detector.CLASSES = ckpt["meta"]["CLASSES"] + if self.detector.CLASSES[0] != "person": + raise Exception( + f"Person detector should have person as the first category, but got {self.detector.CLASSES}" + ) + + def _patch_pools(self): + """Patch pooling functions for ONNX export. + + AVAFastRCNN's bbox head has pooling funcitons, which contain dynamic shaping. + This funciton changes those pooling functions from dynamic shaping to static shaping. + """ + self.roi_head.bbox_head.temporal_pool = ONNXPool3D("temporal", self.roi_head.bbox_head.temporal_pool_type) + self.roi_head.bbox_head.spatial_pool = ONNXPool3D("spatial", self.roi_head.bbox_head.spatial_pool_type) + + # pylint: disable=no-self-argument + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.action.adapters.mmaction.models.detectors.fast_rcnn.AVAFastRCNN.forward" + ) + def forward_infer(ctx, self, imgs, img_metas): + """Forward function for inference without pre-proposal.""" + clip_len = imgs.shape[2] + img = imgs[:, :, int(clip_len / 2), :, :] + det_bboxes, det_labels = self.detector.simple_test(img, img_metas[0]) + prediction = [det_bboxes[0][det_labels[0] == 0]] + prediction = self.simple_test(imgs, img_metas[0], proposals=prediction) + return prediction diff --git a/src/otx/algorithms/action/adapters/mmaction/models/heads/__init__.py b/src/otx/algorithms/action/adapters/mmaction/models/heads/__init__.py new file mode 100644 index 00000000000..dacea417b3a --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/models/heads/__init__.py @@ -0,0 +1,9 @@ +"""OTX Adapters for action recognition backbones - mmaction2.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .movinet_head import MoViNetHead +from .roi_head import AVARoIHead + +__all__ = ["AVARoIHead", "MoViNetHead"] diff --git a/src/otx/algo/action_classification/heads/movinet_head.py b/src/otx/algorithms/action/adapters/mmaction/models/heads/movinet_head.py similarity index 78% rename from src/otx/algo/action_classification/heads/movinet_head.py rename to src/otx/algorithms/action/adapters/mmaction/models/heads/movinet_head.py index 7491df70fd4..7f3b62cd283 100644 --- a/src/otx/algo/action_classification/heads/movinet_head.py +++ b/src/otx/algorithms/action/adapters/mmaction/models/heads/movinet_head.py @@ -1,18 +1,17 @@ -# Copyright (C) 2024 Intel Corporation +"""MoViNet head for otx action recognition.""" +# Copyright (c) OpenMMLab. All rights reserved. +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -"""Custom MoViNet Head for video recognition.""" -from __future__ import annotations - -from mmaction.models import MODELS +from mmaction.models.builder import HEADS from mmaction.models.heads.base import BaseHead -from mmengine.model import normal_init -from torch import Tensor, nn +from mmcv.cnn import normal_init +from torch import nn -from otx.algo.action_classification.backbones.movinet import ConvBlock3D +from otx.algorithms.action.adapters.mmaction.models.backbones.movinet import ConvBlock3D -@MODELS.register_module() +@HEADS.register_module() class MoViNetHead(BaseHead): """Classification head for MoViNet. @@ -36,9 +35,8 @@ def __init__( loss_cls: dict, tf_like: bool = False, conv_type: str = "3d", - average_clips: str | None = None, ): - super().__init__(num_classes, in_channels, loss_cls, average_clips=average_clips) + super().__init__(num_classes, in_channels, loss_cls) self.init_std = 0.1 self.classifier = nn.Sequential( ConvBlock3D( @@ -61,11 +59,11 @@ def __init__( ), ) - def init_weights(self) -> None: + def init_weights(self): """Initialize the parameters from scratch.""" normal_init(self.classifier, std=self.init_std) - def forward(self, x: Tensor, **kwargs) -> Tensor: + def forward(self, x): """Defines the computation performed at every call. Args: @@ -76,5 +74,6 @@ def forward(self, x: Tensor, **kwargs) -> Tensor: """ # [N, in_channels, T, H, W] cls_score = self.classifier(x) + cls_score = cls_score.flatten(1) # [N, num_classes] - return cls_score.flatten(1) + return cls_score diff --git a/src/otx/algorithms/action/adapters/mmaction/models/heads/roi_head.py b/src/otx/algorithms/action/adapters/mmaction/models/heads/roi_head.py new file mode 100644 index 00000000000..df97c642062 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/models/heads/roi_head.py @@ -0,0 +1,30 @@ +"""Adapt AVARoIHead in mmaction into OTX.""" +from mmaction.core.bbox import bbox2result +from mmaction.models.heads import AVARoIHead as MMAVARoIHead +from mmdet.models import HEADS as MMDET_HEADS +from torch.onnx import is_in_onnx_export + + +@MMDET_HEADS.register_module(force=True) +# pylint: disable=abstract-method, unused-argument,too-many-ancestors +class AVARoIHead(MMAVARoIHead): + """AVARoIHead for OTX.""" + + # TODO Remove this after changing from forward_test to onnx_export for onnx export + def simple_test(self, x, proposal_list, img_metas, proposals=None, rescale=False, **kwargs): + """This is temporary soluition, since otx.mmdet is differnt with latest mmdet.""" + assert self.with_bbox, "Bbox head must be implemented." + + if isinstance(x, tuple): + x_shape = x[0].shape + else: + x_shape = x.shape + + assert x_shape[0] == 1, "only accept 1 sample at test mode" + assert x_shape[0] == len(img_metas) == len(proposal_list) + + det_bboxes, det_labels = self.simple_test_bboxes(x, img_metas, proposal_list, self.test_cfg, rescale=rescale) + if is_in_onnx_export(): + return det_bboxes, det_labels + bbox_results = bbox2result(det_bboxes, det_labels, self.bbox_head.num_classes, thr=self.test_cfg.action_thr) + return [bbox_results] diff --git a/src/otx/algorithms/action/adapters/mmaction/models/recognizers/__init__.py b/src/otx/algorithms/action/adapters/mmaction/models/recognizers/__init__.py new file mode 100644 index 00000000000..258cd9d3074 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/models/recognizers/__init__.py @@ -0,0 +1,8 @@ +"""OTX Adapters for action recognition backbones - mmaction2.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .movinet_recognizer import MoViNetRecognizer + +__all__ = ["MoViNetRecognizer"] diff --git a/src/otx/algorithms/action/adapters/mmaction/models/recognizers/movinet_recognizer.py b/src/otx/algorithms/action/adapters/mmaction/models/recognizers/movinet_recognizer.py new file mode 100644 index 00000000000..148dcee1c3b --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/models/recognizers/movinet_recognizer.py @@ -0,0 +1,43 @@ +"""MoViNet Recognizer for OTX compatibility.""" +# pylint: disable=unused-argument +# Copyright (c) OpenMMLab. All rights reserved. +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import functools + +from mmaction.models.builder import RECOGNIZERS +from mmaction.models.recognizers.recognizer3d import Recognizer3D + + +@RECOGNIZERS.register_module() +class MoViNetRecognizer(Recognizer3D): + """MoViNet recognizer model framework for OTX compatibility.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + # Hooks for redirect state_dict load/save + self._register_state_dict_hook(self.state_dict_hook) + self._register_load_state_dict_pre_hook(functools.partial(self.load_state_dict_pre_hook, self)) + + @staticmethod + def state_dict_hook(module, state_dict, *args, **kwargs): + """Redirect model as output state_dict for OTX MoviNet compatibility.""" + for key in list(state_dict.keys()): + val = state_dict.pop(key) + if "cls_head" in key: + key = key.replace("cls_head.", "") + else: + key = key.replace("backbone.", "") + state_dict[key] = val + + @staticmethod + def load_state_dict_pre_hook(module, state_dict, prefix, *args, **kwargs): + """Redirect input state_dict to model for OTX model compatibility.""" + for key in list(state_dict.keys()): + val = state_dict.pop(key) + if "classifier" in key: + key = key.replace("classifier", "cls_head.classifier") + else: + key = prefix + "backbone." + key[len(prefix) :] + state_dict[key] = val diff --git a/src/otx/algorithms/action/adapters/mmaction/task.py b/src/otx/algorithms/action/adapters/mmaction/task.py new file mode 100644 index 00000000000..66a5e981e35 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/task.py @@ -0,0 +1,594 @@ +"""Task of OTX Video Recognition using mmaction training backend.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import glob +import os +import time +from contextlib import nullcontext +from copy import deepcopy +from functools import partial +from typing import Dict, Optional, Union + +import torch +from mmaction import __version__ +from mmaction.apis import train_model +from mmaction.datasets import build_dataloader, build_dataset +from mmaction.models import build_model as build_videomodel +from mmaction.utils import collect_env +from mmcv.runner import CheckpointLoader, load_checkpoint, wrap_fp16_model +from mmcv.utils import Config, ConfigDict, ProgressBar, get_git_hash +from torch import distributed as dist + +from otx.algorithms.action.adapters.mmaction import ( + Exporter, +) +from otx.algorithms.action.task import OTXActionTask +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + BaseRecordingForwardHook, + FeatureVectorHook, +) +from otx.algorithms.common.adapters.mmcv.utils import ( + adapt_batch_size, + build_data_parallel, + get_configs_by_pairs, + patch_adaptive_interval_training, + patch_early_stopping, + patch_from_hyperparams, + patch_persistent_workers, +) +from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + OTXConfig, + update_or_add_custom_hook, +) +from otx.algorithms.common.adapters.torch.utils import convert_sync_batchnorm +from otx.algorithms.common.configs.configuration_enums import BatchSizeAdaptType +from otx.algorithms.common.utils import append_dist_rank_suffix +from otx.algorithms.common.utils.data import get_dataset +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ModelPrecision +from otx.api.entities.model_template import TaskType +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.core.data import caching +from otx.utils.logger import get_logger + +logger = get_logger() + +# TODO Remove unnecessary pylint disable +# pylint: disable=too-many-lines + + +class MMActionTask(OTXActionTask): + """Task class for OTX action using mmaction training backend.""" + + # pylint: disable=too-many-instance-attributes + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__(task_environment, output_path) + self._data_cfg: Optional[Config] = None + self._recipe_cfg: Optional[Config] = None + + # pylint: disable=too-many-locals, too-many-branches, too-many-statements + def _init_task(self, export: bool = False): # noqa + """Initialize task.""" + + self._recipe_cfg = OTXConfig.fromfile(os.path.join(self._model_dir, "model.py")) + self._recipe_cfg.domain = self._task_type.domain + self._config = self._recipe_cfg + + self.set_seed() + + # Belows may go to the configure function + if os.path.isfile(self.data_pipeline_path): + data_pipeline_cfg = Config.fromfile(self.data_pipeline_path) + self._recipe_cfg.merge_from_dict(data_pipeline_cfg) + else: + raise FileNotFoundError(f"data_pipeline: {self.data_pipeline_path} not founded") + + if not export: + patch_from_hyperparams(self._recipe_cfg, self._hyperparams) + self._recipe_cfg.total_epochs = self._recipe_cfg.runner.max_epochs + + if "custom_hooks" in self.override_configs: + override_custom_hooks = self.override_configs.pop("custom_hooks") + for override_custom_hook in override_custom_hooks: + update_or_add_custom_hook(self._recipe_cfg, ConfigDict(override_custom_hook)) + if len(self.override_configs) > 0: + logger.info(f"before override configs merging = {self._recipe_cfg}") + self._recipe_cfg.merge_from_dict(self.override_configs) + logger.info(f"after override configs merging = {self._recipe_cfg}") + + # add Cancel training hook + update_or_add_custom_hook( + self._recipe_cfg, + ConfigDict(type="CancelInterfaceHook", init_callback=self.on_hook_initialized), + ) + if self._time_monitor is not None: + update_or_add_custom_hook( + self._recipe_cfg, + ConfigDict( + type="OTXProgressHook", + time_monitor=self._time_monitor, + verbose=True, + priority=71, + ), + ) + self._recipe_cfg.log_config.hooks.append({"type": "OTXLoggerHook", "curves": self._learning_curves}) + + # Update recipe with caching modules + self._update_caching_modules(self._recipe_cfg.data) + + logger.info("initialized.") + + def build_model( + self, + cfg: Config, + fp16: bool = False, + **kwargs, + ) -> torch.nn.Module: + """Build model from model_builder.""" + model_builder = getattr(self, "model_builder", build_videomodel) + model = model_builder(cfg.model, **kwargs) + + checkpoint = cfg.pop("load_from", None) + if checkpoint is not None: + load_checkpoint(model, checkpoint, map_location="cpu") + cfg.load_from = checkpoint + + if fp16: + wrap_fp16_model(model) + + return model + + # pylint: disable=too-many-arguments + def configure( + self, + training=True, + subset="train", + ir_options=None, + ): + """Patch mmcv configs for OTX action settings.""" + + # deepcopy all configs to make sure + # changes under configuration and below does not take an effect to OTX for clear distinction + recipe_cfg = deepcopy(self._recipe_cfg) + assert recipe_cfg is not None, "'recipe_cfg' is not initialized." + + recipe_cfg.work_dir = self._output_path + recipe_cfg.resume = self._resume + recipe_cfg.omnisource = False + + self._configure_device(recipe_cfg, training) + + if self._data_cfg is not None: + recipe_cfg.merge_from_dict(self._data_cfg) + + if self._task_type == TaskType.ACTION_CLASSIFICATION: + _dataset_type = "OTXActionClsDataset" + else: + _dataset_type = "OTXActionDetDataset" + for subset in ("train", "val", "test", "unlabeled"): + _cfg = recipe_cfg.data.get(subset, None) + if not _cfg: + continue + _cfg.type = _dataset_type + while "dataset" in _cfg: + _cfg = _cfg.dataset + _cfg.labels = self._labels + + if self._task_type == TaskType.ACTION_CLASSIFICATION: + recipe_cfg.model["cls_head"].num_classes = len(self._labels) + elif self._task_type == TaskType.ACTION_DETECTION: + recipe_cfg.model["roi_head"]["bbox_head"].num_classes = len(self._labels) + 1 + if len(self._labels) < 5: + recipe_cfg.model["roi_head"]["bbox_head"]["topk"] = len(self._labels) - 1 + + recipe_cfg.data.videos_per_gpu = recipe_cfg.data.pop("samples_per_gpu", None) + + patch_adaptive_interval_training(recipe_cfg) + patch_early_stopping(recipe_cfg) + patch_persistent_workers(recipe_cfg) + + if self._model_ckpt is not None: + recipe_cfg.load_from = self.get_model_ckpt(self._model_ckpt) + if self._resume: # after updating to mmaction 1.x, need to be removed + recipe_cfg.resume_from = recipe_cfg.load_from + + self._config = recipe_cfg + return recipe_cfg + + @staticmethod + def get_model_ckpt(ckpt_path, new_path=None): + """Get pytorch model weights.""" + ckpt = CheckpointLoader.load_checkpoint(ckpt_path, map_location="cpu") + if "model" in ckpt: + ckpt = ckpt["model"] + if not new_path: + new_path = ckpt_path[:-3] + "converted.pth" + new_path = append_dist_rank_suffix(new_path) + torch.save(ckpt, new_path) + return new_path + return ckpt_path + + def _configure_device(self, cfg: Config, training: bool): + """Setting device for training and inference.""" + cfg.distributed = False + if torch.distributed.is_initialized(): + cfg.gpu_ids = [int(os.environ["LOCAL_RANK"])] + if training: # TODO multi GPU is available only in training. Evaluation needs to be supported later. + cfg.distributed = True + self.configure_distributed(cfg) + elif "gpu_ids" not in cfg: + cfg.gpu_ids = range(1) + + # consider "cuda" and "cpu" device only + if not torch.cuda.is_available(): + cfg.device = "cpu" + cfg.gpu_ids = range(-1, 0) + else: + cfg.device = "cuda" + + @staticmethod + def configure_distributed(cfg: Config): + """Patching for distributed training.""" + if hasattr(cfg, "dist_params") and cfg.dist_params.get("linear_scale_lr", False): + new_lr = dist.get_world_size() * cfg.optimizer.lr + logger.info( + f"enabled linear scaling rule to the learning rate. \ + changed LR from {cfg.optimizer.lr} to {new_lr}" + ) + cfg.optimizer.lr = new_lr + + # pylint: disable=too-many-branches, too-many-statements + def _train_model( + self, + dataset: DatasetEntity, + ): + """Train function in MMActionTask.""" + logger.info("init data cfg.") + self._data_cfg = ConfigDict(data=ConfigDict()) + + for cfg_key, subset in zip( + ["train", "val", "unlabeled"], + [Subset.TRAINING, Subset.VALIDATION, Subset.UNLABELED], + ): + subset = get_dataset(dataset, subset) + if subset and self._data_cfg is not None: + self._data_cfg.data[cfg_key] = ConfigDict( + otx_dataset=subset, + labels=self._labels, + ) + + self._is_training = True + + self._init_task() + + cfg = self.configure(True, "train", None) + logger.info("train!") + + timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) + + # Environment + logger.info(f"cfg.gpu_ids = {cfg.gpu_ids}, distributed = {cfg.distributed}") + env_info_dict = collect_env() + env_info = "\n".join([(f"{k}: {v}") for k, v in env_info_dict.items()]) + dash_line = "-" * 60 + "\n" + logger.info(f"Environment info:\n{dash_line}{env_info}\n{dash_line}") + + # Data + datasets = [build_dataset(cfg.data.train)] + + # FIXME: Currently action do not support multi batch evaluation. This will be fixed + if "val" in cfg.data: + cfg.data.val_dataloader["videos_per_gpu"] = 1 + + # Target classes + if "task_adapt" in cfg: + target_classes = cfg.task_adapt.get("final", []) + else: + target_classes = datasets[0].CLASSES + + # Metadata + meta = dict() + meta["env_info"] = env_info + meta["seed"] = cfg.seed + meta["exp_name"] = cfg.work_dir + if cfg.checkpoint_config is not None: + cfg.checkpoint_config.meta = dict( + mmaction2_version=__version__ + get_git_hash()[:7], + CLASSES=target_classes, + ) + + # Model + model = self.build_model(cfg, fp16=cfg.get("fp16", False)) + model.train() + model.CLASSES = target_classes + + if cfg.distributed: + convert_sync_batchnorm(model) + + validate = bool(cfg.data.get("val", None)) + + if self._hyperparams.learning_parameters.auto_adapt_batch_size != BatchSizeAdaptType.NONE: + train_func = partial(train_model, meta=deepcopy(meta), model=deepcopy(model), distributed=False) + adapt_batch_size( + train_func, + cfg, + datasets, + validate, + not_increase=(self._hyperparams.learning_parameters.auto_adapt_batch_size == BatchSizeAdaptType.SAFE), + ) + + train_model( + model, + datasets, + cfg, + distributed=cfg.distributed, + validate=validate, + timestamp=timestamp, + meta=meta, + ) + + # Save outputs + output_ckpt_path = os.path.join(cfg.work_dir, "latest.pth") + best_ckpt_path = glob.glob(os.path.join(cfg.work_dir, "best_*.pth")) + if best_ckpt_path: + output_ckpt_path = best_ckpt_path[0] + return dict( + final_ckpt=output_ckpt_path, + ) + + def _infer_model( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ): + """Main infer function.""" + self._data_cfg = ConfigDict( + data=ConfigDict( + train=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + test=ConfigDict( + otx_dataset=dataset, + labels=self._labels, + ), + ) + ) + + self._init_task() + + cfg = self.configure(False, "test", None) + logger.info("infer!") + + videos_per_gpu = cfg.data.test_dataloader.get("videos_per_gpu", 1) + + # Data loader + mm_dataset = build_dataset(cfg.data.test) + dataloader = build_dataloader( + mm_dataset, + videos_per_gpu=videos_per_gpu, + workers_per_gpu=cfg.data.test_dataloader.get("workers_per_gpu", 0), + num_gpus=len(cfg.gpu_ids), + dist=cfg.distributed, + seed=cfg.get("seed", None), + shuffle=False, + ) + + # Target classes + if "task_adapt" in cfg: + target_classes = cfg.task_adapt.final + if len(target_classes) < 1: + raise KeyError( + f"target_classes={target_classes} is empty check the metadata from model ckpt or recipe " + "configuration" + ) + else: + target_classes = mm_dataset.CLASSES + + # Model + model = self.build_model(cfg, fp16=cfg.get("fp16", False)) + model.CLASSES = target_classes + model.eval() + feature_model = model + model = build_data_parallel(model, cfg, distributed=False) + + # InferenceProgressCallback (Time Monitor enable into Infer task) + time_monitor = None + if cfg.get("custom_hooks", None): + time_monitor = [hook.time_monitor for hook in cfg.custom_hooks if hook.type == "OTXProgressHook"] + time_monitor = time_monitor[0] if time_monitor else None + if time_monitor is not None: + + # pylint: disable=unused-argument + def pre_hook(module, inp): + time_monitor.on_test_batch_begin(None, None) + + def hook(module, inp, outp): + time_monitor.on_test_batch_end(None, None) + + model.register_forward_pre_hook(pre_hook) + model.register_forward_hook(hook) + + eval_predictions = [] + feature_vectors = [] + saliency_maps = [] + + feature_vector_hook: BaseRecordingForwardHook = FeatureVectorHook(feature_model) + saliency_hook = nullcontext() + + prog_bar = ProgressBar(len(dataloader)) + with feature_vector_hook: + with saliency_hook: + for data in dataloader: + with torch.no_grad(): + result = model(return_loss=False, **data) + eval_predictions.extend(result) + for _ in range(videos_per_gpu): + prog_bar.update() + feature_vectors = feature_vector_hook.records + saliency_maps = [None] * len(mm_dataset) + prog_bar.file.write("\n") + + for key in ["interval", "tmpdir", "start", "gpu_collect", "save_best", "rule", "dynamic_intervals"]: + cfg.evaluation.pop(key, None) + + metric = None + metric_name = self._recipe_cfg.evaluation.final_metric + if inference_parameters: + if inference_parameters.is_evaluation: + metric = mm_dataset.evaluate(eval_predictions, **self._recipe_cfg.evaluation)[metric_name] + + assert len(eval_predictions) == len(feature_vectors), f"{len(eval_predictions)} != {len(feature_vectors)}" + assert len(eval_predictions) == len(saliency_maps), f"{len(eval_predictions)} != {len(saliency_maps)}" + predictions = zip(eval_predictions, feature_vectors, saliency_maps) + + return predictions, metric + + def _export_model(self, precision: ModelPrecision, export_format: ExportType, dump_features: bool): + """Main export function.""" + self._data_cfg = None + self._init_task(export=True) + + cfg = self.configure(False, "test", None) + deploy_cfg = self._init_deploy_cfg(cfg) + + state_dict = torch.load(self._model_ckpt) + if "model" in state_dict.keys(): + state_dict = state_dict["model"] + if "state_dict" in state_dict.keys(): + state_dict = state_dict["state_dict"] + + self._precision[0] = precision + half_precision = precision == ModelPrecision.FP16 + + exporter = Exporter( + cfg, + state_dict, + deploy_cfg, + f"{self._output_path}/openvino", + half_precision, + onnx_only=export_format == ExportType.ONNX, + ) + exporter.export() + + results: Dict[str, Dict[str, str]] = {"outputs": {}} + + if export_format == ExportType.ONNX: + onnx_file = [f for f in os.listdir(self._output_path) if f.endswith(".onnx")][0] + results["outputs"]["onnx"] = os.path.join(self._output_path, onnx_file) + else: + bin_file = [f for f in os.listdir(self._output_path) if f.endswith(".bin")][0] + xml_file = [f for f in os.listdir(self._output_path) if f.endswith(".xml")][0] + results["outputs"]["bin"] = os.path.join(self._output_path, bin_file) + results["outputs"]["xml"] = os.path.join(self._output_path, xml_file) + + return results + + # This should be removed + def update_override_configurations(self, config): + """Update override_configs.""" + logger.info(f"update override config with: {config}") + config = ConfigDict(**config) + self.override_configs.update(config) + + # This should moved somewhere + def _init_deploy_cfg(self, cfg: Config) -> Union[Config, None]: + base_dir = os.path.abspath(os.path.dirname(self._task_environment.model_template.model_template_path)) + deploy_cfg_path = os.path.join(base_dir, "deployment.py") + deploy_cfg = None + if os.path.exists(deploy_cfg_path): + deploy_cfg = OTXConfig.fromfile(deploy_cfg_path) + + def patch_input_preprocessing(deploy_cfg): + normalize_cfg = get_configs_by_pairs( + cfg.data.test.pipeline, + dict(type="Normalize"), + ) + assert len(normalize_cfg) == 1 + normalize_cfg = normalize_cfg[0] + + options = dict(flags=[], args={}) + # NOTE: OTX loads image in RGB format + # so that `to_rgb=True` means a format change to BGR instead. + # Conventionally, OpenVINO IR expects a image in BGR format + # but OpenVINO IR under OTX assumes a image in RGB format. + # + # `to_rgb=True` -> a model was trained with images in BGR format + # and a OpenVINO IR needs to reverse input format from RGB to BGR + # `to_rgb=False` -> a model was trained with images in RGB format + # and a OpenVINO IR does not need to do a reverse + if normalize_cfg.get("to_rgb", False): + options["flags"] += ["--reverse_input_channels"] + # value must be a list not a tuple + if normalize_cfg.get("mean", None) is not None: + options["args"]["--mean_values"] = list(normalize_cfg.get("mean")) + if normalize_cfg.get("std", None) is not None: + options["args"]["--scale_values"] = list(normalize_cfg.get("std")) + + # fill default + backend_config = deploy_cfg.backend_config + if backend_config.get("mo_options") is None: + backend_config.mo_options = ConfigDict() + mo_options = backend_config.mo_options + if mo_options.get("args") is None: + mo_options.args = ConfigDict() + if mo_options.get("flags") is None: + mo_options.flags = [] + + # already defiend options have higher priority + options["args"].update(mo_options.args) + mo_options.args = ConfigDict(options["args"]) + # make sure no duplicates + mo_options.flags.extend(options["flags"]) + mo_options.flags = list(set(mo_options.flags)) + + patch_input_preprocessing(deploy_cfg) + if not deploy_cfg.backend_config.get("model_inputs", []): + raise NotImplementedError("Video recognition task must specify model input info in deployment.py") + + return deploy_cfg + + # These need to be moved somewhere + def _update_caching_modules(self, data_cfg: Config) -> None: + def _find_max_num_workers(cfg: dict): + num_workers = [0] + for key, value in cfg.items(): + if key == "workers_per_gpu" and isinstance(value, int): + num_workers += [value] + elif isinstance(value, dict): + num_workers += [_find_max_num_workers(value)] + + return max(num_workers) + + def _get_mem_cache_size(): + if not hasattr(self._hyperparams.algo_backend, "mem_cache_size"): + return 0 + + return self._hyperparams.algo_backend.mem_cache_size + + max_num_workers = _find_max_num_workers(data_cfg) + mem_cache_size = _get_mem_cache_size() + + mode = "multiprocessing" if max_num_workers > 0 else "singleprocessing" + caching.MemCacheHandlerSingleton.create(mode, mem_cache_size) + + update_or_add_custom_hook( + self._recipe_cfg, + ConfigDict(type="MemCacheHook", priority="VERY_LOW"), + ) diff --git a/src/otx/algorithms/action/adapters/mmaction/utils/__init__.py b/src/otx/algorithms/action/adapters/mmaction/utils/__init__.py new file mode 100644 index 00000000000..788b89a8b30 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/utils/__init__.py @@ -0,0 +1,9 @@ +"""OTX Adapters - mmaction.utils.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .det_eval_utils import det_eval +from .export_utils import Exporter + +__all__ = ["det_eval", "Exporter"] diff --git a/src/otx/algorithms/action/adapters/mmaction/utils/det_eval_utils.py b/src/otx/algorithms/action/adapters/mmaction/utils/det_eval_utils.py new file mode 100644 index 00000000000..9653158a609 --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/utils/det_eval_utils.py @@ -0,0 +1,128 @@ +"""Collection of Action detection evaluiation utils..""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import time +from collections import defaultdict + +import numpy as np +from mmaction.core.evaluation.ava_evaluation import ( + object_detection_evaluation as mm_det_eval, +) +from mmaction.core.evaluation.ava_evaluation import standard_fields +from mmaction.core.evaluation.ava_utils import print_time, read_exclusions + +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-locals, too-many-branches +def det_eval(predictions, result_type, labels, video_infos, exclude_file, verbose=True, custom_classes=None): + """Evaluation method for AVA Dataset.""" + + assert result_type in ["mAP"] + + start = time.time() + categories, class_whitelist = _read_labelmap(labels) + if custom_classes is not None: + custom_classes = custom_classes[1:] + assert set(custom_classes).issubset(set(class_whitelist)) + class_whitelist = custom_classes + categories = [cat for cat in categories if cat["id"] in custom_classes] + + # loading gt, do not need gt score + gt_boxes, gt_labels = _load_gt(video_infos) + if verbose: + print_time("Reading detection results", start) + + if exclude_file is not None: + with open(exclude_file, encoding="utf-8") as ex_file: + excluded_keys = read_exclusions(ex_file) + else: + excluded_keys = [] + + start = time.time() + boxes, labels, scores = predictions + if verbose: + print_time("Reading detection results", start) + + # Evaluation for mAP + pascal_evaluator = mm_det_eval.PascalDetectionEvaluator(categories) + + start = time.time() + for image_key in gt_boxes: + if verbose and image_key in excluded_keys: + logger.info("Found excluded timestamp in detections: %s. It will be ignored.", image_key) + continue + pascal_evaluator.add_single_ground_truth_image_info( + image_key, + { + standard_fields.InputDataFields.groundtruth_boxes: np.array(gt_boxes[image_key], dtype=float), + standard_fields.InputDataFields.groundtruth_classes: np.array(gt_labels[image_key], dtype=int), + }, + ) + if verbose: + print_time("Convert groundtruth", start) + + start = time.time() + for image_key in boxes: + if verbose and image_key in excluded_keys: + logger.info("Found excluded timestamp in detections: %s. It will be ignored.", image_key) + continue + pascal_evaluator.add_single_detected_image_info( + image_key, + { + standard_fields.DetectionResultFields.detection_boxes: np.array(boxes[image_key], dtype=float), + standard_fields.DetectionResultFields.detection_classes: np.array(labels[image_key], dtype=int), + standard_fields.DetectionResultFields.detection_scores: np.array(scores[image_key], dtype=float), + }, + ) + if verbose: + print_time("convert detections", start) + + start = time.time() + metrics = pascal_evaluator.evaluate() + if verbose: + print_time("run_evaluator", start) + for display_name, value in metrics.items(): + print(f"{display_name}=\t{value}") + return {display_name: value for display_name, value in metrics.items() if "ByCategory" not in display_name} + + +def _read_labelmap(labels): + """Generate label map from LabelEntity.""" + labelmap = [] + class_ids = set() + for label in labels: + labelmap.append({"id": int(label.id), "name": str(label.name)}) + class_ids.add(int(label.id)) + return labelmap, class_ids + + +def _load_gt(video_infos): + """Generate ground truth information from video_infos.""" + boxes = defaultdict(list) + labels = defaultdict(list) + for video_info in video_infos: + img_key = video_info["img_key"] + gt_bboxes = video_info["gt_bboxes"] + gt_labels = video_info["gt_labels"] + for gt_label, gt_bbox in zip(gt_labels, gt_bboxes): + for idx, val in enumerate(gt_label): + if val == 1: + boxes[img_key].append(gt_bbox) + labels[img_key].append(idx) + return boxes, labels diff --git a/src/otx/algorithms/action/adapters/mmaction/utils/export_utils.py b/src/otx/algorithms/action/adapters/mmaction/utils/export_utils.py new file mode 100644 index 00000000000..4cfa34ed88a --- /dev/null +++ b/src/otx/algorithms/action/adapters/mmaction/utils/export_utils.py @@ -0,0 +1,148 @@ +"""Utils for Action recognition OpenVINO export task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from collections import OrderedDict +from typing import Any, Dict, Optional, Tuple + +import numpy as np +import torch +from mmcv.runner import BaseModule +from mmcv.utils import Config +from mmdeploy.apis import build_task_processor +from mmdeploy.apis.core.pipeline_manager import no_mp +from mmdeploy.apis.onnx import export +from mmdeploy.backend.openvino.onnx2openvino import from_onnx +from mmdeploy.backend.openvino.utils import ModelOptimizerOptions + +from otx.algorithms.action.adapters.mmaction.models import AVAFastRCNN + + +def _convert_sync_batch_to_normal_batch(module: BaseModule): + """Convert the syncBNs into normal BN3ds.""" + module_output = module + if isinstance(module, torch.nn.SyncBatchNorm): + module_output = torch.nn.BatchNorm3d( + module.num_features, module.eps, module.momentum, module.affine, module.track_running_stats + ) + if module.affine: + module_output.weight.data = module.weight.data.clone().detach() + module_output.bias.data = module.bias.data.clone().detach() + # keep requires_grad unchanged + module_output.weight.requires_grad = module.weight.requires_grad + module_output.bias.requires_grad = module.bias.requires_grad + module_output.running_mean = module.running_mean + module_output.running_var = module.running_var + module_output.num_batches_tracked = module.num_batches_tracked + for name, child in module.named_children(): + module_output.add_module(name, _convert_sync_batch_to_normal_batch(child)) + del module + return module_output + + +# pylint: disable=too-many-instance-attributes +class Exporter: + """Export class for action recognition model using mmdeploy framework.""" + + def __init__( + self, + recipe_cfg: Config, + weights: OrderedDict, + deploy_cfg: Config, + work_dir: str, + half_precision: bool, + onnx_only: bool, + ): + """Initialize Exporter. + + Args: + recipe_cfg (Config): recipe config which contains model config + weights (str): model weights + deploy_cfg (Config): deploy config which contains deploy info + work_dir (str): path to save onnx and openvino xml file + half_precision (bool): whether to use half-precision(FP16) + onnx_only (bool): whether to export only onnx model + """ + + self.task_processor = build_task_processor(recipe_cfg, deploy_cfg, "cpu") + self.weights = weights + + self.deploy_cfg = deploy_cfg + + self.model = self._get_model() + self.input_tensor, self.input_metas = self._get_inputs() + self.work_dir = work_dir + self.context_info = {"deploy_cfg": deploy_cfg} + if half_precision: + self.deploy_cfg.backend_config.mo_options["flags"] = ["--compress_to_fp16"] + self.onnx_only = onnx_only + + def _get_model(self) -> torch.nn.Module: + """Prepare torch model for exporting.""" + + model = self.task_processor.init_pytorch_model(None) + model.load_state_dict(self.weights) + if isinstance(model, AVAFastRCNN): + model.patch_for_export() + return _convert_sync_batch_to_normal_batch(model) + + def _get_inputs(self) -> Tuple[torch.Tensor, Optional[Dict[str, Any]]]: + """Prepare torch model's input and input_metas.""" + + height, width = self.deploy_cfg.backend_config.model_inputs[0]["opt_shapes"]["input"][-2:] + if isinstance(self.model, AVAFastRCNN): + input_tensor = torch.randn(1, 3, 32, height, width) + input_metas = { + "img_metas": [ + [ + { + "ori_shape": (height, width), + "img_shape": (height, width), + "pad_shape": (height, width), + "scale_factor": np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float32), + } + ] + ] + } + else: + input_tensor = torch.randn(1, 1, 3, 32, height, width) + input_metas = None + return input_tensor, input_metas + + def export(self): + """Export action model using mmdeploy apis.""" + + with no_mp(): + export( + self.model, + self.input_tensor, + self.work_dir, + "onnxruntime", + self.input_metas, + self.context_info, + self.deploy_cfg.ir_config.input_names, + self.deploy_cfg.ir_config.output_names, + ) + + if self.onnx_only: + return + + from_onnx( + self.work_dir + ".onnx", + self.work_dir.replace("openvino", ""), + {self.deploy_cfg.ir_config.input_names[0]: self.input_tensor.shape}, + self.deploy_cfg.ir_config.output_names, + ModelOptimizerOptions(self.deploy_cfg.backend_config.mo_options), + ) diff --git a/src/otx/algorithms/action/adapters/openvino/__init__.py b/src/otx/algorithms/action/adapters/openvino/__init__.py new file mode 100644 index 00000000000..776c908de76 --- /dev/null +++ b/src/otx/algorithms/action/adapters/openvino/__init__.py @@ -0,0 +1,20 @@ +"""OpenVINO Adapters for Action Recognition.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .dataloader import ActionOVClsDataLoader, get_ovdataloader +from .model_wrappers import OTXOVActionCls, OTXOVActionDet + +__all__ = ["OTXOVActionCls", "OTXOVActionDet", "ActionOVClsDataLoader", "get_ovdataloader"] diff --git a/src/otx/algorithms/action/adapters/openvino/dataloader.py b/src/otx/algorithms/action/adapters/openvino/dataloader.py new file mode 100644 index 00000000000..d987f322390 --- /dev/null +++ b/src/otx/algorithms/action/adapters/openvino/dataloader.py @@ -0,0 +1,229 @@ +"""Data loaders for OpenVINO action recognition models.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from copy import deepcopy +from typing import Dict, List + +import numpy as np + +from otx.api.entities.annotation import AnnotationSceneEntity +from otx.api.entities.datasets import DatasetEntity, DatasetItemEntity + + +def get_ovdataloader(dataset: DatasetEntity, task_type: str, clip_len: int, width: int, height: int): + """Find proper dataloader for dataset and task type. + + If dataset has only a single video, this returns DataLoader for online demo + If dataset has multiple videos, this return DataLoader for academia evaluation + """ + if _is_multi_video(dataset): + if task_type == "ACTION_CLASSIFICATION": + return ActionOVClsDataLoader(dataset, clip_len, width, height) + if task_type == "ACTION_DETECTION": + return ActionOVDetDataLoader(dataset, clip_len, width, height) + raise NotImplementedError(f"{task_type} is not supported from action task") + return ActionOVDemoDataLoader(dataset, task_type, clip_len, width, height) + + +def _is_multi_video(dataset: DatasetEntity) -> bool: + """Check dataset has multiple videos.""" + _video_id = dataset[0].get_metadata()[0].data.video_id + for data in dataset: + video_id = data.get_metadata()[0].data.video_id + if _video_id != video_id: + return True + return False + + +class ActionOVDemoDataLoader: + """DataLoader for online demo purpose. + + Since it is for online demo purpose it selects background frames from neighbor of key frame + """ + + def __init__(self, dataset: DatasetEntity, task_type: str, clip_len: int, width: int, height: int): + self.task_type = task_type + self.dataset = dataset + self.clip_len = clip_len + self.width = width + self.height = height + self.interval = 2 + + def __len__(self): + """Length of data loader.""" + return len(self.dataset) + + def __getitem__(self, index: int): + """Sample frames from back and forth of key frame.""" + start = index - (self.clip_len // 2) * self.interval + end = index + ((self.clip_len + 1) // 2) * self.interval + frame_inds = list(range(start, end, self.interval)) + frame_inds = np.clip(frame_inds, 0, len(self.dataset) - 1) + dataset_items = [] + for idx in frame_inds: + dataset_item = self.dataset[int(idx)] + dataset_items.append(dataset_item) + return dataset_items + + def add_prediction(self, data: List[DatasetItemEntity], prediction: AnnotationSceneEntity): + """Add prediction results to key frame. + + From sampling methods, we know that data[len(data) // 2] is key frame + """ + dataset_item = data[len(data) // 2] + if self.task_type == "ACTION_CLASSIFICATION": + dataset_item.append_labels(prediction.annotations[0].get_labels()) + else: + dataset_item.append_annotations(prediction.annotations) + + +class ActionOVClsDataLoader: + """DataLoader for evaluation of action classification models. + + It iterates through clustered video, and it samples frames from given video + """ + + def __init__(self, dataset: DatasetEntity, clip_len: int, width: int, height: int): + self.clip_len = clip_len + self.width = width + self.height = height + + video_info: Dict[str, List[DatasetItemEntity]] = {} + for dataset_item in dataset: + metadata = dataset_item.get_metadata()[0].data + video_id = metadata.video_id + if video_id in video_info: + video_info[video_id].append(dataset_item) + else: + video_info[video_id] = [dataset_item] + self.dataset = list(video_info.values()) + + self.interval = 4 + + def __len__(self): + """Length of data loader.""" + return len(self.dataset) + + def __getitem__(self, index: int): + """Sample frames from given video.""" + items = self.dataset[index] + indices = self._get_indices(len(items)) + dataset_items = [] + for idx in indices: + dataset_item = items[idx] + dataset_items.append(dataset_item) + return dataset_items + + def _get_indices(self, video_len: int): + """Sample frame indices from video length.""" + ori_clip_len = self.clip_len * self.interval + if video_len > ori_clip_len - 1: + start = (video_len - ori_clip_len + 1) / 2 + else: + start = 0 + frame_inds = np.arange(self.clip_len) * self.interval + int(start) + frame_inds = np.mod(frame_inds, video_len) + frame_inds = frame_inds.astype(np.int) + return frame_inds + + def add_prediction(self, dataset: DatasetEntity, data: List[DatasetItemEntity], prediction: AnnotationSceneEntity): + """Add prediction to dataset. + + Add prediction result to dataset_item in dataset, which has same video id with video data. + """ + video_id = data[0].get_metadata()[0].data.video_id + for dataset_item in dataset: + if dataset_item.get_metadata()[0].data.video_id == video_id: + dataset_item.append_labels(prediction.annotations[0].get_labels()) + + +class ActionOVDetDataLoader: + """DataLoader for evaluation of spatio-temporal action detection models. + + It iterates through DatasetEntity, which only contains non-empty frame(frame with actor annotation) + It samples background frames from original DatasetEntity, which contain both empty frame and non-empty frame + """ + + def __init__(self, dataset: DatasetEntity, clip_len: int, width: int, height: int): + self.original_dataset = dataset + self.clip_len = clip_len + self.width = width + self.height = height + + self.dataset = deepcopy(dataset) + video_info: Dict[str, Dict[str, int]] = {} + for idx, dataset_item in enumerate(self.dataset): + metadata = dataset_item.get_metadata()[0].data + video_id = metadata.video_id + timestamp = metadata.frame_idx + if video_id in video_info: + if video_info[video_id]["timestamp_start"] > timestamp: + video_info[video_id]["timestamp_start"] = timestamp + if video_info[video_id]["timestamp_end"] < timestamp: + video_info[video_id]["timestamp_end"] = timestamp + else: + video_info[video_id] = { + "start_index": idx, + "timestamp_start": timestamp, + "timestamp_end": timestamp, + } + remove_indices = [] + for idx, dataset_item in enumerate(self.dataset): + metadata = dataset_item.get_metadata()[0].data + if metadata.is_empty_frame: + remove_indices.append(idx) + continue + metadata.update("start_index", video_info[metadata.video_id]["start_index"]) + metadata.update("timestamp_start", video_info[metadata.video_id]["timestamp_start"]) + metadata.update("timestamp_end", video_info[metadata.video_id]["timestamp_end"]) + self.dataset.remove_at_indices(remove_indices) + + self.interval = 2 + self.fps = 1 + + def __len__(self): + """Length of data loader.""" + return len(self.dataset) + + def __getitem__(self, index: int): + """Sample frames from back and forth of key frame, and all frames are from same video with key frame.""" + metadata = self.dataset[index].get_metadata()[0].data + timestamp = metadata.frame_idx + timestamp_start = metadata.timestamp_start + timestamp_end = metadata.timestamp_end + indices = self._get_indices(timestamp, timestamp_start, timestamp_end) + indices = indices - timestamp_start + metadata.start_index + dataset_items = [] + for idx in indices: + dataset_item = self.original_dataset[int(idx)] + dataset_items.append(dataset_item) + return dataset_items + + def _get_indices(self, timestamp: int, timestamp_start: int, timestamp_end: int): + """Get indices from timestamp. + + Samples from back and forth of key timestamp, and clips using start, and end timestamp + """ + start = timestamp - (self.clip_len // 2) * self.interval + end = timestamp + ((self.clip_len + 1) // 2) * self.interval + frame_inds = list(range(start, end, self.interval)) + frame_inds = np.clip(frame_inds, timestamp_start, timestamp_end) + return frame_inds + + def add_prediction(self, data: List[DatasetItemEntity], prediction: AnnotationSceneEntity): + """Add prediction results to key frame.""" + dataset_item = data[len(data) // 2] + dataset_item.append_annotations(prediction.annotations) diff --git a/src/otx/algorithms/action/adapters/openvino/model_wrappers/__init__.py b/src/otx/algorithms/action/adapters/openvino/model_wrappers/__init__.py new file mode 100644 index 00000000000..6e9706fff13 --- /dev/null +++ b/src/otx/algorithms/action/adapters/openvino/model_wrappers/__init__.py @@ -0,0 +1,19 @@ +"""Model wrappers.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .openvino_models import OTXOVActionCls, OTXOVActionDet + +__all__ = ["OTXOVActionCls", "OTXOVActionDet"] diff --git a/src/otx/algorithms/action/adapters/openvino/model_wrappers/openvino_models.py b/src/otx/algorithms/action/adapters/openvino/model_wrappers/openvino_models.py new file mode 100644 index 00000000000..94b920900b1 --- /dev/null +++ b/src/otx/algorithms/action/adapters/openvino/model_wrappers/openvino_models.py @@ -0,0 +1,186 @@ +"""Model wrapper file for openvino.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +from typing import Any, Dict, List + +import numpy as np +from openvino.model_api.adapters import OpenvinoAdapter +from openvino.model_api.adapters.utils import RESIZE_TYPES, InputTransform +from openvino.model_api.models.model import Model +from openvino.model_api.models.utils import ( + ClassificationResult, + Detection, +) + +from otx.api.entities.datasets import DatasetItemEntity + + +def softmax_numpy(x: np.ndarray): + """Softmax numpy.""" + x = np.exp(x - np.max(x)) + x /= np.sum(x) + return x + + +def get_multiclass_predictions(logits: np.ndarray, activate: bool = True): + """Get multiclass predictions.""" + index = np.argmax(logits) + if activate: + logits = softmax_numpy(logits) + + return ClassificationResult([(index, logits[index])], np.ndarray(0), np.ndarray(0), np.ndarray(0)) + + +# pylint: disable=too-many-instance-attributes +class OTXOVActionCls(Model): + """OTX Action Classification model for openvino task.""" + + __model__ = "ACTION_CLASSIFICATION" + + def __init__(self, model_adapter: OpenvinoAdapter, configuration=None, preload=False): + """Image model constructor. + + Calls the `Model` constructor first + """ + super().__init__(model_adapter, configuration, preload) + self.image_blob_names, self.image_info_blob_names = self._get_inputs() + self.image_blob_name = self.image_blob_names[0] + self.out_layer_name = self._get_outputs() + + _, self.n, self.c, self.t, self.h, self.w = self.inputs[self.image_blob_name].shape + self.resize = RESIZE_TYPES["standard"] + self.input_transform = InputTransform(False, None, None) + + # FIXME Below parameters should be changed dynamically from data pipeline config + self.interval = 4 + + def _get_inputs(self): + image_blob_names, image_info_blob_names = [], [] + for name, metadata in self.inputs.items(): + if len(metadata.shape) == 6: + image_blob_names.append(name) + elif len(metadata.shape) == 4: + image_info_blob_names.append(name) + return image_blob_names, image_info_blob_names + + def _get_outputs(self): + layer_name = "logits" + for name, meta in self.outputs.items(): + if "logits" in meta.names: + layer_name = name + return layer_name + + def preprocess(self, inputs: List[DatasetItemEntity]): + """Pre-process.""" + meta = {"original_shape": inputs[0].media.numpy.shape} + frames = [] + for item in inputs: + frame = item.media.numpy + frame = self.resize(frame, (self.w, self.h)) + frames.append(frame) + np_frames = self._reshape(frames) + dict_inputs = {self.image_blob_name: np_frames} + meta.update({"resized_shape": np_frames[0].shape}) + return dict_inputs, meta + + @staticmethod + def _reshape(inputs: List[np.ndarray]) -> np.ndarray: + """Reshape(expand, transpose, permute) the input np.ndarray.""" + np_inputs = np.expand_dims(inputs, axis=(0, 1)) # [1, 1, T, H, W, C] + np_inputs = np_inputs.transpose(0, 1, -1, 2, 3, 4) # [1, 1, C, T, H, W] + return np_inputs + + # pylint: disable=unused-argument + def postprocess(self, outputs: Dict[str, np.ndarray], meta: Dict[str, Any]): + """Post-process.""" + logits = outputs[self.out_layer_name].squeeze() + return get_multiclass_predictions(logits) + + +class OTXOVActionDet(Model): + """OTX Action Detection model for openvino task.""" + + __model__ = "ACTION_DETECTION" + + def __init__(self, model_adapter: OpenvinoAdapter, configuration=None, preload=False): + """Image model constructor. + + Calls the `Model` constructor first + """ + super().__init__(model_adapter, configuration, preload) + self.image_blob_names = self._get_inputs() + self.image_blob_name = self.image_blob_names[0] + self.out_layer_names = self._get_outputs() + + self.n, self.c, self.t, self.h, self.w = self.inputs[self.image_blob_name].shape + self.resize = RESIZE_TYPES["standard"] + self.input_transform = InputTransform(False, None, None) + + # FIXME Below parameters should be changed dynamically from data pipeline config + self.interval = 1 + self.fps = 1 + + def _get_inputs(self): + image_blob_names = [] + for name, metadata in self.inputs.items(): + if len(metadata.shape) == 5: + image_blob_names.append(name) + return image_blob_names + + def _get_outputs(self): + out_names = {} + for name in self.outputs: + if "bboxes" in name: + out_names["bboxes"] = name + elif "labels" in name: + out_names["labels"] = name + return out_names + + def preprocess(self, inputs: List[DatasetItemEntity]): + """Pre-process.""" + meta = {"original_shape": inputs[0].media.numpy.shape} + frames = [] + for item in inputs: + frame = item.media.numpy + frame = self.resize(frame, (self.w, self.h)) + frames.append(frame) + np_frames = self.reshape(frames) + dict_inputs = {self.image_blob_name: np_frames} + meta.update({"resized_shape": np_frames.shape}) + return dict_inputs, meta + + @staticmethod + def reshape(inputs: List[np.ndarray]) -> np.ndarray: + """Reshape(expand, transpose, permute) the input np.ndarray.""" + np_inputs = np.expand_dims(inputs, axis=0) # [1, T, H, W, C] + np_inputs = np_inputs.transpose(0, -1, 1, 2, 3) # [1, C, T, H, W] + return np_inputs + + def postprocess(self, outputs: Dict[str, np.ndarray], meta: Dict[str, Any]): + """Post-process.""" + # TODO Support multi label classification + H, W, _ = meta["original_shape"] + bboxes = outputs[self.out_layer_names["bboxes"]] + labels = outputs[self.out_layer_names["labels"]] + scores = labels[:, 1:].max(axis=1) + labels = labels[:, 1:].argmax(axis=1) + results = [] + for bbox, score, label in zip(bboxes, scores, labels): + bbox *= [W, H, W, H] + results.append(Detection(*bbox, score, label)) + return results diff --git a/src/otx/algorithms/action/adapters/openvino/task.py b/src/otx/algorithms/action/adapters/openvino/task.py new file mode 100644 index 00000000000..6adf7e49a73 --- /dev/null +++ b/src/otx/algorithms/action/adapters/openvino/task.py @@ -0,0 +1,337 @@ +"""Openvino Task of OTX Action Recognition.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import io +import json +import os +import random +import tempfile +from typing import Any, Dict, List, Optional, Tuple, Union +from zipfile import ZipFile + +import nncf +import numpy as np +import openvino.runtime as ov +from mmcv.utils import ProgressBar +from nncf.common.quantization.structs import QuantizationPreset +from openvino.model_api.adapters import OpenvinoAdapter, create_core +from openvino.model_api.models import Model + +from otx.algorithms.action.adapters.openvino import ( + ActionOVClsDataLoader, + get_ovdataloader, + model_wrappers, +) +from otx.algorithms.action.configs.base import ActionConfig +from otx.algorithms.common.utils.ir import check_if_quantized +from otx.api.entities.annotation import AnnotationSceneEntity +from otx.api.entities.datasets import DatasetEntity, DatasetItemEntity +from otx.api.entities.inference_parameters import ( + InferenceParameters, + default_progress_callback, +) +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ( + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.serialization.label_mapper import LabelSchemaMapper, label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.exportable_code import demo +from otx.api.usecases.exportable_code.inference.inference import IInferencer +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( + ClassificationToAnnotationConverter, + DetectionBoxToAnnotationConverter, + IPredictionToAnnotationConverter, +) +from otx.api.usecases.tasks.interfaces.deployment_interface import IDeploymentTask +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.optimization_interface import ( + IOptimizationTask, + OptimizationType, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +# TODO: refactoring to Sphinx style. +class ActionOpenVINOInferencer(IInferencer): + """ActionOpenVINOInferencer class in OpenVINO task for action recognition.""" + + def __init__( + self, + task_type: str, + hparams: ActionConfig, + label_schema: LabelSchemaEntity, + model_file: Union[str, bytes], + weight_file: Union[str, bytes, None] = None, + device: str = "CPU", + num_requests: int = 1, + ): # pylint: disable=unused-argument, too-many-arguments + """Inferencer implementation for OTX Action Recognition using OpenVINO backend. + + Args: + task_type (str): Type of action task. ["ACTION_CLASSIFICATION", "ACTION_DETECTION"] + hparams (ActionConfig): Hyperparameters for action task + label_schema (LabelSchemaEntity): Label schema for model file + model_file (Union[str, bytes]): XML file for model structure + weight_file (Union[str, bytes, None]): Model weights file + Default: None + device (str): Device for inference. Default: "CPU" + num_requests (int): Number of requests + """ + + self.task_type = task_type + self.label_schema = label_schema + model_adapter = OpenvinoAdapter( + create_core(), model_file, weight_file, device=device, max_num_requests=num_requests + ) + self.configuration: Dict[Any, Any] = {} + self.model = Model.create_model(model_adapter, self.task_type, self.configuration, preload=True) + self.converter: IPredictionToAnnotationConverter + if self.task_type == "ACTION_CLASSIFICATION": + self.converter = ClassificationToAnnotationConverter(self.label_schema) + else: + self.converter = DetectionBoxToAnnotationConverter(self.label_schema) + + def pre_process(self, image: List[DatasetItemEntity]) -> Tuple[Dict[str, np.ndarray], Dict[str, Any]]: + """Pre-process function of OpenVINO Inferencer for Action Recognition.""" + return self.model.preprocess(image) + + def post_process( + self, prediction: Dict[str, np.ndarray], metadata: Dict[str, Any] + ) -> Optional[AnnotationSceneEntity]: + """Post-process function of OpenVINO Inferencer for Action Recognition.""" + + prediction = self.model.postprocess(prediction, metadata) + return self.converter.convert_to_annotation(prediction, metadata) + + def predict(self, image: List[DatasetItemEntity]) -> AnnotationSceneEntity: + """Predict function of OpenVINO Action Inferencer for Action Recognition.""" + data, metadata = self.pre_process(image) + raw_predictions = self.forward(data) + predictions = self.post_process(raw_predictions, metadata) + return predictions + + def forward(self, image: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]: + """Forward function of OpenVINO Action Inferencer for Action Recognition.""" + + return self.model.infer_sync(image) + + +class DataLoaderWrapper: + """DataLoader implementation for ActionOpenVINOTask.""" + + def __init__(self, dataloader: Any, inferencer: IInferencer, shuffle: bool = True): + self.dataloader = dataloader + self.inferencer = inferencer + self.shuffler = None + if shuffle: + self.shuffler = list(range(len(dataloader))) + random.shuffle(self.shuffler) + + def __getitem__(self, index: int): + """Get item from dataset.""" + if self.shuffler is not None: + index = self.shuffler[index] + + item = self.dataloader[index] + annotation = item[len(item) // 2].annotation_scene + inputs, _ = self.inferencer.model.preprocess(item) + return inputs, annotation + + def __len__(self): + """Get length of dataset.""" + return len(self.dataloader) + + +class ActionOpenVINOTask(IDeploymentTask, IInferenceTask, IEvaluationTask, IOptimizationTask): + """Task implementation for OTX Action Recognition using OpenVINO backend.""" + + def __init__(self, task_environment: TaskEnvironment): + self.task_environment = task_environment + self.hparams = self.task_environment.get_hyper_parameters(ActionConfig) + self.model = self.task_environment.model + self.task_type = self.task_environment.model_template.task_type.name + self.inferencer = self.load_inferencer() + + def load_inferencer(self) -> ActionOpenVINOInferencer: + """load_inferencer function of OpenVINOTask for Action Recognition.""" + + if self.model is None: + raise RuntimeError("load_inferencer failed, model is None") + + return ActionOpenVINOInferencer( + self.task_type, + self.hparams, + self.task_environment.label_schema, + self.model.get_data("openvino.xml"), + self.model.get_data("openvino.bin"), + ) + + # pylint: disable=no-value-for-parameter + def infer( + self, dataset: DatasetEntity, inference_parameters: Optional[InferenceParameters] = None + ) -> DatasetEntity: + """Infer function of OpenVINOTask for Action Recognition.""" + update_progress_callback = default_progress_callback + clip_len = self.inferencer.model.t + width = self.inferencer.model.w + height = self.inferencer.model.h + dataloader = get_ovdataloader(dataset, self.task_type, clip_len, width, height) + dataset_size = len(dataloader) + prog_bar = ProgressBar(len(dataloader)) + for i, data in enumerate(dataloader): + prediction = self.inferencer.predict(data) + if isinstance(dataloader, ActionOVClsDataLoader): + dataloader.add_prediction(dataset, data, prediction) + else: + dataloader.add_prediction(data, prediction) + update_progress_callback(int(i / dataset_size * 100)) + prog_bar.update() + print("") + return dataset + + def evaluate(self, output_resultset: ResultSetEntity, evaluation_metric: Optional[str] = None): + """Evaluate function of OpenVINOTask.""" + + if evaluation_metric is not None: + logger.warning(f"Requested to use {evaluation_metric} metric," "but parameter is ignored.") + if self.task_type == "ACTION_CLASSIFICATION": + output_resultset.performance = MetricsHelper.compute_accuracy(output_resultset).get_performance() + elif self.task_type == "ACTION_DETECTION": + output_resultset.performance = MetricsHelper.compute_f_measure(output_resultset).get_performance() + + def deploy(self, output_model: ModelEntity) -> None: + """Deploy function of OpenVINOTask.""" + + logger.info("Deploying the model") + + work_dir = os.path.dirname(demo.__file__) + parameters = {} # type: Dict[Any, Any] + parameters["type_of_model"] = f"otx_{self.task_type.lower()}" + parameters["converter_type"] = f"{self.task_type}" + parameters["model_parameters"] = self.inferencer.configuration + parameters["model_parameters"]["labels"] = LabelSchemaMapper.forward(self.task_environment.label_schema) + + if self.model is None: + raise RuntimeError("deploy failed, model is None") + + zip_buffer = io.BytesIO() + with ZipFile(zip_buffer, "w") as arch: + # model files + arch.writestr(os.path.join("model", "model.xml"), self.model.get_data("openvino.xml")) + arch.writestr(os.path.join("model", "model.bin"), self.model.get_data("openvino.bin")) + arch.writestr(os.path.join("model", "config.json"), json.dumps(parameters, ensure_ascii=False, indent=4)) + # model_wrappers files + for root, _, files in os.walk(os.path.dirname(model_wrappers.__file__)): + if "__pycache__" in root: + continue + for file in files: + file_path = os.path.join(root, file) + arch.write( + file_path, os.path.join("python", "model_wrappers", file_path.split("model_wrappers/")[1]) + ) + # other python files + arch.write(os.path.join(work_dir, "requirements.txt"), os.path.join("python", "requirements.txt")) + arch.write(os.path.join(work_dir, "LICENSE"), os.path.join("python", "LICENSE")) + arch.write(os.path.join(work_dir, "demo.py"), os.path.join("python", "demo.py")) + arch.write(os.path.join(work_dir, "README.md"), os.path.join(".", "README.md")) + output_model.exportable_code = zip_buffer.getvalue() + logger.info("Deploying completed") + + def optimize( + self, + optimization_type: OptimizationType, + dataset: DatasetEntity, + output_model: ModelEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + ): # pylint: disable=too-many-locals + """Optimize function of OpenVINOTask.""" + + if optimization_type is not OptimizationType.POT: + raise ValueError("PTQ is the only supported optimization type for OpenVino models") + + clip_len = self.inferencer.model.t + width = self.inferencer.model.w + height = self.inferencer.model.h + dataset = dataset.get_subset(Subset.TRAINING) + data_loader = get_ovdataloader(dataset, self.task_type, clip_len, width, height) + data_loader = DataLoaderWrapper(data_loader, self.inferencer) + quantization_dataset = nncf.Dataset(data_loader, lambda data: data[0]) + + if self.model is None: + raise RuntimeError("optimize failed, model is None") + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, "model.xml") + bin_path = os.path.join(tempdir, "model.bin") + with open(xml_path, "wb") as f: + f.write(self.model.get_data("openvino.xml")) + with open(bin_path, "wb") as f: + f.write(self.model.get_data("openvino.bin")) + + ov_model = ov.Core().read_model(xml_path) + if check_if_quantized(ov_model): + raise RuntimeError("Model is already optimized by PTQ") + + if optimization_parameters is not None: + optimization_parameters.update_progress(10, None) + + stat_subset_size = self.hparams.pot_parameters.stat_subset_size + preset = QuantizationPreset(self.hparams.pot_parameters.preset.name.lower()) + + compressed_model = nncf.quantize( + ov_model, + quantization_dataset, + subset_size=min(stat_subset_size, len(data_loader)), + preset=preset, + ) + + if optimization_parameters is not None: + optimization_parameters.update_progress(90, None) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, "model.xml") + ov.save_model(compressed_model, xml_path) + with open(xml_path, "rb") as f: + output_model.set_data("openvino.xml", f.read()) + with open(os.path.join(tempdir, "model.bin"), "rb") as f: + output_model.set_data("openvino.bin", f.read()) + + output_model.set_data("label_schema.json", label_schema_to_bytes(self.task_environment.label_schema)) + + # set model attributes for quantized model + output_model.model_format = ModelFormat.OPENVINO + output_model.optimization_type = ModelOptimizationType.POT + output_model.optimization_methods = [OptimizationMethod.QUANTIZATION] + output_model.precision = [ModelPrecision.INT8] + + self.model = output_model + self.inferencer = self.load_inferencer() + + if optimization_parameters is not None: + optimization_parameters.update_progress(100, None) + logger.info("PTQ optimization completed") diff --git a/src/otx/algorithms/action/configs/__init__.py b/src/otx/algorithms/action/configs/__init__.py new file mode 100644 index 00000000000..994ac00bb8b --- /dev/null +++ b/src/otx/algorithms/action/configs/__init__.py @@ -0,0 +1,15 @@ +"""Model configurations.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/action/configs/base/__init__.py b/src/otx/algorithms/action/configs/base/__init__.py new file mode 100644 index 00000000000..829f243f76f --- /dev/null +++ b/src/otx/algorithms/action/configs/base/__init__.py @@ -0,0 +1,19 @@ +"""Configs Initialization of OTX Action Tasks.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import ActionConfig + +__all__ = ["ActionConfig"] diff --git a/src/otx/algorithms/action/configs/base/configuration.py b/src/otx/algorithms/action/configs/base/configuration.py new file mode 100644 index 00000000000..4c2809058a9 --- /dev/null +++ b/src/otx/algorithms/action/configs/base/configuration.py @@ -0,0 +1,82 @@ +"""Configuration file of OTX Action Tasks.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.common.configs import BaseConfig, LearningRateSchedule +from otx.api.configuration.elements import ( + add_parameter_group, + boolean_attribute, + selectable, + string_attribute, +) + +# pylint: disable=invalid-name + + +# TODO Check action detection requires another config +@attrs +class ActionConfig(BaseConfig): + """Configurations of OTX Action Tasks.""" + + header = string_attribute("Configuration for an action classification task of OTX") + description = header + + @attrs + class __LearningParameters(BaseConfig.BaseLearningParameters): + header = string_attribute("Learning Parameters") + description = header + + learning_rate_schedule = selectable( + default_value=LearningRateSchedule.COSINE, + header="Learning rate schedule", + description="Specify learning rate scheduling for the MMaction task. " + "When training for a small number of epochs (N < 10), the fixed " + "schedule is recommended. For training for 10 < N < 25 epochs, " + "step-wise or exponential annealing might give better results. " + "Finally, for training on large datasets for at least 20 " + "epochs, cyclic annealing could result in the best model.", + editable=True, + visible_in_ui=True, + ) + + @attrs + class __Postprocessing(BaseConfig.BasePostprocessing): + header = string_attribute("Postprocessing") + description = header + + @attrs + class __NNCFOptimization(BaseConfig.BaseNNCFOptimization): + header = string_attribute("Optimization by NNCF") + description = header + visible_in_ui = boolean_attribute(False) + + @attrs + class __POTParameter(BaseConfig.BasePOTParameter): + header = string_attribute("POT Parameters") + description = header + visible_in_ui = boolean_attribute(False) + + @attrs + class __AlgoBackend(BaseConfig.BaseAlgoBackendParameters): + header = string_attribute("Parameters for the OTX algo-backend") + description = header + + learning_parameters = add_parameter_group(__LearningParameters) + postprocessing = add_parameter_group(__Postprocessing) + nncf_optimization = add_parameter_group(__NNCFOptimization) + pot_parameters = add_parameter_group(__POTParameter) + algo_backend = add_parameter_group(__AlgoBackend) diff --git a/src/otx/algorithms/action/configs/classification/__init__.py b/src/otx/algorithms/action/configs/classification/__init__.py new file mode 100644 index 00000000000..fc4924e56fe --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/__init__.py @@ -0,0 +1,5 @@ +"""Config for action classifcation.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/action/configs/classification/base/__init__.py b/src/otx/algorithms/action/configs/classification/base/__init__.py new file mode 100644 index 00000000000..0e496aa0233 --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/base/__init__.py @@ -0,0 +1,4 @@ +"""Configs Initialization of OTX Action Classification Tasks.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/action/configs/classification/base/base_classification_dynamic.py b/src/otx/algorithms/action/configs/classification/base/base_classification_dynamic.py new file mode 100644 index 00000000000..9e8ecb69281 --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/base/base_classification_dynamic.py @@ -0,0 +1,10 @@ +"""Dynamic Action classification mmdeply cfg.""" + +_base_ = ["./base_classification_static.py"] + +ir_config = dict( + dynamic_axes=dict( + data=dict({0: "batch"}), + logits=dict({0: "batch"}), + ), +) diff --git a/src/otx/algorithms/action/configs/classification/base/base_classification_static.py b/src/otx/algorithms/action/configs/classification/base/base_classification_static.py new file mode 100644 index 00000000000..93c330e57b1 --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/base/base_classification_static.py @@ -0,0 +1,19 @@ +"""Base Action classification mmdeply cfg.""" + +ir_config = dict( + type="onnx", + export_params=True, + keep_initializers_as_inputs=False, + opset_version=11, + save_file="end2end.onnx", + input_names=["data"], + output_names=["logits"], + input_shape=None, + optimize=False, +) +codebase_config = dict(type="mmaction", task="VideoRecognition") +backend_config = dict( + type="openvino", + mo_options=dict(args=dict({"--source_layout": "?bctwh"})), + model_inputs=[dict(opt_shapes=dict(input=[1, 1, 3, 32, 224, 224]))], +) diff --git a/src/otx/algorithms/action/configs/classification/base/supervised.py b/src/otx/algorithms/action/configs/classification/base/supervised.py new file mode 100644 index 00000000000..2eee1e0e3c1 --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/base/supervised.py @@ -0,0 +1,53 @@ +"""Base supervised learning recipe for video classification.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +seed = 2 + +evaluation = dict(interval=1, metrics=["top_k_accuracy", "mean_class_accuracy"], final_metric="mean_class_accuracy") + +optimizer = dict( + type="AdamW", + lr=0.001, + weight_decay=0.0001, +) + +optimizer_config = dict(grad_clip=dict(max_norm=40.0, norm_type=2)) +lr_config = dict( + policy="step", + step=5, + warmup="linear", + warmup_by_epoch=True, + warmup_iters=3, +) + +# runtime settings +checkpoint_config = dict(interval=1) +log_config = dict( + interval=10, + hooks=[ + dict(type="TextLoggerHook", ignore_last=False), + ], +) +# runtime settings +log_level = "INFO" +workflow = [("train", 1)] + +find_unused_parameters = False +gpu_ids = range(0, 1) + +dist_params = dict(backend="nccl") diff --git a/src/otx/algorithms/action/configs/classification/configuration.yaml b/src/otx/algorithms/action/configs/classification/configuration.yaml new file mode 100644 index 00000000000..86dcd72a68a --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/configuration.yaml @@ -0,0 +1,453 @@ +description: Configuration for an action classification task +header: Configuration for an action classification task +learning_parameters: + batch_size: + affects_outcome_of: TRAINING + default_value: 5 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 5 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + description: Learning Parameters + header: Learning Parameters + learning_rate: + affects_outcome_of: TRAINING + default_value: 0.01 + description: + Increasing this value will speed up training convergence but might + make it unstable. + editable: true + header: Learning rate + max_value: 0.1 + min_value: 1.0e-07 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.01 + visible_in_ui: true + warning: null + auto_hpo_state: NOT_POSSIBLE + learning_rate_warmup_iters: + affects_outcome_of: TRAINING + default_value: 100 + description: + In this periods of initial training iterations, the model will be trained in low learning rate, + which will be increased incrementally up to the expected learning rate setting. + This warm-up phase is known to be helpful to stabilize training, thus result in better performance. + editable: true + header: Number of iterations for learning rate warmup + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: null + num_iters: + affects_outcome_of: TRAINING + default_value: 1 + description: + Increasing this value causes the results to be more robust but training + time will be longer. + editable: true + header: Number of training iterations + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: null + num_workers: + affects_outcome_of: NONE + default_value: 0 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of cpu threads to use during batch generation + max_value: 8 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: null + enable_early_stopping: + affects_outcome_of: TRAINING + default_value: false + description: Early exit from training when validation accuracy isn't changed or decreased for several epochs. + editable: true + header: Enable early stopping of the training + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + early_stop_start: + affects_outcome_of: TRAINING + default_value: 3 + editable: true + header: Start epoch for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 3 + visible_in_ui: false + early_stop_patience: + affects_outcome_of: TRAINING + default_value: 10 + description: Training will stop if the model does not improve within the number of epochs of patience. + editable: true + header: Patience for early stopping + max_value: 50 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 10 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + early_stop_iteration_patience: + affects_outcome_of: TRAINING + default_value: 0 + description: + Training will stop if the model does not improve within the number of iterations of patience. + This ensures the model is trained enough with the number of iterations of patience before early stopping. + editable: true + header: Iteration patience for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + use_adaptive_interval: + affects_outcome_of: TRAINING + default_value: false + description: Depending on the size of iteration per epoch, adaptively update the validation interval and related values. + editable: true + header: Use adaptive validation interval + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: This will automatically control the patience and interval when early stopping is enabled. + auto_adapt_batch_size: + affects_outcome_of: TRAINING + default_value: None + description: Safe => Prevent GPU out of memory. Full => Find a batch size using most of GPU memory. + editable: true + enum_name: BatchSizeAdaptType + header: Decrease batch size if current batch size isn't fit to CUDA memory. + options: + NONE: "None" + SAFE: "Safe" + FULL: "Full" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: None + visible_in_ui: true + warning: + Enabling this could change the actual batch size depending on the current GPU status. + The learning rate also could be adjusted according to the adapted batch size. This process might change + a model performance and take some extra computation time to try a few batch size candidates. + auto_num_workers: + affects_outcome_of: TRAINING + default_value: false + description: Adapt num_workers according to current hardware status automatically. + editable: true + header: Enable auto adaptive num_workers + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +postprocessing: + confidence_threshold: + affects_outcome_of: INFERENCE + default_value: 0.35 + description: + This threshold only takes effect if the threshold is not set based + on the result. + editable: true + header: Confidence threshold + max_value: 1 + min_value: 0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + # value: 0.35 + value: 0.01 + visible_in_ui: true + warning: null + description: Postprocessing + header: Postprocessing + result_based_confidence_threshold: + affects_outcome_of: INFERENCE + default_value: true + description: Confidence threshold is derived from the results + editable: true + header: Result based confidence threshold + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +algo_backend: + description: parameters for algo backend + header: Algo backend parameters + train_type: + affects_outcome_of: NONE + default_value: Incremental + description: Quantization preset that defines quantization scheme + editable: false + enum_name: TrainType + header: Train type + options: + Incremental: "Incremental" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Incremental + visible_in_ui: false + warning: null + mem_cache_size: + affects_outcome_of: TRAINING + default_value: 0 + description: Size of memory pool for caching decoded data to load data faster (bytes). + editable: true + header: Size of memory pool + max_value: 9223372036854775807 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + storage_cache_scheme: + affects_outcome_of: TRAINING + default_value: NONE + description: Scheme for storage cache + editable: true + enum_name: StorageCacheScheme + header: Scheme for storage cache + options: + NONE: "NONE" + AS_IS: "AS-IS" + JPEG_75: "JPEG/75" + JPEG_95: "JPEG/95" + PNG: "PNG" + TIFF: "TIFF" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: True + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: True + warning: null + stat_subset_size: + affects_outcome_of: NONE + default_value: 300 + description: Number of data samples used for post-training optimization + editable: True + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: True + warning: null + stat_requests_number: + affects_outcome_of: NONE + default_value: 0 + description: Number of requests during statistics collection + editable: true + header: Number of requests + max_value: 200 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + header: Optimization by NNCF + enable_quantization: + affects_outcome_of: INFERENCE + default_value: True + description: Enable quantization algorithm + editable: false + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: false + warning: null + enable_pruning: + affects_outcome_of: INFERENCE + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + pruning_supported: + affects_outcome_of: TRAINING + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + maximal_accuracy_degradation: + affects_outcome_of: NONE + default_value: 1.0 + description: The maximal allowed accuracy metric drop in absolute values + editable: True + header: Maximum accuracy degradation + max_value: 100.0 + min_value: 0.0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: True + warning: null + type: PARAMETER_GROUP + visible_in_ui: True diff --git a/src/otx/algorithms/action/configs/classification/movinet/__init__.py b/src/otx/algorithms/action/configs/classification/movinet/__init__.py new file mode 100644 index 00000000000..bc73b4ae2e1 --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/movinet/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MoViNet model for Action Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/action/configs/classification/movinet/data_pipeline.py b/src/otx/algorithms/action/configs/classification/movinet/data_pipeline.py new file mode 100644 index 00000000000..8acd0ef5785 --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/movinet/data_pipeline.py @@ -0,0 +1,77 @@ +"""Data Pipeline of MoViNet model for Action Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +# dataset settings +dataset_type = "RawframeDataset" + +img_norm_cfg = dict(mean=[0.0, 0.0, 0.0], std=[255.0, 255.0, 255.0], to_bgr=False) + +clip_len = 8 +frame_interval = 4 +train_pipeline = [ + dict(type="SampleFrames", clip_len=clip_len, frame_interval=frame_interval, num_clips=1), + dict(type="RawFrameDecode"), + dict(type="Resize", scale=(-1, 256)), + dict(type="RandomResizedCrop"), + dict(type="Resize", scale=(224, 224), keep_ratio=False), + dict(type="Flip", flip_ratio=0.5), + dict(type="Normalize", **img_norm_cfg), + dict(type="FormatShape", input_format="NCTHW"), + dict(type="Collect", keys=["imgs", "label"], meta_keys=[]), + dict(type="ToTensor", keys=["imgs", "label"]), +] + +val_pipeline = [ + dict(type="SampleFrames", clip_len=clip_len, frame_interval=frame_interval, num_clips=1, test_mode=True), + dict(type="RawFrameDecode"), + dict(type="Resize", scale=(-1, 256)), + dict(type="CenterCrop", crop_size=224), + dict(type="Normalize", **img_norm_cfg), + dict(type="FormatShape", input_format="NCTHW"), + dict(type="Collect", keys=["imgs", "label"], meta_keys=[]), + dict(type="ToTensor", keys=["imgs"]), +] +# TODO Delete label in meta key in test pipeline +test_pipeline = [ + dict(type="SampleFrames", clip_len=clip_len, frame_interval=frame_interval, num_clips=1, test_mode=True), + dict(type="RawFrameDecode"), + dict(type="Resize", scale=(-1, 256)), + dict(type="CenterCrop", crop_size=224), + dict(type="Normalize", **img_norm_cfg), + dict(type="FormatShape", input_format="NCTHW"), + dict(type="Collect", keys=["imgs"], meta_keys=[]), + dict(type="ToTensor", keys=["imgs"]), +] + +data = dict( + videos_per_gpu=10, + workers_per_gpu=0, + val_dataloader=dict(videos_per_gpu=1), + test_dataloader=dict(videos_per_gpu=1), + train=dict( + type=dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=dataset_type, + pipeline=val_pipeline, + ), + test=dict( + type=dataset_type, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/action/configs/classification/movinet/deployment.py b/src/otx/algorithms/action/configs/classification/movinet/deployment.py new file mode 100644 index 00000000000..37e92c99c2a --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/movinet/deployment.py @@ -0,0 +1,3 @@ +"""MMDeploy config of MoViNet model for Action classification Task.""" + +_base_ = ["../base/base_classification_dynamic.py"] diff --git a/src/otx/algorithms/action/configs/classification/movinet/model.py b/src/otx/algorithms/action/configs/classification/movinet/model.py new file mode 100644 index 00000000000..257d3cbbfdd --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/movinet/model.py @@ -0,0 +1,38 @@ +"""Model configuration of MoViNet model for Action Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = ["../base/supervised.py"] + +num_classes = 400 +model = dict( + type="MoViNetRecognizer", + backbone=dict(type="OTXMoViNet"), + cls_head=dict( + type="MoViNetHead", + in_channels=480, + hidden_dim=2048, + num_classes=num_classes, + loss_cls=dict(type="CrossEntropyLoss", loss_weight=1.0), + ), + # model training and testing settings + train_cfg=None, + test_cfg=dict(average_clips="prob"), +) + +resume_from = None +load_from = "https://github.com/Atze00/MoViNet-pytorch/blob/main/weights/modelA0_statedict_v3?raw=true" diff --git a/src/otx/algorithms/action/configs/classification/movinet/template.yaml b/src/otx/algorithms/action/configs/classification/movinet/template.yaml new file mode 100644 index 00000000000..b2184a1027d --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/movinet/template.yaml @@ -0,0 +1,63 @@ +# Description. +model_template_id: Custom_Action_Classification_MoViNet +name: MoViNet +task_type: ACTION_CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: Basic transfer learning template for MoViNet +application: ~ + +# Algo backend. +framework: OTXAction v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.action.adapters.mmaction.task.MMActionTask + openvino: otx.algorithms.action.adapters.openvino.task.ActionOpenVINOTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.003 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 10 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 2.71 +size: 3.1 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/action/configs/classification/x3d/__init__.py b/src/otx/algorithms/action/configs/classification/x3d/__init__.py new file mode 100644 index 00000000000..239f59be845 --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/x3d/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of X3D model for Action Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/action/configs/classification/x3d/data_pipeline.py b/src/otx/algorithms/action/configs/classification/x3d/data_pipeline.py new file mode 100644 index 00000000000..12b9fa7940f --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/x3d/data_pipeline.py @@ -0,0 +1,78 @@ +"""Data Pipeline of X3D model for Action Cls Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +# dataset settings +seed = 2 +dataset_type = "RawframeDataset" + +img_norm_cfg = dict(mean=[0.0, 0.0, 0.0], std=[255.0, 255.0, 255.0], to_bgr=False) + +clip_len = 8 +frame_interval = 4 +train_pipeline = [ + dict(type="SampleFrames", clip_len=clip_len, frame_interval=frame_interval, num_clips=1), + dict(type="RawFrameDecode"), + dict(type="Resize", scale=(-1, 256)), + dict(type="RandomResizedCrop"), + dict(type="Resize", scale=(224, 224), keep_ratio=False), + dict(type="Flip", flip_ratio=0.5), + dict(type="Normalize", **img_norm_cfg), + dict(type="FormatShape", input_format="NCTHW"), + dict(type="Collect", keys=["imgs", "label"], meta_keys=[]), + dict(type="ToTensor", keys=["imgs", "label"]), +] + +val_pipeline = [ + dict(type="SampleFrames", clip_len=clip_len, frame_interval=frame_interval, num_clips=1, test_mode=True), + dict(type="RawFrameDecode"), + dict(type="Resize", scale=(-1, 256)), + dict(type="CenterCrop", crop_size=224), + dict(type="Normalize", **img_norm_cfg), + dict(type="FormatShape", input_format="NCTHW"), + dict(type="Collect", keys=["imgs", "label"], meta_keys=[]), + dict(type="ToTensor", keys=["imgs"]), +] +# TODO Delete label in meta key in test pipeline +test_pipeline = [ + dict(type="SampleFrames", clip_len=clip_len, frame_interval=frame_interval, num_clips=1, test_mode=True), + dict(type="RawFrameDecode"), + dict(type="Resize", scale=(-1, 256)), + dict(type="CenterCrop", crop_size=224), + dict(type="Normalize", **img_norm_cfg), + dict(type="FormatShape", input_format="NCTHW"), + dict(type="Collect", keys=["imgs"], meta_keys=[]), + dict(type="ToTensor", keys=["imgs"]), +] + +data = dict( + videos_per_gpu=10, + workers_per_gpu=0, + val_dataloader=dict(videos_per_gpu=1), + test_dataloader=dict(videos_per_gpu=1), + train=dict( + type=dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=dataset_type, + pipeline=val_pipeline, + ), + test=dict( + type=dataset_type, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/action/configs/classification/x3d/deployment.py b/src/otx/algorithms/action/configs/classification/x3d/deployment.py new file mode 100644 index 00000000000..4bf0efd8436 --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/x3d/deployment.py @@ -0,0 +1,3 @@ +"""MMDeploy config of X3D model for Action classification Task.""" + +_base_ = ["../base/base_classification_dynamic.py"] diff --git a/src/otx/algorithms/action/configs/classification/x3d/model.py b/src/otx/algorithms/action/configs/classification/x3d/model.py new file mode 100644 index 00000000000..0598ead78c7 --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/x3d/model.py @@ -0,0 +1,38 @@ +"""Model configuration of X3D model for Action Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = ["../base/supervised.py"] + +num_classes = 400 +num_samples = 12 +model = dict( + type="Recognizer3D", + backbone=dict(type="X3D", gamma_w=1, gamma_b=2.25, gamma_d=2.2), + cls_head=dict( + type="X3DHead", in_channels=432, num_classes=num_classes, spatial_type="avg", dropout_ratio=0.5, fc1_bias=False + ), + # model training and testing settings + train_cfg=None, + test_cfg=dict(average_clips="prob"), +) + +resume_from = None +load_from = ( + "https://download.openmmlab.com/mmaction/recognition/x3d/facebook/" + "x3d_m_facebook_16x5x1_kinetics400_rgb_20201027-3f42382a.pth" +) diff --git a/src/otx/algorithms/action/configs/classification/x3d/template.yaml b/src/otx/algorithms/action/configs/classification/x3d/template.yaml new file mode 100644 index 00000000000..e2f025cec63 --- /dev/null +++ b/src/otx/algorithms/action/configs/classification/x3d/template.yaml @@ -0,0 +1,63 @@ +# Description. +model_template_id: Custom_Action_Classification_X3D +name: X3D +task_type: ACTION_CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: Basic transfer learning template for X3D +application: ~ + +# Algo backend. +framework: OTXAction v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.action.adapters.mmaction.task.MMActionTask + openvino: otx.algorithms.action.adapters.openvino.task.ActionOpenVINOTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 10 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 20.6 +size: 9.1 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/action/configs/detection/__init__.py b/src/otx/algorithms/action/configs/detection/__init__.py new file mode 100644 index 00000000000..5afe58c6612 --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/__init__.py @@ -0,0 +1,15 @@ +"""Config Initialization for OTX Action Detection.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/action/configs/detection/base/__init__.py b/src/otx/algorithms/action/configs/detection/base/__init__.py new file mode 100644 index 00000000000..98201af2c8b --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/base/__init__.py @@ -0,0 +1,15 @@ +"""Initialize base confings of OTX Action Detection.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/action/configs/detection/base/ava_data_pipeline.py b/src/otx/algorithms/action/configs/detection/base/ava_data_pipeline.py new file mode 100644 index 00000000000..6650ee3de55 --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/base/ava_data_pipeline.py @@ -0,0 +1,68 @@ +"""Data configuration for AVA dataset.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + + +_base_ = ["./data_pipeline.py"] + +# FIXME Only changes from base is frame interval of SampleAVAFrames +img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_bgr=False) + +train_pipeline = [ + dict(type="SampleAVAFrames", clip_len=32, frame_interval=2), + dict(type="RawFrameDecode"), + dict(type="RandomRescale", scale_range=(256, 320)), + dict(type="RandomCrop", size=256), + dict(type="Flip", flip_ratio=0.5), + dict(type="Normalize", **img_norm_cfg), + dict(type="FormatShape", input_format="NCTHW", collapse=True), + # Rename is needed to use mmdet detectors + dict(type="Rename", mapping=dict(imgs="img")), + dict(type="ToTensor", keys=["img", "proposals", "gt_bboxes", "gt_labels"]), + dict(type="ToDataContainer", fields=[dict(key=["proposals", "gt_bboxes", "gt_labels"], stack=False)]), + dict(type="Collect", keys=["img", "proposals", "gt_bboxes", "gt_labels"], meta_keys=["scores", "entity_ids"]), +] +# The testing is w/o. any cropping / flipping +val_pipeline = [ + dict(type="SampleAVAFrames", clip_len=32, frame_interval=2, test_mode=True), + dict(type="RawFrameDecode"), + dict(type="Resize", scale=(-1, 256)), + dict(type="Normalize", **img_norm_cfg), + dict(type="FormatShape", input_format="NCTHW", collapse=True), + # Rename is needed to use mmdet detectors + dict(type="Rename", mapping=dict(imgs="img")), + dict(type="ToTensor", keys=["img", "proposals"]), + dict(type="ToDataContainer", fields=[dict(key="proposals", stack=False)]), + dict(type="Collect", keys=["img", "proposals"], meta_keys=["scores", "img_shape"], nested=True), +] + +data = dict( + val_dataloader=dict(videos_per_gpu=1), + test_dataloader=dict(videos_per_gpu=1), + train=dict( + pipeline=train_pipeline, + person_det_score_thr=0.9, + fps=30, + ), + val=dict( + pipeline=val_pipeline, + person_det_score_thr=0.9, + fps=30, + test_mode=True, + ), +) +data["test"] = data["val"] diff --git a/src/otx/algorithms/action/configs/detection/base/base_detection_dynamic.py b/src/otx/algorithms/action/configs/detection/base/base_detection_dynamic.py new file mode 100644 index 00000000000..18c070abe8b --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/base/base_detection_dynamic.py @@ -0,0 +1,11 @@ +"""Base Action detection mmdeply cfg.""" + +_base_ = ["./base_detection_static.py"] + +ir_config = dict( + dynamic_axes=dict( + input=dict({0: "batch", 1: "channel", 2: "clip_len", 3: "height", 4: "width"}), + dets=dict({0: "batch", 1: "num_dets"}), + labels=dict({0: "batch", 1: "num_dets"}), + ), +) diff --git a/src/otx/algorithms/action/configs/detection/base/base_detection_static.py b/src/otx/algorithms/action/configs/detection/base/base_detection_static.py new file mode 100644 index 00000000000..f1122e04cbb --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/base/base_detection_static.py @@ -0,0 +1,32 @@ +"""Base Action detection mmdeply cfg.""" + +ir_config = dict( + type="onnx", + export_params=True, + keep_initializers_as_inputs=False, + opset_version=11, + save_file="end2end.onnx", + input_names=["input"], + output_names=["bboxes", "labels"], + input_shape=None, + optimize=False, +) +codebase_config = dict( + type="mmdet", + task="ObjectDetection", + model_type="end2end", + post_processing=dict( + score_threshold=0.05, + confidence_threshold=0.005, + iou_threshold=0.5, + max_output_boxes_per_class=200, + pre_top_k=5000, + keep_top_k=100, + background_label_id=-1, + ), +) +backend_config = dict( + type="openvino", + mo_options=dict(args=dict({"--source_layout": "bctwh"})), + model_inputs=[dict(opt_shapes=dict(input=[1, 3, 32, 256, 256]))], +) diff --git a/src/otx/algorithms/action/configs/detection/base/data_pipeline.py b/src/otx/algorithms/action/configs/detection/base/data_pipeline.py new file mode 100644 index 00000000000..3e9137e9699 --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/base/data_pipeline.py @@ -0,0 +1,64 @@ +"""Data configuration for default action detection dataset.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_bgr=False) + +train_pipeline = [ + dict(type="SampleAVAFrames", clip_len=32, frame_interval=2), + dict(type="RawFrameDecode"), + dict(type="RandomRescale", scale_range=(256, 320)), + dict(type="RandomCrop", size=256), + dict(type="Flip", flip_ratio=0.5), + dict(type="Normalize", **img_norm_cfg), + dict(type="FormatShape", input_format="NCTHW", collapse=True), + # Rename is needed to use mmdet detectors + dict(type="Rename", mapping=dict(imgs="img")), + dict(type="ToTensor", keys=["img", "proposals", "gt_bboxes", "gt_labels"]), + dict(type="ToDataContainer", fields=[dict(key=["proposals", "gt_bboxes", "gt_labels"], stack=False)]), + dict(type="Collect", keys=["img", "proposals", "gt_bboxes", "gt_labels"], meta_keys=["scores"]), +] +# The testing is w/o. any cropping / flipping +val_pipeline = [ + dict(type="SampleAVAFrames", clip_len=32, frame_interval=2, test_mode=True), + dict(type="RawFrameDecode"), + dict(type="Resize", scale=(-1, 256)), + dict(type="Normalize", **img_norm_cfg), + dict(type="FormatShape", input_format="NCTHW", collapse=True), + # Rename is needed to use mmdet detectors + dict(type="Rename", mapping=dict(imgs="img")), + dict(type="ToTensor", keys=["img", "proposals"]), + dict(type="ToDataContainer", fields=[dict(key="proposals", stack=False)]), + dict(type="Collect", keys=["img", "proposals"], meta_keys=["scores", "img_shape"], nested=True), +] + +data = dict( + val_dataloader=dict(videos_per_gpu=1), + test_dataloader=dict(videos_per_gpu=1), + train=dict( + pipeline=train_pipeline, + person_det_score_thr=0.5, + fps=1, + ), + val=dict( + pipeline=val_pipeline, + person_det_score_thr=0.5, + fps=1, + test_mode=True, + ), +) +data["test"] = data["val"] diff --git a/src/otx/algorithms/action/configs/detection/base/faster_rcnn_config.py b/src/otx/algorithms/action/configs/detection/base/faster_rcnn_config.py new file mode 100644 index 00000000000..6260b5723c0 --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/base/faster_rcnn_config.py @@ -0,0 +1,106 @@ +"""Faster RCNN config for Action classification backbone + Fast RCNN model's inference.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +faster_rcnn = dict( + type="FasterRCNN", + backbone=dict( + type="ResNet", + depth=50, + num_stages=4, + out_indices=(0, 1, 2, 3), + frozen_stages=1, + norm_cfg=dict(type="BN", requires_grad=True), + norm_eval=True, + style="pytorch", + init_cfg=dict(type="Pretrained", checkpoint="torchvision://resnet50"), + ), + neck=dict(type="FPN", in_channels=[256, 512, 1024, 2048], out_channels=256, num_outs=5), + rpn_head=dict( + type="RPNHead", + in_channels=256, + feat_channels=256, + anchor_generator=dict(type="AnchorGenerator", scales=[8], ratios=[0.5, 1.0, 2.0], strides=[4, 8, 16, 32, 64]), + bbox_coder=dict(type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[1.0, 1.0, 1.0, 1.0]), + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + roi_head=dict( + type="StandardRoIHead", + bbox_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=7, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + bbox_head=dict( + type="Shared2FCBBoxHead", + in_channels=256, + fc_out_channels=1024, + roi_feat_size=7, + num_classes=80, + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[0.1, 0.1, 0.2, 0.2] + ), + reg_class_agnostic=False, + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + ), + # model training and testing settings + train_cfg=dict( + rpn=dict( + assigner=dict( + type="MaxIoUAssigner", + pos_iou_thr=0.7, + neg_iou_thr=0.3, + min_pos_iou=0.3, + match_low_quality=True, + ignore_iof_thr=-1, + ), + sampler=dict(type="RandomSampler", num=256, pos_fraction=0.5, neg_pos_ub=-1, add_gt_as_proposals=False), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + rpn_proposal=dict(nms_pre=2000, max_per_img=1000, nms=dict(type="nms", iou_threshold=0.7), min_bbox_size=0), + rcnn=dict( + assigner=dict( + type="MaxIoUAssigner", + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=False, + ignore_iof_thr=-1, + ), + sampler=dict(type="RandomSampler", num=512, pos_fraction=0.25, neg_pos_ub=-1, add_gt_as_proposals=True), + pos_weight=-1, + debug=False, + ), + ), + test_cfg=dict( + rpn=dict(nms_pre=1000, max_per_img=1000, nms=dict(type="nms", iou_threshold=0.7), min_bbox_size=0), + rcnn=dict(score_thr=0.05, nms=dict(type="nms", iou_threshold=0.5), max_per_img=100) + # soft-nms is also supported for rcnn testing + # e.g., nms=dict(type='soft_nms', iou_threshold=0.5, min_score=0.05) + ), +) + +faster_rcnn_pretrained = ( + "http://download.openmmlab.com/mmdetection/v2.0/faster_rcnn/faster_rcnn_r50_fpn_2x_coco/" + "faster_rcnn_r50_fpn_2x_coco_bbox_mAP-0.384_20200504_210434-a5d8aa15.pth" +) diff --git a/src/otx/algorithms/action/configs/detection/base/supervised.py b/src/otx/algorithms/action/configs/detection/base/supervised.py new file mode 100644 index 00000000000..c4b6d5a7491 --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/base/supervised.py @@ -0,0 +1,45 @@ +"""Supervised learning settings for video actor localization.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +optimizer = dict(type="SGD", lr=0.1, momentum=0.9, weight_decay=1e-5) +optimizer_config = dict(grad_clip=dict(max_norm=40, norm_type=2)) + +lr_config = dict( + policy="CosineAnnealing", + by_epoch=False, + min_lr=0, + warmup="linear", + warmup_by_epoch=True, + warmup_iters=2, + warmup_ratio=0.1, +) +checkpoint_config = dict(interval=1) +workflow = [("train", 1)] +evaluation = dict(interval=1, save_best="mAP@0.5IOU", final_metric="mAP@0.5IOU") +log_config = dict( + interval=10, + hooks=[ + dict(type="TextLoggerHook"), + ], +) +dist_params = dict(backend="nccl") +log_level = "INFO" +find_unused_parameters = False +# Temporary solution, gpu_ids is not used in otx +gpu_ids = [0] +seed = 2 diff --git a/src/otx/algorithms/action/configs/detection/configuration.yaml b/src/otx/algorithms/action/configs/detection/configuration.yaml new file mode 100644 index 00000000000..86dcd72a68a --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/configuration.yaml @@ -0,0 +1,453 @@ +description: Configuration for an action classification task +header: Configuration for an action classification task +learning_parameters: + batch_size: + affects_outcome_of: TRAINING + default_value: 5 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 5 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + description: Learning Parameters + header: Learning Parameters + learning_rate: + affects_outcome_of: TRAINING + default_value: 0.01 + description: + Increasing this value will speed up training convergence but might + make it unstable. + editable: true + header: Learning rate + max_value: 0.1 + min_value: 1.0e-07 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.01 + visible_in_ui: true + warning: null + auto_hpo_state: NOT_POSSIBLE + learning_rate_warmup_iters: + affects_outcome_of: TRAINING + default_value: 100 + description: + In this periods of initial training iterations, the model will be trained in low learning rate, + which will be increased incrementally up to the expected learning rate setting. + This warm-up phase is known to be helpful to stabilize training, thus result in better performance. + editable: true + header: Number of iterations for learning rate warmup + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: null + num_iters: + affects_outcome_of: TRAINING + default_value: 1 + description: + Increasing this value causes the results to be more robust but training + time will be longer. + editable: true + header: Number of training iterations + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: null + num_workers: + affects_outcome_of: NONE + default_value: 0 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of cpu threads to use during batch generation + max_value: 8 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: null + enable_early_stopping: + affects_outcome_of: TRAINING + default_value: false + description: Early exit from training when validation accuracy isn't changed or decreased for several epochs. + editable: true + header: Enable early stopping of the training + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + early_stop_start: + affects_outcome_of: TRAINING + default_value: 3 + editable: true + header: Start epoch for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 3 + visible_in_ui: false + early_stop_patience: + affects_outcome_of: TRAINING + default_value: 10 + description: Training will stop if the model does not improve within the number of epochs of patience. + editable: true + header: Patience for early stopping + max_value: 50 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 10 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + early_stop_iteration_patience: + affects_outcome_of: TRAINING + default_value: 0 + description: + Training will stop if the model does not improve within the number of iterations of patience. + This ensures the model is trained enough with the number of iterations of patience before early stopping. + editable: true + header: Iteration patience for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + use_adaptive_interval: + affects_outcome_of: TRAINING + default_value: false + description: Depending on the size of iteration per epoch, adaptively update the validation interval and related values. + editable: true + header: Use adaptive validation interval + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: This will automatically control the patience and interval when early stopping is enabled. + auto_adapt_batch_size: + affects_outcome_of: TRAINING + default_value: None + description: Safe => Prevent GPU out of memory. Full => Find a batch size using most of GPU memory. + editable: true + enum_name: BatchSizeAdaptType + header: Decrease batch size if current batch size isn't fit to CUDA memory. + options: + NONE: "None" + SAFE: "Safe" + FULL: "Full" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: None + visible_in_ui: true + warning: + Enabling this could change the actual batch size depending on the current GPU status. + The learning rate also could be adjusted according to the adapted batch size. This process might change + a model performance and take some extra computation time to try a few batch size candidates. + auto_num_workers: + affects_outcome_of: TRAINING + default_value: false + description: Adapt num_workers according to current hardware status automatically. + editable: true + header: Enable auto adaptive num_workers + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +postprocessing: + confidence_threshold: + affects_outcome_of: INFERENCE + default_value: 0.35 + description: + This threshold only takes effect if the threshold is not set based + on the result. + editable: true + header: Confidence threshold + max_value: 1 + min_value: 0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + # value: 0.35 + value: 0.01 + visible_in_ui: true + warning: null + description: Postprocessing + header: Postprocessing + result_based_confidence_threshold: + affects_outcome_of: INFERENCE + default_value: true + description: Confidence threshold is derived from the results + editable: true + header: Result based confidence threshold + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +algo_backend: + description: parameters for algo backend + header: Algo backend parameters + train_type: + affects_outcome_of: NONE + default_value: Incremental + description: Quantization preset that defines quantization scheme + editable: false + enum_name: TrainType + header: Train type + options: + Incremental: "Incremental" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Incremental + visible_in_ui: false + warning: null + mem_cache_size: + affects_outcome_of: TRAINING + default_value: 0 + description: Size of memory pool for caching decoded data to load data faster (bytes). + editable: true + header: Size of memory pool + max_value: 9223372036854775807 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + storage_cache_scheme: + affects_outcome_of: TRAINING + default_value: NONE + description: Scheme for storage cache + editable: true + enum_name: StorageCacheScheme + header: Scheme for storage cache + options: + NONE: "NONE" + AS_IS: "AS-IS" + JPEG_75: "JPEG/75" + JPEG_95: "JPEG/95" + PNG: "PNG" + TIFF: "TIFF" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: True + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: True + warning: null + stat_subset_size: + affects_outcome_of: NONE + default_value: 300 + description: Number of data samples used for post-training optimization + editable: True + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: True + warning: null + stat_requests_number: + affects_outcome_of: NONE + default_value: 0 + description: Number of requests during statistics collection + editable: true + header: Number of requests + max_value: 200 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + header: Optimization by NNCF + enable_quantization: + affects_outcome_of: INFERENCE + default_value: True + description: Enable quantization algorithm + editable: false + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: false + warning: null + enable_pruning: + affects_outcome_of: INFERENCE + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + pruning_supported: + affects_outcome_of: TRAINING + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + maximal_accuracy_degradation: + affects_outcome_of: NONE + default_value: 1.0 + description: The maximal allowed accuracy metric drop in absolute values + editable: True + header: Maximum accuracy degradation + max_value: 100.0 + min_value: 0.0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: True + warning: null + type: PARAMETER_GROUP + visible_in_ui: True diff --git a/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/__init__.py b/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/__init__.py new file mode 100644 index 00000000000..fa2d8af5346 --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of X3D_Fast_RCNN for Action Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/data_pipeline.py b/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/data_pipeline.py new file mode 100644 index 00000000000..69863857578 --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of X3D fa model for Action Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../base/data_pipeline.py"] diff --git a/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/deployment.py b/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/deployment.py new file mode 100644 index 00000000000..071672c3922 --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/deployment.py @@ -0,0 +1,3 @@ +"""MMDeploy config of X3D_FAST_RCNN model for Action detection Task.""" + +_base_ = ["../base/base_detection_dynamic.py"] diff --git a/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/model.py b/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/model.py new file mode 100644 index 00000000000..4d2ff6aec1e --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/model.py @@ -0,0 +1,47 @@ +"""Model configuration of Fast RCNN with X3D for Action Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = ["../base/supervised.py"] + +# model setting +model = dict( + type="AVAFastRCNN", + backbone=dict(type="X3D", gamma_w=1, gamma_b=2.25, gamma_d=2.2), + roi_head=dict( + type="AVARoIHead", + bbox_roi_extractor=dict( + type="SingleRoIExtractor3D", roi_layer_type="RoIAlign", output_size=8, with_temporal_pool=True + ), + bbox_head=dict(type="BBoxHeadAVA", in_channels=432, num_classes=81, multilabel=False, dropout_ratio=0.5), + ), + train_cfg=dict( + rcnn=dict( + assigner=dict(type="MaxIoUAssignerAVA", pos_iou_thr=0.9, neg_iou_thr=0.9, min_pos_iou=0.9), + sampler=dict(type="RandomSampler", num=32, pos_fraction=1, neg_pos_ub=-1, add_gt_as_proposals=True), + pos_weight=1.0, + debug=False, + ) + ), + test_cfg=dict(rcnn=dict(action_thr=0.002)), +) + +load_from = ( + "https://download.openmmlab.com/mmaction/recognition/x3d/facebook/" + "x3d_m_facebook_16x5x1_kinetics400_rgb_20201027-3f42382a.pth" +) +resume_from = None diff --git a/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/template.yaml b/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/template.yaml new file mode 100644 index 00000000000..85d9027b589 --- /dev/null +++ b/src/otx/algorithms/action/configs/detection/x3d_fast_rcnn/template.yaml @@ -0,0 +1,63 @@ +# Description. +model_template_id: Custom_Action_Detection_X3D_FAST_RCNN +name: X3D_FAST_RCNN +task_type: ACTION_DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Basic transfer learning template for Fast RCNN with 3D action backbone +application: ~ + +# Algo backend. +framework: OTXAction v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.action.adapters.mmaction.task.MMActionTask + openvino: otx.algorithms.action.adapters.openvino.task.ActionOpenVINOTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.005 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 10 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 20.6 +size: 9.1 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/action/task.py b/src/otx/algorithms/action/task.py new file mode 100644 index 00000000000..5622da6c6aa --- /dev/null +++ b/src/otx/algorithms/action/task.py @@ -0,0 +1,452 @@ +"""Task of OTX Video Recognition.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import io +import os +from abc import ABC, abstractmethod +from typing import Any, Dict, Iterable, List, Optional, Union + +import numpy as np +import torch +from mmcv.utils import ConfigDict + +from otx.algorithms.action.configs.base import ActionConfig +from otx.algorithms.common.tasks.base_task import TRAIN_TYPE_DIR_PATH, OTXTask +from otx.algorithms.common.utils.callback import ( + InferenceProgressCallback, + TrainingProgressCallback, +) +from otx.api.configuration import cfg_helper +from otx.api.configuration.helper.utils import config_to_bytes, ids_to_strings +from otx.api.entities.annotation import Annotation +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.metrics import ( + BarChartInfo, + BarMetricsGroup, + CurveMetric, + LineChartInfo, + LineMetricsGroup, + MetricsGroup, + ScoreMetric, + VisualizationType, +) +from otx.api.entities.model import ( + ModelEntity, + ModelPrecision, +) +from otx.api.entities.model_template import TaskType +from otx.api.entities.result_media import ResultMediaEntity +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.tensor import TensorEntity +from otx.api.entities.train_parameters import TrainParameters, default_progress_callback +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.evaluation.accuracy import Accuracy +from otx.api.usecases.evaluation.f_measure import FMeasure +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.utils.vis_utils import get_actmap +from otx.cli.utils.multi_gpu import is_multigpu_child_process +from otx.utils.logger import get_logger + +logger = get_logger() + + +class OTXActionTask(OTXTask, ABC): + """Task class for OTX action.""" + + # pylint: disable=too-many-instance-attributes, too-many-locals + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__(task_environment, output_path) + self._task_config = ActionConfig + self._hyperparams: ConfigDict = task_environment.get_hyper_parameters(self._task_config) + self._train_type = self._hyperparams.algo_backend.train_type + self._model_dir = os.path.join( + os.path.abspath(os.path.dirname(self._task_environment.model_template.model_template_path)), + TRAIN_TYPE_DIR_PATH[self._train_type.name], + ) + + if hasattr(self._hyperparams, "postprocessing") and hasattr( + self._hyperparams.postprocessing, "confidence_threshold" + ): + self.confidence_threshold = self._hyperparams.postprocessing.confidence_threshold + else: + self.confidence_threshold = 0.0 + + if task_environment.model is not None: + self._load_model() + + self.data_pipeline_path = os.path.join(self._model_dir, "data_pipeline.py") + + def train( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + train_parameters: Optional[TrainParameters] = None, + seed: Optional[int] = None, + deterministic: bool = False, + ): + """Train function for OTX action task. + + Actual training is processed by _train_model fucntion + """ + logger.info("train()") + # Check for stop signal when training has stopped. + # If should_stop is true, training was cancelled and no new + if self._should_stop: + logger.info("Training cancelled.") + self._should_stop = False + self._is_training = False + return + self.seed = seed + self.deterministic = deterministic + + # Set OTX LoggerHook & Time Monitor + if train_parameters: + update_progress_callback = train_parameters.update_progress + else: + update_progress_callback = default_progress_callback + self._time_monitor = TrainingProgressCallback(update_progress_callback) + + results = self._train_model(dataset) + + # Check for stop signal when training has stopped. If should_stop is true, training was cancelled and no new + if self._should_stop: + logger.info("Training cancelled.") + self._should_stop = False + self._is_training = False + return + + # get output model + model_ckpt = results.get("final_ckpt") + if model_ckpt is None: + logger.error("cannot find final checkpoint from the results.") + return + # update checkpoint to the newly trained model + self._model_ckpt = model_ckpt + + # get prediction on validation set + self._is_training = False + val_dataset = dataset.get_subset(Subset.VALIDATION) + val_preds, val_performance = self._infer_model(val_dataset, InferenceParameters(is_evaluation=True)) + + preds_val_dataset = val_dataset.with_empty_annotations() + if self._task_type == TaskType.ACTION_CLASSIFICATION: + self._add_cls_predictions_to_dataset(val_preds, preds_val_dataset) + elif self._task_type == TaskType.ACTION_DETECTION: + self._add_det_predictions_to_dataset(val_preds, preds_val_dataset, 0.0) + + result_set = ResultSetEntity( + model=output_model, + ground_truth_dataset=val_dataset, + prediction_dataset=preds_val_dataset, + ) + + metric: Union[Accuracy, FMeasure] + + if self._task_type == TaskType.ACTION_CLASSIFICATION: + metric = MetricsHelper.compute_accuracy(result_set) + if self._task_type == TaskType.ACTION_DETECTION: + if self._hyperparams.postprocessing.result_based_confidence_threshold: + best_confidence_threshold = None + logger.info("Adjusting the confidence threshold") + metric = MetricsHelper.compute_f_measure(result_set, vary_confidence_threshold=True) + if metric.best_confidence_threshold: + best_confidence_threshold = metric.best_confidence_threshold.value + if best_confidence_threshold is None: + raise ValueError("Cannot compute metrics: Invalid confidence threshold!") + logger.info(f"Setting confidence threshold to {best_confidence_threshold} based on results") + self.confidence_threshold = best_confidence_threshold + else: + metric = MetricsHelper.compute_f_measure(result_set, vary_confidence_threshold=False) + + # compose performance statistics + performance = metric.get_performance() + performance.dashboard_metrics.extend(self._generate_training_metrics(self._learning_curves, val_performance)) + logger.info(f"Final model performance: {performance}") + # save resulting model + self.save_model(output_model) + output_model.performance = performance + logger.info("train done.") + + @abstractmethod + def _train_model(self, dataset: DatasetEntity): + """Train model and return the results.""" + raise NotImplementedError + + def infer( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ) -> DatasetEntity: + """Main infer function.""" + logger.info("infer()") + + update_progress_callback = default_progress_callback + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress # type: ignore + + self._time_monitor = InferenceProgressCallback(len(dataset), update_progress_callback) + # If confidence threshold is adaptive then up-to-date value should be stored in the model + # and should not be changed during inference. Otherwise user-specified value should be taken. + if not self._hyperparams.postprocessing.result_based_confidence_threshold: + self.confidence_threshold = self._hyperparams.postprocessing.confidence_threshold + logger.info(f"Confidence threshold {self.confidence_threshold}") + + prediction_results, _ = self._infer_model(dataset, inference_parameters) + + if self._task_type == TaskType.ACTION_CLASSIFICATION: + self._add_cls_predictions_to_dataset(prediction_results, dataset) + elif self._task_type == TaskType.ACTION_DETECTION: + self._add_det_predictions_to_dataset(prediction_results, dataset, self.confidence_threshold) + logger.info("Inference completed") + return dataset + + @abstractmethod + def _infer_model( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ): + """Get inference results from dataset.""" + raise NotImplementedError + + def export( + self, + export_type: ExportType, + output_model: ModelEntity, + precision: ModelPrecision = ModelPrecision.FP32, + dump_features: bool = True, + ): + """Export function of OTX Task.""" + if dump_features: + raise NotImplementedError( + "Feature dumping is not implemented for the action task." + "The saliency maps and representation vector outputs will not be dumped in the exported model." + ) + + self._update_model_export_metadata(output_model, export_type, precision, dump_features) + results = self._export_model(precision, export_type, dump_features) + + outputs = results.get("outputs") + logger.debug(f"results of run_task = {outputs}") + if outputs is None: + raise RuntimeError(results.get("msg")) + + if export_type == ExportType.ONNX: + onnx_file = outputs.get("onnx") + with open(onnx_file, "rb") as f: + output_model.set_data("model.onnx", f.read()) + else: + bin_file = outputs.get("bin") + xml_file = outputs.get("xml") + + with open(bin_file, "rb") as f: + output_model.set_data("openvino.bin", f.read()) + with open(xml_file, "rb") as f: + output_model.set_data("openvino.xml", f.read()) + + output_model.set_data( + "confidence_threshold", + np.array([self.confidence_threshold], dtype=np.float32).tobytes(), + ) + output_model.set_data("config.json", config_to_bytes(self._hyperparams)) + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self._task_environment.label_schema), + ) + logger.info("Exporting completed") + + @abstractmethod + def _export_model(self, precision: ModelPrecision, export_format: ExportType, dump_features: bool): + raise NotImplementedError + + def explain( + self, + dataset: DatasetEntity, + explain_parameters: Optional[ExplainParameters] = None, + ) -> DatasetEntity: + """Main explain function of OTX Task.""" + raise NotImplementedError("Video recognition task don't support otx explain yet.") + + def evaluate( + self, + output_resultset: ResultSetEntity, + evaluation_metric: Optional[str] = None, + ): + """Evaluate function of OTX Action Task.""" + logger.info("called evaluate()") + if evaluation_metric is not None: + logger.warning( + f"Requested to use {evaluation_metric} metric, " "but parameter is ignored. Use F-measure instead." + ) + self._remove_empty_frames(output_resultset.ground_truth_dataset) + + metric: Union[Accuracy, FMeasure] + if self._task_type == TaskType.ACTION_CLASSIFICATION: + metric = MetricsHelper.compute_accuracy(output_resultset) + if self._task_type == TaskType.ACTION_DETECTION: + metric = MetricsHelper.compute_f_measure(output_resultset) + performance = metric.get_performance() + logger.info(f"Final model performance: {str(performance)}") + output_resultset.performance = metric.get_performance() + logger.info("Evaluation completed") + + def _remove_empty_frames(self, dataset: DatasetEntity): + """Remove empty frame for action detection dataset.""" + remove_indices = [] + for idx, item in enumerate(dataset): + if item.get_metadata()[0].data.is_empty_frame: + remove_indices.append(idx) + dataset.remove_at_indices(remove_indices) + + def _add_cls_predictions_to_dataset(self, prediction_results: Iterable, dataset: DatasetEntity): + """Loop over dataset again to assign predictions. Convert from MM format to OTX format.""" + prediction_results = list(prediction_results) + video_info: Dict[str, int] = {} + for dataset_item in dataset: + video_id = dataset_item.get_metadata()[0].data.video_id + if video_id not in video_info: + video_info[video_id] = len(video_info) + for dataset_item in dataset: + video_id = dataset_item.get_metadata()[0].data.video_id + all_results, feature_vector, saliency_map = prediction_results[video_info[video_id]] + item_labels = [] + label = ScoredLabel(label=self._labels[all_results.argmax()], probability=all_results.max()) + item_labels.append(label) + dataset_item.append_labels(item_labels) + + if feature_vector is not None: + active_score = TensorEntity(name="representation_vector", numpy=feature_vector.reshape(-1)) + dataset_item.append_metadata_item(active_score, model=self._task_environment.model) + + if saliency_map is not None: + saliency_map = get_actmap(saliency_map, (dataset_item.width, dataset_item.height)) + saliency_map_media = ResultMediaEntity( + name="Saliency Map", + type="saliency_map", + annotation_scene=dataset_item.annotation_scene, + numpy=saliency_map, + roi=dataset_item.roi, + ) + dataset_item.append_metadata_item(saliency_map_media, model=self._task_environment.model) + + def _add_det_predictions_to_dataset( + self, prediction_results: Iterable, dataset: DatasetEntity, confidence_threshold: float = 0.05 + ): + self._remove_empty_frames(dataset) + for dataset_item, (all_results, feature_vector, saliency_map) in zip(dataset, prediction_results): + shapes = [] + for label_idx, detections in enumerate(all_results): + for i in range(detections.shape[0]): + probability = float(detections[i, 4]) + coords = detections[i, :4] + + if probability < confidence_threshold: + continue + if coords[3] - coords[1] <= 0 or coords[2] - coords[0] <= 0: + continue + + assigned_label = [ScoredLabel(self._labels[label_idx], probability=probability)] + shapes.append( + Annotation( + Rectangle(x1=coords[0], y1=coords[1], x2=coords[2], y2=coords[3]), + labels=assigned_label, + ) + ) + dataset_item.append_annotations(shapes) + + if feature_vector is not None: + active_score = TensorEntity(name="representation_vector", numpy=feature_vector.reshape(-1)) + dataset_item.append_metadata_item(active_score, model=self._task_environment.model) + + if saliency_map is not None: + saliency_map = get_actmap(saliency_map, (dataset_item.width, dataset_item.height)) + saliency_map_media = ResultMediaEntity( + name="Saliency Map", + type="saliency_map", + annotation_scene=dataset_item.annotation_scene, + numpy=saliency_map, + roi=dataset_item.roi, + ) + dataset_item.append_metadata_item(saliency_map_media, model=self._task_environment.model) + + @staticmethod + # TODO Implement proper function for action classification + def _generate_training_metrics(learning_curves, scores, metric_name="mAP") -> Iterable[MetricsGroup[Any, Any]]: + """Get Training metrics (epochs & scores). + + Parses the mmaction logs to get metrics from the latest training run + :return output List[MetricsGroup] + """ + output: List[MetricsGroup] = [] + + # Learning curves. + for key, curve in learning_curves.items(): + len_x, len_y = len(curve.x), len(curve.y) + if len_x != len_y: + logger.warning(f"Learning curve {key} has inconsistent number of coordinates ({len_x} vs {len_y}.") + len_x = min(len_x, len_y) + curve.x = curve.x[:len_x] + curve.y = curve.y[:len_x] + metric_curve = CurveMetric( + xs=np.nan_to_num(curve.x).tolist(), + ys=np.nan_to_num(curve.y).tolist(), + name=key, + ) + visualization_info = LineChartInfo(name=key, x_axis_label="Epoch", y_axis_label=key) + output.append(LineMetricsGroup(metrics=[metric_curve], visualization_info=visualization_info)) + + # Final mAP value on the validation set. + output.append( + BarMetricsGroup( + metrics=[ScoreMetric(value=scores, name=f"{metric_name}")], + visualization_info=BarChartInfo("Validation score", visualization_type=VisualizationType.RADIAL_BAR), + ) + ) + + return output + + def save_model(self, output_model: ModelEntity): + """Save best model weights in ActionTrainTask.""" + if is_multigpu_child_process(): + return + + logger.info("called save_model") + buffer = io.BytesIO() + hyperparams_str = ids_to_strings(cfg_helper.convert(self._hyperparams, dict, enum_to_str=True)) + labels = {label.name: label.color.rgb_tuple for label in self._labels} + model_ckpt = torch.load(self._model_ckpt) + modelinfo = { + "model": model_ckpt, + "config": hyperparams_str, + "labels": labels, + "confidence_threshold": self.confidence_threshold, + "VERSION": 1, + } + + torch.save(modelinfo, buffer) + output_model.set_data("weights.pth", buffer.getvalue()) + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self._task_environment.label_schema), + ) + output_model.precision = self._precision diff --git a/src/otx/algorithms/action/tools/__init__.py b/src/otx/algorithms/action/tools/__init__.py new file mode 100644 index 00000000000..5305e88b7cb --- /dev/null +++ b/src/otx/algorithms/action/tools/__init__.py @@ -0,0 +1,5 @@ +"""Tools for action tasks.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/action/tools/sample_classification.py b/src/otx/algorithms/action/tools/sample_classification.py new file mode 100644 index 00000000000..36e86cefcd3 --- /dev/null +++ b/src/otx/algorithms/action/tools/sample_classification.py @@ -0,0 +1,178 @@ +"""Sample Code of otx training for action classification.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# ruff: noqa: E402 + +import argparse +import os +import sys + +os.environ["FEATURE_FLAGS_OTX_ACTION_TASKS"] = "1" + +from otx.algorithms.common.utils import get_task_class +from otx.api.configuration.helper import create +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.core.data.adapter import get_dataset_adapter +from otx.utils.logger import get_logger + +logger = get_logger() + + +def parse_args(): + """Parse function for getting model template & check export.""" + parser = argparse.ArgumentParser(description="Sample showcasing the new API") + parser.add_argument("template_file_path", help="path to template file") + parser.add_argument("--export", action="store_true") + return parser.parse_args() + + +TRAIN_DATA_ROOTS = "tests/assets/cvat_dataset/action_classification/train" +VAL_DATA_ROOTS = "tests/assets/cvat_dataset/action_classification/train" + + +def load_test_dataset(model_template): + """Load Sample dataset for detection.""" + + algo_backend = model_template.hyper_parameters.parameter_overrides["algo_backend"] + train_type = algo_backend["train_type"]["default_value"] + dataset_adapter = get_dataset_adapter( + model_template.task_type, + train_type, + train_data_roots=TRAIN_DATA_ROOTS, + val_data_roots=VAL_DATA_ROOTS, + ) + dataset = dataset_adapter.get_otx_dataset() + label_schema = dataset_adapter.get_label_schema() + return dataset, label_schema + + +# pylint: disable=too-many-locals, too-many-statements +def main(args): + """Main function of Detection Sample.""" + logger.info("Fine tuning sample dataset") + logger.info("Sample dataset can be found at tests/assets/cvat_dataset/action_classification") + + logger.info("Load model template") + model_template = parse_model_template(args.template_file_path) + + logger.info("Get dataset") + dataset, labels_schema = load_test_dataset(model_template) + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 1 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + output_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, output_model) + + logger.info("Get predictions on the validation set") + validation_dataset = dataset.get_subset(Subset.VALIDATION) + predicted_validation_dataset = task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=False), + ) + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + task.evaluate(resultset) + logger.info(str(resultset.performance)) + + if args.export: + logger.info("Export model") + exported_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.export(ExportType.OPENVINO, exported_model, dump_features=False) + + logger.info("Create OpenVINO Task") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + openvino_task_cls = get_task_class(openvino_task_impl_path) + openvino_task = openvino_task_cls(environment) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Run POT optimization") + optimized_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + openvino_task.optimize( + OptimizationType.POT, + dataset.get_subset(Subset.TRAINING), + optimized_model, + OptimizationParameters(), + ) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=optimized_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Performance of optimized model:") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + +if __name__ == "__main__": + sys.exit(main(parse_args()) or 0) diff --git a/src/otx/algorithms/action/tools/sample_detection.py b/src/otx/algorithms/action/tools/sample_detection.py new file mode 100644 index 00000000000..1caf67c7ba0 --- /dev/null +++ b/src/otx/algorithms/action/tools/sample_detection.py @@ -0,0 +1,176 @@ +"""Sample Code of otx training for action detection.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# ruff: noqa: E402 + +import argparse +import os +import sys + +os.environ["FEATURE_FLAGS_OTX_ACTION_TASKS"] = "1" + +from otx.algorithms.common.utils import get_task_class +from otx.api.configuration.helper import create +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.core.data.adapter import get_dataset_adapter +from otx.utils.logger import get_logger + +logger = get_logger() + + +def parse_args(): + """Parse function for getting model template & check export.""" + parser = argparse.ArgumentParser(description="Sample showcasing the new API") + parser.add_argument("template_file_path", help="path to template file") + parser.add_argument("--export", action="store_true") + return parser.parse_args() + + +TRAIN_DATA_ROOTS = "tests/assets/cvat_dataset/action_detection/train" +VAL_DATA_ROOTS = "tests/assets/cvat_dataset/action_detection/train" + + +def load_test_dataset(model_template): + """Load Sample dataset for detection.""" + algo_backend = model_template.hyper_parameters.parameter_overrides["algo_backend"] + train_type = algo_backend["train_type"]["default_value"] + dataset_adapter = get_dataset_adapter( + model_template.task_type, + train_type, + train_data_roots=TRAIN_DATA_ROOTS, + val_data_roots=VAL_DATA_ROOTS, + ) + dataset = dataset_adapter.get_otx_dataset() + label_schema = dataset_adapter.get_label_schema() + return dataset, label_schema + + +# pylint: disable=too-many-locals, too-many-statements +def main(args): + """Main function of Detection Sample.""" + logger.info("Fine tuning sample dataset") + logger.info("Sample dataset can be found at tests/assets/cvat_dataset/action_classification/train") + + logger.info("Load model template") + model_template = parse_model_template(args.template_file_path) + + logger.info("Get dataset") + dataset, labels_schema = load_test_dataset(model_template) + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 5 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + output_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, output_model) + + logger.info("Get predictions on the validation set") + validation_dataset = dataset.get_subset(Subset.VALIDATION) + predicted_validation_dataset = task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=False), + ) + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + task.evaluate(resultset) + logger.info(str(resultset.performance)) + + if args.export: + logger.info("Export model") + exported_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.export(ExportType.OPENVINO, exported_model, dump_features=False) + + logger.info("Create OpenVINO Task") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + openvino_task_cls = get_task_class(openvino_task_impl_path) + openvino_task = openvino_task_cls(environment) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + # FIXME. POT for action detection model fails. + # logger.info("Run POT optimization") + # optimized_model = ModelEntity( + # dataset, + # environment.get_model_configuration(), + # ) + # openvino_task.optimize( + # OptimizationType.POT, + # dataset.get_subset(Subset.TRAINING), + # optimized_model, + # OptimizationParameters(), + # ) + + # logger.info("Get predictions on the validation set") + # predicted_validation_dataset = openvino_task.infer( + # validation_dataset.with_empty_annotations(), + # InferenceParameters(is_evaluation=True), + # ) + # resultset = ResultSetEntity( + # model=optimized_model, + # ground_truth_dataset=validation_dataset, + # prediction_dataset=predicted_validation_dataset, + # ) + # logger.info("Performance of optimized model:") + # openvino_task.evaluate(resultset) + # logger.info(str(resultset.performance)) + + +if __name__ == "__main__": + sys.exit(main(parse_args()) or 0) diff --git a/src/otx/algorithms/action/utils/__init__.py b/src/otx/algorithms/action/utils/__init__.py new file mode 100644 index 00000000000..f000f5fb1b5 --- /dev/null +++ b/src/otx/algorithms/action/utils/__init__.py @@ -0,0 +1,5 @@ +"""Utils for action tasks.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/action/utils/convert_public_data_to_cvat.py b/src/otx/algorithms/action/utils/convert_public_data_to_cvat.py new file mode 100644 index 00000000000..26dd67dc05b --- /dev/null +++ b/src/otx/algorithms/action/utils/convert_public_data_to_cvat.py @@ -0,0 +1,368 @@ +"""Convert dataset: Public dataset (Jester[RawFrames], AVA) --> Datumaro dataset (CVAT). + +This script contains a lot of hardcoding to create an.xml file that Datumaro can consume. + +Current Datumaro format for video (CVAT) + +:: + + root + |- video_0 + | |- images + | |- frames_001.png + | |- frames_002.png + | |- annotations.xml + |- video_1 + | |- images + | |- annotations.xml + |- video_2 + +""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=too-many-locals, c-extension-no-member, invalid-name, too-many-statements +import argparse +import csv +import os +import os.path as osp +import pathlib +import shutil +from typing import List + +import cv2 + +# disable B410 import_lxml - the library used only for the xml writing +from lxml import etree # nosec B410 +from tqdm import tqdm + + +def generate_default_cvat_xml_fields(i, video_path, frame_list): + """Generate default CVAT xml fields required to make multi-video CVAT format by using Jester, and AVA dataset.""" + n_frames = len(frame_list) + annotations = etree.Element("annotations") + + version = etree.Element("version") + version.text = "1.1" + annotations.append(version) + + meta = etree.Element("meta") + annotations.append(meta) + + task = etree.Element("task") + meta.append(task) + + _id = etree.Element("id") + _id.text = str(i) + task.append(_id) + + name = etree.Element("name") + name.text = f"v{i}" + task.append(name) + + size = etree.Element("size") + size.text = str(n_frames) + task.append(size) + + mode = etree.Element("mode") + mode.text = "interpolation" + task.append(mode) + + overlap = etree.Element("overlap") + overlap.text = "2" + task.append(overlap) + + bugtracker = etree.Element("bugtracker") + bugtracker.text = "" + task.append(bugtracker) + + created = etree.Element("created") + created.text = "" + task.append(created) + + updated = etree.Element("updated") + updated.text = "" + task.append(updated) + + start_frame = etree.Element("start_frame") + start_frame.text = "0" + task.append(start_frame) + + stop_frame = etree.Element("stop_frame") + stop_frame.text = str(n_frames - 1) + task.append(stop_frame) + + frame_filter = etree.Element("frame_filter") + frame_filter.text = "1" + task.append(frame_filter) + + z_order = etree.Element("z_order") + z_order.text = str(True) + task.append(z_order) + + labels = etree.Element("labels") + task.append(labels) + + segments = etree.Element("segments") + segments.text = "" + task.append(segments) + + original_size = etree.Element("original_size") + task.append(original_size) + + sample_frame = cv2.imread(osp.join(video_path, frame_list[0])) + original_size_width = etree.Element("width") + original_size_width.text = str(sample_frame.shape[1]) + original_size.append(original_size_width) + + original_size_height = etree.Element("height") + original_size_height.text = str(sample_frame.shape[0]) + original_size.append(original_size_height) + + return annotations, sample_frame.shape, labels + + +# classification +def convert_action_cls_dataset_to_datumaro(src_path: str, dst_path: str, ann_file: str, label_map=None): + """Convert a public dataset to multi-video CVAT (Datumaro) format. + + Supported datasets are: Jester, HMDB51, UCF101 + + Args: + src_path (str): The path to the directory containing the video files as rawframe folder. + dst_path (str): The path to the directory where the multi-video CVAT (Datumaro) format dataset will be saved. + ann_file (str): The path to the file containing the annotations for the videos. + label_map (optional): The path to the file containing the mapping between class IDs and class names. + + Returns: + None + + Examples: + src_path = "./data/hmdb51/rawframes" + dst_path = "./data/hmdb51/CVAT/train" + ann_file = "./data/hmdb51/hmdb51_train_split_1_rawframes.txt" + label_map = "./data/hmdb51/label_map.txt" + convert_public_dataset_to_datumaro(src_path, dst_path, ann_file, label_map=label_map) + """ + + # Load the annotations file. Annotation is supposed as whitespace separated format: Video_name num_frames class_idx + # Ex) kiss/American_History_X_kiss_h_cm_np2_le_goo_40 69 22 + with open(ann_file, "r", encoding="utf-8") as anns: + pathlib.Path(osp.join(dst_path)).mkdir(parents=True, exist_ok=True) + + lines = anns.readlines() + for i, line in tqdm(enumerate(lines), total=len(lines)): + if line[0] == "#": + continue + + # Parse the video directory and class ID from the annotations file + video_dir, _, class_idx = line[:-1].split(" ") + + if label_map is not None: + # Load label mapping file : brush_hair\ncarwheel\ncatch, ... + with open(label_map, "r", encoding="utf-8") as f: + label_names = f.read().splitlines() + label_mapping_dict = {str(idx): label_name for idx, label_name in enumerate(label_names)} + class_name = label_mapping_dict[class_idx] + else: + class_name = class_idx + + # Prepare the output directories and file names + video_path = osp.join(src_path, video_dir) + video_name = f"Video_{i}" + images_dir = osp.join(dst_path, f"{video_name}/images") + + # List the frames in the video and sort them + frame_list = os.listdir(video_path) + frame_list.sort() + + # Generate default CVAT XML fields for the video annotation + annotations, img_shape, labels = generate_default_cvat_xml_fields(i, video_path, frame_list) + + # Add the video label to the annotations + label = etree.Element("label") + labels.append(label) + + name = etree.Element("name") + name.text = class_name + label.append(name) + + attributes = etree.Element("attributes") + attributes.text = "" + label.append(attributes) + + # Copy the video frames to the output directory and create the image tags in the annotations + for j, frame in enumerate(frame_list): + if not osp.exists(images_dir): + os.makedirs(images_dir, exist_ok=True) + + image_name = f"{j+1:05d}.jpg" + shutil.copy(osp.join(video_path, frame), osp.join(images_dir, image_name)) + image = etree.Element( + "image", id=str(j), name=image_name, width=str(img_shape[1]), height=str(img_shape[0]) + ) + tag = etree.Element("tag", label=class_name, source="manual") + tag.text = "" + image.append(tag) + annotations.append(image) + + et = etree.ElementTree(annotations) + et.write( + osp.join(dst_path, f"{video_name}/annotations.xml"), + pretty_print=True, + xml_declaration=True, + encoding="utf-8", + ) + + +def convert_ava_dataset_to_datumaro(src_path: str, dst_path: str, ann_file: str): + """Convert AVA dataset to multi-video CVAT (Datumaro) format. + + Args: + src_path (str): The path to the directory containing the video files as rawframe folder. + dst_path (str): The path to the directory where the multi-video CVAT (Datumaro) format dataset will be saved. + ann_file (str): The path to the file containing the annotations for the videos. + + Returns: + None + + Examples: + src_path = "./data/ava_dataset/frames" + dst_path = "./data/ava_dataset/CVAT/train" + ann_file = "./data/ava_dataset/annotations/train.csv" + convert_ava_dataset_to_datumaro(src_path, dst_path, ann_file) + """ + + video_dict = {} + video_idx = 0 + annot_info = read_ava_csv(ann_file) + video_dirs = os.listdir(src_path) + video_dirs.sort() + for video_dir in video_dirs: + if video_dir not in annot_info: + continue + video_path = osp.join(src_path, video_dir) + frame_list = os.listdir(video_path) + frame_list.sort() + if video_dir not in video_dict: + annotations, _, labels = generate_default_cvat_xml_fields(video_idx, video_path, frame_list) + label_list: List[str] = [] + video_dict[video_dir] = {"annotations": annotations, "labels": labels, "label_list": label_list} + for frame in frame_list: + frame_idx = int(frame.split(".")[0].split("_")[-1]) + if frame_idx in annot_info[video_dir]: + annots = annot_info[video_dir][frame_idx] + else: + annots = [[0, 0, 0, 0, "EmptyFrame"]] + + for annot in annots: + bboxes = annot[:4] + class_idx = annot[4] + track = etree.Element("track", id=str(video_idx), label=str(class_idx)) + if class_idx not in video_dict[video_dir]["label_list"]: + video_dict[video_dir]["label_list"].append(class_idx) + + label = etree.Element("label") + video_dict[video_dir]["labels"].append(label) + + name = etree.Element("name") + name.text = str(class_idx) + label.append(name) + + attributes = etree.Element("attributes") + attributes.text = "" + label.append(attributes) + box = etree.Element( + "box", + frame=str(frame_idx), + outside="0", # not used + occluded="0", # not used + xtl=str(bboxes[0]), + ytl=str(bboxes[1]), + xbr=str(bboxes[2]), + ybr=str(bboxes[3]), + z_order="0", + label=str(class_idx), + ) + box.text = "" + track.append(box) + video_dict[video_dir]["annotations"].append(track) + + for (video_dir, info) in video_dict.items(): + video_path = osp.join(src_path, video_dir) + shutil.copytree(video_path, osp.join(dst_path, f"{video_dir}/images"), copy_function=rename_and_copy) + et = etree.ElementTree(info["annotations"]) + et.write( + osp.join(dst_path, f"{video_dir}/annotations.xml"), + pretty_print=True, + xml_declaration=True, + encoding="utf-8", + ) + + +def rename_and_copy(_src, _dst): + """Change frame name to cvat format.""" + img_name = _dst.rsplit("/", maxsplit=1)[-1] + # FIXME This only support AVA dataset name + if "_" in img_name: + frame_index = int(img_name.split(".")[0].split("_")[-1]) + else: + frame_index = int(img_name.split(".")[0]) + new_img_name = f"frame_{frame_index:06d}.png" + _dst = _dst.replace(img_name, new_img_name) + shutil.copy2(_src, _dst) + + +def read_ava_csv(csv_path): + """Read ava format annotation csv file.""" + annot_info = {} # {video_id: {frame_idx: [annot0, annot1, ..., annotN]}} + with open(csv_path, "r", encoding="utf-8") as csv_file: + csv_reader = csv.reader(csv_file, delimiter=",") + for line in csv_reader: + video_id, frame_idx, bboxes, class_idx = line[0], line[1], line[2:6], line[6] + frame_idx = int(frame_idx) + bboxes.append(class_idx) + if video_id in annot_info: + if frame_idx in annot_info[video_id]: + annot_info[video_id][frame_idx].append(bboxes) + else: + annot_info[video_id][frame_idx] = [bboxes] + else: + annot_info[video_id] = {frame_idx: [bboxes]} + return annot_info + + +def parse_args(): + """Parses command line arguments.""" + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--task", required=True, type=str) + parser.add_argument("--src_path", required=True, type=str) + parser.add_argument("--dst_path", required=True, type=str) + parser.add_argument("--ann_file", required=True, type=str) + parser.add_argument("--label_map", type=str, default=None) + return parser.parse_args() + + +def main(): + """Main function.""" + args = parse_args() + if args.task == "action_classification": + convert_action_cls_dataset_to_datumaro(args.src_path, args.dst_path, args.ann_file, args.label_map) + elif args.task == "action_detection": + convert_ava_dataset_to_datumaro(args.src_path, args.dst_path, args.ann_file) + + +if __name__ == "__main__": + main() diff --git a/src/otx/algorithms/action/utils/data.py b/src/otx/algorithms/action/utils/data.py new file mode 100644 index 00000000000..2248096ea4e --- /dev/null +++ b/src/otx/algorithms/action/utils/data.py @@ -0,0 +1,211 @@ +"""Collection of utils for data in Action Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import os.path as osp +from collections import defaultdict +from typing import List, Optional + +import numpy as np +from mmcv import ConfigDict + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset + + +def find_label_by_name(labels: List[LabelEntity], name: str, domain: Domain): + """Return label from name.""" + matching_labels = [label for label in labels if label.name == name] + if len(matching_labels) == 1: + return matching_labels[0] + if len(matching_labels) == 0: + label = LabelEntity(name=name, domain=domain, id=ID(int(name))) + labels.append(label) + return label + raise ValueError("Found multiple matching labels") + + +def load_cls_annotations(ann_file, data_root): + """Load annotation file to get video information.""" + video_infos = [] + with open(ann_file, "r", encoding="UTF-8") as fin: + for line in fin: + line_split = line.strip().split() + video_info = {} + idx = 0 + # idx for frame_dir + if line_split[0] == "#": + continue + frame_dir = line_split[idx] + if data_root is not None: + frame_dir = osp.join(data_root, frame_dir) + video_info["frame_dir"] = frame_dir + idx += 1 + # idx for total_frames + # TODO Support offsets in dataset + video_info["total_frames"] = int(line_split[idx]) + idx += 1 + # idx for label[s] + # TODO Support multi-label setting + label = [int(x) for x in line_split[idx:]] + assert label, f"missing label in line: {line}" + assert len(label) == 1 + video_info["label"] = label[0] + video_infos.append(video_info) + + return video_infos + + +# pylint: disable=too-many-locals +def load_det_annotations(ann_file, data_root): + """Load AVA annotations.""" + video_infos = [] + records_dict_by_img = defaultdict(list) + with open(ann_file, "r", encoding="utf-8") as fin: + for line in fin: + line_split = line.strip().split(",") + + label = int(line_split[6]) + video_id = line_split[0] + timestamp = int(line_split[1]) + img_key = f"{video_id},{timestamp}" + + entity_box = np.array(list(map(float, line_split[2:6]))) + entity_id = int(line_split[7]) + + video_info = dict( + video_id=video_id, + timestamp=timestamp, + entity_box=entity_box, + label=label, + entity_id=entity_id, + ) + records_dict_by_img[img_key].append(video_info) + + for img_key in records_dict_by_img: + video_id, timestamp = img_key.split(",") + bboxes, labels, entity_ids = parse_img_record(records_dict_by_img[img_key]) + ann = dict(gt_bboxes=bboxes, gt_labels=labels, entity_ids=entity_ids) + frame_dir = video_id + if data_root is not None: + frame_dir = osp.join(data_root, frame_dir) + # FIXME Image shape is hard-coded, this will be replaced with CVAT format + video_info = dict( + frame_dir=frame_dir, + video_id=video_id, + timestamp=int(timestamp), + img_key=img_key, + ann=ann, + width=320, + height=240, + ) + video_infos.append(video_info) + + return video_infos + + +def parse_img_record(img_records): + """Accumulate and colligate bbox annotation info.""" + bboxes, labels, entity_ids = [], [], [] + while len(img_records) > 0: + img_record = img_records[0] + num_img_records = len(img_records) + + selected_records = [x for x in img_records if np.array_equal(x["entity_box"], img_record["entity_box"])] + + num_selected_records = len(selected_records) + img_records = [x for x in img_records if not np.array_equal(x["entity_box"], img_record["entity_box"])] + + assert len(img_records) + num_selected_records == num_img_records + + bboxes.append(img_record["entity_box"]) + valid_labels = np.array([selected_record["label"] for selected_record in selected_records]) + labels.append(valid_labels) + entity_ids.append(img_record["entity_id"]) + + bboxes = np.stack(bboxes) + entity_ids = np.stack(entity_ids) + return bboxes, labels, entity_ids + + +# pylint: disable=too-many-locals +def load_cls_dataset( + ann_file_path: str, + data_root_dir: str, + domain: Domain, + subset: Subset = Subset.NONE, + labels_list: Optional[List[LabelEntity]] = None, +): + """Convert video annotation information into DatasetItemEntity.""" + dataset_items = [] + video_infos = load_cls_annotations(ann_file_path, data_root_dir) + + for video_info in video_infos: + label = video_info.pop("label") + label = find_label_by_name(labels_list, str(label), domain) + shapes = [Annotation(Rectangle.generate_full_box(), [ScoredLabel(label)])] + dataset_item = DatasetItemEntity( + media=video_info, + annotation_scene=AnnotationSceneEntity(annotations=shapes, kind=AnnotationSceneKind.ANNOTATION), + subset=subset, + ) + dataset_items.append(dataset_item) + + return dataset_items + + +# pylint: disable=too-many-locals +def load_det_dataset( + ann_file_path: str, + data_root_dir: str, + domain: Domain, + subset: Subset = Subset.NONE, + labels_list: Optional[List[LabelEntity]] = None, +): + """Convert video annotation information into DatasetItemEntity.""" + dataset_items = [] + video_infos = load_det_annotations(ann_file_path, data_root_dir) + + for video_info in video_infos: + ann = video_info.pop("ann") + # TODO Check use of entity_ids + gt_bboxes = ann["gt_bboxes"] + gt_labels = ann["gt_labels"] + shapes = [] + for bbox, labels in zip(gt_bboxes, gt_labels): + labels = [find_label_by_name(labels_list, str(label), domain) for label in labels] + shapes.append( + Annotation( + Rectangle(bbox[0], bbox[1], bbox[2], bbox[3]), + [ScoredLabel(label, probability=1.0) for label in labels], + ) + ) + dataset_item = DatasetItemEntity( + media=ConfigDict(video_info), + annotation_scene=AnnotationSceneEntity(annotations=shapes, kind=AnnotationSceneKind.ANNOTATION), + subset=subset, + ) + dataset_items.append(dataset_item) + + return dataset_items diff --git a/src/otx/algorithms/anomaly/__init__.py b/src/otx/algorithms/anomaly/__init__.py new file mode 100644 index 00000000000..908d3c99cd9 --- /dev/null +++ b/src/otx/algorithms/anomaly/__init__.py @@ -0,0 +1,4 @@ +"""OTX Algorithms - Anomaly.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/anomaly/adapters/__init__.py b/src/otx/algorithms/anomaly/adapters/__init__.py new file mode 100644 index 00000000000..9c785467cb8 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/__init__.py @@ -0,0 +1,19 @@ +"""Adapters for Anomalib.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +from .anomalib.accelerators.xpu import XPUAccelerator # noqa: F401 +from .anomalib.strategies import SingleXPUStrategy # noqa: F401 diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/__init__.py b/src/otx/algorithms/anomaly/adapters/anomalib/__init__.py new file mode 100644 index 00000000000..5088047c3f6 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/__init__.py @@ -0,0 +1,4 @@ +"""OTX Adapters - Anomalib.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/accelerators/__init__.py b/src/otx/algorithms/anomaly/adapters/anomalib/accelerators/__init__.py new file mode 100644 index 00000000000..b6c9661d650 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/accelerators/__init__.py @@ -0,0 +1,8 @@ +"""Lightning accelerator for XPU device.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .xpu import XPUAccelerator + +__all__ = ["XPUAccelerator"] diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/accelerators/xpu.py b/src/otx/algorithms/anomaly/adapters/anomalib/accelerators/xpu.py new file mode 100644 index 00000000000..624a0ec5308 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/accelerators/xpu.py @@ -0,0 +1,60 @@ +"""Lightning accelerator for XPU device.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Any, Dict, Union + +import torch +from pytorch_lightning.accelerators import AcceleratorRegistry +from pytorch_lightning.accelerators.accelerator import Accelerator + +from otx.algorithms.common.utils.utils import is_xpu_available + + +class XPUAccelerator(Accelerator): + """Support for a XPU, optimized for large-scale machine learning.""" + + accelerator_name = "xpu" + + def setup_device(self, device: torch.device) -> None: + """Sets up the specified device.""" + if device.type != "xpu": + raise RuntimeError(f"Device should be xpu, got {device} instead") + + torch.xpu.set_device(device) + + @staticmethod + def parse_devices(devices: Any) -> Any: + """Parses devices for multi-GPU training.""" + if isinstance(devices, list): + return devices + return [devices] + + @staticmethod + def get_parallel_devices(devices: Any) -> Any: + """Generates a list of parrallel devices.""" + return [torch.device("xpu", idx) for idx in devices] + + @staticmethod + def auto_device_count() -> int: + """Returns number of XPU devices available.""" + return torch.xpu.device_count() + + @staticmethod + def is_available() -> bool: + """Checks if XPU available.""" + return is_xpu_available() + + def get_device_stats(self, device: Union[str, torch.device]) -> Dict[str, Any]: + """Returns XPU devices stats.""" + return {} + + def teardown(self) -> None: + """Cleans-up XPU-related resources.""" + pass + + +AcceleratorRegistry.register( + XPUAccelerator.accelerator_name, XPUAccelerator, description="Accelerator supports XPU devices" +) diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/__init__.py b/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/__init__.py new file mode 100644 index 00000000000..8e9e513dc70 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/__init__.py @@ -0,0 +1,21 @@ +"""Callbacks for OTX inference.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .inference import AnomalyInferenceCallback +from .iteration_timer import IterationTimer +from .progress import ProgressCallback + +__all__ = ["AnomalyInferenceCallback", "IterationTimer", "ProgressCallback"] diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/inference.py b/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/inference.py new file mode 100644 index 00000000000..fdf5ddbd9bc --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/inference.py @@ -0,0 +1,169 @@ +"""Inference Callbacks for OTX inference.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from typing import Any, List + +import numpy as np +import pytorch_lightning as pl +import torch +from anomalib.models import AnomalyModule +from pytorch_lightning.callbacks import Callback +from torch import Tensor + +from otx.api.entities.annotation import Annotation +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import LabelEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.result_media import ResultMediaEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.utils.segmentation_utils import create_annotation_from_segmentation_map +from otx.utils.logger import get_logger + +logger = get_logger() + + +class AnomalyInferenceCallback(Callback): + """Callback that updates the OTX dataset during inference.""" + + def __init__(self, otx_dataset: DatasetEntity, labels: List[LabelEntity], task_type: TaskType): + self.otx_dataset = otx_dataset + self.normal_label = [label for label in labels if not label.is_anomalous][0] + self.anomalous_label = [label for label in labels if label.is_anomalous][0] + self.task_type = task_type + self.label_map = {0: self.normal_label, 1: self.anomalous_label} + + def on_predict_epoch_end(self, _trainer: pl.Trainer, _pl_module: AnomalyModule, outputs: List[Any]): + """Call when the predict epoch ends.""" + # TODO; refactor Ignore too many locals + # pylint: disable=too-many-locals + outputs = outputs[0] + # collect generic predictions + pred_scores = torch.hstack([output["pred_scores"].cpu() for output in outputs]) + pred_labels = torch.hstack([output["pred_labels"].cpu() for output in outputs]) + anomaly_maps = torch.vstack([output["anomaly_maps"].cpu() for output in outputs]) + pred_masks = torch.vstack([output["pred_masks"].cpu() for output in outputs]) + + # add the predictions to the dataset item depending on the task type + if self.task_type == TaskType.ANOMALY_CLASSIFICATION: + self._process_classification_predictions(pred_labels, pred_scores) + elif self.task_type == TaskType.ANOMALY_DETECTION: + # collect detection predictions + pred_boxes = [] + box_scores = [] + box_labels = [] + for output in outputs: + pred_boxes.extend(output["pred_boxes"]) + box_scores.extend(output["box_scores"]) + box_labels.extend(output["box_labels"]) + + self._process_detection_predictions(pred_boxes, box_scores, box_labels, pred_scores, pred_masks.shape[-2:]) + elif self.task_type == TaskType.ANOMALY_SEGMENTATION: + self._process_segmentation_predictions(pred_masks, anomaly_maps, pred_scores) + + # add anomaly map as metadata + for dataset_item, anomaly_map in zip(self.otx_dataset, anomaly_maps): + dataset_item.append_metadata_item( + ResultMediaEntity( + name="Anomaly Map", + type="anomaly_map", + label=dataset_item.annotation_scene.get_labels()[0], + annotation_scene=dataset_item.annotation_scene, + numpy=(anomaly_map * 255).squeeze().cpu().numpy().astype(np.uint8), + ) + ) + + def _process_classification_predictions(self, pred_labels: Tensor, pred_scores: Tensor): + """Add classification predictions to the dataset items. + + Args: + pred_labels (Tensor): Predicted image labels. + pred_scores (Tensor): Predicted image-level anomaly scores. + """ + for dataset_item, pred_label, pred_score in zip(self.otx_dataset, pred_labels, pred_scores): + # get label + label = self.anomalous_label if pred_label else self.normal_label + probability = pred_score if pred_label else 1 - pred_score + # update dataset item + dataset_item.append_labels([ScoredLabel(label=label, probability=float(probability))]) + + def _process_detection_predictions( + self, + pred_boxes: List[Tensor], + box_scores: List[Tensor], + box_labels: List[Tensor], + pred_scores: Tensor, + image_size: torch.Size, + ): + """Add detection predictions to the dataset items. + + Args: + pred_boxes (List[Tensor]): Predicted bounding box locations. + box_scores (List[Tensor]): Predicted anomaly scores for the bounding boxes. + box_labels (List[Tensor]): Predicted labels for the bounding boxes. + pred_scores (Tensor): Predicted image-level anomaly scores. + image_size: (torch.Size): Image size of the original images. + """ + # TODO; refactor Ignore too many locals + # pylint: disable=too-many-locals + height, width = image_size + for dataset_item, im_boxes, im_box_scores, im_box_labels, pred_score in zip( + self.otx_dataset, pred_boxes, box_scores, box_labels, pred_scores + ): + # generate annotations + annotations: List[Annotation] = [] + for box, score, label in zip(im_boxes, im_box_scores, im_box_labels): + if box[0] >= box[2] or box[1] >= box[3]: # discard 1-pixel boxes + continue + shape = Rectangle( + x1=box[0].item() / width, + y1=box[1].item() / height, + x2=box[2].item() / width, + y2=box[3].item() / height, + ) + label = self.label_map[label.item()] + probability = score.item() + annotations.append(Annotation(shape=shape, labels=[ScoredLabel(label=label, probability=probability)])) + # get label + label = self.anomalous_label if annotations else self.normal_label + probability = pred_score if label.is_anomalous else 1 - pred_score + # update dataset item + dataset_item.append_annotations(annotations) + dataset_item.append_labels([ScoredLabel(label=label, probability=float(probability))]) + + def _process_segmentation_predictions(self, pred_masks: Tensor, anomaly_maps: Tensor, pred_scores: Tensor): + """Add segmentation predictions to the dataset items. + + Args: + pred_masks (Tensor): Predicted anomaly masks. + anomaly_maps (Tensor): Predicted pixel-level anomaly scores. + pred_scores (Tensor): Predicted image-level anomaly scores. + """ + for dataset_item, pred_mask, anomaly_map, pred_score in zip( + self.otx_dataset, pred_masks, anomaly_maps, pred_scores + ): + # generate polygon annotations + annotations = create_annotation_from_segmentation_map( + hard_prediction=pred_mask.squeeze().numpy().astype(np.uint8), + soft_prediction=anomaly_map.squeeze().numpy(), + label_map=self.label_map, + ) + # get label + label = self.normal_label if len(annotations) == 0 else self.anomalous_label + probability = pred_score if label.is_anomalous else 1 - pred_score + # update dataset item + dataset_item.append_annotations(annotations) + dataset_item.append_labels([ScoredLabel(label=label, probability=float(probability))]) diff --git a/src/otx/algo/callbacks/iteration_timer.py b/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/iteration_timer.py similarity index 88% rename from src/otx/algo/callbacks/iteration_timer.py rename to src/otx/algorithms/anomaly/adapters/anomalib/callbacks/iteration_timer.py index 2312f410000..84ab3a69d5d 100644 --- a/src/otx/algo/callbacks/iteration_timer.py +++ b/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/iteration_timer.py @@ -1,18 +1,14 @@ -# Copyright (C) 2023 Intel Corporation +"""Timer for logging iteration time for train, val, and test phases.""" +# Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # -"""Timer for logging iteration time for train, val, and test phases.""" - -from __future__ import annotations from collections import defaultdict from time import time -from typing import TYPE_CHECKING, Any - -from lightning import Callback, LightningModule, Trainer +from typing import Any, Dict -if TYPE_CHECKING: - from lightning.pytorch.utilities.types import STEP_OUTPUT +from pytorch_lightning import Callback, LightningModule, Trainer +from pytorch_lightning.utilities.types import STEP_OUTPUT class IterationTimer(Callback): @@ -29,8 +25,8 @@ def __init__( self.on_step = on_step self.on_epoch = on_epoch - self.start_time: dict[str, float] = defaultdict(float) - self.end_time: dict[str, float] = defaultdict(float) + self.start_time: Dict[str, float] = defaultdict(float) + self.end_time: Dict[str, float] = defaultdict(float) def on_train_epoch_start(self, trainer: Trainer, pl_module: LightningModule) -> None: """Reset timer before every train epoch starts.""" @@ -106,7 +102,7 @@ def on_train_batch_start( self._on_batch_start( pl_module=pl_module, phase="train", - batch_size=batch.batch_size, + batch_size=batch["image"].shape[0], ) def on_train_batch_end( @@ -121,7 +117,7 @@ def on_train_batch_end( self._on_batch_end( pl_module=pl_module, phase="train", - batch_size=batch.batch_size, + batch_size=batch["image"].shape[0], ) def on_validation_batch_start( @@ -136,7 +132,7 @@ def on_validation_batch_start( self._on_batch_start( pl_module=pl_module, phase="validation", - batch_size=batch.batch_size, + batch_size=batch["image"].shape[0], ) def on_validation_batch_end( @@ -152,7 +148,7 @@ def on_validation_batch_end( self._on_batch_end( pl_module=pl_module, phase="validation", - batch_size=batch.batch_size, + batch_size=batch["image"].shape[0], ) def on_test_batch_start( @@ -167,7 +163,7 @@ def on_test_batch_start( self._on_batch_start( pl_module=pl_module, phase="test", - batch_size=batch.batch_size, + batch_size=batch["image"].shape[0], ) def on_test_batch_end( @@ -183,5 +179,5 @@ def on_test_batch_end( self._on_batch_end( pl_module=pl_module, phase="test", - batch_size=batch.batch_size, + batch_size=batch["image"].shape[0], ) diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/progress.py b/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/progress.py new file mode 100644 index 00000000000..780109af8f0 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/progress.py @@ -0,0 +1,121 @@ +"""Progressbar and Score Reporting callback Callback for OTX task. + +TODO Since only one progressbar callback is supported HPO is combined into one callback. Remove this after the refactor +""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from typing import Optional, Union + +from pytorch_lightning.callbacks.progress import TQDMProgressBar + +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.train_parameters import TrainParameters, default_progress_callback + + +class ProgressCallback(TQDMProgressBar): + """Progress Callback. + + Modify progress callback to show completion of the entire training step. + """ + + def __init__( + self, parameters: Optional[Union[TrainParameters, InferenceParameters, OptimizationParameters]] = None + ) -> None: + super().__init__() + self.current_epoch: int = 0 + self.max_epochs: int = 0 + self._progress: float = 0 + + if parameters is not None: + self.progress_and_hpo_callback = parameters.update_progress + else: + self.progress_and_hpo_callback = default_progress_callback + + def on_train_start(self, trainer, pl_module): + """Store max epochs and current epoch from trainer.""" + super().on_train_start(trainer, pl_module) + self.current_epoch = trainer.current_epoch + self.max_epochs = trainer.max_epochs + self._reset_progress() + + def on_predict_start(self, trainer, pl_module): + """Reset progress bar when prediction starts.""" + super().on_predict_start(trainer, pl_module) + self._reset_progress() + + def on_test_start(self, trainer, pl_module): + """Reset progress bar when testing starts.""" + super().on_test_start(trainer, pl_module) + self._reset_progress() + + def on_train_batch_end(self, trainer, pl_module, outputs, batch, batch_idx): + """Adds training completion percentage to the progress bar.""" + super().on_train_batch_end(trainer, pl_module, outputs, batch, batch_idx) + self.current_epoch = trainer.current_epoch + self._update_progress(stage="train") + + def on_predict_batch_end(self, trainer, pl_module, outputs, batch, batch_idx, dataloader_idx): + """Adds prediction completion percentage to the progress bar.""" + super().on_predict_batch_end(trainer, pl_module, outputs, batch, batch_idx, dataloader_idx) + self._update_progress(stage="predict") + + def on_test_batch_end(self, trainer, pl_module, outputs, batch, batch_idx, dataloader_idx): + """Adds testing completion percentage to the progress bar.""" + super().on_test_batch_end(trainer, pl_module, outputs, batch, batch_idx, dataloader_idx) + self._update_progress(stage="test") + + def on_validation_epoch_end(self, trainer, pl_module): # pylint: disable=unused-argument + """If score exists in trainer.logged_metrics, report the score.""" + if self.progress_and_hpo_callback is not None: + score = None + metric = getattr(self.progress_and_hpo_callback, "metric", None) + if metric in trainer.logged_metrics: + score = float(trainer.logged_metrics[metric]) + + # Always assumes that hpo validation step is called during training. + self.progress_and_hpo_callback(int(self._get_progress("train")), score) # pylint: disable=not-callable + + def _reset_progress(self): + self._progress = 0.0 + + def _get_progress(self, stage: str = "train") -> float: + """Get progress for train and test stages. + + Args: + stage (str, optional): Train or Test stages. Defaults to "train". + """ + if stage == "train": + # Progress is calculated on the upper bound (max epoch). + # Early stopping might stop the training before the progress reaches 100% + self._progress = ( + (self.train_batch_idx + self.current_epoch * self.total_train_batches) + / (self.total_train_batches * self.max_epochs) + ) * 100 + + elif stage == "predict": + self._progress = (self.predict_batch_idx / (self.total_predict_batches_current_dataloader + 1e-10)) * 100 + + elif stage == "test": + self._progress = (self.test_batch_idx / (self.total_test_batches_current_dataloader + 1e-10)) * 100 + else: + raise ValueError(f"Unknown stage {stage}. Available: train, predict and test") + + return self._progress + + def _update_progress(self, stage: str): + progress = self._get_progress(stage) + self.progress_and_hpo_callback(int(progress), None) diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/config/__init__.py b/src/otx/algorithms/anomaly/adapters/anomalib/config/__init__.py new file mode 100644 index 00000000000..525b1dae3c0 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/config/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Configurable parameter conversion between OTX and Anomalib.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .anomalib_config import get_anomalib_config, update_anomalib_config + +__all__ = ["get_anomalib_config", "update_anomalib_config"] diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/config/anomalib_config.py b/src/otx/algorithms/anomaly/adapters/anomalib/config/anomalib_config.py new file mode 100644 index 00000000000..e6a92f1feb6 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/config/anomalib_config.py @@ -0,0 +1,104 @@ +"""Configurable parameter conversion between OTX and Anomalib.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from pathlib import Path +from typing import Union + +import anomalib +from anomalib.config.config import get_configurable_parameters +from omegaconf import DictConfig, ListConfig + +from otx.api.configuration.configurable_parameters import ConfigurableParameters + + +def get_anomalib_config(task_name: str, otx_config: ConfigurableParameters) -> Union[DictConfig, ListConfig]: + """Get anomalib configuration. + + Create an anomalib config object that matches the values specified in the + OTX config. + + Args: + task_name: Task name to load configuration from the Anomalib + otx_config: ConfigurableParameters: OTX config object parsed from + configuration.yaml file + + Returns: + Anomalib config object for the specified model type with overwritten + default values. + """ + config_path = Path(anomalib.__file__).parent / "models" / task_name.lower() / "config.yaml" + anomalib_config = get_configurable_parameters(model_name=task_name.lower(), config_path=config_path) + # TODO: remove this hard coding of the config location + if anomalib_config.model.name == "draem": + anomalib_config.dataset.transform_config.train = ( + f"src/otx/algorithms/adapters/anomalib/configs/{task_name.lower()}/draem/transform_config.yaml" + ) + anomalib_config.dataset.transform_config.val = ( + f"src/otx/algorithms/adapters/anomalib/configs/{task_name.lower()}/draem/transform_config.yaml" + ) + else: + anomalib_config.dataset.transform_config.train = None + anomalib_config.dataset.transform_config.val = None + update_anomalib_config(anomalib_config, otx_config) + return anomalib_config + + +def _anomalib_config_mapper(anomalib_config: Union[DictConfig, ListConfig], otx_config: ConfigurableParameters): + """Return mapping from learning parameters to anomalib parameters. + + Args: + anomalib_config: DictConfig: Anomalib config object + otx_config: ConfigurableParameters: OTX config object parsed from configuration.yaml file + """ + parameters = otx_config.parameters + groups = otx_config.groups + for name in parameters: + if name == "train_batch_size": + anomalib_config.dataset["train_batch_size"] = getattr(otx_config, "train_batch_size") + elif name == "max_epochs": + anomalib_config.trainer["max_epochs"] = getattr(otx_config, "max_epochs") + else: + assert name in anomalib_config.model.keys(), f"Parameter {name} not present in anomalib config." + sc_value = getattr(otx_config, name) + sc_value = sc_value.value if hasattr(sc_value, "value") else sc_value + anomalib_config.model[name] = sc_value + for group in groups: + update_anomalib_config(anomalib_config.model[group], getattr(otx_config, group)) + + +def update_anomalib_config(anomalib_config: Union[DictConfig, ListConfig], otx_config: ConfigurableParameters): + """Update anomalib configuration. + + Overwrite the default parameter values in the anomalib config with the + values specified in the OTX config. The function is recursively called for + each parameter group present in the OTX config. + + Args: + anomalib_config: DictConfig: Anomalib config object + otx_config: ConfigurableParameters: OTX config object parsed from + configuration.yaml file + """ + for param in otx_config.parameters: + assert param in anomalib_config.keys(), f"Parameter {param} not present in anomalib config." + sc_value = getattr(otx_config, param) + sc_value = sc_value.value if hasattr(sc_value, "value") else sc_value + anomalib_config[param] = sc_value + for group in otx_config.groups: + # Since pot_parameters and nncf_optimization are specific to OTX + if group == "learning_parameters": + _anomalib_config_mapper(anomalib_config, getattr(otx_config, "learning_parameters")) + elif group not in ["pot_parameters", "nncf_optimization"]: + update_anomalib_config(anomalib_config[group], getattr(otx_config, group)) diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/data/__init__.py b/src/otx/algorithms/anomaly/adapters/anomalib/data/__init__.py new file mode 100644 index 00000000000..3c294712e70 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/data/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Anomaly Dataset Utils.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .data import OTXAnomalyDataModule + +__all__ = ["OTXAnomalyDataModule"] diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/data/create_mvtec_ad_json_annotations.py b/src/otx/algorithms/anomaly/adapters/anomalib/data/create_mvtec_ad_json_annotations.py new file mode 100644 index 00000000000..053e27fa2cb --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/data/create_mvtec_ad_json_annotations.py @@ -0,0 +1,275 @@ +# Copyright (C) 2020-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +"""Create MVTec AD (CC BY-NC-SA 4.0) JSON Annotations for OTX CLI. + +Description: + This script converts MVTec AD dataset masks to OTX CLI annotation format for + classification, detection and segmentation tasks. + +License: + MVTec AD dataset is released under the Creative Commons + Attribution-NonCommercial-ShareAlike 4.0 International License + (CC BY-NC-SA 4.0)(https://creativecommons.org/licenses/by-nc-sa/4.0/). + +Reference: + - Paul Bergmann, Kilian Batzner, Michael Fauser, David Sattlegger, Carsten Steger: + The MVTec Anomaly Detection Dataset: A Comprehensive Real-World Dataset for + Unsupervised Anomaly Detection; in: International Journal of Computer Vision + 129(4):1038-1059, 2021, DOI: 10.1007/s11263-020-01400-4. + + - Paul Bergmann, Michael Fauser, David Sattlegger, Carsten Steger: MVTec AD — + A Comprehensive Real-World Dataset for Unsupervised Anomaly Detection; + in: IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), + 9584-9592, 2019, DOI: 10.1109/CVPR.2019.00982. + +Example: + Assume that MVTec AD dataset is located in "./data/anomaly/MVTec/" from the root + directory in training_extensions. JSON annotations could be created by running the + following: + + >>> import os + '~/training_extensions' + >>> os.listdir("./data/anomaly") + ['detection', 'shapes', 'segmentation', 'MVTec', 'classification'] + + The following script will generate the classification, detection and segmentation + JSON annotations to each category in ./data/anomaly/MVTec dataset. + + >>> python external/anomaly/adapters.anomalib/data/create_mvtec_ad_json_annotations.py \ + ... --data_path ./data/anomaly/MVTec/ +""" + +import json +import os +from argparse import ArgumentParser, Namespace +from pathlib import Path +from typing import Any, Dict, List, Optional + +import cv2 +import pandas as pd +from anomalib.data.mvtec import make_mvtec_dataset +from anomalib.data.utils import Split + + +def create_bboxes_from_mask(mask_path: str) -> List[List[float]]: + """Create bounding box from binary mask. + + Args: + mask_path (str): Path to binary mask. + + Returns: + List[List[float]]: Bounding box coordinates. + """ + # pylint: disable-msg=too-many-locals + + mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) + height, width = mask.shape + + bboxes: List[List[float]] = [] + _, _, coordinates, _ = cv2.connectedComponentsWithStats(mask) + for i, coordinate in enumerate(coordinates): + # First row of the coordinates is always backround, + # so should be ignored. + if i == 0: + continue + + # Last column of the coordinates is the area of the connected component. + # It could therefore be ignored. + comp_x, comp_y, comp_w, comp_h, _ = coordinate + x1 = comp_x / width + y1 = comp_y / height + x2 = (comp_x + comp_w) / width + y2 = (comp_y + comp_h) / height + + bboxes.append([x1, y1, x2, y2]) + + return bboxes + + +def create_polygons_from_mask(mask_path: str) -> List[List[List[float]]]: + """Create polygons from binary mask. + + Args: + mask_path (str): Path to binary mask. + + Returns: + List[List[float]]: Polygon coordinates. + """ + mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) + height, width = mask.shape + + polygons = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0] + polygons = [[[point[0][0] / width, point[0][1] / height] for point in polygon] for polygon in polygons] + + return polygons + + +def create_classification_json_items(pd_items: pd.DataFrame) -> Dict[str, Any]: + """Create JSON items for the classification task. + + Args: + pd_items (pd.DataFrame): MVTec AD samples in pandas DataFrame object. + + Returns: + Dict[str, Any]: MVTec AD classification JSON items + """ + json_items: Dict[str, Any] = {"image_path": {}, "label": {}, "masks": {}} + for index, pd_item in pd_items.iterrows(): + json_items["image_path"][str(index)] = pd_item.image_path.replace(pd_item.path, "")[1:] + json_items["label"][str(index)] = pd_item.label + if pd_item.label != "good": + json_items["masks"][str(index)] = pd_item.mask_path.replace(pd_item.path, "")[1:] + + return json_items + + +def create_detection_json_items(pd_items: pd.DataFrame) -> Dict[str, Any]: + """Create JSON items for the detection task. + + Args: + pd_items (pd.DataFrame): MVTec AD samples in pandas DataFrame object. + + Returns: + Dict[str, Any]: MVTec AD detection JSON items + """ + json_items: Dict[str, Any] = {"image_path": {}, "label": {}, "bboxes": {}} + for index, pd_item in pd_items.iterrows(): + json_items["image_path"][str(index)] = pd_item.image_path.replace(pd_item.path, "")[1:] + json_items["label"][str(index)] = pd_item.label + if pd_item.label != "good": + json_items["bboxes"][str(index)] = create_bboxes_from_mask(pd_item.mask_path) + + return json_items + + +def create_segmentation_json_items(pd_items: pd.DataFrame) -> Dict[str, Any]: + """Create JSON items for the segmentation task. + + Args: + pd_items (pd.DataFrame): MVTec AD samples in pandas DataFrame object. + + Returns: + Dict[str, Any]: MVTec AD segmentation JSON items + """ + json_items: Dict[str, Any] = {"image_path": {}, "label": {}, "masks": {}} + for index, pd_item in pd_items.iterrows(): + json_items["image_path"][str(index)] = pd_item.image_path.replace(pd_item.path, "")[1:] + json_items["label"][str(index)] = pd_item.label + if pd_item.label != "good": + json_items["masks"][str(index)] = create_polygons_from_mask(pd_item.mask_path) + + return json_items + + +def save_json_items(json_items: Dict[str, Any], file: str) -> None: + """Save JSON items to file. + + Args: + json_items (Dict[str, Any]): MVTec AD JSON items + file (str): Path to save as a JSON file. + """ + with open(file=file, mode="w", encoding="utf-8") as f: + json.dump(json_items, f) + + +def create_task_annotations(task: str, data_path: str, annotation_path: str) -> None: + """Create MVTec AD categories for a given task. + + Args: + task (str): Task type to save annotations. + data_path (str): Path to MVTec AD category. + annotation_path (str): Path to save MVTec AD category JSON annotation items. + + Raises: + ValueError: When task is not classification, detection or segmentation. + """ + annotation_path = os.path.join(annotation_path, task) + os.makedirs(annotation_path, exist_ok=True) + + for split in ["train", "val", "test"]: + + if task == "classification": + create_json_items = create_classification_json_items + elif task == "detection": + create_json_items = create_detection_json_items + elif task == "segmentation": + create_json_items = create_segmentation_json_items + else: + raise ValueError(f"Unknown task {task}. Available tasks are classification, detection and segmentation.") + + if split == "train": + df_items = make_mvtec_dataset(root=Path(data_path), split=Split.TRAIN) + else: + df_items = make_mvtec_dataset(root=Path(data_path), split=Split.TEST) + json_items = create_json_items(df_items) + save_json_items(json_items, f"{annotation_path}/{split}.json") + + +def create_mvtec_ad_category_annotations(data_path: str, annotation_path: str) -> None: + """Create MVTec AD category annotations for classification, detection and segmentation tasks. + + Args: + data_path (str): Path to MVTec AD category. + annotation_path (str): Path to save MVTec AD category JSON annotation items. + """ + for task in ["classification", "detection", "segmentation"]: + create_task_annotations(task, data_path, annotation_path) + + +def create_mvtec_ad_annotations(mvtec_data_path: str, mvtec_annotation_path: Optional[str] = None) -> None: + """Create JSON annotations for MVTec AD dataset. + + Args: + mvtec_data_path (str): Path to MVTec AD dataset. + mvtec_annotation_path (Optional[str], optional): Path to save JSON annotations. Defaults to None. + """ + if mvtec_annotation_path is None: + mvtec_annotation_path = mvtec_data_path + + categories = [ + "bottle", + "cable", + "capsule", + "carpet", + "grid", + "hazelnut", + "leather", + "metal_nut", + "pill", + "screw", + "tile", + "toothbrush", + "transistor", + "wood", + "zipper", + ] + + for category in categories: + print(f"Creating annotations for {category}") + category_data_path = os.path.join(mvtec_data_path, category) + category_annotation_path = os.path.join(mvtec_annotation_path, category) + create_mvtec_ad_category_annotations(category_data_path, category_annotation_path) + + +def get_args() -> Namespace: + """Get command line arguments. + + Returns: + Namespace: List of arguments. + """ + parser = ArgumentParser() + parser.add_argument("--data_path", type=str, default="./data/anomaly/MVTec/", help="Path to Mvtec AD dataset.") + parser.add_argument("--annotation_path", type=str, required=False, help="Path to create OTX CLI annotations.") + return parser.parse_args() + + +def main(): + """Create MVTec AD Annotations.""" + args = get_args() + create_mvtec_ad_annotations(mvtec_data_path=args.data_path, mvtec_annotation_path=args.annotation_path) + + +if __name__ == "__main__": + main() diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/data/data.py b/src/otx/algorithms/anomaly/adapters/anomalib/data/data.py new file mode 100644 index 00000000000..5a026e47e2d --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/data/data.py @@ -0,0 +1,277 @@ +"""Anomaly Dataset Utils.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from typing import Dict, List, Optional, Union + +import numpy as np +import torch +from anomalib.data.base.datamodule import collate_fn +from anomalib.data.utils.transform import get_transforms +from omegaconf import DictConfig, ListConfig +from pytorch_lightning.core.datamodule import LightningDataModule +from torch import Tensor +from torch.utils.data import DataLoader, Dataset + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.shapes.polygon import Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.utils.dataset_utils import ( + contains_anomalous_images, + split_local_global_dataset, +) +from otx.api.utils.segmentation_utils import mask_from_dataset_item +from otx.utils.logger import get_logger + +logger = get_logger() + + +class OTXAnomalyDataset(Dataset): + """Anomaly Dataset Adaptor. + + This class converts OTX Dataset into Anomalib dataset that + is a sub-class of Vision Dataset. + + Args: + config (Union[DictConfig, ListConfig]): Anomalib config + dataset (DatasetEntity): [description]: OTX SDK Dataset + + Example: + >>> from tests.helpers.dataset import OTXAnomalyDatasetGenerator + >>> from otx.utils.data import AnomalyDataset + + >>> dataset_generator = OTXAnomalyDatasetGenerator() + >>> dataset = dataset_generator.generate() + >>> anomaly_dataset = AnomalyDataset(config=config, dataset=dataset) + >>> anomaly_dataset[0]["image"].shape + torch.Size([3, 256, 256]) + """ + + def __init__(self, config: Union[DictConfig, ListConfig], dataset: DatasetEntity, task_type: TaskType): + self.config = config + self.dataset = dataset + self.task_type = task_type + + # TODO: distinguish between train and val config here + self.transform = get_transforms( + config=config.dataset.transform_config.train, image_size=tuple(config.dataset.image_size), to_tensor=True + ) + + def __len__(self) -> int: + """Get size of the dataset. + + Returns: + int: Size of the dataset. + """ + return len(self.dataset) + + def __getitem__(self, index: int) -> Dict[str, Union[int, Tensor]]: + """Get dataset item. + + Args: + index (int): Index of the dataset sample. + + Raises: + ValueError: When the task type is not supported. + + Returns: + Dict[str, Union[int, Tensor]]: Dataset item. + """ + dataset_item = self.dataset[index] + item: Dict[str, Union[int, Tensor]] = {} + item = {"index": index} + if self.task_type == TaskType.ANOMALY_CLASSIFICATION: + # Detection currently relies on image labels only, meaning it'll use image + # threshold to find the predicted bounding boxes. + item["image"] = self.transform(image=dataset_item.numpy)["image"] + elif self.task_type == TaskType.ANOMALY_DETECTION: + item["image"] = self.transform(image=dataset_item.numpy)["image"] + item["boxes"] = torch.empty((0, 4)) + height, width = self.config.dataset.image_size + boxes = [] + for annotation in dataset_item.get_annotations(): + if isinstance(annotation.shape, Rectangle) and not Rectangle.is_full_box(annotation.shape): + boxes.append( + Tensor( + [ + annotation.shape.x1 * width, + annotation.shape.y1 * height, + annotation.shape.x2 * width, + annotation.shape.y2 * height, + ] + ) + ) + if boxes: + item["boxes"] = torch.stack(boxes) + elif self.task_type == TaskType.ANOMALY_SEGMENTATION: + if any((isinstance(annotation.shape, Polygon) for annotation in dataset_item.get_annotations())): + mask = mask_from_dataset_item(dataset_item, dataset_item.get_shapes_labels()).squeeze() + else: + mask = np.zeros(dataset_item.numpy.shape[:2]).astype(np.int) + pre_processed = self.transform(image=dataset_item.numpy, mask=mask) + item["image"] = pre_processed["image"] + item["mask"] = pre_processed["mask"] + else: + raise ValueError(f"Unsupported task type: {self.task_type}") + + if len(dataset_item.get_shapes_labels()) > 0 and dataset_item.get_shapes_labels()[0].is_anomalous: + item["label"] = 1 + else: + item["label"] = 0 + return item + + +class OTXAnomalyDataModule(LightningDataModule): + """Anomaly DataModule. + + This class converts OTX Dataset into Anomalib dataset and stores + train/val/test dataloaders. + + Args: + config (Union[DictConfig, ListConfig]): Anomalib config + dataset (DatasetEntity): OTX SDK Dataset + + Example: + >>> from tests.helpers.dataset import OTXAnomalyDatasetGenerator + >>> from otx.utils.data import AnomalyDataModule + + >>> dataset_generator = OTXAnomalyDatasetGenerator() + >>> dataset = dataset_generator.generate() + >>> data_module = OTXAnomalyDataModule(config=config, dataset=dataset) + >>> i, data = next(enumerate(data_module.train_dataloader())) + >>> data["image"].shape + torch.Size([32, 3, 256, 256]) + """ + + def __init__(self, config: Union[DictConfig, ListConfig], dataset: DatasetEntity, task_type: TaskType) -> None: + super().__init__() + self.config = config + self.dataset = dataset + self.task_type = task_type + + self.train_otx_dataset: DatasetEntity + self.val_otx_dataset: DatasetEntity + self.test_otx_dataset: DatasetEntity + self.predict_otx_dataset: DatasetEntity + + def setup(self, stage: Optional[str] = None) -> None: + """Setup Anomaly Data Module. + + Args: + stage (Optional[str], optional): train/val/test stages. + Defaults to None. + """ + if not stage == "predict": + self.summary() + + if stage == "fit" or stage is None: + self.train_otx_dataset = self.dataset.get_subset(Subset.TRAINING) + self.val_otx_dataset = self.dataset.get_subset(Subset.VALIDATION) + + if stage == "validate": + self.val_otx_dataset = self.dataset.get_subset(Subset.VALIDATION) + + if stage == "test" or stage is None: + self.test_otx_dataset = self.dataset.get_subset(Subset.TESTING) + + if stage == "predict": + self.predict_otx_dataset = self.dataset + + def summary(self): + """Print size of the dataset, number of anomalous images and number of normal images.""" + for subset in [Subset.TRAINING, Subset.VALIDATION, Subset.TESTING]: + dataset = self.dataset.get_subset(subset) + num_items = len(dataset) + num_normal = len([item for item in dataset if not item.get_shapes_labels()[0].is_anomalous]) + num_anomalous = len([item for item in dataset if item.get_shapes_labels()[0].is_anomalous]) + logger.info( + "'%s' subset size: Total '%d' images. Normal: '%d', images. Anomalous: '%d' images", + subset, + num_items, + num_normal, + num_anomalous, + ) + + def train_dataloader( + self, + ) -> Union[DataLoader, List[DataLoader], Dict[str, DataLoader]]: + """Train Dataloader. + + Returns: + Union[DataLoader, List[DataLoader], Dict[str, DataLoader]]: Train dataloader. + """ + dataset = OTXAnomalyDataset(self.config, self.train_otx_dataset, self.task_type) + return DataLoader( + dataset, + shuffle=False, + batch_size=self.config.dataset.train_batch_size, + num_workers=self.config.dataset.num_workers, + collate_fn=collate_fn, + ) + + def val_dataloader(self) -> Union[DataLoader, List[DataLoader]]: + """Validation Dataloader. + + Returns: + Union[DataLoader, List[DataLoader]]: Validation Dataloader. + """ + global_dataset, local_dataset = split_local_global_dataset(self.val_otx_dataset) + logger.info(f"Global annotations: {len(global_dataset)}") + logger.info(f"Local annotations: {len(local_dataset)}") + if contains_anomalous_images(local_dataset): + logger.info("Dataset contains polygon annotations. Passing masks to anomalib.") + dataset = OTXAnomalyDataset(self.config, local_dataset, self.task_type) + else: + logger.info("Dataset does not contain polygon annotations. Not passing masks to anomalib.") + dataset = OTXAnomalyDataset(self.config, global_dataset, TaskType.ANOMALY_CLASSIFICATION) + return DataLoader( + dataset, + shuffle=False, + batch_size=self.config.dataset.eval_batch_size, + num_workers=self.config.dataset.num_workers, + collate_fn=collate_fn, + ) + + def test_dataloader(self) -> Union[DataLoader, List[DataLoader]]: + """Test Dataloader. + + Returns: + Union[DataLoader, List[DataLoader]]: Test Dataloader. + """ + dataset = OTXAnomalyDataset(self.config, self.test_otx_dataset, self.task_type) + return DataLoader( + dataset, + shuffle=False, + batch_size=self.config.dataset.test_batch_size, + num_workers=self.config.dataset.num_workers, + collate_fn=collate_fn, + ) + + def predict_dataloader(self) -> Union[DataLoader, List[DataLoader]]: + """Predict Dataloader. + + Returns: + Union[DataLoader, List[DataLoader]]: Predict Dataloader. + """ + dataset = OTXAnomalyDataset(self.config, self.predict_otx_dataset, self.task_type) + return DataLoader( + dataset, + shuffle=False, + batch_size=self.config.dataset.eval_batch_size, + num_workers=self.config.dataset.num_workers, + collate_fn=collate_fn, + ) diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/data/dataset.py b/src/otx/algorithms/anomaly/adapters/anomalib/data/dataset.py new file mode 100644 index 00000000000..3db5c341aff --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/data/dataset.py @@ -0,0 +1,323 @@ +"""DataLoaders for Anomaly Tasks.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import warnings +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Dict, List, Optional + +import pandas as pd +from bson import ObjectId + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset + + +class BaseAnomalyDataset(DatasetEntity, ABC): + """Base Dataloader for Anomaly Tasks.""" + + def __init__( + self, + train_subset: Optional[Dict[str, str]] = None, + val_subset: Optional[Dict[str, str]] = None, + test_subset: Optional[Dict[str, str]] = None, + ): + """Base Anomaly Dataset. + + Args: + train_subset (Optional[Dict[str, str]], optional): Path to annotation + and dataset used for training. Defaults to None. + val_subset (Optional[Dict[str, str]], optional): Path to annotation + and dataset used for validation. Defaults to None. + test_subset (Optional[Dict[str, str]], optional): Path to annotation + and dataset used for testing. Defaults to None. + """ + items: List[DatasetItemEntity] = [] + self.normal_label = LabelEntity(id=ID(0), name="Normal", domain=Domain.ANOMALY_CLASSIFICATION) + self.abnormal_label = LabelEntity( + id=ID(1), + name="Anomalous", + domain=Domain.ANOMALY_CLASSIFICATION, + is_anomalous=True, + ) + + if train_subset is not None: + train_ann_file = Path(train_subset["ann_file"]) + train_data_root = Path(train_subset["data_root"]) + items.extend( + self.get_dataset_items( + ann_file_path=train_ann_file, + data_root_dir=train_data_root, + subset=Subset.TRAINING, + ) + ) + + if val_subset is not None: + val_ann_file = Path(val_subset["ann_file"]) + val_data_root = Path(val_subset["data_root"]) + items.extend( + self.get_dataset_items( + ann_file_path=val_ann_file, + data_root_dir=val_data_root, + subset=Subset.VALIDATION, + ) + ) + + if test_subset is not None: + test_ann_file = Path(test_subset["ann_file"]) + test_data_root = Path(test_subset["data_root"]) + items.extend( + self.get_dataset_items( + ann_file_path=test_ann_file, + data_root_dir=test_data_root, + subset=Subset.TESTING, + ) + ) + + super().__init__(items=items) + + @abstractmethod + def get_dataset_items(self, ann_file_path: Path, data_root_dir: Path, subset: Subset) -> List[DatasetItemEntity]: + """To be implemented ib subclasses.""" + raise NotImplementedError + + +class AnomalyClassificationDataset(BaseAnomalyDataset): + """Dataloader for Anomaly Classification Task. + + Example: + >>> train_subset = { + "ann_file": "tests/assets/anomaly/classification/train.json", + "data_root": "tests/assets/anomaly/hazelnut", + } + >>> val_subset = { + "ann_file": "tests/assets/anomaly/classification/val.json", + "data_root": "tests/assets/anomaly/hazelnut" + } + >>> training_dataset = AnomalyClassificationDataset( + train_subset=train_subset, val_subset=val_subset + ) + >>> test_subset = { + "ann_file": "tests/assets/anomaly/classification/test.json", + "data_root": "tests/assets/anomaly/hazelnut" + } + >>> testing_dataset = AnomalyClassificationDataset(test_subset=test_subset) + """ + + def get_dataset_items(self, ann_file_path: Path, data_root_dir: Path, subset: Subset) -> List[DatasetItemEntity]: + """Loads dataset based on the image path in annotation file. + + Args: + ann_file_path (Path): Path to json containing the annotations. + For example of annotation look at `tests/assets/anomaly/[train, test,val].json. + data_root_dir (Path): Path to folder containing images. + subset (Subset): Subset of the dataset. + + Returns: + List[DatasetItemEntity]: List containing subset dataset. + """ + # read annotation file + samples = pd.read_json(ann_file_path) + + dataset_items = [] + for _, sample in samples.iterrows(): + # Create image + # convert path to str as PosixPath is not supported by Image + image = Image(file_path=str(data_root_dir / sample.image_path)) + # Create annotation + shape = Rectangle.generate_full_box() + label: LabelEntity = self.normal_label if sample.label == "good" else self.abnormal_label + labels = [ScoredLabel(label, probability=1.0)] + annotations = [Annotation(shape=shape, labels=labels)] + annotation_scene = AnnotationSceneEntity(annotations=annotations, kind=AnnotationSceneKind.ANNOTATION) + + # Create dataset item + dataset_item = DatasetItemEntity(media=image, annotation_scene=annotation_scene, subset=subset) + # Add to dataset items + dataset_items.append(dataset_item) + + return dataset_items + + +class AnomalySegmentationDataset(BaseAnomalyDataset): + """Dataloader for Anomaly Segmentation Task. + + Example: + >>> train_subset = { + "ann_file": "tests/assets/anomaly/segmentation/train.json", + "data_root": "tests/assets/anomaly/hazelnut", + } + >>> val_subset = { + "ann_file": "tests/assets/anomaly/segmentation/val.json", + "data_root": "tests/assets/anomaly/hazelnut" + } + >>> training_dataset = AnomalySegmentationDataset( + train_subset=train_subset, val_subset=val_subset + ) + >>> test_subset = { + "ann_file": "tests/assets/anomaly/segmentation/test.json", + "data_root": "tests/assets/anomaly/hazelnut" + } + >>> testing_dataset = AnomalySegmentationDataset(test_subset=test_subset) + + """ + + def get_dataset_items(self, ann_file_path: Path, data_root_dir: Path, subset: Subset) -> List[DatasetItemEntity]: + """Loads dataset based on the image path in annotation file. + + Args: + ann_file_path (Path): Path to json containing the annotations. + For example of annotation look at `tests/assets/anomaly/[train, test,val].json. + data_root_dir (Path): Path to folder containing images. + subset (Subset): Subset of the dataset. + + Returns: + List[DatasetItemEntity]: List containing subset dataset. + """ + # read annotation file + samples = pd.read_json(ann_file_path) + + dataset_items = [] + for _, sample in samples.iterrows(): + # Create image + # convert path to str as PosixPath is not supported by Image + image = Image(file_path=str(data_root_dir / sample.image_path)) + # Create annotation + label: LabelEntity = self.normal_label if sample.label == "good" else self.abnormal_label + annotations = [ + Annotation( + Rectangle.generate_full_box(), + labels=[ScoredLabel(label=label, probability=1.0)], + ) + ] + if isinstance(sample.masks, list) and len(sample.masks) > 0: + for contour in sample.masks: + points = [Point(x, y) for x, y in contour] + polygon = Polygon(points=points) + if polygon.get_area() > 0: + # Contour is a closed polygon with area > 0 + annotations.append( + Annotation( + shape=polygon, + labels=[ScoredLabel(label, 1.0)], + id=ID(ObjectId()), + ) + ) + else: + # Contour is a closed polygon with area == 0 + warnings.warn( + "The geometry of the segmentation map you are converting " + "is not fully supported. Polygons with a area of zero " + "will be removed.", + UserWarning, + ) + annotation_scene = AnnotationSceneEntity(annotations=annotations, kind=AnnotationSceneKind.ANNOTATION) + + # Add to dataset items + dataset_items.append(DatasetItemEntity(media=image, annotation_scene=annotation_scene, subset=subset)) + + return dataset_items + + +class AnomalyDetectionDataset(BaseAnomalyDataset): + """Dataloader for Anomaly Segmentation Task. + + Example: + >>> train_subset = { + "ann_file": "tests/assets/anomaly/detection/train.json", + "data_root": "tests/assets/anomaly/hazelnut", + } + >>> val_subset = { + "ann_file": "tests/assets/anomaly/detection/val.json", + "data_root": "tests/assets/anomaly/hazelnut" + } + >>> training_dataset = AnomalyDetectionDataset( + train_subset=train_subset, val_subset=val_subset + ) + >>> test_subset = { + "ann_file": "tests/assets/anomaly/detection/test.json", + "data_root": "tests/assets/anomaly/hazelnut" + } + >>> testing_dataset = AnomalyDetectionDataset(test_subset=test_subset) + + """ + + def get_dataset_items(self, ann_file_path: Path, data_root_dir: Path, subset: Subset) -> List[DatasetItemEntity]: + """Loads dataset based on the image path in annotation file. + + Args: + ann_file_path (Path): Path to json containing the annotations. + For example of annotation look at `tests/assets/anomaly/[train, test,val].json. + data_root_dir (Path): Path to folder containing images. + subset (Subset): Subset of the dataset. + + Returns: + List[DatasetItemEntity]: List containing subset dataset. + """ + # read annotation file + samples = pd.read_json(ann_file_path) + + dataset_items = [] + for _, sample in samples.iterrows(): + # Create image + # convert path to str as PosixPath is not supported by Image + image = Image(file_path=str(data_root_dir / sample.image_path)) + # Create annotation + label: LabelEntity = self.normal_label if sample.label == "good" else self.abnormal_label + annotations = [ + Annotation( + Rectangle.generate_full_box(), + labels=[ScoredLabel(label=label, probability=1.0)], + ) + ] + if isinstance(sample.bboxes, list) and len(sample.bboxes) > 0: + for bbox in sample.bboxes: + box = Rectangle(bbox[0], bbox[1], bbox[2], bbox[3]) + if box.get_area() > 0: + # Contour is a closed polygon with area > 0 + annotations.append( + Annotation( + shape=box, + labels=[ScoredLabel(label, 1.0)], + id=ID(ObjectId()), + ) + ) + else: + # Contour is a closed polygon with area == 0 + warnings.warn( + "The geometry of the segmentation map you are converting " + "is not fully supported. Polygons with a area of zero " + "will be removed.", + UserWarning, + ) + annotation_scene = AnnotationSceneEntity(annotations=annotations, kind=AnnotationSceneKind.ANNOTATION) + + # Add to dataset items + dataset_items.append(DatasetItemEntity(media=image, annotation_scene=annotation_scene, subset=subset)) + + return dataset_items diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/data/mvtec.py b/src/otx/algorithms/anomaly/adapters/anomalib/data/mvtec.py new file mode 100644 index 00000000000..18f792445a9 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/data/mvtec.py @@ -0,0 +1,164 @@ +"""OTX MVTec Dataset facilitate OTX Anomaly Training. + +License: + MVTec AD dataset is released under the Creative Commons + Attribution-NonCommercial-ShareAlike 4.0 International License + (CC BY-NC-SA 4.0)(https://creativecommons.org/licenses/by-nc-sa/4.0/). + +Reference: + - Paul Bergmann, Kilian Batzner, Michael Fauser, David Sattlegger, Carsten Steger: + The MVTec Anomaly Detection Dataset: A Comprehensive Real-World Dataset for + Unsupervised Anomaly Detection; in: International Journal of Computer Vision + 129(4):1038-1059, 2021, DOI: 10.1007/s11263-020-01400-4. + + - Paul Bergmann, Michael Fauser, David Sattlegger, Carsten Steger: MVTec AD — + A Comprehensive Real-World Dataset for Unsupervised Anomaly Detection; + in: IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), + 9584-9592, 2019, DOI: 10.1109/CVPR.2019.00982. +""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from pathlib import Path +from typing import List, Union + +import cv2 +import numpy as np +from anomalib.data.mvtec import make_mvtec_dataset +from pandas.core.frame import DataFrame + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.color import Color +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.utils.segmentation_utils import create_annotation_from_segmentation_map + + +class OtxMvtecDataset: + """Generate OTX MVTec Dataset from the anomaly detection datasets that follows the MVTec format. + + Args: + path (Union[str, Path], optional): Path to the MVTec dataset category. + Defaults to "./datasets/MVTec/bottle". + split_ratio (float, optional): Ratio to split normal training images and add to the + test set in case test set doesn't contain any normal images. + Defaults to 0.5. + seed (int, optional): Random seed to ensure reproducibility when splitting. Defaults to 0. + create_validation_set (bool, optional): Create validation set from the test set by splitting + it to half. Default to True. + + Examples: + >>> dataset_generator = OtxMvtecDataset() + >>> dataset = dataset_generator.generate() + >>> dataset[0].media.numpy.shape + (900, 900, 3) + """ + + # pylint: disable=too-many-instance-attributes + def __init__( + self, + path: Union[str, Path], + task_type: TaskType = TaskType.ANOMALY_CLASSIFICATION, + ): + self.path = path if isinstance(path, Path) else Path(path) + self.task_type = task_type + + if self.task_type == TaskType.ANOMALY_CLASSIFICATION: + self.label_domain = Domain.ANOMALY_CLASSIFICATION + elif self.task_type == TaskType.ANOMALY_SEGMENTATION: + self.label_domain = Domain.ANOMALY_SEGMENTATION + + self.normal_label = LabelEntity(name="Normal", domain=self.label_domain, id=ID(), color=Color(0, 255, 0)) + self.abnormal_label = LabelEntity( + name="Anomalous", + domain=self.label_domain, + id=ID(), + is_anomalous=True, + color=Color(255, 0, 0), + ) + self.label_map = {0: self.normal_label, 1: self.abnormal_label} + + def get_samples(self) -> DataFrame: + """Get MVTec samples. + + Get MVTec samples in a pandas DataFrame. Update the certain columns + to match the OTX naming terminology. For example, column `split` is + renamed to `subset`. Labels are also renamed by creating their + corresponding OTX LabelEntities + + Returns: + DataFrame: Final list of samples comprising all the required + information to create the OTX Dataset. + """ + samples = make_mvtec_dataset(root=self.path) + + # Set the OTX SDK Splits + samples = samples.rename(columns={"split": "subset"}) + samples.loc[samples.subset == "train", "subset"] = Subset.TRAINING + samples.loc[samples.subset == "val", "subset"] = Subset.VALIDATION + samples.loc[samples.subset == "test", "subset"] = Subset.TESTING + + # Create and Set the OTX Labels + samples.loc[samples.label != "good", "label"] = self.abnormal_label + samples.loc[samples.label == "good", "label"] = self.normal_label + + samples = samples.reset_index(drop=True) + + return samples + + def generate(self) -> DatasetEntity: + """Generate OTX Anomaly Dataset. + + Returns: + DatasetEntity: Output OTX Anomaly Dataset from an MVTec + """ + samples = self.get_samples() + dataset_items: List[DatasetItemEntity] = [] + for _, sample in samples.iterrows(): + # Create image + image = Image(file_path=sample.image_path) + + # Create annotation + if self.task_type == TaskType.ANOMALY_CLASSIFICATION or sample.label == self.normal_label: + shape = Rectangle(x1=0, y1=0, x2=1, y2=1) + labels = [ScoredLabel(sample.label)] + annotations = [Annotation(shape=shape, labels=labels)] + annotation_scene = AnnotationSceneEntity(annotations=annotations, kind=AnnotationSceneKind.ANNOTATION) + elif self.task_type == TaskType.ANOMALY_SEGMENTATION and sample.label == self.abnormal_label: + mask = (cv2.imread(sample.mask_path, cv2.IMREAD_GRAYSCALE) / 255).astype(np.uint8) + annotations = create_annotation_from_segmentation_map(mask, np.ones_like(mask), self.label_map) + annotation_scene = AnnotationSceneEntity(annotations=annotations, kind=AnnotationSceneKind.ANNOTATION) + else: + raise ValueError(f"Unknown task type: {self.task_type}") + + # Create dataset item + dataset_item = DatasetItemEntity(media=image, annotation_scene=annotation_scene, subset=sample.subset) + + # Add to dataset items + dataset_items.append(dataset_item) + + dataset = DatasetEntity(items=dataset_items) + return dataset diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/plugins/__init__.py b/src/otx/algorithms/anomaly/adapters/anomalib/plugins/__init__.py new file mode 100644 index 00000000000..df24d838d85 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/plugins/__init__.py @@ -0,0 +1,7 @@ +"""Plugin for mixed-precision training on XPU.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .xpu_precision import MixedPrecisionXPUPlugin + +__all__ = ["MixedPrecisionXPUPlugin"] diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/plugins/xpu_precision.py b/src/otx/algorithms/anomaly/adapters/anomalib/plugins/xpu_precision.py new file mode 100644 index 00000000000..bfd9f5d3b93 --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/plugins/xpu_precision.py @@ -0,0 +1,109 @@ +"""Plugin for mixed-precision training on XPU.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from contextlib import contextmanager +from typing import Any, Callable, Dict, Generator, Optional, Union + +import pytorch_lightning as pl +import torch +from lightning_fabric.utilities.types import Optimizable +from pytorch_lightning.plugins.precision.precision_plugin import PrecisionPlugin +from pytorch_lightning.utilities import GradClipAlgorithmType +from pytorch_lightning.utilities.exceptions import MisconfigurationException +from torch import Tensor +from torch.optim import LBFGS, Optimizer + + +class MixedPrecisionXPUPlugin(PrecisionPlugin): + """Plugin for Automatic Mixed Precision (AMP) training with ``torch.xpu.autocast``. + + Args: + scaler: An optional :class:`torch.cuda.amp.GradScaler` to use. + """ + + def __init__(self, scaler: Optional[Any] = None) -> None: + self.scaler = scaler + + def pre_backward(self, tensor: Tensor, module: "pl.LightningModule") -> Tensor: + """Apply grad scaler before backward.""" + if self.scaler is not None: + tensor = self.scaler.scale(tensor) + return super().pre_backward(tensor, module) + + def optimizer_step( # type: ignore[override] + self, + optimizer: Optimizable, + model: "pl.LightningModule", + optimizer_idx: int, + closure: Callable[[], Any], + **kwargs: Any, + ) -> Any: + """Make an optimizer step using scaler if it was passed.""" + if self.scaler is None: + # skip scaler logic, as bfloat16 does not require scaler + return super().optimizer_step( + optimizer, model=model, optimizer_idx=optimizer_idx, closure=closure, **kwargs + ) + if isinstance(optimizer, LBFGS): + raise MisconfigurationException( + f"Native AMP and the LBFGS optimizer are not compatible (optimizer {optimizer_idx})." + ) + closure_result = closure() + + if not _optimizer_handles_unscaling(optimizer): + # Unscaling needs to be performed here in case we are going to apply gradient clipping. + # Optimizers that perform unscaling in their `.step()` method are not supported (e.g., fused Adam). + # Note: `unscale` happens after the closure is executed, but before the `on_before_optimizer_step` hook. + self.scaler.unscale_(optimizer) + + self._after_closure(model, optimizer, optimizer_idx) + skipped_backward = closure_result is None + # in manual optimization, the closure does not return a value + if not model.automatic_optimization or not skipped_backward: + # note: the scaler will skip the `optimizer.step` if nonfinite gradients are found + step_output = self.scaler.step(optimizer, **kwargs) + self.scaler.update() + return step_output + return closure_result + + def clip_gradients( + self, + optimizer: Optimizer, + clip_val: Union[int, float] = 0.0, + gradient_clip_algorithm: GradClipAlgorithmType = GradClipAlgorithmType.NORM, + ) -> None: + """Handle grad clipping with scaler.""" + if clip_val > 0 and _optimizer_handles_unscaling(optimizer): + raise RuntimeError( + f"The current optimizer, {type(optimizer).__qualname__}, does not allow for gradient clipping" + " because it performs unscaling of gradients internally. HINT: Are you using a 'fused' optimizer?" + ) + super().clip_gradients(optimizer=optimizer, clip_val=clip_val, gradient_clip_algorithm=gradient_clip_algorithm) + + @contextmanager + def forward_context(self) -> Generator[None, None, None]: + """Enable autocast context.""" + with torch.xpu.autocast(True): + yield + + def state_dict(self) -> Dict[str, Any]: + """Returns state dict of the plugin.""" + if self.scaler is not None: + return self.scaler.state_dict() + return {} + + def load_state_dict(self, state_dict: Dict[str, Any]) -> None: + """Loads state dict to the plugin.""" + if self.scaler is not None: + self.scaler.load_state_dict(state_dict) + + +def _optimizer_handles_unscaling(optimizer: Any) -> bool: + """Determines if a PyTorch optimizer handles unscaling gradients in the step method ratherthan through the scaler. + + Since, the current implementation of this function checks a PyTorch internal variable on the optimizer, the return + value will only be reliable for built-in PyTorch optimizers. + """ + return getattr(optimizer, "_step_supports_amp_scaling", False) diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/strategies/__init__.py b/src/otx/algorithms/anomaly/adapters/anomalib/strategies/__init__.py new file mode 100644 index 00000000000..ff3508b3f1c --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/strategies/__init__.py @@ -0,0 +1,8 @@ +"""Lightning strategy for single XPU device.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .xpu_single import SingleXPUStrategy + +__all__ = ["SingleXPUStrategy"] diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/strategies/xpu_single.py b/src/otx/algorithms/anomaly/adapters/anomalib/strategies/xpu_single.py new file mode 100644 index 00000000000..014132a840e --- /dev/null +++ b/src/otx/algorithms/anomaly/adapters/anomalib/strategies/xpu_single.py @@ -0,0 +1,60 @@ +"""Lightning strategy for single XPU device.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Optional + +import pytorch_lightning as pl +import torch +from lightning_fabric.plugins import CheckpointIO +from lightning_fabric.utilities.types import _DEVICE +from pytorch_lightning.plugins.precision import PrecisionPlugin +from pytorch_lightning.strategies import StrategyRegistry +from pytorch_lightning.strategies.single_device import SingleDeviceStrategy +from pytorch_lightning.utilities.exceptions import MisconfigurationException + +from otx.algorithms.common.utils.utils import is_xpu_available + + +class SingleXPUStrategy(SingleDeviceStrategy): + """Strategy for training on single XPU device.""" + + strategy_name = "xpu_single" + + def __init__( + self, + device: _DEVICE = "xpu:0", + accelerator: Optional["pl.accelerators.Accelerator"] = None, + checkpoint_io: Optional[CheckpointIO] = None, + precision_plugin: Optional[PrecisionPlugin] = None, + ): + if not is_xpu_available(): + raise MisconfigurationException("`SingleXPUStrategy` requires XPU devices to run") + + super().__init__( + accelerator=accelerator, + device=device, + checkpoint_io=checkpoint_io, + precision_plugin=precision_plugin, + ) + + @property + def is_distributed(self) -> bool: + """Returns true if the strategy supports distributed training.""" + return False + + def setup_optimizers(self, trainer: "pl.Trainer") -> None: + """Sets up optimizers.""" + super().setup_optimizers(trainer) + if len(self.optimizers) != 1: # type: ignore + raise RuntimeError("XPU strategy doesn't support multiple optimizers") + model, optimizer = torch.xpu.optimize(trainer.model, optimizer=self.optimizers[0]) # type: ignore + self.optimizers = [optimizer] + trainer.model = model + + +StrategyRegistry.register( + SingleXPUStrategy.strategy_name, SingleXPUStrategy, description="Strategy that enables training on single XPU" +) diff --git a/src/otx/algorithms/anomaly/configs/__init__.py b/src/otx/algorithms/anomaly/configs/__init__.py new file mode 100644 index 00000000000..994ac00bb8b --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/__init__.py @@ -0,0 +1,15 @@ +"""Model configurations.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/anomaly/configs/base/__init__.py b/src/otx/algorithms/anomaly/configs/base/__init__.py new file mode 100644 index 00000000000..3ff6c75946b --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/base/__init__.py @@ -0,0 +1,21 @@ +"""Base configurable parameter for anomaly tasks.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .draem import DraemAnomalyBaseConfig +from .padim import PadimAnomalyBaseConfig +from .stfpm import STFPMAnomalyBaseConfig + +__all__ = ["PadimAnomalyBaseConfig", "STFPMAnomalyBaseConfig", "DraemAnomalyBaseConfig"] diff --git a/src/otx/algorithms/anomaly/configs/base/configuration.py b/src/otx/algorithms/anomaly/configs/base/configuration.py new file mode 100644 index 00000000000..197d18260d1 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/base/configuration.py @@ -0,0 +1,130 @@ +"""Configurable parameters for anomaly classification task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base.configuration_enums import ( + POTQuantizationPreset, +) +from otx.api.configuration import ConfigurableParameters +from otx.api.configuration.elements import ( + ParameterGroup, + add_parameter_group, + boolean_attribute, + configurable_boolean, + configurable_integer, + selectable, + string_attribute, +) +from otx.api.configuration.model_lifecycle import ModelLifecycle + + +@attrs +class BaseAnomalyConfig(ConfigurableParameters): + """Base OTX configurable parameters for anomaly classification task.""" + + header = string_attribute("Configuration for an anomaly classification task") + description = header + + @attrs + class LearningParameters(ParameterGroup): + """Parameters that can be tuned using HPO.""" + + header = string_attribute("Learning Parameters") + description = header + + train_batch_size = configurable_integer( + default_value=32, + min_value=1, + max_value=512, + header="Batch size", + description="The number of training samples seen in each iteration of training. Increasing this value " + "improves training time and may make the training more stable. A larger batch size has higher " + "memory requirements.", + warning="Increasing this value may cause the system to use more memory than available, " + "potentially causing out of memory errors, please update with caution.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + @attrs + class DatasetParameters(ParameterGroup): + """Parameters related to dataloader.""" + + header = string_attribute("Dataset Parameters") + description = header + + num_workers = configurable_integer( + default_value=8, + min_value=0, + max_value=36, + header="Number of workers", + description="Increasing this value might improve training speed however it might cause out of memory " + "errors. If the number of workers is set to zero, data loading will happen in the main " + "training thread.", + ) + + @attrs + class POTParameters(ParameterGroup): + """Training parameters for post-training optimization.""" + + header = string_attribute("POT Parameters") + description = header + visible_in_ui = boolean_attribute(False) + + preset = selectable( + default_value=POTQuantizationPreset.PERFORMANCE, + header="Preset", + description="Quantization preset that defines quantization scheme", + ) + + stat_subset_size = configurable_integer( + header="Number of data samples", + description="Number of data samples used for post-training optimization", + default_value=300, + min_value=1, + max_value=1000, + ) + + @attrs + class NNCFOptimization(ParameterGroup): + """Parameters for NNCF optimization.""" + + header = string_attribute("Optimization by NNCF") + description = header + + enable_quantization = configurable_boolean( + default_value=True, + header="Enable quantization algorithm", + description="Enable quantization algorithm", + ) + + enable_pruning = configurable_boolean( + default_value=False, + header="Enable filter pruning algorithm", + description="Enable filter pruning algorithm", + ) + + pruning_supported = configurable_boolean( + default_value=False, + header="Whether filter pruning is supported", + description="Whether filter pruning is supported", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + learning_parameters = add_parameter_group(LearningParameters) + dataset = add_parameter_group(DatasetParameters) + pot_parameters = add_parameter_group(POTParameters) + nncf_optimization = add_parameter_group(NNCFOptimization) diff --git a/src/otx/algorithms/anomaly/configs/base/configuration_enums.py b/src/otx/algorithms/anomaly/configs/base/configuration_enums.py new file mode 100644 index 00000000000..4988c475a70 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/base/configuration_enums.py @@ -0,0 +1,64 @@ +"""Configuration Enums. + +Enums needed to define the options of selectable parameters in the configurable +parameter classes. +""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from otx.api.configuration import ConfigurableEnum + + +class POTQuantizationPreset(ConfigurableEnum): + """POT Quantization Preset Enum. + + This Enum represents the quantization preset for post training optimization. + """ + + PERFORMANCE = "Performance" + MIXED = "Mixed" + + +class EarlyStoppingMetrics(ConfigurableEnum): + """Early Stopping Metric Enum. + + This enum represents the different metrics that can be used for early + stopping. + """ + + IMAGE_ROC_AUC = "image_AUROC" + IMAGE_F1 = "image_F1Score" + + +class ModelName(ConfigurableEnum): + """Model Name Enum. + + This enum represents the different model architectures for anomaly + classification. + """ + + STFPM = "stfpm" + PADIM = "padim" + + +class ModelBackbone(ConfigurableEnum): + """Model Backbone Enum. + + This enum represents the common backbones that can be used with Padim and + STFPM. + """ + + RESNET18 = "resnet18" + WIDE_RESNET_50 = "wide_resnet50_2" diff --git a/src/otx/algorithms/anomaly/configs/base/draem/__init__.py b/src/otx/algorithms/anomaly/configs/base/draem/__init__.py new file mode 100644 index 00000000000..e53ec419601 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/base/draem/__init__.py @@ -0,0 +1,9 @@ +"""Base configuration parameters for Draem.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from .configuration import DraemAnomalyBaseConfig + +__all__ = ["DraemAnomalyBaseConfig"] diff --git a/src/otx/algorithms/anomaly/configs/base/draem/configuration.py b/src/otx/algorithms/anomaly/configs/base/draem/configuration.py new file mode 100644 index 00000000000..e2ad079ca6a --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/base/draem/configuration.py @@ -0,0 +1,96 @@ +"""Configurable parameters for Draem anomaly task.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from attr import attrs + +from otx.algorithms.anomaly.configs.base.configuration import BaseAnomalyConfig +from otx.algorithms.anomaly.configs.base.configuration_enums import EarlyStoppingMetrics +from otx.api.configuration.elements import ( + ParameterGroup, + add_parameter_group, + configurable_float, + configurable_integer, + selectable, + string_attribute, +) +from otx.api.configuration.model_lifecycle import ModelLifecycle + + +@attrs +class DraemAnomalyBaseConfig(BaseAnomalyConfig): + """Configurable parameters for DRAEM anomaly classification task.""" + + header = string_attribute("Configuration for Draem") + description = header + + @attrs + class LearningParameters(BaseAnomalyConfig.LearningParameters): + """Parameters that can be tuned using HPO.""" + + header = string_attribute("Learning Parameters") + description = header + + train_batch_size = configurable_integer( + default_value=8, + min_value=1, + max_value=512, + header="Batch size", + description="The number of training samples seen in each iteration of training. Increasing this value " + "improves training time and may make the training more stable. A larger batch size has higher " + "memory requirements.", + warning="Increasing this value may cause the system to use more memory than available, " + "potentially causing out of memory errors, please update with caution.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + lr = configurable_float( + default_value=0.0001, + header="Learning Rate", + min_value=1e-4, + max_value=1, + description="Learning rate used for optimizing the network.", + ) + + @attrs + class EarlyStoppingParameters(ParameterGroup): + """Early stopping parameters.""" + + header = string_attribute("Early Stopping Parameters") + description = header + + metric = selectable( + default_value=EarlyStoppingMetrics.IMAGE_ROC_AUC, + header="Early Stopping Metric", + description="The metric used to determine if the model should stop training", + ) + + patience = configurable_integer( + default_value=20, + min_value=1, + max_value=100, + header="Early Stopping Patience", + description="Number of epochs to wait for an improvement in the monitored metric. If the metric has " + "not improved for this many epochs, the training will stop and the best model will be " + "returned.", + warning="Setting this value too low might lead to underfitting. Setting the value too high will " + "increase the training time and might lead to overfitting.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + early_stopping = add_parameter_group(EarlyStoppingParameters) + + max_epochs = configurable_integer( + default_value=700, + header="Max Epochs", + min_value=1, + max_value=700, + description="Maximum number of epochs to train the model for.", + warning="Training for very few epochs might lead to poor performance. If Early Stopping is enabled then " + "increasing the value of max epochs might not lead to desired result.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + learning_parameters = add_parameter_group(LearningParameters) diff --git a/src/otx/algorithms/anomaly/configs/base/padim/__init__.py b/src/otx/algorithms/anomaly/configs/base/padim/__init__.py new file mode 100644 index 00000000000..9091f9b00e5 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/base/padim/__init__.py @@ -0,0 +1,19 @@ +"""Base configuration parameters for Padim.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import PadimAnomalyBaseConfig + +__all__ = ["PadimAnomalyBaseConfig"] diff --git a/src/otx/algorithms/anomaly/configs/base/padim/configuration.py b/src/otx/algorithms/anomaly/configs/base/padim/configuration.py new file mode 100644 index 00000000000..69705f3e8af --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/base/padim/configuration.py @@ -0,0 +1,52 @@ +"""Configurable parameters for Padim anomaly task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base.configuration import BaseAnomalyConfig +from otx.algorithms.anomaly.configs.base.configuration_enums import ModelBackbone +from otx.api.configuration.elements import ( + add_parameter_group, + selectable, + string_attribute, +) + + +@attrs +class PadimAnomalyBaseConfig(BaseAnomalyConfig): + """Configurable parameters for PADIM anomaly classification task.""" + + header = string_attribute("Configuration for Padim") + description = header + + @attrs + class LearningParameters(BaseAnomalyConfig.LearningParameters): + """Parameters that can be tuned using HPO.""" + + header = string_attribute("Learning Parameters") + description = header + + # Editable is set to false as WideResNet50 is very large for + # onnx's protobuf (2gb) limit. This ends up crashing the export. + backbone = selectable( + default_value=ModelBackbone.RESNET18, + header="Model Backbone", + description="Pre-trained backbone used for feature extraction", + editable=False, + visible_in_ui=False, + ) + + learning_parameters = add_parameter_group(LearningParameters) diff --git a/src/otx/algorithms/anomaly/configs/base/stfpm/__init__.py b/src/otx/algorithms/anomaly/configs/base/stfpm/__init__.py new file mode 100644 index 00000000000..1366d84f447 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/base/stfpm/__init__.py @@ -0,0 +1,19 @@ +"""Base configuration parameters for STFPM.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import STFPMAnomalyBaseConfig + +__all__ = ["STFPMAnomalyBaseConfig"] diff --git a/src/otx/algorithms/anomaly/configs/base/stfpm/configuration.py b/src/otx/algorithms/anomaly/configs/base/stfpm/configuration.py new file mode 100644 index 00000000000..17d186af36a --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/base/stfpm/configuration.py @@ -0,0 +1,115 @@ +"""Configurable parameters for STFPM anomaly base task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base.configuration import BaseAnomalyConfig +from otx.algorithms.anomaly.configs.base.configuration_enums import ( + EarlyStoppingMetrics, + ModelBackbone, +) +from otx.api.configuration.elements import ( + ParameterGroup, + add_parameter_group, + configurable_float, + configurable_integer, + selectable, + string_attribute, +) +from otx.api.configuration.model_lifecycle import ModelLifecycle + + +@attrs +class STFPMAnomalyBaseConfig(BaseAnomalyConfig): + """Configurable parameters for STFPM anomaly base task.""" + + header = string_attribute("Configuration for STFPM") + description = header + + @attrs + class LearningParameters(BaseAnomalyConfig.LearningParameters): + """Parameters that can be tuned using HPO.""" + + lr = configurable_float( + default_value=0.4, + header="Learning Rate", + min_value=1e-3, + max_value=1, + description="Learning rate used for optimizing the Student network.", + ) + + momentum = configurable_float( + default_value=0.9, + header="Momentum", + min_value=0.1, + max_value=1.0, + description="Momentum used for SGD optimizer", + ) + + weight_decay = configurable_float( + default_value=0.0001, + header="Weight Decay", + min_value=1e-5, + max_value=1, + description="Decay for SGD optimizer", + ) + + backbone = selectable( + default_value=ModelBackbone.RESNET18, + header="Model Backbone", + description="Pre-trained backbone used for feature extraction", + ) + + @attrs + class EarlyStoppingParameters(ParameterGroup): + """Early stopping parameters.""" + + header = string_attribute("Early Stopping Parameters") + description = header + + metric = selectable( + default_value=EarlyStoppingMetrics.IMAGE_F1, + header="Early Stopping Metric", + description="The metric used to determine if the model should stop training", + ) + + patience = configurable_integer( + default_value=10, + min_value=1, + max_value=100, + header="Early Stopping Patience", + description="Number of epochs to wait for an improvement in the monitored metric. If the metric has " + "not improved for this many epochs, the training will stop and the best model will be " + "returned.", + warning="Setting this value too low might lead to underfitting. Setting the value too high will " + "increase the training time and might lead to overfitting.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + early_stopping = add_parameter_group(EarlyStoppingParameters) + + max_epochs = configurable_integer( + default_value=100, + header="Max Epochs", + min_value=1, + max_value=500, + description="Maximum number of epochs to train the model for.", + warning="Training for very few epochs might lead to poor performance. If Early Stopping is enabled then " + "increasing the value of max epochs might not lead to desired result.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + learning_parameters = add_parameter_group(LearningParameters) diff --git a/src/otx/algorithms/anomaly/configs/classification/__init__.py b/src/otx/algorithms/anomaly/configs/classification/__init__.py new file mode 100644 index 00000000000..b24bd9d1806 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/__init__.py @@ -0,0 +1,15 @@ +"""Configuration for classification tasks.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/anomaly/configs/classification/draem/__init__.py b/src/otx/algorithms/anomaly/configs/classification/draem/__init__.py new file mode 100644 index 00000000000..c76cd3508ff --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/draem/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Configurable Parameters for DRAEM Anomaly Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import DraemAnomalyClassificationConfig + +__all__ = ["DraemAnomalyClassificationConfig"] diff --git a/src/otx/algorithms/anomaly/configs/classification/draem/compression_config.json b/src/otx/algorithms/anomaly/configs/classification/draem/compression_config.json new file mode 100644 index 00000000000..0b7922f5a23 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/draem/compression_config.json @@ -0,0 +1,36 @@ +{ + "base": { + "find_unused_parameters": true, + "target_metric_name": "image_F1Score", + "nncf_config": { + "input_info": { + "sample_size": [1, 3, 256, 256] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "model": { + "lr": 0.004 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 250 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 250 + } + }, + "ignored_scopes": [] + } + ] + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/anomaly/configs/classification/draem/configuration.py b/src/otx/algorithms/anomaly/configs/classification/draem/configuration.py new file mode 100644 index 00000000000..b5b5d45712f --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/draem/configuration.py @@ -0,0 +1,24 @@ +"""Configurable parameters for DRAEM anomaly classification task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base import DraemAnomalyBaseConfig + + +@attrs +class DraemAnomalyClassificationConfig(DraemAnomalyBaseConfig): + """Configurable parameters for DRAEM anomaly classification task.""" diff --git a/src/otx/algorithms/anomaly/configs/classification/draem/configuration.yaml b/src/otx/algorithms/anomaly/configs/classification/draem/configuration.yaml new file mode 100644 index 00000000000..ee410672c48 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/draem/configuration.yaml @@ -0,0 +1,242 @@ +dataset: + description: Dataset Parameters + header: Dataset Parameters + num_workers: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of workers + max_value: 36 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +description: Configuration for Draem +header: Configuration for Draem +learning_parameters: + description: Learning Parameters + early_stopping: + description: Early Stopping Parameters + header: Early Stopping Parameters + metric: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: image_AUROC + description: The metric used to determine if the model should stop training + editable: true + enum_name: EarlyStoppingMetrics + header: Early Stopping Metric + options: + IMAGE_F1: image_F1Score + IMAGE_ROC_AUC: image_AUROC + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + patience: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 20 + description: + Number of epochs to wait for an improvement in the monitored metric. + If the metric has not improved for this many epochs, the training will stop + and the best model will be returned. + editable: true + header: Early Stopping Patience + max_value: 100 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Setting this value too low might lead to underfitting. Setting the + value too high will increase the training time and might lead to overfitting. + type: PARAMETER_GROUP + visible_in_ui: true + header: Learning Parameters + lr: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.0001 + description: Learning rate used for optimizing the network. + editable: true + header: Learning Rate + max_value: 1 + min_value: 0.0001 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + max_epochs: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 700 + description: Maximum number of epochs to train the model for. + editable: true + header: Max Epochs + max_value: 700 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Training for very few epochs might lead to poor performance. If Early + Stopping is enabled then increasing the value of max epochs might not lead to + desired result. + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + enable_pruning: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + enable_quantization: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: true + description: Enable quantization algorithm + editable: true + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + header: Optimization by NNCF + pruning_supported: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/anomaly/configs/classification/draem/template_experimental.yaml b/src/otx/algorithms/anomaly/configs/classification/draem/template_experimental.yaml new file mode 100644 index 00000000000..11a3d9c5e18 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/draem/template_experimental.yaml @@ -0,0 +1,31 @@ +# Description. +model_template_id: ote_anomaly_classification_draem +name: DRAEM +task_type: ANOMALY_CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: Most accurate model across datasets, but longer training time. +application: ~ + +# Algo backend. +framework: OTXAnomalyClassification v0.1.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.anomaly.tasks.TrainingTask + openvino: otx.algorithms.anomaly.tasks.OpenVINOTask + nncf: otx.algorithms.anomaly.tasks.NNCFTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 3.9 +size: 168.4 diff --git a/src/otx/algorithms/anomaly/configs/classification/draem/transform_config.yaml b/src/otx/algorithms/anomaly/configs/classification/draem/transform_config.yaml new file mode 100644 index 00000000000..5a379ef7628 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/draem/transform_config.yaml @@ -0,0 +1,26 @@ +{ + "__version__": "1.1.0", + "transform": + { + "__class_fullname__": "Compose", + "p": 1.0, + "transforms": + [ + { + "__class_fullname__": "ToFloat", + "always_apply": false, + "p": 1.0, + "max_value": null, + }, + { + "__class_fullname__": "ToTensorV2", + "always_apply": true, + "p": 1.0, + "transpose_mask": false, + }, + ], + "bbox_params": null, + "keypoint_params": null, + "additional_targets": {}, + }, +} diff --git a/src/otx/algorithms/anomaly/configs/classification/padim/__init__.py b/src/otx/algorithms/anomaly/configs/classification/padim/__init__.py new file mode 100644 index 00000000000..c0fb0ddce1d --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/padim/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Configurable Parameters for PADIM Anomaly Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import PadimAnomalyClassificationConfig + +__all__ = ["PadimAnomalyClassificationConfig"] diff --git a/src/otx/algorithms/anomaly/configs/classification/padim/compression_config.json b/src/otx/algorithms/anomaly/configs/classification/padim/compression_config.json new file mode 100644 index 00000000000..4235946a7d7 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/padim/compression_config.json @@ -0,0 +1,38 @@ +{ + "base": { + "find_unused_parameters": true, + "target_metric_name": "image_F1Score", + "nncf_config": { + "input_info": { + "sample_size": [1, 3, 256, 256] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 250 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 250 + } + }, + "ignored_scopes": [ + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0", + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0", + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/matmul_0", + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0" + ] + } + ] + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/anomaly/configs/classification/padim/configuration.py b/src/otx/algorithms/anomaly/configs/classification/padim/configuration.py new file mode 100644 index 00000000000..05d84c96601 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/padim/configuration.py @@ -0,0 +1,24 @@ +"""Configurable parameters for Padim anomaly classification task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base import PadimAnomalyBaseConfig + + +@attrs +class PadimAnomalyClassificationConfig(PadimAnomalyBaseConfig): + """Configurable parameters for PADIM anomaly classification task.""" diff --git a/src/otx/algorithms/anomaly/configs/classification/padim/configuration.yaml b/src/otx/algorithms/anomaly/configs/classification/padim/configuration.yaml new file mode 100644 index 00000000000..eac893d019c --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/padim/configuration.yaml @@ -0,0 +1,183 @@ +dataset: + description: Dataset Parameters + header: Dataset Parameters + num_workers: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of workers + max_value: 36 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 8 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +description: Configuration for Padim +header: Configuration for Padim +id: "" +learning_parameters: + backbone: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: resnet18 + description: Pre-trained backbone used for feature extraction + editable: false + enum_name: ModelBackbone + header: Model Backbone + options: + RESNET18: resnet18 + WIDE_RESNET_50: wide_resnet50_2 + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: resnet18 + visible_in_ui: false + warning: null + description: Learning Parameters + header: Learning Parameters + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 32 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 32 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + enable_pruning: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + enable_quantization: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: true + description: Enable quantization algorithm + editable: true + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + header: Optimization by NNCF + pruning_supported: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/anomaly/configs/classification/padim/ptq_optimization_config.py b/src/otx/algorithms/anomaly/configs/classification/padim/ptq_optimization_config.py new file mode 100644 index 00000000000..635b4a40c1c --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/padim/ptq_optimization_config.py @@ -0,0 +1,25 @@ +"""PTQ config file.""" +from nncf import IgnoredScope +from nncf.common.quantization.structs import QuantizationPreset +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ) +) + +preset = QuantizationPreset.MIXED + +ignored_scope = IgnoredScope(names=["/anomaly_map_generator/Mul", "/anomaly_map_generator/Sqrt"]) diff --git a/src/otx/algorithms/anomaly/configs/classification/padim/template.yaml b/src/otx/algorithms/anomaly/configs/classification/padim/template.yaml new file mode 100644 index 00000000000..21c557530a9 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/padim/template.yaml @@ -0,0 +1,35 @@ +# Description. +model_template_id: ote_anomaly_classification_padim +name: PADIM +task_type: ANOMALY_CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: This model is faster and in many cases more accurate, but it requires a fixed position of the objects within the image. +application: ~ + +# Algo backend. +framework: OTXAnomalyClassification v0.1.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.anomaly.tasks.TrainingTask + openvino: otx.algorithms.anomaly.tasks.OpenVINOTask + nncf: otx.algorithms.anomaly.tasks.NNCFTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 3.9 +size: 168.4 + +# Model spec +model_category: SPEED +is_default_for_task: true diff --git a/src/otx/algorithms/anomaly/configs/classification/stfpm/__init__.py b/src/otx/algorithms/anomaly/configs/classification/stfpm/__init__.py new file mode 100644 index 00000000000..adb65f754a6 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/stfpm/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Configurable Parameters for STFPM Anomaly Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import STFPMAnomalyClassificationConfig + +__all__ = ["STFPMAnomalyClassificationConfig"] diff --git a/src/otx/algorithms/anomaly/configs/classification/stfpm/compression_config.json b/src/otx/algorithms/anomaly/configs/classification/stfpm/compression_config.json new file mode 100644 index 00000000000..caee63d064b --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/stfpm/compression_config.json @@ -0,0 +1,36 @@ +{ + "base": { + "find_unused_parameters": true, + "target_metric_name": "image_F1Score", + "nncf_config": { + "input_info": { + "sample_size": [1, 3, 256, 256] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "model": { + "lr": 0.004 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 250 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 250 + } + }, + "ignored_scopes": ["{re}.*__pow__.*"] + } + ] + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/anomaly/configs/classification/stfpm/configuration.py b/src/otx/algorithms/anomaly/configs/classification/stfpm/configuration.py new file mode 100644 index 00000000000..5021a390349 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/stfpm/configuration.py @@ -0,0 +1,24 @@ +"""Configurable parameters for STFPM anomaly classification task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base import STFPMAnomalyBaseConfig + + +@attrs +class STFPMAnomalyClassificationConfig(STFPMAnomalyBaseConfig): + """Configurable parameters for STFPM anomaly classification task.""" diff --git a/src/otx/algorithms/anomaly/configs/classification/stfpm/configuration.yaml b/src/otx/algorithms/anomaly/configs/classification/stfpm/configuration.yaml new file mode 100644 index 00000000000..ff3e8ca1517 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/stfpm/configuration.yaml @@ -0,0 +1,312 @@ +dataset: + description: Dataset Parameters + header: Dataset Parameters + num_workers: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of workers + max_value: 36 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 8 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +description: Configuration for STFPM +header: Configuration for STFPM +id: "" +learning_parameters: + backbone: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: resnet18 + description: Pre-trained backbone used for feature extraction + editable: true + enum_name: ModelBackbone + header: Model Backbone + options: + RESNET18: resnet18 + WIDE_RESNET_50: wide_resnet50_2 + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: resnet18 + visible_in_ui: true + warning: null + description: Learning Parameters + early_stopping: + description: Early Stopping Parameters + header: Early Stopping Parameters + metric: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: image_F1Score + description: The metric used to determine if the model should stop training + editable: true + enum_name: EarlyStoppingMetrics + header: Early Stopping Metric + options: + IMAGE_F1: image_F1Score + IMAGE_ROC_AUC: image_AUROC + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: image_F1Score + visible_in_ui: true + warning: null + patience: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 10 + description: + Number of epochs to wait for an improvement in the monitored metric. + If the metric has not improved for this many epochs, the training will stop + and the best model will be returned. + editable: true + header: Early Stopping Patience + max_value: 100 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 10 + visible_in_ui: true + warning: + Setting this value too low might lead to underfitting. Setting the + value too high will increase the training time and might lead to overfitting. + type: PARAMETER_GROUP + visible_in_ui: true + header: Learning Parameters + lr: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.4 + description: Learning rate used for optimizing the Student network. + editable: true + header: Learning Rate + max_value: 1 + min_value: 0.001 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.4 + visible_in_ui: true + warning: null + max_epochs: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 100 + description: Maximum number of epochs to train the model for. + editable: true + header: Max Epochs + max_value: 500 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: + Training for very few epochs might lead to poor performance. If Early + Stopping is enabled then increasing the value of max epochs might not lead to + desired result. + momentum: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.9 + description: Momentum used for SGD optimizer + editable: true + header: Momentum + max_value: 1.0 + min_value: 0.1 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.9 + visible_in_ui: true + warning: null + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 32 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 32 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + type: PARAMETER_GROUP + visible_in_ui: true + weight_decay: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.0001 + description: Decay for SGD optimizer + editable: true + header: Weight Decay + max_value: 1 + min_value: 1.0e-05 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.0001 + visible_in_ui: true + warning: null +nncf_optimization: + description: Optimization by NNCF + enable_pruning: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + enable_quantization: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: true + description: Enable quantization algorithm + editable: true + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + header: Optimization by NNCF + pruning_supported: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/anomaly/configs/classification/stfpm/hpo_config.yaml b/src/otx/algorithms/anomaly/configs/classification/stfpm/hpo_config.yaml new file mode 100644 index 00000000000..ad9db985679 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/stfpm/hpo_config.yaml @@ -0,0 +1,18 @@ +# default model.lr: 0.4, dataset.train_batch_size: 32 +metric: image_F1Score +mode: max +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.lr: + param_type: qloguniform + range: + - 0.04 + - 0.8 + - 0.01 + learning_parameters.train_batch_size: + param_type: qloguniform + range: + - 16 + - 64 + - 2 diff --git a/src/otx/algorithms/anomaly/configs/classification/stfpm/template.yaml b/src/otx/algorithms/anomaly/configs/classification/stfpm/template.yaml new file mode 100644 index 00000000000..42ef6129a32 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/classification/stfpm/template.yaml @@ -0,0 +1,40 @@ +# Description. +model_template_id: ote_anomaly_classification_stfpm +name: STFPM +task_type: ANOMALY_CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: Use this model when the position of the objects in the image frame might differ between images. +application: ~ + +# Algo backend. +framework: OTXAnomalyClassification v0.1.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.anomaly.tasks.TrainingTask + openvino: otx.algorithms.anomaly.tasks.OpenVINOTask + nncf: otx.algorithms.anomaly.tasks.NNCFTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + parameter_overrides: + learning_parameters: + train_batch_size: + auto_hpo_state: POSSIBLE + lr: + auto_hpo_state: POSSIBLE + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 5.6 +size: 21.1 + +# Model spec +model_category: ACCURACY diff --git a/src/otx/algorithms/anomaly/configs/detection/__init__.py b/src/otx/algorithms/anomaly/configs/detection/__init__.py new file mode 100644 index 00000000000..7740bb752e6 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/__init__.py @@ -0,0 +1,15 @@ +"""Configuration for detection tasks.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/anomaly/configs/detection/draem/__init__.py b/src/otx/algorithms/anomaly/configs/detection/draem/__init__.py new file mode 100644 index 00000000000..75b203b8de6 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/draem/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Configurable Parameters for DRAEM Anomaly Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import DraemAnomalyDetectionConfig + +__all__ = ["DraemAnomalyDetectionConfig"] diff --git a/src/otx/algorithms/anomaly/configs/detection/draem/compression_config.json b/src/otx/algorithms/anomaly/configs/detection/draem/compression_config.json new file mode 100644 index 00000000000..0b7922f5a23 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/draem/compression_config.json @@ -0,0 +1,36 @@ +{ + "base": { + "find_unused_parameters": true, + "target_metric_name": "image_F1Score", + "nncf_config": { + "input_info": { + "sample_size": [1, 3, 256, 256] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "model": { + "lr": 0.004 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 250 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 250 + } + }, + "ignored_scopes": [] + } + ] + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/anomaly/configs/detection/draem/configuration.py b/src/otx/algorithms/anomaly/configs/detection/draem/configuration.py new file mode 100644 index 00000000000..16fce027b0a --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/draem/configuration.py @@ -0,0 +1,24 @@ +"""Configurable parameters for DRAEM anomaly Detection task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base import DraemAnomalyBaseConfig + + +@attrs +class DraemAnomalyDetectionConfig(DraemAnomalyBaseConfig): + """Configurable parameters for DRAEM anomaly Detection task.""" diff --git a/src/otx/algorithms/anomaly/configs/detection/draem/configuration.yaml b/src/otx/algorithms/anomaly/configs/detection/draem/configuration.yaml new file mode 100644 index 00000000000..ee410672c48 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/draem/configuration.yaml @@ -0,0 +1,242 @@ +dataset: + description: Dataset Parameters + header: Dataset Parameters + num_workers: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of workers + max_value: 36 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +description: Configuration for Draem +header: Configuration for Draem +learning_parameters: + description: Learning Parameters + early_stopping: + description: Early Stopping Parameters + header: Early Stopping Parameters + metric: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: image_AUROC + description: The metric used to determine if the model should stop training + editable: true + enum_name: EarlyStoppingMetrics + header: Early Stopping Metric + options: + IMAGE_F1: image_F1Score + IMAGE_ROC_AUC: image_AUROC + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + patience: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 20 + description: + Number of epochs to wait for an improvement in the monitored metric. + If the metric has not improved for this many epochs, the training will stop + and the best model will be returned. + editable: true + header: Early Stopping Patience + max_value: 100 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Setting this value too low might lead to underfitting. Setting the + value too high will increase the training time and might lead to overfitting. + type: PARAMETER_GROUP + visible_in_ui: true + header: Learning Parameters + lr: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.0001 + description: Learning rate used for optimizing the network. + editable: true + header: Learning Rate + max_value: 1 + min_value: 0.0001 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + max_epochs: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 700 + description: Maximum number of epochs to train the model for. + editable: true + header: Max Epochs + max_value: 700 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Training for very few epochs might lead to poor performance. If Early + Stopping is enabled then increasing the value of max epochs might not lead to + desired result. + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + enable_pruning: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + enable_quantization: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: true + description: Enable quantization algorithm + editable: true + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + header: Optimization by NNCF + pruning_supported: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/anomaly/configs/detection/draem/template_experimental.yaml b/src/otx/algorithms/anomaly/configs/detection/draem/template_experimental.yaml new file mode 100644 index 00000000000..972bed9140c --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/draem/template_experimental.yaml @@ -0,0 +1,31 @@ +# Description. +model_template_id: ote_anomaly_detection_draem +name: DRAEM +task_type: ANOMALY_DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Most accurate model across datasets, but longer training time. +application: ~ + +# Algo backend. +framework: OTXAnomalyClassification v0.1.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.anomaly.tasks.TrainingTask + openvino: otx.algorithms.anomaly.tasks.OpenVINOTask + nncf: otx.algorithms.anomaly.tasks.NNCFTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 3.9 +size: 168.4 diff --git a/src/otx/algorithms/anomaly/configs/detection/draem/transform_config.yaml b/src/otx/algorithms/anomaly/configs/detection/draem/transform_config.yaml new file mode 100644 index 00000000000..0e44b758c33 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/draem/transform_config.yaml @@ -0,0 +1,31 @@ +# Description. +model_template_id: ote_anomaly_detection_draem +name: DRAEM +task_type: ANOMALY_DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Most accurate model across datasets, but longer training time. +application: ~ + +# Algo backend. +framework: OTXAnomalyClassification v0.1.0 + +# Task implementations. +entrypoints: + base: tasks.TrainingTask + openvino: tasks.OpenVINOTask + nncf: tasks.NNCFTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 3.9 +size: 168.4 diff --git a/src/otx/algorithms/anomaly/configs/detection/padim/__init__.py b/src/otx/algorithms/anomaly/configs/detection/padim/__init__.py new file mode 100644 index 00000000000..33b34eab8bf --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/padim/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Configurable Parameters for PADIM Anomaly Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import PadimAnomalyDetectionConfig + +__all__ = ["PadimAnomalyDetectionConfig"] diff --git a/src/otx/algorithms/anomaly/configs/detection/padim/compression_config.json b/src/otx/algorithms/anomaly/configs/detection/padim/compression_config.json new file mode 100644 index 00000000000..4235946a7d7 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/padim/compression_config.json @@ -0,0 +1,38 @@ +{ + "base": { + "find_unused_parameters": true, + "target_metric_name": "image_F1Score", + "nncf_config": { + "input_info": { + "sample_size": [1, 3, 256, 256] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 250 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 250 + } + }, + "ignored_scopes": [ + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0", + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0", + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/matmul_0", + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0" + ] + } + ] + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/anomaly/configs/detection/padim/configuration.py b/src/otx/algorithms/anomaly/configs/detection/padim/configuration.py new file mode 100644 index 00000000000..d0e4f3f241f --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/padim/configuration.py @@ -0,0 +1,24 @@ +"""Configurable parameters for Padim anomaly Detection task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base import PadimAnomalyBaseConfig + + +@attrs +class PadimAnomalyDetectionConfig(PadimAnomalyBaseConfig): + """Configurable parameters for PADIM anomaly Detection task.""" diff --git a/src/otx/algorithms/anomaly/configs/detection/padim/configuration.yaml b/src/otx/algorithms/anomaly/configs/detection/padim/configuration.yaml new file mode 100644 index 00000000000..eac893d019c --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/padim/configuration.yaml @@ -0,0 +1,183 @@ +dataset: + description: Dataset Parameters + header: Dataset Parameters + num_workers: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of workers + max_value: 36 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 8 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +description: Configuration for Padim +header: Configuration for Padim +id: "" +learning_parameters: + backbone: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: resnet18 + description: Pre-trained backbone used for feature extraction + editable: false + enum_name: ModelBackbone + header: Model Backbone + options: + RESNET18: resnet18 + WIDE_RESNET_50: wide_resnet50_2 + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: resnet18 + visible_in_ui: false + warning: null + description: Learning Parameters + header: Learning Parameters + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 32 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 32 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + enable_pruning: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + enable_quantization: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: true + description: Enable quantization algorithm + editable: true + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + header: Optimization by NNCF + pruning_supported: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/anomaly/configs/detection/padim/ptq_optimization_config.py b/src/otx/algorithms/anomaly/configs/detection/padim/ptq_optimization_config.py new file mode 100644 index 00000000000..635b4a40c1c --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/padim/ptq_optimization_config.py @@ -0,0 +1,25 @@ +"""PTQ config file.""" +from nncf import IgnoredScope +from nncf.common.quantization.structs import QuantizationPreset +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ) +) + +preset = QuantizationPreset.MIXED + +ignored_scope = IgnoredScope(names=["/anomaly_map_generator/Mul", "/anomaly_map_generator/Sqrt"]) diff --git a/src/otx/algorithms/anomaly/configs/detection/padim/template.yaml b/src/otx/algorithms/anomaly/configs/detection/padim/template.yaml new file mode 100644 index 00000000000..93bb00a0dfd --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/padim/template.yaml @@ -0,0 +1,35 @@ +# Description. +model_template_id: ote_anomaly_detection_padim +name: PADIM +task_type: ANOMALY_DETECTION +task_family: VISION +instantiation: "CLASS" +summary: This model is faster and in many cases more accurate, but it requires a fixed position of the objects within the image. +application: ~ + +# Algo backend. +framework: OTXAnomalyClassification v0.1.0 # TODO: update after the name has been changed on the platform side + +# Task implementations. +entrypoints: + base: otx.algorithms.anomaly.tasks.TrainingTask + openvino: otx.algorithms.anomaly.tasks.OpenVINOTask + nncf: otx.algorithms.anomaly.tasks.NNCFTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 3.9 +size: 168.4 + +# Model spec +model_category: SPEED +is_default_for_task: true diff --git a/src/otx/algorithms/anomaly/configs/detection/stfpm/__init__.py b/src/otx/algorithms/anomaly/configs/detection/stfpm/__init__.py new file mode 100644 index 00000000000..7eee52f0ed0 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/stfpm/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Configurable Parameters for STFPM Anomaly Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import STFPMAnomalyDetectionConfig + +__all__ = ["STFPMAnomalyDetectionConfig"] diff --git a/src/otx/algorithms/anomaly/configs/detection/stfpm/compression_config.json b/src/otx/algorithms/anomaly/configs/detection/stfpm/compression_config.json new file mode 100644 index 00000000000..caee63d064b --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/stfpm/compression_config.json @@ -0,0 +1,36 @@ +{ + "base": { + "find_unused_parameters": true, + "target_metric_name": "image_F1Score", + "nncf_config": { + "input_info": { + "sample_size": [1, 3, 256, 256] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "model": { + "lr": 0.004 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 250 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 250 + } + }, + "ignored_scopes": ["{re}.*__pow__.*"] + } + ] + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/anomaly/configs/detection/stfpm/configuration.py b/src/otx/algorithms/anomaly/configs/detection/stfpm/configuration.py new file mode 100644 index 00000000000..4e52407ab19 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/stfpm/configuration.py @@ -0,0 +1,24 @@ +"""Configurable parameters for STFPM anomaly Detection task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base import STFPMAnomalyBaseConfig + + +@attrs +class STFPMAnomalyDetectionConfig(STFPMAnomalyBaseConfig): + """Configurable parameters for STFPM anomaly Detection task.""" diff --git a/src/otx/algorithms/anomaly/configs/detection/stfpm/configuration.yaml b/src/otx/algorithms/anomaly/configs/detection/stfpm/configuration.yaml new file mode 100644 index 00000000000..ff3e8ca1517 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/stfpm/configuration.yaml @@ -0,0 +1,312 @@ +dataset: + description: Dataset Parameters + header: Dataset Parameters + num_workers: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of workers + max_value: 36 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 8 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +description: Configuration for STFPM +header: Configuration for STFPM +id: "" +learning_parameters: + backbone: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: resnet18 + description: Pre-trained backbone used for feature extraction + editable: true + enum_name: ModelBackbone + header: Model Backbone + options: + RESNET18: resnet18 + WIDE_RESNET_50: wide_resnet50_2 + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: resnet18 + visible_in_ui: true + warning: null + description: Learning Parameters + early_stopping: + description: Early Stopping Parameters + header: Early Stopping Parameters + metric: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: image_F1Score + description: The metric used to determine if the model should stop training + editable: true + enum_name: EarlyStoppingMetrics + header: Early Stopping Metric + options: + IMAGE_F1: image_F1Score + IMAGE_ROC_AUC: image_AUROC + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: image_F1Score + visible_in_ui: true + warning: null + patience: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 10 + description: + Number of epochs to wait for an improvement in the monitored metric. + If the metric has not improved for this many epochs, the training will stop + and the best model will be returned. + editable: true + header: Early Stopping Patience + max_value: 100 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 10 + visible_in_ui: true + warning: + Setting this value too low might lead to underfitting. Setting the + value too high will increase the training time and might lead to overfitting. + type: PARAMETER_GROUP + visible_in_ui: true + header: Learning Parameters + lr: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.4 + description: Learning rate used for optimizing the Student network. + editable: true + header: Learning Rate + max_value: 1 + min_value: 0.001 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.4 + visible_in_ui: true + warning: null + max_epochs: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 100 + description: Maximum number of epochs to train the model for. + editable: true + header: Max Epochs + max_value: 500 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: + Training for very few epochs might lead to poor performance. If Early + Stopping is enabled then increasing the value of max epochs might not lead to + desired result. + momentum: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.9 + description: Momentum used for SGD optimizer + editable: true + header: Momentum + max_value: 1.0 + min_value: 0.1 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.9 + visible_in_ui: true + warning: null + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 32 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 32 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + type: PARAMETER_GROUP + visible_in_ui: true + weight_decay: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.0001 + description: Decay for SGD optimizer + editable: true + header: Weight Decay + max_value: 1 + min_value: 1.0e-05 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.0001 + visible_in_ui: true + warning: null +nncf_optimization: + description: Optimization by NNCF + enable_pruning: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + enable_quantization: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: true + description: Enable quantization algorithm + editable: true + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + header: Optimization by NNCF + pruning_supported: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/anomaly/configs/detection/stfpm/hpo_config.yaml b/src/otx/algorithms/anomaly/configs/detection/stfpm/hpo_config.yaml new file mode 100644 index 00000000000..ad9db985679 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/stfpm/hpo_config.yaml @@ -0,0 +1,18 @@ +# default model.lr: 0.4, dataset.train_batch_size: 32 +metric: image_F1Score +mode: max +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.lr: + param_type: qloguniform + range: + - 0.04 + - 0.8 + - 0.01 + learning_parameters.train_batch_size: + param_type: qloguniform + range: + - 16 + - 64 + - 2 diff --git a/src/otx/algorithms/anomaly/configs/detection/stfpm/template.yaml b/src/otx/algorithms/anomaly/configs/detection/stfpm/template.yaml new file mode 100644 index 00000000000..5d4d49832c0 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/detection/stfpm/template.yaml @@ -0,0 +1,40 @@ +# Description. +model_template_id: ote_anomaly_detection_stfpm +name: STFPM +task_type: ANOMALY_DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Use this model when the position of the objects in the image frame might differ between images. +application: ~ + +# Algo backend. +framework: OTXAnomalyClassification v0.1.0 # TODO: update after the name has been changed on the platform side + +# Task implementations. +entrypoints: + base: otx.algorithms.anomaly.tasks.TrainingTask + openvino: otx.algorithms.anomaly.tasks.OpenVINOTask + nncf: otx.algorithms.anomaly.tasks.NNCFTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + parameter_overrides: + learning_parameters: + train_batch_size: + auto_hpo_state: POSSIBLE + lr: + auto_hpo_state: POSSIBLE + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 5.6 +size: 21.1 + +# Model spec +model_category: ACCURACY diff --git a/src/otx/algorithms/anomaly/configs/segmentation/__init__.py b/src/otx/algorithms/anomaly/configs/segmentation/__init__.py new file mode 100644 index 00000000000..540ccf6ad4f --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/__init__.py @@ -0,0 +1,15 @@ +"""Configuration for segmentation tasks.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/anomaly/configs/segmentation/draem/__init__.py b/src/otx/algorithms/anomaly/configs/segmentation/draem/__init__.py new file mode 100644 index 00000000000..fba1af72c8c --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/draem/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Configurable Parameters for DRAEM Anomaly Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import DraemAnomalySegmentationConfig + +__all__ = ["DraemAnomalySegmentationConfig"] diff --git a/src/otx/algorithms/anomaly/configs/segmentation/draem/compression_config.json b/src/otx/algorithms/anomaly/configs/segmentation/draem/compression_config.json new file mode 100644 index 00000000000..0b7922f5a23 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/draem/compression_config.json @@ -0,0 +1,36 @@ +{ + "base": { + "find_unused_parameters": true, + "target_metric_name": "image_F1Score", + "nncf_config": { + "input_info": { + "sample_size": [1, 3, 256, 256] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "model": { + "lr": 0.004 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 250 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 250 + } + }, + "ignored_scopes": [] + } + ] + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/anomaly/configs/segmentation/draem/configuration.py b/src/otx/algorithms/anomaly/configs/segmentation/draem/configuration.py new file mode 100644 index 00000000000..83ee4c8670a --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/draem/configuration.py @@ -0,0 +1,24 @@ +"""Configurable parameters for DRAEM anomaly Segmentation task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base import DraemAnomalyBaseConfig + + +@attrs +class DraemAnomalySegmentationConfig(DraemAnomalyBaseConfig): + """Configurable parameters for DRAEM anomaly Segmentation task.""" diff --git a/src/otx/algorithms/anomaly/configs/segmentation/draem/configuration.yaml b/src/otx/algorithms/anomaly/configs/segmentation/draem/configuration.yaml new file mode 100644 index 00000000000..ee410672c48 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/draem/configuration.yaml @@ -0,0 +1,242 @@ +dataset: + description: Dataset Parameters + header: Dataset Parameters + num_workers: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of workers + max_value: 36 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +description: Configuration for Draem +header: Configuration for Draem +learning_parameters: + description: Learning Parameters + early_stopping: + description: Early Stopping Parameters + header: Early Stopping Parameters + metric: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: image_AUROC + description: The metric used to determine if the model should stop training + editable: true + enum_name: EarlyStoppingMetrics + header: Early Stopping Metric + options: + IMAGE_F1: image_F1Score + IMAGE_ROC_AUC: image_AUROC + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + patience: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 20 + description: + Number of epochs to wait for an improvement in the monitored metric. + If the metric has not improved for this many epochs, the training will stop + and the best model will be returned. + editable: true + header: Early Stopping Patience + max_value: 100 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Setting this value too low might lead to underfitting. Setting the + value too high will increase the training time and might lead to overfitting. + type: PARAMETER_GROUP + visible_in_ui: true + header: Learning Parameters + lr: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.0001 + description: Learning rate used for optimizing the network. + editable: true + header: Learning Rate + max_value: 1 + min_value: 0.0001 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + max_epochs: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 700 + description: Maximum number of epochs to train the model for. + editable: true + header: Max Epochs + max_value: 700 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Training for very few epochs might lead to poor performance. If Early + Stopping is enabled then increasing the value of max epochs might not lead to + desired result. + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + enable_pruning: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + enable_quantization: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: true + description: Enable quantization algorithm + editable: true + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + header: Optimization by NNCF + pruning_supported: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/anomaly/configs/segmentation/draem/template_experimental.yaml b/src/otx/algorithms/anomaly/configs/segmentation/draem/template_experimental.yaml new file mode 100644 index 00000000000..ca28551fd78 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/draem/template_experimental.yaml @@ -0,0 +1,31 @@ +# Description. +model_template_id: ote_anomaly_segmentation_draem +name: DRAEM +task_type: ANOMALY_SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Most accurate model across datasets, but longer training time. +application: ~ + +# Algo backend. +framework: OTXAnomalyClassification v0.1.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.anomaly.tasks.TrainingTask + openvino: otx.algorithms.anomaly.tasks.OpenVINOTask + nncf: otx.algorithms.anomaly.tasks.NNCFTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 3.9 +size: 168.4 diff --git a/src/otx/algorithms/anomaly/configs/segmentation/draem/transform_config.yaml b/src/otx/algorithms/anomaly/configs/segmentation/draem/transform_config.yaml new file mode 100644 index 00000000000..5a379ef7628 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/draem/transform_config.yaml @@ -0,0 +1,26 @@ +{ + "__version__": "1.1.0", + "transform": + { + "__class_fullname__": "Compose", + "p": 1.0, + "transforms": + [ + { + "__class_fullname__": "ToFloat", + "always_apply": false, + "p": 1.0, + "max_value": null, + }, + { + "__class_fullname__": "ToTensorV2", + "always_apply": true, + "p": 1.0, + "transpose_mask": false, + }, + ], + "bbox_params": null, + "keypoint_params": null, + "additional_targets": {}, + }, +} diff --git a/src/otx/algorithms/anomaly/configs/segmentation/padim/__init__.py b/src/otx/algorithms/anomaly/configs/segmentation/padim/__init__.py new file mode 100644 index 00000000000..797a8187404 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/padim/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Configurable Parameters for PADIM Anomaly Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import PadimAnomalySegmentationConfig + +__all__ = ["PadimAnomalySegmentationConfig"] diff --git a/src/otx/algorithms/anomaly/configs/segmentation/padim/compression_config.json b/src/otx/algorithms/anomaly/configs/segmentation/padim/compression_config.json new file mode 100644 index 00000000000..4235946a7d7 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/padim/compression_config.json @@ -0,0 +1,38 @@ +{ + "base": { + "find_unused_parameters": true, + "target_metric_name": "image_F1Score", + "nncf_config": { + "input_info": { + "sample_size": [1, 3, 256, 256] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 250 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 250 + } + }, + "ignored_scopes": [ + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0", + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0", + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/matmul_0", + "PadimModel/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0" + ] + } + ] + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/anomaly/configs/segmentation/padim/configuration.py b/src/otx/algorithms/anomaly/configs/segmentation/padim/configuration.py new file mode 100644 index 00000000000..965f562f7a6 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/padim/configuration.py @@ -0,0 +1,24 @@ +"""Configurable parameters for Padim anomaly Segmentation task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base import PadimAnomalyBaseConfig + + +@attrs +class PadimAnomalySegmentationConfig(PadimAnomalyBaseConfig): + """Configurable parameters for PADIM anomaly Segmentation task.""" diff --git a/src/otx/algorithms/anomaly/configs/segmentation/padim/configuration.yaml b/src/otx/algorithms/anomaly/configs/segmentation/padim/configuration.yaml new file mode 100644 index 00000000000..eac893d019c --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/padim/configuration.yaml @@ -0,0 +1,183 @@ +dataset: + description: Dataset Parameters + header: Dataset Parameters + num_workers: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of workers + max_value: 36 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 8 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +description: Configuration for Padim +header: Configuration for Padim +id: "" +learning_parameters: + backbone: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: resnet18 + description: Pre-trained backbone used for feature extraction + editable: false + enum_name: ModelBackbone + header: Model Backbone + options: + RESNET18: resnet18 + WIDE_RESNET_50: wide_resnet50_2 + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: resnet18 + visible_in_ui: false + warning: null + description: Learning Parameters + header: Learning Parameters + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 32 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 32 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + enable_pruning: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + enable_quantization: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: true + description: Enable quantization algorithm + editable: true + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + header: Optimization by NNCF + pruning_supported: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/anomaly/configs/segmentation/padim/ptq_optimization_config.py b/src/otx/algorithms/anomaly/configs/segmentation/padim/ptq_optimization_config.py new file mode 100644 index 00000000000..635b4a40c1c --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/padim/ptq_optimization_config.py @@ -0,0 +1,25 @@ +"""PTQ config file.""" +from nncf import IgnoredScope +from nncf.common.quantization.structs import QuantizationPreset +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ) +) + +preset = QuantizationPreset.MIXED + +ignored_scope = IgnoredScope(names=["/anomaly_map_generator/Mul", "/anomaly_map_generator/Sqrt"]) diff --git a/src/otx/algorithms/anomaly/configs/segmentation/padim/template.yaml b/src/otx/algorithms/anomaly/configs/segmentation/padim/template.yaml new file mode 100644 index 00000000000..8ac2fdb8a13 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/padim/template.yaml @@ -0,0 +1,35 @@ +# Description. +model_template_id: ote_anomaly_segmentation_padim +name: PADIM +task_type: ANOMALY_SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: This model is faster and in many cases more accurate, but it requires a fixed position of the objects within the image. +application: ~ + +# Algo backend. +framework: OTXAnomalyClassification v0.1.0 # TODO: update after the name has been changed on the platform side + +# Task implementations. +entrypoints: + base: otx.algorithms.anomaly.tasks.TrainingTask + openvino: otx.algorithms.anomaly.tasks.OpenVINOTask + nncf: otx.algorithms.anomaly.tasks.NNCFTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 3.9 +size: 168.4 + +# Model spec +model_category: SPEED +is_default_for_task: true diff --git a/src/otx/algorithms/anomaly/configs/segmentation/stfpm/__init__.py b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/__init__.py new file mode 100644 index 00000000000..179c13a396a --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/__init__.py @@ -0,0 +1,19 @@ +"""Initialization of Configurable Parameters for STFPM Anomaly Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import STFPMAnomalySegmentationConfig + +__all__ = ["STFPMAnomalySegmentationConfig"] diff --git a/src/otx/algorithms/anomaly/configs/segmentation/stfpm/compression_config.json b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/compression_config.json new file mode 100644 index 00000000000..caee63d064b --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/compression_config.json @@ -0,0 +1,36 @@ +{ + "base": { + "find_unused_parameters": true, + "target_metric_name": "image_F1Score", + "nncf_config": { + "input_info": { + "sample_size": [1, 3, 256, 256] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "model": { + "lr": 0.004 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 250 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 250 + } + }, + "ignored_scopes": ["{re}.*__pow__.*"] + } + ] + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/anomaly/configs/segmentation/stfpm/configuration.py b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/configuration.py new file mode 100644 index 00000000000..74eb0a143dd --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/configuration.py @@ -0,0 +1,24 @@ +"""Configurable parameters for STFPM anomaly Segmentation task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from attr import attrs + +from otx.algorithms.anomaly.configs.base import STFPMAnomalyBaseConfig + + +@attrs +class STFPMAnomalySegmentationConfig(STFPMAnomalyBaseConfig): + """Configurable parameters for STFPM anomaly Segmentation task.""" diff --git a/src/otx/algorithms/anomaly/configs/segmentation/stfpm/configuration.yaml b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/configuration.yaml new file mode 100644 index 00000000000..ff3e8ca1517 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/configuration.yaml @@ -0,0 +1,312 @@ +dataset: + description: Dataset Parameters + header: Dataset Parameters + num_workers: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 8 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of workers + max_value: 36 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 8 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +description: Configuration for STFPM +header: Configuration for STFPM +id: "" +learning_parameters: + backbone: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: resnet18 + description: Pre-trained backbone used for feature extraction + editable: true + enum_name: ModelBackbone + header: Model Backbone + options: + RESNET18: resnet18 + WIDE_RESNET_50: wide_resnet50_2 + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: resnet18 + visible_in_ui: true + warning: null + description: Learning Parameters + early_stopping: + description: Early Stopping Parameters + header: Early Stopping Parameters + metric: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: image_F1Score + description: The metric used to determine if the model should stop training + editable: true + enum_name: EarlyStoppingMetrics + header: Early Stopping Metric + options: + IMAGE_F1: image_F1Score + IMAGE_ROC_AUC: image_AUROC + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: image_F1Score + visible_in_ui: true + warning: null + patience: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 10 + description: + Number of epochs to wait for an improvement in the monitored metric. + If the metric has not improved for this many epochs, the training will stop + and the best model will be returned. + editable: true + header: Early Stopping Patience + max_value: 100 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 10 + visible_in_ui: true + warning: + Setting this value too low might lead to underfitting. Setting the + value too high will increase the training time and might lead to overfitting. + type: PARAMETER_GROUP + visible_in_ui: true + header: Learning Parameters + lr: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.4 + description: Learning rate used for optimizing the Student network. + editable: true + header: Learning Rate + max_value: 1 + min_value: 0.001 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.4 + visible_in_ui: true + warning: null + max_epochs: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 100 + description: Maximum number of epochs to train the model for. + editable: true + header: Max Epochs + max_value: 500 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: + Training for very few epochs might lead to poor performance. If Early + Stopping is enabled then increasing the value of max epochs might not lead to + desired result. + momentum: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.9 + description: Momentum used for SGD optimizer + editable: true + header: Momentum + max_value: 1.0 + min_value: 0.1 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.9 + visible_in_ui: true + warning: null + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 32 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 32 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + type: PARAMETER_GROUP + visible_in_ui: true + weight_decay: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 0.0001 + description: Decay for SGD optimizer + editable: true + header: Weight Decay + max_value: 1 + min_value: 1.0e-05 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.0001 + visible_in_ui: true + warning: null +nncf_optimization: + description: Optimization by NNCF + enable_pruning: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + enable_quantization: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: true + description: Enable quantization algorithm + editable: true + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + header: Optimization by NNCF + pruning_supported: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/anomaly/configs/segmentation/stfpm/hpo_config.yaml b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/hpo_config.yaml new file mode 100644 index 00000000000..ad9db985679 --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/hpo_config.yaml @@ -0,0 +1,18 @@ +# default model.lr: 0.4, dataset.train_batch_size: 32 +metric: image_F1Score +mode: max +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.lr: + param_type: qloguniform + range: + - 0.04 + - 0.8 + - 0.01 + learning_parameters.train_batch_size: + param_type: qloguniform + range: + - 16 + - 64 + - 2 diff --git a/src/otx/algorithms/anomaly/configs/segmentation/stfpm/template.yaml b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/template.yaml new file mode 100644 index 00000000000..fa17fc6f07e --- /dev/null +++ b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/template.yaml @@ -0,0 +1,40 @@ +# Description. +model_template_id: ote_anomaly_segmentation_stfpm +name: STFPM +task_type: ANOMALY_SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Use this model when the position of the objects in the image frame might differ between images. +application: ~ + +# Algo backend. +framework: OTXAnomalyClassification v0.1.0 # TODO: update after the name has been changed on the platform side + +# Task implementations. +entrypoints: + base: otx.algorithms.anomaly.tasks.TrainingTask + openvino: otx.algorithms.anomaly.tasks.OpenVINOTask + nncf: otx.algorithms.anomaly.tasks.NNCFTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + parameter_overrides: + learning_parameters: + train_batch_size: + auto_hpo_state: POSSIBLE + lr: + auto_hpo_state: POSSIBLE + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 5.6 +size: 21.1 + +# Model spec +model_category: ACCURACY diff --git a/src/otx/algorithms/anomaly/ote_tests_pytest.ini b/src/otx/algorithms/anomaly/ote_tests_pytest.ini new file mode 100644 index 00000000000..a0afe4fa5f6 --- /dev/null +++ b/src/otx/algorithms/anomaly/ote_tests_pytest.ini @@ -0,0 +1,2 @@ +[pytest] +python_files = test_otx_api.py test_otx_inference.py test_otx_*_training.py diff --git a/src/otx/algorithms/anomaly/tasks/__init__.py b/src/otx/algorithms/anomaly/tasks/__init__.py new file mode 100644 index 00000000000..e1015ff79ec --- /dev/null +++ b/src/otx/algorithms/anomaly/tasks/__init__.py @@ -0,0 +1,22 @@ +"""Initialization of OTX Anomalib.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .inference import InferenceTask +from .nncf import NNCFTask +from .openvino import OpenVINOTask +from .train import TrainingTask + +__all__ = ["InferenceTask", "TrainingTask", "NNCFTask", "OpenVINOTask"] diff --git a/src/otx/algorithms/anomaly/tasks/inference.py b/src/otx/algorithms/anomaly/tasks/inference.py new file mode 100644 index 00000000000..bf430a5dbea --- /dev/null +++ b/src/otx/algorithms/anomaly/tasks/inference.py @@ -0,0 +1,501 @@ +"""Anomaly Classification Task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import ctypes +import io +import json +import os +import shutil +import subprocess # nosec B404 +import tempfile +from glob import glob +from typing import Any, Dict, List, Optional, Tuple, Union +from warnings import warn + +import torch +from anomalib.data.utils.transform import get_transforms +from anomalib.models import AnomalyModule, get_model +from anomalib.post_processing import NormalizationMethod, ThresholdMethod +from anomalib.utils.callbacks import ( + MetricsConfigurationCallback, + MinMaxNormalizationCallback, + PostProcessingConfigurationCallback, +) +from omegaconf import DictConfig, ListConfig +from pytorch_lightning import Trainer + +from otx.algorithms.anomaly.adapters.anomalib.callbacks import ( + AnomalyInferenceCallback, + ProgressCallback, +) +from otx.algorithms.anomaly.adapters.anomalib.config import get_anomalib_config +from otx.algorithms.anomaly.adapters.anomalib.data import OTXAnomalyDataModule +from otx.algorithms.anomaly.configs.base.configuration import BaseAnomalyConfig +from otx.algorithms.common.utils import embed_ir_model_data +from otx.algorithms.common.utils.utils import embed_onnx_model_data +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.metrics import NullPerformance, Performance, ScoreMetric +from otx.api.entities.model import ( + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.model_template import TaskType +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.evaluation.performance_provider_interface import ( + IPerformanceProvider, +) +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.export_interface import ExportType, IExportTask +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.unload_interface import IUnload +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-instance-attributes +class InferenceTask(IInferenceTask, IEvaluationTask, IExportTask, IUnload): + """Base Anomaly Task.""" + + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None) -> None: + """Train, Infer, Export, Optimize and Deploy an Anomaly Classification Task. + + Args: + task_environment (TaskEnvironment): OTX Task environment. + output_path (Optional[str]): output path where task output are saved. + """ + torch.backends.cudnn.enabled = True + logger.info("Initializing the task environment.") + self.task_environment = task_environment + self.task_type = task_environment.model_template.task_type + self.model_name = task_environment.model_template.name + self.labels = task_environment.get_labels() + + template_file_path = task_environment.model_template.model_template_path + self.base_dir = os.path.abspath(os.path.dirname(template_file_path)) + + # Hyperparameters. + self._work_dir_is_temp = False + if output_path is None: + output_path = tempfile.mkdtemp(prefix="otx-anomalib") + self._work_dir_is_temp = True + self.project_path: str = output_path + self.config = self.get_config() + + # Set default model attributes. + self.optimization_methods: List[OptimizationMethod] = [] + self.precision = [ModelPrecision.FP32] + self.optimization_type = ModelOptimizationType.MO + + self.model = self.load_model(otx_model=task_environment.model) + + self.trainer: Trainer + + def get_config(self) -> Union[DictConfig, ListConfig]: + """Get Anomalib Config from task environment. + + Returns: + Union[DictConfig, ListConfig]: Anomalib config. + """ + self.hyper_parameters: BaseAnomalyConfig = self.task_environment.get_hyper_parameters() + config = get_anomalib_config(task_name=self.model_name, otx_config=self.hyper_parameters) + config.project.path = self.project_path + + config.dataset.task = "classification" + + return config + + def load_model(self, otx_model: Optional[ModelEntity]) -> AnomalyModule: + """Create and Load Anomalib Module from OTX Model. + + This method checks if the task environment has a saved OTX Model, + and creates one. If the OTX model already exists, it returns the + the model with the saved weights. + + Args: + otx_model (Optional[ModelEntity]): OTX Model from the + task environment. + + Returns: + AnomalyModule: Anomalib + classification or segmentation model with/without weights. + """ + if otx_model is None: + model = get_model(config=self.config) + logger.info( + "No trained model in project yet. Created new model with '%s'", + self.model_name, + ) + else: + buffer = io.BytesIO(otx_model.get_data("weights.pth")) + model_data = torch.load(buffer, map_location=torch.device("cpu")) + + if model_data["config"]["model"]["backbone"] != self.config["model"]["backbone"]: + logger.warning( + "Backbone of the model in the Task Environment is different from the one in the template. " + f"creating model with backbone={model_data['config']['model']['backbone']}" + ) + self.config["model"]["backbone"] = model_data["config"]["model"]["backbone"] + try: + model = get_model(config=self.config) + model.load_state_dict(model_data["model"]) + logger.info("Loaded model weights from Task Environment") + except BaseException as exception: + raise ValueError("Could not load the saved model. The model file structure is invalid.") from exception + + return model + + def cancel_training(self) -> None: + """Cancel the training `after_batch_end`. + + This terminates the training; however validation is still performed. + """ + logger.info("Cancel training requested.") + self.trainer.should_stop = True + + # The runner periodically checks `.stop_training` file to ensure if cancellation is requested. + cancel_training_file_path = os.path.join(self.config.project.path, ".stop_training") + with open(file=cancel_training_file_path, mode="a", encoding="utf-8"): + pass + + def infer(self, dataset: DatasetEntity, inference_parameters: InferenceParameters) -> DatasetEntity: + """Perform inference on a dataset. + + Args: + dataset (DatasetEntity): Dataset to infer. + inference_parameters (InferenceParameters): Inference parameters. + + Returns: + DatasetEntity: Output dataset with predictions. + """ + logger.info("Performing inference on the validation set using the base torch model.") + config = self.get_config() + datamodule = OTXAnomalyDataModule(config=config, dataset=dataset, task_type=self.task_type) + + logger.info("Inference Configs '%s'", config) + + # Callbacks. + progress = ProgressCallback(parameters=inference_parameters) + inference = AnomalyInferenceCallback(dataset, self.labels, self.task_type) + normalize = MinMaxNormalizationCallback() + metrics_configuration = MetricsConfigurationCallback( + task=config.dataset.task, + image_metrics=config.metrics.image, + pixel_metrics=config.metrics.get("pixel"), + ) + post_processing_configuration = PostProcessingConfigurationCallback( + normalization_method=NormalizationMethod.MIN_MAX, + threshold_method=ThresholdMethod.ADAPTIVE, + manual_image_threshold=config.metrics.threshold.manual_image, + manual_pixel_threshold=config.metrics.threshold.manual_pixel, + ) + callbacks = [progress, normalize, inference, metrics_configuration, post_processing_configuration] + + self.trainer = Trainer(**config.trainer, logger=False, callbacks=callbacks) + self.trainer.predict(model=self.model, datamodule=datamodule) + return dataset + + def evaluate(self, output_resultset: ResultSetEntity, evaluation_metric: Optional[str] = None) -> None: + """Evaluate the performance on a result set. + + Args: + output_resultset (ResultSetEntity): Result Set from which the performance is evaluated. + evaluation_metric (Optional[str], optional): Evaluation metric. Defaults to None. Instead, + metric is chosen depending on the task type. + """ + metric: IPerformanceProvider + if self.task_type == TaskType.ANOMALY_CLASSIFICATION: + metric = MetricsHelper.compute_f_measure(output_resultset) + elif self.task_type == TaskType.ANOMALY_DETECTION: + metric = MetricsHelper.compute_anomaly_detection_scores(output_resultset) + elif self.task_type == TaskType.ANOMALY_SEGMENTATION: + metric = MetricsHelper.compute_anomaly_segmentation_scores(output_resultset) + else: + raise ValueError(f"Unknown task type: {self.task_type}") + output_resultset.performance = metric.get_performance() + + if self.task_type == TaskType.ANOMALY_CLASSIFICATION: + accuracy = MetricsHelper.compute_accuracy(output_resultset).get_performance() + output_resultset.performance.dashboard_metrics.extend(accuracy.dashboard_metrics) + + def _export_to_onnx(self, onnx_path: str): + """Export model to ONNX. + + Args: + onnx_path (str): path to save ONNX file + """ + height, width = self.config.model.input_size + torch.onnx.export( + model=self.model.model, + args=torch.zeros((1, 3, height, width)).to(self.model.device), + f=onnx_path, + opset_version=11, + ) + + def export( + self, + export_type: ExportType, + output_model: ModelEntity, + precision: ModelPrecision = ModelPrecision.FP32, + dump_features: bool = False, + ) -> None: + """Export model to OpenVINO IR. + + Args: + export_type (ExportType): Export type should be ExportType.OPENVINO + output_model (ModelEntity): The model entity in which to write the OpenVINO IR data + precision (bool): Output model weights and inference precision + dump_features (bool): Flag to return "feature_vector" and "saliency_map". + + Raises: + Exception: If export_type is not ExportType.OPENVINO + """ + if dump_features: + logger.warning( + "Feature dumping is not implemented for the anomaly task." + "The saliency maps and representation vector outputs will not be dumped in the exported model." + ) + + if export_type == ExportType.ONNX: + output_model.model_format = ModelFormat.ONNX + output_model.optimization_type = ModelOptimizationType.ONNX + if precision == ModelPrecision.FP16: + raise RuntimeError("Export to FP16 ONNX is not supported") + elif export_type == ExportType.OPENVINO: + output_model.model_format = ModelFormat.OPENVINO + output_model.optimization_type = ModelOptimizationType.MO + else: + raise RuntimeError(f"not supported export type {export_type}") + + self.precision[0] = precision + output_model.has_xai = dump_features + + # pylint: disable=no-member; need to refactor this + logger.info("Exporting the OpenVINO model.") + onnx_path = os.path.join(self.config.project.path, "onnx_model.onnx") + self._export_to_onnx(onnx_path) + + if export_type == ExportType.ONNX: + self._add_metadata_to_ir(onnx_path, export_type) + + with open(onnx_path, "rb") as file: + output_model.set_data("model.onnx", file.read()) + else: + optimize_command = ["mo", "--input_model", onnx_path, "--output_dir", self.config.project.path] + if precision == ModelPrecision.FP16: + optimize_command.append("--compress_to_fp16") + subprocess.run(optimize_command, check=True) + bin_file = glob(os.path.join(self.config.project.path, "*.bin"))[0] + xml_file = glob(os.path.join(self.config.project.path, "*.xml"))[0] + + self._add_metadata_to_ir(xml_file, export_type) + + with open(bin_file, "rb") as file: + output_model.set_data("openvino.bin", file.read()) + with open(xml_file, "rb") as file: + output_model.set_data("openvino.xml", file.read()) + + output_model.precision = self.precision + output_model.optimization_methods = self.optimization_methods + + output_model.set_data("label_schema.json", label_schema_to_bytes(self.task_environment.label_schema)) + self._set_metadata(output_model) + + def _add_metadata_to_ir(self, model_file: str, export_type: ExportType) -> None: + """Adds the metadata to the model IR or ONNX. + + Adds the metadata to the model IR. So that it can be used with the new modelAPI. + This is because the metadata.json is not used by the new modelAPI. + # TODO CVS-114640 + # TODO: Step 1. Remove metadata.json when modelAPI becomes the default inference method. + # TODO: Step 2. Update this function when Anomalib is upgraded as the model graph will contain the required ops + # TODO: Step 3. Update modelAPI to remove pre/post-processing steps when Anomalib version is upgraded. + """ + metadata = self._get_metadata_dict() + extra_model_data: Dict[Tuple[str, str], Any] = {} + for key, value in metadata.items(): + if key in ("transform", "min", "max"): + continue + extra_model_data[("model_info", key)] = value + # Add transforms + if "transform" in metadata: + for transform_dict in metadata["transform"]["transform"]["transforms"]: + transform = transform_dict.pop("__class_fullname__") + if transform == "Normalize": + extra_model_data[("model_info", "mean_values")] = self._serialize_list( + [x * 255.0 for x in transform_dict["mean"]] + ) + extra_model_data[("model_info", "scale_values")] = self._serialize_list( + [x * 255.0 for x in transform_dict["std"]] + ) + elif transform == "Resize": + extra_model_data[("model_info", "orig_height")] = transform_dict["height"] + extra_model_data[("model_info", "orig_width")] = transform_dict["width"] + else: + warn(f"Transform {transform} is not supported currently") + # Since we only need the diff of max and min, we fuse the min and max into one op + if "min" in metadata and "max" in metadata: + extra_model_data[("model_info", "normalization_scale")] = metadata["max"] - metadata["min"] + + extra_model_data[("model_info", "reverse_input_channels")] = False + extra_model_data[("model_info", "model_type")] = "AnomalyDetection" + + labels = [] + label_ids = [] + for label_entity in self.task_environment.label_schema.get_labels(include_empty=False): + label_name = label_entity.name.replace(" ", "_") + # There is a mismatch between labels in OTX and modelAPI + if label_name == "Anomalous": + label_name = "Anomaly" + labels.append(label_name) + label_ids.append(str(label_entity.id_)) + + extra_model_data[("model_info", "labels")] = " ".join(labels) + extra_model_data[("model_info", "label_ids")] = " ".join(label_ids) + + if export_type == ExportType.OPENVINO: + embed_ir_model_data(model_file, extra_model_data) + elif export_type == ExportType.ONNX: + embed_onnx_model_data(model_file, extra_model_data) + else: + raise RuntimeError(f"not supported export type {export_type}") + + def _serialize_list(self, arr: Union[Tuple, List]) -> str: + """Converts a list to space separated string.""" + return " ".join(map(str, arr)) + + def model_info(self) -> Dict: + """Return model info to save the model weights. + + Returns: + Dict: Model info. + """ + return { + "model": self.model.state_dict(), + "config": self.get_config(), + "VERSION": 1, + } + + def save_model(self, output_model: ModelEntity) -> None: + """Save the model after training is completed. + + Args: + output_model (ModelEntity): Output model onto which the weights are saved. + """ + logger.info("Saving the model weights.") + model_info = self.model_info() + buffer = io.BytesIO() + torch.save(model_info, buffer) + output_model.set_data("weights.pth", buffer.getvalue()) + output_model.set_data("label_schema.json", label_schema_to_bytes(self.task_environment.label_schema)) + self._set_metadata(output_model) + + if hasattr(self.model, "image_metrics"): + f1_score = self.model.image_metrics.F1Score.compute().item() + output_model.performance = Performance(score=ScoreMetric(name="F1 Score", value=f1_score)) + else: + output_model.performance = NullPerformance() + output_model.precision = self.precision + output_model.optimization_methods = self.optimization_methods + + def _set_metadata(self, output_model: ModelEntity): + """Sets metadata in output_model.""" + metadata = self._get_metadata_dict() + output_model.set_data("metadata", json.dumps(metadata).encode()) + + def _get_metadata_dict(self) -> Dict[str, Any]: + """Returns metadata dict.""" + image_threshold = ( + self.model.image_threshold.value.cpu().numpy().tolist() if hasattr(self.model, "image_threshold") else 0.5 + ) + pixel_threshold = ( + self.model.pixel_threshold.value.cpu().numpy().tolist() if hasattr(self.model, "pixel_threshold") else 0.5 + ) + min = None + max = None + if hasattr(self.model, "normalization_metrics"): + min = self.model.normalization_metrics.state_dict()["min"].cpu().numpy().tolist() + max = self.model.normalization_metrics.state_dict()["max"].cpu().numpy().tolist() + else: + logger.warning( + "The model was not trained before saving. This will lead to incorrect normalization of the heatmaps." + ) + transform = get_transforms( + config=self.config.dataset.transform_config.train, + image_size=tuple(self.config.dataset.image_size), + to_tensor=True, + ) + if hasattr(self, "trainer") and hasattr(self.trainer, "datamodule"): + if hasattr(self.trainer.datamodule, "test_otx_dataset"): + transform = self.trainer.datamodule.test_dataloader().dataset.transform + else: + transform = self.trainer.datamodule.train_dataloader().dataset.transform + metadata = { + # TODO: Replace with transform.to_dict() when OTX supports albumentations 1.3.0 + "transform": {"transform": transform._to_dict()}, + "image_threshold": image_threshold, + "pixel_threshold": pixel_threshold, + "image_shape": list(self.config.model.input_size), + } + if min is not None and max is not None: + metadata["min"] = min + metadata["max"] = max + # Set the task type for inferencer + metadata["task"] = str(self.task_type).lower().split("_")[-1] + return metadata + + @staticmethod + def _is_docker() -> bool: + """Check whether the task runs in docker container. + + Returns: + bool: True if task runs in docker, False otherwise. + """ + path = "/proc/self/cgroup" + is_in_docker = False + if os.path.isfile(path): + with open(path, encoding="utf-8") as file: + is_in_docker = is_in_docker or any("docker" in line for line in file) + is_in_docker = is_in_docker or os.path.exists("/.dockerenv") + return is_in_docker + + def unload(self) -> None: + """Unload the task.""" + self.cleanup() + + if self._is_docker(): + logger.warning("Got unload request. Unloading models. Throwing Segmentation Fault on purpose") + ctypes.string_at(0) + + else: + logger.warning("Got unload request, but not on Docker. Only clearing CUDA cache") + torch.cuda.empty_cache() + logger.warning( + "Done unloading. Torch is still occupying %f bytes of GPU memory", + torch.cuda.memory_allocated(), + ) + + def cleanup(self) -> None: + """Clean up work directory.""" + if self._work_dir_is_temp and os.path.exists(self.config.project.path): + shutil.rmtree(self.config.project.path, ignore_errors=False) diff --git a/src/otx/algorithms/anomaly/tasks/nncf.py b/src/otx/algorithms/anomaly/tasks/nncf.py new file mode 100644 index 00000000000..006ca11afad --- /dev/null +++ b/src/otx/algorithms/anomaly/tasks/nncf.py @@ -0,0 +1,247 @@ +"""Anomaly Classification Task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from __future__ import annotations + +import io +import json +import os +import re +from typing import Dict, Optional + +import torch +from anomalib.models import AnomalyModule, get_model +from anomalib.post_processing import NormalizationMethod, ThresholdMethod +from anomalib.utils.callbacks import ( + MetricsConfigurationCallback, + MinMaxNormalizationCallback, + PostProcessingConfigurationCallback, +) +from anomalib.utils.callbacks.nncf.callback import NNCFCallback +from anomalib.utils.callbacks.nncf.utils import ( + compose_nncf_config, + is_state_nncf, + wrap_nncf_model, +) +from pytorch_lightning import Trainer +from torch.utils.data.dataloader import DataLoader + +from otx.algorithms.anomaly.adapters.anomalib.callbacks import ProgressCallback +from otx.algorithms.anomaly.adapters.anomalib.data import OTXAnomalyDataModule +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model import ( + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.optimization_interface import ( + IOptimizationTask, + OptimizationType, +) +from otx.utils.logger import get_logger + +from .inference import InferenceTask + +logger = get_logger() + + +class NNCFTask(InferenceTask, IOptimizationTask): + """Base Anomaly Task.""" + + def __init__(self, task_environment: TaskEnvironment, **kwargs) -> None: + """Task for compressing models using NNCF. + + Args: + task_environment (TaskEnvironment): OTX Task environment. + **kwargs: Addition keyword arguments. + """ + self.compression_ctrl = None + self.nncf_preset = "nncf_quantization" + super().__init__(task_environment, **kwargs) + self.optimization_type = ModelOptimizationType.NNCF + + def _set_attributes_by_hyperparams(self): + quantization = self.hyper_parameters.nncf_optimization.enable_quantization + pruning = self.hyper_parameters.nncf_optimization.enable_pruning + if quantization and pruning: + self.nncf_preset = "nncf_quantization_pruning" + self.optimization_methods = [ + OptimizationMethod.QUANTIZATION, + OptimizationMethod.FILTER_PRUNING, + ] + self.precision = [ModelPrecision.INT8] + return + if quantization and not pruning: + self.nncf_preset = "nncf_quantization" + self.optimization_methods = [OptimizationMethod.QUANTIZATION] + self.precision = [ModelPrecision.INT8] + return + if not quantization and pruning: + self.nncf_preset = "nncf_pruning" + self.optimization_methods = [OptimizationMethod.FILTER_PRUNING] + self.precision = [ModelPrecision.FP32] + return + raise RuntimeError("Not selected optimization algorithm") + + def load_model(self, otx_model: Optional[ModelEntity]) -> AnomalyModule: + """Create and Load Anomalib Module from OTX Model. + + This method checks if the task environment has a saved OTX Model, + and creates one. If the OTX model already exists, it returns the + the model with the saved weights. + + Args: + otx_model (Optional[ModelEntity]): OTX Model from the + task environment. + + Returns: + AnomalyModule: Anomalib + classification or segmentation model with/without weights. + """ + nncf_config_path = os.path.join(self.base_dir, "compression_config.json") + + with open(nncf_config_path, encoding="utf8") as nncf_config_file: + common_nncf_config = json.load(nncf_config_file) + + self._set_attributes_by_hyperparams() + self.optimization_config = compose_nncf_config(common_nncf_config, [self.nncf_preset]) + self.config.merge_with(self.optimization_config) + model = get_model(config=self.config) + if otx_model is None: + raise ValueError("No trained model in project. NNCF require pretrained weights to compress the model") + + buffer = io.BytesIO(otx_model.get_data("weights.pth")) # type: ignore + model_data = torch.load(buffer, map_location=torch.device("cpu")) + + if is_state_nncf(model_data): + logger.info("Loaded model weights from Task Environment and wrapped by NNCF") + + # Fix name mismatch for wrapped model by pytorch_lighting + nncf_modules = {} + pl_modules = {} + for key in model_data["model"].keys(): + if key.startswith("model."): + new_key = key.replace("model.", "") + res = re.search(r"(\w+)_feature_extractor\.(.*)", new_key) + if res: + new_key = f"{res.group(1)}_model.feature_extractor.{res.group(2)}" + nncf_modules[new_key] = model_data["model"][key] + else: + pl_modules[key] = model_data["model"][key] + model_data["model"] = nncf_modules + + dataloader: DataLoader | None = None + if hasattr(self, "trainer") and hasattr(self.trainer, "datamodule"): + if self.trainer.datamodule.train_dataset is not None: + dataloader = self.trainer.datamodule.train_dataloader() + elif self.trainer.datamodule.test_dataset is not None: + dataloader = self.trainer.datamodule.test_dataloader() + + self.compression_ctrl, model.model = wrap_nncf_model( + model.model, + self.optimization_config["nncf_config"], + dataloader=dataloader, # type:ignore + init_state_dict=model_data, + ) + # Load extra parameters of pytorch_lighting model + model.load_state_dict(pl_modules, strict=False) + else: + try: + model.load_state_dict(model_data["model"]) + logger.info("Loaded model weights from Task Environment") + except BaseException as exception: + raise ValueError("Could not load the saved model. The model file structure is invalid.") from exception + + return model + + def optimize( + self, + optimization_type: OptimizationType, + dataset: DatasetEntity, + output_model: ModelEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + ): + """Train the anomaly classification model. + + Args: + optimization_type (OptimizationType): Type of optimization. + dataset (DatasetEntity): Input dataset. + output_model (ModelEntity): Output model to save the model weights. + optimization_parameters (OptimizationParameters): Training parameters + """ + logger.info("Optimization the model.") + + if optimization_type is not OptimizationType.NNCF: + raise RuntimeError("NNCF is the only supported optimization") + + datamodule = OTXAnomalyDataModule(config=self.config, dataset=dataset, task_type=self.task_type) + nncf_callback = NNCFCallback(config=self.optimization_config["nncf_config"]) + metrics_configuration = MetricsConfigurationCallback( + task=self.config.dataset.task, + image_metrics=self.config.metrics.image, + pixel_metrics=self.config.metrics.get("pixel"), + ) + post_processing_configuration = PostProcessingConfigurationCallback( + normalization_method=NormalizationMethod.MIN_MAX, + threshold_method=ThresholdMethod.ADAPTIVE, + manual_image_threshold=self.config.metrics.threshold.manual_image, + manual_pixel_threshold=self.config.metrics.threshold.manual_pixel, + ) + callbacks = [ + ProgressCallback(parameters=optimization_parameters), + MinMaxNormalizationCallback(), + nncf_callback, + metrics_configuration, + post_processing_configuration, + ] + + self.trainer = Trainer(**self.config.trainer, logger=False, callbacks=callbacks) + self.trainer.fit(model=self.model, datamodule=datamodule) + self.compression_ctrl = nncf_callback.nncf_ctrl + output_model.model_format = ModelFormat.BASE_FRAMEWORK + output_model.optimization_type = ModelOptimizationType.NNCF + self.save_model(output_model) + + logger.info("Training completed.") + + def model_info(self) -> Dict: + """Return model info to save the model weights. + + Returns: + Dict: Model info. + """ + return { + "compression_state": self.compression_ctrl.get_compression_state(), # type: ignore + "meta": { + "config": self.config, + "nncf_enable_compression": True, + }, + "model": self.model.state_dict(), + "config": self.get_config(), + "VERSION": 1, + } + + def _export_to_onnx(self, onnx_path: str): + """Export model to ONNX. + + Args: + onnx_path (str): path to save ONNX file + """ + self.compression_ctrl.export_model(onnx_path, "onnx_11") # type: ignore diff --git a/src/otx/algorithms/anomaly/tasks/openvino.py b/src/otx/algorithms/anomaly/tasks/openvino.py new file mode 100644 index 00000000000..922ff2b3fd9 --- /dev/null +++ b/src/otx/algorithms/anomaly/tasks/openvino.py @@ -0,0 +1,544 @@ +"""OpenVINO Anomaly Task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import io +import json +import os +import random +import tempfile +from typing import Any, Dict, List, Optional, Tuple, Union +from zipfile import ZipFile + +import nncf +import numpy as np +import openvino.runtime as ov +from addict import Dict as ADDict +from anomalib.data.utils.transform import get_transforms +from nncf.common.quantization.structs import QuantizationPreset +from omegaconf import OmegaConf +from openvino.model_api.models import AnomalyDetection, AnomalyResult + +from otx.algorithms.anomaly.adapters.anomalib.config import get_anomalib_config +from otx.algorithms.anomaly.configs.base.configuration import BaseAnomalyConfig +from otx.algorithms.common.utils import embed_ir_model_data +from otx.algorithms.common.utils.ir import check_if_quantized +from otx.algorithms.common.utils.utils import read_py_config +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import ( + InferenceParameters, + default_progress_callback, +) +from otx.api.entities.model import ( + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.model_template import TaskType +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.result_media import ResultMediaEntity +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.serialization.label_mapper import LabelSchemaMapper, label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.evaluation.performance_provider_interface import ( + IPerformanceProvider, +) +from otx.api.usecases.exportable_code import demo +from otx.api.usecases.tasks.interfaces.deployment_interface import IDeploymentTask +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.optimization_interface import ( + IOptimizationTask, + OptimizationType, +) +from otx.api.utils.anomaly_utils import create_detection_annotation_from_anomaly_heatmap +from otx.api.utils.segmentation_utils import create_annotation_from_segmentation_map +from otx.utils.logger import get_logger + +logger = get_logger() + + +class OTXNNCFAnomalyDataloader: + """Dataloader for loading OTX dataset for NNCF optimization. + + Args: + dataset (DatasetEntity): OTX dataset entity + model: (AnomalyDetection) The modelAPI model used for fetching the transforms. + shuffle (bool, optional): Shuffle dataset. Defaults to True. + """ + + def __init__( + self, + dataset: DatasetEntity, + model: AnomalyDetection, + shuffle: bool = True, + ): + self.dataset = dataset + self.model = model + self.shuffler = None + if shuffle: + self.shuffler = list(range(len(dataset))) + random.shuffle(self.shuffler) + + def __getitem__(self, index: int): + """Get dataset item. + + Args: + index (int): Index of the dataset sample. + + Returns: + Dataset item. + """ + if self.shuffler is not None: + index = self.shuffler[index] + + image = self.dataset[index].numpy + annotation = self.dataset[index].annotation_scene + + resized_image = self.model.resize(image, (self.model.w, self.model.h)) + resized_image = self.model.input_transform(resized_image) + resized_image = self.model._change_layout(resized_image) + + return (index, annotation), resized_image + + def __len__(self) -> int: + """Get size of the dataset. + + Returns: + int: Size of the dataset. + """ + return len(self.dataset) + + +class OpenVINOTask(IInferenceTask, IEvaluationTask, IOptimizationTask, IDeploymentTask): + """OpenVINO inference task. + + Args: + task_environment (TaskEnvironment): task environment of the trained anomaly model + """ + + def __init__(self, task_environment: TaskEnvironment) -> None: + logger.info("Initializing the OpenVINO task.") + self.task_environment = task_environment + self.task_type = self.task_environment.model_template.task_type + self.config = self.get_config() + self.inference_model = self.get_openvino_model() + + labels = self.task_environment.get_labels() + self.normal_label = [label for label in labels if not label.is_anomalous][0] + self.anomalous_label = [label for label in labels if label.is_anomalous][0] + + template_file_path = task_environment.model_template.model_template_path + self._base_dir = os.path.abspath(os.path.dirname(template_file_path)) + + def get_config(self) -> ADDict: + """Get Anomalib Config from task environment. + + Returns: + ADDict: Anomalib config + """ + task_name = self.task_environment.model_template.name + otx_config: ConfigurableParameters = self.task_environment.get_hyper_parameters() + config = get_anomalib_config(task_name=task_name, otx_config=otx_config) + return ADDict(OmegaConf.to_container(config)) + + def infer(self, dataset: DatasetEntity, inference_parameters: InferenceParameters) -> DatasetEntity: + """Perform Inference. + + Args: + dataset (DatasetEntity): Inference dataset + inference_parameters (InferenceParameters): Inference parameters. + + Returns: + DatasetEntity: Output dataset storing inference predictions. + """ + if self.task_environment.model is None: + raise Exception("task_environment.model is None. Cannot access threshold to calculate labels.") + + logger.info("Start OpenVINO inference.") + update_progress_callback = default_progress_callback + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress # type: ignore + + for idx, dataset_item in enumerate(dataset): + image_result: AnomalyResult = self.inference_model(dataset_item.numpy) + + # TODO: inferencer should return predicted label and mask + pred_label = image_result.pred_label + pred_mask = image_result.pred_mask + probability = image_result.pred_score if pred_label == "Anomaly" else 1 - image_result.pred_score + if self.task_type == TaskType.ANOMALY_CLASSIFICATION: + label = self.anomalous_label if image_result.pred_score >= 0.5 else self.normal_label + elif self.task_type == TaskType.ANOMALY_SEGMENTATION: + annotations = create_annotation_from_segmentation_map( + pred_mask, + image_result.anomaly_map.squeeze() / 255.0, + {0: self.normal_label, 1: self.anomalous_label}, + ) + dataset_item.append_annotations(annotations) + label = self.normal_label if len(annotations) == 0 else self.anomalous_label + elif self.task_type == TaskType.ANOMALY_DETECTION: + annotations = create_detection_annotation_from_anomaly_heatmap( + pred_mask, + image_result.anomaly_map.squeeze() / 255.0, + {0: self.normal_label, 1: self.anomalous_label}, + ) + dataset_item.append_annotations(annotations) + label = self.normal_label if len(annotations) == 0 else self.anomalous_label + else: + raise ValueError(f"Unknown task type: {self.task_type}") + + dataset_item.append_labels([ScoredLabel(label=label, probability=float(probability))]) + heatmap_media = ResultMediaEntity( + name="Anomaly Map", + type="anomaly_map", + label=label, + annotation_scene=dataset_item.annotation_scene, + numpy=image_result.anomaly_map, + ) + dataset_item.append_metadata_item(heatmap_media) + update_progress_callback(int((idx + 1) / len(dataset) * 100)) + + return dataset + + def get_metadata(self) -> Dict: + """Get Meta Data.""" + metadata = {} + if self.task_environment.model is not None: + try: + metadata = json.loads(self.task_environment.model.get_data("metadata").decode()) + self._populate_metadata(metadata) + logger.info("Metadata loaded from model v1.4.") + except (KeyError, json.decoder.JSONDecodeError): + # model is from version 1.2.x + metadata = self._populate_metadata_legacy(self.task_environment.model) + logger.info("Metadata loaded from model v1.2.x.") + else: + raise ValueError("Cannot access meta-data. self.task_environment.model is empty.") + + return metadata + + def _populate_metadata_legacy(self, model: ModelEntity) -> Dict[str, Any]: + """Populates metadata for models for version 1.2.x.""" + image_threshold = np.frombuffer(model.get_data("image_threshold"), dtype=np.float32) + pixel_threshold = np.frombuffer(model.get_data("pixel_threshold"), dtype=np.float32) + min_value = np.frombuffer(model.get_data("min"), dtype=np.float32) + max_value = np.frombuffer(model.get_data("max"), dtype=np.float32) + transform = get_transforms( + config=self.config.dataset.transform_config.train, + image_size=tuple(self.config.dataset.image_size), + to_tensor=True, + ) + metadata = { + # TODO: Replace with transform.to_dict() when OTX supports albumentations 1.3.0 + "transform": {"transform": transform._to_dict()}, + "image_threshold": image_threshold, + "pixel_threshold": pixel_threshold, + "min": min_value, + "max": max_value, + "task": str(self.task_type).lower().split("_")[-1], + } + return metadata + + def _populate_metadata(self, metadata: Dict[str, Any]): + """Populates metadata for models from version 1.4 onwards.""" + metadata["image_threshold"] = np.array(metadata["image_threshold"], dtype=np.float32).item() + metadata["pixel_threshold"] = np.array(metadata["pixel_threshold"], dtype=np.float32).item() + metadata["min"] = np.array(metadata["min"], dtype=np.float32).item() + metadata["max"] = np.array(metadata["max"], dtype=np.float32).item() + + def evaluate(self, output_resultset: ResultSetEntity, evaluation_metric: Optional[str] = None): + """Evaluate the performance of the model. + + Args: + output_resultset (ResultSetEntity): Result set storing ground truth and predicted dataset. + evaluation_metric (Optional[str], optional): Evaluation metric. Defaults to None. + """ + metric: IPerformanceProvider + if self.task_type == TaskType.ANOMALY_CLASSIFICATION: + metric = MetricsHelper.compute_f_measure(output_resultset) + elif self.task_type == TaskType.ANOMALY_DETECTION: + metric = MetricsHelper.compute_anomaly_detection_scores(output_resultset) + elif self.task_type == TaskType.ANOMALY_SEGMENTATION: + metric = MetricsHelper.compute_anomaly_segmentation_scores(output_resultset) + else: + raise ValueError(f"Unknown task type: {self.task_type}") + output_resultset.performance = metric.get_performance() + + def _get_optimization_algorithms_config(self) -> ADDict: + """Returns list of optimization algorithms configurations.""" + hparams: BaseAnomalyConfig = self.task_environment.get_hyper_parameters() + + optimization_config_path = os.path.join(self._base_dir, "ptq_optimization_config.py") + ptq_config = ADDict() + if os.path.exists(optimization_config_path): + ptq_config = read_py_config(optimization_config_path) + ptq_config.update( + subset_size=hparams.pot_parameters.stat_subset_size, + preset=QuantizationPreset(hparams.pot_parameters.preset.name.lower()), + ) + + return ptq_config + + def optimize( + self, + optimization_type: OptimizationType, + dataset: DatasetEntity, + output_model: ModelEntity, + optimization_parameters: Optional[OptimizationParameters], + ): + """Optimize the model. + + Args: + optimization_type (OptimizationType): Type of optimization [POT or NNCF] + dataset (DatasetEntity): Input Dataset. + output_model (ModelEntity): Output model. + optimization_parameters (Optional[OptimizationParameters]): Optimization parameters. + + Raises: + ValueError: When the optimization type is not POT, which is the only support type at the moment. + """ + if optimization_type is not OptimizationType.POT: + raise ValueError("PTQ is the only supported optimization type for OpenVINO models") + + # Training subset does not contain example of anomalous images. + # Anomalous examples from all dataset used to get statistics for quantization. + dataset = DatasetEntity( + items=[item for item in dataset if item.get_shapes_labels()[0].is_anomalous], purpose=dataset.purpose + ) + + logger.info("Starting PTQ optimization.") + data_loader = OTXNNCFAnomalyDataloader(dataset=dataset, model=self.inference_model) + quantization_dataset = nncf.Dataset(data_loader, lambda data: data[1]) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, "model.xml") + bin_path = os.path.join(tempdir, "model.bin") + + if self.task_environment.model is not None: + self.__save_weights(xml_path, self.task_environment.model.get_data("openvino.xml")) + self.__save_weights(bin_path, self.task_environment.model.get_data("openvino.bin")) + else: + raise ValueError("Cannot save the weights. self.task_environment.model is None.") + + ov_model = ov.Core().read_model(xml_path) + if check_if_quantized(ov_model): + raise RuntimeError("Model is already optimized by PTQ") + + if optimization_parameters is not None: + optimization_parameters.update_progress(10, None) + + quantization_config = self._get_optimization_algorithms_config() + quantization_config.subset_size = min(quantization_config.subset_size, len(data_loader)) + + compressed_model = nncf.quantize(ov_model, quantization_dataset, **quantization_config) + + if optimization_parameters is not None: + optimization_parameters.update_progress(90, None) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, "model.xml") + ov.save_model(compressed_model, xml_path) + self.__load_weights(path=xml_path, output_model=output_model, key="openvino.xml") + self.__load_weights(path=os.path.join(tempdir, "model.bin"), output_model=output_model, key="openvino.bin") + + output_model.set_data("label_schema.json", label_schema_to_bytes(self.task_environment.label_schema)) + output_model.model_format = ModelFormat.OPENVINO + output_model.optimization_type = ModelOptimizationType.POT + output_model.optimization_methods = [OptimizationMethod.QUANTIZATION] + output_model.precision = [ModelPrecision.INT8] + metadata = self.get_metadata() + output_model.set_data("metadata", json.dumps(metadata).encode()) + + self.task_environment.model = output_model + self.inference_model = self.get_openvino_model() + + if optimization_parameters is not None: + optimization_parameters.update_progress(100, None) + logger.info("PTQ optimization completed") + + def get_openvino_model(self) -> AnomalyDetection: + """Create the OpenVINO inferencer object. + + Returns: + AnomalyDetection model + """ + if self.task_environment.model is None: + raise Exception("task_environment.model is None. Cannot load weights.") + try: + model = AnomalyDetection.create_model( + model=self.task_environment.model.get_data("openvino.xml"), + weights_path=self.task_environment.model.get_data("openvino.bin"), + ) + except RuntimeError as exception: + logger.exception(exception) + logger.info("Possibly a legacy model is being loaded.") + self._create_from_legacy() + model = AnomalyDetection.create_model( + model=self.task_environment.model.get_data("openvino.xml"), + weights_path=self.task_environment.model.get_data("openvino.bin"), + ) + + return model + + def _create_from_legacy(self) -> None: + """Generates an OpenVINO model in new format from the legacy model. + + TODO: This needs to be removed once all projects in Geti have been migrated to the newer version. + + Args: + model_file (str): The XML model file. + """ + extra_model_data = self._metadata_in_ir_format() + + for key, value in extra_model_data.items(): + if isinstance(value, np.ndarray): + extra_model_data[key] = value.tolist() + + with tempfile.TemporaryDirectory() as temp_dir: + xml_data = self.task_environment.model.get_data("openvino.xml") + bin_data = self.task_environment.model.get_data("openvino.bin") + with open(f"{temp_dir}/openvino.xml", "wb") as file: + file.write(xml_data) + with open(f"{temp_dir}/openvino.bin", "wb") as file: + file.write(bin_data) + embed_ir_model_data(f"{temp_dir}/openvino.xml", extra_model_data) + with open(f"{temp_dir}/openvino.xml", "rb") as file: + self.task_environment.model.set_data("openvino.xml", file.read()) + with open(f"{temp_dir}/openvino.bin", "rb") as file: + self.task_environment.model.set_data("openvino.bin", file.read()) + + def _metadata_in_ir_format(self) -> Dict[Tuple[str, str], Union[str, int, float, List[Union[int, float]]]]: + """Return metadata in format of tuple keys that are used in IR with modelAPI.""" + metadata = self.get_metadata() + extra_model_data: Dict[Tuple[str, str], Any] = {} + for key, value in metadata.items(): + if key in ("transform", "min", "max"): + continue + extra_model_data[("model_info", key)] = value + # Add transforms + if "transform" in metadata: + for transform_dict in metadata["transform"]["transform"]["transforms"]: + transform = transform_dict.pop("__class_fullname__") + if transform == "Normalize": + extra_model_data[("model_info", "mean_values")] = self._serialize_list( + [x * 255.0 for x in transform_dict["mean"]] + ) + extra_model_data[("model_info", "scale_values")] = self._serialize_list( + [x * 255.0 for x in transform_dict["std"]] + ) + elif transform == "Resize": + extra_model_data[("model_info", "orig_height")] = transform_dict["height"] + extra_model_data[("model_info", "orig_width")] = transform_dict["width"] + else: + logger.warn(f"Transform {transform} is not supported currently") + # Since we only need the diff of max and min, we fuse the min and max into one op + if "min" in metadata and "max" in metadata: + extra_model_data[("model_info", "normalization_scale")] = metadata["max"] - metadata["min"] + + extra_model_data[("model_info", "reverse_input_channels")] = False + extra_model_data[("model_info", "model_type")] = "AnomalyDetection" + extra_model_data[("model_info", "labels")] = "Normal Anomaly" + return extra_model_data + + def _serialize_list(self, arr: Union[Tuple, List]) -> str: + """Converts a list to space separated string.""" + return " ".join(map(str, arr)) + + @staticmethod + def __save_weights(path: str, data: bytes) -> None: + """Write data to file. + + Args: + path (str): Path of output file + data (bytes): Data to write + """ + with open(path, "wb") as file: + file.write(data) + + @staticmethod + def __load_weights(path: str, output_model: ModelEntity, key: str) -> None: + """Load weights into output model. + + Args: + path (str): Path to weights + output_model (ModelEntity): Model to which the weights are assigned + key (str): Key of the output model into which the weights are assigned + """ + with open(path, "rb") as file: + output_model.set_data(key, file.read()) + + def _get_openvino_configuration(self) -> Dict[str, Any]: + """Return configuration required by the exported model.""" + if self.task_environment.model is None: + raise Exception("task_environment.model is None. Cannot get configuration.") + + configuration: Dict[str, Any] = { + "labels": LabelSchemaMapper.forward(self.task_environment.label_schema), + } + # Add new IR keys to parameters + for key, value in self._metadata_in_ir_format().items(): + # since the same key is used to store label info in OTX SDK format + if key[1] == "labels": + assert isinstance(value, str) + configuration["modelapi_labels"] = [name for name in value.split(" ")] + elif key[1] in ("mean_values", "scale_values"): + assert isinstance(value, str) + configuration[key[1]] = [float(x) for x in value.split(" ")] + else: + configuration[key[1]] = value + + return configuration + + def deploy(self, output_model: ModelEntity) -> None: + """Exports the weights from ``output_model`` along with exportable code. + + Args: + output_model (ModelEntity): Model with ``openvino.xml`` and ``.bin`` keys + + Raises: + Exception: If ``task_environment.model`` is None + """ + logger.info("Deploying Model") + + if self.task_environment.model is None: + raise Exception("task_environment.model is None. Cannot load weights.") + + work_dir = os.path.dirname(demo.__file__) + parameters: Dict[str, Any] = {} + + task_type = str(self.task_type).lower() + + parameters["type_of_model"] = "AnomalyDetection" + parameters["converter_type"] = task_type.upper() + parameters["model_parameters"] = self._get_openvino_configuration() + zip_buffer = io.BytesIO() + with ZipFile(zip_buffer, "w") as arch: + # model files + arch.writestr(os.path.join("model", "model.xml"), self.task_environment.model.get_data("openvino.xml")) + arch.writestr(os.path.join("model", "model.bin"), self.task_environment.model.get_data("openvino.bin")) + arch.writestr(os.path.join("model", "config.json"), json.dumps(parameters, ensure_ascii=False, indent=4)) + # other python files + arch.write(os.path.join(work_dir, "requirements.txt"), os.path.join("python", "requirements.txt")) + arch.write(os.path.join(work_dir, "LICENSE"), os.path.join("python", "LICENSE")) + arch.write(os.path.join(work_dir, "demo.py"), os.path.join("python", "demo.py")) + arch.write(os.path.join(work_dir, "README.md"), os.path.join(".", "README.md")) + output_model.exportable_code = zip_buffer.getvalue() + logger.info("Deployment completed.") diff --git a/src/otx/algorithms/anomaly/tasks/train.py b/src/otx/algorithms/anomaly/tasks/train.py new file mode 100644 index 00000000000..1089f7203b7 --- /dev/null +++ b/src/otx/algorithms/anomaly/tasks/train.py @@ -0,0 +1,152 @@ +"""Anomaly Classification Task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import io +from typing import Optional + +import torch +from anomalib.models import AnomalyModule, get_model +from anomalib.post_processing import NormalizationMethod, ThresholdMethod +from anomalib.utils.callbacks import ( + MetricsConfigurationCallback, + MinMaxNormalizationCallback, + PostProcessingConfigurationCallback, +) +from pytorch_lightning import Trainer, seed_everything +from pytorch_lightning.loggers.csv_logs import CSVLogger + +from otx.algorithms.anomaly.adapters.anomalib.callbacks import IterationTimer, ProgressCallback +from otx.algorithms.anomaly.adapters.anomalib.data import OTXAnomalyDataModule +from otx.algorithms.anomaly.adapters.anomalib.plugins.xpu_precision import MixedPrecisionXPUPlugin +from otx.algorithms.common.utils.utils import is_xpu_available +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.tasks.interfaces.training_interface import ITrainingTask +from otx.utils.logger import get_logger + +from .inference import InferenceTask + +logger = get_logger() + + +class TrainingTask(InferenceTask, ITrainingTask): + """Base Anomaly Task.""" + + def train( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + train_parameters: TrainParameters, + seed: Optional[int] = None, + deterministic: bool = False, + ) -> None: + """Train the anomaly classification model. + + Args: + dataset (DatasetEntity): Input dataset. + output_model (ModelEntity): Output model to save the model weights. + train_parameters (TrainParameters): Training parameters + seed (Optional[int]): Setting seed to a value other than 0 + deterministic (bool): Setting PytorchLightning trainer's deterministic flag. + """ + logger.info("Training the model.") + + config = self.get_config() + + if seed: + logger.info(f"Setting seed to {seed}") + seed_everything(seed, workers=True) + config.trainer.deterministic = "warn" if deterministic else deterministic + + logger.info("Training Configs '%s'", config) + + datamodule = OTXAnomalyDataModule(config=config, dataset=dataset, task_type=self.task_type) + callbacks = [ + ProgressCallback(parameters=train_parameters), + MinMaxNormalizationCallback(), + MetricsConfigurationCallback( + task=config.dataset.task, + image_metrics=config.metrics.image, + pixel_metrics=config.metrics.get("pixel"), + ), + PostProcessingConfigurationCallback( + normalization_method=NormalizationMethod.MIN_MAX, + threshold_method=ThresholdMethod.ADAPTIVE, + manual_image_threshold=config.metrics.threshold.manual_image, + manual_pixel_threshold=config.metrics.threshold.manual_pixel, + ), + IterationTimer(on_step=False), + ] + + plugins = [] + if config.trainer.plugins is not None: + plugins.extend(config.trainer.plugins) + config.trainer.pop("plugins") + + if is_xpu_available(): + config.trainer.strategy = "xpu_single" + config.trainer.accelerator = "xpu" + if config.trainer.precision == 16: + plugins.append(MixedPrecisionXPUPlugin()) + + self.trainer = Trainer( + **config.trainer, logger=CSVLogger(self.project_path, name=""), callbacks=callbacks, plugins=plugins + ) + self.trainer.fit(model=self.model, datamodule=datamodule) + + self.save_model(output_model) + + logger.info("Training completed.") + + def load_model(self, otx_model: Optional[ModelEntity]) -> AnomalyModule: + """Create and Load Anomalib Module from OTX Model. + + This method checks if the task environment has a saved OTX Model, + and creates one. If the OTX model already exists, it returns the + the model with the saved weights. + + Args: + otx_model (Optional[ModelEntity]): OTX Model from the + task environment. + + Returns: + AnomalyModule: Anomalib + classification or segmentation model with/without weights. + """ + model = get_model(config=self.config) + if otx_model is None: + logger.info( + "No trained model in project yet. Created new model with '%s'", + self.model_name, + ) + else: + buffer = io.BytesIO(otx_model.get_data("weights.pth")) + model_data = torch.load(buffer, map_location=torch.device("cpu")) + + try: + if model_data["config"]["model"]["backbone"] == self.config["model"]["backbone"]: + model.load_state_dict(model_data["model"]) + logger.info("Loaded model weights from Task Environment") + else: + logger.info( + "Model backbone does not match. Created new model with '%s'", + self.model_name, + ) + except BaseException as exception: + raise ValueError("Could not load the saved model. The model file structure is invalid.") from exception + + return model diff --git a/src/otx/algorithms/anomaly/tools/README.md b/src/otx/algorithms/anomaly/tools/README.md new file mode 100644 index 00000000000..a09f6a5b44a --- /dev/null +++ b/src/otx/algorithms/anomaly/tools/README.md @@ -0,0 +1,23 @@ +OpenVINO Training Extension interacts with the anomaly detection library ([Anomalib](https://github.com/openvinotoolkit/anomalib)) by providing interfaces in the `external/anomaly` of this repository. The `sample.py` file contained in this folder serves as an end-to-end example of how these interfaces are used. To begin using this script, first ensure that `otx_cli`, `otx_sdk` and `external/anomaly` dependencies are installed. + +To get started, we provide a handy script in `adapters/anomalib/data/create_mvtec_ad_json_annotations.py` to help generate annotation json files for MVTec dataset. Assuming that you have placed the MVTec dataset in a directory your home folder (`~/dataset/MVTec`), you can run the following command to generate the annotations. + +```bash +python create_mvtec_ad_json_annotations.py --data_path ~/datasets/MVTec --annotation_path ~/training_extensions/data/MVtec/ +``` + +This will generate three folders in `~/training_extensions/data/MVtec/` for classification, segmentation and detection task. + +Then, to run sample.py you can use the following command. + +```bash +python tools/sample.py \ + --dataset_path ~/datasets/MVTec \ + --category bottle \ + --train-ann-files ../../data/MVtec/bottle/segmentation/train.json \ + --val-ann-files ../../data/MVtec/bottle/segmentation/val.json \ + --test-ann-files ../../data/MVtec/bottle/segmentation/test.json \ + --model_template_path ./configs/anomaly_segmentation/padim/template.yaml +``` + +Optionally, you can also optimize to `nncf` or `ptq` by using the `--optimization` flag diff --git a/src/otx/algorithms/anomaly/tools/__init__.py b/src/otx/algorithms/anomaly/tools/__init__.py new file mode 100644 index 00000000000..893bfe23484 --- /dev/null +++ b/src/otx/algorithms/anomaly/tools/__init__.py @@ -0,0 +1,15 @@ +"""Collection of tools to run anomaly training extension.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/anomaly/tools/sample.py b/src/otx/algorithms/anomaly/tools/sample.py new file mode 100644 index 00000000000..a8433defb51 --- /dev/null +++ b/src/otx/algorithms/anomaly/tools/sample.py @@ -0,0 +1,393 @@ +"""`sample.py`. + +This is a sample python script showing how to train an end-to-end OTX Anomaly Classification Task. +""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import argparse +import importlib +import os +import shutil +from argparse import Namespace +from typing import Any, Dict, Optional, Type, Union + +from otx.algorithms.anomaly.adapters.anomalib.data.dataset import ( + AnomalyClassificationDataset, + AnomalyDetectionDataset, + AnomalySegmentationDataset, +) +from otx.algorithms.anomaly.tasks import NNCFTask, OpenVINOTask +from otx.api.configuration.helper import create as create_hyper_parameters +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import TaskType, parse_model_template +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.adapters.model_adapter import ModelAdapter +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-instance-attributes +class OtxAnomalyTask: + """OTX Anomaly Classification Task.""" + + def __init__( + self, + dataset_path: str, + train_subset: Dict[str, str], + val_subset: Dict[str, str], + test_subset: Dict[str, str], + model_template_path: str, + seed: Optional[int] = None, + ) -> None: + """Initialize OtxAnomalyTask. + + Args: + dataset_path (str): Path to the MVTec dataset. + train_subset (Dict[str, str]): Dictionary containing path to train annotation file and path to dataset. + val_subset (Dict[str, str]): Dictionary containing path to validation annotation file and path to dataset. + test_subset (Dict[str, str]): Dictionary containing path to test annotation file and path to dataset. + model_template_path (str): Path to model template. + seed (Optional[int]): Setting seed to a value other than 0 also marks PytorchLightning trainer's + deterministic flag to True. + + Example: + >>> import os + >>> os.getcwd() + '~/otx/external/anomaly' + + If MVTec dataset is placed under the above directory, then we could run, + + >>> model_template_path = "./configs/classification/padim/template.yaml" + >>> dataset_path = "./datasets/MVTec" + >>> task = OtxAnomalyTask( + ... dataset_path=dataset_path, + ... train_subset={"ann_file": train.json, "data_root": dataset_path}, + ... val_subset={"ann_file": val.json, "data_root": dataset_path}, + ... test_subset={"ann_file": test.json, "data_root": dataset_path}, + ... model_template_path=model_template_path + ... ) + + >>> task.train() + Performance(score: 1.0, dashboard: (1 metric groups)) + + >>> task.export() + Performance(score: 0.9756097560975608, dashboard: (1 metric groups)) + """ + logger.info("Loading the model template.") + self.model_template = parse_model_template(model_template_path) + + logger.info("Loading MVTec dataset.") + self.task_type = self.model_template.task_type + + dataclass = self.get_dataclass() + + self.dataset = dataclass(train_subset, val_subset, test_subset) + + logger.info("Creating the task-environment.") + self.task_environment = self.create_task_environment() + + logger.info("Creating the base Torch and OpenVINO tasks.") + self.torch_task = self.create_task(task="base") + + self.trained_model: ModelEntity + self.openvino_task: OpenVINOTask + self.nncf_task: NNCFTask + self.results = {"category": dataset_path} + self.seed = seed + + def get_dataclass( + self, + ) -> Union[Type[AnomalyDetectionDataset], Type[AnomalySegmentationDataset], Type[AnomalyClassificationDataset]]: + """Gets the dataloader based on the task type. + + Raises: + ValueError: Validates task type. + + Returns: + Dataloader + """ + dataclass: Union[ + Type[AnomalyDetectionDataset], Type[AnomalySegmentationDataset], Type[AnomalyClassificationDataset] + ] + if self.task_type == TaskType.ANOMALY_DETECTION: + dataclass = AnomalyDetectionDataset + elif self.task_type == TaskType.ANOMALY_SEGMENTATION: + dataclass = AnomalySegmentationDataset + elif self.task_type == TaskType.ANOMALY_CLASSIFICATION: + dataclass = AnomalyClassificationDataset + else: + raise ValueError(f"{self.task_type} not a supported task") + return dataclass + + def create_task_environment(self) -> TaskEnvironment: + """Create task environment.""" + hyper_parameters = create_hyper_parameters(self.model_template.hyper_parameters.data) + labels = self.dataset.get_labels() + label_schema = LabelSchemaEntity.from_labels(labels) + + return TaskEnvironment( + model_template=self.model_template, + model=None, + hyper_parameters=hyper_parameters, + label_schema=label_schema, + ) + + def create_task(self, task: str) -> Any: + """Create base torch or openvino task. + + Args: + task (str): task type. Either base or openvino. + + Returns: + Any: Base Torch or OpenVINO Task Class. + + Example: + >>> self.create_task(task="base") + + + """ + if self.model_template.entrypoints is not None: + task_path = getattr(self.model_template.entrypoints, task) + else: + raise ValueError(f"Cannot create {task} task. `model_template.entrypoint` does not have {task}") + + module_name, class_name = task_path.rsplit(".", 1) + module = importlib.import_module(module_name) + return getattr(module, class_name)(task_environment=self.task_environment) + + def train(self) -> ModelEntity: + """Train the base Torch model.""" + logger.info("Training the model.") + output_model = ModelEntity( + train_dataset=self.dataset, + configuration=self.task_environment.get_model_configuration(), + ) + self.torch_task.train( + dataset=self.dataset, output_model=output_model, train_parameters=TrainParameters(), seed=self.seed + ) + + logger.info("Inferring the base torch model on the validation set.") + result_set = self.infer(self.torch_task, output_model) + + logger.info("Evaluating the base torch model on the validation set.") + self.evaluate(self.torch_task, result_set) + self.results["torch_fp32"] = result_set.performance.score.value + self.trained_model = output_model + return self.trained_model + + def infer(self, task: IInferenceTask, output_model: ModelEntity) -> ResultSetEntity: + """Get the predictions using the base Torch or OpenVINO tasks and models. + + Args: + task (IInferenceTask): Task to infer. Either torch or openvino. + output_model (ModelEntity): Output model on which the weights are saved. + + Returns: + ResultSetEntity: Results set containing the true and pred datasets. + + """ + ground_truth_validation_dataset = self.dataset.get_subset(Subset.VALIDATION) + prediction_validation_dataset = task.infer( + dataset=ground_truth_validation_dataset.with_empty_annotations(), + inference_parameters=InferenceParameters(is_evaluation=True), + ) + + return ResultSetEntity( + model=output_model, + ground_truth_dataset=ground_truth_validation_dataset, + prediction_dataset=prediction_validation_dataset, + ) + + @staticmethod + def evaluate(task: IEvaluationTask, result_set: ResultSetEntity) -> None: + """Evaluate the performance of the model. + + Args: + task (IEvaluationTask): Task to evaluate the performance. Either torch or openvino. + result_set (ResultSetEntity): Results set containing the true and pred datasets. + + """ + task.evaluate(result_set) + logger.info(str(result_set.performance)) + + def export(self) -> ModelEntity: + """Export the model via openvino.""" + logger.info("Exporting the model.") + exported_model = ModelEntity( + train_dataset=self.dataset, + configuration=self.task_environment.get_model_configuration(), + ) + self.torch_task.export(ExportType.OPENVINO, exported_model) + self.task_environment.model = exported_model + + logger.info("Creating the OpenVINO Task.") + + self.openvino_task = self.create_task(task="openvino") + + logger.info("Inferring the exported model on the validation set.") + result_set = self.infer(task=self.openvino_task, output_model=exported_model) + + logger.info("Evaluating the exported model on the validation set.") + self.evaluate(task=self.openvino_task, result_set=result_set) + self.results["vino_fp32"] = result_set.performance.score.value + + return exported_model + + def optimize(self) -> None: + """Optimize the model via POT.""" + logger.info("Running the POT optimization") + optimized_model = ModelEntity( + self.dataset, + configuration=self.task_environment.get_model_configuration(), + ) + + self.openvino_task.optimize( + optimization_type=OptimizationType.POT, + dataset=self.dataset, + output_model=optimized_model, + optimization_parameters=OptimizationParameters(), + ) + + logger.info("Inferring the optimised model on the validation set.") + result_set = self.infer(task=self.openvino_task, output_model=optimized_model) + + logger.info("Evaluating the optimized model on the validation set.") + self.evaluate(task=self.openvino_task, result_set=result_set) + self.results["pot_int8"] = result_set.performance.score.value + + def optimize_nncf(self) -> None: + """Optimize the model via NNCF.""" + logger.info("Running the NNCF optimization") + init_model = ModelEntity( + self.dataset, + configuration=self.task_environment.get_model_configuration(), + model_adapters={"weights.pth": ModelAdapter(self.trained_model.get_data("weights.pth"))}, + ) + + self.task_environment.model = init_model + self.nncf_task = self.create_task("nncf") + + optimized_model = ModelEntity( + self.dataset, + configuration=self.task_environment.get_model_configuration(), + ) + self.nncf_task.optimize(OptimizationType.NNCF, self.dataset, optimized_model) + + logger.info("Inferring the optimised model on the validation set.") + result_set = self.infer(task=self.nncf_task, output_model=optimized_model) + + logger.info("Evaluating the optimized model on the validation set.") + self.evaluate(task=self.nncf_task, result_set=result_set) + self.results["torch_int8"] = result_set.performance.score.value + + def export_nncf(self) -> ModelEntity: + """Export NNCF model via openvino.""" + logger.info("Exporting the model.") + exported_model = ModelEntity( + train_dataset=self.dataset, + configuration=self.task_environment.get_model_configuration(), + ) + self.nncf_task.export(ExportType.OPENVINO, exported_model) + self.task_environment.model = exported_model + + logger.info("Creating the OpenVINO Task.") + + self.openvino_task = self.create_task(task="openvino") + + logger.info("Inferring the exported model on the validation set.") + result_set = self.infer(task=self.openvino_task, output_model=exported_model) + + logger.info("Evaluating the exported model on the validation set.") + self.evaluate(task=self.openvino_task, result_set=result_set) + self.results["vino_int8"] = result_set.performance.score.value + return exported_model + + @staticmethod + def clean_up() -> None: + """Clean up the `results` directory used by `anomalib`.""" + results_dir = "./results" + if os.path.exists(results_dir): + shutil.rmtree(results_dir) + + +def parse_args() -> Namespace: + """Parse CLI arguments. + + Returns: + (Namespace): CLI arguments. + + """ + parser = argparse.ArgumentParser( + description="Sample showcasing how to run Anomaly Classification Task using OTX SDK" + ) + parser.add_argument( + "--model_template_path", + default="./configs/classification/padim/template.yaml", + ) + parser.add_argument("--dataset_path", default="./datasets/MVTec") + parser.add_argument("--category", default="bottle") + parser.add_argument("--train-ann-files", required=True) + parser.add_argument("--val-ann-files", required=True) + parser.add_argument("--test-ann-files", required=True) + parser.add_argument("--optimization", choices=("none", "pot", "nncf"), default="none") + parser.add_argument("--seed", default=0) + return parser.parse_args() + + +def main() -> None: + """Run `sample.py` with given CLI arguments.""" + args = parse_args() + path = os.path.join(args.dataset_path, args.category) + + train_subset = {"ann_file": args.train_ann_files, "data_root": path} + val_subset = {"ann_file": args.val_ann_files, "data_root": path} + test_subset = {"ann_file": args.test_ann_files, "data_root": path} + + task = OtxAnomalyTask( + dataset_path=path, + train_subset=train_subset, + val_subset=val_subset, + test_subset=test_subset, + model_template_path=args.model_template_path, + seed=args.seed, + ) + + task.train() + task.export() + + if args.optimization == "pot": + task.optimize() + + if args.optimization == "nncf": + task.optimize_nncf() + task.export_nncf() + + task.clean_up() + + +if __name__ == "__main__": + main() diff --git a/src/otx/algorithms/classification/__init__.py b/src/otx/algorithms/classification/__init__.py new file mode 100644 index 00000000000..b1a5c62f48d --- /dev/null +++ b/src/otx/algorithms/classification/__init__.py @@ -0,0 +1,22 @@ +"""OTX Algorithms - Classification.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +MMCLS_AVAILABLE = True + +try: + import mmcls # noqa: F401 +except ImportError: + MMCLS_AVAILABLE = False diff --git a/src/otx/algorithms/classification/adapters/__init__.py b/src/otx/algorithms/classification/adapters/__init__.py new file mode 100644 index 00000000000..21400681a8e --- /dev/null +++ b/src/otx/algorithms/classification/adapters/__init__.py @@ -0,0 +1,15 @@ +"""Adapters for Classifcation.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/adapters/mmcls/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/__init__.py new file mode 100644 index 00000000000..30462572f11 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/__init__.py @@ -0,0 +1,34 @@ +"""Adapters of classification - mmcls.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +# The import required to register the backbone used by the OTX Template with the Registry. +import otx.algorithms.common.adapters.mmcv.models as OTXBackbones + +from .datasets import OTXClsDataset, SelfSLDataset +from .models import BYOL, ConstrastiveHead, SelfSLMLP +from .optimizer import LARS + +# fmt: off +# isort: off +# FIXME: openvino pot library adds stream handlers to root logger +# which makes annoying duplicated logging +from mmcls.utils import get_root_logger # pylint: disable=wrong-import-order +get_root_logger().propagate = False +# isort:on +# fmt: on + +__all__ = ["OTXClsDataset", "SelfSLDataset", "BYOL", "SelfSLMLP", "ConstrastiveHead", "LARS", "OTXBackbones"] diff --git a/src/otx/algorithms/classification/adapters/mmcls/apis/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/apis/__init__.py new file mode 100644 index 00000000000..d294852f865 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/apis/__init__.py @@ -0,0 +1,8 @@ +"""Adapters of classification - mmcls.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .train import train_model + +__all__ = ["train_model"] diff --git a/src/otx/algorithms/classification/adapters/mmcls/apis/train.py b/src/otx/algorithms/classification/adapters/mmcls/apis/train.py new file mode 100644 index 00000000000..58ca23b26aa --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/apis/train.py @@ -0,0 +1,162 @@ +"""Train function for classification task.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# Copyright (c) OpenMMLab. All rights reserved. +import warnings + +import torch +from mmcls.core import DistEvalHook, DistOptimizerHook, EvalHook +from mmcls.datasets import build_dataloader, build_dataset +from mmcls.utils import get_root_logger, wrap_distributed_model, wrap_non_distributed_model +from mmcv.runner import DistSamplerSeedHook, build_optimizer, build_runner + +from otx.algorithms.common.adapters.mmcv.utils import HPUDataParallel, XPUDataParallel +from otx.algorithms.common.adapters.mmcv.utils.hpu_optimizers import HABANA_OPTIMIZERS + + +def train_model(model, dataset, cfg, distributed=False, validate=False, timestamp=None, device=None, meta=None): + """Train a model. + + This method will build dataloaders, wrap the model and build a runner + according to the provided config. + + Args: + model (:obj:`torch.nn.Module`): The model to be run. + dataset (:obj:`mmcls.datasets.BaseDataset` | List[BaseDataset]): + The dataset used to train the model. It can be a single dataset, + or a list of dataset with the same length as workflow. + cfg (:obj:`mmcv.utils.Config`): The configs of the experiment. + distributed (bool): Whether to train the model in a distributed + environment. Defaults to False. + validate (bool): Whether to do validation with + :obj:`mmcv.runner.EvalHook`. Defaults to False. + timestamp (str, optional): The timestamp string to auto generate the + name of log files. Defaults to None. + device (str, optional): TODO + meta (dict, optional): A dict records some import information such as + environment info and seed, which will be logged in logger hook. + Defaults to None. + """ + logger = get_root_logger() + + # prepare data loaders + dataset = dataset if isinstance(dataset, (list, tuple)) else [dataset] + + # The default loader config + loader_cfg = dict( + # cfg.gpus will be ignored if distributed + num_gpus=cfg.ipu_replicas if device == "ipu" else len(cfg.gpu_ids), + dist=distributed, + round_up=True, + seed=cfg.get("seed"), + sampler_cfg=cfg.get("sampler", None), + ) + # The overall dataloader settings + loader_cfg.update( + { + k: v + for k, v in cfg.data.items() + if k not in ["train", "val", "test", "train_dataloader", "val_dataloader", "test_dataloader"] + } + ) + # The specific dataloader settings + train_loader_cfg = {**loader_cfg, **cfg.data.get("train_dataloader", {})} + data_loaders = [build_dataloader(ds, **train_loader_cfg) for ds in dataset] + + fp16_cfg = cfg.get("fp16_", None) + # put model on gpus + if distributed: + find_unused_parameters = cfg.get("find_unused_parameters", False) + # Sets the `find_unused_parameters` parameter in + # torch.nn.parallel.DistributedDataParallel + model = wrap_distributed_model( + model, cfg.device, broadcast_buffers=False, find_unused_parameters=find_unused_parameters + ) + elif cfg.device == "xpu": + assert len(cfg.gpu_ids) == 1 + model.to(f"xpu:{cfg.gpu_ids[0]}") + model = XPUDataParallel(model, dim=0, device_ids=cfg.gpu_ids) + elif cfg.device == "hpu": + assert len(cfg.gpu_ids) == 1 + model = HPUDataParallel(model.cuda(), dim=0, device_ids=cfg.gpu_ids, enable_autocast=bool(fp16_cfg)) + else: + model = wrap_non_distributed_model(model, cfg.device, device_ids=cfg.gpu_ids) + + # build runner + if cfg.device == "hpu": + if (new_type := "Fused" + cfg.optimizer.get("type", "SGD")) in HABANA_OPTIMIZERS: + cfg.optimizer["type"] = new_type + + optimizer = build_optimizer(model, cfg.optimizer) + + if cfg.device == "xpu": + if cfg.optimizer_config.get("bf16_training", False): + logger.warning("XPU supports fp32 training only currently.") + dtype = torch.float32 + model.train() + model, optimizer = torch.xpu.optimize(model, optimizer=optimizer, dtype=dtype) + + if "bf16_training" in cfg.optimizer_config: + # Remove unused parameters in runner + cfg.optimizer_config.pop("bf16_training") + + if cfg.get("runner") is None: + cfg.runner = {"type": "EpochBasedRunner", "max_epochs": cfg.total_epochs} + warnings.warn( + "config is now expected to have a `runner` section, " "please set `runner` in your config.", UserWarning + ) + + runner = build_runner( + cfg.runner, + default_args=dict( + model=model, batch_processor=None, optimizer=optimizer, work_dir=cfg.work_dir, logger=logger, meta=meta + ), + ) + + # an ugly walkaround to make the .log and .log.json filenames the same + runner.timestamp = timestamp + + if fp16_cfg is None and distributed and "type" not in cfg.optimizer_config: + optimizer_config = DistOptimizerHook(**cfg.optimizer_config) + else: + optimizer_config = cfg.optimizer_config + + # register hooks + runner.register_training_hooks( + cfg.lr_config, + optimizer_config, + cfg.checkpoint_config, + cfg.log_config, + cfg.get("momentum_config", None), + custom_hooks_config=cfg.get("custom_hooks", None), + ) + if distributed and cfg.runner["type"] == "EpochBasedRunner": + runner.register_hook(DistSamplerSeedHook()) + + # register eval hooks + if validate: + val_dataset = build_dataset(cfg.data.val, dict(test_mode=True)) + # The specific dataloader settings + val_loader_cfg = { + **loader_cfg, + "shuffle": False, # Not shuffle by default + "sampler_cfg": None, # Not use sampler by default + "drop_last": False, # Not drop last by default + **cfg.data.get("val_dataloader", {}), + } + val_dataloader = build_dataloader(val_dataset, **val_loader_cfg) + eval_cfg = cfg.get("evaluation", {}) + eval_cfg["by_epoch"] = cfg.runner["type"] != "IterBasedRunner" + eval_hook = DistEvalHook if distributed else EvalHook + # `EvalHook` needs to be executed after `IterTimerHook`. + # Otherwise, it will cause a bug if use `IterBasedRunner`. + # Refers to https://github.com/open-mmlab/mmcv/issues/1261 + runner.register_hook(eval_hook(val_dataloader, **eval_cfg), priority="LOW") + + if cfg.resume_from: + runner.resume(cfg.resume_from) + elif cfg.load_from: + runner.load_checkpoint(cfg.load_from) + runner.run(data_loaders, cfg.workflow) diff --git a/src/otx/algorithms/classification/adapters/mmcls/configurer.py b/src/otx/algorithms/classification/adapters/mmcls/configurer.py new file mode 100644 index 00000000000..873a6efdbe8 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/configurer.py @@ -0,0 +1,224 @@ +"""Base configurer for mmdet config.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from typing import Optional, Tuple + +import torch +from mmcv import build_from_cfg +from mmcv.utils import ConfigDict + +from otx.algorithms import TRANSFORMER_BACKBONES +from otx.algorithms.classification.adapters.mmcls.utils import ( + patch_datasets, + patch_evaluation, +) +from otx.algorithms.common.adapters.mmcv.clsincr_mixin import IncrConfigurerMixin +from otx.algorithms.common.adapters.mmcv.configurer import BaseConfigurer +from otx.algorithms.common.adapters.mmcv.semisl_mixin import SemiSLConfigurerMixin +from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + InputSizeManager, + recursively_update_cfg, + update_or_add_custom_hook, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-public-methods +class ClassificationConfigurer(BaseConfigurer): + """Patch config to support otx train.""" + + def configure_data_pipeline(self, cfg, input_size, model_ckpt_path, **kwargs): + """Configuration for data pipeline.""" + super().configure_data_pipeline(cfg, input_size, model_ckpt_path) + options_for_patch_datasets = kwargs.get("options_for_patch_datasets", {"type": "OTXClsDataset"}) + patch_datasets(cfg, **options_for_patch_datasets) + + def configure_recipe(self, cfg, **kwargs): + """Configuration for training recipe.""" + options_for_patch_evaluation = kwargs.get("options_for_patch_evaluation", {"task": "normal"}) + patch_evaluation(cfg, **options_for_patch_evaluation) + super().configure_recipe(cfg) + + def configure_backbone(self, cfg, ir_options): # noqa: C901 + """Patch config's model. + + Change model type to super type + Patch for OMZ backbones + """ + + if ir_options is None: + ir_options = {"ir_model_path": None, "ir_weight_path": None, "ir_weight_init": False} + + cfg.model_task = cfg.model.pop("task", self.task) + if cfg.model_task != self.task: + raise ValueError(f"Given cfg ({cfg.filename}) is not supported by {self.task} recipe") + + super_type = cfg.model.pop("super_type", None) + if super_type: + cfg.model.arch_type = cfg.model.type + cfg.model.type = super_type + + # Hierarchical + if cfg.model.get("hierarchical"): + assert cfg.data.train.hierarchical_info == cfg.data.val.hierarchical_info == cfg.data.test.hierarchical_info + cfg.model.head.hierarchical_info = cfg.data.train.hierarchical_info + + # OV-plugin + ir_model_path = ir_options.get("ir_model_path") + if ir_model_path: + + def is_mmov_model(key, value): + if key == "type" and value.startswith("MMOV"): + return True + return False + + ir_weight_path = ir_options.get("ir_weight_path", None) + ir_weight_init = ir_options.get("ir_weight_init", False) + recursively_update_cfg( + cfg, + is_mmov_model, + {"model_path": ir_model_path, "weight_path": ir_weight_path, "init_weight": ir_weight_init}, + ) + + self.configure_in_channel(cfg) + self.configure_topk(cfg) + + def _configure_head(self, cfg): + """Patch nuber of classes of head..""" + cfg.model.head.num_classes = len(self.model_classes) + + # pylint: disable=too-many-branches + @staticmethod + def configure_in_channel(cfg): + """Return whether in_channels need patch.""" + configure_required = False + if cfg.model.get("neck") is not None: + if cfg.model.neck.get("in_channels") is not None and cfg.model.neck.in_channels <= 0: + configure_required = True + if not configure_required and cfg.model.get("head") is not None: + if cfg.model.head.get("in_channels") is not None and cfg.model.head.in_channels <= 0: + configure_required = True + if not configure_required: + return + + # update model layer's in/out configuration + from mmcv.cnn import MODELS as backbone_reg + + layer = build_from_cfg(cfg.model.backbone, backbone_reg) + layer.eval() + input_shape = [3, 224, 224] + # MMOV model + if hasattr(layer, "input_shapes"): + input_shape = next(iter(getattr(layer, "input_shapes").values())) + input_shape = input_shape[1:] + if any(i < 0 for i in input_shape): + input_shape = [3, 244, 244] + logger.debug(f"input shape for backbone {input_shape}") + output = layer(torch.rand([1] + list(input_shape))) + if isinstance(output, (tuple, list)): + output = output[-1] + + if layer.__class__.__name__ in TRANSFORMER_BACKBONES and isinstance(output, (tuple, list)): + # mmcls.VisionTransformer outputs Tuple[List[...]] and the last index of List is the final logit. + _, output = output + + in_channels = output.shape[1] + if cfg.model.get("neck") is not None: + if cfg.model.neck.get("in_channels") is not None: + logger.info( + f"'in_channels' config in model.neck is updated from " + f"{cfg.model.neck.in_channels} to {in_channels}" + ) + cfg.model.neck.in_channels = in_channels + logger.debug(f"input shape for neck {input_shape}") + from mmcls.models.builder import NECKS as neck_reg + + layer = build_from_cfg(cfg.model.neck, neck_reg) + layer.eval() + output = layer(torch.rand(output.shape)) + if isinstance(output, (tuple, list)): + output = output[-1] + in_channels = output.shape[1] + if cfg.model.get("head") is not None: + if cfg.model.head.get("in_channels") is not None: + logger.info( + f"'in_channels' config in model.head is updated from " + f"{cfg.model.head.in_channels} to {in_channels}" + ) + cfg.model.head.in_channels = in_channels + + @staticmethod + def configure_topk(cfg): + """Patch topk in case of num_classes is less than 5.""" + if cfg.model.head.get("topk", False) and isinstance(cfg.model.head.topk, tuple): + cfg.model.head.topk = (1,) if cfg.model.head.num_classes < 5 else (1, 5) + if cfg.model.get("multilabel", False) or cfg.model.get("hierarchical", False): + cfg.model.head.pop("topk", None) + + @staticmethod + def configure_input_size( + cfg, input_size=Optional[Tuple[int, int]], model_ckpt_path: Optional[str] = None, training=True + ): + """Change input size if necessary.""" + if input_size is None: # InputSizePreset.DEFAULT + return + + manager = InputSizeManager(cfg) + + if input_size == (0, 0): # InputSizePreset.AUTO + if training: + input_size = BaseConfigurer.adapt_input_size_to_dataset(cfg, manager, use_annotations=False) + else: + input_size = manager.get_trained_input_size(model_ckpt_path) + if input_size is None: + return + + manager.set_input_size(input_size) + logger.info("Input size is changed to {}".format(input_size)) + + +class IncrClassificationConfigurer(IncrConfigurerMixin, ClassificationConfigurer): + """Patch config to support incremental learning for classification.""" + + def configure_task(self, cfg, **kwargs): + """Patch config to support incremental learning.""" + super().configure_task(cfg, **kwargs) + if "task_adapt" in cfg and self.task_adapt_type == "default_task_adapt": + self.configure_task_adapt_hook(cfg) + if self._is_multiclass(cfg): + self.configure_loss(cfg) + + def configure_loss(self, cfg): + """Patch classification loss.""" + if not self.is_incremental(): + cfg.model.head.loss = dict(type="CrossEntropyLoss", loss_weight=1.0) + else: + cfg.model.head.loss = ConfigDict( + type="IBLoss", + num_classes=cfg.model.head.num_classes, + ) + ib_loss_hook = ConfigDict( + type="IBLossHook", + dst_classes=self.model_classes, + ) + update_or_add_custom_hook(cfg, ib_loss_hook) + + def get_sampler_type(self, cfg): + """Return sampler type.""" + if self._is_multiclass(cfg): + sampler_type = "balanced" + else: + sampler_type = "cls_incr" + return sampler_type + + @staticmethod + def _is_multiclass(cfg) -> bool: + return not cfg.model.get("multilabel", False) and not cfg.model.get("hierarchical", False) + + +class SemiSLClassificationConfigurer(SemiSLConfigurerMixin, ClassificationConfigurer): + """Patch config to support semi supervised learning for classification.""" diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/__init__.py new file mode 100644 index 00000000000..0242a22643b --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/__init__.py @@ -0,0 +1,43 @@ +"""OTX Algorithms - Classification Dataset.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .otx_datasets import ( + OTXClsDataset, + OTXHierarchicalClsDataset, + OTXMultilabelClsDataset, + SelfSLDataset, +) +from .pipelines.transforms import ( + AugMixAugment, + OTXRandAugment, + PILToTensor, + RandomRotate, + TensorNormalize, + TwoCropTransform, +) + +__all__ = [ + "AugMixAugment", + "PILToTensor", + "TensorNormalize", + "RandomRotate", + "OTXRandAugment", + "TwoCropTransform", + "OTXClsDataset", + "OTXMultilabelClsDataset", + "OTXHierarchicalClsDataset", + "SelfSLDataset", +] diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py new file mode 100644 index 00000000000..6f0ba443ebf --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py @@ -0,0 +1,493 @@ +"""Base Dataset for Classification Task.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name, too-many-locals, no-member + +from typing import Any, Dict, List, Union + +import numpy as np +from mmcls.core import average_performance, mAP +from mmcls.datasets.base_dataset import BaseDataset +from mmcls.datasets.builder import DATASETS, PIPELINES +from mmcls.datasets.pipelines import Compose +from mmcv.utils.registry import build_from_cfg +from sklearn.metrics import confusion_matrix as sklearn_confusion_matrix +from torch.utils.data import Dataset + +from otx.algorithms.common.utils import get_cls_img_indices, get_old_new_img_indices +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.label import LabelEntity +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-instance-attributes +@DATASETS.register_module() +class OTXClsDataset(BaseDataset): + """Multi-class classification dataset class.""" + + def __init__( + self, otx_dataset: DatasetEntity, labels: List[LabelEntity], empty_label=None, pipeline=[], **kwargs + ): # pylint: disable=super-init-not-called + self.otx_dataset = otx_dataset + self.labels = labels + self.label_names = [label.name for label in self.labels] + self.label_idx = {label.id: i for i, label in enumerate(labels)} + self.idx_to_label_id = {v: k for k, v in self.label_idx.items()} + self.empty_label = empty_label + self.class_acc = False + + self.CLASSES = list(label.name for label in labels) + self.gt_labels = [] # type: List + self.num_classes = len(self.CLASSES) + + test_mode = kwargs.get("test_mode", False) + if test_mode is False: + new_classes = kwargs.pop("new_classes", []) + self.img_indices = self.get_indices(new_classes) + + self.pipeline = Compose([build_from_cfg(p, PIPELINES) for p in pipeline]) + self.load_annotations() + + def get_indices(self, *args): # pylint: disable=unused-argument + """Get indices.""" + return get_cls_img_indices(self.labels, self.otx_dataset) + + def load_annotations(self): + """Load annotations.""" + include_empty = self.empty_label in self.labels + for i, _ in enumerate(self.otx_dataset): + class_indices = [] + item_labels = self.otx_dataset[i].get_roi_labels(self.labels, include_empty=include_empty) + ignored_labels = self.otx_dataset[i].ignored_labels + if item_labels: + for otx_lbl in item_labels: + if otx_lbl not in ignored_labels: + class_indices.append(self.label_names.index(otx_lbl.name)) + else: + class_indices.append(-1) + else: # this supposed to happen only on inference stage + class_indices.append(-1) + self.gt_labels.append(class_indices) + self.gt_labels = np.array(self.gt_labels) + + def __getitem__(self, index: int): + """Get item from dataset.""" + dataset = self.otx_dataset + item = dataset[index] + ignored_labels = np.array([self.label_idx[lbs.id] for lbs in item.ignored_labels]) + + height, width = item.height, item.width + + gt_label = self.gt_labels[index] + data_info = dict( + dataset_item=item, + width=width, + height=height, + index=index, + gt_label=gt_label, + ignored_labels=ignored_labels, + entity_id=getattr(item, "id_", None), + label_id=self._get_label_id(gt_label), + ) + + if self.pipeline is None: + return data_info + return self.pipeline(data_info) + + def _get_label_id(self, gt_label: np.ndarray) -> Union[ID, List[ID]]: + return self.idx_to_label_id.get(gt_label.item(), ID()) + + def get_gt_labels(self): + """Get all ground-truth labels (categories). + + Returns: + list[int]: categories for all images. + """ + + return self.gt_labels + + def __len__(self): + """Get dataset length.""" + return len(self.otx_dataset) + + def evaluate( + self, results, metric="accuracy", metric_options=None, logger=None + ): # pylint: disable=redefined-outer-name + """Evaluate the dataset with new metric class_accuracy. + + Args: + results (list): Testing results of the dataset. + metric (str | list[str]): Metrics to be evaluated. + Default value is `accuracy`. + 'accuracy', 'precision', 'recall', 'f1_score', 'support', 'class_accuracy' + metric_options (dict, optional): Options for calculating metrics. + Allowed keys are 'topk', 'thrs' and 'average_mode'. + Defaults to None. + logger (logging.Logger | str, optional): Logger used for printing + related information during evaluation. Defaults to None. + + Returns: + dict: evaluation results + """ + + if metric_options is None: + metric_options = {"topk": (1, 5) if self.num_classes >= 5 else (1,)} + + if isinstance(metric, str): + metrics = [metric] + else: + metrics = metric + + if "class_accuracy" in metrics: + metrics.remove("class_accuracy") + self.class_acc = True + + eval_results = super().evaluate(results, metrics, metric_options, logger=logger) + for k in metric_options["topk"]: + eval_results[f"accuracy_top-{k}"] /= 100 + + # Add Evaluation Accuracy score per Class - it can be used only for multi-class dataset. + if self.class_acc: + results = np.vstack(results) + gt_labels = self.get_gt_labels() + accuracies = self.class_accuracy(results, gt_labels) + + if any(np.isnan(accuracies)): + accuracies = np.nan_to_num(accuracies) + + eval_results.update({f"{c} accuracy": a for c, a in zip(self.CLASSES, accuracies)}) + eval_results.update({"mean accuracy": np.mean(accuracies)}) + + eval_results["accuracy"] = eval_results["accuracy_top-1"] + return eval_results + + def class_accuracy(self, results, gt_labels): + """Return per-class accuracy.""" + accracies = [] + pred_label = results.argsort(axis=1)[:, -1:][:, ::-1] + for i in range(self.num_classes): + cls_pred = pred_label == i + cls_pred = cls_pred[gt_labels == i] + if len(cls_pred) > 0: + cls_acc = np.sum(cls_pred) / len(cls_pred) + else: + cls_acc = 0.0 + accracies.append(cls_acc) + return accracies + + +@DATASETS.register_module() +class OTXMultilabelClsDataset(OTXClsDataset): + """Multi-label classification dataset class.""" + + def get_indices(self, new_classes): + """Get indices.""" + return get_old_new_img_indices(self.labels, new_classes, self.otx_dataset) + + def load_annotations(self): + """Load annotations.""" + include_empty = self.empty_label in self.labels + for i, _ in enumerate(self.otx_dataset): + item_labels = self.otx_dataset[i].get_roi_labels(self.labels, include_empty=include_empty) + ignored_labels = self.otx_dataset[i].ignored_labels + onehot_indices = np.zeros(len(self.labels)) + if item_labels: + for otx_lbl in item_labels: + if otx_lbl not in ignored_labels: + onehot_indices[self.label_names.index(otx_lbl.name)] = 1 + else: + # during training we filter ignored classes out, + # during validation mmcv's mAP also filters -1 labels + onehot_indices[self.label_names.index(otx_lbl.name)] = -1 + + self.gt_labels.append(onehot_indices) + self.gt_labels = np.array(self.gt_labels) + + def evaluate( + self, results, metric="mAP", metric_options=None, indices=None, logger=None + ): # pylint: disable=unused-argument, redefined-outer-name, arguments-renamed + """Evaluate the dataset. + + Args: + results (list): Testing results of the dataset. + metric (str | list[str]): Metrics to be evaluated. + Default value is 'mAP'. Options are 'mAP', 'CP', 'CR', 'CF1', + 'OP', 'OR' and 'OF1'. + metric_options (dict, optional): Options for calculating metrics. + Allowed keys are 'k' and 'thr'. Defaults to None + indices (list, optional): Indices to filter the gt label. Defaults to None. + logger (logging.Logger | str, optional): Logger used for printing + related information during evaluation. Defaults to None. + + Returns: + dict: evaluation results + """ + if metric_options is None or metric_options == {}: + metric_options = {"thr": 0.5} + + if isinstance(metric, str): + metrics = [metric] + else: + metrics = metric + allowed_metrics = ["accuracy-mlc", "mAP", "CP", "CR", "CF1", "OP", "OR", "OF1"] + eval_results = {} + results = np.vstack(results) + gt_labels = self.get_gt_labels() + if indices is not None: + gt_labels = gt_labels[indices] + num_imgs = len(results) + assert len(gt_labels) == num_imgs, "dataset testing results should " "be of the same length as gt_labels." + + invalid_metrics = set(metrics) - set(allowed_metrics) + if len(invalid_metrics) != 0: + raise ValueError(f"metric {invalid_metrics} is not supported.") + + if "accuracy-mlc" in metrics: + true_label_idx = [] + pred_label_idx = [] + pos_thr = metric_options.get("thr", 0.5) + + true_label = gt_labels == 1 + pred_label = results > pos_thr + cls_index = [i + 1 for i in range(len(self.labels))] + for true_lbl, pred_lbl in zip(true_label, pred_label): + true_lbl_idx = set(true_lbl * cls_index) - set([0]) # except empty + pred_lbl_idx = set(pred_lbl * cls_index) - set([0]) + true_label_idx.append(true_lbl_idx) + pred_label_idx.append(pred_lbl_idx) + + confusion_matrices = [] + for cls_idx in cls_index: + group_labels_idx = set([cls_idx]) + y_true = [int(not group_labels_idx.issubset(true_labels)) for true_labels in true_label_idx] + y_pred = [int(not group_labels_idx.issubset(pred_labels)) for pred_labels in pred_label_idx] + matrix_data = sklearn_confusion_matrix(y_true, y_pred, labels=list(range(len([0, 1])))) + confusion_matrices.append(matrix_data) + correct_per_label_group = [np.trace(mat) for mat in confusion_matrices] + total_per_label_group = [np.sum(mat) for mat in confusion_matrices] + + acc = np.sum(correct_per_label_group) / np.sum(total_per_label_group) # MICRO average + eval_results["accuracy-mlc"] = acc + eval_results["accuracy"] = eval_results["accuracy-mlc"] + + if "mAP" in metrics: + mAP_value = mAP(results, gt_labels) + eval_results["mAP"] = mAP_value + if len(set(metrics) - {"mAP"}) != 0: + performance_keys = ["CP", "CR", "CF1", "OP", "OR", "OF1"] + performance_values = average_performance(results, gt_labels, **metric_options) + for k, v in zip(performance_keys, performance_values): + if k in metrics: + eval_results[k] = v + + return eval_results + + def _get_label_id(self, gt_label: np.ndarray) -> Union[ID, List[ID]]: + return [self.idx_to_label_id.get(idx, ID()) for idx, v in enumerate(gt_label) if v == 1] + + +@DATASETS.register_module() +class OTXHierarchicalClsDataset(OTXMultilabelClsDataset): + """Hierarchical classification dataset class.""" + + def __init__(self, **kwargs): + self.hierarchical_info = kwargs.pop("hierarchical_info", None) + self.label_schema = kwargs.pop("label_schema", None) + super().__init__(**kwargs) + + def load_annotations(self): + """Load annotations.""" + include_empty = self.empty_label in self.labels + for i, _ in enumerate(self.otx_dataset): + class_indices = [] + item_labels = self.otx_dataset[i].get_roi_labels(self.labels, include_empty=include_empty) + if self.label_schema: + # NOTE: Parent labels might be missing in annotations. + # This code fills the gap just in case. + full_item_labels = set() + for label in item_labels: + full_item_labels.update(self.label_schema.get_ancestors(label)) + item_labels = full_item_labels + ignored_labels = self.otx_dataset[i].ignored_labels + if item_labels: + num_cls_heads = self.hierarchical_info["num_multiclass_heads"] + + class_indices = [0] * (num_cls_heads + self.hierarchical_info["num_multilabel_classes"]) + for j in range(num_cls_heads): + class_indices[j] = -1 + for otx_lbl in item_labels: + group_idx, in_group_idx = self.hierarchical_info["class_to_group_idx"][otx_lbl.name] + if group_idx < num_cls_heads: + class_indices[group_idx] = in_group_idx + elif otx_lbl not in ignored_labels: + class_indices[num_cls_heads + in_group_idx] = 1 + else: + class_indices[num_cls_heads + in_group_idx] = -1 + else: # this supposed to happen only on inference stage or if we have a negative in multilabel data + class_indices = [-1] * ( + self.hierarchical_info["num_multiclass_heads"] + self.hierarchical_info["num_multilabel_classes"] + ) + self.gt_labels.append(class_indices) + self.gt_labels = np.array(self.gt_labels) + + self._update_heads_information() + + def _update_heads_information(self): + """Update heads information to find the empty heads. + + If there are no annotations at a specific head, this should be filtered out to calculate loss correctly. + """ + num_cls_heads = self.hierarchical_info["num_multiclass_heads"] + for head_idx in range(num_cls_heads): + labels_in_head = self.gt_labels[:, head_idx] # type: ignore[call-overload] + if max(labels_in_head) < 0: + self.hierarchical_info["empty_multiclass_head_indices"].append(head_idx) + + @staticmethod + def mean_top_k_accuracy(scores, labels, k=1): + """Return mean of top-k accuracy.""" + idx = np.argsort(-scores, axis=-1)[:, :k] + labels = np.array(labels) + matches = np.any(idx == labels.reshape([-1, 1]), axis=-1) + + classes = np.unique(labels) + + accuracy_values = [] + for class_id in classes: + mask = labels == class_id + num_valid = np.sum(mask) + if num_valid == 0: + continue + + accuracy_values.append(np.sum(matches[mask]) / float(num_valid)) + + return np.mean(accuracy_values) * 100 if len(accuracy_values) > 0 else 1.0 + + def evaluate( + self, results, metric="MHAcc", metric_options=None, indices=None, logger=None + ): # pylint: disable=unused-argument, redefined-outer-name + """Evaluate the dataset. + + Args: + results (list): Testing results of the dataset. + metric (str | list[str]): Metrics to be evaluated. + Default value is 'mAP'. Options are 'mAP', 'CP', 'CR', 'CF1', + 'OP', 'OR' and 'OF1'. + metric_options (dict, optional): Options for calculating metrics. + Allowed keys are 'k' and 'thr'. Defaults to None + indices (list, optional): Indices to filter the gt label. + Defaults to None. + logger (logging.Logger | str, optional): Logger used for printing + related information during evaluation. Defaults to None. + + Returns: + dict: evaluation results + """ + if metric_options is None or metric_options == {}: + metric_options = {"thr": 0.5} + + if isinstance(metric, str): + metrics = [metric] + else: + metrics = metric + + allowed_metrics = ["MHAcc", "avgClsAcc", "mAP"] + eval_results = {} + results = np.vstack(results) + gt_labels = self.get_gt_labels() + if indices is not None: + gt_labels = gt_labels[indices] + num_imgs = len(results) + assert len(gt_labels) == num_imgs, "dataset testing results should " "be of the same length as gt_labels." + + invalid_metrics = set(metrics) - set(allowed_metrics) + if len(invalid_metrics) != 0: + raise ValueError(f"metric {invalid_metrics} is not supported.") + + total_acc = 0.0 + total_acc_sl = 0.0 + for i in range(self.hierarchical_info["num_multiclass_heads"]): + multiclass_logit = results[ + :, + self.hierarchical_info["head_idx_to_logits_range"][str(i)][0] : self.hierarchical_info[ + "head_idx_to_logits_range" + ][str(i)][1], + ] # noqa: E127 + multiclass_gt = gt_labels[:, i] + cls_acc = self.mean_top_k_accuracy(multiclass_logit, multiclass_gt, k=1) + total_acc += cls_acc + total_acc_sl += cls_acc + + mAP_value = 0.0 + if self.hierarchical_info["num_multilabel_classes"] and "mAP" in metrics: + multilabel_logits = results[:, self.hierarchical_info["num_single_label_classes"] :] + multilabel_gt = gt_labels[:, self.hierarchical_info["num_multiclass_heads"] :] + mAP_value = mAP(multilabel_logits, multilabel_gt) + + total_acc += mAP_value + total_acc /= self.hierarchical_info["num_multiclass_heads"] + int( + self.hierarchical_info["num_multilabel_classes"] > 0 + ) + + eval_results["MHAcc"] = total_acc + if self.hierarchical_info["num_multiclass_heads"] > 0: + eval_results["avgClsAcc"] = total_acc_sl / self.hierarchical_info["num_multiclass_heads"] + else: + eval_results["avgClsAcc"] = total_acc_sl + eval_results["mAP"] = mAP_value + eval_results["accuracy"] = total_acc + + return eval_results + + +@DATASETS.register_module() +class SelfSLDataset(Dataset): + """SelfSL dataset that training with two pipelines and no label.""" + + CLASSES = None + + def __init__( + self, otx_dataset: DatasetEntity, pipeline: Dict[str, Any], **kwargs + ): # pylint: disable=unused-argument + super().__init__() + self.otx_dataset = otx_dataset + + self.load_pipeline = build_from_cfg(dict(type="LoadImageFromOTXDataset"), PIPELINES) + self.view0 = Compose([build_from_cfg(p, PIPELINES) for p in pipeline["view0"]]) + self.view1 = Compose([build_from_cfg(p, PIPELINES) for p in pipeline["view1"]]) + + def __len__(self): + """Get dataset length.""" + return len(self.otx_dataset) + + def __getitem__(self, index: int): + """Get item from dataset.""" + dataset = self.otx_dataset + item = dataset[index] + + height, width = item.height, item.width + + data_info = dict( + dataset_item=item, + width=width, + height=height, + index=index, + ) + + loaded_results = self.load_pipeline(data_info) + results1 = self.view0(loaded_results.copy()) + results2 = self.view1(loaded_results.copy()) + + results = {} + for k, v in results1.items(): + results[k + "1"] = v + for k, v in results2.items(): + results[k + "2"] = v + + return results diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/__init__.py new file mode 100644 index 00000000000..40f49b6d32d --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/__init__.py @@ -0,0 +1,36 @@ +"""OTX Algorithms - Classification pipelines.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .otx_pipelines import ( + GaussianBlur, + LoadImageFromOTXDataset, + OTXColorJitter, + PILImageToNDArray, + PostAug, + RandomAppliedTrans, +) +from .transforms import ( + AugMixAugment, + OTXRandAugment, + PILToTensor, + RandomRotate, + TensorNormalize, + TwoCropTransform, +) + +__all__ = [ + "PostAug", + "PILImageToNDArray", + "LoadImageFromOTXDataset", + "RandomAppliedTrans", + "GaussianBlur", + "OTXColorJitter", + "AugMixAugment", + "PILToTensor", + "RandomRotate", + "TensorNormalize", + "OTXRandAugment", + "TwoCropTransform", +] diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/otx_pipelines.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/otx_pipelines.py new file mode 100644 index 00000000000..42088ea9f7f --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/otx_pipelines.py @@ -0,0 +1,187 @@ +"""Collection Pipeline for classification task.""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import copy +from typing import Any, Dict, List, Optional + +import numpy as np +from mmcls.datasets import PIPELINES +from mmcls.datasets.pipelines import Compose, Resize +from mmcv.utils.registry import build_from_cfg +from PIL import Image, ImageFilter +from torchvision import transforms as T + +import otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset as load_image_base + +# TODO: refactoring to common modules +# TODO: refactoring to Sphinx style. + + +@PIPELINES.register_module() +class LoadImageFromOTXDataset(load_image_base.LoadImageFromOTXDataset): + """Pipeline element that loads an image from a OTX Dataset on the fly.""" + + +@PIPELINES.register_module() +class LoadResizeDataFromOTXDataset(load_image_base.LoadResizeDataFromOTXDataset): + """Load and resize image & annotation with cache support.""" + + def _create_resize_op(self, cfg: Optional[Dict]) -> Optional[Any]: + """Creates resize operation.""" + if cfg is None: + return None + return build_from_cfg(cfg, PIPELINES) + + +@PIPELINES.register_module() +class ResizeTo(Resize): + """Resize to specific size. + + This operation works if the input is not in desired shape. + If it's already in the shape, it just returns input dict for efficiency. + + Args: + size (tuple): Images scales for resizing (h, w). + """ + + def __call__(self, results: Dict[str, Any]): + """Callback function of ResizeTo. + + Args: + results: Inputs to be transformed. + """ + img_shape = results.get("img_shape", (0, 0)) + if img_shape[0] == self.size[0] and img_shape[1] == self.size[1]: + return results + return super().__call__(results) + + +@PIPELINES.register_module() +class RandomAppliedTrans: + """Randomly applied transformations. + + :param transforms: List of transformations in dictionaries + :param p: Probability, defaults to 0.5 + """ + + def __init__(self, transforms: List, p: float = 0.5): + t = [build_from_cfg(t, PIPELINES) for t in transforms] # pylint: disable=invalid-name + self.trans = T.RandomApply(t, p=p) + + def __call__(self, results: Dict[str, Any]): + """Callback function of RandomAppliedTrans. + + :param results: Inputs to be transformed. + """ + return self.trans(results) + + def __repr__(self): + """Set repr of RandomAppliedTrans.""" + repr_str = self.__class__.__name__ + return repr_str + + +@PIPELINES.register_module +class OTXColorJitter(T.ColorJitter): + """Wrapper for ColorJitter in torchvision.transforms. + + Use this instead of mmcls's because there is no `hue` parameter in mmcls ColorJitter. + """ + + def __call__(self, results): + """Callback function of OTXColorJitter. + + :param results: Inputs to be transformed. + """ + results["img"] = np.array(self.forward(Image.fromarray(results["img"]))) + return results + + +@PIPELINES.register_module() +class GaussianBlur: + """Gaussian blur augmentation in SimCLR https://arxiv.org/abs/2002.05709. + + :param sigma_min: Minimum value of sigma of gaussian filter. + :param sigma_max: Maximum value of sigma of gaussian filter. + """ + + def __init__(self, sigma_min: float, sigma_max: float): + self.sigma_min = sigma_min + self.sigma_max = sigma_max + + def __call__(self, results: Dict[str, Any]): + """Callback function of GaussianBlur. + + :param results: Inputs to be transformed. + """ + for key in results.get("img_fields", ["img"]): + img = Image.fromarray(results[key]) + sigma = np.random.uniform(self.sigma_min, self.sigma_max) + results[key] = np.array(img.filter(ImageFilter.GaussianBlur(radius=sigma))) + return results + + def __repr__(self): + """Set repr of GaussianBlur.""" + repr_str = self.__class__.__name__ + return repr_str + + +@PIPELINES.register_module() +class PILImageToNDArray: + """Pipeline element that converts an PIL image into numpy array. + + Expected entries in the 'results' dict that should be passed to this pipeline element are: + results['img']: PIL type image in data pipeline. + + Args: + keys (list[str]): list to support multiple image converting from PIL to NDArray. + """ + + def __init__(self, keys=None): + self.keys = keys + + def __call__(self, results): + """Callback function of PILImageToNDArray.""" + for key in self.keys: + img = results[key] + img = np.asarray(img) + results[key] = img + return results + + def __repr__(self): + """Repr function of PILImageToNDArray.""" + repr_str = self.__class__.__name__ + return repr_str + + +@PIPELINES.register_module() +class PostAug: + """Pipeline element that postaugments for mmcls. + + PostAug copies current augmented image and apply post augmentations for given keys. + For example, if we apply PostAug(keys=dict(img_strong=strong_pipeline), + PostAug will copy current augmented image and apply strong pipeline. + Post augmented image will be saved at results["img_strong"]. + + Expected entries in the 'results' dict that should be passed to this pipeline element are: + results['img']: PIL type image in data pipeline. + + Args: + keys (dict): keys to apply postaugmentaion. ex) dict(img_strong=strong_pipeline) + """ + + def __init__(self, keys: dict): + self.pipelines = {key: Compose(pipeline) for key, pipeline in keys.items()} + + def __call__(self, results): + """Callback function of PostAug.""" + for key, pipeline in self.pipelines.items(): + results[key] = pipeline(copy.deepcopy(results))["img"] + results["img_fields"].append(key) + return results + + def __repr__(self): + """Repr function of PostAug.""" + repr_str = self.__class__.__name__ + return repr_str diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/__init__.py new file mode 100644 index 00000000000..5644dffbf01 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/__init__.py @@ -0,0 +1,19 @@ +"""Module to init transforms for OTX classification.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# flake8: noqa + +from .augmix import AugMixAugment +from .otx_transforms import PILToTensor, RandomRotate, TensorNormalize +from .random_augment import OTXRandAugment +from .twocrop_transform import TwoCropTransform + +__all__ = [ + "AugMixAugment", + "PILToTensor", + "TensorNormalize", + "RandomRotate", + "OTXRandAugment", + "TwoCropTransform", +] diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/augmix.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/augmix.py new file mode 100644 index 00000000000..906c5634c62 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/augmix.py @@ -0,0 +1,236 @@ +"""Module for defining AugMix class used for classification task.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import random +import re +from copy import deepcopy + +import numpy as np +from mmcls.datasets.builder import PIPELINES +from mmcv.utils import ConfigDict +from PIL import Image + +from otx.algorithms.common.adapters.mmcv.pipelines.transforms.augments import ( + CythonAugments, +) + +_AUGMIX_TRANSFORMS_GREY = [ + "SharpnessIncreasing", # not in paper + "ShearX", + "ShearY", + "TranslateXRel", + "TranslateYRel", +] + +_AUGMIX_TRANSFORMS = [ + "AutoContrast", + "ColorIncreasing", # not in paper + "ContrastIncreasing", # not in paper + "BrightnessIncreasing", # not in paper + "SharpnessIncreasing", # not in paper + "Equalize", + "PosterizeIncreasing", + "SolarizeIncreasing", + "ShearX", + "ShearY", + "TranslateXRel", + "TranslateYRel", +] + + +class OpsFabric: + """OpsFabric class.""" + + def __init__(self, name, magnitude, hparams, prob=1.0): + self.max_level = 10 + self.prob = prob + self.hparams = hparams + # kwargs for augment functions + self.aug_kwargs = dict(fillcolor=hparams["img_mean"], resample=(Image.BILINEAR, Image.BICUBIC)) + self.level_to_arg = { + "AutoContrast": None, + "Equalize": None, + "Rotate": self._rotate_level_to_arg, + "PosterizeIncreasing": self._posterize_increasing_level_to_arg, + "SolarizeIncreasing": self._solarize_increasing_level_to_arg, + "ColorIncreasing": self._enhance_increasing_level_to_arg, + "ContrastIncreasing": self._enhance_increasing_level_to_arg, + "BrightnessIncreasing": self._enhance_increasing_level_to_arg, + "SharpnessIncreasing": self._enhance_increasing_level_to_arg, + "ShearX": self._shear_level_to_arg, + "ShearY": self._shear_level_to_arg, + "TranslateXRel": self._translate_rel_level_to_arg, + "TranslateYRel": self._translate_rel_level_to_arg, + } + self.name_to_op = { + "AutoContrast": CythonAugments.autocontrast, + "Equalize": CythonAugments.equalize, + "Rotate": CythonAugments.rotate, + "PosterizeIncreasing": CythonAugments.posterize, + "SolarizeIncreasing": CythonAugments.solarize, + "ColorIncreasing": CythonAugments.color, + "ContrastIncreasing": CythonAugments.contrast, + "BrightnessIncreasing": CythonAugments.brightness, + "SharpnessIncreasing": CythonAugments.sharpness, + "ShearX": CythonAugments.shear_x, + "ShearY": CythonAugments.shear_y, + "TranslateXRel": CythonAugments.translate_x_rel, + "TranslateYRel": CythonAugments.translate_y_rel, + } + self.aug_factory = ConfigDict( + aug_fn=self.name_to_op[name], + level_fn=self.level_to_arg[name], + magnitude=magnitude, + magnitude_std=self.hparams.get("magnitude_std", float("inf")), + ) + + @staticmethod + def randomly_negate(value): + """With 50% prob, negate the value.""" + # disable B311 random - used for the random sampling not for security/crypto + return -value if random.random() > 0.5 else value # nosec B311 + + def _rotate_level_to_arg(self, level, _hparams): + # range [-30, 30] + level = (level / self.max_level) * 30.0 + level = self.randomly_negate(level) + return (level,) + + def _enhance_increasing_level_to_arg(self, level, _hparams): + # range [0.1, 1.9] + level = (level / self.max_level) * 0.9 + level = 1.0 + self.randomly_negate(level) + return (level,) + + def _shear_level_to_arg(self, level, _hparams): + # range [-0.3, 0.3] + level = (level / self.max_level) * 0.3 + level = self.randomly_negate(level) + return (level,) + + def _translate_rel_level_to_arg(self, level, hparams): + # default range [-0.45, 0.45] + translate_pct = hparams.get("translate_pct", 0.45) + level = (level / self.max_level) * translate_pct + level = self.randomly_negate(level) + return (level,) + + def _posterize_level_to_arg(self, level, _hparams): + # range [0, 4], 'keep 0 up to 4 MSB of original image' + # intensity/severity of augmentation decreases with level + return (int((level / self.max_level) * 4),) + + def _posterize_increasing_level_to_arg(self, level, hparams): + # range [4, 0], 'keep 4 down to 0 MSB of original image', + # intensity/severity of augmentation increases with level + return (4 - self._posterize_level_to_arg(level, hparams)[0],) + + def _solarize_level_to_arg(self, level, _hparams): + # range [0, 256] + # intensity/severity of augmentation decreases with level + return (int((level / self.max_level) * 256),) + + def _solarize_increasing_level_to_arg(self, level, _hparams): + # range [0, 256] + # intensity/severity of augmentation increases with level + return (256 - self._solarize_level_to_arg(level, _hparams)[0],) + + def __call__(self, img): + """Call method of OpsFabric class.""" + # disable B311 random - used for the random sampling not for security/crypto + if self.prob < 1.0 and random.random() > self.prob: # nosec B311 + return img + magnitude = self.aug_factory.magnitude + magnitude_std = self.aug_factory.magnitude_std + level_fn = self.aug_factory.level_fn + if magnitude_std: + if magnitude_std == float("inf"): + # disable B311 random - used for the random sampling not for security/crypto + magnitude = random.uniform(0, magnitude) # nosec B311 + elif magnitude_std > 0: + magnitude = random.gauss(magnitude, magnitude_std) + magnitude = min(self.max_level, max(0, magnitude)) # clip to valid range + level_args = level_fn(magnitude, self.hparams) if level_fn is not None else tuple() + return self.aug_factory.aug_fn(img, *level_args, **self.aug_kwargs) + + +@PIPELINES.register_module() +class AugMixAugment: + """AugMix Transform. + + Adapted and improved from impl here: https://github.com/google-research/augmix/blob/master/imagenet.py + From paper: 'AugMix: A Simple Data Processing Method to Improve Robustness and Uncertainty - + https://arxiv.org/abs/1912.02781. + """ + + def __init__(self, config_str, image_mean=None, grey=False): + self.ops, self.alpha, self.width, self.depth = self._augmix_ops(config_str, image_mean, grey=grey) + + def _apply_basic(self, img, mixing_weights, m): # pylint: disable=invalid-name + # This is a literal adaptation of the paper/official implementation without normalizations and + # PIL <-> Numpy conversions between every op. It is still quite CPU compute heavy compared to the + # typical augmentation transforms, could use a GPU / Kornia implementation. + mixed = (1 - m) * np.array(img, dtype=np.float32) + for mix_weight in mixing_weights: + depth = self.depth if self.depth > 0 else np.random.randint(1, 4) + ops = np.random.choice(self.ops, depth, replace=True) + img_aug = deepcopy(img) + for op in ops: # pylint: disable=invalid-name + img_aug = op(img_aug) + CythonAugments.blend(img_aug, mixed, mix_weight * m) + np.clip(mixed, 0, 255.0, out=mixed) + return Image.fromarray(mixed.astype(np.uint8)) + + def _augmix_ops(self, config_str, image_mean=None, translate_const=250, grey=False): + if image_mean is None: + image_mean = [0.485, 0.456, 0.406] # imagenet mean + aug_params = ConfigDict(magnitude=3, width=3, depth=-1, alpha=1.0, p=1.0) + hparams = dict( + translate_const=translate_const, + img_mean=tuple(int(c * 256) for c in image_mean), + magnitude_std=float("inf"), + ) + config = config_str.split("-") + assert config[0] == "augmix" + config = config[1:] + for cfg in config: + cfgs = re.split(r"(\d.*)", cfg) + if len(cfgs) < 2: + continue + key, val = cfgs[:2] + if key == "mstd": + hparams.setdefault("magnitude_std", float(val)) + elif key == "m": + aug_params.magnitude = int(val) + elif key == "w": + aug_params.width = int(val) + elif key == "d": + aug_params.depth = int(val) + elif key == "a": + aug_params.alpha = float(val) + elif key == "p": + aug_params.p = float(val) + else: + assert False, "Unknown AugMix config section" + aug_politics = _AUGMIX_TRANSFORMS_GREY if grey else _AUGMIX_TRANSFORMS + return ( + [OpsFabric(name, aug_params.magnitude, hparams, aug_params.p) for name in aug_politics], + aug_params.alpha, + aug_params.width, + aug_params.depth, + ) + + def __call__(self, results): + """Call function applies augmix on image.""" + for key in results.get("img_fields", ["img"]): + img = results[key] + if not Image.isImageType(img): + img = Image.fromarray(img) + mixing_weights = np.float32(np.random.dirichlet([self.alpha] * self.width)) + m = np.float32(np.random.beta(self.alpha, self.alpha)) # pylint: disable=invalid-name + mixed = self._apply_basic(img, mixing_weights, m) + results["augmix"] = True + results[key] = mixed + return results diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/otx_transforms.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/otx_transforms.py new file mode 100644 index 00000000000..19f0e452d56 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/otx_transforms.py @@ -0,0 +1,79 @@ +"""Module for defining transforms used for OTX classification.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import random + +from mmcls.datasets.builder import PIPELINES +from PIL import Image +from torchvision.transforms import functional as F + + +@PIPELINES.register_module() +class PILToTensor: + """Convert PIL image to Tensor.""" + + def __call__(self, results): + """Call function of PILToTensor class.""" + for key in results.get("img_fields", ["img"]): + img = results[key] + if not Image.isImageType(img): + img = Image.fromarray(img) + img = F.to_tensor(img) + results["PILToTensor"] = True + results[key] = img + return results + + +@PIPELINES.register_module() +class TensorNormalize: + """Normalize tensor object.""" + + def __init__(self, mean, std, inplace=False): + self.mean = mean + self.std = std + self.inplace = inplace + + def __call__(self, results): + """Call function of TensorNormalize class.""" + for key in results.get("img_fields", ["img"]): + img = results[key] + img = F.normalize(img, self.mean, self.std, self.inplace) + results["TensorNormalize"] = True + results[key] = img + return results + + +# TODO [Jihwan]: Can be removed by mmcls.dataset.pipelines.auto_augment L398, Roate class +@PIPELINES.register_module() +class RandomRotate: + """Random rotate, from torchreid.data.transforms.""" + + def __init__(self, p=0.5, angle=(-5, 5), values=None): + self.p = p + self.angle = angle + self.discrete = values is not None and len([v for v in values if v != 0]) > 0 + self.values = values + + def __call__(self, results, *args, **kwargs): + """Call function of RandomRotate class.""" + # disable B311 random - used for the random sampling not for security/crypto + if random.uniform(0, 1) > self.p: # nosec B311 + return results + for key in results.get("img_fields", ["img"]): + img = results[key] + if self.discrete: + # disable B311 random - used for the random sampling not for security/crypto + rnd_angle = float(self.values[random.randint(0, len(self.values) - 1)]) # nosec B311 + else: + # disable B311 random - used for the random sampling not for security/crypto + rnd_angle = random.randint(self.angle[0], self.angle[1]) # nosec B311 + if not Image.isImageType(img): + img = Image.fromarray(img) + + img = F.rotate(img, rnd_angle, expand=False, center=None) + results["RandomRotate"] = True + results[key] = img + + return results diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/random_augment.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/random_augment.py new file mode 100644 index 00000000000..27cf45d0d19 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/random_augment.py @@ -0,0 +1,201 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=unused-argument +"""Code in this file is adapted from. + +https://github.com/ildoonet/pytorch-randaugment/blob/master/RandAugment/augmentations.py +https://github.com/google-research/fixmatch/blob/master/third_party/auto_augment/augmentations.py +https://github.com/google-research/fixmatch/blob/master/libml/ctaugment.py +""" + +import random + +import numpy as np +import PIL +from mmcls.datasets.builder import PIPELINES + +PARAMETER_MAX = 10 + + +def auto_contrast(img, **kwargs): + """Applies auto contrast to an image.""" + return PIL.ImageOps.autocontrast(img), None + + +def brightness(img, value, max_value, bias=0): + """Applies brightness adjustment to an image.""" + value = _float_parameter(value, max_value) + bias + return PIL.ImageEnhance.Brightness(img).enhance(value), value + + +def color(img, value, max_value, bias=0): + """Applies color adjustment to an image.""" + value = _float_parameter(value, max_value) + bias + return PIL.ImageEnhance.Color(img).enhance(value), value + + +def contrast(img, value, max_value, bias=0): + """Applies contrast adjustment to an image.""" + value = _float_parameter(value, max_value) + bias + return PIL.ImageEnhance.Contrast(img).enhance(value), value + + +def cutout(img, value, max_value, bias=0): + """Applies cutout augmentation to an image.""" + if value == 0: + return img + value = _float_parameter(value, max_value) + bias + value = int(value * min(img.size)) + return cutout_abs(img, value), value + + +def cutout_abs(img, value, **kwargs): + """Applies cutout with absolute pixel size to an image.""" + w, h = img.size + x0 = np.random.uniform(0, w) + y0 = np.random.uniform(0, h) + x0 = int(max(0, x0 - value / 2.0)) + y0 = int(max(0, y0 - value / 2.0)) + x1 = int(min(w, x0 + value)) + y1 = int(min(h, y0 + value)) + xy = (x0, y0, x1, y1) + # gray + rec_color = (127, 127, 127) + img = img.copy() + PIL.ImageDraw.Draw(img).rectangle(xy, rec_color) + return img, xy, rec_color + + +def equalize(img, **kwargs): + """Applies equalization to an image.""" + return PIL.ImageOps.equalize(img), None + + +def identity(img, **kwargs): + """Returns the original image without any transformation.""" + return img, None + + +def posterize(img, value, max_value, bias=0): + """Applies posterization to an image.""" + value = _int_parameter(value, max_value) + bias + return PIL.ImageOps.posterize(img, value), value + + +def rotate(img, value, max_value, bias=0): + """Applies rotation to an image.""" + value = _int_parameter(value, max_value) + bias + # disable B311 random - used for the random sampling not for security/crypto + if random.random() < 0.5: # nosec B311 + value = -value + return img.rotate(value), value + + +def sharpness(img, value, max_value, bias=0): + """Applies Sharpness to an image.""" + value = _float_parameter(value, max_value) + bias + return PIL.ImageEnhance.Sharpness(img).enhance(value), value + + +def shear_x(img, value, max_value, bias=0): + """Applies ShearX to an image.""" + value = _float_parameter(value, max_value) + bias + # disable B311 random - used for the random sampling not for security/crypto + if random.random() < 0.5: # nosec B311 + value = -value + return img.transform(img.size, PIL.Image.AFFINE, (1, value, 0, 0, 1, 0)), value + + +def shear_y(img, value, max_value, bias=0): + """Applies ShearY to an image.""" + value = _float_parameter(value, max_value) + bias + # disable B311 random - used for the random sampling not for security/crypto + if random.random() < 0.5: # nosec B311 + value = -value + return img.transform(img.size, PIL.Image.AFFINE, (1, 0, 0, value, 1, 0)), value + + +def solarize(img, value, max_value, bias=0): + """Applies Solarize to an image.""" + value = _int_parameter(value, max_value) + bias + return PIL.ImageOps.solarize(img, 256 - value), value + + +def translate_x(img, value, max_value, bias=0): + """Applies TranslateX to an image.""" + value = _float_parameter(value, max_value) + bias + # disable B311 random - used for the random sampling not for security/crypto + if random.random() < 0.5: # nosec B311 + value = -value + value = int(value * img.size[0]) + return img.transform(img.size, PIL.Image.AFFINE, (1, 0, value, 0, 1, 0)), value + + +def translate_y(img, value, max_value, bias=0): + """Applies TranslateX to an image.""" + value = _float_parameter(value, max_value) + bias + # disable B311 random - used for the random sampling not for security/crypto + if random.random() < 0.5: # nosec B311 + value = -value + value = int(value * img.size[1]) + return img.transform(img.size, PIL.Image.AFFINE, (1, 0, 0, 0, 1, value)), value + + +def _float_parameter(value, max_value): + return float(value) * max_value / PARAMETER_MAX + + +def _int_parameter(value, max_value): + return int(value * max_value / PARAMETER_MAX) + + +rand_augment_pool = [ + (auto_contrast, None, None), + (brightness, 0.9, 0.05), + (color, 0.9, 0.05), + (contrast, 0.9, 0.05), + (equalize, None, None), + (identity, None, None), + (posterize, 4, 4), + (rotate, 30, 0), + (sharpness, 0.9, 0.05), + (shear_x, 0.3, 0), + (shear_y, 0.3, 0), + (solarize, 256, 0), + (translate_x, 0.3, 0), + (translate_y, 0.3, 0), +] + + +# TODO: [Jihwan]: Can be removed by mmcls.datasets.pipeline.auto_augment Line 95 RandAugment class +@PIPELINES.register_module() +class OTXRandAugment: + """RandAugment class for OTX classification.""" + + def __init__(self, num_aug, magnitude, cutout_value=16): + assert num_aug >= 1 + assert 1 <= magnitude <= 10 + self.num_aug = num_aug + self.magnitude = magnitude + self.cutout_value = cutout_value + self.augment_pool = rand_augment_pool + + def __call__(self, results): + """Call function of OTXRandAugment class.""" + for key in results.get("img_fields", ["img"]): + img = results[key] + if not PIL.Image.isImageType(img): + img = PIL.Image.fromarray(results[key]) + # disable B311 random - used for the random sampling not for security/crypto + augs = random.choices(self.augment_pool, k=self.num_aug) # nosec B311 + for aug, max_value, bias in augs: + value = np.random.randint(1, self.magnitude) + # disable B311 random - used for the random sampling not for security/crypto + if random.random() < 0.5: # nosec B311 + img, value = aug(img, value=value, max_value=max_value, bias=bias) + results[f"rand_mc_{aug.__name__}"] = value + img, xy, rec_color = cutout_abs(img, self.cutout_value) + results["CutoutAbs"] = (xy, self.cutout_value, rec_color) + results[key] = np.array(img) + return results diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/twocrop_transform.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/twocrop_transform.py new file mode 100644 index 00000000000..4d8c5308c95 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/pipelines/transforms/twocrop_transform.py @@ -0,0 +1,28 @@ +"""Define TwoCropTransform used for self-sl in mmclassification.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from copy import deepcopy + +import numpy as np +from mmcls.datasets.builder import PIPELINES +from mmcls.datasets.pipelines import Compose, to_tensor +from mmcv.utils import build_from_cfg + + +@PIPELINES.register_module() +class TwoCropTransform: + """Generate two different cropped views of an image.""" + + def __init__(self, pipeline): + self.pipeline1 = Compose([build_from_cfg(p, PIPELINES) for p in pipeline]) + self.pipeline2 = Compose([build_from_cfg(p, PIPELINES) for p in pipeline]) + + def __call__(self, data): + """Call method for TwoCropTransform class.""" + data1 = self.pipeline1(deepcopy(data)) + data2 = self.pipeline2(deepcopy(data)) + + data = deepcopy(data1) + data["img"] = to_tensor(np.ascontiguousarray(np.stack((data1["img"], data2["img"]), axis=0))) + return data diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/models/__init__.py new file mode 100644 index 00000000000..873e9fca15f --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/__init__.py @@ -0,0 +1,56 @@ +"""OTX Algorithms - Classification Models.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .classifiers import BYOL, CustomImageClassifier, SemiSLClassifier, SupConClassifier +from .heads import ( + ClsHead, + ConstrastiveHead, + ConvClsHead, + CustomHierarchicalLinearClsHead, + CustomHierarchicalNonLinearClsHead, + CustomLinearClsHead, + CustomMultiLabelLinearClsHead, + CustomMultiLabelNonLinearClsHead, + CustomNonLinearClsHead, + MMOVClsHead, + SemiLinearMultilabelClsHead, + SemiNonLinearMultilabelClsHead, + SupConClsHead, +) +from .losses import ( + AsymmetricAngularLossWithIgnore, + AsymmetricLossWithIgnore, + BarlowTwinsLoss, + CrossEntropyLossWithIgnore, + IBLoss, +) +from .necks import SelfSLMLP + +__all__ = [ + "BYOL", + "CustomImageClassifier", + "SemiSLClassifier", + "SupConClassifier", + "CustomLinearClsHead", + "CustomNonLinearClsHead", + "CustomMultiLabelNonLinearClsHead", + "CustomMultiLabelLinearClsHead", + "CustomHierarchicalLinearClsHead", + "CustomHierarchicalNonLinearClsHead", + "AsymmetricAngularLossWithIgnore", + "SemiLinearMultilabelClsHead", + "SemiNonLinearMultilabelClsHead", + "MMOVClsHead", + "ConvClsHead", + "ClsHead", + "AsymmetricLossWithIgnore", + "BarlowTwinsLoss", + "IBLoss", + "CrossEntropyLossWithIgnore", + "SelfSLMLP", + "ConstrastiveHead", + "SupConClsHead", +] diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/backbones/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/models/backbones/__init__.py new file mode 100644 index 00000000000..07decd4f247 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/backbones/__init__.py @@ -0,0 +1,19 @@ +"""OTX Algorithms - Classification Backbones.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .mmov_backbone import MMOVBackbone + +__all__ = ["MMOVBackbone"] diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/backbones/mmov_backbone.py b/src/otx/algorithms/classification/adapters/mmcls/models/backbones/mmov_backbone.py new file mode 100644 index 00000000000..1480701c509 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/backbones/mmov_backbone.py @@ -0,0 +1,43 @@ +"""Module for the MMOVBackbone class.""" + +from typing import Dict, List, Union + +from mmcls.models.builder import BACKBONES + +from otx.core.ov.graph.parsers.cls import cls_base_parser +from otx.core.ov.models.mmov_model import MMOVModel + + +@BACKBONES.register_module() +class MMOVBackbone(MMOVModel): + """MMOVBackbone class. + + Args: + *args: positional arguments. + **kwargs: keyword arguments. + """ + + @staticmethod + def parser(graph, **kwargs) -> Dict[str, Union[List[str], Dict[str, List[str]]]]: + """Parses the input and output of the model. + + Args: + graph: input graph. + **kwargs: keyword arguments. + + Returns: + Dictionary containing input and output of the model. + """ + output = cls_base_parser(graph, "backbone") + if output is None: + raise ValueError("Parser can not determine input and output of model. Please provide them explicitly") + return output + + def init_weights(self, pretrained=None): # pylint: disable=unused-argument + """Initializes the weights of the model. + + Args: + pretrained: pretrained weights. Default: None. + """ + # TODO + return diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/__init__.py new file mode 100644 index 00000000000..432ff849ac1 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/__init__.py @@ -0,0 +1,13 @@ +"""OTX Algorithms - Classification Classifiers.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .byol import BYOL +from .custom_image_classifier import CustomImageClassifier +from .semisl_classifier import SemiSLClassifier +from .semisl_multilabel_classifier import SemiSLMultilabelClassifier +from .supcon_classifier import SupConClassifier + +__all__ = ["BYOL", "CustomImageClassifier", "SemiSLClassifier", "SemiSLMultilabelClassifier", "SupConClassifier"] diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/byol.py b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/byol.py new file mode 100644 index 00000000000..82e994a7a96 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/byol.py @@ -0,0 +1,227 @@ +"""BYOL (Bootstrap Your Own Latent) implementation for self-supervised learning. + +Original papers: +- 'Bootstrap Your Own Latent: A New Approach to Self-Supervised Learning', https://arxiv.org/abs/2006.07733 +""" + +import copy + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=missing-module-docstring, too-many-instance-attributes, unused-argument, unnecessary-pass, invalid-name # noqa: E501 +from collections import OrderedDict +from typing import Any, Dict, Optional + +import torch +import torch.distributed as dist +from mmcls.models.builder import CLASSIFIERS, build_backbone, build_head, build_neck +from torch import nn + +from otx.utils.logger import get_logger + +logger = get_logger() + + +@CLASSIFIERS.register_module() +class BYOL(nn.Module): + """BYOL Implementation. + + Implementation of 'Bootstrap Your Own Latent: A New Approach to Self-Supervised + Learning (https://arxiv.org/abs/2006.07733)'. + + Args: + backbone (dict): Config dict for module of backbone ConvNet. + neck (dict): Config dict for module of deep features to compact feature vectors. + Default: None. + head (dict): Config dict for module of loss functions. Default: None. + pretrained (str, optional): Path to pre-trained weights. Default: None. + base_momentum (float): The base momentum coefficient for the target network. + Default: 0.996. + **kwargs: Addition keyword arguments. + """ + + def __init__( + self, + backbone: Dict[str, Any], + neck: Optional[Dict[str, Any]] = None, + head: Optional[Dict[str, Any]] = None, + pretrained: Optional[str] = None, + base_momentum: float = 0.996, + **kwargs, # pylint: disable=unused-argument + ): + + super().__init__() + + # build backbone + self.online_backbone = build_backbone(backbone) + + target_backbone_cfg = copy.deepcopy(backbone) + target_backbone_cfg["pretrained"] = None + + self.target_backbone = build_backbone(target_backbone_cfg) + + # build projector + self.online_projector = build_neck(neck) + self.target_projector = build_neck(neck) + + # build head with predictor + self.head = build_head(head) + + self.init_weights(pretrained=pretrained) + + self.base_momentum = base_momentum + self.momentum = base_momentum + + # Hooks for super_type transparent weight save + self._register_state_dict_hook(self.state_dict_hook) + + def init_weights(self, pretrained: Optional[str] = None): + """Initialize the weights of model. + + Args: + pretrained (str, optional): Path to pre-trained weights. + Default: None. + """ + if pretrained is not None: + logger.info(f"load model from: {pretrained}") + + # init backbone + self.online_backbone.init_weights(pretrained=pretrained) + for param_ol, param_tgt in zip(self.online_backbone.parameters(), self.target_backbone.parameters()): + param_tgt.data.copy_(param_ol.data) + param_tgt.requires_grad = False + param_ol.requires_grad = True + + # init projector + self.online_projector.init_weights(init_linear="kaiming") + for param_ol, param_tgt in zip(self.online_projector.parameters(), self.target_projector.parameters()): + param_tgt.data.copy_(param_ol.data) + param_tgt.requires_grad = False + param_ol.requires_grad = True + + # init the predictor + self.head.init_weights() + + @torch.no_grad() + def _momentum_update(self): + """Momentum update of the target network.""" + for param_ol, param_tgt in zip(self.online_backbone.parameters(), self.target_backbone.parameters()): + param_tgt.data = param_tgt.data * self.momentum + param_ol.data * (1.0 - self.momentum) + + for param_ol, param_tgt in zip(self.online_projector.parameters(), self.target_projector.parameters()): + param_tgt.data = param_tgt.data * self.momentum + param_ol.data * (1.0 - self.momentum) + + @torch.no_grad() + def momentum_update(self): + """Momentum update of the target network.""" + self._momentum_update() + + def forward(self, img1: torch.Tensor, img2: torch.Tensor, **kwargs): + """Forward computation during training. + + Args: + img1 (Tensor): Input of two concatenated images of shape (N, C, H, W). + Typically these should be mean centered and std scaled. + img2 (Tensor): Input of two concatenated images of shape (N, C, H, W). + Typically these should be mean centered and std scaled. + **kwargs: Addition keyword arguments. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + proj_1 = self.online_projector(self.online_backbone(img1)) + proj_2 = self.online_projector(self.online_backbone(img2)) + with torch.no_grad(): + proj_1_tgt = self.target_projector(self.target_backbone(img1)).clone().detach() + proj_2_tgt = self.target_projector(self.target_backbone(img2)).clone().detach() + + loss = self.head(proj_1, proj_2_tgt)["loss"] + self.head(proj_2, proj_1_tgt)["loss"] + return dict(loss=loss) + + def train_step(self, data: Dict[str, Any], optimizer): + """The iteration step during training. + + This method defines an iteration step during training, except for the + back propagation and optimizer updating, which are done in an optimizer + hook. Note that in some complicated cases or models, the whole process + including back propagation and optimizer updating are also defined in + this method, such as GAN. + + Args: + data (dict): The output of dataloader. + optimizer (:obj:`torch.optim.Optimizer` | dict): The optimizer of + runner is passed to ``train_step()``. This argument is unused + and reserved. + + Returns: + dict: It should contain at least 3 keys: ``loss``, ``log_vars``, + ``num_samples``. + ``loss`` is a tensor for back propagation, which can be a + weighted sum of multiple losses. + ``log_vars`` contains all the variables to be sent to the + logger. + ``num_samples`` indicates the batch size (when the model is + DDP, it means the batch size on each GPU), which is used for + averaging the logs. + """ + losses = self(**data) + loss, log_vars = self._parse_losses(losses) + + outputs = dict(loss=loss, log_vars=log_vars, num_samples=len(data["img1"].data)) + + return outputs + + def val_step(self, *args): + """Disable validation step during self-supervised learning.""" + pass + + def _parse_losses(self, losses: Dict[str, Any]): + """Parse loss dictionary. + + Args: + losses (dict): Raw output of the network, which usually contain + losses and other necessary information. + + Returns: + tuple[Tensor, dict]: (loss, log_vars), loss is the loss tensor + which may be a weighted sum of all losses, log_vars contains + all the variables to be sent to the logger. + """ + log_vars = OrderedDict() + for loss_name, loss_value in losses.items(): + if isinstance(loss_value, torch.Tensor): + log_vars[loss_name] = loss_value.mean() + elif isinstance(loss_value, list): + log_vars[loss_name] = sum(_loss.mean() for _loss in loss_value) + elif isinstance(loss_value, dict): + for name, value in loss_value.items(): + log_vars[name] = value + else: + raise TypeError(f"{loss_name} is not a tensor or list of tensors") + + loss = sum(_value for _key, _value in log_vars.items() if "loss" in _key) + + log_vars["loss"] = loss + for loss_name, loss_value in log_vars.items(): + # reduce loss when distributed training + if dist.is_available() and dist.is_initialized(): + loss_value = loss_value.data.clone() + dist.all_reduce(loss_value.div_(dist.get_world_size())) + log_vars[loss_name] = loss_value.item() + + return loss, log_vars + + @staticmethod + def state_dict_hook(module, state_dict, prefix, *args, **kwargs): + """Save only online backbone as output state_dict.""" + logger.info("----------------- BYOL.state_dict_hook() called") + for k in list(state_dict.keys()): + v = state_dict.pop(k) + if not prefix or k.startswith(prefix): + k = k.replace(prefix, "", 1) + if k.startswith("online_backbone."): + k = k.replace("online_backbone.", "", 1) + k = prefix + k + state_dict[k] = v + return state_dict diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/custom_image_classifier.py b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/custom_image_classifier.py new file mode 100644 index 00000000000..d2e23dadda5 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/custom_image_classifier.py @@ -0,0 +1,393 @@ +"""Module for defining SAMClassifier for classification task.""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from functools import partial + +import torch +from mmcls.models.backbones.vision_transformer import VisionTransformer +from mmcls.models.builder import CLASSIFIERS +from mmcls.models.classifiers.image import ImageClassifier +from mmcls.models.utils import resize_pos_embed + +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ViTReciproCAMHook +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.utils.logger import get_logger + +from .mixin import ClsLossDynamicsTrackingMixin, SAMClassifierMixin + +logger = get_logger() + + +def is_hierarchical_chkpt(chkpt: dict): + """Detect whether previous checkpoint is hierarchical or not.""" + for k, v in chkpt.items(): + if "fc" in k: + return True + return False + + +@CLASSIFIERS.register_module() +class CustomImageClassifier(SAMClassifierMixin, ClsLossDynamicsTrackingMixin, ImageClassifier): + """SAM-enabled ImageClassifier.""" + + def __init__(self, task_adapt=None, **kwargs): + if "multilabel" in kwargs: + self.multilabel = kwargs.pop("multilabel") + if "hierarchical" in kwargs: + self.hierarchical = kwargs.pop("hierarchical") + super().__init__(**kwargs) + self.is_export = False + # Hooks for redirect state_dict load/save + self._register_state_dict_hook(self.state_dict_hook) + self._register_load_state_dict_pre_hook(partial(self.load_state_dict_pre_hook, self)) + if task_adapt: + self._register_load_state_dict_pre_hook( + partial( + self.load_state_dict_mixing_hook, + self, # model + task_adapt["dst_classes"], # model_classes + task_adapt["src_classes"], # chkpt_classes + ) + ) + + def forward_train(self, img, gt_label, **kwargs): + """Forward computation during training. + + Args: + img (Tensor): of shape (N, C, H, W) encoding input images. + Typically these should be mean centered and std scaled. + + gt_label (Tensor): It should be of shape (N, 1) encoding the + ground-truth label of input images for single label task. It + shoulf be of shape (N, C) encoding the ground-truth label + of input images for multi-labels task. + + **kwargs (Any): Addition keyword arguments. + + Returns: + dict[str, Tensor]: a dictionary of loss components + """ + if self.augments is not None: + img, gt_label = self.augments(img, gt_label) + + x = self.extract_feat(img) + + losses = dict() + + if self.multilabel or self.hierarchical: + loss = self.head.forward_train(x, gt_label, **kwargs) + else: + gt_label = gt_label.squeeze(dim=1) + loss = self.head.forward_train(x, gt_label) + + losses.update(loss) + + return losses + + @staticmethod + def state_dict_hook(module, state_dict, prefix, *args, **kwargs): # noqa: C901 + # pylint: disable=unused-argument, too-many-branches + """Redirect model as output state_dict for OTX model compatibility.""" + backbone_type = type(module.backbone).__name__ + if backbone_type not in ["OTXMobileNetV3", "OTXEfficientNet", "OTXEfficientNetV2"]: + return None + + if backbone_type == "OTXMobileNetV3": # pylint: disable=too-many-nested-blocks + for key in list(state_dict.keys()): + val = state_dict.pop(key) + if not prefix or key.startswith(prefix): + key = key.replace(prefix, "", 1) + if key.startswith("backbone"): + key = key.replace("backbone.", "", 1) + elif key.startswith("head"): + key = key.replace("head.", "", 1) + if "3" in key: # OTX uses "classifier.4". Convert for OTX compatibility. + key = key.replace("3", "4") + if module.multilabel and not module.is_export: + val = val.t() + key = prefix + key + state_dict[key] = val + + elif backbone_type == "OTXEfficientNet": + for key in list(state_dict.keys()): + val = state_dict.pop(key) + if not prefix or key.startswith(prefix): + key = key.replace(prefix, "", 1) + if key.startswith("backbone"): + key = key.replace("backbone.", "", 1) + elif key.startswith("head"): + key = key.replace("head", "output", 1) + if not module.hierarchical and not module.is_export: + key = key.replace("fc", "asl") + val = val.t() + key = prefix + key + state_dict[key] = val + + elif backbone_type == "OTXEfficientNetV2": + for key in list(state_dict.keys()): + val = state_dict.pop(key) + if not prefix or key.startswith(prefix): + key = key.replace(prefix, "", 1) + if key.startswith("backbone"): + key = key.replace("backbone.", "", 1) + elif key == "head.fc.weight": + key = key.replace("head.fc", "model.classifier") + if not module.hierarchical and not module.is_export: + val = val.t() + key = prefix + key + state_dict[key] = val + + return state_dict + + @staticmethod + def load_state_dict_pre_hook(module, state_dict, prefix, *args, **kwargs): # noqa: C901 + # pylint: disable=unused-argument, too-many-branches + """Redirect input state_dict to model for OTX model compatibility.""" + backbone_type = type(module.backbone).__name__ + if backbone_type not in ["OTXMobileNetV3", "OTXEfficientNet", "OTXEfficientNetV2"]: + return + + if backbone_type == "OTXMobileNetV3": # pylint: disable=too-many-nested-blocks + for key in list(state_dict.keys()): + val = state_dict.pop(key) + if not prefix or key.startswith(prefix): + key = key.replace(prefix, "", 1) + if key.startswith("classifier."): + if "4" in key: + key = "head." + key.replace("4", "3") + if module.multilabel: + val = val.t() + else: + key = "head." + key + elif key.startswith("act"): + key = "head." + key + elif not key.startswith("backbone."): + key = "backbone." + key + key = prefix + key + state_dict[key] = val + + elif backbone_type == "OTXEfficientNet": + for key in list(state_dict.keys()): + val = state_dict.pop(key) + if not prefix or key.startswith(prefix): + key = key.replace(prefix, "", 1) + if key.startswith("features.") and "activ" not in key: + key = "backbone." + key + elif key.startswith("output."): + key = key.replace("output", "head") + if not module.hierarchical: + key = key.replace("asl", "fc") + val = val.t() + key = prefix + key + state_dict[key] = val + + elif backbone_type == "OTXEfficientNetV2": + for key in list(state_dict.keys()): + val = state_dict.pop(key) + if not prefix or key.startswith(prefix): + key = key.replace(prefix, "", 1) + if key.startswith("model.classifier"): + key = key.replace("model.classifier", "head.fc") + if not module.hierarchical: + val = val.t() + elif key.startswith("model"): + key = "backbone." + key + key = prefix + key + state_dict[key] = val + else: + logger.info("conversion is not required.") + + @staticmethod + def load_state_dict_mixing_hook( + model, model_classes, chkpt_classes, chkpt_dict, prefix, *args, **kwargs + ): # pylint: disable=unused-argument, too-many-branches, too-many-locals + """Modify input state_dict according to class name matching before weight loading. + + If previous training is hierarchical training, + then the current training should be hierarchical training. vice versa. + + """ + backbone_type = type(model.backbone).__name__ + if backbone_type not in ["OTXMobileNetV3", "OTXEfficientNet", "OTXEfficientNetV2"]: + return + + if model.hierarchical != is_hierarchical_chkpt(chkpt_dict): + return + + # Dst to src mapping index + model_classes = list(model_classes) + chkpt_classes = list(chkpt_classes) + model2chkpt = map_class_names(model_classes, chkpt_classes) + logger.info(f"{chkpt_classes} -> {model_classes} ({model2chkpt})") + model_dict = model.state_dict() + + if backbone_type == "OTXMobileNetV3": + if model.multilabel: + param_names = ["classifier.4.weight"] + else: + param_names = ["classifier.4.weight", "classifier.4.bias"] + + elif backbone_type == "OTXEfficientNet": + if not model.hierarchical: + param_names = ["output.asl.weight"] + else: + param_names = ["output.fc.weight"] + + elif backbone_type == "OTXEfficientNetV2": + param_names = [ + "model.classifier.weight", + ] + if "head.fc.bias" in chkpt_dict.keys(): + param_names.append("head.fc.bias") + + for model_name in param_names: + model_param = model_dict[model_name].clone() + if backbone_type == "OTXMobileNetV3": + chkpt_name = "head." + model_name.replace("4", "3") + if model.multilabel: + model_param = model_param.t() + elif backbone_type in "OTXEfficientNet": + chkpt_name = model_name.replace("output", "head") + if not model.hierarchical: + chkpt_name = chkpt_name.replace("asl", "fc") + model_param = model_param.t() + + elif backbone_type in "OTXEfficientNetV2": + if model_name.endswith("bias"): + chkpt_name = model_name + else: + chkpt_name = model_name.replace("model.classifier", "head.fc") + if not model.hierarchical: + model_param = model_param.t() + + if model_name not in model_dict or chkpt_name not in chkpt_dict: + logger.info(f"Skipping weight copy: {chkpt_name}") + continue + + # Mix weights + # NOTE: Label mix is not supported for H-label classification. + if not model.hierarchical: + chkpt_param = chkpt_dict[chkpt_name] + for module, c in enumerate(model2chkpt): + if c >= 0: + model_param[module].copy_(chkpt_param[c]) + + # Replace checkpoint weight by mixed weights + chkpt_dict[chkpt_name] = model_param + + def extract_feat(self, img): + """Directly extract features from the backbone + neck. + + Overriding for OpenVINO export with features + """ + x = self.backbone(img) + # For Global Backbones (det/seg/etc..), + # In case of tuple or list, only the feat of the last layer is used. + if isinstance(x, (tuple, list)): + x = x[-1] + + if self.with_neck: + x = self.neck(x) + + return x + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER + + from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( # pylint: disable=ungrouped-imports + FeatureVectorHook, + ReciproCAMHook, + ) + + def _extract_vit_feat(model, x): + """Modified forward from mmcls.models.backbones.vision_transformer.VisionTransformer.forward().""" + B = x.shape[0] + x, patch_resolution = model.backbone.patch_embed(x) + + # stole cls_tokens impl from Phil Wang, thanks + cls_tokens = model.backbone.cls_token.expand(B, -1, -1) + x = torch.cat((cls_tokens, x), dim=1) + x = x + resize_pos_embed( + model.backbone.pos_embed, + model.backbone.patch_resolution, + patch_resolution, + mode=model.backbone.interpolate_mode, + num_extra_tokens=model.backbone.num_extra_tokens, + ) + x = model.backbone.drop_after_pos(x) + + if not model.backbone.with_cls_token: + # Remove class token for transformer encoder input + x = x[:, 1:] + + feat = None + layernorm_feat = None + for i, layer in enumerate(model.backbone.layers): + if i == len(model.backbone.layers) - 1: + layernorm_feat = layer.norm1(x) + + x = layer(x) + + if i == len(model.backbone.layers) - 1 and model.backbone.final_norm: + x = model.backbone.norm1(x) + + if i in model.backbone.out_indices: + B, _, C = x.shape + if model.backbone.with_cls_token: + patch_token = x[:, 1:].reshape(B, *patch_resolution, C) + patch_token = patch_token.permute(0, 3, 1, 2) + cls_token = x[:, 0] + else: + patch_token = x.reshape(B, *patch_resolution, C) + patch_token = patch_token.permute(0, 3, 1, 2) + cls_token = None + if model.backbone.output_cls_token: + feat = [patch_token, cls_token] + else: + feat = patch_token + if model.with_neck: + feat = model.neck(feat) + return feat, layernorm_feat + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.classification.adapters.mmcls.models.classifiers.CustomImageClassifier.extract_feat" + ) + def sam_image_classifier__extract_feat(ctx, self, img): # pylint: disable=unused-argument + """Feature extraction function for SAMClassifier with mmdeploy.""" + feat = self.backbone(img) + # For Global Backbones (det/seg/etc..), + # In case of tuple or list, only the feat of the last layer is used. + if isinstance(feat, (tuple, list)): + feat = feat[-1] + backbone_feat = feat + if self.with_neck: + feat = self.neck(feat) + return feat, backbone_feat + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.classification.adapters.mmcls.models.classifiers.CustomImageClassifier.simple_test" + ) + def sam_image_classifier__simple_test(ctx, self, img, img_metas): # pylint: disable=unused-argument + """Simple test function used for inference for SAMClassifier with mmdeploy.""" + vit_backbone = isinstance(self.backbone, VisionTransformer) + if vit_backbone: + feat, layernorm_feat = _extract_vit_feat(self, img) + else: + feat, backbone_feat = self.extract_feat(img) + logit = self.head.simple_test(feat) + + if ctx.cfg["dump_features"]: + if vit_backbone: + assert self.backbone.with_cls_token + _, cls_token = feat + feature_vector = cls_token + saliency_map = ViTReciproCAMHook(self).func(layernorm_feat) + else: + saliency_map = ReciproCAMHook(self).func(backbone_feat) + feature_vector = FeatureVectorHook.func(backbone_feat) + return logit, feature_vector, saliency_map + + return logit diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/mixin.py b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/mixin.py new file mode 100644 index 00000000000..99d899356e1 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/mixin.py @@ -0,0 +1,126 @@ +"""Module defining Mix-in class of SAMClassifier.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from collections import defaultdict +from typing import Any, Dict, List + +import datumaro as dm +import numpy as np +import pandas as pd + +from otx.api.entities.dataset_item import DatasetItemEntityWithID +from otx.core.data.noisy_label_detection import ( + LossDynamicsTracker, + LossDynamicsTrackingMixin, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +class SAMClassifierMixin: + """SAM-enabled BaseClassifier mix-in.""" + + def train_step(self, data, optimizer=None, **kwargs): + """Saving current batch data to compute SAM gradient.""" + self.current_batch = data + return super().train_step(data, optimizer, **kwargs) + + +class MultiClassClsLossDynamicsTracker(LossDynamicsTracker): + """Loss dynamics tracker for multi-class classification task.""" + + TASK_NAME = "OTX-MultiClassCls" + + def __init__(self) -> None: + super().__init__() + self._loss_dynamics: Dict[Any, List] = defaultdict(list) + + def _convert_anns(self, item: DatasetItemEntityWithID): + labels = [ + dm.Label(label=self.otx_label_map[label.id_]) + for ann in item.get_annotations() + for label in ann.get_labels() + ] + return labels + + def accumulate(self, outputs, iter) -> None: + """Accumulate training loss dynamics for each training step.""" + entity_ids = outputs["entity_ids"] + label_ids = np.squeeze(outputs["label_ids"]) + loss_dyns = outputs["loss_dyns"] + + for entity_id, label_id, loss_dyn in zip(entity_ids, label_ids, loss_dyns): + self._loss_dynamics[(entity_id, label_id)].append((iter, loss_dyn)) + + def export(self, output_path: str) -> None: + """Export loss dynamics statistics to Datumaro format.""" + df = pd.DataFrame.from_dict( + { + k: (np.array([iter for iter, _ in arr]), np.array([value for _, value in arr])) + for k, arr in self._loss_dynamics.items() + }, + orient="index", + columns=["iters", "loss_dynamics"], + ) + + for (entity_id, label_id), row in df.iterrows(): + item = self._export_dataset.get(entity_id, "train") + for ann in item.annotations: + if isinstance(ann, dm.Label) and ann.label == self.otx_label_map[label_id]: + ann.attributes = row.to_dict() + + self._export_dataset.export(output_path, format="datumaro") + + +class ClsLossDynamicsTrackingMixin(LossDynamicsTrackingMixin): + """Mix-in to track loss dynamics during training for classification tasks.""" + + def __init__(self, track_loss_dynamics: bool = False, **kwargs): + if track_loss_dynamics: + if getattr(self, "multilabel", False) or getattr(self, "hierarchical", False): + raise RuntimeError("multilabel or hierarchical tasks are not supported now.") + + head_cfg = kwargs.get("head", None) + loss_cfg = head_cfg.get("loss", None) + loss_cfg["reduction"] = "none" + + # This should be called after modifying "reduction" config. + super().__init__(**kwargs) + + # This should be called after super().__init__(), + # since LossDynamicsTrackingMixin.__init__() creates self._loss_dyns_tracker + self._loss_dyns_tracker = MultiClassClsLossDynamicsTracker() + + def train_step(self, data, optimizer=None, **kwargs): + """The iteration step for training. + + If self._track_loss_dynamics = False, just follow BaseClassifier.train_step(). + Otherwise, it steps with tracking loss dynamics. + """ + if self.loss_dyns_tracker.initialized: + return self._train_step_with_tracking(data, optimizer, **kwargs) + return super().train_step(data, optimizer, **kwargs) + + def _train_step_with_tracking(self, data, optimizer=None, **kwargs): + losses = self(**data) + + loss_dyns = losses["loss"].detach().cpu().numpy() + assert not np.isscalar(loss_dyns) + + entity_ids = [img_meta["entity_id"] for img_meta in data["img_metas"]] + label_ids = [img_meta["label_id"] for img_meta in data["img_metas"]] + loss, log_vars = self._parse_losses(losses) + + outputs = dict( + loss=loss, + log_vars=log_vars, + loss_dyns=loss_dyns, + entity_ids=entity_ids, + label_ids=label_ids, + num_samples=len(data["img"].data), + ) + + return outputs diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/semisl_classifier.py b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/semisl_classifier.py new file mode 100644 index 00000000000..a4eddee15b8 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/semisl_classifier.py @@ -0,0 +1,53 @@ +"""Module for defining a semi-supervised classifier using mmcls.""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcls.models.builder import CLASSIFIERS + +from otx.utils.logger import get_logger + +from .custom_image_classifier import CustomImageClassifier + +logger = get_logger() + + +@CLASSIFIERS.register_module() +class SemiSLClassifier(CustomImageClassifier): + """Semi-SL Classifier. + + This classifier supports unlabeled data by overriding forward_train + """ + + def forward_train(self, imgs, **kwargs): + """Data is transmitted as a classifier training function. + + Args: + imgs (list[Tensor]): List of tensors of shape (1, C, H, W) + Typically these should be mean centered and std scaled. + kwargs (keyword arguments): Specific to concrete implementation + """ + if "gt_label" not in kwargs: + raise ValueError("'gt_label' does not exist in the labeled image") + if "extra_0" not in kwargs: + raise ValueError("'extra_0' does not exist in the dataset") + target = kwargs["gt_label"].squeeze(dim=1) + unlabeled_data = kwargs["extra_0"] + x = {} + x["labeled"] = self.extract_feat(imgs) + + img_uw = unlabeled_data["img"] + # weakly augmented images are used only for getting the pseudo label. + # not required to calculate gradients. + with torch.no_grad(): + x["unlabeled_weak"] = self.extract_feat(img_uw) + + img_us = unlabeled_data["img_strong"] + x["unlabeled_strong"] = self.extract_feat(img_us) + + losses = dict() + loss = self.head.forward_train(x, target) + losses.update(loss) + + return losses diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/semisl_multilabel_classifier.py b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/semisl_multilabel_classifier.py new file mode 100644 index 00000000000..aac9ed9688b --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/semisl_multilabel_classifier.py @@ -0,0 +1,49 @@ +"""Module for defining a semi-supervised multi-label classifier using mmcls.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcls.models.builder import CLASSIFIERS + +from otx.utils.logger import get_logger + +from .custom_image_classifier import CustomImageClassifier + +logger = get_logger() + + +@CLASSIFIERS.register_module() +class SemiSLMultilabelClassifier(CustomImageClassifier): + """Semi-SL Multilabel Classifier which supports unlabeled data by overriding forward_train.""" + + def forward_train(self, img, gt_label, **kwargs): + """Data is transmitted as a classifier training function. + + Args: + img (list[Tensor]): List of tensors of shape (1, C, H, W) + Typically these should be mean centered and std scaled. + gt_label (Tensor): Ground truth labels for the input labeled images + kwargs (keyword arguments): Specific to concrete implementation + """ + if "extra_0" not in kwargs: + raise ValueError("'extra_0' does not exist in the dataset") + if "img_strong" not in kwargs: + raise ValueError("'img_strong' does not exist in the dataset") + + target = gt_label.squeeze() + unlabeled_data = kwargs["extra_0"] + x = {} + x["labeled_weak"] = self.extract_feat(img) + x["labeled_strong"] = self.extract_feat(kwargs["img_strong"]) + + img_uw = unlabeled_data["img"] + x["unlabeled_weak"] = self.extract_feat(img_uw) + + img_us = unlabeled_data["img_strong"] + x["unlabeled_strong"] = self.extract_feat(img_us) + + losses = dict() + loss = self.head.forward_train(x, target) + losses.update(loss) + + return losses diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/supcon_classifier.py b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/supcon_classifier.py new file mode 100644 index 00000000000..af08e00d348 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/supcon_classifier.py @@ -0,0 +1,33 @@ +"""This module contains the SupConClassifier implementation for MMClassification.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcls.models.builder import CLASSIFIERS +from mmcls.models.classifiers.image import ImageClassifier + + +@CLASSIFIERS.register_module() +class SupConClassifier(ImageClassifier): + """SupConClassifier with support for classification tasks.""" + + def __init__(self, backbone, neck=None, head=None, pretrained=None, task_adapt=None, **kwargs): + self.multilabel = kwargs.pop("multilabel", False) + self.hierarchical = kwargs.pop("hierarchical", False) + self.task_adapt = task_adapt + super().__init__(backbone, neck=neck, head=head, pretrained=pretrained, **kwargs) + + def forward_train(self, img, gt_label, **kwargs): + """Concatenate the different image views along the batch size.""" + if len(img.shape) == 5: + img = torch.cat([img[:, d, :, :, :] for d in range(img.shape[1])], dim=0) + x = self.extract_feat(img) + losses = dict() + if self.multilabel or self.hierarchical: + loss = self.head.forward_train(x, gt_label, **kwargs) + else: + gt_label = gt_label.squeeze(dim=1) + loss = self.head.forward_train(x, gt_label) + losses.update(loss) + return losses diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/__init__.py new file mode 100644 index 00000000000..5b6a0a69e5b --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/__init__.py @@ -0,0 +1,53 @@ +"""OTX Algorithms - Classification Heads.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .cls_head import ClsHead +from .contrastive_head import ConstrastiveHead +from .conv_head import ConvClsHead +from .custom_cls_head import CustomLinearClsHead, CustomNonLinearClsHead +from .custom_hierarchical_linear_cls_head import CustomHierarchicalLinearClsHead +from .custom_hierarchical_non_linear_cls_head import CustomHierarchicalNonLinearClsHead +from .custom_multi_label_linear_cls_head import CustomMultiLabelLinearClsHead +from .custom_multi_label_non_linear_cls_head import CustomMultiLabelNonLinearClsHead +from .custom_vision_transformer_head import CustomVisionTransformerClsHead +from .mmov_cls_head import MMOVClsHead +from .non_linear_cls_head import NonLinearClsHead +from .semisl_cls_head import SemiLinearClsHead, SemiNonLinearClsHead +from .semisl_multilabel_cls_head import ( + SemiLinearMultilabelClsHead, + SemiNonLinearMultilabelClsHead, +) +from .supcon_cls_head import SupConClsHead + +__all__ = [ + "ConstrastiveHead", + "CustomLinearClsHead", + "CustomNonLinearClsHead", + "CustomHierarchicalLinearClsHead", + "CustomHierarchicalNonLinearClsHead", + "CustomMultiLabelLinearClsHead", + "CustomMultiLabelNonLinearClsHead", + "SemiLinearMultilabelClsHead", + "SemiNonLinearMultilabelClsHead", + "CustomVisionTransformerClsHead", + "NonLinearClsHead", + "SemiLinearClsHead", + "SemiNonLinearClsHead", + "SupConClsHead", + "MMOVClsHead", + "ConvClsHead", + "ClsHead", +] diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/cls_head.py new file mode 100644 index 00000000000..1cf23a187fb --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/cls_head.py @@ -0,0 +1,33 @@ +"""Module defining Classification Head for MMOV inference.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcls.models.builder import HEADS +from mmcls.models.heads import ClsHead as OriginClsHead + + +@HEADS.register_module(force=True) +class ClsHead(OriginClsHead): + """Classification Head for MMOV inference.""" + + def __init__(self, *args, **kwargs): + do_squeeze = kwargs.pop("do_squeeze", False) + super().__init__(*args, **kwargs) + self._do_squeeze = do_squeeze + + def forward(self, x): + """Forward fuction of ClsHead class.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label): + """Forward_train fuction of ClsHead class.""" + if self._do_squeeze: + cls_score = cls_score.unsqueeze(0).squeeze() + return super().forward_train(cls_score, gt_label) + + def simple_test(self, cls_score): + """Test without augmentation.""" + if self._do_squeeze: + cls_score = cls_score.unsqueeze(0).squeeze() + return super().simple_test(cls_score) diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/contrastive_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/contrastive_head.py new file mode 100644 index 00000000000..e60938d5cae --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/contrastive_head.py @@ -0,0 +1,54 @@ +"""Contrastive head to get contrastive loss with predictor head.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=unused-argument +from typing import Any, Dict + +import torch +import torch.nn.functional as F +from mmcls.models.builder import HEADS, build_neck +from torch import nn + + +@HEADS.register_module() +class ConstrastiveHead(nn.Module): + """Head for contrastive learning. + + Args: + predictor (dict): configurations for predictor. + size_average (bool): whether averaging loss using batch size. Default value is True. + """ + + def __init__(self, predictor: Dict[str, Any], size_average: bool = True, **kwargs): + super().__init__() + self.predictor = build_neck(predictor) + self.size_average = size_average + + def init_weights(self, init_linear: str = "normal"): + """Initialize predictor weights. + + Args: + init_linear (str): Option to initialize weights. Default: 'normal' + """ + self.predictor.init_weights(init_linear=init_linear) + + def forward(self, inputs: torch.Tensor, targets: torch.Tensor): + """Forward head. + + Args: + inputs (Tensor): NxC input features. + targets (Tensor): NxC target features. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + pred = self.predictor(inputs) + pred_norm = F.normalize(pred, dim=1) + target_norm = F.normalize(targets, dim=1) + loss = 2 * inputs.size(0) - 2 * (pred_norm * target_norm).sum() + if self.size_average: + loss /= inputs.size(0) + + return dict(loss=loss) diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/conv_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/conv_head.py new file mode 100644 index 00000000000..c13fbd99229 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/conv_head.py @@ -0,0 +1,83 @@ +"""Module for defining ConvClsHead used for MMOV inference.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch.nn.functional as F +from mmcls.models.builder import HEADS +from mmcls.models.heads import ClsHead +from torch import nn + + +@HEADS.register_module() +class ConvClsHead(ClsHead): + """Convolutional classifier head. + + Args: + num_classes (int): Number of categories excluding the background + category. + in_channels (int): Number of channels in the input feature map. + init_cfg (dict | optional): The extra init config of layers. + Defaults to use dict(type='Normal', layer='Linear', std=0.01). + """ + + def __init__(self, num_classes, in_channels, init_cfg=None, **kwargs): + init_cfg = init_cfg if init_cfg else dict(type="Kaiming", layer=["Conv2d"]) + super().__init__(init_cfg=init_cfg, **kwargs) + + self.in_channels = in_channels + self.num_classes = num_classes + + if self.num_classes <= 0: + raise ValueError(f"num_classes={num_classes} must be a positive integer") + + self.conv = nn.Conv2d(self.in_channels, self.num_classes, (1, 1)) + + def pre_logits(self, x): + """Preprocess logits.""" + if isinstance(x, tuple): + x = x[-1] + return x + + def simple_test(self, x, softmax=True, post_process=True): + """Inference without augmentation. + + Args: + x (tuple[Tensor]): The input features. + Multi-stage inputs are acceptable but only the last stage will + be used to classify. The shape of every item should be + ``(num_samples, in_channels)``. + softmax (bool): Whether to softmax the classification score. + post_process (bool): Whether to do post processing the + inference results. It will convert the output to a list. + + Returns: + Tensor | list: The inference results. + + - If no post processing, the output is a tensor with shape + ``(num_samples, num_classes)``. + - If post processing, the output is a multi-dimentional list of + float and the dimensions are ``(num_samples, num_classes)``. + """ + x = self.pre_logits(x) + cls_score = self.conv(x).squeeze() + + if softmax: + pred = F.softmax(cls_score, dim=1) if cls_score is not None else None + else: + pred = cls_score + + if post_process: + return self.post_process(pred) + return pred + + def forward(self, x): + """Forward fuction of ConvClsHead class.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label, **kwargs): + """Forward_train fuction of ConvClsHead class.""" + x = self.pre_logits(cls_score) + cls_score = self.conv(x).squeeze() + losses = self.loss(cls_score, gt_label, **kwargs) + return losses diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_cls_head.py new file mode 100644 index 00000000000..119be69caae --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_cls_head.py @@ -0,0 +1,110 @@ +"""Module defining for OTX Custom Non-linear classification head.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +import torch.nn.functional as F +from mmcls.models.builder import HEADS +from mmcls.models.heads import LinearClsHead + +from otx.algorithms.common.utils import cast_bf16_to_fp32 + +from .non_linear_cls_head import NonLinearClsHead + + +@HEADS.register_module() +class CustomNonLinearClsHead(NonLinearClsHead): + """Custom Nonlinear classifier head.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.loss_type = kwargs.get("loss", dict(type="CrossEntropyLoss"))["type"] + + def loss(self, cls_score, gt_label, feature=None): + """Calculate loss for given cls_score/gt_label.""" + num_samples = len(cls_score) + losses = dict() + # compute loss + if self.loss_type == "IBLoss": + loss = self.compute_loss(cls_score, gt_label, feature=feature) + else: + loss = self.compute_loss(cls_score, gt_label, avg_factor=num_samples) + if self.cal_acc: + # compute accuracy + acc = self.compute_accuracy(cls_score, gt_label) + assert len(acc) == len(self.topk) + losses["accuracy"] = {f"top-{k}": a for k, a in zip(self.topk, acc)} + losses["loss"] = loss + return losses + + def forward(self, x): + """Forward fuction of CustomNonLinearHead class.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label): + """Forward_train fuction of CustomNonLinearHead class.""" + bs = cls_score.shape[0] + if bs == 1: + cls_score = torch.cat([cls_score, cls_score], dim=0) + gt_label = torch.cat([gt_label, gt_label], dim=0) + logit = self.classifier(cls_score) + losses = self.loss(logit, gt_label, feature=cls_score) + return losses + + +@HEADS.register_module() +class CustomLinearClsHead(LinearClsHead): + """Custom linear classifier head. + + Args: + num_classes (int): Number of categories excluding the background + category. + in_channels (int): Number of channels in the input feature map. + init_cfg (dict | optional): The extra init config of layers. + Defaults to use dict(type='Normal', layer='Linear', std=0.01). + """ + + def __init__(self, num_classes, in_channels, init_cfg=None, **kwargs): + init_cfg = init_cfg if init_cfg else dict(type="Normal", layer="Linear", std=0.01) + super().__init__(num_classes, in_channels, init_cfg=init_cfg, **kwargs) + self.loss_type = kwargs.get("loss", dict(type="CrossEntropyLoss"))["type"] + + def loss(self, cls_score, gt_label, feature=None): + """Calculate loss for given cls_score/gt_label.""" + num_samples = len(cls_score) + losses = dict() + # compute loss + if self.loss_type == "IBLoss": + loss = self.compute_loss(cls_score, gt_label, feature=feature) + else: + loss = self.compute_loss(cls_score, gt_label, avg_factor=num_samples) + if self.cal_acc: + # compute accuracy + acc = self.compute_accuracy(cls_score, gt_label) + assert len(acc) == len(self.topk) + losses["accuracy"] = {f"top-{k}": a for k, a in zip(self.topk, acc)} + losses["loss"] = loss + return losses + + def simple_test(self, img): + """Test without augmentation.""" + cls_score = self.fc(img) + if isinstance(cls_score, list): + cls_score = sum(cls_score) / float(len(cls_score)) + if torch.onnx.is_in_onnx_export(): + return cls_score + pred = F.softmax(cls_score, dim=1) if cls_score is not None else None + pred = cast_bf16_to_fp32(pred) + + return self.post_process(pred) + + def forward(self, x): + """Forward fuction of CustomLinearHead class.""" + return self.simple_test(x) + + def forward_train(self, x, gt_label): + """Forward_train fuction of CustomLinearHead class.""" + cls_score = self.fc(x) + losses = self.loss(cls_score, gt_label, feature=x) + return losses diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_hierarchical_linear_cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_hierarchical_linear_cls_head.py new file mode 100644 index 00000000000..3e0de200be2 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_hierarchical_linear_cls_head.py @@ -0,0 +1,182 @@ +"""Module for defining Linear classification head for h-label classification.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcls.models.builder import HEADS, build_loss +from mmcls.models.heads import MultiLabelClsHead +from mmcv.cnn import normal_init +from torch import nn + +from .mixin import OTXHeadMixin + + +@HEADS.register_module() +class CustomHierarchicalLinearClsHead(OTXHeadMixin, MultiLabelClsHead): + """Custom Linear classification head for hierarchical classification task. + + Args: + num_classes (int): Number of categories. + in_channels (int): Number of channels in the input feature map. + loss (dict): Config of classification loss. + multilabel_loss (dict): Config of multi-label classification loss. + """ + + def __init__( + self, + num_classes, + in_channels, + loss=None, + multilabel_loss=None, + **kwargs, + ): + loss = loss if loss else dict(type="CrossEntropyLoss", use_sigmoid=True, reduction="mean", loss_weight=1.0) + multilabel_loss = ( + multilabel_loss if multilabel_loss else dict(type="AsymmetricLoss", reduction="mean", loss_weight=1.0) + ) + self.hierarchical_info = kwargs.pop("hierarchical_info", None) + assert self.hierarchical_info + super().__init__(loss=loss) + if self.hierarchical_info["num_multiclass_heads"] + self.hierarchical_info["num_multilabel_classes"] == 0: + raise ValueError("Invalid classification heads configuration") + self.compute_multilabel_loss = False + if self.hierarchical_info["num_multilabel_classes"] > 0: + self.compute_multilabel_loss = build_loss(multilabel_loss) + + if num_classes <= 0: + raise ValueError(f"num_classes={num_classes} must be a positive integer") + + self.in_channels = in_channels + self.num_classes = num_classes + self._init_layers() + + def _init_layers(self): + self.fc = nn.Linear(self.in_channels, self.num_classes) + + def init_weights(self): + """Initialize weights of head.""" + normal_init(self.fc, mean=0, std=0.01, bias=0) + + def loss(self, cls_score, gt_label, multilabel=False, valid_label_mask=None): + """Calculate loss for given cls_score/gt_label.""" + num_samples = len(cls_score) + # compute loss + if multilabel: + gt_label = gt_label.type_as(cls_score) + # map difficult examples to positive ones + _gt_label = torch.abs(gt_label) + + loss = self.compute_multilabel_loss( + cls_score, _gt_label, valid_label_mask=valid_label_mask, avg_factor=num_samples + ) + else: + loss = self.compute_loss(cls_score, gt_label, avg_factor=num_samples) + + return loss + + def forward(self, x): + """Forward fuction of CustomHierarchicalLinearClsHead.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label, **kwargs): + """Forward_train fuction of CustomHierarchicalLinearClsHead class.""" + img_metas = kwargs.get("img_metas", None) + cls_score = self.pre_logits(cls_score) + gt_label = gt_label.type_as(cls_score) + cls_score = self.fc(cls_score) + + losses = dict(loss=0.0) + num_effective_heads_in_batch = 0 + for i in range(self.hierarchical_info["num_multiclass_heads"]): + if i not in self.hierarchical_info["empty_multiclass_head_indices"]: + head_gt = gt_label[:, i] + head_logits = cls_score[ + :, + self.hierarchical_info["head_idx_to_logits_range"][str(i)][0] : self.hierarchical_info[ + "head_idx_to_logits_range" + ][str(i)][1], + ] + valid_mask = head_gt >= 0 + head_gt = head_gt[valid_mask].long() + if len(head_gt) > 0: + head_logits = head_logits[valid_mask, :] + multiclass_loss = self.loss(head_logits, head_gt) + losses["loss"] += multiclass_loss + num_effective_heads_in_batch += 1 + + if num_effective_heads_in_batch > 0: + losses["loss"] /= num_effective_heads_in_batch + + if self.compute_multilabel_loss: + head_gt = gt_label[:, self.hierarchical_info["num_multiclass_heads"] :] + head_logits = cls_score[:, self.hierarchical_info["num_single_label_classes"] :] + valid_batch_mask = head_gt >= 0 + head_gt = head_gt[ + valid_batch_mask, + ] + head_logits = head_logits[ + valid_batch_mask, + ] + + # multilabel_loss is assumed to perform no batch averaging + if img_metas is not None: + valid_label_mask = self.get_valid_label_mask(img_metas=img_metas)[ + :, self.hierarchical_info["num_single_label_classes"] : + ] + valid_label_mask = valid_label_mask.to(cls_score.device) + valid_label_mask = valid_label_mask[valid_batch_mask] + else: + valid_label_mask = None + multilabel_loss = self.loss(head_logits, head_gt, multilabel=True, valid_label_mask=valid_label_mask) + losses["loss"] += multilabel_loss.mean() + return losses + + def simple_test(self, img): + """Test without augmentation.""" + img = self.pre_logits(img) + cls_score = self.fc(img) + if isinstance(cls_score, list): + cls_score = sum(cls_score) / float(len(cls_score)) + + multiclass_logits = [] + for i in range(self.hierarchical_info["num_multiclass_heads"]): + multiclass_logit = cls_score[ + :, + self.hierarchical_info["head_idx_to_logits_range"][str(i)][0] : self.hierarchical_info[ + "head_idx_to_logits_range" + ][str(i)][1], + ] + if not torch.onnx.is_in_onnx_export(): + multiclass_logit = torch.softmax(multiclass_logit, dim=1) + multiclass_logits.append(multiclass_logit) + multiclass_pred = torch.cat(multiclass_logits, dim=1) if multiclass_logits else None + + if self.compute_multilabel_loss: + multilabel_logits = cls_score[:, self.hierarchical_info["num_single_label_classes"] :] + if not torch.onnx.is_in_onnx_export(): + multilabel_pred = torch.sigmoid(multilabel_logits) if multilabel_logits is not None else None + else: + multilabel_pred = multilabel_logits + if multiclass_pred is not None: + pred = torch.cat([multiclass_pred, multilabel_pred], axis=1) + else: + pred = multilabel_pred + else: + pred = multiclass_pred + + if torch.onnx.is_in_onnx_export(): + return pred + pred = list(pred.detach().cpu().numpy()) + return pred + + def get_valid_label_mask(self, img_metas): + """Get valid label with ignored_label mask.""" + valid_label_mask = [] + for meta in img_metas: + mask = torch.Tensor([1 for _ in range(self.num_classes)]) + if "ignored_labels" in meta and meta["ignored_labels"]: + mask[meta["ignored_labels"]] = 0 + valid_label_mask.append(mask) + valid_label_mask = torch.stack(valid_label_mask, dim=0) + return valid_label_mask diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_hierarchical_non_linear_cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_hierarchical_non_linear_cls_head.py new file mode 100644 index 00000000000..69ea7bb1476 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_hierarchical_non_linear_cls_head.py @@ -0,0 +1,212 @@ +"""Non-linear classification head for hierarhical classification task.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcls.models.builder import HEADS, build_loss +from mmcls.models.heads import MultiLabelClsHead +from mmcv.cnn import build_activation_layer, constant_init, normal_init +from torch import nn + +from .mixin import OTXHeadMixin + + +@HEADS.register_module() +class CustomHierarchicalNonLinearClsHead( + OTXHeadMixin, MultiLabelClsHead +): # pylint: disable=too-many-instance-attributes + """Custom NonLinear classification head for hierarchical classification task. + + Args: + num_classes (int): Number of categories excluding the background + category. + in_channels (int): Number of channels in the input feature map. + hid_channels (int): Number of channels of hidden layer. + act_cfg (dict): Config of activation layer. + loss (dict): Config of classification loss. + multilabel_loss (dict): Config of multi-label classification loss. + """ + + def __init__( + self, + num_classes, + in_channels, + hid_channels=1280, + act_cfg=None, + loss=None, + multilabel_loss=None, + dropout=False, + **kwargs, + ): # pylint: disable=too-many-arguments + act_cfg = act_cfg if act_cfg else dict(type="ReLU") + loss = loss if loss else dict(type="CrossEntropyLoss", use_sigmoid=True, reduction="mean", loss_weight=1.0) + multilabel_loss = ( + multilabel_loss if multilabel_loss else dict(type="AsymmetricLoss", reduction="mean", loss_weight=1.0) + ) + self.hierarchical_info = kwargs.pop("hierarchical_info", None) + assert self.hierarchical_info + super().__init__(loss=loss) + if self.hierarchical_info["num_multiclass_heads"] + self.hierarchical_info["num_multilabel_classes"] == 0: + raise ValueError("Invalid classification heads configuration") + self.compute_multilabel_loss = False + if self.hierarchical_info["num_multilabel_classes"] > 0: + self.compute_multilabel_loss = build_loss(multilabel_loss) + + if num_classes <= 0: + raise ValueError(f"num_classes={num_classes} must be a positive integer") + + self.in_channels = in_channels + self.hid_channels = hid_channels + self.num_classes = num_classes + self.act = build_activation_layer(act_cfg) + self.dropout = dropout + self._init_layers() + + def _init_layers(self): + if self.dropout: + self.classifier = nn.Sequential( + nn.Linear(self.in_channels, self.hid_channels), + nn.BatchNorm1d(self.hid_channels), + self.act, + nn.Dropout(p=0.2), + nn.Linear(self.hid_channels, self.num_classes), + ) + else: + self.classifier = nn.Sequential( + nn.Linear(self.in_channels, self.hid_channels), + nn.BatchNorm1d(self.hid_channels), + self.act, + nn.Linear(self.hid_channels, self.num_classes), + ) + + def init_weights(self): + """Iniitialize weights of classification head.""" + for module in self.classifier: + if isinstance(module, nn.Linear): + normal_init(module, mean=0, std=0.01, bias=0) + elif isinstance(module, nn.BatchNorm1d): + constant_init(module, 1) + + def loss(self, cls_score, gt_label, multilabel=False, valid_label_mask=None): + """Calculate loss for given cls_score and gt_label.""" + num_samples = len(cls_score) + # compute loss + if multilabel: + gt_label = gt_label.type_as(cls_score) + # map difficult examples to positive ones + _gt_label = torch.abs(gt_label) + + loss = self.compute_multilabel_loss( + cls_score, _gt_label, valid_label_mask=valid_label_mask, avg_factor=num_samples + ) + else: + loss = self.compute_loss(cls_score, gt_label, avg_factor=num_samples) + + return loss + + def forward(self, x): + """Forward fuction of CustomHierarchicalNonLinearClsHead class.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label, **kwargs): + """Forward_train fuction of CustomHierarchicalNonLinearClsHead class.""" + img_metas = kwargs.get("img_metas", None) + cls_score = self.pre_logits(cls_score) + gt_label = gt_label.type_as(cls_score) + cls_score = self.classifier(cls_score) + + losses = dict(loss=0.0) + num_effective_heads_in_batch = 0 + for i in range(self.hierarchical_info["num_multiclass_heads"]): + if i not in self.hierarchical_info["empty_multiclass_head_indices"]: + head_gt = gt_label[:, i] + head_logits = cls_score[ + :, + self.hierarchical_info["head_idx_to_logits_range"][str(i)][0] : self.hierarchical_info[ + "head_idx_to_logits_range" + ][str(i)][1], + ] + valid_mask = head_gt >= 0 + head_gt = head_gt[valid_mask].long() + if len(head_gt) > 0: + head_logits = head_logits[valid_mask, :] + multiclass_loss = self.loss(head_logits, head_gt) + losses["loss"] += multiclass_loss + num_effective_heads_in_batch += 1 + + if num_effective_heads_in_batch > 0: + losses["loss"] /= num_effective_heads_in_batch + + if self.compute_multilabel_loss: + head_gt = gt_label[:, self.hierarchical_info["num_multiclass_heads"] :] + head_logits = cls_score[:, self.hierarchical_info["num_single_label_classes"] :] + valid_batch_mask = head_gt >= 0 + head_gt = head_gt[ + valid_batch_mask, + ] + head_logits = head_logits[ + valid_batch_mask, + ] + + # multilabel_loss is assumed to perform no batch averaging + if img_metas is not None: + valid_label_mask = self.get_valid_label_mask(img_metas=img_metas)[ + :, self.hierarchical_info["num_single_label_classes"] : + ] + valid_label_mask = valid_label_mask.to(cls_score.device) + valid_label_mask = valid_label_mask[valid_batch_mask] + else: + valid_label_mask = None + multilabel_loss = self.loss(head_logits, head_gt, multilabel=True, valid_label_mask=valid_label_mask) + losses["loss"] += multilabel_loss.mean() + return losses + + def simple_test(self, img): + """Test without augmentation.""" + img = self.pre_logits(img) + cls_score = self.classifier(img) + if isinstance(cls_score, list): + cls_score = sum(cls_score) / float(len(cls_score)) + + multiclass_logits = [] + for i in range(self.hierarchical_info["num_multiclass_heads"]): + multiclass_logit = cls_score[ + :, + self.hierarchical_info["head_idx_to_logits_range"][str(i)][0] : self.hierarchical_info[ + "head_idx_to_logits_range" + ][str(i)][1], + ] + if not torch.onnx.is_in_onnx_export(): + multiclass_logit = torch.softmax(multiclass_logit, dim=1) + multiclass_logits.append(multiclass_logit) + multiclass_pred = torch.cat(multiclass_logits, dim=1) if multiclass_logits else None + + if self.compute_multilabel_loss: + multilabel_logits = cls_score[:, self.hierarchical_info["num_single_label_classes"] :] + if not torch.onnx.is_in_onnx_export(): + multilabel_pred = torch.sigmoid(multilabel_logits) if multilabel_logits is not None else None + else: + multilabel_pred = multilabel_logits + if multiclass_pred is not None: + pred = torch.cat([multiclass_pred, multilabel_pred], axis=1) + else: + pred = multilabel_pred + else: + pred = multiclass_pred + + if torch.onnx.is_in_onnx_export(): + return pred + pred = list(pred.detach().cpu().numpy()) + return pred + + def get_valid_label_mask(self, img_metas): + """Get valid label mask with ignored_label.""" + valid_label_mask = [] + for meta in img_metas: + mask = torch.Tensor([1 for _ in range(self.num_classes)]) + if "ignored_labels" in meta and meta["ignored_labels"]: + mask[meta["ignored_labels"]] = 0 + valid_label_mask.append(mask) + valid_label_mask = torch.stack(valid_label_mask, dim=0) + return valid_label_mask diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_multi_label_linear_cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_multi_label_linear_cls_head.py new file mode 100644 index 00000000000..cb0b1ecf1ea --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_multi_label_linear_cls_head.py @@ -0,0 +1,143 @@ +"""Module for defining multi-label linear classification head.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +import torch.nn.functional as F +from mmcls.models.builder import HEADS +from mmcls.models.heads import MultiLabelClsHead +from mmcv.cnn import normal_init +from torch import nn + +from .mixin import OTXHeadMixin + + +@HEADS.register_module() +class CustomMultiLabelLinearClsHead(OTXHeadMixin, MultiLabelClsHead): + """Custom Linear classification head for multilabel task. + + Args: + num_classes (int): Number of categories. + in_channels (int): Number of channels in the input feature map. + normalized (bool): Normalize input features and weights. + scale (float): positive scale parameter. + loss (dict): Config of classification loss. + """ + + def __init__( + self, + num_classes, + in_channels, + normalized=False, + scale=1.0, + loss=None, + ): + loss = loss if loss else dict(type="CrossEntropyLoss", use_sigmoid=True, reduction="mean", loss_weight=1.0) + super().__init__(loss=loss) + if num_classes <= 0: + raise ValueError(f"num_classes={num_classes} must be a positive integer") + + self.in_channels = in_channels + self.num_classes = num_classes + self.normalized = normalized + self.scale = scale + self._init_layers() + + def _init_layers(self): + if self.normalized: + self.fc = AnglularLinear(self.in_channels, self.num_classes) + else: + self.fc = nn.Linear(self.in_channels, self.num_classes) + + def init_weights(self): + """Initialize weights of head.""" + if isinstance(self.fc, nn.Linear): + normal_init(self.fc, mean=0, std=0.01, bias=0) + + def loss(self, cls_score, gt_label, valid_label_mask=None): + """Calculate loss for given cls_score/gt_label.""" + gt_label = gt_label.type_as(cls_score) + num_samples = len(cls_score) + losses = dict() + + # map difficult examples to positive ones + _gt_label = torch.abs(gt_label) + # compute loss + loss = self.compute_loss(cls_score, _gt_label, valid_label_mask=valid_label_mask, avg_factor=num_samples) + losses["loss"] = loss / self.scale + return losses + + def forward(self, x): + """Forward fuction of CustomMultiLabelLinearClsHead class.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label, **kwargs): + """Forward_train fuction of CustomMultiLabelLinearClsHead.""" + img_metas = kwargs.get("img_metas", False) + cls_score = self.pre_logits(cls_score) + gt_label = gt_label.type_as(cls_score) + cls_score = self.fc(cls_score) * self.scale + + valid_batch_mask = gt_label >= 0 + gt_label = gt_label[ + valid_batch_mask, + ].view(gt_label.shape[0], -1) + cls_score = cls_score[ + valid_batch_mask, + ].view(cls_score.shape[0], -1) + if img_metas: + valid_label_mask = self.get_valid_label_mask(img_metas=img_metas) + valid_label_mask = valid_label_mask.to(cls_score.device) + valid_label_mask = valid_label_mask[ + valid_batch_mask, + ].view(valid_label_mask.shape[0], -1) + losses = self.loss(cls_score, gt_label, valid_label_mask=valid_label_mask) + else: + losses = self.loss(cls_score, gt_label) + return losses + + def simple_test(self, img): + """Test without augmentation.""" + img = self.pre_logits(img) + cls_score = self.fc(img) * self.scale + if isinstance(cls_score, list): + cls_score = sum(cls_score) / float(len(cls_score)) + if torch.onnx.is_in_onnx_export(): + return cls_score + pred = torch.sigmoid(cls_score) if cls_score is not None else None + pred = list(pred.detach().cpu().numpy()) + return pred + + def get_valid_label_mask(self, img_metas): + """Get valid label mask using ignored_label.""" + valid_label_mask = [] + for meta in img_metas: + mask = torch.Tensor([1 for _ in range(self.num_classes)]) + if "ignored_labels" in meta and meta["ignored_labels"]: + mask[meta["ignored_labels"]] = 0 + valid_label_mask.append(mask) + valid_label_mask = torch.stack(valid_label_mask, dim=0) + return valid_label_mask + + +class AnglularLinear(nn.Module): + """Computes cos of angles between input vectors and weights vectors. + + Args: + in_features (int): Number of input features. + out_features (int): Number of output cosine logits. + """ + + def __init__(self, in_features, out_features): + """Init fuction of AngularLinear class.""" + super().__init__() + self.in_features = in_features + self.out_features = out_features + self.weight = nn.Parameter(torch.Tensor(self.out_features, self.in_features)) + self.weight.data.normal_().renorm_(2, 0, 1e-5).mul_(1e5) + + def forward(self, x): + """Forward fuction of AngularLinear class.""" + cos_theta = F.normalize(x.view(x.shape[0], -1), dim=1).mm(F.normalize(self.weight.t(), p=2, dim=0)) + return cos_theta.clamp(-1, 1) diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_multi_label_non_linear_cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_multi_label_non_linear_cls_head.py new file mode 100644 index 00000000000..4a114b3326c --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_multi_label_non_linear_cls_head.py @@ -0,0 +1,149 @@ +"""This module contains the CustomMultiLabelNonLinearClsHead implementation for MMClassification.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcls.models.builder import HEADS +from mmcls.models.heads import MultiLabelClsHead +from mmcv.cnn import build_activation_layer, constant_init, normal_init +from torch import nn + +from .custom_multi_label_linear_cls_head import AnglularLinear +from .mixin import OTXHeadMixin + + +@HEADS.register_module() +class CustomMultiLabelNonLinearClsHead(OTXHeadMixin, MultiLabelClsHead): + """Non-linear classification head for multilabel task. + + Args: + num_classes (int): Number of categories. + in_channels (int): Number of channels in the input feature map. + loss (dict): Config of classification loss. + scale (float): positive scale parameter. + init_cfg (dict | optional): The extra init config of layers. + Defaults to use dict(type='Normal', layer='Linear', std=0.01). + normalized (bool): Normalize input features and weights in the last linar layer. + """ + + # pylint: disable=too-many-arguments + def __init__( + self, + num_classes, + in_channels, + hid_channels=1280, + act_cfg=None, + scale=1.0, + loss=None, + dropout=False, + normalized=False, + ): + act_cfg = act_cfg if act_cfg else dict(type="ReLU") + loss = loss if loss else dict(type="CrossEntropyLoss", use_sigmoid=True, reduction="mean", loss_weight=1.0) + super().__init__(loss=loss) + + self.in_channels = in_channels + self.num_classes = num_classes + self.hid_channels = hid_channels + self.dropout = dropout + self.normalized = normalized + self.scale = scale + + if self.num_classes <= 0: + raise ValueError(f"num_classes={num_classes} must be a positive integer") + + self._init_layers(act_cfg) + + def _init_layers(self, act_cfg): + modules = [ + nn.Linear(self.in_channels, self.hid_channels), + nn.BatchNorm1d(self.hid_channels), + build_activation_layer(act_cfg), + ] + if self.dropout: + modules.append(nn.Dropout(p=0.2)) + if self.normalized: + modules.append(AnglularLinear(self.hid_channels, self.num_classes)) + else: + modules.append(nn.Linear(self.hid_channels, self.num_classes)) + + self.classifier = nn.Sequential(*modules) + + def init_weights(self): + """Iniitalize weights of model.""" + for module in self.classifier: + if isinstance(module, nn.Linear): + normal_init(module, mean=0, std=0.01, bias=0) + elif isinstance(module, nn.BatchNorm1d): + constant_init(module, 1) + + def loss(self, cls_score, gt_label, valid_label_mask=None): + """Calculate loss for given cls_score/gt_label.""" + gt_label = gt_label.type_as(cls_score) + num_samples = len(cls_score) + losses = dict() + + # map difficult examples to positive ones + _gt_label = torch.abs(gt_label) + # compute loss + loss = self.compute_loss( + cls_score, + _gt_label, + valid_label_mask=valid_label_mask, + avg_factor=num_samples, + ) + losses["loss"] = loss / self.scale + return losses + + def forward(self, x): + """Forward fuction of CustomMultiLabelNonLinearClsHead.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label, **kwargs): + """Forward_train fuction of CustomMultiLabelNonLinearClsHead.""" + img_metas = kwargs.get("img_metas", False) + cls_score = self.pre_logits(cls_score) + gt_label = gt_label.type_as(cls_score) + cls_score = self.classifier(cls_score) * self.scale + + valid_batch_mask = gt_label >= 0 + gt_label = gt_label[ + valid_batch_mask, + ].view(gt_label.shape[0], -1) + cls_score = cls_score[ + valid_batch_mask, + ].view(cls_score.shape[0], -1) + if img_metas: + valid_label_mask = self.get_valid_label_mask(img_metas=img_metas) + valid_label_mask = valid_label_mask.to(cls_score.device) + valid_label_mask = valid_label_mask[ + valid_batch_mask, + ].view(valid_label_mask.shape[0], -1) + losses = self.loss(cls_score, gt_label, valid_label_mask=valid_label_mask) + else: + losses = self.loss(cls_score, gt_label) + return losses + + def simple_test(self, img): + """Test without augmentation.""" + img = self.pre_logits(img) + cls_score = self.classifier(img) * self.scale + if isinstance(cls_score, list): + cls_score = sum(cls_score) / float(len(cls_score)) + if torch.onnx.is_in_onnx_export(): + return cls_score + pred = torch.sigmoid(cls_score) if cls_score is not None else None + pred = list(pred.detach().cpu().numpy()) + return pred + + def get_valid_label_mask(self, img_metas): + """Get valid label with ignored_label mask.""" + valid_label_mask = [] + for meta in img_metas: + mask = torch.Tensor([1 for _ in range(self.num_classes)]) + if "ignored_labels" in meta and meta["ignored_labels"]: + mask[meta["ignored_labels"]] = 0 + valid_label_mask.append(mask) + valid_label_mask = torch.stack(valid_label_mask, dim=0) + return valid_label_mask diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_vision_transformer_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_vision_transformer_head.py new file mode 100644 index 00000000000..38a2d704c2c --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/custom_vision_transformer_head.py @@ -0,0 +1,40 @@ +"""Module to define CustomVisionTransformerClsHead for classification task.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcls.models.builder import HEADS +from mmcls.models.heads import VisionTransformerClsHead + + +@HEADS.register_module() +class CustomVisionTransformerClsHead(VisionTransformerClsHead): + """Custom Vision Transformer classifier head which supports IBLoss loss calculation.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.loss_type = kwargs.get("loss", dict(type="CrossEntropyLoss"))["type"] + + def loss(self, cls_score, gt_label, feature=None): + """Calculate loss for given cls_score/gt_label.""" + num_samples = len(cls_score) + losses = dict() + # compute loss + if self.loss_type == "IBLoss": + loss = self.compute_loss(cls_score, gt_label, feature=feature) + else: + loss = self.compute_loss(cls_score, gt_label, avg_factor=num_samples) + if self.cal_acc: + # compute accuracy + acc = self.compute_accuracy(cls_score, gt_label) + assert len(acc) == len(self.topk) + losses["accuracy"] = {f"top-{k}": a for k, a in zip(self.topk, acc)} + losses["loss"] = loss + return losses + + def forward_train(self, x, gt_label, **kwargs): + """Forward_train fuction of CustomVisionTransformerClsHead class.""" + x = self.pre_logits(x) + cls_score = self.layers.head(x) + losses = self.loss(cls_score, gt_label, feature=x) + return losses diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/mixin.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/mixin.py new file mode 100644 index 00000000000..d547bc3f9ee --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/mixin.py @@ -0,0 +1,16 @@ +"""Module defining Mix-in class of heads.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +class OTXHeadMixin: + """Mix-in class for OTX custom heads.""" + + @staticmethod + def pre_logits(x): + """Preprocess logits before forward. Designed to support vision transformer output.""" + if isinstance(x, list): + x = x[-1] + return x + return x diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/mmov_cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/mmov_cls_head.py new file mode 100644 index 00000000000..3f18ded84ab --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/mmov_cls_head.py @@ -0,0 +1,90 @@ +"""Module for OpenVINO Classification Head adopted with mmclassification.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +import torch.nn.functional as F +from mmcls.models.builder import HEADS +from mmcls.models.heads import ClsHead + +from otx.core.ov.graph.parsers.cls import cls_base_parser +from otx.core.ov.models.mmov_model import MMOVModel + + +@HEADS.register_module() +class MMOVClsHead(ClsHead): + """Head module for MMClassification that uses MMOV for inference. + + Args: + model_path_or_model (Union[str, ov.Model]): Path to the ONNX model file or + the ONNX model object. + weight_path (Optional[str]): Path to the weight file. + inputs (Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]]): + Input shape(s) of the ONNX model. + outputs (Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]]): + Output name(s) of the ONNX model. + init_weight (bool): Whether to initialize the weight from a normal + distribution. + verify_shape (bool): Whether to verify the input shape of the ONNX model. + softmax_at_test (bool): Whether to apply softmax during testing. + """ + + def __init__( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + outputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + init_weight: bool = False, + verify_shape: bool = True, + softmax_at_test: bool = True, + **kwargs, + ): # pylint: disable=too-many-arguments + kwargs.pop("in_channels", None) + kwargs.pop("num_classes", None) + super().__init__(**kwargs) + + self._model_path_or_model = model_path_or_model + self._weight_path = weight_path + self._init_weight = init_weight + self._softmax_at_test = softmax_at_test + + self.model = MMOVModel( + model_path_or_model, + weight_path, + inputs=inputs, + outputs=outputs, + remove_normalize=False, + merge_bn=False, + paired_bn=False, + init_weight=init_weight, + verify_shape=verify_shape, + parser=cls_base_parser, + parser_kwargs=dict(component="head"), + ) + + def forward(self, x): + """Forward fuction of MMOVClsHead class.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label, **kwargs): + """Forward_train fuction of MMOVClsHead.""" + cls_score = self.model(cls_score) + while cls_score.dim() > 2: + cls_score = cls_score.squeeze(2) + losses = self.loss(cls_score, gt_label, **kwargs) + return losses + + def simple_test(self, cls_score): + """Test without augmentation.""" + cls_score = self.model(cls_score) + while cls_score.dim() > 2: + cls_score = cls_score.squeeze(2) + if self._softmax_at_test: + pred = F.softmax(cls_score, dim=1) if cls_score is not None else None + else: + pred = cls_score + return self.post_process(pred) diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/non_linear_cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/non_linear_cls_head.py new file mode 100644 index 00000000000..4dcb387023d --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/non_linear_cls_head.py @@ -0,0 +1,101 @@ +"""Module for defining non-linear classification head.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +import torch.nn.functional as F +from mmcls.models.builder import HEADS +from mmcls.models.heads.cls_head import ClsHead +from mmcv.cnn import build_activation_layer, constant_init, normal_init +from torch import nn + +from otx.algorithms.common.utils import cast_bf16_to_fp32 + + +@HEADS.register_module() +class NonLinearClsHead(ClsHead): + """Nonlinear classifier head. + + Args: + num_classes (int): Number of categories excluding the background + category. + in_channels (int): Number of channels in the input feature map. + hid_channels (int): Number of channels of hidden layer. + act_cfg (dict): Config of activation layer. + loss (dict): Config of classification loss. + topk (int | tuple): Top-k accuracy. + """ # noqa: W605 + + def __init__( + self, + num_classes, + in_channels, + hid_channels=1280, + act_cfg=None, + loss=None, + topk=(1,), + dropout=False, + **kwargs, + ): # pylint: disable=too-many-arguments + topk = (1,) if num_classes < 5 else (1, 5) + act_cfg = act_cfg if act_cfg else dict(type="ReLU") + loss = loss if loss else dict(type="CrossEntropyLoss", loss_weight=1.0) + super().__init__(loss=loss, topk=topk, **kwargs) + self.in_channels = in_channels + self.hid_channels = hid_channels + self.num_classes = num_classes + self.act = build_activation_layer(act_cfg) + self.dropout = dropout + + if self.num_classes <= 0: + raise ValueError(f"num_classes={num_classes} must be a positive integer") + + self._init_layers() + + def _init_layers(self): + if self.dropout: + self.classifier = nn.Sequential( + nn.Linear(self.in_channels, self.hid_channels), + nn.BatchNorm1d(self.hid_channels), + self.act, + nn.Dropout(p=0.2), + nn.Linear(self.hid_channels, self.num_classes), + ) + else: + self.classifier = nn.Sequential( + nn.Linear(self.in_channels, self.hid_channels), + nn.BatchNorm1d(self.hid_channels), + self.act, + nn.Linear(self.hid_channels, self.num_classes), + ) + + def init_weights(self): + """Initialize weights of head.""" + for module in self.classifier: + if isinstance(module, nn.Linear): + normal_init(module, mean=0, std=0.01, bias=0) + elif isinstance(module, nn.BatchNorm1d): + constant_init(module, 1) + + def simple_test(self, img): + """Test without augmentation.""" + cls_score = self.classifier(img) + if isinstance(cls_score, list): + cls_score = sum(cls_score) / float(len(cls_score)) + if torch.onnx.is_in_onnx_export(): + return cls_score + pred = F.softmax(cls_score, dim=1) if cls_score is not None else None + pred = cast_bf16_to_fp32(pred) + pred = list(pred.detach().cpu().numpy()) + return pred + + def forward(self, x): + """Forward fuction of NonLinearClsHead class.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label): + """Forward_train fuction of NonLinearClsHead class.""" + logit = self.classifier(cls_score) + losses = self.loss(logit, gt_label) + return losses diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/semisl_cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/semisl_cls_head.py new file mode 100644 index 00000000000..09efa02a3e1 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/semisl_cls_head.py @@ -0,0 +1,228 @@ +"""Module for defining semi-supervised learning for multi-class classification task.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcls.models.builder import HEADS +from mmcls.models.heads.linear_head import LinearClsHead + +from otx.algorithms.classification.adapters.mmcls.models.heads.non_linear_cls_head import ( + NonLinearClsHead, +) + +from .mixin import OTXHeadMixin + + +class SemiClsHead(OTXHeadMixin): + """Classification head for Semi-SL. + + Args: + unlabeled_coef (float): unlabeled loss coefficient, default is 1.0 + dynamic_threshold (boolean): whether to use dynamic threshold, default is True + min_threshold (float): Minimum value of threshold determining pseudo-label, default is 0.5 + """ + + def __init__(self, unlabeled_coef=1.0, use_dynamic_threshold=True, min_threshold=0.5): + self.unlabeled_coef = unlabeled_coef + self.use_dynamic_threshold = use_dynamic_threshold + self.min_threshold = ( + min_threshold if self.use_dynamic_threshold else 0.95 + ) # the range of threshold will be [min_thr, 1.0] + self.num_pseudo_label = 0 + self.classwise_acc = torch.ones((self.num_classes,)) * self.min_threshold + + def loss(self, logits, gt_label, pseudo_label=None, mask=None): + """Loss function in which unlabeled data is considered. + + Args: + logits (set): (labeled data logit, unlabeled data logit) + gt_label (Tensor): target features for labeled data + pseudo_label (Tensor): target feature for unlabeled data + mask (Tensor): Mask that shows pseudo-label that passes threshold + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + logits_x, logits_u_s = logits + num_samples = len(logits_x) + losses = dict() + + # compute supervised loss + labeled_loss = self.compute_loss(logits_x, gt_label, avg_factor=num_samples) + + unlabeled_loss = 0 + if len(logits_u_s) > 0: + # compute unsupervised loss + unlabeled_loss = self.compute_loss(logits_u_s, pseudo_label, avg_factor=len(logits_u_s)) * mask + losses["loss"] = labeled_loss + self.unlabeled_coef * unlabeled_loss + losses["unlabeled_loss"] = self.unlabeled_coef * unlabeled_loss + + # compute accuracy + acc = self.compute_accuracy(logits_x, gt_label) + losses["accuracy"] = {f"top-{k}": a for k, a in zip(self.topk, acc)} + return losses + + def forward_train(self, x, gt_label, final_layer=None): # pylint: disable=too-many-locals + """Forward_train head using pseudo-label selected through threshold. + + Args: + x (dict or Tensor): dict(labeled, unlabeled_weak, unlabeled_strong) or NxC input features. + gt_label (Tensor): NxC target features. + final_layer (nn.Linear or nn.Sequential): a final layer forwards feature from backbone. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + label_u, mask = None, None + if isinstance(x, dict): + for key in x.keys(): + x[key] = self.pre_logits(x[key]) + outputs = final_layer(x["labeled"]) # Logit of Labeled Img + batch_size = len(outputs) + + with torch.no_grad(): + logit_uw = final_layer(x["unlabeled_weak"]) + pseudo_label = torch.softmax(logit_uw.detach(), dim=-1) + max_probs, label_u = torch.max(pseudo_label, dim=-1) + + # select Pseudo-Label using flexible threhold + self.classwise_acc = self.classwise_acc.to(label_u.device) + mask = max_probs.ge(self.classwise_acc[label_u]).float() + self.num_pseudo_label = mask.sum() + + if self.use_dynamic_threshold: + # get Labeled Data True Positive Confidence + logit_x = torch.softmax(outputs.detach(), dim=-1) + x_probs, x_idx = torch.max(logit_x, dim=-1) + x_probs = x_probs[x_idx == gt_label] + x_idx = x_idx[x_idx == gt_label] + + # get Unlabeled Data Selected Confidence + uw_probs = max_probs[mask == 1] + uw_idx = label_u[mask == 1] + + # update class-wise accuracy + for i in set(x_idx.tolist() + uw_idx.tolist()): + current_conf = torch.tensor([x_probs[x_idx == i].mean(), uw_probs[uw_idx == i].mean()]) + current_conf = current_conf[~current_conf.isnan()].mean() + self.classwise_acc[i] = max(current_conf, self.min_threshold) + + outputs = torch.cat((outputs, final_layer(x["unlabeled_strong"]))) + else: + outputs = final_layer(x) + batch_size = len(outputs) + + logits_x = outputs[:batch_size] + logits_u = outputs[batch_size:] + del outputs + logits = (logits_x, logits_u) + losses = self.loss(logits, gt_label, label_u, mask) + return losses + + +@HEADS.register_module() +class SemiLinearClsHead(SemiClsHead, LinearClsHead): + """Linear classification head for Semi-SL. + + This head is designed to support FixMatch algorithm. (https://arxiv.org/abs/2001.07685) + - [OTX] supports dynamic threshold based on confidence for each class + + Args: + num_classes (int): The number of classes of dataset used for training + in_channels (int): The channels of input data from classifier + loss (dict): configuration of loss, default is CrossEntropyLoss + topk (set): evaluation topk score, default is (1, ) + unlabeled_coef (float): unlabeled loss coefficient, default is 1.0 + dynamic_threshold (boolean): whether to use dynamic threshold, default is True + min_threshold (float): Minimum value of threshold determining pseudo-label, default is 0.5 + """ + + def __init__( + self, + num_classes, + in_channels, + loss=None, + topk=None, + unlabeled_coef=1.0, + use_dynamic_threshold=True, + min_threshold=0.5, + ): # pylint: disable=too-many-arguments + if in_channels <= 0: + raise ValueError(f"in_channels={in_channels} must be a positive integer") + if num_classes <= 0: + raise ValueError("at least one class must be exist num_classes.") + + topk = (1,) if num_classes < 5 else (1, 5) + loss = loss if loss else dict(type="CrossEntropyLoss", loss_weight=1.0) + LinearClsHead.__init__(self, num_classes, in_channels, loss=loss, topk=topk) + SemiClsHead.__init__(self, unlabeled_coef, use_dynamic_threshold, min_threshold) + + def forward(self, x): + """Forward fuction of SemiLinearClsHead class.""" + return self.simple_test(x) + + def forward_train(self, x, gt_label): + """Forward_train fuction of SemiLinearClsHead class.""" + return SemiClsHead.forward_train(self, x, gt_label, final_layer=self.fc) + + +@HEADS.register_module() +class SemiNonLinearClsHead(SemiClsHead, NonLinearClsHead): + """Non-linear classification head for Semi-SL. + + This head is designed to support FixMatch algorithm. (https://arxiv.org/abs/2001.07685) + - [OTX] supports dynamic threshold based on confidence for each class + + Args: + num_classes (int): The number of classes of dataset used for training + in_channels (int): The channels of input data from classifier + hid_channels (int): Number of channels of hidden layer. + act_cfg (dict): Config of activation layer. + loss (dict): configuration of loss, default is CrossEntropyLoss + topk (set): evaluation topk score, default is (1, ) + unlabeled_coef (float): unlabeled loss coefficient, default is 1.0 + dynamic_threshold (boolean): whether to use dynamic threshold, default is True + min_threshold (float): Minimum value of threshold determining pseudo-label, default is 0.5 + """ + + def __init__( + self, + num_classes, + in_channels, + hid_channels=1280, + act_cfg=None, + loss=None, + topk=None, + dropout=False, + unlabeled_coef=1.0, + use_dynamic_threshold=True, + min_threshold=0.5, + ): # pylint: disable=too-many-arguments + if in_channels <= 0: + raise ValueError(f"in_channels={in_channels} must be a positive integer") + if num_classes <= 0: + raise ValueError("at least one class must be exist num_classes.") + + topk = (1,) if num_classes < 5 else (1, 5) + act_cfg = act_cfg if act_cfg else dict(type="ReLU") + loss = loss if loss else dict(type="CrossEntropyLoss", loss_weight=1.0) + NonLinearClsHead.__init__( + self, + num_classes, + in_channels, + hid_channels=hid_channels, + act_cfg=act_cfg, + loss=loss, + topk=topk, + dropout=dropout, + ) + SemiClsHead.__init__(self, unlabeled_coef, use_dynamic_threshold, min_threshold) + + def forward(self, x): + """Forward fuction of SemiNonLinearClsHead class.""" + return self.simple_test(x) + + def forward_train(self, x, gt_label): + """Forward_train fuction of SemiNonLinearClsHead class.""" + return SemiClsHead.forward_train(self, x, gt_label, final_layer=self.classifier) diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/semisl_multilabel_cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/semisl_multilabel_cls_head.py new file mode 100644 index 00000000000..95a0cd0fc7f --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/semisl_multilabel_cls_head.py @@ -0,0 +1,308 @@ +"""Module for defining semi-supervised classification head for multi-label classification task.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcls.models.builder import HEADS, build_loss +from torch import nn + +from otx.algorithms.classification.adapters.mmcls.models.heads.custom_multi_label_linear_cls_head import ( + CustomMultiLabelLinearClsHead, +) +from otx.algorithms.classification.adapters.mmcls.models.heads.custom_multi_label_non_linear_cls_head import ( + CustomMultiLabelNonLinearClsHead, +) + +from .mixin import OTXHeadMixin + + +def generate_aux_mlp(aux_mlp_cfg: dict, in_channels: int): + """Generate auxiliary MLP.""" + out_channels = aux_mlp_cfg["out_channels"] + if out_channels <= 0: + raise ValueError(f"out_channels={out_channels} must be a positive integer") + if "hid_channels" in aux_mlp_cfg and aux_mlp_cfg["hid_channels"] > 0: + hid_channels = aux_mlp_cfg["hid_channels"] + mlp = nn.Sequential( + nn.Linear(in_features=in_channels, out_features=hid_channels), + nn.ReLU(inplace=True), + nn.Linear(in_features=hid_channels, out_features=out_channels), + ) + else: + mlp = nn.Linear(in_features=in_channels, out_features=out_channels) + + return mlp + + +class EMAMeter: + """Exponential Moving Average Meter class.""" + + def __init__(self, alpha=0.9): + """Initialize the Exponential Moving Average Meter. + + Args: + alpha (float): Smoothing factor for the exponential moving average. Defaults to 0.9. + """ + self.alpha = alpha + self.val = 0 + + def reset(self): + """Reset the Exponential Moving Average Meter.""" + self.val = 0 + + def update(self, val): + """Update the Exponential Moving Average Meter with new value. + + Args: + val (float): New value to update the meter. + """ + self.val = self.alpha * self.val + (1 - self.alpha) * val + + +class LossBalancer: + """Loss Balancer class.""" + + def __init__(self, num_losses, weights=None, ema_weight=0.7) -> None: + """Initialize the Loss Balancer. + + Args: + num_losses (int): Number of losses to balance. + weights (list): List of weights to be applied to each loss. If None, equal weights are applied. + ema_weight (float): Smoothing factor for the exponential moving average meter. Defaults to 0.7. + """ + self.epsilon = 1e-9 + self.avg_estimators = [EMAMeter(ema_weight) for _ in range(num_losses)] + + if weights is not None: + assert len(weights) == num_losses + self.final_weights = weights + else: + self.final_weights = [1.0] * num_losses + + def balance_losses(self, losses): + """Balance the given losses using the weights and exponential moving average. + + Args: + losses (list): List of losses to be balanced. + + Returns: + total_loss (float): Balanced loss value. + """ + total_loss = 0.0 + for i, loss in enumerate(losses): + self.avg_estimators[i].update(float(loss)) + total_loss += ( + self.final_weights[i] * loss / (self.avg_estimators[i].val + self.epsilon) * self.avg_estimators[0].val + ) + return total_loss + + +class SemiMultilabelClsHead(OTXHeadMixin): + """Multilabel Classification head for Semi-SL. + + Args: + unlabeled_coef (float): unlabeled loss coefficient, default is 1.0. + use_dynamic_loss_weighting (boolean): whether to use dynamic unlabeled loss weighting, default is True. + aux_loss (dict, optional): auxiliary loss function, default is None. + """ + + def __init__( + self, + unlabeled_coef=0.1, + use_dynamic_loss_weighting=True, + aux_loss=None, + ): + aux_loss = ( + aux_loss if aux_loss else dict(type="BarlowTwinsLoss", off_diag_penality=1.0 / 128.0, loss_weight=1.0) + ) + self.unlabeled_coef = unlabeled_coef + self.use_dynamic_loss_weighting = use_dynamic_loss_weighting + self.aux_loss = build_loss(aux_loss) + if self.use_dynamic_loss_weighting: + self.loss_balancer = LossBalancer(2, [1.0, unlabeled_coef]) + else: + self.loss_balancer = None + self.num_pseudo_label = 0 + + def loss(self, logits, gt_label, features): + """Loss function in which unlabeled data is considered. + + Args: + logits (Tensor): Labeled data logits + gt_label (Tensor): target features for labeled data + features (set): (weak data features, strong data features) + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + num_samples = gt_label.shape[0] + # map difficult examples to positive ones + _gt_label = torch.abs(gt_label) + l_labeled = self.compute_loss( + logits, + _gt_label, + avg_factor=num_samples, + ) + + features_weak, features_strong = features + aux_loss = self.aux_loss(features_weak, features_strong) + + losses = dict(loss=0.0) + if self.use_dynamic_loss_weighting: + losses["loss"] = self.loss_balancer.balance_losses((l_labeled, aux_loss)) + else: + losses["loss"] = l_labeled + self.unlabeled_coef * aux_loss + losses["unlabeled_loss"] = self.unlabeled_coef * aux_loss + + return losses + + def forward_train_with_last_layers(self, x, gt_label, final_cls_layer, final_emb_layer): + """Forwards multilabel semi-sl head and losses. + + Args: + x (dict): dict(labeled_weak. labeled_strong, unlabeled_weak, unlabeled_strong) or NxC input features. + gt_label (Tensor): NxC target features. + final_cls_layer (nn.Linear or nn.Sequential): a final layer forwards feature from backbone. + final_emb_layer (nn.Linear or nn.Sequential): a final layer forwards embeddings from backbone. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + for key in x.keys(): + x[key] = self.pre_logits(x[key]) + logits = final_cls_layer(x["labeled_weak"]) + features_weak = torch.cat((final_emb_layer(x["labeled_weak"]), final_emb_layer(x["unlabeled_weak"]))) + features_strong = torch.cat((final_emb_layer(x["labeled_strong"]), final_emb_layer(x["unlabeled_strong"]))) + features = (features_weak, features_strong) + losses = self.loss(logits, gt_label, features) + return losses + + +@HEADS.register_module() +class SemiLinearMultilabelClsHead(SemiMultilabelClsHead, CustomMultiLabelLinearClsHead): + """Linear multilabel classification head for Semi-SL. + + Args: + num_classes (int): The number of classes of dataset used for training + in_channels (int): The channels of input data from classifier + scale (float): Scale for metric learning loss + normalized (boolean): flag that enables metric learining in loss, + aux_mlp (dict): Config for embeddings MLP + loss (dict): configuration of loss, default is CrossEntropyLoss + unlabeled_coef (float): unlabeled loss coefficient, default is 1.0 + use_dynamic_loss_weighting (boolean): whether to use dynamic unlabeled loss weighting, default is True + """ + + def __init__( + self, + num_classes, + in_channels, + scale=1.0, + normalized=False, + aux_mlp=None, + loss=None, + unlabeled_coef=0.1, + aux_loss=None, + use_dynamic_loss_weighting=True, + ): # pylint: disable=too-many-arguments + if in_channels <= 0: + raise ValueError(f"in_channels={in_channels} must be a positive integer") + if num_classes <= 0: + raise ValueError("at least one class must be exist num_classes.") + aux_mlp = aux_mlp if aux_mlp else dict(hid_channels=0, out_channels=1024) + loss = loss if loss else dict(type="CrossEntropyLoss", loss_weight=1.0) + aux_loss = ( + aux_loss if aux_loss else dict(type="BarlowTwinsLoss", off_diag_penality=1.0 / 128.0, loss_weight=1.0) + ) + CustomMultiLabelLinearClsHead.__init__(self, num_classes, in_channels, normalized, scale, loss) + SemiMultilabelClsHead.__init__(self, unlabeled_coef, use_dynamic_loss_weighting, aux_loss) + + self.aux_mlp = generate_aux_mlp(aux_mlp, in_channels) + + def loss(self, logits, gt_label, features): + """Calculate loss for given logits/gt_label.""" + return SemiMultilabelClsHead.loss(self, logits, gt_label, features) + + def forward(self, x): + """Forward fuction of SemiLinearMultilabelClsHead class.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label): + """Forward_train fuction of SemiLinearMultilabelClsHead class.""" + return self.forward_train_with_last_layers( + cls_score, gt_label, final_cls_layer=self.fc, final_emb_layer=self.aux_mlp + ) + + +@HEADS.register_module() +class SemiNonLinearMultilabelClsHead(SemiMultilabelClsHead, CustomMultiLabelNonLinearClsHead): + """Non-linear classification head for Semi-SL. + + Args: + num_classes (int): The number of classes of dataset used for training + in_channels (int): The channels of input data from classifier + hid_channels (int): Number of channels of hidden layer. + scale (float): Scale for metric learning loss + normalized (boolean): flag that enables metric learining in loss, + aux_mlp (dict): Config for embeddings MLP + act_cfg (dict): Config of activation layer + loss (dict): configuration of loss, default is CrossEntropyLoss + topk (set): evaluation topk score, default is (1, ) + unlabeled_coef (float): unlabeled loss coefficient, default is 0.1 + use_dynamic_loss_weighting (boolean): whether to use dynamic unlabeled loss weighting, default is True + """ + + def __init__( + self, + num_classes, + in_channels, + hid_channels=1280, + scale=1.0, + normalized=False, + aux_mlp=None, + act_cfg=None, + loss=None, + aux_loss=None, + dropout=False, + unlabeled_coef=0.1, + use_dynamic_loss_weighting=True, + ): # pylint: disable=too-many-arguments + if in_channels <= 0: + raise ValueError(f"in_channels={in_channels} must be a positive integer") + if num_classes <= 0: + raise ValueError("at least one class must be exist num_classes.") + aux_mlp = aux_mlp if aux_mlp else dict(hid_channels=0, out_channels=1024) + act_cfg = act_cfg if act_cfg else dict(type="ReLU") + loss = loss if loss else dict(type="CrossEntropyLoss", loss_weight=1.0) + aux_loss = ( + aux_loss if aux_loss else dict(type="BarlowTwinsLoss", off_diag_penality=1.0 / 128.0, loss_weight=1.0) + ) + CustomMultiLabelNonLinearClsHead.__init__( + self, + num_classes, + in_channels, + hid_channels=hid_channels, + act_cfg=act_cfg, + loss=loss, + dropout=dropout, + scale=scale, + normalized=normalized, + ) + SemiMultilabelClsHead.__init__(self, unlabeled_coef, use_dynamic_loss_weighting, aux_loss) + + self.aux_mlp = generate_aux_mlp(aux_mlp, in_channels) + + def loss(self, logits, gt_label, features): + """Calculate loss for given logits/gt_label.""" + return SemiMultilabelClsHead.loss(self, logits, gt_label, features) + + def forward(self, x): + """Forward fuction of SemiNonLinearMultilabelClsHead class.""" + return self.simple_test(x) + + def forward_train(self, cls_score, gt_label): + """Forward_train fuction of SemiNonLinearMultilabelClsHead class.""" + return self.forward_train_with_last_layers( + cls_score, gt_label, final_cls_layer=self.classifier, final_emb_layer=self.aux_mlp + ) diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/heads/supcon_cls_head.py b/src/otx/algorithms/classification/adapters/mmcls/models/heads/supcon_cls_head.py new file mode 100644 index 00000000000..a407fa39e97 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/heads/supcon_cls_head.py @@ -0,0 +1,100 @@ +"""Module for defining classification head for supcon.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import torch +import torch.nn.functional as F +from mmcls.models.builder import HEADS, build_loss +from mmcls.models.heads.base_head import BaseHead +from torch import nn + + +@HEADS.register_module() +class SupConClsHead(BaseHead): + """Supervised Contrastive Learning head for Classification using SelfSL. + + Args: + num_classes (int): The number of classes of dataset used for training + in_channels (int): The channels of input data from the backbone + aux_mlp (dict): A dictionary with the out_channels and optionally the + hid_channels of the auxiliary MLP head. + loss (dict): The SelfSL loss: BarlowTwinsLoss (default) + topk (set): evaluation topk score, default is (1, ) + """ + + def __init__( + self, num_classes: int, in_channels: int, aux_mlp, loss, aux_loss, topk=(1,), init_cfg=None + ): # pylint: disable=too-many-arguments + if in_channels <= 0: + raise ValueError(f"in_channels={in_channels} must be a positive integer") + if num_classes <= 0: + raise ValueError("at least one class must be exist num_classes.") + + if isinstance(topk, int): + topk = (topk,) + for _topk in topk: + assert _topk > 0, "Top-k should be larger than 0" + topk = (1,) if num_classes < 5 else (1, 5) + super().__init__(init_cfg=init_cfg) + + self.topk = topk + self.compute_loss = build_loss(loss) + self.aux_loss = build_loss(aux_loss) + + # Set up the standard classification head + self.num_classes = num_classes + self.fc = nn.Linear(in_features=in_channels, out_features=self.num_classes) + + # Set up the auxiliar head + out_channels = aux_mlp["out_channels"] + if out_channels <= 0: + raise ValueError(f"out_channels={out_channels} must be a positive integer") + if "hid_channels" in aux_mlp and aux_mlp["hid_channels"] > 0: + hid_channels = aux_mlp["hid_channels"] + self.aux_mlp = nn.Sequential( + nn.Linear(in_features=in_channels, out_features=hid_channels), + nn.ReLU(inplace=True), + nn.Linear(in_features=hid_channels, out_features=out_channels), + ) + else: + self.aux_mlp = nn.Linear(in_features=in_channels, out_features=out_channels) + + def forward(self, x): + """Forward fuction of SupConClsHead class.""" + return self.simple_test(x) + + def forward_train(self, x, gt_label): + """Forward train head using the Supervised Contrastive Loss. + + Args: + x (Tensor): features from the backbone. + gt_label (Tensor): ground truth. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + + losses = dict(loss=0.0) + cls_score = self.fc(x) + + bsz = gt_label.shape[0] + # make sure we have two views for each label and split them + assert x.shape[0] == 2 * bsz + feats1, feats2 = torch.split(self.aux_mlp(x), [bsz, bsz], dim=0) + gt_label = torch.cat([gt_label, gt_label], dim=0) + + loss = self.compute_loss(cls_score, gt_label) + aux_loss = self.aux_loss(feats1, feats2) + losses["loss"] = loss + aux_loss + return losses + + def simple_test(self, img): + """Test without data augmentation.""" + cls_score = self.fc(img) + if isinstance(cls_score, list): + cls_score = sum(cls_score) / float(len(cls_score)) + pred = F.softmax(cls_score, dim=1) if cls_score is not None else None + if torch.onnx.is_in_onnx_export(): + return pred + pred = list(pred.detach().cpu().numpy()) + return pred diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/losses/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/models/losses/__init__.py new file mode 100644 index 00000000000..f43e9e11bfc --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/losses/__init__.py @@ -0,0 +1,29 @@ +"""OTX Algorithms - Classification Losses.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .asymmetric_angular_loss_with_ignore import AsymmetricAngularLossWithIgnore +from .asymmetric_loss_with_ignore import AsymmetricLossWithIgnore +from .barlowtwins_loss import BarlowTwinsLoss +from .cross_entropy_loss import CrossEntropyLossWithIgnore +from .ib_loss import IBLoss + +__all__ = [ + "AsymmetricAngularLossWithIgnore", + "AsymmetricLossWithIgnore", + "BarlowTwinsLoss", + "CrossEntropyLossWithIgnore", + "IBLoss", +] diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/losses/asymmetric_angular_loss_with_ignore.py b/src/otx/algorithms/classification/adapters/mmcls/models/losses/asymmetric_angular_loss_with_ignore.py new file mode 100644 index 00000000000..7a703528d96 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/losses/asymmetric_angular_loss_with_ignore.py @@ -0,0 +1,129 @@ +"""Module for defining AsymmetricAngularLossWithIgnore.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcls.models.builder import LOSSES +from mmcls.models.losses.utils import weight_reduce_loss +from torch import nn + + +def asymmetric_angular_loss_with_ignore( + pred, + target, + valid_label_mask=None, + weight=None, + gamma_pos=0.0, + gamma_neg=1.0, + clip=0.05, + k=0.8, + reduction="mean", + avg_factor=None, +): # pylint: disable=too-many-arguments, too-many-locals + """Asymmetric angular loss. + + Args: + pred (torch.Tensor): The prediction with shape (N, *). + target (torch.Tensor): The ground truth label of the prediction with + shape (N, *). + valid_label_mask (torch.Tensor, optional): Label mask for consideration + of ignored label. + weight (torch.Tensor, optional): Sample-wise loss weight with shape + (N, ). Dafaults to None. + gamma_pos (float): positive focusing parameter. Defaults to 0.0. + gamma_neg (float): Negative focusing parameter. We usually set + gamma_neg > gamma_pos. Defaults to 1.0. + k (float): positive balance parameter. Defaults to 0.8. + clip (float, optional): Probability margin. Defaults to 0.05. + reduction (str): The method used to reduce the loss. + Options are "none", "mean" and "sum". If reduction is 'none' , loss + is same shape as pred and label. Defaults to 'mean'. + avg_factor (int, optional): Average factor that is used to average + the loss. Defaults to None. + + Returns: + torch.Tensor: Loss. + """ + assert pred.shape == target.shape, "pred and target should be in the same shape." + + eps = 1e-8 + target = target.type_as(pred) + anti_target = 1 - target + + xs_pos = torch.sigmoid(pred) + xs_neg = torch.sigmoid(-pred) + + if clip > 0: + xs_neg = (xs_neg + clip).clamp(max=1) + + asymmetric_focus = gamma_pos > 0 or gamma_neg > 0 + if asymmetric_focus: + pos_target0 = xs_neg * target + pos_target1 = xs_pos * anti_target + pos_target = pos_target0 + pos_target1 + one_sided_gamma = gamma_pos * target + gamma_neg * anti_target + one_sided_w = torch.pow(pos_target, one_sided_gamma) + + loss = -k * target * torch.log(xs_pos.clamp(min=eps)) - (1 - k) * anti_target * torch.log(xs_neg.clamp(min=eps)) + + if asymmetric_focus: + loss *= one_sided_w + + if valid_label_mask is not None: + loss = loss * valid_label_mask + + if weight is not None: + assert weight.dim() == 1 + weight = weight.float() + if pred.dim() > 1: + weight = weight.reshape(-1, 1) + if reduction != "mean": + avg_factor = None + loss = weight_reduce_loss(loss, weight, reduction, avg_factor) + return loss + + +@LOSSES.register_module() +class AsymmetricAngularLossWithIgnore(nn.Module): + """Asymmetric angular loss. + + Args: + gamma_pos (float): positive focusing parameter. + Defaults to 0.0. + gamma_neg (float): Negative focusing parameter. We + usually set gamma_neg > gamma_pos. Defaults to 1.0. + k (float): positive balance parameter. Defaults to 0.8. + clip (float): Probability margin. Defaults to 0.05. + reduction (str): The method used to reduce the loss into + a scalar. + loss_weight (float): Weight of loss. Defaults to 1.0. + """ + + def __init__(self, gamma_pos=0.0, gamma_neg=1.0, k=0.8, clip=0.05, reduction="mean", loss_weight=1.0): + """Init fuction of AsymmetricAngularLossWithIgnore class.""" + super().__init__() + self.gamma_pos = gamma_pos + self.gamma_neg = gamma_neg + self.k = k + self.clip = clip + self.reduction = reduction + self.loss_weight = loss_weight + + def forward(self, pred, target, valid_label_mask=None, weight=None, avg_factor=None, reduction_override=None): + """Asymmetric angular loss.""" + assert reduction_override in (None, "none", "mean", "sum") + reduction = reduction_override if reduction_override else self.reduction + loss_cls = self.loss_weight * asymmetric_angular_loss_with_ignore( + pred, + target, + valid_label_mask, + weight, + gamma_pos=self.gamma_pos, + gamma_neg=self.gamma_neg, + k=self.k, + clip=self.clip, + reduction=reduction, + avg_factor=avg_factor, + ) + return loss_cls diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/losses/asymmetric_loss_with_ignore.py b/src/otx/algorithms/classification/adapters/mmcls/models/losses/asymmetric_loss_with_ignore.py new file mode 100644 index 00000000000..d0bc5196cde --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/losses/asymmetric_loss_with_ignore.py @@ -0,0 +1,110 @@ +"""Module for defining AsymmetricLossWithIgnore.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcls.models.builder import LOSSES +from mmcls.models.losses.utils import weight_reduce_loss +from torch import nn + + +def asymmetric_loss_with_ignore( + pred, + target, + valid_label_mask=None, + weight=None, + gamma_pos=1.0, + gamma_neg=4.0, + clip=0.05, + reduction="none", + avg_factor=None, +): # pylint: disable=too-many-arguments + """Asymmetric loss, please refer to the `paper `_ for details. + + Args: + pred (torch.Tensor): The prediction with shape (N, *). + target (torch.Tensor): The ground truth label of the prediction with + shape (N, *). + valid_label_mask (torch.Tensor, optional): Label mask for consideration of ignored label. + weight (torch.Tensor, optional): Sample-wise loss weight with shape + (N, ). Dafaults to None. + gamma_pos (float): positive focusing parameter. Defaults to 0.0. + gamma_neg (float): Negative focusing parameter. We usually set + gamma_neg > gamma_pos. Defaults to 4.0. + clip (float, optional): Probability margin. Defaults to 0.05. + reduction (str): The method used to reduce the loss. + Options are "none", "mean" and "sum". If reduction is 'none' , loss + is same shape as pred and label. Defaults to 'mean'. + avg_factor (int, optional): Average factor that is used to average + the loss. Defaults to None. + + Returns: + torch.Tensor: Loss. + """ + assert pred.shape == target.shape, "pred and target should be in the same shape." + + eps = 1e-8 + pred_sigmoid = pred.sigmoid() + target = target.type_as(pred) + if reduction != "mean": # we don't use avg factor with other reductions + avg_factor = None # if we are not set this to None the exception will be throwed + + if clip and clip > 0: + pos_target = (1 - pred_sigmoid + clip).clamp(max=1) * (1 - target) + pred_sigmoid * target + else: + pos_target = (1 - pred_sigmoid) * (1 - target) + pred_sigmoid * target + asymmetric_weight = (1 - pos_target).pow(gamma_pos * target + gamma_neg * (1 - target)) + loss = -torch.log(pos_target.clamp(min=eps)) * asymmetric_weight + + if valid_label_mask is not None: + loss = loss * valid_label_mask + + if weight is not None: + assert weight.dim() == 1 + weight = weight.float() + if pred.dim() > 1: + weight = weight.reshape(-1, 1) + loss = weight_reduce_loss(loss, weight, reduction, avg_factor) + return loss + + +@LOSSES.register_module() +class AsymmetricLossWithIgnore(nn.Module): + """Asymmetric loss. + + Args: + gamma_pos (float): positive focusing parameter. + Defaults to 0.0. + gamma_neg (float): Negative focusing parameter. We + usually set gamma_neg > gamma_pos. Defaults to 4.0. + clip (float, optional): Probability margin. Defaults to 0.05. + reduction (str): The method used to reduce the loss into + a scalar. + loss_weight (float): Weight of loss. Defaults to 1.0. + """ + + def __init__(self, gamma_pos=0.0, gamma_neg=4.0, clip=0.05, reduction="none", loss_weight=1.0): + super().__init__() + self.gamma_pos = gamma_pos + self.gamma_neg = gamma_neg + self.clip = clip + self.reduction = reduction + self.loss_weight = loss_weight + + def forward(self, pred, target, valid_label_mask=None, weight=None, avg_factor=None, reduction_override=None): + """Forward fuction of asymmetric loss.""" + assert reduction_override in (None, "none", "mean", "sum") + reduction = reduction_override if reduction_override else self.reduction + loss_cls = self.loss_weight * asymmetric_loss_with_ignore( + pred, + target, + valid_label_mask, + weight, + gamma_pos=self.gamma_pos, + gamma_neg=self.gamma_neg, + clip=self.clip, + reduction=reduction, + avg_factor=avg_factor, + ) + return loss_cls diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/losses/barlowtwins_loss.py b/src/otx/algorithms/classification/adapters/mmcls/models/losses/barlowtwins_loss.py new file mode 100644 index 00000000000..dfbdeabd139 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/losses/barlowtwins_loss.py @@ -0,0 +1,55 @@ +"""Module for defining BarlowTwinsLoss for supcon in classification task.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import torch +from mmcls.models.builder import LOSSES +from torch import Tensor, nn + + +def off_diagonal(x: Tensor): + """Return a tensor containing all the elements outside the diagonal of x.""" + assert x.shape[0] == x.shape[1] + return x.flatten()[:-1].view(x.shape[0] - 1, x.shape[0] + 1)[:, 1:].flatten() + + +@LOSSES.register_module() +class BarlowTwinsLoss(nn.Module): + """Barlow Twins Loss: https://arxiv.org/abs/2103.03230. + + Self-Supervised Learning via Redundancy Reduction + Code adapted from https://github.com/facebookresearch/barlowtwins. + """ + + def __init__(self, off_diag_penality, loss_weight=1.0): + super().__init__() + self.penalty = off_diag_penality + self.loss_weight = loss_weight + + def forward(self, feats1: Tensor, feats2: Tensor): + """Compute Barlow Twins Loss and, if labels are not none, also the Cross-Entropy loss. + + Args: + feats1 (torch.Tensor): vectors of shape [bsz, ...]. Corresponding to one of two views of the same samples. + feats2 (torch.Tensor): vectors of shape [bsz, ...]. Corresponding to one of two views of the same samples. + + Returns: + A floating point number describing the Barlow Twins loss + """ + + batch_size = feats1.shape[0] + assert batch_size == feats2.shape[0] + dimensionality = feats1.shape[1] + assert dimensionality == feats2.shape[1] + + # Barlow Twins loss: redundancy reduction + batch_norm = nn.BatchNorm1d(dimensionality, affine=False, track_running_stats=False) + # empirical cross-correlation matrix + eccm = batch_norm(feats1).T @ batch_norm(feats2) + eccm.div_(batch_size) + + # Compute the invariance term (diagonal) and redundacy term (off-diagonal) + on_diag = torch.diagonal(eccm).add(-1).pow_(2).sum() + off_diag = off_diagonal(eccm).pow_(2).sum() + # Normalize the loss by the dimensionality of the projector + return self.loss_weight * (on_diag + self.penalty * off_diag) / dimensionality diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/losses/cross_entropy_loss.py b/src/otx/algorithms/classification/adapters/mmcls/models/losses/cross_entropy_loss.py new file mode 100644 index 00000000000..28d8d2a57fd --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/losses/cross_entropy_loss.py @@ -0,0 +1,53 @@ +"""Module for defining cross entropy loss for classification task.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch.nn.functional as F +from mmcls.models.builder import LOSSES +from mmcls.models.losses.utils import weight_reduce_loss +from torch import nn + + +def cross_entropy(pred, label, weight=None, reduction="mean", avg_factor=None, class_weight=None, ignore_index=None): + """Calculate cross entropy for given pred, label pairs.""" + # element-wise losses + if ignore_index is not None: + loss = F.cross_entropy(pred, label, reduction="none", weight=class_weight, ignore_index=ignore_index) + else: + loss = F.cross_entropy(pred, label, reduction="none", weight=class_weight) + + # apply weights and do the reduction + if weight is not None: + weight = weight.float() + loss = weight_reduce_loss(loss, weight=weight, reduction=reduction, avg_factor=avg_factor) + + return loss + + +@LOSSES.register_module() +class CrossEntropyLossWithIgnore(nn.Module): + """Defining CrossEntropyLossWothIgnore which supports ignored_label masking.""" + + def __init__(self, reduction="mean", loss_weight=1.0, ignore_index=None): + super().__init__() + self.reduction = reduction + self.loss_weight = loss_weight + self.ignore_index = ignore_index + + self.cls_criterion = cross_entropy + + def forward(self, cls_score, label, weight=None, avg_factor=None, reduction_override=None, **kwargs): + """Forward function of CrossEntropyLossWithIgnore class.""" + assert reduction_override in (None, "none", "mean", "sum") + reduction = reduction_override if reduction_override else self.reduction + loss_cls = self.loss_weight * self.cls_criterion( + cls_score, + label, + weight, + ignore_index=self.ignore_index, + reduction=reduction, + avg_factor=avg_factor, + **kwargs + ) + return loss_cls diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/losses/ib_loss.py b/src/otx/algorithms/classification/adapters/mmcls/models/losses/ib_loss.py new file mode 100644 index 00000000000..8d585d79b24 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/losses/ib_loss.py @@ -0,0 +1,66 @@ +"""Module for defining IB Loss which alleviate effect of imbalanced dataset.""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import torch +import torch.nn.functional as F +from mmcls.models.builder import LOSSES +from mmcls.models.losses import CrossEntropyLoss + + +@LOSSES.register_module() +class IBLoss(CrossEntropyLoss): + """IB Loss, Influence-Balanced Loss for Imbalanced Visual Classification, https://arxiv.org/abs/2110.02444.""" + + def __init__(self, num_classes, start=5, alpha=1000.0, reduction: str = "mean"): + """Init fuction of IBLoss. + + Args: + num_classes (int): Number of classes in dataset + start (int): Epoch to start finetuning with IB loss + alpha (float): Hyper-parameter for an adjustment for IB loss re-weighting + reduction (str): How to reduce the output. Available options are "none" or "mean". Defaults to 'mean'. + """ + super().__init__(loss_weight=1.0, reduction=reduction) + if alpha < 0: + raise ValueError("Alpha for IB loss should be bigger than 0") + self.alpha = alpha + self.epsilon = 0.001 + self.num_classes = num_classes + self.register_buffer("weight", torch.ones(size=(self.num_classes,))) + self._start_epoch = start + self._cur_epoch = 0 + if reduction not in {"mean", "none"}: + raise ValueError(f"reduction={reduction} is not allowed.") + + @property + def cur_epoch(self): + """Return current epoch.""" + return self._cur_epoch + + @cur_epoch.setter + def cur_epoch(self, epoch): + self._cur_epoch = epoch + + def update_weight(self, cls_num_list): + """Update loss weight per class.""" + if len(cls_num_list) == 0: + raise ValueError("Cannot compute the IB loss weight with empty cls_num_list.") + per_cls_weights = 1.0 / (np.array(cls_num_list) + self.epsilon) + per_cls_weights = per_cls_weights / np.sum(per_cls_weights) * len(cls_num_list) + per_cls_weights = torch.FloatTensor(per_cls_weights) + self.weight.data = per_cls_weights.to(device=self.weight.device) + + def forward(self, x, target, feature): + """Forward fuction of IBLoss.""" + if self._cur_epoch < self._start_epoch: + return super().forward(x, target) + grads = torch.sum(torch.abs(F.softmax(x, dim=1) - F.one_hot(target, self.num_classes)), 1) + feature = torch.sum(torch.abs(feature), 1).reshape(-1, 1) + scaler = grads * feature.reshape(-1) + scaler = self.alpha / (scaler + self.epsilon) + ce_loss = F.cross_entropy(x, target, weight=self.weight, reduction="none") + loss = ce_loss * scaler + return loss.mean() if self.reduction == "mean" else loss diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/necks/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/models/necks/__init__.py new file mode 100644 index 00000000000..d2cb363f53f --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/necks/__init__.py @@ -0,0 +1,20 @@ +"""OTX Algorithms - Classification Necks.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .mmov_neck import MMOVNeck +from .selfsl_mlp import SelfSLMLP + +__all__ = ["SelfSLMLP", "MMOVNeck"] diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/necks/mmov_neck.py b/src/otx/algorithms/classification/adapters/mmcls/models/necks/mmov_neck.py new file mode 100644 index 00000000000..a4e96798b6a --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/necks/mmov_neck.py @@ -0,0 +1,27 @@ +"""Module for defining MMOVNeck for inference.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict, List, Union + +from mmcls.models.builder import NECKS + +from otx.core.ov.graph.parsers.cls import cls_base_parser +from otx.core.ov.models.mmov_model import MMOVModel + + +@NECKS.register_module() +class MMOVNeck(MMOVModel): + """Neck class for MMOV inference.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @staticmethod + def parser(graph, **kwargs) -> Dict[str, Union[List[str], Dict[str, List[str]]]]: + """Parser function returns base_parser for given graph.""" + output = cls_base_parser(graph, "neck") + if output is None: + raise ValueError("Parser can not determine input and output of model. Please provide them explicitly") + return output diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/necks/selfsl_mlp.py b/src/otx/algorithms/classification/adapters/mmcls/models/necks/selfsl_mlp.py new file mode 100644 index 00000000000..b787a911851 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/models/necks/selfsl_mlp.py @@ -0,0 +1,106 @@ +"""Multi-layer Perceptron (MLP) for Self-supervised learning methods. + +This MLP consists of fc (conv) - norm - relu - fc (conv). +""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=missing-module-docstring +from typing import Any, Dict, List, Tuple, Union + +import torch +from mmcls.models.builder import NECKS +from mmcv.cnn import build_norm_layer, kaiming_init, normal_init +from torch import nn + + +@NECKS.register_module() +class SelfSLMLP(nn.Module): + """The SelfSLMLP neck: fc/conv-bn-relu-fc/conv. + + Args: + in_channels (int): The number of feature output channels from backbone. + hid_channels (int): The number of channels for a hidden layer. + out_channels (int): The number of output channels of SelfSLMLP. + norm_cfg (dict): Normalize configuration. Default: dict(type="BN1d"). + use_conv (bool): Whether using conv instead of fc. Default: False. + with_avg_pool (bool): Whether using average pooling before passing MLP. + Default: True. + """ + + def __init__( + self, + in_channels: int, + hid_channels: int, + out_channels: int, + norm_cfg: Dict[str, Any] = None, + use_conv: bool = False, + with_avg_pool: bool = True, + ): + norm_cfg = norm_cfg if norm_cfg else dict(type="BN1d") + super().__init__() + + self.with_avg_pool = with_avg_pool + if with_avg_pool: + self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) + + self.use_conv = use_conv + if use_conv: + self.mlp = nn.Sequential( + nn.Conv2d(in_channels, hid_channels, 1), + build_norm_layer(norm_cfg, hid_channels)[1], + nn.ReLU(inplace=True), + nn.Conv2d(hid_channels, out_channels, 1), + ) + else: + self.mlp = nn.Sequential( + nn.Linear(in_channels, hid_channels), + build_norm_layer(norm_cfg, hid_channels)[1], + nn.ReLU(inplace=True), + nn.Linear(hid_channels, out_channels), + ) + + def init_weights(self, init_linear: str = "normal", std: float = 0.01, bias: float = 0.0): + """Initialize SelfSLMLP weights. + + Args: + init_linear (str): Option to initialize weights. Default: "normal". + std (float): Standard deviation for normal initialization. Default: 0.01. + bias (float): Bias for normal initialization. Default: 0. + """ + if init_linear not in ["normal", "kaiming"]: + raise ValueError(f"Undefined init_linear: {init_linear}") + for m in self.modules(): # pylint: disable=invalid-name + if isinstance(m, nn.Linear): + if init_linear == "normal": + normal_init(m, std=std, bias=bias) + else: + kaiming_init(m, mode="fan_in", nonlinearity="relu") + elif isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.GroupNorm, nn.SyncBatchNorm)): + if m.weight is not None: + nn.init.constant_(m.weight, 1) + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, x: Union[torch.Tensor, Tuple, List]): + """Forward SelfSLMLP. + + Args: + x (Tensor, tuple, list): Inputs to pass MLP. + If a type of the inputs is tuple or list, just use the last index. + + Return: + Tensor: Features passed SelfSLMLP. + """ + if isinstance(x, (tuple, list)): + # using last output + x = x[-1] + if not isinstance(x, torch.Tensor): + raise TypeError("neck inputs should be tuple or torch.tensor") + if self.with_avg_pool: + x = self.avgpool(x) + if self.use_conv: # pylint: disable=no-else-return + return self.mlp(x) + else: + return self.mlp(x.view(x.size(0), -1)) diff --git a/src/otx/algorithms/classification/adapters/mmcls/nncf/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/nncf/__init__.py new file mode 100644 index 00000000000..956350467d3 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/nncf/__init__.py @@ -0,0 +1,14 @@ +"""NNCF utils for mmcls.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# flake8: noqa + +from .builder import build_nncf_classifier +from .task import ClassificationNNCFTask + +__all__ = [ + "build_nncf_classifier", + "ClassificationNNCFTask", +] diff --git a/src/otx/algorithms/classification/adapters/mmcls/nncf/builder.py b/src/otx/algorithms/classification/adapters/mmcls/nncf/builder.py new file mode 100644 index 00000000000..0b4f7b7fe28 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/nncf/builder.py @@ -0,0 +1,171 @@ +"""NNCF wrapped mmcls models builder.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from functools import partial +from typing import Optional, Union + +import torch +from mmcv.parallel import DataContainer +from mmcv.runner import CheckpointLoader +from mmcv.utils import Config, ConfigDict + +from otx.algorithms.classification.adapters.mmcls.utils import build_classifier +from otx.algorithms.common.adapters.mmcv.nncf.runners import NNCF_META_KEY +from otx.algorithms.common.adapters.mmcv.utils import ( + get_configs_by_pairs, + remove_from_configs_by_type, +) +from otx.algorithms.common.adapters.nncf import is_accuracy_aware_training_set +from otx.algorithms.common.adapters.nncf.compression import NNCFMetaState +from otx.utils.logger import get_logger + +logger = get_logger() + + +def build_nncf_classifier( # pylint: disable=too-many-locals,too-many-statements + config: Config, + checkpoint: Optional[str] = None, + device: Union[str, torch.device] = "cpu", + cfg_options: Optional[Union[Config, ConfigDict]] = None, + distributed: bool = False, +): + """A function to build NNCF wrapped mmcls model.""" + + from mmcls.apis import multi_gpu_test, single_gpu_test + from mmcls.datasets import build_dataloader as mmcls_build_dataloader + from mmcls.datasets import build_dataset as mmcls_build_dataset + from mmcls.datasets.pipelines import Compose + + from otx.algorithms.common.adapters.mmcv.nncf.utils import ( + get_fake_input, + model_eval, + wrap_nncf_model, + ) + from otx.algorithms.common.adapters.mmcv.utils.builder import ( + build_dataloader, + build_dataset, + ) + + if checkpoint is None: + # load model in this function not in runner + checkpoint = config.get("load_from") + assert checkpoint is not None, "checkpoint is not given. NNCF model must be initialized with pretrained model" + + model = build_classifier(config, cfg_options=cfg_options, from_scratch=True) + model = model.to(device) + + state_dict = CheckpointLoader.load_checkpoint(checkpoint, map_location=device) + + is_acc_aware = is_accuracy_aware_training_set(config.get("nncf_config")) + + init_dataloader = None + model_eval_fn = None + if "meta" in state_dict and NNCF_META_KEY in state_dict["meta"]: + # NNCF ckpt + nncf_meta_state = state_dict["meta"][NNCF_META_KEY] + data_to_build_nncf = nncf_meta_state.data_to_build + state_to_build_nncf = nncf_meta_state.state_to_build + else: + # pytorch ckpt + state_to_build_nncf = state_dict + if "state_dict" in state_dict: + state_to_build_nncf = state_dict["state_dict"] + + init_dataloader = build_dataloader( + build_dataset( + config, + subset="train", + dataset_builder=mmcls_build_dataset, + ), + config, + subset="val", + dataloader_builder=mmcls_build_dataloader, + distributed=distributed, + persistent_workers=False, + ) + + # This data and state dict will be used to build NNCF graph later + # when loading NNCF model + # because some models run their subcomponents based on intermediate outputs + # resulting differently and partially traced NNCF graph + data_to_build_nncf = next(iter(init_dataloader))["img"] + if isinstance(data_to_build_nncf, DataContainer): + data_to_build_nncf = data_to_build_nncf.data[0] + data_to_build_nncf = data_to_build_nncf.cpu().numpy() + if len(data_to_build_nncf.shape) == 4: + data_to_build_nncf = data_to_build_nncf[0] + if data_to_build_nncf.shape[0] == 3: + data_to_build_nncf = data_to_build_nncf.transpose(1, 2, 0) + + val_dataloader = None + if is_acc_aware: + val_dataloader = build_dataloader( + build_dataset( + config, + subset="val", + dataset_builder=mmcls_build_dataset, + ), + config, + subset="val", + dataloader_builder=mmcls_build_dataloader, + distributed=distributed, + persistent_workers=False, + ) + + model_eval_fn = partial( + model_eval, + config=config, + val_dataloader=val_dataloader, + evaluate_fn=multi_gpu_test if distributed else single_gpu_test, + distributed=distributed, + ) + state_dict = None + + if config.data.test.pipeline[0]["type"] == "LoadImageFromOTXDataset": + config.data.test.pipeline = config.data.test.pipeline[1:] + test_pipeline = Compose(config.data.test.pipeline) + get_fake_input_fn = partial( + get_fake_input, + preprocessor=test_pipeline, + data=data_to_build_nncf, + ) + + compression_ctrl, model = wrap_nncf_model( + config, + model, + model_eval_fn=model_eval_fn, + get_fake_input_fn=get_fake_input_fn, + dataloader_for_init=init_dataloader, + is_accuracy_aware=is_acc_aware, + ) + + # update runner to save metadata + config.runner.nncf_meta = NNCFMetaState( + state_to_build=state_to_build_nncf, + data_to_build=data_to_build_nncf, + ) + + # update custom hooks + custom_hooks = config.get("custom_hooks", []) + custom_hooks.append(ConfigDict({"type": "CancelTrainingHook"})) + custom_hooks.append( + ConfigDict( + type="CompressionHook", + compression_ctrl=compression_ctrl, + ) + ) + # TODO: move this to OTX task + remove_from_configs_by_type(custom_hooks, "CancelInterfaceHook") + remove_from_configs_by_type(custom_hooks, "TaskAdaptHook") + remove_from_configs_by_type(custom_hooks, "LazyEarlyStoppingHook") + remove_from_configs_by_type(custom_hooks, "EarlyStoppingHook") + config.custom_hooks = custom_hooks + + for hook in get_configs_by_pairs(custom_hooks, dict(type="OTXProgressHook")): + time_monitor = hook.get("time_monitor", None) + if time_monitor and getattr(time_monitor, "on_initialization_end", None) is not None: + time_monitor.on_initialization_end() + + return compression_ctrl, model diff --git a/src/otx/algorithms/classification/adapters/mmcls/nncf/patches.py b/src/otx/algorithms/classification/adapters/mmcls/nncf/patches.py new file mode 100644 index 00000000000..c5988049c89 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/nncf/patches.py @@ -0,0 +1,11 @@ +"""Patch mmcls.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcls.models.classifiers.base import BaseClassifier + +from otx.algorithms.common.adapters.nncf.patches import nncf_trace_context + +# add nncf context method that will be used when nncf tracing +BaseClassifier.nncf_trace_context = nncf_trace_context diff --git a/src/otx/algorithms/classification/adapters/mmcls/nncf/registers.py b/src/otx/algorithms/classification/adapters/mmcls/nncf/registers.py new file mode 100644 index 00000000000..c708a3a1267 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/nncf/registers.py @@ -0,0 +1,17 @@ +"""Register custom modules for mmcls models.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.nncf.utils import is_nncf_enabled + +if is_nncf_enabled(): + from nncf.torch import register_module + from timm.models.layers.conv2d_same import Conv2dSame + + # Register custom modules. + # Users of nncf should manually check every custom + # layer with weights which should be compressed and + # in case such layers are not wrapping by nncf, + # wrap such custom module by yourself. + register_module(ignored_algorithms=[])(Conv2dSame) diff --git a/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py b/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py new file mode 100644 index 00000000000..987a9438b43 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py @@ -0,0 +1,115 @@ +"""NNCF Task for OTX Classification.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from functools import partial +from typing import List, Optional + +import otx.algorithms.classification.adapters.mmcls.nncf.patches # noqa: F401 # pylint: disable=unused-import +import otx.algorithms.classification.adapters.mmcls.nncf.registers # noqa: F401 # pylint: disable=unused-import +from otx.algorithms.classification.adapters.mmcls.nncf.builder import ( + build_nncf_classifier, +) +from otx.algorithms.classification.adapters.mmcls.task import MMClassificationTask +from otx.algorithms.common.tasks.nncf_task import NNCFBaseTask +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.metrics import ( + CurveMetric, + LineChartInfo, + LineMetricsGroup, + MetricsGroup, + Performance, + ScoreMetric, +) +from otx.api.entities.model import ModelEntity # ModelStatus +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.task_environment import TaskEnvironment +from otx.utils.logger import get_logger + +logger = get_logger() + + +class ClassificationNNCFTask(NNCFBaseTask, MMClassificationTask): # pylint: disable=too-many-ancestors + """ClassificationNNCFTask.""" + + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__() + super(NNCFBaseTask, self).__init__(task_environment, output_path) + self._set_attributes_by_hyperparams() + + def configure( + self, + training=True, + ir_options=None, + export=False, + ): + """Configure configs for nncf task.""" + super(NNCFBaseTask, self).configure(training, ir_options, export) + self._prepare_optimize(export) + return self._config + + def _prepare_optimize(self, export=False): + super()._prepare_optimize() + + self.model_builder = partial( + self.model_builder, + nncf_model_builder=build_nncf_classifier, + return_compression_ctrl=False, + is_export=export, + ) + + def _optimize( + self, + dataset: DatasetEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + ): + results = self._train_model(dataset) + + return results + + def _optimize_post_hook( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + ): + # Get training metrics group from learning curves + training_metrics, final_acc = self._generate_training_metrics_group(self._learning_curves) + performance = Performance( + score=ScoreMetric(value=final_acc, name="accuracy"), + dashboard_metrics=training_metrics, + ) + + logger.info(f"Final model performance: {str(performance)}") + output_model.performance = performance + + def _generate_training_metrics_group(self, learning_curves): + """Parses the classification logs to get metrics from the latest training run. + + :return output List[MetricsGroup] + """ + output: List[MetricsGroup] = [] + + if self._multilabel: + metric_key = "val/accuracy-mlc" + elif self._hierarchical: + metric_key = "val/MHAcc" + else: + metric_key = "val/accuracy_top-1" + + # Learning curves + best_acc = -1 + if learning_curves is None: + return output + + for key, curve in learning_curves.items(): + metric_curve = CurveMetric(xs=curve.x, ys=curve.y, name=key) + if key == metric_key: + best_acc = max(curve.y) + visualization_info = LineChartInfo(name=key, x_axis_label="Timestamp", y_axis_label=key) + output.append(LineMetricsGroup(metrics=[metric_curve], visualization_info=visualization_info)) + + return output, best_acc + + def _save_model_post_hook(self, modelinfo): + modelinfo["input_size"] = self._input_size diff --git a/src/otx/algorithms/classification/adapters/mmcls/optimizer/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/optimizer/__init__.py new file mode 100644 index 00000000000..271368e0d6a --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/optimizer/__init__.py @@ -0,0 +1,19 @@ +"""OTX Algorithms - Classification Optimizers.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .lars import LARS + +__all__ = ["LARS"] diff --git a/src/otx/algorithms/classification/adapters/mmcls/optimizer/lars.py b/src/otx/algorithms/classification/adapters/mmcls/optimizer/lars.py new file mode 100644 index 00000000000..cc453e40a51 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/optimizer/lars.py @@ -0,0 +1,152 @@ +"""Module for defining LARS optimizer for classification task.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcv.runner import OPTIMIZERS +from torch.optim.optimizer import Optimizer, required + + +@OPTIMIZERS.register_module() +class LARS(Optimizer): + r"""Implements layer-wise adaptive rate scaling for SGD. + + Args: + params (iterable): iterable of parameters to optimize or dicts defining + parameter groups + lr (float): base learning rate (\gamma_0) + momentum (float, optional): momentum factor (default: 0) ("m") + weight_decay (float, optional): weight decay (L2 penalty) (default: 0) + ("\beta") + dampening (float, optional): dampening for momentum (default: 0) + eta (float, optional): LARS coefficient + nesterov (bool, optional): enables Nesterov momentum (default: False) + + Based on Algorithm 1 of the following paper by You, Gitman, and Ginsburg. + Large Batch Training of Convolutional Networks: + https://arxiv.org/abs/1708.03888 + + Example: + >>> optimizer = LARS(model.parameters(), lr=0.1, momentum=0.9, + >>> weight_decay=1e-4, eta=1e-3) + >>> optimizer.zero_grad() + >>> loss_fn(model(input), target).backward() + >>> optimizer.step() + """ + + def __init__( + self, + params, + lr=required, + momentum=0, + dampening=0, + weight_decay=0, + eta=0.001, + nesterov=False, + mode=None, + exclude_bn_from_weight_decay=False, + ): # pylint: disable=too-many-arguments, too-many-locals + if lr is not required and lr < 0.0: + raise ValueError(f"Invalid learning rate: {lr}") + if momentum < 0.0: + raise ValueError(f"Invalid momentum value: {momentum}") + if weight_decay < 0.0: + raise ValueError(f"Invalid weight_decay value: {weight_decay}") + if eta < 0.0: + raise ValueError(f"Invalid LARS coefficient value: {eta}") + + defaults = dict( + lr=lr, momentum=momentum, dampening=dampening, weight_decay=weight_decay, nesterov=nesterov, eta=eta + ) + if nesterov and (momentum <= 0 or dampening != 0): + raise ValueError("Nesterov momentum requires a momentum and zero dampening") + + # split param group into weight_decay/non-weight decay. + if exclude_bn_from_weight_decay: + param_groups = list(params) + if not isinstance(param_groups[0], dict): + param_groups = [{"params": param_groups}] + + new_param_groups = [] + for param_group in param_groups: + decay, no_decay = [], [] + for param in param_group.pop("params", []): + if not param.requires_grad: + continue + + if len(param.shape) == 1: + no_decay.append(param) + else: + decay.append(param) + + decay_param_group = param_group.copy() + decay_param_group["params"] = decay + + no_decay_param_group = param_group.copy() + no_decay_param_group["params"] = no_decay + no_decay_param_group["weight_decay"] = 0 + no_decay_param_group["lars_exclude"] = True + + new_param_groups.append(decay_param_group) + new_param_groups.append(no_decay_param_group) + + self.mode = mode + + super().__init__(new_param_groups, defaults) + + def __setstate__(self, state): + """Set state for parameter groups.""" + super().__setstate__(state) + for group in self.param_groups: + group.setdefault("nesterov", False) + + @torch.no_grad() + def step(self, closure=None): + """Performs a single optimization step. + + Args: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + """ + loss = None + if closure is not None: + with torch.enable_grad(): + loss = closure() + + for group in self.param_groups: + weight_decay = group["weight_decay"] + momentum = group["momentum"] + nesterov = group["nesterov"] + eta = group["eta"] + + for p in group["params"]: + if p.grad is None: + continue + d_p = p.grad + + # Add weight decay before computing adaptive LR. + # Seems to be pretty important in SIMclr style models. + local_lr = 1.0 + if not group.get("lars_exclude", False): + weight_norm = torch.norm(p).item() + grad_norm = torch.norm(d_p).item() + if self.mode == "selfsl" and weight_norm > 0 and grad_norm > 0: + local_lr = eta * weight_norm / grad_norm + else: + local_lr = eta * weight_norm / (grad_norm + weight_decay * weight_norm) + if weight_decay != 0: + d_p = d_p.add(p, alpha=weight_decay) + + d_p = d_p.mul(local_lr) + + if momentum != 0: + param_state = self.state[p] + if "momentum_buffer" not in param_state: + buf = param_state["momentum_buffer"] = torch.clone(d_p).detach() + else: + buf = param_state["momentum_buffer"] + buf.mul_(momentum).add_(d_p, alpha=1 - group["dampening"]) + d_p = d_p.add(buf, alpha=momentum) if nesterov else buf + p.add_(d_p, alpha=-group["lr"]) + return loss diff --git a/src/otx/algorithms/classification/adapters/mmcls/task.py b/src/otx/algorithms/classification/adapters/mmcls/task.py new file mode 100644 index 00000000000..9ae0b5721e8 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/task.py @@ -0,0 +1,652 @@ +"""Task of OTX Classification using mmclassification training backend.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import glob +import os +import time +from contextlib import nullcontext +from copy import deepcopy +from functools import partial +from typing import Any, Dict, Optional, Type, Union + +import torch +from mmcls.datasets import build_dataloader, build_dataset +from mmcls.models.backbones.vision_transformer import VisionTransformer +from mmcls.utils import collect_env +from mmcv.runner import wrap_fp16_model +from mmcv.utils import Config, ConfigDict + +from otx.algorithms.classification.adapters.mmcls.apis.train import train_model +from otx.algorithms.classification.adapters.mmcls.utils.exporter import ( + ClassificationExporter, +) +from otx.algorithms.classification.task import OTXClassificationTask +from otx.algorithms.common.adapters.mmcv.hooks import LossDynamicsTrackingHook +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + ActivationMapHook, + BaseRecordingForwardHook, + EigenCamHook, + FeatureVectorHook, + ReciproCAMHook, + ViTFeatureVectorHook, + ViTReciproCAMHook, +) +from otx.algorithms.common.adapters.mmcv.utils import ( + adapt_batch_size, + build_data_parallel, + get_configs_by_pairs, +) +from otx.algorithms.common.adapters.mmcv.utils import ( + build_dataloader as otx_build_dataloader, +) +from otx.algorithms.common.adapters.mmcv.utils import ( + build_dataset as otx_build_dataset, +) +from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + InputSizeManager, + OTXConfig, +) +from otx.algorithms.common.adapters.torch.utils import convert_sync_batchnorm +from otx.algorithms.common.configs.configuration_enums import BatchSizeAdaptType +from otx.algorithms.common.configs.training_base import TrainType +from otx.algorithms.common.tasks.nncf_task import NNCFBaseTask +from otx.algorithms.common.utils import is_hpu_available +from otx.algorithms.common.utils.data import get_dataset +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ModelPrecision +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.utils.logger import get_logger + +from .configurer import ( + ClassificationConfigurer, + IncrClassificationConfigurer, + SemiSLClassificationConfigurer, +) +from .utils import build_classifier + +if is_hpu_available(): + import habana_frameworks.torch.core as htcore + +logger = get_logger() + +# TODO Remove unnecessary pylint disable +# pylint: disable=too-many-lines + + +class MMClassificationTask(OTXClassificationTask): + """Task class for OTX classification using mmclassification training backend.""" + + # pylint: disable=too-many-instance-attributes + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__(task_environment, output_path) + self._data_cfg: Optional[Config] = None + self._recipe_cfg: Optional[Config] = None + + # pylint: disable=too-many-locals, too-many-branches, too-many-statements + def _init_task(self): # noqa + """Initialize task.""" + + if self._multilabel: + cfg_path = os.path.join(self._model_dir, "model_multilabel.py") + elif self._hierarchical: + cfg_path = os.path.join(self._model_dir, "model_hierarchical.py") + else: + cfg_path = os.path.join(self._model_dir, "model.py") + self._recipe_cfg = OTXConfig.fromfile(cfg_path) + self._recipe_cfg.domain = self._task_type.domain + self._recipe_cfg.model.multilabel = self._multilabel + self._recipe_cfg.model.hierarchical = self._hierarchical + if self._hierarchical: + self._recipe_cfg.model.head.hierarchical_info = self._hierarchical_info + self._config = self._recipe_cfg + + self.set_seed() + + # Loss dynamics tracking + if getattr(self._hyperparams.algo_backend, "enable_noisy_label_detection", False): + LossDynamicsTrackingHook.configure_recipe(self._recipe_cfg, self._output_path) + + # pylint: disable=too-many-arguments + def configure( + self, + training=True, + ir_options=None, + export=False, + ): + """Patch mmcv configs for OTX classification settings.""" + + # deepcopy all configs to make sure + # changes under Configurer and below does not take an effect to OTX for clear distinction + recipe_cfg = deepcopy(self._recipe_cfg) + assert recipe_cfg is not None, "'recipe_cfg' is not initialized." + + if self._data_cfg is not None: + data_classes = [label.name for label in self._labels] + else: + data_classes = None + model_classes = [label.name for label in self._model_label_schema] + + recipe_cfg.work_dir = self._output_path + recipe_cfg.resume = self._resume + + if self._train_type == TrainType.Incremental: + configurer = IncrClassificationConfigurer( + "classification", + training, + export, + self.override_configs, + self.on_hook_initialized, + self._time_monitor, + self._learning_curves, + ) + elif self._train_type == TrainType.Semisupervised: + configurer = SemiSLClassificationConfigurer( + "classification", + training, + export, + self.override_configs, + self.on_hook_initialized, + self._time_monitor, + self._learning_curves, + ) + else: + configurer = ClassificationConfigurer( + "classification", + training, + export, + self.override_configs, + self.on_hook_initialized, + self._time_monitor, + self._learning_curves, + ) + + options_for_patch_datasets = {"type": "OTXClsDataset", "empty_label": self._empty_label} + options_for_patch_evaluation = {"task": "normal"} + if self._multilabel: + options_for_patch_datasets["type"] = "OTXMultilabelClsDataset" + options_for_patch_evaluation["task"] = "multilabel" + elif self._hierarchical: + options_for_patch_datasets["type"] = "OTXHierarchicalClsDataset" + options_for_patch_datasets["hierarchical_info"] = self._hierarchical_info + options_for_patch_datasets["label_schema"] = self._task_environment.label_schema + options_for_patch_evaluation["task"] = "hierarchical" + elif self._selfsl: + options_for_patch_datasets["type"] = "SelfSLDataset" + + cfg = configurer.configure( + recipe_cfg, + self.data_pipeline_path, + self._hyperparams, + self._model_ckpt, + self._data_cfg, + ir_options, + data_classes, + model_classes, + self._input_size, + options_for_patch_datasets=options_for_patch_datasets, + options_for_patch_evaluation=options_for_patch_evaluation, + ) + self._config = cfg + self._input_size = cfg.model.pop("input_size", None) + return cfg + + def build_model( + self, + cfg: Config, + fp16: bool = False, + **kwargs, + ) -> torch.nn.Module: + """Build model from model_builder.""" + model_builder = getattr(self, "model_builder", build_classifier) + model = model_builder(cfg, **kwargs) + if bool(fp16): + wrap_fp16_model(model) + if bool(cfg.get("channel_last", False)): + model = model.to(memory_format=torch.channels_last) + return model + + def _infer_model( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ): + """Main infer function.""" + self._data_cfg = ConfigDict( + data=ConfigDict( + train=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + test=ConfigDict( + otx_dataset=dataset, + labels=self._labels, + ), + ) + ) + + dump_saliency_map = not inference_parameters.is_evaluation if inference_parameters else True + + self._init_task() + + cfg = self.configure(False, None) + logger.info("infer!") + + # Data loader + mm_dataset = build_dataset(cfg.data.test) + workers_per_gpu = cfg.data.test_dataloader.get("workers_per_gpu", 0) + dataloader = build_dataloader( + mm_dataset, + samples_per_gpu=cfg.data.test_dataloader.get("samples_per_gpu", 1), + workers_per_gpu=workers_per_gpu, + num_gpus=len(cfg.gpu_ids), + dist=cfg.distributed, + seed=cfg.get("seed", None), + shuffle=False, + persistent_workers=(workers_per_gpu > 0), + ) + + # Model + model = self.build_model(cfg, fp16=cfg.get("fp16", False)) + + model.eval() + feature_model = model + model = build_data_parallel(model, cfg, distributed=False) + + # InferenceProgressCallback (Time Monitor enable into Infer task) + time_monitor = None + if cfg.get("custom_hooks", None): + time_monitor = [hook.time_monitor for hook in cfg.custom_hooks if hook.type == "OTXProgressHook"] + time_monitor = time_monitor[0] if time_monitor else None + if time_monitor is not None: + # pylint: disable=unused-argument + def pre_hook(module, inp): + time_monitor.on_test_batch_begin(None, None) + + def hook(module, inp, outp): + time_monitor.on_test_batch_end(None, None) + + model.register_forward_pre_hook(pre_hook) + model.register_forward_hook(hook) + + model_type = cfg.model.backbone.type.split(".")[-1] # mmcls.VisionTransformer => VisionTransformer + forward_explainer_hook: Union[nullcontext, BaseRecordingForwardHook] + if model_type == "VisionTransformer": + forward_explainer_hook = ViTReciproCAMHook(feature_model) + elif not dump_saliency_map: + forward_explainer_hook = nullcontext() + else: + forward_explainer_hook = ReciproCAMHook(feature_model) + + feature_vector_hook: Union[nullcontext, BaseRecordingForwardHook] + if model_type == "VisionTransformer": + feature_vector_hook = ViTFeatureVectorHook(feature_model) + elif not dump_saliency_map: + feature_vector_hook = nullcontext() + else: + feature_vector_hook = FeatureVectorHook(feature_model) + + eval_predictions = [] + feature_vectors = [] + saliency_maps = [] + + with feature_vector_hook: + with forward_explainer_hook: + for data in dataloader: + with torch.no_grad(): + result = model(return_loss=False, **data) + eval_predictions.extend(result) + if isinstance(feature_vector_hook, nullcontext): + feature_vectors = [None] * len(mm_dataset) + else: + feature_vectors = feature_vector_hook.records # pylint: disable=no-member + if isinstance(forward_explainer_hook, nullcontext): + saliency_maps = [None] * len(mm_dataset) + else: + saliency_maps = forward_explainer_hook.records # pylint: disable=no-member + if len(eval_predictions) == 0: + eval_predictions = [None] * len(mm_dataset) + + assert len(eval_predictions) == len(feature_vectors) == len(saliency_maps), ( + "Number of elements should be the same, however, number of outputs are " + f"{len(eval_predictions)}, {len(feature_vectors)}, and {len(saliency_maps)}" + ) + + outputs = dict( + eval_predictions=eval_predictions, + feature_vectors=feature_vectors, + saliency_maps=saliency_maps, + ) + return outputs + + # pylint: disable=too-many-branches, too-many-statements + def _train_model( + self, + dataset: DatasetEntity, + ): + """Train function in MMClassificationTask.""" + self._data_cfg = ConfigDict(data=ConfigDict()) + + for cfg_key, subset in zip( + ["train", "val", "unlabeled"], + [Subset.TRAINING, Subset.VALIDATION, Subset.UNLABELED], + ): + subset = get_dataset(dataset, subset) + if subset and self._data_cfg is not None: + self._data_cfg.data[cfg_key] = ConfigDict( + otx_dataset=subset, + labels=self._labels, + ) + + self._is_training = True + + self._init_task() + + cfg = self.configure(True, None) + logger.info("train!") + + timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) + + # Environment + logger.info(f"cfg.gpu_ids = {cfg.gpu_ids}, distributed = {cfg.distributed}") + env_info_dict = collect_env() + env_info = "\n".join([(f"{k}: {v}") for k, v in env_info_dict.items()]) + dash_line = "-" * 60 + "\n" + logger.info(f"Environment info:\n{dash_line}{env_info}\n{dash_line}") + + # Data + datasets = [build_dataset(cfg.data.train)] + + # Metadata + meta = dict() + meta["env_info"] = env_info + meta["seed"] = cfg.get("seed", 5) + meta["exp_name"] = cfg.work_dir + + # Model + model = self.build_model(cfg, fp16=cfg.get("fp16", False)) + if cfg.device == "cpu": + # NOTE: mmcls does not wrap models w/ DP for CPU training not like mmdet + # Raw DataContainer "img_metas" is exposed, which results in errors + model = build_data_parallel(model, cfg, distributed=False) + model.train() + if is_hpu_available(): + # TODO (sungchul): move it to appropriate location if needed + htcore.hpu.ModuleCacher(max_graphs=10)(model=model.backbone, inplace=True) + htcore.hpu.ModuleCacher(max_graphs=10)(model=model.head, inplace=True) + + if cfg.distributed: + convert_sync_batchnorm(model) + + validate = bool(cfg.data.get("val", None)) + if validate: + val_dataset = build_dataset(cfg.data.val, dict(test_mode=True)) + val_loader_cfg = Config( + cfg_dict={ + "num_gpus": len(cfg.gpu_ids), + "dist": cfg.distributed, + "round_up": True, + "seed": cfg.seed, + "shuffle": False, # Not shuffle by default + "sampler_cfg": None, # Not use sampler by default + **cfg.data.get("val_dataloader", {}), + } + ) + val_dataloader = build_dataloader(val_dataset, **val_loader_cfg) + eval_cfg = cfg.get("evaluation", {}) + eval_cfg["by_epoch"] = cfg.runner["type"] != "IterBasedRunner" + cfg.custom_hooks.append( + dict( + type="DistCustomEvalHook" if cfg.distributed else "CustomEvalHook", + dataloader=val_dataloader, + priority="ABOVE_NORMAL", + **eval_cfg, + ) + ) + + if self._hyperparams.learning_parameters.auto_adapt_batch_size != BatchSizeAdaptType.NONE: + train_func = partial(train_model, meta=deepcopy(meta), model=deepcopy(model), distributed=False) + adapt_batch_size( + train_func, + cfg, + datasets, + isinstance(self, NNCFBaseTask), # nncf needs eval hooks + not_increase=(self._hyperparams.learning_parameters.auto_adapt_batch_size == BatchSizeAdaptType.SAFE), + ) + + train_model( + model, + datasets, + cfg, + distributed=cfg.distributed, + validate=False, # For using CustomEvalHook + timestamp=timestamp, + meta=meta, + ) + + # Save outputs + output_ckpt_path = os.path.join(cfg.work_dir, "latest.pth") + best_ckpt_path = glob.glob(os.path.join(cfg.work_dir, "best_*.pth")) + if len(best_ckpt_path) > 0: + output_ckpt_path = best_ckpt_path[0] + return dict( + final_ckpt=output_ckpt_path, + ) + + def _explain_model(self, dataset: DatasetEntity, explain_parameters: Optional[ExplainParameters]): + """Explain function in MMClassificationTask.""" + self._data_cfg = ConfigDict( + data=ConfigDict( + train=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + test=ConfigDict( + otx_dataset=dataset, + labels=self._labels, + ), + ) + ) + + self._init_task() + cfg = self.configure(False, None) + # Data loader + mm_dataset = otx_build_dataset(cfg, "test", build_dataset) + dataloader = otx_build_dataloader( + mm_dataset, + cfg, + "test", + build_dataloader, + distributed=False, + round_up=False, + ) + model = self.build_model(cfg, fp16=cfg.get("fp16", False)) + model.eval() + feature_model = model + model = build_data_parallel(model, cfg, distributed=False) + + # InferenceProgressCallback (Time Monitor enable into Infer task) + time_monitor = None + if cfg.get("custom_hooks", None): + time_monitor = [hook.time_monitor for hook in cfg.custom_hooks if hook.type == "OTXProgressHook"] + time_monitor = time_monitor[0] if time_monitor else None + if time_monitor is not None: + + def pre_hook(module, inp): # pylint: disable=unused-argument + time_monitor.on_test_batch_begin(None, None) + + def hook(module, inp, outp): # pylint: disable=unused-argument + time_monitor.on_test_batch_end(None, None) + + model.register_forward_pre_hook(pre_hook) + model.register_forward_hook(hook) + + per_class_xai_algorithm: Union[Type[ViTReciproCAMHook], Type[ReciproCAMHook]] + if isinstance(model.module.backbone, VisionTransformer): + per_class_xai_algorithm = ViTReciproCAMHook + else: + per_class_xai_algorithm = ReciproCAMHook + explainer_hook_selector = { + "eigencam": EigenCamHook, + "activationmap": ActivationMapHook, + "classwisesaliencymap": per_class_xai_algorithm, + } + explainer = explain_parameters.explainer if explain_parameters else None + if explainer is not None: + explainer_hook = explainer_hook_selector.get(explainer.lower()) + else: + explainer_hook = None + if explainer_hook is None: + raise NotImplementedError("Explainer algorithm not supported!") + + eval_predictions = [] + with explainer_hook(feature_model) as forward_explainer_hook: + # do inference and record intermediate fmap + for data in dataloader: + with torch.no_grad(): + result = model(return_loss=False, **data) + eval_predictions.extend(result) + saliency_maps = forward_explainer_hook.records + + assert len(eval_predictions) == len(saliency_maps), ( + "Number of elements should be the same, however, number of outputs are " + f"{len(eval_predictions)}, and {len(saliency_maps)}" + ) + + return eval_predictions, saliency_maps + + def _export_model(self, precision: ModelPrecision, export_format: ExportType, dump_features: bool): + self._data_cfg = ConfigDict( + data=ConfigDict( + train=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + test=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + ) + ) + self._init_task() + + cfg = self.configure(False, None, export=True) + + self._precision[0] = precision + assert len(self._precision) == 1 + export_options: Dict[str, Any] = {} + export_options["deploy_cfg"] = self._init_deploy_cfg(cfg) + + export_options["precision"] = str(precision) + export_options["type"] = str(export_format) + + export_options["deploy_cfg"]["dump_features"] = dump_features + if dump_features: + output_names = export_options["deploy_cfg"]["ir_config"]["output_names"] + if "feature_vector" not in output_names: + output_names.append("feature_vector") + if export_options["deploy_cfg"]["codebase_config"]["task"] != "Segmentation": + if "saliency_map" not in output_names: + output_names.append("saliency_map") + export_options["model_builder"] = getattr(self, "model_builder", build_classifier) + + if precision == ModelPrecision.FP16: + export_options["deploy_cfg"]["backend_config"]["mo_options"]["flags"].append("--compress_to_fp16") + + backend_cfg_backup = {} + if export_format == ExportType.ONNX: + backend_cfg_backup = export_options["deploy_cfg"]["backend_config"] + export_options["deploy_cfg"]["backend_config"] = {"type": "onnxruntime"} + export_options["deploy_cfg"]["ir_config"]["dynamic_axes"]["data"] = {0: "batch"} + + exporter = ClassificationExporter() + results = exporter.run( + cfg, + **export_options, + ) + + if export_format == ExportType.ONNX: + results["inference_parameters"] = {} + results["inference_parameters"]["mean_values"] = " ".join( + map(str, backend_cfg_backup["mo_options"]["args"]["--mean_values"]) + ) + results["inference_parameters"]["scale_values"] = " ".join( + map(str, backend_cfg_backup["mo_options"]["args"]["--scale_values"]) + ) + + return results + + def _init_deploy_cfg(self, cfg: Config) -> Union[Config, None]: + base_dir = os.path.abspath(os.path.dirname(self._task_environment.model_template.model_template_path)) + deploy_cfg_path = os.path.join(base_dir, "deployment.py") + deploy_cfg = None + if os.path.exists(deploy_cfg_path): + deploy_cfg = OTXConfig.fromfile(deploy_cfg_path) + + def patch_input_preprocessing(deploy_cfg): + normalize_cfg = get_configs_by_pairs( + cfg.data.test.pipeline, + dict(type="Normalize"), + ) + assert len(normalize_cfg) == 1 + normalize_cfg = normalize_cfg[0] + + options = dict(flags=[], args={}) + # NOTE: OTX loads image in RGB format + # so that `to_rgb=True` means a format change to BGR instead. + # Conventionally, OpenVINO IR expects a image in BGR format + # but OpenVINO IR under OTX assumes a image in RGB format. + # + # `to_rgb=True` -> a model was trained with images in BGR format + # and a OpenVINO IR needs to reverse input format from RGB to BGR + # `to_rgb=False` -> a model was trained with images in RGB format + # and a OpenVINO IR does not need to do a reverse + if normalize_cfg.get("to_rgb", False): + options["flags"] += ["--reverse_input_channels"] + # value must be a list not a tuple + if normalize_cfg.get("mean", None) is not None: + options["args"]["--mean_values"] = list(normalize_cfg.get("mean")) + if normalize_cfg.get("std", None) is not None: + options["args"]["--scale_values"] = list(normalize_cfg.get("std")) + + # fill default + backend_config = deploy_cfg.backend_config + if backend_config.get("mo_options") is None: + backend_config.mo_options = ConfigDict() + mo_options = backend_config.mo_options + if mo_options.get("args") is None: + mo_options.args = ConfigDict() + if mo_options.get("flags") is None: + mo_options.flags = [] + + # already defiend options have higher priority + options["args"].update(mo_options.args) + mo_options.args = ConfigDict(options["args"]) + # make sure no duplicates + mo_options.flags.extend(options["flags"]) + mo_options.flags = list(set(mo_options.flags)) + + def patch_input_shape(deploy_cfg): + input_size_manager = InputSizeManager(cfg) + size = input_size_manager.get_input_size_from_cfg("test") + assert all(isinstance(i, int) and i > 0 for i in size) + # default is static shape to prevent an unexpected error + # when converting to OpenVINO IR + deploy_cfg.backend_config.model_inputs = [ConfigDict(opt_shapes=ConfigDict(input=[1, 3, *size]))] + + patch_input_preprocessing(deploy_cfg) + patch_input_shape(deploy_cfg) + + return deploy_cfg + + # This should be removed + def update_override_configurations(self, config): + """Update override_configs.""" + logger.info(f"update override config with: {config}") + config = ConfigDict(**config) + self.override_configs.update(config) diff --git a/src/otx/algorithms/classification/adapters/mmcls/utils/__init__.py b/src/otx/algorithms/classification/adapters/mmcls/utils/__init__.py new file mode 100644 index 00000000000..f0ab064d07c --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/utils/__init__.py @@ -0,0 +1,12 @@ +"""OTX Adapters - mmdet.utils.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .builder import build_classifier +from .config_utils import patch_datasets, patch_evaluation + +__all__ = [ + "build_classifier", + "patch_datasets", + "patch_evaluation", +] diff --git a/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py b/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py new file mode 100644 index 00000000000..9496930cce5 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py @@ -0,0 +1,48 @@ +"""MMcls model builder.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Optional, Union + +import torch +from mmcv.runner import load_checkpoint +from mmcv.utils import Config, ConfigDict, get_logger + +from otx.utils.logger import LEVEL + +mmcls_logger = get_logger("mmcls") + + +def build_classifier( + config: Config, + checkpoint: Optional[str] = None, + device: Union[str, torch.device] = "cpu", + cfg_options: Optional[Union[Config, ConfigDict]] = None, + from_scratch: bool = False, +) -> torch.nn.Module: + """A builder function for mmcls model. + + Creates a model, based on the configuration in config. + Note that this function updates 'load_from' attribute of 'config'. + """ + + from mmcls.models import build_classifier as origin_build_classifier + + if cfg_options is not None: + config.merge_from_dict(cfg_options) + + model_cfg = deepcopy(config.model) + model = origin_build_classifier(model_cfg) + mmcls_logger.setLevel("WARNING") # make logger less verbose temporally + model.init_weights() + mmcls_logger.setLevel(LEVEL) + model = model.to(device) + + checkpoint = checkpoint if checkpoint else config.pop("load_from", None) + if checkpoint is not None and not from_scratch: + load_checkpoint(model, checkpoint, map_location=device) + config.load_from = checkpoint + + return model diff --git a/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py b/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py new file mode 100644 index 00000000000..710eb2e605f --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py @@ -0,0 +1,66 @@ +"""Collection of utils for task implementation in Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from typing import List + +from mmcv import Config, ConfigDict + +from otx.algorithms.common.adapters.mmcv.utils import ( + get_dataset_configs, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +def patch_datasets( + config: Config, + subsets: List[str] = ["train", "val", "test", "unlabeled"], + **kwargs, +): + """Update dataset configs.""" + assert "data" in config + assert "type" in kwargs + + for subset in subsets: + if subset not in config.data: + continue + config.data[f"{subset}_dataloader"] = config.data.get(f"{subset}_dataloader", ConfigDict()) + + # For stable hierarchical information indexing + if subset == "train" and kwargs["type"] == "OTXHierarchicalClsDataset": + config.data[f"{subset}_dataloader"]["drop_last"] = True + + cfgs = get_dataset_configs(config, subset) + for cfg in cfgs: + cfg.update(kwargs) + + +def patch_evaluation(config: Config, task: str): + """Patch evaluation.""" + cfg = config.get("evaluation", None) + if cfg: + if task == "multilabel": + cfg.metric = ["accuracy-mlc", "mAP", "CP", "OP", "CR", "OR", "CF1", "OF1"] + config.early_stop_metric = "mAP" + elif task == "hierarchical": + cfg.metric = ["MHAcc", "avgClsAcc", "mAP"] + config.early_stop_metric = "MHAcc" + elif task == "normal": + cfg.metric = ["accuracy", "class_accuracy"] + config.early_stop_metric = "accuracy" + else: + raise NotImplementedError diff --git a/src/otx/algorithms/classification/adapters/mmcls/utils/exporter.py b/src/otx/algorithms/classification/adapters/mmcls/utils/exporter.py new file mode 100644 index 00000000000..ecc4dbfe35e --- /dev/null +++ b/src/otx/algorithms/classification/adapters/mmcls/utils/exporter.py @@ -0,0 +1,78 @@ +"""Exporter for OTX Classification task with MMClassification training backend.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +from mmcv.runner import wrap_fp16_model + +from otx.algorithms.classification.adapters.mmcls.utils.builder import build_classifier +from otx.algorithms.common.adapters.mmcv.tasks.exporter import Exporter +from otx.algorithms.common.adapters.mmdeploy.utils.utils import ( + sync_batchnorm_2_batchnorm, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +class ClassificationExporter(Exporter): + """Exporter for OTX Classification using mmclassification training backend.""" + + def run(self, cfg, **kwargs): # noqa: C901 + """Run exporter stage.""" + + precision = kwargs.get("precision", "FP32") + model_builder = kwargs.get("model_builder", build_classifier) + if cfg.data.test.pipeline[0]["type"] == "LoadImageFromOTXDataset": + cfg.data.test.pipeline = cfg.data.test.pipeline[1:] + + def model_builder_helper(*args, **kwargs): + model = model_builder(*args, **kwargs) + # TODO: handle various input size + model = sync_batchnorm_2_batchnorm(model, 2) + + if hasattr(model, "is_export"): + model.is_export = True + + if precision == "FP16": + wrap_fp16_model(model) + elif precision == "INT8": + from nncf.torch.nncf_network import NNCFNetwork + + assert isinstance(model, NNCFNetwork) + + return model + + kwargs["model_builder"] = model_builder_helper + return super().run(cfg, **kwargs) + + @staticmethod + def naive_export(output_dir, model_builder, precision, export_type, cfg, model_name="model"): + """Export procedure with pytorch backend.""" + from mmcls.datasets.pipelines import Compose + + from otx.algorithms.common.adapters.mmdeploy.apis import NaiveExporter + + def get_fake_data(cfg, orig_img_shape=(128, 128, 3)): + pipeline = cfg.data.test.pipeline + pipeline = Compose(pipeline) + data = dict(img=np.zeros(orig_img_shape, dtype=np.uint8)) + data = pipeline(data) + return data + + fake_data = get_fake_data(cfg) + opset_version = 11 + + NaiveExporter.export2backend( + output_dir, + model_builder, + cfg, + fake_data, + precision=precision, + model_name=model_name, + input_names=["data"], + output_names=["logits"], + opset_version=opset_version, + export_type=export_type, + ) diff --git a/src/otx/algorithms/classification/adapters/openvino/__init__.py b/src/otx/algorithms/classification/adapters/openvino/__init__.py new file mode 100644 index 00000000000..e5d882a77b6 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/openvino/__init__.py @@ -0,0 +1,19 @@ +"""Adapters of classification - openvino.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .task import ClassificationOpenVINOTask + +__all__ = ["ClassificationOpenVINOTask"] diff --git a/src/otx/algorithms/classification/adapters/openvino/task.py b/src/otx/algorithms/classification/adapters/openvino/task.py new file mode 100644 index 00000000000..135f8b20d98 --- /dev/null +++ b/src/otx/algorithms/classification/adapters/openvino/task.py @@ -0,0 +1,447 @@ +"""Openvino Task of OTX Classification.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import io +import json +import os +import tempfile +import time +import warnings +from typing import Any, List, Optional, Tuple, Union +from zipfile import ZipFile + +import nncf +import numpy as np +import openvino.runtime as ov +from addict import Dict as ADDict +from nncf.common.quantization.structs import QuantizationPreset +from openvino.model_api.adapters import OpenvinoAdapter, create_core +from openvino.model_api.models import Model +from openvino.model_api.models.utils import ClassificationResult + +from otx.algorithms.classification.configs import ClassificationConfig +from otx.algorithms.classification.utils import ( + get_cls_deploy_config, + get_cls_inferencer_configuration, + get_hierarchical_label_list, +) +from otx.algorithms.common.utils import OTXOpenVinoDataLoader +from otx.algorithms.common.utils.ir import check_if_quantized +from otx.algorithms.common.utils.utils import get_default_async_reqs_num, read_py_config +from otx.api.entities.annotation import AnnotationSceneEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import ( + InferenceParameters, + default_progress_callback, +) +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.metadata import FloatMetadata, FloatType +from otx.api.entities.model import ( + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.tensor import TensorEntity +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.exportable_code import demo +from otx.api.usecases.exportable_code.inference.inference import IInferencer +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( + ClassificationToAnnotationConverter, +) +from otx.api.usecases.tasks.interfaces.deployment_interface import IDeploymentTask +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.explain_interface import IExplainTask +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.optimization_interface import ( + IOptimizationTask, + OptimizationType, +) +from otx.api.utils.dataset_utils import add_saliency_maps_to_dataset_item +from otx.utils.logger import get_logger + +logger = get_logger() + + +# TODO: refactoring to Sphinx style. +class ClassificationOpenVINOInferencer(IInferencer): + """ClassificationOpenVINOInferencer class in OpenVINO task.""" + + def __init__( + self, + hparams: ClassificationConfig, + label_schema: LabelSchemaEntity, + model_file: Union[str, bytes], + weight_file: Union[str, bytes, None] = None, + device: str = "CPU", + num_requests: int = 1, + ): # pylint: disable=unused-argument + """Inferencer implementation for OTXClassification using OpenVINO backend. + + :param model: Path to model to load, `.xml`, `.bin` or `.onnx` file. + :param hparams: Hyper parameters that the model should use. + :param num_requests: Maximum number of requests that the inferencer can make. + Good value is the number of available cores. Defaults to 1. + :param device: Device to run inference on, such as CPU, GPU or MYRIAD. Defaults to "CPU". + """ + + self.label_schema = label_schema + model_adapter = OpenvinoAdapter( + create_core(), + model_file, + weight_file, + device=device, + max_num_requests=num_requests, + plugin_config={"PERFORMANCE_HINT": "THROUGHPUT"}, + ) + self.configuration = get_cls_inferencer_configuration(self.label_schema) + + # create a dummy hierarchical config for backward compatibility, which is not actually used + if self.configuration["hierarchical"]: + try: + model_adapter.get_rt_info(["model_info", "hierarchical_config"]) + except RuntimeError: + self.configuration["hierarchical_config"] = json.dumps( + {"cls_heads_info": {"label_to_idx": [], "all_groups": []}, "label_tree_edges": []} + ) + + self.model = Model.create_model(model_adapter, "Classification", self.configuration, preload=True) + + self.converter = ClassificationToAnnotationConverter(self.label_schema) + self.callback_exceptions: List[Exception] = [] + self.model.inference_adapter.set_callback(self._async_callback) + + def _async_callback(self, request: Any, callback_args: tuple) -> None: + """Fetches the results of async inference.""" + try: + id, preprocessing_meta, result_handler = callback_args + raw_prediction = self.model.inference_adapter.copy_raw_result(request) + processed_prediciton = self.model.postprocess(raw_prediction, preprocessing_meta) + annotation = self.converter.convert_to_annotation(processed_prediciton, preprocessing_meta) + aux_data = ( + processed_prediciton.raw_scores, + processed_prediciton.saliency_map, + processed_prediciton.feature_vector, + ) + result_handler(id, annotation, aux_data) + + except Exception as e: + self.callback_exceptions.append(e) + + def predict(self, image: np.ndarray) -> Tuple[ClassificationResult, AnnotationSceneEntity]: + """Predict function of OpenVINO Classification Inferencer.""" + cls_result = self.model(image) + return cls_result, self.converter.convert_to_annotation(cls_result) + + def enqueue_prediction(self, image: np.ndarray, id: int, result_handler: Any) -> None: + """Runs async inference.""" + if not self.model.is_ready(): + self.model.await_any() + image, metadata = self.model.preprocess(image) + callback_data = id, metadata, result_handler + self.model.inference_adapter.infer_async(image, callback_data) + + def await_all(self) -> None: + """Await all running infer requests if any.""" + self.model.await_all() + + +class ClassificationOpenVINOTask(IDeploymentTask, IInferenceTask, IEvaluationTask, IExplainTask, IOptimizationTask): + """Task implementation for OTXClassification using OpenVINO backend.""" + + def __init__(self, task_environment: TaskEnvironment): + self.task_environment = task_environment + self.hparams = self.task_environment.get_hyper_parameters(ClassificationConfig) + self.model = self.task_environment.model + self.inferencer = self.load_inferencer() + template_file_path = self.task_environment.model_template.model_template_path + self._base_dir = os.path.abspath(os.path.dirname(template_file_path)) + self._avg_time_per_image: Optional[float] = None + + @property + def avg_time_per_image(self) -> Optional[float]: + """Average inference time per image.""" + return self._avg_time_per_image + + def load_inferencer(self) -> ClassificationOpenVINOInferencer: + """load_inferencer function of ClassificationOpenVINOTask.""" + + if self.model is None: + raise RuntimeError("load_inferencer failed, model is None") + + return ClassificationOpenVINOInferencer( + self.hparams, + self.task_environment.label_schema, + self.model.get_data("openvino.xml"), + self.model.get_data("openvino.bin"), + num_requests=get_default_async_reqs_num(), + ) + + # pylint: disable-msg=too-many-locals + def infer( + self, dataset: DatasetEntity, inference_parameters: Optional[InferenceParameters] = None + ) -> DatasetEntity: + """Infer function of ClassificationOpenVINOTask.""" + + update_progress_callback = default_progress_callback + dump_features = False + process_saliency_maps = False + explain_predicted_classes = True + enable_async_inference = True + + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress # type: ignore + dump_features = not inference_parameters.is_evaluation + process_saliency_maps = inference_parameters.process_saliency_maps + explain_predicted_classes = inference_parameters.explain_predicted_classes + enable_async_inference = inference_parameters.enable_async_inference + + def add_prediction(id: int, predicted_scene: AnnotationSceneEntity, aux_data: tuple): + dataset_item = dataset[id] + probs, saliency_map, repr_vector = aux_data + item_labels = predicted_scene.annotations[0].get_labels() + dataset_item.append_labels(item_labels) + + if probs is not None: + act_score = np.max(probs) - np.min(probs) + active_score_media = FloatMetadata( + name="active_score", value=act_score, float_type=FloatType.ACTIVE_SCORE + ) + dataset_item.append_metadata_item(active_score_media, model=self.model) + + probs_meta = TensorEntity(name="probabilities", numpy=probs.reshape(-1)) + dataset_item.append_metadata_item(probs_meta, model=self.model) + + if dump_features: + if saliency_map.ndim > 1 and repr_vector.ndim > 0: + feature_vec_media = TensorEntity(name="representation_vector", numpy=repr_vector.reshape(-1)) + dataset_item.append_metadata_item(feature_vec_media, model=self.model) + label_list = self.task_environment.get_labels() + # Fix the order for hierarchical labels to adjust classes with model outputs + if self.inferencer.model.hierarchical: + label_list = get_hierarchical_label_list( + self.inferencer.model.hierarchical_info["cls_heads_info"], label_list + ) + if saliency_map.ndim == 4 and saliency_map.shape[0] == 1: + saliency_map = saliency_map.squeeze() + + add_saliency_maps_to_dataset_item( + dataset_item=dataset_item, + saliency_map=saliency_map, + model=self.model, + labels=label_list, + predicted_scored_labels=item_labels, + explain_predicted_classes=explain_predicted_classes, + process_saliency_maps=process_saliency_maps, + ) + else: + warnings.warn( + "Could not find Feature Vector and Saliency Map in OpenVINO output. " + "Please rerun OpenVINO export or retrain the model." + ) + + dataset_size = len(dataset) + total_time = 0.0 + for i, dataset_item in enumerate(dataset, 1): + start_time = time.perf_counter() + if enable_async_inference: + self.inferencer.enqueue_prediction(dataset_item.numpy, i - 1, add_prediction) + else: + cls_result, predicted_scene = self.inferencer.predict(dataset_item.numpy) + add_prediction(i - 1, predicted_scene, (None, cls_result.saliency_map, cls_result.feature_vector)) + + end_time = time.perf_counter() - start_time + total_time += end_time + update_progress_callback(int(i / dataset_size * 100)) + + self.inferencer.await_all() + + self._avg_time_per_image = total_time / len(dataset) + logger.info(f"Avg time per image: {self._avg_time_per_image} secs") + logger.info(f"Total time: {total_time} secs") + logger.info("Classification OpenVINO inference completed") + + return dataset + + def explain( + self, + dataset: DatasetEntity, + explain_parameters: Optional[ExplainParameters] = None, + ) -> DatasetEntity: + """Explain function of ClassificationOpenVINOTask.""" + + update_progress_callback = default_progress_callback + process_saliency_maps = False + explain_predicted_classes = True + if explain_parameters is not None: + update_progress_callback = explain_parameters.update_progress # type: ignore + process_saliency_maps = explain_parameters.process_saliency_maps + explain_predicted_classes = explain_parameters.explain_predicted_classes + + dataset_size = len(dataset) + label_list = self.task_environment.get_labels() + # Fix the order for hierarchical labels to adjust classes with model outputs + if self.inferencer.model.hierarchical: + label_list = get_hierarchical_label_list( + self.inferencer.model.hierarchical_info["cls_heads_info"], label_list + ) + for i, dataset_item in enumerate(dataset, 1): + cls_result, predicted_scene = self.inferencer.predict(dataset_item.numpy) + + if cls_result.saliency_map.ndim < 2: + raise RuntimeError( + "There is no Saliency Map in OpenVINO IR model output. " + "Please export model to OpenVINO IR with dump_features" + ) + + if cls_result.saliency_map.ndim == 4 and cls_result.saliency_map.shape[0] == 1: + saliency_map = cls_result.saliency_map.squeeze() + else: + saliency_map = cls_result.saliency_map + + item_labels = predicted_scene.annotations[0].get_labels() + dataset_item.append_labels(item_labels) + add_saliency_maps_to_dataset_item( + dataset_item=dataset_item, + saliency_map=saliency_map, + model=self.model, + labels=label_list, + predicted_scored_labels=item_labels, + explain_predicted_classes=explain_predicted_classes, + process_saliency_maps=process_saliency_maps, + ) + update_progress_callback(int(i / dataset_size * 100)) + return dataset + + def evaluate(self, output_resultset: ResultSetEntity, evaluation_metric: Optional[str] = None): + """Evaluate function of ClassificationOpenVINOTask.""" + + if evaluation_metric is not None: + logger.warning( + f"Requested to use {evaluation_metric} metric," "but parameter is ignored. Use accuracy instead." + ) + output_resultset.performance = MetricsHelper.compute_accuracy(output_resultset).get_performance() + + def deploy(self, output_model: ModelEntity) -> None: + """Deploy function of ClassificationOpenVINOTask.""" + + logger.info("Deploying the model") + + work_dir = os.path.dirname(demo.__file__) + parameters = get_cls_deploy_config(self.task_environment.label_schema, {}) + + if self.model is None: + raise RuntimeError("deploy failed, model is None") + + zip_buffer = io.BytesIO() + with ZipFile(zip_buffer, "w") as arch: + # model files + arch.writestr(os.path.join("model", "model.xml"), self.model.get_data("openvino.xml")) + arch.writestr(os.path.join("model", "model.bin"), self.model.get_data("openvino.bin")) + arch.writestr(os.path.join("model", "config.json"), json.dumps(parameters, ensure_ascii=False, indent=4)) + # other python files + arch.write(os.path.join(work_dir, "requirements.txt"), os.path.join("python", "requirements.txt")) + arch.write(os.path.join(work_dir, "LICENSE"), os.path.join("python", "LICENSE")) + arch.write(os.path.join(work_dir, "demo.py"), os.path.join("python", "demo.py")) + arch.write(os.path.join(work_dir, "README.md"), os.path.join(".", "README.md")) + output_model.exportable_code = zip_buffer.getvalue() + logger.info("Deploying completed") + + def optimize( + self, + optimization_type: OptimizationType, + dataset: DatasetEntity, + output_model: ModelEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + ): # pylint: disable=too-many-locals + """Optimize function of ClassificationOpenVINOTask.""" + + if optimization_type is not OptimizationType.POT: + raise ValueError("PTQ is the only supported optimization type for OpenVino models") + + dataset = dataset.get_combined_subset([Subset.TRAINING, Subset.UNLABELED]) + data_loader = OTXOpenVinoDataLoader(dataset, self.inferencer) + + quantization_dataset = nncf.Dataset(data_loader, lambda data: data[0]) + + if self.model is None: + raise RuntimeError("optimize failed, model is None") + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, "model.xml") + bin_path = os.path.join(tempdir, "model.bin") + with open(xml_path, "wb") as f: + f.write(self.model.get_data("openvino.xml")) + with open(bin_path, "wb") as f: + f.write(self.model.get_data("openvino.bin")) + + ov_model = ov.Core().read_model(xml_path) + if check_if_quantized(ov_model): + raise RuntimeError("Model is already optimized by PTQ") + + if optimization_parameters is not None: + optimization_parameters.update_progress(10, None) + + optimization_config_path = os.path.join(self._base_dir, "ptq_optimization_config.py") + ptq_config = ADDict() + if os.path.exists(optimization_config_path): + ptq_config = read_py_config(optimization_config_path) + else: + logger.info("PTQ config is not loaded") + + ptq_config.update( + subset_size=min(self.hparams.pot_parameters.stat_subset_size, len(data_loader)), + preset=QuantizationPreset(self.hparams.pot_parameters.preset.name.lower()), + ) + + compressed_model = nncf.quantize( + ov_model, + quantization_dataset, + **ptq_config, + ) + + if optimization_parameters is not None: + optimization_parameters.update_progress(90, None) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, "model.xml") + ov.save_model(compressed_model, xml_path) + with open(xml_path, "rb") as f: + output_model.set_data("openvino.xml", f.read()) + with open(os.path.join(tempdir, "model.bin"), "rb") as f: + output_model.set_data("openvino.bin", f.read()) + + output_model.set_data("label_schema.json", label_schema_to_bytes(self.task_environment.label_schema)) + + # set model attributes for quantized model + output_model.model_format = ModelFormat.OPENVINO + output_model.optimization_type = ModelOptimizationType.POT + output_model.optimization_methods = [OptimizationMethod.QUANTIZATION] + output_model.precision = [ModelPrecision.INT8] + + self.model = output_model + self.inferencer = self.load_inferencer() + + if optimization_parameters is not None: + optimization_parameters.update_progress(100, None) + logger.info("PQT optimization completed") diff --git a/src/otx/algorithms/classification/configs/__init__.py b/src/otx/algorithms/classification/configs/__init__.py new file mode 100644 index 00000000000..c5267b859f0 --- /dev/null +++ b/src/otx/algorithms/classification/configs/__init__.py @@ -0,0 +1,19 @@ +"""Config Initialization for OTX Classification.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .base import ClassificationConfig + +__all__ = ["ClassificationConfig"] diff --git a/src/otx/algorithms/classification/configs/base/__init__.py b/src/otx/algorithms/classification/configs/base/__init__.py new file mode 100644 index 00000000000..6da1cba654d --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/__init__.py @@ -0,0 +1,21 @@ +"""Base configurations.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import ClassificationConfig + +__all__ = [ + "ClassificationConfig", +] diff --git a/src/otx/algorithms/classification/configs/base/configuration.py b/src/otx/algorithms/classification/configs/base/configuration.py new file mode 100644 index 00000000000..1bfab96697e --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/configuration.py @@ -0,0 +1,82 @@ +"""Configuration file of OTX Classification.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=invalid-name + +from attr import attrs + +from otx.algorithms.common.configs import BaseConfig +from otx.api.configuration.elements import ( + add_parameter_group, + boolean_attribute, + configurable_boolean, + configurable_integer, + string_attribute, +) +from otx.api.configuration.enums import ModelLifecycle + + +@attrs +class ClassificationConfig(BaseConfig): + """Configurations of classification task.""" + + @attrs + class __LearningParameters(BaseConfig.BaseLearningParameters): + """Learning parameter configurations.""" + + header = string_attribute("Learning Parameters") + description = header + + max_num_epochs = configurable_integer( + default_value=200, + min_value=1, + max_value=1000, + header="Maximum number of training epochs", + description="Increasing this value causes the results to be more robust but training time " + "will be longer.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + enable_lr_finder = configurable_boolean( + default_value=False, + header="Enable automatic learing rate estimation", + description="Learning rate parameter value will be ignored if enabled.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + @attrs + class __AlgoBackend(BaseConfig.BaseAlgoBackendParameters): + """Algorithm backend configurations.""" + + header = string_attribute("Parameters for the OTX algo-backend") + description = header + + enable_noisy_label_detection = configurable_boolean( + default_value=False, + header="Enable loss dynamics tracking for noisy label detection", + description="Set to True to enable loss dynamics tracking for each sample to detect noisy labeled samples.", + editable=False, + visible_in_ui=False, + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + @attrs + class __POTParameter(BaseConfig.BasePOTParameter): + """POT-related parameter configurations.""" + + header = string_attribute("POT Parameters") + description = header + visible_in_ui = boolean_attribute(False) + + @attrs + class __NNCFOptimization(BaseConfig.BaseNNCFOptimization): + header = string_attribute("Optimization by NNCF") + description = header + visible_in_ui = boolean_attribute(False) + + learning_parameters = add_parameter_group(__LearningParameters) + algo_backend = add_parameter_group(__AlgoBackend) + pot_parameters = add_parameter_group(__POTParameter) + nncf_optimization = add_parameter_group(__NNCFOptimization) diff --git a/src/otx/algorithms/classification/configs/base/data/__init__.py b/src/otx/algorithms/classification/configs/base/data/__init__.py new file mode 100644 index 00000000000..a56659ae452 --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/data/__init__.py @@ -0,0 +1,14 @@ +"""Base data configurations pipeline folder.""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/base/data/data_pipeline.py b/src/otx/algorithms/classification/configs/base/data/data_pipeline.py new file mode 100644 index 00000000000..ff58fc5c319 --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/data/data_pipeline.py @@ -0,0 +1,69 @@ +"""Data Pipeline of Class-Incr model for Classification Task.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) +__resize_target_size = 224 + +__train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", size=__resize_target_size, downscale_only=True), + # To be resized in this op only if input is larger than expected size + # for speed & cache memory efficiency. + enable_memcache=True, # Cache after resizing image + ), + dict(type="RandomResizedCrop", size=__resize_target_size, efficientnet_style=True), + dict(type="RandomFlip", flip_prob=0.5, direction="horizontal"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="ToTensor", keys=["gt_label"]), + dict( + type="Collect", + keys=["img", "gt_label"], + meta_keys=[ + "flip_direction", + "entity_id", + "ori_filename", + "filename", + "img_norm_cfg", + "img_shape", + "label_id", + "pad_shape", + "scale_factor", + "flip", + "ori_shape", + "ignored_labels", + ], + ), +] + +__val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", size=__resize_target_size), + enable_memcache=True, # Cache after resizing image + ), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] + +__test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict(type="ResizeTo", size=__resize_target_size), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] + +__dataset_type = "OTXClsDataset" + +data = dict( + train=dict(type=__dataset_type, pipeline=__train_pipeline), + val=dict(type=__dataset_type, test_mode=True, pipeline=__val_pipeline), + test=dict(type=__dataset_type, test_mode=True, pipeline=__test_pipeline), +) diff --git a/src/otx/algorithms/classification/configs/base/data/selfsl/__init__.py b/src/otx/algorithms/classification/configs/base/data/selfsl/__init__.py new file mode 100644 index 00000000000..adf59dbe3d8 --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/data/selfsl/__init__.py @@ -0,0 +1,14 @@ +"""Self-SL data configurations pipeline folder.""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/base/data/selfsl/data_pipeline.py b/src/otx/algorithms/classification/configs/base/data/selfsl/data_pipeline.py new file mode 100644 index 00000000000..c957e04e59d --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/data/selfsl/data_pipeline.py @@ -0,0 +1,61 @@ +"""Data Pipeline of Self-SL model for Classification Task.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) +__img_size = 224 + +__train_pipeline_v0 = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", size=__img_size, downscale_only=True), + # To be resized in this op only if input is larger than expected size + # for speed & cache memory efficiency. + enable_memcache=True, # Cache after resizing image + ), + dict(type="RandomResizedCrop", size=__img_size), + dict(type="RandomFlip"), + dict( + type="RandomAppliedTrans", + transforms=[ + dict(type="OTXColorJitter", brightness=0.4, contrast=0.4, saturation=0.2, hue=0.1), + ], + p=0.8, + ), + dict(type="RandomGrayscale", gray_prob=0.2), + dict(type="GaussianBlur", sigma_min=0.1, sigma_max=2.0), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] +__train_pipeline_v1 = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", size=__img_size, downscale_only=True), + # To be resized in this op only if input is larger than expected size + # for speed & cache memory efficiency. + enable_memcache=True, # Cache after resizing image + ), + dict(type="RandomResizedCrop", size=__img_size), + dict(type="RandomFlip"), + dict( + type="RandomAppliedTrans", + transforms=[ + dict(type="OTXColorJitter", brightness=0.4, contrast=0.4, saturation=0.2, hue=0.1), + ], + p=0.8, + ), + dict(type="RandomGrayscale", gray_prob=0.2), + dict(type="RandomAppliedTrans", transforms=[dict(type="GaussianBlur", sigma_min=0.1, sigma_max=2.0)], p=0.1), + dict(type="RandomAppliedTrans", transforms=[dict(type="Solarize", thr=128)], p=0.2), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] + +data = dict( + train=dict(pipeline=dict(view0=__train_pipeline_v0, view1=__train_pipeline_v1)), +) diff --git a/src/otx/algorithms/classification/configs/base/data/semisl/__init__.py b/src/otx/algorithms/classification/configs/base/data/semisl/__init__.py new file mode 100644 index 00000000000..a84e38d0dda --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/data/semisl/__init__.py @@ -0,0 +1,14 @@ +"""Semi-SL data configurations pipeline folder.""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/base/data/semisl/data_pipeline.py b/src/otx/algorithms/classification/configs/base/data/semisl/data_pipeline.py new file mode 100644 index 00000000000..ee0af082eba --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/data/semisl/data_pipeline.py @@ -0,0 +1,83 @@ +"""Data Pipeline of Semi-SL model for Classification Task.""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) +__resize_target_size = 224 + +__common_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", size=__resize_target_size, downscale_only=False), + enable_memcache=True, # Cache after resizing image + ), + dict(type="RandomFlip", flip_prob=0.5, direction="horizontal"), + dict(type="AugMixAugment", config_str="augmix-m5-w3"), + dict(type="RandomRotate", p=0.35, angle=(-10, 10)), +] + +__strong_pipeline = [ + dict(type="OTXRandAugment", num_aug=8, magnitude=10), +] + +__train_pipeline = [ + *__common_pipeline, + dict(type="PostAug", keys=dict(img_strong=__strong_pipeline)), + dict(type="PILImageToNDArray", keys=["img", "img_strong"]), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img", "img_strong"]), + dict(type="ToTensor", keys=["gt_label"]), + dict(type="Collect", keys=["img", "img_strong", "gt_label"]), +] + +__unlabeled_pipeline = [ + *__common_pipeline, + dict(type="PostAug", keys=dict(img_strong=__strong_pipeline)), + dict(type="PILImageToNDArray", keys=["img", "img_strong"]), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img", "img_strong"]), + dict(type="Collect", keys=["img", "img_strong"]), +] + +__val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", size=__resize_target_size, downscale_only=False), + enable_memcache=True, # Cache after resizing image + ), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] + +__test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict(type="Resize", size=__resize_target_size), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] + +__dataset_type = "OTXClsDataset" + +data = dict( + train=dict(type=__dataset_type, pipeline=__train_pipeline), + unlabeled=dict( + type=__dataset_type, + pipeline=__unlabeled_pipeline, + ), + val=dict(type=__dataset_type, test_mode=True, pipeline=__val_pipeline), + test=dict(type=__dataset_type, test_mode=True, pipeline=__test_pipeline), +) diff --git a/src/otx/algorithms/classification/configs/base/data/supcon/__init__.py b/src/otx/algorithms/classification/configs/base/data/supcon/__init__.py new file mode 100644 index 00000000000..210f962a790 --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/data/supcon/__init__.py @@ -0,0 +1,14 @@ +"""SupCon data configurations pipeline folder.""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/base/data/supcon/data_pipeline.py b/src/otx/algorithms/classification/configs/base/data/supcon/data_pipeline.py new file mode 100644 index 00000000000..344b2d5b147 --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/data/supcon/data_pipeline.py @@ -0,0 +1,58 @@ +"""Data Pipeline of SupCon model for Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) +__resize_target_size = 224 + + +__train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", size=__resize_target_size, downscale_only=False), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="TwoCropTransform", + pipeline=[ + dict(type="RandomFlip", flip_prob=0.5, direction="horizontal"), + dict(type="AugMixAugment", config_str="augmix-m5-w3"), + dict(type="RandomRotate", p=0.35, angle=(-10, 10)), + dict(type="ToNumpy"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="ToTensor", keys=["gt_label"]), + dict(type="Collect", keys=["img", "gt_label"]), + ], + ), +] + +__val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", size=__resize_target_size, downscale_only=False), + enable_memcache=True, # Cache after resizing image + ), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] + +__test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict(type="Resize", size=__resize_target_size), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] + +__dataset_type = "OTXClsDataset" + +data = dict( + train=dict(type=__dataset_type, pipeline=__train_pipeline), + val=dict(type=__dataset_type, test_mode=True, pipeline=__val_pipeline), + test=dict(type=__dataset_type, test_mode=True, pipeline=__test_pipeline), +) diff --git a/src/otx/algorithms/classification/configs/base/deployments/__init__.py b/src/otx/algorithms/classification/configs/base/deployments/__init__.py new file mode 100644 index 00000000000..40a29b3998a --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/deployments/__init__.py @@ -0,0 +1,5 @@ +"""Base deploy setting for classification.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/classification/configs/base/deployments/base_classification_dynamic.py b/src/otx/algorithms/classification/configs/base/deployments/base_classification_dynamic.py new file mode 100644 index 00000000000..ec2c7e80b4e --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/deployments/base_classification_dynamic.py @@ -0,0 +1,17 @@ +"""Classification models dynamic deploy config.""" + +_base_ = ["./base_classification_static.py"] + +ir_config = dict( + dynamic_axes={ + "data": { + 0: "batch", + 1: "channel", + 2: "height", + 3: "width", + }, + "logits": { + 0: "batch", + }, + } +) diff --git a/src/otx/algorithms/classification/configs/base/deployments/base_classification_static.py b/src/otx/algorithms/classification/configs/base/deployments/base_classification_static.py new file mode 100644 index 00000000000..fc4303f08ad --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/deployments/base_classification_static.py @@ -0,0 +1,28 @@ +"""Classification models static deploy config.""" + +ir_config = dict( + type="onnx", + export_params=True, + keep_initializers_as_inputs=False, + opset_version=11, + save_file="end2end.onnx", + input_names=["data"], + output_names=["logits"], + input_shape=None, + # TODO + # optimizing onnx graph mess up NNCF graph at some point + # where we need to look into + optimize=False, +) + +codebase_config = dict(type="mmcls", task="Classification") + +backend_config = dict( + type="openvino", + mo_options=None, +) + +input_data = dict( + shape=(128, 128, 3), + file_path=None, +) diff --git a/src/otx/algorithms/classification/configs/base/models/__init__.py b/src/otx/algorithms/classification/configs/base/models/__init__.py new file mode 100644 index 00000000000..8eee411fdfc --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/models/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2022 Intel Corporation +"""Base model configurations.""" + +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/base/models/deit.py b/src/otx/algorithms/classification/configs/base/models/deit.py new file mode 100644 index 00000000000..7310541f57f --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/models/deit.py @@ -0,0 +1,18 @@ +"""Base deit config.""" + +# model settings +model = dict( + type="ImageClassifier", + backbone=dict(type="mmcls.VisionTransformer", arch="deit-small", img_size=224, patch_size=16), + neck=None, + head=dict( + type="CustomVisionTransformerClsHead", + num_classes=1000, + in_channels=384, + loss=dict(type="CrossEntropyLoss", loss_weight=1.0), + ), + init_cfg=[ + dict(type="TruncNormal", layer="Linear", std=0.02), + dict(type="Constant", layer="LayerNorm", val=1.0, bias=0.0), + ], +) diff --git a/src/otx/algorithms/classification/configs/base/models/efficientnet.py b/src/otx/algorithms/classification/configs/base/models/efficientnet.py new file mode 100644 index 00000000000..f194b42d449 --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/models/efficientnet.py @@ -0,0 +1,14 @@ +"""Base EfficientNet.""" + +# model settings +model = dict( + type="ImageClassifier", + backbone=dict(type="otx.OTXEfficientNet", pretrained=True, version="b0"), + neck=dict(type="GlobalAveragePooling"), + head=dict( + type="LinearClsHead", + num_classes=1000, + in_channels=1280, + loss=dict(type="CrossEntropyLoss", loss_weight=1.0), + ), +) diff --git a/src/otx/algorithms/classification/configs/base/models/efficientnet_v2.py b/src/otx/algorithms/classification/configs/base/models/efficientnet_v2.py new file mode 100644 index 00000000000..89b46f82f14 --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/models/efficientnet_v2.py @@ -0,0 +1,14 @@ +"""Base EfficientNet-V2.""" + +# model settings +model = dict( + type="ImageClassifier", + backbone=dict(type="otx.OTXEfficientNetV2", pretrained=True, version="s_21k"), + neck=dict(type="GlobalAveragePooling"), + head=dict( + type="LinearClsHead", + num_classes=1000, + in_channels=1280, + loss=dict(type="CrossEntropyLoss", loss_weight=1.0), + ), +) diff --git a/src/otx/algorithms/classification/configs/base/models/mobilenet_v3.py b/src/otx/algorithms/classification/configs/base/models/mobilenet_v3.py new file mode 100644 index 00000000000..f74cb119803 --- /dev/null +++ b/src/otx/algorithms/classification/configs/base/models/mobilenet_v3.py @@ -0,0 +1,16 @@ +"""Base MobileNet-V3.""" + +# model settings +model = dict( + type="ImageClassifier", + backbone=dict(type="otx.OTXMobileNetV3", pretrained=True, mode="small", width_mult=1.0), + neck=dict(type="GlobalAveragePooling"), + head=dict( + type="NonLinearClsHead", + num_classes=1000, + in_channels=576, + hid_channels=1024, + act_cfg=dict(type="HSwish"), + loss=dict(type="CrossEntropyLoss", loss_weight=1.0), + ), +) diff --git a/src/otx/algorithms/classification/configs/configuration.yaml b/src/otx/algorithms/classification/configs/configuration.yaml new file mode 100644 index 00000000000..18de282bf68 --- /dev/null +++ b/src/otx/algorithms/classification/configs/configuration.yaml @@ -0,0 +1,496 @@ +description: Configuration for an image classification task +header: Configuration for an image classification task +learning_parameters: + batch_size: + affects_outcome_of: TRAINING + default_value: 32 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 2048 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + unlabeled_batch_size: + affects_outcome_of: TRAINING + default_value: 32 + description: + The number of unlabeled training samples seen in each iteration of semi-supervised learning. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Unlabeled batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + description: Learning Parameters + header: Learning Parameters + learning_rate: + affects_outcome_of: TRAINING + default_value: 0.01 + description: + Increasing this value will speed up training convergence but might + make it unstable. + editable: true + header: Learning rate + max_value: 1.0 + min_value: 1.0e-07 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + auto_hpo_state: NOT_POSSIBLE + max_num_epochs: + affects_outcome_of: TRAINING + default_value: 200 + description: + Increasing this value causes the results to be more robust but training + time will be longer. + editable: true + header: Maximum number of training epochs + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + num_iters: + affects_outcome_of: TRAINING + default_value: 1 + description: + Increasing this value causes the results to be more robust but training + time will be longer. + editable: true + header: Number of training iterations + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: null + num_workers: + affects_outcome_of: NONE + default_value: 2 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of cpu threads to use during batch generation + max_value: 8 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: null + learning_rate_warmup_iters: + affects_outcome_of: TRAINING + default_value: 100 + description: + In this periods of initial training iterations, the model will be trained in low learning rate, + which will be increased incrementally up to the expected learning rate setting. + This warm-up phase is known to be helpful to stabilize training, thus result in better performance. + editable: true + header: Number of iterations for learning rate warmup + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: null + enable_early_stopping: + affects_outcome_of: TRAINING + default_value: true + description: Early exit from training when validation accuracy is not changed or decreased for several epochs. + editable: true + header: Enable early stopping of the training + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + early_stop_start: + affects_outcome_of: TRAINING + default_value: 3 + editable: true + header: Start epoch for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 3 + visible_in_ui: false + early_stop_patience: + affects_outcome_of: TRAINING + default_value: 3 + description: Training will stop if the model does not improve within the number of epochs of patience. + editable: true + header: Patience for early stopping + max_value: 50 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 8 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + early_stop_iteration_patience: + affects_outcome_of: TRAINING + default_value: 0 + description: + Training will stop if the model does not improve within the number of iterations of patience. + This ensures the model is trained enough with the number of iterations of patience before early stopping. + editable: true + header: Iteration patience for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + use_adaptive_interval: + affects_outcome_of: TRAINING + default_value: true + description: Depending on the size of iteration per epoch, adaptively update the validation interval and related values. + editable: true + header: Use adaptive validation interval + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: This will automatically control the patience and interval when early stopping is enabled. + enable_supcon: + affects_outcome_of: TRAINING + default_value: false + description: + Enable an auxiliar supervised contrastive loss, which might increase robustness + and accuracy for small datasets. + editable: true + header: Enable Supervised Contrastive helper loss + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + auto_adapt_batch_size: + affects_outcome_of: TRAINING + default_value: None + description: Safe => Prevent GPU out of memory. Full => Find a batch size using most of GPU memory. + editable: true + enum_name: BatchSizeAdaptType + header: Decrease batch size if current batch size isn't fit to CUDA memory. + options: + NONE: "None" + SAFE: "Safe" + FULL: "Full" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: None + visible_in_ui: true + warning: + Enabling this could change the actual batch size depending on the current GPU status. + The learning rate also could be adjusted according to the adapted batch size. This process might change + a model performance and take some extra computation time to try a few batch size candidates. + auto_num_workers: + affects_outcome_of: TRAINING + default_value: false + description: Adapt num_workers according to current hardware status automatically. + editable: true + header: Enable auto adaptive num_workers + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + input_size: + affects_outcome_of: INFERENCE + default_value: Default + description: + The input size of the given model could be configured to one of the predefined resolutions. + Reduced training and inference time could be expected by using smaller input size. + In Auto mode, the input size is automatically determined based on dataset statistics. + Defaults to per-model default resolution. + editable: true + enum_name: InputSizePreset + header: Configure model input size. + options: + DEFAULT: "Default" + AUTO: "Auto" + _64x64: "64x64" + _128x128: "128x128" + _224x224: "224x224" + _256x256: "256x256" + _384x384: "384x384" + _512x512: "512x512" + _768x768: "768x768" + _1024x1024: "1024x1024" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Default + visible_in_ui: false + warning: Modifying input size may decrease model performance. + type: PARAMETER_GROUP + visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: false + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + stat_subset_size: + affects_outcome_of: NONE + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + header: Optimization by NNCF + enable_quantization: + affects_outcome_of: TRAINING + default_value: true + description: Enable quantization algorithm + editable: true + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + enable_pruning: + affects_outcome_of: TRAINING + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + pruning_supported: + affects_outcome_of: TRAINING + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + maximal_accuracy_degradation: + affects_outcome_of: TRAINING + default_value: 1.0 + description: The maximal allowed accuracy metric drop + editable: true + header: Maximum accuracy degradation + max_value: 100.0 + min_value: 0.0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +algo_backend: + description: parameters for algo backend + header: Algo backend parameters + train_type: + affects_outcome_of: TRAINING + default_value: Incremental + description: Training scheme option that determines how to train the model + editable: True + enum_name: TrainType + header: Train type + options: + Incremental: "Incremental" + Semisupervised: "Semisupervised" + Selfsupervised: "Selfsupervised" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Incremental + visible_in_ui: false + warning: null + mem_cache_size: + affects_outcome_of: TRAINING + default_value: 1000000000 + description: Size of memory pool for caching decoded data to load data faster (bytes). + editable: true + header: Size of memory pool + max_value: 9223372036854775807 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + storage_cache_scheme: + affects_outcome_of: TRAINING + default_value: NONE + description: Scheme for storage cache + editable: true + enum_name: StorageCacheScheme + header: Scheme for storage cache + options: + NONE: "NONE" + AS_IS: "AS-IS" + JPEG_75: "JPEG/75" + JPEG_95: "JPEG/95" + PNG: "PNG" + TIFF: "TIFF" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + enable_noisy_label_detection: + affects_outcome_of: TRAINING + default_value: false + description: Set to True to enable loss dynamics tracking for each sample to detect noisy labeled samples. + editable: true + header: Enable loss dynamics tracking for noisy label detection + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: false diff --git a/src/otx/algorithms/classification/configs/deit_tiny/__init__.py b/src/otx/algorithms/classification/configs/deit_tiny/__init__.py new file mode 100644 index 00000000000..c03c4b787c3 --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of deit-tiny model for Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/deit_tiny/data_pipeline.py b/src/otx/algorithms/classification/configs/deit_tiny/data_pipeline.py new file mode 100644 index 00000000000..f02b402e07b --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of deit-tiny model for Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/deit_tiny/deployment.py b/src/otx/algorithms/classification/configs/deit_tiny/deployment.py new file mode 100644 index 00000000000..2240f5170bb --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/deployment.py @@ -0,0 +1,11 @@ +"""deit-tiny for multi-class MMDeploy config.""" + +_base_ = ["../base/deployments/base_classification_dynamic.py"] + +ir_config = dict( + output_names=["logits"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 224, 224]))], +) diff --git a/src/otx/algorithms/classification/configs/deit_tiny/hpo_config.yaml b/src/otx/algorithms/classification/configs/deit_tiny/hpo_config.yaml new file mode 100644 index 00000000000..a2657f19e1a --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: accuracy +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.00001 + - 0.001 + - 0.00001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 42 + - 96 + - 2 diff --git a/src/otx/algorithms/classification/configs/deit_tiny/model.py b/src/otx/algorithms/classification/configs/deit_tiny/model.py new file mode 100644 index 00000000000..083166b9e49 --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/model.py @@ -0,0 +1,20 @@ +"""deit-tiny for multi-class config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/deit.py"] +ckpt_url = "https://download.openmmlab.com/mmclassification/v0/deit/deit-tiny_pt-4xb256_in1k_20220218-13b382a0.pth" + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict(arch="deit-tiny", init_cfg=dict(type="Pretrained", checkpoint=ckpt_url, prefix="backbone")), +) + +fp16 = dict(loss_scale=512.0, bf16_training=False) + +optimizer = dict(_delete_=True, type="AdamW", lr=0.01, weight_decay=0.05) +optimizer_config = dict(_delete_=True) diff --git a/src/otx/algorithms/classification/configs/deit_tiny/model_hierarchical.py b/src/otx/algorithms/classification/configs/deit_tiny/model_hierarchical.py new file mode 100644 index 00000000000..cfe90a9036e --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/model_hierarchical.py @@ -0,0 +1,26 @@ +"""deit-tiny for hierarchical config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/deit.py"] +ckpt_url = "https://download.openmmlab.com/mmclassification/v0/deit/deit-tiny_pt-4xb256_in1k_20220218-13b382a0.pth" + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict(arch="deit-tiny", init_cfg=dict(type="Pretrained", checkpoint=ckpt_url, prefix="backbone")), + head=dict( + type="CustomHierarchicalLinearClsHead", + multilabel_loss=dict( + type="AsymmetricLossWithIgnore", + gamma_pos=0.0, + gamma_neg=4.0, + ), + ), +) + +optimizer = dict(_delete_=True, type="AdamW", lr=0.01, weight_decay=0.05) +optimizer_config = dict(_delete_=True) diff --git a/src/otx/algorithms/classification/configs/deit_tiny/model_multilabel.py b/src/otx/algorithms/classification/configs/deit_tiny/model_multilabel.py new file mode 100644 index 00000000000..3238948cace --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/model_multilabel.py @@ -0,0 +1,24 @@ +"""deit-tiny for multi-label config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/multilabel/incremental.yaml", "../base/models/deit.py"] +ckpt_url = "https://download.openmmlab.com/mmclassification/v0/deit/deit-tiny_pt-4xb256_in1k_20220218-13b382a0.pth" + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict(arch="deit-tiny", init_cfg=dict(type="Pretrained", checkpoint=ckpt_url, prefix="backbone")), + head=dict( + type="CustomMultiLabelLinearClsHead", + loss=dict(type="AsymmetricLossWithIgnore"), + ), +) + +fp16 = dict(loss_scale=512.0) + +optimizer = dict(_delete_=True, type="AdamW", lr=0.01, weight_decay=0.05) +optimizer_config = dict(_delete_=True) diff --git a/src/otx/algorithms/classification/configs/deit_tiny/ptq_optimization_config.py b/src/otx/algorithms/classification/configs/deit_tiny/ptq_optimization_config.py new file mode 100644 index 00000000000..440243f5c67 --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/ptq_optimization_config.py @@ -0,0 +1,4 @@ +"""PTQ config file.""" +from nncf.parameters import ModelType + +model_type = ModelType.TRANSFORMER diff --git a/src/otx/algorithms/classification/configs/deit_tiny/semisl/__init__.py b/src/otx/algorithms/classification/configs/deit_tiny/semisl/__init__.py new file mode 100644 index 00000000000..eb6e8a81c97 --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of deit-tiny model for Semi-SL Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/deit_tiny/semisl/data_pipeline.py b/src/otx/algorithms/classification/configs/deit_tiny/semisl/data_pipeline.py new file mode 100644 index 00000000000..fc51ecee36b --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/semisl/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of deit-tiny model for Classification Semi-SL Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/deit_tiny/semisl/hparam.yaml b/src/otx/algorithms/classification/configs/deit_tiny/semisl/hparam.yaml new file mode 100644 index 00000000000..d282469bb8e --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/semisl/hparam.yaml @@ -0,0 +1,23 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 16 + auto_hpo_state: POSSIBLE + unlabeled_batch_size: + default_value: 48 + learning_rate: + default_value: 0.00245 + auto_hpo_state: POSSIBLE + early_stop_start: + default_value: 50 + early_stop_patience: + default_value: 10 + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 90 + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/classification/configs/deit_tiny/semisl/model.py b/src/otx/algorithms/classification/configs/deit_tiny/semisl/model.py new file mode 100644 index 00000000000..c58063c247d --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/semisl/model.py @@ -0,0 +1,21 @@ +"""deit-tiny config for semi-supervised multi-class classification.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/semisl.yaml", "../../base/models/deit.py"] +ckpt_url = "https://download.openmmlab.com/mmclassification/v0/deit/deit-tiny_pt-4xb256_in1k_20220218-13b382a0.pth" + +model = dict( + type="SemiSLClassifier", + task="classification", + backbone=dict(arch="deit-tiny", init_cfg=dict(type="Pretrained", checkpoint=ckpt_url, prefix="backbone")), + head=dict( + type="SemiLinearClsHead", + loss=dict( + type="CrossEntropyLoss", + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/deit_tiny/semisl/model_multilabel.py b/src/otx/algorithms/classification/configs/deit_tiny/semisl/model_multilabel.py new file mode 100644 index 00000000000..7c0939580db --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/semisl/model_multilabel.py @@ -0,0 +1,29 @@ +"""deit-tiny config for semi-supervised multi-label classification.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/multilabel/semisl.yaml", "../../base/models/efficientnet.py"] +ckpt_url = "https://download.openmmlab.com/mmclassification/v0/deit/deit-tiny_pt-4xb256_in1k_20220218-13b382a0.pth" + +model = dict( + type="SemiSLMultilabelClassifier", + task="classification", + backbone=dict(arch="deit-tiny", init_cfg=dict(type="Pretrained", checkpoint=ckpt_url, prefix="backbone")), + head=dict( + type="SemiLinearMultilabelClsHead", + use_dynamic_loss_weighting=True, + unlabeled_coef=0.1, + in_channels=-1, + aux_mlp=dict(hid_channels=0, out_channels=1024), + normalized=True, + scale=7.0, + loss=dict(type="AsymmetricAngularLossWithIgnore", gamma_pos=0.0, gamma_neg=1.0, reduction="sum"), + aux_loss=dict( + type="BarlowTwinsLoss", + off_diag_penality=1.0 / 128.0, + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/deit_tiny/template.yaml b/src/otx/algorithms/classification/configs/deit_tiny/template.yaml new file mode 100644 index 00000000000..1b5162ae540 --- /dev/null +++ b/src/otx/algorithms/classification/configs/deit_tiny/template.yaml @@ -0,0 +1,49 @@ +# Description. +model_template_id: Custom_Image_Classification_DeiT-Tiny +name: DeiT-Tiny +task_type: CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: Custom Image Classification for DeiT-Tiny +application: ~ + +# Algo backend. +framework: OTXClassification v1.2.3 + +# Task implementations. +entrypoints: + base: otx.algorithms.classification.adapters.mmcls.task.MMClassificationTask + openvino: otx.algorithms.classification.adapters.openvino.task.ClassificationOpenVINOTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 64 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.0001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 90 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 1.26 +size: 5.72 diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/__init__.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/__init__.py new file mode 100644 index 00000000000..2739d6bd067 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of EfficinetNet-B0 model for Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/compression_config.json b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/compression_config.json new file mode 100644 index 00000000000..c0799ac2fbb --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/compression_config.json @@ -0,0 +1,75 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 8192 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 8192 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 100 + } + } + } + }, + "nncf_quantization_pruning": { + "nncf": { + "coeff_decrease_lr_for_nncf": 1.0 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "filter_pruning", + "pruning_init": 0.1, + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median", + "prune_downsample_convs": true + } + }, + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 8192 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 8192 + } + } + } + ], + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "initial_training_phase_epochs": 100, + "patience_epochs": 100, + "maximal_total_epochs": 200 + } + } + } + }, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning"] +} diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/data_pipeline.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/data_pipeline.py new file mode 100644 index 00000000000..c847ddbe876 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of EfficientNet model for Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/deployment.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/deployment.py new file mode 100644 index 00000000000..27ac2597fdd --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/deployment.py @@ -0,0 +1,11 @@ +"""EfficientNet-B0 for multi-class MMDeploy config.""" + +_base_ = ["../base/deployments/base_classification_dynamic.py"] + +ir_config = dict( + output_names=["logits"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 224, 224]))], +) diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/hpo_config.yaml b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/hpo_config.yaml new file mode 100644 index 00000000000..613f82e7c36 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: accuracy +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.00098 + - 0.0245 + - 0.00001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 42 + - 96 + - 2 diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model.py new file mode 100644 index 00000000000..c397f704649 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model.py @@ -0,0 +1,25 @@ +"""EfficientNet-B0 for multi-class config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/efficientnet.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict( + version="b0", + ), + head=dict( + type="CustomLinearClsHead", + loss=dict( + type="CrossEntropyLoss", + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0, bf16_training=False) diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model_hierarchical.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model_hierarchical.py new file mode 100644 index 00000000000..d022dab122a --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model_hierarchical.py @@ -0,0 +1,24 @@ +"""EfficientNet-B0 for hierarchical config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/efficientnet.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict( + version="b0", + ), + head=dict( + type="CustomHierarchicalLinearClsHead", + multilabel_loss=dict( + type="AsymmetricLossWithIgnore", + gamma_pos=0.0, + gamma_neg=4.0, + ), + ), +) diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model_multilabel.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model_multilabel.py new file mode 100644 index 00000000000..1dad67ed874 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model_multilabel.py @@ -0,0 +1,24 @@ +"""EfficientNet-B0 for multi-label config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/multilabel/incremental.yaml", "../base/models/efficientnet.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict( + version="b0", + ), + head=dict( + type="CustomMultiLabelLinearClsHead", + normalized=True, + scale=7.0, + loss=dict(type="AsymmetricAngularLossWithIgnore", gamma_pos=0.0, gamma_neg=1.0, reduction="sum"), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/__init__.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/__init__.py new file mode 100644 index 00000000000..01dcc0cb67a --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of EfficinetNet-B0 model for Classification Task with Self-Supervised Learning.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/data_pipeline.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/data_pipeline.py new file mode 100644 index 00000000000..3d93cd46e56 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of EfficientNet model for Classification Self-SL Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/hparam.yaml b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/hparam.yaml new file mode 100644 index 00000000000..894d85d7186 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/hparam.yaml @@ -0,0 +1,29 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 64 + auto_hpo_state: POSSIBLE + num_workers: + default_value: 0 + learning_rate: + default_value: 0.45 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 50 + num_iters: + default_value: 1000 + enable_early_stopping: + default_value: false + use_adaptive_interval: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/model.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/model.py new file mode 100644 index 00000000000..5eb1c341d2b --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/selfsl/model.py @@ -0,0 +1,26 @@ +"""EfficientNet-B0 for self-supervised learning config.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/selfsl.yaml", "../../base/models/efficientnet.py"] + +model = dict( + type="BYOL", + task="classification", + backbone=dict( + version="b0", + ), + base_momentum=0.996, + neck=dict(type="SelfSLMLP", in_channels=1280, hid_channels=4096, out_channels=256, with_avg_pool=True), + head=dict( + _delete_=True, + type="ConstrastiveHead", + predictor=dict(type="SelfSLMLP", in_channels=256, hid_channels=4096, out_channels=256, with_avg_pool=False), + ), +) + +load_from = None + +resume_from = None + +fp16 = None diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/__init__.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/__init__.py new file mode 100644 index 00000000000..8f99d3dcb01 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of EfficinetNet-B0 model for Semi-SL Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/data_pipeline.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/data_pipeline.py new file mode 100644 index 00000000000..22e4a341147 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of EfficientNet model for Classification Semi-SL Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/hparam.yaml b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/hparam.yaml new file mode 100644 index 00000000000..a9a82600fbe --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/hparam.yaml @@ -0,0 +1,25 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 16 + auto_hpo_state: POSSIBLE + unlabeled_batch_size: + default_value: 48 + learning_rate: + default_value: 0.00245 + auto_hpo_state: POSSIBLE + early_stop_start: + default_value: 50 + early_stop_patience: + default_value: 10 + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 90 + use_adaptive_interval: + default_value: true + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/model.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/model.py new file mode 100644 index 00000000000..78fe7076069 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/model.py @@ -0,0 +1,22 @@ +"""EfficientNet-B0 config for semi-supervised multi-class classification.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/semisl.yaml", "../../base/models/efficientnet.py"] + +model = dict( + type="SemiSLClassifier", + task="classification", + backbone=dict( + version="b0", + ), + head=dict( + type="SemiLinearClsHead", + loss=dict( + type="CrossEntropyLoss", + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/model_multilabel.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/model_multilabel.py new file mode 100644 index 00000000000..ecac117cb36 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/semisl/model_multilabel.py @@ -0,0 +1,30 @@ +"""EfficientNet-B0 config for semi-supervised multi-label classification.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/multilabel/semisl.yaml", "../../base/models/efficientnet.py"] + +model = dict( + task="classification", + type="SemiSLMultilabelClassifier", + backbone=dict( + version="b0", + ), + head=dict( + type="SemiLinearMultilabelClsHead", + use_dynamic_loss_weighting=True, + unlabeled_coef=0.1, + in_channels=-1, + aux_mlp=dict(hid_channels=0, out_channels=1024), + normalized=True, + scale=7.0, + loss=dict(type="AsymmetricAngularLossWithIgnore", gamma_pos=0.0, gamma_neg=1.0, reduction="sum"), + aux_loss=dict( + type="BarlowTwinsLoss", + off_diag_penality=1.0 / 128.0, + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/__init__.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/__init__.py new file mode 100644 index 00000000000..730cc243601 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of EfficinetNet-B0 model for SupCon Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/data_pipeline.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/data_pipeline.py new file mode 100644 index 00000000000..d941e7fc690 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of EfficientNet model for Classification SupCon Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/supcon/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/hparam.yaml b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/hparam.yaml new file mode 100644 index 00000000000..3eae50ce136 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + enable_supcon: + default_value: True diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/model.py b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/model.py new file mode 100644 index 00000000000..a67240b4223 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/supcon/model.py @@ -0,0 +1,26 @@ +"""EfficientNet-B0 config for multi-class with contrastive loss for small datasets.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/supcon.yaml", "../../base/models/efficientnet.py"] + +model = dict( + task="classification", + type="SupConClassifier", + backbone=dict( + version="b0", + ), + head=dict( + type="SupConClsHead", + in_channels=-1, + aux_mlp=dict(hid_channels=0, out_channels=1024), + loss=dict(type="CrossEntropyLoss", loss_weight=1.0), + aux_loss=dict( + type="BarlowTwinsLoss", + off_diag_penality=1.0 / 128.0, + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/template.yaml b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/template.yaml new file mode 100644 index 00000000000..1781b32e82e --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/template.yaml @@ -0,0 +1,63 @@ +# Description. +model_template_id: Custom_Image_Classification_EfficinetNet-B0 +name: EfficientNet-B0 +task_type: CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Image Classification for EfficientNet-B0 +application: ~ + +# Algo backend. +framework: OTXClassification v1.2.3 + +# Task implementations. +entrypoints: + base: otx.algorithms.classification.adapters.mmcls.task.MMClassificationTask + openvino: otx.algorithms.classification.adapters.openvino.task.ClassificationOpenVINOTask + nncf: otx.algorithms.classification.adapters.mmcls.nncf.task.ClassificationNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 64 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.0049 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 90 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 0.81 +size: 4.09 + +# Model spec +model_category: BALANCE +is_default_for_task: true diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/__init__.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/__init__.py new file mode 100644 index 00000000000..9222127e9f2 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of EfficinetNet-V2 model for Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/compression_config.json b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/compression_config.json new file mode 100644 index 00000000000..d9b0fd23545 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/compression_config.json @@ -0,0 +1,35 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 8192 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 8192 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 100 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/data_pipeline.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/data_pipeline.py new file mode 100644 index 00000000000..b946a0632bd --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of EfficientNet V2 model for Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/deployment.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/deployment.py new file mode 100644 index 00000000000..f4b171c8bd4 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/deployment.py @@ -0,0 +1,11 @@ +"""EfficientNet-V2 for multi-class MMDeploy config.""" + +_base_ = ["../base/deployments/base_classification_dynamic.py"] + +ir_config = dict( + output_names=["logits"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 224, 224]))], +) diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/hpo_config.yaml b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/hpo_config.yaml new file mode 100644 index 00000000000..4b79b24194c --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: accuracy +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0007 + - 0.07 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 32 + - 128 + - 2 diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model.py new file mode 100644 index 00000000000..ea5ef1ff773 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model.py @@ -0,0 +1,19 @@ +"""EfficientNet-V2 for multi-class config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/efficientnet_v2.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict( + version="s_21k", + ), + head=dict(type="CustomLinearClsHead", loss=dict(type="CrossEntropyLoss", loss_weight=1.0)), +) + +fp16 = dict(loss_scale=512.0, bf16_training=False) diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model_hierarchical.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model_hierarchical.py new file mode 100644 index 00000000000..fbd590c9535 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model_hierarchical.py @@ -0,0 +1,22 @@ +"""EfficientNet-V2 for hierarchical config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/efficientnet_v2.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict(version="s_21k"), + head=dict( + type="CustomHierarchicalLinearClsHead", + multilabel_loss=dict( + type="AsymmetricLossWithIgnore", + gamma_pos=0.0, + gamma_neg=4.0, + ), + ), +) diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model_multilabel.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model_multilabel.py new file mode 100644 index 00000000000..b7c4743baf0 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/model_multilabel.py @@ -0,0 +1,24 @@ +"""EfficientNet-V2 for multi-label config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/multilabel/incremental.yaml", "../base/models/efficientnet_v2.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict( + version="s_21k", + ), + head=dict( + type="CustomMultiLabelLinearClsHead", + normalized=True, + scale=7.0, + loss=dict(type="AsymmetricAngularLossWithIgnore", gamma_pos=0.0, gamma_neg=1.0, reduction="sum"), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/__init__.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/__init__.py new file mode 100644 index 00000000000..663b45dd3c6 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of EfficinetNet-V2 model for Classification Task with Self-Supervised Learning.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/data_pipeline.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/data_pipeline.py new file mode 100644 index 00000000000..3d93cd46e56 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of EfficientNet model for Classification Self-SL Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/hparam.yaml b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/hparam.yaml new file mode 100644 index 00000000000..894d85d7186 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/hparam.yaml @@ -0,0 +1,29 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 64 + auto_hpo_state: POSSIBLE + num_workers: + default_value: 0 + learning_rate: + default_value: 0.45 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 50 + num_iters: + default_value: 1000 + enable_early_stopping: + default_value: false + use_adaptive_interval: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/model.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/model.py new file mode 100644 index 00000000000..ccef9d88a3d --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/selfsl/model.py @@ -0,0 +1,26 @@ +"""EfficientNet-V2 for self-supervised learning config.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/selfsl.yaml", "../../base/models/efficientnet_v2.py"] + +model = dict( + type="BYOL", + task="classification", + backbone=dict( + version="s_21k", + ), + base_momentum=0.996, + neck=dict(type="SelfSLMLP", in_channels=1280, hid_channels=4096, out_channels=256, with_avg_pool=True), + head=dict( + _delete_=True, + type="ConstrastiveHead", + predictor=dict(type="SelfSLMLP", in_channels=256, hid_channels=4096, out_channels=256, with_avg_pool=False), + ), +) + +load_from = None + +resume_from = None + +fp16 = None diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/__init__.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/__init__.py new file mode 100644 index 00000000000..ffe1266a6ed --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of EfficinetNet-V2 model for Semi-SL Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/data_pipeline.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/data_pipeline.py new file mode 100644 index 00000000000..22e4a341147 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of EfficientNet model for Classification Semi-SL Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/hparam.yaml b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/hparam.yaml new file mode 100644 index 00000000000..846660f0737 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/hparam.yaml @@ -0,0 +1,25 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 16 + auto_hpo_state: POSSIBLE + unlabeled_batch_size: + default_value: 48 + learning_rate: + default_value: 0.00355 + auto_hpo_state: POSSIBLE + early_stop_start: + default_value: 50 + early_stop_patience: + default_value: 10 + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 90 + use_adaptive_interval: + default_value: true + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/model.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/model.py new file mode 100644 index 00000000000..9611b69b646 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/model.py @@ -0,0 +1,16 @@ +"""EfficientNet-V2 config for semi-supervised multi-class classification.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/semisl.yaml", "../../base/models/efficientnet_v2.py"] + +model = dict( + type="SemiSLClassifier", + task="classification", + backbone=dict( + version="s_21k", + ), + head=dict(type="SemiLinearClsHead", loss=dict(type="CrossEntropyLoss", loss_weight=1.0)), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/model_multilabel.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/model_multilabel.py new file mode 100644 index 00000000000..1ef6a0d2fbc --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/semisl/model_multilabel.py @@ -0,0 +1,30 @@ +"""EfficientNet-V2 config for semi-supervised multi-label classification.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/multilabel/semisl.yaml", "../../base/models/efficientnet_v2.py"] + +model = dict( + task="classification", + type="SemiSLMultilabelClassifier", + backbone=dict( + version="s_21k", + ), + head=dict( + type="SemiLinearMultilabelClsHead", + use_dynamic_loss_weighting=True, + unlabeled_coef=0.1, + in_channels=-1, + aux_mlp=dict(hid_channels=0, out_channels=1024), + normalized=True, + scale=7.0, + loss=dict(type="AsymmetricAngularLossWithIgnore", gamma_pos=0.0, gamma_neg=1.0, reduction="sum"), + aux_loss=dict( + type="BarlowTwinsLoss", + off_diag_penality=1.0 / 128.0, + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/__init__.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/__init__.py new file mode 100644 index 00000000000..9115e38981a --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of EfficinetNet-V2 model for SupCon Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/data_pipeline.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/data_pipeline.py new file mode 100644 index 00000000000..d941e7fc690 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of EfficientNet model for Classification SupCon Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/supcon/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/hparam.yaml b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/hparam.yaml new file mode 100644 index 00000000000..3eae50ce136 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + enable_supcon: + default_value: True diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/model.py b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/model.py new file mode 100644 index 00000000000..fda22fa0945 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/supcon/model.py @@ -0,0 +1,26 @@ +"""EfficientNet-V2 config for multi-class with contrastive loss for small datasets.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/supcon.yaml", "../../base/models/efficientnet_v2.py"] + +model = dict( + task="classification", + type="SupConClassifier", + backbone=dict( + version="s_21k", + ), + head=dict( + type="SupConClsHead", + in_channels=-1, + aux_mlp=dict(hid_channels=0, out_channels=1024), + loss=dict(type="CrossEntropyLoss", loss_weight=1.0), + aux_loss=dict( + type="BarlowTwinsLoss", + off_diag_penality=1.0 / 128.0, + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/template.yaml b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/template.yaml new file mode 100644 index 00000000000..514815b1631 --- /dev/null +++ b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/template.yaml @@ -0,0 +1,62 @@ +# Description. +model_template_id: Custom_Image_Classification_EfficientNet-V2-S +name: EfficientNet-V2-S +task_type: CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Image Classification for EfficientNet-V2-S +application: ~ + +# Algo backend. +framework: OTXClassification v1.2.3 + +# Task implementations. +entrypoints: + base: otx.algorithms.classification.adapters.mmcls.task.MMClassificationTask + openvino: otx.algorithms.classification.adapters.openvino.task.ClassificationOpenVINOTask + nncf: otx.algorithms.classification.adapters.mmcls.nncf.task.ClassificationNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 64 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.0071 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 90 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 5.76 +size: 20.23 + +# Model spec +model_category: ACCURACY diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/__init__.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/__init__.py new file mode 100644 index 00000000000..8fae4288d4b --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNet-V3-large-075 model for Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/compression_config.json b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/compression_config.json new file mode 100644 index 00000000000..c0799ac2fbb --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/compression_config.json @@ -0,0 +1,75 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 8192 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 8192 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 100 + } + } + } + }, + "nncf_quantization_pruning": { + "nncf": { + "coeff_decrease_lr_for_nncf": 1.0 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "filter_pruning", + "pruning_init": 0.1, + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median", + "prune_downsample_convs": true + } + }, + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 8192 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 8192 + } + } + } + ], + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "initial_training_phase_epochs": 100, + "patience_epochs": 100, + "maximal_total_epochs": 200 + } + } + } + }, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning"] +} diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/data_pipeline.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/data_pipeline.py new file mode 100644 index 00000000000..d911f3e2f8b --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of MV3-Large-075 model for Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/hpo_config.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/hpo_config.yaml new file mode 100644 index 00000000000..3759cb116c2 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: accuracy +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0032 + - 0.08 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 20 + - 48 + - 2 diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model.py new file mode 100644 index 00000000000..36c0986b557 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model.py @@ -0,0 +1,21 @@ +"""MobileNet-V3-large-075 for multi-class config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/mobilenet_v3.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict( + mode="large", + width_mult=0.75, + ), + head=dict( + in_channels=720, + hid_channels=1280, + ), +) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model_hierarchical.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model_hierarchical.py new file mode 100644 index 00000000000..1ac0b51bcd1 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model_hierarchical.py @@ -0,0 +1,27 @@ +"""MobileNet-V3-large-075 for hierarchical config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/mobilenet_v3.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict( + mode="large", + width_mult=0.75, + ), + head=dict( + type="CustomHierarchicalNonLinearClsHead", + in_channels=720, + hid_channels=1280, + multilabel_loss=dict( + type="AsymmetricLossWithIgnore", + gamma_pos=0.0, + gamma_neg=4.0, + ), + ), +) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model_multilabel.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model_multilabel.py new file mode 100644 index 00000000000..38c370e2af5 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/model_multilabel.py @@ -0,0 +1,27 @@ +"""MobileNet-V3-large-075 for multi-label config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/multilabel/incremental.yaml", "../base/models/mobilenet_v3.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict( + mode="large", + width_mult=0.75, + ), + head=dict( + type="CustomMultiLabelNonLinearClsHead", + in_channels=720, + hid_channels=1280, + loss=dict( + type="AsymmetricLossWithIgnore", + gamma_pos=0.0, + gamma_neg=0.0, + ), + ), +) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/__init__.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/__init__.py new file mode 100644 index 00000000000..a73b8f8668b --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNet-V3-large-1 model for Classification Task with Self-Supervised Learning.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/data_pipeline.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/data_pipeline.py new file mode 100644 index 00000000000..603a2da3926 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of MV3-large-075 model for Classification Self-SL Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/hparam.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/hparam.yaml new file mode 100644 index 00000000000..894d85d7186 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/hparam.yaml @@ -0,0 +1,29 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 64 + auto_hpo_state: POSSIBLE + num_workers: + default_value: 0 + learning_rate: + default_value: 0.45 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 50 + num_iters: + default_value: 1000 + enable_early_stopping: + default_value: false + use_adaptive_interval: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/model.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/model.py new file mode 100644 index 00000000000..a5a901369ee --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/selfsl/model.py @@ -0,0 +1,28 @@ +"""MobileNet-V3-large-075 for self-supervised learning config.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/selfsl.yaml", "../../base/models/mobilenet_v3.py"] + + +model = dict( + type="BYOL", + task="classification", + backbone=dict( + mode="large", + width_mult=0.75, + ), + base_momentum=0.996, + neck=dict(type="SelfSLMLP", in_channels=720, hid_channels=4096, out_channels=256, with_avg_pool=True), + head=dict( + _delete_=True, + type="ConstrastiveHead", + predictor=dict(type="SelfSLMLP", in_channels=256, hid_channels=4096, out_channels=256, with_avg_pool=False), + ), +) + +load_from = None + +resume_from = None + +fp16 = None diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/__init__.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/__init__.py new file mode 100644 index 00000000000..652042efb62 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNet-V3-large-075 model for SupCon Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/data_pipeline.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/data_pipeline.py new file mode 100644 index 00000000000..1e157883c67 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of MV3-large-075 model for Classification SupCon Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/supcon/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/hparam.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/hparam.yaml new file mode 100644 index 00000000000..3eae50ce136 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + enable_supcon: + default_value: True diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/model.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/model.py new file mode 100644 index 00000000000..6deee6b8eff --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/supcon/model.py @@ -0,0 +1,25 @@ +"""MobileNet-V3-large-075 config for multi-class with contrastive loss for small datasets.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/supcon.yaml", "../../base/models/mobilenet_v3.py"] + +model = dict( + task="classification", + type="SupConClassifier", + backbone=dict( + mode="large", + width_mult=0.75, + ), + head=dict( + type="SupConClsHead", + in_channels=-1, + aux_mlp=dict(hid_channels=0, out_channels=1024), + loss=dict(type="CrossEntropyLoss", loss_weight=1.0), + aux_loss=dict( + type="BarlowTwinsLoss", + off_diag_penality=1.0 / 128.0, + loss_weight=1.0, + ), + ), +) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/template_experiment.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/template_experiment.yaml new file mode 100644 index 00000000000..5c1bd2edee7 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_075_cls_incr/template_experiment.yaml @@ -0,0 +1,50 @@ +# Description. +model_template_id: Custom_Image_Classification_MobileNet-V3-large-0.75x +name: MobileNet-V3-large-0.75x +task_type: CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Image Classification for MobileNet-V3-learge-0.75x +application: ~ + +# Algo backend. +framework: OTXClassification v1.2.3 + +# Task implementations. +entrypoints: + base: otx.algorithms.classification.adapters.mmcls.task.MMClassificationTask + openvino: otx.algorithms.classification.adapters.openvino.task.ClassificationOpenVINOTask + nncf: otx.algorithms.classification.adapters.mmcls.nncf.task.ClassificationNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 32 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.016 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 20 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 0.32 +size: 2.76 diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/__init__.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/__init__.py new file mode 100644 index 00000000000..8da71d83cc5 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNet-V3-large-1 model for Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/compression_config.json b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/compression_config.json new file mode 100644 index 00000000000..c0799ac2fbb --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/compression_config.json @@ -0,0 +1,75 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 8192 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 8192 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 100 + } + } + } + }, + "nncf_quantization_pruning": { + "nncf": { + "coeff_decrease_lr_for_nncf": 1.0 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "filter_pruning", + "pruning_init": 0.1, + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median", + "prune_downsample_convs": true + } + }, + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 8192 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 8192 + } + } + } + ], + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "initial_training_phase_epochs": 100, + "patience_epochs": 100, + "maximal_total_epochs": 200 + } + } + } + }, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning"] +} diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/data_pipeline.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/data_pipeline.py new file mode 100644 index 00000000000..852ab63af6d --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of MV3-large model for Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/deployment.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/deployment.py new file mode 100644 index 00000000000..4af7a810348 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/deployment.py @@ -0,0 +1,11 @@ +"""MobileNet-V3-large-1 for multi-class MMDeploy config.""" + +_base_ = ["../base/deployments/base_classification_dynamic.py"] + +ir_config = dict( + output_names=["logits"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 224, 224]))], +) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/hpo_config.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/hpo_config.yaml new file mode 100644 index 00000000000..2202588ff32 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: accuracy +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.00029 + - 0.1 + - 0.00001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 64 + - 256 + - 2 diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model.py new file mode 100644 index 00000000000..f8cbfe4b01a --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model.py @@ -0,0 +1,25 @@ +"""MobileNet-V3-large-1 for multi-class config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/mobilenet_v3.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict(mode="large"), + head=dict( + type="CustomNonLinearClsHead", + in_channels=960, + hid_channels=1280, + loss=dict( + type="CrossEntropyLoss", + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0, bf16_training=False) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model_hierarchical.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model_hierarchical.py new file mode 100644 index 00000000000..8e5853da48c --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model_hierarchical.py @@ -0,0 +1,24 @@ +"""MobileNet-V3-large-1 for hierarchical config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/mobilenet_v3.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict(mode="large"), + head=dict( + type="CustomHierarchicalNonLinearClsHead", + in_channels=960, + hid_channels=1280, + multilabel_loss=dict( + type="AsymmetricLossWithIgnore", + gamma_pos=0.0, + gamma_neg=4.0, + ), + ), +) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model_multilabel.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model_multilabel.py new file mode 100644 index 00000000000..e14c5479cff --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/model_multilabel.py @@ -0,0 +1,32 @@ +"""MobileNet-V3-large-1 for multi-label config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/multilabel/incremental.yaml", "../base/models/mobilenet_v3.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + backbone=dict(mode="large"), + head=dict( + type="CustomMultiLabelNonLinearClsHead", + in_channels=960, + hid_channels=1280, + normalized=True, + scale=7.0, + act_cfg=dict( + type="PReLU", + ), + loss=dict( + type="AsymmetricAngularLossWithIgnore", + gamma_pos=0.0, + gamma_neg=1.0, + reduction="sum", + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/__init__.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/__init__.py new file mode 100644 index 00000000000..53571bd90c5 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNet-V3-large-075 model for Classification Task with Self-Supervised Learning.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/data_pipeline.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/data_pipeline.py new file mode 100644 index 00000000000..b10a04b901f --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of MV3-large model for Classification Self-SL Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/hparam.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/hparam.yaml new file mode 100644 index 00000000000..894d85d7186 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/hparam.yaml @@ -0,0 +1,29 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 64 + auto_hpo_state: POSSIBLE + num_workers: + default_value: 0 + learning_rate: + default_value: 0.45 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 50 + num_iters: + default_value: 1000 + enable_early_stopping: + default_value: false + use_adaptive_interval: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/model.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/model.py new file mode 100644 index 00000000000..1685acad7e1 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/selfsl/model.py @@ -0,0 +1,26 @@ +"""MobileNet-V3-large-1 for self-supervised learning config.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/selfsl.yaml", "../../base/models/mobilenet_v3.py"] + +model = dict( + type="BYOL", + task="classification", + backbone=dict(mode="large"), + base_momentum=0.996, + neck=dict(type="SelfSLMLP", in_channels=960, hid_channels=4096, out_channels=256, with_avg_pool=True), + head=dict( + _delete_=True, + type="ConstrastiveHead", + predictor=dict(type="SelfSLMLP", in_channels=256, hid_channels=4096, out_channels=256, with_avg_pool=False), + in_channels=960, + hid_channels=1280, + ), +) + +load_from = None + +resume_from = None + +fp16 = None diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/__init__.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/__init__.py new file mode 100644 index 00000000000..273034ff6ad --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNet-V3-large-1 model for Semi-SL Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/data_pipeline.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/data_pipeline.py new file mode 100644 index 00000000000..7fa731c2da1 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of MV3-large model for Classification Semi-SL Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/hparam.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/hparam.yaml new file mode 100644 index 00000000000..cad82643fff --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/hparam.yaml @@ -0,0 +1,25 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 16 + auto_hpo_state: POSSIBLE + unlabeled_batch_size: + default_value: 48 + learning_rate: + default_value: 0.0029 + auto_hpo_state: POSSIBLE + early_stop_start: + default_value: 50 + early_stop_patience: + default_value: 10 + learning_rate_warmup_iters: + default_value: 10 + num_iters: + default_value: 90 + use_adaptive_interval: + default_value: true + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/model.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/model.py new file mode 100644 index 00000000000..ff1889dcce9 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/model.py @@ -0,0 +1,22 @@ +"""MobileNet-V3-large-1 config for semi-supervised multi-class classification.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/semisl.yaml", "../../base/models/mobilenet_v3.py"] + +model = dict( + type="SemiSLClassifier", + task="classification", + backbone=dict(mode="large"), + head=dict( + type="SemiNonLinearClsHead", + in_channels=960, + hid_channels=1280, + loss=dict( + type="CrossEntropyLoss", + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/model_multilabel.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/model_multilabel.py new file mode 100644 index 00000000000..6421128d601 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/semisl/model_multilabel.py @@ -0,0 +1,29 @@ +"""MobileNet-V3-large-1 config for semi-supervised multi-label classification.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/multilabel/semisl.yaml", "../../base/models/mobilenet_v3.py"] + +model = dict( + task="classification", + type="SemiSLMultilabelClassifier", + backbone=dict(mode="large"), + head=dict( + type="SemiNonLinearMultilabelClsHead", + in_channels=960, + hid_channels=1280, + use_dynamic_loss_weighting=True, + unlabeled_coef=0.1, + aux_mlp=dict(hid_channels=0, out_channels=1024), + normalized=True, + scale=7.0, + loss=dict(type="AsymmetricAngularLossWithIgnore", gamma_pos=0.0, gamma_neg=1.0, reduction="sum"), + aux_loss=dict( + type="BarlowTwinsLoss", + off_diag_penality=1.0 / 128.0, + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/__init__.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/__init__.py new file mode 100644 index 00000000000..32ea19f94f1 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNet-V3-large-1 model for SupCon Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/data_pipeline.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/data_pipeline.py new file mode 100644 index 00000000000..8d5be5ed9c1 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of MV3-large model for Classification SupCon Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/supcon/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/hparam.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/hparam.yaml new file mode 100644 index 00000000000..3eae50ce136 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + enable_supcon: + default_value: True diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/model.py b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/model.py new file mode 100644 index 00000000000..80a8b25ea50 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/supcon/model.py @@ -0,0 +1,25 @@ +"""MobileNet-V3-large-1 config for multi-class with contrastive loss for small datasets.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/supcon.yaml", "../../base/models/mobilenet_v3.py"] + +model = dict( + task="classification", + type="SupConClassifier", + backbone=dict(mode="large"), + head=dict( + _delete_=True, + type="SupConClsHead", + in_channels=-1, + aux_mlp=dict(hid_channels=0, out_channels=1024), + loss=dict(type="CrossEntropyLoss", loss_weight=1.0), + aux_loss=dict( + type="BarlowTwinsLoss", + off_diag_penality=1.0 / 128.0, + loss_weight=1.0, + ), + ), +) + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/template.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/template.yaml new file mode 100644 index 00000000000..60573d606c5 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/template.yaml @@ -0,0 +1,62 @@ +# Description. +model_template_id: Custom_Image_Classification_MobileNet-V3-large-1x +name: MobileNet-V3-large-1x +task_type: CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Image Classification for MobileNet-V3-large-1x +application: ~ + +# Algo backend. +framework: OTXClassification v1.2.3 + +# Task implementations. +entrypoints: + base: otx.algorithms.classification.adapters.mmcls.task.MMClassificationTask + openvino: otx.algorithms.classification.adapters.openvino.task.ClassificationOpenVINOTask + nncf: otx.algorithms.classification.adapters.mmcls.nncf.task.ClassificationNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 64 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.0058 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 10 + num_iters: + default_value: 90 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 0.44 +size: 4.29 + +# Model spec +model_category: SPEED diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/__init__.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/__init__.py new file mode 100644 index 00000000000..b1ad9dafe94 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNet-V3-Small for Classification Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/compression_config.json b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/compression_config.json new file mode 100644 index 00000000000..c0799ac2fbb --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/compression_config.json @@ -0,0 +1,75 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 8192 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 8192 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 100 + } + } + } + }, + "nncf_quantization_pruning": { + "nncf": { + "coeff_decrease_lr_for_nncf": 1.0 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "filter_pruning", + "pruning_init": 0.1, + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median", + "prune_downsample_convs": true + } + }, + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 8192 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 8192 + } + } + } + ], + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "initial_training_phase_epochs": 100, + "patience_epochs": 100, + "maximal_total_epochs": 200 + } + } + } + }, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning"] +} diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/data_pipeline.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/data_pipeline.py new file mode 100644 index 00000000000..246e7e2dd1a --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of MV3-small model for Classification Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/hpo_config.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/hpo_config.yaml new file mode 100644 index 00000000000..3cd14bac17e --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: accuracy +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0032 + - 0.08 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 20 + - 48 + - 2 diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model.py new file mode 100644 index 00000000000..2117d8a3014 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model.py @@ -0,0 +1,13 @@ +"""MobileNet-V3-Small for multi-class config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/mobilenet_v3.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", +) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model_hierarchical.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model_hierarchical.py new file mode 100644 index 00000000000..77f108deb3e --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model_hierarchical.py @@ -0,0 +1,21 @@ +"""MobileNet-V3-Small for hierarchical config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/incremental.yaml", "../base/models/mobilenet_v3.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + head=dict( + type="CustomHierarchicalNonLinearClsHead", + multilabel_loss=dict( + type="AsymmetricLossWithIgnore", + gamma_pos=0.0, + gamma_neg=4.0, + ), + ), +) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model_multilabel.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model_multilabel.py new file mode 100644 index 00000000000..918699d51a0 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/model_multilabel.py @@ -0,0 +1,21 @@ +"""MobileNet-V3-Small for multi-label config.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = ["../../../../recipes/stages/classification/multilabel/incremental.yaml", "../base/models/mobilenet_v3.py"] + +model = dict( + type="CustomImageClassifier", + task="classification", + head=dict( + type="CustomMultiLabelNonLinearClsHead", + loss=dict( + type="AsymmetricLossWithIgnore", + gamma_pos=0.0, + gamma_neg=0.0, + ), + ), +) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/__init__.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/__init__.py new file mode 100644 index 00000000000..d0830e5d2fd --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNet-V3-Small model for Classification Task with Self-Supervised Learning.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/data_pipeline.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/data_pipeline.py new file mode 100644 index 00000000000..2e5d26d56e8 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of MV3-small model for Classification Self-SL Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/hparam.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/hparam.yaml new file mode 100644 index 00000000000..894d85d7186 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/hparam.yaml @@ -0,0 +1,29 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 64 + auto_hpo_state: POSSIBLE + num_workers: + default_value: 0 + learning_rate: + default_value: 0.45 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 50 + num_iters: + default_value: 1000 + enable_early_stopping: + default_value: false + use_adaptive_interval: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/model.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/model.py new file mode 100644 index 00000000000..c8be36bad38 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/selfsl/model.py @@ -0,0 +1,23 @@ +"""MobileNet-V3-Small for self-supervised learning config.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/selfsl.yaml", "../../base/models/mobilenet_v3.py"] + +model = dict( + type="BYOL", + task="classification", + base_momentum=0.996, + neck=dict(type="SelfSLMLP", in_channels=576, hid_channels=4096, out_channels=256, with_avg_pool=True), + head=dict( + _delete_=True, + type="ConstrastiveHead", + predictor=dict(type="SelfSLMLP", in_channels=256, hid_channels=4096, out_channels=256, with_avg_pool=False), + ), +) + +load_from = None + +resume_from = None + +fp16 = None diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/__init__.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/__init__.py new file mode 100644 index 00000000000..b94d2fde97d --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNet-V3-Small for SupCon Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/data_pipeline.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/data_pipeline.py new file mode 100644 index 00000000000..90a376dc2d0 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of MV3-small model for Classification SupCon Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../../base/data/supcon/data_pipeline.py"] diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/hparam.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/hparam.yaml new file mode 100644 index 00000000000..3eae50ce136 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + enable_supcon: + default_value: True diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/model.py b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/model.py new file mode 100644 index 00000000000..f09190e45c6 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/supcon/model.py @@ -0,0 +1,21 @@ +"""MobileNet-V3-Small config for multi-class with contrastive loss for small datasets.""" + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/classification/supcon.yaml", "../../base/models/mobilenet_v3.py"] + +model = dict( + task="classification", + type="SupConClassifier", + head=dict( + type="SupConClsHead", + in_channels=-1, + aux_mlp=dict(hid_channels=0, out_channels=1024), + loss=dict(type="CrossEntropyLoss", loss_weight=1.0), + aux_loss=dict( + type="BarlowTwinsLoss", + off_diag_penality=1.0 / 128.0, + loss_weight=1.0, + ), + ), +) diff --git a/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/template_experiment.yaml b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/template_experiment.yaml new file mode 100644 index 00000000000..948570ff9d4 --- /dev/null +++ b/src/otx/algorithms/classification/configs/mobilenet_v3_small_cls_incr/template_experiment.yaml @@ -0,0 +1,50 @@ +# Description. +model_template_id: Custom_Image_Classification_MobileNet-V3-small +name: MobileNet-V3-small +task_type: CLASSIFICATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Image Classification for MobileNet-V3-small +application: ~ + +# Algo backend. +framework: OTXClassification v1.2.3 + +# Task implementations. +entrypoints: + base: otx.algorithms.classification.adapters.mmcls.task.MMClassificationTask + openvino: otx.algorithms.classification.adapters.openvino.task.ClassificationOpenVINOTask + nncf: otx.algorithms.classification.adapters.mmcls.nncf.task.ClassificationNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 32 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.016 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 20 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 0.12 +size: 1.56 diff --git a/src/otx/algorithms/classification/task.py b/src/otx/algorithms/classification/task.py new file mode 100644 index 00000000000..107cc31c225 --- /dev/null +++ b/src/otx/algorithms/classification/task.py @@ -0,0 +1,545 @@ +"""Task of OTX Classification.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import io +import json +import os +from abc import ABC, abstractmethod +from typing import List, Optional + +import numpy as np +import torch + +from otx.algorithms.classification.configs.base import ClassificationConfig +from otx.algorithms.classification.utils import ( + get_cls_deploy_config, + get_cls_inferencer_configuration, + get_cls_model_api_configuration, + get_hierarchical_label_list, +) +from otx.algorithms.classification.utils import ( + get_multihead_class_info as get_hierarchical_info, +) +from otx.algorithms.common.configs import TrainType +from otx.algorithms.common.configs.configuration_enums import InputSizePreset +from otx.algorithms.common.tasks.base_task import TRAIN_TYPE_DIR_PATH, OTXTask +from otx.algorithms.common.utils import embed_ir_model_data +from otx.algorithms.common.utils.callback import TrainingProgressCallback +from otx.algorithms.common.utils.utils import embed_onnx_model_data +from otx.api.configuration import cfg_helper +from otx.api.configuration.helper.utils import ids_to_strings +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import ( + InferenceParameters, +) +from otx.api.entities.inference_parameters import ( + default_progress_callback as default_infer_progress_callback, +) +from otx.api.entities.label import LabelEntity +from otx.api.entities.label_schema import LabelGroup +from otx.api.entities.metadata import FloatMetadata, FloatType +from otx.api.entities.metrics import ( + CurveMetric, + LineChartInfo, + LineMetricsGroup, + MetricsGroup, + Performance, + ScoreMetric, +) +from otx.api.entities.model import ( + ModelEntity, + ModelPrecision, +) +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.tensor import TensorEntity +from otx.api.entities.train_parameters import ( + TrainParameters, +) +from otx.api.entities.train_parameters import ( + default_progress_callback as default_train_progress_callback, +) +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.utils.dataset_utils import add_saliency_maps_to_dataset_item +from otx.api.utils.labels_utils import get_empty_label +from otx.cli.utils.multi_gpu import is_multigpu_child_process +from otx.core.data.caching.mem_cache_handler import MemCacheHandlerSingleton +from otx.utils.logger import get_logger + +logger = get_logger() +RECIPE_TRAIN_TYPE = { + TrainType.Semisupervised: "semisl.yaml", + TrainType.Incremental: "incremental.yaml", + TrainType.Selfsupervised: "selfsl.yaml", +} + + +class OTXClassificationTask(OTXTask, ABC): + """Task class for OTX classification.""" + + # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-boolean-expressions + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__(task_environment, output_path) + self._task_config = ClassificationConfig + self._hyperparams = self._task_environment.get_hyper_parameters(self._task_config) + if len(self._task_environment.get_labels(False)) == 1: + self._labels = self._task_environment.get_labels(include_empty=True) + else: + self._labels = self._task_environment.get_labels(include_empty=False) + self._empty_label = get_empty_label(self._task_environment.label_schema) + + self._multilabel = False + self._hierarchical = False + self._hierarchical_info = None + self._selfsl = False + self._set_train_mode() + + self._train_type = self._hyperparams.algo_backend.train_type + self._model_dir = os.path.join( + os.path.abspath(os.path.dirname(self._task_environment.model_template.model_template_path)), + TRAIN_TYPE_DIR_PATH[self._train_type.name], + ) + if ( + self._train_type in RECIPE_TRAIN_TYPE + and self._train_type == TrainType.Incremental + and not self._multilabel + and not self._hierarchical + and self._hyperparams.learning_parameters.enable_supcon + and not self._model_dir.endswith("supcon") + ): + self._model_dir = os.path.join(self._model_dir, "supcon") + + self.data_pipeline_path = os.path.join(self._model_dir, "data_pipeline.py") + + if self._task_environment.model is not None: + self._load_model() + if hasattr(self._hyperparams.learning_parameters, "input_size"): + input_size_cfg = InputSizePreset(self._hyperparams.learning_parameters.input_size.value) + else: + input_size_cfg = InputSizePreset.DEFAULT + self._input_size = input_size_cfg.tuple + + if hasattr(self._hyperparams.learning_parameters, "input_size"): + input_size_cfg = InputSizePreset(self._hyperparams.learning_parameters.input_size.value) + else: + input_size_cfg = InputSizePreset.DEFAULT + self._input_size = input_size_cfg.tuple + + def _is_multi_label(self, label_groups: List[LabelGroup], all_labels: List[LabelEntity]): + """Check whether the current training mode is multi-label or not.""" + # NOTE: In the current Geti, multi-label should have `___` symbol for all group names. + find_multilabel_symbol = ["___" in getattr(i, "name", "") for i in label_groups] + return ( + (len(label_groups) > 1) and (len(label_groups) == len(all_labels)) and (False not in find_multilabel_symbol) + ) + + def _set_train_mode(self): + label_groups = self._task_environment.label_schema.get_groups(include_empty=False) + all_labels = self._task_environment.label_schema.get_labels(include_empty=False) + + self._multilabel = self._is_multi_label(label_groups, all_labels) + if self._multilabel: + logger.info("Classification mode: multilabel") + elif len(label_groups) > 1: + logger.info("Classification mode: hierarchical") + self._hierarchical = True + self._hierarchical_info = get_hierarchical_info(self._task_environment.label_schema) + if not self._multilabel and not self._hierarchical: + logger.info("Classification mode: multiclass") + + if self._hyperparams.algo_backend.train_type == TrainType.Selfsupervised: + self._selfsl = True + + def infer( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ) -> DatasetEntity: + """Main infer function of OTX Classification.""" + + logger.info("infer()") + + results = self._infer_model(dataset, inference_parameters) + prediction_results = zip( + results["eval_predictions"], + results["feature_vectors"], + results["saliency_maps"], + ) + + update_progress_callback = default_infer_progress_callback + process_saliency_maps = False + explain_predicted_classes = True + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress # type: ignore + process_saliency_maps = inference_parameters.process_saliency_maps + explain_predicted_classes = inference_parameters.explain_predicted_classes + + self._add_predictions_to_dataset( + prediction_results, dataset, update_progress_callback, process_saliency_maps, explain_predicted_classes + ) + return dataset + + def train( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + train_parameters: Optional[TrainParameters] = None, + seed: Optional[int] = None, + deterministic: bool = False, + ): + """Train function for OTX classification task. + + Actual training is processed by _train_model fucntion + """ + logger.info("train()") + # Check for stop signal when training has stopped. + # If should_stop is true, training was cancelled and no new + if self._should_stop: + logger.info("Training cancelled.") + self._should_stop = False + self._is_training = False + return + self.seed = seed + self.deterministic = deterministic + + # Set OTX LoggerHook & Time Monitor + if train_parameters: + update_progress_callback = train_parameters.update_progress + else: + update_progress_callback = default_train_progress_callback + self._time_monitor = TrainingProgressCallback(update_progress_callback) + + results = self._train_model(dataset) + + MemCacheHandlerSingleton.delete() + + # Check for stop signal when training has stopped. If should_stop is true, training was cancelled and no new + if self._should_stop: + logger.info("Training cancelled.") + self._should_stop = False + self._is_training = False + return + + # get output model + model_ckpt = results.get("final_ckpt") + if model_ckpt is None: + logger.error("cannot find final checkpoint from the results.") + return + # update checkpoint to the newly trained model + self._model_ckpt = model_ckpt + + # compose performance statistics + training_metrics, final_acc = self._generate_training_metrics(self._learning_curves) + # save resulting model + self.save_model(output_model) + performance = Performance( + score=ScoreMetric(value=final_acc, name="accuracy"), + dashboard_metrics=training_metrics, + ) + logger.info(f"Final model performance: {str(performance)}") + output_model.performance = performance + self._is_training = False + logger.info("train done.") + + def export( + self, + export_type: ExportType, + output_model: ModelEntity, + precision: ModelPrecision = ModelPrecision.FP32, + dump_features: bool = True, + ): + """Export function of OTX Classification Task.""" + + logger.info("Exporting the model") + + self._update_model_export_metadata(output_model, export_type, precision, dump_features) + results = self._export_model(precision, export_type, dump_features) + outputs = results.get("outputs") + logger.debug(f"results of run_task = {outputs}") + if outputs is None: + raise RuntimeError(results.get("msg")) + + inference_config = get_cls_inferencer_configuration(self._task_environment.label_schema) + extra_model_data = get_cls_model_api_configuration(self._task_environment.label_schema, inference_config) + if export_type == ExportType.ONNX: + extra_model_data[("model_info", "mean_values")] = results.get("inference_parameters").get("mean_values") + extra_model_data[("model_info", "scale_values")] = results.get("inference_parameters").get("scale_values") + + onnx_file = outputs.get("onnx") + embed_onnx_model_data(onnx_file, extra_model_data) + with open(onnx_file, "rb") as f: + output_model.set_data("model.onnx", f.read()) + else: + bin_file = outputs.get("bin") + xml_file = outputs.get("xml") + + deploy_cfg = get_cls_deploy_config(self._task_environment.label_schema, inference_config) + extra_model_data[("otx_config",)] = json.dumps(deploy_cfg, ensure_ascii=False) + embed_ir_model_data(xml_file, extra_model_data) + + with open(bin_file, "rb") as f: + output_model.set_data("openvino.bin", f.read()) + with open(xml_file, "rb") as f: + output_model.set_data("openvino.xml", f.read()) + + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self._task_environment.label_schema), + ) + logger.info("Exporting completed") + + def explain( + self, + dataset: DatasetEntity, + explain_parameters: Optional[ExplainParameters] = None, + ) -> DatasetEntity: + """Main explain function of OTX Classification Task.""" + + predictions, saliency_maps = self._explain_model( + dataset, + explain_parameters=explain_parameters, + ) + + update_progress_callback = default_infer_progress_callback + process_saliency_maps = False + explain_predicted_classes = True + if explain_parameters is not None: + update_progress_callback = explain_parameters.update_progress # type: ignore + process_saliency_maps = explain_parameters.process_saliency_maps + explain_predicted_classes = explain_parameters.explain_predicted_classes + + self._add_explanations_to_dataset( + predictions, + saliency_maps, + dataset, + update_progress_callback, + process_saliency_maps, + explain_predicted_classes, + ) + logger.info("Explain completed") + return dataset + + def evaluate( + self, + output_resultset: ResultSetEntity, + evaluation_metric: Optional[str] = None, + ): + """Evaluate function of OTX Classification Task.""" + + logger.info("called evaluate()") + if evaluation_metric is not None: + logger.warning( + f"Requested to use {evaluation_metric} metric, " "but parameter is ignored. Use accuracy instead." + ) + metric = MetricsHelper.compute_accuracy(output_resultset) + logger.info(f"Accuracy after evaluation: {metric.accuracy.value}") + output_resultset.performance = metric.get_performance() + logger.info("Evaluation completed") + + # pylint: disable=too-many-branches, too-many-locals + def _add_predictions_to_dataset( + self, + prediction_results, + dataset, + update_progress_callback, + process_saliency_maps=False, + explain_predicted_classes=True, + ): + """Loop over dataset again to assign predictions.Convert from MMClassification format to OTX format.""" + + dataset_size = len(dataset) + pos_thr = 0.5 + label_list = self._labels + # Fix the order for hierarchical labels to adjust classes with model outputs + if self._hierarchical: + label_list = get_hierarchical_label_list(self._hierarchical_info, label_list) + for i, (dataset_item, prediction_items) in enumerate(zip(dataset, prediction_results)): + prediction_item, feature_vector, saliency_map = prediction_items + if any(np.isnan(prediction_item)): + logger.info("Nan in prediction_item.") + + item_labels = self._get_item_labels(prediction_item, pos_thr) + + dataset_item.append_labels(item_labels) + + probs = TensorEntity(name="probabilities", numpy=prediction_item.reshape(-1)) + dataset_item.append_metadata_item(probs, model=self._task_environment.model) + + top_idxs = np.argpartition(prediction_item, -2)[-2:] + top_probs = prediction_item[top_idxs] + active_score_media = FloatMetadata( + name="active_score", value=top_probs[1] - top_probs[0], float_type=FloatType.ACTIVE_SCORE + ) + dataset_item.append_metadata_item(active_score_media, model=self._task_environment.model) + + if feature_vector is not None: + active_score = TensorEntity(name="representation_vector", numpy=feature_vector.reshape(-1)) + dataset_item.append_metadata_item(active_score, model=self._task_environment.model) + + if saliency_map is not None: + add_saliency_maps_to_dataset_item( + dataset_item=dataset_item, + saliency_map=saliency_map, + model=self._task_environment.model, + labels=label_list, + predicted_scored_labels=item_labels, + explain_predicted_classes=explain_predicted_classes, + process_saliency_maps=process_saliency_maps, + ) + update_progress_callback(int(i / dataset_size * 100)) + + # pylint: disable=too-many-locals + def _get_item_labels(self, prediction_item, pos_thr): + item_labels = [] + if self._multilabel: + if max(prediction_item) < pos_thr: + logger.info("Confidence is smaller than pos_thr, empty_label will be appended to item_labels.") + item_labels.append(ScoredLabel(self._empty_label, probability=1.0)) + else: + for cls_idx, pred_item in enumerate(prediction_item): + if pred_item > pos_thr: + cls_label = ScoredLabel(self._labels[cls_idx], probability=float(pred_item)) + item_labels.append(cls_label) + + elif self._hierarchical: + for head_idx in range(self._hierarchical_info["num_multiclass_heads"]): + logits_begin, logits_end = self._hierarchical_info["head_idx_to_logits_range"][str(head_idx)] + head_logits = prediction_item[logits_begin:logits_end] + head_pred = np.argmax(head_logits) # Assume logits already passed softmax + label_str = self._hierarchical_info["all_groups"][head_idx][head_pred] + otx_label = next(x for x in self._labels if x.name == label_str) + item_labels.append(ScoredLabel(label=otx_label, probability=float(head_logits[head_pred]))) + + if self._hierarchical_info["num_multilabel_classes"]: + head_logits = prediction_item[self._hierarchical_info["num_single_label_classes"] :] + for logit_idx, logit in enumerate(head_logits): + if logit > pos_thr: # Assume logits already passed sigmoid + label_str_idx = self._hierarchical_info["num_multiclass_heads"] + logit_idx + label_str = self._hierarchical_info["all_groups"][label_str_idx][0] + otx_label = next(x for x in self._labels if x.name == label_str) + item_labels.append(ScoredLabel(label=otx_label, probability=float(logit))) + item_labels = self._task_environment.label_schema.resolve_labels_greedily(item_labels) + if not item_labels: + logger.info("item_labels is empty.") + item_labels.append(ScoredLabel(self._empty_label, probability=1.0)) + + else: + label_idx = prediction_item.argmax() + cls_label = ScoredLabel( + self._labels[label_idx], + probability=float(prediction_item[label_idx]), + ) + item_labels.append(cls_label) + return item_labels + + def _add_explanations_to_dataset( + self, + predictions, + saliency_maps, + dataset, + update_progress_callback, + process_saliency_maps, + explain_predicted_classes, + ): + """Loop over dataset again and assign saliency maps.""" + dataset_size = len(dataset) + label_list = self._labels + # Fix the order for hierarchical labels to adjust classes with model outputs + if self._hierarchical: + label_list = get_hierarchical_label_list(self._hierarchical_info, label_list) + for i, (dataset_item, prediction_item, saliency_map) in enumerate(zip(dataset, predictions, saliency_maps)): + item_labels = self._get_item_labels(prediction_item, pos_thr=0.5) + add_saliency_maps_to_dataset_item( + dataset_item=dataset_item, + saliency_map=saliency_map, + model=self._task_environment.model, + labels=label_list, + predicted_scored_labels=item_labels, + explain_predicted_classes=explain_predicted_classes, + process_saliency_maps=process_saliency_maps, + ) + update_progress_callback(int(i / dataset_size * 100)) + + def save_model(self, output_model: ModelEntity): + """Save best model weights in ClassificationTrainTask.""" + if is_multigpu_child_process(): + return + + logger.info("called save_model") + buffer = io.BytesIO() + hyperparams_str = ids_to_strings(cfg_helper.convert(self._hyperparams, dict, enum_to_str=True)) + labels = {label.name: label.color.rgb_tuple for label in self._labels} + model_ckpt = torch.load(self._model_ckpt) + modelinfo = { + "model": model_ckpt, + "config": hyperparams_str, + "labels": labels, + "input_size": self._input_size, + "VERSION": 1, + } + + torch.save(modelinfo, buffer) + output_model.set_data("weights.pth", buffer.getvalue()) + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self._task_environment.label_schema), + ) + output_model.precision = self._precision + + def _generate_training_metrics(self, learning_curves): # pylint: disable=arguments-renamed + """Parses the classification logs to get metrics from the latest training run. + + :return output List[MetricsGroup] + """ + output: List[MetricsGroup] = [] + + if self._multilabel: + metric_key = "val/accuracy-mlc" + elif self._hierarchical: + metric_key = "val/MHAcc" + else: + metric_key = "val/accuracy (%)" + + # Learning curves + best_acc = -1 + if learning_curves is None: + return output + + for key, curve in learning_curves.items(): + metric_curve = CurveMetric(xs=curve.x, ys=curve.y, name=key) + if key == metric_key: + best_acc = max(curve.y) + visualization_info = LineChartInfo(name=key, x_axis_label="Timestamp", y_axis_label=key) + output.append(LineMetricsGroup(metrics=[metric_curve], visualization_info=visualization_info)) + + return output, best_acc + + @abstractmethod + def _train_model(self, dataset: DatasetEntity): + """Train model and return the results.""" + raise NotImplementedError + + @abstractmethod + def _export_model(self, precision: ModelPrecision, export_format: ExportType, dump_features: bool): + """Export model and return the results.""" + raise NotImplementedError + + @abstractmethod + def _explain_model(self, dataset: DatasetEntity, explain_parameters: Optional[ExplainParameters]): + """Explain model and return the results.""" + raise NotImplementedError + + @abstractmethod + def _infer_model( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ): + """Get inference results from dataset.""" + raise NotImplementedError diff --git a/src/otx/algorithms/classification/tools/__init__.py b/src/otx/algorithms/classification/tools/__init__.py new file mode 100644 index 00000000000..759d3934faf --- /dev/null +++ b/src/otx/algorithms/classification/tools/__init__.py @@ -0,0 +1,15 @@ +"""Collection of tools to run classification training extension.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/classification/tools/classification_sample.py b/src/otx/algorithms/classification/tools/classification_sample.py new file mode 100644 index 00000000000..de9283dfbed --- /dev/null +++ b/src/otx/algorithms/classification/tools/classification_sample.py @@ -0,0 +1,369 @@ +"""Sample code of otx training for classification.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=too-many-locals, invalid-name, too-many-statements + +import argparse +import random +import sys + +import numpy as np +import torch + +from otx.algorithms.common.utils import get_task_class +from otx.api.configuration.helper import create +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import Domain +from otx.api.entities.label_schema import ( + LabelEntity, + LabelGroup, + LabelGroupType, + LabelSchemaEntity, +) +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.utils.logger import get_logger + +SEED = 5 +random.seed(SEED) +np.random.seed(SEED) +torch.manual_seed(SEED) +torch.cuda.manual_seed_all(SEED) +torch.backends.cudnn.deterministic = True +torch.backends.cudnn.benchmark = False + +logger = get_logger() + + +def parse_args(): + """Parse function for getting model template & check export.""" + parser = argparse.ArgumentParser(description="Sample showcasing the new API") + parser.add_argument("template_file_path", help="path to template file") + parser.add_argument("--export", action="store_true") + parser.add_argument("--multilabel", action="store_true") + parser.add_argument("--hierarchical", action="store_true") + + return parser.parse_args() + + +def load_test_dataset(data_type, args): + """Load test dataset.""" + import PIL + from PIL import ImageDraw + + from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, + ) + from otx.api.entities.dataset_item import DatasetItemEntity + from otx.api.entities.image import Image + from otx.api.entities.scored_label import ScoredLabel + from otx.api.entities.shapes.rectangle import Rectangle + + def gen_image(resolution, shape=None): + image = PIL.Image.new("RGB", resolution, (255, 255, 255)) + draw = ImageDraw.Draw(image) + h, w = image.size + shape = shape.split("+") if "+" in shape else [shape] + for s in shape: + if s == "rectangle": + draw.rectangle((h * 0.1, w * 0.1, h * 0.4, w * 0.4), fill=(0, 192, 192), outline=(0, 0, 0)) + if s == "triangle": + draw.polygon( + ((h * 0.5, w * 0.25), (h, w * 0.25), (h * 0.8, w * 0.5)), fill=(255, 255, 0), outline=(0, 0, 0) + ) + if s == "pieslice": + draw.pieslice( + ((h * 0.1, w * 0.5), (h * 0.5, w * 0.9)), start=50, end=250, fill=(0, 255, 0), outline=(0, 0, 0) + ) + if s == "circle": + draw.ellipse((h * 0.5, w * 0.5, h * 0.9, w * 0.9), fill="blue", outline="blue") + if s == "text": + draw.text((0, 0), "Intel", fill="blue", align="center") + return np.array(image), shape + + datas = [ + gen_image((32, 32), shape="rectangle"), + gen_image((32, 32), shape="triangle"), + gen_image((32, 32), shape="rectangle+triangle"), # for multilabel (old) + gen_image((32, 32), shape="pieslice"), + gen_image((32, 32), shape="pieslice+rectangle"), + gen_image((32, 32), shape="pieslice+triangle"), + gen_image((32, 32), shape="pieslice+rectangle+triangle"), # for multilabel (new) + gen_image((32, 32), shape="circle"), + gen_image((32, 32), shape="circle+text"), # for hierarchical (new) + ] + + labels = { + "rectangle": LabelEntity(name="rectangle", domain=Domain.CLASSIFICATION, id=0), + "triangle": LabelEntity(name="triangle", domain=Domain.CLASSIFICATION, id=1), + "pieslice": LabelEntity(name="pieslice", domain=Domain.CLASSIFICATION, id=2), + "circle": LabelEntity(name="circle", domain=Domain.CLASSIFICATION, id=3), + "text": LabelEntity(name="text", domain=Domain.CLASSIFICATION, id=4), + } + + def get_image(i, subset, ignored_labels=None): + image, shape = datas[i] + lbl = [ScoredLabel(label=labels[s], probability=1.0) for s in shape] + return DatasetItemEntity( + media=Image(data=image), + annotation_scene=AnnotationSceneEntity( + annotations=[ + Annotation( + Rectangle(x1=0.0, y1=0.0, x2=1.0, y2=1.0), + labels=lbl, + ) + ], + kind=AnnotationSceneKind.ANNOTATION, + ), + subset=subset, + ignored_labels=ignored_labels, + ) + + def gen_old_new_dataset(multilabel=False, hierarchical=False): + old_train, old_val, new_train, new_val = [], [], [], [] + old_repeat = 8 + new_repeat = 4 + if multilabel: + old_img_idx = [0, 1, 2] + new_img_idx = [0, 1, 2, 3, 4, 5, 6] + ignored_labels = [labels["pieslice"]] + elif hierarchical: + old_img_idx = [0, 1, 2, 3, 8] + new_img_idx = [0, 1, 2, 3, 8, -1] + ignored_labels = [labels["text"]] + else: + old_img_idx = [0, 1] + new_img_idx = [0, 1, 3] + + for _ in range(old_repeat): + for idx in old_img_idx: + old_train.append(get_image(idx, Subset.TRAINING)) + old_val.append(get_image(idx, Subset.VALIDATION)) + for _ in range(new_repeat): + for idx in new_img_idx: + if multilabel: + new_train.append(get_image(idx, Subset.TRAINING, ignored_labels=ignored_labels)) + elif hierarchical: + new_train.append(get_image(idx, Subset.TRAINING, ignored_labels=ignored_labels)) + else: + new_train.append(get_image(idx, Subset.TRAINING)) + new_val.append(get_image(idx, Subset.VALIDATION)) + + return old_train + old_val, new_train + new_val + + old, new = gen_old_new_dataset(args.multilabel, args.hierarchical) + + if not args.hierarchical: + labels = [labels["rectangle"], labels["triangle"], labels["pieslice"]] + else: + labels = list(labels.values()) + + if data_type == "old": + return DatasetEntity(old), labels[:-1] + return DatasetEntity(old + new), labels + + +def get_label_schema(labels, multilabel=False, hierarchical=False): + """Get label schema.""" + label_schema = LabelSchemaEntity() + + emptylabel = LabelEntity(id=-1, name="Empty label", is_empty=True, domain=Domain.CLASSIFICATION) + empty_group = LabelGroup(name="empty", labels=[emptylabel], group_type=LabelGroupType.EMPTY_LABEL) + + if multilabel: + for label in labels: + label_schema.add_group(LabelGroup(name=label.name, labels=[label], group_type=LabelGroupType.EXCLUSIVE)) + label_schema.add_group(empty_group) + elif hierarchical: + single_label_classes = ["pieslice", "circle"] + multi_label_classes = ["rectangle", "triangle", "text"] + + single_labels = [label for label in labels if label.name in single_label_classes] + single_label_group = LabelGroup(name="labels", labels=single_labels, group_type=LabelGroupType.EXCLUSIVE) + label_schema.add_group(single_label_group) + + for label in labels: + if label.name in multi_label_classes: + label_schema.add_group( + LabelGroup( + name=f"{label.name}____{label.name}_group", labels=[label], group_type=LabelGroupType.EXCLUSIVE + ) + ) + else: + main_group = LabelGroup(name="labels", labels=labels, group_type=LabelGroupType.EXCLUSIVE) + label_schema.add_group(main_group) + + return label_schema + + +def validate(task, validation_dataset, model): + """Validate.""" + print("Get predictions on the validation set") + predicted_validation_dataset = task.infer( + validation_dataset.with_empty_annotations(), InferenceParameters(is_evaluation=True) + ) + resultset = ResultSetEntity( + model=model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + print("Estimate quality on validation set") + task.evaluate(resultset) + print(str(resultset.performance)) + + +def main(args): + """Main of Classification Sample Test.""" + + logger.info("Train initial model with OLD dataset") + dataset, labels_list = load_test_dataset("old", args) + labels_schema = get_label_schema(labels_list, multilabel=args.multilabel, hierarchical=args.hierarchical) + + logger.info(f"Train dataset: {len(dataset.get_subset(Subset.TRAINING))} items") + logger.info(f"Validation dataset: {len(dataset.get_subset(Subset.VALIDATION))} items") + + logger.info("Load model template") + model_template = parse_model_template(args.template_file_path) + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 10 + params.learning_parameters.learning_rate = 0.03 + params.learning_parameters.learning_rate_warmup_iters = 4 + params.learning_parameters.batch_size = 16 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=None, hyper_parameters=params, label_schema=labels_schema, model_template=model_template + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + initial_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, initial_model) + + logger.info("Class-incremental learning with OLD + NEW dataset") + dataset, labels_list = load_test_dataset("new") + labels_schema = get_label_schema(labels_list, multilabel=args.multilabel, hierarchical=args.hierarchical) + + logger.info(f"Train dataset: {len(dataset.get_subset(Subset.TRAINING))} items") + logger.info(f"Validation dataset: {len(dataset.get_subset(Subset.VALIDATION))} items") + + logger.info("Load model template") + model_template = parse_model_template(args.template_file_path) + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 10 + params.learning_parameters.learning_rate = 0.03 + params.learning_parameters.learning_rate_warmup_iters = 4 + params.learning_parameters.batch_size = 16 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=initial_model, hyper_parameters=params, label_schema=labels_schema, model_template=model_template + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + output_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, output_model) + + logger.info("Get predictions on the validation set") + validation_dataset = dataset.get_subset(Subset.VALIDATION) + predicted_validation_dataset = task.infer( + validation_dataset.with_empty_annotations(), InferenceParameters(is_evaluation=True) + ) + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + task.evaluate(resultset) + logger.info(str(resultset.performance)) + + if args.export or args.multilabel or args.hierarchical: + logger.info("Export model") + exported_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.export(ExportType.OPENVINO, exported_model) + + logger.info("Create OpenVINO Task") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + openvino_task_cls = get_task_class(openvino_task_impl_path) + openvino_task = openvino_task_cls(environment) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), InferenceParameters(is_evaluation=True) + ) + + resultset = ResultSetEntity( + model=exported_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + # POT test + logger.info("Run POT optimization") + optimized_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + openvino_task.optimize(OptimizationType.POT, dataset, optimized_model, OptimizationParameters()) + logger.info("Run POT deploy") + openvino_task.deploy(optimized_model) + validate(task, validation_dataset, optimized_model) + + # NNCF test + task_impl_path = model_template.entrypoints.nncf + nncf_task_cls = get_task_class(task_impl_path) + + print("Create NNCF Task") + environment.model = output_model + task = nncf_task_cls(task_environment=environment) + + print("Optimize model by NNCF") + optimized_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.optimize(OptimizationType.NNCF, dataset, optimized_model, None) + validate(task, validation_dataset, output_model) + + +if __name__ == "__main__": + sys.exit(main(parse_args()) or 0) diff --git a/src/otx/algorithms/classification/utils/__init__.py b/src/otx/algorithms/classification/utils/__init__.py new file mode 100644 index 00000000000..536cd56bff7 --- /dev/null +++ b/src/otx/algorithms/classification/utils/__init__.py @@ -0,0 +1,21 @@ +"""OTX Algorithms - Utils.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .cls_utils import ( + get_cls_deploy_config, + get_cls_inferencer_configuration, + get_cls_model_api_configuration, + get_hierarchical_label_list, + get_multihead_class_info, +) + +__all__ = [ + "get_hierarchical_label_list", + "get_multihead_class_info", + "get_cls_inferencer_configuration", + "get_cls_deploy_config", + "get_cls_model_api_configuration", +] diff --git a/src/otx/algorithms/classification/utils/cls_utils.py b/src/otx/algorithms/classification/utils/cls_utils.py new file mode 100644 index 00000000000..67e3b1c5601 --- /dev/null +++ b/src/otx/algorithms/classification/utils/cls_utils.py @@ -0,0 +1,144 @@ +"""Collection of utils about labels in Classifation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=too-many-nested-blocks, invalid-name + +import json +from operator import itemgetter +from typing import Any, Dict + +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.serialization.label_mapper import LabelSchemaMapper + + +def get_multihead_class_info(label_schema: LabelSchemaEntity): # pylint: disable=too-many-locals + """Get multihead info by label schema.""" + all_groups = label_schema.get_groups(include_empty=False) + all_groups_str = [] + for g in all_groups: + group_labels_str = [lbl.name for lbl in g.labels] + all_groups_str.append(group_labels_str) + + single_label_groups = [g for g in all_groups_str if len(g) == 1] + exclusive_groups = [sorted(g) for g in all_groups_str if len(g) > 1] + single_label_groups.sort(key=itemgetter(0)) + exclusive_groups.sort(key=itemgetter(0)) + class_to_idx = {} + head_idx_to_logits_range = {} + num_single_label_classes = 0 + last_logits_pos = 0 + for i, group in enumerate(exclusive_groups): + head_idx_to_logits_range[str(i)] = (last_logits_pos, last_logits_pos + len(group)) + last_logits_pos += len(group) + for j, c in enumerate(group): + class_to_idx[c] = (i, j) # group idx and idx inside group + num_single_label_classes += 1 + + # other labels are in multilabel group + for j, group in enumerate(single_label_groups): + class_to_idx[group[0]] = (len(exclusive_groups), j) + + all_labels = label_schema.get_labels(include_empty=False) + label_to_idx = {lbl.name: i for i, lbl in enumerate(all_labels)} + + mixed_cls_heads_info = { + "num_multiclass_heads": len(exclusive_groups), + "num_multilabel_classes": len(single_label_groups), + "head_idx_to_logits_range": head_idx_to_logits_range, + "num_single_label_classes": num_single_label_classes, + "class_to_group_idx": class_to_idx, + "all_groups": exclusive_groups + single_label_groups, + "label_to_idx": label_to_idx, + "empty_multiclass_head_indices": [], + } + return mixed_cls_heads_info + + +def get_cls_inferencer_configuration(label_schema: LabelSchemaEntity): + """Get classification inferencer config by label schema.""" + multilabel = len(label_schema.get_groups(False)) > 1 and len(label_schema.get_groups(False)) == len( + label_schema.get_labels(include_empty=False) + ) + hierarchical = not multilabel and len(label_schema.get_groups(False)) > 1 + multihead_class_info = {} + if hierarchical: + multihead_class_info = get_multihead_class_info(label_schema) + return { + "multilabel": multilabel, + "hierarchical": hierarchical, + "multihead_class_info": multihead_class_info, + "confidence_threshold": 0.5, + } + + +def get_cls_deploy_config(label_schema: LabelSchemaEntity, inference_config: Dict[str, Any]): + """Get classification deploy config.""" + parameters = {} # type: Dict[Any, Any] + parameters["type_of_model"] = "Classification" + parameters["converter_type"] = "CLASSIFICATION" + parameters["model_parameters"] = inference_config + parameters["model_parameters"]["labels"] = LabelSchemaMapper.forward(label_schema) + return parameters + + +def get_cls_model_api_configuration(label_schema: LabelSchemaEntity, inference_config: Dict[str, Any]): + """Get ModelAPI config.""" + mapi_config = {} + mapi_config[("model_info", "model_type")] = "Classification" + mapi_config[("model_info", "task_type")] = "classification" + mapi_config[("model_info", "confidence_threshold")] = str(inference_config["confidence_threshold"]) + mapi_config[("model_info", "multilabel")] = str(inference_config["multilabel"]) + mapi_config[("model_info", "hierarchical")] = str(inference_config["hierarchical"]) + mapi_config[("model_info", "output_raw_scores")] = str(True) + + all_labels = "" + all_label_ids = "" + for lbl in label_schema.get_labels(include_empty=False): + all_labels += lbl.name.replace(" ", "_") + " " + all_label_ids += f"{lbl.id_} " + + mapi_config[("model_info", "labels")] = all_labels.strip() + mapi_config[("model_info", "label_ids")] = all_label_ids.strip() + + hierarchical_config = {} + hierarchical_config["cls_heads_info"] = get_multihead_class_info(label_schema) + hierarchical_config["label_tree_edges"] = [] + for edge in label_schema.label_tree.edges: # (child, parent) + hierarchical_config["label_tree_edges"].append((edge[0].name, edge[1].name)) + + mapi_config[("model_info", "hierarchical_config")] = json.dumps(hierarchical_config) + return mapi_config + + +def get_hierarchical_label_list(hierarchical_info, labels): + """Return hierarchical labels list which is adjusted to model outputs classes.""" + hierarchical_labels = [] + for head_idx in range(hierarchical_info["num_multiclass_heads"]): + logits_begin, logits_end = hierarchical_info["head_idx_to_logits_range"][str(head_idx)] + for logit in range(0, logits_end - logits_begin): + label_str = hierarchical_info["all_groups"][head_idx][logit] + label_idx = hierarchical_info["label_to_idx"][label_str] + hierarchical_labels.append(labels[label_idx]) + + if hierarchical_info["num_multilabel_classes"]: + logits_begin = hierarchical_info["num_single_label_classes"] + logits_end = len(labels) + for logit_idx, logit in enumerate(range(0, logits_end - logits_begin)): + label_str_idx = hierarchical_info["num_multiclass_heads"] + logit_idx + label_str = hierarchical_info["all_groups"][label_str_idx][0] + label_idx = hierarchical_info["label_to_idx"][label_str] + hierarchical_labels.append(labels[label_idx]) + return hierarchical_labels diff --git a/src/otx/algorithms/classification/utils/convert_coco_to_multilabel.py b/src/otx/algorithms/classification/utils/convert_coco_to_multilabel.py new file mode 100644 index 00000000000..a9a8e9dc567 --- /dev/null +++ b/src/otx/algorithms/classification/utils/convert_coco_to_multilabel.py @@ -0,0 +1,109 @@ +"""Convert dataset: Public dataset (COCO) --> Multi-label dataset (Datumaro format). + +this script contains a lot of hard-coding to create .json file that Datumaro can consume. +""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import argparse +import json +from typing import Any, Dict, List + +from otx.algorithms.detection.utils.data import CocoDataset + +multilabel_ann_format: Dict[str, Any] = { + "info": {}, + "categories": { + "label": { + "label_groups": [], + "labels": [], + "attributes": [], + } + }, + "items": [], +} + + +def coco_to_datumaro_multilabel(ann_file_path: str, data_root_dir: str, output: str, test_mode: bool = False): + """Convert coco dataset to datumaro multi-label format. + + Args: + ann_file_path (str): The path of annotation file (COCO) + data_root_dir (str): The path of images folder (COCO) + output (str): Destination path of converted data (CVAT multi-label format) + test_mode (bool): Omit filtering irrelevant images during COCO dataset initialization for testing purposes. + """ + + # Prepare COCO dataset to load annotations + coco_dataset = CocoDataset( + ann_file=ann_file_path, + data_root=data_root_dir, + classes=None, + test_mode=test_mode, + with_mask=False, + ) + + # Fill the categories part + # For the multi-label classification, + # Datumaro will make label_groups and labels + overall_classes: List = coco_dataset.get_classes() + for class_name in overall_classes: + multilabel_ann_format["categories"]["label"]["label_groups"].append( + {"name": f"___{str(class_name)}", "group_type": "exclusive", "labels": [str(class_name)]} + ) + + multilabel_ann_format["categories"]["label"]["labels"].append( + {"name": class_name, "parent": "", "attributes": []} + ) + + # Fill the items part + for item in coco_dataset: + filename = item["img_info"]["filename"] + file_id = filename.split(".")[0] + labels = item["gt_labels"] + + annotations = [] + for i, label in enumerate(labels): + annotations.append({"id": int(i), "type": "label", "group": 0, "label_id": int(label)}) + + multilabel_ann_format["items"].append( + {"id": str(file_id), "annotations": annotations, "image": {"path": str(filename)}} + ) + print(f"Saving logfile to: {output}") + with open(output, "w", encoding="utf-8") as out_file: + json.dump(multilabel_ann_format, out_file) + + +def parse_args(): + """Parses command line arguments.""" + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--ann_file_path", required=True, type=str) + parser.add_argument("--data_root_dir", required=True, type=str) + parser.add_argument("--output", required=True, type=str) + parser.add_argument("--data_format", type=str, default="coco") + return parser.parse_args() + + +def main(): + """Main function.""" + args = parse_args() + if args.data_format == "coco": + coco_to_datumaro_multilabel(args.ann_file_path, args.data_root_dir, args.output) + else: + raise ValueError(f"Unknown data format {args.data_format}.This script only support `coco`.") + + +if __name__ == "__main__": + main() diff --git a/src/otx/algorithms/common/__init__.py b/src/otx/algorithms/common/__init__.py new file mode 100644 index 00000000000..22fff849261 --- /dev/null +++ b/src/otx/algorithms/common/__init__.py @@ -0,0 +1,4 @@ +"""OTX Algorithms - Common.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/common/adapters/__init__.py b/src/otx/algorithms/common/adapters/__init__.py new file mode 100644 index 00000000000..40dddbdffc2 --- /dev/null +++ b/src/otx/algorithms/common/adapters/__init__.py @@ -0,0 +1,15 @@ +"""Adapters for OTX Common Algorithms.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/common/adapters/mmcv/__init__.py b/src/otx/algorithms/common/adapters/mmcv/__init__.py new file mode 100644 index 00000000000..0890a669fdb --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/__init__.py @@ -0,0 +1,66 @@ +"""Adapters for mmcv support.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .hooks import ( + CancelTrainingHook, + CheckpointHookWithValResults, + CustomEvalHook, + EarlyStoppingHook, + EMAMomentumUpdateHook, + EnsureCorrectBestCheckpointHook, + Fp16SAMOptimizerHook, + IBLossHook, + LossDynamicsTrackingHook, + MemCacheHook, + NoBiasDecayHook, + OTXLoggerHook, + OTXProgressHook, + ReduceLROnPlateauLrUpdaterHook, + SAMOptimizerHook, + SemiSLClsHook, + StopLossNanTrainingHook, + TwoCropTransformHook, +) +from .nncf.hooks import CompressionHook +from .nncf.runners import AccuracyAwareRunner +from .ops import multi_scale_deformable_attn_pytorch +from .runner import EpochRunnerWithCancel, IterBasedRunnerWithCancel + +__all__ = [ + "EpochRunnerWithCancel", + "IterBasedRunnerWithCancel", + "CheckpointHookWithValResults", + "CustomEvalHook", + "Fp16SAMOptimizerHook", + "IBLossHook", + "SAMOptimizerHook", + "NoBiasDecayHook", + "SemiSLClsHook", + "CancelTrainingHook", + "OTXLoggerHook", + "OTXProgressHook", + "EarlyStoppingHook", + "ReduceLROnPlateauLrUpdaterHook", + "EnsureCorrectBestCheckpointHook", + "StopLossNanTrainingHook", + "EMAMomentumUpdateHook", + "CompressionHook", + "AccuracyAwareRunner", + "TwoCropTransformHook", + "MemCacheHook", + "LossDynamicsTrackingHook", + "multi_scale_deformable_attn_pytorch", +] diff --git a/src/otx/algorithms/common/adapters/mmcv/clsincr_mixin.py b/src/otx/algorithms/common/adapters/mmcv/clsincr_mixin.py new file mode 100644 index 00000000000..02251a8f200 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/clsincr_mixin.py @@ -0,0 +1,51 @@ +"""Class Incremental Learning configuration mixin.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import List + +from mmcv.utils import ConfigDict + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import update_or_add_custom_hook + + +class IncrConfigurerMixin: + """Patch config to support incremental learning for object detection.""" + + org_model_classes: List = [] + model_classes: List = [] + + def configure_task(self, cfg, **kwargs): + """Patch config to support incremental learning.""" + super().configure_task(cfg, **kwargs) + if "task_adapt" in cfg and self.task_adapt_type == "default_task_adapt": + self.configure_task_adapt_hook(cfg) + + def configure_task_adapt_hook(self, cfg): + """Add TaskAdaptHook for sampler.""" + sampler_flag = self.is_incremental() + + update_or_add_custom_hook( + cfg, + ConfigDict( + type="TaskAdaptHook", + src_classes=self.org_model_classes, + dst_classes=self.model_classes, + model_type=cfg.model.type, + sampler_flag=sampler_flag, + sampler_type=self.get_sampler_type(cfg), + efficient_mode=cfg["task_adapt"].get("efficient_mode", False), + priority="NORMAL", + ), + ) + + def is_incremental(self) -> bool: + """Return whether current model classes is increased from original model classes.""" + return len(set(self.org_model_classes) & set(self.model_classes)) > 0 and set(self.org_model_classes) != set( + self.model_classes + ) + + def get_sampler_type(self, cfg) -> str: + """Return sampler type.""" + return "cls_incr" diff --git a/src/otx/algorithms/common/adapters/mmcv/configs/__init__.py b/src/otx/algorithms/common/adapters/mmcv/configs/__init__.py new file mode 100644 index 00000000000..75a52e73ff3 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configs/__init__.py @@ -0,0 +1,15 @@ +"""MMCV model configs for OTX Common Algorithms.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/common/adapters/mmcv/configs/backbones/__init__.py b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/__init__.py new file mode 100644 index 00000000000..7055622ea92 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/__init__.py @@ -0,0 +1,15 @@ +"""MMCV model backbones for OTX Common Algorithms.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/common/adapters/mmcv/configs/backbones/efficientnet_b2b.yaml b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/efficientnet_b2b.yaml new file mode 100644 index 00000000000..a0c69954cd3 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/efficientnet_b2b.yaml @@ -0,0 +1,11 @@ +model: + backbone: + type: efficientnet_b2b + out_indices: !!python/tuple [2, 3, 4, 5] + frozen_stages: -1 + pretrained: True + activation_cfg: + type: torch_swish + norm_cfg: + type: BN + requires_grad: True diff --git a/src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_18.py b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_18.py new file mode 100644 index 00000000000..50d332d0797 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_18.py @@ -0,0 +1,59 @@ +"""Backbone config of OCR-Lite-HRnet-18.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +model = dict( + backbone=dict( + type="LiteHRNet", + norm_cfg=dict(type="BN", requires_grad=True), + norm_eval=False, + extra=dict( + stem=dict( + stem_channels=32, + out_channels=32, + expand_ratio=1, + strides=(2, 2), + extra_stride=False, + input_norm=False, + ), + num_stages=3, + stages_spec=dict( + num_modules=(2, 4, 2), + num_branches=(2, 3, 4), + num_blocks=(2, 2, 2), + module_type=("LITE", "LITE", "LITE"), + with_fuse=(True, True, True), + reduce_ratios=(8, 8, 8), + num_channels=( + (40, 80), + (40, 80, 160), + (40, 80, 160, 320), + ), + ), + out_modules=dict( + conv=dict(enable=False, channels=320), + position_att=dict( + enable=False, + key_channels=128, + value_channels=320, + psp_size=(1, 3, 6, 8), + ), + local_att=dict(enable=False), + ), + out_aggregator=dict(enable=False), + add_input=False, + ), + ), +) diff --git a/src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_s.py b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_s.py new file mode 100644 index 00000000000..95ae51667ad --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_s.py @@ -0,0 +1,60 @@ +"""Backbone config of OCR-Lite-HRnet-s.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +model = dict( + backbone=dict( + type="LiteHRNet", + norm_cfg=dict(type="BN", requires_grad=True), + norm_eval=False, + extra=dict( + stem=dict( + stem_channels=32, + out_channels=32, + expand_ratio=1, + strides=(2, 2), + extra_stride=True, + input_norm=False, + ), + num_stages=2, + stages_spec=dict( + neighbour_weighting=False, + weighting_module_version="v1", + num_modules=(4, 4), + num_branches=(2, 3), + num_blocks=(2, 2), + module_type=("LITE", "LITE"), + with_fuse=(True, True), + reduce_ratios=(8, 8), + num_channels=( + (60, 120), + (60, 120, 240), + ), + ), + out_modules=dict( + conv=dict(enable=False, channels=160), + position_att=dict( + enable=False, + key_channels=64, + value_channels=240, + psp_size=(1, 3, 6, 8), + ), + local_att=dict(enable=False), + ), + out_aggregator=dict(enable=False), + add_input=False, + ), + ), +) diff --git a/src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_x.py b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_x.py new file mode 100644 index 00000000000..7c08a484a32 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/lite_hrnet_x.py @@ -0,0 +1,61 @@ +"""Backbone config of OCR-Lite-HRnet-x.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +model = dict( + backbone=dict( + type="LiteHRNet", + norm_cfg=dict(type="BN", requires_grad=True), + norm_eval=False, + extra=dict( + stem=dict( + stem_channels=60, + out_channels=60, + expand_ratio=1, + strides=(2, 1), + extra_stride=False, + input_norm=False, + ), + num_stages=4, + stages_spec=dict( + weighting_module_version="v1", + num_modules=(2, 4, 4, 2), + num_branches=(2, 3, 4, 5), + num_blocks=(2, 2, 2, 2), + module_type=("LITE", "LITE", "LITE", "LITE"), + with_fuse=(True, True, True, True), + reduce_ratios=(2, 4, 8, 8), + num_channels=( + (18, 60), + (18, 60, 80), + (18, 60, 80, 160), + (18, 60, 80, 160, 320), + ), + ), + out_modules=dict( + conv=dict(enable=False, channels=320), + position_att=dict( + enable=False, + key_channels=128, + value_channels=320, + psp_size=(1, 3, 6, 8), + ), + local_att=dict(enable=False), + ), + out_aggregator=dict(enable=False), + add_input=False, + ), + ), +) diff --git a/src/otx/algorithms/common/adapters/mmcv/configs/backbones/mobilenet_v2_w1.yaml b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/mobilenet_v2_w1.yaml new file mode 100644 index 00000000000..90d358813c1 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/mobilenet_v2_w1.yaml @@ -0,0 +1,7 @@ +model: + backbone: + type: mobilenetv2_w1 + out_indices: !!python/tuple [4, 5] + frozen_stages: -1 + norm_eval: false + pretrained: true diff --git a/src/otx/algorithms/common/adapters/mmcv/configs/backbones/resnet18.yaml b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/resnet18.yaml new file mode 100644 index 00000000000..4a3a3ffa123 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/resnet18.yaml @@ -0,0 +1,10 @@ +model: + pretrained: torchvision://resnet18 + backbone: + type: ResNet + depth: 18 + num_stages: 4 + out_indices: !!python/tuple [3] + style: pytorch + # head: + # in_channels: 512 diff --git a/src/otx/algorithms/common/adapters/mmcv/configs/backbones/resnet50.yaml b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/resnet50.yaml new file mode 100644 index 00000000000..8a988f4568d --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/resnet50.yaml @@ -0,0 +1,13 @@ +_base_: "./resnet18.yaml" + +model: + pretrained: torchvision://resnet50 + backbone: + depth: 50 + out_indices: !!python/tuple [0, 1, 2, 3] + frozen_stages: 1 + norm_cfg: + type: BN + requires_grad: true + norm_eval: true + style: pytorch diff --git a/src/otx/algorithms/common/adapters/mmcv/configs/backbones/segnext.py b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/segnext.py new file mode 100644 index 00000000000..5b0714aebec --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configs/backbones/segnext.py @@ -0,0 +1,36 @@ +"""Backbone config of SegNext models.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +model = dict( + type="EncoderDecoder", + backbone=dict( + type="MSCAN", + embed_dims=[32, 64, 160, 256], + mlp_ratios=[8, 8, 4, 4], + drop_rate=0.0, + drop_path_rate=0.1, + depths=[3, 3, 5, 2], + attention_kernel_sizes=[5, [1, 7], [1, 11], [1, 21]], + attention_kernel_paddings=[2, [0, 3], [0, 5], [0, 10]], + act_cfg=dict(type="GELU"), + norm_cfg=dict(type="BN", requires_grad=True), + ), + # model training and testing settings + train_cfg=dict(), + test_cfg=dict(mode="whole"), +) + +load_from = None diff --git a/src/otx/algorithms/common/adapters/mmcv/configurer.py b/src/otx/algorithms/common/adapters/mmcv/configurer.py new file mode 100644 index 00000000000..0d7b427e114 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/configurer.py @@ -0,0 +1,587 @@ +"""Implementation of class for default patches of mmcv configs.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import json +import os +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np +import torch +from mmcv.runner import CheckpointLoader +from mmcv.utils import Config, ConfigDict +from torch import distributed as dist + +from otx.algorithms.common.adapters.mmcv.utils import ( + patch_adaptive_interval_training, + patch_early_stopping, + patch_persistent_workers, + remove_from_configs_by_type, +) +from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + InputSizeManager, + patch_color_conversion, + patch_from_hyperparams, + recursively_update_cfg, + update_or_add_custom_hook, +) +from otx.algorithms.common.tasks.base_task import OnHookInitialized +from otx.algorithms.common.utils import ( + UncopiableDefaultDict, + append_dist_rank_suffix, + is_hpu_available, + is_xpu_available, +) +from otx.algorithms.common.utils.data import compute_robust_dataset_statistics +from otx.api.usecases.reporting.time_monitor_callback import TimeMonitorCallback +from otx.core.data import caching +from otx.utils.logger import get_logger + +logger = get_logger() + + +class BaseConfigurer: + """Base configurer class for mmcv configs.""" + + def __init__( + self, + task: str, + training: bool, + export: bool, + override_configs: Dict[str, str], + on_hook_initialized: OnHookInitialized, + time_monitor: Optional[TimeMonitorCallback], + learning_curves: UncopiableDefaultDict, + ): + self.task_adapt_type: Optional[str] = None + self.task_adapt_op: str = "REPLACE" + self.org_model_classes: List[str] = [] + self.model_classes: List[str] = [] + self.data_classes: List[str] = [] + self.task: str = task + self.training: bool = training + self.export: bool = export + self.ema_hooks: List[str] = ["EMAHook", "CustomModelEMAHook"] # EMA hooks supporting resume + self.override_configs: Dict[str, str] = override_configs + self.on_hook_initialized: OnHookInitialized = on_hook_initialized + self.time_monitor: Optional[TimeMonitorCallback] = time_monitor + self.learning_curves: UncopiableDefaultDict = learning_curves + + def configure( + self, + cfg: Config, + data_pipeline_path: str, + hyperparams_from_otx: ConfigDict, + model_ckpt_path: str, + data_cfg: Config, + ir_options: Optional[Config] = None, + data_classes: Optional[List[str]] = None, + model_classes: Optional[List[str]] = None, + input_size: Optional[Tuple[int, int]] = None, + **kwargs: Dict[Any, Any], + ) -> Config: + """Create MMCV-consumable config from given inputs.""" + logger.info(f"configure!: training={self.training}") + + cfg.model_task = cfg.model.pop("task", self.task) + if cfg.model_task != self.task: + raise ValueError(f"Given cfg ({cfg.filename}) is not supported by {self.task} recipe") + + self.merge_configs(cfg, data_cfg, data_pipeline_path, hyperparams_from_otx, **kwargs) + + self.configure_ckpt(cfg, model_ckpt_path) + self.configure_env(cfg) + self.configure_data_pipeline(cfg, input_size, model_ckpt_path, **kwargs) + self.configure_recipe(cfg, **kwargs) + self.configure_model(cfg, data_classes, model_classes, ir_options, **kwargs) + self.configure_hooks( + cfg, + ) + self.configure_compat_cfg(cfg) + return cfg + + def merge_configs(self, cfg, data_cfg, data_pipeline_path, hyperparams_from_otx, **kwargs): + """Merge model cfg, data_pipeline cfg, data_cfg, and hyperparams from otx cli.""" + + logger.debug("merge_configs()") + if os.path.isfile(data_pipeline_path): + data_pipeline_cfg = Config.fromfile(data_pipeline_path) + cfg.merge_from_dict(data_pipeline_cfg) + else: + raise FileNotFoundError(f"data_pipeline: {data_pipeline_path} not founded") + + self.override_from_hyperparams(cfg, hyperparams_from_otx, **kwargs) + + if data_cfg: + for subset in data_cfg.data: + if subset in cfg.data: + src_data_cfg = self.get_subset_data_cfg(cfg, subset) + new_data_cfg = self.get_subset_data_cfg(data_cfg, subset) + for key in new_data_cfg: + src_data_cfg[key] = new_data_cfg[key] + else: + raise ValueError(f"{subset} of data_cfg is not in cfg") + + def override_from_hyperparams(self, config, hyperparams, **kwargs): + """Override config using hyperparams from OTX CLI.""" + if not self.export: + patch_from_hyperparams(config, hyperparams, **kwargs) + + def configure_ckpt(self, cfg, model_ckpt_path): + """Patch checkpoint path for pretrained weight. + + Replace cfg.load_from to model_ckpt_path + Replace cfg.load_from to pretrained + Replace cfg.resume_from to cfg.load_from + """ + if model_ckpt_path: + cfg.load_from = self.get_model_ckpt(model_ckpt_path) + if cfg.get("resume", False): + cfg.resume_from = cfg.load_from + for hook in cfg.custom_hooks: + if hook.type in self.ema_hooks: + hook.resume_from = cfg.resume_from + if cfg.get("load_from", None) and cfg.model.backbone.get("pretrained", None): + cfg.model.backbone.pretrained = None + + @staticmethod + def get_model_ckpt(ckpt_path, new_path=None): + """Get pytorch model weights.""" + ckpt = CheckpointLoader.load_checkpoint(ckpt_path, map_location="cpu") + if "model" in ckpt: + ckpt = ckpt["model"] + if not new_path: + new_path = ckpt_path[:-3] + "converted.pth" + new_path = append_dist_rank_suffix(new_path) + torch.save(ckpt, new_path) + return new_path + return ckpt_path + + def configure_env(self, cfg): + """Configuration for environment settings.""" + + patch_persistent_workers(cfg) + self.configure_device(cfg) + self.configure_samples_per_gpu(cfg) + + def configure_device(self, cfg): + """Setting device for training and inference.""" + cfg.distributed = False + if torch.distributed.is_initialized(): + cfg.gpu_ids = [int(os.environ["LOCAL_RANK"])] + if self.training: # TODO multi GPU is available only in training. Evaluation needs to be supported later. + cfg.distributed = True + self.configure_distributed(cfg) + elif "gpu_ids" not in cfg: + cfg.gpu_ids = range(1) + + # consider "cuda", "xpu", "hpu" and "cpu" device only + if is_hpu_available(): + cfg.device = "hpu" + elif torch.cuda.is_available(): + cfg.device = "cuda" + elif is_xpu_available(): + cfg.device = "xpu" + else: + cfg.device = "cpu" + cfg.gpu_ids = range(-1, 0) + + @staticmethod + def configure_distributed(cfg: Config) -> None: + """Patching for distributed training.""" + if hasattr(cfg, "dist_params") and cfg.dist_params.get("linear_scale_lr", False): + new_lr = dist.get_world_size() * cfg.optimizer.lr + logger.info( + f"enabled linear scaling rule to the learning rate. \ + changed LR from {cfg.optimizer.lr} to {new_lr}" + ) + cfg.optimizer.lr = new_lr + + def configure_samples_per_gpu( + self, + cfg: Config, + subsets: List[str] = ["train", "val", "test", "unlabeled"], + ): + """Settings samples_per_gpu for each dataloader. + + samples_per_gpu can be changed if it is larger than length of datset + """ + for subset in subsets: + if cfg.data.get(subset, None): + dataloader_cfg = cfg.data.get(f"{subset}_dataloader", ConfigDict()) + samples_per_gpu = dataloader_cfg.get("samples_per_gpu", cfg.data.get("samples_per_gpu", 1)) + + data_cfg = self.get_subset_data_cfg(cfg, subset) + if data_cfg.get("otx_dataset") is not None: + dataset_len = len(data_cfg.otx_dataset) + + if getattr(cfg, "distributed", False): + dataset_len = dataset_len // dist.get_world_size() + + # set batch size as a total dataset + # if it is smaller than total dataset + if dataset_len < samples_per_gpu: + dataloader_cfg.samples_per_gpu = dataset_len + logger.info(f"{subset}'s samples_per_gpu: {samples_per_gpu} --> {dataset_len}") + + # drop the last batch if the last batch size is 1 + # batch size of 1 is a runtime error for training batch normalization layer + if subset in ("train", "unlabeled") and dataset_len % samples_per_gpu == 1: + dataloader_cfg["drop_last"] = True + + cfg.data[f"{subset}_dataloader"] = dataloader_cfg + + def configure_data_pipeline(self, cfg, input_size, model_ckpt_path, **kwargs): + """Configuration data pipeline settings.""" + + patch_color_conversion(cfg) + self.configure_input_size(cfg, input_size, model_ckpt_path, self.training) + + def configure_recipe(self, cfg, **kwargs): + """Configuration training recipe settings.""" + + patch_adaptive_interval_training(cfg) + patch_early_stopping(cfg) + self.configure_fp16(cfg) + + @staticmethod + def configure_fp16(cfg: Config): + """Configure Fp16OptimizerHook and Fp16SAMOptimizerHook.""" + + fp16_config = cfg.pop("fp16", None) + # workaround to forward FP16 config to mmapi.train funcitons + cfg.fp16_ = fp16_config + + optim_type = cfg.optimizer_config.get("type", "OptimizerHook") + distributed = getattr(cfg, "distributed", False) + opts: Dict[str, Any] = {} + if fp16_config is not None: + if is_hpu_available(): + if optim_type == "SAMOptimizerHook": + # TODO (sungchul): consider SAM optimizer + logger.warning("SAMOptimizerHook is not supported on HPU. Changed to OptimizerHook.") + opts["type"] = "HPUOptimizerHook" + cfg.optimizer_config.update(opts) + elif is_xpu_available(): + if optim_type == "SAMOptimizerHook": + logger.warning("SAMOptimizerHook is not supported on XPU yet, changed to OptimizerHook.") + opts["type"] = "OptimizerHook" + cfg.optimizer_config.update(opts) + logger.warning("XPU doesn't support mixed precision training currently.") + elif torch.cuda.is_available(): + opts.update({"distributed": distributed, **fp16_config}) + if optim_type == "SAMOptimizerHook": + opts["type"] = "Fp16SAMOptimizerHook" + elif optim_type == "OptimizerHook": + opts["type"] = "Fp16OptimizerHook" + else: + # does not support optimizerhook type + # let mm library handle it + cfg.fp16 = fp16_config + opts = dict() + cfg.optimizer_config.update(opts) + else: + logger.info("Revert FP16 to FP32 on CPU device") + + elif is_hpu_available(): + if distributed: + opts["type"] = "HPUDistOptimizerHook" + else: + opts["type"] = "HPUOptimizerHook" + cfg.optimizer_config.update(opts) + + else: + logger.info("Revert FP16 to FP32 on CPU device") + + def configure_model(self, cfg, data_classes, model_classes, ir_options, **kwargs): + """Configuration model config settings.""" + + self.model_classes = model_classes + self.data_classes = data_classes + if data_classes is not None: + train_data_cfg = self.get_subset_data_cfg(cfg, "train") + train_data_cfg["data_classes"] = data_classes + new_classes = np.setdiff1d(data_classes, model_classes).tolist() + train_data_cfg["new_classes"] = new_classes + self.configure_backbone(cfg, ir_options) + self.configure_task(cfg, **kwargs) + + def configure_backbone(self, cfg, ir_options): + """Patch config's model. + + Change model type to super type + Patch for OMZ backbones + """ + + if ir_options is None: + ir_options = {"ir_model_path": None, "ir_weight_path": None, "ir_weight_init": False} + + super_type = cfg.model.pop("super_type", None) + if super_type: + cfg.model.arch_type = cfg.model.type + cfg.model.type = super_type + + # OV-plugin + ir_model_path = ir_options.get("ir_model_path") + if ir_model_path: + + def is_mmov_model(key, value): + if key == "type" and value.startswith("MMOV"): + return True + return False + + ir_weight_path = ir_options.get("ir_weight_path", None) + ir_weight_init = ir_options.get("ir_weight_init", False) + recursively_update_cfg( + cfg, + is_mmov_model, + {"model_path": ir_model_path, "weight_path": ir_weight_path, "init_weight": ir_weight_init}, + ) + + def configure_task(self, cfg, **kwargs): + """Patch config to support training algorithm.""" + if "task_adapt" in cfg: + logger.info(f"task config!!!!: training={self.training}") + self.task_adapt_type = cfg["task_adapt"].get("type", None) + self.task_adapt_op = cfg["task_adapt"].get("op", "REPLACE") + self.configure_classes(cfg) + + def configure_classes(self, cfg): + """Patch classes for model and dataset.""" + org_model_classes = self.get_model_classes(cfg) + data_classes = self.get_data_classes(cfg) + + # Model classes + if self.task_adapt_op == "REPLACE": + if len(data_classes) == 0: + model_classes = org_model_classes.copy() + else: + model_classes = data_classes.copy() + elif self.task_adapt_op == "MERGE": + model_classes = org_model_classes + [cls for cls in data_classes if cls not in org_model_classes] + else: + raise KeyError(f"{self.task_adapt_op} is not supported for task_adapt options!") + + cfg.task_adapt.final = model_classes + cfg.model.task_adapt = ConfigDict( + src_classes=org_model_classes, + dst_classes=model_classes, + ) + + self.org_model_classes = org_model_classes + self.model_classes = model_classes + self.data_classes = data_classes + + self._configure_head(cfg) + + def _configure_head(self, cfg): + raise NotImplementedError + + @staticmethod + def configure_compat_cfg(cfg: Config): + """Modify config to keep the compatibility.""" + + global_dataloader_cfg: Dict[str, str] = {} + global_dataloader_cfg.update( + { + k: cfg.data.pop(k) + for k in list(cfg.data.keys()) + if k + not in [ + "train", + "val", + "test", + "unlabeled", + "train_dataloader", + "val_dataloader", + "test_dataloader", + "unlabeled_dataloader", + ] + } + ) + + for subset in ["train", "val", "test", "unlabeled"]: + if subset not in cfg.data: + continue + dataloader_cfg = cfg.data.get(f"{subset}_dataloader", None) + if dataloader_cfg is None: + raise AttributeError(f"{subset}_dataloader is not found in config.") + dataloader_cfg = Config(cfg_dict={**global_dataloader_cfg, **dataloader_cfg}) + cfg.data[f"{subset}_dataloader"] = dataloader_cfg + + def configure_hooks( + self, + cfg, + ): + """Add or update hooks.""" + + if "custom_hooks" in self.override_configs: + override_custom_hooks = self.override_configs.pop("custom_hooks") + for override_custom_hook in override_custom_hooks: + update_or_add_custom_hook(cfg, ConfigDict(override_custom_hook)) + if len(self.override_configs) > 0: + logger.info(f"before override configs merging = {cfg}") + cfg.merge_from_dict(self.override_configs) + logger.info(f"after override configs merging = {cfg}") + + # add Cancel training hook + update_or_add_custom_hook( + cfg, + ConfigDict(type="CancelInterfaceHook", init_callback=self.on_hook_initialized), + ) + if self.time_monitor is not None: + update_or_add_custom_hook( + cfg, + ConfigDict( + type="OTXProgressHook", + time_monitor=self.time_monitor, + verbose=True, + priority=71, + ), + ) + cfg.log_config.hooks.append({"type": "OTXLoggerHook", "curves": self.learning_curves}) + if hasattr(cfg, "algo_backend"): + self._update_caching_modules(cfg) + + # Update adaptive repeat + if not self.training: + remove_from_configs_by_type(cfg.custom_hooks, "AdaptiveRepeatDataHook") + return + for custom_hook in cfg.custom_hooks: + if custom_hook["type"] == "AdaptiveRepeatDataHook": + data_cfg = cfg.get("data", {}) + bs = data_cfg.get("train_dataloader", {}).get("samples_per_gpu", None) + bs = bs if bs is not None else data_cfg.get("samples_per_gpu", 0) + custom_hook["train_batch_size"] = bs + custom_hook["train_data_size"] = len(data_cfg.get("train", {}).get("otx_dataset", [])) + break + + @staticmethod + def _update_caching_modules(cfg: Config) -> None: + def _find_max_num_workers(cfg: dict): + num_workers = [0] + for key, value in cfg.items(): + if key == "workers_per_gpu" and isinstance(value, int): + num_workers += [value] + elif isinstance(value, dict): + num_workers += [_find_max_num_workers(value)] + + return max(num_workers) + + def _get_mem_cache_size(cfg): + if not hasattr(cfg.algo_backend, "mem_cache_size"): + return 0 + + return cfg.algo_backend.mem_cache_size + + max_num_workers = _find_max_num_workers(cfg.data) + mem_cache_size = _get_mem_cache_size(cfg) + + mode = "multiprocessing" if max_num_workers > 0 else "singleprocessing" + caching.MemCacheHandlerSingleton.create(mode, mem_cache_size) + + update_or_add_custom_hook( + cfg, + ConfigDict(type="MemCacheHook", priority="VERY_LOW"), + ) + + def get_model_classes(self, cfg): + """Extract trained classes info from checkpoint file. + + MMCV-based models would save class info in ckpt['meta']['CLASSES'] + For other cases, try to get the info from cfg.model.classes (with pop()) + - Which means that model classes should be specified in model-cfg for + non-MMCV models (e.g. OMZ models) + """ + + def get_model_meta(cfg): + ckpt_path = cfg.get("load_from", None) + meta = {} + if ckpt_path: + ckpt = CheckpointLoader.load_checkpoint(ckpt_path, map_location="cpu") + meta = ckpt.get("meta", {}) + return meta + + classes = [] + meta = get_model_meta(cfg) + # for OTX classification legacy compatibility + classes = meta.get("CLASSES", []) + classes = meta.get("classes", classes) + if classes is None: + classes = [] + + if len(classes) == 0: + ckpt_path = cfg.get("load_from", None) + if ckpt_path: + classes = self.model_classes + if len(classes) == 0: + classes = cfg.model.pop("classes", cfg.pop("model_classes", [])) + return classes + + def get_data_classes(self, cfg): + """Get data classes from train cfg.""" + data_classes = [] + train_cfg = self.get_subset_data_cfg(cfg, "train") + if "data_classes" in train_cfg: + data_classes = list(train_cfg.pop("data_classes", [])) + elif "classes" in train_cfg: + data_classes = list(train_cfg.classes) + return data_classes + + @staticmethod + def get_subset_data_cfg(cfg, subset): + """Get subset's data cfg.""" + assert subset in ["train", "val", "test", "unlabeled"], f"Unknown subset:{subset}" + if "dataset" in cfg.data[subset]: # Concat|RepeatDataset + dataset = cfg.data[subset].dataset + while hasattr(dataset, "dataset"): + dataset = dataset.dataset + return dataset + return cfg.data[subset] + + @staticmethod + def adapt_input_size_to_dataset( + cfg, input_size_manager: InputSizeManager, downscale_only: bool = True, use_annotations: bool = False + ) -> Optional[Tuple[int, int]]: + """Compute appropriate model input size w.r.t. dataset statistics. + + Args: + cfg (Dict): Global configuration. + input_size_manager: (InputSizeManager): Pre-configured input size manager + downscale_only (bool) : Whether to allow only smaller size than default setting. Defaults to True. + use_annotations (bool): Whether to consider annotation shapes to compute input size. Defaults to False. + + Returns: + Tuple[int, int]: (width, height) or None + """ + + data_cfg = BaseConfigurer.get_subset_data_cfg(cfg, "train") + dataset = data_cfg.get("otx_dataset", None) + if dataset is None: + return None + + stat = compute_robust_dataset_statistics(dataset, use_annotations) + if not stat: + return None + + def format_float(obj): + if isinstance(obj, float): + return f"{obj:.2f}" + if isinstance(obj, dict): + return {k: format_float(v) for k, v in obj.items()} + return obj + + logger.info(f"Dataset stat: {json.dumps(format_float(stat), indent=4)}") + + # Fit to typical large image size (conservative) + # -> "avg" size might be preferrable for efficiency + image_size = stat["image"]["robust_max"] + object_size = None + if use_annotations and stat["annotation"]: + # Refine using annotation shape size stat + # Fit to typical small object size (conservative) + # -> "avg" size might be preferrable for efficiency + object_size = stat["annotation"].get("size_of_shape", {}).get("robust_min", None) + + return input_size_manager.adapt_input_size_to_dataset(image_size, object_size, downscale_only) diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/__init__.py b/src/otx/algorithms/common/adapters/mmcv/hooks/__init__.py new file mode 100644 index 00000000000..4aed0db6e6d --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/__init__.py @@ -0,0 +1,107 @@ +"""Adapters for mmcv support.""" + +# Copyright (C) 2022-2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .adaptive_repeat_data_hook import AdaptiveRepeatDataHook +from .adaptive_training_hook import AdaptiveTrainSchedulingHook +from .cancel_hook import CancelInterfaceHook, CancelTrainingHook +from .checkpoint_hook import ( + CheckpointHookWithValResults, + EnsureCorrectBestCheckpointHook, + SaveInitialWeightHook, +) +from .composed_dataloaders_hook import ComposedDataLoadersHook +from .custom_model_ema_hook import CustomModelEMAHook, EMAMomentumUpdateHook +from .dual_model_ema_hook import DualModelEMAHook +from .early_stopping_hook import ( + EarlyStoppingHook, + LazyEarlyStoppingHook, + ReduceLROnPlateauLrUpdaterHook, + StopLossNanTrainingHook, +) +from .eval_hook import CustomEvalHook, DistCustomEvalHook +from .force_train_hook import ForceTrainModeHook +from .fp16_sam_optimizer_hook import Fp16SAMOptimizerHook +from .ib_loss_hook import IBLossHook +from .logger_hook import LoggerReplaceHook, OTXLoggerHook +from .loss_dynamics_tracking_hook import LossDynamicsTrackingHook +from .mean_teacher_hook import MeanTeacherHook +from .mem_cache_hook import MemCacheHook +from .model_ema_v2_hook import ModelEmaV2Hook +from .no_bias_decay_hook import NoBiasDecayHook +from .progress_hook import OTXProgressHook +from .recording_forward_hook import ( + ActivationMapHook, + BaseRecordingForwardHook, + EigenCamHook, + FeatureVectorHook, +) +from .sam_optimizer_hook import SAMOptimizerHook +from .semisl_cls_hook import SemiSLClsHook +from .task_adapt_hook import TaskAdaptHook +from .two_crop_transform_hook import TwoCropTransformHook + +__all__ = [ + "AdaptiveRepeatDataHook", + "AdaptiveTrainSchedulingHook", + "CancelInterfaceHook", + "CancelTrainingHook", + "CheckpointHookWithValResults", + "EnsureCorrectBestCheckpointHook", + "ComposedDataLoadersHook", + "CustomEvalHook", + "DistCustomEvalHook", + "EarlyStoppingHook", + "LazyEarlyStoppingHook", + "ReduceLROnPlateauLrUpdaterHook", + "EMAMomentumUpdateHook", + "ForceTrainModeHook", + "Fp16SAMOptimizerHook", + "StopLossNanTrainingHook", + "IBLossHook", + "OTXLoggerHook", + "LoggerReplaceHook", + "CustomModelEMAHook", + "DualModelEMAHook", + "ModelEmaV2Hook", + "NoBiasDecayHook", + "OTXProgressHook", + "BaseRecordingForwardHook", + "EigenCamHook", + "ActivationMapHook", + "FeatureVectorHook", + "SAMOptimizerHook", + "SaveInitialWeightHook", + "SemiSLClsHook", + "TaskAdaptHook", + "TwoCropTransformHook", + "MeanTeacherHook", + "MemCacheHook", + "LossDynamicsTrackingHook", +] + +try: + from .hpu_optimizer_hook import HPUOptimizerHook + + __all__ += ["HPUOptimizerHook"] +except: # noqa: E722 + pass + +try: + from .xpu_optimizer_hook import BFp16XPUOptimizerHook + + __all__ += ["BFp16XPUOptimizerHook"] +except: # noqa: E722 + pass diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_repeat_data_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_repeat_data_hook.py new file mode 100644 index 00000000000..a04657fd324 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_repeat_data_hook.py @@ -0,0 +1,81 @@ +"""Adaptive repeat data hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.runner import HOOKS, Hook, get_dist_info +from torch.utils.data import DataLoader + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import get_proper_repeat_times +from otx.algorithms.common.adapters.torch.dataloaders.samplers import OTXSampler +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class AdaptiveRepeatDataHook(Hook): + """Hook that adaptively repeats the dataset to control the number of iterations. + + Args: + train_batch_size (int) : The batch size of the train dataloader + train_data_size (int) : The number of the training dataset + coef (float, optional) : coefficient that effects to number of repeats + (coef * math.sqrt(num_iters-1)) +5 + min_repeat (float, optional) : minimum repeats + """ + + def __init__(self, train_batch_size: int, train_data_size: int, coef: float = -0.7, min_repeat: float = 1.0): + self.coef = coef + self.min_repeat = min_repeat + + self.train_batch_size = train_batch_size + self.train_data_size = train_data_size + + self.n_repeats = get_proper_repeat_times( + self.train_data_size, self.train_batch_size, self.coef, self.min_repeat + ) + self.rank, self.world_size = get_dist_info() + + def before_run(self, runner): + """Change the runner's max_iter.""" + if self.n_repeats > 1: + iter_per_epoch = int(self.train_data_size / self.train_batch_size) + + logger.info("Adaptive repeat is enabled") + logger.info(f"- Repeat times: {self.n_repeats}") + logger.info(f"- Batch size: {self.train_batch_size}") + logger.info(f"- Num iters per epoch: {iter_per_epoch} -> {iter_per_epoch * self.n_repeats}") + logger.info(f"- Total iters: {runner.max_iters} -> {runner.max_iters * self.n_repeats}") + + # FIXME, although runner._max_iters is the protected attribute, + # There is no way to control the max_iters of runner. + runner._max_iters = int(runner.max_iters * self.n_repeats) + + def before_epoch(self, runner): + """Convert to OTX Sampler.""" + dataset = runner.data_loader.dataset + num_workers = runner.data_loader.num_workers + collate_fn = runner.data_loader.collate_fn + worker_init_fn = runner.data_loader.worker_init_fn + + sampler = OTXSampler( + dataset=dataset, + samples_per_gpu=self.train_batch_size, + num_replicas=self.world_size, + rank=self.rank, + shuffle=True, + coef=self.coef, + min_repeat=self.min_repeat, + n_repeats=self.n_repeats, + ) + + runner.data_loader = DataLoader( + dataset, + batch_size=self.train_batch_size, + sampler=sampler, + num_workers=num_workers, + collate_fn=collate_fn, + pin_memory=False, + worker_init_fn=worker_init_fn, + ) diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_training_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_training_hook.py new file mode 100644 index 00000000000..747b638fb3a --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_training_hook.py @@ -0,0 +1,138 @@ +"""Adaptive training schedule hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import math + +from mmcv.runner import HOOKS, Hook, LrUpdaterHook +from mmcv.runner.hooks.checkpoint import CheckpointHook +from mmcv.runner.hooks.evaluation import EvalHook + +from otx.algorithms.common.adapters.mmcv.hooks.early_stopping_hook import ( + EarlyStoppingHook, +) +from otx.utils.logger import get_logger + +logger = get_logger() + +# pylint: disable=too-many-arguments, too-many-instance-attributes + + +@HOOKS.register_module() +class AdaptiveTrainSchedulingHook(Hook): + """Adaptive Training Scheduling Hook. + + Depending on the size of iteration per epoch, adaptively update the validation interval and related values. + + Args: + base_lr_patience (int): The value of LR drop patience are expected in total epoch. + Patience used when interval is 1, Defaults to 5. + min_lr_patience (int): Minumum value of LR drop patience. + Defaults to 2. + base_es_patience (int): The value of Early-Stopping patience are expected in total epoch. + Patience used when interval is 1, Defaults to 10. + max_interval (int): Maximum value of validation interval. + Defaults to 5. + decay (float): Parameter to control the interval. This value is set by manual manner. + Defaults to -0.025. + enable_adaptive_interval_hook (bool): If True, adaptive interval will be enabled. + Defaults to False. + enable_eval_before_run (bool): If True, initial evaluation before training will be enabled. + Defaults to False. + """ + + def __init__( + self, + max_interval=5, + base_lr_patience=5, + min_lr_patience=2, + base_es_patience=10, + min_es_patience=3, + decay=-0.025, + enable_adaptive_interval_hook=False, + enable_eval_before_run=False, + **kwargs, + ): + super().__init__(**kwargs) + + self.max_interval = max_interval + self.base_lr_patience = base_lr_patience + self.min_lr_patience = min_lr_patience + self.base_es_patience = base_es_patience + self.min_es_patience = min_es_patience + self.decay = decay + self.enable_adaptive_interval_hook = enable_adaptive_interval_hook + self.enable_eval_before_run = enable_eval_before_run + + self._initialized = False + self._original_interval = None + + def before_run(self, runner): + """Before run.""" + if self.enable_eval_before_run: + hook = self.get_evalhook(runner) + if hook is None: + logger.warning("EvalHook is not found in runner. Skipping enabling evaluation before run.") + return + self._original_interval = hook.interval + hook.interval = 1 + hook.start = 0 + + def before_train_iter(self, runner): + """Before train iter.""" + if self.enable_eval_before_run and self._original_interval is not None: + hook = self.get_evalhook(runner) + hook.interval = self._original_interval + self._original_interval = None + + if self.enable_adaptive_interval_hook and not self._initialized: + self.max_interval = min(self.max_interval, runner.max_epochs - runner.epoch) + iter_per_epoch = len(runner.data_loader) + adaptive_interval = self.get_adaptive_interval(iter_per_epoch) + for hook in runner.hooks: + if isinstance(hook, EvalHook): + # make sure evaluation is done at last to save best checkpoint + limit = runner.max_epochs if hook.by_epoch else runner.max_iters + adaptive_interval = min(adaptive_interval, limit) + logger.info(f"Update EvalHook interval: {hook.interval} -> {adaptive_interval}") + hook.interval = adaptive_interval + elif isinstance(hook, LrUpdaterHook): + patience = max( + math.ceil((self.base_lr_patience / adaptive_interval)), + self.min_lr_patience, + ) + if hasattr(hook, "interval") and hasattr(hook, "patience"): + hook.interval = adaptive_interval + hook.patience = patience + logger.info(f"Update LrUpdaterHook patience: {hook.patience} -> {patience}") + elif isinstance(hook, EarlyStoppingHook): + patience = max( + math.ceil((self.base_es_patience / adaptive_interval)), + self.min_es_patience, + ) + logger.info(f"Update EarlyStoppingHook patience: {hook.patience} -> {patience}") + hook.start = adaptive_interval + hook.interval = adaptive_interval + hook.patience = patience + elif isinstance(hook, CheckpointHook): + # make sure checkpoint is saved at last + limit = runner.max_epochs if hook.by_epoch else runner.max_iters + adaptive_interval = min(adaptive_interval, limit) + logger.info(f"Update CheckpointHook interval: {hook.interval} -> {adaptive_interval}") + hook.interval = adaptive_interval + self._initialized = True + + def get_adaptive_interval(self, iter_per_epoch): + """Get adaptive interval.""" + adaptive_interval = max(round(math.exp(self.decay * iter_per_epoch) * self.max_interval), 1) + return adaptive_interval + + def get_evalhook(self, runner): + """Get evaluation hook.""" + target_hook = None + for hook in runner.hooks: + if isinstance(hook, EvalHook): + assert target_hook is None, "More than 1 EvalHook is found in runner." + target_hook = hook + return target_hook diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/cancel_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/cancel_hook.py new file mode 100644 index 00000000000..9f92c73caa0 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/cancel_hook.py @@ -0,0 +1,86 @@ +"""Cancel hooks.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import os +from typing import Callable + +from mmcv.runner import BaseRunner, EpochBasedRunner +from mmcv.runner.hooks import HOOKS, Hook + +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-instance-attributes, protected-access, too-many-arguments, unused-argument +@HOOKS.register_module() +class CancelTrainingHook(Hook): + """CancelTrainingHook for Training Stopping.""" + + def __init__(self, interval: int = 5): + """Periodically check whether whether a stop signal is sent to the runner during model training. + + Every 'check_interval' iterations, the work_dir for the runner is checked to see if a file '.stop_training' + is present. If it is, training is stopped. + + :param interval: Period for checking for stop signal, given in iterations. + + """ + self.interval = interval + + @staticmethod + def _check_for_stop_signal(runner: BaseRunner): + """Log _check_for_stop_signal for CancelTrainingHook.""" + work_dir = runner.work_dir + stop_filepath = os.path.join(work_dir, ".stop_training") + if os.path.exists(stop_filepath): + if isinstance(runner, EpochBasedRunner): + epoch = runner.epoch + runner._max_epochs = epoch # Force runner to stop by pretending it has reached it's max_epoch + runner.should_stop = True # Set this flag to true to stop the current training epoch + os.remove(stop_filepath) + + def after_train_iter(self, runner: BaseRunner): + """Log after_train_iter for CancelTrainingHook.""" + if not self.every_n_iters(runner, self.interval): + return + self._check_for_stop_signal(runner) + + +@HOOKS.register_module() +class CancelInterfaceHook(Hook): + """Cancel interface. If called, running job will be terminated.""" + + def __init__(self, init_callback: Callable, interval=5): + self.on_init_callback = init_callback + self.runner = None + self.interval = interval + + def cancel(self): + """Cancel.""" + logger.info("CancelInterfaceHook.cancel() is called.") + if self.runner is None: + logger.warning("runner is not configured yet. ignored this request.") + return + + if self.runner.should_stop: + logger.warning("cancel already requested.") + return + + if isinstance(self.runner, EpochBasedRunner): + epoch = self.runner.epoch + self.runner._max_epochs = epoch # Force runner to stop by pretending it has reached it's max_epoch + self.runner.should_stop = True # Set this flag to true to stop the current training epoch + logger.info("requested stopping to the runner") + + def before_run(self, runner): + """Before run.""" + self.runner = runner + self.on_init_callback(self) + + def after_run(self, runner): + """After run.""" + self.runner = None diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/checkpoint_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/checkpoint_hook.py new file mode 100644 index 00000000000..1c568b9e315 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/checkpoint_hook.py @@ -0,0 +1,173 @@ +"""CheckpointHook with validation results for classification task.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# Copyright (c) Open-MMLab. All rights reserved. +from pathlib import Path +from typing import Optional + +from mmcv.runner import BaseRunner +from mmcv.runner.dist_utils import allreduce_params, master_only +from mmcv.runner.hooks.hook import HOOKS, Hook + + +@HOOKS.register_module() +class CheckpointHookWithValResults(Hook): # pylint: disable=too-many-instance-attributes + """Save checkpoints periodically. + + Args: + interval (int): The saving period. If ``by_epoch=True``, interval + indicates epochs, otherwise it indicates iterations. + Default: -1, which means "never". + by_epoch (bool): Saving checkpoints by epoch or by iteration. + Default: True. + save_optimizer (bool): Whether to save optimizer state_dict in the + checkpoint. It is usually used for resuming experiments. + Default: True. + out_dir (str, optional): The directory to save checkpoints. If not + specified, ``runner.work_dir`` will be used by default. + max_keep_ckpts (int, optional): The maximum checkpoints to keep. + In some cases we want only the latest few checkpoints and would + like to delete old ones to save the disk space. + Default: -1, which means unlimited. + sync_buffer (bool): Whether to synchronize buffers in different + gpus. Default: False. + """ + + def __init__( + self, + interval=-1, + by_epoch=True, + save_optimizer=True, + out_dir=None, + max_keep_ckpts=-1, + sync_buffer=False, + **kwargs, + ) -> None: + self.interval = interval + self.by_epoch = by_epoch + self.save_optimizer = save_optimizer + self.out_dir = out_dir + self.max_keep_ckpts = max_keep_ckpts + self.args = kwargs + self.sync_buffer = sync_buffer + self._best_model_weight: Optional[Path] = None + + def before_run(self, runner): + """Set output directopy if not set.""" + if not self.out_dir: + self.out_dir = runner.work_dir + + def after_train_epoch(self, runner): + """Checkpoint stuffs after train epoch.""" + if not self.by_epoch or not self.every_n_epochs(runner, self.interval): + return + + if self.sync_buffer: + allreduce_params(runner.model.buffers()) + save_ema_model = hasattr(runner, "save_ema_model") and runner.save_ema_model + if save_ema_model: + backup_model = runner.model + runner.model = runner.ema_model + if getattr(runner, "save_ckpt", False): + runner.logger.info(f"Saving best checkpoint at {runner.epoch + 1} epochs") + self._save_best_checkpoint(runner) + runner.save_ckpt = False + + self._save_latest_checkpoint(runner) + + if save_ema_model: + runner.model = backup_model + runner.save_ema_model = False + + @master_only + def _save_best_checkpoint(self, runner): + """Save the current checkpoint and delete unwanted checkpoint.""" + if self._best_model_weight is not None: # remove previous best model weight + prev_model_weight = self.out_dir / self._best_model_weight + if prev_model_weight.exists(): + prev_model_weight.unlink() + + if self.by_epoch: + weight_name = f"best_epoch_{runner.epoch + 1}.pth" + else: + weight_name = f"best_iter_{runner.iter + 1}.pth" + runner.save_checkpoint(self.out_dir, filename_tmpl=weight_name, save_optimizer=self.save_optimizer, **self.args) + + self._best_model_weight = Path(weight_name) + if runner.meta is not None: + runner.meta.setdefault("hook_msgs", dict()) + runner.meta["hook_msgs"]["best_ckpt"] = str(self.out_dir / self._best_model_weight) + + @master_only + def _save_latest_checkpoint(self, runner): + """Save the current checkpoint and delete unwanted checkpoint.""" + if self.by_epoch: + weight_name_format = "epoch_{}.pth" + cur_step = runner.epoch + 1 + else: + weight_name_format = "iter_{}.pth" + cur_step = runner.iter + 1 + + runner.save_checkpoint( + self.out_dir, + filename_tmpl=weight_name_format.format(cur_step), + save_optimizer=self.save_optimizer, + **self.args, + ) + + # remove other checkpoints + if self.max_keep_ckpts > 0: + for _step in range(cur_step - self.max_keep_ckpts * self.interval, 0, -self.interval): + ckpt_path = self.out_dir / Path(weight_name_format.format(_step)) + if ckpt_path.exists(): + ckpt_path.unlink() + + if runner.meta is not None: + cur_ckpt_filename = Path(self.args.get("filename_tmpl", weight_name_format.format(cur_step))) + runner.meta.setdefault("hook_msgs", dict()) + runner.meta["hook_msgs"]["last_ckpt"] = str(self.out_dir / cur_ckpt_filename) + + def after_train_iter(self, runner): + """Checkpoint stuffs after train iteration.""" + if self.by_epoch or not self.every_n_iters(runner, self.interval): + return + + if hasattr(runner, "save_ckpt"): + if runner.save_ckpt: + runner.logger.info(f"Saving checkpoint at {runner.iter + 1} iterations") + if self.sync_buffer: + allreduce_params(runner.model.buffers()) + self._save_checkpoint(runner) + runner.save_ckpt = False + + +@HOOKS.register_module() +class EnsureCorrectBestCheckpointHook(Hook): + """EnsureCorrectBestCheckpointHook. + + This hook makes sure that the 'best_mAP' checkpoint points properly to the best model, even if the best model is + created in the last epoch. + """ + + def after_run(self, runner: BaseRunner): + """Called after train epoch hooks.""" + runner.call_hook("after_train_epoch") + + +@HOOKS.register_module() +class SaveInitialWeightHook(Hook): + """Save the initial weights before training.""" + + def __init__(self, save_path, file_name: str = "weights.pth", **kwargs): + self._save_path = save_path + self._file_name = file_name + self._args = kwargs + + def before_run(self, runner): + """Save initial the weights before training.""" + runner.logger.info("Saving weight before training") + runner.save_checkpoint( + self._save_path, filename_tmpl=self._file_name, save_optimizer=False, create_symlink=False, **self._args + ) diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/composed_dataloaders_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/composed_dataloaders_hook.py new file mode 100644 index 00000000000..a70a01b68b1 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/composed_dataloaders_hook.py @@ -0,0 +1,50 @@ +"""Composed dataloader hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import List, Sequence, Union + +from mmcv.runner import HOOKS, Hook +from torch.utils.data import DataLoader + +from otx.algorithms.common.adapters.torch.dataloaders import ComposedDL +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class ComposedDataLoadersHook(Hook): + """Composed dataloader hook, which makes a composed dataloader which can combine multiple data loaders. + + Especially used for semi-supervised learning to aggregate a unlabeled dataloader and a labeled dataloader. + """ + + def __init__( + self, + data_loaders: Union[Sequence[DataLoader], DataLoader], + ): + self.data_loaders: List[DataLoader] = [] + self.composed_loader = None + + self.add_dataloaders(data_loaders) + + def add_dataloaders(self, data_loaders: Union[Sequence[DataLoader], DataLoader]): + """Create data_loaders to be added into composed dataloader.""" + if isinstance(data_loaders, DataLoader): + data_loaders = [data_loaders] + else: + data_loaders = list(data_loaders) + + self.data_loaders.extend(data_loaders) + self.composed_loader = None + + def before_epoch(self, runner): + """Create composedDL before running epoch.""" + if self.composed_loader is None: + logger.info("Creating ComposedDL " f"(runner's -> {runner.data_loader}, " f"hook's -> {self.data_loaders})") + self.composed_loader = ComposedDL([runner.data_loader, *self.data_loaders]) + # Per-epoch replacement: train-only loader -> train loader + additional loaders + # (It's similar to local variable in epoch. Need to update every epoch...) + runner.data_loader = self.composed_loader diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/custom_model_ema_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/custom_model_ema_hook.py new file mode 100644 index 00000000000..b30b080c052 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/custom_model_ema_hook.py @@ -0,0 +1,133 @@ +"""EMA hooks.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import math +from math import cos, pi + +from mmcv.parallel import is_module_wrapper +from mmcv.runner import HOOKS, BaseRunner, Hook +from mmcv.runner.hooks.ema import EMAHook + +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class CustomModelEMAHook(EMAHook): + """Custom EMAHook to update momentum for ema over training.""" + + def __init__(self, momentum=0.0002, epoch_momentum=0.0, interval=1, **kwargs): + super().__init__(momentum=momentum, interval=interval, **kwargs) + self.momentum = momentum + self.epoch_momentum = epoch_momentum + self.interval = interval + + def before_run(self, runner): + """To resume model with it's ema parameters more friendly. + + Register ema parameter as ``named_buffer`` to model + """ + if is_module_wrapper(runner.model): + model = runner.model.module.model_s if hasattr(runner.model.module, "model_s") else runner.model.module + else: + model = runner.model.model_s if hasattr(runner.model, "model_s") else runner.model + self.param_ema_buffer = {} + self.model_parameters = dict(model.named_parameters(recurse=True)) + for name, value in self.model_parameters.items(): + # "." is not allowed in module's buffer name + buffer_name = f"ema_{name.replace('.', '_')}" + self.param_ema_buffer[name] = buffer_name + model.register_buffer(buffer_name, value.data.clone()) + self.model_buffers = dict(model.named_buffers(recurse=True)) + if self.checkpoint is not None: + runner.resume(self.checkpoint) + + def before_train_epoch(self, runner): + """Update the momentum.""" + if self.epoch_momentum > 0.0: + iter_per_epoch = len(runner.data_loader) + epoch_decay = 1 - self.epoch_momentum + iter_decay = math.pow(epoch_decay, self.interval / iter_per_epoch) + self.momentum = 1 - iter_decay + logger.info(f"Update EMA momentum: {self.momentum}") + self.epoch_momentum = 0.0 # disable re-compute + + super().before_train_epoch(runner) + + +@HOOKS.register_module() +class EMAMomentumUpdateHook(Hook): + """Exponential moving average (EMA) momentum update hook for self-supervised methods. + + This hook includes momentum adjustment in self-supervised methods following: + m = 1 - ( 1- m_0) * (cos(pi * k / K) + 1) / 2, + k: current step, K: total steps. + + :param end_momentum: The final momentum coefficient for the target network, defaults to 1. + :param update_interval: Interval to update new momentum, defaults to 1. + :param by_epoch: Whether updating momentum by epoch or not, defaults to False. + """ + + def __init__(self, end_momentum: float = 1.0, update_interval: int = 1, by_epoch: bool = False): + self.by_epoch = by_epoch + self.end_momentum = end_momentum + self.update_interval = update_interval + + def before_train_epoch(self, runner: BaseRunner): + """Called before_train_epoch in EMAMomentumUpdateHook.""" + if not self.by_epoch: + return + + if is_module_wrapper(runner.model): + model = runner.model.module + else: + model = runner.model + + if not hasattr(model, "momentum"): + raise AttributeError('The model must have attribute "momentum".') + if not hasattr(model, "base_momentum"): + raise AttributeError('The model must have attribute "base_momentum".') + + if self.every_n_epochs(runner, self.update_interval): + cur_epoch = runner.epoch + max_epoch = runner.max_epochs + base_m = model.base_momentum + updated_m = ( + self.end_momentum - (self.end_momentum - base_m) * (cos(pi * cur_epoch / float(max_epoch)) + 1) / 2 + ) + model.momentum = updated_m + + def before_train_iter(self, runner: BaseRunner): + """Called before_train_iter in EMAMomentumUpdateHook.""" + if self.by_epoch: + return + + if is_module_wrapper(runner.model): + model = runner.model.module + else: + model = runner.model + + if not hasattr(model, "momentum"): + raise AttributeError('The model must have attribute "momentum".') + if not hasattr(model, "base_momentum"): + raise AttributeError('The model must have attribute "base_momentum".') + + if self.every_n_iters(runner, self.update_interval): + cur_iter = runner.iter + max_iter = runner.max_iters + base_m = model.base_momentum + updated_m = ( + self.end_momentum - (self.end_momentum - base_m) * (cos(pi * cur_iter / float(max_iter)) + 1) / 2 + ) + model.momentum = updated_m + + def after_train_iter(self, runner: BaseRunner): + """Called after_train_iter in EMAMomentumUpdateHook.""" + if self.every_n_iters(runner, self.update_interval): + if is_module_wrapper(runner.model): + runner.model.module.momentum_update() + else: + runner.model.momentum_update() diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/dual_model_ema_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/dual_model_ema_hook.py new file mode 100644 index 00000000000..9708c8cb05a --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/dual_model_ema_hook.py @@ -0,0 +1,143 @@ +"""Dual model EMA hooks.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import math + +import torch +from mmcv.parallel import is_module_wrapper +from mmcv.runner import HOOKS, Hook + +from otx.utils.logger import get_logger + +logger = get_logger() + +# pylint: disable=too-many-instance-attributes + + +@HOOKS.register_module() +class DualModelEMAHook(Hook): + r"""Generalized re-implementation of mmcv.runner.EMAHook. + + Source model paramters would be exponentially averaged + onto destination model pararmeters on given intervals + + .. math:: + + \text{Xema_{t+1}} = (1 - \text{momentum}) \times + \text{Xema_{t}} + \text{momentum} \times X_t + + Args: + momentum (float): The momentum used for updating ema parameter. + Defaults to 0.0002. + epoch_momentum (float): if > 0, momentum is ignored and re-calculated. + momentum = 1 - exp(1 - epoch_momentum, 1/num_iter_per_epoch) + Defaults to 0. + interval (int): Update ema parameter every interval iteration. + Defaults to 1. + start_epoch (int): During initial a few epochs, we just copy values + to update ema parameters. Defaults to 5. + src_model_name (str): Source model for EMA (X) + dst_model_name (str): Destination model for EMA (Xema) + """ + + def __init__( + self, + momentum=0.0002, + epoch_momentum=0.0, + interval=1, + start_epoch=5, + src_model_name="model_s", + dst_model_name="model_t", + **kwargs, + ): + super().__init__(**kwargs) + self.momentum = 1 - (1 - momentum) ** interval + self.epoch_momentum = epoch_momentum + self.interval = interval + self.start_epoch = start_epoch + self.src_model = None + self.dst_model = None + self.src_model_name = src_model_name + self.dst_model_name = dst_model_name + self.src_params = None + self.dst_params = None + self.enabled = False + + def before_run(self, runner): + """Set up src & dst model parameters.""" + model = self._get_model(runner) + self.src_model = getattr(model, self.src_model_name, None) + self.dst_model = getattr(model, self.dst_model_name, None) + if self.src_model and self.dst_model: + self.enabled = True + self.src_params = self.src_model.state_dict(keep_vars=True) + self.dst_params = self.dst_model.state_dict(keep_vars=True) + if runner.epoch == 0 and runner.iter == 0: + self._copy_model(sync_model=True) + logger.info("Initialized student model by teacher model") + logger.info(f"model_s model_t diff: {self._diff_model()}") + + def before_epoch(self, runner): + """Compute adaptive EMA momentum.""" + if self.epoch_momentum > 0.0: + iter_per_epoch = len(runner.data_loader) + epoch_decay = 1 - self.epoch_momentum + iter_decay = math.pow(epoch_decay, self.interval / iter_per_epoch) + self.momentum = 1 - iter_decay + logger.info(f"EMA: epoch_decay={epoch_decay} / iter_decay={iter_decay}") + self.epoch_momentum = 0.0 # disable re-compute + + def after_train_iter(self, runner): + """Update ema parameter every self.interval iterations.""" + if not self.enabled or (runner.iter % self.interval != 0): + # Skip update + return + + if runner.epoch + 1 < self.start_epoch: + # Just copy parameters before start epoch + self._copy_model() + return + + # EMA + self._ema_model() + + def after_train_epoch(self, runner): + """Log difference between models if enabled.""" + if self.enabled: + logger.info(f"model_s model_t diff: {self._diff_model()}") + + def _get_model(self, runner): + model = runner.model + if is_module_wrapper(model): + model = model.module + return model + + def _copy_model(self, sync_model=False): + with torch.no_grad(): + for name, src_param in self.src_params.items(): + if not name.startswith("ema_"): + dst_param = self.dst_params[name] + if sync_model: + src_param.data.copy_(dst_param.data) + else: + dst_param.data.copy_(src_param.data) + + def _ema_model(self): + momentum = min(self.momentum, 1.0) + with torch.no_grad(): + for name, src_param in self.src_params.items(): + if not name.startswith("ema_"): + dst_param = self.dst_params[name] + dst_param.data.copy_(dst_param.data * (1 - momentum) + src_param.data * momentum) + + def _diff_model(self): + diff_sum = 0.0 + with torch.no_grad(): + for name, src_param in self.src_params.items(): + if not name.startswith("ema_"): + dst_param = self.dst_params[name] + diff = ((src_param - dst_param) ** 2).sum() + diff_sum += diff + return diff_sum diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/early_stopping_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/early_stopping_hook.py new file mode 100644 index 00000000000..4be96b9516c --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/early_stopping_hook.py @@ -0,0 +1,389 @@ +"""Early stopping hooks.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from math import inf, isnan +from typing import List, Optional + +from mmcv.runner import BaseRunner, LrUpdaterHook +from mmcv.runner.hooks import HOOKS, Hook +from mmcv.utils import print_log + +from otx.utils.logger import get_logger + +logger = get_logger() + +# pylint: disable=too-many-arguments, too-many-instance-attributes + + +@HOOKS.register_module() +class EarlyStoppingHook(Hook): + """Cancel training when a metric has stopped improving. + + Early Stopping hook monitors a metric quantity and if no improvement is seen for a ‘patience’ + number of epochs, the training is cancelled. + + :param interval: the number of intervals for checking early stop. The interval number should be + the same as the evaluation interval - the `interval` variable set in + `evaluation` config. + :param metric: the metric name to be monitored + :param rule: greater or less. In `less` mode, training will stop when the metric has stopped + decreasing and in `greater` mode it will stop when the metric has stopped + increasing. + :param patience: Number of epochs with no improvement after which the training will be reduced. + For example, if patience = 2, then we will ignore the first 2 epochs with no + improvement, and will only cancel the training after the 3rd epoch if the + metric still hasn’t improved then + :param iteration_patience: Number of iterations must be trained after the last improvement + before training stops. The same as patience but the training + continues if the number of iteration is lower than iteration_patience + This variable makes sure a model is trained enough for some + iterations after the last improvement before stopping. + :param min_delta_ratio: Minimal ratio value to check the best score. If the difference between current and + best score is smaller than (current_score * (1-min_delta_ratio)), best score will not be changed. + """ + + rule_map = {"greater": lambda x, y: x > y, "less": lambda x, y: x < y} + init_value_map = {"greater": -inf, "less": inf} + greater_keys = ["acc", "top", "AR@", "auc", "precision", "mAP", "mDice", "mIoU", "mAcc", "aAcc", "MHAcc"] + less_keys = ["loss"] + + def __init__( + self, + interval: int, + metric: str = "bbox_mAP", + rule: Optional[str] = None, + patience: int = 5, + iteration_patience: int = 500, + min_delta_ratio: float = 0.0, + ): + super().__init__() + self.patience = patience + self.iteration_patience = iteration_patience + self.interval = interval + self.min_delta_ratio = min_delta_ratio + self._init_rule(rule, metric) + + self.min_delta_ratio *= 1 if self.rule == "greater" else -1 + self.last_iter = 0 + self.wait_count = 0 + self.best_score = self.init_value_map[self.rule] + self.warmup_iters = None + self.by_epoch = True + + def _init_rule(self, rule, key_indicator): + """Initialize rule, key_indicator, comparison_func, and best score. + + Here is the rule to determine which rule is used for key indicator + when the rule is not specific: + 1. If the key indicator is in ``self.greater_keys``, the rule will be + specified as 'greater'. + 2. Or if the key indicator is in ``self.less_keys``, the rule will be + specified as 'less'. + 3. Or if the key indicator is equal to the substring in any one item + in ``self.greater_keys``, the rule will be specified as 'greater'. + 4. Or if the key indicator is equal to the substring in any one item + in ``self.less_keys``, the rule will be specified as 'less'. + + Args: + rule (str | None): Comparison rule for best score. + key_indicator (str | None): Key indicator to determine the + comparison rule. + """ + if rule not in self.rule_map and rule is not None: + raise KeyError(f"rule must be greater, less or None, " f"but got {rule}.") + + if rule is None: + if key_indicator in self.greater_keys or any(key in key_indicator for key in self.greater_keys): + rule = "greater" + elif key_indicator in self.less_keys or any(key in key_indicator for key in self.less_keys): + rule = "less" + else: + raise ValueError( + f"Cannot infer the rule for key " f"{key_indicator}, thus a specific rule " f"must be specified." + ) + self.rule = rule + self.key_indicator = key_indicator + self.compare_func = self.rule_map[self.rule] + + def before_run(self, runner: BaseRunner): + """Called before_run in EarlyStoppingHook.""" + if runner.max_epochs is None: + self.by_epoch = False + for hook in runner.hooks: + if isinstance(hook, LrUpdaterHook): + self.warmup_iters = hook.warmup_iters + break + if getattr(self, "warmup_iters", None) is None: + raise ValueError("LrUpdaterHook must be registered to runner.") + + def after_train_iter(self, runner: BaseRunner): + """Called after every training iter to evaluate the results.""" + if not self.by_epoch: + self._do_check_stopping(runner) + + def after_train_epoch(self, runner: BaseRunner): + """Called after every training epoch to evaluate the results.""" + if self.by_epoch: + self._do_check_stopping(runner) + + def _do_check_stopping(self, runner): + """Called _do_check_stopping in EarlyStoppingHook.""" + if not self._should_check_stopping(runner) or self.warmup_iters > runner.iter: + return + + if runner.rank == 0: + if self.key_indicator not in runner.log_buffer.output: + raise KeyError( + f"metric {self.key_indicator} does not exist in buffer. Please check " + f"{self.key_indicator} is cached in evaluation output buffer" + ) + + key_score = runner.log_buffer.output[self.key_indicator] + if self.compare_func(key_score - (key_score * self.min_delta_ratio), self.best_score): + self.best_score = key_score + self.wait_count = 0 + self.last_iter = runner.iter + else: + self.wait_count += 1 + if self.wait_count >= self.patience: + if runner.iter - self.last_iter < self.iteration_patience: + print_log( + f"\nSkip early stopping. Accumulated iteration " + f"{runner.iter - self.last_iter} from the last " + f"improvement must be larger than {self.iteration_patience} to trigger " + f"Early Stopping.", + logger=runner.logger, + ) + return + stop_point = runner.epoch if self.by_epoch else runner.iter + print_log( + f"\nEarly Stopping at :{stop_point} with " f"best {self.key_indicator}: {self.best_score}", + logger=runner.logger, + ) + runner.should_stop = True + + def _should_check_stopping(self, runner): + """Called _should_check_stopping in EarlyStoppingHook.""" + check_time = self.every_n_epochs if self.by_epoch else self.every_n_iters + if not check_time(runner, self.interval): + # No evaluation during the interval. + return False + return True + + +@HOOKS.register_module() +class LazyEarlyStoppingHook(EarlyStoppingHook): + """Lazy early stop hook.""" + + def __init__( + self, + interval: int, + metric: str = "bbox_mAP", + rule: str = None, + patience: int = 5, + iteration_patience: int = 500, + min_delta_ratio: float = 0.0, + start: int = None, + ): + self.start = start + super().__init__(interval, metric, rule, patience, iteration_patience, min_delta_ratio) + + def _should_check_stopping(self, runner): + if self.by_epoch: + current = runner.epoch + check_time = self.every_n_epochs + else: + current = runner.iter + check_time = self.every_n_iters + + if self.start is None: + if not check_time(runner, self.interval): + # No evaluation during the interval. + return False + elif (current + 1) < self.start: + return False + elif (current + 1 - self.start) % self.interval: + return False + return True + + +@HOOKS.register_module(force=True) +class ReduceLROnPlateauLrUpdaterHook(LrUpdaterHook): + """Reduce learning rate when a metric has stopped improving. + + Models often benefit from reducing the learning rate by a factor of 2-10 once learning stagnates. + This scheduler reads a metrics quantity and if no improvement is seen for a ‘patience’ + number of epochs, the learning rate is reduced. + + :param min_lr: minimum learning rate. The lower bound of the desired learning rate. + :param interval: the number of intervals for checking the hook. The interval number should be + the same as the evaluation interval - the `interval` variable set in + `evaluation` config. + :param metric: the metric name to be monitored + :param rule: greater or less. In `less` mode, learning rate will be dropped if the metric has + stopped decreasing and in `greater` mode it will be dropped when the metric has + stopped increasing. + :param patience: Number of epochs with no improvement after which learning rate will be reduced. + For example, if patience = 2, then we will ignore the first 2 epochs with no + improvement, and will only drop LR after the 3rd epoch if the metric still + hasn’t improved then + :param iteration_patience: Number of iterations must be trained after the last improvement + before LR drops. The same as patience but the LR remains the same if + the number of iteration is lower than iteration_patience. This + variable makes sure a model is trained enough for some iterations + after the last improvement before dropping the LR. + :param factor: Factor to be multiply with the learning rate. + For example, new_lr = current_lr * factor + """ + + rule_map = {"greater": lambda x, y: x >= y, "less": lambda x, y: x < y} + init_value_map = {"greater": -inf, "less": inf} + greater_keys = ["acc", "top", "AR@", "auc", "precision", "mAP", "mDice", "mIoU", "mAcc", "aAcc", "MHAcc"] + less_keys = ["loss"] + + def __init__( + self, + min_lr: float, + interval: int, + metric: str = "bbox_mAP", + rule: Optional[str] = None, + factor: float = 0.1, + patience: int = 3, + iteration_patience: int = 300, + **kwargs, + ): + super().__init__(**kwargs) + self.interval = interval + self.min_lr = min_lr + self.factor = factor + self.patience = patience + self.iteration_patience = iteration_patience + self.metric = metric + self.bad_count = 0 + self.bad_count_iter = 0 + self.last_iter = 0 + self.current_lr = -1.0 + self.base_lr: List[float] = [] + self._init_rule(rule, metric) + self.best_score = self.init_value_map[self.rule] + + def _init_rule(self, rule, key_indicator): + """Initialize rule, key_indicator, comparison_func, and best score. + + Here is the rule to determine which rule is used for key indicator + when the rule is not specific: + 1. If the key indicator is in ``self.greater_keys``, the rule will be + specified as 'greater'. + 2. Or if the key indicator is in ``self.less_keys``, the rule will be + specified as 'less'. + 3. Or if the key indicator is equal to the substring in any one item + in ``self.greater_keys``, the rule will be specified as 'greater'. + 4. Or if the key indicator is equal to the substring in any one item + in ``self.less_keys``, the rule will be specified as 'less'. + + Args: + rule (str | None): Comparison rule for best score. + key_indicator (str | None): Key indicator to determine the + comparison rule. + """ + if rule not in self.rule_map and rule is not None: + raise KeyError(f"rule must be greater, less or None, " f"but got {rule}.") + + if rule is None: + if key_indicator in self.greater_keys or any(key in key_indicator for key in self.greater_keys): + rule = "greater" + elif key_indicator in self.less_keys or any(key in key_indicator for key in self.less_keys): + rule = "less" + else: + raise ValueError( + f"Cannot infer the rule for key " f"{key_indicator}, thus a specific rule " f"must be specified." + ) + self.rule = rule + self.key_indicator = key_indicator + self.compare_func = self.rule_map[self.rule] + + def _is_check_timing(self, runner: BaseRunner) -> bool: + """Check whether current epoch or iter is multiple of self.interval, skip during warmup interations.""" + check_time = self.after_each_n_epochs if self.by_epoch else self.after_each_n_iters + return check_time(runner, self.interval) and (self.warmup_iters <= runner.iter) + + def after_each_n_epochs(self, runner: BaseRunner, interval: int) -> bool: + """Check whether current epoch is a next epoch after multiples of interval.""" + return runner.epoch % interval == 0 if interval > 0 and runner.epoch != 0 else False + + def after_each_n_iters(self, runner: BaseRunner, interval: int) -> bool: + """Check whether current iter is a next iter after multiples of interval.""" + return runner.iter % interval == 0 if interval > 0 and runner.iter != 0 else False + + def get_lr(self, runner: BaseRunner, base_lr: float): + """Called get_lr in ReduceLROnPlateauLrUpdaterHook.""" + if self.current_lr < 0: + self.current_lr = base_lr + + # NOTE: get_lr could be called multiple times in a list comprehensive fashion in LrUpdateHook + if not self._is_check_timing(runner) or self.current_lr == self.min_lr or self.bad_count_iter == runner.iter: + return self.current_lr + + if hasattr(runner, "all_metrics"): + score = runner.all_metrics.get(self.metric, 0.0) + else: + return self.current_lr + + if self.compare_func(score, self.best_score): + self.best_score = score + self.bad_count = 0 + self.last_iter = runner.iter + else: + self.bad_count_iter = runner.iter + self.bad_count += 1 + + print_log( + f"\nBest Score: {self.best_score}, Current Score: {score}, Patience: {self.patience} " + f"Count: {self.bad_count}", + logger=runner.logger, + ) + + if self.bad_count >= self.patience: + if runner.iter - self.last_iter < self.iteration_patience: + print_log( + f"\nSkip LR dropping. Accumulated iteration " + f"{runner.iter - self.last_iter} from the last " + f"improvement must be larger than {self.iteration_patience} to trigger " + f"LR dropping.", + logger=runner.logger, + ) + return self.current_lr + + self.last_iter = runner.iter + self.bad_count = 0 + print_log( + f"\nDrop LR from: {self.current_lr}, to: " f"{max(self.current_lr * self.factor, self.min_lr)}", + logger=runner.logger, + ) + self.current_lr = max(self.current_lr * self.factor, self.min_lr) + return self.current_lr + + def before_run(self, runner: BaseRunner): + """Called before_run in ReduceLROnPlateauLrUpdaterHook.""" + # TODO: remove overloaded method after fixing the issue + # https://github.com/open-mmlab/mmdetection/issues/6572 + for group in runner.optimizer.param_groups: + group.setdefault("initial_lr", group["lr"]) + self.base_lr = [group["initial_lr"] for group in runner.optimizer.param_groups] + self.bad_count = 0 + self.last_iter = 0 + self.current_lr = -1.0 + self.best_score = self.init_value_map[self.rule] + + +@HOOKS.register_module(force=True) +class StopLossNanTrainingHook(Hook): + """StopLossNanTrainingHook.""" + + def after_train_iter(self, runner: BaseRunner): + """Called after_train_iter in StopLossNanTrainingHook.""" + if isnan(runner.outputs["loss"].item()): + logger.warning("Early Stopping since loss is NaN") + runner.should_stop = True diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/eval_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/eval_hook.py new file mode 100644 index 00000000000..f2fec6cbfb7 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/eval_hook.py @@ -0,0 +1,139 @@ +"""Module for definig CustomEvalHook for classification task.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from os import path as osp + +import mmcv +import torch +from mmcv.runner import HOOKS, EvalHook +from torch.utils.data import DataLoader + + +@HOOKS.register_module() +class CustomEvalHook(EvalHook): + """Custom Evaluation hook for the OTX. + + Args: + dataloader (DataLoader): A PyTorch dataloader. + interval (int): Evaluation interval (by epochs). Default: 1. + """ + + def __init__( + self, + *args, + ema_eval_start_epoch=10, + **kwargs, + ): + metric = kwargs["metric"] + self.metric = None + if isinstance(metric, str): + self.metric = "top-1" if metric == "accuracy" else metric + else: + self.metric = metric[0] + if metric.count("class_accuracy") > 0: + self.metric = "accuracy" + elif metric.count("accuracy") > 0: + self.metric = "top-1" + super().__init__(*args, **kwargs, save_best=self.metric, rule="greater") + self.ema_eval_start_epoch = ema_eval_start_epoch + + self.best_loss = 9999999.0 + self.best_score = 0.0 + self.save_mode = self.eval_kwargs.get("save_mode", "score") + + def _do_evaluate(self, runner, ema=False): + """Perform evaluation.""" + results = single_gpu_test(runner.model, self.dataloader) + if ema and hasattr(runner, "ema_model") and (runner.epoch >= self.ema_eval_start_epoch): + results_ema = single_gpu_test(runner.ema_model.module, self.dataloader) + self.evaluate(runner, results, results_ema) + else: + self.evaluate(runner, results) + + def after_train_epoch(self, runner): + """Check whether current epoch is to be evaluated or not.""" + if not self.by_epoch or not self.every_n_epochs(runner, self.interval): + return + self._do_evaluate(runner, ema=True) + + def after_train_iter(self, runner): + """Check whether current iteration is to be evaluated or not.""" + if self.by_epoch or not self.every_n_iters(runner, self.interval): + return + runner.log_buffer.clear() + self._do_evaluate(runner) + + def evaluate(self, runner, results, results_ema=None): + """Evaluate predictions from model with ground truth.""" + eval_res = self.dataloader.dataset.evaluate(results, logger=runner.logger, **self.eval_kwargs) + score = eval_res[self.metric] + for name, val in eval_res.items(): + runner.log_buffer.output[name] = val + + if results_ema: + eval_res_ema = self.dataloader.dataset.evaluate(results_ema, logger=runner.logger, **self.eval_kwargs) + score_ema = eval_res_ema[self.metric] + for name, val in eval_res_ema.items(): + runner.log_buffer.output[name + "_EMA"] = val + if score_ema > score: + runner.save_ema_model = True + + runner.log_buffer.ready = True + if score >= self.best_score: + self.best_score = score + runner.save_ckpt = True + + +def single_gpu_test(model, data_loader): + """Single gpu test for inference.""" + model.eval() + results = [] + dataset = data_loader.dataset + prog_bar = mmcv.ProgressBar(len(dataset)) + for data in data_loader: + with torch.no_grad(): + result = model(return_loss=False, **data) + results.append(result) + + batch_size = data["img"].size(0) + for _ in range(batch_size): + prog_bar.update() + prog_bar.file.write("\n") + return results + + +@HOOKS.register_module() +class DistCustomEvalHook(CustomEvalHook): + """Distributed Custom Evaluation Hook for Multi-GPU environment.""" + + def __init__(self, dataloader, interval=1, gpu_collect=False, by_epoch=True, **eval_kwargs): + if not isinstance(dataloader, DataLoader): + raise TypeError("dataloader must be a pytorch DataLoader, but got " f"{type(dataloader)}") + self.gpu_collect = gpu_collect + super().__init__(dataloader, interval, by_epoch=by_epoch, **eval_kwargs) + + def _do_evaluate(self, runner): + """Perform evaluation.""" + from mmcls.apis import multi_gpu_test + + results = multi_gpu_test( + runner.model, self.dataloader, tmpdir=osp.join(runner.work_dir, ".eval_hook"), gpu_collect=self.gpu_collect + ) + if runner.rank == 0: + print("\n") + self.evaluate(runner, results) + + def after_train_epoch(self, runner): + """Check whether current epoch is to be evaluated or not.""" + if not self.by_epoch or not self.every_n_epochs(runner, self.interval): + return + self._do_evaluate(runner) + + def after_train_iter(self, runner): + """Check whether current iteration is to be evaluated or not.""" + if self.by_epoch or not self.every_n_iters(runner, self.interval): + return + runner.log_buffer.clear() + self._do_evaluate(runner) diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/force_train_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/force_train_hook.py new file mode 100644 index 00000000000..b79c7b89a2e --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/force_train_hook.py @@ -0,0 +1,38 @@ +"""Collections of hooks for common OTX algorithms.""" + +# Copyright (C) 2021-2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from mmcv.runner.hooks import HOOKS, Hook + +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class ForceTrainModeHook(Hook): + """Force train mode for model. + + This is a workaround of a bug in EvalHook from MMCV. + If a model evaluation is enabled before training by setting 'start=0' in EvalHook, + EvalHook does not put a model in a training mode again after evaluation. + + This simple hook forces to put a model in a training mode before every train epoch + with the lowest priority. + """ + + def before_train_epoch(self, runner): + """Make sure to put a model in a training mode before train epoch.""" + runner.model.train() diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/fp16_sam_optimizer_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/fp16_sam_optimizer_hook.py new file mode 100644 index 00000000000..410b4ee65dc --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/fp16_sam_optimizer_hook.py @@ -0,0 +1,103 @@ +"""Module for Sharpness-aware Minimization optimizer hook implementation for MMCV Runners with FP16 precision.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcv.runner import HOOKS, Fp16OptimizerHook + + +@HOOKS.register_module() +class Fp16SAMOptimizerHook(Fp16OptimizerHook): + """Sharpness-aware Minimization optimizer hook. + + Implemented as OptimizerHook for MMCV Runners + - Paper ref: https://arxiv.org/abs/2010.01412 + - code ref: https://github.com/davda54/sam + """ + + def __init__(self, rho=0.05, start_epoch=1, **kwargs): + super().__init__(**kwargs) + self.rho = rho + self.start_epoch = start_epoch + if rho < 0.0: + raise ValueError("rho should be greater than 0 for SAM optimizer") + + def after_train_iter(self, runner): + """Perform SAM optimization. + + 0. compute current loss (DONE IN model.train_step()) + 1. compute current gradient + 2. move param to the approximate local maximum: w + e(w) = w + rho*norm_grad + 3. compute maximum loss + 4. compute SAM gradient on maximum loss + 5. restore parram to original param + 6. update param using SAM gradient + + Assuming model.current_batch had been set in model.train_step() + """ + current_batch = self._get_current_batch(runner.model) + if current_batch is None or runner.epoch + 1 < self.start_epoch: + # Apply original parameter update + return super().after_train_iter(runner) + + # Current gradient + runner.optimizer.zero_grad() + curr_loss = runner.outputs["loss"] + curr_loss.backward() + + # Move to local maximum + param2move = {} + with torch.no_grad(): + scale = self.rho / (self._grad_norm(runner.optimizer) + 1e-12) + for param_group in runner.optimizer.param_groups: + for param in param_group["params"]: + if param.grad is None: + continue + e_w = param.grad * scale.to(param) + param.add_(e_w) # Climb toward gradient (increasing loss) + param2move[param] = e_w # Saving for param restore + + # SAM gradient + runner.optimizer.zero_grad() + max_outputs = runner.model.train_step(current_batch, runner.optimizer) # forward() on maxima param + max_loss = max_outputs["loss"] + self.loss_scaler.scale(max_loss).backward() + self.loss_scaler.unscale_(runner.optimizer) + + # Restore to original param + with torch.no_grad(): + for param_group in runner.optimizer.param_groups: + for param in param_group["params"]: + if param.grad is None: + continue + param.sub_(param2move[param]) # Down to original param + + # Shaprpness-aware param update + self.loss_scaler.step(runner.optimizer) # param -= lr * sam_grad + self.loss_scaler.update(self._scale_update_param) + + runner.meta.setdefault("fp16", {})["loss_scaler"] = self.loss_scaler.state_dict() + runner.log_buffer.update({"sharpness": float(max_loss - curr_loss), "max_loss": float(max_loss)}) + return None + + def _get_current_batch(self, model): + if hasattr(model, "module"): + model = model.module + return getattr(model, "current_batch", None) + + def _grad_norm(self, optimizer): + # put everything on the same device, in case of model parallelism + shared_device = optimizer.param_groups[0]["params"][0].device + norm = torch.norm( + torch.stack( + [ + p.grad.norm(p=2).to(shared_device) + for group in optimizer.param_groups + for p in group["params"] + if p.grad is not None + ] + ), + p=2, + ) + return norm diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/hpu_optimizer_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/hpu_optimizer_hook.py new file mode 100644 index 00000000000..f5e26c49083 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/hpu_optimizer_hook.py @@ -0,0 +1,30 @@ +"""Optimizer hook for HPU.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import habana_frameworks.torch.core as htcore +from mmcv.runner import HOOKS, OptimizerHook + + +@HOOKS.register_module() +class HPUOptimizerHook(OptimizerHook): + """A hook contains custom operations for the optimizer on HPU.""" + + def after_train_iter(self, runner): + """After train iter.""" + runner.optimizer.zero_grad() + if self.detect_anomalous_params: + self.detect_anomalous_parameters(runner.outputs["loss"], runner) + runner.outputs["loss"].backward() + htcore.mark_step() + + if self.grad_clip is not None: + grad_norm = self.clip_grads(runner.model.parameters()) + if grad_norm is not None: + # Add grad norm to the logger + runner.log_buffer.update({"grad_norm": float(grad_norm)}, runner.outputs["num_samples"]) + + runner.optimizer.step() + htcore.mark_step() diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/ib_loss_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/ib_loss_hook.py new file mode 100644 index 00000000000..a9a2fdf3007 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/ib_loss_hook.py @@ -0,0 +1,42 @@ +"""Module for defining a hook for IB loss using mmcls.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.parallel import is_module_wrapper +from mmcv.runner import HOOKS, Hook + + +@HOOKS.register_module() +class IBLossHook(Hook): + """Hook for IB loss. + + It passes the number of data per class and current epoch to IB loss class. + """ + + def __init__(self, dst_classes): + """Initialize the IBLossHook. + + Args: + dst_classes (list): A list of classes including new_classes to be newly learned + """ + self.cls_num_list = None + self.dst_classes = dst_classes + + def before_train_epoch(self, runner): + """Get loss from model and pass the number of data per class and current epoch to IB loss.""" + model_loss = self._get_model_loss(runner) + if runner.epoch == 0: + dataset = runner.data_loader.dataset + num_data = self._get_num_data(dataset) + model_loss.update_weight(num_data) + model_loss.cur_epoch = runner.epoch + + def _get_num_data(self, dataset): + return [len(dataset.img_indices[data_cls]) for data_cls in self.dst_classes] + + def _get_model_loss(self, runner): + model = runner.model + if is_module_wrapper(model): + model = model.module + return model.head.compute_loss diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py new file mode 100644 index 00000000000..6889db20b5d --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py @@ -0,0 +1,106 @@ +"""Logger hooks.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from collections import defaultdict +from typing import Any, Dict, Optional + +from mmcv.runner import BaseRunner +from mmcv.runner.dist_utils import master_only +from mmcv.runner.hooks import HOOKS, Hook, LoggerHook + +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class OTXLoggerHook(LoggerHook): + """OTXLoggerHook for Logging.""" + + class Curve: + """Curve with x (epochs) & y (scores).""" + + def __init__(self): + self.x = [] + self.y = [] + + def __repr__(self): + """Repr function.""" + points = [] + for x, y in zip(self.x, self.y): + points.append(f"({x},{y})") + return "curve[" + ",".join(points) + "]" + + _TAGS_TO_SKIP = ( + "accuracy_top-1", + "current_iters", + "decode.acc_seg", + "decode.loss_ce_ignore", + ) + + _TAGS_TO_RENAME = { + "train/time": "train/time (sec/iter)", + "train/data_time": "train/data_time (sec/iter)", + "val/accuracy": "val/accuracy (%)", + } + + def __init__( + self, + curves: Optional[Dict[Any, Curve]] = None, + interval: int = 10, + ignore_last: bool = True, + reset_flag: bool = True, + by_epoch: bool = True, + ): + super().__init__(interval, ignore_last, reset_flag, by_epoch) + self.curves = curves if curves is not None else defaultdict(self.Curve) + + @master_only + def log(self, runner: BaseRunner): + """Log function for OTXLoggerHook.""" + tags = self.get_loggable_tags(runner, allow_text=False, tags_to_skip=self._TAGS_TO_SKIP) + if runner.max_epochs is not None: + normalized_iter = self.get_iter(runner) / runner.max_iters * runner.max_epochs + else: + normalized_iter = self.get_iter(runner) + for tag, value in tags.items(): + tag = self._TAGS_TO_RENAME.get(tag, tag) + curve = self.curves[tag] + # Remove duplicates. + if len(curve.x) > 0 and curve.x[-1] == normalized_iter: + curve.x.pop() + curve.y.pop() + curve.x.append(normalized_iter) + curve.y.append(value) + + def before_run(self, runner: BaseRunner): + """Called before_run in OTXLoggerHook.""" + super().before_run(runner) + self.curves.clear() + + def after_train_epoch(self, runner: BaseRunner): + """Called after_train_epoch in OTXLoggerHook.""" + # Iteration counter is increased right after the last iteration in the epoch, + # temporarily decrease it back. + runner._iter -= 1 + super().after_train_epoch(runner) + runner._iter += 1 + + +@HOOKS.register_module() +class LoggerReplaceHook(Hook): + """replace logger in the runner to the OTX logger. + + DO NOT INCLUDE this hook to the recipe directly. + OTX will add this hook to all recipe internally. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def before_run(self, runner): + """Replace logger.""" + runner.logger = logger + logger.info("logger in the runner is replaced to the OTX logger") diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/loss_dynamics_tracking_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/loss_dynamics_tracking_hook.py new file mode 100644 index 00000000000..041445bf22b --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/loss_dynamics_tracking_hook.py @@ -0,0 +1,90 @@ +"""Hook module to track loss dynamics during training and export these statistics to Datumaro format.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os.path as osp + +from mmcv.parallel import MMDataParallel +from mmcv.runner import BaseRunner +from mmcv.runner.hooks import HOOKS, Hook +from mmcv.utils import Config, ConfigDict + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + update_or_add_custom_hook, +) +from otx.api.entities.datasets import DatasetEntity +from otx.core.data.noisy_label_detection.base import LossDynamicsTracker, LossDynamicsTrackingMixin +from otx.utils.logger import get_logger + +logger = get_logger() + +__all__ = ["LossDynamicsTrackingHook"] + + +@HOOKS.register_module() +class LossDynamicsTrackingHook(Hook): + """Tracking loss dynamics during training and export it to Datumaro dataset format.""" + + def __init__(self, output_path: str, alpha: float = 0.001) -> None: + self._output_fpath = osp.join(output_path, "noisy_label_detection") + + def before_run(self, runner): + """Before run, check the type of model for safe running.""" + if not isinstance(runner.model, MMDataParallel): + raise NotImplementedError(f"Except MMDataParallel, runner.model={type(runner.model)} is not supported now.") + + def before_train_epoch(self, runner: BaseRunner): + """Initialize the tracker for training loss dynamics tracking. + + Tracker needs the training dataset for initialization. + However, there is no way to access to dataloader until the beginning of training epoch. + """ + self._init_tracker(runner, runner.data_loader.dataset.otx_dataset) + + def _get_tracker(self, runner: BaseRunner) -> LossDynamicsTracker: + model = runner.model.module + + if not isinstance(model, LossDynamicsTrackingMixin): + raise RuntimeError( + f"The model should be an instance of LossDynamicsTrackingMixin, but type(model)={type(model)}." + ) + return model.loss_dyns_tracker + + def _init_tracker(self, runner: BaseRunner, otx_dataset: DatasetEntity) -> None: + tracker = self._get_tracker(runner) + if tracker.initialized: + return + + logger.info("Initialize training loss dynamics tracker.") + tracker.init_with_otx_dataset(otx_dataset) + + def after_train_iter(self, runner): + """Accumulate training loss dynamics. + + It should be here because it needs to access the training iteration. + """ + tracker = self._get_tracker(runner) + tracker.accumulate(runner.outputs, runner.iter) + + def after_run(self, runner: BaseRunner) -> None: + """Export loss dynamics statistics to Datumaro format.""" + + tracker = self._get_tracker(runner) + + if tracker.initialized: + logger.info(f"Export training loss dynamics to {self._output_fpath}") + tracker.export(self._output_fpath) + + @classmethod + def configure_recipe(cls, recipe_cfg: Config, output_path: str) -> None: + """Configure recipe to enable loss dynamics tracking.""" + recipe_cfg.model["track_loss_dynamics"] = True + + update_or_add_custom_hook( + recipe_cfg, + ConfigDict( + type="LossDynamicsTrackingHook", + priority="LOWEST", + output_path=output_path, + ), + ) diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/mean_teacher_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/mean_teacher_hook.py new file mode 100644 index 00000000000..e34b36fc7e6 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/mean_teacher_hook.py @@ -0,0 +1,60 @@ +"""Unbiased-teacher hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.runner import HOOKS + +from otx.algorithms.common.adapters.mmcv.hooks.dual_model_ema_hook import ( + DualModelEMAHook, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class MeanTeacherHook(DualModelEMAHook): + """MeanTeacherHook for semi-supervised learnings.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.unlabeled_loss_enabled = False + + def before_train_epoch(self, runner): + """Enable unlabeled loss if over start epoch.""" + if runner.epoch + 1 < self.start_epoch: + return + if self.unlabeled_loss_enabled: + return + + super().before_train_epoch(runner) + + average_pseudo_label_ratio = self._get_average_pseudo_label_ratio(runner) + logger.info(f"avr_ps_ratio: {average_pseudo_label_ratio}") + self._get_model(runner).enable_unlabeled_loss(True) + self.unlabeled_loss_enabled = True + logger.info("---------- Enabled unlabeled loss and EMA smoothing ----------") + + def after_train_iter(self, runner): + """Update ema parameter every self.interval iterations.""" + if runner.iter % self.interval != 0: + # Skip update + return + + if runner.epoch + 1 < self.start_epoch or self.unlabeled_loss_enabled is False: + # Just copy parameters before enabled + self._copy_model() + return + + # EMA + self._ema_model() + + def _get_average_pseudo_label_ratio(self, runner): + output_backup = runner.log_buffer.output.copy() + was_ready = runner.log_buffer.ready + runner.log_buffer.average(100) + average_pseudo_label_ratio = runner.log_buffer.output.get("ps_ratio", 0.0) + runner.log_buffer.output = output_backup + runner.ready = was_ready + return average_pseudo_label_ratio diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/mem_cache_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/mem_cache_hook.py new file mode 100644 index 00000000000..ac7f8963193 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/mem_cache_hook.py @@ -0,0 +1,33 @@ +"""Memory cache hook for logging and freezing MemCacheHandler.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.runner.hooks import HOOKS, Hook + +from otx.core.data.caching.mem_cache_handler import MemCacheHandlerSingleton + + +@HOOKS.register_module() +class MemCacheHook(Hook): + """Memory cache hook for logging and freezing MemCacheHandler.""" + + def __init__(self) -> None: + self.handler = MemCacheHandlerSingleton.get() + # It is because the first evaluation comes at the very beginning of the training. + # We don't want to cache validation samples first. + self.handler.freeze() + + def before_epoch(self, runner): + """Before training, unfreeze the handler.""" + # We want to cache training samples first. + self.handler.unfreeze() + + def after_epoch(self, runner): + """After epoch. Log the handler statistics. + + To prevent it from skipping the validation samples, + this hook should have lower priority than CustomEvalHook. + """ + self.handler.freeze() + runner.logger.info(f"{self.handler}") diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/model_ema_v2_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/model_ema_v2_hook.py new file mode 100644 index 00000000000..f9ef09f69df --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/model_ema_v2_hook.py @@ -0,0 +1,120 @@ +"""Model EMA V2 hooks.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy + +import torch +from mmcv.runner import HOOKS, Hook +from torch import nn + +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class ModelEmaV2Hook(Hook): + r"""ModelEmaV2Hook. + + Source model paramters would be exponentially averaged + onto destination model pararmeters on given intervals + .. math:: + \text{Xema_{t+1}} = (1 - \text{momentum}) \times + \text{Xema_{t}} + \text{momentum} \times X_t + Args: + ema_decay (float): EMA decay value used for updating ema parameter. + Defaults to 0.999. + interval (int): Update ema parameter every interval iteration. + Defaults to 1. + start_epoch (int): During initial a few epochs, we just copy values + to update ema parameters. Defaults to 5. + dataset_len_thr (int): number of train images in the dataset when to enable the EMA hook + """ + + def __init__(self, ema_decay=0.9995, interval=1, start_epoch=0, dataset_len_thr=2000, **kwargs): + super().__init__(**kwargs) + self.ema_decay = ema_decay + self.interval = interval + self.start_epoch = start_epoch + self.dataset_len_thr = dataset_len_thr + self.use_ema = None + + def before_train_epoch(self, runner): + """Make emav2 model before run epoch.""" + if not hasattr(self, "use_ema"): + self.use_ema = len(runner.data_loader.dataset) > self.dataset_len_thr + + if self.use_ema and not hasattr(runner, "ema_model"): + model = runner.model + ema_model = ModelEmaV2(model, decay=self.ema_decay, dataset_len_thr=self.dataset_len_thr) + runner.ema_model = ema_model + + def before_run(self, runner): + """Log before run.""" + logger.info("\t* EMA V2 Enable") + + def after_train_iter(self, runner): + """Update ema parameter every self.interval iterations.""" + if not self.use_ema: + return + + if runner.iter % self.interval != 0: + # Skip update + return + + if runner.epoch < self.start_epoch: + # Just copy parameters before start epoch + return + + runner.ema_model.update() + + +class ModelEmaV2(nn.Module): + """Model Exponential Moving Average V2. + + Keep a moving average of everything in the model state_dict (parameters and buffers). + V2 of this module is simpler, it does not match params/buffers based on name but simply + iterates in order. It works with torchscript (JIT of full model). + This is intended to allow functionality like + https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage + A smoothed version of the weights is necessary for some training schemes to perform well. + E.g. Google's hyper-params for training MNASNet, MobileNet-V3, EfficientNet, etc that use + RMSprop with a short 2.4-3 epoch decay period and slow LR decay rate of .96-.99 requires EMA + smoothing of weights to match results. Pay attention to the decay constant you are using + relative to your update count per epoch. + To keep EMA from using GPU resources, set device='cpu'. This will save a bit of memory but + disable validation of the EMA weights. Validation will have to be done manually in a separate + process, or after the training stops converging. + This class is sensitive where it is initialized in the sequence of model init, + GPU assignment and distributed training wrappers. + """ + + def __init__(self, model, decay=0.9999, dataset_len_thr=None, device=None): + super().__init__() + # make a copy of the model for accumulating moving average of weights + self.module = deepcopy(model) + self.module.eval() + self.src_model = model.state_dict() + self.dst_model = self.module.state_dict() + self.decay = decay + self.device = device # perform ema on different device from model if set + self.dataset_len_thr = dataset_len_thr + if self.device is not None: + self.module.to(device=device) + + def forward(self): + """Forward.""" + return + + def _update(self, update_fn): + with torch.no_grad(): + for ema_v, model_v in zip(self.dst_model.values(), self.src_model.values()): + if self.device is not None: + model_v = model_v.to(device=self.device) + ema_v.copy_(update_fn(ema_v, model_v)) + + def update(self): + """Update.""" + self._update(update_fn=lambda e, m: self.decay * e + (1.0 - self.decay) * m) diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/no_bias_decay_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/no_bias_decay_hook.py new file mode 100644 index 00000000000..b930029a1ca --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/no_bias_decay_hook.py @@ -0,0 +1,73 @@ +"""Module for NoBiasDecayHook used in classification.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.runner import HOOKS, Hook +from torch import nn + +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class NoBiasDecayHook(Hook): + """Hook for No Bias Decay Method (Bag of Tricks for Image Classification). + + This hook divides model's weight & bias to 3 parameter groups + [weight with decay, weight without decay, bias without decay]. + """ + + def before_train_epoch(self, runner): + """Split weights into decay/no-decay groups.""" + weight_decay, bias_no_decay, weight_no_decay = [], [], [] + for module in runner.model.modules(): + if isinstance(module, (nn.Conv2d, nn.Linear)): + weight_decay.append(module.weight) + if module.bias is not None: + bias_no_decay.append(module.bias) + elif hasattr(module, "weight") or hasattr(module, "bias"): + if hasattr(module, "weight"): + weight_no_decay.append(module.weight) + if hasattr(module, "bias"): + bias_no_decay.append(module.bias) + elif len(list(module.children())) == 0: + for p in module.parameters(): + weight_decay.append(p) + + weight_decay_group = runner.optimizer.param_groups[0].copy() + weight_decay_group["params"] = weight_decay + + bias_group = runner.optimizer.param_groups[0].copy() + bias_group["params"] = bias_no_decay + bias_group["weight_decay"] = 0.0 + bias_group["lr"] *= 2 + + weight_no_decay_group = runner.optimizer.param_groups[0].copy() + weight_no_decay_group["params"] = weight_no_decay + weight_no_decay_group["weight_decay"] = 0.0 + + param_groups = [weight_decay_group, bias_group, weight_no_decay_group] + runner.optimizer.param_groups = param_groups + + def after_train_epoch(self, runner): + """Merge splited groups before saving checkpoint.""" + params = [] + for module in runner.model.modules(): + if isinstance(module, (nn.Conv2d, nn.Linear)): + params.append(module.weight) + if module.bias is not None: + params.append(module.bias) + elif hasattr(module, "weight") or hasattr(module, "bias"): + if hasattr(module, "weight"): + params.append(module.weight) + if hasattr(module, "bias"): + params.append(module.bias) + elif len(list(module.children())) == 0: + for p in module.parameters(): + params.append(p) + + param_groups = runner.optimizer.param_groups[0].copy() + param_groups["params"] = params + runner.optimizer.param_groups = [param_groups] diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/progress_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/progress_hook.py new file mode 100644 index 00000000000..03115f32f5b --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/progress_hook.py @@ -0,0 +1,91 @@ +"""Collections of hooks for common OTX algorithms.""" + +# Copyright (C) 2021-2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import math + +from mmcv.runner import BaseRunner +from mmcv.runner.hooks import HOOKS, Hook + +from otx.api.usecases.reporting.time_monitor_callback import TimeMonitorCallback +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class OTXProgressHook(Hook): + """OTXProgressHook for getting progress.""" + + def __init__(self, time_monitor: TimeMonitorCallback, verbose: bool = False): + super().__init__() + self.time_monitor = time_monitor + self.verbose = verbose + self.print_threshold = 1 + + def before_run(self, runner: BaseRunner): + """Called before_run in OTXProgressHook.""" + total_epochs = runner.max_epochs if runner.max_epochs is not None else 1 + self.time_monitor.total_epochs = total_epochs + self.time_monitor.train_steps = runner.max_iters // total_epochs if total_epochs else 1 + self.time_monitor.steps_per_epoch = self.time_monitor.train_steps + self.time_monitor.val_steps + self.time_monitor.total_steps = max(math.ceil(self.time_monitor.steps_per_epoch * total_epochs), 1) + self.time_monitor.current_step = 0 + self.time_monitor.current_epoch = 0 + self.time_monitor.on_train_begin() + + def before_epoch(self, runner: BaseRunner): + """Called before_epoch in OTXProgressHook.""" + self.time_monitor.on_epoch_begin(runner.epoch) + + def after_epoch(self, runner: BaseRunner): + """Called after_epoch in OTXProgressHook.""" + # put some runner's training status to use on the other hooks + runner.log_buffer.output["current_iters"] = runner.iter + self.time_monitor.on_epoch_end(runner.epoch, runner.log_buffer.output) + + def before_iter(self, runner: BaseRunner): + """Called before_iter in OTXProgressHook.""" + self.time_monitor.on_train_batch_begin(1) + + def after_iter(self, runner: BaseRunner): + """Called after_iter in OTXProgressHook.""" + # put some runner's training status to use on the other hooks + runner.log_buffer.output["current_iters"] = runner.iter + self.time_monitor.on_train_batch_end(1) + if self.verbose: + progress = self.progress + if progress >= self.print_threshold: + logger.info(f"training progress {progress:.0f}%") + self.print_threshold = (progress + 10) // 10 * 10 + + def before_val_iter(self, runner: BaseRunner): + """Called before_val_iter in OTXProgressHook.""" + self.time_monitor.on_test_batch_begin(1, logger) + + def after_val_iter(self, runner: BaseRunner): + """Called after_val_iter in OTXProgressHook.""" + self.time_monitor.on_test_batch_end(1, logger) + + def after_run(self, runner: BaseRunner): + """Called after_run in OTXProgressHook.""" + self.time_monitor.on_train_end(1) + if self.time_monitor.update_progress_callback: + self.time_monitor.update_progress_callback(int(self.time_monitor.get_progress())) + + @property + def progress(self): + """Getting Progress from time monitor.""" + return self.time_monitor.get_progress() diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/recording_forward_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/recording_forward_hook.py new file mode 100644 index 00000000000..9ba901f8fde --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/recording_forward_hook.py @@ -0,0 +1,379 @@ +"""Recording forward hooks for explain mode.""" +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from __future__ import annotations + +from abc import ABC +from typing import List, Optional, Sequence, Tuple, Union + +import numpy as np +import torch +from torch.nn import LayerNorm + +from otx.algorithms.classification import MMCLS_AVAILABLE +from otx.algorithms.common.utils.utils import cast_bf16_to_fp32 + +if MMCLS_AVAILABLE: + from mmcls.models.necks.gap import GlobalAveragePooling + + +class BaseRecordingForwardHook(ABC): + """While registered with the designated PyTorch module, this class caches feature vector during forward pass. + + Example:: + with BaseRecordingForwardHook(model.module.backbone) as hook: + with torch.no_grad(): + result = model(return_loss=False, **data) + print(hook.records) + + Args: + module (torch.nn.Module): The PyTorch module to be registered in forward pass + fpn_idx (int, optional): The layer index to be processed if the model is a FPN. + Defaults to 0 which uses the largest feature map from FPN. + normalize (bool): Whether to normalize the resulting saliency maps. + """ + + def __init__(self, module: torch.nn.Module, fpn_idx: int = -1, normalize: bool = True) -> None: + self._module = module + self._handle = None + self._records: List[torch.Tensor] = [] + self._fpn_idx = fpn_idx + self._norm_saliency_maps = normalize + + @property + def records(self): + """Return records.""" + return self._records + + def func(self, feature_map: torch.Tensor, fpn_idx: int = -1) -> torch.Tensor: + """This method get the feature vector or saliency map from the output of the module. + + Args: + feature_map (torch.Tensor): Feature map from the backbone module + fpn_idx (int, optional): The layer index to be processed if the model is a FPN. + Defaults to 0 which uses the largest feature map from FPN. + + Returns: + torch.Tensor (torch.Tensor): Saliency map for feature vector + """ + raise NotImplementedError + + def _recording_forward( + self, _: torch.nn.Module, x: torch.Tensor, output: torch.Tensor + ): # pylint: disable=unused-argument + tensors = self.func(output) + if isinstance(tensors, torch.Tensor): + tensors_np = cast_bf16_to_fp32(tensors).detach().cpu().numpy() + elif isinstance(tensors, np.ndarray): + tensors_np = tensors + else: + self._torch_to_numpy_from_list(tensors) + tensors_np = tensors + + for tensor in tensors_np: + self._records.append(tensor) + + def _torch_to_numpy_from_list(self, tensor_list: List[Optional[torch.Tensor]]): + for i in range(len(tensor_list)): + if isinstance(tensor_list[i], list): + self._torch_to_numpy_from_list(tensor_list[i]) + elif isinstance(tensor_list[i], torch.Tensor): + tensor_list[i] = tensor_list[i].detach().cpu().numpy() + + def __enter__(self) -> BaseRecordingForwardHook: + """Enter.""" + self._handle = self._module.backbone.register_forward_hook(self._recording_forward) + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Exit.""" + self._handle.remove() + + def _normalize_map(self, saliency_maps: torch.Tensor) -> torch.Tensor: + """Normalize saliency maps.""" + max_values, _ = torch.max(saliency_maps, -1) + min_values, _ = torch.min(saliency_maps, -1) + if len(saliency_maps.shape) == 2: + saliency_maps = 255 * (saliency_maps - min_values[:, None]) / (max_values - min_values + 1e-12)[:, None] + else: + saliency_maps = ( + 255 * (saliency_maps - min_values[:, :, None]) / (max_values - min_values + 1e-12)[:, :, None] + ) + return saliency_maps.to(torch.uint8) + + +class EigenCamHook(BaseRecordingForwardHook): + """EigenCamHook.""" + + def func(self, feature_map: Union[torch.Tensor, Sequence[torch.Tensor]], fpn_idx: int = -1) -> torch.Tensor: + """Generate the saliency map.""" + if isinstance(feature_map, (list, tuple)): + feature_map = feature_map[fpn_idx] + + x = feature_map.type(torch.float) + batch_size, channel, h, w = x.size() + reshaped_fmap = x.reshape((batch_size, channel, h * w)).transpose(1, 2) + reshaped_fmap = reshaped_fmap - reshaped_fmap.mean(1)[:, None, :] + _, _, vh = torch.linalg.svd(reshaped_fmap, full_matrices=True) # pylint: disable=invalid-name + + if self._norm_saliency_maps: + saliency_map = (reshaped_fmap @ vh[:, 0][:, :, None]).squeeze(-1) + self._normalize_map(saliency_map) + + saliency_map = saliency_map.reshape((batch_size, h, w)) + return saliency_map + + +class ActivationMapHook(BaseRecordingForwardHook): + """ActivationMapHook.""" + + def func(self, feature_map: Union[torch.Tensor, Sequence[torch.Tensor]], fpn_idx: int = -1) -> torch.Tensor: + """Generate the saliency map by average feature maps then normalizing to (0, 255).""" + if isinstance(feature_map, (list, tuple)): + assert fpn_idx < len( + feature_map + ), f"fpn_idx: {fpn_idx} is out of scope of feature_map length {len(feature_map)}!" + feature_map = feature_map[fpn_idx] + + batch_size, _, h, w = feature_map.size() + activation_map = torch.mean(feature_map, dim=1) + + if self._norm_saliency_maps: + activation_map = activation_map.reshape((batch_size, h * w)) + activation_map = self._normalize_map(activation_map) + + activation_map = activation_map.reshape((batch_size, h, w)) + return activation_map + + +class FeatureVectorHook(BaseRecordingForwardHook): + """FeatureVectorHook.""" + + @staticmethod + def func(feature_map: Union[torch.Tensor, Sequence[torch.Tensor]], fpn_idx: int = -1) -> torch.Tensor: + """Generate the feature vector by average pooling feature maps.""" + if isinstance(feature_map, (list, tuple)): + # aggregate feature maps from Feature Pyramid Network + feature_vector = [torch.nn.functional.adaptive_avg_pool2d(f, (1, 1)) for f in feature_map] + feature_vector = torch.cat(feature_vector, 1) + else: + feature_vector = torch.nn.functional.adaptive_avg_pool2d(feature_map, (1, 1)) + return feature_vector + + +class ViTFeatureVectorHook(BaseRecordingForwardHook): + """FeatureVectorHook for transformer-based classifiers.""" + + @staticmethod + def func(features: Tuple[List[torch.Tensor]], fpn_idx: int = -1) -> torch.Tensor: + """Generate the feature vector for transformer-based classifiers by returning the cls token.""" + _, cls_token = features[0] + return cls_token + + +class ReciproCAMHook(BaseRecordingForwardHook): + """Implementation of recipro-cam for class-wise saliency map. + + recipro-cam: gradient-free reciprocal class activation map (https://arxiv.org/pdf/2209.14074.pdf) + """ + + def __init__(self, module: torch.nn.Module, fpn_idx: int = -1) -> None: + super().__init__(module, fpn_idx) + self._neck = module.neck if module.with_neck else None + self._head = module.head + self._num_classes = module.head.num_classes + + def func(self, feature_map: Union[torch.Tensor, Sequence[torch.Tensor]], fpn_idx: int = -1) -> torch.Tensor: + """Generate the class-wise saliency maps using Recipro-CAM and then normalizing to (0, 255). + + Args: + feature_map (Union[torch.Tensor, List[torch.Tensor]]): feature maps from backbone or list of feature maps + from FPN. + fpn_idx (int, optional): The layer index to be processed if the model is a FPN. + Defaults to 0 which uses the largest feature map from FPN. + + Returns: + torch.Tensor: Class-wise Saliency Maps. One saliency map per each class - [batch, class_id, H, W] + """ + if isinstance(feature_map, (list, tuple)): + feature_map = feature_map[fpn_idx] + + batch_size, channel, h, w = feature_map.size() + saliency_maps = torch.empty(batch_size, self._num_classes, h, w) + for f in range(batch_size): + mosaic_feature_map = self._get_mosaic_feature_map(feature_map[f], channel, h, w) + mosaic_prediction = self._predict_from_feature_map(mosaic_feature_map) + saliency_maps[f] = mosaic_prediction.transpose(0, 1).reshape((self._num_classes, h, w)) + + if self._norm_saliency_maps: + saliency_maps = saliency_maps.reshape((batch_size, self._num_classes, h * w)) + saliency_maps = self._normalize_map(saliency_maps) + + saliency_maps = saliency_maps.reshape((batch_size, self._num_classes, h, w)) + return saliency_maps + + def _predict_from_feature_map(self, x: torch.Tensor) -> torch.Tensor: + with torch.no_grad(): + if self._neck is not None: + x = self._neck(x) + logits = self._head.simple_test(x) + if not isinstance(logits, torch.Tensor): + logits = torch.tensor(logits) + return logits + + def _get_mosaic_feature_map(self, feature_map: torch.Tensor, c: int, h: int, w: int) -> torch.Tensor: + if MMCLS_AVAILABLE and self._neck is not None and isinstance(self._neck, GlobalAveragePooling): + # Optimization workaround for the GAP case (simulate GAP with more simple compute graph) + # Possible due to static sparsity of mosaic_feature_map + # Makes the downstream GAP operation to be dummy + feature_map_transposed = torch.flatten(feature_map, start_dim=1).transpose(0, 1)[:, :, None, None] + mosaic_feature_map = feature_map_transposed / (h * w) + else: + feature_map_repeated = feature_map.repeat(h * w, 1, 1, 1) + mosaic_feature_map_mask = torch.zeros(h * w, c, h, w).to(feature_map.device) + spacial_order = torch.arange(h * w).reshape(h, w) + for i in range(h): + for j in range(w): + k = spacial_order[i, j] + mosaic_feature_map_mask[k, :, i, j] = torch.ones(c).to(feature_map.device) + mosaic_feature_map = feature_map_repeated * mosaic_feature_map_mask + return mosaic_feature_map + + +class ViTReciproCAMHook(BaseRecordingForwardHook): + """Implementation of ViTRecipro-CAM for class-wise saliency map for transformer-based classifiers. + + Args: + module (torch.nn.Module): The PyTorch module. + layer_index (int): Index of the target transformer_encoder layer. + use_gaussian (bool): Defines kernel type for mosaic feature map generation. + If True, use gaussian 3x3 kernel. If False, use 1x1 kernel. + cls_token (bool): If True, includes classification token into the mosaic feature map. + """ + + def __init__( + self, module: torch.nn.Module, layer_index: int = -1, use_gaussian: bool = True, cls_token: bool = True + ): + super().__init__(module) + self._layer_index = layer_index + self._target_layernorm = self._get_target_layernorm() + self._final_norm = module.backbone.norm1 if module.backbone.final_norm else None + self._neck = module.neck if module.with_neck else None + self._num_classes = module.head.num_classes + self._use_gaussian = use_gaussian + self._cls_token = cls_token + + def _get_target_layernorm(self) -> torch.nn.Module: + """Returns the first (out of two) layernorm layer from the layer_index backbone layer.""" + assert self._layer_index < 0, "negative index expected, e.g. -1 for the last layer." + layernorm_layers = [] + for module in self._module.backbone.modules(): + if isinstance(module, LayerNorm): + layernorm_layers.append(module) + assert len(layernorm_layers) == self._module.backbone.num_layers * 2 + int(self._module.backbone.final_norm) + target_layernorm_index = self._layer_index * 2 - int(self._module.backbone.final_norm) + return layernorm_layers[target_layernorm_index] + + def func(self, feature_map: torch.Tensor, _: int = -1) -> torch.Tensor: + """Generate the class-wise saliency maps using ViTRecipro-CAM and then normalizing to (0, 255). + + Args: + feature_map (torch.Tensor): feature maps from target layernorm layer. + + Returns: + torch.Tensor: Class-wise Saliency Maps. One saliency map per each class - [batch, class_id, H, W] + """ + batch_size, token_number, _ = feature_map.size() + h = w = int((token_number - 1) ** 0.5) + saliency_maps = torch.empty(batch_size, self._num_classes, h, w) + for i in range(batch_size): + mosaic_feature_map = self._get_mosaic_feature_map(feature_map[i]) + mosaic_prediction = self._predict_from_feature_map(mosaic_feature_map) + saliency_maps[i] = mosaic_prediction.transpose(1, 0).reshape((self._num_classes, h, w)) + + if self._norm_saliency_maps: + saliency_maps = saliency_maps.reshape((batch_size, self._num_classes, h * w)) + saliency_maps = self._normalize_map(saliency_maps) + + saliency_maps = saliency_maps.reshape((batch_size, self._num_classes, h, w)) + return saliency_maps + + def _get_mosaic_feature_map(self, feature_map: torch.Tensor) -> torch.Tensor: + token_number, dim = feature_map.size() + mosaic_feature_map = torch.zeros(token_number - 1, token_number, dim).to(feature_map.device) + h = w = int((token_number - 1) ** 0.5) + + if self._use_gaussian: + if self._cls_token: + mosaic_feature_map[:, 0, :] = feature_map[0, :] + feature_map_spacial = feature_map[1:, :].reshape(1, h, w, dim) + feature_map_spacial_repeated = feature_map_spacial.repeat(h * w, 1, 1, 1) # 196, 14, 14, 192 + + spacial_order = torch.arange(h * w).reshape(h, w) + gaussian = torch.tensor( + [[1 / 16.0, 1 / 8.0, 1 / 16.0], [1 / 8.0, 1 / 4.0, 1 / 8.0], [1 / 16.0, 1 / 8.0, 1 / 16.0]] + ).to(feature_map.device) + mosaic_feature_map_mask_padded = torch.zeros(h * w, h + 2, w + 2).to(feature_map.device) + for i in range(h): + for j in range(w): + k = spacial_order[i, j] + i_pad = i + 1 + j_pad = j + 1 + mosaic_feature_map_mask_padded[k, i_pad - 1 : i_pad + 2, j_pad - 1 : j_pad + 2] = gaussian + mosaic_feature_map_mask = mosaic_feature_map_mask_padded[:, 1:-1, 1:-1] + mosaic_feature_map_mask = torch.tensor(mosaic_feature_map_mask.unsqueeze(3).repeat(1, 1, 1, dim)) + + mosaic_fm_wo_cls_token = feature_map_spacial_repeated * mosaic_feature_map_mask + mosaic_feature_map[:, 1:, :] = mosaic_fm_wo_cls_token.reshape(h * w, h * w, dim) + else: + feature_map_repeated = feature_map.unsqueeze(0).repeat(h * w, 1, 1) + mosaic_feature_map_mask = torch.zeros(h * w, token_number).to(feature_map.device) + for i in range(h * w): + mosaic_feature_map_mask[i, i + 1] = torch.ones(1).to(feature_map.device) + if self._cls_token: + mosaic_feature_map_mask[:, 0] = torch.ones(1).to(feature_map.device) + mosaic_feature_map_mask = torch.tensor(mosaic_feature_map_mask.unsqueeze(2).repeat(1, 1, dim)) + mosaic_feature_map = feature_map_repeated * mosaic_feature_map_mask + + return mosaic_feature_map + + def _predict_from_feature_map(self, x: torch.Tensor) -> torch.Tensor: + with torch.no_grad(): + # Part of the target transformer_encoder layer (except first LayerNorm) + target_layer = self._module.backbone.layers[self._layer_index] + x = x + target_layer.attn(x) + x = target_layer.ffn(target_layer.norm2(x), identity=x) + + # Rest transformer_encoder layers, if not the last one picked as a target + if self._layer_index < -1: + for layer in self._module.backbone.layers[(self._layer_index + 1) :]: + x = layer(x) + + if self._final_norm: + x = self._final_norm(x) + if self._neck: + x = self._neck(x) + + cls_token = x[:, 0] + layer_output = [None, cls_token] + logit = self._module.head.simple_test(layer_output) + if isinstance(logit, list): + logit = torch.from_numpy(np.array(logit)) + return logit + + def __enter__(self) -> BaseRecordingForwardHook: + """Enter.""" + self._handle = self._target_layernorm.register_forward_hook(self._recording_forward) + return self diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/sam_optimizer_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/sam_optimizer_hook.py new file mode 100644 index 00000000000..ffa08b12ae8 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/sam_optimizer_hook.py @@ -0,0 +1,99 @@ +"""This module contains the Sharpness-aware Minimization optimizer hook implementation for MMCV Runners.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcv.runner import HOOKS, OptimizerHook + + +@HOOKS.register_module() +class SAMOptimizerHook(OptimizerHook): + """Sharpness-aware Minimization optimizer hook. + + Implemented as OptimizerHook for MMCV Runners + - Paper ref: https://arxiv.org/abs/2010.01412 + - code ref: https://github.com/davda54/sam + """ + + def __init__(self, rho=0.05, start_epoch=1, **kwargs): + super().__init__(**kwargs) + self.rho = rho + self.start_epoch = start_epoch + if rho < 0.0: + raise ValueError("rho should be greater than 0 for SAM optimizer") + + def after_train_iter(self, runner): + """Perform SAM optimization. + + 0. compute current loss (DONE IN model.train_step()) + 1. compute current gradient + 2. move param to the approximate local maximum: w + e(w) = w + rho*norm_grad + 3. compute maximum loss + 4. compute SAM gradient on maximum loss + 5. restore parram to original param + 6. update param using SAM gradient + + Assuming model.current_batch had been set in model.train_step() + """ + current_batch = self._get_current_batch(runner.model) + if current_batch is None or runner.epoch + 1 < self.start_epoch: + # Apply original parameter update + return super().after_train_iter(runner) + + # Current gradient + runner.optimizer.zero_grad() + curr_loss = runner.outputs["loss"] + curr_loss.backward() + + # Move to local maximum + param2move = {} + with torch.no_grad(): + scale = self.rho / (self._grad_norm(runner.optimizer) + 1e-12) + for param_group in runner.optimizer.param_groups: + for param in param_group["params"]: + if param.grad is None: + continue + e_w = param.grad * scale.to(param) + param.add_(e_w) # Climb toward gradient (increasing loss) + param2move[param] = e_w # Saving for param restore + + # SAM gradient + runner.optimizer.zero_grad() + max_outputs = runner.model.train_step(current_batch, runner.optimizer) # forward() on maxima param + max_loss = max_outputs["loss"] + max_loss.backward() + + # Restore to original param + with torch.no_grad(): + for param_group in runner.optimizer.param_groups: + for param in param_group["params"]: + if param.grad is None: + continue + param.sub_(param2move[param]) # Down to original param + + # Shaprpness-aware param update + runner.optimizer.step() # param -= lr * sam_grad + runner.log_buffer.update({"sharpness": float(max_loss - curr_loss), "max_loss": float(max_loss)}) + return None + + def _get_current_batch(self, model): + if hasattr(model, "module"): + model = model.module + return getattr(model, "current_batch", None) + + def _grad_norm(self, optimizer): + # put everything on the same device, in case of model parallelism + shared_device = optimizer.param_groups[0]["params"][0].device + norm = torch.norm( + torch.stack( + [ + p.grad.norm(p=2).to(shared_device) + for group in optimizer.param_groups + for p in group["params"] + if p.grad is not None + ] + ), + p=2, + ) + return norm diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/semisl_cls_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/semisl_cls_hook.py new file mode 100644 index 00000000000..178e3c9b3aa --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/semisl_cls_hook.py @@ -0,0 +1,63 @@ +"""Module for defining hook for semi-supervised learning for classification task.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import math + +from mmcv.parallel import is_module_wrapper +from mmcv.runner import HOOKS, Hook + + +@HOOKS.register_module() +class SemiSLClsHook(Hook): + """Hook for SemiSL for classification. + + This hook includes unlabeled warm-up loss coefficient (default: True): + unlabeled_coef = (0.5 - cos(min(pi, 2 * pi * k) / K)) / 2 + k: current step, K: total steps + Also, this hook adds semi-sl-related data to the log (unlabeled_coef, pseudo_label) + + Args: + total_steps (int): total steps for training (iteration) + Raise the coefficient from 0 to 1 during half the duration of total_steps + default: 0, use runner.max_iters + unlabeled_warmup (boolean): enable unlabeled warm-up loss coefficient + If False, Semi-SL uses 1 as unlabeled loss coefficient + """ + + def __init__(self, total_steps=0, unlabeled_warmup=True): + self.unlabeled_warmup = unlabeled_warmup + self.total_steps = total_steps + self.current_step, self.unlabeled_coef = 0, 0 + self.num_pseudo_label = 0 + + def before_train_iter(self, runner): + """Calculate the unlabeled warm-up loss coefficient before training iteration.""" + if self.unlabeled_warmup and self.unlabeled_coef < 1.0: + if self.total_steps == 0: + self.total_steps = runner.max_iters + self.unlabeled_coef = 0.5 * ( + 1 - math.cos(min(math.pi, (2 * math.pi * self.current_step) / self.total_steps)) + ) + model = self._get_model(runner) + model.head.unlabeled_coef = self.unlabeled_coef + self.current_step += 1 + + def after_train_iter(self, runner): + """Add the number of pseudo-labels correctly selected from iteration.""" + model = self._get_model(runner) + self.num_pseudo_label += int(model.head.num_pseudo_label) + + def after_epoch(self, runner): + """Add data related to Semi-SL to the log.""" + if self.unlabeled_warmup: + runner.log_buffer.output.update({"unlabeled_coef": round(self.unlabeled_coef, 4)}) + runner.log_buffer.output.update({"pseudo_label": self.num_pseudo_label}) + self.num_pseudo_label = 0 + + def _get_model(self, runner): + model = runner.model + if is_module_wrapper(model): + model = model.module + return model diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/task_adapt_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/task_adapt_hook.py new file mode 100644 index 00000000000..d3e4f9ad68b --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/task_adapt_hook.py @@ -0,0 +1,93 @@ +"""Task adapt hook which selects a proper sampler.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.runner import HOOKS, Hook, get_dist_info +from torch.utils.data import DataLoader + +from otx.algorithms.common.adapters.torch.dataloaders.samplers import ( + BalancedSampler, + ClsIncrSampler, + OTXSampler, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class TaskAdaptHook(Hook): + """Task Adaptation Hook for Task-Inc & Class-Inc. + + Args: + src_classes (list): A list of old classes used in the existing model + dst_classes (list): A list of classes including new_classes to be newly learned + model_type (str): Types of models used for learning + sampler_flag (bool): Flag about using ClsIncrSampler + efficient_mode (bool): Flag about using efficient mode sampler + use_adaptive_repeat (bool): Flag about using adaptive repeat data + """ + + def __init__( + self, + src_classes, + dst_classes, + model_type="FasterRCNN", + sampler_flag=False, + sampler_type="cls_incr", + efficient_mode=False, + ): + self.src_classes = src_classes + self.dst_classes = dst_classes + self.model_type = model_type + self.sampler_flag = sampler_flag + self.sampler_type = sampler_type + self.efficient_mode = efficient_mode + + logger.info(f"Task Adaptation: {self.src_classes} => {self.dst_classes}") + logger.info(f"- Efficient Mode: {self.efficient_mode}") + logger.info(f"- Sampler type: {self.sampler_type}") + logger.info(f"- Sampler flag: {self.sampler_flag}") + + def before_epoch(self, runner): + """Produce a proper sampler for task-adaptation.""" + if self.sampler_flag: + dataset = runner.data_loader.dataset + batch_size = runner.data_loader.batch_size + num_workers = runner.data_loader.num_workers + collate_fn = runner.data_loader.collate_fn + worker_init_fn = runner.data_loader.worker_init_fn + rank, world_size = get_dist_info() + + if isinstance(runner.data_loader.sampler, OTXSampler): + repeat = runner.data_loader.sampler.repeat + else: + repeat = 1 + if self.sampler_type == "balanced": + sampler = BalancedSampler( + dataset, + batch_size, + efficient_mode=self.efficient_mode, + num_replicas=world_size, + rank=rank, + n_repeats=repeat, + ) + else: + sampler = ClsIncrSampler( + dataset, + batch_size, + efficient_mode=self.efficient_mode, + num_replicas=world_size, + rank=rank, + n_repeats=repeat, + ) + runner.data_loader = DataLoader( + dataset, + batch_size=batch_size, + sampler=sampler, + num_workers=num_workers, + collate_fn=collate_fn, + pin_memory=False, + worker_init_fn=worker_init_fn, + ) diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/two_crop_transform_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/two_crop_transform_hook.py new file mode 100644 index 00000000000..d225e3b44e4 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/two_crop_transform_hook.py @@ -0,0 +1,86 @@ +"""Two crop transform hook.""" +from typing import List + +from mmcv.runner import BaseRunner +from mmcv.runner.hooks import HOOKS, Hook + +from otx.utils.logger import get_logger + +logger = get_logger() + + +@HOOKS.register_module() +class TwoCropTransformHook(Hook): + """TwoCropTransformHook with every specific interval. + + This hook decides whether using single pipeline or two pipelines + implemented in `TwoCropTransform` for the current iteration. + + Args: + interval (int): If `interval` == 1, both pipelines is used. + If `interval` > 1, the first pipeline is used and then + both pipelines are used every `interval`. Defaults to 1. + by_epoch (bool): (TODO) Use `interval` by epoch. Defaults to False. + """ + + def __init__(self, interval: int = 1, by_epoch: bool = False): + assert interval > 0, f"interval (={interval}) must be positive value." + if by_epoch: + raise NotImplementedError("by_epoch is not implemented.") + + self.interval = interval + self.cnt = 0 + + def _get_dataset(self, runner: BaseRunner): + """Get dataset to handle `is_both`.""" + if hasattr(runner.data_loader.dataset, "dataset"): + # for RepeatDataset + dataset = runner.data_loader.dataset.dataset + else: + dataset = runner.data_loader.dataset + + return dataset + + # pylint: disable=inconsistent-return-statements + def _find_two_crop_transform(self, transforms: List[object]): + """Find TwoCropTransform among transforms.""" + for transform in transforms: + if transform.__class__.__name__ == "TwoCropTransform": + return transform + + def before_train_epoch(self, runner: BaseRunner): + """Called before_train_epoch in TwoCropTransformHook.""" + # Always keep `TwoCropTransform` enabled. + if self.interval == 1: + return + + dataset = self._get_dataset(runner) + two_crop_transform = self._find_two_crop_transform(dataset.pipeline.transforms) + if self.cnt == self.interval - 1: + # start using both pipelines + two_crop_transform.is_both = True + else: + two_crop_transform.is_both = False + + def after_train_iter(self, runner: BaseRunner): + """Called after_train_iter in TwoCropTransformHook.""" + # Always keep `TwoCropTransform` enabled. + if self.interval == 1: + return + + if self.cnt < self.interval - 1: + # Instead of using `runner.every_n_iters` or `runner.every_n_inner_iters`, + # this condition is used to compare `self.cnt` with `self.interval` throughout the entire epochs. + self.cnt += 1 + + if self.cnt == self.interval - 1: + dataset = self._get_dataset(runner) + two_crop_transform = self._find_two_crop_transform(dataset.pipeline.transforms) + if not two_crop_transform.is_both: + # If `self.cnt` == `self.interval`-1, there are two cases, + # 1. `self.cnt` was updated in L709, so `is_both` must be on for the next iter. + # 2. if the current iter was already conducted, `is_both` must be off. + two_crop_transform.is_both = True + else: + two_crop_transform.is_both = False + self.cnt = 0 diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/xpu_optimizer_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/xpu_optimizer_hook.py new file mode 100644 index 00000000000..2f3bd5d944a --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/xpu_optimizer_hook.py @@ -0,0 +1,38 @@ +"""Custom Optimizer Hook for mixed precision training on XPU.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from typing import Optional, Union + +from mmcv.runner.hooks import HOOKS, Fp16OptimizerHook + +from otx.algorithms.common.adapters.torch.amp import XPUGradScaler + + +@HOOKS.register_module() +class BFp16XPUOptimizerHook(Fp16OptimizerHook): + """Custom Optimizer Hook for mixed & lower precision training on XPU.""" + + def __init__( + self, + grad_clip: Optional[dict] = None, + coalesce: bool = True, + bucket_size_mb: int = -1, + loss_scale: Union[float, str, dict] = 512.0, + distributed: bool = True, + ) -> None: + self.grad_clip = grad_clip + self.coalesce = coalesce + self.bucket_size_mb = bucket_size_mb + self.distributed = distributed + self._scale_update_param = None + if loss_scale == "dynamic": + self.loss_scaler = XPUGradScaler() + elif isinstance(loss_scale, float): + self._scale_update_param = loss_scale + self.loss_scaler = XPUGradScaler(init_scale=loss_scale) + elif isinstance(loss_scale, dict): + self.loss_scaler = XPUGradScaler(**loss_scale) + else: + raise ValueError("loss_scale must be of type float, dict, or " f'"dynamic", got {loss_scale}') diff --git a/src/otx/algorithms/common/adapters/mmcv/models/__init__.py b/src/otx/algorithms/common/adapters/mmcv/models/__init__.py new file mode 100644 index 00000000000..b285c3163dc --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/models/__init__.py @@ -0,0 +1,21 @@ +"""Adapters for OTX Common Algorithm. - mmcv.model.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +from .backbones import OTXEfficientNet, OTXEfficientNetV2, OTXMobileNetV3 +from .builder import BACKBONES, TORCHVISION_BACKBONES + +__all__ = ["OTXEfficientNet", "OTXEfficientNetV2", "OTXMobileNetV3", "BACKBONES", "TORCHVISION_BACKBONES"] diff --git a/src/otx/algorithms/common/adapters/mmcv/models/backbones/__init__.py b/src/otx/algorithms/common/adapters/mmcv/models/backbones/__init__.py new file mode 100644 index 00000000000..0bf0e19c101 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/models/backbones/__init__.py @@ -0,0 +1,22 @@ +"""OTX Custom backbones.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from . import torchvision_backbones # noqa: F401 +from .efficientnet import OTXEfficientNet +from .efficientnetv2 import OTXEfficientNetV2 +from .mobilenetv3 import OTXMobileNetV3 + +__all__ = ["OTXEfficientNet", "OTXEfficientNetV2", "OTXMobileNetV3"] diff --git a/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnet.py b/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnet.py new file mode 100644 index 00000000000..ce341d13a87 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnet.py @@ -0,0 +1,1313 @@ +"""Implementation of EfficientNet. + +Original papers: +- 'EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks,' https://arxiv.org/abs/1905.11946, +- 'Adversarial Examples Improve Image Recognition,' https://arxiv.org/abs/1911.09665. +""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=too-many-arguments, invalid-name, unused-argument, too-many-lines +# pylint: disable=too-many-instance-attributes,too-many-statements, too-many-branches, too-many-locals + +import math +import os + +import torch.nn.functional as F +from mmcv.cnn import build_activation_layer +from mmcv.cnn.bricks import ConvModule +from mmcv.runner import load_checkpoint +from pytorchcv.models.model_store import download_model +from torch import nn +from torch.nn import init + +from otx.utils.logger import get_logger + +from ..builder import BACKBONES + +logger = get_logger() + +PRETRAINED_ROOT = "https://github.com/osmr/imgclsmob/releases/download/v0.0.364/" +pretrained_urls = { + "efficientnet_b0": PRETRAINED_ROOT + "efficientnet_b0-0752-0e386130.pth.zip", +} + + +def conv1x1_block( + in_channels, + out_channels, + stride=1, + padding=0, + groups=1, + bias=False, + use_bn=True, + bn_eps=1e-5, + activation="ReLU", +): + """Conv block.""" + + return ConvModule( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=stride, + padding=padding, + groups=groups, + bias=bias, + norm_cfg=(dict(type="BN", eps=bn_eps) if use_bn else None), + act_cfg=(dict(type=activation) if activation else None), + ) + + +def conv3x3_block( + in_channels, + out_channels, + stride=1, + padding=1, + dilation=1, + groups=1, + bias=False, + use_bn=True, + bn_eps=1e-5, + activation="ReLU", + IN_conv=False, +): + """Conv block.""" + + return ConvModule( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + padding=padding, + dilation=dilation, + groups=groups, + bias=bias, + norm_cfg=(dict(type="BN", eps=bn_eps) if use_bn else None), + act_cfg=(dict(type=activation) if activation else None), + ) + + +def dwconv3x3_block( + in_channels, + out_channels, + stride=1, + padding=1, + dilation=1, + bias=False, + use_bn=True, + bn_eps=1e-5, + activation="ReLU", +): + """Conv block.""" + + return ConvModule( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + padding=padding, + dilation=dilation, + groups=out_channels, + bias=bias, + norm_cfg=(dict(type="BN", eps=bn_eps) if use_bn else None), + act_cfg=(dict(type=activation) if activation else None), + ) + + +def dwconv5x5_block( + in_channels, + out_channels, + stride=1, + padding=2, + dilation=1, + bias=False, + use_bn=True, + bn_eps=1e-5, + activation="ReLU", +): + """Conv block.""" + return ConvModule( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=5, + stride=stride, + padding=padding, + dilation=dilation, + groups=out_channels, + bias=bias, + norm_cfg=(dict(type="BN", eps=bn_eps) if use_bn else None), + act_cfg=(dict(type=activation) if activation else None), + ) + + +def round_channels(channels, divisor=8): + """Round weighted channel number (make divisible operation). + + Args: + channels : int or float. Original number of channels. + divisor : int, default 8. Alignment value. + """ + rounded_channels = max(int(channels + divisor / 2.0) // divisor * divisor, divisor) + if float(rounded_channels) < 0.9 * channels: + rounded_channels += divisor + return rounded_channels + + +def calc_tf_padding(x, kernel_size, stride=1, dilation=1): + """Calculate TF-same like padding size. + + Args: + x : tensor. Input tensor. + kernel_size : int. Convolution window size. + stride : int, default 1. Strides of the convolution. + dilation : int, default 1. Dilation value for convolution layer. + """ + height, width = x.size()[2:] + oh = math.ceil(height / stride) + ow = math.ceil(width / stride) + pad_h = max((oh - 1) * stride + (kernel_size - 1) * dilation + 1 - height, 0) + pad_w = max((ow - 1) * stride + (kernel_size - 1) * dilation + 1 - width, 0) + return pad_h // 2, pad_h - pad_h // 2, pad_w // 2, pad_w - pad_w // 2 + + +class SEBlock(nn.Module): + """Squeeze-and-Excitation block from 'Squeeze-and-Excitation Networks,'. + + https://arxiv.org/abs/1709.01507. + + Args: + channels : int. Number of channels. + reduction : int, default 16. Squeeze reduction value. + mid_channels : int or None, default None. Number of middle channels. + round_mid : bool, default False. Whether to round middle channel number (make divisible by 8). + use_conv : bool, default True. Whether to convolutional layers instead of fully-connected ones. + activation : function, or str, or nn.Module, default 'relu'. Activation function after the first convolution. + out_activation : function, or str, or nn.Module, Activation function after the last convolution. + """ + + def __init__( + self, + channels, + reduction=16, + mid_channels=None, + round_mid=False, + use_conv=True, + mid_activation="ReLU", + out_activation="Sigmoid", + ): + super().__init__() + self.use_conv = use_conv + if mid_channels is None: + mid_channels = channels // reduction if not round_mid else round_channels(float(channels) / reduction) + + self.pool = nn.AdaptiveAvgPool2d(output_size=1) + if use_conv: + self.conv1 = nn.Conv2d( + in_channels=channels, + out_channels=mid_channels, + kernel_size=1, + stride=1, + groups=1, + bias=True, + ) + else: + self.fc1 = nn.Linear(in_features=channels, out_features=mid_channels) + self.activ = build_activation_layer(dict(type=mid_activation)) + if use_conv: + self.conv2 = nn.Conv2d( + in_channels=mid_channels, + out_channels=channels, + kernel_size=1, + stride=1, + groups=1, + bias=True, + ) + else: + self.fc2 = nn.Linear(in_features=mid_channels, out_features=channels) + self.sigmoid = build_activation_layer(dict(type=out_activation)) + + def forward(self, x): + """Forward.""" + w = self.pool(x) + if not self.use_conv: + w = w.view(x.size(0), -1) + w = self.conv1(w) if self.use_conv else self.fc1(w) + w = self.activ(w) + w = self.conv2(w) if self.use_conv else self.fc2(w) + w = self.sigmoid(w) + if not self.use_conv: + w = w.unsqueeze(2).unsqueeze(3) + x = x * w + return x + + +class EffiDwsConvUnit(nn.Module): + """EfficientNet specific depthwise separable conv block/unit with BatchNorms and activations at each conv. + + Args: + in_channels : int. Number of input channels. + out_channels : int. Number of output channels. + stride : int or tuple/list of 2 int. Strides of the second convolution layer. + bn_eps : float. Small float added to variance in Batch norm. + activation : str. Name of activation function. + tf_mode : bool. Whether to use TF-like mode. + """ + + def __init__(self, in_channels, out_channels, stride, bn_eps, activation, tf_mode): + super().__init__() + self.tf_mode = tf_mode + self.residual = (in_channels == out_channels) and (stride == 1) + + self.dw_conv = dwconv3x3_block( + in_channels=in_channels, + out_channels=in_channels, + padding=(0 if tf_mode else 1), + bn_eps=bn_eps, + activation=activation, + ) + self.se = SEBlock(channels=in_channels, reduction=4, mid_activation=activation) + self.pw_conv = conv1x1_block( + in_channels=in_channels, + out_channels=out_channels, + bn_eps=bn_eps, + activation=None, + ) + + def forward(self, x): + """Forward.""" + if self.residual: + identity = x + if self.tf_mode: + x = F.pad(x, pad=calc_tf_padding(x, kernel_size=3)) + x = self.dw_conv(x) + x = self.se(x) + x = self.pw_conv(x) + if self.residual: + x = x + identity + return x + + +class EffiInvResUnit(nn.Module): + """EfficientNet inverted residual unit. + + Args: + in_channels : int. Number of input channels. + out_channels : int. Number of output channels. + kernel_size : int or tuple/list of 2 int. Convolution window size. + stride : int or tuple/list of 2 int. Strides of the second convolution layer. + exp_factor : int. Factor for expansion of channels. + se_factor : int. SE reduction factor for each unit. + bn_eps : float. Small float added to variance in Batch norm. + activation : str. Name of activation function. + tf_mode : bool. Whether to use TF-like mode. + """ + + def __init__( + self, + in_channels, + out_channels, + kernel_size, + stride, + exp_factor, + se_factor, + bn_eps, + activation, + tf_mode, + ): + super().__init__() + self.kernel_size = kernel_size + self.stride = stride + self.tf_mode = tf_mode + self.residual = (in_channels == out_channels) and (stride == 1) + self.use_se = se_factor > 0 + mid_channels = in_channels * exp_factor + dwconv_block_fn = dwconv3x3_block if kernel_size == 3 else (dwconv5x5_block if kernel_size == 5 else None) + + self.conv1 = conv1x1_block( + in_channels=in_channels, + out_channels=mid_channels, + bn_eps=bn_eps, + activation=activation, + ) + self.conv2 = dwconv_block_fn( + in_channels=mid_channels, + out_channels=mid_channels, + stride=stride, + padding=(0 if tf_mode else kernel_size // 2), + bn_eps=bn_eps, + activation=activation, + ) + if self.use_se: + self.se = SEBlock( + channels=mid_channels, + reduction=(exp_factor * se_factor), + mid_activation=activation, + ) + self.conv3 = conv1x1_block( + in_channels=mid_channels, + out_channels=out_channels, + bn_eps=bn_eps, + activation=None, + ) + + def forward(self, x): + """Forward.""" + if self.residual: + identity = x + x = self.conv1(x) + if self.tf_mode: + x = F.pad( + x, + pad=calc_tf_padding(x, kernel_size=self.kernel_size, stride=self.stride), + ) + x = self.conv2(x) + if self.use_se: + x = self.se(x) + x = self.conv3(x) + if self.residual: + x = x + identity + return x + + +class EffiInitBlock(nn.Module): + """EfficientNet specific initial block. + + Args: + in_channels : int. Number of input channels. + out_channels : int. Number of output channels. + bn_eps : float. Small float added to variance in Batch norm. + activation : str. Name of activation function. + tf_mode : bool. Whether to use TF-like mode. + """ + + def __init__(self, in_channels, out_channels, bn_eps, activation, tf_mode, IN_conv1): + super().__init__() + self.tf_mode = tf_mode + + self.conv = conv3x3_block( + in_channels=in_channels, + out_channels=out_channels, + stride=2, + padding=(0 if tf_mode else 1), + bn_eps=bn_eps, + activation=activation, + IN_conv=IN_conv1, + ) + + def forward(self, x): + """Forward.""" + if self.tf_mode: + x = F.pad(x, pad=calc_tf_padding(x, kernel_size=3, stride=2)) + x = self.conv(x) + return x + + +class EfficientNet(nn.Module): + """EfficientNet. + + Args: + channels : list of list of int. Number of output channels for each unit. + init_block_channels : int. Number of output channels for initial unit. + final_block_channels : int. Number of output channels for the final block of the feature extractor. + kernel_sizes : list of list of int. Number of kernel sizes for each unit. + strides_per_stage : list int. Stride value for the first unit of each stage. + expansion_factors : list of list of int. Number of expansion factors for each unit. + dropout_rate : float, default 0.2. Fraction of the input units to drop. Must be a number between 0 and 1. + tf_mode : bool, default False. Whether to use TF-like mode. + bn_eps : float, default 1e-5. Small float added to variance in Batch norm. + in_channels : int, default 3. Number of input channels. + in_size : tuple of two ints, default (224, 224). Spatial size of the expected input image. + num_classes : int, default 1000. Number of classification classes. + """ + + def __init__( + self, + channels, + init_block_channels, + final_block_channels, + kernel_sizes, + strides_per_stage, + expansion_factors, + tf_mode=False, + bn_eps=1e-5, + in_channels=3, + in_size=(224, 224), + dropout_cls=None, + pooling_type="avg", + bn_eval=False, + bn_frozen=False, + IN_first=False, + IN_conv1=False, + pretrained=False, + **kwargs, + ): + + super().__init__(**kwargs) + self.num_classes = 1000 + self.pretrained = pretrained + self.in_size = in_size + self.input_IN = nn.InstanceNorm2d(3, affine=True) if IN_first else None + self.bn_eval = bn_eval + self.bn_frozen = bn_frozen + self.pooling_type = pooling_type + self.num_features = self.num_head_features = final_block_channels + activation = "Swish" + self.features = nn.Sequential() + self.features.add_module( + "init_block", + EffiInitBlock( + in_channels=in_channels, + out_channels=init_block_channels, + bn_eps=bn_eps, + activation=activation, + tf_mode=tf_mode, + IN_conv1=IN_conv1, + ), + ) + in_channels = init_block_channels + for i, channels_per_stage in enumerate(channels): + kernel_sizes_per_stage = kernel_sizes[i] + expansion_factors_per_stage = expansion_factors[i] + stage = nn.Sequential() + for j, out_channels in enumerate(channels_per_stage): + kernel_size = kernel_sizes_per_stage[j] + expansion_factor = expansion_factors_per_stage[j] + stride = strides_per_stage[i] if (j == 0) else 1 + if i == 0: + stage.add_module( + f"unit{j + 1}", + EffiDwsConvUnit( + in_channels=in_channels, + out_channels=out_channels, + stride=stride, + bn_eps=bn_eps, + activation=activation, + tf_mode=tf_mode, + ), + ) + else: + stage.add_module( + f"unit{j + 1}", + EffiInvResUnit( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + exp_factor=expansion_factor, + se_factor=4, + bn_eps=bn_eps, + activation=activation, + tf_mode=tf_mode, + ), + ) + in_channels = out_channels + self.features.add_module(f"stage{i+1}", stage) + # activation = activation if self.loss == 'softmax': else lambda: nn.PReLU(init=0.25) + self.features.add_module( + "final_block", + conv1x1_block( + in_channels=in_channels, out_channels=final_block_channels, bn_eps=bn_eps, activation=activation + ), + ) + self._init_params() + + def _init_params(self): + for module in self.named_modules(): + if isinstance(module, nn.Conv2d): + init.kaiming_uniform_(module.weight) + if module.bias is not None: + init.constant_(module.bias, 0) + + def forward(self, x, return_featuremaps=False, get_embeddings=False): + """Forward.""" + if self.input_IN is not None: + x = self.input_IN(x) # pylint: disable=not-callable + + y = self.features(x) + if return_featuremaps: + return y + + glob_features = self._glob_feature_vector(y, self.pooling_type, reduce_dims=False) + + logits = self.output(glob_features.view(x.shape[0], -1)) + + if not self.training and self.classification: + return [logits] + + if get_embeddings: + out_data = [logits, glob_features.view(x.shape[0], -1)] + elif self.loss in ["softmax", "am_softmax"]: + if self.lr_finder.enable and self.lr_finder.mode == "automatic": + out_data = logits + else: + out_data = [logits] + + elif self.loss in ["triplet"]: + out_data = [logits, glob_features] + else: + raise KeyError(f"Unsupported loss: {self.loss}") + + if self.lr_finder.enable and self.lr_finder.mode == "automatic": + return out_data + return tuple(out_data) + + +def get_efficientnet( + version, + in_size, + tf_mode=False, + bn_eps=1e-5, + model_name=None, + pretrained=False, + root=os.path.join("~", ".torch", "models"), + **kwargs, +): + """Create EfficientNet model with specific parameters. + + Args: + version : str. Version of EfficientNet ('b0'...'b8'). + in_size : tuple of two ints. Spatial size of the expected input image. + tf_mode : bool, default False. Whether to use TF-like mode. + bn_eps : float, default 1e-5. Small float added to variance in Batch norm. + model_name : str or None, default None. Model name for loading pretrained model. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + if version == "b0": + assert in_size == (224, 224) + depth_factor = 1.0 + width_factor = 1.0 + dropout_rate = 0.2 + elif version == "b1": + assert in_size == (240, 240) + depth_factor = 1.1 + width_factor = 1.0 + dropout_rate = 0.2 + elif version == "b2": + assert in_size == (260, 260) + depth_factor = 1.2 + width_factor = 1.1 + dropout_rate = 0.3 + elif version == "b3": + assert in_size == (300, 300) + depth_factor = 1.4 + width_factor = 1.2 + dropout_rate = 0.3 + elif version == "b4": + assert in_size == (380, 380) + depth_factor = 1.8 + width_factor = 1.4 + dropout_rate = 0.4 + elif version == "b5": + assert in_size == (456, 456) + depth_factor = 2.2 + width_factor = 1.6 + dropout_rate = 0.4 + elif version == "b6": + assert in_size == (528, 528) + depth_factor = 2.6 + width_factor = 1.8 + dropout_rate = 0.5 + elif version == "b7": + assert in_size == (600, 600) + depth_factor = 3.1 + width_factor = 2.0 + dropout_rate = 0.5 + elif version == "b8": + assert in_size == (672, 672) + depth_factor = 3.6 + width_factor = 2.2 + dropout_rate = 0.5 + else: + raise ValueError(f"Unsupported EfficientNet version {version}") + + init_block_channels = 32 + layers = [1, 2, 2, 3, 3, 4, 1] + downsample = [1, 1, 1, 1, 0, 1, 0] + channels_per_layers = [16, 24, 40, 80, 112, 192, 320] + expansion_factors_per_layers = [1, 6, 6, 6, 6, 6, 6] + kernel_sizes_per_layers = [3, 3, 5, 3, 5, 5, 3] + strides_per_stage = [1, 2, 2, 2, 1, 2, 1] + final_block_channels = 1280 + + layers = [int(math.ceil(li * depth_factor)) for li in layers] + channels_per_layers = [round_channels(ci * width_factor) for ci in channels_per_layers] + + from functools import reduce + + channels = reduce( + lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], + zip(channels_per_layers, layers, downsample), + [], + ) + kernel_sizes = reduce( + lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], + zip(kernel_sizes_per_layers, layers, downsample), + [], + ) + expansion_factors = reduce( + lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], + zip(expansion_factors_per_layers, layers, downsample), + [], + ) + strides_per_stage = reduce( + lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], + zip(strides_per_stage, layers, downsample), + [], + ) + strides_per_stage = [si[0] for si in strides_per_stage] + + init_block_channels = round_channels(init_block_channels * width_factor) + + if width_factor > 1.0: + assert int(final_block_channels * width_factor) == round_channels(final_block_channels * width_factor) + final_block_channels = round_channels(final_block_channels * width_factor) + + net = EfficientNet( + channels=channels, + init_block_channels=init_block_channels, + final_block_channels=final_block_channels, + kernel_sizes=kernel_sizes, + strides_per_stage=strides_per_stage, + expansion_factors=expansion_factors, + dropout_rate=dropout_rate, + tf_mode=tf_mode, + bn_eps=bn_eps, + in_size=in_size, + **kwargs, + ) + + return net + + +def efficientnet_b0(in_size=(224, 224), **kwargs): + """EfficientNet-B0. + + Args: + in_size : tuple of two ints, default (224, 224). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet(version="b0", in_size=in_size, model_name="efficientnet_b0", **kwargs) + + +def efficientnet_b1(in_size=(240, 240), **kwargs): + """EfficientNet-B1. + + Args: + in_size : tuple of two ints, default (240, 240). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet(version="b1", in_size=in_size, model_name="efficientnet_b1", **kwargs) + + +def efficientnet_b2(in_size=(260, 260), **kwargs): + """EfficientNet-B2. + + Args: + in_size : tuple of two ints, default (260, 260). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet(version="b2", in_size=in_size, model_name="efficientnet_b2", **kwargs) + + +def efficientnet_b3(in_size=(300, 300), **kwargs): + """EfficientNet-B3. + + Args: + in_size : tuple of two ints, default (300, 300). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet(version="b3", in_size=in_size, model_name="efficientnet_b3", **kwargs) + + +def efficientnet_b4(in_size=(380, 380), **kwargs): + """EfficientNet-B4. + + Args: + in_size : tuple of two ints, default (380, 380). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet(version="b4", in_size=in_size, model_name="efficientnet_b4", **kwargs) + + +def efficientnet_b5(in_size=(456, 456), **kwargs): + """EfficientNet-B5. + + Args: + in_size : tuple of two ints, default (456, 456). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet(version="b5", in_size=in_size, model_name="efficientnet_b5", **kwargs) + + +def efficientnet_b6(in_size=(528, 528), **kwargs): + """EfficientNet-B6 model. + + Args: + in_size : tuple of two ints, default (528, 528). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet(version="b6", in_size=in_size, model_name="efficientnet_b6", **kwargs) + + +def efficientnet_b7(in_size=(600, 600), **kwargs): + """EfficientNet-B7 model. + + Args: + in_size : tuple of two ints, default (600, 600). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet(version="b7", in_size=in_size, model_name="efficientnet_b7", **kwargs) + + +def efficientnet_b8(in_size=(672, 672), **kwargs): + """EfficientNet-B8. + + Args: + in_size : tuple of two ints, default (672, 672). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet(version="b8", in_size=in_size, model_name="efficientnet_b8", **kwargs) + + +def efficientnet_b0b(in_size=(224, 224), **kwargs): + """EfficientNet-B0-b. + + Args: + in_size : tuple of two ints, default (224, 224). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b0", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b0b", + **kwargs, + ) + + +def efficientnet_b1b(in_size=(240, 240), **kwargs): + """EfficientNet-B1-b. + + Args: + in_size : tuple of two ints, default (240, 240). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b1", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b1b", + **kwargs, + ) + + +def efficientnet_b2b(in_size=(260, 260), **kwargs): + """EfficientNet-B2-b. + + Args: + in_size : tuple of two ints, default (260, 260). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b2", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b2b", + **kwargs, + ) + + +def efficientnet_b3b(in_size=(300, 300), **kwargs): + """EfficientNet-B3-b. + + Args: + in_size : tuple of two ints, default (300, 300). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b3", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b3b", + **kwargs, + ) + + +def efficientnet_b4b(in_size=(380, 380), **kwargs): + """EfficientNet-B4-b. + + Args: + in_size : tuple of two ints, default (380, 380). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b4", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b4b", + **kwargs, + ) + + +def efficientnet_b5b(in_size=(456, 456), **kwargs): + """EfficientNet-B5-b. + + Args: + in_size : tuple of two ints, default (456, 456). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b5", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b5b", + **kwargs, + ) + + +def efficientnet_b6b(in_size=(528, 528), **kwargs): + """EfficientNet-B6-b. + + Args: + in_size : tuple of two ints, default (528, 528). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b6", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b6b", + **kwargs, + ) + + +def efficientnet_b7b(in_size=(600, 600), **kwargs): + """EfficientNet-B7-b. + + Args: + in_size : tuple of two ints, default (600, 600). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b7", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b7b", + **kwargs, + ) + + +def efficientnet_b0c(in_size=(224, 224), **kwargs): + """EfficientNet-B0-c. + + Args: + in_size : tuple of two ints, default (224, 224). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b0", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b0c", + **kwargs, + ) + + +def efficientnet_b1c(in_size=(240, 240), **kwargs): + """EfficientNet-B1-c. + + Args: + in_size : tuple of two ints, default (240, 240). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b1", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b1c", + **kwargs, + ) + + +def efficientnet_b2c(in_size=(260, 260), **kwargs): + """EfficientNet-B2-c. + + Args: + in_size : tuple of two ints, default (260, 260). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b2", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b2c", + **kwargs, + ) + + +def efficientnet_b3c(in_size=(300, 300), **kwargs): + """EfficientNet-B3-c. + + Args: + in_size : tuple of two ints, default (300, 300). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b3", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b3c", + **kwargs, + ) + + +def efficientnet_b4c(in_size=(380, 380), **kwargs): + """EfficientNet-B4-c. + + Args: + in_size : tuple of two ints, default (380, 380). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b4", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b4c", + **kwargs, + ) + + +def efficientnet_b5c(in_size=(456, 456), **kwargs): + """EfficientNet-B5-c. + + Args: + in_size : tuple of two ints, default (456, 456). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b5", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b5c", + **kwargs, + ) + + +def efficientnet_b6c(in_size=(528, 528), **kwargs): + """EfficientNet-B6-c. + + Args: + in_size : tuple of two ints, default (528, 528). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b6", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b6c", + **kwargs, + ) + + +def efficientnet_b7c(in_size=(600, 600), **kwargs): + """EfficientNet-B7-c. + + Args: + in_size : tuple of two ints, default (600, 600). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + return get_efficientnet( + version="b7", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b7c", + **kwargs, + ) + + +def efficientnet_b8c(in_size=(672, 672), **kwargs): + """EfficientNet-B8-c. + + Args: + in_size : tuple of two ints, default (672, 672). Spatial size of the expected input image. + pretrained : bool, default False. Whether to load the pretrained weights for model. + root : str, default '~/.torch/models'. Location for keeping the model parameters. + **kwargs: Addition keyword arguments. + """ + + return get_efficientnet( + version="b8", + in_size=in_size, + tf_mode=True, + bn_eps=1e-3, + model_name="efficientnet_b8c", + **kwargs, + ) + + +def _calc_width(net): + import numpy as np + + net_params = filter(lambda p: p.requires_grad, net.parameters()) + weight_count = 0 + for param in net_params: + weight_count += np.prod(param.size()) + return weight_count + + +def _test(): + import torch + + pretrained = False + + models = [ + efficientnet_b0, + efficientnet_b1, + efficientnet_b2, + efficientnet_b3, + efficientnet_b4, + efficientnet_b5, + efficientnet_b6, + efficientnet_b7, + efficientnet_b8, + efficientnet_b0b, + efficientnet_b1b, + efficientnet_b2b, + efficientnet_b3b, + efficientnet_b4b, + efficientnet_b5b, + efficientnet_b6b, + efficientnet_b7b, + efficientnet_b0c, + efficientnet_b1c, + efficientnet_b2c, + efficientnet_b3c, + efficientnet_b4c, + efficientnet_b5c, + efficientnet_b6c, + efficientnet_b7c, + efficientnet_b8c, + ] + + for model in models: + + net = model(pretrained=pretrained) + + # net.train() + net.eval() + weight_count = _calc_width(net) + print(f"m={model.__name__}, {weight_count}") + assert model != efficientnet_b0 or weight_count == 5288548 + assert model != efficientnet_b1 or weight_count == 7794184 + assert model != efficientnet_b2 or weight_count == 9109994 + assert model != efficientnet_b3 or weight_count == 12233232 + assert model != efficientnet_b4 or weight_count == 19341616 + assert model != efficientnet_b5 or weight_count == 30389784 + assert model != efficientnet_b6 or weight_count == 43040704 + assert model != efficientnet_b7 or weight_count == 66347960 + assert model != efficientnet_b8 or weight_count == 87413142 + assert model != efficientnet_b0b or weight_count == 5288548 + assert model != efficientnet_b1b or weight_count == 7794184 + assert model != efficientnet_b2b or weight_count == 9109994 + assert model != efficientnet_b3b or weight_count == 12233232 + assert model != efficientnet_b4b or weight_count == 19341616 + assert model != efficientnet_b5b or weight_count == 30389784 + assert model != efficientnet_b6b or weight_count == 43040704 + assert model != efficientnet_b7b or weight_count == 66347960 + + x = torch.randn(1, 3, net.in_size[0], net.in_size[1]) + y = net(x) + y.sum().backward() + assert tuple(y.size()) == (1, 1000) + + +@BACKBONES.register_module() +class OTXEfficientNet(EfficientNet): + """Create EfficientNet model with specific parameters. + + Args: + version : str. Version of EfficientNet ('b0'...'b8'). + in_size : tuple of two ints. Spatial size of the expected input image. + """ + + def __init__(self, version, **kwargs): + self.model_name = "efficientnet_" + version + + if version == "b0": + in_size = (224, 224) + depth_factor = 1.0 + width_factor = 1.0 + elif version == "b1": + in_size = (240, 240) + depth_factor = 1.1 + width_factor = 1.0 + elif version == "b2": + in_size = (260, 260) + depth_factor = 1.2 + width_factor = 1.1 + elif version == "b3": + in_size = (300, 300) + depth_factor = 1.4 + width_factor = 1.2 + elif version == "b4": + in_size = (380, 380) + depth_factor = 1.8 + width_factor = 1.4 + elif version == "b5": + in_size = (456, 456) + depth_factor = 2.2 + width_factor = 1.6 + elif version == "b6": + in_size = (528, 528) + depth_factor = 2.6 + width_factor = 1.8 + elif version == "b7": + in_size = (600, 600) + depth_factor = 3.1 + width_factor = 2.0 + elif version == "b8": + in_size = (672, 672) + depth_factor = 3.6 + width_factor = 2.2 + else: + raise ValueError(f"Unsupported EfficientNet version {version}") + + init_block_channels = 32 + layers = [1, 2, 2, 3, 3, 4, 1] + downsample = [1, 1, 1, 1, 0, 1, 0] + channels_per_layers = [16, 24, 40, 80, 112, 192, 320] + expansion_factors_per_layers = [1, 6, 6, 6, 6, 6, 6] + kernel_sizes_per_layers = [3, 3, 5, 3, 5, 5, 3] + strides_per_stage = [1, 2, 2, 2, 1, 2, 1] + final_block_channels = 1280 + + layers = [int(math.ceil(li * depth_factor)) for li in layers] + channels_per_layers = [round_channels(ci * width_factor) for ci in channels_per_layers] + + from functools import reduce + + channels = reduce( + lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], + zip(channels_per_layers, layers, downsample), + [], + ) + kernel_sizes = reduce( + lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], + zip(kernel_sizes_per_layers, layers, downsample), + [], + ) + expansion_factors = reduce( + lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], + zip(expansion_factors_per_layers, layers, downsample), + [], + ) + strides_per_stage = reduce( + lambda x, y: x + [[y[0]] * y[1]] if y[2] != 0 else x[:-1] + [x[-1] + [y[0]] * y[1]], + zip(strides_per_stage, layers, downsample), + [], + ) + strides_per_stage = [si[0] for si in strides_per_stage] + + init_block_channels = round_channels(init_block_channels * width_factor) + + if width_factor > 1.0: + assert int(final_block_channels * width_factor) == round_channels(final_block_channels * width_factor) + final_block_channels = round_channels(final_block_channels * width_factor) + + super().__init__( + channels=channels, + init_block_channels=init_block_channels, + final_block_channels=final_block_channels, + kernel_sizes=kernel_sizes, + strides_per_stage=strides_per_stage, + expansion_factors=expansion_factors, + dropout_cls=dict(dist="none"), + tf_mode=False, + bn_eps=1e-5, + in_size=in_size, + **kwargs, + ) + self.init_weights(self.pretrained) + + def forward(self, x): + """Forward.""" + return super().forward(x, return_featuremaps=True) + + def init_weights(self, pretrained=None): + """Initialize weights.""" + if isinstance(pretrained, str) and os.path.exists(pretrained): + load_checkpoint(self, pretrained) + logger.info(f"init weight - {pretrained}") + elif pretrained is not None: + download_model(net=self, model_name=self.model_name) + logger.info(f"init weight - {pretrained_urls[self.model_name]}") diff --git a/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnetv2.py b/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnetv2.py new file mode 100644 index 00000000000..631e2fc612f --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnetv2.py @@ -0,0 +1,111 @@ +"""Implementation of EfficientNetV2. + +Original papers: +- 'EfficientNetV2: Smaller Models and Faster Training,' https://arxiv.org/abs/2104.00298, +- 'Adversarial Examples Improve Image Recognition,' https://arxiv.org/abs/1911.09665. +""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=unused-argument, invalid-name + +import os + +import timm +from mmcv.runner import load_checkpoint +from torch import nn + +from otx.utils.logger import get_logger + +from ..builder import BACKBONES + +logger = get_logger() + +PRETRAINED_ROOT = "https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-effv2-weights/" +pretrained_urls = { + "efficientnetv2_s_21k": PRETRAINED_ROOT + "tf_efficientnetv2_s_21k-6337ad01.pth", + "efficientnetv2_s_1k": PRETRAINED_ROOT + "tf_efficientnetv2_s_21ft1k-d7dafa41.pth", +} + +NAME_DICT = { + "mobilenetv3_large_21k": "mobilenetv3_large_100_miil_in21k", + "mobilenetv3_large_1k": "mobilenetv3_large_100_miil", + "tresnet": "tresnet_m", + "efficientnetv2_s_21k": "tf_efficientnetv2_s_in21k", + "efficientnetv2_s_1k": "tf_efficientnetv2_s_in21ft1k", + "efficientnetv2_m_21k": "tf_efficientnetv2_m_in21k", + "efficientnetv2_m_1k": "tf_efficientnetv2_m_in21ft1k", + "efficientnetv2_b0": "tf_efficientnetv2_b0", +} + + +class TimmModelsWrapper(nn.Module): + """Timm model wrapper.""" + + def __init__(self, model_name, pretrained=False, pooling_type="avg", **kwargs): + super().__init__(**kwargs) + self.model_name = model_name + self.pretrained = pretrained + if model_name in ["mobilenetv3_large_100_miil_in21k", "mobilenetv3_large_100_miil"]: + self.is_mobilenet = True + else: + self.is_mobilenet = False + + self.model = timm.create_model(NAME_DICT[self.model_name], pretrained=pretrained, num_classes=1000) + if self.pretrained: + logger.info(f"init weight - {pretrained_urls[self.model_name]}") + self.model.classifier = None # Detach classifier. Only use 'backbone' part in otx. + self.num_head_features = self.model.num_features + self.num_features = self.model.conv_head.in_channels if self.is_mobilenet else self.model.num_features + self.pooling_type = pooling_type + + def forward(self, x, **kwargs): + """Forward.""" + y = self.extract_features(x) + return y + + def extract_features(self, x): + """Extract features.""" + if self.is_mobilenet: + x = self.model.conv_stem(x) + x = self.model.bn1(x) + x = self.model.act1(x) + y = self.model.blocks(x) + return y + return self.model.forward_features(x) + + def get_config_optim(self, lrs): + """Get optimizer configs.""" + parameters = [ + {"params": self.model.named_parameters()}, + ] + if isinstance(lrs, list): + assert len(lrs) == len(parameters) + for lr, param_dict in zip(lrs, parameters): + param_dict["lr"] = lr + else: + assert isinstance(lrs, float) + for param_dict in parameters: + param_dict["lr"] = lrs + + return parameters + + +@BACKBONES.register_module() +class OTXEfficientNetV2(TimmModelsWrapper): + """EfficientNetV2 for OTX.""" + + def __init__(self, version="s_21k", **kwargs): + self.model_name = "efficientnetv2_" + version + super().__init__(model_name=self.model_name, **kwargs) + + def init_weights(self, pretrained=None): + """Initialize weights.""" + if isinstance(pretrained, str) and os.path.exists(pretrained): + load_checkpoint(self, pretrained) + logger.info(f"init weight - {pretrained}") + elif pretrained is not None: + load_checkpoint(self, pretrained_urls[self.model_name]) + logger.info(f"init weight - {pretrained_urls[self.model_name]}") diff --git a/src/otx/algorithms/common/adapters/mmcv/models/backbones/mobilenetv3.py b/src/otx/algorithms/common/adapters/mmcv/models/backbones/mobilenetv3.py new file mode 100644 index 00000000000..cd63173afc1 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/models/backbones/mobilenetv3.py @@ -0,0 +1,387 @@ +"""Implementation of MobileNetV3. + +Original papers: +- 'Searching for MobileNetV3,' https://arxiv.org/abs/1905.02244. +""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name, too-many-arguments, unused-argument +# pylint: disable=too-many-locals, too-many-instance-attributes, abstract-method + +import math +import os + +import torch.nn.functional as F +from mmcls.models.utils import make_divisible +from mmcv.runner import load_checkpoint +from torch import nn + +from otx.utils.logger import get_logger + +from ..builder import BACKBONES + +logger = get_logger() + +pretrained_root = "https://github.com/d-li14/mobilenetv3.pytorch/blob/master/pretrained/" +pretrained_urls = { + "mobilenetv3_small": pretrained_root + "mobilenetv3-small-55df8e1f.pth?raw=true", + "mobilenetv3_large": pretrained_root + "mobilenetv3-large-1cd25616.pth?raw=true", + "mobilenetv3_large_075": pretrained_root + "mobilenetv3-large-0.75-9632d2a8.pth?raw=true", +} + + +class ModelInterface(nn.Module): + """Model Interface.""" + + def __init__( + self, + classification=False, + contrastive=False, + pretrained=False, + loss="softmax", + **kwargs, + ): + super().__init__() + + self.classification = classification + self.contrastive = contrastive + self.pretrained = pretrained + self.classification_classes = {} + self.loss = loss + self.is_ie_model = False + if loss == "am_softmax": + self.use_angle_simple_linear = True + else: + self.use_angle_simple_linear = False + + @staticmethod + def _glob_feature_vector(x, mode, reduce_dims=True): + if mode == "avg": + out = F.adaptive_avg_pool2d(x, 1) + elif mode == "max": + out = F.adaptive_max_pool2d(x, 1) + elif mode == "avg+max": + avg_pool = F.adaptive_avg_pool2d(x, 1) + max_pool = F.adaptive_max_pool2d(x, 1) + out = avg_pool + max_pool + else: + raise ValueError(f"Unknown pooling mode: {mode}") + + if reduce_dims: + return out.view(x.size(0), -1) + return out + + +class HSigmoid(nn.Module): + """Approximated sigmoid function, so-called hard-version of sigmoid from 'Searching for MobileNetV3,'. + + https://arxiv.org/abs/1905.02244. + """ + + def forward(self, x): + """Forward.""" + return F.relu6(x + 3.0, inplace=True) / 6.0 + + +class HSwish(nn.Module): + """H-Swish activation function from 'Searching for MobileNetV3,'. + + https://arxiv.org/abs/1905.02244. + + Parameters: + inplace : bool, Whether to use inplace version of the module. + """ + + def __init__(self, inplace=False): + super().__init__() + self.inplace = inplace + + def forward(self, x): + """Forward.""" + return x * F.relu6(x + 3.0, inplace=self.inplace) / 6.0 + + +class SELayer(nn.Module): + """SE layer.""" + + def __init__(self, channel, reduction=4): + super().__init__() + self.avg_pool = nn.AdaptiveAvgPool2d(1) + self.fc = nn.Sequential( + nn.Linear(channel, make_divisible(channel // reduction, 8)), + nn.ReLU(inplace=True), + nn.Linear(make_divisible(channel // reduction, 8), channel), + HSigmoid(), + ) + + def forward(self, x): + """Forward.""" + + # with no_nncf_se_layer_context(): + b, c, _, _ = x.size() + y = self.avg_pool(x).view(b, c) + y = self.fc(y).view(b, c, 1, 1) + return x * y + + +def conv_3x3_bn(inp, oup, stride, IN_conv1=False): + """Conv 3x3 layer with batch-norm.""" + + return nn.Sequential( + nn.Conv2d(inp, oup, 3, stride, 1, bias=False), + nn.BatchNorm2d(oup) if not IN_conv1 else nn.InstanceNorm2d(oup, affine=True), + HSwish(), + ) + + +def conv_1x1_bn(inp, oup, loss="softmax"): + """Conv 1x1 layer with batch-norm.""" + + return nn.Sequential( + nn.Conv2d(inp, oup, 1, 1, 0, bias=False), + nn.BatchNorm2d(oup), + HSwish() if loss == "softmax" else nn.PReLU(), + ) + + +class InvertedResidual(nn.Module): + """Inverted residual.""" + + def __init__(self, inp, hidden_dim, oup, kernel_size, stride, use_se, use_hs): + super().__init__() + assert stride in [1, 2] + + self.identity = stride == 1 and inp == oup + + if inp == hidden_dim: + self.conv = nn.Sequential( + # dw + nn.Conv2d( + hidden_dim, + hidden_dim, + kernel_size, + stride, + (kernel_size - 1) // 2, + groups=hidden_dim, + bias=False, + ), + nn.BatchNorm2d(hidden_dim), + HSwish() if use_hs else nn.ReLU(inplace=True), + # Squeeze-and-Excite + SELayer(hidden_dim) if use_se else nn.Identity(), + # pw-linear + nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False), + nn.BatchNorm2d(oup), + ) + else: + self.conv = nn.Sequential( + # pw + nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False), + nn.BatchNorm2d(hidden_dim), + HSwish() if use_hs else nn.ReLU(inplace=True), + # dw + nn.Conv2d( + hidden_dim, + hidden_dim, + kernel_size, + stride, + (kernel_size - 1) // 2, + groups=hidden_dim, + bias=False, + ), + nn.BatchNorm2d(hidden_dim), + # Squeeze-and-Excite + SELayer(hidden_dim) if use_se else nn.Identity(), + HSwish() if use_hs else nn.ReLU(inplace=True), + # pw-linear + nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False), + nn.BatchNorm2d(oup), + ) + + def forward(self, x): + """Forward.""" + + if self.identity: + return x + self.conv(x) + return self.conv(x) + + +class MobileNetV3Base(ModelInterface): + """Base model of MobileNetV3.""" + + def __init__( + self, + num_classes=1000, + width_mult=1.0, + in_channels=3, + input_size=(224, 224), + dropout_cls=None, + pooling_type="avg", + feature_dim=1280, + IN_first=False, + self_challenging_cfg=False, + lr_finder=None, + **kwargs, + ): + + super().__init__(**kwargs) + self.in_size = input_size + self.num_classes = num_classes + self.input_IN = nn.InstanceNorm2d(in_channels, affine=True) if IN_first else None + self.pooling_type = pooling_type + self.self_challenging_cfg = self_challenging_cfg + self.width_mult = width_mult + self.dropout_cls = dropout_cls + self.lr_finder = lr_finder + self.feature_dim = feature_dim + + def infer_head(self, x, skip_pool=False): + """Inference head.""" + raise NotImplementedError + + def extract_features(self, x): + """Extract features.""" + raise NotImplementedError + + def forward(self, x, return_featuremaps=False, get_embeddings=False, gt_labels=None): + """Forward.""" + if self.input_IN is not None: + x = self.input_IN(x) # pylint: disable=not-callable + + y = self.extract_features(x) + if return_featuremaps: + return y + # should be checked + return y + + +class MobileNetV3(MobileNetV3Base): + """MobileNetV3.""" + + def __init__(self, cfgs, mode, IN_conv1=False, **kwargs): + super().__init__(**kwargs) + # setting of inverted residual blocks + self.cfgs = cfgs + assert mode in ["large", "small"] + # building first layer + input_channel = make_divisible(16 * self.width_mult, 8) + stride = 1 if self.in_size[0] < 100 else 2 + layers = [conv_3x3_bn(3, input_channel, stride, IN_conv1)] + # building inverted residual blocks + block = InvertedResidual + flag = True + for k, t, c, use_se, use_hs, s in self.cfgs: + if (self.in_size[0] < 100) and (s == 2) and flag: + s = 1 + flag = False + output_channel = make_divisible(c * self.width_mult, 8) + exp_size = make_divisible(input_channel * t, 8) + layers.append(block(input_channel, exp_size, output_channel, k, s, use_se, use_hs)) + input_channel = output_channel + self.features = nn.Sequential(*layers) + # building last several layers + self.conv = conv_1x1_bn(input_channel, exp_size, self.loss) + output_channel = {"large": 1280, "small": 1024} + output_channel = ( + make_divisible(output_channel[mode] * self.width_mult, 8) if self.width_mult > 1.0 else output_channel[mode] + ) + self._initialize_weights() + + def extract_features(self, x): + """Extract features.""" + + y = self.conv(self.features(x)) + return y + + def infer_head(self, x, skip_pool=False): + """Inference head.""" + + if not skip_pool: + glob_features = self._glob_feature_vector(x, self.pooling_type, reduce_dims=False) + else: + glob_features = x + + logits = self.classifier(glob_features.view(x.shape[0], -1)) + return glob_features, logits + + def _initialize_weights(self): + """Initialize weights.""" + + for m in self.modules(): + if isinstance(m, nn.Conv2d): + n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels + m.weight.data.normal_(0, math.sqrt(2.0 / n)) + if m.bias is not None: + m.bias.data.zero_() + elif isinstance(m, nn.BatchNorm2d): + m.weight.data.fill_(1) + m.bias.data.zero_() + elif isinstance(m, nn.Linear): + n = m.weight.size(1) + m.weight.data.normal_(0, 0.01) + m.bias.data.zero_() + + +@BACKBONES.register_module() +class OTXMobileNetV3(MobileNetV3): + """MobileNetV3 model for OTX.""" + + cfgs = dict( + small=[ + # k, t, c, SE, HS, s + [3, 1, 16, 1, 0, 2], + [3, 4.5, 24, 0, 0, 2], + [3, 3.67, 24, 0, 0, 1], + [5, 4, 40, 1, 1, 2], + [5, 6, 40, 1, 1, 1], + [5, 6, 40, 1, 1, 1], + [5, 3, 48, 1, 1, 1], + [5, 3, 48, 1, 1, 1], + [5, 6, 96, 1, 1, 2], + [5, 6, 96, 1, 1, 1], + [5, 6, 96, 1, 1, 1], + ], + large=[ + # k, t, c, SE, HS, s + [3, 1, 16, 0, 0, 1], + [3, 4, 24, 0, 0, 2], + [3, 3, 24, 0, 0, 1], + [5, 3, 40, 1, 0, 2], + [5, 3, 40, 1, 0, 1], + [5, 3, 40, 1, 0, 1], + [3, 6, 80, 0, 1, 2], + [3, 2.5, 80, 0, 1, 1], + [3, 2.3, 80, 0, 1, 1], + [3, 2.3, 80, 0, 1, 1], + [3, 6, 112, 1, 1, 1], + [3, 6, 112, 1, 1, 1], + [5, 6, 160, 1, 1, 2], + [5, 6, 160, 1, 1, 1], + [5, 6, 160, 1, 1, 1], + ], + ) + + def __init__(self, mode="large", width_mult=1.0, **kwargs): + super().__init__(self.cfgs[mode], mode=mode, width_mult=width_mult, **kwargs) + self.key = "mobilenetv3_" + mode + if width_mult != 1.0: + self.key = self.key + "_{:03d}".format(int(width_mult * 100)) # pylint: disable=consider-using-f-string + self.init_weights(self.pretrained) + + def forward(self, x): + """Forward.""" + + return super().forward(x, return_featuremaps=True) + + def init_weights(self, pretrained=None): + """Initialize weights.""" + + if isinstance(pretrained, str) and os.path.exists(pretrained): + load_checkpoint(self, pretrained) + logger.info(f"init weight - {pretrained}") + elif pretrained is not None: + load_checkpoint(self, pretrained_urls[self.key]) + logger.info(f"init weight - {pretrained_urls[self.key]}") diff --git a/src/otx/algorithms/common/adapters/mmcv/models/backbones/torchvision_backbones.py b/src/otx/algorithms/common/adapters/mmcv/models/backbones/torchvision_backbones.py new file mode 100644 index 00000000000..1bc634c23b2 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/models/backbones/torchvision_backbones.py @@ -0,0 +1,249 @@ +"""Torchvision Model Backbone Class Generation. + +For torchvision backbone support for OTX models, +this is a code that converts torchvision backbone classes to match +the mmcv backbone class format and registers them in the mmcv registry. +This copied the format of "mmdet/models/backbones/imgclsmob.py" +as it is and made some modifications & code cleaning. +""" +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import inspect +import os + +from mmcv.cnn import build_activation_layer, build_norm_layer +from torch import nn +from torch.hub import load_state_dict_from_url +from torch.nn.modules.batchnorm import _BatchNorm +from torchvision import models + +from ..builder import TORCHVISION_BACKBONES + +# pylint: disable=protected-access, assignment-from-no-return, no-value-for-parameter, too-many-statements + + +def get_torchvision_models(): + """Get torchvision backbones of current version.""" + torchvision_urls = {} + torchvision_models = {} + for model_key, model_value in models.__dict__.items(): + if callable(model_value) and model_key[0].islower() and model_key[0] != "_": + torchvision_models[model_key] = model_value + elif inspect.ismodule(model_value) and hasattr(model_value, "model_urls"): + torchvision_urls.update(model_value.model_urls) + return torchvision_models, torchvision_urls + + +TORCHVISION_MODELS, TORCHVISION_MODEL_URLS = get_torchvision_models() + + +def replace_activation(model, activation_cfg): + """Replace Activate function (copy from mmdet).""" + for name, module in model._modules.items(): + if len(list(module.children())) > 0: + model._modules[name] = replace_activation(module, activation_cfg) + if name == "activ": + if activation_cfg["type"] == "torch_swish": + model._modules[name] = nn.SiLU() + else: + model._modules[name] = build_activation_layer(activation_cfg) + return model + + +def replace_norm(model, cfg): + """Replace Norm function (copy from mmdet).""" + for name, module in model._modules.items(): + if len(list(module.children())) > 0: + model._modules[name] = replace_norm(module, cfg) + if name == "bn": + model._modules[name] = build_norm_layer(cfg, num_features=module.num_features)[1] + return model + + +def resnet_forward(self, x): + """Resnet forward function for wrapping model (refer to torchvision).""" + outputs = [] + y = x + stages = [self.layer1, self.layer2, self.layer3, self.layer4] + last_stage = max(self.out_indices) + y = self.conv1(y) + y = self.bn1(y) + y = self.relu(y) + y = self.maxpool(y) + for i, stage in enumerate(stages): + y = stage(y) + if i in self.out_indices: + outputs.append(y) + if i == last_stage: + break + return tuple(outputs) + + +def shufflenet_forward(self, x): + """Shufflenet forward function for wrapping model (refer to torchvision).""" + outputs = [] + y = x + y = self.conv1(y) + y = self.maxpool(y) + stages = [self.stage2, self.stage3, self.stage4, self.conv5] + last_stage = max(self.out_indices) + for i, stage in enumerate(stages): + y = stage(y) + if i in self.out_indices: + outputs.append(y) + if i == last_stage: + break + return tuple(outputs) + + +def multioutput_forward(self, x): + """Multioutput forward function for new model (copy from mmdet).""" + outputs = [] + y = x + + last_stage = max(self.out_indices) + if hasattr(self, "features"): + stages = self.features + elif hasattr(self, "layers"): + stages = self.layers + else: + raise ValueError(f"Not supported multioutput forward: {self}") + + for i, stage in enumerate(stages): + y = stage(y) + temp_s = str(i) + " " + str(y.shape) + if i in self.out_indices: + outputs.append(y) + temp_s += "*" + if self.verbose: + print(temp_s) + if i == last_stage: + break + return tuple(outputs) + + +def train(self, mode=True): + """Train forward function for new model (copy from mmdet).""" + super(self.__class__, self).train(mode) + + if hasattr(self, "features"): + stages = self.features + elif hasattr(self, "layers"): + stages = self.layers + else: + raise ValueError(f"Not supported multioutput forward: {self}") + + for i in range(self.frozen_stages + 1): + temp_m = stages[i] + temp_m.eval() + for param in temp_m.parameters(): + param.requires_grad = False + + if mode and self.norm_eval: + for mmodule in self.modules(): + # trick: eval have effect on BatchNorm only + if isinstance(mmodule, _BatchNorm): + mmodule.eval() + + +def init_weights(self): + """Init weights function for new model (copy from mmdet).""" + if self.init_cfg.get("Pretrained", False) and self.model_urls: + state_dict = load_state_dict_from_url(self.model_urls) + self.load_state_dict(state_dict) + + +def generate_torchvision_backbones(): + """Regist Torchvision Backbone into mmX Registry (copy from mmdet).""" + for model_name, model_builder in TORCHVISION_MODELS.items(): + + def closure(model_name, model_builder): + """Get Model builder for mmcv (copy from mmdet).""" + + class TorchvisionModelWrapper(nn.Module): # pylint: disable=abstract-method + """Torchvision Model to MMX.model Wrapper (copy from mmdet).""" + + def __init__( + self, + *args, + out_indices=(0, 1, 2, 3), + frozen_stages=0, + norm_eval=False, + verbose=False, + activation_cfg=None, + norm_cfg=None, + init_cfg=None, + **kwargs, + ): + super().__init__() + models_cache_root = kwargs.get("root", os.path.join("~", ".torch", "models")) + model = model_builder(*args, **kwargs) + if activation_cfg: + model = replace_activation(model, activation_cfg) + if norm_cfg: + model = replace_norm(model, norm_cfg) + model.out_indices = out_indices + model.frozen_stages = frozen_stages + model.norm_eval = norm_eval + model.verbose = verbose + model_name = str(self).strip("()") + model.model_urls = ( + TORCHVISION_MODEL_URLS[model_name] if model_name in TORCHVISION_MODEL_URLS else None + ) + model.models_cache_root = models_cache_root + model.init_cfg = init_cfg + model.init_weights = init_weights.__get__(model) + if hasattr(model, "features") and isinstance(model.features, nn.Sequential): + # Save original forward, just in case. + model.forward_single_output = model.forward + model.forward = multioutput_forward.__get__(model) + model.train = train.__get__(model) + + model.output = None + for i, _ in enumerate(model.features): + if out_indices is not None and i > max(out_indices): + model.features[i] = None + elif hasattr(model, "layers") and isinstance(model.layers, nn.Sequential): + # Save original forward, just in case. + model.forward_single_output = model.forward + model.forward = multioutput_forward.__get__(model) + model.train = train.__get__(model) + + model.classifier = None + for i, _ in enumerate(model.layers): + if out_indices is not None and i > max(out_indices): + model.layers[i] = None + elif model_name.startswith(("resne", "wide_resne")): + # torchvision.resne* -> resnet_forward + model.forward = resnet_forward.__get__(model) + model.fc = None + elif model_name.startswith("shufflenet"): + model.forward = shufflenet_forward.__get__(model) + model.fc = None + else: + raise ValueError( + "Failed to automatically wrap backbone network. " + f"Object of type {model.__class__} has no valid attribute called " + '"features".' + ) + self.__dict__.update(model.__dict__) + + TorchvisionModelWrapper.__name__ = model_name + return TorchvisionModelWrapper + + TORCHVISION_BACKBONES.register_module(name=model_name, module=closure(model_name, model_builder)) + + +generate_torchvision_backbones() diff --git a/src/otx/algorithms/common/adapters/mmcv/models/builder.py b/src/otx/algorithms/common/adapters/mmcv/models/builder.py new file mode 100644 index 00000000000..cd5c6b204ff --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/models/builder.py @@ -0,0 +1,21 @@ +"""Builder for OTX Common Algorithm. - mmcv.models.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from mmcv.cnn import MODELS as MMCV_MODELS +from mmcv.utils import Registry + +BACKBONES = Registry("models", parent=MMCV_MODELS, scope="otx") +TORCHVISION_BACKBONES = Registry("models", parent=MMCV_MODELS, scope="torchvision") diff --git a/src/otx/algorithms/common/adapters/mmcv/nncf/__init__.py b/src/otx/algorithms/common/adapters/mmcv/nncf/__init__.py new file mode 100644 index 00000000000..7e44ab9a772 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/nncf/__init__.py @@ -0,0 +1,14 @@ +"""NNCF for mmcv.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# flake8: noqa + +from .utils import get_fake_input, model_eval, wrap_nncf_model + +__all__ = [ + "get_fake_input", + "model_eval", + "wrap_nncf_model", +] diff --git a/src/otx/algorithms/common/adapters/mmcv/nncf/hooks.py b/src/otx/algorithms/common/adapters/mmcv/nncf/hooks.py new file mode 100644 index 00000000000..8bc3cb233b4 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/nncf/hooks.py @@ -0,0 +1,32 @@ +"""NNCF task related hooks.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.runner.hooks.hook import HOOKS, Hook + + +@HOOKS.register_module() +class CompressionHook(Hook): + """CompressionHook.""" + + COMPRESSION_STATE_FILE_NAME = "meta_state.pth" + + def __init__(self, compression_ctrl=None): + self.compression_ctrl = compression_ctrl + + def after_train_iter(self, runner): + """Called after train iter.""" + self.compression_ctrl.scheduler.step() + + def after_train_epoch(self, runner): + """Called after train epoch.""" + self.compression_ctrl.scheduler.epoch_step() + if runner.rank == 0: + runner.logger.info(self.compression_ctrl.statistics().to_str()) + + def before_run(self, runner): + """Called before run.""" + runner.compression_ctrl = self.compression_ctrl + if runner.rank == 0: + runner.logger.info(self.compression_ctrl.statistics().to_str()) diff --git a/src/otx/algorithms/common/adapters/mmcv/nncf/patches.py b/src/otx/algorithms/common/adapters/mmcv/nncf/patches.py new file mode 100644 index 00000000000..d8ddb788f1e --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/nncf/patches.py @@ -0,0 +1,38 @@ +"""Patch mmcv stuff.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy + +from otx.algorithms.common.adapters.nncf import ( + NNCF_PATCHER, + no_nncf_trace_wrapper, +) + + +# pylint: disable-next=unused-argument,invalid-name +def _evaluation_wrapper(self, fn, runner, *args, **kwargs): + # TODO: move this patch to upper level (mmcv) + # as this is not only nncf required feature. + # one example is ReduceLROnPlateauLrUpdaterHook + out = fn(runner, *args, **kwargs) + setattr(runner, "all_metrics", deepcopy(runner.log_buffer.output)) + return out + + +NNCF_PATCHER.patch("mmcv.runner.EvalHook.evaluate", _evaluation_wrapper) +NNCF_PATCHER.patch("otx.algorithms.common.adapters.mmcv.hooks.eval_hook.CustomEvalHook.evaluate", _evaluation_wrapper) + +NNCF_PATCHER.patch( + "otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook.FeatureVectorHook.func", + no_nncf_trace_wrapper, +) +NNCF_PATCHER.patch( + "otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook.ActivationMapHook.func", + no_nncf_trace_wrapper, +) +NNCF_PATCHER.patch( + "otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook.ReciproCAMHook.func", + no_nncf_trace_wrapper, +) diff --git a/src/otx/algorithms/common/adapters/mmcv/nncf/runners.py b/src/otx/algorithms/common/adapters/mmcv/nncf/runners.py new file mode 100644 index 00000000000..f9f6f3b111c --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/nncf/runners.py @@ -0,0 +1,181 @@ +"""AccuracyAwareRunner for NNCF task.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import time +from dataclasses import asdict + +from mmcv.runner import RUNNERS +from mmcv.runner.hooks.evaluation import EvalHook +from mmcv.runner.hooks.lr_updater import LrUpdaterHook +from mmcv.runner.utils import get_host_info + +from otx.algorithms.common.adapters.mmcv.nncf.hooks import CompressionHook +from otx.algorithms.common.adapters.mmcv.runner import EpochRunnerWithCancel +from otx.algorithms.common.adapters.nncf import ( + AccuracyAwareLrUpdater, + check_nncf_is_enabled, +) +from otx.algorithms.common.adapters.nncf.compression import NNCFMetaState + +NNCF_META_KEY = "nncf_meta" + + +# TODO: refactoring +@RUNNERS.register_module() +class AccuracyAwareRunner(EpochRunnerWithCancel): # pylint: disable=too-many-instance-attributes + """AccuracyAwareRunner for NNCF task. + + An mmcv training runner to be used with NNCF-based accuracy-aware training. + Inherited from the standard EpochBasedRunner with the overridden "run" method. + This runner does not use the "workflow" and "max_epochs" parameters that are + used by the EpochBasedRunner since the training is controlled by NNCF's + AdaptiveCompressionTrainingLoop that does the scheduling of the compression-aware + training loop using the parameters specified in the "accuracy_aware_training". + """ + + def __init__(self, *args, nncf_config, nncf_meta=None, **kwargs): + super().__init__(*args, **kwargs) + self.nncf_config = nncf_config + + if nncf_meta is None: + nncf_meta = NNCFMetaState() + self.nncf_meta = nncf_meta + + self.compression_ctrl = None + self._target_metric_name = nncf_config["target_metric_name"] + self._train_data_loader = None + self._eval_hook = None + + def run(self, data_loaders, *args, **kwargs): # pylint: disable=unused-argument + """run.""" + check_nncf_is_enabled() + + from nncf.common.accuracy_aware_training import ( + create_accuracy_aware_training_loop, + ) + + assert isinstance(data_loaders, list) + + lr_update_hook = [] + eval_hook = [] + found_compression_hook = False + for hook in self.hooks: + if isinstance(hook, LrUpdaterHook): + lr_update_hook.append(hook) + if isinstance(hook, CompressionHook): + found_compression_hook = True + if isinstance(hook, EvalHook): + eval_hook.append(hook) + assert found_compression_hook, f"{CompressionHook} must be registered to {self}." + assert len(lr_update_hook) <= 1, ( + f"More than 1 lr update hooks ({len(lr_update_hook)} " f"are registered to {self}" + ) + assert len(eval_hook) == 1, f"{EvalHook} must be registered to {self}" + self._eval_hook = eval_hook[0] + assert self._eval_hook.save_best == self.nncf_config.target_metric_name, ( + "'target_metric_name' from nncf_config is not identical to 'save_best' in 'EvalHook'. " + f"({self._eval_hook.save_best} != {self.nncf_config.target_metric_name})" + ) + + work_dir = self.work_dir if self.work_dir is not None else "NONE" + self.logger.info("Start running, host: %s, work_dir: %s", get_host_info(), work_dir) + self.logger.warning( + "Note that the workflow and max_epochs parameters are not used in NNCF-based accuracy-aware training" + ) + + # taking only the first data loader for NNCF training + self._train_data_loader = data_loaders[0] + # Maximum possible number of iterations, needs for progress tracking + params = self.nncf_config["accuracy_aware_training"]["params"] + self._max_epochs = params["maximal_total_epochs"] + self._max_iters = self._max_epochs * len(self._train_data_loader) + + self.logger.info("Start running, host: %s, work_dir: %s", get_host_info(), work_dir) + self.logger.info("Hooks will be executed in the following order:\n%s", self.get_hook_info()) + self.call_hook("before_run") + + def configure_optimizers_fn(): + return self.optimizer, None + + if len(lr_update_hook) == 1: + lr_update_hook = lr_update_hook[0] + + def configure_optimizers_fn(): # noqa: F811 # pylint: disable=function-redefined + return self.optimizer, AccuracyAwareLrUpdater(lr_update_hook) + + # pylint: disable-next=unused-argument + def dump_checkpoint_fn(model, compression_ctrl, nncf_runner, save_dir): + # pylint: disable-next=protected-access + self._eval_hook._save_ckpt(self, nncf_runner.best_val_metric_value) + return self._eval_hook.best_ckpt_path + + if hasattr(self.model, "module"): + uncompressed_model_accuracy = self.model.module.nncf._uncompressed_model_accuracy + else: + uncompressed_model_accuracy = self.model.nncf._uncompressed_model_accuracy + + acc_aware_training_loop = create_accuracy_aware_training_loop( + self.nncf_config, + self.compression_ctrl, + verbose=False, + uncompressed_model_accuracy=uncompressed_model_accuracy, + ) + + model = acc_aware_training_loop.run( + self.model, + train_epoch_fn=self.train_fn, + validate_fn=self.validation_fn, + configure_optimizers_fn=configure_optimizers_fn, + dump_checkpoint_fn=dump_checkpoint_fn, + log_dir=self.work_dir, + ) + + time.sleep(1) # wait for some hooks like loggers to finish + self.call_hook("after_run") + return model + + def train_fn(self, *args, **kwargs): # pylint: disable=unused-argument + """train_fn. + + Train the model for a single epoch. + This method is used in NNCF-based accuracy-aware training. + """ + self.train(self._train_data_loader) + + def validation_fn(self, *args, **kwargs): # pylint: disable=unused-argument + """validation_fn. + + Return the target metric value on the validation dataset. + This method is used in NNCF-based accuracy-aware training. + """ + + # make sure evaluation hook is in a 'should_evaluate' state + interval_bak = self._eval_hook.interval + self._eval_hook.interval = 1 + self._eval_hook._do_evaluate(self) # pylint: disable=protected-access + self._eval_hook.interval = interval_bak + # Get metric from runner's attributes that set in EvalHook.evaluate() function + all_metrics = getattr(self, "all_metrics", {}) + metric = all_metrics.get(self._target_metric_name, None) + if metric is None: + raise RuntimeError(f"Could not find the {self._target_metric_name} key") + return metric + + def save_checkpoint(self, *args, **kwargs) -> None: + """Save checkpoint with NNCF meta state.""" + + compression_state = self.compression_ctrl.get_compression_state() + for algo_state in compression_state.get("ctrl_state", {}).values(): + if not algo_state.get("scheduler_state"): + algo_state["scheduler_state"] = {"current_step": 0, "current_epoch": 0} + + nncf_meta = NNCFMetaState( + **{**asdict(self.nncf_meta), "compression_ctrl": compression_state}, + ) + + meta = kwargs.pop("meta", {}) + meta[NNCF_META_KEY] = nncf_meta + meta["nncf_enable_compression"] = True + super().save_checkpoint(*args, **kwargs, meta=meta) diff --git a/src/otx/algorithms/common/adapters/mmcv/nncf/utils.py b/src/otx/algorithms/common/adapters/mmcv/nncf/utils.py new file mode 100644 index 00000000000..072b5ca7cf9 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/nncf/utils.py @@ -0,0 +1,329 @@ +"""NNCF utils.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import inspect +import os +from copy import deepcopy +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +import numpy as np +import torch +from mmcv import Config +from mmcv.parallel import DataContainer, collate, scatter +from torch import nn +from torch.utils.data import DataLoader + +from otx.algorithms.common.adapters.mmcv.nncf.runners import NNCF_META_KEY +from otx.algorithms.common.adapters.mmcv.utils.builder import build_data_parallel +from otx.algorithms.common.adapters.nncf.compression import ( + is_checkpoint_nncf, + is_state_nncf, +) +from otx.algorithms.common.adapters.nncf.utils import ( + check_nncf_is_enabled, + load_checkpoint, + no_nncf_trace, +) +from otx.algorithms.common.utils import get_arg_spec +from otx.utils.logger import get_logger + +logger = get_logger() + + +def get_fake_input( + preprocessor: Callable[..., Dict[str, Any]], + data: Optional[np.ndarray] = None, + shape: Tuple[int, ...] = (128, 128, 3), + device: Union[str, torch.device] = "cpu", +): + """A function to generate fake data.""" + + if isinstance(device, str): + device = torch.device(device) + + if data is None: + data = dict(img=np.zeros(shape, dtype=np.uint8)) + else: + data = dict(img=data) + data = preprocessor(data) + + for key, value in data.items(): + if not isinstance(value, list): + data[key] = [value] + + if device.type == "cpu": + data = scatter(collate([data], samples_per_gpu=1), [-1])[0] + elif device.type == "cuda": + data = scatter(collate([data], samples_per_gpu=1), [device.index])[0] + elif device.type == "xpu": + data = scatter(collate([data], samples_per_gpu=1), [-1])[0] + else: + raise NotImplementedError() + return data + + +def model_eval( + model: nn.Module, + *, + config: Config, + val_dataloader: DataLoader, + evaluate_fn: Callable, + distributed: bool, +): + """A model evaluation function for NNCF. + + Runs evaluation of the model on the validation set and + returns the target metric value. + Used to evaluate the original model before compression + if NNCF-based accuracy-aware training is used. + """ + if val_dataloader is None: + raise RuntimeError( + "Cannot perform model evaluation on the validation " + "dataset since the validation data loader was not passed " + "to wrap_nncf_model" + ) + + nncf_config = config.get("nncf_config") + metric_name = nncf_config.get("target_metric_name") + prepared_model = build_data_parallel(model, config, distributed=distributed) + + logger.info("Calculating an original model accuracy") + + evaluation_cfg = deepcopy(config.evaluation) + spec = get_arg_spec(val_dataloader.dataset.evaluate) + for key in list(evaluation_cfg.keys()): + if key not in spec: + evaluation_cfg.pop(key) + evaluation_cfg["metric"] = metric_name + + if distributed: # pylint: disable=no-else-return + dist_eval_res: List[Dict[str, Any]] = [{}] + results = evaluate_fn(prepared_model, val_dataloader, gpu_collect=True) + if torch.distributed.get_rank() == 0: + eval_res = val_dataloader.dataset.evaluate(results, **evaluation_cfg) + if metric_name not in eval_res: + raise RuntimeError(f"Cannot find {metric_name} metric in the evaluation result dict") + dist_eval_res[0] = eval_res + + torch.distributed.broadcast_object_list(dist_eval_res, src=0) + return dist_eval_res[0][metric_name] + else: + results = evaluate_fn(prepared_model, val_dataloader, show=False) + eval_res = val_dataloader.dataset.evaluate(results, **evaluation_cfg) + + if metric_name not in eval_res: + raise RuntimeError(f"Cannot find {metric_name} metric in the evaluation result dict {eval_res.keys()}") + + return eval_res[metric_name] + + +def nncf_state_dict_pre_hook(state_dict, prefix, *args, **kwargs): + """NNCF-specific state dict pre-hook. + + This hook removes extra prefixes from nncf-related parameters + before loading to NNCF-ready model. + """ + for key in list(state_dict.keys()): + val = state_dict.pop(key) + if "_nncf" in key: + if key.startswith("backbone"): + key = key.replace("backbone.", "", 1) + state_dict[key] = val + + return state_dict + + +def nncf_state_dict_hook(module, state_dict, prefix, *args, **kwargs): + """NNCF-specific state dict post-hook. + + This hook prevents extra buffers from being saved to state dict, + reverting this behavior, introduced by mmcv. + """ + for key in list(state_dict.keys()): + val = state_dict.pop(key) + if "_level_high" in key or "_level_low" in key: + continue + + state_dict[key] = val + + return state_dict + + +# pylint: disable-next=too-many-branches,too-many-statements,too-many-locals +def wrap_nncf_model( # noqa: C901 + config: Config, + model: nn.Module, + *, + model_eval_fn: Optional[Callable] = None, + dummy_forward_fn: Optional[Callable] = None, + get_fake_input_fn: Optional[Callable] = None, + wrap_inputs_fn: Optional[Callable] = None, + dataloader_for_init: Optional[DataLoader] = None, + init_state_dict: Optional[Dict[Any, Any]] = None, + is_accuracy_aware: bool = False, +): + """The function wraps mmcv model by NNCF.""" + + check_nncf_is_enabled() + + from nncf import NNCFConfig + from nncf.config.utils import is_accuracy_aware_training + from nncf.torch import ( + create_compressed_model, + load_state, + register_default_init_args, + ) + from nncf.torch.dynamic_graph.io_handling import nncf_model_input + from nncf.torch.initialization import PTInitializingDataLoader + + class _MMInitializeDataLoader(PTInitializingDataLoader): + def get_inputs(self, dataloader_output): + # redefined PTInitializingDataLoader because + # of DataContainer format in mmdet + kwargs = {k: v.data[0] if isinstance(v, DataContainer) else v for k, v in dataloader_output.items()} + # TODO: Check ignore scopes for models! + # We substituted training to validation inference pipeline. + # The graphs for model have been changed. Now, we may don't need some ignored scopes in NNCF config. + kwargs["return_loss"] = False + kwargs["img_metas"] = [kwargs["img_metas"]] + kwargs["img"] = [kwargs["img"]] + # delete labels + new_kwargs = dict() + for key, val in kwargs.items(): + if not key.startswith("gt_"): + new_kwargs[key] = val + + return (), new_kwargs + + nncf_config = NNCFConfig(config.nncf_config) + resuming_state_dict = None + + if dataloader_for_init: + wrapped_loader = _MMInitializeDataLoader(dataloader_for_init) + eval_fn = model_eval_fn if is_accuracy_aware else None + nncf_config = register_default_init_args( + nncf_config, + wrapped_loader, + model_eval_fn=eval_fn, + device=next(model.parameters()).device, + ) + + if config.get("resume_from"): + checkpoint_path = config.get("resume_from") + assert is_checkpoint_nncf(checkpoint_path), ( + "It is possible to resume training with NNCF compression from NNCF checkpoints only. " + 'Use "load_from" with non-compressed model for further compression by NNCF.' + ) + elif config.get("load_from"): + checkpoint_path = config.get("load_from") + if not is_checkpoint_nncf(checkpoint_path): + logger.info("Received non-NNCF checkpoint to start training -- initialization of NNCF fields will be done") + else: + checkpoint_path = None + + if not dataloader_for_init and not checkpoint_path and not init_state_dict: + logger.warning( + "Either dataloader_for_init or NNCF pre-trained " + "model checkpoint should be set. Without this, " + "quantizers will not be initialized" + ) + + if init_state_dict: + assert is_state_nncf(init_state_dict) + meta_state = init_state_dict["meta"][NNCF_META_KEY] + resuming_state_dict = init_state_dict["state_dict"] + compression_state = meta_state.compression_ctrl + elif checkpoint_path: + logger.info(f"Loading NNCF checkpoint from {checkpoint_path}") + logger.info( + "Please, note that this first loading is made before addition of " + "NNCF FakeQuantize nodes to the model, so there may be some " + "warnings on unexpected keys" + ) + compression_state, resuming_state_dict = load_checkpoint(model, checkpoint_path) + logger.info(f"Loaded NNCF checkpoint from {checkpoint_path}") + else: + compression_state = None + + if dummy_forward_fn is None: + assert get_fake_input_fn is not None + + def _get_fake_data_for_forward(nncf_config): + device = next(model.parameters()).device + + if nncf_config.get("input_info", None) and nncf_config.get("input_info").get("sample_size", None): + input_size = nncf_config.get("input_info").get("sample_size") + assert len(input_size) == 4 and input_size[0] == 1 + H, W, C = input_size[2], input_size[3], input_size[1] # pylint: disable=invalid-name + shape = tuple([H, W, C]) + else: + shape = (128, 128, 3) + + with no_nncf_trace(): + return get_fake_input_fn(shape=shape, device=device) + + def dummy_forward_fn(model): + fake_data = _get_fake_data_for_forward(nncf_config) + img, img_metas = fake_data["img"], fake_data["img_metas"] + + ctx = model.nncf_trace_context(img_metas) + with ctx: + # The device where model is could be changed under this context + img = [i.to(next(model.parameters()).device) for i in img] + # Marking data as NNCF network input must be after device movement + img = [nncf_model_input(i) for i in img] + model(img) + + if wrap_inputs_fn is None: + + def wrap_inputs_fn(args, kwargs): + img = kwargs.get("img") if "img" in kwargs else args[0] + if isinstance(img, list): + assert len(img) == 1, "Input list must have a length 1" + assert torch.is_tensor(img[0]), "Input for a model must be a tensor" + img[0] = nncf_model_input(img[0]) + else: + assert torch.is_tensor(img), "Input for a model must be a tensor" + img = nncf_model_input(img) + if "img" in kwargs: + kwargs["img"] = img + else: + args = (img, *args[1:]) + return args, kwargs + + if "log_dir" in nncf_config: + os.makedirs(nncf_config["log_dir"], exist_ok=True) + + uncompressed_model_accuracy = None + if is_accuracy_aware_training(nncf_config) and model_eval_fn is not None: + # Evaluate model before compressing + uncompressed_model_accuracy = model_eval_fn(model) + + model._register_state_dict_hook(nncf_state_dict_hook) + model._register_load_state_dict_pre_hook(nncf_state_dict_pre_hook) + + compression_ctrl, model = create_compressed_model( + model, + nncf_config, + dummy_forward_fn=dummy_forward_fn, + wrap_inputs_fn=wrap_inputs_fn, + compression_state=compression_state, + ) + + if uncompressed_model_accuracy is not None: + model.nncf._uncompressed_model_accuracy = uncompressed_model_accuracy + + # Hiding signature of the forward method is required for model export to work + model.__class__.forward.__signature__ = inspect.Signature( + [ + inspect.Parameter("args", inspect.Parameter.VAR_POSITIONAL), + inspect.Parameter("kwargs", inspect.Parameter.VAR_KEYWORD), + ] + ) + + if resuming_state_dict: + load_state(model, resuming_state_dict, is_resume=True) + + return compression_ctrl, model diff --git a/src/otx/algorithms/common/adapters/mmcv/ops/__init__.py b/src/otx/algorithms/common/adapters/mmcv/ops/__init__.py new file mode 100644 index 00000000000..eeae7de787f --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/ops/__init__.py @@ -0,0 +1,8 @@ +"""Initial file for mmcv ops.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .multi_scale_deformable_attn_pytorch import multi_scale_deformable_attn_pytorch + +__all__ = ["multi_scale_deformable_attn_pytorch"] diff --git a/src/otx/algorithms/common/adapters/mmcv/ops/multi_scale_deformable_attn_pytorch.py b/src/otx/algorithms/common/adapters/mmcv/ops/multi_scale_deformable_attn_pytorch.py new file mode 100644 index 00000000000..cb37d284407 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/ops/multi_scale_deformable_attn_pytorch.py @@ -0,0 +1,163 @@ +"""Custom patch of multi_scale_deformable_attn_pytorch for openvino export.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +import torch.nn.functional as F +from mmcv.ops import multi_scale_deform_attn + +from otx.utils.logger import get_logger + +logger = get_logger() + + +def multi_scale_deformable_attn_pytorch( + value: torch.Tensor, + value_spatial_shapes: torch.Tensor, + sampling_locations: torch.Tensor, + attention_weights: torch.Tensor, +) -> torch.Tensor: + """Custom patch for multi_scale_deformable_attn_pytorch function. + + Original implementation in mmcv.ops use torch.nn.functional.grid_sample. + It raises errors during inference with OpenVINO exported model. + Therefore this function change grid_sample function to _custom_grid_sample function. + """ + + bs, _, num_heads, embed_dims = value.shape + _, num_queries, num_heads, num_levels, num_points, _ = sampling_locations.shape + value_list = value.split([H_ * W_ for H_, W_ in value_spatial_shapes], dim=1) + sampling_grids = 2 * sampling_locations - 1 + sampling_value_list = [] + for level, (H_, W_) in enumerate(value_spatial_shapes): + # bs, H_*W_, num_heads, embed_dims -> + # bs, H_*W_, num_heads*embed_dims -> + # bs, num_heads*embed_dims, H_*W_ -> + # bs*num_heads, embed_dims, H_, W_ + value_l_ = value_list[level].flatten(2).transpose(1, 2).reshape(bs * num_heads, embed_dims, H_, W_) + # bs, num_queries, num_heads, num_points, 2 -> + # bs, num_heads, num_queries, num_points, 2 -> + # bs*num_heads, num_queries, num_points, 2 + sampling_grid_l_ = sampling_grids[:, :, :, level].transpose(1, 2).flatten(0, 1) + # bs*num_heads, embed_dims, num_queries, num_points + sampling_value_l_ = _custom_grid_sample( + value_l_, + sampling_grid_l_, + # mode='bilinear', + # padding_mode='zeros', + align_corners=False, + ) + sampling_value_list.append(sampling_value_l_) + # (bs, num_queries, num_heads, num_levels, num_points) -> + # (bs, num_heads, num_queries, num_levels, num_points) -> + # (bs, num_heads, 1, num_queries, num_levels*num_points) + attention_weights = attention_weights.transpose(1, 2).reshape( + bs * num_heads, 1, num_queries, num_levels * num_points + ) + output = ( + (torch.stack(sampling_value_list, dim=-2).flatten(-2) * attention_weights) + .sum(-1) + .view(bs, num_heads * embed_dims, num_queries) + ) + return output.transpose(1, 2).contiguous() + + +_warning_custom_grid_sample = False + + +def _custom_grid_sample(im: torch.Tensor, grid: torch.Tensor, align_corners: bool = False) -> torch.Tensor: + """Custom patch for mmcv.ops.point_sample.bilinear_grid_sample. + + This function is almost same with mmcv.ops.point_sample.bilinear_grid_sample. + The only difference is this function use reshape instead of view. + + Args: + im (torch.Tensor): Input feature map, shape (N, C, H, W) + grid (torch.Tensor): Point coordinates, shape (N, Hg, Wg, 2) + align_corners (bool): If set to True, the extrema (-1 and 1) are + considered as referring to the center points of the input’s + corner pixels. If set to False, they are instead considered as + referring to the corner points of the input’s corner pixels, + making the sampling more resolution agnostic. + + Returns: + torch.Tensor: A tensor with sampled points, shape (N, C, Hg, Wg) + """ + ori_device = im.device + + if ori_device != "cpu": + global _warning_custom_grid_sample # noqa: PLW0603 + if not _warning_custom_grid_sample: + logger.warning( + "Sampling during 'multi_scale_deformable_attn_pytorch' is executed on CPU to avoid out of memory." + ) + _warning_custom_grid_sample = True + im = im.to("cpu") + grid = grid.to("cpu") + + n, c, h, w = im.shape + gn, gh, gw, _ = grid.shape + assert n == gn + + x = grid[:, :, :, 0] + y = grid[:, :, :, 1] + + if align_corners: + x = ((x + 1) / 2) * (w - 1) + y = ((y + 1) / 2) * (h - 1) + else: + x = ((x + 1) * w - 1) / 2 + y = ((y + 1) * h - 1) / 2 + + x = x.reshape(n, -1) + y = y.reshape(n, -1) + + x0 = torch.floor(x).long() + y0 = torch.floor(y).long() + x1 = x0 + 1 + y1 = y0 + 1 + + wa = ((x1 - x) * (y1 - y)).unsqueeze(1) + wb = ((x1 - x) * (y - y0)).unsqueeze(1) + wc = ((x - x0) * (y1 - y)).unsqueeze(1) + wd = ((x - x0) * (y - y0)).unsqueeze(1) + + # Apply default for grid_sample function zero padding + im_padded = F.pad(im, pad=[1, 1, 1, 1], mode="constant", value=0) + padded_h = h + 2 + padded_w = w + 2 + # save points positions after padding + x0, x1, y0, y1 = x0 + 1, x1 + 1, y0 + 1, y1 + 1 + + # Clip coordinates to padded image size + x0 = torch.where(x0 < 0, torch.tensor(0).to("cpu"), x0) + x0 = torch.where(x0 > padded_w - 1, torch.tensor(padded_w - 1).to("cpu"), x0) + x1 = torch.where(x1 < 0, torch.tensor(0).to("cpu"), x1) + x1 = torch.where(x1 > padded_w - 1, torch.tensor(padded_w - 1).to("cpu"), x1) + y0 = torch.where(y0 < 0, torch.tensor(0).to("cpu"), y0) + y0 = torch.where(y0 > padded_h - 1, torch.tensor(padded_h - 1).to("cpu"), y0) + y1 = torch.where(y1 < 0, torch.tensor(0).to("cpu"), y1) + y1 = torch.where(y1 > padded_h - 1, torch.tensor(padded_h - 1).to("cpu"), y1) + + im_padded = im_padded.view(n, c, -1) + + x0_y0 = (x0 + y0 * padded_w).unsqueeze(1).expand(-1, c, -1) + x0_y1 = (x0 + y1 * padded_w).unsqueeze(1).expand(-1, c, -1) + x1_y0 = (x1 + y0 * padded_w).unsqueeze(1).expand(-1, c, -1) + x1_y1 = (x1 + y1 * padded_w).unsqueeze(1).expand(-1, c, -1) + + Ia = torch.gather(im_padded, 2, x0_y0) + Ib = torch.gather(im_padded, 2, x0_y1) + Ic = torch.gather(im_padded, 2, x1_y0) + Id = torch.gather(im_padded, 2, x1_y1) + + result = (Ia * wa + Ib * wb + Ic * wc + Id * wd).reshape(n, c, gh, gw) + + if ori_device != "cpu": + return result.to(ori_device) + return result + + +multi_scale_deform_attn.multi_scale_deformable_attn_pytorch = multi_scale_deformable_attn_pytorch diff --git a/src/otx/algorithms/common/adapters/mmcv/pipelines/__init__.py b/src/otx/algorithms/common/adapters/mmcv/pipelines/__init__.py new file mode 100644 index 00000000000..274a4d10038 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/pipelines/__init__.py @@ -0,0 +1,4 @@ +"""Pipelines for mmcv.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/common/adapters/mmcv/pipelines/load_image_from_otx_dataset.py b/src/otx/algorithms/common/adapters/mmcv/pipelines/load_image_from_otx_dataset.py new file mode 100644 index 00000000000..b5df29dcd96 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/pipelines/load_image_from_otx_dataset.py @@ -0,0 +1,210 @@ +"""Pipeline element that loads an image from a OTX Dataset on the fly.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from tempfile import TemporaryDirectory +from typing import Any, Dict, Optional, Tuple + +import numpy as np + +from otx.algorithms.common.utils.data import get_image +from otx.core.data.caching import MemCacheHandlerError, MemCacheHandlerSingleton + +_CACHE_DIR = TemporaryDirectory(prefix="img-cache-") # pylint: disable=consider-using-with + +# TODO: refactoring to common modules + + +class LoadImageFromOTXDataset: + """Pipeline element that loads an image from a OTX Dataset on the fly. + + Can do conversion to float 32 if needed. + Expected entries in the 'results' dict that should be passed to this pipeline element are: + results['dataset_item']: dataset_item from which to load the image + results['dataset_id']: id of the dataset to which the item belongs + results['index']: index of the item in the dataset + + Args: + to_float32 (bool, optional): True to convert images to fp32. defaults to False. + enable_memcache (bool, optional): True to enable in-memory cache. defaults to True. + """ + + def __init__(self, to_float32: bool = False, enable_memcache: bool = True): + self._to_float32 = to_float32 + self._enable_memcache = enable_memcache + + @staticmethod + def _get_unique_key(results: Dict[str, Any]) -> Tuple: + """Returns unique key of data item based on the contents.""" + # TODO: We should improve it by assigning an unique id to DatasetItemEntity. + # This is because there is a case which + # d_item.media.path is None, but d_item.media.data is not None + if "cache_key" in results: + return results["cache_key"] + d_item = results["dataset_item"] + results["cache_key"] = d_item.media.path, d_item.roi.id + return results["cache_key"] + + def _get_memcache_handler(self): + """Get memcache handler.""" + try: + mem_cache_handler = MemCacheHandlerSingleton.get() + except MemCacheHandlerError: + # Create a null handler + MemCacheHandlerSingleton.create(mode="null", mem_size=0) + mem_cache_handler = MemCacheHandlerSingleton.get() + + return mem_cache_handler + + def __call__(self, results: Dict[str, Any]): + """Callback function of LoadImageFromOTXDataset.""" + img = None + mem_cache_handler = self._get_memcache_handler() + + if self._enable_memcache: + key = self._get_unique_key(results) + img, meta = mem_cache_handler.get(key) + + if img is None: + # Get image (possibly from file cache) + img = get_image(results, _CACHE_DIR.name, to_float32=False) + if self._enable_memcache: + mem_cache_handler.put(key, img) + + if self._to_float32: + img = img.astype(np.float32) + shape = img.shape + + if img.shape[0] != results["height"]: + results["height"] = img.shape[0] + + if img.shape[1] != results["width"]: + results["width"] = img.shape[1] + + filename = f"Dataset item index {results['index']}" + results["filename"] = filename + results["ori_filename"] = filename + results["img"] = img + results["img_shape"] = shape + results["ori_shape"] = shape + # Set initial values for default meta_keys + results["pad_shape"] = shape + num_channels = 1 if len(shape) < 3 else shape[2] + results["img_norm_cfg"] = dict( + mean=np.zeros(num_channels, dtype=np.float32), + std=np.ones(num_channels, dtype=np.float32), + to_rgb=False, + ) + results["img_fields"] = ["img"] + results["entity_id"] = results.get("entity_id") + results["label_id"] = results.get("label_id") + + return results + + +class LoadResizeDataFromOTXDataset(LoadImageFromOTXDataset): + """Load and resize image & annotation with cache support. + + This base operation loads image and optionally loads annotations. + Then, resize the image and annotation accordingly if resize_cfg given & it's beneficial, + e.g. the size is smaller than original input size. + Finally, if enabled, cache the result and use pre-computed ones from next iterations. + + Args: + load_ann_cfg (Dict, optional): Optionally creates annotation loading operation based on the config. + Defaults to None. + resize_cfg (Dict, optional): Optionally creates resize operation based on the config. Defaults to None. + """ + + def __init__( + self, + load_ann_cfg: Optional[Dict] = None, + resize_cfg: Optional[Dict] = None, + **kwargs, + ): + self._enable_outer_memcache = kwargs.get("enable_memcache", True) + kwargs["enable_memcache"] = False # will use outer cache + super().__init__(**kwargs) + self._load_ann_op = self._create_load_ann_op(load_ann_cfg) + self._downscale_only = resize_cfg.pop("downscale_only", False) if resize_cfg else False + self._resize_op = self._create_resize_op(resize_cfg) + if self._resize_op is not None: + self._resize_shape = resize_cfg.get("size", resize_cfg.get("img_scale")) + if isinstance(self._resize_shape, int): + self._resize_shape = (self._resize_shape, self._resize_shape) + assert isinstance(self._resize_shape, tuple), f"Random scale is not supported by {self.__class__.__name__}" + else: + self._resize_shape = None + + def _create_load_ann_op(self, cfg: Optional[Dict]) -> Optional[Any]: + """Creates annotation loading operation.""" + return None # Should be overrided in task-specific implementation + + def _create_resize_op(self, cfg: Optional[Dict]) -> Optional[Any]: + """Creates resize operation.""" + return None # Should be overrided in task-specific implementation + + def _load_img(self, results: Dict[str, Any]) -> Dict[str, Any]: + """Load image and fill the results dict.""" + return super().__call__(results) # Use image load logic from base class + + def _load_ann_if_any(self, results: Dict[str, Any]) -> Dict[str, Any]: + """Load annotations and fill the results dict.""" + if self._load_ann_op is None: + return results + return self._load_ann_op(results) + + def _resize_img_ann_if_any(self, results: Dict[str, Any]) -> Dict[str, Any]: + """Resize image and annotations if needed and fill the results dict.""" + if self._resize_op is None: + return results + original_shape = results.get("img_shape", self._resize_shape) + if original_shape is None: + return results + if self._downscale_only: + if original_shape[0] * original_shape[1] <= self._resize_shape[0] * self._resize_shape[1]: + # No benfit of early resizing if resize_shape is larger than original_shape + return results + return self._resize_op(results) + + def _load_cache(self, results: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Try to load pre-computed results from cache.""" + + if not self._enable_outer_memcache: + return None + key = self._get_unique_key(results) + + mem_cache_handler = self._get_memcache_handler() + img, meta = mem_cache_handler.get(key) + if img is None or meta is None: + return None + dataset_item = results.pop("dataset_item") + results = meta.copy() + results["img"] = img + results["dataset_item"] = dataset_item + return results + + def _save_cache(self, results: Dict[str, Any]): + """Try to save pre-computed results to cache.""" + if not self._enable_outer_memcache: + return + + key = self._get_unique_key(results) + meta = results.copy() + img = meta.pop("img") + + mem_cache_handler = self._get_memcache_handler() + mem_cache_handler.put(key, img, meta) + + def __call__(self, results: Dict[str, Any]) -> Dict[str, Any]: + """Callback function.""" + results = results.copy() + cached_results = self._load_cache(results) + if cached_results: + return cached_results + results = self._load_img(results) + results = self._load_ann_if_any(results) + results.pop("dataset_item", None) # Prevent deepcopy or caching + results = self._resize_img_ann_if_any(results) + self._save_cache(results) + return results diff --git a/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/__init__.py b/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/__init__.py new file mode 100644 index 00000000000..48f52baf0c8 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/__init__.py @@ -0,0 +1,4 @@ +"""Transforms for mmcv.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/augments.py b/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/augments.py new file mode 100644 index 00000000000..f8b2ed09e67 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/augments.py @@ -0,0 +1,216 @@ +"""Module for defining Augments and CythonArguments class used for classification task.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import random +from typing import Union + +from numpy import ndarray as CvImage +from PIL import Image, ImageEnhance, ImageOps +from PIL.Image import Image as PILImage +from PIL.Image import Resampling + +# type: ignore[attr-defined] +# pylint: disable = no-name-in-module +import otx.algorithms.common.adapters.mmcv.pipelines.transforms.cython_augments.pil_augment as pil_aug + +ImgTypes = Union[PILImage, CvImage] + + +class Augments: # pylint: disable=unused-argument + """Augments class that implements various augmentations via plain PIL.""" + + @staticmethod + def _check_args_tf(kwargs): + def _interpolation(kwargs): + interpolation = kwargs.pop("resample", Resampling.BILINEAR) + if isinstance(interpolation, (list, tuple)): + # disable B311 random - used for the random sampling not for security/crypto + return random.choice(interpolation) # nosec B311 + return interpolation + + new_kwargs = {**kwargs, "resample": _interpolation(kwargs)} + return new_kwargs + + @staticmethod + def autocontrast(img: PILImage, *args, **kwargs) -> PILImage: + """Apply autocontrast for an given image.""" + return ImageOps.autocontrast(img) + + @staticmethod + def equalize(img: PILImage, *args, **kwargs) -> PILImage: + """Apply equalize for an given image.""" + return ImageOps.equalize(img) + + @staticmethod + def solarize(img: PILImage, threshold: int, *args, **kwargs) -> PILImage: + """Apply solarize for an given image.""" + return ImageOps.solarize(img, threshold) + + @staticmethod + def posterize(img: PILImage, bits_to_keep: int, *args, **kwargs) -> PILImage: + """Apply posterize for an given image.""" + if bits_to_keep >= 8: + return img + return ImageOps.posterize(img, bits_to_keep) + + @staticmethod + def color(img: PILImage, factor: float, *args, **kwargs) -> PILImage: + """Apply color for an given image.""" + return ImageEnhance.Color(img).enhance(factor) + + @staticmethod + def contrast(img: PILImage, factor: float, *args, **kwargs) -> PILImage: + """Apply contrast for an given image.""" + return ImageEnhance.Contrast(img).enhance(factor) + + @staticmethod + def brightness(img: PILImage, factor: float, *args, **kwargs) -> PILImage: + """Apply brightness for an given image.""" + return ImageEnhance.Brightness(img).enhance(factor) + + @staticmethod + def sharpness(img: PILImage, factor: float, *args, **kwargs) -> PILImage: + """Apply sharpness for an given image.""" + return ImageEnhance.Sharpness(img).enhance(factor) + + @staticmethod + def rotate(img: PILImage, degree: float, *args, **kwargs) -> PILImage: + """Apply rotate for an given image.""" + kwargs = Augments._check_args_tf(kwargs) + return img.rotate(degree, **kwargs) + + @staticmethod + def shear_x(img: PILImage, factor: float, *args, **kwargs) -> PILImage: + """Apply shear_x for an given image.""" + kwargs = Augments._check_args_tf(kwargs) + return img.transform(img.size, Image.AFFINE, (1, factor, 0, 0, 1, 0), **kwargs) + + @staticmethod + def shear_y(img: PILImage, factor: float, *args, **kwargs) -> PILImage: + """Apply shear_y for an given image.""" + kwargs = Augments._check_args_tf(kwargs) + return img.transform(img.size, Image.AFFINE, (1, 0, 0, factor, 1, 0), **kwargs) + + @staticmethod + def translate_x_rel(img: PILImage, pct: float, *args, **kwargs) -> PILImage: + """Apply translate_x_rel for an given image.""" + kwargs = Augments._check_args_tf(kwargs) + pixels = pct * img.size[0] + return img.transform(img.size, Image.AFFINE, (1, 0, pixels, 0, 1, 0), **kwargs) + + @staticmethod + def translate_y_rel(img: PILImage, pct: float, *args, **kwargs) -> PILImage: + """Apply translate_y_rel for an given image.""" + kwargs = Augments._check_args_tf(kwargs) + pixels = pct * img.size[1] + return img.transform(img.size, Image.AFFINE, (1, 0, 0, 0, 1, pixels), **kwargs) + + +class CythonAugments(Augments): + """CythonAugments class that support faster augmentation with cythonizing.""" + + @staticmethod + def autocontrast(img: ImgTypes, *args, **kwargs) -> ImgTypes: + """Apply autocontrast for an given image.""" + if Image.isImageType(img): + return pil_aug.autocontrast(img) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def equalize(img: ImgTypes, *args, **kwargs) -> ImgTypes: + """Apply equalize for an given image.""" + if Image.isImageType(img): + return pil_aug.equalize(img) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def solarize(img: ImgTypes, threshold: int, *args, **kwargs) -> ImgTypes: + """Apply solarize for an given image.""" + if Image.isImageType(img): + return pil_aug.solarize(img, threshold) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def posterize(img: ImgTypes, bits_to_keep: int, *args, **kwargs) -> ImgTypes: + """Apply posterize for an given image.""" + if Image.isImageType(img): + if bits_to_keep >= 8: + return img + return pil_aug.posterize(img, bits_to_keep) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def color(img: ImgTypes, factor: float, *args, **kwargs) -> ImgTypes: + """Apply color for an given image.""" + if Image.isImageType(img): + return pil_aug.color(img, factor) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def contrast(img: ImgTypes, factor: float, *args, **kwargs) -> ImgTypes: + """Apply contrast for an given image.""" + if Image.isImageType(img): + return pil_aug.contrast(img, factor) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def brightness(img: ImgTypes, factor: float, *args, **kwargs) -> ImgTypes: + """Apply brightness for an given image.""" + if Image.isImageType(img): + return pil_aug.brightness(img, factor) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def sharpness(img: ImgTypes, factor: float, *args, **kwargs) -> ImgTypes: + """Apply sharpness for an given image.""" + if Image.isImageType(img): + return pil_aug.sharpness(img, factor) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def rotate(img: ImgTypes, degree: float, *args, **kwargs) -> ImgTypes: + """Apply rotate for an given image.""" + Augments._check_args_tf(kwargs) + + if Image.isImageType(img): + return pil_aug.rotate(img, degree) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def shear_x(img: ImgTypes, factor: float, *args, **kwargs) -> ImgTypes: + """Apply shear_x for an given image.""" + Augments._check_args_tf(kwargs) + if Image.isImageType(img): + return pil_aug.shear_x(img, factor) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def shear_y(img: ImgTypes, factor: float, *args, **kwargs) -> ImgTypes: + """Apply shear_y for an given image.""" + if Image.isImageType(img): + return pil_aug.shear_y(img, factor) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def translate_x_rel(img: ImgTypes, pct: float, *args, **kwargs) -> ImgTypes: + """Apply translate_x_rel for an given image.""" + if Image.isImageType(img): + return pil_aug.translate_x_rel(img, pct) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def translate_y_rel(img: ImgTypes, pct: float, *args, **kwargs) -> ImgTypes: + """Apply translate_y_rel for an given image.""" + if Image.isImageType(img): + return pil_aug.translate_y_rel(img, pct) + raise NotImplementedError(f"Unknown type: {type(img)}") + + @staticmethod + def blend(src: ImgTypes, dst: CvImage, weight: float = 0.0): + """Apply blend for an given image.""" + assert isinstance(dst, CvImage), f"Type of dst should be numpy array, but type(dst)={type(dst)}." + if Image.isImageType(src): + return pil_aug.blend(src, dst, weight) + raise NotImplementedError(f"Unknown type: {type(src)}") diff --git a/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/__init__.py b/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/__init__.py new file mode 100644 index 00000000000..25b78835d40 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/__init__.py @@ -0,0 +1,3 @@ +"""Module to init cython augments.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/cv_augment.pyx b/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/cv_augment.pyx new file mode 100644 index 00000000000..c07d951b313 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/cv_augment.pyx @@ -0,0 +1,146 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# cython: language_level=3 + +import cython +import numpy as np + +cimport numpy as np + +np.import_array() +import cv2 +from PIL import Image + +ctypedef np.int32_t INT32_t +ctypedef np.uint8_t UINT8_t + + +@cython.boundscheck(False) +@cython.wraparound(False) +cpdef np.ndarray[INT32_t, ndim=1] c_histogram(const UINT8_t[:, :, :] image): + cdef np.ndarray[INT32_t, ndim=1] hist = np.zeros((768,), dtype=np.int32) + cdef INT32_t [:] hist_view = hist + cdef int height, width, y, x + cdef UINT8_t r, g, b + + height = image.shape[0] + width = image.shape[1] + + for y in range(height): + for x in range(width): + hist_view[image[y][x][0] + 000] += 1 + hist_view[image[y][x][1] + 256] += 1 + hist_view[image[y][x][2] + 512] += 1 + + return hist + + +@cython.boundscheck(False) +@cython.wraparound(False) +cdef void _c_lut(np.ndarray[UINT8_t, ndim=3] image, const UINT8_t[:] r_lut): + assert np.PyArray_ISCONTIGUOUS(image), "Image should be contiguous." + assert image.ndim == 3, "Image should be 3D array." + assert image.shape[2] == 3, "Image should have 3 channels." + + cdef int h = image.shape[0] + cdef int w = image.shape[1] + cdef int x, y, curr_idx + cdef int r, g, b + cdef UINT8_t* data_ptr = &image[0,0,0] + + for y in range(h): + for x in range(w): + r = image[y, x, 0] + 000 + g = image[y, x, 1] + 256 + b = image[y, x, 2] + 512 + + curr_idx = 3 * w * y + 3 * x + + data_ptr[curr_idx + 0] = r_lut[r] + data_ptr[curr_idx + 1] = r_lut[g] + data_ptr[curr_idx + 2] = r_lut[b] + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def autocontrast(np.ndarray[UINT8_t, ndim=3] image, cutoff=0, ignore=None) -> np.ndarray: + cdef int* h + cdef int i, lo, hi, ix, cut, layer + cdef double scale, offset + cdef int[:] histogram + cdef np.ndarray[UINT8_t, ndim=1] lut = np.zeros((768,), dtype=np.uint8) + cdef UINT8_t[:] lut_view = lut + cdef np.ndarray[UINT8_t, ndim=3] result + + histogram = c_histogram(image) + + for layer in range(0, 768, 256): + h = &histogram[layer] + + if ignore is not None: + # get rid of outliers + try: + h[ignore] = 0 + except TypeError: + # assume sequence + for ix in ignore: + h[ix] = 0 + if cutoff: + # cut off pixels from both ends of the histogram + if not isinstance(cutoff, tuple): + cutoff = (cutoff, cutoff) + # get number of pixels + n = 0 + for ix in range(256): + n = n + h[ix] + # remove cutoff% pixels from the low end + cut = n * cutoff[0] // 100 + for lo in range(256): + if cut > h[lo]: + cut = cut - h[lo] + h[lo] = 0 + else: + h[lo] -= cut + cut = 0 + if cut <= 0: + break + # remove cutoff% samples from the high end + cut = n * cutoff[1] // 100 + for hi in range(255, -1, -1): + if cut > h[hi]: + cut = cut - h[hi] + h[hi] = 0 + else: + h[hi] -= cut + cut = 0 + if cut <= 0: + break + # find lowest/highest samples after preprocessing + for lo in range(256): + if h[lo]: + break + for hi in range(255, -1, -1): + if h[hi]: + break + if hi <= lo: + # don't bother + for i in range(256): + lut_view[layer + i] = i + else: + scale = 255.0 / (hi - lo) + offset = -lo * scale + for ix in range(256): + i = ix + ix = (int)(ix * scale + offset) + if ix < 0: + ix = 0 + elif ix > 255: + ix = 255 + lut_view[layer + i] = ix + + layer += 256 + + _c_lut(image, lut_view) + return image diff --git a/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/pil_augment.pyx b/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/pil_augment.pyx new file mode 100644 index 00000000000..d51d6215215 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/pipelines/transforms/cython_augments/pil_augment.pyx @@ -0,0 +1,500 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# cython: language_level=3 + +import cython + +from cython.view cimport array as cvarray + +import numpy as np + +cimport numpy as np + +import cv2 +from PIL import Image +from PIL.Image import Resampling + +np.import_array() + + +cdef struct PixelRGBA: + unsigned char r + unsigned char g + unsigned char b + unsigned char a + + +cdef struct ImageInfo: + int width + int height + PixelRGBA** img_ptr + + +cdef ImageInfo parse_img_info(image: Image): + cdef ImageInfo info + cdef unsigned long long ptr_val + + info.width = image.size[0] + info.height = image.size[1] + + ptr_val = dict(image.getdata().unsafe_ptrs)['image'] + info.img_ptr = (ptr_val) + + return info + + +cdef inline int L24(PixelRGBA rgb): + return rgb.r * 19595 + rgb.g * 38470 + rgb.b * 7471 + 0x8000 + + +cdef inline unsigned char clip(float v): + if v < 0.0: + return 0 + if v >= 255.0: + return 255 + + return v + + +@cython.boundscheck(False) +@cython.wraparound(False) +cdef void _c_lut(image: Image, int[:] lut): + cdef ImageInfo info + info = parse_img_info(image) + + for y in range(info.height): + for x in range(info.width): + info.img_ptr[y][x].r = lut[info.img_ptr[y][x].r] + info.img_ptr[y][x].g = lut[info.img_ptr[y][x].g + 256] + info.img_ptr[y][x].b = lut[info.img_ptr[y][x].b + 512] + + +@cython.boundscheck(False) +@cython.wraparound(False) +cdef int[:] c_histogram(image: Image): + cdef ImageInfo info + cdef int x, y + cdef int[:] hist = cvarray(shape=(768,), itemsize=sizeof(int), format="i") + + info = parse_img_info(image) + + for x in range(768): + hist[x] = 0 + + for y in range(info.height): + for x in range(info.width): + hist[info.img_ptr[y][x].r] += 1 + hist[info.img_ptr[y][x].g + 256] += 1 + hist[info.img_ptr[y][x].b + 512] += 1 + + return hist + + +def histogram(image: Image): + cdef int[:] hist = c_histogram(image) + cdef int i + cdef int return_vals[768] + + for i in range(768): + return_vals[i] = hist[i] + + return return_vals + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def autocontrast(image: Image, cutoff=0, ignore=None): + if image.mode != "RGB": + image = image.convert("RGB") + cdef int layer = 0 + cdef int* h + cdef int i, lo, hi, ix, cut, n + cdef double scale, offset + cdef int[:] histogram + cdef int[:] lut = cvarray(shape=(768,), itemsize=sizeof(int), format="i") + + histogram = c_histogram(image) + + for layer in range(0, 768, 256): + h = &histogram[layer] + + if ignore is not None: + # get rid of outliers + try: + h[ignore] = 0 + except TypeError: + # assume sequence + for ix in ignore: + h[ix] = 0 + if cutoff: + # cut off pixels from both ends of the histogram + if not isinstance(cutoff, tuple): + cutoff = (cutoff, cutoff) + # get number of pixels + n = 0 + for ix in range(256): + n = n + h[ix] + # remove cutoff% pixels from the low end + cut = n * cutoff[0] // 100 + for lo in range(256): + if cut > h[lo]: + cut = cut - h[lo] + h[lo] = 0 + else: + h[lo] -= cut + cut = 0 + if cut <= 0: + break + # remove cutoff% samples from the high end + cut = n * cutoff[1] // 100 + for hi in range(255, -1, -1): + if cut > h[hi]: + cut = cut - h[hi] + h[hi] = 0 + else: + h[hi] -= cut + cut = 0 + if cut <= 0: + break + # find lowest/highest samples after preprocessing + for lo in range(256): + if h[lo]: + break + for hi in range(255, -1, -1): + if h[hi]: + break + if hi <= lo: + # don't bother + for i in range(256): + lut[layer + i] = i + else: + scale = 255.0 / (hi - lo) + offset = -lo * scale + for ix in range(256): + i = ix + ix = (int)(ix * scale + offset) + if ix < 0: + ix = 0 + elif ix > 255: + ix = 255 + lut[layer + i] = ix + + layer += 256 + + _c_lut(image, lut) + + return image + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def equalize(image: Image): + if image.mode != "RGB": + image = image.convert("RGB") + cdef int[:] h + cdef int[:] lut = cvarray(shape=(768,), itemsize=sizeof(int), format="i") + + h = c_histogram(image) + + cdef int b, histo_len, histo_sum, i, n, step, num + cdef int histo[256] + + for b in range(0, 768, 256): + histo_len = 0 + histo_sum = 0 + + for i in range(256): + num = h[b + i] + if num > 0: + histo[histo_len] = num + histo_sum += num + histo_len += 1 + + if histo_len <= 1: + for i in range(256): + lut[b + i] = i + else: + step = (histo_sum - histo[histo_len - 1]) // 255 + if not step: + for i in range(256): + lut[b + i] = i + else: + n = step // 2 + for i in range(256): + lut[b + i] = n // step + n = n + h[i + b] + + _c_lut(image, lut) + + return image + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def posterize(image: Image, bits: int): + if image.mode != "RGB": + image = image.convert("RGB") + + cdef int[:] lut = cvarray(shape=(768,), itemsize=sizeof(int), format="i") + cdef int i, b, c_bits + cdef unsigned char mask + + c_bits = bits + + mask = ~(2 ** (8 - c_bits) - 1) + for b in range(0, 768, 256): + for i in range(256): + lut[b + i] = i & mask + _c_lut(image, lut) + return image + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def solarize(image: Image, threshold: int = 128): + if image.mode != "RGB": + image = image.convert("RGB") + + cdef int[:] lut = cvarray(shape=(768,), itemsize=sizeof(int), format="i") + cdef int i, b, c_threshold + cdef ImageInfo info + + c_threshold = threshold + + for b in range(0, 768, 256): + for i in range(256): + if i < c_threshold: + lut[b + i] = i + else: + lut[b + i] = 255 - i + + _c_lut(image, lut) + return image + + +@cython.boundscheck(False) +@cython.wraparound(False) +def color(image: Image, factor: float): + if image.mode != "RGB": + image = image.convert("RGB") + + cdef ImageInfo info + cdef int x, y + cdef float grey_val, c_factor + + info = parse_img_info(image) + c_factor = factor + + for y in range(info.height): + for x in range(info.width): + grey_val = (L24(info.img_ptr[y][x]) >> 16) + if 0.0 <= c_factor and c_factor <= 1.0: + info.img_ptr[y][x].r = (info.img_ptr[y][x].r * c_factor + grey_val * (1 - c_factor)) + info.img_ptr[y][x].g = (info.img_ptr[y][x].g * c_factor + grey_val * (1 - c_factor)) + info.img_ptr[y][x].b = (info.img_ptr[y][x].b * c_factor + grey_val * (1 - c_factor)) + else: + info.img_ptr[y][x].r = clip(info.img_ptr[y][x].r * c_factor + grey_val * (1 - c_factor)) + info.img_ptr[y][x].g = clip(info.img_ptr[y][x].g * c_factor + grey_val * (1 - c_factor)) + info.img_ptr[y][x].b = clip(info.img_ptr[y][x].b * c_factor + grey_val * (1 - c_factor)) + + return image + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def contrast(image: Image, factor: float): + if image.mode != "RGB": + image = image.convert("RGB") + + cdef ImageInfo info + cdef int i_sum, x, y + cdef float c_factor, f_mean + + info = parse_img_info(image) + c_factor = factor + + f_mean = 0 + i_sum = 0 + for y in range(info.height): + for x in range(info.width): + i_sum += L24(info.img_ptr[y][x]) >> 16 + f_mean = i_sum / (info.height * info.width) + + for y in range(info.height): + for x in range(info.width): + if 0.0 <= c_factor and c_factor <= 1.0: + info.img_ptr[y][x].r = (info.img_ptr[y][x].r * c_factor + f_mean * (1 - c_factor)) + info.img_ptr[y][x].g = (info.img_ptr[y][x].g * c_factor + f_mean * (1 - c_factor)) + info.img_ptr[y][x].b = (info.img_ptr[y][x].b * c_factor + f_mean * (1 - c_factor)) + else: + info.img_ptr[y][x].r = clip(info.img_ptr[y][x].r * c_factor + f_mean * (1 - c_factor)) + info.img_ptr[y][x].g = clip(info.img_ptr[y][x].g * c_factor + f_mean * (1 - c_factor)) + info.img_ptr[y][x].b = clip(info.img_ptr[y][x].b * c_factor + f_mean * (1 - c_factor)) + + return image + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def brightness(image: Image, factor: float): + if image.mode != "RGB": + image = image.convert("RGB") + + cdef ImageInfo info + cdef int x, y + cdef float c_factor + + info = parse_img_info(image) + c_factor = factor + + for y in range(info.height): + for x in range(info.width): + if 0.0 <= c_factor and c_factor <= 1.0: + info.img_ptr[y][x].r = (info.img_ptr[y][x].r * c_factor) + info.img_ptr[y][x].g = (info.img_ptr[y][x].g * c_factor) + info.img_ptr[y][x].b = (info.img_ptr[y][x].b * c_factor) + else: + info.img_ptr[y][x].r = clip(info.img_ptr[y][x].r * c_factor) + info.img_ptr[y][x].g = clip(info.img_ptr[y][x].g * c_factor) + info.img_ptr[y][x].b = clip(info.img_ptr[y][x].b * c_factor) + + return image + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def sharpness(image: Image, factor: float): + if image.mode != "RGB": + image = image.convert("RGB") + + cdef ImageInfo info + cdef int x, y, i, j + cdef float c_factor + cdef float smooth_kernel[3][3] + smooth_kernel[0][:] = [1 / 13., 1 / 13., 1 / 13.] + smooth_kernel[1][:] = [1 / 13., 5 / 13., 1 / 13.] + smooth_kernel[2][:] = [1 / 13., 1 / 13., 1 / 13.] + cdef float r, g, b, div + + info = parse_img_info(image) + c_factor = factor + + for i in range(3): + for j in range(3): + smooth_kernel[i][j] = smooth_kernel[i][j] * (1 - c_factor) + + smooth_kernel[1][1] += c_factor + + for y in range(1, info.height - 1): + for x in range(1, info.width - 1): + r = g = b = div = 0 + + for i in range(3): + for j in range(3): + r += smooth_kernel[i][j] * info.img_ptr[y + i - 1][x + j - 1].r + g += smooth_kernel[i][j] * info.img_ptr[y + i - 1][x + j - 1].g + b += smooth_kernel[i][j] * info.img_ptr[y + i - 1][x + j - 1].b + + info.img_ptr[y][x].r = clip(r) + info.img_ptr[y][x].g = clip(g) + info.img_ptr[y][x].b = clip(b) + + return image + + +def _convert_flag_pil_to_cv(flag: Resampling) -> int: + flag_map = { + Resampling.NEAREST: cv2.INTER_NEAREST, + Resampling.BOX: cv2.INTER_NEAREST, + Resampling.BILINEAR: cv2.INTER_LINEAR, + Resampling.HAMMING: cv2.INTER_LINEAR, + Resampling.BICUBIC: cv2.INTER_CUBIC, + Resampling.LANCZOS: cv2.INTER_LANCZOS4 + } + + if flag in flag_map: + return flag_map[flag] + return cv2.INTER_LINEAR + + +def rotate(image: Image, angle: float, resample: Resampling = Resampling.BILINEAR): + if image.mode != "RGB": + image = image.convert("RGB") + + image = np.asarray(image) + image_center = tuple(np.array(image.shape[1::-1]) / 2) + rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0) + flags = _convert_flag_pil_to_cv(resample) + result = cv2.warpAffine(image, rot_mat, image.shape[1::-1], flags=flags) + result = Image.fromarray(result) + return result + + +def translate_x_rel(image: Image, pct: float, resample: Resampling = Resampling.BILINEAR): + if image.mode != "RGB": + image = image.convert("RGB") + + pixels = pct * image.width + image = np.asarray(image) + aff_mat = np.asarray((1, 0, -pixels, 0, 1, 1)).reshape([2, 3]) + flags = _convert_flag_pil_to_cv(resample) + result = cv2.warpAffine(image, aff_mat, image.shape[1::-1], flags=flags) + return Image.fromarray(result) + + +def translate_y_rel(image: Image, pct: float, resample: Resampling = Resampling.BILINEAR): + if image.mode != "RGB": + image = image.convert("RGB") + + pixels = pct * image.height + image = np.asarray(image) + aff_mat = np.asarray((1, 0, 0, 0, 1, -pixels)).reshape([2, 3]) + flags = _convert_flag_pil_to_cv(resample) + result = cv2.warpAffine(image, aff_mat, image.shape[1::-1], flags=flags) + return Image.fromarray(result) + + +def shear_x(image: Image, factor: float, resample: Resampling = Resampling.BILINEAR): + if image.mode != "RGB": + image = image.convert("RGB") + + image = np.asarray(image) + aff_mat = np.asarray((1, -factor, 0, 0, 1, 0)).reshape([2, 3]) + flags = _convert_flag_pil_to_cv(resample) + result = cv2.warpAffine(image, aff_mat, image.shape[1::-1], flags=flags) + return Image.fromarray(result) + + +def shear_y(image: Image, factor: float, resample: Resampling = Resampling.BILINEAR): + if image.mode != "RGB": + image = image.convert("RGB") + + image = np.asarray(image) + aff_mat = np.asarray((1, 0, 0, -factor, 1, 0)).reshape([2, 3]) + flags = _convert_flag_pil_to_cv(resample) + result = cv2.warpAffine(image, aff_mat, image.shape[1::-1], flags=flags) + return Image.fromarray(result) + + +@cython.boundscheck(False) +@cython.wraparound(False) +def blend(img_to_mix: Image, np.ndarray[np.float32_t, ndim=3] img_dst, weight: float) -> None: + if img_to_mix.mode != "RGB": + img_to_mix = img_to_mix.convert("RGB") + + cdef ImageInfo info = parse_img_info(img_to_mix) + cdef float c_weight = weight + + for y in range(info.height): + for x in range(info.width): + img_dst[y, x, 0] += c_weight * info.img_ptr[y][x].r + img_dst[y, x, 1] += c_weight * info.img_ptr[y][x].g + img_dst[y, x, 2] += c_weight * info.img_ptr[y][x].b diff --git a/src/otx/algorithms/common/adapters/mmcv/runner.py b/src/otx/algorithms/common/adapters/mmcv/runner.py new file mode 100644 index 00000000000..f74b98ee3ba --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/runner.py @@ -0,0 +1,150 @@ +"""Runner with cancel for common OTX algorithms.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# Is based on +# * https://github.com/open-mmlab/mmcv/blob/master/mmcv/runner/epoch_based_runner.py +# * https://github.com/open-mmlab/mmcv/blob/master/mmcv/runner/iter_based_runner.py + +import time +import warnings +from typing import List, Optional, Sequence + +import mmcv +import torch.distributed as dist +from mmcv.runner import ( + RUNNERS, + EpochBasedRunner, + IterBasedRunner, + IterLoader, + get_dist_info, +) +from mmcv.runner.utils import get_host_info +from torch.utils.data.dataloader import DataLoader + + +# pylint: disable=too-many-instance-attributes, attribute-defined-outside-init +@RUNNERS.register_module() +class EpochRunnerWithCancel(EpochBasedRunner): + """Simple modification to EpochBasedRunner to allow cancelling the training during an epoch. + + A stopping hook should set the runner.should_stop flag to True if stopping is required. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.should_stop = False + _, world_size = get_dist_info() + self.distributed = world_size > 1 + self.save_ema_model = False + + def stop(self) -> bool: + """Returning a boolean to break the training loop. + + This method supports distributed training by broadcasting should_stop to other ranks + :return: a cancellation bool + """ + broadcast_obj = [False] + if self.rank == 0 and self.should_stop: + broadcast_obj = [True] + + if self.distributed: + dist.broadcast_object_list(broadcast_obj, src=0) + if broadcast_obj[0]: + self._max_epochs = self.epoch + return broadcast_obj[0] + + def train(self, data_loader: DataLoader, **kwargs): + """Train call hook.""" + self.model.train() + self.mode = "train" + self.data_loader = data_loader + self._max_iters = self._max_epochs * len(self.data_loader) + self.call_hook("before_train_epoch") + if self.distributed: + time.sleep(2) # Prevent possible multi-gpu deadlock during epoch transition + for i, data_batch in enumerate(self.data_loader): + self._inner_iter = i + self.call_hook("before_train_iter") + self.run_iter(data_batch, train_mode=True, **kwargs) + self.call_hook("after_train_iter") + if self.stop(): + break + self._iter += 1 + self.save_ema_model = False # revert ema status before new iter + self.call_hook("after_train_epoch") + self.stop() + self._epoch += 1 + + +@RUNNERS.register_module() +class IterBasedRunnerWithCancel(IterBasedRunner): + """Runner With Cancel for early-stopping (Iter based). + + Simple modification to IterBasedRunner to allow cancelling the training. The cancel training hook + should set the runner.should_stop flag to True if stopping is required. + + # TODO: Implement cancelling of training via keyboard interrupt signal, instead of should_stop + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.should_stop = False + + def main_loop(self, workflow: List[tuple], iter_loaders: Sequence[IterLoader], **kwargs): + """Main loop function in IterBasedRunnerWithCancel.""" + while self.iter < self._max_iters: + for i, flow in enumerate(workflow): + self._inner_iter = 0 + mode, iters = flow + if not isinstance(mode, str) or not hasattr(self, mode): + raise ValueError(f'runner has no method named "{mode}" to run a workflow') + iter_runner = getattr(self, mode) + for _ in range(iters): + if mode == "train" and self.iter >= self._max_iters: + break + iter_runner(iter_loaders[i], **kwargs) + if self.should_stop: + return + + def run(self, data_loaders: Sequence[DataLoader], workflow: List[tuple], max_iters: Optional[int] = None, **kwargs): + """Function of main run.""" + assert isinstance(data_loaders, list) + assert mmcv.is_list_of(workflow, tuple) + assert len(data_loaders) == len(workflow) + if max_iters is not None: + warnings.warn( + "setting max_iters in run is deprecated, please set max_iters in runner_config", + DeprecationWarning, + ) + self._max_iters = max_iters + assert self._max_iters is not None, "max_iters must be specified during instantiation" + + work_dir = self.work_dir if self.work_dir is not None else "NONE" + self.logger.info("Start running, host: %s, work_dir: %s", get_host_info(), work_dir) + self.logger.info("workflow: %s, max: %d iters", workflow, self._max_iters) + self.call_hook("before_run") + + iter_loaders = [IterLoader(x) for x in data_loaders] + + self.call_hook("before_epoch") + + self.should_stop = False + self.main_loop(workflow, iter_loaders, **kwargs) + self.should_stop = False + + # time.sleep(1) # wait for some hooks like loggers to finish + self.call_hook("after_epoch") + self.call_hook("after_run") diff --git a/src/otx/algorithms/common/adapters/mmcv/semisl_mixin.py b/src/otx/algorithms/common/adapters/mmcv/semisl_mixin.py new file mode 100644 index 00000000000..8be53f8ed23 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/semisl_mixin.py @@ -0,0 +1,62 @@ +"""SemiSL configuration mixin.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import importlib + +from mmcv.utils import Config, ConfigDict + +from otx.algorithms.common.adapters.mmcv.utils import ( + build_dataloader, + build_dataset, +) + + +class SemiSLConfigurerMixin: + """Patch config to support semi supervised learning.""" + + def configure_data_pipeline(self, cfg, input_size, model_ckpt_path, **kwargs): + """Patch cfg.data.""" + super().configure_data_pipeline(cfg, input_size, model_ckpt_path, **kwargs) + # Set unlabeled data hook + if self.training: + if cfg.data.get("unlabeled", False) and cfg.data.unlabeled.get("otx_dataset", False): + if len(cfg.data.unlabeled.get("pipeline", [])) == 0: + cfg.data.unlabeled.pipeline = cfg.data.train.pipeline.copy() + self.configure_unlabeled_dataloader(cfg) + + @staticmethod + def configure_unlabeled_dataloader(cfg: Config): + """Patch for unlabled dataloader.""" + + model_task = {"classification": "mmcls", "detection": "mmdet", "segmentation": "mmseg"} + if "unlabeled" in cfg.data: + task_lib_module = importlib.import_module(f"{model_task[cfg.model_task]}.datasets") + dataset_builder = getattr(task_lib_module, "build_dataset") + dataloader_builder = getattr(task_lib_module, "build_dataloader") + + dataset = build_dataset(cfg, "unlabeled", dataset_builder, consume=True) + unlabeled_dataloader = build_dataloader( + dataset, + cfg, + "unlabeled", + dataloader_builder, + distributed=cfg.distributed, + consume=True, + ) + + custom_hooks = cfg.get("custom_hooks", []) + updated = False + for custom_hook in custom_hooks: + if custom_hook["type"] == "ComposedDataLoadersHook": + custom_hook["data_loaders"] = [*custom_hook["data_loaders"], unlabeled_dataloader] + updated = True + if not updated: + custom_hooks.append( + ConfigDict( + type="ComposedDataLoadersHook", + data_loaders=unlabeled_dataloader, + ) + ) + cfg.custom_hooks = custom_hooks diff --git a/src/otx/algorithms/common/adapters/mmcv/tasks/__init__.py b/src/otx/algorithms/common/adapters/mmcv/tasks/__init__.py new file mode 100644 index 00000000000..20f278eef96 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/tasks/__init__.py @@ -0,0 +1,28 @@ +"""Initialzation OTX Tasks with MMCV framework.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# flake8: noqa +import os + +from .version import __version__, get_version + + +class OTXConstants: + """Various path for OTX.""" + + PACKAGE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + # PACKAGE_ROOT = os.path.dirname(Path(__file__).) + RECIPES_PATH = os.path.join(PACKAGE_ROOT, "recipes") + SAMPLES_PATH = os.path.join(PACKAGE_ROOT, "samples") + MODELS_PATH = os.path.join(PACKAGE_ROOT, "models") + + +# print(f'pkg root ======> {OTXConstants.PACKAGE_ROOT}') + +__all__ = [ + "get_version", + "__version__", + "OTXConstants", +] diff --git a/src/otx/algorithms/common/adapters/mmcv/tasks/exporter.py b/src/otx/algorithms/common/adapters/mmcv/tasks/exporter.py new file mode 100644 index 00000000000..995f46d5399 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/tasks/exporter.py @@ -0,0 +1,110 @@ +"""Base Exporter for OTX tasks.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import traceback + +from otx.utils.logger import get_logger + +logger = get_logger() + + +class Exporter: + """Exporter class for OTX export.""" + + def run(self, cfg, **kwargs): # noqa: C901 + """Run export procedure.""" + logger.info("export!") + + precision = kwargs.pop("precision", "FP32") + export_type = kwargs.pop("type", "OPENVINO") + if precision not in ("FP32", "FP16", "INT8"): + raise NotImplementedError + logger.info(f"Model will be exported with precision {precision}") + model_name = cfg.get("model_name", "model") + + # TODO: handle complicated pipeline + # If test dataset is a wrapper dataset + # pipeline may not include load transformation which is assumed to be included afterwards + # Here, we assume simple wrapper datasets where pipeline of the wrapper is just a consecutive one. + if cfg.data.test.get("dataset", None) or cfg.data.test.get("datasets", None): + dataset = cfg.data.test.get("dataset", cfg.data.test.get("datasets", [None])[0]) + assert dataset is not None + pipeline = dataset.get("pipeline", []) + pipeline += cfg.data.test.get("pipeline", []) + cfg.data.test.pipeline = pipeline + for pipeline in cfg.data.test.pipeline: + if pipeline.get("transforms", None): + transforms = pipeline.transforms + for transform in transforms: + if transform.type == "Collect": + for collect_key in transform["keys"]: + if collect_key != "img": + transform["keys"].remove(collect_key) + + model_builder = kwargs.get("model_builder") + try: + deploy_cfg = kwargs.get("deploy_cfg", None) + if deploy_cfg is not None: + self.mmdeploy_export( + cfg.work_dir, + model_builder, + precision, + export_type, + cfg, + deploy_cfg, + model_name, + ) + else: + self.naive_export(cfg.work_dir, model_builder, precision, export_type, cfg, model_name) + except RuntimeError as ex: + # output_model.model_status = ModelStatus.FAILED + # raise RuntimeError('Optimization was unsuccessful.') from ex + return { + "outputs": None, + "msg": f"exception {type(ex)}: {ex}\n\n{traceback.format_exc()}", + } + + return { + "outputs": { + "bin": os.path.join(cfg.work_dir, f"{model_name}.bin"), + "xml": os.path.join(cfg.work_dir, f"{model_name}.xml"), + "onnx": os.path.join(cfg.work_dir, f"{model_name}.onnx"), + "partitioned": [ + { + f"{os.path.splitext(name)[0]}": { + "bin": os.path.join(cfg.work_dir, name.replace(".onnx", ".bin")), + "xml": os.path.join(cfg.work_dir, name.replace(".onnx", ".xml")), + "onnx": os.path.join(cfg.work_dir, name), + } + } + for name in os.listdir(cfg.work_dir) + if name.endswith(".onnx") and name != f"{model_name}.onnx" + ], + }, + "msg": "", + } + + @staticmethod + def mmdeploy_export( + output_dir, + model_builder, + precision, + export_type, + cfg, + deploy_cfg, + model_name="model", + ): + """Export procedure using mmdeploy backend.""" + from otx.algorithms.common.adapters.mmdeploy.apis import MMdeployExporter + + if precision == "FP16": + deploy_cfg.backend_config.mo_options.flags.append("--compress_to_fp16") + MMdeployExporter.export2backend(output_dir, model_builder, cfg, deploy_cfg, export_type, model_name=model_name) + + @staticmethod + def naive_export(output_dir, model_builder, precision, export_type, cfg, model_name="model"): + """Export using pytorch backend.""" + raise NotImplementedError() diff --git a/src/otx/algorithms/common/adapters/mmcv/tasks/registry.py b/src/otx/algorithms/common/adapters/mmcv/tasks/registry.py new file mode 100644 index 00000000000..9182c760d6c --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/tasks/registry.py @@ -0,0 +1,8 @@ +"""Registry of Explainers.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.utils import Registry + +EXPLAINERS = Registry("explainers") diff --git a/src/otx/algorithms/common/adapters/mmcv/tasks/version.py b/src/otx/algorithms/common/adapters/mmcv/tasks/version.py new file mode 100644 index 00000000000..6d59ff872f2 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/tasks/version.py @@ -0,0 +1,11 @@ +"""Return current version, this should be removed.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +__version__ = "1.2.1" + + +def get_version(): + """Return version.""" + return __version__ diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/__init__.py b/src/otx/algorithms/common/adapters/mmcv/utils/__init__.py new file mode 100644 index 00000000000..4c5a77e1d87 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/utils/__init__.py @@ -0,0 +1,53 @@ +"""OTX Adapters - mmcv.utils.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from ._builder_build_data_parallel import HPUDataParallel, XPUDataParallel, build_data_parallel +from ._config_utils_get_configs_by_keys import get_configs_by_keys +from ._config_utils_get_configs_by_pairs import get_configs_by_pairs +from .automatic_bs import adapt_batch_size +from .builder import build_dataloader, build_dataset +from .config_utils import ( + InputSizeManager, + OTXConfig, + config_from_string, + get_dataset_configs, + is_epoch_based_runner, + patch_adaptive_interval_training, + patch_color_conversion, + patch_early_stopping, + patch_from_hyperparams, + patch_persistent_workers, + prepare_for_testing, + prepare_work_dir, + remove_from_config, + remove_from_configs_by_type, + update_config, +) + +__all__ = [ + "build_dataset", + "build_dataloader", + "build_data_parallel", + "remove_from_config", + "remove_from_configs_by_type", + "get_configs_by_pairs", + "get_configs_by_keys", + "update_config", + "get_dataset_configs", + "prepare_for_testing", + "is_epoch_based_runner", + "config_from_string", + "patch_adaptive_interval_training", + "patch_color_conversion", + "patch_early_stopping", + "patch_persistent_workers", + "prepare_work_dir", + "OTXConfig", + "adapt_batch_size", + "InputSizeManager", + "XPUDataParallel", + "HPUDataParallel", + "patch_from_hyperparams", +] diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/_builder_build_data_parallel.py b/src/otx/algorithms/common/adapters/mmcv/utils/_builder_build_data_parallel.py new file mode 100644 index 00000000000..9f19ba25340 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/utils/_builder_build_data_parallel.py @@ -0,0 +1,164 @@ +"""A file for a function build_data_parallel().""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# NOTE: a workaround for https://github.com/python/mypy/issues/5028 + +import os +from typing import Literal, Union, overload + +import torch +from mmcv import Config +from mmcv.parallel import MMDataParallel, MMDistributedDataParallel + +from otx.algorithms.common.utils import is_hpu_available, is_xpu_available + + +@overload +def build_data_parallel( + model: torch.nn.Module, + config: Config, + *, + distributed: Literal[True], +) -> MMDistributedDataParallel: + ... + + +@overload +def build_data_parallel( + model: torch.nn.Module, + config: Config, + *, + distributed: Literal[False] = False, +) -> MMDataParallel: + ... + + +@overload +def build_data_parallel( + model: torch.nn.Module, + config: Config, + *, + distributed: bool, +) -> Union[MMDataParallel, MMDistributedDataParallel]: + ... + + +def build_data_parallel( + model: torch.nn.Module, + config: Config, + *, + distributed: bool = False, +) -> Union[MMDataParallel, MMDistributedDataParallel]: + """Prepare model for execution. + + Return MMDataParallel or MMDistributedDataParallel model. + + :param model: Model. + :param config: config. + :param distributed: Enable distributed training mode. + :return: + """ + if is_xpu_available() and config.get("gpu_ids", []): + model = model.xpu() + model = XPUDataParallel(model, device_ids=config.gpu_ids) + elif is_hpu_available() and config.get("gpu_ids", []): + model = model.to("hpu") + model = HPUDataParallel(model, device_ids=config.gpu_ids) + elif torch.cuda.is_available() and config.get("gpu_ids", []): + if distributed: + model = model.cuda() + # put model on gpus + find_unused_parameters = config.get("find_unused_parameters", False) + # Sets the `find_unused_parameters` parameter in + # torch.nn.parallel.DistributedDataParallel + model = MMDistributedDataParallel( + model, + device_ids=[int(os.environ["LOCAL_RANK"])], + broadcast_buffers=False, + find_unused_parameters=find_unused_parameters, + ) + else: + model = model.cuda(config.gpu_ids[0]) + model = MMDataParallel(model, device_ids=config.gpu_ids) + else: + # temporarily disable cuda for cpu data parallel + bak = torch.cuda.is_available + setattr(torch.cuda, "is_available", lambda: False) + model = MMDataParallel(model, device_ids=[]) + torch.cuda.is_available = bak + return model + + +class XPUDataParallel(MMDataParallel): + def scatter(self, inputs, kwargs, device_ids): + inputs, kwargs = super().scatter(inputs, kwargs, [-1]) + target_device = torch.device(f"xpu:{device_ids[0]}") + + def change_tensor_device(obj): + if isinstance(obj, list): + obj = list(map(change_tensor_device, obj)) + elif isinstance(obj, tuple): + obj = tuple(map(change_tensor_device, obj)) + elif isinstance(obj, dict): + obj = {key: change_tensor_device(val) for key, val in obj.items()} + elif isinstance(obj, torch.Tensor): + obj = obj.to(target_device) + return obj + + inputs = change_tensor_device(inputs) + kwargs = change_tensor_device(kwargs) + + return inputs, kwargs + + +class HPUDataParallel(MMDataParallel): + def __init__(self, *args, enable_autocast: bool = False, put_gt_on_device=True, **kwargs): + super().__init__(*args, **kwargs) + self.enable_autocast = enable_autocast + self.put_gt_on_device = put_gt_on_device + self.src_device_obj = torch.device("hpu", self.device_ids[0]) + + def scatter(self, inputs, kwargs, device_ids): + inputs, kwargs = super().scatter(inputs, kwargs, [-1]) + + for x in inputs: + if isinstance(x, tuple): + for val in x: + if isinstance(val, dict): + for k in val: + # don't put annotations on the HPU to proceed + # post-processing on the CPU + if not self.put_gt_on_device and k.startswith("gt_"): + continue + if isinstance(val[k], torch.Tensor): + val[k] = val[k].to(self.src_device_obj) + elif isinstance(val[k], list): + for i, item in enumerate(val[k]): + if isinstance(item, torch.Tensor): + val[k][i] = item.to(self.src_device_obj) + + for x in kwargs: + if isinstance(x, dict): + for k in x: + if isinstance(x[k], torch.Tensor): + x[k] = x[k].to(f"hpu:{device_ids[0]}") + elif isinstance(x[k], list): + for i, item in enumerate(x[k]): + if isinstance(item, torch.Tensor): + x[k][i] = item.to(self.src_device_obj) + + return inputs, kwargs + + def forward(self, *inputs, **kwargs): + with torch.cuda.amp.autocast(dtype=torch.bfloat16, enabled=self.enable_autocast): + return super().forward(*inputs, **kwargs) + + def train_step(self, *inputs, **kwargs): + with torch.cuda.amp.autocast(dtype=torch.bfloat16, enabled=self.enable_autocast): + return super().train_step(*inputs, **kwargs) + + def val_step(self, *inputs, **kwargs): + with torch.cuda.amp.autocast(dtype=torch.bfloat16, enabled=self.enable_autocast): + return super().val_step(*inputs, **kwargs) diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/_config_utils_get_configs_by_keys.py b/src/otx/algorithms/common/adapters/mmcv/utils/_config_utils_get_configs_by_keys.py new file mode 100644 index 00000000000..b7b38fdacf5 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/utils/_config_utils_get_configs_by_keys.py @@ -0,0 +1,78 @@ +"""A file for a function get_configs_by_keys().""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# NOTE: a workaround for https://github.com/python/mypy/issues/5028 + +from collections.abc import Mapping +from typing import Any, Dict, List, Literal, Sequence, Tuple, Union, overload + +from mmcv import Config, ConfigDict + + +@overload +def get_configs_by_keys( + configs: Union[Config, ConfigDict, Sequence[Config], Sequence[ConfigDict]], + keys: Union[Any, List[Any]], + *, + return_path: Literal[True], +) -> Dict[Tuple[Any, ...], ConfigDict]: + ... + + +@overload +def get_configs_by_keys( + configs: Union[Config, ConfigDict, Sequence[Config], Sequence[ConfigDict]], + keys: Union[Any, List[Any]], + *, + return_path: Literal[False] = False, +) -> List[ConfigDict]: + ... + + +@overload +def get_configs_by_keys( + configs: Union[Config, ConfigDict, Sequence[Config], Sequence[ConfigDict]], + keys: Union[Any, List[Any]], + *, + return_path: bool, +) -> Union[List[ConfigDict], Dict[Tuple[Any, ...], ConfigDict]]: + ... + + +def get_configs_by_keys( # noqa: C901 + configs: Union[Config, ConfigDict, Sequence[Config], Sequence[ConfigDict]], + keys: Union[Any, List[Any]], + *, + return_path: bool = False, +) -> Union[List[ConfigDict], Dict[Tuple[Any, ...], ConfigDict]]: + """Get a list of configs by keys.""" + + if not isinstance(keys, list): + keys = [keys] + + def get_config(config, path=()): + if path and path[-1] in keys: + return {path: config} + + out = {} + if isinstance(config, (Config, Mapping)): + for key, value in config.items(): + out.update(get_config(value, (*path, key))) + elif isinstance(config, (list, tuple)): + for idx, value in enumerate(config): + out.update(get_config(value, (*path, idx))) + return out + + out = get_config(configs) + if return_path: + return out + + out_: List[ConfigDict] = [] + for found in out.values(): + if isinstance(found, (list, tuple)): + out_.extend(found) + else: + out_.append(found) + return out_ diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/_config_utils_get_configs_by_pairs.py b/src/otx/algorithms/common/adapters/mmcv/utils/_config_utils_get_configs_by_pairs.py new file mode 100644 index 00000000000..4decbd161df --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/utils/_config_utils_get_configs_by_pairs.py @@ -0,0 +1,79 @@ +"""A file for a function get_configs_by_pairs().""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# NOTE: a workaround for https://github.com/python/mypy/issues/5028 + +from collections.abc import Mapping +from typing import Any, Dict, List, Literal, Sequence, Tuple, Union, overload + +from mmcv import Config, ConfigDict + + +@overload +def get_configs_by_pairs( + configs: Union[Config, ConfigDict, Sequence[Config], Sequence[ConfigDict]], + pairs: Union[Dict[Any, Any], List[Dict[Any, Any]]], + *, + return_path: Literal[True], +) -> Dict[Tuple[Any, ...], ConfigDict]: + ... + + +@overload +def get_configs_by_pairs( + configs: Union[Config, ConfigDict, Sequence[Config], Sequence[ConfigDict]], + pairs: Union[Dict[Any, Any], List[Dict[Any, Any]]], + *, + return_path: Literal[False] = False, +) -> List[ConfigDict]: + ... + + +@overload +def get_configs_by_pairs( + configs: Union[Config, ConfigDict, Sequence[Config], Sequence[ConfigDict]], + pairs: Union[Dict[Any, Any], List[Dict[Any, Any]]], + *, + return_path: bool, +) -> Union[List[ConfigDict], Dict[Tuple[Any, ...], ConfigDict]]: + ... + + +def get_configs_by_pairs( # noqa: C901 + configs: Union[Config, ConfigDict, Sequence[Config], Sequence[ConfigDict]], + pairs: Union[Dict[Any, Any], List[Dict[Any, Any]]], + *, + return_path: bool = False, +) -> Union[List[ConfigDict], Dict[Tuple[Any, ...], ConfigDict]]: + """Get a list of configs by key, value pairs.""" + # TODO: multiple instance + + if not isinstance(pairs, list): + pairs = [pairs] + + def get_config(config, path=()): + out = {} + if isinstance(config, (Config, Mapping)): + for pair in pairs: + if all(config.get(key, None) == value for key, value in pair.items()): + return {path: config} + for key, value in config.items(): + out.update(get_config(value, (*path, key))) + elif isinstance(config, (list, tuple)): + for idx, value in enumerate(config): + out.update(get_config(value, (*path, idx))) + return out + + out = get_config(configs) + if return_path: + return out + + out_: List[ConfigDict] = [] + for found in out.values(): + if isinstance(found, (list, tuple)): + out_.extend(found) + else: + out_.append(found) + return out_ diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py b/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py new file mode 100644 index 00000000000..cfc4b6eb07d --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py @@ -0,0 +1,188 @@ +"""Algorithm to find a proper batch size which is fit to current GPU device for tasks using mmcv.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from copy import deepcopy +from math import sqrt +from typing import Callable, Dict, List + +import numpy as np +from torch.cuda import is_available as cuda_available + +from otx.algorithms.common.adapters.torch.utils import BsSearchAlgo +from otx.utils.logger import get_logger + +logger = get_logger() + + +def _set_value_at_dict_in_dict(target: Dict, key_path: str, value): + """Set value at dictionary hierarchy structure. + + This function is for setting a value at leaf dictionary node in dictionary hierarchy structure. + If key doesn't exist in the middle node dictionaray, then make a new dictionary at that and keep going. + For example, if you want to set value at target["a"]["b"]["c"], then you can call the function as below. + _set_value_at_dict_in_dict(target, "a.b.c", value) + + Args: + target (Dict): Target variable. + key_path (str): Dot delimited dictionary key string. + value : Value to set. + """ + keys = key_path.split(".") + for key in keys[:-1]: + if key not in target: + target[key] = {} + target = target[key] + + target[keys[-1]] = value + + +def adapt_batch_size(train_func: Callable, cfg, datasets: List, validate: bool = False, not_increase: bool = True): + """Decrease batch size if default batch size isn't fit to current GPU device. + + This function just setup for single iteration training to reduce time for adapting. + The core part of adapting batch size is done in adapt_batch_size in the torch.utils package. + + Args: + train_func (Callable): The function to train a model. + Only cfg, dataset and meta are passed to the function when invoking it. + cfg: Configuration of a training. + meta (Dict): A dict records some meta information of a training. + datasets (List): List of datasets. + validate (bool): Whether do vlidation or not. + not_increase (bool) : Whether adapting batch size to larger value than default value or not. + """ + + if not cuda_available(): + logger.warning("Skip Auto-adaptive batch size: CUDA should be available, but it isn't.") + return + + def train_func_single_iter(batch_size): + copied_cfg = deepcopy(cfg) + _set_batch_size(copied_cfg, batch_size) + _set_max_epoch(copied_cfg, 1) # setup for training a single iter to reduce time + + # Remove hooks due to reasons below + # OTXProgressHook => prevent progress bar from being 0 and 100 repeatably + # earlystoppinghook => if eval hook is excluded, this hook makes an error due to absence of score history + # CustomEvalHook => exclude validation in classification task + idx_hooks_to_remove = [] + hooks_to_remove = ["OTXProgressHook", "earlystoppinghook", "CustomEvalHook"] + for i, hook in enumerate(copied_cfg.custom_hooks): + if not validate and hook["type"] == "AdaptiveTrainSchedulingHook": + hook["enable_eval_before_run"] = False + for hook_to_remove in hooks_to_remove: + if hook_to_remove.lower() in hook["type"].lower(): + idx_hooks_to_remove.append(i) + + if idx_hooks_to_remove: + idx_hooks_to_remove.sort() + for i in reversed(idx_hooks_to_remove): + del copied_cfg.custom_hooks[i] + + new_datasets = [SubDataset(datasets[0], batch_size)] + + train_func( + dataset=new_datasets, + cfg=copied_cfg, + validate=validate, + ) + + default_bs = _get_batch_size(cfg) + bs_search_algo = BsSearchAlgo( + train_func=train_func_single_iter, + default_bs=default_bs, + max_bs=len(datasets[0]), + ) + if not_increase: + new_batch_size = bs_search_algo.auto_decrease_batch_size() + else: + drop_last = cfg.data.get("train_dataloader", {}).get("drop_last", False) + new_batch_size = bs_search_algo.find_big_enough_batch_size(drop_last) + + if default_bs != new_batch_size: + _set_batch_size(cfg, new_batch_size) + origin_lr = cfg.optimizer.lr + bs_change_ratio = new_batch_size / default_bs + cfg.optimizer.lr *= sqrt(bs_change_ratio) # Using root scale instead of linear scale + + logger.info("Adapting batch size is done.") + logger.info(f"Batch size is adapted : {default_bs} -> {new_batch_size}") + logger.info(f"learning rate is adapted : {origin_lr} -> {cfg.optimizer.lr}") + else: + logger.info("Adapting batch size is done. Batch size isn't changed.") + + +def _get_batch_size(cfg) -> int: + if "action" in str(cfg.domain).lower(): + return cfg.data.videos_per_gpu + return cfg.data.train_dataloader["samples_per_gpu"] + + +def _set_batch_size(cfg, batch_size: int): + if "action" in str(cfg.domain).lower(): + cfg.data.videos_per_gpu = batch_size + else: + cfg.data.train_dataloader["samples_per_gpu"] = batch_size + for custom_hook in cfg.custom_hooks: + if custom_hook["type"] == "AdaptiveRepeatDataHook": + custom_hook["train_batch_size"] = batch_size + + +def _set_max_epoch(cfg, max_epoch: int): + if cfg.runner.get("type") == "AccuracyAwareRunner": # nncf case + if "nncf_config" in cfg.runner: + _set_value_at_dict_in_dict( + cfg.runner["nncf_config"], "accuracy_aware_training.params.maximal_total_epochs", max_epoch + ) + else: + runner_type = cfg.runner.get("type") + if runner_type is not None and "iterbased" in runner_type.lower(): + cfg.runner["max_iters"] = max_epoch + else: + cfg.runner["max_epochs"] = max_epoch + + +class SubDataset: + """Wrapper class to make dataset pretend to have specified number of images. + + Args: + fullset: Original dataset. + num_samples (int): Number of images to pretend to have. It should be positive. + """ + + def __init__(self, fullset, num_samples: int): + if num_samples <= 0: + raise ValueError(f"num_samples should be positive. But, current value is {num_samples}.") + + self.fullset = fullset + self.num_samples = num_samples + self.img_indices = { # for class incremental case + "old": [i for i in range(num_samples // 2)], + "new": [i for i in range(num_samples // 2, num_samples)], + } + + def __len__(self) -> int: + """Get length of subset.""" + return self.num_samples + + def __getitem__(self, indx) -> dict: + """Get dataset at index.""" + return self.fullset[indx] + + def __getattr__(self, name): + """When trying to get other attributes, not dataset, get values from fullset.""" + if name == "__setstate__": + raise AttributeError(name) + return getattr(self.fullset, name) + + @property + def flag(self): + """Getter of flag for detection task. + + Sampler of the detection task decides length of dataset checking sum of flag array. + To consider that case, return flag array with length of num_samples. + + """ + return np.zeros(self.num_samples, dtype=np.uint8) diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/builder.py b/src/otx/algorithms/common/adapters/mmcv/utils/builder.py new file mode 100644 index 00000000000..533f94bd075 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/utils/builder.py @@ -0,0 +1,83 @@ +"""MMCV general build functions.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Callable + +from mmcv import Config +from torch.utils.data import DataLoader, Dataset + +# pylint: disable-next=unused-import +from ._builder_build_data_parallel import build_data_parallel # noqa: F401 + + +def build_dataset( + config: Config, + subset: str, + dataset_builder: Callable, + *, + consume: bool = False, +) -> Dataset: + """Build dataset.""" + + if subset in ["test", "val"]: + default_args = dict(test_mode=True) + else: + default_args = dict(test_mode=False) + + dataset_cfg = config.data.pop(subset) if consume else config.data.get(subset) + dataset = dataset_builder(dataset_cfg, default_args) + return dataset + + +def build_dataloader( + dataset, + config: Config, + subset: str, + dataloader_builder: Callable, + *, + distributed: bool = False, + consume: bool = False, + **kwargs, +) -> DataLoader: + """Build dataloader.""" + + loader_cfg = dict( + samples_per_gpu=config.data.get("samples_per_gpu", 1), + workers_per_gpu=config.data.get("workers_per_gpu", 0), + num_gpus=len(config.gpu_ids), + dist=distributed, + seed=config.get("seed", None), + shuffle=False if subset in ["test", "val"] else True, # pylint: disable=simplifiable-if-expression + ) + + # The overall dataloader settings + loader_cfg.update( + { + k: v + for k, v in config.data.items() + if k + not in [ + "train", + "val", + "test", + "unlabeled", + "train_dataloader", + "val_dataloader", + "test_dataloader", + "unlabeled_dataloader", + ] + } + ) + + specific_loader_cfg = ( + config.data.pop(f"{subset}_dataloader", {}) if consume else config.data.get(f"{subset}_dataloader", {}) + ) + loader_cfg = Config(cfg_dict={**loader_cfg, **specific_loader_cfg, **kwargs}) + + dataloader = dataloader_builder( + dataset, + **loader_cfg, + ) + return dataloader diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py new file mode 100644 index 00000000000..baca58788a3 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -0,0 +1,990 @@ +"""Utils for common OTX algorithms.""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import copy +import glob +import math +import multiprocessing +import os +import os.path as osp +import platform +import shutil +import sys +import tempfile +import warnings +from collections.abc import Mapping +from importlib import import_module +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +import numpy as np +import torch +from mmcv import Config, ConfigDict +from mmcv.utils.config import BASE_KEY, DEPRECATION_KEY +from mmcv.utils.misc import import_modules_from_strings +from mmcv.utils.path import check_file_exist + +from otx.algorithms.common.configs.configuration_enums import InputSizePreset +from otx.algorithms.common.utils import is_xpu_available +from otx.api.entities.datasets import DatasetEntity +from otx.utils.logger import get_logger + +from ._config_utils_get_configs_by_keys import get_configs_by_keys +from ._config_utils_get_configs_by_pairs import get_configs_by_pairs + +logger = get_logger() + + +# TODO: refactor Config +class OTXConfig(Config): + """A class that extends the base `Config` class, adds additional functionality for loading configuration files.""" + + @staticmethod + def _file2dict( + filename, use_predefined_variables=True + ): # pylint: disable=too-many-locals, too-many-branches, too-many-statements + """Static method that loads the configuration file and returns a dictionary of its contents. + + :param filename: str, the path of the configuration file to be loaded. + :param use_predefined_variables: bool, a flag indicating whether to substitute predefined variables in the + configuration file. + :return: tuple of dictionary and string. Returns a dictionary containing the contents of the configuration file + and a string representation of the configuration file. + :raises: IOError if the file type is not supported. + """ + filename = osp.abspath(osp.expanduser(filename)) + check_file_exist(filename) + extender = osp.splitext(filename)[1] + if extender not in [".py", ".json", ".yaml", ".yml"]: + raise IOError("Only py/yml/yaml/json type are supported now!") + + with tempfile.TemporaryDirectory() as temp_config_dir: + with tempfile.NamedTemporaryFile(dir=temp_config_dir, suffix=extender) as temp_config_file: + if platform.system() == "Windows": + temp_config_file.close() + temp_config_name = osp.basename(temp_config_file.name) + # Substitute predefined variables + if use_predefined_variables: + Config._substitute_predefined_vars(filename, temp_config_file.name) + else: + shutil.copyfile(filename, temp_config_file.name) + # Substitute base variables from placeholders to strings + base_var_dict = Config._pre_substitute_base_vars(temp_config_file.name, temp_config_file.name) + if filename.endswith(".py"): + temp_module_name = osp.splitext(temp_config_name)[0] + sys.path.insert(0, temp_config_dir) + Config._validate_py_syntax(filename) + mod = import_module(temp_module_name) + sys.path.pop(0) + cfg_dict = {name: value for name, value in mod.__dict__.items() if not name.startswith("__")} + # delete imported module + del sys.modules[temp_module_name] + elif filename.endswith((".yml", ".yaml", ".json")): + import mmcv + + cfg_dict = mmcv.load(temp_config_file.name) + + # check deprecation information + if DEPRECATION_KEY in cfg_dict: + deprecation_info = cfg_dict.pop(DEPRECATION_KEY) + warning_msg = f"The config file {filename} will be deprecated " "in the future." + if "expected" in deprecation_info: + warning_msg += f' Please use {deprecation_info["expected"]} ' "instead." + if "reference" in deprecation_info: + warning_msg += " More information can be found at " f'{deprecation_info["reference"]}' + warnings.warn(warning_msg) + + cfg_text = filename + "\n" + with open(filename, "r", encoding="utf-8") as f: + # Setting encoding explicitly to resolve coding issue on windows + cfg_text += f.read() + + if BASE_KEY in cfg_dict: + cfg_dir = osp.dirname(filename) + base_filename = cfg_dict.pop(BASE_KEY) + base_filename = base_filename if isinstance(base_filename, list) else [base_filename] + + cfg_dict_list = [] + cfg_text_list = [] + for f in base_filename: + _cfg_dict, _cfg_text = OTXConfig._file2dict(osp.join(cfg_dir, f)) + cfg_dict_list.append(_cfg_dict) + cfg_text_list.append(_cfg_text) + + base_cfg_dict = dict() + # for c in cfg_dict_list: + # duplicate_keys = base_cfg_dict.keys() & c.keys() + # if len(duplicate_keys) > 0: + # raise KeyError('Duplicate key is not allowed among bases. ' + # f'Duplicate keys: {duplicate_keys}') + # base_cfg_dict.update(c) + for c in cfg_dict_list: + if len(base_cfg_dict.keys() & c.keys()) > 0: + # raise KeyError(f'Duplicate key is not allowed among bases [{base_cfg_dict.keys() & c.keys()}]') + logger.warning(f"Duplicate key is detected among bases [{base_cfg_dict.keys() & c.keys()}]") + logger.debug(f"base = {base_cfg_dict}, cfg = {c}") + base_cfg_dict = Config._merge_a_into_b(base_cfg_dict, c) + logger.debug(f"merged dict = {base_cfg_dict}") + else: + base_cfg_dict.update(c) + + # Subtitute base variables from strings to their actual values + cfg_dict = Config._substitute_base_vars(cfg_dict, base_var_dict, base_cfg_dict) + + base_cfg_dict = Config._merge_a_into_b(cfg_dict, base_cfg_dict) + cfg_dict = base_cfg_dict + + # merge cfg_text + cfg_text_list.append(cfg_text) + cfg_text = "\n".join(cfg_text_list) + + return cfg_dict, cfg_text + + @staticmethod + def fromfile(filename, use_predefined_variables=True, import_custom_modules=True): + """Static method that loads a configuration file and returns an instance of `Config` class. + + :param filename: str, the path of the configuration file to be loaded. + :param use_predefined_variables: bool, a flag indicating whether to substitute predefined variables in the + configuration file. + :param import_custom_modules: bool, a flag indicating whether to import custom modules. + :return: Config object, an instance of `Config` class containing the contents of the configuration file. + """ + cfg_dict, cfg_text = OTXConfig._file2dict(filename, use_predefined_variables) + if import_custom_modules and cfg_dict.get("custom_imports", None): + import_modules_from_strings(**cfg_dict["custom_imports"]) + return OTXConfig(cfg_dict, cfg_text=cfg_text, filename=filename) + + @property + def pretty_text(self): + """Make python file human-readable. + + It's almost same as mmcv.Config's code but code to reformat using yapf is removed to reduce time. + """ + + indent = 4 + + def _indent(s_, num_spaces): + s = s_.split("\n") + if len(s) == 1: + return s_ + first = s.pop(0) + s = [(num_spaces * " ") + line for line in s] + s = "\n".join(s) + s = first + "\n" + s + return s + + def _format_basic_types(k, v, use_mapping=False): + if isinstance(v, str): + v_str = f"'{v}'" + else: + v_str = str(v) + + if use_mapping: + k_str = f"'{k}'" if isinstance(k, str) else str(k) + attr_str = f"{k_str}: {v_str}" + else: + attr_str = f"{str(k)}={v_str}" + attr_str = _indent(attr_str, indent) + + return attr_str + + def _format_list(k, v, use_mapping=False): + # check if all items in the list are dict + if all(isinstance(_, dict) for _ in v): + v_str = "[\n" + v_str += "\n".join(f"dict({_indent(_format_dict(v_), indent)})," for v_ in v).rstrip(",") + if use_mapping: + k_str = f"'{k}'" if isinstance(k, str) else str(k) + attr_str = f"{k_str}: {v_str}" + else: + attr_str = f"{str(k)}={v_str}" + attr_str = _indent(attr_str, indent) + "]" + else: + attr_str = _format_basic_types(k, v, use_mapping) + return attr_str + + def _contain_invalid_identifier(dict_str): + contain_invalid_identifier = False + for key_name in dict_str: + contain_invalid_identifier |= not str(key_name).isidentifier() + return contain_invalid_identifier + + def _format_dict(input_dict, outest_level=False): + r = "" + s = [] + + use_mapping = _contain_invalid_identifier(input_dict) + if use_mapping: + r += "{" + for idx, (k, v) in enumerate(input_dict.items()): + is_last = idx >= len(input_dict) - 1 + end = "" if outest_level or is_last else "," + if isinstance(v, dict): + v_str = "\n" + _format_dict(v) + if use_mapping: + k_str = f"'{k}'" if isinstance(k, str) else str(k) + attr_str = f"{k_str}: dict({v_str}" + else: + attr_str = f"{str(k)}=dict({v_str}" + attr_str = _indent(attr_str, indent) + ")" + end + elif isinstance(v, list): + attr_str = _format_list(k, v, use_mapping) + end + else: + attr_str = _format_basic_types(k, v, use_mapping) + end + + s.append(attr_str) + r += "\n".join(s) + if use_mapping: + r += "}" + return r + + cfg_dict = self._cfg_dict.to_dict() + text = _format_dict(cfg_dict, outest_level=True) + + return text + + +def copy_config(cfg): + """A function that creates a deep copy of the input configuration object. + + :param cfg: Config object, an instance of `Config` class to be copied. + :return: Config object, a deep copy of the input configuration object. + :raises: ValueError if the input object is not an instance of `Config` class. + """ + if not isinstance(cfg, Config): + raise ValueError(f"cannot copy this instance {type(cfg)}") + + # disable [B301, B403] pickle, import-pickle - the library used for converting cfg object + import pickle # nosec B403 + + data = pickle.dumps(cfg) + return pickle.loads(data) # nosec B301 + + +def update_or_add_custom_hook(cfg: Config, hook_cfg: ConfigDict): + """Update hook cfg if same type is in custom_hook or append it.""" + custom_hooks = cfg.get("custom_hooks", []) + custom_hooks_updated = False + for custom_hook in custom_hooks: + if custom_hook["type"] == hook_cfg["type"]: + custom_hook.update(hook_cfg) + custom_hooks_updated = True + break + if not custom_hooks_updated: + custom_hooks.append(hook_cfg) + cfg["custom_hooks"] = custom_hooks + + +def remove_custom_hook(cfg: Config, hook_type: str): + """Remove hook cfg if hook_type is in custom_hook.""" + custom_hooks = cfg.get("custom_hooks", []) + if len(custom_hooks) > 0: + idx_to_del = None + for i, custom_hook in enumerate(custom_hooks): + if custom_hook["type"] == hook_type: + idx_to_del = i + break + if idx_to_del is not None: + del custom_hooks[idx_to_del] + + +def recursively_update_cfg( + cfg: Union[Config, dict], + criterion: Callable[[Any, Any], bool], + update_dict: Any, +): + """A function that recursively updates the input dictionary or `Config` object with a new dictionary. + + :param cfg: Union[Config, dict], an input dictionary or `Config` object to be updated. + :param criterion: Callable[[Any, Any], bool], a function that determines whether to update a key-value pair based on + a criterion. The function takes two arguments: key and value, and returns a boolean. + :param update_dict: Any, a dictionary to be used for updating the input dictionary. + :return: None + """ + for key, val in list(cfg.items()): + if isinstance(val, dict): + recursively_update_cfg(val, criterion, update_dict) + if criterion(key, val): + cfg.update(update_dict) + + +def add_custom_hook_if_not_exists(cfg: Config, hook_cfg: ConfigDict): + """A function that adds a custom hook to the input `Config` object if it doesn't already exist. + + :param cfg: Config object, an instance of `Config` class to which the custom hook will be added. + :param hook_cfg: ConfigDict object, an instance of `ConfigDict` class representing the custom hook to be added. + :return: None + """ + custom_hooks = cfg.get("custom_hooks", []) + found = False + for hook in custom_hooks: + if hook["type"] == hook_cfg["type"]: + found = True + break + if not found: + custom_hooks.append(hook_cfg) + cfg["custom_hooks"] = custom_hooks + + +def remove_from_config(config: Union[Config, ConfigDict], key: str): + """Update & Remove configs.""" + if key in config: + if isinstance(config, Config): + del config._cfg_dict[key] # pylint: disable=protected-access + elif isinstance(config, ConfigDict): + del config[key] + else: + raise ValueError(f"Unknown config type {type(config)}") + + +def remove_from_configs_by_type(configs: List[ConfigDict], type_name: str): + """Update & remove by type.""" + indices = [] + for i, config in enumerate(configs): + type_name_ = config.get("type", None) + if type_name_ == type_name: + indices.append(i) + for i in reversed(indices): + configs.pop(i) + + +def update_config( + config: Union[Config, ConfigDict], + pairs: Dict[Tuple[Any, ...], Any], +): + """Update configs by path as a key and value as a target.""" + for path, value in pairs.items(): + path_ = list(reversed(path)) + ptr = config + key = None + while path_: + key = path_.pop() + if isinstance(ptr, (Config, Mapping)): + if key not in ptr: + ptr[key] = ConfigDict() + elif isinstance(ptr, (list, tuple)): + assert isinstance(key, int), f"{key} of {path} must be int for ({type(ptr)}: {ptr})" + assert len(ptr) < key, f"{key} of {path} exceeds {len(ptr)}" + if len(path_) == 0: + ptr[key] = value + ptr = ptr[key] + + +def get_dataset_configs(config: Union[Config, ConfigDict], subset: str) -> List[ConfigDict]: + """A function that retrieves 'datasets' configurations from the input `Config` object or `ConfigDict` object. + + :param config: Union[Config, ConfigDict], an instance of `Config` class or `ConfigDict` class containing the + configurations. + :param subset: str, a string representing the subset for which the 'datasets' configuration is required. + :return: List[ConfigDict], a list of 'datasets' configuration dictionaries. + """ + if config.data.get(subset, None) is None: + return [] + data_cfg = config.data[subset] + data_cfgs = get_configs_by_keys(data_cfg, ["dataset", "datasets"]) + return data_cfgs if data_cfgs else [data_cfg] + + +def prepare_for_testing(config: Union[Config, ConfigDict], dataset: DatasetEntity) -> Config: + """Prepare configs for testing phase.""" + config = copy.deepcopy(config) + # FIXME. Should working directories be modified here? + config.data.test.otx_dataset = dataset + return config + + +def is_epoch_based_runner(runner_config: ConfigDict): + """Check Epoch based or Iter based runner.""" + return "Epoch" in runner_config.type + + +def config_from_string(config_string: str) -> Config: + """Generate an mmcv config dict object from a string. + + :param config_string: string to parse + :return config: configuration object + """ + with tempfile.NamedTemporaryFile("w", suffix=".py") as temp_file: + temp_file.write(config_string) + temp_file.flush() + return Config.fromfile(temp_file.name) + + +def patch_color_conversion(config: Config): + """Patch color conversion.""" + assert "data" in config + + for cfg in get_configs_by_pairs(config.data, dict(type="Normalize")): + to_rgb = False + if "to_rgb" in cfg: + to_rgb = cfg.to_rgb + cfg.to_rgb = not bool(to_rgb) + + +def patch_adaptive_interval_training(config: Config): + """Update adaptive interval settings for OTX training. + + This function can be removed by adding custom hook cfg into recipe.py directly. + """ + # Add/remove adaptive interval hook + if config.get("use_adaptive_interval", False): + update_or_add_custom_hook( + config, + ConfigDict( + { + "type": "AdaptiveTrainSchedulingHook", + "max_interval": 5, + "enable_adaptive_interval_hook": True, + "enable_eval_before_run": True, + **config.pop("adaptive_validation_interval", {}), + } + ), + ) + else: + config.pop("adaptive_validation_interval", None) + + +def patch_early_stopping(config: Config): + """Update early stop settings for OTX training. + + This function can be removed by adding custom hook cfg into recipe.py directly. + """ + if "early_stop" in config: + remove_custom_hook(config, "EarlyStoppingHook") + early_stop = config.get("early_stop", False) + if early_stop: + early_stop_hook = ConfigDict( + type="LazyEarlyStoppingHook", + start=early_stop.start, + patience=early_stop.patience, + iteration_patience=early_stop.iteration_patience, + interval=1, + metric=config.early_stop_metric, + priority=75, + ) + update_or_add_custom_hook(config, early_stop_hook) + else: + remove_custom_hook(config, "LazyEarlyStoppingHook") + + # make sure model to be in a training mode even after model is evaluated (mmcv bug) + update_or_add_custom_hook( + config, + ConfigDict(type="ForceTrainModeHook", priority="LOWEST"), + ) + + +def patch_persistent_workers(config: Config): + """Set persistent_workers as False in some conditions. + + persistent_workers is set as 0 in two cases below: + case 1) num_workers is 0 + case 2) semi-SL with distributed training. Because it uses two data loaders in each processes, + it makes workers for data loaders unstable, which makes errors during training. + + """ + dist_semi_sl = "unlabeled" in config.data and torch.distributed.is_initialized() + data_cfg = config.data + for subset in ["train", "val", "test", "unlabeled"]: + if subset not in data_cfg: + continue + dataloader_cfg = data_cfg.get(f"{subset}_dataloader", ConfigDict()) + workers_per_gpu = dataloader_cfg.get( + "workers_per_gpu", + data_cfg.get("workers_per_gpu", 0), + ) + if workers_per_gpu == 0 or dist_semi_sl: + dataloader_cfg["persistent_workers"] = False + elif "persistent_workers" not in dataloader_cfg: + dataloader_cfg["persistent_workers"] = True + + if "pin_memory" not in dataloader_cfg: + dataloader_cfg["pin_memory"] = True + + data_cfg[f"{subset}_dataloader"] = dataloader_cfg + + +def get_adaptive_num_workers(num_dataloader: int = 1) -> Union[int, None]: + """Measure appropriate num_workers value and return it.""" + if is_xpu_available(): + num_devices = torch.xpu.device_count() + else: + num_devices = torch.cuda.device_count() + if num_devices == 0: + logger.warning("There is no GPUs. Use existing num_worker value.") + return None + return min(multiprocessing.cpu_count() // (num_dataloader * num_devices), 8) # max available num_workers is 8 + + +def patch_from_hyperparams(config: Config, hyperparams, **kwargs): + """Patch config parameters from hyperparams.""" + params = hyperparams.learning_parameters + algo_backend = hyperparams.algo_backend + warmup_iters = int(params.learning_rate_warmup_iters) + + lr_config = ( + ConfigDict(warmup_iters=warmup_iters) + if warmup_iters > 0 + else ConfigDict(warmup_iters=warmup_iters, warmup=None) + ) + + if params.enable_early_stopping and config.get("evaluation", None): + early_stop = ConfigDict( + start=int(params.early_stop_start), + patience=int(params.early_stop_patience), + iteration_patience=int(params.early_stop_iteration_patience), + ) + else: + early_stop = False + + runner = ConfigDict(max_epochs=int(params.num_iters)) + if config.get("runner", None) and config.runner.get("type").startswith("IterBasedRunner"): + runner = ConfigDict(max_iters=int(params.num_iters)) + + hparams = ConfigDict( + optimizer=ConfigDict(lr=params.learning_rate), + lr_config=lr_config, + early_stop=early_stop, + data=ConfigDict( + samples_per_gpu=int(params.batch_size), + workers_per_gpu=int(params.num_workers), + ), + runner=runner, + algo_backend=algo_backend, + ) + + # NOTE: Not all algorithms are compatible with the parameter `inference_batch_size`, + # as `samples_per_gpu might` not be a valid argument for certain algorithms. + if hasattr(config, "task"): + if config.task == "instance-segmentation" or config.task == "detection": + hparams.update( + ConfigDict( + data=ConfigDict( + val_dataloader=ConfigDict(samples_per_gpu=int(params.inference_batch_size)), + test_dataloader=ConfigDict(samples_per_gpu=int(params.inference_batch_size)), + ), + ) + ) + is_semi_sl = algo_backend.train_type.name == "Semisupervised" + + if hyperparams.learning_parameters.auto_num_workers: + adapted_num_worker = get_adaptive_num_workers(2 if is_semi_sl else 1) + if adapted_num_worker is not None: + hparams.data.workers_per_gpu = adapted_num_worker + + if is_semi_sl: + unlabeled_config = ConfigDict( + data=ConfigDict( + unlabeled_dataloader=ConfigDict( + samples_per_gpu=int(params.unlabeled_batch_size), + workers_per_gpu=int(params.num_workers), + ) + ) + ) + config.update(unlabeled_config) + + hparams["use_adaptive_interval"] = hyperparams.learning_parameters.use_adaptive_interval + config.merge_from_dict(hparams) + + +def prepare_work_dir(config: Union[Config, ConfigDict]) -> str: + """Prepare configs of working directory.""" + base_work_dir = config.work_dir + checkpoint_dirs = glob.glob(os.path.join(base_work_dir, "checkpoints_round_*")) + train_round_checkpoint_dir = os.path.join(base_work_dir, f"checkpoints_round_{len(checkpoint_dirs)}") + os.makedirs(train_round_checkpoint_dir) + config.work_dir = train_round_checkpoint_dir + if "meta" not in config.runner: + config.runner.meta = ConfigDict() + config.runner.meta.exp_name = f"train_round_{len(checkpoint_dirs)}" + return train_round_checkpoint_dir + + +class InputSizeManager: + """Class for changing input size and getting input size value by checking data pipeline. + + NOTE: "resize", "pad", "crop", "mosaic", "randomaffine", "multiscaleflipaug" , "AutoAugment" and "TwoCropTransform" + are considered at now. If other data pipelines exist, it can work differently than expected. + + Args: + config (Dict): Global configuration including data config w/ "train", "val" or "test" data pipeline. + base_input_size (Optional[Union[int, List[int], Dict[str, Union[int, List[int]]]]], optional): + Default input size. If it's a None, it's estimated based on data pipeline. If it's an integer, + it's expected that all data pipeline have (base_input_size x base_input_size) input size. + If it's an integer list, all data pipeline have same (base_input_size[0] x base_input_size[1]) input size. + If it's dictionary, each data pipeline has specified input size. It should have format as below: + {"train" : [w, h], "val" : [w, h], "test" : [w, h]} + """ + + PIPELINE_TO_CHANGE: Dict[str, List[str]] = { + "resize": ["size", "img_scale"], + "pad": ["size"], + "crop": ["crop_size"], + "mosaic": ["img_scale"], + "randomaffine": ["border"], + "multiscaleflipaug": ["img_scale"], + } + PIPELINE_WRAPPER: Dict[str, List[str]] = { + "MultiScaleFlipAug": ["transforms"], + "AutoAugment": ["policies"], + "TwoCropTransform": ["view0", "view1", "pipeline"], + "LoadResizeDataFromOTXDataset": ["resize_cfg"], + } + SUBSET_TYPES: Tuple[str, str, str, str] = ("train", "val", "test", "unlabeled") + + MIN_RECOGNIZABLE_OBJECT_SIZE = 32 # Minimum object size recognizable by NNs: typically 16 ~ 32 + # meaning NxN input pixels being downscaled to 1x1 on feature map + MIN_DETECTION_INPUT_SIZE = 256 # Minimum input size for object detection + + def __init__( + self, + config: Dict, + base_input_size: Optional[Union[int, Tuple[int, int], Dict[str, int], Dict[str, Tuple[int, int]]]] = None, + ): + self._config = config + self._data_config = config.get("data", {}) + if isinstance(base_input_size, int): + base_input_size = (base_input_size, base_input_size) + elif isinstance(base_input_size, dict): + for subset in base_input_size.keys(): + if isinstance(base_input_size[subset], int): + size = base_input_size[subset] + base_input_size[subset] = (size, size) # type: ignore[assignment] + for subset in self.SUBSET_TYPES: + if subset in self._data_config and subset not in base_input_size: + raise ValueError(f"There is {subset} data configuration but base input size for it doesn't exists.") + + self._base_input_size = base_input_size + + def set_input_size(self, input_size: Union[int, List[int], Tuple[int, int]]): + """Set input size in data pipe line. + + Args: + input_size (Union[int, List[int]]): + input size to set. If it's an integer, (input_size x input_size) will be set. + If input_size is an integer list, (input_size[0] x input_size[1]) will be set. + """ + if isinstance(input_size, int): + input_size = (input_size, input_size) + if not isinstance(self.base_input_size, dict): + resize_ratio = (input_size[0] / self.base_input_size[0], input_size[1] / self.base_input_size[1]) + + # Scale size values in data pipelines + for subset in self.SUBSET_TYPES: + if subset in self._data_config: + if isinstance(self.base_input_size, dict): + resize_ratio = ( + input_size[0] / self.base_input_size[subset][0], + input_size[1] / self.base_input_size[subset][1], + ) + pipelines = self._get_pipelines(subset) + if isinstance(pipelines, dict): + # Deals with {"view0": [...], "view1": [...]} + for pipeline in pipelines.values(): + self._set_pipeline_size_value(pipeline, resize_ratio) + else: + self._set_pipeline_size_value(pipelines, resize_ratio) + + # Set model size + model_cfg = self._config.get("model", {}) + model_cfg["input_size"] = input_size + if model_cfg.get("type", "") == "CustomYOLOX": + # - needed only for YOLOX + if input_size[0] % 32 != 0 or input_size[1] % 32 != 0: + raise ValueError("YOLOX should have input size being multiple of 32.") + + @property + def base_input_size(self) -> Union[Tuple[int, int], Dict[str, Tuple[int, int]]]: + """Getter function of `base_input_size` attirbute. + + If it isn't set when intializing class, it's estimated by checking data pipeline. + Same value is returned after estimation. + + Raises: + RuntimeError: If failed to estimate base input size from data pipeline, raise an error. + + Returns: + Union[List[int], Dict[str, List[int]]]: Base input size. + """ + if self._base_input_size is not None: + return self._base_input_size # type: ignore[return-value] + + input_size = self.get_input_size_from_cfg() + if input_size is None: + raise RuntimeError("There isn't any pipeline in the data configurations.") + + self._base_input_size = input_size + return input_size + + def get_input_size_from_cfg( + self, subset: Union[str, List[str]] = ["test", "val", "train"] + ) -> Optional[Tuple[int, int]]: + """Estimate image size using data pipeline. + + Args: + subset (Union[str, List[str]], optional): Which pipelines to check. Defaults to ["test", "val", "train"]. + + Returns: + Union[None, List[int]]: Return estimiated input size. If failed to estimate, return None. + """ + if isinstance(subset, str): + subset = [subset] + + for target_subset in subset: + if target_subset in self._data_config: + input_size = self._estimate_post_img_size(self._data_config[target_subset]["pipeline"]) + if input_size is not None: + return tuple(input_size) # type: ignore[return-value] + + return None + + def _estimate_post_img_size( + self, pipelines: Union[Dict, List[Dict]], default_size: Optional[List[int]] = None + ) -> Union[List[int], None]: + # NOTE: Mosaic isn't considered in this step because Mosaic and following RandomAffine don't change image size + post_img_size = default_size + + if isinstance(pipelines, dict): + for pipeline in pipelines.values(): + # Deals with {"view0": [...], "view1": [...]} + # Just using the first one to estimate + return self._estimate_post_img_size(pipeline, post_img_size) + + for pipeline in pipelines: + if "resize" in pipeline["type"].lower(): + img_size = self._get_size_value(pipeline, "resize") + if img_size is not None: + post_img_size = img_size + elif "pad" in pipeline["type"].lower(): + img_size = self._get_size_value(pipeline, "pad") + if img_size is not None: + if post_img_size is None: + post_img_size = img_size + else: + for i in range(2): + if post_img_size[i] < img_size[i]: + post_img_size[i] = img_size[i] + elif "crop" in pipeline["type"].lower(): + img_size = self._get_size_value(pipeline, "crop") + if img_size is not None: + if post_img_size is None: + post_img_size = img_size + else: + for i in range(2): + if post_img_size[i] > img_size[i]: + post_img_size[i] = img_size[i] + elif pipeline["type"] == "MultiScaleFlipAug": + img_size = self._get_size_value(pipeline, "multiscaleflipaug") + if img_size is not None: + post_img_size = img_size + + for pipeline_name, sub_pipeline_names in self.PIPELINE_WRAPPER.items(): + if pipeline_name == pipeline["type"]: + for sub_pipeline_name in sub_pipeline_names: + if sub_pipeline_name in pipeline: + sub_pipeline = pipeline[sub_pipeline_name] + if isinstance(sub_pipeline, dict): + sub_pipeline = [sub_pipeline] + elif isinstance(sub_pipeline[0], list): + sub_pipeline = sub_pipeline[0] + post_img_size = self._estimate_post_img_size(sub_pipeline, post_img_size) + break + + return post_img_size + + @classmethod + def _get_size_value(cls, pipeline: Dict, attr: str) -> Union[List[int], None]: + for pipeline_attr in cls.PIPELINE_TO_CHANGE[attr]: + if pipeline_attr not in pipeline: + continue + size_val = pipeline[pipeline_attr] + if isinstance(size_val, int): + return [size_val, size_val] + elif isinstance(size_val, tuple): + return list(size_val) + elif isinstance(size_val, list): + return list(size_val[0]) + + return None + + def _get_pipelines(self, subset: str): + if "pipeline" in self._data_config[subset]: + return self._data_config[subset]["pipeline"] + if "dataset" in self._data_config[subset]: + return self._data_config[subset]["dataset"]["pipeline"] + raise RuntimeError("Failed to find pipeline.") + + def _set_pipeline_size_value( + self, pipeline: Union[Dict, List[Dict]], scale: Tuple[Union[int, float], Union[int, float]] + ): + + if isinstance(pipeline, list): + for sub_pipeline in pipeline: + self._set_pipeline_size_value(sub_pipeline, scale) + return + + updated = False + for pipeline_name, pipeline_attrs in self.PIPELINE_TO_CHANGE.items(): + if pipeline_name in pipeline["type"].lower(): + for pipeline_attr in pipeline_attrs: + if pipeline_attr in pipeline: + self._set_size_value(pipeline, pipeline_attr, scale) + updated = True + if updated: + break + + for pipeline_name, sub_pipeline_names in self.PIPELINE_WRAPPER.items(): + if pipeline_name == pipeline["type"]: + for sub_pipeline_name in sub_pipeline_names: + if sub_pipeline_name in pipeline: + if isinstance(pipeline[sub_pipeline_name], dict): + self._set_pipeline_size_value(pipeline[sub_pipeline_name], scale) + elif isinstance(pipeline[sub_pipeline_name][0], dict): + for sub_pipeline in pipeline[sub_pipeline_name]: + self._set_pipeline_size_value(sub_pipeline, scale) + elif isinstance(pipeline[sub_pipeline_name][0], list): + for sub_pipelines in pipeline[sub_pipeline_name]: + for sub_pipeline in sub_pipelines: + self._set_pipeline_size_value(sub_pipeline, scale) + else: + raise ValueError( + "Dataset pipeline in pipeline wrapper type should be" + "either dict, list[dict] or list[list[dict]]." + ) + + @staticmethod + def _set_size_value(pipeline: Dict, attr: str, scale: Tuple[Union[int, float], Union[int, float]]): + if isinstance(pipeline[attr], int): + pipeline[attr] = round(pipeline[attr] * scale[0]) + elif isinstance(pipeline[attr], list) and isinstance(pipeline[attr][0], tuple): + for idx in range(len(pipeline[attr])): + pipeline[attr][idx] = ( + round(pipeline[attr][idx][0] * scale[0]), + round(pipeline[attr][idx][1] * scale[1]), + ) + else: + pipeline[attr] = (round(pipeline[attr][0] * scale[0]), round(pipeline[attr][1] * scale[1])) + + @staticmethod + def get_trained_input_size(model_ckpt: Optional[str] = None) -> Optional[Tuple[int, int]]: + """Get trained input size from checkpoint. If it doesn't exist, return None. + + Args: + model_ckpt (Optional[str], optional): Model weight to load. Defaults to None. + + Returns: + Optional[Tuple[int, int]]: Pair of width and height. If there is no input size configuration, return None. + """ + if model_ckpt is None: + return None + + model_info = torch.load(model_ckpt, map_location="cpu") + if model_info is None: + return None + + input_size = model_info.get("input_size", None) + if not input_size: + return None + + logger.info("Given model weight was trained with {} input size.".format(input_size)) + return input_size + + @staticmethod + def select_closest_size(input_size: Tuple[int, int], preset_sizes: List[Tuple[int, int]]): + """Select the most closest size from preset sizes in log scale. + + Args: + input_size (Tuple[int, int]): Query input size + preset_sizes (List[Tuple[int, int]]): List of preset input sizes + + Returns: + Tuple[int, int]: Best matching size out of preset. Returns input_size if preset is empty. + """ + if len(preset_sizes) == 0: + return input_size + + def to_log_scale(x): + return np.log(np.sqrt(x[0] * x[1])) + + input_scale = to_log_scale(input_size) + preset_scales = np.array(list(map(to_log_scale, preset_sizes))) + abs_diff = np.abs(preset_scales - input_scale) + return preset_sizes[np.argmin(abs_diff)] + + def adapt_input_size_to_dataset( + self, max_image_size: int, min_object_size: Optional[int] = None, downscale_only: bool = True + ) -> Tuple[int, int]: + """Compute appropriate model input size w.r.t. dataset statistics. + + Args: + max_image_size (int): Typical large image size of dataset in pixels. + min_object_size (int, optional): Typical small object size of dataset in pixels. + None to consider only image size. Defaults to None. + downscale_only (bool) : Whether to allow only smaller size than default setting. Defaults to True. + + Returns: + Tuple[int, int]: (width, height) + """ + + logger.info("Adapting model input size based on dataset stat") + + base_input_size = self.base_input_size + if isinstance(base_input_size, Dict): + base_input_size = base_input_size.get("train", base_input_size.get("test", None)) + logger.info(f"-> Current base input size: {base_input_size}") + + if max_image_size <= 0: + return base_input_size + + image_size = max_image_size + logger.info(f"-> Based on typical large image size: {image_size}") + + # Refine using annotation shape size stat + if min_object_size is not None and min_object_size > 0: + image_size = round(image_size * self.MIN_RECOGNIZABLE_OBJECT_SIZE / min_object_size) + logger.info(f"-> Based on typical small object size {min_object_size}: {image_size}") + if image_size > max_image_size: + image_size = max_image_size + logger.info(f"-> Restrict to max image size: {image_size}") + if image_size < self.MIN_DETECTION_INPUT_SIZE: + image_size = self.MIN_DETECTION_INPUT_SIZE + logger.info(f"-> Based on minimum object detection input size: {image_size}") + + input_size = (round(image_size), round(image_size)) + + if downscale_only: + + def area(x): + return x[0] * x[1] + + if base_input_size and area(input_size) >= area(base_input_size): + logger.info(f"-> Downscale only: {input_size} -> {base_input_size}") + return base_input_size + + # Closest preset + input_size_preset = InputSizePreset.input_sizes() + input_size = self.select_closest_size(input_size, input_size_preset) + logger.info(f"-> Closest preset: {input_size}") + return input_size + + +def get_proper_repeat_times( + data_size: int, + batch_size: int, + coef: float, + min_repeat: float, +) -> float: + """Get proper repeat times for adaptive training. + + Args: + data_size (int): The total number of the training dataset + batch_size (int): The batch size for the training data loader + coef (float) : coefficient that effects to number of repeats + (coef * math.sqrt(num_iters-1)) +5 + min_repeat (float) : minimum repeats + """ + if data_size == 0 or batch_size == 0: + logger.info("Repeat dataset enabled, but not a train mode. repeat times set to 1.") + return 1 + n_iters_per_epoch = math.ceil(data_size / batch_size) + return math.floor(max(coef * math.sqrt(n_iters_per_epoch - 1) + 5, min_repeat)) diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/fp16_utils.py b/src/otx/algorithms/common/adapters/mmcv/utils/fp16_utils.py new file mode 100644 index 00000000000..b5961575db7 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/utils/fp16_utils.py @@ -0,0 +1,133 @@ +"""Custom fp16 related modules to enable XPU modules.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import functools +from inspect import getfullargspec +from typing import Callable, Iterable, Optional + +import torch +from mmcv.runner.fp16_utils import cast_tensor_type +from mmcv.utils import IS_NPU_AVAILABLE, TORCH_VERSION, digit_version +from torch import nn + +from otx.algorithms.common.utils import is_xpu_available + +try: + if is_xpu_available(): + from torch.xpu.amp import autocast + elif IS_NPU_AVAILABLE: + from torch.npu.amp import autocast + else: + from torch.cuda.amp import autocast +except ImportError: + pass + + +def custom_auto_fp16( + apply_to: Optional[Iterable] = None, + out_fp32: bool = False, + supported_types: tuple = (nn.Module,), +) -> Callable: + """Custom decorator to enable fp16 training automatically on XPU as well.""" + + def auto_fp16_wrapper(old_func: Callable) -> Callable: + @functools.wraps(old_func) + def new_func(*args, **kwargs) -> Callable: + # check if the module has set the attribute `fp16_enabled`, if not, + # just fallback to the original method. + if not isinstance(args[0], supported_types): + raise TypeError( + "@auto_fp16 can only be used to decorate the " f"method of those classes {supported_types}" + ) + if not (hasattr(args[0], "fp16_enabled") and args[0].fp16_enabled): + return old_func(*args, **kwargs) + + target_dtype = torch.bfloat16 if is_xpu_available() else torch.half + # get the arg spec of the decorated method + args_info = getfullargspec(old_func) + # get the argument names to be casted + args_to_cast = args_info.args if apply_to is None else apply_to + # convert the args that need to be processed + new_args = [] + # NOTE: default args are not taken into consideration + if args: + arg_names = args_info.args[: len(args)] + for i, arg_name in enumerate(arg_names): + if arg_name in args_to_cast: + new_args.append(cast_tensor_type(args[i], torch.float, target_dtype)) + else: + new_args.append(args[i]) + # convert the kwargs that need to be processed + new_kwargs = {} + if kwargs: + for arg_name, arg_value in kwargs.items(): + if arg_name in args_to_cast: + new_kwargs[arg_name] = cast_tensor_type(arg_value, torch.float, target_dtype) + else: + new_kwargs[arg_name] = arg_value + # apply converted arguments to the decorated method + if TORCH_VERSION != "parrots" and digit_version(TORCH_VERSION) >= digit_version("1.6.0"): + with autocast(enabled=True): + output = old_func(*new_args, **new_kwargs) + else: + output = old_func(*new_args, **new_kwargs) + # cast the results back to fp32 if necessary + if out_fp32: + output = cast_tensor_type(output, target_dtype, torch.float) + return output + + return new_func + + return auto_fp16_wrapper + + +def custom_force_fp32(apply_to: Optional[Iterable] = None, out_fp16: bool = False) -> Callable: + """Custom decorator to convert input arguments to fp32 in force on XPU as well.""" + + def force_fp32_wrapper(old_func): + @functools.wraps(old_func) + def new_func(*args, **kwargs) -> Callable: + # check if the module has set the attribute `fp16_enabled`, if not, + # just fallback to the original method. + if not isinstance(args[0], torch.nn.Module): + raise TypeError("@force_fp32 can only be used to decorate the " "method of nn.Module") + if not (hasattr(args[0], "fp16_enabled") and args[0].fp16_enabled): + return old_func(*args, **kwargs) + + source_dtype = torch.bfloat16 if is_xpu_available() else torch.half + # get the arg spec of the decorated method + args_info = getfullargspec(old_func) + # get the argument names to be casted + args_to_cast = args_info.args if apply_to is None else apply_to + # convert the args that need to be processed + new_args = [] + if args: + arg_names = args_info.args[: len(args)] + for i, arg_name in enumerate(arg_names): + if arg_name in args_to_cast: + new_args.append(cast_tensor_type(args[i], source_dtype, torch.float)) + else: + new_args.append(args[i]) + # convert the kwargs that need to be processed + new_kwargs = dict() + if kwargs: + for arg_name, arg_value in kwargs.items(): + if arg_name in args_to_cast: + new_kwargs[arg_name] = cast_tensor_type(arg_value, source_dtype, torch.float) + else: + new_kwargs[arg_name] = arg_value + # apply converted arguments to the decorated method + if TORCH_VERSION != "parrots" and digit_version(TORCH_VERSION) >= digit_version("1.6.0"): + with autocast(enabled=False): + output = old_func(*new_args, **new_kwargs) + else: + output = old_func(*new_args, **new_kwargs) + # cast the results back to fp32 if necessary + if out_fp16: + output = cast_tensor_type(output, torch.float, source_dtype) + return output + + return new_func + + return force_fp32_wrapper diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/hpu_optimizers.py b/src/otx/algorithms/common/adapters/mmcv/utils/hpu_optimizers.py new file mode 100644 index 00000000000..eabb26fbcf1 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmcv/utils/hpu_optimizers.py @@ -0,0 +1,31 @@ +"""Optimizers for HPU.""" + +import inspect +from typing import List + +import torch +from mmcv.runner import OPTIMIZERS + +try: + import habana_frameworks.torch.hpex.optimizers as hoptimizers +except ImportError: + hoptimizers = None + + +def register_habana_optimizers() -> List: + """Register habana optimizers.""" + if hoptimizers is None: + return [] + + habana_optimizers = [] + for module_name in dir(hoptimizers): + if module_name.startswith("__"): + continue + _optim = getattr(hoptimizers, module_name) + if inspect.isclass(_optim) and issubclass(_optim, torch.optim.Optimizer): + OPTIMIZERS.register_module()(_optim) + habana_optimizers.append(module_name) + return habana_optimizers + + +HABANA_OPTIMIZERS = register_habana_optimizers() diff --git a/src/otx/algorithms/common/adapters/mmdeploy/__init__.py b/src/otx/algorithms/common/adapters/mmdeploy/__init__.py new file mode 100644 index 00000000000..f0dceb2b560 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmdeploy/__init__.py @@ -0,0 +1,12 @@ +"""Adapters for mmdeploy.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .ops import squeeze__default +from .utils.mmdeploy import is_mmdeploy_enabled + +__all__ = [ + "squeeze__default", + "is_mmdeploy_enabled", +] diff --git a/src/otx/algorithms/common/adapters/mmdeploy/apis.py b/src/otx/algorithms/common/adapters/mmdeploy/apis.py new file mode 100644 index 00000000000..d46bb9a7e5b --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmdeploy/apis.py @@ -0,0 +1,377 @@ +"""API of otx.algorithms.common.adapters.mmdeploy.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import time +from collections.abc import Mapping +from copy import deepcopy +from functools import partial +from subprocess import CalledProcessError +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +import mmcv +import numpy as np +import torch +from mmcv.parallel import collate, scatter + +from .utils.mmdeploy import ( + is_mmdeploy_enabled, + mmdeploy_init_model_helper, + update_deploy_cfg, +) +from .utils.onnx import prepare_onnx_for_openvino +from .utils.utils import numpy_2_list + +# pylint: disable=too-many-locals + + +class NaiveExporter: + """NaiveExporter for non-mmdeploy export.""" + + @staticmethod + def export2backend( + output_dir: str, + model_builder: Callable, + cfg: mmcv.Config, + input_data: Dict[Any, Any], + *, + precision: str = "FP32", + model_name: str = "model", + input_names: Optional[List[str]] = None, + output_names: Optional[List[str]] = None, + opset_version: int = 11, + dynamic_axes: Optional[Dict[Any, Any]] = None, + mo_transforms: str = "", + export_type: str = "OPENVINO", + ): + """Function for exporting to openvino.""" + input_data = scatter(collate([input_data], samples_per_gpu=1), [-1])[0] + + model = model_builder(cfg) + model = model.cpu().eval() + dynamic_axes = dynamic_axes if dynamic_axes else dict() + + onnx_path = NaiveExporter.torch2onnx( + output_dir, + model, + input_data, + model_name=model_name, + input_names=input_names, + output_names=output_names, + opset_version=opset_version, + dynamic_axes=dynamic_axes, + ) + + if "ONNX" in export_type: + return + + def get_normalize_cfg(cfg): + def _get_normalize_cfg(cfg_): + out = [] + if isinstance(cfg_, Mapping): + for key, value in cfg_.items(): + if isinstance(value, (Mapping, list)): + out += _get_normalize_cfg(value) + if key == "type" and value == "Normalize": + return [cfg_] + elif isinstance(cfg_, list): + for value in cfg_: + if isinstance(value, (Mapping, list)): + out += _get_normalize_cfg(value) + return out + + cfg = _get_normalize_cfg(cfg) + assert len(cfg) == 1 + return cfg[0] + + mo_args = {} + + normalize_cfg = get_normalize_cfg(cfg.data.test) + if normalize_cfg.get("mean", None) is not None: + mo_args["mean_values"] = normalize_cfg.get("mean") + if normalize_cfg.get("std", None) is not None: + mo_args["scale_values"] = normalize_cfg.get("std") + if normalize_cfg.get("to_rgb", False): + mo_args["reverse_input_channels"] = None + + if precision == "FP16": + mo_args["compress_to_fp16"] = None + if mo_transforms: + mo_args["transform"] = mo_transforms + + NaiveExporter.onnx2openvino( + output_dir, + onnx_path, + model_name=model_name, + **mo_args, + ) + + @staticmethod + def torch2onnx( + output_dir: str, + model: torch.nn.Module, + input_data: Dict[Any, Any], + *, + model_name: str = "model", + input_names: Optional[List[str]] = None, + output_names: Optional[List[str]] = None, + opset_version: int = 11, + dynamic_axes: Optional[Dict[Any, Any]] = None, + verbose: bool = False, + **onnx_options, + ) -> str: + """Function for torch to onnx exporting.""" + + img_metas = input_data.get("img_metas") + numpy_2_list(img_metas) + imgs = input_data.get("img") + model.forward = partial(model.forward, img_metas=img_metas, return_loss=False) + + onnx_file_name = model_name + ".onnx" + dynamic_axes = dynamic_axes if dynamic_axes else dict() + torch.onnx.export( + model, + imgs, + os.path.join(output_dir, onnx_file_name), + verbose=verbose, + export_params=True, + input_names=input_names, + output_names=output_names, + dynamic_axes=dynamic_axes, + opset_version=opset_version, + operator_export_type=torch.onnx.OperatorExportTypes.ONNX, + **onnx_options, + ) + model.__dict__.pop("forward") + return os.path.join(output_dir, onnx_file_name) + + @staticmethod + def onnx2openvino( + output_dir: str, + onnx_path: str, + *, + model_name: str = "model", + **openvino_options, + ) -> Tuple[str, str]: + """Function for onnx to openvino exporting.""" + from otx.algorithms.common.utils import mo_wrapper + + mo_args = { + "input_model": onnx_path, + "output_dir": output_dir, + "model_name": model_name, + } + mo_args.update(openvino_options) + + ret, msg = mo_wrapper.generate_ir(output_dir, output_dir, silent=False, **mo_args) + if ret != 0: + raise ValueError(msg) + return ( + os.path.join(output_dir, model_name + ".xml"), + os.path.join(output_dir, model_name + ".bin"), + ) + + +if is_mmdeploy_enabled(): + import mmdeploy.apis.openvino as openvino_api + from mmdeploy.apis import build_task_processor, extract_model, torch2onnx + from mmdeploy.apis.openvino import get_input_info_from_cfg, get_mo_options_from_cfg + from mmdeploy.core import reset_mark_function_count + + # from mmdeploy.core import FUNCTION_REWRITER + from mmdeploy.utils import get_ir_config, get_partition_config + + class MMdeployExporter: + """MMdeployExporter for mmdeploy exporting.""" + + @staticmethod + def export2backend( + output_dir: str, + model_builder: Callable, + cfg: mmcv.Config, + deploy_cfg: mmcv.Config, + export_type: str, + *, + model_name: str = "model", + ): + """Function for exporting to openvino.""" + + task_processor = build_task_processor(cfg, deploy_cfg, "cpu") + + def helper(*args, **kwargs): + return mmdeploy_init_model_helper(*args, **kwargs, model_builder=model_builder) + + task_processor.__class__.init_pytorch_model = helper + + input_data_cfg = deploy_cfg.pop( + "input_data", + {"shape": (128, 128, 3), "file_path": None}, + ) + + if input_data_cfg.get("file_path"): + import cv2 + + input_data = cv2.imread(input_data_cfg.get("file_path")) + # image assumed to be RGB format under OTX + input_data = cv2.cvtColor(input_data, cv2.COLOR_BGR2RGB) + else: + input_data = np.zeros(input_data_cfg["shape"], dtype=np.uint8) + + partition_cfgs = get_partition_config(deploy_cfg) + if partition_cfgs: + MMdeployExporter.extract_partition( + output_dir, + input_data, + cfg, + deploy_cfg, + export_type, + model_name=model_name, + ) + + onnx_paths = [] + onnx_paths.append( + MMdeployExporter.torch2onnx( + output_dir, + input_data, + cfg, + deploy_cfg, + model_name=model_name, + ) + ) + if "ONNX" not in export_type: + for onnx_path in onnx_paths: + deploy_cfg_ = deepcopy(deploy_cfg) + update_deploy_cfg(onnx_path, deploy_cfg_) + MMdeployExporter.onnx2openvino( + output_dir, + onnx_path, + deploy_cfg_, + ) + + @staticmethod + def extract_partition( + output_dir: str, + input_data: Any, + cfg: mmcv.Config, + deploy_cfg: mmcv.Config, + export_type: str, + *, + model_name: str = "model", + ): + """Function for extracting partition.""" + reset_mark_function_count() + model_onnx = MMdeployExporter.torch2onnx( + output_dir, + input_data, + cfg, + deploy_cfg, + model_name=model_name, + ) + + partition_cfgs = get_partition_config(deploy_cfg) + partition_cfgs = partition_cfgs.get("partition_cfg", None) + partition_onnx = MMdeployExporter.partition_onnx( + output_dir, + model_onnx, + partition_cfgs, + ) + + if "ONNX" not in export_type: + deploy_cfg_ = deepcopy(deploy_cfg) + update_deploy_cfg(partition_onnx[0], deploy_cfg_) + MMdeployExporter.onnx2openvino( + output_dir, + partition_onnx[0], + deploy_cfg_, + ) + deploy_cfg["partition_config"]["apply_marks"] = False + reset_mark_function_count() + + @staticmethod + def torch2onnx( + output_dir: str, + input_data: Any, + cfg: mmcv.Config, + deploy_cfg: mmcv.Config, + *, + model_name: str = "model", + ) -> str: + """Function for torch to onnx exporting.""" + onnx_file_name = model_name + ".onnx" + torch2onnx( + input_data, + output_dir, + onnx_file_name, + deploy_cfg=deploy_cfg, + model_cfg=cfg, + model_checkpoint=cfg.get("load_from", None), + device="cpu", + ) + return os.path.join(output_dir, onnx_file_name) + + @staticmethod + def partition_onnx( + output_dir, + onnx_path: str, + partition_cfgs: Union[mmcv.ConfigDict, List[mmcv.ConfigDict]], + ) -> Tuple[str, ...]: + """Function for parition onnx.""" + partitioned_paths = [] + + if not isinstance(partition_cfgs, list): + partition_cfgs = [partition_cfgs] + + for partition_cfg in partition_cfgs: + save_file = partition_cfg["save_file"] + save_path = os.path.join(output_dir, save_file) + start = partition_cfg["start"] + end = partition_cfg["end"] + dynamic_axes = partition_cfg.get("dynamic_axes", None) + + extract_model(onnx_path, start, end, dynamic_axes=dynamic_axes, save_file=save_path) + partitioned_paths.append(save_path) + return tuple(partitioned_paths) + + @staticmethod + def onnx2openvino( + output_dir: str, + onnx_path: str, + deploy_cfg: Union[str, mmcv.Config], + *, + model_name: Optional[str] = None, + ) -> Tuple[str, str]: + """Function for onnx to openvino exporting.""" + + input_info = get_input_info_from_cfg(deploy_cfg) + output_names = get_ir_config(deploy_cfg).output_names + mo_options = get_mo_options_from_cfg(deploy_cfg) + + if not model_name: + model_name = os.path.basename(onnx_path).replace(".onnx", "") + mo_options.args += f'--model_name "{model_name}" ' + + onnx_ready_path = os.path.join(os.path.dirname(onnx_path), f"{model_name}_ready.onnx") + prepare_onnx_for_openvino(onnx_path, os.path.join(os.path.dirname(onnx_path), f"{model_name}_ready.onnx")) + + try: + openvino_api.from_onnx(onnx_ready_path, output_dir, input_info, output_names, mo_options) + except CalledProcessError as e: + # NOTE: mo returns non zero return code (245) even though it successfully generate IR + cur_time = time.time() + time_threshold = 5 + if not ( + e.returncode == 245 + and not {model_name + ".bin", model_name + ".xml"} - set(os.listdir(output_dir)) + and ( + os.path.getmtime(os.path.join(output_dir, model_name + ".bin")) - cur_time < time_threshold + and os.path.getmtime(os.path.join(output_dir, model_name + ".xml")) - cur_time < time_threshold + ) + ): + raise e + + return ( + os.path.join(output_dir, model_name + ".xml"), + os.path.join(output_dir, model_name + ".bin"), + ) diff --git a/src/otx/algorithms/common/adapters/mmdeploy/ops/__init__.py b/src/otx/algorithms/common/adapters/mmdeploy/ops/__init__.py new file mode 100644 index 00000000000..1e8ac2cb03e --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmdeploy/ops/__init__.py @@ -0,0 +1,8 @@ +"""Initial file for mmdeploy ops.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .custom_ops import squeeze__default + +__all__ = ["squeeze__default"] diff --git a/src/otx/algorithms/common/adapters/mmdeploy/ops/custom_ops.py b/src/otx/algorithms/common/adapters/mmdeploy/ops/custom_ops.py new file mode 100644 index 00000000000..c64fbdcac2a --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmdeploy/ops/custom_ops.py @@ -0,0 +1,39 @@ +"""Custom patch of mmdeploy ops for openvino export.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmdeploy.core import SYMBOLIC_REWRITER +from mmdeploy.utils import get_ir_config +from torch.onnx import symbolic_helper + +# Remove previous registered symbolic +SYMBOLIC_REWRITER._registry._rewrite_records["squeeze"] = list() + + +@SYMBOLIC_REWRITER.register_symbolic("squeeze", is_pytorch=True) +def squeeze__default(ctx, g, self, dim=None): + """Register default symbolic function for `squeeze`. + + squeeze might be exported with IF node in ONNX, which is not supported in + lots of backend. + + mmdeploy 0.x version do not support opset13 version squeeze, therefore this function is for + custom patch for supporting opset13 version squeeze. + + If we adapt mmdeploy1.x version, then this function is no longer needed. + """ + if dim is None: + dims = [] + for i, size in enumerate(self.type().sizes()): + if size == 1: + dims.append(i) + else: + dims = [symbolic_helper._get_const(dim, "i", "dim")] + + if get_ir_config(ctx.cfg).get("opset_version", 11) >= 13: + axes = g.op("Constant", value_t=torch.tensor(dims, dtype=torch.long)) + return g.op("Squeeze", self, axes) + + return g.op("Squeeze", self, axes_i=dims) diff --git a/src/otx/algorithms/common/adapters/mmdeploy/utils/__init__.py b/src/otx/algorithms/common/adapters/mmdeploy/utils/__init__.py new file mode 100644 index 00000000000..0ef8a6ef89a --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmdeploy/utils/__init__.py @@ -0,0 +1,16 @@ +"""Init file for otx.algorithms.common.adapters.mmdeploy.utils.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .mmdeploy import is_mmdeploy_enabled +from .onnx import prepare_onnx_for_openvino, remove_nodes_by_op_type +from .utils import numpy_2_list, sync_batchnorm_2_batchnorm + +__all__ = [ + "is_mmdeploy_enabled", + "sync_batchnorm_2_batchnorm", + "numpy_2_list", + "prepare_onnx_for_openvino", + "remove_nodes_by_op_type", +] diff --git a/src/otx/algorithms/common/adapters/mmdeploy/utils/mmdeploy.py b/src/otx/algorithms/common/adapters/mmdeploy/utils/mmdeploy.py new file mode 100644 index 00000000000..ee33bbd3705 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmdeploy/utils/mmdeploy.py @@ -0,0 +1,73 @@ +"""Functions for mmdeploy adapters.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import importlib + +import onnx + + +def is_mmdeploy_enabled(): + """Checks if the 'mmdeploy' Python module is installed and available for use. + + Returns: + bool: True if 'mmdeploy' is installed, False otherwise. + + Example: + >>> is_mmdeploy_enabled() + True + """ + return importlib.util.find_spec("mmdeploy") is not None + + +def mmdeploy_init_model_helper(ctx, model_checkpoint=None, cfg_options=None, **kwargs): + """Helper function for initializing a model for inference using the 'mmdeploy' library.""" + + model_builder = kwargs.pop("model_builder") + model = model_builder( + ctx.model_cfg, + checkpoint=model_checkpoint, + device=ctx.device, + cfg_options=cfg_options, + ) + + # TODO: Need to investigate it why + # NNCF compressed model lost trace context from time to time with no reason + # even with 'torch.no_grad()'. Explicitly setting 'requires_grad' to'False' + # makes things easier. + for i in model.parameters(): + i.requires_grad = False + + return model + + +def update_deploy_cfg(onnx_path, deploy_cfg, mo_options=None): + """Update the 'deploy_cfg' configuration file based on the ONNX model specified by 'onnx_path'.""" + + from mmdeploy.utils import get_backend_config, get_ir_config + + onnx_model = onnx.load(onnx_path) + ir_config = get_ir_config(deploy_cfg) + get_backend_config(deploy_cfg) + + # update input + input_names = [i.name for i in onnx_model.graph.input] + ir_config["input_names"] = input_names + + # update output + output_names = [i.name for i in onnx_model.graph.output] + ir_config["output_names"] = output_names + + # update mo options + mo_options = mo_options if mo_options else dict() + deploy_cfg.merge_from_dict({"backend_config": {"mo_options": mo_options}}) + + +if is_mmdeploy_enabled(): + # fmt: off + # FIXME: openvino pot library adds stream handlers to root logger + # which makes annoying duplicated logging + from mmdeploy.utils import get_root_logger + get_root_logger().propagate = False + # fmt: on diff --git a/src/otx/algorithms/common/adapters/mmdeploy/utils/onnx.py b/src/otx/algorithms/common/adapters/mmdeploy/utils/onnx.py new file mode 100644 index 00000000000..7e19f57b6be --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmdeploy/utils/onnx.py @@ -0,0 +1,123 @@ +"""Functions for onnx adapters.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from functools import partial +from typing import Callable + +import onnx +from onnx import ModelProto, NodeProto # pylint: disable = no-name-in-module + + +def remove_nodes(model: ModelProto, predicate: Callable) -> ModelProto: + """Remove nodes from ONNX model. + + Args: + model (onnx.ModelProto): Input onnx model. + predicate (Callable): A function to predicate a node. + + Returns: + onnx.ModelProto: Modified onnx model. + """ + # ! this doesn't handle inputs/outputs + while True: + connect = None + for i, node in enumerate(model.graph.node): + if predicate(node): + assert len(node.input) == 1 + assert len(node.output) == 1 + connect = (node.input[0], node.output[0]) + del model.graph.node[i] + break + if not connect: + break + src, dst = connect + for node in model.graph.node: + for i, _input in enumerate(node.input): + if _input == dst: + node.input[i] = src + return model + + +def is_op(node: NodeProto, op_name) -> bool: + """Check if an op is identity.""" + return node.op_type == op_name + + +def remove_node(model: ModelProto, op_name: str) -> ModelProto: # noqa: C901 + """Remove identity node from an ONNX model. + + Args: + model (onnx.ModelProto): Input onnx model. + op_name (str): Operation name. + """ + graph = model.graph + + def simplify_inputs(): + connect = None + for _input in graph.input: + for i, node in enumerate(graph.node): + if node.op_type == op_name and node.input[0] == _input.name: + connect = (node.input[0], node.output[0]) + del graph.node[i] + break + if connect: + break + if not connect: + return False + src, dst = connect + for node in graph.node: + for i, input_name in enumerate(node.input): + if input_name == dst: + node.input[i] = src + # the input just changed won't be an output + return True + + def simplify_outputs(): + connect = None + for output in graph.output: + for i, node in enumerate(graph.node): + if node.op_type == op_name and node.output[0] == output.name: + connect = (node.input[0], node.output[0]) + del graph.node[i] + break + if connect: + break + if not connect: + return False + src, dst = connect + for node in graph.node: + for i, output_name in enumerate(node.output): + if output_name == src: + node.output[i] = dst + # the output just renamed may be someone's input + for i, input_name in enumerate(node.input): + if input_name == src: + node.input[i] = dst + return True + + while simplify_inputs(): + pass + + while simplify_outputs(): + pass + + new_op = partial(is_op, op_name=op_name) + remove_nodes(model, new_op) + + +def remove_nodes_by_op_type(onnx_model, op_type): + """Remove all nodes of a specified op type from the ONNX model.""" + # TODO: support more nodes + remove_node(onnx_model, op_type) + onnx.checker.check_model(onnx_model) + return onnx_model + + +def prepare_onnx_for_openvino(in_path, out_path): + """Modify the specified ONNX model to be compatible with OpenVINO by removing 'Mark' op nodes.""" + onnx_model = onnx.load(in_path) + onnx_model = remove_nodes_by_op_type(onnx_model, "Mark") + onnx.checker.check_model(onnx_model) + onnx.save(onnx_model, out_path) diff --git a/src/otx/algorithms/common/adapters/mmdeploy/utils/operations_domain.py b/src/otx/algorithms/common/adapters/mmdeploy/utils/operations_domain.py new file mode 100644 index 00000000000..e54af8bf1ab --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmdeploy/utils/operations_domain.py @@ -0,0 +1,11 @@ +"""Add domain function.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +DOMAIN_CUSTOM_OPS_NAME = "org.openvinotoolkit" + + +def add_domain(name_operator: str) -> str: + """Function for adding to DOMAIN_CUSTOM_OPS_NAME.""" + return DOMAIN_CUSTOM_OPS_NAME + "::" + name_operator diff --git a/src/otx/algorithms/common/adapters/mmdeploy/utils/utils.py b/src/otx/algorithms/common/adapters/mmdeploy/utils/utils.py new file mode 100644 index 00000000000..a0025bc8364 --- /dev/null +++ b/src/otx/algorithms/common/adapters/mmdeploy/utils/utils.py @@ -0,0 +1,69 @@ +"""Util functions of otx.algorithms.common.adapters.mmdeploy.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from collections.abc import MutableMapping + +import numpy as np +import torch + + +def sync_batchnorm_2_batchnorm(module, dim=2): + """Syncs the BatchNorm layers in a model to use regular BatchNorm layers.""" + if dim == 1: + bn = torch.nn.BatchNorm1d + elif dim == 2: + bn = torch.nn.BatchNorm2d + elif dim == 3: + bn = torch.nn.BatchNorm3d + else: + raise NotImplementedError() + + module_output = module + if isinstance(module, torch.nn.SyncBatchNorm): + module_output = bn( + module.num_features, + module.eps, + module.momentum, + module.affine, + module.track_running_stats, + ) + if module.affine: + module_output.weight.data = module.weight.data.clone().detach() + module_output.bias.data = module.bias.data.clone().detach() + module_output.weight.requires_grad = module.weight.requires_grad + module_output.bias.requires_grad = module.bias.requires_grad + + module_output.running_mean = module.running_mean + module_output.running_var = module.running_var + module_output.num_batches_tracked = module.num_batches_tracked + if hasattr(module, "qconfig"): + module_output.qconfig = module.qconfig + + for name, child in module.named_children(): + module_output.add_module(name, sync_batchnorm_2_batchnorm(child, dim)) + + del module + + return module_output + + +def numpy_2_list(data): + """Converts NumPy arrays to Python lists.""" + + if isinstance(data, np.ndarray): + return data.tolist() + + if isinstance(data, MutableMapping): + for key, value in data.items(): + data[key] = numpy_2_list(value) + elif isinstance(data, (list, tuple)): + data_ = [] + for value in data: + data_.append(numpy_2_list(value)) + if isinstance(data, tuple): + data = tuple(data_) + else: + data = data_ + return data diff --git a/src/otx/algorithms/common/adapters/nncf/__init__.py b/src/otx/algorithms/common/adapters/nncf/__init__.py new file mode 100644 index 00000000000..95410b6eb15 --- /dev/null +++ b/src/otx/algorithms/common/adapters/nncf/__init__.py @@ -0,0 +1,48 @@ +"""Adapters for nncf support.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# flake8: noqa + +from otx.core.patcher import Patcher + +from .compression import ( + AccuracyAwareLrUpdater, + get_nncf_metadata, + is_checkpoint_nncf, + is_state_nncf, +) +from .patches import ( + nncf_trace_context, + nncf_trace_wrapper, + no_nncf_trace_wrapper, +) +from .utils import ( + check_nncf_is_enabled, + get_nncf_version, + is_accuracy_aware_training_set, + is_in_nncf_tracing, + is_nncf_enabled, + no_nncf_trace, +) + +NNCF_PATCHER = Patcher() + + +__all__ = [ + "NNCF_PATCHER", + "AccuracyAwareLrUpdater", + "check_nncf_is_enabled", + "get_nncf_metadata", + "get_nncf_version", + "is_accuracy_aware_training_set", + "is_checkpoint_nncf", + "is_in_nncf_tracing", + "is_state_nncf", + "no_nncf_trace", + "nncf_trace_context", + "no_nncf_trace_wrapper", + "nncf_trace_wrapper", + "is_nncf_enabled", +] diff --git a/src/otx/algorithms/common/adapters/nncf/compression.py b/src/otx/algorithms/common/adapters/nncf/compression.py new file mode 100644 index 00000000000..1c5ab2267a9 --- /dev/null +++ b/src/otx/algorithms/common/adapters/nncf/compression.py @@ -0,0 +1,90 @@ +"""NNCF utils.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from dataclasses import dataclass, field +from typing import Any, Dict, Optional + +import numpy as np +import torch + +from .utils import check_nncf_is_enabled, get_nncf_version + + +@dataclass +class NNCFMetaState: + """NNCF meta state wrapper.""" + + state_to_build: Optional[Dict[str, torch.Tensor]] = field(default=None) + data_to_build: Optional[np.ndarray] = field(default=None) + compression_ctrl: Optional[Dict[Any, Any]] = field(default=None) + + def __repr__(self): + """Repr.""" + out = f"{self.__class__.__name__}(" + if self.state_to_build is not None: + out += "state_to_build='', " + if self.data_to_build is not None: + out += "data_to_build='', " + if self.compression_ctrl is not None: + out += "compression_ctrl='', " + if out[-2:] == ", ": + out = out[:-2] + out += ")" + return out + + +def get_nncf_metadata(): + """Get NNCF related metadata. + + The function returns NNCF metadata that should be stored into a checkpoint. + The metadata is used to check in wrap_nncf_model if the checkpoint should be used + to resume NNCF training or initialize NNCF fields of NNCF-wrapped model. + """ + check_nncf_is_enabled() + return dict(nncf_enable_compression=True, nncf_version=get_nncf_version()) + + +def is_state_nncf(state): + """Check if state_dict is NNCF state_dict. + + The function uses metadata stored in a dict_state to check if the + checkpoint was the result of trainning of NNCF-compressed model. + See the function get_nncf_metadata above. + """ + return bool(state.get("meta", {}).get("nncf_enable_compression", False)) + + +def is_checkpoint_nncf(path): + """Check if path is NNCF checkpoint. + + The function uses metadata stored in a checkpoint to check if the + checkpoint was the result of trainning of NNCF-compressed model. + See the function get_nncf_metadata above. + """ + try: + checkpoint = torch.load(path, map_location="cpu") + return is_state_nncf(checkpoint) + except FileNotFoundError: + return False + + +class AccuracyAwareLrUpdater: + """AccuracyAwareLrUpdater.""" + + def __init__(self, lr_hook): + self._lr_hook = lr_hook + self._lr_hook.warmup_iters = 0 + + def step(self, *args, **kwargs): + """step.""" + + @property + def base_lrs(self): + """base_lrs.""" + return self._lr_hook.base_lr + + @base_lrs.setter + def base_lrs(self, value): + self._lr_hook.base_lr = value diff --git a/src/otx/algorithms/common/adapters/nncf/config.py b/src/otx/algorithms/common/adapters/nncf/config.py new file mode 100644 index 00000000000..f56e2519d28 --- /dev/null +++ b/src/otx/algorithms/common/adapters/nncf/config.py @@ -0,0 +1,119 @@ +"""NNCF config utils.""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import json +from copy import copy + + +def load_nncf_config(path): + """loading_nncf_config.""" + assert path.endswith(".json"), f"Only json files are allowed as optimisation configs, provided {path}" + with open(path, encoding="utf-8") as f_src: + nncf_config = json.load(f_src) + return nncf_config + + +def compose_nncf_config(nncf_config, enabled_options): + """compose_nncf_config.""" + optimisation_parts = nncf_config + + if "order_of_parts" in optimisation_parts: + # The result of applying the changes from optimisation parts + # may depend on the order of applying the changes + # (e.g. if for nncf_quantization it is sufficient to have `total_epochs=2`, + # but for sparsity it is required `total_epochs=50`) + # So, user can define `order_of_parts` in the optimisation_config + # to specify the order of applying the parts. + order_of_parts = optimisation_parts["order_of_parts"] + assert isinstance(order_of_parts, list), 'The field "order_of_parts" in optimisation config should be a list' + + for part in enabled_options: + assert ( + part in order_of_parts + ), f"The part {part} is selected, but it is absent in order_of_parts={order_of_parts}" + + optimisation_parts_to_choose = [part for part in order_of_parts if part in enabled_options] + + assert "base" in optimisation_parts, 'Error: the optimisation config does not contain the "base" part' + nncf_config_part = optimisation_parts["base"] + + for part in optimisation_parts_to_choose: + assert part in optimisation_parts, f'Error: the optimisation config does not contain the part "{part}"' + optimisation_part_dict = optimisation_parts[part] + try: + nncf_config_part = merge_dicts_and_lists_b_into_a(nncf_config_part, optimisation_part_dict) + except AssertionError as cur_error: + err_descr = ( + f"Error during merging the parts of nncf configs:\n" + f"the current part={part}, " + f"the order of merging parts into base is {optimisation_parts_to_choose}.\n" + f"The error is:\n{cur_error}" + ) + raise RuntimeError(err_descr) from None + + return nncf_config_part + + +def merge_dicts_and_lists_b_into_a(a, b): # pylint: disable=invalid-name + """merge_dict.""" + return _merge_dicts_and_lists_b_into_a(a, b, "") + + +def _merge_dicts_and_lists_b_into_a(a, b, cur_key=None): # pylint: disable=invalid-name + """merge_dict inner function. + + The function is inspired by mmcf.Config._merge_a_into_b, + but it + * works with usual dicts and lists and derived types + * supports merging of lists (by concatenating the lists) + * makes recursive merging for dict + dict case + * overwrites when merging scalar into scalar + Note that we merge b into a (whereas Config makes merge a into b), + since otherwise the order of list merging is counter-intuitive. + """ + + def _err_str(_a, _b, _key): + if _key is None: + _key_str = "of whole structures" + else: + _key_str = f"during merging for key=`{_key}`" + return ( + f"Error in merging parts of config: different types {_key_str}," + f" type(a) = {type(_a)}," + f" type(b) = {type(_b)}" + ) + + assert isinstance(a, (dict, list)), f"Can merge only dicts and lists, whereas type(a)={type(a)}" + assert isinstance(b, (dict, list)), _err_str(a, b, cur_key) + assert isinstance(a, list) == isinstance(b, list), _err_str(a, b, cur_key) + if isinstance(a, list): + # the main diff w.r.t. mmcf.Config -- merging of lists + return a + b + + a = copy(a) + for k in b.keys(): + if k not in a: + a[k] = copy(b[k]) + continue + new_cur_key = cur_key + "." + k if cur_key else k + if isinstance(a[k], (dict, list)): + a[k] = _merge_dicts_and_lists_b_into_a(a[k], b[k], new_cur_key) + continue + + assert not isinstance(b[k], (dict, list)), _err_str(a[k], b[k], new_cur_key) + + # suppose here that a[k] and b[k] are scalars, just overwrite + a[k] = b[k] + return a diff --git a/src/otx/algorithms/common/adapters/nncf/patches.py b/src/otx/algorithms/common/adapters/nncf/patches.py new file mode 100644 index 00000000000..e1dcf86489f --- /dev/null +++ b/src/otx/algorithms/common/adapters/nncf/patches.py @@ -0,0 +1,44 @@ +"""NNCFNetwork patch util functions for mmcv models.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from contextlib import contextmanager +from functools import partial + +from otx.algorithms.common.adapters.nncf.utils import nncf_trace, no_nncf_trace + + +@contextmanager +def nncf_trace_context(self, img_metas, nncf_compress_postprocessing=True): + """A context manager for nncf graph tracing.""" + + # onnx_export in mmdet head has a bug on GPU + # it must be on CPU + device_backup = next(self.parameters()).device # pylint: disable=stop-iteration-return + self = self.to("cpu") + + if nncf_compress_postprocessing: + self.forward = partial(self.forward, img_metas=img_metas, return_loss=False) + else: + self.forward = partial(self.forward_dummy) + + yield + + # make everything normal + self.__dict__.pop("forward") + self = self.to(device_backup) + + +def no_nncf_trace_wrapper(self, fn, *args, **kwargs): # pylint: disable=unused-argument,invalid-name + """A wrapper function not to trace in NNCF.""" + + with no_nncf_trace(): + return fn(*args, **kwargs) + + +def nncf_trace_wrapper(self, fn, *args, **kwargs): # pylint: disable=unused-argument,invalid-name + """A wrapper function to trace in NNCF.""" + + with nncf_trace(): + return fn(*args, **kwargs) diff --git a/src/otx/algorithms/common/adapters/nncf/utils/__init__.py b/src/otx/algorithms/common/adapters/nncf/utils/__init__.py new file mode 100644 index 00000000000..ec2231db85a --- /dev/null +++ b/src/otx/algorithms/common/adapters/nncf/utils/__init__.py @@ -0,0 +1,26 @@ +"""NNCF utils.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .utils import ( + check_nncf_is_enabled, + get_nncf_version, + is_accuracy_aware_training_set, + is_in_nncf_tracing, + is_nncf_enabled, + load_checkpoint, + nncf_trace, + no_nncf_trace, +) + +__all__ = [ + "is_nncf_enabled", + "check_nncf_is_enabled", + "get_nncf_version", + "load_checkpoint", + "no_nncf_trace", + "nncf_trace", + "is_in_nncf_tracing", + "is_accuracy_aware_training_set", +] diff --git a/src/otx/algorithms/common/adapters/nncf/utils/utils.py b/src/otx/algorithms/common/adapters/nncf/utils/utils.py new file mode 100644 index 00000000000..5f60504d046 --- /dev/null +++ b/src/otx/algorithms/common/adapters/nncf/utils/utils.py @@ -0,0 +1,129 @@ +"""NNCF utils.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from collections import OrderedDict +from contextlib import contextmanager +from importlib.util import find_spec + +import torch + +_is_nncf_enabled = find_spec("nncf") is not None + + +def is_nncf_enabled(): + """is_nncf_enabled.""" + return _is_nncf_enabled + + +def check_nncf_is_enabled(): + """check_nncf_is_enabled.""" + if not is_nncf_enabled(): + raise RuntimeError("Tried to use NNCF, but NNCF is not installed") + + +def get_nncf_version(): + """get_nncf_version.""" + if not is_nncf_enabled(): + return None + import nncf + + return nncf.__version__ + + +def load_checkpoint(model, filename, map_location=None, strict=False): + """Load checkpoint from a file or URI. + + Args: + model (Module): Module to load checkpoint. + filename (str): Either a filepath or URL or modelzoo://xxxxxxx. + map_location (str): Same as :func:`torch.load`. + strict (bool): Whether to allow different params for the model and + checkpoint. + + Returns: + dict or OrderedDict: The loaded checkpoint. + """ + # from nncf.torch import load_state + from mmcv.runner import load_state_dict + + checkpoint = torch.load(filename, map_location=map_location) + nncf_state = None + compression_state = None + # get state_dict from checkpoint + if isinstance(checkpoint, OrderedDict): + base_state = checkpoint + elif isinstance(checkpoint, dict) and "state_dict" in checkpoint: + if "meta" in checkpoint and "nncf_meta" in checkpoint["meta"]: + nncf_state = checkpoint["state_dict"] + compression_state = checkpoint["meta"]["nncf_meta"].compression_ctrl + base_state = checkpoint["meta"]["nncf_meta"].state_to_build + else: + base_state = checkpoint["state_dict"] + else: + raise RuntimeError(f"No state_dict found in checkpoint file {filename}") + load_state_dict(model, base_state, strict=strict) + return compression_state, nncf_state + + +@contextmanager +def nullcontext(): + """Context which does nothing.""" + yield + + +def no_nncf_trace(): + """Wrapper for original NNCF no_nncf_trace context.""" + + if is_nncf_enabled(): + from nncf.torch.dynamic_graph.context import ( + no_nncf_trace as original_no_nncf_trace, + ) + + return original_no_nncf_trace() + return nullcontext() + + +def nncf_trace(): + """Trace nncf context.""" + if is_nncf_enabled(): + + @contextmanager + def _nncf_trace(): + from nncf.torch.dynamic_graph.context import get_current_context + + ctx = get_current_context() + if ctx is not None and not ctx.is_tracing: + ctx.enable_tracing() + yield + ctx.disable_tracing() + else: + yield + + return _nncf_trace() + return nullcontext() + + +def is_in_nncf_tracing(): + """is_in_nncf_tracing.""" + if not is_nncf_enabled(): + return False + + from nncf.torch.dynamic_graph.context import get_current_context + + ctx = get_current_context() + + if ctx is None: + return False + return ctx.is_tracing + + +def is_accuracy_aware_training_set(nncf_config): + """is_accuracy_aware_training_set.""" + if not is_nncf_enabled(): + return False + from nncf.config.utils import is_accuracy_aware_training + + is_acc_aware_training_set = is_accuracy_aware_training(nncf_config) + return is_acc_aware_training_set diff --git a/src/otx/algorithms/common/adapters/torch/__init__.py b/src/otx/algorithms/common/adapters/torch/__init__.py new file mode 100644 index 00000000000..7d2cf100bb3 --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/__init__.py @@ -0,0 +1,4 @@ +"""Adapters for OTX Common Algorithms.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/common/adapters/torch/amp/__init__.py b/src/otx/algorithms/common/adapters/torch/amp/__init__.py new file mode 100644 index 00000000000..1b0ce69ed34 --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/amp/__init__.py @@ -0,0 +1,9 @@ +"""Custom AMP (Automatic Mixed Precision package) in OTX.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +try: + from .xpu_grad_scaler import XPUGradScaler # noqa: F401 +except: # noqa: E722 + pass diff --git a/src/otx/algorithms/common/adapters/torch/amp/xpu_grad_scaler.py b/src/otx/algorithms/common/adapters/torch/amp/xpu_grad_scaler.py new file mode 100644 index 00000000000..be37f003b78 --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/amp/xpu_grad_scaler.py @@ -0,0 +1,120 @@ +"""Custom GradScaler to scale loss.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from collections import abc, defaultdict +from typing import List + +import torch + +from otx.algorithms.common.utils.utils import is_xpu_available + +if is_xpu_available(): + from intel_extension_for_pytorch.cpu.autocast._grad_scaler import _MultiDeviceReplicator +from torch.cuda.amp.grad_scaler import GradScaler, _refresh_per_optimizer_state + + +class XPUGradScaler(GradScaler): + """GradScaler for XPU.""" + + def __init__(self, init_scale=2.0**16, growth_factor=2.0, backoff_factor=0.5, growth_interval=2000, enabled=True): + self._enabled = enabled + if not is_xpu_available(): + raise RuntimeError("XPU GradScaler requires XPU device.") + + if self._enabled: + assert growth_factor > 1.0, "The growth factor must be > 1.0." + assert backoff_factor < 1.0, "The backoff factor must be < 1.0." + + self._init_scale = init_scale + # self._scale will be lazily initialized during the first call to scale() + self._scale = None + self._growth_factor = growth_factor + self._backoff_factor = backoff_factor + self._growth_interval = growth_interval + self._init_growth_tracker = 0 + # self._growth_tracker will be lazily initialized during the first call to scale() + self._growth_tracker = None + self._per_optimizer_states = defaultdict(_refresh_per_optimizer_state) + + def scale(self, outputs): + """Multiplies ('scales') a tensor or list of tensors by the scale factor. + + Returns scaled outputs. If this instance of :class:`GradScaler` is not enabled, outputs are returned + unmodified. + + Args: + outputs (Tensor or iterable of Tensors): Outputs to scale. + """ + if not self._enabled: + return outputs + + # Short-circuit for the common case. + if isinstance(outputs, torch.Tensor): + assert outputs.device.type == "xpu" + if self._scale is None: + self._lazy_init_scale_growth_tracker(outputs.device) + assert self._scale is not None + return outputs * self._scale.to(device=outputs.device, non_blocking=True) + + # Invoke the more complex machinery only if we're treating multiple outputs. + stash: List[_MultiDeviceReplicator] = [] # holds a reference that can be overwritten by apply_scale + + def apply_scale(val): + if isinstance(val, torch.Tensor): + assert val.device.type == "xpu" + if len(stash) == 0: + if self._scale is None: + self._lazy_init_scale_growth_tracker(val.device) + assert self._scale is not None + stash.append(_MultiDeviceReplicator(self._scale)) + return val * stash[0].get(val.device) + elif isinstance(val, abc.Iterable): + iterable = map(apply_scale, val) + if isinstance(val, (list, tuple)): + return type(val)(iterable) + else: + return iterable + else: + raise ValueError("outputs must be a Tensor or an iterable of Tensors") + + return apply_scale(outputs) + + def _unscale_grads_(self, optimizer, inv_scale, found_inf, allow_bf16=False): + per_device_inv_scale = _MultiDeviceReplicator(inv_scale) + per_device_found_inf = _MultiDeviceReplicator(found_inf) + + # To set up _amp_foreach_non_finite_check_and_unscale_, split grads by device and dtype. + # There could be hundreds of grads, so we'd like to iterate through them just once. + # However, we don't know their devices or dtypes in advance. + + # https://stackoverflow.com/questions/5029934/defaultdict-of-defaultdict + # Google says mypy struggles with defaultdicts type annotations. + per_device_and_dtype_grads = defaultdict(lambda: defaultdict(list)) # type: ignore[var-annotated] + with torch.no_grad(): + for group in optimizer.param_groups: + for param in group["params"]: + if param.grad is None: + continue + if param.grad.is_sparse: + # is_coalesced() == False means the sparse grad has values with duplicate indices. + # coalesce() deduplicates indices and adds all values that have the same index. + # For scaled bf16 values, there's a good chance coalescing will cause overflow, + # so we should check the coalesced _values(). + if param.grad.dtype is torch.bfloat16: + param.grad = param.grad.coalesce() + to_unscale = param.grad._values() + else: + to_unscale = param.grad + + # TODO: is there a way to split by device and dtype without appending in the inner loop? + per_device_and_dtype_grads[to_unscale.device][to_unscale.dtype].append(to_unscale) + + for device, per_dtype_grads in per_device_and_dtype_grads.items(): + for grads in per_dtype_grads.values(): + torch._amp_foreach_non_finite_check_and_unscale_( + grads, per_device_found_inf.get(device), per_device_inv_scale.get(device) + ) + + return per_device_found_inf._per_device_tensors diff --git a/src/otx/algorithms/common/adapters/torch/dataloaders/__init__.py b/src/otx/algorithms/common/adapters/torch/dataloaders/__init__.py new file mode 100644 index 00000000000..3132fbf7e0f --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/__init__.py @@ -0,0 +1,10 @@ +"""Dataloaders used in OTX.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# flake8: noqa + +from .composed_dataloader import ComposedDL + +__all__ = ["ComposedDL"] diff --git a/src/otx/algorithms/common/adapters/torch/dataloaders/composed_dataloader.py b/src/otx/algorithms/common/adapters/torch/dataloaders/composed_dataloader.py new file mode 100644 index 00000000000..463f5771333 --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/composed_dataloader.py @@ -0,0 +1,71 @@ +"""Composed dataloader.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.utils.logger import get_logger + +logger = get_logger() + + +class CDLIterator: + """Iterator for aligning the number of batches as many as samples in the first iterator.""" + + def __init__(self, cdl): + self._cdl = cdl + self._index = 0 + self._cdl_iter = [iter(dl) for dl in self._cdl.loaders] + + def __next__(self): + """Generate the next batch.""" + if self._index < self._cdl.max_iter: + batches = {} + for i, iterator in enumerate(self._cdl_iter): + if i == 0: + batches = next(iterator) + else: + try: + batches[f"extra_{i-1}"] = next(iterator) + except StopIteration: + self._cdl_iter[1] = iter(self._cdl.loaders[1]) + batches[f"extra_{i-1}"] = next(self._cdl_iter[1]) + self._index += 1 + return batches + raise StopIteration + + +class ComposedDL: + """Composed dataloader for combining two or more loaders together.""" + + class DummySampler: + """Dummy sampler class to relay set_epoch() call to the list of data loaders in the CDL.""" + + def __init__(self, cdl): + self.cdl = cdl + + def set_epoch(self, epoch): + """Set epoch.""" + loaders = self.cdl.loaders + for loader in loaders: + loader.sampler.set_epoch(epoch) + + def __init__(self, loaders=None): + if loaders is None: + loaders = [] + self.loaders = loaders + self.max_iter = len(self.loaders[0]) + logger.info(f"possible max iterations = {self.max_iter}") + self._sampler = ComposedDL.DummySampler(self) + + def __len__(self): + """Return length of the first loader.""" + return self.max_iter + + def __iter__(self): + """Iter.""" + return CDLIterator(self) + + @property + def sampler(self): + """Return sampler.""" + return self._sampler diff --git a/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/__init__.py b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/__init__.py new file mode 100644 index 00000000000..eca373245fa --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/__init__.py @@ -0,0 +1,12 @@ +"""Samplers for imbalanced and incremental learning.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# flake8: noqa + +from .otx_sampler import OTXSampler +from .balanced_sampler import BalancedSampler +from .cls_incr_sampler import ClsIncrSampler + +__all__ = ["OTXSampler", "BalancedSampler", "ClsIncrSampler"] diff --git a/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py new file mode 100644 index 00000000000..12492a46b31 --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py @@ -0,0 +1,139 @@ +"""Balanced sampler for imbalanced data.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import math +from typing import Union + +import numpy as np +from torch.utils.data import Dataset + +from otx.utils.logger import get_logger + +from .otx_sampler import OTXSampler + +logger = get_logger() + + +class BalancedSampler(OTXSampler): # pylint: disable=too-many-instance-attributes + """Balanced sampler for imbalanced data for class-incremental task. + + This sampler is a sampler that creates an effective batch + In reduce mode, + reduce the iteration size by estimating the trials + that all samples in the tail class are selected more than once with probability 0.999 + + Args: + dataset (Dataset): A built-up dataset + samples_per_gpu (int): batch size of Sampling + efficient_mode (bool): Flag about using efficient mode + num_replicas (int, optional): Number of processes participating in + distributed training. By default, :attr:`world_size` is retrieved from the + current distributed group. + rank (int, optional): Rank of the current process within :attr:`num_replicas`. + By default, :attr:`rank` is retrieved from the current distributed + group. + drop_last (bool, optional): if ``True``, then the sampler will drop the + tail of the data to make it evenly divisible across the number of + replicas. If ``False``, the sampler will add extra indices to make + the data evenly divisible across the replicas. Default: ``False``. + n_repeats (Union[float, int, str], optional) : number of iterations for manual setting + """ + + def __init__( + self, + dataset: Dataset, + samples_per_gpu: int, + efficient_mode: bool = False, + num_replicas: int = 1, + rank: int = 0, + drop_last: bool = False, + n_repeats: Union[float, int, str] = 1, + ): + self.samples_per_gpu = samples_per_gpu + self.num_replicas = num_replicas + self.rank = rank + self.drop_last = drop_last + + super().__init__(dataset, samples_per_gpu, n_repeats=n_repeats) + + self.img_indices = {k: v for k, v in self.dataset.img_indices.items() if len(v) > 0} + self.num_cls = len(self.img_indices.keys()) + self.data_length = len(self.dataset) + self.num_trials = int(self.data_length / self.num_cls) + + if efficient_mode: + # Reduce the # of sampling (sampling data for a single epoch) + num_tail = min(len(cls_indices) for cls_indices in self.img_indices.values()) + if num_tail > 1: + base = 1 - (1 / num_tail) + num_reduced_trials = int(math.log(0.001, base)) + self.num_trials = min(num_reduced_trials, self.num_trials) + + self.num_samples = self._calculate_num_samples() + + logger.info( + "Balanced sampler will select balanced samples " f"{math.ceil(self.num_samples/samples_per_gpu)} times" + ) + + def _calculate_num_samples(self): + num_samples = self.num_trials * self.num_cls * self.repeat + + if self.num_replicas > 1: + # If the dataset length is evenly divisible by # of replicas, then there + # is no need to drop any data, since the dataset will be split equally. + if self.drop_last and num_samples % self.num_replicas != 0: # type: ignore + # Split to nearest available length that is evenly divisible. + # This is to ensure each rank receives the same amount of data when + # using this Sampler. + num_samples = math.ceil( + # `type:ignore` is required because Dataset cannot provide a default __len__ + # see NOTE in pytorch/torch/utils/data/sampler.py + (num_samples - self.num_replicas) + / self.num_replicas # type: ignore + ) + else: + num_samples = math.ceil(num_samples / self.num_replicas) # type: ignore + self.total_size = num_samples * self.num_replicas + + return num_samples + + def __iter__(self): + """Iter.""" + indices = [] + for _ in range(self.repeat): + for _ in range(self.num_trials): + indice = np.concatenate( + [np.random.choice(self.img_indices[cls_indices], 1) for cls_indices in self.img_indices.keys()] + ) + indices.append(indice) + + indices = np.concatenate(indices) + indices = indices.astype(np.int64).tolist() + + if self.num_replicas > 1: + if not self.drop_last: + # add extra samples to make it evenly divisible + padding_size = self.total_size - len(indices) + if padding_size <= len(indices): + indices += indices[:padding_size] + else: + indices += (indices * math.ceil(padding_size / len(indices)))[:padding_size] + else: + # remove tail of data to make it evenly divisible. + indices = indices[: self.total_size] + assert len(indices) == self.total_size + + # split and distribute indices + len_indices = len(indices) + indices = indices[ + self.rank * len_indices // self.num_replicas : (self.rank + 1) * len_indices // self.num_replicas + ] + + assert len(indices) == self.num_samples + return iter(indices) + + def __len__(self): + """Return length of selected samples.""" + return self.num_samples diff --git a/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/cls_incr_sampler.py b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/cls_incr_sampler.py new file mode 100644 index 00000000000..58622a354f0 --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/cls_incr_sampler.py @@ -0,0 +1,150 @@ +"""Class incremental sampler for cls-incremental learning.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import math +import random +from typing import Union + +import numpy as np +from torch.utils.data import Dataset + +from .otx_sampler import OTXSampler + + +class ClsIncrSampler(OTXSampler): # pylint: disable=too-many-instance-attributes + """Sampler for Class-Incremental Task. + + This sampler is a sampler that creates an effective batch + For default setting, + the square root of (number of old data/number of new data) is used as the ratio of old data + In effective mode, + the ratio of old and new data is used as 1:1 + + Args: + dataset (Dataset): A built-up dataset + samples_per_gpu (int): batch size of Sampling + efficient_mode (bool): Flag about using efficient mode + num_replicas (int, optional): Number of processes participating in + distributed training. By default, :attr:`world_size` is retrieved from the + current distributed group. + rank (int, optional): Rank of the current process within :attr:`num_replicas`. + By default, :attr:`rank` is retrieved from the current distributed + group. + drop_last (bool, optional): if ``True``, then the sampler will drop the + tail of the data to make it evenly divisible across the number of + replicas. If ``False``, the sampler will add extra indices to make + the data evenly divisible across the replicas. Default: ``False``. + n_repeats (Union[float, int, str], optional) : number of iterations for manual setting + """ + + def __init__( + self, + dataset: Dataset, + samples_per_gpu: int, + efficient_mode: bool = False, + num_replicas: int = 1, + rank: int = 0, + drop_last: bool = False, + n_repeats: Union[float, int, str] = 1, + ): + self.samples_per_gpu = samples_per_gpu + self.num_replicas = num_replicas + self.rank = rank + self.drop_last = drop_last + + super().__init__(dataset, samples_per_gpu, n_repeats=n_repeats) + + if hasattr(self.dataset, "img_indices"): + self.new_indices = self.dataset.img_indices["new"] + self.old_indices = self.dataset.img_indices["old"] + else: + raise TypeError(f"{self.dataset} type does not have img_indices") + + if not len(self.new_indices) > 0: + self.new_indices = self.old_indices + self.old_indices = [] + + old_new_ratio = np.sqrt(len(self.old_indices) / len(self.new_indices)) + + if efficient_mode: + self.data_length = int(len(self.new_indices) * (1 + old_new_ratio)) + self.old_new_ratio = 1 + else: + self.data_length = len(self.dataset) + self.old_new_ratio = int(old_new_ratio) + + self.num_samples = self._calcuate_num_samples() + + def _calcuate_num_samples(self): + num_samples = self.repeat * (1 + self.old_new_ratio) * int(self.data_length / (1 + self.old_new_ratio)) + + if not self.drop_last: + num_samples += ( + int(np.ceil(self.data_length * self.repeat / self.samples_per_gpu)) * self.samples_per_gpu - num_samples + ) + + if self.num_replicas > 1: + # If the dataset length is evenly divisible by # of replicas, then there + # is no need to drop any data, since the dataset will be split equally. + if self.drop_last and num_samples % self.num_replicas != 0: # type: ignore + # Split to nearest available length that is evenly divisible. + # This is to ensure each rank receives the same amount of data when + # using this Sampler. + num_samples = math.ceil( + # `type:ignore` is required because Dataset cannot provide a default __len__ + # see NOTE in pytorch/torch/utils/data/sampler.py + (num_samples - self.num_replicas) + / self.num_replicas # type: ignore + ) + else: + num_samples = math.ceil(num_samples / self.num_replicas) # type: ignore + self.total_size = num_samples * self.num_replicas + + return num_samples + + def __iter__(self): + """Iter.""" + indices = [] + for _ in range(self.repeat): + for _ in range(int(self.data_length / (1 + self.old_new_ratio))): + indice = np.concatenate( + [np.random.choice(self.new_indices, 1), np.random.choice(self.old_indices, self.old_new_ratio)] + ) + indices.append(indice) + + indices = np.concatenate(indices) + if not self.drop_last: + num_extra = int( + np.ceil(self.data_length * self.repeat / self.samples_per_gpu) + ) * self.samples_per_gpu - len(indices) + indices = np.concatenate([indices, np.random.choice(indices, num_extra)]) + indices = indices.astype(np.int64).tolist() + + if self.num_replicas > 1: + if not self.drop_last: + # add extra samples to make it evenly divisible + padding_size = self.total_size - len(indices) + # add extra samples to make it evenly divisible + if padding_size <= len(indices): + indices += indices[:padding_size] + else: + indices += (indices * math.ceil(padding_size / len(indices)))[:padding_size] + else: + # remove tail of data to make it evenly divisible. + indices = indices[: self.total_size] + assert len(indices) == self.total_size + + # shuffle before distributing indices + random.shuffle(indices) + + # subsample + indices = indices[self.rank : self.total_size : self.num_replicas] + assert len(indices) == self.num_samples + + return iter(indices) + + def __len__(self): + """Return length of selected samples.""" + return self.num_samples diff --git a/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/otx_sampler.py b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/otx_sampler.py new file mode 100644 index 00000000000..ada7250c65c --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/otx_sampler.py @@ -0,0 +1,124 @@ +"""OTX sampler.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import math +from typing import Optional, Union + +import numpy as np +import torch +from torch.utils.data import Dataset +from torch.utils.data.sampler import Sampler + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import get_proper_repeat_times +from otx.algorithms.common.utils.task_adapt import unwrap_dataset +from otx.utils.logger import get_logger + +logger = get_logger() + + +class OTXSampler(Sampler): # pylint: disable=too-many-instance-attributes + """Sampler that easily adapts to the dataset statistics. + + In the exterme small dataset, the iteration per epoch could be set to 1 and then it could make slow training + since DataLoader reinitialized at every epoch. So, in the small dataset case, + OTXSampler repeats the dataset to enlarge the iterations per epoch. + + In the large dataset, the useful information is not totally linear relationship with the number of datasets. + It is close to the log scale relationship, rather. + + So, this sampler samples or repeats the datasets acoording to the statistics of dataset. + + Args: + dataset (Dataset): A built-up dataset + samples_per_gpu (int): batch size of Sampling + num_replicas (int, optional): Number of processes participating in + distributed training. By default, :attr:`world_size` is retrieved from the + current distributed group. + rank (int, optional): Rank of the current process within :attr:`num_replicas`. + By default, :attr:`rank` is retrieved from the current distributed + group. + shuffle (bool, optional): Flag about shuffling + coef (int, optional): controls the repeat value + min_repeat (float, optional): minimum value of the repeat dataset + n_repeats (Union[float, int str], optional) : number of iterations for manual setting + seed (int, optional): Random seed used to shuffle the sampler if + :attr:`shuffle=True`. This number should be identical across all + processes in the distributed group. Defaults to None. + """ + + def __init__( + self, + dataset: Dataset, + samples_per_gpu: int, + num_replicas: int = 1, + rank: int = 0, + shuffle: bool = True, + coef: float = -0.7, + min_repeat: float = 1.0, + n_repeats: Union[float, int, str] = "auto", + seed: Optional[int] = None, + ): + + self.dataset, _ = unwrap_dataset(dataset) + self.samples_per_gpu = samples_per_gpu + self.num_replicas = num_replicas + self.rank = rank + self.shuffle = shuffle + if n_repeats == "auto": + repeat = get_proper_repeat_times(len(self.dataset), self.samples_per_gpu, coef, min_repeat) + elif isinstance(n_repeats, (int, float)): + repeat = float(n_repeats) + else: + raise ValueError(f"n_repeats: {n_repeats} should be auto or float or int value") + # TODO: Currently, only supporting the int variable. + # Will be removed. + self.repeat = int(repeat) + self.num_samples = math.ceil(len(self.dataset) * self.repeat / self.num_replicas) + self.total_size = self.num_samples * self.num_replicas + + if seed is None: + seed = np.random.randint(2**31) + + self.seed = seed + self.epoch = 0 + + def __iter__(self): + """Iter.""" + if self.shuffle: + g = torch.Generator() + g.manual_seed(self.seed + self.epoch) + indices = torch.randperm(len(self.dataset), generator=g).tolist() + else: + indices = list(range(len(self.dataset))) + + # produce repeats e.g. [0, 0, 0, 1, 1, 1, 2, 2, 2....] + indices = [x for x in indices for _ in range(self.repeat)] + # add extra samples to make it evenly divisible + padding_size = self.total_size - len(indices) + indices += indices[:padding_size] + assert len(indices) == self.total_size + + # subsample per rank + indices = indices[self.rank : self.total_size : self.num_replicas] + assert len(indices) == self.num_samples + + # return up to num selected samples + return iter(indices) + + def __len__(self): + """Return length of selected samples.""" + return self.total_size + + def set_epoch(self, epoch: int) -> None: + """Sets the epoch for this sampler. + + When :attr:`shuffle=True`, this ensures all replicas use a different + random ordering for each epoch. Otherwise, the next iteration of this + sampler will yield the same ordering. + + Args: + epoch (int): Epoch number. + """ + self.epoch = epoch diff --git a/src/otx/algorithms/common/adapters/torch/utils/__init__.py b/src/otx/algorithms/common/adapters/torch/utils/__init__.py new file mode 100644 index 00000000000..b0b2ea4a3d0 --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/utils/__init__.py @@ -0,0 +1,9 @@ +"""Utils for modules using torch.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .bs_search_algo import BsSearchAlgo +from .utils import convert_sync_batchnorm, model_from_timm + +__all__ = ["BsSearchAlgo", "model_from_timm", "convert_sync_batchnorm"] diff --git a/src/otx/algorithms/common/adapters/torch/utils/bs_search_algo.py b/src/otx/algorithms/common/adapters/torch/utils/bs_search_algo.py new file mode 100644 index 00000000000..eaf8c1116e6 --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/utils/bs_search_algo.py @@ -0,0 +1,235 @@ +"""Algorithm to find a proper batch size which is fit to current GPU device.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from typing import Callable, Dict, Tuple + +import torch +import torch.distributed as dist + +from otx.utils.logger import get_logger + +logger = get_logger() + + +class BsSearchAlgo: + """Algorithm class to find optimal batch size. + + Args: + train_func (Callable[[int], None]): Training function with single arugment to set batch size. + default_bs (int): Default batch size. It should be bigger than 0. + max_bs (int): Maximum batch size. It should be bigger than 0. + """ + + def __init__(self, train_func: Callable[[int], None], default_bs: int, max_bs: int): + if default_bs <= 0: + raise ValueError("Batch size should be bigger than 0.") + if max_bs <= 0: + raise ValueError("train data set size should be bigger than 0.") + + if max_bs < default_bs: + default_bs = max_bs + + self._train_func = train_func + self._default_bs = default_bs + self._max_bs = max_bs + self._bs_try_history: Dict[int, int] = {} + _, self._total_mem = torch.cuda.mem_get_info() + self._mem_lower_bound = 0.8 * self._total_mem + self._mem_upper_bound = 0.85 * self._total_mem + + def _try_batch_size(self, bs: int) -> Tuple[bool, int]: + cuda_oom = False + torch.cuda.reset_max_memory_cached(device=None) + torch.cuda.empty_cache() + + try: + self._train_func(bs) + except RuntimeError as e: + if str(e).startswith("CUDA out of memory."): + cuda_oom = True + else: + raise e + + max_memory_reserved = torch.cuda.max_memory_reserved(device=None) + + if dist.is_initialized(): # Aggregate all results and broadcast to all processes + rank = dist.get_rank() + try_result = torch.tensor([int(cuda_oom), max_memory_reserved], dtype=torch.int64).cuda() + + if rank == 0: + try_result_arr = [torch.empty(2, dtype=torch.int64).cuda() for _ in range(dist.get_world_size())] + dist.gather(try_result, gather_list=try_result_arr, dst=0) + else: + dist.gather(try_result, dst=0) + + if rank == 0: + try_result_arr = torch.stack(try_result_arr) + cuda_oom = torch.any(try_result_arr[:, 0]) # type: ignore + max_memory_reserved = torch.max(try_result_arr[:, 1]) # type: ignore + total_try_result = torch.tensor([cuda_oom, max_memory_reserved], dtype=torch.int64).cuda() + else: + total_try_result = torch.empty(2, dtype=torch.int64).cuda() + + dist.broadcast(total_try_result, src=0) + + cuda_oom = total_try_result[0].bool().item() + max_memory_reserved = total_try_result[1].item() + + if not cuda_oom: + # Because heapq only supports min heap, use negatized batch size + self._bs_try_history[bs] = max_memory_reserved + + logger.debug( + f"Adapting Batch size => bs : {bs}, CUDA_OOM : {cuda_oom}, " + f"GPU memory usage : {max_memory_reserved / self._total_mem}%" + ) + torch.cuda.empty_cache() + + return cuda_oom, max_memory_reserved + + @staticmethod + def _get_even_center_val(val1: int, val2: int) -> int: + ret = (val1 + val2) // 2 + if ret % 2 == 1: + ret += 1 + return ret + + def auto_decrease_batch_size(self) -> int: + """Decrease batch size if default batch size isn't fit to current GPU device. + + Returns: + int: Proper batch size possibly decreased as default value isn't fit + """ + available_bs = 0 + current_bs = self._default_bs + lowest_unavailable_bs = self._default_bs + 2 + + while True: + cuda_oom, max_memory_reserved = self._try_batch_size(current_bs) + + # If GPU memory usage is too close to limit, CUDA OOM can be raised during training + if cuda_oom or max_memory_reserved > self._mem_upper_bound: + if current_bs < lowest_unavailable_bs: + lowest_unavailable_bs = current_bs + current_bs = self._get_even_center_val(current_bs, available_bs) + else: + available_bs = current_bs + current_bs = self._get_even_center_val(current_bs, lowest_unavailable_bs) + + if lowest_unavailable_bs - available_bs <= 2: + break + + if available_bs == 0: + raise RuntimeError("Current device can't train model even with 2.") + + return available_bs + + def find_big_enough_batch_size(self, drop_last: bool = False) -> int: + """Find a big enough batch size. + + This function finds a big enough batch size by training with various batch sizes. + It estimate a batch size using equation is estimated using training history. + The reason why using the word "big enough" is that it tries to find not maxmium but big enough value which uses + GPU memory between lower and upper bound. + + Args: + drop_last (bool): Whether to drop the last incomplete batch. + + Raises: + RuntimeError: If training with batch size 2 can't be run, raise an error. + + Returns: + int: Big enough batch size. + """ + estimated_bs = self._default_bs + + # try default batch size + cuda_oom, bs_mem_usage = self._try_batch_size(estimated_bs) + if cuda_oom or bs_mem_usage > self._mem_upper_bound: + self._default_bs -= 2 + if self._default_bs <= 0: + raise RuntimeError("Current device can't train model even with 2.") + + return self.auto_decrease_batch_size() + + # try default batch size + 2 + estimated_bs += 2 + if estimated_bs > self._max_bs: + return self._default_bs + cuda_oom, bs_mem_usage = self._try_batch_size(estimated_bs) + if cuda_oom or bs_mem_usage > self._mem_upper_bound: + return self._default_bs + + # estimate batch size using equation + estimation_pct = 0.82 + while True: + estimated_bs = self._estimate_batch_size(estimation_pct) + if estimated_bs in self._bs_try_history: + break + cuda_oom, mem_usage = self._try_batch_size(estimated_bs) + + if cuda_oom: + estimation_pct -= 0.1 + if estimation_pct <= 0: + estimated_bs = self._default_bs + 2 + break + elif self._mem_lower_bound <= mem_usage <= self._mem_upper_bound: + break + else: + estimation_pct = 0.82 + + if drop_last and (self._max_bs // 2 < estimated_bs < self._max_bs): + estimated_bs = self._max_bs // 2 + + return estimated_bs + + def _estimate_batch_size(self, estimation_pct: float) -> int: + if len(self._bs_try_history) < 2: + raise RuntimeError("At least two trials should be done without CUDA OOM to estimate batch size.") + + def distance_from_bound(val): + if val[1] < self._mem_lower_bound: + # if memory usage is same, then higher batch size is preferred + return self._mem_lower_bound - val[1] - val[0] / 10000 + elif self._mem_upper_bound < val[1]: + # if memory usage is same, then lower batch size is preferred + return val[1] - self._mem_upper_bound + val[0] / 10000 + else: + return 0 + + bs_arr = sorted([(bs, mem_usage) for bs, mem_usage in self._bs_try_history.items()], key=distance_from_bound) + bs1 = bs_arr[0][0] + bs1_mem_usage = bs_arr[0][1] + + for i in range(1, len(bs_arr)): + graident = (bs_arr[i][1] - bs1_mem_usage) / (bs_arr[i][0] - bs1) + b = bs1_mem_usage - graident * bs1 + if graident != 0: + break + + if graident == 0: # all batch size history used same GPU memory + if bs1_mem_usage < self._mem_lower_bound: + return bs1 + 2 + elif bs1_mem_usage > self._mem_upper_bound: + if bs1 <= 2: + return 2 + return bs1 - 2 + else: + return bs1 + + estimated_bs = round(((self._total_mem * estimation_pct) - b) / (graident * 2)) * 2 + + # If estimated_bs is already tried and it used GPU memory more than upper bound, + # set estimated_bs as lowest value of batch sizes using GPU memory more than uppoer bound - 2 + if estimated_bs in self._bs_try_history and self._bs_try_history[estimated_bs] > self._mem_upper_bound: + for bs, mem_usage in bs_arr: + if mem_usage > self._mem_upper_bound: + estimated_bs = bs - 2 + break + + if estimated_bs > self._max_bs: + estimated_bs = self._max_bs + + return estimated_bs diff --git a/src/otx/algorithms/common/adapters/torch/utils/utils.py b/src/otx/algorithms/common/adapters/torch/utils/utils.py new file mode 100644 index 00000000000..3bca141d23b --- /dev/null +++ b/src/otx/algorithms/common/adapters/torch/utils/utils.py @@ -0,0 +1,48 @@ +"""Collections of util functions related to torch.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import torch +from torch.nn import Module + +try: + from timm.models.layers import convert_sync_batchnorm as timm_cvt_sycnbn +except ImportError: + timm_cvt_sycnbn = None + + +def model_from_timm(model: Module) -> bool: + """Check a model comes from timm module. + + Args: + model (Module): model to check it comes from timm module. + + Returns: + bool : whether model comes from timm or not. + """ + if "timm" in model.__module__.split("."): + return True + + is_fisrt = True + for sub_module in model.modules(): + if is_fisrt: # First module is the module itself. + is_fisrt = False + continue + + if model_from_timm(sub_module): + return True + + return False + + +def convert_sync_batchnorm(model: Module): + """Convert BatchNorm layers to SyncBatchNorm layers. + + Args: + model (Module): model containing batchnorm layers. + """ + if timm_cvt_sycnbn is not None and model_from_timm(model): + timm_cvt_sycnbn(model) + else: + torch.nn.SyncBatchNorm.convert_sync_batchnorm(model) diff --git a/src/otx/algorithms/common/configs/__init__.py b/src/otx/algorithms/common/configs/__init__.py new file mode 100644 index 00000000000..fd9eb9fa2cd --- /dev/null +++ b/src/otx/algorithms/common/configs/__init__.py @@ -0,0 +1,20 @@ +"""Configs Initialization of OTX Common Algorithms.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration_enums import POTQuantizationPreset +from .training_base import BaseConfig, LearningRateSchedule, TrainType + +__all__ = ["BaseConfig", "TrainType", "LearningRateSchedule", "POTQuantizationPreset"] diff --git a/src/otx/algorithms/common/configs/configuration_enums.py b/src/otx/algorithms/common/configs/configuration_enums.py new file mode 100644 index 00000000000..dceea219f4f --- /dev/null +++ b/src/otx/algorithms/common/configs/configuration_enums.py @@ -0,0 +1,76 @@ +"""Quantization preset Enums for post training optimization.""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import re +from typing import Optional, Tuple + +from otx.api.configuration import ConfigurableEnum + + +class POTQuantizationPreset(ConfigurableEnum): + """This Enum represents the quantization preset for post training optimization.""" + + PERFORMANCE = "Performance" + MIXED = "Mixed" + + +class StorageCacheScheme(ConfigurableEnum): + """This Enum represents the storage scheme for Datumaro arrow format.""" + + NONE = "NONE" + AS_IS = "AS-IS" + JPEG_75 = "JPEG/75" + JPEG_95 = "JPEG/95" + PNG = "PNG" + TIFF = "TIFF" + + +class BatchSizeAdaptType(ConfigurableEnum): + """This Enum represents the type of adapting batch size. + + None : Not adapt batch size. + Safe : Find a batch size preventing GPU out of memory. + Full : Find a batch size using almost GPU memory. + """ + + NONE = "None" + SAFE = "Safe" + FULL = "Full" + + +class InputSizePreset(ConfigurableEnum): + """Configurable input size preset.""" + + DEFAULT = "Default" + AUTO = "Auto" + _64x64 = "64x64" + _128x128 = "128x128" + _224x224 = "224x224" + _256x256 = "256x256" + _384x384 = "384x384" + _512x512 = "512x512" + _768x768 = "768x768" + _1024x1024 = "1024x1024" + + @staticmethod + def parse(value: str) -> Optional[Tuple[int, int]]: + """Parse string value to tuple.""" + if value == "Default": + return None + if value == "Auto": + return (0, 0) + parsed_tocken = re.match("(\\d+)x(\\d+)", value) + if parsed_tocken is None: + return None + return (int(parsed_tocken.group(1)), int(parsed_tocken.group(2))) + + @property + def tuple(self) -> Optional[Tuple[int, int]]: + """Returns parsed tuple.""" + return InputSizePreset.parse(self.value) + + @classmethod + def input_sizes(cls): + """Returns list of actual size tuples.""" + return [e.tuple for e in cls if e.value[0].isdigit()] diff --git a/src/otx/algorithms/common/configs/training_base.py b/src/otx/algorithms/common/configs/training_base.py new file mode 100644 index 00000000000..d4d2b964dd4 --- /dev/null +++ b/src/otx/algorithms/common/configs/training_base.py @@ -0,0 +1,443 @@ +"""Base Configuration of OTX Common Algorithms.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from sys import maxsize + +from attr import attrs + +from otx.api.configuration import ConfigurableEnum, ConfigurableParameters +from otx.api.configuration.elements import ( + ParameterGroup, + add_parameter_group, + configurable_boolean, + configurable_float, + configurable_integer, + selectable, + string_attribute, +) +from otx.api.configuration.model_lifecycle import ModelLifecycle + +from .configuration_enums import BatchSizeAdaptType, InputSizePreset, POTQuantizationPreset, StorageCacheScheme + +# pylint: disable=invalid-name + + +class TrainType(ConfigurableEnum): + """TrainType for OTX Algorithms.""" + + Finetune = "Finetune" + Semisupervised = "Semisupervised" + Selfsupervised = "Selfsupervised" + Incremental = "Incremental" + Zeroshot = "Zeroshot" + Futurework = "Futurework" + + +class LearningRateSchedule(ConfigurableEnum): + """LearningRateSchedule for OTX Algorithms.""" + + FIXED = "fixed" + EXPONENTIAL = "exponential" + COSINE = "cosine" + STEP_WISE = "step_wise" + CYCLIC = "cyclic" + CUSTOM = "custom" + + +@attrs +class BaseConfig(ConfigurableParameters): + """BaseConfig Class for OTX Algorithms.""" + + @attrs + class BaseLearningParameters(ParameterGroup): + """BaseLearningParameters for OTX Algorithms.""" + + batch_size = configurable_integer( + default_value=5, + min_value=1, + max_value=2048, + header="Batch size", + description="The number of training samples seen in each iteration of training. Increasing this value " + "improves training time and may make the training more stable. A larger batch size has higher " + "memory requirements.", + warning="Increasing this value may cause the system to use more memory than available, " + "potentially causing out of memory errors, please update with caution.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + inference_batch_size = configurable_integer( + default_value=1, + min_value=1, + max_value=512, + header="Inference batch size", + description="The number of samples seen in each iteration of inference. Increasing this value " + "improves inference time. A larger batch size has higher memory requirements.", + warning="Increasing this value may cause the system to use more memory than available, " + "potentially causing out of memory errors, please update with caution.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + unlabeled_batch_size = configurable_integer( + default_value=5, + min_value=1, + max_value=512, + header="Unlabeled batch size", + description="The number of unlabeled training samples seen in each iteration of semi-supervised learning." + "Increasing this value improves training time and may make the training more stable." + "A larger batch size has higher memory requirements.", + warning="Increasing this value may cause the system to use more memory than available, " + "potentially causing out of memory errors, please update with caution.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + num_iters = configurable_integer( + default_value=1, + min_value=1, + max_value=1000, + header="Number of training iterations", + description="Increasing this value causes the results to be more robust but training time will be longer.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + learning_rate = configurable_float( + default_value=0.01, + min_value=1e-07, + max_value=1.0, + header="Learning rate", + description="Increasing this value will speed up training convergence but might make it unstable.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + learning_rate_warmup_iters = configurable_integer( + default_value=100, + min_value=0, + max_value=10000, + header="Number of iterations for learning rate warmup", + description="", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + use_adaptive_interval = configurable_boolean( + default_value=False, + header="Use adaptive validation interval", + description="Depending on the size of iteration per epoch, \ + adaptively update the validation interval and related values.", + warning="This will automatically control the patience and interval when early stopping is enabled.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + enable_early_stopping = configurable_boolean( + default_value=True, + header="Enable early stopping of the training", + description="Early exit from training when validation accuracy isn't \ + changed or decreased for several epochs.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + early_stop_start = configurable_integer( + default_value=3, + min_value=0, + max_value=1000, + header="Start epoch for early stopping", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + early_stop_patience = configurable_integer( + default_value=5, + min_value=0, + max_value=50, + header="Patience for early stopping", + description="Training will stop if the model does not improve within the number of epochs of patience.", + warning="This is applied exclusively when early stopping is enabled.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + early_stop_iteration_patience = configurable_integer( + default_value=0, + min_value=0, + max_value=1000, + header="Iteration patience for early stopping", + description="Training will stop if the model does not improve within the number of iterations of patience. \ + the model is trained enough with the number of iterations of patience before early stopping.", + warning="This is applied exclusively when early stopping is enabled.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + num_workers = configurable_integer( + default_value=0, + min_value=0, + max_value=8, + header="Number of cpu threads to use during batch generation", + description="Increasing this value might improve training speed however it might cause out of memory " + "errors. If the number of workers is set to zero, data loading will happen in the main " + "training thread.", + affects_outcome_of=ModelLifecycle.NONE, + ) + + enable_supcon = configurable_boolean( + default_value=False, + header="Enable Supervised Contrastive helper loss", + description="This auxiliar loss might increase robustness and accuracy for small datasets", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + auto_adapt_batch_size = selectable( + default_value=BatchSizeAdaptType.NONE, + header="Adapt batch size according to current GPU memory.", + description="Safe => Prevent GPU out of memory. Full => Find a batch size using most of GPU memory.", + warning="Enabling this could change the actual batch size depending on the current GPU status. " + "The learning rate also could be adjusted according to the adapted batch size. This process " + "might change a model performance and take some extra computation time to try a few batch size candidates.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + auto_num_workers = configurable_boolean( + default_value=False, + header="Enable auto adaptive num_workers", + description="Adapt num_workers according to current hardware status automatically.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + input_size = selectable( + default_value=InputSizePreset.DEFAULT, + header="Configure model input size.", + description="The input size of the given model could be configured to one of the predefined resolutions." + "Reduced training and inference time could be expected by using smaller input size." + "Defaults to per-model default resolution.", + warning="Modifying input size may decrease model performance.", + affects_outcome_of=ModelLifecycle.NONE, + visible_in_ui=False, + ) + + @attrs + class BasePostprocessing(ParameterGroup): + """BasePostprocessing for OTX Algorithms.""" + + result_based_confidence_threshold = configurable_boolean( + default_value=True, + header="Result based confidence threshold", + description="Confidence threshold is derived from the results", + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + confidence_threshold = configurable_float( + default_value=0.35, + min_value=0, + max_value=1, + header="Confidence threshold", + description="This threshold only takes effect if the threshold is not set based on the result.", + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + max_num_detections = configurable_integer( + header="Maximum number of detection per image", + description="Extra detection outputs will be discared in non-maximum suppression process. " + "Defaults to 0, which means per-model default value.", + default_value=0, + min_value=0, + max_value=10000, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + use_ellipse_shapes = configurable_boolean( + default_value=False, + header="Use ellipse shapes", + description="Use direct ellipse shape in inference instead of polygon from mask", + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + @attrs + class BaseNNCFOptimization(ParameterGroup): + """BaseNNCFOptimization for OTX Algorithms.""" + + enable_quantization = configurable_boolean( + default_value=True, + header="Enable quantization algorithm", + description="Enable quantization algorithm", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + enable_pruning = configurable_boolean( + default_value=False, + header="Enable filter pruning algorithm", + description="Enable filter pruning algorithm", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + pruning_supported = configurable_boolean( + default_value=False, + header="Whether filter pruning is supported", + description="Whether filter pruning is supported", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + maximal_accuracy_degradation = configurable_float( + default_value=1.0, + min_value=0.0, + max_value=100.0, + header="Maximum accuracy degradation", + description="The maximal allowed accuracy metric drop", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + @attrs + class BasePOTParameter(ParameterGroup): + """BasePOTParameter for OTX Algorithms.""" + + stat_subset_size = configurable_integer( + header="Number of data samples", + description="Number of data samples used for post-training optimization", + default_value=300, + min_value=1, + max_value=1000, + ) + + stat_requests_number = configurable_integer( + header="Number of requests", + description="Number of requests during statistics collection", + default_value=0, + min_value=0, + max_value=200, + ) + + preset = selectable( + default_value=POTQuantizationPreset.PERFORMANCE, + header="Preset", + description="Quantization preset that defines quantization scheme", + editable=True, + visible_in_ui=True, + ) + + @attrs + class BaseAlgoBackendParameters(ParameterGroup): + """BaseAlgoBackendParameters for OTX Algorithms.""" + + train_type = selectable( + default_value=TrainType.Incremental, + header="train type", + description="Training scheme option that determines how to train the model", + editable=False, + visible_in_ui=False, + ) + + mem_cache_size = configurable_integer( + header="Size of memory pool for caching decoded data to load data faster", + description="Size of memory pool for caching decoded data to load data faster", + default_value=0, + min_value=0, + max_value=maxsize, + visible_in_ui=False, + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + storage_cache_scheme = selectable( + default_value=StorageCacheScheme.NONE, + header="Scheme for storage cache", + description="Scheme for storage cache", + editable=False, + visible_in_ui=False, + ) + + @attrs + class BaseTilingParameters(ParameterGroup): + """BaseTilingParameters for OTX Algorithms.""" + + header = string_attribute("Tiling Parameters") + enable_tiling = configurable_boolean( + default_value=False, + header="Enable tiling", + description="Set to True to allow tiny objects to be better detected.", + warning="Tiling trades off speed for accuracy as it increases the number of images to be processed. " + "Important: In the current version, depending on the dataset size and the available hardware resources, " + "a model may not train successfully when tiling is enabled.", + affects_outcome_of=ModelLifecycle.NONE, + ) + + enable_tile_classifier = configurable_boolean( + default_value=False, + header="Enable tile classifier", + description="Enabling tile classifier enhances the speed of tiling inference by incorporating a tile " + "classifier into the instance segmentation model. This feature prevents the detector from " + "making predictions on tiles that do not contain any objects, thus optimizing its " + "speed performance.", + warning="The tile classifier prioritizes inference speed over training speed, it requires more training " + "in order to achieve its optimized performance.", + affects_outcome_of=ModelLifecycle.NONE, + ) + + enable_adaptive_params = configurable_boolean( + default_value=True, + header="Enable adaptive tiling parameters", + description="Config tile size and tile overlap adaptively based on annotated dataset statistic", + warning="", + affects_outcome_of=ModelLifecycle.NONE, + ) + + tile_size = configurable_integer( + header="Tile Image Size", + description="Tile Image Size", + default_value=400, + min_value=100, + max_value=4096, + affects_outcome_of=ModelLifecycle.NONE, + ) + + tile_overlap = configurable_float( + header="Tile Overlap", + description="Overlap between each two neighboring tiles.", + default_value=0.2, + min_value=0.0, + max_value=0.9, + affects_outcome_of=ModelLifecycle.NONE, + ) + + tile_max_number = configurable_integer( + header="Max object per image", + description="Max object per image", + default_value=1500, + min_value=1, + max_value=5000, + affects_outcome_of=ModelLifecycle.NONE, + ) + + tile_ir_scale_factor = configurable_float( + header="OpenVINO IR Scale Factor", + description="The purpose of the scale parameter is to optimize the performance and " + "efficiency of tiling in OpenVINO IR during inference. By controlling the increase in tile size and " + "input size, the scale parameter allows for more efficient parallelization of the workload and " + "improve the overall performance and efficiency of the inference process on OpenVINO.", + warning="Setting the scale factor value too high may cause the application " + "to crash or result in out-of-memory errors. It is recommended to " + "adjust the scale factor value carefully based on the available " + "hardware resources and the needs of the application.", + default_value=1.0, + min_value=1.0, + max_value=4.0, + affects_outcome_of=ModelLifecycle.NONE, + ) + + tile_sampling_ratio = configurable_float( + header="Sampling Ratio for entire tiling", + description="Since tiling train and validation to all tile from large image, " + "usually it takes lots of time than normal training." + "The tile_sampling_ratio is ratio for sampling entire tile dataset." + "Sampling tile dataset would save lots of time for training and validation time." + "Note that sampling will be applied to training and validation dataset, not test dataset.", + default_value=1.0, + min_value=0.000001, + max_value=1.0, + affects_outcome_of=ModelLifecycle.NONE, + ) + + object_tile_ratio = configurable_float( + header="Object tile ratio", + description="The desired ratio of min object size and tile size.", + default_value=0.03, + min_value=0.00, + max_value=1.00, + affects_outcome_of=ModelLifecycle.NONE, + ) + + tiling_parameters = add_parameter_group(BaseTilingParameters) diff --git a/src/otx/algorithms/common/tasks/__init__.py b/src/otx/algorithms/common/tasks/__init__.py new file mode 100644 index 00000000000..e0dc4c6af19 --- /dev/null +++ b/src/otx/algorithms/common/tasks/__init__.py @@ -0,0 +1,15 @@ +"""Task Initialization of OTX Common Algorithms.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/common/tasks/base_task.py b/src/otx/algorithms/common/tasks/base_task.py new file mode 100644 index 00000000000..93f0435240c --- /dev/null +++ b/src/otx/algorithms/common/tasks/base_task.py @@ -0,0 +1,356 @@ +"""Base task of OTX.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import io +import logging +import os +import shutil +import tempfile +from abc import ABC, abstractmethod +from datetime import timedelta +from typing import Any, Dict, Iterable, List, Optional + +import torch +from torch import distributed as dist + +from otx.algorithms.common.adapters.mmcv.hooks import OTXLoggerHook +from otx.algorithms.common.adapters.mmcv.hooks.cancel_hook import CancelInterfaceHook +from otx.algorithms.common.configs.training_base import TrainType +from otx.algorithms.common.utils import UncopiableDefaultDict, append_dist_rank_suffix, set_random_seed +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import LabelEntity +from otx.api.entities.metrics import MetricsGroup +from otx.api.entities.model import ModelEntity, ModelFormat, ModelOptimizationType, ModelPrecision, OptimizationMethod +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.train_parameters import TrainParameters +from otx.api.serialization.label_mapper import LabelSchemaMapper +from otx.api.usecases.reporting.time_monitor_callback import TimeMonitorCallback +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.export_interface import ExportType, IExportTask +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.unload_interface import IUnload +from otx.utils.logger import get_logger + +TRAIN_TYPE_DIR_PATH = { + TrainType.Incremental.name: ".", + TrainType.Selfsupervised.name: "selfsl", + TrainType.Semisupervised.name: "semisl", +} + +logger = get_logger() + + +class OnHookInitialized: + """OnHookInitialized class.""" + + def __init__(self, task_instance): + self.task_instance = task_instance + self.__findable = False # a barrier to block segmentation fault + + def __call__(self, cancel_interface): + """Function call in OnHookInitialized.""" + if isinstance(self.task_instance, int) and self.__findable: + import ctypes + + # NOTE: BE AWARE OF SEGMENTATION FAULT + self.task_instance = ctypes.cast(self.task_instance, ctypes.py_object).value + self.task_instance.cancel_hook_initialized(cancel_interface) + + def __repr__(self): + """Function repr in OnHookInitialized.""" + return f"'{__name__}.OnHookInitialized'" + + def __deepcopy__(self, memo): + """Function deepcopy in OnHookInitialized.""" + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + result.task_instance = self.task_instance + result.__findable = True # pylint: disable=unused-private-member, protected-access + return result + + def __reduce__(self): + """Function reduce in OnHookInitialized.""" + return (self.__class__, (id(self.task_instance),)) + + +# pylint: disable=too-many-instance-attributes +class OTXTask(IInferenceTask, IExportTask, IEvaluationTask, IUnload, ABC): + """Base task of OTX.""" + + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + self._config: Dict[Any, Any] = {} + self._task_environment = task_environment + self._task_type = task_environment.model_template.task_type + self._labels = task_environment.get_labels(include_empty=False) + self._work_dir_is_temp = False + self._output_path = output_path + self._output_path = output_path if output_path is not None else self._get_tmp_dir() + self._time_monitor: Optional[TimeMonitorCallback] = None + self.on_hook_initialized = OnHookInitialized(self) + self._learning_curves = UncopiableDefaultDict(OTXLoggerHook.Curve) + self._model_label_schema: List[LabelEntity] = [] + self._resume = False + self._should_stop: bool = False + self.cancel_interface: Optional[CancelInterfaceHook] = None + self.reserved_cancel = False + self._model_ckpt = None + self._precision = [ModelPrecision.FP32] + self._optimization_methods: List[OptimizationMethod] = [] + self._is_training = False + self.seed: Optional[int] = None + self.deterministic: bool = False + + self.override_configs: Dict[str, str] = {} + + # This is for hpo, and this should be removed + self.project_path = self._output_path + + if self._is_distributed_training(): + self._setup_distributed_training() + + @staticmethod + def _is_distributed_training(): + multi_gpu_env = ["MASTER_ADDR", "MASTER_PORT", "LOCAL_WORLD_SIZE", "WORLD_SIZE", "LOCAL_RANK", "RANK"] + for env in multi_gpu_env: + if env not in os.environ: + return False + + return torch.cuda.is_available() + + @staticmethod + def _setup_distributed_training(): + if not dist.is_initialized(): + torch.cuda.set_device(int(os.environ["LOCAL_RANK"])) + dist.init_process_group( + backend="nccl", + init_method="env://", + timeout=timedelta(seconds=int(os.environ.get("TORCH_DIST_TIMEOUT", 60))), + ) + rank = dist.get_rank() + logger.info(f"Dist info: rank {rank} / {dist.get_world_size()} world_size") + if rank != 0: + logging.disable(logging.WARNING) + + def _get_tmp_dir(self): + self._work_dir_is_temp = True + # If training is excuted with torchrun, set all trainings' output directory same + if "TORCHELASTIC_RUN_ID" in os.environ: + return os.path.join(tempfile.gettempdir(), f"OTX-task-torchelastic-{os.environ['TORCHELASTIC_RUN_ID']}") + return tempfile.mkdtemp(prefix="OTX-task-") + + def _load_model(self): + """Loading model from checkpoint.""" + + def _load_model_label_schema(model: Optional[ModelEntity]): + # If a model has been trained and saved for the task already, create empty model and load weights here + if model and "label_schema.json" in model.model_adapters: + import json + + buffer = json.loads(model.get_data("label_schema.json").decode("utf-8")) + model_label_schema = LabelSchemaMapper().backward(buffer) + return model_label_schema.get_labels(include_empty=False) + return self._labels + + logger.info("loading the model from the task env.") + model = self._task_environment.model + state_dict = self._load_model_ckpt(model) + if state_dict: + self._model_ckpt = append_dist_rank_suffix(os.path.join(self._output_path, "env_model_ckpt.pth")) + if os.path.exists(self._model_ckpt): + os.remove(self._model_ckpt) + torch.save(state_dict, self._model_ckpt) + self._model_label_schema = _load_model_label_schema(model) + if model is not None: + self._resume = model.model_adapters.get("resume", False) + + def _load_model_ckpt(self, model: Optional[ModelEntity]): + if model and "weights.pth" in model.model_adapters: + # If a model has been trained and saved for the task already, create empty model and load weights here + buffer = io.BytesIO(model.get_data("weights.pth")) + model_data = torch.load(buffer, map_location=torch.device("cpu")) + return model_data + return None + + @abstractmethod + def train( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + train_parameters: Optional[TrainParameters] = None, + seed: Optional[int] = None, + deterministic: bool = False, + ): + """Train function for OTX task.""" + raise NotImplementedError + + @abstractmethod + def infer( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ) -> DatasetEntity: + """Main infer function.""" + raise NotImplementedError + + @abstractmethod + def export( + self, + export_type: ExportType, + output_model: ModelEntity, + precision: ModelPrecision = ModelPrecision.FP32, + dump_features: bool = True, + ): + """Export function of OTX Task.""" + raise NotImplementedError + + @abstractmethod + def explain( + self, + dataset: DatasetEntity, + explain_parameters: Optional[ExplainParameters] = None, + ) -> DatasetEntity: + """Main explain function of OTX Task.""" + raise NotImplementedError + + @abstractmethod + def evaluate( + self, + output_resultset: ResultSetEntity, + evaluation_metric: Optional[str] = None, + ): + """Evaluate function of OTX Task.""" + raise NotImplementedError + + @staticmethod + @abstractmethod + def _generate_training_metrics(learning_curves, scores) -> Iterable[MetricsGroup[Any, Any]]: + """Get Training metrics (epochs & scores). + + Parses the training logs to get metrics from the latest training run + :return output List[MetricsGroup] + """ + raise NotImplementedError + + @abstractmethod + def save_model(self, output_model: ModelEntity): + """Save best model weights in trining task.""" + raise NotImplementedError + + def cancel_training(self): + """Cancel training function in trining task. + + Sends a cancel training signal to gracefully stop the optimizer. The signal consists of creating a + '.stop_training' file in the current work_dir. The runner checks for this file periodically. + The stopping mechanism allows stopping after each iteration, but validation will still be carried out. Stopping + will therefore take some time. + """ + logger.info("Cancel training requested.") + self._should_stop = True + if self.cancel_interface is not None: + self.cancel_interface.cancel() + else: + logger.info("but training was not started yet. reserved it to cancel") + self.reserved_cancel = True + + def cancel_hook_initialized(self, cancel_interface: CancelInterfaceHook): + """Initialization of cancel_interface hook.""" + logger.info("cancel hook is initialized") + self.cancel_interface = cancel_interface + if self.reserved_cancel and self.cancel_interface: + self.cancel_interface.cancel() + + def cleanup(self): + """Clean up work directory if user specified it.""" + if self._work_dir_is_temp: + self._delete_scratch_space() + + def _delete_scratch_space(self): + """Remove model checkpoints and otx logs.""" + if os.path.exists(self._output_path): + shutil.rmtree(self._output_path, ignore_errors=False) + + def unload(self): + """Unload the task.""" + self.cleanup() + if self._is_docker(): + logger.warning("Got unload request. Unloading models. Throwing Segmentation Fault on purpose") + import ctypes + + ctypes.string_at(0) + else: + logger.warning("Got unload request, but not on Docker. Only clearing CUDA cache") + torch.cuda.empty_cache() + logger.warning( + f"Done unloading. " f"Torch is still occupying {torch.cuda.memory_allocated()} bytes of GPU memory" + ) + + @staticmethod + def _is_docker(): + """Checks whether the task runs in docker container. + + :return bool: True if task runs in docker + """ + path = "/proc/self/cgroup" + is_in_docker = False + if os.path.isfile(path): + with open(path, encoding="UTF-8") as f: + is_in_docker = is_in_docker or any("docker" in line for line in f) + is_in_docker = is_in_docker or os.path.exists("/.dockerenv") + return is_in_docker + + def set_seed(self): + """Set seed and deterministic.""" + if self.seed is None: + # If the seed is not present via task.train, it will be found in the recipe. + self.seed = self.config.get("seed", 5) + if not self.deterministic: + # deterministic is the same. + self.deterministic = self.config.get("deterministic", False) + self.config["seed"] = self.seed + self.config["deterministic"] = self.deterministic + set_random_seed(self.seed, logger, self.deterministic) + + @property + def config(self): + """Config of OTX task.""" + return self._config + + @config.setter + def config(self, config: Dict[Any, Any]): + self._config = config + + def _update_model_export_metadata( + self, output_model: ModelEntity, export_type: ExportType, precision: ModelPrecision, dump_features: bool + ) -> None: + """Updates a model entity with format and optimization related attributes.""" + if export_type == ExportType.ONNX: + output_model.model_format = ModelFormat.ONNX + output_model.optimization_type = ModelOptimizationType.ONNX + if precision == ModelPrecision.FP16: + raise RuntimeError("Export to FP16 ONNX is not supported") + elif export_type == ExportType.OPENVINO: + output_model.model_format = ModelFormat.OPENVINO + output_model.optimization_type = ModelOptimizationType.MO + else: + raise RuntimeError(f"not supported export type {export_type}") + + output_model.has_xai = dump_features + output_model.optimization_methods = self._optimization_methods + output_model.precision = [precision] diff --git a/src/otx/algorithms/common/tasks/nncf_task.py b/src/otx/algorithms/common/tasks/nncf_task.py new file mode 100644 index 00000000000..30d11750b04 --- /dev/null +++ b/src/otx/algorithms/common/tasks/nncf_task.py @@ -0,0 +1,350 @@ +"""BaseTask for NNCF.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +import io +import json +import os +from copy import deepcopy +from typing import Dict, List, Optional + +import torch +from mmcv.utils import ConfigDict + +import otx.algorithms.common.adapters.mmcv.nncf.patches # noqa: F401 # pylint: disable=unused-import +from otx.algorithms.common.adapters.mmcv.utils import ( + get_configs_by_keys, + remove_from_config, + remove_from_configs_by_type, +) +from otx.algorithms.common.adapters.nncf import ( + check_nncf_is_enabled, + is_accuracy_aware_training_set, +) +from otx.algorithms.common.adapters.nncf.config import compose_nncf_config +from otx.algorithms.common.utils.callback import OptimizationProgressCallback +from otx.algorithms.common.utils.data import get_dataset +from otx.api.configuration import cfg_helper +from otx.api.configuration.helper.utils import ids_to_strings +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model import ( + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.optimization_parameters import ( + OptimizationParameters, + default_progress_callback, +) +from otx.api.entities.subset import Subset +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.tasks.interfaces.optimization_interface import ( + IOptimizationTask, + OptimizationType, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +class NNCFBaseTask(IOptimizationTask): # pylint: disable=too-many-instance-attributes + """NNCFBaseTask.""" + + def __init__(self): + check_nncf_is_enabled() + self._nncf_data_to_build = None + self._nncf_state_dict_to_build: Dict[str, torch.Tensor] = {} + self._nncf_preset = None + self._optimization_methods: List[OptimizationMethod] = [] + self._precision = [ModelPrecision.FP32] + + # Extra control variables. + self._training_work_dir = None + self._is_training = False + self._should_stop = False + self._optimization_type = ModelOptimizationType.NNCF + self._time_monitor = None + + # Variables will be set in training backend task + self._data_cfg = None + self._model_ckpt = None + self._model_dir = None + self._labels = None + self._recipe_cfg = None + self._hyperparams = None + self._task_environment = None + + logger.info("Task initialization completed") + + def _set_attributes_by_hyperparams(self): + quantization = self._hyperparams.nncf_optimization.enable_quantization + pruning = self._hyperparams.nncf_optimization.enable_pruning + if quantization and pruning: + self._nncf_preset = "nncf_quantization_pruning" + self._optimization_methods = [ + OptimizationMethod.QUANTIZATION, + OptimizationMethod.FILTER_PRUNING, + ] + self._precision = [ModelPrecision.INT8] + return + if quantization and not pruning: + self._nncf_preset = "nncf_quantization" + self._optimization_methods = [OptimizationMethod.QUANTIZATION] + self._precision = [ModelPrecision.INT8] + return + if not quantization and pruning: + self._nncf_preset = "nncf_pruning" + self._optimization_methods = [OptimizationMethod.FILTER_PRUNING] + self._precision = [ModelPrecision.FP32] + return + # FIXEME: Error rasing should be re-enabled after Geti issue resolved + # raise RuntimeError("Not selected optimization algorithm") + logger.warning("Not selected optimization algorithm. Defaults to quantization") + self._nncf_preset = "nncf_quantization" + self._optimization_methods = [OptimizationMethod.QUANTIZATION] + self._precision = [ModelPrecision.INT8] + + def _init_train_data_cfg(self, dataset: DatasetEntity): + logger.info("init data cfg.") + data_cfg = ConfigDict(data=ConfigDict()) + + for cfg_key, subset in zip( + ["train", "val"], + [Subset.TRAINING, Subset.VALIDATION], + ): + subset = get_dataset(dataset, subset) + if subset: + data_cfg.data[cfg_key] = ConfigDict( + otx_dataset=subset, + labels=self._labels, + ) + + return data_cfg + + def _init_nncf_cfg(self): + nncf_config_path = os.path.join(self._model_dir, "compression_config.json") + + with open(nncf_config_path, encoding="UTF-8") as nncf_config_file: + common_nncf_config = json.load(nncf_config_file) + + optimization_config = compose_nncf_config(common_nncf_config, [self._nncf_preset]) + + max_acc_drop = self._hyperparams.nncf_optimization.maximal_accuracy_degradation / 100 + if "accuracy_aware_training" in optimization_config["nncf_config"]: + # Update maximal_absolute_accuracy_degradation + ( + optimization_config["nncf_config"]["accuracy_aware_training"]["params"][ + "maximal_absolute_accuracy_degradation" + ] + ) = max_acc_drop + # Force evaluation interval + self._config.evaluation.interval = 1 + else: + logger.info("NNCF config has no accuracy_aware_training parameters") + + return ConfigDict(optimization_config) + + def _prepare_optimize(self): + assert self._config is not None + + # TODO: more delicate configuration change control in OTX side + + # last batch size of 1 causes undefined behaviour for batch normalization + # when initializing and training NNCF + if self._data_cfg is not None: + data_loader = self._config.data.get("train_dataloader", ConfigDict()) + samples_per_gpu = data_loader.get("samples_per_gpu", self._config.data.get("samples_per_gpu")) + otx_dataset = get_configs_by_keys(self._data_cfg.data.train, "otx_dataset") + assert len(otx_dataset) == 1 + otx_dataset = otx_dataset[0] + if otx_dataset is not None and len(otx_dataset) % samples_per_gpu == 1: + data_loader["drop_last"] = True + self._config.data["train_dataloader"] = data_loader + + # nncf does not suppoer FP16 + if "fp16" in self._config: + remove_from_config(self._config, "fp16") + logger.warning("fp16 option is not supported in NNCF. Switch to fp32.") + + # FIXME: nncf quantizer does not work with SAMoptimizer + optimizer_config = self._config.optimizer_config + if optimizer_config.get("type", "OptimizerHook") == "SAMOptimizerHook": + optimizer_config.type = "OptimizerHook" + logger.warning("Updateed SAMOptimizerHook to OptimizerHook as not supported.") + + # merge nncf_cfg + nncf_cfg = self._init_nncf_cfg() + self._config.merge_from_dict(nncf_cfg) + + # configure nncf + nncf_config = self._config.get("nncf_config", {}) + if nncf_config.get("target_metric_name", None) is None: + metric_name = self._config.evaluation.metric + if isinstance(metric_name, list): + metric_name = metric_name[0] + nncf_config.target_metric_name = metric_name + logger.info(f"'target_metric_name' not found in nncf config. Using {metric_name} as target metric") + + if is_accuracy_aware_training_set(nncf_config): + # Prepare runner for Accuracy Aware + self._config.runner = { + "type": "AccuracyAwareRunner", + "nncf_config": nncf_config, + } + + # AccuracyAwareRunner needs to evaluate a model when it needs + # unlike other runners counting on periodically evaluated score by 'EvalHook'. + # To configure 'interval' to 'max_epoch' makes sure 'EvalHook' not to evaluate + # during training. + max_epoch = nncf_config.accuracy_aware_training.params.maximal_total_epochs + self._config.evaluation.interval = max_epoch + # Disable 'AdaptiveTrainSchedulingHook' as training is managed by AccuracyAwareRunner + remove_from_configs_by_type(self._config.custom_hooks, "AdaptiveTrainSchedulingHook") + + @staticmethod + def model_builder( + config, + *args, + nncf_model_builder, + model_config=None, + data_config=None, + is_export=False, + return_compression_ctrl=False, + **kwargs, + ): + """model_builder.""" + + if model_config is not None or data_config is not None: + config = deepcopy(config) + if model_config is not None: + config.merge_from_dict(model_config) + if data_config is not None: + config.merge_from_dict(data_config) + + compression_ctrl, model, = nncf_model_builder( + config, + distributed=False, + *args, + **kwargs, + ) + + if is_export: + compression_ctrl.prepare_for_export() + model.nncf.disable_dynamic_graph_building() + + if return_compression_ctrl: + return compression_ctrl, model + return model + + def _optimize( + self, + dataset: DatasetEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + ): + raise NotImplementedError + + def _optimize_post_hook( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + ): + pass + + def optimize( + self, + optimization_type: OptimizationType, + dataset: DatasetEntity, + output_model: ModelEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + ): + """NNCF Optimization.""" + if optimization_type is not OptimizationType.NNCF: + raise RuntimeError("NNCF is the only supported optimization") + + if optimization_parameters is not None: + update_progress_callback = optimization_parameters.update_progress + else: + update_progress_callback = default_progress_callback + + self._time_monitor = OptimizationProgressCallback( + update_progress_callback, + loading_stage_progress_percentage=5, + initialization_stage_progress_percentage=5, + ) + + self._data_cfg = self._init_train_data_cfg(dataset) + self._is_training = True + + results = self._optimize(dataset, optimization_parameters) + + # Check for stop signal when training has stopped. + # If should_stop is true, training was cancelled + if self._should_stop: + logger.info("Training cancelled.") + self._should_stop = False + self._is_training = False + return + + model_ckpt = results.get("final_ckpt") + if model_ckpt is None: + logger.error("cannot find final checkpoint from the results.") + # output_model.model_status = ModelStatus.FAILED + return + # update checkpoint to the newly trained model + self._model_ckpt = model_ckpt + + self._optimize_post_hook(dataset, output_model) + + self.save_model(output_model) + + output_model.model_format = ModelFormat.BASE_FRAMEWORK + output_model.optimization_type = self._optimization_type + output_model.optimization_methods = self._optimization_methods + output_model.precision = self._precision + + self._is_training = False + + def _save_model_post_hook(self, modelinfo): + pass + + def save_model(self, output_model: ModelEntity): + """Saving model function for NNCF Task.""" + assert self._recipe_cfg is not None + + buffer = io.BytesIO() + hyperparams_str = ids_to_strings(cfg_helper.convert(self._hyperparams, dict, enum_to_str=True)) + labels = {label.name: label.color.rgb_tuple for label in self._labels} + + model_ckpt = torch.load(self._model_ckpt, map_location=torch.device("cpu")) + modelinfo = { + "model": model_ckpt, + "config": hyperparams_str, + "labels": labels, + "VERSION": 1, + "meta": { + "nncf_enable_compression": True, + }, + } + self._save_model_post_hook(modelinfo) + + torch.save(modelinfo, buffer) + output_model.set_data("weights.pth", buffer.getvalue()) + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self._task_environment.label_schema), + ) diff --git a/src/otx/algorithms/common/tools/__init__.py b/src/otx/algorithms/common/tools/__init__.py new file mode 100644 index 00000000000..1a7b41db1f0 --- /dev/null +++ b/src/otx/algorithms/common/tools/__init__.py @@ -0,0 +1,15 @@ +"""Collection of tools to run common OTX algorithms.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/common/utils/__init__.py b/src/otx/algorithms/common/utils/__init__.py new file mode 100644 index 00000000000..2a6d2475724 --- /dev/null +++ b/src/otx/algorithms/common/utils/__init__.py @@ -0,0 +1,79 @@ +"""Collection of utils to run common OTX algorithms.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import os + +from .callback import ( + InferenceProgressCallback, + OptimizationProgressCallback, + TrainingProgressCallback, +) +from .data import OTXOpenVinoDataLoader, get_cls_img_indices, get_image, get_old_new_img_indices +from .dist_utils import append_dist_rank_suffix +from .ir import embed_ir_model_data +from .utils import ( + UncopiableDefaultDict, + cast_bf16_to_fp32, + get_arg_spec, + get_cfg_based_on_device, + get_default_async_reqs_num, + get_task_class, + is_hpu_available, + is_xpu_available, + load_template, + read_py_config, + set_random_seed, +) + +__all__ = [ + "embed_ir_model_data", + "get_cls_img_indices", + "get_old_new_img_indices", + "TrainingProgressCallback", + "InferenceProgressCallback", + "OptimizationProgressCallback", + "UncopiableDefaultDict", + "load_template", + "get_task_class", + "get_arg_spec", + "get_image", + "set_random_seed", + "append_dist_rank_suffix", + "OTXOpenVinoDataLoader", + "read_py_config", + "get_default_async_reqs_num", + "is_xpu_available", + "is_hpu_available", + "cast_bf16_to_fp32", + "get_cfg_based_on_device", +] + + +if is_hpu_available(): + os.environ["PT_HPU_LAZY_MODE"] = "1" + import habana_frameworks.torch.gpu_migration # noqa: F401 + + +if is_xpu_available(): + try: + import mmcv + + from otx.algorithms.common.adapters.mmcv.utils.fp16_utils import custom_auto_fp16, custom_force_fp32 + + mmcv.runner.auto_fp16 = custom_auto_fp16 + mmcv.runner.force_fp32 = custom_force_fp32 + except ImportError: + pass diff --git a/src/otx/algorithms/common/utils/callback.py b/src/otx/algorithms/common/utils/callback.py new file mode 100644 index 00000000000..df72998d484 --- /dev/null +++ b/src/otx/algorithms/common/utils/callback.py @@ -0,0 +1,111 @@ +"""Collection of callback utils to run common OTX algorithms.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import time + +from otx.api.usecases.reporting.time_monitor_callback import TimeMonitorCallback + + +class TrainingProgressCallback(TimeMonitorCallback): + """TrainingProgressCallback class for time monitoring.""" + + def __init__(self, update_progress_callback, **kwargs): + super().__init__(update_progress_callback=update_progress_callback, **kwargs) + + def on_train_batch_end(self, batch, logs=None): + """Callback function on training batch ended.""" + super().on_train_batch_end(batch, logs) + self.update_progress_callback(self.get_progress()) + + def on_epoch_end(self, epoch, logs=None): + """Callback function on epoch ended.""" + self.past_epoch_duration.append(time.time() - self.start_epoch_time) + progress = ((epoch + 1) / self.total_epochs) * 100 + self._calculate_average_epoch() + score = None + if hasattr(self.update_progress_callback, "metric") and isinstance(logs, dict): + score = logs.get(self.update_progress_callback.metric, None) + self.update_progress_callback(progress, score=score) + + +class InferenceProgressCallback(TimeMonitorCallback): + """InferenceProgressCallback class for time monitoring.""" + + def __init__(self, num_test_steps, update_progress_callback, **kwargs): + super().__init__( + num_epoch=0, + num_train_steps=0, + num_val_steps=0, + num_test_steps=num_test_steps, + update_progress_callback=update_progress_callback, + **kwargs, + ) + + def on_test_batch_end(self, batch=None, logs=None): + """Callback function on testing batch ended.""" + super().on_test_batch_end(batch, logs) + self.update_progress_callback(int(self.get_progress())) + + +class OptimizationProgressCallback(TrainingProgressCallback): + """Progress callback used for optimization using NNCF. + + There are three stages to the progress bar: + - 5 % model is loaded + - 10 % compressed model is initialized + - 10-100 % compressed model is being fine-tuned + """ + + def __init__( + self, + update_progress_callback, + loading_stage_progress_percentage: int = 5, + initialization_stage_progress_percentage: int = 5, + **kwargs, + ): + super().__init__(update_progress_callback=update_progress_callback, **kwargs) + if loading_stage_progress_percentage + initialization_stage_progress_percentage >= 100: + raise RuntimeError("Total optimization progress percentage is more than 100%") + + self.loading_stage_progress_percentage = loading_stage_progress_percentage + self.initialization_stage_progress_percentage = initialization_stage_progress_percentage + + # set loading_stage_progress_percentage from the start as the model is already loaded at this point + if self.update_progress_callback: + self.update_progress_callback(loading_stage_progress_percentage) + + def on_train_begin(self, logs=None): + """Callback function when training beginning.""" + super().on_train_begin(logs) + # Callback initialization takes place here after OTXProgressHook.before_run() is called + train_percentage = 100 - self.loading_stage_progress_percentage - self.initialization_stage_progress_percentage + loading_stage_steps = self.total_steps * self.loading_stage_progress_percentage / train_percentage + initialization_stage_steps = self.total_steps * self.initialization_stage_progress_percentage / train_percentage + self.total_steps += loading_stage_steps + initialization_stage_steps + + self.current_step = loading_stage_steps + initialization_stage_steps + self.update_progress_callback(self.get_progress()) + + def on_train_end(self, logs=None): + """Callback function on training ended.""" + super().on_train_end(logs) + self.update_progress_callback(self.get_progress(), score=logs) + + def on_initialization_end(self): + """on_initialization_end callback for optimization using NNCF.""" + self.update_progress_callback( + self.loading_stage_progress_percentage + self.initialization_stage_progress_percentage + ) diff --git a/src/otx/algorithms/common/utils/data.py b/src/otx/algorithms/common/utils/data.py new file mode 100644 index 00000000000..0297700045c --- /dev/null +++ b/src/otx/algorithms/common/utils/data.py @@ -0,0 +1,332 @@ +"""Collections of Dataset utils for common OTX algorithms.""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +import glob +import os +import random +from typing import Any, Dict, List, Optional, Union + +import cv2 +import numpy as np + +from otx.api.entities.annotation import NullAnnotationSceneEntity +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.subset import Subset +from otx.api.utils.argument_checks import IMAGE_FILE_EXTENSIONS +from otx.utils.logger import get_logger + +logger = get_logger() + + +def get_unlabeled_filename(base_root: str, file_list_path: str): + """This method checks and gets image file paths, which are listed in file_list_path. + + The content of file_list_path is expected to specify relative paths of each image file to base_root line by line. + It returns the list of image filenames only which will compose unlabeled dataset. + + Args: + base_root (str): path of base root dir where unlabeled images are. + file_list_path (str) : path of file which contains relative paths of unlabeled data to base_root. + + Returns: + List[str]: a list of existing image file paths which will be unlabeled data items. + """ + + def is_valid(file_path): + return file_path.lower().endswith(tuple(IMAGE_FILE_EXTENSIONS)) + + with open(file_list_path, "r", encoding="UTF-8") as f: + file_names = f.read().splitlines() + unlabeled_files = [] + for fn in file_names: + file_path = os.path.join(base_root, fn.strip()) + if is_valid(file_path) and os.path.isfile(file_path): + unlabeled_files.append(file_path) + return unlabeled_files + + +def load_unlabeled_dataset_items( + data_root_dir: str, + file_list_path: Optional[str] = None, +): + """This method loads unlabeled dataset items from images in data_root_dir. + + Args: + data_root_dir (str): path of base root directory where unlabeled images are. + file_list_path (str) : path of a file which contains relative paths of unlabeled data to base_root. + subset (Subset) : Entity subset category + Returns: + List[DatasetItemEntity]: a list of unlabeled dataset item entity. + """ + if file_list_path is not None: + data_list = get_unlabeled_filename(data_root_dir, file_list_path) + + else: + data_list = [] + + for ext in IMAGE_FILE_EXTENSIONS: + data_list.extend(glob.glob(f"{data_root_dir}/**/*{ext}", recursive=True)) + + dataset_items = [] + + for filename in data_list: + dataset_item = DatasetItemEntity( + media=Image(file_path=filename), + annotation_scene=NullAnnotationSceneEntity(), + subset=Subset.UNLABELED, + ) + dataset_items.append(dataset_item) + return dataset_items + + +def get_dataset(dataset: DatasetEntity, subset: Subset): + """Get dataset from datasetentity.""" + data = dataset.get_subset(subset) + return data if len(data) > 0 else None + + +def get_cls_img_indices(labels, dataset): + """Function for getting image indices per class. + + Args: + labels (List[LabelEntity]): List of labels + dataset(DatasetEntity): dataset entity + """ + img_indices = {label.name: [] for label in labels} + for i, item in enumerate(dataset): + item_labels = item.annotation_scene.get_labels() + for i_l in item_labels: + if i_l in labels: + img_indices[i_l.name].append(i) + + return img_indices + + +def get_old_new_img_indices(labels, new_classes, dataset): + """Function for getting old & new indices of dataset. + + Args: + labels (List[LabelEntity]): List of labels + new_classes(List[str]): List of new classes + dataset(DatasetEntity): dataset entity + """ + ids_old, ids_new = [], [] + _dataset_label_schema_map = {label.name: label for label in labels} + new_classes = [_dataset_label_schema_map[new_class] for new_class in new_classes] + for i, item in enumerate(dataset): + if item.annotation_scene.contains_any(new_classes): + ids_new.append(i) + else: + ids_old.append(i) + return {"old": ids_old, "new": ids_new} + + +def get_image(results: Dict[str, Any], cache_dir: str, to_float32=False) -> np.ndarray: + """Load an image and cache it if it's a training video frame. + + Args: + results (Dict[str, Any]): A dictionary that contains information about the dataset item. + cache_dir (str): A directory path where the cached images will be stored. + to_float32 (bool, optional): A flag indicating whether to convert the image to float32. Defaults to False. + + Returns: + np.ndarray: The loaded image. + """ + + def is_training_video_frame(subset, media) -> bool: + return subset.name in ["TRAINING", "VALIDATION"] and "VideoFrame" in repr(media) + + def load_image_from_cache(filename: str, to_float32=False) -> Union[np.ndarray, None]: + try: + cached_img = cv2.imread(filename) + if to_float32: + cached_img = cached_img.astype(np.float32) + return cached_img + except Exception as e: # pylint: disable=broad-except + logger.warning(f"Skip loading cached {filename} \nError msg: {e}") + return None + + def save_image_to_cache(img: np.array, filename: str): + tmp_filename = filename.replace(".png", "-tmp.png") + if os.path.exists(filename) or os.path.exists(tmp_filename): # if image is cached or caching + return + try: + cv2.imwrite(tmp_filename, img=img) + except Exception as e: # pylint: disable=broad-except + logger.warning(f"Skip caching for {filename} \nError msg: {e}") + return + + if os.path.exists(tmp_filename) and not os.path.exists(filename): + try: + os.replace(tmp_filename, filename) + except Exception as e: # pylint: disable=broad-except + os.remove(tmp_filename) + logger.warning(f"Failed to rename {tmp_filename} -> {filename} \nError msg: {e}") + + subset = results["dataset_item"].subset + media = results["dataset_item"].media + if is_training_video_frame(subset, media): + index = results["index"] + filename = os.path.join(cache_dir, f"{subset}-{index:06d}.png") + if os.path.exists(filename): + loaded_img = load_image_from_cache(filename, to_float32=to_float32) + if loaded_img is not None: + return loaded_img + + img = results["dataset_item"].numpy # this takes long for VideoFrame + if to_float32: + img = img.astype(np.float32) + + if is_training_video_frame(subset, media): + save_image_to_cache(img, filename) + + return img + + +class OTXOpenVinoDataLoader: + """DataLoader implementation for ClassificationOpenVINOTask.""" + + def __init__(self, dataset: DatasetEntity, inferencer: Any, shuffle: bool = True): + super().__init__() + self.dataset = dataset + self.inferencer = inferencer + self.shuffler = None + if shuffle: + self.shuffler = list(range(len(dataset))) + random.shuffle(self.shuffler) + + def __getitem__(self, index: int): + """Get item from dataset.""" + if self.shuffler is not None: + index = self.shuffler[index] + + image = self.dataset[index].numpy + annotation = self.dataset[index].annotation_scene + + resized_image = self.inferencer.model.resize(image, (self.inferencer.model.w, self.inferencer.model.h)) + resized_image = self.inferencer.model.input_transform(resized_image) + resized_image = self.inferencer.model._change_layout(resized_image) + + return resized_image, annotation + + def __len__(self): + """Get length of dataset.""" + + return len(self.dataset) + + +def compute_robust_statistics(values: np.array) -> Dict[str, float]: + """Computes robust statistics of given samples. + + Args: + values (np.array): Array of samples + + Returns: + Dict[str, float]: Robust avg, min, max values + """ + stat: Dict = {} + if values.size == 0: + return stat + + avg_value = np.mean(values) + std_value = np.std(values) + avg_3std_min_value = avg_value - 3 * std_value + avg_3std_max_value = avg_value + 3 * std_value + min_value = np.min(values) + max_value = np.max(values) + + # Refine min/max to reduce outlier effect + robust_min_value = max(min_value, avg_3std_min_value) + robust_max_value = min(max_value, avg_3std_max_value) + + stat["avg"] = float(avg_value) + stat["std"] = float(std_value) + stat["min"] = float(min_value) + stat["max"] = float(max_value) + stat["robust_min"] = float(robust_min_value) + stat["robust_max"] = float(robust_max_value) + return stat + + +def compute_robust_scale_statistics(values: np.array) -> Dict[str, float]: + """Computes robust statistics of scale values. + + Average of 0.5x scale and 2x scale should be 1x + + Args: + values (np.array): Array of positive scale values + + Returns: + Dict[str, float]: Robust avg, min, max values + """ + # Compute stat in log scale & convert back to original scale + if values.size == 0: + return {} + + stat = compute_robust_statistics(np.log(values)) + stat = {k: float(np.exp(v)) for k, v in stat.items()} + stat["std"] = float(np.std(values)) # Normal scale std is better for understanding + return stat + + +def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False, max_samples=1000) -> Dict[str, Any]: + """Computes robust statistics of image & annotation sizes. + + Args: + dataset (DatasetEntity): Input dataset. + ann_stat (bool, optional): Whether to compute annotation size statistics. Defaults to False. + max_samples (int, optional): Maximum number of dataset subsamples to analyze. Defaults to 1000. + + Returns: + Dict[str, Any]: Robust avg, min, max values for images, and annotations optionally. + ex) stat = { + "image": {"avg": ...}, + "annotation": { + "num_per_image": {"avg": ...}, + "size_of_shape": {"avg": ...}, + } + } + """ + stat: Dict = {} + if len(dataset) == 0 or max_samples <= 0: + return stat + + max_image_samples = min(max_samples, len(dataset)) + image_indices = np.random.permutation(len(dataset))[:max_image_samples] + + image_sizes = [] + for i in image_indices: + data = dataset[int(i)] + image_sizes.append(np.sqrt(data.width * data.height)) + stat["image"] = compute_robust_scale_statistics(np.array(image_sizes)) + + if ann_stat: + stat["annotation"] = {} + num_per_images: List[int] = [] + size_of_shapes: List[float] = [] + for i in image_indices: + data = dataset[int(i)] + annotations = data.get_annotations() + num_per_images.append(len(annotations)) + + if len(size_of_shapes) >= max_samples: + continue + + image_area = data.width * data.height + + def scale_of(ann): + return np.sqrt(image_area * ann.shape.get_area()) + + size_of_shapes.extend( + filter(lambda x: x >= 1, map(scale_of, annotations)) + ) # Filter out shapes smaller than 1 pixel as outlier + + stat["annotation"]["num_per_image"] = compute_robust_statistics(np.array(num_per_images)) + stat["annotation"]["size_of_shape"] = compute_robust_scale_statistics(np.array(size_of_shapes)) + + return stat diff --git a/src/otx/algorithms/common/utils/dist_utils.py b/src/otx/algorithms/common/utils/dist_utils.py new file mode 100644 index 00000000000..29fed75868f --- /dev/null +++ b/src/otx/algorithms/common/utils/dist_utils.py @@ -0,0 +1,29 @@ +"""Module for defining distance utils.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os +from pathlib import Path +from typing import Union + +import torch.distributed as dist + + +def get_dist_info(): # pylint: disable=inconsistent-return-statements + """A function that retrieves information about the current distributed training environment.""" + if dist.is_available(): + # data distributed parallel + try: + return dist.get_rank(), dist.get_world_size(), True + except RuntimeError: + return 0, 1, False + + +def append_dist_rank_suffix(file_name: Union[str, Path]) -> str: + """Append distributed training rank suffix to the file name.""" + if "LOCAL_RANK" in os.environ: + file_name = Path(file_name) + dist_suffix = f"_proc{os.environ['LOCAL_RANK']}" + file_name = file_name.parent / f"{file_name.stem}{dist_suffix}{file_name.suffix}" + + return str(file_name) diff --git a/src/otx/algorithms/common/utils/ext_loader.py b/src/otx/algorithms/common/utils/ext_loader.py new file mode 100644 index 00000000000..a2641e814bc --- /dev/null +++ b/src/otx/algorithms/common/utils/ext_loader.py @@ -0,0 +1,21 @@ +"""Module for defining ext loader.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import importlib + + +def load_ext(path, funcs): + """A function that loads module and verifies that the specified functions are present in the module. + + :param path: str, the file path of the module to load. + :param funcs: list of str, the names of the functions to verify in the loaded module. + :return: the loaded module object. + :raises: AssertionError if any of the specified functions are missing from the loaded module. + """ + ext = importlib.import_module(path) + for fun in funcs: + assert hasattr(ext, fun), f"{fun} miss in module {path}" + + return ext diff --git a/src/otx/algorithms/common/utils/ir.py b/src/otx/algorithms/common/utils/ir.py new file mode 100644 index 00000000000..e342c41f771 --- /dev/null +++ b/src/otx/algorithms/common/utils/ir.py @@ -0,0 +1,39 @@ +"""Collections of IR-related utils for common OTX algorithms.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from pathlib import Path +from typing import Any, Dict, Tuple + +from openvino.runtime import Core, save_model + + +def check_if_quantized(model: Any) -> bool: + """Checks if OpenVINO model is already quantized.""" + nodes = model.get_ops() + for op in nodes: + if "FakeQuantize" == op.get_type_name(): + return True + return False + + +def embed_ir_model_data(xml_file: str, data_items: Dict[Tuple[str, str], Any]) -> None: + """Embeds serialized data to IR xml file. + + Args: + xml_file : a path to IR xml file. + data_items : a dict with tuple-keyworded serialized objects. + """ + + core = Core() + model = core.read_model(xml_file) + for k, data in data_items.items(): + model.set_rt_info(data, list(k)) + + # workaround for CVS-110054 + tmp_xml_path = Path(Path(xml_file).parent) / "tmp.xml" + save_model(model, str(tmp_xml_path), compress_to_fp16=False) + tmp_xml_path.rename(xml_file) + Path(str(tmp_xml_path.parent / tmp_xml_path.stem) + ".bin").unlink() diff --git a/src/otx/algorithms/common/utils/mask_to_bbox.py b/src/otx/algorithms/common/utils/mask_to_bbox.py new file mode 100644 index 00000000000..1d2267cf8d1 --- /dev/null +++ b/src/otx/algorithms/common/utils/mask_to_bbox.py @@ -0,0 +1,71 @@ +"""Convert a mask to a border image.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +from typing import List + +import numpy as np +from skimage.measure import find_contours, label, regionprops + + +def mask_to_border(mask): + """Make a border by using a binary mask. + + Args: + mask (np.ndarray): Input binary mask + + Returns: + np.ndarray: Border image. + """ + h, w = mask.shape + border = np.zeros((h, w)) + + contours = find_contours(mask, 0.5) # since the input range is [0, 1], the threshold is 0.5 + for contour in contours: + for c in contour: + x = int(c[0]) + y = int(c[1]) + border[x][y] = 1 # since the input is binary, the value is 1 + + return border + + +def mask2bbox(mask) -> List[List[int]]: + """Mask to bounding boxes. + + Args: + mask (np.ndarray): Input binary mask + + Returns: + List[List[int]]: Bounding box coordinates + """ + bboxes: List[List[int]] = [] + + mask = mask_to_border(mask) + print(np.unique(mask)) + lbl = label(mask) + props = regionprops(lbl) + for prop in props: + x1 = prop.bbox[1] + y1 = prop.bbox[0] + + x2 = prop.bbox[3] + y2 = prop.bbox[2] + + bboxes.append([x1, y1, x2, y2]) + + return bboxes diff --git a/src/otx/algorithms/common/utils/mo_wrapper.py b/src/otx/algorithms/common/utils/mo_wrapper.py new file mode 100644 index 00000000000..ab123917f48 --- /dev/null +++ b/src/otx/algorithms/common/utils/mo_wrapper.py @@ -0,0 +1,171 @@ +"""Module for defining Model Optimizer (mo) wrapper.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=import-error + +import os +import subprocess +import sys +import time + + +def __mo_check_requirements(framework="onnx"): + from mo.utils.versions_checker import check_requirements as mo_check_requirements + + return mo_check_requirements(framework) + + +def check_requirements_with_version(framework=None): + """A function that checks the compatibility of Model Optimizer with a specific target and framework version. + + :param framework: str, a string representing the framework name. Default is None. + :return: bool, True if the compatibility check is successful, False otherwise. + """ + from mo.utils.version import get_version as mo_get_version + + mo_version = mo_get_version() + print("mo vesion =", mo_version) + # # TODO: model optimizer version in OpenVINO 2019.3.3 is 2019.3.0 + # # it will be changed before official release to meet with requirement of VAS + # required_mo_version = '2019.3.0' + # if target.lower() == 'fr': + # required_mo_version = '2019.1.1' + # if not mo_version.startswith(required_mo_version): + # print('current MO version: {} but required version: {}'.format(mo_version, required_mo_version)) + # return False + err_code = __mo_check_requirements(framework) + if err_code: + print(f"mo_check_requriements returns: {err_code}") + return False + return True + + +def check_requirements(framework="onnx"): + """A function that checks the compatibility of Model Optimizer with a specific framework version. + + :param framework: str, a string representing the framework name. Default is "onnx". + :return: bool, True if the compatibility check is successful, False otherwise. + """ + err_code = __mo_check_requirements(framework) + if err_code: + print("mo_check_requriements returns: {err_code}") + return False + return True + + +def __mo_main_wrapper(argv, framework=None): + """MO main wrapper. + + See: openvino_2019.1.094/deployment_tools/model_optimizer/mo/main.py + """ + # substitute the value of sys.argv with a proper cli arguments to call the + # OpenVINO model optimizer's main function + from mo.main import main as mo_main + + old_argv = sys.argv + sys.argv = [sys.argv[0]] + argv + + # run mo + from mo.utils.cli_parser import get_all_cli_parser + + ret = mo_main(get_all_cli_parser(), framework) + + # restore sys.argv + sys.argv = old_argv + + return ret + + +MO_LOG_LEVELS = ["CRITICAL", "ERROR", "WARN", "WARNING", "INFO", "DEBUG", "NOTSET"] + +MO_ARGS = [ + "input_model", + "input_shape", + "input", + "mean_values", + "scale", + "model_name", + "log_level", + "compress_to_fp16", + "scale_values", + "disable_fusing", + "transformations_config", + "reverse_input_channels", + "output_dir", +] + + +def generate_ir(output_path, model_path, silent, save_xml=True, **mo_kwargs): + """A function that generates IR from a given model using the Model Optimizer. + + :param output_path: str, a string representing the path to save the IR files. + :param model_path: str, a string representing the path to the original model file. + :param silent: bool, a flag indicating whether to suppress the output messages. If True, + the function will redirect stdout to null device. If False, print messages to the console. + :param save_xml: bool, a flag indicating whether to save the XML file. Default is True. + :param mo_kwargs: keyword arguments for the Model Optimizer. + :return: tuple of int and str, the return code of the Model Optimizer and the output message. + """ + # parse kwargs for the model optimizer + mo_args = [] + for key, value in mo_kwargs.items(): + if key not in MO_ARGS: + return -1, "Not supported argument: {key}" + if value is not None: + mo_args.append(f"--{key}={value}") + else: + mo_args.append(f"--{key}") + + is_output_dir_provided = False + for mo_arg in mo_args: + if mo_arg.startswith("--output_dir"): + is_output_dir_provided = True + break + if not is_output_dir_provided: + mo_args.append("--output_dir={model_path}") + print(f"mo-args: {mo_args}") + + if silent: + # redirect stdout messages from MO to null device + devnull = open("/dev/null", "w", encoding="utf-8") # pylint: disable=consider-using-with + old_stdout = sys.stdout + sys.stdout = devnull + + # ret = __mo_main_wrapper(mo_args, None) + # ret = os.system('mo.py ' + ' '.join(mo_args)) + ret = subprocess.run(["mo"] + mo_args, shell=False, check=True).returncode + + if silent: + # return back stdout + sys.stdout = old_stdout + + # NOTE: mo returns non zero return code (245) even though it successfully generate IR + cur_time = time.time() + model_name = mo_kwargs.get("model_name", "model") + if not ( + ret == 245 + and not {f"{model_name}.bin", f"{model_name}.xml"} - set(os.listdir(model_path)) + and ( + os.path.getmtime(os.path.join(model_path, f"{model_name}.bin")) - cur_time < 5 + and os.path.getmtime(os.path.join(model_path, f"{model_name}.xml")) - cur_time < 5 + ) + ): + return ret, "Failed to run the model optimizer to convert a model" + + print("*** Model optimization completed ***") + # move bin files to workspace + + if not os.path.exists(output_path): + os.makedirs(output_path) + os.rename( + os.path.join(model_path, model_name + ".bin"), + os.path.join(output_path, model_name + ".bin"), + ) + if save_xml: + os.rename( + os.path.join(model_path, model_name + ".xml"), + os.path.join(output_path, model_name + ".xml"), + ) + + return 0, f"Saved outputs into {output_path}" diff --git a/src/otx/algorithms/common/utils/task_adapt.py b/src/otx/algorithms/common/utils/task_adapt.py new file mode 100644 index 00000000000..b720de811cb --- /dev/null +++ b/src/otx/algorithms/common/utils/task_adapt.py @@ -0,0 +1,108 @@ +"""Module for defining task adapt related utils.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np + +from otx.utils.logger import get_logger + +logger = get_logger() + + +def map_class_names(src_classes, dst_classes): + """Computes src to dst index mapping. + + src2dst[src_idx] = dst_idx + # according to class name matching, -1 for non-matched ones + assert(len(src2dst) == len(src_classes)) + ex) + src_classes = ['person', 'car', 'tree'] + dst_classes = ['tree', 'person', 'sky', 'ball'] + -> Returns src2dst = [1, -1, 0] + """ + src2dst = [] + for src_class in src_classes: + if src_class in dst_classes: + src2dst.append(dst_classes.index(src_class)) + else: + src2dst.append(-1) + return src2dst + + +def refine_results(results): + """A function that concatenates the results of multiple runs into a single array. + + :param results: list, a list of dictionaries or arrays containing the results. + :return: numpy.ndarray or dict, the concatenated results. + """ + if isinstance(results[0], dict): + tasks = results[0].keys() + res_refine = {} + for task in tasks: + res_refine[task] = np.concatenate([res[task] for res in results]) + else: + res_refine = np.vstack(results) + return res_refine + + +def extract_anchor_ratio(dataset, num_ratios=5): + """A function that extracts anchor ratios from a given dataset. + + :param dataset: dataset object, an instance of a dataset. + :param num_ratios: int, the number of anchor ratios to be extracted. + :return: list, a list of extracted anchor ratios. + """ + ratio_dict = dict(info=[], step=-1) + dataset, _ = unwrap_dataset(dataset) + for item in dataset: + ori_shape = item["img_metas"].data["ori_shape"] + img_shape = item["img_metas"].data["img_shape"] + bboxes = item["gt_bboxes"].data.numpy() + for bbox in bboxes: + w_o = bbox[2] - bbox[0] + h_o = bbox[3] - bbox[1] + if w_o > 0.04 * ori_shape[1] and h_o > 0.04 * ori_shape[0]: + w_i = w_o * img_shape[1] / ori_shape[1] + h_i = h_o * img_shape[0] / ori_shape[0] + ratio_dict["info"].append(w_i / h_i) + ratio_dict["info"] = np.sort(np.array(ratio_dict["info"])) + ratio_dict["step"] = int(len(ratio_dict["info"]) / num_ratios) + proposal_ratio = [] + for i in range(num_ratios): + r = np.mean(ratio_dict["info"][i * ratio_dict["step"] : (i + 1) * ratio_dict["step"]]) + proposal_ratio.append(r) + return proposal_ratio + + +def map_cat_and_cls_as_order(classes, cats): + """A function that maps classes and categories to label orders. + + :param classes: list, a list of class names. + :param cats: dict, a dictionary containing category information. + :return: tuple of dict and list, a dictionary mapping category IDs to label orders and a list of category IDs. + """ + cat2label = {} + cat_ids = [] + for i, cls in enumerate(classes): + for _, cat in cats.items(): + if cls == cat["name"]: + cat_id = cat["id"] + cat_ids.append(cat_id) + cat2label.update({cat_id: i}) + return cat2label, cat_ids + + +def unwrap_dataset(dataset): + """A function that unwraps a dataset object to its base dataset. + + :param dataset: dataset object, an instance of a dataset. + :return: tuple of dataset object and int, the base dataset and the number of times to repeat the dataset. + """ + times = 1 + target_dataset = dataset + while hasattr(target_dataset, "dataset"): + if hasattr(target_dataset, "times"): + times = target_dataset.times + target_dataset = target_dataset.dataset + return target_dataset, times diff --git a/src/otx/algorithms/common/utils/utils.py b/src/otx/algorithms/common/utils/utils.py new file mode 100644 index 00000000000..89ef89545bc --- /dev/null +++ b/src/otx/algorithms/common/utils/utils.py @@ -0,0 +1,221 @@ +"""Collections of Utils for common OTX algorithms.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import importlib +import inspect +import os +import random +import sys +from collections import defaultdict +from pathlib import Path +from typing import Any, Callable, Dict, Optional, Tuple, Union + +import numpy as np +import onnx +import torch +import yaml +from addict import Dict as adict + +from otx.utils.logger import get_logger +from otx.utils.utils import add_suffix_to_filename + +logger = get_logger() + + +HPU_AVAILABLE = None +try: + import habana_frameworks.torch as htorch +except ImportError: + HPU_AVAILABLE = False + htorch = None + +XPU_AVAILABLE = None +try: + import intel_extension_for_pytorch as ipex +except ImportError: + XPU_AVAILABLE = False + ipex = None + + +class UncopiableDefaultDict(defaultdict): + """Defauldict type object to avoid deepcopy.""" + + def __deepcopy__(self, memo): + """Deepcopy.""" + return self + + +def load_template(path): + """Loading model template function.""" + with open(path, encoding="UTF-8") as f: + template = yaml.safe_load(f) + return template + + +def get_task_class(path: str): + """Return Task classes.""" + module_name, class_name = path.rsplit(".", 1) + module = importlib.import_module(module_name) + return getattr(module, class_name) + + +def get_arg_spec( # noqa: C901 # pylint: disable=too-many-branches + fn: Callable, # pylint: disable=invalid-name + depth: Optional[int] = None, +) -> Tuple[str, ...]: + """Get argument spec of function.""" + + args = set() + + cls_obj = None + if inspect.ismethod(fn): + fn_name = fn.__name__ + cls_obj = fn.__self__ + if not inspect.isclass(cls_obj): + cls_obj = cls_obj.__class__ + else: + fn_name = fn.__name__ + names = fn.__qualname__.split(".") + if len(names) > 1 and names[-1] == fn_name: + cls_obj = globals()[".".join(names[:-1])] + + if cls_obj: + for obj in cls_obj.mro(): # type: ignore + fn_obj = cls_obj.__dict__.get(fn_name, None) + if fn_obj is not None: + if isinstance(fn_obj, staticmethod): + cls_obj = None + break + + if cls_obj is None: + # function, staticmethod + spec = inspect.getfullargspec(fn) + args.update(spec.args) + else: + # method, classmethod + for i, obj in enumerate(cls_obj.mro()): # type: ignore + if depth is not None and i == depth: + break + method = getattr(obj, fn_name, None) + if method is None: + break + spec = inspect.getfullargspec(method) + args.update(spec.args[1:]) + if spec.varkw is None and spec.varargs is None: + break + return tuple(args) + + +def set_random_seed(seed, logger=None, deterministic=False): + """Set random seed. + + Args: + seed (int): Seed to be used. + logger (logging.Logger): logger for logging seed info + deterministic (bool): Whether to set the deterministic option for + CUDNN backend, i.e., set `torch.backends.cudnn.deterministic` + to True and `torch.backends.cudnn.benchmark` to False. + Default: False. + """ + import torch + + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + if is_xpu_available(): + torch.xpu.manual_seed_all(seed) + os.environ["PYTHONHASHSEED"] = str(seed) + if logger: + logger.info(f"Training seed was set to {seed} w/ deterministic={deterministic}.") + if deterministic: + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + +def get_default_async_reqs_num() -> int: + """Returns a default number of infer request for OV models.""" + reqs_num = os.cpu_count() + if reqs_num is not None: + reqs_num = max(1, int(reqs_num / 2)) + return reqs_num + else: + return 1 + + +def read_py_config(filename: str) -> adict: + """Reads py config to a dict.""" + filename = str(Path(filename).resolve()) + if not Path(filename).is_file: + raise RuntimeError("config not found") + assert filename.endswith(".py") + module_name = Path(filename).stem + if "." in module_name: + raise ValueError("Dots are not allowed in config file path.") + config_dir = Path(filename).parent + sys.path.insert(0, str(config_dir)) + mod = importlib.import_module(module_name) + sys.path.pop(0) + cfg_dict = adict( + { + name: value + for name, value in mod.__dict__.items() + if not name.startswith("__") and not inspect.isclass(value) and not inspect.ismodule(value) + } + ) + + return cfg_dict + + +def embed_onnx_model_data(onnx_file: str, extra_model_data: Dict[Tuple[str, str], Any]) -> None: + """Embeds model api config to onnx file.""" + model = onnx.load(onnx_file) + + for item in extra_model_data: + meta = model.metadata_props.add() + attr_path = " ".join(map(str, item)) + meta.key = attr_path.strip() + meta.value = str(extra_model_data[item]) + + onnx.save(model, onnx_file) + + +def is_xpu_available() -> bool: + """Checks if XPU device is available.""" + global XPU_AVAILABLE # noqa: PLW0603 + if XPU_AVAILABLE is None: + XPU_AVAILABLE = hasattr(torch, "xpu") and torch.xpu.is_available() + return XPU_AVAILABLE + + +def is_hpu_available() -> bool: + """Check if HPU device is available.""" + global HPU_AVAILABLE # noqa: PLW0603 + if HPU_AVAILABLE is None: + HPU_AVAILABLE = htorch.hpu.is_available() + return HPU_AVAILABLE + + +def cast_bf16_to_fp32(tensor: torch.Tensor) -> torch.Tensor: + """Cast bf16 tensor to fp32 before processed by numpy. + + numpy doesn't support bfloat16, it is required to convert bfloat16 tensor to float32. + """ + if tensor.dtype == torch.bfloat16: + tensor = tensor.to(torch.float32) + return tensor + + +def get_cfg_based_on_device(cfg_file_path: Union[str, Path]) -> str: + """Find a config file according to device.""" + if is_xpu_available(): + cfg_for_device = add_suffix_to_filename(cfg_file_path, "_xpu") + if cfg_for_device.exists(): + logger.info( + f"XPU is detected. XPU config file will be used : {Path(cfg_file_path).name} -> {cfg_for_device.name}" + ) + cfg_file_path = cfg_for_device + + return str(cfg_file_path) diff --git a/src/otx/algorithms/detection/__init__.py b/src/otx/algorithms/detection/__init__.py new file mode 100644 index 00000000000..02a2d5f45d1 --- /dev/null +++ b/src/otx/algorithms/detection/__init__.py @@ -0,0 +1,11 @@ +"""OTX Algorithms - Detection.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +MMDET_AVAILABLE = True + +try: + import mmdet # noqa: F401 +except ImportError: + MMDET_AVAILABLE = False diff --git a/src/otx/algorithms/detection/adapters/__init__.py b/src/otx/algorithms/detection/adapters/__init__.py new file mode 100644 index 00000000000..cf70c848ec9 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/__init__.py @@ -0,0 +1,15 @@ +"""Adapters for Detection.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/adapters/mmdet/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/__init__.py new file mode 100644 index 00000000000..dcd7ebf9f7f --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/__init__.py @@ -0,0 +1,19 @@ +"""OTX Adapters - mmdet.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from . import models +from .datasets.dataset import OTXDetDataset + +# fmt: off +# isort: off +# FIXME: openvino pot library adds stream handlers to root logger +# which makes annoying duplicated logging +from mmdet.utils import get_root_logger # pylint: disable=wrong-import-order +get_root_logger().propagate = False # pylint: disable=wrong-import-order +# isort:on +# fmt: on + +__all__ = ["OTXDetDataset", "models"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/apis/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/apis/__init__.py new file mode 100644 index 00000000000..dd3628c1d0b --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/apis/__init__.py @@ -0,0 +1,8 @@ +"""Adapters of classification - mmdet.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .train import train_detector + +__all__ = ["train_detector"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/apis/train.py b/src/otx/algorithms/detection/adapters/mmdet/apis/train.py new file mode 100644 index 00000000000..3a731513f59 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/apis/train.py @@ -0,0 +1,202 @@ +"""Train function for detection task.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# Copyright (c) OpenMMLab. All rights reserved. +import os + +import torch +from mmcv.runner import ( + DistSamplerSeedHook, + EpochBasedRunner, + OptimizerHook, + build_runner, + get_dist_info, +) +from mmdet.core import DistEvalHook, EvalHook, build_optimizer +from mmdet.datasets import build_dataloader, build_dataset, replace_ImageToTensor +from mmdet.utils import build_ddp, compat_cfg, find_latest_checkpoint, get_root_logger +from mmdet.utils.util_distribution import build_dp, dp_factory + +from otx.algorithms.common.adapters.mmcv.utils import HPUDataParallel, XPUDataParallel +from otx.algorithms.common.adapters.mmcv.utils.hpu_optimizers import HABANA_OPTIMIZERS + +dp_factory["xpu"] = XPUDataParallel +dp_factory["hpu"] = HPUDataParallel + + +def auto_scale_lr(cfg, distributed, logger): + """Automatically scaling LR according to GPU number and sample per GPU. + + Args: + cfg (config): Training config. + distributed (bool): Using distributed or not. + logger (logging.Logger): Logger. + """ + # Get flag from config + if ("auto_scale_lr" not in cfg) or (not cfg.auto_scale_lr.get("enable", False)): + logger.info("Automatic scaling of learning rate (LR)" " has been disabled.") + return + + # Get base batch size from config + base_batch_size = cfg.auto_scale_lr.get("base_batch_size", None) + if base_batch_size is None: + return + + # Get gpu number + if distributed: + _, world_size = get_dist_info() + num_gpus = len(range(world_size)) + else: + num_gpus = len(cfg.gpu_ids) + + # calculate the batch size + samples_per_gpu = cfg.data.train_dataloader.samples_per_gpu + batch_size = num_gpus * samples_per_gpu + logger.info( + f"Training with {num_gpus} GPU(s) with {samples_per_gpu} " + f"samples per GPU. The total batch size is {batch_size}." + ) + + if batch_size != base_batch_size: + # scale LR with + # [linear scaling rule](https://arxiv.org/abs/1706.02677) + scaled_lr = (batch_size / base_batch_size) * cfg.optimizer.lr + logger.info("LR has been automatically scaled " f"from {cfg.optimizer.lr} to {scaled_lr}") + cfg.optimizer.lr = scaled_lr + else: + logger.info( + "The batch size match the " + f"base batch size: {base_batch_size}, " + f"will not scaling the LR ({cfg.optimizer.lr})." + ) + + +def train_detector(model, dataset, cfg, distributed=False, validate=False, timestamp=None, meta=None): + """Trains a detector via mmdet.""" + + cfg = compat_cfg(cfg) + logger = get_root_logger(log_level=cfg.log_level) + + # prepare data loaders + dataset = dataset if isinstance(dataset, (list, tuple)) else [dataset] + + runner_type = "EpochBasedRunner" if "runner" not in cfg else cfg.runner["type"] + + train_dataloader_default_args = dict( + samples_per_gpu=2, + workers_per_gpu=2, + # `num_gpus` will be ignored if distributed + num_gpus=len(cfg.gpu_ids), + dist=distributed, + seed=cfg.seed, + runner_type=runner_type, + persistent_workers=False, + ) + + train_loader_cfg = {**train_dataloader_default_args, **cfg.data.get("train_dataloader", {})} + + data_loaders = [build_dataloader(ds, **train_loader_cfg) for ds in dataset] + + fp16_cfg = cfg.get("fp16_", None) + # put model on gpus + if cfg.device == "xpu": + model.to(f"xpu:{cfg.gpu_ids[0]}") + + if distributed: + find_unused_parameters = cfg.get("find_unused_parameters", False) + # Sets the `find_unused_parameters` parameter in + # torch.nn.parallel.DistributedDataParallel + model = build_ddp( + model, + cfg.device, + device_ids=[int(os.environ["LOCAL_RANK"])], + broadcast_buffers=False, + find_unused_parameters=find_unused_parameters, + ) + elif cfg.device == "hpu": + model = build_dp( + model, cfg.device, device_ids=cfg.gpu_ids, dim=0, enable_autocast=bool(fp16_cfg), put_gt_on_device=False + ) + # patch optimizer + if (new_type := "Fused" + cfg.optimizer.get("type", "SGD")) in HABANA_OPTIMIZERS: + cfg.optimizer["type"] = new_type + else: + model = build_dp(model, cfg.device, device_ids=cfg.gpu_ids) + + # build optimizer + auto_scale_lr(cfg, distributed, logger) + + optimizer = build_optimizer(model, cfg.optimizer) + + if cfg.device == "xpu": + if cfg.optimizer_config.get("bf16_training", False): + logger.warning("XPU supports fp32 training only currently.") + dtype = torch.float32 + model.train() + model, optimizer = torch.xpu.optimize(model, optimizer=optimizer, dtype=dtype) + + if "bf16_training" in cfg.optimizer_config: + # Remove unused parameters in runner + cfg.optimizer_config.pop("bf16_training") + + runner = build_runner( + cfg.runner, default_args=dict(model=model, optimizer=optimizer, work_dir=cfg.work_dir, logger=logger, meta=meta) + ) + + # an ugly workaround to make .log and .log.json filenames the same + runner.timestamp = timestamp + + if fp16_cfg is None and distributed and "type" not in cfg.optimizer_config: + optimizer_config = OptimizerHook(**cfg.optimizer_config) + else: + optimizer_config = cfg.optimizer_config + + # register hooks + runner.register_training_hooks( + cfg.lr_config, + optimizer_config, + cfg.checkpoint_config, + cfg.log_config, + cfg.get("momentum_config", None), + custom_hooks_config=cfg.get("custom_hooks", None), + ) + + if distributed: + if isinstance(runner, EpochBasedRunner): + runner.register_hook(DistSamplerSeedHook()) + + # register eval hooks + if validate: + val_dataloader_default_args = dict( + samples_per_gpu=1, workers_per_gpu=2, dist=distributed, shuffle=False, persistent_workers=False + ) + + val_dataloader_args = {**val_dataloader_default_args, **cfg.data.get("val_dataloader", {})} + # Support batch_size > 1 in validation + + if val_dataloader_args["samples_per_gpu"] > 1: + # Replace 'ImageToTensor' to 'DefaultFormatBundle' + cfg.data.val.pipeline = replace_ImageToTensor(cfg.data.val.pipeline) + val_dataset = build_dataset(cfg.data.val, dict(test_mode=True)) + + val_dataloader = build_dataloader(val_dataset, **val_dataloader_args) + eval_cfg = cfg.get("evaluation", {}) + eval_cfg["by_epoch"] = cfg.runner["type"] != "IterBasedRunner" + eval_hook = DistEvalHook if distributed else EvalHook + # In this PR (https://github.com/open-mmlab/mmcv/pull/1193), the + # priority of IterTimerHook has been modified from 'NORMAL' to 'LOW'. + runner.register_hook(eval_hook(val_dataloader, **eval_cfg), priority="LOW") + + resume_from = None + if cfg.resume_from is None and cfg.get("auto_resume"): + resume_from = find_latest_checkpoint(cfg.work_dir) + if resume_from is not None: + cfg.resume_from = resume_from + + if cfg.resume_from: + runner.resume(cfg.resume_from) + elif cfg.load_from: + runner.load_checkpoint(cfg.load_from) + runner.run(data_loaders, cfg.workflow) diff --git a/src/otx/algorithms/detection/adapters/mmdet/configurer.py b/src/otx/algorithms/detection/adapters/mmdet/configurer.py new file mode 100644 index 00000000000..d72cba65b67 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/configurer.py @@ -0,0 +1,228 @@ +"""Base configurer for mmdet config.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from typing import Optional, Tuple + +from mmcv.ops.nms import NMSop +from mmcv.ops.roi_align import RoIAlign +from mmcv.utils import ConfigDict + +from otx.algorithms.common.adapters.mmcv.clsincr_mixin import IncrConfigurerMixin +from otx.algorithms.common.adapters.mmcv.configurer import BaseConfigurer +from otx.algorithms.common.adapters.mmcv.semisl_mixin import SemiSLConfigurerMixin +from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + InputSizeManager, +) +from otx.algorithms.detection.adapters.mmdet.utils import ( + cluster_anchors, + monkey_patched_nms, + monkey_patched_roi_align, + patch_tiling, + should_cluster_anchors, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-public-methods +class DetectionConfigurer(BaseConfigurer): + """Patch config to support otx train.""" + + def override_from_hyperparams(self, config, hyperparams, **kwargs): + """Override config using hyperparameters from OTX cli.""" + dataset = kwargs.get("train_dataset", None) + super().override_from_hyperparams(config, hyperparams) + patch_tiling(config, hyperparams, dataset) + + def configure_model(self, cfg, data_classes, model_classes, ir_options, **kwargs): + """Configuration for model config.""" + super().configure_model(cfg, data_classes, model_classes, ir_options, **kwargs) + self.configure_regularization(cfg) + self.configure_max_num_detections(cfg, kwargs.get("max_num_detections", 0)) + + def configure_max_num_detections(self, cfg, max_num_detections): + """Patch config for maximum number of detections.""" + if max_num_detections > 0: + logger.info(f"Model max_num_detections: {max_num_detections}") + test_cfg = cfg.model.test_cfg + test_cfg.max_per_img = max_num_detections + test_cfg.nms_pre = max_num_detections * 10 + # Special cases for 2-stage detectors (e.g. MaskRCNN) + if hasattr(test_cfg, "rpn"): + test_cfg.rpn.nms_pre = max_num_detections * 20 + test_cfg.rpn.max_per_img = max_num_detections * 10 + if hasattr(test_cfg, "rcnn"): + test_cfg.rcnn.max_per_img = max_num_detections + train_cfg = cfg.model.train_cfg + if hasattr(train_cfg, "rpn_proposal"): + train_cfg.rpn_proposal.nms_pre = max_num_detections * 20 + train_cfg.rpn_proposal.max_per_img = max_num_detections * 10 + + def configure_regularization(self, cfg): # noqa: C901 + """Patch regularization parameters.""" + if self.training: + if cfg.model.get("l2sp_weight", 0.0) > 0.0: + logger.info("regularization config!!!!") + + # Checkpoint + l2sp_ckpt = cfg.model.get("l2sp_ckpt", None) + if l2sp_ckpt is None: + if "pretrained" in cfg.model: + l2sp_ckpt = cfg.model.pretrained + if cfg.load_from: + l2sp_ckpt = cfg.load_from + cfg.model.l2sp_ckpt = l2sp_ckpt + + # Disable weight decay + if "weight_decay" in cfg.optimizer: + cfg.optimizer.weight_decay = 0.0 + + def configure_task(self, cfg, **kwargs): + """Patch config to support training algorithm.""" + + assert "train_dataset" in kwargs + train_dataset = kwargs["train_dataset"] + + super().configure_task(cfg, **kwargs) + if "task_adapt" in cfg: + if self.data_classes != self.model_classes: + self.configure_task_data_pipeline(cfg) + if cfg["task_adapt"].get("use_adaptive_anchor", False): + self.configure_anchor(cfg, train_dataset) + if self.task_adapt_type == "default_task_adapt": + self.configure_bbox_head(cfg) + + def configure_device(self, cfg): + """Setting device for training and inference.""" + super().configure_device(cfg) + if cfg.device in ["xpu", "hpu"]: + NMSop.forward = monkey_patched_nms + RoIAlign.forward = monkey_patched_roi_align + + def configure_classes(self, cfg): + """Patch classes for model and dataset.""" + super().configure_classes(cfg) + self._configure_eval_dataset(cfg) + + def _configure_head(self, cfg): + """Patch number of classes of head.""" + head_names = ("mask_head", "bbox_head", "segm_head") + num_classes = len(self.model_classes) + if "roi_head" in cfg.model: + # For Faster-RCNNs + for head_name in head_names: + if head_name in cfg.model.roi_head: + if isinstance(cfg.model.roi_head[head_name], list): + for head in cfg.model.roi_head[head_name]: + head.num_classes = num_classes + else: + cfg.model.roi_head[head_name].num_classes = num_classes + else: + # For other architectures (including SSD) + for head_name in head_names: + if head_name in cfg.model: + cfg.model[head_name].num_classes = num_classes + + def _configure_eval_dataset(self, cfg): + if cfg.get("task", "detection") == "detection": + eval_types = ["val", "test"] + for eval_type in eval_types: + if cfg.data[eval_type]["type"] == "TaskAdaptEvalDataset": + cfg.data[eval_type]["model_classes"] = self.model_classes + else: + # Wrap original dataset config + org_type = cfg.data[eval_type]["type"] + cfg.data[eval_type]["type"] = "TaskAdaptEvalDataset" + cfg.data[eval_type]["org_type"] = org_type + cfg.data[eval_type]["model_classes"] = self.model_classes + + def configure_task_data_pipeline(self, cfg): + """Trying to alter class indices of training data according to model class order.""" + tr_data_cfg = self.get_subset_data_cfg(cfg, "train") + class_adapt_cfg = dict(type="AdaptClassLabels", src_classes=self.data_classes, dst_classes=self.model_classes) + pipeline_cfg = tr_data_cfg.pipeline + for i, operation in enumerate(pipeline_cfg): + if operation["type"] in [ + "LoadAnnotationFromOTXDataset", + "LoadResizeDataFromOTXDataset", + ]: # insert just after this operation + op_next_ann = pipeline_cfg[i + 1] if i + 1 < len(pipeline_cfg) else {} + if op_next_ann.get("type", "") == class_adapt_cfg["type"]: + op_next_ann.update(class_adapt_cfg) + else: + pipeline_cfg.insert(i + 1, class_adapt_cfg) + break + + def configure_anchor(self, cfg, train_dataset): + """Patch anchor settings for single stage detector.""" + if cfg.model.type in ["SingleStageDetector", "CustomSingleStageDetector"]: + anchor_cfg = cfg.model.bbox_head.anchor_generator + if anchor_cfg.type == "SSDAnchorGeneratorClustered": + cfg.model.bbox_head.anchor_generator.pop("input_size", None) + if should_cluster_anchors(cfg) and train_dataset is not None: + cluster_anchors(cfg, train_dataset) + + def configure_bbox_head(self, cfg): + """Patch classification loss if there are ignore labels.""" + if cfg.get("task", "detection") == "detection": + bbox_head = cfg.model.bbox_head + else: + bbox_head = cfg.model.roi_head.bbox_head + + if cfg.get("ignore", False): + bbox_head.loss_cls = ConfigDict( + type="CrossSigmoidFocalLoss", + use_sigmoid=True, + num_classes=len(self.model_classes), + alpha=bbox_head.loss_cls.get("alpha", 0.25), + gamma=bbox_head.loss_cls.get("gamma", 2.0), + ) + + @staticmethod + def configure_input_size( + cfg, input_size=Optional[Tuple[int, int]], model_ckpt_path: Optional[str] = None, training=True + ): + """Change input size if necessary.""" + if input_size is None: # InputSizePreset.DEFAULT + return + + # YOLOX tiny has a different input size in train and val data pipeline + base_input_size = None + model_cfg = cfg.get("model") + if model_cfg is not None: + if cfg.model.type == "CustomYOLOX" and cfg.model.backbone.widen_factor == 0.375: # YOLOX tiny case + base_input_size = { + "train": (640, 640), + "val": (416, 416), + "test": (416, 416), + "unlabeled": (992, 736), + } + manager = InputSizeManager(cfg, base_input_size) + + if input_size == (0, 0): # InputSizePreset.AUTO + if training: + input_size = BaseConfigurer.adapt_input_size_to_dataset(cfg, manager, use_annotations=True) + else: + input_size = manager.get_trained_input_size(model_ckpt_path) + if input_size is None: + return + + manager.set_input_size(input_size) + logger.info("Input size is changed to {}".format(input_size)) + + +class IncrDetectionConfigurer(IncrConfigurerMixin, DetectionConfigurer): + """Patch config to support incremental learning for object detection.""" + + def configure_task(self, cfg, **kwargs): + """Patch config to support incremental learning.""" + super(IncrConfigurerMixin, self).configure_task(cfg, **kwargs) + if "task_adapt" in cfg and self.task_adapt_type == "default_task_adapt": + self.configure_task_adapt_hook(cfg) + + +class SemiSLDetectionConfigurer(SemiSLConfigurerMixin, DetectionConfigurer): + """Patch config to support semi supervised learning for object detection.""" diff --git a/src/otx/algorithms/detection/adapters/mmdet/datasets/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/datasets/__init__.py new file mode 100644 index 00000000000..6d5942854d7 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/datasets/__init__.py @@ -0,0 +1,21 @@ +"""OTX Algorithms - Detection Dataset.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from . import pipelines +from .dataset import ImageTilingDataset, OTXDetDataset +from .task_adapt_dataset import TaskAdaptEvalDataset + +__all__ = ["OTXDetDataset", "pipelines", "ImageTilingDataset", "TaskAdaptEvalDataset"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/datasets/dataset.py b/src/otx/algorithms/detection/adapters/mmdet/datasets/dataset.py new file mode 100644 index 00000000000..7f1fb146311 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/datasets/dataset.py @@ -0,0 +1,428 @@ +"""Base MMDataset for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from collections import OrderedDict +from copy import copy +from typing import Any, Dict, List, Sequence, Tuple, Union + +import numpy as np +from mmcv import Config +from mmcv.utils import print_log +from mmdet.core import PolygonMasks +from mmdet.datasets.builder import DATASETS, build_dataset +from mmdet.datasets.custom import CustomDataset +from mmdet.datasets.pipelines import Compose + +from otx.algorithms.common.utils.data import get_old_new_img_indices +from otx.algorithms.detection.adapters.mmdet.evaluation import Evaluator +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.subset import Subset +from otx.api.utils.shape_factory import ShapeFactory + +from .tiling import Tile + + +# pylint: disable=invalid-name, too-many-locals, too-many-instance-attributes, super-init-not-called +def get_annotation_mmdet_format( + dataset_item: DatasetItemEntity, + labels: List[LabelEntity], + domain: Domain, + min_size: int = -1, +) -> dict: + """Function to convert a OTX annotation to mmdetection format. + + This is used both in the OTXDataset class defined in + this file as in the custom pipeline element 'LoadAnnotationFromOTXDataset' + + :param dataset_item: DatasetItem for which to get annotations + :param labels: List of labels that are used in the task + :return dict: annotation information dict in mmdet format + """ + width, height = dataset_item.width, dataset_item.height + + # load annotations for item + gt_bboxes = [] + gt_labels = [] + gt_polygons = [] + gt_ann_ids = [] + + label_idx = {label.id: i for i, label in enumerate(labels)} + + for annotation in dataset_item.get_annotations(labels=labels, include_empty=False, preserve_id=True): + box = ShapeFactory.shape_as_rectangle(annotation.shape) + + if min(box.width * width, box.height * height) < min_size: + continue + + class_indices = [ + label_idx[label.id] for label in annotation.get_labels(include_empty=False) if label.domain == domain + ] + + n = len(class_indices) + gt_bboxes.extend([[box.x1 * width, box.y1 * height, box.x2 * width, box.y2 * height] for _ in range(n)]) + if domain != Domain.DETECTION: + polygon = ShapeFactory.shape_as_polygon(annotation.shape) + polygon = np.array([p for point in polygon.points for p in [point.x * width, point.y * height]]) + gt_polygons.extend([[polygon] for _ in range(n)]) + gt_labels.extend(class_indices) + item_id = getattr(dataset_item, "id_", None) + gt_ann_ids.append((item_id, annotation.id_)) + + if len(gt_bboxes) > 0: + ann_info = dict( + bboxes=np.array(gt_bboxes, dtype=np.float32).reshape(-1, 4), + labels=np.array(gt_labels, dtype=int), + masks=PolygonMasks(gt_polygons, height=height, width=width) if gt_polygons else [], + ann_ids=gt_ann_ids, + ) + else: + ann_info = dict( + bboxes=np.zeros((0, 4), dtype=np.float32), + labels=np.array([], dtype=int), + masks=[], + ann_ids=[], + ) + return ann_info + + +@DATASETS.register_module() +class OTXDetDataset(CustomDataset): + """Wrapper that allows using a OTX dataset to train mmdetection models. + + This wrapper is not based on the filesystem, + but instead loads the items here directly from the OTX DatasetEntity object. + + The wrapper overwrites some methods of the CustomDataset class: prepare_train_img, prepare_test_img and prepipeline + Naming of certain attributes might seem a bit peculiar but this is due to the conventions set in CustomDataset. For + instance, CustomDatasets expects the dataset items to be stored in the attribute data_infos, which is why it is + named like that and not dataset_items. + + """ + + class _DataInfoProxy: + """This class is intended to be a wrapper to use it in CustomDataset-derived class as `self.data_infos`. + + Instead of using list `data_infos` as in CustomDataset, our implementation of dataset OTXDataset + uses this proxy class with overriden __len__ and __getitem__; this proxy class + forwards data access operations to otx_dataset and converts the dataset items to the view + convenient for mmdetection. + """ + + def __init__(self, otx_dataset, labels): + self.otx_dataset = otx_dataset + self.labels = labels + self.label_idx = {label.id: i for i, label in enumerate(labels)} + + def __len__(self): + return len(self.otx_dataset) + + def __getitem__(self, index): + """Prepare a dict 'data_info' that is expected by the mmdet pipeline to handle images and annotations. + + :return data_info: dictionary that contains the image and image metadata, as well as the labels of + the objects in the image + """ + + dataset = self.otx_dataset + item = dataset[index] + ignored_labels = np.array([self.label_idx[lbs.id] for lbs in item.ignored_labels]) + + height, width = item.height, item.width + + data_info = dict( + dataset_item=item, + width=width, + height=height, + index=index, + ann_info=dict(label_list=self.labels), + ignored_labels=ignored_labels, + ) + + return data_info + + def __init__( + self, + otx_dataset: DatasetEntity, + labels: List[LabelEntity], + pipeline: Sequence[dict], + test_mode: bool = False, + **kwargs, + ): + dataset_cfg = kwargs.copy() + _ = dataset_cfg.pop("org_type", None) + new_classes = dataset_cfg.pop("new_classes", []) + self.otx_dataset = otx_dataset + self.labels = labels + self.CLASSES = list(label.name for label in labels) + self.domain = self.labels[0].domain + self.test_mode = test_mode + + # Instead of using list data_infos as in CustomDataset, this implementation of dataset + # uses a proxy class with overriden __len__ and __getitem__; this proxy class + # forwards data access operations to otx_dataset. + # Note that list `data_infos` cannot be used here, since OTX dataset class does not have interface to + # get only annotation of a data item, so we would load the whole data item (including image) + # even if we need only checking aspect ratio of the image; due to it + # this implementation of dataset does not uses such tricks as skipping images with wrong aspect ratios or + # small image size, since otherwise reading the whole dataset during initialization will be required. + self.data_infos = OTXDetDataset._DataInfoProxy(otx_dataset, labels) + + self.proposals = None # Attribute expected by mmdet but not used for OTX datasets + + if not test_mode: + self._set_group_flag() + self.img_indices = get_old_new_img_indices(self.labels, new_classes, self.otx_dataset) + + self.pipeline = Compose(pipeline) + annotation = [self.get_ann_info(i) for i in range(len(self))] + self.evaluator = Evaluator(annotation, self.domain, self.CLASSES) + + def _set_group_flag(self): + """Set flag for grouping images. + + Originally, in Custom dataset, images with aspect ratio greater than 1 will be set as group 1, + otherwise group 0. + This implementation will set group 0 for every image. + """ + self.flag = np.zeros(len(self), dtype=np.uint8) + + def _rand_another(self, idx): + _ = idx + return np.random.choice(len(self)) + + def prepare_train_img(self, idx: int) -> dict: + """Get training data and annotations after pipeline. + + :param idx: int, Index of data. + :return dict: Training data and annotation after pipeline with new keys introduced by pipeline. + """ + item = copy(self.data_infos[idx]) # Copying dict(), not contents + self.pre_pipeline(item) + return self.pipeline(item) + + def prepare_test_img(self, idx: int) -> dict: + """Get testing data after pipeline. + + :param idx: int, Index of data. + :return dict: Testing data after pipeline with new keys introduced by pipeline. + """ + item = copy(self.data_infos[idx]) # Copying dict(), not contents + self.pre_pipeline(item) + return self.pipeline(item) + + @staticmethod + def pre_pipeline(results: Dict[str, Any]): + """Prepare results dict for pipeline. Add expected keys to the dict.""" + results["bbox_fields"] = [] + results["mask_fields"] = [] + results["seg_fields"] = [] + + def get_ann_info(self, idx: int): + """This method is used for evaluation of predictions. + + The CustomDataset class implements a method + CustomDataset.evaluate, which uses the class method get_ann_info to retrieve annotations. + + :param idx: index of the dataset item for which to get the annotations + :return ann_info: dict that contains the coordinates of the bboxes and their corresponding labels + """ + dataset_item = self.otx_dataset[idx] + labels = self.labels + return get_annotation_mmdet_format(dataset_item, labels, self.domain) + + def evaluate( # pylint: disable=too-many-branches + self, + results, + metric="mAP", + logger=None, + proposal_nums=(100, 300, 1000), + iou_thr=0.5, + scale_ranges=None, + ): + """Evaluate the dataset. + + Args: + results (list): Testing results of the dataset. + metric (str | list[str]): Metrics to be evaluated. + logger (logging.Logger | None | str): Logger used for printing + related information during evaluation. Default: None. + proposal_nums (Sequence[int]): Proposal number used for evaluating + recalls, such as recall@100, recall@1000. + Default: (100, 300, 1000). + iou_thr (float | list[float]): IoU threshold. Default: 0.5. + scale_ranges (list[tuple] | None): Scale ranges for evaluating mAP. + Default: None. + """ + allowed_metrics = ["mAP"] + eval_results = OrderedDict() + if metric not in allowed_metrics: + raise KeyError(f"metric {metric} is not supported") + iou_thrs = [iou_thr] if isinstance(iou_thr, float) else iou_thr + assert isinstance(iou_thrs, list) + mean_aps = [] + for iou_thr in iou_thrs: # pylint: disable=redefined-argument-from-local + print_log(f'\n{"-" * 15}iou_thr: {iou_thr}{"-" * 15}', logger) + mean_ap, _ = self.evaluator.evaluate(results, logger, iou_thr, scale_ranges) + mean_aps.append(mean_ap) + eval_results[f"AP{int(iou_thr * 100):02d}"] = round(mean_ap, 3) + eval_results["mAP"] = sum(mean_aps) / len(mean_aps) + return eval_results + + +# pylint: disable=too-many-arguments +@DATASETS.register_module() +class ImageTilingDataset(OTXDetDataset): + """A wrapper of tiling dataset. + + Suitable for training small object dataset. This wrapper composed of `Tile` + that crops an image into tiles and merges tile-level predictions to + image-level prediction for evaluation. + + Args: + dataset (Config): The dataset to be tiled. + pipeline (List): Sequence of transform object or + config dict to be composed. + tile_size (int): the length of side of each tile + min_area_ratio (float, optional): The minimum overlap area ratio + between a tiled image and its annotations. Ground-truth box is + discarded if the overlap area is less than this value. + Defaults to 0.8. + overlap_ratio (float, optional): ratio of each tile to overlap with + each of the tiles in its 4-neighborhood. Defaults to 0.2. + iou_threshold (float, optional): IoU threshold to be used to suppress + boxes in tiles' overlap areas. Defaults to 0.45. + max_per_img (int, optional): if there are more than max_per_img bboxes + after NMS, only top max_per_img will be kept. Defaults to 200. + max_annotation (int, optional): Limit the number of ground truth by + randomly select 5000 due to RAM OOM. Defaults to 5000. + sampling_ratio (flaot): Ratio for sampling entire tile dataset. + include_full_img (bool): Whether to include full image in the dataset. + """ + + def __init__( + self, + dataset: Config, + pipeline: List[dict], + tile_size: int, + min_area_ratio=0.8, + overlap_ratio=0.2, + iou_threshold=0.45, + max_per_img=200, + max_annotation=5000, + filter_empty_gt=True, + test_mode=False, + sampling_ratio=1.0, + include_full_img=False, + ): + self.dataset = build_dataset(dataset) + self.CLASSES = self.dataset.CLASSES + data_subset = self.dataset.otx_dataset[0].subset + self.tile_dataset = Tile( + self.dataset, + pipeline, + tile_size=tile_size, + overlap=overlap_ratio, + min_area_ratio=min_area_ratio, + iou_threshold=iou_threshold, + max_per_img=max_per_img, + max_annotation=max_annotation, + filter_empty_gt=filter_empty_gt if data_subset != Subset.TESTING else False, + sampling_ratio=sampling_ratio if data_subset != Subset.TESTING else 1.0, + include_full_img=include_full_img if data_subset != Subset.TESTING else True, + ) + self.flag = np.zeros(len(self), dtype=np.uint8) + self.pipeline = Compose(pipeline) + self.test_mode = test_mode + self.num_samples = len(self.dataset) # number of original samples + annotation = [self.get_ann_info(i) for i in range(len(self))] + self.evaluator = Evaluator(annotation, self.dataset.domain, self.CLASSES) + + def __len__(self) -> int: + """Get the length of the dataset.""" + return len(self.tile_dataset) + + def __getitem__(self, idx: int) -> Dict: + """Get training/test tile. + + Args: + idx (int): Index of data. + + Returns: + dict: Training/test data (with annotation if `test_mode` is set + True). + """ + return self.pipeline(self.tile_dataset[idx]) + + def get_ann_info(self, idx): + """Get annotation information of a tile. + + Args: + idx (int): Index of data. + + Returns: + dict: Annotation information of a tile. + """ + return self.tile_dataset.get_ann_info(idx) + + def merge(self, results) -> Union[List[Tuple[np.ndarray, list]], List[np.ndarray]]: + """Merge tile-level results to image-level results. + + Args: + results: tile-level results. + + Returns: + merged_results (list[list | tuple]): Merged results of the dataset. + """ + return self.tile_dataset.merge(results) + + def merge_vectors(self, feature_vectors: List[np.ndarray], dump_vectors: bool) -> Union[np.ndarray, List[None]]: + """Merge tile-level feature vectors to image-level feature-vector. + + Args: + feature_vectors (list[np.ndarray]): tile-level feature vectors. + dump_vectors (bool): whether to dump vectors. + + Returns: + merged_vectors (np.ndarray | List[None]): Merged vector for each image. + """ + + if dump_vectors: + return self.tile_dataset.merge_vectors(feature_vectors) + else: + return [None] * self.num_samples + + def merge_maps(self, saliency_maps: List, dump_maps: bool) -> List: + """Merge tile-level saliency maps to image-level saliency map. + + Args: + saliency_maps (list[list | np.ndarray]): tile-level saliency maps. + dump_maps (bool): whether to dump saliency maps. + + Returns: + merged_maps (List[list | np.ndarray | None]): Merged saliency map for each image. + """ + + if dump_maps: + return self.tile_dataset.merge_maps(saliency_maps) + else: + return [None] * self.num_samples + + def __del__(self): + """Delete the temporary directory when the object is deleted.""" + if getattr(self, "tmp_dir", False): + self.tmp_dir.cleanup() diff --git a/src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/__init__.py new file mode 100644 index 00000000000..55efa8bd7e8 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/__init__.py @@ -0,0 +1,38 @@ +"""Initial file for mmdetection hooks.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .load_pipelines import ( + LoadAnnotationFromOTXDataset, + LoadImageFromOTXDataset, + LoadResizeDataFromOTXDataset, + ResizeTo, +) +from .torchvision2mmdet import ( + BranchImage, + ColorJitter, + NDArrayToPILImage, + NDArrayToTensor, + PILImageToNDArray, + RandomApply, + RandomErasing, + RandomGaussianBlur, + RandomGrayscale, +) + +__all__ = [ + "LoadImageFromOTXDataset", + "LoadAnnotationFromOTXDataset", + "LoadResizeDataFromOTXDataset", + "ResizeTo", + "ColorJitter", + "RandomGrayscale", + "RandomErasing", + "RandomGaussianBlur", + "RandomApply", + "NDArrayToTensor", + "NDArrayToPILImage", + "PILImageToNDArray", + "BranchImage", +] diff --git a/src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/load_pipelines.py b/src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/load_pipelines.py new file mode 100644 index 00000000000..3eda94767e3 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/load_pipelines.py @@ -0,0 +1,133 @@ +"""Collection Pipeline for detection task.""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import copy +from typing import Any, Dict, Optional + +from mmdet.datasets.builder import PIPELINES, build_from_cfg +from mmdet.datasets.pipelines import Resize + +import otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset as load_image_base +from otx.algorithms.detection.adapters.mmdet.datasets.dataset import ( + get_annotation_mmdet_format, +) +from otx.api.entities.label import Domain + + +# pylint: disable=too-many-instance-attributes, too-many-arguments +@PIPELINES.register_module() +class LoadImageFromOTXDataset(load_image_base.LoadImageFromOTXDataset): + """Pipeline element that loads an image from a OTX Dataset on the fly.""" + + +@PIPELINES.register_module() +class LoadResizeDataFromOTXDataset(load_image_base.LoadResizeDataFromOTXDataset): + """Load and resize image & annotation with cache support.""" + + def _create_load_ann_op(self, cfg: Optional[Dict]) -> Optional[Any]: + """Creates resize operation.""" + if cfg is None: + return None + return build_from_cfg(cfg, PIPELINES) + + def _create_resize_op(self, cfg: Optional[Dict]) -> Optional[Any]: + """Creates resize operation.""" + if cfg is None: + return None + return build_from_cfg(cfg, PIPELINES) + + +@PIPELINES.register_module() +class ResizeTo(Resize): + """Resize to specific size. + + This operation works if the input is not in desired shape. + If it's already in the shape, it just returns input dict for efficiency. + + Args: + img_scale (tuple): Images scales for resizing (w, h). + """ + + def __init__(self, **kwargs): + super().__init__(override=True, **kwargs) # Allow multiple calls + + def __call__(self, results: Dict[str, Any]): + """Callback function of ResizeTo. + + Args: + results: Inputs to be transformed. + """ + img_shape = results.get("img_shape", (0, 0)) + img_scale = self.img_scale[0] + if img_shape[0] == img_scale[0] and img_shape[1] == img_scale[1]: + return results + return super().__call__(results) + + +@PIPELINES.register_module() +class LoadAnnotationFromOTXDataset: + """Pipeline element that loads an annotation from a OTX Dataset on the fly. + + Expected entries in the 'results' dict that should be passed to this pipeline element are: + results['dataset_item']: dataset_item from which to load the annotation + results['ann_info']['label_list']: list of all labels in the project + """ + + def __init__( + self, + min_size: int = -1, + with_bbox: bool = True, + with_label: bool = True, + with_mask: bool = False, + with_seg: bool = False, + poly2mask: bool = True, + with_text: bool = False, + domain: str = "detection", + ): + self._domain_dict = { + "detection": Domain.DETECTION, + "instance_segmentation": Domain.INSTANCE_SEGMENTATION, + "rotated_detection": Domain.ROTATED_DETECTION, + } + self.with_bbox = with_bbox + self.with_label = with_label + self.with_mask = with_mask + self.with_seg = with_seg + self.poly2mask = poly2mask + self.with_text = with_text + self.domain = self._domain_dict[domain.lower()] + self.min_size = min_size + + @staticmethod + def _load_bboxes(results, ann_info): + results["bbox_fields"].append("gt_bboxes") + results["gt_bboxes"] = copy.deepcopy(ann_info["bboxes"]) + results["gt_ann_ids"] = copy.deepcopy(ann_info["ann_ids"]) + return results + + @staticmethod + def _load_labels(results, ann_info): + results["gt_labels"] = copy.deepcopy(ann_info["labels"]) + return results + + @staticmethod + def _load_masks(results, ann_info): + results["mask_fields"].append("gt_masks") + results["gt_masks"] = copy.deepcopy(ann_info["masks"]) + return results + + def __call__(self, results: Dict[str, Any]): + """Callback function of LoadAnnotationFromOTXDataset.""" + dataset_item = results.pop("dataset_item") # Prevent unnecessary deepcopy + label_list = results.pop("ann_info")["label_list"] + ann_info = get_annotation_mmdet_format(dataset_item, label_list, self.domain, self.min_size) + if self.with_bbox: + results = self._load_bboxes(results, ann_info) + if results is None or len(results["gt_bboxes"]) == 0: + return None + if self.with_label: + results = self._load_labels(results, ann_info) + if self.with_mask: + results = self._load_masks(results, ann_info) + return results diff --git a/src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/torchvision2mmdet.py b/src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/torchvision2mmdet.py new file mode 100644 index 00000000000..0f7e419407b --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/datasets/pipelines/torchvision2mmdet.py @@ -0,0 +1,181 @@ +"""Torchvision transforms to MMDetection pipeline.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +from mmcv.utils import build_from_cfg +from mmdet.datasets import PIPELINES +from mmdet.datasets.pipelines.formatting import ImageToTensor, to_tensor +from PIL import Image, ImageFilter +from torchvision import transforms as T + + +@PIPELINES.register_module() +class ColorJitter(T.ColorJitter): + """MMDet adapter.""" + + def __init__(self, key_maps=None, **kwargs): + super().__init__(**kwargs) + key_maps = key_maps if key_maps else list([("img", "img")]) + self.key_maps = key_maps + + def forward(self, img): + """Forward function of ColorJitter.""" + outputs = img.copy() + for key_map in self.key_maps: + outputs[key_map[0]] = super().forward(img[key_map[1]]) + return outputs + + +@PIPELINES.register_module() +class RandomGrayscale(T.RandomGrayscale): + """MMDet adapter.""" + + def __init__(self, key_maps=None, **kwargs): + super().__init__(**kwargs) + key_maps = key_maps if key_maps else list([("img", "img")]) + self.key_maps = key_maps + + def forward(self, img): + """Forward function of RandomGrayscale.""" + outputs = img.copy() + for key_map in self.key_maps: + outputs[key_map[0]] = super().forward(img[key_map[1]]) + return outputs + + +@PIPELINES.register_module() +class RandomErasing(T.RandomErasing): + """MMDet adapter.""" + + def __init__(self, key_maps=None, **kwargs): + super().__init__(**kwargs) + key_maps = key_maps if key_maps else list([("img", "img")]) + self.key_maps = key_maps + + def forward(self, img): + """Forward function of RandomErasing.""" + outputs = img.copy() + for key_map in self.key_maps: + outputs[key_map[0]] = super().forward(img[key_map[1]]) + return outputs + + +@PIPELINES.register_module() +class RandomGaussianBlur: + """Gaussian blur augmentation in SimCLR https://arxiv.org/abs/2002.05709.""" + + def __init__(self, sigma_min, sigma_max, key_maps=None): + self.sigma_min = sigma_min + self.sigma_max = sigma_max + + self.key_maps = key_maps if key_maps else list([("img", "img")]) + + def __call__(self, inputs): + """Call function of RandomGaussianBlur.""" + outputs = inputs.copy() + sigma = np.random.uniform(self.sigma_min, self.sigma_max) + # ksize = 2*int(np.ceil(2.0*sigma)) + 1 + for key_map in self.key_maps: + img = inputs[key_map[0]] + img = img.filter(ImageFilter.GaussianBlur(radius=sigma)) + # img = cv.GaussianBlur(img, ksize=(0,0), sigmaX=sigma) + # img = F.gaussian_blur(img, ksize, [sigma, sigma]) + outputs[key_map[1]] = img + return outputs + + def __repr__(self): + """Repr function of RandomGaussianBlur.""" + repr_str = self.__class__.__name__ + return repr_str + + +@PIPELINES.register_module() +class RandomApply(T.RandomApply): + """MMDet adapter.""" + + def __init__(self, transform_cfgs, p=0.5): + transforms = [] + for transform_cfg in transform_cfgs: + transforms.append(build_from_cfg(transform_cfg, PIPELINES)) + super().__init__(transforms, p=p) + + +@PIPELINES.register_module() +class NDArrayToTensor(ImageToTensor): + """MMDet adapter.""" + + def __call__(self, results): + """Call function of NDArrayToTensor.""" + for key in self.keys: + img = results[key] + if len(img.shape) < 3: + img = np.expand_dims(img, -1) + img = np.ascontiguousarray(img.transpose(2, 0, 1)) + results[key] = to_tensor(img) + return results + + +@PIPELINES.register_module() +class NDArrayToPILImage: + """NDArrayToPILImage.""" + + def __init__(self, keys=None): + self.keys = keys if keys else list(["img"]) + + def __call__(self, results): + """Call function of NDArrayToPILImage.""" + for key in self.keys: + img = results[key] + img = Image.fromarray(img, mode="RGB") + results[key] = img + return results + + def __repr__(self): + """Repr function of NDArrayToPILImage.""" + repr_str = self.__class__.__name__ + return repr_str + + +@PIPELINES.register_module() +class PILImageToNDArray: + """PILImageToNDArray.""" + + def __init__(self, keys=None): + self.keys = keys if keys else list(["img"]) + + def __call__(self, results): + """Call function of PILImageToNDArray.""" + for key in self.keys: + img = results[key] + img = np.asarray(img) + results[key] = img + return results + + def __repr__(self): + """Repr function of PILImageToNDArray.""" + repr_str = self.__class__.__name__ + return repr_str + + +@PIPELINES.register_module() +class BranchImage: + """BranchImage.""" + + def __init__(self, key_map=None): + self.key_map = key_map if key_map else dict() + + def __call__(self, results): + """Call function of BranchImage.""" + for key_1, key_2 in self.key_map.items(): + if key_1 in results: + results[key_2] = results[key_1] + if key_1 in results["img_fields"]: + results["img_fields"].append(key_2) + return results + + def __repr__(self): + """Repr function of BranchImage.""" + repr_str = self.__class__.__name__ + return repr_str diff --git a/src/otx/algorithms/detection/adapters/mmdet/datasets/task_adapt_dataset.py b/src/otx/algorithms/detection/adapters/mmdet/datasets/task_adapt_dataset.py new file mode 100644 index 00000000000..abd350b61ba --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/datasets/task_adapt_dataset.py @@ -0,0 +1,85 @@ +"""Task Adapt Dataset for detection task.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# import torch +import numpy as np +from mmdet.datasets import DATASETS, PIPELINES, build_dataset + +from otx.algorithms.common.utils.task_adapt import ( + map_cat_and_cls_as_order, + map_class_names, +) + + +# pylint: disable=invalid-name +@DATASETS.register_module() +class TaskAdaptEvalDataset: + """Dataset wrapper for task-adaptive evaluation. + + Attributes: + model_classes: + **kwargs: + + Notes: + TODO[Eugene/Jaeguk]: check if this class is still valid in OTX + """ + + def __init__(self, model_classes, **kwargs): + dataset_cfg = kwargs.copy() + org_type = dataset_cfg.pop("org_type") + dataset_cfg["type"] = org_type + self.dataset = build_dataset(dataset_cfg) + self.model_classes = model_classes + self.CLASSES = self.dataset.CLASSES + self.data2model = map_class_names(self.CLASSES, self.model_classes) + if org_type == "CocoDataset": + self.dataset.cat2label, self.dataset.cat_ids = map_cat_and_cls_as_order( + self.CLASSES, self.dataset.coco.cats + ) + + def __getitem__(self, idx): + """Get item from TaskAdaptEvalDataset.""" + return self.dataset[idx] + + def __len__(self): + """Length of TaskAdaptEvalDataset.""" + return len(self.dataset) + + def evaluate(self, results, **kwargs): + """Filter & reorder detection results.""" + adapt_results = [] + for result in results: # for each image + adapt_result = [] + for model_class_index in self.data2model: # for each class + # Gather per-class results according to index mapping + if model_class_index >= 0: + adapt_result.append(result[model_class_index]) + else: + adapt_result.append(np.empty([0, 5])) + adapt_results.append(adapt_result) + + # Call evaluation w/ org arguments + return self.dataset.evaluate(adapt_results, **kwargs) + + +@PIPELINES.register_module() +class AdaptClassLabels: + """Data processor for task-adative annotation loading.""" + + def __init__(self, src_classes, dst_classes): + self.src2dst = map_class_names(src_classes, dst_classes) + print("AdaptClassLabels") + print("src_classes", src_classes) + print("dst_classes", dst_classes) + print("src2dst", self.src2dst) + + def __call__(self, data): + """Call function of AdaptClassLabels.""" + src_labels = data["gt_labels"] + dst_labels = [] + for src_label in src_labels: + dst_labels.append(self.src2dst[src_label]) + data["gt_labels"] = np.array(dst_labels) + return data diff --git a/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py b/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py new file mode 100644 index 00000000000..79e0bfd5b53 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py @@ -0,0 +1,591 @@ +"""Tiling for detection and instance segmentation task.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import uuid +from itertools import product +from random import sample +from time import time +from typing import Callable, Dict, List, Tuple, Union + +import cv2 +import numpy as np +from mmcv.ops import nms +from mmdet.core import BitmapMasks, bbox2result +from tqdm import tqdm + +from otx.api.utils.dataset_utils import non_linear_normalization + + +def timeit(func) -> Callable: + """Decorator to measure time of function execution. + + Args: + func: Function to be the target for measuring. + + Returns: + Callable function with time measurement. + """ + + def wrapper(*args, **kwargs): + begin = time() + result = func(*args, **kwargs) + print(f"\n==== {func.__name__}: {time() - begin} sec ====\n") + return result + + return wrapper + + +# pylint: disable=too-many-instance-attributes, too-many-arguments +class Tile: + """Tile and merge datasets. + + Args: + dataset (CustomDataset): the dataset to be tiled. + tile_size (int): the length of side of each tile. Defaults to 400 + overlap (float, optional): ratio of each tile to overlap with each of + the tiles in its 4-neighborhood. Defaults to 0.2. + min_area_ratio (float, optional): the minimum overlap area ratio + between a tiled image and its annotations. Ground-truth box is + discarded if the overlap area is less than this value. + Defaults to 0.9. + iou_threshold (float, optional): IoU threshold to be used to suppress + boxes in tiles' overlap areas. Defaults to 0.45. + max_per_img (int, optional): if there are more than max_per_img bboxes + after NMS, only top max_per_img will be kept. Defaults to 1500. + max_annotation(int, optional): Limit the number of ground truth by + randomly select 5000 due to RAM OOM. + filter_empty_gt (bool, optional): If set true, images without bounding + boxes of the dataset's classes will be filtered out. This option + only works when `test_mode=False`, i.e., we never filter images + during tests. Defaults to True. + nproc (int, optional): Processes used for processing masks. Default: 4. + sampling_ratio (float): Ratio for sampling entire tile dataset. Default: 1.0.(No sample) + include_full_img (bool): Whether to include full-size image for inference or training. Default: False. + """ + + def __init__( + self, + dataset, + pipeline, + tile_size: int = 400, + overlap: float = 0.2, + min_area_ratio: float = 0.9, + iou_threshold: float = 0.45, + max_per_img: int = 1500, + max_annotation: int = 2000, + filter_empty_gt: bool = True, + nproc: int = 2, + sampling_ratio: float = 1.0, + include_full_img: bool = False, + ): + self.min_area_ratio = min_area_ratio + self.filter_empty_gt = filter_empty_gt + self.iou_threshold = iou_threshold + self.max_per_img = max_per_img + self.max_annotation = max_annotation + self.tile_size = tile_size + self.overlap = overlap + self.stride = int(tile_size * (1 - overlap)) + self.num_images = len(dataset) + self.num_classes = len(dataset.CLASSES) + self.CLASSES = dataset.CLASSES # pylint: disable=invalid-name + self.nproc = nproc + self.img2fp32 = False + for p in pipeline: + if p.type == "PhotoMetricDistortion": + self.img2fp32 = True + break + + self.dataset = dataset + self.tiles_all, self.cached_results = self.gen_tile_ann(include_full_img) + self.sample_num = max(int(len(self.tiles_all) * sampling_ratio), 1) + if sampling_ratio < 1.0: + self.tiles = sample(self.tiles_all, self.sample_num) + else: + self.tiles = self.tiles_all + + @timeit + def gen_tile_ann(self, include_full_img) -> Tuple[List[Dict], List[Dict]]: + """Generate tile annotations and cache the original image-level annotations. + + Returns: + tiles: a list of tile annotations with some other useful information for data pipeline. + cache_result: a list of original image-level annotations. + include_full_img: whether to include full-size image for inference or training. + """ + tiles = [] + cache_result = [] + for result in tqdm(self.dataset, desc="Loading dataset annotations..."): + cache_result.append(result) + + pbar = tqdm(total=len(self.dataset) * 2, desc="Generating tile annotations...") + for idx, result in enumerate(cache_result): + if include_full_img: + tiles.append(self.gen_single_img(result, dataset_idx=idx)) + pbar.update(1) + + for idx, result in enumerate(cache_result): + tiles.extend(self.gen_tiles_single_img(result, dataset_idx=idx)) + pbar.update(1) + return tiles, cache_result + + def random_select_gt(self, result: Dict, num: int): + """Randomly select ground truth masks for each image. + + Limit the number of ground truth masks by randomly select `num` due to RAM OOM for Instance Segmentation task. + + Args: + result (Dict): the original image-level result (i.e. the original image annotation) + num (int): the number of ground truth masks to be selected + """ + + if "gt_masks" in result and len(result["gt_masks"]) > num: + indices = np.random.choice(len(result["gt_bboxes"]), size=num, replace=False) + result["gt_bboxes"] = result["gt_bboxes"][indices] + result["gt_labels"] = result["gt_labels"][indices] + result["gt_masks"] = result["gt_masks"][indices] + + def gen_single_img(self, result: Dict, dataset_idx: int) -> Dict: + """Add full-size image for inference or training. + + Args: + result (Dict): the original image-level result (i.e. the original image annotation) + dataset_idx (int): the image index this tile belongs to + + Returns: + Dict: annotation with some other useful information for data pipeline. + """ + self.random_select_gt(result, self.max_annotation) + result["full_res_image"] = True + result["tile_box"] = (0, 0, result["img_shape"][1], result["img_shape"][0]) + result["dataset_idx"] = dataset_idx + result["original_shape_"] = result["img_shape"] + result["uuid"] = str(uuid.uuid4()) + result["gt_bboxes"] = result["gt_bboxes"] if "gt_bboxes" in result else np.zeros((0, 4), dtype=np.float32) + result["gt_labels"] = result["gt_labels"] if "gt_labels" in result else np.array([], dtype=int) + result["gt_masks"] = result["gt_masks"] if "gt_masks" in result else [] + return result + + # pylint: disable=too-many-locals + def gen_tiles_single_img(self, result: Dict, dataset_idx: int) -> List[Dict]: + """Generate tile annotation for a single image. + + Args: + result (Dict): the original image-level result (i.e. the original image annotation) + dataset_idx (int): the image index this tile belongs to + + Returns: + List[Dict]: a list of tile annotation with some other useful information for data pipeline. + """ + tile_list = [] + self.random_select_gt(result, self.max_annotation) + gt_bboxes = result.get("gt_bboxes", np.zeros((0, 4), dtype=np.float32)) + gt_masks = result.get("gt_masks", None) + gt_bboxes_ignore = result.get("gt_bboxes_ignore", np.zeros((0, 4), dtype=np.float32)) + gt_labels = result.get("gt_labels", np.array([], dtype=np.int64)) + img_shape = result.get("img_shape") + height, width = img_shape[:2] + _tile = self.prepare_result(result) + + num_patches_h = (height + self.stride - 1) // self.stride + num_patches_w = (width + self.stride - 1) // self.stride + for (_, _), (loc_i, loc_j) in zip( + product(range(num_patches_h), range(num_patches_w)), + product( + range(0, height, self.stride), + range(0, width, self.stride), + ), + ): + x_1 = loc_j + x_2 = min(loc_j + self.tile_size, width) + y_1 = loc_i + y_2 = min(loc_i + self.tile_size, height) + tile = copy.deepcopy(_tile) + tile["full_res_image"] = False + tile["original_shape_"] = img_shape + tile["ori_shape"] = (y_2 - y_1, x_2 - x_1, 3) + tile["img_shape"] = tile["ori_shape"] + tile["tile_box"] = (x_1, y_1, x_2, y_2) + tile["dataset_idx"] = dataset_idx + tile["gt_bboxes_ignore"] = gt_bboxes_ignore + tile["uuid"] = str(uuid.uuid4()) + self.tile_ann_assignment(tile, np.array([[x_1, y_1, x_2, y_2]]), gt_bboxes, gt_masks, gt_labels) + # filter empty ground truth + if self.filter_empty_gt and len(tile["gt_labels"]) == 0: + continue + tile_list.append(tile) + if dataset_idx == 0: + print(f"image: {height}x{width} ~ tile_size: {self.tile_size}") + print(f"{num_patches_h}x{num_patches_w} tiles -> {len(tile_list)} tiles after filtering") + return tile_list + + def prepare_result(self, result: Dict) -> Dict: + """Prepare results dict for pipeline. + + Args: + result (Dict): original image-level result for a tile + + Returns: + Dict: result template with useful information for data pipeline. + """ + result_template = dict( + ori_filename=result["ori_filename"], + filename=result["filename"], + bbox_fields=result["bbox_fields"], + mask_fields=result["mask_fields"], + seg_fields=result["seg_fields"], + img_fields=result["img_fields"], + ) + return result_template + + def tile_ann_assignment( + self, + tile_result: Dict, + tile_box: np.ndarray, + gt_bboxes: np.ndarray, + gt_masks: BitmapMasks, + gt_labels: np.ndarray, + ): + """Assign new annotation to this tile. + + Ground-truth is discarded if the overlap with this tile is lower than + min_area_ratio. + + Args: + tile_result (Dict): the tile-level result (i.e. the tile annotation) + tile_box (np.ndarray): the coordinate for this tile box (i.e. the tile coordinate relative to the image) + gt_bboxes (np.ndarray): the original image-level boxes + gt_masks (BitmapMasks): the original image-level masks + gt_labels (np.ndarray): the original image-level labels + """ + x_1, y_1 = tile_box[0][:2] + matched_indices = self.tile_boxes_overlap(tile_box, gt_bboxes) + + if len(matched_indices): + tile_lables = gt_labels[matched_indices][:] + tile_bboxes = gt_bboxes[matched_indices][:] + tile_bboxes[:, 0] -= x_1 + tile_bboxes[:, 1] -= y_1 + tile_bboxes[:, 2] -= x_1 + tile_bboxes[:, 3] -= y_1 + tile_bboxes[:, 0] = np.maximum(0, tile_bboxes[:, 0]) + tile_bboxes[:, 1] = np.maximum(0, tile_bboxes[:, 1]) + tile_bboxes[:, 2] = np.minimum(self.tile_size, tile_bboxes[:, 2]) + tile_bboxes[:, 3] = np.minimum(self.tile_size, tile_bboxes[:, 3]) + tile_result["gt_bboxes"] = tile_bboxes + tile_result["gt_labels"] = tile_lables + tile_result["gt_masks"] = gt_masks[matched_indices].crop(tile_box[0]) if gt_masks is not None else [] + else: + tile_result.pop("bbox_fields") + tile_result.pop("mask_fields") + tile_result.pop("seg_fields") + tile_result.pop("img_fields") + tile_result["gt_bboxes"] = np.zeros((0, 4), dtype=np.float32) + tile_result["gt_labels"] = np.array([], dtype=int) + tile_result["gt_masks"] = [] + + if gt_masks is None: + tile_result.pop("gt_masks") + + def tile_boxes_overlap(self, tile_box: np.ndarray, boxes: np.ndarray) -> np.ndarray: + """Compute overlapping ratio over boxes. + + Args: + tile_box (np.ndarray): box in shape (1, 4). + boxes (np.ndarray): boxes in shape (N, 4). + + Returns: + np.ndarray: matched indices. + """ + x1, y1, x2, y2 = tile_box[0] + match_indices = (boxes[:, 0] > x1) & (boxes[:, 1] > y1) & (boxes[:, 2] < x2) & (boxes[:, 3] < y2) + match_indices = np.argwhere(match_indices == 1).flatten() + return match_indices + + def multiclass_nms( + self, boxes: np.ndarray, scores: np.ndarray, idxs: np.ndarray, iou_threshold: float, max_num: int + ): + """NMS for multi-class bboxes. + + Args: + boxes (np.ndarray): boxes in shape (N, 4). + scores (np.ndarray): scores in shape (N, ). + idxs (np.ndarray): each index value correspond to a bbox cluster, + and NMS will not be applied between elements of different idxs, + shape (N, ). + iou_threshold (float): IoU threshold to be used to suppress boxes + in tiles' overlap areas. + max_num (int): if there are more than max_per_img bboxes after + NMS, only top max_per_img will be kept. + + Returns: + tuple: tuple: kept dets and indice. + """ + if len(boxes) == 0: + return None, [] + max_coordinate = boxes.max() + offsets = idxs.astype(boxes.dtype) * (max_coordinate + 1) + boxes_for_nms = boxes + offsets[:, None] + dets, keep = nms(boxes_for_nms, scores, iou_threshold) + if max_num > 0: + dets = dets[:max_num] + keep = keep[:max_num] + return dets, keep + + def tile_nms( + self, + bbox_results: List[np.ndarray], + mask_results: List[List], + label_results: List[np.ndarray], + iou_threshold: float, + max_per_img: int, + detection: bool, + ): + """NMS after aggregation suppressing duplicate boxes in tile-overlap areas. + + Args: + bbox_results (List[List]): image-level box prediction + mask_results (List[np.ndarray]): image-level mask prediction + label_results (List[List]): image-level label prediction + iou_threshold (float): IoU threshold to be used to suppress boxes in tiles' overlap areas. + max_per_img (int): if there are more than max_per_img bboxes after NMS, only top max_per_img will be kept. + detection (bool): whether it is a detection task + """ + assert len(bbox_results) == len(mask_results) == len(label_results) + for i, result in enumerate(zip(bbox_results, mask_results, label_results)): + score_bboxes, masks, labels = result + bboxes = score_bboxes[:, :4] + scores = np.ascontiguousarray(score_bboxes[:, 4]) + _, keep_indices = self.multiclass_nms( + bboxes, scores, labels, iou_threshold=iou_threshold, max_num=max_per_img + ) + + bboxes = bboxes[keep_indices] + labels = labels[keep_indices] + scores = scores[keep_indices] + bbox_results[i] = bbox2result(np.concatenate([bboxes, scores[:, None]], -1), labels, self.num_classes) + + if not detection: + masks = np.array([masks[keep_idx] for keep_idx in keep_indices]) + mask_results[i] = [list(masks[labels == i]) for i in range(self.num_classes)] + + def __len__(self): + """Total number of tiles.""" + return len(self.tiles) + + def __getitem__(self, idx): + """Get training/test tile. + + Args: + idx (int): Index of data. + + Returns: + dict: Training/test data. + """ + result = copy.deepcopy(self.tiles[idx]) + dataset_idx = result["dataset_idx"] + x_1, y_1, x_2, y_2 = result["tile_box"] + ori_img = self.cached_results[dataset_idx]["img"] + cropped_tile = ori_img[y_1:y_2, x_1:x_2, :] + if self.img2fp32: + cropped_tile = cropped_tile.astype(np.float32) + result["img"] = cropped_tile + return result + + # pylint: disable=too-many-locals + @timeit + def merge(self, results: List[List]) -> Union[List[Tuple[np.ndarray, list]], List[np.ndarray]]: + """Merge/Aggregate tile-level prediction to image-level prediction. + + Args: + results (list[list | tuple]): Testing tile results of the dataset. + + Returns: + List[List]: Testing image results of the dataset. + """ + assert len(results) == len(self.tiles) + + detection = False + if isinstance(results[0], tuple): + num_classes = len(results[0][0]) + dtype = results[0][0][0].dtype + elif isinstance(results[0], list): + detection = True + num_classes = len(results[0]) + dtype = results[0][0].dtype + else: + raise RuntimeError("Unknown data type") + + merged_bbox_results: List[np.ndarray] = [np.empty((0, 5), dtype=dtype) for _ in range(self.num_images)] + merged_mask_results: List[List] = [[] for _ in range(self.num_images)] + merged_label_results: List[Union[List, np.ndarray]] = [np.array([]) for _ in range(self.num_images)] + + for result, tile in zip(results, self.tiles): + tile_x1, tile_y1, _, _ = tile["tile_box"] + img_idx = tile["dataset_idx"] + img_h, img_w, _ = tile["original_shape_"] + + mask_result: List[List] = [[] for _ in range(num_classes)] + if isinstance(result, tuple): + bbox_result, mask_result = result + else: + bbox_result = result + + for cls_idx, cls_result in enumerate(zip(bbox_result, mask_result)): + cls_bbox_result, cls_mask_result = cls_result + _tmp_cls_bbox_result = np.zeros_like(cls_bbox_result) + _tmp_cls_bbox_result[:, 0] = cls_bbox_result[:, 0] + tile_x1 + _tmp_cls_bbox_result[:, 1] = cls_bbox_result[:, 1] + tile_y1 + _tmp_cls_bbox_result[:, 2] = cls_bbox_result[:, 2] + tile_x1 + _tmp_cls_bbox_result[:, 3] = cls_bbox_result[:, 3] + tile_y1 + _tmp_cls_bbox_result[:, 4] = cls_bbox_result[:, 4] + + merged_bbox_results[img_idx] = np.concatenate((merged_bbox_results[img_idx], _tmp_cls_bbox_result)) + merged_label_results[img_idx] = np.concatenate( + [merged_label_results[img_idx], len(cls_bbox_result) * [cls_idx]] + ) + + for cls_mask_dict in cls_mask_result: + cls_mask_dict.update(dict(tile_box=tile["tile_box"], img_size=(img_h, img_w))) + merged_mask_results[img_idx] += cls_mask_result + + # run NMS after aggregation suppressing duplicate boxes in + # overlapping areas + self.tile_nms( + merged_bbox_results, + merged_mask_results, + merged_label_results, + iou_threshold=self.iou_threshold, + max_per_img=self.max_per_img, + detection=detection, + ) + + assert len(merged_bbox_results) == len(merged_mask_results) + if detection: + return list(merged_bbox_results) + return list(zip(merged_bbox_results, merged_mask_results)) + + def get_ann_info(self, idx): + """Get annotation by index. + + Args: + idx (int): Index of data. + + Returns: + dict: Annotation info of specified index. + """ + ann = {} + if "gt_bboxes" in self.tiles[idx]: + ann["bboxes"] = self.tiles[idx]["gt_bboxes"] + if "gt_masks" in self.tiles[idx]: + ann["masks"] = self.tiles[idx]["gt_masks"] + if "gt_labels" in self.tiles[idx]: + ann["labels"] = self.tiles[idx]["gt_labels"] + return ann + + def merge_vectors(self, feature_vectors: List[np.ndarray]) -> np.ndarray: + """Merge tile-level feature vectors to image-level feature vector. + + Args: + feature_vectors (List[np.ndarray]): tile-level feature vectors. + + Returns: + merged_vectors (List[np.ndarray]): Merged vectors for each image. + """ + + image_vectors: dict = {} + for vector, tile in zip(feature_vectors, self.tiles): + data_idx = tile.get("index", None) if "index" in tile else tile.get("dataset_idx", None) + if data_idx in image_vectors: + # tile vectors + image_vectors[data_idx].append(vector) + else: + # whole image vector + image_vectors[data_idx] = [vector] + return [np.average(image, axis=0) for idx, image in image_vectors.items()] + + def merge_maps(self, saliency_maps: Union[List[List[np.ndarray]], List[np.ndarray]]) -> List: + """Merge tile-level saliency maps to image-level saliency map. + + Args: + saliency_maps (List[List[np.array] | np.ndarray]): tile-level saliency maps. + Each map is a list of maps for each detected class or None if class wasn't detected. + + Returns: + merged_maps (List[list | np.ndarray | None]): Merged saliency maps for each image. + """ + + dtype = None + for map in saliency_maps: + for cl_map in map: + # find first class map which is not None + if cl_map is not None and dtype is None: + dtype = cl_map.dtype + feat_h, feat_w = cl_map.shape + break + if dtype is not None: + break + else: + # if None for each class for each image + return saliency_maps[: self.num_images] + + merged_maps = [] + ratios = {} + num_classes = len(saliency_maps[0]) + + for orig_image in self.cached_results: + img_idx = orig_image["index"] + image_h, image_w = orig_image["height"], orig_image["width"] + ratios[img_idx] = np.array([feat_h / min(self.tile_size, image_h), feat_w / min(self.tile_size, image_w)]) + + image_map_h = int(image_h * ratios[img_idx][0]) + image_map_w = int(image_w * ratios[img_idx][1]) + merged_maps.append([np.zeros((image_map_h, image_map_w)) for _ in range(num_classes)]) + + for map, tile in zip(saliency_maps[self.num_images :], self.tiles[self.num_images :]): + for class_idx in range(num_classes): + if map[class_idx] is None: + continue + cls_map = map[class_idx] + img_idx = tile["dataset_idx"] + x_1, y_1, x_2, y_2 = tile["tile_box"] + y_1, x_1 = ((y_1, x_1) * ratios[img_idx]).astype(np.uint16) + y_2, x_2 = ((y_2, x_2) * ratios[img_idx]).astype(np.uint16) + + map_h, map_w = cls_map.shape + # resize feature map if it got from the tile which width and height is less the tile_size + if (map_h > y_2 - y_1 > 0) and (map_w > x_2 - x_1 > 0): + cls_map = cv2.resize(cls_map, (x_2 - x_1, y_2 - y_1)) + # cut the rest of the feature map that went out of the image borders + map_h, map_w = y_2 - y_1, x_2 - x_1 + + for hi, wi in [(h_, w_) for h_ in range(map_h) for w_ in range(map_w)]: + map_pixel = cls_map[hi, wi] + # on tile overlap add 0.5 value of each tile + if merged_maps[img_idx][class_idx][y_1 + hi, x_1 + wi] != 0: + merged_maps[img_idx][class_idx][y_1 + hi, x_1 + wi] = 0.5 * ( + map_pixel + merged_maps[img_idx][class_idx][y_1 + hi, x_1 + wi] + ) + else: + merged_maps[img_idx][class_idx][y_1 + hi, x_1 + wi] = map_pixel + + norm_maps = [] + for merged_map, image_sal_map in zip(merged_maps, saliency_maps[: self.num_images]): + for class_idx in range(num_classes): + # don't have detections for this class on merged map + if (merged_map[class_idx] == 0).all(): + merged_map[class_idx] = None + else: + image_map_cls = image_sal_map[class_idx] + # resize the feature map for whole image to add it to merged saliency maps + if image_map_cls is not None: + map_h, map_w = merged_map[class_idx].shape + image_map_cls = cv2.resize(image_map_cls, (map_w, map_h)) + merged_map[class_idx] += (0.5 * image_map_cls).astype(dtype) + merged_map[class_idx] = non_linear_normalization(merged_map[class_idx]) + norm_maps.append(merged_map) + + return norm_maps diff --git a/src/otx/algorithms/detection/adapters/mmdet/evaluation/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/evaluation/__init__.py new file mode 100644 index 00000000000..711f42527c5 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/evaluation/__init__.py @@ -0,0 +1,10 @@ +"""Evaluation methods for mmdetection.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .evaluator import Evaluator + +__all__ = [ + "Evaluator", +] diff --git a/src/otx/algorithms/detection/adapters/mmdet/evaluation/evaluator.py b/src/otx/algorithms/detection/adapters/mmdet/evaluation/evaluator.py new file mode 100644 index 00000000000..36bda12206f --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/evaluation/evaluator.py @@ -0,0 +1,403 @@ +"""Evaluator of OTX Detection.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import multiprocessing as mp +import time +from typing import Dict, List, Tuple, Union + +import mmcv +import numpy as np +import pycocotools.mask as mask_util +from mmcv.utils import print_log +from mmdet.core import BitmapMasks, PolygonMasks, eval_map +from mmdet.core.evaluation import mean_ap +from mmdet.core.evaluation.bbox_overlaps import bbox_overlaps +from mmdet.core.evaluation.class_names import get_classes +from mmdet.core.evaluation.mean_ap import average_precision +from terminaltables import AsciiTable + +from otx.algorithms.common.utils.utils import is_hpu_available +from otx.api.entities.label import Domain +from otx.api.utils.time_utils import timeit + + +def print_map_summary( # pylint: disable=too-many-locals,too-many-branches + mean_ap, results, dataset=None, scale_ranges=None, logger=None +): + """Print mAP/mIoU and results of each class. + + A table will be printed to show the gts/dets/recall/AP/IoU of each class + and the mAP/mIoU. + + Args: + mean_ap (float): Calculated from `eval_map()`. + results (list[dict]): Calculated from `eval_map()`. + dataset (list[str] | str | None): Dataset name or dataset classes. + scale_ranges (list[tuple] | None): Range of scales to be evaluated. + logger (logging.Logger | str | None): The way to print the mAP + summary. See `mmcv.utils.print_log()` for details. Default: None. + """ + + if logger == "silent": + return + + if isinstance(results[0]["ap"], np.ndarray): + num_scales = len(results[0]["ap"]) + else: + num_scales = 1 + + if scale_ranges is not None: + assert len(scale_ranges) == num_scales + + segmentation = "miou" in results + num_classes = len(results) + + recalls = np.zeros((num_scales, num_classes), dtype=np.float32) + aps = np.zeros((num_scales, num_classes), dtype=np.float32) + num_gts = np.zeros((num_scales, num_classes), dtype=int) + mious = np.zeros((num_scales, num_classes), dtype=np.float32) + for i, cls_result in enumerate(results): + if cls_result["recall"].size > 0: + recalls[:, i] = np.array(cls_result["recall"], ndmin=2)[:, -1] + aps[:, i] = cls_result["ap"] + if segmentation: + mious[:, i] = cls_result["miou"] + num_gts[:, i] = cls_result["num_gts"] + + if dataset is None: + label_names = [str(i) for i in range(num_classes)] + elif mmcv.is_str(dataset): + label_names = get_classes(dataset) + else: + label_names = dataset + + if not isinstance(mean_ap, list): + mean_ap = [mean_ap] + + header = ["class", "gts", "dets", "recall", "ap"] + if segmentation: + header.append("miou") + for i in range(num_scales): + if scale_ranges is not None: + print_log(f"Scale range {scale_ranges[i]}", logger=logger) + table_data = [header] + for j in range(num_classes): + row_data = [ + label_names[j], + num_gts[i, j], + results[j]["num_dets"], + f"{recalls[i, j]:.3f}", + f"{aps[i, j]:.3f}", + ] + if segmentation: + row_data.append(f"{mious[i, j]:.3f}") + table_data.append(row_data) + table_ = ( + ["mAP", "", "", "", f"{mean_ap[i]:.3f}", f"{np.mean(mious[i]):.3f}"] + if segmentation + else ["mAP", "", "", "", f"{mean_ap[i]:.3f}"] + ) + table_data.append(table_) + table = AsciiTable(table_data) + table.inner_footing_row_border = True + if is_hpu_available(): + time.sleep(0.1) # prevent segmentation fault + print_log("\n" + table.table, logger=logger) + + +def sanitize_coordinates(bbox: np.ndarray, height: int, width: int, padding=1) -> np.ndarray: + """Sanitize coordinates of bounding boxes so that they fit within the image. + + Args: + bbox (np.ndarray): bounding boxes with shape (4, ) + height (int): image height + width (int): image width + padding (int, optional): padding added to each side of the bounding box. Defaults to 1. + + Returns: + np.ndarray: sanitized bounding boxes with shape (4, ) + """ + x1, y1, x2, y2 = bbox.astype(np.int) + x1 = max(0, x1 - padding) + y1 = max(0, y1 - padding) + x2 = min(width, x2 + padding) + y2 = min(height, y2 + padding) + return np.array([x1, y1, x2, y2]) + + +def mask_iou(det: Tuple[np.ndarray, BitmapMasks], gt_masks: PolygonMasks, iou_thr: float) -> np.ndarray: + """Compute the intersection over union between the detected masks and ground truth masks. + + Args: + det (Tuple[np.ndarray, BitmapMasks]): detected bboxes and masks + gt_masks (PolygonMasks): ground truth masks + iou_thr (float): IoU threshold + + Note: + It first compute IoU between bounding boxes, then compute IoU between masks + if IoU between bounding boxes is greater than 0. + Detection mask is resized to detected bounding box size and + padded to the same size as ground truth mask in order to compute IoU. + + Returns: + np.ndarray: iou between detected masks and ground truth masks + + """ + det_bboxes, det_masks = det + gt_bboxes = gt_masks.get_bboxes() + img_h, img_w = gt_masks.height, gt_masks.width + ious = bbox_overlaps(det_bboxes, gt_bboxes, mode="iou") + ious[ious < iou_thr] = 0.0 + if not ious.any(): + return ious + # NOTE: further speed optimization (vectorization) could be done here + for coord in np.argwhere(ious): + m, n = coord + det_bbox, det_mask = sanitize_coordinates(det_bboxes[m], img_h, img_w), det_masks[m] + gt_bbox, gt_mask = sanitize_coordinates(gt_bboxes[n], img_h, img_w), gt_masks[n] + # add padding to det_mask and gt_mask so that they have the same size + min_x1 = min(det_bbox[0], gt_bbox[0]) + min_y1 = min(det_bbox[1], gt_bbox[1]) + max_x2 = max(det_bbox[2], gt_bbox[2]) + max_y2 = max(det_bbox[3], gt_bbox[3]) + + det_bbox_h, det_bbox_w = det_bbox[3] - det_bbox[1], det_bbox[2] - det_bbox[0] + det_mask = det_mask.resize((det_bbox_h, det_bbox_w)) + det_mask = det_mask.expand(max_y2 - min_y1, max_x2 - min_x1, det_bbox[1] - min_y1, det_bbox[0] - min_x1) + gt_mask = gt_mask.crop(gt_bbox) + gt_mask = gt_mask.to_bitmap() + gt_mask = gt_mask.expand(max_y2 - min_y1, max_x2 - min_x1, gt_bbox[1] - min_y1, gt_bbox[0] - min_x1) + # compute iou between det_mask and gt_mask + det_mask = det_mask.to_ndarray() + gt_mask = gt_mask.to_ndarray() + assert det_mask.shape == gt_mask.shape, f"det_mask.shape={det_mask.shape} != gt_mask.shape={gt_mask.shape}" + ious[m, n] = np.sum(det_mask & gt_mask) / np.sum(det_mask | gt_mask) + return ious + + +def tpfpmiou_func( # pylint: disable=too-many-locals + det: Tuple[np.ndarray, Union[BitmapMasks, List]], gt_masks: PolygonMasks, cls_scores, iou_thr=0.5 +): + """Compute tp, fp, miou for each image. + + Args: + det (Tuple[np.ndarray, BitmapMasks]): detected bboxes and masks + gt_masks (PolygonMasks): ground truth polygons + cls_scores (np.ndarray): class scores + iou_thr (float, optional): IoU threshold. Defaults to 0.5. + + Returns: + Tuple[np.ndarray, np.ndarray, float]: tp, fp, miou for each image + """ + num_dets = len(det[0]) # M + num_gts = len(gt_masks) # N + + tp = np.zeros(num_dets, dtype=np.float32) # pylint: disable=invalid-name + fp = np.zeros(num_dets, dtype=np.float32) # pylint: disable=invalid-name + gt_covered_iou = np.zeros(num_gts, dtype=np.float32) + + if len(gt_masks) == 0: + fp[...] = 1 + return tp, fp, 0.0 + if num_dets == 0: + return tp, fp, 0.0 + + ious = mask_iou(det, gt_masks, iou_thr) # (M, N) + + # for each det, the max iou with all gts + ious_max = ious.max(axis=1) + # for each det, which gt overlaps most with it + ious_argmax = ious.argmax(axis=1) + # sort all dets in descending order by scores + sort_inds = np.argsort(-cls_scores) + + gt_covered = np.zeros(num_gts, dtype=bool) + # if no area range is specified, gt_area_ignore is all False + for i in sort_inds: + if ious_max[i] >= iou_thr: + matched_gt = ious_argmax[i] + if not gt_covered[matched_gt]: + gt_covered[matched_gt] = True + gt_covered_iou[matched_gt] = ious_max[i] + tp[i] = 1 + else: + fp[i] = 1 + # otherwise ignore this detected bbox, tp = 0, fp = 0 + else: + fp[i] = 1 + return tp, fp, np.mean(gt_covered_iou) + + +class Evaluator: + """OTX Evaluator for mAP and mIoU. + + Args: + annotation (list(dict)): ground truth annotation + domain (Domain): OTX algorithm domain + classes (list): list of classes + nproc (int, optional): number of processes. Defaults to 4. + """ + + def __init__(self, annotation: List[Dict], domain: Domain, classes: List[str], nproc=4): + self.domain = domain + self.classes = classes + self.num_classes = len(classes) + if domain != Domain.DETECTION: + self.annotation = self.get_gt_instance_masks(annotation) + else: + self.annotation = annotation + self.nproc = nproc + mean_ap.print_map_summary = print_map_summary + + def get_gt_instance_masks(self, annotation: List[Dict]): + """Format ground truth instance mask annotation. + + Args: + annotation (List[Dict]): per-image ground truth annotation + + Returns: + cls_anno_list: per-class ground truth instance mask list + """ + cls_anno_list: List[List] = [[] for _ in range(self.num_classes)] + for class_id in range(self.num_classes): + for ann in annotation: + gt_inds = ann["labels"] == class_id + polygon_masks = [] + if gt_inds.any(): + gt_inds = np.where(gt_inds == 1)[0] + polygon_masks = ann["masks"][gt_inds] + cls_anno_list[class_id].append(polygon_masks) + return cls_anno_list + + def get_mask_det_results(self, det_results: List[Tuple], class_id: int) -> Tuple[List, List]: + """Get mask detection results for a specific class. + + Args: + det_results (list(tuple)): detection results including bboxes and masks + class_id (int): class index + + Returns: + cls_dets: per-class detection results including bboxes and decoded masks + cls_scores: class scores + """ + cls_scores = [img_res[0][class_id][..., -1] for img_res in det_results] + cls_dets: List[Tuple] = [] + for det in det_results: + det_bboxes = det[0][class_id][:, :4] + det_masks = det[1][class_id] + if len(det_masks) == 0: + cls_dets.append(([], [])) + else: + # Convert 28x28 encoded RLE mask detection to 28x28 BitmapMasks. + det_masks = mask_util.decode(det_masks) + det_masks = det_masks.transpose(2, 0, 1) + det_masks = BitmapMasks(det_masks, *det_masks.shape[1:]) + cls_dets.append((det_bboxes, det_masks)) + return cls_dets, cls_scores + + def evaluate_mask(self, results, logger, iou_thr): + """Evaluate mask results. + + Args: + results (list): list of prediction + logger (Logger): OTX logger + iou_thr (float): IoU threshold + + Returns: + metric: mAP and mIoU metric + """ + assert len(results) == len(self.annotation[0]), "number of images should be equal!" + num_imgs = len(results) + eval_results = [] + + ctx = mp.get_context("spawn") + with ctx.Pool(self.nproc) as p: + for class_id in range(self.num_classes): + # get gt and det bboxes of this class + cls_dets, cls_scores = self.get_mask_det_results(results, class_id) + cls_gts = self.annotation[class_id] + + # compute tp and fp for each image with multiple processes + tpfpmiou = p.starmap( + tpfpmiou_func, zip(cls_dets, cls_gts, cls_scores, [iou_thr for _ in range(num_imgs)]) + ) + tp, fp, miou = tuple(zip(*tpfpmiou)) # pylint: disable=invalid-name + + # sort all det bboxes by score, also sort tp and fp + cls_scores = np.hstack(cls_scores) + num_dets = cls_scores.shape[0] + num_gts = np.sum([len(cls_gts) for cls_gts in cls_gts]) + sort_inds = np.argsort(cls_scores)[::-1] + tp = np.hstack(tp)[sort_inds] # pylint: disable=invalid-name + fp = np.hstack(fp)[sort_inds] # pylint: disable=invalid-name + # calculate recall and precision with tp and fp + tp = np.cumsum(tp) # pylint: disable=invalid-name + fp = np.cumsum(fp) # pylint: disable=invalid-name + eps = np.finfo(np.float32).eps + recalls = tp / np.maximum(num_gts, eps) + precisions = tp / np.maximum((tp + fp), eps) + miou = np.mean(np.stack(miou)) + # calculate AP + ap = average_precision(recalls, precisions, "area") # pylint: disable=invalid-name + eval_results.append( + { + "num_gts": num_gts, + "num_dets": num_dets, + "recall": recalls, + "precision": precisions, + "ap": ap, + "miou": miou, + } + ) + + metrics = {"mAP": 0.0, "mIoU": 0.0} + mious, aps = [], [] + for cls_result in eval_results: + if cls_result["num_gts"] > 0: + aps.append(cls_result["ap"]) + mious.append(cls_result["miou"]) + mean_ap = np.array(aps).mean().item() if aps else 0.0 + mean_miou = np.array(mious).mean().item() if mious else 0.0 + metrics["mAP"] = mean_ap + metrics["mIoU"] = mean_miou + + print_map_summary(mean_ap, eval_results, self.classes, None, logger=logger) + + return metrics["mAP"], eval_results + + @timeit + def evaluate(self, results, logger, iou_thr, scale_ranges): + """Evaluate detection results. + + Args: + results (list): list of prediction + logger (Logger): OTX logger + iou_thr (float): IoU threshold + scale_ranges (list): scale range for object detection evaluation + + Returns: + metric: mAP and mIoU metric + """ + if self.domain == Domain.DETECTION: + return eval_map( + results, + self.annotation, + scale_ranges=scale_ranges, + iou_thr=iou_thr, + dataset=self.classes, + logger=logger, + ) + return self.evaluate_mask(results, logger, iou_thr) diff --git a/src/otx/algorithms/detection/adapters/mmdet/hooks/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/hooks/__init__.py new file mode 100644 index 00000000000..1a8a890dd85 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/hooks/__init__.py @@ -0,0 +1,9 @@ +"""Initial file for mmdetection hooks.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .det_class_probability_map_hook import DetClassProbabilityMapHook +from .tile_sampling_hook import TileSamplingHook + +__all__ = ["DetClassProbabilityMapHook", "TileSamplingHook"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/hooks/det_class_probability_map_hook.py b/src/otx/algorithms/detection/adapters/mmdet/hooks/det_class_probability_map_hook.py new file mode 100644 index 00000000000..2847f1c573a --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/hooks/det_class_probability_map_hook.py @@ -0,0 +1,253 @@ +"""Detection Saliency Map Hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch +import torch.nn.functional as F +from mmdet.core import bbox2roi + +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + BaseRecordingForwardHook, +) +from otx.algorithms.detection.adapters.mmdet.models.heads.custom_atss_head import ( + CustomATSSHead, +) +from otx.algorithms.detection.adapters.mmdet.models.heads.custom_ssd_head import ( + CustomSSDHead, +) +from otx.algorithms.detection.adapters.mmdet.models.heads.custom_vfnet_head import ( + CustomVFNetHead, +) +from otx.algorithms.detection.adapters.mmdet.models.heads.custom_yolox_head import ( + CustomYOLOXHead, +) + +# pylint: disable=too-many-locals + + +class DetClassProbabilityMapHook(BaseRecordingForwardHook): + """Saliency map hook for object detection models.""" + + def __init__(self, module: torch.nn.Module, normalize: bool = True, use_cls_softmax: bool = True) -> None: + super().__init__(module, normalize=normalize) + self._neck = module.neck if module.with_neck else None + self._bbox_head = module.bbox_head + self._num_cls_out_channels = module.bbox_head.cls_out_channels # SSD-like heads also have background class + if hasattr(module.bbox_head, "anchor_generator"): + self._num_anchors = module.bbox_head.anchor_generator.num_base_anchors + else: + self._num_anchors = [1] * 10 + self.use_cls_softmax = use_cls_softmax + + def func( + self, + feature_map: Union[torch.Tensor, List[torch.Tensor], Tuple[torch.Tensor]], + _: int = -1, + cls_scores_provided: bool = False, + ) -> torch.Tensor: + """Generate the saliency map from raw classification head output, then normalizing to (0, 255). + + :param x: Feature maps from backbone/FPN or classification scores from cls_head + :param cls_scores_provided: If True - use 'x' as is, otherwise forward 'x' through the classification head + :return: Class-wise Saliency Maps. One saliency map per each class - [batch, class_id, H, W] + """ + if cls_scores_provided: + cls_scores = feature_map + else: + cls_scores = self._get_cls_scores_from_feature_map(feature_map) + + middle_idx = len(cls_scores) // 2 + # resize to the middle feature map + batch_size, _, height, width = cls_scores[middle_idx].size() + saliency_maps = torch.empty(batch_size, self._num_cls_out_channels, height, width) + for batch_idx in range(batch_size): + cls_scores_anchorless = [] + for scale_idx, cls_scores_per_scale in enumerate(cls_scores): + cls_scores_anchor_grouped = cls_scores_per_scale[batch_idx].reshape( + self._num_anchors[scale_idx], (self._num_cls_out_channels), *cls_scores_per_scale.shape[-2:] + ) + cls_scores_out, _ = cls_scores_anchor_grouped.max(dim=0) + cls_scores_anchorless.append(cls_scores_out.unsqueeze(0)) + cls_scores_anchorless_resized = [] + for cls_scores_anchorless_per_level in cls_scores_anchorless: + cls_scores_anchorless_resized.append( + F.interpolate(cls_scores_anchorless_per_level, (height, width), mode="bilinear") + ) + saliency_maps[batch_idx] = torch.cat(cls_scores_anchorless_resized, dim=0).mean(dim=0) + + # Don't use softmax for tiles in tiling detection, if the tile doesn't contain objects, + # it would highlight one of the class maps as a background class + if self.use_cls_softmax: + saliency_maps[0] = torch.stack([torch.softmax(t, dim=1) for t in saliency_maps[0]]) + + if self._norm_saliency_maps: + saliency_maps = saliency_maps.reshape((batch_size, self._num_cls_out_channels, -1)) + saliency_maps = self._normalize_map(saliency_maps) + + saliency_maps = saliency_maps.reshape((batch_size, self._num_cls_out_channels, height, width)) + + return saliency_maps + + def _get_cls_scores_from_feature_map(self, x: torch.Tensor) -> List: + """Forward features through the classification head of the detector.""" + with torch.no_grad(): + if self._neck is not None: + x = self._neck(x) + + if isinstance(self._bbox_head, CustomSSDHead): + cls_scores = [] + for feat, cls_conv in zip(x, self._bbox_head.cls_convs): + cls_scores.append(cls_conv(feat)) + elif isinstance(self._bbox_head, CustomATSSHead): + cls_scores = [] + for cls_feat in x: + for cls_conv in self._bbox_head.cls_convs: + cls_feat = cls_conv(cls_feat) + cls_score = self._bbox_head.atss_cls(cls_feat) + cls_scores.append(cls_score) + elif isinstance(self._bbox_head, CustomVFNetHead): + # Not clear how to separate cls_scores from bbox_preds + cls_scores, _, _ = self._bbox_head(x) + elif isinstance(self._bbox_head, CustomYOLOXHead): + + def forward_single(x, cls_convs, conv_cls): + """Forward feature of a single scale level.""" + cls_feat = cls_convs(x) + cls_score = conv_cls(cls_feat) + return cls_score + + map_results = map( + forward_single, x, self._bbox_head.multi_level_cls_convs, self._bbox_head.multi_level_conv_cls + ) + cls_scores = list(map_results) + else: + raise NotImplementedError( + "Not supported detection head provided. " + "DetClassProbabilityMap supports only the following single stage detectors: " + "YOLOXHead, ATSSHead, SSDHead, VFNetHead." + ) + return cls_scores + + +class MaskRCNNRecordingForwardHook(BaseRecordingForwardHook): + """Saliency map hook for Mask R-CNN model. Only for torch model, does not support OpenVINO IR model. + + Args: + module (torch.nn.Module): Mask R-CNN model. + input_img_shape (Tuple[int]): Resolution of the model input image. + saliency_map_shape (Tuple[int]): Resolution of the output saliency map. + max_detections_per_img (int): Upper limit of the number of detections + from which soft mask predictions are getting aggregated. + normalize (bool): Flag that defines if the output saliency map will be normalized. + Although, partial normalization is anyway done by segmentation mask head. + """ + + def __init__( + self, + module: torch.nn.Module, + input_img_shape: Tuple[int, int], + saliency_map_shape: Tuple[int, int] = (224, 224), + max_detections_per_img: int = 300, + normalize: bool = True, + ) -> None: + super().__init__(module) + self._neck = module.neck if module.with_neck else None + self._input_img_shape = input_img_shape + self._saliency_map_shape = saliency_map_shape + self._max_detections_per_img = max_detections_per_img + self._norm_saliency_maps = normalize + + def func( + self, + feature_map: Union[torch.Tensor, List[torch.Tensor], Tuple[torch.Tensor]], + _: int = -1, + ) -> List[List[Optional[np.ndarray]]]: + """Generate saliency maps by aggregating per-class soft predictions of mask head for all detected boxes. + + :param feature_map: Feature maps from backbone. + :return: Class-wise Saliency Maps. One saliency map per each predicted class. + """ + with torch.no_grad(): + if self._neck is not None: + feature_map = self._module.neck(feature_map) + + det_bboxes, det_labels = self._get_detections(feature_map) + saliency_maps = self._get_saliency_maps_from_mask_predictions(feature_map, det_bboxes, det_labels) + if self._norm_saliency_maps: + saliency_maps = self._normalize(saliency_maps) + return saliency_maps + + def _get_detections(self, x: torch.Tensor) -> Tuple[List[torch.Tensor], List[torch.Tensor]]: + batch_size = x[0].shape[0] + img_metas = [ + { + "scale_factor": [1, 1, 1, 1], # dummy scale_factor, not used + "img_shape": self._input_img_shape, + } + ] + img_metas *= batch_size + proposals = self._module.rpn_head.simple_test_rpn(x, img_metas) + test_cfg = copy.deepcopy(self._module.roi_head.test_cfg) + test_cfg["max_per_img"] = self._max_detections_per_img + test_cfg["nms"]["iou_threshold"] = 1 + test_cfg["nms"]["max_num"] = self._max_detections_per_img + det_bboxes, det_labels = self._module.roi_head.simple_test_bboxes( + x, img_metas, proposals, test_cfg, rescale=False + ) + return det_bboxes, det_labels + + def _get_saliency_maps_from_mask_predictions( + self, x: torch.Tensor, det_bboxes: List[torch.Tensor], det_labels: List[torch.Tensor] + ) -> List[List[Optional[np.ndarray]]]: + _bboxes = [det_bboxes[i][:, :4] for i in range(len(det_bboxes))] + mask_rois = bbox2roi(_bboxes) + mask_results = self._module.roi_head._mask_forward(x, mask_rois) + mask_pred = mask_results["mask_pred"] + num_mask_roi_per_img = [len(det_bbox) for det_bbox in det_bboxes] + mask_preds = mask_pred.split(num_mask_roi_per_img, 0) + + batch_size = x[0].shape[0] + + scale_x = self._input_img_shape[1] / self._saliency_map_shape[1] + scale_y = self._input_img_shape[0] / self._saliency_map_shape[0] + scale_factor = torch.FloatTensor((scale_x, scale_y, scale_x, scale_y)) + test_cfg = self._module.roi_head.test_cfg.copy() + test_cfg["mask_thr_binary"] = -1 + + saliency_maps = [[None for _ in range(self._module.roi_head.mask_head.num_classes)] for _ in range(batch_size)] + + for i in range(batch_size): + if det_bboxes[i].shape[0] == 0: + continue + else: + segm_result = self._module.roi_head.mask_head.get_seg_masks( + mask_preds[i], + _bboxes[i], + det_labels[i], + test_cfg, + self._saliency_map_shape, + scale_factor=scale_factor, + rescale=True, + ) + for class_id, segm_res in enumerate(segm_result): + if segm_res: + saliency_maps[i][class_id] = np.mean(np.array(segm_res), axis=0) + return saliency_maps + + @staticmethod + def _normalize(saliency_maps: List[List[Optional[np.ndarray]]]) -> List[List[Optional[np.ndarray]]]: + batch_size = len(saliency_maps) + num_classes = len(saliency_maps[0]) + for i in range(batch_size): + for class_id in range(num_classes): + per_class_map = saliency_maps[i][class_id] + if per_class_map is not None: + max_values = np.max(per_class_map) + per_class_map = 255 * (per_class_map) / (max_values + 1e-12) + per_class_map = per_class_map.astype(np.uint8) + saliency_maps[i][class_id] = per_class_map + return saliency_maps diff --git a/src/otx/algorithms/detection/adapters/mmdet/hooks/tile_sampling_hook.py b/src/otx/algorithms/detection/adapters/mmdet/hooks/tile_sampling_hook.py new file mode 100644 index 00000000000..eac4b7ea775 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/hooks/tile_sampling_hook.py @@ -0,0 +1,24 @@ +"""Tile Samipling Hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from random import sample + +from mmcv.runner import HOOKS, Hook + + +@HOOKS.register_module() +class TileSamplingHook(Hook): + """Tile Sampling Hook. + + Usually training model with tile requires lots of time due to large images generate plenty of tiles. + To save training and validation time, OTX offers samipling method to entire tile datset. + Especially tile sampling hook samples tiles whenever epoch starts to train model various tile samples + """ + + def before_epoch(self, runner): + """Sample tiles from training datset when epoch starts.""" + if hasattr(runner.data_loader.dataset, "tile_dataset"): + tile_dataset = runner.data_loader.dataset.tile_dataset + tile_dataset.tiles = sample(tile_dataset.tiles_all, tile_dataset.sample_num) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/__init__.py new file mode 100644 index 00000000000..9efe5351905 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/__init__.py @@ -0,0 +1,19 @@ +"""Initial file for mmdetection models.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from . import ( + assigners, + backbones, + dense_heads, + detectors, + heads, + layers, + losses, + necks, + patch_mmdeploy, # noqa: F401 + roi_heads, +) + +__all__ = ["assigners", "backbones", "dense_heads", "detectors", "heads", "layers", "losses", "necks", "roi_heads"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/assigners/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/assigners/__init__.py new file mode 100644 index 00000000000..71418724251 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/assigners/__init__.py @@ -0,0 +1,8 @@ +"""Initial file for mmdetection assigners.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .custom_max_iou_assigner import CustomMaxIoUAssigner + +__all__ = ["CustomMaxIoUAssigner"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/assigners/custom_max_iou_assigner.py b/src/otx/algorithms/detection/adapters/mmdet/models/assigners/custom_max_iou_assigner.py new file mode 100644 index 00000000000..31e4a1674e2 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/assigners/custom_max_iou_assigner.py @@ -0,0 +1,104 @@ +"""Custom assigner for mmdet MaxIouAssigner.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmdet.core.bbox.assigners import MaxIoUAssigner +from mmdet.core.bbox.builder import BBOX_ASSIGNERS + + +@BBOX_ASSIGNERS.register_module() +class CustomMaxIoUAssigner(MaxIoUAssigner): + """Assign a corresponding gt bbox or background to each bbox. + + Each proposals will be assigned with `-1`, or a semi-positive integer + indicating the ground truth index. + + - -1: negative sample, no assigned gt + - semi-positive integer: positive sample, index (0-based) of assigned gt + + This CustomMaxIoUAssigner patches assign funtion of mmdet's MaxIouAssigner + so that it can prevent CPU OOM for images whose gt is extremely large + """ + + cpu_assign_thr = 1000 + + def assign(self, bboxes, gt_bboxes, gt_bboxes_ignore=None, gt_labels=None): + """Assign gt to bboxes. + + This method assign a gt bbox to every bbox (proposal/anchor), each bbox + will be assigned with -1, or a semi-positive number. -1 means negative + sample, semi-positive number is the index (0-based) of assigned gt. + The assignment is done in following steps, the order matters. + + Especially CustomMaxIoUAssigner split gt_bboxes tensor into small tensors + when gt_bboxes is too large. + + 1. assign every bbox to the background + 2. assign proposals whose iou with all gts < neg_iou_thr to 0 + 3. for each bbox, if the iou with its nearest gt >= pos_iou_thr, + assign it to that bbox + 4. for each gt bbox, assign its nearest proposals (may be more than + one) to itself + + Args: + bboxes (Tensor): Bounding boxes to be assigned, shape(n, 4). + gt_bboxes (Tensor): Groundtruth boxes, shape (k, 4). + gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are + labelled as `ignored`, e.g., crowd boxes in COCO. + gt_labels (Tensor, optional): Label of gt_bboxes, shape (k, ). + + Returns: + :obj:`AssignResult`: The assign result. + + Example: + >>> self = MaxIoUAssigner(0.5, 0.5) + >>> bboxes = torch.Tensor([[0, 0, 10, 10], [10, 10, 20, 20]]) + >>> gt_bboxes = torch.Tensor([[0, 0, 10, 9]]) + >>> assign_result = self.assign(bboxes, gt_bboxes) + >>> expected_gt_inds = torch.LongTensor([1, 0]) + >>> assert torch.all(assign_result.gt_inds == expected_gt_inds) + """ + assign_on_cpu = True if (self.gpu_assign_thr > 0) and (gt_bboxes.shape[0] > self.gpu_assign_thr) else False + # compute overlap and assign gt on CPU when number of GT is large + if assign_on_cpu: + device = bboxes.device + bboxes = bboxes.cpu() + gt_bboxes = gt_bboxes.cpu() + if gt_bboxes_ignore is not None: + gt_bboxes_ignore = gt_bboxes_ignore.cpu() + if gt_labels is not None: + gt_labels = gt_labels.cpu() + + if assign_on_cpu and gt_bboxes.shape[0] > self.cpu_assign_thr: + split_length = gt_bboxes.shape[0] // self.cpu_assign_thr + 1 + overlaps = [] + for i in range(split_length): + gt_bboxes_split = gt_bboxes[i * self.cpu_assign_thr : (i + 1) * self.cpu_assign_thr] + overlaps.append(self.iou_calculator(gt_bboxes_split, bboxes)) + overlaps = torch.concat(overlaps, dim=0) + else: + overlaps = self.iou_calculator(gt_bboxes, bboxes) + + if ( + self.ignore_iof_thr > 0 + and gt_bboxes_ignore is not None + and gt_bboxes_ignore.numel() > 0 + and bboxes.numel() > 0 + ): + if self.ignore_wrt_candidates: + ignore_overlaps = self.iou_calculator(bboxes, gt_bboxes_ignore, mode="iof") + ignore_max_overlaps, _ = ignore_overlaps.max(dim=1) + else: + ignore_overlaps = self.iou_calculator(gt_bboxes_ignore, bboxes, mode="iof") + ignore_max_overlaps, _ = ignore_overlaps.max(dim=0) + overlaps[:, ignore_max_overlaps > self.ignore_iof_thr] = -1 + + assign_result = self.assign_wrt_overlaps(overlaps, gt_labels) + if assign_on_cpu: + assign_result.gt_inds = assign_result.gt_inds.to(device) + assign_result.max_overlaps = assign_result.max_overlaps.to(device) + if assign_result.labels is not None: + assign_result.labels = assign_result.labels.to(device) + return assign_result diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/backbones/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/backbones/__init__.py new file mode 100644 index 00000000000..8ced39c37e7 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/backbones/__init__.py @@ -0,0 +1,9 @@ +"""Initial file for mmdetection backbones.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from . import imgclsmob +from .mmov_backbone import MMOVBackbone + +__all__ = ["imgclsmob", "MMOVBackbone"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/backbones/imgclsmob.py b/src/otx/algorithms/detection/adapters/mmdet/models/backbones/imgclsmob.py new file mode 100644 index 00000000000..e9c93e1fa84 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/backbones/imgclsmob.py @@ -0,0 +1,162 @@ +"""Backbone of pytorchcv for mmdetection backbones.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +from mmcv.cnn import build_activation_layer, build_norm_layer +from mmcv.runner import get_dist_info +from mmdet.models.builder import BACKBONES +from pytorchcv.model_provider import _models +from pytorchcv.models.model_store import download_model +from torch import distributed, nn +from torch.nn.modules.batchnorm import _BatchNorm + +from otx.utils.logger import get_logger + +# TODO: Need to fix pylint issues +# pylint: disable=protected-access, abstract-method, no-value-for-parameter, assignment-from-no-return + +logger = get_logger() + + +def replace_activation(model, activation_cfg): + """Replace activate funtion.""" + for name, module in model._modules.items(): + if len(list(module.children())) > 0: + model._modules[name] = replace_activation(module, activation_cfg) + if name == "activ": + if activation_cfg["type"] == "torch_swish": + model._modules[name] = nn.SiLU() + else: + model._modules[name] = build_activation_layer(activation_cfg) + return model + + +def replace_norm(model, cfg): + """Replace norm funtion.""" + for name, module in model._modules.items(): + if len(list(module.children())) > 0: + model._modules[name] = replace_norm(module, cfg) + if name == "bn": + model._modules[name] = build_norm_layer(cfg, num_features=module.num_features)[1] + return model + + +def multioutput_forward(self, x): + """Multioutput forward function for new model (copy from mmdet older).""" + outputs = [] + y = x + + last_stage = max(self.out_indices) + for i, stage in enumerate(self.features): + y = stage(y) + s_verbose = str(i) + " " + str(y.shape) + if i in self.out_indices: + outputs.append(y) + s_verbose += "*" + if self.verbose: + print(s_verbose) + if i == last_stage: + break + + return outputs + + +def train(self, mode=True): + """Train forward function for new model (copy from mmdet older).""" + super(self.__class__, self).train(mode) + + for i in range(self.frozen_stages + 1): + feature = self.features[i] + feature.eval() + for param in feature.parameters(): + param.requires_grad = False + + if mode and self.norm_eval: + for module in self.modules(): + # trick: eval have effect on BatchNorm only + if isinstance(module, _BatchNorm): + module.eval() + + +def init_weights(self, pretrained=True): + """Init weights function for new model (copy from mmdet).""" + if pretrained: + rank, world_size = get_dist_info() + if rank == 0: + # Make sure that model is fetched to the local storage. + download_model(net=self, model_name=self.model_name, local_model_store_dir_path=self.models_cache_root) + if world_size > 1: + distributed.barrier() + else: + # Wait for model to be in the local storage, then load it. + distributed.barrier() + download_model(net=self, model_name=self.model_name, local_model_store_dir_path=self.models_cache_root) + + +def generate_backbones(): + """Generate backbones of pytorchcv funtion.""" + for model_name, model_getter in _models.items(): + + def closure(model_name, model_getter): + """Get Model builder for mmcv (copy from mmdet old version).""" + + class CustomModelGetter(nn.Module): + """Custom Model getter class.""" + + def __init__( + self, + *args, + out_indices=None, + frozen_stages=0, + norm_eval=False, + verbose=False, + activation_cfg=None, + norm_cfg=None, + **kwargs, + ): + super().__init__() + models_cache_root = kwargs.get("root", os.path.join("~", ".torch", "models")) + is_pretrained = kwargs.get("pretrained", False) + logger.warning( + f"Init model {model_name}, pretrained={is_pretrained}, models cache {models_cache_root}" + ) + model = model_getter(*args, **kwargs) + if activation_cfg: + model = replace_activation(model, activation_cfg) + if norm_cfg: + model = replace_norm(model, norm_cfg) + model.out_indices = out_indices + model.frozen_stages = frozen_stages + model.norm_eval = norm_eval + model.verbose = verbose + model.model_name = model_name + model.models_cache_root = models_cache_root + if hasattr(model, "features") and isinstance(model.features, nn.Sequential): + # Save original forward, just in case. + model.forward_single_output = model.forward + model.forward = multioutput_forward.__get__(model) + model.init_weights = init_weights.__get__(model) + model.train = train.__get__(model) + + model.output = None + for i, _ in enumerate(model.features): + if i > max(out_indices): + model.features[i] = None + else: + raise ValueError( + "Failed to automatically wrap backbone network. " + f"Object of type {model.__class__} has no valid attribute called " + "'features'." + ) + self.__dict__.update(model.__dict__) + + CustomModelGetter.__name__ = model_name + return CustomModelGetter + + BACKBONES.register_module(name=model_name, module=closure(model_name, model_getter)) + + +generate_backbones() diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/backbones/mmov_backbone.py b/src/otx/algorithms/detection/adapters/mmdet/models/backbones/mmov_backbone.py new file mode 100644 index 00000000000..baa12457bc9 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/backbones/mmov_backbone.py @@ -0,0 +1,29 @@ +"""Backbone Class of OMZ model for mmdetection backbones.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmdet.models.builder import BACKBONES + +from otx.core.ov.models.mmov_model import MMOVModel + + +@BACKBONES.register_module() +class MMOVBackbone(MMOVModel): + """MMOVBackbone Class.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def forward(self, *args, **kwargs): + """Forward function of MMOVBackbone.""" + outputs = super().forward(*args, **kwargs) + if not isinstance(outputs, tuple): + outputs = (outputs,) + # must return tuple + return outputs + + def init_weights(self, pretrained=None): # pylint: disable=unused-argument + """Initial weights function of MMOVBackbone.""" + # TODO + return diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/__init__.py new file mode 100644 index 00000000000..dece478ce55 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/__init__.py @@ -0,0 +1,10 @@ +"""Initial file for mmdetection dense heads.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .mmov_rpn_head import MMOVRPNHead +from .mmov_ssd_head import MMOVSSDHead +from .mmov_yolov3_head import MMOVYOLOV3Head + +__all__ = ["MMOVRPNHead", "MMOVSSDHead", "MMOVYOLOV3Head"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_rpn_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_rpn_head.py new file mode 100644 index 00000000000..7befbb7ca7a --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_rpn_head.py @@ -0,0 +1,94 @@ +"""MMOV RPN Head for OTX.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +import torch +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads.rpn_head import RPNHead + +from otx.core.ov.models.mmov_model import MMOVModel +from otx.utils.logger import get_logger + +logger = get_logger() + +# TODO: Need to fix pylint issues +# pylint: disable=too-many-instance-attributes, too-many-arguments, keyword-arg-before-vararg + + +@HEADS.register_module() +class MMOVRPNHead(RPNHead): + """MMOVRPNHead class for OTX.""" + + def __init__( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + outputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + init_weight: bool = False, + verify_shape: bool = True, + transpose_cls: bool = False, + transpose_reg: bool = False, + *args, + **kwargs, + ): + self._model_path_or_model = model_path_or_model + self._weight_path = weight_path + self._inputs = deepcopy(inputs) + self._outputs = deepcopy(outputs) + self._init_weight = init_weight + self._verify_shape = verify_shape + self._transpose_cls = transpose_cls + self._transpose_reg = transpose_reg + + # dummy input + in_channels = 1 + super().__init__(in_channels=in_channels, *args, **kwargs) + + def _init_layers(self): + """Initialize layers of MMOVModel.""" + self.model = MMOVModel( + self._model_path_or_model, + self._weight_path, + inputs=self._inputs, + outputs=self._outputs, + remove_normalize=False, + merge_bn=False, + paired_bn=False, + verify_shape=self._verify_shape, + init_weight=self._init_weight, + ) + + def init_weights(self): + """Initial weight function of MMOVRPNHead.""" + # TODO + return + + def forward_single(self, x): + """Forward funtion for MMOVRPNHead.""" + rpn_cls_score, rpn_bbox_pred = self.model(x) + + if self._transpose_reg: + # [B, 4 * num_anchors, H, W] -> [B, num_anchors * 4, H, W] + shape = rpn_bbox_pred.shape + rpn_bbox_pred = rpn_bbox_pred.reshape(shape[0], 4, -1, *shape[2:]).transpose(1, 2).reshape(shape) + + if self._transpose_cls: + # [B, 2 * num_anchors, H, W] -> [B, num_anchors * 2, H, W] + shape = rpn_cls_score.shape + rpn_cls_score = rpn_cls_score.reshape(shape[0], 2, -1, *shape[2:]).transpose(1, 2).reshape(shape) + + # We set FG labels to [0, num_class-1] and BG label to + # num_class in RPN head since mmdet v2.5, which is unified to + # be consistent with other head since mmdet v2.0. In mmdet v2.0 + # to v2.4 we keep BG label as 0 and FG label as 1 in rpn head. + background = rpn_cls_score[:, 0::2] + foreground = rpn_cls_score[:, 1::2] + rpn_cls_score = torch.flatten(torch.stack([foreground, background], dim=2), 1, 2) + + return rpn_cls_score, rpn_bbox_pred diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_ssd_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_ssd_head.py new file mode 100644 index 00000000000..90a2b573cdd --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_ssd_head.py @@ -0,0 +1,147 @@ +"""MMOV SSD Head for OTX.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +import torch +from mmdet.core import build_anchor_generator +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads.ssd_head import SSDHead + +from otx.core.ov.models.mmov_model import MMOVModel + +# TODO: Need to fix pylint issues +# pylint: disable=redefined-argument-from-local, too-many-instance-attributes +# pylint: disable=too-many-arguments, keyword-arg-before-vararg + + +@HEADS.register_module() +class MMOVSSDHead(SSDHead): + """MMOVSSDHead class for OTX.""" + + def __init__( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + outputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + init_weight: bool = False, + verify_shape: bool = True, + transpose_cls: bool = False, + transpose_reg: bool = False, + background_index: Optional[int] = None, + *args, + **kwargs, + ): + + self._model_path_or_model = model_path_or_model + self._weight_path = weight_path + self._inputs = deepcopy(inputs) + self._outputs = deepcopy(outputs) + self._init_weight = init_weight + self._verify_shape = verify_shape + self._transpose_cls = transpose_cls + self._transpose_reg = transpose_reg + self._background_index = background_index + + # dummy input + anchor_generator = build_anchor_generator(kwargs["anchor_generator"]) + num_anchors = anchor_generator.num_base_anchors + in_channels = [256 for _ in num_anchors] + super().__init__(in_channels=in_channels, *args, **kwargs) + + self.cls_convs = torch.nn.ModuleList() + self.reg_convs = torch.nn.ModuleList() + + # TODO: Need to fix what exactly the types of inputs and outputs are. + if not isinstance(self._inputs, dict) or not isinstance(self._outputs, dict): + raise ValueError("The type of inputs & outputs is invalid.") + for ( + inputs, + outputs, + ) in zip(self._inputs["cls_convs"], self._outputs["cls_convs"]): + self.cls_convs.append( + MMOVModel( + self._model_path_or_model, + self._weight_path, + inputs=inputs, + outputs=outputs, + remove_normalize=False, + merge_bn=False, + paired_bn=False, + init_weight=self._init_weight, + verify_shape=self._verify_shape, + ) + ) + + for ( + inputs, + outputs, + ) in zip(self._inputs["reg_convs"], self._outputs["reg_convs"]): + self.reg_convs.append( + MMOVModel( + self._model_path_or_model, + self._weight_path, + inputs=inputs, + outputs=outputs, + remove_normalize=False, + merge_bn=False, + paired_bn=False, + init_weight=self._init_weight, + verify_shape=self._verify_shape, + ) + ) + + def forward(self, feats): + """Forward function for MMOVSSDHead.""" + cls_scores = [] + bbox_preds = [] + for feat, reg_conv, cls_conv in zip(feats, self.reg_convs, self.cls_convs): + + cls_score = cls_conv(feat) + bbox_pred = reg_conv(feat) + + if self._transpose_cls: + # [B, cls_out_channels * num_anchors, H, W] + # -> [B, num_anchors * cls_out_channels, H, W] + shape = cls_score.shape + cls_score = ( + cls_score.reshape(shape[0], self.cls_out_channels, -1, *shape[2:]).transpose(1, 2).reshape(shape) + ) + + if self._transpose_reg: + # [B, 4 * num_anchors, H, W] -> [B, num_anchors * 4, H, W] + shape = bbox_pred.shape + bbox_pred = bbox_pred.reshape(shape[0], 4, -1, *shape[2:]).transpose(1, 2).reshape(shape) + + # since mmdet v2.0, SSDHead is supposed to be + # that FG labels to [0, num_class-1] and BG labels to num_class + # but ssd300, ssd512, etc. from OMZ are + # that FG labels to [1, num_class] and BG labels to 0 + if self._background_index is not None and cls_score is not None: + cls_score = cls_score.permute(0, 2, 3, 1) + shape = cls_score.shape + cls_score = cls_score.reshape(-1, self.cls_out_channels) + cls_score = torch.cat( + ( + cls_score[:, : self._background_index], + cls_score[:, self._background_index + 1 :], + cls_score[:, self._background_index : self._background_index + 1], + ), + -1, + ) + cls_score = cls_score.reshape(shape) + cls_score = cls_score.permute(0, 3, 1, 2) + + cls_scores.append(cls_score) + bbox_preds.append(bbox_pred) + return cls_scores, bbox_preds + + def init_weights(self): + """Initialize weights function of MMOVSSDHead.""" + # TODO + return diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_yolov3_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_yolov3_head.py new file mode 100644 index 00000000000..be9bfb1ecd2 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/dense_heads/mmov_yolov3_head.py @@ -0,0 +1,84 @@ +"""MMOV YOLOX Head for OTX.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +import torch +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads.yolo_head import YOLOV3Head + +from otx.core.ov.models.mmov_model import MMOVModel + +# TODO: Need to fix pylint issues +# pylint: disable=too-many-instance-attributes, keyword-arg-before-vararg + + +@HEADS.register_module() +class MMOVYOLOV3Head(YOLOV3Head): + """MMOVYOLOV3Head class for OTX.""" + + def __init__( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + outputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + init_weight: bool = False, + verify_shape: bool = True, + *args, + **kwargs, + ): + self._model_path_or_model = model_path_or_model + self._weight_path = weight_path + self._inputs = deepcopy(inputs) + self._outputs = deepcopy(outputs) + self._init_weight = init_weight + self._verify_shape = verify_shape + + # dummy input + in_channels = (512, 256, 128) + out_channels = (1024, 512, 256) + if "featmap_strides" in kwargs: + in_channels = kwargs["featmap_strides"] + out_channels = kwargs["featmap_strides"] + super().__init__(in_channels=in_channels, out_channels=out_channels, *args, **kwargs) + + def _init_layers(self): + """Initialize layers of MMOVModels.""" + self.convs_bridge = torch.nn.ModuleList() + self.convs_pred = torch.nn.ModuleList() + + for inputs, outputs in zip(self._inputs["convs_bridge"], self._outputs["convs_bridge"]): + self.convs_bridge.append( + MMOVModel( + self._model_path_or_model, + self._weight_path, + inputs=inputs, + outputs=outputs, + remove_normalize=False, + init_weight=self._init_weight, + verify_shape=self._verify_shape, + ) + ) + + for inputs, outputs in zip(self._inputs["convs_pred"], self._outputs["convs_pred"]): + self.convs_pred.append( + MMOVModel( + self._model_path_or_model, + self._weight_path, + inputs=inputs, + outputs=outputs, + remove_normalize=False, + init_weight=self._init_weight, + verify_shape=self._verify_shape, + ) + ) + + def init_weights(self): + """Initialize weights of MMOVYOLOV3Head.""" + # TODO + return diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/__init__.py new file mode 100644 index 00000000000..5695d7b38fc --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/__init__.py @@ -0,0 +1,34 @@ +"""Initial file for mmdetection detectors.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .custom_atss_detector import CustomATSS +from .custom_deformable_detr_detector import CustomDeformableDETR +from .custom_dino_detector import CustomDINO +from .custom_lite_dino import CustomLiteDINO +from .custom_maskrcnn_detector import CustomMaskRCNN +from .custom_maskrcnn_tile_optimized import CustomMaskRCNNTileOptimized +from .custom_single_stage_detector import CustomSingleStageDetector +from .custom_two_stage_detector import CustomTwoStageDetector +from .custom_vfnet_detector import CustomVFNet +from .custom_yolox_detector import CustomYOLOX +from .l2sp_detector_mixin import L2SPDetectorMixin +from .mean_teacher import MeanTeacher +from .sam_detector_mixin import SAMDetectorMixin + +__all__ = [ + "CustomATSS", + "CustomDeformableDETR", + "CustomLiteDINO", + "CustomDINO", + "CustomMaskRCNN", + "CustomSingleStageDetector", + "CustomTwoStageDetector", + "CustomVFNet", + "CustomYOLOX", + "L2SPDetectorMixin", + "SAMDetectorMixin", + "CustomMaskRCNNTileOptimized", + "MeanTeacher", +] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_atss_detector.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_atss_detector.py new file mode 100644 index 00000000000..63d94f894d9 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_atss_detector.py @@ -0,0 +1,141 @@ +"""OTX ATSS Class for mmdetection detectors.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import functools + +import torch +from mmdet.models.builder import DETECTORS +from mmdet.models.detectors.atss import ATSS + +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + FeatureVectorHook, +) +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.algorithms.detection.adapters.mmdet.hooks.det_class_probability_map_hook import ( + DetClassProbabilityMapHook, +) +from otx.algorithms.detection.adapters.mmdet.models.loss_dyns import TrackingLossType +from otx.utils.logger import get_logger + +from .l2sp_detector_mixin import L2SPDetectorMixin +from .loss_dynamics_mixin import DetLossDynamicsTrackingMixin +from .sam_detector_mixin import SAMDetectorMixin + +logger = get_logger() + +# TODO: Need to fix pylint issues +# pylint: disable=abstract-method, unused-argument, too-many-locals, protected-access + + +@DETECTORS.register_module() +class CustomATSS(SAMDetectorMixin, DetLossDynamicsTrackingMixin, L2SPDetectorMixin, ATSS): + """SAM optimizer & L2SP regularizer enabled custom ATSS.""" + + TRACKING_LOSS_TYPE = (TrackingLossType.cls, TrackingLossType.bbox, TrackingLossType.centerness) + + def __init__(self, *args, task_adapt=None, **kwargs): + super().__init__(*args, **kwargs) + + # Hook for class-sensitive weight loading + if task_adapt: + self._register_load_state_dict_pre_hook( + functools.partial( + self.load_state_dict_pre_hook, + self, # model + task_adapt["dst_classes"], # model_classes + task_adapt["src_classes"], # chkpt_classes + ) + ) + + @staticmethod + def load_state_dict_pre_hook(model, model_classes, chkpt_classes, chkpt_dict, prefix, *args, **kwargs): + """Modify input state_dict according to class name matching before weight loading.""" + logger.info(f"----------------- CustomATSS.load_state_dict_pre_hook() called w/ prefix: {prefix}") + + # Dst to src mapping index + model_classes = list(model_classes) + chkpt_classes = list(chkpt_classes) + model2chkpt = map_class_names(model_classes, chkpt_classes) + logger.info(f"{chkpt_classes} -> {model_classes} ({model2chkpt})") + + model_dict = model.state_dict() + param_names = [ + "bbox_head.atss_cls.weight", + "bbox_head.atss_cls.bias", + ] + for model_name in param_names: + chkpt_name = prefix + model_name + if model_name not in model_dict or chkpt_name not in chkpt_dict: + logger.info(f"Skipping weight copy: {chkpt_name}") + continue + + # Mix weights + model_param = model_dict[model_name].clone() + chkpt_param = chkpt_dict[chkpt_name] + for model_t, ckpt_t in enumerate(model2chkpt): + if ckpt_t >= 0: + model_param[model_t].copy_(chkpt_param[ckpt_t]) + + # Replace checkpoint weight by mixed weights + chkpt_dict[chkpt_name] = model_param + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER, mark + from mmdeploy.utils import is_dynamic_shape + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_atss_detector.CustomATSS.simple_test" + ) + def custom_atss__simple_test(ctx, self, img, img_metas, **kwargs): + """Function for custom_atss__simple_test.""" + feat = self.extract_feat(img) + outs = self.bbox_head(feat) + bbox_results = self.bbox_head.get_bboxes(*outs, img_metas=img_metas, cfg=self.test_cfg, **kwargs) + + if ctx.cfg["dump_features"]: + feature_vector = FeatureVectorHook.func(feat) + cls_scores = outs[0] + postprocess_kwargs = { + "normalize": ctx.cfg["normalize_saliency_maps"], + "use_cls_softmax": ctx.cfg["softmax_saliency_maps"], + } + saliency_map = DetClassProbabilityMapHook(self, **postprocess_kwargs).func( + feature_map=cls_scores, cls_scores_provided=True + ) + return (*bbox_results, feature_vector, saliency_map) + + return bbox_results + + @mark("custom_atss_forward", inputs=["input"], outputs=["dets", "labels", "feats", "saliencies"]) + def __forward_impl(ctx, self, img, img_metas, **kwargs): + """Internal Function for __forward_impl.""" + assert isinstance(img, torch.Tensor) + + deploy_cfg = ctx.cfg + is_dynamic_flag = is_dynamic_shape(deploy_cfg) + # get origin input shape as tensor to support onnx dynamic shape + img_shape = torch._shape_as_tensor(img)[2:] + if not is_dynamic_flag: + img_shape = [int(val) for val in img_shape] + img_metas[0]["img_shape"] = img_shape + return self.simple_test(img, img_metas, **kwargs) + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_atss_detector.CustomATSS.forward" + ) + def custom_atss__forward(ctx, self, img, img_metas=None, return_loss=False, **kwargs): + """Internal Function for __forward for CustomATSS.""" + if img_metas is None: + img_metas = [{}] + else: + assert len(img_metas) == 1, "do not support aug_test" + img_metas = img_metas[0] + + if isinstance(img, list): + img = img[0] + + return __forward_impl(ctx, self, img, img_metas=img_metas, **kwargs) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_deformable_detr_detector.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_deformable_detr_detector.py new file mode 100644 index 00000000000..4a9a18c312d --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_deformable_detr_detector.py @@ -0,0 +1,96 @@ +"""OTX Deformable DETR Class for mmdetection detectors.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import functools + +from mmdet.models.builder import DETECTORS +from mmdet.models.detectors.deformable_detr import DeformableDETR + +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + ActivationMapHook, + FeatureVectorHook, +) +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.utils.logger import get_logger + +logger = get_logger() + + +@DETECTORS.register_module() +class CustomDeformableDETR(DeformableDETR): + """Custom Deformable DETR with task adapt. + + Deformable DETR does not support task adapt, so it just take task_adpat argument. + """ + + def __init__(self, *args, task_adapt=None, **kwargs): + super().__init__(*args, **kwargs) + if task_adapt: + self._register_load_state_dict_pre_hook( + functools.partial( + self.load_state_dict_pre_hook, + task_adapt["dst_classes"], # model_classes + task_adapt["src_classes"], # ckpt_classes + ) + ) + self.cls_layers = ["cls_branches"] + + def load_state_dict_pre_hook(self, model_classes, ckpt_classes, ckpt_dict, *args, **kwargs): + """Modify input state_dict according to class name matching before weight loading.""" + logger.info("----------------- CustomDeformableDETR.load_state_dict_pre_hook() called") + + model_classes = list(model_classes) + ckpt_classes = list(ckpt_classes) + model2ckpt = map_class_names(model_classes, ckpt_classes) + logger.info(f"{ckpt_classes} -> {model_classes} ({model2ckpt})") + + model_dict = self.state_dict() + param_names = [] + for model_name in model_dict: + for cls_layer in self.cls_layers: + if cls_layer in model_name: + param_names.append(model_name) + for param_name in param_names: + ckpt_name = param_name + if param_name not in model_dict or ckpt_name not in ckpt_dict: + logger.info(f"Skipping weight copy: {ckpt_name}") + continue + + # Mix weights + model_param = model_dict[param_name].clone() + ckpt_param = ckpt_dict[ckpt_name] + for model_t, ckpt_t in enumerate(model2ckpt): + if ckpt_t >= 0: + model_param[model_t].copy_(ckpt_param[ckpt_t]) + + # Replace checkpoint weight by mixed weights + ckpt_dict[ckpt_name] = model_param + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_deformable_detr_detector.CustomDeformableDETR.simple_test" + ) + def custom_deformable_detr__simple_test(ctx, self, img, img_metas, **kwargs): + """Function for custom_deformable_detr__simple_test.""" + height = int(img_metas[0]["img_shape"][0]) + width = int(img_metas[0]["img_shape"][1]) + img_metas[0]["batch_input_shape"] = (height, width) + img_metas[0]["img_shape"] = (height, width, 3) + feat = self.extract_feat(img) + outs = self.bbox_head(feat, img_metas) + bbox_results = self.bbox_head.get_bboxes(*outs, img_metas=img_metas, **kwargs) + + if ctx.cfg["dump_features"]: + feature_vector = FeatureVectorHook.func(feat) + cls_scores = outs[0] + saliency_map = ActivationMapHook(self).func(cls_scores) + return (*bbox_results, feature_vector, saliency_map) + + return bbox_results diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_dino_detector.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_dino_detector.py new file mode 100644 index 00000000000..3d739fcd292 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_dino_detector.py @@ -0,0 +1,170 @@ +"""OTX DINO Class for mmdetection detectors.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmdet.models.builder import DETECTORS + +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + ActivationMapHook, + FeatureVectorHook, +) +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +from otx.algorithms.detection.adapters.mmdet.models.detectors import CustomDeformableDETR +from otx.utils.logger import get_logger + +logger = get_logger() + + +@DETECTORS.register_module() +class CustomDINO(CustomDeformableDETR): + """Custom DINO detector.""" + + def __init__(self, *args, task_adapt=None, **kwargs): + super().__init__(*args, task_adapt=task_adapt, **kwargs) + + self.cls_layers.append("dn_query_generator.label_embedding.weight") + + def load_state_dict_pre_hook(self, model_classes, ckpt_classes, ckpt_dict, *args, **kwargs): + """Modify mmdet3.x version's weights before weight loading.""" + + if list(ckpt_dict.keys())[0] == "level_embed": + logger.info("----------------- CustomDINO.load_state_dict_pre_hook() called") + # This ckpt_dict comes from mmdet3.x + ckpt_classes = [ + "person", + "bicycle", + "car", + "motorcycle", + "airplane", + "bus", + "train", + "truck", + "boat", + "traffic light", + "fire hydrant", + "stop sign", + "parking meter", + "bench", + "bird", + "cat", + "dog", + "horse", + "sheep", + "cow", + "elephant", + "bear", + "zebra", + "giraffe", + "backpack", + "umbrella", + "handbag", + "tie", + "suitcase", + "frisbee", + "skis", + "snowboard", + "sports ball", + "kite", + "baseball bat", + "baseball glove", + "skateboard", + "surfboard", + "tennis racket", + "bottle", + "wine glass", + "cup", + "fork", + "knife", + "spoon", + "bowl", + "banana", + "apple", + "sandwich", + "orange", + "broccoli", + "carrot", + "hot dog", + "pizza", + "donut", + "cake", + "chair", + "couch", + "potted plant", + "bed", + "dining table", + "toilet", + "tv", + "laptop", + "mouse", + "remote", + "keyboard", + "cell phone", + "microwave", + "oven", + "toaster", + "sink", + "refrigerator", + "book", + "clock", + "vase", + "scissors", + "teddy bear", + "hair drier", + "toothbrush", + ] + ckpt_dict["bbox_head.transformer.level_embeds"] = ckpt_dict.pop("level_embed") + replaced_params = {} + for param in ckpt_dict: + new_param = None + if "encoder" in param or "decoder" in param: + new_param = "bbox_head.transformer." + param + new_param = new_param.replace("self_attn", "attentions.0") + new_param = new_param.replace("cross_attn", "attentions.1") + new_param = new_param.replace("ffn", "ffns.0") + elif param == "query_embedding.weight": + new_param = "bbox_head." + param + elif param == "dn_query_generator.label_embedding.weight": + new_param = "bbox_head.transformer." + param + elif "memory_trans" in param: + new_param = "bbox_head.transformer." + param + new_param = new_param.replace("memory_trans_fc", "enc_output") + new_param = new_param.replace("memory_trans_norm", "enc_output_norm") + if new_param is not None: + replaced_params[param] = new_param + + for origin, new in replaced_params.items(): + ckpt_dict[new] = ckpt_dict.pop(origin) + super().load_state_dict_pre_hook(model_classes, ckpt_classes, ckpt_dict, *args, **kwargs) + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_dino_detector.CustomDINO.simple_test" + ) + def custom_dino__simple_test(ctx, self, img, img_metas, **kwargs): + """Function for custom_dino__simple_test.""" + height = int(img_metas[0]["img_shape"][0]) + width = int(img_metas[0]["img_shape"][1]) + img_metas[0]["batch_input_shape"] = (height, width) + img_metas[0]["img_shape"] = (height, width, 3) + feats = self.extract_feat(img) + gt_bboxes = [None] * len(feats) + gt_labels = [None] * len(feats) + hidden_states, references, enc_output_class, enc_output_coord, _ = self.bbox_head.forward_transformer( + feats, gt_bboxes, gt_labels, img_metas + ) + cls_scores, bbox_preds = self.bbox_head(hidden_states, references) + bbox_results = self.bbox_head.get_bboxes( + cls_scores, bbox_preds, enc_output_class, enc_output_coord, img_metas=img_metas, **kwargs + ) + + if ctx.cfg["dump_features"]: + feature_vector = FeatureVectorHook.func(feats) + saliency_map = ActivationMapHook(self).func(cls_scores) + return (*bbox_results, feature_vector, saliency_map) + + return bbox_results diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_lite_dino.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_lite_dino.py new file mode 100644 index 00000000000..be71f1b8b7c --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_lite_dino.py @@ -0,0 +1,21 @@ +"""OTX Lite-DINO Class for object detection.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmdet.models.builder import DETECTORS + +from otx.algorithms.detection.adapters.mmdet.models.detectors import CustomDINO +from otx.utils.logger import get_logger + +logger = get_logger() + + +@DETECTORS.register_module() +class CustomLiteDINO(CustomDINO): + """Custom Lite-DINO for object detection.""" + + def load_state_dict_pre_hook(self, model_classes, ckpt_classes, ckpt_dict, *args, **kwargs): + """Modify official lite dino version's weights before weight loading.""" + super(CustomDINO, self).load_state_dict_pre_hook(model_classes, ckpt_classes, ckpt_dict, *args, *kwargs) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_maskrcnn_detector.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_maskrcnn_detector.py new file mode 100644 index 00000000000..3d80c497a4c --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_maskrcnn_detector.py @@ -0,0 +1,144 @@ +"""OTX MaskRCNN Class for mmdetection detectors.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import functools + +import torch +from mmdet.models.builder import DETECTORS +from mmdet.models.detectors.mask_rcnn import MaskRCNN + +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + FeatureVectorHook, +) +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.utils.logger import get_logger + +from .l2sp_detector_mixin import L2SPDetectorMixin +from .sam_detector_mixin import SAMDetectorMixin + +logger = get_logger() + +# pylint: disable=too-many-locals, protected-access, unused-argument + + +@DETECTORS.register_module() +class CustomMaskRCNN(SAMDetectorMixin, L2SPDetectorMixin, MaskRCNN): + """CustomMaskRCNN Class for mmdetection detectors.""" + + def __init__(self, *args, task_adapt=None, **kwargs): + super().__init__(*args, **kwargs) + + # Hook for class-sensitive weight loading + if task_adapt: + self._register_load_state_dict_pre_hook( + functools.partial( + self.load_state_dict_pre_hook, + self, # model + task_adapt["dst_classes"], # model_classes + task_adapt["src_classes"], # chkpt_classes + ) + ) + + @staticmethod + def load_state_dict_pre_hook(model, model_classes, chkpt_classes, chkpt_dict, prefix, *args, **kwargs): + """Modify input state_dict according to class name matching before weight loading.""" + logger.info(f"----------------- CustomMaskRCNN.load_state_dict_pre_hook() called w/ prefix: {prefix}") + + # Dst to src mapping index + model_dict = model.state_dict() + model_classes = list(model_classes) + chkpt_classes = list(chkpt_classes) + model2chkpt = map_class_names(model_classes, chkpt_classes) + logger.info(f"{chkpt_classes} -> {model_classes} ({model2chkpt})") + + # List of class-relevant params & their row-stride + param_strides = { + "roi_head.bbox_head.fc_cls.weight": 1, + "roi_head.bbox_head.fc_cls.bias": 1, + "roi_head.bbox_head.fc_reg.weight": 4, # 4 rows (bbox) for each class + "roi_head.bbox_head.fc_reg.bias": 4, + } + + for model_name, stride in param_strides.items(): + chkpt_name = prefix + model_name + if model_name not in model_dict or chkpt_name not in chkpt_dict: + logger.info(f"Skipping weight copy: {chkpt_name}") + continue + + # Mix weights + model_param = model_dict[model_name].clone() + chkpt_param = chkpt_dict[chkpt_name] + for model_t, ckpt_t in enumerate(model2chkpt): + if ckpt_t >= 0: + # Copying only matched weight rows + model_param[(model_t) * stride : (model_t + 1) * stride].copy_( + chkpt_param[(ckpt_t) * stride : (ckpt_t + 1) * stride] + ) + if model_param.shape[0] > len(model_classes * stride): # BG class + num_ckpt_class = len(chkpt_classes) + num_model_class = len(model_classes) + model_param[(num_model_class) * stride : (num_model_class + 1) * stride].copy_( + chkpt_param[(num_ckpt_class) * stride : (num_ckpt_class + 1) * stride] + ) + + # Replace checkpoint weight by mixed weights + chkpt_dict[chkpt_name] = model_param + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER, mark + from mmdeploy.utils import is_dynamic_shape + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_maskrcnn_detector.CustomMaskRCNN.simple_test" + ) + def custom_mask_rcnn__simple_test(ctx, self, img, img_metas, proposals=None, **kwargs): + """Function for custom_mask_rcnn__simple_test.""" + assert self.with_bbox, "Bbox head must be implemented." + x = self.backbone(img) + if self.with_neck: + x = self.neck(x) + if proposals is None: + proposals, _ = self.rpn_head.simple_test_rpn(x, img_metas) + out = self.roi_head.simple_test(x, proposals, img_metas, rescale=False) + + if ctx.cfg["dump_features"]: + feature_vector = FeatureVectorHook.func(x) + # Saliency map will be generated from predictions. Generate dummy saliency_map. + saliency_map = torch.empty(1, dtype=torch.uint8) + return (*out, feature_vector, saliency_map) + + return out + + @mark("custom_maskrcnn_forward", inputs=["input"], outputs=["dets", "labels", "masks", "feats", "saliencies"]) + def __forward_impl(ctx, self, img, img_metas, **kwargs): + """Internal Function for __forward_impl.""" + assert isinstance(img, torch.Tensor) + + deploy_cfg = ctx.cfg + is_dynamic_flag = is_dynamic_shape(deploy_cfg) + # get origin input shape as tensor to support onnx dynamic shape + img_shape = torch._shape_as_tensor(img)[2:] + if not is_dynamic_flag: + img_shape = [int(val) for val in img_shape] + img_metas[0]["img_shape"] = img_shape + return self.simple_test(img, img_metas, **kwargs) + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_maskrcnn_detector.CustomMaskRCNN.forward" + ) + def custom_maskrcnn__forward(ctx, self, img, img_metas=None, return_loss=False, **kwargs): + """Internal Function for __forward for CustomMaskRCNN.""" + if img_metas is None: + img_metas = [{}] + else: + assert len(img_metas) == 1, "do not support aug_test" + img_metas = img_metas[0] + + if isinstance(img, list): + img = img[0] + + return __forward_impl(ctx, self, img, img_metas=img_metas, **kwargs) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_maskrcnn_tile_optimized.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_maskrcnn_tile_optimized.py new file mode 100644 index 00000000000..2574a19f259 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_maskrcnn_tile_optimized.py @@ -0,0 +1,347 @@ +"""Custom MaskRCNN detector with tile classifier.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import numpy as np +import torch +from mmcls.models.necks.gap import GlobalAveragePooling +from mmcv.cnn import ConvModule +from mmcv.runner import auto_fp16 +from mmdet.models.builder import DETECTORS +from torch import nn + +from otx.algorithms.common.adapters.mmdeploy import is_mmdeploy_enabled +from otx.algorithms.common.adapters.nncf import no_nncf_trace + +from .custom_maskrcnn_detector import CustomMaskRCNN + + +class TileClassifier(torch.nn.Module): + """Tile classifier for the tile optimised MaskRCNN model.""" + + def __init__(self): + super().__init__() + self.fp16_enabled = False + self.features = nn.Sequential( + ConvModule(3, 64, 11, stride=4, padding=2, act_cfg=dict(type="ReLU")), + nn.MaxPool2d(kernel_size=3, stride=2), + ConvModule(64, 192, 5, padding=2, act_cfg=dict(type="ReLU")), + nn.MaxPool2d(kernel_size=3, stride=2), + ConvModule(192, 256, 3, padding=1, act_cfg=dict(type="ReLU")), + nn.MaxPool2d(kernel_size=3, stride=2), + ConvModule(256, 256, 3, padding=1, act_cfg=dict(type="ReLU")), + nn.MaxPool2d(kernel_size=3, stride=2), + ) + # NOTE: Original Adaptive Avg Pooling is replaced with Global Avg Pooling + # due to ONNX tracing issues: https://github.com/openvinotoolkit/training_extensions/pull/2337 + + self.gap = GlobalAveragePooling() + self.classifier = torch.nn.Sequential( + torch.nn.Linear(256, 256), + torch.nn.ReLU(inplace=True), + torch.nn.Linear(256, 256), + torch.nn.ReLU(inplace=True), + torch.nn.Linear(256, 1), + ) + + self.loss_func = torch.nn.BCEWithLogitsLoss() + self.sigmoid = torch.nn.Sigmoid() + + @auto_fp16() + def forward(self, img: torch.Tensor) -> torch.Tensor: + """Forward pass. + + Args: + img (torch.Tensor): input image + + Returns: + torch.Tensor: logits + """ + x = self.features(img) + x = self.gap(x) + y = self.classifier(x) + return y + + @auto_fp16() + def loss(self, pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor: + """Calculate BCE loss. + + Args: + pred (torch.Tensor): logits + target (torch.Tensor): binary target + + Returns: + torch.Tensor: BCE loss + """ + loss = self.loss_func(pred, target) + return loss + + @auto_fp16() + def simple_test(self, img: torch.Tensor) -> torch.Tensor: + """Simple test. + + Args: + img (torch.Tensor): input image + + Returns: + torch.Tensor: objectness score + """ + + out = self.forward(img) + with no_nncf_trace(): + return self.sigmoid(out).flatten() + + +# pylint: disable=too-many-ancestors +@DETECTORS.register_module() +class CustomMaskRCNNTileOptimized(CustomMaskRCNN): + """Custom MaskRCNN detector with tile classifier. + + Args: + *args: args + **kwargs: kwargs + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tile_classifier = TileClassifier() + + # pylint: disable=too-many-arguments + def forward_train( + self, img, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore=None, gt_masks=None, proposals=None, **kwargs + ): + """Forward pass during training. + + Joint training of tile classifier and MaskRCNN. + + Args: + img (torch.Tensor): input image + img_metas (list): image meta data + gt_bboxes (list): ground truth bounding boxes + gt_labels (list): ground truth labels + gt_bboxes_ignore (list, optional): ground truth bounding boxes to be ignored. Defaults to None. + gt_masks (list, optional): ground truth masks. Defaults to None. + proposals (list, optional): proposals. Defaults to None. + kwargs: kwargs + """ + + losses = dict() + targets = [len(gt_bbox) > 0 for gt_bbox in gt_bboxes] + pred = self.tile_classifier(img) + target_labels = torch.tensor(targets, device=pred.device) + loss_tile = self.tile_classifier.loss(pred, target_labels.unsqueeze(1).float()) + + losses.update(dict(loss_tile=loss_tile)) + + if not any(targets): + return losses + + img = img[targets] + img_metas = [item for keep, item in zip(targets, img_metas) if keep] + gt_labels = [item for keep, item in zip(targets, gt_labels) if keep] + gt_bboxes = [item for keep, item in zip(targets, gt_bboxes) if keep] + gt_masks = [item for keep, item in zip(targets, gt_masks) if keep] + if gt_bboxes_ignore: + gt_bboxes_ignore = [item for keep, item in zip(targets, gt_bboxes_ignore) if keep] + rcnn_loss = super().forward_train( + img, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore, gt_masks, proposals, **kwargs + ) + losses.update(rcnn_loss) + return losses + + @staticmethod + def make_fake_results(num_classes): + """Make fake results. + + Returns: + tuple: MaskRCNN output + """ + bbox_results = [] + mask_results = [] + for _ in range(num_classes): + bbox_results.append(np.empty((0, 5), dtype=np.float32)) + mask_results.append([]) + return bbox_results, mask_results + + def simple_test(self, img, img_metas, proposals=None, rescale=False, full_res_image=False): + """Simple test. + + Tile classifier is used to filter out images without any objects. + If no objects are present, empty results are returned. Otherwise, MaskRCNN is used to detect objects. + + Args: + img (torch.Tensor): input image + img_metas (list): image meta data + proposals (list, optional): proposals. Defaults to None. + rescale (bool, optional): rescale. Defaults to False. + full_res_image (bool, optional): if the image is full resolution or not. Defaults to False. + + Returns: + tuple: MaskRCNN output + """ + keep = self.tile_classifier.simple_test(img) > 0.45 + if isinstance(full_res_image, bool): + full_res_image = [full_res_image] + keep = full_res_image[0] | keep + + results = [] + for _ in range(len(img)): + fake_result = CustomMaskRCNNTileOptimized.make_fake_results(self.roi_head.bbox_head.num_classes) + results.append(fake_result) + + if any(keep): + img = img[keep] + img_metas = [item for keep, item in zip(keep, img_metas) if keep] + assert self.with_bbox, "Bbox head must be implemented." + x = self.extract_feat(img) + + if proposals is None: + proposal_list = self.rpn_head.simple_test_rpn(x, img_metas) + else: + proposal_list = proposals + maskrcnn_results = self.roi_head.simple_test(x, proposal_list, img_metas, rescale=rescale) + for i, keep_flag in enumerate(keep): + if keep_flag: + results[i] = maskrcnn_results.pop(0) + return results + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER, mark + from mmdeploy.utils import is_dynamic_shape + + # pylint: disable=ungrouped-imports + from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + FeatureVectorHook, + ) + + @mark("tile_classifier", inputs=["image"], outputs=["tile_prob"]) + def tile_classifier__simple_test_impl(self, img): + """Tile Classifier Simple Test Impl with added mmdeploy marking for model partitioning. + + Partition tile classifier by marking tile classifier in tracing. + + Args: + self (object): object + img: input image + + Returns: + torch.Tensor: objectness score + """ + return self.sigmoid(self.forward(img))[0][0] + + # pylint: disable=line-too-long, unused-argument + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_maskrcnn_tile_optimized.TileClassifier.simple_test" # noqa: E501 + ) + def tile_classifier__simple_test(ctx, self, img): + """Tile Classifier Simple Test Rewriter. + + Partition tile classifier by rewriting tile classifier simple test. + + Args: + ctx (object): object context + self (object): object + img (torch.Tensor): input image + + Returns: + torch.Tensor: objectness score + """ + return tile_classifier__simple_test_impl(self, img) + + # pylint: disable=protected-access + @mark( + "custom_maskrcnn_forward", + inputs=["input"], + outputs=["dets", "labels", "masks", "tile_prob", "feats", "saliencies"], + ) + def __forward_impl(ctx, self, img, img_metas, **kwargs): + """Custom MaskRCNN Forward Impl with added mmdeploy marking for model partitioning. + + Args: + ctx (object): object context + self (object): object + img (torch.Tensor): input image + img_metas (dict): image meta data + **kwargs: kwargs + + Returns: + simple test: MaskRCNN output + """ + assert isinstance(img, torch.Tensor) + + deploy_cfg = ctx.cfg + is_dynamic_flag = is_dynamic_shape(deploy_cfg) + # get origin input shape as tensor to support onnx dynamic shape + img_shape = torch._shape_as_tensor(img)[2:] + if not is_dynamic_flag: + img_shape = [int(val) for val in img_shape] + img_metas[0]["img_shape"] = img_shape + return self.simple_test(img, img_metas, **kwargs) + + # pylint: disable=line-too-long + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_maskrcnn_tile_optimized.CustomMaskRCNNTileOptimized.forward" # noqa: E501 + ) + def custom_maskrcnn__forward(ctx, self, img, img_metas=None, **kwargs): + """Custom MaskRCNN Forward Rewriter. + + Args: + ctx (object): object context + self (object): object + img (torch.Tensor): input image + img_metas (dict, optional): image meta data. Defaults to None. + **kwargs: kwargs + + Returns: + MaskRCNN output + """ + if img_metas is None: + img_metas = [{}] + else: + assert len(img_metas) == 1, "do not support aug_test" + img_metas = img_metas[0] + + if isinstance(img, list): + img = img[0] + + return __forward_impl(ctx, self, img, img_metas=img_metas, **kwargs) + + # pylint: disable=line-too-long + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_maskrcnn_tile_optimized.CustomMaskRCNNTileOptimized.simple_test" # noqa: E501 + ) + def custom_mask_rcnn__simple_test(ctx, self, img, img_metas, proposals=None): + """Custom Mask RCNN Simple Test Rewriter for ONNX tracing. + + Tile classifier is added to ONNX tracing in order to partition the model. + + Args: + ctx (object): object context + self (object): object + img (torch.Tensor): input image + img_metas (list): image meta data + proposals (list, optional): proposals. Defaults to None. + + Returns: + tuple: MaskRCNN output with tile classifier output + """ + assert self.with_bbox, "Bbox head must be implemented." + tile_prob = self.tile_classifier.simple_test(img) + + x = self.backbone(img) + if self.with_neck: + x = self.neck(x) + if proposals is None: + proposals, _ = self.rpn_head.simple_test_rpn(x, img_metas) + out = self.roi_head.simple_test(x, proposals, img_metas, rescale=False) + + if ctx.cfg["dump_features"]: + feature_vector = FeatureVectorHook.func(x) + # Saliency map will be generated from predictions. Generate dummy saliency_map. + saliency_map = torch.empty(1, dtype=torch.uint8) + return (*out, tile_prob, feature_vector, saliency_map) + + return (*out, tile_prob) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_single_stage_detector.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_single_stage_detector.py new file mode 100644 index 00000000000..f98b67c631d --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_single_stage_detector.py @@ -0,0 +1,205 @@ +"""OTX SSD Class for mmdetection detectors.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import functools + +import torch +from mmdet.models.builder import DETECTORS +from mmdet.models.detectors.single_stage import SingleStageDetector + +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + FeatureVectorHook, +) +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.algorithms.detection.adapters.mmdet.hooks.det_class_probability_map_hook import ( + DetClassProbabilityMapHook, +) +from otx.algorithms.detection.adapters.mmdet.models.detectors.loss_dynamics_mixin import DetLossDynamicsTrackingMixin +from otx.algorithms.detection.adapters.mmdet.models.loss_dyns import TrackingLossType +from otx.utils.logger import get_logger + +from .l2sp_detector_mixin import L2SPDetectorMixin +from .sam_detector_mixin import SAMDetectorMixin + +logger = get_logger() + +# TODO: Need to check pylint issues +# pylint: disable=abstract-method, too-many-locals, unused-argument, protected-access + + +@DETECTORS.register_module() +class CustomSingleStageDetector(SAMDetectorMixin, DetLossDynamicsTrackingMixin, L2SPDetectorMixin, SingleStageDetector): + """SAM optimizer & L2SP regularizer enabled custom SSD.""" + + TRACKING_LOSS_TYPE = (TrackingLossType.cls, TrackingLossType.bbox) + + def __init__(self, *args, task_adapt=None, **kwargs): + super().__init__(*args, **kwargs) + # Hook for class-sensitive weight loading + if task_adapt: + self._register_load_state_dict_pre_hook( + functools.partial( + self.load_state_dict_pre_hook, + self, # model + task_adapt["dst_classes"], # model_classes + task_adapt["src_classes"], # chkpt_classes + ) + ) + + def forward_train(self, img, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore=None, **kwargs): + """Forward function for CustomSSD. + + Args: + img (Tensor): Input images of shape (N, C, H, W). + Typically these should be mean centered and std scaled. + img_metas (list[dict]): A List of image info dict where each dict + has: 'img_shape', 'scale_factor', 'flip', and may also contain + 'filename', 'ori_shape', 'pad_shape', and 'img_norm_cfg'. + For details on the values of these keys see + :class:`mmdet.datasets.pipelines.Collect`. + gt_bboxes (list[Tensor]): Each item are the truth boxes for each + image in [tl_x, tl_y, br_x, br_y] format. + gt_labels (list[Tensor]): Class indices corresponding to each box + gt_bboxes_ignore (None | list[Tensor]): Specify which bounding + boxes can be ignored when computing the loss. + **kwargs (Any): Addition keyword arguments. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + batch_input_shape = tuple(img[0].size()[-2:]) + for img_meta in img_metas: + img_meta["batch_input_shape"] = batch_input_shape + x = self.extract_feat(img) + losses = self.bbox_head.forward_train(x, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore, **kwargs) + return losses + + @staticmethod + def load_state_dict_pre_hook(model, model_classes, chkpt_classes, chkpt_dict, prefix, *args, **kwargs): + """Modify input state_dict according to class name matching before weight loading.""" + logger.info(f"----------------- CustomSSD.load_state_dict_pre_hook() called w/ prefix: {prefix}") + + # Dst to chkpt mapping index (including BG class) + model_dict = model.state_dict() + chkpt_classes = list(chkpt_classes) + ["__BG__"] + model_classes = list(model_classes) + ["__BG__"] + num_chkpt_classes = len(chkpt_classes) + num_model_classes = len(model_classes) + model2chkpt = map_class_names(model_classes, chkpt_classes) + logger.info(f"{chkpt_classes} -> {model_classes}") + + # List of class-relevant params + if prefix + "bbox_head.cls_convs.0.weight" in chkpt_dict: + param_names = [ + "bbox_head.cls_convs.{}.weight", # normal + "bbox_head.cls_convs.{}.bias", + ] + elif prefix + "bbox_head.cls_convs.0.0.weight" in chkpt_dict: + param_names = [ + "bbox_head.cls_convs.{}.3.weight", # depth-wise: (0)conv -> (1)bn -> (2)act -> (3)conv + "bbox_head.cls_convs.{}.3.bias", + ] + else: + param_names = [] + + # Weight mixing + for level in range(10): # For each level (safer inspection loop than 'while True'. Mostly has 2~3 levels) + level_found = False + for model_name in param_names: + model_name = model_name.format(level) + chkpt_name = prefix + model_name + if model_name not in model_dict or chkpt_name not in chkpt_dict: + logger.info(f"Skipping weight copy: {chkpt_name}") + break + level_found = True + + model_param = model_dict[model_name].clone() + chkpt_param = chkpt_dict[chkpt_name] + + num_chkpt_anchors = int(chkpt_param.shape[0] / num_chkpt_classes) + num_model_anchors = int(model_param.shape[0] / num_model_classes) + num_anchors = min(num_chkpt_anchors, num_model_anchors) + logger.info( + f"Mixing {model_name}: {num_chkpt_anchors}x{num_chkpt_classes} -> " + f"{num_model_anchors}x{num_model_classes} anchors" + ) + + for anchor_idx in range(num_anchors): # For each anchor + for model_t, ckpt_t in enumerate(model2chkpt): + if ckpt_t >= 0: + # Copying only matched weight rows + model_param[anchor_idx * num_model_classes + model_t].copy_( + chkpt_param[anchor_idx * num_chkpt_classes + ckpt_t] + ) + + # Replace checkpoint weight by mixed weights + chkpt_dict[chkpt_name] = model_param + if not level_found: + break + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER, mark + from mmdeploy.utils import is_dynamic_shape + + SIMPLE_TEST_IMPORT = ( + "otx.algorithms.detection.adapters.mmdet.models.detectors." + "custom_single_stage_detector.CustomSingleStageDetector.simple_test" + ) + + @FUNCTION_REWRITER.register_rewriter(SIMPLE_TEST_IMPORT) + def custom_single_stage_detector__simple_test(ctx, self, img, img_metas, **kwargs): + """Function for custom_single_stage_detector__simple_test.""" + feat = self.extract_feat(img) + outs = self.bbox_head(feat) + bbox_results = self.bbox_head.get_bboxes(*outs, img_metas=img_metas, cfg=self.test_cfg, **kwargs) + + if ctx.cfg["dump_features"]: + feature_vector = FeatureVectorHook.func(feat) + cls_scores = outs[0] + postprocess_kwargs = { + "normalize": ctx.cfg["normalize_saliency_maps"], + "use_cls_softmax": ctx.cfg["softmax_saliency_maps"], + } + saliency_map = DetClassProbabilityMapHook(self, **postprocess_kwargs).func( + cls_scores, cls_scores_provided=True + ) + return (*bbox_results, feature_vector, saliency_map) + + return bbox_results + + @mark("custom_ssd_forward", inputs=["input"], outputs=["dets", "labels", "feats", "saliencies"]) + def __forward_impl(ctx, self, img, img_metas, **kwargs): + """Internal Function for __forward_impl.""" + assert isinstance(img, torch.Tensor) + + deploy_cfg = ctx.cfg + is_dynamic_flag = is_dynamic_shape(deploy_cfg) + # get origin input shape as tensor to support onnx dynamic shape + img_shape = torch._shape_as_tensor(img)[2:] + if not is_dynamic_flag: + img_shape = [int(val) for val in img_shape] + img_metas[0]["img_shape"] = img_shape + return self.simple_test(img, img_metas, **kwargs) + + FORWARD_IMPORT = ( + "otx.algorithms.detection.adapters.mmdet.models.detectors." + "custom_single_stage_detector.CustomSingleStageDetector.forward" + ) + + @FUNCTION_REWRITER.register_rewriter(FORWARD_IMPORT) + def custom_ssd__forward(ctx, self, img, img_metas=None, return_loss=False, **kwargs): + """Internal Function for __forward for CustomSSD.""" + if img_metas is None: + img_metas = [{}] + else: + assert len(img_metas) == 1, "do not support aug_test" + img_metas = img_metas[0] + + if isinstance(img, list): + img = img[0] + + return __forward_impl(ctx, self, img, img_metas=img_metas, **kwargs) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_two_stage_detector.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_two_stage_detector.py new file mode 100644 index 00000000000..1e9f663fffc --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_two_stage_detector.py @@ -0,0 +1,87 @@ +"""OTX Two Stage Detector Class for mmdetection detectors.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import functools + +from mmdet.models.builder import DETECTORS +from mmdet.models.detectors.two_stage import TwoStageDetector + +from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.utils.logger import get_logger + +from .l2sp_detector_mixin import L2SPDetectorMixin +from .sam_detector_mixin import SAMDetectorMixin + +logger = get_logger() + +# pylint: disable=too-many-locals, unused-argument + + +@DETECTORS.register_module() +class CustomTwoStageDetector(SAMDetectorMixin, L2SPDetectorMixin, TwoStageDetector): + """SAM optimizer & L2SP regularizer enabled custom 2-stage detector.""" + + def __init__(self, *args, task_adapt=None, **kwargs): + super().__init__(*args, **kwargs) + + # Hook for class-sensitive weight loading + if task_adapt: + self._register_load_state_dict_pre_hook( + functools.partial( + self.load_state_dict_pre_hook, + self, # model + task_adapt["dst_classes"], # model_classes + task_adapt["src_classes"], # chkpt_classes + ) + ) + + def forward_train(self, img, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore=None, **kwargs): + """Forward function for CustomTwoStageDetector.""" + return super().forward_train(img, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore=gt_bboxes_ignore) + + @staticmethod + def load_state_dict_pre_hook(model, model_classes, chkpt_classes, chkpt_dict, prefix, *args, **kwargs): + """Modify input state_dict according to class name matching before weight loading.""" + logger.info(f"----------------- CustomTwoStageDetector.load_state_dict_pre_hook() called w/ prefix: {prefix}") + + # Dst to src mapping index + model_dict = model.state_dict() + model_classes = list(model_classes) + chkpt_classes = list(chkpt_classes) + model2chkpt = map_class_names(model_classes, chkpt_classes) + logger.info(f"{chkpt_classes} -> {model_classes} ({model2chkpt})") + + # List of class-relevant params & their row-stride + param_strides = { + "roi_head.bbox_head.fc_cls.weight": 1, + "roi_head.bbox_head.fc_cls.bias": 1, + "roi_head.bbox_head.fc_reg.weight": 4, # 4 rows (bbox) for each class + "roi_head.bbox_head.fc_reg.bias": 4, + } + + for model_name, stride in param_strides.items(): + chkpt_name = prefix + model_name + if model_name not in model_dict or chkpt_name not in chkpt_dict: + logger.info(f"Skipping weight copy: {chkpt_name}") + continue + + # Mix weights + model_param = model_dict[model_name].clone() + chkpt_param = chkpt_dict[chkpt_name] + for model_t, ckpt_t in enumerate(model2chkpt): + if ckpt_t >= 0: + # Copying only matched weight rows + model_param[(model_t) * stride : (model_t + 1) * stride].copy_( + chkpt_param[(ckpt_t) * stride : (ckpt_t + 1) * stride] + ) + if model_param.shape[0] > len(model_classes * stride): # BG class + num_ckpt_class = len(chkpt_classes) + num_model_class = len(model_classes) + model_param[(num_model_class) * stride : (num_model_class + 1) * stride].copy_( + chkpt_param[(num_ckpt_class) * stride : (num_ckpt_class + 1) * stride] + ) + + # Replace checkpoint weight by mixed weights + chkpt_dict[chkpt_name] = model_param diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_vfnet_detector.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_vfnet_detector.py new file mode 100644 index 00000000000..14e746f76f7 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_vfnet_detector.py @@ -0,0 +1,79 @@ +"""OTX CustomVFNet Class for mmdetection detectors.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import functools + +from mmdet.models.builder import DETECTORS +from mmdet.models.detectors.vfnet import VFNet + +from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.algorithms.detection.adapters.mmdet.models.detectors.loss_dynamics_mixin import DetLossDynamicsTrackingMixin +from otx.algorithms.detection.adapters.mmdet.models.loss_dyns import TrackingLossType +from otx.utils.logger import get_logger + +from .l2sp_detector_mixin import L2SPDetectorMixin +from .sam_detector_mixin import SAMDetectorMixin + +logger = get_logger() + +# TODO: Need to fix pylint issues +# pylint: disable=abstract-method, too-many-locals, unused-argument + + +@DETECTORS.register_module() +class CustomVFNet(SAMDetectorMixin, DetLossDynamicsTrackingMixin, L2SPDetectorMixin, VFNet): + """SAM optimizer & L2SP regularizer enabled custom VFNet.""" + + TRACKING_LOSS_TYPE = (TrackingLossType.cls, TrackingLossType.bbox, TrackingLossType.bbox_refine) + + def __init__(self, *args, task_adapt=None, **kwargs): + super().__init__(*args, **kwargs) + + # Hook for class-sensitive weight loading + if task_adapt: + self._register_load_state_dict_pre_hook( + functools.partial( + self.load_state_dict_pre_hook, + self, # model + task_adapt["dst_classes"], # model_classes + task_adapt["src_classes"], # chkpt_classes + ) + ) + + def forward_train(self, img, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore=None, **kwargs): + """Forward function for CustomVFNet.""" + return super().forward_train(img, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore=gt_bboxes_ignore) + + @staticmethod + def load_state_dict_pre_hook(model, model_classes, chkpt_classes, chkpt_dict, prefix, *args, **kwargs): + """Modify input state_dict according to class name matching before weight loading.""" + logger.info(f"----------------- CustomVFNet.load_state_dict_pre_hook() called w/ prefix: {prefix}") + + # Dst to src mapping index + model_classes = list(model_classes) + chkpt_classes = list(chkpt_classes) + model2chkpt = map_class_names(model_classes, chkpt_classes) + logger.info(f"{chkpt_classes} -> {model_classes} ({model2chkpt})") + + model_dict = model.state_dict() + param_names = [ + "bbox_head.vfnet_cls.weight", + "bbox_head.vfnet_cls.bias", + ] + for model_name in param_names: + chkpt_name = prefix + model_name + if model_name not in model_dict or chkpt_name not in chkpt_dict: + logger.info(f"Skipping weight copy: {chkpt_name}") + continue + + # Mix weights + model_param = model_dict[model_name].clone() + chkpt_param = chkpt_dict[chkpt_name] + for model_t, ckpt_t in enumerate(model2chkpt): + if ckpt_t >= 0: + model_param[model_t].copy_(chkpt_param[ckpt_t]) + + # Replace checkpoint weight by mixed weights + chkpt_dict[chkpt_name] = model_param diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_yolox_detector.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_yolox_detector.py new file mode 100644 index 00000000000..c8658c489f6 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/custom_yolox_detector.py @@ -0,0 +1,192 @@ +"""OTX YOLOX Class for mmdetection detectors.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import functools + +import torch +from mmdet.models.builder import DETECTORS +from mmdet.models.detectors.yolox import YOLOX + +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + FeatureVectorHook, +) +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.algorithms.detection.adapters.mmdet.hooks.det_class_probability_map_hook import ( + DetClassProbabilityMapHook, +) +from otx.algorithms.detection.adapters.mmdet.models.detectors.loss_dynamics_mixin import ( + DetLossDynamicsTrackingMixin, +) +from otx.algorithms.detection.adapters.mmdet.models.loss_dyns import TrackingLossType +from otx.utils.logger import get_logger + +from .l2sp_detector_mixin import L2SPDetectorMixin +from .sam_detector_mixin import SAMDetectorMixin + +logger = get_logger() + +# TODO: Need to fix pylint issues +# pylint: disable=too-many-locals, unused-argument, protected-access, abstract-method + + +@DETECTORS.register_module() +class CustomYOLOX(SAMDetectorMixin, DetLossDynamicsTrackingMixin, L2SPDetectorMixin, YOLOX): + """SAM optimizer & L2SP regularizer enabled custom YOLOX.""" + + TRACKING_LOSS_TYPE = (TrackingLossType.cls, TrackingLossType.bbox) + + def __init__(self, *args, task_adapt=None, **kwargs): + super().__init__(*args, **kwargs) + + # Hook for class-sensitive weight loading + if task_adapt: + self._register_load_state_dict_pre_hook( + functools.partial( + self.load_state_dict_pre_hook, + self, # model + task_adapt["dst_classes"], # model_classes + task_adapt["src_classes"], # chkpt_classes + ) + ) + + def forward_train(self, img, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore=None, **kwargs): + """Forward function for CustomYOLOX.""" + return super().forward_train(img, img_metas, gt_bboxes, gt_labels, gt_bboxes_ignore=gt_bboxes_ignore) + + def extract_feat(self, img): + """Directly extract features from the backbone+neck.""" + x = self.backbone(img) + if self.with_neck: + x = self.neck(x) + return x + + @staticmethod + def load_state_dict_pre_hook(model, model_classes, chkpt_classes, chkpt_dict, prefix, *args, **kwargs): + """Modify input state_dict according to class name matching before weight loading.""" + logger.info(f"----------------- CustomYOLOX.load_state_dict_pre_hook() called w/ prefix: {prefix}") + + # Dst to src mapping index + model_classes = list(model_classes) + chkpt_classes = list(chkpt_classes) + model2chkpt = map_class_names(model_classes, chkpt_classes) + logger.info(f"{chkpt_classes} -> {model_classes} ({model2chkpt})") + + model_dict = model.state_dict() + param_names = [ + "bbox_head.multi_level_conv_cls.0.weight", + "bbox_head.multi_level_conv_cls.0.bias", + "bbox_head.multi_level_conv_cls.1.weight", + "bbox_head.multi_level_conv_cls.1.bias", + "bbox_head.multi_level_conv_cls.2.weight", + "bbox_head.multi_level_conv_cls.2.bias", + ] + for model_name in param_names: + chkpt_name = prefix + model_name + if model_name not in model_dict or chkpt_name not in chkpt_dict: + logger.info(f"Skipping weight copy: {chkpt_name}") + continue + + # Mix weights + model_param = model_dict[model_name].clone() + chkpt_param = chkpt_dict[chkpt_name] + for model_t, ckpt_t in enumerate(model2chkpt): + if ckpt_t >= 0: + model_param[model_t].copy_(chkpt_param[ckpt_t]) + + # Replace checkpoint weight by mixed weights + chkpt_dict[chkpt_name] = model_param + + def onnx_export(self, img, img_metas): + """Test function without test time augmentation. + + Args: + img (torch.Tensor): input images. + img_metas (list[dict]): List of image information. + + Returns: + tuple[Tensor, Tensor]: dets of shape [N, num_det, 5] + and class labels of shape [N, num_det]. + """ + x = self.extract_feat(img) + outs = self.bbox_head(x) + # get origin input shape to support onnx dynamic shape + + # get shape as tensor + img_shape = torch._shape_as_tensor(img)[2:] + img_metas[0]["img_shape_for_onnx"] = img_shape + # get pad input shape to support onnx dynamic shape for exporting + # `CornerNet` and `CentripetalNet`, which 'pad_shape' is used + # for inference + img_metas[0]["pad_shape_for_onnx"] = img_shape + + if len(outs) == 2: + # add dummy score_factor + outs = (*outs, None) + + # FIXME: mmdet does not support yolox onnx export for now + # This is a temporary workaround + # https://github.com/open-mmlab/mmdetection/issues/6487 + det_bboxes, det_labels = self.bbox_head.get_bboxes(*outs, img_metas)[0] + + return det_bboxes, det_labels + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER, mark + from mmdeploy.utils import is_dynamic_shape + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_yolox_detector.CustomYOLOX.simple_test" + ) + def custom_yolox__simple_test(ctx, self, img, img_metas, **kwargs): + """Function for custom_yolox__simple_test.""" + feat = self.extract_feat(img) + outs = self.bbox_head(feat) + bbox_results = self.bbox_head.get_bboxes(*outs, img_metas=img_metas, cfg=self.test_cfg, **kwargs) + + if ctx.cfg["dump_features"]: + feature_vector = FeatureVectorHook.func(feat) + cls_scores = outs[0] + postprocess_kwargs = { + "use_cls_softmax": ctx.cfg["softmax_saliency_maps"], + "normalize": ctx.cfg["normalize_saliency_maps"], + } + saliency_map = DetClassProbabilityMapHook(self, **postprocess_kwargs).func( + cls_scores, cls_scores_provided=True + ) + return (*bbox_results, feature_vector, saliency_map) + + return bbox_results + + @mark("custom_yolox_forward", inputs=["input"], outputs=["dets", "labels", "feats", "saliencies"]) + def __forward_impl(ctx, self, img, img_metas, **kwargs): + """Internal Function for __forward_impl.""" + assert isinstance(img, torch.Tensor) + + deploy_cfg = ctx.cfg + is_dynamic_flag = is_dynamic_shape(deploy_cfg) + # get origin input shape as tensor to support onnx dynamic shape + img_shape = torch._shape_as_tensor(img)[2:] + if not is_dynamic_flag: + img_shape = [int(val) for val in img_shape] + img_metas[0]["img_shape"] = img_shape + return self.simple_test(img, img_metas, **kwargs) + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models.detectors.custom_yolox_detector.CustomYOLOX.forward" + ) + def custom_yolox__forward(ctx, self, img, img_metas=None, return_loss=False, **kwargs): + """Internal Function for __forward for CustomYOLOX.""" + if img_metas is None: + img_metas = [{}] + else: + assert len(img_metas) == 1, "do not support aug_test" + img_metas = img_metas[0] + + if isinstance(img, list): + img = img[0] + + return __forward_impl(ctx, self, img, img_metas=img_metas, **kwargs) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/l2sp_detector_mixin.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/l2sp_detector_mixin.py new file mode 100644 index 00000000000..9d213cdef27 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/l2sp_detector_mixin.py @@ -0,0 +1,28 @@ +"""L2SPDetectorMixin Class for mmdetection detectors.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.detection.adapters.mmdet.models.losses.l2sp_loss import L2SPLoss + + +class L2SPDetectorMixin: + """L2SP-enabled detector mix-in.""" + + def __init__(self, l2sp_ckpt=None, l2sp_weight=None, **kwargs): + super().__init__(**kwargs) + if l2sp_ckpt and l2sp_weight: + self.l2sp = L2SPLoss(self, l2sp_ckpt, l2sp_weight) + print("L2SP initilaized!") + else: + self.l2sp = None + + def forward_train(self, *args, **kwargs): + """Forward function for L2SPDetectorMixin.""" + losses = super().forward_train(*args, **kwargs) + + # Add L2SP regularization loss + # (Assuming weight decay is disable in optimizer setting) + if self.l2sp: + losses.update(dict(loss_l2sp=self.l2sp())) + return losses diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/loss_dynamics_mixin.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/loss_dynamics_mixin.py new file mode 100644 index 00000000000..8f3fe5ddfd0 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/loss_dynamics_mixin.py @@ -0,0 +1,131 @@ +"""LossDynamics Mix-in for detection tasks.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from collections import defaultdict +from typing import Dict, Sequence, Tuple + +import datumaro as dm +import numpy as np +import pandas as pd + +from otx.algorithms.detection.adapters.mmdet.models.loss_dyns import TrackingLossType +from otx.api.entities.dataset_item import DatasetItemEntityWithID +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.shapes.rectangle import Rectangle +from otx.core.data.noisy_label_detection import ( + LossDynamicsTracker, + LossDynamicsTrackingMixin, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +class DetLossDynamicsTracker(LossDynamicsTracker): + """Loss dynamics tracker for detection tasks.""" + + TASK_NAME = "OTX-Det" + + def __init__(self, tracking_loss_types: Sequence[TrackingLossType]) -> None: + super().__init__() + self._loss_dynamics: Dict[TrackingLossType, Dict] = { + loss_type: defaultdict(list) for loss_type in tracking_loss_types + } + + def _convert_anns(self, item: DatasetItemEntityWithID): + labels = [] + + cnt = 0 + for ann in item.get_annotations(preserve_id=True): + if isinstance(ann.shape, Rectangle): + for label in ann.get_labels(): + bbox = dm.Bbox( + x=ann.shape.x1 * item.width, + y=ann.shape.y1 * item.height, + w=ann.shape.width * item.width, + h=ann.shape.height * item.height, + label=self.otx_label_map[label.id_], + id=cnt, + ) + labels.append(bbox) + self.otx_ann_id_to_dm_ann_map[(item.id_, ann.id_)] = bbox + cnt += 1 + + return labels + + def init_with_otx_dataset(self, otx_dataset: DatasetEntity[DatasetItemEntityWithID]) -> None: + """DatasetEntity should be injected to the tracker for the initialization.""" + self.otx_ann_id_to_dm_ann_map: Dict[Tuple[str, str], dm.Bbox] = {} + super().init_with_otx_dataset(otx_dataset) + + def accumulate(self, outputs, iter) -> None: + """Accumulate training loss dynamics for each training step.""" + for key, loss_dyns in outputs.items(): + if isinstance(key, TrackingLossType): + for (entity_id, ann_id), value in loss_dyns.items(): + self._loss_dynamics[key][(entity_id, ann_id)].append((iter, value)) + + def export(self, output_path: str) -> None: + """Export loss dynamics statistics to Datumaro format.""" + dfs = [ + pd.DataFrame.from_dict( + { + k: (np.array([iter for iter, _ in arr]), np.array([value for _, value in arr])) + for k, arr in loss_dyns.items() + }, + orient="index", + columns=["iters", f"loss_dynamics_{key.name}"], + ) + for key, loss_dyns in self._loss_dynamics.items() + ] + df = pd.concat(dfs, axis=1) + df = df.loc[:, ~df.columns.duplicated()] + + for (entity_id, ann_id), row in df.iterrows(): + ann = self.otx_ann_id_to_dm_ann_map.get((entity_id, ann_id), None) + if ann: + ann.attributes = row.to_dict() + + self._export_dataset.export(output_path, format="datumaro") + + +class DetLossDynamicsTrackingMixin(LossDynamicsTrackingMixin): + """Mix-in to track loss dynamics during training for classification tasks.""" + + TRACKING_LOSS_TYPE: Tuple[TrackingLossType, ...] = () + + def __init__(self, track_loss_dynamics: bool = False, **kwargs): + if track_loss_dynamics: + head_cfg = kwargs.get("bbox_head", None) + head_type = head_cfg.get("type", None) + assert head_type is not None, "head_type should be specified from the config." + new_head_type = head_type + "TrackingLossDynamics" + head_cfg["type"] = new_head_type + logger.info(f"Replace head_type from {head_type} to {new_head_type}.") + + super().__init__(**kwargs) + + # This should be called after super().__init__(), + # since LossDynamicsTrackingMixin.__init__() creates self._loss_dyns_tracker + self._loss_dyns_tracker = DetLossDynamicsTracker(self.TRACKING_LOSS_TYPE) + + def train_step(self, data, optimizer): + """The iteration step during training.""" + + outputs = super().train_step(data, optimizer) + + if self.loss_dyns_tracker.initialized: + gt_ann_ids = [item["gt_ann_ids"] for item in data["img_metas"]] + + to_update = {} + for key, loss_dyns in self.bbox_head.loss_dyns.items(): + to_update[key] = {} + for (batch_idx, bbox_idx), value in loss_dyns.items(): + entity_id, ann_id = gt_ann_ids[batch_idx][bbox_idx] + to_update[key][(entity_id, ann_id)] = value.mean + + outputs.update(to_update) + + return outputs diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/mean_teacher.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/mean_teacher.py new file mode 100644 index 00000000000..150140c9d7d --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/mean_teacher.py @@ -0,0 +1,341 @@ +"""UnbiasedTeacher Class for mmdetection detectors.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import functools + +import cv2 +import mmcv +import numpy as np +import torch +from mmdet.core import bbox2result, bbox2roi +from mmdet.core.mask.structures import BitmapMasks +from mmdet.models import DETECTORS, build_detector +from mmdet.models.detectors import BaseDetector +from torch import distributed as dist + +from otx.utils.logger import get_logger + +from .sam_detector_mixin import SAMDetectorMixin + +logger = get_logger() + +# TODO: Need to fix pylint issues +# pylint: disable=abstract-method, too-many-locals, unused-argument + + +@DETECTORS.register_module() +class MeanTeacher(SAMDetectorMixin, BaseDetector): + """Mean teacher framework for detection and instance segmentation.""" + + def __init__( + self, + arch_type, + unlabeled_loss_weights={"cls": 1.0, "bbox": 1.0, "mask": 1.0}, + pseudo_conf_thresh=0.7, + bg_loss_weight=-1.0, + min_pseudo_label_ratio=0.0, + visualize=False, + filter_empty_annotations=False, + **kwargs, + ): + super().__init__() + self.unlabeled_loss_weights = unlabeled_loss_weights + self.pseudo_conf_thresh = pseudo_conf_thresh + self.bg_loss_weight = bg_loss_weight + self.min_pseudo_label_ratio = min_pseudo_label_ratio + self.filter_empty_annotations = filter_empty_annotations + self.visualize = visualize + cfg = kwargs.copy() + cfg["type"] = arch_type + self.model_s = build_detector(cfg) + self.model_t = copy.deepcopy(self.model_s) + self.model_t.eval() + # warmup for first epochs + self.enable_unlabeled_loss(False) + self.cur_iter = 0 + + # Hooks for super_type transparent weight load/save + self._register_state_dict_hook(self.state_dict_hook) + self._register_load_state_dict_pre_hook(functools.partial(self.load_state_dict_pre_hook, self)) + + def extract_feat(self, imgs): + """Extract features for UnbiasedTeacher.""" + return self.model_t.extract_feat(imgs) + + def simple_test(self, img, img_metas, **kwargs): + """Test from img with UnbiasedTeacher.""" + return self.model_t.simple_test(img, img_metas, **kwargs) + + def aug_test(self, imgs, img_metas, **kwargs): + """Aug Test from img with UnbiasedTeacher.""" + return self.model_t.aug_test(imgs, img_metas, **kwargs) + + def forward_dummy(self, img, **kwargs): + """Dummy forward function for UnbiasedTeacher.""" + return self.model_t.forward_dummy(img, **kwargs) + + def enable_unlabeled_loss(self, mode=True): + """Enable function for UnbiasedTeacher unlabeled loss.""" + self.unlabeled_loss_enabled = mode + + def forward_teacher(self, img, img_metas): + """Method to extract predictions (pseudo labeles) from teacher.""" + x = self.model_t.extract_feat(img) + proposal_list = self.model_t.rpn_head.simple_test_rpn(x, img_metas) + + det_bboxes, det_labels = self.model_t.roi_head.simple_test_bboxes( + x, img_metas, proposal_list, self.model_t.test_cfg.rcnn, rescale=False + ) + + bbox_results = [ + bbox2result(det_bboxes[i], det_labels[i], self.model_t.roi_head.bbox_head.num_classes) + for i in range(len(det_bboxes)) + ] + + if not self.model_t.with_mask: + return bbox_results + else: + ori_shapes = tuple(meta["ori_shape"] for meta in img_metas) + scale_factors = tuple(meta["scale_factor"] for meta in img_metas) + + num_imgs = len(det_bboxes) + if all(det_bbox.shape[0] == 0 for det_bbox in det_bboxes): + segm_results = [ + [[] for _ in range(self.model_t.roi_head.mask_head.num_classes)] for _ in range(num_imgs) + ] + else: + _bboxes = [det_bboxes[i][:, :4] for i in range(len(det_bboxes))] + mask_rois = bbox2roi(_bboxes) + mask_results = self.model_t.roi_head._mask_forward(x, mask_rois) + mask_pred = mask_results["mask_pred"] + # split batch mask prediction back to each image + num_mask_roi_per_img = [len(det_bbox) for det_bbox in det_bboxes] + mask_preds = mask_pred.split(num_mask_roi_per_img, 0) + + # apply mask post-processing to each image individually + segm_results = [] + for i in range(num_imgs): + if det_bboxes[i].shape[0] == 0: + segm_results.append([[] for _ in range(self.model_t.roi_head.mask_head.num_classes)]) + else: + segm_result = self.model_t.roi_head.mask_head.get_scaled_seg_masks( + mask_preds[i], + _bboxes[i], + det_labels[i], + self.model_t.test_cfg.rcnn, + ori_shapes[i], + scale_factors[i], + rescale=False, + ) + segm_results.append(segm_result) + + return list(zip(bbox_results, segm_results)) + + def forward_train( + self, img, img_metas, img0=None, gt_bboxes=None, gt_labels=None, gt_masks=None, gt_bboxes_ignore=None, **kwargs + ): + """Forward function for UnbiasedTeacher.""" + losses = {} + self.cur_iter += 1 + # Supervised loss + # TODO: check img0 only option (which is common for mean teacher method) + if img0 is not None: + img = torch.cat((img0, img)) # weak + hard augmented images + img_metas = img_metas + img_metas + gt_bboxes = gt_bboxes + gt_bboxes + gt_labels = gt_labels + gt_labels + gt_bboxes_ignore = gt_bboxes_ignore + gt_bboxes_ignore if gt_bboxes_ignore else None + gt_masks = gt_masks + gt_masks if self.model_s.with_mask else None + + forward_train = functools.partial( + self.model_s.forward_train, + img, + img_metas, + gt_bboxes, + gt_labels, + gt_bboxes_ignore if gt_bboxes_ignore else None, + ) + if self.model_s.with_mask: + sl_losses = forward_train(gt_masks=gt_masks) + else: + sl_losses = forward_train() + losses.update(sl_losses) + + if not self.unlabeled_loss_enabled: + return losses + + # Pseudo labels from teacher + ul_args = kwargs.get("extra_0", {}) + ul_img = ul_args.get("img") + ul_img0 = ul_args.get("img0") + ul_img_metas = ul_args.get("img_metas") + if ul_img is None: + return losses + with torch.no_grad(): + if self.model_t.with_mask: + teacher_outputs = self.forward_teacher(ul_img0, ul_img_metas) + else: + teacher_outputs = self.model_t.forward_test([ul_img0], [ul_img_metas], rescale=False) + current_device = ul_img0[0].device + pseudo_bboxes, pseudo_labels, pseudo_masks, pseudo_ratio = self.generate_pseudo_labels( + teacher_outputs, device=current_device, img_meta=ul_img_metas, **kwargs + ) + non_empty = [bool(len(i)) for i in pseudo_labels] if self.filter_empty_annotations else [True] + get_unlabeled_loss = pseudo_ratio >= self.min_pseudo_label_ratio and any(non_empty) + + if dist.is_initialized(): + reduced_get_unlabeled_loss = torch.tensor(int(get_unlabeled_loss)).to(current_device) + dist.all_reduce(reduced_get_unlabeled_loss) + if dont_have_to_train := not get_unlabeled_loss and reduced_get_unlabeled_loss > 0: + get_unlabeled_loss = True + non_empty[0] = True + + losses.update(ps_ratio=torch.tensor([pseudo_ratio], device=current_device)) + + # Unsupervised loss + # Compute only if min_pseudo_label_ratio is reached + if get_unlabeled_loss: + if self.filter_empty_annotations: + pseudo_bboxes = [pb for i, pb in enumerate(pseudo_bboxes) if non_empty[i]] + pseudo_labels = [pl for i, pl in enumerate(pseudo_labels) if non_empty[i]] + pseudo_masks = [pm for i, pm in enumerate(pseudo_masks) if non_empty[i]] + ul_img_metas = [im for i, im in enumerate(ul_img_metas) if non_empty[i]] + ul_img = ul_img[non_empty] + if self.visualize: + self._visual_online(ul_img, pseudo_bboxes, pseudo_labels) + if self.bg_loss_weight >= 0.0: + self.model_s.bbox_head.bg_loss_weight = self.bg_loss_weight + if self.model_t.with_mask: + ul_losses = self.model_s.forward_train( + ul_img, ul_img_metas, pseudo_bboxes, pseudo_labels, gt_masks=pseudo_masks + ) + else: + ul_losses = self.model_s.forward_train(ul_img, ul_img_metas, pseudo_bboxes, pseudo_labels) + + if self.bg_loss_weight >= 0.0: + self.model_s.bbox_head.bg_loss_weight = -1.0 + + for ul_loss_name in ul_losses.keys(): + if ul_loss_name.startswith("loss_"): + ul_loss = ul_losses[ul_loss_name] + target_loss = ul_loss_name.split("_")[-1] + if dist.is_initialized(): + if dont_have_to_train: + self.unlabeled_loss_weights[target_loss] = 0 + elif self.unlabeled_loss_weights[target_loss] == 0: + continue + self._update_unlabeled_loss(losses, ul_loss, ul_loss_name, self.unlabeled_loss_weights[target_loss]) + return losses + + def _visual_online(self, img, boxes_list, labels_list, img_id=0, boxes_ignore_list=None, proposal_list=None): + if not img.size(0): + return + img_norm_cfg = dict(mean=np.array([0, 0, 0]), std=np.array([255, 255, 255])) + img_np = img[img_id].permute(1, 2, 0).cpu().numpy() + img_np = mmcv.imdenormalize(img_np, **img_norm_cfg) + img_np = cv2.cvtColor(img_np, cv2.COLOR_BGR2RGB) + boxes, labels = boxes_list[img_id], labels_list[img_id] + # proposal + if proposal_list: + proposal = proposal_list[img_id] + for idx, box in enumerate(proposal[:, :4]): + x1, y1, x2, y2 = [int(a.cpu().item()) for a in box] + img_np = cv2.rectangle(img_np, (x1, y1), (x2, y2), (214, 39, 40), 2) + cv2.putText(img_np, f"{idx}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (214, 39, 40), 2) + # ignore + if boxes_ignore_list: + boxes_ignore = boxes_ignore_list[img_id] + for idx, box in enumerate(boxes_ignore): + x1, y1, x2, y2 = [int(a.cpu().item()) for a in box] + img_np = cv2.rectangle(img_np, (x1, y1), (x2, y2), (44, 160, 44), 2) + cv2.putText(img_np, f"{idx}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (44, 160, 44), 2) + # pseudo gt + for idx, (box, label) in enumerate(zip(boxes, labels)): + x1, y1, x2, y2 = [int(a.cpu().item()) for a in box] + img_np = cv2.rectangle(img_np, (x1, y1), (x2, y2), (157, 80, 136), 2) + cv2.putText( + img_np, f"{idx}, {self.CLASSES[label]}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (157, 80, 136), 2 + ) + + img_np = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR) + cv2.imwrite(f"test_debug_images/image_{self.cur_iter}.png", img_np) + + def generate_pseudo_labels(self, teacher_outputs, img_meta, **kwargs): + """Generate pseudo label for UnbiasedTeacher.""" + device = kwargs.pop("device") + all_pseudo_bboxes = [] + all_pseudo_labels = [] + all_pseudo_masks = [] + num_all_bboxes = 0 + num_all_pseudo = 0 + for i, teacher_bboxes_labels in enumerate(teacher_outputs): + image_shape = img_meta[i]["img_shape"][:-1] + pseudo_bboxes = [] + pseudo_labels = [] + pseudo_masks = [] + if self.model_t.with_mask: + teacher_bboxes_labels = zip(*teacher_bboxes_labels) + for label, teacher_bboxes_masks in enumerate(teacher_bboxes_labels): + if self.model_t.with_mask: + teacher_bboxes = teacher_bboxes_masks[0] + teacher_masks = teacher_bboxes_masks[1] + else: + teacher_bboxes = teacher_bboxes_masks + confidences = teacher_bboxes[:, -1] + pseudo_indices = confidences > self.pseudo_conf_thresh + pseudo_bboxes.append(teacher_bboxes[pseudo_indices, :4]) # model output: [x y w h conf] + pseudo_labels.append(np.full([sum(pseudo_indices)], label)) + if self.model_t.with_mask: + if np.any(pseudo_indices): + teacher_masks = [np.expand_dims(mask, 0) for mask in teacher_masks] + pseudo_masks.append(np.concatenate(teacher_masks)[pseudo_indices]) + else: + pseudo_masks.append(np.array([]).reshape(0, *image_shape)) + + num_all_bboxes += teacher_bboxes.shape[0] + if len(pseudo_bboxes): + num_all_pseudo += pseudo_bboxes[-1].shape[0] + + if len(pseudo_bboxes) > 0: + all_pseudo_bboxes.append(torch.from_numpy(np.concatenate(pseudo_bboxes)).to(device)) + all_pseudo_labels.append(torch.from_numpy(np.concatenate(pseudo_labels)).to(device)) + if self.model_t.with_mask: + all_pseudo_masks.append(BitmapMasks(np.concatenate(pseudo_masks), *image_shape)) + + pseudo_ratio = float(num_all_pseudo) / num_all_bboxes if num_all_bboxes > 0 else 0.0 + return all_pseudo_bboxes, all_pseudo_labels, all_pseudo_masks, pseudo_ratio + + @staticmethod + def _update_unlabeled_loss(sum_loss, loss, loss_name, weight): + if isinstance(loss, list): + sum_loss[loss_name + "_ul"] = [cur_loss * weight for cur_loss in loss] + else: + sum_loss[loss_name + "_ul"] = loss * weight + + @staticmethod + def state_dict_hook(module, state_dict, prefix, *args, **kwargs): # pylint: disable=unused-argument + """Redirect student model as output state_dict (teacher as auxilliary).""" + logger.info("----------------- MeanTeacherSegmentor.state_dict_hook() called") + for key in list(state_dict.keys()): + value = state_dict.pop(key) + if not prefix or key.startswith(prefix): + key = key.replace(prefix, "", 1) + if key.startswith("model_t."): + key = key.replace("model_t.", "", 1) + elif key.startswith("model_s."): + continue + key = prefix + key + state_dict[key] = value + return state_dict + + @staticmethod + def load_state_dict_pre_hook(module, state_dict, *args, **kwargs): # pylint: disable=unused-argument + """Redirect input state_dict to teacher model.""" + logger.info("----------------- MeanTeacherSegmentor.load_state_dict_pre_hook() called") + for key in list(state_dict.keys()): + value = state_dict.pop(key) + state_dict["model_s." + key] = value + state_dict["model_t." + key] = value diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/sam_detector_mixin.py b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/sam_detector_mixin.py new file mode 100644 index 00000000000..627175c0c07 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/sam_detector_mixin.py @@ -0,0 +1,15 @@ +"""Detector Class for SAM optimizer.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +class SAMDetectorMixin: + """SAM-enabled BaseDetector mix-in.""" + + def train_step(self, data, optimizer, **kwargs): + """Saving current batch data to compute SAM gradient.""" + # Rest of SAM logics are implented in SAMOptimizerHook + self.current_batch = data + + return super().train_step(data, optimizer, **kwargs) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/__init__.py new file mode 100644 index 00000000000..e705d18bdc8 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/__init__.py @@ -0,0 +1,34 @@ +"""Initial file for mmdetection heads.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .cross_dataset_detector_head import CrossDatasetDetectorHead +from .custom_anchor_generator import SSDAnchorGeneratorClustered +from .custom_atss_head import CustomATSSHead, CustomATSSHeadTrackingLossDynamics +from .custom_dino_head import CustomDINOHead +from .custom_fcn_mask_head import CustomFCNMaskHead +from .custom_retina_head import CustomRetinaHead +from .custom_roi_head import CustomRoIHead +from .custom_rpn_head import CustomRPNHead +from .custom_ssd_head import CustomSSDHead +from .custom_vfnet_head import CustomVFNetHead +from .custom_yolox_head import CustomYOLOXHead +from .detr_head import DETRHeadExtension + +__all__ = [ + "CrossDatasetDetectorHead", + "SSDAnchorGeneratorClustered", + "CustomATSSHead", + "CustomDINOHead", + "CustomFCNMaskHead", + "CustomRetinaHead", + "CustomSSDHead", + "CustomRoIHead", + "CustomVFNetHead", + "CustomYOLOXHead", + "DETRHeadExtension", + "CustomRPNHead", + # Loss dynamics tracking + "CustomATSSHeadTrackingLossDynamics", +] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/cross_dataset_detector_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/cross_dataset_detector_head.py new file mode 100644 index 00000000000..88583fcd718 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/cross_dataset_detector_head.py @@ -0,0 +1,349 @@ +"""Cross Dataset Detector head for Ignore labels.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import functools +import inspect +from collections import defaultdict +from typing import Dict, Tuple + +import torch +from mmdet.core import images_to_levels, multi_apply +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads.base_dense_head import BaseDenseHead +from mmdet.models.losses.utils import weight_reduce_loss + +from otx.algorithms.detection.adapters.mmdet.models.loss_dyns import ( + LossAccumulator, + TrackingLossType, +) + +# TODO: Need to fix pylint issues +# pylint: disable=too-many-locals, too-many-arguments, abstract-method + + +@HEADS.register_module() +class CrossDatasetDetectorHead(BaseDenseHead): + """Head class for Ignore labels.""" + + def get_atss_targets( + self, + anchor_list, + valid_flag_list, + gt_bboxes_list, + img_metas, + gt_bboxes_ignore_list=None, + gt_labels_list=None, + label_channels=1, + unmap_outputs=True, + ): + """Get targets for Detection head. + + This method is almost the same as `AnchorHead.get_targets()`. Besides + returning the targets as the parent method does, it also returns the + anchors as the first element of the returned tuple. + However, if the detector's head loss uses CrossSigmoidFocalLoss, + the labels_weights_list consists of (binarized label schema * weights) of batch images + """ + num_imgs = len(img_metas) + assert len(anchor_list) == len(valid_flag_list) == num_imgs + + # anchor number of multi levels + num_level_anchors = [anchors.size(0) for anchors in anchor_list[0]] + num_level_anchors_list = [num_level_anchors] * num_imgs + + # concat all level anchors and flags to a single tensor + for i in range(num_imgs): + assert len(anchor_list[i]) == len(valid_flag_list[i]) + anchor_list[i] = torch.cat(anchor_list[i]) + valid_flag_list[i] = torch.cat(valid_flag_list[i]) + + # compute targets for each image + if gt_bboxes_ignore_list is None: + gt_bboxes_ignore_list = [None for _ in range(num_imgs)] + if gt_labels_list is None: + gt_labels_list = [None for _ in range(num_imgs)] + ( + all_anchors, + all_labels, + all_label_weights, + all_bbox_targets, + all_bbox_weights, + pos_inds_list, + neg_inds_list, + ) = multi_apply( + self._get_target_single, + anchor_list, + valid_flag_list, + num_level_anchors_list, + gt_bboxes_list, + gt_bboxes_ignore_list, + gt_labels_list, + img_metas, + label_channels=label_channels, + unmap_outputs=unmap_outputs, + ) + # no valid anchors + if not all(labels is not None for labels in all_labels): + return None + # sampled anchors of all images + num_total_pos = sum(max(inds.numel(), 1) for inds in pos_inds_list) + num_total_neg = sum(max(inds.numel(), 1) for inds in neg_inds_list) + # split targets to a list w.r.t. multiple levels + anchors_list = images_to_levels(all_anchors, num_level_anchors) + labels_list = images_to_levels(all_labels, num_level_anchors) + valid_label_mask = self.get_valid_label_mask(img_metas=img_metas, all_labels=all_labels) + valid_label_mask = [i.to(anchor_list[0].device) for i in valid_label_mask] + if len(valid_label_mask) > 0: + valid_label_mask = images_to_levels(valid_label_mask, num_level_anchors) + + label_weights_list = images_to_levels(all_label_weights, num_level_anchors) + bbox_targets_list = images_to_levels(all_bbox_targets, num_level_anchors) + bbox_weights_list = images_to_levels(all_bbox_weights, num_level_anchors) + return ( + anchors_list, + labels_list, + label_weights_list, + bbox_targets_list, + bbox_weights_list, + valid_label_mask, + num_total_pos, + num_total_neg, + ) + + def get_fcos_targets(self, points, gt_bboxes_list, gt_labels_list, img_metas): + """Compute regression, classification and centerss targets for points in multiple images. + + Args: + points (list[Tensor]): Points of each fpn level, each has shape + (num_points, 2). + gt_bboxes_list (list[Tensor]): Ground truth bboxes of each image, + each has shape (num_gt, 4). + gt_labels_list (list[Tensor]): Ground truth labels of each box, + each has shape (num_gt,). + img_metas (list[dict]): Meta information for the image. + + Returns: + tuple: + concat_lvl_labels (list[Tensor]): Labels of each level. \ + concat_lvl_bbox_targets (list[Tensor]): BBox targets of each \ + level. + """ + assert len(points) == len(self.regress_ranges) + num_levels = len(points) + # expand regress ranges to align with points + expanded_regress_ranges = [ + points[i].new_tensor(self.regress_ranges[i])[None].expand_as(points[i]) for i in range(num_levels) + ] + # concat all levels points and regress ranges + concat_regress_ranges = torch.cat(expanded_regress_ranges, dim=0) + concat_points = torch.cat(points, dim=0) + + # the number of points per img, per lvl + num_points = [center.size(0) for center in points] + + # get labels and bbox_targets of each image + labels_list, bbox_targets_list = multi_apply( + self._get_target_single, + gt_bboxes_list, + gt_labels_list, + points=concat_points, + regress_ranges=concat_regress_ranges, + num_points_per_lvl=num_points, + ) + + # split to per img, per level + valid_label_mask = self.get_valid_label_mask(img_metas=img_metas, all_labels=labels_list) + valid_label_mask = [i.to(points[0].device) for i in valid_label_mask] + labels_list = [labels.split(num_points, 0) for labels in labels_list] + bbox_targets_list = [bbox_targets.split(num_points, 0) for bbox_targets in bbox_targets_list] + + # concat per level image + concat_lvl_labels = [] + concat_lvl_bbox_targets = [] + for i in range(num_levels): + concat_lvl_labels.append(torch.cat([labels[i] for labels in labels_list])) + bbox_targets = torch.cat([bbox_targets[i] for bbox_targets in bbox_targets_list]) + if self.norm_on_bbox: + bbox_targets = bbox_targets / self.strides[i] + concat_lvl_bbox_targets.append(bbox_targets) + + label_weights, bbox_weights = None, None + return concat_lvl_labels, label_weights, concat_lvl_bbox_targets, bbox_weights, valid_label_mask + + def vfnet_to_atss_targets(self, cls_scores, mlvl_points, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore=None): + """A wrapper for computing ATSS targets for points in multiple images. + + Args: + cls_scores (list[Tensor]): Box iou-aware scores for each scale + level with shape (N, num_points * num_classes, H, W). + mlvl_points (list[Tensor]): Points of each fpn level, each has + shape (num_points, 2). + gt_bboxes (list[Tensor]): Ground truth bboxes of each image, + each has shape (num_gt, 4). + gt_labels (list[Tensor]): Ground truth labels of each box, + each has shape (num_gt,). + img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + gt_bboxes_ignore (None | Tensor): Ground truth bboxes to be + ignored, shape (num_ignored_gts, 4). Default: None. + + Returns: + tuple: + labels_list (list[Tensor]): Labels of each level. + label_weights (Tensor): Label weights of all levels. + bbox_targets_list (list[Tensor]): Regression targets of each + level, (l, t, r, b). + bbox_weights (Tensor): Bbox weights of all levels. + """ + featmap_sizes = [featmap.size()[-2:] for featmap in cls_scores] + assert len(featmap_sizes) == self.anchor_generator.num_levels + + device = cls_scores[0].device + anchor_list, valid_flag_list = self.get_anchors(featmap_sizes, img_metas, device=device) + label_channels = self.cls_out_channels if self.use_sigmoid_cls else 1 + + cls_reg_targets = self.get_atss_targets( + anchor_list, + valid_flag_list, + gt_bboxes, + img_metas, + gt_bboxes_ignore_list=gt_bboxes_ignore, + gt_labels_list=gt_labels, + label_channels=label_channels, + unmap_outputs=True, + ) + if cls_reg_targets is None: + return None + + ( + anchor_list, + labels_list, + label_weights_list, + bbox_targets_list, + bbox_weights_list, + valid_label_mask, + num_total_pos, # pylint: disable=unused-variable + num_total_neg, # pylint: disable=unused-variable + ) = cls_reg_targets + + bbox_targets_list = [bbox_targets.reshape(-1, 4) for bbox_targets in bbox_targets_list] + + num_imgs = len(img_metas) + # transform bbox_targets (x1, y1, x2, y2) into (l, t, r, b) format + bbox_targets_list = self.transform_bbox_targets(bbox_targets_list, mlvl_points, num_imgs) + + labels_list = [labels.reshape(-1) for labels in labels_list] + label_weights_list = [label_weights.reshape(-1) for label_weights in label_weights_list] + bbox_weights_list = [bbox_weights.reshape(-1) for bbox_weights in bbox_weights_list] + label_weights = torch.cat(label_weights_list) + bbox_weights = torch.cat(bbox_weights_list) + return labels_list, label_weights, bbox_targets_list, bbox_weights, valid_label_mask + + def get_valid_label_mask(self, img_metas, all_labels, use_bg=False): + """Getter function valid_label_mask.""" + num_classes = self.num_classes + 1 if use_bg else self.num_classes + valid_label_mask = [] + for i, meta in enumerate(img_metas): + mask = torch.Tensor([1 for _ in range(num_classes)]) + if "ignored_labels" in meta and len(meta["ignored_labels"]) > 0: + mask[meta["ignored_labels"]] = 0 + if use_bg: + mask[self.num_classes] = 0 + mask = mask.repeat(len(all_labels[i]), 1) + valid_label_mask.append(mask) + return valid_label_mask + + +@HEADS.register_module() +class TrackingLossDynamicsMixIn: + """Mix-In class for tracking loss dynamics.""" + + tracking_loss_types: Tuple[TrackingLossType, ...] = () + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._loss_dyns: Dict[TrackingLossType, Dict[Tuple[int, int], LossAccumulator]] = {} + + def _get_pos_inds(self, labels): + pos_inds = super()._get_pos_inds(labels) + + if len(pos_inds) > 0: + pos_assigned_gt_inds = self.all_pos_assigned_gt_inds[self.cur_loss_idx].reshape(-1) + + gt_inds = pos_assigned_gt_inds[pos_inds].cpu() + + self.batch_inds = gt_inds // self.max_gt_bboxes_len + self.bbox_inds = gt_inds % self.max_gt_bboxes_len + + self.pos_inds = pos_inds + return pos_inds + + def _store_loss_dyns(self, losses: torch.Tensor, key: TrackingLossType) -> None: + if len(self.pos_inds) == 0: + return + + loss_dyns = self.loss_dyns[key] + for batch_idx, bbox_idx, loss_item in zip(self.batch_inds, self.bbox_inds, losses.detach().cpu()): + loss_dyns[(batch_idx.item(), bbox_idx.item())].add(loss_item.item()) + + def _postprocess_loss(self, losses: torch.Tensor, reduction: str, avg_factor: float) -> torch.Tensor: + return weight_reduce_loss(losses, reduction=reduction, avg_factor=avg_factor) + + @property + def loss_dyns(self) -> Dict[TrackingLossType, Dict[Tuple[int, int], LossAccumulator]]: + """Loss dynamics dict.""" + return self._loss_dyns + + @staticmethod + def _wrap_loss(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + self.cur_loss_idx = 0 + self._loss_dyns = {loss_type: defaultdict(LossAccumulator) for loss_type in self.tracking_loss_types} + losses = func(self, *args, **kwargs) + return losses + + return wrapper + + @staticmethod + def _wrap_loss_single(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + losses = func(self, *args, **kwargs) + self.cur_loss_idx += 1 + return losses + + return wrapper + + @staticmethod + def _wrap_get_targets(concatenate_last: bool = False, flatten: bool = False): + def wrapper_with_option(func): + signature = inspect.signature(func) + + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + anchor_list = gt_bboxes_list = None + for idx, key in enumerate(signature.parameters): + if key == "anchor_list": + anchor_list = kwargs.get(key) if key in kwargs else args[idx - 1] + elif key == "gt_bboxes_list": + gt_bboxes_list = kwargs.get(key) if key in kwargs else args[idx - 1] + + assert anchor_list is not None and gt_bboxes_list is not None + num_level_anchors = [anchors.size(0) for anchors in anchor_list[0]] + self.max_gt_bboxes_len = max([len(gt_bboxes) for gt_bboxes in gt_bboxes_list]) + self.cur_batch_idx = 0 + self.pos_assigned_gt_inds_list = [] + targets = func(self, *args, **kwargs) + self.all_pos_assigned_gt_inds = images_to_levels(self.pos_assigned_gt_inds_list, num_level_anchors) + if flatten: + self.all_pos_assigned_gt_inds = [gt_ind.reshape(-1) for gt_ind in self.all_pos_assigned_gt_inds] + if concatenate_last: + self.all_pos_assigned_gt_inds = torch.cat(self.all_pos_assigned_gt_inds, -1) + return targets + + return wrapper + + return wrapper_with_option diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_anchor_generator.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_anchor_generator.py new file mode 100644 index 00000000000..7c0cdc14337 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_anchor_generator.py @@ -0,0 +1,62 @@ +"""Custom Anchor Generator for SSD.""" +# Copyright (C) 2018-2021 OpenMMLab +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2020-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmdet.core.anchor.anchor_generator import AnchorGenerator +from mmdet.core.anchor.builder import PRIOR_GENERATORS +from torch.nn.modules.utils import _pair + +# TODO: Need to fix pylint issues +# pylint: disable=super-init-not-called, unused-argument + + +@PRIOR_GENERATORS.register_module() +class SSDAnchorGeneratorClustered(AnchorGenerator): + """Custom Anchor Generator for SSD.""" + + def __init__(self, strides, widths, heights, reclustering_anchors=False): + self.strides = [_pair(stride) for stride in strides] + self.widths = widths + self.heights = heights + self.centers = [(stride / 2.0, stride / 2.0) for stride in strides] + + self.center_offset = 0 + self.base_anchors = self.gen_base_anchors() + + def gen_base_anchors(self): + """Generate base anchor for SSD.""" + multi_level_base_anchors = [] + for widths, heights, centers in zip(self.widths, self.heights, self.centers): + base_anchors = self.gen_single_level_base_anchors( + ws=torch.Tensor(widths), hs=torch.Tensor(heights), center=torch.Tensor(centers) + ) + multi_level_base_anchors.append(base_anchors) + return multi_level_base_anchors + + def gen_single_level_base_anchors(self, ws, hs, center): + """Generate single_level_base_anchors for SSD.""" + x_center, y_center = center + + # use float anchor and the anchor's center is aligned with the + # pixel center + base_anchors = [x_center - 0.5 * ws, y_center - 0.5 * hs, x_center + 0.5 * ws, y_center + 0.5 * hs] + base_anchors = torch.stack(base_anchors, dim=-1) + + return base_anchors + + def __repr__(self): + """Str: a string that describes the module.""" + indent_str = " " + repr_str = self.__class__.__name__ + "(\n" + repr_str += f"{indent_str}strides={self.strides},\n" + repr_str += f"{indent_str}widths={self.widths},\n" + repr_str += f"{indent_str}heights={self.heights},\n" + repr_str += f"{indent_str}num_levels={self.num_levels}\n" + repr_str += f"{indent_str}centers={self.centers},\n" + repr_str += f"{indent_str}center_offset={self.center_offset})" + return repr_str diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_atss_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_atss_head.py new file mode 100644 index 00000000000..41b7fd3aa8b --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_atss_head.py @@ -0,0 +1,514 @@ +"""Custom ATSS head for OTX template.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcv.runner import force_fp32 +from mmdet.core import ( + anchor_inside_flags, + bbox_overlaps, + multi_apply, + reduce_mean, + unmap, +) +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads.atss_head import ATSSHead + +from otx.algorithms.detection.adapters.mmdet.models.heads.cross_dataset_detector_head import ( + CrossDatasetDetectorHead, + TrackingLossDynamicsMixIn, +) +from otx.algorithms.detection.adapters.mmdet.models.loss_dyns import TrackingLossType +from otx.algorithms.detection.adapters.mmdet.models.losses.cross_focal_loss import ( + CrossSigmoidFocalLoss, +) + +EPS = 1e-12 + +# pylint: disable=too-many-arguments, too-many-locals, + + +@HEADS.register_module() +class CustomATSSHead(CrossDatasetDetectorHead, ATSSHead): + """CustomATSSHead for OTX template.""" + + def __init__(self, *args, bg_loss_weight=-1.0, use_qfl=False, qfl_cfg=None, **kwargs): + if use_qfl: + kwargs["loss_cls"] = ( + qfl_cfg + if qfl_cfg + else dict( + type="QualityFocalLoss", + use_sigmoid=True, + beta=2.0, + loss_weight=1.0, + ) + ) + super().__init__(*args, **kwargs) + self.bg_loss_weight = bg_loss_weight + self.use_qfl = use_qfl + + def forward_single(self, x, scale): + """Forward feature of a single scale level. + + Args: + x (Tensor): Features of a single scale level. + scale (:obj: `mmcv.cnn.Scale`): Learnable scale module to resize + the bbox prediction. + + Returns: + tuple: + cls_score (Tensor): Cls scores for a single scale level + the channels number is num_anchors * num_classes. + bbox_pred (Tensor): Box energies / deltas for a single scale + level, the channels number is num_anchors * 4. + centerness (Tensor): Centerness for a single scale level, the + channel number is (N, num_anchors * 1, H, W). + """ + cls_score, bbox_pred, centerness = super().forward_single(x, scale) + if cls_score.device.type == "hpu": + # put further post-processing on cpu + cls_score = cls_score.cpu() + bbox_pred = bbox_pred.cpu() + centerness = centerness.cpu() + + return cls_score, bbox_pred, centerness + + @force_fp32(apply_to=("cls_scores", "bbox_preds", "centernesses")) + def loss(self, cls_scores, bbox_preds, centernesses, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore=None): + """Compute losses of the head. + + Args: + cls_scores (list[Tensor]): Box scores for each scale level + Has shape (N, num_anchors * num_classes, H, W) + bbox_preds (list[Tensor]): Box energies / deltas for each scale + level with shape (N, num_anchors * 4, H, W) + centernesses (list[Tensor]): Centerness for each scale + level with shape (N, num_anchors * 1, H, W) + gt_bboxes (list[Tensor]): Ground truth bboxes for each image with + shape (num_gts, 4) in [tl_x, tl_y, br_x, br_y] format. + gt_labels (list[Tensor]): class indices corresponding to each box + img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + gt_bboxes_ignore (list[Tensor] | None): specify which bounding + boxes can be ignored when computing the loss. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + featmap_sizes = [featmap.size()[-2:] for featmap in cls_scores] + assert len(featmap_sizes) == self.anchor_generator.num_levels + + device = cls_scores[0].device + anchor_list, valid_flag_list = self.get_anchors(featmap_sizes, img_metas, device=device) + label_channels = self.cls_out_channels if self.use_sigmoid_cls else 1 + + cls_reg_targets = self.get_targets( + anchor_list, + valid_flag_list, + gt_bboxes, + img_metas, + gt_bboxes_ignore_list=gt_bboxes_ignore, + gt_labels_list=gt_labels, + label_channels=label_channels, + ) + if cls_reg_targets is None: + return None + + ( + anchor_list, + labels_list, + label_weights_list, + bbox_targets_list, + _, + valid_label_mask, + num_total_pos, + __, + ) = cls_reg_targets + + num_total_samples = reduce_mean(torch.tensor(num_total_pos, dtype=torch.float, device=device)).item() + num_total_samples = max(num_total_samples, 1.0) + + losses_cls, losses_bbox, loss_centerness, bbox_avg_factor = multi_apply( + self.loss_single, + anchor_list, + cls_scores, + bbox_preds, + centernesses, + labels_list, + label_weights_list, + bbox_targets_list, + valid_label_mask, + num_total_samples=num_total_samples, + ) + + bbox_avg_factor = sum(bbox_avg_factor) + bbox_avg_factor = reduce_mean(bbox_avg_factor).item() + if bbox_avg_factor < EPS: + bbox_avg_factor = 1 + losses_bbox = list(map(lambda x: x / bbox_avg_factor, losses_bbox)) + return dict(loss_cls=losses_cls, loss_bbox=losses_bbox, loss_centerness=loss_centerness) + + def loss_single( + self, + anchors, + cls_score, + bbox_pred, + centerness, + labels, + label_weights, + bbox_targets, + valid_label_mask, + num_total_samples, + ): + """Compute loss of a single scale level. + + Args: + anchors (Tensor): Box reference for each scale level with shape + (N, num_total_anchors, 4). + cls_score (Tensor): Box scores for each scale level + Has shape (N, num_anchors * num_classes, H, W). + bbox_pred (Tensor): Box energies / deltas for each scale + level with shape (N, num_anchors * 4, H, W). + centerness (list[Tensor]): Centerness for each scale + level with shape (N, num_anchors * num_classes, H, W) + labels (Tensor): Labels of each anchors with shape + (N, num_total_anchors). + label_weights (Tensor): Label weights of each anchor with shape + (N, num_total_anchors) + bbox_targets (Tensor): BBox regression targets of each anchor wight + shape (N, num_total_anchors, 4). + valid_label_mask (Tensor): Label mask for consideration of ignored + label with shape (N, num_total_anchors, 1). + num_total_samples (int): Number of positive samples that is + reduced over all GPUs. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + + anchors = anchors.reshape(-1, 4) + cls_score = cls_score.permute(0, 2, 3, 1).reshape(-1, self.cls_out_channels).contiguous() + bbox_pred = bbox_pred.permute(0, 2, 3, 1).reshape(-1, 4) + centerness = centerness.permute(0, 2, 3, 1).reshape(-1) + bbox_targets = bbox_targets.reshape(-1, 4) + labels = labels.reshape(-1) + label_weights = label_weights.reshape(-1) + valid_label_mask = valid_label_mask.reshape(-1, self.cls_out_channels) + + # FG cat_id: [0, num_classes -1], BG cat_id: num_classes + pos_inds = self._get_pos_inds(labels) + + if self.use_qfl: + quality = label_weights.new_zeros(labels.shape) + + if len(pos_inds) > 0: + pos_bbox_targets = bbox_targets[pos_inds] + pos_bbox_pred = bbox_pred[pos_inds] + pos_anchors = anchors[pos_inds] + pos_centerness = centerness[pos_inds] + + centerness_targets = self.centerness_target(pos_anchors, pos_bbox_targets) + if self.reg_decoded_bbox: + pos_bbox_pred = self.bbox_coder.decode(pos_anchors, pos_bbox_pred) + + if self.use_qfl: + quality[pos_inds] = bbox_overlaps(pos_bbox_pred.detach(), pos_bbox_targets, is_aligned=True).clamp( + min=1e-6 + ) + + # regression loss + loss_bbox = self._get_loss_bbox(pos_bbox_targets, pos_bbox_pred, centerness_targets) + + # centerness loss + loss_centerness = self._get_loss_centerness(num_total_samples, pos_centerness, centerness_targets) + + else: + loss_bbox = bbox_pred.sum() * 0 + loss_centerness = centerness.sum() * 0 + centerness_targets = bbox_targets.new_tensor(0.0) + + # Re-weigting BG loss + if self.bg_loss_weight >= 0.0: + neg_indices = labels == self.num_classes + label_weights[neg_indices] = self.bg_loss_weight + + if self.use_qfl: + labels = (labels, quality) # For quality focal loss arg spec + + # classification loss + loss_cls = self._get_loss_cls(cls_score, labels, label_weights, valid_label_mask, num_total_samples) + + return loss_cls, loss_bbox, loss_centerness, centerness_targets.sum() + + def _get_pos_inds(self, labels): + bg_class_ind = self.num_classes + pos_inds = ((labels >= 0) & (labels < bg_class_ind)).nonzero().squeeze(1) + return pos_inds + + def _get_loss_cls(self, cls_score, labels, label_weights, valid_label_mask, num_total_samples): + if isinstance(self.loss_cls, CrossSigmoidFocalLoss): + loss_cls = self.loss_cls( + cls_score, labels, label_weights, avg_factor=num_total_samples, valid_label_mask=valid_label_mask + ) + else: + loss_cls = self.loss_cls(cls_score, labels, label_weights, avg_factor=num_total_samples) + return loss_cls + + def _get_loss_centerness(self, num_total_samples, pos_centerness, centerness_targets): + return self.loss_centerness(pos_centerness, centerness_targets, avg_factor=num_total_samples) + + def _get_loss_bbox(self, pos_bbox_targets, pos_bbox_pred, centerness_targets): + return self.loss_bbox(pos_bbox_pred, pos_bbox_targets, weight=centerness_targets, avg_factor=1.0) + + def get_targets( + self, + anchor_list, + valid_flag_list, + gt_bboxes_list, + img_metas, + gt_bboxes_ignore_list=None, + gt_labels_list=None, + label_channels=1, + unmap_outputs=True, + ): + """Get targets for Detection head. + + This method is almost the same as `AnchorHead.get_targets()`. Besides + returning the targets as the parent method does, it also returns the + anchors as the first element of the returned tuple. + However, if the detector's head loss uses CrossSigmoidFocalLoss, + the labels_weights_list consists of (binarized label schema * weights) of batch images + """ + return self.get_atss_targets( + anchor_list, + valid_flag_list, + gt_bboxes_list, + img_metas, + gt_bboxes_ignore_list, + gt_labels_list, + label_channels, + unmap_outputs, + ) + + +@HEADS.register_module() +class CustomATSSHeadTrackingLossDynamics(TrackingLossDynamicsMixIn, CustomATSSHead): + """CustomATSSHead which supports tracking loss dynamics.""" + + tracking_loss_types = (TrackingLossType.cls, TrackingLossType.bbox, TrackingLossType.centerness) + + def __init__(self, *args, bg_loss_weight=-1, use_qfl=False, qfl_cfg=None, **kwargs): + super().__init__(*args, bg_loss_weight=bg_loss_weight, use_qfl=use_qfl, qfl_cfg=qfl_cfg, **kwargs) + + @TrackingLossDynamicsMixIn._wrap_loss + def loss(self, cls_scores, bbox_preds, centernesses, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore=None): + """Compute losses of the head. + + Args: + cls_scores (list[Tensor]): Box scores for each scale level + Has shape (N, num_anchors * num_classes, H, W) + bbox_preds (list[Tensor]): Box energies / deltas for each scale + level with shape (N, num_anchors * 4, H, W) + centernesses (list[Tensor]): Centerness for each scale + level with shape (N, num_anchors * 1, H, W) + gt_bboxes (list[Tensor]): Ground truth bboxes for each image with + shape (num_gts, 4) in [tl_x, tl_y, br_x, br_y] format. + gt_labels (list[Tensor]): class indices corresponding to each box + img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + gt_bboxes_ignore (list[Tensor] | None): specify which bounding + boxes can be ignored when computing the loss. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + return super().loss(cls_scores, bbox_preds, centernesses, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore) + + @TrackingLossDynamicsMixIn._wrap_loss_single + def loss_single( + self, + anchors, + cls_score, + bbox_pred, + centerness, + labels, + label_weights, + bbox_targets, + valid_label_mask, + num_total_samples, + ): + """Compute loss of a single scale level. + + Args: + anchors (Tensor): Box reference for each scale level with shape + (N, num_total_anchors, 4). + cls_score (Tensor): Box scores for each scale level + Has shape (N, num_anchors * num_classes, H, W). + bbox_pred (Tensor): Box energies / deltas for each scale + level with shape (N, num_anchors * 4, H, W). + centerness (list[Tensor]): Centerness for each scale + level with shape (N, num_anchors * num_classes, H, W) + labels (Tensor): Labels of each anchors with shape + (N, num_total_anchors). + label_weights (Tensor): Label weights of each anchor with shape + (N, num_total_anchors) + bbox_targets (Tensor): BBox regression targets of each anchor wight + shape (N, num_total_anchors, 4). + valid_label_mask (Tensor): Label mask for consideration of ignored + label with shape (N, num_total_anchors, 1). + num_total_samples (int): Number of positive samples that is + reduced over all GPUs. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + return super().loss_single( + anchors, + cls_score, + bbox_pred, + centerness, + labels, + label_weights, + bbox_targets, + valid_label_mask, + num_total_samples, + ) + + def _get_loss_cls(self, cls_score, labels, label_weights, valid_label_mask, num_total_samples): + if isinstance(self.loss_cls, CrossSigmoidFocalLoss): + loss_cls = self.loss_cls( + cls_score, + labels, + label_weights, + avg_factor=num_total_samples, + valid_label_mask=valid_label_mask, + reduction_override="none", + ) + else: + loss_cls = self.loss_cls( + cls_score, labels, label_weights, avg_factor=num_total_samples, reduction_override="none" + ) + + self._store_loss_dyns(loss_cls[self.pos_inds].detach().mean(-1), TrackingLossType.cls) + return self._postprocess_loss(loss_cls, self.loss_cls.reduction, avg_factor=num_total_samples) + + def _get_loss_centerness(self, num_total_samples, pos_centerness, centerness_targets): + loss_centerness = self.loss_centerness( + pos_centerness, centerness_targets, avg_factor=num_total_samples, reduction_override="none" + ) + self._store_loss_dyns(loss_centerness, TrackingLossType.centerness) + return self._postprocess_loss(loss_centerness, self.loss_centerness.reduction, avg_factor=num_total_samples) + + def _get_loss_bbox(self, pos_bbox_targets, pos_bbox_pred, centerness_targets): + loss_bbox = self.loss_bbox( + pos_bbox_pred, pos_bbox_targets, weight=centerness_targets, avg_factor=1.0, reduction_override="none" + ) + self._store_loss_dyns(loss_bbox, TrackingLossType.bbox) + return self._postprocess_loss(loss_bbox, self.loss_centerness.reduction, avg_factor=1.0) + + def _get_target_single( + self, + flat_anchors, + valid_flags, + num_level_anchors, + gt_bboxes, + gt_bboxes_ignore, + gt_labels, + img_meta, + label_channels=1, + unmap_outputs=True, + ): + """Compute regression, classification targets for anchors in a single image.""" + inside_flags = anchor_inside_flags( + flat_anchors, valid_flags, img_meta["img_shape"][:2], self.train_cfg.allowed_border + ) + if not inside_flags.any(): + return (None,) * 7 + # assign gt and sample anchors + anchors = flat_anchors[inside_flags, :] + + num_level_anchors_inside = self.get_num_level_anchors_inside(num_level_anchors, inside_flags) + assign_result = self.assigner.assign(anchors, num_level_anchors_inside, gt_bboxes, gt_bboxes_ignore, gt_labels) + + sampling_result = self.sampler.sample(assign_result, anchors, gt_bboxes) + + num_valid_anchors = anchors.shape[0] + bbox_targets = torch.zeros_like(anchors) + bbox_weights = torch.zeros_like(anchors) + labels = anchors.new_full((num_valid_anchors,), self.num_classes, dtype=torch.long) + label_weights = anchors.new_zeros(num_valid_anchors, dtype=torch.float) + + pos_inds = sampling_result.pos_inds + neg_inds = sampling_result.neg_inds + if len(pos_inds) > 0: + if self.reg_decoded_bbox: + pos_bbox_targets = sampling_result.pos_gt_bboxes + else: + pos_bbox_targets = self.bbox_coder.encode(sampling_result.pos_bboxes, sampling_result.pos_gt_bboxes) + + bbox_targets[pos_inds, :] = pos_bbox_targets + bbox_weights[pos_inds, :] = 1.0 + if gt_labels is None: + # Only rpn gives gt_labels as None + # Foreground is the first class since v2.5.0 + labels[pos_inds] = 0 + else: + labels[pos_inds] = gt_labels[sampling_result.pos_assigned_gt_inds] + if self.train_cfg.pos_weight <= 0: + label_weights[pos_inds] = 1.0 + else: + label_weights[pos_inds] = self.train_cfg.pos_weight + + if len(neg_inds) > 0: + label_weights[neg_inds] = 1.0 + + # map up to original set of anchors + if unmap_outputs: + num_total_anchors = flat_anchors.size(0) + anchors = unmap(anchors, num_total_anchors, inside_flags) + labels = unmap(labels, num_total_anchors, inside_flags, fill=self.num_classes) + label_weights = unmap(label_weights, num_total_anchors, inside_flags) + bbox_targets = unmap(bbox_targets, num_total_anchors, inside_flags) + bbox_weights = unmap(bbox_weights, num_total_anchors, inside_flags) + + ########## What we changed from the original mmdet code ############### + # Store all_pos_assigned_gt_inds to member variable + # to look up training loss dynamics for each gt_bboxes afterwards + pos_assigned_gt_inds = anchors.new_full((num_valid_anchors,), -1, dtype=torch.long) + if len(pos_inds) > 0: + pos_assigned_gt_inds[pos_inds] = ( + self.cur_batch_idx * self.max_gt_bboxes_len + sampling_result.pos_assigned_gt_inds + ) + if unmap_outputs: + pos_assigned_gt_inds = unmap(pos_assigned_gt_inds, num_total_anchors, inside_flags, fill=-1) + self.pos_assigned_gt_inds_list += [pos_assigned_gt_inds] + self.cur_batch_idx += 1 + ######################################################################## + + return (anchors, labels, label_weights, bbox_targets, bbox_weights, pos_inds, neg_inds) + + @TrackingLossDynamicsMixIn._wrap_get_targets(False) + def get_targets( + self, + anchor_list, + valid_flag_list, + gt_bboxes_list, + img_metas, + gt_bboxes_ignore_list=None, + gt_labels_list=None, + label_channels=1, + unmap_outputs=True, + ): + """Get targets for Detection head.""" + return super().get_targets( + anchor_list, + valid_flag_list, + gt_bboxes_list, + img_metas, + gt_bboxes_ignore_list, + gt_labels_list, + label_channels, + unmap_outputs, + ) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_dino_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_dino_head.py new file mode 100644 index 00000000000..7da7d4fa8a8 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_dino_head.py @@ -0,0 +1,709 @@ +"""Custom DINO head for OTX template.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Any, Dict, List, Optional, Tuple + +import torch +import torch.nn.functional as F +from mmcv.utils import Config +from mmdet.core import bbox_cxcywh_to_xyxy, bbox_xyxy_to_cxcywh, multi_apply, reduce_mean +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads import DeformableDETRHead +from mmdet.models.utils.transformer import inverse_sigmoid +from torch import Tensor + +from otx.algorithms.detection.adapters.mmdet.models.heads.detr_head import DETRHeadExtension +from otx.algorithms.detection.adapters.mmdet.models.layers import CdnQueryGenerator + + +@HEADS.register_module() +class CustomDINOHead(DeformableDETRHead, DETRHeadExtension): + """Head of DINO. + + Based on detr_head.py and deformable_detr.py in mmdet2.x, some functions from dino_head.py in mmdet3.x are added. + Forward structure: + - Training: self.forward_train -> self.forward_transformer -> self.forward -> self.loss + - Inference: self.simple_test_bboxes -> self.forward_transformer -> self.forward -> self.get_bboxes + """ + + def __init__(self, *args, dn_cfg: Optional[Config] = None, **kwargs): + super().__init__(*args, **kwargs) + + if dn_cfg is not None: + assert "num_classes" not in dn_cfg and "num_queries" not in dn_cfg and "hidden_dim" not in dn_cfg, ( + "The three keyword args `num_classes`, `embed_dims`, and " + "`num_matching_queries` are set in `detector.__init__()`, " + "users should not set them in `dn_cfg` config." + ) + dn_cfg["num_classes"] = self.num_classes + dn_cfg["embed_dims"] = self.embed_dims + dn_cfg["num_matching_queries"] = self.num_query + self.transformer.dn_query_generator = CdnQueryGenerator(**dn_cfg) + self.transformer.two_stage_num_proposals = self.num_query + + def _init_layers(self): + """Initialize classification branch and regression branch of head.""" + super()._init_layers() + self.query_embedding = torch.nn.Embedding(self.num_query, self.embed_dims) + + def forward_train( + self, + x: Tuple[Tensor], + img_metas: List[Dict[str, Any]], + gt_bboxes: List[Tensor], + gt_labels: Optional[List[Tensor]] = None, + gt_bboxes_ignore: Optional[List[Tensor]] = None, + proposal_cfg: Optional[Config] = None, + ): + """Forward function for training mode. + + Origin impelmentation: forward_train function of detr_head.py in mmdet2.x + What's changed: Divided self.forward into self.forward_transformer + self.forward. + This kind of structure is from mmdet3.x. + + Args: + x (list[Tensor]): Features from backbone. + img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + gt_bboxes (List[Tensor]): Ground truth bboxes of the image, + shape (num_gts, 4). + gt_labels (List[Tensor]): Ground truth labels of each box, + shape (num_gts,). + gt_bboxes_ignore (List[Tensor]): Ground truth bboxes to be + ignored, shape (num_ignored_gts, 4). + proposal_cfg (mmcv.Config): Test / postprocessing configuration, + if None, test_cfg would be used. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + assert proposal_cfg is None, '"proposal_cfg" must be None' + outs = self.forward_transformer(x, gt_bboxes, gt_labels, img_metas) + batch_data_samples = [] + for img_meta, gt_bbox, gt_label in zip(img_metas, gt_bboxes, gt_labels): + info = Config({"metainfo": img_meta, "gt_instances": {"bboxes": gt_bbox, "labels": gt_label}}) + batch_data_samples.append(info) + loss_inputs = outs + (batch_data_samples,) + losses = self.loss(*loss_inputs) + return losses + + def forward_transformer( + self, + mlvl_feats: Tuple[Tensor], + gt_bboxes: Optional[List[Tensor]], + gt_labels: Optional[List[Tensor]], + img_metas: List[Dict[str, Any]], + ): + """Transformers's forward function. + + Origin implementation: forward function of deformable_detr_head.py in mmdet2.x + What's changed: Original implementation has post-processing process after getting outputs from + self.transformer. However, this function directly return outputs from self.transformer + + Args: + mlvl_feats (tuple[Tensor]): Features from the upstream + network, each is a 4D-tensor with shape + (N, C, H, W). + gt_bboxes (List[Tensor | None]): List of ground truth bboxes. + When model is evaluated, it will be list of None. + gt_labels (List[Tensor | None]): List of ground truth labels. + When model is evaluated, it will be list of None. + img_metas (list[dict]): List of image information. + + Returns: + all_cls_scores (Tensor): Outputs from the classification head, \ + shape [nb_dec, bs, num_query, cls_out_channels]. Note \ + cls_out_channels should includes background. + all_bbox_preds (Tensor): Sigmoid outputs from the regression \ + head with normalized coordinate format (cx, cy, w, h). \ + Shape [nb_dec, bs, num_query, 4]. + enc_outputs_class (Tensor): The score of each point on encode \ + feature map, has shape (N, h*w, num_class). Only when \ + as_two_stage is True it would be returned, otherwise \ + `None` would be returned. + enc_outputs_coord (Tensor): The proposal generate from the \ + encode feature map, has shape (N, h*w, 4). Only when \ + as_two_stage is True it would be returned, otherwise \ + `None` would be returned. + dn_meta (Dict[str, int]): The dictionary saves information about + group collation, including 'num_denoising_queries' and + 'num_denoising_groups'. It will be used for split outputs of + denoising and matching parts and loss calculation. + """ + + batch_size = mlvl_feats[0].size(0) + input_img_h, input_img_w = img_metas[0]["batch_input_shape"] + img_masks = mlvl_feats[0].new_ones((batch_size, input_img_h, input_img_w)) + for img_id in range(batch_size): + img_h, img_w, _ = img_metas[img_id]["img_shape"] + img_masks[img_id, :img_h, :img_w] = 0 + + mlvl_masks = [] + mlvl_positional_encodings = [] + for feat in mlvl_feats: + mlvl_masks.append(F.interpolate(img_masks[None], size=feat.shape[-2:]).to(torch.bool).squeeze(0)) + mlvl_positional_encodings.append(self.positional_encoding(mlvl_masks[-1])) + + query_embeds = self.query_embedding.weight + batch_info = [] + for img_meta, gt_bbox, gt_label in zip(img_metas, gt_bboxes, gt_labels): + info = { + "img_shape": img_meta["img_shape"][:2], + "bboxes": gt_bbox, + "labels": gt_label, + } + batch_info.append(info) + return self.transformer( + batch_info, + mlvl_feats, + mlvl_masks, + query_embeds, + mlvl_positional_encodings, + reg_branches=self.reg_branches, + cls_branches=self.cls_branches, + ) + + def loss( + self, + hidden_states: Tensor, + references: List[Tensor], + enc_outputs_class: Tensor, + enc_outputs_coord: Tensor, + dn_meta: Dict[str, int], + batch_data_samples: List[Config], + ) -> dict: + """Perform forward propagation and loss calculation. + + Original implementation: loss function of dino_head.py in mmdet3.x + What's changed: Change the name of function of loss_by_feat to loss_by_feat_two_stage since + there are changes in function input from parent's implementation. + + Args: + hidden_states (Tensor): Hidden states output from each decoder + layer, has shape (num_decoder_layers, bs, num_queries_total, + dim), where `num_queries_total` is the sum of + `num_denoising_queries` and `num_matching_queries` when + `self.training` is `True`, else `num_matching_queries`. + references (list[Tensor]): List of the reference from the decoder. + The first reference is the `init_reference` (initial) and the + other num_decoder_layers(6) references are `inter_references` + (intermediate). The `init_reference` has shape (bs, + num_queries_total, 4) and each `inter_reference` has shape + (bs, num_queries, 4) with the last dimension arranged as + (cx, cy, w, h). + enc_outputs_class (Tensor): The score of each point on encode + feature map, has shape (bs, num_feat_points, cls_out_channels). + enc_outputs_coord (Tensor): The proposal generate from the + encode feature map, has shape (bs, num_feat_points, 4) with the + last dimension arranged as (cx, cy, w, h). + dn_meta (Dict[str, int]): The dictionary saves information about + group collation, including 'num_denoising_queries' and + 'num_denoising_groups'. It will be used for split outputs of + denoising and matching parts and loss calculation. + batch_data_samples (List[Config]): This is same with batch_data_samples in mmdet3.x + It contains meta_info(==img_metas) and gt_instances(==(gt_bboxes, gt_labels)) + + Returns: + dict: A dictionary of loss components. + """ + batch_gt_instances = [] + batch_img_metas = [] + for data_sample in batch_data_samples: + batch_img_metas.append(data_sample.metainfo) + batch_gt_instances.append(data_sample.gt_instances) + + outs = self(hidden_states, references) + loss_inputs = outs + (enc_outputs_class, enc_outputs_coord, batch_gt_instances, batch_img_metas, dn_meta) + losses = self.loss_by_feat_two_stage(*loss_inputs) + return losses + + def forward(self, hidden_states: Tensor, references: List[Tensor]): + """Forward function. + + Original implementation: forward function of deformable_detr_head.py in mmdet3.x + What's changed: None + + Args: + hidden_states (Tensor): Hidden states output from each decoder + layer, has shape (num_decoder_layers, bs, num_queries, dim). + references (list[Tensor]): List of the reference from the decoder. + The first reference is the `init_reference` (initial) and the + other num_decoder_layers(6) references are `inter_references` + (intermediate). The `init_reference` has shape (bs, + num_queries, 4) when `as_two_stage` of the detector is `True`, + otherwise (bs, num_queries, 2). Each `inter_reference` has + shape (bs, num_queries, 4) when `with_box_refine` of the + detector is `True`, otherwise (bs, num_queries, 2). The + coordinates are arranged as (cx, cy) when the last dimension is + 2, and (cx, cy, w, h) when it is 4. + + Returns: + tuple[Tensor]: results of head containing the following tensor. + + - all_layers_outputs_classes (Tensor): Outputs from the + classification head, has shape (num_decoder_layers, bs, + num_queries, cls_out_channels). + - all_layers_outputs_coords (Tensor): Sigmoid outputs from the + regression head with normalized coordinate format (cx, cy, w, + h), has shape (num_decoder_layers, bs, num_queries, 4) with the + last dimension arranged as (cx, cy, w, h). + """ + + all_layers_outputs_classes = [] + all_layers_outputs_coords = [] + + for layer_id in range(hidden_states.shape[0]): + reference = inverse_sigmoid(references[layer_id]) + # NOTE The last reference will not be used. + hidden_state = hidden_states[layer_id] + outputs_class = self.cls_branches[layer_id](hidden_state) + tmp_reg_preds = self.reg_branches[layer_id](hidden_state) + if reference.shape[-1] == 4: + # When `layer` is 0 and `as_two_stage` of the detector + # is `True`, or when `layer` is greater than 0 and + # `with_box_refine` of the detector is `True`. + tmp_reg_preds += reference + else: + # When `layer` is 0 and `as_two_stage` of the detector + # is `False`, or when `layer` is greater than 0 and + # `with_box_refine` of the detector is `False`. + assert reference.shape[-1] == 2 + tmp_reg_preds[..., :2] += reference + outputs_coord = tmp_reg_preds.sigmoid() + all_layers_outputs_classes.append(outputs_class) + all_layers_outputs_coords.append(outputs_coord) + + all_layers_outputs_classes = torch.stack(all_layers_outputs_classes) + all_layers_outputs_coords = torch.stack(all_layers_outputs_coords) + + return all_layers_outputs_classes, all_layers_outputs_coords + + def loss_by_feat_two_stage( + self, + all_layers_cls_scores: Tensor, + all_layers_bbox_preds: Tensor, + enc_cls_scores: Tensor, + enc_bbox_preds: Tensor, + batch_gt_instances: List[Config], + batch_img_metas: List[dict], + dn_meta: Dict[str, int], + batch_gt_instances_ignore=None, + ) -> Dict[str, Tensor]: + """Loss function. + + Original implementation: loss_by_feat function of dino_head.py in mmdet3.x + What's changed: Name of function is changed. Parent's loss_by_feat function has different inputs. + + Args: + all_layers_cls_scores (Tensor): Classification scores of all + decoder layers, has shape (num_decoder_layers, bs, + num_queries_total, cls_out_channels), where + `num_queries_total` is the sum of `num_denoising_queries` + and `num_matching_queries`. + all_layers_bbox_preds (Tensor): Regression outputs of all decoder + layers. Each is a 4D-tensor with normalized coordinate format + (cx, cy, w, h) and has shape (num_decoder_layers, bs, + num_queries_total, 4). + enc_cls_scores (Tensor): The score of each point on encode + feature map, has shape (bs, num_feat_points, cls_out_channels). + enc_bbox_preds (Tensor): The proposal generate from the encode + feature map, has shape (bs, num_feat_points, 4) with the last + dimension arranged as (cx, cy, w, h). + batch_gt_instances (List[Config]): Batch of gt_instance. + It usually includes ``bboxes`` and ``labels`` attributes. + batch_img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + dn_meta (Dict[str, int]): The dictionary saves information about + group collation, including 'num_denoising_queries' and + 'num_denoising_groups'. It will be used for split outputs of + denoising and matching parts and loss calculation. + batch_gt_instances_ignore (list[:obj:`InstanceData`], optional): + Batch of gt_instances_ignore. It includes ``bboxes`` attribute + data that is ignored during training and testing. + Defaults to None. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + # extract denoising and matching part of outputs + ( + all_layers_matching_cls_scores, + all_layers_matching_bbox_preds, + all_layers_denoising_cls_scores, + all_layers_denoising_bbox_preds, + ) = self.split_outputs(all_layers_cls_scores, all_layers_bbox_preds, dn_meta) + + loss_dict = super(DeformableDETRHead, self).loss_by_feat( + all_layers_matching_cls_scores, + all_layers_matching_bbox_preds, + batch_gt_instances, + batch_img_metas, + batch_gt_instances_ignore, + ) + + # loss of proposal generated from encode feature map. + if enc_cls_scores is not None: + # NOTE The enc_loss calculation of the DINO is + # different from that of Deformable DETR. + enc_loss_cls, enc_losses_bbox, enc_losses_iou = self.loss_by_feat_single( + enc_cls_scores, enc_bbox_preds, batch_gt_instances=batch_gt_instances, batch_img_metas=batch_img_metas + ) + loss_dict["enc_loss_cls"] = enc_loss_cls + loss_dict["enc_loss_bbox"] = enc_losses_bbox + loss_dict["enc_loss_iou"] = enc_losses_iou + + if all_layers_denoising_cls_scores is not None: + # calculate denoising loss from all decoder layers + dn_losses_cls, dn_losses_bbox, dn_losses_iou = self.loss_dn( + all_layers_denoising_cls_scores, + all_layers_denoising_bbox_preds, + batch_gt_instances=batch_gt_instances, + batch_img_metas=batch_img_metas, + dn_meta=dn_meta, + ) + # collate denoising loss + loss_dict["dn_loss_cls"] = dn_losses_cls[-1] + loss_dict["dn_loss_bbox"] = dn_losses_bbox[-1] + loss_dict["dn_loss_iou"] = dn_losses_iou[-1] + for num_dec_layer, (loss_cls_i, loss_bbox_i, loss_iou_i) in enumerate( + zip(dn_losses_cls[:-1], dn_losses_bbox[:-1], dn_losses_iou[:-1]) + ): + loss_dict[f"d{num_dec_layer}.dn_loss_cls"] = loss_cls_i + loss_dict[f"d{num_dec_layer}.dn_loss_bbox"] = loss_bbox_i + loss_dict[f"d{num_dec_layer}.dn_loss_iou"] = loss_iou_i + return loss_dict + + def loss_dn( + self, + all_layers_denoising_cls_scores: Tensor, + all_layers_denoising_bbox_preds: Tensor, + batch_gt_instances: List[Config], + batch_img_metas: List[dict], + dn_meta: Dict[str, int], + ) -> Tuple[List[Tensor], ...]: + """Calculate denoising loss. + + Original implementation: loss_dn function of dino_head.py in mmdet3.x + What's changed: None + + Args: + all_layers_denoising_cls_scores (Tensor): Classification scores of + all decoder layers in denoising part, has shape ( + num_decoder_layers, bs, num_denoising_queries, + cls_out_channels). + all_layers_denoising_bbox_preds (Tensor): Regression outputs of all + decoder layers in denoising part. Each is a 4D-tensor with + normalized coordinate format (cx, cy, w, h) and has shape + (num_decoder_layers, bs, num_denoising_queries, 4). + batch_gt_instances (List[Config]): Batch of gt_instance. + It usually includes ``bboxes`` and ``labels`` attributes. + batch_img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + dn_meta (Dict[str, int]): The dictionary saves information about + group collation, including 'num_denoising_queries' and + 'num_denoising_groups'. It will be used for split outputs of + denoising and matching parts and loss calculation. + + Returns: + Tuple[List[Tensor]]: The loss_dn_cls, loss_dn_bbox, and loss_dn_iou + of each decoder layers. + """ + return multi_apply( + self._loss_dn_single, + all_layers_denoising_cls_scores, + all_layers_denoising_bbox_preds, + batch_gt_instances=batch_gt_instances, + batch_img_metas=batch_img_metas, + dn_meta=dn_meta, + ) + + def _loss_dn_single( + self, + dn_cls_scores: Tensor, + dn_bbox_preds: Tensor, + batch_gt_instances: List[Config], + batch_img_metas: List[dict], + dn_meta: Dict[str, int], + ) -> Tuple[Tensor, ...]: + """Denoising loss for outputs from a single decoder layer. + + Original implementation: _loss_dn_single function of dino_head.py in mmdet3.x + What's changed: None + + Args: + dn_cls_scores (Tensor): Classification scores of a single decoder + layer in denoising part, has shape (bs, num_denoising_queries, + cls_out_channels). + dn_bbox_preds (Tensor): Regression outputs of a single decoder + layer in denoising part. Each is a 4D-tensor with normalized + coordinate format (cx, cy, w, h) and has shape + (bs, num_denoising_queries, 4). + batch_gt_instances (List[Config]): Batch of gt_instance. + It usually includes ``bboxes`` and ``labels`` attributes. + batch_img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + dn_meta (Dict[str, int]): The dictionary saves information about + group collation, including 'num_denoising_queries' and + 'num_denoising_groups'. It will be used for split outputs of + denoising and matching parts and loss calculation. + + Returns: + Tuple[Tensor]: A tuple including `loss_cls`, `loss_box` and + `loss_iou`. + """ + cls_reg_targets = self.get_dn_targets(batch_gt_instances, batch_img_metas, dn_meta) + ( + labels_list, + label_weights_list, + bbox_targets_list, + bbox_weights_list, + num_total_pos, + num_total_neg, + ) = cls_reg_targets + labels = torch.cat(labels_list, 0) + label_weights = torch.cat(label_weights_list, 0) + bbox_targets = torch.cat(bbox_targets_list, 0) + bbox_weights = torch.cat(bbox_weights_list, 0) + + # classification loss + cls_scores = dn_cls_scores.reshape(-1, self.cls_out_channels) + # construct weighted avg_factor to match with the official DETR repo + cls_avg_factor = num_total_pos * 1.0 + num_total_neg * self.bg_cls_weight + if self.sync_cls_avg_factor: + cls_avg_factor = reduce_mean(cls_scores.new_tensor([cls_avg_factor])) + cls_avg_factor = max(cls_avg_factor, 1) + + if len(cls_scores) > 0: + loss_cls = self.loss_cls(cls_scores, labels, label_weights, avg_factor=cls_avg_factor) + else: + loss_cls = torch.zeros(1, dtype=cls_scores.dtype, device=cls_scores.device) + + # Compute the average number of gt boxes across all gpus, for + # normalization purposes + num_total_pos = loss_cls.new_tensor([num_total_pos]) + num_total_pos = torch.clamp(reduce_mean(num_total_pos), min=1).item() + + # construct factors used for rescale bboxes + factors = [] + for img_meta, bbox_pred in zip(batch_img_metas, dn_bbox_preds): + img_h, img_w = img_meta["img_shape"][:2] + factor = bbox_pred.new_tensor([img_w, img_h, img_w, img_h]).unsqueeze(0).repeat(bbox_pred.size(0), 1) + factors.append(factor) + factors = torch.cat(factors) + + # DETR regress the relative position of boxes (cxcywh) in the image, + # thus the learning target is normalized by the image size. So here + # we need to re-scale them for calculating IoU loss + bbox_preds = dn_bbox_preds.reshape(-1, 4) + bboxes = bbox_cxcywh_to_xyxy(bbox_preds) * factors + bboxes_gt = bbox_cxcywh_to_xyxy(bbox_targets) * factors + + # regression IoU loss, defaultly GIoU loss + loss_iou = self.loss_iou(bboxes, bboxes_gt, bbox_weights, avg_factor=num_total_pos) + + # regression L1 loss + loss_bbox = self.loss_bbox(bbox_preds, bbox_targets, bbox_weights, avg_factor=num_total_pos) + return loss_cls, loss_bbox, loss_iou + + def get_dn_targets( + self, batch_gt_instances: List[Config], batch_img_metas: List[Dict], dn_meta: Dict[str, int] + ) -> tuple: + """Get targets in denoising part for a batch of images. + + Original implementation: get_dn_targets function of dino_head.py in mmdet3.x + What's changed: None + + Args: + batch_gt_instances (List[Config]): Batch of gt_instance. + It usually includes ``bboxes`` and ``labels`` attributes. + batch_img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + dn_meta (Dict[str, int]): The dictionary saves information about + group collation, including 'num_denoising_queries' and + 'num_denoising_groups'. It will be used for split outputs of + denoising and matching parts and loss calculation. + + Returns: + tuple: a tuple containing the following targets. + + - labels_list (list[Tensor]): Labels for all images. + - label_weights_list (list[Tensor]): Label weights for all images. + - bbox_targets_list (list[Tensor]): BBox targets for all images. + - bbox_weights_list (list[Tensor]): BBox weights for all images. + - num_total_pos (int): Number of positive samples in all images. + - num_total_neg (int): Number of negative samples in all images. + """ + ( + labels_list, + label_weights_list, + bbox_targets_list, + bbox_weights_list, + pos_inds_list, + neg_inds_list, + ) = multi_apply(self._get_dn_targets_single, batch_gt_instances, batch_img_metas, dn_meta=dn_meta) + num_total_pos = sum((inds.numel() for inds in pos_inds_list)) + num_total_neg = sum((inds.numel() for inds in neg_inds_list)) + return (labels_list, label_weights_list, bbox_targets_list, bbox_weights_list, num_total_pos, num_total_neg) + + def _get_dn_targets_single(self, gt_instances: Config, img_meta: dict, dn_meta: Dict[str, int]) -> tuple: + """Get targets in denoising part for one image. + + Original implementation: _get_dn_targets_single function of dino_head.py in mmdet3.x + What's changed: None + + Args: + gt_instances (Config): A gt_instance which usually includes ``bboxes`` and ``labels`` attributes. + img_meta (dict): Meta information for one image. + dn_meta (Dict[str, int]): The dictionary saves information about + group collation, including 'num_denoising_queries' and + 'num_denoising_groups'. It will be used for split outputs of + denoising and matching parts and loss calculation. + + Returns: + tuple[Tensor]: a tuple containing the following for one image. + + - labels (Tensor): Labels of each image. + - label_weights (Tensor]): Label weights of each image. + - bbox_targets (Tensor): BBox targets of each image. + - bbox_weights (Tensor): BBox weights of each image. + - pos_inds (Tensor): Sampled positive indices for each image. + - neg_inds (Tensor): Sampled negative indices for each image. + """ + gt_bboxes = gt_instances.bboxes + gt_labels = gt_instances.labels + num_groups = dn_meta["num_denoising_groups"] + num_denoising_queries = dn_meta["num_denoising_queries"] + num_queries_each_group = int(num_denoising_queries / num_groups) + device = gt_bboxes.device + + if len(gt_labels) > 0: + t = torch.arange(len(gt_labels), dtype=torch.long, device=device) + t = t.unsqueeze(0).repeat(num_groups, 1) + pos_assigned_gt_inds = t.flatten() + pos_inds = torch.arange(num_groups, dtype=torch.long, device=device) + pos_inds = pos_inds.unsqueeze(1) * num_queries_each_group + t + pos_inds = pos_inds.flatten() + else: + pos_inds = pos_assigned_gt_inds = gt_bboxes.new_tensor([], dtype=torch.long) + + neg_inds = pos_inds + num_queries_each_group // 2 + + # label targets + labels = gt_bboxes.new_full((num_denoising_queries,), self.num_classes, dtype=torch.long) + labels[pos_inds] = gt_labels[pos_assigned_gt_inds] + label_weights = gt_bboxes.new_ones(num_denoising_queries) + + # bbox targets + bbox_targets = torch.zeros(num_denoising_queries, 4, device=device) + bbox_weights = torch.zeros(num_denoising_queries, 4, device=device) + bbox_weights[pos_inds] = 1.0 + img_h, img_w = img_meta["img_shape"][:2] + + # DETR regress the relative position of boxes (cxcywh) in the image. + # Thus the learning target should be normalized by the image size, also + # the box format should be converted from defaultly x1y1x2y2 to cxcywh. + factor = gt_bboxes.new_tensor([img_w, img_h, img_w, img_h]).unsqueeze(0) + gt_bboxes_normalized = gt_bboxes / factor + gt_bboxes_targets = bbox_xyxy_to_cxcywh(gt_bboxes_normalized) + bbox_targets[pos_inds] = gt_bboxes_targets.repeat([num_groups, 1]) + + return (labels, label_weights, bbox_targets, bbox_weights, pos_inds, neg_inds) + + @staticmethod + def split_outputs( + all_layers_cls_scores: Tensor, all_layers_bbox_preds: Tensor, dn_meta: Dict[str, int] + ) -> Tuple[Tensor, ...]: + """Split outputs of the denoising part and the matching part. + + Original implementation: split_outputs function of dino_head.py in mmdet3.x + What's changed: None + + For the total outputs of `num_queries_total` length, the former + `num_denoising_queries` outputs are from denoising queries, and + the rest `num_matching_queries` ones are from matching queries, + where `num_queries_total` is the sum of `num_denoising_queries` and + `num_matching_queries`. + + Args: + all_layers_cls_scores (Tensor): Classification scores of all + decoder layers, has shape (num_decoder_layers, bs, + num_queries_total, cls_out_channels). + all_layers_bbox_preds (Tensor): Regression outputs of all decoder + layers. Each is a 4D-tensor with normalized coordinate format + (cx, cy, w, h) and has shape (num_decoder_layers, bs, + num_queries_total, 4). + dn_meta (Dict[str, int]): The dictionary saves information about + group collation, including 'num_denoising_queries' and + 'num_denoising_groups'. + + Returns: + Tuple[Tensor]: a tuple containing the following outputs. + + - all_layers_matching_cls_scores (Tensor): Classification scores + of all decoder layers in matching part, has shape + (num_decoder_layers, bs, num_matching_queries, cls_out_channels). + - all_layers_matching_bbox_preds (Tensor): Regression outputs of + all decoder layers in matching part. Each is a 4D-tensor with + normalized coordinate format (cx, cy, w, h) and has shape + (num_decoder_layers, bs, num_matching_queries, 4). + - all_layers_denoising_cls_scores (Tensor): Classification scores + of all decoder layers in denoising part, has shape + (num_decoder_layers, bs, num_denoising_queries, + cls_out_channels). + - all_layers_denoising_bbox_preds (Tensor): Regression outputs of + all decoder layers in denoising part. Each is a 4D-tensor with + normalized coordinate format (cx, cy, w, h) and has shape + (num_decoder_layers, bs, num_denoising_queries, 4). + """ + num_denoising_queries = dn_meta["num_denoising_queries"] + if dn_meta is not None: + all_layers_denoising_cls_scores = all_layers_cls_scores[:, :, :num_denoising_queries, :] + all_layers_denoising_bbox_preds = all_layers_bbox_preds[:, :, :num_denoising_queries, :] + all_layers_matching_cls_scores = all_layers_cls_scores[:, :, num_denoising_queries:, :] + all_layers_matching_bbox_preds = all_layers_bbox_preds[:, :, num_denoising_queries:, :] + else: + all_layers_denoising_cls_scores = None + all_layers_denoising_bbox_preds = None + all_layers_matching_cls_scores = all_layers_cls_scores + all_layers_matching_bbox_preds = all_layers_bbox_preds + return ( + all_layers_matching_cls_scores, + all_layers_matching_bbox_preds, + all_layers_denoising_cls_scores, + all_layers_denoising_bbox_preds, + ) + + def simple_test_bboxes(self, feats: Tuple[Tensor], img_metas: List[Dict[str, Any]], rescale=False): + """Test det bboxes without test-time augmentation. + + Original implementation: simple_test_bboxes funciton of detr_head.py in mmdet2.x + What's changed: self.forward function is divided into self.forward_transformer and self.forward function. + This changes is from mmdet3.x + + Args: + feats (tuple[torch.Tensor]): Multi-level features from the + upstream network, each is a 4D-tensor. + img_metas (list[dict]): List of image information. + rescale (bool, optional): Whether to rescale the results. + Defaults to False. + + Returns: + list[tuple[Tensor, Tensor]]: Each item in result_list is 2-tuple. + The first item is ``bboxes`` with shape (n, 5), + where 5 represent (tl_x, tl_y, br_x, br_y, score). + The shape of the second tensor in the tuple is ``labels`` + with shape (n,) + """ + # forward of this head requires img_metas + gt_bboxes = [None] * len(feats) + gt_labels = [None] * len(feats) + hidden_states, references, enc_output_class, enc_output_coord, _ = self.forward_transformer( + feats, gt_bboxes, gt_labels, img_metas + ) + cls_scores, bbox_preds = self(hidden_states, references) + results_list = self.get_bboxes( + cls_scores, bbox_preds, enc_output_class, enc_output_coord, img_metas, rescale=rescale + ) + return results_list diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_fcn_mask_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_fcn_mask_head.py new file mode 100644 index 00000000000..9b7fa999196 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_fcn_mask_head.py @@ -0,0 +1,113 @@ +"""CustomFCNMaskHead for OTX template.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmdet.models.builder import HEADS +from mmdet.models.roi_heads.mask_heads.fcn_mask_head import FCNMaskHead + +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled + + +@HEADS.register_module() +class CustomFCNMaskHead(FCNMaskHead): + """Custom FCN Mask Head for fast mask evaluation.""" + + def get_seg_masks(self, mask_pred, det_bboxes, det_labels, rcnn_test_cfg, ori_shape, scale_factor, rescale): + """Get segmentation masks from mask_pred and bboxes. + + The original `FCNMaskHead.get_seg_masks` grid sampled 28 x 28 masks to the original image resolution. + As a result, the resized masks occupy a large amount of memory and slow down the inference. + This method directly returns 28 x 28 masks and resize to bounding boxes size in post-processing step. + Doing so can save memory and speed up the inference. + + Args: + mask_pred (Tensor or ndarray): shape (n, #class, h, w). + For single-scale testing, mask_pred is the direct output of + model, whose type is Tensor, while for multi-scale testing, + it will be converted to numpy array outside of this method. + det_bboxes (Tensor): shape (n, 4/5) + det_labels (Tensor): shape (n, ) + rcnn_test_cfg (dict): rcnn testing config + ori_shape (Tuple): original image height and width, shape (2,) + scale_factor(ndarray | Tensor): If ``rescale is True``, box + coordinates are divided by this scale factor to fit + ``ori_shape``. + rescale (bool): If True, the resulting masks will be rescaled to + ``ori_shape``. + + Returns: + list[list]: encoded masks. The c-th item in the outer list + corresponds to the c-th class. Given the c-th outer list, the + i-th item in that inner list is the mask for the i-th box with + class label c. + + """ + if isinstance(mask_pred, torch.Tensor): + mask_pred = mask_pred.sigmoid() + else: + # In AugTest, has been activated before + mask_pred = det_bboxes.new_tensor(mask_pred) + + cls_segms = [[] for _ in range(self.num_classes)] # BG is not included in num_classes + labels = det_labels + + N = len(mask_pred) + # The actual implementation split the input into chunks, + # and paste them chunk by chunk. + + threshold = rcnn_test_cfg.mask_thr_binary + + if not self.class_agnostic: + mask_pred = mask_pred[range(N), labels][:, None] + + for i in range(N): + mask = mask_pred[i] + if threshold >= 0: + mask = (mask >= threshold).to(dtype=torch.bool) + else: + # for visualization and debugging + mask = (mask * 255).to(dtype=torch.uint8) + mask = mask.detach().cpu().numpy() + cls_segms[labels[i]].append(mask[0]) + return cls_segms + + def get_scaled_seg_masks(self, *args, **kwargs): + """Original method "get_seg_mask" from FCNMaskHead. Used in Semi-SL algorithm.""" + return super().get_seg_masks(*args, **kwargs) + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER + + @FUNCTION_REWRITER.register_rewriter( + "otx.algorithms.detection.adapters.mmdet.models." "heads.custom_fcn_mask_head.CustomFCNMaskHead.get_seg_masks" + ) + def custom_fcn_mask_head__get_seg_masks( + ctx, self, mask_pred, det_bboxes, det_labels, rcnn_test_cfg, ori_shape, **kwargs + ): + """Rewrite `get_seg_masks` of `FCNMaskHead` for default backend. + + Rewrite the get_seg_masks for only fcn_mask_head inference. + + Args: + ctx (dict): context dict + self (CustomFCNMaskHead): CustomFCNMaskHead instance + mask_pred (Tensor): shape (n, #class, h, w). + det_bboxes (Tensor): shape (n, 4/5) + det_labels (Tensor): shape (n, ) + rcnn_test_cfg (dict): rcnn testing config + ori_shape (Tuple): original image height and width, shape (2,) + kwargs (dict): other arguments + + Returns: + Tensor: a mask of shape (N, img_h, img_w). + """ + mask_pred = mask_pred.sigmoid() + bboxes = det_bboxes[:, :4] + labels = det_labels + if not self.class_agnostic: + box_inds = torch.arange(mask_pred.shape[0], device=bboxes.device) + mask_pred = mask_pred[box_inds, labels][:, None] + return mask_pred diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_retina_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_retina_head.py new file mode 100644 index 00000000000..f20787723dc --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_retina_head.py @@ -0,0 +1,68 @@ +"""Custom Retina Head for OTX.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads.retina_head import RetinaHead + +# pylint: disable=too-many-arguments + + +@HEADS.register_module() +class CustomRetinaHead(RetinaHead): + """CustomRetinaHead class for OTX.""" + + def __init__(self, *args, bg_loss_weight=-1.0, **kwargs): + super().__init__(*args, **kwargs) + self.bg_loss_weight = bg_loss_weight + + def loss_single( + self, cls_score, bbox_pred, anchors, labels, label_weights, bbox_targets, bbox_weights, num_total_samples + ): + """Compute loss of a single scale level. + + Args: + cls_score (Tensor): Box scores for each scale level + Has shape (N, num_anchors * num_classes, H, W). + bbox_pred (Tensor): Box energies / deltas for each scale + level with shape (N, num_anchors * 4, H, W). + anchors (Tensor): Box reference for each scale level with shape + (N, num_total_anchors, 4). + labels (Tensor): Labels of each anchors with shape + (N, num_total_anchors). + label_weights (Tensor): Label weights of each anchor with shape + (N, num_total_anchors) + bbox_targets (Tensor): BBox regression targets of each anchor wight + shape (N, num_total_anchors, 4). + bbox_weights (Tensor): BBox regression loss weights of each anchor + with shape (N, num_total_anchors, 4). + num_total_samples (int): If sampling, num total samples equal to + the number of total anchors; Otherwise, it is the number of + positive anchors. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + # classification loss + labels = labels.reshape(-1) + label_weights = label_weights.reshape(-1) + cls_score = cls_score.permute(0, 2, 3, 1).reshape(-1, self.cls_out_channels) + # Re-weigting BG loss + if self.bg_loss_weight >= 0.0: + neg_indices = labels == self.num_classes + label_weights[neg_indices] = self.bg_loss_weight + + loss_cls = self.loss_cls(cls_score, labels, label_weights, avg_factor=num_total_samples) + # regression loss + bbox_targets = bbox_targets.reshape(-1, 4) + bbox_weights = bbox_weights.reshape(-1, 4) + bbox_pred = bbox_pred.permute(0, 2, 3, 1).reshape(-1, 4) + if self.reg_decoded_bbox: + # When the regression loss (e.g. `IouLoss`, `GIouLoss`) + # is applied directly on the decoded bounding boxes, it + # decodes the already encoded coordinates to absolute format. + anchors = anchors.reshape(-1, 4) + bbox_pred = self.bbox_coder.decode(anchors, bbox_pred) + loss_bbox = self.loss_bbox(bbox_pred, bbox_targets, bbox_weights, avg_factor=num_total_samples) + return loss_cls, loss_bbox diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_roi_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_roi_head.py new file mode 100644 index 00000000000..d8e546a5f91 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_roi_head.py @@ -0,0 +1,206 @@ +"""Custom ROI head for OTX template.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcv.runner import force_fp32 +from mmdet.core import bbox2roi, multi_apply +from mmdet.models.builder import HEADS, build_head, build_roi_extractor +from mmdet.models.losses import accuracy +from mmdet.models.roi_heads.bbox_heads.convfc_bbox_head import Shared2FCBBoxHead +from mmdet.models.roi_heads.standard_roi_head import StandardRoIHead + +from otx.algorithms.detection.adapters.mmdet.models.heads.cross_dataset_detector_head import ( + CrossDatasetDetectorHead, +) +from otx.algorithms.detection.adapters.mmdet.models.losses.cross_focal_loss import ( + CrossSigmoidFocalLoss, +) + +# TODO: Need to fix pylint issues +# pylint: disable=abstract-method, arguments-renamed, too-many-locals, too-many-arguments + + +@HEADS.register_module() +class CustomRoIHead(StandardRoIHead): + """CustomROIHead class for OTX.""" + + def init_bbox_head(self, bbox_roi_extractor, bbox_head): + """Initialize ``bbox_head``.""" + self.bbox_roi_extractor = build_roi_extractor(bbox_roi_extractor) + if bbox_head.type == "Shared2FCBBoxHead": + bbox_head.type = "CustomConvFCBBoxHead" + self.bbox_head = build_head(bbox_head) + + def _bbox_forward_train(self, x, sampling_results, gt_bboxes, gt_labels, img_metas): + """Run forward function and calculate loss for box head in training.""" + rois = bbox2roi([res.bboxes for res in sampling_results]) + bbox_results = self._bbox_forward(x, rois) + + labels, label_weights, bbox_targets, bbox_weights, valid_label_mask = self.bbox_head.get_targets( + sampling_results, gt_bboxes, gt_labels, img_metas, self.train_cfg + ) + loss_bbox = self.bbox_head.loss( + bbox_results["cls_score"], + bbox_results["bbox_pred"], + rois, + labels, + label_weights, + bbox_targets, + bbox_weights, + valid_label_mask=valid_label_mask, + ) + bbox_results.update(loss_bbox=loss_bbox) + return bbox_results + + def _mask_forward(self, x, rois=None, pos_inds=None, bbox_feats=None): + """Mask head forward function used in both training and testing.""" + mask_results = super()._mask_forward(x, rois, pos_inds, bbox_feats) + if mask_results["mask_pred"].device.type == "hpu": + mask_results["mask_pred"] = mask_results["mask_pred"].cpu() + mask_results["mask_feats"] = mask_results["mask_feats"].cpu() + return mask_results + + +@HEADS.register_module() +class CustomConvFCBBoxHead(Shared2FCBBoxHead, CrossDatasetDetectorHead): + """CustomConvFCBBoxHead class for OTX.""" + + def get_targets(self, sampling_results, gt_bboxes, gt_labels, img_metas, rcnn_train_cfg, concat=True): + """Calculate the ground truth for all samples in a batch according to the sampling_results. + + Almost the same as the implementation in bbox_head, we passed + additional parameters pos_inds_list and neg_inds_list to + `_get_target_single` function. + + Args: + sampling_results (List[obj:SamplingResults]): Assign results of + all images in a batch after sampling. + gt_bboxes (list[Tensor]): Gt_bboxes of all images in a batch, + each tensor has shape (num_gt, 4), the last dimension 4 + represents [tl_x, tl_y, br_x, br_y]. + gt_labels (list[Tensor]): Gt_labels of all images in a batch, + each tensor has shape (num_gt,). + img_metas (list[dict]): Meta information of each image. + rcnn_train_cfg (obj:ConfigDict): `train_cfg` of RCNN. + concat (bool): Whether to concatenate the results of all + the images in a single batch. + + Returns: + Tuple[Tensor]: Ground truth for proposals in a single image. + Containing the following list of Tensors: + + - labels (list[Tensor],Tensor): Gt_labels for all + proposals in a batch, each tensor in list has + shape (num_proposals,) when `concat=False`, otherwise + just a single tensor has shape (num_all_proposals,). + - label_weights (list[Tensor]): Labels_weights for + all proposals in a batch, each tensor in list has + shape (num_proposals,) when `concat=False`, otherwise + just a single tensor has shape (num_all_proposals,). + - bbox_targets (list[Tensor],Tensor): Regression target + for all proposals in a batch, each tensor in list + has shape (num_proposals, 4) when `concat=False`, + otherwise just a single tensor has shape + (num_all_proposals, 4), the last dimension 4 represents + [tl_x, tl_y, br_x, br_y]. + - bbox_weights (list[tensor],Tensor): Regression weights for + all proposals in a batch, each tensor in list has shape + (num_proposals, 4) when `concat=False`, otherwise just a + single tensor has shape (num_all_proposals, 4). + """ + pos_bboxes_list = [res.pos_bboxes for res in sampling_results] + neg_bboxes_list = [res.neg_bboxes for res in sampling_results] + pos_gt_bboxes_list = [res.pos_gt_bboxes for res in sampling_results] + pos_gt_labels_list = [res.pos_gt_labels for res in sampling_results] + labels, label_weights, bbox_targets, bbox_weights = multi_apply( + self._get_target_single, + pos_bboxes_list, + neg_bboxes_list, + pos_gt_bboxes_list, + pos_gt_labels_list, + cfg=rcnn_train_cfg, + ) + valid_label_mask = self.get_valid_label_mask(img_metas=img_metas, all_labels=labels, use_bg=True) + valid_label_mask = [i.to(gt_bboxes[0].device) for i in valid_label_mask] + + if concat: + labels = torch.cat(labels, 0) + label_weights = torch.cat(label_weights, 0) + bbox_targets = torch.cat(bbox_targets, 0) + bbox_weights = torch.cat(bbox_weights, 0) + valid_label_mask = torch.cat(valid_label_mask, 0) + return labels, label_weights, bbox_targets, bbox_weights, valid_label_mask + + def forward(self, x): + """ConvFCBBoxHead forward.""" + # shared part + cls_score, bbox_pred = super().forward(x) + if cls_score.device.type == "hpu": + cls_score = cls_score.cpu() + bbox_pred = bbox_pred.cpu() + + return cls_score, bbox_pred + + @force_fp32(apply_to=("cls_score", "bbox_pred")) + def loss( + self, + cls_score, + bbox_pred, + rois, + labels, + label_weights, + bbox_targets, + bbox_weights, + reduction_override=None, + valid_label_mask=None, + ): + """Loss function for CustomConvFCBBoxHead.""" + losses = dict() + if cls_score is not None and cls_score.numel() > 0: + avg_factor = max(torch.sum(label_weights > 0).float().item(), 1.0) + + if isinstance(self.loss_cls, CrossSigmoidFocalLoss): + losses["loss_cls"] = self.loss_cls( + cls_score, + labels, + label_weights, + avg_factor=avg_factor, + reduction_override=reduction_override, + use_bg=True, + valid_label_mask=valid_label_mask, + ) + else: + losses["loss_cls"] = self.loss_cls( + cls_score, labels, label_weights, avg_factor=avg_factor, reduction_override=reduction_override + ) + losses["acc"] = accuracy(cls_score, labels) + if bbox_pred is not None: + bg_class_ind = self.num_classes + # 0~self.num_classes-1 are FG, self.num_classes is BG + pos_inds = (labels >= 0) & (labels < bg_class_ind) + # do not perform bounding box regression for BG anymore. + if pos_inds.any(): + if self.reg_decoded_bbox: + # When the regression loss (e.g. `IouLoss`, + # `GIouLoss`, `DIouLoss`) is applied directly on + # the decoded bounding boxes, it decodes the + # already encoded coordinates to absolute format. + bbox_pred = self.bbox_coder.decode(rois[:, 1:], bbox_pred) + if self.reg_class_agnostic: + pos_bbox_pred = bbox_pred.view(bbox_pred.size(0), 4)[pos_inds.type(torch.bool)] + else: + pos_bbox_pred = bbox_pred.view(bbox_pred.size(0), -1, 4)[ + pos_inds.type(torch.bool), labels[pos_inds.type(torch.bool)] + ] + losses["loss_bbox"] = self.loss_bbox( + pos_bbox_pred, + bbox_targets[pos_inds.type(torch.bool)], + bbox_weights[pos_inds.type(torch.bool)], + avg_factor=bbox_targets.size(0), + reduction_override=reduction_override, + ) + else: + losses["loss_bbox"] = bbox_pred[pos_inds].sum() + return losses diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_rpn_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_rpn_head.py new file mode 100644 index 00000000000..b5bb4184fe3 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_rpn_head.py @@ -0,0 +1,25 @@ +"""Custom RPN head for OTX template.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads import RPNHead + + +@HEADS.register_module() +class CustomRPNHead(RPNHead): + """RPN head. + + Args: + in_channels (int): Number of channels in the input feature map. + init_cfg (dict or list[dict], optional): Initialization config dict. + num_convs (int): Number of convolution layers in the head. Default 1. + """ + + def forward_single(self, x): + """Forward feature map of a single scale level.""" + rpn_cls_score, rpn_bbox_pred = super().forward_single(x) + if rpn_cls_score.device.type == "hpu": + rpn_cls_score = rpn_cls_score.cpu() + rpn_bbox_pred = rpn_bbox_pred.cpu() + return rpn_cls_score, rpn_bbox_pred diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_ssd_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_ssd_head.py new file mode 100644 index 00000000000..7aebbcb3173 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_ssd_head.py @@ -0,0 +1,390 @@ +"""Custom SSD head for OTX template.""" +# Copyright (C) 2018-2021 OpenMMLab +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2020-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcv.cnn import build_activation_layer +from mmdet.core import anchor_inside_flags, unmap +from mmdet.models.builder import HEADS, build_loss +from mmdet.models.dense_heads.ssd_head import SSDHead +from mmdet.models.losses import smooth_l1_loss +from torch import nn + +from otx.algorithms.detection.adapters.mmdet.models.heads.cross_dataset_detector_head import TrackingLossDynamicsMixIn +from otx.algorithms.detection.adapters.mmdet.models.loss_dyns import ( + TrackingLossType, +) + +# pylint: disable=too-many-arguments, too-many-locals + + +@HEADS.register_module() +class CustomSSDHead(SSDHead): + """CustomSSDHead class for OTX.""" + + def __init__(self, *args, bg_loss_weight=-1.0, loss_cls=None, loss_balancing=False, **kwargs): + super().__init__(*args, **kwargs) + if loss_cls is None: + loss_cls = dict( + type="CrossEntropyLoss", + use_sigmoid=False, + reduction="none", + loss_weight=1.0, + ) + self.loss_cls = build_loss(loss_cls) + self.bg_loss_weight = bg_loss_weight + self.loss_balancing = loss_balancing + if self.loss_balancing: + self.loss_weights = torch.nn.Parameter(torch.FloatTensor(2)) + for i in range(2): + self.loss_weights.data[i] = 0.0 + + # TODO: remove this internal method + # _init_layers of CustomSSDHead(this) and of SSDHead(parent) + # Initialize almost the same model structure. + # However, there are subtle differences + # Theses differences make `load_state_dict_pre_hook()` go wrong + def _init_layers(self): + """Initialize layers of the head.""" + self.cls_convs = nn.ModuleList() + self.reg_convs = nn.ModuleList() + + act_cfg = self.act_cfg.copy() + act_cfg.setdefault("inplace", True) + for in_channel, num_base_priors in zip(self.in_channels, self.num_base_priors): + if self.use_depthwise: + activation_layer = build_activation_layer(act_cfg) + + self.reg_convs.append( + nn.Sequential( + nn.Conv2d(in_channel, in_channel, kernel_size=3, padding=1, groups=in_channel), + nn.BatchNorm2d(in_channel), + activation_layer, + nn.Conv2d(in_channel, num_base_priors * 4, kernel_size=1, padding=0), + ) + ) + self.cls_convs.append( + nn.Sequential( + nn.Conv2d(in_channel, in_channel, kernel_size=3, padding=1, groups=in_channel), + nn.BatchNorm2d(in_channel), + activation_layer, + nn.Conv2d(in_channel, num_base_priors * self.cls_out_channels, kernel_size=1, padding=0), + ) + ) + else: + self.reg_convs.append(nn.Conv2d(in_channel, num_base_priors * 4, kernel_size=3, padding=1)) + self.cls_convs.append( + nn.Conv2d(in_channel, num_base_priors * self.cls_out_channels, kernel_size=3, padding=1) + ) + + def forward(self, feats): + """Forward features from the upstream network. + + Args: + feats (tuple[Tensor]): Features from the upstream network, each is + a 4D-tensor. + + Returns: + tuple: + cls_scores (list[Tensor]): Classification scores for all scale + levels, each is a 4D-tensor, the channels number is + num_anchors * num_classes. + bbox_preds (list[Tensor]): Box energies / deltas for all scale + levels, each is a 4D-tensor, the channels number is + num_anchors * 4. + """ + cls_scores = [] + bbox_preds = [] + for feat, reg_conv, cls_conv in zip(feats, self.reg_convs, self.cls_convs): + cls_out = cls_conv(feat) + reg_out = reg_conv(feat) + if cls_out.device.type == "hpu": + cls_scores.append(cls_out.cpu()) + bbox_preds.append(reg_out.cpu()) + else: + cls_scores.append(cls_out) + bbox_preds.append(reg_out) + return cls_scores, bbox_preds + + def loss_single( + self, + cls_score, + bbox_pred, + anchor, + labels, + label_weights, + bbox_targets, + bbox_weights, + num_total_samples, + ): + """Compute loss of a single image. + + Args: + cls_score (Tensor): Box scores for eachimage + Has shape (num_total_anchors, num_classes). + bbox_pred (Tensor): Box energies / deltas for each image + level with shape (num_total_anchors, 4). + anchor (Tensor): Box reference for each scale level with shape + (num_total_anchors, 4). + labels (Tensor): Labels of each anchors with shape + (num_total_anchors,). + label_weights (Tensor): Label weights of each anchor with shape + (num_total_anchors,) + bbox_targets (Tensor): BBox regression targets of each anchor + weight shape (num_total_anchors, 4). + bbox_weights (Tensor): BBox regression loss weights of each anchor + with shape (num_total_anchors, 4). + num_total_samples (int): If sampling, num total samples equal to + the number of total anchors; Otherwise, it is the number of + positive anchors. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + + # Re-weigting BG loss + label_weights = label_weights.reshape(-1) + if self.bg_loss_weight >= 0.0: + neg_indices = labels == self.num_classes + label_weights = label_weights.clone() + label_weights[neg_indices] = self.bg_loss_weight + + loss_cls_all = self.loss_cls(cls_score, labels, label_weights) + if len(loss_cls_all.shape) > 1: + loss_cls_all = loss_cls_all.sum(-1) + # FG cat_id: [0, num_classes -1], BG cat_id: num_classes + pos_inds = self._get_pos_inds(labels) + neg_inds = (labels == self.num_classes).nonzero(as_tuple=False).view(-1) + + num_pos_samples = pos_inds.size(0) + num_neg_samples = self.train_cfg.neg_pos_ratio * num_pos_samples + if num_neg_samples > neg_inds.size(0): + num_neg_samples = neg_inds.size(0) + topk_loss_cls_neg, _ = loss_cls_all[neg_inds].topk(num_neg_samples) + loss_cls = self._get_loss_cls(num_total_samples, loss_cls_all, pos_inds, topk_loss_cls_neg) + + if self.reg_decoded_bbox: + # When the regression loss (e.g. `IouLoss`, `GIouLoss`) + # is applied directly on the decoded bounding boxes, it + # decodes the already encoded coordinates to absolute format. + bbox_pred = self.bbox_coder.decode(anchor, bbox_pred) + + # TODO: We need to verify that this is working properly. + # pylint: disable=redundant-keyword-arg + loss_bbox = self._get_loss_bbox(bbox_pred, bbox_targets, bbox_weights, num_total_samples) + return loss_cls[None], loss_bbox + + def _get_pos_inds(self, labels): + pos_inds = ((labels >= 0) & (labels < self.num_classes)).nonzero(as_tuple=False).reshape(-1) + return pos_inds + + def _get_loss_bbox(self, bbox_pred, bbox_targets, bbox_weights, num_total_samples): + loss_bbox = smooth_l1_loss( + bbox_pred, + bbox_targets, + bbox_weights, + beta=self.train_cfg.smoothl1_beta, + avg_factor=num_total_samples, + ) + + return loss_bbox + + def _get_loss_cls(self, num_total_samples, loss_cls_all, pos_inds, topk_loss_cls_neg): + loss_cls_pos = loss_cls_all[pos_inds].sum() + loss_cls_neg = topk_loss_cls_neg.sum() + loss_cls = (loss_cls_pos + loss_cls_neg) / num_total_samples + return loss_cls + + def loss(self, cls_scores, bbox_preds, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore=None): + """Loss function.""" + losses = super().loss(cls_scores, bbox_preds, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore) + losses_cls = losses["loss_cls"] + losses_bbox = losses["loss_bbox"] + + if self.loss_balancing: + losses_cls, losses_bbox = self._balance_losses(losses_cls, losses_bbox) + + return dict(loss_cls=losses_cls, loss_bbox=losses_bbox) + + def _balance_losses(self, losses_cls, losses_reg): + loss_cls = sum(_loss.mean() for _loss in losses_cls) + loss_cls = torch.exp(-self.loss_weights[0]) * loss_cls + 0.5 * self.loss_weights[0] + + loss_reg = sum(_loss.mean() for _loss in losses_reg) + loss_reg = torch.exp(-self.loss_weights[1]) * loss_reg + 0.5 * self.loss_weights[1] + + return (loss_cls, loss_reg) + + +@HEADS.register_module() +class CustomSSDHeadTrackingLossDynamics(TrackingLossDynamicsMixIn, CustomSSDHead): + """CustomSSDHead which supports tracking loss dynamics.""" + + tracking_loss_types = (TrackingLossType.cls, TrackingLossType.bbox, TrackingLossType.centerness) + + @TrackingLossDynamicsMixIn._wrap_loss + def loss(self, cls_scores, bbox_preds, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore=None): + """Compute loss from the head and prepare for loss dynamics tracking.""" + return super().loss(cls_scores, bbox_preds, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore) + + @TrackingLossDynamicsMixIn._wrap_loss_single + def loss_single( + self, cls_score, bbox_pred, anchor, labels, label_weights, bbox_targets, bbox_weights, num_total_samples + ): + """Compute loss of a single image and increase `self.cur_loss_idx` counter for loss dynamics tracking.""" + return super().loss_single( + cls_score, bbox_pred, anchor, labels, label_weights, bbox_targets, bbox_weights, num_total_samples + ) + + def _get_loss_cls(self, num_total_samples, loss_cls_all, pos_inds, topk_loss_cls_neg): + loss_cls_pos = loss_cls_all[pos_inds] + loss_cls_neg = topk_loss_cls_neg.sum() + loss_cls = (loss_cls_pos.sum() + loss_cls_neg) / num_total_samples + + self._store_loss_dyns(loss_cls_pos.detach(), TrackingLossType.cls) + return loss_cls + + def _get_loss_bbox(self, bbox_pred, bbox_targets, bbox_weights, num_total_samples): + loss_bbox = smooth_l1_loss( + bbox_pred, + bbox_targets, + bbox_weights, + beta=self.train_cfg.smoothl1_beta, + avg_factor=num_total_samples, + reduction="none", + ) + + self._store_loss_dyns(loss_bbox[self.pos_inds].detach().mean(-1), TrackingLossType.bbox) + return self._postprocess_loss(loss_bbox, reduction="mean", avg_factor=num_total_samples) + + @TrackingLossDynamicsMixIn._wrap_get_targets(concatenate_last=True) + def get_targets( + self, + anchor_list, + valid_flag_list, + gt_bboxes_list, + img_metas, + gt_bboxes_ignore_list=None, + gt_labels_list=None, + label_channels=1, + unmap_outputs=True, + return_sampling_results=False, + ): + """Get targets.""" + return super().get_targets( + anchor_list, + valid_flag_list, + gt_bboxes_list, + img_metas, + gt_bboxes_ignore_list, + gt_labels_list, + label_channels, + unmap_outputs, + return_sampling_results, + ) + + def _get_targets_single( + self, + flat_anchors, + valid_flags, + gt_bboxes, + gt_bboxes_ignore, + gt_labels, + img_meta, + label_channels=1, + unmap_outputs=True, + ): + """Compute regression and classification targets for anchors in a single image. + + Args: + flat_anchors (Tensor): Multi-level anchors of the image, which are + concatenated into a single tensor of shape (num_anchors ,4) + valid_flags (Tensor): Multi level valid flags of the image, + which are concatenated into a single tensor of + shape (num_anchors,). + gt_bboxes (Tensor): Ground truth bboxes of the image, + shape (num_gts, 4). + gt_bboxes_ignore (Tensor): Ground truth bboxes to be + ignored, shape (num_ignored_gts, 4). + img_meta (dict): Meta info of the image. + gt_labels (Tensor): Ground truth labels of each box, + shape (num_gts,). + label_channels (int): Channel of label. + unmap_outputs (bool): Whether to map outputs back to the original + set of anchors. + + Returns: + tuple: + labels_list (list[Tensor]): Labels of each level + label_weights_list (list[Tensor]): Label weights of each level + bbox_targets_list (list[Tensor]): BBox targets of each level + bbox_weights_list (list[Tensor]): BBox weights of each level + num_total_pos (int): Number of positive samples in all images + num_total_neg (int): Number of negative samples in all images + """ + inside_flags = anchor_inside_flags( + flat_anchors, valid_flags, img_meta["img_shape"][:2], self.train_cfg.allowed_border + ) + if not inside_flags.any(): + return (None,) * 7 + # assign gt and sample anchors + anchors = flat_anchors[inside_flags, :] + + assign_result = self.assigner.assign(anchors, gt_bboxes, gt_bboxes_ignore, None if self.sampling else gt_labels) + sampling_result = self.sampler.sample(assign_result, anchors, gt_bboxes) + + num_valid_anchors = anchors.shape[0] + bbox_targets = torch.zeros_like(anchors) + bbox_weights = torch.zeros_like(anchors) + labels = anchors.new_full((num_valid_anchors,), self.num_classes, dtype=torch.long) + label_weights = anchors.new_zeros(num_valid_anchors, dtype=torch.float) + + pos_inds = sampling_result.pos_inds + neg_inds = sampling_result.neg_inds + if len(pos_inds) > 0: + if not self.reg_decoded_bbox: + pos_bbox_targets = self.bbox_coder.encode(sampling_result.pos_bboxes, sampling_result.pos_gt_bboxes) + else: + pos_bbox_targets = sampling_result.pos_gt_bboxes + bbox_targets[pos_inds, :] = pos_bbox_targets + bbox_weights[pos_inds, :] = 1.0 + if gt_labels is None: + # Only rpn gives gt_labels as None + # Foreground is the first class since v2.5.0 + labels[pos_inds] = 0 + else: + labels[pos_inds] = gt_labels[sampling_result.pos_assigned_gt_inds] + if self.train_cfg.pos_weight <= 0: + label_weights[pos_inds] = 1.0 + else: + label_weights[pos_inds] = self.train_cfg.pos_weight + if len(neg_inds) > 0: + label_weights[neg_inds] = 1.0 + + # map up to original set of anchors + if unmap_outputs: + num_total_anchors = flat_anchors.size(0) + labels = unmap(labels, num_total_anchors, inside_flags, fill=self.num_classes) # fill bg label + label_weights = unmap(label_weights, num_total_anchors, inside_flags) + bbox_targets = unmap(bbox_targets, num_total_anchors, inside_flags) + bbox_weights = unmap(bbox_weights, num_total_anchors, inside_flags) + + ########## What we changed from the original mmdet code ############### + # Store all_pos_assigned_gt_inds to member variable + # to look up training loss dynamics for each gt_bboxes afterwards + pos_assigned_gt_inds = anchors.new_full((num_valid_anchors,), -1, dtype=torch.long) + if len(pos_inds) > 0: + pos_assigned_gt_inds[pos_inds] = ( + self.cur_batch_idx * self.max_gt_bboxes_len + sampling_result.pos_assigned_gt_inds + ) + if unmap_outputs: + pos_assigned_gt_inds = unmap(pos_assigned_gt_inds, num_total_anchors, inside_flags, fill=-1) + self.pos_assigned_gt_inds_list += [pos_assigned_gt_inds] + self.cur_batch_idx += 1 + ######################################################################## + + return (labels, label_weights, bbox_targets, bbox_weights, pos_inds, neg_inds, sampling_result) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_vfnet_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_vfnet_head.py new file mode 100644 index 00000000000..61ad4547436 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_vfnet_head.py @@ -0,0 +1,372 @@ +"""Custom VFNet head for OTX template.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcv.runner import force_fp32 +from mmdet.core import bbox_overlaps, distance2bbox, reduce_mean +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads.vfnet_head import VFNetHead + +from otx.algorithms.detection.adapters.mmdet.models.heads.cross_dataset_detector_head import ( + CrossDatasetDetectorHead, + TrackingLossDynamicsMixIn, +) +from otx.algorithms.detection.adapters.mmdet.models.loss_dyns import TrackingLossType +from otx.algorithms.detection.adapters.mmdet.models.losses.cross_focal_loss import ( + CrossSigmoidFocalLoss, +) + +from .custom_atss_head import CustomATSSHeadTrackingLossDynamics + +# pylint: disable=too-many-ancestors, too-many-arguments, too-many-statements, too-many-locals + + +@HEADS.register_module() +class CustomVFNetHead(CrossDatasetDetectorHead, VFNetHead): + """CustomVFNetHead class for OTX.""" + + def __init__(self, *args, bg_loss_weight=-1.0, **kwargs): + super().__init__(*args, **kwargs) + self.bg_loss_weight = bg_loss_weight + + @force_fp32(apply_to=("cls_scores", "bbox_preds", "bbox_preds_refine")) + def loss(self, cls_scores, bbox_preds, bbox_preds_refine, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore=None): + """Compute loss of the head. + + Args: + cls_scores (list[Tensor]): Box iou-aware scores for each scale + level, each is a 4D-tensor, the channel number is + num_points * num_classes. + bbox_preds (list[Tensor]): Box offsets for each + scale level, each is a 4D-tensor, the channel number is + num_points * 4. + bbox_preds_refine (list[Tensor]): Refined Box offsets for + each scale level, each is a 4D-tensor, the channel + number is num_points * 4. + gt_bboxes (list[Tensor]): Ground truth bboxes for each image with + shape (num_gts, 4) in [tl_x, tl_y, br_x, br_y] format. + gt_labels (list[Tensor]): class indices corresponding to each box + img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + gt_bboxes_ignore (None | list[Tensor]): specify which bounding + boxes can be ignored when computing the loss. + Default: None. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + assert len(cls_scores) == len(bbox_preds) == len(bbox_preds_refine) + featmap_sizes = [featmap.size()[-2:] for featmap in cls_scores] + all_level_points = self.get_points(featmap_sizes, bbox_preds[0].dtype, bbox_preds[0].device) + labels, label_weights, bbox_targets, _, valid_label_mask = self.get_targets( + cls_scores, all_level_points, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore + ) + + num_imgs = cls_scores[0].size(0) + # flatten cls_scores, bbox_preds and bbox_preds_refine + flatten_cls_scores = [ + cls_score.permute(0, 2, 3, 1).reshape(-1, self.cls_out_channels).contiguous() for cls_score in cls_scores + ] + flatten_bbox_preds = [bbox_pred.permute(0, 2, 3, 1).reshape(-1, 4).contiguous() for bbox_pred in bbox_preds] + flatten_bbox_preds_refine = [ + bbox_pred_refine.permute(0, 2, 3, 1).reshape(-1, 4).contiguous() for bbox_pred_refine in bbox_preds_refine + ] + flatten_cls_scores = torch.cat(flatten_cls_scores) + flatten_bbox_preds = torch.cat(flatten_bbox_preds) + flatten_bbox_preds_refine = torch.cat(flatten_bbox_preds_refine) + flatten_labels = torch.cat(labels) + flatten_bbox_targets = torch.cat(bbox_targets) + + valid_label_mask = [ + valid_mask.reshape(-1, self.cls_out_channels).contiguous() for valid_mask in valid_label_mask + ] + flatten_valid_label_mask = torch.cat(valid_label_mask) + # repeat points to align with bbox_preds + flatten_points = torch.cat([points.repeat(num_imgs, 1) for points in all_level_points]) + + # FG cat_id: [0, num_classes - 1], BG cat_id: num_classes + bg_class_ind = self.num_classes + pos_inds = self._get_pos_inds(flatten_labels, bg_class_ind) + num_pos = len(pos_inds) + + pos_bbox_preds = flatten_bbox_preds[pos_inds] + pos_bbox_preds_refine = flatten_bbox_preds_refine[pos_inds] + pos_labels = flatten_labels[pos_inds] + + # sync num_pos across all gpus + if self.sync_num_pos: + num_pos_avg_per_gpu = reduce_mean(pos_inds.new_tensor(num_pos).float()).item() + num_pos_avg_per_gpu = max(num_pos_avg_per_gpu, 1.0) + else: + num_pos_avg_per_gpu = num_pos + if num_pos > 0: + pos_bbox_targets = flatten_bbox_targets[pos_inds] + pos_points = flatten_points[pos_inds] + + pos_decoded_bbox_preds = distance2bbox(pos_points, pos_bbox_preds) + pos_decoded_target_preds = distance2bbox(pos_points, pos_bbox_targets) + iou_targets_ini = bbox_overlaps( + pos_decoded_bbox_preds, pos_decoded_target_preds.detach(), is_aligned=True + ).clamp(min=1e-6) + bbox_weights_ini = iou_targets_ini.clone().detach() + iou_targets_ini_avg_per_gpu = reduce_mean(bbox_weights_ini.sum()).item() + bbox_avg_factor_ini = max(iou_targets_ini_avg_per_gpu, 1.0) + loss_bbox = self._get_loss_bbox( + pos_decoded_bbox_preds, pos_decoded_target_preds, bbox_weights_ini, bbox_avg_factor_ini + ) + + pos_decoded_bbox_preds_refine = distance2bbox(pos_points, pos_bbox_preds_refine) + iou_targets_rf = bbox_overlaps( + pos_decoded_bbox_preds_refine, pos_decoded_target_preds.detach(), is_aligned=True + ).clamp(min=1e-6) + bbox_weights_rf = iou_targets_rf.clone().detach() + iou_targets_rf_avg_per_gpu = reduce_mean(bbox_weights_rf.sum()).item() + bbox_avg_factor_rf = max(iou_targets_rf_avg_per_gpu, 1.0) + loss_bbox_refine = self._get_loss_bbox_refine( + pos_decoded_target_preds, pos_decoded_bbox_preds_refine, bbox_weights_rf, bbox_avg_factor_rf + ) + + # build IoU-aware cls_score targets + if self.use_vfl: + pos_ious = iou_targets_rf.clone().detach() + cls_iou_targets = torch.zeros_like(flatten_cls_scores) + cls_iou_targets[pos_inds, pos_labels] = pos_ious + else: + loss_bbox = pos_bbox_preds.sum() * 0 + loss_bbox_refine = pos_bbox_preds_refine.sum() * 0 + if self.use_vfl: + cls_iou_targets = torch.zeros_like(flatten_cls_scores) + # Re-weigting BG loss + if self.bg_loss_weight >= 0.0: + neg_indices = flatten_labels == self.num_classes + label_weights[neg_indices] = self.bg_loss_weight + + loss_cls = self._get_loss_cls( + label_weights, + flatten_cls_scores, + flatten_labels, + flatten_valid_label_mask, + num_pos_avg_per_gpu, + cls_iou_targets, + ) + + return dict(loss_cls=loss_cls, loss_bbox=loss_bbox, loss_bbox_rf=loss_bbox_refine) + + def _get_loss_cls( + self, + label_weights, + flatten_cls_scores, + flatten_labels, + flatten_valid_label_mask, + num_pos_avg_per_gpu, + cls_iou_targets, + ): + if self.use_vfl: + if label_weights is not None: + label_weights = label_weights.unsqueeze(-1) + if isinstance(self.loss_cls, CrossSigmoidFocalLoss): + loss_cls = self.loss_cls( + flatten_cls_scores, + cls_iou_targets, + weight=label_weights, + avg_factor=num_pos_avg_per_gpu, + use_vfl=self.use_vfl, + valid_label_mask=flatten_valid_label_mask, + ) + else: + loss_cls = self.loss_cls( + flatten_cls_scores, cls_iou_targets, weight=label_weights, avg_factor=num_pos_avg_per_gpu + ) + else: + loss_cls = self.loss_cls( + flatten_cls_scores, flatten_labels, weight=label_weights, avg_factor=num_pos_avg_per_gpu + ) + + return loss_cls + + def _get_loss_bbox_refine( + self, pos_decoded_target_preds, pos_decoded_bbox_preds_refine, bbox_weights_rf, bbox_avg_factor_rf + ): + return self.loss_bbox_refine( + pos_decoded_bbox_preds_refine, + pos_decoded_target_preds.detach(), + weight=bbox_weights_rf, + avg_factor=bbox_avg_factor_rf, + ) + + def _get_loss_bbox(self, pos_decoded_bbox_preds, pos_decoded_target_preds, bbox_weights_ini, bbox_avg_factor_ini): + return self.loss_bbox( + pos_decoded_bbox_preds, + pos_decoded_target_preds.detach(), + weight=bbox_weights_ini, + avg_factor=bbox_avg_factor_ini, + ) + + def _get_pos_inds(self, flatten_labels, bg_class_ind): + pos_inds = torch.where(((flatten_labels >= 0) & (flatten_labels < bg_class_ind)) > 0)[0] + return pos_inds + + def get_targets(self, cls_scores, mlvl_points, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore): + """A wrapper for computing ATSS and FCOS targets for points in multiple images. + + Args: + cls_scores (list[Tensor]): Box iou-aware scores for each scale + level with shape (N, num_points * num_classes, H, W). + mlvl_points (list[Tensor]): Points of each fpn level, each has + shape (num_points, 2). + gt_bboxes (list[Tensor]): Ground truth bboxes of each image, + each has shape (num_gt, 4). + gt_labels (list[Tensor]): Ground truth labels of each box, + each has shape (num_gt,). + img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + gt_bboxes_ignore (None | Tensor): Ground truth bboxes to be + ignored, shape (num_ignored_gts, 4). + + Returns: + tuple: + labels_list (list[Tensor]): Labels of each level. + label_weights (Tensor/None): Label weights of all levels. + bbox_targets_list (list[Tensor]): Regression targets of each + level, (l, t, r, b). + bbox_weights (Tensor/None): Bbox weights of all levels. + """ + if self.use_atss: + return self.vfnet_to_atss_targets( + cls_scores, mlvl_points, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore + ) + self.norm_on_bbox = False + return self.get_fcos_targets(mlvl_points, gt_bboxes, gt_labels, img_metas) + + +@HEADS.register_module() +class CustomVFNetHeadTrackingLossDynamics(TrackingLossDynamicsMixIn, CustomVFNetHead): + """CustomVFNetHead which supports tracking loss dynamics.""" + + tracking_loss_types = (TrackingLossType.cls, TrackingLossType.bbox, TrackingLossType.bbox_refine) + + def __init__(self, *args, bg_loss_weight=-1, **kwargs): + super().__init__(*args, bg_loss_weight=bg_loss_weight, **kwargs) + + if not self.use_atss: + raise NotImplementedError( + "Loss dynamics tracking for VFNetHead with use_atss=False is currently not supported." + ) + + @TrackingLossDynamicsMixIn._wrap_get_targets(concatenate_last=True, flatten=True) + def get_atss_targets( + self, + anchor_list, + valid_flag_list, + gt_bboxes_list, + img_metas, + gt_bboxes_ignore_list=None, + gt_labels_list=None, + label_channels=1, + unmap_outputs=True, + ): + """Extract ATSS targets and store `self.all_pos_assigned_gt_inds` for tracking loss dynamics.""" + return super().get_atss_targets( + anchor_list, + valid_flag_list, + gt_bboxes_list, + img_metas, + gt_bboxes_ignore_list, + gt_labels_list, + label_channels, + unmap_outputs, + ) + + def _get_target_single(self, *args, **kwargs): + return CustomATSSHeadTrackingLossDynamics._get_target_single(self, *args, **kwargs) + + @TrackingLossDynamicsMixIn._wrap_loss + def loss(self, cls_scores, bbox_preds, bbox_preds_refine, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore=None): + """Obtain detection task losses and store loss dynamics.""" + return super().loss( + cls_scores, bbox_preds, bbox_preds_refine, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore + ) + + def _get_loss_cls( + self, + label_weights, + flatten_cls_scores, + flatten_labels, + flatten_valid_label_mask, + num_pos_avg_per_gpu, + cls_iou_targets, + ): + if self.use_vfl: + if label_weights is not None: + label_weights = label_weights.unsqueeze(-1) + if isinstance(self.loss_cls, CrossSigmoidFocalLoss): + loss_cls = self.loss_cls( + flatten_cls_scores, + cls_iou_targets, + weight=label_weights, + avg_factor=num_pos_avg_per_gpu, + use_vfl=self.use_vfl, + valid_label_mask=flatten_valid_label_mask, + reduction_override="none", + ) + else: + loss_cls = self.loss_cls( + flatten_cls_scores, + cls_iou_targets, + weight=label_weights, + avg_factor=num_pos_avg_per_gpu, + reduction_override="none", + ) + else: + loss_cls = self.loss_cls( + flatten_cls_scores, + flatten_labels, + weight=label_weights, + avg_factor=num_pos_avg_per_gpu, + reduction_override="none", + ) + + if len(self.pos_inds) > 0: + self._store_loss_dyns(loss_cls[self.pos_inds].detach().mean(-1), TrackingLossType.cls) + + return self._postprocess_loss(loss_cls, self.loss_cls.reduction, avg_factor=num_pos_avg_per_gpu) + + def _get_loss_bbox_refine( + self, pos_decoded_target_preds, pos_decoded_bbox_preds_refine, bbox_weights_rf, bbox_avg_factor_rf + ): + loss_bbox_refine = self.loss_bbox_refine( + pos_decoded_bbox_preds_refine, + pos_decoded_target_preds.detach(), + weight=bbox_weights_rf, + avg_factor=bbox_avg_factor_rf, + reduction_override="none", + ) + self._store_loss_dyns(loss_bbox_refine.detach(), TrackingLossType.bbox_refine) + + return self._postprocess_loss(loss_bbox_refine, self.loss_cls.reduction, avg_factor=bbox_avg_factor_rf) + + def _get_loss_bbox(self, pos_decoded_bbox_preds, pos_decoded_target_preds, bbox_weights_ini, bbox_avg_factor_ini): + loss_bbox = self.loss_bbox( + pos_decoded_bbox_preds, + pos_decoded_target_preds.detach(), + weight=bbox_weights_ini, + avg_factor=bbox_avg_factor_ini, + reduction_override="none", + ) + self._store_loss_dyns(loss_bbox.detach(), TrackingLossType.bbox) + + return self._postprocess_loss(loss_bbox, self.loss_cls.reduction, avg_factor=bbox_avg_factor_ini) + + def _get_pos_inds(self, labels, *args, **kwargs): + pos_inds = CustomVFNetHead._get_pos_inds(self, labels, *args, **kwargs) + + if len(pos_inds) > 0: + gt_inds = self.all_pos_assigned_gt_inds[pos_inds].cpu() + + self.batch_inds = gt_inds // self.max_gt_bboxes_len + self.bbox_inds = gt_inds % self.max_gt_bboxes_len + + self.pos_inds = pos_inds + return pos_inds diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_yolox_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_yolox_head.py new file mode 100644 index 00000000000..161d692e4f4 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/custom_yolox_head.py @@ -0,0 +1,304 @@ +"""Custom YOLOX head for OTX template.""" + +import torch +import torch.nn.functional as F +from mmcv.runner import force_fp32 +from mmdet.core import multi_apply, reduce_mean +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads.yolox_head import YOLOXHead +from mmdet.models.losses.utils import weight_reduce_loss + +from otx.algorithms.detection.adapters.mmdet.models.heads.cross_dataset_detector_head import ( + TrackingLossDynamicsMixIn, +) +from otx.algorithms.detection.adapters.mmdet.models.loss_dyns import TrackingLossType + + +@HEADS.register_module() +class CustomYOLOXHead(YOLOXHead): + """CustomYOLOXHead class for OTX.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @force_fp32(apply_to=("cls_scores", "bbox_preds", "objectnesses")) + def loss(self, cls_scores, bbox_preds, objectnesses, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore=None): + """Compute loss of the head. + + Args: + cls_scores (list[Tensor]): Box scores for each scale level, + each is a 4D-tensor, the channel number is + num_priors * num_classes. + bbox_preds (list[Tensor]): Box energies / deltas for each scale + level, each is a 4D-tensor, the channel number is + num_priors * 4. + objectnesses (list[Tensor], Optional): Score factor for + all scale level, each is a 4D-tensor, has shape + (batch_size, 1, H, W). + gt_bboxes (list[Tensor]): Ground truth bboxes for each image with + shape (num_gts, 4) in [tl_x, tl_y, br_x, br_y] format. + gt_labels (list[Tensor]): class indices corresponding to each box + img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + gt_bboxes_ignore (None | list[Tensor]): specify which bounding + boxes can be ignored when computing the loss. + """ + num_imgs = len(img_metas) + featmap_sizes = [cls_score.shape[2:] for cls_score in cls_scores] + mlvl_priors = self.prior_generator.grid_priors( + featmap_sizes, dtype=cls_scores[0].dtype, device=cls_scores[0].device, with_stride=True + ) + + flatten_cls_preds = [ + cls_pred.permute(0, 2, 3, 1).reshape(num_imgs, -1, self.cls_out_channels) for cls_pred in cls_scores + ] + flatten_bbox_preds = [bbox_pred.permute(0, 2, 3, 1).reshape(num_imgs, -1, 4) for bbox_pred in bbox_preds] + flatten_objectness = [objectness.permute(0, 2, 3, 1).reshape(num_imgs, -1) for objectness in objectnesses] + + flatten_cls_preds = torch.cat(flatten_cls_preds, dim=1) + flatten_bbox_preds = torch.cat(flatten_bbox_preds, dim=1) + flatten_objectness = torch.cat(flatten_objectness, dim=1) + flatten_priors = torch.cat(mlvl_priors) + flatten_bboxes = self._bbox_decode(flatten_priors, flatten_bbox_preds) + + (pos_masks, cls_targets, obj_targets, bbox_targets, l1_targets, num_fg_imgs) = multi_apply( + self._get_target_single, + flatten_cls_preds.detach(), + flatten_objectness.detach(), + flatten_priors.unsqueeze(0).repeat(num_imgs, 1, 1), + flatten_bboxes.detach(), + gt_bboxes, + gt_labels, + ) + + # The experimental results show that ‘reduce_mean’ can improve + # performance on the COCO dataset. + num_pos = torch.tensor(sum(num_fg_imgs), dtype=torch.float, device=flatten_cls_preds.device) + num_total_samples = max(reduce_mean(num_pos), 1.0) + + pos_masks = torch.cat(pos_masks, 0) + cls_targets = torch.cat(cls_targets, 0) + obj_targets = torch.cat(obj_targets, 0) + bbox_targets = torch.cat(bbox_targets, 0) + if self.use_l1: + l1_targets = torch.cat(l1_targets, 0) + + loss_bbox = self.loss_bbox(flatten_bboxes.view(-1, 4)[pos_masks], bbox_targets) / num_total_samples + loss_obj = self.loss_obj(flatten_objectness.view(-1, 1), obj_targets) / num_total_samples + if "xpu" in str(flatten_cls_preds.device): + loss_cls = ( + self.loss_cls(flatten_cls_preds.reshape(-1, self.num_classes)[pos_masks], cls_targets) + / num_total_samples + ) + else: + loss_cls = ( + self.loss_cls(flatten_cls_preds.view(-1, self.num_classes)[pos_masks], cls_targets) / num_total_samples + ) + + loss_dict = dict(loss_cls=loss_cls, loss_bbox=loss_bbox, loss_obj=loss_obj) + + if self.use_l1: + loss_l1 = self.loss_l1(flatten_bbox_preds.view(-1, 4)[pos_masks], l1_targets) / num_total_samples + loss_dict.update(loss_l1=loss_l1) + + return loss_dict + + def forward_single(self, x, cls_convs, reg_convs, conv_cls, conv_reg, conv_obj): + """Forward feature of a single scale level.""" + cls_score, bbox_pred, objectness = super().forward_single(x, cls_convs, reg_convs, conv_cls, conv_reg, conv_obj) + if cls_score.device.type == "hpu": + # put on cpu for further post-processing + cls_score = cls_score.cpu() + bbox_pred = bbox_pred.cpu() + objectness = objectness.cpu() + return cls_score, bbox_pred, objectness + + +@HEADS.register_module() +class CustomYOLOXHeadTrackingLossDynamics(TrackingLossDynamicsMixIn, CustomYOLOXHead): + """CustomYOLOXHead which supports loss dynamics tracking.""" + + tracking_loss_types = (TrackingLossType.cls, TrackingLossType.bbox) + + @TrackingLossDynamicsMixIn._wrap_loss + @force_fp32(apply_to=("cls_scores", "bbox_preds", "objectnesses")) + def loss(self, cls_scores, bbox_preds, objectnesses, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore=None): + """Compute loss of the head. + + Args: + cls_scores (list[Tensor]): Box scores for each scale level, + each is a 4D-tensor, the channel number is + num_priors * num_classes. + bbox_preds (list[Tensor]): Box energies / deltas for each scale + level, each is a 4D-tensor, the channel number is + num_priors * 4. + objectnesses (list[Tensor], Optional): Score factor for + all scale level, each is a 4D-tensor, has shape + (batch_size, 1, H, W). + gt_bboxes (list[Tensor]): Ground truth bboxes for each image with + shape (num_gts, 4) in [tl_x, tl_y, br_x, br_y] format. + gt_labels (list[Tensor]): class indices corresponding to each box + img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + gt_bboxes_ignore (None | list[Tensor]): specify which bounding + boxes can be ignored when computing the loss. + """ + num_imgs = len(img_metas) + featmap_sizes = [cls_score.shape[2:] for cls_score in cls_scores] + mlvl_priors = self.prior_generator.grid_priors( + featmap_sizes, dtype=cls_scores[0].dtype, device=cls_scores[0].device, with_stride=True + ) + + flatten_cls_preds = [ + cls_pred.permute(0, 2, 3, 1).reshape(num_imgs, -1, self.cls_out_channels) for cls_pred in cls_scores + ] + flatten_bbox_preds = [bbox_pred.permute(0, 2, 3, 1).reshape(num_imgs, -1, 4) for bbox_pred in bbox_preds] + flatten_objectness = [objectness.permute(0, 2, 3, 1).reshape(num_imgs, -1) for objectness in objectnesses] + + flatten_cls_preds = torch.cat(flatten_cls_preds, dim=1) + flatten_bbox_preds = torch.cat(flatten_bbox_preds, dim=1) + flatten_objectness = torch.cat(flatten_objectness, dim=1) + flatten_priors = torch.cat(mlvl_priors) + flatten_bboxes = self._bbox_decode(flatten_priors, flatten_bbox_preds) + + # Init variables for loss dynamics tracking + self.cur_batch_idx = 0 + self.max_gt_bboxes_len = max([len(gt_bbox) for gt_bbox in gt_bboxes]) + + ( + pos_masks, + cls_targets, + obj_targets, + bbox_targets, + l1_targets, + num_fg_imgs, + pos_assigned_gt_inds_list, + ) = multi_apply( + self._get_target_single, + flatten_cls_preds.detach(), + flatten_objectness.detach(), + flatten_priors.unsqueeze(0).repeat(num_imgs, 1, 1), + flatten_bboxes.detach(), + gt_bboxes, + gt_labels, + ) + + # The experimental results show that ‘reduce_mean’ can improve + # performance on the COCO dataset. + num_pos = torch.tensor(sum(num_fg_imgs), dtype=torch.float, device=flatten_cls_preds.device) + num_total_samples = max(reduce_mean(num_pos), 1.0) + + pos_masks = torch.cat(pos_masks, 0) + cls_targets = torch.cat(cls_targets, 0) + obj_targets = torch.cat(obj_targets, 0) + bbox_targets = torch.cat(bbox_targets, 0) + + # For storing loss dynamics + pos_assigned_gt_inds = torch.cat(pos_assigned_gt_inds_list, 0) + self.batch_inds = pos_assigned_gt_inds // self.max_gt_bboxes_len + self.bbox_inds = pos_assigned_gt_inds % self.max_gt_bboxes_len + + if self.use_l1: + l1_targets = torch.cat(l1_targets, 0) + + loss_bbox = ( + self.loss_bbox( + flatten_bboxes.view(-1, 4)[pos_masks], + bbox_targets, + reduction_override="none", + ) + / num_total_samples + ) + loss_obj = self.loss_obj(flatten_objectness.view(-1, 1), obj_targets) / num_total_samples + loss_cls = ( + self.loss_cls( + flatten_cls_preds.view(-1, self.num_classes)[pos_masks], + cls_targets, + reduction_override="none", + ) + / num_total_samples + ) + self._store_loss_dyns(loss_bbox, TrackingLossType.bbox) + self._store_loss_dyns(loss_cls.mean(-1), TrackingLossType.cls) + + loss_bbox = weight_reduce_loss(loss_bbox, reduction=self.loss_bbox.reduction) + loss_cls = weight_reduce_loss(loss_cls, reduction=self.loss_cls.reduction) + + loss_dict = dict(loss_cls=loss_cls, loss_bbox=loss_bbox, loss_obj=loss_obj) + + if self.use_l1: + loss_l1 = self.loss_l1(flatten_bbox_preds.view(-1, 4)[pos_masks], l1_targets) / num_total_samples + loss_dict.update(loss_l1=loss_l1) + + return loss_dict + + @torch.no_grad() + def _get_target_single(self, cls_preds, objectness, priors, decoded_bboxes, gt_bboxes, gt_labels): + """Compute classification, regression, and objectness targets for priors in a single image. + + Args: + cls_preds (Tensor): Classification predictions of one image, + a 2D-Tensor with shape [num_priors, num_classes] + objectness (Tensor): Objectness predictions of one image, + a 1D-Tensor with shape [num_priors] + priors (Tensor): All priors of one image, a 2D-Tensor with shape + [num_priors, 4] in [cx, xy, stride_w, stride_y] format. + decoded_bboxes (Tensor): Decoded bboxes predictions of one image, + a 2D-Tensor with shape [num_priors, 4] in [tl_x, tl_y, + br_x, br_y] format. + gt_bboxes (Tensor): Ground truth bboxes of one image, a 2D-Tensor + with shape [num_gts, 4] in [tl_x, tl_y, br_x, br_y] format. + gt_labels (Tensor): Ground truth labels of one image, a Tensor + with shape [num_gts]. + """ + + num_priors = priors.size(0) + num_gts = gt_labels.size(0) + gt_bboxes = gt_bboxes.to(decoded_bboxes.dtype) + + # No target + if num_gts == 0: + cls_target = cls_preds.new_zeros((0, self.num_classes)) + bbox_target = cls_preds.new_zeros((0, 4)) + l1_target = cls_preds.new_zeros((0, 4)) + obj_target = cls_preds.new_zeros((num_priors, 1)) + foreground_mask = cls_preds.new_zeros(num_priors).bool() + return (foreground_mask, cls_target, obj_target, bbox_target, l1_target, 0) + + # YOLOX uses center priors with 0.5 offset to assign targets, + # but use center priors without offset to regress bboxes. + offset_priors = torch.cat([priors[:, :2] + priors[:, 2:] * 0.5, priors[:, 2:]], dim=-1) + + assign_result = self.assigner.assign( + cls_preds.sigmoid() * objectness.unsqueeze(1).sigmoid(), offset_priors, decoded_bboxes, gt_bboxes, gt_labels + ) + + sampling_result = self.sampler.sample(assign_result, priors, gt_bboxes) + pos_inds = sampling_result.pos_inds + num_pos_per_img = pos_inds.size(0) + + pos_ious = assign_result.max_overlaps[pos_inds] + # IOU aware classification score + cls_target = F.one_hot(sampling_result.pos_gt_labels, self.num_classes) * pos_ious.unsqueeze(-1) + obj_target = torch.zeros_like(objectness).unsqueeze(-1) + obj_target[pos_inds] = 1 + bbox_target = sampling_result.pos_gt_bboxes + l1_target = cls_preds.new_zeros((num_pos_per_img, 4)) + if self.use_l1: + l1_target = self._get_l1_target(l1_target, bbox_target, priors[pos_inds]) + foreground_mask = torch.zeros_like(objectness).to(torch.bool) + foreground_mask[pos_inds] = 1 + + pos_assigned_gt_inds = self.cur_batch_idx * self.max_gt_bboxes_len + sampling_result.pos_assigned_gt_inds + self.cur_batch_idx += 1 + self.pos_inds = pos_inds + + return ( + foreground_mask, + cls_target, + obj_target, + bbox_target, + l1_target, + num_pos_per_img, + pos_assigned_gt_inds, + ) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/heads/detr_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/heads/detr_head.py new file mode 100644 index 00000000000..29ab16e22e2 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/heads/detr_head.py @@ -0,0 +1,266 @@ +"""DETR Head extension for OTX DINO.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict, List, Tuple + +import torch +from mmcv.runner import BaseModule +from mmcv.utils import Config +from mmdet.core import bbox_cxcywh_to_xyxy, bbox_xyxy_to_cxcywh, multi_apply, reduce_mean +from torch import Tensor + + +class DETRHeadExtension(BaseModule): + """Head of DETR. DETR:End-to-End Object Detection with Transformers. + + Origin implementation: DETRHead of detr_head.py in mmdet3.x + What's changed: Change data type of batch_gt_instances from InstanceList to List[Config]. + Since InstanceList is a new data type from mmdet3.x, List[Config] will replace it. + """ + + def loss_by_feat( + self, + all_layers_cls_scores: Tensor, + all_layers_bbox_preds: Tensor, + batch_gt_instances: List[Config], + batch_img_metas: List[dict], + batch_gt_instances_ignore=None, + ) -> Dict[str, Tensor]: + """Loss function. + + Only outputs from the last feature level are used for computing + losses by default. + + Args: + all_layers_cls_scores (Tensor): Classification outputs + of each decoder layers. Each is a 4D-tensor, has shape + (num_decoder_layers, bs, num_queries, cls_out_channels). + all_layers_bbox_preds (Tensor): Sigmoid regression + outputs of each decoder layers. Each is a 4D-tensor with + normalized coordinate format (cx, cy, w, h) and shape + (num_decoder_layers, bs, num_queries, 4). + batch_gt_instances (List[Config]): Batch of gt_instance. + It usually includes ``bboxes`` and ``labels`` attributes. + batch_img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + batch_gt_instances_ignore (list[:obj:`InstanceData`], optional): + Batch of gt_instances_ignore. It includes ``bboxes`` attribute + data that is ignored during training and testing. + Defaults to None. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + assert batch_gt_instances_ignore is None, ( + f"{self.__class__.__name__} only supports " "for batch_gt_instances_ignore setting to None." + ) + + losses_cls, losses_bbox, losses_iou = multi_apply( + self.loss_by_feat_single, + all_layers_cls_scores, + all_layers_bbox_preds, + batch_gt_instances=batch_gt_instances, + batch_img_metas=batch_img_metas, + ) + + loss_dict = dict() + # loss from the last decoder layer + loss_dict["loss_cls"] = losses_cls[-1] + loss_dict["loss_bbox"] = losses_bbox[-1] + loss_dict["loss_iou"] = losses_iou[-1] + # loss from other decoder layers + num_dec_layer = 0 + for loss_cls_i, loss_bbox_i, loss_iou_i in zip(losses_cls[:-1], losses_bbox[:-1], losses_iou[:-1]): + loss_dict[f"d{num_dec_layer}.loss_cls"] = loss_cls_i + loss_dict[f"d{num_dec_layer}.loss_bbox"] = loss_bbox_i + loss_dict[f"d{num_dec_layer}.loss_iou"] = loss_iou_i + num_dec_layer += 1 + return loss_dict + + def loss_by_feat_single( + self, cls_scores: Tensor, bbox_preds: Tensor, batch_gt_instances: List[Config], batch_img_metas: List[dict] + ) -> Tuple[Tensor, Tensor, Tensor]: + """Loss function for outputs from a single decoder layer of a single feature level. + + Args: + cls_scores (Tensor): Box score logits from a single decoder layer + for all images, has shape (bs, num_queries, cls_out_channels). + bbox_preds (Tensor): Sigmoid outputs from a single decoder layer + for all images, with normalized coordinate (cx, cy, w, h) and + shape (bs, num_queries, 4). + batch_gt_instances (List[Config]): Batch of gt_instance. + It usually includes ``bboxes`` and ``labels`` attributes. + batch_img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + + Returns: + Tuple[Tensor]: A tuple including `loss_cls`, `loss_box` and + `loss_iou`. + """ + num_imgs = cls_scores.size(0) + cls_scores_list = [cls_scores[i] for i in range(num_imgs)] + bbox_preds_list = [bbox_preds[i] for i in range(num_imgs)] + cls_reg_targets = self._get_targets(cls_scores_list, bbox_preds_list, batch_gt_instances, batch_img_metas) + ( + labels_list, + label_weights_list, + bbox_targets_list, + bbox_weights_list, + num_total_pos, + num_total_neg, + ) = cls_reg_targets + labels = torch.cat(labels_list, 0) + label_weights = torch.cat(label_weights_list, 0) + bbox_targets = torch.cat(bbox_targets_list, 0) + bbox_weights = torch.cat(bbox_weights_list, 0) + + # classification loss + cls_scores = cls_scores.reshape(-1, self.cls_out_channels) + # construct weighted avg_factor to match with the official DETR repo + cls_avg_factor = num_total_pos * 1.0 + num_total_neg * self.bg_cls_weight + if self.sync_cls_avg_factor: + cls_avg_factor = reduce_mean(cls_scores.new_tensor([cls_avg_factor])) + cls_avg_factor = max(cls_avg_factor, 1) + + loss_cls = self.loss_cls(cls_scores, labels, label_weights, avg_factor=cls_avg_factor) + + # Compute the average number of gt boxes across all gpus, for + # normalization purposes + num_total_pos = loss_cls.new_tensor([num_total_pos]) + num_total_pos = torch.clamp(reduce_mean(num_total_pos), min=1).item() + + # construct factors used for rescale bboxes + factors = [] + for img_meta, bbox_pred in zip(batch_img_metas, bbox_preds): + (img_h, img_w,) = img_meta[ + "img_shape" + ][:2] + factor = bbox_pred.new_tensor([img_w, img_h, img_w, img_h]).unsqueeze(0).repeat(bbox_pred.size(0), 1) + factors.append(factor) + factors = torch.cat(factors, 0) + + # DETR regress the relative position of boxes (cxcywh) in the image, + # thus the learning target is normalized by the image size. So here + # we need to re-scale them for calculating IoU loss + bbox_preds = bbox_preds.reshape(-1, 4) + bboxes = bbox_cxcywh_to_xyxy(bbox_preds) * factors + bboxes_gt = bbox_cxcywh_to_xyxy(bbox_targets) * factors + + # regression IoU loss, defaultly GIoU loss + loss_iou = self.loss_iou(bboxes, bboxes_gt, bbox_weights, avg_factor=num_total_pos) + + # regression L1 loss + loss_bbox = self.loss_bbox(bbox_preds, bbox_targets, bbox_weights, avg_factor=num_total_pos) + return loss_cls, loss_bbox, loss_iou + + def _get_targets( + self, + cls_scores_list: List[Tensor], + bbox_preds_list: List[Tensor], + batch_gt_instances: List[Config], + batch_img_metas: List[dict], + ) -> tuple: + """Compute regression and classification targets for a batch image. + + Outputs from a single decoder layer of a single feature level are used. + + Args: + cls_scores_list (list[Tensor]): Box score logits from a single + decoder layer for each image, has shape [num_queries, + cls_out_channels]. + bbox_preds_list (list[Tensor]): Sigmoid outputs from a single + decoder layer for each image, with normalized coordinate + (cx, cy, w, h) and shape [num_queries, 4]. + batch_gt_instances (List[Config]): Batch of gt_instance. + It usually includes ``bboxes`` and ``labels`` attributes. + batch_img_metas (list[dict]): Meta information of each image, e.g., + image size, scaling factor, etc. + + Returns: + tuple: a tuple containing the following targets. + + - labels_list (list[Tensor]): Labels for all images. + - label_weights_list (list[Tensor]): Label weights for all images. + - bbox_targets_list (list[Tensor]): BBox targets for all images. + - bbox_weights_list (list[Tensor]): BBox weights for all images. + - num_total_pos (int): Number of positive samples in all images. + - num_total_neg (int): Number of negative samples in all images. + """ + ( + labels_list, + label_weights_list, + bbox_targets_list, + bbox_weights_list, + pos_inds_list, + neg_inds_list, + ) = multi_apply( + self.__get_targets_single, cls_scores_list, bbox_preds_list, batch_gt_instances, batch_img_metas + ) + num_total_pos = sum((inds.numel() for inds in pos_inds_list)) + num_total_neg = sum((inds.numel() for inds in neg_inds_list)) + return (labels_list, label_weights_list, bbox_targets_list, bbox_weights_list, num_total_pos, num_total_neg) + + def __get_targets_single(self, cls_score: Tensor, bbox_pred: Tensor, gt_instances, img_meta: dict) -> tuple: + """Compute regression and classification targets for one image. + + Outputs from a single decoder layer of a single feature level are used. + + Args: + cls_score (Tensor): Box score logits from a single decoder layer + for one image. Shape [num_queries, cls_out_channels]. + bbox_pred (Tensor): Sigmoid outputs from a single decoder layer + for one image, with normalized coordinate (cx, cy, w, h) and + shape [num_queries, 4]. + gt_instances (:obj:`InstanceData`): Ground truth of instance + annotations. It should includes ``bboxes`` and ``labels`` + attributes. + img_meta (dict): Meta information for one image. + + Returns: + tuple[Tensor]: a tuple containing the following for one image. + + - labels (Tensor): Labels of each image. + - label_weights (Tensor]): Label weights of each image. + - bbox_targets (Tensor): BBox targets of each image. + - bbox_weights (Tensor): BBox weights of each image. + - pos_inds (Tensor): Sampled positive indices for each image. + - neg_inds (Tensor): Sampled negative indices for each image. + """ + img_h, img_w = img_meta["img_shape"][:2] + factor = bbox_pred.new_tensor([img_w, img_h, img_w, img_h]).unsqueeze(0) + num_bboxes = bbox_pred.size(0) + # # convert bbox_pred from xywh, normalized to xyxy, unnormalized + # bbox_pred = bbox_cxcywh_to_xyxy(bbox_pred) + # bbox_pred = bbox_pred * factor + + # assigner and sampler + assign_result = self.assigner.assign( + bbox_pred, cls_score, gt_instances.bboxes, gt_instances.labels, img_meta=img_meta + ) + + gt_bboxes = gt_instances.bboxes + gt_labels = gt_instances.labels + pos_inds = torch.nonzero(assign_result.gt_inds > 0, as_tuple=False).squeeze(-1).unique() + neg_inds = torch.nonzero(assign_result.gt_inds == 0, as_tuple=False).squeeze(-1).unique() + pos_assigned_gt_inds = assign_result.gt_inds[pos_inds] - 1 + pos_gt_bboxes = gt_bboxes[pos_assigned_gt_inds.long(), :] + + # label targets + labels = gt_bboxes.new_full((num_bboxes,), self.num_classes, dtype=torch.long) + labels[pos_inds] = gt_labels[pos_assigned_gt_inds] + label_weights = gt_bboxes.new_ones(num_bboxes) + + # bbox targets + bbox_targets = torch.zeros_like(bbox_pred) + bbox_weights = torch.zeros_like(bbox_pred) + bbox_weights[pos_inds] = 1.0 + + # DETR regress the relative position of boxes (cxcywh) in the image. + # Thus the learning target should be normalized by the image size, also + # the box format should be converted from defaultly x1y1x2y2 to cxcywh. + pos_gt_bboxes_normalized = pos_gt_bboxes / factor + pos_gt_bboxes_targets = bbox_xyxy_to_cxcywh(pos_gt_bboxes_normalized) + bbox_targets[pos_inds] = pos_gt_bboxes_targets + return (labels, label_weights, bbox_targets, bbox_weights, pos_inds, neg_inds) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/layers/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/layers/__init__.py new file mode 100644 index 00000000000..6dc878e1ce0 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/layers/__init__.py @@ -0,0 +1,17 @@ +"""Initial file for mmdetection layers for models.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .dino import CustomDINOTransformer +from .dino_layers import CdnQueryGenerator, DINOTransformerDecoder +from .lite_detr_layers import EfficientTransformerEncoder, EfficientTransformerLayer, SmallExpandFFN + +__all__ = [ + "CustomDINOTransformer", + "DINOTransformerDecoder", + "CdnQueryGenerator", + "EfficientTransformerEncoder", + "EfficientTransformerLayer", + "SmallExpandFFN", +] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/layers/dino.py b/src/otx/algorithms/detection/adapters/mmdet/models/layers/dino.py new file mode 100644 index 00000000000..573417cfabf --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/layers/dino.py @@ -0,0 +1,191 @@ +"""Custom DINO transformer for OTX template.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict, List, Optional, Tuple, Union + +import torch +from mmdet.models.utils.builder import TRANSFORMER +from mmdet.models.utils.transformer import DeformableDetrTransformer +from torch import Tensor, nn + + +@TRANSFORMER.register_module() +class CustomDINOTransformer(DeformableDetrTransformer): + """Custom DINO transformer. + + Original implementation: mmdet.models.utils.transformer.DeformableDETR in mmdet2.x + What's changed: The forward function is modified. + Modified implementations come from mmdet.models.detectors.dino.DINO in mmdet3.x + """ + + def init_layers(self): + """Initialize layers of the DINO. + + Unlike Deformable DETR, DINO does not need pos_trans, pos_trans_norm. + """ + self.level_embeds = torch.nn.Parameter(torch.Tensor(self.num_feature_levels, self.embed_dims)) + + self.enc_output = torch.nn.Linear(self.embed_dims, self.embed_dims) + self.enc_output_norm = torch.nn.LayerNorm(self.embed_dims) + + def forward( + self, + batch_info: List[Dict[str, Union[Tuple, Tensor]]], + mlvl_feats: List[Tensor], + mlvl_masks: List[Tensor], + query_embed: Tensor, + mlvl_pos_embeds: List[Tensor], + reg_branches: Optional[nn.ModuleList] = None, + cls_branches: Optional[nn.ModuleList] = None, + **kwargs + ): + """Forward function for `Transformer`. + + What's changed: + In mmdet3.x forward of transformer is divided into + pre_transformer() -> forward_encoder() -> pre_decoder() -> forward_decoder(). + In comparison, mmdet2.x forward function takes charge of all functions above. + The differences in Deformable DETR and DINO are occured in pre_decoder(), forward_decoder(). + Therefore this function modified those parts. Modified implementations come from + pre_decoder(), and forward_decoder() of mmdet.models.detectors.dino.DINO in mmdet3.x. + + + Args: + batch_info(list(dict(str, union(tuple, tensor)))): + Information about batch such as image shape, + gt information. + mlvl_feats (list(Tensor)): Input queries from + different level. Each element has shape + [bs, embed_dims, h, w]. + mlvl_masks (list(Tensor)): The key_padding_mask from + different level used for encoder and decoder, + each element has shape [bs, h, w]. + query_embed (Tensor): The query embedding for decoder, + with shape [num_query, c]. + mlvl_pos_embeds (list(Tensor)): The positional encoding + of feats from different level, has the shape + [bs, embed_dims, h, w]. + reg_branches (obj:`nn.ModuleList`): Regression heads for + feature maps from each decoder layer. Only would + be passed when + `with_box_refine` is True. Default to None. + cls_branches (obj:`nn.ModuleList`): Classification heads + for feature maps from each decoder layer. Only would + be passed when `as_two_stage` + is True. Default to None. + kwargs: Additional argument for forward_transformer function. + + + Returns: + tuple[Tensor]: results of decoder containing the following tensor. + + - inter_states: Outputs from decoder. If + return_intermediate_dec is True output has shape \ + (num_dec_layers, bs, num_query, embed_dims), else has \ + shape (1, bs, num_query, embed_dims). + - inter_references_out: The internal value of reference \ + points in decoder, has shape \ + (num_dec_layers, bs,num_query, embed_dims) + - enc_outputs_class: The classification score of \ + proposals generated from \ + encoder's feature maps, has shape \ + (batch, h*w, num_classes). \ + Only would be returned when `as_two_stage` is True, \ + otherwise None. + - enc_outputs_coord_unact: The regression results \ + generated from encoder's feature maps., has shape \ + (batch, h*w, 4). Only would \ + be returned when `as_two_stage` is True, \ + otherwise None. + - dn_meta (Dict[str, int]): The dictionary saves information about + group collation, including 'num_denoising_queries' and + 'num_denoising_groups'. It will be used for split outputs of + denoising and matching parts and loss calculation. + """ + feat_flatten: Union[Tensor, List[Tensor]] = [] + mask_flatten: Union[Tensor, List[Tensor]] = [] + lvl_pos_embed_flatten: Union[Tensor, List[Tensor]] = [] + spatial_shapes: Union[Tensor, List[Tensor]] = [] + for lvl, (feat, mask, pos_embed) in enumerate(zip(mlvl_feats, mlvl_masks, mlvl_pos_embeds)): + bs, c, h, w = feat.shape + spatial_shape = (h, w) + spatial_shapes.append(spatial_shape) + feat = feat.flatten(2).transpose(1, 2) + mask = mask.flatten(1) + pos_embed = pos_embed.flatten(2).transpose(1, 2) + lvl_pos_embed = pos_embed + self.level_embeds[lvl].view(1, 1, -1) + lvl_pos_embed_flatten.append(lvl_pos_embed) + feat_flatten.append(feat) + mask_flatten.append(mask) + feat_flatten = torch.cat(feat_flatten, 1) + mask_flatten = torch.cat(mask_flatten, 1) + lvl_pos_embed_flatten = torch.cat(lvl_pos_embed_flatten, 1) + spatial_shapes = torch.as_tensor(spatial_shapes, dtype=torch.long, device=feat_flatten.device) + level_start_index = torch.cat((spatial_shapes.new_zeros((1,)), spatial_shapes.prod(1).cumsum(0)[:-1])) + valid_ratios = torch.stack([self.get_valid_ratio(m) for m in mlvl_masks], 1) + + reference_points = self.get_reference_points(spatial_shapes, valid_ratios, device=feat.device) + + feat_flatten = feat_flatten.permute(1, 0, 2) # (H*W, bs, embed_dims) + lvl_pos_embed_flatten = lvl_pos_embed_flatten.permute(1, 0, 2) # (H*W, bs, embed_dims) + memory = self.encoder( + query=feat_flatten, + key=None, + value=None, + query_pos=lvl_pos_embed_flatten, + query_key_padding_mask=mask_flatten, + spatial_shapes=spatial_shapes, + reference_points=reference_points, + level_start_index=level_start_index, + valid_ratios=valid_ratios, + **kwargs + ) + + # pre_decoder part at mmdet 3.x version + memory = memory.permute(1, 0, 2) + bs, _, c = memory.shape + cls_out_features = cls_branches[self.decoder.num_layers].out_features + output_memory, output_proposals = self.gen_encoder_output_proposals(memory, mask_flatten, spatial_shapes) + enc_outputs_class = cls_branches[self.decoder.num_layers](output_memory) + enc_outputs_coord_unact = reg_branches[self.decoder.num_layers](output_memory) + output_proposals + + topk_indices = torch.topk(enc_outputs_class.max(-1)[0], k=self.two_stage_num_proposals, dim=1)[1] + topk_scores = torch.gather(enc_outputs_class, 1, topk_indices.unsqueeze(-1).repeat(1, 1, cls_out_features)) + topk_coords_unact = torch.gather(enc_outputs_coord_unact, 1, topk_indices.unsqueeze(-1).repeat(1, 1, 4)) + topk_coords = topk_coords_unact.sigmoid() + topk_coords_unact = topk_coords_unact.detach() + + query = query_embed[:, None, :] + query = query.repeat(1, bs, 1).transpose(0, 1) + if self.training: + dn_label_query, dn_bbox_query, dn_mask, dn_meta = self.dn_query_generator(batch_info) + query = torch.cat([dn_label_query, query], dim=1) + reference_points = torch.cat([dn_bbox_query, topk_coords_unact], dim=1) + else: + reference_points = topk_coords_unact + dn_mask, dn_meta = None, None + reference_points = reference_points.sigmoid() + + # forward_decoder part in mmdet 3.x + inter_states, references = self.decoder( + query=query, + value=memory, + key_padding_mask=mask_flatten, + self_attn_mask=dn_mask, + reference_points=reference_points, + spatial_shapes=spatial_shapes, + level_start_index=level_start_index, + valid_ratios=valid_ratios, + reg_branches=reg_branches, + ) + + if len(query) == self.two_stage_num_proposals: + # NOTE: This is to make sure label_embeding can be involved to + # produce loss even if there is no denoising query (no ground truth + # target in this GPU), otherwise, this will raise runtime error in + # distributed training. + inter_states[0] += self.dn_query_generator.label_embedding.weight[0, 0] * 0.0 + + return inter_states, list(references), topk_scores, topk_coords, dn_meta diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/layers/dino_layers.py b/src/otx/algorithms/detection/adapters/mmdet/models/layers/dino_layers.py new file mode 100644 index 00000000000..4dda964e3d8 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/layers/dino_layers.py @@ -0,0 +1,616 @@ +"""Initial file for mmdetection layers for models.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import math +import warnings +from typing import Any, Dict, List, Optional, Tuple, Union + +import torch +import torch.nn.functional as F +from mmcv.cnn.bricks.registry import TRANSFORMER_LAYER_SEQUENCE +from mmcv.runner import BaseModule +from mmcv.utils import Config +from mmdet.core import bbox_xyxy_to_cxcywh +from mmdet.models.utils.transformer import DeformableDetrTransformerDecoder, inverse_sigmoid +from torch import Tensor, nn + + +@TRANSFORMER_LAYER_SEQUENCE.register_module() +class DINOTransformerDecoder(DeformableDetrTransformerDecoder): + """Transformer encoder of DINO.""" + + def __init__(self, *args, return_intermediate=False, **kwargs): + super().__init__(*args, return_intermediate=return_intermediate, **kwargs) + self.ref_point_head = MLP(self.embed_dims * 2, self.embed_dims, self.embed_dims, 2) + self.norm = nn.LayerNorm(self.embed_dims) + + def forward( + self, + query: Tensor, + value: Tensor, + key_padding_mask: Tensor, + self_attn_mask: Tensor, + reference_points: Tensor, + spatial_shapes: Tensor, + level_start_index: Tensor, + valid_ratios: Tensor, + reg_branches: nn.ModuleList, + **kwargs, + ) -> Tensor: + """Forward function of Transformer decoder. + + Original implementation: forward function of DinoTransformerDecoder in mmdet3.x. + What's change: Since implementation of base transformer layer is different between mmdet2.x and mmdet3.x, + input shape of layer and some input parameters of layer is modified. + + Args: + query (Tensor): The input query, has shape (num_queries, bs, dim). + value (Tensor): The input values, has shape (num_value, bs, dim). + key_padding_mask (Tensor): The `key_padding_mask` of `self_attn` + input. ByteTensor, has shape (num_queries, bs). + self_attn_mask (Tensor): The attention mask to prevent information + leakage from different denoising groups and matching parts, has + shape (num_queries_total, num_queries_total). It is `None` when + `self.training` is `False`. + reference_points (Tensor): The initial reference, has shape + (bs, num_queries, 4) with the last dimension arranged as + (cx, cy, w, h). + spatial_shapes (Tensor): Spatial shapes of features in all levels, + has shape (num_levels, 2), last dimension represents (h, w). + level_start_index (Tensor): The start index of each level. + A tensor has shape (num_levels, ) and can be represented + as [0, h_0*w_0, h_0*w_0+h_1*w_1, ...]. + valid_ratios (Tensor): The ratios of the valid width and the valid + height relative to the width and the height of features in all + levels, has shape (bs, num_levels, 2). + reg_branches: (obj:`nn.ModuleList`): Used for refining the + regression results. + kwargs: Additional argument for attention layers. + + Returns: + Tensor: Output queries of Transformer encoder, which is also + called 'encoder output embeddings' or 'memory', has shape + (num_queries, bs, dim) + """ + intermediate = [] + intermediate_reference_points = [reference_points] + for lid, layer in enumerate(self.layers): + if reference_points.shape[-1] == 4: + reference_points_input = ( + reference_points[:, :, None] * torch.cat([valid_ratios, valid_ratios], -1)[:, None] + ) + else: + assert reference_points.shape[-1] == 2 + reference_points_input = reference_points[:, :, None] * valid_ratios[:, None] + + query_sine_embed = coordinate_to_encoding(reference_points_input[:, :, 0, :]) + query_pos = self.ref_point_head(query_sine_embed) + + query = layer( + query.permute(1, 0, 2), + query_pos=query_pos.permute(1, 0, 2), + value=value.permute(1, 0, 2), + key_padding_mask=key_padding_mask, + attn_masks=[self_attn_mask, None], + spatial_shapes=spatial_shapes, + level_start_index=level_start_index, + valid_ratios=valid_ratios, + reference_points=reference_points_input, + **kwargs, + ) + + query = query.permute(1, 0, 2) + if reg_branches is not None: + tmp = reg_branches[lid](query) + assert reference_points.shape[-1] == 4 + new_reference_points = tmp + inverse_sigmoid(reference_points, eps=1e-3) + new_reference_points = new_reference_points.sigmoid() + reference_points = new_reference_points.detach() + + if self.return_intermediate: + intermediate.append(self.norm(query)) + intermediate_reference_points.append(new_reference_points) + # NOTE this is for the "Look Forward Twice" module, + # in the DeformDETR, reference_points was appended. + + if self.return_intermediate: + return torch.stack(intermediate), torch.stack(intermediate_reference_points) + + return query, reference_points + + +class CdnQueryGenerator(BaseModule): + """Implement query generator of the Contrastive denoising (CDN). + + Proposed in`DINO: DETR with Improved DeNoising Anchor Boxes for End-to-End Object + Detection `_. + + Code is modified from the `official github repo + `_. + + Original implementation: mmdet.models.layers.transformer.dino_layers.CdnQueryGenerator + What's changed: None + + Args: + num_classes (int): Number of object classes. + embed_dims (int): The embedding dimensions of the generated queries. + num_matching_queries (int): The queries number of the matching part. + Used for generating dn_mask. + label_noise_scale (float): The scale of label noise, defaults to 0.5. + box_noise_scale (float): The scale of box noise, defaults to 1.0. + group_cfg (:obj:`ConfigDict` or dict, optional): The config of the + denoising queries grouping, includes `dynamic`, `num_dn_queries`, + and `num_groups`. Two grouping strategies, 'static dn groups' and + 'dynamic dn groups', are supported. When `dynamic` is `False`, + the `num_groups` should be set, and the number of denoising query + groups will always be `num_groups`. When `dynamic` is `True`, the + `num_dn_queries` should be set, and the group number will be + dynamic to ensure that the denoising queries number will not exceed + `num_dn_queries` to prevent large fluctuations of memory. Defaults + to `None`. + """ + + def __init__( + self, + num_classes: int, + embed_dims: int, + num_matching_queries: int, + label_noise_scale: float = 0.5, + box_noise_scale: float = 1.0, + group_cfg: Optional[Config] = None, + ) -> None: + super().__init__() + self.num_classes = num_classes + self.embed_dims = embed_dims + self.num_matching_queries = num_matching_queries + self.label_noise_scale = label_noise_scale + self.box_noise_scale = box_noise_scale + + # prepare grouping strategy + group_cfg = {} if group_cfg is None else group_cfg + self.dynamic_dn_groups = group_cfg.get("dynamic", True) + if self.dynamic_dn_groups: + if "num_dn_queries" not in group_cfg: + warnings.warn("'num_dn_queries' should be set when using " "dynamic dn groups, use 100 as default.") + self.num_dn_queries = group_cfg.get("num_dn_queries", 100) + assert isinstance(self.num_dn_queries, int), ( + f"Expected the num_dn_queries to have type int, but got " + f"{self.num_dn_queries}({type(self.num_dn_queries)}). " + ) + else: + assert "num_groups" in group_cfg, "num_groups should be set when using static dn groups" + self.num_groups = group_cfg["num_groups"] + assert isinstance(self.num_groups, int), ( + f"Expected the num_groups to have type int, but got " f"{self.num_groups}({type(self.num_groups)}). " + ) + + # NOTE The original repo of DINO set the num_embeddings 92 for coco, + # 91 (0~90) of which represents target classes and the 92 (91) + # indicates `Unknown` class. However, the embedding of `unknown` class + # is not used in the original DINO. + # TODO: num_classes + 1 or num_classes ? + self.label_embedding = nn.Embedding(self.num_classes, self.embed_dims) + + def __call__(self, batch_info: List[Dict[str, Any]]) -> tuple: + """Generate contrastive denoising (cdn) queries with ground truth. + + Descriptions of the Number Values in code and comments: + - num_target_total: the total target number of the input batch + samples. + - max_num_target: the max target number of the input batch samples. + - num_noisy_targets: the total targets number after adding noise, + i.e., num_target_total * num_groups * 2. + - num_denoising_queries: the length of the output batched queries, + i.e., max_num_target * num_groups * 2. + + NOTE The format of input bboxes in batch_info is unnormalized + (x, y, x, y), and the output bbox queries are embedded by normalized + (cx, cy, w, h) format bboxes going through inverse_sigmoid. + + Args: + batch_info (list[dict[str, union[tuple, tensor]]]): List of the batch + information such as image size, and gt information. + + Returns: + tuple: The outputs of the dn query generator. + + - dn_label_query (Tensor): The output content queries for denoising + part, has shape (bs, num_denoising_queries, dim), where + `num_denoising_queries = max_num_target * num_groups * 2`. + - dn_bbox_query (Tensor): The output reference bboxes as positions + of queries for denoising part, which are embedded by normalized + (cx, cy, w, h) format bboxes going through inverse_sigmoid, has + shape (bs, num_denoising_queries, 4) with the last dimension + arranged as (cx, cy, w, h). + - attn_mask (Tensor): The attention mask to prevent information + leakage from different denoising groups and matching parts, + will be used as `self_attn_mask` of the `decoder`, has shape + (num_queries_total, num_queries_total), where `num_queries_total` + is the sum of `num_denoising_queries` and `num_matching_queries`. + - dn_meta (Dict[str, int]): The dictionary saves information about + group collation, including 'num_denoising_queries' and + 'num_denoising_groups'. It will be used for split outputs of + denoising and matching parts and loss calculation. + """ + # normalize bbox and collate ground truth (gt) + gt_labels_list = [] + gt_bboxes_list = [] + for sample in batch_info: + img_h, img_w = sample["img_shape"] + bboxes = sample["bboxes"] + factor = bboxes.new_tensor([img_w, img_h, img_w, img_h]).unsqueeze(0) + bboxes_normalized = bboxes / factor + gt_bboxes_list.append(bboxes_normalized) + gt_labels_list.append(sample["labels"]) + gt_labels = torch.cat(gt_labels_list) # (num_target_total, 4) + gt_bboxes = torch.cat(gt_bboxes_list) + + num_target_list = [len(bboxes) for bboxes in gt_bboxes_list] + max_num_target = max(num_target_list) + num_groups = self.get_num_groups(max_num_target) + + dn_label_query = self.generate_dn_label_query(gt_labels, num_groups) + dn_bbox_query = self.generate_dn_bbox_query(gt_bboxes, num_groups) + + # The `batch_idx` saves the batch index of the corresponding sample + # for each target, has shape (num_target_total). + batch_idx = torch.cat([torch.full_like(t.long(), i) for i, t in enumerate(gt_labels_list)]) + dn_label_query, dn_bbox_query = self.collate_dn_queries( + dn_label_query, dn_bbox_query, batch_idx, len(batch_info), num_groups + ) + + attn_mask = self.generate_dn_mask(max_num_target, num_groups, device=dn_label_query.device) + + dn_meta = dict(num_denoising_queries=int(max_num_target * 2 * num_groups), num_denoising_groups=num_groups) + + return dn_label_query, dn_bbox_query, attn_mask, dn_meta + + def get_num_groups(self, max_num_target: int = None) -> int: + """Calculate denoising query groups number. + + Two grouping strategies, 'static dn groups' and 'dynamic dn groups', + are supported. When `self.dynamic_dn_groups` is `False`, the number + of denoising query groups will always be `self.num_groups`. When + `self.dynamic_dn_groups` is `True`, the group number will be dynamic, + ensuring the denoising queries number will not exceed + `self.num_dn_queries` to prevent large fluctuations of memory. + + NOTE The `num_group` is shared for different samples in a batch. When + the target numbers in the samples varies, the denoising queries of the + samples containing fewer targets are padded to the max length. + + Args: + max_num_target (int, optional): The max target number of the batch + samples. It will only be used when `self.dynamic_dn_groups` is + `True`. Defaults to `None`. + + Returns: + int: The denoising group number of the current batch. + """ + if self.dynamic_dn_groups: + assert max_num_target is not None, "group_queries should be provided when using " "dynamic dn groups" + if max_num_target == 0: + num_groups = 1 + else: + num_groups = self.num_dn_queries // max_num_target + else: + num_groups = self.num_groups + if num_groups < 1: + num_groups = 1 + return int(num_groups) + + def generate_dn_label_query(self, gt_labels: Tensor, num_groups: int) -> Tensor: + """Generate noisy labels and their query embeddings. + + The strategy for generating noisy labels is: Randomly choose labels of + `self.label_noise_scale * 0.5` proportion and override each of them + with a random object category label. + + NOTE Not add noise to all labels. Besides, the `self.label_noise_scale + * 0.5` arg is the ratio of the chosen positions, which is higher than + the actual proportion of noisy labels, because the labels to override + may be correct. And the gap becomes larger as the number of target + categories decreases. The users should notice this and modify the scale + arg or the corresponding logic according to specific dataset. + + Args: + gt_labels (Tensor): The concatenated gt labels of all samples + in the batch, has shape (num_target_total, ) where + `num_target_total = sum(num_target_list)`. + num_groups (int): The number of denoising query groups. + + Returns: + Tensor: The query embeddings of noisy labels, has shape + (num_noisy_targets, embed_dims), where `num_noisy_targets = + num_target_total * num_groups * 2`. + """ + assert self.label_noise_scale > 0 + gt_labels_expand = gt_labels.repeat(2 * num_groups, 1).view(-1) # Note `* 2` # noqa + p = torch.rand_like(gt_labels_expand.float()) + chosen_indice = torch.nonzero(p < (self.label_noise_scale * 0.5)).view(-1) # Note `* 0.5` + new_labels = torch.randint_like(chosen_indice, 0, self.num_classes) + noisy_labels_expand = gt_labels_expand.scatter(0, chosen_indice, new_labels) + dn_label_query = self.label_embedding(noisy_labels_expand) + return dn_label_query + + def generate_dn_bbox_query(self, gt_bboxes: Tensor, num_groups: int) -> Tensor: + """Generate noisy bboxes and their query embeddings. + + The strategy for generating noisy bboxes is as follow: + + .. code:: text + + +--------------------+ + | negative | + | +----------+ | + | | positive | | + | | +-----|----+------------+ + | | | | | | + | +----+-----+ | | + | | | | + +---------+----------+ | + | | + | gt bbox | + | | + | +---------+----------+ + | | | | + | | +----+-----+ | + | | | | | | + +-------------|--- +----+ | | + | | positive | | + | +----------+ | + | negative | + +--------------------+ + + The random noise is added to the top-left and down-right point + positions, hence, normalized (x, y, x, y) format of bboxes are + required. The noisy bboxes of positive queries have the points + both within the inner square, while those of negative queries + have the points both between the inner and outer squares. + + Besides, the length of outer square is twice as long as that of + the inner square, i.e., self.box_noise_scale * w_or_h / 2. + NOTE The noise is added to all the bboxes. Moreover, there is still + unconsidered case when one point is within the positive square and + the others is between the inner and outer squares. + + Args: + gt_bboxes (Tensor): The concatenated gt bboxes of all samples + in the batch, has shape (num_target_total, 4) with the last + dimension arranged as (cx, cy, w, h) where + `num_target_total = sum(num_target_list)`. + num_groups (int): The number of denoising query groups. + + Returns: + Tensor: The output noisy bboxes, which are embedded by normalized + (cx, cy, w, h) format bboxes going through inverse_sigmoid, has + shape (num_noisy_targets, 4) with the last dimension arranged as + (cx, cy, w, h), where + `num_noisy_targets = num_target_total * num_groups * 2`. + """ + assert self.box_noise_scale > 0 + device = gt_bboxes.device + + # expand gt_bboxes as groups + gt_bboxes_expand = gt_bboxes.repeat(2 * num_groups, 1) # xyxy + + # obtain index of negative queries in gt_bboxes_expand + positive_idx = torch.arange(len(gt_bboxes), dtype=torch.long, device=device) + positive_idx = positive_idx.unsqueeze(0).repeat(num_groups, 1) + positive_idx += 2 * len(gt_bboxes) * torch.arange(num_groups, dtype=torch.long, device=device)[:, None] + positive_idx = positive_idx.flatten() + negative_idx = positive_idx + len(gt_bboxes) + + # determine the sign of each element in the random part of the added + # noise to be positive or negative randomly. + rand_sign = ( + torch.randint_like(gt_bboxes_expand, low=0, high=2, dtype=torch.float32) * 2.0 - 1.0 + ) # [low, high), 1 or -1, randomly + + # calculate the random part of the added noise + rand_part = torch.rand_like(gt_bboxes_expand) # [0, 1) + rand_part[negative_idx] += 1.0 # pos: [0, 1); neg: [1, 2) + rand_part *= rand_sign # pos: (-1, 1); neg: (-2, -1] U [1, 2) + + # add noise to the bboxes + bboxes_whwh = bbox_xyxy_to_cxcywh(gt_bboxes_expand)[:, 2:].repeat(1, 2) + noisy_bboxes_expand = gt_bboxes_expand + torch.mul(rand_part, bboxes_whwh) * self.box_noise_scale / 2 # xyxy + noisy_bboxes_expand = noisy_bboxes_expand.clamp(min=0.0, max=1.0) + noisy_bboxes_expand = bbox_xyxy_to_cxcywh(noisy_bboxes_expand) + + dn_bbox_query = inverse_sigmoid(noisy_bboxes_expand, eps=1e-3) + return dn_bbox_query + + def collate_dn_queries( + self, input_label_query: Tensor, input_bbox_query: Tensor, batch_idx: Tensor, batch_size: int, num_groups: int + ) -> Tuple[Tensor, Tensor]: + """Collate generated queries to obtain batched dn queries. + + The strategy for query collation is as follow: + + .. code:: text + + input_queries (num_target_total, query_dim) + P_A1 P_B1 P_B2 N_A1 N_B1 N_B2 P'A1 P'B1 P'B2 N'A1 N'B1 N'B2 + |________ group1 ________| |________ group2 ________| + | + V + P_A1 Pad0 N_A1 Pad0 P'A1 Pad0 N'A1 Pad0 + P_B1 P_B2 N_B1 N_B2 P'B1 P'B2 N'B1 N'B2 + |____ group1 ____| |____ group2 ____| + batched_queries (batch_size, max_num_target, query_dim) + + where query_dim is 4 for bbox and self.embed_dims for label. + Notation: _-group 1; '-group 2; + A-Sample1(has 1 target); B-sample2(has 2 targets) + + Args: + input_label_query (Tensor): The generated label queries of all + targets, has shape (num_target_total, embed_dims) where + `num_target_total = sum(num_target_list)`. + input_bbox_query (Tensor): The generated bbox queries of all + targets, has shape (num_target_total, 4) with the last + dimension arranged as (cx, cy, w, h). + batch_idx (Tensor): The batch index of the corresponding sample + for each target, has shape (num_target_total). + batch_size (int): The size of the input batch. + num_groups (int): The number of denoising query groups. + + Returns: + tuple[Tensor]: Output batched label and bbox queries. + - batched_label_query (Tensor): The output batched label queries, + has shape (batch_size, max_num_target, embed_dims). + - batched_bbox_query (Tensor): The output batched bbox queries, + has shape (batch_size, max_num_target, 4) with the last dimension + arranged as (cx, cy, w, h). + """ + device = input_label_query.device + num_target_list = [torch.sum(batch_idx == idx) for idx in range(batch_size)] + max_num_target = max(num_target_list) + num_denoising_queries = int(max_num_target * 2 * num_groups) + + map_query_index = torch.cat([torch.arange(num_target, device=device) for num_target in num_target_list]) + map_query_index = torch.cat([map_query_index + max_num_target * i for i in range(2 * num_groups)]).long() + batch_idx_expand = batch_idx.repeat(2 * num_groups, 1).view(-1) + mapper = (batch_idx_expand, map_query_index) + + batched_label_query = torch.zeros(batch_size, num_denoising_queries, self.embed_dims, device=device) + batched_bbox_query = torch.zeros(batch_size, num_denoising_queries, 4, device=device) + + batched_label_query[mapper] = input_label_query + batched_bbox_query[mapper] = input_bbox_query + return batched_label_query, batched_bbox_query + + def generate_dn_mask(self, max_num_target: int, num_groups: int, device: Union[torch.device, str]) -> Tensor: + """Generate attention mask to prevent information leakage from different denoising groups and matching parts. + + .. code:: text + + 0 0 0 0 1 1 1 1 0 0 0 0 0 + 0 0 0 0 1 1 1 1 0 0 0 0 0 + 0 0 0 0 1 1 1 1 0 0 0 0 0 + 0 0 0 0 1 1 1 1 0 0 0 0 0 + 1 1 1 1 0 0 0 0 0 0 0 0 0 + 1 1 1 1 0 0 0 0 0 0 0 0 0 + 1 1 1 1 0 0 0 0 0 0 0 0 0 + 1 1 1 1 0 0 0 0 0 0 0 0 0 + 1 1 1 1 1 1 1 1 0 0 0 0 0 + 1 1 1 1 1 1 1 1 0 0 0 0 0 + 1 1 1 1 1 1 1 1 0 0 0 0 0 + 1 1 1 1 1 1 1 1 0 0 0 0 0 + 1 1 1 1 1 1 1 1 0 0 0 0 0 + max_num_target |_| |_________| num_matching_queries + |_____________| num_denoising_queries + + 1 -> True (Masked), means 'can not see'. + 0 -> False (UnMasked), means 'can see'. + + Args: + max_num_target (int): The max target number of the input batch + samples. + num_groups (int): The number of denoising query groups. + device (obj:`device` or str): The device of generated mask. + + Returns: + Tensor: The attention mask to prevent information leakage from + different denoising groups and matching parts, will be used as + `self_attn_mask` of the `decoder`, has shape (num_queries_total, + num_queries_total), where `num_queries_total` is the sum of + `num_denoising_queries` and `num_matching_queries`. + """ + num_denoising_queries = int(max_num_target * 2 * num_groups) + num_queries_total = num_denoising_queries + self.num_matching_queries + attn_mask = torch.zeros(num_queries_total, num_queries_total, device=device, dtype=torch.bool) + # Make the matching part cannot see the denoising groups + attn_mask[num_denoising_queries:, :num_denoising_queries] = True + # Make the denoising groups cannot see each other + for i in range(num_groups): + # Mask rows of one group per step. + row_scope = slice(max_num_target * 2 * i, max_num_target * 2 * (i + 1)) + left_scope = slice(max_num_target * 2 * i) + right_scope = slice(max_num_target * 2 * (i + 1), num_denoising_queries) + attn_mask[row_scope, right_scope] = True + attn_mask[row_scope, left_scope] = True + return attn_mask + + +class MLP(BaseModule): + """Very simple multi-layer perceptron (also called FFN) with relu. Mostly used in DETR series detectors. + + Args: + input_dim (int): Feature dim of the input tensor. + hidden_dim (int): Feature dim of the hidden layer. + output_dim (int): Feature dim of the output tensor. + num_layers (int): Number of FFN layers. As the last + layer of MLP only contains FFN (Linear). + """ + + def __init__(self, input_dim: int, hidden_dim: int, output_dim: int, num_layers: int) -> None: + super().__init__() + self.num_layers = num_layers + h = [hidden_dim] * (num_layers - 1) + self.layers = nn.ModuleList(nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim])) + + def forward(self, x: Tensor) -> Tensor: + """Forward function of MLP. + + Args: + x (Tensor): The input feature, has shape + (num_queries, bs, input_dim). + + + Returns: + Tensor: The output feature, has shape + (num_queries, bs, output_dim). + """ + for i, layer in enumerate(self.layers): + x = F.relu(layer(x)) if i < self.num_layers - 1 else layer(x) + return x + + +def coordinate_to_encoding( + coord_tensor: Tensor, num_feats: int = 128, temperature: int = 10000, scale: float = 2 * math.pi +): + """Convert coordinate tensor to positional encoding. + + Args: + coord_tensor (Tensor): Coordinate tensor to be converted to + positional encoding. With the last dimension as 2 or 4. + num_feats (int, optional): The feature dimension for each position + along x-axis or y-axis. Note the final returned dimension + for each position is 2 times of this value. Defaults to 128. + temperature (int, optional): The temperature used for scaling + the position embedding. Defaults to 10000. + scale (float, optional): A scale factor that scales the position + embedding. The scale will be used only when `normalize` is True. + Defaults to 2*pi. + + + Returns: + Tensor: Returned encoded positional tensor. + """ + dim_t = torch.arange(num_feats, dtype=torch.float32, device=coord_tensor.device) + dim_t = temperature ** (2 * (dim_t // 2) / num_feats) + x_embed = coord_tensor[..., 0] * scale + y_embed = coord_tensor[..., 1] * scale + pos_x = x_embed[..., None] / dim_t + pos_y = y_embed[..., None] / dim_t + pos_x = torch.stack((pos_x[..., 0::2].sin(), pos_x[..., 1::2].cos()), dim=-1).flatten(2) + pos_y = torch.stack((pos_y[..., 0::2].sin(), pos_y[..., 1::2].cos()), dim=-1).flatten(2) + if coord_tensor.size(-1) == 2: + pos = torch.cat((pos_y, pos_x), dim=-1) + elif coord_tensor.size(-1) == 4: + w_embed = coord_tensor[..., 2] * scale + pos_w = w_embed[..., None] / dim_t + pos_w = torch.stack((pos_w[..., 0::2].sin(), pos_w[..., 1::2].cos()), dim=-1).flatten(2) + + h_embed = coord_tensor[..., 3] * scale + pos_h = h_embed[..., None] / dim_t + pos_h = torch.stack((pos_h[..., 0::2].sin(), pos_h[..., 1::2].cos()), dim=-1).flatten(2) + + pos = torch.cat((pos_y, pos_x, pos_w, pos_h), dim=-1) + else: + raise ValueError("Unknown pos_tensor shape(-1):{}".format(coord_tensor.size(-1))) + return pos diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/layers/lite_detr_layers.py b/src/otx/algorithms/detection/adapters/mmdet/models/layers/lite_detr_layers.py new file mode 100644 index 00000000000..af7d57a7497 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/layers/lite_detr_layers.py @@ -0,0 +1,395 @@ +"""Layers for Lite-DETR.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import copy +import warnings + +import torch +from mmcv.cnn import Linear, build_norm_layer +from mmcv.cnn.bricks.registry import FEEDFORWARD_NETWORK, TRANSFORMER_LAYER, TRANSFORMER_LAYER_SEQUENCE +from mmcv.cnn.bricks.transformer import FFN, BaseTransformerLayer, build_transformer_layer +from mmcv.runner.base_module import BaseModule, Sequential +from torch import nn + + +@FEEDFORWARD_NETWORK.register_module() +class SmallExpandFFN(FFN): + """Implements feed-forward networks (FFNs) with small expand. + + Args: + embed_dims (int): The feature dimension. Same as + `MultiheadAttention`. Defaults: 256. + feedforward_channels (int): The hidden dimension of FFNs. + Defaults: 1024. + num_fcs (int, optional): The number of fully-connected layers in + FFNs. Default: 2. + act_cfg (dict, optional): The activation config for FFNs. + Default: dict(type='ReLU') + ffn_drop (float, optional): Probability of an element to be + zeroed in FFN. Default 0.0. + add_identity (bool, optional): Whether to add the + identity connection. Default: `True`. + dropout_layer (obj:`ConfigDict`): The dropout_layer used + when adding the shortcut. + init_cfg (obj:`mmcv.ConfigDict`): The Config for initialization. + Default: None. + """ + + def __init__( + self, + embed_dims=256, + feedforward_channels=1024, + num_fcs=2, + act_cfg=dict(type="ReLU", inplace=True), + ffn_drop=0.0, + dropout_layer=None, + add_identity=True, + init_cfg=None, + **kwargs, + ): + super().__init__( + embed_dims, + feedforward_channels, + num_fcs, + act_cfg, + ffn_drop, + dropout_layer, + add_identity, + init_cfg, + **kwargs, + ) + + layers = [] + for _ in range(num_fcs - 1): + layers.append(Sequential(Linear(embed_dims, embed_dims), self.activate, nn.Dropout(ffn_drop))) + layers.append(Linear(embed_dims, embed_dims)) + layers.append(nn.Dropout(ffn_drop)) + self.small_expand_layers = Sequential(*layers) + + self.norm1 = nn.LayerNorm(embed_dims) + self.norm2 = nn.LayerNorm(embed_dims) + + def forward(self, x, level_start_index, enc_scale, identity=None): + """Forward function for FFN.""" + x_3s = x[level_start_index[4 - enc_scale] :] + x_4s = x[: level_start_index[4 - enc_scale]] + x_4s = self.forward_ffn(self.small_expand_layers, self.norm2, x_4s, identity) + x_3s = self.forward_ffn(self.layers, self.norm1, x_3s, identity) + x = torch.cat([x_4s, x_3s], 0) + + return x + + def forward_ffn(self, layers, norm, x, identity=None): + """Forward Feed Forward Network given layers.""" + out = layers(x) + if not self.add_identity: + return self.dropout_layer(out) + if identity is None: + identity = x + return norm(identity + self.dropout_layer(out)) + + +@TRANSFORMER_LAYER.register_module() +class EfficientTransformerLayer(BaseTransformerLayer): + """Efficient TransformerLayer for Lite-DETR. + + It is base transformer encoder layer for Lite-DETR `_ . + + Args: + attn_cfgs (list[`mmcv.ConfigDict`] | obj:`mmcv.ConfigDict` | None )): + Configs for `self_attention` or `cross_attention` modules, + The order of the configs in the list should be consistent with + corresponding attentions in operation_order. + If it is a dict, all of the attention modules in operation_order + will be built with this config. Default: None. + ffn_cfgs (list[`mmcv.ConfigDict`] | obj:`mmcv.ConfigDict` | None )): + Configs for FFN, The order of the configs in the list should be + consistent with corresponding ffn in operation_order. + If it is a dict, all of the attention modules in operation_order + will be built with this config. + operation_order (tuple[str]): The execution order of operation + in transformer. Such as ('self_attn', 'norm', 'ffn', 'norm'). + Support `prenorm` when you specifying first element as `norm`. + Default:None. + norm_cfg (dict): Config dict for normalization layer. + Default: dict(type='LN'). + init_cfg (obj:`mmcv.ConfigDict`): The Config for initialization. + Default: None. + batch_first (bool): Key, Query and Value are shape + of (batch, n, embed_dim) + or (n, batch, embed_dim). Default to False. + enc_scale (int): Scale of high level features. Default is 3. + """ + + def __init__( + self, + small_expand=False, + attn_cfgs=None, + ffn_cfgs=dict( + type="FFN", + embed_dims=256, + feedforward_channels=1024, + num_fcs=2, + ffn_drop=0.0, + act_cfg=dict(type="ReLU", inplace=True), + ), + operation_order=None, + norm_cfg=dict(type="LN"), + init_cfg=None, + batch_first=False, + enc_scale=3, + **kwargs, + ): + + super().__init__(attn_cfgs, ffn_cfgs, operation_order, norm_cfg, init_cfg, batch_first, **kwargs) + self.enc_scale = enc_scale + self.small_expand = small_expand + + def forward( + self, + query, + key=None, + value=None, + query_pos=None, + key_pos=None, + attn_masks=None, + query_key_padding_mask=None, + key_padding_mask=None, + level_start_index=None, + **kwargs, + ): + """Forward function for `TransformerDecoderLayer`. + + **kwargs contains some specific arguments of attentions. + + Args: + query (Tensor): The input query with shape + [num_queries, bs, embed_dims] if + self.batch_first is False, else + [bs, num_queries embed_dims]. + key (Tensor): The key tensor with shape [num_keys, bs, + embed_dims] if self.batch_first is False, else + [bs, num_keys, embed_dims] . + value (Tensor): The value tensor with same shape as `key`. + query_pos (Tensor): The positional encoding for `query`. + Default: None. + key_pos (Tensor): The positional encoding for `key`. + Default: None. + attn_masks (List[Tensor] | None): 2D Tensor used in + calculation of corresponding attention. The length of + it should equal to the number of `attention` in + `operation_order`. Default: None. + query_key_padding_mask (Tensor): ByteTensor for `query`, with + shape [bs, num_queries]. Only used in `self_attn` layer. + Defaults to None. + key_padding_mask (Tensor): ByteTensor for `query`, with + shape [bs, num_keys]. Default: None. + level_start_index (Tensor): Start index for each level. + kwargs: Additional arguments. + + Returns: + Tensor: forwarded results with shape [num_queries, bs, embed_dims]. + """ + + norm_index = 0 + attn_index = 0 + ffn_index = 0 + identity = query + if attn_masks is None: + attn_masks = [None for _ in range(self.num_attn)] + elif isinstance(attn_masks, torch.Tensor): + attn_masks = [copy.deepcopy(attn_masks) for _ in range(self.num_attn)] + warnings.warn(f"Use same attn_mask in all attentions in " f"{self.__class__.__name__} ") + else: + assert len(attn_masks) == self.num_attn, ( + f"The length of " + f"attn_masks {len(attn_masks)} must be equal " + f"to the number of attention in " + f"operation_order {self.num_attn}" + ) + + for layer in self.operation_order: + if layer == "self_attn": + query = self.attentions[attn_index]( + query, + key, + value, + identity if self.pre_norm else None, + query_pos=query_pos, + key_pos=query_pos, + attn_mask=attn_masks[attn_index], + key_padding_mask=query_key_padding_mask, + level_start_index=level_start_index, + **kwargs, + ) + attn_index += 1 + identity = query + + elif layer == "norm": + query = self.norms[norm_index](query) + norm_index += 1 + + elif layer == "cross_attn": + query = self.attentions[attn_index]( + query, + key, + value, + identity if self.pre_norm else None, + query_pos=query_pos, + key_pos=key_pos, + attn_mask=attn_masks[attn_index], + key_padding_mask=key_padding_mask, + level_start_index=level_start_index, + **kwargs, + ) + attn_index += 1 + identity = query + + elif layer == "ffn": + if self.small_expand: + query = self.ffns[ffn_index]( + query, level_start_index, self.enc_scale, identity if self.pre_norm else None + ) + else: + query = self.ffns[ffn_index](query, identity if self.pre_norm else None) + ffn_index += 1 + + return query + + +@TRANSFORMER_LAYER_SEQUENCE.register_module() +class EfficientTransformerEncoder(BaseModule): + """TransformerEncoder of Lite-DETR. + + Args: + post_norm_cfg (dict): Config of last normalization layer. Default: + `LN`. Only used when `self.pre_norm` is `True` + """ + + def __init__( + self, + transformerlayers=None, + num_layers=None, + init_cfg=None, + post_norm_cfg=dict(type="LN"), + enc_scale=3, + num_expansion=3, + **kwargs, + ): + super().__init__(init_cfg) + if len(transformerlayers) == 2 and num_layers != 2: + if num_expansion == 1: + _transformerlayers = [copy.deepcopy(transformerlayers[0]) for _ in range(num_layers - 1)] + [ + transformerlayers[1] + ] + else: + _transformerlayers = [] + for i in range(num_expansion): + for j in range(int(num_layers / num_expansion) - 1): + _transformerlayers.append(copy.deepcopy(transformerlayers[0])) + _transformerlayers.append(copy.deepcopy(transformerlayers[1])) + else: + assert isinstance(transformerlayers, list) and len(transformerlayers) == num_layers + self.num_layers = num_layers + self.layers = nn.ModuleList() + for layer in _transformerlayers: + layer = build_transformer_layer(layer) + assert layer.enc_scale == enc_scale + self.layers.append(layer) + self.embed_dims = self.layers[0].embed_dims + self.pre_norm = self.layers[0].pre_norm + self.num_expansion = num_expansion + self.enc_scale = enc_scale + + if post_norm_cfg is not None: + self.post_norm = build_norm_layer(post_norm_cfg, self.embed_dims)[1] if self.pre_norm else None + else: + assert not self.pre_norm, f"Use prenorm in " f"{self.__class__.__name__}," f"Please specify post_norm_cfg" + self.post_norm = None + + def forward( + self, + query, + key, + value, + query_pos=None, + key_pos=None, + attn_masks=None, + query_key_padding_mask=None, + key_padding_mask=None, + level_start_index=None, + reference_points=None, + **kwargs, + ): + """Forward function for `TransformerCoder`. + + Args: + query (Tensor): Input query with shape + `(num_queries, bs, embed_dims)`. + key (Tensor): The key tensor with shape + `(num_keys, bs, embed_dims)`. + value (Tensor): The value tensor with shape + `(num_keys, bs, embed_dims)`. + query_pos (Tensor): The positional encoding for `query`. + Default: None. + key_pos (Tensor): The positional encoding for `key`. + Default: None. + attn_masks (List[Tensor], optional): Each element is 2D Tensor + which is used in calculation of corresponding attention in + operation_order. Default: None. + query_key_padding_mask (Tensor): ByteTensor for `query`, with + shape [bs, num_queries]. Only used in self-attention + Default: None. + key_padding_mask (Tensor): ByteTensor for `query`, with + shape [bs, num_keys]. Default: None. + level_start_index (Tensor): Start index for each level. + reference_points (Tensor): BBox predictions' reference. + kwargs: Additional arguments. + + Returns: + Tensor: results with shape [num_queries, bs, embed_dims]. + """ + value = query + value_tgt = value[level_start_index[4 - self.enc_scale] :] + query = value_tgt + reference_points_tgt = reference_points[:, level_start_index[4 - self.enc_scale] :] + query_pos_tgt = query_pos[level_start_index[4 - self.enc_scale] :] + for layer_id, layer in enumerate(self.layers): + if (layer_id + 1) % (self.num_layers / self.num_expansion) == 0: + query = value + output = layer( + query, + key, + value, + query_pos=query_pos, + reference_points=reference_points, + level_start_index=level_start_index, + key_pos=key_pos, + attn_masks=attn_masks, + query_key_padding_mask=query_key_padding_mask, + key_padding_mask=key_padding_mask, + **kwargs, + ) + query = output[level_start_index[4 - self.enc_scale] :] + value = output + else: + output = layer( + query, + key, + value, + query_pos=query_pos_tgt, + reference_points=reference_points_tgt, + level_start_index=level_start_index, + key_pos=key_pos, + attn_masks=attn_masks, + query_key_padding_mask=query_key_padding_mask, + key_padding_mask=key_padding_mask, + **kwargs, + ) + query = output + value = torch.cat([value[: level_start_index[4 - self.enc_scale]], query], 0) + if self.post_norm is not None: + output = self.post_norm(output) + return value diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/loss_dyns.py b/src/otx/algorithms/detection/adapters/mmdet/models/loss_dyns.py new file mode 100644 index 00000000000..f58bec60ae7 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/loss_dyns.py @@ -0,0 +1,42 @@ +"""Utililty classes for tracking loss dynamics.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from enum import IntEnum + + +class TrackingLossType(IntEnum): + """Type of loss functions to track.""" + + cls = 0 + bbox = 1 + centerness = 2 + bbox_refine = 3 + + +class LossAccumulator: + """Accumulate for tracking loss dynamics.""" + + def __init__(self): + self.sum = 0.0 + self.cnt = 0 + + def add(self, value): + """Add loss value to itself.""" + if isinstance(value, float): + self.sum += value + self.cnt += 1 + elif isinstance(value, LossAccumulator): + self.sum += value.sum + self.cnt += value.cnt + else: + raise NotImplementedError() + + @property + def mean(self): + """Obtain mean from the accumulated values.""" + if self.cnt == 0: + return 0.0 + + return self.sum / self.cnt diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/losses/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/losses/__init__.py new file mode 100644 index 00000000000..622c1710a06 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/losses/__init__.py @@ -0,0 +1,9 @@ +"""Loss list of mmdetection adapters.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .cross_focal_loss import CrossSigmoidFocalLoss, OrdinaryFocalLoss +from .l2sp_loss import L2SPLoss + +__all__ = ["CrossSigmoidFocalLoss", "L2SPLoss", "OrdinaryFocalLoss"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/losses/cross_focal_loss.py b/src/otx/algorithms/detection/adapters/mmdet/models/losses/cross_focal_loss.py new file mode 100644 index 00000000000..dec1182efae --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/losses/cross_focal_loss.py @@ -0,0 +1,153 @@ +"""Cross Focal Loss for ignore labels.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +import torch.nn.functional as F +from mmdet.models import LOSSES +from mmdet.models.losses.focal_loss import py_sigmoid_focal_loss, sigmoid_focal_loss +from mmdet.models.losses.varifocal_loss import varifocal_loss +from torch import nn + +# pylint: disable=too-many-arguments, too-many-locals, too-many-instance-attributes, unused-argument + + +def cross_sigmoid_focal_loss( + inputs, + targets, + weight=None, + num_classes=None, + alpha=0.25, + gamma=2, + reduction="mean", + avg_factor=None, + use_vfl=False, + valid_label_mask=None, +): + """Cross Focal Loss for ignore labels. + + Args: + inputs: inputs Tensor (N * C). + targets: targets Tensor (N), if use_vfl, then Tensor (N * C). + weight: weight Tensor (N), consists of (binarized label schema * weight). + num_classes: number of classes for training. + alpha: focal loss alpha. + gamma: focal loss gamma. + reduction: default = mean. + avg_factor: average factors. + use_vfl: check use vfl. + valid_label_mask: ignore label mask. + """ + cross_mask = inputs.new_ones(inputs.shape, dtype=torch.int8) + if valid_label_mask is not None: + neg_mask = targets.sum(axis=1) == 0 if use_vfl else targets == num_classes + neg_idx = neg_mask.nonzero(as_tuple=True)[0] + cross_mask[neg_idx] = valid_label_mask[neg_idx].type(torch.int8) + + if use_vfl: + calculate_loss_func = varifocal_loss + elif torch.cuda.is_available() and inputs.is_cuda: + calculate_loss_func = sigmoid_focal_loss + else: + inputs_size = inputs.size(1) + targets = F.one_hot(targets, num_classes=inputs_size + 1) + targets = targets[:, :inputs_size] + calculate_loss_func = py_sigmoid_focal_loss + + loss = ( + calculate_loss_func(inputs, targets, weight=weight, gamma=gamma, alpha=alpha, reduction="none", avg_factor=None) + * cross_mask + ) + + if reduction == "mean": + if avg_factor is None: + loss = loss.mean() + else: + loss = loss.sum() / avg_factor + elif reduction == "sum": + loss = loss.sum() + return loss + + +@LOSSES.register_module() +class CrossSigmoidFocalLoss(nn.Module): + """CrossSigmoidFocalLoss class for ignore labels with sigmoid.""" + + def __init__( + self, + use_sigmoid=True, + num_classes=None, + gamma=2.0, + alpha=0.25, + reduction="mean", + loss_weight=1.0, + ignore_index=None, + ): + super().__init__() + self.reduction = reduction + self.loss_weight = loss_weight + self.gamma = gamma + self.alpha = alpha + self.ignore_index = ignore_index + self.num_classes = num_classes + self.use_sigmoid = use_sigmoid + + self.cls_criterion = cross_sigmoid_focal_loss + + def forward( + self, + pred, + targets, + weight=None, + reduction_override=None, + avg_factor=None, + use_vfl=False, + valid_label_mask=None, + **kwargs + ): + """Forward funtion of CrossSigmoidFocalLoss.""" + assert reduction_override in (None, "none", "mean", "sum") + reduction = reduction_override if reduction_override else self.reduction + loss_cls = self.loss_weight * self.cls_criterion( + pred, + targets, + weight=weight, + num_classes=self.num_classes, + alpha=self.alpha, + gamma=self.gamma, + reduction=reduction, + avg_factor=avg_factor, + use_vfl=use_vfl, + valid_label_mask=valid_label_mask, + ) + return loss_cls + + +@LOSSES.register_module() +class OrdinaryFocalLoss(nn.Module): + """Focal loss without balancing.""" + + def __init__(self, gamma=1.5, **kwargs): + super(OrdinaryFocalLoss, self).__init__() + assert gamma >= 0 + self.gamma = gamma + + def forward(self, input, target, label_weights=None, avg_factor=None, reduction="mean", **kwars): + """Forward function for focal loss.""" + if target.numel() == 0: + return 0.0 * input.sum() + + CE = F.cross_entropy(input, target, reduction="none") + p = torch.exp(-CE) + loss = (1 - p) ** self.gamma * CE + if label_weights is not None: + assert len(loss) == len(label_weights) + loss = loss * label_weights + if avg_factor is None: + avg_factor = target.shape[0] + if reduction == "sum": + return loss.sum() + if reduction == "mean": + return loss.sum() / avg_factor + return loss diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/losses/l2sp_loss.py b/src/otx/algorithms/detection/adapters/mmdet/models/losses/l2sp_loss.py new file mode 100644 index 00000000000..6b9d534fe8b --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/losses/l2sp_loss.py @@ -0,0 +1,96 @@ +"""L2SP loss for mmdetection adapters.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.runner.checkpoint import _load_checkpoint +from mmdet.models import LOSSES +from torch import nn + +# TODO: Need to fix pylint issues +# pylint: disable=unused-argument + + +@LOSSES.register_module() +class L2SPLoss(nn.Module): + """L2-SP regularization Class for mmdetection adapter.""" + + def __init__(self, model, model_ckpt, loss_weight=0.0001): + """L2-SP regularization loss. + + Args: + model (nn.Module): Input module to regularize + model_ckpt (str): Starting-point model checkpoint + Matched params in model would be regularized to be close to starting-point params + loss_weight (float, optional): Weight of the loss. Defaults to 0.0001 + """ + super().__init__() + self.model_ckpt = model_ckpt + self.loss_weight = loss_weight + print("L2SP loss init!") + print(f" - starting-point: {model_ckpt}") + + if model_ckpt is None: + raise ValueError("Model checkpoint path should be provided to enable L2-SP loss!") + if loss_weight <= 0.0: + raise ValueError("Loss weight should be a positive value!") + + # Load weights + src_weights = _load_checkpoint(self.model_ckpt) + if "state_dict" in src_weights: + src_weights = src_weights["state_dict"] + dst_weights = model.named_parameters() + + # Strip 'module.' from weight names if any + src_weights = {k.replace("module.", ""): v for k, v in src_weights.items()} + src_weights = {k.replace("model_s.", ""): v for k, v in src_weights.items()} + # for name in src_weights: + # print(name) + + # Match weight name & shape + self.l2_weights = [] + self.l2sp_weights = [] + for name, p in dst_weights: + name = name.replace("module.", "") + if not p.requires_grad: + print(f"{name}: skip - no grad") + continue + + if name in src_weights: + src_w = src_weights[name] + elif name.replace("backbone.", "") in src_weights: + # In case of backbone only weights like ImageNet pretrained + # -> names are stating w/o 'backbone.' + src_w = src_weights[name.replace("backbone.", "")] + else: + self.l2_weights.append(p) + print(f"{name}: l2 - new param") + continue + + if p.shape != src_w.shape: + # Same name but w/ different shape + self.l2_weights.append(p) + print(f"{name}: l2 - diff shape ({p.shape} vs {src_w.shape}") + continue + + src_w.requires_grad = False + self.l2sp_weights.append((p, src_w)) + print(f"{name}: l2sp") + + def forward(self, **kwargs): + """Forward function. + + Returns: + torch.Tensor: The calculated loss + """ + + # loss = torch.tensor(0.0, requires_grad=True) + loss = 0.0 + # L2 loss + for weight in self.l2_weights: + loss += (weight**2).sum() + # L2-SP loss + for weight, weight_0 in self.l2sp_weights: + loss += ((weight - weight_0.to(weight)) ** 2).sum() + + return self.loss_weight * loss diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/necks/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/necks/__init__.py new file mode 100644 index 00000000000..3c98f2f96a7 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/necks/__init__.py @@ -0,0 +1,14 @@ +"""Neck list of mmdetection adapters.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .mmov_fpn import MMOVFPN +from .mmov_ssd_neck import MMOVSSDNeck +from .mmov_yolov3_neck import MMOVYOLOV3Neck + +__all__ = [ + "MMOVFPN", + "MMOVSSDNeck", + "MMOVYOLOV3Neck", +] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_fpn.py b/src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_fpn.py new file mode 100644 index 00000000000..bbb9cea7849 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_fpn.py @@ -0,0 +1,87 @@ +"""MMOV FPN of mmdetection adapters.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +from mmdet.models.builder import NECKS +from mmdet.models.necks.fpn import FPN +from torch import nn + +from otx.core.ov.models.mmov_model import MMOVModel + +# TODO: Need to fix pylint issues +# pylint: disable=keyword-arg-before-vararg, too-many-locals + + +@NECKS.register_module() +class MMOVFPN(FPN): + """MMOVFPN class for OMZ models.""" + + def __init__( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + outputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + init_weight: bool = False, + verify_shape: bool = True, + *args, + **kwargs + ): + + # dummy + # TODO: Need to fix what exactly the types of inputs and outputs are. + if not isinstance(inputs, dict) or not isinstance(outputs, dict): + raise ValueError("The type of inputs & outputs is invalid.") + in_channels = [8 for _ in inputs["laterals"]] + out_channels = 8 + relu_before_extra_convs = False + super().__init__( + in_channels=in_channels, + out_channels=out_channels, + relu_before_extra_convs=relu_before_extra_convs * args, + **kwargs + ) + + self.lateral_convs = nn.ModuleList() + for input_laterals, output_laterals in zip(inputs["laterals"], outputs["laterals"]): + self.lateral_convs.append( + MMOVModel( + model_path_or_model, + weight_path, + inputs=input_laterals, + outputs=output_laterals, + remove_normalize=False, + merge_bn=False, + paired_bn=False, + init_weight=init_weight, + verify_shape=verify_shape, + ) + ) + + self.fpn_convs = nn.ModuleList() + for input_fpn, output_fpn in zip(inputs["fpn"], outputs["fpn"]): + if input_fpn and output_fpn: + self.fpn_convs.append( + MMOVModel( + model_path_or_model, + weight_path, + inputs=input_fpn, + outputs=output_fpn, + remove_normalize=False, + merge_bn=True, + paired_bn=True, + init_weight=init_weight, + verify_shape=verify_shape, + ) + ) + else: + self.fpn_convs.append(nn.Identity()) + + def init_weights(self, pretrained=None): # pylint: disable=unused-argument + """Initial weights function of MMOVFPN.""" + # TODO + return diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_ssd_neck.py b/src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_ssd_neck.py new file mode 100644 index 00000000000..27df619302c --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_ssd_neck.py @@ -0,0 +1,212 @@ +"""MMOVSSDNeck class for OMZ models.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +import torch +from mmcv.cnn import ConvModule, DepthwiseSeparableConvModule +from mmcv.runner import BaseModule +from mmdet.models.builder import NECKS +from torch import nn + +from otx.core.ov.models.mmov_model import MMOVModel + +# pylint: disable=too-many-arguments, too-many-locals + + +# FIXME: get rid of defined SSDNeck as this is a workaround for forked/deprecated mmdet +class SSDNeck(BaseModule): + """Extra layers of SSD backbone to generate multi-scale feature maps. + + Args: + in_channels (Sequence[int]): Number of input channels per scale. + out_channels (Sequence[int]): Number of output channels per scale. + level_strides (Sequence[int]): Stride of 3x3 conv per level. + level_paddings (Sequence[int]): Padding size of 3x3 conv per level. + l2_norm_scale (float|None): L2 normalization layer init scale. + If None, not use L2 normalization on the first input feature. + last_kernel_size (int): Kernel size of the last conv layer. + Default: 3. + use_depthwise (bool): Whether to use DepthwiseSeparableConv. + Default: False. + conv_cfg (dict): Config dict for convolution layer. Default: None. + norm_cfg (dict): Dictionary to construct and config norm layer. + Default: None. + act_cfg (dict): Config dict for activation layer. + Default: dict(type='ReLU'). + init_cfg (dict or list[dict], optional): Initialization config dict. + """ + + def __init__( + self, + in_channels, + out_channels, + level_strides, + level_paddings, + l2_norm_scale=20.0, + last_kernel_size=3, + use_depthwise=False, + conv_cfg=None, + norm_cfg=None, + act_cfg=None, + init_cfg=None, + ): + if init_cfg is None: + init_cfg = [ + dict(type="Xavier", distribution="uniform", layer="Conv2d"), + dict(type="Constant", val=1, layer="BatchNorm2d"), + ] + super().__init__(init_cfg) + assert len(out_channels) > len(in_channels) + assert len(out_channels) - len(in_channels) == len(level_strides) + assert len(level_strides) == len(level_paddings) + assert in_channels == out_channels[: len(in_channels)] + + act_cfg = dict(type="ReLU") if act_cfg is None else act_cfg + + if l2_norm_scale: + self.l2_norm = L2Norm(in_channels[0], l2_norm_scale) + self.init_cfg += [dict(type="Constant", val=self.l2_norm.scale, override=dict(name="l2_norm"))] + + self.extra_layers = nn.ModuleList() + extra_layer_channels = out_channels[len(in_channels) :] + second_conv = DepthwiseSeparableConvModule if use_depthwise else ConvModule + + for i, (out_channel, stride, padding) in enumerate(zip(extra_layer_channels, level_strides, level_paddings)): + kernel_size = last_kernel_size if i == len(extra_layer_channels) - 1 else 3 + per_lvl_convs = nn.Sequential( + ConvModule( + out_channels[len(in_channels) - 1 + i], + out_channel // 2, + 1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=act_cfg, + ), + second_conv( + out_channel // 2, + out_channel, + kernel_size, + stride=stride, + padding=padding, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=act_cfg, + ), + ) + self.extra_layers.append(per_lvl_convs) + + def forward(self, inputs): + """Forward function.""" + outs = list(inputs) + if hasattr(self, "l2_norm"): + outs[0] = self.l2_norm(outs[0]) + + feat = outs[-1] + for layer in self.extra_layers: + feat = layer(feat) + outs.append(feat) + return tuple(outs) + + +class L2Norm(nn.Module): + """L2 normalization class.""" + + def __init__(self, n_dims, scale=20.0, eps=1e-10): + """L2 normalization layer. + + Args: + n_dims (int): Number of dimensions to be normalized + scale (float, optional): Defaults to 20.. + eps (float, optional): Used to avoid division by zero. + Defaults to 1e-10. + """ + super().__init__() + self.n_dims = n_dims + self.weight = nn.Parameter(torch.Tensor(self.n_dims)) + self.eps = eps + self.scale = scale + + def forward(self, x): + """Forward function.""" + # normalization layer convert to FP32 in FP16 training + x_float = x.float() + norm = x_float.pow(2).sum(1, keepdim=True).sqrt() + self.eps + return (self.weight[None, :, None, None].float().expand_as(x_float) * x_float / norm).type_as(x) + + +@NECKS.register_module() +class MMOVSSDNeck(SSDNeck): + """MMOVSSDNeck class for OMZ models.""" + + def __init__( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + outputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + init_weight: bool = False, + verify_shape: bool = True, + ): + # dummy + in_channels = (512, 1024) + out_channels = (512, 1024, 512, 256, 256, 256) + level_strides = (2, 2, 1, 1) + level_paddings = (1, 1, 0, 0) + l2_norm_scale = None + super().__init__( + in_channels=in_channels, + out_channels=out_channels, + level_strides=level_strides, + level_paddings=level_paddings, + l2_norm_scale=l2_norm_scale, + ) + + self._model_path_or_model = model_path_or_model + self._weight_path = weight_path + self._init_weight = init_weight + + self.extra_layers = torch.nn.ModuleList() + + # TODO: Need to fix what exactly the types of inputs and outputs are. + if not isinstance(inputs, dict) or not isinstance(outputs, dict): + raise ValueError("The type of inputs & outputs is invalid.") + for input_e, output_e in zip(inputs["extra_layers"], outputs["extra_layers"]): + if input_e and output_e: + self.extra_layers.append( + MMOVModel( + model_path_or_model, + weight_path, + inputs=input_e, + outputs=output_e, + remove_normalize=False, + merge_bn=False, + paired_bn=False, + init_weight=init_weight, + verify_shape=verify_shape, + ) + ) + else: + self.extra_layers.append(torch.nn.Identity()) + + if "l2_norm" in inputs and "l2_norm" in outputs: + for input_l2, output_l2 in zip(inputs["l2_norm"], outputs["l2_norm"]): + self.l2_norm = MMOVModel( + model_path_or_model, + weight_path, + inputs=input_l2, + outputs=output_l2, + remove_normalize=False, + merge_bn=False, + paired_bn=False, + init_weight=init_weight, + verify_shape=verify_shape, + ) + + def init_weights(self, pretrained=None): # pylint: disable=unused-argument + """Initial weights of MMOVSSDNeck.""" + # TODO + return diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_yolov3_neck.py b/src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_yolov3_neck.py new file mode 100644 index 00000000000..24473f4c34e --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/necks/mmov_yolov3_neck.py @@ -0,0 +1,71 @@ +"""MMOVYOLOV3Neck class for OMZ models.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +import torch +from mmdet.models.builder import NECKS +from mmdet.models.necks.yolo_neck import YOLOV3Neck + +from otx.core.ov.models.mmov_model import MMOVModel +from otx.core.ov.models.parser_mixin import ParserMixin # type: ignore[attr-defined] + + +@NECKS.register_module() +class MMOVYOLOV3Neck(YOLOV3Neck, ParserMixin): + """MMOVYOLOV3Neck class for OMZ models.""" + + def __init__( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + outputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + init_weight: bool = False, + verify_shape: bool = True, + ): + super(YOLOV3Neck, self).__init__() + + self._model_path_or_model = model_path_or_model + self._weight_path = weight_path + self._init_weight = init_weight + + inputs, outputs = super().parse( + model_path_or_model=model_path_or_model, + weight_path=weight_path, + inputs=inputs, + outputs=outputs, + ) + + # TODO: Need to fix what exactly the types of inputs and outputs are. + if not isinstance(inputs, dict) or not isinstance(outputs, dict): + raise ValueError("The type of inputs & outputs is invalid.") + for key in inputs.keys(): + input_t = inputs[key] + output_t = outputs[key] + + if input_t and output_t: + self.add_module( + key, + MMOVModel( + model_path_or_model, + weight_path, + inputs=input_t, + outputs=output_t, + remove_normalize=False, + init_weight=init_weight, + verify_shape=verify_shape, + ), + ) + else: + self.add_module(key, torch.nn.Identity()) + + self.num_scales = len([key for key in inputs.keys() if key.startswith("detect")]) + + def init_weights(self, pretrained=None): # pylint: disable=unused-argument + """Initial weights of MMOVYOLOV3Neck.""" + # TODO + return diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/patch_mmdeploy.py b/src/otx/algorithms/detection/adapters/mmdet/models/patch_mmdeploy.py new file mode 100644 index 00000000000..32165c12dd9 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/patch_mmdeploy.py @@ -0,0 +1,73 @@ +"""Patch mmdeploy code.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch + +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled + + +def _select_nms_index( + scores: torch.Tensor, boxes: torch.Tensor, nms_index: torch.Tensor, batch_size: int, keep_top_k: int = -1 +): + """Transform NMS output. + + Args: + scores (Tensor): The detection scores of shape + [N, num_classes, num_boxes]. + boxes (Tensor): The bounding boxes of shape [N, num_boxes, 4]. + nms_index (Tensor): NMS output of bounding boxes indexing. + batch_size (int): Batch size of the input image. + keep_top_k (int): Number of top K boxes to keep after nms. + Defaults to -1. + + Returns: + tuple[Tensor, Tensor]: (dets, labels), `dets` of shape [N, num_det, 5] + and `labels` of shape [N, num_det]. + """ + batch_inds, cls_inds = nms_index[:, 0], nms_index[:, 1] + box_inds = nms_index[:, 2] + + # index by nms output + scores = scores[batch_inds, cls_inds, box_inds].unsqueeze(1) + boxes = boxes[batch_inds, box_inds, ...] + dets = torch.cat([boxes, scores], dim=1) + + # batch all + batched_dets = dets.unsqueeze(0).repeat(batch_size, 1, 1) + batch_template = torch.arange(0, batch_size, dtype=batch_inds.dtype, device=batch_inds.device) + batched_dets = batched_dets.where( + (batch_inds == batch_template.unsqueeze(1)).unsqueeze(-1), batched_dets.new_zeros(1) + ) + + batched_labels = cls_inds.unsqueeze(0).repeat(batch_size, 1) + batched_labels = batched_labels.where( + (batch_inds == batch_template.unsqueeze(1)), batched_labels.new_ones(1, dtype=batched_labels.dtype) * -1 + ) # this line is only different line from original code + + N = batched_dets.shape[0] + + # expand tensor to eliminate [0, ...] tensor + batched_dets = torch.cat((batched_dets, batched_dets.new_zeros((N, 1, 5))), 1) + batched_labels = torch.cat((batched_labels, batched_labels.new_zeros((N, 1))), 1) + + # sort + is_use_topk = keep_top_k > 0 and (torch.onnx.is_in_onnx_export() or keep_top_k < batched_dets.shape[1]) + if is_use_topk: + _, topk_inds = batched_dets[:, :, -1].topk(keep_top_k, dim=1) + else: + _, topk_inds = batched_dets[:, :, -1].sort(dim=1, descending=True) + topk_batch_inds = torch.arange(batch_size, dtype=topk_inds.dtype, device=topk_inds.device).view(-1, 1) + batched_dets = batched_dets[topk_batch_inds, topk_inds, ...] + batched_labels = batched_labels[topk_batch_inds, topk_inds, ...] + + # slice and recover the tensor + return batched_dets, batched_labels + + +if is_mmdeploy_enabled(): + + from mmdeploy.codebase.mmdet.core.post_processing import bbox_nms + + bbox_nms.select_nms_index = _select_nms_index diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/__init__.py new file mode 100644 index 00000000000..350d4f413fb --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/__init__.py @@ -0,0 +1,9 @@ +"""OTX algorithms.detection.adapters.mmdet.models.roi_heads.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# flake8: noqa +from . import bbox_heads, mask_heads, roi_extractors + +__all__ = ["bbox_heads", "mask_heads", "roi_extractors"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/bbox_heads/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/bbox_heads/__init__.py new file mode 100644 index 00000000000..06419cca561 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/bbox_heads/__init__.py @@ -0,0 +1,8 @@ +"""Bbox Head list for mmdetection adapters.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .mmov_bbox_head import MMOVBBoxHead + +__all__ = ["MMOVBBoxHead"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/bbox_heads/mmov_bbox_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/bbox_heads/mmov_bbox_head.py new file mode 100644 index 00000000000..c144fe9e7c4 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/bbox_heads/mmov_bbox_head.py @@ -0,0 +1,116 @@ +"""MMOV bbox head for OTX.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +import torch +from mmdet.models.builder import HEADS +from mmdet.models.roi_heads.bbox_heads.bbox_head import BBoxHead + +from otx.core.ov.models.mmov_model import MMOVModel + +# TODO: Need to fix pylint issues +# pylint: disable=too-many-instance-attributes, too-many-arguments, keyword-arg-before-vararg, dangerous-default-value + + +@HEADS.register_module() +class MMOVBBoxHead(BBoxHead): + """MMOVBBoxHead class for OTX.""" + + def __init__( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Dict[str, Union[str, List[str]]] = {}, + outputs: Dict[str, Union[str, List[str]]] = {}, + init_weight: bool = False, + verify_shape: bool = True, + background_index: Optional[int] = None, + *args, + **kwargs, + ): + self._model_path_or_model = model_path_or_model + self._weight_path = weight_path + self._inputs = deepcopy(inputs) + self._outputs = deepcopy(outputs) + self._init_weight = init_weight + self._verify_sahpe = verify_shape + self._background_index = background_index + super().__init__(*args, **kwargs) + + if self._background_index is not None and self._background_index < 0: + self._background_index = self.num_classes + 1 - self._background_index + + if "extractor" in inputs and "extractor" in outputs: + self.extractor = MMOVModel( + self._model_path_or_model, + inputs=inputs["extractor"], + outputs=outputs["extractor"], + remove_normalize=False, + merge_bn=True, + paired_bn=True, + init_weight=self._init_weight, + verify_shape=self._verify_sahpe, + ) + + if self.with_cls: + assert "fc_cls" in inputs and "fc_cls" in outputs + self.fc_cls = MMOVModel( + self._model_path_or_model, + inputs=inputs["fc_cls"], + outputs=outputs["fc_cls"], + remove_normalize=False, + merge_bn=False, + paired_bn=False, + init_weight=self._init_weight, + verify_shape=self._verify_sahpe, + ) + + if self.with_reg: + assert "fc_reg" in inputs and "fc_reg" in outputs + self.fc_reg = MMOVModel( + self._model_path_or_model, + inputs=inputs["fc_reg"], + outputs=outputs["fc_reg"], + remove_normalize=False, + merge_bn=False, + paired_bn=False, + init_weight=self._init_weight, + verify_shape=self._verify_sahpe, + ) + + def init_weights(self): + """Initialize weights of MMOVBBoxHead.""" + # TODO + return + + def forward(self, x): + """Forward function of MMOVBBoxHead.""" + if getattr(self, "extractor"): + x = self.extractor(x) + cls_score = self.fc_cls(x) if self.with_cls else None + bbox_pred = self.fc_reg(x) if self.with_reg else None + + # since mmdet v2.0, BBoxHead is supposed to be + # that FG labels to [0, num_class-1] and BG labels to num_class + # but faster_rcnn_resnet50_coco, etc. from OMZ are + # that FG labels to be [1, num_class] and BG labels to be 0 + if ( + self._background_index is not None + and cls_score is not None + and self._background_index != cls_score.shape(-1) + ): + cls_score = torch.cat( + ( + cls_score[:, : self._background_index], + cls_score[:, self._background_index + 1 :], + cls_score[:, self._background_index : self._background_index + 1], + ), + -1, + ) + + return (cls_score, bbox_pred) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/mask_heads/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/mask_heads/__init__.py new file mode 100644 index 00000000000..42fee6a5d52 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/mask_heads/__init__.py @@ -0,0 +1,8 @@ +"""Mask Head list for mmdetection adapters.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .mmov_mask_head import MMOVMaskHead + +__all__ = ["MMOVMaskHead"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/mask_heads/mmov_mask_head.py b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/mask_heads/mmov_mask_head.py new file mode 100644 index 00000000000..29475519b44 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/mask_heads/mmov_mask_head.py @@ -0,0 +1,71 @@ +"""MMOV Mask Head for OTX.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +from mmdet.models.builder import HEADS +from mmdet.models.roi_heads.mask_heads.fcn_mask_head import FCNMaskHead + +from otx.core.ov.models.mmov_model import MMOVModel + +# TODO: Need to fix pylint issues +# pylint: disable=too-many-instance-attributes, too-many-arguments, keyword-arg-before-vararg, dangerous-default-value + + +@HEADS.register_module() +class MMOVMaskHead(FCNMaskHead): + """MMOVMaskHead class for OTX.""" + + def __init__( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Dict[str, Union[str, List[str]]] = {}, + outputs: Dict[str, Union[str, List[str]]] = {}, + init_weight: bool = False, + verify_shape: bool = True, + background_index: Optional[int] = None, + *args, + **kwargs, + ): + self._model_path_or_model = model_path_or_model + self._weight_path = weight_path + self._inputs = deepcopy(inputs) + self._outputs = deepcopy(outputs) + self._init_weight = init_weight + self._verify_sahpe = verify_shape + self._background_index = background_index + + # dummy input + super().__init__(*args, **kwargs) + delattr(self, "convs") + delattr(self, "upsample") + delattr(self, "conv_logits") + delattr(self, "relu") + + # if self._background_index is not None and self._background_index < 0: + # self._background_index = self.num_classes + 1 - self._background_index + + self.model = MMOVModel( + self._model_path_or_model, + inputs=inputs, + outputs=outputs, + remove_normalize=False, + merge_bn=True, + paired_bn=True, + init_weight=self._init_weight, + verify_shape=self._verify_sahpe, + ) + + def init_weights(self): + """Initial weights of MMOVMaskHead.""" + # TODO + return + + def forward(self, x): + """Forward function of MMOVMaskHead.""" + return self.model(x) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/__init__.py new file mode 100644 index 00000000000..2a3b227c607 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/__init__.py @@ -0,0 +1,8 @@ +"""ROI Extractor list for mmdetection adapters.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .single_level_roi_extractor import SingleRoIExtractor + +__all__ = ["SingleRoIExtractor"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/single_level_roi_extractor.py b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/single_level_roi_extractor.py new file mode 100644 index 00000000000..c163bd79c59 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/single_level_roi_extractor.py @@ -0,0 +1,65 @@ +"""Single ROI Extractor of mmdetection adapters.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmdet.models.builder import ROI_EXTRACTORS +from mmdet.models.roi_heads.roi_extractors.single_level_roi_extractor import ( + SingleRoIExtractor as OriginSingleRoIExtractor, +) +from torch import nn +from torch.nn import functional as F +from torch.nn.modules.utils import _pair + + +@ROI_EXTRACTORS.register_module(force=True) +class SingleRoIExtractor(OriginSingleRoIExtractor): + """SingleRoIExtractor class for mmdetection adapters.""" + + def build_roi_layers(self, layer_cfg, featmap_strides): + """Build ROI layers.""" + if layer_cfg["type"] == "RoIInterpolationPool": + cfg = layer_cfg.copy() + cfg.pop("type") + return nn.ModuleList([RoIInterpolationPool(spatial_scale=1 / s, **cfg) for s in featmap_strides]) + return super().build_roi_layers(layer_cfg, featmap_strides) + + +class RoIInterpolationPool(nn.Module): + """RoIInterpolationPool class for mmdetection adapters.""" + + def __init__(self, output_size, spatial_scale, mode="bilinear"): + super().__init__() + + self.output_size = _pair(output_size) + self.spatial_scale = float(spatial_scale) + self.mode = mode + + def forward(self, inputs, rois): + """Forward function of RoIInterpolationPool.""" + outs = [] + for roi in rois: + batch_idx = roi[0].to(dtype=torch.int) + roi = roi[1:] + roi = roi * self.spatial_scale + x1, y1 = roi[:2].floor().to(dtype=torch.int) + x2, y2 = roi[2:].ceil().to(dtype=torch.int) + outs.append( + F.interpolate( + inputs[batch_idx : batch_idx + 1, :, y1:y2, x1:x2], + self.output_size, + mode=self.mode, + align_corners=True, + ) + ) + outs = torch.cat(outs, 0) + return outs + + def __repr__(self): + """Repr function of RoIInterpolationPool.""" + name = self.__class__.__name__ + name += f"(output_size={self.output_size}, " + name += f"spatial_scale={self.spatial_scale}, " + name += f"mode={self.mode})" + return name diff --git a/src/otx/algorithms/detection/adapters/mmdet/nncf/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/nncf/__init__.py new file mode 100644 index 00000000000..c3109f2dff0 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/nncf/__init__.py @@ -0,0 +1,12 @@ +"""NNCF utils for mmdet.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# flake8: noqa + +from .builder import build_nncf_detector + +__all__ = [ + "build_nncf_detector", +] diff --git a/src/otx/algorithms/detection/adapters/mmdet/nncf/builder.py b/src/otx/algorithms/detection/adapters/mmdet/nncf/builder.py new file mode 100644 index 00000000000..9befb61cbea --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/nncf/builder.py @@ -0,0 +1,241 @@ +"""NNCF wrapped mmdet models builder.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from functools import partial +from typing import Optional, Union + +import torch +from mmcv.parallel import DataContainer +from mmcv.runner import CheckpointLoader +from mmcv.utils import Config, ConfigDict + +from otx.algorithms.common.adapters.mmcv.nncf.runners import NNCF_META_KEY +from otx.algorithms.common.adapters.mmcv.utils import ( + get_configs_by_pairs, + remove_from_configs_by_type, +) +from otx.algorithms.common.adapters.nncf import is_accuracy_aware_training_set +from otx.algorithms.common.adapters.nncf.compression import NNCFMetaState +from otx.algorithms.common.adapters.nncf.utils import no_nncf_trace +from otx.algorithms.detection.adapters.mmdet.utils import build_detector +from otx.utils.logger import get_logger + +logger = get_logger() + + +def build_nncf_detector( # pylint: disable=too-many-locals,too-many-statements + config: Config, + train_cfg: Optional[Union[Config, ConfigDict]] = None, + test_cfg: Optional[Union[Config, ConfigDict]] = None, + checkpoint: Optional[str] = None, + device: Union[str, torch.device] = "cpu", + cfg_options: Optional[Union[Config, ConfigDict]] = None, + distributed=False, +): + """A function to build NNCF wrapped mmdet model.""" + + from mmdet.apis import multi_gpu_test, single_gpu_test + from mmdet.apis.inference import LoadImage + from mmdet.datasets import build_dataloader as mmdet_build_dataloader + from mmdet.datasets import build_dataset as mmdet_build_dataset + from mmdet.datasets.pipelines import Compose + from nncf.torch.dynamic_graph.io_handling import nncf_model_input + + from otx.algorithms.common.adapters.mmcv.nncf.utils import ( + get_fake_input, + model_eval, + wrap_nncf_model, + ) + from otx.algorithms.common.adapters.mmcv.utils.builder import ( + build_dataloader, + build_dataset, + ) + + if checkpoint is None: + # load model in this function not in runner + checkpoint = config.get("load_from") + assert checkpoint is not None, "checkpoint is not given. NNCF model must be initialized with pretrained model" + + model = build_detector( + config, + train_cfg=train_cfg, + test_cfg=test_cfg, + cfg_options=cfg_options, + from_scratch=True, + ) + model = model.to(device) + + state_dict = CheckpointLoader.load_checkpoint(checkpoint, map_location=device) + + is_acc_aware = is_accuracy_aware_training_set(config.get("nncf_config")) + + init_dataloader = None + model_eval_fn = None + if "meta" in state_dict and NNCF_META_KEY in state_dict["meta"]: + # NNCF ckpt + nncf_meta_state = state_dict["meta"][NNCF_META_KEY] + data_to_build_nncf = nncf_meta_state.data_to_build + state_to_build_nncf = nncf_meta_state.state_to_build + else: + # pytorch ckpt + state_to_build_nncf = state_dict + if "state_dict" in state_dict: + state_to_build_nncf = state_dict["state_dict"] + + init_dataloader = build_dataloader( + build_dataset( + config, + subset="train", + dataset_builder=mmdet_build_dataset, + ), + config, + subset="val", + dataloader_builder=mmdet_build_dataloader, + distributed=distributed, + persistent_workers=False, + ) + + # This data and state dict will be used to build NNCF graph later + # when loading NNCF model + # because some models run their subcomponents based on intermediate outputs + # resulting differently and partially traced NNCF graph + data_to_build_nncf = next(iter(init_dataloader))["img"] + if isinstance(data_to_build_nncf, DataContainer): + data_to_build_nncf = data_to_build_nncf.data[0] + data_to_build_nncf = data_to_build_nncf.cpu().numpy() + if len(data_to_build_nncf.shape) == 4: + data_to_build_nncf = data_to_build_nncf[0] + if data_to_build_nncf.shape[0] == 3: + data_to_build_nncf = data_to_build_nncf.transpose(1, 2, 0) + + val_dataloader = None + if is_acc_aware: + val_dataloader = build_dataloader( + build_dataset( + config, + subset="val", + dataset_builder=mmdet_build_dataset, + ), + config, + subset="val", + dataloader_builder=mmdet_build_dataloader, + distributed=distributed, + persistent_workers=False, + ) + + model_eval_fn = partial( + model_eval, + config=config, + val_dataloader=val_dataloader, + evaluate_fn=multi_gpu_test if distributed else single_gpu_test, + distributed=distributed, + ) + state_dict = None + + test_pipeline = [LoadImage()] + for pipeline in config.data.test.pipeline: + if not pipeline.type.startswith("LoadImage"): + test_pipeline.append(pipeline) + if pipeline.get("transforms", None): + transforms = pipeline.transforms + for transform in transforms: + if transform.type == "Collect": + for collect_key in transform["keys"]: + if collect_key != "img": + transform["keys"].remove(collect_key) + + test_pipeline = Compose(test_pipeline) + get_fake_input_fn = partial( + get_fake_input, + preprocessor=test_pipeline, + data=data_to_build_nncf, + ) + + if "nncf_compress_postprocessing" in config: + # NB: This parameter is used to choose if we should try to make NNCF compression + # for a whole model graph including postprocessing (`nncf_compress_postprocessing=True`), + # or make NNCF compression of the part of the model without postprocessing + # (`nncf_compress_postprocessing=False`). + # Our primary goal is to make NNCF compression of such big part of the model as + # possible, so `nncf_compress_postprocessing=True` is our primary choice, whereas + # `nncf_compress_postprocessing=False` is our fallback decision. + # When we manage to enable NNCF compression for sufficiently many models, + # we should keep one choice only. + nncf_compress_postprocessing = config.get("nncf_compress_postprocessing") + logger.debug(f"set should_compress_postprocessing={nncf_compress_postprocessing}") + else: + # TODO: Do we have to keep this configuration? + # This configuration is not enabled in forked mmdetection library in the first place + nncf_compress_postprocessing = True + + def dummy_forward_fn(model): + def _get_fake_data_for_forward(nncf_config): + device = next(model.parameters()).device + + if nncf_config.get("input_info", None) and nncf_config.get("input_info").get("sample_size", None): + input_size = nncf_config.get("input_info").get("sample_size") + assert len(input_size) == 4 and input_size[0] == 1 + H, W, C = input_size[2], input_size[3], input_size[1] # pylint: disable=invalid-name + shape = tuple([H, W, C]) + else: + shape = (128, 128, 3) + + with no_nncf_trace(): + return get_fake_input_fn(shape=shape, device=device) + + fake_data = _get_fake_data_for_forward(config.nncf_config) + img, img_metas = fake_data["img"], fake_data["img_metas"] + ctx = model.nncf_trace_context(img_metas, nncf_compress_postprocessing) + with ctx: + # The device where model is could be changed under this context + img = [i.to(next(model.parameters()).device) for i in img] + # Marking data as NNCF network input must be after device movement + img = [nncf_model_input(i) for i in img] + if nncf_compress_postprocessing: + logger.debug("NNCF will try to compress a postprocessing part of the model") + else: + logger.debug("NNCF will NOT compress a postprocessing part of the model") + img = img[0] + model(img) + + compression_ctrl, model = wrap_nncf_model( + config, + model, + model_eval_fn=model_eval_fn, + dummy_forward_fn=dummy_forward_fn, + dataloader_for_init=init_dataloader, + is_accuracy_aware=is_acc_aware, + ) + + # update runner to save metadata + config.runner.nncf_meta = NNCFMetaState( + state_to_build=state_to_build_nncf, + data_to_build=data_to_build_nncf, + ) + + # update custom hooks + custom_hooks = config.get("custom_hooks", []) + custom_hooks.append(ConfigDict({"type": "CancelTrainingHook"})) + custom_hooks.append( + ConfigDict( + type="CompressionHook", + compression_ctrl=compression_ctrl, + ) + ) + # TODO: move this to OTX task + remove_from_configs_by_type(custom_hooks, "CancelInterfaceHook") + remove_from_configs_by_type(custom_hooks, "TaskAdaptHook") + remove_from_configs_by_type(custom_hooks, "LazyEarlyStoppingHook") + remove_from_configs_by_type(custom_hooks, "EarlyStoppingHook") + remove_from_configs_by_type(custom_hooks, "EMAHook") + remove_from_configs_by_type(custom_hooks, "CustomModelEMAHook") + config.custom_hooks = custom_hooks + + for hook in get_configs_by_pairs(custom_hooks, dict(type="OTXProgressHook")): + time_monitor = hook.get("time_monitor", None) + if time_monitor and getattr(time_monitor, "on_initialization_end", None) is not None: + time_monitor.on_initialization_end() + + return compression_ctrl, model diff --git a/src/otx/algorithms/detection/adapters/mmdet/nncf/patches.py b/src/otx/algorithms/detection/adapters/mmdet/nncf/patches.py new file mode 100644 index 00000000000..da640d248c9 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/nncf/patches.py @@ -0,0 +1,167 @@ +"""Patch mmdet.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=protected-access,redefined-outer-name + +from functools import partial + +from mmdet.core.bbox.assigners.base_assigner import BaseAssigner +from mmdet.core.bbox.builder import BBOX_ASSIGNERS, BBOX_SAMPLERS +from mmdet.core.bbox.samplers import BaseSampler +from mmdet.models.builder import HEADS +from mmdet.models.dense_heads.base_dense_head import BaseDenseHead +from mmdet.models.dense_heads.base_mask_head import BaseMaskHead +from mmdet.models.detectors.base import BaseDetector +from mmdet.models.roi_heads.base_roi_head import BaseRoIHead +from mmdet.models.roi_heads.bbox_heads.bbox_head import BBoxHead +from mmdet.models.roi_heads.bbox_heads.sabl_head import SABLHead +from mmdet.models.roi_heads.mask_heads.fcn_mask_head import FCNMaskHead + +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +from otx.algorithms.common.adapters.nncf import ( + NNCF_PATCHER, + is_in_nncf_tracing, + nncf_trace_wrapper, + no_nncf_trace_wrapper, +) +from otx.algorithms.common.adapters.nncf.patches import nncf_trace_context + +HEADS_TARGETS = dict( + classes=( + BaseDenseHead, + BaseMaskHead, + BaseRoIHead, + FCNMaskHead, + BBoxHead, + SABLHead, + ), + fn_names=("loss", "onnx_export", "get_bboxes", "get_seg_masks"), +) + +BBOX_ASSIGNERS_TARGETS = dict( + classes=(BaseAssigner,), + fn_names=("assign",), +) + +SAMPLERS_TARGETS = dict( + classes=(BaseSampler,), + fn_names=("sample",), +) + + +def _should_wrap(obj_cls, fn_name, targets): + classes = targets["classes"] + fn_names = targets["fn_names"] + + if obj_cls is None: + return False + if fn_name not in fn_names: + return False + if issubclass(obj_cls, classes) and getattr(obj_cls, fn_name, None): + return True + return False + + +def _wrap_mmdet_head(obj_cls): + for fn_name in HEADS_TARGETS["fn_names"]: + if _should_wrap(obj_cls, fn_name, HEADS_TARGETS): + NNCF_PATCHER.patch((obj_cls, fn_name), no_nncf_trace_wrapper) + # 'onnx_export' method calls 'forward' method which need to be traced + NNCF_PATCHER.patch((obj_cls, "forward"), nncf_trace_wrapper) + + +def _wrap_mmdet_bbox_assigner(obj_cls): + for fn_name in BBOX_ASSIGNERS_TARGETS["fn_names"]: + if _should_wrap(obj_cls, fn_name, BBOX_ASSIGNERS_TARGETS): + NNCF_PATCHER.patch((obj_cls, fn_name), no_nncf_trace_wrapper) + + +def _wrap_mmdet_sampler(obj_cls): + for fn_name in SAMPLERS_TARGETS["fn_names"]: + if _should_wrap(obj_cls, fn_name, SAMPLERS_TARGETS): + NNCF_PATCHER.patch((obj_cls, fn_name), no_nncf_trace_wrapper) + + +# pylint: disable=invalid-name,unused-argument +def _wrap_register_module(self, fn, *args, **kwargs): + """A function to wrap classes lazily defined such as custom ones.""" + + module = kwargs.get("module", args[0] if args else None) + assert module is not None + _wrap_mmdet_head(module) + _wrap_mmdet_bbox_assigner(module) + _wrap_mmdet_sampler(module) + return fn(*args, **kwargs) + + +# for mmdet defined heads +for head_cls in [BaseDenseHead, BaseMaskHead, BaseRoIHead] + list(HEADS.module_dict.values()): + _wrap_mmdet_head(head_cls) + +# for mmdet defined bbox assigners +for bbox_assigner_cls in [BaseAssigner] + list(BBOX_ASSIGNERS.module_dict.values()): + _wrap_mmdet_bbox_assigner(bbox_assigner_cls) + +# for mmdet defined samplers +# NNCF can not trace this part with torch older 1.11.0 +for sampler_cls in [BaseSampler] + list(BBOX_SAMPLERS.module_dict.values()): + _wrap_mmdet_sampler(sampler_cls) + +# for custom defined +NNCF_PATCHER.patch(HEADS._register_module, _wrap_register_module) +NNCF_PATCHER.patch(BBOX_ASSIGNERS._register_module, _wrap_register_module) +NNCF_PATCHER.patch(BBOX_SAMPLERS._register_module, _wrap_register_module) +NNCF_PATCHER.patch( + "mmdet.models.roi_heads.roi_extractors.SingleRoIExtractor.map_roi_levels", + no_nncf_trace_wrapper, +) +NNCF_PATCHER.patch("mmdet.core.bbox2result", no_nncf_trace_wrapper) +NNCF_PATCHER.patch("mmdet.core.bbox2roi", no_nncf_trace_wrapper) + + +def _wrap_is_in_onnx_export(ctx, fn): + # TODO: find a better way to solve this w/o patching 'torch.onnx.is_in_onnx_export' + # + # prevent incomplete graph building for MaskRCNN models + # + # possible alternatives + # - take the onnx branch in all cases + + import sys + + frame = sys._getframe() + ctr = 2 + while frame is not None and ctr: + frame = frame.f_back + ctr -= 1 + if ( + frame is not None + and frame.f_code.co_name == "forward" + and "self" in frame.f_locals.keys() + and frame.f_locals["self"].__class__.__name__ == "SingleRoIExtractor" + ): + return fn() or is_in_nncf_tracing() + return fn() + + +NNCF_PATCHER.patch("torch.onnx.is_in_onnx_export", _wrap_is_in_onnx_export) + +# add nncf context method that will be used when nncf tracing +BaseDetector.nncf_trace_context = nncf_trace_context + + +if is_mmdeploy_enabled(): + import mmdeploy.codebase.mmdet # noqa: F401 # pylint: disable=unused-import + from mmdeploy.core import FUNCTION_REWRITER + from mmdeploy.core.rewriters.rewriter_utils import import_function + + for fn_path, record_dicts in FUNCTION_REWRITER._registry._rewrite_records.items(): + if fn_path.startswith("torch"): + continue + obj, obj_cls = import_function(fn_path) + fn_name = fn_path.split(".")[-1] + if _should_wrap(obj_cls, fn_name, HEADS_TARGETS): + for record_dict in record_dicts: + record_dict["_object"] = partial(no_nncf_trace_wrapper, None, record_dict["_object"]) diff --git a/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py b/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py new file mode 100644 index 00000000000..265e0d5ac36 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py @@ -0,0 +1,115 @@ +"""NNCF Task of OTX Detection.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from functools import partial +from typing import Optional + +import otx.algorithms.detection.adapters.mmdet.nncf.patches # noqa: F401 # pylint: disable=unused-import +from otx.algorithms.common.tasks.nncf_task import NNCFBaseTask +from otx.algorithms.detection.adapters.mmdet.nncf import build_nncf_detector +from otx.algorithms.detection.adapters.mmdet.task import MMDetectionTask +from otx.algorithms.detection.adapters.mmdet.utils.config_utils import ( + should_cluster_anchors, +) +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ModelEntity +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-ancestors +class DetectionNNCFTask(NNCFBaseTask, MMDetectionTask): + """DetectionNNCFTask.""" + + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__() + super(NNCFBaseTask, self).__init__(task_environment, output_path) + self._set_attributes_by_hyperparams() + + def configure( + self, + training=True, + ir_options=None, + train_dataset=None, + export=False, + ): + """Configure configs for nncf task.""" + super(NNCFBaseTask, self).configure(training, ir_options, train_dataset, export) + self._prepare_optimize(export) + return self._config + + def _prepare_optimize(self, export=False): + super()._prepare_optimize() + + self.model_builder = partial( + self.model_builder, + nncf_model_builder=build_nncf_detector, + return_compression_ctrl=False, + is_export=export, + ) + + def _optimize( + self, + dataset: DatasetEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + ): + results = self._train_model(dataset) + + return results + + def _optimize_post_hook( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + ): + # get prediction on validation set + val_dataset = dataset.get_subset(Subset.VALIDATION) + val_preds, val_map = self._infer_model(val_dataset, InferenceParameters(is_evaluation=True)) + + preds_val_dataset = val_dataset.with_empty_annotations() + self._add_predictions_to_dataset(val_preds, preds_val_dataset, 0.0) + + result_set = ResultSetEntity( + model=output_model, + ground_truth_dataset=val_dataset, + prediction_dataset=preds_val_dataset, + ) + + # adjust confidence threshold + if self._hyperparams.postprocessing.result_based_confidence_threshold: + best_confidence_threshold = None + logger.info("Adjusting the confidence threshold") + metric = MetricsHelper.compute_f_measure(result_set, vary_confidence_threshold=True) + if metric.best_confidence_threshold: + best_confidence_threshold = metric.best_confidence_threshold.value + if best_confidence_threshold is None: + raise ValueError("Cannot compute metrics: Invalid confidence threshold!") + logger.info(f"Setting confidence threshold to {best_confidence_threshold} based on results") + self.confidence_threshold = best_confidence_threshold + else: + metric = MetricsHelper.compute_f_measure(result_set, vary_confidence_threshold=False) + + performance = metric.get_performance() + logger.info(f"Final model performance: {str(performance)}") + performance.dashboard_metrics.extend( + # pylint: disable-next=protected-access + self._generate_training_metrics(self._learning_curves, val_map) + ) + output_model.performance = performance + + def _save_model_post_hook(self, modelinfo): + if self._recipe_cfg is not None and should_cluster_anchors(self._recipe_cfg): + modelinfo["anchors"] = {} + self._update_anchors(modelinfo["anchors"], self.config.model.bbox_head.anchor_generator) + + modelinfo["confidence_threshold"] = self.confidence_threshold + modelinfo["input_size"] = self._input_size diff --git a/src/otx/algorithms/detection/adapters/mmdet/task.py b/src/otx/algorithms/detection/adapters/mmdet/task.py new file mode 100644 index 00000000000..ebf33f616b3 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/task.py @@ -0,0 +1,720 @@ +"""Task of OTX Detection using mmdetection training backend.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import glob +import io +import os +import time +from contextlib import nullcontext +from copy import deepcopy +from functools import partial +from typing import Any, Dict, Optional, Union + +import torch +from mmcv.runner import wrap_fp16_model +from mmcv.utils import Config, ConfigDict, get_git_hash +from mmdet import __version__ +from mmdet.apis import single_gpu_test +from mmdet.datasets import build_dataloader, build_dataset, replace_ImageToTensor +from mmdet.models.detectors import DETR, TwoStageDetector +from mmdet.utils import collect_env + +from otx.algorithms.common.adapters.mmcv.hooks import LossDynamicsTrackingHook +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + ActivationMapHook, + BaseRecordingForwardHook, + EigenCamHook, + FeatureVectorHook, +) +from otx.algorithms.common.adapters.mmcv.utils import ( + adapt_batch_size, + build_data_parallel, +) +from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + OTXConfig, +) +from otx.algorithms.common.adapters.torch.utils import convert_sync_batchnorm +from otx.algorithms.common.configs.configuration_enums import BatchSizeAdaptType +from otx.algorithms.common.configs.training_base import TrainType +from otx.algorithms.common.tasks.nncf_task import NNCFBaseTask +from otx.algorithms.common.utils.data import get_dataset +from otx.algorithms.common.utils.utils import get_cfg_based_on_device +from otx.algorithms.detection.adapters.mmdet.apis.train import train_detector +from otx.algorithms.detection.adapters.mmdet.configurer import ( + DetectionConfigurer, + IncrDetectionConfigurer, + SemiSLDetectionConfigurer, +) +from otx.algorithms.detection.adapters.mmdet.datasets import ImageTilingDataset +from otx.algorithms.detection.adapters.mmdet.hooks.det_class_probability_map_hook import ( + DetClassProbabilityMapHook, + MaskRCNNRecordingForwardHook, +) +from otx.algorithms.detection.adapters.mmdet.utils import ( + patch_input_preprocessing, + patch_input_shape, + patch_ir_scale_factor, +) +from otx.algorithms.detection.adapters.mmdet.utils.builder import build_detector +from otx.algorithms.detection.adapters.mmdet.utils.config_utils import ( + should_cluster_anchors, +) +from otx.algorithms.detection.adapters.mmdet.utils.exporter import DetectionExporter +from otx.algorithms.detection.task import OTXDetectionTask +from otx.api.configuration import cfg_helper +from otx.api.configuration.helper.utils import ids_to_strings +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ( + ModelEntity, + ModelPrecision, +) +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.utils.logger import get_logger + +logger = get_logger() + +# TODO Remove unnecessary pylint disable +# pylint: disable=too-many-lines + + +class MMDetectionTask(OTXDetectionTask): + """Task class for OTX detection using mmdetection training backend.""" + + # pylint: disable=too-many-instance-attributes + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__(task_environment, output_path) + self._data_cfg: Optional[Config] = None + self._recipe_cfg: Optional[Config] = None + + def _init_task(self): # noqa + """Initialize task.""" + self._recipe_cfg = OTXConfig.fromfile(get_cfg_based_on_device(os.path.join(self._model_dir, "model.py"))) + self._recipe_cfg.domain = self._task_type.domain + self._config = self._recipe_cfg + + self.set_seed() + + # Loss dynamics tracking + if getattr(self._hyperparams.algo_backend, "enable_noisy_label_detection", False): + LossDynamicsTrackingHook.configure_recipe(self._recipe_cfg, self._output_path) + + logger.info("initialized.") + + def build_model( + self, + cfg: Config, + fp16: bool = False, + **kwargs, + ) -> torch.nn.Module: + """Build model from model_builder.""" + model_builder = getattr(self, "model_builder", build_detector) + model = model_builder(cfg, **kwargs) + if bool(fp16): + wrap_fp16_model(model) + return model + + # pylint: disable=too-many-arguments + def configure(self, training=True, ir_options=None, train_dataset=None, export=False): + """Patch mmcv configs for OTX detection settings.""" + + # deepcopy all configs to make sure + # changes under Configurer and below does not take an effect to OTX for clear distinction + recipe_cfg = deepcopy(self._recipe_cfg) + assert recipe_cfg is not None, "'recipe_cfg' is not initialized." + + if self._data_cfg is not None: + data_classes = [label.name for label in self._labels] + else: + data_classes = None + model_classes = [label.name for label in self._model_label_schema] + + recipe_cfg.work_dir = self._output_path + recipe_cfg.resume = self._resume + + if self._train_type == TrainType.Incremental: + configurer = IncrDetectionConfigurer( + "detection", + training, + export, + self.override_configs, + self.on_hook_initialized, + self._time_monitor, + self._learning_curves, + ) + elif self._train_type == TrainType.Semisupervised: + configurer = SemiSLDetectionConfigurer( + "detection", + training, + export, + self.override_configs, + self.on_hook_initialized, + self._time_monitor, + self._learning_curves, + ) + else: + configurer = DetectionConfigurer( + "detection", + training, + export, + self.override_configs, + self.on_hook_initialized, + self._time_monitor, + self._learning_curves, + ) + cfg = configurer.configure( + recipe_cfg, + self.data_pipeline_path, + self._hyperparams, + self._model_ckpt, + self._data_cfg, + ir_options, + data_classes, + model_classes, + self._input_size, + train_dataset=train_dataset, + max_num_detections=self.max_num_detections, + ) + if should_cluster_anchors(self._recipe_cfg): + if train_dataset is not None: + self._anchors = cfg.model.bbox_head.anchor_generator + elif self._anchors is not None: + self._update_anchors(cfg.model.bbox_head.anchor_generator, self._anchors) + self._config = cfg + self._input_size = cfg.model.pop("input_size", None) + + return cfg + + # pylint: disable=too-many-branches, too-many-statements + def _train_model( + self, + dataset: DatasetEntity, + ): + """Train function in MMDetectionTask.""" + logger.info("init data cfg.") + self._data_cfg = ConfigDict(data=ConfigDict()) + + for cfg_key, subset in zip( + ["train", "val", "unlabeled"], + [Subset.TRAINING, Subset.VALIDATION, Subset.UNLABELED], + ): + subset = get_dataset(dataset, subset) + if subset and self._data_cfg is not None: + self._data_cfg.data[cfg_key] = ConfigDict( + otx_dataset=subset, + labels=self._labels, + ) + + self._is_training = True + + self._init_task() + + cfg = self.configure(True, None, get_dataset(dataset, Subset.TRAINING)) + logger.info("train!") + + timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) + + # Environment + logger.info(f"cfg.gpu_ids = {cfg.gpu_ids}, distributed = {cfg.distributed}") + env_info_dict = collect_env() + env_info = "\n".join([(f"{k}: {v}") for k, v in env_info_dict.items()]) + dash_line = "-" * 60 + "\n" + logger.info(f"Environment info:\n{dash_line}{env_info}\n{dash_line}") + + # Data + datasets = [build_dataset(cfg.data.train)] + + # Target classes + if "task_adapt" in cfg: + target_classes = cfg.task_adapt.get("final", []) + else: + target_classes = datasets[0].CLASSES + + # Metadata + meta = dict() + meta["env_info"] = env_info + # meta['config'] = cfg.pretty_text + meta["seed"] = cfg.seed + meta["exp_name"] = cfg.work_dir + if cfg.checkpoint_config is not None: + cfg.checkpoint_config.meta = dict( + mmdet_version=__version__ + get_git_hash()[:7], + CLASSES=target_classes, + ) + + # Model + model = self.build_model(cfg, fp16=cfg.get("fp16", False)) + model.train() + model.CLASSES = target_classes + + if cfg.distributed: + convert_sync_batchnorm(model) + + validate = bool(cfg.data.get("val", None)) + + if self._hyperparams.learning_parameters.auto_adapt_batch_size != BatchSizeAdaptType.NONE: + train_func = partial(train_detector, meta=deepcopy(meta), model=deepcopy(model), distributed=False) + adapt_batch_size( + train_func, + cfg, + datasets, + isinstance(self, NNCFBaseTask), # nncf needs eval hooks + not_increase=(self._hyperparams.learning_parameters.auto_adapt_batch_size == BatchSizeAdaptType.SAFE), + ) + + train_detector( + model, + datasets, + cfg, + distributed=cfg.distributed, + validate=validate, + timestamp=timestamp, + meta=meta, + ) + + # Save outputs + output_ckpt_path = os.path.join(cfg.work_dir, "latest.pth") + best_ckpt_path = glob.glob(os.path.join(cfg.work_dir, "best_*.pth")) + if len(best_ckpt_path) > 0: + output_ckpt_path = best_ckpt_path[0] + return dict( + final_ckpt=output_ckpt_path, + ) + + def _infer_model( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ): + """Main infer function.""" + original_subset = dataset[0].subset + for item in dataset: + item.subset = Subset.TESTING + self._data_cfg = ConfigDict( + data=ConfigDict( + train=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + test=ConfigDict( + otx_dataset=dataset, + labels=self._labels, + ), + ) + ) + + dump_features = True + dump_saliency_map = not inference_parameters.is_evaluation if inference_parameters else True + if isinstance(self, NNCFBaseTask): + dump_saliency_map = False + + self._init_task() + + cfg = self.configure(False, None) + logger.info("infer!") + + # Data loader + mm_dataset = build_dataset(cfg.data.test) + samples_per_gpu = cfg.data.test_dataloader.get("samples_per_gpu", 1) + # If the batch size and the number of data are not divisible, the metric may score differently. + # To avoid this, use 1 if they are not divisible. + samples_per_gpu = samples_per_gpu if len(mm_dataset) % samples_per_gpu == 0 else 1 + dataloader = build_dataloader( + mm_dataset, + samples_per_gpu=samples_per_gpu, + workers_per_gpu=cfg.data.test_dataloader.get("workers_per_gpu", 0), + num_gpus=len(cfg.gpu_ids), + dist=cfg.distributed, + seed=cfg.get("seed", None), + shuffle=False, + ) + + # Target classes + if "task_adapt" in cfg: + target_classes = cfg.task_adapt.final + if len(target_classes) < 1: + raise KeyError( + f"target_classes={target_classes} is empty check the metadata from model ckpt or recipe " + "configuration" + ) + else: + target_classes = mm_dataset.CLASSES + + # Model + model = self.build_model(cfg, fp16=cfg.get("fp16", False)) + model.CLASSES = target_classes + model.eval() + feature_model = model.model_t if self._train_type == TrainType.Semisupervised else model + model = build_data_parallel(model, cfg, distributed=False) + + # InferenceProgressCallback (Time Monitor enable into Infer task) + time_monitor = None + if cfg.get("custom_hooks", None): + time_monitor = [hook.time_monitor for hook in cfg.custom_hooks if hook.type == "OTXProgressHook"] + time_monitor = time_monitor[0] if time_monitor else None + if time_monitor is not None: + # pylint: disable=unused-argument + def pre_hook(module, inp): + time_monitor.on_test_batch_begin(None, None) + + def hook(module, inp, outp): + time_monitor.on_test_batch_end(None, None) + + model.register_forward_pre_hook(pre_hook) + model.register_forward_hook(hook) + + # Check and unwrap ImageTilingDataset object from TaskAdaptEvalDataset + while hasattr(mm_dataset, "dataset") and not isinstance(mm_dataset, ImageTilingDataset): + mm_dataset = mm_dataset.dataset + + # Class-wise Saliency map for Single-Stage Detector, otherwise use class-ignore saliency map. + if not dump_saliency_map: + saliency_hook: Union[nullcontext, BaseRecordingForwardHook] = nullcontext() + else: + raw_model = feature_model + if isinstance(raw_model, TwoStageDetector): + height, width, _ = mm_dataset[0]["img_metas"][0].data["img_shape"] + saliency_hook = MaskRCNNRecordingForwardHook( + feature_model, + input_img_shape=(height, width), + normalize=not isinstance(mm_dataset, ImageTilingDataset), + ) + elif isinstance(raw_model, DETR): + saliency_hook = ActivationMapHook(feature_model) + else: + saliency_hook = DetClassProbabilityMapHook( + feature_model, + use_cls_softmax=not isinstance(mm_dataset, ImageTilingDataset), + normalize=not isinstance(mm_dataset, ImageTilingDataset), + ) + + if not dump_features: + feature_vector_hook: Union[nullcontext, BaseRecordingForwardHook] = nullcontext() + else: + feature_vector_hook = FeatureVectorHook(feature_model) + + eval_predictions = [] + # pylint: disable=no-member + with feature_vector_hook: + with saliency_hook: + eval_predictions = single_gpu_test(model, dataloader) + if isinstance(feature_vector_hook, nullcontext): + feature_vectors = [None] * len(mm_dataset) + else: + feature_vectors = feature_vector_hook.records + if isinstance(saliency_hook, nullcontext): + saliency_maps = [None] * len(mm_dataset) + else: + saliency_maps = saliency_hook.records + + for key in ["interval", "tmpdir", "start", "gpu_collect", "save_best", "rule", "dynamic_intervals"]: + cfg.evaluation.pop(key, None) + + if isinstance(mm_dataset, ImageTilingDataset): + eval_predictions = mm_dataset.merge(eval_predictions) + # average tile feature vertors for each image + feature_vectors = mm_dataset.merge_vectors(feature_vectors, dump_features) + saliency_maps = mm_dataset.merge_maps(saliency_maps, dump_saliency_map) + + metric = None + if inference_parameters and inference_parameters.is_evaluation: + if isinstance(mm_dataset, ImageTilingDataset): + metric = mm_dataset.dataset.evaluate(eval_predictions, **cfg.evaluation) + else: + metric = mm_dataset.evaluate(eval_predictions, **cfg.evaluation) + metric = metric["mAP"] if isinstance(cfg.evaluation.metric, list) else metric[cfg.evaluation.metric] + + assert len(eval_predictions) == len(feature_vectors) == len(saliency_maps), ( + "Number of elements should be the same, however, number of outputs are " + f"{len(eval_predictions)}, {len(feature_vectors)}, and {len(saliency_maps)}" + ) + results = dict( + outputs=dict( + classes=target_classes, + detections=eval_predictions, + metric=metric, + feature_vectors=feature_vectors, + saliency_maps=saliency_maps, + ) + ) + + # TODO: InferenceProgressCallback register + output = results["outputs"] + metric = output["metric"] + predictions = output["detections"] + assert len(output["detections"]) == len(output["feature_vectors"]) == len(output["saliency_maps"]), ( + "Number of elements should be the same, however, number of outputs are " + f"{len(output['detections'])}, {len(output['feature_vectors'])}, and {len(output['saliency_maps'])}" + ) + prediction_results = zip(predictions, output["feature_vectors"], output["saliency_maps"]) + # FIXME. This is temporary solution. + # All task(e.g. classification, segmentation) should change item's type to Subset.TESTING + # when the phase is inference. + for item in dataset: + item.subset = original_subset + return prediction_results, metric + + # pylint: disable=too-many-statements + def _export_model( + self, + precision: ModelPrecision, + export_format: ExportType, + dump_features: bool, + ): + """Main export function of OTX MMDetection Task.""" + self._data_cfg = ConfigDict( + data=ConfigDict( + train=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + test=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + ) + ) + self._init_task() + + cfg = self.configure(False, None, export=True) + + self._precision[0] = precision + export_options: Dict[str, Any] = {} + export_options["deploy_cfg"] = self._init_deploy_cfg(cfg) + assert len(self._precision) == 1 + export_options["precision"] = str(self._precision[0]) + export_options["type"] = str(export_format) + if self.max_num_detections > 0: + logger.info(f"Export max_num_detections: {self.max_num_detections}") + post_proc_cfg = export_options["deploy_cfg"]["codebase_config"]["post_processing"] + post_proc_cfg["max_output_boxes_per_class"] = self.max_num_detections + post_proc_cfg["keep_top_k"] = self.max_num_detections + post_proc_cfg["pre_top_k"] = self.max_num_detections * 10 + + export_options["deploy_cfg"]["dump_features"] = dump_features + if dump_features: + output_names = export_options["deploy_cfg"]["ir_config"]["output_names"] + if "feature_vector" not in output_names: + output_names.append("feature_vector") + if export_options["deploy_cfg"]["codebase_config"]["task"] != "Segmentation": + if "saliency_map" not in output_names: + output_names.append("saliency_map") + # disable softmax and normalization to merge saliency map for tiles and postprocess them altogether + tiling_detection = "tile_cfg" in cfg + export_options["deploy_cfg"]["softmax_saliency_maps"] = not tiling_detection + export_options["deploy_cfg"]["normalize_saliency_maps"] = not tiling_detection + + export_options["model_builder"] = getattr(self, "model_builder", build_detector) + + if self._precision[0] == ModelPrecision.FP16: + export_options["deploy_cfg"]["backend_config"]["mo_options"]["flags"].append("--compress_to_fp16") + + backend_cfg_backup = {} + if export_format == ExportType.ONNX: + backend_cfg_backup = export_options["deploy_cfg"]["backend_config"] + export_options["deploy_cfg"]["backend_config"] = {"type": "onnxruntime"} + export_options["deploy_cfg"]["ir_config"]["dynamic_axes"]["image"] = {0: "batch"} + + exporter = DetectionExporter() + results = exporter.run( + cfg, + **export_options, + ) + + if export_format == ExportType.ONNX: + results["inference_parameters"] = {} + results["inference_parameters"]["mean_values"] = " ".join( + map(str, backend_cfg_backup["mo_options"]["args"]["--mean_values"]) + ) + results["inference_parameters"]["scale_values"] = " ".join( + map(str, backend_cfg_backup["mo_options"]["args"]["--scale_values"]) + ) + + return results + + def _explain_model( + self, + dataset: DatasetEntity, + explain_parameters: Optional[ExplainParameters] = None, + ) -> Dict[str, Any]: + """Main explain function of MMDetectionTask.""" + for item in dataset: + item.subset = Subset.TESTING + + self._data_cfg = ConfigDict( + data=ConfigDict( + train=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + test=ConfigDict( + otx_dataset=dataset, + labels=self._labels, + ), + ) + ) + + self._init_task() + + cfg = self.configure(False, None) + + samples_per_gpu = cfg.data.test_dataloader.get("samples_per_gpu", 1) + if samples_per_gpu > 1: + # Replace 'ImageToTensor' to 'DefaultFormatBundle' + cfg.data.test.pipeline = replace_ImageToTensor(cfg.data.test.pipeline) + + # Data loader + mm_dataset = build_dataset(cfg.data.test) + dataloader = build_dataloader( + mm_dataset, + samples_per_gpu=cfg.data.get("samples_per_gpu", 1), + workers_per_gpu=cfg.data.get("workers_per_gpu", 0), + num_gpus=len(cfg.gpu_ids), + dist=cfg.distributed, + seed=cfg.get("seed", None), + shuffle=False, + ) + + # Target classes + if "task_adapt" in cfg: + target_classes = cfg.task_adapt.final + if len(target_classes) < 1: + raise KeyError( + f"target_classes={target_classes} is empty check the metadata from model ckpt or recipe " + "configuration" + ) + else: + target_classes = mm_dataset.CLASSES + + # TODO: Check Inference FP16 Support + model = self.build_model(cfg, fp16=cfg.get("fp16", False)) + model.CLASSES = target_classes + model.eval() + feature_model = model.model_t if self._train_type == TrainType.Semisupervised else model + model = build_data_parallel(model, cfg, distributed=False) + + # InferenceProgressCallback (Time Monitor enable into Infer task) + time_monitor = None + if cfg.get("custom_hooks", None): + time_monitor = [hook.time_monitor for hook in cfg.custom_hooks if hook.type == "OTXProgressHook"] + time_monitor = time_monitor[0] if time_monitor else None + if time_monitor is not None: + # pylint: disable=unused-argument + def pre_hook(module, inp): + time_monitor.on_test_batch_begin(None, None) + + def hook(module, inp, outp): + time_monitor.on_test_batch_end(None, None) + + model.register_forward_pre_hook(pre_hook) + model.register_forward_hook(hook) + + # Check and unwrap ImageTilingDataset object from TaskAdaptEvalDataset + while hasattr(mm_dataset, "dataset") and not isinstance(mm_dataset, ImageTilingDataset): + mm_dataset = mm_dataset.dataset + + per_class_xai_algorithm: Union[partial[MaskRCNNRecordingForwardHook], partial[DetClassProbabilityMapHook]] + if isinstance(feature_model, TwoStageDetector): + height, width, _ = mm_dataset[0]["img_metas"][0].data["img_shape"] + per_class_xai_algorithm = partial( + MaskRCNNRecordingForwardHook, input_img_shape=(width, height), normalize=True + ) + else: + per_class_xai_algorithm = partial( + DetClassProbabilityMapHook, + use_cls_softmax=not isinstance(mm_dataset, ImageTilingDataset), + normalize=not isinstance(mm_dataset, ImageTilingDataset), + ) + + explainer_hook_selector = { + "classwisesaliencymap": per_class_xai_algorithm, + "eigencam": EigenCamHook, + "activationmap": ActivationMapHook, + } + + explainer = explain_parameters.explainer if explain_parameters else None + if explainer is not None: + explainer_hook = explainer_hook_selector.get(explainer.lower(), None) + else: + explainer_hook = None + if explainer_hook is None: + raise NotImplementedError(f"Explainer algorithm {explainer} not supported!") + logger.info(f"Explainer algorithm: {explainer}") + + eval_predictions = [] + with explainer_hook(feature_model) as saliency_hook: # type: ignore + for data in dataloader: + with torch.no_grad(): + result = model(return_loss=False, rescale=True, **data) + eval_predictions.extend(result) + saliency_maps = saliency_hook.records + + # In the tiling case, merge saliency map from each tile into united map for image + if isinstance(mm_dataset, ImageTilingDataset): + saliency_maps = mm_dataset.merge_maps(saliency_maps, dump_maps=True) + + outputs = dict(detections=eval_predictions, saliency_maps=saliency_maps) + return outputs + + # This should be removed + def update_override_configurations(self, config): + """Update override_configs.""" + logger.info(f"update override config with: {config}") + config = ConfigDict(**config) + self.override_configs.update(config) + + # This should moved somewhere + def _init_deploy_cfg(self, cfg) -> Union[Config, None]: + base_dir = os.path.abspath(os.path.dirname(self._task_environment.model_template.model_template_path)) + if self._hyperparams.tiling_parameters.enable_tile_classifier: + deploy_cfg_path = os.path.join(base_dir, "deployment_tile_classifier.py") + else: + deploy_cfg_path = os.path.join(base_dir, "deployment.py") + deploy_cfg = None + if os.path.exists(deploy_cfg_path): + deploy_cfg = OTXConfig.fromfile(deploy_cfg_path) + + patch_input_preprocessing(cfg, deploy_cfg) + patch_input_shape(cfg, deploy_cfg) + patch_ir_scale_factor(deploy_cfg, self._hyperparams) + + return deploy_cfg + + def save_model(self, output_model: ModelEntity): + """Save best model weights in DetectionTrainTask.""" + logger.info("called save_model") + buffer = io.BytesIO() + hyperparams_str = ids_to_strings(cfg_helper.convert(self._hyperparams, dict, enum_to_str=True)) + labels = {label.name: label.color.rgb_tuple for label in self._labels} + model_ckpt = torch.load(self._model_ckpt) + modelinfo = { + "model": model_ckpt, + "config": hyperparams_str, + "labels": labels, + "confidence_threshold": self.confidence_threshold, + "input_size": self._input_size, + "VERSION": 1, + } + if self.config is not None and should_cluster_anchors(self.config): + modelinfo["anchors"] = {} + self._update_anchors(modelinfo["anchors"], self.config.model.bbox_head.anchor_generator) + + torch.save(modelinfo, buffer) + output_model.set_data("weights.pth", buffer.getvalue()) + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self._task_environment.label_schema), + ) + output_model.precision = self._precision + + @staticmethod + def _update_anchors(origin, new): + logger.info("Updating anchors") + origin["heights"] = new["heights"] + origin["widths"] = new["widths"] diff --git a/src/otx/algorithms/detection/adapters/mmdet/utils/__init__.py b/src/otx/algorithms/detection/adapters/mmdet/utils/__init__.py new file mode 100644 index 00000000000..f7e7bfc289e --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/utils/__init__.py @@ -0,0 +1,28 @@ +"""OTX Adapters - mmdet.utils.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .builder import build_detector +from .config_utils import ( + cluster_anchors, + monkey_patched_nms, + monkey_patched_roi_align, + patch_input_preprocessing, + patch_input_shape, + patch_ir_scale_factor, + patch_tiling, + should_cluster_anchors, +) + +__all__ = [ + "cluster_anchors", + "build_detector", + "patch_tiling", + "patch_input_preprocessing", + "patch_input_shape", + "patch_ir_scale_factor", + "should_cluster_anchors", + "monkey_patched_nms", + "monkey_patched_roi_align", +] diff --git a/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py b/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py new file mode 100644 index 00000000000..c2e1ee54db2 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py @@ -0,0 +1,50 @@ +"""MMdet model builder.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Optional, Union + +import torch +from mmcv.runner import load_checkpoint +from mmcv.utils import Config, ConfigDict, get_logger + +from otx.utils.logger import LEVEL + +mmdet_logger = get_logger("mmdet") + + +def build_detector( + config: Config, + train_cfg: Optional[Union[Config, ConfigDict]] = None, + test_cfg: Optional[Union[Config, ConfigDict]] = None, + checkpoint: Optional[str] = None, + device: Union[str, torch.device] = "cpu", + cfg_options: Optional[Union[Config, ConfigDict]] = None, + from_scratch: bool = False, +) -> torch.nn.Module: + """A builder function for mmdet model. + + Creates a model, based on the configuration in config. + Note that this function updates 'load_from' attribute of 'config'. + """ + + from mmdet.models import build_detector as origin_build_detector + + if cfg_options is not None: + config.merge_from_dict(cfg_options) + + model_cfg = deepcopy(config.model) + model = origin_build_detector(model_cfg, train_cfg=train_cfg, test_cfg=test_cfg) + mmdet_logger.setLevel("WARNING") # make logger less verbose temporally + model.init_weights() + mmdet_logger.setLevel(LEVEL) + model = model.to(device) + + checkpoint = checkpoint if checkpoint else config.pop("load_from", None) + if checkpoint is not None and not from_scratch: + load_checkpoint(model, checkpoint, map_location=device) + config.load_from = checkpoint + + return model diff --git a/src/otx/algorithms/detection/adapters/mmdet/utils/config_utils.py b/src/otx/algorithms/detection/adapters/mmdet/utils/config_utils.py new file mode 100644 index 00000000000..f0f7245fda2 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/utils/config_utils.py @@ -0,0 +1,297 @@ +"""Collection of utils for task implementation in Detection Task.""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import torch +from mmcv import Config, ConfigDict +from mmcv.utils import ext_loader +from torchvision.ops import nms as tv_nms +from torchvision.ops import roi_align as tv_roi_align + +from otx.algorithms.common.adapters.mmcv.utils import ( + InputSizeManager, + get_configs_by_pairs, +) +from otx.algorithms.detection.configs.base import DetectionConfig +from otx.algorithms.detection.utils.data import ( + adaptive_tile_params, + format_list_to_str, + get_anchor_boxes, + get_sizes_from_dataset_entity, +) +from otx.api.entities.datasets import DatasetEntity, DatasetPurpose +from otx.api.entities.subset import Subset +from otx.utils.logger import get_logger + +try: + from sklearn.cluster import KMeans + + __all__ = ["KMeans"] + + KMEANS_IMPORT = True +except ImportError: + KMEANS_IMPORT = False + + +logger = get_logger() +ext_module = ext_loader.load_ext("_ext", ["nms", "softnms", "nms_match", "nms_rotated", "nms_quadri"]) + + +def should_cluster_anchors(model_cfg: Config): + """Check whether cluster anchors or not.""" + if ( + hasattr(model_cfg.model, "bbox_head") + and hasattr(model_cfg.model.bbox_head, "anchor_generator") + and getattr( + model_cfg.model.bbox_head.anchor_generator, + "reclustering_anchors", + False, + ) + ): + return True + return False + + +def cluster_anchors(recipe_config: Config, dataset: DatasetEntity): + """Update configs for cluster_anchors.""" + if not KMEANS_IMPORT: + raise ImportError( + "Sklearn package is not installed. To enable anchor boxes clustering, please install " + "packages from requirements/optional.txt or just scikit-learn package." + ) + + logger.info("Collecting statistics from training dataset to cluster anchor boxes...") + [target_wh] = [ + transforms.img_scale + for transforms in recipe_config.data.test.pipeline + if transforms.type == "MultiScaleFlipAug" + ] + prev_generator = recipe_config.model.bbox_head.anchor_generator + group_as = [len(width) for width in prev_generator.widths] + wh_stats = get_sizes_from_dataset_entity(dataset, list(target_wh)) + + if len(wh_stats) < sum(group_as): + logger.warning( + f"There are not enough objects to cluster: {len(wh_stats)} were detected, while it should be " + f"at least {sum(group_as)}. Anchor box clustering was skipped." + ) + return + + widths, heights = get_anchor_boxes(wh_stats, group_as) + logger.info( + f"Anchor boxes widths have been updated from {format_list_to_str(prev_generator.widths)} " + f"to {format_list_to_str(widths)}" + ) + logger.info( + f"Anchor boxes heights have been updated from {format_list_to_str(prev_generator.heights)} " + f"to {format_list_to_str(heights)}" + ) + config_generator = recipe_config.model.bbox_head.anchor_generator + config_generator.widths, config_generator.heights = widths, heights + + recipe_config.model.bbox_head.anchor_generator = config_generator + + +def patch_tiling(config, hparams, dataset=None): + """Update config for tiling. + + Args: + config (dict): OTX config containing configuration settings. + hparams (DetectionConfig): DetectionConfig containing hyperparameters. + dataset (DatasetEntity, optional): A dataset entity. Defaults to None. + + Returns: + dict: The updated configuration dictionary. + """ + if hparams.tiling_parameters.enable_tiling: + logger.info("Tiling enabled") + + if dataset and dataset.purpose != DatasetPurpose.INFERENCE and hparams.tiling_parameters.enable_adaptive_params: + adaptive_tile_params(hparams.tiling_parameters, dataset.get_subset(Subset.TRAINING)) + + if hparams.tiling_parameters.enable_tile_classifier: + logger.info("Tile classifier enabled") + logger.info(f"Patch model from: {config.model.type} to CustomMaskRCNNTileOptimized") + config.model.type = "CustomMaskRCNNTileOptimized" + + for subset in ("val", "test"): + if "transforms" in config.data[subset].pipeline[0]: + transforms = config.data[subset].pipeline[0]["transforms"] + if transforms[-1]["type"] == "Collect": + transforms[-1]["keys"].append("full_res_image") + + if config.model.backbone.type == "efficientnet_b2b": + learning_rate = 0.002 + logger.info( + f"Patched {config.model.backbone.type} LR: " + f"{hparams.learning_parameters.learning_rate} -> {learning_rate}" + ) + hparams.learning_parameters.learning_rate = learning_rate + + config.data.train.filter_empty_gt = False + + config.data.train.sampling_ratio = hparams.tiling_parameters.tile_sampling_ratio + config.data.val.sampling_ratio = hparams.tiling_parameters.tile_sampling_ratio + if hparams.tiling_parameters.tile_sampling_ratio < 1.0: + config.custom_hooks.append(ConfigDict({"type": "TileSamplingHook"})) + + tiling_params = ConfigDict( + tile_size=int(hparams.tiling_parameters.tile_size), + overlap_ratio=float(hparams.tiling_parameters.tile_overlap), + max_per_img=int(hparams.tiling_parameters.tile_max_number), + ) + config.update( + ConfigDict( + data=ConfigDict( + train=tiling_params, + val=tiling_params, + test=tiling_params, + ) + ) + ) + config.update(dict(evaluation=dict(iou_thr=[0.5]))) + + return config + + +def patch_input_preprocessing(cfg: ConfigDict, deploy_cfg: ConfigDict): + """Update backend configuration with input preprocessing options. + + - If `"to_rgb"` in Normalize config is truthy, it adds `"--reverse_input_channels"` as a flag. + + The function then sets default values for the backend configuration in `deploy_cfg`. + + Args: + cfg (mmcv.ConfigDict): Config object containing test pipeline and other configurations. + deploy_cfg (mmcv.ConfigDict): DeployConfig object containing backend configuration. + + Returns: + None: This function updates the input `deploy_cfg` object directly. + """ + normalize_cfgs = get_configs_by_pairs(cfg.data.test.pipeline, dict(type="Normalize")) + assert len(normalize_cfgs) == 1 + normalize_cfg: dict = normalize_cfgs[0] + + # Set options based on Normalize config + options = { + "flags": ["--reverse_input_channels"] if normalize_cfg.get("to_rgb", False) else [], + "args": { + "--mean_values": list(normalize_cfg.get("mean", [])), + "--scale_values": list(normalize_cfg.get("std", [])), + }, + } + + # Set default backend configuration + mo_options = deploy_cfg.backend_config.get("mo_options", ConfigDict()) + mo_options = ConfigDict() if mo_options is None else mo_options + mo_options.args = mo_options.get("args", ConfigDict()) + mo_options.flags = mo_options.get("flags", []) + + # Override backend configuration with options from Normalize config + mo_options.args.update(options["args"]) + mo_options.flags = list(set(mo_options.flags + options["flags"])) + + deploy_cfg.backend_config.mo_options = mo_options + + +def patch_input_shape(cfg: ConfigDict, deploy_cfg: ConfigDict): + """Update backend configuration with input shape information. + + This function retrieves the input size from `cfg.data.test.pipeline`, + then sets the input shape for the backend model in `deploy_cfg` + + ``` + { + "opt_shapes": { + "input": [1, 3, *size] + } + } + ``` + + Args: + cfg (Config): Config object containing test pipeline and other configurations. + deploy_cfg (DeployConfig): DeployConfig object containing backend configuration. + + Returns: + None: This function updates the input `deploy_cfg` object directly. + """ + input_size_manager = InputSizeManager(cfg) + size = input_size_manager.get_input_size_from_cfg("test") + + assert all(isinstance(i, int) and i > 0 for i in size) + # default is static shape to prevent an unexpected error + # when converting to OpenVINO IR + w, h = size + logger.info(f"Patching OpenVINO IR input shape: {size}") + deploy_cfg.ir_config.input_shape = (w, h) + deploy_cfg.backend_config.model_inputs = [ConfigDict(opt_shapes=ConfigDict(input=[-1, 3, h, w]))] + + +def patch_ir_scale_factor(deploy_cfg: ConfigDict, hyper_parameters: DetectionConfig): + """Patch IR scale factor inplace from hyper parameters to deploy config. + + Args: + deploy_cfg (ConfigDict): mmcv deploy config + hyper_parameters (DetectionConfig): OTX detection hyper parameters + """ + + if hyper_parameters.tiling_parameters.enable_tiling: + scale_ir_input = deploy_cfg.get("scale_ir_input", False) + if scale_ir_input: + tile_ir_scale_factor = hyper_parameters.tiling_parameters.tile_ir_scale_factor + logger.info(f"Apply OpenVINO IR scale factor: {tile_ir_scale_factor}") + ir_input_shape = deploy_cfg.backend_config.model_inputs[0].opt_shapes.input + ir_input_shape[2] = int(ir_input_shape[2] * tile_ir_scale_factor) # height + ir_input_shape[3] = int(ir_input_shape[3] * tile_ir_scale_factor) # width + deploy_cfg.ir_config.input_shape = (ir_input_shape[3], ir_input_shape[2]) # width, height + deploy_cfg.backend_config.model_inputs = [ + ConfigDict(opt_shapes=ConfigDict(input=[1, 3, ir_input_shape[2], ir_input_shape[3]])) + ] + print(f"-----------------> x {tile_ir_scale_factor} = {ir_input_shape}") + + +def monkey_patched_nms(ctx, bboxes, scores, iou_threshold, offset, score_threshold, max_num): + """Runs MMCVs NMS with torchvision.nms, or forces NMS from MMCV to run on CPU.""" + is_filtering_by_score = score_threshold > 0 + if is_filtering_by_score: + valid_mask = scores > score_threshold + bboxes, scores = bboxes[valid_mask], scores[valid_mask] + valid_inds = torch.nonzero(valid_mask, as_tuple=False).squeeze(dim=1) + + if bboxes.dtype == torch.bfloat16: + bboxes = bboxes.to(torch.float32) + if scores.dtype == torch.bfloat16: + scores = scores.to(torch.float32) + + if offset == 0: + inds = tv_nms(bboxes, scores, float(iou_threshold)) + else: + device = bboxes.device + bboxes = bboxes.to("cpu") + scores = scores.to("cpu") + inds = ext_module.nms(bboxes, scores, iou_threshold=float(iou_threshold), offset=offset) + bboxes = bboxes.to(device) + scores = scores.to(device) + + if max_num > 0: + inds = inds[:max_num] + if is_filtering_by_score: + inds = valid_inds[inds] + return inds + + +def monkey_patched_roi_align(self, input, rois): + """Replaces MMCVs roi align with the one from torchvision. + + Args: + self: patched instance + input: NCHW images + rois: Bx5 boxes. First column is the index into N. The other 4 columns are xyxy. + """ + + if "aligned" in tv_roi_align.__code__.co_varnames: + return tv_roi_align(input, rois, self.output_size, self.spatial_scale, self.sampling_ratio, self.aligned) + else: + if self.aligned: + rois -= rois.new_tensor([0.0] + [0.5 / self.spatial_scale] * 4) + return tv_roi_align(input, rois, self.output_size, self.spatial_scale, self.sampling_ratio) diff --git a/src/otx/algorithms/detection/adapters/mmdet/utils/exporter.py b/src/otx/algorithms/detection/adapters/mmdet/utils/exporter.py new file mode 100644 index 00000000000..a2c4b71c68c --- /dev/null +++ b/src/otx/algorithms/detection/adapters/mmdet/utils/exporter.py @@ -0,0 +1,72 @@ +"""Exporter for OTX Detection task with MMDETECTION training backend.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np + +from otx.algorithms.common.adapters.mmcv.tasks.exporter import Exporter +from otx.algorithms.common.adapters.mmdeploy.utils.utils import ( + sync_batchnorm_2_batchnorm, +) +from otx.algorithms.detection.adapters.mmdet.utils.builder import build_detector +from otx.utils.logger import get_logger + +logger = get_logger() + + +class DetectionExporter(Exporter): + """Exporter for OTX Detection using mmdetection training backend.""" + + def run(self, cfg, **kwargs): # noqa: C901 + """Run exporter stage.""" + + precision = kwargs.get("precision", "FP32") + model_builder = kwargs.get("model_builder", build_detector) + + def model_builder_helper(*args, **kwargs): + model = model_builder(*args, **kwargs) + # TODO: handle various input size + model = sync_batchnorm_2_batchnorm(model, 2) + + if precision == "INT8": + from nncf.torch.nncf_network import NNCFNetwork + + assert isinstance(model, NNCFNetwork) + + return model + + kwargs["model_builder"] = model_builder_helper + + return super().run(cfg, **kwargs) + + @staticmethod + def naive_export(output_dir, model_builder, precision, export_type, cfg, model_name="model"): + """Export using torch.onnx directly.""" + from mmdet.apis.inference import LoadImage + from mmdet.datasets.pipelines import Compose + + from otx.algorithms.common.adapters.mmdeploy.apis import NaiveExporter + + def get_fake_data(cfg, orig_img_shape=(128, 128, 3)): + pipeline = [LoadImage()] + cfg.data.test.pipeline[1:] + pipeline = Compose(pipeline) + data = dict(img=np.zeros(orig_img_shape, dtype=np.uint8)) + data = pipeline(data) + return data + + fake_data = get_fake_data(cfg) + opset_version = 11 + + NaiveExporter.export2backend( + output_dir, + model_builder, + cfg, + fake_data, + precision=precision, + model_name=model_name, + input_names=["image"], + output_names=["boxes", "labels"], + opset_version=opset_version, + export_type=export_type, + ) diff --git a/src/otx/algorithms/detection/adapters/openvino/__init__.py b/src/otx/algorithms/detection/adapters/openvino/__init__.py new file mode 100644 index 00000000000..da9b8319b9e --- /dev/null +++ b/src/otx/algorithms/detection/adapters/openvino/__init__.py @@ -0,0 +1,4 @@ +"""OTX Adapters - openvino.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/adapters/openvino/model_wrappers/__init__.py b/src/otx/algorithms/detection/adapters/openvino/model_wrappers/__init__.py new file mode 100644 index 00000000000..60da385d413 --- /dev/null +++ b/src/otx/algorithms/detection/adapters/openvino/model_wrappers/__init__.py @@ -0,0 +1,15 @@ +"""Model Wrapper Initialization of OTX Detection.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/adapters/openvino/task.py b/src/otx/algorithms/detection/adapters/openvino/task.py new file mode 100644 index 00000000000..21ea2fca7ba --- /dev/null +++ b/src/otx/algorithms/detection/adapters/openvino/task.py @@ -0,0 +1,760 @@ +"""Openvino Task of Detection.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import copy +import io +import json +import os +import tempfile +import time +import warnings +from typing import Any, Dict, List, Optional, Tuple, Union +from zipfile import ZipFile + +import attr +import nncf +import numpy as np +import openvino.runtime as ov +from addict import Dict as ADDict +from nncf.common.quantization.structs import QuantizationPreset +from openvino.model_api.adapters import OpenvinoAdapter, create_core +from openvino.model_api.models import ImageModel, Model +from openvino.model_api.tilers import DetectionTiler, InstanceSegmentationTiler + +from otx.algorithms.common.utils import OTXOpenVinoDataLoader +from otx.algorithms.common.utils.ir import check_if_quantized +from otx.algorithms.common.utils.utils import get_default_async_reqs_num +from otx.algorithms.detection.adapters.openvino import model_wrappers +from otx.algorithms.detection.configs.base import DetectionConfig +from otx.api.configuration.helper.utils import ( + config_to_bytes, + flatten_config_values, + flatten_detection_config_groups, + merge_a_into_b, +) +from otx.api.entities.annotation import AnnotationSceneEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import ( + InferenceParameters, + default_progress_callback, +) +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ( + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.model_template import TaskType +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.tensor import TensorEntity +from otx.api.serialization.label_mapper import LabelSchemaMapper, label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.exportable_code import demo +from otx.api.usecases.exportable_code.inference import IInferencer +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( + DetectionToAnnotationConverter, + IPredictionToAnnotationConverter, + MaskToAnnotationConverter, + RotatedRectToAnnotationConverter, +) +from otx.api.usecases.tasks.interfaces.deployment_interface import IDeploymentTask +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.optimization_interface import ( + IOptimizationTask, + OptimizationType, +) +from otx.api.utils.dataset_utils import add_saliency_maps_to_dataset_item +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-locals +class BaseInferencerWithConverter(IInferencer): + """BaseInferencerWithConverter class in OpenVINO task.""" + + def __init__( + self, + configuration: dict, + model: Model, + converter: IPredictionToAnnotationConverter, + ) -> None: + self.configuration = configuration + self.model = model + self.converter = converter + self.callback_exceptions: List[Exception] = [] + self.is_callback_set = False + + def pre_process(self, image: np.ndarray) -> Tuple[Dict[str, np.ndarray], Dict[str, Any]]: + """Pre-process function of OpenVINO Detection Inferencer.""" + return self.model.preprocess(image) + + def get_saliency_map(self, prediction: Any): + """Saliency map function of OpenVINO Detection Inferencer.""" + if isinstance(prediction.saliency_map, list): + return prediction.saliency_map + + if prediction.saliency_map.shape[0] == 1: + return prediction.saliency_map[0] + + return prediction.saliency_map + + def predict(self, image: np.ndarray): + """Predict function of OpenVINO Detection Inferencer.""" + image, metadata = self.pre_process(image) + raw_predictions = self.forward(image) + detections = self.model.postprocess(raw_predictions, metadata) + predictions = self.converter.convert_to_annotation(detections, metadata) + if "feature_vector" not in raw_predictions or "saliency_map" not in raw_predictions: + warnings.warn( + "Could not find Feature Vector and Saliency Map in OpenVINO output. " + "Please rerun OpenVINO export or retrain the model." + ) + features = (None, None) + else: + features = ( + detections.feature_vector.reshape(-1), + self.get_saliency_map(detections), + ) + return predictions, features + + def forward(self, image: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]: + """Forward function of OpenVINO Detection Inferencer.""" + return self.model.infer_sync(image) + + def _async_callback(self, request: Any, callback_args: tuple) -> None: + """Fetches the results of async inference.""" + try: + id, preprocessing_meta, result_handler = callback_args + prediction = self.model.inference_adapter.copy_raw_result(request) + detections = self.model.postprocess(prediction, preprocessing_meta) + processed_prediciton = self.converter.convert_to_annotation(detections, preprocessing_meta) + + if "feature_vector" not in prediction or "saliency_map" not in prediction: + warnings.warn( + "Could not find Feature Vector and Saliency Map in OpenVINO output. " + "Please rerun OpenVINO export or retrain the model." + ) + features = (None, None) + else: + features = ( + copy.deepcopy(detections.feature_vector.reshape(-1)), + self.get_saliency_map(detections), + ) + + result_handler(id, processed_prediciton, features) + + except Exception as e: + self.callback_exceptions.append(e) + + def enqueue_prediction(self, image: np.ndarray, id: int, result_handler: Any) -> None: + """Runs async inference.""" + if not self.is_callback_set: + self.model.inference_adapter.set_callback(self._async_callback) + self.is_callback_set = True + + if not self.model.is_ready(): + self.model.await_any() + image, metadata = self.pre_process(image) + callback_data = id, metadata, result_handler + self.model.inference_adapter.infer_async(image, callback_data) + + def await_all(self) -> None: + """Await all running infer requests if any.""" + self.model.await_all() + + +class OpenVINODetectionInferencer(BaseInferencerWithConverter): + """Inferencer implementation for OTXDetection using OpenVINO backend.""" + + def __init__( + self, + hparams: DetectionConfig, + label_schema: LabelSchemaEntity, + model_file: Union[str, bytes], + weight_file: Union[str, bytes, None] = None, + device: str = "CPU", + num_requests: int = 1, + model_configuration: Dict[str, Any] = {}, + ): + """Initialize for OpenVINODetectionInferencer. + + :param hparams: Hyper parameters that the model should use. + :param label_schema: LabelSchemaEntity that was used during model training. + :param model_file: Path OpenVINO IR model definition file. + :param weight_file: Path OpenVINO IR model weights file. + :param device: Device to run inference on, such as CPU, GPU or MYRIAD. Defaults to "CPU". + :param num_requests: Maximum number of requests that the inferencer can make. Defaults to 1. + """ + + model_adapter = OpenvinoAdapter( + create_core(), + model_file, + weight_file, + device=device, + max_num_requests=num_requests, + plugin_config={"PERFORMANCE_HINT": "THROUGHPUT"}, + ) + configuration = { + **attr.asdict( + hparams.postprocessing, + filter=lambda attr, _: attr.name not in ["header", "description", "type", "visible_in_ui"], + ) + } + configuration.update(model_configuration) + model = Model.create_model(model_adapter, "SSD", configuration, preload=True) + converter = DetectionToAnnotationConverter(label_schema, configuration) + + super().__init__(configuration, model, converter) + + +class OpenVINOMaskInferencer(BaseInferencerWithConverter): + """Mask Inferencer implementation for OTXDetection using OpenVINO backend.""" + + def __init__( + self, + hparams: DetectionConfig, + label_schema: LabelSchemaEntity, + model_file: Union[str, bytes], + weight_file: Union[str, bytes, None] = None, + device: str = "CPU", + num_requests: int = 1, + model_configuration: Dict[str, Any] = {}, + ): + model_adapter = OpenvinoAdapter( + create_core(), + model_file, + weight_file, + device=device, + max_num_requests=num_requests, + plugin_config={"PERFORMANCE_HINT": "THROUGHPUT"}, + ) + + configuration = { + **attr.asdict( + hparams.postprocessing, + filter=lambda attr, value: attr.name not in ["header", "description", "type", "visible_in_ui"], + ), + } + configuration.update(model_configuration) + + model = Model.create_model(model_adapter, "MaskRCNN", configuration, preload=True) + converter = MaskToAnnotationConverter(label_schema, configuration) + + super().__init__(configuration, model, converter) + + +class OpenVINORotatedRectInferencer(BaseInferencerWithConverter): + """Rotated Rect Inferencer implementation for OTXDetection using OpenVINO backend.""" + + def __init__( + self, + hparams: DetectionConfig, + label_schema: LabelSchemaEntity, + model_file: Union[str, bytes], + weight_file: Union[str, bytes, None] = None, + device: str = "CPU", + num_requests: int = 1, + ): + model_adapter = OpenvinoAdapter( + create_core(), + model_file, + weight_file, + device=device, + max_num_requests=num_requests, + plugin_config={"PERFORMANCE_HINT": "THROUGHPUT"}, + ) + + configuration = { + **attr.asdict( + hparams.postprocessing, + filter=lambda attr, value: attr.name not in ["header", "description", "type", "visible_in_ui"], + ) + } + + model = Model.create_model(model_adapter, "MaskRCNN", configuration, preload=True) + converter = RotatedRectToAnnotationConverter(label_schema, configuration) + + super().__init__(configuration, model, converter) + + +class OpenVINOTileClassifierWrapper(BaseInferencerWithConverter): + """Wrapper for OpenVINO Tiling. + + Args: + inferencer (BaseInferencerWithConverter): inferencer to wrap + tile_size (int): tile size + overlap (float): overlap ratio between tiles + max_number (int): maximum number of objects per image + tile_ir_scale_factor (float, optional): scale factor for tile size + tile_classifier_model_file (Union[str, bytes, None], optional): tile classifier xml. Defaults to None. + tile_classifier_weight_file (Union[str, bytes, None], optional): til classifier weight bin. Defaults to None. + device (str, optional): device to run inference on, such as CPU, GPU or MYRIAD. Defaults to "CPU". + num_requests (int, optional): number of request for OpenVINO adapter. Defaults to 1. + mode (str, optional): run inference in sync or async mode. Defaults to "async". + """ + + def __init__( + self, + inferencer: BaseInferencerWithConverter, + tile_size: int = 400, + overlap: float = 0.5, + max_number: int = 100, + tile_ir_scale_factor: float = 1.0, + tile_classifier_model_file: Union[str, bytes, None] = None, + tile_classifier_weight_file: Union[str, bytes, None] = None, + device: str = "CPU", + num_requests: int = 1, + mode: str = "async", + ): # pylint: disable=too-many-arguments + assert mode in ["async", "sync"], "mode should be async or sync" + classifier = None + if tile_classifier_model_file is not None or tile_classifier_weight_file is not None: + adapter = OpenvinoAdapter( + create_core(), + tile_classifier_model_file, + tile_classifier_weight_file, + device=device, + max_num_requests=num_requests, + ) + classifier = ImageModel(inference_adapter=adapter, configuration={}, preload=True) + + tiler_config = { + "tile_size": int(tile_size * tile_ir_scale_factor), + "tiles_overlap": overlap / tile_ir_scale_factor, + "max_pred_number": max_number, + } + + is_segm = isinstance(inferencer.converter, (MaskToAnnotationConverter, RotatedRectToAnnotationConverter)) + if is_segm: + self.tiler = InstanceSegmentationTiler( + inferencer.model, tiler_config, execution_mode=mode, tile_classifier_model=classifier + ) + else: + self.tiler = DetectionTiler(inferencer.model, tiler_config, execution_mode=mode) + + super().__init__(inferencer.configuration, inferencer.model, inferencer.converter) + + def predict(self, image: np.ndarray) -> Tuple[AnnotationSceneEntity, Tuple[np.ndarray, np.ndarray]]: + """Run prediction by tiling image to small patches. + + Args: + image (np.ndarray): input image + + Returns: + detections: AnnotationSceneEntity + features: list including feature vector and saliency map + """ + detections = self.tiler(image) + annotations = self.converter.convert_to_annotation(detections, metadata={"original_shape": image.shape}) + features = ( + detections.feature_vector.reshape(-1), + self.get_saliency_map(detections), + ) + + return annotations, features + + +class OpenVINODetectionTask(IDeploymentTask, IInferenceTask, IEvaluationTask, IOptimizationTask): + """Task implementation for OTXDetection using OpenVINO backend.""" + + def __init__(self, task_environment: TaskEnvironment): + logger.info("Loading OpenVINO OTXDetectionTask") + self.task_environment = task_environment + self.model = self.task_environment.model + self.task_type = self.task_environment.model_template.task_type + self.confidence_threshold: float = 0.0 + self.config = self.load_config() + self.inferencer = self.load_inferencer() + self._avg_time_per_image: Optional[float] = None + logger.info("OpenVINO task initialization completed") + + @property + def hparams(self): + """Hparams of OpenVINO Detection Task.""" + return self.task_environment.get_hyper_parameters(DetectionConfig) + + @property + def avg_time_per_image(self) -> Optional[float]: + """Average inference time per image.""" + return self._avg_time_per_image + + def load_config(self) -> ADDict: + """Load configurable parameters from model adapter. + + Returns: + ADDict: config dictionary + """ + config = vars(self.hparams) + flatten_detection_config_groups(config) + try: + if self.model is not None and self.model.get_data("config.json"): + json_dict = json.loads(self.model.get_data("config.json")) + flatten_config_values(json_dict) + # NOTE: for backward compatibility + json_dict["tiling_parameters"]["tile_ir_scale_factor"] = json_dict["tiling_parameters"].get( + "tile_ir_scale_factor", 1.0 + ) + config = merge_a_into_b(json_dict, config) + except Exception as e: # pylint: disable=broad-except + logger.warning(f"Failed to load config.json: {e}") + config = ADDict(config) + return config + + def load_inferencer( + self, + ) -> Union[ + OpenVINODetectionInferencer, + OpenVINOMaskInferencer, + OpenVINORotatedRectInferencer, + OpenVINOTileClassifierWrapper, + ]: + """load_inferencer function of OpenVINO Detection Task.""" + if self.model is None: + raise RuntimeError("load_inferencer failed, model is None") + _hparams = copy.deepcopy(self.hparams) + if _hparams.postprocessing.result_based_confidence_threshold: + self.confidence_threshold = float( + np.frombuffer(self.model.get_data("confidence_threshold"), dtype=np.float32)[0] + ) + _hparams.postprocessing.confidence_threshold = self.confidence_threshold + logger.info(f"Confidence Threshold: {_hparams.postprocessing.confidence_threshold}") + _hparams.postprocessing.use_ellipse_shapes = self.config.postprocessing.use_ellipse_shapes + async_requests_num = get_default_async_reqs_num() + args = [ + _hparams, + self.task_environment.label_schema, + self.model.get_data("openvino.xml"), + self.model.get_data("openvino.bin"), + "CPU", + async_requests_num, + ] + if self.task_type == TaskType.DETECTION: + if ( + "YOLOX" in self.task_environment.model_template.model_template_id + and not self.config.tiling_parameters.enable_tiling + ): + args.append({"resize_type": "fit_to_window_letterbox", "pad_value": 114}) + inferencer: BaseInferencerWithConverter = OpenVINODetectionInferencer(*args) + if self.task_type == TaskType.INSTANCE_SEGMENTATION or self.task_type == TaskType.ROTATED_DETECTION: + if not self.config.tiling_parameters.enable_tiling: + args.append({"resize_type": "standard"}) + else: + args.append({"resize_type": "fit_to_window_letterbox", "pad_value": 0}) + + if self.task_type == TaskType.INSTANCE_SEGMENTATION: + inferencer = OpenVINOMaskInferencer(*args) + else: + inferencer = OpenVINORotatedRectInferencer(*args) + + if self.config.tiling_parameters.enable_tiling: + logger.info("Tiling is enabled. Wrap inferencer with tile inference.") + tile_classifier_model_file, tile_classifier_weight_file = None, None + if self.config.tiling_parameters.enable_tile_classifier: + logger.info("Tile classifier is enabled. Load tile classifier model.") + tile_classifier_model_file = self.model.get_data("tile_classifier.xml") + tile_classifier_weight_file = self.model.get_data("tile_classifier.bin") + inferencer = OpenVINOTileClassifierWrapper( + inferencer, + self.config.tiling_parameters.tile_size, + self.config.tiling_parameters.tile_overlap, + self.config.tiling_parameters.tile_max_number, + self.config.tiling_parameters.tile_ir_scale_factor, + tile_classifier_model_file, + tile_classifier_weight_file, + ) + if not isinstance( + inferencer, + ( + OpenVINODetectionInferencer, + OpenVINOMaskInferencer, + OpenVINORotatedRectInferencer, + OpenVINOTileClassifierWrapper, + ), + ): + raise RuntimeError(f"Unknown OpenVINO Inferencer TaskType: {self.task_type}") + return inferencer + + def infer( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ) -> DatasetEntity: + """Infer function of OpenVINODetectionTask.""" + logger.info("Start OpenVINO inference") + + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress + add_saliency_map = not inference_parameters.is_evaluation + process_saliency_maps = inference_parameters.process_saliency_maps + explain_predicted_classes = inference_parameters.explain_predicted_classes + enable_async_inference = inference_parameters.enable_async_inference + else: + update_progress_callback = default_progress_callback + add_saliency_map = True + process_saliency_maps = False + explain_predicted_classes = True + enable_async_inference = True + + if self.config.tiling_parameters.enable_tiling: + enable_async_inference = False + + def add_prediction(id: int, predicted_scene: AnnotationSceneEntity, aux_data: tuple): + dataset_item = dataset[id] + dataset_item.append_annotations(predicted_scene.annotations) + feature_vector, saliency_map = aux_data + if feature_vector is not None: + representation_vector = TensorEntity(name="representation_vector", numpy=feature_vector.reshape(-1)) + dataset_item.append_metadata_item(representation_vector, model=self.model) + + if add_saliency_map and saliency_map is not None and len(saliency_map) > 0: + labels = self.task_environment.get_labels().copy() + if len(saliency_map) == len(labels) + 1: + # Include the background as the last category + labels.append(LabelEntity("background", Domain.DETECTION)) + + predicted_scored_labels: List = [] + for bbox in predicted_scene.annotations: + predicted_scored_labels += bbox.get_labels() + + add_saliency_maps_to_dataset_item( + dataset_item=dataset_item, + saliency_map=saliency_map, + model=self.model, + labels=labels, + predicted_scored_labels=predicted_scored_labels, + explain_predicted_classes=explain_predicted_classes, + process_saliency_maps=process_saliency_maps, + ) + + total_time = 0.0 + dataset_size = len(dataset) + for i, dataset_item in enumerate(dataset, 1): + start_time = time.perf_counter() + + if enable_async_inference: + self.inferencer.enqueue_prediction(dataset_item.numpy, i - 1, add_prediction) + else: + predicted_scene, features = self.inferencer.predict(dataset_item.numpy) + add_prediction(i - 1, predicted_scene, features) + + update_progress_callback(int(i / dataset_size * 100), None) + end_time = time.perf_counter() - start_time + logger.info(f"{end_time} secs") + total_time += end_time + + self.inferencer.await_all() + + self._avg_time_per_image = total_time / len(dataset) + logger.info(f"Avg time per image: {self._avg_time_per_image} secs") + logger.info(f"Total time: {total_time} secs") + logger.info("OpenVINO inference completed") + return dataset + + def explain( + self, + dataset: DatasetEntity, + explain_parameters: Optional[ExplainParameters] = None, + ) -> DatasetEntity: + """Explain function of OpenVINODetectionTask.""" + logger.info("Start OpenVINO explain") + + update_progress_callback = default_progress_callback + process_saliency_maps = False + explain_predicted_classes = True + if explain_parameters is not None: + update_progress_callback = explain_parameters.update_progress # type: ignore + process_saliency_maps = explain_parameters.process_saliency_maps + explain_predicted_classes = explain_parameters.explain_predicted_classes + + dataset_size = len(dataset) + for i, dataset_item in enumerate(dataset, 1): + predicted_scene, features = self.inferencer.predict(dataset_item.numpy) + dataset_item.append_annotations(predicted_scene.annotations) + update_progress_callback(int(i / dataset_size * 100), None) + _, saliency_map = features + if saliency_map is None: + raise RuntimeError( + "There is no Saliency Map in OpenVINO IR model output. " + "Please export model to OpenVINO IR with dump_features" + ) + + labels = self.task_environment.get_labels().copy() + if len(saliency_map) == len(labels) + 1: + # Include the background as the last category + labels.append(LabelEntity("background", Domain.DETECTION)) + + predicted_scored_labels: List = [] + for bbox in predicted_scene.annotations: + predicted_scored_labels += bbox.get_labels() + + add_saliency_maps_to_dataset_item( + dataset_item=dataset_item, + saliency_map=saliency_map, + model=self.model, + labels=labels, + predicted_scored_labels=predicted_scored_labels, + explain_predicted_classes=explain_predicted_classes, + process_saliency_maps=process_saliency_maps, + ) + logger.info("OpenVINO explain completed") + return dataset + + def evaluate( + self, + output_resultset: ResultSetEntity, + evaluation_metric: Optional[str] = None, + ): + """Evaluate function of OpenVINODetectionTask.""" + logger.info("Start OpenVINO metric evaluation") + if evaluation_metric is not None: + logger.warning( + f"Requested to use {evaluation_metric} metric, but parameter is ignored. Use F-measure instead." + ) + output_resultset.performance = MetricsHelper.compute_f_measure(output_resultset).get_performance() + logger.info(f"F-measure after evaluation: {output_resultset.performance}") + logger.info("OpenVINO metric evaluation completed") + + def deploy(self, output_model: ModelEntity) -> None: + """Deploy function of OpenVINODetectionTask.""" + logger.info("Deploying the model") + + work_dir = os.path.dirname(demo.__file__) + parameters = {} + parameters["type_of_model"] = self.inferencer.model.__model__ + parameters["converter_type"] = str(self.task_type) + parameters["model_parameters"] = self.inferencer.configuration + parameters["model_parameters"]["labels"] = LabelSchemaMapper.forward(self.task_environment.label_schema) + if self.config.tiling_parameters.get("type"): + self.config.tiling_parameters["type"] = str(self.config.tiling_parameters["type"]) + parameters["tiling_parameters"] = self.config.tiling_parameters + + zip_buffer = io.BytesIO() + with ZipFile(zip_buffer, "w") as arch: + # model files + if self.model is None: + raise ValueError("Deploy failed, model is None") + arch.writestr(os.path.join("model", "model.xml"), self.model.get_data("openvino.xml")) + arch.writestr(os.path.join("model", "model.bin"), self.model.get_data("openvino.bin")) + if self.config.tiling_parameters.enable_tiling and self.config.tiling_parameters.enable_tile_classifier: + arch.writestr(os.path.join("model", "tile_classifier.xml"), self.model.get_data("tile_classifier.xml")) + arch.writestr(os.path.join("model", "tile_classifier.bin"), self.model.get_data("tile_classifier.bin")) + arch.writestr( + os.path.join("model", "config.json"), + json.dumps(parameters, ensure_ascii=False, indent=4), + ) + # model_wrappers files + for root, _, files in os.walk(os.path.dirname(model_wrappers.__file__)): + if "__pycache__" in root: + continue + for file in files: + file_path = os.path.join(root, file) + arch.write( + file_path, + os.path.join( + "python", + "model_wrappers", + file_path.split("model_wrappers/")[1], + ), + ) + # python files + arch.write( + os.path.join(work_dir, "requirements.txt"), + os.path.join("python", "requirements.txt"), + ) + arch.write(os.path.join(work_dir, "LICENSE"), os.path.join("python", "LICENSE")) + arch.write(os.path.join(work_dir, "demo.py"), os.path.join("python", "demo.py")) + arch.write(os.path.join(work_dir, "README.md"), os.path.join(".", "README.md")) + output_model.exportable_code = zip_buffer.getvalue() + logger.info("Deploying completed") + + def optimize( + self, + optimization_type: OptimizationType, + dataset: DatasetEntity, + output_model: ModelEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + ): + """Optimize function of OpenVINODetectionTask.""" + logger.info("Start PTQ optimization") + + if optimization_type is not OptimizationType.POT: + raise ValueError("PTQ is the only supported optimization type for OpenVino models") + if self.model is None: + raise RuntimeError("Optimize failed, model is None") + + dataset = dataset.get_combined_subset([Subset.TRAINING, Subset.UNLABELED]) + data_loader = OTXOpenVinoDataLoader(dataset, self.inferencer) + + quantization_dataset = nncf.Dataset(data_loader, lambda data: data[0]) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, "model.xml") + bin_path = os.path.join(tempdir, "model.bin") + with open(xml_path, "wb") as f: + f.write(self.model.get_data("openvino.xml")) + with open(bin_path, "wb") as f: + f.write(self.model.get_data("openvino.bin")) + + ov_model = ov.Core().read_model(xml_path) + if check_if_quantized(ov_model): + raise RuntimeError("Model is already optimized by PTQ") + + if optimization_parameters: + optimization_parameters.update_progress(10, None) + + stat_subset_size = self.hparams.pot_parameters.stat_subset_size + preset = QuantizationPreset(self.hparams.pot_parameters.preset.name.lower()) + + compressed_model = nncf.quantize( + ov_model, quantization_dataset, subset_size=min(stat_subset_size, len(data_loader)), preset=preset + ) + + if optimization_parameters: + optimization_parameters.update_progress(90, None) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, "model.xml") + ov.save_model(compressed_model, xml_path) + with open(xml_path, "rb") as f: + output_model.set_data("openvino.xml", f.read()) + with open(os.path.join(tempdir, "model.bin"), "rb") as f: + output_model.set_data("openvino.bin", f.read()) + output_model.set_data( + "confidence_threshold", + np.array([self.confidence_threshold], dtype=np.float32).tobytes(), + ) + + # tile classifier is bypassed PTQ for now + if self.config.tiling_parameters.enable_tiling and self.config.tiling_parameters.enable_tile_classifier: + output_model.set_data("tile_classifier.xml", self.model.get_data("tile_classifier.xml")) + output_model.set_data("tile_classifier.bin", self.model.get_data("tile_classifier.bin")) + + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self.task_environment.label_schema), + ) + output_model.set_data("config.json", config_to_bytes(self.hparams)) + + # set model attributes for quantized model + output_model.model_format = ModelFormat.OPENVINO + output_model.optimization_type = ModelOptimizationType.POT + output_model.optimization_methods = [OptimizationMethod.QUANTIZATION] + output_model.precision = [ModelPrecision.INT8] + + self.model = output_model + self.inferencer = self.load_inferencer() + logger.info("PTQ optimization completed") + + if optimization_parameters: + optimization_parameters.update_progress(100, None) diff --git a/src/otx/algorithms/detection/configs/__init__.py b/src/otx/algorithms/detection/configs/__init__.py new file mode 100644 index 00000000000..994ac00bb8b --- /dev/null +++ b/src/otx/algorithms/detection/configs/__init__.py @@ -0,0 +1,15 @@ +"""Model configurations.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/base/__init__.py b/src/otx/algorithms/detection/configs/base/__init__.py new file mode 100644 index 00000000000..c47810d0e6c --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/__init__.py @@ -0,0 +1,19 @@ +"""Configs Initialization of OTX Detection.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import DetectionConfig + +__all__ = ["DetectionConfig"] diff --git a/src/otx/algorithms/detection/configs/base/configuration.py b/src/otx/algorithms/detection/configs/base/configuration.py new file mode 100644 index 00000000000..188d447cfab --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/configuration.py @@ -0,0 +1,87 @@ +"""Configuration file of OTX Detection.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from attr import attrs + +from otx.algorithms.common.configs import BaseConfig, LearningRateSchedule +from otx.api.configuration.elements import ( + add_parameter_group, + boolean_attribute, + selectable, + string_attribute, +) +from otx.api.configuration.elements.primitive_parameters import configurable_boolean +from otx.api.configuration.enums.model_lifecycle import ModelLifecycle + +# pylint: disable=invalid-name + + +@attrs +class DetectionConfig(BaseConfig): + """Configurations of OTX Detection.""" + + header = string_attribute("Configuration for an object detection task of OTX") + description = header + + @attrs + class __LearningParameters(BaseConfig.BaseLearningParameters): + header = string_attribute("Learning Parameters") + description = header + + learning_rate_schedule = selectable( + default_value=LearningRateSchedule.COSINE, + header="Learning rate schedule", + description="Specify learning rate scheduling for the MMDetection task. " + "When training for a small number of epochs (N < 10), the fixed " + "schedule is recommended. For training for 10 < N < 25 epochs, " + "step-wise or exponential annealing might give better results. " + "Finally, for training on large datasets for at least 20 " + "epochs, cyclic annealing could result in the best model.", + editable=True, + visible_in_ui=True, + ) + + @attrs + class __Postprocessing(BaseConfig.BasePostprocessing): + header = string_attribute("Postprocessing") + description = header + + @attrs + class __NNCFOptimization(BaseConfig.BaseNNCFOptimization): + header = string_attribute("Optimization by NNCF") + description = header + visible_in_ui = boolean_attribute(False) + + @attrs + class __POTParameter(BaseConfig.BasePOTParameter): + header = string_attribute("POT Parameters") + description = header + visible_in_ui = boolean_attribute(False) + + @attrs + class __AlgoBackend(BaseConfig.BaseAlgoBackendParameters): + header = string_attribute("Parameters for the OTX algo-backend") + description = header + + enable_noisy_label_detection = configurable_boolean( + default_value=False, + header="Enable loss dynamics tracking for noisy label detection", + description="Set to True to enable loss dynamics tracking for each sample to detect noisy labeled samples.", + editable=False, + visible_in_ui=False, + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + @attrs + class __TilingParameters(BaseConfig.BaseTilingParameters): + header = string_attribute("Tiling Parameters") + description = header + + learning_parameters = add_parameter_group(__LearningParameters) + postprocessing = add_parameter_group(__Postprocessing) + nncf_optimization = add_parameter_group(__NNCFOptimization) + pot_parameters = add_parameter_group(__POTParameter) + algo_backend = add_parameter_group(__AlgoBackend) + tiling_parameters = add_parameter_group(__TilingParameters) diff --git a/src/otx/algorithms/detection/configs/base/data/__init__.py b/src/otx/algorithms/detection/configs/base/data/__init__.py new file mode 100644 index 00000000000..b353ebb7201 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/__init__.py @@ -0,0 +1,4 @@ +"""Base Data Pipeline for Detection/Instance-Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/base/data/atss_data_pipeline.py b/src/otx/algorithms/detection/configs/base/data/atss_data_pipeline.py new file mode 100644 index 00000000000..24b9b26b429 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/atss_data_pipeline.py @@ -0,0 +1,106 @@ +"""Data Pipeline of ATSS model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_size = (992, 736) +__img_norm_cfg = dict(mean=[0, 0, 0], std=[255, 255, 255], to_rgb=True) + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + resize_cfg=dict( + type="Resize", + img_scale=(1088, 800), # max sizes in random image scales + keep_ratio=True, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="MinIoURandomCrop", min_ious=(0.1, 0.3, 0.5, 0.7, 0.9), min_crop_size=0.3), + dict( + type="Resize", + img_scale=[(992, 736), (896, 736), (1088, 736), (992, 672), (992, 800)], + multiscale_mode="value", + keep_ratio=False, + override=True, # Allow multiple resize + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_size, keep_ratio=False), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/data/detr_data_pipeline.py b/src/otx/algorithms/detection/configs/base/data/detr_data_pipeline.py new file mode 100644 index 00000000000..6672ae46d5d --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/detr_data_pipeline.py @@ -0,0 +1,162 @@ +"""Data pipeline for DETR based models.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +# dataset settings +dataset_type = "OTXDetDataset" +img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +# train_pipeline, NOTE the img_scale and the Pad's size_divisor is different +# from the default setting in mmdet. +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + resize_cfg=dict( + type="Resize", + img_scale=(1333, 800), # max sizes in random image scales + keep_ratio=True, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict( + type="AutoAugment", + policies=[ + [ + dict( + type="Resize", + img_scale=[ + (480, 1333), + (512, 1333), + (544, 1333), + (576, 1333), + (608, 1333), + (640, 1333), + (672, 1333), + (704, 1333), + (736, 1333), + (768, 1333), + (800, 1333), + ], + multiscale_mode="value", + keep_ratio=True, + override=True, # Allows multiple resize + ) + ], + [ + dict( + type="Resize", + # The radio of all image in train dataset < 7 + # follow the original impl + img_scale=[(400, 4200), (500, 4200), (600, 4200)], + multiscale_mode="value", + keep_ratio=True, + override=True, # Allows multiple resize + ), + dict(type="RandomCrop", crop_type="absolute_range", crop_size=(384, 600), allow_negative_crop=True), + dict( + type="Resize", + img_scale=[ + (480, 1333), + (512, 1333), + (544, 1333), + (576, 1333), + (608, 1333), + (640, 1333), + (672, 1333), + (704, 1333), + (736, 1333), + (768, 1333), + (800, 1333), + ], + multiscale_mode="value", + keep_ratio=True, + override=True, # Allows multiple resize + ), + ], + ], + ), + dict(type="Normalize", **img_norm_cfg), + dict(type="Pad", size_divisor=1), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=(1333, 800), + keep_ratio=True, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict( + type="MultiScaleFlipAug", + img_scale=(1333, 800), + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **img_norm_cfg), + dict(type="Pad", size_divisor=1), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] +# test_pipeline, NOTE the Pad's size_divisor is different from the default +# setting (size_divisor=32). While there is little effect on the performance +# whether we use the default setting or use size_divisor=1. +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=(1333, 800), + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Normalize", **img_norm_cfg), + dict(type="Pad", size_divisor=1), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] +data = dict( + train=dict( + type=dataset_type, + filter_empty_gt=False, + pipeline=train_pipeline, + ), + val=dict( + type=dataset_type, + pipeline=val_pipeline, + ), + test=dict( + type=dataset_type, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/data/iseg_efficientnet_data_pipeline.py b/src/otx/algorithms/detection/configs/base/data/iseg_efficientnet_data_pipeline.py new file mode 100644 index 00000000000..66ef798c1a5 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/iseg_efficientnet_data_pipeline.py @@ -0,0 +1,107 @@ +"""Data Pipeline of EfficientNetB2B model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_size = (1024, 1024) + +# TODO: A comparison experiment is needed to determine which value is appropriate for to_rgb. +__img_norm_cfg = dict(mean=(103.53, 116.28, 123.675), std=(1.0, 1.0, 1.0), to_rgb=True) + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_size, keep_ratio=False), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/data/iseg_efficientnet_data_pipeline_xpu.py b/src/otx/algorithms/detection/configs/base/data/iseg_efficientnet_data_pipeline_xpu.py new file mode 100644 index 00000000000..b6a8f94e76e --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/iseg_efficientnet_data_pipeline_xpu.py @@ -0,0 +1,107 @@ +"""Data Pipeline of EfficientNetB2B model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_size = (1024, 1024) + +# TODO: A comparison experiment is needed to determine which value is appropriate for to_rgb. +__img_norm_cfg = dict(mean=(103.53, 116.28, 123.675), std=(1.0, 1.0, 1.0), to_rgb=True) + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=True, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__img_size), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_size, keep_ratio=True), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__img_size), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__img_size), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/data/iseg_resnet_data_pipeline.py b/src/otx/algorithms/detection/configs/base/data/iseg_resnet_data_pipeline.py new file mode 100644 index 00000000000..fa10fd4778c --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/iseg_resnet_data_pipeline.py @@ -0,0 +1,107 @@ +"""Data Pipeline of Resnet model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_size = (1024, 1024) + +# TODO: A comparison experiment is needed to determine which value is appropriate for to_rgb. +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_size, keep_ratio=False), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/data/iseg_resnet_data_pipeline_xpu.py b/src/otx/algorithms/detection/configs/base/data/iseg_resnet_data_pipeline_xpu.py new file mode 100644 index 00000000000..4205e2f42f9 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/iseg_resnet_data_pipeline_xpu.py @@ -0,0 +1,107 @@ +"""Data Pipeline of Resnet model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_size = (1024, 1024) + +# TODO: A comparison experiment is needed to determine which value is appropriate for to_rgb. +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=True, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__img_size), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_size, keep_ratio=True), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__img_size), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__img_size), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/data/semisl/__init__.py b/src/otx/algorithms/detection/configs/base/data/semisl/__init__.py new file mode 100644 index 00000000000..97ac507f711 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/semisl/__init__.py @@ -0,0 +1,4 @@ +"""Base Semisl Data Pipeline for Detection/Instance-Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/base/data/semisl/base_semisl_det_data_pipeline.py b/src/otx/algorithms/detection/configs/base/data/semisl/base_semisl_det_data_pipeline.py new file mode 100644 index 00000000000..ac404bc72d7 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/semisl/base_semisl_det_data_pipeline.py @@ -0,0 +1,198 @@ +"""Data Pipeline for Semi-Supervised Learning Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +# This is from otx/recipes/stages/_base_/data/pipelines/ubt.py +# This could be needed sync with incr-learning's data pipeline +__img_scale_test = (992, 736) + +__img_norm_cfg = dict(mean=[0, 0, 0], std=[255, 255, 255], to_rgb=True) + +common_pipeline = [ + dict( + type="Resize", + img_scale=[ + (992, 736), + (896, 736), + (1088, 736), + (992, 672), + (992, 800), + ], + multiscale_mode="value", + keep_ratio=False, + override=True, # Allow multiple resize + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="BranchImage", key_map=dict(img="img0")), + dict(type="NDArrayToPILImage", keys=["img"]), + dict( + type="RandomApply", + transform_cfgs=[ + dict( + type="ColorJitter", + brightness=0.4, + contrast=0.4, + saturation=0.4, + hue=0.1, + ) + ], + p=0.8, + ), + dict(type="RandomGrayscale", p=0.2), + dict( + type="RandomApply", + transform_cfgs=[ + dict( + type="RandomGaussianBlur", + sigma_min=0.1, + sigma_max=2.0, + ) + ], + p=0.5, + ), + dict(type="PILImageToNDArray", keys=["img"]), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="NDArrayToTensor", keys=["img", "img0"]), + dict( + type="RandomErasing", + p=0.7, + scale=[0.05, 0.2], + ratio=[0.3, 3.3], + value="random", + ), + dict( + type="RandomErasing", + p=0.5, + scale=[0.02, 0.2], + ratio=[0.10, 6.0], + value="random", + ), + dict( + type="RandomErasing", + p=0.3, + scale=[0.02, 0.2], + ratio=[0.05, 8.0], + value="random", + ), +] + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + resize_cfg=dict( + type="Resize", + img_scale=(1088, 800), # max sizes in random image scales + keep_ratio=True, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="MinIoURandomCrop", min_ious=(0.1, 0.3, 0.5, 0.7, 0.9), min_crop_size=0.3), + *common_pipeline, + dict(type="ToTensor", keys=["gt_bboxes", "gt_labels"]), + dict( + type="ToDataContainer", + fields=[ + dict(key="img", stack=True), + dict(key="img0", stack=True), + dict(key="gt_bboxes"), + dict(key="gt_labels"), + ], + ), + dict( + type="Collect", + keys=["img", "img0", "gt_bboxes", "gt_labels"], + ), +] + +unlabeled_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=(1088, 800), # max sizes in random image scales + keep_ratio=True, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + enable_memcache=True, # Cache after resizing image & annotations + ), + *common_pipeline, + dict( + type="ToDataContainer", + fields=[ + dict(key="img", stack=True), + dict(key="img0", stack=True), + ], + ), + dict( + type="Collect", + keys=[ + "img", + "img0", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=__img_scale_test, + keep_ratio=False, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_scale_test, + flip=False, + transforms=[ + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_scale_test, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +data = dict( + train=dict( + type="OTXDetDataset", + pipeline=train_pipeline, + ), + val=dict( + type="OTXDetDataset", + pipeline=val_pipeline, + ), + test=dict( + type="OTXDetDataset", + pipeline=test_pipeline, + ), + unlabeled=dict( + type="OTXDetDataset", + pipeline=unlabeled_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/data/semisl/semisl_is_eff_data_pipeline.py b/src/otx/algorithms/detection/configs/base/data/semisl/semisl_is_eff_data_pipeline.py new file mode 100644 index 00000000000..6fa3950295c --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/semisl/semisl_is_eff_data_pipeline.py @@ -0,0 +1,200 @@ +"""Data Pipeline for Semi-Supervised Learning Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +# This is from otx/recipes/stages/_base_/data/pipelines/ubt.py +# This could be needed sync with incr-learning's data pipeline +__dataset_type = "OTXDetDataset" +__img_size = (1024, 1024) +__img_norm_cfg = dict(mean=(103.53, 116.28, 123.675), std=(1.0, 1.0, 1.0), to_rgb=True) + +common_pipeline = [ + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="BranchImage", key_map=dict(img="img0")), + dict(type="NDArrayToPILImage", keys=["img"]), + dict( + type="RandomApply", + transform_cfgs=[ + dict( + type="ColorJitter", + brightness=0.4, + contrast=0.4, + saturation=0.4, + hue=0.1, + ) + ], + p=0.8, + ), + dict(type="RandomGrayscale", p=0.2), + dict( + type="RandomApply", + transform_cfgs=[ + dict( + type="RandomGaussianBlur", + sigma_min=0.1, + sigma_max=2.0, + ) + ], + p=0.5, + ), + dict(type="PILImageToNDArray", keys=["img"]), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="NDArrayToTensor", keys=["img", "img0"]), + dict( + type="RandomErasing", + p=0.7, + scale=[0.05, 0.2], + ratio=[0.3, 3.3], + value="random", + ), + dict( + type="RandomErasing", + p=0.5, + scale=[0.02, 0.2], + ratio=[0.10, 6.0], + value="random", + ), + dict( + type="RandomErasing", + p=0.3, + scale=[0.02, 0.2], + ratio=[0.05, 8.0], + value="random", + ), +] + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=False, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +unlabeled_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=False, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + *common_pipeline, + dict( + type="ToDataContainer", + fields=[ + dict(key="img", stack=True), + dict(key="img0", stack=True), + ], + ), + dict( + type="Collect", + keys=[ + "img", + "img0", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=False, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +data = dict( + samples_per_gpu=4, + workers_per_gpu=2, + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), + unlabeled=dict( + type=__dataset_type, + pipeline=unlabeled_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/data/semisl/semisl_is_res_data_pipeline.py b/src/otx/algorithms/detection/configs/base/data/semisl/semisl_is_res_data_pipeline.py new file mode 100644 index 00000000000..ff1bcefb5c3 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/semisl/semisl_is_res_data_pipeline.py @@ -0,0 +1,191 @@ +"""Data Pipeline for Semi-Supervised Learning Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +# This is from otx/recipes/stages/_base_/data/pipelines/ubt.py +# This could be needed sync with incr-learning's data pipeline +__img_size = (1344, 800) +__dataset_type = "OTXDetDataset" +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +common_pipeline = [ + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="BranchImage", key_map=dict(img="img0")), + dict(type="NDArrayToPILImage", keys=["img"]), + dict( + type="RandomApply", + transform_cfgs=[ + dict( + type="ColorJitter", + brightness=0.4, + contrast=0.4, + saturation=0.4, + hue=0.1, + ) + ], + p=0.8, + ), + dict(type="RandomGrayscale", p=0.2), + dict( + type="RandomApply", + transform_cfgs=[ + dict( + type="RandomGaussianBlur", + sigma_min=0.1, + sigma_max=2.0, + ) + ], + p=0.5, + ), + dict(type="PILImageToNDArray", keys=["img"]), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="NDArrayToTensor", keys=["img", "img0"]), + dict( + type="RandomErasing", + p=0.7, + scale=[0.05, 0.2], + ratio=[0.3, 3.3], + value="random", + ), + dict( + type="RandomErasing", + p=0.5, + scale=[0.02, 0.2], + ratio=[0.10, 6.0], + value="random", + ), + dict( + type="RandomErasing", + p=0.3, + scale=[0.02, 0.2], + ratio=[0.05, 8.0], + value="random", + ), +] + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=True, + downscale_only=True, + # No need for upscale before caching due to upcoming random crop & resize + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="MinIoURandomCrop", min_ious=(0.1, 0.3, 0.5, 0.7, 0.9), min_crop_size=0.3), + dict( + type="Resize", + img_scale=[(1333, 400), (1333, 1200)], + keep_ratio=False, + override=True, # Allow multiple resize + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img", "gt_bboxes", "gt_labels", "gt_masks"]), +] + +unlabeled_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=False, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + *common_pipeline, + dict( + type="ToDataContainer", + fields=[ + dict(key="img", stack=True), + dict(key="img0", stack=True), + ], + ), + dict( + type="Collect", + keys=[ + "img", + "img0", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=False, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +data = dict( + samples_per_gpu=4, + workers_per_gpu=2, + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), + unlabeled=dict( + type=__dataset_type, + pipeline=unlabeled_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/data/tiling/__init__.py b/src/otx/algorithms/detection/configs/base/data/tiling/__init__.py new file mode 100644 index 00000000000..35f06d6ba90 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/tiling/__init__.py @@ -0,0 +1,4 @@ +"""Base Tiling Data Pipeline for Detection/Instance-Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/base/data/tiling/atss_tile_pipeline.py b/src/otx/algorithms/detection/configs/base/data/tiling/atss_tile_pipeline.py new file mode 100644 index 00000000000..15fdc48eecd --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/tiling/atss_tile_pipeline.py @@ -0,0 +1,99 @@ +"""Tiling Pipeline of ATSS model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +img_size = (992, 736) + +tile_cfg = dict( + tile_size=400, min_area_ratio=0.9, overlap_ratio=0.2, iou_threshold=0.45, max_per_img=1500, filter_empty_gt=True +) + +img_norm_cfg = dict(mean=[0, 0, 0], std=[255, 255, 255], to_rgb=True) + +train_pipeline = [ + dict(type="MinIoURandomCrop", min_ious=(0.1, 0.3, 0.5, 0.7, 0.9), min_crop_size=0.3), + dict( + type="Resize", + img_scale=[(992, 736), (896, 736), (1088, 736), (992, 672), (992, 800)], + multiscale_mode="value", + keep_ratio=False, + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=[ + "filename", + "ori_filename", + "ori_shape", + "img_shape", + "pad_shape", + "scale_factor", + "flip", + "flip_direction", + "img_norm_cfg", + ], + ), +] + +test_pipeline = [ + dict( + type="MultiScaleFlipAug", + img_scale=img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ) +] + +__dataset_type = "OTXDetDataset" + +train_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + ], + ), + pipeline=train_pipeline, + **tile_cfg +) + +val_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + ], + ), + pipeline=test_pipeline, + **tile_cfg +) + +test_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + test_mode=True, + pipeline=[dict(type="LoadImageFromOTXDataset")], + ), + pipeline=test_pipeline, + **tile_cfg +) + + +data = dict(train=train_dataset, val=val_dataset, test=test_dataset) diff --git a/src/otx/algorithms/detection/configs/base/data/tiling/base_iseg_tile_pipeline.py b/src/otx/algorithms/detection/configs/base/data/tiling/base_iseg_tile_pipeline.py new file mode 100644 index 00000000000..82a55b636ee --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/tiling/base_iseg_tile_pipeline.py @@ -0,0 +1,95 @@ +"""Tiling Pipeline for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +img_size = (512, 512) + +tile_cfg = dict( + tile_size=400, min_area_ratio=0.9, overlap_ratio=0.2, iou_threshold=0.45, max_per_img=1500, filter_empty_gt=True +) + +img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +train_pipeline = [ + dict(type="Resize", img_scale=img_size, keep_ratio=True), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **img_norm_cfg), + dict(type="Pad", size=img_size), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=[ + "filename", + "ori_filename", + "ori_shape", + "img_shape", + "pad_shape", + "scale_factor", + "flip", + "flip_direction", + "img_norm_cfg", + ], + ), +] + +test_pipeline = [ + dict( + type="MultiScaleFlipAug", + img_scale=img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Normalize", **img_norm_cfg), + dict(type="Pad", size=img_size), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ) +] + +__dataset_type = "OTXDetDataset" + +train_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", domain="instance_segmentation", with_bbox=True, with_mask=True), + ], + ), + pipeline=train_pipeline, + **tile_cfg +) + +val_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", domain="instance_segmentation", with_bbox=True, with_mask=True), + ], + ), + pipeline=test_pipeline, + **tile_cfg +) + +test_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + test_mode=True, + pipeline=[dict(type="LoadImageFromOTXDataset")], + ), + pipeline=test_pipeline, + **tile_cfg +) + + +data = dict(train=train_dataset, val=val_dataset, test=test_dataset) diff --git a/src/otx/algorithms/detection/configs/base/data/tiling/efficientnet_iseg_tile_pipeline.py b/src/otx/algorithms/detection/configs/base/data/tiling/efficientnet_iseg_tile_pipeline.py new file mode 100644 index 00000000000..c9fd97366b6 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/tiling/efficientnet_iseg_tile_pipeline.py @@ -0,0 +1,95 @@ +"""Tiling Pipeline of EfficientNetB2B model.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +img_size = (512, 512) + +tile_cfg = dict( + tile_size=400, min_area_ratio=0.9, overlap_ratio=0.2, iou_threshold=0.45, max_per_img=1500, filter_empty_gt=True +) + +img_norm_cfg = dict(mean=(103.53, 116.28, 123.675), std=(1.0, 1.0, 1.0), to_rgb=True) + +train_pipeline = [ + dict(type="Resize", img_scale=img_size, keep_ratio=True), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=[ + "filename", + "ori_filename", + "ori_shape", + "img_shape", + "pad_shape", + "scale_factor", + "flip", + "flip_direction", + "img_norm_cfg", + ], + ), +] + +test_pipeline = [ + dict( + type="MultiScaleFlipAug", + img_scale=img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Normalize", **img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ) +] + +__dataset_type = "OTXDetDataset" + +train_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", domain="instance_segmentation", with_bbox=True, with_mask=True), + ], + ), + pipeline=train_pipeline, + **tile_cfg +) + +val_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", domain="instance_segmentation", with_bbox=True, with_mask=True), + ], + ), + pipeline=test_pipeline, + **tile_cfg +) + +test_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + test_mode=True, + pipeline=[dict(type="LoadImageFromOTXDataset")], + ), + pipeline=test_pipeline, + **tile_cfg +) + + +data = dict(train=train_dataset, val=val_dataset, test=test_dataset) diff --git a/src/otx/algorithms/detection/configs/base/data/tiling/yolox_tile_pipeline.py b/src/otx/algorithms/detection/configs/base/data/tiling/yolox_tile_pipeline.py new file mode 100644 index 00000000000..c1d1ee5c50e --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/tiling/yolox_tile_pipeline.py @@ -0,0 +1,102 @@ +"""Tiling Pipeline of YOLOX variants for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +# NOTE: SKIP MOSAIC AND MultiImageMixDataset in tiling + +img_scale = (640, 640) + +tile_cfg = dict( + tile_size=400, min_area_ratio=0.9, overlap_ratio=0.2, iou_threshold=0.45, max_per_img=1500, filter_empty_gt=True +) + +img_norm_cfg = dict(mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0], to_rgb=False) + +train_pipeline = [ + dict( + type="RandomAffine", + scaling_ratio_range=(0.1, 2), + border=(-img_scale[0] // 2, -img_scale[1] // 2), + ), + dict(type="YOLOXHSVRandomAug"), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Resize", img_scale=img_scale, keep_ratio=True), + dict(type="Pad", pad_to_square=True, pad_val=dict(img=(114.0, 114.0, 114.0))), + dict(type="Normalize", **img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=[ + "filename", + "ori_filename", + "ori_shape", + "img_shape", + "pad_shape", + "scale_factor", + "flip", + "flip_direction", + "img_norm_cfg", + ], + ), +] + +test_pipeline = [ + dict( + type="MultiScaleFlipAug", + img_scale=img_scale, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +train_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + ], + ), + pipeline=train_pipeline, + **tile_cfg +) + +val_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + ], + ), + pipeline=test_pipeline, + **tile_cfg +) + +test_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + test_mode=True, + pipeline=[dict(type="LoadImageFromOTXDataset")], + ), + pipeline=test_pipeline, + **tile_cfg +) + + +data = dict(train=train_dataset, val=val_dataset, test=test_dataset) diff --git a/src/otx/algorithms/detection/configs/base/data/yolox_data_pipeline.py b/src/otx/algorithms/detection/configs/base/data/yolox_data_pipeline.py new file mode 100644 index 00000000000..672fad21e68 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/data/yolox_data_pipeline.py @@ -0,0 +1,120 @@ +"""Data Pipeline of YOLOX variants for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_size = (640, 640) +__img_norm_cfg = dict(mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0], to_rgb=False) + +train_pipeline = [ + dict(type="Mosaic", img_scale=__img_size, pad_val=114.0), + dict( + type="RandomAffine", + scaling_ratio_range=(0.1, 2), + border=(-__img_size[0] // 2, -__img_size[1] // 2), + ), + dict(type="MixUp", img_scale=__img_size, ratio_range=(0.8, 1.6), pad_val=114.0), + dict(type="YOLOXHSVRandomAug"), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Resize", img_scale=__img_size, keep_ratio=True, override=True), + dict(type="Pad", pad_to_square=True, pad_val=dict(img=(114.0, 114.0, 114.0))), + dict(type="Normalize", **__img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=True, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Pad", size=__img_size, pad_val=114.0), + dict(type="Normalize", **__img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Pad", size=__img_size, pad_val=114.0), + dict(type="Normalize", **__img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type="MultiImageMixDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=True, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + to_float32=False, + enable_memcache=True, # Cache after resizing image & annotations + ), + ], + ), + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/deployments/__init__.py b/src/otx/algorithms/detection/configs/base/deployments/__init__.py new file mode 100644 index 00000000000..935dc89de3a --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/deployments/__init__.py @@ -0,0 +1,5 @@ +"""Base deploy setting for detection.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/detection/configs/base/deployments/base_detection_dynamic.py b/src/otx/algorithms/detection/configs/base/deployments/base_detection_dynamic.py new file mode 100644 index 00000000000..7b99e4f3e90 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/deployments/base_detection_dynamic.py @@ -0,0 +1,22 @@ +"""Detection models dynamic deploy config.""" + +_base_ = ["./base_detection_static.py"] + +ir_config = dict( + dynamic_axes={ + "image": { + 0: "batch", + 1: "channel", + 2: "height", + 3: "width", + }, + "boxes": { + 0: "batch", + 1: "num_dets", + }, + "labels": { + 0: "batch", + 1: "num_dets", + }, + }, +) diff --git a/src/otx/algorithms/detection/configs/base/deployments/base_detection_static.py b/src/otx/algorithms/detection/configs/base/deployments/base_detection_static.py new file mode 100644 index 00000000000..a49a631ea54 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/deployments/base_detection_static.py @@ -0,0 +1,41 @@ +"""Detection models static deploy config.""" + +ir_config = dict( + type="onnx", + export_params=True, + keep_initializers_as_inputs=False, + opset_version=11, + save_file="end2end.onnx", + input_names=["image"], + output_names=["boxes", "labels"], + input_shape=None, + # TODO + # optimizing onnx graph mess up NNCF graph at some point + # where we need to look into + optimize=False, +) + +codebase_config = dict( + type="mmdet", + task="ObjectDetection", + model_type="end2end", + post_processing=dict( + score_threshold=0.05, + confidence_threshold=0.005, # for YOLOv3 + iou_threshold=0.5, + max_output_boxes_per_class=200, + pre_top_k=5000, + keep_top_k=100, + background_label_id=-1, + ), +) + +backend_config = dict( + type="openvino", + mo_options=None, +) + +input_data = dict( + shape=(128, 128, 3), + file_path=None, +) diff --git a/src/otx/algorithms/detection/configs/base/deployments/base_instance_segmentation_dynamic.py b/src/otx/algorithms/detection/configs/base/deployments/base_instance_segmentation_dynamic.py new file mode 100644 index 00000000000..a34051e4fc0 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/deployments/base_instance_segmentation_dynamic.py @@ -0,0 +1,28 @@ +"""Instance segmentation models dynamic deploy config.""" + +_base_ = ["./base_instance_segmentation_static.py"] + +ir_config = dict( + dynamic_axes={ + "image": { + 0: "batch", + 1: "channel", + 2: "height", + 3: "width", + }, + "boxes": { + 0: "batch", + 1: "num_dets", + }, + "labels": { + 0: "batch", + 1: "num_dets", + }, + "masks": { + 0: "batch", + 1: "num_dets", + 2: "height", + 3: "width", + }, + } +) diff --git a/src/otx/algorithms/detection/configs/base/deployments/base_instance_segmentation_static.py b/src/otx/algorithms/detection/configs/base/deployments/base_instance_segmentation_static.py new file mode 100644 index 00000000000..e8004a7dafb --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/deployments/base_instance_segmentation_static.py @@ -0,0 +1,17 @@ +"""Instance segmentation models static deploy config.""" + +_base_ = ["./base_detection_static.py"] + +ir_config = dict( + output_names=[ + "boxes", + "labels", + "masks", + ] +) + +codebase_config = dict( + post_processing=dict( + export_postprocess_mask=False, + ) +) diff --git a/src/otx/algorithms/detection/configs/base/models/__init__.py b/src/otx/algorithms/detection/configs/base/models/__init__.py new file mode 100644 index 00000000000..268db3c4cac --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/models/__init__.py @@ -0,0 +1,15 @@ +"""Configs base models of OTX Detection.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/base/models/detector.py b/src/otx/algorithms/detection/configs/base/models/detector.py new file mode 100644 index 00000000000..f5de60bd861 --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/models/detector.py @@ -0,0 +1,23 @@ +"""Base detector configuration of OTX Detection.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = "./model.py" + +task = "detection" + +model = dict(train_cfg={}, test_cfg={}) # type: dict diff --git a/src/otx/algorithms/detection/configs/base/models/model.py b/src/otx/algorithms/detection/configs/base/models/model.py new file mode 100644 index 00000000000..da5cd9a594a --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/models/model.py @@ -0,0 +1,25 @@ +"""Base detector configuration of OTX Detection.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +model = {} # type: dict + +load_from = None + +resume_from = None + +checkpoint_config = dict(interval=1, max_keep_ckpts=1) diff --git a/src/otx/algorithms/detection/configs/base/models/single_stage_detector.py b/src/otx/algorithms/detection/configs/base/models/single_stage_detector.py new file mode 100644 index 00000000000..852c1b827eb --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/models/single_stage_detector.py @@ -0,0 +1,42 @@ +"""Single Stage detector configuration of OTX Detection.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = "./detector.py" + +model = dict( + type="SingleStageDetector", + train_cfg=dict( + assigner=dict( + type="MaxIoUAssigner", + min_pos_iou=0.0, + ignore_iof_thr=-1, + gt_max_assign_all=False, + ), + smoothl1_beta=1.0, + allowed_border=-1, + pos_weight=-1, + neg_pos_ratio=3, + debug=False, + ), + test_cfg=dict( + nms=dict(type="nms", iou_threshold=0.45), + min_bbox_size=0, + score_thr=0.02, + max_per_img=200, + ), +) diff --git a/src/otx/algorithms/detection/configs/base/models/unbiased_teacher.py b/src/otx/algorithms/detection/configs/base/models/unbiased_teacher.py new file mode 100644 index 00000000000..c62793b3eed --- /dev/null +++ b/src/otx/algorithms/detection/configs/base/models/unbiased_teacher.py @@ -0,0 +1,25 @@ +"""Unbiased teacher detector configuration of OTX Semi-SL Detection.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = "./detector.py" + +model = dict( + super_type="UnbiasedTeacher", + pseudo_conf_thresh=0.25, + unlabeled_loss_weight=1.0, +) diff --git a/src/otx/algorithms/detection/configs/detection/__init__.py b/src/otx/algorithms/detection/configs/detection/__init__.py new file mode 100644 index 00000000000..cb2864ca1d8 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/__init__.py @@ -0,0 +1,5 @@ +"""Base configs for detection.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/detection/configs/detection/configuration.yaml b/src/otx/algorithms/detection/configs/detection/configuration.yaml new file mode 100644 index 00000000000..a332cad8711 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/configuration.yaml @@ -0,0 +1,682 @@ +description: Configuration for an object detection task +header: Configuration for an object detection task +learning_parameters: + batch_size: + affects_outcome_of: TRAINING + default_value: 5 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 5 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + inference_batch_size: + affects_outcome_of: TRAINING + default_value: 1 + description: The number of samples seen in each iteration of inference. + Increasing this value improves inference time and may make the inference more + stable. A larger batch size has higher memory requirements. + editable: true + header: Inference batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + description: Learning Parameters + header: Learning Parameters + learning_rate: + affects_outcome_of: TRAINING + default_value: 0.01 + description: + Increasing this value will speed up training convergence but might + make it unstable. + editable: true + header: Learning rate + max_value: 0.1 + min_value: 1.0e-07 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.01 + visible_in_ui: true + warning: null + auto_hpo_state: NOT_POSSIBLE + learning_rate_warmup_iters: + affects_outcome_of: TRAINING + default_value: 100 + description: + In this periods of initial training iterations, the model will be trained in low learning rate, + which will be increased incrementally up to the expected learning rate setting. + This warm-up phase is known to be helpful to stabilize training, thus result in better performance. + editable: true + header: Number of iterations for learning rate warmup + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: null + num_iters: + affects_outcome_of: TRAINING + default_value: 1 + description: + Increasing this value causes the results to be more robust but training + time will be longer. + editable: true + header: Number of training iterations + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: null + num_workers: + affects_outcome_of: NONE + default_value: 2 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of cpu threads to use during batch generation + max_value: 8 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: null + enable_early_stopping: + affects_outcome_of: TRAINING + default_value: true + description: Early exit from training when validation accuracy isn't changed or decreased for several epochs. + editable: true + header: Enable early stopping of the training + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + early_stop_start: + affects_outcome_of: TRAINING + default_value: 3 + editable: true + header: Start epoch for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 3 + visible_in_ui: false + early_stop_patience: + affects_outcome_of: TRAINING + default_value: 10 + description: Training will stop if the model does not improve within the number of epochs of patience. + editable: true + header: Patience for early stopping + max_value: 50 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 10 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + early_stop_iteration_patience: + affects_outcome_of: TRAINING + default_value: 0 + description: + Training will stop if the model does not improve within the number of iterations of patience. + This ensures the model is trained enough with the number of iterations of patience before early stopping. + editable: true + header: Iteration patience for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + use_adaptive_interval: + affects_outcome_of: TRAINING + default_value: true + description: Depending on the size of iteration per epoch, adaptively update the validation interval and related values. + editable: true + header: Use adaptive validation interval + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: This will automatically control the patience and interval when early stopping is enabled. + auto_adapt_batch_size: + affects_outcome_of: TRAINING + default_value: None + description: Safe => Prevent GPU out of memory. Full => Find a batch size using most of GPU memory. + editable: true + enum_name: BatchSizeAdaptType + header: Decrease batch size if current batch size isn't fit to CUDA memory. + options: + NONE: "None" + SAFE: "Safe" + FULL: "Full" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: None + visible_in_ui: true + warning: + Enabling this could change the actual batch size depending on the current GPU status. + The learning rate also could be adjusted according to the adapted batch size. This process might change + a model performance and take some extra computation time to try a few batch size candidates. + auto_num_workers: + affects_outcome_of: TRAINING + default_value: false + description: Adapt num_workers according to current hardware status automatically. + editable: true + header: Enable auto adaptive num_workers + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + input_size: + affects_outcome_of: INFERENCE + default_value: Default + description: + The input size of the given model could be configured to one of the predefined resolutions. + Reduced training and inference time could be expected by using smaller input size. + In Auto mode, the input size is automatically determined based on dataset statistics. + Defaults to per-model default resolution. + editable: true + enum_name: InputSizePreset + header: Configure model input size. + options: + DEFAULT: "Default" + AUTO: "Auto" + _256x256: "256x256" + _384x384: "384x384" + _512x512: "512x512" + _768x768: "768x768" + _1024x1024: "1024x1024" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Default + visible_in_ui: false + warning: Modifying input size may decrease model performance. + type: PARAMETER_GROUP + visible_in_ui: true +postprocessing: + confidence_threshold: + affects_outcome_of: INFERENCE + default_value: 0.35 + description: + This threshold only takes effect if the threshold is not set based + on the result. + editable: true + header: Confidence threshold + max_value: 1 + min_value: 0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + # value: 0.35 + value: 0.01 + visible_in_ui: true + warning: null + max_num_detections: + affects_outcome_of: INFERENCE + default_value: 0 + description: + Extra detection outputs will be discared in non-maximum suppression process. + Defaults to 0, which means per-model default values. + editable: true + header: Maximum number of detections per image + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: null + use_ellipse_shapes: + affects_outcome_of: INFERENCE + default_value: false + description: Use direct ellipse shape in inference instead of polygon from mask + editable: true + header: Use ellipse shapes + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + description: Postprocessing + header: Postprocessing + result_based_confidence_threshold: + affects_outcome_of: INFERENCE + default_value: true + description: Confidence threshold is derived from the results + editable: true + header: Result based confidence threshold + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +algo_backend: + description: parameters for algo backend + header: Algo backend parameters + train_type: + affects_outcome_of: TRAINING + default_value: Incremental + description: Training scheme option that determines how to train the model + editable: True + enum_name: TrainType + header: Train type + options: + Incremental: "Incremental" + Semisupervised: "Semisupervised" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Incremental + visible_in_ui: false + warning: null + mem_cache_size: + affects_outcome_of: TRAINING + default_value: 1000000000 + description: Size of memory pool for caching decoded data to load data faster (bytes). + editable: true + header: Size of memory pool + max_value: 9223372036854775807 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + storage_cache_scheme: + affects_outcome_of: TRAINING + default_value: NONE + description: Scheme for storage cache + editable: true + enum_name: StorageCacheScheme + header: Scheme for storage cache + options: + NONE: "NONE" + AS_IS: "AS-IS" + JPEG_75: "JPEG/75" + JPEG_95: "JPEG/95" + PNG: "PNG" + TIFF: "TIFF" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + enable_noisy_label_detection: + affects_outcome_of: TRAINING + default_value: false + description: Set to True to enable loss dynamics tracking for each sample to detect noisy labeled samples. + editable: true + header: Enable loss dynamics tracking for noisy label detection + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: True + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: True + warning: null + stat_subset_size: + affects_outcome_of: NONE + default_value: 300 + description: Number of data samples used for post-training optimization + editable: True + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: True + warning: null + stat_requests_number: + affects_outcome_of: NONE + default_value: 0 + description: Number of requests during statistics collection + editable: true + header: Number of requests + max_value: 100000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + header: Optimization by NNCF + enable_quantization: + affects_outcome_of: INFERENCE + default_value: True + description: Enable quantization algorithm + editable: false + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: false + warning: null + enable_pruning: + affects_outcome_of: INFERENCE + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + pruning_supported: + affects_outcome_of: TRAINING + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + maximal_accuracy_degradation: + affects_outcome_of: NONE + default_value: 1.0 + description: The maximal allowed accuracy metric drop in absolute values + editable: True + header: Maximum accuracy degradation + max_value: 100.0 + min_value: 0.0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: True + warning: null + type: PARAMETER_GROUP + visible_in_ui: True + +tiling_parameters: + header: Tiling + description: Crop dataset to tiles + + enable_tiling: + header: Enable tiling + description: Set to True to allow tiny objects to be better detected. + default_value: false + editable: true + affects_outcome_of: TRAINING + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: "Tiling trades off speed for accuracy as it increases the number of images to be processed. In turn, it's memory efficient as smaller resolution patches are handled at onces so that the possibility of OOM issues could be reduced. Important: In the current version, depending on the dataset size and the available hardware resources, a model may not train successfully when tiling is enabled." + + enable_adaptive_params: + header: Enable adaptive tiling parameters + description: Config tile size and tile overlap adaptively based on annotated dataset statistic. Manual settings well be ignored if it's turned on. Please turn off this option in order to tune tiling parameters manually. + default_value: true + editable: true + affects_outcome_of: TRAINING + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + + tile_size: + header: Tile Image Size + description: Tile image size. (tile_size x tile_size) sub images will be the unit of computation. + affects_outcome_of: TRAINING + default_value: 400 + min_value: 100 + max_value: 4096 + type: INTEGER + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 400 + visible_in_ui: true + warning: null + + tile_overlap: + header: Tile Overlap + description: Overlap ratio between each two neighboring tiles. Recommend to set as large_object_size / tile_size. + affects_outcome_of: TRAINING + default_value: 0.2 + min_value: 0.0 + max_value: 0.9 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.2 + visible_in_ui: true + warning: null + + tile_max_number: + header: Max object per tile + description: Maximum number of objects per tile + affects_outcome_of: TRAINING + default_value: 1500 + min_value: 1 + max_value: 5000 + type: INTEGER + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1500 + visible_in_ui: true + warning: null + + tile_sampling_ratio: + header: Sampling Ratio for entire tiling + description: Since tiling train and validation to all tile from large image, usually it takes lots of time than normal training. The tile_sampling_ratio is ratio for sampling entire tile dataset. Sampling tile dataset would save lots of time for training and validation time. Note that sampling will be applied to training and validation dataset, not test dataset. + affects_outcome_of: TRAINING + default_value: 1.0 + min_value: 0.000001 + max_value: 1.0 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: true + warning: null + + object_tile_ratio: + header: Object tile ratio + description: The desired ratio of min object size and tile size. + affects_outcome_of: TRAINING + default_value: 0.03 + min_value: 0.00 + max_value: 1.00 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.03 + visible_in_ui: false + warning: null + + type: PARAMETER_GROUP + visible_in_ui: true diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/__init__.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/__init__.py new file mode 100644 index 00000000000..6525d113c29 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of YOLOX_L model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/compression_config.json b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/compression_config.json new file mode 100644 index 00000000000..7eafa3929f5 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/compression_config.json @@ -0,0 +1,33 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 640, 640] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization" + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/data_pipeline.py new file mode 100644 index 00000000000..522c9b3815e --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/data_pipeline.py @@ -0,0 +1,9 @@ +"""Data Pipeline of YOLOX_L model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + + +_base_ = ["../../base/data/yolox_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/deployment.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/deployment.py new file mode 100644 index 00000000000..678dcde99a8 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/deployment.py @@ -0,0 +1,14 @@ +"""MMDeploy config of YOLOX_L model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +_base_ = ["../../base/deployments/base_detection_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 640, 640]))], +) diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/hpo_config.yaml b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/hpo_config.yaml new file mode 100644 index 00000000000..b01cbe54f8f --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0001 + - 0.01 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 4 + - 16 + - 2 diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/model.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/model.py new file mode 100644 index 00000000000..2d20a9ede43 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/model.py @@ -0,0 +1,24 @@ +"""Model configuration of YOLOX_L model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/detection/incremental.py", "../../base/models/detector.py"] + +model = dict( + type="CustomYOLOX", + backbone=dict(type="CSPDarknet", deepen_factor=1.0, widen_factor=1.0, out_indices=(2, 3, 4)), + neck=dict(type="YOLOXPAFPN", in_channels=[256, 512, 1024], out_channels=256, num_csp_blocks=3), + bbox_head=dict(type="CustomYOLOXHead", num_classes=80, in_channels=256, feat_channels=256), + train_cfg=dict(assigner=dict(type="SimOTAAssigner", center_radius=2.5)), + # In order to align the source code, the threshold of the val phase is + # 0.01, and the threshold of the test phase is 0.001. + test_cfg=dict(score_thr=0.01, nms=dict(type="nms", iou_threshold=0.65), max_per_img=100), +) +load_from = "https://download.openmmlab.com/mmdetection/v2.0/yolox/\ +yolox_l_8x8_300e_coco/yolox_l_8x8_300e_coco_20211126_140236-d3bd2b23.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/model_xpu.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/model_xpu.py new file mode 100644 index 00000000000..d83f70f6c60 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/model_xpu.py @@ -0,0 +1,26 @@ +"""Model configuration of YOLOX_L model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/detection/incremental.py", "../../base/models/detector.py"] + +model = dict( + type="CustomYOLOX", + backbone=dict(type="CSPDarknet", deepen_factor=1.0, widen_factor=1.0, out_indices=(2, 3, 4)), + neck=dict(type="YOLOXPAFPN", in_channels=[256, 512, 1024], out_channels=256, num_csp_blocks=3), + bbox_head=dict(type="CustomYOLOXHead", num_classes=80, in_channels=256, feat_channels=256), + train_cfg=dict(assigner=dict(type="SimOTAAssigner", center_radius=2.5)), + # In order to align the source code, the threshold of the val phase is + # 0.01, and the threshold of the test phase is 0.001. + test_cfg=dict(score_thr=0.01, nms=dict(type="nms", iou_threshold=0.65), max_per_img=100), + size_multiplier=160, + random_size_range=(3, 5), +) +load_from = "https://download.openmmlab.com/mmdetection/v2.0/yolox/\ +yolox_l_8x8_300e_coco/yolox_l_8x8_300e_coco_20211126_140236-d3bd2b23.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml new file mode 100644 index 00000000000..f06013bba10 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml @@ -0,0 +1,65 @@ +# Description. +model_template_id: Object_Detection_YOLOX_L +name: YOLOX-L +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for YOLOX_L +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 8 + learning_rate: + default_value: 0.001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 200 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 194.57 +size: 207 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/tile_pipeline.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/tile_pipeline.py new file mode 100644 index 00000000000..ce20f79cd20 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of YOLOX_L model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/yolox_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/__init__.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/__init__.py new file mode 100644 index 00000000000..feb13a8b8fe --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of YOLOX_S model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/compression_config.json b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/compression_config.json new file mode 100644 index 00000000000..7eafa3929f5 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/compression_config.json @@ -0,0 +1,33 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 640, 640] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization" + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/data_pipeline.py new file mode 100644 index 00000000000..826e3059907 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/data_pipeline.py @@ -0,0 +1,9 @@ +"""Data Pipeline of YOLOX_S model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + + +_base_ = ["../../base/data/yolox_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/deployment.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/deployment.py new file mode 100644 index 00000000000..041eb30fa2e --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/deployment.py @@ -0,0 +1,13 @@ +"""MMDeploy config of YOLOX_S model for Detection Task.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +_base_ = ["../../base/deployments/base_detection_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 640, 640]))], +) diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/hpo_config.yaml b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/hpo_config.yaml new file mode 100644 index 00000000000..b01cbe54f8f --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0001 + - 0.01 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 4 + - 16 + - 2 diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/model.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/model.py new file mode 100644 index 00000000000..58114e4f173 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/model.py @@ -0,0 +1,24 @@ +"""Model configuration of YOLOX_S model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/detection/incremental.py", "../../base/models/detector.py"] + +model = dict( + type="CustomYOLOX", + backbone=dict(type="CSPDarknet", deepen_factor=0.33, widen_factor=0.5, out_indices=(2, 3, 4)), + neck=dict(type="YOLOXPAFPN", in_channels=[128, 256, 512], out_channels=128, num_csp_blocks=4), + bbox_head=dict(type="CustomYOLOXHead", num_classes=80, in_channels=128, feat_channels=128), + train_cfg=dict(assigner=dict(type="SimOTAAssigner", center_radius=2.5)), + # In order to align the source code, the threshold of the val phase is + # 0.01, and the threshold of the test phase is 0.001. + test_cfg=dict(score_thr=0.01, nms=dict(type="nms", iou_threshold=0.65), max_per_img=100), +) +load_from = "https://download.openmmlab.com/mmdetection/v2.0/yolox/\ +yolox_s_8x8_300e_coco/yolox_s_8x8_300e_coco_20211121_095711-4592a793.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/model_xpu.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/model_xpu.py new file mode 100644 index 00000000000..b5f07241f66 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/model_xpu.py @@ -0,0 +1,26 @@ +"""Model configuration of YOLOX_S model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/detection/incremental.py", "../../base/models/detector.py"] + +model = dict( + type="CustomYOLOX", + backbone=dict(type="CSPDarknet", deepen_factor=0.33, widen_factor=0.5, out_indices=(2, 3, 4)), + neck=dict(type="YOLOXPAFPN", in_channels=[128, 256, 512], out_channels=128, num_csp_blocks=4), + bbox_head=dict(type="CustomYOLOXHead", num_classes=80, in_channels=128, feat_channels=128), + train_cfg=dict(assigner=dict(type="SimOTAAssigner", center_radius=2.5)), + # In order to align the source code, the threshold of the val phase is + # 0.01, and the threshold of the test phase is 0.001. + test_cfg=dict(score_thr=0.01, nms=dict(type="nms", iou_threshold=0.65), max_per_img=100), + size_multiplier=160, + random_size_range=(3, 5), +) +load_from = "https://download.openmmlab.com/mmdetection/v2.0/yolox/\ +yolox_s_8x8_300e_coco/yolox_s_8x8_300e_coco_20211121_095711-4592a793.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml new file mode 100644 index 00000000000..335b07f8099 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml @@ -0,0 +1,65 @@ +# Description. +model_template_id: Object_Detection_YOLOX_S +name: YOLOX-S +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for YOLOX_S +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 8 + learning_rate: + default_value: 0.001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 200 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 33.51 +size: 46 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/tile_pipeline.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/tile_pipeline.py new file mode 100644 index 00000000000..dcb7c0caaa3 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of YOLOX_S model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/yolox_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/__init__.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/__init__.py new file mode 100644 index 00000000000..e8c550026f4 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of YOLOX Tiny model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/compression_config.json b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/compression_config.json new file mode 100644 index 00000000000..7eafa3929f5 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/compression_config.json @@ -0,0 +1,33 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 640, 640] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization" + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/data_pipeline.py new file mode 100644 index 00000000000..2573663c8ec --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/data_pipeline.py @@ -0,0 +1,126 @@ +"""Data Pipeline of YOLOX Tiny model for Detection Task.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) +__img_size = (640, 640) +__img_size_test = (416, 416) + +train_pipeline = [ + dict(type="Mosaic", img_scale=__img_size, pad_val=114.0), + dict( + type="RandomAffine", + scaling_ratio_range=(0.5, 1.5), + border=(-__img_size[0] // 2, -__img_size[1] // 2), + ), + dict( + type="PhotoMetricDistortion", + brightness_delta=32, + contrast_range=(0.5, 1.5), + saturation_range=(0.5, 1.5), + hue_delta=18, + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Resize", img_scale=__img_size, keep_ratio=True, override=True), # Allow multiple resize + dict(type="Pad", pad_to_square=True, pad_val=114.0), + dict(type="Normalize", **__img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=__img_size_test, + keep_ratio=True, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size_test, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Pad", size=__img_size_test, pad_val=114.0), + dict(type="Normalize", **__img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size_test, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Pad", size=__img_size_test, pad_val=114.0), + dict(type="Normalize", **__img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type="MultiImageMixDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=True, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + to_float32=False, + enable_memcache=True, # Cache after resizing image & annotations + ), + ], + ), + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/deployment.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/deployment.py new file mode 100644 index 00000000000..5cc28292e96 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/deployment.py @@ -0,0 +1,11 @@ +"""MMDeploy config of YOLOX Tiny model for Detection Task.""" + +_base_ = ["../../base/deployments/base_detection_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 416, 416]))], +) diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/hpo_config.yaml b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/hpo_config.yaml new file mode 100644 index 00000000000..a89872d9b69 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.00002 + - 0.002 + - 0.00001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 4 + - 16 + - 2 diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/model.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/model.py new file mode 100644 index 00000000000..6d88d486b7e --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/model.py @@ -0,0 +1,35 @@ +"""Model configuration of YOLOX Tiny model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/detection/incremental.py", "../../base/models/detector.py"] + +model = dict( + type="CustomYOLOX", + backbone=dict(type="CSPDarknet", deepen_factor=0.33, widen_factor=0.375, out_indices=(2, 3, 4)), + neck=dict(type="YOLOXPAFPN", in_channels=[96, 192, 384], out_channels=96, num_csp_blocks=1), + bbox_head=dict(type="CustomYOLOXHead", num_classes=80, in_channels=96, feat_channels=96), + train_cfg=dict(assigner=dict(type="SimOTAAssigner", center_radius=2.5)), + # In order to align the source code, the threshold of the val phase is + # 0.01, and the threshold of the test phase is 0.001. + test_cfg=dict(score_thr=0.01, nms=dict(type="nms", iou_threshold=0.65), max_per_img=100), +) +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/object_detection/v2/yolox_tiny_8x8.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/model_xpu.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/model_xpu.py new file mode 100644 index 00000000000..e7269d687fd --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/model_xpu.py @@ -0,0 +1,37 @@ +"""Model configuration of YOLOX Tiny model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/detection/incremental.py", "../../base/models/detector.py"] + +model = dict( + type="CustomYOLOX", + backbone=dict(type="CSPDarknet", deepen_factor=0.33, widen_factor=0.375, out_indices=(2, 3, 4)), + neck=dict(type="YOLOXPAFPN", in_channels=[96, 192, 384], out_channels=96, num_csp_blocks=1), + bbox_head=dict(type="CustomYOLOXHead", num_classes=80, in_channels=96, feat_channels=96), + train_cfg=dict(assigner=dict(type="SimOTAAssigner", center_radius=2.5)), + # In order to align the source code, the threshold of the val phase is + # 0.01, and the threshold of the test phase is 0.001. + test_cfg=dict(score_thr=0.01, nms=dict(type="nms", iou_threshold=0.65), max_per_img=100), + size_multiplier=160, + random_size_range=(3, 5), +) +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/object_detection/v2/yolox_tiny_8x8.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/template.yaml b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/template.yaml new file mode 100644 index 00000000000..5937daa1064 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/template.yaml @@ -0,0 +1,64 @@ +# Description. +model_template_id: Custom_Object_Detection_YOLOX +name: YOLOX-TINY +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for YOLOX-TINY +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + nncf: otx.algorithms.detection.adapters.mmdet.nncf.task.DetectionNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 8 + learning_rate: + default_value: 0.0002 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 200 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 6.5 +size: 20.4 + +# Model spec +model_category: SPEED diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/tile_pipeline.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/tile_pipeline.py new file mode 100644 index 00000000000..af72de6ea48 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_tiny/tile_pipeline.py @@ -0,0 +1,118 @@ +"""Tiling Pipeline of YOLOX Tiny model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +# NOTE: SKIP MOSAIC AND MultiImageMixDataset in tiling + +dataset_type = "OTXDetDataset" + +img_scale = (640, 640) + +tile_cfg = dict( + tile_size=400, min_area_ratio=0.9, overlap_ratio=0.2, iou_threshold=0.45, max_per_img=1500, filter_empty_gt=True +) + +img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +train_pipeline = [ + dict(type="RandomAffine", scaling_ratio_range=(0.5, 1.5), border=(-img_scale[0] // 2, -img_scale[1] // 2)), + dict( + type="PhotoMetricDistortion", + brightness_delta=32, + contrast_range=(0.5, 1.5), + saturation_range=(0.5, 1.5), + hue_delta=18, + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Resize", img_scale=img_scale, keep_ratio=False), + dict(type="Pad", pad_to_square=True, pad_val=114.0), + dict(type="Normalize", **img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=[ + "filename", + "ori_filename", + "ori_shape", + "img_shape", + "pad_shape", + "scale_factor", + "flip", + "flip_direction", + "img_norm_cfg", + ], + ), +] + +test_pipeline = [ + dict( + type="MultiScaleFlipAug", + img_scale=(416, 416), + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Pad", size=(416, 416), pad_val=114.0), + dict(type="Normalize", **img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ) +] + +__dataset_type = "OTXDetDataset" + +train_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + ], + ), + pipeline=train_pipeline, + **tile_cfg +) + +val_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + ], + ), + pipeline=test_pipeline, + **tile_cfg +) + +test_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + test_mode=True, + pipeline=[dict(type="LoadImageFromOTXDataset")], + ), + pipeline=test_pipeline, + **tile_cfg +) + + +data = dict(train=train_dataset, val=val_dataset, test=test_dataset) diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/__init__.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/__init__.py new file mode 100644 index 00000000000..524fa8521c9 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of YOLOX_X model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/compression_config.json b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/compression_config.json new file mode 100644 index 00000000000..7eafa3929f5 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/compression_config.json @@ -0,0 +1,33 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 640, 640] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization" + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/data_pipeline.py new file mode 100644 index 00000000000..27f9cdc3d09 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/data_pipeline.py @@ -0,0 +1,9 @@ +"""Data Pipeline of YOLOX_X model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + + +_base_ = ["../../base/data/yolox_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/deployment.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/deployment.py new file mode 100644 index 00000000000..cd50d6d0ba1 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/deployment.py @@ -0,0 +1,13 @@ +"""MMDeploy config of YOLOX_X model for Detection Task.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +_base_ = ["../../base/deployments/base_detection_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 640, 640]))], +) diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/hpo_config.yaml b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/hpo_config.yaml new file mode 100644 index 00000000000..b01cbe54f8f --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0001 + - 0.01 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 4 + - 16 + - 2 diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/model.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/model.py new file mode 100644 index 00000000000..e8c197507bc --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/model.py @@ -0,0 +1,24 @@ +"""Model configuration of YOLOX_X model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/detection/incremental.py", "../../base/models/detector.py"] + +model = dict( + type="CustomYOLOX", + backbone=dict(type="CSPDarknet", deepen_factor=1.33, widen_factor=1.25, out_indices=(2, 3, 4)), + neck=dict(type="YOLOXPAFPN", in_channels=[320, 640, 1280], out_channels=320, num_csp_blocks=4), + bbox_head=dict(type="CustomYOLOXHead", num_classes=80, in_channels=320, feat_channels=320), + train_cfg=dict(assigner=dict(type="SimOTAAssigner", center_radius=2.5)), + # In order to align the source code, the threshold of the val phase is + # 0.01, and the threshold of the test phase is 0.001. + test_cfg=dict(score_thr=0.01, nms=dict(type="nms", iou_threshold=0.65), max_per_img=100), +) +load_from = "https://download.openmmlab.com/mmdetection/v2.0/yolox\ +/yolox_x_8x8_300e_coco/yolox_x_8x8_300e_coco_20211126_140254-1ef88d67.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/model_xpy.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/model_xpy.py new file mode 100644 index 00000000000..d54001b888d --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/model_xpy.py @@ -0,0 +1,26 @@ +"""Model configuration of YOLOX_X model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = ["../../../../../recipes/stages/detection/incremental.py", "../../base/models/detector.py"] + +model = dict( + type="CustomYOLOX", + backbone=dict(type="CSPDarknet", deepen_factor=1.33, widen_factor=1.25, out_indices=(2, 3, 4)), + neck=dict(type="YOLOXPAFPN", in_channels=[320, 640, 1280], out_channels=320, num_csp_blocks=4), + bbox_head=dict(type="CustomYOLOXHead", num_classes=80, in_channels=320, feat_channels=320), + train_cfg=dict(assigner=dict(type="SimOTAAssigner", center_radius=2.5)), + # In order to align the source code, the threshold of the val phase is + # 0.01, and the threshold of the test phase is 0.001. + test_cfg=dict(score_thr=0.01, nms=dict(type="nms", iou_threshold=0.65), max_per_img=100), + size_multiplier=160, + random_size_range=(3, 5), +) +load_from = "https://download.openmmlab.com/mmdetection/v2.0/yolox\ +/yolox_x_8x8_300e_coco/yolox_x_8x8_300e_coco_20211126_140254-1ef88d67.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml new file mode 100644 index 00000000000..1fdf665d533 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml @@ -0,0 +1,65 @@ +# Description. +model_template_id: Object_Detection_YOLOX_X +name: YOLOX-X +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for YOLOX_X +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 4 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 4 + learning_rate: + default_value: 0.001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 200 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 352.42 +size: 378 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/tile_pipeline.py b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/tile_pipeline.py new file mode 100644 index 00000000000..c6866809888 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of YOLOX_X model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/yolox_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/__init__.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/__init__.py new file mode 100644 index 00000000000..a2e9b592e2f --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNetV2-ATSS model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/compression_config.json b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/compression_config.json new file mode 100644 index 00000000000..7936d13ac21 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/compression_config.json @@ -0,0 +1,74 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 992, 736] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "nncf_quantization_pruning": { + "nncf_config": { + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "initial_training_phase_epochs": 5, + "maximal_total_epochs": 100, + "patience_epochs": 5 + } + }, + "compression": [ + { + "algorithm": "filter_pruning", + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median" + } + }, + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ] + } + }, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning"] +} diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/data_pipeline.py new file mode 100644 index 00000000000..03a9046cd48 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of MobileNetV2-ATSS model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/atss_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/deployment.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/deployment.py new file mode 100644 index 00000000000..f716e4af3ba --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/deployment.py @@ -0,0 +1,11 @@ +"""MMDeploy config of MobileNetV2-ATSS model for Detection Task.""" + +_base_ = ["../../base/deployments/base_detection_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 736, 992]))], +) diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/hpo_config.yaml b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/hpo_config.yaml new file mode 100644 index 00000000000..8df3d6be9dd --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0004 + - 0.04 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 4 + - 16 + - 2 diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/model.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/model.py new file mode 100644 index 00000000000..1dfc05f19f5 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/model.py @@ -0,0 +1,91 @@ +"""Model configuration of MobileNetV2-ATSS model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/detection/incremental.py", + "../../../../common/adapters/mmcv/configs/backbones/mobilenet_v2_w1.yaml", + "../../base/models/detector.py", +] + +model = dict( + type="CustomATSS", + neck=dict( + type="FPN", + in_channels=[24, 32, 96, 320], + out_channels=64, + start_level=1, + add_extra_convs="on_output", + num_outs=5, + relu_before_extra_convs=True, + ), + bbox_head=dict( + type="CustomATSSHead", + num_classes=2, + in_channels=64, + stacked_convs=4, + feat_channels=64, + anchor_generator=dict( + type="AnchorGenerator", + ratios=[1.0], + octave_base_scale=8, + scales_per_octave=1, + strides=[8, 16, 32, 64, 128], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[0.1, 0.1, 0.2, 0.2], + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="GIoULoss", loss_weight=2.0), + loss_centerness=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + use_qfl=False, + qfl_cfg=dict( + type="QualityFocalLoss", + use_sigmoid=True, + beta=2.0, + loss_weight=1.0, + ), + ), + train_cfg=dict( + assigner=dict(type="ATSSAssigner", topk=9), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + test_cfg=dict( + nms_pre=1000, + min_bbox_size=0, + score_thr=0.05, + nms=dict(type="nms", iou_threshold=0.6), + max_per_img=100, + ), + backbone=dict( + out_indices=( + 2, + 3, + 4, + 5, + ) + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/object_detection/v2/mobilenet_v2-atss.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/__init__.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/__init__.py new file mode 100644 index 00000000000..117edfb950e --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of MobileNetV2-ATSS model for Semi-SL Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/compression_config.json b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/compression_config.json new file mode 100644 index 00000000000..7936d13ac21 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/compression_config.json @@ -0,0 +1,74 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 992, 736] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "nncf_quantization_pruning": { + "nncf_config": { + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "initial_training_phase_epochs": 5, + "maximal_total_epochs": 100, + "patience_epochs": 5 + } + }, + "compression": [ + { + "algorithm": "filter_pruning", + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median" + } + }, + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ] + } + }, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning"] +} diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/data_pipeline.py new file mode 100644 index 00000000000..f48f80903a1 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of MobileNetV2-ATSS model for Semi-Supervised Learning Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../../base/data/semisl/base_semisl_det_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/hparam.yaml b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/hparam.yaml new file mode 100644 index 00000000000..580462daa1e --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/model.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/model.py new file mode 100644 index 00000000000..c38fbcdd179 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/semisl/model.py @@ -0,0 +1,90 @@ +"""Model configuration of MobileNetV2-ATSS model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../../recipes/stages/detection/semisl.py", + "../../../../../common/adapters/mmcv/configs/backbones/mobilenet_v2_w1.yaml", + "../../../base/models/detector.py", +] + +model = dict( + super_type="MeanTeacher", + pseudo_conf_thresh=0.25, + unlabeled_loss_weights={"cls": 0.1, "bbox": 0.0, "obj": 0.0, "centerness": 0.0}, + filter_empty_annotations=True, + visualize=False, + type="CustomATSS", + neck=dict( + type="FPN", + in_channels=[24, 32, 96, 320], + out_channels=64, + start_level=1, + add_extra_convs="on_output", + num_outs=5, + relu_before_extra_convs=True, + ), + bbox_head=dict( + type="CustomATSSHead", + num_classes=2, + in_channels=64, + stacked_convs=4, + feat_channels=64, + anchor_generator=dict( + type="AnchorGenerator", + ratios=[1.0], + octave_base_scale=8, + scales_per_octave=1, + strides=[8, 16, 32, 64, 128], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[0.1, 0.1, 0.2, 0.2], + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="GIoULoss", loss_weight=2.0), + loss_centerness=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + use_qfl=False, + ), + train_cfg=dict( + assigner=dict(type="ATSSAssigner", topk=9), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + test_cfg=dict( + nms_pre=1000, + min_bbox_size=0, + score_thr=0.05, + nms=dict(type="nms", iou_threshold=0.6), + max_per_img=100, + ), + backbone=dict( + out_indices=( + 2, + 3, + 4, + 5, + ) + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/object_detection/v2/mobilenet_v2-atss.pth" + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml new file mode 100644 index 00000000000..59fe2f3ec4d --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml @@ -0,0 +1,65 @@ +# Description. +model_template_id: Custom_Object_Detection_Gen3_ATSS +name: MobileNetV2-ATSS +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for MobileNetV2-ATSS +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + nncf: otx.algorithms.detection.adapters.mmdet.nncf.task.DetectionNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 8 + learning_rate: + default_value: 0.004 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 200 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 20.6 +size: 9.1 + +# Model spec +model_category: ACCURACY +is_default_for_task: true diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/tile_pipeline.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/tile_pipeline.py new file mode 100644 index 00000000000..c0a7ad69587 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of MobileNetV2-ATSS model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/atss_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/__init__.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/__init__.py new file mode 100644 index 00000000000..bfc664a5e8b --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of SSD model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/compression_config.json b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/compression_config.json new file mode 100644 index 00000000000..3298a831f1a --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/compression_config.json @@ -0,0 +1,77 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 864, 864] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "nncf_quantization_pruning": { + "nncf_config": { + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "initial_training_phase_epochs": 5, + "maximal_total_epochs": 100, + "patience_epochs": 5 + } + }, + "compression": [ + { + "algorithm": "filter_pruning", + "ignored_scopes": [ + "{re}CustomSingleStageDetector/CustomSSDHead\\[bbox_head\\].*" + ], + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median" + } + }, + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ] + } + }, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning"] +} diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/data_pipeline.py new file mode 100644 index 00000000000..fe689639c9b --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/data_pipeline.py @@ -0,0 +1,101 @@ +"""Data Pipeline of SSD model for Detection Task.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__dataset_type = "OTXDetDataset" +__img_size = (864, 864) +__img_norm_cfg = dict(mean=[0, 0, 0], std=[255, 255, 255], to_rgb=True) + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=True, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + to_float32=True, + enable_memcache=True, # Cache after resizing image & annotations + ), + dict( + type="PhotoMetricDistortion", + brightness_delta=32, + contrast_range=(0.5, 1.5), + saturation_range=(0.5, 1.5), + hue_delta=18, + ), + dict(type="MinIoURandomCrop", min_ious=(0.1, 0.3, 0.5, 0.7, 0.9), min_crop_size=0.1), + dict(type="Resize", img_scale=__img_size, keep_ratio=False, override=True), # Allow multiple resize + dict(type="Normalize", **__img_norm_cfg), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_size, keep_ratio=False), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/deployment.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/deployment.py new file mode 100644 index 00000000000..68378ad83d4 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/deployment.py @@ -0,0 +1,11 @@ +"""MMDeploy config of SSD model for Detection Task.""" + +_base_ = ["../../base/deployments/base_detection_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 864, 864]))], +) diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/hpo_config.yaml b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/hpo_config.yaml new file mode 100644 index 00000000000..8a5b9ff69fa --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.002 + - 0.05 + - 0.001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 6 + - 12 + - 2 diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/model.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/model.py new file mode 100644 index 00000000000..4c88b23d60a --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/model.py @@ -0,0 +1,99 @@ +"""Model Configuration of SSD model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/detection/incremental.py", + "../../../../common/adapters/mmcv/configs/backbones/mobilenet_v2_w1.yaml", + "../../base/models/single_stage_detector.py", +] + +__width_mult = 1.0 + +model = dict( + type="CustomSingleStageDetector", + bbox_head=dict( + type="CustomSSDHead", + num_classes=80, + in_channels=(int(__width_mult * 96), int(__width_mult * 320)), + use_depthwise=True, + norm_cfg=dict(type="BN"), + act_cfg=dict(type="ReLU"), + init_cfg=dict(type="Xavier", layer="Conv2d", distribution="uniform"), + loss_balancing=False, + anchor_generator=dict( + type="SSDAnchorGeneratorClustered", + strides=(16, 32), + reclustering_anchors=True, + widths=[ + [ + 38.641007923271076, + 92.49516032784699, + 271.4234764938237, + 141.53469410876247, + ], + [ + 206.04136086566515, + 386.6542727907841, + 716.9892752215089, + 453.75609561761405, + 788.4629155558277, + ], + ], + heights=[ + [ + 48.9243877087132, + 147.73088476194903, + 158.23569788707474, + 324.14510379107367, + ], + [ + 587.6216059488938, + 381.60024152086544, + 323.5988913027747, + 702.7486097568518, + 741.4865860938451, + ], + ], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=(0.0, 0.0, 0.0, 0.0), + target_stds=(0.1, 0.1, 0.2, 0.2), + ), + ), + train_cfg=dict( + assigner=dict( + pos_iou_thr=0.4, + neg_iou_thr=0.4, + ), + use_giou=False, + use_focal=False, + ), + backbone=dict( + out_indices=( + 4, + 5, + ) + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/object_detection/v2/mobilenet_v2-2s_ssd-992x736.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/__init__.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/__init__.py new file mode 100644 index 00000000000..74b1ed6349b --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of SSD model for Semi-SL Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/compression_config.json b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/compression_config.json new file mode 100644 index 00000000000..13dbf3c5ce3 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/compression_config.json @@ -0,0 +1,77 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 864, 864] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "nncf_quantization_pruning": { + "nncf_config": { + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "initial_training_phase_epochs": 5, + "maximal_total_epochs": 100, + "patience_epochs": 5 + } + }, + "compression": [ + { + "algorithm": "filter_pruning", + "ignored_scopes": [ + "{re}SingleStageDetector/SSDHead\\[bbox_head\\].*" + ], + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median" + } + }, + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ] + } + }, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning"] +} diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/data_pipeline.py new file mode 100644 index 00000000000..e3acb581abb --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of SSD model for Semi-Supervised Learning Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../../base/data/semisl/base_semisl_det_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/hparam.yaml b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/hparam.yaml new file mode 100644 index 00000000000..580462daa1e --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/model.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/model.py new file mode 100644 index 00000000000..8a3ce9fa92e --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/semisl/model.py @@ -0,0 +1,104 @@ +"""Model Configuration of SSD model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../../recipes/stages/detection/semisl.py", + "../../../../../common/adapters/mmcv/configs/backbones/mobilenet_v2_w1.yaml", + "../../../base/models/single_stage_detector.py", +] + +__width_mult = 1.0 + +model = dict( + super_type="MeanTeacher", + pseudo_conf_thresh=0.3, + unlabeled_loss_weights={"cls": 0.1, "bbox": 0.0, "obj": 0.0, "centerness": 0.0}, + filter_empty_annotations=True, + visualize=False, + type="CustomSingleStageDetector", + bbox_head=dict( + type="CustomSSDHead", + num_classes=80, + in_channels=(int(__width_mult * 96), int(__width_mult * 320)), + use_depthwise=True, + norm_cfg=dict(type="BN"), + act_cfg=dict(type="ReLU"), + init_cfg=dict(type="Xavier", layer="Conv2d", distribution="uniform"), + loss_balancing=False, + anchor_generator=dict( + type="SSDAnchorGeneratorClustered", + strides=(16, 32), + reclustering_anchors=True, + widths=[ + [ + 38.641007923271076, + 92.49516032784699, + 271.4234764938237, + 141.53469410876247, + ], + [ + 206.04136086566515, + 386.6542727907841, + 716.9892752215089, + 453.75609561761405, + 788.4629155558277, + ], + ], + heights=[ + [ + 48.9243877087132, + 147.73088476194903, + 158.23569788707474, + 324.14510379107367, + ], + [ + 587.6216059488938, + 381.60024152086544, + 323.5988913027747, + 702.7486097568518, + 741.4865860938451, + ], + ], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=(0.0, 0.0, 0.0, 0.0), + target_stds=(0.1, 0.1, 0.2, 0.2), + ), + ), + train_cfg=dict( + assigner=dict( + pos_iou_thr=0.4, + neg_iou_thr=0.4, + ), + use_giou=False, + use_focal=False, + ), + backbone=dict( + out_indices=( + 4, + 5, + ) + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/object_detection/v2/mobilenet_v2-2s_ssd-992x736.pth" + +fp16 = dict(loss_scale=512.0) +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml new file mode 100644 index 00000000000..7b517542b35 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml @@ -0,0 +1,64 @@ +# Description. +model_template_id: Custom_Object_Detection_Gen3_SSD +name: SSD +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for SSD +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + nncf: otx.algorithms.detection.adapters.mmdet.nncf.task.DetectionNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 8 + learning_rate: + default_value: 0.01 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 200 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 9.4 +size: 7.6 + +# Model spec +model_category: BALANCE diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/tile_pipeline.py b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/tile_pipeline.py new file mode 100644 index 00000000000..25754b3b6fd --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/tile_pipeline.py @@ -0,0 +1,107 @@ +"""Tiling Pipeline of SSD model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +img_size = (864, 864) + +tile_cfg = dict( + tile_size=400, min_area_ratio=0.9, overlap_ratio=0.2, iou_threshold=0.45, max_per_img=1500, filter_empty_gt=True +) + +img_norm_cfg = dict(mean=[0, 0, 0], std=[255, 255, 255], to_rgb=True) + +train_pipeline = [ + dict(type="Resize", img_scale=img_size, keep_ratio=False), + dict(type="Normalize", **img_norm_cfg), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=[ + "filename", + "ori_filename", + "ori_shape", + "img_shape", + "pad_shape", + "scale_factor", + "flip", + "flip_direction", + "img_norm_cfg", + ], + ), +] + +test_pipeline = [ + dict( + type="MultiScaleFlipAug", + img_scale=img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="Normalize", **img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ) +] + +__dataset_type = "OTXDetDataset" + +train_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + ], + ), + pipeline=train_pipeline, + **tile_cfg +) + +val_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + pipeline=[ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + ], + ), + pipeline=test_pipeline, + **tile_cfg +) + +test_dataset = dict( + type="ImageTilingDataset", + dataset=dict( + type=__dataset_type, + test_mode=True, + pipeline=[dict(type="LoadImageFromOTXDataset")], + ), + pipeline=test_pipeline, + **tile_cfg +) + + +data = dict( + train=train_dataset, + val=val_dataset, + test=test_dataset, +) diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/__init__.py b/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/__init__.py new file mode 100644 index 00000000000..a579e224816 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/__init__.py @@ -0,0 +1,3 @@ +"""Initialization of Deformable DETR for OTX Detection.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/data_pipeline.py new file mode 100644 index 00000000000..4e98e233701 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/data_pipeline.py @@ -0,0 +1,4 @@ +"""Data pipeline for Deformable DETR.""" + + +_base_ = ["../../base/data/detr_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/deployment.py b/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/deployment.py new file mode 100644 index 00000000000..76b4a6544f5 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/deployment.py @@ -0,0 +1,12 @@ +"""MMDeploy config of Deformable DETR model for Detection Task.""" + +_base_ = ["../../base/deployments/base_detection_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels"], + opset_version=16, +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 800, 1333]))], +) diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/model.py b/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/model.py new file mode 100644 index 00000000000..41a683385b7 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/model.py @@ -0,0 +1,92 @@ +"""Model config for Deformable DETR.""" +_base_ = [ + "../../../../../recipes/stages/detection/incremental.py", +] +model = dict( + type="CustomDeformableDETR", + backbone=dict( + type="ResNet", + depth=50, + num_stages=4, + out_indices=(1, 2, 3), + frozen_stages=1, + norm_cfg=dict(type="BN", requires_grad=False), + norm_eval=True, + style="pytorch", + init_cfg=dict(type="Pretrained", checkpoint="torchvision://resnet50"), + ), + neck=dict( + type="ChannelMapper", + in_channels=[512, 1024, 2048], + kernel_size=1, + out_channels=256, + act_cfg=None, + norm_cfg=dict(type="GN", num_groups=32), + num_outs=4, + ), + bbox_head=dict( + type="DeformableDETRHead", + num_query=300, + num_classes=80, + in_channels=2048, + sync_cls_avg_factor=True, + with_box_refine=True, + as_two_stage=True, + transformer=dict( + type="DeformableDetrTransformer", + encoder=dict( + type="DetrTransformerEncoder", + num_layers=6, + transformerlayers=dict( + type="BaseTransformerLayer", + attn_cfgs=dict(type="MultiScaleDeformableAttention", embed_dims=256), + feedforward_channels=1024, + ffn_dropout=0.1, + operation_order=("self_attn", "norm", "ffn", "norm"), + ), + ), + decoder=dict( + type="DeformableDetrTransformerDecoder", + num_layers=6, + return_intermediate=True, + transformerlayers=dict( + type="DetrTransformerDecoderLayer", + attn_cfgs=[ + dict(type="MultiheadAttention", embed_dims=256, num_heads=8, dropout=0.1), + dict(type="MultiScaleDeformableAttention", embed_dims=256), + ], + feedforward_channels=1024, + ffn_dropout=0.1, + operation_order=("self_attn", "norm", "cross_attn", "norm", "ffn", "norm"), + ), + ), + ), + positional_encoding=dict(type="SinePositionalEncoding", num_feats=128, normalize=True, offset=-0.5), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=2.0), + loss_bbox=dict(type="L1Loss", loss_weight=5.0), + loss_iou=dict(type="GIoULoss", loss_weight=2.0), + ), + # training and testing settings + train_cfg=dict( + assigner=dict( + type="HungarianAssigner", + cls_cost=dict(type="FocalLossCost", weight=2.0), + reg_cost=dict(type="BBoxL1Cost", weight=5.0, box_format="xywh"), + iou_cost=dict(type="IoUCost", iou_mode="giou", weight=2.0), + ) + ), + test_cfg=dict(max_per_img=100), +) +# optimizer +optimizer = dict( + _delete_=True, + type="AdamW", + lr=2e-4, + weight_decay=0.0001, +) +optimizer_config = dict(grad_clip=dict(max_norm=0.1, norm_type=2)) +load_from = "https://download.openmmlab.com/mmdetection/v2.0/deformable_detr/\ +deformable_detr_twostage_refine_r50_16x2_50e_coco/\ +deformable_detr_twostage_refine_r50_16x2_50e_coco_20210419_220613-9d28ab72.pth" +resume_from = None +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/template_experimental.yaml b/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/template_experimental.yaml new file mode 100644 index 00000000000..a306a66a3dd --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/template_experimental.yaml @@ -0,0 +1,66 @@ +# Description. +model_template_id: Object_Detection_Deformable_DETR +name: Deformable_DETR +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for Deformable_DETR +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + nncf: otx.algorithms.detection.adapters.mmdet.nncf.task.DetectionNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 2 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 2 + learning_rate: + default_value: 0.0002 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 200 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 165 +size: 157.0 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_dino/__init__.py b/src/otx/algorithms/detection/configs/detection/resnet50_dino/__init__.py new file mode 100644 index 00000000000..b44c1bf35a3 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_dino/__init__.py @@ -0,0 +1,3 @@ +"""Initialization of DETR for OTX Detection.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_dino/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/resnet50_dino/data_pipeline.py new file mode 100644 index 00000000000..3bf867788dc --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_dino/data_pipeline.py @@ -0,0 +1,4 @@ +"""Data pipeline for DINO.""" + + +_base_ = ["../../base/data/detr_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_dino/deployment.py b/src/otx/algorithms/detection/configs/detection/resnet50_dino/deployment.py new file mode 100644 index 00000000000..6e7d1fba3ed --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_dino/deployment.py @@ -0,0 +1,12 @@ +"""MMDeploy config of DINO model for Detection Task.""" + +_base_ = ["../../base/deployments/base_detection_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels"], + opset_version=16, +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 800, 1333]))], +) diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_dino/model.py b/src/otx/algorithms/detection/configs/detection/resnet50_dino/model.py new file mode 100644 index 00000000000..2761cf4b635 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_dino/model.py @@ -0,0 +1,100 @@ +"""Model config for DINO.""" +_base_ = [ + "../../../../../recipes/stages/detection/incremental.py", +] +model = dict( + type="CustomDINO", + backbone=dict( + type="ResNet", + depth=50, + num_stages=4, + out_indices=(1, 2, 3), + frozen_stages=1, + norm_cfg=dict(type="BN", requires_grad=False), + norm_eval=True, + style="pytorch", + init_cfg=dict(type="Pretrained", checkpoint="torchvision://resnet50"), + ), + neck=dict( + type="ChannelMapper", + in_channels=[512, 1024, 2048], + kernel_size=1, + out_channels=256, + act_cfg=None, + norm_cfg=dict(type="GN", num_groups=32), + num_outs=4, + ), + bbox_head=dict( + type="CustomDINOHead", + num_query=900, + num_classes=80, + in_channels=2048, + sync_cls_avg_factor=True, + with_box_refine=True, + as_two_stage=True, + transformer=dict( + type="CustomDINOTransformer", + encoder=dict( + type="DetrTransformerEncoder", + num_layers=6, + transformerlayers=dict( + type="BaseTransformerLayer", + attn_cfgs=dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "ffn", "norm"), + ), + ), + decoder=dict( + type="DINOTransformerDecoder", + num_layers=6, + return_intermediate=True, + transformerlayers=dict( + type="DetrTransformerDecoderLayer", + attn_cfgs=[ + dict(type="MultiheadAttention", embed_dims=256, num_heads=8, dropout=0.0), + dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + ], + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "cross_attn", "norm", "ffn", "norm"), + ), + ), + ), + positional_encoding=dict( + type="SinePositionalEncoding", num_feats=128, normalize=True, offset=0.0, temperature=20 + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=5.0), + loss_iou=dict(type="GIoULoss", loss_weight=2.0), + dn_cfg=dict( + label_noise_scale=0.5, + box_noise_scale=1.0, # 0.4 for DN-DETR + group_cfg=dict(dynamic=True, num_groups=None, num_dn_queries=100), + ), + ), + # training and testing settings + train_cfg=dict( + assigner=dict( + type="HungarianAssigner", + cls_cost=dict(type="FocalLossCost", weight=1.0), + reg_cost=dict(type="BBoxL1Cost", weight=5.0, box_format="xywh"), + iou_cost=dict(type="IoUCost", iou_mode="giou", weight=2.0), + ) + ), + test_cfg=dict(max_per_img=300), +) +# optimizer +optimizer = dict( + _delete_=True, + type="AdamW", + lr=1e-4, + weight_decay=0.0001, +) +optimizer_config = dict(grad_clip=dict(max_norm=0.1, norm_type=2)) +load_from = ( + "https://download.openmmlab.com/mmdetection/v3.0/dino/" + "dino-4scale_r50_8xb2-12e_coco/dino-4scale_r50_8xb2-12e_coco_20221202_182705-55b2bba2.pth" +) +resume_from = None +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_dino/template_experimental.yaml b/src/otx/algorithms/detection/configs/detection/resnet50_dino/template_experimental.yaml new file mode 100644 index 00000000000..ee0fef31702 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_dino/template_experimental.yaml @@ -0,0 +1,66 @@ +# Description. +model_template_id: Object_Detection_DINO +name: DINO +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for DINO +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + nncf: otx.algorithms.detection.adapters.mmdet.nncf.task.DetectionNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 2 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 2 + learning_rate: + default_value: 0.0001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 200 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 235 +size: 182.0 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/__init__.py b/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/__init__.py new file mode 100644 index 00000000000..6ed610c151c --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/__init__.py @@ -0,0 +1,3 @@ +"""Initialization of Lite DINO for OTX Detection.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/data_pipeline.py new file mode 100644 index 00000000000..d353a35bbaf --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/data_pipeline.py @@ -0,0 +1,4 @@ +"""Data pipeline for Lite DINO.""" + + +_base_ = ["../../base/data/detr_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/deployment.py b/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/deployment.py new file mode 100644 index 00000000000..f9f8653afc4 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/deployment.py @@ -0,0 +1,12 @@ +"""MMDeploy config of Lite DINO model for Detection Task.""" + +_base_ = ["../../base/deployments/base_detection_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels"], + opset_version=16, +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 800, 1333]))], +) diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/model.py b/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/model.py new file mode 100644 index 00000000000..07789654825 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/model.py @@ -0,0 +1,120 @@ +"""Model config for Lite DINO.""" +_base_ = [ + "../../../../../recipes/stages/detection/incremental.py", +] +model = dict( + type="CustomLiteDINO", + backbone=dict( + type="ResNet", + depth=50, + num_stages=4, + out_indices=(1, 2, 3), + frozen_stages=1, + norm_cfg=dict(type="BN", requires_grad=False), + norm_eval=True, + style="pytorch", + init_cfg=dict(type="Pretrained", checkpoint="torchvision://resnet50"), + ), + neck=dict( + type="ChannelMapper", + in_channels=[512, 1024, 2048], + kernel_size=1, + out_channels=256, + act_cfg=None, + norm_cfg=dict(type="GN", num_groups=32), + num_outs=4, + ), + bbox_head=dict( + type="CustomDINOHead", + num_query=900, + num_classes=80, + in_channels=2048, + sync_cls_avg_factor=True, + with_box_refine=True, + as_two_stage=True, + transformer=dict( + type="CustomDINOTransformer", + encoder=dict( + type="EfficientTransformerEncoder", + num_expansion=3, + enc_scale=1, + num_layers=6, + transformerlayers=[ + dict( + type="EfficientTransformerLayer", + enc_scale=1, + attn_cfgs=dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "ffn", "norm"), + ), + dict( + type="EfficientTransformerLayer", + enc_scale=1, + small_expand=True, + attn_cfgs=dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + ffn_cfgs=dict( + type="SmallExpandFFN", + embed_dims=256, + feedforward_channels=1024, + num_fcs=2, + ffn_drop=0.0, + act_cfg=dict(type="ReLU", inplace=True), + ), + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "ffn"), + ), + ], + ), + decoder=dict( + type="DINOTransformerDecoder", + num_layers=6, + return_intermediate=True, + transformerlayers=dict( + type="DetrTransformerDecoderLayer", + attn_cfgs=[ + dict(type="MultiheadAttention", embed_dims=256, num_heads=8, dropout=0.0), + dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + ], + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "cross_attn", "norm", "ffn", "norm"), + ), + ), + ), + positional_encoding=dict( + type="SinePositionalEncoding", num_feats=128, normalize=True, offset=0.0, temperature=20 + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=5.0), + loss_iou=dict(type="GIoULoss", loss_weight=2.0), + dn_cfg=dict( + label_noise_scale=0.5, + box_noise_scale=1.0, # 0.4 for DN-DETR + group_cfg=dict(dynamic=True, num_groups=None, num_dn_queries=100), + ), + ), + # training and testing settings + train_cfg=dict( + assigner=dict( + type="HungarianAssigner", + cls_cost=dict(type="FocalLossCost", weight=1.0), + reg_cost=dict(type="BBoxL1Cost", weight=5.0, box_format="xywh"), + iou_cost=dict(type="IoUCost", iou_mode="giou", weight=2.0), + ) + ), + test_cfg=dict(max_per_img=300), +) +# optimizer +optimizer = dict( + _delete_=True, + type="AdamW", + lr=1e-4, + weight_decay=0.0001, +) +optimizer_config = dict(grad_clip=dict(max_norm=0.1, norm_type=2)) +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/\ +models/object_detection/v2/lite-dino-coco.pth" +resume_from = None +ignore = False diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/template_experimental.yaml b/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/template_experimental.yaml new file mode 100644 index 00000000000..18e7963be63 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/template_experimental.yaml @@ -0,0 +1,64 @@ +# Description. +model_template_id: Object_Detection_Lite_DINO +name: Lite-DINO +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for Lite DINO +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + nncf: otx.algorithms.detection.adapters.mmdet.nncf.task.DetectionNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 4 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.0001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 200 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 140 +size: 192.0 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/__init__.py b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/__init__.py new file mode 100644 index 00000000000..1ff1ef96849 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of VFNet model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/compression_config.json b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/compression_config.json new file mode 100644 index 00000000000..a25860f8cb1 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/compression_config.json @@ -0,0 +1,41 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 1333, 800] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 1000 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 1000 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/data_pipeline.py new file mode 100644 index 00000000000..9b80fbc10bb --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/data_pipeline.py @@ -0,0 +1,71 @@ +"""Data Configuration of VFNet model for Detection Task.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +__train_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict(type="LoadAnnotationFromOTXDataset", with_bbox=True), + dict(type="MinIoURandomCrop", min_ious=(0.1, 0.3, 0.5, 0.7, 0.9), min_crop_size=0.1), + dict( + type="Resize", + img_scale=[(1344, 480), (1344, 960)], + multiscale_mode="range", + keep_ratio=False, + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] +__test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=(1344, 800), + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=__train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=__test_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=__test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/hpo_config.yaml b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/hpo_config.yaml new file mode 100644 index 00000000000..f21916e091c --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: smbo +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: quniform + range: + - 0.001 + - 0.1 + - 0.001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 2 + - 4 + - 2 diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/model.py b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/model.py new file mode 100644 index 00000000000..2dcecaba0a2 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/model.py @@ -0,0 +1,74 @@ +"""Model Configuration of VFNet model for Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/detection/incremental.py", + "../../../../common/adapters/mmcv/configs/backbones/resnet50.yaml", + "../../base/models/detector.py", +] + +model = dict( + type="CustomVFNet", + neck=dict( + type="FPN", + in_channels=[256, 512, 1024, 2048], + out_channels=256, + start_level=1, + add_extra_convs="on_output", + num_outs=5, + relu_before_extra_convs=True, + ), + bbox_head=dict( + type="CustomVFNetHead", + num_classes=2, + in_channels=256, + stacked_convs=3, + feat_channels=256, + strides=[8, 16, 32, 64, 128], + center_sampling=False, + dcn_on_last_conv=False, + use_atss=True, + use_vfl=True, + loss_cls=dict( + type="VarifocalLoss", + use_sigmoid=True, + alpha=0.75, + gamma=2.0, + iou_weighted=True, + loss_weight=1.0, + ), + loss_bbox=dict(type="GIoULoss", loss_weight=1.5), + loss_bbox_refine=dict(type="GIoULoss", loss_weight=2.0), + ), + train_cfg=dict( + assigner=dict(type="ATSSAssigner", topk=9), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + test_cfg=dict( + nms_pre=1000, + min_bbox_size=0, + score_thr=0.01, + nms=dict(type="nms", iou_threshold=0.5), + max_per_img=100, + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/object_detection/v2/resnet50-vfnet.pth" diff --git a/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/template_experimental.yaml b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/template_experimental.yaml new file mode 100644 index 00000000000..c71837d9f5a --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnet50_vfnet/template_experimental.yaml @@ -0,0 +1,54 @@ +# Description. +model_template_id: Custom_Object_Detection_Gen3_VFNet +name: VFNet +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for VFNet +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 4 + inference_batch_size: + default_value: 4 + learning_rate: + default_value: 0.001 + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 100 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 457.4 +size: 126.0 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/__init__.py b/src/otx/algorithms/detection/configs/detection/resnext101_atss/__init__.py new file mode 100644 index 00000000000..6d040468c22 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of ResNeXt101-ATSS model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/compression_config.json b/src/otx/algorithms/detection/configs/detection/resnext101_atss/compression_config.json new file mode 100644 index 00000000000..7936d13ac21 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/compression_config.json @@ -0,0 +1,74 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 992, 736] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "nncf_quantization_pruning": { + "nncf_config": { + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "initial_training_phase_epochs": 5, + "maximal_total_epochs": 100, + "patience_epochs": 5 + } + }, + "compression": [ + { + "algorithm": "filter_pruning", + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median" + } + }, + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ] + } + }, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning"] +} diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/resnext101_atss/data_pipeline.py new file mode 100644 index 00000000000..c10c74971ac --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of ResNeXt101-ATSS model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/atss_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/deployment.py b/src/otx/algorithms/detection/configs/detection/resnext101_atss/deployment.py new file mode 100644 index 00000000000..8a57fec8766 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/deployment.py @@ -0,0 +1,14 @@ +"""MMDeploy config of ResNeXt101-ATSS model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +_base_ = ["../../base/deployments/base_detection_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 736, 992]))], +) diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/hpo_config.yaml b/src/otx/algorithms/detection/configs/detection/resnext101_atss/hpo_config.yaml new file mode 100644 index 00000000000..8df3d6be9dd --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0004 + - 0.04 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 4 + - 16 + - 2 diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/model.py b/src/otx/algorithms/detection/configs/detection/resnext101_atss/model.py new file mode 100644 index 00000000000..6d02dd868ab --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/model.py @@ -0,0 +1,83 @@ +"""Model configuration of ResNeXt101-ATSS model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/detection/incremental.py", + "../../base/models/detector.py", +] + +model = dict( + type="CustomATSS", + backbone=dict( + type="ResNeXt", + depth=101, + groups=64, + base_width=4, + num_stages=4, + out_indices=(0, 1, 2, 3), + frozen_stages=1, + norm_cfg=dict(type="BN", requires_grad=True), + style="pytorch", + init_cfg=dict(type="Pretrained", checkpoint="open-mmlab://resnext101_64x4d"), + ), + neck=dict( + type="FPN", + in_channels=[256, 512, 1024, 2048], + out_channels=256, + start_level=1, + add_extra_convs="on_output", + num_outs=5, + relu_before_extra_convs=True, + ), + bbox_head=dict( + type="CustomATSSHead", + num_classes=2, + in_channels=256, + stacked_convs=4, + feat_channels=256, + anchor_generator=dict( + type="AnchorGenerator", + ratios=[1.0], + octave_base_scale=8, + scales_per_octave=1, + strides=[8, 16, 32, 64, 128], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[0.1, 0.1, 0.2, 0.2], + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="GIoULoss", loss_weight=2.0), + loss_centerness=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + use_qfl=False, + qfl_cfg=dict( + type="QualityFocalLoss", + use_sigmoid=True, + beta=2.0, + loss_weight=1.0, + ), + ), + train_cfg=dict( + assigner=dict(type="ATSSAssigner", topk=9), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + test_cfg=dict( + nms_pre=1000, + min_bbox_size=0, + score_thr=0.05, + nms=dict(type="nms", iou_threshold=0.6), + max_per_img=100, + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/\ +models/object_detection/v2/resnext101_atss_070623.pth" + +fp16 = dict(loss_scale=512.0, bf16_training=False) diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/__init__.py b/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/__init__.py new file mode 100644 index 00000000000..17d44dd3afd --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of ResNeXt101-ATSS model for Semi-SL Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/compression_config.json b/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/compression_config.json new file mode 100644 index 00000000000..7936d13ac21 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/compression_config.json @@ -0,0 +1,74 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 992, 736] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "nncf_quantization_pruning": { + "nncf_config": { + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "initial_training_phase_epochs": 5, + "maximal_total_epochs": 100, + "patience_epochs": 5 + } + }, + "compression": [ + { + "algorithm": "filter_pruning", + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median" + } + }, + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 300 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 300 + } + } + } + ] + } + }, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning"] +} diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/data_pipeline.py b/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/data_pipeline.py new file mode 100644 index 00000000000..06dc8432d1e --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of ResNeXt101-ATSS model for Semi-Supervised Learning Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../../base/data/semisl/base_semisl_det_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/hparam.yaml b/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/hparam.yaml new file mode 100644 index 00000000000..580462daa1e --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/model.py b/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/model.py new file mode 100644 index 00000000000..cfa32cc0d9e --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/semisl/model.py @@ -0,0 +1,87 @@ +"""Model configuration of ResNeXt101-ATSS model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../../recipes/stages/detection/semisl.py", + "../../../base/models/detector.py", +] + +model = dict( + super_type="MeanTeacher", + pseudo_conf_thresh=0.25, + unlabeled_loss_weights={"cls": 0.1, "bbox": 0.0, "obj": 0.0, "centerness": 0.0}, + filter_empty_annotations=True, + type="CustomATSS", + backbone=dict( + type="ResNeXt", + depth=101, + groups=64, + base_width=4, + num_stages=4, + out_indices=(0, 1, 2, 3), + frozen_stages=1, + norm_cfg=dict(type="BN", requires_grad=True), + style="pytorch", + init_cfg=dict(type="Pretrained", checkpoint="open-mmlab://resnext101_64x4d"), + ), + neck=dict( + type="FPN", + in_channels=[256, 512, 1024, 2048], + out_channels=256, + start_level=1, + add_extra_convs="on_output", + num_outs=5, + relu_before_extra_convs=True, + ), + bbox_head=dict( + type="CustomATSSHead", + num_classes=2, + in_channels=256, + stacked_convs=4, + feat_channels=256, + anchor_generator=dict( + type="AnchorGenerator", + ratios=[1.0], + octave_base_scale=8, + scales_per_octave=1, + strides=[8, 16, 32, 64, 128], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[0.1, 0.1, 0.2, 0.2], + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="GIoULoss", loss_weight=2.0), + loss_centerness=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + use_qfl=False, + qfl_cfg=dict( + type="QualityFocalLoss", + use_sigmoid=True, + beta=2.0, + loss_weight=1.0, + ), + ), + train_cfg=dict( + assigner=dict(type="ATSSAssigner", topk=9), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + test_cfg=dict( + nms_pre=1000, + min_bbox_size=0, + score_thr=0.05, + nms=dict(type="nms", iou_threshold=0.6), + max_per_img=100, + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions/\ +models/object_detection/v2/resnext101_atss_070623.pth" + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml b/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml new file mode 100644 index 00000000000..79308f5388a --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml @@ -0,0 +1,65 @@ +# Description. +model_template_id: Object_Detection_ResNeXt101_ATSS +name: ResNeXt101-ATSS +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Object Detection for ResNeXt101-ATSS +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 4 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 4 + learning_rate: + default_value: 0.004 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 3 + num_iters: + default_value: 200 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: true + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 434.75 +size: 344 +# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. +# inference_targets: +# - CPU +# - GPU +# - VPU diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/tile_pipeline.py b/src/otx/algorithms/detection/configs/detection/resnext101_atss/tile_pipeline.py new file mode 100644 index 00000000000..85482e6dd89 --- /dev/null +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of ResNeXt101-ATSS model for Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/atss_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/__init__.py b/src/otx/algorithms/detection/configs/instance_segmentation/__init__.py new file mode 100644 index 00000000000..1c0c29ef3d6 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/__init__.py @@ -0,0 +1,15 @@ +"""Instance-Segmentation Models configurations.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml new file mode 100644 index 00000000000..59cef3af6c5 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml @@ -0,0 +1,701 @@ +description: Configuration for an instance segmentation task +header: Configuration for an instance segmentation task +learning_parameters: + batch_size: + affects_outcome_of: TRAINING + default_value: 5 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 5 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + inference_batch_size: + affects_outcome_of: TRAINING + default_value: 1 + description: The number of samples seen in each iteration of inference. + Increasing this value improves inference time and may make the inference more + stable. A larger batch size has higher memory requirements. + editable: true + header: Inference batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + description: Learning Parameters + header: Learning Parameters + learning_rate: + affects_outcome_of: TRAINING + default_value: 0.01 + description: + Increasing this value will speed up training convergence but might + make it unstable. + editable: true + header: Learning rate + max_value: 0.1 + min_value: 1.0e-07 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.01 + visible_in_ui: true + warning: null + auto_hpo_state: NOT_POSSIBLE + learning_rate_warmup_iters: + affects_outcome_of: TRAINING + default_value: 100 + description: + In this periods of initial training iterations, the model will be trained in low learning rate, + which will be increased incrementally up to the expected learning rate setting. + This warm-up phase is known to be helpful to stabilize training, thus result in better performance. + editable: true + header: Number of iterations for learning rate warmup + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: null + num_iters: + affects_outcome_of: TRAINING + default_value: 1 + description: + Increasing this value causes the results to be more robust but training + time will be longer. + editable: true + header: Number of training iterations + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: null + num_workers: + affects_outcome_of: NONE + default_value: 2 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of cpu threads to use during batch generation + max_value: 8 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 2 + visible_in_ui: true + warning: null + enable_early_stopping: + affects_outcome_of: TRAINING + default_value: true + description: Early exit from training when validation accuracy isn't changed or decreased for several epochs. + editable: true + header: Enable early stopping of the training + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + early_stop_start: + affects_outcome_of: TRAINING + default_value: 3 + editable: true + header: Start epoch for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 3 + visible_in_ui: false + early_stop_patience: + affects_outcome_of: TRAINING + default_value: 10 + description: Training will stop if the model does not improve within the number of epochs of patience. + editable: true + header: Patience for early stopping + max_value: 50 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 10 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + early_stop_iteration_patience: + affects_outcome_of: TRAINING + default_value: 0 + description: + Training will stop if the model does not improve within the number of iterations of patience. + This ensures the model is trained enough with the number of iterations of patience before early stopping. + editable: true + header: Iteration patience for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + use_adaptive_interval: + affects_outcome_of: TRAINING + default_value: true + description: Depending on the size of iteration per epoch, adaptively update the validation interval and related values. + editable: true + header: Use adaptive validation interval + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: This will automatically control the patience and interval when early stopping is enabled. + auto_adapt_batch_size: + affects_outcome_of: TRAINING + default_value: Safe + description: Safe => Prevent GPU out of memory. Full => Find a batch size using most of GPU memory. + editable: true + enum_name: BatchSizeAdaptType + header: Decrease batch size if current batch size isn't fit to CUDA memory. + options: + NONE: "None" + SAFE: "Safe" + FULL: "Full" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Safe + visible_in_ui: true + warning: + Enabling this could change the actual batch size depending on the current GPU status. + The learning rate also could be adjusted according to the adapted batch size. This process might change + a model performance and take some extra computation time to try a few batch size candidates. + auto_num_workers: + affects_outcome_of: TRAINING + default_value: false + description: Adapt num_workers according to current hardware status automatically. + editable: true + header: Enable auto adaptive num_workers + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + input_size: + affects_outcome_of: INFERENCE + default_value: Default + description: + The input size of the given model could be configured to one of the predefined resolutions. + Reduced training and inference time could be expected by using smaller input size. + In Auto mode, the input size is automatically determined based on dataset statistics. + Defaults to per-model default resolution. + editable: true + enum_name: InputSizePreset + header: Configure model input size. + options: + DEFAULT: "Default" + AUTO: "Auto" + _256x256: "256x256" + _384x384: "384x384" + _512x512: "512x512" + _768x768: "768x768" + _1024x1024: "1024x1024" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Default + visible_in_ui: false + warning: Modifying input size may decrease model performance. + type: PARAMETER_GROUP + visible_in_ui: true +postprocessing: + confidence_threshold: + affects_outcome_of: INFERENCE + default_value: 0.35 + description: + This threshold only takes effect if the threshold is not set based + on the result. + editable: true + header: Confidence threshold + max_value: 1 + min_value: 0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + # value: 0.35 + value: 0.01 + visible_in_ui: true + warning: null + max_num_detections: + affects_outcome_of: INFERENCE + default_value: 0 + description: + Extra detection outputs will be discared in non-maximum suppression process. + Defaults to 0, which means per-model default values. + editable: true + header: Maximum number of detections per image + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: null + use_ellipse_shapes: + affects_outcome_of: INFERENCE + default_value: false + description: Use direct ellipse shape in inference instead of polygon from mask + editable: true + header: Use ellipse shapes + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + description: Postprocessing + header: Postprocessing + result_based_confidence_threshold: + affects_outcome_of: INFERENCE + default_value: true + description: Confidence threshold is derived from the results + editable: true + header: Result based confidence threshold + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +algo_backend: + description: parameters for algo backend + header: Algo backend parameters + train_type: + affects_outcome_of: TRAINING + default_value: Incremental + description: Training scheme option that determines how to train the model + editable: True + enum_name: TrainType + header: Train type + options: + Incremental: "Incremental" + Semisupervised: "Semisupervised" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Incremental + visible_in_ui: false + warning: null + mem_cache_size: + affects_outcome_of: TRAINING + default_value: 1000000000 + description: Size of memory pool for caching decoded data to load data faster (bytes). + editable: true + header: Size of memory pool + max_value: 9223372036854775807 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + storage_cache_scheme: + affects_outcome_of: TRAINING + default_value: NONE + description: Scheme for storage cache + editable: true + enum_name: StorageCacheScheme + header: Scheme for storage cache + options: + NONE: "NONE" + AS_IS: "AS-IS" + JPEG_75: "JPEG/75" + JPEG_95: "JPEG/95" + PNG: "PNG" + TIFF: "TIFF" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: True + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: True + warning: null + stat_subset_size: + affects_outcome_of: NONE + default_value: 300 + description: Number of data samples used for post-training optimization + editable: True + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: True + warning: null + stat_requests_number: + affects_outcome_of: NONE + default_value: 0 + description: Number of requests during statistics collection + editable: true + header: Number of requests + max_value: 200 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + header: Optimization by NNCF + enable_quantization: + affects_outcome_of: INFERENCE + default_value: True + description: Enable quantization algorithm + editable: false + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: false + warning: null + enable_pruning: + affects_outcome_of: INFERENCE + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + pruning_supported: + affects_outcome_of: TRAINING + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + maximal_accuracy_degradation: + affects_outcome_of: NONE + default_value: 1.0 + description: The maximal allowed accuracy metric drop in absolute values + editable: True + header: Maximum accuracy degradation + max_value: 100.0 + min_value: 0.0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: True + warning: null + type: PARAMETER_GROUP + visible_in_ui: True + +tiling_parameters: + header: Tiling + description: Crop dataset to tiles + + enable_tiling: + header: Enable tiling + description: Set to True to allow tiny objects to be better detected. + default_value: false + editable: true + affects_outcome_of: TRAINING + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: "Tiling trades off speed for accuracy as it increases the number of images to be processed. In turn, it's memory efficient as smaller resolution patches are handled at onces so that the possibility of OOM issues could be reduced. Important: In the current version, depending on the dataset size and the available hardware resources, a model may not train successfully when tiling is enabled." + + enable_tile_classifier: + header: Enable tile classifier + description: Enabling tile classifier enhances the speed of tiling inference by incorporating a tile classifier into the instance segmentation model. This feature prevents the detector from making predictions on tiles that do not contain any objects, thus optimizing its speed performance. + default_value: false + editable: true + affects_outcome_of: TRAINING + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: The tile classifier prioritizes inference speed over training speed, it requires more training in order to achieve its optimized performance. + + enable_adaptive_params: + header: Enable adaptive tiling parameters + description: Config tile size and tile overlap adaptively based on annotated dataset statistic. Manual settings well be ignored if it's turned on. Please turn off this option in order to tune tiling parameters manually. + default_value: true + editable: true + affects_outcome_of: TRAINING + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + + tile_size: + header: Tile Image Size + description: Tile image size. (tile_size x tile_size) sub images will be the unit of computation. + affects_outcome_of: TRAINING + default_value: 400 + min_value: 100 + max_value: 4096 + type: INTEGER + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 400 + visible_in_ui: true + warning: null + + tile_overlap: + header: Tile Overlap + description: Overlap ratio between each two neighboring tiles. Recommend to set as large_object_size / tile_size. + affects_outcome_of: TRAINING + default_value: 0.2 + min_value: 0.0 + max_value: 0.9 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.2 + visible_in_ui: true + warning: null + + tile_max_number: + header: Max object per tile + description: Maximum number of objects per tile + affects_outcome_of: TRAINING + default_value: 1500 + min_value: 1 + max_value: 5000 + type: INTEGER + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1500 + visible_in_ui: true + warning: null + + tile_ir_scale_factor: + header: OpenVINO IR Scale Factor + description: The purpose of the scale parameter is to optimize the performance and efficiency of tiling in OpenVINO IR during inference. By controlling the increase in tile size and input size, the scale parameter allows for more efficient parallelization of the workload and improve the overall performance and efficiency of the inference process on OpenVINO. + affects_outcome_of: TRAINING + default_value: 1.0 + min_value: 1.0 + max_value: 4.0 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: true + warning: null + + tile_sampling_ratio: + header: Sampling Ratio for entire tiling + description: Since tiling train and validation to all tile from large image, usually it takes lots of time than normal training. The tile_sampling_ratio is ratio for sampling entire tile dataset. Sampling tile dataset would save lots of time for training and validation time. Note that sampling will be applied to training and validation dataset, not test dataset. + affects_outcome_of: TRAINING + default_value: 1.0 + min_value: 0.000001 + max_value: 1.0 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: true + warning: null + + object_tile_ratio: + header: Object tile ratio + description: The desired ratio of min object size and tile size. + affects_outcome_of: TRAINING + default_value: 0.03 + min_value: 0.00 + max_value: 1.00 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.03 + visible_in_ui: false + warning: null + + type: PARAMETER_GROUP + visible_in_ui: true diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/__init__.py b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/__init__.py new file mode 100644 index 00000000000..66566861e6a --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of ConvNeXt-T-MaskRCNN model for Instance-Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/compression_config.json b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/compression_config.json new file mode 100644 index 00000000000..c88a5c2744e --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/compression_config.json @@ -0,0 +1,46 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 1024, 1024] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "type": "percentile", + "params": { + "min_percentile": 0, + "max_percentile": 100 + }, + "num_init_samples": 1000 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 1000 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/data_pipeline.py b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/data_pipeline.py new file mode 100644 index 00000000000..8f29acbae89 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/data_pipeline.py @@ -0,0 +1,80 @@ +"""Data Pipeline of ConvNeXt model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_size = (1024, 1024) + +# TODO: A comparison experiment is needed to determine which value is appropriate for to_rgb. +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +train_pipeline = [ + dict(type="LoadImageFromOTXDataset", enable_memcache=True), + dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + dict(type="Resize", img_scale=__img_size, keep_ratio=False), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/deployment.py b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/deployment.py new file mode 100644 index 00000000000..2bde65b8647 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/deployment.py @@ -0,0 +1,9 @@ +"""MMDployment config of Resnet model for Instance-Seg Task.""" + +_base_ = ["../../base/deployments/base_instance_segmentation_dynamic.py"] + +scale_ir_input = True + +ir_config = dict( + output_names=["boxes", "labels", "masks"], +) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/deployment_tile_classifier.py b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/deployment_tile_classifier.py new file mode 100644 index 00000000000..e0a36a5bae5 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/deployment_tile_classifier.py @@ -0,0 +1,22 @@ +"""MMDeploy config partitioning ConvNeXt-T MaskRCNN model to tile classifier and MaskRCNN model.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +_base_ = ["./deployment.py"] + +ir_config = dict( + output_names=["boxes", "labels", "masks", "tile_prob"], +) + +partition_config = dict( + type="tile_classifier", + apply_marks=True, + partition_cfg=[ + dict( + save_file="tile_classifier.onnx", + start=["tile_classifier:input"], + end=["tile_classifier:output"], + output_names=["tile_prob"], + ) + ], +) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/hpo_config.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/hpo_config.yaml new file mode 100644 index 00000000000..03e00764354 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0001 + - 0.01 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 2 + - 6 + - 2 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/model.py b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/model.py new file mode 100644 index 00000000000..a0069179b4f --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/model.py @@ -0,0 +1,131 @@ +"""Model configuration of ConvNeXt-T-MaskRCNN model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/instance_segmentation/incremental.py", + "../../base/models/detector.py", +] + +task = "instance-segmentation" + +model = dict( + type="CustomMaskRCNN", + backbone=dict( + type="mmcls.ConvNeXt", + arch="tiny", + out_indices=[0, 1, 2, 3], + drop_path_rate=0.4, + layer_scale_init_value=1.0, + gap_before_final_norm=False, + ), + neck=dict(type="FPN", in_channels=[96, 192, 384, 768], out_channels=256, num_outs=5), + rpn_head=dict( + type="RPNHead", + in_channels=256, + feat_channels=256, + anchor_generator=dict(type="AnchorGenerator", scales=[8], ratios=[0.5, 1.0, 2.0], strides=[4, 8, 16, 32, 64]), + bbox_coder=dict(type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[1.0, 1.0, 1.0, 1.0]), + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + roi_head=dict( + type="CustomRoIHead", + bbox_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=7, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + bbox_head=dict( + type="Shared2FCBBoxHead", + in_channels=256, + fc_out_channels=1024, + roi_feat_size=7, + num_classes=80, + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[0.1, 0.1, 0.2, 0.2] + ), + reg_class_agnostic=False, + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + mask_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=14, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + mask_head=dict( + type="CustomFCNMaskHead", + num_convs=4, + in_channels=256, + conv_out_channels=256, + num_classes=80, + loss_mask=dict(type="CrossEntropyLoss", use_mask=True, loss_weight=1.0), + ), + ), + train_cfg=dict( + rpn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.7, + neg_iou_thr=0.3, + min_pos_iou=0.3, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict(type="RandomSampler", num=256, pos_fraction=0.5, neg_pos_ub=-1, add_gt_as_proposals=False), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + rpn_proposal=dict( + nms_across_levels=False, + nms_pre=2000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.7), + min_bbox_size=0, + ), + rcnn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict(type="RandomSampler", num=512, pos_fraction=0.25, neg_pos_ub=-1, add_gt_as_proposals=True), + mask_size=28, + pos_weight=-1, + debug=False, + ), + ), + test_cfg=dict( + rpn=dict( + nms_across_levels=False, + nms_pre=1000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.7), + min_bbox_size=0, + ), + rcnn=dict(score_thr=0.05, nms=dict(type="nms", iou_threshold=0.5), max_per_img=100, mask_thr_binary=0.5), + ), +) + +load_from = "https://storage.openvinotoolkit.org/\ +repositories/openvino_training_extensions/\ +models/instance_segmentation/\ +mask_rcnn_convnext-t_p4_w7_fpn_fp16.pth" + +evaluation = dict(interval=1, metric="mAP", save_best="mAP", iou_thr=[0.5]) +ignore = True + +custom_imports = dict(imports=["mmcls.models"], allow_failed_imports=False) +fp16 = dict(loss_scale=dict(init_scale=512.0)) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/template_experimental.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/template_experimental.yaml new file mode 100644 index 00000000000..b12d8f7e5fb --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/template_experimental.yaml @@ -0,0 +1,63 @@ +# Description. +model_template_id: Custom_Counting_Instance_Segmentation_MaskRCNN_ConvNeXt +name: MaskRCNN-ConvNeXt +task_type: INSTANCE_SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Instance Segmentation for MaskRCNN-ConvNeXt +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 2 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 1 + learning_rate: + default_value: 0.001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 100 + pot_parameters: + stat_requests_number: + default_value: 1 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 266.78 +size: 192.4 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/tile_pipeline.py b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/tile_pipeline.py new file mode 100644 index 00000000000..6ccc6d22401 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of ConvNeXt model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/base_iseg_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/__init__.py b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/__init__.py new file mode 100644 index 00000000000..efd9f577a1d --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of EfficientNetB2B-MaskRCNN model for Instance-Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/compression_config.json b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/compression_config.json new file mode 100644 index 00000000000..c88a5c2744e --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/compression_config.json @@ -0,0 +1,46 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 1024, 1024] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "type": "percentile", + "params": { + "min_percentile": 0, + "max_percentile": 100 + }, + "num_init_samples": 1000 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 1000 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/data_pipeline.py b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/data_pipeline.py new file mode 100644 index 00000000000..50509040a12 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of EfficientNetB2B model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/iseg_efficientnet_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/data_pipeline_xpu.py b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/data_pipeline_xpu.py new file mode 100644 index 00000000000..b6b58f84442 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/data_pipeline_xpu.py @@ -0,0 +1,7 @@ +"""Data Pipeline of EfficientNetB2B model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/iseg_efficientnet_data_pipeline_xpu.py"] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/deployment.py b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/deployment.py new file mode 100644 index 00000000000..96d895a9255 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/deployment.py @@ -0,0 +1,9 @@ +"""MMDeploy config of EfficientNetB2B model for Instance-Seg Task.""" + +_base_ = ["../../base/deployments/base_instance_segmentation_dynamic.py"] + +scale_ir_input = True + +ir_config = dict( + output_names=["boxes", "labels", "masks"], +) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/deployment_tile_classifier.py b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/deployment_tile_classifier.py new file mode 100644 index 00000000000..9df2fca9dc0 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/deployment_tile_classifier.py @@ -0,0 +1,23 @@ +"""MMDeploy config partitioning EfficientNetB2B MaskRCNN model to tile classifier and MaskRCNN model.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +_base_ = ["./deployment.py"] + +ir_config = dict( + output_names=["boxes", "labels", "masks", "tile_prob"], +) + +partition_config = dict( + type="tile_classifier", + apply_marks=True, + partition_cfg=[ + dict( + save_file="tile_classifier.onnx", + start=["tile_classifier:input"], + end=["tile_classifier:output"], + output_names=["tile_prob"], + ) + ], +) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/hpo_config.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/hpo_config.yaml new file mode 100644 index 00000000000..e2d5ccb9a00 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.003 + - 0.075 + - 0.001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 2 + - 6 + - 2 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/model.py b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/model.py new file mode 100644 index 00000000000..a7aaa5ca571 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/model.py @@ -0,0 +1,131 @@ +"""Model configuration of EfficientNetB2B-MaskRCNN model for Instance-Seg Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/instance_segmentation/incremental.py", + "../../../../common/adapters/mmcv/configs/backbones/efficientnet_b2b.yaml", + "../../base/models/detector.py", +] + +task = "instance-segmentation" + +model = dict( + type="CustomMaskRCNN", # Use CustomMaskRCNN for Incremental Learning + neck=dict(type="FPN", in_channels=[24, 48, 120, 352], out_channels=80, num_outs=5), + rpn_head=dict( + type="CustomRPNHead", + in_channels=80, + feat_channels=80, + anchor_generator=dict(type="AnchorGenerator", scales=[8], ratios=[0.5, 1.0, 2.0], strides=[4, 8, 16, 32, 64]), + bbox_coder=dict(type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[1.0, 1.0, 1.0, 1.0]), + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + roi_head=dict( + type="CustomRoIHead", # Use CustomROIHead for Ignore mode + bbox_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=7, sampling_ratio=0), + out_channels=80, + featmap_strides=[4, 8, 16, 32], + ), + bbox_head=dict( + type="Shared2FCBBoxHead", + in_channels=80, + fc_out_channels=1024, + roi_feat_size=7, + num_classes=80, + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[0.1, 0.1, 0.2, 0.2] + ), + reg_class_agnostic=False, + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + mask_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=14, sampling_ratio=0), + out_channels=80, + featmap_strides=[4, 8, 16, 32], + ), + mask_head=dict( + type="CustomFCNMaskHead", + num_convs=4, + in_channels=80, + conv_out_channels=80, + num_classes=80, + loss_mask=dict(type="CrossEntropyLoss", use_mask=True, loss_weight=1.0), + ), + ), + train_cfg=dict( + rpn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.7, + neg_iou_thr=0.3, + min_pos_iou=0.3, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict(type="RandomSampler", num=256, pos_fraction=0.5, neg_pos_ub=-1, add_gt_as_proposals=False), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + rpn_proposal=dict( + nms_across_levels=False, + nms_pre=2000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.8), + min_bbox_size=0, + ), + rcnn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict(type="RandomSampler", num=256, pos_fraction=0.25, neg_pos_ub=-1, add_gt_as_proposals=True), + mask_size=28, + pos_weight=-1, + debug=False, + ), + ), + test_cfg=dict( + rpn=dict( + nms_across_levels=False, + nms_pre=800, + max_per_img=500, + nms=dict(type="nms", iou_threshold=0.8), + min_bbox_size=0, + ), + rcnn=dict(score_thr=0.05, nms=dict(type="nms", iou_threshold=0.7), max_per_img=500, mask_thr_binary=0.5), + ), +) +load_from = "https://storage.openvinotoolkit.org/repositories/\ +openvino_training_extensions/models/instance_segmentation/\ +v2/efficientnet_b2b-mask_rcnn-576x576.pth" + +evaluation = dict(interval=1, metric="mAP", save_best="mAP", iou_thr=[0.5]) +fp16 = dict(loss_scale=512.0, bf16_training=False) +ignore = True diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/__init__.py b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/__init__.py new file mode 100644 index 00000000000..abad44e5db3 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of Mask-RCNN EfficientNetb2b model for Semi-SL Instance Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/compression_config.json b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/compression_config.json new file mode 100644 index 00000000000..7e0cba46aa9 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/compression_config.json @@ -0,0 +1,41 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 1024, 1024] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 1000 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 1000 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/data_pipeline.py b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/data_pipeline.py new file mode 100644 index 00000000000..9bca72665e9 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/data_pipeline.py @@ -0,0 +1,6 @@ +"""Data Pipeline of Mask-RCNN EfficientNetb2b model for Semi-Supervised Learning Instance Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +_base_ = ["../../../base/data/semisl/semisl_is_eff_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/hparam.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/hparam.yaml new file mode 100644 index 00000000000..dc1c58367fe --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/hparam.yaml @@ -0,0 +1,13 @@ +# Hyperparameters. +# since we use repeat dataset for semi-sl +# -> change iteration related parameters +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised + learning_parameters: + num_iters: + default_value: 200 # it is found that sometimes it takes more epochs to train the semi-sl approach + early_stop_start: + default_value: 7 # when unlabeled branch enabled diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/model.py b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/model.py new file mode 100644 index 00000000000..17dae156d2b --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/semisl/model.py @@ -0,0 +1,127 @@ +"""Model Configuration of Mask-RCNN EfficientNetb2b model for Semi-Supervised Learning Instance Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../../recipes/stages/instance_segmentation/semisl.py", + "../../../../../common/adapters/mmcv/configs/backbones/efficientnet_b2b.yaml", + "../../../base/models/detector.py", +] + +task = "instance-segmentation" + +model = dict( + super_type="MeanTeacher", + pseudo_conf_thresh=0.7, + unlabeled_loss_weights={"cls": 0.1, "bbox": 0.1, "mask": 0.1}, + type="CustomMaskRCNN", + neck=dict(type="FPN", in_channels=[24, 48, 120, 352], out_channels=80, num_outs=5), + rpn_head=dict( + type="RPNHead", + in_channels=80, + feat_channels=80, + anchor_generator=dict(type="AnchorGenerator", scales=[8], ratios=[0.5, 1.0, 2.0], strides=[4, 8, 16, 32, 64]), + bbox_coder=dict(type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[1.0, 1.0, 1.0, 1.0]), + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + roi_head=dict( + type="CustomRoIHead", # Use CustomROIHead for Ignore mode + bbox_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=7, sampling_ratio=0), + out_channels=80, + featmap_strides=[4, 8, 16, 32], + ), + bbox_head=dict( + type="Shared2FCBBoxHead", + in_channels=80, + fc_out_channels=1024, + roi_feat_size=7, + num_classes=80, + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[0.1, 0.1, 0.2, 0.2] + ), + reg_class_agnostic=False, + loss_cls=dict(type="OrdinaryFocalLoss", gamma=1.5, loss_weight=1.0), + loss_bbox=dict(type="SmoothL1Loss", beta=1.0, loss_weight=1.0), + ), + mask_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=14, sampling_ratio=0), + out_channels=80, + featmap_strides=[4, 8, 16, 32], + ), + mask_head=dict( + type="CustomFCNMaskHead", + num_convs=4, + in_channels=80, + conv_out_channels=80, + num_classes=80, + loss_mask=dict(type="CrossEntropyLoss", use_mask=True, loss_weight=1.0), + ), + ), + train_cfg=dict( + rpn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.7, + neg_iou_thr=0.3, + min_pos_iou=0.3, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict(type="RandomSampler", num=256, pos_fraction=0.5, neg_pos_ub=-1, add_gt_as_proposals=False), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + rpn_proposal=dict( + nms_across_levels=False, + nms_pre=2000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.8), + min_bbox_size=0, + ), + rcnn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict(type="RandomSampler", num=256, pos_fraction=0.25, neg_pos_ub=-1, add_gt_as_proposals=True), + mask_size=28, + pos_weight=-1, + debug=False, + ), + ), + test_cfg=dict( + rpn=dict( + nms_across_levels=False, + nms_pre=800, + max_per_img=500, + nms=dict(type="nms", iou_threshold=0.8), + min_bbox_size=0, + ), + rcnn=dict(score_thr=0.05, nms=dict(type="nms", iou_threshold=0.7), max_per_img=500, mask_thr_binary=0.5), + ), +) +load_from = "https://storage.openvinotoolkit.org/repositories/\ +openvino_training_extensions/models/instance_segmentation/\ +v2/efficientnet_b2b-mask_rcnn-576x576.pth" + +evaluation = dict(interval=1, metric="mAP", save_best="mAP", iou_thr=[0.5]) +fp16 = dict(loss_scale=512.0) +ignore = True + +custom_hooks = [ + dict(type="MeanTeacherHook", epoch_momentum=0.0, start_epoch=5, momentum=0.0004), +] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/template.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/template.yaml new file mode 100644 index 00000000000..f825fbac61d --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/template.yaml @@ -0,0 +1,67 @@ +# Description. +model_template_id: Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B +name: MaskRCNN-EfficientNetB2B +task_type: INSTANCE_SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Instance Segmentation for MaskRCNN-EfficientNetB2B +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + nncf: otx.algorithms.detection.adapters.mmdet.nncf.task.DetectionNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 4 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 1 + learning_rate: + default_value: 0.015 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 100 + pot_parameters: + stat_requests_number: + default_value: 1 + nncf_optimization: + enable_quantization: + default_value: false + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 68.48 +size: 13.27 + +# Model spec +model_category: SPEED diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/tile_pipeline.py b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/tile_pipeline.py new file mode 100644 index 00000000000..fb25aab8478 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of EfficientNetB2B model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/efficientnet_iseg_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/__init__.py b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/__init__.py new file mode 100644 index 00000000000..401233b9ee7 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of MaskRCNN-SwinTransformer model for Instance-Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/compression_config.json b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/compression_config.json new file mode 100644 index 00000000000..c8b0d164aa9 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/compression_config.json @@ -0,0 +1,46 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 1344, 1344] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "type": "percentile", + "params": { + "min_percentile": 0, + "max_percentile": 100 + }, + "num_init_samples": 1000 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 1000 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/data_pipeline.py b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/data_pipeline.py new file mode 100644 index 00000000000..3083f519db8 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/data_pipeline.py @@ -0,0 +1,108 @@ +"""Data Pipeline of MaskRCNN-SwinT-FP16 model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +__img_size = (1344, 1344) + +meta_keys = [ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", +] + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=meta_keys, + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_size, keep_ratio=False), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/data_pipeline_xpu.py b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/data_pipeline_xpu.py new file mode 100644 index 00000000000..6ff89c87dc6 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/data_pipeline_xpu.py @@ -0,0 +1,108 @@ +"""Data Pipeline of MaskRCNN-SwinT-FP16 model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +__img_size = (1344, 1344) + +meta_keys = [ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", +] + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=True, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__img_size), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=meta_keys, + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_size, keep_ratio=True), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__img_size), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__img_size), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/deployment.py b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/deployment.py new file mode 100644 index 00000000000..d9ee1805b11 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/deployment.py @@ -0,0 +1,14 @@ +"""MMDployment config of MaskRCNN-SwinT-FP16 model for Instance-Seg Task.""" + +_base_ = ["../../base/deployments/base_instance_segmentation_dynamic.py"] + + +# NOTE: Its necessary to use opset11 as squeeze>=opset13 does not work in +# mmdeploy::mmcv::ops::roi_align::roi_align_default. +# Refer to src/otx/algorithms/common/adapters/mmdeploy/ops/custom_ops.py::squeeze__default for future rewrite. +ir_config = dict( + output_names=["boxes", "labels", "masks"], + opset_version=11, +) + +scale_ir_input = False diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/deployment_tile_classifier.py b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/deployment_tile_classifier.py new file mode 100644 index 00000000000..ee740e81bfa --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/deployment_tile_classifier.py @@ -0,0 +1,22 @@ +"""MMDeploy config partitioning Swin-T MaskRCNN model to tile classifier and MaskRCNN model.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +_base_ = ["./deployment.py"] + +ir_config = dict( + output_names=["boxes", "labels", "masks", "tile_prob"], +) + +partition_config = dict( + type="tile_classifier", + apply_marks=True, + partition_cfg=[ + dict( + save_file="tile_classifier.onnx", + start=["tile_classifier:input"], + end=["tile_classifier:output"], + output_names=["tile_prob"], + ) + ], +) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/hpo_config.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/hpo_config.yaml new file mode 100644 index 00000000000..cd7082d1e89 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.00001 + - 0.001 + - 0.00001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 2 + - 8 + - 2 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/model.py b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/model.py new file mode 100644 index 00000000000..8c2de6032d2 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/model.py @@ -0,0 +1,164 @@ +"""Model configuration of MaskRCNN-SwinT-FP16 model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/instance_segmentation/incremental.py", + "../../base/models/detector.py", +] + +task = "instance-segmentation" + +pretrained = ( + "https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_tiny_patch4_window7_224.pth" # noqa +) + +model = dict( + type="CustomMaskRCNN", + backbone=dict( + type="SwinTransformer", + embed_dims=96, + depths=[2, 2, 6, 2], + num_heads=[3, 6, 12, 24], + window_size=7, + mlp_ratio=4, + qkv_bias=True, + qk_scale=None, + drop_rate=0.0, + attn_drop_rate=0.0, + drop_path_rate=0.2, + patch_norm=True, + out_indices=(0, 1, 2, 3), + with_cp=False, + convert_weights=True, + init_cfg=dict(type="Pretrained", checkpoint=pretrained), + ), + neck=dict(type="FPN", in_channels=[96, 192, 384, 768], out_channels=256, num_outs=5), + rpn_head=dict( + type="CustomRPNHead", + in_channels=256, + feat_channels=256, + anchor_generator=dict(type="AnchorGenerator", scales=[8], ratios=[0.5, 1.0, 2.0], strides=[4, 8, 16, 32, 64]), + bbox_coder=dict(type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[1.0, 1.0, 1.0, 1.0]), + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + roi_head=dict( + type="CustomRoIHead", + bbox_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=7, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + bbox_head=dict( + type="Shared2FCBBoxHead", + in_channels=256, + fc_out_channels=1024, + roi_feat_size=7, + num_classes=80, + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[0.1, 0.1, 0.2, 0.2] + ), + reg_class_agnostic=False, + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + mask_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=14, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + mask_head=dict( + type="CustomFCNMaskHead", + num_convs=4, + in_channels=256, + conv_out_channels=256, + num_classes=80, + loss_mask=dict(type="CrossEntropyLoss", use_mask=True, loss_weight=1.0), + ), + ), + train_cfg=dict( + rpn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.7, + neg_iou_thr=0.3, + min_pos_iou=0.3, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict(type="RandomSampler", num=256, pos_fraction=0.5, neg_pos_ub=-1, add_gt_as_proposals=False), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + rpn_proposal=dict( + nms_across_levels=False, + nms_pre=2000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.7), + min_bbox_size=0, + ), + rcnn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict(type="RandomSampler", num=512, pos_fraction=0.25, neg_pos_ub=-1, add_gt_as_proposals=True), + mask_size=28, + pos_weight=-1, + debug=False, + ), + ), + test_cfg=dict( + rpn=dict( + nms_across_levels=False, + nms_pre=1000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.8), + min_bbox_size=0, + ), + rcnn=dict(score_thr=0.05, nms=dict(type="nms", iou_threshold=0.7), max_per_img=500, mask_thr_binary=0.5), + ), +) + +evaluation = dict(interval=1, metric="mAP", save_best="mAP", iou_thr=[0.5]) +optimizer = dict( + _delete_=True, + type="AdamW", + lr=0.0001, + betas=(0.9, 0.999), + weight_decay=0.05, + paramwise_cfg=dict( + custom_keys=dict( + absolute_pos_embed=dict(decay_mult=0.0), + relative_position_bias_table=dict(decay_mult=0.0), + norm=dict(decay_mult=0.0), + ) + ), +) + +lr_config = dict(min_lr=1e-08) + +optimizer_config = dict(_delete_=True, grad_clip=None) + +fp16 = dict(loss_scale=dict(init_scale=512), bf16_training=False) + +load_from = ( + "https://download.openmmlab.com/mmdetection/v2.0/swin/" + "mask_rcnn_swin-t-p4-w7_fpn_fp16_ms-crop-3x_coco/" + "mask_rcnn_swin-t-p4-w7_fpn_fp16_ms-crop-3x_coco_20210908_165006-90a4008c.pth" +) + +ignore = True diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/template.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/template.yaml new file mode 100644 index 00000000000..61f359406e9 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/template.yaml @@ -0,0 +1,63 @@ +# Description. +model_template_id: Custom_Counting_Instance_Segmentation_MaskRCNN_SwinT_FP16 +name: MaskRCNN-SwinT-FP16 +task_type: INSTANCE_SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Instance Segmentation for MaskRCNN-SwinT-FP16 +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 4 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 1 + learning_rate: + default_value: 0.0001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 100 + pot_parameters: + stat_requests_number: + default_value: 1 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 407.32 +size: 191.46 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/tile_pipeline.py b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/tile_pipeline.py new file mode 100644 index 00000000000..05dc7e59f2c --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/maskrcnn_swin_t/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of SwinT model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/base_iseg_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/__init__.py b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/__init__.py new file mode 100644 index 00000000000..f6e14848a3b --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of Resnet50-MaskRCNN model for Instance-Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/compression_config.json b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/compression_config.json new file mode 100644 index 00000000000..af3c5ef8937 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/compression_config.json @@ -0,0 +1,46 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 1344, 800] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "type": "percentile", + "params": { + "min_percentile": 0, + "max_percentile": 100 + }, + "num_init_samples": 1000 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 1000 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/data_pipeline.py b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/data_pipeline.py new file mode 100644 index 00000000000..df7859b2ea2 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of Resnet model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/iseg_resnet_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/data_pipeline_xpu.py b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/data_pipeline_xpu.py new file mode 100644 index 00000000000..4efb33a8fb0 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/data_pipeline_xpu.py @@ -0,0 +1,7 @@ +"""Data Pipeline of Resnet model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/iseg_resnet_data_pipeline_xpu.py"] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/deployment.py b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/deployment.py new file mode 100644 index 00000000000..2bde65b8647 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/deployment.py @@ -0,0 +1,9 @@ +"""MMDployment config of Resnet model for Instance-Seg Task.""" + +_base_ = ["../../base/deployments/base_instance_segmentation_dynamic.py"] + +scale_ir_input = True + +ir_config = dict( + output_names=["boxes", "labels", "masks"], +) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/deployment_tile_classifier.py b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/deployment_tile_classifier.py new file mode 100644 index 00000000000..c7e3e46aa64 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/deployment_tile_classifier.py @@ -0,0 +1,23 @@ +"""MMDeploy config partitioning ResNet50 MaskRCNN model to tile classifier and MaskRCNN model.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +_base_ = ["./deployment.py"] + +ir_config = dict( + output_names=["boxes", "labels", "masks", "tile_prob"], +) + +partition_config = dict( + type="tile_classifier", + apply_marks=True, + partition_cfg=[ + dict( + save_file="tile_classifier.onnx", + start=["tile_classifier:input"], + end=["tile_classifier:output"], + output_names=["tile_prob"], + ) + ], +) diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/hpo_config.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/hpo_config.yaml new file mode 100644 index 00000000000..b6aa855ccff --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0001 + - 0.01 + - 0.001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 2 + - 6 + - 2 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/model.py b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/model.py new file mode 100644 index 00000000000..e67f6352be9 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/model.py @@ -0,0 +1,152 @@ +"""Model configuration of Resnet50-MaskRCNN model for Instance-Seg Task.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/instance_segmentation/incremental.py", + "../../../../common/adapters/mmcv/configs/backbones/resnet50.yaml", + "../../base/models/detector.py", +] + +task = "instance-segmentation" + +model = dict( + type="CustomMaskRCNN", # Use CustomMaskRCNN for Incremental Learning + neck=dict( + type="FPN", + in_channels=[256, 512, 1024, 2048], + out_channels=256, + num_outs=5, + ), + rpn_head=dict( + type="CustomRPNHead", + in_channels=256, + feat_channels=256, + anchor_generator=dict( + type="AnchorGenerator", + scales=[8], + ratios=[0.5, 1.0, 2.0], + strides=[4, 8, 16, 32, 64], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[1.0, 1.0, 1.0, 1.0], + ), + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + roi_head=dict( + type="CustomRoIHead", # Use CustomROIHead for Ignore mode + bbox_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=7, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + bbox_head=dict( + type="Shared2FCBBoxHead", + in_channels=256, + fc_out_channels=1024, + roi_feat_size=7, + num_classes=80, + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[0.1, 0.1, 0.2, 0.2], + ), + reg_class_agnostic=False, + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + mask_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=14, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + mask_head=dict( + type="CustomFCNMaskHead", + num_convs=4, + in_channels=256, + conv_out_channels=256, + num_classes=80, + loss_mask=dict(type="CrossEntropyLoss", use_mask=True, loss_weight=1.0), + ), + ), + train_cfg=dict( + rpn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.7, + neg_iou_thr=0.3, + min_pos_iou=0.3, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict( + type="RandomSampler", + num=256, + pos_fraction=0.5, + neg_pos_ub=-1, + add_gt_as_proposals=False, + ), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + rpn_proposal=dict( + nms_across_levels=False, + nms_pre=2000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.7), + min_bbox_size=0, + ), + rcnn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict( + type="RandomSampler", + num=512, + pos_fraction=0.25, + neg_pos_ub=-1, + add_gt_as_proposals=True, + ), + mask_size=28, + pos_weight=-1, + debug=False, + ), + ), + test_cfg=dict( + rpn=dict( + nms_across_levels=False, + nms_pre=1000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.7), + min_bbox_size=0, + ), + rcnn=dict( + score_thr=0.05, + nms=dict(type="nms", iou_threshold=0.5), + max_per_img=100, + mask_thr_binary=0.5, + ), + ), +) +load_from = "https://download.openmmlab.com/mmdetection/\ +v2.0/mask_rcnn/mask_rcnn_r50_fpn_mstrain-poly_3x_coco/\ +mask_rcnn_r50_fpn_mstrain-poly_3x_coco_20210524_201154-21b550bb.pth" + +evaluation = dict(interval=1, metric="mAP", save_best="mAP", iou_thr=[0.5]) +ignore = True diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/__init__.py b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/__init__.py new file mode 100644 index 00000000000..3539217f31c --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of Mask-RCNN ResNet50 model for Semi-SL Instance Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/compression_config.json b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/compression_config.json new file mode 100644 index 00000000000..ab687b9c6a2 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/compression_config.json @@ -0,0 +1,41 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 1344, 800] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 1000 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 1000 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/data_pipeline.py b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/data_pipeline.py new file mode 100644 index 00000000000..79928399ac3 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of Mask-RCNN ResNet50 model for Semi-Supervised Learning Instance Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../../base/data/semisl/semisl_is_res_data_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/hparam.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/hparam.yaml new file mode 100644 index 00000000000..dc1c58367fe --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/hparam.yaml @@ -0,0 +1,13 @@ +# Hyperparameters. +# since we use repeat dataset for semi-sl +# -> change iteration related parameters +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised + learning_parameters: + num_iters: + default_value: 200 # it is found that sometimes it takes more epochs to train the semi-sl approach + early_stop_start: + default_value: 7 # when unlabeled branch enabled diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/model.py b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/model.py new file mode 100644 index 00000000000..6f9cce1a468 --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/semisl/model.py @@ -0,0 +1,156 @@ +"""Model Configuration of Mask-RCNN ResNet50 model for Semi-Supervised Learning Instance Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../../recipes/stages/instance_segmentation/semisl.py", + "../../../../../common/adapters/mmcv/configs/backbones/resnet50.yaml", + "../../../base/models/detector.py", +] + +task = "instance-segmentation" + +model = dict( + super_type="MeanTeacher", + pseudo_conf_thresh=0.7, + unlabeled_loss_weights={"cls": 1.0, "bbox": 1.0, "mask": 1.0}, + filter_empty_annotations=False, + type="CustomMaskRCNN", + neck=dict( + type="FPN", + in_channels=[256, 512, 1024, 2048], + out_channels=256, + num_outs=5, + ), + rpn_head=dict( + type="RPNHead", + in_channels=256, + feat_channels=256, + anchor_generator=dict( + type="AnchorGenerator", + scales=[8], + ratios=[0.5, 1.0, 2.0], + strides=[4, 8, 16, 32, 64], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[1.0, 1.0, 1.0, 1.0], + ), + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + roi_head=dict( + type="CustomRoIHead", # Use CustomROIHead for Ignore mode + bbox_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=7, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + bbox_head=dict( + type="Shared2FCBBoxHead", + in_channels=256, + fc_out_channels=1024, + roi_feat_size=7, + num_classes=80, + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[0.1, 0.1, 0.2, 0.2], + ), + reg_class_agnostic=False, + loss_cls=dict(type="OrdinaryFocalLoss", gamma=1.5, loss_weight=1.0), + loss_bbox=dict(type="SmoothL1Loss", beta=1.0, loss_weight=1.0), + ), + mask_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=14, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + mask_head=dict( + type="CustomFCNMaskHead", + num_convs=4, + in_channels=256, + conv_out_channels=256, + num_classes=80, + loss_mask=dict(type="CrossEntropyLoss", use_mask=True, loss_weight=1.0), + ), + ), + train_cfg=dict( + rpn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.7, + neg_iou_thr=0.3, + min_pos_iou=0.3, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict( + type="RandomSampler", + num=256, + pos_fraction=0.5, + neg_pos_ub=-1, + add_gt_as_proposals=False, + ), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + rpn_proposal=dict( + nms_across_levels=False, + nms_pre=2000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.7), + min_bbox_size=0, + ), + rcnn=dict( + assigner=dict( + type="CustomMaxIoUAssigner", + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict( + type="RandomSampler", + num=512, + pos_fraction=0.25, + neg_pos_ub=-1, + add_gt_as_proposals=True, + ), + mask_size=28, + pos_weight=-1, + debug=False, + ), + ), + test_cfg=dict( + rpn=dict( + nms_across_levels=False, + nms_pre=1000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.7), + min_bbox_size=0, + ), + rcnn=dict( + score_thr=0.05, + nms=dict(type="nms", iou_threshold=0.5, max_num=100), + max_per_img=100, + mask_thr_binary=0.5, + ), + ), +) +load_from = "https://download.openmmlab.com/mmdetection/\ +v2.0/mask_rcnn/mask_rcnn_r50_fpn_mstrain-poly_3x_coco/\ +mask_rcnn_r50_fpn_mstrain-poly_3x_coco_20210524_201154-21b550bb.pth" + +evaluation = dict(interval=1, metric="mAP", save_best="mAP", iou_thr=[0.5]) +ignore = True diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/template.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/template.yaml new file mode 100644 index 00000000000..17a74b1c25e --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/template.yaml @@ -0,0 +1,68 @@ +# Description. +model_template_id: Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50 +name: MaskRCNN-ResNet50 +task_type: INSTANCE_SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Instance Segmentation for MaskRCNN-ResNet50 +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + nncf: otx.algorithms.detection.adapters.mmdet.nncf.task.DetectionNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 4 + auto_hpo_state: POSSIBLE + inference_batch_size: + default_value: 1 + learning_rate: + default_value: 0.007 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 100 + pot_parameters: + stat_requests_number: + default_value: 1 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 533.8 +size: 177.9 + +# Model spec +model_category: ACCURACY +is_default_for_task: true diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/tile_pipeline.py b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/tile_pipeline.py new file mode 100644 index 00000000000..ad2ebc72f9d --- /dev/null +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of Resnet model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/base_iseg_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/rotated_detection/__init__.py b/src/otx/algorithms/detection/configs/rotated_detection/__init__.py new file mode 100644 index 00000000000..f6d35704981 --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/__init__.py @@ -0,0 +1,5 @@ +"""Rotated-Detection Models configurations.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml new file mode 100644 index 00000000000..824093460ac --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml @@ -0,0 +1,686 @@ +description: Configuration for an rotated detection task +header: Configuration for an rotated detection task +learning_parameters: + batch_size: + affects_outcome_of: TRAINING + default_value: 5 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 5 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + inference_batch_size: + affects_outcome_of: TRAINING + default_value: 1 + description: The number of samples seen in each iteration of inference. + Increasing this value improves inference time and may make the inference more + stable. A larger batch size has higher memory requirements. + editable: true + header: Inference batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + description: Learning Parameters + header: Learning Parameters + learning_rate: + affects_outcome_of: TRAINING + default_value: 0.01 + description: + Increasing this value will speed up training convergence but might + make it unstable. + editable: true + header: Learning rate + max_value: 0.1 + min_value: 1.0e-07 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.01 + visible_in_ui: true + warning: null + auto_hpo_state: NOT_POSSIBLE + learning_rate_warmup_iters: + affects_outcome_of: TRAINING + default_value: 100 + description: + In this periods of initial training iterations, the model will be trained in low learning rate, + which will be increased incrementally up to the expected learning rate setting. + This warm-up phase is known to be helpful to stabilize training, thus result in better performance. + editable: true + header: Number of iterations for learning rate warmup + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: null + num_iters: + affects_outcome_of: TRAINING + default_value: 1 + description: + Increasing this value causes the results to be more robust but training + time will be longer. + editable: true + header: Number of training iterations + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: null + num_workers: + affects_outcome_of: NONE + default_value: 2 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of cpu threads to use during batch generation + max_value: 8 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 2 + visible_in_ui: true + warning: null + enable_early_stopping: + affects_outcome_of: TRAINING + default_value: true + description: Early exit from training when validation accuracy isn't changed or decreased for several epochs. + editable: true + header: Enable early stopping of the training + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + early_stop_start: + affects_outcome_of: TRAINING + default_value: 3 + editable: true + header: Start epoch for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 3 + visible_in_ui: false + early_stop_patience: + affects_outcome_of: TRAINING + default_value: 10 + description: Training will stop if the model does not improve within the number of epochs of patience. + editable: true + header: Patience for early stopping + max_value: 50 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 10 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + early_stop_iteration_patience: + affects_outcome_of: TRAINING + default_value: 0 + description: + Training will stop if the model does not improve within the number of iterations of patience. + This ensures the model is trained enough with the number of iterations of patience before early stopping. + editable: true + header: Iteration patience for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + use_adaptive_interval: + affects_outcome_of: TRAINING + default_value: true + description: Depending on the size of iteration per epoch, adaptively update the validation interval and related values. + editable: true + header: Use adaptive validation interval + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: This will automatically control the patience and interval when early stopping is enabled. + auto_adapt_batch_size: + affects_outcome_of: TRAINING + default_value: Safe + description: Safe => Prevent GPU out of memory. Full => Find a batch size using most of GPU memory. + editable: true + enum_name: BatchSizeAdaptType + header: Decrease batch size if current batch size isn't fit to CUDA memory. + options: + NONE: "None" + SAFE: "Safe" + FULL: "Full" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Safe + visible_in_ui: true + warning: + Enabling this could change the actual batch size depending on the current GPU status. + The learning rate also could be adjusted according to the adapted batch size. This process might change + a model performance and take some extra computation time to try a few batch size candidates. + auto_num_workers: + affects_outcome_of: TRAINING + default_value: false + description: Adapt num_workers according to current hardware status automatically. + editable: true + header: Enable auto adaptive num_workers + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + input_size: + affects_outcome_of: INFERENCE + default_value: Default + description: + The input size of the given model could be configured to one of the predefined resolutions. + Reduced training and inference time could be expected by using smaller input size. + In Auto mode, the input size is automatically determined based on dataset statistics. + Defaults to per-model default resolution. + editable: true + enum_name: InputSizePreset + header: Configure model input size. + options: + DEFAULT: "Default" + AUTO: "Auto" + _256x256: "256x256" + _384x384: "384x384" + _512x512: "512x512" + _768x768: "768x768" + _1024x1024: "1024x1024" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Default + visible_in_ui: false + warning: Modifying input size may decrease model performance. + type: PARAMETER_GROUP + visible_in_ui: true +postprocessing: + confidence_threshold: + affects_outcome_of: INFERENCE + default_value: 0.35 + description: + This threshold only takes effect if the threshold is not set based + on the result. + editable: true + header: Confidence threshold + max_value: 1 + min_value: 0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + # value: 0.35 + value: 0.01 + visible_in_ui: true + warning: null + description: Postprocessing + header: Postprocessing + result_based_confidence_threshold: + affects_outcome_of: INFERENCE + default_value: true + description: Confidence threshold is derived from the results + editable: true + header: Result based confidence threshold + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true + max_num_detections: + affects_outcome_of: INFERENCE + default_value: 0 + description: + Extra detection outputs will be discared in non-maximum suppression process. + Defaults to 0, which means per-model default values. + editable: true + header: Maximum number of detections per image + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: null +algo_backend: + description: parameters for algo backend + header: Algo backend parameters + train_type: + affects_outcome_of: TRAINING + default_value: Incremental + description: Training scheme option that determines how to train the model + editable: True + enum_name: TrainType + header: Train type + options: + Incremental: "Incremental" + Semisupervised: "Semisupervised" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Incremental + visible_in_ui: false + warning: null + mem_cache_size: + affects_outcome_of: TRAINING + default_value: 1000000000 + description: Size of memory pool for caching decoded data to load data faster (bytes). + editable: true + header: Size of memory pool + max_value: 9223372036854775807 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + storage_cache_scheme: + affects_outcome_of: TRAINING + default_value: NONE + description: Scheme for storage cache + editable: true + enum_name: StorageCacheScheme + header: Scheme for storage cache + options: + NONE: "NONE" + AS_IS: "AS-IS" + JPEG_75: "JPEG/75" + JPEG_95: "JPEG/95" + PNG: "PNG" + TIFF: "TIFF" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: True + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: True + warning: null + stat_subset_size: + affects_outcome_of: NONE + default_value: 300 + description: Number of data samples used for post-training optimization + editable: True + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: True + warning: null + stat_requests_number: + affects_outcome_of: NONE + default_value: 0 + description: Number of requests during statistics collection + editable: true + header: Number of requests + max_value: 200 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + header: Optimization by NNCF + enable_quantization: + affects_outcome_of: INFERENCE + default_value: True + description: Enable quantization algorithm + editable: false + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: false + warning: null + enable_pruning: + affects_outcome_of: INFERENCE + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + pruning_supported: + affects_outcome_of: TRAINING + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + maximal_accuracy_degradation: + affects_outcome_of: NONE + default_value: 1.0 + description: The maximal allowed accuracy metric drop in absolute values + editable: True + header: Maximum accuracy degradation + max_value: 100.0 + min_value: 0.0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: True + warning: null + type: PARAMETER_GROUP + visible_in_ui: True + +tiling_parameters: + header: Tiling + description: Crop dataset to tiles + + enable_tiling: + header: Enable tiling + description: Set to True to allow tiny objects to be better detected. + default_value: false + editable: true + affects_outcome_of: TRAINING + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: "Tiling trades off speed for accuracy as it increases the number of images to be processed. In turn, it's memory efficient as smaller resolution patches are handled at onces so that the possibility of OOM issues could be reduced. Important: In the current version, depending on the dataset size and the available hardware resources, a model may not train successfully when tiling is enabled." + + enable_tile_classifier: + header: Enable tile classifier + description: Enabling tile classifier enhances the speed of tiling inference by incorporating a tile classifier into the instance segmentation model. This feature prevents the detector from making predictions on tiles that do not contain any objects, thus optimizing its speed performance. + default_value: false + editable: true + affects_outcome_of: TRAINING + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: The tile classifier prioritizes inference speed over training speed, it requires more training in order to achieve its optimized performance. + + enable_adaptive_params: + header: Enable adaptive tiling parameters + description: Config tile size and tile overlap adaptively based on annotated dataset statistic. Manual settings well be ignored if it's turned on. Please turn off this option in order to tune tiling parameters manually. + default_value: true + editable: true + affects_outcome_of: TRAINING + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + + tile_size: + header: Tile Image Size + description: Tile image size. (tile_size x tile_size) sub images will be the unit of computation. + affects_outcome_of: TRAINING + default_value: 400 + min_value: 100 + max_value: 4096 + type: INTEGER + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 400 + visible_in_ui: true + warning: null + + tile_overlap: + header: Tile Overlap + description: Overlap ratio between each two neighboring tiles. Recommend to set as large_object_size / tile_size. + affects_outcome_of: TRAINING + default_value: 0.2 + min_value: 0.0 + max_value: 0.9 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.2 + visible_in_ui: true + warning: null + + tile_max_number: + header: Max object per tile + description: Maximum number of objects per tile + affects_outcome_of: TRAINING + default_value: 1500 + min_value: 1 + max_value: 5000 + type: INTEGER + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1500 + visible_in_ui: true + warning: null + + tile_ir_scale_factor: + header: OpenVINO IR Scale Factor + description: The purpose of the scale parameter is to optimize the performance and efficiency of tiling in OpenVINO IR during inference. By controlling the increase in tile size and input size, the scale parameter allows for more efficient parallelization of the workload and improve the overall performance and efficiency of the inference process on OpenVINO. + affects_outcome_of: TRAINING + default_value: 1.0 + min_value: 1.0 + max_value: 4.0 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: true + warning: null + + tile_sampling_ratio: + header: Sampling Ratio for entire tiling + description: Since tiling train and validation to all tile from large image, usually it takes lots of time than normal training. The tile_sampling_ratio is ratio for sampling entire tile dataset. Sampling tile dataset would save lots of time for training and validation time. Note that sampling will be applied to training and validation dataset, not test dataset. + affects_outcome_of: TRAINING + default_value: 1.0 + min_value: 0.000001 + max_value: 1.0 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: true + warning: null + + object_tile_ratio: + header: Object tile ratio + description: The desired ratio of min object size and tile size. + affects_outcome_of: TRAINING + default_value: 0.03 + min_value: 0.00 + max_value: 1.00 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.03 + visible_in_ui: false + warning: null + + type: PARAMETER_GROUP + visible_in_ui: true diff --git a/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/__init__.py b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/__init__.py new file mode 100644 index 00000000000..9cce2062a15 --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/__init__.py @@ -0,0 +1,5 @@ +"""Initialization of EfficientNetB2B-MaskRCNN model for Rotated-Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/compression_config.json b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/compression_config.json new file mode 100644 index 00000000000..7e0cba46aa9 --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/compression_config.json @@ -0,0 +1,41 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 1024, 1024] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 1000 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 1000 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/data_pipeline.py b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/data_pipeline.py new file mode 100644 index 00000000000..16ffc3042dd --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/data_pipeline.py @@ -0,0 +1,108 @@ +"""Data Pipeline of EfficientNetB2B model for Rotated-Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +__img_size = (1024, 1024) + +# TODO: A comparison experiment is needed to determine which value is appropriate for to_rgb. +__img_norm_cfg = dict(mean=(103.53, 116.28, 123.675), std=(1.0, 1.0, 1.0), to_rgb=True) + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="rotated_detection", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=True, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_size, keep_ratio=True), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/deployment.py b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/deployment.py new file mode 100644 index 00000000000..1222c0296ad --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/deployment.py @@ -0,0 +1,12 @@ +"""MMDeploy config of EfficientNetB2B model for Rotated-Detection Task.""" + +_base_ = ["../../base/deployments/base_instance_segmentation_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels", "masks"], +) + +backend_config = dict( + # dynamic batch causes forever running openvino process + model_inputs=[dict(opt_shapes=dict(input=[1, 3, 1024, 1024]))], +) diff --git a/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/hpo_config.yaml b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/hpo_config.yaml new file mode 100644 index 00000000000..0fc1c4073b5 --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0015 + - 0.1 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 2 + - 8 + - 2 diff --git a/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/model.py b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/model.py new file mode 100644 index 00000000000..5a7a818925c --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/model.py @@ -0,0 +1,121 @@ +"""Model configuration of EfficientNetB2B-MaskRCNN model for Rotated-Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/instance_segmentation/incremental.py", + "../../../../common/adapters/mmcv/configs/backbones/efficientnet_b2b.yaml", + "../../base/models/detector.py", +] + +task = "instance-segmentation" + +model = dict( + type="CustomMaskRCNN", # Use CustomMaskRCNN for Incremental Learning + neck=dict(type="FPN", in_channels=[24, 48, 120, 352], out_channels=80, num_outs=5), + rpn_head=dict( + type="RPNHead", + in_channels=80, + feat_channels=80, + anchor_generator=dict(type="AnchorGenerator", scales=[8], ratios=[0.5, 1.0, 2.0], strides=[4, 8, 16, 32, 64]), + bbox_coder=dict(type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[1.0, 1.0, 1.0, 1.0]), + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + roi_head=dict( + type="CustomRoIHead", # Use CustomROIHead for Ignore mode + bbox_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=7, sampling_ratio=0), + out_channels=80, + featmap_strides=[4, 8, 16, 32], + ), + bbox_head=dict( + type="Shared2FCBBoxHead", + in_channels=80, + fc_out_channels=1024, + roi_feat_size=7, + num_classes=80, + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[0.1, 0.1, 0.2, 0.2] + ), + reg_class_agnostic=False, + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + mask_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=14, sampling_ratio=0), + out_channels=80, + featmap_strides=[4, 8, 16, 32], + ), + mask_head=dict( + type="CustomFCNMaskHead", + num_convs=4, + in_channels=80, + conv_out_channels=80, + num_classes=80, + loss_mask=dict(type="CrossEntropyLoss", use_mask=True, loss_weight=1.0), + ), + ), + train_cfg=dict( + rpn=dict( + assigner=dict( + type="MaxIoUAssigner", + pos_iou_thr=0.7, + neg_iou_thr=0.3, + min_pos_iou=0.3, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict(type="RandomSampler", num=256, pos_fraction=0.5, neg_pos_ub=-1, add_gt_as_proposals=False), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + rpn_proposal=dict( + nms_across_levels=False, + nms_pre=2000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.8), + min_bbox_size=0, + ), + rcnn=dict( + assigner=dict( + type="MaxIoUAssigner", + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict(type="RandomSampler", num=256, pos_fraction=0.25, neg_pos_ub=-1, add_gt_as_proposals=True), + mask_size=28, + pos_weight=-1, + debug=False, + ), + ), + test_cfg=dict( + rpn=dict( + nms_across_levels=False, + nms_pre=800, + max_per_img=500, + nms=dict(type="nms", iou_threshold=0.8), + min_bbox_size=0, + ), + rcnn=dict(score_thr=0.05, nms=dict(type="nms", iou_threshold=0.7), max_per_img=500, mask_thr_binary=0.5), + ), +) +load_from = "https://storage.openvinotoolkit.org/repositories/\ +openvino_training_extensions/models/instance_segmentation/\ +v2/efficientnet_b2b-mask_rcnn-576x576.pth" + +evaluation = dict(interval=1, metric="mAP", save_best="mAP", iou_thr=[0.5]) +fp16 = dict(loss_scale=512.0) +ignore = True diff --git a/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/template.yaml b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/template.yaml new file mode 100644 index 00000000000..34891f7e14e --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/template.yaml @@ -0,0 +1,65 @@ +# Description. +model_template_id: Custom_Rotated_Detection_via_Instance_Segmentation_MaskRCNN_EfficientNetB2B +name: MaskRCNN-EfficientNetB2B +task_type: ROTATED_DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Rotated object detection for MaskRCNN-EfficientNetB2B +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + nncf: otx.algorithms.detection.adapters.mmdet.nncf.task.DetectionNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 4 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.015 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 100 + pot_parameters: + stat_requests_number: + default_value: 1 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 68.48 +size: 13.27 + +# Model spec +model_category: SPEED diff --git a/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/tile_pipeline.py b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/tile_pipeline.py new file mode 100644 index 00000000000..fb25aab8478 --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of EfficientNetB2B model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/efficientnet_iseg_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/__init__.py b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/__init__.py new file mode 100644 index 00000000000..c1277776379 --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/__init__.py @@ -0,0 +1,5 @@ +"""Initialization of Resnet50-MaskRCNN model for Rotated-Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/compression_config.json b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/compression_config.json new file mode 100644 index 00000000000..ab687b9c6a2 --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/compression_config.json @@ -0,0 +1,41 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": { + "sample_size": [1, 3, 1344, 800] + }, + "compression": [], + "log_dir": "/tmp" + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 0.0005 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": { + "num_init_samples": 1000 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 1000 + } + } + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/data_pipeline.py b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/data_pipeline.py new file mode 100644 index 00000000000..461ff4cf7df --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/data_pipeline.py @@ -0,0 +1,106 @@ +"""Data Pipeline of Resnet model for Rotated-Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +__img_size = (1024, 1024) + +# TODO: A comparison experiment is needed to determine which value is appropriate for to_rgb. +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="rotated_detection", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict( + type="Resize", + img_scale=__img_size, + keep_ratio=True, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels", "gt_masks"], + meta_keys=[ + "ori_filename", + "flip_direction", + "scale_factor", + "img_norm_cfg", + "gt_ann_ids", + "flip", + "ignored_labels", + "ori_shape", + "filename", + "img_shape", + "pad_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_size, keep_ratio=True), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] + +__dataset_type = "OTXDetDataset" + +data = dict( + train=dict( + type=__dataset_type, + pipeline=train_pipeline, + ), + val=dict( + type=__dataset_type, + test_mode=True, + pipeline=val_pipeline, + ), + test=dict( + type=__dataset_type, + test_mode=True, + pipeline=test_pipeline, + ), +) diff --git a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/deployment.py b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/deployment.py new file mode 100644 index 00000000000..48819dd7d49 --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/deployment.py @@ -0,0 +1,12 @@ +"""MMDployment config of Resnet model for Rotated-Detection Task.""" + +_base_ = ["../../base/deployments/base_instance_segmentation_dynamic.py"] + +ir_config = dict( + output_names=["boxes", "labels", "masks"], +) + +backend_config = dict( + # dynamic batch causes forever running openvino process + model_inputs=[dict(opt_shapes=dict(input=[1, 3, 800, 1344]))], +) diff --git a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/hpo_config.yaml b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/hpo_config.yaml new file mode 100644 index 00000000000..40c40c05bdf --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/hpo_config.yaml @@ -0,0 +1,16 @@ +metric: mAP +search_algorithm: asha +early_stop: None +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0001 + - 0.01 + - 0.001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 2 + - 8 + - 2 diff --git a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/model.py b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/model.py new file mode 100644 index 00000000000..5d528fe4796 --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/model.py @@ -0,0 +1,153 @@ +"""Model configuration of Resnet50-MaskRCNN model for Rotated-Detection Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/instance_segmentation/incremental.py", + "../../../../common/adapters/mmcv/configs/backbones/resnet50.yaml", + "../../base/models/detector.py", +] + +task = "instance-segmentation" + +model = dict( + type="CustomMaskRCNN", # Use CustomMaskRCNN for Incremental Learning + neck=dict( + type="FPN", + in_channels=[256, 512, 1024, 2048], + out_channels=256, + num_outs=5, + ), + rpn_head=dict( + type="RPNHead", + in_channels=256, + feat_channels=256, + anchor_generator=dict( + type="AnchorGenerator", + scales=[8], + ratios=[0.5, 1.0, 2.0], + strides=[4, 8, 16, 32, 64], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[1.0, 1.0, 1.0, 1.0], + ), + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + roi_head=dict( + type="CustomRoIHead", # Use CustomROIHead for Ignore mode + bbox_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=7, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + bbox_head=dict( + type="Shared2FCBBoxHead", + in_channels=256, + fc_out_channels=1024, + roi_feat_size=7, + num_classes=80, + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[0.1, 0.1, 0.2, 0.2], + ), + reg_class_agnostic=False, + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + mask_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=14, sampling_ratio=0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + mask_head=dict( + type="CustomFCNMaskHead", + num_convs=4, + in_channels=256, + conv_out_channels=256, + num_classes=80, + loss_mask=dict(type="CrossEntropyLoss", use_mask=True, loss_weight=1.0), + ), + ), + train_cfg=dict( + rpn=dict( + assigner=dict( + type="MaxIoUAssigner", + pos_iou_thr=0.7, + neg_iou_thr=0.3, + min_pos_iou=0.3, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict( + type="RandomSampler", + num=256, + pos_fraction=0.5, + neg_pos_ub=-1, + add_gt_as_proposals=False, + ), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + rpn_proposal=dict( + nms_across_levels=False, + nms_pre=2000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.7), + min_bbox_size=0, + ), + rcnn=dict( + assigner=dict( + type="MaxIoUAssigner", + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ), + sampler=dict( + type="RandomSampler", + num=512, + pos_fraction=0.25, + neg_pos_ub=-1, + add_gt_as_proposals=True, + ), + mask_size=28, + pos_weight=-1, + debug=False, + ), + ), + test_cfg=dict( + rpn=dict( + nms_across_levels=False, + nms_pre=1000, + max_per_img=1000, + nms=dict(type="nms", iou_threshold=0.7), + min_bbox_size=0, + ), + rcnn=dict( + score_thr=0.05, + nms=dict(type="nms", iou_threshold=0.5), + max_per_img=100, + mask_thr_binary=0.5, + ), + ), +) +load_from = "https://download.openmmlab.com/mmdetection/\ +v2.0/mask_rcnn/mask_rcnn_r50_fpn_mstrain-poly_3x_coco/\ +mask_rcnn_r50_fpn_mstrain-poly_3x_coco_20210524_201154-21b550bb.pth" + +evaluation = dict(interval=1, metric="mAP", save_best="mAP", iou_thr=[0.5]) +ignore = True diff --git a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/template.yaml b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/template.yaml new file mode 100644 index 00000000000..31e74540bda --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/template.yaml @@ -0,0 +1,66 @@ +# Description. +model_template_id: Custom_Rotated_Detection_via_Instance_Segmentation_MaskRCNN_ResNet50 +name: MaskRCNN-ResNet50 +task_type: ROTATED_DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Rotated object detection for MaskRCNN-ResNet50 +application: ~ + +# Algo backend. +framework: OTXDetection v2.9.1 + +# Task implementations. +entrypoints: + base: otx.algorithms.detection.adapters.mmdet.task.MMDetectionTask + openvino: otx.algorithms.detection.adapters.openvino.task.OpenVINODetectionTask + nncf: otx.algorithms.detection.adapters.mmdet.nncf.task.DetectionNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 4 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 100 + pot_parameters: + stat_requests_number: + default_value: 1 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 533.8 +size: 177.9 + +# Model spec +model_category: ACCURACY +is_default_for_task: true diff --git a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/tile_pipeline.py b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/tile_pipeline.py new file mode 100644 index 00000000000..ad2ebc72f9d --- /dev/null +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/tile_pipeline.py @@ -0,0 +1,7 @@ +"""Tiling Pipeline of Resnet model for Instance-Seg Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +_base_ = ["../../base/data/tiling/base_iseg_tile_pipeline.py"] diff --git a/src/otx/algorithms/detection/task.py b/src/otx/algorithms/detection/task.py new file mode 100644 index 00000000000..7174315481f --- /dev/null +++ b/src/otx/algorithms/detection/task.py @@ -0,0 +1,607 @@ +"""Task of OTX Detection.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import io +import os +from abc import ABC, abstractmethod +from typing import Any, Dict, Iterable, List, Optional + +import numpy as np +import psutil +import torch +from mmcv.utils import ConfigDict + +from otx.algorithms.common.configs.configuration_enums import InputSizePreset +from otx.algorithms.common.tasks.base_task import TRAIN_TYPE_DIR_PATH, OTXTask +from otx.algorithms.common.utils.callback import ( + InferenceProgressCallback, + TrainingProgressCallback, +) +from otx.algorithms.common.utils.ir import embed_ir_model_data +from otx.algorithms.common.utils.utils import embed_onnx_model_data, get_cfg_based_on_device +from otx.algorithms.detection.configs.base import DetectionConfig +from otx.algorithms.detection.utils import create_detection_shapes, create_mask_shapes, get_det_model_api_configuration +from otx.api.configuration import cfg_helper +from otx.api.configuration.helper.utils import config_to_bytes, ids_to_strings +from otx.api.entities.datasets import DatasetEntity, DatasetPurpose +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.metrics import ( + BarChartInfo, + BarMetricsGroup, + CurveMetric, + LineChartInfo, + LineMetricsGroup, + MetricsGroup, + ScoreMetric, + VisualizationType, +) +from otx.api.entities.model import ( + ModelEntity, + ModelPrecision, +) +from otx.api.entities.model_template import TaskType +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.tensor import TensorEntity +from otx.api.entities.train_parameters import TrainParameters, default_progress_callback +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.utils.dataset_utils import add_saliency_maps_to_dataset_item +from otx.cli.utils.multi_gpu import is_multigpu_child_process +from otx.core.data.caching.mem_cache_handler import MemCacheHandlerSingleton +from otx.utils.logger import get_logger + +logger = get_logger() + + +class OTXDetectionTask(OTXTask, ABC): + """Task class for OTX detection.""" + + # pylint: disable=too-many-instance-attributes, too-many-locals + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__(task_environment, output_path) + self._task_config = DetectionConfig + self._hyperparams: ConfigDict = task_environment.get_hyper_parameters(self._task_config) + self._train_type = self._hyperparams.algo_backend.train_type + self._model_dir = os.path.join( + os.path.abspath(os.path.dirname(self._task_environment.model_template.model_template_path)), + TRAIN_TYPE_DIR_PATH[self._train_type.name], + ) + self._anchors: Dict[str, int] = {} + + self.confidence_threshold = 0.0 + self.max_num_detections = 0 + if hasattr(self._hyperparams, "postprocessing"): + if hasattr(self._hyperparams.postprocessing, "confidence_threshold"): + self.confidence_threshold = self._hyperparams.postprocessing.confidence_threshold + if hasattr(self._hyperparams.postprocessing, "max_num_detections"): + self.max_num_detections = self._hyperparams.postprocessing.max_num_detections + + if task_environment.model is not None: + self._load_model() + + self.use_ellipse_shapes = self._hyperparams.postprocessing.use_ellipse_shapes + + if self._hyperparams.tiling_parameters.enable_tiling: + self.data_pipeline_path = os.path.join(self._model_dir, "tile_pipeline.py") + else: + self.data_pipeline_path = get_cfg_based_on_device(os.path.join(self._model_dir, "data_pipeline.py")) + + if hasattr(self._hyperparams.learning_parameters, "input_size"): + input_size_cfg = InputSizePreset(self._hyperparams.learning_parameters.input_size.value) + else: + input_size_cfg = InputSizePreset.DEFAULT + if self._hyperparams.tiling_parameters.enable_tiling: + # Disable auto input size if tiling is enabled + input_size_cfg = InputSizePreset.DEFAULT + self._input_size = input_size_cfg.tuple + + def _load_postprocessing(self, model_data): + """Load postprocessing configs form PyTorch model. + + Args: + model_data: The model data. + """ + loaded_postprocessing = model_data.get("config", {}).get("postprocessing", {}) + hparams = self._hyperparams.postprocessing + if "use_ellipse_shapes" in loaded_postprocessing: + hparams.use_ellipse_shapes = loaded_postprocessing["use_ellipse_shapes"]["value"] + else: + hparams.use_ellipse_shapes = False + if "max_num_detections" in loaded_postprocessing: + trained_max_num_detections = loaded_postprocessing["max_num_detections"]["value"] + # Prefer new hparam value set by user (>0) intentionally than trained value + if self.max_num_detections == 0: + self.max_num_detections = trained_max_num_detections + + # If confidence threshold is adaptive then up-to-date value should be stored in the model + # and should not be changed during inference. Otherwise user-specified value should be taken. + if hparams.result_based_confidence_threshold: + self.confidence_threshold = model_data.get("confidence_threshold", self.confidence_threshold) + else: + self.confidence_threshold = hparams.confidence_threshold + logger.info(f"Confidence threshold {self.confidence_threshold}") + + def _load_tiling_parameters(self, model_data): + """Load tiling parameters from PyTorch model. + + Args: + model_data: The model data. + + Raises: + RuntimeError: If tile classifier is enabled but not found in the trained model. + """ + loaded_tiling_parameters = model_data.get("config", {}).get("tiling_parameters", {}) + if loaded_tiling_parameters.get("enable_tiling", {}).get("value", False): + logger.info("Load tiling parameters") + hparams = self._hyperparams.tiling_parameters + hparams.enable_tiling = loaded_tiling_parameters["enable_tiling"]["value"] + hparams.tile_size = loaded_tiling_parameters["tile_size"]["value"] + hparams.tile_overlap = loaded_tiling_parameters["tile_overlap"]["value"] + hparams.tile_max_number = loaded_tiling_parameters["tile_max_number"]["value"] + hparams.tile_ir_scale_factor = loaded_tiling_parameters["tile_ir_scale_factor"]["value"] + hparams.object_tile_ratio = loaded_tiling_parameters["object_tile_ratio"]["value"] + # check backward compatibility + enable_tile_classifier = loaded_tiling_parameters.get("enable_tile_classifier", {}).get("value", False) + if enable_tile_classifier: + found_tile_classifier = any( + layer_name.startswith("tile_classifier") for layer_name in model_data["model"]["state_dict"].keys() + ) + if not found_tile_classifier: + raise RuntimeError( + "Tile classifier is enabled but not found in the trained model. Please retrain your model." + ) + hparams.enable_tile_classifier = loaded_tiling_parameters["enable_tile_classifier"]["value"] + + def _load_model_ckpt(self, model: Optional[ModelEntity]) -> Optional[Dict]: + """Load model checkpoint from model entity. + + Args: + model (Optional[ModelEntity]): The model entity. + + Returns: + dict: The model checkpoint including model weights and other parameters. + """ + if model and "weights.pth" in model.model_adapters: + # If a model has been trained and saved for the task already, create empty model and load weights here + buffer = io.BytesIO(model.get_data("weights.pth")) + model_data = torch.load(buffer, map_location=torch.device("cpu")) + + if model_data.get("anchors"): + self._anchors = model_data["anchors"] + self._load_postprocessing(model_data) + self._load_tiling_parameters(model_data) + return model_data + return None + + def train( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + train_parameters: Optional[TrainParameters] = None, + seed: Optional[int] = None, + deterministic: bool = False, + ): + """Train function for OTX detection task. + + Actual training is processed by _train_model fucntion + """ + logger.info("train()") + logger.info(f"------> system virtual mem: {psutil.virtual_memory()}") + # Check for stop signal when training has stopped. + # If should_stop is true, training was cancelled and no new + if self._should_stop: + logger.info("Training cancelled.") + self._should_stop = False + self._is_training = False + return + self.seed = seed + self.deterministic = deterministic + + # Set OTX LoggerHook & Time Monitor + if train_parameters: + update_progress_callback = train_parameters.update_progress + else: + update_progress_callback = default_progress_callback + self._time_monitor = TrainingProgressCallback(update_progress_callback) + + dataset.purpose = DatasetPurpose.TRAINING + results = self._train_model(dataset) + + # Check for stop signal when training has stopped. If should_stop is true, training was cancelled and no new + if self._should_stop: + logger.info("Training cancelled.") + self._should_stop = False + self._is_training = False + return + + # get output model + model_ckpt = results.get("final_ckpt") + if model_ckpt is None: + logger.error("cannot find final checkpoint from the results.") + # output_model.model_status = ModelStatus.FAILED + return + # update checkpoint to the newly trained model + self._model_ckpt = model_ckpt + + # get prediction on validation set + self._is_training = False + val_dataset = dataset.get_subset(Subset.VALIDATION) + val_dataset.purpose = DatasetPurpose.INFERENCE + val_preds, val_map = self._infer_model(val_dataset, InferenceParameters(is_evaluation=True)) + + MemCacheHandlerSingleton.delete() + + preds_val_dataset = val_dataset.with_empty_annotations() + if self._hyperparams.postprocessing.result_based_confidence_threshold: + confidence_threshold = 0.0 # Use all predictions to compute best threshold + else: + confidence_threshold = self.confidence_threshold + self._add_predictions_to_dataset( + val_preds, + preds_val_dataset, + confidence_threshold=confidence_threshold, + use_ellipse_shapes=self.use_ellipse_shapes, + ) + + result_set = ResultSetEntity( + model=output_model, + ground_truth_dataset=val_dataset, + prediction_dataset=preds_val_dataset, + ) + + # adjust confidence threshold + if self._hyperparams.postprocessing.result_based_confidence_threshold: + best_confidence_threshold = None + logger.info("Adjusting the confidence threshold") + metric = MetricsHelper.compute_f_measure(result_set, vary_confidence_threshold=True) + if metric.best_confidence_threshold: + best_confidence_threshold = metric.best_confidence_threshold.value + if best_confidence_threshold is None: + raise ValueError("Cannot compute metrics: Invalid confidence threshold!") + logger.info(f"Setting confidence threshold to {best_confidence_threshold} based on results") + self.confidence_threshold = best_confidence_threshold + else: + metric = MetricsHelper.compute_f_measure(result_set, vary_confidence_threshold=False) + + # compose performance statistics + # TODO[EUGENE]: HOW TO ADD A MAE CURVE FOR TaskType.COUNTING? + performance = metric.get_performance() + performance.dashboard_metrics.extend(self._generate_training_metrics(self._learning_curves, val_map)) + logger.info(f"Final model performance: {str(performance)}") + # save resulting model + self.save_model(output_model) + output_model.performance = performance + logger.info("train done.") + + @abstractmethod + def _train_model(self, dataset: DatasetEntity): + """Train model and return the results.""" + raise NotImplementedError + + def infer( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ) -> DatasetEntity: + """Main infer function.""" + logger.info("infer()") + process_saliency_maps = False + explain_predicted_classes = True + + update_progress_callback = default_progress_callback + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress # type: ignore + process_saliency_maps = inference_parameters.process_saliency_maps + explain_predicted_classes = inference_parameters.explain_predicted_classes + + self._time_monitor = InferenceProgressCallback(len(dataset), update_progress_callback) + + dataset.purpose = DatasetPurpose.INFERENCE + prediction_results, _ = self._infer_model(dataset, inference_parameters) + self._add_predictions_to_dataset( + prediction_results, + dataset, + self.confidence_threshold, + process_saliency_maps, + explain_predicted_classes, + self.use_ellipse_shapes, + ) + logger.info("Inference completed") + return dataset + + @abstractmethod + def _infer_model( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ): + """Get inference results from dataset.""" + raise NotImplementedError + + def export( + self, + export_type: ExportType, + output_model: ModelEntity, + precision: ModelPrecision = ModelPrecision.FP32, + dump_features: bool = True, + ): + """Export function of OTX Detection Task.""" + logger.info("Exporting the model") + + self._update_model_export_metadata(output_model, export_type, precision, dump_features) + + results = self._export_model(precision, export_type, dump_features) + outputs = results.get("outputs") + logger.debug(f"results of run_task = {outputs}") + if outputs is None: + raise RuntimeError(results.get("msg")) + + ir_extra_data = get_det_model_api_configuration( + self._task_environment.label_schema, + self._task_type, + self.confidence_threshold, + self._hyperparams.tiling_parameters, + ) + + if export_type == ExportType.ONNX: + ir_extra_data[("model_info", "mean_values")] = results.get("inference_parameters").get("mean_values") + ir_extra_data[("model_info", "scale_values")] = results.get("inference_parameters").get("scale_values") + + onnx_file = outputs.get("onnx") + embed_onnx_model_data(onnx_file, ir_extra_data) + with open(onnx_file, "rb") as f: + output_model.set_data("model.onnx", f.read()) + else: + bin_file = outputs.get("bin") + xml_file = outputs.get("xml") + + embed_ir_model_data(xml_file, ir_extra_data) + + with open(bin_file, "rb") as f: + output_model.set_data("openvino.bin", f.read()) + with open(xml_file, "rb") as f: + output_model.set_data("openvino.xml", f.read()) + + if self._hyperparams.tiling_parameters.enable_tile_classifier: + tile_classifier = None + for partition in outputs.get("partitioned", {}): + if partition.get("tile_classifier"): + tile_classifier = partition.get("tile_classifier") + break + if tile_classifier is None: + raise RuntimeError("invalid status of exporting. tile_classifier should not be None") + if export_type == ExportType.ONNX: + with open(tile_classifier["onnx"], "rb") as f: + output_model.set_data("tile_classifier.onnx", f.read()) + else: + with open(tile_classifier["bin"], "rb") as f: + output_model.set_data("tile_classifier.bin", f.read()) + with open(tile_classifier["xml"], "rb") as f: + output_model.set_data("tile_classifier.xml", f.read()) + + output_model.set_data( + "confidence_threshold", + np.array([self.confidence_threshold], dtype=np.float32).tobytes(), + ) + output_model.set_data("config.json", config_to_bytes(self._hyperparams)) + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self._task_environment.label_schema), + ) + logger.info("Exporting completed") + + @abstractmethod + def _export_model(self, precision: ModelPrecision, export_format: ExportType, dump_features: bool): + """Main export function using training backend.""" + raise NotImplementedError + + def explain( + self, + dataset: DatasetEntity, + explain_parameters: Optional[ExplainParameters] = None, + ) -> DatasetEntity: + """Main explain function of OTX Task.""" + logger.info("explain()") + + update_progress_callback = default_progress_callback + process_saliency_maps = False + explain_predicted_classes = True + if explain_parameters is not None: + update_progress_callback = explain_parameters.update_progress # type: ignore + process_saliency_maps = explain_parameters.process_saliency_maps + explain_predicted_classes = explain_parameters.explain_predicted_classes + + self._time_monitor = InferenceProgressCallback(len(dataset), update_progress_callback) + + outputs = self._explain_model(dataset, explain_parameters) + detections = outputs["detections"] + explain_results = outputs["saliency_maps"] + + self._add_explanations_to_dataset( + detections, explain_results, dataset, process_saliency_maps, explain_predicted_classes + ) + logger.info("Explain completed") + return dataset + + @abstractmethod + def _explain_model(self, dataset: DatasetEntity, explain_parameters: Optional[ExplainParameters]): + raise NotImplementedError + + def evaluate( + self, + output_resultset: ResultSetEntity, + evaluation_metric: Optional[str] = None, + ): + """Evaluate function of OTX Detection Task.""" + logger.info("called evaluate()") + if evaluation_metric is not None: + logger.warning( + f"Requested to use {evaluation_metric} metric, " "but parameter is ignored. Use F-measure instead." + ) + metric = MetricsHelper.compute_f_measure(output_resultset) + output_resultset.performance = metric.get_performance() + logger.info(f"F-measure after evaluation: {output_resultset.performance}") + logger.info("Evaluation completed") + + def _add_predictions_to_dataset( + self, + prediction_results, + dataset, + confidence_threshold=0.0, + process_saliency_maps=False, + explain_predicted_classes=True, + use_ellipse_shapes=False, + ): + """Loop over dataset again to assign predictions. Convert from MMDetection format to OTX format.""" + for dataset_item, (all_results, feature_vector, saliency_map) in zip(dataset, prediction_results): + shapes = self._get_shapes( + all_results, dataset_item.width, dataset_item.height, confidence_threshold, use_ellipse_shapes + ) + dataset_item.append_annotations(shapes) + + if feature_vector is not None: + active_score = TensorEntity(name="representation_vector", numpy=feature_vector.reshape(-1)) + dataset_item.append_metadata_item(active_score, model=self._task_environment.model) + + if saliency_map is not None: + labels = self._labels.copy() + if len(saliency_map) == len(labels) + 1: + # Include the background as the last category + labels.append(LabelEntity("background", Domain.DETECTION)) + + predicted_scored_labels = [] + for shape in shapes: + predicted_scored_labels += shape.get_labels() + + add_saliency_maps_to_dataset_item( + dataset_item=dataset_item, + saliency_map=saliency_map, + model=self._task_environment.model, + labels=labels, + predicted_scored_labels=predicted_scored_labels, + explain_predicted_classes=explain_predicted_classes, + process_saliency_maps=process_saliency_maps, + ) + + def _get_shapes(self, all_results, width, height, confidence_threshold, use_ellipse_shapes): + if self._task_type == TaskType.DETECTION: + shapes = create_detection_shapes( + all_results, width, height, confidence_threshold, use_ellipse_shapes, self._labels + ) + elif self._task_type in { + TaskType.INSTANCE_SEGMENTATION, + TaskType.ROTATED_DETECTION, + }: + shapes = create_mask_shapes( + all_results, + width, + height, + confidence_threshold, + use_ellipse_shapes, + self._labels, + self._task_type is TaskType.ROTATED_DETECTION, + ) + else: + raise RuntimeError(f"OTX results assignment not implemented for task: {self._task_type}") + return shapes + + def _add_explanations_to_dataset( + self, + detections, + explain_results, + dataset, + process_saliency_maps, + explain_predicted_classes, + use_ellipse_shapes=False, + ): + """Add saliency map to the dataset.""" + for dataset_item, detection, saliency_map in zip(dataset, detections, explain_results): + labels = self._labels.copy() + if len(saliency_map) == len(labels) + 1: + # Include the background as the last category + labels.append(LabelEntity("background", Domain.DETECTION)) + + shapes = self._get_shapes( + detection, dataset_item.width, dataset_item.height, self.confidence_threshold, use_ellipse_shapes + ) + predicted_scored_labels = [] + for shape in shapes: + predicted_scored_labels += shape.get_labels() + + add_saliency_maps_to_dataset_item( + dataset_item=dataset_item, + saliency_map=saliency_map, + model=self._task_environment.model, + labels=labels, + predicted_scored_labels=predicted_scored_labels, + explain_predicted_classes=explain_predicted_classes, + process_saliency_maps=process_saliency_maps, + ) + + @staticmethod + def _generate_training_metrics(learning_curves, scores) -> Iterable[MetricsGroup[Any, Any]]: + """Get Training metrics (epochs & scores). + + Parses the mmdetection logs to get metrics from the latest training run + :return output List[MetricsGroup] + """ + output: List[MetricsGroup] = [] + + # Learning curves. + for key, curve in learning_curves.items(): + len_x, len_y = len(curve.x), len(curve.y) + if len_x != len_y: + logger.warning(f"Learning curve {key} has inconsistent number of coordinates ({len_x} vs {len_y}.") + len_x = min(len_x, len_y) + curve.x = curve.x[:len_x] + curve.y = curve.y[:len_x] + metric_curve = CurveMetric( + xs=np.nan_to_num(curve.x).tolist(), + ys=np.nan_to_num(curve.y).tolist(), + name=key, + ) + visualization_info = LineChartInfo(name=key, x_axis_label="Epoch", y_axis_label=key) + output.append(LineMetricsGroup(metrics=[metric_curve], visualization_info=visualization_info)) + + # Final mAP value on the validation set. + output.append( + BarMetricsGroup( + metrics=[ScoreMetric(value=scores, name="mAP")], + visualization_info=BarChartInfo("Validation score", visualization_type=VisualizationType.RADIAL_BAR), + ) + ) + + return output + + def save_model(self, output_model: ModelEntity): + """Save best model weights in DetectionTrainTask.""" + if is_multigpu_child_process(): + return + + logger.info("called save_model") + buffer = io.BytesIO() + hyperparams_str = ids_to_strings(cfg_helper.convert(self._hyperparams, dict, enum_to_str=True)) + labels = {label.name: label.color.rgb_tuple for label in self._labels} + model_ckpt = torch.load(self._model_ckpt) + modelinfo = { + "model": model_ckpt, + "config": hyperparams_str, + "labels": labels, + "confidence_threshold": self.confidence_threshold, + "input_size": self._input_size, + "VERSION": 1, + } + torch.save(modelinfo, buffer) + output_model.set_data("weights.pth", buffer.getvalue()) + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self._task_environment.label_schema), + ) + output_model.precision = self._precision diff --git a/src/otx/algorithms/detection/tools/__init__.py b/src/otx/algorithms/detection/tools/__init__.py new file mode 100644 index 00000000000..b5e3118fe5a --- /dev/null +++ b/src/otx/algorithms/detection/tools/__init__.py @@ -0,0 +1,15 @@ +"""Collection of tools to run detection training extension.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/detection/tools/detection_sample.py b/src/otx/algorithms/detection/tools/detection_sample.py new file mode 100644 index 00000000000..ace9058df0f --- /dev/null +++ b/src/otx/algorithms/detection/tools/detection_sample.py @@ -0,0 +1,358 @@ +"""Sample Code of otx training for detection.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import argparse +import sys + +import numpy as np + +from otx.algorithms.common.utils import get_task_class +from otx.api.configuration.helper import create +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.utils.logger import get_logger + +logger = get_logger() + + +def parse_args(): + """Parse function for getting model template & check export.""" + parser = argparse.ArgumentParser(description="Sample showcasing the new API") + parser.add_argument("template_file_path", help="path to template file") + parser.add_argument("--export", action="store_true") + return parser.parse_args() + + +colors = dict(red=(255, 0, 0), green=(0, 255, 0)) + + +def load_test_dataset(data_type): + """Load Sample dataset for detection.""" + + def gen_image(resolution, x1, y1, x2, y2, color): + width, height = resolution + image = np.full([height, width, 3], fill_value=255, dtype=np.uint8) + image[int(y1 * height) : int(y2 * height), int(x1 * width) : int(x2 * width), :] = np.full( + [int(height * (y2 - y1)), int(width * (x2 - x1)), 3], + fill_value=colors[color], + dtype=np.uint8, + ) + return (image, Rectangle(x1=x1, y1=y1, x2=x2, y2=y2)) + + images = [ + (0.0, 0.0, 0.5, 0.5), + (0.5, 0.0, 1.0, 0.5), + (0.0, 0.5, 0.5, 1.0), + (0.5, 0.5, 1.0, 1.0), + ] + labels = [ + LabelEntity(name="red", domain=Domain.DETECTION, id=0), # OLD class + LabelEntity(name="green", domain=Domain.DETECTION, id=1), + ] + + def get_image(i, subset, label_id): + image, bbox = gen_image((640, 480), *images[i], labels[label_id].name) + return DatasetItemEntity( + media=Image(data=image), + annotation_scene=AnnotationSceneEntity( + annotations=[Annotation(bbox, labels=[ScoredLabel(label=labels[label_id])])], + kind=AnnotationSceneKind.ANNOTATION, + ), + subset=subset, + ) + + old_train = [ + get_image(0, Subset.TRAINING, 0), + get_image(1, Subset.TRAINING, 0), + get_image(2, Subset.TRAINING, 0), + get_image(3, Subset.TRAINING, 0), + get_image(0, Subset.TRAINING, 0), + get_image(1, Subset.TRAINING, 0), + get_image(2, Subset.TRAINING, 0), + get_image(3, Subset.TRAINING, 0), + get_image(0, Subset.TRAINING, 0), + get_image(1, Subset.TRAINING, 0), + ] + old_val = [ + get_image(0, Subset.VALIDATION, 0), + get_image(1, Subset.VALIDATION, 0), + get_image(2, Subset.VALIDATION, 0), + get_image(3, Subset.VALIDATION, 0), + get_image(0, Subset.TESTING, 0), + get_image(1, Subset.TESTING, 0), + get_image(2, Subset.TESTING, 0), + get_image(3, Subset.TESTING, 0), + ] + new_train = [ + get_image(0, Subset.TRAINING, 0), + get_image(1, Subset.TRAINING, 0), + get_image(2, Subset.TRAINING, 0), + get_image(3, Subset.TRAINING, 0), + get_image(0, Subset.TRAINING, 1), + get_image(1, Subset.TRAINING, 1), + get_image(2, Subset.TRAINING, 1), + get_image(3, Subset.TRAINING, 1), + get_image(0, Subset.TRAINING, 1), + get_image(1, Subset.TRAINING, 1), + ] + new_val = [ + get_image(0, Subset.VALIDATION, 0), + get_image(1, Subset.VALIDATION, 0), + get_image(2, Subset.VALIDATION, 0), + get_image(3, Subset.VALIDATION, 0), + get_image(1, Subset.VALIDATION, 1), + get_image(2, Subset.VALIDATION, 1), + get_image(3, Subset.VALIDATION, 1), + get_image(0, Subset.TESTING, 0), + get_image(1, Subset.TESTING, 0), + get_image(2, Subset.TESTING, 0), + get_image(3, Subset.TESTING, 0), + get_image(1, Subset.TESTING, 1), + get_image(2, Subset.TESTING, 1), + get_image(3, Subset.TESTING, 1), + ] + old = old_train + old_val + new = new_train + new_val + if data_type == "old": + return DatasetEntity(old), [labels[0]] + return DatasetEntity(old + new), labels + + +# pylint: disable=too-many-locals, too-many-statements +def main(args): + """Main function of Detection Sample.""" + logger.info("[SL] Train initial model with OLD dataset") + dataset, labels_list = load_test_dataset("old") + labels_schema = LabelSchemaEntity.from_labels(labels_list) + + logger.info(f"Train dataset: {len(dataset.get_subset(Subset.TRAINING))} items") + logger.info(f"Validation dataset: {len(dataset.get_subset(Subset.VALIDATION))} items") + + logger.info("Load model template") + model_template = parse_model_template(args.template_file_path) + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 5 + params.learning_parameters.learning_rate = 0.01 + params.learning_parameters.learning_rate_warmup_iters = 1 + params.learning_parameters.batch_size = 4 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + initial_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, initial_model) + + logger.info("Class-incremental learning with OLD + NEW dataset") + dataset, labels_list = load_test_dataset("new") + labels_schema = LabelSchemaEntity.from_labels(labels_list) + + logger.info(f"Train dataset: {len(dataset.get_subset(Subset.TRAINING))} items") + logger.info(f"Validation dataset: {len(dataset.get_subset(Subset.VALIDATION))} items") + + logger.info("Load model template") + model_template = parse_model_template(args.template_file_path) + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 5 + params.learning_parameters.learning_rate = 0.01 + params.learning_parameters.learning_rate_warmup_iters = 1 + params.learning_parameters.batch_size = 4 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=initial_model, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + output_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, output_model) + + logger.info("Get predictions on the validation set") + validation_dataset = dataset.get_subset(Subset.VALIDATION) + predicted_validation_dataset = task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + task.evaluate(resultset) + logger.info(str(resultset.performance)) + + if args.export: + logger.info("Export model") + exported_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.export(ExportType.OPENVINO, exported_model) + + logger.info("Create OpenVINO Task") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + openvino_task_cls = get_task_class(openvino_task_impl_path) + openvino_task = openvino_task_cls(environment) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Run POT optimization") + optimized_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + openvino_task.optimize( + OptimizationType.POT, + dataset, + optimized_model, + OptimizationParameters(), + ) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=optimized_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Performance of optimized model:") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Running the NNCF optimization") + environment.model = output_model + nncf_task_impl_path = model_template.entrypoints.nncf + nncf_task_cls = get_task_class(nncf_task_impl_path) + nncf_task = nncf_task_cls(environment) + + optimized_model = ModelEntity( + dataset, + configuration=environment.get_model_configuration(), + ) + nncf_task.optimize(OptimizationType.NNCF, dataset, optimized_model) + + logger.info("Inferring the optimised model on the validation set.") + predicted_validation_dataset = nncf_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=optimized_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + + logger.info("Evaluating the optimized model on the validation set.") + nncf_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Exporting the model.") + exported_model = ModelEntity( + train_dataset=dataset, + configuration=environment.get_model_configuration(), + ) + nncf_task.export(ExportType.OPENVINO, exported_model) + environment.model = exported_model + + logger.info("Creating the OpenVINO Task.") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + nncf_openvino_task_cls = get_task_class(openvino_task_impl_path) + nncf_openvino_task = nncf_openvino_task_cls(environment) + + logger.info("Inferring the exported model on the validation set.") + predicted_validation_dataset = nncf_openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + + logger.info("Evaluating the exported model on the validation set.") + resultset = ResultSetEntity( + model=exported_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + nncf_openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + +if __name__ == "__main__": + sys.exit(main(parse_args()) or 0) diff --git a/src/otx/algorithms/detection/tools/detection_semisl_sample.py b/src/otx/algorithms/detection/tools/detection_semisl_sample.py new file mode 100644 index 00000000000..a4ed1c1d652 --- /dev/null +++ b/src/otx/algorithms/detection/tools/detection_semisl_sample.py @@ -0,0 +1,288 @@ +"""Sample Code of otx training for detection.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import argparse +import sys +from random import randint + +import numpy as np + +from otx.algorithms.common.utils import get_task_class +from otx.api.configuration.helper import create +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, + NullAnnotationSceneEntity, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.utils.logger import get_logger + +logger = get_logger() + + +def parse_args(): + """Parse function for getting model template & check export.""" + parser = argparse.ArgumentParser(description="Sample showcasing the new API") + parser.add_argument("template_file_path", help="path to template file") + parser.add_argument("--export", action="store_true") + return parser.parse_args() + + +colors = dict(red=(255, 0, 0), green=(0, 255, 0)) + + +def load_test_dataset(): + """Load Sample dataset for detection.""" + + def gen_image(resolution, x1, y1, x2, y2, color): + width, height = resolution + image = np.full([height, width, 3], fill_value=255, dtype=np.uint8) + image[int(y1 * height) : int(y2 * height), int(x1 * width) : int(x2 * width), :] = np.full( + [int(height * y2) - int(height * y1), int(width * x2) - int(width * x1), 3], + fill_value=colors[color], + dtype=np.uint8, + ) + return (image, Rectangle(x1=x1, y1=y1, x2=x2, y2=y2)) + + labels = [ + LabelEntity(name="red", domain=Domain.DETECTION, id=0), # OLD class + LabelEntity(name="green", domain=Domain.DETECTION, id=1), + ] + + def get_image(subset, label_id): + def get_randcoord(): + # disable B311 random - used for the random sampling not for security/crypto + x1 = randint(0, 9) # nosec B311 + y1 = randint(0, 9) # nosec B311 + x2 = min(x1 + 2, 10) + y2 = min(y1 + 2, 10) + coord = (x1 / 10, y1 / 10, x2 / 10, y2 / 10) + return coord + + coord = get_randcoord() + image, bbox = gen_image((640, 480), *coord, labels[label_id].name) + if subset != Subset.UNLABELED: + return DatasetItemEntity( + media=Image(data=image), + annotation_scene=AnnotationSceneEntity( + annotations=[Annotation(bbox, labels=[ScoredLabel(label=labels[label_id])])], + kind=AnnotationSceneKind.ANNOTATION, + ), + subset=subset, + ) + return DatasetItemEntity( + media=Image(data=image), + annotation_scene=NullAnnotationSceneEntity(), + subset=subset, + ) + + train = [get_image(Subset.TRAINING, 0) for i in range(10)] + train += [get_image(Subset.TRAINING, 1) for i in range(10)] + val = [get_image(Subset.VALIDATION, 0) for i in range(2)] + val += [get_image(Subset.VALIDATION, 1) for i in range(2)] + val += [get_image(Subset.TESTING, 0) for i in range(2)] + val += [get_image(Subset.TESTING, 1) for i in range(2)] + unlabeled = [get_image(Subset.UNLABELED, 0) for i in range(100)] + unlabeled += [get_image(Subset.UNLABELED, 1) for i in range(100)] + + return DatasetEntity(train + val + unlabeled), labels + + +# pylint: disable=too-many-locals, too-many-statements +def main(args): + """Main function of Detection Sample.""" + logger.info("[Semi-SL] Train model with unlabeled dataset") + dataset, labels_list = load_test_dataset() + labels_schema = LabelSchemaEntity.from_labels(labels_list) + + logger.info(f"Train dataset: {len(dataset.get_subset(Subset.TRAINING))} items") + logger.info(f"Validation dataset: {len(dataset.get_subset(Subset.VALIDATION))} items") + + logger.info("Load model template") + model_template = parse_model_template(args.template_file_path) + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 5 + params.learning_parameters.learning_rate = 0.01 + params.learning_parameters.learning_rate_warmup_iters = 1 + params.learning_parameters.batch_size = 4 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + output_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, output_model) + + logger.info("Get predictions on the validation set") + validation_dataset = dataset.get_subset(Subset.VALIDATION) + predicted_validation_dataset = task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=False), + ) + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + task.evaluate(resultset) + logger.info(str(resultset.performance)) + + if args.export: + logger.info("Export model") + exported_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.export(ExportType.OPENVINO, exported_model) + + logger.info("Create OpenVINO Task") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + openvino_task_cls = get_task_class(openvino_task_impl_path) + openvino_task = openvino_task_cls(environment) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Run POT optimization") + optimized_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + openvino_task.optimize( + OptimizationType.POT, + dataset.get_subset(Subset.TRAINING), + optimized_model, + OptimizationParameters(), + ) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=optimized_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Performance of optimized model:") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Running the NNCF optimization") + environment.model = output_model + nncf_task_impl_path = model_template.entrypoints.nncf + nncf_task_cls = get_task_class(nncf_task_impl_path) + nncf_task = nncf_task_cls(environment) + + optimized_model = ModelEntity( + dataset, + configuration=environment.get_model_configuration(), + ) + nncf_task.optimize(OptimizationType.NNCF, dataset, optimized_model) + + logger.info("Inferring the optimised model on the validation set.") + predicted_validation_dataset = nncf_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=optimized_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + + logger.info("Evaluating the optimized model on the validation set.") + nncf_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Exporting the model.") + exported_model = ModelEntity( + train_dataset=dataset, + configuration=environment.get_model_configuration(), + ) + nncf_task.export(ExportType.OPENVINO, exported_model) + environment.model = exported_model + + logger.info("Creating the OpenVINO Task.") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + nncf_openvino_task_cls = get_task_class(openvino_task_impl_path) + nncf_openvino_task = nncf_openvino_task_cls(environment) + + logger.info("Inferring the exported model on the validation set.") + predicted_validation_dataset = nncf_openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + + logger.info("Evaluating the exported model on the validation set.") + resultset = ResultSetEntity( + model=exported_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + nncf_openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + +if __name__ == "__main__": + sys.exit(main(parse_args()) or 0) diff --git a/src/otx/algorithms/detection/tools/instance_segmentation_sample.py b/src/otx/algorithms/detection/tools/instance_segmentation_sample.py new file mode 100644 index 00000000000..354bb40e673 --- /dev/null +++ b/src/otx/algorithms/detection/tools/instance_segmentation_sample.py @@ -0,0 +1,391 @@ +"""Sample Code of otx training for instance segmentation.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import argparse +import sys + +import cv2 +import numpy as np + +from otx.algorithms.common.utils import get_task_class +from otx.api.configuration.helper import create +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.utils.logger import get_logger + +logger = get_logger() + +# pylint: disable=too-many-locals, too-many-statements + + +def parse_args(): + """Parse function for getting model template & check export.""" + parser = argparse.ArgumentParser(description="Sample showcasing the new API") + parser.add_argument("template_file_path", help="path to template file") + parser.add_argument("--export", action="store_true") + return parser.parse_args() + + +colors = dict(red=(255, 0, 0), green=(0, 255, 0)) + + +def load_test_dataset(data_type, task_type=Domain.INSTANCE_SEGMENTATION): + """Load Sample dataset for Instance_segmentation.""" + + def gen_circle_image(resolution): + width, height = resolution + image = np.full([height, width, 3], fill_value=255, dtype=np.uint8) + gt_label = np.full([height, width, 1], fill_value=0, dtype=np.uint8) + cv2.circle(image, (int(height / 2), int(width / 2)), 90, (0, 0, 255), -1) + cv2.circle(gt_label, (int(height / 2), int(width / 2)), 90, 1, -1) + return (image, gt_label) + + def gen_rect_image(resolution): + width, height = resolution + image = np.full([height, width, 3], fill_value=255, dtype=np.uint8) + gt_label = np.full([height, width, 1], fill_value=0, dtype=np.uint8) + cv2.rectangle(image, (int(height * 0.1), int(width * 0.1)), (int(height / 2), int(width / 2)), (0, 255, 0), -1) + cv2.rectangle(gt_label, (int(height * 0.1), int(width * 0.1)), (int(height / 2), int(width / 2)), 2, -1) + return (image, gt_label) + + labels = [ + LabelEntity(name="circle", domain=task_type, id=1), # OLD class + LabelEntity(name="rect", domain=task_type, id=2), + ] + + def get_image(split_type, subset, label_id): + ignored_labels = [] + height, width = 1280, 720 + if label_id == 1: + image, gt_label = gen_circle_image((height, width)) + if split_type == "new" and subset == Subset.TRAINING: + ignored_labels = [LabelEntity(name="rect", domain=Domain.INSTANCE_SEGMENTATION, id=2)] + else: + image, gt_label = gen_rect_image((height, width)) + + height, width = gt_label.shape[:2] + label_mask = gt_label == label_id + label_index_map = label_mask.astype(np.uint8) + contours, hierarchies = cv2.findContours(label_index_map, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + + for contour, hierarchy in zip(contours, hierarchies[0]): + if hierarchy[3] != -1: + continue + + contour = list(contour) + if len(contour) <= 2: + continue + + points = [Point(x=point[0][0] / (width - 1), y=point[0][1] / (height - 1)) for point in contour] + + return DatasetItemEntity( + media=Image(data=image), + annotation_scene=AnnotationSceneEntity( + annotations=[Annotation(Polygon(points=points), labels=[ScoredLabel(label=labels[label_id - 1])])], + kind=AnnotationSceneKind.ANNOTATION, + ), + subset=subset, + ignored_labels=ignored_labels, + ) + + old_train = [ + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + ] + + old_val = [ + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + ] + + new_train = [ + get_image("new", Subset.TRAINING, 1), + get_image("new", Subset.TRAINING, 1), + get_image("new", Subset.TRAINING, 1), + get_image("new", Subset.TRAINING, 1), + get_image("new", Subset.TRAINING, 2), + get_image("new", Subset.TRAINING, 2), + get_image("new", Subset.TRAINING, 2), + get_image("new", Subset.TRAINING, 2), + ] + + new_val = [ + get_image("new", Subset.VALIDATION, 1), + get_image("new", Subset.VALIDATION, 1), + get_image("new", Subset.VALIDATION, 1), + get_image("new", Subset.VALIDATION, 1), + get_image("new", Subset.VALIDATION, 2), + get_image("new", Subset.VALIDATION, 2), + get_image("new", Subset.VALIDATION, 2), + get_image("new", Subset.VALIDATION, 2), + ] + + new_test = [ + get_image("old", Subset.TESTING, 1), + get_image("old", Subset.TESTING, 1), + get_image("old", Subset.TESTING, 1), + get_image("old", Subset.TESTING, 1), + get_image("old", Subset.TESTING, 1), + get_image("old", Subset.TESTING, 1), + get_image("old", Subset.TESTING, 1), + get_image("old", Subset.TESTING, 1), + get_image("new", Subset.TESTING, 1), + get_image("new", Subset.TESTING, 1), + get_image("new", Subset.TESTING, 2), + get_image("new", Subset.TESTING, 2), + get_image("new", Subset.TESTING, 2), + get_image("new", Subset.TESTING, 2), + get_image("new", Subset.TESTING, 2), + get_image("new", Subset.TESTING, 2), + get_image("new", Subset.TESTING, 2), + get_image("new", Subset.TESTING, 2), + get_image("new", Subset.TESTING, 2), + get_image("new", Subset.TESTING, 2), + ] + + old = old_train + old_val + new = new_train + new_val + if data_type == "old": + return DatasetEntity(old * 5), [labels[0]] + if data_type == "test": + return DatasetEntity(new_test * 5), labels + return DatasetEntity((old * 5 + new * 3)), labels + + +def main(args): + """Main function of Detection Sample.""" + logger.info("[SL] Train initial model with OLD dataset") + model_template = parse_model_template(args.template_file_path) + task_type = model_template.task_type.domain + + logger.info("Train initial model with OLD dataset") + dataset, labels_list = load_test_dataset("old", task_type) + labels_schema = LabelSchemaEntity.from_labels(labels_list) + + logger.info(f"Train dataset: {len(dataset.get_subset(Subset.TRAINING))} items") + logger.info(f"Validation dataset: {len(dataset.get_subset(Subset.VALIDATION))} items") + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 5 + params.learning_parameters.learning_rate = 0.01 + params.learning_parameters.learning_rate_warmup_iters = 1 + params.learning_parameters.batch_size = 4 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=None, hyper_parameters=params, label_schema=labels_schema, model_template=model_template + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + initial_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, initial_model) + + logger.info("Class-incremental learning with OLD + NEW dataset") + dataset, labels_list = load_test_dataset("new", task_type) + labels_schema = LabelSchemaEntity.from_labels(labels_list) + + logger.info(f"Train dataset: {len(dataset.get_subset(Subset.TRAINING))} items") + logger.info(f"Validation dataset: {len(dataset.get_subset(Subset.VALIDATION))} items") + + logger.info("Load model template") + model_template = parse_model_template(args.template_file_path) + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 5 + params.learning_parameters.learning_rate = 0.015 + params.learning_parameters.learning_rate_warmup_iters = 1 + params.learning_parameters.batch_size = 4 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=initial_model, hyper_parameters=params, label_schema=labels_schema, model_template=model_template + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + output_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, output_model) + + logger.info("Get predictions on the test set") + testset, _ = load_test_dataset("test", task_type) + validation_dataset = testset.get_subset(Subset.TESTING) + predicted_validation_dataset = task.infer( + validation_dataset.with_empty_annotations(), InferenceParameters(is_evaluation=True) + ) + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + task.evaluate(resultset) + logger.info(str(resultset.performance)) + + if args.export: + logger.info("Export model") + exported_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.export(ExportType.OPENVINO, exported_model) + + logger.info("Create OpenVINO Task") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + openvino_task_cls = get_task_class(openvino_task_impl_path) + openvino_task = openvino_task_cls(environment) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), InferenceParameters(is_evaluation=True) + ) + + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Run POT optimization") + optimized_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + openvino_task.optimize(OptimizationType.POT, dataset, optimized_model, OptimizationParameters()) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), InferenceParameters(is_evaluation=True) + ) + resultset = ResultSetEntity( + model=optimized_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Performance of optimized model:") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Running the NNCF optimization") + environment.model = output_model + nncf_task_impl_path = model_template.entrypoints.nncf + nncf_task_cls = get_task_class(nncf_task_impl_path) + nncf_task = nncf_task_cls(environment) + + optimized_model = ModelEntity( + dataset, + configuration=environment.get_model_configuration(), + ) + nncf_task.optimize(OptimizationType.NNCF, dataset, optimized_model) + + logger.info("Inferring the optimised model on the validation set.") + predicted_validation_dataset = nncf_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=optimized_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + + logger.info("Evaluating the optimized model on the validation set.") + nncf_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Exporting the model.") + exported_model = ModelEntity( + train_dataset=dataset, + configuration=environment.get_model_configuration(), + ) + nncf_task.export(ExportType.OPENVINO, exported_model) + environment.model = exported_model + + logger.info("Creating the OpenVINO Task.") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + nncf_openvino_task_cls = get_task_class(openvino_task_impl_path) + nncf_openvino_task = nncf_openvino_task_cls(environment) + + logger.info("Inferring the exported model on the validation set.") + predicted_validation_dataset = nncf_openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + + logger.info("Evaluating the exported model on the validation set.") + resultset = ResultSetEntity( + model=exported_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + nncf_openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + +if __name__ == "__main__": + sys.exit(main(parse_args()) or 0) diff --git a/src/otx/algorithms/detection/utils/__init__.py b/src/otx/algorithms/detection/utils/__init__.py new file mode 100644 index 00000000000..2a1fe100ce5 --- /dev/null +++ b/src/otx/algorithms/detection/utils/__init__.py @@ -0,0 +1,34 @@ +"""Collection of utils for task implementation in Detection Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .data import ( + format_list_to_str, + get_anchor_boxes, + get_sizes_from_dataset_entity, + load_dataset_items_coco_format, +) +from .utils import create_detection_shapes, create_mask_shapes, generate_label_schema, get_det_model_api_configuration + +__all__ = [ + "get_det_model_api_configuration", + "load_dataset_items_coco_format", + "get_sizes_from_dataset_entity", + "get_anchor_boxes", + "format_list_to_str", + "generate_label_schema", + "create_detection_shapes", + "create_mask_shapes", +] diff --git a/src/otx/algorithms/detection/utils/data.py b/src/otx/algorithms/detection/utils/data.py new file mode 100644 index 00000000000..3dcd7a741af --- /dev/null +++ b/src/otx/algorithms/detection/utils/data.py @@ -0,0 +1,494 @@ +"""Collection of utils for data in Detection Task.""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import json +import os.path as osp +from typing import Any, Dict, List, Optional, Sequence + +import numpy as np +from mmdet.datasets.api_wrappers.coco_api import COCO + +from otx.algorithms.common.utils.data import compute_robust_dataset_statistics +from otx.algorithms.detection.configs.base.configuration import DetectionConfig +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.utils.shape_factory import ShapeFactory +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-instance-attributes, too-many-arguments +def get_classes_from_annotation(path): + """Return classes from annotation.""" + with open(path, encoding="UTF-8") as read_file: + content = json.load(read_file) + categories = [v["name"] for v in sorted(content["categories"], key=lambda x: x["id"])] + return categories + + +class LoadAnnotations: + """Load Annotations class.""" + + def __init__(self, with_bbox: bool = True, with_label: bool = True, with_mask: bool = False): + self.with_bbox = with_bbox + self.with_label = with_label + self.with_mask = with_mask + + @staticmethod + def _load_bboxes(results): + ann_info = results["ann_info"] + results["gt_bboxes"] = ann_info["bboxes"].copy() + + gt_bboxes_ignore = ann_info.get("bboxes_ignore", None) + if gt_bboxes_ignore is not None: + results["gt_bboxes_ignore"] = gt_bboxes_ignore.copy() + results["bbox_fields"].append("gt_bboxes_ignore") + results["bbox_fields"].append("gt_bboxes") + return results + + @staticmethod + def _load_labels(results): + results["gt_labels"] = results["ann_info"]["labels"].copy() + return results + + @staticmethod + def _load_masks(results): + gt_masks = results["ann_info"]["masks"] + results["gt_masks"] = gt_masks + results["mask_fields"].append("gt_masks") + return results + + def __call__(self, results: Dict[str, Any]): + """Callback function of LoadAnnotations.""" + if self.with_bbox: + results = LoadAnnotations._load_bboxes(results) + if results is None: + return None + if self.with_label: + results = LoadAnnotations._load_labels(results) + if self.with_mask: + results = LoadAnnotations._load_masks(results) + + return results + + def __repr__(self): + """String function of LoadAnnotations.""" + repr_str = self.__class__.__name__ + repr_str += f"(with_bbox={self.with_bbox}, " + repr_str += f"with_label={self.with_label})" + return repr_str + + +class CocoDataset: + """CocoDataset.""" + + def __init__( + self, + ann_file: str, + classes: Optional[Sequence[str]] = None, + data_root: Optional[str] = None, + img_prefix: str = "", + test_mode: bool = False, + filter_empty_gt: bool = True, + min_size: Optional[int] = None, + with_mask: bool = False, + ): + self.ann_file = ann_file + self.data_root = data_root + self.img_prefix = img_prefix + self.test_mode = test_mode + self.filter_empty_gt = filter_empty_gt + self.classes = self.get_classes(classes) + self.min_size = min_size + self.with_mask = with_mask + + if self.data_root is not None: + # if not osp.isabs(self.ann_file): + # self.ann_file = osp.join(self.data_root, self.ann_file) + if not (self.img_prefix is None or osp.isabs(self.img_prefix)): + self.img_prefix = osp.join(self.data_root, self.img_prefix) + + self.data_infos = self.load_annotations(self.ann_file) + + if not test_mode: + valid_inds = self._filter_imgs() + self.data_infos = [self.data_infos[i] for i in valid_inds] + + def __len__(self): + """Length of CocoDataset.""" + return len(self.data_infos) + + def pre_pipeline(self, results: Dict[str, Any]): + """Initialize pipeline.""" + results["img_prefix"] = self.img_prefix + results["bbox_fields"] = [] + results["mask_fields"] = [] + results["seg_fields"] = [] + + def _rand_another(self, idx): + """Get Random indices.""" + pool = np.where(self.flag == self.flag[idx])[0] + return np.random.choice(pool) + + def __getitem__(self, idx: int): + """Return dataset item from index.""" + return self.prepare_img(idx) + + def __iter__(self): + """Iterator of CocoDataset.""" + for i in range(len(self)): + yield self[i] + + def prepare_img(self, idx: int): + """Load Annotations function with images.""" + img_info = self.data_infos[idx] + ann_info = self.get_ann_info(idx) + results = dict(img_info=img_info, ann_info=ann_info) + self.pre_pipeline(results) + return LoadAnnotations(with_mask=self.with_mask)(results) + + def get_classes(self, classes: Optional[Sequence[str]] = None): + """Return classes function.""" + if classes is None: + return get_classes_from_annotation(self.ann_file) + + if isinstance(classes, (tuple, list)): + return classes + + raise ValueError(f"Unsupported type {type(classes)} of classes.") + + def load_annotations(self, ann_file): + """Load annotations function from coco.""" + self.coco = COCO(ann_file) + self.cat_ids = self.coco.get_cat_ids(cat_names=self.classes) + self.cat2label = {cat_id: i for i, cat_id in enumerate(self.cat_ids)} + self.img_ids = self.coco.get_img_ids() + data_infos = [] + for i in self.img_ids: + info = self.coco.load_imgs([i])[0] + info["filename"] = info["file_name"] + data_infos.append(info) + return data_infos + + def get_ann_info(self, idx: int): + """Getting Annotation info.""" + img_id = self.data_infos[idx]["id"] + ann_ids = self.coco.get_ann_ids(img_ids=[img_id]) + ann_info = self.coco.load_anns(ann_ids) + return self._parse_ann_info(self.data_infos[idx], ann_info) + + def get_cat_ids(self, idx: int): + """Getting cat_ids.""" + img_id = self.data_infos[idx]["id"] + ann_ids = self.coco.get_ann_ids(img_ids=[img_id]) + ann_info = self.coco.load_anns(ann_ids) + return [ann["category_id"] for ann in ann_info] + + def _filter_imgs(self, min_size=32): + """Filter images too small or without ground truths.""" + valid_inds = [] + # obtain images that contain annotation + ids_with_ann = set(_["image_id"] for _ in self.coco.anns.values()) + # obtain images that contain annotations of the required categories + ids_in_cat = set() + for i, class_id in enumerate(self.cat_ids): + ids_in_cat |= set(self.coco.cat_img_map[class_id]) + # merge the image id sets of the two conditions and use the merged set + # to filter out images if self.filter_empty_gt=True + ids_in_cat &= ids_with_ann + + valid_img_ids = [] + for i, img_info in enumerate(self.data_infos): + img_id = self.img_ids[i] + if self.filter_empty_gt and img_id not in ids_in_cat: + continue + if min(img_info["width"], img_info["height"]) >= min_size: + valid_inds.append(i) + valid_img_ids.append(img_id) + self.img_ids = valid_img_ids + return valid_inds + + def _parse_ann_info(self, img_info, ann_info): # pylint: disable=too-many-locals, too-many-branches + """Parse annotation info.""" + gt_bboxes = [] + gt_labels = [] + gt_bboxes_ignore = [] + gt_masks_ann = [] + for ann in ann_info: + if ann.get("ignore", False): + continue + x1, y1, width, height = ann["bbox"] + inter_w = max(0, min(x1 + width, img_info["width"]) - max(x1, 0)) + inter_h = max(0, min(y1 + height, img_info["height"]) - max(y1, 0)) + if inter_w * inter_h == 0: + continue + if ann["area"] <= 0 or width < 1 or height < 1: + continue + if self.min_size is not None: + if width < self.min_size or height < self.min_size: + continue + if ann["category_id"] not in self.cat_ids: + continue + bbox = [x1, y1, x1 + width, y1 + height] + if ann.get("iscrowd", False): + gt_bboxes_ignore.append(bbox) + else: + gt_bboxes.append(bbox) + gt_labels.append(self.cat2label[ann["category_id"]]) + gt_masks_ann.append(ann.get("segmentation", None)) + + if gt_bboxes: + gt_bboxes = np.array(gt_bboxes, dtype=np.float32) + gt_labels = np.array(gt_labels, dtype=np.int64) + else: + gt_bboxes = np.zeros((0, 4), dtype=np.float32) + gt_labels = np.array([], dtype=np.int64) + + if gt_bboxes_ignore: + gt_bboxes_ignore = np.array(gt_bboxes_ignore, dtype=np.float32) + else: + gt_bboxes_ignore = np.zeros((0, 4), dtype=np.float32) + + seg_map = img_info["filename"].replace("jpg", "png") + + ann = dict( + bboxes=gt_bboxes, + labels=gt_labels, + bboxes_ignore=gt_bboxes_ignore, + masks=gt_masks_ann, + seg_map=seg_map, + ) + + return ann + + +def find_label_by_name(labels: List[LabelEntity], name: str, domain: Domain): + """Return label from name.""" + matching_labels = [label for label in labels if label.name == name] + if len(matching_labels) == 1: + return matching_labels[0] + if len(matching_labels) == 0: + label = LabelEntity(name=name, domain=domain, id=ID(len(labels))) + labels.append(label) + return label + raise ValueError("Found multiple matching labels") + + +def load_dataset_items_coco_format( + ann_file_path: str, + data_root_dir: str, + domain: Domain, + subset: Subset = Subset.NONE, + labels_list: Optional[List[LabelEntity]] = None, + with_mask: bool = False, +): # pylint: disable=too-many-locals + """Load dataset from CocoDataset.""" + test_mode = subset in {Subset.VALIDATION, Subset.TESTING} + + coco_dataset = CocoDataset( + ann_file=ann_file_path, + data_root=data_root_dir, + classes=None, + test_mode=test_mode, + with_mask=with_mask, + ) + coco_dataset.test_mode = False + for label_name in coco_dataset.classes: + find_label_by_name(labels_list, label_name, domain) + + dataset_items = [] + for item in coco_dataset: + + def create_gt_box(x1, y1, x2, y2, label_name): + return Annotation( + Rectangle(x1=x1, y1=y1, x2=x2, y2=y2), + labels=[ScoredLabel(label=find_label_by_name(labels_list, label_name, domain))], + ) + + def create_gt_polygon(polygon_group, label_name): + if len(polygon_group) != 1: + raise RuntimeError( + "Complex instance segmentation masks consisting of several polygons are not supported." + ) + + return Annotation( + Polygon(points=polygon_group[0]), + labels=[ScoredLabel(label=find_label_by_name(labels_list, label_name, domain))], + ) + + img_height = item["img_info"].get("height") + img_width = item["img_info"].get("width") + divisor = np.array( + [img_width, img_height, img_width, img_height], + dtype=item["gt_bboxes"].dtype, + ) + bboxes = item["gt_bboxes"] / divisor + labels = item["gt_labels"] + + assert len(bboxes) == len(labels) + if with_mask: + polygons = item["gt_masks"] + assert len(bboxes) == len(polygons) + normalized_polygons = [] # type: List + for polygon_group in polygons: + normalized_polygons.append([]) + for polygon in polygon_group: + normalized_polygon = [p / divisor[i % 2] for i, p in enumerate(polygon)] + points = [ + Point(normalized_polygon[i], normalized_polygon[i + 1]) for i in range(0, len(polygon), 2) + ] + normalized_polygons[-1].append(points) + + if item["img_prefix"] is not None: + filename = osp.join(item["img_prefix"], item["img_info"]["filename"]) + else: + filename = item["img_info"]["filename"] + + if with_mask: + shapes = [ + create_gt_polygon(polygon_group, coco_dataset.classes[label_id]) + for polygon_group, label_id in zip(normalized_polygons, labels) + ] + else: + shapes = [ + create_gt_box(x1, y1, x2, y2, coco_dataset.classes[label_id]) + for (x1, y1, x2, y2), label_id in zip(bboxes, labels) + ] + + dataset_item = DatasetItemEntity( + media=Image(file_path=filename), + annotation_scene=AnnotationSceneEntity(annotations=shapes, kind=AnnotationSceneKind.ANNOTATION), + subset=subset, + ) + dataset_items.append(dataset_item) + + return dataset_items + + +def get_sizes_from_dataset_entity(dataset: DatasetEntity, target_wh: List[int]): + """Function to get sizes of instances in DatasetEntity and to resize it to the target size. + + :param dataset: DatasetEntity in which to get statistics + :param target_wh: target width and height of the dataset + :return list: tuples with width and height of each instance + """ + wh_stats = [] + for item in dataset: + for ann in item.get_annotations(include_empty=False): + has_detection_labels = any( + label.domain == Domain.DETECTION for label in ann.get_labels(include_empty=False) + ) + if has_detection_labels: + box = ShapeFactory.shape_as_rectangle(ann.shape) + width = box.width * target_wh[0] + height = box.height * target_wh[1] + wh_stats.append((width, height)) + return wh_stats + + +def get_anchor_boxes(wh_stats: List[tuple], group_as: List[int]): + """Get anchor box widths & heights.""" + from sklearn.cluster import KMeans + + kmeans = KMeans(init="k-means++", n_clusters=sum(group_as), random_state=0).fit(wh_stats) + centers = kmeans.cluster_centers_ + + areas = np.sqrt(np.prod(centers, axis=1)) + idx = np.argsort(areas) + + widths = centers[idx, 0] + heights = centers[idx, 1] + + group_as = np.cumsum(group_as[:-1]) + widths, heights = np.split(widths, group_as), np.split(heights, group_as) + widths = [width.tolist() for width in widths] + heights = [height.tolist() for height in heights] + return widths, heights + + +def format_list_to_str(value_lists: list): + """Decrease floating point digits in logs.""" + str_value = "" + for value_list in value_lists: + str_value += "[" + ", ".join(f"{value:.2f}" for value in value_list) + "], " + return f"[{str_value[:-2]}]" + + +def adaptive_tile_params( + tiling_parameters: DetectionConfig.BaseTilingParameters, dataset: DatasetEntity, object_tile_ratio=0.03, rule="avg" +): + """Config tile parameters. + + Adapt based on annotation statistics. + i.e. tile size, tile overlap, ratio and max objects per sample + + Args: + tiling_parameters (BaseTilingParameters): tiling parameters of the model + dataset (DatasetEntity): training dataset + object_tile_ratio (float, optional): The desired ratio of object size and tile size. Defaults to 16/512=0.03. + rule (str, optional): min or avg. In `min` mode, tile size is computed based on the smallest object, and in + `avg` mode tile size is computed by averaging all the object areas. Defaults to "avg". + + """ + assert rule in ["min", "avg"], f"Unknown rule: {rule}" + + stat = compute_robust_dataset_statistics(dataset, ann_stat=True) + max_num_objects = round(stat["annotation"]["num_per_image"]["max"]) + avg_size = stat["annotation"]["size_of_shape"]["avg"] + min_size = stat["annotation"]["size_of_shape"]["robust_min"] + max_size = stat["annotation"]["size_of_shape"]["robust_max"] + logger.info(f"----> [stat] scale avg: {avg_size}") + logger.info(f"----> [stat] scale min: {min_size}") + logger.info(f"----> [stat] scale max: {max_size}") + + if rule == "min": + object_size = min_size + elif rule == "avg": + object_size = avg_size + + logger.info("[Adaptive tiling pararms]") + object_tile_ratio = tiling_parameters.object_tile_ratio + tile_size = int(object_size / object_tile_ratio) + tile_overlap = max_size / tile_size + logger.info(f"----> {rule}_object_size: {object_size}") + logger.info(f"----> max_object_size: {max_size}") + logger.info(f"----> object_tile_ratio: {object_tile_ratio}") + logger.info(f"----> tile_size: {object_size} / {object_tile_ratio} = {tile_size}") + logger.info(f"----> tile_overlap: {max_size} / {tile_size} = {tile_overlap}") + + if tile_overlap >= tiling_parameters.get_metadata("tile_overlap")["max_value"]: + # Use the average object area if the tile overlap is too large to prevent 0 stride. + tile_overlap = object_size / tile_size + logger.info(f"----> (too big) tile_overlap: {object_size} / {tile_size} = {tile_overlap}") + + # validate parameters are in range + tile_size = max( + tiling_parameters.get_metadata("tile_size")["min_value"], + min(tiling_parameters.get_metadata("tile_size")["max_value"], tile_size), + ) + tile_overlap = max( + tiling_parameters.get_metadata("tile_overlap")["min_value"], + min(tiling_parameters.get_metadata("tile_overlap")["max_value"], tile_overlap), + ) + max_num_objects = max( + tiling_parameters.get_metadata("tile_max_number")["min_value"], + min(tiling_parameters.get_metadata("tile_max_number")["max_value"], max_num_objects), + ) + + tiling_parameters.tile_size = tile_size + tiling_parameters.tile_max_number = max_num_objects + tiling_parameters.tile_overlap = tile_overlap diff --git a/src/otx/algorithms/detection/utils/utils.py b/src/otx/algorithms/detection/utils/utils.py new file mode 100644 index 00000000000..5c68ecfa31e --- /dev/null +++ b/src/otx/algorithms/detection/utils/utils.py @@ -0,0 +1,323 @@ +"""Utils for OTX Detection.""" +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import colorsys +import random +from typing import Any, List, Optional, Sequence, Tuple + +import cv2 +import numpy as np +import pycocotools.mask as mask_util + +from otx.api.entities.annotation import Annotation +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle + +# pylint: disable=invalid-name + + +class ColorPalette: + """ColorPalette class.""" + + def __init__(self, n: int, rng: Optional[random.Random] = None): + assert n > 0 + + if rng is None: + rng = random.Random(0xACE) + + candidates_num = 100 + hsv_colors = [(1.0, 1.0, 1.0)] + for _ in range(1, n): + colors_candidates = [ + (rng.random(), rng.uniform(0.8, 1.0), rng.uniform(0.5, 1.0)) for _ in range(candidates_num) + ] + min_distances = [self._min_distance(hsv_colors, c) for c in colors_candidates] + arg_max = np.argmax(min_distances) + hsv_colors.append(colors_candidates[arg_max]) + + self.palette = [Color(*self._hsv2rgb(*hsv)) for hsv in hsv_colors] + + @staticmethod + def _dist(c1, c2): + dh = min(abs(c1[0] - c2[0]), 1 - abs(c1[0] - c2[0])) * 2 + ds = abs(c1[1] - c2[1]) + dv = abs(c1[2] - c2[2]) + return dh * dh + ds * ds + dv * dv + + @classmethod + def _min_distance(cls, colors_set, color_candidate): + distances = [cls._dist(o, color_candidate) for o in colors_set] + return np.min(distances) + + @staticmethod + def _hsv2rgb(h, s, v): + return tuple(round(c * 255) for c in colorsys.hsv_to_rgb(h, s, v)) + + def __getitem__(self, n: int): + """Return item from index function ColorPalette.""" + return self.palette[n % len(self.palette)] + + def __len__(self): + """Return length of ColorPalette.""" + return len(self.palette) + + +def generate_label_schema(label_names: Sequence[str], label_domain: Domain = Domain.DETECTION): + """Generating label_schema function.""" + colors = ColorPalette(len(label_names)) if len(label_names) > 0 else [] + not_empty_labels = [ + LabelEntity(name=name, color=colors[i], domain=label_domain, id=ID(f"{i:08}")) # type: ignore + for i, name in enumerate(label_names) + ] + emptylabel = LabelEntity( + name="Empty label", + color=Color(42, 43, 46), + is_empty=True, + domain=label_domain, + id=ID(f"{len(not_empty_labels):08}"), + ) + + label_schema = LabelSchemaEntity() + exclusive_group = LabelGroup(name="labels", labels=not_empty_labels, group_type=LabelGroupType.EXCLUSIVE) + empty_group = LabelGroup(name="empty", labels=[emptylabel], group_type=LabelGroupType.EMPTY_LABEL) + label_schema.add_group(exclusive_group) + label_schema.add_group(empty_group) + return label_schema + + +def get_det_model_api_configuration( + label_schema: LabelSchemaEntity, task_type: TaskType, confidence_threshold: float, tiling_parameters: Any +): + """Get ModelAPI config.""" + omz_config = {} + all_labels = "" + all_label_ids = "" + if task_type == TaskType.DETECTION: + omz_config[("model_info", "model_type")] = "ssd" + omz_config[("model_info", "task_type")] = "detection" + if task_type == TaskType.INSTANCE_SEGMENTATION: + omz_config[("model_info", "model_type")] = "MaskRCNN" + omz_config[("model_info", "task_type")] = "instance_segmentation" + all_labels = "otx_empty_lbl " + all_label_ids = "None " + if tiling_parameters.enable_tiling: + omz_config[("model_info", "resize_type")] = "fit_to_window_letterbox" + if task_type == TaskType.ROTATED_DETECTION: + omz_config[("model_info", "model_type")] = "MaskRCNN" + omz_config[("model_info", "task_type")] = "rotated_detection" + all_labels = "otx_empty_lbl " + all_label_ids = "None " + if tiling_parameters.enable_tiling: + omz_config[("model_info", "resize_type")] = "fit_to_window_letterbox" + + omz_config[("model_info", "confidence_threshold")] = str(confidence_threshold) + omz_config[("model_info", "iou_threshold")] = str(0.5) + + if tiling_parameters.enable_tiling: + omz_config[("model_info", "tile_size")] = str( + int(tiling_parameters.tile_size * tiling_parameters.tile_ir_scale_factor) + ) + omz_config[("model_info", "tiles_overlap")] = str( + tiling_parameters.tile_overlap / tiling_parameters.tile_ir_scale_factor + ) + omz_config[("model_info", "max_pred_number")] = str(tiling_parameters.tile_max_number) + + for lbl in label_schema.get_labels(include_empty=False): + all_labels += lbl.name.replace(" ", "_") + " " + all_label_ids += f"{lbl.id_} " + + omz_config[("model_info", "labels")] = all_labels.strip() + omz_config[("model_info", "label_ids")] = all_label_ids.strip() + + return omz_config + + +def expand_box(box: np.ndarray, scale_h: float, scale_w: float): + """Expand the box. + + Args: + box (np.ndarray): bounding box + scale_h (float): scaling factor for height + scale_w (float): scaling factor for width + + Returns: + expanded box (np.ndarray): x1, y1, x2, y2 coordinates of the expanded box + """ + w_half = (box[2] - box[0]) * 0.5 + h_half = (box[3] - box[1]) * 0.5 + x_c = (box[2] + box[0]) * 0.5 + y_c = (box[3] + box[1]) * 0.5 + w_half *= scale_w + h_half *= scale_h + expanded_box = np.zeros(box.shape) + expanded_box[0] = x_c - w_half + expanded_box[2] = x_c + w_half + expanded_box[1] = y_c - h_half + expanded_box[3] = y_c + h_half + return expanded_box + + +def mask_resize(box: np.ndarray, mask: np.ndarray, img_height: int, img_width: int): + """Resize mask to the size of the bounding box. + + Args: + box (np.ndarray): bounding box which enclosing the mask + mask (np.ndarray): mask to be resize + img_height (int): image height + img_width (int): image width + + Returns: + bit_mask (np.ndarray): full size mask + """ + # scaling bbox to prevent up-sampling artifacts on segment borders. + mask = np.pad(mask, ((1, 1), (1, 1)), "constant", constant_values=0) + scale_h = mask.shape[0] / (mask.shape[0] - 2.0) + scale_w = mask.shape[1] / (mask.shape[1] - 2.0) + extended_box = expand_box(box, scale_h=scale_h, scale_w=scale_w).astype(int) + w, h = np.maximum(extended_box[2:] - extended_box[:2] + 1, 1) + x0, y0 = np.clip(extended_box[:2], a_min=0, a_max=[img_width, img_height]) + x1, y1 = np.clip(extended_box[2:] + 1, a_min=0, a_max=[img_width, img_height]) + mask = cv2.resize(mask.astype(np.float32), (w, h)) > 0.5 + mask = mask.astype(np.uint8) + bit_mask = np.zeros((img_height, img_width), dtype=np.uint8) + bit_mask[y0:y1, x0:x1] = mask[ + (y0 - extended_box[1]) : (y1 - extended_box[1]), + (x0 - extended_box[0]) : (x1 - extended_box[0]), + ] + return bit_mask + + +def create_detection_shapes( + pred_results: List[np.ndarray], + width: int, + height: int, + confidence_threshold: float, + use_ellipse_shapes: bool, + labels: List, +): + """Create prediction detection shapes. + + Args: + pred_results (list(np.ndarray)): per class predicted boxes + width (int): image width + height (int): image height + confidence_threshold (float): confidence threshold for filtering predictions + use_ellipse_shapes (bool): if True, use ellipse shapes + labels (list): dataset labels + + Returns: + shapes: list of prediction shapes (Annotation) + """ + + shapes = [] + for label_idx, detections in enumerate(pred_results): + for det in detections: + probability = float(det[4]) + coords = det[:4].astype(float).copy() + coords /= np.array([width, height, width, height], dtype=float) + coords = np.clip(coords, 0, 1) + + if (probability < confidence_threshold) or (coords[3] - coords[1] <= 0 or coords[2] - coords[0] <= 0): + continue + + assigned_label = [ScoredLabel(labels[label_idx], probability=probability)] + if not use_ellipse_shapes: + shapes.append( + Annotation( + Rectangle(x1=coords[0], y1=coords[1], x2=coords[2], y2=coords[3]), + labels=assigned_label, + ) + ) + else: + shapes.append( + Annotation( + Ellipse(coords[0], coords[1], coords[2], coords[3]), + labels=assigned_label, + ) + ) + return shapes + + +def create_mask_shapes( + pred_results: Tuple, + width: int, + height: int, + confidence_threshold: float, + use_ellipse_shapes: bool, + labels: List, + rotated_polygon: bool = False, +): + """Create prediction mask shapes. + + Args: + pred_results (tuple): tuple of predicted boxes and masks for each dataset item + width (int): image width + height (int): image height + confidence_threshold (float): confidence threshold for filtering predictions + use_ellipse_shapes (bool): if True, use ellipse shapes + labels (list): dataset labels + rotated_polygon (bool, optional): if True, use rotated polygons for mask shapes + + Returns: + shapes: list of prediction shapes (Annotation) + """ + shapes = [] + for label_idx, (boxes, masks) in enumerate(zip(*pred_results)): + for mask, box in zip(masks, boxes): + probability = float(box[4]) + if probability < confidence_threshold: + continue + + assigned_label = [ScoredLabel(labels[label_idx], probability=probability)] + if not use_ellipse_shapes: + if isinstance(mask, dict): + mask = mask_util.decode(mask) + + if mask.shape[0] != height or mask.shape[1] != width: + # resize mask to the size of the bounding box + coords = box[:4].astype(float).copy() + mask = mask_resize(coords, mask, height, width) + + if mask.sum() == 0: + continue + + contours, hierarchies = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + if hierarchies is None: + continue + for contour, hierarchy in zip(contours, hierarchies[0]): + # skip inner contours + if hierarchy[3] != -1 or len(contour) <= 2: + continue + + if rotated_polygon: + box_points = cv2.boxPoints(cv2.minAreaRect(contour)) + points = [Point(x=(point[0]) / width, y=(point[1]) / height) for point in box_points] + else: + points = [Point(x=(point[0][0]) / width, y=(point[0][1]) / height) for point in contour] + + polygon = Polygon(points=points) + if cv2.contourArea(contour) > 0 and polygon.get_area() > 1e-12: + shapes.append(Annotation(polygon, labels=assigned_label, id=ID(f"{label_idx:08}"))) + else: + ellipse = Ellipse((box[0]) / width, (box[1]) / height, (box[2]) / width, (box[3]) / height) + shapes.append(Annotation(ellipse, labels=assigned_label, id=ID(f"{label_idx:08}"))) + return shapes diff --git a/src/otx/algorithms/segmentation/__init__.py b/src/otx/algorithms/segmentation/__init__.py new file mode 100644 index 00000000000..c0318fc2bbc --- /dev/null +++ b/src/otx/algorithms/segmentation/__init__.py @@ -0,0 +1,11 @@ +"""OTX Algorithms - Segmentation.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +MMSEG_AVAILABLE = True + +try: + import mmseg # noqa: F401 +except ImportError: + MMSEG_AVAILABLE = False diff --git a/src/otx/algorithms/segmentation/adapters/__init__.py b/src/otx/algorithms/segmentation/adapters/__init__.py new file mode 100644 index 00000000000..53d34a44210 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/__init__.py @@ -0,0 +1,16 @@ +"""Adapters for Segmentation.""" + + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/__init__.py new file mode 100644 index 00000000000..16c19a4ccfd --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/__init__.py @@ -0,0 +1,58 @@ +"""OTX Adapters - mmseg.""" + + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .datasets import OTXSegDataset +from .models import ( + ConstantScalarScheduler, + CrossEntropyLossWithIgnore, + DetConB, + DetConLoss, + LiteHRNet, + MeanTeacherSegmentor, + MMOVBackbone, + MMOVDecodeHead, + PolyScalarScheduler, + SelfSLMLP, + StepScalarScheduler, + SupConDetConB, +) + +# fmt: off +# isort: off +# FIXME: openvino pot library adds stream handlers to root logger +# which makes annoying duplicated logging +# pylint: disable=no-name-in-module,wrong-import-order +from mmseg.utils import get_root_logger # type: ignore # (false positive) +get_root_logger().propagate = False +# fmt: off +# isort: on + +__all__ = [ + "OTXSegDataset", + "LiteHRNet", + "MMOVBackbone", + "MMOVDecodeHead", + "DetConLoss", + "SelfSLMLP", + "ConstantScalarScheduler", + "PolyScalarScheduler", + "StepScalarScheduler", + "DetConB", + "CrossEntropyLossWithIgnore", + "SupConDetConB", + "MeanTeacherSegmentor", +] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/apis/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/apis/__init__.py new file mode 100644 index 00000000000..9a003db651d --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/apis/__init__.py @@ -0,0 +1,8 @@ +"""Adapters of classification - mmseg.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .train import train_segmentor + +__all__ = ["train_segmentor"] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/apis/train.py b/src/otx/algorithms/segmentation/adapters/mmseg/apis/train.py new file mode 100644 index 00000000000..f0e4975939d --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/apis/train.py @@ -0,0 +1,170 @@ +"""Train function for segmentation task.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# Copyright (c) OpenMMLab. All rights reserved. +import os +import warnings + +import mmcv +import torch +from mmcv.runner import HOOKS, DistSamplerSeedHook, EpochBasedRunner, build_runner +from mmcv.utils import build_from_cfg +from mmseg import digit_version +from mmseg.core import DistEvalHook, EvalHook, build_optimizer +from mmseg.datasets import build_dataloader, build_dataset +from mmseg.utils import build_ddp, find_latest_checkpoint, get_root_logger +from mmseg.utils.util_distribution import build_dp, dp_factory + +from otx.algorithms.common.adapters.mmcv.utils import HPUDataParallel, XPUDataParallel +from otx.algorithms.common.adapters.mmcv.utils.hpu_optimizers import HABANA_OPTIMIZERS + +dp_factory["xpu"] = XPUDataParallel +dp_factory["hpu"] = HPUDataParallel + + +def train_segmentor(model, dataset, cfg, distributed=False, validate=False, timestamp=None, meta=None): + """Launch segmentor training.""" + logger = get_root_logger(cfg.log_level) + + # prepare data loaders + dataset = dataset if isinstance(dataset, (list, tuple)) else [dataset] + # The default loader config + loader_cfg = dict( + # cfg.gpus will be ignored if distributed + num_gpus=len(cfg.gpu_ids), + dist=distributed, + seed=cfg.seed, + drop_last=True, + ) + # The overall dataloader settings + loader_cfg.update( + { + k: v + for k, v in cfg.data.items() + if k not in ["train", "val", "test", "train_dataloader", "val_dataloader", "test_dataloader"] + } + ) + + # The specific dataloader settings + train_loader_cfg = {**loader_cfg, **cfg.data.get("train_dataloader", {})} + data_loaders = [build_dataloader(ds, **train_loader_cfg) for ds in dataset] + + if cfg.device == "xpu": + model.to(f"xpu:{cfg.gpu_ids[0]}") + + # put model on devices + if distributed: + find_unused_parameters = cfg.get("find_unused_parameters", False) + # Sets the `find_unused_parameters` parameter in + # DDP wrapper + model = build_ddp( + model, + cfg.device, + device_ids=[int(os.environ["LOCAL_RANK"])], + broadcast_buffers=False, + find_unused_parameters=find_unused_parameters, + ) + else: + if not torch.cuda.is_available(): # noqa + assert digit_version(mmcv.__version__) >= digit_version( + "1.4.4" + ), "Please use MMCV >= 1.4.4 for CPU training!" + + if cfg.device == "hpu": + use_autocast = bool(cfg.get("fp16_", False)) + model = build_dp(model, cfg.device, device_ids=cfg.gpu_ids, enable_autocast=use_autocast) + model.to(model.src_device_obj) + else: + model = build_dp(model, cfg.device, device_ids=cfg.gpu_ids) + + # build runner + if cfg.device == "hpu": + optim_type = cfg.optimizer.get("type", "SGD") + if optim_type == "Adam": # to avoid segmentation fault + optim_type = "AdamW" + cfg.optimizer.type = optim_type + if (new_type := "Fused" + optim_type) in HABANA_OPTIMIZERS: + cfg.optimizer["type"] = new_type + + optimizer = build_optimizer(model, cfg.optimizer) + + if cfg.device == "xpu": + if cfg.optimizer_config.get("bf16_training", False): + logger.warning("XPU supports fp32 training only currently.") + dtype = torch.float32 + model.train() + model, optimizer = torch.xpu.optimize(model, optimizer=optimizer, dtype=dtype) + + if "bf16_training" in cfg.optimizer_config: + # Remove unused parameters in runner + cfg.optimizer_config.pop("bf16_training") + + if cfg.get("runner") is None: + cfg.runner = {"type": "IterBasedRunner", "max_iters": cfg.total_iters} + warnings.warn( + "config is now expected to have a `runner` section, " "please set `runner` in your config.", UserWarning + ) + + runner = build_runner( + cfg.runner, + default_args=dict( + model=model, batch_processor=None, optimizer=optimizer, work_dir=cfg.work_dir, logger=logger, meta=meta + ), + ) + + # register hooks + runner.register_training_hooks( + cfg.lr_config, cfg.optimizer_config, cfg.checkpoint_config, cfg.log_config, cfg.get("momentum_config", None) + ) + if distributed: + # when distributed training by epoch, using`DistSamplerSeedHook` to set + # the different seed to distributed sampler for each epoch, it will + # shuffle dataset at each epoch and avoid overfitting. + if isinstance(runner, EpochBasedRunner): + runner.register_hook(DistSamplerSeedHook()) + + # an ugly walkaround to make the .log and .log.json filenames the same + runner.timestamp = timestamp + + # register eval hooks + if validate: + val_dataset = build_dataset(cfg.data.val, dict(test_mode=True)) + # The specific dataloader settings + val_loader_cfg = { + **loader_cfg, + "samples_per_gpu": 1, + "shuffle": False, # Not shuffle by default + **cfg.data.get("val_dataloader", {}), + } + val_dataloader = build_dataloader(val_dataset, **val_loader_cfg) + eval_cfg = cfg.get("evaluation", {}) + eval_cfg["by_epoch"] = cfg.runner["type"] != "IterBasedRunner" + eval_hook = DistEvalHook if distributed else EvalHook + # In this PR (https://github.com/open-mmlab/mmcv/pull/1193), the + # priority of IterTimerHook has been modified from 'NORMAL' to 'LOW'. + runner.register_hook(eval_hook(val_dataloader, **eval_cfg), priority="LOW") + + # user-defined hooks + if cfg.get("custom_hooks", None): + custom_hooks = cfg.custom_hooks + assert isinstance(custom_hooks, list), f"custom_hooks expect list type, but got {type(custom_hooks)}" + for hook_cfg in cfg.custom_hooks: + assert isinstance(hook_cfg, dict), ( + "Each item in custom_hooks expects dict type, but got " f"{type(hook_cfg)}" + ) + hook_cfg = hook_cfg.copy() + priority = hook_cfg.pop("priority", "NORMAL") + hook = build_from_cfg(hook_cfg, HOOKS) + runner.register_hook(hook, priority=priority) + + if cfg.resume_from is None and cfg.get("auto_resume"): + resume_from = find_latest_checkpoint(cfg.work_dir) + if resume_from is not None: + cfg.resume_from = resume_from + if cfg.resume_from: + runner.resume(cfg.resume_from) + elif cfg.load_from: + runner.load_checkpoint(cfg.load_from) + runner.run(data_loaders, cfg.workflow) diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py b/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py new file mode 100644 index 00000000000..4a28eb80a5f --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py @@ -0,0 +1,199 @@ +"""Base configurer for mmseg config.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import os +from collections import OrderedDict +from typing import Any, Optional, Tuple + +import torch +from mmcv.runner import CheckpointLoader +from mmcv.utils import Config, ConfigDict + +from otx.algorithms.common.adapters.mmcv.clsincr_mixin import IncrConfigurerMixin +from otx.algorithms.common.adapters.mmcv.configurer import BaseConfigurer +from otx.algorithms.common.adapters.mmcv.semisl_mixin import SemiSLConfigurerMixin +from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + InputSizeManager, + remove_custom_hook, +) +from otx.algorithms.common.utils import append_dist_rank_suffix +from otx.algorithms.segmentation.adapters.mmseg.models.heads import otx_head_factory +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-public-methods +class SegmentationConfigurer(BaseConfigurer): + """Patch config to support otx train.""" + + def configure_task( + self, + cfg: Config, + **kwargs, + ) -> None: + """Patch config to support training algorithm.""" + super().configure_task(cfg, **kwargs) + if "task_adapt" in cfg: + self.configure_decode_head(cfg) + + def configure_classes(self, cfg): + """Patch classes for model and dataset.""" + org_model_classes = self.get_model_classes(cfg) + data_classes = self.get_data_classes(cfg) + + if "background" not in org_model_classes: + org_model_classes = ["background"] + org_model_classes + if "background" not in data_classes: + data_classes = ["background"] + data_classes + + # Model classes + if self.task_adapt_op == "REPLACE": + if len(data_classes) == 1: # background + model_classes = org_model_classes.copy() + else: + model_classes = data_classes.copy() + elif self.task_adapt_op == "MERGE": + model_classes = org_model_classes + [cls for cls in data_classes if cls not in org_model_classes] + else: + raise KeyError(f"{self.task_adapt_op} is not supported for task_adapt options!") + + cfg.task_adapt.final = model_classes + cfg.model.task_adapt = ConfigDict( + src_classes=org_model_classes, + dst_classes=model_classes, + ) + + self.org_model_classes = org_model_classes + self.model_classes = model_classes + self.data_classes = data_classes + + self._configure_head(cfg) + + def configure_decode_head(self, cfg: Config) -> None: + """Change to incremental loss (ignore mode) and substitute head with otx universal head.""" + ignore = cfg.get("ignore", False) + for head in ("decode_head", "auxiliary_head"): + decode_head = cfg.model.get(head, None) + if decode_head is not None: + decode_head.base_type = decode_head.type + decode_head.type = otx_head_factory + if ignore: + cfg_loss_decode = ConfigDict( + type="CrossEntropyLossWithIgnore", + use_sigmoid=False, + loss_weight=1.0, + ) + decode_head.loss_decode = cfg_loss_decode + + # pylint: disable=too-many-branches + def _configure_head(self, cfg: Config) -> None: + """Patch number of classes of head.""" + if "decode_head" in cfg.model: + decode_head = cfg.model.decode_head + if isinstance(decode_head, list): + for head in decode_head: + head.num_classes = len(self.model_classes) + else: + decode_head.num_classes = len(self.model_classes) + + # For SupConDetCon + if "SupConDetCon" in cfg.model.type: + cfg.model.num_classes = len(self.model_classes) + + if "auxiliary_head" in cfg.model: + cfg.model.auxiliary_head.num_classes = len(self.model_classes) + + def configure_ckpt(self, cfg: Config, model_ckpt_path: str) -> None: + """Patch checkpoint path for pretrained weight. + + Replace cfg.load_from to model_ckpt_path + Replace cfg.load_from to pretrained + Replace cfg.resume_from to cfg.load_from + """ + super().configure_ckpt(cfg, model_ckpt_path) + # patch checkpoint if needed (e.g. pretrained weights from mmseg) + if cfg.get("load_from", None) and not model_ckpt_path and not cfg.get("resume", False): + cfg.load_from = self.patch_chkpt(cfg.load_from) + + @staticmethod + def patch_chkpt(ckpt_path: str, new_path: Optional[str] = None) -> str: + """Modify state dict for pretrained weights to match model state dict.""" + ckpt = CheckpointLoader.load_checkpoint(ckpt_path, map_location="cpu") + local_torch_hub_folder = torch.hub.get_dir() + if "state_dict" in ckpt: + ckpt = ckpt["state_dict"] + new_ckpt = OrderedDict() + modified = False + # patch pre-trained checkpoint for model + for name in ckpt: + # we should add backbone prefix to backbone parameters names to load it for our models + if not name.startswith("backbone") and "head" not in name: + new_name = "backbone." + name + modified = True + else: + new_name = name + new_ckpt[new_name] = ckpt[name] + if modified: + if not new_path: + new_path = os.path.join(local_torch_hub_folder, "converted.pth") + new_path = append_dist_rank_suffix(new_path) + torch.save(new_ckpt, new_path) + return new_path + return ckpt_path + + @staticmethod + def configure_input_size( + cfg, input_size=Optional[Tuple[int, int]], model_ckpt_path: Optional[str] = None, training=True + ): + """Change input size if necessary.""" + if input_size is None: # InputSizePreset.DEFAULT + return + + # Segmentation models have different input size in train and val data pipeline + base_input_size = { + "train": 512, + "val": 544, + "test": 544, + "unlabeled": 512, + } + manager = InputSizeManager(cfg, base_input_size) + + if input_size == (0, 0): # InputSizePreset.AUTO + if training: + input_size = BaseConfigurer.adapt_input_size_to_dataset(cfg, manager, use_annotations=False) + else: + input_size = manager.get_trained_input_size(model_ckpt_path) + if input_size is None: + return + + manager.set_input_size(input_size) + logger.info("Input size is changed to {}".format(input_size)) + + +class IncrSegmentationConfigurer(IncrConfigurerMixin, SegmentationConfigurer): + """Patch config to support incremental learning for semantic segmentation.""" + + def is_incremental(self) -> bool: + """Return whether current model classes is increased from original model classes.""" + # TODO: Revisit this part when removing bg label -> it should be 1 because of 'background' label + if len(set(self.org_model_classes) & set(self.model_classes)) == 1 or set(self.org_model_classes) == set( + self.model_classes + ): + is_cls_incr = False + else: + is_cls_incr = True + return is_cls_incr + + +class SemiSLSegmentationConfigurer(SemiSLConfigurerMixin, SegmentationConfigurer): + """Patch config to support semi supervised learning for semantic segmentation.""" + + def configure_task(self, cfg: ConfigDict, **kwargs: Any) -> None: + """Adjust settings for task adaptation.""" + super().configure_task(cfg, **kwargs) + + # Remove task adapt hook (set default torch random sampler) + remove_custom_hook(cfg, "TaskAdaptHook") diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/datasets/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/__init__.py new file mode 100644 index 00000000000..d8b49e9e89a --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/__init__.py @@ -0,0 +1,36 @@ +"""OTX Algorithms - Segmentation Dataset.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .dataset import OTXSegDataset, get_annotation_mmseg_format +from .pipelines import ( + LoadAnnotationFromOTXDataset, + LoadImageFromOTXDataset, + LoadResizeDataFromOTXDataset, + MaskCompose, + ProbCompose, + TwoCropTransform, +) + +__all__ = [ + "LoadAnnotationFromOTXDataset", + "LoadImageFromOTXDataset", + "LoadResizeDataFromOTXDataset", + "MaskCompose", + "ProbCompose", + "TwoCropTransform", + "get_annotation_mmseg_format", + "OTXSegDataset", +] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/datasets/dataset.py b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/dataset.py new file mode 100644 index 00000000000..7bb8625a932 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/dataset.py @@ -0,0 +1,281 @@ +"""Base MMDataset for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from abc import ABCMeta +from typing import Any, Dict, List, Optional, Sequence + +import numpy as np +from mmseg.datasets.builder import DATASETS +from mmseg.datasets.custom import CustomDataset +from mmseg.datasets.pipelines import Compose + +from otx.algorithms.common.utils.data import get_old_new_img_indices +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import LabelEntity +from otx.api.utils.segmentation_utils import mask_from_dataset_item + + +# pylint: disable=invalid-name, too-many-locals, too-many-instance-attributes, super-init-not-called +def get_annotation_mmseg_format( + dataset_item: DatasetItemEntity, + labels: List[LabelEntity], + use_otx_adapter: bool = True, +) -> dict: + """Function to convert a OTX annotation to mmsegmentation format. + + This is used both in the OTXDataset class defined in this file + as in the custom pipeline element 'LoadAnnotationFromOTXDataset' + + :param dataset_item: DatasetItem for which to get annotations + :param labels: List of labels in the project + :return dict: annotation information dict in mmseg format + """ + gt_seg_map = mask_from_dataset_item(dataset_item, labels, use_otx_adapter) + + gt_seg_map = gt_seg_map.squeeze(2).astype(np.uint8) + ann_info = dict(gt_semantic_seg=gt_seg_map) + + return ann_info + + +@DATASETS.register_module() +class _OTXSegDataset(CustomDataset, metaclass=ABCMeta): + """Wrapper that allows using a OTX dataset to train mmsegmentation models. + + This wrapper is not based on the filesystem, + but instead loads the items here directly from the OTX Dataset object. + + The wrapper overwrites some methods of the CustomDataset class: prepare_train_img, prepare_test_img and prepipeline + Naming of certain attributes might seem a bit peculiar but this is due to the conventions set in CustomDataset. For + instance, CustomDatasets expects the dataset items to be stored in the attribute data_infos, which is why it is + named like that and not dataset_items. + + """ + + class _DataInfoProxy: + """This class is intended to be a wrapper to use it in CustomDataset-derived class as `self.data_infos`. + + Instead of using list `data_infos` as in CustomDataset, our implementation of dataset OTXDataset + uses this proxy class with overriden __len__ and __getitem__; this proxy class + forwards data access operations to otx_dataset and converts the dataset items to the view + convenient for mmsegmentation. + """ + + def __init__( + self, + otx_dataset, + labels=None, + **kwargs, # pylint: disable=unused-argument + ): + self.otx_dataset = otx_dataset + self.labels = labels + self.label_idx = {label.id: i for i, label in enumerate(labels)} + + def __len__(self): + return len(self.otx_dataset) + + def __getitem__(self, index): + """Prepare a dict 'data_info' that is expected by the mmseg pipeline to handle images and annotations. + + :return data_info: dictionary that contains the image and image metadata, as well as the labels of + the objects in the image + """ + dataset = self.otx_dataset + item = dataset[index] + ignored_labels = np.array([self.label_idx[lbs.id] + 1 for lbs in item.ignored_labels]) + + data_info = dict( + dataset_item=item, + width=item.width, + height=item.height, + index=index, + ann_info=dict(labels=self.labels), + ignored_labels=ignored_labels, + ) + + return data_info + + def __init__( + self, + otx_dataset: DatasetEntity, + pipeline: Sequence[dict], + classes: Optional[List[str]] = None, + test_mode: bool = False, + use_otx_adapter: bool = True, + ): + self.otx_dataset = otx_dataset + self.test_mode = test_mode + + self.ignore_index = 255 + self.reduce_zero_label = False + self.label_map = None + self.use_otx_adapter = use_otx_adapter + + dataset_labels = self.otx_dataset.get_labels(include_empty=False) + self.project_labels = self.filter_labels(dataset_labels, classes) + self.CLASSES, self.PALETTE = self.get_classes_and_palette(classes, None) + + # Instead of using list data_infos as in CustomDataset, this implementation of dataset + # uses a proxy class with overriden __len__ and __getitem__; this proxy class + # forwards data access operations to otx_dataset. + # Note that list `data_infos` cannot be used here, since OTX dataset class does not have interface to + # get only annotation of a data item, so we would load the whole data item (including image) + # even if we need only checking aspect ratio of the image; due to it + # this implementation of dataset does not uses such tricks as skipping images with wrong aspect ratios or + # small image size, since otherwise reading the whole dataset during initialization will be required. + self.data_infos = _OTXSegDataset._DataInfoProxy(self.otx_dataset, self.project_labels) + + self.pipeline = Compose(pipeline) + + @staticmethod + def filter_labels(all_labels: List[LabelEntity], label_names: List[str]) -> List[LabelEntity]: + """Filter and collect actual label entities.""" + filtered_labels = [] + for label_name in label_names: + matches = [label for label in all_labels if label.name == label_name] + if len(matches) == 0: + continue + + assert len(matches) == 1 + + filtered_labels.append(matches[0]) + + return filtered_labels + + def __len__(self): + """Total number of samples of data.""" + + return len(self.data_infos) + + def pre_pipeline(self, results: Dict[str, Any]): + """Prepare results dict for pipeline.""" + + results["seg_fields"] = [] + + def prepare_train_img(self, idx: int) -> dict: + """Get training data and annotations after pipeline. + + Args: + idx (int): Index of data. + + Returns: + dict: Training data and annotation after pipeline with new keys introduced by pipeline. + """ + + item = self.data_infos[idx] + + self.pre_pipeline(item) + out = self.pipeline(item) + + return out + + def prepare_test_img(self, idx: int) -> dict: + """Get testing data after pipeline. + + Args: + idx (int): Index of data. + + Returns: + dict: Testing data after pipeline with new keys introduced by pipeline. + """ + + item = self.data_infos[idx] + + self.pre_pipeline(item) + out = self.pipeline(item) + + return out + + def get_ann_info(self, idx: int): + """This method is used for evaluation of predictions. + + The CustomDataset class implements a method + CustomDataset.evaluate, which uses the class method get_ann_info to retrieve annotations. + + :param idx: index of the dataset item for which to get the annotations + :return ann_info: dict that contains the coordinates of the bboxes and their corresponding labels + """ + + dataset_item = self.otx_dataset[idx] + ann_info = get_annotation_mmseg_format(dataset_item, self.project_labels, self.use_otx_adapter) + + return ann_info + + def get_gt_seg_maps(self, efficient_test: bool = False): + """Get ground truth segmentation maps for evaluation.""" + + gt_seg_maps = [] + for item_id in range(len(self)): + ann_info = self.get_ann_info(item_id) + gt_seg_maps.append(ann_info["gt_semantic_seg"]) + if efficient_test: + pass + + return gt_seg_maps + + +@DATASETS.register_module() +class OTXSegDataset(_OTXSegDataset, metaclass=ABCMeta): + """Wrapper dataset that allows using a OTX dataset to train models.""" + + def __init__(self, **kwargs): + pipeline = [] + test_mode = kwargs.get("test_mode", False) + use_otx_adapter = True + if "dataset" in kwargs: + dataset = kwargs["dataset"] + otx_dataset = dataset.otx_dataset + pipeline = dataset.pipeline + classes = dataset.labels + new_classes = dataset.new_classes + else: + otx_dataset = kwargs["otx_dataset"] + pipeline = kwargs["pipeline"] + classes = kwargs["labels"] + new_classes = kwargs.get("new_classes", []) + + if test_mode is False: + self.img_indices = get_old_new_img_indices(classes, new_classes, otx_dataset) + + for pipe in pipeline: + if pipe["type"] == "LoadImageFromOTXDataset" and "use_otx_adapter" in pipe: + use_otx_adapter = pipe["use_otx_adapter"] + break + + if classes: + classes = [c.name for c in classes] + classes = ["background"] + classes + else: + classes = [] + super().__init__( + otx_dataset=otx_dataset, + pipeline=pipeline, + classes=classes, + use_otx_adapter=use_otx_adapter, + ) + + self.CLASSES = [label.name for label in self.project_labels] + if "background" not in self.CLASSES: + self.CLASSES = ["background"] + self.CLASSES + + if self.label_map is None: + self.label_map = {} + for i, c in enumerate(self.CLASSES): + if c not in classes: + self.label_map[i] = -1 + else: + self.label_map[i] = classes.index(c) diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py new file mode 100644 index 00000000000..0c8df57a6fe --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py @@ -0,0 +1,28 @@ +"""OTX Algorithms - Segmentation pipelines.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .compose import MaskCompose, ProbCompose +from .loads import LoadAnnotationFromOTXDataset, LoadImageFromOTXDataset, LoadResizeDataFromOTXDataset +from .transforms import TwoCropTransform + +__all__ = [ + "MaskCompose", + "ProbCompose", + "LoadImageFromOTXDataset", + "LoadAnnotationFromOTXDataset", + "LoadResizeDataFromOTXDataset", + "TwoCropTransform", +] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/compose.py b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/compose.py new file mode 100644 index 00000000000..99ecfc4bf36 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/compose.py @@ -0,0 +1,143 @@ +"""Collection of compose pipelines for segmentation task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from collections.abc import Sequence +from copy import deepcopy + +import numpy as np +from mmcv.utils import build_from_cfg +from mmseg.datasets.builder import PIPELINES +from scipy.ndimage import gaussian_filter + + +# pylint: disable=consider-using-f-string +@PIPELINES.register_module() +class ProbCompose: + """Compose pipelines in a list and enable or disable them with the probability.""" + + def __init__(self, transforms, probs): + assert isinstance(transforms, Sequence) + assert isinstance(probs, Sequence) + assert len(transforms) == len(probs) + assert all(p >= 0.0 for p in probs) + + sum_probs = float(sum(probs)) + assert sum_probs > 0.0 + norm_probs = [float(p) / sum_probs for p in probs] + self.limits = np.cumsum([0.0] + norm_probs) + + self.transforms = [] + for transform in transforms: + if isinstance(transform, dict): + transform = build_from_cfg(transform, PIPELINES) + self.transforms.append(transform) + elif callable(transform): + self.transforms.append(transform) + else: + raise TypeError(f"transform must be callable or a dict, but got {type(transform)}") + + def __call__(self, data): + """Callback function of ProbCompose.""" + rand_value = np.random.rand() + transform_id = np.max(np.where(rand_value > self.limits)[0]) + + transform = self.transforms[transform_id] + data = transform(data) + + return data + + def __repr__(self): + """Repr.""" + format_string = self.__class__.__name__ + "(" + for t in self.transforms: + format_string += "\n" + format_string += f" {t}" + format_string += "\n)" + + return format_string + + +@PIPELINES.register_module() +class MaskCompose: + """Compose mask-related pipelines in a list and enable or disable them with the probability.""" + + def __init__(self, transforms, prob, lambda_limits=(4, 16), keep_original=False): + self.keep_original = keep_original + self.prob = prob + assert 0.0 <= self.prob <= 1.0 + + assert isinstance(lambda_limits, Sequence) + assert len(lambda_limits) == 2 + assert 0.0 < lambda_limits[0] < lambda_limits[1] + self.lambda_limits = lambda_limits + + assert isinstance(transforms, Sequence) + self.transforms = [] + for transform in transforms: + if isinstance(transform, dict): + transform = build_from_cfg(transform, PIPELINES) + self.transforms.append(transform) + elif callable(transform): + self.transforms.append(transform) + else: + raise TypeError(f"transform must be callable or a dict, but got {type(transform)}") + + @staticmethod + def _apply_transforms(data, transforms): + for t in transforms: + data = t(data) + if data is None: + return None + + return data + + @staticmethod + def _generate_mask(shape, lambda_limits): + noise = np.random.randn(*shape) + + sigma = np.exp(np.log10(np.random.uniform(lambda_limits[0], lambda_limits[1]))) + soft_mask = gaussian_filter(noise, sigma=sigma) + + threshold = np.median(soft_mask) + hard_mask = soft_mask > threshold + + return hard_mask + + @staticmethod + def _mix_img(main_img, aux_img, mask): + return np.where(np.expand_dims(mask, axis=2), main_img, aux_img) + + def __call__(self, data): + """Callback function of MaskCompose.""" + main_data = self._apply_transforms(deepcopy(data), self.transforms) + assert main_data is not None + if not self.keep_original and np.random.rand() > self.prob: + return main_data + + aux_data = self._apply_transforms(deepcopy(data), self.transforms) + assert aux_data is not None + + assert main_data["img"].shape == aux_data["img"].shape + + mask = self._generate_mask(main_data["img"].shape[:2], self.lambda_limits) + mixed_img = self._mix_img(main_data["img"], aux_data["img"], mask) + + if self.keep_original: + main_data["aux_img"] = mixed_img + else: + main_data["img"] = mixed_img + + return main_data + + def __repr__(self): + """Repr.""" + format_string = self.__class__.__name__ + "(" + for t in self.transforms: + format_string += "\n" + format_string += f" {t}" + format_string += "\n)" + + return format_string diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/loads.py b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/loads.py new file mode 100644 index 00000000000..bb00bbae05f --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/loads.py @@ -0,0 +1,98 @@ +"""Collection of load pipelines for segmentation task.""" +# Copyright (C) 2021-23 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any, Dict, Optional + +import numpy as np +from mmseg.datasets.builder import PIPELINES, build_from_cfg + +import otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset as load_image_base +from otx.algorithms.segmentation.adapters.mmseg.datasets.dataset import ( + get_annotation_mmseg_format, +) + + +# pylint: disable=too-many-instance-attributes, too-many-arguments +@PIPELINES.register_module() +class LoadImageFromOTXDataset(load_image_base.LoadImageFromOTXDataset): + """Pipeline element that loads an image from a OTX Dataset on the fly.""" + + def __init__(self, use_otx_adapter: bool = True, **kwargs): + self.use_otx_adapter = use_otx_adapter + super().__init__(**kwargs) + + +@PIPELINES.register_module() +class LoadResizeDataFromOTXDataset(load_image_base.LoadResizeDataFromOTXDataset): + """Load and resize image & annotation with cache support.""" + + def __init__(self, use_otx_adapter: bool = True, **kwargs): + self.use_otx_adapter = use_otx_adapter + super().__init__(**kwargs) + + def _create_load_ann_op(self, cfg: Optional[Dict]) -> Optional[Any]: + """Creates resize operation.""" + if cfg is None: + return None + return build_from_cfg(cfg, PIPELINES) + + def _create_resize_op(self, cfg: Optional[Dict]) -> Optional[Any]: + """Creates resize operation.""" + if cfg is None: + return None + return build_from_cfg(cfg, PIPELINES) + + def _load_cache(self, results: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Try to load pre-computed results from cache.""" + results = super()._load_cache(results) + if results is None: + return None + # Split image & mask from cached 4D map + img = results["img"] + if img.shape[-1] == 4: + results["img"] = img[:, :, :-1] + results["gt_semantic_seg"] = img[:, :, -1] + return results + + def _save_cache(self, results: Dict[str, Any]): + """Try to save pre-computed results to cache.""" + if not self._enable_outer_memcache: + return + key = self._get_unique_key(results) + meta = results.copy() + img = meta.pop("img") + mask = meta.pop("gt_semantic_seg", None) + if mask is not None: + # Concat mask to image if size matches + if mask.dtype == img.dtype and mask.shape[:2] == img.shape[:2]: + img = np.concatenate((img, mask[:, :, np.newaxis]), axis=-1) + + mem_cache_handler = self._get_memcache_handler() + mem_cache_handler.put(key, img, meta) + + +@PIPELINES.register_module() +class LoadAnnotationFromOTXDataset: + """Pipeline element that loads an annotation from a OTX Dataset on the fly. + + Expected entries in the 'results' dict that should be passed to this pipeline element are: + results['dataset_item']: dataset_item from which to load the annotation + results['ann_info']['label_list']: list of all labels in the project + + """ + + def __init__(self, use_otx_adapter=True): + self.use_otx_adapter = use_otx_adapter + + def __call__(self, results: Dict[str, Any]): + """Callback function of LoadAnnotationFromOTXDataset.""" + dataset_item = results.pop("dataset_item") # Prevent unnessary deepcopy + labels = results["ann_info"]["labels"] + + ann_info = get_annotation_mmseg_format(dataset_item, labels, self.use_otx_adapter) + + results["gt_semantic_seg"] = ann_info["gt_semantic_seg"] + results["seg_fields"].append("gt_semantic_seg") + + return results diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/transforms.py b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/transforms.py new file mode 100644 index 00000000000..5c1c6fcc999 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/transforms.py @@ -0,0 +1,416 @@ +"""Collection of transfrom pipelines for segmentation task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Any, Dict, List + +import mmcv +import numpy as np +from mmcv.parallel import DataContainer as DC +from mmcv.utils import build_from_cfg +from mmseg.datasets.builder import PIPELINES +from mmseg.datasets.pipelines import Compose +from mmseg.datasets.pipelines.formatting import to_tensor +from PIL import Image +from torchvision import transforms as T +from torchvision.transforms import functional as F + + +@PIPELINES.register_module(force=True) +class Normalize: + """Normalize the image. + + Added key is "img_norm_cfg". + + Args: + mean (sequence): Mean values of 3 channels. + std (sequence): Std values of 3 channels. + to_rgb (bool): Whether to convert the image from BGR to RGB, + default is true. + """ + + def __init__(self, mean, std, to_rgb=True): + self.mean = np.array(mean, dtype=np.float32) + self.std = np.array(std, dtype=np.float32) + self.to_rgb = to_rgb + + def __call__(self, results): + """Call function to normalize images. + + Args: + results (dict): Result dict from loading pipeline. + + Returns: + dict: Normalized results, 'img_norm_cfg' key is added into + result dict. + """ + + for target in ["img", "ul_w_img", "aux_img"]: + if target in results: + results[target] = mmcv.imnormalize(results[target], self.mean, self.std, self.to_rgb) + results["img_norm_cfg"] = dict(mean=self.mean, std=self.std, to_rgb=self.to_rgb) + + return results + + def __repr__(self): + """Repr.""" + repr_str = self.__class__.__name__ + repr_str += f"(mean={self.mean}, std={self.std}, to_rgb=" f"{self.to_rgb})" + return repr_str + + +@PIPELINES.register_module(force=True) +class DefaultFormatBundle: + """Default formatting bundle. + + It simplifies the pipeline of formatting common fields, including "img" + and "gt_semantic_seg". These fields are formatted as follows. + + - img: (1)transpose, (2)to tensor, (3)to DataContainer (stack=True) + - gt_semantic_seg: (1)unsqueeze dim-0 (2)to tensor, + (3)to DataContainer (stack=True) + """ + + def __call__(self, results): + """Call function to transform and format common fields in results. + + Args: + results (dict): Result dict contains the data to convert. + + Returns: + dict: The result dict contains the data that is formatted with + default bundle. + """ + for target in ["img", "ul_w_img", "aux_img"]: + if target not in results: + continue + + img = results[target] + if len(img.shape) < 3: + img = np.expand_dims(img, -1) + + if len(img.shape) == 3: + img = np.ascontiguousarray(img.transpose(2, 0, 1)).astype(np.float32) + elif len(img.shape) == 4: + # for selfsl or supcon + img = np.ascontiguousarray(img.transpose(0, 3, 1, 2)).astype(np.float32) + else: + raise ValueError(f"img.shape={img.shape} is not supported.") + + results[target] = DC(to_tensor(img), stack=True) + + for trg_name in ["gt_semantic_seg", "gt_class_borders", "pixel_weights"]: + if trg_name not in results: + continue + + out_type = np.float32 if trg_name == "pixel_weights" else np.int64 + results[trg_name] = DC(to_tensor(results[trg_name][None, ...].astype(out_type)), stack=True) + + return results + + def __repr__(self): + """Repr.""" + return self.__class__.__name__ + + +@PIPELINES.register_module() +class BranchImage: + """Branch images by copying with name of key. + + Args: + key_map (dict): keys to name each image. + """ + + def __init__(self, key_map): + self.key_map = key_map + + def __call__(self, results): + """Call function to branch images in img_fields in results. + + Args: + results (dict): Result dict contains the image data to branch. + + Returns: + dict: The result dict contains the original image data and copied image data. + """ + for key1, key2 in self.key_map.items(): + if key1 in results: + results[key2] = results[key1] + if key1 in results["img_fields"]: + results["img_fields"].append(key2) + return results + + def __repr__(self): + """Repr.""" + + repr_str = self.__class__.__name__ + return repr_str + + +@PIPELINES.register_module() +class TwoCropTransform: + """TwoCropTransform to combine two pipelines. + + Through `TwoCropTransformHook`, how frequently both pipelines (view0 + view1) is applied can be set. + + Args: + view0 (list): Pipeline for online network. + view1 (list): Pipeline for target network. + """ + + def __init__(self, view0: List, view1: List): + self.view0 = Compose([build_from_cfg(p, PIPELINES) for p in view0]) + self.view1 = Compose([build_from_cfg(p, PIPELINES) for p in view1]) + self.is_both = True + + def __call__(self, results: Dict[str, Any]): + """Callback function of TwoCropTransform. + + Args: + results (dict): Inputs to be transformed. + + Returns: + dict: Dictionary that includes stuffs for training. + They have different shape or attribute depending on `self.is_both`. + """ + if self.is_both: + results1 = self.view0(deepcopy(results)) + results2 = self.view1(deepcopy(results)) + + results = deepcopy(results1) + results["img"] = np.stack((results1["img"], results2["img"]), axis=0) + results["gt_semantic_seg"] = np.stack((results1["gt_semantic_seg"], results2["gt_semantic_seg"]), axis=0) + results["flip"] = [results1["flip"], results2["flip"]] + + else: + results = self.view0(results) + + results["is_both"] = self.is_both + + return results + + +@PIPELINES.register_module +class RandomResizedCrop(T.RandomResizedCrop): + """Wrapper for RandomResizedCrop in torchvision.transforms. + + Since this transformation is applied to PIL Image, + `NDArrayToPILImage` must be applied first before this is applied. + """ + + def __call__(self, results: Dict[str, Any]): + """Callback function of RandomResizedCrop. + + Args: + results (dict): Inputs to be transformed. + + Returns: + dict: Dictionary that includes transformed img and related information. + """ + img = results["img"] + i, j, height, width = self.get_params(img, self.scale, self.ratio) + img = F.resized_crop(img, i, j, height, width, self.size, self.interpolation) + results["img"] = img + results["img_shape"] = img.size + for key in results.get("seg_fields", []): + results[key] = np.array( + F.resized_crop( + Image.fromarray(results[key]), + i, + j, + height, + width, + self.size, + self.interpolation, + ) + ) + + # change order because of difference between numpy and PIL + w_scale = results["img_shape"][0] / results["ori_shape"][1] + h_scale = results["img_shape"][1] / results["ori_shape"][0] + results["scale_factor"] = np.array([w_scale, h_scale, w_scale, h_scale], dtype=np.float32) + + return results + + +@PIPELINES.register_module +class RandomColorJitter(T.ColorJitter): + """Wrapper for ColorJitter in torchvision.transforms. + + Since this transformation is applied to PIL Image, + `NDArrayToPILImage` must be applied first before this is applied. + + Args: + p (float): Probability for transformation. Defaults to 0.8. + """ + + def __init__(self, p: float = 0.8, **kwargs): + super().__init__(**kwargs) + assert 0 <= p <= 1 + self.p = p + + def __call__(self, results: Dict[str, Any]): + """Callback function of ColorJitter. + + Args: + results (dict): Inputs to be transformed. + + Returns: + dict: Dictionary that includes transformed img. + """ + if np.random.random() < self.p: + results["img"] = self.forward(results["img"]) + return results + + +@PIPELINES.register_module +class RandomGrayscale(T.RandomGrayscale): + """Wrapper for RandomGrayscale in torchvision.transforms. + + Since this transformation is applied to PIL Image, + `NDArrayToPILImage` must be applied first before this is applied. + """ + + def __call__(self, results: Dict[str, Any]): + """Callback function of RandomGrayscale. + + Args: + results (dict): Inputs to be transformed. + + Returns: + dict: Dictionary that includes transformed img. + """ + results["img"] = self.forward(results["img"]) + return results + + +@PIPELINES.register_module +class RandomGaussianBlur(T.GaussianBlur): + """Random Gaussian Blur augmentation inherited from torchvision.transforms.GaussianBlur. + + Since this transformation is applied to PIL Image, + `NDArrayToPILImage` must be applied first before this is applied. + + Args: + p (float): Probability for transformation. Defaults to 0.1. + """ + + def __init__(self, p: float = 0.1, **kwargs): + super().__init__(**kwargs) + assert 0 <= p <= 1 + self.p = p + + def __call__(self, results: Dict[str, Any]): + """Callback function of GaussianBlur. + + Args: + results (dict): Inputs to be transformed. + + Returns: + dict: Dictionary that includes transformed img. + """ + if np.random.random() < self.p: + results["img"] = self.forward(results["img"]) + return results + + +@PIPELINES.register_module +class RandomSolarization: + """Random Solarization augmentation. + + Args: + threshold (int): Threshold for solarization. Defaults to 128. + p (float): Probability for transformation. Defaults to 0.2. + """ + + def __init__(self, threshold: int = 128, p: float = 0.2): + assert 0 <= p <= 1 + self.threshold = threshold + self.p = p + + def __call__(self, results: Dict[str, Any]): + """Callback function of Solarization. + + Args: + results (dict): Inputs to be transformed. + + Returns: + dict: Dictionary that includes transformed img. + """ + if np.random.random() < self.p: + img = results["img"] + img = np.where(img < self.threshold, img, 255 - img) + results["img"] = img + return results + + def __repr__(self): + """Set repr of Solarization.""" + repr_str = self.__class__.__name__ + return repr_str + + +@PIPELINES.register_module() +class NDArrayToPILImage: + """Convert image from numpy to PIL. + + Args: + keys (list): Keys to be transformed. + """ + + def __init__(self, keys: List[str]): + self.keys = keys + + def __call__(self, results: Dict[str, Any]): + """Callback function of NDArrayToPILImage. + + Args: + results (dict): Inputs to be transformed. + + Returns: + dict: Dictionary that includes transformed img. + """ + for key in self.keys: + img = results[key] + img = Image.fromarray(img) + results[key] = img + return results + + def __repr__(self): + """Set repr of NDArrayToPILImage.""" + repr_str = self.__class__.__name__ + return repr_str + + +@PIPELINES.register_module() +class PILImageToNDArray: + """Convert image from PIL to numpy. + + Args: + keys (list): Keys to be transformed. + """ + + def __init__(self, keys: List[str]): + self.keys = keys + + def __call__(self, results: Dict[str, Any]): + """Callback function of PILImageToNDArray. + + Args: + results (dict): Inputs to be transformed. + + Returns: + dict: Dictionary that includes transformed img. + """ + for key in self.keys: + img = results[key] + img = np.asarray(img) + results[key] = img + return results + + def __repr__(self): + """Set repr of PILImageToNDArray.""" + repr_str = self.__class__.__name__ + return repr_str diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/__init__.py new file mode 100644 index 00000000000..cee5565fe08 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/__init__.py @@ -0,0 +1,48 @@ +"""Adapters for OTX Common Algorithm. - mmseg.model.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .backbones import MSCAN, LiteHRNet, MMOVBackbone +from .heads import DetConHead, LightHamHead, MMOVDecodeHead +from .losses import CrossEntropyLossWithIgnore, DetConLoss +from .necks import SelfSLMLP +from .schedulers import ( + ConstantScalarScheduler, + PolyScalarScheduler, + StepScalarScheduler, +) +from .segmentors import ( + DetConB, + MeanTeacherSegmentor, + SupConDetConB, +) + +__all__ = [ + "LiteHRNet", + "MSCAN", + "MMOVBackbone", + "MMOVDecodeHead", + "DetConLoss", + "SelfSLMLP", + "ConstantScalarScheduler", + "PolyScalarScheduler", + "StepScalarScheduler", + "DetConB", + "CrossEntropyLossWithIgnore", + "SupConDetConB", + "MeanTeacherSegmentor", + "DetConHead", + "LightHamHead", +] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py new file mode 100644 index 00000000000..0121adddc3c --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py @@ -0,0 +1,22 @@ +"""Backbones for semantic segmentation.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +from .litehrnet import LiteHRNet +from .mmov_backbone import MMOVBackbone +from .mscan import MSCAN + +__all__ = ["LiteHRNet", "MMOVBackbone", "MSCAN"] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py new file mode 100644 index 00000000000..7faeaafc518 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py @@ -0,0 +1,1506 @@ +"""HRNet network modules for base backbone. + +Modified from: +- https://github.com/HRNet/Lite-HRNet +""" + +# Copyright (c) 2018-2020 Open-MMLab. +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (c) 2021 DeLightCMU +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import mmcv +import torch +import torch.nn.functional as F +import torch.utils.checkpoint as cp +from mmcv.cnn import ( + ConvModule, + build_conv_layer, + build_norm_layer, + constant_init, + normal_init, +) +from mmcv.runner import BaseModule, load_checkpoint +from mmcv.utils.parrots_wrapper import _BatchNorm +from mmseg.models.backbones.resnet import BasicBlock, Bottleneck +from mmseg.models.builder import BACKBONES +from torch import nn + +from otx.algorithms.segmentation.adapters.mmseg.models.utils import ( + AsymmetricPositionAttentionModule, + IterativeAggregator, + LocalAttentionModule, + channel_shuffle, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=invalid-name, too-many-lines, too-many-instance-attributes, too-many-locals, too-many-arguments +# pylint: disable=unused-argument, consider-using-enumerate +class NeighbourSupport(nn.Module): + """Neighbour support module.""" + + def __init__( + self, + channels, + kernel_size=3, + key_ratio=8, + value_ratio=8, + conv_cfg=None, + norm_cfg=None, + ): + super().__init__() + + self.in_channels = channels + self.key_channels = int(channels / key_ratio) + self.value_channels = int(channels / value_ratio) + self.kernel_size = kernel_size + + self.key = nn.Sequential( + ConvModule( + in_channels=self.in_channels, + out_channels=self.key_channels, + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ), + ConvModule( + self.key_channels, + self.key_channels, + kernel_size=self.kernel_size, + stride=1, + padding=(self.kernel_size - 1) // 2, + groups=self.key_channels, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ), + ConvModule( + in_channels=self.key_channels, + out_channels=self.kernel_size * self.kernel_size, + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ), + ) + self.value = nn.Sequential( + ConvModule( + in_channels=self.in_channels, + out_channels=self.value_channels, + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ), + nn.Unfold(kernel_size=self.kernel_size, stride=1, padding=1), + ) + self.out_conv = ConvModule( + in_channels=self.value_channels, + out_channels=self.in_channels, + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ) + + def forward(self, x): + """Forward.""" + h, w = [int(_) for _ in x.size()[-2:]] + + key = self.key(x).view(-1, 1, self.kernel_size**2, h, w) + weights = torch.softmax(key, dim=2) + + value = self.value(x).view(-1, self.value_channels, self.kernel_size**2, h, w) + y = torch.sum(weights * value, dim=2) + y = self.out_conv(y) + + out = x + y + + return out + + +class CrossResolutionWeighting(nn.Module): + """Cross resolution weighting.""" + + def __init__( + self, + channels, + ratio=16, + conv_cfg=None, + norm_cfg=None, + act_cfg=(dict(type="ReLU"), dict(type="Sigmoid")), + ): + super().__init__() + + if isinstance(act_cfg, dict): + act_cfg = (act_cfg, act_cfg) + assert len(act_cfg) == 2 + assert mmcv.is_tuple_of(act_cfg, dict) + + self.channels = channels + total_channel = sum(channels) + + self.conv1 = ConvModule( + in_channels=total_channel, + out_channels=int(total_channel / ratio), + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=act_cfg[0], + ) + self.conv2 = ConvModule( + in_channels=int(total_channel / ratio), + out_channels=total_channel, + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=act_cfg[1], + ) + + def forward(self, x): + """Forward.""" + min_size = [int(_) for _ in x[-1].size()[-2:]] + + out = [F.adaptive_avg_pool2d(s, min_size) for s in x[:-1]] + [x[-1]] + out = torch.cat(out, dim=1) + out = self.conv1(out) + out = self.conv2(out) + out = torch.split(out, self.channels, dim=1) + out = [s * F.interpolate(a, size=s.size()[-2:], mode="nearest") for s, a in zip(x, out)] + + return out + + +class SpatialWeighting(nn.Module): + """Spatial weighting.""" + + def __init__( + self, + channels, + ratio=16, + conv_cfg=None, + act_cfg=(dict(type="ReLU"), dict(type="Sigmoid")), + **kwargs, + ): + super().__init__() + + if isinstance(act_cfg, dict): + act_cfg = (act_cfg, act_cfg) + assert len(act_cfg) == 2 + assert mmcv.is_tuple_of(act_cfg, dict) + + self.global_avgpool = nn.AdaptiveAvgPool2d(1) + self.conv1 = ConvModule( + in_channels=channels, + out_channels=int(channels / ratio), + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + act_cfg=act_cfg[0], + ) + self.conv2 = ConvModule( + in_channels=int(channels / ratio), + out_channels=channels, + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + act_cfg=act_cfg[1], + ) + + def forward(self, x): + """Forward.""" + out = self.global_avgpool(x) + out = self.conv1(out) + out = self.conv2(out) + + return x * out + + +class SpatialWeightingV2(nn.Module): + """The original repo: https://github.com/DeLightCMU/PSA.""" + + def __init__( + self, + channels, + ratio=16, + conv_cfg=None, + norm_cfg=None, + enable_norm=False, + **kwargs, + ): + super().__init__() + + self.in_channels = channels + self.internal_channels = int(channels / ratio) + + # channel-only branch + self.v_channel = ConvModule( + in_channels=self.in_channels, + out_channels=self.internal_channels, + kernel_size=1, + stride=1, + bias=False, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg if enable_norm else None, + act_cfg=None, + ) + self.q_channel = ConvModule( + in_channels=self.in_channels, + out_channels=1, + kernel_size=1, + stride=1, + bias=False, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg if enable_norm else None, + act_cfg=None, + ) + self.out_channel = ConvModule( + in_channels=self.internal_channels, + out_channels=self.in_channels, + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="Sigmoid"), + ) + + # spatial-only branch + self.v_spatial = ConvModule( + in_channels=self.in_channels, + out_channels=self.internal_channels, + kernel_size=1, + stride=1, + bias=False, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg if enable_norm else None, + act_cfg=None, + ) + self.q_spatial = ConvModule( + in_channels=self.in_channels, + out_channels=self.internal_channels, + kernel_size=1, + stride=1, + bias=False, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg if enable_norm else None, + act_cfg=None, + ) + self.global_avgpool = nn.AdaptiveAvgPool2d(1) + + def _channel_weighting(self, x): + h, w = [int(_) for _ in x.size()[-2:]] + + v = self.v_channel(x).view(-1, self.internal_channels, h * w) + + q = self.q_channel(x).view(-1, h * w, 1) + q = torch.softmax(q, dim=1) + + y = torch.matmul(v, q) + y = y.view(-1, self.internal_channels, 1, 1) + y = self.out_channel(y) + + out = x * y + + return out + + def _spatial_weighting(self, x): + h, w = [int(_) for _ in x.size()[-2:]] + + v = self.v_spatial(x) + v = v.view(-1, self.internal_channels, h * w) + + q = self.q_spatial(x) + q = self.global_avgpool(q) + q = torch.softmax(q, dim=1) + q = q.view(-1, 1, self.internal_channels) + + y = torch.matmul(q, v) + y = y.view(-1, 1, h, w) + y = torch.sigmoid(y) + + out = x * y + + return out + + def forward(self, x): + """Forward.""" + y_channel = self._channel_weighting(x) + y_spatial = self._spatial_weighting(x) + out = y_channel + y_spatial + + return out + + +class ConditionalChannelWeighting(nn.Module): + """Conditional channel weighting module.""" + + def __init__( + self, + in_channels, + stride, + reduce_ratio, + conv_cfg=None, + norm_cfg=None, + with_cp=False, + dropout=None, + weighting_module_version="v1", + neighbour_weighting=False, + dw_ksize=3, + ): + super().__init__() + + if norm_cfg is None: + norm_cfg = dict(type="BN") + + self.with_cp = with_cp + self.stride = stride + assert stride in [1, 2] + + spatial_weighting_module = SpatialWeighting if weighting_module_version == "v1" else SpatialWeightingV2 + branch_channels = [channel // 2 for channel in in_channels] + + self.cross_resolution_weighting = CrossResolutionWeighting( + branch_channels, ratio=reduce_ratio, conv_cfg=conv_cfg, norm_cfg=norm_cfg + ) + self.depthwise_convs = nn.ModuleList( + [ + ConvModule( + channel, + channel, + kernel_size=dw_ksize, + stride=self.stride, + padding=dw_ksize // 2, + groups=channel, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ) + for channel in branch_channels + ] + ) + self.spatial_weighting = nn.ModuleList( + [ + spatial_weighting_module( + channels=channel, + ratio=4, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + enable_norm=True, + ) + for channel in branch_channels + ] + ) + + self.neighbour_weighting = None + if neighbour_weighting: + self.neighbour_weighting = nn.ModuleList( + [ + NeighbourSupport( + channel, + kernel_size=3, + key_ratio=8, + value_ratio=4, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + ) + for channel in branch_channels + ] + ) + + self.dropout = None + if dropout is not None and dropout > 0: + self.dropout = nn.ModuleList([nn.Dropout(p=dropout) for _ in branch_channels]) + + def _inner_forward(self, x): + x = [s.chunk(2, dim=1) for s in x] + x1 = [s[0] for s in x] + x2 = [s[1] for s in x] + + x2 = self.cross_resolution_weighting(x2) + x2 = [dw(s) for s, dw in zip(x2, self.depthwise_convs)] + + if self.neighbour_weighting is not None: + x2 = [nw(s) for s, nw in zip(x2, self.neighbour_weighting)] + + x2 = [sw(s) for s, sw in zip(x2, self.spatial_weighting)] + + if self.dropout is not None: + x2 = [dropout(s) for s, dropout in zip(x2, self.dropout)] + + out = [torch.cat([s1, s2], dim=1) for s1, s2 in zip(x1, x2)] + out = [channel_shuffle(s, 2) for s in out] + + return out + + def forward(self, x): + """Forward.""" + if self.with_cp and x.requires_grad: + out = cp.checkpoint(self._inner_forward, x) + else: + out = self._inner_forward(x) + + return out + + +class Stem(nn.Module): + """Stem.""" + + def __init__( + self, + in_channels, + stem_channels, + out_channels, + expand_ratio, + conv_cfg=None, + norm_cfg=None, + with_cp=False, + strides=(2, 2), + extra_stride=False, + input_norm=False, + ): + super().__init__() + + if norm_cfg is None: + norm_cfg = dict(type="BN") + + assert isinstance(strides, (tuple, list)) + assert len(strides) == 2 + + self.in_channels = in_channels + self.out_channels = out_channels + self.conv_cfg = conv_cfg + self.norm_cfg = norm_cfg + self.with_cp = with_cp + + self.input_norm = None + if input_norm: + self.input_norm = nn.InstanceNorm2d(in_channels) + + self.conv1 = ConvModule( + in_channels=in_channels, + out_channels=stem_channels, + kernel_size=3, + stride=strides[0], + padding=1, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + ) + + self.conv2 = None + if extra_stride: + self.conv2 = ConvModule( + in_channels=stem_channels, + out_channels=stem_channels, + kernel_size=3, + stride=2, + padding=1, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + ) + + mid_channels = int(round(stem_channels * expand_ratio)) + branch_channels = stem_channels // 2 + if stem_channels == self.out_channels: + inc_channels = self.out_channels - branch_channels + else: + inc_channels = self.out_channels - stem_channels + + self.branch1 = nn.Sequential( + ConvModule( + branch_channels, + branch_channels, + kernel_size=3, + stride=strides[1], + padding=1, + groups=branch_channels, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ), + ConvModule( + branch_channels, + inc_channels, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ), + ) + + self.expand_conv = ConvModule( + branch_channels, + mid_channels, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ) + self.depthwise_conv = ConvModule( + mid_channels, + mid_channels, + kernel_size=3, + stride=strides[1], + padding=1, + groups=mid_channels, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ) + self.linear_conv = ConvModule( + mid_channels, + branch_channels if stem_channels == self.out_channels else stem_channels, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ) + + def _inner_forward(self, x): + if self.input_norm is not None: + x = self.input_norm(x) + + x = self.conv1(x) + if self.conv2 is not None: + x = self.conv2(x) + + x1, x2 = x.chunk(2, dim=1) + + x1 = self.branch1(x1) + + x2 = self.expand_conv(x2) + x2 = self.depthwise_conv(x2) + x2 = self.linear_conv(x2) + + out = torch.cat((x1, x2), dim=1) + out = channel_shuffle(out, 2) + + return out + + def forward(self, x): + """Forward.""" + if self.with_cp and x.requires_grad: + out = cp.checkpoint(self._inner_forward, x) + else: + out = self._inner_forward(x) + + return out + + +class StemV2(nn.Module): + """StemV2.""" + + def __init__( + self, + in_channels, + stem_channels, + out_channels, + expand_ratio, + conv_cfg=None, + norm_cfg=None, + with_cp=False, + num_stages=1, + strides=(2, 2), + extra_stride=False, + input_norm=False, + ): + super().__init__() + + if norm_cfg is None: + norm_cfg = dict(type="BN") + + assert num_stages > 0 + assert isinstance(strides, (tuple, list)) + assert len(strides) == 1 + num_stages + + self.in_channels = in_channels + self.out_channels = out_channels + self.conv_cfg = conv_cfg + self.norm_cfg = norm_cfg + self.with_cp = with_cp + self.num_stages = num_stages + + self.input_norm = None + if input_norm: + self.input_norm = nn.InstanceNorm2d(in_channels) + + self.conv1 = ConvModule( + in_channels=in_channels, + out_channels=stem_channels, + kernel_size=3, + stride=strides[0], + padding=1, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + ) + + self.conv2 = None + if extra_stride: + self.conv2 = ConvModule( + in_channels=stem_channels, + out_channels=stem_channels, + kernel_size=3, + stride=2, + padding=1, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + ) + + mid_channels = int(round(stem_channels * expand_ratio)) + internal_branch_channels = stem_channels // 2 + out_branch_channels = self.out_channels // 2 + + self.branch1, self.branch2 = nn.ModuleList(), nn.ModuleList() + for stage in range(1, num_stages + 1): + self.branch1.append( + nn.Sequential( + ConvModule( + internal_branch_channels, + internal_branch_channels, + kernel_size=3, + stride=strides[stage], + padding=1, + groups=internal_branch_channels, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ), + ConvModule( + internal_branch_channels, + out_branch_channels if stage == num_stages else internal_branch_channels, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ), + ) + ) + + self.branch2.append( + nn.Sequential( + ConvModule( + internal_branch_channels, + mid_channels, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ), + ConvModule( + mid_channels, + mid_channels, + kernel_size=3, + stride=strides[stage], + padding=1, + groups=mid_channels, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ), + ConvModule( + mid_channels, + out_branch_channels if stage == num_stages else internal_branch_channels, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ), + ) + ) + + def _inner_forward(self, x): + if self.input_norm is not None: + x = self.input_norm(x) + + y = self.conv1(x) + if self.conv2 is not None: + y = self.conv2(y) + + out_list = [y] + for stage in range(self.num_stages): + y1, y2 = y.chunk(2, dim=1) + + y1 = self.branch1[stage](y1) + y2 = self.branch2[stage](y2) + + y = torch.cat((y1, y2), dim=1) + y = channel_shuffle(y, 2) + out_list.append(y) + + return out_list + + def forward(self, x): + """Forward.""" + if self.with_cp and x.requires_grad: + out = cp.checkpoint(self._inner_forward, x) + else: + out = self._inner_forward(x) + + return out + + +class ShuffleUnit(nn.Module): + """InvertedResidual block for ShuffleNetV2 backbone. + + Args: + in_channels (int): The input channels of the block. + out_channels (int): The output channels of the block. + stride (int): Stride of the 3x3 convolution layer. Default: 1 + conv_cfg (dict): Config dict for convolution layer. + Default: None, which means using conv2d. + norm_cfg (dict): Config dict for normalization layer. + Default: dict(type='BN'). + act_cfg (dict): Config dict for activation layer. + Default: dict(type='ReLU'). + with_cp (bool): Use checkpoint or not. Using checkpoint will save some + memory while slowing down the training speed. Default: False. + """ + + def __init__( + self, + in_channels, + out_channels, + stride=1, + conv_cfg=None, + norm_cfg=None, + act_cfg=None, + with_cp=False, + ): + super().__init__() + + if norm_cfg is None: + norm_cfg = dict(type="BN") + if act_cfg is None: + act_cfg = dict(type="ReLU") + + self.stride = stride + self.with_cp = with_cp + + branch_features = out_channels // 2 + if self.stride == 1: + assert in_channels == branch_features * 2, ( + f"in_channels ({in_channels}) should equal to " + f"branch_features * 2 ({branch_features * 2}) " + "when stride is 1" + ) + + if in_channels != branch_features * 2: + assert self.stride != 1, ( + f"stride ({self.stride}) should not equal 1 when " f"in_channels != branch_features * 2" + ) + + if self.stride > 1: + self.branch1 = nn.Sequential( + ConvModule( + in_channels, + in_channels, + kernel_size=3, + stride=self.stride, + padding=1, + groups=in_channels, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ), + ConvModule( + in_channels, + branch_features, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=act_cfg, + ), + ) + + self.branch2 = nn.Sequential( + ConvModule( + in_channels if (self.stride > 1) else branch_features, + branch_features, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=act_cfg, + ), + ConvModule( + branch_features, + branch_features, + kernel_size=3, + stride=self.stride, + padding=1, + groups=branch_features, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ), + ConvModule( + branch_features, + branch_features, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=act_cfg, + ), + ) + + def _inner_forward(self, x): + if self.stride > 1: + out = torch.cat((self.branch1(x), self.branch2(x)), dim=1) + else: + x1, x2 = x.chunk(2, dim=1) + out = torch.cat((x1, self.branch2(x2)), dim=1) + + out = channel_shuffle(out, 2) + + return out + + def forward(self, x): + """Forward.""" + if self.with_cp and x.requires_grad: + out = cp.checkpoint(self._inner_forward, x) + else: + out = self._inner_forward(x) + + return out + + +class LiteHRModule(nn.Module): + """LiteHR module.""" + + def __init__( + self, + num_branches, + num_blocks, + in_channels, + reduce_ratio, + module_type, + multiscale_output=False, + with_fuse=True, + conv_cfg=None, + norm_cfg=None, + with_cp=False, + dropout=None, + weighting_module_version="v1", + neighbour_weighting=False, + ): + super().__init__() + + if norm_cfg is None: + norm_cfg = dict(type="BN") + self._check_branches(num_branches, in_channels) + + self.in_channels = in_channels + self.num_branches = num_branches + + self.module_type = module_type + self.multiscale_output = multiscale_output + self.with_fuse = with_fuse + self.norm_cfg = norm_cfg + self.conv_cfg = conv_cfg + self.with_cp = with_cp + self.weighting_module_version = weighting_module_version + self.neighbour_weighting = neighbour_weighting + + if self.module_type == "LITE": + self.layers = self._make_weighting_blocks(num_blocks, reduce_ratio, dropout=dropout) + elif self.module_type == "NAIVE": + self.layers = self._make_naive_branches(num_branches, num_blocks) + + if self.with_fuse: + self.fuse_layers = self._make_fuse_layers() + self.relu = nn.ReLU() + + @staticmethod + def _check_branches(num_branches, in_channels): + """Check input to avoid ValueError.""" + + if num_branches != len(in_channels): + error_msg = f"NUM_BRANCHES({num_branches}) != NUM_INCHANNELS({len(in_channels)})" + raise ValueError(error_msg) + + def _make_weighting_blocks(self, num_blocks, reduce_ratio, stride=1, dropout=None): + layers = [] + for _ in range(num_blocks): + layers.append( + ConditionalChannelWeighting( + self.in_channels, + stride=stride, + reduce_ratio=reduce_ratio, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + with_cp=self.with_cp, + dropout=dropout, + weighting_module_version=self.weighting_module_version, + neighbour_weighting=self.neighbour_weighting, + ) + ) + + return nn.Sequential(*layers) + + def _make_one_branch(self, branch_index, num_blocks, stride=1): + """Make one branch.""" + + layers = [ + ShuffleUnit( + self.in_channels[branch_index], + self.in_channels[branch_index], + stride=stride, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + with_cp=self.with_cp, + ) + ] + for _ in range(1, num_blocks): + layers.append( + ShuffleUnit( + self.in_channels[branch_index], + self.in_channels[branch_index], + stride=1, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + with_cp=self.with_cp, + ) + ) + + return nn.Sequential(*layers) + + def _make_naive_branches(self, num_branches, num_blocks): + """Make branches.""" + + branches = [] + for i in range(num_branches): + branches.append(self._make_one_branch(i, num_blocks)) + + return nn.ModuleList(branches) + + def _make_fuse_layers(self): + """Make fuse layer.""" + + if self.num_branches == 1: + return None + + num_branches = self.num_branches + in_channels = self.in_channels + num_out_branches = num_branches if self.multiscale_output else 1 + + fuse_layers = [] + for i in range(num_out_branches): + fuse_layer = [] + for j in range(num_branches): + if j > i: + fuse_layer.append( + nn.Sequential( + build_conv_layer( + self.conv_cfg, + in_channels[j], + in_channels[i], + kernel_size=1, + stride=1, + padding=0, + bias=False, + ), + build_norm_layer(self.norm_cfg, in_channels[i])[1], + ) + ) + elif j == i: + fuse_layer.append(None) + else: + conv_downsamples = [] + for k in range(i - j): + if k == i - j - 1: + conv_downsamples.append( + nn.Sequential( + build_conv_layer( + self.conv_cfg, + in_channels[j], + in_channels[j], + kernel_size=3, + stride=2, + padding=1, + groups=in_channels[j], + bias=False, + ), + build_norm_layer(self.norm_cfg, in_channels[j])[1], + build_conv_layer( + self.conv_cfg, + in_channels[j], + in_channels[i], + kernel_size=1, + stride=1, + padding=0, + bias=False, + ), + build_norm_layer(self.norm_cfg, in_channels[i])[1], + ) + ) + else: + conv_downsamples.append( + nn.Sequential( + build_conv_layer( + self.conv_cfg, + in_channels[j], + in_channels[j], + kernel_size=3, + stride=2, + padding=1, + groups=in_channels[j], + bias=False, + ), + build_norm_layer(self.norm_cfg, in_channels[j])[1], + build_conv_layer( + self.conv_cfg, + in_channels[j], + in_channels[j], + kernel_size=1, + stride=1, + padding=0, + bias=False, + ), + build_norm_layer(self.norm_cfg, in_channels[j])[1], + nn.ReLU(inplace=True), + ) + ) + fuse_layer.append(nn.Sequential(*conv_downsamples)) + fuse_layers.append(nn.ModuleList(fuse_layer)) + + return nn.ModuleList(fuse_layers) + + def forward(self, x): + """Forward function.""" + + if self.num_branches == 1: + return [self.layers[0](x[0])] + + if self.module_type == "LITE": + out = self.layers(x) + elif self.module_type == "NAIVE": + for i in range(self.num_branches): + x[i] = self.layers[i](x[i]) + out = x + + if self.with_fuse: + out_fuse = [] + for i in range(len(self.fuse_layers)): + y = out[0] if i == 0 else self.fuse_layers[i][0](out[0]) + for j in range(self.num_branches): + if i == j: + fuse_y = out[j] + else: + fuse_y = self.fuse_layers[i][j](out[j]) + + if fuse_y.size()[-2:] != y.size()[-2:]: + fuse_y = F.interpolate(fuse_y, size=y.size()[-2:], mode="nearest") + + y += fuse_y + + out_fuse.append(self.relu(y)) + + out = out_fuse + elif not self.multiscale_output: + out = [out[0]] + + return out + + +@BACKBONES.register_module() +class LiteHRNet(BaseModule): + """Lite-HRNet backbone. + + `High-Resolution Representations for Labeling Pixels and Regions + `_ + + Args: + extra (dict): detailed configuration for each stage of HRNet. + in_channels (int): Number of input image channels. Default: 3. + conv_cfg (dict): dictionary to construct and config conv layer. + norm_cfg (dict): dictionary to construct and config norm layer. + norm_eval (bool): Whether to set norm layers to eval mode, namely, + freeze running stats (mean and var). Note: Effect on Batch Norm + and its variants only. Default: False + with_cp (bool): Use checkpoint or not. Using checkpoint will save some + memory while slowing down the training speed. + zero_init_residual (bool): whether to use zero init for last norm layer + in resblocks to let them behave as identity. + """ + + def __init__( + self, + extra, + in_channels=3, + conv_cfg=None, + norm_cfg=None, + norm_eval=False, + with_cp=False, + zero_init_residual=False, + dropout=None, + init_cfg=None, + ): + super().__init__(init_cfg=init_cfg) + + if norm_cfg is None: + norm_cfg = dict(type="BN") + + self.extra = extra + self.conv_cfg = conv_cfg + self.norm_cfg = norm_cfg + self.norm_eval = norm_eval + self.with_cp = with_cp + self.zero_init_residual = zero_init_residual + + self.stem = Stem( + in_channels, + input_norm=self.extra["stem"]["input_norm"], + stem_channels=self.extra["stem"]["stem_channels"], + out_channels=self.extra["stem"]["out_channels"], + expand_ratio=self.extra["stem"]["expand_ratio"], + strides=self.extra["stem"]["strides"], + extra_stride=self.extra["stem"]["extra_stride"], + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + ) + + self.enable_stem_pool = self.extra["stem"].get("out_pool", False) + if self.enable_stem_pool: + self.stem_pool = nn.AvgPool2d(kernel_size=3, stride=2) + + self.num_stages = self.extra["num_stages"] + self.stages_spec = self.extra["stages_spec"] + + num_channels_last = [ + self.stem.out_channels, + ] + for i in range(self.num_stages): + num_channels = self.stages_spec["num_channels"][i] + num_channels = [num_channels[i] for i in range(len(num_channels))] + + setattr( + self, + f"transition{i}", + self._make_transition_layer(num_channels_last, num_channels), + ) + + stage, num_channels_last = self._make_stage( + self.stages_spec, + i, + num_channels, + multiscale_output=True, + dropout=dropout, + ) + setattr(self, f"stage{i}", stage) + + self.out_modules = None + if self.extra.get("out_modules") is not None: + out_modules = [] + in_modules_channels, out_modules_channels = num_channels_last[-1], None + if self.extra["out_modules"]["conv"]["enable"]: + out_modules_channels = self.extra["out_modules"]["conv"]["channels"] + out_modules.append( + ConvModule( + in_channels=in_modules_channels, + out_channels=out_modules_channels, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + ) + ) + in_modules_channels = out_modules_channels + if self.extra["out_modules"]["position_att"]["enable"]: + out_modules.append( + AsymmetricPositionAttentionModule( + in_channels=in_modules_channels, + key_channels=self.extra["out_modules"]["position_att"]["key_channels"], + value_channels=self.extra["out_modules"]["position_att"]["value_channels"], + psp_size=self.extra["out_modules"]["position_att"]["psp_size"], + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + ) + ) + if self.extra["out_modules"]["local_att"]["enable"]: + out_modules.append( + LocalAttentionModule( + num_channels=in_modules_channels, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + ) + ) + + if len(out_modules) > 0: + self.out_modules = nn.Sequential(*out_modules) + num_channels_last.append(in_modules_channels) + + self.add_stem_features = self.extra.get("add_stem_features", False) + if self.add_stem_features: + self.stem_transition = nn.Sequential( + ConvModule( + self.stem.out_channels, + self.stem.out_channels, + kernel_size=3, + stride=1, + padding=1, + groups=self.stem.out_channels, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=None, + ), + ConvModule( + self.stem.out_channels, + num_channels_last[0], + kernel_size=1, + stride=1, + padding=0, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ), + ) + + num_channels_last = [num_channels_last[0]] + num_channels_last + + self.with_aggregator = self.extra.get("out_aggregator") and self.extra["out_aggregator"]["enable"] + if self.with_aggregator: + self.aggregator = IterativeAggregator( + in_channels=num_channels_last, + min_channels=self.extra["out_aggregator"].get("min_channels", None), + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + ) + + def _make_transition_layer(self, num_channels_pre_layer, num_channels_cur_layer): + """Make transition layer.""" + + num_branches_cur = len(num_channels_cur_layer) + num_branches_pre = len(num_channels_pre_layer) + + transition_layers = [] + for i in range(num_branches_cur): + if i < num_branches_pre: + if num_channels_cur_layer[i] != num_channels_pre_layer[i]: + transition_layers.append( + nn.Sequential( + build_conv_layer( + self.conv_cfg, + num_channels_pre_layer[i], + num_channels_pre_layer[i], + kernel_size=3, + stride=1, + padding=1, + groups=num_channels_pre_layer[i], + bias=False, + ), + build_norm_layer(self.norm_cfg, num_channels_pre_layer[i])[1], + build_conv_layer( + self.conv_cfg, + num_channels_pre_layer[i], + num_channels_cur_layer[i], + kernel_size=1, + stride=1, + padding=0, + bias=False, + ), + build_norm_layer(self.norm_cfg, num_channels_cur_layer[i])[1], + nn.ReLU(), + ) + ) + else: + transition_layers.append(None) + else: + conv_downsamples = [] + for j in range(i + 1 - num_branches_pre): + in_channels = num_channels_pre_layer[-1] + out_channels = num_channels_cur_layer[i] if j == i - num_branches_pre else in_channels + conv_downsamples.append( + nn.Sequential( + build_conv_layer( + self.conv_cfg, + in_channels, + in_channels, + kernel_size=3, + stride=2, + padding=1, + groups=in_channels, + bias=False, + ), + build_norm_layer(self.norm_cfg, in_channels)[1], + build_conv_layer( + self.conv_cfg, + in_channels, + out_channels, + kernel_size=1, + stride=1, + padding=0, + bias=False, + ), + build_norm_layer(self.norm_cfg, out_channels)[1], + nn.ReLU(), + ) + ) + transition_layers.append(nn.Sequential(*conv_downsamples)) + + return nn.ModuleList(transition_layers) + + def _make_stage( + self, + stages_spec, + stage_index, + in_channels, + multiscale_output=True, + dropout=None, + ): + num_modules = stages_spec["num_modules"][stage_index] + num_branches = stages_spec["num_branches"][stage_index] + num_blocks = stages_spec["num_blocks"][stage_index] + reduce_ratio = stages_spec["reduce_ratios"][stage_index] + with_fuse = stages_spec["with_fuse"][stage_index] + module_type = stages_spec["module_type"][stage_index] + weighting_module_version = stages_spec.get("weighting_module_version", "v1") + neighbour_weighting = stages_spec.get("neighbour_weighting", False) + + modules = [] + for i in range(num_modules): + # multi_scale_output is only used last module + if not multiscale_output and i == num_modules - 1: + reset_multiscale_output = False + else: + reset_multiscale_output = True + + modules.append( + LiteHRModule( + num_branches, + num_blocks, + in_channels, + reduce_ratio, + module_type, + multiscale_output=reset_multiscale_output, + with_fuse=with_fuse, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + with_cp=self.with_cp, + dropout=dropout, + weighting_module_version=weighting_module_version, + neighbour_weighting=neighbour_weighting, + ) + ) + in_channels = modules[-1].in_channels + + return nn.Sequential(*modules), in_channels + + def init_weights(self, pretrained=None): + """Initialize the weights in backbone. + + Args: + pretrained (str, optional): Path to pre-trained weights. + Defaults to None. + """ + + if isinstance(pretrained, str): + load_checkpoint(self, pretrained, strict=False, logger=logger) + elif pretrained is None: + for m in self.modules(): + if isinstance(m, nn.Conv2d): + normal_init(m, std=0.001) + elif isinstance(m, (_BatchNorm, nn.GroupNorm)): + constant_init(m, 1) + + if self.zero_init_residual: + for m in self.modules(): + if isinstance(m, Bottleneck): + constant_init(m.norm3, 0) + elif isinstance(m, BasicBlock): + constant_init(m.norm2, 0) + else: + raise TypeError("pretrained must be a str or None") + + def forward(self, x): + """Forward function.""" + + stem_outputs = self.stem(x) + y_x2 = y_x4 = stem_outputs + # y_x2, y_x4 = stem_outputs[-2:] + y = y_x4 + + if self.enable_stem_pool: + y = self.stem_pool(y) + + y_list = [y] + for i in range(self.num_stages): + transition_modules = getattr(self, f"transition{i}") + + stage_inputs = [] + for j in range(self.stages_spec["num_branches"][i]): + if transition_modules[j]: + if j >= len(y_list): + stage_inputs.append(transition_modules[j](y_list[-1])) + else: + stage_inputs.append(transition_modules[j](y_list[j])) + else: + stage_inputs.append(y_list[j]) + + stage_module = getattr(self, f"stage{i}") + y_list = stage_module(stage_inputs) + + if self.out_modules is not None: + y_list.append(self.out_modules(y_list[-1])) + + if self.add_stem_features: + y_stem = self.stem_transition(y_x2) + y_list = [y_stem] + y_list + + out = y_list + if self.with_aggregator: + out = self.aggregator(out) + + if self.extra.get("add_input", False): + out = [x] + out + + return out + + def train(self, mode=True): + """Convert the model into training mode.""" + + super().train(mode) + + if mode and self.norm_eval: + for m in self.modules(): + if isinstance(m, _BatchNorm): + m.eval() diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/mmov_backbone.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/mmov_backbone.py new file mode 100644 index 00000000000..9ab524dcfb7 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/mmov_backbone.py @@ -0,0 +1,32 @@ +"""Backbone used for openvino export.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmseg.models.builder import BACKBONES + +from otx.core.ov.models.mmov_model import MMOVModel + +# pylint: disable=unused-argument + + +@BACKBONES.register_module() +class MMOVBackbone(MMOVModel): + """MMOVBackbone.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def forward(self, *args, **kwargs): + """Forward.""" + outputs = super().forward(*args, **kwargs) + if not isinstance(outputs, tuple): + outputs = (outputs,) + # must return tuple + return outputs + + def init_weights(self, pretrained=None): + """Initialize the weights.""" + # TODO + return diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/mscan.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/mscan.py new file mode 100644 index 00000000000..0db141d6c4a --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/mscan.py @@ -0,0 +1,471 @@ +"""Implementation of MSCAN backbone.""" +# Copyright (c) OpenMMLab. All rights reserved. +# Originally from https://github.com/visual-attention-network/segnext +# SPDX-License-Identifier: Apache-2.0 + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import math +import warnings + +import torch +from mmcv.cnn import build_activation_layer, build_norm_layer +from mmcv.cnn.bricks import DropPath +from mmcv.cnn.utils.weight_init import constant_init, normal_init, trunc_normal_init +from mmcv.runner import BaseModule +from mmseg.models.builder import BACKBONES +from torch import nn + + +class Mlp(BaseModule): + """Multi Layer Perceptron (MLP) Module. + + Args: + in_features (int): The dimension of input features. + hidden_features (int): The dimension of hidden features. + Defaults: None. + out_features (int): The dimension of output features. + Defaults: None. + act_cfg (dict): Config dict for activation layer in block. + Default: dict(type='GELU'). + drop (float): The number of dropout rate in MLP block. + Defaults: 0.0. + """ + + def __init__( + self, + in_features, + hidden_features=None, + out_features=None, + act_cfg=dict(type="GELU"), + drop=0.0, + ): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Conv2d(in_features, hidden_features, 1) + self.dwconv = nn.Conv2d(hidden_features, hidden_features, 3, 1, 1, bias=True, groups=hidden_features) + self.act = build_activation_layer(act_cfg) + self.fc2 = nn.Conv2d(hidden_features, out_features, 1) + self.drop = nn.Dropout(drop) + + def forward(self, x): + """Forward function.""" + + x = self.fc1(x) + + x = self.dwconv(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + + return x + + +class StemConv(BaseModule): + """Stem Block at the beginning of Semantic Branch. + + Args: + in_channels (int): The dimension of input channels. + out_channels (int): The dimension of output channels. + act_cfg (dict): Config dict for activation layer in block. + Default: dict(type='GELU'). + norm_cfg (dict): Config dict for normalization layer. + Defaults: dict(type='SyncBN', requires_grad=True). + """ + + def __init__( + self, + in_channels, + out_channels, + act_cfg=dict(type="GELU"), + norm_cfg=dict(type="SyncBN", requires_grad=True), + ): + super().__init__() + + self.proj = nn.Sequential( + nn.Conv2d( + in_channels, + out_channels // 2, + kernel_size=(3, 3), + stride=(2, 2), + padding=(1, 1), + ), + build_norm_layer(norm_cfg, out_channels // 2)[1], + build_activation_layer(act_cfg), + nn.Conv2d( + out_channels // 2, + out_channels, + kernel_size=(3, 3), + stride=(2, 2), + padding=(1, 1), + ), + build_norm_layer(norm_cfg, out_channels)[1], + ) + + def forward(self, x): + """Forward function.""" + + x = self.proj(x) + _, _, H, W = x.size() + x = x.flatten(2).transpose(1, 2) + return x, H, W + + +class MSCAAttention(BaseModule): + """Attention Module in Multi-Scale Convolutional Attention Module (MSCA). + + Args: + channels (int): The dimension of channels. + kernel_sizes (list): The size of attention + kernel. Defaults: [5, [1, 7], [1, 11], [1, 21]]. + paddings (list): The number of + corresponding padding value in attention module. + Defaults: [2, [0, 3], [0, 5], [0, 10]]. + """ + + def __init__( + self, + channels, + kernel_sizes=[5, [1, 7], [1, 11], [1, 21]], + paddings=[2, [0, 3], [0, 5], [0, 10]], + ): + super().__init__() + self.conv0 = nn.Conv2d( + channels, + channels, + kernel_size=kernel_sizes[0], + padding=paddings[0], + groups=channels, + ) + for i, (kernel_size, padding) in enumerate(zip(kernel_sizes[1:], paddings[1:])): + kernel_size_ = [kernel_size, kernel_size[::-1]] + padding_ = [padding, padding[::-1]] + conv_name = [f"conv{i}_1", f"conv{i}_2"] + for i_kernel, i_pad, i_conv in zip(kernel_size_, padding_, conv_name): + self.add_module( + i_conv, + nn.Conv2d( + channels, + channels, + tuple(i_kernel), + padding=i_pad, + groups=channels, + ), + ) + self.conv3 = nn.Conv2d(channels, channels, 1) + + def forward(self, x): + """Forward function.""" + + u = x.clone() + + attn = self.conv0(x) + + # Multi-Scale Feature extraction + attn_0 = self.conv0_1(attn) + attn_0 = self.conv0_2(attn_0) + + attn_1 = self.conv1_1(attn) + attn_1 = self.conv1_2(attn_1) + + attn_2 = self.conv2_1(attn) + attn_2 = self.conv2_2(attn_2) + + attn = attn + attn_0 + attn_1 + attn_2 + # Channel Mixing + attn = self.conv3(attn) + + # Convolutional Attention + x = attn * u + + return x + + +class MSCASpatialAttention(BaseModule): + """Spatial Attention Module in Multi-Scale Convolutional Attention Module (MSCA). + + Args: + in_channels (int): The dimension of channels. + attention_kernel_sizes (list): The size of attention + kernel. Defaults: [5, [1, 7], [1, 11], [1, 21]]. + attention_kernel_paddings (list): The number of + corresponding padding value in attention module. + Defaults: [2, [0, 3], [0, 5], [0, 10]]. + act_cfg (dict): Config dict for activation layer in block. + Default: dict(type='GELU'). + """ + + def __init__( + self, + in_channels, + attention_kernel_sizes=[5, [1, 7], [1, 11], [1, 21]], + attention_kernel_paddings=[2, [0, 3], [0, 5], [0, 10]], + act_cfg=dict(type="GELU"), + ): + super().__init__() + self.proj_1 = nn.Conv2d(in_channels, in_channels, 1) + self.activation = build_activation_layer(act_cfg) + self.spatial_gating_unit = MSCAAttention(in_channels, attention_kernel_sizes, attention_kernel_paddings) + self.proj_2 = nn.Conv2d(in_channels, in_channels, 1) + + def forward(self, x): + """Forward function.""" + + shorcut = x.clone() + x = self.proj_1(x) + x = self.activation(x) + x = self.spatial_gating_unit(x) + x = self.proj_2(x) + x = x + shorcut + return x + + +class MSCABlock(BaseModule): + """Basic Multi-Scale Convolutional Attention Block. + + It leverage the large- kernel attention (LKA) mechanism to build both channel + and spatial attention. In each branch, it uses two depth-wise strip convolutions + to approximate standard depth-wise convolutions with large kernels. The kernel + size for each branch is set to 7, 11, and 21, respectively. + + Args: + channels (int): The dimension of channels. + attention_kernel_sizes (list): The size of attention + kernel. Defaults: [5, [1, 7], [1, 11], [1, 21]]. + attention_kernel_paddings (list): The number of + corresponding padding value in attention module. + Defaults: [2, [0, 3], [0, 5], [0, 10]]. + mlp_ratio (float): The ratio of multiple input dimension to + calculate hidden feature in MLP layer. Defaults: 4.0. + drop (float): The number of dropout rate in MLP block. + Defaults: 0.0. + drop_path (float): The ratio of drop paths. + Defaults: 0.0. + act_cfg (dict): Config dict for activation layer in block. + Default: dict(type='GELU'). + norm_cfg (dict): Config dict for normalization layer. + Defaults: dict(type='SyncBN', requires_grad=True). + """ + + def __init__( + self, + channels, + attention_kernel_sizes=[5, [1, 7], [1, 11], [1, 21]], + attention_kernel_paddings=[2, [0, 3], [0, 5], [0, 10]], + mlp_ratio=4.0, + drop=0.0, + drop_path=0.0, + act_cfg=dict(type="GELU"), + norm_cfg=dict(type="SyncBN", requires_grad=True), + ): + super().__init__() + self.norm1 = build_norm_layer(norm_cfg, channels)[1] + self.attn = MSCASpatialAttention(channels, attention_kernel_sizes, attention_kernel_paddings, act_cfg) + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + self.norm2 = build_norm_layer(norm_cfg, channels)[1] + mlp_hidden_channels = int(channels * mlp_ratio) + self.mlp = Mlp( + in_features=channels, + hidden_features=mlp_hidden_channels, + act_cfg=act_cfg, + drop=drop, + ) + layer_scale_init_value = 1e-2 + self.layer_scale_1 = nn.Parameter(layer_scale_init_value * torch.ones(channels), requires_grad=True) + self.layer_scale_2 = nn.Parameter(layer_scale_init_value * torch.ones(channels), requires_grad=True) + + def forward(self, x, H, W): + """Forward function.""" + + B, N, C = x.shape + x = x.permute(0, 2, 1).view(B, C, H, W) + x = x + self.drop_path(self.layer_scale_1.unsqueeze(-1).unsqueeze(-1) * self.attn(self.norm1(x))) + x = x + self.drop_path(self.layer_scale_2.unsqueeze(-1).unsqueeze(-1) * self.mlp(self.norm2(x))) + x = x.view(B, C, N).permute(0, 2, 1) + return x + + +class OverlapPatchEmbed(BaseModule): + """Image to Patch Embedding. + + Args: + patch_size (int): The patch size. + Defaults: 7. + stride (int): Stride of the convolutional layer. + Default: 4. + in_channels (int): The number of input channels. + Defaults: 3. + embed_dims (int): The dimensions of embedding. + Defaults: 768. + norm_cfg (dict): Config dict for normalization layer. + Defaults: dict(type='SyncBN', requires_grad=True). + """ + + def __init__( + self, + patch_size=7, + stride=4, + in_channels=3, + embed_dim=768, + norm_cfg=dict(type="SyncBN", requires_grad=True), + ): + super().__init__() + + self.proj = nn.Conv2d( + in_channels, + embed_dim, + kernel_size=patch_size, + stride=stride, + padding=patch_size // 2, + ) + self.norm = build_norm_layer(norm_cfg, embed_dim)[1] + + def forward(self, x): + """Forward function.""" + + x = self.proj(x) + _, _, H, W = x.shape + x = self.norm(x) + + x = x.flatten(2).transpose(1, 2) + + return x, H, W + + +@BACKBONES.register_module() +class MSCAN(BaseModule): + """SegNeXt Multi-Scale Convolutional Attention Network (MCSAN) backbone. + + This backbone is the implementation of `SegNeXt: Rethinking + Convolutional Attention Design for Semantic + Segmentation `_. + Inspiration from https://github.com/visual-attention-network/segnext. + + Args: + in_channels (int): The number of input channels. Defaults: 3. + embed_dims (list[int]): Embedding dimension. + Defaults: [64, 128, 256, 512]. + mlp_ratios (list[int]): Ratio of mlp hidden dim to embedding dim. + Defaults: [4, 4, 4, 4]. + drop_rate (float): Dropout rate. Defaults: 0. + drop_path_rate (float): Stochastic depth rate. Defaults: 0. + depths (list[int]): Depths of each Swin Transformer stage. + Default: [3, 4, 6, 3]. + num_stages (int): MSCAN stages. Default: 4. + attention_kernel_sizes (list): Size of attention kernel in + Attention Module (Figure 2(b) of original paper). + Defaults: [5, [1, 7], [1, 11], [1, 21]]. + attention_kernel_paddings (list): Size of attention paddings + in Attention Module (Figure 2(b) of original paper). + Defaults: [2, [0, 3], [0, 5], [0, 10]]. + norm_cfg (dict): Config of norm layers. + Defaults: dict(type='SyncBN', requires_grad=True). + pretrained (str, optional): model pretrained path. + Default: None. + init_cfg (dict or list[dict], optional): Initialization config dict. + Default: None. + """ + + def __init__( + self, + in_channels=3, + embed_dims=[64, 128, 256, 512], + mlp_ratios=[4, 4, 4, 4], + drop_rate=0.0, + drop_path_rate=0.0, + depths=[3, 4, 6, 3], + num_stages=4, + attention_kernel_sizes=[5, [1, 7], [1, 11], [1, 21]], + attention_kernel_paddings=[2, [0, 3], [0, 5], [0, 10]], + act_cfg=dict(type="GELU"), + norm_cfg=dict(type="SyncBN", requires_grad=True), + pretrained=None, + init_cfg=None, + ): + super().__init__(init_cfg=init_cfg) + + assert not (init_cfg and pretrained), "init_cfg and pretrained cannot be set at the same time" + if isinstance(pretrained, str): + warnings.warn("DeprecationWarning: pretrained is deprecated, " 'please use "init_cfg" instead') + self.init_cfg = dict(type="Pretrained", checkpoint=pretrained) + elif pretrained is not None: + raise TypeError("pretrained must be a str or None") + + self.depths = depths + self.num_stages = num_stages + + dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule + cur = 0 + + for i in range(num_stages): + if i == 0: + patch_embed = StemConv(3, embed_dims[0], norm_cfg=norm_cfg) + else: + patch_embed = OverlapPatchEmbed( + patch_size=7 if i == 0 else 3, + stride=4 if i == 0 else 2, + in_channels=in_channels if i == 0 else embed_dims[i - 1], + embed_dim=embed_dims[i], + norm_cfg=norm_cfg, + ) + + block = nn.ModuleList( + [ + MSCABlock( + channels=embed_dims[i], + attention_kernel_sizes=attention_kernel_sizes, + attention_kernel_paddings=attention_kernel_paddings, + mlp_ratio=mlp_ratios[i], + drop=drop_rate, + drop_path=dpr[cur + j], + act_cfg=act_cfg, + norm_cfg=norm_cfg, + ) + for j in range(depths[i]) + ] + ) + norm = nn.LayerNorm(embed_dims[i]) + cur += depths[i] + + setattr(self, f"patch_embed{i + 1}", patch_embed) + setattr(self, f"block{i + 1}", block) + setattr(self, f"norm{i + 1}", norm) + + def init_weights(self): + """Initialize modules of MSCAN.""" + + print("init cfg", self.init_cfg) + if self.init_cfg is None: + for m in self.modules(): + if isinstance(m, nn.Linear): + trunc_normal_init(m, std=0.02, bias=0.0) + elif isinstance(m, nn.LayerNorm): + constant_init(m, val=1.0, bias=0.0) + elif isinstance(m, nn.Conv2d): + fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels + fan_out //= m.groups + normal_init(m, mean=0, std=math.sqrt(2.0 / fan_out), bias=0) + else: + super().init_weights() + + def forward(self, x): + """Forward function.""" + + B = x.shape[0] + outs = [] + + for i in range(self.num_stages): + patch_embed = getattr(self, f"patch_embed{i + 1}") + block = getattr(self, f"block{i + 1}") + norm = getattr(self, f"norm{i + 1}") + x, H, W = patch_embed(x) + for blk in block: + x = blk(x, H, W) + x = norm(x) + x = x.reshape(B, H, W, -1).permute(0, 3, 1, 2).contiguous() + outs.append(x) + + return outs diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py new file mode 100644 index 00000000000..5498ac53fae --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py @@ -0,0 +1,22 @@ +"""Semantic segmentation heads.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .custom_otx_head import otx_head_factory +from .detcon_head import DetConHead +from .light_ham import LightHamHead +from .mmov_decode_head import MMOVDecodeHead + +__all__ = ["MMOVDecodeHead", "DetConHead", "otx_head_factory", "LightHamHead"] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/custom_otx_head.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/custom_otx_head.py new file mode 100644 index 00000000000..4f62c26fa6e --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/custom_otx_head.py @@ -0,0 +1,198 @@ +"""Custom universal class incremental otx head.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from typing import Dict, List, Optional + +import torch +from mmcv.runner import force_fp32 +from mmseg.models.decode_heads.fcn_head import FCNHead +from mmseg.models.decode_heads.sep_aspp_head import DepthwiseSeparableASPPHead +from mmseg.models.losses import accuracy +from mmseg.ops import resize +from torch import nn + +from otx.algorithms.segmentation.adapters.mmseg.models.utils import IterativeAggregator +from otx.algorithms.segmentation.adapters.mmseg.utils import ( + get_valid_label_mask_per_batch, +) + +from .light_ham import LightHamHead + +KNOWN_HEADS = { + "FCNHead": FCNHead, + "ASPPHead": DepthwiseSeparableASPPHead, + "LightHamHead": LightHamHead, +} + + +def otx_head_factory(*args, base_type="FCNHead", **kwargs): + """Factory function for creating custom otx head based on mmsegmentation heads.""" + + head_base_cls = KNOWN_HEADS[base_type] + + class CustomOTXHead(head_base_cls): + """Custom OTX head for Semantic Segmentation. + + This Head added head aggregator used in Lite-HRNet by + DepthwiseSeparableConvModule. + Please refer to https://github.com/HRNet/Lite-HRNet. + It also provides interface for incremental learning + inside OTX framework. + + Args: + base_type (bool): base type of segmentation head + enable_aggregator (bool): If true, will use aggregator + concating all inputs from backbone by DepthwiseSeparableConvModule. + aggregator_min_channels (int, optional): The number of channels of output of aggregator. + It would work only if enable_aggregator is true. + aggregator_merge_norm (str, optional): normalize the output of expanders of aggregator. + options : "none", "channel", or None + aggregator_use_concat (str, optional): Whether to concat the last input + with the output of expanders. + """ + + def __init__( + self, + base_type: str = "FCNHead", + enable_aggregator: bool = False, + aggregator_min_channels: Optional[int] = None, + aggregator_merge_norm: Optional[str] = None, + aggregator_use_concat: bool = False, + *args, + **kwargs + ): + + in_channels = kwargs.get("in_channels") + in_index = kwargs.get("in_index") + norm_cfg = kwargs.get("norm_cfg") + conv_cfg = kwargs.get("conv_cfg") + input_transform = kwargs.get("input_transform") + + aggregator = None + if enable_aggregator: # Lite-HRNet aggregator + assert isinstance(in_channels, (tuple, list)) + assert len(in_channels) > 1 + + aggregator = IterativeAggregator( + in_channels=in_channels, + min_channels=aggregator_min_channels, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + merge_norm=aggregator_merge_norm, + use_concat=aggregator_use_concat, + ) + + aggregator_min_channels = aggregator_min_channels if aggregator_min_channels is not None else 0 + # change arguments temporarily + kwargs["in_channels"] = max(in_channels[0], aggregator_min_channels) + kwargs["input_transform"] = None + if in_index is not None: + kwargs["in_index"] = in_index[0] + + super().__init__(*args, **kwargs) + + self.aggregator = aggregator + + # re-define variables + self.in_channels = in_channels + self.input_transform = input_transform + self.in_index = in_index + + self.ignore_index = 255 + + # get rid of last activation of convs module + if self.act_cfg and base_type == "FCNHead": + self.convs[-1].with_activation = False + delattr(self.convs[-1], "activate") + + if kwargs.get("init_cfg", {}): + self.init_weights() + + def _transform_inputs(self, inputs: torch.Tensor): + if self.aggregator is not None: + inputs = self.aggregator(inputs)[0] + else: + inputs = super()._transform_inputs(inputs) + + return inputs + + def forward_train( + self, + inputs: torch.Tensor, + img_metas: List[Dict], + gt_semantic_seg: torch.Tensor, + train_cfg: Dict = dict(), + loss_only: bool = False, + ): + """Forward function for training. + + Args: + inputs (list[Tensor]): List of multi-level img features. + img_metas (list[dict]): List of image info dict where each dict + has: 'img_shape', 'scale_factor', 'flip', and may also contain + 'filename', 'ori_shape', 'pad_shape', and 'img_norm_cfg'. + For details on the values of these keys see + `mmseg/datasets/pipelines/formatting.py:Collect`. + gt_semantic_seg (Tensor): Semantic segmentation masks + used if the architecture supports semantic segmentation task. + train_cfg (dict): The training config. + loss_only (bool): If true computes loss only without head forward + + Returns: + dict[str, Tensor]: a dictionary of loss components + """ + # is loss_only is True -> inputs are already model logits + seg_logits = self(inputs) if not loss_only else inputs + valid_label_mask = get_valid_label_mask_per_batch(img_metas, self.num_classes) + losses = self.losses(seg_logits, gt_semantic_seg, valid_label_mask=valid_label_mask) + return losses + + @force_fp32(apply_to=("seg_logit",)) + def losses( + self, + seg_logit: torch.Tensor, + seg_label: torch.Tensor, + valid_label_mask: Optional[torch.Tensor] = None, + ): + """Compute segmentation loss.""" + loss = dict() + + seg_logit = resize( + input=seg_logit, + size=seg_label.shape[2:], + mode="bilinear", + align_corners=self.align_corners, + ) + if self.sampler is not None: + seg_weight = self.sampler.sample(seg_logit, seg_label) + else: + seg_weight = None + seg_label = seg_label.squeeze(1) + + if not isinstance(self.loss_decode, nn.ModuleList): + losses_decode = [self.loss_decode] + else: + losses_decode = self.loss_decode + for loss_decode in losses_decode: + valid_label_mask_cfg = dict() + if loss_decode.loss_name == "loss_ce_ignore": + valid_label_mask_cfg["valid_label_mask"] = valid_label_mask + if loss_decode.loss_name not in loss: + loss[loss_decode.loss_name] = loss_decode( + seg_logit, seg_label, weight=seg_weight, ignore_index=self.ignore_index, **valid_label_mask_cfg + ) + else: + loss[loss_decode.loss_name] += loss_decode( + seg_logit, seg_label, weight=seg_weight, ignore_index=self.ignore_index, **valid_label_mask_cfg + ) + + if seg_logit.device.type == "hpu": + seg_logit = seg_logit.detach().clone().to("cpu") + + loss["acc_seg"] = accuracy(seg_logit, seg_label, ignore_index=self.ignore_index) + + return loss + + return CustomOTXHead(base_type, *args, **kwargs) diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/detcon_head.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/detcon_head.py new file mode 100644 index 00000000000..3466fe54143 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/detcon_head.py @@ -0,0 +1,72 @@ +"""DetCon head to get pixel-wise contrastive loss with predictor head.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=unused-argument +from typing import Any, Dict + +import torch +from mmseg.models.builder import HEADS, build_loss, build_neck +from torch import nn + + +@HEADS.register_module() +class DetConHead(nn.Module): + """DetCon head for pixel-wise contrastive learning. + + Args: + predictor (dict): configurations for predictor. + loss_cfg (dict): configurations for detcon loss. + """ + + def __init__(self, predictor: Dict[str, Any], loss_cfg: Dict[str, Any], **kwargs): + super().__init__() + self.predictor = build_neck(predictor) + self.detcon_loss = build_loss(loss_cfg) + + def init_weights(self): + """Initialize predictor weights.""" + self.predictor.init_weights() + + # pylint: disable=too-many-locals + def forward( + self, + projs: torch.Tensor, + projs_tgt: torch.Tensor, + ids: torch.Tensor, + ids_tgt: torch.Tensor, + batch_size: int, + num_samples: int, + ) -> Dict[str, torch.Tensor]: + """Forward head. + + Args: + projs (Tensor): NxC input features. + projs_tgt (Tensor): NxC target features. + ids (Tensor): NxC input ids. + ids_tgt (Tensor): NxC target ids. + batch_size (int): Batch size to split concatenated features. + num_samples (int): The number of samples to be sampled. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + preds = self.predictor(projs) + pred1, pred2 = torch.split(preds.reshape((-1, num_samples, preds.shape[-1])), batch_size) + id1, id2 = torch.split(ids, batch_size) + proj1_tgt, proj2_tgt = torch.split(projs_tgt.reshape((-1, num_samples, projs_tgt.shape[-1])), batch_size) + id1_tgt, id2_tgt = torch.split(ids_tgt, batch_size) + + loss = self.detcon_loss( + pred1=pred1, + pred2=pred2, + target1=proj1_tgt, + target2=proj2_tgt, + pind1=id1, + pind2=id2, + tind1=id1_tgt, + tind2=id2_tgt, + ) + + return dict(loss=loss) diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/light_ham.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/light_ham.py new file mode 100644 index 00000000000..00508130735 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/light_ham.py @@ -0,0 +1,253 @@ +"""Implementation of HamburgerNet head.""" +# Copyright (c) OpenMMLab. All rights reserved. +# Originally from https://github.com/visual-attention-network/segnext +# SPDX-License-Identifier: Apache-2.0 + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import torch +import torch.nn.functional as F +from mmcv.cnn import ConvModule +from mmseg.models.builder import HEADS +from mmseg.models.decode_heads.decode_head import BaseDecodeHead +from mmseg.ops import resize +from torch import nn + + +class Matrix_Decomposition_2D_Base(nn.Module): + """Base class of 2D Matrix Decomposition. + + Args: + MD_S (int): The number of spatial coefficient in + Matrix Decomposition, it may be used for calculation + of the number of latent dimension D in Matrix + Decomposition. Defaults: 1. + MD_R (int): The number of latent dimension R in + Matrix Decomposition. Defaults: 64. + train_steps (int): The number of iteration steps in + Multiplicative Update (MU) rule to solve Non-negative + Matrix Factorization (NMF) in training. Defaults: 6. + eval_steps (int): The number of iteration steps in + Multiplicative Update (MU) rule to solve Non-negative + Matrix Factorization (NMF) in evaluation. Defaults: 7. + inv_t (int): Inverted multiple number to make coefficient + smaller in softmax. Defaults: 100. + rand_init (bool): Whether to initialize randomly. + Defaults: True. + """ + + def __init__(self, MD_S=1, MD_R=64, in_channels=256, train_steps=6, eval_steps=7, inv_t=100): + super().__init__() + + self.S = MD_S + self.R = MD_R + + self.train_steps = train_steps + self.eval_steps = eval_steps + + self.inv_t = inv_t + + bases = self._build_bases(1, self.S, in_channels // self.S, self.R) + self.register_buffer("bases", bases) + + def _build_bases(self, B, S, D, R, device=None): + raise NotImplementedError + + def local_step(self, x, bases, coef): + """Local step in iteration to renew bases and coefficient.""" + raise NotImplementedError + + def local_inference(self, x, bases): + """Local inference.""" + # (B * S, D, N)^T @ (B * S, D, R) -> (B * S, N, R) + coef = torch.bmm(x.transpose(1, 2), bases) + coef = F.softmax(self.inv_t * coef, dim=-1) + + steps = self.train_steps if self.training else self.eval_steps + for _ in range(steps): + bases, coef = self.local_step(x, bases, coef) + + return bases, coef + + def compute_coef(self, x, bases, coef): + """Compute coefficient.""" + raise NotImplementedError + + def forward(self, x, return_bases=False): + """Forward Function.""" + B, C, H, W = x.shape + + # (B, C, H, W) -> (B * S, D, N) + D = C // self.S + N = H * W + x = x.view(B * self.S, D, N) + bases = self.bases.repeat(B, 1, 1) + bases, coef = self.local_inference(x, bases) + + # (B * S, N, R) + coef = self.compute_coef(x, bases, coef) + + # (B * S, D, R) @ (B * S, N, R)^T -> (B * S, D, N) + x = torch.bmm(bases, coef.transpose(1, 2)) + + # (B * S, D, N) -> (B, C, H, W) + x = x.view(B, C, H, W) + + return x + + +class NMF2D(Matrix_Decomposition_2D_Base): + """Non-negative Matrix Factorization (NMF) module. + + It is inherited from ``Matrix_Decomposition_2D_Base`` module. + """ + + def __init__(self, args=dict()): + super().__init__(**args) + + self.inv_t = 1 + + def _build_bases(self, B, S, D, R, device=None): + """Build bases in initialization.""" + bases = torch.rand((B * S, D, R)).to(device) + bases = F.normalize(bases, dim=1) + + return bases + + def local_step(self, x, bases, coef): + """Local step in iteration to renew bases and coefficient.""" + # (B * S, D, N)^T @ (B * S, D, R) -> (B * S, N, R) + numerator = torch.bmm(x.transpose(1, 2), bases) + # (B * S, N, R) @ [(B * S, D, R)^T @ (B * S, D, R)] -> (B * S, N, R) + denominator = coef.bmm(bases.transpose(1, 2).bmm(bases)) + # Multiplicative Update + coef = coef * numerator / (denominator + 1e-6) + + # (B * S, D, N) @ (B * S, N, R) -> (B * S, D, R) + numerator = torch.bmm(x, coef) + # (B * S, D, R) @ [(B * S, N, R)^T @ (B * S, N, R)] -> (B * S, D, R) + denominator = bases.bmm(coef.transpose(1, 2).bmm(coef)) + # Multiplicative Update + bases = bases * numerator / (denominator + 1e-6) + + return bases, coef + + def compute_coef(self, x, bases, coef): + """Compute coefficient.""" + # (B * S, D, N)^T @ (B * S, D, R) -> (B * S, N, R) + numerator = torch.bmm(x.transpose(1, 2), bases) + # (B * S, N, R) @ (B * S, D, R)^T @ (B * S, D, R) -> (B * S, N, R) + denominator = coef.bmm(bases.transpose(1, 2).bmm(bases)) + # multiplication update + coef = coef * numerator / (denominator + 1e-6) + + return coef + + +class Hamburger(nn.Module): + """Hamburger Module. + + It consists of one slice of "ham" (matrix decomposition) + and two slices of "bread" (linear transformation). + + Args: + ham_channels (int): Input and output channels of feature. + ham_kwargs (dict): Config of matrix decomposition module. + norm_cfg (dict | None): Config of norm layers. + """ + + def __init__(self, ham_channels=512, ham_kwargs=dict(), norm_cfg=None, **kwargs): + super().__init__() + + self.ham_in = ConvModule(ham_channels, ham_channels, 1, norm_cfg=None, act_cfg=None) + + self.ham = NMF2D(ham_kwargs) + + self.ham_out = ConvModule(ham_channels, ham_channels, 1, norm_cfg=norm_cfg, act_cfg=None) + + def forward(self, x): + """Forward function for Hamburger Module.""" + enjoy = self.ham_in(x) + enjoy = F.relu(enjoy, inplace=True) + enjoy = self.ham(enjoy) + enjoy = self.ham_out(enjoy) + ham = F.relu(x + enjoy, inplace=True) + + return ham + + +@HEADS.register_module() +class LightHamHead(BaseDecodeHead): + """SegNeXt decode head. + + This decode head is the implementation of `SegNeXt: Rethinking + Convolutional Attention Design for Semantic + Segmentation `_. + Inspiration from https://github.com/visual-attention-network/segnext. + Specifically, LightHamHead is inspired by HamNet from + `Is Attention Better Than Matrix Decomposition? + `. + + Args: + ham_channels (int): input channels for Hamburger. + Defaults: 512. + ham_kwargs (int): kwagrs for Ham. Defaults: dict(). + """ + + def __init__(self, ham_channels=512, ham_kwargs=dict(), **kwargs): + super().__init__(**kwargs) + self.ham_channels = ham_channels + + self.squeeze = ConvModule( + sum(self.in_channels), + self.ham_channels, + 1, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=self.act_cfg, + ) + + self.hamburger = Hamburger(ham_channels, ham_kwargs, **kwargs) + + self.align = ConvModule( + self.ham_channels, + self.channels, + 1, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=self.act_cfg, + ) + + def _forward_feature(self, inputs): + """Forward features function.""" + inputs = self._transform_inputs(inputs) + + inputs = [ + resize( + level, + size=inputs[0].shape[2:], + mode="bilinear", + align_corners=self.align_corners, + ) + for level in inputs + ] + + inputs = torch.cat(inputs, dim=1) + # apply a conv block to squeeze feature map + x = self.squeeze(inputs) + # apply hamburger module + x = self.hamburger(x) + return x + + def _forward_cls(self, feat): + """Forward classifier.""" + output = self.align(feat) + output = self.cls_seg(output) + return output + + def forward(self, inputs): + """Forward function.""" + feats = self._forward_feature(inputs) + output = self._forward_cls(feats) + return output diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/mmov_decode_head.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/mmov_decode_head.py new file mode 100644 index 00000000000..f34c72b9840 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/mmov_decode_head.py @@ -0,0 +1,89 @@ +"""Decode-head used for openvino export.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +from mmseg.models.decode_heads.decode_head import BaseDecodeHead + +from otx.core.ov.models.mmov_model import MMOVModel + +# pylint: disable=too-many-instance-attributes, keyword-arg-before-vararg + + +class MMOVDecodeHead(BaseDecodeHead): + """MMOVDecodeHead.""" + + def __init__( + self, + model_path_or_model: Union[str, ov.Model] = None, + weight_path: Optional[str] = None, + inputs: Optional[Dict[str, Union[str, List[str]]]] = None, + outputs: Optional[Dict[str, Union[str, List[str]]]] = None, + init_weight: bool = False, + verify_shape: bool = True, + *args, + **kwargs + ): + if inputs is None: + inputs = {} + if outputs is None: + outputs = {} + self._model_path_or_model = model_path_or_model + self._weight_path = weight_path + self._inputs = deepcopy(inputs) + self._outputs = deepcopy(outputs) + self._init_weight = init_weight + self._verify_shape = verify_shape + + # dummy input + channels = 1 + super().__init__( + channels=channels, + *args, + **kwargs, + ) + delattr(self, "channels") + + if "extractor" in inputs and "extractor" in outputs: + self.extractor = MMOVModel( + self._model_path_or_model, + self._weight_path, + inputs=inputs["extractor"], + outputs=outputs["extractor"], + remove_normalize=False, + merge_bn=True, + paired_bn=True, + verify_shape=self._verify_shape, + init_weight=self._init_weight, + ) + + assert "cls_seg" in inputs and "cls_seg" in outputs + self.conv_seg = MMOVModel( + self._model_path_or_model, + self._weight_path, + inputs=inputs["cls_seg"], + outputs=outputs["cls_seg"], + remove_normalize=False, + merge_bn=False, + paired_bn=False, + verify_shape=self._verify_shape, + init_weight=self._init_weight, + ) + + def init_weights(self): + """Init weights.""" + # TODO + return + + def forward(self, inputs): + """Forward.""" + outputs = self._transform_inputs(inputs) + if getattr(self, "extractor"): + outputs = self.extractor(outputs) + outputs = self.cls_seg(outputs) + return outputs diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/proto_head.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/proto_head.py new file mode 100644 index 00000000000..887e2a1800f --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/heads/proto_head.py @@ -0,0 +1,142 @@ +"""Prototype based head.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import torch +import torch.distributed as dist +import torch.nn.functional as F +from einops import rearrange, repeat +from mmcv.runner import force_fp32 +from mmseg.models.builder import HEADS +from mmseg.models.decode_heads.decode_head import BaseDecodeHead +from mmseg.models.losses import accuracy +from torch import nn + +from otx.algorithms.segmentation.adapters.mmseg.models.losses import PixelPrototypeCELoss +from otx.algorithms.segmentation.adapters.mmseg.models.utils import ( + ProjectionHead, + distributed_sinkhorn, + momentum_update, + trunc_normal_, +) + + +@HEADS.register_module() +class ProtoNet(BaseDecodeHead): + """Prototype based head. + + This head introduce prototype view learning. Prediction is achieved + by nonparametric nearest prototype retrieving. This allows our model + to directly shape the pixel embedding space, by optimizing the arrangement + between embedded pixels and anchored prototypes. + This network was developed based on two articles: https://arxiv.org/abs/2203.15102 + and https://arxiv.org/abs/2210.04388 + + Args: + gamma (bool): parameter used for momentum update. + Defines influence of past states during the prototypes update. + num_prototype (int): number of prototypes per class. + in_proto_channels (int): number of channels of the prototypes (dimension). + num_classes (int): number of classes. + """ + + def __init__(self, gamma, num_prototype, in_proto_channels, num_classes, **kwargs): + super().__init__(num_classes=num_classes, **kwargs) + self.gamma = gamma + self.num_prototype = num_prototype + self.num_classes = num_classes + self.prototypes = nn.Parameter( + torch.zeros(self.num_classes, self.num_prototype, in_proto_channels), requires_grad=False + ) + trunc_normal_(self.prototypes, std=0.02) + self.avg_pool = nn.AdaptiveAvgPool2d(in_proto_channels) + self.proj_head = ProjectionHead(in_proto_channels, in_proto_channels) + self.feat_norm = nn.LayerNorm(in_proto_channels) + self.mask_norm = nn.LayerNorm(self.num_classes) + + def prototype_learning(self, _c, out_seg, gt_seg, masks): + """Prototype learning algorithm.""" + pred_seg = torch.max(out_seg, 1)[1] + mask = gt_seg == pred_seg.view(-1) + + cosine_similarity = torch.mm(_c, self.prototypes.view(-1, self.prototypes.shape[-1]).t()) + + proto_logits = cosine_similarity + proto_target = gt_seg.clone().float() + + # clustering for each class + protos = self.prototypes.data.clone() + + for k in range(self.num_classes): + init_q = masks[..., k] + init_q = init_q[gt_seg == k, ...] + if init_q.shape[0] == 0: + continue + + q, indexs = distributed_sinkhorn(init_q) + m_k = mask[gt_seg == k] + c_k = _c[gt_seg == k, ...] + m_k_tile = repeat(m_k, "n -> n tile", tile=self.num_prototype) + m_q = q * m_k_tile # n x self.num_prototype + c_k_tile = repeat(m_k, "n -> n tile", tile=c_k.shape[-1]) + c_q = c_k * c_k_tile # n x embedding_dim + f = m_q.transpose(0, 1) @ c_q # self.num_prototype x embedding_dim + n = torch.sum(m_q, dim=0) + proto_target[gt_seg == k] = indexs.float() + (self.num_prototype * k) + + if torch.sum(n) > 0: + f = F.normalize(f, p=2, dim=-1) + new_value = momentum_update( + old_value=protos[k, n != 0, :], new_value=f[n != 0, :], momentum=self.gamma, debug=False + ) + protos[k, n != 0, :] = new_value + + self.prototypes = nn.Parameter(F.normalize(protos, p=2, dim=-1), requires_grad=False) + + if dist.is_available() and dist.is_initialized(): + protos = self.prototypes.data.clone() + dist.all_reduce(protos.div_(dist.get_world_size())) + self.prototypes = nn.Parameter(protos, requires_grad=False) + + return proto_logits, proto_target + + def forward(self, inputs, gt_semantic_seg): + """Forward method.""" + c = self.proj_head(inputs) + _c = rearrange(c, "b c h w -> (b h w) c") + _c = self.feat_norm(_c) + _c = F.normalize(_c, p=2, dim=-1) + self.prototypes.data.copy_(F.normalize(self.prototypes, p=2, dim=-1)) + # n: h*w, k: num_class, m: num_prototype + masks = torch.einsum("nd,kmd->nmk", _c, self.prototypes) + + out_seg = torch.amax(masks, dim=1) + out_seg = self.mask_norm(out_seg) + out_seg = rearrange(out_seg, "(b h w) k -> b k h w", b=inputs.shape[0], h=inputs.shape[2]) + gt_seg = F.interpolate(gt_semantic_seg.float(), size=inputs.size()[2:], mode="nearest").view(-1) + contrast_logits, contrast_target = self.prototype_learning(_c, out_seg, gt_seg, masks) + out_seg = F.interpolate(out_seg, size=gt_semantic_seg.shape[-2:], mode="bilinear") + proto_out = {"out_seg": out_seg, "contrast_logits": contrast_logits, "contrast_target": contrast_target} + + return proto_out + + @force_fp32( + apply_to=( + "out_seg", + "contrast_logits", + "contrast_target", + ) + ) + def losses(self, out_seg, contrast_logits, contrast_target, seg_label): + """Computes loss function.""" + loss = dict() + + if not isinstance(self.loss_decode, PixelPrototypeCELoss): + raise ValueError("decode loss should be PixelPrototypeCELoss") + + loss[self.loss_decode.loss_name] = self.loss_decode(out_seg, contrast_logits, contrast_target, seg_label) + + loss["acc_seg"] = accuracy(out_seg, seg_label.squeeze(1), ignore_index=self.ignore_index) + + return loss diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py new file mode 100644 index 00000000000..3d64e0b1365 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py @@ -0,0 +1,21 @@ +"""Segmentation losses.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .cross_entropy_loss_with_ignore import CrossEntropyLossWithIgnore +from .detcon_loss import DetConLoss +from .pixel_prototype_ce_loss import PixelPrototypeCELoss + +__all__ = ["DetConLoss", "CrossEntropyLossWithIgnore", "PixelPrototypeCELoss"] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/cross_entropy_loss_with_ignore.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/cross_entropy_loss_with_ignore.py new file mode 100644 index 00000000000..16c1f83718c --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/cross_entropy_loss_with_ignore.py @@ -0,0 +1,99 @@ +"""Cross entropy loss for ignored mode in class-incremental learning.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Optional + +import torch +import torch.nn.functional as F +from mmseg.models.builder import LOSSES +from mmseg.models.losses import CrossEntropyLoss +from mmseg.models.losses.utils import weight_reduce_loss + + +@LOSSES.register_module() +class CrossEntropyLossWithIgnore(CrossEntropyLoss): + """CrossEntropyLossWithIgnore with Ignore Mode Support for Class Incremental Learning. + + When new classes are added through continual training cycles, images from previous cycles + may become partially annotated if they are not revisited. + To prevent the model from predicting these new classes for such images, + CrossEntropyLossWithIgnore can be used to ignore the unseen classes. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._loss_name = "loss_ce_ignore" + + def forward( + self, + cls_score: Optional[torch.Tensor], + label: Optional[torch.Tensor], + weight: Optional[torch.Tensor] = None, + avg_factor: Optional[int] = None, + reduction_override: Optional[str] = "mean", + ignore_index: int = 255, + valid_label_mask: Optional[torch.Tensor] = None, + **kwargs + ): + """Forward. + + Args: + cls_score (torch.Tensor, optional): The prediction with shape (N, 1). + label (torch.Tensor, optional): The learning label of the prediction. + weight (torch.Tensor, optional): Sample-wise loss weight. + Default: None. + class_weight (list[float], optional): The weight for each class. + Default: None. + avg_factor (int, optional): Average factor that is used to average + the loss. Default: None. + reduction_override (str, optional): The method used to reduce the loss. + Options are 'none', 'mean' and 'sum'. Default: 'mean'. + ignore_index (int): Specifies a target value that is ignored and + does not contribute to the input gradients. When + ``avg_non_ignore `` is ``True``, and the ``reduction`` is + ``''mean''``, the loss is averaged over non-ignored targets. + Defaults: 255. + valid_label_mask (torch.Tensor, optional): The valid labels with + shape (N, num_classes). + If the value in the valid_label_mask is 0, mask label of the + the mask label of the class corresponding to its index will be + ignored like ignore_index. + **kwargs (Any): Additional keyword arguments. + """ + if valid_label_mask is None: + losses = super().forward(cls_score, label, weight, avg_factor, reduction_override, ignore_index, **kwargs) + return losses + else: + assert reduction_override in (None, "none", "mean", "sum") + reduction = reduction_override if reduction_override else self.reduction + batch_size = label.shape[0] + for i in range(batch_size): + invalid_labels = (valid_label_mask[i] == 0).nonzero(as_tuple=False) + + for inv_l in invalid_labels: + label[i] = torch.where(label[i] == inv_l.item(), ignore_index, label[i]) + + losses = F.cross_entropy(cls_score, label, reduction="none", ignore_index=ignore_index) + + if weight is not None: + weight = weight.float() + losses = weight_reduce_loss(losses, weight=weight, reduction=reduction, avg_factor=avg_factor) + + return losses + + @property + def loss_name(self): + """Loss Name. + + This function must be implemented and will return the name of this + loss function. This name will be used to combine different loss items + by simple sum operation. In addition, if you want this loss item to be + included into the backward graph, `loss_` must be the prefix of the + name. + + Returns: + str: The name of this loss item. + """ + return self._loss_name diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/detcon_loss.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/detcon_loss.py new file mode 100644 index 00000000000..20c61f50ea9 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/detcon_loss.py @@ -0,0 +1,184 @@ +"""DetCon loss.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=no-name-in-module, not-callable +import itertools + +import torch +import torch.distributed as dist +import torch.nn.functional as F +from mmseg.models.builder import LOSSES +from torch import nn + + +def manual_cross_entropy(logits, labels, weight): + """Manually calculate weighted cross entropy.""" + cross_entropy = -weight * torch.sum(labels * F.log_softmax(logits, dim=-1), dim=-1) + return torch.mean(cross_entropy) + + +@LOSSES.register_module +class DetConLoss(nn.Module): + """Modified from https://github.com/deepmind/detcon/blob/main/utils/losses.py. + + Compute the NCE scores from pairs of predictions and targets. + This implements the batched form of the loss described in + Section 3.1, Equation 3 in https://arxiv.org/pdf/2103.10957.pdf. + + Args: + temperature: (float) the temperature to use for the NCE loss. + use_replicator_loss (bool): use cross-replica samples. + """ + + def __init__( + self, + temperature: float = 0.1, + use_replicator_loss: bool = True, + ignore_index: int = 255, + ): # pylint: disable=unused-argument + super().__init__() + assert temperature > 0 + self.temperature = torch.tensor(temperature) + self.use_replicator_loss = use_replicator_loss + + def get_distributed_tensors(self, target1, target2, batch_size, num_samples, num_features, device): + """Grab tensors across replicas during distributed training.""" + if dist.is_initialized() and self.use_replicator_loss: + # Grab tensor across replicas and expand first dimension + world_size = dist.get_world_size() + target1_large = [torch.zeros_like(target1) for _ in range(world_size)] + target2_large = [torch.zeros_like(target2) for _ in range(world_size)] + dist.all_gather(target1_large, target1) + dist.all_gather(target2_large, target2) + target1_large = torch.cat(target1_large, dim=0) + target2_large = torch.cat(target2_large, dim=0) + + # Fold into batch dimension + target1_large = target1_large.reshape(-1, num_samples, num_features) + target2_large = target2_large.reshape(-1, num_samples, num_features) + + # Create the labels by using the current replica ID and offsetting. + replica_id = dist.get_rank() + labels_idx = torch.arange(batch_size) + replica_id * batch_size + enlarged_bs = target1_large.shape[0] + labels = F.one_hot(labels_idx, num_classes=enlarged_bs).to(device) + else: + target1_large = target1 + target2_large = target2 + labels = F.one_hot(torch.arange(batch_size), num_classes=batch_size).to(device) + + labels = labels.unsqueeze(dim=2).unsqueeze(dim=1) + + return target1_large, target2_large, labels + + # pylint: disable=too-many-arguments, too-many-locals + def forward( + self, + pred1, + pred2, + target1, + target2, + pind1, + pind2, + tind1, + tind2, + local_negatives=True, + ): + """Forward loss. + + Args: + pred1 (Tensor): (b, num_samples, d) the prediction from first view. + pred2 (Tensor): (b, num_samples, d) the prediction from second view. + target1 (Tensor): (b, num_samples, d) the projection from first view. + target2 (Tensor): (b, num_samples, d) the projection from second view. + pind1 (Tensor): (b, num_samples) mask indices for first view's prediction. + pind2 (Tensor): (b, num_samples) mask indices for second view's prediction. + tind1 (Tensor): (b, num_samples) mask indices for first view's projection. + tind2 (Tensor): (b, num_samples) mask indices for second view's projection. + local_negatives (bool): whether to include local negatives. + + Returns: + dict[str, Tensor]: A single scalar loss for the XT-NCE objective. + """ + batch_size, num_samples, num_features = pred1.shape + main_dtype = pred1.dtype + # infinity_proxy is reduced to avoid overflow when training w/ fp16. + infinity_proxy = 1e4 # Used for masks to proxy a very large number. + + def make_same_obj(ind_0, ind_1): + same_obj = torch.eq( + ind_0.reshape([batch_size, num_samples, 1]), + ind_1.reshape([batch_size, 1, num_samples]), + ) + same_obj = same_obj.unsqueeze(2).to(main_dtype) + return same_obj + + same_obj_dict = {} + for pair, (pind, tind) in zip( + ["aa", "ab", "ba", "bb"], + list(itertools.product([pind1, pind2], [tind1, tind2])), + ): + same_obj_dict[pair] = make_same_obj(pind, tind) + + # L2 normalize the tensors to use for the cosine-similarity + def normalize_same_dtype(logit, p=2, dim=1, eps=1e-12, dtype=None): + # modified from torch.nn.functional.normalize + denom = logit.norm(p, dim, keepdim=True, dtype=dtype).clamp_min(eps).expand_as(logit) + return logit / denom + + pred1 = normalize_same_dtype(pred1, dim=-1, dtype=main_dtype) + pred2 = normalize_same_dtype(pred2, dim=-1, dtype=main_dtype) + target1 = normalize_same_dtype(target1, dim=-1, dtype=main_dtype) + target2 = normalize_same_dtype(target2, dim=-1, dtype=main_dtype) + target1_large, target2_large, labels = self.get_distributed_tensors( + target1, target2, batch_size, num_samples, num_features, pred1.device + ) + + # Do our matmuls and mask out appropriately. + logits_dict = {} + for pair, (pred, target) in zip( + ["aa", "ab", "ba", "bb"], + list(itertools.product([pred1, pred2], [target1_large, target2_large])), + ): + logits_dict[pair] = torch.einsum("abk,uvk->abuv", pred, target) / self.temperature + + labels_dict = {key: labels * same_obj for key, same_obj in same_obj_dict.items()} + for pair in ["aa", "bb"]: + logits_dict[pair] -= infinity_proxy * labels * same_obj_dict[pair] + labels_dict[pair] *= 0.0 + + if not local_negatives: + for pair in ["aa", "ab", "ba", "bb"]: + logits_dict[pair] -= infinity_proxy * labels * (1 - same_obj_dict[pair]) + + labels_concat = [ + torch.cat([labels_dict["ab"], labels_dict["aa"]], dim=2).reshape((batch_size, num_samples, -1)), + torch.cat([labels_dict["ba"], labels_dict["bb"]], dim=2).reshape((batch_size, num_samples, -1)), + ] + + num_positives = [torch.sum(label_concat, dim=-1, keepdim=True) for label_concat in labels_concat] + + labels_concat = [ + label_concat / torch.maximum(num_positive, torch.tensor(1.0, device=num_positive.device)) + for label_concat, num_positive in zip(labels_concat, num_positives) + ] + + obj_areas = [torch.sum(make_same_obj(pind, pind), dim=(2, 3)) for pind in [pind1, pind2]] + + weights = [ + torch.greater(num_positive[..., 0], 1e-3).to(torch.float32) / obj_area + for num_positive, obj_area in zip(num_positives, obj_areas) + ] + + logits_concat = [ + torch.cat([logits_dict["ab"], logits_dict["aa"]], dim=2).reshape((batch_size, num_samples, -1)), + torch.cat([logits_dict["ba"], logits_dict["bb"]], dim=2).reshape((batch_size, num_samples, -1)), + ] + + loss_a = manual_cross_entropy(logits_concat[0], labels_concat[0], weight=weights[0]) + loss_b = manual_cross_entropy(logits_concat[1], labels_concat[1], weight=weights[1]) + loss = loss_a + loss_b + + return loss diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/pixel_prototype_ce_loss.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/pixel_prototype_ce_loss.py new file mode 100644 index 00000000000..8676e785053 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/losses/pixel_prototype_ce_loss.py @@ -0,0 +1,101 @@ +"""Pixel Prototype Cross Entropy Loss.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from __future__ import absolute_import, division, print_function + +from abc import ABC + +import torch +import torch.nn.functional as F +from mmseg.models.builder import LOSSES +from torch import nn + +from otx.algorithms.segmentation.adapters.mmseg.models.losses import CrossEntropyLossWithIgnore + + +class PPC(nn.Module, ABC): + """Pixel-prototype contrastive loss.""" + + def __init__(self): + super(PPC, self).__init__() + + def forward(self, contrast_logits, contrast_target): + """Forward function.""" + loss_ppc = F.cross_entropy(contrast_logits, contrast_target.long(), ignore_index=255) + return loss_ppc + + +class PPD(nn.Module, ABC): + """Pixel-prototype distance loss.""" + + def __init__(self): + super(PPD, self).__init__() + self.ignore_label = 255 + + def forward(self, contrast_logits, contrast_target): + """Forward function.""" + contrast_logits = contrast_logits[contrast_target != self.ignore_label, :] + contrast_target = contrast_target[contrast_target != self.ignore_label] + + logits = torch.gather(contrast_logits, 1, contrast_target[:, None].long()) + loss_ppd = (1 - logits).pow(2).mean() + + return loss_ppd + + +@LOSSES.register_module() +class PixelPrototypeCELoss(nn.Module, ABC): + """Prototype based loss. + + Computes cross entropy loss beetwen computeted angles (prototypes and pixel embedings) and target. + Includes pixel-prototype contrastive Learning and pixel-prototype distance optimization + Args: + loss_ppc_weight (float): weight for pixel-prototype contrastive loss. Default: 0.001 + loss_ppd_weight (float): weight for pixel-prototype distance loss. Default: 0.01 + ignore_index (int): index to ignore. Default: 255 + ignore_mode (bool): ignore mode, used for class incremental learning. Default: False + """ + + def __init__(self, loss_ppc_weight=0.01, loss_ppd_weight=0.001, ignore_index=255, ignore_mode=False, **kwargs): + super(PixelPrototypeCELoss, self).__init__() + self._loss_name = "pixel_proto_ce_loss" + ignore_index = ignore_index + self.loss_ppc_weight = loss_ppc_weight + self.loss_ppd_weight = loss_ppd_weight + if ignore_mode: + self.seg_criterion = CrossEntropyLossWithIgnore(**kwargs) + else: + self.seg_criterion = nn.CrossEntropyLoss(ignore_index=ignore_index) + + self.ppc_criterion = PPC() + self.ppd_criterion = PPD() + + def forward(self, seg_out, proto_logits, proto_targets, target): + """Forward function.""" + if self.loss_ppc_weight > 0: + loss_ppc = self.ppc_criterion(proto_logits, proto_targets) + else: + loss_ppc = 0 + if self.loss_ppd_weight > 0: + loss_ppd = self.ppd_criterion(proto_logits, proto_targets) + else: + loss_ppd = 0 + loss = self.seg_criterion(seg_out, target.squeeze(1).long()) + return loss + self.loss_ppc_weight * loss_ppc + self.loss_ppd_weight * loss_ppd + + @property + def loss_name(self): + """Loss Name. + + This function must be implemented and will return the name of this + loss function. This name will be used to combine different loss items + by simple sum operation. In addition, if you want this loss item to be + included into the backward graph, `loss_` must be the prefix of the + name. + + Returns: + str: The name of this loss item. + """ + return self._loss_name diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py new file mode 100644 index 00000000000..cf76dc5c172 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py @@ -0,0 +1,19 @@ +"""OTX Algorithms - Segmentation Necks.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .selfsl_mlp import SelfSLMLP + +__all__ = ["SelfSLMLP"] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/necks/selfsl_mlp.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/necks/selfsl_mlp.py new file mode 100644 index 00000000000..e7656efd150 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/necks/selfsl_mlp.py @@ -0,0 +1,105 @@ +"""Multi-layer Perceptron (MLP) for Self-supervised learning methods. + +This MLP consists of fc (conv) - norm - relu - fc (conv). +""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=dangerous-default-value +from typing import Any, Dict + +import torch +from mmcv.cnn import build_norm_layer, kaiming_init, normal_init +from mmseg.models.builder import NECKS # pylint: disable=no-name-in-module +from torch import nn + + +@NECKS.register_module() +class SelfSLMLP(nn.Module): + """The SelfSLMLP neck: fc/conv-bn-relu-fc/conv. + + Args: + in_channels (int): The number of feature output channels from backbone. + hid_channels (int): The number of channels for a hidden layer. + out_channels (int): The number of output channels of SelfSLMLP. + norm_cfg (dict): Normalize configuration. Default: dict(type="BN1d"). + use_conv (bool): Whether using conv instead of fc. Default: False. + with_avg_pool (bool): Whether using average pooling before passing MLP. + Default: True. + """ + + def __init__( + self, + in_channels: int, + hid_channels: int, + out_channels: int, + norm_cfg: Dict[str, Any] = dict(type="BN1d"), + use_conv: bool = False, + with_avg_pool: bool = True, + ): + super().__init__() + + self.with_avg_pool = with_avg_pool + if with_avg_pool: + self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) + + self.use_conv = use_conv + if use_conv: + self.mlp = nn.Sequential( + nn.Conv2d(in_channels, hid_channels, 1), + build_norm_layer(norm_cfg, hid_channels)[1], + nn.ReLU(inplace=True), + nn.Conv2d(hid_channels, out_channels, 1), + ) + else: + self.mlp = nn.Sequential( + nn.Linear(in_channels, hid_channels), + build_norm_layer(norm_cfg, hid_channels)[1], + nn.ReLU(inplace=True), + nn.Linear(hid_channels, out_channels), + ) + + def init_weights(self, init_linear: str = "normal", std: float = 0.01, bias: float = 0.0): + """Initialize SelfSLMLP weights. + + Args: + init_linear (str): Option to initialize weights. Default: "normal". + std (float): Standard deviation for normal initialization. Default: 0.01. + bias (float): Bias for normal initialization. Default: 0. + """ + if init_linear not in ["normal", "kaiming"]: + raise ValueError(f"Undefined init_linear: {init_linear}") + for m in self.modules(): # pylint: disable=invalid-name + if isinstance(m, nn.Linear): + if init_linear == "normal": + normal_init(m, std=std, bias=bias) + else: + kaiming_init(m, mode="fan_in", nonlinearity="relu") + elif isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.GroupNorm, nn.SyncBatchNorm)): + if m.weight is not None: + nn.init.constant_(m.weight, 1) + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, x): + """Forward SelfSLMLP. + + Args: + x (Tensor, tuple, list): Inputs to pass MLP. + If a type of the inputs is tuple or list, just use the last index. + + Return: + Tensor: Features passed SelfSLMLP. + """ + if isinstance(x, (tuple, list)): + # using last output + x = x[-1] + if not isinstance(x, torch.Tensor): + raise TypeError("neck inputs should be tuple or torch.tensor") + if self.with_avg_pool: + x = self.avgpool(x) + if self.use_conv: # pylint: disable=no-else-return + return self.mlp(x) + else: + return self.mlp(x.view(x.size(0), -1)) diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/__init__.py new file mode 100644 index 00000000000..47c10da0113 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/__init__.py @@ -0,0 +1,25 @@ +"""Scaler schedulers for semantic segmentation.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .constant import ConstantScalarScheduler +from .poly import PolyScalarScheduler +from .step import StepScalarScheduler + +__all__ = [ + "ConstantScalarScheduler", + "PolyScalarScheduler", + "StepScalarScheduler", +] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/base.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/base.py new file mode 100644 index 00000000000..600309d8f1d --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/base.py @@ -0,0 +1,18 @@ +"""Base scalar scheduler.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from abc import ABCMeta, abstractmethod + + +class BaseScalarScheduler(metaclass=ABCMeta): + """Base scalar scheduler.""" + + def __call__(self, step, epoch_size) -> float: + """Callback function of BaseScalarScheduler.""" + return self._get_value(step, epoch_size) + + @abstractmethod + def _get_value(self, step, epoch_size) -> float: + raise NotImplementedError("Subclass must implement this method") diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/constant.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/constant.py new file mode 100644 index 00000000000..f7819d18ce5 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/constant.py @@ -0,0 +1,28 @@ +"""Constant scheduler.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import SCALAR_SCHEDULERS + +from .base import BaseScalarScheduler + + +@SCALAR_SCHEDULERS.register_module() +class ConstantScalarScheduler(BaseScalarScheduler): + """The learning rate remains constant over time. + + The learning rate equals the scale. + + Args: + scale (float): The learning rate scale. + """ + + def __init__(self, scale: float = 30.0): + super().__init__() + + self._end_s = scale + assert self._end_s > 0.0 + + def _get_value(self, step, epoch_size): + return self._end_s diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/poly.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/poly.py new file mode 100644 index 00000000000..c4f79b1bddd --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/poly.py @@ -0,0 +1,66 @@ +"""Polynomial scheduler.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np + +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import SCALAR_SCHEDULERS + +from .base import BaseScalarScheduler + + +@SCALAR_SCHEDULERS.register_module() +class PolyScalarScheduler(BaseScalarScheduler): + """The learning rate changes over time according to a polynomial schedule. + + Args: + start_scale (float): The initial learning rate scale. + end_scale (float): The final learning rate scale. + num_iters (int): The number of iterations to reach the final learning rate. + power (float): The power of the polynomial schedule. + by_epoch (bool): Whether to use epoch as the unit of iteration. + """ + + def __init__( + self, + start_scale: float, + end_scale: float, + num_iters: int, + power: float = 1.2, + by_epoch: bool = False, + ): + super().__init__() + + self._start_s = start_scale + assert self._start_s >= 0.0 + self._end_s = end_scale + assert self._end_s >= 0.0 + self._num_iters = num_iters + assert self._num_iters >= 0 + self._power = power + assert self._power >= 0.0 + self.by_epoch = by_epoch + + def _get_value(self, step, epoch_size): + if step is None: + return float(self._end_s) + + if self.by_epoch: + num_iters = epoch_size * self._num_iters + else: + num_iters = self._num_iters + if num_iters == 0: + return self._end_s + + if step < num_iters: + factor = (self._end_s - self._start_s) / (1.0 - self._power) + var_a = factor / (num_iters**self._power) + var_b = -factor * self._power / float(num_iters) + + out_value = var_a * np.power(step, self._power) + var_b * step + self._start_s + else: + out_value = self._end_s + + return out_value diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/step.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/step.py new file mode 100644 index 00000000000..19c4f81563d --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/step.py @@ -0,0 +1,57 @@ +"""Step scheduler.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import List + +import numpy as np + +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import SCALAR_SCHEDULERS + +from .base import BaseScalarScheduler + + +@SCALAR_SCHEDULERS.register_module() +class StepScalarScheduler(BaseScalarScheduler): + """Step learning rate scheduler. + + Example: + >>> scheduler = StepScalarScheduler(scales=[1.0, 0.1, 0.01], num_iters=[100, 200]) + This means that the learning rate will be 1.0 for the first 100 iterations, + 0.1 for the next 200 iterations, and 0.01 for the rest of the iterations. + + Args: + scales (List[int]): List of learning rate scales. + num_iters (List[int]): A list specifying the count of iterations at each scale. + by_epoch (bool): Whether to use epoch as the unit of iteration. + """ + + def __init__(self, scales: List[float], num_iters: List[int], by_epoch: bool = False): + super().__init__() + + self.by_epoch = by_epoch + + assert len(scales) == len(num_iters) + 1 + assert len(scales) > 0 + + self._scales = list(scales) + self._iter_ranges = list(num_iters) + [np.iinfo(np.int32).max] + + def _get_value(self, step, epoch_size) -> float: + if step is None: + return float(self._scales[-1]) + + out_scale_idx = 0 + for iter_range in self._iter_ranges: + if self.by_epoch: + iter_threshold = epoch_size * iter_range + else: + iter_threshold = iter_range + + if step < iter_threshold: + break + + out_scale_idx += 1 + + return float(self._scales[out_scale_idx]) diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py new file mode 100644 index 00000000000..5e3af6f2a9e --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py @@ -0,0 +1,20 @@ +"""OTX Algorithms - Segmentation Segmentors.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .detcon import DetConB, SupConDetConB +from .mean_teacher_segmentor import MeanTeacherSegmentor + +__all__ = ["DetConB", "SupConDetConB", "MeanTeacherSegmentor"] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py new file mode 100644 index 00000000000..46e119d9a9c --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py @@ -0,0 +1,559 @@ +"""DetCon implementation for self-supervised learning. + +Original papers: +- 'Efficient Visual Pretraining with Contrastive Detection', https://arxiv.org/abs/2103.10957 +""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=unused-argument, invalid-name, unnecessary-pass, not-callable + +from collections import OrderedDict +from typing import Any, Dict, List, Optional, Tuple, Union + +import torch +import torch.distributed as dist +from mmcv.runner import load_checkpoint +from mmseg.models.builder import ( # pylint: disable=no-name-in-module + SEGMENTORS, + build_backbone, + build_head, + build_neck, +) +from mmseg.ops import resize +from torch import nn + +from otx.utils.logger import get_logger + +from .otx_encoder_decoder import OTXEncoderDecoder + +logger = get_logger() + + +class MaskPooling(nn.Module): + """Mask pooling module to filter each class with the same class. + + Args: + num_classes (int): The number of classes to be considered as pseudo classes. Default: 256. + num_samples (int): The number of samples to be sampled. Default: 16. + downsample (int): The ratio of the mask size to the feature size. Default: 32. + replacement (bool): Whether samples are drawn with replacement or not. + It can be used when `num_classes` is small rather than `num_samples`. Default: True. + """ + + def __init__( + self, + num_classes: int = 256, + num_samples: int = 16, + downsample: int = 32, + replacement: bool = True, + ): + + super().__init__() + + self.num_classes = num_classes + self.num_samples = num_samples + self.replacement = replacement + + self.mask_ids = torch.arange(num_classes) + self.pool = nn.AvgPool2d(kernel_size=downsample, stride=downsample) + + def pool_masks(self, masks: torch.Tensor): + """Perform mask pooling and create binary masks. + + Args: + masks (Tensor): Ground truth masks. + + Returns: + Tensor: Pooled binary masks. + """ + if masks.ndim < 4: + masks = masks.unsqueeze(dim=1) + + masks = masks == self.mask_ids[None, :, None, None].to(masks.device) + masks = self.pool(masks.to(torch.float)) + + b, c, h, w = masks.shape + masks = torch.reshape(masks, (b, c, h * w)) + masks = torch.argmax(masks, dim=1) + masks = torch.eye(self.num_classes).to(masks.device)[masks] + masks = torch.transpose(masks, 1, 2) + + return masks + + def sample_masks(self, masks: torch.Tensor): + """Samples of which binary masks to use in the loss. + + Args: + masks (Tensor): Pooled binary masks from `self.pool_masks`. + + Returns: + tuple[Tensor, Tensor]: (sampled_masks, mask_ids), + sampled binary masks and ids used to sample masks. + """ + assert masks.ndim == 3 + + batch_size = masks.shape[0] + mask_exists = torch.greater(masks.sum(dim=-1), 1e-3) + sel_masks = mask_exists.to(torch.float) + 1e-11 + + mask_ids = torch.multinomial(sel_masks, num_samples=self.num_samples, replacement=self.replacement) + sampled_masks = torch.stack([masks[b][mask_ids[b]] for b in range(batch_size)]) + + return sampled_masks, mask_ids + + def forward(self, masks: torch.Tensor): + """Forward function for mask pooling. + + Args: + masks (Tensor): Ground truth masks to be sampled. + + Returns: + tuple[Tensor, Tensor]: (sampled_masks, sampled_mask_ids), + normalized sampled binary masks and ids used to sample masks. + """ + binary_masks = self.pool_masks(masks) + sampled_masks, sampled_mask_ids = self.sample_masks(binary_masks) + areas = sampled_masks.sum(dim=-1, keepdim=True) + sampled_masks = sampled_masks / torch.maximum(areas, torch.tensor(1.0, device=areas.device)) + + return sampled_masks, sampled_mask_ids + + +# pylint: disable=too-many-arguments, dangerous-default-value, too-many-instance-attributes +@SEGMENTORS.register_module() +class DetConB(nn.Module): + """DetCon Implementation. + + Implementation of 'Efficient Visual Pretraining with Contrastive Detection' + (https://arxiv.org/abs/2103.10957). + + Args: + backbone (dict): Config dict for module of backbone ConvNet. + neck (dict, optional): Config dict for module of deep features to compact feature vectors. + Default: None. + head (dict, optional): Config dict for module of loss functions. Default: None. + pretrained (str, optional): Path to pre-trained weights. Default: None. + base_momentum (float): The base momentum coefficient for the target network. + Default: 0.996. + num_classes (int): The number of classes to be considered as pseudo classes. Default: 256. + num_samples (int): The number of samples to be sampled. Default: 16. + downsample (int): The ratio of the mask size to the feature size. Default: 32. + input_transform (str): Input transform of features from backbone. Default: "resize_concat". + in_index (list): Feature index to be used for DetCon if the backbone outputs + multi-scale features wrapped by list or tuple. Default: [0]. + align_corners (bool): Whether apply `align_corners` during resize. Default: False. + """ + + def __init__( + self, + backbone: Dict[str, Any], + neck: Optional[Dict[str, Any]] = None, + head: Optional[Dict[str, Any]] = None, + pretrained: Optional[str] = None, + base_momentum: float = 0.996, + num_classes: int = 256, + num_samples: int = 16, + downsample: int = 32, + input_transform: str = "resize_concat", + in_index: Union[List[int], int] = [0], + align_corners: bool = False, + **kwargs, + ): + super().__init__() + + self.base_momentum = base_momentum + self.momentum = base_momentum + self.num_classes = num_classes + self.num_samples = num_samples + self.downsample = downsample + self.input_transform = input_transform + self.in_index = in_index + self.align_corners = align_corners + + # build backbone + self.online_backbone = build_backbone(backbone) + self.target_backbone = build_backbone(backbone) + + # build projector + self.online_projector = build_neck(neck) + self.target_projector = build_neck(neck) + + # build head with predictor + self.predictor = build_head(head) + + # set maskpooling + self.mask_pool = MaskPooling(num_classes, num_samples, downsample) + + self.init_weights(pretrained=pretrained) + + # Hooks for super_type transparent weight save + self._register_state_dict_hook(self.state_dict_hook) + + def init_weights(self, pretrained: Optional[str] = None): + """Initialize the weights of model. + + Args: + pretrained (str, optional): Path to pre-trained weights. + Default: None. + """ + if pretrained is not None: + logger.info(f"load model from: {pretrained}") + load_checkpoint( + self.online_backbone, + pretrained, + strict=False, + map_location=None, + logger=logger, + revise_keys=[(r"^backbone\.", "")], + ) + + # init backbone + for param_ol, param_tgt in zip(self.online_backbone.parameters(), self.target_backbone.parameters()): + param_tgt.data.copy_(param_ol.data) + param_tgt.requires_grad = False + param_ol.requires_grad = True + + # init projector + self.online_projector.init_weights(init_linear="kaiming") + for param_ol, param_tgt in zip(self.online_projector.parameters(), self.target_projector.parameters()): + param_tgt.data.copy_(param_ol.data) + param_tgt.requires_grad = False + param_ol.requires_grad = True + + # init the predictor + self.predictor.init_weights() + + @torch.no_grad() + def _momentum_update(self): + """Momentum update of the target network.""" + for param_ol, param_tgt in zip(self.online_backbone.parameters(), self.target_backbone.parameters()): + param_tgt.data = param_tgt.data * self.momentum + param_ol.data * (1.0 - self.momentum) + + for param_ol, param_tgt in zip(self.online_projector.parameters(), self.target_projector.parameters()): + param_tgt.data = param_tgt.data * self.momentum + param_ol.data * (1.0 - self.momentum) + + def transform_inputs(self, inputs: Union[List, Tuple]): + """Transform inputs for decoder. + + Args: + inputs (list, tuple): List (or tuple) of multi-level img features. + + Returns: + Tensor: The transformed inputs. + """ + # TODO (sungchul): consider tensor component, too + if self.input_transform == "resize_concat" and isinstance(self.in_index, (list, tuple)): + inputs = [inputs[i] for i in self.in_index] + upsampled_inputs = [ + resize( + input=x, + size=inputs[0].shape[2:], + mode="bilinear", + align_corners=self.align_corners, + ) + for x in inputs + ] + inputs = torch.cat(upsampled_inputs, dim=1) + elif self.input_transform == "multiple_select" and isinstance(self.in_index, (list, tuple)): + inputs = [inputs[i] for i in self.in_index] + else: + if isinstance(self.in_index, (list, tuple)): + self.in_index = self.in_index[0] + inputs = inputs[self.in_index] # type: ignore + + return inputs + + def extract_feat(self, img: torch.Tensor): + """Extract features from images. + + Args: + img (Tensor): Input image. + + Return: + Tensor: Features from the online_backbone. + """ + x = self.online_backbone(img) + return x + + def sample_masked_feats( + self, + feats: Union[torch.Tensor, List, Tuple], + masks: torch.Tensor, + projector: nn.Module, + ): + """Sampled features from mask. + + Args: + feats (list, tuple, Tensor): Features from the backbone. + masks (Tensor): Ground truth masks to be sampled and to be used to filter `feats`. + projector (nn.Module): Projector MLP. + + Returns: + tuple[Tensor, Tensor]: (proj, sampled_mask_ids), features from the projector and ids used to sample masks. + """ + if isinstance(feats, (list, tuple)) and len(feats) > 1: + feats = self.transform_inputs(feats) + + # TODO (sungchul): consider self.input_transform == "multiple_select" + sampled_masks, sampled_mask_ids = self.mask_pool(masks) + + b, c, h, w = feats.shape # type: ignore + feats = feats.reshape((b, c, h * w)).transpose(1, 2) # type: ignore + sampled_feats = sampled_masks @ feats + sampled_feats = sampled_feats.reshape((-1, c)) + + proj = projector(sampled_feats) + + return proj, sampled_mask_ids + + # pylint: disable=too-many-locals + def forward_train( + self, + img: torch.Tensor, + img_metas: List[Dict], + gt_semantic_seg: torch.Tensor, + return_embedding: bool = False, + ): + """Forward function for training. + + Args: + img (Tensor): Input images. + img_metas (list[dict]): Input information. + gt_semantic_seg (Tensor): Pseudo masks. + It is used to organize features among the same classes. + return_embedding (bool): Whether returning embeddings from the online backbone. + It can be used for SupCon. Default: False. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + assert img.ndim == 5 and gt_semantic_seg.ndim == 5 + batch_size = img.shape[0] + imgs = torch.cat((img[:, 0], img[:, 1]), dim=0) + masks = torch.cat((gt_semantic_seg[:, :, 0], gt_semantic_seg[:, :, 1]), dim=0) + + embds = self.online_backbone(imgs) + projs, ids = self.sample_masked_feats(embds, masks, self.online_projector) + + with torch.no_grad(): + self._momentum_update() + projs_tgt, ids_tgt = self.sample_masked_feats(self.target_backbone(imgs), masks, self.target_projector) + + # predictor + loss = self.predictor(projs, projs_tgt, ids, ids_tgt, batch_size, self.num_samples) + + if return_embedding: + return loss, embds, masks + return loss + + def forward(self, img, img_metas, return_loss=True, **kwargs): + """Calls either :func:`forward_train` or :func:`forward_test` depending on whether ``return_loss`` is ``True``. + + Note this setting will change the expected inputs. When + ``return_loss=True``, img and img_meta are single-nested (i.e. Tensor + and List[dict]), and when ``resturn_loss=False``, img and img_meta + should be double nested (i.e. List[Tensor], List[List[dict]]), with + the outer list indicating test time augmentations. + """ + if return_loss: + return self.forward_train(img, img_metas, **kwargs) + raise AttributeError("Self-SL doesn't support `forward_test` for evaluation.") + + def train_step( + self, + data_batch: Dict[str, Any], + optimizer: Union[torch.optim.Optimizer, Dict], + **kwargs, + ): + """The iteration step during training. + + This method defines an iteration step during training, except for the + back propagation and optimizer updating, which are done in an optimizer + hook. Note that in some complicated cases or models, the whole process + including back propagation and optimizer updating is also defined in + this method, such as GAN. + + Args: + data_batch (dict): The output of dataloader. + optimizer (:obj:`torch.optim.Optimizer` | dict): The optimizer of + runner is passed to ``train_step()``. This argument is unused + and reserved. + **kwargs (Any): Addition keyword arguments. + + Returns: + dict: It should contain at least 3 keys: ``loss``, ``log_vars``, + ``num_samples``. + ``loss`` is a tensor for back propagation, which can be a + weighted sum of multiple losses. + ``log_vars`` contains all the variables to be sent to the + logger. + ``num_samples`` indicates the batch size (when the model is + DDP, it means the batch size on each GPU), which is used for + averaging the logs. + """ + losses = self(**data_batch) + loss, log_vars = self._parse_losses(losses) + + outputs = dict(loss=loss, log_vars=log_vars, num_samples=len(data_batch["img_metas"])) + + return outputs + + def val_step(self, **kwargs): + """Disenable validation step during self-supervised learning.""" + pass + + def _parse_losses(self, losses: Dict[str, Any]): + """Parse the raw outputs (losses) of the network. + + Args: + losses (dict): Raw output of the network, which usually contain + losses and other necessary information. + + Returns: + tuple[Tensor, dict]: (loss, log_vars), loss is the loss tensor + which may be a weighted sum of all losses, log_vars contains + all the variables to be sent to the logger. + """ + log_vars = OrderedDict() + for var_name, var_value in losses.items(): + if isinstance(var_value, torch.Tensor): + log_vars[var_name] = var_value.mean() + elif isinstance(var_value, list): + log_vars[var_name] = sum(_loss.mean() for _loss in var_value) + elif isinstance(var_value, (int, float)): + log_vars[var_name] = var_value + else: + raise TypeError(f"{var_name} is not a tensor or list of tensors") + + loss = sum(_value for _key, _value in log_vars.items() if "loss" in _key) + + log_vars["loss"] = loss + for var_name, var_value in log_vars.items(): + if isinstance(var_value, (int, float)): + continue + + # reduce loss when distributed training + if dist.is_available() and dist.is_initialized(): + var_value = var_value.data.clone() + dist.all_reduce(var_value.div_(dist.get_world_size())) + + log_vars[var_name] = var_value.item() + + return loss, log_vars + + def set_step_params(self, init_iter, epoch_size): + """`set_step_params` to be skipped.""" + pass + + @staticmethod + def state_dict_hook(module, state_dict, *args, **kwargs): + """Save only online backbone as output state_dict.""" + logger.info("----------------- BYOL.state_dict_hook() called") + output = OrderedDict() + for k, v in state_dict.items(): + if "online_backbone." in k: + k = k.replace("online_backbone.", "backbone.") + output[k] = v + return output + + +# pylint: disable=too-many-locals +@SEGMENTORS.register_module() +class SupConDetConB(OTXEncoderDecoder): # pylint: disable=too-many-ancestors + """Apply DetConB as a contrastive part of `Supervised Contrastive Learning` (https://arxiv.org/abs/2004.11362). + + SupCon with DetConB uses ground truth masks instead of pseudo masks to organize features among the same classes. + + Args: + decode_head (dict, optional): Config dict for module of decode head. Default: None. + train_cfg (dict, optional): Config dict for training. Default: None. + """ + + def __init__( + self, + backbone: Dict[str, Any], + decode_head: Optional[Dict[str, Any]] = None, + neck: Optional[Dict[str, Any]] = None, + head: Optional[Dict[str, Any]] = None, + pretrained: Optional[str] = None, + base_momentum: float = 0.996, + num_classes: int = 256, + num_samples: int = 16, + downsample: int = 32, + input_transform: str = "resize_concat", + in_index: Union[List[int], int] = [0], + align_corners: bool = False, + train_cfg: Optional[Dict[str, Any]] = None, + test_cfg: Optional[Dict[str, Any]] = None, + **kwargs, + ): + super().__init__( + backbone=backbone, + decode_head=decode_head, + train_cfg=train_cfg, + test_cfg=test_cfg, + **kwargs, + ) + + self.detconb = DetConB( + backbone=backbone, + neck=neck, + head=head, + pretrained=pretrained, + base_momentum=base_momentum, + num_classes=num_classes, + num_samples=num_samples, + downsample=downsample, + input_transform=input_transform, + in_index=in_index, + align_corners=align_corners, + **kwargs, + ) + self.backbone = self.detconb.online_backbone + # TODO (sungchul): Is state_dict_hook needed to save segmentor only? + # 1. use state_dict_hook : we can save memory as only saving backbone + decode_head. + # 2. save all : we can use additional training with the whole weights (backbone + decode_head + detcon). + + # pylint: disable=arguments-renamed + def forward_train( + self, + img, + img_metas, + gt_semantic_seg, + **kwargs, + ): + """Forward function for training. + + Args: + img (Tensor): Input images. + img_metas (list[dict]): Input information. + gt_semantic_seg (Tensor): Ground truth masks. + It is used to organize features among the same classes. + **kwargs (Any): Addition keyword arguments. + + Returns: + dict[str, Tensor]: A dictionary of loss components. + """ + losses = {} + if img.ndim == 4: + # supervised learning with interval + embds = self.detconb.online_backbone(img) + masks = gt_semantic_seg + else: + # supcon training + loss_detcon, embds, masks = self.detconb.forward_train( + img=img, + img_metas=img_metas, + gt_semantic_seg=gt_semantic_seg, + return_embedding=True, + ) + losses.update(dict(loss_detcon=loss_detcon["loss"])) + img_metas += img_metas + + # decode head + loss_decode = self._decode_head_forward_train(embds, img_metas, gt_semantic_seg=masks) + losses.update(loss_decode) + + return losses diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/mean_teacher_segmentor.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/mean_teacher_segmentor.py new file mode 100644 index 00000000000..01bafd40ec9 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/mean_teacher_segmentor.py @@ -0,0 +1,246 @@ +"""Mean teacher segmentor for semi-supervised learning.""" + +import functools + +import numpy as np +import torch +from mmseg.models import SEGMENTORS, build_segmentor +from mmseg.models.segmentors.base import BaseSegmentor +from mmseg.ops import resize + +from otx.algorithms.segmentation.adapters.mmseg.models.heads.proto_head import ProtoNet +from otx.utils.logger import get_logger + +logger = get_logger() + +# pylint: disable=too-many-locals, protected-access + + +@SEGMENTORS.register_module() +class MeanTeacherSegmentor(BaseSegmentor): + """Mean teacher segmentor for semi-supervised learning. + + It creates two models and ema from one to the other for consistency loss. + + Args: + orig_type (BaseSegmentor): original type of segmentor to build student and teacher models + num_iters_per_epoch (int): number of iterations per training epoch. + unsup_weight (float): loss weight for unsupervised part. Default: 0.1 + proto_weight (float): loss weight for pixel prototype cross entropy loss. Default: 0.7 + drop_unrel_pixels_percent (int): starting precentage of pixels with high entropy + to drop from teachers pseudo labels. Default: 20 + semisl_start_epoch (int): epoch to start learning with unlabeled images. Default: 1 + proto_head (dict): configuration to constract prototype network. Default: None + """ + + def __init__( + self, + orig_type, + num_iters_per_epoch=None, + unsup_weight=0.1, + proto_weight=0.7, + drop_unrel_pixels_percent=20, + semisl_start_epoch=1, + proto_head=None, + **kwargs + ): + super().__init__() + self.test_cfg = kwargs["test_cfg"] + self.count_iter = 0 + # num_iters_per_epoch will be None during validation + # Overwise it should be overwritten in train_task + if num_iters_per_epoch is not None: + # filter unreliable pixels during first 100 epochs + self.filter_pixels_iters = num_iters_per_epoch * 100 + self.semisl_start_iter = num_iters_per_epoch * semisl_start_epoch + self.drop_unrel_pixels_percent = drop_unrel_pixels_percent + cfg = kwargs.copy() + cfg["type"] = orig_type + self.align_corners = cfg["decode_head"].get("align_corners", False) + self.model_s = build_segmentor(cfg) + self.model_t = build_segmentor(cfg) + self.use_prototype_head = False + self.unsup_weight = unsup_weight + if proto_head is not None and hasattr(self.model_s.decode_head, "_forward_feature"): + self.proto_net = ProtoNet(num_classes=self.model_s.decode_head.num_classes, **proto_head) + self.use_prototype_head = True + elif proto_head is not None: + logger.warning( + "Prototype head isn't supported by this model. " + "_forward_feature() method is required to be presented in the main decode head. " + "This function will be disabled and standard Mean Teacher algorithm will be utilized" + ) + self.proto_weight = proto_weight + self.losses = dict() + # Hooks for super_type transparent weight load/save + self._register_state_dict_hook(self.state_dict_hook) + self._register_load_state_dict_pre_hook(functools.partial(self.load_state_dict_pre_hook, self)) + + def encode_decode(self, img, img_metas): + """Encode and decode images.""" + return self.model_s.encode_decode(img, img_metas) + + def decode_proto_network( + self, sup_input, gt_semantic_seg, unsup_input=None, pl_from_teacher=None, reweight_unsup=1.0 + ): + """Forward prototype network, compute proto loss. + + If there is no unsupervised part, only supervised loss will be computed. + + Args: + sup_input (torch.Tensor): student output from labeled images + gt_semantic_seg (torch.Tensor): ground truth semantic segmentation label maps + unsup_input (torch.Tensor): student output from unlabeled images. Default: None + pl_from_teacher (torch.Tensor): teacher generated pseudo labels. Default: None + reweight_unsup (float): reweighting coefficient for unsupervised part after + filtering high entropy pixels. Default: 1.0 + """ + + # supervised branch + head_features_sup = self.model_s.decode_head._forward_feature(sup_input) + proto_out_supervised = self.proto_net(head_features_sup, gt_semantic_seg) + loss_proto = self.proto_net.losses(**proto_out_supervised, seg_label=gt_semantic_seg) + self._update_summary_loss(loss_proto, loss_weight=self.proto_weight) + # unsupervised branch + if unsup_input is not None and pl_from_teacher is not None: + head_features_unsup = self.model_s.decode_head._forward_feature(unsup_input) + proto_out_unsupervised = self.proto_net(head_features_unsup, pl_from_teacher) + loss_proto_u = self.proto_net.losses(**proto_out_unsupervised, seg_label=pl_from_teacher) + self._update_summary_loss(loss_proto_u, loss_weight=self.unsup_weight * reweight_unsup * self.proto_weight) + + def generate_pseudo_labels(self, ul_w_img, ul_img_metas): + """Generate pseudo labels from teacher model, apply filter loss method. + + Args: + ul_w_img (torch.Tensor): weakly augmented unlabeled images + ul_img_metas (dict): unlabeled images meta data + + """ + + with torch.no_grad(): + teacher_feat = self.model_t.extract_feat(ul_w_img) + teacher_out = self.model_t._decode_head_forward_test(teacher_feat, ul_img_metas) + teacher_out = resize( + input=teacher_out, size=ul_w_img.shape[2:], mode="bilinear", align_corners=self.align_corners + ) + teacher_prob_unsup = torch.softmax(teacher_out, axis=1) + _, pl_from_teacher = torch.max(teacher_prob_unsup, axis=1, keepdim=True) + + # drop pixels with high entropy + percent_unreliable = self.drop_unrel_pixels_percent * (1 - self.count_iter / self.filter_pixels_iters) + reweight_unsup = 1.0 + if percent_unreliable > 0: + keep_percent = 100 - percent_unreliable + batch_size, _, h, w = teacher_out.shape + + entropy = -torch.sum(teacher_prob_unsup * torch.log(teacher_prob_unsup + 1e-10), dim=1, keepdim=True) + + thresh = np.percentile(entropy[pl_from_teacher != 255].detach().cpu().numpy().flatten(), keep_percent) + thresh_mask = entropy.ge(thresh).bool() * (pl_from_teacher != 255).bool() + + pl_from_teacher[thresh_mask] = 255 + # reweight unsupervised loss + reweight_unsup = batch_size * h * w / torch.sum(pl_from_teacher != 255) + + return pl_from_teacher, reweight_unsup + + def extract_feat(self, imgs): + """Extract feature.""" + return self.model_s.extract_feat(imgs) + + def simple_test(self, img, img_meta, **kwargs): + """Simple test.""" + return self.model_s.simple_test(img, img_meta, **kwargs) + + def aug_test(self, imgs, img_metas, **kwargs): + """Aug test.""" + return self.model_s.aug_test(imgs, img_metas, **kwargs) + + def forward_dummy(self, img, **kwargs): + """Forward dummy.""" + return self.model_s.forward_dummy(img, **kwargs) + + def forward_train(self, img, img_metas, gt_semantic_seg, **kwargs): + """Forward train. + + Args: + img (torch.Tensor): labeled images + img_metas (dict): labeled images meta data + gt_semantic_seg (torch.Tensor): semantic segmentation label maps + kwargs (dict): key arguments with unlabeled components and additional information + """ + self.count_iter += 1 + self.losses["sum_loss"] = 0.0 + if self.semisl_start_iter >= self.count_iter or "extra_0" not in kwargs: + x = self.model_s.extract_feat(img) + loss_decode = self.model_s._decode_head_forward_train(x, img_metas, gt_semantic_seg=gt_semantic_seg) + self._update_summary_loss(loss_decode) + if self.use_prototype_head: + self.decode_proto_network(x, gt_semantic_seg) + + # add information about accuracy + for key in loss_decode: + if "acc" in key and loss_decode[key] is not None: + self.losses["decode_acc"] = loss_decode[key] + + return self.losses + + # + unsupervised part + ul_data = kwargs["extra_0"] + ul_s_img = ul_data["img"] # strongly augmented + ul_w_img = ul_data["ul_w_img"] # weakly augmented + ul_img_metas = ul_data["img_metas"] + + # generate pseudo labels, filter high entropy pixels, compute loss reweight + pl_from_teacher, reweight_unsup = self.generate_pseudo_labels(ul_w_img, ul_img_metas) + + # extract features from labeled and unlabeled augmented images + x = self.model_s.extract_feat(img) + x_u = self.model_s.extract_feat(ul_s_img) + loss_decode = self.model_s._decode_head_forward_train(x, img_metas, gt_semantic_seg=gt_semantic_seg) + loss_decode_u = self.model_s._decode_head_forward_train(x_u, ul_img_metas, gt_semantic_seg=pl_from_teacher) + self._update_summary_loss(loss_decode) + self._update_summary_loss(loss_decode_u, loss_weight=self.unsup_weight * reweight_unsup) + + if self.use_prototype_head: + # for proto head we need to derive head features + self.decode_proto_network(x, gt_semantic_seg, x_u, pl_from_teacher, reweight_unsup) + + # add information about accuracy + for key in loss_decode: + if "acc" in key and loss_decode[key] is not None: + self.losses["decode_acc"] = loss_decode[key] + self.losses["decode_acc_unsup"] = loss_decode_u[key] + + return self.losses + + def _update_summary_loss(self, decode_loss, loss_weight=1.0): + for name, value in decode_loss.items(): + if value is None or "loss" not in name: + continue + self.losses["sum_loss"] += value * loss_weight + + @staticmethod + def state_dict_hook(module, state_dict, prefix, *args, **kwargs): # pylint: disable=unused-argument + """Redirect student model as output state_dict (teacher as auxilliary).""" + logger.info("----------------- MeanTeacherSegmentor.state_dict_hook() called") + for key in list(state_dict.keys()): + value = state_dict.pop(key) + if not prefix or key.startswith(prefix): + key = key.replace(prefix, "", 1) + if key.startswith("model_s."): + key = key.replace("model_s.", "", 1) + elif key.startswith("model_t."): + continue + key = prefix + key + state_dict[key] = value + return state_dict + + @staticmethod + def load_state_dict_pre_hook(module, state_dict, *args, **kwargs): # pylint: disable=unused-argument + """Redirect input state_dict to teacher model.""" + logger.info("----------------- MeanTeacherSegmentor.load_state_dict_pre_hook() called") + for key in list(state_dict.keys()): + value = state_dict.pop(key) + state_dict["model_s." + key] = value + state_dict["model_t." + key] = value diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/otx_encoder_decoder.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/otx_encoder_decoder.py new file mode 100644 index 00000000000..14b5cfa6bad --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/otx_encoder_decoder.py @@ -0,0 +1,118 @@ +"""OTX encoder decoder for semantic segmentation.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import functools + +import torch +from mmseg.models import SEGMENTORS +from mmseg.models.segmentors.encoder_decoder import EncoderDecoder + +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=unused-argument, line-too-long +@SEGMENTORS.register_module() +class OTXEncoderDecoder(EncoderDecoder): + """OTX encoder decoder.""" + + def __init__(self, task_adapt=None, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Hook for class-sensitive weight loading + assert task_adapt is not None, "When using task_adapt, task_adapt must be set." + + self._register_load_state_dict_pre_hook( + functools.partial( + self.load_state_dict_pre_hook, + self, # model + task_adapt["dst_classes"], # model_classes + task_adapt["src_classes"], # chkpt_classes + ) + ) + + def simple_test(self, img, img_meta, rescale=True, output_logits=False): + """Simple test with single image.""" + seg_logit = self.inference(img, img_meta, rescale) + if output_logits: + seg_pred = seg_logit + elif self.out_channels == 1: + seg_pred = (seg_logit > self.decode_head.threshold).to(seg_logit).squeeze(1) + else: + seg_pred = seg_logit.argmax(dim=1) + if torch.onnx.is_in_onnx_export(): + # our inference backend only support 4D output + if seg_pred.dim() != 4: + seg_pred = seg_pred.unsqueeze(0) + return seg_pred + seg_pred = seg_pred.cpu().numpy() + seg_pred = list(seg_pred) + return seg_pred + + @staticmethod + def load_state_dict_pre_hook( + model, model_classes, chkpt_classes, chkpt_dict, prefix, *args, **kwargs + ): # pylint: disable=too-many-locals, unused-argument + """Modify input state_dict according to class name matching before weight loading.""" + logger.info(f"----------------- OTXEncoderDecoder.load_state_dict_pre_hook() called w/ prefix: {prefix}") + + # Dst to src mapping index + model_classes = list(model_classes) + chkpt_classes = list(chkpt_classes) + model2chkpt = map_class_names(model_classes, chkpt_classes) + logger.info(f"{chkpt_classes} -> {model_classes} ({model2chkpt})") + + model_dict = model.state_dict() + param_names = [ + "decode_head.conv_seg.weight", + "decode_head.conv_seg.bias", + ] + for model_name in param_names: + chkpt_name = prefix + model_name + if model_name not in model_dict or chkpt_name not in chkpt_dict: + logger.info(f"Skipping weight copy: {chkpt_name}") + continue + + # Mix weights + model_param = model_dict[model_name].clone() + chkpt_param = chkpt_dict[chkpt_name] + for model_key, c in enumerate(model2chkpt): + if c >= 0: + model_param[model_key].copy_(chkpt_param[c]) + + # Replace checkpoint weight by mixed weights + chkpt_dict[chkpt_name] = model_param + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER + + from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( # pylint: disable=ungrouped-imports + FeatureVectorHook, + ) + + BASE_CLASS = "otx.algorithms.segmentation.adapters.mmseg.models.segmentors.otx_encoder_decoder.OTXEncoderDecoder" + + @FUNCTION_REWRITER.register_rewriter(f"{BASE_CLASS}.extract_feat") + def single_stage_detector__extract_feat(ctx, self, img): + """Extract feature.""" + feat = self.backbone(img) + self.feature_map = feat + if self.with_neck: + feat = self.neck(feat) + return feat + + @FUNCTION_REWRITER.register_rewriter(f"{BASE_CLASS}.simple_test") + def single_stage_detector__simple_test(ctx, self, img, img_metas, **kwargs): + """Test.""" + # with output activation + seg_logit = self.inference(img, img_metas, True) + if ctx.cfg["dump_features"]: + feature_vector = FeatureVectorHook.func(self.feature_map) + return seg_logit, feature_vector + return seg_logit diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/__init__.py new file mode 100644 index 00000000000..0473d7c9b71 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/__init__.py @@ -0,0 +1,33 @@ +"""Utils used for mmseg model.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (c) 2020-2021 The MMSegmentation Authors +# SPDX-License-Identifier: Apache-2.0 +# + +from .aggregator import IterativeAggregator, IterativeConcatAggregator +from .angular_pw_conv import AngularPWConv +from .asymmetric_position_attention import AsymmetricPositionAttentionModule +from .channel_shuffle import channel_shuffle +from .local_attention import LocalAttentionModule +from .loss_equalizer import LossEqualizer +from .normalize import normalize +from .proto_utils import ProjectionHead, distributed_sinkhorn, momentum_update, trunc_normal_ +from .psp_layer import PSPModule + +__all__ = [ + "IterativeAggregator", + "IterativeConcatAggregator", + "channel_shuffle", + "LocalAttentionModule", + "LossEqualizer", + "PSPModule", + "AsymmetricPositionAttentionModule", + "AngularPWConv", + "normalize", + "distributed_sinkhorn", + "momentum_update", + "ProjectionHead", + "trunc_normal_", +] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/aggregator.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/aggregator.py new file mode 100644 index 00000000000..3d561ca6856 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/aggregator.py @@ -0,0 +1,227 @@ +"""Aggregators.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +import torch.nn.functional as F +from mmcv.cnn import ConvModule, DepthwiseSeparableConvModule +from torch import nn + +from otx.algorithms.segmentation.adapters.mmseg.models.utils import normalize + + +# pylint: disable=invalid-name +class IterativeAggregator(nn.Module): + """IterativeAggregator. + + Based on: https://github.com/HRNet/Lite-HRNet. + """ + + def __init__( + self, + in_channels, + min_channels=None, + conv_cfg=None, + norm_cfg=None, + merge_norm=None, + use_concat=False, + ): + if norm_cfg is None: + norm_cfg = dict(type="BN") + super().__init__() + + self.use_concat = use_concat + + num_branches = len(in_channels) + self.in_channels = in_channels[::-1] + + min_channels = min_channels if min_channels is not None else 0 + assert min_channels >= 0 + + out_channels = None + projects, expanders, fuse_layers = [], [], [] + for i in range(num_branches): + if not self.use_concat or i == 0: + fuse_layers.append(None) + else: + fuse_layers.append( + ConvModule( + in_channels=2 * out_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ) + ) + + if i != num_branches - 1: + out_channels = max(self.in_channels[i + 1], min_channels) + else: + out_channels = max(self.in_channels[i], min_channels) + + projects.append( + DepthwiseSeparableConvModule( + in_channels=max(self.in_channels[i], min_channels), + out_channels=out_channels, + kernel_size=3, + stride=1, + padding=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + dw_act_cfg=None, + pw_act_cfg=dict(type="ReLU"), + ) + ) + + if self.in_channels[i] < min_channels: + expanders.append( + ConvModule( + in_channels=self.in_channels[i], + out_channels=min_channels, + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ) + ) + else: + expanders.append(None) + + self.projects = nn.ModuleList(projects) + self.expanders = nn.ModuleList(expanders) + self.fuse_layers = nn.ModuleList(fuse_layers) + + assert merge_norm in [None, "none", "channel", "spatial"] + self.merge_norm = merge_norm + + @staticmethod + def _norm(x, mode=None): + if mode is None or mode == "none": + out = x + elif mode == "channel": + out = normalize(x, dim=1, p=2) + else: + _, c, h, w = x.size() + y = x.view(-1, c, h * w) + y = normalize(y, dim=2, p=2) + out = y.view(-1, c, h, w) + + return out + + def forward(self, x): + """Forward.""" + x = x[::-1] + + y_list = [] + last_x = None + for i, s in enumerate(x): + if self.expanders[i] is not None: + s = self.expanders[i](s) + + if last_x is not None: + last_x = F.interpolate(last_x, size=s.size()[-2:], mode="bilinear", align_corners=True) + + norm_s = self._norm(s, self.merge_norm) + norm_x = self._norm(last_x, self.merge_norm) + + if self.use_concat: + concat_s = torch.cat([norm_s, norm_x], dim=1) + s = self.fuse_layers[i](concat_s) + else: + s = norm_s + norm_x + + s = self.projects[i](s) + last_x = s + + y_list.append(s) + + return y_list[::-1] + + +class IterativeConcatAggregator(nn.Module): + """IterativeConcatAggregator.""" + + def __init__( + self, + in_channels, + min_channels=None, + conv_cfg=None, + norm_cfg=None, + merge_norm=None, + ): + if norm_cfg is None: + norm_cfg = dict(type="BN") + + super().__init__() + + num_branches = len(in_channels) + self.in_channels = in_channels[::-1] + + min_channels = min_channels if min_channels is not None else 0 + assert min_channels >= 0 + + fuse_layers = [None] + for i in range(1, num_branches): + if i == 1: + num_input_channels = self.in_channels[i - 1] + self.in_channels[i] + else: + num_input_channels = max(self.in_channels[i - 1], min_channels) + self.in_channels[i] + + num_out_channels = max(self.in_channels[i], min_channels) + + fuse_layers.append( + ConvModule( + in_channels=num_input_channels, + out_channels=num_out_channels, + kernel_size=1, + stride=1, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + act_cfg=dict(type="ReLU"), + ) + ) + + self.fuse_layers = nn.ModuleList(fuse_layers) + + assert merge_norm in [None, "none", "channel", "spatial"] + self.merge_norm = merge_norm + + @staticmethod + def _norm(x, mode=None): + if mode is None or mode == "none": + out = x + elif mode == "channel": + out = normalize(x, dim=1, p=2) + else: + _, c, h, w = x.size() + y = x.view(-1, c, h * w) + y = normalize(y, dim=2, p=2) + out = y.view(-1, c, h, w) + + return out + + def forward(self, x): + """Forward.""" + x = x[::-1] + + y_list = [] + last_x = None + for i, s in enumerate(x): + if last_x is not None: + last_x = F.interpolate(last_x, size=s.size()[-2:], mode="bilinear", align_corners=True) + + norm_s = self._norm(s, self.merge_norm) + norm_x = self._norm(last_x, self.merge_norm) + + concat_s = torch.cat([norm_s, norm_x], dim=1) + s = self.fuse_layers[i](concat_s) + + last_x = s + y_list.append(s) + + return y_list[::-1] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/angular_pw_conv.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/angular_pw_conv.py new file mode 100644 index 00000000000..50246412580 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/angular_pw_conv.py @@ -0,0 +1,36 @@ +"""Angular pw conv.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +import torch.nn.functional as F +from torch import nn + +from otx.algorithms.segmentation.adapters.mmseg.models.utils import normalize + + +class AngularPWConv(nn.Module): + """AngularPWConv.""" + + def __init__(self, in_features, out_features, clip_output=False): + super().__init__() + + self.in_features = in_features + assert in_features > 0 + self.out_features = out_features + assert out_features >= 2 + self.clip_output = clip_output + + self.weight = nn.Parameter(torch.Tensor(out_features, in_features)) + self.weight.data.normal_().renorm_(2, 1, 1e-5).mul_(1e5) + + def forward(self, x): + """Forward.""" + weight = normalize(self.weight, dim=1, p=2).view(self.out_features, self.in_features, 1, 1) + out = F.conv2d(x, weight) + + if self.clip_output and not torch.onnx.is_in_onnx_export(): + out = out.clamp(-1.0, 1.0) + + return out diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/asymmetric_position_attention.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/asymmetric_position_attention.py new file mode 100644 index 00000000000..f1dfea2e16e --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/asymmetric_position_attention.py @@ -0,0 +1,102 @@ +"""Asymmetric position attention module.""" +# Copyright (c) 2019 MendelXu +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +import torch.nn.functional as F +from mmcv.cnn import ConvModule +from torch import nn + +from .psp_layer import PSPModule + + +# pylint: disable=too-many-instance-attributes +class AsymmetricPositionAttentionModule(nn.Module): + """AsymmetricPositionAttentionModule. + + Reference: https://github.com/MendelXu/ANN. + """ + + def __init__( + self, + in_channels, + key_channels, + value_channels=None, + psp_size=None, + conv_cfg=None, + norm_cfg=None, + ): + super().__init__() + + if psp_size is None: + psp_size = (1, 3, 6, 8) + if norm_cfg is None: + norm_cfg = dict(type="BN") + + self.in_channels = in_channels + self.key_channels = key_channels + self.value_channels = value_channels if value_channels is not None else in_channels + self.conv_cfg = conv_cfg + self.norm_cfg = norm_cfg + + self.query_key = ConvModule( + in_channels=self.in_channels, + out_channels=self.key_channels, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + ) + self.key_psp = PSPModule(psp_size, method="max") + + self.value = ConvModule( + in_channels=self.in_channels, + out_channels=self.value_channels, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + ) + self.value_psp = PSPModule(psp_size, method="max") + + self.out_conv = ConvModule( + in_channels=self.value_channels, + out_channels=self.in_channels, + kernel_size=1, + stride=1, + padding=0, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=None, + ) + + def forward(self, x): + """Forward.""" + batch_size, _, _ = x.size(0), x.size(2), x.size(3) + + query_key = self.query_key(x) + + key = self.key_psp(query_key) + value = self.value_psp(self.value(x)).permute(0, 2, 1) + query = query_key.view(batch_size, self.key_channels, -1).permute(0, 2, 1) + + similarity_scores = torch.matmul(query, key) + similarity_scores = (self.key_channels**-0.5) * similarity_scores + similarity_scores = F.softmax(similarity_scores, dim=-1) + + y = torch.matmul(similarity_scores, value) + y = y.permute(0, 2, 1).contiguous() + y = y.view(batch_size, self.value_channels, *x.size()[2:]) + y = self.out_conv(y) + + out = x + y + + return out diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/channel_shuffle.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/channel_shuffle.py new file mode 100644 index 00000000000..a5697b7d984 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/channel_shuffle.py @@ -0,0 +1,35 @@ +"""Channel shuffle method.""" +# Copyright (c) 2018-2020 Open-MMLab. +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch + + +def channel_shuffle(x, groups): + """Channel Shuffle operation. + + This function enables cross-group information flow for multiple groups + convolution layers. + + Args: + x (Tensor): The input tensor. + groups (int): The number of groups to divide the input tensor in the channel dimension. + + Returns: + Tensor: The output tensor after channel shuffle operation. + """ + + batch_size, num_channels, height, width = x.size() + assert num_channels % groups == 0, "num_channels should be divisible by groups" + + channels_per_group = num_channels // groups + + x = x.view(batch_size, groups, channels_per_group, height, width) + x = torch.transpose(x, 1, 2).contiguous() + x = x.view(batch_size, -1, height, width) + + return x diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/local_attention.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/local_attention.py new file mode 100644 index 00000000000..2e743fac2cf --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/local_attention.py @@ -0,0 +1,76 @@ +"""Local attention module.""" +# Copyright (C) 2019-2021 Xiangtai Lee +# SPDX-License-Identifier: MIT +# +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch.nn.functional as F +from mmcv.cnn import ConvModule +from torch import nn + + +class LocalAttentionModule(nn.Module): + """LocalAttentionModule. + + Reference: https://github.com/lxtGH/GALD-DGCNet. + """ + + def __init__(self, num_channels, conv_cfg=None, norm_cfg=None): + if norm_cfg is None: + norm_cfg = dict(type="BN") + super().__init__() + + self.num_channels = num_channels + self.conv_cfg = conv_cfg + self.norm_cfg = norm_cfg + + self.dwconv1 = ConvModule( + in_channels=self.num_channels, + out_channels=self.num_channels, + kernel_size=3, + stride=2, + padding=1, + groups=self.num_channels, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + ) + self.dwconv2 = ConvModule( + in_channels=self.num_channels, + out_channels=self.num_channels, + kernel_size=3, + stride=2, + padding=1, + groups=self.num_channels, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + ) + self.dwconv3 = ConvModule( + in_channels=self.num_channels, + out_channels=self.num_channels, + kernel_size=3, + stride=2, + padding=1, + groups=self.num_channels, + conv_cfg=self.conv_cfg, + norm_cfg=self.norm_cfg, + act_cfg=dict(type="ReLU"), + ) + self.sigmoid_spatial = nn.Sigmoid() + + def forward(self, x): + """Forward.""" + _, _, h, w = x.size() + + y = self.dwconv1(x) + y = self.dwconv2(y) + y = self.dwconv3(y) + y = F.interpolate(y, size=(h, w), mode="bilinear", align_corners=True) + mask = self.sigmoid_spatial(y) + + out = x + x * mask + + return out diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/loss_equalizer.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/loss_equalizer.py new file mode 100644 index 00000000000..1491b15e89f --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/loss_equalizer.py @@ -0,0 +1,61 @@ +"""Loss equalizer.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +class LossEqualizer: + """Loss equalizer.""" + + def __init__(self, weights=None, momentum=0.1): + self.momentum = momentum + + self.trg_ratios = None + if weights is not None: + assert isinstance(weights, dict) + assert len(weights) > 0 + + sum_weight = 0.0 + for loss_weight in weights.values(): + assert loss_weight > 0 + sum_weight += float(loss_weight) + assert sum_weight > 0.0 + + self.trg_ratios = {loss_name: float(loss_weight) / sum_weight for loss_name, loss_weight in weights.items()} + + self._smoothed_values = dict() + + def reweight(self, losses): + """Reweight.""" + assert isinstance(losses, dict) + + if len(losses) == 0: + return losses + + for loss_name, loss_value in losses.items(): + if loss_name not in self._smoothed_values: + self._smoothed_values[loss_name] = loss_value.item() + else: + smoothed_loss = self._smoothed_values[loss_name] + self._smoothed_values[loss_name] = ( + 1.0 - self.momentum + ) * smoothed_loss + self.momentum * loss_value.item() + + if len(self._smoothed_values) == 1: + return losses + + total_sum = sum(self._smoothed_values.values()) + trg_value_default = total_sum / float(len(self._smoothed_values)) + + weighted_losses = dict() + for loss_name, loss_value in losses.items(): + if self.trg_ratios is not None: + assert loss_name in self.trg_ratios.keys() + trg_value = self.trg_ratios[loss_name] * total_sum + else: + trg_value = trg_value_default + + loss_weight = trg_value / self._smoothed_values[loss_name] + weighted_losses[loss_name] = loss_weight * loss_value + + return weighted_losses diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/normalize.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/normalize.py new file mode 100644 index 00000000000..db72f6884b2 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/normalize.py @@ -0,0 +1,45 @@ +"""Normalization.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +import torch.nn.functional as F +from torch import nn + + +class OnnxLpNormalization(torch.autograd.Function): # pylint: disable=abstract-method + """OnnxLpNormalization.""" + + @staticmethod + def forward(ctx, x, axis=0, p=2, eps=1e-12): # pylint: disable=unused-argument + """Forward.""" + denom = x.norm(2, axis, True).clamp_min(eps).expand_as(x) + return x / denom + + @staticmethod + def symbolic(g, x, axis=0, p=2, eps=1e-12): # pylint: disable=invalid-name, unused-argument + """Symbolic onnxLpNormalization.""" + return g.op("LpNormalization", x, axis_i=int(axis), p_i=int(p)) + + +def normalize(x, dim, p=2, eps=1e-12): + """Normalize method.""" + if torch.onnx.is_in_onnx_export(): + return OnnxLpNormalization.apply(x, dim, p, eps) + return F.normalize(x, dim=dim, p=p, eps=eps) + + +class Normalize(nn.Module): + """Normalize.""" + + def __init__(self, dim=1, p=2, eps=1e-12): + super().__init__() + + self.dim = dim + self.p = p + self.eps = eps + + def forward(self, x): + """Forward.""" + return normalize(x, self.dim, self.p, self.eps) diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/proto_utils.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/proto_utils.py new file mode 100644 index 00000000000..6afa6270941 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/proto_utils.py @@ -0,0 +1,122 @@ +"""Utils for prototype learning.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import math +import warnings + +import torch +import torch.nn.functional as F +from torch import nn + + +def distributed_sinkhorn(out, sinkhorn_iterations=3, epsilon=0.05): + """Sinkhorn distribution.""" + L = torch.exp(out / epsilon).t() # K x B + B = L.shape[1] + K = L.shape[0] + + # make the matrix sums to 1 + sum_L = torch.sum(L) + L /= sum_L + + for _ in range(sinkhorn_iterations): + L /= torch.sum(L, dim=1, keepdim=True) + L /= K + + L /= torch.sum(L, dim=0, keepdim=True) + L /= B + + L *= B + L = L.t() + + indexs = torch.argmax(L, dim=1) + L = F.gumbel_softmax(L, tau=0.5, hard=True) + + return L, indexs + + +def momentum_update(old_value, new_value, momentum, debug=False): + """EMA update function.""" + update = momentum * old_value + (1 - momentum) * new_value + if debug: + print( + "old prot: {:.3f} x |{:.3f}|, new val: {:.3f} x |{:.3f}|, result= |{:.3f}|".format( + momentum, + torch.norm(old_value, p=2), + (1 - momentum), + torch.norm(new_value, p=2), + torch.norm(update, p=2), + ) + ) + return update + + +class ProjectionHead(nn.Module): + """Projection head to transfrom features space used further for prototype learning.""" + + def __init__(self, dim_in, proj_dim=256): + super(ProjectionHead, self).__init__() + + self.proj = nn.Sequential(nn.Conv2d(dim_in, dim_in, 1), nn.ReLU(inplace=True), nn.Conv2d(dim_in, proj_dim, 1)) + + def forward(self, x): + """Farward method.""" + return F.normalize(self.proj(x), p=2, dim=-1) + + +def trunc_normal_(tensor, mean=0.0, std=1.0, a=-2.0, b=2.0): + r"""Truncated normal distribution. + + Fills the input Tensor with values drawn from a truncated + normal distribution. The values are effectively drawn from the + normal distribution :math:`\mathcal{N}(\text{mean}, \text{std}^2)` + with values outside :math:`[a, b]` redrawn until they are within + the bounds. The method used for generating the random values works + best when :math:`a \leq \text{mean} \leq b`. + + Args: + tensor: an n-dimensional `torch.Tensor` + mean: the mean of the normal distribution + std: the standard deviation of the normal distribution + a: the minimum cutoff value + b: the maximum cutoff value + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.trunc_normal_(w). + """ + # Cut & paste from PyTorch official master until it's in a few official releases - RW + # Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf + def norm_cdf(x): + # Computes standard normal cumulative distribution function + return (1.0 + math.erf(x / math.sqrt(2.0))) / 2.0 + + if (mean < a - 2 * std) or (mean > b + 2 * std): + warnings.warn( + "mean is more than 2 std from [a, b] in nn.init.trunc_normal_. " + "The distribution of values may be incorrect.", + stacklevel=2, + ) + + with torch.no_grad(): + # Values are generated by using a truncated uniform distribution and + # then using the inverse CDF for the normal distribution. + # Get upper and lower cdf values + lower = norm_cdf((a - mean) / std) + upper = norm_cdf((b - mean) / std) + + # Uniformly fill tensor with values from [l, u], then translate to + # [2l-1, 2u-1]. + tensor.uniform_(2 * lower - 1, 2 * upper - 1) + + # Use inverse cdf transform for normal distribution to get truncated + # standard normal + tensor.erfinv_() + + # Transform to proper mean, std + tensor.mul_(std * math.sqrt(2.0)) + tensor.add_(mean) + + # Clamp to ensure it's in the proper range + tensor.clamp_(min=a, max=b) + return tensor diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/psp_layer.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/psp_layer.py new file mode 100644 index 00000000000..bd3ad2d4357 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/psp_layer.py @@ -0,0 +1,36 @@ +"""PSP module.""" +# Copyright (c) 2019 MendelXu +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from torch import nn + + +class PSPModule(nn.Module): + """PSP module. + + Reference: https://github.com/MendelXu/ANN. + """ + + methods = {"max": nn.AdaptiveMaxPool2d, "avg": nn.AdaptiveAvgPool2d} + + def __init__(self, sizes=(1, 3, 6, 8), method="max"): + super().__init__() + + assert method in self.methods + pool_block = self.methods[method] + + self.stages = nn.ModuleList([pool_block(output_size=(size, size)) for size in sizes]) + + def forward(self, feats): + """Forward.""" + batch_size, c, _, _ = feats.size() + + priors = [stage(feats).view(batch_size, c, -1) for stage in self.stages] + out = torch.cat(priors, -1) + + return out diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/nncf/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/__init__.py new file mode 100644 index 00000000000..361ebcbb38a --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/__init__.py @@ -0,0 +1,21 @@ +"""NNCF utils for mmseg.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .builder import build_nncf_segmentor + +__all__ = [ + "build_nncf_segmentor", +] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py new file mode 100644 index 00000000000..d7c2d3ac78b --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py @@ -0,0 +1,183 @@ +"""NNCF wrapped mmcls models builder.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from functools import partial +from typing import Optional, Union + +import torch +from mmcv.parallel import DataContainer +from mmcv.runner import CheckpointLoader +from mmcv.utils import Config, ConfigDict + +# pylint: disable=no-name-in-module +from otx.algorithms.common.adapters.mmcv.nncf.runners import NNCF_META_KEY +from otx.algorithms.common.adapters.mmcv.utils import ( + get_configs_by_pairs, + remove_from_configs_by_type, +) +from otx.algorithms.common.adapters.nncf import is_accuracy_aware_training_set +from otx.algorithms.common.adapters.nncf.compression import NNCFMetaState +from otx.algorithms.segmentation.adapters.mmseg.utils import build_segmentor +from otx.utils.logger import get_logger + +logger = get_logger() + + +def build_nncf_segmentor( # noqa: C901 # pylint: disable=too-many-locals,too-many-statements + config: Config, + train_cfg: Optional[Union[Config, ConfigDict]] = None, + test_cfg: Optional[Union[Config, ConfigDict]] = None, + checkpoint: Optional[str] = None, + device: Union[str, torch.device] = "cpu", + cfg_options: Optional[Union[Config, ConfigDict]] = None, + distributed=False, + **kwargs +): + """A function to build NNCF wrapped mmcls model.""" + + from mmseg.apis import multi_gpu_test, single_gpu_test + from mmseg.apis.inference import LoadImage + from mmseg.datasets import build_dataloader as mmseg_build_dataloader + from mmseg.datasets import build_dataset as mmseg_build_dataset + from mmseg.datasets.pipelines import Compose + + from otx.algorithms.common.adapters.mmcv.nncf.utils import ( + get_fake_input, + model_eval, + wrap_nncf_model, + ) + from otx.algorithms.common.adapters.mmcv.utils.builder import ( + build_dataloader, + build_dataset, + ) + + if checkpoint is None: + # load model in this function not in runner + checkpoint = config.get("load_from") + assert checkpoint is not None, "checkpoint is not given. NNCF model must be initialized with pretrained model" + + model = build_segmentor( + config, + train_cfg=train_cfg, + test_cfg=test_cfg, + cfg_options=cfg_options, + from_scratch=True, + ) + model = model.to(device) + + state_dict = CheckpointLoader.load_checkpoint(checkpoint, map_location=device) + + is_acc_aware = is_accuracy_aware_training_set(config.get("nncf_config")) + + init_dataloader = None + model_eval_fn = None + if "meta" in state_dict and NNCF_META_KEY in state_dict["meta"]: + # NNCF ckpt + nncf_meta_state = state_dict["meta"][NNCF_META_KEY] + data_to_build_nncf = nncf_meta_state.data_to_build + state_to_build_nncf = nncf_meta_state.state_to_build + else: + # pytorch ckpt + state_to_build_nncf = state_dict + if "state_dict" in state_dict: + state_to_build_nncf = state_dict["state_dict"] + + init_dataloader = build_dataloader( + build_dataset( + config, + subset="train", + dataset_builder=mmseg_build_dataset, + ), + config, + subset="val", + dataloader_builder=mmseg_build_dataloader, + distributed=distributed, + persistent_workers=False, + ) + + # This data and state dict will be used to build NNCF graph later + # when loading NNCF model + # because some models run their subcomponents based on intermediate outputs + # resulting differently and partially traced NNCF graph + data_to_build_nncf = next(iter(init_dataloader))["img"] + if isinstance(data_to_build_nncf, DataContainer): + data_to_build_nncf = data_to_build_nncf.data[0] + data_to_build_nncf = data_to_build_nncf.cpu().numpy() + if len(data_to_build_nncf.shape) == 4: + data_to_build_nncf = data_to_build_nncf[0] + if data_to_build_nncf.shape[0] == 3: + data_to_build_nncf = data_to_build_nncf.transpose(1, 2, 0) + + val_dataloader = None + if is_acc_aware: + val_dataloader = build_dataloader( + build_dataset( + config, + subset="val", + dataset_builder=mmseg_build_dataset, + ), + config, + subset="val", + dataloader_builder=mmseg_build_dataloader, + distributed=distributed, + # segmentor does not support various sized batch images + samples_per_gpu=1, + persistent_workers=False, + ) + + model_eval_fn = partial( + model_eval, + config=config, + val_dataloader=val_dataloader, + evaluate_fn=multi_gpu_test if distributed else single_gpu_test, + distributed=distributed, + ) + state_dict = None + + test_pipeline = [LoadImage()] + config.data.test.pipeline[1:] + test_pipeline = Compose(test_pipeline) + get_fake_input_fn = partial( + get_fake_input, + preprocessor=test_pipeline, + data=data_to_build_nncf, + ) + + compression_ctrl, model = wrap_nncf_model( + config, + model, + model_eval_fn=model_eval_fn, + get_fake_input_fn=get_fake_input_fn, + dataloader_for_init=init_dataloader, + is_accuracy_aware=is_acc_aware, + ) + + # update runner to save metadata + config.runner.nncf_meta = NNCFMetaState( + state_to_build=state_to_build_nncf, + data_to_build=data_to_build_nncf, + ) + + # update custom hooks + custom_hooks = config.get("custom_hooks", []) + custom_hooks.append(ConfigDict({"type": "CancelTrainingHook"})) + custom_hooks.append( + ConfigDict( + type="CompressionHook", + compression_ctrl=compression_ctrl, + ) + ) + # TODO: move this to OTX task + remove_from_configs_by_type(custom_hooks, "CancelInterfaceHook") + remove_from_configs_by_type(custom_hooks, "TaskAdaptHook") + remove_from_configs_by_type(custom_hooks, "LazyEarlyStoppingHook") + remove_from_configs_by_type(custom_hooks, "EarlyStoppingHook") + config.custom_hooks = custom_hooks + + for hook in get_configs_by_pairs(custom_hooks, dict(type="OTXProgressHook")): + time_monitor = hook.get("time_monitor", None) + if time_monitor and getattr(time_monitor, "on_initialization_end", None) is not None: + time_monitor.on_initialization_end() + + return compression_ctrl, model diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/nncf/patches.py b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/patches.py new file mode 100644 index 00000000000..6955d15c02f --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/patches.py @@ -0,0 +1,12 @@ +"""Patch mmseg library.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# false positive pylint: disable=no-name-in-module +from mmseg.models.segmentors.base import BaseSegmentor + +from otx.algorithms.common.adapters.nncf.patches import nncf_trace_context + +# add nncf context method that will be used when nncf tracing +BaseSegmentor.nncf_trace_context = nncf_trace_context diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py new file mode 100644 index 00000000000..bab4bd206fa --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py @@ -0,0 +1,116 @@ +"""NNCF Task of OTX Segmentation.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from functools import partial +from typing import List, Optional + +import otx.algorithms.segmentation.adapters.mmseg.nncf.patches # noqa: F401 # pylint: disable=unused-import +from otx.algorithms.common.tasks.nncf_task import NNCFBaseTask +from otx.algorithms.segmentation.adapters.mmseg.nncf import build_nncf_segmentor +from otx.algorithms.segmentation.adapters.mmseg.task import MMSegmentationTask +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.metrics import ( + CurveMetric, + InfoMetric, + LineChartInfo, + MetricsGroup, + Performance, + ScoreMetric, + VisualizationInfo, + VisualizationType, +) +from otx.api.entities.model import ( + ModelEntity, +) +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.task_environment import TaskEnvironment +from otx.utils.logger import get_logger + +logger = get_logger() + + +class SegmentationNNCFTask(NNCFBaseTask, MMSegmentationTask): # pylint: disable=too-many-ancestors + """SegmentationNNCFTask.""" + + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__() # type: ignore [call-arg] + super(NNCFBaseTask, self).__init__(task_environment, output_path) + self._set_attributes_by_hyperparams() + + def configure( + self, + training=True, + ir_options=None, + export=False, + ): + """Configure configs for nncf task.""" + super(NNCFBaseTask, self).configure(training, ir_options, export) + self._prepare_optimize(export) + return self._config + + def _prepare_optimize(self, export=False): + super()._prepare_optimize() + + self.model_builder = partial( + self.model_builder, + nncf_model_builder=build_nncf_segmentor, + return_compression_ctrl=False, + is_export=export, + ) + + def _optimize( + self, + dataset: DatasetEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + ): + results = self._train_model(dataset) + + return results + + def _optimize_post_hook( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + ): + # Get training metrics group from learning curves + training_metrics, best_score = self._generate_training_metrics_group(self._learning_curves) + performance = Performance( + score=ScoreMetric(value=best_score, name=self.metric), + dashboard_metrics=training_metrics, + ) + + logger.info(f"Final model performance: {str(performance)}") + output_model.performance = performance + + def _generate_training_metrics_group(self, learning_curves): + """Get Training metrics (epochs & scores). + + Parses the mmsegmentation logs to get metrics from the latest training run + :return output List[MetricsGroup] + """ + output: List[MetricsGroup] = [] + # Model architecture + architecture = InfoMetric(name="Model architecture", value=self._model_name) + visualization_info_architecture = VisualizationInfo( + name="Model architecture", visualisation_type=VisualizationType.TEXT + ) + output.append( + MetricsGroup( + metrics=[architecture], + visualization_info=visualization_info_architecture, + ) + ) + # Learning curves + best_score = -1 + for key, curve in learning_curves.items(): + metric_curve = CurveMetric(xs=curve.x, ys=curve.y, name=key) + if key == f"val/{self.metric}": + best_score = max(curve.y) + visualization_info = LineChartInfo(name=key, x_axis_label="Epoch", y_axis_label=key) + output.append(MetricsGroup(metrics=[metric_curve], visualization_info=visualization_info)) + return output, best_score + + def _save_model_post_hook(self, modelinfo): + modelinfo["input_size"] = self._input_size diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/task.py b/src/otx/algorithms/segmentation/adapters/mmseg/task.py new file mode 100644 index 00000000000..ade39b64224 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/task.py @@ -0,0 +1,565 @@ +"""Task of OTX Segmentation using mmsegmentation training backend.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import glob +import io +import math +import os +import time +from contextlib import nullcontext +from copy import deepcopy +from functools import partial +from typing import Any, Dict, Optional, Union + +import torch +from mmcv.runner import wrap_fp16_model +from mmcv.utils import Config, ConfigDict, get_git_hash +from mmseg import __version__ +from mmseg.datasets import build_dataloader, build_dataset +from mmseg.utils import collect_env + +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + BaseRecordingForwardHook, + FeatureVectorHook, +) +from otx.algorithms.common.adapters.mmcv.utils import ( + adapt_batch_size, + build_data_parallel, + get_configs_by_pairs, +) +from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + InputSizeManager, + OTXConfig, +) +from otx.algorithms.common.adapters.torch.utils import convert_sync_batchnorm +from otx.algorithms.common.configs.configuration_enums import BatchSizeAdaptType +from otx.algorithms.common.configs.training_base import TrainType +from otx.algorithms.common.tasks.nncf_task import NNCFBaseTask +from otx.algorithms.common.utils import is_hpu_available +from otx.algorithms.common.utils.data import get_dataset +from otx.algorithms.segmentation.adapters.mmseg.apis.train import train_segmentor +from otx.algorithms.segmentation.adapters.mmseg.configurer import ( + IncrSegmentationConfigurer, + SegmentationConfigurer, + SemiSLSegmentationConfigurer, +) +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import build_segmentor +from otx.algorithms.segmentation.adapters.mmseg.utils.exporter import ( + SegmentationExporter, +) +from otx.algorithms.segmentation.task import OTXSegmentationTask +from otx.api.configuration import cfg_helper +from otx.api.configuration.helper.utils import ids_to_strings +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ( + ModelEntity, + ModelPrecision, +) +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.utils.logger import get_logger + +if is_hpu_available(): + import habana_frameworks.torch.core as htcore + +logger = get_logger() + +# TODO Remove unnecessary pylint disable +# pylint: disable=too-many-lines + + +class MMSegmentationTask(OTXSegmentationTask): + """Task class for OTX segmentation using mmsegmentation training backend.""" + + # pylint: disable=too-many-instance-attributes + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__(task_environment, output_path) + self._data_cfg: Optional[Config] = None + self._recipe_cfg: Optional[Config] = None + + # pylint: disable=too-many-locals, too-many-branches, too-many-statements + def _init_task(self): # noqa + """Initialize task.""" + self._recipe_cfg = OTXConfig.fromfile(os.path.join(self._model_dir, "model.py")) + self._recipe_cfg.domain = self._task_type.domain + self._config = self._recipe_cfg + + self.set_seed() + + logger.info("initialized.") + + # pylint: disable=too-many-arguments + def configure( + self, + training=True, + ir_options=None, + export=False, + ): + """Patch mmcv configs for OTX segmentation settings.""" + + # deepcopy all configs to make sure + # changes under Configuerer and below does not take an effect to OTX for clear distinction + recipe_cfg = deepcopy(self._recipe_cfg) + assert recipe_cfg is not None, "'recipe_cfg' is not initialized." + + if self._data_cfg is not None: + data_classes = [label.name for label in self._labels] + else: + data_classes = None + model_classes = [label.name for label in self._model_label_schema] + + recipe_cfg.work_dir = self._output_path + recipe_cfg.resume = self._resume + + if self._train_type == TrainType.Incremental: + configurer = IncrSegmentationConfigurer( + "segmentation", + training, + export, + self.override_configs, + self.on_hook_initialized, + self._time_monitor, + self._learning_curves, + ) + elif self._train_type == TrainType.Semisupervised: + configurer = SemiSLSegmentationConfigurer( + "segmentation", + training, + export, + self.override_configs, + self.on_hook_initialized, + self._time_monitor, + self._learning_curves, + ) + else: + configurer = SegmentationConfigurer( + "segmentation", + training, + export, + self.override_configs, + self.on_hook_initialized, + self._time_monitor, + self._learning_curves, + ) + cfg = configurer.configure( + recipe_cfg, + self.data_pipeline_path, + self._hyperparams, + self._model_ckpt, + self._data_cfg, + ir_options, + data_classes, + model_classes, + self._input_size, + ) + self._config = cfg + self._input_size = cfg.model.pop("input_size", None) + + return cfg + + def build_model( + self, + cfg: Config, + fp16: bool = False, + **kwargs, + ) -> torch.nn.Module: + """Build model from model_builder.""" + model_builder = getattr(self, "model_builder", build_segmentor) + model = model_builder(cfg, **kwargs) + if bool(fp16): + wrap_fp16_model(model) + return model + + def _infer_model( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ): + """Main infer function.""" + self._data_cfg = ConfigDict( + data=ConfigDict( + train=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + test=ConfigDict( + otx_dataset=dataset, + labels=self._labels, + ), + ) + ) + + dump_features = True + + self._init_task() + + cfg = self.configure(False, None) + logger.info("infer!") + + # FIXME: Currently segmentor does not support multi batch inference. + if "test" in cfg.data and "test_dataloader" in cfg.data: + cfg.data.test_dataloader["samples_per_gpu"] = 1 + + # Data loader + mm_dataset = build_dataset(cfg.data.test) + dataloader = build_dataloader( + mm_dataset, + samples_per_gpu=cfg.data.test_dataloader.get("samples_per_gpu", 1), + workers_per_gpu=cfg.data.test_dataloader.get("workers_per_gpu", 0), + num_gpus=len(cfg.gpu_ids), + dist=cfg.distributed, + seed=cfg.get("seed", None), + persistent_workers=False, + shuffle=False, + ) + + # Target classes + if "task_adapt" in cfg: + target_classes = cfg.task_adapt.final + if len(target_classes) < 1: + raise KeyError( + f"target_classes={target_classes} is empty check the metadata from model ckpt or recipe " + "configuration" + ) + else: + target_classes = mm_dataset.CLASSES + + # Model + model = self.build_model(cfg, fp16=cfg.get("fp16", False)) + model.CLASSES = target_classes + model.eval() + feature_model = model.model_s if self._train_type == TrainType.Semisupervised else model + model = build_data_parallel(model, cfg, distributed=False) + + # InferenceProgressCallback (Time Monitor enable into Infer task) + time_monitor = None + if cfg.get("custom_hooks", None): + time_monitor = [hook.time_monitor for hook in cfg.custom_hooks if hook.type == "OTXProgressHook"] + time_monitor = time_monitor[0] if time_monitor else None + if time_monitor is not None: + + # pylint: disable=unused-argument + def pre_hook(module, inp): + time_monitor.on_test_batch_begin(None, None) + + def hook(module, inp, outp): + time_monitor.on_test_batch_end(None, None) + + model.register_forward_pre_hook(pre_hook) + model.register_forward_hook(hook) + + eval_predictions = [] + feature_vectors = [] + + if not dump_features: + feature_vector_hook: Union[nullcontext, BaseRecordingForwardHook] = nullcontext() + else: + feature_vector_hook = FeatureVectorHook(feature_model) + + with feature_vector_hook: + for data in dataloader: + with torch.no_grad(): + result = model(return_loss=False, output_logits=True, **data) + eval_predictions.append(result) + if isinstance(feature_vector_hook, nullcontext): + feature_vectors = [None] * len(mm_dataset) + else: + feature_vectors = feature_vector_hook.records + + assert len(eval_predictions) == len(feature_vectors), ( + "Number of elements should be the same, however, number of outputs are ", + f"{len(eval_predictions)} and {len(feature_vectors)}", + ) + + outputs = dict( + classes=target_classes, + eval_predictions=eval_predictions, + feature_vectors=feature_vectors, + ) + return outputs + + # pylint: disable=too-many-branches, too-many-statements + def _train_model( + self, + dataset: DatasetEntity, + ): + """Train function in MMSegmentationTask.""" + logger.info("init data cfg.") + self._data_cfg = ConfigDict(data=ConfigDict()) + + for cfg_key, subset in zip( + ["train", "val", "unlabeled"], + [Subset.TRAINING, Subset.VALIDATION, Subset.UNLABELED], + ): + subset = get_dataset(dataset, subset) + if subset and self._data_cfg is not None: + self._data_cfg.data[cfg_key] = ConfigDict( + otx_dataset=subset, + labels=self._labels, + ) + + self._is_training = True + + self._init_task() + + cfg = self.configure(True, None) + logger.info("train!") + + timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) + + # Environment + logger.info(f"cfg.gpu_ids = {cfg.gpu_ids}, distributed = {cfg.distributed}") + env_info_dict = collect_env() + env_info = "\n".join([(f"{k}: {v}") for k, v in env_info_dict.items()]) + dash_line = "-" * 60 + "\n" + logger.info(f"Environment info:\n{dash_line}{env_info}\n{dash_line}") + + # Data + datasets = [build_dataset(cfg.data.train)] + + if self._train_type == TrainType.Semisupervised: + # forward the knowledge of num iters per epoch to model for filter loss + bs_per_gpu = cfg.data.train_dataloader["samples_per_gpu"] + actual_bs = bs_per_gpu * torch.distributed.get_world_size() if cfg.distributed else bs_per_gpu + cfg.model.num_iters_per_epoch = math.ceil(len(datasets[0]) / actual_bs) + + # FIXME: Currently segmentor does not support multi batch evaluation. + # For the Self-SL case, there is no val data. So, need to check the + + if "val" in cfg.data and "val_dataloader" in cfg.data: + cfg.data.val_dataloader["samples_per_gpu"] = 1 + + # Target classes + if "task_adapt" in cfg: + target_classes = cfg.task_adapt.final + else: + target_classes = datasets[0].CLASSES + + # Metadata + meta = dict() + meta["env_info"] = env_info + meta["seed"] = cfg.seed + meta["exp_name"] = cfg.work_dir + if cfg.checkpoint_config is not None: + cfg.checkpoint_config.meta = dict( + mmseg_version=__version__ + get_git_hash()[:7], + CLASSES=target_classes, + ) + + # Model + model = self.build_model(cfg, fp16=cfg.get("fp16", False), is_training=self._is_training) + model.train() + model.CLASSES = target_classes + + if is_hpu_available(): + # TODO (sungchul): move it to appropriate location if needed + htcore.hpu.ModuleCacher(max_graphs=10)(model=model.backbone, inplace=True) + htcore.hpu.ModuleCacher(max_graphs=10)(model=model.decode_head, inplace=True) + + if cfg.distributed: + convert_sync_batchnorm(model) + + validate = bool(cfg.data.get("val", None)) + + if self._hyperparams.learning_parameters.auto_adapt_batch_size != BatchSizeAdaptType.NONE: + train_func = partial(train_segmentor, meta=deepcopy(meta), model=deepcopy(model), distributed=False) + adapt_batch_size( + train_func, + cfg, + datasets, + isinstance(self, NNCFBaseTask), # nncf needs eval hooks + not_increase=(self._hyperparams.learning_parameters.auto_adapt_batch_size == BatchSizeAdaptType.SAFE), + ) + + train_segmentor( + model, + datasets, + cfg, + distributed=cfg.distributed, + validate=validate, + timestamp=timestamp, + meta=meta, + ) + + # Save outputs + output_ckpt_path = os.path.join(cfg.work_dir, "latest.pth") + best_ckpt_path = glob.glob(os.path.join(cfg.work_dir, "best_mDice_*.pth")) + if len(best_ckpt_path) > 0: + output_ckpt_path = best_ckpt_path[0] + best_ckpt_path = glob.glob(os.path.join(cfg.work_dir, "best_mIoU_*.pth")) + if len(best_ckpt_path) > 0: + output_ckpt_path = best_ckpt_path[0] + return dict( + final_ckpt=output_ckpt_path, + ) + + def _explain_model(self): + """Explain function of OTX Segmentation Task.""" + raise NotImplementedError + + # pylint: disable=too-many-statements + def _export_model( + self, + precision: ModelPrecision = ModelPrecision.FP32, + export_format: ExportType = ExportType.ONNX, + dump_features: bool = True, + ): + """Export function of OTX Segmentation Task.""" + # copied from OTX inference_task.py + self._data_cfg = ConfigDict( + data=ConfigDict( + train=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + test=ConfigDict( + otx_dataset=None, + labels=self._labels, + ), + ) + ) + self._init_task() + + cfg = self.configure(False, None, export=True) + + self._precision[0] = precision + export_options: Dict[str, Any] = {} + export_options["deploy_cfg"] = self._init_deploy_cfg(cfg) + assert len(self._precision) == 1 + export_options["precision"] = str(self._precision[0]) + export_options["type"] = str(export_format) + + export_options["deploy_cfg"]["dump_features"] = dump_features + if dump_features: + output_names = export_options["deploy_cfg"]["ir_config"]["output_names"] + if "feature_vector" not in output_names: + output_names.append("feature_vector") + if export_options["deploy_cfg"]["codebase_config"]["task"] != "Segmentation": + if "saliency_map" not in output_names: + output_names.append("saliency_map") + export_options["model_builder"] = getattr(self, "model_builder", build_segmentor) + + if self._precision[0] == ModelPrecision.FP16: + export_options["deploy_cfg"]["backend_config"]["mo_options"]["flags"].append("--compress_to_fp16") + + backend_cfg_backup = {} + if export_format == ExportType.ONNX: + backend_cfg_backup = export_options["deploy_cfg"]["backend_config"] + export_options["deploy_cfg"]["backend_config"] = {"type": "onnxruntime"} + export_options["deploy_cfg"]["ir_config"]["dynamic_axes"]["input"] = {0: "batch"} + + exporter = SegmentationExporter() + results = exporter.run( + cfg, + **export_options, + ) + + if export_format == ExportType.ONNX: + results["inference_parameters"] = {} + results["inference_parameters"]["mean_values"] = " ".join( + map(str, backend_cfg_backup["mo_options"]["args"]["--mean_values"]) + ) + results["inference_parameters"]["scale_values"] = " ".join( + map(str, backend_cfg_backup["mo_options"]["args"]["--scale_values"]) + ) + + return results + + # This should moved somewhere + def _init_deploy_cfg(self, cfg: Config) -> Union[Config, None]: + base_dir = os.path.abspath(os.path.dirname(self._task_environment.model_template.model_template_path)) + deploy_cfg_path = os.path.join(base_dir, "deployment.py") + deploy_cfg = None + if os.path.exists(deploy_cfg_path): + deploy_cfg = OTXConfig.fromfile(deploy_cfg_path) + + def patch_input_preprocessing(deploy_cfg): + normalize_cfg = get_configs_by_pairs( + cfg.data.test.pipeline, + dict(type="Normalize"), + ) + assert len(normalize_cfg) == 1 + normalize_cfg = normalize_cfg[0] + + options = dict(flags=[], args={}) + # NOTE: OTX loads image in RGB format + # so that `to_rgb=True` means a format change to BGR instead. + # Conventionally, OpenVINO IR expects a image in BGR format + # but OpenVINO IR under OTX assumes a image in RGB format. + # + # `to_rgb=True` -> a model was trained with images in BGR format + # and a OpenVINO IR needs to reverse input format from RGB to BGR + # `to_rgb=False` -> a model was trained with images in RGB format + # and a OpenVINO IR does not need to do a reverse + if normalize_cfg.get("to_rgb", False): + options["flags"] += ["--reverse_input_channels"] + # value must be a list not a tuple + if normalize_cfg.get("mean", None) is not None: + options["args"]["--mean_values"] = list(normalize_cfg.get("mean")) + if normalize_cfg.get("std", None) is not None: + options["args"]["--scale_values"] = list(normalize_cfg.get("std")) + + # fill default + backend_config = deploy_cfg.backend_config + if backend_config.get("mo_options") is None: + backend_config.mo_options = ConfigDict() + mo_options = backend_config.mo_options + if mo_options.get("args") is None: + mo_options.args = ConfigDict() + if mo_options.get("flags") is None: + mo_options.flags = [] + + # already defiend options have higher priority + options["args"].update(mo_options.args) + mo_options.args = ConfigDict(options["args"]) + # make sure no duplicates + mo_options.flags.extend(options["flags"]) + mo_options.flags = list(set(mo_options.flags)) + + def patch_input_shape(deploy_cfg): + input_size_manager = InputSizeManager(cfg) + size = input_size_manager.get_input_size_from_cfg("test") + assert all(isinstance(i, int) and i > 0 for i in size) + # default is static shape to prevent an unexpected error + # when converting to OpenVINO IR + deploy_cfg.backend_config.model_inputs = [ConfigDict(opt_shapes=ConfigDict(input=[1, 3, *size]))] + + patch_input_preprocessing(deploy_cfg) + patch_input_shape(deploy_cfg) + + return deploy_cfg + + # This should be removed + def update_override_configurations(self, config): + """Update override_configs.""" + logger.info(f"update override config with: {config}") + config = ConfigDict(**config) + self.override_configs.update(config) + + def save_model(self, output_model: ModelEntity): + """Save best model weights in SegmentationTrainTask.""" + logger.info("called save_model") + buffer = io.BytesIO() + hyperparams_str = ids_to_strings(cfg_helper.convert(self._hyperparams, dict, enum_to_str=True)) + labels = {label.name: label.color.rgb_tuple for label in self._labels} + model_ckpt = torch.load(self._model_ckpt) + modelinfo = { + "model": model_ckpt, + "config": hyperparams_str, + "labels": labels, + "input_size": self._input_size, + "VERSION": 1, + } + + torch.save(modelinfo, buffer) + output_model.set_data("weights.pth", buffer.getvalue()) + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self._task_environment.label_schema), + ) + output_model.precision = self._precision diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/utils/__init__.py b/src/otx/algorithms/segmentation/adapters/mmseg/utils/__init__.py new file mode 100644 index 00000000000..c6319cab77e --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/utils/__init__.py @@ -0,0 +1,27 @@ +"""OTX Adapters - mmseg.utils.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .builder import build_scalar_scheduler, build_segmentor +from .data_utils import get_valid_label_mask_per_batch, load_dataset_items +from .exporter import SegmentationExporter + +__all__ = [ + "SegmentationExporter", + "load_dataset_items", + "build_scalar_scheduler", + "build_segmentor", + "get_valid_label_mask_per_batch", +] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/utils/builder.py b/src/otx/algorithms/segmentation/adapters/mmseg/utils/builder.py new file mode 100644 index 00000000000..35332e7b749 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/utils/builder.py @@ -0,0 +1,70 @@ +"""MMseg model builder.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Optional, Union + +import torch +from mmcv.runner import load_checkpoint +from mmcv.utils import Config, ConfigDict +from mmseg.models.builder import MODELS + +SCALAR_SCHEDULERS = MODELS + + +def build_scalar_scheduler(cfg, default_value=None): + """Build scalar scheduler.""" + if cfg is None: + if default_value is not None: + assert isinstance(default_value, (int, float)) + cfg = dict(type="ConstantScalarScheduler", scale=float(default_value)) + else: + return None + elif isinstance(cfg, (int, float)): + cfg = dict(type="ConstantScalarScheduler", scale=float(cfg)) + + return SCALAR_SCHEDULERS.build(cfg) + + +def build_segmentor( + config: Config, + train_cfg: Optional[Union[Config, ConfigDict]] = None, + test_cfg: Optional[Union[Config, ConfigDict]] = None, + checkpoint: Optional[str] = None, + device: Union[str, torch.device] = "cpu", + cfg_options: Optional[Union[Config, ConfigDict]] = None, + from_scratch: bool = False, + is_training: bool = False, +) -> torch.nn.Module: + """A builder function for mmseg model. + + Creates a model, based on the configuration in config. + Note that this function updates 'load_from' attribute of 'config'. + """ + + # fmt: off + # isort: off + # false positive (mypy, pylint) + # pylint: disable-next=no-name-in-module + from mmseg.models import build_segmentor as origin_build_segmentor # type: ignore[attr-defined] + # isort: on + # fmt: on + + if cfg_options is not None: + config.merge_from_dict(cfg_options) + + model_cfg = deepcopy(config.model) + model = origin_build_segmentor(model_cfg, train_cfg=train_cfg, test_cfg=test_cfg) + model = model.to(device) + + checkpoint = checkpoint if checkpoint else config.get("load_from", None) + config.load_from = checkpoint + + if checkpoint is not None and not from_scratch: + load_checkpoint(model, checkpoint, map_location=device) + if is_training is True: + config.load_from = None # To prevent the repeated ckpt loading in mmseg.apis.train_segmentor + + return model diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py b/src/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py new file mode 100644 index 00000000000..bdfe9ce32cd --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py @@ -0,0 +1,242 @@ +"""Collection of utils for dataset in Segmentation Task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import json +import os +from typing import List, Optional + +import cv2 +import numpy as np +import torch +import tqdm +from mmseg.datasets.custom import CustomDataset +from skimage.segmentation import felzenszwalb + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.subset import Subset +from otx.utils.logger import get_logger + +logger = get_logger() + +# pylint: disable=too-many-locals + + +def get_classes_from_annotation(annot_path): + """Getter function of classes from annotation.""" + with open(annot_path, encoding="UTF-8") as input_stream: + content = json.load(input_stream) + labels_map = content["labels_map"] + + categories = [(v["name"], v["id"]) for v in sorted(labels_map, key=lambda tup: int(tup["id"]))] + + return categories + + +def abs_path_if_valid(value): + """Valid function of abs_path.""" + if value: + return os.path.abspath(value) + return None + + +def create_annotation_from_hard_seg_map(hard_seg_map: np.ndarray, labels: List[LabelEntity]): + """Creation function from hard seg_map.""" + height, width = hard_seg_map.shape[:2] + unique_labels = np.unique(hard_seg_map) + + annotations: List[Annotation] = [] + for label_id in unique_labels: + label_id_entity = ID(f"{label_id:08}") + matches = [label for label in labels if label.id == label_id_entity] + if len(matches) == 0: + continue + + assert len(matches) == 1 + label = matches[0] + + label_mask = hard_seg_map == label_id + label_index_map = label_mask.astype(np.uint8) + + contours, hierarchies = cv2.findContours(label_index_map, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + + if hierarchies is None: + continue + + for contour, hierarchy in zip(contours, hierarchies[0]): + if hierarchy[3] != -1: + continue + + contour = list(contour) + if len(contour) <= 2: + continue + + points = [Point(x=point[0][0] / (width - 1), y=point[0][1] / (height - 1)) for point in contour] + + annotations.append( + Annotation( + Polygon(points=points), + labels=[ScoredLabel(label)], + id=ID(f"{label_id:08}"), + ) + ) + + return annotations + + +def load_labels_from_annotation(ann_dir): + """Load labels function from annotation.""" + if ann_dir is None: + return [] + + labels_map_path = os.path.join(ann_dir, "meta.json") + labels = get_classes_from_annotation(labels_map_path) + + return labels + + +def add_labels(cur_labels: List[LabelEntity], new_labels: List[tuple]): + """Add labels function.""" + for label_name, label_id in new_labels: + matching_labels = [label for label in cur_labels if label.name == label_name] + if len(matching_labels) > 1: + raise ValueError("Found multiple matching labels") + if len(matching_labels) == 0: + label_id = label_id if label_id is not None else len(cur_labels) + label = LabelEntity(name=label_name, domain=Domain.SEGMENTATION, id=ID(f"{label_id:08}")) + cur_labels.append(label) + + +def check_labels(cur_labels: List[LabelEntity], new_labels: List[tuple]): + """Check labels function.""" + cur_names = {label.name for label in cur_labels} + new_names = {label[0] for label in new_labels} + if cur_names != new_names: + raise ValueError("Class names don't match from file to file") + + +def get_extended_label_names(labels: List[LabelEntity]): + """Getter function of extended label names.""" + target_labels = [v.name for v in sorted(labels, key=lambda x: x.id)] + all_labels = ["background"] + target_labels + return all_labels + + +def get_valid_label_mask_per_batch(img_metas, num_classes): + """Get valid label mask removing ignored classes to zero mask in a batch.""" + valid_label_mask_per_batch = [] + for _, meta in enumerate(img_metas): + valid_label_mask = torch.Tensor([1 for _ in range(num_classes)]) + if "ignored_labels" in meta and meta["ignored_labels"]: + valid_label_mask[meta["ignored_labels"]] = 0 + valid_label_mask_per_batch.append(valid_label_mask) + return valid_label_mask_per_batch + + +def create_pseudo_masks(ann_file_path: str, data_root_dir: str, mode="FH"): + """Create pseudo masks for Self-SL using DetCon.""" + if not os.path.isdir(ann_file_path): + logger.info( + ( + f"Creating pseudo masks with mode={mode} is required. " + f"It may take some time. Once this process has been performed, " + f"there is no need to proceed again with " + f"ann_file_path={ann_file_path} and data_root_dir={data_root_dir}." + ) + ) + os.makedirs(ann_file_path, exist_ok=False) + img_list = os.listdir(data_root_dir) + total_labels = [] + # create pseudo masks + for path in tqdm.tqdm(img_list, total=len(img_list)): + save_path = path.replace(".jpg", ".png") if path.endswith(".jpg") else path + img = cv2.imread(os.path.join(data_root_dir, path))[..., ::-1] + if mode == "FH": + pseudo_mask = felzenszwalb(img, scale=1000, min_size=1000) + else: + raise ValueError( + (f"{mode} is not supported to create pseudo masks for DetCon." 'Choose one of ["FH"].') + ) + cv2.imwrite(os.path.join(ann_file_path, save_path), pseudo_mask.astype(np.uint8)) + + # get labels to create meta.json + labels = np.unique(pseudo_mask) + for label in labels: + if label not in total_labels: + total_labels.append(label) + + # create meta.json + # TODO (sungchul): to be updated as max(total_labels) -> max(total_labels)+1 + # Currently, background class is automatically added in the backend. + # Considering background class, labels_map in meta.json should have one less than the number of labels. + # If we don't need to consider background class, it will be updated to consider all labels. + meta = {"labels_map": [{"name": f"target{i+1}", "id": i + 1} for i in range(max(total_labels))]} + with open(os.path.join(ann_file_path, "meta.json"), "w", encoding="UTF-8") as f: + json.dump(meta, f, indent=4) + + +def load_dataset_items( + ann_file_path: str, + data_root_dir: str, + subset: Subset = Subset.NONE, + labels_list: Optional[List[LabelEntity]] = None, +): + """Load dataset items.""" + if "detcon" in ann_file_path: # TODO (sungchul): deterministic condition + create_pseudo_masks(ann_file_path, data_root_dir) + + ann_dir = abs_path_if_valid(ann_file_path) + img_dir = abs_path_if_valid(data_root_dir) + + annot_labels = load_labels_from_annotation(ann_dir) + + if labels_list is None: + labels_list = [] + if len(labels_list) == 0: + add_labels(labels_list, annot_labels) + else: + check_labels(labels_list, annot_labels) + + test_mode = subset in {Subset.VALIDATION, Subset.TESTING} + pipeline = [dict(type="LoadAnnotations")] + + dataset = CustomDataset( + img_dir=img_dir, + ann_dir=ann_dir, + pipeline=pipeline, + classes=get_extended_label_names(labels_list), + test_mode=test_mode, + ) + dataset.test_mode = False + + dataset_items = [] + for item in dataset: + annotations = create_annotation_from_hard_seg_map(hard_seg_map=item["gt_semantic_seg"], labels=labels_list) + filename = os.path.join(item["img_prefix"], item["img_info"]["filename"]) + image = Image(file_path=filename) + annotation_scene = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=annotations) + dataset_items.append(DatasetItemEntity(media=image, annotation_scene=annotation_scene, subset=subset)) + + return dataset_items diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/utils/exporter.py b/src/otx/algorithms/segmentation/adapters/mmseg/utils/exporter.py new file mode 100644 index 00000000000..b7317c8b090 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/mmseg/utils/exporter.py @@ -0,0 +1,73 @@ +"""Export task for OTX Segmentation with MMSEG.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +from mmcv.runner import wrap_fp16_model + +from otx.algorithms.common.adapters.mmcv.tasks.exporter import Exporter +from otx.algorithms.common.adapters.mmdeploy.utils import sync_batchnorm_2_batchnorm +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import build_segmentor +from otx.utils.logger import get_logger + +logger = get_logger() + + +class SegmentationExporter(Exporter): + """Exporter for OTX Segmentation using mmsegmentation training backend.""" + + def run(self, cfg, **kwargs): # noqa: C901 + """Run exporter stage.""" + + precision = kwargs.get("precision", "FP32") + model_builder = kwargs.get("model_builder", build_segmentor) + + def model_builder_helper(*args, **kwargs): + model = model_builder(*args, **kwargs) + # TODO: handle various input size + model = sync_batchnorm_2_batchnorm(model, 2) + + if precision == "FP16": + wrap_fp16_model(model) + elif precision == "INT8": + from nncf.torch.nncf_network import NNCFNetwork + + assert isinstance(model, NNCFNetwork) + + return model + + kwargs["model_builder"] = model_builder_helper + + return super().run(cfg, **kwargs) + + @staticmethod + def naive_export(output_dir, model_builder, precision, export_type, cfg, model_name="model"): + """Export using pytorch backend.""" + from mmseg.apis.inference import LoadImage + from mmseg.datasets.pipelines import Compose + + from otx.algorithms.common.adapters.mmdeploy.apis import NaiveExporter + + def get_fake_data(cfg, orig_img_shape=(128, 128, 3)): + pipeline = [LoadImage()] + cfg.data.test.pipeline[1:] + pipeline = Compose(pipeline) + data = dict(img=np.zeros(orig_img_shape, dtype=np.uint8)) + data = pipeline(data) + return data + + fake_data = get_fake_data(cfg) + opset_version = 11 + + NaiveExporter.export2backend( + output_dir, + model_builder, + cfg, + fake_data, + precision=precision, + model_name=model_name, + input_names=["input"], + output_names=["output"], + opset_version=opset_version, + export_type=export_type, + ) diff --git a/src/otx/algorithms/segmentation/adapters/openvino/__init__.py b/src/otx/algorithms/segmentation/adapters/openvino/__init__.py new file mode 100644 index 00000000000..ccd9c37a9ae --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/openvino/__init__.py @@ -0,0 +1,14 @@ +"""OTX Adapters - openvino.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .task import ( + OpenVINOSegmentationInferencer, + OpenVINOSegmentationTask, +) + +__all__ = [ + "OpenVINOSegmentationTask", + "OpenVINOSegmentationInferencer", +] diff --git a/src/otx/algorithms/segmentation/adapters/openvino/model_wrappers/__init__.py b/src/otx/algorithms/segmentation/adapters/openvino/model_wrappers/__init__.py new file mode 100644 index 00000000000..4b704bae9c5 --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/openvino/model_wrappers/__init__.py @@ -0,0 +1,15 @@ +"""Model Wrapper Initialization of OTX Segmentation.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/adapters/openvino/task.py b/src/otx/algorithms/segmentation/adapters/openvino/task.py new file mode 100644 index 00000000000..4aca3b9a7fe --- /dev/null +++ b/src/otx/algorithms/segmentation/adapters/openvino/task.py @@ -0,0 +1,392 @@ +"""Openvino Task of Segmentation.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import io +import json +import os +import tempfile +import time +from typing import Any, Dict, List, Optional, Tuple, Union +from zipfile import ZipFile + +import attr +import nncf +import numpy as np +import openvino.runtime as ov +from addict import Dict as ADDict +from nncf.common.quantization.structs import QuantizationPreset +from openvino.model_api.adapters import OpenvinoAdapter, create_core +from openvino.model_api.models import Model +from openvino.model_api.models.utils import ImageResultWithSoftPrediction + +from otx.algorithms.common.utils import OTXOpenVinoDataLoader, get_default_async_reqs_num, read_py_config +from otx.algorithms.common.utils.ir import check_if_quantized +from otx.algorithms.segmentation.adapters.openvino import model_wrappers +from otx.algorithms.segmentation.configs.base import SegmentationConfig +from otx.algorithms.segmentation.utils import get_activation_map +from otx.api.entities.annotation import AnnotationSceneEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import ( + InferenceParameters, + default_progress_callback, +) +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ( + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.result_media import ResultMediaEntity +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.tensor import TensorEntity +from otx.api.serialization.label_mapper import LabelSchemaMapper, label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.exportable_code import demo +from otx.api.usecases.exportable_code.inference import IInferencer +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( + SegmentationToAnnotationConverter, +) +from otx.api.usecases.tasks.interfaces.deployment_interface import IDeploymentTask +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.optimization_interface import ( + IOptimizationTask, + OptimizationType, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-locals, too-many-statements, unused-argument +class OpenVINOSegmentationInferencer(IInferencer): + """Inferencer implementation for Segmentation using OpenVINO backend.""" + + def __init__( + self, + hparams: SegmentationConfig, + label_schema: LabelSchemaEntity, + model_file: Union[str, bytes], + weight_file: Union[str, bytes, None] = None, + device: str = "CPU", + num_requests: int = 1, + ): + """Inferencer implementation for Segmentation using OpenVINO backend. + + :param hparams: Hyper parameters that the model should use. + :param label_schema: LabelSchemaEntity that was used during model training. + :param model_file: Path to model to load, `.xml`, `.bin` or `.onnx` file. + :param device: Device to run inference on, such as CPU, GPU or MYRIAD. Defaults to "CPU". + :param num_requests: Maximum number of requests that the inferencer can make. + Good value is the number of available cores. Defaults to 1. + """ + + model_adapter = OpenvinoAdapter( + create_core(), + model_file, + weight_file, + device=device, + max_num_requests=num_requests, + plugin_config={"PERFORMANCE_HINT": "THROUGHPUT"}, + ) + self.configuration = { + **attr.asdict( + hparams.postprocessing, + filter=lambda attr, value: attr.name + not in ["header", "description", "type", "visible_in_ui", "class_name"], + ) + } + self.model = Model.create_model( + model_adapter, + "Segmentation", + self.configuration, + preload=True, + ) + self.converter = SegmentationToAnnotationConverter(label_schema) + self.callback_exceptions: List[Exception] = [] + self.model.inference_adapter.set_callback(self._async_callback) + + def predict(self, image: np.ndarray) -> Tuple[ImageResultWithSoftPrediction, AnnotationSceneEntity]: + """Perform a prediction for a given input image.""" + result = self.model(image) + return result, self.converter.convert_to_annotation(result) + + def enqueue_prediction(self, image: np.ndarray, id: int, result_handler: Any) -> None: + """Runs async inference.""" + if not self.model.is_ready(): + self.model.await_any() + image, metadata = self.model.preprocess(image) + callback_data = id, metadata, result_handler + self.model.inference_adapter.infer_async(image, callback_data) + + def await_all(self) -> None: + """Await all running infer requests if any.""" + self.model.await_all() + + def _async_callback(self, request: Any, callback_args: tuple) -> None: + """Fetches the results of async inference.""" + try: + id, preprocessing_meta, result_handler = callback_args + raw_prediction = self.model.inference_adapter.copy_raw_result(request) + processed_prediciton = self.model.postprocess(raw_prediction, preprocessing_meta) + annotation = self.converter.convert_to_annotation(processed_prediciton, preprocessing_meta) + result_handler(id, annotation, processed_prediciton.feature_vector, processed_prediciton.saliency_map) + + except Exception as e: + self.callback_exceptions.append(e) + + +class OpenVINOSegmentationTask(IDeploymentTask, IInferenceTask, IEvaluationTask, IOptimizationTask): + """Task implementation for Segmentation using OpenVINO backend.""" + + def __init__(self, task_environment: TaskEnvironment): + self.task_environment = task_environment + self.model = self.task_environment.model + self.model_name = self.task_environment.model_template.model_template_id + self.inferencer = self.load_inferencer() + self._avg_time_per_image: Optional[float] = None + + labels = task_environment.get_labels(include_empty=False) + self._label_dictionary = dict(enumerate(labels, 1)) + template_file_path = self.task_environment.model_template.model_template_path + self._base_dir = os.path.abspath(os.path.dirname(template_file_path)) + + @property + def hparams(self): + """Hparams of OpenVINO Segmentation Task.""" + return self.task_environment.get_hyper_parameters(SegmentationConfig) + + @property + def avg_time_per_image(self) -> Optional[float]: + """Average inference time per image.""" + return self._avg_time_per_image + + def load_inferencer(self) -> OpenVINOSegmentationInferencer: + """load_inferencer function of OpenVINO Segmentation Task.""" + if self.model is None: + raise RuntimeError("load_inferencer failed, model is None") + return OpenVINOSegmentationInferencer( + self.hparams, + self.task_environment.label_schema, + self.model.get_data("openvino.xml"), + self.model.get_data("openvino.bin"), + num_requests=get_default_async_reqs_num(), + ) + + def infer( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ) -> DatasetEntity: + """Infer function of OpenVINOSegmentationTask.""" + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress + dump_soft_prediction = not inference_parameters.is_evaluation + process_soft_prediction = inference_parameters.process_saliency_maps + enable_async_inference = inference_parameters.enable_async_inference + else: + update_progress_callback = default_progress_callback + dump_soft_prediction = True + process_soft_prediction = False + enable_async_inference = True + + def add_prediction( + id: int, + predicted_scene: AnnotationSceneEntity, + feature_vector: Union[np.ndarray, None], + soft_prediction: Union[np.ndarray, None], + ): + dataset_item = dataset[id] + dataset_item.append_annotations(predicted_scene.annotations) + + if feature_vector is not None: + feature_vector_media = TensorEntity(name="representation_vector", numpy=feature_vector.reshape(-1)) + dataset_item.append_metadata_item(feature_vector_media, model=self.model) + + if dump_soft_prediction and soft_prediction is not None and soft_prediction.ndim > 1: + for label_index, label in self._label_dictionary.items(): + current_label_soft_prediction = soft_prediction[:, :, label_index] + if process_soft_prediction: + current_label_soft_prediction = get_activation_map( + current_label_soft_prediction, normalize=False + ) + result_media = ResultMediaEntity( + name=label.name, + type="soft_prediction", + label=label, + annotation_scene=dataset_item.annotation_scene, + roi=dataset_item.roi, + numpy=current_label_soft_prediction, + ) + dataset_item.append_metadata_item(result_media, model=self.model) + + total_time = 0.0 + dataset_size = len(dataset) + for i, dataset_item in enumerate(dataset, 1): + start_time = time.perf_counter() + if enable_async_inference: + self.inferencer.enqueue_prediction(dataset_item.numpy, i - 1, add_prediction) + else: + result, predicted_scene = self.inferencer.predict(dataset_item.numpy) + add_prediction(i - 1, predicted_scene, result.feature_vector, result.saliency_map) + end_time = time.perf_counter() - start_time + total_time += end_time + + update_progress_callback(int(i / dataset_size * 100), None) + + self.inferencer.await_all() + + self._avg_time_per_image = total_time / len(dataset) + logger.info(f"Avg time per image: {self._avg_time_per_image} secs") + logger.info(f"Total time: {total_time} secs") + logger.info("Segmentation OpenVINO inference completed") + + return dataset + + def evaluate(self, output_resultset: ResultSetEntity, evaluation_metric: Optional[str] = None): + """Evaluate function of OpenVINOSegmentationTask.""" + logger.info("Computing mDice") + metrics = MetricsHelper.compute_dice_averaged_over_pixels(output_resultset) + logger.info(f"mDice after evaluation: {metrics.overall_dice.value}") + + output_resultset.performance = metrics.get_performance() + + def deploy(self, output_model: ModelEntity) -> None: + """Deploy function of OpenVINOSegmentationTask.""" + logger.info("Deploying the model") + if self.model is None: + raise RuntimeError("deploy failed, model is None") + + work_dir = os.path.dirname(demo.__file__) + parameters: Dict[str, Any] = {} + parameters["type_of_model"] = "Segmentation" + parameters["converter_type"] = "SEGMENTATION" + parameters["model_parameters"] = self.inferencer.configuration + parameters["model_parameters"]["labels"] = LabelSchemaMapper.forward(self.task_environment.label_schema) + + zip_buffer = io.BytesIO() + with ZipFile(zip_buffer, "w") as arch: + # model files + arch.writestr(os.path.join("model", "model.xml"), self.model.get_data("openvino.xml")) + arch.writestr(os.path.join("model", "model.bin"), self.model.get_data("openvino.bin")) + arch.writestr( + os.path.join("model", "config.json"), + json.dumps(parameters, ensure_ascii=False, indent=4), + ) + # model_wrappers files + for root, _, files in os.walk(os.path.dirname(model_wrappers.__file__)): + if "__pycache__" in root: + continue + for file in files: + file_path = os.path.join(root, file) + arch.write( + file_path, + os.path.join( + "python", + "model_wrappers", + file_path.split("model_wrappers/")[1], + ), + ) + # other python files + arch.write(os.path.join(work_dir, "requirements.txt"), os.path.join("python", "requirements.txt")) + arch.write(os.path.join(work_dir, "LICENSE"), os.path.join("python", "LICENSE")) + arch.write(os.path.join(work_dir, "demo.py"), os.path.join("python", "demo.py")) + arch.write(os.path.join(work_dir, "README.md"), os.path.join(".", "README.md")) + output_model.exportable_code = zip_buffer.getvalue() + logger.info("Deploying completed") + + def optimize( + self, + optimization_type: OptimizationType, + dataset: DatasetEntity, + output_model: ModelEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + ): + """Optimize function of OpenVINOSegmentationTask.""" + logger.info("Start PTQ optimization") + if self.model is None: + raise RuntimeError("PTQ optimize failed, model is None") + + if optimization_type is not OptimizationType.POT: + raise ValueError("PTQ is the only supported optimization type for OpenVino models") + + dataset = dataset.get_combined_subset([Subset.TRAINING, Subset.UNLABELED]) + data_loader = OTXOpenVinoDataLoader(dataset, self.inferencer) + quantization_dataset = nncf.Dataset(data_loader, lambda data: data[0]) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, "model.xml") + bin_path = os.path.join(tempdir, "model.bin") + with open(xml_path, "wb") as f: + f.write(self.model.get_data("openvino.xml")) + with open(bin_path, "wb") as f: + f.write(self.model.get_data("openvino.bin")) + + ov_model = ov.Core().read_model(xml_path) + if check_if_quantized(ov_model): + raise RuntimeError("Model is already optimized by PTQ") + + if optimization_parameters is not None: + optimization_parameters.update_progress(10, None) + + optimization_config_path = os.path.join(self._base_dir, "ptq_optimization_config.py") + ptq_config = ADDict() + if os.path.exists(optimization_config_path): + ptq_config = read_py_config(optimization_config_path) + ptq_config.update( + subset_size=min(self.hparams.pot_parameters.stat_subset_size, len(data_loader)), + preset=QuantizationPreset(self.hparams.pot_parameters.preset.name.lower()), + ) + + compressed_model = nncf.quantize( + ov_model, + quantization_dataset, + **ptq_config, + ) + + if optimization_parameters is not None: + optimization_parameters.update_progress(90, None) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, "model.xml") + ov.save_model(compressed_model, xml_path) + with open(xml_path, "rb") as f: + output_model.set_data("openvino.xml", f.read()) + with open(os.path.join(tempdir, "model.bin"), "rb") as f: + output_model.set_data("openvino.bin", f.read()) + + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self.task_environment.label_schema), + ) + + # set model attributes for quantized model + output_model.model_format = ModelFormat.OPENVINO + output_model.optimization_type = ModelOptimizationType.POT + output_model.optimization_methods = [OptimizationMethod.QUANTIZATION] + output_model.precision = [ModelPrecision.INT8] + + self.model = output_model + self.inferencer = self.load_inferencer() + + if optimization_parameters is not None: + optimization_parameters.update_progress(100, None) + logger.info("PTQ optimization completed") diff --git a/src/otx/algorithms/segmentation/configs/__init__.py b/src/otx/algorithms/segmentation/configs/__init__.py new file mode 100644 index 00000000000..5d6dcb495a6 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/__init__.py @@ -0,0 +1,4 @@ +"""Configurations for Segmentation.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/segmentation/configs/base/__init__.py b/src/otx/algorithms/segmentation/configs/base/__init__.py new file mode 100644 index 00000000000..2f7e5e0808c --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/__init__.py @@ -0,0 +1,19 @@ +"""Configs Initialization of OTX Segmentation.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import SegmentationConfig + +__all__ = ["SegmentationConfig"] diff --git a/src/otx/algorithms/segmentation/configs/base/configuration.py b/src/otx/algorithms/segmentation/configs/base/configuration.py new file mode 100644 index 00000000000..eb21e4b3761 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/configuration.py @@ -0,0 +1,154 @@ +"""Configuration file of OTX Segmentation.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +from attr import attrs + +from otx.algorithms.common.configs import ( + BaseConfig, + LearningRateSchedule, + POTQuantizationPreset, +) +from otx.api.configuration.elements import ( + add_parameter_group, + boolean_attribute, + configurable_boolean, + configurable_float, + configurable_integer, + selectable, + string_attribute, +) +from otx.api.configuration.model_lifecycle import ModelLifecycle + +# pylint: disable=invalid-name + + +@attrs +class SegmentationConfig(BaseConfig): + """Configurations of OTX Segmentation.""" + + header = string_attribute("Configuration for an object semantic segmentation task of OTX") + description = header + + @attrs + class __LearningParameters(BaseConfig.BaseLearningParameters): + header = string_attribute("Learning Parameters") + description = header + + learning_rate_schedule = selectable( + default_value=LearningRateSchedule.COSINE, + header="Learning rate schedule", + description="Specify learning rate scheduling for the MMDetection task. " + "When training for a small number of epochs (N < 10), the fixed " + "schedule is recommended. For training for 10 < N < 25 epochs, " + "step-wise or exponential annealing might give better results. " + "Finally, for training on large datasets for at least 20 " + "epochs, cyclic annealing could result in the best model.", + editable=True, + visible_in_ui=True, + ) + + @attrs + class __AlgoBackend(BaseConfig.BaseAlgoBackendParameters): + header = string_attribute("Parameters for the OTX algo-backend") + description = header + + @attrs + class __Postprocessing(BaseConfig.BasePostprocessing): + header = string_attribute("Postprocessing") + description = header + + blur_strength = configurable_integer( + header="Blur strength", + description="With a higher value, the segmentation output will be smoother, but less accurate.", + default_value=1, + min_value=1, + max_value=25, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + soft_threshold = configurable_float( + default_value=0.5, + header="Soft threshold", + description="The threshold to apply to the probability output of the model, for each pixel. A higher value " + "means a stricter segmentation prediction.", + min_value=0.0, + max_value=1.0, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + @attrs + class __POTParameter(BaseConfig.BasePOTParameter): + header = string_attribute("POT Parameters") + description = header + visible_in_ui = boolean_attribute(False) + + stat_subset_size = configurable_integer( + header="Number of data samples", + description="Number of data samples used for post-training optimization", + default_value=300, + min_value=1, + max_value=1000, + ) + + preset = selectable( + default_value=POTQuantizationPreset.PERFORMANCE, + header="Preset", + description="Quantization preset that defines quantization scheme", + editable=False, + visible_in_ui=False, + ) + + @attrs + class __NNCFOptimization(BaseConfig.BaseNNCFOptimization): + header = string_attribute("Optimization by NNCF") + description = header + visible_in_ui = boolean_attribute(False) + + enable_quantization = configurable_boolean( + default_value=True, + header="Enable quantization algorithm", + description="Enable quantization algorithm", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + enable_pruning = configurable_boolean( + default_value=False, + header="Enable filter pruning algorithm", + description="Enable filter pruning algorithm", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + pruning_supported = configurable_boolean( + default_value=False, + header="Whether filter pruning is supported", + description="Whether filter pruning is supported", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + maximal_accuracy_degradation = configurable_float( + default_value=1.0, + min_value=0.0, + max_value=100.0, + header="Maximum accuracy degradation", + description="The maximal allowed accuracy metric drop", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + learning_parameters = add_parameter_group(__LearningParameters) + postprocessing = add_parameter_group(__Postprocessing) + algo_backend = add_parameter_group(__AlgoBackend) + nncf_optimization = add_parameter_group(__NNCFOptimization) + pot_parameters = add_parameter_group(__POTParameter) diff --git a/src/otx/algorithms/segmentation/configs/base/data/__init__.py b/src/otx/algorithms/segmentation/configs/base/data/__init__.py new file mode 100644 index 00000000000..2593a7e6a22 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/data/__init__.py @@ -0,0 +1,14 @@ +"""Base data pipeline configurations folder.""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/base/data/data_pipeline.py b/src/otx/algorithms/segmentation/configs/base/data/data_pipeline.py new file mode 100644 index 00000000000..fb3ae33521e --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/data/data_pipeline.py @@ -0,0 +1,91 @@ +"""Data Pipeline for Cls-Incr model of Segmentation Task.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) +__img_scale = (544, 544) +__crop_size = (512, 512) + +train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict(type="LoadAnnotationFromOTXDataset"), + resize_cfg=dict( + type="Resize", + img_scale=__img_scale, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="Resize", img_scale=__img_scale, ratio_range=(0.5, 2.0)), + dict(type="RandomCrop", crop_size=__crop_size, cat_max_ratio=0.75), + dict(type="RandomFlip", prob=0.5, direction="horizontal"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__crop_size, pad_val=0, seg_pad_val=255), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_semantic_seg"], + meta_keys=[ + "ori_shape", + "pad_shape", + "ori_filename", + "filename", + "scale_factor", + "flip", + "img_norm_cfg", + "flip_direction", + "ignored_labels", + "img_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", img_scale=__img_scale, keep_ratio=False), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_scale, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict( + type="Collect", + keys=["img"], + ), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_scale, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict( + type="Collect", + keys=["img"], + ), + ], + ), +] + +data = dict( + train=dict(type="OTXSegDataset", pipeline=train_pipeline), + val=dict(type="OTXSegDataset", pipeline=val_pipeline), + test=dict(type="OTXSegDataset", pipeline=test_pipeline), +) diff --git a/src/otx/algorithms/segmentation/configs/base/data/selfsl/__init__.py b/src/otx/algorithms/segmentation/configs/base/data/selfsl/__init__.py new file mode 100644 index 00000000000..2c0f4bc1598 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/data/selfsl/__init__.py @@ -0,0 +1,14 @@ +"""Self-SL data pipeline configurations folder.""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/base/data/selfsl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/base/data/selfsl/data_pipeline.py new file mode 100644 index 00000000000..d8bfec9f658 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/data/selfsl/data_pipeline.py @@ -0,0 +1,79 @@ +"""Data Pipeline for Self-SL model of Segmentation Task.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__resize_target_size = (224, 224) +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +__train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict(type="LoadAnnotationFromOTXDataset"), + resize_cfg=dict( + type="Resize", + img_scale=__resize_target_size, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + enable_memcache=True, # Cache after resizing image & annotations + ), + dict( + type="TwoCropTransform", + view0=[ + dict(type="NDArrayToPILImage", keys=["img"]), + dict(type="RandomResizedCrop", size=__resize_target_size), + dict( + type="RandomColorJitter", + brightness=0.4, + contrast=0.4, + saturation=0.2, + hue=0.1, + p=0.8, + ), + dict(type="RandomGrayscale", p=0.2), + dict(type="RandomGaussianBlur", kernel_size=23, p=1.0), + dict(type="PILImageToNDArray", keys=["img"]), + dict(type="RandomFlip", prob=0.5, direction="horizontal"), + dict(type="Normalize", **__img_norm_cfg), + ], + view1=[ + dict(type="NDArrayToPILImage", keys=["img"]), + dict(type="RandomResizedCrop", size=__resize_target_size), + dict( + type="RandomColorJitter", + brightness=0.4, + contrast=0.4, + saturation=0.2, + hue=0.1, + p=0.8, + ), + dict(type="RandomGrayscale", p=0.2), + dict(type="RandomGaussianBlur", kernel_size=23, p=0.1), + dict(type="PILImageToNDArray", keys=["img"]), + dict(type="RandomSolarization", threshold=128, p=0.2), + dict(type="RandomFlip", prob=0.5, direction="horizontal"), + dict(type="Normalize", **__img_norm_cfg), + ], + ), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_semantic_seg"], + meta_keys=[ + "ori_shape", + "pad_shape", + "ori_filename", + "filename", + "scale_factor", + "flip", + "img_norm_cfg", + "flip_direction", + "ignored_labels", + "img_shape", + ], + ), +] + +data = dict(train=dict(type="OTXSegDataset", pipeline=__train_pipeline)) diff --git a/src/otx/algorithms/segmentation/configs/base/data/semisl/__init__.py b/src/otx/algorithms/segmentation/configs/base/data/semisl/__init__.py new file mode 100644 index 00000000000..477d7c58d8c --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/data/semisl/__init__.py @@ -0,0 +1,4 @@ +"""Semi-SL data pipeline configuration for segmenation.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/segmentation/configs/base/data/semisl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/base/data/semisl/data_pipeline.py new file mode 100644 index 00000000000..99999a10cc0 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/data/semisl/data_pipeline.py @@ -0,0 +1,128 @@ +"""Data Pipeline for Semi-SL model of Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) +__img_scale = (544, 544) +__crop_size = (512, 512) + +__common_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict(type="LoadAnnotationFromOTXDataset"), + resize_cfg=dict( + type="Resize", + img_scale=__img_scale, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + enable_memcache=True, # Cache after resizing image & annotations + ), + dict(type="Resize", img_scale=__img_scale, ratio_range=(0.5, 2.0), keep_ratio=False), + dict(type="RandomCrop", crop_size=__crop_size, cat_max_ratio=0.75), + dict(type="RandomFlip", prob=0.5, direction="horizontal"), + dict(type="RandomRotate", prob=0.5, degree=30, pad_val=0, seg_pad_val=255), +] + +train_pipeline = [ + *__common_pipeline, + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__crop_size, pad_val=0, seg_pad_val=255), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_semantic_seg"], + meta_keys=[ + "ori_shape", + "pad_shape", + "ori_filename", + "filename", + "scale_factor", + "flip", + "img_norm_cfg", + "flip_direction", + "ignored_labels", + "img_shape", + ], + ), +] + +val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=__img_scale, + keep_ratio=False, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image & annotations + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_scale, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict( + type="Collect", + keys=["img"], + ), + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_scale, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict( + type="Collect", + keys=["img"], + ), + ], + ), +] + +unlabeled_pipeline = [ + *__common_pipeline, + dict(type="Pad", size=__crop_size, pad_val=0, seg_pad_val=255), + dict(type="BranchImage", key_map=dict(img="ul_w_img")), + dict( + type="ProbCompose", + probs=[0.7, 0.3], + transforms=[ + dict( + type="PhotoMetricDistortion", + brightness_delta=32, + contrast_range=(0.5, 1.5), + saturation_range=(0.5, 1.5), + hue_delta=18, + ), + dict(type="RGB2Gray"), + ], + ), + dict(type="RandomCutOut", prob=0.35, n_holes=(1, 8), cutout_ratio=(0.2, 0.4)), + dict(type="Normalize", **__img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "ul_w_img"], + ), +] + +data = dict( + train=dict(type="OTXSegDataset", pipeline=train_pipeline), + val=dict(type="OTXSegDataset", pipeline=val_pipeline), + test=dict(type="OTXSegDataset", pipeline=test_pipeline), + unlabeled=dict(type="OTXSegDataset", pipeline=unlabeled_pipeline), +) diff --git a/src/otx/algorithms/segmentation/configs/base/data/supcon/__init__.py b/src/otx/algorithms/segmentation/configs/base/data/supcon/__init__.py new file mode 100644 index 00000000000..1b19d59ff9a --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/data/supcon/__init__.py @@ -0,0 +1,14 @@ +"""SupCon data pipeline configurations folder.""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/base/data/supcon/data_pipeline.py b/src/otx/algorithms/segmentation/configs/base/data/supcon/data_pipeline.py new file mode 100644 index 00000000000..0bcb02da1af --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/data/supcon/data_pipeline.py @@ -0,0 +1,130 @@ +"""Data Pipeline for SupCon model of Segmentation Task.""" + +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) +__img_scale = (544, 544) +__resize_target_size = (512, 512) + +__train_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + load_ann_cfg=dict(type="LoadAnnotationFromOTXDataset"), + resize_cfg=dict( + type="Resize", + img_scale=__img_scale, + downscale_only=True, + ), # Resize to intermediate size if org image is bigger + enable_memcache=True, # Cache after resizing image & annotations + ), + dict( + type="TwoCropTransform", + view0=[ + dict(type="NDArrayToPILImage", keys=["img"]), + dict(type="RandomResizedCrop", size=__resize_target_size), + dict( + type="RandomColorJitter", + brightness=0.4, + contrast=0.4, + saturation=0.2, + hue=0.1, + p=0.8, + ), + dict(type="RandomGrayscale", p=0.2), + dict(type="RandomGaussianBlur", kernel_size=23, p=1.0), + dict(type="PILImageToNDArray", keys=["img"]), + dict(type="RandomFlip", prob=0.5, direction="horizontal"), + dict(type="Normalize", **__img_norm_cfg), + ], + view1=[ + dict(type="NDArrayToPILImage", keys=["img"]), + dict(type="RandomResizedCrop", size=__resize_target_size), + dict( + type="RandomColorJitter", + brightness=0.4, + contrast=0.4, + saturation=0.2, + hue=0.1, + p=0.8, + ), + dict(type="RandomGrayscale", p=0.2), + dict(type="RandomGaussianBlur", kernel_size=23, p=0.1), + dict(type="PILImageToNDArray", keys=["img"]), + dict(type="RandomSolarization", threshold=128, p=0.2), + dict(type="RandomFlip", prob=0.5, direction="horizontal"), + dict(type="Normalize", **__img_norm_cfg), + ], + ), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_semantic_seg"], + meta_keys=[ + "ori_shape", + "pad_shape", + "ori_filename", + "filename", + "scale_factor", + "flip", + "img_norm_cfg", + "flip_direction", + "ignored_labels", + "img_shape", + ], + ), +] + +__val_pipeline = [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict( + type="Resize", + img_scale=__img_scale, + keep_ratio=False, + downscale_only=False, + ), + enable_memcache=True, # Cache after resizing image + ), + dict( + type="MultiScaleFlipAug", + img_scale=__img_scale, + flip=False, + transforms=[ + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict( + type="Collect", + keys=["img"], + ), + ], + ), +] + +__test_pipeline = [ + dict(type="LoadImageFromOTXDataset"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_scale, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict( + type="Collect", + keys=["img"], + ), + ], + ), +] + +data = dict( + train=dict(type="OTXSegDataset", pipeline=__train_pipeline), + val=dict(type="OTXSegDataset", pipeline=__val_pipeline), + test=dict(type="OTXSegDataset", pipeline=__test_pipeline), +) diff --git a/src/otx/algorithms/segmentation/configs/base/deployments/__init__.py b/src/otx/algorithms/segmentation/configs/base/deployments/__init__.py new file mode 100644 index 00000000000..5d836e60eb7 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/deployments/__init__.py @@ -0,0 +1,4 @@ +"""Base deployment configuration for segmenation.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/segmentation/configs/base/deployments/base_segmentation_dynamic.py b/src/otx/algorithms/segmentation/configs/base/deployments/base_segmentation_dynamic.py new file mode 100644 index 00000000000..123ae602fcb --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/deployments/base_segmentation_dynamic.py @@ -0,0 +1,18 @@ +"""Segmentation models dynamic deploy config.""" + +_base_ = ["./base_segmentation_static.py"] + +ir_config = dict( + dynamic_axes={ + "input": { + 0: "batch", + 2: "height", + 3: "width", + }, + "output": { + 0: "batch", + 2: "height", + 3: "width", + }, + }, +) diff --git a/src/otx/algorithms/segmentation/configs/base/deployments/base_segmentation_static.py b/src/otx/algorithms/segmentation/configs/base/deployments/base_segmentation_static.py new file mode 100644 index 00000000000..7aa1339ea93 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/base/deployments/base_segmentation_static.py @@ -0,0 +1,31 @@ +"""Segmentation models static deploy config.""" + +ir_config = dict( + type="onnx", + export_params=True, + keep_initializers_as_inputs=False, + opset_version=11, + save_file="end2end.onnx", + input_names=["input"], + output_names=["output"], + input_shape=None, + # TODO + # optimizing onnx graph mess up NNCF graph at some point + # where we need to look into + optimize=False, +) + +codebase_config = dict( + type="mmseg", + task="Segmentation", +) + +backend_config = dict( + type="openvino", + mo_options=None, +) + +input_data = dict( + shape=(128, 128, 3), + file_path=None, +) diff --git a/src/otx/algorithms/segmentation/configs/configuration.yaml b/src/otx/algorithms/segmentation/configs/configuration.yaml new file mode 100644 index 00000000000..fa835827add --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/configuration.yaml @@ -0,0 +1,473 @@ +description: Configuration for an semantic segmentation task +header: Configuration for an semantic segmentation task +id: "" +learning_parameters: + batch_size: + affects_outcome_of: TRAINING + default_value: 5 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 5 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE + description: Learning Parameters + header: Learning Parameters + learning_rate: + affects_outcome_of: TRAINING + default_value: 0.01 + description: + Increasing this value will speed up training convergence but might + make it unstable. + editable: true + header: Learning rate + max_value: 0.1 + min_value: 1.0e-07 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.001 + visible_in_ui: true + warning: null + auto_hpo_state: NOT_POSSIBLE + learning_rate_warmup_iters: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 100 + description: + In this periods of initial training iterations, the model will be trained in low learning rate, + which will be increased incrementally up to the expected learning rate setting. + This warm-up phase is known to be helpful to stabilize training, thus result in better performance. + editable: true + header: Number of iterations for learning rate warmup + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: null + num_iters: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 1 + description: + Increasing this value causes the results to be more robust but training + time will be longer. + editable: true + header: Number of training iterations + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: null + num_workers: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 2 + description: + Increasing this value might improve training speed however it might + cause out of memory errors. If the number of workers is set to zero, data loading + will happen in the main training thread. + editable: true + header: Number of cpu threads to use during batch generation + max_value: 8 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: null + enable_early_stopping: + affects_outcome_of: TRAINING + default_value: true + description: Early exit from training when validation accuracy isn't changed or decreased for several epochs. + editable: true + header: Enable early stopping of the training + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + early_stop_start: + affects_outcome_of: TRAINING + default_value: 3 + editable: true + header: Start epoch for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 3 + visible_in_ui: false + early_stop_patience: + affects_outcome_of: TRAINING + default_value: 5 + description: Training will stop if the model does not improve within the number of epochs of patience. + editable: true + header: Patience for early stopping + max_value: 50 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 5 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + early_stop_iteration_patience: + affects_outcome_of: TRAINING + default_value: 0 + description: + Training will stop if the model does not improve within the number of iterations of patience. + This ensures the model is trained enough with the number of iterations of patience before early stopping. + editable: true + header: Iteration patience for early stopping + max_value: 1000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: This is applied exclusively when early stopping is enabled. + enable_supcon: + affects_outcome_of: TRAINING + default_value: false + description: + Enable an auxiliar supervised contrastive loss, which might increase robustness + and accuracy for small datasets. + editable: true + header: Enable Supervised Contrastive helper loss + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + auto_adapt_batch_size: + affects_outcome_of: TRAINING + default_value: Safe + description: Safe => Prevent GPU out of memory. Full => Find a batch size using most of GPU memory. + editable: true + enum_name: BatchSizeAdaptType + header: Decrease batch size if current batch size isn't fit to CUDA memory. + options: + NONE: "None" + SAFE: "Safe" + FULL: "Full" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Safe + visible_in_ui: true + warning: + Enabling this could change the actual batch size depending on the current GPU status. + The learning rate also could be adjusted according to the adapted batch size. This process might change + a model performance and take some extra computation time to try a few batch size candidates. + auto_num_workers: + affects_outcome_of: TRAINING + default_value: false + description: Adapt num_workers according to current hardware status automatically. + editable: true + header: Enable auto adaptive num_workers + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: true + warning: null + input_size: + affects_outcome_of: INFERENCE + default_value: Auto + description: + The input size of the given model could be configured to one of the predefined resolutions. + Reduced training and inference time could be expected by using smaller input size. + Defaults to Auto, in which input size is automatically determined based on dataset statistics. + editable: true + enum_name: InputSizePreset + header: Configure model input size. + options: + DEFAULT: "Default" + AUTO: "Auto" + _256x256: "256x256" + _384x384: "384x384" + _512x512: "512x512" + _768x768: "768x768" + _1024x1024: "1024x1024" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Default + visible_in_ui: false + warning: Modifying input size may decrease model performance. + type: PARAMETER_GROUP + visible_in_ui: true +postprocessing: + confidence_threshold: + affects_outcome_of: INFERENCE + default_value: 0.35 + description: + This threshold only takes effect if the threshold is not set based + on the result. + editable: true + header: Confidence threshold + max_value: 1 + min_value: 0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.35 + visible_in_ui: true + warning: null + description: Postprocessing + header: Postprocessing + result_based_confidence_threshold: + affects_outcome_of: INFERENCE + default_value: true + description: Confidence threshold is derived from the results + editable: true + header: Result based confidence threshold + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +algo_backend: + description: parameters for algo backend + header: Algo backend parameters + train_type: + affects_outcome_of: TRAINING + default_value: Incremental + description: Training scheme option that determines how to train the model + editable: True + enum_name: TrainType + header: Train type + options: + Incremental: "Incremental" + Semisupervised: "Semisupervised" + Selfsupervised: "Selfsupervised" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Incremental + visible_in_ui: false + warning: null + mem_cache_size: + affects_outcome_of: TRAINING + default_value: 1000000000 + description: Size of memory pool for caching decoded data to load data faster (bytes). + editable: true + header: Size of memory pool + max_value: 9223372036854775807 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + storage_cache_scheme: + affects_outcome_of: TRAINING + default_value: NONE + description: Scheme for storage cache + editable: true + enum_name: StorageCacheScheme + header: Scheme for storage cache + options: + NONE: "NONE" + AS_IS: "AS-IS" + JPEG_75: "JPEG/75" + JPEG_95: "JPEG/95" + PNG: "PNG" + TIFF: "TIFF" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + default_value: Performance + description: Quantization preset that defines quantization scheme + editable: True + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Performance + visible_in_ui: True + warning: null + stat_subset_size: + affects_outcome_of: NONE + default_value: 300 + description: Number of data samples used for post-training optimization + editable: True + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: True + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +nncf_optimization: + description: Optimization by NNCF + header: Optimization by NNCF + enable_quantization: + affects_outcome_of: INFERENCE + default_value: True + description: Enable quantization algorithm + editable: false + header: Enable quantization algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: false + warning: null + enable_pruning: + affects_outcome_of: INFERENCE + default_value: false + description: Enable filter pruning algorithm + editable: true + header: Enable filter pruning algorithm + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + pruning_supported: + affects_outcome_of: TRAINING + default_value: false + description: Whether filter pruning is supported + editable: false + header: Whether filter pruning is supported + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: false + warning: null + maximal_accuracy_degradation: + affects_outcome_of: NONE + default_value: 1.0 + description: The maximal allowed accuracy metric drop in absolute values + editable: True + header: Maximum accuracy degradation + max_value: 100.0 + min_value: 0.0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: True + warning: null + type: PARAMETER_GROUP + visible_in_ui: True diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/__init__.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/__init__.py new file mode 100644 index 00000000000..5b90f450f89 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of SegNext-B model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/compression_config.json b/src/otx/algorithms/segmentation/configs/ham_segnext_b/compression_config.json new file mode 100644 index 00000000000..623a203e955 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/compression_config.json @@ -0,0 +1,50 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mDice", + "input_info": { + "sample_size": [1, 3, 512, 512] + }, + "compression": [], + "log_dir": "/tmp" + }, + "params_config": { + "iters": 0, + "open_layers": [] + }, + "checkpoint_config": { + "interval": -1 + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 1e-4 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 500 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 500 + } + }, + "ignored_scopes": [] + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 1.0, + "maximal_total_epochs": 40 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/data_pipeline.py new file mode 100644 index 00000000000..beb5ca35318 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of SegNext model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/deployment.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/deployment.py new file mode 100644 index 00000000000..19d6723d951 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/deployment.py @@ -0,0 +1,11 @@ +"""MMDeploy config of SegNext-B model for Segmentation Task.""" + +_base_ = ["../base/deployments/base_segmentation_dynamic.py"] + +ir_config = dict( + output_names=["output"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 544, 544]))], +) diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/hpo_config.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_b/hpo_config.yaml new file mode 100644 index 00000000000..4ec70db652f --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: mDice +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.00005 + - 0.005 + - 0.00001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 6 + - 12 + - 2 diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/model.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/model.py new file mode 100644 index 00000000000..99354c95284 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/model.py @@ -0,0 +1,53 @@ +"""Model configuration of SegNext-B model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../recipes/stages/segmentation/incremental_poly.py", + "../../../common/adapters/mmcv/configs/backbones/segnext.py", +] + +ham_norm_cfg = dict(type="GN", num_groups=32, requires_grad=True) +model = dict( + type="OTXEncoderDecoder", + backbone=dict( + embed_dims=[64, 128, 320, 512], + depths=[3, 3, 12, 3], + drop_path_rate=0.1, + norm_cfg=dict(type="BN", requires_grad=True), + ), + decode_head=dict( + type="LightHamHead", + input_transform="multiple_select", + in_channels=[128, 320, 512], + in_index=[1, 2, 3], + channels=512, + ham_channels=512, + dropout_ratio=0.1, + num_classes=150, + norm_cfg=ham_norm_cfg, + align_corners=False, + loss_decode=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + ham_kwargs=dict(MD_S=1, MD_R=16, train_steps=6, in_channels=512, eval_steps=7, inv_t=100), + ), + # model training and testing settings + train_cfg=dict(mix_loss=dict(enable=False, weight=0.1)), +) + +optimizer = dict(paramwise_cfg=dict(custom_keys={"pos_block": dict(decay_mult=0.0), "norm": dict(decay_mult=0.0)})) + +load_from = "https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_b_20230227-3ab7d230.pth" diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/ptq_optimization_config.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/ptq_optimization_config.py new file mode 100644 index 00000000000..db8a1682c3b --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/ptq_optimization_config.py @@ -0,0 +1,12 @@ +"""PTQ config file.""" +from nncf import IgnoredScope + +ignored_scope = IgnoredScope( + patterns=["/hamburger/"], + types=[ + "Add", + "MVN", + "Divide", + "Multiply", + ], +) diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/__init__.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/__init__.py new file mode 100644 index 00000000000..fb71121089c --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of SegNext-B model for Self-SL Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/data_pipeline.py new file mode 100644 index 00000000000..36a050464fc --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of SegNext model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/hparam.yaml new file mode 100644 index 00000000000..a0c8d7feea3 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/hparam.yaml @@ -0,0 +1,17 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 32 + learning_rate: + default_value: 0.0001 + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 10 + enable_early_stopping: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/model.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/model.py new file mode 100644 index 00000000000..6376f0de6a4 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/selfsl/model.py @@ -0,0 +1,53 @@ +"""Model configuration of SegNext-B model for Self-SL Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/selfsl.py", + "../../../../common/adapters/mmcv/configs/backbones/segnext.py", +] + + +model = dict( + type="DetConB", + pretrained="https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_b_20230227-3ab7d230.pth", + num_classes=256, + num_samples=16, + downsample=8, + input_transform="resize_concat", + in_index=[1, 2, 3], + backbone=dict( + embed_dims=[64, 128, 320, 512], + depths=[3, 3, 12, 3], + drop_path_rate=0.1, + norm_cfg=dict(type="BN", requires_grad=True), + ), + neck=dict( + type="SelfSLMLP", + in_channels=960, + hid_channels=1920, + out_channels=256, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=256, + hid_channels=1920, + out_channels=256, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), +) + +optimizer = dict(paramwise_cfg=dict(custom_keys={"pos_block": dict(decay_mult=0.0), "norm": dict(decay_mult=0.0)})) +load_from = None +resume_from = None +fp16 = None diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/__init__.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/__init__.py new file mode 100644 index 00000000000..817a3e0cb81 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of SegNext-s model for Semi-SL Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/data_pipeline.py new file mode 100644 index 00000000000..8da1d207460 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of SegNext model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/hparam.yaml new file mode 100644 index 00000000000..580462daa1e --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/model.py b/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/model.py new file mode 100644 index 00000000000..621d5cb671c --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/semisl/model.py @@ -0,0 +1,70 @@ +"""Semi-SL model configuration of SegNext-s model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/semisl_poly.py", + "../../../../common/adapters/mmcv/configs/backbones/segnext.py", +] + +ham_norm_cfg = dict(type="GN", num_groups=32, requires_grad=True) +norm_cfg = dict(type="BN", requires_grad=True) +model = dict( + type="MeanTeacherSegmentor", + orig_type="OTXEncoderDecoder", + unsup_weight=0.1, + proto_weight=0.1, + semisl_start_epoch=1, + train_cfg=dict(mix_loss=dict(enable=False, weight=0.1)), + test_cfg=dict(mode="whole", output_scale=5.0), + backbone=dict( + embed_dims=[64, 128, 320, 512], + depths=[3, 3, 12, 3], + drop_path_rate=0.1, + norm_cfg=dict(type="BN", requires_grad=True), + ), + decode_head=dict( + type="LightHamHead", + input_transform="multiple_select", + in_channels=[128, 320, 512], + in_index=[1, 2, 3], + channels=512, + ham_channels=512, + dropout_ratio=0.1, + num_classes=150, + norm_cfg=ham_norm_cfg, + align_corners=False, + loss_decode=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + ham_kwargs=dict(MD_S=1, MD_R=16, train_steps=6, in_channels=512, eval_steps=7, inv_t=100), + ), + proto_head=dict( + in_channels=512, + channels=512, + norm_cfg=norm_cfg, + dropout_ratio=0.1, + align_corners=False, + gamma=0.999, + num_prototype=4, + in_proto_channels=512, + loss_decode=dict(type="PixelPrototypeCELoss", loss_ppc_weight=0.01, loss_ppd_weight=0.001, ignore_index=255), + ) + # model training and testing settings +) + +load_from = "https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_b_20230227-3ab7d230.pth" +optimizer = dict(paramwise_cfg=dict(custom_keys={"pos_block": dict(decay_mult=0.0), "norm": dict(decay_mult=0.0)})) +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/template.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_b/template.yaml new file mode 100644 index 00000000000..50ec187dd37 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/template.yaml @@ -0,0 +1,65 @@ +# Description. +model_template_id: Custom_Semantic_Segmentation_SegNext_B +name: SegNext-B +task_type: SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Semantic Segmentation with larger architecture which based on the MSCAN backbone for the better accuracy. +application: ~ + +# Algo backend. +framework: OTXSegmentation v0.14.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.segmentation.adapters.mmseg.task.MMSegmentationTask + openvino: otx.algorithms.segmentation.adapters.openvino.task.OpenVINOSegmentationTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.00008 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 20 + num_iters: + default_value: 150 + early_stop_start: + default_value: 15 + early_stop_patience: + default_value: 10 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + pot_parameters: + preset: + default_value: Mixed + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 32.08 +size: 27.56 diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/__init__.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/__init__.py new file mode 100644 index 00000000000..7af095f91c3 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of SegNext-S model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/compression_config.json b/src/otx/algorithms/segmentation/configs/ham_segnext_s/compression_config.json new file mode 100644 index 00000000000..623a203e955 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/compression_config.json @@ -0,0 +1,50 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mDice", + "input_info": { + "sample_size": [1, 3, 512, 512] + }, + "compression": [], + "log_dir": "/tmp" + }, + "params_config": { + "iters": 0, + "open_layers": [] + }, + "checkpoint_config": { + "interval": -1 + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 1e-4 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 500 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 500 + } + }, + "ignored_scopes": [] + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 1.0, + "maximal_total_epochs": 40 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/data_pipeline.py new file mode 100644 index 00000000000..beb5ca35318 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of SegNext model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/deployment.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/deployment.py new file mode 100644 index 00000000000..1db60d1b0dd --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/deployment.py @@ -0,0 +1,11 @@ +"""MMDeploy config of SegNext-S model for Segmentation Task.""" + +_base_ = ["../base/deployments/base_segmentation_dynamic.py"] + +ir_config = dict( + output_names=["output"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 544, 544]))], +) diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/hpo_config.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_s/hpo_config.yaml new file mode 100644 index 00000000000..4ec70db652f --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: mDice +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.00005 + - 0.005 + - 0.00001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 6 + - 12 + - 2 diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/model.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/model.py new file mode 100644 index 00000000000..35bcb9b64e1 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/model.py @@ -0,0 +1,52 @@ +"""Model configuration of SegNext-S model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../recipes/stages/segmentation/incremental_poly.py", + "../../../common/adapters/mmcv/configs/backbones/segnext.py", +] + +ham_norm_cfg = dict(type="GN", num_groups=32, requires_grad=True) +model = dict( + type="OTXEncoderDecoder", + backbone=dict( + embed_dims=[64, 128, 320, 512], + depths=[2, 2, 4, 2], + norm_cfg=dict(type="BN", requires_grad=True), + ), + decode_head=dict( + type="LightHamHead", + input_transform="multiple_select", + in_channels=[128, 320, 512], + in_index=[1, 2, 3], + channels=256, + ham_channels=256, + dropout_ratio=0.1, + num_classes=150, + norm_cfg=ham_norm_cfg, + align_corners=False, + loss_decode=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + ham_kwargs=dict(MD_S=1, MD_R=16, train_steps=6, in_channels=256, eval_steps=7, inv_t=100), + ), + # model training and testing settings + train_cfg=dict(mix_loss=dict(enable=False, weight=0.1)), +) + +optimizer = dict(paramwise_cfg=dict(custom_keys={"pos_block": dict(decay_mult=0.0), "norm": dict(decay_mult=0.0)})) + +load_from = "https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_s_20230227-f33ccdf2.pth" diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/ptq_optimization_config.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/ptq_optimization_config.py new file mode 100644 index 00000000000..db8a1682c3b --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/ptq_optimization_config.py @@ -0,0 +1,12 @@ +"""PTQ config file.""" +from nncf import IgnoredScope + +ignored_scope = IgnoredScope( + patterns=["/hamburger/"], + types=[ + "Add", + "MVN", + "Divide", + "Multiply", + ], +) diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/__init__.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/__init__.py new file mode 100644 index 00000000000..6244f3289e4 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of SegNext-S model for Self-SL Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/data_pipeline.py new file mode 100644 index 00000000000..36a050464fc --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of SegNext model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/hparam.yaml new file mode 100644 index 00000000000..a0c8d7feea3 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/hparam.yaml @@ -0,0 +1,17 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 32 + learning_rate: + default_value: 0.0001 + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 10 + enable_early_stopping: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/model.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/model.py new file mode 100644 index 00000000000..c017b7573bf --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/selfsl/model.py @@ -0,0 +1,52 @@ +"""Model configuration of SegNext-S model for Self-SL Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/selfsl.py", + "../../../../common/adapters/mmcv/configs/backbones/segnext.py", +] + + +model = dict( + type="DetConB", + pretrained="https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_s_20230227-f33ccdf2.pth", + num_classes=256, + num_samples=16, + downsample=8, + input_transform="resize_concat", + in_index=[1, 2, 3], + backbone=dict( + embed_dims=[64, 128, 320, 512], + depths=[2, 2, 4, 2], + norm_cfg=dict(type="BN", requires_grad=True), + ), + neck=dict( + type="SelfSLMLP", + in_channels=960, + hid_channels=1920, + out_channels=256, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=256, + hid_channels=1920, + out_channels=256, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), +) + +optimizer = dict(paramwise_cfg=dict(custom_keys={"pos_block": dict(decay_mult=0.0), "norm": dict(decay_mult=0.0)})) +load_from = None +resume_from = None +fp16 = None diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/__init__.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/__init__.py new file mode 100644 index 00000000000..817a3e0cb81 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of SegNext-s model for Semi-SL Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/data_pipeline.py new file mode 100644 index 00000000000..8da1d207460 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of SegNext model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/hparam.yaml new file mode 100644 index 00000000000..580462daa1e --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/model.py b/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/model.py new file mode 100644 index 00000000000..f9324e54ec2 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/semisl/model.py @@ -0,0 +1,69 @@ +"""Semi-SL model configuration of SegNext-s model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/semisl_poly.py", + "../../../../common/adapters/mmcv/configs/backbones/segnext.py", +] + +ham_norm_cfg = dict(type="GN", num_groups=32, requires_grad=True) +norm_cfg = dict(type="BN", requires_grad=True) +model = dict( + type="MeanTeacherSegmentor", + orig_type="OTXEncoderDecoder", + unsup_weight=0.1, + proto_weight=0.1, + semisl_start_epoch=1, + train_cfg=dict(mix_loss=dict(enable=False, weight=0.1)), + test_cfg=dict(mode="whole", output_scale=5.0), + backbone=dict( + embed_dims=[64, 128, 320, 512], + depths=[2, 2, 4, 2], + norm_cfg=dict(type="BN", requires_grad=True), + ), + decode_head=dict( + type="LightHamHead", + input_transform="multiple_select", + in_channels=[128, 320, 512], + in_index=[1, 2, 3], + channels=256, + ham_channels=256, + dropout_ratio=0.1, + num_classes=150, + norm_cfg=ham_norm_cfg, + align_corners=False, + loss_decode=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + ham_kwargs=dict(MD_S=1, MD_R=16, train_steps=6, in_channels=256, eval_steps=7, inv_t=100), + ), + proto_head=dict( + in_channels=256, + channels=256, + norm_cfg=norm_cfg, + dropout_ratio=0.1, + align_corners=False, + gamma=0.999, + num_prototype=4, + in_proto_channels=256, + loss_decode=dict(type="PixelPrototypeCELoss", loss_ppc_weight=0.01, loss_ppd_weight=0.001, ignore_index=255), + ) + # model training and testing settings +) + +load_from = "https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_s_20230227-f33ccdf2.pth" +optimizer = dict(paramwise_cfg=dict(custom_keys={"pos_block": dict(decay_mult=0.0), "norm": dict(decay_mult=0.0)})) +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/template.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_s/template.yaml new file mode 100644 index 00000000000..f28a01c3464 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/template.yaml @@ -0,0 +1,65 @@ +# Description. +model_template_id: Custom_Semantic_Segmentation_SegNext_s +name: SegNext-s +task_type: SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Semantic Segmentation with medium-sized architecture which based on the MSCAN backbone for the balance between accuracy and fast inference. +application: ~ + +# Algo backend. +framework: OTXSegmentation v0.14.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.segmentation.adapters.mmseg.task.MMSegmentationTask + openvino: otx.algorithms.segmentation.adapters.openvino.task.OpenVINOSegmentationTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.00008 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 20 + num_iters: + default_value: 170 + early_stop_start: + default_value: 15 + early_stop_patience: + default_value: 10 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + pot_parameters: + preset: + default_value: Mixed + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 15.35 +size: 13.9 diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/__init__.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/__init__.py new file mode 100644 index 00000000000..bab29c5761c --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of SegNext-T model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/compression_config.json b/src/otx/algorithms/segmentation/configs/ham_segnext_t/compression_config.json new file mode 100644 index 00000000000..623a203e955 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/compression_config.json @@ -0,0 +1,50 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mDice", + "input_info": { + "sample_size": [1, 3, 512, 512] + }, + "compression": [], + "log_dir": "/tmp" + }, + "params_config": { + "iters": 0, + "open_layers": [] + }, + "checkpoint_config": { + "interval": -1 + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 1e-4 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 500 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 500 + } + }, + "ignored_scopes": [] + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 1.0, + "maximal_total_epochs": 40 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/data_pipeline.py new file mode 100644 index 00000000000..beb5ca35318 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/data_pipeline.py @@ -0,0 +1,18 @@ +"""Data Pipeline of SegNext model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/deployment.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/deployment.py new file mode 100644 index 00000000000..5241c4c4a62 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/deployment.py @@ -0,0 +1,11 @@ +"""MMDeploy config of SegNext-T model for Segmentation Task.""" + +_base_ = ["../base/deployments/base_segmentation_dynamic.py"] + +ir_config = dict( + output_names=["output"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 544, 544]))], +) diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/hpo_config.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_t/hpo_config.yaml new file mode 100644 index 00000000000..4ec70db652f --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: mDice +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.00005 + - 0.005 + - 0.00001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 6 + - 12 + - 2 diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/model.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/model.py new file mode 100644 index 00000000000..daa8546ae8c --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/model.py @@ -0,0 +1,44 @@ +"""Model configuration of SegNext-T model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../recipes/stages/segmentation/incremental_poly.py", + "../../../common/adapters/mmcv/configs/backbones/segnext.py", +] + +ham_norm_cfg = dict(type="GN", num_groups=32, requires_grad=True) +model = dict( + type="OTXEncoderDecoder", + decode_head=dict( + type="LightHamHead", + input_transform="multiple_select", + in_channels=[64, 160, 256], + in_index=[1, 2, 3], + channels=256, + ham_channels=256, + dropout_ratio=0.1, + num_classes=150, + norm_cfg=ham_norm_cfg, + align_corners=False, + loss_decode=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + ham_kwargs=dict(MD_S=1, MD_R=16, train_steps=6, in_channels=256, eval_steps=7, inv_t=100), + ), +) + +optimizer = dict(paramwise_cfg=dict(custom_keys={"pos_block": dict(decay_mult=0.0), "norm": dict(decay_mult=0.0)})) +load_from = "https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_t_20230227-119e8c9f.pth" diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/ptq_optimization_config.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/ptq_optimization_config.py new file mode 100644 index 00000000000..db8a1682c3b --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/ptq_optimization_config.py @@ -0,0 +1,12 @@ +"""PTQ config file.""" +from nncf import IgnoredScope + +ignored_scope = IgnoredScope( + patterns=["/hamburger/"], + types=[ + "Add", + "MVN", + "Divide", + "Multiply", + ], +) diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/__init__.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/__init__.py new file mode 100644 index 00000000000..06ef0d3ca53 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/__init__.py @@ -0,0 +1,4 @@ +"""Initialization of SegNext-T model for Self-SL Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/data_pipeline.py new file mode 100644 index 00000000000..36a050464fc --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of SegNext model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/hparam.yaml new file mode 100644 index 00000000000..a0c8d7feea3 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/hparam.yaml @@ -0,0 +1,17 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 32 + learning_rate: + default_value: 0.0001 + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 10 + enable_early_stopping: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/model.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/model.py new file mode 100644 index 00000000000..3056ab7accf --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/selfsl/model.py @@ -0,0 +1,47 @@ +"""Model configuration of SegNext-T model for Self-SL Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/selfsl.py", + "../../../../common/adapters/mmcv/configs/backbones/segnext.py", +] + + +model = dict( + type="DetConB", + pretrained="https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_t_20230227-119e8c9f.pth", + num_classes=256, + num_samples=16, + downsample=8, + input_transform="resize_concat", + in_index=[1, 2, 3], + neck=dict( + type="SelfSLMLP", + in_channels=480, + hid_channels=960, + out_channels=256, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=256, + hid_channels=960, + out_channels=256, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), +) + +optimizer = dict(paramwise_cfg=dict(custom_keys={"pos_block": dict(decay_mult=0.0), "norm": dict(decay_mult=0.0)})) +load_from = None +resume_from = None +fp16 = None diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/__init__.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/__init__.py new file mode 100644 index 00000000000..817a3e0cb81 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of SegNext-s model for Semi-SL Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/data_pipeline.py new file mode 100644 index 00000000000..8da1d207460 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of SegNext model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/hparam.yaml new file mode 100644 index 00000000000..580462daa1e --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/model.py b/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/model.py new file mode 100644 index 00000000000..5715f075753 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/semisl/model.py @@ -0,0 +1,64 @@ +"""Semi-SL model configuration of SegNext-t model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/semisl_poly.py", + "../../../../common/adapters/mmcv/configs/backbones/segnext.py", +] + +ham_norm_cfg = dict(type="GN", num_groups=32, requires_grad=True) +norm_cfg = dict(type="BN", requires_grad=True) +model = dict( + type="MeanTeacherSegmentor", + orig_type="OTXEncoderDecoder", + unsup_weight=0.1, + proto_weight=0.1, + semisl_start_epoch=1, + train_cfg=dict(mix_loss=dict(enable=False, weight=0.1)), + test_cfg=dict(mode="whole", output_scale=5.0), + decode_head=dict( + type="LightHamHead", + input_transform="multiple_select", + in_channels=[64, 160, 256], + in_index=[1, 2, 3], + channels=256, + ham_channels=256, + dropout_ratio=0.1, + num_classes=19, + norm_cfg=ham_norm_cfg, + align_corners=False, + loss_decode=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + ham_kwargs=dict(MD_S=1, MD_R=16, train_steps=6, in_channels=256, eval_steps=7, inv_t=100), + ), + proto_head=dict( + in_channels=256, + channels=256, + norm_cfg=norm_cfg, + dropout_ratio=0.1, + align_corners=False, + gamma=0.999, + num_prototype=4, + in_proto_channels=256, + loss_decode=dict(type="PixelPrototypeCELoss", loss_ppc_weight=0.01, loss_ppd_weight=0.001, ignore_index=255), + ) + # model training and testing settings +) + +load_from = "https://download.openmmlab.com/mmsegmentation/v0.5/pretrain/segnext/mscan_t_20230227-119e8c9f.pth" +optimizer = dict(paramwise_cfg=dict(custom_keys={"pos_block": dict(decay_mult=0.0), "norm": dict(decay_mult=0.0)})) +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/template.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_t/template.yaml new file mode 100644 index 00000000000..ad041eea837 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/template.yaml @@ -0,0 +1,65 @@ +# Description. +model_template_id: Custom_Semantic_Segmentation_SegNext_t +name: SegNext-t +task_type: SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Semantic Segmentation with small-sized architecture which based on the MSCAN backbone for faster inference while preserving competetive accuracy. +application: ~ + +# Algo backend. +framework: OTXSegmentation v0.14.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.segmentation.adapters.mmseg.task.MMSegmentationTask + openvino: otx.algorithms.segmentation.adapters.openvino.task.OpenVINOSegmentationTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.00008 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 20 + num_iters: + default_value: 170 + early_stop_start: + default_value: 15 + early_stop_patience: + default_value: 10 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + pot_parameters: + preset: + default_value: Mixed + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 6.07 +size: 4.23 diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/__init__.py new file mode 100644 index 00000000000..ca70d544934 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-18 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/compression_config.json b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/compression_config.json new file mode 100644 index 00000000000..f5635404ea7 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/compression_config.json @@ -0,0 +1,53 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mDice", + "input_info": { + "sample_size": [1, 3, 512, 512] + }, + "compression": [], + "log_dir": "/tmp" + }, + "params_config": { + "iters": 0, + "open_layers": [] + }, + "checkpoint_config": { + "interval": -1 + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 1e-4 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 500 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 500 + } + }, + "ignored_scopes": [ + "{re}.*cross_resolution_weighting.*__mul__.*", + "{re}.*spatial_weighting.*__mul__.*" + ] + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 1.0, + "maximal_total_epochs": 40 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/data_pipeline.py new file mode 100644 index 00000000000..b690bf2ad88 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/deployment.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/deployment.py new file mode 100644 index 00000000000..9b50ed0087f --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/deployment.py @@ -0,0 +1,11 @@ +"""MMDeploy config of OCR-Lite-HRnet-18 model for Segmentation Task.""" + +_base_ = ["../base/deployments/base_segmentation_dynamic.py"] + +ir_config = dict( + output_names=["output"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 544, 544]))], +) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/hpo_config.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/hpo_config.yaml new file mode 100644 index 00000000000..819a3ac0368 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: mDice +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0002 + - 0.005 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 6 + - 12 + - 2 diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/model.py new file mode 100644 index 00000000000..38aa4c18635 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/model.py @@ -0,0 +1,54 @@ +"""Model configuration of OCR-Lite-HRnet-18 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../recipes/stages/segmentation/incremental.py", + "../../../common/adapters/mmcv/configs/backbones/lite_hrnet_18.py", +] + +model = dict( + type="OTXEncoderDecoder", + pretrained=None, + decode_head=dict( + type="FCNHead", + in_channels=[40, 80, 160, 320], + in_index=[0, 1, 2, 3], + input_transform="multiple_select", + channels=40, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + loss_decode=[ + dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + ], + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/custom_semantic_segmentation/litehrnet18_imagenet1k_rsc.pth" + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/ptq_optimization_config.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/ptq_optimization_config.py new file mode 100644 index 00000000000..1deba994c9b --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/ptq_optimization_config.py @@ -0,0 +1,23 @@ +"""PTQ config file.""" +from nncf.common.quantization.structs import QuantizationPreset +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ), + backend_params={"use_pot": True}, +) + +preset = QuantizationPreset.PERFORMANCE diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/__init__.py new file mode 100644 index 00000000000..52d1b061805 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-18 model for Self-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/data_pipeline.py new file mode 100644 index 00000000000..b740c613ce6 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/hparam.yaml new file mode 100644 index 00000000000..e031fda8bb0 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/hparam.yaml @@ -0,0 +1,17 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 16 + learning_rate: + default_value: 0.001 + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 10 + enable_early_stopping: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/model.py new file mode 100644 index 00000000000..760978ee781 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/selfsl/model.py @@ -0,0 +1,64 @@ +"""Model configuration of OCR-Lite-HRnet-18 model for Self-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/selfsl.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_18.py", +] + + +model = dict( + type="DetConB", + pretrained=( + "https://storage.openvinotoolkit.org/repositories/" + "openvino_training_extensions/models/custom_semantic_segmentation/" + "litehrnet18_imagenet1k_rsc.pth" + ), + num_classes=256, + num_samples=16, + downsample=4, + input_transform="resize_concat", + in_index=[0, 1, 2, 3], + neck=dict( + type="SelfSLMLP", + in_channels=600, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=128, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), +) + +load_from = None + +resume_from = None + +fp16 = None diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/__init__.py new file mode 100644 index 00000000000..9696f44cc4d --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-18 model for Semi-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/data_pipeline.py new file mode 100644 index 00000000000..b02be685cce --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/hparam.yaml new file mode 100644 index 00000000000..580462daa1e --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/model.py new file mode 100644 index 00000000000..1ecd859a785 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/semisl/model.py @@ -0,0 +1,59 @@ +"""Semi-SL model configuration of OCR-Lite-HRnet-18 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/semisl.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_18.py", +] + +model = dict( + type="MeanTeacherSegmentor", + orig_type="OTXEncoderDecoder", + unsup_weight=0.1, + train_cfg=dict(mix_loss=dict(enable=False, weight=0.1)), + test_cfg=dict(mode="whole", output_scale=5.0), + pretrained=None, + decode_head=dict( + type="FCNHead", + in_channels=[40, 80, 160, 320], + in_index=[0, 1, 2, 3], + input_transform="multiple_select", + channels=40, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + loss_decode=[ + dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + ], + ), +) +__norm_cfg = dict(type="BN", requires_grad=True) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/custom_semantic_segmentation/litehrnet18_imagenet1k_rsc.pth" + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/__init__.py new file mode 100644 index 00000000000..5b2e9a3dfc6 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-18 model for SupCon Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/data_pipeline.py new file mode 100644 index 00000000000..2f3cfb53602 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/supcon/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/hparam.yaml new file mode 100644 index 00000000000..2827eb5f677 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/hparam.yaml @@ -0,0 +1,8 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + enable_supcon: + default_value: True + learning_rate_warmup_iters: + default_value: 50 diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/model.py new file mode 100644 index 00000000000..60b26f9051a --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/supcon/model.py @@ -0,0 +1,85 @@ +"""Model configuration of OCR-Lite-HRnet-18 model for SupCon Segmentation Task.""" + + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/supcon.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_18.py", +] + +model = dict( + type="SupConDetConB", + pretrained=( + "https://storage.openvinotoolkit.org/repositories/" + "openvino_training_extensions/models/custom_semantic_segmentation/" + "litehrnet18_imagenet1k_rsc.pth" + ), + num_classes=256, + num_samples=16, + downsample=4, + input_transform="resize_concat", + in_index=[0, 1, 2, 3], + neck=dict( + type="SelfSLMLP", + in_channels=600, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=128, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), + decode_head=dict( + type="FCNHead", + in_channels=[40, 80, 160, 320], + in_index=[0, 1, 2, 3], + input_transform="multiple_select", + channels=40, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + loss_decode=[ + dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + ], + ), +) + +load_from = None + +resume_from = None + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/template.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/template.yaml new file mode 100644 index 00000000000..2bbc732092f --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18/template.yaml @@ -0,0 +1,49 @@ +# Description. +model_template_id: Custom_Semantic_Segmentation_Lite-HRNet-18_OCR +name: Lite-HRNet-18 +task_type: SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Semantic Segmentation with middle-sized architecture which based on the Lite-HRNet backbone for the balance between the fast inference and long training. (deprecated in next version) +application: ~ + +# Algo backend. +framework: OTXSegmentation v0.14.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.segmentation.adapters.mmseg.task.MMSegmentationTask + openvino: otx.algorithms.segmentation.adapters.openvino.task.OpenVINOSegmentationTask + nncf: otx.algorithms.segmentation.adapters.mmseg.nncf.task.SegmentationNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + learning_rate: + default_value: 0.001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 300 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 3.45 +size: 4.5 diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/__init__.py new file mode 100644 index 00000000000..899ef1d8c05 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-18-mod2 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/compression_config.json b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/compression_config.json new file mode 100644 index 00000000000..9b2f6090367 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/compression_config.json @@ -0,0 +1,53 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mDice", + "input_info": { + "sample_size": [1, 3, 512, 512] + }, + "compression": [], + "log_dir": "/tmp" + }, + "params_config": { + "iters": 0, + "open_layers": [] + }, + "checkpoint_config": { + "interval": -1 + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 1e-4 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 500 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 500 + } + }, + "ignored_scopes": [ + "{re}.*cross_resolution_weighting.*__mul__.*", + "{re}.*spatial_weighting.*__mul__.*" + ] + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 1.0, + "maximal_total_epochs": 180 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/data_pipeline.py new file mode 100644 index 00000000000..b690bf2ad88 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/deployment.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/deployment.py new file mode 100644 index 00000000000..ea7da47fce4 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/deployment.py @@ -0,0 +1,11 @@ +"""MMDeploy config of OCR-Lite-HRnet-18-mod2 model for Segmentation Task.""" + +_base_ = ["../base/deployments/base_segmentation_dynamic.py"] + +ir_config = dict( + output_names=["output"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 544, 544]))], +) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/hpo_config.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/hpo_config.yaml new file mode 100644 index 00000000000..819a3ac0368 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: mDice +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0002 + - 0.005 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 6 + - 12 + - 2 diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/model.py new file mode 100644 index 00000000000..20f30c1580b --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/model.py @@ -0,0 +1,52 @@ +"""Model configuration of OCR-Lite-HRnet-18-mod2 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../recipes/stages/segmentation/incremental.py", + "../../../common/adapters/mmcv/configs/backbones/lite_hrnet_18.py", +] + +model = dict( + type="OTXEncoderDecoder", + pretrained=None, + decode_head=dict( + type="FCNHead", + in_channels=[40, 80, 160, 320], + in_index=[0, 1, 2, 3], + input_transform="multiple_select", + channels=40, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + loss_decode=dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/custom_semantic_segmentation/litehrnet18_imagenet1k_rsc.pth" + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/ptq_optimization_config.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/ptq_optimization_config.py new file mode 100644 index 00000000000..4e5ce69c89c --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/ptq_optimization_config.py @@ -0,0 +1,107 @@ +"""PTQ config file.""" +from nncf import IgnoredScope +from nncf.common.quantization.structs import QuantizationPreset +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ), +) + +preset = QuantizationPreset.MIXED + +ignored_scope = IgnoredScope( + patterns=["/backbone/*"], + names=[ + "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.0/Add_1", + "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.1/Add_1", + "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.0/Add_1", + "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.0/Add_2", + "/backbone/stage1/stage1.0/Add_5", + "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.1/Add_1", + "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.1/Add_2", + "/backbone/stage1/stage1.1/Add_5", + "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.2/Add_1", + "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.2/Add_2", + "/backbone/stage1/stage1.2/Add_5", + "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.3/Add_1", + "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.3/Add_2", + "/backbone/stage1/stage1.3/Add_5", + "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.0/Add_1", + "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.0/Add_2", + "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.0/Add_3", + "/backbone/stage2/stage2.0/Add_6", + "/backbone/stage2/stage2.0/Add_7", + "/backbone/stage2/stage2.0/Add_11", + "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.1/Add_1", + "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.1/Add_2", + "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.1/Add_3", + "/backbone/stage2/stage2.1/Add_6", + "/backbone/stage2/stage2.1/Add_7", + "/backbone/stage2/stage2.1/Add_11", + "/aggregator/Add", + "/aggregator/Add_1", + "/aggregator/Add_2", + "/backbone/stage2/stage2.1/Add", + ], +) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/__init__.py new file mode 100644 index 00000000000..9a0e6d6c1aa --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-18-mod2 model for Self-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/data_pipeline.py new file mode 100644 index 00000000000..b740c613ce6 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/hparam.yaml new file mode 100644 index 00000000000..e031fda8bb0 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/hparam.yaml @@ -0,0 +1,17 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 16 + learning_rate: + default_value: 0.001 + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 10 + enable_early_stopping: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/model.py new file mode 100644 index 00000000000..8f6c40e15e2 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/selfsl/model.py @@ -0,0 +1,64 @@ +"""Model configuration of OCR-Lite-HRnet-18-mod2 model for Self-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/selfsl.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_18.py", +] + + +model = dict( + type="DetConB", + pretrained=( + "https://storage.openvinotoolkit.org/repositories/" + "openvino_training_extensions/models/custom_semantic_segmentation/" + "litehrnet18_imagenet1k_rsc.pth" + ), + num_classes=256, + num_samples=16, + downsample=4, + input_transform="resize_concat", + in_index=[0, 1, 2, 3], + neck=dict( + type="SelfSLMLP", + in_channels=600, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=128, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), +) + +load_from = None + +resume_from = None + +fp16 = None diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/__init__.py new file mode 100644 index 00000000000..a150d3479f8 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-18-mod2 model for Semi-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/data_pipeline.py new file mode 100644 index 00000000000..b02be685cce --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/hparam.yaml new file mode 100644 index 00000000000..580462daa1e --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/model.py new file mode 100644 index 00000000000..d158f661b66 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/semisl/model.py @@ -0,0 +1,59 @@ +"""Semi-SL model configuration of OCR-Lite-HRnet-18-mod2 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/semisl.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_18.py", +] + +model = dict( + type="MeanTeacherSegmentor", + orig_type="OTXEncoderDecoder", + unsup_weight=0.1, + train_cfg=dict(mix_loss=dict(enable=False, weight=0.1)), + test_cfg=dict(mode="whole", output_scale=5.0), + pretrained=None, + decode_head=dict( + type="FCNHead", + in_channels=[40, 80, 160, 320], + in_index=[0, 1, 2, 3], + input_transform="multiple_select", + channels=40, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + loss_decode=[ + dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + ], + ), +) +__norm_cfg = dict(type="BN", requires_grad=True) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/custom_semantic_segmentation/litehrnet18_imagenet1k_rsc.pth" + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/__init__.py new file mode 100644 index 00000000000..d142d4743d2 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-18-mod2 model for SupCon Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/data_pipeline.py new file mode 100644 index 00000000000..2f3cfb53602 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/supcon/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/hparam.yaml new file mode 100644 index 00000000000..2827eb5f677 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/hparam.yaml @@ -0,0 +1,8 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + enable_supcon: + default_value: True + learning_rate_warmup_iters: + default_value: 50 diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/model.py new file mode 100644 index 00000000000..96e3f9032eb --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/supcon/model.py @@ -0,0 +1,84 @@ +"""Model configuration of OCR-Lite-HRnet-18-mod2 model for SupCon Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/supcon.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_18.py", +] + +model = dict( + type="SupConDetConB", + pretrained=( + "https://storage.openvinotoolkit.org/repositories/" + "openvino_training_extensions/models/custom_semantic_segmentation/" + "litehrnet18_imagenet1k_rsc.pth" + ), + num_classes=256, + num_samples=16, + downsample=4, + input_transform="resize_concat", + in_index=[0, 1, 2, 3], + neck=dict( + type="SelfSLMLP", + in_channels=600, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=128, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), + decode_head=dict( + type="FCNHead", + in_channels=[40, 80, 160, 320], + in_index=[0, 1, 2, 3], + input_transform="multiple_select", + channels=40, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + loss_decode=[ + dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + ], + ), +) + +load_from = None + +resume_from = None + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/template.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/template.yaml new file mode 100644 index 00000000000..74b14a71c3e --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_18_mod2/template.yaml @@ -0,0 +1,62 @@ +# Description. +model_template_id: Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR +name: Lite-HRNet-18-mod2 +task_type: SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Semantic Segmentation with middle-sized architecture which based on the Lite-HRNet backbone for the balance between the fast inference and long training. +application: ~ + +# Algo backend. +framework: OTXSegmentation v0.14.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.segmentation.adapters.mmseg.task.MMSegmentationTask + openvino: otx.algorithms.segmentation.adapters.openvino.task.OpenVINOSegmentationTask + nncf: otx.algorithms.segmentation.adapters.mmseg.nncf.task.SegmentationNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + learning_rate: + default_value: 0.001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 300 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 3.63 +size: 4.8 + +# Model spec +model_category: BALANCE +is_default_for_task: true diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/__init__.py new file mode 100644 index 00000000000..aa07d2c4c01 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-s-mod2 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/compression_config.json b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/compression_config.json new file mode 100644 index 00000000000..8ab743abe1e --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/compression_config.json @@ -0,0 +1,53 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mDice", + "input_info": { + "sample_size": [1, 3, 512, 512] + }, + "compression": [], + "log_dir": "/tmp" + }, + "params_config": { + "iters": 0, + "open_layers": [] + }, + "checkpoint_config": { + "interval": -1 + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 1e-4 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 5 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 5 + } + }, + "ignored_scopes": [ + "{re}.*cross_resolution_weighting.*__mul__.*", + "{re}.*spatial_weighting.*__mul__.*" + ] + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 1.0, + "maximal_total_epochs": 40 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/data_pipeline.py new file mode 100644 index 00000000000..b690bf2ad88 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/deployment.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/deployment.py new file mode 100644 index 00000000000..158207b747b --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/deployment.py @@ -0,0 +1,11 @@ +"""MMDeploy config of OCR-Lite-HRnet-s-mod2 model for Segmentation Task.""" + +_base_ = ["../base/deployments/base_segmentation_dynamic.py"] + +ir_config = dict( + output_names=["output"], +) + +backend_config = dict( + model_inputs=[dict(opt_shapes=dict(input=[-1, 3, 544, 544]))], +) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/hpo_config.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/hpo_config.yaml new file mode 100644 index 00000000000..819a3ac0368 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: mDice +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0002 + - 0.005 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 6 + - 12 + - 2 diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/model.py new file mode 100644 index 00000000000..d586953e071 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/model.py @@ -0,0 +1,54 @@ +"""Model configuration of OCR-Lite-HRnet-s-mod2 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../recipes/stages/segmentation/incremental.py", + "../../../common/adapters/mmcv/configs/backbones/lite_hrnet_s.py", +] + +model = dict( + type="OTXEncoderDecoder", + pretrained=None, + decode_head=dict( + type="FCNHead", + in_channels=[60, 120, 240], + in_index=[0, 1, 2], + input_transform="multiple_select", + channels=60, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + aggregator_merge_norm=None, + aggregator_use_concat=False, + loss_decode=dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/custom_semantic_segmentation/litehrnetsv2_imagenet1k_rsc.pth" + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/ptq_optimization_config.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/ptq_optimization_config.py new file mode 100644 index 00000000000..442c139c4a2 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/ptq_optimization_config.py @@ -0,0 +1,86 @@ +"""PTQ config file.""" +from nncf import IgnoredScope +from nncf.common.quantization.structs import QuantizationPreset +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ), +) + +preset = QuantizationPreset.MIXED + +ignored_scope = IgnoredScope( + names=[ + "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.0/Add_1", + "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.1/Add_1", + "/backbone/stage0/stage0.2/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.2/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.2/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.2/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.2/Add_1", + "/backbone/stage0/stage0.3/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.3/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.3/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.3/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.3/Add_1", + "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.0/Add_1", + "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.0/Add_2", + "/backbone/stage1/stage1.0/Add_5", + "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.1/Add_1", + "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.1/Add_2", + "/backbone/stage1/stage1.1/Add_5", + "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.2/Add_1", + "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.2/Add_2", + "/backbone/stage1/stage1.2/Add_5", + "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.3/Add_1", + "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.3/Add_2", + "/backbone/stage1/stage1.3/Add_5", + "/aggregator/Add", + "/aggregator/Add_1", + ] +) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/__init__.py new file mode 100644 index 00000000000..1c54c62a1ec --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-s-mod2 model for Self-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/data_pipeline.py new file mode 100644 index 00000000000..b740c613ce6 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/hparam.yaml new file mode 100644 index 00000000000..e031fda8bb0 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/hparam.yaml @@ -0,0 +1,17 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 16 + learning_rate: + default_value: 0.001 + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 10 + enable_early_stopping: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/model.py new file mode 100644 index 00000000000..aa95811f388 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/selfsl/model.py @@ -0,0 +1,62 @@ +"""Model configuration of OCR-Lite-HRnet-s-mod2 model for Self-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/selfsl.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_s.py", +] + +model = dict( + type="DetConB", + pretrained=( + "https://storage.openvinotoolkit.org/repositories/" + "openvino_training_extensions/models/custom_semantic_segmentation/" + "litehrnetsv2_imagenet1k_rsc.pth" + ), + num_classes=256, + num_samples=16, + downsample=8, + input_transform="resize_concat", + in_index=[0, 1, 2], + neck=dict( + type="SelfSLMLP", + in_channels=420, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=128, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), +) + +load_from = None + +resume_from = None + +fp16 = None diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/__init__.py new file mode 100644 index 00000000000..30181bad05c --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-s-mod2 model for Semi-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/data_pipeline.py new file mode 100644 index 00000000000..b02be685cce --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/hparam.yaml new file mode 100644 index 00000000000..580462daa1e --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/model.py new file mode 100644 index 00000000000..9a7fe5bbee1 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/semisl/model.py @@ -0,0 +1,65 @@ +"""Semi-SL model configuration of OCR-Lite-HRnet-s-mod2 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + + +_base_ = [ + "../../../../../recipes/stages/segmentation/semisl.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_s.py", +] + +model = dict( + type="MeanTeacherSegmentor", + orig_type="OTXEncoderDecoder", + unsup_weight=0.1, + train_cfg=dict(mix_loss=dict(enable=False, weight=0.1)), + test_cfg=dict(mode="whole", output_scale=5.0), + pretrained=None, + decode_head=dict( + type="FCNHead", + in_channels=[60, 120, 240], + in_index=[0, 1, 2], + input_transform="multiple_select", + channels=60, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + aggregator_merge_norm=None, + aggregator_use_concat=False, + loss_decode=dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + init_cfg=dict( + type="Normal", + mean=0, + std=0.01, + override=dict(name="conv_seg"), + ), + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/custom_semantic_segmentation/litehrnetsv2_imagenet1k_rsc.pth" + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/__init__.py new file mode 100644 index 00000000000..1c4bf51e3eb --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-s-mod2 model for SupCon Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/data_pipeline.py new file mode 100644 index 00000000000..2f3cfb53602 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/supcon/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/hparam.yaml new file mode 100644 index 00000000000..2827eb5f677 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/hparam.yaml @@ -0,0 +1,8 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + enable_supcon: + default_value: True + learning_rate_warmup_iters: + default_value: 50 diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/model.py new file mode 100644 index 00000000000..20d7fafa184 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/supcon/model.py @@ -0,0 +1,90 @@ +"""Model configuration of OCR-Lite-HRnet-s-mod2 model for SupCon Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/supcon.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_s.py", +] + +model = dict( + type="SupConDetConB", + pretrained=( + "https://storage.openvinotoolkit.org/repositories/" + "openvino_training_extensions/models/custom_semantic_segmentation/" + "litehrnetsv2_imagenet1k_rsc.pth" + ), + num_classes=256, + num_samples=16, + downsample=8, + input_transform="resize_concat", + in_index=[0, 1, 2], + neck=dict( + type="SelfSLMLP", + in_channels=420, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=128, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), + decode_head=dict( + type="FCNHead", + in_channels=[60, 120, 240], + in_index=[0, 1, 2], + input_transform="multiple_select", + channels=60, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + aggregator_merge_norm=None, + aggregator_use_concat=False, + loss_decode=dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + init_cfg=dict( + type="Normal", + mean=0, + std=0.01, + override=dict(name="conv_seg"), + ), + ), +) + +load_from = None + +resume_from = None + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/template.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/template.yaml new file mode 100644 index 00000000000..101770cf6a3 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/template.yaml @@ -0,0 +1,62 @@ +# Description. +model_template_id: Custom_Semantic_Segmentation_Lite-HRNet-s-mod2_OCR +name: Lite-HRNet-s-mod2 +task_type: SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Semantic Segmentation with lightweight architecture which based on the Lite-HRNet backbone for the fast inference and training on the limited amount of data. +application: ~ + +# Algo backend. +framework: OTXSegmentation v0.14.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.segmentation.adapters.mmseg.task.MMSegmentationTask + openvino: otx.algorithms.segmentation.adapters.openvino.task.OpenVINOSegmentationTask + nncf: otx.algorithms.segmentation.adapters.mmseg.nncf.task.SegmentationNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 300 + nncf_optimization: + enable_quantization: + default_value: true + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 1.82 +size: 3.5 + +# Model spec +model_category: SPEED diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/__init__.py new file mode 100644 index 00000000000..b7a27b591b4 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-x-mod3 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/compression_config.json b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/compression_config.json new file mode 100644 index 00000000000..f5635404ea7 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/compression_config.json @@ -0,0 +1,53 @@ +{ + "base": { + "find_unused_parameters": true, + "nncf_config": { + "target_metric_name": "mDice", + "input_info": { + "sample_size": [1, 3, 512, 512] + }, + "compression": [], + "log_dir": "/tmp" + }, + "params_config": { + "iters": 0, + "open_layers": [] + }, + "checkpoint_config": { + "interval": -1 + } + }, + "nncf_quantization": { + "optimizer": { + "lr": 1e-4 + }, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": { + "num_init_samples": 500 + }, + "batchnorm_adaptation": { + "num_bn_adaptation_samples": 500 + } + }, + "ignored_scopes": [ + "{re}.*cross_resolution_weighting.*__mul__.*", + "{re}.*spatial_weighting.*__mul__.*" + ] + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 1.0, + "maximal_total_epochs": 40 + } + } + } + }, + "order_of_parts": ["nncf_quantization"] +} diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/data_pipeline.py new file mode 100644 index 00000000000..b690bf2ad88 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../base/data/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/deployment.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/deployment.py new file mode 100644 index 00000000000..c8c6f19c54d --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/deployment.py @@ -0,0 +1,12 @@ +"""MMDeploy config of OCR-Lite-HRnet-x-mod3 model for Segmentation Task.""" + +_base_ = ["../base/deployments/base_segmentation_dynamic.py"] + +ir_config = dict( + output_names=["output"], +) + +backend_config = dict( + # dynamic batch causes openvino runtime error + model_inputs=[dict(opt_shapes=dict(input=[1, 3, 544, 544]))], +) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/hpo_config.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/hpo_config.yaml new file mode 100644 index 00000000000..3b8a4838ecc --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/hpo_config.yaml @@ -0,0 +1,15 @@ +metric: mDice +search_algorithm: asha +hp_space: + learning_parameters.learning_rate: + param_type: qloguniform + range: + - 0.0001 + - 0.01 + - 0.0001 + learning_parameters.batch_size: + param_type: qloguniform + range: + - 4 + - 16 + - 2 diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/model.py new file mode 100644 index 00000000000..6691637aac5 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/model.py @@ -0,0 +1,55 @@ +"""Model configuration of OCR-Lite-HRnet-x-mod3 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../recipes/stages/segmentation/incremental.py", + "../../../common/adapters/mmcv/configs/backbones/lite_hrnet_x.py", +] + +model = dict( + type="OTXEncoderDecoder", + pretrained=None, + decode_head=dict( + type="FCNHead", + in_channels=[18, 60, 80, 160, 320], + in_index=[0, 1, 2, 3, 4], + input_transform="multiple_select", + channels=60, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + aggregator_min_channels=60, + aggregator_merge_norm=None, + aggregator_use_concat=False, + loss_decode=dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + ), +) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/custom_semantic_segmentation/litehrnetxv3_imagenet1k_rsc.pth" + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/ptq_optimization_config.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/ptq_optimization_config.py new file mode 100644 index 00000000000..b678c2eeeb3 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/ptq_optimization_config.py @@ -0,0 +1,186 @@ +"""PTQ config file.""" +from nncf import IgnoredScope +from nncf.common.quantization.structs import QuantizationPreset +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ), +) + +preset = QuantizationPreset.PERFORMANCE + +ignored_scope = IgnoredScope( + names=[ + "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.0/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.0/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.0/Add_1", + "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.1/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage0/stage0.1/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage0/stage0.1/Add_1", + "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.0/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.0/Add_1", + "/backbone/stage1/stage1.0/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.0/Add_2", + "/backbone/stage1/stage1.0/Add_5", + "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.1/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.1/Add_1", + "/backbone/stage1/stage1.1/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.1/Add_2", + "/backbone/stage1/stage1.1/Add_5", + "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.2/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.2/Add_1", + "/backbone/stage1/stage1.2/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.2/Add_2", + "/backbone/stage1/stage1.2/Add_5", + "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.3/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage1/stage1.3/Add_1", + "/backbone/stage1/stage1.3/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage1/stage1.3/Add_2", + "/backbone/stage1/stage1.3/Add_5", + "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.0/layers/layers.0/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.0/Add_1", + "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.0/Add_2", + "/backbone/stage2/stage2.0/layers/layers.1/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.0/Add_3", + "/backbone/stage2/stage2.0/Add_6", + "/backbone/stage2/stage2.0/Add_7", + "/backbone/stage2/stage2.0/Add_11", + "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.1/layers/layers.0/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.1/Add_1", + "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.1/Add_2", + "/backbone/stage2/stage2.1/layers/layers.1/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.1/Add_3", + "/backbone/stage2/stage2.1/Add_6", + "/backbone/stage2/stage2.1/Add_7", + "/backbone/stage2/stage2.1/Add_11", + "/backbone/stage2/stage2.2/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.2/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.2/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.2/layers/layers.0/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.2/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.2/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.2/Add_1", + "/backbone/stage2/stage2.2/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.2/Add_2", + "/backbone/stage2/stage2.2/layers/layers.1/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.2/Add_3", + "/backbone/stage2/stage2.2/Add_6", + "/backbone/stage2/stage2.2/Add_7", + "/backbone/stage2/stage2.2/Add_11", + "/backbone/stage2/stage2.3/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.3/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.3/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.3/layers/layers.0/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.3/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage2/stage2.3/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage2/stage2.3/Add_1", + "/backbone/stage2/stage2.3/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage2/stage2.3/Add_2", + "/backbone/stage2/stage2.3/layers/layers.1/cross_resolution_weighting/Mul_3", + "/backbone/stage2/stage2.3/Add_3", + "/backbone/stage2/stage2.3/Add_6", + "/backbone/stage2/stage2.3/Add_7", + "/backbone/stage2/stage2.3/Add_11", + "/backbone/stage3/stage3.0/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage3/stage3.0/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage3/stage3.0/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage3/stage3.0/layers/layers.0/cross_resolution_weighting/Mul_3", + "/backbone/stage3/stage3.0/layers/layers.0/cross_resolution_weighting/Mul_4", + "/backbone/stage3/stage3.0/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage3/stage3.0/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage3/stage3.0/Add_1", + "/backbone/stage3/stage3.0/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage3/stage3.0/Add_2", + "/backbone/stage3/stage3.0/layers/layers.1/cross_resolution_weighting/Mul_3", + "/backbone/stage3/stage3.0/Add_3", + "/backbone/stage3/stage3.0/layers/layers.1/cross_resolution_weighting/Mul_4", + "/backbone/stage3/stage3.0/Add_4", + "/backbone/stage3/stage3.0/Add_7", + "/backbone/stage3/stage3.0/Add_8", + "/backbone/stage3/stage3.0/Add_9", + "/backbone/stage3/stage3.0/Add_13", + "/backbone/stage3/stage3.0/Add_14", + "/backbone/stage3/stage3.0/Add_19", + "/backbone/stage3/stage3.1/layers/layers.0/cross_resolution_weighting/Mul", + "/backbone/stage3/stage3.1/layers/layers.0/cross_resolution_weighting/Mul_1", + "/backbone/stage3/stage3.1/layers/layers.0/cross_resolution_weighting/Mul_2", + "/backbone/stage3/stage3.1/layers/layers.0/cross_resolution_weighting/Mul_3", + "/backbone/stage3/stage3.1/layers/layers.0/cross_resolution_weighting/Mul_4", + "/backbone/stage3/stage3.1/layers/layers.1/cross_resolution_weighting/Mul", + "/backbone/stage3/stage3.1/layers/layers.1/cross_resolution_weighting/Mul_1", + "/backbone/stage3/stage3.1/Add_1", + "/backbone/stage3/stage3.1/layers/layers.1/cross_resolution_weighting/Mul_2", + "/backbone/stage3/stage3.1/Add_2", + "/backbone/stage3/stage3.1/layers/layers.1/cross_resolution_weighting/Mul_3", + "/backbone/stage3/stage3.1/Add_3", + "/backbone/stage3/stage3.1/layers/layers.1/cross_resolution_weighting/Mul_4", + "/backbone/stage3/stage3.1/Add_4", + "/backbone/stage3/stage3.1/Add_7", + "/backbone/stage3/stage3.1/Add_8", + "/backbone/stage3/stage3.1/Add_9", + "/backbone/stage3/stage3.1/Add_13", + "/backbone/stage3/stage3.1/Add_14", + "/backbone/stage3/stage3.1/Add_19", + "/aggregator/Add", + "/aggregator/Add_1", + "/aggregator/Add_2", + "/aggregator/Add_3", + "/backbone/stage0/stage0.0/Add", + "/backbone/stage0/stage0.1/Add", + "/backbone/stage1/stage1.0/Add", + "/backbone/stage1/stage1.1/Add", + "/backbone/stage1/stage1.2/Add", + "/backbone/stage1/stage1.3/Add", + "/backbone/stage2/stage2.0/Add", + "/backbone/stage2/stage2.1/Add", + "/backbone/stage2/stage2.2/Add", + "/backbone/stage2/stage2.3/Add", + "/backbone/stage3/stage3.0/Add", + "/backbone/stage3/stage3.1/Add", + ] +) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/__init__.py new file mode 100644 index 00000000000..96bfefdc4f7 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-x-mod3 model for Self-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/data_pipeline.py new file mode 100644 index 00000000000..b740c613ce6 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/selfsl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/hparam.yaml new file mode 100644 index 00000000000..e031fda8bb0 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/hparam.yaml @@ -0,0 +1,17 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + batch_size: + default_value: 16 + learning_rate: + default_value: 0.001 + learning_rate_warmup_iters: + default_value: 0 + num_iters: + default_value: 10 + enable_early_stopping: + default_value: false + algo_backend: + train_type: + default_value: Selfsupervised diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/model.py new file mode 100644 index 00000000000..682e263ac77 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/selfsl/model.py @@ -0,0 +1,62 @@ +"""Model configuration of OCR-Lite-HRnet-x-mod3 model for Self-SL Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/selfsl.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_x.py", +] + +model = dict( + type="DetConB", + pretrained=( + "https://storage.openvinotoolkit.org/repositories/" + "openvino_training_extensions/models/custom_semantic_segmentation/" + "litehrnetxv3_imagenet1k_rsc.pth" + ), + num_classes=256, + num_samples=16, + downsample=2, + input_transform="resize_concat", + in_index=[0, 1, 2, 3, 4], + neck=dict( + type="SelfSLMLP", + in_channels=638, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=128, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), +) + +load_from = None + +resume_from = None + +fp16 = None diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/__init__.py new file mode 100644 index 00000000000..42f3e480b36 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-x-mod3 model for Semi-SL egmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/data_pipeline.py new file mode 100644 index 00000000000..b02be685cce --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/semisl/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/hparam.yaml new file mode 100644 index 00000000000..580462daa1e --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/hparam.yaml @@ -0,0 +1,6 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + algo_backend: + train_type: + default_value: Semisupervised diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/model.py new file mode 100644 index 00000000000..5869d8cf3fc --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/semisl/model.py @@ -0,0 +1,64 @@ +"""Semi-SL model configuration of OCR-Lite-HRnet-x-mod3 model for Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + + +_base_ = [ + "../../../../../recipes/stages/segmentation/semisl.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_x.py", +] + +model = dict( + type="MeanTeacherSegmentor", + orig_type="OTXEncoderDecoder", + unsup_weight=0.1, + train_cfg=dict(mix_loss=dict(enable=False, weight=0.1)), + test_cfg=dict(mode="whole", output_scale=5.0), + pretrained=None, + decode_head=dict( + type="FCNHead", + in_channels=[18, 60, 80, 160, 320], + in_index=[0, 1, 2, 3, 4], + input_transform="multiple_select", + channels=60, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + aggregator_min_channels=60, + aggregator_merge_norm=None, + aggregator_use_concat=False, + loss_decode=[ + dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + ], + ), +) + +__norm_cfg = dict(type="BN", requires_grad=True) + +load_from = "https://storage.openvinotoolkit.org/repositories/openvino_training_extensions\ +/models/custom_semantic_segmentation/litehrnetxv3_imagenet1k_rsc.pth" + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/__init__.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/__init__.py new file mode 100644 index 00000000000..23e005dbb0b --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of OCR-Lite-HRnet-x-mod3 model for SupCon Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/data_pipeline.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/data_pipeline.py new file mode 100644 index 00000000000..2f3cfb53602 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/data_pipeline.py @@ -0,0 +1,7 @@ +"""Data Pipeline of HR-Net model for Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name +_base_ = ["../../base/data/supcon/data_pipeline.py"] diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/hparam.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/hparam.yaml new file mode 100644 index 00000000000..2827eb5f677 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/hparam.yaml @@ -0,0 +1,8 @@ +# Hyperparameters. +hyper_parameters: + parameter_overrides: + learning_parameters: + enable_supcon: + default_value: True + learning_rate_warmup_iters: + default_value: 50 diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/model.py b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/model.py new file mode 100644 index 00000000000..776c8b658f0 --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/supcon/model.py @@ -0,0 +1,87 @@ +"""Model configuration of OCR-Lite-HRnet-x-mod3 model for SupCon Segmentation Task.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=invalid-name + +_base_ = [ + "../../../../../recipes/stages/segmentation/supcon.py", + "../../../../common/adapters/mmcv/configs/backbones/lite_hrnet_x.py", +] + +model = dict( + type="SupConDetConB", + pretrained=( + "https://storage.openvinotoolkit.org/repositories/" + "openvino_training_extensions/models/custom_semantic_segmentation/" + "litehrnetxv3_imagenet1k_rsc.pth" + ), + num_classes=256, + num_samples=16, + downsample=2, + input_transform="resize_concat", + in_index=[0, 1, 2, 3, 4], + neck=dict( + type="SelfSLMLP", + in_channels=638, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + head=dict( + type="DetConHead", + predictor=dict( + type="SelfSLMLP", + in_channels=128, + hid_channels=256, + out_channels=128, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ), + decode_head=dict( + type="FCNHead", + in_channels=[18, 60, 80, 160, 320], + in_index=[0, 1, 2, 3, 4], + input_transform="multiple_select", + channels=60, + kernel_size=1, + num_convs=1, + concat_input=False, + dropout_ratio=-1, + num_classes=2, + norm_cfg=dict(type="BN", requires_grad=True), + align_corners=False, + enable_aggregator=True, + aggregator_min_channels=60, + aggregator_merge_norm=None, + aggregator_use_concat=False, + loss_decode=[ + dict( + type="CrossEntropyLoss", + use_sigmoid=False, + loss_weight=1.0, + ), + ], + ), +) + +load_from = None + +resume_from = None + +fp16 = dict(loss_scale=512.0) diff --git a/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/template.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/template.yaml new file mode 100644 index 00000000000..88dfe188e1a --- /dev/null +++ b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/template.yaml @@ -0,0 +1,62 @@ +# Description. +model_template_id: Custom_Semantic_Segmentation_Lite-HRNet-x-mod3_OCR +name: Lite-HRNet-x-mod3 +task_type: SEGMENTATION +task_family: VISION +instantiation: "CLASS" +summary: Class-Incremental Semantic Segmentation with heavy-size architecture which based on the Lite-HRNet backbone for the accurate predictions but long training. +application: ~ + +# Algo backend. +framework: OTXSegmentation v0.14.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.segmentation.adapters.mmseg.task.MMSegmentationTask + openvino: otx.algorithms.segmentation.adapters.openvino.task.OpenVINOSegmentationTask + nncf: otx.algorithms.segmentation.adapters.mmseg.nncf.task.SegmentationNNCFTask + +# Capabilities. +capabilities: + - compute_representations + +# Hyperparameters. +hyper_parameters: + base_path: ../configuration.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 8 + auto_hpo_state: POSSIBLE + learning_rate: + default_value: 0.001 + auto_hpo_state: POSSIBLE + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 300 + nncf_optimization: + enable_quantization: + default_value: false + enable_pruning: + default_value: false + pruning_supported: + default_value: false + maximal_accuracy_degradation: + default_value: 1.0 + algo_backend: + train_type: + default_value: Incremental + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Stats. +gigaflops: 13.97 +size: 6.4 + +# Model spec +model_category: ACCURACY diff --git a/src/otx/algorithms/segmentation/task.py b/src/otx/algorithms/segmentation/task.py new file mode 100644 index 00000000000..dac8fe574cb --- /dev/null +++ b/src/otx/algorithms/segmentation/task.py @@ -0,0 +1,385 @@ +"""Task of OTX Segmentation.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import io +import os +from abc import ABC, abstractmethod +from typing import List, Optional + +import numpy as np +import torch +from mmcv.utils import ConfigDict + +from otx.algorithms.common.configs.configuration_enums import InputSizePreset +from otx.algorithms.common.configs.training_base import TrainType +from otx.algorithms.common.tasks.base_task import TRAIN_TYPE_DIR_PATH, OTXTask +from otx.algorithms.common.utils.callback import ( + InferenceProgressCallback, + TrainingProgressCallback, +) +from otx.algorithms.common.utils.ir import embed_ir_model_data +from otx.algorithms.common.utils.utils import embed_onnx_model_data +from otx.algorithms.segmentation.configs.base import SegmentationConfig +from otx.algorithms.segmentation.utils import get_activation_map +from otx.algorithms.segmentation.utils.metadata import get_seg_model_api_configuration +from otx.api.configuration import cfg_helper +from otx.api.configuration.helper.utils import ids_to_strings +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.inference_parameters import ( + default_progress_callback as default_infer_progress_callback, +) +from otx.api.entities.metrics import ( + CurveMetric, + InfoMetric, + LineChartInfo, + MetricsGroup, + Performance, + ScoreMetric, + VisualizationInfo, + VisualizationType, +) +from otx.api.entities.model import ( + ModelEntity, + ModelPrecision, +) +from otx.api.entities.result_media import ResultMediaEntity +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.tensor import TensorEntity +from otx.api.entities.train_parameters import TrainParameters, default_progress_callback +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.utils.segmentation_utils import ( + create_annotation_from_segmentation_map, + create_hard_prediction_from_soft_prediction, +) +from otx.cli.utils.multi_gpu import is_multigpu_child_process +from otx.core.data.caching.mem_cache_handler import MemCacheHandlerSingleton +from otx.utils.logger import get_logger + +logger = get_logger() +RECIPE_TRAIN_TYPE = { + TrainType.Semisupervised: "semisl.py", + TrainType.Incremental: "incremental.py", + TrainType.Selfsupervised: "selfsl.py", +} + + +class OTXSegmentationTask(OTXTask, ABC): + """Task class for OTX segmentation.""" + + # pylint: disable=too-many-instance-attributes, too-many-locals + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None): + super().__init__(task_environment, output_path) + self._task_config = SegmentationConfig + self._hyperparams: ConfigDict = task_environment.get_hyper_parameters(self._task_config) + self._model_name = task_environment.model_template.name + self._train_type = self._hyperparams.algo_backend.train_type + self.metric = "mDice" + self._label_dictionary = dict(enumerate(self._labels, 1)) # It should have same order as model class order + + self._model_dir = os.path.join( + os.path.abspath(os.path.dirname(self._task_environment.model_template.model_template_path)), + TRAIN_TYPE_DIR_PATH[self._train_type.name], + ) + if ( + self._train_type in RECIPE_TRAIN_TYPE + and self._train_type == TrainType.Incremental + and self._hyperparams.learning_parameters.enable_supcon + and not self._model_dir.endswith("supcon") + ): + self._model_dir = os.path.join(self._model_dir, "supcon") + + if task_environment.model is not None: + self._load_model() + + self.data_pipeline_path = os.path.join(self._model_dir, "data_pipeline.py") + + if hasattr(self._hyperparams.learning_parameters, "input_size"): + input_size_cfg = InputSizePreset(self._hyperparams.learning_parameters.input_size.value) + else: + input_size_cfg = InputSizePreset.DEFAULT + self._input_size = input_size_cfg.tuple + + def infer( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ) -> DatasetEntity: + """Main infer function.""" + logger.info("infer()") + + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress + dump_soft_prediction = not inference_parameters.is_evaluation + process_soft_prediction = inference_parameters.process_saliency_maps + else: + update_progress_callback = default_infer_progress_callback + dump_soft_prediction = True + process_soft_prediction = False + + update_progress_callback = default_progress_callback + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress # type: ignore + + self._time_monitor = InferenceProgressCallback(len(dataset), update_progress_callback) + + predictions = self._infer_model(dataset, InferenceParameters(is_evaluation=True)) + prediction_results = zip(predictions["eval_predictions"], predictions["feature_vectors"]) + self._add_predictions_to_dataset(prediction_results, dataset, dump_soft_prediction, process_soft_prediction) + + logger.info("Inference completed") + return dataset + + def train( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + train_parameters: Optional[TrainParameters] = None, + seed: Optional[int] = None, + deterministic: bool = False, + ): + """Train function for OTX segmentation task. + + Actual training is processed by _train_model fucntion + """ + logger.info("train()") + # Check for stop signal when training has stopped. + # If should_stop is true, training was cancelled and no new + if self._should_stop: # type: ignore + logger.info("Training cancelled.") + self._should_stop = False + self._is_training = False + return + self.seed = seed + self.deterministic = deterministic + + # Set OTX LoggerHook & Time Monitor + if train_parameters: + update_progress_callback = train_parameters.update_progress + else: + update_progress_callback = default_progress_callback + self._time_monitor = TrainingProgressCallback(update_progress_callback) + + results = self._train_model(dataset) + + MemCacheHandlerSingleton.delete() + + # Check for stop signal when training has stopped. If should_stop is true, training was cancelled and no new + if self._should_stop: + logger.info("Training cancelled.") + self._should_stop = False + self._is_training = False + return + + # get output model + model_ckpt = results.get("final_ckpt") + if model_ckpt is None: + logger.error("cannot find final checkpoint from the results.") + # output_model.model_status = ModelStatus.FAILED + return + # update checkpoint to the newly trained model + self._model_ckpt = model_ckpt + + # get prediction on validation set + self._is_training = False + + # Get training metrics group from learning curves + training_metrics, best_score = self._generate_training_metrics(self._learning_curves) + performance = Performance( + score=ScoreMetric(value=best_score, name=self.metric), + dashboard_metrics=training_metrics, + ) + + logger.info(f"Final model performance: {str(performance)}") + # save resulting model + self.save_model(output_model) + output_model.performance = performance + self._is_training = False + logger.info("train done.") + + def export( + self, + export_type: ExportType, + output_model: ModelEntity, + precision: ModelPrecision = ModelPrecision.FP32, + dump_features: bool = True, + ): + """Export function of OTX Task.""" + logger.info("Exporting the model") + + self._update_model_export_metadata(output_model, export_type, precision, dump_features) + results = self._export_model(precision, export_type, dump_features) + outputs = results.get("outputs") + logger.debug(f"results of run_task = {outputs}") + if outputs is None: + raise RuntimeError(results.get("msg")) + + ir_extra_data = get_seg_model_api_configuration(self._task_environment.label_schema, self._hyperparams) + + if export_type == ExportType.ONNX: + ir_extra_data[("model_info", "mean_values")] = results.get("inference_parameters").get("mean_values") + ir_extra_data[("model_info", "scale_values")] = results.get("inference_parameters").get("scale_values") + + onnx_file = outputs.get("onnx") + embed_onnx_model_data(onnx_file, ir_extra_data) + with open(onnx_file, "rb") as f: + output_model.set_data("model.onnx", f.read()) + else: + bin_file = outputs.get("bin") + xml_file = outputs.get("xml") + + embed_ir_model_data(xml_file, ir_extra_data) + + with open(bin_file, "rb") as f: + output_model.set_data("openvino.bin", f.read()) + with open(xml_file, "rb") as f: + output_model.set_data("openvino.xml", f.read()) + + output_model.set_data("label_schema.json", label_schema_to_bytes(self._task_environment.label_schema)) + logger.info("Exporting completed") + + def explain( + self, + dataset: DatasetEntity, + explain_parameters: Optional[ExplainParameters] = None, + ) -> DatasetEntity: + """Main explain function of OTX Task.""" + raise NotImplementedError + + def evaluate( + self, + output_resultset: ResultSetEntity, + evaluation_metric: Optional[str] = None, + ): + """Evaluate function of OTX Segmentation Task.""" + logger.info("called evaluate()") + if evaluation_metric is not None: + logger.warning( + f"Requested to use {evaluation_metric} metric, " "but parameter is ignored. Use mDice instead." + ) + metric = MetricsHelper.compute_dice_averaged_over_pixels(output_resultset) + logger.info(f"mDice after evaluation: {metric.overall_dice.value}") + output_resultset.performance = metric.get_performance() + logger.info("Evaluation completed") + + def _add_predictions_to_dataset(self, prediction_results, dataset, dump_soft_prediction, process_soft_prediction): + """Loop over dataset again to assign predictions. Convert from MMSegmentation format to OTX format.""" + for dataset_item, (prediction, feature_vector) in zip(dataset, prediction_results): + soft_prediction = np.transpose(prediction[0], axes=(1, 2, 0)) + hard_prediction = create_hard_prediction_from_soft_prediction( + soft_prediction=soft_prediction, + soft_threshold=self._hyperparams.postprocessing.soft_threshold, + blur_strength=self._hyperparams.postprocessing.blur_strength, + ) + annotations = create_annotation_from_segmentation_map( + hard_prediction=hard_prediction, + soft_prediction=soft_prediction, + label_map=self._label_dictionary, + ) + dataset_item.append_annotations(annotations=annotations) + + if feature_vector is not None: + active_score = TensorEntity(name="representation_vector", numpy=feature_vector.reshape(-1)) + dataset_item.append_metadata_item(active_score, model=self._task_environment.model) + + if dump_soft_prediction: + for label_index, label in self._label_dictionary.items(): + current_label_soft_prediction = soft_prediction[:, :, label_index] + if process_soft_prediction: + current_label_soft_prediction = get_activation_map(current_label_soft_prediction) + else: + current_label_soft_prediction = (current_label_soft_prediction * 255).astype(np.uint8) + result_media = ResultMediaEntity( + name=label.name, + type="soft_prediction", + label=label, + annotation_scene=dataset_item.annotation_scene, + roi=dataset_item.roi, + numpy=current_label_soft_prediction, + ) + dataset_item.append_metadata_item(result_media, model=self._task_environment.model) + + def save_model(self, output_model: ModelEntity): + """Save best model weights in SegmentationTrainTask.""" + + if is_multigpu_child_process(): + return + logger.info("called save_model") + buffer = io.BytesIO() + hyperparams_str = ids_to_strings(cfg_helper.convert(self._hyperparams, dict, enum_to_str=True)) + labels = {label.name: label.color.rgb_tuple for label in self._labels} + model_ckpt = torch.load(self._model_ckpt) + modelinfo = { + "model": model_ckpt, + "config": hyperparams_str, + "labels": labels, + "input_size": self._input_size, + "VERSION": 1, + } + + torch.save(modelinfo, buffer) + output_model.set_data("weights.pth", buffer.getvalue()) + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self._task_environment.label_schema), + ) + output_model.precision = self._precision + + def _generate_training_metrics(self, learning_curves): + """Get Training metrics (epochs & scores). + + Parses the mmsegmentation logs to get metrics from the latest training run + :return output List[MetricsGroup] + """ + output: List[MetricsGroup] = [] + # Model architecture + architecture = InfoMetric(name="Model architecture", value=self._model_name) + visualization_info_architecture = VisualizationInfo( + name="Model architecture", visualisation_type=VisualizationType.TEXT + ) + output.append( + MetricsGroup( + metrics=[architecture], + visualization_info=visualization_info_architecture, + ) + ) + # Learning curves + best_score = -1 + for key, curve in learning_curves.items(): + metric_curve = CurveMetric(xs=curve.x, ys=curve.y, name=key) + if key == f"val/{self.metric}": + best_score = max(curve.y) + visualization_info = LineChartInfo(name=key, x_axis_label="Epoch", y_axis_label=key) + output.append(MetricsGroup(metrics=[metric_curve], visualization_info=visualization_info)) + + return output, best_score + + @abstractmethod + def _train_model(self, dataset: DatasetEntity): + """Train model and return the results.""" + raise NotImplementedError + + @abstractmethod + def _infer_model( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ): + """Get inference results from dataset.""" + raise NotImplementedError + + @abstractmethod + def _export_model(self, precision: ModelPrecision, export_format: ExportType, dump_features: bool): + """Export model and return the results.""" + raise NotImplementedError + + @abstractmethod + def _explain_model(self, dataset: DatasetEntity, explain_parameters: Optional[ExplainParameters]): + """Explain model and return the results.""" + raise NotImplementedError diff --git a/src/otx/algorithms/segmentation/tools/__init__.py b/src/otx/algorithms/segmentation/tools/__init__.py new file mode 100644 index 00000000000..8789f4d0b48 --- /dev/null +++ b/src/otx/algorithms/segmentation/tools/__init__.py @@ -0,0 +1,15 @@ +"""Collection of tools to run segmentation training extension.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/segmentation/tools/segmentation_sample.py b/src/otx/algorithms/segmentation/tools/segmentation_sample.py new file mode 100644 index 00000000000..0f5bb730939 --- /dev/null +++ b/src/otx/algorithms/segmentation/tools/segmentation_sample.py @@ -0,0 +1,407 @@ +"""Sample Code of otx training for detection.""" + +# Copyright (C) 2021-2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import argparse +import sys + +import cv2 +import numpy as np + +from otx.algorithms.common.utils import get_task_class +from otx.api.configuration.helper import create +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.utils.logger import get_logger + +logger = get_logger() + + +def parse_args(): + """Parse function for getting model template & check export.""" + parser = argparse.ArgumentParser(description="Sample showcasing the new API") + parser.add_argument("template_file_path", help="path to template file") + parser.add_argument("--export", action="store_true") + return parser.parse_args() + + +colors = [(128, 0, 0), (0, 128, 0)] + + +def load_test_dataset(data_type): + """Load Sample dataset for detection.""" + + def gen_circle_image(resolution): + width, height = resolution + image = np.full([height, width, 3], fill_value=128, dtype=np.uint8) + true_labels = np.full([height, width, 1], fill_value=0, dtype=np.uint8) + cv2.circle(image, (int(height / 2), int(width / 2)), 90, (0, 0, 255), -1) + cv2.circle(true_labels, (int(height / 2), int(width / 2)), 90, 1, -1) + return (image, true_labels) + + def gen_rect_image(resolution): + width, height = resolution + image = np.full([height, width, 3], fill_value=128, dtype=np.uint8) + true_labels = np.full([height, width, 1], fill_value=0, dtype=np.uint8) + cv2.rectangle( + image, + (int(height * 0.1), int(width * 0.1)), + (int(height / 2), int(width / 2)), + (0, 255, 0), + -1, + ) + cv2.rectangle( + true_labels, + (int(height * 0.1), int(width * 0.1)), + (int(height / 2), int(width / 2)), + 2, + -1, + ) + return (image, true_labels) + + labels = [ + LabelEntity(name="circle", domain=Domain.SEGMENTATION, id=1), # OLD class + LabelEntity(name="rect", domain=Domain.SEGMENTATION, id=2), + ] + + def get_image(data_type, subset, label_id): + ignored_labels = [] + if label_id == 1: + image, true_label = gen_circle_image((640, 480)) + if data_type == "new" and subset == Subset.TRAINING: + ignored_labels = [LabelEntity(name="rect", domain=Domain.SEGMENTATION, id=2)] + else: + image, true_label = gen_rect_image((640, 480)) + + height, width = true_label.shape[:2] + label_mask = true_label == label_id + label_index_map = label_mask.astype(np.uint8) + contours, hierarchies = cv2.findContours(label_index_map, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + for contour, hierarchy in zip(contours, hierarchies[0]): + if hierarchy[3] != -1: + continue + + contour = list(contour) + if len(contour) <= 2: + continue + + points = [Point(x=point[0][0] / (width - 1), y=point[0][1] / (height - 1)) for point in contour] + return DatasetItemEntity( + media=Image(data=image), + annotation_scene=AnnotationSceneEntity( + annotations=[ + Annotation( + Polygon(points=points), + labels=[ScoredLabel(label=labels[label_id - 1])], + ) + ], + kind=AnnotationSceneKind.ANNOTATION, + ), + subset=subset, + ignored_labels=ignored_labels, + ) + + old_train = [ + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + get_image("old", Subset.TRAINING, 1), + ] + + old_val = [ + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + get_image("old", Subset.VALIDATION, 1), + ] + + new_train = [ + get_image("new", Subset.TRAINING, 1), + get_image("new", Subset.TRAINING, 1), + get_image("new", Subset.TRAINING, 1), + get_image("new", Subset.TRAINING, 1), + get_image("new", Subset.TRAINING, 2), + get_image("new", Subset.TRAINING, 2), + get_image("new", Subset.TRAINING, 2), + get_image("new", Subset.TRAINING, 2), + ] + + new_val = [ + get_image("new", Subset.VALIDATION, 1), + get_image("new", Subset.VALIDATION, 1), + get_image("new", Subset.VALIDATION, 1), + get_image("new", Subset.VALIDATION, 1), + get_image("new", Subset.VALIDATION, 2), + get_image("new", Subset.VALIDATION, 2), + get_image("new", Subset.VALIDATION, 2), + get_image("new", Subset.VALIDATION, 2), + ] + old = old_train + old_val + new = new_train + new_val + if data_type == "old": + return DatasetEntity(old * 3), [labels[0]] + return DatasetEntity((old * 3 + new * 2)), labels + + +# pylint: disable=too-many-locals, too-many-statements +def main(args): + """Main function of Detection Sample.""" + logger.info("[SL] Train initial model with OLD dataset") + dataset, labels_list = load_test_dataset("old") + labels_schema = LabelSchemaEntity.from_labels(labels_list) + + logger.info(f"Train dataset: {len(dataset.get_subset(Subset.TRAINING))} items") + logger.info(f"Validation dataset: {len(dataset.get_subset(Subset.VALIDATION))} items") + + logger.info("Load model template") + model_template = parse_model_template(args.template_file_path) + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 5 + params.learning_parameters.batch_size = 8 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + initial_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, initial_model) + + logger.info("Class-incremental learning with OLD + NEW dataset") + dataset, labels_list = load_test_dataset("new") + labels_schema = LabelSchemaEntity.from_labels(labels_list) + + logger.info(f"Train dataset: {len(dataset.get_subset(Subset.TRAINING))} items") + logger.info(f"Validation dataset: {len(dataset.get_subset(Subset.VALIDATION))} items") + + logger.info("Load model template") + model_template = parse_model_template(args.template_file_path) + + logger.info("Set hyperparameters") + params = create(model_template.hyper_parameters.data) + params.learning_parameters.num_iters = 5 + params.learning_parameters.batch_size = 8 + + logger.info("Setup environment") + environment = TaskEnvironment( + model=initial_model, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + environment.model = ModelEntity( + train_dataset=dataset, + configuration=environment.get_model_configuration(), + model_adapters=initial_model.model_adapters, + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_task_class(task_impl_path) + task = task_cls(task_environment=environment) + + logger.info("Train model") + output_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.train(dataset, output_model) + + logger.info("Get predictions on the validation set") + validation_dataset = dataset.get_subset(Subset.VALIDATION) + predicted_validation_dataset = task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + task.evaluate(resultset) + logger.info(str(resultset.performance)) + + if args.export: + logger.info("Export model") + exported_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + task.export(ExportType.OPENVINO, exported_model) + + logger.info("Create OpenVINO Task") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + openvino_task_cls = get_task_class(openvino_task_impl_path) + openvino_task = openvino_task_cls(environment) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=exported_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Estimate quality on validation set") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + # TODO: Need to check POT with openvino 2022.2 + pot_check = "FAILED" + try: + # POT + logger.info("Run POT optimization") + optimized_model = ModelEntity( + dataset, + environment.get_model_configuration(), + ) + openvino_task.optimize(OptimizationType.POT, dataset, optimized_model, OptimizationParameters()) + + logger.info("Get predictions on the validation set") + predicted_validation_dataset = openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=optimized_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + logger.info("Performance of optimized model:") + openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + pot_check = "PASSED" + except Exception: # pylint: disable=broad-except + logger.warning("POT is not working..") + + nncf_check = "FAILED" + try: + # NNCF test + task_impl_path = model_template.entrypoints.nncf + nncf_task_cls = get_task_class(task_impl_path) + + print("Create NNCF Task") + environment.model = output_model + nncf_task_impl_path = model_template.entrypoints.nncf + nncf_task_cls = get_task_class(nncf_task_impl_path) + nncf_task = nncf_task_cls(environment) + + optimized_model = ModelEntity( + dataset, + configuration=environment.get_model_configuration(), + ) + nncf_task.optimize(OptimizationType.NNCF, dataset, optimized_model) + + logger.info("Inferring the optimised model on the validation set.") + predicted_validation_dataset = nncf_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + resultset = ResultSetEntity( + model=optimized_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + + logger.info("Evaluating the optimized model on the validation set.") + nncf_task.evaluate(resultset) + logger.info(str(resultset.performance)) + + logger.info("Exporting the model.") + exported_model = ModelEntity( + train_dataset=dataset, + configuration=environment.get_model_configuration(), + ) + nncf_task.export(ExportType.OPENVINO, exported_model) + environment.model = exported_model + + logger.info("Creating the OpenVINO Task.") + environment.model = exported_model + openvino_task_impl_path = model_template.entrypoints.openvino + nncf_openvino_task_cls = get_task_class(openvino_task_impl_path) + nncf_openvino_task = nncf_openvino_task_cls(environment) + + logger.info("Inferring the exported model on the validation set.") + predicted_validation_dataset = nncf_openvino_task.infer( + validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=True), + ) + + logger.info("Evaluating the exported model on the validation set.") + resultset = ResultSetEntity( + model=exported_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + nncf_openvino_task.evaluate(resultset) + logger.info(str(resultset.performance)) + nncf_check = "PASSED" + except Exception: # pylint: disable=broad-except + logger.warning("NNCF is not working") + + logger.info("train: PASSED") + logger.info("export: PASSED") + logger.info(f"POT: {pot_check}") + logger.info(f"NNCF: {nncf_check}") + + +if __name__ == "__main__": + sys.exit(main(parse_args()) or 0) diff --git a/src/otx/algorithms/segmentation/utils/__init__.py b/src/otx/algorithms/segmentation/utils/__init__.py new file mode 100644 index 00000000000..23a35640410 --- /dev/null +++ b/src/otx/algorithms/segmentation/utils/__init__.py @@ -0,0 +1,10 @@ +"""Collection of utils for task implementation in Segmentation Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from .metadata import get_seg_model_api_configuration +from .processing import get_activation_map + +__all__ = ["get_activation_map", "get_seg_model_api_configuration"] diff --git a/src/otx/algorithms/segmentation/utils/metadata.py b/src/otx/algorithms/segmentation/utils/metadata.py new file mode 100644 index 00000000000..0245d3de03c --- /dev/null +++ b/src/otx/algorithms/segmentation/utils/metadata.py @@ -0,0 +1,28 @@ +"""Utils for hadnling metadata of segmentation models.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from mmcv.utils import ConfigDict + +from otx.api.entities.label_schema import LabelSchemaEntity + + +def get_seg_model_api_configuration(label_schema: LabelSchemaEntity, hyperparams: ConfigDict): + """Get ModelAPI config.""" + all_labels = "" + all_label_ids = "" + for lbl in label_schema.get_labels(include_empty=False): + all_labels += lbl.name.replace(" ", "_") + " " + all_label_ids += f"{lbl.id_} " + + return { + ("model_info", "model_type"): "Segmentation", + ("model_info", "soft_threshold"): str(hyperparams.postprocessing.soft_threshold), + ("model_info", "blur_strength"): str(hyperparams.postprocessing.blur_strength), + ("model_info", "return_soft_prediction"): "True", + ("model_info", "labels"): all_labels.strip(), + ("model_info", "label_ids"): all_label_ids.strip(), + ("model_info", "task_type"): "segmentation", + } diff --git a/src/otx/algorithms/segmentation/utils/processing.py b/src/otx/algorithms/segmentation/utils/processing.py new file mode 100644 index 00000000000..380e4d41ea9 --- /dev/null +++ b/src/otx/algorithms/segmentation/utils/processing.py @@ -0,0 +1,26 @@ +"""Utils for processing of segmentation results.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from typing import Iterable, Union + +import cv2 +import numpy as np + + +def get_activation_map(features: Union[np.ndarray, Iterable, int, float], normalize: bool = True): + """Getter activation_map functions.""" + if normalize: + min_soft_score = np.min(features) + max_soft_score = np.max(features) + factor = 255.0 / (max_soft_score - min_soft_score + 1e-12) + + float_act_map = factor * (features - min_soft_score) + int_act_map = np.uint8(np.floor(float_act_map)) + else: + int_act_map = features + + int_act_map = cv2.applyColorMap(int_act_map, cv2.COLORMAP_JET) + int_act_map = cv2.cvtColor(int_act_map, cv2.COLOR_BGR2RGB) + return int_act_map diff --git a/src/otx/algorithms/visual_prompting/__init__.py b/src/otx/algorithms/visual_prompting/__init__.py new file mode 100644 index 00000000000..37330de0d28 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/__init__.py @@ -0,0 +1,4 @@ +"""OTX Algorithms - Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/visual_prompting/adapters/__init__.py b/src/otx/algorithms/visual_prompting/adapters/__init__.py new file mode 100644 index 00000000000..8bbf482bb01 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/__init__.py @@ -0,0 +1,4 @@ +"""OTX Algorithms - Visual Prompting adapters.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/visual_prompting/adapters/openvino/__init__.py b/src/otx/algorithms/visual_prompting/adapters/openvino/__init__.py new file mode 100644 index 00000000000..d56d86cdd80 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/openvino/__init__.py @@ -0,0 +1,17 @@ +"""OpenVINO modules for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .model_wrappers import * # noqa: F403 diff --git a/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py new file mode 100644 index 00000000000..1c22c536057 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py @@ -0,0 +1,17 @@ +"""Wrapper Initialization of OTX Visual Prompting.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .openvino_models import Decoder, ImageEncoder # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_models.py b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_models.py new file mode 100644 index 00000000000..c703df70218 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_models.py @@ -0,0 +1,155 @@ +"""Openvino Model Wrappers of OTX Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from copy import deepcopy +from typing import Any, Dict, List, Optional, Tuple, Union + +import numpy as np +from openvino.model_api.adapters.inference_adapter import InferenceAdapter +from openvino.model_api.models import ImageModel, SegmentationModel +from openvino.model_api.models.types import NumericalValue, StringValue + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.pipelines import ResizeLongestSide + + +class ImageEncoder(ImageModel): + """Image encoder class for visual prompting of openvino model wrapper.""" + + __model__ = "image_encoder" + + def __init__(self, inference_adapter, configuration=None, preload=False): + super().__init__(inference_adapter, configuration, preload) + + @classmethod + def parameters(cls) -> Dict[str, Any]: # noqa: D102 + parameters = super().parameters() + parameters.update( + { + "resize_type": StringValue(default_value="fit_to_window"), + "image_size": NumericalValue(value_type=int, default_value=1024, min=0, max=2048), + "downsizing": NumericalValue(value_type=int, default_value=64, min=1, max=1024), + } + ) + return parameters + + def preprocess( + self, inputs: np.ndarray, extra_processing: bool = False + ) -> Tuple[Dict[str, np.ndarray], Dict[str, Any]]: + """Update meta for image encoder.""" + dict_inputs, meta = super().preprocess(inputs) + if extra_processing: + dict_inputs["images"] = ResizeLongestSide.apply_image(dict_inputs["images"][0], self.image_size).transpose( + 2, 0, 1 + )[None] + meta["resize_type"] = self.resize_type + return dict_inputs, meta + + +class Decoder(SegmentationModel): + """Decoder class for visual prompting of openvino model wrapper.""" + + __model__ = "decoder" + + def __init__( + self, + model_adapter: InferenceAdapter, + configuration: Optional[dict] = None, + preload: bool = False, + ): + super().__init__(model_adapter, configuration, preload) + + self.mask_input = np.zeros((1, 1, 256, 256), dtype=np.float32) + self.has_mask_input = np.zeros((1, 1), dtype=np.float32) + + @classmethod + def parameters(cls): # noqa: D102 + parameters = super().parameters() + parameters.update({"image_size": NumericalValue(value_type=int, default_value=1024, min=0, max=2048)}) + parameters.update({"mask_threshold": NumericalValue(value_type=float, default_value=0.0, min=0, max=1)}) + return parameters + + def _get_outputs(self): + return "upscaled_masks" + + def preprocess(self, inputs: Dict[str, Any], meta: Dict[str, Any]) -> List[Dict[str, Any]]: + """Preprocess prompts.""" + processed_prompts = [] + for prompt_name in ["bboxes", "points"]: + for prompt, label in zip(inputs.get(prompt_name), inputs["labels"].get(prompt_name, [])): + if prompt_name == "bboxes": + point_coords = self._apply_coords(prompt.reshape(-1, 2, 2), inputs["original_size"]) + point_labels = np.array([2, 3], dtype=np.float32).reshape(-1, 2) + else: + point_coords = self._apply_coords(prompt.reshape(-1, 1, 2), inputs["original_size"]) + point_labels = np.array([1], dtype=np.float32).reshape(-1, 1) + + processed_prompts.append( + { + "point_coords": point_coords, + "point_labels": point_labels, + "mask_input": self.mask_input, + "has_mask_input": self.has_mask_input, + "orig_size": np.asarray(inputs["original_size"], dtype=np.int64).reshape(-1, 2), + "label": label, + } + ) + return processed_prompts + + def _apply_coords(self, coords: np.ndarray, original_size: Union[List[int], Tuple[int, int]]) -> np.ndarray: + """Process coords according to preprocessed image size using image meta.""" + old_h, old_w = original_size + new_h, new_w = self._get_preprocess_shape(original_size[0], original_size[1], self.image_size) + coords = deepcopy(coords).astype(np.float32) + coords[..., 0] = coords[..., 0] * (new_w / old_w) + coords[..., 1] = coords[..., 1] * (new_h / old_h) + return coords + + def _get_preprocess_shape(self, old_h: int, old_w: int, image_size: int) -> Tuple[int, int]: + """Compute the output size given input size and target image size.""" + scale = image_size / max(old_h, old_w) + new_h, new_w = old_h * scale, old_w * scale + new_w = int(new_w + 0.5) + new_h = int(new_h + 0.5) + return (new_h, new_w) + + def _check_io_number(self, number_of_inputs, number_of_outputs): + pass + + def _get_inputs(self): + """Get input layer name and shape.""" + image_blob_names = [name for name in self.inputs.keys()] + image_info_blob_names = [] + return image_blob_names, image_info_blob_names + + def postprocess(self, outputs: Dict[str, np.ndarray], meta: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]: + """Postprocess to convert soft prediction to hard prediction. + + Args: + outputs (Dict[str, np.ndarray]): The output of the model. + meta (Dict[str, Any]): Contain label and original size. + + Returns: + hard_prediction (np.ndarray): The hard prediction. + soft_prediction (np.ndarray): The soft prediction. + """ + probability = max(min(float(outputs["scores"]), 1.0), 0.0) + hard_prediction = outputs[self.output_blob_name].squeeze() > self.mask_threshold + soft_prediction = hard_prediction * probability + + meta["soft_prediction"] = soft_prediction + meta["label"].probability = probability + + return hard_prediction, soft_prediction diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/__init__.py new file mode 100644 index 00000000000..c86b8fe514d --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/__init__.py @@ -0,0 +1,15 @@ +"""Pytorch lightning modules for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/__init__.py new file mode 100644 index 00000000000..0b8f9b0c619 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/__init__.py @@ -0,0 +1,17 @@ +"""Callbacks for OTX inference.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .inference import InferenceCallback, ZeroShotInferenceCallback # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/inference.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/inference.py new file mode 100644 index 00000000000..df751eeaba5 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/inference.py @@ -0,0 +1,135 @@ +"""Inference Callbacks for OTX inference.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from typing import Any, List + +import numpy as np +from bson import ObjectId +from pytorch_lightning import LightningModule, Trainer +from pytorch_lightning.callbacks import Callback + +from otx.api.entities.annotation import Annotation +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.utils.segmentation_utils import ( + create_annotation_from_segmentation_map, + create_hard_prediction_from_soft_prediction, +) + + +class InferenceCallback(Callback): + """Callback that updates otx_dataset during inference. + + Args: + otx_dataset (DatasetEntity): Dataset that predictions will be updated. + """ + + def __init__(self, otx_dataset: DatasetEntity): + # decide if using mask or polygon annotations for predictions + if any(isinstance(shape, Image) for shape in otx_dataset[0].annotation_scene.shapes): + self.use_mask = True + else: + self.use_mask = False + self.otx_dataset = otx_dataset.with_empty_annotations() + + def on_predict_epoch_end(self, _trainer: Trainer, _pl_module: LightningModule, outputs: List[Any]) -> None: + """Call when the predict epoch ends.""" + # collect generic predictions + pred_masks: List = [] + iou_predictions: List = [] + pred_labels: List = [] + for output in outputs[0]: + pred_masks.append(output["masks"][0]) + iou_predictions.append(output["iou_predictions"][0]) + pred_labels.append(output["labels"][0].get("bboxes", []) + output["labels"][0].get("points", [])) + + for dataset_item, pred_mask, iou_prediction, labels in zip( + self.otx_dataset, pred_masks, iou_predictions, pred_labels + ): + annotations: List[Annotation] = [] + for soft_prediction, iou, label in zip(pred_mask, iou_prediction, labels): + probability = max(min(float(iou), 1.0), 0.0) + label.probability = probability + soft_prediction = soft_prediction.numpy() + hard_prediction = create_hard_prediction_from_soft_prediction( + soft_prediction=soft_prediction, soft_threshold=0.5 + ) + + if self.use_mask: + # set mask as annotation + annotation = [ + Annotation( + shape=Image( + data=hard_prediction.astype(np.uint8), size=hard_prediction.shape + ), # type: ignore[arg-type] + labels=[ScoredLabel(label=label.label, probability=probability)], + id=ID(ObjectId()), + ) + ] + else: + # generate polygon annotations + annotation = create_annotation_from_segmentation_map( + hard_prediction=hard_prediction, + soft_prediction=soft_prediction, + label_map={1: label.label}, + ) + + annotations.extend(annotation) + if self.use_mask: + dataset_item.annotation_scene.append_annotations(annotations) + else: + dataset_item.append_annotations(annotations) + + +class ZeroShotInferenceCallback(Callback): + """Callback that updates otx_dataset during zero-shot inference. + + Args: + otx_dataset (DatasetEntity): Dataset that predictions will be updated. + label_schema (LabelSchemaEntity): Label schema information. + """ + + def __init__(self, otx_dataset: DatasetEntity, label_schema: LabelSchemaEntity): + # TODO (sungchul): consider use_mask + self.otx_dataset = otx_dataset.with_empty_annotations() + self.label_schema = {int(label.id): label for label in label_schema.get_labels(include_empty=True)} + + def on_predict_epoch_end(self, _trainer: Trainer, _pl_module: LightningModule, outputs: List[Any]) -> None: + """Call when the predict epoch ends.""" + for batch_output, dataset_item in zip(outputs[0], self.otx_dataset): + # TODO (sungchul): currently, single batch inference is only supported + output = batch_output[0] + annotations: List[Annotation] = [] + for label, masks in output.items(): + for soft_prediction in map(lambda x: x.numpy(), masks): + hard_prediction = create_hard_prediction_from_soft_prediction( + soft_prediction=soft_prediction, soft_threshold=0.5 + ) + + # TODO (sungchul): consider use_mask + # generate polygon annotations + annotation = create_annotation_from_segmentation_map( + hard_prediction=hard_prediction, + soft_prediction=soft_prediction, + label_map={1: self.label_schema.get(label)}, + ) + annotations.extend(annotation) + + # TODO (sungchul): consider use_mask + dataset_item.append_annotations(annotations) diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/config/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/config/__init__.py new file mode 100644 index 00000000000..274b680fb06 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/config/__init__.py @@ -0,0 +1,20 @@ +"""Config setting for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .visual_prompting_config import ( + get_visual_promtping_config, # noqa: F401 + update_visual_prompting_config, # noqa: F401 +) diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/config/visual_prompting_config.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/config/visual_prompting_config.py new file mode 100644 index 00000000000..3e4cbe8b574 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/config/visual_prompting_config.py @@ -0,0 +1,123 @@ +"""Set configurable parameters for Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import os +from pathlib import Path +from typing import Optional, Union + +from omegaconf import DictConfig, ListConfig, OmegaConf + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.utils.logger import get_logger + +logger = get_logger() + + +def get_visual_promtping_config( + task_name: str, + otx_config: ConfigurableParameters, + config_dir: str, + mode: str = "train", + model_checkpoint: Optional[str] = None, + resume_from_checkpoint: Optional[str] = None, +) -> Union[DictConfig, ListConfig]: + """Get visual prompting configuration. + + Create a visual prompting config object that matches the values specified in the + OTX config. + + Args: + task_name (str): Task name to load configuration from visual prompting. + otx_config (ConfigurableParameters): OTX config object parsed from `configuration.yaml` file. + config_dir (str): Path to load raw `config.yaml` or save updated `config.yaml`. + mode (str): Mode to run visual prompting task. Default: "train". + model_checkpoint (Optional[str]): Path to the checkpoint to load the model weights. + resume_from_checkpoint (Optional[str]): Path to the checkpoint to resume training. + + Returns: + Union[DictConfig, ListConfig]: Visual prompting config object for the specified model type + with overwritten default values. + """ + if os.path.isfile(os.path.join(config_dir, "config.yaml")): + # If there is already a config.yaml file in the output path, load it + config_path = os.path.join(config_dir, "config.yaml") + else: + # Load the default config.yaml file + logger.info("[*] Load default config.yaml.") + config_path = f"src/otx/algorithms/visual_prompting/configs/{task_name.lower()}/config.yaml" + + config = OmegaConf.load(config_path) + logger.info(f"[*] Load configuration file at {config_path}") + + update_visual_prompting_config(config, otx_config) + + if mode == "train": + # update model_checkpoint + if model_checkpoint: + config.model.checkpoint = model_checkpoint + + # update resume_from_checkpoint + config.trainer.resume_from_checkpoint = resume_from_checkpoint + + save_path = Path(os.path.join(config_dir, "config.yaml")) + save_path.write_text(OmegaConf.to_yaml(config)) + logger.info(f"[*] Save updated configuration file at {str(save_path)}") + + return config + + +def update_visual_prompting_config( + visual_prompting_config: Union[DictConfig, ListConfig], otx_config: ConfigurableParameters +) -> None: + """Update visual prompting configuration. + + Overwrite the default parameter values in the visual prompting config with the + values specified in the OTX config. The function is recursively called for + each parameter group present in the OTX config. + + Args: + visual_prompting_config (Union[DictConfig, ListConfig]): Visual prompting config object + for the specified model type with overwritten default values. + otx_config (ConfigurableParameters): OTX config object parsed from configuration.yaml file. + """ + groups = getattr(otx_config, "groups", None) + if groups: + for group in groups: + if group in [ + "learning_parameters", + "nncf_optimization", + "pot_parameters", + "postprocessing", + "algo_backend", + ]: + if group in ["nncf_optimization"]: + # TODO (sungchul): Consider nncf_optimization + logger.warning(f"{group} will be implemented.") + continue + update_visual_prompting_config(visual_prompting_config, getattr(otx_config, group)) + else: + update_visual_prompting_config(visual_prompting_config[group], getattr(otx_config, group)) + + parameters = getattr(otx_config, "parameters") + for param in parameters: + if param not in visual_prompting_config.keys(): + logger.info(f"[*] {param} is not presented in visual prompting config.") + logger.info(f" --> Available parameters are {visual_prompting_config.keys()}") + continue + sc_value = getattr(otx_config, param) + sc_value = sc_value.value if hasattr(sc_value, "value") else sc_value + logger.info(f"[*] Update {param}: {visual_prompting_config[param]} -> {sc_value}") + visual_prompting_config[param] = sc_value diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/__init__.py new file mode 100644 index 00000000000..9c9fe5170ab --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/__init__.py @@ -0,0 +1,11 @@ +"""OTX Algorithms - Visual prompting Dataset.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .dataset import ( + OTXVisualPromptingDataModule, # noqa: F401 + OTXVisualPromptingDataset, # noqa: F401 + get_transform, # noqa: F401 +) +from .pipelines import MultipleInputsCompose, Pad, ResizeLongestSide # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/dataset.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/dataset.py new file mode 100644 index 00000000000..dcf336776b1 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/dataset.py @@ -0,0 +1,472 @@ +"""Visual Prompting Dataset & DataModule.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from collections import defaultdict +from typing import Any, DefaultDict, Dict, List, Optional, Union + +import cv2 +import numpy as np +from omegaconf import DictConfig, ListConfig +from pytorch_lightning import LightningDataModule +from torch import Tensor +from torch.utils.data import DataLoader, Dataset +from torchvision import transforms + +from otx.algorithms.common.configs.training_base import TrainType +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.pipelines import ( + MultipleInputsCompose, + Pad, + ResizeLongestSide, + collate_fn, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.label import LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Polygon +from otx.api.entities.subset import Subset +from otx.api.utils.shape_factory import ShapeFactory +from otx.utils.logger import get_logger + +logger = get_logger() + + +def get_transform( + image_size: int = 1024, mean: List[float] = [123.675, 116.28, 103.53], std: List[float] = [58.395, 57.12, 57.375] +) -> MultipleInputsCompose: + """Get transform pipeline. + + Args: + image_size (int): Size of image. Defaults to 1024. + mean (List[float]): Mean for normalization. Defaults to [123.675, 116.28, 103.53]. + std (List[float]): Standard deviation for normalization. Defaults to [58.395, 57.12, 57.375]. + + Returns: + MultipleInputsCompose: Transform pipeline. + """ + return MultipleInputsCompose( + [ + ResizeLongestSide(target_length=image_size), + Pad(), + transforms.Normalize(mean=mean, std=std), + ] + ) + + +def convert_polygon_to_mask(shape: Polygon, width: int, height: int) -> np.ndarray: + """Convert polygon to mask. + + Args: + shape (Polygon): Polygon to convert. + width (int): Width of image. + height (int): Height of image. + + Returns: + np.ndarray: Generated mask from given polygon. + """ + polygon = ShapeFactory.shape_as_polygon(shape) + contour = [[int(point.x * width), int(point.y * height)] for point in polygon.points] + gt_mask = np.zeros(shape=(height, width), dtype=np.uint8) + gt_mask = cv2.drawContours(gt_mask, np.asarray([contour]), 0, 1, -1) + return gt_mask + + +def generate_bbox( # noqa: D417 + x1: int, y1: int, x2: int, y2: int, width: int, height: int, offset_bbox: int = 0 +) -> List[int]: + """Generate bounding box. + + Args: + x1, y1, x2, y2 (int): Bounding box coordinates. # type: ignore + width (int): Width of image. + height (int): Height of image. + offset_bbox (int): Offset to apply to the bounding box, defaults to 0. + + Returns: + List[int]: Generated bounding box. + """ + + def get_randomness(length: int) -> int: + if offset_bbox == 0: + return 0 + return np.random.normal(0, min(length * 0.1, offset_bbox)) + + bbox = [ + max(0, x1 + get_randomness(width)), + max(0, y1 + get_randomness(height)), + min(width, x2 + get_randomness(width)), + min(height, y2 + get_randomness(height)), + ] + return bbox + + +def generate_bbox_from_mask(gt_mask: np.ndarray, width: int, height: int) -> List[int]: + """Generate bounding box from given mask. + + Args: + gt_mask (np.ndarry): Mask to generate bounding box. + width (int): Width of image. + height (int): Height of image. + + Returns: + List[int]: Generated bounding box from given mask. + """ + y_indices, x_indices = np.where(gt_mask == 1) + x_min, x_max = np.min(x_indices), np.max(x_indices) + y_min, y_max = np.min(y_indices), np.max(y_indices) + return generate_bbox(x_min, y_min, x_max, y_max, width, height) + + +class OTXVisualPromptingDataset(Dataset): + """Visual Prompting Dataset Adaptor. + + Args: + dataset (DatasetEntity): Dataset entity. + image_size (int): Target size to resize image. + mean (List[float]): Mean for normalization. + std (List[float]): Standard deviation for normalization. + offset_bbox (int): Offset to apply to the bounding box, defaults to 0. + """ + + def __init__( + self, + mode: Subset, + dataset: DatasetEntity, + image_size: int, + mean: List[float], + std: List[float], + offset_bbox: int = 0, + use_point: bool = False, + use_bbox: bool = False, + ) -> None: + self.mode = mode + self.dataset = dataset + self.transform = get_transform(image_size, mean, std) + self.offset_bbox = offset_bbox + self.labels = dataset.get_labels() + + if not use_bbox and not use_point: + # if both are False, use bbox as default + use_bbox = True + self.prob = 1.0 # if using only bbox prompt + if use_bbox and use_point: + # if using both prompts, divide prob into both + self.prob = 0.5 + if not use_bbox and use_point: + # if using only point prompt + self.prob = 0.0 + + def __len__(self) -> int: + """Get size of the dataset. + + Returns: + int: Size of the dataset. + """ + return len(self.dataset) + + @staticmethod + def get_prompts( + dataset_item: DatasetItemEntity, + dataset_labels: List[LabelEntity], + prob: float = 1.0, + mode: Subset = Subset.TESTING, + ) -> Dict[str, Any]: + """Get propmts from dataset_item. + + Args: + dataset_item (DatasetItemEntity): Dataset item entity. + dataset_labels (List[LabelEntity]): Label information. + prob (float): Probability of which prompts will be generated. + mode (Subset): To check which mode is used between training, validation, and testing. + + Returns: + Dict[str, Any]: Processed prompts with ground truths. + """ + width, height = dataset_item.width, dataset_item.height + bboxes: List[np.ndarray] = [] + points: List[np.ndarray] = [] + gt_masks: List[np.ndarray] = [] + labels: DefaultDict[str, List[ScoredLabel]] = defaultdict(list) + for annotation in dataset_item.get_annotations(labels=dataset_labels, include_empty=False, preserve_id=True): + if isinstance(annotation.shape, Image): + # use mask as-is + gt_mask = annotation.shape.numpy.astype(np.uint8) + elif isinstance(annotation.shape, Polygon): + # convert polygon to mask + gt_mask = convert_polygon_to_mask(annotation.shape, width, height) + else: + continue + + if gt_mask.sum() == 0: + # pass no gt or very small region + continue + + gt_masks.append(gt_mask) + + mask_points = np.nonzero(gt_mask) + if np.random.rand() < prob: + # generate bbox based on gt_mask + bbox = generate_bbox_from_mask(gt_mask, width, height) + bboxes.append(bbox) + labels["bboxes"].extend(annotation.get_labels(include_empty=False)) + else: + # generate point based on gt_mask + if mode == Subset.TRAINING: + # get random point from the mask + idx_chosen = np.random.permutation(len(mask_points[0]))[0] # noqa: NPY002 + point = np.array([mask_points[1][idx_chosen], mask_points[0][idx_chosen]]) + else: + # get averaged point + point = np.array([mask_points[1].mean(), mask_points[0].mean()]) + points.append(point) + labels["points"].extend(annotation.get_labels(include_empty=False)) + + bboxes = np.array(bboxes, dtype=np.float32) if len(bboxes) > 0 else np.zeros((0, 4), dtype=np.float32) + points = np.array(points, dtype=np.float32) if len(points) > 0 else np.zeros((0, 2), dtype=np.float32) + return dict( + original_size=np.array((height, width), dtype=np.int64), + gt_masks=gt_masks, + bboxes=bboxes, + points=points, + labels=labels, + ) + + def __getitem__(self, index: int) -> Dict[str, Union[int, List, Tensor]]: + """Get dataset item. + + Args: + index (int): Index of the dataset sample. + + Returns: + Dict[str, Union[int, List, Tensor]]: Dataset item. + """ + dataset_item = self.dataset[index] + item: Dict[str, Union[int, Tensor]] = {"index": index, "images": dataset_item.numpy} + + prompts = self.get_prompts(dataset_item, self.labels, self.prob, self.mode) + if len(prompts["gt_masks"]) == 0: + return { + "images": [], + "bboxes": [], + "points": [], + "gt_masks": [], + "original_size": [], + "path": [], + "labels": [], + } + + item.update({**prompts, "path": dataset_item.media.path}) + item = self.transform(item) + return item + + +class OTXZeroShotVisualPromptingDataset(OTXVisualPromptingDataset): + """Visual Prompting for Zero-shot learning Dataset Adaptor.""" + + def __getitem__(self, index: int) -> Dict[str, Union[int, List, Tensor]]: + """Get dataset item. + + Args: + index (int): Index of the dataset sample. + + Returns: + Dict[str, Union[int, List, Tensor]]: Dataset item. + """ + dataset_item = self.dataset[index] + item: Dict[str, Union[int, Tensor]] = {"index": index, "images": dataset_item.numpy} + + prompts = self.get_prompts(dataset_item, self.labels, self.prob) + item.update({**prompts, "path": dataset_item.media.path}) + + return item + + +class OTXVisualPromptingDataModule(LightningDataModule): + """Visual Prompting DataModule. + + Args: + config (Union[DictConfig, ListConfig]): Configuration. + dataset (DatasetEntity): Dataset entity. + """ + + DATASETS = { + TrainType.Incremental: OTXVisualPromptingDataset, + TrainType.Zeroshot: OTXZeroShotVisualPromptingDataset, + } + + def __init__( + self, + config: Union[DictConfig, ListConfig], + dataset: DatasetEntity, + train_type: TrainType = TrainType.Incremental, + ) -> None: + super().__init__() + self.config = config + self.dataset = dataset + self.train_type = train_type + self.kwargs = {} + if self.train_type == TrainType.Zeroshot: + # check zero-shot configs + if self.config.get("train_batch_size", 1) != 1: + logger.warning( + ( + f"Zero-shot learning only supports single batch, " + f"update {self.config.get('train_batch_size', 1)} to 1." + ) + ) + self.config["train_batch_size"] = 1 + + self.kwargs.update( + { + "use_point": self.config.get("use_point", False), + "use_bbox": self.config.get("use_bbox", False), + } + ) + + self.train_otx_dataset: DatasetEntity + self.val_otx_dataset: DatasetEntity + self.test_otx_dataset: DatasetEntity + self.predict_otx_dataset: DatasetEntity + + def setup(self, stage: Optional[str] = None) -> None: + """Setup Visual Prompting Data Module. + + Args: + stage (Optional[str], optional): train/val/test stages, defaults to None. + """ + if not stage == "predict": + self.summary() + + image_size = self.config.image_size + mean = self.config.normalize.mean + std = self.config.normalize.std + if stage == "fit" or stage is None: + self.train_dataset = self.DATASETS[self.train_type]( + mode=Subset.TRAINING, + dataset=self.dataset.get_subset(Subset.TRAINING), + image_size=image_size, + mean=mean, + std=std, + offset_bbox=self.config.offset_bbox, + **self.kwargs, + ) + + # self.val_dataset = None + if self.train_type == TrainType.Incremental: + self.val_dataset = self.DATASETS[self.train_type]( + mode=Subset.VALIDATION, + dataset=self.dataset.get_subset(Subset.VALIDATION), + image_size=image_size, + mean=mean, + std=std, + **self.kwargs, + ) + + if stage == "test": + self.test_dataset = self.DATASETS[self.train_type]( + mode=Subset.TESTING, + dataset=self.dataset.get_subset(Subset.TESTING), + image_size=image_size, + mean=mean, + std=std, + **self.kwargs, + ) + + if stage == "predict": + self.predict_dataset = self.DATASETS[self.train_type]( + mode=Subset.TESTING, + dataset=self.dataset.get_subset(Subset.TESTING), + image_size=image_size, + mean=mean, + std=std, + **self.kwargs, + ) + + def summary(self): + """Print size of the dataset, number of images.""" + for subset in [Subset.TRAINING, Subset.VALIDATION, Subset.TESTING]: + dataset = self.dataset.get_subset(subset) + num_items = len(dataset) + logger.info( + "'%s' subset size: Total '%d' images.", + subset, + num_items, + ) + + def train_dataloader(self) -> DataLoader: + """Train Dataloader. + + Returns: + DataLoader: Train dataloader. + """ + return DataLoader( + self.train_dataset, + shuffle=True, + batch_size=self.config.train_batch_size, + num_workers=self.config.num_workers, + collate_fn=collate_fn + if self.train_type != TrainType.Zeroshot + else lambda x: x, # type: ignore[return-value] + ) + + def val_dataloader(self) -> DataLoader: + """Validation Dataloader. + + Returns: + DataLoader: Validation Dataloader. + """ + return DataLoader( + self.val_dataset, + shuffle=False, + batch_size=self.config.val_batch_size, + num_workers=self.config.num_workers, + collate_fn=collate_fn + if self.train_type != TrainType.Zeroshot + else lambda x: x, # type: ignore[return-value] + ) + + def test_dataloader(self) -> DataLoader: + """Test Dataloader. + + Returns: + DataLoader: Test Dataloader. + """ + return DataLoader( + self.test_dataset, + shuffle=False, + batch_size=self.config.test_batch_size, + num_workers=self.config.num_workers, + collate_fn=collate_fn + if self.train_type != TrainType.Zeroshot + else lambda x: x, # type: ignore[return-value] + ) + + def predict_dataloader(self) -> DataLoader: + """Predict Dataloader. + + Returns: + DataLoader: Predict Dataloader. + """ + return DataLoader( + self.predict_dataset, + shuffle=False, + batch_size=1, + num_workers=self.config.num_workers, + collate_fn=collate_fn + if self.train_type != TrainType.Zeroshot + else lambda x: x, # type: ignore[return-value] + ) diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/__init__.py new file mode 100644 index 00000000000..f14f12c37cd --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/__init__.py @@ -0,0 +1,7 @@ +"""OTX Algorithms - Visual prompting pipelines.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .sam_transforms import ResizeLongestSide # noqa: F401 +from .transforms import MultipleInputsCompose, Pad, collate_fn # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/sam_transforms.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/sam_transforms.py new file mode 100644 index 00000000000..63a58b9229e --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/sam_transforms.py @@ -0,0 +1,126 @@ +"""SAM transfrom pipeline for visual prompting task.""" + +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# + +from typing import Dict, List, Tuple, Union + +import numpy as np +import torch +from torch import Tensor +from torchvision.transforms.functional import resize, to_pil_image # type: ignore + + +class ResizeLongestSide: + """Resizes images to the longest side target_length, as well as provides methods for resizing coordinates and boxes. + + Provides methods for transforming both numpy array and batched torch tensors. + + Args: + target_length (int): The length of the longest side of the image. + """ + + def __init__(self, target_length: int) -> None: + self.target_length = target_length + + def __call__(self, item: Dict[str, Union[List, Tensor]]) -> Dict[str, Union[List, Tensor]]: + """Applies the transformation to a single sample. + + Args: + item (Dict[str, Union[List, Tensor]]): Dictionary of batch data. + + Returns: + Dict[str, Union[List, Tensor]]: Dictionary of batch data. + """ + item["images"] = torch.as_tensor( + self.apply_image(item["images"], self.target_length).transpose((2, 0, 1)), dtype=torch.get_default_dtype() + ) + if "gt_masks" in item: + item["gt_masks"] = [torch.as_tensor(gt_mask) for gt_mask in item["gt_masks"]] + if "bboxes" in item: + item["bboxes"] = self.apply_boxes(item["bboxes"], item["original_size"], self.target_length) + if "points" in item: + item["points"] = self.apply_coords(item["points"], item["original_size"], self.target_length) + return item + + @classmethod + def apply_image(cls, image: np.ndarray, target_length: int) -> np.ndarray: + """Expects a numpy array with shape HxWxC in uint8 format. + + Args: + image (np.ndarray): Image array. + target_length (int): The length of the longest side of the image. + + Returns: + np.ndarray: Resized image. + """ + target_size = cls.get_preprocess_shape(image.shape[0], image.shape[1], target_length) + return np.array(resize(to_pil_image(image), target_size)) + + @classmethod + def apply_coords( + cls, + coords: Union[np.ndarray, Tensor], + original_size: Union[List[int], Tuple[int, int], Tensor], + target_length: int, + ) -> Union[np.ndarray, Tensor]: + """Expects a numpy array / torch tensor of length 2 in the final dimension. + + Requires the original image size in (H, W) format. + + Args: + coords (Union[np.ndarray, Tensor]): Coordinates array/tensor. + original_size (Union[List[int], Tuple[int, int], Tensor]): Original size of image. + target_length (int): The length of the longest side of the image. + + Returns: + Union[np.ndarray, Tensor]: Resized coordinates. + """ + old_h, old_w = original_size + new_h, new_w = cls.get_preprocess_shape(original_size[0], original_size[1], target_length) + if isinstance(coords, np.ndarray): + coords = coords.astype(np.float32) + else: + coords = coords.to(torch.float32) + coords[..., 0] = coords[..., 0] * (new_w / old_w) + coords[..., 1] = coords[..., 1] * (new_h / old_h) + return coords + + @classmethod + def apply_boxes( + cls, + boxes: Union[np.ndarray, Tensor], + original_size: Union[List[int], Tuple[int, int], Tensor], + target_length: int, + ) -> Union[np.ndarray, Tensor]: + """Expects a numpy array / torch tensor shape Bx4. Requires the original image size in (H, W) format. + + Args: + boxes (Union[np.ndarray, Tensor]): Boxes array/tensor. + original_size (Union[List[int], Tuple[int, int], Tensor]): Original size of image. + target_length (int): The length of the longest side of the image. + + Returns: + Union[np.ndarray, Tensor]: Resized boxes. + """ + boxes = cls.apply_coords(boxes.reshape(-1, 2, 2), original_size, target_length) + return boxes.reshape(-1, 4) + + @staticmethod + def get_preprocess_shape(oldh: int, oldw: int, long_side_length: int) -> Tuple[int, int]: + """Compute the output size given input size and target long side length. + + Args: + oldh (int): Original height. + oldw (int): Original width. + long_side_length (int): Target long side length. + + Returns: + Tuple[int, int]: Output size. + """ + scale = long_side_length * 1.0 / max(oldh, oldw) + newh, neww = oldh * scale, oldw * scale + neww = int(neww + 0.5) + newh = int(newh + 0.5) + return (newh, neww) diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/transforms.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/transforms.py new file mode 100644 index 00000000000..dd1abddf740 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/transforms.py @@ -0,0 +1,111 @@ +"""Collection of transfrom pipelines for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Any, Dict, List, Union + +import torch +from torch import Tensor +from torchvision import transforms +from torchvision.transforms import Compose +from torchvision.transforms.functional import pad # type: ignore + + +def collate_fn(batch: List[Any]) -> Dict: + """Collate function for dataloader. + + Args: + batch (List): List of batch data. + + Returns: + Dict: Collated batch data. + """ + + def _convert_empty_to_none(x: str, dtype: torch.dtype = torch.float32) -> List: + """Convert empty list to None. + + Args: + x (str): Key of batch data. + dtype (torch.dtype): Dtype to be applied to tensors. + + Returns: + List: List of batch data. + """ + func = torch.stack if x == "gt_masks" else torch.tensor + items = [func(item[x]).to(dtype) if len(item[x]) > 0 else None for item in batch] + return items + + index = [item["index"] for item in batch] + images = torch.stack([item["images"] for item in batch]) + bboxes = _convert_empty_to_none("bboxes") + points = _convert_empty_to_none("points") + gt_masks = _convert_empty_to_none("gt_masks", torch.int32) + original_size = _convert_empty_to_none("original_size") + path = [item["path"] for item in batch] + labels = [item["labels"] for item in batch] + if gt_masks: + return { + "index": index, + "images": images, + "bboxes": bboxes, + "points": points, + "gt_masks": gt_masks, + "original_size": original_size, + "path": path, + "labels": labels, + } + return { + "index": -1, + "images": [], + "bboxes": [], + "points": [], + "gt_masks": [], + "original_size": [], + "path": [], + "labels": [], + } + + +class Pad: + """Pad images, gt_masks, bboxes, and points to the same size.""" + + def __call__(self, item: Dict[str, Union[List[Any], Tensor]]) -> Dict[str, Union[int, Tensor]]: + """Pad images, gt_masks, bboxes, and points to the same size. + + Args: + item (Dict[str, Union[int, Tensor]]): Input item. + + Returns: + Dict[str, Union[int, Tensor]]: Padded item. + """ + _, h, w = item["images"].shape # type: ignore + max_dim = max(w, h) + pad_w = max_dim - w + pad_h = max_dim - h + padding = (0, 0, pad_w, pad_h) + + item["images"] = pad(item["images"], padding, fill=0, padding_mode="constant") + + return item + + +class MultipleInputsCompose(Compose): + """Composes several transforms have multiple inputs together.""" + + def __call__(self, item: Dict[str, Union[int, Tensor]]) -> Dict[str, Union[int, Tensor]]: + """Composes several transforms have multiple inputs together. + + Args: + item (Dict[str, Union[int, Tensor]]): Input item. + + Returns: + Dict[str, Union[int, Tensor]]: Transformed item. + """ + for t in self.transforms: + if isinstance(t, transforms.Normalize): + item["images"] = t(item["images"]) + else: + item = t(item) + return item diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/__init__.py new file mode 100644 index 00000000000..49caf262735 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/__init__.py @@ -0,0 +1,6 @@ +"""Visual prompting model.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .visual_prompters import SegmentAnything, ZeroShotSegmentAnything # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/__init__.py new file mode 100644 index 00000000000..9aa16f2e359 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/__init__.py @@ -0,0 +1,7 @@ +"""Backbones for visual prompting model.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .tiny_vit import build_tiny_vit # noqa: F401 +from .vit import build_vit # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/tiny_vit.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/tiny_vit.py new file mode 100644 index 00000000000..32be4f4c0e9 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/tiny_vit.py @@ -0,0 +1,744 @@ +"""TinyViT for MobileSAM.""" + +# Copyright (c) 2022 Microsoft +# https://github.com/ChaoningZhang/MobileSAM +# + +import itertools +from typing import Dict, List, Optional, Set, Tuple, Union + +import torch +import torch.nn.functional as F +from timm.models.layers import DropPath as TimmDropPath +from timm.models.layers import to_2tuple, trunc_normal_ +from torch import Tensor, nn +from torch.utils import checkpoint + + +class Conv2d_BN(nn.Sequential): + """Conv2d_BN for TinyViT.""" + + def __init__( + self, + a: int, + b: int, + ks: int = 1, + stride: int = 1, + pad: int = 0, + dilation: int = 1, + groups: int = 1, + bn_weight_init: float = 1.0, + ) -> None: + super().__init__() + self.add_module("c", nn.Conv2d(a, b, ks, stride, pad, dilation, groups, bias=False)) + bn = nn.BatchNorm2d(b) + nn.init.constant_(bn.weight, bn_weight_init) + nn.init.constant_(bn.bias, 0) + self.add_module("bn", bn) + + @torch.no_grad() + def fuse(self) -> nn.Module: + """Fuse weights and biases.""" + c, bn = self._modules.values() + w = bn.weight / (bn.running_var + bn.eps) ** 0.5 + w = c.weight * w[:, None, None, None] + b = bn.bias - bn.running_mean * bn.weight / (bn.running_var + bn.eps) ** 0.5 + m = nn.Conv2d( + w.size(1) * self.c.groups, + w.size(0), + w.shape[2:], + stride=self.c.stride, + padding=self.c.padding, + dilation=self.c.dilation, + groups=self.c.groups, + ) + m.weight.data.copy_(w) + m.bias.data.copy_(b) + return m + + +class DropPath(TimmDropPath): + """DropPath for TinyViT.""" + + def __init__(self, drop_prob: Union[List[float], float] = None) -> None: + super().__init__(drop_prob=drop_prob) + self.drop_prob = drop_prob + + def __repr__(self) -> str: # noqa: D105 + msg = super().__repr__() + msg += f"(drop_prob={self.drop_prob})" + return msg + + +class PatchEmbed(nn.Module): + """PatchEmbed for TinyViT.""" + + def __init__(self, in_chans: int, embed_dim: int, resolution: int, activation: nn.Module) -> None: + super().__init__() + img_size: Tuple[int, int] = to_2tuple(resolution) + self.patches_resolution = (img_size[0] // 4, img_size[1] // 4) + self.num_patches = self.patches_resolution[0] * self.patches_resolution[1] + self.in_chans = in_chans + self.embed_dim = embed_dim + n = embed_dim + self.seq = nn.Sequential( + Conv2d_BN(in_chans, n // 2, 3, 2, 1), + activation(), + Conv2d_BN(n // 2, n, 3, 2, 1), + ) + + def forward(self, x: Tensor) -> Tensor: + """Forward call.""" + return self.seq(x) + + +class MBConv(nn.Module): + """MBConv for TinyViT.""" + + def __init__( + self, + in_chans: int, + out_chans: int, + expand_ratio: float, + activation: nn.Module, + drop_path: Union[List[float], float], + ) -> None: + super().__init__() + self.in_chans = in_chans + self.hidden_chans = int(in_chans * expand_ratio) + self.out_chans = out_chans + + self.conv1 = Conv2d_BN(in_chans, self.hidden_chans, ks=1) + self.act1 = activation() + + self.conv2 = Conv2d_BN(self.hidden_chans, self.hidden_chans, ks=3, stride=1, pad=1, groups=self.hidden_chans) + self.act2 = activation() + + self.conv3 = Conv2d_BN(self.hidden_chans, out_chans, ks=1, bn_weight_init=0.0) + self.act3 = activation() + + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() # type: ignore + + def forward(self, x: Tensor) -> Tensor: + """Forward call.""" + shortcut = x + + x = self.conv1(x) + x = self.act1(x) + + x = self.conv2(x) + x = self.act2(x) + + x = self.conv3(x) + + x = self.drop_path(x) + + x += shortcut + x = self.act3(x) + + return x + + +class PatchMerging(nn.Module): + """PatchMerging for TinyViT.""" + + def __init__(self, input_resolution: Tuple[int, int], dim: int, out_dim: int, activation: nn.Module) -> None: + super().__init__() + + self.input_resolution = input_resolution + self.dim = dim + self.out_dim = out_dim + self.act = activation() + self.conv1 = Conv2d_BN(dim, out_dim, 1, 1, 0) + stride_c = 2 + if out_dim == 320 or out_dim == 448 or out_dim == 576: + stride_c = 1 + self.conv2 = Conv2d_BN(out_dim, out_dim, 3, stride_c, 1, groups=out_dim) + self.conv3 = Conv2d_BN(out_dim, out_dim, 1, 1, 0) + + def forward(self, x: Tensor) -> Tensor: + """Forward call.""" + if x.ndim == 3: + H, W = self.input_resolution + B = len(x) + # (B, C, H, W) + x = x.view(B, H, W, -1).permute(0, 3, 1, 2) + + x = self.conv1(x) + x = self.act(x) + + x = self.conv2(x) + x = self.act(x) + x = self.conv3(x) + x = x.flatten(2).transpose(1, 2) + return x + + +class ConvLayer(nn.Module): + """ConvLayer for TinyViT.""" + + def __init__( + self, + dim: int, + input_resolution: int, + depth: int, + activation: nn.Module, + drop_path: Union[List[float], float] = 0.0, + downsample: Optional[nn.Module] = None, + use_checkpoint: bool = False, + out_dim: Optional[int] = None, + conv_expand_ratio: float = 4.0, + ) -> None: + + super().__init__() + self.dim = dim + self.input_resolution = input_resolution + self.depth = depth + self.use_checkpoint = use_checkpoint + + # build blocks + self.blocks = nn.ModuleList( + [ + MBConv( + dim, + dim, + conv_expand_ratio, + activation, + drop_path[i] if isinstance(drop_path, list) else drop_path, + ) + for i in range(depth) + ] + ) + + # patch merging layer + if downsample is not None: + self.downsample = downsample(input_resolution, dim=dim, out_dim=out_dim, activation=activation) + else: + self.downsample = None + + def forward(self, x: Tensor) -> Tensor: + """Forward call.""" + for blk in self.blocks: + if self.use_checkpoint: + x = checkpoint.checkpoint(blk, x) + else: + x = blk(x) + if self.downsample is not None: + x = self.downsample(x) + return x + + +class Mlp(nn.Module): + """MLP for TinyViT.""" + + def __init__( + self, + in_features: int, + hidden_features: Optional[int] = None, + out_features: Optional[int] = None, + act_layer: nn.Module = nn.GELU, + drop: float = 0.0, + ) -> None: + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.norm = nn.LayerNorm(in_features) + self.fc1 = nn.Linear(in_features, hidden_features) + self.fc2 = nn.Linear(hidden_features, out_features) + self.act = act_layer() + self.drop = nn.Dropout(drop) + + def forward(self, x: Tensor) -> Tensor: + """Forward call.""" + x = self.norm(x) + + x = self.fc1(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + return x + + +class Attention(nn.Module): + """Attention block for TinyViT.""" + + def __init__( + self, + dim: int, + key_dim: int, + num_heads: int = 8, + attn_ratio: int = 4, + resolution: Tuple[int, int] = (14, 14), + ) -> None: + + super().__init__() + # (h, w) + assert isinstance(resolution, tuple) and len(resolution) == 2 + self.num_heads = num_heads + self.scale = key_dim**-0.5 + self.key_dim = key_dim + self.nh_kd = nh_kd = key_dim * num_heads + self.d = int(attn_ratio * key_dim) + self.dh = int(attn_ratio * key_dim) * num_heads + self.attn_ratio = attn_ratio + h = self.dh + nh_kd * 2 + + self.norm = nn.LayerNorm(dim) + self.qkv = nn.Linear(dim, h) + self.proj = nn.Linear(self.dh, dim) + + points = list(itertools.product(range(resolution[0]), range(resolution[1]))) + N = len(points) + attention_offsets: Dict[int, int] = {} + idxs = [] + for p1 in points: + for p2 in points: + offset = (abs(p1[0] - p2[0]), abs(p1[1] - p2[1])) + if offset not in attention_offsets: + attention_offsets[offset] = len(attention_offsets) # type: ignore + idxs.append(attention_offsets[offset]) # type: ignore + self.attention_biases = nn.Parameter(torch.zeros(num_heads, len(attention_offsets))) + self.register_buffer("attention_bias_idxs", torch.LongTensor(idxs).view(N, N), persistent=False) + + @torch.no_grad() + def train(self, mode: bool = True) -> None: # noqa: D102 + super().train(mode) + if mode and hasattr(self, "ab"): + del self.ab + else: + self.register_buffer("ab", self.attention_biases[:, self.attention_bias_idxs], persistent=False) + + def forward(self, x: Tensor) -> Tensor: # x (B,N,C) + """Forward call.""" + B, N, _ = x.shape + + # Normalization + x = self.norm(x) + + qkv = self.qkv(x) + # (B, N, num_heads, d) + q, k, v = qkv.view(B, N, self.num_heads, -1).split([self.key_dim, self.key_dim, self.d], dim=3) + # (B, num_heads, N, d) + q = q.permute(0, 2, 1, 3) + k = k.permute(0, 2, 1, 3) + v = v.permute(0, 2, 1, 3) + + attn = (q @ k.transpose(-2, -1)) * self.scale + ( + self.attention_biases[:, self.attention_bias_idxs] if self.training else self.ab + ) + attn = attn.softmax(dim=-1) + x = (attn @ v).transpose(1, 2).reshape(B, N, self.dh) + x = self.proj(x) + return x + + +class TinyViTBlock(nn.Module): + r"""TinyViT Block. + + Args: + dim (int): Number of input channels. + input_resolution (tuple[int, int]): Input resolution. + num_heads (int): Number of attention heads. + window_size (int): Window size. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. + drop (float, optional): Dropout rate. Default: 0.0 + drop_path (float, optional): Stochastic depth rate. Default: 0.0 + local_conv_size (int): the kernel size of the convolution between + Attention and MLP. Default: 3 + activation: the activation function. Default: nn.GELU + """ + + def __init__( + self, + dim: int, + input_resolution: Tuple[int, int], + num_heads: int, + window_size: int = 7, + mlp_ratio: float = 4.0, + drop: float = 0.0, + drop_path: Union[List[float], float] = 0.0, + local_conv_size: int = 3, + activation: nn.Module = nn.GELU, + ) -> None: + + super().__init__() + self.dim = dim + self.input_resolution = input_resolution + self.num_heads = num_heads + assert window_size > 0, "window_size must be greater than 0" + self.window_size = window_size + self.mlp_ratio = mlp_ratio + + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() # type: ignore + + assert dim % num_heads == 0, "dim must be divisible by num_heads" + head_dim = dim // num_heads + + window_resolution = (window_size, window_size) + self.attn = Attention(dim, head_dim, num_heads, attn_ratio=1, resolution=window_resolution) + + mlp_hidden_dim = int(dim * mlp_ratio) + mlp_activation = activation + self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=mlp_activation, drop=drop) + + pad = local_conv_size // 2 + self.local_conv = Conv2d_BN(dim, dim, ks=local_conv_size, stride=1, pad=pad, groups=dim) + + def forward(self, x: Tensor) -> Tensor: + """Forward call.""" + H, W = self.input_resolution + B, L, C = x.shape + assert L == H * W, "input feature has wrong size" + res_x = x + if H == self.window_size and W == self.window_size: + x = self.attn(x) + else: + x = x.view(B, H, W, C) + pad_b = (self.window_size - H % self.window_size) % self.window_size + pad_r = (self.window_size - W % self.window_size) % self.window_size + padding = pad_b > 0 or pad_r > 0 + + if padding: + x = F.pad(x, (0, 0, 0, pad_r, 0, pad_b)) + + pH, pW = H + pad_b, W + pad_r + nH = pH // self.window_size + nW = pW // self.window_size + # window partition + x = ( + x.view(B, nH, self.window_size, nW, self.window_size, C) + .transpose(2, 3) + .reshape(B * nH * nW, self.window_size * self.window_size, C) + ) + x = self.attn(x) + # window reverse + x = x.view(B, nH, nW, self.window_size, self.window_size, C).transpose(2, 3).reshape(B, pH, pW, C) + + if padding: + x = x[:, :H, :W].contiguous() + + x = x.view(B, L, C) + + x = res_x + self.drop_path(x) + + x = x.transpose(1, 2).reshape(B, C, H, W) + x = self.local_conv(x) + x = x.view(B, C, L).transpose(1, 2) + + x = x + self.drop_path(self.mlp(x)) + return x + + def extra_repr(self) -> str: # noqa: D102 + return ( + f"dim={self.dim}, input_resolution={self.input_resolution}, num_heads={self.num_heads}, " + f"window_size={self.window_size}, mlp_ratio={self.mlp_ratio}" + ) + + +class BasicLayer(nn.Module): + """A basic TinyViT layer for one stage. + + Args: + dim (int): Number of input channels. + input_resolution (tuple[int]): Input resolution. + depth (int): Number of blocks. + num_heads (int): Number of attention heads. + window_size (int): Local window size. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. + drop (float, optional): Dropout rate. Default: 0.0 + drop_path (float | tuple[float], optional): Stochastic depth rate. Default: 0.0 + downsample (nn.Module | None, optional): Downsample layer at the end of the layer. Default: None + use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False. + local_conv_size: the kernel size of the depthwise convolution between attention and MLP. Default: 3 + activation: the activation function. Default: nn.GELU + out_dim: the output dimension of the layer. Default: dim + """ + + def __init__( + self, + dim: int, + input_resolution: Tuple[int, int], + depth: int, + num_heads: int, + window_size: int, + mlp_ratio: float = 4.0, + drop: float = 0.0, + drop_path: Union[List[float], float] = 0.0, + downsample: Optional[nn.Module] = None, + use_checkpoint: bool = False, + local_conv_size: int = 3, + activation: nn.Module = nn.GELU, + out_dim: Optional[int] = None, + ) -> None: + + super().__init__() + self.dim = dim + self.input_resolution = input_resolution + self.depth = depth + self.use_checkpoint = use_checkpoint + + # build blocks + self.blocks = nn.ModuleList( + [ + TinyViTBlock( + dim=dim, + input_resolution=input_resolution, + num_heads=num_heads, + window_size=window_size, + mlp_ratio=mlp_ratio, + drop=drop, + drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path, + local_conv_size=local_conv_size, + activation=activation, + ) + for i in range(depth) + ] + ) + + # patch merging layer + if downsample is not None: + self.downsample = downsample(input_resolution, dim=dim, out_dim=out_dim, activation=activation) + else: + self.downsample = None + + def forward(self, x: Tensor) -> Tensor: + """Forward call.""" + for blk in self.blocks: + if self.use_checkpoint: + x = checkpoint.checkpoint(blk, x) + else: + x = blk(x) + if self.downsample is not None: + x = self.downsample(x) + return x + + def extra_repr(self) -> str: # noqa: D102 + return f"dim={self.dim}, input_resolution={self.input_resolution}, depth={self.depth}" + + +class LayerNorm2d(nn.Module): + """2D-Layer Normalize for TinyViT.""" + + def __init__(self, num_channels: int, eps: float = 1e-6) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(num_channels)) + self.bias = nn.Parameter(torch.zeros(num_channels)) + self.eps = eps + + def forward(self, x: Tensor) -> Tensor: + """Forward call.""" + u = x.mean(1, keepdim=True) + s = (x - u).pow(2).mean(1, keepdim=True) + x = (x - u) / torch.sqrt(s + self.eps) + x = self.weight[:, None, None] * x + self.bias[:, None, None] + return x + + +class TinyViT(nn.Module): + """TinyViT for MobileSAM.""" + + def __init__( + self, + img_size: int = 224, + in_chans: int = 3, + num_classes: int = 1000, + embed_dims: List[int] = [96, 192, 384, 768], + depths: List[int] = [2, 2, 6, 2], + num_heads: List[int] = [3, 6, 12, 24], + window_sizes: List[int] = [7, 7, 14, 7], + mlp_ratio: float = 4.0, + drop_rate: float = 0.0, + drop_path_rate: float = 0.1, + use_checkpoint: bool = False, + mbconv_expand_ratio: float = 4.0, + local_conv_size: int = 3, + layer_lr_decay: float = 1.0, + ) -> None: + + super().__init__() + self.img_size = img_size + self.num_classes = num_classes + self.depths = depths + self.num_layers = len(depths) + self.mlp_ratio = mlp_ratio + + activation = nn.GELU + + self.patch_embed = PatchEmbed( + in_chans=in_chans, embed_dim=embed_dims[0], resolution=img_size, activation=activation + ) + + patches_resolution = self.patch_embed.patches_resolution + self.patches_resolution = patches_resolution + + # stochastic depth + dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule + + # build layers + self.layers = nn.ModuleList() + for i_layer in range(self.num_layers): + kwargs = dict( + dim=embed_dims[i_layer], + input_resolution=( + patches_resolution[0] // (2 ** (i_layer - 1 if i_layer == 3 else i_layer)), + patches_resolution[1] // (2 ** (i_layer - 1 if i_layer == 3 else i_layer)), + ), + # input_resolution=(patches_resolution[0] // (2 ** i_layer), + # patches_resolution[1] // (2 ** i_layer)), + depth=depths[i_layer], + drop_path=dpr[sum(depths[:i_layer]) : sum(depths[: i_layer + 1])], + downsample=PatchMerging if (i_layer < self.num_layers - 1) else None, + use_checkpoint=use_checkpoint, + out_dim=embed_dims[min(i_layer + 1, len(embed_dims) - 1)], + activation=activation, + ) + if i_layer == 0: + layer = ConvLayer( + conv_expand_ratio=mbconv_expand_ratio, + **kwargs, + ) + else: + layer = BasicLayer( + num_heads=num_heads[i_layer], + window_size=window_sizes[i_layer], + mlp_ratio=self.mlp_ratio, + drop=drop_rate, + local_conv_size=local_conv_size, + **kwargs, + ) + self.layers.append(layer) + + # Classifier head + # self.norm_head = nn.LayerNorm(embed_dims[-1]) + # self.head = nn.Linear( + # embed_dims[-1], num_classes) if num_classes > 0 else nn.Identity() + + # init weights + self.apply(self._init_weights) + self.set_layer_lr_decay(layer_lr_decay) + self.neck = nn.Sequential( + nn.Conv2d( + embed_dims[-1], + 256, + kernel_size=1, + bias=False, + ), + LayerNorm2d(256), + nn.Conv2d( + 256, + 256, + kernel_size=3, + padding=1, + bias=False, + ), + LayerNorm2d(256), + ) + + def set_layer_lr_decay(self, layer_lr_decay: float) -> None: + """Set layer lr decay.""" + decay_rate = layer_lr_decay + + # layers -> blocks (depth) + depth = sum(self.depths) + lr_scales = [decay_rate ** (depth - i - 1) for i in range(depth)] + # print("LR SCALES:", lr_scales) + + def _set_lr_scale(m, scale): + for p in m.parameters(): + p.lr_scale = scale + + self.patch_embed.apply(lambda x: _set_lr_scale(x, lr_scales[0])) + i = 0 + for layer in self.layers: + for block in layer.blocks: + block.apply(lambda x: _set_lr_scale(x, lr_scales[i])) + i += 1 + if layer.downsample is not None: + layer.downsample.apply(lambda x: _set_lr_scale(x, lr_scales[i - 1])) + assert i == depth + # for m in [self.norm_head, self.head]: + # m.apply(lambda x: _set_lr_scale(x, lr_scales[-1])) + + for k, p in self.named_parameters(): + p.param_name = k + + def _check_lr_scale(m): + for p in m.parameters(): + assert hasattr(p, "lr_scale"), p.param_name + + self.apply(_check_lr_scale) + + def _init_weights(self, m: nn.Module) -> None: + """Initialize weights.""" + if isinstance(m, nn.Linear): + trunc_normal_(m.weight, std=0.02) + if isinstance(m, nn.Linear) and m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.LayerNorm): + nn.init.constant_(m.bias, 0) + nn.init.constant_(m.weight, 1.0) + + @torch.jit.ignore + def no_weight_decay_keywords(self) -> Set[str]: + """Keyworkds for no weight decay.""" + return {"attention_biases"} + + def forward_features(self, x: Tensor) -> Tensor: + """Forward call. + + Args: + x (Tensor): Input image tensor with shape (B, C, H, W). + + Returns: + Tensor: Output tensor with shape (B, H', W', C'). + """ + # x: (N, C, H, W) + x = self.patch_embed(x) + + x = self.layers[0](x) + start_i = 1 + + for i in range(start_i, len(self.layers)): + layer = self.layers[i] + x = layer(x) + B, _, C = x.size() + x = x.view(B, 64, 64, C) + x = x.permute(0, 3, 1, 2) + x = self.neck(x) + return x + + def forward(self, x: Tensor) -> Tensor: + """Forward call. + + Args: + x (Tensor): Input image tensor with shape (B, C, H, W). + + Returns: + Tensor: Output tensor with shape (B, H', W', C'). + """ + x = self.forward_features(x) + # x = self.norm_head(x) + # x = self.head(x) + return x + + +def build_tiny_vit(img_size: int = 1024, drop_path_rate: float = 0.0): + """Build TinyViT backbone. + + Args: + img_size (int): Input image size. + drop_path_rate (float): Drop path rate for stochastic depth. + + Returns: + TinyViT: TinyViT backbone. + """ + return TinyViT( + img_size=img_size, + num_classes=1, + embed_dims=[64, 128, 160, 320], + depths=[2, 2, 6, 2], + num_heads=[2, 4, 5, 10], + window_sizes=[7, 7, 14, 7], + drop_path_rate=drop_path_rate, + ) diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/vit.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/vit.py new file mode 100644 index 00000000000..6ef1d934cad --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/vit.py @@ -0,0 +1,472 @@ +"""Vision Transformers.""" + +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# + +from functools import partial +from typing import Optional, Tuple, Type + +import torch +import torch.nn.functional as F +from torch import Tensor, nn + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.utils import ( + LayerNorm2d, + MLPBlock, +) + + +# This class and its supporting functions below lightly adapted from the ViTDet backbone available at: https://github.com/facebookresearch/detectron2/blob/main/detectron2/modeling/backbone/vit.py # noqa +class ViT(nn.Module): + """Vision Transformer for visual prompting task. + + Args: + img_size (int): Input image size. + patch_size (int): Patch size. + in_chans (int): Number of input image channels. + embed_dim (int): Patch embedding dimension. + depth (int): Depth of ViT. + num_heads (int): Number of attention heads in each ViT block. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. + out_chans (int): Number of output channels. + qkv_bias (bool): If True, add a learnable bias to query, key, value. + norm_layer (nn.Module): Normalization layer. + act_layer (nn.Module): Activation layer. + use_abs_pos (bool): If True, use absolute positional embeddings. + use_rel_pos (bool): If True, add relative positional embeddings to the attention map. + rel_pos_zero_init (bool): If True, zero initialize relative positional parameters. + window_size (int): Window size for window attention blocks. + global_attn_indexes (list): Indexes for blocks using global attention. + """ + + def __init__( + self, + img_size: int = 1024, + patch_size: int = 16, + in_chans: int = 3, + embed_dim: int = 768, + depth: int = 12, + num_heads: int = 12, + mlp_ratio: float = 4.0, + out_chans: int = 256, + qkv_bias: bool = True, + norm_layer: nn.Module = nn.LayerNorm, + act_layer: nn.Module = nn.GELU, + use_abs_pos: bool = True, + use_rel_pos: bool = False, + rel_pos_zero_init: bool = True, + window_size: int = 0, + global_attn_indexes: Tuple[int, ...] = (), + ) -> None: + + super().__init__() + self.img_size = img_size + + self.patch_embed = PatchEmbed( + kernel_size=(patch_size, patch_size), + stride=(patch_size, patch_size), + in_chans=in_chans, + embed_dim=embed_dim, + ) + + self.pos_embed: Optional[nn.Parameter] = None + if use_abs_pos: + # Initialize absolute positional embedding with pretrain image size. + self.pos_embed = nn.Parameter(torch.zeros(1, img_size // patch_size, img_size // patch_size, embed_dim)) + + self.blocks = nn.ModuleList() + for i in range(depth): + block = Block( + dim=embed_dim, + num_heads=num_heads, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + norm_layer=norm_layer, + act_layer=act_layer, + use_rel_pos=use_rel_pos, + rel_pos_zero_init=rel_pos_zero_init, + window_size=window_size if i not in global_attn_indexes else 0, + input_size=(img_size // patch_size, img_size // patch_size), + ) + self.blocks.append(block) + + self.neck = nn.Sequential( + nn.Conv2d( + embed_dim, + out_chans, + kernel_size=1, + bias=False, + ), + LayerNorm2d(out_chans), + nn.Conv2d( + out_chans, + out_chans, + kernel_size=3, + padding=1, + bias=False, + ), + LayerNorm2d(out_chans), + ) + + def forward(self, x: Tensor) -> Tensor: + """Forward function. + + Args: + x (Tensor): Input tensor of shape (B, C, H, W). + + Returns: + Tensor: Output tensor of shape (B, out_chans, H, W). + """ + x = self.patch_embed(x) + if self.pos_embed is not None: + x = x + self.pos_embed + + for blk in self.blocks: + x = blk(x) + + x = self.neck(x.permute(0, 3, 1, 2)) + + return x + + +class Block(nn.Module): + """Transformer blocks with support of window attention and residual propagation blocks. + + Args: + dim (int): Number of input channels. + num_heads (int): Number of attention heads in each ViT block. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. + qkv_bias (bool): If True, add a learnable bias to query, key, value. + norm_layer (nn.Module): Normalization layer. + act_layer (nn.Module): Activation layer. + use_rel_pos (bool): If True, add relative positional embeddings to the attention map. + rel_pos_zero_init (bool): If True, zero initialize relative positional parameters. + window_size (int): Window size for window attention blocks. If it equals 0, then + use global attention. + input_size (tuple(int, int) or None): Input resolution for calculating the relative + positional parameter size. + """ + + def __init__( + self, + dim: int, + num_heads: int, + mlp_ratio: float = 4.0, + qkv_bias: bool = True, + norm_layer: Type[nn.Module] = nn.LayerNorm, + act_layer: Type[nn.Module] = nn.GELU, + use_rel_pos: bool = False, + rel_pos_zero_init: bool = True, + window_size: int = 0, + input_size: Optional[Tuple[int, int]] = None, + ) -> None: + + super().__init__() + self.norm1 = norm_layer(dim) + self.attn = Attention( + dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + use_rel_pos=use_rel_pos, + rel_pos_zero_init=rel_pos_zero_init, + input_size=input_size if window_size == 0 else (window_size, window_size), + ) + + self.norm2 = norm_layer(dim) + self.mlp = MLPBlock(embedding_dim=dim, mlp_dim=int(dim * mlp_ratio), act=act_layer) + + self.window_size = window_size + + def forward(self, x: Tensor) -> Tensor: + """Forward function. + + Args: + x (Tensor): Input tensor of shape (B, H, W, C). + + Returns: + Tensor: Output tensor of shape (B, H, W, C). + """ + shortcut = x + x = self.norm1(x) + # Window partition + if self.window_size > 0: + H, W = x.shape[1], x.shape[2] + x, pad_hw = window_partition(x, self.window_size) + + x = self.attn(x) + # Reverse window partition + if self.window_size > 0: + x = window_unpartition(x, self.window_size, pad_hw, (H, W)) + + x = shortcut + x + x = x + self.mlp(self.norm2(x)) + + return x + + +class Attention(nn.Module): + """Multi-head Attention block with relative position embeddings. + + Args: + dim (int): Number of input channels. + num_heads (int): Number of attention heads. + qkv_bias (bool): If True, add a learnable bias to query, key, value. + use_rel_pos (bool): If True, add relative positional embeddings to the attention map. + rel_pos_zero_init (bool): If True, zero initialize relative positional parameters. + input_size (tuple(int, int) or None): Input resolution for calculating the relative + positional parameter size. + """ + + def __init__( + self, + dim: int, + num_heads: int = 8, + qkv_bias: bool = True, + use_rel_pos: bool = False, + rel_pos_zero_init: bool = True, + input_size: Optional[Tuple[int, int]] = None, + ) -> None: + + super().__init__() + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = head_dim**-0.5 + + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.proj = nn.Linear(dim, dim) + + self.use_rel_pos = use_rel_pos + if self.use_rel_pos: + assert input_size is not None, "Input size must be provided if using relative positional encoding." + # initialize relative positional embeddings + self.rel_pos_h = nn.Parameter(torch.zeros(2 * input_size[0] - 1, head_dim)) + self.rel_pos_w = nn.Parameter(torch.zeros(2 * input_size[1] - 1, head_dim)) + + def forward(self, x: Tensor) -> Tensor: + """Forward function. + + Args: + x (Tensor): Input tensor of shape (B, H, W, C). + + Returns: + Tensor: Output tensor of shape (B, H, W, C). + """ + B, H, W, _ = x.shape + # qkv with shape (3, B, nHead, H * W, C) + qkv = self.qkv(x).reshape(B, H * W, 3, self.num_heads, -1).permute(2, 0, 3, 1, 4) + # q, k, v with shape (B * nHead, H * W, C) + q, k, v = qkv.reshape(3, B * self.num_heads, H * W, -1).unbind(0) + + attn = (q * self.scale) @ k.transpose(-2, -1) + + if self.use_rel_pos: + attn = add_decomposed_rel_pos(attn, q, self.rel_pos_h, self.rel_pos_w, (H, W), (H, W)) + + attn = attn.softmax(dim=-1) + x = (attn @ v).view(B, self.num_heads, H, W, -1).permute(0, 2, 3, 1, 4).reshape(B, H, W, -1) + x = self.proj(x) + + return x + + +def window_partition(x: Tensor, window_size: int) -> Tuple[Tensor, Tuple[int, int]]: + """Partition into non-overlapping windows with padding if needed. + + Args: + x (Tensor): Input tokens with [B, H, W, C]. + window_size (int): Window size. + + Returns: + windows (Tensor): windows after partition with [B * num_windows, window_size, window_size, C]. + (Hp, Wp) (Tuple[int, int]): padded height and width before partition + """ + B, H, W, C = x.shape + + pad_h = (window_size - H % window_size) % window_size + pad_w = (window_size - W % window_size) % window_size + if pad_h > 0 or pad_w > 0: + x = F.pad(x, (0, 0, 0, pad_w, 0, pad_h)) + Hp, Wp = H + pad_h, W + pad_w + + x = x.view(B, Hp // window_size, window_size, Wp // window_size, window_size, C) + windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C) + return windows, (Hp, Wp) + + +def window_unpartition(windows: Tensor, window_size: int, pad_hw: Tuple[int, int], hw: Tuple[int, int]) -> Tensor: + """Window unpartition into original sequences and removing padding. + + Args: + windows (Tensor): input tokens with [B * num_windows, window_size, window_size, C]. + window_size (int): window size. + pad_hw (Tuple): padded height and width (Hp, Wp). + hw (Tuple): original height and width (H, W) before padding. + + Returns: + x (Tensor): unpartitioned sequences with [B, H, W, C]. + """ + Hp, Wp = pad_hw + H, W = hw + B = windows.shape[0] // (Hp * Wp // window_size // window_size) + x = windows.view(B, Hp // window_size, Wp // window_size, window_size, window_size, -1) + x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, Hp, Wp, -1) + + if Hp > H or Wp > W: + x = x[:, :H, :W, :].contiguous() + return x + + +def get_rel_pos(q_size: int, k_size: int, rel_pos: Tensor) -> Tensor: + """Get relative positional embeddings according to the relative positions of query and key sizes. + + Args: + q_size (int): size of query q. + k_size (int): size of key k. + rel_pos (Tensor): relative position embeddings (L, C). + + Returns: + Tensor: Extracted positional embeddings according to relative positions. + """ + max_rel_dist = int(2 * max(q_size, k_size) - 1) + # Interpolate rel pos if needed. + if rel_pos.shape[0] != max_rel_dist: + # Interpolate rel pos. + rel_pos_resized = F.interpolate( + rel_pos.reshape(1, rel_pos.shape[0], -1).permute(0, 2, 1), + size=max_rel_dist, + mode="linear", + ) + rel_pos_resized = rel_pos_resized.reshape(-1, max_rel_dist).permute(1, 0) + else: + rel_pos_resized = rel_pos + + # Scale the coords with short length if shapes for q and k are different. + q_coords = torch.arange(q_size)[:, None] * max(k_size / q_size, 1.0) + k_coords = torch.arange(k_size)[None, :] * max(q_size / k_size, 1.0) + relative_coords = (q_coords - k_coords) + (k_size - 1) * max(q_size / k_size, 1.0) + + return rel_pos_resized[relative_coords.long()] + + +def add_decomposed_rel_pos( + attn: Tensor, + q: Tensor, + rel_pos_h: Tensor, + rel_pos_w: Tensor, + q_size: Tuple[int, int], + k_size: Tuple[int, int], +) -> Tensor: + """Calculate decomposed Relative Positional Embeddings from `mvitv2`. + + https://github.com/facebookresearch/mvit/blob/19786631e330df9f3622e5402b4a419a263a2c80/mvit/models/attention.py + + Args: + attn (Tensor): attention map. + q (Tensor): query q in the attention layer with shape (B, q_h * q_w, C). + rel_pos_h (Tensor): relative position embeddings (Lh, C) for height axis. + rel_pos_w (Tensor): relative position embeddings (Lw, C) for width axis. + q_size (Tuple): spatial sequence size of query q with (q_h, q_w). + k_size (Tuple): spatial sequence size of key k with (k_h, k_w). + + Returns: + attn (Tensor): attention map with added relative positional embeddings. + """ + q_h, q_w = q_size + k_h, k_w = k_size + Rh = get_rel_pos(q_h, k_h, rel_pos_h) + Rw = get_rel_pos(q_w, k_w, rel_pos_w) + + B, _, dim = q.shape + r_q = q.reshape(B, q_h, q_w, dim) + rel_h = torch.einsum("bhwc,hkc->bhwk", r_q, Rh) + rel_w = torch.einsum("bhwc,wkc->bhwk", r_q, Rw) + + attn = (attn.view(B, q_h, q_w, k_h, k_w) + rel_h[:, :, :, :, None] + rel_w[:, :, :, None, :]).view( + B, q_h * q_w, k_h * k_w + ) + + return attn + + +class PatchEmbed(nn.Module): + """Image to Patch Embedding. + + Args: + kernel_size (Tuple): kernel size of the projection layer. + stride (Tuple): stride of the projection layer. + padding (Tuple): padding size of the projection layer. + in_chans (int): Number of input image channels. + embed_dim (int): Patch embedding dimension. + """ + + def __init__( + self, + kernel_size: Tuple[int, int] = (16, 16), + stride: Tuple[int, int] = (16, 16), + padding: Tuple[int, int] = (0, 0), + in_chans: int = 3, + embed_dim: int = 768, + ) -> None: + + super().__init__() + + self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=kernel_size, stride=stride, padding=padding) + + def forward(self, x: Tensor) -> Tensor: + """Forward call. + + Args: + x (Tensor): input image tensor with shape (B, C, H, W). + + Returns: + Tensor: output tensor with shape (B, H', W', C'). + """ + x = self.proj(x) + # B C H W -> B H W C + x = x.permute(0, 2, 3, 1) + return x + + +def build_vit(backbone: str, image_size: int): + """Build ViT backbone. + + Args: + backbone (str): backbone name. + image_size (int): input image size. + + Returns: + ViT: ViT backbone. + """ + model_params = dict( + vit_h=dict( + embed_dim=1280, + depth=32, + num_heads=16, + global_attn_indexes=[7, 15, 23, 31], + ), + vit_l=dict( + embed_dim=1024, + depth=24, + num_heads=16, + global_attn_indexes=[5, 11, 17, 23], + ), + vit_b=dict( + embed_dim=768, + depth=12, + num_heads=12, + global_attn_indexes=[2, 5, 8, 11], + ), + ) + + return ViT( + img_size=image_size, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + out_chans=256, + patch_size=16, + mlp_ratio=4, + qkv_bias=True, + use_rel_pos=True, + window_size=14, + **model_params[backbone] # type: ignore + ) diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/__init__.py new file mode 100644 index 00000000000..fa85e705222 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/__init__.py @@ -0,0 +1,6 @@ +"""Encoders for visual prompting model.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .sam_mask_decoder import SAMMaskDecoder # noqa: F401 diff --git a/src/otx/algo/visual_prompting/decoders/sam_mask_decoder.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/sam_mask_decoder.py similarity index 93% rename from src/otx/algo/visual_prompting/decoders/sam_mask_decoder.py rename to src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/sam_mask_decoder.py index bc3d04c718b..34c3a7b54f7 100644 --- a/src/otx/algo/visual_prompting/decoders/sam_mask_decoder.py +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/sam_mask_decoder.py @@ -1,24 +1,25 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 +"""Mask decoder module for SAM.""" -"""SAM mask decoder model for the OTX visual prompting.""" - -from __future__ import annotations +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# import math +from typing import List, Tuple, Type import torch from torch import Tensor, nn -from torch.nn import functional as F # noqa: N812 +from torch.nn import functional as F -from otx.algo.visual_prompting.utils import LayerNorm2d, MLPBlock +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.utils import ( + LayerNorm2d, + MLPBlock, +) class SAMMaskDecoder(nn.Module): """Predicts masks given an image and prompt embeddings, using a transformer architecture. - Reference: https://github.com/facebookresearch/segment-anything - Args: transformer_dim (int): Channel dimension of the transformer. transformer_cfg (dict): Configuration of the transformer. @@ -34,10 +35,11 @@ def __init__( transformer_dim: int, transformer_cfg: dict, num_multimask_outputs: int = 3, - activation: type[nn.Module] = nn.GELU, + activation: Type[nn.Module] = nn.GELU, iou_head_depth: int = 3, iou_head_hidden_dim: int = 256, ) -> None: + super().__init__() self.transformer_dim = transformer_dim self.transformer = TwoWayTransformer(**transformer_cfg) @@ -56,7 +58,7 @@ def __init__( activation(), ) self.output_hypernetworks_mlps = nn.ModuleList( - [MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3) for i in range(self.num_mask_tokens)], + [MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3) for i in range(self.num_mask_tokens)] ) self.iou_prediction_head = MLP(transformer_dim, iou_head_hidden_dim, self.num_mask_tokens, iou_head_depth) @@ -68,7 +70,7 @@ def forward( sparse_prompt_embeddings: Tensor, dense_prompt_embeddings: Tensor, multimask_output: bool, - ) -> tuple[Tensor, Tensor]: + ) -> Tuple[Tensor, Tensor]: """Predict masks given image and prompt embeddings. Args: @@ -90,7 +92,10 @@ def forward( ) # Select the correct mask or masks for output - mask_slice = slice(1, None) if multimask_output else slice(0, 1) + if multimask_output: + mask_slice = slice(1, None) + else: + mask_slice = slice(0, 1) masks = masks[:, mask_slice, :, :] iou_pred = iou_pred[:, mask_slice] @@ -103,7 +108,7 @@ def predict_masks( image_pe: Tensor, sparse_prompt_embeddings: Tensor, dense_prompt_embeddings: Tensor, - ) -> tuple[Tensor, Tensor]: + ) -> Tuple[Tensor, Tensor]: """Predicts masks. See 'forward' for more details. Args: @@ -135,9 +140,9 @@ def predict_masks( # Upscale mask embeddings and predict masks using the mask tokens src = src.transpose(1, 2).view(b, c, h, w) upscaled_embedding = self.output_upscaling(src) - hyper_in_list: list[Tensor] = [ - self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :]) for i in range(self.num_mask_tokens) - ] + hyper_in_list: List[Tensor] = [] + for i in range(self.num_mask_tokens): + hyper_in_list.append(self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :])) hyper_in = torch.stack(hyper_in_list, dim=1) b, c, h, w = upscaled_embedding.shape masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view(b, -1, h, w) @@ -167,10 +172,11 @@ def __init__( num_layers: int, sigmoid_output: bool = False, ) -> None: + super().__init__() self.num_layers = num_layers h = [hidden_dim] * (num_layers - 1) - self.layers = nn.ModuleList(nn.Linear(n, k) for n, k in zip([input_dim, *h], [*h, output_dim])) + self.layers = nn.ModuleList(nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim])) self.sigmoid_output = sigmoid_output def forward(self, x: Tensor) -> Tensor: @@ -206,9 +212,10 @@ def __init__( embedding_dim: int, num_heads: int, mlp_dim: int, - activation: type[nn.Module] = nn.ReLU, + activation: Type[nn.Module] = nn.ReLU, attention_downsample_rate: int = 2, ) -> None: + super().__init__() self.depth = depth self.embedding_dim = embedding_dim @@ -225,7 +232,7 @@ def __init__( activation=activation, attention_downsample_rate=attention_downsample_rate, skip_first_layer_pe=(i == 0), - ), + ) ) self.final_attn_token_to_image = Attention(embedding_dim, num_heads, downsample_rate=attention_downsample_rate) @@ -236,7 +243,7 @@ def forward( image_embedding: Tensor, image_pe: Tensor, point_embedding: Tensor, - ) -> tuple[Tensor, Tensor]: + ) -> Tuple[Tensor, Tensor]: """Apply the transformer to the image and point embeddings. Args: @@ -298,10 +305,11 @@ def __init__( embedding_dim: int, num_heads: int, mlp_dim: int = 2048, - activation: type[nn.Module] = nn.ReLU, + activation: Type[nn.Module] = nn.ReLU, attention_downsample_rate: int = 2, skip_first_layer_pe: bool = False, ) -> None: + super().__init__() self.self_attn = Attention(embedding_dim, num_heads) self.norm1 = nn.LayerNorm(embedding_dim) @@ -317,7 +325,7 @@ def __init__( self.skip_first_layer_pe = skip_first_layer_pe - def forward(self, queries: Tensor, keys: Tensor, query_pe: Tensor, key_pe: Tensor) -> tuple[Tensor, Tensor]: + def forward(self, queries: Tensor, keys: Tensor, query_pe: Tensor, key_pe: Tensor) -> Tuple[Tensor, Tensor]: """Apply the transformer block to the queries and keys. Args: @@ -378,11 +386,12 @@ def __init__( num_heads: int, downsample_rate: int = 1, ) -> None: + super().__init__() self.embedding_dim = embedding_dim self.internal_dim = embedding_dim // downsample_rate self.num_heads = num_heads - assert self.internal_dim % num_heads == 0, "num_heads must divide embedding_dim." # noqa: S101 + assert self.internal_dim % num_heads == 0, "num_heads must divide embedding_dim." self.q_proj = nn.Linear(embedding_dim, self.internal_dim) self.k_proj = nn.Linear(embedding_dim, self.internal_dim) @@ -446,4 +455,6 @@ def forward(self, q: Tensor, k: Tensor, v: Tensor) -> Tensor: # Get output out = attn @ v out = self._recombine_heads(out) - return self.out_proj(out) + out = self.out_proj(out) + + return out diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/__init__.py new file mode 100644 index 00000000000..60e38e73c00 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/__init__.py @@ -0,0 +1,7 @@ +"""Encoders for visual prompting model.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .sam_image_encoder import SAMImageEncoder # noqa: F401 +from .sam_prompt_encoder import SAMPromptEncoder # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/sam_image_encoder.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/sam_image_encoder.py new file mode 100644 index 00000000000..6944754c660 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/sam_image_encoder.py @@ -0,0 +1,32 @@ +"""Image encoder module for SAM.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from omegaconf import DictConfig +from torch import nn + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.backbones import ( + build_tiny_vit, + build_vit, +) + + +class SAMImageEncoder(nn.Module): + """Image encoder module for SAM. + + Args: + config (DictConfig): Config for image encoder. + """ + + def __new__(cls, config: DictConfig): + """Initialize SAM image encoder to the target backbone.""" + if "tiny_vit" == config.backbone: + return build_tiny_vit(config.image_size) + elif "vit" in config.backbone: + return build_vit(config.backbone, config.image_size) + else: + raise NotImplementedError( + (f"{config.backbone} for image encoder of SAM is not implemented yet. " f"Use vit_b, l, or h.") + ) diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/sam_prompt_encoder.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/sam_prompt_encoder.py new file mode 100644 index 00000000000..167e44c4d7f --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/sam_prompt_encoder.py @@ -0,0 +1,271 @@ +"""Prompt encoder module for SAM.""" + +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# + +from typing import Any, Optional, Tuple, Type + +import numpy as np +import torch +from torch import Tensor, nn + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.utils import ( + LayerNorm2d, +) + + +class SAMPromptEncoder(nn.Module): + """Encodes prompts for input to SAM's mask decoder. + + Args: + embed_dim (int): The prompts' embedding dimension. + image_embedding_size (tuple(int, int)): The spatial size of the image embedding, as (H, W). + input_image_size (int): The padded size of the image as input to the image encoder, as (H, W). + mask_in_chans (int): The number of hidden channels used for encoding input masks. + activation (nn.Module): The activation to use when encoding input masks. + """ + + def __init__( + self, + embed_dim: int, + image_embedding_size: Tuple[int, int], + input_image_size: Tuple[int, int], + mask_in_chans: int, + activation: Type[nn.Module] = nn.GELU, + ) -> None: + + super().__init__() + self.embed_dim = embed_dim + self.input_image_size = input_image_size + self.image_embedding_size = image_embedding_size + self.pe_layer = PositionEmbeddingRandom(embed_dim // 2) + + self.num_point_embeddings: int = 4 # pos/neg point + 2 box corners + point_embeddings = [nn.Embedding(1, embed_dim) for i in range(self.num_point_embeddings)] + self.point_embeddings = nn.ModuleList(point_embeddings) + self.not_a_point_embed = nn.Embedding(1, embed_dim) + + self.mask_input_size = (4 * image_embedding_size[0], 4 * image_embedding_size[1]) + self.mask_downscaling = nn.Sequential( + nn.Conv2d(1, mask_in_chans // 4, kernel_size=2, stride=2), + LayerNorm2d(mask_in_chans // 4), + activation(), + nn.Conv2d(mask_in_chans // 4, mask_in_chans, kernel_size=2, stride=2), + LayerNorm2d(mask_in_chans), + activation(), + nn.Conv2d(mask_in_chans, embed_dim, kernel_size=1), + ) + self.no_mask_embed = nn.Embedding(1, embed_dim) + + def get_dense_pe(self) -> Tensor: + """Returns the positional encoding. + + It used to encode point prompts, applied to a dense set of points the shape of the image encoding. + + Returns: + Tensor: Positional encoding with shape 1x(embed_dim)x(embedding_h)x(embedding_w). + """ + return self.pe_layer(self.image_embedding_size).unsqueeze(0) + + def _embed_points(self, points: Tensor, labels: Tensor, pad: bool) -> Tensor: + """Embeds point prompts. + + Args: + points (Tensor): A BxNx2 array of point prompts to the model. + Each point is in (X,Y) in pixels. + labels (Tensor): A BxN array of labels for the point prompts. + 1 indicates a foreground point and 0 indicates a background point. + pad (bool): Whether to pad the points with a zero point. + + Returns: + Tensor: The embedded points, as (N, embed_dim). + """ + points = points + 0.5 # Shift to center of pixel + if pad: + padding_point = torch.zeros((points.shape[0], 1, 2), device=points.device) + padding_label = -torch.ones((labels.shape[0], 1), device=labels.device) + points = torch.cat([points, padding_point], dim=1) + labels = torch.cat([labels, padding_label], dim=1) + point_embedding = self.pe_layer.forward_with_coords(points, self.input_image_size) + point_embedding[labels == -1] = 0.0 + point_embedding[labels == -1] += self.not_a_point_embed.weight + point_embedding[labels == 0] += self.point_embeddings[0].weight + point_embedding[labels == 1] += self.point_embeddings[1].weight + return point_embedding + + def _embed_boxes(self, boxes: Tensor) -> Tensor: + """Embeds box prompts. + + Args: + boxes (Tensor): A Bx4 array given a box prompt to the model, in XYXY format. + + Returns: + Tensor: The embedded boxes, as (N, embed_dim). + """ + boxes = boxes + 0.5 # Shift to center of pixel + coords = boxes.reshape(-1, 2, 2) + corner_embedding = self.pe_layer.forward_with_coords(coords, self.input_image_size) + corner_embedding[:, 0, :] += self.point_embeddings[2].weight + corner_embedding[:, 1, :] += self.point_embeddings[3].weight + return corner_embedding + + def _embed_masks(self, masks: Tensor) -> Tensor: + """Embeds mask inputs. + + Args: + masks (Tensor): A low resolution mask input to the model, typically + coming from a previous prediction iteration. Has form Bx1xHxW, where + for SAM, H=W=256. Masks returned by a previous iteration of the + predict method do not need further transformation. + + Returns: + Tensor: The embedded masks, as (N, embed_dim). + """ + mask_embedding = self.mask_downscaling(masks) + return mask_embedding + + def _get_batch_size( + self, + points: Optional[Tuple[Tensor, Tensor]], + boxes: Optional[Tensor], + masks: Optional[Tensor], + ) -> int: + """Gets the batch size of the output given the batch size of the input prompts. + + Args: + points (tuple(Tensor, Tensor) or none): point coordinates and labels to embed. + boxes (Tensor or none): boxes to embed. + masks (Tensor or none): masks to embed. + + Returns: + int: The batch size of the output. + """ + if points is not None: + return points[0].shape[0] + elif boxes is not None: + return boxes.shape[0] + elif masks is not None: + return masks.shape[0] + else: + return 1 + + def _get_device(self) -> torch.device: + """Gets the device of the embeddings. + + Returns: + torch.device: The device of the embeddings. + """ + return self.point_embeddings[0].weight.device + + def forward( + self, + points: Optional[Tuple[Tensor, Tensor]], + boxes: Optional[Tensor], + masks: Optional[Tensor], + ) -> Tuple[Tensor, Tensor]: + """Embeds different types of prompts, returning both sparse and dense embeddings. + + Args: + points (tuple(Tensor, Tensor) or none): Point coordinates and labels to embed. + Point coordinates are BxNx2 arrays of point prompts to the model. + Each point is in (X,Y) in pixels. Labels are BxN arrays of labels for the point prompts. + 1 indicates a foreground point and 0 indicates a background point. + boxes (Tensor or none): A Bx4 array given a box prompt to the model, in XYXY format. + masks (Tensor or none): A low resolution mask input to the model, typically + coming from a previous prediction iteration. Has form Bx1xHxW, where + for SAM, H=W=256. Masks returned by a previous iteration of the + predict method do not need further transformation. + + Returns: + sparse_embeddings (Tensor): sparse embeddings for the points and boxes, with shape Nx1x(embed_dim), + where N is determined by the number of input points and boxes. + dense_embeddings (Tensor): dense embeddings for the masks, in the shape Nx(embed_dim)x(embed_H)x(embed_W). + """ + bs = self._get_batch_size(points, boxes, masks) + sparse_embeddings = torch.empty((bs, 0, self.embed_dim), device=self._get_device()) + if points is not None: + coords, labels = points + point_embeddings = self._embed_points(coords, labels, pad=(boxes is None)) + sparse_embeddings = torch.cat([sparse_embeddings, point_embeddings], dim=1) + if boxes is not None: + box_embeddings = self._embed_boxes(boxes) + sparse_embeddings = torch.cat([sparse_embeddings, box_embeddings], dim=1) + + if masks is not None: + dense_embeddings = self._embed_masks(masks) + else: + dense_embeddings = self.no_mask_embed.weight.reshape(1, -1, 1, 1).expand( + bs, -1, self.image_embedding_size[0], self.image_embedding_size[1] + ) + + return sparse_embeddings, dense_embeddings + + +class PositionEmbeddingRandom(nn.Module): + """Positional encoding using random spatial frequencies. + + Args: + num_pos_feats (int): The number of positional frequencies. + scale (float): The scale of the positional encoding. + """ + + def __init__(self, num_pos_feats: int = 64, scale: Optional[float] = None) -> None: + super().__init__() + if scale is None or scale <= 0.0: + scale = 1.0 + self.register_buffer( + "positional_encoding_gaussian_matrix", + scale * torch.randn((2, num_pos_feats)), + ) + + def _pe_encoding(self, coords: Tensor) -> Tensor: + """Positionally encode points that are normalized to [0,1]. + + Args: + coords (Tensor): Stacked x-y grids, as (H, W, 2). + + Returns: + Tensor: The positional encoding, as (H, W, num_pos_feats * 2). + """ + # assuming coords are in [0, 1]^2 square and have d_1 x ... x d_n x 2 shape + coords = 2 * coords - 1 + coords = coords @ self.positional_encoding_gaussian_matrix + coords = 2 * np.pi * coords + # outputs d_1 x ... x d_n x C shape + return torch.cat([torch.sin(coords), torch.cos(coords)], dim=-1) + + def forward(self, size: Tuple[int, int]) -> Tensor: + """Generate positional encoding for a grid of the specified size. + + Args: + size (tuple(int, int)): The size of the grid to generate the encoding for. + + Returns: + Tensor: The positional encoding, as (num_pos_feats * 2, H, W). + """ + h, w = size + device: Any = self.positional_encoding_gaussian_matrix.device + grid = torch.ones((h, w), device=device, dtype=torch.float32) + y_embed = grid.cumsum(dim=0) - 0.5 + x_embed = grid.cumsum(dim=1) - 0.5 + y_embed = y_embed / h + x_embed = x_embed / w + + pe = self._pe_encoding(torch.stack([x_embed, y_embed], dim=-1)) + return pe.permute(2, 0, 1) # C x H x W + + def forward_with_coords(self, coords_input: Tensor, image_size: Tuple[int, int]) -> Tensor: + """Positionally encode points that are not normalized to [0,1]. + + Args: + coords_input (Tensor): The coordinates to encode, as (N, 1, 2). + image_size (tuple(int, int)): The size of the image the coordinates are from. + + Returns: + Tensor: The positional encoding, as (N, 1, num_pos_feats * 2). + """ + coords = coords_input.clone() + coords[:, :, 0] = coords[:, :, 0] / image_size[1] + coords[:, :, 1] = coords[:, :, 1] / image_size[0] + return self._pe_encoding(coords.to(torch.float)) # B x N x C diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/__init__.py new file mode 100644 index 00000000000..a91c8199534 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/__init__.py @@ -0,0 +1,7 @@ +"""Utils used for visual prompting model.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .layer_norm import LayerNorm2d # noqa: F401 +from .mlp_block import MLPBlock # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/layer_norm.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/layer_norm.py new file mode 100644 index 00000000000..2479e5cf667 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/layer_norm.py @@ -0,0 +1,42 @@ +"""Layer normalization module.""" + +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# + +import torch +from torch import Tensor, nn + + +# From https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/batch_norm.py # noqa +# Itself from https://github.com/facebookresearch/ConvNeXt/blob/d1fa8f6fef0a165b27399986cc2bdacc92777e40/models/convnext.py#L119 # noqa +class LayerNorm2d(nn.Module): + """LayerNorm2d module. + + Reference: https://github.com/facebookresearch/segment-anything + + Args: + num_channels (int): Number of channels. + eps (float): Epsilon value. + """ + + def __init__(self, num_channels: int, eps: float = 1e-6) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(num_channels)) + self.bias = nn.Parameter(torch.zeros(num_channels)) + self.eps = eps + + def forward(self, x: Tensor) -> Tensor: + """Forward function of LayerNorm2d. + + Args: + x (Tensor): Input tensor. + + Returns: + Tensor: Output tensor. + """ + u = x.mean(1, keepdim=True) + s = (x - u).pow(2).mean(1, keepdim=True) + x = (x - u) / torch.sqrt(s + self.eps) + x = self.weight[:, None, None] * x + self.bias[:, None, None] + return x diff --git a/src/otx/algo/visual_prompting/utils/mlp_block.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/mlp_block.py similarity index 80% rename from src/otx/algo/visual_prompting/utils/mlp_block.py rename to src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/mlp_block.py index 8a62de2af93..6e845cc93a1 100644 --- a/src/otx/algo/visual_prompting/utils/mlp_block.py +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/utils/mlp_block.py @@ -1,9 +1,10 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 +"""MLP block module.""" -"""MLP block module for the OTX visual prompting.""" +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# -from __future__ import annotations +from typing import Type from torch import Tensor, nn @@ -23,8 +24,9 @@ def __init__( self, embedding_dim: int, mlp_dim: int, - act: type[nn.Module] = nn.GELU, + act: Type[nn.Module] = nn.GELU, ) -> None: + super().__init__() self.lin1 = nn.Linear(embedding_dim, mlp_dim) self.lin2 = nn.Linear(mlp_dim, embedding_dim) diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/__init__.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/__init__.py new file mode 100644 index 00000000000..c7493b86fa6 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/__init__.py @@ -0,0 +1,7 @@ +"""Visual prompters.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .segment_anything import SegmentAnything # noqa: F401 +from .zero_shot_segment_anything import ZeroShotSegmentAnything # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/segment_anything.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/segment_anything.py new file mode 100644 index 00000000000..eeaa3f5586d --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/segment_anything.py @@ -0,0 +1,600 @@ +"""SAM module for visual prompting. + +paper: https://arxiv.org/abs/2304.02643 +reference: https://github.com/facebookresearch/segment-anything +""" + +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + + +from collections import OrderedDict +from typing import Dict, List, Optional, Tuple + +import torch +from omegaconf import DictConfig +from pytorch_lightning import LightningModule +from torch import Tensor, optim +from torch.nn import functional as F +from torchmetrics import MeanMetric, MetricCollection +from torchmetrics.classification import BinaryF1Score, BinaryJaccardIndex, Dice + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.decoders import ( + SAMMaskDecoder, +) +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.encoders import ( + SAMImageEncoder, + SAMPromptEncoder, +) + +CKPT_PATHS = { + "tiny_vit": "https://github.com/ChaoningZhang/MobileSAM/raw/master/weights/mobile_sam.pt", + "vit_b": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth", + "vit_l": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth", + "vit_h": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth", +} + + +class SegmentAnything(LightningModule): + """SAM predicts object masks from an image and input prompts. + + Args: + config (DictConfig): Config for SAM. + state_dict (Optional[OrderedDict], optional): State dict of SAM. Defaults to None. + """ + + def __init__(self, config: DictConfig, state_dict: Optional[OrderedDict] = None) -> None: + super().__init__() + self.save_hyperparameters(ignore="state_dict") + self.config = config + + self.set_models() + self.freeze_networks() + self.set_metrics() + self.load_checkpoint(state_dict=state_dict) + + def set_models(self) -> None: + """Set models for SAM.""" + # TODO (sungchul): Currently, backbone is assumed as vit. + # Depending on backbone, image_embedding_size can be changed. + if "vit" in self.config.model.backbone: + patch_size = 16 + self.image_embedding_size = self.config.model.image_size // patch_size + else: + raise NotImplementedError( + ( + f"{self.config.model.backbone} for image encoder of SAM is not implemented yet. " + f"Use vit_b, l, or h." + ) + ) + + self.image_encoder = SAMImageEncoder(self.config.model) + self.prompt_encoder = SAMPromptEncoder( + embed_dim=256, + image_embedding_size=(self.image_embedding_size, self.image_embedding_size), + input_image_size=(self.config.model.image_size, self.config.model.image_size), + mask_in_chans=16, + ) + self.mask_decoder = SAMMaskDecoder( + num_multimask_outputs=3, + transformer_cfg=dict(depth=2, embedding_dim=256, mlp_dim=2048, num_heads=8), + transformer_dim=256, + iou_head_depth=3, + iou_head_hidden_dim=256, + ) + + def freeze_networks(self) -> None: + """Freeze networks depending on config.""" + if self.config.model.freeze_image_encoder: + for param in self.image_encoder.parameters(): + param.requires_grad = False + + if self.config.model.freeze_prompt_encoder: + for param in self.prompt_encoder.parameters(): + param.requires_grad = False + + if self.config.model.freeze_mask_decoder: + for param in self.mask_decoder.parameters(): + param.requires_grad = False + + def set_metrics(self) -> None: + """Set metrics for SAM.""" + assert self.config.model.loss_type.lower() in ["sam", "medsam"], ValueError( + f"{self.config.model.loss_type} is not supported. Please use 'sam' or 'medsam'." + ) + + # set train metrics + self.train_metrics = MetricCollection( + dict( + train_IoU=BinaryJaccardIndex(), + train_F1=BinaryF1Score(), + train_Dice=Dice(), + train_loss=MeanMetric(), + train_loss_dice=MeanMetric(), + ) + ) + if self.config.model.loss_type.lower() == "sam": + self.train_metrics.add_metrics( + dict( + train_loss_focal=MeanMetric(), + train_loss_iou=MeanMetric(), + ) + ) + elif self.config.model.loss_type.lower() == "medsam": + self.train_metrics.add_metrics(dict(train_loss_ce=MeanMetric())) + + # set val metrics + self.val_metrics = MetricCollection( + dict( + val_IoU=BinaryJaccardIndex(), + val_F1=BinaryF1Score(), + val_Dice=Dice(), + ) + ) + + def load_checkpoint(self, state_dict: Optional[OrderedDict] = None) -> None: + """Load checkpoint for SAM. + + Args: + state_dict (Optional[OrderedDict], optional): State dict of SAM. Defaults to None. + """ + if state_dict: + # state_dict from args.load_from + self.load_state_dict(state_dict) + elif self.config.model.checkpoint: + if str(self.config.model.checkpoint).endswith(".ckpt"): + # load lightning checkpoint + self.load_from_checkpoint(self.config.model.checkpoint, strict=False) + else: + if str(self.config.model.checkpoint).startswith("http"): + # get checkpoint from url + state_dict = torch.hub.load_state_dict_from_url(str(self.config.model.checkpoint)) + else: + # load checkpoint from local + with open(self.config.model.checkpoint, "rb") as f: + state_dict = torch.load(f) + + self.load_state_dict(state_dict, strict=False) + else: + # use default checkpoint + state_dict = torch.hub.load_state_dict_from_url(CKPT_PATHS[self.config.model.backbone]) + self.load_state_dict(state_dict, strict=False) + + ########################################################## + # forward for inference (export/deploy/optimize) # + ########################################################## + @torch.no_grad() + def forward( + self, + image_embeddings: Tensor, + point_coords: Tensor, + point_labels: Tensor, + mask_input: Tensor, + has_mask_input: Tensor, + orig_size: Tensor, + ): + """Forward method for SAM inference (export/deploy). + + Args: + image_embeddings (Tensor): The image embedding with a batch index of length 1. + If it is a zero tensor, the image embedding will be computed from the image. + point_coords (Tensor): Coordinates of sparse input prompts, + corresponding to both point inputs and box inputs. + Boxes are encoded using two points, one for the top-left corner and one for the bottom-right corner. + Coordinates must already be transformed to long-side 1024. Has a batch index of length 1. + point_labels (Tensor): Labels for the sparse input prompts. + 0 is a negative input point, 1 is a positive input point, + 2 is a top-left box corner, 3 is a bottom-right box corner, and -1 is a padding point. + If there is no box input, a single padding point with label -1 and + coordinates (0.0, 0.0) should be concatenated. + mask_input (Tensor): A mask input to the model with shape 1x1x256x256. + This must be supplied even if there is no mask input. In this case, it can just be zeros. + has_mask_input (Tensor): An indicator for the mask input. + 1 indicates a mask input, 0 indicates no mask input. + This input has 1x1 shape due to supporting openvino input layout. + orig_size (Tensor): The size of the input image in (H,W) format, before any transformation. + This input has 1x2 shape due to supporting openvino input layout. + """ + sparse_embedding = self._embed_points(point_coords, point_labels) + dense_embedding = self._embed_masks(mask_input, has_mask_input) + + masks, scores = self.mask_decoder.predict_masks( + image_embeddings=image_embeddings, + image_pe=self.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embedding, + dense_prompt_embeddings=dense_embedding, + ) + + if self.config.model.use_stability_score: + scores = self.calculate_stability_score( + masks, self.config.model.mask_threshold, self.config.model.stability_score_offset + ) + + if self.config.model.return_single_mask: + masks, scores = self.select_masks(masks, scores, point_coords.shape[1]) + + upscaled_masks = self.postprocess_masks(masks, self.config.model.image_size, orig_size[0]) + + if self.config.model.return_extra_metrics: + stability_scores = self.calculate_stability_score( + upscaled_masks, self.config.model.mask_threshold, self.config.model.stability_score_offset + ) + areas = (upscaled_masks > self.config.model.mask_threshold).sum(-1).sum(-1) + return upscaled_masks, scores, stability_scores, areas, masks + + return upscaled_masks, scores, masks + + def _embed_points(self, point_coords: Tensor, point_labels: Tensor) -> Tensor: + """Embed sparse input prompts. + + Args: + point_coords (Tensor): Coordinates of sparse input prompts, + corresponding to both point inputs and box inputs. Boxes are encoded using two points, + one for the top-left corner and one for the bottom-right corner. + Coordinates must already be transformed to long-side 1024. Has a batch index of length 1. + point_labels (Tensor): Labels for the sparse input prompts. + 0 is a negative input point, 1 is a positive input point, + 2 is a top-left box corner, 3 is a bottom-right box corner, and -1 is a padding point. + If there is no box input, a single padding point with label -1 and + coordinates (0.0, 0.0) should be concatenated. + + + Returns: + point_embedding (Tensor): The embedded sparse input prompts. + """ + point_coords = point_coords + 0.5 + point_coords = point_coords / self.config.model.image_size + point_embedding = self.prompt_encoder.pe_layer._pe_encoding(point_coords) + point_labels = point_labels.unsqueeze(-1).expand_as(point_embedding) + + point_embedding = point_embedding * (point_labels != -1) + point_embedding = point_embedding + self.prompt_encoder.not_a_point_embed.weight * (point_labels == -1) + + for i in range(self.prompt_encoder.num_point_embeddings): + point_embedding = point_embedding + self.prompt_encoder.point_embeddings[i].weight * (point_labels == i) + + return point_embedding + + def _embed_masks(self, input_mask: Tensor, has_mask_input: Tensor) -> Tensor: + """Embed the mask input. + + Args: + input_mask (Tensor): A mask input to the model with shape 1x1x256x256. + This must be supplied even if there is no mask input. In this case, it can just be zeros. + has_mask_input (Tensor): An indicator for the mask input. + 1 indicates a mask input, 0 indicates no mask input. + + Returns: + mask_embedding (Tensor): The embedded mask input. + """ + mask_embedding = has_mask_input * self.prompt_encoder.mask_downscaling(input_mask) + mask_embedding = mask_embedding + (1 - has_mask_input) * self.prompt_encoder.no_mask_embed.weight.reshape( + 1, -1, 1, 1 + ) + return mask_embedding + + def calculate_stability_score(self, masks: Tensor, mask_threshold: float, threshold_offset: float = 1.0) -> Tensor: + """Computes the stability score for a batch of masks. + + The stability score is the IoU between the binary masks obtained + by thresholding the predicted mask logits at high and low values. + + Args: + masks (Tensor): A batch of predicted masks with shape BxHxW. + mask_threshold (float): The threshold used to binarize the masks. + threshold_offset (float, optional): The offset used to compute the stability score. + + Returns: + stability_scores (Tensor): The stability scores for the batch of masks. + """ + # One mask is always contained inside the other. + # Save memory by preventing unnecessary cast to torch.int64 + intersections = ( + (masks > (mask_threshold + threshold_offset)).sum(-1, dtype=torch.int16).sum(-1, dtype=torch.int32) + ) + unions = (masks > (mask_threshold - threshold_offset)).sum(-1, dtype=torch.int16).sum(-1, dtype=torch.int32) + return intersections / unions + + def select_masks(self, masks: Tensor, iou_preds: Tensor, num_points: int) -> Tuple[Tensor, Tensor]: + """Selects the best mask from a batch of masks. + + Args: + masks (Tensor): A batch of predicted masks with shape BxMxHxW. + iou_preds (Tensor): A batch of predicted IoU scores with shape BxM. + num_points (int): The number of points in the input. + + Returns: + masks (Tensor): The selected masks with shape Bx1xHxW. + iou_preds (Tensor): The selected IoU scores with shape Bx1. + """ + # Determine if we should return the multiclick mask or not from the number of points. + # The reweighting is used to avoid control flow. + score_reweight = torch.tensor([[1000] + [0] * (self.mask_decoder.num_mask_tokens - 1)]).to(iou_preds.device) + score = iou_preds + (num_points - 2.5) * score_reweight + best_idx = torch.argmax(score, dim=1) + masks = masks[torch.arange(masks.shape[0]), best_idx, :, :].unsqueeze(1) + iou_preds = iou_preds[torch.arange(masks.shape[0]), best_idx].unsqueeze(1) + + return masks, iou_preds + + @classmethod + def postprocess_masks(cls, masks: Tensor, input_size: int, orig_size: Tensor) -> Tensor: + """Postprocess the predicted masks. + + Args: + masks (Tensor): A batch of predicted masks with shape Bx1xHxW. + input_size (int): The size of the image input to the model. Used to remove padding. + orig_size (Tensor): The original image size with shape Bx2. + + Returns: + masks (Tensor): The postprocessed masks with shape Bx1xHxW. + """ + masks = F.interpolate(masks, size=(input_size, input_size), mode="bilinear", align_corners=False) + + prepadded_size = cls.get_prepadded_size(cls, orig_size, input_size) # type: ignore[arg-type] + masks = masks[..., : prepadded_size[0], : prepadded_size[1]] + + orig_size = orig_size.to(torch.int64) + h, w = orig_size[0], orig_size[1] + return F.interpolate(masks, size=(h, w), mode="bilinear", align_corners=False) + + def get_prepadded_size(self, input_image_size: Tensor, longest_side: int) -> Tensor: + """Get pre-padded size.""" + scale = longest_side / torch.max(input_image_size) + transformed_size = scale * input_image_size + return torch.floor(transformed_size + 0.5).to(torch.int64) + + ###################################################### + # forward for training/validation/prediction # + ###################################################### + def forward_train( + self, + images: Tensor, + bboxes: List[Tensor], + points: Optional[Tuple[Tensor, Tensor]] = None, + masks: Optional[Tensor] = None, + ) -> Tuple[List[Tensor], List[Tensor]]: + """Forward method for SAM training/validation/prediction. + + Args: + images (Tensor): Images with shape (B, C, H, W). + bboxes (List[Tensor]): A Nx4 array given a box prompt to the model, in XYXY format. + points (Tuple[Tensor, Tensor], optional): Point coordinates and labels to embed. + Point coordinates are BxNx2 arrays of point prompts to the model. + Each point is in (X,Y) in pixels. Labels are BxN arrays of labels for the point prompts. + 1 indicates a foreground point and 0 indicates a background point. + masks (Optional[Tensor], optional): A low resolution mask input to the model, typically + coming from a previous prediction iteration. Has form Bx1xHxW, where + for SAM, H=W=256. Masks returned by a previous iteration of the + predict method do not need further transformation. + + Returns: + pred_masks (List[Tensor]): List with predicted masks with shape (B, 1, H, W). + ious (List[Tensor]): List with IoU predictions with shape (N, 1). + """ + image_embeddings = self.image_encoder(images) + pred_masks = [] + ious = [] + for idx, embedding in enumerate(image_embeddings): + low_res_masks, iou_predictions = [], [] + for idx_prompt, prompt in enumerate([bboxes[idx], points[idx]]): + if prompt is None: + continue + + sparse_embeddings, dense_embeddings = self.prompt_encoder( + points=(prompt.unsqueeze(1), torch.ones(len(prompt), 1, device=prompt.device)) + if idx_prompt == 1 + else None, + boxes=prompt if idx_prompt == 0 else None, + masks=None, + ) + + _low_res_masks, _iou_predictions = self.mask_decoder( + image_embeddings=embedding.unsqueeze(0), + image_pe=self.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=False, # when given multiple prompts. if there is single prompt True would be better. # noqa: E501 + ) + low_res_masks.append(_low_res_masks) + iou_predictions.append(_iou_predictions) + + pred_masks.append(torch.cat(low_res_masks, dim=0)) + ious.append(torch.cat(iou_predictions, dim=0)) + + return pred_masks, ious + + def training_step(self, batch, batch_idx) -> Tensor: + """Training step for SAM. + + Args: + batch (Dict): Batch data. + batch_idx (int): Batch index. + + Returns: + loss (Tensor): Loss tensor. + """ + images = batch["images"] + bboxes = batch["bboxes"] + points = batch["points"] + gt_masks = batch["gt_masks"] + + pred_masks, iou_predictions = self.forward_train(images, bboxes, points) + + loss_dice = 0.0 + if self.config.model.loss_type.lower() == "sam": + loss_focal = 0.0 + loss_iou = 0.0 + elif self.config.model.loss_type.lower() == "medsam": + loss_ce = 0.0 + + num_masks = sum(len(pred_mask) for pred_mask in pred_masks) + for i, (pred_mask, gt_mask, iou_prediction) in enumerate(zip(pred_masks, gt_masks, iou_predictions)): + pred_mask = self.postprocess_masks(pred_mask, self.config.model.image_size, batch["original_size"][i]) + pred_mask = pred_mask.sigmoid().squeeze(1) + self.train_metrics["train_IoU"].update(pred_mask, gt_mask) + self.train_metrics["train_F1"].update(pred_mask, gt_mask) + self.train_metrics["train_Dice"].update(pred_mask, gt_mask) + pred_mask = pred_mask.flatten(1) + gt_mask = gt_mask.flatten(1).float() + + # calculate losses + loss_dice += self.calculate_dice_loss(pred_mask, gt_mask, num_masks) + if self.config.model.loss_type.lower() == "sam": + loss_focal += self.calculate_sigmoid_ce_focal_loss(pred_mask, gt_mask, num_masks) + batch_iou = self.calculate_iou(pred_mask, gt_mask) + loss_iou += F.mse_loss(iou_prediction, batch_iou.unsqueeze(1), reduction="sum") / num_masks + + elif self.config.model.loss_type.lower() == "medsam": + loss_ce += self.calculate_sigmoid_ce_focal_loss(pred_mask, gt_mask, num_masks) + + if self.config.model.loss_type.lower() == "sam": + loss = 20.0 * loss_focal + loss_dice + loss_iou + self.train_metrics["train_loss_focal"].update(loss_focal) + self.train_metrics["train_loss_iou"].update(loss_iou) + + elif self.config.model.loss_type.lower() == "medsam": + loss = loss_dice + loss_ce + self.train_metrics["train_loss_ce"].update(loss_ce) + + self.train_metrics["train_loss"].update(loss) + self.train_metrics["train_loss_dice"].update(loss_dice) + + self.log_dict(self.train_metrics.compute(), prog_bar=True) + + return loss + + def training_epoch_end(self, outputs) -> None: + """Training epoch end for SAM.""" + for v in self.train_metrics.values(): + v.reset() + + def validation_step(self, batch, batch_idx) -> MetricCollection: + """Validation step of SAM. + + Args: + batch (Dict): Batch data. + batch_idx (int): Batch index. + + Returns: + val_metrics (MetricCollection): Validation metrics. + """ + images = batch["images"] + bboxes = batch["bboxes"] + points = batch["points"] + gt_masks = batch["gt_masks"] + + pred_masks, _ = self.forward_train(images, bboxes, points) + for i, (pred_mask, gt_mask) in enumerate(zip(pred_masks, gt_masks)): + pred_mask = self.postprocess_masks(pred_mask, self.config.model.image_size, batch["original_size"][i]) + pred_mask = pred_mask.sigmoid().squeeze(1) + for k, v in self.val_metrics.items(): + v.update(pred_mask, gt_mask) + + return self.val_metrics + + def validation_epoch_end(self, outputs) -> None: + """Validation epoch end for SAM.""" + self.log_dict(self.val_metrics.compute(), on_epoch=True, prog_bar=True) + for v in self.val_metrics.values(): + v.reset() + + def predict_step(self, batch, batch_idx) -> Dict[str, Tensor]: + """Predict step of SAM. + + Args: + batch (Dict): Batch data. + batch_idx (int): Batch index. + + Returns: + Dict[str, Tensor]: Predicted masks, IoU predictions, image paths, and labels. + """ + images = batch["images"] + bboxes = batch["bboxes"] + points = batch["points"] + + pred_masks, iou_predictions = self.forward_train(images, bboxes, points) + + masks: List[Tensor] = [] + for i, pred_mask in enumerate(pred_masks): + mask = self.postprocess_masks(pred_mask, self.config.model.image_size, batch["original_size"][i]) + if not self.config.model.return_logits: + mask = (mask > self.config.model.mask_threshold).to(mask.dtype) + else: + mask = mask.sigmoid() + masks.append(mask.squeeze(1)) + + return dict(masks=masks, iou_predictions=iou_predictions, path=batch["path"], labels=batch["labels"]) + + def configure_optimizers(self) -> optim: + """Configure the optimizer for SAM. + + Returns: + optim: Optimizer. + """ + name = self.config.optimizer.pop("name") + optimizer = getattr(optim, name)(self.parameters(), **self.config.optimizer) + return optimizer + + def calculate_dice_loss(self, inputs: Tensor, targets: Tensor, num_masks: int) -> Tensor: + """Compute the DICE loss, similar to generalized IOU for masks. + + Args: + inputs (Tensor): A tensor representing a mask. + targets (Tensor): A tensor with the same shape as inputs. Stores the binary classification labels + for each element in inputs (0 for the negative class and 1 for the positive class). + num_masks (int): The number of masks present in the current batch, used for normalization. + + Returns: + Tensor: The DICE loss. + """ + numerator = 2 * (inputs * targets).sum(-1) + denominator = inputs.sum(-1) + targets.sum(-1) + loss = 1 - (numerator + 1) / (denominator + 1) + return loss.sum() / num_masks + + def calculate_sigmoid_ce_focal_loss( + self, inputs: Tensor, targets: Tensor, num_masks: int, alpha: float = 0.25, gamma: float = 2 + ) -> Tensor: + r"""Loss used in RetinaNet for dense detection: https://arxiv.org/abs/1708.02002. # noqa: D301. + + Args: + inputs (Tensor): A float tensor of arbitrary shape. + targets (Tensor): A tensor with the same shape as inputs. Stores the binary classification labels + for each element in inputs (0 for the negative class and 1 for the positive class). + num_masks (int): The number of masks present in the current batch, used for normalization. + alpha (float, *optional*, defaults to 0.25): Weighting factor in range (0,1) + to balance positive vs negative examples. + gamma (float, *optional*, defaults to 2.0): Exponent of the modulating factor \\(1 - p_t\\) + to balance easy vs hard examples. + + Returns: + Tensor: The focal loss. + """ + loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none") + if self.config.model.loss_type.lower() == "sam": + # focal loss for SAM loss + p_t = inputs * targets + (1 - inputs) * (1 - targets) + loss = loss * ((1 - p_t) ** gamma) + if alpha >= 0: + alpha_t = alpha * targets + (1 - alpha) * (1 - targets) + loss = alpha_t * loss + return loss.mean(1).sum() / num_masks + + def calculate_iou(self, inputs: Tensor, targets: Tensor, epsilon: float = 1e-7) -> Tensor: + """Calculate the intersection over union (IOU) between the predicted mask and the ground truth mask. + + Args: + inputs (Tensor): A tensor representing a mask. + targets (Tensor): A tensor with the same shape as inputs. Stores the binary classification labels + for each element in inputs (0 for the negative class and 1 for the positive class). + epsilon (float, *optional*, defaults to 1e-7): A small value to prevent division by zero. + + Returns: + Tensor: The IOU between the predicted mask and the ground truth mask. + """ + pred_mask = (inputs >= 0.5).float() + intersection = torch.sum(torch.mul(pred_mask, targets), dim=1) + union = torch.sum(pred_mask, dim=1) + torch.sum(targets, dim=1) - intersection + iou = intersection / (union + epsilon) + return iou diff --git a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/zero_shot_segment_anything.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/zero_shot_segment_anything.py new file mode 100644 index 00000000000..f20858554fb --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/zero_shot_segment_anything.py @@ -0,0 +1,772 @@ +"""SAM module for visual prompting zero-shot learning.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import json +import os +import pickle +import time +from collections import OrderedDict, defaultdict +from copy import deepcopy +from itertools import product +from typing import Any, DefaultDict, Dict, List, Optional, Tuple, Union + +import cv2 +import numpy as np +import torch +from omegaconf import DictConfig +from torch import Tensor, nn +from torch.nn import Parameter, ParameterDict +from torch.nn import functional as F + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset import get_transform +from otx.api.entities.scored_label import ScoredLabel +from otx.utils.logger import get_logger + +from .segment_anything import SegmentAnything + +logger = get_logger() + + +class PromptGetter(nn.Module): + """Prompt getter for zero-shot learning.""" + + default_threshold_reference = 0.3 + default_threshold_target = 0.65 + + def __init__(self, image_size: int, downsizing: int = 64) -> None: + super().__init__() + self.image_size = image_size + self.downsizing = downsizing + + def set_default_thresholds(self, default_threshold_reference: float, default_threshold_target: float) -> None: + """Set default thresholds.""" + self.default_threshold_reference = default_threshold_reference + self.default_threshold_target = default_threshold_target + + def get_prompt_candidates( + self, + image_embeddings: Tensor, + reference_feats: Tensor, + used_indices: Tensor, + original_size: Tensor, + threshold: Tensor = torch.tensor([[0.0]], dtype=torch.float32), + num_bg_points: Tensor = torch.tensor([[1]], dtype=torch.int64), + device: Union[torch.device, str] = torch.device("cpu"), + ) -> Tuple[Dict[int, Tensor], Dict[int, Tensor]]: + """Get prompt candidates.""" + threshold = threshold.to(device) + + total_points_scores: Dict[int, Tensor] = {} + total_bg_coords: Dict[int, Tensor] = {} + for label in map(int, used_indices): + points_scores, bg_coords = self( + image_embeddings=image_embeddings, + reference_feat=reference_feats[label], + original_size=original_size, + threshold=threshold, + num_bg_points=num_bg_points, + ) + + total_points_scores[label] = points_scores + total_bg_coords[label] = bg_coords + + return total_points_scores, total_bg_coords + + def forward( + self, + image_embeddings: Tensor, + reference_feat: Tensor, + original_size: Tensor, + threshold: Tensor = torch.tensor([[0.0]], dtype=torch.float32), + num_bg_points: Tensor = torch.tensor([[1]], dtype=torch.int64), + ) -> Tuple[Tensor, Tensor]: + """Get prompt candidates from given reference and target features.""" + original_size = original_size.squeeze() + threshold = threshold.squeeze() + num_bg_points = num_bg_points.squeeze() + + target_feat = image_embeddings.squeeze() + c_feat, h_feat, w_feat = target_feat.shape + target_feat = target_feat / target_feat.norm(dim=0, keepdim=True) + target_feat = target_feat.reshape(c_feat, h_feat * w_feat) + + sim = reference_feat @ target_feat + sim = sim.reshape(1, 1, h_feat, w_feat) + sim = ZeroShotSegmentAnything.postprocess_masks(sim, self.image_size, original_size) + + threshold = (threshold == 0) * self.default_threshold_target + threshold + points_scores, bg_coords = self._point_selection( + mask_sim=sim[0, 0], + original_size=original_size, + threshold=threshold, + num_bg_points=num_bg_points, + ) + + return points_scores, bg_coords + + def _point_selection( + self, + mask_sim: Tensor, + original_size: Tensor, + threshold: Union[Tensor, float] = 0.0, + num_bg_points: Union[Tensor, int] = 1, + ) -> Tuple[Tensor, Tensor]: + """Select point used as point prompts.""" + _, w_sim = mask_sim.shape + + # Top-last point selection + bg_indices = mask_sim.flatten().topk(num_bg_points, largest=False)[1] + bg_x = (bg_indices // w_sim).unsqueeze(0) + bg_y = bg_indices - bg_x * w_sim + bg_coords = torch.cat((bg_y, bg_x), dim=0).permute(1, 0) + bg_coords = bg_coords.to(torch.float32) + + point_coords = torch.where(mask_sim > threshold) + fg_coords_scores = torch.stack(point_coords[::-1] + (mask_sim[point_coords],), dim=0).T + + # to handle empty tensor + len_fg_coords_scores = len(fg_coords_scores) + fg_coords_scores = F.pad(fg_coords_scores, (0, 0, 0, max(0, 1 - len_fg_coords_scores)), value=-1) + + ratio = self.image_size / original_size.max() + width = (original_size[1] * ratio).to(torch.int64) + n_w = width // self.downsizing + + # get grid numbers + idx_grid = ( + fg_coords_scores[:, 1] * ratio // self.downsizing * n_w + fg_coords_scores[:, 0] * ratio // self.downsizing + ) + idx_grid_unique = torch.unique( + idx_grid.to(torch.int64) + ) # unique op only supports INT64, INT8, FLOAT, STRING in ORT + + # get matched indices + matched_matrix = idx_grid.unsqueeze(-1) == idx_grid_unique # (totalN, uniqueN) + + # sample fg_coords_scores matched by matched_matrix + matched_grid = fg_coords_scores.unsqueeze(1) * matched_matrix.unsqueeze(-1) + + # sample the highest score one of the samples that are in the same grid + matched_indices = matched_grid[..., -1].topk(k=1, dim=0, largest=True)[1][0].to(torch.int64) + points_scores = matched_grid[matched_indices].diagonal().T + + # sort by the highest score + sorted_points_scores_indices = torch.argsort(points_scores[:, -1], descending=True).to(torch.int64) + points_scores = points_scores[sorted_points_scores_indices] + + return points_scores, bg_coords + + +class ZeroShotSegmentAnything(SegmentAnything): + """Zero-shot learning module using Segment Anything.""" + + def __init__( + self, + config: Optional[DictConfig] = None, + manual_config_update: Optional[Dict] = None, + state_dict: Optional[OrderedDict] = None, + ) -> None: + if config is None: + config = self.set_default_config() + + if ( + manual_config_update is not None + and isinstance(manual_config_update, dict) + and len(manual_config_update) > 0 + ): + for k, v in manual_config_update.items(): + exec(f"config.{k} = {v}") + + # check freeze conditions + for condition in ["freeze_image_encoder", "freeze_prompt_encoder", "freeze_mask_decoder"]: + if not getattr(config.model, condition, False): + logger.warning(f"config.model.{condition}(=False) must be set to True, changed.") + setattr(config.model, condition, True) + + super().__init__(config, state_dict) + + self.set_empty_reference_info() + + self.prompt_getter = PromptGetter(image_size=config.model.image_size) + self.prompt_getter.set_default_thresholds( + default_threshold_reference=config.model.default_threshold_reference, + default_threshold_target=config.model.default_threshold_target, + ) + + self.point_labels_box = torch.tensor([[2, 3]], dtype=torch.float32) + self.has_mask_inputs = [torch.tensor([[0.0]]), torch.tensor([[1.0]])] + + self.transforms = get_transform( + image_size=config.model.image_size, mean=config.dataset.normalize.mean, std=config.dataset.normalize.std + ) + + self.path_reference_info = "vpm_zsl_reference_infos/{}/reference_info.pt" + + def load_state_dict_pre_hook(self, state_dict: Dict[str, Any], prefix: str = "", *args, **kwargs) -> None: + """Load reference info manually.""" + _reference_feats: Tensor = state_dict.get( + "reference_info.reference_feats", torch.tensor([], dtype=torch.float32) + ) + _used_indices: Tensor = state_dict.get("reference_info.used_indices", torch.tensor([], dtype=torch.int64)) + self.reference_info = ParameterDict( + { + "reference_feats": Parameter(_reference_feats, requires_grad=False), + "used_indices": Parameter(_used_indices, requires_grad=False), + }, + ) + + def set_default_config(self) -> DictConfig: + """Set default config when using independently.""" + return DictConfig( + { + "model": { + "backbone": "tiny_vit", + "checkpoint": "https://github.com/ChaoningZhang/MobileSAM/raw/master/weights/mobile_sam.pt", + "default_threshold_reference": 0.3, + "default_threshold_target": 0.65, + "freeze_image_encoder": True, + "freeze_mask_decoder": True, + "freeze_prompt_encoder": True, + "image_size": 1024, + "mask_threshold": 0.0, + "return_single_mask": False, + "use_stability_score": False, + "stability_score_offset": 1.0, + "return_extra_metrics": False, + }, + "dataset": { + "normalize": { + "mean": [123.675, 116.28, 103.53], + "std": [58.395, 57.12, 57.375], + } + }, + } + ) + + def set_empty_reference_info(self) -> None: + """Set empty reference information.""" + reference_feats: Parameter = Parameter(torch.tensor([], dtype=torch.float32), requires_grad=False) + used_indices: Parameter = Parameter(torch.tensor([], dtype=torch.int64), requires_grad=False) + self.reference_info = ParameterDict( + { + "reference_feats": reference_feats, + "used_indices": used_indices, + }, + ) + self.is_reference_info_empty = True + + def initialize_reference_info(self) -> None: + """Initialize reference information.""" + self.reference_info["reference_feats"] = Parameter(torch.zeros(0, 1, 256), requires_grad=False) + self.reference_info["used_indices"] = Parameter(torch.tensor([], dtype=torch.int64), requires_grad=False) + self.is_reference_info_empty = False + + def expand_reference_info(self, new_largest_label: int) -> None: + """Expand reference info dimensions if newly given processed prompts have more lables.""" + if new_largest_label > (cur_largest_label := len(self.reference_info["reference_feats"]) - 1): + diff = new_largest_label - cur_largest_label + padded_reference_feats = F.pad(self.reference_info["reference_feats"], (0, 0, 0, 0, 0, diff), value=0.0) + self.reference_info["reference_feats"] = Parameter(padded_reference_feats, requires_grad=False) + + @torch.no_grad() + def learn(self, batch: List[Dict[str, Any]], reset_feat: bool = False) -> Union[None, Tuple[ParameterDict, Tensor]]: + """Get reference features. + + Using given images, get reference features and save it to PromptGetter. + These reference features will be used for `infer` to get target results. + Currently, single batch is only supported. + + Args: + batch (List[Dict[str, Any]]): List of dictionaries containing images, prompts, and metas. + `batch` must contain images, prompts with bboxes, points, annotations, and polygons. + reset_feat (bool): Whether reset reference_info. + For OTX standalone, resetting reference_info will be conducted in on_train_start. + For other frameworks, setting it to True is required to reset reference_info. Defaults to False. + + Returns: + (Tuple[ParameterDict, Tensor]): reference_info and ref_masks. + """ + if reset_feat: + self.initialize_reference_info() + + # preprocess images and prompts + transformed_batch = [self.transforms(b.copy()) for b in batch] + processed_prompts = [self._preprocess_prompts(tb) for tb in transformed_batch] + + # initialize tensors to contain reference features and prompts + largest_label = max([label for pp in processed_prompts for label in pp.keys()]) + self.expand_reference_info(largest_label) + # TODO(sungchul): consider who to handle multiple reference features, currently replace it + + batch_ref_masks: List[Tensor] = [] + for tb, pp in zip(transformed_batch, processed_prompts): + # assign components + images = tb["images"].unsqueeze(0).to(self.device) # type: ignore[union-attr] + original_size = torch.as_tensor(tb["original_size"]) + + image_embeddings = self.image_encoder(images) + processed_embedding = image_embeddings.squeeze().permute(1, 2, 0) + + ref_masks = torch.zeros(largest_label + 1, *map(int, original_size)) + for label, input_prompts in pp.items(): + # TODO (sungchul): how to skip background class + + # generate reference mask + # TODO (sungchul): ensemble multi reference features (current : use merged masks) + ref_mask = torch.zeros(*map(int, original_size), dtype=torch.uint8, device=self.device) + for input_prompt in input_prompts: + if (prompt := input_prompt.get("annotations", None)) is not None: + # directly use annotation information as a mask + ref_mask[prompt == 1] += 1 + elif (prompt := input_prompt.get("polygons", None)) is not None: + for polygon in prompt["polygons"]: + contour = [[int(point[0]), int(point[1])] for point in polygon] + mask_from_polygon = np.zeros(original_size, dtype=np.uint8) + mask_from_polygon = cv2.drawContours(mask_from_polygon, np.asarray([contour]), 0, 1, -1) + ref_mask[mask_from_polygon == 1] += 1 + elif (prompt := input_prompt.get("scribble_annotation", None)) is not None: + logger.warning("scribble_annotation is not supported yet.") + continue + elif (prompt := input_prompt.get("scribble_polygon", None)) is not None: + logger.warning("scribble_polygon is not supported yet.") + continue + else: + point_coords = [] + point_labels = [] + if (prompt := input_prompt.get("bboxes", None)) is not None: + point_coords = prompt["point_coords"].reshape(1, 2, 2) + + elif (prompt := input_prompt.get("points", None)) is not None: + point_coords = prompt["point_coords"].reshape(1, 1, 2) + + point_labels = prompt["point_labels"] + + masks = self._predict_masks( + image_embeddings=image_embeddings, + point_coords=point_coords, + point_labels=point_labels, + original_size=original_size, + is_cascade=False, + ) + ref_mask[masks] += 1 + ref_mask = torch.clip(ref_mask, 0, 1).to(torch.float32) + + ref_feat = None + default_threshold_reference = deepcopy(self.prompt_getter.default_threshold_reference) + while ref_feat is None: + logger.info(f"[*] default_threshold_reference : {default_threshold_reference:.4f}") + ref_feat = self._generate_masked_features( + processed_embedding, ref_mask, default_threshold_reference + ) + default_threshold_reference -= 0.05 + + self.reference_info["reference_feats"][label] = ref_feat.detach().cpu() + self.reference_info["used_indices"] = Parameter( + torch.cat((self.reference_info["used_indices"], torch.tensor([[label]]))), + requires_grad=False, + ) + ref_masks[label] = ref_mask.detach().cpu() + batch_ref_masks.append(ref_masks) + return self.reference_info, batch_ref_masks + + @torch.no_grad() + def infer( + self, + batch: List[Dict[str, Any]], + reference_feats: Union[np.ndarray, Tensor], + used_indices: Union[np.ndarray, Tensor], + is_cascade: bool = False, + ) -> List[List[DefaultDict[int, List[Tensor]]]]: + """Zero-shot inference with reference features. + + Get target results by using reference features and target images' features. + + Args: + batch (List[Dict[str, Any]]): List of dictionaries containing images and metas. + reference_feats (Union[np.ndarray, Tensor]): Reference features for target prediction. + If it is np.ndarray, it will be converted to torch tensor. + used_indices (Union[np.ndarray, Tensor]): To check which indices of reference features are validate. + If it is np.ndarray, it will be converted to torch tensor. + is_cascade (bool): Whether use cascade inference. Defaults to False. + + Returns: + (List[List[DefaultDict[int, List[Tensor]]]]): Target results. + Lists wrapping results is following this order: + 1. Target images + 2. Tuple of predicted masks and used points gotten by point selection + """ + if isinstance(reference_feats, np.ndarray): + reference_feats = torch.as_tensor(reference_feats, device=self.device) + if isinstance(used_indices, np.ndarray): + used_indices = torch.as_tensor(used_indices, device=self.device) + + # preprocess images and prompts + transformed_batch = [self.transforms(b.copy()) for b in batch] + + total_results: List[List[Tensor]] = [] + for tb in transformed_batch: + # assign components + images = tb["images"].unsqueeze(0).to(self.device) # type: ignore[union-attr] + original_size = torch.as_tensor(tb["original_size"]) + + image_embeddings = self.image_encoder(images) + total_points_scores, total_bg_coords = self.prompt_getter.get_prompt_candidates( + image_embeddings=image_embeddings, + reference_feats=reference_feats, + used_indices=used_indices, + original_size=original_size, + device=self.device, + ) + predicted_masks: defaultdict = defaultdict(list) + used_points: defaultdict = defaultdict(list) + for label in total_points_scores.keys(): + points_scores = total_points_scores[label] + bg_coords = total_bg_coords[label] + for points_score in points_scores: + x, y = points_score[:2] + is_done = False + for pm in predicted_masks.get(label, []): + # check if that point is already assigned + if pm[int(y), int(x)] > 0: + is_done = True + break + if is_done: + continue + + point_coords = torch.cat((points_score[:2].unsqueeze(0), bg_coords), dim=0).unsqueeze(0) + point_coords = self._preprocess_coords(point_coords, original_size, self.config.model.image_size) + point_labels = torch.tensor( + [1] + [0] * len(bg_coords), dtype=torch.float32, device=self.device + ).unsqueeze(0) + mask = self._predict_masks( + image_embeddings=image_embeddings, + point_coords=point_coords, + point_labels=point_labels, + original_size=original_size, + is_cascade=is_cascade, + ) + predicted_masks[label].append((mask * points_score[2]).detach().cpu()) + used_points[label].append(points_score.detach().cpu()) + + # check overlapping area between different label masks + self._inspect_overlapping_areas(predicted_masks, used_points) + total_results.append([predicted_masks, used_points]) + return total_results + + def _inspect_overlapping_areas( + self, + predicted_masks: Dict[int, List[Tensor]], + used_points: Dict[int, List[Tensor]], + threshold_iou: float = 0.8, + ) -> None: + def _calculate_mask_iou(mask1: Tensor, mask2: Tensor): + assert mask1.ndim == 2 and mask2.ndim == 2 + intersection = torch.logical_and(mask1, mask2).sum().item() + union = torch.logical_or(mask1, mask2).sum().item() + + # Avoid division by zero + if union == 0: + return 0.0 + iou = intersection / union + return iou + + for (label, masks), (other_label, other_masks) in product(predicted_masks.items(), predicted_masks.items()): + if other_label <= label: + continue + + overlapped_label = [] + overlapped_other_label = [] + for (im, mask), (jm, other_mask) in product(enumerate(masks), enumerate(other_masks)): + _mask_iou = _calculate_mask_iou(mask, other_mask) + if _mask_iou > threshold_iou: + # compare overlapped regions between different labels and filter out the lower score + if used_points[label][im][2] > used_points[other_label][jm][2]: + overlapped_other_label.append(jm) + else: + overlapped_label.append(im) + elif _mask_iou > 0: + # refine the slightly overlapping region + overlapped_coords = torch.where(torch.logical_and(mask, other_mask)) + if used_points[label][im][2] > used_points[other_label][jm][2]: + other_mask[overlapped_coords] = 0.0 + else: + mask[overlapped_coords] = 0.0 + + for im in sorted(list(set(overlapped_label)), reverse=True): + masks.pop(im) + used_points[label].pop(im) + + for jm in sorted(list(set(overlapped_other_label)), reverse=True): + other_masks.pop(jm) + used_points[other_label].pop(jm) + + def _predict_masks( + self, + image_embeddings: Tensor, + point_coords: Tensor, + point_labels: Tensor, + original_size: Tensor, + is_cascade: bool = True, + ) -> Tensor: + """Predict target masks.""" + masks: Tensor + logits: Tensor + scores: Tensor + num_iter = 3 if is_cascade else 1 + for i in range(num_iter): + if i == 0: + # First-step prediction + mask_input = torch.zeros(1, 1, *map(lambda x: x * 4, image_embeddings.shape[2:]), device=self.device) + has_mask_input = self.has_mask_inputs[0].to(self.device) + + elif is_cascade and i == 1: + # Cascaded Post-refinement-1 + mask_input, masks = self._postprocess_masks(masks, logits, scores, is_single=True) # noqa: F821 + if masks.sum() == 0: + return masks + + has_mask_input = self.has_mask_inputs[1].to(self.device) + + elif is_cascade and i == 2: + # Cascaded Post-refinement-2 + mask_input, masks = self._postprocess_masks(masks, logits, scores) # noqa: F821 + if masks.sum() == 0: + return masks + + has_mask_input = self.has_mask_inputs[1].to(self.device) + coords = torch.nonzero(masks) + y, x = coords[:, 0], coords[:, 1] + box_coords = self._preprocess_coords( + torch.as_tensor( + [[[x.min(), y.min()], [x.max(), y.max()]]], dtype=torch.float32, device=self.device + ), + original_size, + self.config.model.image_size, + ) + point_coords = torch.cat((point_coords, box_coords), dim=1) + point_labels = torch.cat((point_labels, self.point_labels_box.to(self.device)), dim=1) + + high_res_masks, scores, logits = self( + image_embeddings=image_embeddings, + point_coords=point_coords, + point_labels=point_labels, + mask_input=mask_input, + has_mask_input=has_mask_input, + orig_size=original_size.unsqueeze(0), + ) + masks = high_res_masks > self.config.model.mask_threshold + _, masks = self._postprocess_masks(masks, logits, scores) + return masks + + def training_step(self, batch, batch_idx) -> None: + """Training step for `learn`.""" + self.learn(batch) + + def predict_step(self, batch, batch_idx): + """Predict step for `infer`.""" + results = self.infer(batch, self.reference_info["reference_feats"], self.reference_info["used_indices"]) + return [result[0] for result in results] # tmp: only mask + + def _preprocess_prompts(self, batch: Dict[str, Any]) -> Dict[Any, Any]: + """Preprocess prompts. + + Currently, preprocessing for bounding boxes is only supported. + + Args: + batch (Dict[str, Any]): Dictionary containing data and prompts information. + + Returns: + (Dict[Any, Any]): Processed and arranged each single prompt + using label information as keys. Unlike other prompts, `annotation` prompts will be aggregated + as single annotation. + """ + processed_prompts = defaultdict(list) + for prompt_name in ["annotations", "polygons", "bboxes", "points"]: + prompts = batch.get(prompt_name, None) + labels = batch["labels"].get(prompt_name, None) + if prompts is None or len(prompts) == 0: + continue + for prompt, label in zip(prompts, labels): + if isinstance(label, ScoredLabel): + label = int(label.id_) + # TODO (sungchul): revisit annotations and polygons + if prompt_name == "annotations": + processed_prompts[label].append({prompt_name: torch.as_tensor(prompt, device=self.device)}) + elif prompt_name == "polygons": + masks = [] + for polygon in prompt: + contour = [[int(point[0]), int(point[1])] for point in polygon] + mask_from_polygon = np.zeros(batch["original_size"], dtype=np.uint8) + mask_from_polygon = cv2.drawContours(mask_from_polygon, np.asarray([contour]), 0, 1, -1) + masks.append(mask_from_polygon) + processed_prompts[label].append({prompt_name: torch.tensor(prompt, device=self.device)}) + elif prompt_name == "bboxes": + processed_prompts[label].append( + { + prompt_name: { + "point_coords": torch.as_tensor(prompt.reshape(-1, 2, 2), device=self.device), + "point_labels": torch.tensor([[1]], device=self.device), + } + } + ) + elif prompt_name == "points": + processed_prompts[label].append( + { + prompt_name: { + "point_coords": torch.as_tensor(prompt.reshape(-1, 2), device=self.device), + "point_labels": torch.tensor([[1]], device=self.device), + } + } + ) + + processed_prompts = dict(sorted(processed_prompts.items(), key=lambda x: x)) # type: ignore[assignment] + return processed_prompts + + def _preprocess_coords( + self, + coords: Tensor, + ori_shape: Union[List[int], Tuple[int, int], Tensor], + target_length: int, + ) -> Tensor: + """Expects a torch tensor of length 2 in the final dimension. + + Requires the original image size in (H, W) format. + + Args: + coords (Tensor): Coordinates tensor. + ori_shape (Union[List[int], Tuple[int, int], Tensor]): Original size of image. + target_length (int): The length of the longest side of the image. + + Returns: + (Tensor): Resized coordinates. + """ + old_h, old_w = ori_shape + new_h, new_w = self.get_prepadded_size(ori_shape, target_length) + coords[..., 0] = coords[..., 0] * (new_w / old_w) + coords[..., 1] = coords[..., 1] * (new_h / old_h) + return coords + + def _generate_masked_features( + self, + feats: Tensor, + masks: Tensor, + threshold_mask: float, + ) -> Tuple[Tensor, ...]: + """Generate masked features. + + Args: + feats (Tensor): Raw reference features. It will be filtered with masks. + masks (Tensor): Reference masks used to filter features. + threshold_mask (float): Threshold to control masked region. + + Returns: + (Tensor): Masked features. + """ + scale_factor = self.config.model.image_size / max(masks.shape) + + # Post-process masks + masks = F.interpolate(masks.unsqueeze(0).unsqueeze(0), scale_factor=scale_factor, mode="bilinear").squeeze() + masks = self._pad_to_square(masks) + masks = F.interpolate(masks.unsqueeze(0).unsqueeze(0), size=feats.shape[0:2], mode="bilinear").squeeze() + + # Target feature extraction + if (masks > threshold_mask).sum() == 0: + # (for stability) there is no area to be extracted + return None + + masked_feat = feats[masks > threshold_mask] + masked_feat = masked_feat.mean(0).unsqueeze(0) + masked_feat = masked_feat / masked_feat.norm(dim=-1, keepdim=True) + + return masked_feat + + def _pad_to_square(self, x: Tensor) -> Tensor: + """Pad to a square input. + + Args: + x (Tensor): Mask to be padded. + + Returns: + (Tensor): Padded mask. + """ + h, w = x.shape[-2:] + padh = self.config.model.image_size - h + padw = self.config.model.image_size - w + x = F.pad(x, (0, padw, 0, padh)) + return x + + def _postprocess_masks( + self, + masks: Tensor, + logits: Tensor, + scores: Tensor, + is_single: bool = False, + ): + """Post-process masks for cascaded post-refinements.""" + if is_single: + best_idx = 0 + else: + # skip the first index components + scores, masks, logits = map(lambda x: x[:, 1:], (scores, masks, logits)) + + # filter zero masks + while len(scores[0]) > 0 and masks[0, (best_idx := torch.argmax(scores[0]))].sum() == 0: + scores, masks, logits = map( + lambda x: torch.cat((x[:, :best_idx], x[:, best_idx + 1 :]), dim=1), (scores, masks, logits) + ) + + if len(scores[0]) == 0: + # all predicted masks were zero masks, ignore them. + return None, torch.zeros(masks.shape[-2:], device="cpu") + + best_idx = torch.argmax(scores[0]) + return logits[:, best_idx], masks[0, best_idx] + + def set_metrics(self) -> None: + """Skip set_metrics unused in zero-shot learning.""" + pass + + def configure_optimizers(self) -> None: + """Skip configure_optimizers unused in zero-shot learning.""" + pass + + def _find_latest_reference_info(self, root: str = "vpm_zsl_reference_infos") -> Union[str, None]: + """Find latest reference info to be used.""" + if not os.path.isdir(root): + return None + if len(stamps := sorted(os.listdir(root), reverse=True)) > 0: + return stamps[0] + return None + + def on_train_start(self) -> None: + """Called at the beginning of training after sanity check.""" + self.initialize_reference_info() + + def on_predict_start(self) -> None: + """Called at the beginning of predicting.""" + if (latest_stamp := self._find_latest_reference_info()) is not None: + latest_reference_info = self.path_reference_info.format(latest_stamp) + self.reference_info = torch.load(latest_reference_info) + self.reference_info.to(self.device) + logger.info(f"reference info saved at {latest_reference_info} was successfully loaded.") + + def training_epoch_end(self, outputs) -> None: + """Called in the training loop at the very end of the epoch.""" + self.reference_info["used_indices"] = Parameter( + self.reference_info["used_indices"].unique(), requires_grad=False + ) + if self.config.model.save_outputs: + path_reference_info = self.path_reference_info.format(time.strftime("%Y%m%d-%H%M%S")) + os.makedirs(os.path.dirname(path_reference_info), exist_ok=True) + torch.save(self.reference_info, path_reference_info) + pickle.dump( + {k: v.numpy() for k, v in self.reference_info.items()}, + open(path_reference_info.replace(".pt", ".pickle"), "wb"), + ) + json.dump( + repr(self.trainer.datamodule.train_dataset.dataset), + open(path_reference_info.replace("reference_info.pt", "reference_meta.json"), "w"), + ) + logger.info(f"Saved reference info at {path_reference_info}.") diff --git a/src/otx/algorithms/visual_prompting/configs/__init__.py b/src/otx/algorithms/visual_prompting/configs/__init__.py new file mode 100644 index 00000000000..37330de0d28 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/__init__.py @@ -0,0 +1,4 @@ +"""OTX Algorithms - Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/algorithms/visual_prompting/configs/base/__init__.py b/src/otx/algorithms/visual_prompting/configs/base/__init__.py new file mode 100644 index 00000000000..2b214adc03d --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/base/__init__.py @@ -0,0 +1,18 @@ +"""Configs Initialization of OTX Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .configuration import VisualPromptingBaseConfig # noqa: F401 +from .configuration_enums import Models # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/configs/base/configuration.py b/src/otx/algorithms/visual_prompting/configs/base/configuration.py new file mode 100644 index 00000000000..d0505d0ae40 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/base/configuration.py @@ -0,0 +1,143 @@ +"""Configuration file of OTX Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +from attr import attrs + +from otx.algorithms.common.configs import BaseConfig, POTQuantizationPreset +from otx.api.configuration.elements import ( + ParameterGroup, + add_parameter_group, + boolean_attribute, + configurable_boolean, + configurable_float, + configurable_integer, + selectable, + string_attribute, +) +from otx.api.configuration.model_lifecycle import ModelLifecycle + + +@attrs +class VisualPromptingBaseConfig(BaseConfig): + """Configurations of OTX Visual Prompting.""" + + header = string_attribute("Configuration for a visual prompting task of OTX") + description = header + + @attrs + class __LearningParameters(BaseConfig.BaseLearningParameters): + header = string_attribute("Learning Parameters") + description = header + + @attrs + class __AlgoBackend(BaseConfig.BaseAlgoBackendParameters): + header = string_attribute("Parameters for the OTX algo-backend") + description = header + + @attrs + class __Postprocessing(ParameterGroup): + header = string_attribute("Postprocessing") + description = header + + image_size = configurable_integer( + header="Image size", + description="The size of the input image to the model.", + default_value=1024, + min_value=0, + max_value=2048, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + blur_strength = configurable_integer( + header="Blur strength", + description="With a higher value, the segmentation output will be smoother, but less accurate.", + default_value=1, + min_value=1, + max_value=25, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + soft_threshold = configurable_float( + default_value=0.5, + header="Soft threshold", + description="The threshold to apply to the probability output of the model, for each pixel. A higher value " + "means a stricter segmentation prediction.", + min_value=0.0, + max_value=1.0, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + embedded_processing = configurable_boolean( + default_value=True, + header="Embedded processing", + description="Flag that pre/postprocessing embedded.", + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + orig_width = configurable_integer( + header="Original width", + description="Model input width before embedding processing.", + default_value=64, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + orig_height = configurable_integer( + header="Original height", + description="Model input height before embedding processing.", + default_value=64, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + mask_threshold = configurable_float( + default_value=0.0, + header="Mask threshold", + description=( + "The threshold to apply to the raw logit output of the model, for each pixel. " + "A higher value means a stricter segmentation prediction." + ), + min_value=0.0, + max_value=1.0, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + downsizing = configurable_integer( + default_value=64, + header="The downsizing ratio", + description="The downsizing ratio of image encoder.", + min_value=1, + max_value=1024, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + @attrs + class __POTParameter(BaseConfig.BasePOTParameter): + header = string_attribute("POT Parameters") + description = header + visible_in_ui = boolean_attribute(False) + + preset = selectable( + default_value=POTQuantizationPreset.MIXED, + header="Preset", + description="Quantization preset that defines quantization scheme", + editable=True, + visible_in_ui=True, + ) + + learning_parameters = add_parameter_group(__LearningParameters) + algo_backend = add_parameter_group(__AlgoBackend) + postprocessing = add_parameter_group(__Postprocessing) + pot_parameters = add_parameter_group(__POTParameter) diff --git a/src/otx/algorithms/visual_prompting/configs/base/configuration_enums.py b/src/otx/algorithms/visual_prompting/configs/base/configuration_enums.py new file mode 100644 index 00000000000..c02458e8797 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/base/configuration_enums.py @@ -0,0 +1,25 @@ +"""Collection of utils for task implementation in Visual Prompting Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from otx.api.configuration import ConfigurableEnum + + +# pylint: disable=invalid-name +class Models(ConfigurableEnum): + """This Enum represents the types of models for inference.""" + + ImageEncoder = "image_encoder" + Decoder = "decoder" diff --git a/src/otx/algorithms/visual_prompting/configs/configuration.yaml b/src/otx/algorithms/visual_prompting/configs/configuration.yaml new file mode 100644 index 00000000000..40187dffd2d --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/configuration.yaml @@ -0,0 +1,235 @@ +description: Configuration for SAM +header: Configuration for SAM +id: "" +learning_parameters: + description: Learning Parameters + header: Learning Parameters + type: PARAMETER_GROUP + visible_in_ui: true + trainer: + description: Trainer Parameters + header: Trainer Parameters + type: PARAMETER_GROUP + visible_in_ui: true + max_epochs: + affects_outcome_of: TRAINING + default_value: 100 + description: + Maximum number of epochs to train for. If not specified, the training will + run until the early stopping criteria is met. + editable: true + header: Maximum number of epochs + max_value: 1000 + min_value: 1 + type: INTEGER + value: 100 + dataset: + description: Dataset Parameters + header: Dataset Parameters + type: PARAMETER_GROUP + visible_in_ui: true + use_mask: + header: Flag about using mask as label + affects_outcome_of: TRAINING + default_value: false + description: If using mask as-is (true) or converting it to polygon (false) + editable: true + value: false + type: BOOLEAN + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 2 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 32 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + optimizer: + description: Optimizer Parameters + header: Optimizer Parameters + type: PARAMETER_GROUP + visible_in_ui: true + lr: + affects_outcome_of: TRAINING + default_value: 0.0001 + description: + Increasing this value will speed up training convergence but might + make it unstable. + editable: true + header: Learning rate + max_value: 10 + min_value: 1.0e-07 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.0001 + visible_in_ui: true + warning: null + auto_hpo_state: NOT_POSSIBLE +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Mixed + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Mixed + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +postprocessing: + confidence_threshold: + affects_outcome_of: INFERENCE + default_value: 0.5 + description: + This threshold only takes effect if the threshold is not set based + on the result. + editable: true + header: Confidence threshold + max_value: 1 + min_value: 0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.5 + visible_in_ui: true + warning: null + description: Postprocessing + header: Postprocessing + result_based_confidence_threshold: + affects_outcome_of: INFERENCE + default_value: false + description: Confidence threshold is derived from the results + editable: true + header: Result based confidence threshold + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +algo_backend: + description: parameters for algo backend + header: Algo backend parameters + train_type: + affects_outcome_of: TRAINING + default_value: Incremental + description: Training scheme option that determines how to train the model + editable: True + enum_name: TrainType + header: Train type + options: + Incremental: "Incremental" + Zeroshot: "Zeroshot" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Incremental + visible_in_ui: false + warning: null + mem_cache_size: + affects_outcome_of: TRAINING + default_value: 1000000000 + description: Size of memory pool for caching decoded data to load data faster (bytes). + editable: true + header: Size of memory pool + max_value: 9223372036854775807 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + storage_cache_scheme: + affects_outcome_of: TRAINING + default_value: NONE + description: Scheme for storage cache + editable: true + enum_name: StorageCacheScheme + header: Scheme for storage cache + options: + NONE: "NONE" + AS_IS: "AS-IS" + JPEG_75: "JPEG/75" + JPEG_95: "JPEG/95" + PNG: "PNG" + TIFF: "TIFF" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/__init__.py b/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/__init__.py new file mode 100644 index 00000000000..7703180b940 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/__init__.py @@ -0,0 +1,6 @@ +"""Initialization of Configurable Parameters for SAM Visual Prompting Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .configuration import VisualPromptingConfig # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/config.yaml b/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/config.yaml new file mode 100644 index 00000000000..f0dd50ca827 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/config.yaml @@ -0,0 +1,94 @@ +dataset: + task: visual_prompting + train_batch_size: 2 + val_batch_size: 1 + test_batch_size: 1 + num_workers: 4 + image_size: 1024 # dimensions to which images are resized (mandatory) + normalize: + mean: + - 123.675 + - 116.28 + - 103.53 + std: + - 58.395 + - 57.12 + - 57.375 + offset_bbox: 20 # randomness for generating bounding box, pixel + use_point: false + use_bbox: false + +model: + name: SAM + image_size: 1024 + mask_threshold: 0. + return_logits: true + backbone: tiny_vit + loss_type: sam # <"sam", "medsam"> + freeze_image_encoder: true + freeze_prompt_encoder: true + freeze_mask_decoder: false + checkpoint: https://github.com/ChaoningZhang/MobileSAM/raw/master/weights/mobile_sam.pt + # just for inference + return_single_mask: true + use_stability_score: false + stability_score_offset: 1. + return_extra_metrics: false + +optimizer: + name: Adam + lr: 0.00001 + +callback: + checkpoint: # arguments for ModelCheckpoint + monitor: val_Dice + mode: max + save_last: true + verbose: true + early_stopping: # arguments for EarlyStopping + monitor: val_Dice + mode: max + verbose: true + +# PL Trainer Args. Don't add extra parameter here. +trainer: + enable_checkpointing: true + default_root_dir: null + gradient_clip_val: 0 + gradient_clip_algorithm: norm + num_nodes: 1 + devices: 1 + enable_progress_bar: true + overfit_batches: 0.0 + track_grad_norm: -1 + check_val_every_n_epoch: 1 # Don't validate before extracting features. + fast_dev_run: false + accumulate_grad_batches: 1 + max_epochs: 100 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: 1.0 + limit_val_batches: 1.0 + limit_test_batches: 1.0 + limit_predict_batches: 1.0 + val_check_interval: 1.0 + log_every_n_steps: 10 + accelerator: auto # <"cpu", "gpu", "tpu", "ipu", "hpu", "auto"> + strategy: null + sync_batchnorm: false + precision: 32 + enable_model_summary: true + num_sanity_val_steps: 0 + profiler: null + benchmark: false + deterministic: false + reload_dataloaders_every_n_epochs: 0 + auto_lr_find: false + replace_sampler_ddp: true + detect_anomaly: false + auto_scale_batch_size: false + plugins: null + move_metrics_to_cpu: false + multiple_trainloader_mode: max_size_cycle diff --git a/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/configuration.py b/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/configuration.py new file mode 100644 index 00000000000..166e904997e --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/configuration.py @@ -0,0 +1,14 @@ +"""Configuration file of OTX Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from attr import attrs + +from otx.algorithms.visual_prompting.configs.base import VisualPromptingBaseConfig + + +@attrs +class VisualPromptingConfig(VisualPromptingBaseConfig): + """Configurable parameters for Visual Prompting task.""" diff --git a/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/ptq_optimization_config.py b/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/ptq_optimization_config.py new file mode 100644 index 00000000000..9496ea6e22b --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/ptq_optimization_config.py @@ -0,0 +1,22 @@ +"""PTQ config file.""" +from nncf.parameters import ModelType +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ), +) + +model_type = ModelType.TRANSFORMER diff --git a/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/template.yaml b/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/template.yaml new file mode 100644 index 00000000000..46d70eae7ed --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_tiny_vit/template.yaml @@ -0,0 +1,30 @@ +# Description. +model_template_id: Visual_Prompting_SAM_Tiny_ViT +name: SAM_Tiny_ViT +task_type: VISUAL_PROMPTING +task_family: VISION +instantiation: "CLASS" +summary: Visual Prompting with TinyViT for the accurate predictions +application: ~ + +# Algo backend. +framework: OTXVisualPrompting v0.1.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.visual_prompting.tasks.TrainingTask + openvino: otx.algorithms.visual_prompting.tasks.openvino.OpenVINOVisualPromptingTask + +# Hyper Parameters +hyper_parameters: + base_path: ../configuration.yaml + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 38.95 +size: 47 diff --git a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/__init__.py b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/__init__.py new file mode 100644 index 00000000000..7703180b940 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/__init__.py @@ -0,0 +1,6 @@ +"""Initialization of Configurable Parameters for SAM Visual Prompting Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .configuration import VisualPromptingConfig # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/config.yaml b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/config.yaml new file mode 100644 index 00000000000..3738303c911 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/config.yaml @@ -0,0 +1,92 @@ +dataset: + task: visual_prompting + train_batch_size: 4 + val_batch_size: 1 + test_batch_size: 1 + num_workers: 4 + image_size: 1024 # dimensions to which images are resized (mandatory) + normalize: + mean: + - 123.675 + - 116.28 + - 103.53 + std: + - 58.395 + - 57.12 + - 57.375 + offset_bbox: 20 # randomness for generating bounding box, pixel + +model: + name: SAM + image_size: 1024 + mask_threshold: 0. + return_logits: true + backbone: vit_b + loss_type: sam # <"sam", "medsam"> + freeze_image_encoder: true + freeze_prompt_encoder: true + freeze_mask_decoder: false + checkpoint: https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth + # just for inference + return_single_mask: true + use_stability_score: false + stability_score_offset: 1. + return_extra_metrics: false + +optimizer: + name: Adam + lr: 0.000001 + +callback: + checkpoint: # arguments for ModelCheckpoint + monitor: val_Dice + mode: max + save_last: true + verbose: true + early_stopping: # arguments for EarlyStopping + monitor: val_Dice + mode: max + verbose: true + +# PL Trainer Args. Don't add extra parameter here. +trainer: + enable_checkpointing: true + default_root_dir: null + gradient_clip_val: 0 + gradient_clip_algorithm: norm + num_nodes: 1 + devices: 1 + enable_progress_bar: true + overfit_batches: 0.0 + track_grad_norm: -1 + check_val_every_n_epoch: 1 # Don't validate before extracting features. + fast_dev_run: false + accumulate_grad_batches: 1 + max_epochs: 100 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: 1.0 + limit_val_batches: 1.0 + limit_test_batches: 1.0 + limit_predict_batches: 1.0 + val_check_interval: 1.0 + log_every_n_steps: 10 + accelerator: auto # <"cpu", "gpu", "tpu", "ipu", "hpu", "auto"> + strategy: null + sync_batchnorm: false + precision: 32 + enable_model_summary: true + num_sanity_val_steps: 0 + profiler: null + benchmark: false + deterministic: false + reload_dataloaders_every_n_epochs: 0 + auto_lr_find: false + replace_sampler_ddp: true + detect_anomaly: false + auto_scale_batch_size: false + plugins: null + move_metrics_to_cpu: false + multiple_trainloader_mode: max_size_cycle diff --git a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.py b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.py new file mode 100644 index 00000000000..166e904997e --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.py @@ -0,0 +1,14 @@ +"""Configuration file of OTX Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from attr import attrs + +from otx.algorithms.visual_prompting.configs.base import VisualPromptingBaseConfig + + +@attrs +class VisualPromptingConfig(VisualPromptingBaseConfig): + """Configurable parameters for Visual Prompting task.""" diff --git a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/ptq_optimization_config.py b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/ptq_optimization_config.py new file mode 100644 index 00000000000..9496ea6e22b --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/ptq_optimization_config.py @@ -0,0 +1,22 @@ +"""PTQ config file.""" +from nncf.parameters import ModelType +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ), +) + +model_type = ModelType.TRANSFORMER diff --git a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/template.yaml b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/template.yaml new file mode 100644 index 00000000000..7bb33c1a560 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/template.yaml @@ -0,0 +1,30 @@ +# Description. +model_template_id: Visual_Prompting_SAM_ViT_B +name: SAM_ViT_B +task_type: VISUAL_PROMPTING +task_family: VISION +instantiation: "CLASS" +summary: Visual Prompting with ViT-B for the accurate predictions +application: ~ + +# Algo backend. +framework: OTXVisualPrompting v0.1.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.visual_prompting.tasks.TrainingTask + openvino: otx.algorithms.visual_prompting.tasks.openvino.OpenVINOVisualPromptingTask + +# Hyper Parameters +hyper_parameters: + base_path: ../configuration.yaml + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 483.71 +size: 362 diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/__init__.py b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/__init__.py new file mode 100644 index 00000000000..7703180b940 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/__init__.py @@ -0,0 +1,6 @@ +"""Initialization of Configurable Parameters for SAM Visual Prompting Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .configuration import VisualPromptingConfig # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/config.yaml b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/config.yaml new file mode 100644 index 00000000000..a50ea244fbd --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/config.yaml @@ -0,0 +1,81 @@ +dataset: + task: visual_prompting + train_batch_size: 1 + val_batch_size: 1 + test_batch_size: 1 + num_workers: 4 + image_size: 1024 # dimensions to which images are resized (mandatory) + normalize: + mean: + - 123.675 + - 116.28 + - 103.53 + std: + - 58.395 + - 57.12 + - 57.375 + offset_bbox: 0 + use_point: false + use_bbox: false + +model: + name: SAM + image_size: 1024 + mask_threshold: 0. + return_logits: true + backbone: tiny_vit + freeze_image_encoder: true + freeze_prompt_encoder: true + freeze_mask_decoder: true + checkpoint: https://github.com/ChaoningZhang/MobileSAM/raw/master/weights/mobile_sam.pt + # just for inference + return_single_mask: false + use_stability_score: false + stability_score_offset: 1. + return_extra_metrics: false + # zero-shot + default_threshold_reference: 0.3 + default_threshold_target: 0.65 + save_outputs: True + +# PL Trainer Args. Don't add extra parameter here. +trainer: + enable_checkpointing: false + gradient_clip_val: 0 + gradient_clip_algorithm: norm + num_nodes: 1 + devices: 1 + enable_progress_bar: true + overfit_batches: 0.0 + track_grad_norm: -1 + check_val_every_n_epoch: 1 # Don't validate before extracting features. + fast_dev_run: false + accumulate_grad_batches: 1 + max_epochs: 1 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: 1.0 + limit_val_batches: 0 # No validation + limit_test_batches: 1.0 + limit_predict_batches: 1.0 + val_check_interval: 1.0 + log_every_n_steps: 10 + accelerator: auto # <"cpu", "gpu", "tpu", "ipu", "hpu", "auto"> + strategy: null + sync_batchnorm: false + precision: 32 + enable_model_summary: true + num_sanity_val_steps: 0 + profiler: null + benchmark: false + deterministic: false + reload_dataloaders_every_n_epochs: 0 + auto_lr_find: false + replace_sampler_ddp: true + detect_anomaly: false + auto_scale_batch_size: false + plugins: null + move_metrics_to_cpu: false + multiple_trainloader_mode: max_size_cycle diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/configuration.py b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/configuration.py new file mode 100644 index 00000000000..166e904997e --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/configuration.py @@ -0,0 +1,14 @@ +"""Configuration file of OTX Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from attr import attrs + +from otx.algorithms.visual_prompting.configs.base import VisualPromptingBaseConfig + + +@attrs +class VisualPromptingConfig(VisualPromptingBaseConfig): + """Configurable parameters for Visual Prompting task.""" diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/configuration.yaml b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/configuration.yaml new file mode 100644 index 00000000000..917aba5f0c5 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/configuration.yaml @@ -0,0 +1,210 @@ +description: Configuration for SAM +header: Configuration for SAM +id: "" +learning_parameters: + description: Learning Parameters + header: Learning Parameters + type: PARAMETER_GROUP + visible_in_ui: true + trainer: + description: Trainer Parameters + header: Trainer Parameters + type: PARAMETER_GROUP + visible_in_ui: true + max_epochs: + affects_outcome_of: TRAINING + default_value: 1 + description: + Maximum number of epochs to train for. If not specified, the training will + run until the early stopping criteria is met. + editable: true + header: Maximum number of epochs + max_value: 1 + min_value: 1 + type: INTEGER + value: 1 + dataset: + description: Dataset Parameters + header: Dataset Parameters + type: PARAMETER_GROUP + visible_in_ui: true + use_mask: + header: Flag about using mask as label + affects_outcome_of: TRAINING + default_value: false + description: If using mask as-is (true) or converting it to polygon (false) + editable: true + value: false + type: BOOLEAN + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 2 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 32 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Mixed + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Mixed + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +postprocessing: + confidence_threshold: + affects_outcome_of: INFERENCE + default_value: 0.5 + description: + This threshold only takes effect if the threshold is not set based + on the result. + editable: true + header: Confidence threshold + max_value: 1 + min_value: 0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.5 + visible_in_ui: true + warning: null + description: Postprocessing + header: Postprocessing + result_based_confidence_threshold: + affects_outcome_of: INFERENCE + default_value: false + description: Confidence threshold is derived from the results + editable: true + header: Result based confidence threshold + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +algo_backend: + description: parameters for algo backend + header: Algo backend parameters + train_type: + affects_outcome_of: TRAINING + default_value: Incremental + description: Training scheme option that determines how to train the model + editable: True + enum_name: TrainType + header: Train type + options: + Incremental: "Incremental" + Zeroshot: "Zeroshot" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Incremental + visible_in_ui: false + warning: null + mem_cache_size: + affects_outcome_of: TRAINING + default_value: 1000000000 + description: Size of memory pool for caching decoded data to load data faster (bytes). + editable: true + header: Size of memory pool + max_value: 9223372036854775807 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + storage_cache_scheme: + affects_outcome_of: TRAINING + default_value: NONE + description: Scheme for storage cache + editable: true + enum_name: StorageCacheScheme + header: Scheme for storage cache + options: + NONE: "NONE" + AS_IS: "AS-IS" + JPEG_75: "JPEG/75" + JPEG_95: "JPEG/95" + PNG: "PNG" + TIFF: "TIFF" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/ptq_optimization_config.py b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/ptq_optimization_config.py new file mode 100644 index 00000000000..9496ea6e22b --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/ptq_optimization_config.py @@ -0,0 +1,22 @@ +"""PTQ config file.""" +from nncf.parameters import ModelType +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ), +) + +model_type = ModelType.TRANSFORMER diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/template.yaml b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/template.yaml new file mode 100644 index 00000000000..d29f564e46a --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/template.yaml @@ -0,0 +1,38 @@ +# Description. +model_template_id: Zero_Shot_SAM_Tiny_ViT +name: Zero_Shot_SAM_Tiny_ViT +task_type: VISUAL_PROMPTING +task_family: VISION +instantiation: "CLASS" +summary: Zero SHot Visual Prompting with TinyViT for the accurate predictions +application: ~ + +# Algo backend. +framework: OTXVisualPrompting v0.1.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.visual_prompting.tasks.ZeroShotTask + openvino: otx.algorithms.visual_prompting.tasks.openvino.OpenVINOZeroShotVisualPromptingTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + parameter_overrides: + learning_parameters: + dataset: + train_batch_size: + default_value: 1 + algo_backend: + train_type: + default_value: Zeroshot + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 38.18 +size: 25 diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/__init__.py b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/__init__.py new file mode 100644 index 00000000000..7703180b940 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/__init__.py @@ -0,0 +1,6 @@ +"""Initialization of Configurable Parameters for SAM Visual Prompting Task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .configuration import VisualPromptingConfig # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/config.yaml b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/config.yaml new file mode 100644 index 00000000000..d033d45e48a --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/config.yaml @@ -0,0 +1,81 @@ +dataset: + task: visual_prompting + train_batch_size: 1 + val_batch_size: 1 + test_batch_size: 1 + num_workers: 4 + image_size: 1024 # dimensions to which images are resized (mandatory) + normalize: + mean: + - 123.675 + - 116.28 + - 103.53 + std: + - 58.395 + - 57.12 + - 57.375 + offset_bbox: 0 + use_point: false + use_bbox: false + +model: + name: SAM + image_size: 1024 + mask_threshold: 0. + return_logits: true + backbone: vit_b + freeze_image_encoder: true + freeze_prompt_encoder: true + freeze_mask_decoder: true + checkpoint: https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth + # just for inference + return_single_mask: false + use_stability_score: false + stability_score_offset: 1. + return_extra_metrics: false + # zero-shot + default_threshold_reference: 0.3 + default_threshold_target: 0.65 + save_outputs: True + +# PL Trainer Args. Don't add extra parameter here. +trainer: + enable_checkpointing: false + gradient_clip_val: 0 + gradient_clip_algorithm: norm + num_nodes: 1 + devices: 1 + enable_progress_bar: true + overfit_batches: 0.0 + track_grad_norm: -1 + check_val_every_n_epoch: 1 # Don't validate before extracting features. + fast_dev_run: false + accumulate_grad_batches: 1 + max_epochs: 1 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: 1.0 + limit_val_batches: 0 # No validation + limit_test_batches: 1.0 + limit_predict_batches: 1.0 + val_check_interval: 1.0 + log_every_n_steps: 10 + accelerator: auto # <"cpu", "gpu", "tpu", "ipu", "hpu", "auto"> + strategy: null + sync_batchnorm: false + precision: 32 + enable_model_summary: true + num_sanity_val_steps: 0 + profiler: null + benchmark: false + deterministic: false + reload_dataloaders_every_n_epochs: 0 + auto_lr_find: false + replace_sampler_ddp: true + detect_anomaly: false + auto_scale_batch_size: false + plugins: null + move_metrics_to_cpu: false + multiple_trainloader_mode: max_size_cycle diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/configuration.py b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/configuration.py new file mode 100644 index 00000000000..166e904997e --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/configuration.py @@ -0,0 +1,14 @@ +"""Configuration file of OTX Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from attr import attrs + +from otx.algorithms.visual_prompting.configs.base import VisualPromptingBaseConfig + + +@attrs +class VisualPromptingConfig(VisualPromptingBaseConfig): + """Configurable parameters for Visual Prompting task.""" diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/configuration.yaml b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/configuration.yaml new file mode 100644 index 00000000000..917aba5f0c5 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/configuration.yaml @@ -0,0 +1,210 @@ +description: Configuration for SAM +header: Configuration for SAM +id: "" +learning_parameters: + description: Learning Parameters + header: Learning Parameters + type: PARAMETER_GROUP + visible_in_ui: true + trainer: + description: Trainer Parameters + header: Trainer Parameters + type: PARAMETER_GROUP + visible_in_ui: true + max_epochs: + affects_outcome_of: TRAINING + default_value: 1 + description: + Maximum number of epochs to train for. If not specified, the training will + run until the early stopping criteria is met. + editable: true + header: Maximum number of epochs + max_value: 1 + min_value: 1 + type: INTEGER + value: 1 + dataset: + description: Dataset Parameters + header: Dataset Parameters + type: PARAMETER_GROUP + visible_in_ui: true + use_mask: + header: Flag about using mask as label + affects_outcome_of: TRAINING + default_value: false + description: If using mask as-is (true) or converting it to polygon (false) + editable: true + value: false + type: BOOLEAN + train_batch_size: + affects_outcome_of: TRAINING + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 2 + description: + The number of training samples seen in each iteration of training. + Increasing this value improves training time and may make the training more + stable. A larger batch size has higher memory requirements. + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 32 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. +pot_parameters: + description: POT Parameters + header: POT Parameters + preset: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: Mixed + description: Quantization preset that defines quantization scheme + editable: true + enum_name: POTQuantizationPreset + header: Preset + options: + MIXED: Mixed + PERFORMANCE: Performance + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Mixed + visible_in_ui: true + warning: null + stat_subset_size: + affects_outcome_of: NONE + auto_hpo_state: not_possible + auto_hpo_value: null + default_value: 300 + description: Number of data samples used for post-training optimization + editable: true + header: Number of data samples + max_value: 1000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 300 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +postprocessing: + confidence_threshold: + affects_outcome_of: INFERENCE + default_value: 0.5 + description: + This threshold only takes effect if the threshold is not set based + on the result. + editable: true + header: Confidence threshold + max_value: 1 + min_value: 0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.5 + visible_in_ui: true + warning: null + description: Postprocessing + header: Postprocessing + result_based_confidence_threshold: + affects_outcome_of: INFERENCE + default_value: false + description: Confidence threshold is derived from the results + editable: true + header: Result based confidence threshold + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: false + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +algo_backend: + description: parameters for algo backend + header: Algo backend parameters + train_type: + affects_outcome_of: TRAINING + default_value: Incremental + description: Training scheme option that determines how to train the model + editable: True + enum_name: TrainType + header: Train type + options: + Incremental: "Incremental" + Zeroshot: "Zeroshot" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: Incremental + visible_in_ui: false + warning: null + mem_cache_size: + affects_outcome_of: TRAINING + default_value: 1000000000 + description: Size of memory pool for caching decoded data to load data faster (bytes). + editable: true + header: Size of memory pool + max_value: 9223372036854775807 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + storage_cache_scheme: + affects_outcome_of: TRAINING + default_value: NONE + description: Scheme for storage cache + editable: true + enum_name: StorageCacheScheme + header: Scheme for storage cache + options: + NONE: "NONE" + AS_IS: "AS-IS" + JPEG_75: "JPEG/75" + JPEG_95: "JPEG/95" + PNG: "PNG" + TIFF: "TIFF" + type: SELECTABLE + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + visible_in_ui: false + warning: null + type: PARAMETER_GROUP + visible_in_ui: false +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/ptq_optimization_config.py b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/ptq_optimization_config.py new file mode 100644 index 00000000000..9496ea6e22b --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/ptq_optimization_config.py @@ -0,0 +1,22 @@ +"""PTQ config file.""" +from nncf.parameters import ModelType +from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters +from nncf.quantization.range_estimator import ( + AggregatorType, + RangeEstimatorParameters, + StatisticsCollectorParameters, + StatisticsType, +) + +advanced_parameters = AdvancedQuantizationParameters( + activations_range_estimator_params=RangeEstimatorParameters( + min=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MIN, quantile_outlier_prob=1e-4 + ), + max=StatisticsCollectorParameters( + statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 + ), + ), +) + +model_type = ModelType.TRANSFORMER diff --git a/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/template.yaml b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/template.yaml new file mode 100644 index 00000000000..df353fbd235 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_vit_b/template.yaml @@ -0,0 +1,38 @@ +# Description. +model_template_id: Zero_Shot_SAM_ViT_B +name: Zero_Shot_SAM_ViT_B +task_type: VISUAL_PROMPTING +task_family: VISION +instantiation: "CLASS" +summary: Zero SHot Visual Prompting with ViT-B for the accurate predictions +application: ~ + +# Algo backend. +framework: OTXVisualPrompting v0.1.0 + +# Task implementations. +entrypoints: + base: otx.algorithms.visual_prompting.tasks.ZeroShotTask + openvino: otx.algorithms.visual_prompting.tasks.openvino.OpenVINOZeroShotVisualPromptingTask + +# Hyper Parameters +hyper_parameters: + base_path: ./configuration.yaml + parameter_overrides: + learning_parameters: + dataset: + train_batch_size: + default_value: 1 + algo_backend: + train_type: + default_value: Zeroshot + +# Training resources. +max_nodes: 1 +training_targets: + - GPU + - CPU + +# Computational Complexity +gigaflops: 483.71 +size: 362 diff --git a/src/otx/algorithms/visual_prompting/tasks/__init__.py b/src/otx/algorithms/visual_prompting/tasks/__init__.py new file mode 100644 index 00000000000..a4c0a3e0366 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/tasks/__init__.py @@ -0,0 +1,8 @@ +"""OTX Algorithms - Visual Prompting.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .inference import InferenceTask, ZeroShotTask # noqa: F401 +from .openvino import OpenVINOVisualPromptingTask # noqa: F401 +from .train import TrainingTask # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/tasks/inference.py b/src/otx/algorithms/visual_prompting/tasks/inference.py new file mode 100644 index 00000000000..97030f3108c --- /dev/null +++ b/src/otx/algorithms/visual_prompting/tasks/inference.py @@ -0,0 +1,564 @@ +"""Visual Prompting Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import ctypes +import io +import json +import os +import shutil +import tempfile +import time +import warnings +from collections import OrderedDict +from typing import Any, Dict, List, Optional, Union + +import openvino as ov +import torch +from omegaconf import DictConfig, ListConfig +from openvino.tools import mo +from pytorch_lightning import LightningModule, Trainer +from pytorch_lightning.callbacks import TQDMProgressBar +from pytorch_lightning.loggers import CSVLogger + +from otx.algorithms.common.configs.training_base import TrainType +from otx.algorithms.common.utils import set_random_seed +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.callbacks import ( + InferenceCallback, + ZeroShotInferenceCallback, +) +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.config import ( + get_visual_promtping_config, +) +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets import ( + OTXVisualPromptingDataModule, +) +from otx.algorithms.visual_prompting.configs.base.configuration import ( + VisualPromptingBaseConfig, +) +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ( + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.train_parameters import TrainParameters +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.export_interface import ExportType, IExportTask +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.unload_interface import IUnload +from otx.utils.logger import get_logger + +logger = get_logger() + + +# pylint: disable=too-many-instance-attributes +class InferenceTask(IInferenceTask, IEvaluationTask, IExportTask, IUnload): + """Base Visual Prompting Task. + + Train, Infer, and Export an Visual Prompting Task. + + Args: + task_environment (TaskEnvironment): OTX Task environment. + output_path (Optional[str]): output path where task output are saved. + """ + + def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] = None) -> None: + torch.backends.cudnn.enabled = True + logger.info("Initializing the task environment.") + self.task_environment = task_environment + self.task_type = task_environment.model_template.task_type + self.model_name = task_environment.model_template.name + self.labels = task_environment.get_labels() + self.hyper_parameters: VisualPromptingBaseConfig = self.task_environment.get_hyper_parameters() + self.train_type = self.hyper_parameters.algo_backend.train_type # type: ignore[attr-defined] + + template_file_path = task_environment.model_template.model_template_path + self.base_dir = os.path.abspath(os.path.dirname(template_file_path)) + + # Hyperparameters. + self._work_dir_is_temp = False + self.output_path = output_path + self.mode = "train" + if task_environment.model is not None and task_environment.model.train_dataset is None: + self.mode = "export" + if self.output_path is None: + self.output_path = tempfile.mkdtemp(prefix="otx-visual_prompting") + self._work_dir_is_temp = True + self.mode = "inference" + self.config = self.get_config() + + # Set default model attributes. + self.optimization_methods: List[OptimizationMethod] = [] + self.precision = [ModelPrecision.FP32] + self.optimization_type = ModelOptimizationType.MO + + self.trainer: Trainer + self._model_ckpt: Optional[str] = None + + self.timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) + + def set_seed(self): + """Set seed and deterministic.""" + if self.seed is None: + # If the seed is not present via task.train, it will be found in the recipe. + self.seed = self.config.get("seed", 5) + if not self.deterministic: + # deterministic is the same. + self.deterministic = self.config.get("deterministic", False) + self.config["seed"] = self.seed + self.config["deterministic"] = self.deterministic + set_random_seed(self.seed, logger, self.deterministic) + + def get_config(self) -> Union[DictConfig, ListConfig]: + """Get Visual Prompting Config from task environment. + + Returns: + Union[DictConfig, ListConfig]: Visual Prompting config. + """ + # set checkpoints + model_checkpoint: Optional[str] = None + resume_from_checkpoint: Optional[str] = None + if self.mode == "train" and self.task_environment.model is not None: + # when args.load_weights or args.resume_from is set + checkpoint_path = str(self.task_environment.model.model_adapters.get("path", None)) + if self.task_environment.model.model_adapters.get("resume", False): + resume_from_checkpoint = checkpoint_path + else: + model_checkpoint = checkpoint_path + + config = get_visual_promtping_config( + task_name=self.model_name, + otx_config=self.hyper_parameters, + config_dir=self.base_dir, + mode=self.mode, + model_checkpoint=model_checkpoint, + resume_from_checkpoint=resume_from_checkpoint, + ) + + config.dataset.task = "visual_prompting" + + return config + + def load_model(self, otx_model: Optional[ModelEntity] = None) -> LightningModule: + """Create and Load Visual Prompting Module. + + Currently, load model through `sam_model_registry` because there is only SAM. + If other visual prompting model is added, loading model process must be changed. + + Args: + otx_model (Optional[ModelEntity]): OTX Model from the task environment. + + Returns: + LightningModule: Visual prompting model with/without weights. + """ + + def get_model(config: DictConfig, train_type: TrainType, state_dict: Optional[OrderedDict] = None): + if config.model.name == "SAM": + if train_type == TrainType.Incremental: + from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models import ( + SegmentAnything as VisualPrompter, + ) + elif train_type == TrainType.Zeroshot: + from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models import ( # type: ignore[assignment] # noqa: E501 + ZeroShotSegmentAnything as VisualPrompter, + ) + + model = VisualPrompter(config=config, state_dict=state_dict) + else: + raise NotImplementedError( + (f"Current selected model {config.model.name} is not implemented. " f"Use SAM instead.") + ) + return model + + state_dict = None + if otx_model is None: + logger.info( + "No trained model in project yet. Created new model with '%s'", + self.model_name, + ) + elif otx_model.model_adapters.get("resume", False): + # If resuming, pass this part to load checkpoint in Trainer + logger.info(f"To resume {otx_model.model_adapters.get('path')}, the checkpoint will be loaded in Trainer.") + + else: + # Load state_dict + buffer = io.BytesIO(otx_model.get_data("weights.pth")) + model_data = torch.load(buffer, map_location=torch.device("cpu")) + if model_data.get("state_dict", None) and model_data.get("pytorch-lightning_version", None): + # Load state_dict from pytorch lightning checkpoint or weights.pth saved by visual prompting task + # In pytorch lightning checkpoint, there are metas: epoch, global_step, pytorch-lightning_version, + # state_dict, loops, callbacks, optimizer_states, lr_schedulers, hparams_name, hyper_parameters. + # To confirm if it is from pytorch lightning, check if one or two of them is in model_data. + state_dict = model_data["state_dict"] + + elif model_data.get("model", None) and model_data.get("config", None): + # Load state_dict from checkpoint saved by otx other tasks + if model_data["config"]["model"]["backbone"] != self.config["model"]["backbone"]: + logger.warning( + "Backbone of the model in the Task Environment is different from the one in the template. " + f"creating model with backbone={model_data['config']['model']['backbone']}" + ) + self.config["model"]["backbone"] = model_data["config"]["model"]["backbone"] + state_dict = model_data["model"] + + else: + # Load state_dict from naive pytorch checkpoint + state_dict = model_data + + try: + model = get_model(config=self.config, train_type=self.train_type, state_dict=state_dict) + logger.info("Complete to load model.") + except BaseException as exception: + raise ValueError("Could not load the saved model. The model file structure is invalid.") from exception + + return model + + def cancel_training(self) -> None: # noqa: D102 + raise NotImplementedError + + def infer(self, dataset: DatasetEntity, inference_parameters: InferenceParameters) -> DatasetEntity: + """Perform inference on a dataset. + + Args: + dataset (DatasetEntity): Dataset to infer. + inference_parameters (InferenceParameters): Inference parameters. + + Returns: + DatasetEntity: Output dataset with predictions. + """ + logger.info("Performing inference on the validation set using the base torch model.") + self.model = self.load_model(otx_model=self.task_environment.model) + datamodule = OTXVisualPromptingDataModule( + config=self.config.dataset, dataset=dataset, train_type=self.train_type + ) + + logger.info("Inference Configs '%s'", self.config) + + # Callbacks + inference_callback = InferenceCallback(otx_dataset=dataset) + callbacks = [TQDMProgressBar(), inference_callback] + + self.trainer = Trainer(**self.config.trainer, logger=False, callbacks=callbacks) + self.trainer.predict(model=self.model, datamodule=datamodule) + + return inference_callback.otx_dataset + + def evaluate(self, output_resultset: ResultSetEntity, evaluation_metric: Optional[str] = None) -> None: + """Evaluate the performance on a result set. + + Args: + output_resultset (ResultSetEntity): Result Set from which the performance is evaluated. + evaluation_metric (Optional[str], optional): Evaluation metric. Defaults to None. Instead, + metric is chosen depending on the task type. + """ + metric = MetricsHelper.compute_dice_averaged_over_pixels(output_resultset) + logger.info(f"mDice after evaluation: {metric.overall_dice.value}") + output_resultset.performance = metric.get_performance() + logger.info("Evaluation completed") + + def _export_to_onnx(self, onnx_path: Dict[str, str]): + """Export model to ONNX. + + Args: + onnx_path (Dict[str, str]): Paths to save ONNX models. + """ + height = width = self.config.model.image_size + for module, path in onnx_path.items(): + if module == "visual_prompting_image_encoder": + dummy_inputs = {"images": torch.randn(1, 3, height, width, dtype=torch.float32)} + output_names = ["image_embeddings"] + dynamic_axes = None + model_to_export = self.model.image_encoder + + else: + # sam without backbone + embed_dim = self.model.prompt_encoder.embed_dim + embed_size = self.model.prompt_encoder.image_embedding_size + mask_input_size = [4 * x for x in embed_size] + dynamic_axes = { + "point_coords": {1: "num_points"}, + "point_labels": {1: "num_points"}, + } + dummy_inputs = { + "image_embeddings": torch.zeros(1, embed_dim, *embed_size, dtype=torch.float32), + "point_coords": torch.randint(low=0, high=1024, size=(1, 2, 2), dtype=torch.float32), + "point_labels": torch.randint(low=0, high=4, size=(1, 2), dtype=torch.float32), + "mask_input": torch.randn(1, 1, *mask_input_size, dtype=torch.float32), + "has_mask_input": torch.tensor([[1]], dtype=torch.float32), + "orig_size": torch.randint(low=256, high=2048, size=(1, 2), dtype=torch.int64), + } + output_names = ["upscaled_masks", "iou_predictions", "low_res_masks"] + model_to_export = self.model + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=torch.jit.TracerWarning) + warnings.filterwarnings("ignore", category=UserWarning) + with open(path, "wb") as f: + torch.onnx.export( + model_to_export, + tuple(dummy_inputs.values()), + f, + export_params=True, + verbose=False, + opset_version=13, + do_constant_folding=True, + input_names=list(dummy_inputs.keys()), + output_names=output_names, + dynamic_axes=dynamic_axes, + ) + + def export( # noqa: D102 + self, + export_type: ExportType, + output_model: ModelEntity, + precision: ModelPrecision = ModelPrecision.FP32, + dump_features: bool = False, + ) -> None: + """Export model to OpenVINO IR. + + When SAM gets an image for inference, image encoder runs just once to get image embedding. + After that, prompt encoder + mask decoder runs repeatedly to get mask prediction. + For this case, SAM should be divided into two parts, image encoder and prompt encoder + mask decoder. + + Args: + export_type (ExportType): Export type should be ExportType.OPENVINO + output_model (ModelEntity): The model entity in which to write the OpenVINO IR data + precision (bool): Output model weights and inference precision + dump_features (bool): Flag to return "feature_vector" and "saliency_map". + + Raises: + Exception: If export_type is not ExportType.OPENVINO + """ + if dump_features: + logger.warning( + "Feature dumping is not implemented for the visual prompting task." + "The saliency maps and representation vector outputs will not be dumped in the exported model." + ) + + self.model = self.load_model(otx_model=self.task_environment.model) + if export_type == ExportType.ONNX: + output_model.model_format = ModelFormat.ONNX + output_model.optimization_type = ModelOptimizationType.ONNX + if precision == ModelPrecision.FP16: + raise RuntimeError("Export to FP16 ONNX is not supported") + elif export_type == ExportType.OPENVINO: + output_model.model_format = ModelFormat.OPENVINO + output_model.optimization_type = ModelOptimizationType.MO + else: + raise RuntimeError(f"not supported export type {export_type}") + + self.precision[0] = precision + output_model.has_xai = dump_features + + logger.info("Exporting to the OpenVINO model.") + onnx_path = { + "visual_prompting_image_encoder": os.path.join(self.output_path, "visual_prompting_image_encoder.onnx"), + "visual_prompting_decoder": os.path.join(self.output_path, "visual_prompting_decoder.onnx"), + } + self._export_to_onnx(onnx_path) + + if export_type == ExportType.ONNX: + for module, path in onnx_path.items(): + with open(path, "rb") as file: + output_model.set_data(f"{module}.onnx", file.read()) + else: + for module, path in onnx_path.items(): + mo_args: Dict[str, Any] = {"input_model": path} + if module == "visual_prompting_image_encoder": + mo_args.update( + { + "mean_values": list(self.config.dataset.normalize.mean), + "scale_values": list(self.config.dataset.normalize.std), + } + ) + if precision == ModelPrecision.FP16: + mo_args.update({"compress_to_fp16": True}) + + ov_model = mo.convert_model(**mo_args) + ov.save_model(ov_model, os.path.join(self.output_path, f"{module}.xml")) + with open(path.replace(".onnx", ".bin"), "rb") as file: + output_model.set_data(f"{module}.bin", file.read()) + with open(path.replace(".onnx", ".xml"), "rb") as file: + output_model.set_data(f"{module}.xml", file.read()) + + output_model.precision = self.precision + output_model.optimization_methods = self.optimization_methods + + output_model.set_data("label_schema.json", label_schema_to_bytes(self.task_environment.label_schema)) + self._set_metadata(output_model) + + def model_info(self) -> Dict: + """Return model info to save the model weights. + + Returns: + Dict: Model info. + """ + if not self._model_ckpt: + logger.warn("model checkpoint is not set, return empty dictionary.") + return {} + return torch.load(self._model_ckpt, map_location="cpu") + + def save_model(self, output_model: ModelEntity) -> None: + """Save the model after training is completed. + + Args: + output_model (ModelEntity): Output model onto which the weights are saved. + """ + logger.info("Saving the model weights.") + model_info = self.model_info() + buffer = io.BytesIO() + torch.save(model_info, buffer) + output_model.set_data("weights.pth", buffer.getvalue()) + output_model.set_data("label_schema.json", label_schema_to_bytes(self.task_environment.label_schema)) + + output_model.precision = self.precision + output_model.optimization_methods = self.optimization_methods + + def _set_metadata(self, output_model: ModelEntity) -> None: + """Set metadata to the output model.""" + metadata = {"image_size": int(self.config.dataset.image_size)} + + # Set the task type for inferencer + metadata["task"] = str(self.task_type).lower().split("_")[-1] # type: ignore + output_model.set_data("metadata", json.dumps(metadata).encode()) + + @staticmethod + def _is_docker() -> bool: + raise NotImplementedError + + def unload(self) -> None: + """Unload the task.""" + self.cleanup() + + if self._is_docker(): + logger.warning("Got unload request. Unloading models. Throwing Segmentation Fault on purpose") + ctypes.string_at(0) + + else: + logger.warning("Got unload request, but not on Docker. Only clearing CUDA cache") + torch.cuda.empty_cache() + logger.warning( + "Done unloading. Torch is still occupying %f bytes of GPU memory", + torch.cuda.memory_allocated(), + ) + + def cleanup(self) -> None: + """Clean up work directory.""" + if self._work_dir_is_temp: + self._delete_scratch_space() + + def _delete_scratch_space(self) -> None: + """Remove model checkpoints and otx logs.""" + if os.path.exists(self.output_path): + shutil.rmtree(self.output_path, ignore_errors=False) + + +class ZeroShotTask(InferenceTask): + """Learn task for Zero-shot learning. + + **There are two ways to be decided: + 1. use it independently <-- temporarily current setting + 2. use it depending on template + + The objective of this task is to get reference features and export it with decoder modules. + """ + + def train( # noqa: D102 + self, + dataset: DatasetEntity, + output_model: ModelEntity, + train_parameters: TrainParameters, + seed: Optional[int] = None, + deterministic: bool = False, + ) -> None: + logger.info("Training the model.") + + self.seed = seed + self.deterministic = deterministic + self.set_seed() + self.config.trainer.deterministic = "warn" if deterministic else deterministic + + logger.info(f"Training Configs {self.config}") + + self.model = self.load_model(otx_model=self.task_environment.model) + + datamodule = OTXVisualPromptingDataModule( + config=self.config.dataset, dataset=dataset, train_type=self.train_type + ) + + self.trainer = Trainer( + logger=CSVLogger(save_dir=self.output_path, name=".", version=self.timestamp), **self.config.trainer + ) + self.trainer.fit(model=self.model, datamodule=datamodule) + + # save resulting model + self.save_model(output_model) + + def infer(self, dataset: DatasetEntity, inference_parameters: InferenceParameters) -> DatasetEntity: + """Perform inference on a dataset. + + Args: + dataset (DatasetEntity): Dataset to infer. + inference_parameters (InferenceParameters): Inference parameters. + + Returns: + DatasetEntity: Output dataset with predictions. + """ + logger.info("Performing inference on the validation set using the base torch model.") + self.model = self.load_model(otx_model=self.task_environment.model) + datamodule = OTXVisualPromptingDataModule( + config=self.config.dataset, dataset=dataset, train_type=self.train_type + ) + + logger.info("Inference Configs '%s'", self.config) + + # Callbacks + inference_callback = ZeroShotInferenceCallback( + otx_dataset=dataset, label_schema=self.task_environment.label_schema + ) + callbacks = [TQDMProgressBar(), inference_callback] + + self.trainer = Trainer(**self.config.trainer, logger=False, callbacks=callbacks) + self.trainer.predict(model=self.model, datamodule=datamodule) + + return inference_callback.otx_dataset + + def save_model(self, output_model: ModelEntity) -> None: + """Save the model after training is completed. + + Args: + output_model (ModelEntity): Output model onto which the weights are saved. + """ + logger.info("Saving the model weights and reference features.") + + model_info = self.model.state_dict() + model_info.pop("reference_info.reference_feats") + model_info.pop("reference_info.used_indices") + + buffer = io.BytesIO() + torch.save(model_info, buffer) + output_model.set_data("weights.pth", buffer.getvalue()) + output_model.set_data("label_schema.json", label_schema_to_bytes(self.task_environment.label_schema)) + + output_model.precision = self.precision + output_model.optimization_methods = self.optimization_methods diff --git a/src/otx/algorithms/visual_prompting/tasks/openvino.py b/src/otx/algorithms/visual_prompting/tasks/openvino.py new file mode 100644 index 00000000000..632d35749da --- /dev/null +++ b/src/otx/algorithms/visual_prompting/tasks/openvino.py @@ -0,0 +1,1147 @@ +"""OpenVINO Visual Prompting Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import io +import json +import os +import pickle +import random +import tempfile +import time +from collections import defaultdict +from copy import deepcopy +from itertools import product +from pathlib import Path +from typing import Any, DefaultDict, Dict, List, Optional, Tuple, Type, Union +from zipfile import ZipFile + +import attr +import cv2 +import nncf +import numpy as np +import openvino.runtime as ov +from addict import Dict as ADDict +from nncf.common.quantization.structs import QuantizationPreset +from openvino.model_api.adapters import OpenvinoAdapter, create_core +from openvino.model_api.models import Model + +from otx.algorithms.common.utils import get_default_async_reqs_num, read_py_config +from otx.algorithms.common.utils.ir import check_if_quantized +from otx.algorithms.visual_prompting.adapters.openvino import model_wrappers +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset import ( + OTXVisualPromptingDataset, + get_transform, +) +from otx.algorithms.visual_prompting.configs.base import VisualPromptingBaseConfig +from otx.api.entities.annotation import Annotation +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import ( + InferenceParameters, + default_progress_callback, +) +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ( + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.model_template import TaskType +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.serialization.label_mapper import LabelSchemaMapper, label_schema_to_bytes +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.exportable_code import demo +from otx.api.usecases.exportable_code.inference.inference import IInferencer +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( + VisualPromptingToAnnotationConverter, +) +from otx.api.usecases.tasks.interfaces.deployment_interface import IDeploymentTask +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.optimization_interface import ( + IOptimizationTask, + OptimizationType, +) +from otx.utils.logger import get_logger + +logger = get_logger() + + +class OpenVINOVisualPromptingInferencer(IInferencer): + """Inferencer implementation for Visual Prompting using OpenVINO backend. + + This inferencer has two models, image encoder and decoder. + + Args: + hparams (VisualPromptingBaseConfig): Hyper parameters that the model should use. + label_schema (LabelSchemaEntity): LabelSchemaEntity that was used during model training. + model_files (Dict[str, Union[str, Path, bytes]]): Path or bytes to model to load, + `.xml`, `.bin` or `.onnx` file. + weight_files (Dict[str, Union[str, Path, bytes, None]], optional): Path or bytes to weights to load, + `.xml`, `.bin` or `.onnx` file. Defaults to None. + device (str): Device to run inference on, such as CPU, GPU or MYRIAD. Defaults to "CPU". + num_requests (int) : Maximum number of requests that the inferencer can make. + Good value is the number of available cores. Defaults to 1. + """ + + def __init__( + self, + hparams: VisualPromptingBaseConfig, + label_schema: LabelSchemaEntity, + model_files: Dict[str, Union[str, Path, bytes]], + weight_files: Optional[Dict[str, Union[str, Path, bytes, None]]] = {}, + device: str = "CPU", + num_requests: int = 1, + ): + + assert all(module in model_files for module in ["image_encoder", "decoder"]) + + self.model = {} + model_parameters = {"decoder": {"input_layouts": "image_embeddings:NCHW"}} + self.configuration = { + "image_encoder": { + **attr.asdict( + hparams.postprocessing, + filter=lambda attr, value: attr.name in ["image_size", "resize_type", "downsizing"], + ) + }, + "decoder": { + **attr.asdict( + hparams.postprocessing, + filter=lambda attr, value: attr.name + not in [ + "header", + "description", + "type", + "visible_in_ui", + "class_name", + "downsizing", + ], + ) + }, + } + for name in ["image_encoder", "decoder"]: + model_adapter = OpenvinoAdapter( + core=create_core(), + model=model_files.get(name), + weights_path=weight_files.get(name, None), + model_parameters=model_parameters.get(name, {}), + device=device, + max_num_requests=num_requests, + plugin_config={"PERFORMANCE_HINT": "THROUGHPUT"}, + ) + self.model[name] = Model.create_model(model_adapter, name, self.configuration.get(name, {}), preload=True) + self.converter = VisualPromptingToAnnotationConverter() + self.labels = label_schema.get_labels(include_empty=False) + self.transform = get_transform() # TODO (sungchul): insert args + + def pre_process( + self, + dataset_item: DatasetItemEntity, + extra_processing: bool = False, + use_bbox: bool = False, + use_point: bool = False, + ) -> Tuple[Dict[str, Any], Dict[str, Any], List[Dict[str, Any]]]: + """Pre-process function of OpenVINO Visual Prompting Inferencer for image encoder.""" + if use_bbox and use_point: + logger.warning("If both use_bbox and use_point are set, bboxes and points will be generated randomly.") + + prob = 1.0 if not use_point else 0.0 if not use_bbox and use_point else 0.5 + images, meta = self.model["image_encoder"].preprocess(dataset_item.numpy, extra_processing) + prompts = OTXVisualPromptingDataset.get_prompts(dataset_item, self.labels, prob=prob) + prompts = self.model["decoder"].preprocess(prompts, meta) + return images, meta, prompts # type: ignore + + def post_process( + self, prediction: Dict[str, np.ndarray], metadata: Dict[str, Any] + ) -> Tuple[List[Annotation], Any, Any]: + """Post-process function of OpenVINO Visual Prompting Inferencer.""" + hard_prediction, soft_prediction = self.model["decoder"].postprocess(prediction, metadata) + annotation = self.converter.convert_to_annotation(hard_prediction, metadata) + return annotation, hard_prediction, soft_prediction + + def predict(self, dataset_item: DatasetItemEntity) -> List[Annotation]: # type: ignore + """Perform a prediction for a given input image.""" + # forward image encoder + images, meta, prompts = self.pre_process(dataset_item) + image_embeddings = self.forward_image_encoder(images) + + annotations: List[Annotation] = [] + hard_predictions: List[np.ndarray] = [] + soft_predictions: List[np.ndarray] = [] + for prompt in prompts: + label = prompt.pop("label") + prompt.update(image_embeddings) + + # forward decoder to get predicted mask + prediction = self.forward_decoder(prompt) + prediction["scores"] = prediction["iou_predictions"] + metadata = {"label": label} + + # set annotation for eval + annotation, hard_prediction, soft_prediction = self.post_process(prediction, metadata) + annotations.extend(annotation) + hard_predictions.append(hard_prediction) + soft_predictions.append(soft_prediction) + return annotations + + def forward_image_encoder(self, inputs: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]: + """Forward function of OpenVINO Visual Prompting Inferencer.""" + return self.model["image_encoder"].infer_sync(inputs) + + def forward_decoder(self, inputs: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]: + """Forward function of OpenVINO Visual Prompting Inferencer.""" + return self.model["decoder"].infer_sync(inputs) + + def await_all(self) -> None: + """Await all running infer requests if any.""" + self.model["image_encoder"].await_all() + self.model["decoder"].await_all() + + +class OpenVINOZeroShotVisualPromptingInferencer(OpenVINOVisualPromptingInferencer): + """Inferencer implementation for Zero-shot Visual Prompting using OpenVINO backend. + + This inferencer has two models, image encoder and decoder. + + Args: + hparams (VisualPromptingBaseConfig): Hyper parameters that the model should use. + label_schema (LabelSchemaEntity): LabelSchemaEntity that was used during model training. + model_files (Dict[str, Union[str, Path, bytes]]): Path or bytes to model to load, + `.xml`, `.bin` or `.onnx` file. + weight_files (Dict[str, Union[str, Path, bytes, None]], optional): Path or bytes to weights to load, + `.xml`, `.bin` or `.onnx` file. Defaults to None. + device (str): Device to run inference on, such as CPU, GPU or MYRIAD. Defaults to "CPU". + num_requests (int) : Maximum number of requests that the inferencer can make. + Good value is the number of available cores. Defaults to 1. + """ + + def __init__( + self, + hparams: VisualPromptingBaseConfig, + label_schema: LabelSchemaEntity, + model_files: Dict[str, Union[str, Path, bytes]], + weight_files: Optional[Dict[str, Union[str, Path, bytes, None]]] = {}, + device: str = "CPU", + num_requests: int = 1, + ): + super().__init__(hparams, label_schema, model_files, weight_files, device, num_requests) + + self.point_labels_box = np.array([[2, 3]], dtype=np.float32) + self.has_mask_inputs = [np.array([[0.0]]), np.array([[1.0]])] + + self.reference_feats: Optional[np.ndarray] = None + self.used_indices: Optional[np.ndarray] = None + + def pre_process_image_encoder( + self, inputs: np.ndarray, extra_processing: bool = False + ) -> Tuple[Dict[str, np.ndarray], Dict[str, Any]]: + """Pre-process function of OpenVINO Zero-shot Visual Prompting Inferencer for image encoder.""" + return self.model["image_encoder"].preprocess(inputs, extra_processing) + + def learn( + self, + dataset_item: DatasetItemEntity, + reset_feat: bool = False, + use_bbox: bool = False, + use_point: bool = False, + path_reference_info: str = "vpm_zsl_reference_infos/{}/reference_info.pickle", + default_threshold_reference: float = 0.3, + ) -> Tuple[Dict[str, np.ndarray], np.ndarray]: + """Learn for reference features.""" + ref_masks: np.ndarray + + if reset_feat or self.reference_feats is None: + self.initialize_reference_info() + + images, meta, prompts = self.pre_process(dataset_item, use_bbox, use_point) + largest_label: int = max([int(p["label"].id) for p in prompts]) + self.expand_reference_info(largest_label) + + image_embeddings = self.forward_image_encoder(images) + processed_embedding = image_embeddings["image_embeddings"].squeeze().transpose(1, 2, 0) + original_size = meta["original_shape"][:2] + + ref_masks = np.zeros((largest_label + 1, *map(int, original_size)), dtype=np.uint8) + for prompt in prompts: + if "point_coords" in prompt: + # bboxes and points + label = prompt.pop("label") + original_size = prompt.get("orig_size") + prompt.update(image_embeddings) + + prediction = self.forward_decoder(prompt, original_size, is_cascade=False) + ref_mask = prediction["upscaled_masks"] + else: + logger.warning("annotation and polygon will be supported.") + continue + ref_masks[int(label.id)] += ref_mask + + ref_masks = np.clip(ref_masks, 0, 1) + for label in range(largest_label + 1): + ref_mask = ref_masks[label] + if ref_mask.sum() == 0: + # empty prediction + continue + + ref_feat = None + cur_default_threshold_reference = deepcopy(default_threshold_reference) + while ref_feat is None: + logger.info(f"[*] default_threshold_reference : {cur_default_threshold_reference:.4f}") + ref_feat = self._generate_masked_features( + processed_embedding, ref_masks[label], cur_default_threshold_reference + ) + cur_default_threshold_reference -= 0.05 + + self.reference_feats[label] = ref_feat + self.used_indices = np.concatenate((self.used_indices, np.array([label]))) + + reference_info = {"reference_feats": self.reference_feats, "used_indices": self.used_indices} + path_reference_info = path_reference_info.format(time.strftime("%Y%m%d-%H%M%S")) + logger.info(f"Saved reference info at {path_reference_info}.") + pickle.dump(reference_info, open(path_reference_info, "wb")) + return reference_info, ref_masks + + def infer( + self, + images: np.ndarray, + reference_feats: np.ndarray, + used_indices: np.ndarray, + is_cascade: bool = False, + threshold: float = 0.0, + num_bg_points: int = 1, + default_threshold_target: float = 0.65, + ) -> Tuple[List[Any], DefaultDict[Any, Any], DefaultDict[Any, Any]]: + """Perform a prediction for a given input image.""" + points_score: np.ndarray + + # forward image encoder + images, meta = self.pre_process_image_encoder(images) + original_shape = np.asarray(meta["original_shape"][:2], dtype=np.int64) + image_embeddings = self.forward_image_encoder(images) + + # get point candidates + total_points_scores, total_bg_coords = self._get_prompt_candidates( + image_embeddings=image_embeddings["image_embeddings"], + reference_feats=reference_feats, + used_indices=used_indices, + original_shape=original_shape, + threshold=threshold, + num_bg_points=num_bg_points, + default_threshold_target=default_threshold_target, + image_size=self.model["image_encoder"].image_size, + downsizing=self.model["image_encoder"].downsizing, + ) + + annotations: DefaultDict = defaultdict(list) + predicted_masks: DefaultDict = defaultdict(list) + used_points: DefaultDict = defaultdict(list) + for label in total_points_scores.keys(): + points_scores = total_points_scores[label] + bg_coords = total_bg_coords[label] + for points_score in points_scores: + if points_score[-1] in [-1.0, 0.0]: + continue + + x, y = points_score[:2] + is_done = False + for pm in predicted_masks.get(label, []): + # check if that point is already assigned + if pm[int(y), int(x)] > 0: + is_done = True + break + if is_done: + continue + + point_coords = np.concatenate((np.array([[x, y]]), bg_coords), axis=0, dtype=np.float32) + point_coords = self.model["decoder"]._apply_coords(point_coords, original_shape) + point_labels = np.array([1] + [0] * len(bg_coords), dtype=np.float32) + inputs_decoder = { + "point_coords": point_coords[None], + "point_labels": point_labels[None], + "orig_size": original_shape[None], + } + inputs_decoder.update(image_embeddings) + + prediction = self.forward_decoder(inputs_decoder, original_shape, is_cascade) + prediction.update({"scores": points_score[-1]}) + + predicted_masks[label].append(prediction[self.model["decoder"].output_blob_name]) + used_points[label].append(points_score) + + self._inspect_overlapping_areas(predicted_masks, used_points) + + for label, predictions in predicted_masks.items(): + if len(predictions) == 0: + continue + metadata = { + "label": [_label for _label in self.labels if int(_label.id_) == label][0], + "original_size": original_shape, + } + for prediction, used_point in zip(predictions, used_points[label]): + annotation, _, _ = self.post_process( + {self.model["decoder"].output_blob_name: prediction, "scores": used_point[-1]}, metadata + ) + annotations[label].extend(annotation) + + return sum(annotations.values(), []), predicted_masks, used_points + + def forward_decoder( # type: ignore + self, + inputs: Dict[str, np.ndarray], + original_size: np.ndarray, + is_cascade: bool = True, + ) -> Dict[str, np.ndarray]: + """Forward function of OpenVINO Visual Prompting Inferencer.""" + masks: np.ndarray + logits: np.ndarray + scores: np.ndarray + num_iter = 3 if is_cascade else 1 + for i in range(num_iter): + if i == 0: + # First-step prediction + mask_input = np.zeros( + (1, 1, *map(lambda x: x * 4, inputs["image_embeddings"].shape[2:])), dtype=np.float32 + ) + has_mask_input = self.has_mask_inputs[0] + + elif i == 1: + # Cascaded Post-refinement-1 + mask_input, masks = self._postprocess_masks(masks, logits, scores, is_single=True) # noqa: F821 + if masks.sum() == 0: + return {"upscaled_masks": masks} + + has_mask_input = self.has_mask_inputs[1] + + elif i == 2: + # Cascaded Post-refinement-2 + mask_input, masks = self._postprocess_masks(masks, logits, scores) # noqa: F821 + if masks.sum() == 0: + return {"upscaled_masks": masks} + + has_mask_input = self.has_mask_inputs[1] + y, x = np.nonzero(masks) + box_coords = self.model["decoder"]._apply_coords( + np.array([[[x.min(), y.min()], [x.max(), y.max()]]], dtype=np.float32), original_size + ) + inputs.update( + { + "point_coords": np.concatenate((inputs["point_coords"], box_coords), axis=1), + "point_labels": np.concatenate((inputs["point_labels"], self.point_labels_box), axis=1), + } + ) + + inputs.update({"mask_input": mask_input, "has_mask_input": has_mask_input}) + prediction = self.model["decoder"].infer_sync(inputs) + upscaled_masks, scores, logits = ( + prediction["upscaled_masks"], + prediction["iou_predictions"], + prediction["low_res_masks"], + ) + masks = upscaled_masks > self.model["decoder"].mask_threshold + + _, masks = self._postprocess_masks(masks, logits, scores) + return {"upscaled_masks": masks} + + def _get_prompt_candidates( + self, + image_embeddings: np.ndarray, + reference_feats: np.ndarray, + used_indices: np.ndarray, + original_shape: np.ndarray, + threshold: float = 0.0, + num_bg_points: int = 1, + default_threshold_target: float = 0.65, + image_size: int = 1024, + downsizing: int = 64, + ) -> Tuple[Dict[int, np.ndarray], Dict[int, np.ndarray]]: + """Get prompt candidates.""" + target_feat = image_embeddings.squeeze() + c_feat, h_feat, w_feat = target_feat.shape + target_feat = target_feat / np.linalg.norm(target_feat, axis=0, keepdims=True) + target_feat = target_feat.reshape(c_feat, h_feat * w_feat) + + total_points_scores: Dict[int, np.ndarray] = {} + total_bg_coords: Dict[int, np.ndarray] = {} + for label in used_indices: + sim = reference_feats[label] @ target_feat + sim = sim.reshape(h_feat, w_feat) + sim = self._resize_to_original_shape(sim, image_size, original_shape) + + threshold = (threshold == 0) * default_threshold_target + threshold + points_scores, bg_coords = self._point_selection( + mask_sim=sim, + original_shape=original_shape, + threshold=threshold, + num_bg_points=num_bg_points, + image_size=image_size, + downsizing=downsizing, + ) + + if points_scores is not None: + total_points_scores[label] = points_scores + total_bg_coords[label] = bg_coords + return total_points_scores, total_bg_coords + + def _point_selection( + self, + mask_sim: np.ndarray, + original_shape: np.ndarray, + threshold: float = 0.0, + num_bg_points: int = 1, + image_size: int = 1024, + downsizing: int = 64, + ) -> Tuple[np.ndarray, np.ndarray]: + """Select point used as point prompts.""" + _, w_sim = mask_sim.shape + + # Top-first point selection + point_coords = np.where(mask_sim > threshold) + fg_coords_scores = np.stack(point_coords[::-1] + (mask_sim[point_coords],), axis=0).T + + ## skip if there is no point coords + if len(fg_coords_scores) == 0: + return None, None + + ratio = image_size / original_shape.max() + width = (original_shape[1] * ratio).astype(np.int64) + n_w = width // downsizing + + ## get grid numbers + idx_grid = fg_coords_scores[:, 1] * ratio // downsizing * n_w + fg_coords_scores[:, 0] * ratio // downsizing + idx_grid_unique = np.unique(idx_grid.astype(np.int64)) + + ## get matched indices + matched_matrix = np.expand_dims(idx_grid, axis=-1) == idx_grid_unique # (totalN, uniqueN) + + ## sample fg_coords_scores matched by matched_matrix + matched_grid = np.expand_dims(fg_coords_scores, axis=1) * np.expand_dims(matched_matrix, axis=-1) + + ## sample the highest score one of the samples that are in the same grid + matched_indices = self._topk_numpy(matched_grid[..., -1], k=1, axis=0, largest=True)[1][0].astype(np.int64) + points_scores = matched_grid[matched_indices].diagonal().T + + ## sort by the highest score + sorted_points_scores_indices = np.flip(np.argsort(points_scores[:, -1]), axis=-1).astype(np.int64) + points_scores = points_scores[sorted_points_scores_indices] + + # Top-last point selection + bg_indices = self._topk_numpy(mask_sim.flatten(), num_bg_points, largest=False)[1] + bg_x = np.expand_dims(bg_indices // w_sim, axis=0) + bg_y = bg_indices - bg_x * w_sim + bg_coords = np.concatenate((bg_y, bg_x), axis=0).transpose(1, 0) + bg_coords = bg_coords.astype(np.float32) + + return points_scores, bg_coords + + def _postprocess_masks( + self, masks: np.ndarray, logits: np.ndarray, scores: np.ndarray, is_single: bool = False + ) -> Tuple[np.ndarray, ...]: + """Post-process logits for resized masks according to best index based on scores.""" + if is_single: + best_idx = 0 + else: + # skip the first index components + scores, masks, logits = map(lambda x: x[:, 1:], (scores, masks, logits)) + + # filter zero masks + while len(scores[0]) > 0 and masks[0, (best_idx := np.argmax(scores[0]))].sum() == 0: + scores, masks, logits = map( + lambda x: np.concatenate((x[:, :best_idx], x[:, best_idx + 1 :]), axis=1), (scores, masks, logits) + ) + + if len(scores[0]) == 0: + # all predicted masks were zero masks, ignore them. + return None, np.zeros(masks.shape[-2:]) + + best_idx = np.argmax(scores[0]) + return logits[:, [best_idx]], masks[0, best_idx] + + def _resize_to_original_shape(self, masks: np.ndarray, image_size: int, original_shape: np.ndarray) -> np.ndarray: + """Resize feature size to original shape.""" + # resize feature size to input size + masks = cv2.resize(masks, (image_size, image_size), interpolation=cv2.INTER_LINEAR) + + # remove pad + prepadded_size = self._get_prepadded_size(original_shape, image_size) + masks = masks[..., : prepadded_size[0], : prepadded_size[1]] + + # resize unpadded one to original shape + original_shape = original_shape.astype(np.int64) + h, w = original_shape[0], original_shape[1] + return cv2.resize(masks, (w, h), interpolation=cv2.INTER_LINEAR) + + def _get_prepadded_size(self, original_shape: int, image_size: int) -> np.ndarray: + """Get pre-padded size.""" + scale = image_size / np.max(original_shape) + transformed_size = scale * original_shape + return np.floor(transformed_size + 0.5).astype(np.int64) + + def _inspect_overlapping_areas( + self, + predicted_masks: Dict[int, List[np.ndarray]], + used_points: Dict[int, List[np.ndarray]], + threshold_iou: float = 0.8, + ): + def _calculate_mask_iou(mask1: np.ndarray, mask2: np.ndarray): + assert mask1.ndim == 2 and mask2.ndim == 2 + intersection = np.logical_and(mask1, mask2).sum().item() + union = np.logical_or(mask1, mask2).sum().item() + + # Avoid division by zero + if union == 0: + return 0.0 + iou = intersection / union + return iou + + for (label, masks), (other_label, other_masks) in product(predicted_masks.items(), predicted_masks.items()): + if other_label <= label: + continue + + overlapped_label = [] + overlapped_other_label = [] + for (im, mask), (jm, other_mask) in product(enumerate(masks), enumerate(other_masks)): + _mask_iou = _calculate_mask_iou(mask, other_mask) + if _mask_iou > threshold_iou: + if used_points[label][im][2] > used_points[other_label][jm][2]: + overlapped_other_label.append(jm) + else: + overlapped_label.append(im) + elif _mask_iou > 0: + # refine the slightly overlapping region + overlapped_coords = np.where(np.logical_and(mask, other_mask)) + if used_points[label][im][2] > used_points[other_label][jm][2]: + other_mask[overlapped_coords] = 0.0 + else: + mask[overlapped_coords] = 0.0 + + for im in sorted(list(set(overlapped_label)), reverse=True): + masks.pop(im) + used_points[label].pop(im) + + for jm in sorted(list(set(overlapped_other_label)), reverse=True): + other_masks.pop(jm) + used_points[other_label].pop(jm) + + def predict(self, dataset_item: DatasetItemEntity) -> List[Annotation]: # type: ignore + """Perform a prediction for a given input image.""" + results = self.infer(dataset_item.numpy, self.reference_feats, self.used_indices) + return results[0] + + def _find_latest_reference_info(self, root: str = "vpm_zsl_reference_infos") -> Union[str, None]: + """Find latest reference info to be used.""" + if not os.path.isdir(root): + return None + if len(stamps := sorted(os.listdir(root), reverse=True)) > 0: + return stamps[0] + return None + + def _get_reference_info( + self, root: str = "vpm_zsl_reference_infos", path_reference_info: str = "{}/reference_info.pickle" + ) -> Union[Tuple[np.ndarray, np.ndarray], None]: + """Get reference info through loading previously saved one or running `learn`.""" + if (latest_stamp := self._find_latest_reference_info(root)) is not None: + # load previously saved reference info + latest_reference_info = os.path.join(root, path_reference_info.format(latest_stamp)) + reference_info = pickle.load(open(latest_reference_info, "rb")) + return reference_info["reference_feats"], reference_info["used_indices"] + return None, None + + def initialize_reference_info(self) -> None: + """Initialize reference information.""" + self.reference_feats = np.zeros((0, 1, 256), dtype=np.float32) + self.used_indices = np.array([], dtype=np.int64) + + def expand_reference_info(self, new_largest_label: int) -> None: + """Expand reference info dimensions if newly given processed prompts have more lables.""" + if new_largest_label > (cur_largest_label := len(self.reference_feats) - 1): + diff = new_largest_label - cur_largest_label + self.reference_feats = np.pad(self.reference_feats, ((0, diff), (0, 0), (0, 0)), constant_values=0.0) + + def _generate_masked_features( + self, + feats: np.ndarray, + masks: np.ndarray, + threshold_mask: float, + ) -> Tuple[np.ndarray, ...]: + """Generate masked features. + + Args: + feats (np.ndarray): Raw reference features. It will be filtered with masks. + masks (np.ndarray): Reference masks used to filter features. + threshold_mask (float): Threshold to control masked region. + + Returns: + (np.ndarray): Masked features. + """ + target_shape = self.model["image_encoder"].image_size / max(masks.shape) * np.array(masks.shape) + target_shape = target_shape[::-1].astype(np.int32) + + # Post-process masks + masks = cv2.resize(masks, target_shape, interpolation=cv2.INTER_LINEAR) + masks = self._pad_to_square(masks) + masks = cv2.resize(masks, feats.shape[:2][::-1], interpolation=cv2.INTER_LINEAR) + + # Target feature extraction + if (masks > threshold_mask).sum() == 0: + # (for stability) there is no area to be extracted + return None + + masked_feat = feats[masks > threshold_mask] + masked_feat = masked_feat.mean(0)[None] + masked_feat = masked_feat / np.linalg.norm(masked_feat, axis=-1, keepdims=True) + + return masked_feat + + def _pad_to_square(self, x: np.ndarray) -> np.ndarray: + """Pad to a square input. + + Args: + x (np.ndarray): Mask to be padded. + + Returns: + (np.ndarray): Padded mask. + """ + h, w = x.shape[-2:] + padh = self.model["image_encoder"].image_size - h + padw = self.model["image_encoder"].image_size - w + x = np.pad(x, ((0, padh), (0, padw)), constant_values=0.0) + return x + + def _topk_numpy(self, x: np.ndarray, k: int, axis: int = -1, largest: bool = True) -> np.ndarray: + """Top-k function for numpy same with torch.topk.""" + if largest: + k = -k + indices = range(k, 0) + else: + indices = range(k) + partitioned_ind = np.argpartition(x, k, axis=axis).take(indices=indices, axis=axis) + partitioned_scores = np.take_along_axis(x, partitioned_ind, axis=axis) + sorted_trunc_ind = np.flip(np.argsort(partitioned_scores, axis=axis), axis=axis) + ind = np.take_along_axis(partitioned_ind, sorted_trunc_ind, axis=axis) + scores = np.take_along_axis(partitioned_scores, sorted_trunc_ind, axis=axis) + return scores, ind + + +class OTXOpenVinoDataLoader: + """DataLoader implementation for VisualPromptingOpenVINOTask.""" + + def __init__( + self, + dataset: Any, + inferencer: OpenVINOVisualPromptingInferencer, + module_name: str, + shuffle: bool = True, + output_model: Optional[ModelEntity] = None, + **kwargs, + ): + self.dataset = dataset + self.inferencer = inferencer + self.module_name = module_name + self.shuffler = None + if shuffle: + self.shuffler = list(range(len(dataset))) + random.shuffle(self.shuffler) + + self.target_length = self.inferencer.model["image_encoder"].orig_width + if self.module_name not in ["image_encoder"]: + self.image_encoder = self._load_module("image_encoder", output_model) + + def _load_module(self, module_name: str, output_model: ModelEntity, core=ov.Core()): + """Load specific module.""" + compressed_model = core.read_model( + output_model.get_data(f"visual_prompting_{module_name}.xml"), + output_model.get_data(f"visual_prompting_{module_name}.bin"), + ) + return core.compile_model( + model=compressed_model, device_name=self.inferencer.model[module_name].inference_adapter.device + ) + + def __getitem__(self, index: int): + """Get item from dataset.""" + if self.shuffler is not None: + index = self.shuffler[index] + + items = self.dataset[index] + images, _, prompts = self.inferencer.pre_process(items, extra_processing=True) + _, _, h, w = images["images"].shape + pad_width = ((0, 0), (0, 0), (0, self.target_length - h), (0, self.target_length - w)) + images["images"] = np.pad(images["images"], pad_width, mode="constant", constant_values=0) + if self.module_name == "image_encoder": + return images + else: + image_embeddings = self.image_encoder(images["images"]) + prompt = prompts[0] # only use the first prompt + prompt.pop("label") + prompt.update({"image_embeddings": image_embeddings["image_embeddings"]}) + return prompt + # TODO (sungchul): change has_mask_input + + def __len__(self): + """Get length of dataset.""" + return len(self.dataset) + + +class OpenVINOVisualPromptingTask(IInferenceTask, IEvaluationTask, IOptimizationTask, IDeploymentTask): + """Task implementation for Visual Prompting using OpenVINO backend.""" + + def __init__(self, task_environment: TaskEnvironment) -> None: + self.task_environment = task_environment + self.model = self.task_environment.model + self.model_name = self.task_environment.model_template.model_template_id + self.inferencer = self.load_inferencer() + self._avg_time_per_image: Optional[float] = None + + labels = task_environment.get_labels(include_empty=False) + self._label_dictionary = dict(enumerate(labels, 1)) + template_file_path = self.task_environment.model_template.model_template_path + self._base_dir = os.path.abspath(os.path.dirname(template_file_path)) + self.task_type = TaskType.VISUAL_PROMPTING + + @property + def hparams(self): + """Hparams of OpenVINO Visual Prompting Task.""" + return self.task_environment.get_hyper_parameters(VisualPromptingBaseConfig) + + @property + def avg_time_per_image(self) -> Optional[float]: + """Average inference time per image.""" + return self._avg_time_per_image + + def load_inferencer(self) -> OpenVINOVisualPromptingInferencer: + """Load OpenVINO Visual Prompting Inferencer.""" + if self.model is None: + raise RuntimeError("load_inferencer failed, model is None") + return OpenVINOVisualPromptingInferencer( + self.hparams, + self.task_environment.label_schema, + { + "image_encoder": self.model.get_data("visual_prompting_image_encoder.xml"), + "decoder": self.model.get_data("visual_prompting_decoder.xml"), + }, + { + "image_encoder": self.model.get_data("visual_prompting_image_encoder.bin"), + "decoder": self.model.get_data("visual_prompting_decoder.bin"), + }, + num_requests=get_default_async_reqs_num(), + ) + + def infer( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + ) -> DatasetEntity: + """Infer function of OpenVINOVisualPromptingTask. + + Currently, asynchronous execution is not supported, synchronous execution will be executed instead. + """ + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress + enable_async_inference = inference_parameters.enable_async_inference + else: + update_progress_callback = default_progress_callback + enable_async_inference = True + + # FIXME (sungchul): Support async inference. + if enable_async_inference: + logger.warning("Asynchronous inference doesn't work, synchronous inference will be executed.") + enable_async_inference = False + predicted_validation_dataset = dataset.with_empty_annotations() + + def add_prediction(id: int, annotations: List[Annotation]): + dataset_item = predicted_validation_dataset[id] + dataset_item.append_annotations(annotations) + + total_time = 0.0 + dataset_size = len(dataset) + for i, dataset_item in enumerate(dataset, 1): + start_time = time.perf_counter() + + annotations = self.inferencer.predict(dataset_item) + add_prediction(i - 1, annotations) + + end_time = time.perf_counter() - start_time + total_time += end_time + update_progress_callback(int(i / dataset_size * 100), None) + + self.inferencer.await_all() + + self._avg_time_per_image = total_time / len(dataset) + logger.info(f"Avg time per image: {self._avg_time_per_image} secs") + logger.info(f"Total time: {total_time} secs") + logger.info("Visual Prompting OpenVINO inference completed") + + return predicted_validation_dataset + + def evaluate(self, output_resultset: ResultSetEntity, evaluation_metric: Optional[str] = None): + """Evaluate function of OpenVINOVisualPromptingTask.""" + logger.info("Computing mDice") + metrics = MetricsHelper.compute_dice_averaged_over_pixels(output_resultset) + logger.info(f"mDice after evaluation: {metrics.overall_dice.value}") + + output_resultset.performance = metrics.get_performance() + + def deploy(self, output_model: ModelEntity) -> None: + """Deploy function of OpenVINOVisualPromptingTask.""" + logger.info("Deploying the model") + if self.model is None: + raise RuntimeError("deploy failed, model is None") + + work_dir = os.path.dirname(demo.__file__) + parameters: Dict[str, Any] = {} + parameters["converter_type"] = f"{self.task_type}" + parameters["model_parameters"] = self.inferencer.configuration + parameters["model_parameters"]["labels"] = LabelSchemaMapper.forward(self.task_environment.label_schema) + + zip_buffer = io.BytesIO() + with ZipFile(zip_buffer, "w") as arch: + # model files + arch.writestr( + os.path.join("model", "visual_prompting_image_encoder.xml"), + self.model.get_data("visual_prompting_image_encoder.xml"), + ) + arch.writestr( + os.path.join("model", "visual_prompting_image_encoder.bin"), + self.model.get_data("visual_prompting_image_encoder.bin"), + ) + arch.writestr( + os.path.join("model", "visual_prompting_decoder.xml"), + self.model.get_data("visual_prompting_decoder.xml"), + ) + arch.writestr( + os.path.join("model", "visual_prompting_decoder.bin"), + self.model.get_data("visual_prompting_decoder.bin"), + ) + arch.writestr( + os.path.join("model", "config.json"), + json.dumps(parameters, ensure_ascii=False, indent=4), + ) + # model_wrappers files + for root, _, files in os.walk(os.path.dirname(model_wrappers.__file__)): + if "__pycache__" in root: + continue + for file in files: + file_path = os.path.join(root, file) + arch.write( + file_path, + os.path.join( + "python", + "model_wrappers", + file_path.split("model_wrappers/")[0], + ), + ) + # other python files + arch.write(os.path.join(work_dir, "requirements.txt"), os.path.join("python", "requirements.txt")) + arch.write(os.path.join(work_dir, "LICENSE"), os.path.join("python", "LICENSE")) + arch.write(os.path.join(work_dir, "demo.py"), os.path.join("python", "demo.py")) + arch.write(os.path.join(work_dir, "README.md"), os.path.join(".", "README.md")) + output_model.exportable_code = zip_buffer.getvalue() + logger.info("Deploying completed") + + def optimize( + self, + optimization_type: OptimizationType, + dataset: DatasetEntity, + output_model: ModelEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + module_names: List[str] = ["image_encoder", "decoder"], + ov_dataloader: Type[OTXOpenVinoDataLoader] = OTXOpenVinoDataLoader, + **kwargs, + ): + """Optimize function of OpenVINOVisualPromptingTask.""" + logger.info("Start PTQ optimization") + if self.model is None: + raise RuntimeError("PTQ optimize failed, model is None") + + if optimization_type is not OptimizationType.POT: + raise ValueError("PTQ is the only supported optimization type for OpenVino models") + + dataset = dataset.get_subset(Subset.TRAINING) + + for i, module_name in enumerate(module_names, 1): + data_loader = ov_dataloader( + dataset, self.inferencer, module_name=module_name, output_model=output_model, **kwargs + ) + quantization_dataset = nncf.Dataset(data_loader, lambda data: data) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, f"visual_prompting_{module_name}.xml") + bin_path = os.path.join(tempdir, f"visual_prompting_{module_name}.bin") + with open(xml_path, "wb") as f: + f.write(self.model.get_data(f"visual_prompting_{module_name}.xml")) + with open(bin_path, "wb") as f: + f.write(self.model.get_data(f"visual_prompting_{module_name}.bin")) + + ov_model = ov.Core().read_model(xml_path, bin_path) + if check_if_quantized(ov_model): + raise RuntimeError("Model is already optimized by PTQ") + + optimization_config_path = os.path.join(self._base_dir, "ptq_optimization_config.py") + ptq_config = ADDict() + if os.path.exists(optimization_config_path): + ptq_config = read_py_config(optimization_config_path) + ptq_config.update( + subset_size=min(self.hparams.pot_parameters.stat_subset_size, len(data_loader)), + preset=QuantizationPreset(self.hparams.pot_parameters.preset.name.lower()), + ) + + compressed_model = nncf.quantize(ov_model, quantization_dataset, **ptq_config) + + if optimization_parameters is not None: + optimization_parameters.update_progress(90 // len(module_names) * i, None) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, f"visual_prompting_{module_name}.xml") + bin_path = os.path.join(tempdir, f"visual_prompting_{module_name}.bin") + ov.save_model(compressed_model, xml_path) + with open(xml_path, "rb") as f: + output_model.set_data(f"visual_prompting_{module_name}.xml", f.read()) + with open(bin_path, "rb") as f: + output_model.set_data(f"visual_prompting_{module_name}.bin", f.read()) + + output_model.set_data( + "label_schema.json", + label_schema_to_bytes(self.task_environment.label_schema), + ) + + # set model attributes for quantized model + output_model.model_format = ModelFormat.OPENVINO + output_model.optimization_type = ModelOptimizationType.POT + output_model.optimization_methods = [OptimizationMethod.QUANTIZATION] + output_model.precision = [ModelPrecision.INT8] + + self.model = output_model + self.inferencer = self.load_inferencer() + + if optimization_parameters is not None: + optimization_parameters.update_progress(100, None) + logger.info("PTQ optimization completed") + + +class OpenVINOZeroShotVisualPromptingTask(OpenVINOVisualPromptingTask): + """Task implementation for Zero-shot Visual Prompting using OpenVINO backend.""" + + def load_inferencer(self) -> OpenVINOZeroShotVisualPromptingInferencer: + """Load OpenVINO Zero-shot Visual Prompting Inferencer.""" + if self.model is None: + raise RuntimeError("load_inferencer failed, model is None") + return OpenVINOZeroShotVisualPromptingInferencer( + self.hparams, + self.task_environment.label_schema, + model_files={ + "image_encoder": self.model.get_data("visual_prompting_image_encoder.xml"), + "decoder": self.model.get_data("visual_prompting_decoder.xml"), + }, + weight_files={ + "image_encoder": self.model.get_data("visual_prompting_image_encoder.bin"), + "decoder": self.model.get_data("visual_prompting_decoder.bin"), + }, + num_requests=get_default_async_reqs_num(), + ) + + def infer( + self, + dataset: DatasetEntity, + inference_parameters: Optional[InferenceParameters] = None, + root: str = "vpm_zsl_reference_infos", + path_reference_info: str = "{}/reference_info.pickle", + ) -> DatasetEntity: + """Infer function of OpenVINOVisualPromptingTask. + + Currently, asynchronous execution is not supported, synchronous execution will be executed instead. + """ + if inference_parameters is not None: + update_progress_callback = inference_parameters.update_progress + enable_async_inference = inference_parameters.enable_async_inference + else: + update_progress_callback = default_progress_callback + enable_async_inference = True + + # FIXME (sungchul): Support async inference. + if enable_async_inference: + logger.warning("Asynchronous inference doesn't work, synchronous inference will be executed.") + enable_async_inference = False + predicted_validation_dataset = dataset.with_empty_annotations() + + def add_prediction(id: int, annotations: List[Annotation]): + dataset_item = predicted_validation_dataset[id] + dataset_item.append_annotations(annotations) + + total_time = 0.0 + dataset_size = len(dataset) + + if self.inferencer.reference_feats is None and self.inferencer.used_indices is None: + # set reference_feats and used_indices from previously saved reference_info + self.inferencer.reference_feats, self.inferencer.used_indices = self.inferencer._get_reference_info( + root, path_reference_info + ) + if self.inferencer.reference_feats is None and self.inferencer.used_indices is None: + # if they are empty, stop inference and return empty dataset + logger.warning( + ( + "reference_feats and used_indices are empty, stop inference and return empty dataset. " + "Please run learn function first." + ) + ) + return predicted_validation_dataset + + for i, dataset_item in enumerate(dataset, 1): + start_time = time.perf_counter() + + annotations = self.inferencer.predict(dataset_item) + add_prediction(i - 1, annotations) + + end_time = time.perf_counter() - start_time + total_time += end_time + update_progress_callback(int(i / dataset_size * 100), None) + + self.inferencer.await_all() + + self._avg_time_per_image = total_time / len(dataset) + logger.info(f"Avg time per image: {self._avg_time_per_image} secs") + logger.info(f"Total time: {total_time} secs") + logger.info("Visual Prompting OpenVINO inference completed") + + return predicted_validation_dataset + + def optimize( + self, + optimization_type: OptimizationType, + dataset: DatasetEntity, + output_model: ModelEntity, + optimization_parameters: Optional[OptimizationParameters] = None, + module_names: List[str] = ["image_encoder", "decoder"], + ov_dataloader: Type[OTXOpenVinoDataLoader] = OTXOpenVinoDataLoader, + **kwargs, + ): + """Optimize function of OpenVINOZeroShotVisualPromptingTask.""" + self.inferencer: OpenVINOZeroShotVisualPromptingInferencer + reference_feats, used_indices = self.inferencer._get_reference_info() + return super().optimize( + optimization_type=optimization_type, + dataset=dataset, + output_model=output_model, + optimization_parameters=optimization_parameters, + module_names=module_names, + ov_dataloader=ov_dataloader, + reference_feats=reference_feats, + used_indices=used_indices, + ) diff --git a/src/otx/algorithms/visual_prompting/tasks/train.py b/src/otx/algorithms/visual_prompting/tasks/train.py new file mode 100644 index 00000000000..fc2b5311d2d --- /dev/null +++ b/src/otx/algorithms/visual_prompting/tasks/train.py @@ -0,0 +1,110 @@ +"""Visual Prompting Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from typing import Optional + +from pytorch_lightning import Trainer +from pytorch_lightning.callbacks import ( + EarlyStopping, + LearningRateMonitor, + ModelCheckpoint, + TQDMProgressBar, +) +from pytorch_lightning.loggers import CSVLogger + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets import ( + OTXVisualPromptingDataModule, +) +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.metrics import Performance, ScoreMetric +from otx.api.entities.model import ModelEntity +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.tasks.interfaces.training_interface import ITrainingTask +from otx.utils.logger import get_logger + +from .inference import InferenceTask + +logger = get_logger() + + +class TrainingTask(InferenceTask, ITrainingTask): + """Training Task for Visual Prompting. + + Args: + dataset (DatasetEntity): Input dataset. + output_model (ModelEntity): Output model to save the model weights. + train_parameters (TrainParameters): Training parameters + seed (Optional[int]): Setting seed to a value other than 0 + deterministic (bool): Setting PytorchLightning trainer's deterministic flag. + """ + + def train( # noqa: D102 + self, + dataset: DatasetEntity, + output_model: ModelEntity, + train_parameters: TrainParameters, + seed: Optional[int] = None, + deterministic: bool = False, + ) -> None: + + logger.info("Training the model.") + + self.seed = seed + self.deterministic = deterministic + self.set_seed() + self.config.trainer.deterministic = "warn" if deterministic else deterministic + + logger.info("Training Configs '%s'", self.config) + + self.model = self.load_model(otx_model=self.task_environment.model) + + datamodule = OTXVisualPromptingDataModule( + config=self.config.dataset, dataset=dataset, train_type=self.train_type + ) + loggers = CSVLogger(save_dir=self.output_path, name=".", version=self.timestamp) + callbacks = [ + TQDMProgressBar(), + ModelCheckpoint(dirpath=loggers.log_dir, filename="{epoch:02d}", **self.config.callback.checkpoint), + LearningRateMonitor(), + EarlyStopping(**self.config.callback.early_stopping), + ] + + self.trainer = Trainer(**self.config.trainer, logger=loggers, callbacks=callbacks) + self.trainer.fit(model=self.model, datamodule=datamodule) + + model_ckpt = self.trainer.checkpoint_callback.best_model_path + if not model_ckpt: + logger.error("cannot find final checkpoint from the results.") + return + # update checkpoint to the newly trained model + self._model_ckpt = model_ckpt + + # compose performance statistics + best_score = self.trainer.checkpoint_callback.best_model_score + if best_score is None: + results = self.trainer.validate(model=self.model, datamodule=datamodule) + best_score = results[0].get(self.config.callback.checkpoint.monitor) + + # save resulting model + self.save_model(output_model) + performance = Performance( + score=ScoreMetric(value=best_score, name=self.trainer.checkpoint_callback.monitor) + # TODO (sungchul): dashboard? -> only for Geti + ) + logger.info(f"Final model performance: {str(performance)}") + output_model.performance = performance + + logger.info("train done.") diff --git a/src/otx/algorithms/visual_prompting/utils/__init__.py b/src/otx/algorithms/visual_prompting/utils/__init__.py new file mode 100644 index 00000000000..8d1bc210803 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/utils/__init__.py @@ -0,0 +1,19 @@ +"""Collection of utils for task implementation in Visual Prompting Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .visual_prompting_utils import get_visual_prompting_inferencer_configuration + +__all__ = ["get_visual_prompting_inferencer_configuration"] diff --git a/src/otx/algorithms/visual_prompting/utils/visual_prompting_utils.py b/src/otx/algorithms/visual_prompting/utils/visual_prompting_utils.py new file mode 100644 index 00000000000..ea61398b155 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/utils/visual_prompting_utils.py @@ -0,0 +1,22 @@ +"""Collection of utils about labels in Visual Prompting Task.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from otx.api.entities.label_schema import LabelSchemaEntity + + +def get_visual_prompting_inferencer_configuration(label_schema: LabelSchemaEntity): + """Get visual prompting inferencer config by label schema.""" + return {} diff --git a/src/otx/api/__init__.py b/src/otx/api/__init__.py new file mode 100644 index 00000000000..86117aeb18b --- /dev/null +++ b/src/otx/api/__init__.py @@ -0,0 +1,4 @@ +"""OpenVINO Training Extensions API.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/api/configuration/__init__.py b/src/otx/api/configuration/__init__.py new file mode 100644 index 00000000000..e706fd27c4a --- /dev/null +++ b/src/otx/api/configuration/__init__.py @@ -0,0 +1,36 @@ +"""OTX configurable parameters and helper utilities. + +This module contains base elements that make up OTX ConfigurableParameters, as well as a collection of helper +functions to interact with them. + +The configuration helper module can be imported as `otx_config_helper` and implements the following: +""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import otx.api.configuration.helper as otx_config_helper # for backward compatibility +import otx.api.configuration.helper as cfg_helper # pylint: disable=reimported +from otx.api.configuration.elements import metadata_keys +from otx.api.configuration.elements.configurable_enum import ConfigurableEnum +from otx.api.configuration.enums.model_lifecycle import ModelLifecycle +from otx.api.configuration.ui_rules import Action, NullUIRules, Operator, Rule, UIRules + +from .configurable_parameters import ConfigurableParameters +from .default_model_parameters import DefaultModelParameters + +__all__ = [ + "metadata_keys", + "cfg_helper", + "otx_config_helper", + "ConfigurableEnum", + "ModelLifecycle", + "Action", + "NullUIRules", + "Operator", + "Rule", + "UIRules", + "DefaultModelParameters", + "ConfigurableParameters", +] diff --git a/src/otx/api/configuration/configurable_parameters.py b/src/otx/api/configuration/configurable_parameters.py new file mode 100644 index 00000000000..9bc510bf72b --- /dev/null +++ b/src/otx/api/configuration/configurable_parameters.py @@ -0,0 +1,32 @@ +"""This module contains the base class to define ConfigurableParameters within OTX.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from attr import attrib, attrs, setters + +from otx.api.entities.id import ID + +from .elements.parameter_group import ParameterGroup +from .elements.utils import convert_string_to_id +from .enums.config_element_type import ConfigElementType + + +@attrs(auto_attribs=True, order=False, eq=False) +class ConfigurableParameters(ParameterGroup): + """Base class representing a generic set of configurable parameters. + + A ConfigurableParameters instance is essentially a parameter group with an id + attached to it, so that it can be uniquely identified in the repositories. + + Attributes: + id (ID): ID that uniquely identifies the ConfigurableParameters + type (ConfigElementType): Type of the ConfigurableParameters + """ + + id: ID = attrib(default=ID(), kw_only=True, converter=convert_string_to_id) + type: ConfigElementType = attrib( + default=ConfigElementType.CONFIGURABLE_PARAMETERS, + repr=False, + init=False, + on_setattr=setters.frozen, + ) diff --git a/src/otx/api/configuration/default_model_parameters.py b/src/otx/api/configuration/default_model_parameters.py new file mode 100644 index 00000000000..c674ef0ccf7 --- /dev/null +++ b/src/otx/api/configuration/default_model_parameters.py @@ -0,0 +1,76 @@ +"""This module contains a default set of configurable parameters for a model.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from attr import attrib, attrs + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.elements import ( + ParameterGroup, + add_parameter_group, + boolean_attribute, + configurable_float, + configurable_integer, + string_attribute, +) +from otx.api.configuration.enums.model_lifecycle import ModelLifecycle + + +@attrs +class DefaultModelParameters(ConfigurableParameters): + """Configuration element representing a the default set of hyper parameters for a model. + + Attributes: + header (str): Name of parameter group + description (str): User friendly string describing what the ModelConfig represents, that will be displayed in + the UI. + """ + + header: str = attrib(default="Default model hyper parameters") + description: str = attrib(default="Default model hyper parameter section description", kw_only=True) + + @attrs + class _LearningParameters(ParameterGroup): + # Set defaults for the learning parameters. Learning parameters consist of at + # least batch_size, epochs and learning_rate. These correspond to the 'basic' + # hyper parameters in model template + header = string_attribute("Learning Parameters") + description = string_attribute("Parameters to control basic training behavior.") + visible_in_ui = boolean_attribute(True) + + batch_size = configurable_integer( + header="Batch size", + description="The number of training samples seen in each " + "iteration of training. Setting this higher will " + "make the training more stable, but will require " + "more memory. Setting this lower will make the " + "training less stable, but will require less " + "memory.", + warning="Increasing this value may cause the system to use " + "more memory than available, potentially causing out " + "of memory errors, please update with caution.", + min_value=1, + max_value=1000, + default_value=4, + affects_outcome_of=ModelLifecycle.TRAINING, + ) + epochs = configurable_integer( + header="Number of epochs", + default_value=10, + min_value=1, + max_value=10000, + description="Increasing this value causes the results to be more " + "robust but training time will be longer.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + learning_rate = configurable_float( + header="Learning rate", + default_value=1e-3, + min_value=1e-30, + max_value=1e10, + description="Increasing this value will speed up training " "convergence but might make it unstable.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + learning_parameters = add_parameter_group(_LearningParameters) diff --git a/src/otx/api/configuration/elements/__init__.py b/src/otx/api/configuration/elements/__init__.py new file mode 100644 index 00000000000..966abad167f --- /dev/null +++ b/src/otx/api/configuration/elements/__init__.py @@ -0,0 +1,30 @@ +"""This module contains all elements needed to construct a OTX configuration object.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from .configurable_enum import ConfigurableEnum +from .parameter_group import ParameterGroup, add_parameter_group +from .primitive_parameters import ( + boolean_attribute, + configurable_boolean, + configurable_float, + configurable_integer, + float_selectable, + selectable, + string_attribute, +) + +__all__ = [ + "ConfigurableEnum", + "ParameterGroup", + "add_parameter_group", + "boolean_attribute", + "configurable_boolean", + "configurable_float", + "configurable_integer", + "float_selectable", + "selectable", + "string_attribute", +] diff --git a/src/otx/api/configuration/elements/configurable_enum.py b/src/otx/api/configuration/elements/configurable_enum.py new file mode 100644 index 00000000000..2b68161c2c8 --- /dev/null +++ b/src/otx/api/configuration/elements/configurable_enum.py @@ -0,0 +1,60 @@ +"""This module contains the ConfigurableEnum, that is used to define Enums which can be configured by the user.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from enum import Enum +from typing import List + +from .metadata_keys import ENUM_NAME, OPTIONS + + +class ConfigurableEnum(Enum): + """This class is used as the basis for defining `selectable` configurable parameters in the OTX API. + + Enums reflecting `selectable` options should inherit from thisclass. + """ + + def __str__(self): + """Retrieves the string representation of an instance of the ConfigurableEnum (or subclasses thereof).""" + return self.value + + def __eq__(self, other) -> bool: + """Returns True if the ConfigurableEnum instance is equal to the other ConfigurableEnum instance. + + Checks whether one ConfigurableEnum instance (or instance of a subclass thereof) is equal to the `other` + object. Comparison is made based on class name, instance value and instance name. + + Returns: + bool: True if the two instances are equal, False otherwise. + """ + if isinstance(other, ConfigurableEnum) and self.__class__.__name__ == other.__class__.__name__: + return self.value == other.value and self.name == other.name + return False + + def __hash__(self): + """Computes hash for the ConfigurableEnum instance.""" + return hash(self.name) + + @classmethod + def get_class_info(cls) -> dict: + """Creates a dictionary representation of the ConfigurableEnum. + + This includes the name of the enum and the (name, value) pairs representing its members. + + Returns: + dict: Dictionary representation of the ConfigurableEnum. + """ + options_dict = {name: instance.value for name, instance in cls.__members__.items()} + + return {ENUM_NAME: cls.__name__, OPTIONS: options_dict} + + @classmethod + def get_names(cls) -> List[str]: + """Returns a list of names that can be used to index the Enum.""" + return [x.name for x in cls] + + @classmethod + def get_values(cls) -> List[str]: + """Returns a list of values that can be used to index the Enum.""" + return [x.value for x in cls] diff --git a/src/otx/api/configuration/elements/metadata_keys.py b/src/otx/api/configuration/elements/metadata_keys.py new file mode 100644 index 00000000000..26f26684afc --- /dev/null +++ b/src/otx/api/configuration/elements/metadata_keys.py @@ -0,0 +1,90 @@ +"""This module contains the keys that can be used to retrieve parameter metadata.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from typing import List + +DEFAULT_VALUE = "default_value" +MIN_VALUE = "min_value" +MAX_VALUE = "max_value" +STEP_SIZE = "step_size" +DESCRIPTION = "description" +HEADER = "header" +WARNING = "warning" +EDITABLE = "editable" +VISIBLE_IN_UI = "visible_in_ui" +AFFECTS_OUTCOME_OF = "affects_outcome_of" +UI_RULES = "ui_rules" +TYPE = "type" +OPTIONS = "options" +ENUM_NAME = "enum_name" +AUTO_HPO_STATE = "auto_hpo_state" +AUTO_HPO_VALUE = "auto_hpo_value" + + +def allows_model_template_override(keyword: str) -> bool: + """Returns True if the metadata element described by `keyword` can be overridden in a model template file. + + Args: + keyword (str): Name of the metadata key to check. + + Returns: + bool: True if the metadata indicated by `keyword` can be overridden in a model template .yaml file, False + otherwise. + """ + overrideable_keys = [ + DEFAULT_VALUE, + MIN_VALUE, + MAX_VALUE, + DESCRIPTION, + HEADER, + EDITABLE, + WARNING, + VISIBLE_IN_UI, + OPTIONS, + ENUM_NAME, + UI_RULES, + AFFECTS_OUTCOME_OF, + AUTO_HPO_STATE, + ] + return keyword in overrideable_keys + + +def allows_dictionary_values(keyword: str) -> bool: + """Returns True if the metadata element described by `keyword` allows having a dictionary as its value. + + Args: + keyword (str): Name of the metadata key to check. + + Returns: + bool: True if the metadata indicated by `keyword` allows having a dictionary as its value, False otherwise. + """ + keys_allowing_dictionary_values = [OPTIONS, UI_RULES] + return keyword in keys_allowing_dictionary_values + + +def all_keys() -> List[str]: + """Returns a list of all metadata keys. + + Returns: + List[str]: List of all available metadata keys + """ + return [ + DEFAULT_VALUE, + MIN_VALUE, + MAX_VALUE, + DESCRIPTION, + HEADER, + WARNING, + EDITABLE, + VISIBLE_IN_UI, + AFFECTS_OUTCOME_OF, + UI_RULES, + TYPE, + OPTIONS, + ENUM_NAME, + AUTO_HPO_STATE, + AUTO_HPO_VALUE, + ] diff --git a/src/otx/api/configuration/elements/parameter_group.py b/src/otx/api/configuration/elements/parameter_group.py new file mode 100644 index 00000000000..030a50baf02 --- /dev/null +++ b/src/otx/api/configuration/elements/parameter_group.py @@ -0,0 +1,187 @@ +"""ParameterGroup is the main class responsible for grouping configurable parameters together.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from enum import Enum +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from otx.api.configuration.elements import metadata_keys +from otx.api.configuration.enums import AutoHPOState +from otx.api.configuration.enums.config_element_type import ( + ConfigElementType, + ElementCategory, +) + + +@attr.s(auto_attribs=True, order=False, eq=False) +class ParameterGroup: + """A group of configuration elements. + + Parameters living within the parameter group are typed attrs Attributes. The schema for each parameter is defined + in its metadata, which can be retrieved using the `get_metadata` method from the parent ParameterGroup instance. + + Attributes: + header (str): User friendly name for the parameter group, that will be + displayed in the UI + description (str): User friendly string describing what the parameter + group represents, that will be displayed in the UI. + visible_in_ui (bool): Boolean that controls whether or not this + parameter group will be exposed through the REST API and + shown in the UI. Set to False to hide this group. Defaults + to True + """ + + header: str = attr.ib() + description: str = attr.ib(default="Default parameter group description") + visible_in_ui: bool = attr.ib(default=True) + type: ConfigElementType = attr.ib( + default=ConfigElementType.PARAMETER_GROUP, + repr=False, + init=False, + on_setattr=attr.setters.frozen, + ) + + def __attrs_post_init__(self) -> None: + """Update parameter and group after __init__. + + This method is called after the __init__ method to update the parameter and group fields of the ParameterGroup + instance. + """ + groups: List[str] = [] + parameters: List[str] = [] + self.__metadata_overrides: Dict[str, Any] = {} # pylint:disable=attribute-defined-outside-init + + for attribute_or_method_name in dir(self): + # Go over all attributes and methods of the class instance + attribute_or_method = getattr(self, attribute_or_method_name) + + metadata = self.get_metadata(attribute_or_method_name) + if metadata: + # If the attribute or method has metadata, it might be a configurable parameter. In that case, check + # its type to make sure it is one of the primitive parameter types + _type = metadata.get("type", None) + if _type is not None: + if _type.category == ElementCategory.PRIMITIVES: + # Add the parameter name to the `parameters` attribute + parameters.append(attribute_or_method_name) + + if isinstance(attribute_or_method, ParameterGroup): + # Add the parameter group name to the `groups` attribute + groups.append(attribute_or_method_name) + # Also run the post_init for all groups in the group, in case of nested groups + attribute_or_method.__attrs_post_init__() + + self.groups = groups # pylint:disable=attribute-defined-outside-init + self.parameters = parameters # pylint:disable=attribute-defined-outside-init + + def get_metadata(self, parameter_name: str) -> dict: + """Retrieve the metadata for a particular parameter from the group. + + Args: + parameter_name (str): name of the parameter for which to get the + metadata + + Returns: + dict: dictionary containing the metadata for the requested + parameter. Returns an empty dict if no metadata was found + for the parameter, or if the parameter was not found in the + group. + """ + parameter = getattr(attr.fields(type(self)), parameter_name, None) + if parameter is not None: + parameter_metadata = getattr(parameter, "metadata", {}) + metadata_dict = dict(parameter_metadata) + parameter_overrides = self.__metadata_overrides.get(parameter_name, None) + if parameter_overrides is not None: + for metadata_key, value_override in parameter_overrides.items(): + metadata_dict.update({metadata_key: value_override}) + return metadata_dict + return {} + + def set_metadata_value( + self, + parameter_name: str, + metadata_key: str, + value: Union[int, float, str, bool, Enum], + ) -> bool: + """Sets the value of a specific metadata item `metadata_key` for the parameter named `parameter_name`. + + Args: + parameter_name (str): name of the parameter for which to get the + metadata item + metadata_key (str): name of the metadata value to set + value (Union[int, float, str, bool, Enum]): New value to assign to the metadata item accessed by + `metadata_key`. The type of `value` has to exactly match + the type of the current value of the metadata item + + Returns: + True if the metadata item was successfully updated, False + otherwise + """ + parameter = getattr(attr.fields(type(self)), parameter_name, None) + if parameter is None: + return False + parameter_metadata = dict(getattr(parameter, "metadata", {})) + if metadata_key not in metadata_keys.all_keys(): + return False + metadata_value = parameter_metadata[metadata_key] + if metadata_value is not None and type(metadata_value) is not type( + value + ): # pylint: disable=unidiomatic-typecheck + return False + existing_overrides = self.__metadata_overrides.get(parameter_name, None) + if existing_overrides is None: + self.__metadata_overrides[parameter_name] = {metadata_key: value} + else: + existing_overrides.update({metadata_key: value}) + return True + + def update_auto_hpo_states(self): + """Update hpo state based on teh value of parameters. + + Updates the `auto_hpo_state` metadata field for all parameters in the parameter + group, based on the values of the parameters and the values of their + `auto_hpo_value` metadata fields. + """ + for parameter_name in self.parameters: + metadata = self.get_metadata(parameter_name) + if metadata[metadata_keys.AUTO_HPO_STATE] == AutoHPOState.NOT_POSSIBLE: + continue + auto_hpo_value = metadata[metadata_keys.AUTO_HPO_VALUE] + if auto_hpo_value is None: + continue + if auto_hpo_value != getattr(self, parameter_name): + auto_hpo_state = AutoHPOState.OVERRIDDEN + else: + auto_hpo_state = AutoHPOState.OPTIMIZED + self.set_metadata_value( + parameter_name=parameter_name, + metadata_key=metadata_keys.AUTO_HPO_STATE, + value=auto_hpo_state, + ) + for group_name in self.groups: + group = getattr(self, group_name) + group.update_auto_hpo_states() + + def __eq__(self, other): + """Comparison with support for dynamically generated ParameterGroups. + + Override default implementation of __eq__ to enable comparison of + ParameterGroups generated dynamically via the config helper. + """ + other_type = getattr(other, "type", None) + if other_type == self.type: + return self.__dict__ == other.__dict__ + return False + + +_ParameterGroup = TypeVar("_ParameterGroup", bound=ParameterGroup) + + +def add_parameter_group(group: Type[_ParameterGroup]) -> _ParameterGroup: + """Wrapper to attr.ib to add nested parameter groups to a configuration.""" + return attr.ib(factory=group, type=group) diff --git a/src/otx/api/configuration/elements/primitive_parameters.py b/src/otx/api/configuration/elements/primitive_parameters.py new file mode 100644 index 00000000000..109ab933f5c --- /dev/null +++ b/src/otx/api/configuration/elements/primitive_parameters.py @@ -0,0 +1,534 @@ +"""This module contains constructor functions for the primitive configurable parameter types. + +The available parameter types are: `configurable_integer`, `configurable_float`, `configurable_boolean`, +`string_selectable` and `float_selectable`. +""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from typing import List, Optional, TypeVar, Union + +import attr + +from otx.api.configuration.enums import AutoHPOState, ConfigElementType, ModelLifecycle +from otx.api.configuration.ui_rules import NullUIRules, UIRules + +from .configurable_enum import ConfigurableEnum +from .metadata_keys import ( + AFFECTS_OUTCOME_OF, + AUTO_HPO_STATE, + AUTO_HPO_VALUE, + DEFAULT_VALUE, + DESCRIPTION, + EDITABLE, + HEADER, + MAX_VALUE, + MIN_VALUE, + OPTIONS, + STEP_SIZE, + TYPE, + UI_RULES, + VISIBLE_IN_UI, + WARNING, +) +from .utils import ( + attr_strict_float_converter, + attr_strict_float_on_setattr, + attr_strict_int_validator, + construct_attr_enum_selectable_converter, + construct_attr_enum_selectable_onsetattr, + construct_attr_selectable_validator, + construct_attr_value_validator, +) + +# pylint:disable=too-many-arguments + +_ConfigurableEnum = TypeVar("_ConfigurableEnum", bound=ConfigurableEnum) + + +def set_common_metadata( + default_value: Union[int, float, str, bool, ConfigurableEnum], + header: str, + description: str, + warning: Optional[str], + editable: bool, + affects_outcome_of: ModelLifecycle, + ui_rules: UIRules, + visible_in_ui: bool, + parameter_type: ConfigElementType, + auto_hpo_state: AutoHPOState, + auto_hpo_value: Optional[Union[int, float, str, bool, ConfigurableEnum]], +) -> dict: + """Function to construct the dictionary of metadata that is common for all parameter types.""" + metadata = { + DEFAULT_VALUE: default_value, + DESCRIPTION: description, + HEADER: header, + WARNING: warning, + EDITABLE: editable, + VISIBLE_IN_UI: visible_in_ui, + AFFECTS_OUTCOME_OF: affects_outcome_of, + UI_RULES: ui_rules, + TYPE: parameter_type, + AUTO_HPO_STATE: auto_hpo_state, + AUTO_HPO_VALUE: auto_hpo_value, + } + return metadata + + +def configurable_integer( + default_value: int, + header: str, + min_value: int = 0, + max_value: int = 255, + description: str = "Default integer description", + warning: str = None, + editable: bool = True, + visible_in_ui: bool = True, + affects_outcome_of: ModelLifecycle = ModelLifecycle.NONE, + ui_rules: UIRules = NullUIRules(), + auto_hpo_state: AutoHPOState = AutoHPOState.NOT_POSSIBLE, + auto_hpo_value: Optional[int] = None, +) -> int: + """Constructs a configurable integer attribute, with the appropriate metadata. + + Args: + default_value: integer to use as default for the parameter + header: User friendly name for the parameter, which will be + shown in the UI + min_value: lower bound of the range of values this parameter can + take. Defaults to 0 + max_value: upper bound of the range of values this parameter can + take Defaults to 255 + description: A user friendly description of what this parameter + does, what does it represent and what are the effects of + changing it? + warning: An optional warning message to caution users when + changing this parameter. This message will be displayed in + the UI. For example, for the parameter batch_size: + `Increasing batch size increases GPU memory demands and may + result in out of memory errors. Please update batch size + with caution.` + editable: Set to False to prevent the parameter from being + edited in the UI. It can still be edited through the REST + API or the SDK. Defaults to True + visible_in_ui: Set to False to hide the parameter from the UI + and the REST API. It will still be visible through the SDK. + Defaults to True + affects_outcome_of: Describes the stage of the ModelLifecycle in + which this parameter modifies the outcome. See the + documentation for the ModelLifecycle Enum for further + details + ui_rules: Set of rules to control UI behavior for this + parameter. For example, the parameter can be shown or hidden + from the UI based on the value of other parameters in the + configuration. Have a look at the UIRules class for more + details. Defaults to NullUIRules. + auto_hpo_state: This flag reflects whether the parameter can be + (or has been) optimized through automatic hyper parameter + tuning (auto-HPO) + auto_hpo_value: If auto-HPO has been executed for this + parameter, this field will hold the optimized value for the + configurable integer + + Returns: + attrs Attribute of type `int`, with its metadata set according + to the inputs + """ + metadata = set_common_metadata( + default_value=default_value, + header=header, + description=description, + warning=warning, + editable=editable, + visible_in_ui=visible_in_ui, + ui_rules=ui_rules, + affects_outcome_of=affects_outcome_of, + parameter_type=ConfigElementType.INTEGER, + auto_hpo_state=auto_hpo_state, + auto_hpo_value=auto_hpo_value, + ) + + metadata.update({MIN_VALUE: min_value, MAX_VALUE: max_value}) + value_validator = construct_attr_value_validator(min_value, max_value) + type_validator = attr_strict_int_validator + + return attr.ib( + default=default_value, + type=int, + validator=[value_validator, type_validator], + on_setattr=attr.setters.validate, + metadata=metadata, + ) + + +def configurable_float( + default_value: float, + header: str, + min_value: float = 0.0, + max_value: float = 255.0, + step_size: Optional[float] = None, + description: str = "Default float description", + warning: str = None, + editable: bool = True, + visible_in_ui: bool = True, + affects_outcome_of: ModelLifecycle = ModelLifecycle.NONE, + ui_rules: UIRules = NullUIRules(), + auto_hpo_state: AutoHPOState = AutoHPOState.NOT_POSSIBLE, + auto_hpo_value: Optional[float] = None, +) -> float: + """Constructs a configurable float attribute, with the appropriate metadata. + + Args: + default_value: float to use as default for the parameter + header: User friendly name for the parameter, which will be + shown in the UI + min_value: lower bound of the range of values this parameter can + take. Defaults to 0.0 + max_value: upper bound of the range of values this parameter can + take Defaults to 255.0 + step_size: Recommended step size to increment/decrement this parameter, + mainly for visualization purposes. If None, step size is arbitrary. + description: A user friendly description of what this parameter + does, what does it represent and what are the effects of + changing it? + warning: An optional warning message to caution users when + changing this parameter. This message will be displayed in + the UI. For example, for the parameter batch_size: + `Increasing batch size increases GPU memory demands and may + result in out of memory errors. Please update batch size + with caution.` + editable: Set to False to prevent the parameter from being + edited in the UI. It can still be edited through the REST + API or the SDK. Defaults to True + visible_in_ui: Set to False to hide the parameter from the UI + and the REST API. It will still be visible through the SDK. + Defaults to True + affects_outcome_of: Describes the stage of the ModelLifecycle in + which this parameter modifies the outcome. See the + documentation for the ModelLifecycle Enum for further + details + ui_rules: Set of rules to control UI behavior for this + parameter. For example, the parameter can be shown or hidden + from the UI based on the value of other parameters in the + configuration. Have a look at the UIRules class for more + details. Defaults to NullUIRules. + auto_hpo_state: This flag reflects whether the parameter can be + (or has been) optimized through automatic hyper parameter + tuning (auto-HPO) + auto_hpo_value: If auto-HPO has been executed for this + parameter, this field will hold the optimized value for the + configurable float + + Returns: + attrs Attribute of type `float`, with its metadata set according + to the inputs + """ + metadata = set_common_metadata( + default_value=default_value, + header=header, + description=description, + warning=warning, + editable=editable, + visible_in_ui=visible_in_ui, + ui_rules=ui_rules, + affects_outcome_of=affects_outcome_of, + parameter_type=ConfigElementType.FLOAT, + auto_hpo_state=auto_hpo_state, + auto_hpo_value=auto_hpo_value, + ) + + metadata.update({MIN_VALUE: min_value, MAX_VALUE: max_value, STEP_SIZE: step_size}) + value_validator = construct_attr_value_validator(min_value, max_value) + type_validator = attr_strict_float_on_setattr + + return attr.ib( + default=default_value, + type=float, + validator=[value_validator, type_validator], + converter=attr_strict_float_converter, + on_setattr=[attr.setters.convert, attr.setters.validate], + metadata=metadata, + ) + + +def configurable_boolean( + default_value: bool, + header: str, + description: str = "Default configurable boolean description", + warning: str = None, + editable: bool = True, + visible_in_ui: bool = True, + affects_outcome_of: ModelLifecycle = ModelLifecycle.NONE, + ui_rules: UIRules = NullUIRules(), + auto_hpo_state: AutoHPOState = AutoHPOState.NOT_POSSIBLE, + auto_hpo_value: Optional[bool] = None, +) -> bool: + """Constructs a configurable boolean attribute, with the appropriate metadata. + + Args: + default_value: boolean to use as default for the parameter + header: User friendly name for the parameter, which will be + shown in the UI + description: A user friendly description of what this parameter + does, what does it represent and what are the effects of + changing it? + warning: An optional warning message to caution users when + changing this parameter. This message will be displayed in + the UI. For example, for the parameter batch_size: + `Increasing batch size increases GPU memory demands and may + result in out of memory errors. Please update batch size + with caution.` + editable: Set to False to prevent the parameter from being + edited in the UI. It can still be edited through the REST + API or the SDK. Defaults to True + visible_in_ui: Set to False to hide the parameter from the UI + and the REST API. It will still be visible through the SDK. + Defaults to True + affects_outcome_of: Describes the stage of the ModelLifecycle in + which this parameter modifies the outcome. See the + documentation for the ModelLifecycle Enum for further + details + ui_rules: Set of rules to control UI behavior for this + parameter. For example, the parameter can be shown or hidden + from the UI based on the value of other parameters in the + configuration. Have a look at the UIRules class for more + details. Defaults to NullUIRules. + auto_hpo_state: This flag reflects whether the parameter can be + (or has been) optimized through automatic hyper parameter + tuning (auto-HPO) + auto_hpo_value: If auto-HPO has been executed for this + parameter, this field will hold the optimized value for the + configurable boolean + + Returns: + attrs Attribute of type `bool`, with its metadata set according + to the inputs + """ + metadata = set_common_metadata( + default_value=default_value, + header=header, + description=description, + warning=warning, + editable=editable, + visible_in_ui=visible_in_ui, + ui_rules=ui_rules, + affects_outcome_of=affects_outcome_of, + parameter_type=ConfigElementType.BOOLEAN, + auto_hpo_state=auto_hpo_state, + auto_hpo_value=auto_hpo_value, + ) + type_validator = attr.validators.instance_of(bool) + + return attr.ib( + default=default_value, + metadata=metadata, + type=bool, + validator=type_validator, + on_setattr=attr.setters.validate, + ) + + +def float_selectable( + default_value: float, + header: str, + options: List[float], + description: str = "Default selectable description", + warning: str = None, + editable: bool = True, + visible_in_ui: bool = True, + affects_outcome_of: ModelLifecycle = ModelLifecycle.NONE, + ui_rules: UIRules = NullUIRules(), + auto_hpo_state: AutoHPOState = AutoHPOState.NOT_POSSIBLE, + auto_hpo_value: Optional[float] = None, +) -> float: + """Constructs a configurable float selectable attribute, with the appropriate metadata. + + Args: + default_value: float to use as default for the parameter + header: User friendly name for the parameter, which will be + shown in the UI + options: list of float options representing the values that this + parameter can take + description: A user friendly description of what this parameter + does, what does it represent and what are the effects of + changing it? + warning: An optional warning message to caution users when + changing this parameter. This message will be displayed in + the UI. For example, for the parameter batch_size: + `Increasing batch size increases GPU memory demands and may + result in out of memory errors. Please update batch size + with caution.` + editable: Set to False to prevent the parameter from being + edited in the UI. It can still be edited through the REST + API or the SDK. Defaults to True + visible_in_ui: Set to False to hide the parameter from the UI + and the REST API. It will still be visible through the SDK. + Defaults to True + affects_outcome_of: Describes the stage of the ModelLifecycle in + which this parameter modifies the outcome. See the + documentation for the ModelLifecycle Enum for further + details + ui_rules: Set of rules to control UI behavior for this + parameter. For example, the parameter can be shown or hidden + from the UI based on the value of other parameters in the + configuration. Have a look at the UIRules class for more + details. Defaults to NullUIRules. + auto_hpo_state: This flag reflects whether the parameter can be + (or has been) optimized through automatic hyper parameter + tuning (auto-HPO) + auto_hpo_value: If auto-HPO has been executed for this + parameter, this field will hold the optimized value for the + float selectable + + Returns: + attrs Attribute of type `float`, with its metadata set according + to the inputs + """ + metadata = set_common_metadata( + default_value=default_value, + header=header, + description=description, + warning=warning, + editable=editable, + visible_in_ui=visible_in_ui, + ui_rules=ui_rules, + affects_outcome_of=affects_outcome_of, + parameter_type=ConfigElementType.FLOAT_SELECTABLE, + auto_hpo_state=auto_hpo_state, + auto_hpo_value=auto_hpo_value, + ) + + metadata.update({OPTIONS: options}) + value_validator = construct_attr_selectable_validator(options) + type_validator = attr_strict_float_on_setattr + + return attr.ib( + default=default_value, + type=float, + validator=[value_validator, type_validator], + converter=attr_strict_float_converter, + on_setattr=[attr.setters.convert, attr.setters.validate], + metadata=metadata, + ) + + +def selectable( + default_value: _ConfigurableEnum, + header: str, + description: str = "Default selectable description", + warning: str = None, + editable: bool = True, + visible_in_ui: bool = True, + affects_outcome_of: ModelLifecycle = ModelLifecycle.NONE, + ui_rules: UIRules = NullUIRules(), + auto_hpo_state: AutoHPOState = AutoHPOState.NOT_POSSIBLE, + auto_hpo_value: Optional[str] = None, +) -> _ConfigurableEnum: + """Constructs a selectable attribute from a pre-defined Enum, with the appropriate metadata. + + The list of options for display in the UI is inferred from the type of the ConfigurableEnum instance passed in + as default_value. + + Args: + default_value: OTXConfigurationEnum instance to use as default + for the parameter + header: User friendly name for the parameter, which will be + shown in the UI + description: A user friendly description of what this parameter + does, what does it represent and what are the effects of + changing it? + warning: An optional warning message to caution users when + changing this parameter. This message will be displayed in + the UI. For example, for the parameter batch_size: + `Increasing batch size increases GPU memory demands and may + result in out of memory errors. Please update batch size + with caution.` + editable: Set to False to prevent the parameter from being + edited in the UI. It can still be edited through the REST + API or the SDK. Defaults to True + visible_in_ui: Set to False to hide the parameter from the UI + and the REST API. It will still be visible through the SDK. + Defaults to True + affects_outcome_of: Describes the stage of the ModelLifecycle in + which this parameter modifies the outcome. See the + documentation for the ModelLifecycle Enum for further + details + ui_rules: Set of rules to control UI behavior for this + parameter. For example, the parameter can be shown or hidden + from the UI based on the value of other parameters in the + configuration. Have a look at the UIRules class for more + details. Defaults to NullUIRules. + auto_hpo_state: This flag reflects whether the parameter can be + (or has been) optimized through automatic hyper parameter + tuning (auto-HPO) + auto_hpo_value: If auto-HPO has been executed for this + parameter, this field will hold the optimized value for the + string selectable + + Returns: + attrs Attribute, with its type matching the type of + `default_value`, and its metadata set according to the inputs + """ + metadata = set_common_metadata( + default_value=default_value, + header=header, + description=description, + warning=warning, + editable=editable, + visible_in_ui=visible_in_ui, + ui_rules=ui_rules, + affects_outcome_of=affects_outcome_of, + parameter_type=ConfigElementType.SELECTABLE, + auto_hpo_state=auto_hpo_state, + auto_hpo_value=auto_hpo_value, + ) + + metadata.update(default_value.get_class_info()) + + type_validator = attr.validators.instance_of(ConfigurableEnum) + value_validator = construct_attr_enum_selectable_onsetattr(default_value) + + # The Attribute returned by attr.ib is not compatible with the return typevar _ConfigurableEnum. However, as the + # class containing the Attribute is instantiated the selectable type will correspond to the _ConfigurableEnum, so + # mypy can ignore the error. + return attr.ib( + default=default_value, + type=ConfigurableEnum, + validator=[type_validator, value_validator], # type: ignore + converter=construct_attr_enum_selectable_converter(default_value), + on_setattr=[attr.setters.convert, value_validator], + metadata=metadata, + ) # type: ignore + + +def string_attribute(value: str) -> str: + """String attribute. + + Wrapper for attr.ib that can be used to overwrite simple string attributes in a class or parameter group + definition. + + Args: + value: string to be added as attribute + + Returns: + attr.ib string attribute with its default value set to value + """ + return attr.ib(default=value, type=str, kw_only=True) + + +def boolean_attribute(value: bool) -> bool: + """Boolean attribute wrapper. + + Wrapper for attr.ib that can be used to overwrite simple boolean attributes in a class or parameter group + definition. + + Args: + value: boolean to be added as attribute + + Returns: + attr.ib boolean attribute with its default value set to value + """ + return attr.ib(default=value, type=bool, kw_only=True) diff --git a/src/otx/api/configuration/elements/utils.py b/src/otx/api/configuration/elements/utils.py new file mode 100644 index 00000000000..0023f56b528 --- /dev/null +++ b/src/otx/api/configuration/elements/utils.py @@ -0,0 +1,283 @@ +"""Utility functions for attr package. + +This module contains utility functions to use with the attr package, concerning for instance parameter validation +or serialization. They are used within the cfg_helper or the configuration elements. +""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from enum import Enum +from typing import Callable, List, Optional, Type, TypeVar, Union + +from attr import Attribute + +from otx.api.configuration.elements.configurable_enum import ConfigurableEnum +from otx.api.configuration.elements.parameter_group import ParameterGroup +from otx.api.entities.id import ID + +NumericTypeVar = TypeVar("NumericTypeVar", int, float) +SelectableTypeVar = TypeVar("SelectableTypeVar", float, str) +ConfigurableEnumTypeVar = TypeVar("ConfigurableEnumTypeVar", bound=ConfigurableEnum) + + +def attr_enum_to_str_serializer( + instance: object, # pylint: disable=unused-argument + attribute: Attribute, # pylint: disable=unused-argument + value: Union[Enum, str], +) -> str: + """This function converts Enums to their string representation. + + It is used when converting between yaml and python object representation of the configuration. + The function signature matches what is expected by the `attr.asdict` value_serializer argument. + + Args: + instance (object): (Unused) The instance of the class that the attribute is attached to. + attribute (Attribute): (unused) The attribute that is being serialized. + value (Union[Enum, str]): The value to serialize. + + Returns: + str: The string representation of the value. + """ + if isinstance(value, Enum): + return str(value) + return value + + +def _convert_enum_selectable_value( + value: Union[str, ConfigurableEnumTypeVar], + enum_class: Type[ConfigurableEnumTypeVar], +) -> ConfigurableEnumTypeVar: + """Helper function that converts the input value to an instance of the correct ConfigurableEnum. + + Args: + value (Union[str, ConfigurableEnumTypeVar]): input value to convert + enum_class (Type[ConfigurableEnumTypeVar]): Type of the Enum to convert to + + Returns: + ConfigurableEnumTypeVar: Instance of the correct ConfigurableEnum + """ + if isinstance(value, str): + try: + enum_value = enum_class(value) + except ValueError as ex: + raise ValueError( + f"The value {value} is an invalid option for {enum_class.__name__}. Valid options are:" + f" {enum_class.get_values()}" + ) from ex + return enum_value + return value + + +def construct_attr_enum_selectable_converter( + default_value: ConfigurableEnumTypeVar, +) -> Callable[[Union[str, ConfigurableEnumTypeVar]], ConfigurableEnumTypeVar]: + """This function converts an input value to the correct instance of the ConfigurableEnum. + + It is used when initializing a selectable parameter. + + Args: + default_value (ConfigurableEnumTypeVar): Default value for the selectable + + Returns: + Callable[[Union[str, ConfigurableEnumTypeVar]], ConfigurableEnumTypeVar]: + Function that converts an input value to the correct instance of the ConfigurableEnum. + """ + enum_class = type(default_value) + + def attr_convert_enum_selectable_value(value: Union[str, ConfigurableEnumTypeVar]) -> ConfigurableEnumTypeVar: + """Function that converts an input value to an instance of the appropriate ConfigurableEnum. + + Can be used as a `converter` for attrs.Attributes of type ConfigurableEnum. + + Args: + value (Union[str, ConfigurableEnumTypeVar]): Value to convert to ConfigurableEnum instance + + Returns: + ConfigurableEnumTypeVar: Instance of the correct ConfigurableEnum + """ + return _convert_enum_selectable_value(value, enum_class=enum_class) + + return attr_convert_enum_selectable_value + + +def construct_attr_enum_selectable_onsetattr( + default_value: ConfigurableEnumTypeVar, +) -> Callable[[ParameterGroup, Attribute, Union[str, ConfigurableEnumTypeVar]], ConfigurableEnumTypeVar]: + """This function converts an input value to the correct instance of the ConfigurableEnum. + + It is used when setting a value for a selectable parameter. + + Args: + default_value (ConfigurableEnumTypeVar): Default value for the enum_selectable. + + Returns: + Callable[[Union[str, ConfigurableEnumTypeVar]], ConfigurableEnumTypeVar]: Function that converts an input + value to the correct instance of the ConfigurableEnum. + """ + enum_class = type(default_value) + + def attr_convert_enum_selectable_value( + instance: ParameterGroup, # pylint: disable=unused-argument + attribute: Attribute, # pylint: disable=unused-argument + value: Union[str, ConfigurableEnumTypeVar], + ) -> ConfigurableEnumTypeVar: + """Function that converts an input value to an instance of the appropriate ConfigurableEnum. + + Can be used with the `on_setattr` hook of the attrs package. + """ + return _convert_enum_selectable_value(value, enum_class=enum_class) + + return attr_convert_enum_selectable_value + + +def construct_attr_value_validator( + min_value: NumericTypeVar, max_value: NumericTypeVar +) -> Callable[[ParameterGroup, Attribute, NumericTypeVar], None]: + """Constructs a validator function that is used in the attribute validation of numeric configurable parameters. + + Args: + min_value (NumericTypeVar): Minimum value for the parameter + max_value (NumericTypeVar): Maximum value for the parameter + + Returns: + Callable[[ParameterGroup, Attribute, NumericTypeVar], None]: Function that validates the input value. + """ + + def attr_validate_value( + instance: ParameterGroup, attribute: Attribute, value: NumericTypeVar + ): # pylint: disable=unused-argument + """This function is used to validate values for numeric ConfigurableParameters.""" + if not min_value <= value <= max_value: + raise ValueError(f"Invalid value set for {attribute.name}: {value} is out of bounds.") + + return attr_validate_value + + +def construct_attr_selectable_validator( + options: List[SelectableTypeVar], +) -> Callable[[ParameterGroup, Attribute, SelectableTypeVar], None]: + """Constructs a validator function that is used in the attribute validation of selectable configurable parameters. + + Args: + options (List[SelectableTypeVar]): List of valid options for the parameter. + + Returns: + Callable[[ParameterGroup, Attribute, SelectableTypeVar], None]: Function that validates the input value. + """ + + def attr_validate_selectable( + instance: ParameterGroup, attribute: Attribute, value: SelectableTypeVar + ): # pylint: disable=unused-argument + """This function is used to validate values for selectable ConfigurableParameters.""" + if value not in options: + raise ValueError( + f"Invalid value set for {attribute.name}: {value} is not a valid option for this " f"parameter." + ) + + return attr_validate_selectable + + +def convert_string_to_id(id_string: Optional[Union[str, ID]]) -> ID: + """This function converts an input string representing an ID into an OTX ID object. + + Inputs that are already in the form of an ID are left untouched. + + Args: + id_string (Optional[Union[str, ID]]): string, ID or None object that should be converted to an ID. + + Returns: + ID: the input as an instance of ID + """ + if id_string is None: + output_id = ID() + elif isinstance(id_string, str): + output_id = ID(id_string) + else: + output_id = id_string + return output_id + + +def attr_strict_int_validator( + instance: ParameterGroup, # pylint: disable=unused-argument + attribute: Attribute, + value: int, +) -> None: + """Validates that the value set for an attribute is an integer. + + Args: + instance (ParameterGroup): (Unused) ParameterGroup to which the attribute belongs + attribute (Attribute): Attribute for which to validate the value + value (int): Value to validate + + Raises: + TypeError: if the value passed to the validator is not an integer + """ + is_strict_int = isinstance(value, int) and not isinstance(value, bool) + if not is_strict_int: + raise TypeError(f"Invalid argument type for {attribute.name}: {value} is not of type 'int'") + + +def _validate_and_convert_float(value: float) -> Optional[float]: + """Validate that a value is a float, or a number that can be converted to a float. + + If the value is valid, this method will return the value as float. Otherwise, this + method returns None + + Args: + value (float): Value to validate and convert + + Returns: + Optional[float]: The value as a float, or None if the value is not valid + """ + valid = True + if not isinstance(value, (float, int)): + valid = False + if isinstance(value, bool): + valid = False + if valid: + return float(value) + return None + + +def attr_strict_float_on_setattr( + instance: ParameterGroup, # pylint: disable=unused-argument + attribute: Attribute, + value: float, +) -> float: + """Validate that the value set for an attribute is a float, or a number that can be converted to a float. + + Args: + instance (ParameterGroup): ParameterGroup to which the attribute belongs + attribute (Attribute): Attribute for which to validate the value + value (float): Value to validate + + Raises: + TypeError: if the value passed to the validator is not a float or number + + Returns: + float: The value as a float + """ + float_value = _validate_and_convert_float(value) + if float_value is None: + raise TypeError(f"Invalid argument type for {attribute.name}: {value} is not of type " f"'float'") + return float_value + + +def attr_strict_float_converter(value: float) -> float: + """Converts a value to float. + + Args: + value (float): value to convert + + Raises: + TypeError: if value cannot be converted to float + + Returns: + Value as float + """ + float_value = _validate_and_convert_float(value) + if float_value is None: + raise TypeError(f"Invalid value passed for parameter. Value {value} of type {type(value)} " f"is not a float.") + return float_value diff --git a/src/otx/api/configuration/enums/__init__.py b/src/otx/api/configuration/enums/__init__.py new file mode 100644 index 00000000000..f39c2ccb97a --- /dev/null +++ b/src/otx/api/configuration/enums/__init__.py @@ -0,0 +1,11 @@ +"""This module contains Enums used in the configurable parameters within the OTX.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .auto_hpo_state import AutoHPOState +from .config_element_type import ConfigElementType +from .model_lifecycle import ModelLifecycle + +__all__ = ["ConfigElementType", "ModelLifecycle", "AutoHPOState"] diff --git a/src/otx/api/configuration/enums/auto_hpo_state.py b/src/otx/api/configuration/enums/auto_hpo_state.py new file mode 100644 index 00000000000..ac19dc4dea8 --- /dev/null +++ b/src/otx/api/configuration/enums/auto_hpo_state.py @@ -0,0 +1,33 @@ +"""This module contains the AutoHPOState Enum.""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from enum import Enum + + +class AutoHPOState(Enum): + """Holds metadata related to automatic hyper parameter optimization (auto-HPO) for a single configurable parameter. + + It contains the following values: + NOT_POSSIBLE - This implies that the parameter cannot be optimized via auto-HPO + POSSIBLE - This implies that the parameter can potentially be optimized via + auto-HPO, but auto-HPO has not been carried out for this parameter + yet + OPTIMIZED - This implies that the parameter has been optimized via auto-HPO, + such that the current value of the parameter reflects it's optimal + value + OVERRIDDEN - This implies that the parameter has previously been optimized via + auto-HPO, but it's value has been manually overridden + """ + + NOT_POSSIBLE = "not_possible" + POSSIBLE = "possible" + OPTIMIZED = "optimized" + OVERRIDDEN = "overridden" + + def __str__(self): + """Retrieves the string representation of an instance of the Enum.""" + return self.value diff --git a/src/otx/api/configuration/enums/config_element_type.py b/src/otx/api/configuration/enums/config_element_type.py new file mode 100644 index 00000000000..7b21b306190 --- /dev/null +++ b/src/otx/api/configuration/enums/config_element_type.py @@ -0,0 +1,69 @@ +"""Enums for configuration element types used to construct/interact with OTX configuration objects.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from __future__ import annotations + +from enum import Enum, auto + + +class ElementCategory(Enum): + """This Enum represents the categories of configuration elements that are known in OTX.""" + + PRIMITIVES = auto() + GROUPS = auto() + RULES = auto() + + def __str__(self): + """Retrieves the string representation of an instance of the Enum.""" + return self.name + + +class ConfigElementType(Enum): + """This Enum represents the available elements to compose a configuration. + + Each instance holds a name, value and category representing a configuration element. + """ + + # Because this Enum takes both a value and a category, the auto() mechanism cannot be used to assign values. Hence, + # they are assigned manually. + INTEGER = 0, ElementCategory.PRIMITIVES + FLOAT = 1, ElementCategory.PRIMITIVES + BOOLEAN = 2, ElementCategory.PRIMITIVES + FLOAT_SELECTABLE = 3, ElementCategory.PRIMITIVES + SELECTABLE = 4, ElementCategory.PRIMITIVES + PARAMETER_GROUP = 5, ElementCategory.GROUPS + CONFIGURABLE_PARAMETERS = 6, ElementCategory.GROUPS + RULE = 7, ElementCategory.RULES + UI_RULES = 8, ElementCategory.RULES + + def __new__(cls, value: int, category: ElementCategory): # pylint: disable=unused-argument + """Creates a new instance of the Enum. + + The ConfigElementType Enum holds both a value and a category. In this method the `value` argument is parsed and + assigned. + """ + obj = object.__new__(cls) + # Only the value is assigned here, since the _category_ attribute does not exists yet. + obj._value_ = value + return obj + + def __init__(self, value: int, category: ElementCategory): # pylint: disable=unused-argument + """Upon initialization, the Enum category is assigned.""" + # We cannot assign to _category_ in the __new__ method since it is not a valid attribute yet until the Enum is + # initialized + self._category_ = category + + @property + def category(self) -> ElementCategory: + """Returns the element category which the ConfigElementType belongs to. + + Categories are instances of the `otx.api.configuration.configuration_types.ElementCategory` Enum. + """ + return self._category_ + + def __str__(self): + """Retrieves the string representation of an instance of the Enum.""" + return self.name diff --git a/src/otx/api/configuration/enums/model_lifecycle.py b/src/otx/api/configuration/enums/model_lifecycle.py new file mode 100644 index 00000000000..d0a781aa660 --- /dev/null +++ b/src/otx/api/configuration/enums/model_lifecycle.py @@ -0,0 +1,44 @@ +"""This module contains the ModelLifecycle Enum.""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from enum import Enum, auto + + +class ModelLifecycle(Enum): + """This Enum represents the different stages in the ModelLifecycle. + + It is used by configuration parameters to indicate + in which stage of the model lifecycle the parameter takes effect. Selecting a stage early in the lifecycle implies + that all downstream stages are affected as well (e.g. if this is set to `ModelLifecycle.TRAINING`, it is assumed + that inference and testing are also impacted). + + Currently the following stages are possible: + ARCHITECTURE - Select this stage if the parameter modifies the model architecture, such that the most recently + trained weights cannot directly by used for the next training round due to a model topology mismatch. For + example, a parameter `model_depth` that controls the number of downsampling steps in a UNet model should + have this stage set. + TRAINING - Select this stage if the parameter is likely to change the outcome of the training process. For example, + the parameter `learning_rate` should have this stage set. + INFERENCE - Select this stage if the parameter changes the result of inference. For example, a parameter + `probability_threshold` that controls the threshold for binary classification should have this stage set. + TESTING - Select this stage if the parameter changes the outcome of the evaluation process. For example, a parameter + 'test_metric` that controls which metric to use for testing does not change training or inference results, but + does affect the final evaluation of the model. Therefore, it should have this stage set. + NONE - Select this stage if the parameter is non-functional, for example if it only impacts training speed but + should not change the training outcome. For example, a parameter `num_workers` that controls the number of + threads used in a dataloader should have this stage set. + """ + + NONE = auto() + ARCHITECTURE = auto() + TRAINING = auto() + INFERENCE = auto() + TESTING = auto() + + def __str__(self): + """Retrieves the string representation of an instance of the Enum.""" + return self.name diff --git a/src/otx/api/configuration/enums/utils.py b/src/otx/api/configuration/enums/utils.py new file mode 100644 index 00000000000..1002333a68c --- /dev/null +++ b/src/otx/api/configuration/enums/utils.py @@ -0,0 +1,20 @@ +"""This module contains utility functions related to Enums.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from enum import Enum +from typing import List, Type + + +def get_enum_names(enum_cls: Type[Enum]) -> List[str]: + """Returns a list containing the names of all members of the Enum class passed as `enum_cls`. + + Args: + enum_cls (Type[Enum]): The Enum class to get the names of its members. + + Returns: + List[str]: The list of names of all members of the Enum class passed as `enum_cls`. + """ + return [member.name for member in enum_cls] diff --git a/src/otx/api/configuration/helper/__init__.py b/src/otx/api/configuration/helper/__init__.py new file mode 100644 index 00000000000..0eb74a3b550 --- /dev/null +++ b/src/otx/api/configuration/helper/__init__.py @@ -0,0 +1,26 @@ +"""This module contains the configuration helper functions. + +These can be used to create, convert or interact with OTX configuration objects or dictionaries, yaml strings or yaml +files representing those objects. +""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from .convert import convert +from .create import create +from .substitute import substitute_values, substitute_values_for_lifecycle +from .utils import config_to_bytes, flatten_config_values, merge_a_into_b +from .validate import validate + +__all__ = [ + "create", + "config_to_bytes", + "validate", + "convert", + "substitute_values", + "substitute_values_for_lifecycle", + "flatten_config_values", + "merge_a_into_b", +] diff --git a/src/otx/api/configuration/helper/config_element_mapping.py b/src/otx/api/configuration/helper/config_element_mapping.py new file mode 100644 index 00000000000..8296f3ca4d5 --- /dev/null +++ b/src/otx/api/configuration/helper/config_element_mapping.py @@ -0,0 +1,72 @@ +"""This module contains mappings from ConfigElementType names to the appropriate constructor classes or functions. + +Currently three different mappings are defined: `PrimitiveElementMapping` representing the different base parameter +types, `GroupElementMapping` representing the different configuration groups and `RuleElementMapping` representing the +different ui exposure logic classes +""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from enum import Enum +from functools import partial + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.elements import ( + ParameterGroup, + configurable_boolean, + configurable_float, + configurable_integer, + float_selectable, + selectable, +) +from otx.api.configuration.ui_rules.rules import Rule, UIRules + + +class PrimitiveElementMapping(Enum): + """This Enum represents the mapping from primitive configuration element names to their constructors. + + It is only used by the ConfigHelper to be able to reconstruct a configuration object out of a dictionary + or yaml string. + """ + + INTEGER = partial(configurable_integer) + FLOAT = partial(configurable_float) + BOOLEAN = partial(configurable_boolean) + FLOAT_SELECTABLE = partial(float_selectable) + SELECTABLE = partial(selectable) + + def __str__(self): + """Retrieves the string representation of an instance of the Enum.""" + return self.name + + +class GroupElementMapping(Enum): + """This Enum represents the mapping from configuration group names to their constructors. + + It is only used by the ConfigHelper to be able to reconstruct a configuration object out of a dictionary or yaml + string. + """ + + PARAMETER_GROUP = ParameterGroup + CONFIGURABLE_PARAMETERS = ConfigurableParameters + + def __str__(self): + """Retrieves the string representation of an instance of the Enum.""" + return self.name + + +class RuleElementMapping(Enum): + """This Enum represents the mapping from configuration logic names to their constructors. + + It is only used by the ConfigHelper to be able to reconstruct a configuration object out of a dictionary or yaml + string. + """ + + RULE = Rule + UI_RULES = UIRules + + def __str__(self): + """Retrieves the string representation of an instance of the Enum.""" + return self.name diff --git a/src/otx/api/configuration/helper/convert.py b/src/otx/api/configuration/helper/convert.py new file mode 100644 index 00000000000..350293655f7 --- /dev/null +++ b/src/otx/api/configuration/helper/convert.py @@ -0,0 +1,139 @@ +"""This module contains the definition for the `convert` function within the configuration helper. + +This function can be used to convert a OTX configuration object to a dictionary or yaml representation. +""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from enum import Enum +from typing import Type, TypeVar, Dict, Any + +import yaml +from omegaconf import DictConfig, OmegaConf + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.elements import ( + ConfigurableEnum, + ParameterGroup, + metadata_keys, +) + +ConvertTypeVar = TypeVar("ConvertTypeVar", str, DictConfig, dict) + + +def serialize_metadata(metadata_dict: dict, enum_to_str: bool = True) -> dict: + """This function converts Enums in the metadata_dict to their string representation. + + It is used when converting between yaml and python object representation of the configuration. + + Args: + metadata_dict (dict): Dictionary containing the metadata to convert + enum_to_str (bool): True to convert enums to their string representation, False to leave them as Enums + """ + for key, value in metadata_dict.items(): + if isinstance(value, Enum): + if enum_to_str: + metadata_dict[key] = str(value) + if key == metadata_keys.UI_RULES: + metadata_dict[key] = value.to_dict(enum_to_str) + return metadata_dict + + +def parameter_group_to_dict( + parameter_group: ParameterGroup, + enum_to_str: bool = False, + values_only: bool = False, +) -> dict: + """Converts an instance of a `ParameterGroup` configuration element to its dictionary representation. + + Args: + parameter_group (ParameterGroup): ParameterGroup to convert to dictionary representation + enum_to_str (bool): Set to True to convert any Enum fields in the configuration to + their string representation. + values_only (bool): True to keep only the parameter values, and remove all meta + data from the output dictionary + + Returns: + dict: Nested dictionary with keys and values corresponding to the configuration + defined in the instance of `ParameterGroup` for which the `to_dict` method was + called. + """ + parameter_group.update_auto_hpo_states() + attribute_names = [attribute.name for attribute in parameter_group.__attrs_attrs__] # type: ignore + # The __attrs_attrs__ attribute is added through the attrs package, mypy doesn't recognize it so we can ignore the + # type error + attribute_values = [getattr(parameter_group, attribute_name) for attribute_name in attribute_names] + + dictionary_representation = {} + for name, value in zip(attribute_names, attribute_values): + # Go through all simple attributes first, add them to the dictionary representation + if isinstance(value, Enum) and enum_to_str: + value = str(value) + dictionary_representation[name] = value + for group_name in parameter_group.groups: + # Then, recursively add all parameter groups to the dictionary representation + group = getattr(parameter_group, group_name) + dictionary_representation.update({group_name: parameter_group_to_dict(group, enum_to_str, values_only)}) + for parameter_name in parameter_group.parameters: + # Then, add all parameters for this group to the dictionary representation + # For each parameter, construct a dict with its value and then update it with the metadata + value = getattr(parameter_group, parameter_name) + if isinstance(value, ConfigurableEnum) and enum_to_str: + value = str(value) + if values_only: + parameter_dictionary = value + else: + parameter_dictionary = {"value": value} + parameter_dictionary.update(serialize_metadata(parameter_group.get_metadata(parameter_name), enum_to_str)) + # Finally, add the parameter to the dictionary representation of the group + dictionary_representation.update({parameter_name: parameter_dictionary}) + return dictionary_representation + + +def convert( + config: ConfigurableParameters, + target: Type[ConvertTypeVar], + enum_to_str: bool = False, + id_to_str: bool = False, + values_only: bool = False, +) -> Any: + """Convert a configuration object to either a yaml string, a dictionary or an OmegaConf DictConfig object. + + Args: + config (ConfigurableParameters): ConfigurableParameters object to convert + target (Type[ConvertTypeVar]): target type to convert to. Options are [str, dict, DictConfig] + enum_to_str (bool) : Boolean specifying whether to convert enums within the config + to their string representation. For conversion to yaml, enums are automatically converted and this option + is disregarded. + id_to_str (bool): True to convert the id of the configurable parameters to a string + representation, False to leave it as an ID object + values_only (bool): True to keep only the parameter values, and remove all meta + data from the target output + + Raises: + ValueError: if an unsupported conversion target is supplied + + Returns: + ConvertTypeVar: Result of the conversion, the configuration specified in `config` in the + representation specified in `target` + """ + if target == str: + enum_to_str = True + + config_dict = parameter_group_to_dict(config, enum_to_str=enum_to_str, values_only=values_only) + + if id_to_str or target == str or target == DictConfig: + config_id = config_dict.get("id", None) + config_dict["id"] = str(config_id) if config_id is not None else None + + result: Any = None + if target == str: + return yaml.dump(config_dict) + elif target == dict: + return config_dict + elif target == DictConfig: + return OmegaConf.create(config_dict) + else: + raise ValueError("Unsupported conversion target! Supported target types are [str, dict, DictConfig]") diff --git a/src/otx/api/configuration/helper/create.py b/src/otx/api/configuration/helper/create.py new file mode 100644 index 00000000000..3a54181cc90 --- /dev/null +++ b/src/otx/api/configuration/helper/create.py @@ -0,0 +1,380 @@ +"""This module contains the definition for the `create` function within the configuration helper. + +This function can be used to create a OTX configuration object from a dictionary or yaml representation. +""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from __future__ import annotations + +import copy +from enum import Enum +from typing import Dict, List, TypeVar, Union + +import attr +from omegaconf import DictConfig, OmegaConf + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.elements import ( + ConfigurableEnum, + ParameterGroup, + metadata_keys, +) +from otx.api.configuration.enums import AutoHPOState, ModelLifecycle +from otx.api.configuration.enums.config_element_type import ( + ConfigElementType, + ElementCategory, +) +from otx.api.configuration.enums.utils import get_enum_names +from otx.api.configuration.ui_rules.rules import NullUIRules, Rule, UIRules + +from .config_element_mapping import ( + GroupElementMapping, + PrimitiveElementMapping, + RuleElementMapping, +) +from .utils import deserialize_enum_value, input_to_config_dict + +ParameterGroupTypeVar = TypeVar("ParameterGroupTypeVar", ParameterGroup, ConfigurableParameters) +ExposureTypeVar = TypeVar("ExposureTypeVar", UIRules, Rule) + +METADATA_ENUMS = { + metadata_keys.AFFECTS_OUTCOME_OF: ModelLifecycle, + metadata_keys.AUTO_HPO_STATE: AutoHPOState, +} + + +def construct_attrib_from_dict(dict_object: Union[dict, DictConfig]) -> ExposureTypeVar: + """Constructs a ui exposure element from an input dictionary or DictConfig. + + Elements are mapped according to the 'type' field in the input dict. + + Args: + dict_object (Union[dict, DictConfig]): Dictionary containing the arguments for the element constructor. + + Returns: + ExposureTypeVar: Rule or UIRules element, constructed according to the input dict_object. + """ + value_dict = dict(dict_object) + object_type = str(value_dict.pop(metadata_keys.TYPE)) + if object_type in get_enum_names(RuleElementMapping): + mapping = RuleElementMapping + else: + raise ValueError(f"Invalid type found in configuration dictionary: {object_type}") + cls_constructor = mapping[object_type].value + return cls_constructor(**value_dict) + + +def construct_ui_rules_from_dict(ui_exposure_settings: Union[dict, DictConfig]) -> UIRules: + """Takes a dictionary representation of ui exposure logic and constructs an UIRules element out of this. + + Args: + ui_exposure_settings: dictionary representing the logic to govern exposure of a parameter in the UI + + Returns: + Exposure element constructed according to the settings passed in ui_exposure_settings + """ + if ui_exposure_settings is None: + return NullUIRules() + + rules_dict = ui_exposure_settings.pop("rules", None) + ui_exposure_settings["rules"] = [] + rules: List[Union[UIRules, Rule]] = [] + + if len(rules_dict) == 0: + return NullUIRules() + + for rule in rules_dict: + rule_type = str(rule.get(metadata_keys.TYPE, "")) + if rule_type == RuleElementMapping.UI_RULES.name: + rules.append(construct_ui_rules_from_dict(rule)) + elif rule_type == RuleElementMapping.RULE.name: + rules.append(construct_attrib_from_dict(rule)) + else: + raise ValueError( + f"Invalid UI exposure settings passed to parser. Configuration contains invalid rule " + f"type: {rule_type}" + ) + ui_rules: UIRules = construct_attrib_from_dict(ui_exposure_settings) + ui_rules.rules = rules + return ui_rules + + +def create_default_configurable_enum_from_dict(parameter_dict: Union[dict, DictConfig]) -> dict: + """Create a default configurable enum from a dictionary. + + Takes a parameter_dict representing a configurable Enum and consumes the ENUM_NAME, OPTIONS and DEFAULT_VALUE + metadata. + + From this, a new subclass of ConfigurableEnum is constructed. The DEFAULT_VALUE of the parameter_dict is updated + with the appropriate ConfigurableEnum instance. + + Args: + parameter_dict: Dictionary representation of an enum_selectable parameter. Can be either serialized or not. + + Returns: + parameter_dict containing the default_value instantiated as a ConfigurableEnum + """ + # Some input type validation here first, to keep mypy happy + if isinstance(parameter_dict, DictConfig): + param_dict = OmegaConf.to_container(parameter_dict) + if isinstance(param_dict, dict): + parameter_dict = param_dict + else: + raise TypeError(f"Invalid input parameter_dict of type {type(parameter_dict)}") + + # Create the enum using the functional Enum API. Unfortunately this doesn't play nice with mypy, so ignoring the + # type error for now + # pylint: disable=unexpected-keyword-arg + configurable_enum = ConfigurableEnum( + parameter_dict.pop(metadata_keys.ENUM_NAME), + names=parameter_dict.pop(metadata_keys.OPTIONS), + ) # type: ignore + # pylint: enable=unexpected-keyword-arg + serialized_default_value = parameter_dict.pop(metadata_keys.DEFAULT_VALUE) + + if isinstance(serialized_default_value, ConfigurableEnum): + default_value = serialized_default_value + else: + # This instantiates the enum, but mypy doesn't pick up that the created configurable_enum type is callable. + default_value = configurable_enum(serialized_default_value) # type: ignore + + parameter_dict.update({metadata_keys.DEFAULT_VALUE: default_value}) + return parameter_dict + + +def gather_parameter_arguments_and_values_from_dict(config_dict_section: Union[dict, DictConfig]) -> dict: + """Collect arguments needed to construct attrs class out of a config dict section representing a parameter group. + + Parameters living in the group are constructed in this function as well. + + This function returns a dictionary that contains the keys `make_arguments`, `call_arguments` and `values`. + make_arguments are the arguments that should be passed to attr.make_class to dynamically generate a class + constructor + call_arguments are the arguments that should be passed in the initialization call to the constructor + values are the parameter values, that can be set once the instance of the parameter group is created. + + Args: + config_dict_section: Dictionary representation of a parameter + group in a configuration, for which to gather the + constructor arguments + + Returns: + dictionary containing the make_arguments, call_arguments and + values parsed from the config_dict_section + """ + # pylint: disable=too-many-locals + make_arguments = {} + call_arguments: dict = {} + all_parameter_values = {} + for key, value in config_dict_section.items(): + if isinstance(value, (DictConfig, dict)): + # In case of a nested dict, value represents the settings for a parameter. The arguments are parsed from + # the dict here and passed to the constructor function for the parameter, according to its `type`. + parameter_dict = copy.deepcopy(value) + parameter_type = str(parameter_dict.pop(metadata_keys.TYPE, None)) + if parameter_type == str(ConfigElementType.SELECTABLE): + parameter_dict = create_default_configurable_enum_from_dict(parameter_dict) + elif parameter_type == str(None): + raise ValueError( + f"No type was specified for the configurable " f"parameter or parameter group named '{key}'" + ) + parameter_value = parameter_dict.pop("value", None) + + metadata_enums: Dict[str, Enum] = {} + for metadata_key, enum_type in METADATA_ENUMS.items(): + enum_value = parameter_dict.pop(metadata_key, None) + if enum_value is not None: + metadata_enums.update({metadata_key: deserialize_enum_value(enum_value, enum_type=enum_type)}) + + parameter_ui_rules_dict = parameter_dict.pop(metadata_keys.UI_RULES, None) + parameter_constructor = PrimitiveElementMapping[parameter_type].value + parameter_ui_rules = construct_ui_rules_from_dict(parameter_ui_rules_dict) + parameter_make_arguments = { + key: parameter_constructor(**parameter_dict, ui_rules=parameter_ui_rules, **metadata_enums) + } + make_arguments.update(parameter_make_arguments) + all_parameter_values.update({key: parameter_value}) + else: + # Flat values will be passed to the initialization call directly. The only exception is the 'type' item, + # this should not be included in the call_arguments as it is used to determine the type of constructor for + # attrs to generate + if key != metadata_keys.TYPE: + call_arguments.update({key: value}) + return { + "make_arguments": make_arguments, + "call_arguments": call_arguments, + "values": all_parameter_values, + } + + +def create_parameter_group(config_dict_section: Union[dict, DictConfig]) -> ParameterGroupTypeVar: + """Creates a parameter group object out of a config_dict_section. + + config_dict_section is a dictionary or DictConfig representing a parameter group. + This method should only be used for simple groups, i.e. parameter groups not containing any other parameter groups. + For nested groups, the function 'create_nested_parameter_group' should be used instead. + + Args: + config_dict_section: Dictionary representation of the parameter + group to construct + + Returns: + ParameterGroup or ConfigurableParameters object constructed + according to config_dict_section + """ + params_and_values = gather_parameter_arguments_and_values_from_dict(config_dict_section) + make_arguments = params_and_values["make_arguments"] + call_arguments = params_and_values["call_arguments"] + all_parameter_values = params_and_values["values"] + group_type = str(config_dict_section.pop(metadata_keys.TYPE)) + + group_constructor_type = GroupElementMapping[group_type].value + group_constructor = attr.make_class( + GroupElementMapping[group_type].name, + bases=(group_constructor_type,), + attrs=make_arguments, + eq=False, + order=False, + ) + + parameter_group = group_constructor(**call_arguments) + + for parameter, value in all_parameter_values.items(): + if value is not None: + setattr(parameter_group, parameter, value) + + parameter_group.update_auto_hpo_states() + return parameter_group + + +def create_nested_parameter_group(config_dict_section: Union[dict, DictConfig]) -> ParameterGroup: + """Creates a parameter group object out of a config_dict_section. + + config_dict_section is a dictionary or DictConfig representing a parameter group. This method should be used for + nested groups, and uses recursion to reconstruct those. + + Args: + config_dict_section: Dictionary representation of the parameter group to construct + + Returns: + ParameterGroup or Configuration object constructed according to config_dict_section + """ + groups = {} + main_config = copy.deepcopy(config_dict_section) + + group_names = contains_parameter_groups(config_dict_section) + for group_name in group_names: + group_config_section = config_dict_section.pop(group_name) + + if not contains_parameter_groups(group_config_section): + childless_parameter_group: ParameterGroup = create_parameter_group(group_config_section) + groups.update({group_name: childless_parameter_group}) + + else: + parameter_group_with_children: ParameterGroup = create_nested_parameter_group(group_config_section) + groups.update({group_name: parameter_group_with_children}) + + main_config.pop(group_name, None) + + parameter_group: ParameterGroup = create_parameter_group(main_config) + + for group_name, group in groups.items(): + setattr(parameter_group, group_name, group) + + parameter_group.__attrs_post_init__() + + return parameter_group + + +def contains_parameter_groups(config_dict: Union[dict, DictConfig]) -> List[str]: + """Checks whether a configuration or configuration section specified in `config_dict` contains parameter groups. + + Returns a list of the group names if it does, and an empty list otherwise + + Args: + config_dict: Dictionary or DictConfig representing a configuration or configuration section. + + Returns: + List of names of parameter groups that are defined in the config_dict, if any. Empty list otherwise. + """ + if isinstance(config_dict, DictConfig): + input_dict: dict = OmegaConf.to_container(config_dict) # type: ignore + # The config_dict will always be converted to a dict by the OmegaConf.to_container call, but this is not + # reflected by the return type signature. Therefore, mypy can ignore this error + else: + input_dict = config_dict + + groups: List[str] = [] + group_category_types: List[str] = [str(x) for x in ConfigElementType if x.category == ElementCategory.GROUPS] + + for field_name, field_value in input_dict.items(): + if isinstance(field_value, dict): + # If the field is a dict, check whether the type is a parameter group (or derived from it) + _type = field_value.get(metadata_keys.TYPE, None) + if str(_type) in group_category_types: + groups.append(field_name) + return groups + + +def from_dict_attr(config_dict: Union[dict, DictConfig]) -> ConfigurableParameters: + """Creates a configuration object from an input config_dict. + + Uses recursion to handle nested parameter groups in the config + + Args: + config_dict (Union[dict, DictConfig]): Dictionary representation of a TaskConfig, + ProjectConfig or ComponentConfig + + Returns: + ConfigurableParameters: ParameterGroup object constructed according to config_dict + """ + local_config_dict = copy.deepcopy(config_dict) + + # Initialize all groups in the config, and collect them into the config_groups dict to assign them to the final + # configuration later on + config_groups: Dict[str, ParameterGroup] = {} + + # Determine all parameter groups living at the first level of the config + base_config_groups = contains_parameter_groups(config_dict) + for group_name in base_config_groups: + group_config_section = local_config_dict.pop(group_name) + # If the group itself contains groups, it is nested. If not, it's flat. Initialization for nested and flat + # groups is different, hence we check this. + if contains_parameter_groups(group_config_section): + config_groups.update({group_name: create_nested_parameter_group(group_config_section)}) + else: + config_groups.update({group_name: create_parameter_group(group_config_section)}) + + # Collect parameters for the high level config from the config_dict and create the config constructor, using the + # type defined in the dict + config: ConfigurableParameters = create_parameter_group(local_config_dict) + + # Add the groups to the config + for group_name, group in config_groups.items(): + setattr(config, group_name, group) + + # Run post-initialization to set the groups and parameters attributes correctly + config.__attrs_post_init__() + + return config + + +def create(input_config: Union[str, DictConfig, dict]) -> ConfigurableParameters: + """Create a configuration object from a yaml string, yaml file path, dictionary or OmegaConf DictConfig object. + + Args: + input_config: yaml string, dictionary, DictConfig or filepath + describing a configuration. + + Returns: + ConfigurableParameters object + """ + # Parse input, validate config type and convert to dict if needed + config_dict = input_to_config_dict(copy.deepcopy(input_config)) + # Create config from the resulting dictionary + config: ConfigurableParameters = from_dict_attr(config_dict) + + return config diff --git a/src/otx/api/configuration/helper/substitute.py b/src/otx/api/configuration/helper/substitute.py new file mode 100644 index 00000000000..d0a1926b76f --- /dev/null +++ b/src/otx/api/configuration/helper/substitute.py @@ -0,0 +1,196 @@ +"""Definitions for `substitute_values` and `substitute_values_for_lifecycle` functions within the configuration helper. + +These functions can be used to update values or ids in a OTX configuration object, according to a value dictionary or +configuration object +""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from typing import Dict, Optional, Sequence, Union + +from omegaconf import DictConfig + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.elements import ParameterGroup, metadata_keys +from otx.api.configuration.enums import ModelLifecycle + +from .convert import convert +from .create import input_to_config_dict +from .utils import search_in_config_dict +from .validate import validate + + +def _should_parameter_be_updated( + config_element: ParameterGroup, + parameter_name: str, + model_lifecycle: Optional[Union[Sequence[ModelLifecycle], ModelLifecycle]], +) -> bool: + """Check if parameter should be updated. + + Checks whether a parameter `parameter_name` belonging to a ParameterGroup + `config_element` fullfills the criterion for updating it's value, giving in + `model_lifecycle`. + + config_element (ParameterGroup): ParameterGroup which the parameter belongs to + parameter_name (str): Name of the parameter + model_lifecycle (Optional[Union[Sequence[ModelLifecycle], ModelLifecycle]]): + Phase or list of phases of the model lifecycle which the parameter should + affect in order to update it. + + Returns: + bool: True if the parameter should be updated, False otherwise + """ + should_update = True + if model_lifecycle is not None: + # Skip parameter substitution if model lifecycle does not match + # provided value + metadata = config_element.get_metadata(parameter_name) + parameter_affects_outcome_of = metadata[metadata_keys.AFFECTS_OUTCOME_OF] + if isinstance(model_lifecycle, list): + should_update = parameter_affects_outcome_of in model_lifecycle + else: + should_update = parameter_affects_outcome_of == model_lifecycle + return should_update + + +def _substitute( + config: ConfigurableParameters, + value_input: Dict, + allow_missing_values: bool = False, + model_lifecycle: Optional[Union[ModelLifecycle, Sequence[ModelLifecycle]]] = None, +): + """Substitutes values from value_input into the config object. + + The structures of value_input and config have to match in order for the values to be substituted + correctly. If the argument `model_lifecycle` is provided, only parameters that + affect the specified phase in the model lifecycle will be substituted. + + Values are substituted in place. + + Args: + config (ConfigurableParameters): ConfigurableParameter object to substitute values into. + value_input (Dict): ConfigurableParameters (either in object, dict, yaml or DictConfig representation) to take + the values to be substituted from. + allow_missing_values (bool): True to allow missing values in the configuration, i.e. if a value is found in + `value_input`, but not in `config`, it will silently be ignored. If set to False, an AttributeError will be + raised. Defaults to False. + model_lifecycle (Optional[Union[ModelLifecycle, Sequence[ModelLifecycle]]]): Optional phase or list of phases + in the model lifecycle to carry out the substitution for. If no `model_lifecycle` is provided, substitution + will be carried out for all parameters. + """ + # Search all 'value' entries in the input dict + values_and_paths_list = search_in_config_dict(value_input, key_to_search="value") + + # Substitute the values in the config, according to their paths + for (value, path) in values_and_paths_list: + if metadata_keys.UI_RULES in path: + # Skip entries that involve ui rules (these entries also have a `value` key), since we are only + # concerned about parameter values here + continue + config_element = config + + for attribute_name in path: + if attribute_name != path[-1]: + # Traverse the config according to path + if hasattr(config_element, attribute_name): + config_element = getattr(config_element, attribute_name) + else: + if not allow_missing_values: + raise AttributeError( + f"Configuration does not match structure of the values to " + f"substitute from. The configuration {config_element} does " + f"not contain the parameter group {attribute_name}" + ) + else: + # At the end of the path, update the attribute value + if hasattr(config_element, attribute_name): + if _should_parameter_be_updated( + config_element=config_element, + parameter_name=attribute_name, + model_lifecycle=model_lifecycle, + ): + setattr(config_element, attribute_name, value) + else: + if not allow_missing_values: + raise AttributeError( + f"Configuration does not match structure of the values " + f"to substitute from. Target parameter group " + f"{config_element} does not contain the parameter " + f"{attribute_name}." + ) + + +def substitute_values( + config: ConfigurableParameters, + value_input: Union[str, DictConfig, dict, ConfigurableParameters], + allow_missing_values: bool = False, +): + """Substitutes values from value_input into the config object. + + The structures of value_input and config have to match in order for the values to be substituted + correctly. + + Values are substituted in place. + + Args: + config (ConfigurableParameters): ConfigurableParameter object to substitute values into. + value_input (Union[str, DictConfig, dict, ConfigurableParameters]): ConfigurableParameters (either in object, + dict, yaml or DictConfig representation) to take the values to be substituted from. + allow_missing_values (bool): True to allow missing values in the configuration, + i.e. if a value is found in `value_input`, but not in `config`, it will + silently be ignored. If set to False, an AttributeError will be + raised. Defaults to False. + """ + # Parse input and convert to dict if needed + if isinstance(value_input, ConfigurableParameters): + input_dict: Dict = convert(value_input, dict) + else: + input_dict = input_to_config_dict(value_input, check_config_type=False) + + _substitute(config, input_dict, allow_missing_values=allow_missing_values) + + # Finally, validate the config with the updated values + validate(config) + + +def substitute_values_for_lifecycle( + config: ConfigurableParameters, + value_input: ConfigurableParameters, + model_lifecycle: Union[ModelLifecycle, Sequence[ModelLifecycle]], + allow_missing_values: bool = True, +): + """Substitutes values from value_input into the config object. + + The structures of value_input and config have to match in order for the values to be substituted correctly. + If the argument `model_lifecycle` is provided, only parameters that affect the specified phase in the model + lifecycle will be substituted. + + Values are substituted in place. + + Args: + config (ConfigurableParameters): ConfigurableParameter object to substitute values into + value_input (ConfigurableParameters): ConfigurableParameters to take the values to be substituted from. + model_lifecycle (Union[ModelLifecycle, Sequence[ModelLifecycle]]): Phase or list of phases in the + model lifecycle to carry out the substitution for. For example, if + `model_lifecycle = ModelLifecycle.INFERENCE` is passed, only parameters that + affect inference will be updated, and the rest of the parameters will + remain untouched. + allow_missing_values (bool): True to allow missing values in the configuration, + i.e. if a value is found in `value_input`, but not in `config`, it will + silently be ignored. If set to False, an AttributeError will be + raised. Defaults to True. + """ + input_dict: dict = convert(value_input, dict) + + _substitute( + config, + input_dict, + allow_missing_values=allow_missing_values, + model_lifecycle=model_lifecycle, + ) + + # Finally, validate the config with the updated values + validate(config) diff --git a/src/otx/api/configuration/helper/utils.py b/src/otx/api/configuration/helper/utils.py new file mode 100644 index 00000000000..3631b97bd63 --- /dev/null +++ b/src/otx/api/configuration/helper/utils.py @@ -0,0 +1,249 @@ +"""This module contains utility functions used within the configuration helper module.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import copy +import json +import os +from enum import Enum +from typing import Any, List, Tuple, Type, Union + +import yaml +from omegaconf import DictConfig, OmegaConf + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.enums.utils import get_enum_names +from otx.api.entities.id import ID + +from .config_element_mapping import ( + GroupElementMapping, + PrimitiveElementMapping, + RuleElementMapping, +) +from .convert import convert + + +def _search_in_config_dict_inner( + config_dict: dict, + key_to_search: str, + prior_keys: List[str] = None, + results: List[Tuple[Any, List[str]]] = None, +) -> List[Tuple[Any, List[str]]]: + """Helper function for the `search_in_config_dict` function defined below. + + Args: + config_dict: dict to search in + key_to_search: dict key to look for + prior_keys: List of prior keys leading to the key_to_search. + results: List of previously found results + + Returns: + List of (value_at_key_to_search, key_path_to_key_to_search) + tuples, representing each occurrence of key_to_search within + config_dict + """ + if prior_keys is None: + prior_keys = [] + if results is None: + results = [] + if isinstance(config_dict, List): + dict_to_search_in = dict(zip(range(len(config_dict)), config_dict)) + else: + dict_to_search_in = config_dict + if not (issubclass(type(dict_to_search_in), dict) or isinstance(dict_to_search_in, DictConfig)): + return results + for key, value in dict_to_search_in.items(): + current_key_path = prior_keys + [key] + if key == key_to_search: + results.append((value, prior_keys)) + _search_in_config_dict_inner(value, key_to_search, current_key_path, results) + return results + + +def search_in_config_dict(config_dict: dict, key_to_search: str) -> List[Tuple[Any, List[str]]]: + """Recursively searches a config_dict for all instances of key_to_search and returns the key path to them. + + Args: + config_dict: dict to search in + key_to_search: dict key to look for + + Returns: + List of (value_at_key_to_search, key_path_to_key_to_search) + tuples, representing each occurrence of key_to_search within + config_dict + """ + return _search_in_config_dict_inner(config_dict, key_to_search=key_to_search) + + +def input_to_config_dict(input_config: Union[str, DictConfig, dict], check_config_type: bool = True) -> dict: + """Validate input configuration. + + Takes an input_config which can be a string, filepath, dict or DictConfig and performs basic validation that it + can be converted into a configuration. + + Args: + input_config: String, filepath, dict or DictConfig describing a + configuration + check_config_type: True to check that the input has a proper + `type` attribute in order to be converted into a + ConfigurableParameters object. False to disable this check. + Defaults to True. + + Returns: + dictionary or DictConfig + """ + valid_types = ( + get_enum_names(PrimitiveElementMapping) + + get_enum_names(RuleElementMapping) + + get_enum_names(GroupElementMapping) + ) + + if isinstance(input_config, str): + if os.path.exists(input_config): + with open(input_config, "r", encoding="UTF-8") as file: + result = yaml.safe_load(file) + else: + result = yaml.safe_load(input_config) + elif issubclass(type(input_config), dict): + result = input_config + elif isinstance(input_config, DictConfig): + result = OmegaConf.to_container(input_config) + else: + raise ValueError('Invalid input_config type! Valid types are "str", "DictConfig", "dict".') + + if check_config_type: + config_type = str(result.get("type", None)) + + if config_type is None: + raise ValueError( + f"Input cannot be converted to a valid configuration. No " + f"configuration type was found in the following input: {input_config}" + ) + if config_type not in valid_types: + raise ValueError( + f"Invalid configuration element type: {config_type} found in input. " + f"Unable to parse input. Supported configuration element types are:" + f" {valid_types}" + ) + return result + + +def deserialize_enum_value(value: Union[str, Enum], enum_type: Type[Enum]): + """Deserializes a value to an instance of a certain Enum. + + This checks whether the `value` passed is already an instance of the target Enum, in which case this function just + returns the input `value`. If value is a string, this function returns the corresponding instance of the Enum + passed in `enum_type`. + + Args: + value: value to deserialize + enum_type: class (should be a subclass of Enum) that the name + belongs to + + Returns: + instance of `enum_type`.`value` + """ + if isinstance(value, enum_type): + instance = value + elif isinstance(value, str): + try: + instance = enum_type[value.upper()] + except KeyError: + try: + instance = enum_type(value) # type: ignore + except ValueError as error: + raise KeyError(f"Value `{value}` cannot be converted to an instance of " f"`{enum_type}`") from error + else: + raise ValueError( + f"Invalid input data type, {type(value)} cannot be converted to an instance " f"of {enum_type}." + ) + return instance + + +def ids_to_strings(config_dict: dict) -> dict: + """Converts ID's in the `config_dict` to their string representation. + + Args: + config_dict: Dictionary in which to replace the ID's by strings + + Returns: + Updated config_dict dictionary + """ + for key, value in config_dict.items(): + if isinstance(value, ID): + config_dict[key] = str(value) + return config_dict + + +def config_to_bytes(config: ConfigurableParameters) -> bytes: + """Converts ConfigurableParameters to bytes. + + Args: + config: configurable parameters + + Retruns: + JSON in bytes + """ + config_dict = convert(config, dict, enum_to_str=True) + return json.dumps(config_dict, indent=4).encode() + + +def flatten_config_values(config: dict): + """Extracts the "value" field from any nested config. + + Flattening the structure of the config dictionary. The original config dictionary is modified in-place. + + Args: + config (dict): config dictionary + """ + for key, value in config.items(): + if isinstance(value, dict): + if "value" in value: + config[key] = value["value"] + else: + flatten_config_values(value) + + +def flatten_detection_config_groups(config: dict): + """Converts all Detection Config Group objects in a config dictionary to their dictionary representation. + + Args: + config (dict): config dictionary + """ + for key, value in config.items(): + if hasattr(value, "__dict__"): + config[key] = value.__dict__ + elif isinstance(value, dict): + flatten_detection_config_groups(value) + + +def merge_a_into_b(dict_a, dict_b): + """Inspired by mmcv.Config.merge_a_into_b by merging dict ``a`` into dict ``b`` (non-inplace). + + Values in ``a`` will overwrite ``b``. ``b`` is copied first to avoid + in-place modifications. + + Args: + dict_a (dict): The source dict to be merged into ``b``. + dict_b (dict): The origin dict to be fetch keys from ``a``. + + Returns: + dict: The modified dict of ``b`` using ``a``. + + Examples: + # Normally merge a into b. + >>> merge_a_into_b({'a': {'d': 5, 'e': 6}, 'b': 4}, {'a': {'c': 1, 'd': 4}, 'b': 3}) + {'a': {'c': 1, 'd': 5, 'e': 6}, 'b': 4} + """ + dict_b = copy.deepcopy(dict_b) + for key, value in dict_a.items(): + if isinstance(value, dict): + if key in dict_b: + dict_b[key] = merge_a_into_b(value, dict_b[key]) + else: + dict_b[key] = value + else: + dict_b[key] = value + return dict_b diff --git a/src/otx/api/configuration/helper/validate.py b/src/otx/api/configuration/helper/validate.py new file mode 100644 index 00000000000..613a55cb34a --- /dev/null +++ b/src/otx/api/configuration/helper/validate.py @@ -0,0 +1,38 @@ +"""This module contains the definition for the `validate` function within the configuration helper. + +This function can be used to validate the values of a OTX configuration object, checking that each of the parameter +values in the configuration are within their allowed bounds or options. +""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import attr + +from otx.api.configuration.elements import ParameterGroup + + +def _validate_inner(config: ParameterGroup): + """Recursive method that performs validation on all parameters within a parameter group. + + Uses recursion to validate parameters living inside nested groups. + """ + attr.validate(config) + if config.groups: + for group_name in config.groups: + group = getattr(config, group_name) + _validate_inner(group) + + +def validate(config: ParameterGroup) -> bool: + """Validate a configuration object. + + Args: + config: Configuration to validate + + Returns: + True if config is valid + """ + _validate_inner(config) + return True diff --git a/src/otx/api/configuration/model_lifecycle.py b/src/otx/api/configuration/model_lifecycle.py new file mode 100644 index 00000000000..1cda8a67f66 --- /dev/null +++ b/src/otx/api/configuration/model_lifecycle.py @@ -0,0 +1,14 @@ +"""This is a legacy file that serves to maintain compatibility with the OTX detection framework. + +It links to the model_lifecycle enum under 'enums' + +# TODO: Remove once https://jira.devtools.intel.com/browse/CVS-67869 is done +""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .enums.model_lifecycle import ModelLifecycle + +__all__ = ["ModelLifecycle"] diff --git a/src/otx/api/configuration/ui_rules/__init__.py b/src/otx/api/configuration/ui_rules/__init__.py new file mode 100644 index 00000000000..c75ec7c2072 --- /dev/null +++ b/src/otx/api/configuration/ui_rules/__init__.py @@ -0,0 +1,17 @@ +"""UI rules configuration.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from .rules import NullUIRules, Rule, UIRules +from .types import Action, Operator + +__all__ = [ + "NullUIRules", + "Rule", + "UIRules", + "Action", + "Operator", +] diff --git a/src/otx/api/configuration/ui_rules/rules.py b/src/otx/api/configuration/ui_rules/rules.py new file mode 100644 index 00000000000..aabf7ddb994 --- /dev/null +++ b/src/otx/api/configuration/ui_rules/rules.py @@ -0,0 +1,103 @@ +"""This module contains the different ui rules elements, Rule and UIRules. + +They are used to define rules for disabling configuration parameters in the ui, conditional on the value of other +parameters. +""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from __future__ import annotations + +from typing import Callable, List, Optional, Union + +from attr import asdict, attrib, attrs, setters + +from otx.api.configuration.elements.utils import attr_enum_to_str_serializer +from otx.api.configuration.enums.config_element_type import ConfigElementType + +from .types import Action, Operator +from .utils import attr_convert_action, attr_convert_operator + +ALLOWED_RULE_VALUE_TYPES = Union[int, str, float, bool] # pylint: disable=invalid-name + + +@attrs(auto_attribs=True) +class Rule: + """This class represents a `operator` applied to the `value` of the configurable parameter `parameter`. + + The parameter for which the rule should be evaluated is identified by name, or by a list of names representing the + attribute path to the parameter in case of a nested configuration. + """ + + parameter: Union[str, List[str]] + value: ALLOWED_RULE_VALUE_TYPES + operator: Operator = attrib(default=Operator.EQUAL_TO, converter=attr_convert_operator) + type: ConfigElementType = attrib(default=ConfigElementType.RULE, on_setattr=setters.frozen) + + def to_dict(self, enum_to_str: bool = True) -> dict: + """Method to serialize a Rule instance to its dictionary representation. + + Args: + enum_to_str (bool): Set to True to convert Enum instances to their string representation + + Returns: + dict: dictionary representation of the Rule object for which this method is called + """ + if enum_to_str: + serializer: Optional[Callable] = attr_enum_to_str_serializer + else: + serializer = None + return asdict(self, value_serializer=serializer) + + +@attrs(auto_attribs=True) +class UIRules: + """This class allows the combination of ExposureRules using boolean logic. + + The class can be set as an attribute of a configurable parameter. If the `rules` + (combined according to the `operator`) evaluate to True, the corresponding`action` will be taken in the UI. + + If UIRules are nested, only the `action` of the outermost UIRule will be considered. + """ + + rules: List[Union[Rule, UIRules]] = attrib(kw_only=True) + operator: Operator = attrib(default=Operator.AND, converter=attr_convert_operator) + action: Action = attrib(default=Action.DISABLE_EDITING, converter=attr_convert_action) + type: ConfigElementType = attrib(default=ConfigElementType.UI_RULES, on_setattr=setters.frozen) + + def add_rule(self, rule: Union[Rule, UIRules]): + """Adds rule.""" + self.rules.append(rule) + + def to_dict(self, enum_to_str: bool = True) -> dict: + """Method to serialize an UIRules instance to its dictionary representation. + + Applies recursion to convert nested rules, if applicable. + + Args: + enum_to_str: Set to True to convert Enum instances to their + string representation + + Returns: + dictionary representation of the UIRules object for which + this method is called + """ + if enum_to_str: + serializer: Optional[Callable] = attr_enum_to_str_serializer + else: + serializer = None + rules_list = [] + for rule in self.rules: + rules_list.append(rule.to_dict(enum_to_str)) + dictionary_representation = asdict(self, value_serializer=serializer) + dictionary_representation.update({"rules": rules_list}) + return dictionary_representation + + +@attrs +class NullUIRules(UIRules): + """This class represents an empty, unset UIRules element.""" + + rules: List[Union[Rule, UIRules]] = attrib(factory=list) diff --git a/src/otx/api/configuration/ui_rules/types.py b/src/otx/api/configuration/ui_rules/types.py new file mode 100644 index 00000000000..69c0d3785f9 --- /dev/null +++ b/src/otx/api/configuration/ui_rules/types.py @@ -0,0 +1,39 @@ +"""This module contains the types used for specifying UI interaction with the configuration.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from enum import Enum, auto + + +class Operator(Enum): + """This Enum represents the allowed operators for use in constructing UI rules for configuration parameters. + + These operators can be used to disable a configuration parameter, conditional on the value of another parameter. + """ + + NOT = auto() + EQUAL_TO = auto() + LESS_THAN = auto() + GREATER_THAN = auto() + AND = auto() + OR = auto() + XOR = auto() + + def __str__(self): + """Retrieves the string representation of an instance of the Enum.""" + return self.name + + +class Action(Enum): + """This Enum represents the allowed actions that UI rules can dictate on configuration parameters.""" + + HIDE = auto() + SHOW = auto() + ENABLE_EDITING = auto() + DISABLE_EDITING = auto() + + def __str__(self): + """Retrieves the string representation of an instance of the Enum.""" + return self.name diff --git a/src/otx/api/configuration/ui_rules/utils.py b/src/otx/api/configuration/ui_rules/utils.py new file mode 100644 index 00000000000..4870a3b8b31 --- /dev/null +++ b/src/otx/api/configuration/ui_rules/utils.py @@ -0,0 +1,29 @@ +"""This module contains utility functions for use in defining ui rules.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from typing import Union + +from otx.api.configuration.ui_rules.types import Action, Operator + + +def attr_convert_operator(operator: Union[str, Operator]) -> Operator: + """This function converts an input operator to the correct instance of the Operator Enum. + + It is used when loading an Rule element from a yaml file. + """ + if isinstance(operator, str): + return Operator[operator] + return operator + + +def attr_convert_action(action: Union[str, Action]) -> Action: + """This function converts an input action to the correct instance of the Action Enum. + + It is used when loading an Rule element from a yaml file. + """ + if isinstance(action, str): + return Action[action] + return action diff --git a/src/otx/api/entities/__init__.py b/src/otx/api/entities/__init__.py new file mode 100644 index 00000000000..a1c225b4f87 --- /dev/null +++ b/src/otx/api/entities/__init__.py @@ -0,0 +1,4 @@ +"""Shape and graph entities.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/api/entities/annotation.py b/src/otx/api/entities/annotation.py new file mode 100644 index 00000000000..0870b26971d --- /dev/null +++ b/src/otx/api/entities/annotation.py @@ -0,0 +1,326 @@ +"""This module define the annotation entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +import datetime +from enum import Enum +from typing import Dict, List, Optional, Set + +from bson import ObjectId + +from otx.api.entities.id import ID +from otx.api.entities.label import LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.shape import ShapeEntity +from otx.api.utils.time_utils import now + + +class Annotation(metaclass=abc.ABCMeta): + """Base class for annotation objects. + + Args: + shape (ShapeEntity): the shape of the annotation + labels (List[ScoredLabel]): the labels of the annotation + id (Optional[ID]): the id of the annotation + """ + + # pylint: disable=redefined-builtin; + def __init__(self, shape: ShapeEntity, labels: List[ScoredLabel], id: Optional[ID] = None): + self.__id_ = ID(ObjectId()) if id is None else id + self.__shape = shape + self.__labels = labels + + def __repr__(self): + """String representation of the annotation.""" + return ( + f"{self.__class__.__name__}(" + f"shape={self.shape}, " + f"labels={self.get_labels(include_empty=True)}, " + f"id={self.id_})" + ) + + @property + def id_(self): + """Returns the id for the annotation.""" + return self.__id_ + + @id_.setter + def id_(self, value): + self.__id_ = value + + @property + def id(self): + """DEPRECATED.""" + return self.__id_ + + @id.setter + def id(self, value): + """DEPRECATED.""" + self.__id_ = value + + @property + def shape(self) -> ShapeEntity: + """Returns the shape that is in the annotation.""" + return self.__shape + + @shape.setter + def shape(self, value) -> None: + self.__shape = value + + def get_labels(self, include_empty: bool = False) -> List[ScoredLabel]: + """Get scored labels that are assigned to this annotation. + + Args: + include_empty (bool): set to True to include empty label (if exists) in the output. Defaults to False. + + Returns: + List of labels in annotation + """ + return [label for label in self.__labels if include_empty or (not label.is_empty)] + + def get_label_ids(self, include_empty: bool = False) -> Set[ID]: + """Get a set of ID's of labels that are assigned to this annotation. + + Args: + include_empty (bool): set to True to include empty label (if exists) in the output. Defaults to False. + + Returns: + Set of label id's in annotation + """ + return {label.id_ for label in self.__labels if include_empty or (not label.is_empty)} + + def append_label(self, label: ScoredLabel) -> None: + """Appends the scored label to the annotation. + + Args: + label (ScoredLabel): the scored label to be appended to the annotation + """ + self.__labels.append(label) + + def set_labels(self, labels: List[ScoredLabel]) -> None: + """Sets the labels of the annotation to be the input of the function. + + Args: + labels (List[ScoredLabel]): the scored labels to be set as annotation labels + """ + self.__labels = labels + + def __eq__(self, other: object) -> bool: + """Checks if the two annotations are equal. + + Args: + other (Annotation): Annotation to compare with. + + Returns: + bool: True if the two annotations are equal, False otherwise. + """ + if isinstance(other, Annotation): + return ( + self.id_ == other.id_ and self.get_labels(True) == other.get_labels(True) and self.shape == other.shape + ) + return False + + +class AnnotationSceneKind(Enum): + """AnnotationSceneKinds for an Annotation object.""" + + #: NONE represents NULLAnnotationScene's (See :class:`NullAnnotationScene`) + NONE = 0 + #: ANNOTATION represents user annotation + ANNOTATION = 1 + #: PREDICTION represents analysis result, which will be shown to the user + PREDICTION = 2 + #: EVALUATION represents analysis result for evaluation purposes, which will NOT be shown to the user + EVALUATION = 3 + #: INTERMEDIATE represents intermediary state. + #: This is used when the analysis is being transferred from one task to another. + #: This will not be shown to the user. + #: This state will be changed to either PREDICTION or EVALUATION at the end of analysis process. + INTERMEDIATE = 4 + #: TASK_PREDICTION represents analysis results for a single task + TASK_PREDICTION = 5 + + def __str__(self): + """String representation of the AnnotationSceneKind.""" + return str(self.name) + + +class AnnotationSceneEntity(metaclass=abc.ABCMeta): + """This class represents a user annotation or a result (prediction). + + It serves as a collection of shapes, with a relation to the media entity. + + Example: + Creating an annotation: + + >>> from otx.api.entities.annotation import Annotation, AnnotationSceneEntity, AnnotationSceneKind + >>> from otx.api.entities.shapes.rectangle import Rectangle + >>> box = Rectangle(x1=0.0, y1=0.0, x2=0.5, y2=0.5) # Box covering top-left quart of image + >>> AnnotationSceneEntity(annotations=[Annotation(shape=box, labels=[])], kind=AnnotationSceneKind.ANNOTATION) + + Args: + annotations (List[Annotation]): List of annotations in the scene + kind (AnnotationSceneKind): Kind of the annotation scene. E.g. `AnnotationSceneKind.ANNOTATION`. + editor (str): The user that made this annotation scene object. + creation_date (Optional[datetime.datetime]): Creation date of annotation scene entity. If None, current time is + used. Defaults to None. + id (Optional[ID]): ID of AnnotationSceneEntity. If None a new `ID` is created. Defaults to None. + """ + + # pylint: disable=too-many-arguments, redefined-builtin + def __init__( + self, + annotations: List[Annotation], + kind: AnnotationSceneKind, + editor: str = "", + creation_date: Optional[datetime.datetime] = None, + id: Optional[ID] = None, + ): + self.__annotations = annotations + self.__kind = kind + self.__editor = editor + self.__creation_date = now() if creation_date is None else creation_date + self.__id_ = ID() if id is None else id + + def __repr__(self): + """String representation of the annotation scene.""" + return ( + f"{self.__class__.__name__}(" + f"annotations={self.annotations}, " + f"kind={self.kind}, " + f"editor={self.editor_name}, " + f"creation_date={self.creation_date}, " + f"id={self.id_})" + ) + + @property + def id_(self) -> ID: + """Returns the ID of the AnnotationSceneEntity.""" + return self.__id_ + + @id_.setter + def id_(self, value) -> None: + self.__id_ = value + + @property + def id(self): + """DEPRECATED.""" + return self.__id_ + + @id.setter + def id(self, value): + """DEPRECATED.""" + self.__id_ = value + + @property + def kind(self) -> AnnotationSceneKind: + """Returns the AnnotationSceneKind of the AnnotationSceneEntity.""" + return self.__kind + + @kind.setter + def kind(self, value) -> None: + self.__kind = value + + @property + def editor_name(self) -> str: + """Returns the editor's name that made the AnnotationSceneEntity object.""" + return self.__editor + + @editor_name.setter + def editor_name(self, value) -> None: + self.__editor = value + + @property + def creation_date(self) -> datetime.datetime: + """Returns the creation date of the AnnotationSceneEntity object.""" + return self.__creation_date + + @creation_date.setter + def creation_date(self, value) -> None: + self.__creation_date = value + + @property + def annotations(self) -> List[Annotation]: + """Return the Annotations that are present in the AnnotationSceneEntity.""" + return self.__annotations + + @annotations.setter + def annotations(self, value: List[Annotation]): + self.__annotations = value + + @property + def shapes(self) -> List[ShapeEntity]: + """Returns all shapes that are inside the annotations of the AnnotationSceneEntity.""" + return [annotation.shape for annotation in self.annotations] + + def contains_any(self, labels: List[LabelEntity]) -> bool: + """Checks whether the annotation contains any labels in the input parameter. + + Args: + labels (List[LabelEntity]): List of labels to compare to. + + Returns: + bool: True if there is any intersection between self.get_labels(include_empty=True) with labels. + """ + label_names = {label.name for label in labels} + return len({label.name for label in self.get_labels(include_empty=True)}.intersection(label_names)) != 0 + + def append_annotation(self, annotation: Annotation) -> None: + """Appends the passed annotation to the list of annotations present in the AnnotationSceneEntity object.""" + self.annotations.append(annotation) + + def append_annotations(self, annotations: List[Annotation]) -> None: + """Adds a list of annotations to the annotation scene.""" + self.annotations.extend(annotations) + + def get_labels(self, include_empty: bool = False) -> List[LabelEntity]: + """Returns a list of unique labels which appear in this annotation scene. + + Args: + include_empty (bool): Set to True to include empty label (if exists) in the output. Defaults to False. + + Returns: + List[LabelEntity]: a list of labels which appear in this annotation. + """ + + labels: Dict[str, LabelEntity] = {} + for annotation in self.annotations: + for label in annotation.get_labels(include_empty=include_empty): + id_ = label.id_ + if id_ not in labels: + labels[id_] = label.get_label() + return list(labels.values()) + + def get_label_ids(self, include_empty: bool = False) -> Set[ID]: + """Returns a set of the ID's of unique labels which appear in this annotation scene. + + Args: + include_empty (bool): Set to True to include empty label (if exists) in the output. Defaults to False. + + Returns: + Set[ID]: a set of the ID's of labels which appear in this annotation. + """ + + output: Set[ID] = set() + for annotation in self.annotations: + output.update(set(annotation.get_label_ids(include_empty=include_empty))) + return output + + +class NullAnnotationSceneEntity(AnnotationSceneEntity): + """Represents 'AnnotationSceneEntity not found.""" + + def __init__(self) -> None: + super().__init__( + id=ID(), + kind=AnnotationSceneKind.NONE, + editor="", + creation_date=datetime.datetime.now(), + annotations=[], + ) + + def __repr__(self): + """String representation NullAnnotationSceneEntity.""" + return "NullAnnotationSceneEntity()" diff --git a/src/otx/api/entities/color.py b/src/otx/api/entities/color.py new file mode 100644 index 00000000000..d1f56d071e2 --- /dev/null +++ b/src/otx/api/entities/color.py @@ -0,0 +1,169 @@ +"""This module define the color entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import abc +import random +from typing import Tuple + + +class ColorEntity(metaclass=abc.ABCMeta): + """This class represents an abstract Color, some functions are still abstract.""" + + def __init__(self, red: int, green: int, blue: int, alpha: int): + self.__red = red + self.__green = green + self.__blue = blue + self.__alpha = alpha + + @property + def red(self) -> int: + """Returns the red color value for the ColorEntity object.""" + return self.__red + + @red.setter + def red(self, value): + self.__red = value + + @property + def green(self) -> int: + """Returns the green color value for the ColorEntity object.""" + return self.__green + + @green.setter + def green(self, value): + self.__green = value + + @property + def blue(self) -> int: + """Returns the blue color value for the ColorEntity object.""" + return self.__blue + + @blue.setter + def blue(self, value): + self.__blue = value + + @property + def alpha(self) -> int: + """Returns the alpha value for the ColorEntity object.""" + return self.__alpha + + @alpha.setter + def alpha(self, value): + self.__alpha = value + + @property + @abc.abstractmethod + def hex_str(self) -> str: + """Returns the color in a Hex representation.""" + raise NotImplementedError + + @classmethod + @abc.abstractmethod + def from_hex_str(cls, string: str): + """Converts a hex string to a color. + + Args: + string (str): The hex string + """ + raise NotImplementedError + + @classmethod + @abc.abstractmethod + def random(cls): + """Generates a random Color.""" + raise NotImplementedError + + +class Color(ColorEntity): + """Represents an RGBA color.""" + + def __init__(self, red: int, green: int, blue: int, alpha: int = 255): + super().__init__(red=red, green=green, blue=blue, alpha=alpha) + + def __repr__(self): + """Returns string representation of the color.""" + return f"Color(red={self.red}, green={self.green}, blue={self.blue}, alpha={self.alpha})" + + def __eq__(self, other): + """Returns True if both colors are equal.""" + if isinstance(other, Color): + return ( + self.red == other.red + and self.green == other.green + and self.blue == other.blue + and self.alpha == other.alpha + ) + return False + + @property + def hex_str(self) -> str: + """Returns the color in a Hex representation.""" + return f"#{self.red:02x}{self.green:02x}{self.blue:02x}{self.alpha:02x}" + + @classmethod + def from_hex_str(cls, string: str) -> "Color": + """Creates Color() instance given a hex string. + + Supports 6 character hex string (RGB), or 8 character hex string (RGBA). + The string might optionally start with a number sign (#). + + Example: + Creating color object: + + >>> Color.from_hex_str("#ff0000") + Color(red=255, green=0, blue=0, alpha=255) + + >>> Color.from_hex_str("0000ff") + Color(red=0, green=0, blue=255, alpha=255) + + >>> Color.from_hex_str("#96Ff00C8") + Color(red=150, green=255, blue=0, alpha=200) + + Args: + string (str): Hex string + + Returns: + Color instance + """ + string = string.lstrip("#").lower() + if len(string) < 8: + # If alpha channel misses, add it + string += (8 - len(string)) * "f" + red, green, blue, alpha = tuple(int(string[i : i + 2], 16) for i in (0, 2, 4, 6)) + return cls(red=red, green=green, blue=blue, alpha=alpha) + + @classmethod + def random(cls) -> "Color": + """Generate random Color() instance. + + Returns: + Color instance with random color + """ + red, green, blue = ( + # disable B311 random - used for the random sampling not for security/crypto + random.randint(0, 255), # nosec B311 + random.randint(0, 255), # nosec B311 + random.randint(0, 255), # nosec B311 + ) + return cls(red=red, green=green, blue=blue, alpha=255) + + @property + def rgb_tuple(self) -> Tuple[int, int, int]: + """Retrieves the Color as a RGB tuple. + + Returns: + Tuple[int, int, int] + """ + return self.red, self.green, self.blue + + @property + def bgr_tuple(self) -> Tuple[int, int, int]: + """Retrieves the Color as a BGR tuple. + + Returns: + Tuple[int, int, int] + """ + return self.blue, self.green, self.red diff --git a/src/otx/api/entities/coordinate.py b/src/otx/api/entities/coordinate.py new file mode 100644 index 00000000000..2d743282a70 --- /dev/null +++ b/src/otx/api/entities/coordinate.py @@ -0,0 +1,42 @@ +"""This module implements the Coordinate entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from typing import Tuple + + +class Coordinate: + """Represents a 2D-coordinate with an x-position and a y-position. + + NB most coordinates are normalized (between 0.0 and 1.0) + + Args: + x (float): x-coordinate + y (float): y-coordinate + """ + + __slots__ = ["x", "y"] + + def __init__(self, x: float, y: float): + self.x = x + self.y = y + + def __repr__(self): + """String representation of the coordinate. Returns the x and y coordinates.""" + return f"Coordinate(x={self.x}, y={self.y})" + + def __eq__(self, other): + """Returns True if the coordinates are equal.""" + return self.x == other.x and self.y == other.y + + def __hash__(self): + """Returns the hash of the coordinate.""" + return hash(str(self)) + + def as_tuple(self) -> Tuple[float, float]: + """Convert the coordinates to a pair (x,y).""" + return self.x, self.y + + def as_int_tuple(self) -> Tuple[int, int]: + """Convert the coordinates to a pair of integer coordinates (x,y).""" + return int(self.x), int(self.y) diff --git a/src/otx/api/entities/dataset_item.py b/src/otx/api/entities/dataset_item.py new file mode 100644 index 00000000000..77a0d119fe5 --- /dev/null +++ b/src/otx/api/entities/dataset_item.py @@ -0,0 +1,550 @@ +"""This module implements the dataset item entity.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# pylint: disable=cyclic-import + +import abc +import copy +from inspect import signature +import itertools +from threading import Lock +from typing import List, Optional, Sequence, Set, Tuple, TypeVar, Union +from bson import ObjectId +import numpy as np + +from otx.utils.logger import get_logger +from otx.api.entities.annotation import Annotation, AnnotationSceneEntity +from otx.api.entities.id import ID +from otx.api.entities.label import LabelEntity +from otx.api.entities.media import IMedia2DEntity +from otx.api.entities.metadata import IMetadata, MetadataItemEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.utils.shape_factory import ShapeFactory + +logger = get_logger() + + +T = TypeVar("T", bound="DatasetItemEntity") + + +class DatasetItemEntity(metaclass=abc.ABCMeta): + """DatasetItemEntity represents an item in the DatasetEntity. + + It holds a media item, annotation and an ROI. The ROI determines the region of interest for the dataset item, and + is described by a shape entity. + + The fundamental properties of a dataset item are: + + - A 2d media entity (e.g. Image) + - A 2d annotation entity for the full resolution media entity + - An ROI, describing the region of interest. + - The subset it belongs to + - Metadata for the media entity (e.g. saliency map or active score) + - A list of labels to ignore + + .. rubric:: Getting data from dataset item + + The first step is to fetch the input data for the network. + + >>> dataset_item = DatasetItemEntity() + >>> media_numpy = dataset_item.numpy # RGB media data (Height, Width, Channels) + + This returns the numpy data for the assigned ROI. But it is possible to extract any arbitrary region. + + >>> from otx.api.entities.shapes.rectangle import Rectangle + >>> top_left_quart_roi = Annotation(Rectangle(x1=0.0, y1=0.0, x2=0.5, y2=0.5), labels=[]) + >>> top_left_quart_numpy = dataset_item.roi_numpy(roi=top_left_quart_roi) + + Get the subset of labels for the item ROI: + + >>> labels = dataset_item.get_roi_labels(labels=...) + + Get the annotations __visible__ in the ROI: + + >>> dataset_item.get_annotations() + + .. rubric:: Adding output data to dataset item + + It is possible to add shapes or just labels for the ROI. + + Add shapes to dataset item: + + >>> box = Rectangle(x1=0.2, y1=0.3, x2=0.6, y2=0.5) + >>> dataset_item.append_annotations(annotations=[Annotation(box, labels=[...])]) + + Add labels to ROI: + + >>> dataset_item.append_labels(labels=[...]) + + Args: + media (IMedia2DEntity): Media item + annotation_scene (AnnotationSceneEntity): Annotation scene + roi (Optional[Annotation]): Region Of Interest + metadata (Optional[List[MetadataItemEntity]]): Metadata attached to dataset item + subset (Subset): `Subset` for item. E.g. `Subset.VALIDATION` + ignored_labels (Optional[Union[List[LabelEntity], Tuple[LabelEntity, ...], Set[LabelEntity]]]): Collection of + labels that should be ignored in this dataset item. For instance, in a training scenario, this parameter is + used to ignore certain labels within the existing annotations because their status becomes uncertain + following a label schema change. + """ + + # pylint: disable=too-many-arguments + def __init__( + self, + media: IMedia2DEntity, + annotation_scene: AnnotationSceneEntity, + roi: Optional[Annotation] = None, + metadata: Optional[List[MetadataItemEntity]] = None, + subset: Subset = Subset.NONE, + ignored_labels: Optional[Union[List[LabelEntity], Tuple[LabelEntity, ...], Set[LabelEntity]]] = None, + ): + self.__media: IMedia2DEntity = media + self.__annotation_scene: AnnotationSceneEntity = annotation_scene + self.__subset: Subset = subset + self.__roi_lock = Lock() + + # set ROI + if roi is None: + for annotation in annotation_scene.annotations: + # if there is a full box in annotation.shapes, set it as ROI + if Rectangle.is_full_box(annotation.shape): + roi = annotation + break + if roi is None: + roi = Annotation(Rectangle.generate_full_box(), labels=[]) + self.__roi = roi + + self.__metadata: List[MetadataItemEntity] = [] + if metadata is not None: + self.__metadata = metadata + + self.__ignored_labels: Set[LabelEntity] = set() if ignored_labels is None else set(ignored_labels) + + def set_metadata(self, metadata: List[MetadataItemEntity]): + """Sets the metadata.""" + self.__metadata = metadata + + def get_metadata(self) -> List[MetadataItemEntity]: + """Returns the metadata.""" + return self.__metadata + + @property + def ignored_labels(self) -> Set[LabelEntity]: + """Get the IDs of the labels to ignore in this dataset item.""" + return self.__ignored_labels + + @ignored_labels.setter + def ignored_labels(self, value: Union[List[LabelEntity], Tuple[LabelEntity, ...], Set[LabelEntity]]): + self.__ignored_labels = set(value) + + def __repr__(self): + """String representation of the dataset item.""" + return ( + f"{self.__class__.__name__}(" + f"media={self.media}, " + f"annotation_scene={self.annotation_scene}, " + f"roi={self.roi}, " + f"subset={self.subset}), " + f"meta={self.get_metadata()}" + ) + + @property + def roi(self) -> Annotation: + """Region Of Interest.""" + with self.__roi_lock: + return self.__roi + + @roi.setter + def roi(self, roi: Optional[Annotation]): + with self.__roi_lock: + if roi is None: + roi = Annotation(Rectangle.generate_full_box(), labels=[]) + self.__roi = roi + + @property + def subset(self) -> Subset: + """Returns the subset that the IDatasetItem belongs to. e.g. Subset.TRAINING.""" + return self.__subset + + @subset.setter + def subset(self, value: Subset): + self.__subset = value + + @property + def media(self) -> IMedia2DEntity: + """Media.""" + return self.__media + + def roi_numpy(self, roi: Optional[Annotation] = None) -> np.ndarray: + """Gives the numpy data for the media, given an ROI. + + This function allows to take a crop of any arbitrary region of the media in the Dataset entity. + If the ROI is not given, the ROI assigned to the DatasetItem will be used as default. + + Args: + roi (Optional[Annotation]): Shape entity. The shape will be converted if needed, to extract the ROI numpy. + + Returns: + np.ndarray: Numpy array with media data + """ + if roi is None: + roi = self.roi + + if roi is not None: + roi.shape = ShapeFactory.shape_as_rectangle(roi.shape) + + return self.media.roi_numpy(roi=roi) + + @property + def numpy(self) -> np.ndarray: + """Returns the numpy data for the media, taking ROI into account. + + Returns: + np.ndarrray: Numpy array. RGB array of shape (Height, Width, Channels) + """ + return self.roi_numpy() + + @property + def width(self) -> int: + """The width of the dataset item, taking into account the ROI.""" + roi_shape_as_box = ShapeFactory.shape_as_rectangle(self.roi.shape) + roi_shape_as_box = roi_shape_as_box.clip_to_visible_region() + width = self.media.width + + # Note that we cannot directly use roi_shape_as_box.width due to the rounding + # because round(x2 - x1) is not always equal to round(x2) - round(x1) + x1 = int(round(roi_shape_as_box.x1 * width)) + x2 = int(round(roi_shape_as_box.x2 * width)) + return x2 - x1 + + @property + def height(self) -> int: + """The height of the dataset item, taking into account the ROI.""" + roi_shape_as_box = ShapeFactory.shape_as_rectangle(self.roi.shape) + roi_shape_as_box = roi_shape_as_box.clip_to_visible_region() + height = self.media.height + + # Note that we cannot directly use roi_shape_as_box.height due to the rounding + # because round(y2 - y1) is not always equal to round(y2) - round(y1) + y1 = int(round(roi_shape_as_box.y1 * height)) + y2 = int(round(roi_shape_as_box.y2 * height)) + return y2 - y1 + + @property + def annotation_scene(self) -> AnnotationSceneEntity: + """Access to annotation scene.""" + return self.__annotation_scene + + @annotation_scene.setter + def annotation_scene(self, value: AnnotationSceneEntity): + self.__annotation_scene = value + + def get_annotations( + self, + labels: Optional[List[LabelEntity]] = None, + include_empty: bool = False, + include_ignored: bool = False, + preserve_id: bool = False, + ) -> List[Annotation]: + """Returns a list of annotations that exist in the dataset item (wrt. ROI). + + This is done by checking that the center of the annotation is located in the ROI. + + Args: + labels (Optional[LabelEntity]): Subset of input labels to filter with; if ``None``, all the shapes within + the ROI are returned. + include_empty (bool): if True, returns both empty and non-empty labels + include_ignored (bool): if True, includes the labels in ignored_labels + preserve_id (bool): if True, preserve the annotation id when copying + + Returns: + List[Annotation]: The intersection of the input label set and those present within the ROI + """ + is_full_box = Rectangle.is_full_box(self.roi.shape) + annotations = [] + if is_full_box and labels is None and include_empty and include_ignored: + # Fast path for the case where we do not need to change the shapes + annotations = self.annotation_scene.annotations + else: + # Todo: improve speed. This is O(n) for n shapes. + roi_as_box = ShapeFactory.shape_as_rectangle(self.roi.shape) + + labels_set = {label.name for label in labels} if labels is not None else set() + + for annotation in self.annotation_scene.annotations: + if not is_full_box and not self.roi.shape.contains_center(annotation.shape): + continue + + shape_labels = annotation.get_labels(include_empty) + + check_labels = False + if not include_ignored: + shape_labels = [label for label in shape_labels if label.label not in self.ignored_labels] + check_labels = True + + if labels is not None: + shape_labels = [label for label in shape_labels if label.name in labels_set] + check_labels = True + + if check_labels and len(shape_labels) == 0: + continue + + if not is_full_box: + # Create a denormalized copy of the shape. + shape = annotation.shape.denormalize_wrt_roi_shape(roi_as_box) + else: + # Also create a copy of the shape, so that we can safely modify the labels + # without tampering with the original shape. + shape = copy.deepcopy(annotation.shape) + + annotations.append( + Annotation( + shape=shape, + labels=shape_labels, + id=annotation.id_ if preserve_id else None, + ) + ) + return annotations + + def append_annotations(self, annotations: Sequence[Annotation]): + """Adds a list of shapes to the annotation.""" + roi_as_box = ShapeFactory.shape_as_rectangle(self.roi.shape) + + validated_annotations = [ + Annotation( + shape=annotation.shape.normalize_wrt_roi_shape(roi_as_box), + labels=annotation.get_labels(), + ) + for annotation in annotations + if ShapeFactory().shape_produces_valid_crop( + shape=annotation.shape, + media_width=self.media.width, + media_height=self.media.height, + ) + ] + + n_invalid_shapes = len(annotations) - len(validated_annotations) + if n_invalid_shapes > 0: + logger.info( + "%d shapes will not be added to the dataset item as they " + "would produce invalid crops (this is expected for some tasks, " + "such as segmentation).", + n_invalid_shapes, + ) + + self.annotation_scene.append_annotations(validated_annotations) + + def get_roi_labels( + self, + labels: Optional[List[LabelEntity]] = None, + include_empty: bool = False, + include_ignored: bool = False, + ) -> List[LabelEntity]: + """Return the subset of the input labels which exist in the dataset item (wrt. ROI). + + Args: + labels (Optional[List[LabelEntity]]): Subset of input labels to filter with; if ``None``, all the labels + within the ROI are returned. + include_empty (bool): if True, returns both empty and non-empty labels + include_ignored (bool): if True, includes the labels in ignored_labels + + Return: + List[LabelEntity]: The intersection of the input label set and those present within the ROI. + """ + filtered_labels = set() + for label in self.roi.get_labels(include_empty): + if labels is None or label.get_label() in labels: + filtered_labels.add(label.get_label()) + if not include_ignored: + filtered_labels -= self.ignored_labels + return sorted(list(filtered_labels), key=lambda x: x.name) + + def get_shapes_labels( + self, + labels: Optional[List[LabelEntity]] = None, + include_empty: bool = False, + include_ignored: bool = False, + ) -> List[LabelEntity]: + """Get the labels of the shapes present in this dataset item. + + if a label list is supplied, only labels present within that list are returned. if include_empty is True, + present empty labels are returned as well. + + Args: + labels (Optional[List[LabelEntity]]): if supplied only labels present in this list are returned. + Defaults to None. + include_empty (bool): if True, returns both empty and non-empty labels. Defaults to False. + include_ignored (bool): if True, includes the labels in ignored_labels. Defaults to False. + + Returns: + List[LabelEntity]: a list of labels from the shapes within the roi of this dataset item + """ + annotations = self.get_annotations(labels=labels, include_empty=include_empty, include_ignored=include_ignored) + scored_label_set = set(itertools.chain(*[annotation.get_labels(include_empty) for annotation in annotations])) + label_set = {scored_label.get_label() for scored_label in scored_label_set} + if not include_ignored: + label_set -= self.ignored_labels + if labels is None: + return list(label_set) + return [label for label in label_set if label in labels] + + def append_labels(self, labels: List[ScoredLabel]): + """Appends labels to the DatasetItem and adds it to the the annotation label as well if it's not yet there. + + Args: + labels (List[ScoredLabel]): list of labels to be appended. + """ + if len(labels) == 0: + return + + roi_annotation = None + for annotation in self.annotation_scene.annotations: + if annotation.shape == self.roi.shape: + roi_annotation = annotation + break + + if roi_annotation is None: # no annotation found with shape + roi_annotation = self.roi + self.annotation_scene.append_annotation(roi_annotation) + + for label in labels: + if label not in self.roi.get_labels(include_empty=True): + self.roi.append_label(label) + if label not in roi_annotation.get_labels(include_empty=True): + roi_annotation.append_label(label) + + def __eq__(self, other): + """Compares if two DatasetItems are equal. + + Args: + other ("DatasetItems"): other DatasetItem to compare with. + + Returns: + bool: True if equal, False otherwise. + """ + if isinstance(other, DatasetItemEntity): + return ( + self.media == other.media + and self.annotation_scene == other.annotation_scene + and self.roi == other.roi + and self.subset == other.subset + and self.ignored_labels == other.ignored_labels + ) + return False + + def __deepcopy__(self, memo): + """Avoids copying the lock and unintentional ID sharing among AnnotationSceneEntity instances. + + When we deepcopy this object, be sure not to deep copy the lock, as this is not possible, + make a new lock instead. In addition, we prevent deepcopy of AnnotationSceneEntity member + variable to avoid unintentional ID sharing among instances. Same instance reference is + copied to the output instead. + """ + # Call ROI getter to ensure original object has an ROI. + _ = self.roi + + clone = copy.copy(self) + + for name, value in vars(self).items(): + if "__roi_lock" in name: + setattr(clone, name, Lock()) + elif "__annotation_scene" in name: + pass # Keep the same instance + else: + setattr(clone, name, copy.deepcopy(value, memo)) + return clone + + def append_metadata_item(self, data: IMetadata, model: Optional[ModelEntity] = None): + """Appends metadata produced by some model to the dataset item. + + .. rubric:: Adding visualization heatmap (ResultMediaEntity) to DatasetItemEntity + + >>> from otx.api.entities.image import Image + >>> from otx.api.entities.result_media import ResultMediaEntity + >>> media = Image(file_path='image.jpeg') + >>> annotation = NullAnnotationSceneEntity() + >>> dataset_item = DatasetItem(media=media, annotation_scene=annotation) + >>> data = np.ones((120, 120, 3)).astype(np.uint8) * 255 # Saliency numpy + >>> result_media = ResultMediaEntity(name="Gradcam++", + ... type="Gradcam++", + ... annotation_scene=annotation, + ... numpy=data) + >>> dataset_item.append_metadata_item(result_media) + + .. rubric:: Representation vector for active learning + + >>> from otx.api.entities.tensor import TensorEntity + >>> tensor = TensorEntity(name="representation_vector", numpy=data) + >>> dataset_item.append_metadata_item(data=tensor, model=model) + + Args: + data (IMetadata): any object of a class inherited from IMetadata. (e.g., FloatMetadata, Tensor) + model (Optional[ModelEntity]): model that was used to generated metadata + """ + self.__metadata.append(MetadataItemEntity(data=data, model=model)) + + def get_metadata_by_name_and_model(self, name: str, model: Optional[ModelEntity]) -> Sequence[MetadataItemEntity]: + """Returns a metadata item with `name` and generated by `model`. + + Args: + name (str): the name of the metadata + model (Optional[ModelEntity]): the model which was used to generate the metadata. + + Returns: + Sequence[MetadataItemEntity]: a list of metadata items with `name` and generated by `model`. + """ + return [meta for meta in self.get_metadata() if meta.data.name == name and meta.model == model] + + def wrap(self: T, **kwargs) -> T: + """Creates a new DatasetItemEntity, overriding only the given arguments to the existing ones for this instance.""" + params = { + name: getattr(self, name) + for name in signature(self.__class__.__init__).parameters.keys() + if hasattr(self, name) + } + params.update({"metadata": self.get_metadata()}) + params.update(**kwargs) + + return self.__class__(**params) + + +# TODO: This should be removed in the near future and DatasetItemEntity should have id_ field. +class DatasetItemEntityWithID(DatasetItemEntity): + def __init__( + self, + media: IMedia2DEntity, + annotation_scene: AnnotationSceneEntity, + roi: Optional[Annotation] = None, + metadata: Optional[List[MetadataItemEntity]] = None, + subset: Subset = Subset.NONE, + ignored_labels: Optional[Union[List[LabelEntity], Tuple[LabelEntity, ...], Set[LabelEntity]]] = None, + id_: Optional[Union[str, ObjectId]] = None, + ): + super().__init__(media, annotation_scene, roi, metadata, subset, ignored_labels) + self._id_ = ID(id_) if id_ is not None else ID(ObjectId()) + + @property + def id_(self) -> ID: + """ + Returns the id of this entity + + :return: ID of this entity + """ + return self._id_ + + @id_.setter + def id_(self, value: ID) -> None: + """ + Set the unique id for this entity + + :param value: a unique ID to set + """ + self._id_ = value + + def __eq__(self, other): + return super().__eq__(other) and self.id_ == other.id_ diff --git a/src/otx/api/entities/datasets.py b/src/otx/api/entities/datasets.py new file mode 100644 index 00000000000..aa5f82f373b --- /dev/null +++ b/src/otx/api/entities/datasets.py @@ -0,0 +1,449 @@ +"""This module implements the Dataset entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=redefined-builtin, invalid-name + +import collections.abc +import copy +import itertools +import logging +from enum import Enum +from typing import Generic, Iterator, List, Optional, TypeVar, Union, cast, overload + +from bson.objectid import ObjectId + +from otx.utils.logger import get_logger +from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.id import ID +from otx.api.entities.label import LabelEntity +from otx.api.entities.subset import Subset + +logger = get_logger() + + +class DatasetPurpose(Enum): + """Describes the purpose for the dataset. + + This makes it possible to identify datasets for a particular use. + """ + + #: used for user inference. + # Prediction will generate AnnotationSceneKind.PREDICTION + INFERENCE = 0 + #: used for training. + # AnnotationScene contains user annotation of type AnnotationSceneKind.ANNOTATION + TRAINING = 1 + #: used for pre-evaluation, evaluation, or testing. + # Prediction will generate AnnotationSceneKind.EVALUATION + EVALUATION = 2 + #: used for generating output stage. + # Prediction will generate AnnotationSceneKind.PREDICTION + GENERATING_OUTPUT = 3 + #: used for dataset slices which are used for analysis. + # Prediction will generate AnnotationSceneKind.INTERMEDIATE + TEMPORARY_DATASET = 4 + #: used for task analysis. + # Prediction will generate AnnotationSceneKind.TASK_PREDICTION + TASK_INFERENCE = 5 + + def __str__(self): + """Returns the dataset purpose as string.""" + return str(self.name) + + +class DatasetIterator(collections.abc.Iterator): + """This DatasetIterator iterates over the dataset lazily. + + Implements collections.abc.Iterator. + + Args: + dataset (DatasetEntity): Dataset to iterate over. + """ + + def __init__(self, dataset: "DatasetEntity"): + self.dataset = dataset + self.index = 0 + + def __next__(self) -> DatasetItemEntity: + """Returns the next dataset item. + + Raises: + StopIteration: if the end of the dataset is reached. + + Returns: + DatasetItemEntity: Dataset item. + """ + if self.index >= len(self.dataset): + raise StopIteration + item = self.dataset[self.index] + self.index += 1 + return item + + +TDatasetItemEntity = TypeVar("TDatasetItemEntity", bound="DatasetItemEntity") + + +class DatasetEntity(Generic[TDatasetItemEntity]): + """A dataset consists of a list of DatasetItemEntities and a purpose. + + ## With dataset items + + This way assumes the dataset item entities are constructed before the dataset entity is made. + + >>> from otx.api.entities.image import Image + >>> from otx.api.entities.annotation import NullAnnotationSceneEntity + >>> from otx.api.entities.dataset_item import DatasetItemEntity + >>> item = DatasetItemEntity(media=Image(file_path="image.jpg"), annotation_scene=NullAnnotationSceneEntity()) + >>> dataset = DatasetEntity(items=[item]) + + ## Iterate over dataset + + Regardless of the instantiation method chosen, the Dataset will work the same. + The dataset can be iterated: + + >>> dataset = DatasetEntity(items=[item_1]) + >>> for dataset_item in dataset: + ... print(dataset_item) + DatasetItemEntity( + media=Image(image.jpg, width=640, height=480), + annotation_scene=NullAnnotationSceneEntity(), + roi=Annotation( + shape=Rectangle( + x=0.0, + y=0.0, + width=1.0, + height=1.0 + ), + labels=[], + id=6149e454893b7ebbe3a8faf6 + ), + subset=NONE + ) + + A particular item can also be fetched: + + >>> first_item = dataset[0] + + Or a slice: + + >>> first_ten = dataset[:10] + >>> last_ten = dataset[-10:] + + ## Get a subset of Dataset + + To get the test data for validating the network: + + >>> dataset = DatasetEntity() + >>> testing_subset = dataset.get_subset(Subset.TESTING) + + This subset is also a DatasetEntity. The entities in the subset dataset refer to the same entities as + in the original dataset. Altering one of the objects in the subset, will also alter them in the original. + + Args: + items (Optional[List[DatasetItemEntity]]): A list of dataset items to create dataset with. Defaults to None. + purpose (DatasetPurpose): Purpose for dataset. Refer to :class:`DatasetPurpose` for more info. + Defaults to DatasetPurpose.INFERENCE. + """ + + def __init__( + self, + items: Optional[List[TDatasetItemEntity]] = None, + purpose: DatasetPurpose = DatasetPurpose.INFERENCE, + ): + self._items = [] if items is None else items + self._purpose = purpose + + @property + def purpose(self) -> DatasetPurpose: + """Returns the DatasetPurpose. For example DatasetPurpose.ANALYSIS. + + Returns: + DatasetPurpose + """ + return self._purpose + + @purpose.setter + def purpose(self, value: DatasetPurpose) -> None: + self._purpose = value + + def _fetch(self, key: Union[slice, int]) -> Union[DatasetItemEntity, List[DatasetItemEntity]]: + """Fetch the given entity/entities from the items. + + Helper function for __getitem__ + + Args: + key (Union[slice, int]): Key called on the dataset. E.g. int (dataset[0]) or slice (dataset[5:9]) + + Raises: + TypeError: If key is not a slice or int. + + Returns: + Union[DatasetItemEntity, List[DatasetItemEntity]]: The entity/entities requested. + """ + if isinstance(key, list): + return [self._fetch(ii) for ii in key] # type: ignore + if isinstance(key, slice): + # Get the start, stop, and step from the slice + return [self._fetch(ii) for ii in range(*key.indices(len(self._items)))] # type: ignore + if isinstance(key, int): + return self._items[key] + raise TypeError( + f"Instance of type `{type(key).__name__}` cannot be used to access Dataset items. " + f"Only slice and int are supported" + ) + + def __repr__(self): + """Returns string representation of the dataset.""" + return f"{self.__class__.__name__}(items={self._items}, purpose={self.purpose})" + + def __str__(self): + """Returns string representation of the dataset.""" + return f"{self.__class__.__name__}(size={len(self)}, purpose={self.purpose})" + + def __len__(self): + """Returns the number of items in the dataset.""" + return len(self._items) + + def __eq__(self, other: object) -> bool: + """Checks whether the dataset is equal to the operand. + + Args: + other (DatasetEntity): the dataset operand to the equal operator. + + Returns: + bool: True if the datasets are equal + """ + if isinstance(other, DatasetEntity) and len(self) == len(other) and self.purpose == other.purpose: + return all(self[i] == other[i] for i in range(len(self))) + return False + + def __add__(self, other: Union["DatasetEntity", List[DatasetItemEntity]]) -> "DatasetEntity": + """Returns a new dataset which contains the items of self added with the input dataset. + + Note that additional info of the dataset might be incoherent to the addition operands. + + Args: + other (Union[DatasetEntity, List[DatasetItemEntity]]): dataset to be added to output + + Returns: + DatasetEntity: new dataset with the items of self added with the input dataset + """ + items: List[DatasetItemEntity] + + if isinstance(other, DatasetEntity): + items = self._items + list(other) + elif isinstance(other, list): + items = self._items + [o for o in other if isinstance(o, DatasetItemEntity)] + else: + raise ValueError(f"Cannot add other of type {type(other)}") + + return DatasetEntity(items=items, purpose=self.purpose) + + @overload + def __getitem__(self, key: int) -> DatasetItemEntity: + """Returns the item at the given index.""" + return cast(DatasetItemEntity, self._fetch(key)) + + @overload # Overload for proper type hinting of indexing on dataset. + def __getitem__(self, key: slice) -> List[DatasetItemEntity]: + """Returns items for the given slice.""" + return cast(List[DatasetItemEntity], self._fetch(key)) + + def __getitem__(self, key: Union[slice, int]) -> Union["DatasetItemEntity", List["DatasetItemEntity"]]: + """Return a DatasetItemEntity or a list of DatasetItemEntity, given a slice or an integer. + + Example: + Given an integer index: + + >>> dataset = DatasetEntity(items=[...]) + >>> first_item = dataset[0] + + Or a slice: + + >>> first_ten = dataset[0:9] + >>> last_ten = dataset[-9:] + + Args: + key (Union[slice, int]): key to fetch. Should be `slice` or `int` + + Returns: + Union["DatasetItemEntity", List["DatasetItemEntity"]]: List of DatasetItemEntity or single DatasetItemEntity + """ + return self._fetch(key) + + def __iter__(self) -> Iterator[TDatasetItemEntity]: + """Return an iterator for the DatasetEntity. + + This iterator is able to iterate over the DatasetEntity lazily. + + Returns: + DatasetIterator: DatasetIterator instance + """ + return DatasetIterator(self) + + def with_empty_annotations( + self, annotation_kind: AnnotationSceneKind = AnnotationSceneKind.PREDICTION + ) -> "DatasetEntity": + """Produces a new dataset with empty annotation objects (no shapes or labels). + + This is a convenience function to generate a dataset with empty annotations from another dataset. + This is particularly useful for evaluation on validation data and to build resultsets. + + Assume a dataset containing user annotations. + + >>> labeled_dataset = Dataset() # user annotated dataset + + Then, we want to see the performance of our task on this labeled_dataset, + which means we need to create a new dataset to be passed for analysis. + + >>> prediction_dataset = labeled_dataset.with_empty_annotations() + + Later, we can pass this prediction_dataset to the task analysis function. + By pairing the labeled_dataset and the prediction_dataset, the resultset can then be constructed. + Refer to `otx.api.entities.resultset.ResultSetEntity` for more info. + + Args: + annotation_kind (AnnotationSceneKind): Sets the empty annotation to this kind. + Defaults to AnnotationSceneKind.PREDICTION + + Returns: + DatasetEntity: a new dataset containing the same items, with empty annotation objects. + """ + new_dataset = DatasetEntity[TDatasetItemEntity](purpose=self.purpose) + + for dataset_item in self: + if isinstance(dataset_item, DatasetItemEntity): + empty_annotation = AnnotationSceneEntity(annotations=[], kind=annotation_kind) + + # reset ROI + roi = copy.copy(dataset_item.roi) + roi.id_ = ID(ObjectId()) + roi.set_labels([]) + + new_dataset_item = dataset_item.wrap( + media=dataset_item.media, + annotation_scene=empty_annotation, + roi=roi, + subset=dataset_item.subset, + metadata=dataset_item.get_metadata(), + ) + new_dataset.append(new_dataset_item) + return new_dataset + + def get_combined_subset(self, subsets: List[Subset]) -> "DatasetEntity": + """Returns a new DatasetEntity with just the dataset items matching the subsets. + + These subsets are DatasetEntity. The dataset items in the subset datasets are the same dataset items as + in the original dataset. + Altering one of the objects in the output of this function, will also alter them in the original. + + Example: + >>> dataset = DatasetEntity() + >>> training_subset = dataset.get_combined_subset([Subset.TRAINING, Subset.UNLABELED]) + + Args: + subsets (List): List of subsets to return. + + Returns: + DatasetEntity: DatasetEntity with items matching subsets + """ + to_keep = set(subsets) + dataset = DatasetEntity( + items=[item for item in self if item.subset in to_keep], + purpose=self.purpose, + ) + return dataset + + def get_subset(self, subset: Subset) -> "DatasetEntity": + """Returns a new DatasetEntity with just the dataset items matching the subset. + + This subset is also a DatasetEntity. The dataset items in the subset dataset are the same dataset items as + in the original dataset. + Altering one of the objects in the output of this function, will also alter them in the original. + + Example: + >>> dataset = DatasetEntity() + >>> training_subset = dataset.get_subset(Subset.TRAINING) + + Args: + subset (Subset): `Subset` to return. + + Returns: + DatasetEntity: DatasetEntity with items matching subset + """ + dataset = DatasetEntity( + items=[item for item in self._items if item.subset == subset], + purpose=self.purpose, + ) + return dataset + + def remove(self, item: TDatasetItemEntity) -> None: + """Remove an item from the items. + + This function calls remove_at_indices function. + + Args: + item (DatasetItemEntity): the item to be deleted. + + Raises: + ValueError: if the input item is not in the dataset + """ + index = self._items.index(item) + self.remove_at_indices([index]) + + def append(self, item: TDatasetItemEntity) -> None: + """Append a DatasetItemEntity to the dataset. + + Example: + Appending a dataset item to a dataset + + >>> from otx.api.entities.image import Image + >>> from otx.api.entities.annotation import NullAnnotationSceneEntity + >>> from otx.api.entities.dataset_item import DatasetItemEntity + >>> dataset = DatasetEntity() + >>> media = Image(file_path='image.jpg') + >>> annotation = NullAnnotationSceneEntity() + >>> dataset_item = DatasetItemEntity(media=media, annotation_scene=annotation) + >>> dataset.append(dataset_item) + + Args: + item (DatasetItemEntity): item to append + """ + + if item.media is None: + raise ValueError("Media in dataset item cannot be None") + self._items.append(item) + + def sort_items(self) -> None: + """Order the dataset items. Does nothing here, but may be overridden in child classes. + + Returns: + None + """ + + def remove_at_indices(self, indices: List[int]) -> None: + """Delete items based on the `indices`. + + Args: + indices (List[int]): the indices of the items that will be deleted from the items. + """ + indices.sort(reverse=True) # sort in descending order + for i_item in indices: + del self._items[i_item] + + def get_labels(self, include_empty: bool = False) -> List[LabelEntity]: + """Returns the list of all unique labels that are in the dataset. + + Note: This does not respect the ROI of the dataset items. + + Args: + include_empty (bool): set to True to include empty label (if exists) in the output. Defaults to False. + + Returns: + List[LabelEntity]: list of labels that appear in the dataset + """ + label_set = set(itertools.chain(*[item.annotation_scene.get_labels(include_empty) for item in self])) + return list(label_set) diff --git a/src/otx/api/entities/explain_parameters.py b/src/otx/api/entities/explain_parameters.py new file mode 100644 index 00000000000..1cf0fa54d8a --- /dev/null +++ b/src/otx/api/entities/explain_parameters.py @@ -0,0 +1,33 @@ +"""This module define the Explain entity.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from dataclasses import dataclass +from typing import Any, Callable, Optional + + +# pylint: disable=unused-argument +def default_progress_callback(progress: int, score: Optional[float] = None): + """This is the default progress callback for OptimizationParameters.""" + + +@dataclass +class ExplainParameters: + """Explain parameters. + + Attributes: + explainer: Explain algorithm to be used in explanation mode. + Will be converted automatically to lowercase. + process_saliency_maps: Processing of saliency map includes (1) resize to input image resolution + and (2) apply a colormap. + explain_predicted_classes: Provides explanations only for predicted classes. + Otherwise, explain all classes. + """ + + update_progress: Callable[[int, Optional[float]], Any] = default_progress_callback + + explainer: str = "" + process_saliency_maps: bool = False + explain_predicted_classes: bool = True diff --git a/src/otx/api/entities/graph.py b/src/otx/api/entities/graph.py new file mode 100644 index 00000000000..d5ac8ed164b --- /dev/null +++ b/src/otx/api/entities/graph.py @@ -0,0 +1,146 @@ +"""This module implements the TrainParameters entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Union + +import networkx as nx + +from otx.api.entities.interfaces.graph_interface import IGraph + + +class Graph(IGraph): + """The concrete implementation of IGraph. This implementation is using networkx library. + + Args: + directed (bool): set to True if the graph is a directed graph. + """ + + def __init__(self, directed: bool = False): + self._graph: Union[nx.Graph, nx.MultiDiGraph] = nx.Graph() if not directed else nx.MultiDiGraph() + self.directed = directed + + def get_graph(self) -> Union[nx.Graph, nx.MultiDiGraph]: + """Get the underlying NetworkX graph.""" + return self._graph + + def set_graph(self, graph: Union[nx.Graph, nx.MultiDiGraph]): + """Set the underlying NetworkX graph.""" + self._graph = graph + + def add_edge(self, node1, node2, edge_value=None): + """Adds edge between node1 and node2.""" + # pylint: disable=arguments-differ + self._graph.add_edge(node1, node2, value=edge_value) + + def num_nodes(self) -> int: + """Returns the number of nodes in the graph.""" + return self._graph.number_of_nodes() + + def add_node(self, node): + """Adds node to the graph.""" + if node not in self._graph.nodes: + self._graph.add_node(node) + + def has_edge_between(self, node1, node2): + """Returns True if there is an edge between node1 and node2.""" + return node1 in self.neighbors(node2) + + def neighbors(self, node): + """Returns neighbors of `label`. + + Note: when `node` does not exist in the graph an empty list is returned + """ + try: + result = list(self._graph.neighbors(node)) + except nx.NetworkXError: + result = [] + return result + + def find_out_edges(self, node): + """Returns the edges that have `node` as a destination.""" + # pylint: disable=no-member + if node not in self._graph.nodes: + raise KeyError(f"The node `{node}` is not part of the graph") + + if isinstance(self._graph, nx.MultiDiGraph): + return self._graph.out_edges(node) + return [] + + def find_in_edges(self, node): + """Returns the edges that have `node` as a source.""" + # pylint: disable=no-member + if node not in self._graph.nodes: + raise KeyError(f"The node `{node}` is not part of the graph") + + if isinstance(self._graph, nx.MultiDiGraph): + return self._graph.in_edges(node) + return [] + + def find_cliques(self): + """Returns cliques in the graph.""" + return nx.algorithms.clique.find_cliques(self._graph) + + @property + def nodes(self): + """Returns the nodes in the graph.""" + return self._graph.nodes + + @property + def edges(self): + """Returns all the edges in the graph.""" + if isinstance(self._graph, nx.MultiDiGraph): + all_edges = self._graph.edges(keys=True, data=True) + else: + all_edges = self._graph.edges(data=True) + return all_edges + + @property + def num_labels(self): + """Returns the number of labels in the graph.""" + return nx.convert_matrix.to_numpy_matrix(self._graph).shape[0] + + def remove_edges(self, node1, node2): + """Removes edges between both the nodes.""" + self._graph.remove_edge(node1, node2) + + def remove_node(self, node): + """Remove node from graph. + + Args: + node: node to remove + """ + self._graph.remove_node(node) + + def descendants(self, parent): + """Returns descendants. + + (children and children of children, etc.) of `parent`. + """ + try: + edges = list(nx.edge_dfs(self._graph, parent, orientation="reverse")) + except nx.exception.NetworkXError: + edges = [] + return [edge[0] for edge in edges] + + def __eq__(self, other: object) -> bool: + """Returns True if the two graphs are equal.""" + if isinstance(other, Graph): + return ( + self.directed == other.directed + and self._graph.nodes == other._graph.nodes + and self._graph.edges == other._graph.edges + ) + return False + + +class MultiDiGraph(Graph): + """Multi Dimensional implementation of a Graph.""" + + def __init__(self) -> None: + super().__init__(directed=True) + + def topological_sort(self): + """Returns a generator of nodes in topologically sorted order.""" + return nx.topological_sort(self._graph) diff --git a/src/otx/api/entities/id.py b/src/otx/api/entities/id.py new file mode 100644 index 00000000000..78ba2d539c3 --- /dev/null +++ b/src/otx/api/entities/id.py @@ -0,0 +1,56 @@ +"""This module implements the ID entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Optional, Union + +from bson import ObjectId + + +class ID(str): + """An identifier for objects that can be persisted in repositories. + + Usually the creation of IDs is handled by the repositories. In that case objects are initialized with an empty ID() + >>> str(ID()) + '' + + Args: + Optional[Union[str, ObjectId]]: value of the identifier + """ + + # Instead of using composition, we directly subclass the str builtin type. + # This allows us to reuse most of the builtin functions of the str type, + # and avoid the overhead of Python function calls for operations such as hashing and comparison. + # In Python 3.6, using this approach, hash and equality are about 5 times faster than using composition. + # Since the str type is immutable, we cannot handle the object construction arguments in __init__ + # as we would normally and instead have to use __new__. + # The __init__ function is still left for typing correctness and so that Sphinx can get the prototype of the class. + + def __new__(cls, representation: Optional[Union[str, ObjectId]] = None): + """Creates a new ID object.""" + if representation is None: + representation = "" + elif isinstance(representation, ObjectId): + representation = str(representation) + else: + representation = str(representation).strip().lower() + + # Ignore typing error because Mypy does not support calling super().__new__ with a parameter. + return super().__new__(cls, representation) # type: ignore + + # See comment block above + # pylint: disable=W0231 + def __init__(self, representation: Optional[Union[str, ObjectId]] = None): + pass + + # This property name is there to allow automatic mapping between ID <> IDMessage + # It should be the same as the argument in init + @property + def representation(self): + """Returns the value of the identifier.""" + return self + + def __repr__(self): + """Returns the representation of the identifier.""" + return f"ID({self})" diff --git a/src/otx/api/entities/image.py b/src/otx/api/entities/image.py new file mode 100644 index 00000000000..e841820c92d --- /dev/null +++ b/src/otx/api/entities/image.py @@ -0,0 +1,154 @@ +"""This module implements the Image entity.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from typing import Optional, Tuple, Callable, Union + +import cv2 +import imagesize +import numpy as np +from PIL import Image as PILImage + +from otx.api.entities.annotation import Annotation +from otx.api.entities.media import IMedia2DEntity +from otx.api.entities.shapes.rectangle import Rectangle + + +class Image(IMedia2DEntity): + """Represents a 2D image. + + The image must be instantiated with either a NumPy array containing the image data + or a path to an image file. + + Args: + data (Optional[np.ndarray]): NumPy data. + file_path (Optional[str]): Path to image file. + """ + + # pylint: disable=too-many-arguments, redefined-builtin + def __init__( + self, + data: Optional[Union[np.ndarray, Callable[[], np.ndarray]]] = None, + file_path: Optional[str] = None, + size: Optional[Union[Tuple[int, int], Callable[[], Tuple[int, int]]]] = None, + ): + if (data is None) == (file_path is None): + raise ValueError("Either path to image file or image data should be provided.") + self.__data: Optional[Union[np.ndarray, Callable[[], np.ndarray]]] = data + self.__file_path: Optional[str] = file_path + self.__height: Optional[int] = None + self.__width: Optional[int] = None + # TODO: refactor this + self.__size: Optional[Union[Tuple[int, int], Callable[[], Tuple[int, int]]]] = size + + def __str__(self): + """String representation of the image. Returns the image format, name and dimensions.""" + return ( + f"{self.__class__.__name__}" + f"({self.__file_path if self.__data is None else 'with data'}, " + f"width={self.width}, height={self.height})" + ) + + def __get_size(self) -> Tuple[int, int]: + """Returns image size. + + Returns: + Tuple[int, int]: Image size as a (height, width) tuple. + """ + if callable(self.__size): + height, width = self.__size() + self.__size = None + return height, width + if self.__size is not None: + height, width = self.__size + self.__size = None + return height, width + if callable(self.__data): + height, width = self.__data().shape[:2] + return height, width + if self.__data is not None: + return self.__data.shape[0], self.__data.shape[1] + if self.__file_path is not None: + try: + width, height = imagesize.get(self.__file_path) + if width <= 0 or height <= 0: + raise ValueError("Invalide image size") + except ValueError: + image = cv2.imread(self.__file_path) + height, width = image.shape[:2] + return height, width + raise NotImplementedError + + @property + def numpy(self) -> np.ndarray: + """Numpy representation of the image. + + For color images the dimensions are (height, width, color) with RGB color channel order. + + Returns: + np.ndarray: NumPy representation of the image. + """ + if self.__data is None: + try: + image = PILImage.open(self.__file_path) + image = np.asarray(image.convert("RGB")) + except ValueError: + image = cv2.cvtColor(cv2.imread(self.__file_path), cv2.COLOR_BGR2RGB) + return image + if callable(self.__data): + return self.__data() + return self.__data + + @numpy.setter + def numpy(self, value: np.ndarray): + self.__data = value + self.__file_path = None + self.__size = None + self.__height, self.__width = self.__get_size() + + def roi_numpy(self, roi: Optional[Annotation] = None) -> np.ndarray: + """Obtains the numpy representation of the image for a selection region of interest (roi). + + Args: + roi (Optional[Annotaiton]): The region of interest can be Rectangle in the relative + coordinate system of the full-annotation. + + Returns: + np.ndarray: Selected region as numpy array. + """ + data = self.numpy + if roi is None: + return data + + if not isinstance(roi.shape, Rectangle): + raise ValueError("roi shape is not a Rectangle") + + if data is None: + raise ValueError("Numpy array is None, and thus cannot be cropped") + + if len(data.shape) < 2: + raise ValueError("This image is one dimensional, and thus cannot be cropped") + + return roi.shape.crop_numpy_array(data) + + @property + def height(self) -> int: + """Returns the height of the image.""" + if self.__height is None: + self.__height, self.__width = self.__get_size() + return self.__height + + @property + def width(self) -> int: + """Returns the width of the image.""" + if self.__width is None: + self.__height, self.__width = self.__get_size() + return self.__width + + @property + def path(self) -> Optional[str]: + """Returns the file path of the image.""" + return self.__file_path diff --git a/src/otx/api/entities/inference_parameters.py b/src/otx/api/entities/inference_parameters.py new file mode 100644 index 00000000000..717663c3e5d --- /dev/null +++ b/src/otx/api/entities/inference_parameters.py @@ -0,0 +1,43 @@ +"""This module implements the AnalyseParameters entity.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from dataclasses import dataclass +from typing import Any, Callable, Optional + + +# pylint: disable=unused-argument +def default_progress_callback(progress: int, score: Optional[float] = None): + """This is the default progress callback for OptimizationParameters.""" + + +@dataclass +class InferenceParameters: + """Inference parameters. + + Attributes: + is_evaluation: Set to ``True`` if the output dataset is intended + to be used for evaluation purposes. In this scenario, any + postprocessing filtering (such as thresholding and NMS) + should be disabled to avoid interfering with algorithms such + as NMS. + update_progress: Callback which can be used to provide updates + about the progress of a task. + explainer: Explain algorithm to be used in explanation mode. + Will be converted automatically to lowercase. + process_saliency_maps: Process saliency map to input image resolution and apply colormap + explain_predicted_classes: If set to True, provide explanations only for predicted classes. + Otherwise, explain all classes. + enable_async_inference: Enables async inference to increase performance. + """ + + is_evaluation: bool = False + update_progress: Callable[[int, Optional[float]], Any] = default_progress_callback + + explainer: str = "" + process_saliency_maps: bool = False + explain_predicted_classes: bool = True + enable_async_inference: bool = True diff --git a/src/otx/api/entities/interfaces/__init__.py b/src/otx/api/entities/interfaces/__init__.py new file mode 100644 index 00000000000..b44e8322865 --- /dev/null +++ b/src/otx/api/entities/interfaces/__init__.py @@ -0,0 +1,4 @@ +"""Interfaces.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/api/entities/interfaces/graph_interface.py b/src/otx/api/entities/interfaces/graph_interface.py new file mode 100644 index 00000000000..5b9539c298c --- /dev/null +++ b/src/otx/api/entities/interfaces/graph_interface.py @@ -0,0 +1,75 @@ +"""This module implements the Graph interface.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import abc +from typing import Iterator, List + +import networkx as nx + + +class IGraph(metaclass=abc.ABCMeta): + """This interface describes how the interface of the Graph looks like. + + This interface is used to represent the TaskGraph inside project as well as the label tree inside LabelSchema + """ + + @abc.abstractmethod + def add_node(self, node): + """Add node to the graph.""" + raise NotImplementedError + + @abc.abstractmethod + def add_edge(self, node1, node2): + """Add an edge between node1 and node2.""" + raise NotImplementedError + + @abc.abstractmethod + def has_edge_between(self, node1, node2): + """Returns whether there is an edge between `node1` and `node2`.""" + raise NotImplementedError + + @abc.abstractmethod + def neighbors(self, node) -> List[dict]: + """Returns neighbors of `node`.""" + raise NotImplementedError + + @abc.abstractmethod + def find_cliques(self) -> Iterator[List[dict]]: + """Returns cliques in the graph.""" + raise NotImplementedError + + @abc.abstractmethod + def num_nodes(self) -> int: + """Returns number of nodes.""" + raise NotImplementedError + + @abc.abstractmethod + def remove_edges(self, node1, node2) -> None: + """Removes the edges between two nodes.""" + raise NotImplementedError + + @abc.abstractmethod + def find_out_edges(self, node) -> nx.reportviews.OutMultiEdgeView: + """Returns the edges coming out of the node.""" + raise NotImplementedError + + @abc.abstractmethod + def find_in_edges(self, node) -> nx.reportviews.InMultiEdgeView: + """Returns the edges coming in to the node.""" + raise NotImplementedError + + @property + @abc.abstractmethod + def edges(self) -> nx.reportviews.OutMultiEdgeView: + """Returns the edges in the Graph.""" + raise NotImplementedError + + @property + @abc.abstractmethod + def nodes(self) -> nx.reportviews.NodeView: + """Return nodes in the graph.""" + raise NotImplementedError diff --git a/src/otx/api/entities/label.py b/src/otx/api/entities/label.py new file mode 100644 index 00000000000..0beb30bd0da --- /dev/null +++ b/src/otx/api/entities/label.py @@ -0,0 +1,215 @@ +"""This module define the label entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import datetime +import os +from enum import Enum, auto +from typing import Optional + +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.utils.time_utils import now + + +class Domain(Enum): + """Describes an algorithm domain like classification, detection, etc.""" + + NULL = auto() + CLASSIFICATION = auto() + DETECTION = auto() + SEGMENTATION = auto() + ANOMALY_CLASSIFICATION = auto() + ANOMALY_DETECTION = auto() + ANOMALY_SEGMENTATION = auto() + INSTANCE_SEGMENTATION = auto() + ROTATED_DETECTION = auto() + if os.getenv("FEATURE_FLAGS_OTX_ACTION_TASKS", "0") == "1": + ACTION_CLASSIFICATION = auto() + ACTION_DETECTION = auto() + if os.getenv("FEATURE_FLAGS_OTX_VISUAL_PROMPTING_TASKS", "0") == "1": + VISUAL_PROMPTING = auto() + + def __str__(self): + """Returns Domain name.""" + return str(self.name) + + +class LabelEntity: + """This represents a label. The Label is the object that the user annotates and the tasks predict. + + For example, a label with name "car" can be constructed as follows. + + >>> car = LabelEntity(name="car", domain=Domain.DETECTION) + + .. rubric:: About Empty Label + + In addition to representing the presence of a certain object, the label can also + be used to represent the absence of objects in the image (or other media types). + Such a label is referred to as empty label. + The empty label is constructed as follows: + + >>> empty = LabelEntity(name="empty", domain=Domain.DETECTION, is_empty=True) + + Empty label is used to declare that there is nothing of interest inside this image. + For example, let's assume a car detection project. During annotation process, + for positive images (images with cars), the users are asked to annotate the images + with bounding boxes with car label. However, when the user sees a negative image + (no car), the user needs to annotate this image with an empty label. + + The empty label is particularly useful to distinguish images with no objects + of interest from images that have not been annotated, especially in task-chain + scenario. Let's assume car detection task that is followed with with another + detection task which detects the driver inside the car. There are two issues here: + + 1. The user can (intentionally or unintentionally) miss to annotate + the driver inside a car. + 2. There is no driver inside the car. + + Without empty label, these two cases cannot be distinguished. + This is why an empty label is introduced. The empty label makes an explicit + distinction between missing annotations and "negative" images. + + Args: + name: the name of the label + domain: the algorithm domain this label is associated to + color: the color of the label (See :class:`Color`) + hotkey: key or combination of keys to select this label in the + UI + creation_date: the date time of the label creation + is_empty: set to True if the label is an empty label. + id: the ID of the label. Set to ID() so that a new unique ID + will be assigned upon saving. If the argument is None, it + will be set to ID() + is_anomalous: boolean that indicates whether the label is the + Anomalous label. Always set to False for non- anomaly + projects. + """ + + # pylint: disable=redefined-builtin, too-many-instance-attributes, too-many-arguments; Requires refactor + def __init__( + self, + name: str, + domain: Domain, + color: Optional[Color] = None, + hotkey: str = "", + creation_date: Optional[datetime.datetime] = None, + is_empty: bool = False, + id: Optional[ID] = None, + is_anomalous: bool = False, + ): + id = ID() if id is None else id + color = Color.random() if color is None else color + creation_date = now() if creation_date is None else creation_date + + self._name = name + self._color = color + self._hotkey = hotkey + self._domain = domain + self._is_empty = is_empty + self._creation_date = creation_date + self.__id_ = id + self.is_anomalous = is_anomalous + + @property + def name(self): + """Returns the label name.""" + return self._name + + @name.setter + def name(self, value): + self._name = value + + @property + def color(self) -> Color: + """Returns the Color object for the label.""" + return self._color + + @color.setter + def color(self, value): + self._color = value + + @property + def hotkey(self) -> str: + """Returns the hotkey for the label.""" + return self._hotkey + + @hotkey.setter + def hotkey(self, value): + self._hotkey = value + + @property + def domain(self): + """Returns the algorithm domain associated to this label.""" + return self._domain + + @domain.setter + def domain(self, value): + self._domain = value + + @property + def is_empty(self) -> bool: + """Returns a boolean indicating if the label is an empty label.""" + return self._is_empty + + @property + def creation_date(self) -> datetime.datetime: + """Returns the creation date of the label.""" + return self._creation_date + + @property + def id_(self) -> ID: + """Returns the label id.""" + return self.__id_ + + @id_.setter + def id_(self, value: ID): + self.__id_ = value + + @property + def id(self) -> ID: + """DEPRECATED.""" + return self.__id_ + + @id.setter + def id(self, value: ID): + """DEPRECATED.""" + self.__id_ = value + + def __repr__(self): + """String representation of the label.""" + return ( + f"LabelEntity({self.id_}, name={self.name}, hotkey={self.hotkey}, " + f"domain={self.domain}, color={self.color}, is_anomalous={self.is_anomalous})" + ) + + def __eq__(self, other): + """Returns True if the two labels are equal.""" + if isinstance(other, LabelEntity): + return ( + self.id_ == other.id_ + and self.name == other.name + and self.color == other.color + and self.hotkey == other.hotkey + and self.domain == other.domain + and self.is_anomalous == other.is_anomalous + ) + return False + + def __lt__(self, other): + """Returns True if self.id < other.id.""" + if isinstance(other, LabelEntity): + return self.id_ < other.id_ + return False + + def __gt__(self, other): + """Returns True if self.id is greater than other.id.""" + if isinstance(other, LabelEntity): + return self.id_ > other.id_ + return False + + def __hash__(self): + """Returns hash of the label.""" + return hash(str(self)) diff --git a/src/otx/api/entities/label_schema.py b/src/otx/api/entities/label_schema.py new file mode 100644 index 00000000000..fdb3bf047fc --- /dev/null +++ b/src/otx/api/entities/label_schema.py @@ -0,0 +1,791 @@ +"""This module implements the LabelSchema entity.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +from otx.utils.logger import get_logger +import re +from enum import Enum +from typing import Dict, List, Optional, Sequence, Union + +import numpy as np +from bson import ObjectId + +from otx.api.entities.graph import MultiDiGraph +from otx.api.entities.id import ID +from otx.api.entities.label import LabelEntity +from otx.api.entities.scored_label import ScoredLabel + +logger = get_logger() + + +def natural_sort_label_id(target: Union[ID, LabelEntity, ScoredLabel]) -> List[Union[int, str]]: + """Generates a natural sort key for a LabelEntity object based on its ID. + + Args: + target (Union[ID, LabelEntity]): The ID or LabelEntity or ScoredLabel object to be sorted. + + Returns: + List[Union[int, str]]: A list of integers representing the numeric substrings in the ID + in the order they appear. + + Example: + origin_sorted_labels = sorted(labels, key=lambda x: x.id_) + natural_sorted_labels = sorted(labels, key=lambda x: x.natural_sort_label_id) + + print(origin_sorted_labels) # Output: [LabelEntity(0), LabelEntity(1), LabelEntity(10), ... LabelEntity(2)] + print(natural_sorted_labels) # Output: [LabelEntity(0), LabelEntity(1), LabelEntity(2), ... LabelEntity(10)] + """ + + if isinstance(target, (LabelEntity, ScoredLabel)): + target = target.id_ + if isinstance(target, str) and target.isdecimal(): + return ["", int(target)] # "" is added for the case where id of some lables is None + return [target] + + +class LabelGroupExistsException(ValueError): + """Exception thrown if the LabelGroup already exists.""" + + +class LabelGroupDoesNotExistException(ValueError): + """Exception thrown if the LabelGroup does not exist.""" + + +class LabelGroupType(Enum): + """Enum to indicate the LabelGroupType.""" + + EXCLUSIVE = 1 + EMPTY_LABEL = 2 + + +class LabelGroup: + """A label group which has exclusive (multiclass) or contains the empty label. + + Non-exclusive (multilabel) relationships are represented by multiple (exclusive) + label groups. + + The labels have to be from one task. + + Args: + name (str): Descriptive name of the label group + labels (Sequence[LabelEntity]): Labels that form the group + group_type (LabelGroupType): EXCLUSIVE or EMPTY_LABEL + id (ID): ID of the LabelGroup. If no ID is provided, a new ObjectId() + will be assigned + """ + + # pylint: disable=redefined-builtin + def __init__( + self, + name: str, + labels: Sequence[LabelEntity], + group_type: LabelGroupType = LabelGroupType.EXCLUSIVE, + id: ID = None, + ): + self.id_ = ID(ObjectId()) if id is None else id + + self.labels = sorted(labels, key=natural_sort_label_id) + self.name = name + self.group_type = group_type + + @property + def id(self) -> ID: + """DEPRECATED.""" + return self.id_ + + @id.setter + def id(self, value: ID): + """DEPRECATED.""" + self.id_ = value + + @property + def minimum_label_id(self) -> ID: + """Returns the minimum (oldest) label ID, which is the first label in self.labels since this list is sorted.""" + return self.labels[0].id_ + + def remove_label(self, label: LabelEntity) -> None: + """Remove label from label group if it exists in the group. + + Args: + label (LabelEntity): label to remove + """ + if label in self.labels: + self.labels.remove(label) + + def is_single_label(self) -> bool: + """Returns True if the label group only contains one label. + + Returns: + bool: True if the label group only contains one label. + """ + return len(self.labels) == 1 + + def __eq__(self, other: object): + """Returns True if the LabelGroup is equal to the other object.""" + if not isinstance(other, LabelGroup): + return False + return self.id_ == other.id_ and (set(self.labels) == set(other.labels) and self.group_type == other.group_type) + + def __repr__(self) -> str: + """Returns the string representation of the LabelGroup.""" + return f"LabelGroup(id={self.id_}, name={self.name}, group_type={self.group_type}," f" labels={self.labels})" + + +class LabelTree(MultiDiGraph): + """Represents a hierarchy of labels in the form a tree. + + The tree is represented by a directed graph + """ + + def __init__(self) -> None: + super().__init__() + + self.__topological_order_cache: Optional[List[LabelEntity]] = None + + def add_edge(self, node1, node2, edge_value=None): + """Add edge between two nodes in the tree. + + Args: + node1: first node + node2: second node + edge_value: The value of the new edge. Defaults to None. + """ + super().add_edge(node1, node2, edge_value) + self.clear_topological_cache() + + def add_node(self, node): + """Add node to the tree.""" + super().add_node(node) + self.clear_topological_cache() + + def add_edges(self, edges): + """Add edges between Labels.""" + self._graph.add_edges_from(edges) + self.clear_topological_cache() + + def remove_node(self, node): + """Remove node from the tree.""" + super().remove_node(node) + self.clear_topological_cache() + + @property + def num_labels(self): + """Return the number of labels in the tree.""" + return self.num_nodes() + + def clear_topological_cache(self): + """Clear the internal cache of the list of labels sorted in topological order. + + This function should be called if the topology of the graph has changed to + prevent the cache from being stale. + Note that it is automatically called when modifying the topology through the + methods provided by this class. + """ + self.__topological_order_cache = None + + def get_labels_in_topological_order(self) -> List[LabelEntity]: + """Return a list of the labels in this graph sorted in topological order. + + To avoid performance issues, the output of this function is cached. + + Returns: + List[LabelEntity]: sorted list of labels + """ + if self.__topological_order_cache is None: + # TODO: It seems that we are storing the edges the wrong way around. + # To work around this issue, we have to reverse the sorted list. + self.__topological_order_cache = list(reversed(list(self.topological_sort()))) + + return self.__topological_order_cache + + @property + def type(self): + """Returns the type of the LabelTree.""" + return "tree" + + def add_child(self, parent: LabelEntity, child: LabelEntity): + """Add a `child` Label to `parent`.""" + self.add_edge(child, parent) + self.clear_topological_cache() + + def get_parent(self, label: LabelEntity) -> Optional[LabelEntity]: + """Returns the parent of `label`. + + Returns: + the parent if it has one otherwise None + """ + result = self.neighbors(label) + return result[0] if len(result) > 0 else None + + def get_children(self, parent: LabelEntity) -> List[LabelEntity]: + """Returns children of `parent`.""" + if parent not in self._graph.nodes: + return [] + return list(self._graph.predecessors(parent)) # pylint: disable=no-member + + def get_descendants(self, parent: LabelEntity) -> List[LabelEntity]: + """Returns descendants (children and children of children, etc.) of `parent`.""" + return self.descendants(parent) + + def get_siblings(self, label: LabelEntity) -> List[LabelEntity]: + """Returns the siblings of a label.""" + parent = self.get_parent(label) + if parent is None: + siblings = [] + else: + siblings = [u for u, v in self._graph.in_edges(parent) if u != label] # pylint: disable=no-member + return siblings + + def get_ancestors(self, label: LabelEntity) -> List[LabelEntity]: + """Returns ancestors of `label`, including self.""" + result = [] + parent: Optional[LabelEntity] = label + while parent is not None: + result.append(parent) + parent = self.get_parent(parent) + return result + + def subgraph(self, labels: Sequence[LabelEntity]) -> "LabelTree": + """Return the subgraph containing the given labels.""" + new_graph = LabelTree() + new_graph.set_graph(self.get_graph().subgraph(labels).copy()) + return new_graph + + def __eq__(self, other) -> bool: + """Check if two LabelTrees are equal.""" + if isinstance(other, LabelTree): + return super().__eq__(other) + return False + + +class LabelSchemaEntity: + """This class represents the relationships of labels. + + This class currently keeps track of the following relationships: + + - parent/child label relationship + - label group relationships + + Args: + label_tree (LabelTree): a hierarchy of labels represented as a tree + label_groups (List[LabelGroup]): list of groups of labels that form logical groups. + E.g. a group of mutually exclusive labels. + """ + + # pylint: disable=too-many-public-methods, too-many-arguments + def __init__( + self, + label_tree: Optional[LabelTree] = None, + label_groups: Optional[List[LabelGroup]] = None, + ): + if label_tree is None: + label_tree = LabelTree() + self.label_tree = label_tree + + if label_groups is None: + label_groups = [] + self._groups = label_groups + + def get_labels(self, include_empty: bool) -> List[LabelEntity]: + """Get the labels in the label schema. + + Args: + include_empty (bool): flag determining whether to include empty + labels + + Returns: + List[LabelEntity]: list of all labels in the label schema + """ + labels = {label for group in self._groups for label in group.labels if include_empty or not label.is_empty} + return sorted(list(labels), key=natural_sort_label_id) + + def get_groups(self, include_empty: bool = False) -> List[LabelGroup]: + """Get the label groups in the label schema. + + Args: + include_empty (bool): flag determining whether to include empty + label groups + + Returns: + List[LabelGroup]: list of all label groups in the label schema + """ + if include_empty: + return self._groups + + return [group for group in self._groups if group.group_type != LabelGroupType.EMPTY_LABEL] + + def add_group(self, label_group: LabelGroup): + """Adding a group to label schema. + + Args: + label_group (LabelGroup): label group to add + + Returns: + None + """ + if label_group.name in [group.name for group in self._groups]: + raise LabelGroupExistsException( + f"group with '{label_group.name}' exists, " f"use add_labels_to_group_by_group_name instead" + ) + self.__append_group(label_group) + + def add_child(self, parent: LabelEntity, child: LabelEntity): + """Add a `child` Label to `parent`.""" + parent = self.__get_label(parent) + child = self.__get_label(child) + self.label_tree.add_child(parent, child) + + def get_parent(self, label: LabelEntity) -> Optional[LabelEntity]: + """Returns the parent of `label`. + + Returns: + Optional[LabelEntity]: the parent if it has one otherwise None + """ + label = self.__get_label(label) + return self.label_tree.get_parent(label) + + def get_label_ids(self, include_empty: bool) -> List[ID]: + """Returns a list of label ids that are in the LabelSchema. + + Args: + include_empty (bool): Include empty label id or not. + + Returns: + List[ID]: sorted list of label ids + """ + label_ids = { + label.id_ for group in self._groups for label in group.labels if include_empty or not label.is_empty + } + return sorted(list(label_ids), key=natural_sort_label_id) + + def get_label_group_by_name(self, group_name: str) -> Optional[LabelGroup]: + """Get the label group by the passed group_name. + + Args: + group_name (str): name of the group to get + + Returns: + Optional[LabelGroup] + """ + for label_group in self._groups: + if group_name == label_group.name: + return label_group + return None + + def get_exclusive_groups(self) -> List[LabelGroup]: + """Returns exclusive groups in the LabelSchema.""" + + return [group for group in self._groups if group.group_type == LabelGroupType.EXCLUSIVE] + + def add_labels_to_group_by_group_name(self, group_name: str, labels: Sequence[LabelEntity]): + """Adds `labels` to group named `group_name`. + + Args: + labels (str): list of Label + group_name (Sequence[LabelEntity]): group name + + Raises: + LabelGroupDoesNotExistException: This is raised if the group + does not exist + """ + group = self.get_label_group_by_name(group_name) + if group is not None: + group.labels.extend(labels) + else: + raise LabelGroupDoesNotExistException(f"group with name '{group_name}' does not exist, cannot add") + + def __append_group(self, label_group: LabelGroup): + """Convenience function for appending `label_group` to the necessary internal data structures. + + Args: + label_group (LabelGroup): label group to append + """ + if label_group not in self._groups: + self._groups.append(label_group) + + def are_exclusive(self, label1: LabelEntity, label2: LabelEntity) -> bool: + """Returns whether `label` and `label2` are mutually exclusive.""" + return label2 in self.get_labels_exclusive_to(label1) + + def get_children(self, parent: LabelEntity) -> List[LabelEntity]: + """Return a list of the children of the passed parent Label.""" + parent = self.__get_label(parent) + return self.label_tree.get_children(parent) + + def get_siblings_in_group(self, label: LabelEntity) -> List[LabelEntity]: + """Return a list of the 'siblings', which are all labels within the same group as a label.""" + containing_group = self.get_group_containing_label(label) + if containing_group is None: + return [] + return [label_iter for label_iter in containing_group.labels if not label_iter == label] + + def get_descendants(self, parent: LabelEntity) -> List[LabelEntity]: + """Returns descendants (children and children of children, etc.) of `parent`.""" + parent = self.__get_label(parent) + return self.label_tree.get_descendants(parent) + + def get_ancestors(self, label: LabelEntity) -> List[LabelEntity]: + """Returns ancestors of `label`, including self.""" + label = self.__get_label(label) + return self.label_tree.get_ancestors(label) + + def get_group_containing_label(self, label: LabelEntity) -> Optional[LabelGroup]: + """Returns the label group which contains the label. + + Args: + label (LabelEntity): the query label + + Returns: + Optional[LabelGroup]: the group containing the label + """ + label = self.__get_label(label) + for group in self._groups: + if label in group.labels: + return group + return None + + def get_labels_exclusive_to(self, label: LabelEntity) -> List[LabelEntity]: + """Returns a list of labels that are exclusive to the passed label.""" + if label.is_empty: + exclusive_labels = self.__get_exclusivity_for_empty_label(label=label) + else: + exclusive_labels = self.__get_exclusivity_recursion(label=label) + return exclusive_labels + + def __get_exclusivity_recursion(self, label: LabelEntity, add_empty: bool = True) -> List[LabelEntity]: + """Recursively computes all labels exclusive to a non-empty label. + + A label is exclusive with: + - All labels in the same group + - All children of labels in the same group + - All labels in the same group as any of the label's ancestors + - All children of labels in the same group as any of the label's ancestors + - All empty labels that are not descendants of the label + + Args: + label (LabelEntity): The label to get exclusive labels for + add_empty (bool): If set to True, adds all empty labels that are + not descendants of the label. This is only + needed for the first recursion iteration. + + Returns: + List[LabelEntity]: List of labels exclusive to the label. + """ + output = [] + + # Add all labels in the same group + siblings = self.get_siblings_in_group(label) + output += siblings + + # Add all children of labels in the same group + for sibling in siblings: + output += self.get_children(sibling) + + # Do the same for the parent of the label + parent = self.get_parent(label) + if parent is not None: + output += self.__get_exclusivity_recursion(parent, add_empty=False) + + # Add all empty labels that are not descendants of the label. We don't need to check the ancestors for being + # empty, because empty label's don't have descendants. + if add_empty: + descendants = self.get_descendants(label) + exclusive_empty_labels = [ + label_iter + for label_iter in self.get_labels(include_empty=True) + if label_iter.is_empty and label_iter not in descendants + ] + output = list(set(output + exclusive_empty_labels)) + return output + + def __get_exclusivity_for_empty_label(self, label: LabelEntity) -> List[LabelEntity]: + """Get the labels exclusive to an empty label. + + For an empty label, all labels are exclusive to it except it's ancestors. + + Args: + label (LabelEntity): empty Label to get exclusive labels for + + Returns: + List[LabelEntity]: List of Labels exclusive to the Label + """ + ancestors = self.get_ancestors(label) + return [label for label in self.get_labels(include_empty=True) if label not in ancestors] + + @staticmethod + def __get_label(label: Union[ScoredLabel, LabelEntity]) -> LabelEntity: + """Returns Label object from possibly non-label object. + + Args: + label (Union[ScoredLabel, LabelEntity]): label to get Label object for + + Returns: + LabelEntity: Label object + """ + if isinstance(label, ScoredLabel): + returned_label = label.get_label() + elif isinstance(label, LabelEntity): + returned_label = label + else: + raise ValueError("Input of __get_label is not of type Label or ScoredLabel") + return returned_label # type: ignore + + def __repr__(self) -> str: + """String representation of LabelSchemaEntity.""" + return f"LabelSchemaEntity(label_groups={self._groups})" + + def __eq__(self, other) -> bool: + """Returns whether two LabelSchemaEntities are equal.""" + if isinstance(other, LabelSchemaEntity): + return self.label_tree == other.label_tree and self.get_groups(include_empty=True) == other.get_groups( + include_empty=True + ) + return False + + @classmethod + def from_labels(cls, labels: Sequence[LabelEntity]) -> "LabelSchemaEntity": + """Create LabelSchemaEntity from a list of exclusive labels. + + Args: + labels (Sequence[LabelEntity]): list of labels + + Returns: + LabelSchemaEntity from the given labels + """ + label_group = LabelGroup(name="from_label_list", labels=labels) + return LabelSchemaEntity(label_groups=[label_group]) + + def resolve_labels_greedily(self, scored_labels: List[ScoredLabel]) -> List[ScoredLabel]: + """Resolves hierarchical labels and exclusivity based on a list of ScoredLabels (labels with probability). + + The following two steps are taken: + + - select the most likely label from each label group + - add it and it's predecessors if they are also most likely labels (greedy approach). + + Args: + scored_labels (List[LabelEntity]): list of labels to resolve + + Returns: + List[ScoredLabel]: List of ScoredLabels (labels with probability) + """ + + def get_predecessors(lbl: LabelEntity, candidates: List[LabelEntity]) -> List[LabelEntity]: + """Returns all the predecessors of the input label or an empty list if one of the predecessors is not a candidate.""" + predecessors = [] + last_parent = self.get_parent(lbl) + if last_parent is None: + return [lbl] + + while last_parent is not None: + if last_parent not in candidates: + return [] + predecessors.append(last_parent) + last_parent = self.get_parent(last_parent) + + if predecessors: + predecessors.append(lbl) + return predecessors + + label_to_prob = {lbl: 0.0 for lbl in self.get_labels(include_empty=True)} + for s_lbl in scored_labels: + label_to_prob[s_lbl.label] = s_lbl.probability + + candidates = [] + for g in self.get_groups(): + if g.is_single_label(): + candidates.append(g.labels[0]) + else: + max_prob = 0.0 + max_label = None + for lbl in g.labels: + if label_to_prob[lbl] > max_prob: + max_prob = label_to_prob[lbl] + max_label = lbl + if max_label is not None: + candidates.append(max_label) + + output_labels = [] + for lbl in candidates: + if lbl in output_labels: + continue + labels_to_add = get_predecessors(lbl, candidates) + for new_lbl in labels_to_add: + if new_lbl not in output_labels: + output_labels.append(new_lbl) + + output_scored_labels = [ScoredLabel(lbl, label_to_prob[lbl]) for lbl in output_labels] + return output_scored_labels + + def resolve_labels_probabilistic( + self, + scored_labels: List[ScoredLabel], + selected_labels: List[LabelEntity] = None, + ) -> List[ScoredLabel]: + """Resolves hierarchical labels and exclusivity based on a list of ScoredLabels (labels with probability). + + The following two steps are taken: + + - selects the most likely label from an exclusive (multiclass) group + - removes children of "not-most-likely" (non-max) parents in an exclusive group (top-down approach) + + The method is intended to post-process the output of probabilistic systems such as predictions coming from + machine learning methods to resolve ambiguities and logical impossibilities. When processing (non-probabilistic) + user input please use `complete_labels` instead. + + Args: + scored_labels (List[ScoredLabel]): a list of ScoredLabels (labels with + probability) + selected_labels (List[LabelEntity]): if not None, will only consider labels + within `selected_labels` for resolving. Any other labels + which have relations with selected_labels (e.g. parent), + but are outside `selected_labels` are set to a default + probability of 1.0 + """ + input_domains = set(lbl.domain for lbl in scored_labels) + label_to_probability = {scored_label.get_label(): scored_label.probability for scored_label in scored_labels} + resolved_labels = self.__resolve_labels_probabilistic(label_to_probability, selected_labels) + output_domains = set(lbl.domain for lbl in resolved_labels) + if input_domains != output_domains: + logger.error( + "Something went wrong in 'resolve_labels_probabilistic', " + "some tasks (domains) lost all their labels; " + "label_schema: %s input_labels: %s output_labels: %s", + self, + scored_labels, + resolved_labels, + ) + return resolved_labels + + def __resolve_labels_probabilistic( + self, + label_to_probability: Dict[LabelEntity, float], + selected_labels: Optional[Sequence[LabelEntity]], + ) -> List[ScoredLabel]: + """Resolves hierarchical labels and exclusivity based on a probabilistic label output. + + - selects the most likely (max) label from an exclusive group + - removes children of non-max parents in an exclusive group + + See `resolve_labels_probabilistic` for parameter descriptions + + Args: + label_to_probability (Dict[LabelEntity, float]): map from `Label` to float. + selected_labels (Optional[Sequence[LabelEntity]]): Subset of labels. + + Returns: + List[ScoredLabel]: List of ScoredLabels (labels with probability) + """ + # add (potentially) missing ancestors labels for children with probability 0 + # this is needed so that suppression of children of non-max exclusive labels works when the exclusive + # group has only one member + label_to_probability = self.__add_missing_ancestors(label_to_probability, selected_labels) + + hard_classification = self.__resolve_exclusive_labels(label_to_probability) + + # suppress the output of children of parent nodes that are not the most likely label within their group + resolved = self.__suppress_descendant_output(hard_classification) + + result = [] + for label, probability in resolved.items(): + if probability > 0: # only return labels with non-zero probability + result.append( + ScoredLabel( + label, + probability=( + probability + * label_to_probability.get(label, 1.0) + # retain the original probability in the output + ), + ) + ) + return result + + def __suppress_descendant_output(self, hard_classification: Dict[LabelEntity, float]) -> Dict[LabelEntity, float]: + """Suppresses outputs in `label_to_probability`. + + Sets probability to 0.0 for descendants of parents that have 0 probability in `hard_classification`. + """ + + # Input: Conditional probability of each label given its parent label + # Output: Marginal probability of each label + + # We recursively compute the marginal probability of each node by multiplying the conditional probability + # with the marginal probability of its parent. That is: + # P(B) = P(B|A) * P(A) + # The recursion is done a topologically sorted list of labels to ensure that the marginal probability + # of the parent label has been computed before trying to compute the child's probability. + + label_tree = self.label_tree + all_labels = label_tree.get_labels_in_topological_order() + + for child in all_labels: + if child in hard_classification: + # Get the immediate parents (should be at most one element; zero for root labels) + parents = label_tree.neighbors(child) + + if len(parents) > 0: + parent = parents[0] + if parent in hard_classification: + hard_classification[child] *= hard_classification[parent] + + return hard_classification + + def __resolve_exclusive_labels(self, label_to_probability: Dict[LabelEntity, float]) -> Dict[LabelEntity, float]: + """Resolve exclusive labels. + + For labels in `label_to_probability` sets labels that are most likely (maximum probability) in their exclusive + group to 1.0 and other (non-max) labels to probability 0. + """ + hard_classification: Dict[LabelEntity, float] = {} + top_level_labels_in_label_schema = [ + label_ for label_ in self.label_tree.get_labels_in_topological_order() if self.get_parent(label_) is None + ] + + for label, probability in label_to_probability.items(): + if label not in hard_classification: + label_parent = self.get_parent(label) + if label_parent is None: + # The label itself is a top-level label + exclusive_neighbours = [label_ for label_ in top_level_labels_in_label_schema if label_ != label] + else: + exclusive_neighbours = [label_ for label_ in self.get_children(label_parent) if label_ != label] + + probabilities = [probability] + neighbours_ = [label] + for neighbor in exclusive_neighbours: + neighbours_.append(neighbor) + probabilities.append(label_to_probability.get(neighbor, 0)) + if len(probabilities) > 1: + max_index = np.argmax(probabilities) + for idx, neighbor in enumerate(neighbours_): + hard_classification[neighbor] = float(max_index == idx) + else: + # single node group, interpret as multilabel node + hard_classification[label] = float(label_to_probability[label] > 0.0) + return hard_classification + + def __add_missing_ancestors( + self, + label_to_probability: Dict[LabelEntity, float], + selected_labels: Optional[Sequence[LabelEntity]], + ) -> Dict[LabelEntity, float]: + """Adds missing ancestors (of the same task) to the `label_to_probability` map. + + Missing ancestors get probability `probability` + """ + updated_label_to_probability = copy.deepcopy(label_to_probability) + for label in label_to_probability: + for ancestor in self.get_ancestors(label): + if ancestor not in updated_label_to_probability: + updated_label_to_probability[ancestor] = ( + 0.0 # by default missing ancestors get probability 0.0 + if selected_labels is None + else (ancestor not in selected_labels) * 1.0 + # ... unless label selection is used, in that case + # the ancestor will get probability 1.0 if it is missing + ) + return updated_label_to_probability diff --git a/src/otx/api/entities/media.py b/src/otx/api/entities/media.py new file mode 100644 index 00000000000..b32deb8f831 --- /dev/null +++ b/src/otx/api/entities/media.py @@ -0,0 +1,57 @@ +"""This module implements the Media entity.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +from typing import Optional + +import numpy as np + +from otx.api.entities.annotation import Annotation + + +class IMediaEntity(metaclass=abc.ABCMeta): + """Media entity interface. + + This interface is used to represent any kind of media data, on which users can annotate and tasks can perform + training/analysis. + """ + + +class IMedia2DEntity(IMediaEntity, metaclass=abc.ABCMeta): + """This interface is used to represent IMedia which is 2-dimensional media, i.e., containing height and width.""" + + @property # type:ignore + @abc.abstractmethod + def numpy(self) -> np.ndarray: + """Returns the numpy representation of the 2D Media object.""" + raise NotImplementedError + + @numpy.setter # type:ignore + @abc.abstractmethod + def numpy(self, value: np.ndarray): + raise NotImplementedError + + @abc.abstractmethod + def roi_numpy(self, roi: Optional[Annotation]) -> np.ndarray: + """Returns the numpy representation of the 2D Media object while taking the roi into account.""" + raise NotImplementedError + + @property + @abc.abstractmethod + def height(self) -> int: + """Returns the height of the 2D Media object.""" + raise NotImplementedError + + @property + @abc.abstractmethod + def width(self) -> int: + """Returns the width representation of the 2D Media object.""" + raise NotImplementedError + + @property + def path(self) -> Optional[str]: + """Returns the path of the 2D Media object.""" + return None diff --git a/src/otx/api/entities/metadata.py b/src/otx/api/entities/metadata.py new file mode 100644 index 00000000000..0e6869044bd --- /dev/null +++ b/src/otx/api/entities/metadata.py @@ -0,0 +1,115 @@ +"""This module defines classes representing metadata information.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import abc +from enum import Enum, auto +from typing import Any, Optional + +from otx.api.entities.model import ModelEntity + + +class IMetadata(metaclass=abc.ABCMeta): + """This interface represents any additional metadata information which can be connected to an IMedia.""" + + __name = Optional[str] + + @property + def name(self): + """Gets or sets the name of the Metadata item.""" + return self.__name + + @name.setter + def name(self, value): + self.__name = value + + +class FloatType(Enum): + """Represents the use of the FloatMetadata.""" + + FLOAT = auto() # Regular float, without particular context + EMBEDDING_VALUE = auto() + ACTIVE_SCORE = auto() + + def __str__(self): + """Return the name of FloatType enum.""" + return str(self.name) + + +class FloatMetadata(IMetadata): + """This class represents metadata of type float. + + Args: + name (str): Name of the metadata. + value (float): Value of the metadata. + float_type (FloatType): Type of the metadata. + """ + + def __init__(self, name: str, value: float, float_type: FloatType = FloatType.FLOAT): + self.name = name + self.value = value + self.float_type = float_type + + def __repr__(self): + """Prints the model, data and type of the MetadataItemEntity.""" + return f"FloatMetadata({self.name}, {self.value}, {self.float_type})" + + def __eq__(self, other): + """Checks if two FloatMetadata have the same name, value and type.""" + return self.name == other.name and self.value == other.value and self.float_type == other.float_type + + +class VideoMetadata(IMetadata): + """This class represents metadata of video. + + Args: + video_id (str): id(name) for video. + frame_idx (int): Index for frame. + is_empty_frame(bool): whether this is empty frame(for action detection) + """ + + def __init__(self, video_id: str, frame_idx: int, is_empty_frame: bool): + self.video_id = video_id + self.frame_idx = frame_idx + self.is_empty_frame = is_empty_frame + self.metadata = {"video_id": video_id, "frame_idx": frame_idx, "is_empty_frame": is_empty_frame} + + def __repr__(self): + """Prints the video_id, frame_id and type of the MetadataItemEntity.""" + out_string = "VideoMetadata" + out_string += f"({self.metadata})" + return out_string + + def __eq__(self, other): + """Checks if two VideoMetadata have the same name, value and type.""" + return self.metadata == other.metadata + + def update(self, key: str, value: Any): + """Update metadata infomation.""" + setattr(self, key, value) + self.metadata[key] = value + + +class MetadataItemEntity: + """This class is a wrapper class which connects the metadata value to model, which was used to generate it. + + Args: + data (IMetadata): The metadata value. + model (Optional[ModelEntity]): The model which was used to generate the metadata. Defaults to None. + """ + + def __init__( + self, + data: Any, + model: Optional[ModelEntity] = None, + ): + self.data = data + self.model = model + + def __repr__(self): + """Prints the model and data of the MetadataItemEntity.""" + return f"MetadataItemEntity(model={self.model})" + + def __eq__(self, other): + """Returns true if the model and the data match the other MetadataItemEntity.""" + return self.model == other.model and self.data == other.data diff --git a/src/otx/api/entities/metrics.py b/src/otx/api/entities/metrics.py new file mode 100644 index 00000000000..f33ba288832 --- /dev/null +++ b/src/otx/api/entities/metrics.py @@ -0,0 +1,747 @@ +"""This module implements the Metric entities.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +import datetime +import math +from enum import Enum +from typing import Generic, List, Optional, Sequence, TypeVar, Union +from otx.utils.logger import get_logger + +import numpy as np + +from otx.api.utils.time_utils import now + +logger = get_logger() + + +class MetricEntity(metaclass=abc.ABCMeta): + """This interface represents a metric, which is the smallest building block for the performance statistics. + + It only contains the name of the metric. + See also :class:`MetricsGroup` and :class:`Performance` for the structure of performance statistics. + """ + + __name = None + + @property + def name(self): + """Returns the name of the Metric Entity.""" + return self.__name + + @name.setter + def name(self, value): + self.__name = value + + @staticmethod + def type() -> str: + """Returns the type of the MetricEntity, e.g. "curve.""" + + +class CountMetric(MetricEntity): + """This metric represents an integer value. + + Args: + name: The name of the metric + value: The value of the metric + + Example: + The count for number of images in a project + + >>> count_metric = CountMetric(name="Number of images", value=20) + + """ + + value: int + + def __init__(self, name: str, value: int): + self.name = name + self.value = value + + @staticmethod + def type(): + """Returns the type of the MetricEntity.""" + return "count" + + +class InfoMetric(MetricEntity): + """This metric represents a string value. + + Args: + name: The name of the info metric + value: The info of the metric + + Example: + An info metric of training from scratch + + >>> info_metric = InfoMetric(name="Model info", value="This model is trained from scratch") + + """ + + value: str + + def __init__(self, name: str, value: str): + self.name = name + self.value = value + + @staticmethod + def type(): + """Returns the type of the MetricEntity.""" + return "string" + + +class DateMetric(MetricEntity): + """This metric represents a date time value. + + Args: + name: The name of the date metric + date: The datetime value of the metric + + Example: + A DateMetric for model creation date (e.g., now). + + >>> metric = DateMetric(name="Model creation", date=datetime.datetime.now(datetime.timezone.utc)) + + """ + + date: datetime.datetime + + def __init__(self, name: str, date: Optional[datetime.datetime] = None): + self.name = name + if date is None: + date = now() + self.date = date + + @staticmethod + def type(): + """Returns the type of the MetricEntity.""" + return "date" + + +class ScoreMetric(MetricEntity): + """This metric represents a float value. + + This metric is typically used for storing performance metrics, such as accuracy, f-measure, dice score, etc. + + Args: + name: The name of the score + value: The value of the score + + Example: + Accuracy of a model + + >>> score_metric = ScoreMetric(name="Model accuracy", value=0.5) + + """ + + def __init__(self, name: str, value: float): + self.name = name + self.value = value + + if math.isnan(value): + raise ValueError("The value of a ScoreMetric is not allowed to be NaN.") + + def __eq__(self, other: object) -> bool: + """Returns True if the score metrics are equal.""" + if not isinstance(other, ScoreMetric): + return False + return self.name == other.name and self.value == other.value + + def __repr__(self): + """Returns the representation of the score metric.""" + return f"ScoreMetric(name=`{self.name}`, score=`{self.value}`)" + + @staticmethod + def type(): + """Returns the type of the MetricEntity.""" + return "score" + + +class DurationMetric(MetricEntity): + """This metric represents a duration metric, which include hour (int), minute (int), and second (float). + + Args: + name: The name of the duration metric + hour: The hour value of the metric + minute: The minute value of the metric + second: The second value of the metric + + Example: + Creating a metric for training duration of 1 hour 5 minutes. + + >>> duration_metric = DurationMetric(name="Training duration", hour=1, minute=5, second=0) + + """ + + def __init__(self, name: str, hour: int, minute: int, second: float): + self.hour = hour + self.minute = minute + self.second = second + self.name = name + + def get_duration_string(self) -> str: + """Returns the string representation of the duration. + + Example: + Duration string of 1 hour 1 minute and 1.50 seconds. + + >>> from otx.api.entities.metrics import DurationMetric + >>> dur_met = DurationMetric("test", 1, 1, 1.5) # 1 hour 1 minute and 1.5 seconds + >>> dur_met.get_duration_string() + '1 hour 1 minute 1.50 seconds' + + Returns: + the string representation of the duration. + """ + output: str = "" + if self.hour != 0: + output += f"{self.hour} hour{'s ' if self.hour > 1 else ' '}" + if self.minute != 0: + output += f"{self.minute} minute{'s ' if self.minute > 1 else ' '}" + if self.second != 0: + output += f"{self.second:.02f} second{'s ' if self.second > 1 else ' '}" + output = output.strip() + return output + + @staticmethod + def from_seconds(name: str, seconds: float) -> "DurationMetric": + """Returns a duration metrics, with name and converted durations from seconds. + + Example: + Converting 70 seconds to duration metric. + + >>> from otx.api.entities.metrics import DurationMetric + >>> dur_met = DurationMetric.from_seconds("test", 70) # 1 hour 1 minute and 1.5 seconds + >>> dur_met.get_duration_string() + '1 minute 10.00 seconds' + + Args: + name + seconds + + Returns: + DurationMetric: the duration metric with name and converted durations from seconds. + """ + hour = int(seconds // 3600) + modulo = seconds % 3600 + minute = int(modulo // 60) + second = modulo % 60 + return DurationMetric(name=name, hour=hour, minute=minute, second=second) + + @staticmethod + def type(): + """Returns the type of the MetricEntity.""" + return "duration" + + +class CurveMetric(MetricEntity): + """This metric represents a curve. The coordinates are represented as x and y lists. + + Example: + A line curve of: [(0,1), (1, 5), (2, 8)] + + >>> CurveMetric("Line", xs=[0, 1, 2], ys=[1, 5, 8]) + CurveMetric(name=`Line`, ys=(3 values), xs=(3 values)) + + A curve can also be defined only using the y values. For example, a loss curve of loss values: [0.5, 0.2, 0.1]. + The x values will be automatically generated as a 1-based index (1, 2, 3, ...) + + >>> CurveMetric("Loss", ys=[0.5, 0.2, 0.1]) + CurveMetric(name=`Loss`, ys=(3 values), xs=(None values)) + + Args: + name: The name of the curve + xs: the list of floats in x-axis + ys: the list of floats in y-axis + """ + + def __init__(self, name: str, ys: List[float], xs: Optional[List[float]] = None): + self.name = name + self.__ys = ys + if xs is not None: + if len(xs) != len(self.__ys): + raise ValueError(f"Curve error must contain the same length for x and y: ({len(xs)} vs {len(self.ys)})") + self.__xs = xs + else: + # if x values are not provided, set them to the 1-index of the y values + self.__xs = list(range(1, len(self.__ys) + 1)) + + @property + def ys(self) -> List[float]: + """Returns the list of floats on y-axis.""" + return self.__ys + + @property + def xs(self) -> List[float]: + """Returns the list of floats on x-axis.""" + return self.__xs + + def __repr__(self): + """Returns the string representation of the object.""" + return ( + f"CurveMetric(name=`{self.name}`, ys=({len(self.ys)} values), " + f"xs=({len(self.xs) if self.xs is not None else 'None'} values))" + ) + + @staticmethod + def type(): + """Returns the type of the MetricEntity.""" + return "curve" + + +class MatrixMetric(MetricEntity): + """This metric represents a matrix. The cells are represented as a list of lists of integers. + + In the case of a confusion matrix, the rows represent the ground truth items and the columns represent the + predicted items. + + Example: + A matrix of: [[4,0,1], [0,3,2], [1,2,2]] + + >>> MatrixMetric("Confusion Matrix", matrix_values=np.array([[4,0,1], [0,3,2], [1,2,2]])) + MatrixMetric(name=`Confusion Matrix`, matrix_values=(3x3) matrix, row labels=None, column labels=None) + + Args: + name: The name of the matrix + matrix_values: the matrix data + row_labels: labels for the rows + column_labels: labels for the columns + normalize: set to True to normalize each row of the matrix + """ + + __row_labels: Optional[List[str]] = None + __column_labels: Optional[List[str]] = None + + # pylint: disable=too-many-arguments; Requires refactor + def __init__( + self, + name: str, + matrix_values: np.ndarray, + row_labels: Optional[List[str]] = None, + column_labels: Optional[List[str]] = None, + normalize: bool = False, + ): + self.name = name + self.__matrix_values = matrix_values + self.__matrix_values.astype(np.float32) + + if row_labels is not None: + self.__row_labels = row_labels + if self.__matrix_values.shape[0] != len(self.__row_labels): + raise ValueError( + f"Number of rows of the matrix and number of row labels must be equal. The shape " + f"has {self.__matrix_values.shape[0]} rows and {len(self.__row_labels)} row labels" + ) + + if column_labels is not None: + self.__column_labels = column_labels + if self.__matrix_values.shape[1] != len(self.__column_labels): + raise ValueError( + f"Number of columns of the matrix and number of column labels must be equal. The " + f"shape has {self.__matrix_values.shape[1]} columns and {len(self.__column_labels)} column " + "labels" + ) + + if normalize: + self.normalize() + + @property + def matrix_values(self) -> np.ndarray: + """Returns the matrix data.""" + return self.__matrix_values + + @property + def row_labels(self) -> Optional[List[str]]: + """Returns the row labels.""" + return self.__row_labels + + @property + def column_labels(self) -> Optional[List[str]]: + """Returns the column labels.""" + return self.__column_labels + + def normalize(self): + """Normalizes the confusion matrix by dividing by the sum of the rows.""" + self.__matrix_values = self.__matrix_values.astype(np.float32) / self.__matrix_values.astype(np.float32).sum( + axis=1, keepdims=True + ) # Divide all values by the sum of its row + + if not np.all(self.__matrix_values.sum(axis=1, keepdims=True) > 0): + self.__matrix_values = np.nan_to_num(self.__matrix_values) + + logger.warning("Replacing NaN in the matrix with zeroes since the sum of one (or more) row(s) was zero.") + + def __repr__(self): + """Returns the string representation of the object.""" + return ( + f"MatrixMetric(name=`{self.name}`, matrix_values=({self.__matrix_values.shape[0]}x" + f"{self.__matrix_values.shape[1]}) matrix, row labels={self.__row_labels}, column labels" + f"={self.__column_labels})" + ) + + @staticmethod + def type(): + """Returns the type of the MetricEntity.""" + return "matrix" + + +class NullMetric(MetricEntity): + """Represents 'Metric not found'.""" + + def __init__(self) -> None: + self.name = "NullMetric" + + def __repr__(self): + """Returns the string representation of the object.""" + return "NullMetric()" + + def __eq__(self, other): + """Returns True if the other object is a NullMetric.""" + return isinstance(other, NullMetric) + + @staticmethod + def type(): + """Returns the type of the MetricEntity.""" + return "null" + + +class VisualizationType(Enum): + """This enum defines how the metrics will be visualized on the UI.""" + + TEXT = 0 + RADIAL_BAR = 1 + BAR = 2 + LINE = 3 + MATRIX = 4 + + +class ColorPalette(Enum): + """Enum class specifying the color palette to be used by the UI to display statistics. + + If the statistics are per label, set to LABEL so the UI will use the label color palette. + Otherwise, set to DEFAULT (allow the UI to choose a color palette) + """ + + DEFAULT = 0 + LABEL = 1 + + +class VisualizationInfo: + """This represents the visualization info a metrics group. See :class:`MetricsGroup`.""" + + __type: VisualizationType + name: str # todo: this should be a part of MetricsGroup, not the visualization info. + + def __init__( + self, + name: str, + visualisation_type: VisualizationType, + palette: ColorPalette = ColorPalette.DEFAULT, + ): + self.__type = visualisation_type + self.name = name + self.palette = palette + + @property + def type(self) -> VisualizationType: + """Returns the type of the visualization.""" + return self.__type + + def __repr__(self): + """Returns the string representation of the object.""" + return f"VisualizationInfo(name='{self.name}', type='{self.type.name}', palette='{self.palette.name}')" + + +class TextChartInfo(VisualizationInfo): + """This represents a visualization using text, which uses only a single string.""" + + def __init__( + self, + name: str, + ): + super().__init__(name, VisualizationType.TEXT) + + def __repr__(self): + """Returns the string representation of the object.""" + return f"TextChartInfo(name='{self.name}, 'type='{self.type}')" + + +class LineChartInfo(VisualizationInfo): + """This represents a visualization using a line chart.""" + + x_axis_label: str + y_axis_label: str + + def __init__( + self, + name: str, + x_axis_label: str = None, + y_axis_label: str = None, + palette: ColorPalette = ColorPalette.DEFAULT, + ): + super().__init__(name, VisualizationType.LINE, palette) + if x_axis_label is None: + x_axis_label = "" + if y_axis_label is None: + y_axis_label = "" + + self.x_axis_label = x_axis_label + self.y_axis_label = y_axis_label + + def __repr__(self): + """Returns the string representation of the object.""" + return ( + f"LineChartInfo(name='{self.name}, 'type='{self.type}', x_axis_label='{self.x_axis_label}', " + f"y_axis_label='{self.y_axis_label}')" + ) + + +class BarChartInfo(VisualizationInfo): + """This represents a visualization using a bar chart.""" + + def __init__( + self, + name: str, + palette: ColorPalette = ColorPalette.DEFAULT, + visualization_type: VisualizationType = VisualizationType.BAR, + ): + if visualization_type not in ( + VisualizationType.BAR, + VisualizationType.RADIAL_BAR, + ): + raise ValueError("Visualization type for BarChartInfo must be BAR or RADIAL_BAR") + super().__init__(name, visualization_type, palette) + + def __repr__(self): + """Returns the string representation of the object.""" + return f"BarChartInfo(name='{self.name}', type='{self.type}')" + + +class MatrixChartInfo(VisualizationInfo): + """This represents a visualization using a matrix.""" + + header: str + row_header: str + column_header: str + + # pylint: disable=too-many-arguments; Requires refactor + def __init__( + self, + name: str, + header: str = None, + row_header: str = None, + column_header: str = None, + palette: ColorPalette = ColorPalette.DEFAULT, + ): + super().__init__(name, VisualizationType.MATRIX, palette) + if header is not None: + self.header = header + if row_header is not None: + self.row_header = row_header + if column_header is not None: + self.column_header = column_header + + def __repr__(self): + """Returns the string representation of the object.""" + return ( + f"MatrixChartInfo(name='{self.name}', type='{self.type}', header='{self.header}', row_header='" + f"{self.row_header}', column_header='{self.column_header}')" + ) + + +_Metric = TypeVar("_Metric", bound=MetricEntity) +_VisualizationInfo = TypeVar("_VisualizationInfo", bound=VisualizationInfo) + + +class MetricsGroup(Generic[_Metric, _VisualizationInfo]): + """This class aggregates a list of metric entities and defines how this group will be visualized on the UI. + + This class is the parent class to the different types of MetricsGroup that each represent a different type of chart + in the UI. + + Example: + An accuracy as a metrics group + >>> acc = ScoreMetric("Accuracy", 0.5) + >>> visual_info = BarChartInfo("Accuracy", visualization_type=_VisualizationInfo.BAR) # show as radial bar + >>> metrics_group = BarMetricsGroup([acc], visual_info) + + Loss curves as a metrics group + >>> train_loss = CurveMetric("Train loss", xs=[0, 1, 2], ys=[5, 3, 1]) + >>> val_loss = CurveMetric("Validation", xs=[0, 1, 2], ys=[6, 4, 2]) + >>> visual_info = LineChartInfo("Loss curve", x_axis_label="# epoch", y_axis_label="Loss") + >>> metrics_group = LineMetricsGroup([train_loss, val_loss], visual_info) + """ + + def __init__(self, metrics: Sequence[_Metric], visualization_info: _VisualizationInfo): + if metrics is None or len(metrics) == 0: + raise ValueError("Metrics cannot be None or empty") + if visualization_info is None: + raise ValueError("visualization_info cannot be None") + self.metrics = metrics + self.visualization_info = visualization_info + + +class MatrixMetricsGroup(MetricsGroup[MatrixMetric, MatrixChartInfo]): + """This class represent a matrix chart in the UI. + + Multiple matrices can be displayed in the same chart. + """ + + def __init__(self, metrics: Sequence[MatrixMetric], visualization_info: MatrixChartInfo): + super().__init__(metrics=metrics, visualization_info=visualization_info) + + +class LineMetricsGroup(MetricsGroup[CurveMetric, LineChartInfo]): + """This class represent a line chart in the UI. + + Multiple lines can be displayed in a single chart. + """ + + def __init__(self, metrics: Sequence[CurveMetric], visualization_info: LineChartInfo): + super().__init__(metrics=metrics, visualization_info=visualization_info) + + +class BarMetricsGroup(MetricsGroup[Union[ScoreMetric, CountMetric], BarChartInfo]): + """This class represent a bar or radial bar chart in the UI. + + Each metric in the metrics group represents the value of a single bar/radial bar in the chart. + """ + + def __init__( + self, + metrics: Sequence[Union[ScoreMetric, CountMetric]], + visualization_info: BarChartInfo, + ): + super().__init__(metrics=metrics, visualization_info=visualization_info) + + +class TextMetricsGroup( + MetricsGroup[ + Union[ScoreMetric, CountMetric, InfoMetric, DateMetric, DurationMetric], + TextChartInfo, + ] +): + """This class represent a text chart in the UI. + + Text charts contain only one metric, which can be of type CountMetric, ScoreMetric, DateMetric, DurationMetric or + InfoMetric. + """ + + def __init__( + self, + metrics: Sequence[Union[ScoreMetric, CountMetric, InfoMetric, DateMetric, DurationMetric]], + visualization_info: TextChartInfo, + ): + if not len(metrics) == 1: + raise ValueError( + "A text metrics group can contain only a single " + "ScoreMetric, CountMetric, InfoMetric, DateMetric or " + "DurationMetric." + ) + super().__init__(metrics=metrics, visualization_info=visualization_info) + + +class Performance: + """This performance class wraps the statistics of an entity (e.g., Model, Resultset). + + Args: + score: the performance score. This will be the point of + comparison between two performances. + dashboard_metrics: (optional) additional statistics, containing + charts, curves, and other additional info. + """ + + def __init__(self, score: ScoreMetric, dashboard_metrics: Optional[List[MetricsGroup]] = None): + if not isinstance(score, ScoreMetric): + raise ValueError(f"Expected score to be of type `ScoreMetric`, got type `{type(score)}` instead.") + self._score: ScoreMetric = score + self.dashboard_metrics: List[MetricsGroup] = [] if dashboard_metrics is None else dashboard_metrics + + @property + def score(self): + """Return the score metric.""" + return self._score + + def __eq__(self, other: object) -> bool: + """Returns True if self and other have the same score and dashboard metrics.""" + if not isinstance(other, Performance): + return False + return self.score == other.score + + def __repr__(self): + """Returns a string representation of the performance.""" + return f"Performance(score: {self.score.value}, dashboard: ({len(self.dashboard_metrics)} metric groups))" + + +class NullPerformance(Performance): + """This is used to represent 'Performance not found'.""" + + def __init__(self) -> None: + super().__init__(score=ScoreMetric(name="Null score", value=0.0)) + + def __repr__(self): + """Returns a string representation of the performance.""" + return "NullPerformance()" + + def __eq__(self, other): + """Returns True if other is a NullPerformance.""" + return isinstance(other, NullPerformance) + + +class MultiScorePerformance(Performance): + """This class can be used in tasks where performance is measured by multiple metrics. + + Args: + primary_score: The main performance score. + additional_metrics: List of additional scores. When no primary + score is provided, the first additional score takes priority + as the main project score. + dashboard_metrics: (optional) additional statistics, containing + charts, curves, and other additional info. + """ + + def __init__( + self, + primary_score: Optional[ScoreMetric] = None, + additional_scores: Optional[List[ScoreMetric]] = None, + dashboard_metrics: Optional[List[MetricsGroup]] = None, + ): + assert primary_score is not None or ( + additional_scores is not None and len(additional_scores) > 0 + ), "Provide at least one primary or additional score." + + self._primary_score = primary_score + self._additional_scores: List[ScoreMetric] = [] if additional_scores is None else additional_scores + self.dashboard_metrics: List[MetricsGroup] = [] if dashboard_metrics is None else dashboard_metrics + + if self.primary_score is None: + super().__init__(self.additional_scores[0], dashboard_metrics) + else: + super().__init__(self.primary_score, dashboard_metrics) + + @property + def primary_score(self) -> Optional[ScoreMetric]: + """Return the primary score metric.""" + return self._primary_score + + @property + def additional_scores(self) -> List[ScoreMetric]: + """Return the additional score metrics.""" + return self._additional_scores + + def __eq__(self, other: object) -> bool: + """Returns True if the other object is a MultiScorePerformance with the same primary and additional scores.""" + if not isinstance(other, MultiScorePerformance): + return False + return self.primary_score == other.primary_score and self.additional_scores == other.additional_scores + + def __repr__(self): + """Returns the representation of the performance.""" + return ( + f"MultiScorePerformance(score: {self.score.value}, primary_metric: {self.primary_score}, " + f"additional_metrics: ({len(self.additional_scores)} metrics), " + f"dashboard: ({len(self.dashboard_metrics)} metric groups))" + ) diff --git a/src/otx/api/entities/model.py b/src/otx/api/entities/model.py new file mode 100644 index 00000000000..8b16cfc83c7 --- /dev/null +++ b/src/otx/api/entities/model.py @@ -0,0 +1,462 @@ +"""This file defines the ModelConfiguration, ModelEntity and Model classes.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import datetime +from enum import IntEnum, auto +from typing import TYPE_CHECKING, Dict, List, Optional, Union + +from otx.api.configuration import ConfigurableParameters +from otx.api.entities.id import ID +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.metrics import NullPerformance, Performance +from otx.api.entities.model_template import TargetDevice +from otx.api.usecases.adapters.model_adapter import ( + ExportableCodeAdapter, + IDataSource, + ModelAdapter, +) +from otx.api.utils.time_utils import now + +if TYPE_CHECKING: + # pylint: disable=ungrouped-imports + from otx.api.entities.datasets import DatasetEntity + + +class ModelPrecision(IntEnum): + """Represents the ModelPrecision of a Model.""" + + INT4 = auto() + INT8 = auto() + FP16 = auto() + FP32 = auto() + + def __str__(self): + """String.""" + return self.name + + +class ModelConfiguration: + """This class represents the task configuration which was used to generate a specific model. + + Those are the parameters that a task may need in order to use the model. + + Args: + configurable_parameters: Task configurable parameters used to + generate the model + label_schema: Label schema inside the project used to generate + the model + """ + + configurable_parameters: ConfigurableParameters + + def __init__( + self, + configurable_parameters: ConfigurableParameters, + label_schema: LabelSchemaEntity, + ): + self.configurable_parameters = configurable_parameters + self.__label_schema = label_schema + + def get_label_schema(self) -> LabelSchemaEntity: + """Get the LabelSchema.""" + return self.__label_schema + + +class ModelFormat(IntEnum): + """Indicate the format of the model.""" + + OPENVINO = auto() + BASE_FRAMEWORK = auto() + ONNX = auto() + + +class ModelOptimizationType(IntEnum): + """Represents optimization type that is used to optimize the model.""" + + NONE = auto() + MO = auto() + NNCF = auto() + POT = auto() + ONNX = auto() + + +class OptimizationMethod(IntEnum): + """Represents optimization method that is used to optimize the model.""" + + FILTER_PRUNING = auto() + QUANTIZATION = auto() + + +# pylint: disable=too-many-instance-attributes, too-many-public-methods +class ModelEntity: + """Represents the Entity of a Model.""" + + # TODO: add tags and allow filtering on those in modelrepo + # pylint: disable=too-many-arguments,too-many-locals; Requires refactor + def __init__( + self, + train_dataset: "DatasetEntity", + configuration: ModelConfiguration, + *, + creation_date: Optional[datetime.datetime] = None, + performance: Optional[Performance] = None, + previous_trained_revision: Optional["ModelEntity"] = None, + previous_revision: Optional["ModelEntity"] = None, + version: int = 1, + tags: Optional[List[str]] = None, + model_format: ModelFormat = ModelFormat.OPENVINO, + training_duration: float = 0.0, + model_adapters: Optional[Dict[str, ModelAdapter]] = None, + exportable_code_adapter: Optional[ExportableCodeAdapter] = None, + precision: Optional[List[ModelPrecision]] = None, + latency: int = 0, + fps_throughput: int = 0, + target_device: TargetDevice = TargetDevice.CPU, + target_device_type: Optional[str] = None, + optimization_type: ModelOptimizationType = ModelOptimizationType.NONE, + optimization_methods: List[OptimizationMethod] = None, + optimization_objectives: Dict[str, str] = None, + performance_improvement: Dict[str, float] = None, + model_size_reduction: float = 0.0, + _id: Optional[ID] = None, + has_xai: bool = False, + ): + _id = ID() if _id is None else _id + performance = NullPerformance() if performance is None else performance + creation_date = now() if creation_date is None else creation_date + + optimization_methods = [] if optimization_methods is None else optimization_methods + optimization_objectives = {} if optimization_objectives is None else optimization_objectives + performance_improvement = {} if performance_improvement is None else performance_improvement + + tags = [] if tags is None else tags + precision = [ModelPrecision.FP32] if precision is None else precision + + if model_adapters is None: + model_adapters = {} + + self.__id_ = _id + self.__creation_date = creation_date + self.__train_dataset = train_dataset + self.__previous_trained_revision = previous_trained_revision + self.__previous_revision = previous_revision + self.__version = version + self.__tags = tags + self.__model_format = model_format + self.__performance = performance + self.__training_duration = training_duration + self.__configuration = configuration + self.__model_adapters = model_adapters + self.__exportable_code_adapter = exportable_code_adapter + self.model_adapters_to_delete: List[ModelAdapter] = [] + self.__precision = precision + self.__latency = latency + self.__fps_throughput = fps_throughput + self.__target_device = target_device + self.__target_device_type = target_device_type + self.__optimization_type = optimization_type + self.__optimization_methods = optimization_methods + self.__optimization_objectives = optimization_objectives + self.__performance_improvement = performance_improvement + self.__model_size_reduction = model_size_reduction + self.__has_xai = has_xai + + @property + def id_(self) -> ID: + """Gets or sets the id of a Model.""" + return self.__id_ + + @id_.setter + def id_(self, value: ID): + self.__id_ = value + + @property + def id(self) -> ID: + """DEPRECATED.""" + return self.__id_ + + @id.setter + def id(self, value: ID): + """DEPRECATED.""" + self.__id_ = value + + @property + def configuration(self) -> ModelConfiguration: + """Gets or sets the configuration of the Model.""" + return self.__configuration + + @configuration.setter + def configuration(self, value: ModelConfiguration): + self.__configuration = value + + @property + def creation_date(self) -> datetime.datetime: + """Gets or sets the creation_date of the Model.""" + return self.__creation_date + + @creation_date.setter + def creation_date(self, value: datetime.datetime): + self.__creation_date = value + + @property + def train_dataset(self) -> "DatasetEntity": + """Gets or sets the current Training Dataset.""" + return self.__train_dataset + + @train_dataset.setter + def train_dataset(self, value: "DatasetEntity"): + self.__train_dataset = value + + @property + def previous_trained_revision(self) -> Union[None, "ModelEntity"]: + """Gets or sets the previous model. + + Returns: + None if no previous_trained_revision has been created + """ + return self.__previous_trained_revision + + @previous_trained_revision.setter + def previous_trained_revision(self, value: "ModelEntity"): + self.__previous_trained_revision = value + + @property + def previous_revision(self) -> Union[None, "ModelEntity"]: + """Gets or sets the previous model.""" + return self.__previous_revision + + @previous_revision.setter + def previous_revision(self, value: "ModelEntity"): + self.__previous_revision = value + + @property + def version(self) -> int: + """Gets or sets the version.""" + return self.__version + + @version.setter + def version(self, value: int): + self.__version = value + + @property + def tags(self) -> List[str]: + """Gets or sets the tags of the Model.""" + return self.__tags + + @tags.setter + def tags(self, value: List[str]): + self.__tags = value + + @property + def model_format(self) -> ModelFormat: + """Gets the model format.""" + return self.__model_format + + @model_format.setter + def model_format(self, value: ModelFormat): + self.__model_format = value + + @property + def performance(self) -> Performance: + """Gets or sets the current Performance of the Model.""" + return self.__performance + + @performance.setter + def performance(self, value: Performance): + self.__performance = value + + @property + def training_duration(self) -> float: + """Gets or sets the current training duration.""" + return self.__training_duration + + @training_duration.setter + def training_duration(self, value: float): + self.__training_duration = value + + @property + def precision(self) -> List[ModelPrecision]: + """Get or set the precision for the model. + + This has effect on accuracy, latency and throughput of the model. + """ + return self.__precision + + @precision.setter + def precision(self, value: List[ModelPrecision]): + self.__precision = value + + @property + def latency(self) -> int: + """Get or set the latency of the model. + + Unit is milliseconds (ms) + """ + return self.__latency + + @latency.setter + def latency(self, value: int): + self.__latency = value + + @property + def fps_throughput(self) -> int: + """Get or set the throughput of the model. + + Unit is frames per second (fps) + """ + return self.__fps_throughput + + @fps_throughput.setter + def fps_throughput(self, value: int): + self.__fps_throughput = value + + @property + def target_device(self) -> TargetDevice: + """Get or set the device on which the model will be deployed.""" + return self.__target_device + + @target_device.setter + def target_device(self, value: TargetDevice): + self.__target_device = value + + @property + def target_device_type(self) -> Optional[str]: + """Get or set the type of the target device used by the model.""" + return self.__target_device_type + + @target_device_type.setter + def target_device_type(self, value: str): + self.__target_device_type = value + + @property + def optimization_methods(self) -> Optional[List[OptimizationMethod]]: + """Get or set the optimization methods used on the model.""" + return self.__optimization_methods + + @optimization_methods.setter + def optimization_methods(self, value: List[OptimizationMethod]): + self.__optimization_methods = value + + @property + def optimization_type(self) -> ModelOptimizationType: + """Get or set the optimization type used for the model.""" + return self.__optimization_type + + @optimization_type.setter + def optimization_type(self, value: ModelOptimizationType): + self.__optimization_type = value + + @property + def optimization_objectives(self) -> Optional[Dict[str, str]]: + """Get or set the optimization level of the model.""" + return self.__optimization_objectives + + @optimization_objectives.setter + def optimization_objectives(self, value: Dict[str, str]): + self.__optimization_objectives = value + + @property + def performance_improvement(self) -> Optional[Dict[str, float]]: + """Get or set the performance improvement of the model.""" + return self.__performance_improvement + + @performance_improvement.setter + def performance_improvement(self, value: Dict[str, float]): + self.__performance_improvement = value + + @property + def model_size_reduction(self) -> float: + """Get or set the reduction in model size by optimizing.""" + return self.__model_size_reduction + + @model_size_reduction.setter + def model_size_reduction(self, value: float): + self.__model_size_reduction = value + + @property + def exportable_code(self) -> Optional[bytes]: + """Get the exportable_code from the exportable code adapter.""" + if self.__exportable_code_adapter is not None: + return self.__exportable_code_adapter.data + return None + + @exportable_code.setter + def exportable_code(self, data: Union[bytes, IDataSource]): + """Set the exportable code using the exportable code adapter.""" + self.__exportable_code_adapter = ExportableCodeAdapter(data_source=data) + + @property + def has_xai(self) -> float: + """Get or set the xAI flag.""" + return self.__has_xai + + @has_xai.setter + def has_xai(self, value: bool): + self.__has_xai = value + + @property + def exportable_code_adapter(self) -> Optional[ExportableCodeAdapter]: + """Returns the exportable code adapter.""" + return self.__exportable_code_adapter + + def get_data(self, key: str) -> bytes: + """Fetches byte data for a certain model. + + Args: + key: key to fetch data for + + Returns: + bytes: data for the key. + """ + return self.__model_adapters[key].data + + def set_data(self, key: str, data: Union[bytes, IDataSource], skip_deletion=False): + """Sets the data for a specified key, either from a binary blob or from a data source. + + If the key already exists it appends existing data url to a list of urls that will be removed upon saving the + model. Skip deletion parameter should only be true if replacing bytes data with a file. + """ + if not skip_deletion: + self.delete_data(key) + self.__model_adapters[key] = ModelAdapter(data) + + def delete_data(self, key: str): + """This function is used to delete data sources that are on the filesystem. + + If the key exists the model adapter will be appended to a list of model adapter that will be removed once the + model is saved by the repo. Note that an optimized model must contain at least 1 DataSource otherwise you are + left with an invalid optimized model. + """ + if key in self.__model_adapters: + self.model_adapters_to_delete.append(self.__model_adapters[key]) + del self.__model_adapters[key] + + @property + def model_adapters(self) -> Dict[str, ModelAdapter]: + """Returns the dictionary of model adapters for each data key.""" + return self.__model_adapters + + def is_optimized(self) -> bool: + """Returns a boolean indicating if the model has been optimized or not.""" + if self.optimization_type == ModelOptimizationType.NONE: + return False + return True + + def __eq__(self, other) -> bool: + """Compares if both the ModelEntities use the same dataset and have the same performance. + + Args: + other (ModelEntity): ModelEntity to compare with. + + Returns: + bool: True if the two ModelEntities are equal, False otherwise. + """ + if isinstance(other, ModelEntity): + return ( + self.id_ == other.id_ + and self.train_dataset == other.train_dataset + and self.performance == other.performance + ) + return False diff --git a/src/otx/api/entities/model_template.py b/src/otx/api/entities/model_template.py new file mode 100644 index 00000000000..fe7b100c7fb --- /dev/null +++ b/src/otx/api/entities/model_template.py @@ -0,0 +1,691 @@ +"""This file defines the ModelConfiguration, ModelEntity and Model classes.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import os +from dataclasses import dataclass, field +from enum import Enum, IntEnum, auto +from typing import Dict, List, NamedTuple, Optional, Sequence, Union, cast + +from omegaconf import DictConfig, ListConfig, OmegaConf + +from otx.api.configuration.elements import metadata_keys +from otx.api.configuration.enums import AutoHPOState +from otx.api.configuration.helper.utils import search_in_config_dict +from otx.api.entities.label import Domain + + +class TargetDevice(IntEnum): + """Represents the target device for a given model. + + This device might be used for instance be used for training or inference. + """ + + UNSPECIFIED = auto() + CPU = auto() + GPU = auto() + VPU = auto() + + +class ModelOptimizationMethod(Enum): + """Optimized model format.""" + + TENSORRT = auto() + OPENVINO = auto() + + def __str__(self) -> str: + """Returns ModelOptimizationMethod as string.""" + return str(self.name) + + +@dataclass +class DatasetRequirements: + """Expected requirements for the dataset in order to use this algorithm. + + Attributes: + classes (Optional[List[str]]): Classes which must be present in the dataset + """ + + classes: Optional[List[str]] = None + + +@dataclass +class ExportableCodePaths: + """The paths to the different versions of the exportable code for a given model template.""" + + default: Optional[str] = None + openvino: Optional[str] = None + + +class TaskFamily(Enum): + """Overall task family.""" + + VISION = auto() + FLOW_CONTROL = auto() + DATASET = auto() + + def __str__(self) -> str: + """Returns task family as a string.""" + return str(self.name) + + +class TaskInfo(NamedTuple): + """Task information. + + NamedTuple to store information about the task type like label domain, if it is + trainable, if it is an anomaly task and if it supports global or local labels. + """ + + domain: Domain + is_trainable: bool + is_anomaly: bool + is_global: bool + is_local: bool + + +class TaskType(Enum): + """The type of algorithm within the task family. + + Also contains relevant information about the task type like label domain, if it is trainable, + if it is an anomaly task or if it supports global or local labels. + + Args: + value (int): (Unused) Unique integer for .value property of Enum (auto() does not work) + task_info (TaskInfo): NamedTuple containing information about the task's capabilities + """ + + # pylint: disable=unused-argument + def __init__( + self, + value: int, + task_info: TaskInfo, + ): + self.domain = task_info.domain + self.is_trainable = task_info.is_trainable + self.is_anomaly = task_info.is_anomaly + self.is_global = task_info.is_global + self.is_local = task_info.is_local + + def __new__(cls, *args): + """Returns new instance.""" + obj = object.__new__(cls) + obj._value_ = args[0] + return obj + + NULL = 1, TaskInfo( + domain=Domain.NULL, + is_trainable=False, + is_anomaly=False, + is_global=False, + is_local=False, + ) + DATASET = 2, TaskInfo( + domain=Domain.NULL, + is_trainable=False, + is_anomaly=False, + is_global=False, + is_local=False, + ) + CLASSIFICATION = 3, TaskInfo( + domain=Domain.CLASSIFICATION, + is_trainable=True, + is_anomaly=False, + is_global=True, + is_local=False, + ) + SEGMENTATION = 4, TaskInfo( + domain=Domain.SEGMENTATION, + is_trainable=True, + is_anomaly=False, + is_global=False, + is_local=True, + ) + DETECTION = 5, TaskInfo( + domain=Domain.DETECTION, + is_trainable=True, + is_anomaly=False, + is_global=False, + is_local=True, + ) + ANOMALY_DETECTION = 6, TaskInfo( + domain=Domain.ANOMALY_DETECTION, + is_trainable=True, + is_anomaly=True, + is_global=False, + is_local=True, + ) + CROP = 7, TaskInfo( + domain=Domain.NULL, + is_trainable=False, + is_anomaly=False, + is_global=False, + is_local=False, + ) + TILE = 8, TaskInfo( + domain=Domain.NULL, + is_trainable=False, + is_anomaly=False, + is_global=False, + is_local=False, + ) + INSTANCE_SEGMENTATION = 9, TaskInfo( + domain=Domain.INSTANCE_SEGMENTATION, + is_trainable=True, + is_anomaly=False, + is_global=False, + is_local=True, + ) + ACTIVELEARNING = 10, TaskInfo( + domain=Domain.NULL, + is_trainable=False, + is_anomaly=False, + is_global=False, + is_local=False, + ) + ANOMALY_SEGMENTATION = 11, TaskInfo( + domain=Domain.ANOMALY_SEGMENTATION, + is_trainable=True, + is_anomaly=True, + is_global=False, + is_local=True, + ) + ANOMALY_CLASSIFICATION = 12, TaskInfo( + domain=Domain.ANOMALY_CLASSIFICATION, + is_trainable=True, + is_anomaly=True, + is_global=True, + is_local=False, + ) + ROTATED_DETECTION = 13, TaskInfo( + domain=Domain.ROTATED_DETECTION, + is_trainable=True, + is_anomaly=False, + is_global=False, + is_local=True, + ) + if os.getenv("FEATURE_FLAGS_OTX_ACTION_TASKS", "0") == "1": + ACTION_CLASSIFICATION = 14, TaskInfo( + domain=Domain.ACTION_CLASSIFICATION, + is_trainable=True, + is_anomaly=False, + is_global=False, + is_local=True, + ) + ACTION_DETECTION = 15, TaskInfo( + domain=Domain.ACTION_DETECTION, is_trainable=True, is_anomaly=False, is_global=False, is_local=True + ) + if os.getenv("FEATURE_FLAGS_OTX_VISUAL_PROMPTING_TASKS", "0") == "1": + VISUAL_PROMPTING = 16, TaskInfo( # TODO: Is 16 okay when action flag is False? + domain=Domain.VISUAL_PROMPTING, + is_trainable=True, + is_anomaly=False, + is_global=False, + is_local=True, # TODO: check whether is it local or not + ) + + def __str__(self) -> str: + """Returns name.""" + return self.name + + def __repr__(self) -> str: + """Returns name.""" + return self.name + + +def task_type_to_label_domain(task_type: TaskType) -> Domain: + """Links the task type to the label domain enum. + + Note that not all task types have an associated domain (e.g. crop task). + In this case, a ``ValueError`` is raised. + + Args: + task_type (TaskType): The task type to get the label domain for. + + Returns: + Domain: The label domain for the task type. + """ + mapping = { + TaskType.CLASSIFICATION: Domain.CLASSIFICATION, + TaskType.DETECTION: Domain.DETECTION, + TaskType.SEGMENTATION: Domain.SEGMENTATION, + TaskType.INSTANCE_SEGMENTATION: Domain.INSTANCE_SEGMENTATION, + TaskType.ANOMALY_CLASSIFICATION: Domain.ANOMALY_CLASSIFICATION, + TaskType.ANOMALY_DETECTION: Domain.ANOMALY_DETECTION, + TaskType.ANOMALY_SEGMENTATION: Domain.ANOMALY_SEGMENTATION, + TaskType.ROTATED_DETECTION: Domain.ROTATED_DETECTION, + } + if os.getenv("FEATURE_FLAGS_OTX_ACTION_TASKS", "0") == "1": + mapping = { + **mapping, + TaskType.ACTION_CLASSIFICATION: Domain.ACTION_CLASSIFICATION, + TaskType.ACTION_DETECTION: Domain.ACTION_DETECTION, + } + if os.getenv("FEATURE_FLAGS_OTX_VISUAL_PROMPTING_TASKS", "0") == "1": + mapping = { + **mapping, + TaskType.VISUAL_PROMPTING: Domain.VISUAL_PROMPTING, + } + + try: + return mapping[task_type] + except KeyError as exc: + raise ValueError(f"Task type {task_type} does not have any associated label domain.") from exc + + +@dataclass +class HyperParameterData: + """HyperParameter Data. + + Class that contains the raw hyper parameter data, for those hyper parameters for the model that are + user-configurable. + + Attributes: + base_path (Optional[str]): The path to the yaml file specifying the base configurable parameters to use in the + model. Defaults to None. + parameter_overrides (Dict): Nested dictionary that describes overrides for the metadata for the + user-configurable hyper parameters that are used in the model. This allows multiple models to share the + same base hyper-parameters, while for each individual model the defaults, parameter ranges, descriptions, + etc. can still be customized. + """ + + base_path: Optional[str] = None + parameter_overrides: Dict = field(default_factory=dict) + __data: Dict = field(default_factory=dict, repr=False) + __has_valid_configurable_parameters: bool = field(default=False, repr=False) + + def load_parameters(self, model_template_path: str): + """Load hyper parameters. + + Loads the actual hyper parameters defined in the file at `base_path`, and performs any overrides specified in + the `parameter_overrides`. + + Args: + model_template_path (str): file path to the model template file in which the HyperParameters live. + """ + has_valid_configurable_parameters = False + if self.base_path is not None and os.path.exists(model_template_path): + model_template_dir = os.path.dirname(model_template_path) + base_hyper_parameter_path = os.path.join(model_template_dir, self.base_path) + + config_dict = OmegaConf.load(base_hyper_parameter_path) + data = OmegaConf.to_container(config_dict) + if isinstance(data, dict): + self.__remove_parameter_values_from_data(data) + self.__data = data + has_valid_configurable_parameters = True + else: + raise ValueError( + f"Unexpected configurable parameter file found at path {base_hyper_parameter_path}" + f", expected a dictionary-like format, got list-like instead." + ) + if self.has_overrides and has_valid_configurable_parameters: + self.substitute_parameter_overrides() + self.__has_valid_configurable_parameters = has_valid_configurable_parameters + + @property + def data(self) -> Dict: + """Returns a dictionary containing the set of hyper parameters defined in the ModelTemplate. + + This does not contain the actual parameter values, but instead holds the parameter schema's in + a structured manner. The actual values should be either loaded from the database, or will be initialized from + the defaults upon creating a configurable parameter object out of this data. + """ + return self.__data + + @property + def has_overrides(self) -> bool: + """Returns True if any parameter overrides are defined by the HyperParameters instance, False otherwise.""" + return self.parameter_overrides != {} + + @property + def has_valid_configurable_parameters(self) -> bool: + """Check if configurable parameters are valid. + + Returns True if the HyperParameterData instance contains valid configurable parameters, extracted from the + model template. False otherwise. + """ + return self.__has_valid_configurable_parameters + + def substitute_parameter_overrides(self): + """Carries out the parameter overrides specified in the `parameter_overrides` attribute. + + Validates whether the overridden parameters exist in the base set of configurable parameters, + and whether the metadata values that should be overridden are valid metadata attributes. + """ + self.__substitute_parameter_overrides(self.parameter_overrides, self.__data) + + def __substitute_parameter_overrides(self, override_dict: Dict, parameter_dict: Dict): + """Substitutes parameters form override_dict into parameter_dict. + + Recursively substitutes overridden parameter values specified in `override_dict` into the base set of + hyper parameters passed in as `parameter_dict` + + Args: + override_dict (Dict): dictionary containing the parameter overrides + parameter_dict (Dict): dictionary that contains the base set of hyper parameters, in which the overridden + values are substituted + """ + for key, value in override_dict.items(): + if isinstance(value, dict) and not metadata_keys.allows_dictionary_values(key): + if key in parameter_dict.keys(): + self.__substitute_parameter_overrides(value, parameter_dict[key]) + else: + raise ValueError( + f"Unable to perform parameter override. Parameter or parameter group named {key} " + f"is not valid for the base hyper parameters specified in {self.base_path}" + ) + else: + if metadata_keys.allows_model_template_override(key): + parameter_dict[key] = value + else: + raise KeyError(f"{key} is not a valid keyword for hyper parameter overrides") + + @classmethod + def __remove_parameter_values_from_data(cls, data: dict): + """This method removes the actual parameter values from the input parameter data. + + These values should be removed because the parameters should be instantiated + from the default_values, instead of their values. + + NOTE: This method modifies its input dictionary, it does not return a new copy + + Args: + data: Parameter dictionary to remove values from + """ + data_copy = copy.deepcopy(data) + for key, value in data_copy.items(): + if isinstance(value, dict): + if key != metadata_keys.UI_RULES: + cls.__remove_parameter_values_from_data(data[key]) + elif key == "value": + data.pop(key) + + def manually_set_data_and_validate(self, hyper_parameters: dict): + """This function is used to manually set the hyper parameter data from a dictionary. + + It is meant to be used in testing only, in cases where the model + template is not backed up by an actual yaml file. + + Args: + hyper_parameters (Dict): Dictionary containing the data to be set + """ + self.__data = hyper_parameters + self.__has_valid_configurable_parameters = True + + +class InstantiationType(Enum): + """The method to instantiate a given task.""" + + NONE = auto() + CLASS = auto() + GRPC = auto() + + def __str__(self) -> str: + """Returns the name of the instantiation type.""" + return str(self.name) + + +@dataclass +class Dependency: + """Dependency required by the task. + + Attributes: + source (str): Source of the dependency + destination (str): Destination folder to install the dependency + size (Optional[int]): Size of the dependency in bytes + sha256 (Optional[str]): SHA-256 checksum of the dependency file + """ + + source: str + destination: str + size: Optional[int] = None + sha256: Optional[str] = None + + +@dataclass +class EntryPoints: + """Path of the Python classes implementing the task interface. + + Attributes: + base (str): Base interface implementing the functionality in a framework such as PyTorch or TensorFlow + openvino (Optional[str]): OpenVINO interface. + nncf (Optional[str]): NNCF interface + """ + + base: str + openvino: Optional[str] = None + nncf: Optional[str] = None + + +class ModelCategory(Enum): + """Represents model category regarding accuracy & speed trade-off.""" + + SPEED = auto() + BALANCE = auto() + ACCURACY = auto() + OTHER = auto() + + def __str__(self) -> str: + """Returns the name of the model category.""" + return str(self.name) + + +class ModelStatus(Enum): + """Represents model status regarding deprecation process.""" + + ACTIVE = auto() + DEPRECATED = auto() + + def __str__(self) -> str: + """Returns the name of the model status.""" + return str(self.name) + + +# pylint: disable=too-many-instance-attributes +@dataclass +class ModelTemplate: + """This class represents a Task in the Task database. + + It can be either a CLASS type, with the class path specified or a GRPC type with its address. + The task chain uses this information to setup a `ChainLink` (A task in the chain) + + model_template_id (str): ID of the model template + model_template_path (str): path to the original model template file + name (str): user-friendly name for the algorithm used in the task + task_family (TaskFamily): overall task family of the task. One of VISION, FLOW_CONTROL AND DATASET. + task_type (TaskType): Type of algorithm within task family. + instantiation (InstantiationType): InstantiationType (CLASS or GRPC) + summary (str): Summary of what the algorithm does. Defaults to "". + framework (Optional[str]): The framework used by the algorithm. Defaults to None. + max_nodes (int): Max number of nodes for training. Defaults to 1. + application (Optional[str]): Name of the application solved by this algorithm. Defaults to None. + dependencies (Liar[Dependency]): List of dependencies required by the algorithm. Defaults to empty `field`. + initial_weights (Optional[str]): Optional URL to the initial weights used by the algorithm. Defaults to None + training_targets (List[TargetDevice]): device used for training. Defaults to empty `field`. + inference_targets (List[TargetDevices]): device used for inference. Defaults to empty `field`. + dataset_requirements (DatasetRequirements): list of dataset requirements. Defaults to empty `field`. + model_optimization_methods (List[ModelOptimizationMethod]): list of ModelOptimizationMethod. + This lists all methods available to optimize the inference model for the task + hyper_parameters (HyperParameterData): HyperParameterData object containing the base path to the configurable + parameter definition, as well as any overrides for the base parameters that are specific for the + current template. + is_trainable (bool): specify whether task is trainable + capabilities (List[str]): list of task capabilities + grpc_address (Optional[str]): the grpc host address (for instantiation type == GRPC) + entrypoints (Optional[Entrypoints]): Entrypoints implementing the Python task interface + base_model_path (str): Path to template file for the base model used for nncf compression. + exportable_code_paths (ExportableCodePaths): if it exists, the path to the exportable code sources. + Defaults to empty `field`. + task_type_sort_priority (int): priority of order of how tasks are shown in the pipeline dropdown for a given task + type. E.g. for classification Inception is default and has weight 0. Unassigned priority will have -1 as + priority. mobilenet is less important, and has a higher value. Default is zero (the highest priority). + gigaflops (float): how many billions of operations are required to do inference on a single data item. + size (float): how much disk space the model will approximately take. + model_category (ModelCategory): Represents model category regarding accuracy & speed trade-off. Default to OTHER. + model_status (ModelStatus): Represents model status regarding deprecation process. Default to ACTIVE. + is_default_for_task (bool): Whether this model is a default recommendation for the task + """ + + model_template_id: str + model_template_path: str + name: str + task_family: TaskFamily + task_type: TaskType + instantiation: InstantiationType + summary: str = "" + framework: Optional[str] = None + max_nodes: int = 1 + application: Optional[str] = None + dependencies: List[Dependency] = field(default_factory=list) + initial_weights: Optional[str] = None + training_targets: List[TargetDevice] = field(default_factory=list) + inference_targets: List[TargetDevice] = field(default_factory=list) + dataset_requirements: DatasetRequirements = field(default_factory=DatasetRequirements) + model_optimization_methods: List[ModelOptimizationMethod] = field(default_factory=list) + hyper_parameters: HyperParameterData = field(default_factory=HyperParameterData) + is_trainable: bool = True + capabilities: List[str] = field(default_factory=list) + grpc_address: Optional[str] = None + entrypoints: Optional[EntryPoints] = None + base_model_path: str = "" + exportable_code_paths: ExportableCodePaths = field(default_factory=ExportableCodePaths) + task_type_sort_priority: int = -1 + gigaflops: float = 0 + size: float = 0 + hpo: Optional[Dict] = None + model_category: ModelCategory = ModelCategory.OTHER + model_status: ModelStatus = ModelStatus.ACTIVE + is_default_for_task: bool = False + + def __post_init__(self): + """Do sanitation checks before loading the hyper-parameters.""" + if self.instantiation == InstantiationType.GRPC and self.grpc_address == "": + raise ValueError("Task is registered as gRPC, but no gRPC address is specified") + if self.instantiation == InstantiationType.CLASS and self.entrypoints is None: + raise ValueError("Task is registered as CLASS, but entrypoints were not specified") + if self.task_family == TaskFamily.VISION and self.hyper_parameters.base_path is None: + raise ValueError("Task is registered as a VISION task but no hyper parameters were defined.") + if self.task_family != TaskFamily.VISION and self.hyper_parameters.base_path is not None: + raise ValueError("Hyper parameters are currently not supported for non-VISION tasks.") + + # Load the full hyper parameters + self.hyper_parameters.load_parameters(self.model_template_path) + + def computes_uncertainty_score(self) -> bool: + """Returns true if "compute_uncertainty_score" is in capabilities false otherwise.""" + return "compute_uncertainty_score" in self.capabilities + + def computes_representations(self) -> bool: + """Returns true if "compute_representations" is in capabilities.""" + return "compute_representations" in self.capabilities + + def is_task_global(self) -> bool: + """Returns ``True`` if the task is global task i.e. if task produces global labels.""" + return self.task_type.is_global + + def supports_auto_hpo(self) -> bool: + """Returns `True` if the algorithm supports automatic hyper parameter optimization, `False` otherwise.""" + if not self.hyper_parameters.has_valid_configurable_parameters: + return False + auto_hpo_state_results = search_in_config_dict( + self.hyper_parameters.data, key_to_search=metadata_keys.AUTO_HPO_STATE + ) + for result in auto_hpo_state_results: + if str(result[0]).lower() == str(AutoHPOState.POSSIBLE): + return True + return False + + +class NullModelTemplate(ModelTemplate): + """Represent an empty model template. Note that a task based on this model template cannot be instantiated.""" + + def __init__(self) -> None: + super().__init__( + model_template_id="", + model_template_path="", + task_family=TaskFamily.FLOW_CONTROL, + task_type=TaskType.NULL, + name="Null algorithm", + instantiation=InstantiationType.NONE, + capabilities=[], + ) + + +ANOMALY_TASK_TYPES: Sequence[TaskType] = ( + TaskType.ANOMALY_DETECTION, + TaskType.ANOMALY_CLASSIFICATION, + TaskType.ANOMALY_SEGMENTATION, +) + + +TRAINABLE_TASK_TYPES: Sequence[TaskType] = ( + TaskType.CLASSIFICATION, + TaskType.DETECTION, + TaskType.SEGMENTATION, + TaskType.INSTANCE_SEGMENTATION, + TaskType.ANOMALY_DETECTION, + TaskType.ANOMALY_CLASSIFICATION, + TaskType.ANOMALY_SEGMENTATION, + TaskType.ROTATED_DETECTION, +) +if os.getenv("FEATURE_FLAGS_OTX_ACTION_TASKS", "0") == "1": + TRAINABLE_TASK_TYPES = ( + *TRAINABLE_TASK_TYPES, + TaskType.ACTION_CLASSIFICATION, + TaskType.ACTION_DETECTION, + ) +if os.getenv("FEATURE_FLAGS_OTX_VISUAL_PROMPTING_TASKS", "0") == "1": + TRAINABLE_TASK_TYPES = ( + *TRAINABLE_TASK_TYPES, + TaskType.VISUAL_PROMPTING, + ) + + +def _parse_model_template_from_omegaconf(config: Union[DictConfig, ListConfig]) -> ModelTemplate: + """Parse an OmegaConf configuration into a model template. + + Args: + config (Union[DictConfig, ListConfig]): The configuration to parse. + + Returns: + ModelTemplate: The parsed model template. + """ + schema = OmegaConf.structured(ModelTemplate) + config = OmegaConf.merge(schema, config) + return cast(ModelTemplate, OmegaConf.to_object(config)) + + +def parse_model_template(model_template_path: str) -> ModelTemplate: + """Read a model template from a file. + + Args: + model_template_path (str): Path to the model template template.yaml file + + Returns: + ModelTemplate: The model template parsed from the file. + """ + config = OmegaConf.load(model_template_path) + if not isinstance(config, DictConfig): + raise ValueError("Expected the configuration file to contain a dictionary, not a list") + + if "model_template_id" not in config: + config["model_template_id"] = config["name"].replace(" ", "_") + config["model_template_path"] = model_template_path + return _parse_model_template_from_omegaconf(config) + + +def parse_model_template_from_dict(model_template_dict: dict) -> ModelTemplate: + """Read a model template from a dictionary. + + Note that the model_template_id must be defined inside the dictionary. + + Args: + model_template_dict (dict): Dictionary containing the model template. + + Returns: + ModelTemplate: The model template. + """ + config = OmegaConf.create(model_template_dict) + return _parse_model_template_from_omegaconf(config) diff --git a/src/otx/api/entities/optimization_parameters.py b/src/otx/api/entities/optimization_parameters.py new file mode 100644 index 00000000000..7a35c7c2ed9 --- /dev/null +++ b/src/otx/api/entities/optimization_parameters.py @@ -0,0 +1,34 @@ +"""This module implements the OptimizationParameters entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from dataclasses import dataclass +from typing import Any, Callable, Optional + + +# pylint: disable=unused-argument +def default_progress_callback(progress: int, score: Optional[float] = None): + """This is the default progress callback for OptimizationParameters.""" + + +def default_save_model_callback(): + """This is the default save model callback for OptimizationParameters.""" + + +@dataclass +class OptimizationParameters: + """Optimization parameters. + + resume (bool): Set to ``True`` if optimization must be resume with the optimizer state; + set to ``False`` to discard the optimizer state and start with fresh optimizer + update_progress (Callable[int], None): Callback which can be used to provide updates about the progress of a task. + save_model (Callable[List, None]): Callback to notify that the model weights have been changed. + This callback can be used by the task when temporary weights should be saved (for instance, at the + end of an epoch). If this callback has been used to save temporary weights, those weights will be + used to resume optimization if for some reason training was suspended. + """ + + resume: bool = False + update_progress: Callable[[int, Optional[float]], Any] = default_progress_callback + save_model: Callable[[], None] = default_save_model_callback diff --git a/src/otx/api/entities/result_media.py b/src/otx/api/entities/result_media.py new file mode 100644 index 00000000000..6079f3da48f --- /dev/null +++ b/src/otx/api/entities/result_media.py @@ -0,0 +1,121 @@ +"""This module implements the ResultMediaEntity.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Optional + +import numpy as np + +from otx.api.entities.annotation import Annotation, AnnotationSceneEntity +from otx.api.entities.label import LabelEntity +from otx.api.entities.metadata import IMetadata +from otx.api.entities.shapes.rectangle import Rectangle + + +# pylint: disable=too-many-instance-attributes; Requires refactor +class ResultMediaEntity(IMetadata): + """Represents a media (e.g. an image which was generated by a task). + + For instance, a `ResultMediaEntity` could be an attention map generated by a classification task. + + The result media contains media data, which is associated with a + `otx.api.entities.annotation.AnnotationSceneEntity` and related to an optional + `otx.api.entities.label.LabelEntity`. + + Example: + >>> from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, + ) + >>> from otx.api.entities.id import ID + >>> from otx.api.entities.label import Domain, LabelEntity + >>> from otx.api.entities.result_media import ResultMediaEntity + >>> from otx.api.entities.scored_label import LabelSource, ScoredLabel + >>> from otx.api.entities.shapes.rectangle import Rectangle + + >>> source = LabelSource( + user_id="user_entity", model_id=ID("efficientnet"), model_storage_id=ID("efficientnet-storage") + ) + >>> falcon_label = LabelEntity(name="Falcon", domain=Domain.DETECTION) + >>> eagle_label = LabelEntity(name="Eagle", domain=Domain.DETECTION) + >>> falcon_bb = Rectangle(x1=0.0, y1=0.0, x2=0.5, y2=0.5) + >>> falcon_scored_label = ScoredLabel(label=falcon_label, probability=0.9, label_source=source) + >>> eagle_bb = Rectangle(x1=0.2, y1=0.2, x2=0.8, y2=0.8) + >>> eagle_scored_label = ScoredLabel(label=eagle_label, probability=0.6, label_source=source) + >>> annotation_scene = AnnotationSceneEntity( + annotations=[ + Annotation(shape=falcon_bb, labels=[falcon_scored_label]), + Annotation(shape=eagle_bb, labels=[eagle_scored_label]), + ], kind=AnnotationSceneKind.PREDICTION + ) + >>> ResultMediaEntity( + name="Model Predictions", + type="Bounding Box Annotations", + annotation_scene=annotation_scene, + numpy=image_array + ) + + Args: + name (str): Name. + type (str): The type of data (e.g. Attention map). This type is descriptive. + annotation_scene (AnnotationScene Entity): Associated annotation which was generated by the task + alongside this media. + numpy (np.ndarray): The data as a numpy array. + roi (Optional[Annotation]): The ROI covered by this media. If null, assume the entire image. Defaults to None. + label (Optional[LabelEntity]): A label associated with this media. Defaults to None. + """ + + # pylint: disable=redefined-builtin, too-many-arguments; + def __init__( + self, + name: str, + type: str, + annotation_scene: AnnotationSceneEntity, + numpy: np.ndarray, + roi: Optional[Annotation] = None, + label: Optional[LabelEntity] = None, + ): + self.name = name + self.type = type + self.annotation_scene = annotation_scene + self.roi = Annotation(Rectangle.generate_full_box(), labels=[]) if roi is None else roi + self.label = label + self._numpy = np.copy(numpy) + + def __repr__(self): + """Returns a string with all the attributes of the ResultMediaEntity.""" + return ( + "ResultMediaEntity(" + f"name={self.name}, " + f"type={self.type}, " + f"annotation_scene={self.annotation_scene}, " + f"roi={self.roi}, " + f"label={self.label})" + ) + + @property + def width(self) -> int: + """Returns the width of the result media.""" + return self.numpy.shape[1] + + @property + def height(self) -> int: + """Returns the height of the result media.""" + return self.numpy.shape[0] + + @property + def numpy(self) -> np.ndarray: + """Returns the data.""" + return self._numpy + + @numpy.setter + def numpy(self, value): + self._numpy = value + + def __eq__(self, other): + """Checks if the annotation_scene and roi matches with the other ResultMediaEntity.""" + if isinstance(other, ResultMediaEntity): + return self.annotation_scene == other.annotation_scene and self.roi == other.roi + return False diff --git a/src/otx/api/entities/resultset.py b/src/otx/api/entities/resultset.py new file mode 100644 index 00000000000..8eba89a58f6 --- /dev/null +++ b/src/otx/api/entities/resultset.py @@ -0,0 +1,185 @@ +"""This module implements the ResultSet entity.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import abc +import datetime +from enum import Enum +from typing import Optional + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.metrics import NullPerformance, Performance +from otx.api.entities.model import ModelEntity +from otx.api.utils.time_utils import now + + +class ResultsetPurpose(Enum): + """This defines the purpose of the resultset. + + EVALUATION denotes resultsets generated at Evaluation stage on validation subset. + + TEST denotes resultsets generated at Evaluation stage on test subset. + + PREEVALUATION denotes resultsets generated at Preevaluation stage (e.g., train from + scratch) onn validation subset. + """ + + EVALUATION = 0 + TEST = 1 + PREEVALUATION = 2 + + def __repr__(self): + """Returns ResultsetPurpose as a string.""" + return str(self.name) + + def __str__(self): + """Returns a user friendly representation of the ResultSetPurpose. + + This that can be used for instance in a progress reporting message. + """ + user_friendly_names = {0: "Validation", 1: "Test", 2: "Pre-validation"} + return user_friendly_names[self.value] + + +class ResultSetEntity(metaclass=abc.ABCMeta): + """ResultsetEntity. + + It aggregates: + - the dataset containing ground truth (based on user annotations) + - the dataset containing predictions for the above ground truth dataset + + In addition, it links to the model which computed the predictions, as well as the performance of this model on the + ground truth dataset. + + Args: + model: the model using which the prediction_dataset has been + generated + ground_truth_dataset: the dataset containing ground truth + annotation + prediction_dataset: the dataset containing prediction + purpose: see :class:`ResultsetPurpose` + performance: the performance of the model on the ground truth + dataset + creation_date: the date time which the resultset is created. Set + to None to set this to + id: the id of the resultset. Set to ID() so that a new unique ID + will be assigned upon saving. If the argument is None, it + will be set to ID() + datetime.now(datetime.timezone.utc) + """ + + # pylint: disable=redefined-builtin, too-many-arguments; Requires refactor + def __init__( + self, + model: ModelEntity, + ground_truth_dataset: DatasetEntity, + prediction_dataset: DatasetEntity, + purpose: ResultsetPurpose = ResultsetPurpose.EVALUATION, + performance: Optional[Performance] = None, + creation_date: Optional[datetime.datetime] = None, + id: Optional[ID] = None, + ): + id = ID() if id is None else id + performance = NullPerformance() if performance is None else performance + creation_date = now() if creation_date is None else creation_date + self.__id_ = id + self.__model = model + self.__prediction_dataset = prediction_dataset + self.__ground_truth_dataset = ground_truth_dataset + self.__performance = performance + self.__purpose = purpose + self.__creation_date = creation_date + + @property + def id_(self) -> ID: + """Returns the id of the ResultSet.""" + return self.__id_ + + @id_.setter + def id_(self, value: ID) -> None: + self.__id_ = value + + @property + def id(self) -> ID: + """DEPRECATED.""" + return self.__id_ + + @id.setter + def id(self, value: ID): + """DEPRECATED.""" + self.__id_ = value + + @property + def model(self) -> ModelEntity: + """Returns the model that is used for the ResultSet.""" + return self.__model + + @model.setter + def model(self, value: ModelEntity) -> None: + self.__model = value + + @property + def prediction_dataset(self) -> DatasetEntity: + """Returns the prediction dataset that is used in the ResultSet.""" + return self.__prediction_dataset + + @prediction_dataset.setter + def prediction_dataset(self, value: DatasetEntity) -> None: + self.__prediction_dataset = value + + @property + def ground_truth_dataset(self) -> DatasetEntity: + """Returns the ground truth dataset that is used in the ResultSet.""" + return self.__ground_truth_dataset + + @ground_truth_dataset.setter + def ground_truth_dataset(self, value: DatasetEntity) -> None: + self.__ground_truth_dataset = value + + @property + def performance(self) -> Performance: + """Returns the performance of the model on the ground truth dataset.""" + return self.__performance + + @performance.setter + def performance(self, value: Performance) -> None: + self.__performance = value + + @property + def purpose(self) -> ResultsetPurpose: + """Returns the purpose of the ResultSet, for example ResultSetPurpose.EVALUATION.""" + return self.__purpose + + @purpose.setter + def purpose(self, value: ResultsetPurpose) -> None: + self.__purpose = value + + @property + def creation_date(self) -> datetime.datetime: + """Returns the creation date of the ResultSet.""" + return self.__creation_date + + @creation_date.setter + def creation_date(self, value: datetime.datetime) -> None: + self.__creation_date = value + + def has_score_metric(self) -> bool: + """Returns True if the resultset contains non-null performance and score value.""" + return not isinstance(self.performance, NullPerformance) + + def __repr__(self): + """String representation of the resultset.""" + return ( + f"{type(self).__name__}(" + f"model={self.model}, " + f"ground_truth_dataset={self.ground_truth_dataset}, " + f"prediction_dataset={self.prediction_dataset}, " + f"purpose={self.purpose}, " + f"performance={self.performance}, " + f"creation_date={self.creation_date}, " + f"id={self.id_})" + ) diff --git a/src/otx/api/entities/scored_label.py b/src/otx/api/entities/scored_label.py new file mode 100644 index 00000000000..c801e2969c4 --- /dev/null +++ b/src/otx/api/entities/scored_label.py @@ -0,0 +1,130 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +"""This module define the scored label entity.""" + +import datetime +import math +from dataclasses import dataclass +from typing import Optional + +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity + + +@dataclass +class LabelSource: + """This dataclass contains information about the source of a scored label. + + For annotations, the id of the user who created the label and for predictions, the + id and model storage id of the model that created the prediction. When a user has + accepted a predictions as is, both the user id of the user who accepted and the + model/model storage id of the model that predicted should be filled in. + """ + + user_id: str = "" + model_id: ID = ID() + model_storage_id: ID = ID() + + +class ScoredLabel: + """This represents a label along with a probability. This is used inside `Annotation` class. + + Args: + label (LabelEntity): Label entity to which probability and source are attached. + probability (float): a float denoting the probability of the shape belonging to the label. + label_source (LabelSource): a LabelSource dataclass containing the id of the user who created + or the model that predicted this label. + """ + + def __init__( + self, + label: LabelEntity, + probability: float = 0.0, + label_source: Optional[LabelSource] = None, + ): + if math.isnan(probability) or (not 0 <= probability <= 1.0): + raise ValueError(f"Probability should be in range [0, 1], {probability} is given") + + self.label = label + self.probability = probability + self.label_source = label_source if label_source is not None else LabelSource() + + @property + def name(self) -> str: + """Name of the label.""" + return self.label.name + + @property + def id_(self) -> ID: + """Returns the label id.""" + return self.label.id_ + + @property + def id(self) -> ID: + """DEPRECATED.""" + return self.label.id + + @property + def color(self) -> Color: + """Color of the label.""" + return self.label.color + + @property + def hotkey(self) -> str: + """Hotkey of the label.""" + return self.label.hotkey + + @property + def domain(self) -> Domain: + """Domain of the label.""" + return self.label.domain + + @property + def is_empty(self) -> bool: + """Check if the label is empty.""" + return self.label.is_empty + + @property + def creation_date(self) -> datetime.datetime: + """Creation data of the label.""" + return self.label.creation_date + + def get_label(self) -> LabelEntity: + """Gets the label that the ScoredLabel object was initialized with.""" + return self.label + + def __repr__(self): + """String representation of the label.""" + return ( + f"ScoredLabel({self.id_}, name={self.name}, probability={self.probability}, " + f"domain={self.domain}, color={self.color}, hotkey={self.hotkey}, " + f"label_source={self.label_source})" + ) + + def __eq__(self, other: object) -> bool: + """Checks if the label is equal to the other label. + + Args: + other (ScoredLabel): Label to compare with + + Returns: + bool: True if the labels are equal, False otherwise + """ + if isinstance(other, ScoredLabel): + return ( + self.id_ == other.id_ + and self.name == other.name + and self.color == other.color + and self.hotkey == other.hotkey + and self.probability == other.probability + and self.domain == other.domain + and self.label_source == other.label_source + ) + return False + + def __hash__(self): + """Returns hash of the label.""" + return hash(str(self)) diff --git a/src/otx/api/entities/shapes/__init__.py b/src/otx/api/entities/shapes/__init__.py new file mode 100644 index 00000000000..76a8f014fe6 --- /dev/null +++ b/src/otx/api/entities/shapes/__init__.py @@ -0,0 +1,10 @@ +"""Entities containing shapes.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# from .rectangle import * +# from .circle import * +# from .polygon import * +# from .shape import * diff --git a/src/otx/api/entities/shapes/ellipse.py b/src/otx/api/entities/shapes/ellipse.py new file mode 100644 index 00000000000..303d66fb482 --- /dev/null +++ b/src/otx/api/entities/shapes/ellipse.py @@ -0,0 +1,283 @@ +"""This module implements the Ellipse shape entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# Conflict with Isort +# pylint: disable=wrong-import-order + +import datetime +import math +from typing import List, Optional, Tuple + +import numpy as np +from scipy import optimize, special +from shapely.geometry import Polygon as shapely_polygon + +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.shapes.shape import Shape, ShapeType +from otx.api.utils.time_utils import now + +# pylint: disable=invalid-name + + +class Ellipse(Shape): + """Ellipse represents an ellipse that is encapsulated by a Rectangle. + + - x1 and y1 represent the top-left coordinate of the encapsulating rectangle + - x2 and y2 representing the bottom-right coordinate of the encapsulating rectangle + + Args: + x1: left x coordinate of encapsulating rectangle + y1: top y coordinate of encapsulating rectangle + x2: right x coordinate of encapsulating rectangle + y2: bottom y coordinate of encapsulating rectangle + modification_date: last modified date + """ + + # pylint: disable=too-many-arguments; Requires refactor + def __init__( + self, + x1: float, + y1: float, + x2: float, + y2: float, + modification_date: Optional[datetime.datetime] = None, + ): + modification_date = now() if modification_date is None else modification_date + super().__init__( + shape_type=ShapeType.ELLIPSE, + modification_date=modification_date, + ) + + for (x, y) in [(x1, y1), (x2, y2)]: + self._validate_coordinates(x, y) + + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + if self.width <= 0 or self.height <= 0: + raise ValueError( + f"Invalid Ellipse with coordinates: x1={self.x1}, y1={self.y1}, x2={self.x2}," f" y2={self.y2}" + ) + + def __repr__(self): + """Returns the representation of the Ellipse.""" + return f"Ellipse(x1={self.x1}, y1={self.y1}, x2={self.x2}, y2={self.y2})" + + def __eq__(self, other): + """Returns True if Ellipse is equal to other.""" + if isinstance(other, Ellipse): + return ( + self.x1 == other.x1 + and self.y1 == other.y1 + and self.x2 == other.x2 + and self.y2 == other.y2 + and self.modification_date == other.modification_date + ) + return False + + def __hash__(self): + """Returns the hash of the Ellipse.""" + return hash(str(self)) + + @property + def width(self) -> float: + """Returns the width [x-axis] of the ellipse. + + Example: + + >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.5) + >>> e1.width + 0.5 + + Returns: + the width of the ellipse. (x-axis) + """ + return self.x2 - self.x1 + + @property + def height(self) -> float: + """Returns the height [y-axis] of the ellipse. + + Example: + + >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.5) + >>> e1.height + 0.5 + + Returns: + the height of the ellipse. (y-axis) + """ + return self.y2 - self.y1 + + @property + def x_center(self) -> float: + """Returns the x coordinate in the center of the ellipse.""" + return self.x1 + self.width / 2 + + @property + def y_center(self) -> float: + """Returns the y coordinate in the center of the ellipse.""" + return self.y1 + self.height / 2 + + @property + def minor_axis(self) -> float: + """Returns the minor axis of the ellipse. + + Example: + + >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.4) + >>> e1.minor_axis + 0.2 + + Returns: + minor axis of ellipse. + """ + if self.width > self.height: + return self.height / 2 + return self.width / 2 + + @property + def major_axis(self) -> float: + """Returns the major axis of the ellipse. + + Example: + + >>> e1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.4) + >>> e1.major_axis + 0.25 + + Returns: + major axis of ellipse. + """ + if self.height > self.width: + return self.height / 2 + return self.width / 2 + + def normalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Ellipse": + """Transforms from the `roi` coordinate system to the normalized coordinate system. + + This function is the inverse of ``denormalize_wrt_roi_shape``. + + Example: + Assume we have Ellipse `c1` which lives in the top-right quarter of a 2D space. + The 2D space where `c1` lives in is an `roi` living in the top-left quarter of the normalized coordinate + space. This function returns Ellipse `c1` expressed in the normalized coordinate space. + + >>> from otx.api.entities.annotation import Annotation + >>> from otx.api.entities.shapes.rectangle import Rectangle + >>> from otx.api.entities.shapes.ellipse import Ellipse + >>> c1 = Ellipse(x1=0.5, y1=0.5, x2=0.6, y2=0.6) + >>> roi = Rectangle(x1=0.0, x2=0.5, y1=0.0, y2=0.5) + >>> normalized = c1.normalize_wrt_roi_shape(roi_shape) + >>> normalized + Ellipse(, x1=0.25, y1=0.25, x2=0.3, y2=0.3) + + Args: + roi_shape: Region of Interest + + Returns: + New polygon in the image coordinate system + """ + + if not isinstance(roi_shape, Rectangle): + raise ValueError("roi_shape has to be a Rectangle.") + + roi_shape = roi_shape.clip_to_visible_region() + + return Ellipse( + x1=self.x1 * roi_shape.width + roi_shape.x1, + y1=self.y1 * roi_shape.height + roi_shape.y1, + x2=self.x2 * roi_shape.width + roi_shape.x1, + y2=self.y2 * roi_shape.height + roi_shape.y1, + ) + + def denormalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Ellipse": + """Transforming shape from the normalized coordinate system to the `roi` coordinate system. + + This function is the inverse of ``normalize_wrt_roi_shape`` + + Example: + Assume we have Ellipse `c1` which lives in the top-right quarter of the normalized coordinate space. + The `roi` is a rectangle living in the half right of the normalized coordinate space. + This function returns Ellipse `c1` expressed in the coordinate space of `roi`. (should return top-half) + + Ellipse denormalized to a rectangle as ROI + + >>> from otx.api.entities.annotation import Annotation + >>> from otx.api.entities.shapes.ellipse import Ellipse + >>> c1 = Ellipse(x1=0.5, x2=1.0, y1=0.0, y2=0.5) # An ellipse in the top right + >>> roi = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0) # the half-right + >>> normalized = c1.denormalize_wrt_roi_shape(roi_shape) # should return top half + >>> normalized + Ellipse(, x1=0.0, y1=0.0, x2=1.0, y2=0.5) + + Args: + roi_shape: Region of Interest + + Returns: + New polygon in the ROI coordinate system + """ + if not isinstance(roi_shape, Rectangle): + raise ValueError("roi_shape has to be a Rectangle.") + + roi_shape = roi_shape.clip_to_visible_region() + + x1 = (self.x1 - roi_shape.x1) / roi_shape.width + y1 = (self.y1 - roi_shape.y1) / roi_shape.height + x2 = (self.x2 - roi_shape.x1) / roi_shape.width + y2 = (self.y2 - roi_shape.y1) / roi_shape.height + + return Ellipse(x1=x1, y1=y1, x2=x2, y2=y2) + + # pylint: disable=no-member; PyLint cannot find scipy.special.ellipeinc() + def get_evenly_distributed_ellipse_coordinates(self, number_of_coordinates: int = 50) -> List[Tuple[float, float]]: + """Returns evenly distributed coordinates along the ellipse. + + Makes use of scipy.special.ellipeinc() which provides the numerical integral along the perimeter of the ellipse, + and scipy.optimize.root() for solving the equal-arcs length equation for the angles. + + Args: + number_of_coordinates: number of evenly distributed points + to generate along the ellipsis line + + Returns: + list of tuple's with coordinates along the ellipse line + """ + angles = 2 * np.pi * np.arange(number_of_coordinates) / number_of_coordinates + e = (1.0 - self.minor_axis**2.0 / self.major_axis**2.0) ** 0.5 + total_size = special.ellipeinc(2.0 * np.pi, e) + arc_size = total_size / number_of_coordinates + arcs = np.arange(number_of_coordinates) * arc_size + res = optimize.root(lambda x: (special.ellipeinc(x, e) - arcs), angles) + angles = res.x + if self.width > self.height: + x_points = list(self.major_axis * np.sin(angles)) + y_points = list(self.minor_axis * np.cos(angles)) + else: + x_points = list(self.minor_axis * np.cos(angles)) + y_points = list(self.major_axis * np.sin(angles)) + coordinates = [ + (point_x + self.x_center, point_y + self.y_center) for point_x, point_y in zip(x_points, y_points) + ] + return coordinates + + def _as_shapely_polygon(self) -> shapely_polygon: + coordinates = self.get_evenly_distributed_ellipse_coordinates() + return shapely_polygon(coordinates) + + def get_area(self) -> float: + """Computes the approximate area of the Ellipse. + + Area is a value between 0 and 1, computed as + `pi * vertex * co-vertex`. + + >>> Ellipse(x1=0, y1=0, x2=0.8, y2=0.4).get_area() + 0.25132741228718347 + + Returns: + area of the shape + """ + return math.pi * self.minor_axis * self.major_axis diff --git a/src/otx/api/entities/shapes/polygon.py b/src/otx/api/entities/shapes/polygon.py new file mode 100644 index 00000000000..f510a8eeaab --- /dev/null +++ b/src/otx/api/entities/shapes/polygon.py @@ -0,0 +1,228 @@ +"""This module implements the Polygon Shape entity.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# Conflict with Isort +# pylint: disable=wrong-import-order + +import datetime +import warnings +from operator import attrgetter +from typing import List, Optional +import numpy as np + +from shapely.geometry import Polygon as shapely_polygon + +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.shapes.shape import Shape, ShapeType +from otx.api.utils.time_utils import now + + +class Point: + """This class defines a Point with an X and Y coordinate. + + Multiple points can be used to represent a Polygon + """ + + __slots__ = ["x", "y"] + + def __init__(self, x: float, y: float): + self.x = np.clip(x, a_min=0.0, a_max=1.0) + self.y = np.clip(y, a_min=0.0, a_max=1.0) + + def __repr__(self): + """String representation of the point.""" + return f"Point({self.x}, {self.y})" + + def __eq__(self, other): + """Checks if two points have the same x and y coordinates.""" + if isinstance(other, Point): + return self.x == other.x and self.y == other.y + return False + + def normalize_wrt_roi(self, roi_shape: Rectangle) -> "Point": + """The inverse of denormalize_wrt_roi_shape. + + Transforming Polygon from the `roi` coordinate system to the normalized coordinate system. + This is used when the tasks want to save the analysis results. + + For example in Detection -> Segmentation pipeline, the analysis results of segmentation + needs to be normalized to the roi (bounding boxes) coming from the detection. + + Args: + roi_shape (Point): the shape of the roi + """ + roi_shape = roi_shape.clip_to_visible_region() + width = roi_shape.width + height = roi_shape.height + x1 = roi_shape.x1 + y1 = roi_shape.y1 + return Point(x=self.x * width + x1, y=self.y * height + y1) + + def denormalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Point": + """The inverse of normalize_wrt_roi_shape. + + Transforming Polygon from the normalized coordinate system to the `roi` coordinate system. + This is used to pull ground truth during training process of the tasks. + Examples given in the Shape implementations. + + Args: + roi_shape (Rectangle): the shape of the roi + """ + roi_shape = roi_shape.clip_to_visible_region() + + return Point( + x=(self.x - roi_shape.x1) / roi_shape.width, + y=(self.y - roi_shape.y1) / roi_shape.height, + ) + + +class Polygon(Shape): + """Represents a polygon formed by a list of coordinates. + + NB Freehand drawings are also stored as polygons. + + Args: + points: list of Point's forming the polygon + modification_date: last modified date + """ + + # pylint: disable=too-many-arguments; Requires refactor + def __init__( + self, + points: List[Point], + modification_date: Optional[datetime.datetime] = None, + ): + modification_date = now() if modification_date is None else modification_date + super().__init__( + shape_type=ShapeType.POLYGON, + modification_date=modification_date, + ) + + if len(points) == 0: + raise ValueError("Cannot create polygon with no points") + + self.points = points + + self.min_x = min(points, key=attrgetter("x")).x + self.max_x = max(points, key=attrgetter("x")).x + self.min_y = min(points, key=attrgetter("y")).y + self.max_y = max(points, key=attrgetter("y")).y + + is_valid = True + for (x, y) in [(self.min_x, self.min_y), (self.max_x, self.max_y)]: + is_valid = is_valid and self._validate_coordinates(x, y) + if not is_valid: + points_str = "; ".join(str(p) for p in self.points) + warnings.warn( + f"{type(self).__name__} coordinates are invalid : {points_str}", + UserWarning, + ) + + def __repr__(self): + """String representation of the polygon.""" + return ( + f"Polygon(len(points)={len(self.points)}," + f" min_x={self.min_x}, max_x={self.max_x}, min_y={self.min_y}, max_y={self.max_y})" + ) + + def __eq__(self, other): + """Compares if the polygon has the same points and modification date.""" + if isinstance(other, Polygon): + return self.points == other.points and self.modification_date == other.modification_date + return False + + def __hash__(self): + """Returns hash of the Polygon object.""" + return hash(str(self)) + + def normalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Polygon": + """Transforms from the `roi` coordinate system to the normalized coordinate system. + + This function is the inverse of ``denormalize_wrt_roi_shape``. + + Example: + Assume we have Polygon `p1` which lives in the top-right quarter of a 2D space. + The 2D space where `p1` lives in is an `roi` living in the top-left quarter of the normalized coordinate + space. This function returns Polygon `p1` expressed in the normalized coordinate space. + + >>> from otx.api.entities.annotation import Annotation + >>> from otx.api.entities.shapes.rectangle import Rectangle + >>> p1 = Polygon(points=[Point(x=0.5, y=0.0), Point(x=0.75, y=0.2), Point(x=0.6, y=0.1)]) + >>> roi = Rectangle(x1=0.0, x2=0.5, y1=0.0, y2=0.5) + >>> normalized = p1.normalize_wrt_roi_shape(roi_shape) + >>> normalized + Polygon(, len(points)=3) + + Args: + roi_shape: Region of Interest + + Returns: + New polygon in the image coordinate system + """ + if not isinstance(roi_shape, Rectangle): + raise ValueError("roi_shape has to be a Rectangle.") + + roi_shape = roi_shape.clip_to_visible_region() + + points = [p.normalize_wrt_roi(roi_shape) for p in self.points] + return Polygon(points=points) + + def denormalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Polygon": + """Transforming shape from the normalized coordinate system to the `roi` coordinate system. + + This function is the inverse of ``normalize_wrt_roi_shape`` + + Example: + Assume we have Polygon `p1` which lives in the top-right quarter of the normalized coordinate space. + The `roi` is a rectangle living in the half right of the normalized coordinate space. + This function returns Polygon `p1` expressed in the coordinate space of `roi`. (should return top-half) + + Polygon denormalized to a rectangle as ROI + + >>> from otx.api.entities.shapes.rectangle import Rectangle + >>> from otx.api.entities.annotation import Annotation + >>> p1 = Polygon(points=[Point(x=0.5, y=0.0), Point(x=0.75, y=0.2), Point(x=0.6, y=0.1)]) + >>> roi = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0) # the half-right + >>> normalized = p1.denormalize_wrt_roi_shape(roi_shape) + >>> normalized + Polygon(, len(points)=3) + + Args: + roi_shape: Region of Interest + + Returns: + New polygon in the ROI coordinate system + """ + if not isinstance(roi_shape, Rectangle): + raise ValueError("roi_shape has to be a Rectangle.") + + roi_shape = roi_shape.clip_to_visible_region() + + points = [p.denormalize_wrt_roi_shape(roi_shape) for p in self.points] + return Polygon(points=points) + + def _as_shapely_polygon(self) -> shapely_polygon: + """Returns the Polygon object as a shapely polygon which is used for calculating intersection between shapes.""" + return shapely_polygon([(point.x, point.y) for point in self.points]) + + def get_area(self) -> float: + """Returns the approximate area of the shape. + + Area is a value between 0 and 1, computed by converting the Polygon to a shapely polygon and reading the + `.area` property. + + NOTE: This method should not be relied on for exact area computation. The area is approximate, because shapes + are continuous, but pixels are discrete. + + Example: + + >>> Polygon(points=[Point(x=0.0, y=0.5), Point(x=0.5, y=0.5), Point(x=0.75, y=0.75)]).get_area() + 0.0625 + + Returns: + area of the shape + """ + return self._as_shapely_polygon().area diff --git a/src/otx/api/entities/shapes/rectangle.py b/src/otx/api/entities/shapes/rectangle.py new file mode 100644 index 00000000000..32456a0de5f --- /dev/null +++ b/src/otx/api/entities/shapes/rectangle.py @@ -0,0 +1,330 @@ +"""This module implements the Rectangle shape entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# Conflict with Isort +# pylint: disable=wrong-import-order, cyclic-import + +import datetime +import math +import warnings +from typing import Optional + +import numpy as np +from shapely.geometry import Polygon as shapely_polygon + +from otx.api.entities.shapes.shape import Shape, ShapeEntity, ShapeType +from otx.api.utils.time_utils import now + +# pylint: disable=invalid-name + + +class Rectangle(Shape): + """Rectangle represents a rectangular shape. + + Rectangle are used to annotate detection and classification tasks. In the + classification case, the rectangle is a full rectangle spanning the whole related + item (could be an image, video frame, a region of interest). + + - x1 and y1 represent the top-left coordinate of the rectangle + - x2 and y2 representing the bottom-right coordinate of the rectangle + + Args: + x1 (float): x-coordinate of the top-left corner of the rectangle + y1 (float): y-coordinate of the top-left corner of the rectangle + x2 (float): x-coordinate of the bottom-right corner of the rectangle + y2 (float): y-coordinate of the bottom-right corner of the rectangle + modification_date (datetime.datetime): Date of the last modification of the rectangle + """ + + # pylint: disable=too-many-arguments; Requires refactor + def __init__( + self, + x1: float, + y1: float, + x2: float, + y2: float, + modification_date: Optional[datetime.datetime] = None, + ): + modification_date = now() if modification_date is None else modification_date + super().__init__( + shape_type=ShapeType.RECTANGLE, + modification_date=modification_date, + ) + + is_valid = True + for (x, y) in [(x1, y1), (x2, y2)]: + is_valid = is_valid and self._validate_coordinates(x, y) + if not is_valid: + warnings.warn( + f"{type(self).__name__} coordinates are invalid : x1={x1}, y1={y1}, x2={x2}, y2={y2}", + UserWarning, + ) + + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + if self.width <= 0 or self.height <= 0: + raise ValueError( + f"Invalid rectangle with coordinates: x1={self.x1}, y1={self.y1}, " f"x2={self.x2}, y2={self.y2}" + ) + + def __repr__(self): + """String representation of the rectangle.""" + return f"Rectangle(x={self.x1}, y={self.y1}, width={self.width}, " f"height={self.height})" + + def __eq__(self, other: object): + """Returns True if `other` is a `Rectangle` with the same coordinates.""" + if isinstance(other, Rectangle): + return ( + self.x1 == other.x1 + and self.y1 == other.y1 + and self.x2 == other.x2 + and self.y2 == other.y2 + and self.modification_date == other.modification_date + ) + return False + + def __hash__(self): + """Returns hash of the rectangle.""" + return hash(str(self)) + + def clip_to_visible_region(self) -> "Rectangle": + """Clip the rectangle to the [0, 1] visible region of an image. + + Returns: + Rectangle: Clipped rectangle. + """ + x1 = min(max(0.0, self.x1), 1.0) + y1 = min(max(0.0, self.y1), 1.0) + x2 = min(max(0.0, self.x2), 1.0) + y2 = min(max(0.0, self.y2), 1.0) + + return Rectangle(x1=x1, y1=y1, x2=x2, y2=y2, modification_date=self.modification_date) + + def normalize_wrt_roi_shape(self, roi_shape: ShapeEntity) -> "Rectangle": + """Transforms from the `roi` coordinate system to the normalized coordinate system. + + Example: + Assume we have rectangle `b1` which lives in the top-right quarter of + a 2D space. The 2D space where `b1` lives in is an `roi` living in the top-left + quarter of the normalized coordinate space. This function returns rectangle + `b1` expressed in the normalized coordinate space. + + >>> from otx.api.entities.annotation import Annotation + >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5) + >>> roi = Rectangle(x1=0.0, x2=0.5, y1=0.0, y2=0.5) + >>> normalized = b1.normalize_wrt_roi_shape(roi_shape) + >>> normalized + Box(, x=0.25, y=0.0, width=0.25, height=0.25) + + Args: + roi_shape (ShapeEntity): Region of Interest. + + Raises: + ValueError: If the `roi_shape` is not a `Rectangle`. + + Returns: + New polygon in the image coordinate system + """ + if not isinstance(roi_shape, Rectangle): + raise ValueError("roi_shape has to be a Rectangle.") + + roi_shape = roi_shape.clip_to_visible_region() + + return Rectangle( + x1=self.x1 * roi_shape.width + roi_shape.x1, + y1=self.y1 * roi_shape.height + roi_shape.y1, + x2=self.x2 * roi_shape.width + roi_shape.x1, + y2=self.y2 * roi_shape.height + roi_shape.y1, + modification_date=self.modification_date, + ) + + def denormalize_wrt_roi_shape(self, roi_shape: ShapeEntity) -> "Rectangle": + """Transforming shape from the normalized coordinate system to the `roi` coordinate system. + + Example: + + Assume we have rectangle `b1` which lives in the top-right quarter of + the normalized coordinate space. The `roi` is a rectangle living in the half + right of the normalized coordinate space. This function returns rectangle + `b1` expressed in the coordinate space of `roi`. (should return top-half) + Box denormalized to a rectangle as ROI + + >>> from otx.api.entities.annotation import Annotation + >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5) + # the top-right + >>> roi = Annotation(Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0)) + # the half-right + >>> normalized = b1.denormalize_wrt_roi_shape(roi_shape) + # should return top half + >>> normalized + Box(, x=0.0, y=0.0, width=1.0, height=0.5) + + Args: + roi_shape (ShapeEntity): Region of Interest + + Raises: + ValueError: If the `roi_shape` is not a `Rectangle`. + + Returns: + Rectangle: New polygon in the ROI coordinate system + """ + if not isinstance(roi_shape, Rectangle): + raise ValueError("roi_shape has to be a Rectangle.") + + roi_shape = roi_shape.clip_to_visible_region() + + x1 = (self.x1 - roi_shape.x1) / roi_shape.width + y1 = (self.y1 - roi_shape.y1) / roi_shape.height + x2 = (self.x2 - roi_shape.x1) / roi_shape.width + y2 = (self.y2 - roi_shape.y1) / roi_shape.height + + return Rectangle( + x1=x1, + y1=y1, + x2=x2, + y2=y2, + modification_date=self.modification_date, + ) + + def _as_shapely_polygon(self) -> shapely_polygon: + points = [ + (self.x1, self.y1), + (self.x2, self.y1), + (self.x2, self.y2), + (self.x1, self.y2), + (self.x1, self.y1), + ] + return shapely_polygon(points) + + @classmethod + def generate_full_box(cls) -> "Rectangle": + """Returns a rectangle that fully encapsulates the normalized coordinate space. + + Example: + >>> Rectangle.generate_full_box() + Box(, x=0.0, y=0.0, width=1.0, height=1.0) + + Returns: + Rectangle: A rectangle that fully encapsulates the normalized coordinate space. + """ + return cls(x1=0.0, y1=0.0, x2=1.0, y2=1.0) + + @staticmethod + def is_full_box(rectangle: ShapeEntity) -> bool: + """Returns true if rectangle is a full box (occupying the full normalized coordinate space). + + Example: + + >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0) + >>> Rectangle.is_full_box(b1) + False + + >>> b2 = Rectangle(x1=0.0, x2=1.0, y1=0.0, y2=1.0) + >>> Rectangle.is_full_box(b2) + True + + Args: + rectangle (ShapeEntity): rectangle to evaluate + + Returns: + bool: true if it fully encapsulate normalized coordinate space. + """ + if ( + isinstance(rectangle, Rectangle) + and rectangle.x1 == 0 + and rectangle.y1 == 0 + and rectangle.height == 1 + and rectangle.width == 1 + ): + return True + return False + + def crop_numpy_array(self, data: np.ndarray) -> np.ndarray: + """Crop the given Numpy array to the region of interest represented by this rectangle. + + Args: + data (np.ndarray): Image to crop. + + Returns: + np.ndarray: Cropped image. + """ + + # We clip negative values to zero since Numpy uses negative values + # to represent indexing from the right side of the array. + # However, on the other hand, it is safe to have indices larger than the size + # of the dimension; therefore, we do not clip values larger than the width and + # height. + x1 = max(int(round(self.x1 * data.shape[1])), 0) + x2 = max(int(round(self.x2 * data.shape[1])), 0) + y1 = max(int(round(self.y1 * data.shape[0])), 0) + y2 = max(int(round(self.y2 * data.shape[0])), 0) + + return data[y1:y2, x1:x2, ::] + + @property + def width(self) -> float: + """Returns the width of the rectangle (x-axis). + + Example: + + >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5) + >>> b1.width + 0.5 + + Returns: + float: the width of the rectangle. (x-axis) + """ + return self.x2 - self.x1 + + @property + def height(self) -> float: + """Returns the height of the rectangle (y-axis). + + Example: + + >>> b1 = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5) + >>> b1.height + 0.5 + + Returns: + float: the height of the rectangle. (y-axis) + """ + return self.y2 - self.y1 + + @property + def diagonal(self) -> float: + """Returns the diagonal size/hypotenuse of the rectangle (x-axis). + + Example: + + >>> b1 = Rectangle(x1=0.0, x2=0.3, y1=0.0, y2=0.4) + >>> b1.diagonal + 0.5 + + Returns: + float: the width of the rectangle. (x-axis) + """ + return math.hypot(self.width, self.height) + + def get_area(self) -> float: + """Computes the approximate area of the shape. + + Area is a value between 0 and 1, calculated as (x2-x1) * (y2-y1) + + NOTE: This method should not be relied on for exact area computation. The area + is approximate, because shapes are continuous, but pixels are discrete. + + Example: + >>> Rectangle(0, 0, 1, 1).get_area() + 1.0 + >>> Rectangle(0.5, 0.5, 1.0, 1.0).get_area() + 0.25 + + Returns: + float: Approximate area of the shape. + """ + return (self.x2 - self.x1) * (self.y2 - self.y1) diff --git a/src/otx/api/entities/shapes/shape.py b/src/otx/api/entities/shapes/shape.py new file mode 100644 index 00000000000..2c4f552a9c3 --- /dev/null +++ b/src/otx/api/entities/shapes/shape.py @@ -0,0 +1,189 @@ +"""This file defines the ShapeEntity interface and the Shape abstract class.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +import datetime +import warnings +from enum import IntEnum, auto +from typing import TYPE_CHECKING + +from shapely.errors import PredicateError, TopologicalError +from shapely.geometry import Polygon as shapely_polygon + +if TYPE_CHECKING: + from otx.api.entities.shapes.rectangle import Rectangle + + +class GeometryException(ValueError): + """Exception that is thrown if the geometry of a Shape is invalid.""" + + +class ShapeType(IntEnum): + """Shows which type of Shape is being used.""" + + ELLIPSE = auto() + RECTANGLE = auto() + POLYGON = auto() + + +class ShapeEntity(metaclass=abc.ABCMeta): + """This interface represents the annotation shapes on the media given by user annotations or system analysis. + + The shapes is a 2D geometric shape living in a normalized coordinate system (the values range from 0 to 1). + """ + + # pylint: disable=redefined-builtin + def __init__(self, shape_type: ShapeType): + self._type = shape_type + + @property + def type(self) -> ShapeType: + """Get the type of Shape that this Shape represents.""" + return self._type + + @abc.abstractmethod + def get_area(self) -> float: + """Get the area of the shape.""" + raise NotImplementedError + + @abc.abstractmethod + def intersects(self, other: "Shape") -> bool: + """Returns true if other intersects with shape, otherwise returns false. + + Args: + other (Shape): Shape to compare with + + Returns: + bool: true if other intersects with shape, otherwise returns false + """ + raise NotImplementedError + + @abc.abstractmethod + def contains_center(self, other: "ShapeEntity") -> bool: + """Checks whether the center of the 'other' shape is located in the shape. + + Args: + other (ShapeEntity): Shape to compare with + + Returns: + bool: true if the center of the 'other' shape is located in the shape, otherwise returns false + """ + raise NotImplementedError + + @abc.abstractmethod + def normalize_wrt_roi_shape(self, roi_shape: "Rectangle") -> "Shape": + """The inverse of denormalize_wrt_roi_shape. + + Transforming shape from the `roi` coordinate system to the normalized coordinate system. + This is used when the tasks want to save the analysis results. + + For example in Detection -> Segmentation pipeline, the analysis results of segmentation + needs to be normalized to the roi (bounding boxes) coming from the detection. + + Args: + roi_shape (Rectangle): Shape of the roi. + + Returns: + Shape: Shape in the normalized coordinate system. + """ + raise NotImplementedError + + @abc.abstractmethod + def denormalize_wrt_roi_shape(self, roi_shape: "Rectangle") -> "ShapeEntity": + """The inverse of normalize_wrt_roi_shape. + + Transforming shape from the normalized coordinate system to the `roi` coordinate system. + This is used to pull ground truth during training process of the tasks. + Examples given in the Shape implementations. + + Args: + roi_shape (Rectangle): Shape of the roi. + + Returns: + ShapeEntity: Shape in the `roi` coordinate system. + """ + raise NotImplementedError + + @abc.abstractmethod + def _as_shapely_polygon(self) -> shapely_polygon: + """Convert shape to a shapely polygon. + + Shapely polygons are within the SDK used to calculate the intersection between Shapes. + It is also used in the SDK to find shapes that are visible within a given ROI. + + Returns: + shapely_polygon: Shapely polygon representation of the shape. + """ + raise NotImplementedError + + +class Shape(ShapeEntity): + """Base class for Shape entities.""" + + # pylint: disable=redefined-builtin, too-many-arguments; Requires refactor + def __init__(self, shape_type: ShapeType, modification_date: datetime.datetime): + super().__init__(shape_type=shape_type) + self.modification_date = modification_date + + def __repr__(self): + """Returns the date of the last modification of the shape.""" + return f"Shape with modification date:('{self.modification_date}')" + + def get_area(self) -> float: + """Get the area of the shape.""" + raise NotImplementedError + + # pylint: disable=protected-access + def intersects(self, other: "Shape") -> bool: + """Returns True, if other intersects with shape, otherwise returns False.""" + polygon_roi = self._as_shapely_polygon() + polygon_shape = other._as_shapely_polygon() + try: + return polygon_roi.intersects(polygon_shape) + except (PredicateError, TopologicalError) as exception: + raise GeometryException( + f"The intersection between the shapes {self} and {other} could not be computed: " f"{exception}." + ) from exception + + # pylint: disable=protected-access + def contains_center(self, other: "ShapeEntity") -> bool: + """Checks whether the center of the 'other' shape is located in the shape. + + Args: + other (ShapeEntity): Shape to compare with. + + Returns: + bool: Boolean that indicates whether the center of the other shape is located in the shape + """ + polygon_roi = self._as_shapely_polygon() + polygon_shape = other._as_shapely_polygon() + return polygon_roi.contains(polygon_shape.centroid) + + def _validate_coordinates(self, x: float, y: float) -> bool: + """Check if coordinate is valid. + + Checks whether the values for a given x,y coordinate pair lie within the range of (0,1) that is expected for + the normalized coordinate system. Issues a warning if the coordinates are out of bounds. + + Args: + x (float): x-coordinate to validate + y (float): y-coordinate to validate + + Returns: + bool: ``True`` if coordinates are within expected range, ``False`` otherwise + """ + if not ((0.0 <= x <= 1.0) and (0.0 <= y <= 1.0)): + warnings.warn( + f"{type(self).__name__} coordinates (x={x}, y={y}) are out of bounds, a normalized " + f"coordinate system is assumed. All coordinates are expected to be in range (0,1).", + UserWarning, + ) + return False + return True + + def __hash__(self): + """Returns the hash of shape.""" + return hash(str(self)) diff --git a/src/otx/api/entities/subset.py b/src/otx/api/entities/subset.py new file mode 100644 index 00000000000..ce4b8a7d2de --- /dev/null +++ b/src/otx/api/entities/subset.py @@ -0,0 +1,27 @@ +"""This file defines the Subset enum for use in datasets.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from enum import Enum + + +class Subset(Enum): + """Describes the Subset a DatasetItem is assigned to.""" + + NONE = 0 + TRAINING = 1 + VALIDATION = 2 + TESTING = 3 + UNLABELED = 4 + PSEUDOLABELED = 5 + UNASSIGNED = 6 + + def __str__(self): + """Returns name of subset.""" + return str(self.name) + + def __repr__(self): + """Returns name of subset.""" + return f"Subset.{self.name}" diff --git a/src/otx/api/entities/task_environment.py b/src/otx/api/entities/task_environment.py new file mode 100644 index 00000000000..4ead2a6487a --- /dev/null +++ b/src/otx/api/entities/task_environment.py @@ -0,0 +1,133 @@ +"""This module implements the TaskEnvironment entity.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import List, Optional, Type, TypeVar + +from otx.api.configuration import ConfigurableParameters, cfg_helper +from otx.api.entities.label import LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ModelConfiguration, ModelEntity +from otx.api.entities.model_template import ModelTemplate + +TypeVariable = TypeVar("TypeVariable", bound=ConfigurableParameters) + + +# pylint: disable=too-many-instance-attributes; Requires refactor +class TaskEnvironment: + """Defines the machine learning environment the task runs in. + + Args: + model_template (ModelTemplate): The model template used for this task + model (Optional[ModelEntity]): Model to use; if not specified, the task must be either weight-less + or use pre-trained or randomly initialised weights. + hyper_parameters (ConfigurableParameters): Set of hyper parameters + label_schema (LabelSchemaEntity): Label schema associated to this task + """ + + def __init__( + self, + model_template: ModelTemplate, + model: Optional[ModelEntity], + hyper_parameters: ConfigurableParameters, + label_schema: LabelSchemaEntity, + ): + + self.model_template = model_template + self.model = model + self.__hyper_parameters = hyper_parameters + self.label_schema = label_schema + + def __repr__(self): + """String representation of the TaskEnvironment object.""" + return ( + f"TaskEnvironment(model={self.model}, label_schema={self.label_schema}, " + f"hyper_params={self.__hyper_parameters})" + ) + + def __eq__(self, other: object) -> bool: + """Compares two TaskEnvironment objects. + + Args: + other (TaskEnvironment): Object to compare with. + + Returns: + bool: True if equal, False otherwise. + """ + if isinstance(other, TaskEnvironment): + return ( + self.model == other.model + and self.label_schema == other.label_schema + # TODO get_hyperparameters should return Union rather than TypeVariable + and self.get_hyper_parameters(instance_of=None) # type: ignore + == other.get_hyper_parameters(instance_of=None) + ) + return False + + def get_labels(self, include_empty: bool = False) -> List[LabelEntity]: + """Return the labels in this task environment (based on the label schema). + + Args: + include_empty (bool): Include the empty label if ``True``. Defaults to False. + + Returns: + List[LabelEntity]: List of labels + """ + return self.label_schema.get_labels(include_empty) + + def get_hyper_parameters(self, instance_of: Optional[Type[TypeVariable]] = None) -> TypeVariable: + """Returns Configuration for the task, de-serialized as type specified in `instance_of`. + + If the type of the configurable parameters is unknown, a generic + ConfigurableParameters object with all available parameters can be obtained + by calling method with instance_of = None. + + Example: + >>> self.get_hyper_parameters(instance_of=TorchSegmentationConfig) + TorchSegmentationConfig() + + Args: + instance_of (Optional[Type[TypeVariable]]): subtype of ModelConfig of the hyperparamters. Defaults to None. + + Returns: + TypeVariable: ConfigurableParameters entity + """ + if instance_of is None: + # If the instance_of is None, the type variable is not defined so the + # return type won't be deduced correctly + return self.__hyper_parameters # type: ignore + + # Otherwise, update the base config according to what is stored in the repo. + base_config = instance_of(header=self.__hyper_parameters.header) + + cfg_helper.substitute_values(base_config, value_input=self.__hyper_parameters, allow_missing_values=True) + + return base_config + + def set_hyper_parameters(self, hyper_parameters: ConfigurableParameters): + """Sets the hyper parameters for the task. + + Example: + >>> self.set_hyper_parameters(hyper_parameters=TorchSegmentationParameters()) + None + Args: + hyper_parameters (ConfigurationParameter): ConfigurableParameters entity to assign to task + """ + if not isinstance(hyper_parameters, ConfigurableParameters): + raise ValueError(f"Unable to set hyper parameters, invalid input: {hyper_parameters}") + self.__hyper_parameters = hyper_parameters + + def get_model_configuration(self) -> ModelConfiguration: + """Get the configuration needed to use the current model. + + That is the current set of: + * configurable parameters + * labels + * label schema + + Returns: + ModelConfiguration: Model configuration + """ + return ModelConfiguration(self.__hyper_parameters, self.label_schema) diff --git a/src/otx/api/entities/tensor.py b/src/otx/api/entities/tensor.py new file mode 100644 index 00000000000..89b32109300 --- /dev/null +++ b/src/otx/api/entities/tensor.py @@ -0,0 +1,53 @@ +"""This module implements the Tensor entity.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Tuple + +import numpy as np + +from otx.api.entities.metadata import IMetadata + + +class TensorEntity(IMetadata): + """Represents a metadata of tensor type in OTX. + + Args: + name: name of metadata + numpy: the numpy data of the tensor + """ + + def __init__(self, name: str, numpy: np.ndarray): + self.name = name + # Copying Numpy array as it points to the same memory address + self._numpy = np.copy(numpy) + + @property + def numpy(self) -> np.ndarray: + """Returns the numpy representation of the tensor.""" + return self._numpy + + @numpy.setter + def numpy(self, value): + self._numpy = value + + @property + def shape(self) -> Tuple[int, ...]: + """Returns the shape of the tensor.""" + return self._numpy.shape + + def __eq__(self, other): + """Returns True if the tensors are equal.""" + if isinstance(other, TensorEntity): + return np.array_equal(self.numpy, other.numpy) + return False + + def __str__(self): + """Returns the string representation of the tensor.""" + return f"{self.__class__.__name__}(name={self.name}, shape={self.shape})" + + def __repr__(self): + """Returns the representation of the tensor.""" + return f"{self.__class__.__name__}(name={self.name})" diff --git a/src/otx/api/entities/train_parameters.py b/src/otx/api/entities/train_parameters.py new file mode 100644 index 00000000000..6f88a23260d --- /dev/null +++ b/src/otx/api/entities/train_parameters.py @@ -0,0 +1,61 @@ +"""This module implements the TrainingParameters entity.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from dataclasses import dataclass +from typing import Any, Callable, Optional, Protocol + + +class UpdateProgressCallback(Protocol): + """UpdateProgressCallback protocol. + + Used as a replacement of Callable[] type since Callable doesn’t handle default parameters like + `score: Optional[float] = None` + """ + + def __call__(self, progress: float, score: Optional[float] = None): + """Callback to provide updates about the progress of a task. + + It is recommended to call this function at least once per epoch. + However, the exact frequency is left to the task implementer. + + An optional `score` can also be passed. If specified, this score can be used by HPO + to monitor the improvement of the task. + + Args: + progress: Progress as a percentage + score: Optional validation score + """ + + +# pylint: disable=unused-argument +def default_progress_callback(progress: float, score: Optional[float] = None): + """Default progress callback. It is a placeholder (does nothing) and is used in empty TrainParameters.""" + + +def default_save_model_callback(): + """Default save model callback. It is a placeholder (does nothing) and is used in empty TrainParameters.""" + + +@dataclass +class TrainParameters: + """Train parameters. + + Attributes: + resume: Set to ``True`` if training must be resume with the + optimizer state; set to ``False`` to discard the optimizer + state and start with fresh optimizer + update_progress: Callback which can be used to provide updates + about the progress of a task. + save_model: Callback to notify that the model weights have been + changed. This callback can be used by the task when + temporary weights should be saved (for instance, at the end + of an epoch). If this callback has been used to save + temporary weights, those weights will be used to resume + training if for some reason training was suspended. + """ + + resume: bool = False + update_progress: Callable[[int, Optional[float]], Any] = default_progress_callback + save_model: Callable[[], None] = default_save_model_callback diff --git a/src/otx/api/py.typed b/src/otx/api/py.typed new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/src/otx/api/py.typed @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/api/serialization/__init__.py b/src/otx/api/serialization/__init__.py new file mode 100644 index 00000000000..15035e41b77 --- /dev/null +++ b/src/otx/api/serialization/__init__.py @@ -0,0 +1,6 @@ +"""Utilities to save and load data to/from files.""" + +# +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/api/serialization/datetime_mapper.py b/src/otx/api/serialization/datetime_mapper.py new file mode 100644 index 00000000000..7fccde8abf0 --- /dev/null +++ b/src/otx/api/serialization/datetime_mapper.py @@ -0,0 +1,32 @@ +"""This module contains the mapper for datetime.""" + +# +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import datetime +from typing import Union + +from otx.api.utils.time_utils import now + + +class DatetimeMapper: + """This class maps a `datetime.datetime` entity to a string, and vice versa.""" + + @staticmethod + def forward(instance: datetime.datetime) -> str: + """Serializes datetime to str.""" + + return instance.strftime("%Y-%m-%dT%H:%M:%S.%f") + + @staticmethod + def backward(instance: Union[None, str]) -> datetime.datetime: + """Deserializes datetime from str or create new one if it is None.""" + + if isinstance(instance, str): + modification_date = datetime.datetime.strptime(instance, "%Y-%m-%dT%H:%M:%S.%f") + return modification_date.replace(tzinfo=datetime.timezone.utc) + + return now() diff --git a/src/otx/api/serialization/id_mapper.py b/src/otx/api/serialization/id_mapper.py new file mode 100644 index 00000000000..76e091ae4d1 --- /dev/null +++ b/src/otx/api/serialization/id_mapper.py @@ -0,0 +1,24 @@ +"""This module contains the mapper for ID entities.""" +# +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from otx.api.entities.id import ID + + +class IDMapper: + """This class maps an `ID` entity to a string, and vice versa.""" + + @staticmethod + def forward(instance: ID) -> str: + """Serializes ID to str.""" + + return str(instance) + + @staticmethod + def backward(instance: str) -> ID: + """Deserializes ID from str.""" + + return ID(str(instance)) diff --git a/src/otx/api/serialization/label_mapper.py b/src/otx/api/serialization/label_mapper.py new file mode 100644 index 00000000000..b78fe68eb33 --- /dev/null +++ b/src/otx/api/serialization/label_mapper.py @@ -0,0 +1,195 @@ +""".This module contains the mapper for label related entities.""" +# +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import json +from typing import Dict, cast + +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import ( + LabelGroup, + LabelGroupType, + LabelSchemaEntity, + LabelTree, +) + +from .datetime_mapper import DatetimeMapper +from .id_mapper import IDMapper + + +class ColorMapper: + """This class maps a `Color` entity to a serialized dictionary, and vice versa.""" + + @staticmethod + def forward(instance: Color) -> dict: + """Serializes to dict.""" + + return { + "red": instance.red, + "green": instance.green, + "blue": instance.blue, + "alpha": instance.alpha, + } + + @staticmethod + def backward(instance: dict) -> Color: + """Deserializes from dict.""" + + return Color(instance["red"], instance["green"], instance["blue"], instance["alpha"]) + + +class LabelMapper: + """This class maps a `Label` entity to a serialized dictionary, and vice versa.""" + + @staticmethod + def forward( + instance: LabelEntity, + ) -> dict: + """Serializes to dict.""" + + return { + "_id": IDMapper().forward(instance.id_), + "name": instance.name, + "color": ColorMapper().forward(instance.color), + "hotkey": instance.hotkey, + "domain": str(instance.domain), + "creation_date": DatetimeMapper.forward(instance.creation_date), + "is_empty": instance.is_empty, + "is_anomalous": instance.is_anomalous, + } + + @staticmethod + def backward(instance: dict) -> LabelEntity: + """Deserializes from dict.""" + + label_id = IDMapper().backward(instance["_id"]) + + domain = str(instance.get("domain")) + label_domain = Domain[domain] + + label = LabelEntity( + id=label_id, + name=instance["name"], + color=ColorMapper().backward(instance["color"]), + hotkey=instance.get("hotkey", ""), + domain=label_domain, + creation_date=DatetimeMapper.backward(instance["creation_date"]), + is_empty=instance.get("is_empty", False), + is_anomalous=instance.get("is_anomalous", False), + ) + return label + + +class LabelGroupMapper: + """This class maps a `LabelGroup` entity to a serialized dictionary, and vice versa.""" + + @staticmethod + def forward(instance: LabelGroup) -> dict: + """Serializes to dict.""" + + return { + "_id": IDMapper().forward(instance.id_), + "name": instance.name, + "label_ids": [IDMapper().forward(label.id_) for label in instance.labels], + "relation_type": instance.group_type.name, + } + + @staticmethod + def backward(instance: dict, all_labels: Dict[ID, LabelEntity]) -> LabelGroup: + """Deserializes from dict.""" + + return LabelGroup( + id=IDMapper().backward(instance["_id"]), + name=instance["name"], + group_type=LabelGroupType[instance["relation_type"]], + labels=[all_labels[IDMapper().backward(label_id)] for label_id in instance["label_ids"]], + ) + + +class LabelTreeMapper: + """This class maps a `LabelTree` entity to a serialized dictionary, and vice versa.""" + + @staticmethod + def forward(instance: LabelTree) -> dict: + """Serializes to dict.""" + + return { + "type": instance.type, + "directed": instance.directed, + "nodes": [IDMapper().forward(label.id_) for label in instance.nodes], + "edges": [(IDMapper().forward(edge[0].id_), IDMapper().forward(edge[1].id_)) for edge in instance.edges], + } + + @staticmethod + def backward(instance: dict, all_labels: Dict[ID, LabelEntity]) -> LabelTree: + """Deserializes from dict.""" + + output: LabelTree + + instance_type = instance["type"] + if instance_type == "tree": + output = LabelTree() + else: + raise ValueError(f"Unsupported type `{instance_type}` for label graph") + + label_map = {label_id: all_labels.get(IDMapper().backward(label_id)) for label_id in instance["nodes"]} + for label in label_map.values(): + if label: + output.add_node(label) + for edge in instance["edges"]: + node1 = label_map.get(edge[0]) + node2 = label_map.get(edge[1]) + if node1 and node2: + output.add_edge(node1, node2) + + return output + + +class LabelSchemaMapper: + """This class maps a `LabelSchema` entity to a serialized dictionary, and vice versa.""" + + @staticmethod + def forward( + instance: LabelSchemaEntity, + ) -> dict: + """Serializes to dict.""" + + label_groups = [LabelGroupMapper().forward(group) for group in instance.get_groups(include_empty=True)] + + return { + "label_tree": LabelTreeMapper().forward(instance.label_tree), + "label_groups": label_groups, + "all_labels": { + IDMapper().forward(label.id_): LabelMapper().forward(label) for label in instance.get_labels(True) + }, + } + + @staticmethod + def backward(instance: dict) -> LabelSchemaEntity: + """Deserializes from dict.""" + + all_labels = { + IDMapper().backward(id): LabelMapper().backward(label) for id, label in instance["all_labels"].items() + } + + label_tree = LabelTreeMapper().backward(instance["label_tree"], all_labels) + label_groups = [ + LabelGroupMapper().backward(label_group, all_labels) for label_group in instance["label_groups"] + ] + output = LabelSchemaEntity( + label_tree=cast(LabelTree, label_tree), + label_groups=label_groups, + ) + return output + + +def label_schema_to_bytes(label_schema: LabelSchemaEntity) -> bytes: + """Returns json-serialized LabelSchemaEntity as bytes.""" + + serialized_label_schema = LabelSchemaMapper.forward(label_schema) + return json.dumps(serialized_label_schema, indent=4).encode() diff --git a/src/otx/api/usecases/__init__.py b/src/otx/api/usecases/__init__.py new file mode 100644 index 00000000000..3bea124f765 --- /dev/null +++ b/src/otx/api/usecases/__init__.py @@ -0,0 +1,4 @@ +"""Utilities and use cases built on top of OTX API.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/api/usecases/adapters/__init__.py b/src/otx/api/usecases/adapters/__init__.py new file mode 100644 index 00000000000..b3fd5e29cdc --- /dev/null +++ b/src/otx/api/usecases/adapters/__init__.py @@ -0,0 +1,7 @@ +"""Adapters. + +These connect datasource to OTX entities. +""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/api/usecases/adapters/model_adapter.py b/src/otx/api/usecases/adapters/model_adapter.py new file mode 100644 index 00000000000..916ef9b560f --- /dev/null +++ b/src/otx/api/usecases/adapters/model_adapter.py @@ -0,0 +1,56 @@ +"""This module define a module to adapt model weights from a data source.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +from typing import Union + + +class IDataSource: + """Class that holds a combination of both a repo and a filename which can be used to fetch data.""" + + @property + @abc.abstractmethod + def data(self): + """Returns the data of the source.""" + raise NotImplementedError + + +class ModelAdapter(metaclass=abc.ABCMeta): + """The ModelAdapter is an adapter is intended to lazily fetch its binary data from a given data source.""" + + def __init__(self, data_source: Union[IDataSource, bytes]): + self.__data_source = data_source + + @property + def data_source(self): + """Returns the data source of the adapter.""" + return self.__data_source + + @data_source.setter + def data_source(self, value: Union[IDataSource, bytes]): + self.__data_source = value + + @property + def data(self): + """Returns the data of the Model.""" + if isinstance(self.__data_source, IDataSource): + return self.__data_source.data + if isinstance(self.__data_source, bytes): + return self.__data_source + raise ValueError("This model adapter is not properly initialized with a source of data") + + @property + def from_file_storage(self) -> bool: + """Returns if the ModelAdapters data comes from the file storage or not. + + This is used in the model repo to know if the data of the model should be saved or not. + """ + if isinstance(self.data_source, bytes): + return False + return True + + +class ExportableCodeAdapter(ModelAdapter): + """Adapter intended to lazily fetch raw exportable code data from a given data source.""" diff --git a/src/otx/api/usecases/evaluation/__init__.py b/src/otx/api/usecases/evaluation/__init__.py new file mode 100644 index 00000000000..5705195bec4 --- /dev/null +++ b/src/otx/api/usecases/evaluation/__init__.py @@ -0,0 +1,31 @@ +"""Evaluation metrics.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .accuracy import Accuracy +from .averaging import MetricAverageMethod +from .basic_operations import ( + get_intersections_and_cardinalities, + intersection_box, + intersection_over_union, + precision_per_class, + recall_per_class, +) +from .dice import DiceAverage +from .f_measure import FMeasure +from .metrics_helper import MetricsHelper + +__all__ = [ + "Accuracy", + "MetricAverageMethod", + "DiceAverage", + "FMeasure", + "intersection_box", + "intersection_over_union", + "MetricsHelper", + "precision_per_class", + "recall_per_class", + "get_intersections_and_cardinalities", +] diff --git a/src/otx/api/usecases/evaluation/accuracy.py b/src/otx/api/usecases/evaluation/accuracy.py new file mode 100644 index 00000000000..2344a7d3cfa --- /dev/null +++ b/src/otx/api/usecases/evaluation/accuracy.py @@ -0,0 +1,335 @@ +"""This module contains the implementation of Accuracy performance provider.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import copy +from typing import List, Set, Tuple + +import numpy as np +from sklearn.metrics import confusion_matrix as sklearn_confusion_matrix + +from otx.utils.logger import get_logger +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import LabelEntity +from otx.api.entities.label_schema import LabelGroup +from otx.api.entities.metrics import ( + BarChartInfo, + BarMetricsGroup, + ColorPalette, + MatrixChartInfo, + MatrixMetric, + MatrixMetricsGroup, + MetricsGroup, + Performance, + ScoreMetric, +) +from otx.api.entities.resultset import ResultSetEntity +from otx.api.usecases.evaluation.averaging import MetricAverageMethod +from otx.api.usecases.evaluation.basic_operations import ( + precision_per_class, + recall_per_class, +) +from otx.api.usecases.evaluation.performance_provider_interface import ( + IPerformanceProvider, +) + +logger = get_logger() + + +class Accuracy(IPerformanceProvider): + """This class is responsible for providing Accuracy measures; mainly for Classification problems. + + The calculation both supports multi label and binary label predictions. + + Accuracy is the proportion of the predicted correct labels, to the total number (predicted and actual) + labels for that instance. Overall accuracy is the average across all instances. + + Args: + resultset (ResultSetEntity): ResultSet that score will be computed for + average (MetricAverageMethod, optional): The averaging method, either MICRO or MACRO + MICRO: compute average over all predictions in all label groups + MACRO: compute accuracy per label group, return the average of the per-label-group accuracy scores + """ + + def __init__( + self, + resultset: ResultSetEntity, + average: MetricAverageMethod = MetricAverageMethod.MICRO, + ): + self._unnormalized_matrices: List[MatrixMetric] = compute_unnormalized_confusion_matrices_from_resultset( + resultset + ) + + # accuracy computation + mean_accuracy = self._compute_accuracy(average=average, confusion_matrices=self._unnormalized_matrices) + self._accuracy = ScoreMetric(value=mean_accuracy, name="Accuracy") + + @property + def accuracy(self) -> ScoreMetric: + """Returns the accuracy as ScoreMetric.""" + return self._accuracy + + def get_performance(self) -> Performance: + """Returns the performance with accuracy and confusion metrics.""" + confusion_matrix_dashboard_metrics: List[MetricsGroup] = [] + + # Use normalized matrix for UI + normalized_matrices: List[MatrixMetric] = copy.deepcopy(self._unnormalized_matrices) + for unnormalized_matrix in normalized_matrices: + unnormalized_matrix.normalize() + + confusion_matrix_info = MatrixChartInfo( + name="Confusion matrix", + header="confusion", + row_header="Predicted label", + column_header="True label", + ) + confusion_matrix_dashboard_metrics.append( + MatrixMetricsGroup(metrics=normalized_matrices, visualization_info=confusion_matrix_info) + ) + # Compute precision and recall MetricGroups and append them to the dashboard metrics + for _confusion_matrix in self._unnormalized_matrices: + confusion_matrix_dashboard_metrics.append(precision_metrics_group(_confusion_matrix)) + confusion_matrix_dashboard_metrics.append(recall_metrics_group(_confusion_matrix)) + + return Performance(score=self.accuracy, dashboard_metrics=confusion_matrix_dashboard_metrics) + + @staticmethod + def _compute_accuracy(average: MetricAverageMethod, confusion_matrices: List[MatrixMetric]) -> float: + """Compute accuracy using the confusion matrices. + + Args: + average (MatricAverageMethod): The averaging method, either MICRO or MACRO + MICRO: compute average over all predictions in all label groups + MACRO: compute accuracy per label group, return the average of the per-label-group accuracy scores + confusion_matrices (List[MatrixMetric]): the confusion matrices to compute accuracy from. + MUST be unnormalized. + + Raises + ValueError: when the ground truth dataset does not contain annotations + RuntimeError: when the averaging methods is not known + Returns: + float: the accuracy score for the provided confusion matrix + """ + # count correct predictions and total annotations + correct_per_label_group = [np.trace(mat.matrix_values) for mat in confusion_matrices] + total_per_label_group = [np.sum(mat.matrix_values) for mat in confusion_matrices] + # check if all label groups have annotations + if not np.any(total_per_label_group): + raise ValueError("The ground truth dataset must contain annotations.") + # return micro or macro average + if average == MetricAverageMethod.MACRO: + # compute accuracy for each label group, then average across groups, ignoring groups without annotations + return np.nanmean(np.divide(correct_per_label_group, total_per_label_group)) + if average == MetricAverageMethod.MICRO: + # average over all predictions in all label groups + return np.sum(correct_per_label_group) / np.sum(total_per_label_group) + + raise RuntimeError(f"Unknown averaging method: {average}") + + +def precision_metrics_group(confusion_matrix: MatrixMetric) -> MetricsGroup: + """Computes the precision per class based on a confusion matrix and returns them as ScoreMetrics in a MetricsGroup. + + Args: + confusion_matrix: matrix to compute the precision per class for + + Returns: + a BarMetricsGroup with the per class precision. + """ + labels = confusion_matrix.row_labels + if labels is None: + # If no labels are given, just number the classes by index + if confusion_matrix.matrix_values is not None: + label_range = confusion_matrix.matrix_values.shape[0] + else: + label_range = 0 + labels = np.arange(label_range) + + per_class_precision = [ + ScoreMetric(class_, value=precision) + for (class_, precision) in zip(labels, precision_per_class(confusion_matrix.matrix_values)) + ] + + return BarMetricsGroup( + metrics=per_class_precision, + visualization_info=BarChartInfo( + name="Precision per class", + palette=ColorPalette.LABEL, + ), + ) + + +def recall_metrics_group(confusion_matrix: MatrixMetric) -> MetricsGroup: + """Computes the recall per class based on a confusion matrix and returns them as ScoreMetrics in a MetricsGroup. + + Args: + confusion_matrix: matrix to compute the recall per class for + + Returns: + a BarMetricsGroup with the per class recall + """ + labels = confusion_matrix.row_labels + if labels is None: + # If no labels are given, just number the classes by index + if confusion_matrix.matrix_values is not None: + label_range = confusion_matrix.matrix_values.shape[0] + else: + label_range = 0 + labels = np.arange(label_range) + + per_class_recall = [ + ScoreMetric(class_, value=recall) + for (class_, recall) in zip(labels, recall_per_class(confusion_matrix.matrix_values)) + ] + + return BarMetricsGroup( + metrics=per_class_recall, + visualization_info=BarChartInfo( + name="Recall per class", + palette=ColorPalette.LABEL, + ), + ) + + +def __get_gt_and_predicted_label_indices_from_resultset( + resultset: ResultSetEntity, +) -> Tuple[List[Set[int]], List[Set[int]]]: + """Returns the label indices lists for ground truth and prediction datasets in a tuple. + + Args: + resultset + + Returns: + a tuple containing two lists. The first list contains the ground truth label indices, and the second contains + the prediction label indices. + """ + true_label_idx = [] + predicted_label_idx = [] + + gt_dataset: DatasetEntity = resultset.ground_truth_dataset + pred_dataset: DatasetEntity = resultset.prediction_dataset + + gt_dataset.sort_items() + pred_dataset.sort_items() + + # Iterate over each dataset item, and collect the labels for this item (pred and gt) + task_labels = resultset.model.configuration.get_label_schema().get_labels(include_empty=True) + for gt_item, pred_item in zip(gt_dataset, pred_dataset): + if isinstance(gt_item, DatasetItemEntity) and isinstance(pred_item, DatasetItemEntity): + true_label_idx.append({task_labels.index(label) for label in gt_item.get_roi_labels(task_labels)}) + predicted_label_idx.append({task_labels.index(label) for label in pred_item.get_roi_labels(task_labels)}) + + return true_label_idx, predicted_label_idx + + +def __compute_unnormalized_confusion_matrices_for_label_group( + true_label_idx: List[Set[int]], + predicted_label_idx: List[Set[int]], + label_group: LabelGroup, + task_labels: List[LabelEntity], +) -> MatrixMetric: + """Returns matrix metric for a certain label group. + + Args: + true_label_idx (List[Set[int]]): list of sets of label indices for the ground truth dataset + predicted_label_idx (List[Set[int]]): list of sets of label indices for the prediction dataset + label_group (LabelGroup): label group to compute the confusion matrix for + task_labels (List[LabelEntity]): list of labels for the task + + Returns: + MatrixMetric: confusion matrix for the label group + """ + map_task_labels_idx_to_group_idx = { + task_labels.index(label): i_group for i_group, label in enumerate(label_group.labels) + } + set_group_labels_idx = set(map_task_labels_idx_to_group_idx.keys()) + group_label_names = [task_labels[label_idx].name for label_idx in set_group_labels_idx] + + if len(group_label_names) == 1: + # Single-class + # we use "not" to make presence of a class to be at index 0, while the absence of it at index 1 + y_true = [int(not set_group_labels_idx.issubset(true_labels)) for true_labels in true_label_idx] + y_pred = [int(not set_group_labels_idx.issubset(pred_labels)) for pred_labels in predicted_label_idx] + group_label_names += [f"~ {group_label_names[0]}"] + column_labels = group_label_names.copy() + remove_last_row = False + else: + # Multiclass + undefined_idx = len(group_label_names) # to define missing value + + # find the intersections between GT and task labels, and Prediction and task labels + true_intersections = [true_labels.intersection(set_group_labels_idx) for true_labels in true_label_idx] + pred_intersections = [pred_labels.intersection(set_group_labels_idx) for pred_labels in predicted_label_idx] + + # map the intersection to 0-index value + y_true = [ + map_task_labels_idx_to_group_idx[list(true_intersection)[0]] + if len(true_intersection) != 0 + else undefined_idx + for true_intersection in true_intersections + ] + y_pred = [ + map_task_labels_idx_to_group_idx[list(pred_intersection)[0]] + if len(pred_intersection) != 0 + else undefined_idx + for pred_intersection in pred_intersections + ] + + column_labels = group_label_names.copy() + column_labels.append("Other") + remove_last_row = True + + matrix_data = sklearn_confusion_matrix(y_true, y_pred, labels=list(range(len(column_labels)))) + if remove_last_row: + # matrix clean up + matrix_data = np.delete(matrix_data, -1, 0) + if sum(matrix_data[:, -1]) == 0: + # if none of the GT is classified as classes from other groups, clean it up too + matrix_data = np.delete(matrix_data, -1, 1) + column_labels.remove(column_labels[-1]) + + # Use unnormalized matrix for statistics computation (accuracy, precision, recall) + return MatrixMetric( + name=f"{label_group.name}", + matrix_values=matrix_data, + row_labels=group_label_names, + column_labels=column_labels, + normalize=False, + ) + + +def compute_unnormalized_confusion_matrices_from_resultset( + resultset: ResultSetEntity, +) -> List[MatrixMetric]: + """Computes an (unnormalized) confusion matrix for every label group in the resultset. + + Args: + resultset: the input resultset + + Returns: + the computed unnormalized confusion matrices + """ + + if len(resultset.ground_truth_dataset) == 0 or len(resultset.prediction_dataset) == 0: + raise ValueError("Cannot compute the confusion matrix of an empty result set.") + + unnormalized_confusion_matrices: List[MatrixMetric] = [] + ( + true_label_idx, + predicted_label_idx, + ) = __get_gt_and_predicted_label_indices_from_resultset(resultset) + task_labels = resultset.model.configuration.get_label_schema().get_labels(include_empty=False) + + # Confusion matrix computation + for label_group in resultset.model.configuration.get_label_schema().get_groups(): + matrix = __compute_unnormalized_confusion_matrices_for_label_group( + true_label_idx, predicted_label_idx, label_group, task_labels + ) + unnormalized_confusion_matrices.append(matrix) + + return unnormalized_confusion_matrices diff --git a/src/otx/api/usecases/evaluation/anomaly_metrics.py b/src/otx/api/usecases/evaluation/anomaly_metrics.py new file mode 100644 index 00000000000..a1981e42143 --- /dev/null +++ b/src/otx/api/usecases/evaluation/anomaly_metrics.py @@ -0,0 +1,123 @@ +"""This module contains the implementations of performance providers for multi-score anomaly metrics.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from abc import ABC +from typing import List, Optional + +from otx.api.entities.metrics import ( + MetricsGroup, + MultiScorePerformance, + Performance, + ScoreMetric, +) +from otx.api.entities.resultset import ResultSetEntity +from otx.api.usecases.evaluation.averaging import MetricAverageMethod +from otx.api.usecases.evaluation.dice import DiceAverage +from otx.api.usecases.evaluation.f_measure import FMeasure +from otx.api.usecases.evaluation.performance_provider_interface import ( + IPerformanceProvider, +) +from otx.api.utils.dataset_utils import ( + contains_anomalous_images, + split_local_global_resultset, +) + + +class AnomalyLocalizationPerformance(MultiScorePerformance): + """Anomaly specific MultiScorePerformance. + + This class implements a special case of the MultiScorePerformance, specific for anomaly tasks that perform + anomaly localization (detection/segmentation), in addition to anomaly classification. + + Args: + global_score: Image-level performance metric. + local_score: Pixel- or bbox-level performance metric, depending + on the task type. + dashboard_metrics: (optional) additional statistics, containing + charts, curves, and other additional info. + """ + + def __init__( + self, + global_score: ScoreMetric, + local_score: Optional[ScoreMetric], + dashboard_metrics: Optional[List[MetricsGroup]], + ): + super().__init__( + primary_score=local_score, + additional_scores=[global_score], + dashboard_metrics=dashboard_metrics, + ) + self._global_score = global_score + self._local_score = local_score + + @property + def global_score(self): + """Return the global (image-level) score metric.""" + return self._global_score + + @property + def local_score(self): + """Return the local (pixel-/bbox-level) score metric.""" + return self._local_score + + +class AnomalyLocalizationScores(IPerformanceProvider, ABC): + """AnomalyLocalizationPerformance object for anomaly segmentation and anomaly detection tasks. + + Depending on the subclass, the `get_performance` method returns an AnomalyLocalizationPerformance object with the + pixel- or bbox-level metric as the primary score. The global (image-level) performance metric is included as an + additional metric. + + Args: + resultset: ResultSet that scores will be computed for + """ + + def __init__(self, resultset: ResultSetEntity): + self.local_score: Optional[ScoreMetric] = None + self.dashboard_metrics: List[MetricsGroup] = [] + + global_resultset, local_resultset = split_local_global_resultset(resultset) + + global_metric = FMeasure(resultset=global_resultset) + global_performance = global_metric.get_performance() + self.global_score = global_performance.score + self.dashboard_metrics += global_performance.dashboard_metrics + + if contains_anomalous_images(local_resultset.ground_truth_dataset): + local_metric = self._get_local_metric(local_resultset) + local_performance = local_metric.get_performance() + self.local_score = local_performance.score + self.dashboard_metrics += local_performance.dashboard_metrics + + @staticmethod + def _get_local_metric(local_resultset: ResultSetEntity) -> IPerformanceProvider: + """Return the local performance metric for the resultset.""" + raise NotImplementedError + + def get_performance(self) -> Performance: + """Return the performance object for the resultset.""" + return AnomalyLocalizationPerformance( + global_score=self.global_score, + local_score=self.local_score, + dashboard_metrics=self.dashboard_metrics, + ) + + +class AnomalySegmentationScores(AnomalyLocalizationScores): + """Performance provider for anomaly segmentation tasks.""" + + @staticmethod + def _get_local_metric(local_resultset: ResultSetEntity) -> IPerformanceProvider: + return DiceAverage(resultset=local_resultset, average=MetricAverageMethod.MICRO) + + +class AnomalyDetectionScores(AnomalyLocalizationScores): + """Performance provider for anomaly detection tasks.""" + + @staticmethod + def _get_local_metric(local_resultset: ResultSetEntity) -> IPerformanceProvider: + return FMeasure(resultset=local_resultset) diff --git a/src/otx/api/usecases/evaluation/averaging.py b/src/otx/api/usecases/evaluation/averaging.py new file mode 100644 index 00000000000..3b8c70fe1fc --- /dev/null +++ b/src/otx/api/usecases/evaluation/averaging.py @@ -0,0 +1,15 @@ +"""Averaging module contains averaging method enumeration.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from enum import Enum, auto + + +class MetricAverageMethod(Enum): + """This defines the metrics averaging method.""" + + MICRO = auto() + MACRO = auto() diff --git a/src/otx/api/usecases/evaluation/basic_operations.py b/src/otx/api/usecases/evaluation/basic_operations.py new file mode 100644 index 00000000000..9fc87dcfb00 --- /dev/null +++ b/src/otx/api/usecases/evaluation/basic_operations.py @@ -0,0 +1,153 @@ +"""This module contains functions for basic operations.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from typing import Dict, List, Optional, Tuple + +import numpy as np + +from otx.api.entities.label import LabelEntity +from otx.api.entities.shapes.rectangle import Rectangle + +#: Dictionary storing a number for each label. The ``None`` key represents "all labels" +NumberPerLabel = Dict[Optional[LabelEntity], int] + + +def get_intersections_and_cardinalities( + references: List[np.ndarray], + predictions: List[np.ndarray], + labels: List[LabelEntity], +) -> Tuple[NumberPerLabel, NumberPerLabel]: + """Returns all intersections and cardinalities between reference masks and prediction masks. + + Intersections and cardinalities are each returned in a dictionary mapping each label to its corresponding + number of intersection/cardinality pixels + + Args: + references (List[np.ndarray]): reference masks,s one mask per image + predictions (List[np.ndarray]): prediction masks, one mask per image + labels (List[LabelEntity]): labels in input masks + + Returns: + Tuple[NumberPerLabel, NumberPerLabel]: (all_intersections, all_cardinalities) + """ + + # TODO [Soobee] : Add score for background label and align the calculation method with validation + all_intersections: NumberPerLabel = {label: 0 for label in labels} + all_intersections[None] = 0 + all_cardinalities: NumberPerLabel = {label: 0 for label in labels} + all_cardinalities[None] = 0 + for reference, prediction in zip(references, predictions): + intersection = np.where(reference == prediction, reference, 0) + all_intersections[None] += np.count_nonzero(intersection) + all_cardinalities[None] += np.count_nonzero(reference) + np.count_nonzero(prediction) + for i, label in enumerate(labels): + label_num = i + 1 + all_intersections[label] += np.count_nonzero(intersection == label_num) + reference_area = np.count_nonzero(reference == label_num) + prediction_area = np.count_nonzero(prediction == label_num) + all_cardinalities[label] += reference_area + prediction_area + return all_intersections, all_cardinalities + + +def intersection_box(box1: Rectangle, box2: Rectangle) -> Optional[List[float]]: + """Calculate the intersection box of two bounding boxes. + + Args: + box1: a Rectangle that represents the first bounding box + box2: a Rectangle that represents the second bounding box + + Returns: + a Rectangle that represents the intersection box if inputs have + a valid intersection, else None + """ + x_left = max(box1.x1, box2.x1) + y_top = max(box1.y1, box2.y1) + x_right = min(box1.x2, box2.x2) + y_bottom = min(box1.y2, box2.y2) + if x_right <= x_left or y_bottom <= y_top: + return None + return [x_left, y_top, x_right, y_bottom] + + +def intersection_over_union(box1: Rectangle, box2: Rectangle, intersection: Optional[List[float]] = None) -> float: + """Calculate the Intersection over Union (IoU) of two bounding boxes. + + Args: + box1: a Rectangle representing a bounding box + box2: a Rectangle representing a second bounding box + intersection: precomputed intersection between two boxes (see + intersection_box function), if exists. + + Returns: + intersection-over-union of box1 and box2 + """ + iou = 0.0 + if intersection is None: + intersection = intersection_box(box1, box2) + if intersection is not None: + intersection_area = (intersection[2] - intersection[0]) * (intersection[3] - intersection[1]) + box1_area = (box1.x2 - box1.x1) * (box1.y2 - box1.y1) + box2_area = (box2.x2 - box2.x1) * (box2.y2 - box2.y1) + union_area = float(box1_area + box2_area - intersection_area) + if union_area != 0: + iou = intersection_area / union_area + if iou < 0.0 or iou > 1.0: + raise ValueError(f"intersection over union should be in range [0,1], instead got iou={iou}") + return iou + + +def precision_per_class(matrix: np.ndarray) -> np.ndarray: + """Compute the precision per class based on the confusion matrix. + + Args: + matrix: the computed confusion matrix + + Returns: + the precision (per class), defined as TP/(TP+FP) + """ + if not matrix.shape[0] == matrix.shape[1]: + # If the matrix is not square (there is a column for "other" label), the "other" column is deleted. + # Otherwise, there will be 3 elements in TP and 4 in TP+FP meaning they can't be divided. + matrix = np.delete(matrix, -1, 1) + + tp_per_class = matrix.diagonal() + sum_tp_fp_per_class = matrix.sum(0) + return divide_arrays_with_possible_zeros(tp_per_class, sum_tp_fp_per_class) + + +def recall_per_class(matrix: np.ndarray) -> np.ndarray: + """Compute the recall per class based on the confusion matrix. + + Args: + matrix: the computed confusion matrix + + Returns: + the recall (per class), defined as TP/(TP+FN) + """ + tp_per_class = matrix.diagonal() + sum_tp_fn_per_class = matrix.sum(1) + return divide_arrays_with_possible_zeros(tp_per_class, sum_tp_fn_per_class) + + +def divide_arrays_with_possible_zeros(array1: np.ndarray, array2: np.ndarray) -> np.ndarray: + """Sometimes the denominator in the precision or recall computation can contain a zero. + + In that case, a zero is returned for that element (https://stackoverflow.com/a/32106804). + + Args: + array1: the numerator + array2: the denominator + + Returns: + the divided arrays (numerator/denominator) with a value of zero + where the denominator was zero. + """ + with np.errstate(divide="ignore", invalid="ignore"): + result = np.true_divide(array1, array2) + result[result == np.inf] = 0 # If the denominator is a float, np.inf is returned + result = np.nan_to_num(result) # If the denominator is an int, np.nan is returned + return result diff --git a/src/otx/api/usecases/evaluation/dice.py b/src/otx/api/usecases/evaluation/dice.py new file mode 100644 index 00000000000..13ef986a12f --- /dev/null +++ b/src/otx/api/usecases/evaluation/dice.py @@ -0,0 +1,226 @@ +"""This module contains the Dice performance provider.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from typing import Dict, List, Optional, Tuple +import numpy as np + +from otx.api.entities.label import LabelEntity +from otx.api.entities.metrics import ( + BarChartInfo, + BarMetricsGroup, + ColorPalette, + MetricsGroup, + Performance, + ScoreMetric, +) +from otx.api.entities.resultset import ResultSetEntity +from otx.api.usecases.evaluation.averaging import MetricAverageMethod +from otx.api.usecases.evaluation.basic_operations import ( + get_intersections_and_cardinalities, +) +from otx.api.usecases.evaluation.performance_provider_interface import ( + IPerformanceProvider, +) +from otx.api.utils.segmentation_utils import mask_from_dataset_item +from otx.api.utils.time_utils import timeit +from otx.api.entities.image import Image + + +class DiceAverage(IPerformanceProvider): + """Computes the average Dice coefficient overall and for individual labels. + + See https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient for background information. + + To compute the Dice coefficient the shapes in the dataset items of the prediction and ground truth + dataset are first converted to masks. + + Dice is computed by computing the intersection and union computed over the whole dataset, instead of + computing intersection and union for individual images and then averaging. + + Args: + resultset (ResultSetEntity): ResultSet that score will be computed for + average (MetricAverageMethod): One of + - MICRO: every pixel has the same weight, regardless of label + - MACRO: compute score per label, return the average of the per-label scores + """ + + def __init__( + self, + resultset: ResultSetEntity, + average: MetricAverageMethod = MetricAverageMethod.MACRO, + ): + self.average = average + ( + self._overall_dice, + self._dice_per_label, + ) = self.__compute_dice_averaged_over_pixels(resultset, average) + + @property + def overall_dice(self) -> ScoreMetric: + """Returns the dice average as ScoreMetric.""" + return self._overall_dice + + @property + def dice_per_label(self) -> Dict[LabelEntity, ScoreMetric]: + """Returns a dictionary mapping the label to its corresponding dice score (as ScoreMetric).""" + return self._dice_per_label + + def get_performance(self) -> Performance: + """Returns the performance of the resultset.""" + score = self.overall_dice + dashboard_metrics: Optional[List[MetricsGroup]] + if len(self.dice_per_label) == 0: + dashboard_metrics = None + else: + dashboard_metrics = [ + BarMetricsGroup( + metrics=list(self.dice_per_label.values()), + visualization_info=BarChartInfo( + name="Dice Average Per Label", + palette=ColorPalette.LABEL, + ), + ) + ] + return Performance(score=score, dashboard_metrics=dashboard_metrics) + + @classmethod + @timeit + def __compute_dice_averaged_over_pixels( + cls, resultset: ResultSetEntity, average: MetricAverageMethod + ) -> Tuple[ScoreMetric, Dict[LabelEntity, ScoreMetric]]: + """Computes the diced averaged over pixels. + + Args: + resultset (ResultSetEntity): Result set to use + average (MetricAverageMethod): Averaging method to use + + Returns: + Tuple[ScoreMetric, Dict[LabelEntity, ScoreMetric]]: Tuple of the overall dice and the dice averaged over + pixels for each label. + """ + if len(resultset.prediction_dataset) == 0: + raise ValueError("Cannot compute the DICE score of an empty result set.") + + if len(resultset.prediction_dataset) != len(resultset.ground_truth_dataset): + raise ValueError( + f"Prediction and ground truth dataset should have the same length. " + f"Ground truth dataset has {len(resultset.ground_truth_dataset)} items, " + f"prediction dataset has {len(resultset.prediction_dataset)} items" + ) + resultset_labels = set(resultset.prediction_dataset.get_labels() + resultset.ground_truth_dataset.get_labels()) + model_labels = set(resultset.model.configuration.get_label_schema().get_labels(include_empty=False)) + labels = sorted(resultset_labels.intersection(model_labels)) + hard_predictions = [] + hard_references = [] + for prediction_item, reference_item in zip( + list(resultset.prediction_dataset), list(resultset.ground_truth_dataset) + ): + try: + hard_predictions.append(mask_from_dataset_item(prediction_item, labels)) + hard_references.append(mask_from_dataset_item(reference_item, labels)) + except: + # when item consists of masks with Image properties + # TODO (sungchul): how to add condition to check if polygon or mask? + labels_map = {label: i + 1 for i, label in enumerate(labels)} + + def combine_masks(annotations): + combined_mask = None + for annotation in annotations: + if isinstance(annotation.shape, Image): + scored_label = annotation.get_labels()[0] + label = scored_label.label + if combined_mask is None: + combined_mask = np.where(annotation.shape.numpy > 0, labels_map[label], 0) + else: + combined_mask += np.where(annotation.shape.numpy > 0, labels_map[label], 0) + combined_mask = np.expand_dims(combined_mask, axis=2) + return combined_mask + + hard_predictions.append(combine_masks(prediction_item.get_annotations())) + hard_references.append(combine_masks(reference_item.get_annotations())) + + all_intersection, all_cardinality = get_intersections_and_cardinalities( + hard_references, hard_predictions, labels + ) + + return cls.compute_dice_using_intersection_and_cardinality(all_intersection, all_cardinality, average) + + @classmethod + def compute_dice_using_intersection_and_cardinality( + cls, + all_intersection: Dict[Optional[LabelEntity], int], + all_cardinality: Dict[Optional[LabelEntity], int], + average: MetricAverageMethod, + ) -> Tuple[ScoreMetric, Dict[LabelEntity, ScoreMetric]]: + """Computes dice score using intersection and cardinality dictionaries. + + Both dictionaries must contain the same set of keys. + Dice score is computed by: 2 * intersection / cardinality + + Args: + average: Averaging method to use + all_intersection: collection of intersections per label + all_cardinality: collection of cardinality per label + + Returns: + A tuple containing the overall DICE score, and per label + DICE score + + Raises: + KeyError: if the keys in intersection and cardinality do not + match + KeyError: if the key `None` is not present in either + all_intersection or all_cardinality + ValueError: if the intersection for a certain key is larger + than its corresponding cardinality + """ + dice_per_label: Dict[LabelEntity, ScoreMetric] = {} + + for label, intersection in all_intersection.items(): + cardinality = all_cardinality[label] + dice_score = cls.__compute_single_dice_score_using_intersection_and_cardinality(intersection, cardinality) + + # If label is None, then the dice score corresponds to the overall dice score + # rather than a per-label dice score. + # This score is calculated last because it can depend on the values in dice_per_label + if label is not None: + dice_per_label[label] = ScoreMetric(value=dice_score, name=label.name) + + # Set overall_dice to 0 in case the score cannot be computed + overall_dice = ScoreMetric(value=0.0, name="Dice Average") + if len(dice_per_label) == 0: # dataset consists of background pixels only + pass # Use the default value of 0 + elif average == MetricAverageMethod.MICRO: + overall_cardinality = all_cardinality[None] + overall_intersection = all_intersection[None] + dice_score = cls.__compute_single_dice_score_using_intersection_and_cardinality( + overall_intersection, overall_cardinality + ) + overall_dice = ScoreMetric(value=dice_score, name="Dice Average") + elif average == MetricAverageMethod.MACRO: + scores = [item.value for item in dice_per_label.values()] + macro_average_score = sum(scores) / len(scores) + overall_dice = ScoreMetric(value=macro_average_score, name="Dice Average") + + return overall_dice, dice_per_label + + @staticmethod + def __compute_single_dice_score_using_intersection_and_cardinality(intersection: int, cardinality: int): + """Computes a single dice score using intersection and cardinality. + + Dice score is computed by: 2 * intersection / cardinality + + Raises: + ValueError: If intersection is larger than cardinality + """ + if intersection > cardinality: + raise ValueError("intersection cannot be larger than cardinality") + if cardinality == 0 and intersection == 0: + dice_score = 0.0 + else: + dice_score = float(2 * intersection / cardinality) + return dice_score diff --git a/src/otx/api/usecases/evaluation/f_measure.py b/src/otx/api/usecases/evaluation/f_measure.py new file mode 100644 index 00000000000..4837fcf193a --- /dev/null +++ b/src/otx/api/usecases/evaluation/f_measure.py @@ -0,0 +1,871 @@ +"""This module contains the f-measure performance provider class.""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict, List, Optional, Tuple +from otx.utils.logger import get_logger + +import numpy as np + +from otx.api.entities.annotation import Annotation +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import LabelEntity +from otx.api.entities.metrics import ( + BarChartInfo, + BarMetricsGroup, + ColorPalette, + CurveMetric, + LineChartInfo, + LineMetricsGroup, + MetricsGroup, + MultiScorePerformance, + ScoreMetric, + TextChartInfo, + TextMetricsGroup, + VisualizationType, +) +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.usecases.evaluation.performance_provider_interface import ( + IPerformanceProvider, +) +from otx.api.utils.shape_factory import ShapeFactory + +logger = get_logger() +ALL_CLASSES_NAME = "All Classes" + + +def intersection_box( + box1: Tuple[float, float, float, float, str, float], box2: Tuple[float, float, float, float, str, float] +) -> Tuple[float, float, float, float]: + """Calculate the intersection box of two bounding boxes. + + Args: + box1 (Tuple[float, float, float, float, str, float]): (x1, y1, x2, y2, class, score) + box2 (Tuple[float, float, float, float, str, float]): (x1, y1, x2, y2, class, score) + + Returns: + Tuple[float, float, float, float]: (x_left, x_right, y_bottom, y_top) + """ + x_left = max(box1[0], box2[0]) + y_top = max(box1[1], box2[1]) + x_right = min(box1[2], box2[2]) + y_bottom = min(box1[3], box2[3]) + return (x_left, x_right, y_bottom, y_top) + + +def bounding_box_intersection_over_union( + box1: Tuple[float, float, float, float, str, float], box2: Tuple[float, float, float, float, str, float] +) -> float: + """Calculate the Intersection over Union (IoU) of two bounding boxes. + + Args: + box1 (Tuple[float, float, float, float, str, float]): (x1, y1, x2, y2, class, score) + box2 (Tuple[float, float, float, float, str, float]): (x1, y1, x2, y2, class, score) + + Raises: + ValueError: In case the IoU is outside of [0.0, 1.0] + + Returns: + float: Intersection-over-union of box1 and box2. + """ + + x_left, x_right, y_bottom, y_top = intersection_box(box1, box2) + + if x_right <= x_left or y_bottom <= y_top: + iou = 0.0 + else: + intersection_area = (x_right - x_left) * (y_bottom - y_top) + bb1_area = (box1[2] - box1[0]) * (box1[3] - box1[1]) + bb2_area = (box2[2] - box2[0]) * (box2[3] - box2[1]) + union_area = float(bb1_area + bb2_area - intersection_area) + if union_area == 0: + iou = 0.0 + else: + iou = intersection_area / union_area + if iou < 0.0 or iou > 1.0: + raise ValueError(f"intersection over union should be in range [0,1], actual={iou}") + return iou + + +def get_iou_matrix( + ground_truth: List[Tuple[float, float, float, float, str, float]], + predicted: List[Tuple[float, float, float, float, str, float]], +) -> np.ndarray: + """Constructs an iou matrix of shape [num_ground_truth_boxes, num_predicted_boxes]. + + Each cell(x,y) in the iou matrix contains the intersection over union of ground truth box(x) and predicted box(y) + An iou matrix corresponds to a single image + + Args: + ground_truth (List[Tuple[float, float, float, float, str, float]]): List of ground truth boxes. + Each box is a list of (x,y) coordinates and a label. + a box: [x1: float, y1, x2, y2, class: str, score: float] + boxes_per_image: [box1, box2, …] + boxes1: [boxes_per_image_1, boxes_per_image_2, boxes_per_image_3, …] + predicted (List[Tuple[float, float, float, float, str, float]]): List of predicted boxes. + Each box is a list of (x,y) coordinates and a label. + a box: [x1: float, y1, x2, y2, class: str, score: float] + boxes_per_image: [box1, box2, …] + boxes2: [boxes_per_image_1, boxes_per_image_2, boxes_per_image_3, …] + + Returns: + np.ndarray: IoU matrix of shape [ground_truth_boxes, predicted_boxes] + """ + matrix = np.array( + [[bounding_box_intersection_over_union(gts, preds) for preds in predicted] for gts in ground_truth] + ) + return matrix + + +def get_n_false_negatives(iou_matrix: np.ndarray, iou_threshold: float) -> int: + """Get the number of false negatives inside the IoU matrix for a given threshold. + + The first loop accounts for all the ground truth boxes which do not have a high enough iou with any predicted + box (they go undetected) + The second loop accounts for the much rarer case where two ground truth boxes are detected by the same predicted + box. The principle is that each ground truth box requires a unique prediction box + + Args: + iou_matrix (np.ndarray): IoU matrix of shape [ground_truth_boxes, predicted_boxes] + iou_threshold (float): IoU threshold to use for the false negatives. + + Returns: + int: Number of false negatives + """ + n_false_negatives = 0 + for row in iou_matrix: + if max(row) < iou_threshold: + n_false_negatives += 1 + for column in np.rot90(iou_matrix): + indices = np.where(column > iou_threshold) + n_false_negatives += max(len(indices[0]) - 1, 0) + return n_false_negatives + + +class _Metrics: + """This class collects the metrics related to detection. + + Args: + f_measure (float): F-measure of the model. + precision (float): Precision of the model. + recall (float): Recall of the model. + """ + + def __init__(self, f_measure: float, precision: float, recall: float): + self.f_measure = f_measure + self.precision = precision + self.recall = recall + + +class _ResultCounters: + """This class collects the number of prediction, TP and FN. + + Args: + n_false_negatives (int): Number of false negatives. + n_true (int): Number of true positives. + n_predictions (int): Number of predictions. + """ + + def __init__(self, n_false_negatives: int, n_true: int, n_predicted: int): + self.n_false_negatives = n_false_negatives + self.n_true = n_true + self.n_predicted = n_predicted + + def calculate_f_measure(self) -> _Metrics: + """Calculates and returns precision, recall, and f-measure. + + Returns: + _Metrics: _Metrics object with Precision, recall, and f-measure. + """ + n_true_positives = self.n_true - self.n_false_negatives + + if self.n_predicted == 0: + precision = 1.0 + recall = 0.0 + elif self.n_true == 0: + precision = 0.0 + recall = 1.0 + else: + precision = n_true_positives / self.n_predicted + recall = n_true_positives / self.n_true + + f_measure = (2 * precision * recall) / (precision + recall + np.finfo(float).eps) + return _Metrics(f_measure, precision, recall) + + +class _AggregatedResults: + """This class collects the aggregated results for F-measure. + + The result contains: + - f_measure_curve + - precision_curve + - recall_curve + - all_classes_f_measure_curve + - best_f_measure + - best_threshold + - best_f_measure_metrics + + Args: + classes (List[str]): List of classes. + """ + + def __init__(self, classes: List[str]): + self.f_measure_curve: Dict[str, List[float]] = {class_name: [] for class_name in classes} + self.precision_curve: Dict[str, List[float]] = {class_name: [] for class_name in classes} + self.recall_curve: Dict[str, List[float]] = {class_name: [] for class_name in classes} + self.all_classes_f_measure_curve: List[float] = [] + self.best_f_measure: float = 0.0 + self.best_threshold: float = 0.0 + self.best_f_measure_metrics: _Metrics = _Metrics(0.0, 0.0, 0.0) + + +class _OverallResults: + """This class collects the overall results that is computed by the F-measure performance provider. + + Args: + per_confidence (_AggregatedResults): _AggregatedResults object for each confidence level. + per_nms (Optional[_AggregatedResults]): _AggregatedResults object for each NMS threshold. + best_f_measure_per_class (Dict[str, float]): Best f-measure per class. + best_f_measure (float): Best f-measure. + """ + + def __init__( + self, + per_confidence: _AggregatedResults, + per_nms: Optional[_AggregatedResults], + best_f_measure_per_class: Dict[str, float], + best_f_measure: float, + ): + self.per_confidence = per_confidence + self.per_nms = per_nms + self.best_f_measure_per_class = best_f_measure_per_class + self.best_f_measure = best_f_measure + + +class _FMeasureCalculator: + """This class contains the functions to calculate FMeasure. + + Args: + ground_truth_boxes_per_image (List[List[Tuple[float, float, float, float, str, float]]]): + a box: [x1: float, y1, x2, y2, class: str, score: float] + boxes_per_image: [box1, box2, …] + ground_truth_boxes_per_image: [boxes_per_image_1, boxes_per_image_2, boxes_per_image_3, …] + prediction_boxes_per_image (List[List[Tuple[float, float, float, float, str, float]]]): + a box: [x1: float, y1, x2, y2, class: str, score: float] + boxes_per_image: [box1, box2, …] + predicted_boxes_per_image: [boxes_per_image_1, boxes_per_image_2, boxes_per_image_3, …] + """ + + def __init__( + self, + ground_truth_boxes_per_image: List[List[Tuple[float, float, float, float, str, float]]], + prediction_boxes_per_image: List[List[Tuple[float, float, float, float, str, float]]], + ): + self.ground_truth_boxes_per_image = ground_truth_boxes_per_image + self.prediction_boxes_per_image = prediction_boxes_per_image + self.confidence_range = [0.025, 1.0, 0.025] + self.nms_range = [0.1, 1, 0.05] + self.default_confidence_threshold = 0.35 + + def evaluate_detections( + self, + classes: List[str], + iou_threshold: float = 0.5, + result_based_nms_threshold: bool = False, + cross_class_nms: bool = False, + ) -> _OverallResults: + """Evaluates detections by computing f_measures across multiple confidence thresholds and iou thresholds. + + By default, this function evaluates 39 confidence thresholds, finds the best confidence threshold and appends + it to the result Dict + Each one of the (default 39+20) pairs of confidence and nms thresholds is used to evaluate the f-measure for + each class, then the intermediate metrics are summed across classes to compute an all_classes f_measure. + Finally, the best results across all evaluations are appended to the result dictionary along with the thresholds + used to achieve them. + + Args: + classes (List[str]): Names of classes to be evaluated. + iou_threshold (float): IOU threshold. Defaults to 0.5. + result_based_nms_threshold (bool): Boolean that determines whether multiple nms threshold are examined. + Defaults to False. + cross_class_nms (bool): Set to True to perform NMS between boxes with different classes. Defaults to False. + + Returns: + _OverallResults: _OverallResults object with the result statistics (e.g F-measure). + """ + + best_f_measure_per_class = {} + + results_per_confidence = self.get_results_per_confidence( + classes=classes, + confidence_range=self.confidence_range, + iou_threshold=iou_threshold, + ) + + best_f_measure = results_per_confidence.best_f_measure + + for class_name in classes: + best_f_measure_per_class[class_name] = max(results_per_confidence.f_measure_curve[class_name]) + + results_per_nms: Optional[_AggregatedResults] = None + + if result_based_nms_threshold: + results_per_nms = self.get_results_per_nms( + classes=classes, + iou_threshold=iou_threshold, + min_f_measure=results_per_confidence.best_f_measure, + cross_class_nms=cross_class_nms, + ) + + for class_name in classes: + best_f_measure_per_class[class_name] = max(results_per_nms.f_measure_curve[class_name]) + + result = _OverallResults( + results_per_confidence, + results_per_nms, + best_f_measure_per_class, + best_f_measure, + ) + + return result + + def get_results_per_confidence( + self, classes: List[str], confidence_range: List[float], iou_threshold: float + ) -> _AggregatedResults: + """Returns the results for confidence threshold in range confidence_range. + + Varies confidence based on confidence_range, the results are appended in a dictionary and returned, it also + returns the best f_measure found and the confidence threshold used to get said f_measure + + Args: + classes (List[str]): Names of classes to be evaluated. + confidence_range (List[float]): List of confidence thresholds to be evaluated. + iou_threshold (float): IoU threshold to use for false negatives. + + Returns: + _AggregatedResults: _AggregatedResults object with the result statistics (e.g F-measure). + """ + result = _AggregatedResults(classes) + result.best_threshold = 0.1 + + for confidence_threshold in np.arange(*confidence_range): + result_point = self.evaluate_classes( + classes=classes.copy(), + iou_threshold=iou_threshold, + confidence_threshold=confidence_threshold, + ) + all_classes_f_measure = result_point[ALL_CLASSES_NAME].f_measure + result.all_classes_f_measure_curve.append(all_classes_f_measure) + + for class_name in classes: + result.f_measure_curve[class_name].append(result_point[class_name].f_measure) + result.precision_curve[class_name].append(result_point[class_name].precision) + result.recall_curve[class_name].append(result_point[class_name].recall) + if all_classes_f_measure > 0.0 and all_classes_f_measure >= result.best_f_measure: + result.best_f_measure = all_classes_f_measure + result.best_threshold = confidence_threshold + result.best_f_measure_metrics = result_point[ALL_CLASSES_NAME] + return result + + def get_results_per_nms( + self, + classes: List[str], + iou_threshold: float, + min_f_measure: float, + cross_class_nms: bool = False, + ) -> _AggregatedResults: + """Returns results for nms threshold in range nms_range. + + First, we calculate the critical nms of each box, meaning the nms_threshold + that would cause it to be disappear + This is an expensive O(n**2) operation, however, doing this makes filtering for every single nms_threshold much + faster at O(n) + + Args: + classes (List[str]): List of classes + iou_threshold (float): IoU threshold + min_f_measure (float): the minimum F-measure required to select a NMS threshold + cross_class_nms (bool): set to True to perform NMS between boxes with different classes. Defaults to False. + + Returns: + _AggregatedResults: Object containing the results for each NMS threshold value + """ + result = _AggregatedResults(classes) + result.best_f_measure = min_f_measure + result.best_threshold = 0.5 + + critical_nms_per_image = self.__get_critical_nms(self.prediction_boxes_per_image, cross_class_nms) + + for nms_threshold in np.arange(*self.nms_range): + predicted_boxes_per_image_per_nms = self.__filter_nms( + self.prediction_boxes_per_image, critical_nms_per_image, nms_threshold + ) + boxes_pair_for_nms = _FMeasureCalculator( + self.ground_truth_boxes_per_image, predicted_boxes_per_image_per_nms + ) + result_point = boxes_pair_for_nms.evaluate_classes( + classes=classes.copy(), + iou_threshold=iou_threshold, + confidence_threshold=self.default_confidence_threshold, + ) + all_classes_f_measure = result_point[ALL_CLASSES_NAME].f_measure + result.all_classes_f_measure_curve.append(all_classes_f_measure) + + for class_name in classes: + result.f_measure_curve[class_name].append(result_point[class_name].f_measure) + result.precision_curve[class_name].append(result_point[class_name].precision) + result.recall_curve[class_name].append(result_point[class_name].recall) + + if all_classes_f_measure > 0.0 and all_classes_f_measure >= result.best_f_measure: + result.best_f_measure = all_classes_f_measure + result.best_threshold = nms_threshold + result.best_f_measure_metrics = result_point[ALL_CLASSES_NAME] + return result + + def evaluate_classes( + self, classes: List[str], iou_threshold: float, confidence_threshold: float + ) -> Dict[str, _Metrics]: + """Returns Dict of f_measure, precision and recall for each class. + + Args: + classes (List[str]): List of classes to be evaluated. + iou_threshold (float): IoU threshold to use for false negatives. + confidence_threshold (float): Confidence threshold to use for false negatives. + + Returns: + Dict[str, _Metrics]: The metrics (e.g. F-measure) for each class. + """ + result: Dict[str, _Metrics] = {} + + all_classes_counters = _ResultCounters(0, 0, 0) + + if ALL_CLASSES_NAME in classes: + classes.remove(ALL_CLASSES_NAME) + for class_name in classes: + metrics, counters = self.get_f_measure_for_class( + class_name=class_name, + iou_threshold=iou_threshold, + confidence_threshold=confidence_threshold, + ) + result[class_name] = metrics + all_classes_counters.n_false_negatives += counters.n_false_negatives + all_classes_counters.n_true += counters.n_true + all_classes_counters.n_predicted += counters.n_predicted + + # for all classes + result[ALL_CLASSES_NAME] = all_classes_counters.calculate_f_measure() + return result + + def get_f_measure_for_class( + self, class_name: str, iou_threshold: float, confidence_threshold: float + ) -> Tuple[_Metrics, _ResultCounters]: + """Get f_measure for specific class, iou threshold, and confidence threshold. + + In order to reduce the number of redundant iterations and allow for cleaner, more general code later on, + all boxes are filtered at this stage by class and predicted boxes are filtered by confidence threshold + + Args: + + class_name (str): Name of the class for which the F measure is computed + iou_threshold (float): IoU threshold + confidence_threshold (float): Confidence threshold + + Returns: + Tuple[_Metrics, _ResultCounters]: a structure containing the statistics (e.g. f_measure) and a structure + containing the intermediated counters used to derive the stats (e.g. num. false positives) + """ + class_ground_truth_boxes_per_image = self.__filter_class(self.ground_truth_boxes_per_image, class_name) + confidence_predicted_boxes_per_image = self.__filter_confidence( + self.prediction_boxes_per_image, confidence_threshold + ) + class_predicted_boxes_per_image = self.__filter_class(confidence_predicted_boxes_per_image, class_name) + if len(class_ground_truth_boxes_per_image) > 0: + boxes_pair_per_class = _FMeasureCalculator( + ground_truth_boxes_per_image=class_ground_truth_boxes_per_image, + prediction_boxes_per_image=class_predicted_boxes_per_image, + ) + result_counters = boxes_pair_per_class.get_counters(iou_threshold=iou_threshold) + result_metrics = result_counters.calculate_f_measure() + results = (result_metrics, result_counters) + else: + logger.warning("No ground truth images supplied for f-measure calculation.") + # [f_measure, precision, recall, n_false_negatives, n_true, n_predicted] + results = (_Metrics(0.0, 0.0, 0.0), _ResultCounters(0, 0, 0)) + return results + + @staticmethod + def __get_critical_nms( + boxes_per_image: List[List[Tuple[float, float, float, float, str, float]]], cross_class_nms: bool = False + ) -> List[List[float]]: + """Return list of critical NMS values for each box in each image. + + Maps each predicted box to the highest nms-threshold which would suppress that box, aka the smallest + nms_threshold before the box disappears. + Having these values allows us to later filter by nms-threshold in O(n) rather than O(n**2) + Highest losing iou, holds the value of the highest iou that a box has with any + other box of the same class and higher confidence score. + + Args: + boxes_per_image (List[List[Tuple[float, float, float, float, str, float]]]): List of predicted boxes per + image. + a box: [x1: float, y1, x2, y2, class: str, score: float] + boxes_per_image: [box1, box2, …] + cross_class_nms (bool): Whether to use cross class NMS. + + Returns: + List[List[float]]: List of critical NMS values for each box in each image. + """ + critical_nms_per_image = [] + for boxes in boxes_per_image: + critical_nms_per_box = [] + for box1 in boxes: + highest_losing_iou = 0.0 + for box2 in boxes: + iou = bounding_box_intersection_over_union(box1, box2) + if ( + (cross_class_nms or box1[FMeasure.box_class_index] == box2[FMeasure.box_class_index]) + # TODO boxes Tuple should be refactored to dataclass. + and box1[FMeasure.box_score_index] < box2[FMeasure.box_score_index] # type: ignore[operator] + and iou > highest_losing_iou + ): + highest_losing_iou = iou + critical_nms_per_box.append(highest_losing_iou) + critical_nms_per_image.append(critical_nms_per_box) + return critical_nms_per_image + + @staticmethod + def __filter_nms( + boxes_per_image: List[List[Tuple[float, float, float, float, str, float]]], + critical_nms: List[List[float]], + nms_threshold: float, + ) -> List[List[Tuple[float, float, float, float, str, float]]]: + """Filters out predicted boxes whose critical nms is higher than the given nms_threshold. + + Args: + boxes_per_image (List[List[Tuple[float, float, float, float, str, float]]]): List of boxes per image. + a box: [x1: float, y1, x2, y2, class: str, score: float] + boxes_per_image: [box1, box2, …] + critical_nms (List[List[float]]): List of list of critical nms for each box in each image + nms_threshold (float): NMS threshold used for filtering + + Returns: + List[List[Tuple[float, float, float, float, str, float]]]: List of list of filtered boxes in each image + """ + new_boxes_per_image = [] + for boxes, boxes_nms in zip(boxes_per_image, critical_nms): + new_boxes = [] + for box, nms in zip(boxes, boxes_nms): + if nms < nms_threshold: + new_boxes.append(box) + new_boxes_per_image.append(new_boxes) + return new_boxes_per_image + + @staticmethod + def __filter_class( + boxes_per_image: List[List[Tuple[float, float, float, float, str, float]]], class_name: str + ) -> List[List[Tuple[float, float, float, float, str, float]]]: + """Filters boxes to only keep members of one class. + + Args: + boxes_per_image (List[List[Tuple[float, float, float, float, str, float]]]): a list of lists of boxes + class_name (str): Name of the class for which the boxes are filtered + + Returns: + List[List[Tuple[float, float, float, float, str, float]]]: a list of lists of boxes + """ + filtered_boxes_per_image = [] + for boxes in boxes_per_image: + filtered_boxes = [] + for box in boxes: + # TODO boxes Tuple should be refactored to dataclass. This way we can access box.class + if box[FMeasure.box_class_index].lower() == class_name.lower(): # type: ignore[union-attr] + filtered_boxes.append(box) + filtered_boxes_per_image.append(filtered_boxes) + return filtered_boxes_per_image + + @staticmethod + def __filter_confidence( + boxes_per_image: List[List[Tuple[float, float, float, float, str, float]]], confidence_threshold: float + ) -> List[List[Tuple[float, float, float, float, str, float]]]: + """Filters boxes to only keep ones with higher confidence than a given confidence threshold. + + Args: + boxes_per_image (List[List[Tuple[float, float, float, float, str, float]]]): + a box: [x1: float, y1, x2, y2, class: str, score: float] + boxes_per_image: [box1, box2, …] + confidence_threshold (float): Confidence threshold + + Returns: + List[List[Tuple[float, float, float, float, str, float]]]: Boxes with higher confidence than the given + threshold. + """ + filtered_boxes_per_image = [] + for boxes in boxes_per_image: + filtered_boxes = [] + for box in boxes: + if float(box[FMeasure.box_score_index]) > confidence_threshold: + filtered_boxes.append(box) + filtered_boxes_per_image.append(filtered_boxes) + return filtered_boxes_per_image + + def get_counters(self, iou_threshold: float) -> _ResultCounters: + """Return counts of true positives, false positives and false negatives for a given iou threshold. + + For each image (the loop), compute the number of false negatives, the number of predicted boxes, and the number + of ground truth boxes, then add each value to its corresponding counter + + Args: + iou_threshold (float): IoU threshold + + Returns: + _ResultCounters: Structure containing the number of false negatives, true positives and predictions. + """ + n_false_negatives = 0 + n_true = 0 + n_predicted = 0 + for ground_truth_boxes, predicted_boxes in zip( + self.ground_truth_boxes_per_image, self.prediction_boxes_per_image + ): + n_true += len(ground_truth_boxes) + n_predicted += len(predicted_boxes) + if len(predicted_boxes) > 0: + if len(ground_truth_boxes) > 0: + iou_matrix = get_iou_matrix(ground_truth_boxes, predicted_boxes) + n_false_negatives += get_n_false_negatives(iou_matrix, iou_threshold) + else: + n_false_negatives += len(ground_truth_boxes) + return _ResultCounters(n_false_negatives, n_true, n_predicted) + + +class FMeasure(IPerformanceProvider): + """Computes the f-measure (also known as F1-score) for a resultset. + + The f-measure is typically used in detection (localization) tasks to obtain a single number that balances precision + and recall. + + To determine whether a predicted box matches a ground truth box an overlap measured + is used based on a minimum + intersection-over-union (IoU), by default a value of 0.5 is used. + + In addition spurious results are eliminated by applying non-max suppression (NMS) so that two predicted boxes with + IoU > threshold are reduced to one. This threshold can be determined automatically by setting `vary_nms_threshold` + to True. + + Args: + resultset (ResultSetEntity) :ResultSet entity used for calculating the F-Measure + vary_confidence_threshold (bool): if True the maximal F-measure is determined by optimizing for different + confidence threshold values Defaults to False. + vary_nms_threshold (bool): if True the maximal F-measure is determined by optimizing for different NMS threshold + values. Defaults to False. + cross_class_nms (bool): Whether non-max suppression should be applied cross-class. If True this will eliminate + boxes with sufficient overlap even if they are from different classes. Defaults to False. + + Raises: + ValueError: if prediction dataset and ground truth dataset are empty + """ + + def __init__( + self, + resultset: ResultSetEntity, + vary_confidence_threshold: bool = False, + vary_nms_threshold: bool = False, + cross_class_nms: bool = False, + ): + + ground_truth_dataset: DatasetEntity = resultset.ground_truth_dataset + prediction_dataset: DatasetEntity = resultset.prediction_dataset + + if len(prediction_dataset) == 0 or len(ground_truth_dataset) == 0: + raise ValueError("Cannot compute the F-measure of an empty result set.") + + labels = resultset.model.configuration.get_label_schema().get_labels(include_empty=False) + classes = [label.name for label in labels] + boxes_pair = _FMeasureCalculator( + FMeasure.__get_boxes_from_dataset_as_list(ground_truth_dataset, labels), + FMeasure.__get_boxes_from_dataset_as_list(prediction_dataset, labels), + ) + result = boxes_pair.evaluate_detections( + result_based_nms_threshold=vary_nms_threshold, + classes=classes, + cross_class_nms=cross_class_nms, + ) + self._f_measure = ScoreMetric(name="f-measure", value=result.best_f_measure) + self._f_measure_per_label: Dict[LabelEntity, ScoreMetric] = {} + for label in labels: + self.f_measure_per_label[label] = ScoreMetric( + name=label.name, value=result.best_f_measure_per_class[label.name] + ) + self._precision = ScoreMetric(name="Precision", value=result.per_confidence.best_f_measure_metrics.precision) + self._recall = ScoreMetric(name="Recall", value=result.per_confidence.best_f_measure_metrics.recall) + + self._f_measure_per_confidence: Optional[CurveMetric] = None + self._best_confidence_threshold: Optional[ScoreMetric] = None + + if vary_confidence_threshold: + self._f_measure_per_confidence = CurveMetric( + name="f-measure per confidence", + xs=list(np.arange(*boxes_pair.confidence_range)), + ys=result.per_confidence.all_classes_f_measure_curve, + ) + self._best_confidence_threshold = ScoreMetric( + name="Optimal confidence threshold", + value=result.per_confidence.best_threshold, + ) + + self._f_measure_per_nms: Optional[CurveMetric] = None + self._best_nms_threshold: Optional[ScoreMetric] = None + + if vary_nms_threshold and result.per_nms is not None: + self._f_measure_per_nms = CurveMetric( + name="f-measure per nms", + xs=list(np.arange(*boxes_pair.nms_range)), + ys=result.per_nms.all_classes_f_measure_curve, + ) + self._best_nms_threshold = ScoreMetric(name="Optimal nms threshold", value=result.per_nms.best_threshold) + + box_class_index = 4 + box_score_index = 5 + + @property + def f_measure(self) -> ScoreMetric: + """Returns the f-measure as ScoreMetric.""" + return self._f_measure + + @property + def f_measure_per_label(self) -> Dict[LabelEntity, ScoreMetric]: + """Returns the f-measure per label as dictionary (Label -> ScoreMetric).""" + return self._f_measure_per_label + + @property + def f_measure_per_confidence(self) -> Optional[CurveMetric]: + """Returns the curve for f-measure per confidence as CurveMetric if exists.""" + return self._f_measure_per_confidence + + @property + def best_confidence_threshold(self) -> Optional[ScoreMetric]: + """Returns best confidence threshold as ScoreMetric if exists.""" + return self._best_confidence_threshold + + @property + def f_measure_per_nms(self) -> Optional[CurveMetric]: + """Returns the curve for f-measure per nms threshold as CurveMetric if exists.""" + return self._f_measure_per_nms + + @property + def best_nms_threshold(self) -> Optional[ScoreMetric]: + """Returns the best NMS threshold as ScoreMetric if exists.""" + return self._best_nms_threshold + + def get_performance(self) -> MultiScorePerformance: + """Returns the performance which consists of the F-Measure score and the dashboard metrics. + + Returns: + MultiScorePerformance: MultiScorePerformance object containing the F-Measure scores and the dashboard metrics. + """ + dashboard_metrics: List[MetricsGroup] = [] + dashboard_metrics.append( + BarMetricsGroup( + metrics=list(self.f_measure_per_label.values()), + visualization_info=BarChartInfo( + name="F-measure per label", + palette=ColorPalette.LABEL, + visualization_type=VisualizationType.RADIAL_BAR, + ), + ) + ) + if self.f_measure_per_confidence is not None: + dashboard_metrics.append( + LineMetricsGroup( + metrics=[self.f_measure_per_confidence], + visualization_info=LineChartInfo( + name="F-measure per confidence", + x_axis_label="Confidence threshold", + y_axis_label="F-measure", + ), + ) + ) + + if self.best_confidence_threshold is not None: + dashboard_metrics.append( + TextMetricsGroup( + metrics=[self.best_confidence_threshold], + visualization_info=TextChartInfo( + name="Optimal confidence threshold", + ), + ) + ) + + if self.f_measure_per_nms is not None: + dashboard_metrics.append( + LineMetricsGroup( + metrics=[self.f_measure_per_nms], + visualization_info=LineChartInfo( + name="F-measure per nms", + x_axis_label="NMS threshold", + y_axis_label="F-measure", + ), + ) + ) + + if self.best_nms_threshold is not None: + dashboard_metrics.append( + TextMetricsGroup( + metrics=[self.best_nms_threshold], + visualization_info=TextChartInfo( + name="Optimal nms threshold", + ), + ) + ) + return MultiScorePerformance( + primary_score=self.f_measure, + additional_scores=[self._precision, self._recall], + dashboard_metrics=dashboard_metrics, + ) + + @staticmethod + def __get_boxes_from_dataset_as_list( + dataset: DatasetEntity, labels: List[LabelEntity] + ) -> List[List[Tuple[float, float, float, float, str, float]]]: + """Return list of boxes from dataset. + + Explanation of output shape: + a box: [x1: float, y1, x2, y2, class: str, score: float] + boxes_per_image: [box1, box2, …] + ground_truth_boxes_per_image: [boxes_per_image_1, boxes_per_image_2, boxes_per_image_3, …] + + Args: + dataset (DatasetEntity): Dataset to get boxes from. + labels (List[LabelEntity]): Labels to get boxes for. + + Returns: + List[List[Tuple[float, float, float, float, str, float]]]: List of boxes for each image in the dataset. + """ + boxes_per_image = [] + converted_types_to_box = set() + label_names = {label.name for label in labels} + for item in dataset: + boxes: List[Tuple[float, float, float, float, str, float]] = [] + roi_as_box = Annotation(ShapeFactory.shape_as_rectangle(item.roi.shape), labels=[]) + for annotation in item.annotation_scene.annotations: + shape_as_box = ShapeFactory.shape_as_rectangle(annotation.shape) + box = shape_as_box.normalize_wrt_roi_shape(roi_as_box.shape) + n_boxes_before = len(boxes) + boxes.extend( + [ + (box.x1, box.y1, box.x2, box.y2, label.name, label.probability) + for label in annotation.get_labels() + if label.name in label_names + ] + ) + if not isinstance(annotation.shape, Rectangle) and len(boxes) > n_boxes_before: + converted_types_to_box.add(annotation.shape.__class__.__name__) + boxes_per_image.append(boxes) + if len(converted_types_to_box) > 0: + logger.warning( + f"The shapes of types {tuple(converted_types_to_box)} have been converted to their " + f"full enclosing Box representation in order to compute the f-measure" + ) + + return boxes_per_image diff --git a/src/otx/api/usecases/evaluation/metrics_helper.py b/src/otx/api/usecases/evaluation/metrics_helper.py new file mode 100644 index 00000000000..0216dbdab24 --- /dev/null +++ b/src/otx/api/usecases/evaluation/metrics_helper.py @@ -0,0 +1,110 @@ +"""Helper functions for computing metrics. + +This module contains the helper functions which can be called directly by algorithm implementers to obtain the metrics. +""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from otx.api.entities.resultset import ResultSetEntity +from otx.api.usecases.evaluation.accuracy import Accuracy +from otx.api.usecases.evaluation.anomaly_metrics import ( + AnomalyDetectionScores, + AnomalySegmentationScores, +) +from otx.api.usecases.evaluation.averaging import MetricAverageMethod +from otx.api.usecases.evaluation.dice import DiceAverage +from otx.api.usecases.evaluation.f_measure import FMeasure + + +class MetricsHelper: + """Contains metrics computation functions. + + TODO: subject for refactoring. + """ + + @staticmethod + def compute_f_measure( + resultset: ResultSetEntity, + vary_confidence_threshold: bool = False, + vary_nms_threshold: bool = False, + cross_class_nms: bool = False, + ) -> FMeasure: + """Compute the F-Measure on a resultset given some parameters. + + Args: + resultset: The resultset used to compute f-measure + vary_confidence_threshold: Flag specifying whether f-measure + shall be computed for different confidence threshold + values + vary_nms_threshold: Flag specifying whether f-measure shall + be computed for different NMS threshold values + cross_class_nms: Whether non-max suppression should be + applied cross-class + + Returns: + FMeasure object + """ + return FMeasure(resultset, vary_confidence_threshold, vary_nms_threshold, cross_class_nms) + + @staticmethod + def compute_dice_averaged_over_pixels( + resultset: ResultSetEntity, + average: MetricAverageMethod = MetricAverageMethod.MACRO, + ) -> DiceAverage: + """Compute the Dice average on a resultset, averaged over the pixels. + + Args: + resultset: The resultset used to compute the Dice average + average: The averaging method, either MICRO or MACRO + + Returns: + DiceAverage object + """ + return DiceAverage(resultset=resultset, average=average) + + @staticmethod + def compute_accuracy( + resultset: ResultSetEntity, + average: MetricAverageMethod = MetricAverageMethod.MICRO, + ) -> Accuracy: + """Compute the Accuracy on a resultset, averaged over the different label groups. + + Args: + resultset: The resultset used to compute the accuracy + average: The averaging method, either MICRO or MACRO + + Returns: + Accuracy object + """ + return Accuracy(resultset=resultset, average=average) + + @staticmethod + def compute_anomaly_segmentation_scores( + resultset: ResultSetEntity, + ) -> AnomalySegmentationScores: + """Compute the anomaly localization performance metrics on an anomaly segmentation resultset. + + Args: + resultset: The resultset used to compute the metrics + + Returns: + AnomalyLocalizationScores object + """ + return AnomalySegmentationScores(resultset) + + @staticmethod + def compute_anomaly_detection_scores( + resultset: ResultSetEntity, + ) -> AnomalyDetectionScores: + """Compute the anomaly localization performance metrics on an anomaly detection resultset. + + Args: + resultset: The resultset used to compute the metrics + + Returns: + AnomalyLocalizationScores object + """ + return AnomalyDetectionScores(resultset) diff --git a/src/otx/api/usecases/evaluation/performance_provider_interface.py b/src/otx/api/usecases/evaluation/performance_provider_interface.py new file mode 100644 index 00000000000..31b69afba90 --- /dev/null +++ b/src/otx/api/usecases/evaluation/performance_provider_interface.py @@ -0,0 +1,22 @@ +"""This module contains interface for performance providers.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import abc + +from otx.api.entities.metrics import Performance + + +class IPerformanceProvider(metaclass=abc.ABCMeta): + """Interface for performance provider. + + TODO: subject for refactoring. + """ + + @abc.abstractmethod + def get_performance(self) -> Performance: + """Returns the computed performance.""" + raise NotImplementedError diff --git a/src/otx/api/usecases/exportable_code/__init__.py b/src/otx/api/usecases/exportable_code/__init__.py new file mode 100644 index 00000000000..6bce81f214f --- /dev/null +++ b/src/otx/api/usecases/exportable_code/__init__.py @@ -0,0 +1,4 @@ +"""Methods for exportable code.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/api/usecases/exportable_code/demo/LICENSE b/src/otx/api/usecases/exportable_code/demo/LICENSE new file mode 100644 index 00000000000..24bdeff5e1b --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/LICENSE @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "[]" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright (C) 2018-2021 Intel Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/src/otx/api/usecases/exportable_code/demo/README.md b/src/otx/api/usecases/exportable_code/demo/README.md new file mode 100644 index 00000000000..89bd18528b7 --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/README.md @@ -0,0 +1,151 @@ +# Exportable code + +Exportable code is a .zip archive that contains simple demo to get and visualize result of model inference. + +## Structure of generated zip + +- `README.md` +- model + - `model.xml` + - `model.bin` + - `config.json` +- python + - model_wrappers (Optional) + - `__init__.py` + - model_wrappers required to run demo + - `LICENSE` + - `demo.py` + - `requirements.txt` + +> **NOTE**: Zip archive contains model_wrappers when [ModelAPI](https://github.com/openvinotoolkit/model_api) has no appropriate standard model wrapper for the model. + +## Prerequisites + +- [Python 3.8](https://www.python.org/downloads/) +- [Git](https://git-scm.com/) + +## Install requirements to run demo + +1. Install [prerequisites](#prerequisites). You may also need to [install pip](https://pip.pypa.io/en/stable/installation/). For example, on Ubuntu execute the following command to get pip installed: + + ```bash + sudo apt install python3-pip + ``` + +1. Create clean virtual environment: + + One of the possible ways for creating a virtual environment is to use `virtualenv`: + + ```bash + python -m pip install virtualenv + python -m virtualenv + ``` + + Before starting to work inside virtual environment, it should be activated: + + On Linux and macOS: + + ```bash + source /bin/activate + ``` + + On Windows: + + ```bash + .\\Scripts\activate + ``` + + Please make sure that the environment contains [wheel](https://pypi.org/project/wheel/) by calling the following command: + + ```bash + python -m pip install wheel + ``` + + > **NOTE**: On Linux and macOS, you may need to type `python3` instead of `python`. + +1. Install requirements in the environment: + + ```bash + python -m pip install -r requirements.txt + ``` + +## Usecase + +1. Running the `demo.py` application with the `-h` option yields the following usage message: + + ```bash + usage: demo.py [-h] -i INPUT -m MODELS [MODELS ...] [-it {sync,async}] [-l] [--no_show] [-d {CPU,GPU}] [--output OUTPUT] + + Options: + -h, --help Show this help message and exit. + -i INPUT, --input INPUT + Required. An input to process. The input must be a single image, a folder of images, video file or camera id. + -m MODELS [MODELS ...], --models MODELS [MODELS ...] + Optional. Path to directory with trained model and configuration file. If you provide several models you will start the task chain pipeline with the provided models in the order in which they were specified. Default value points to deployed model folder '../model'. + -it {sync,async}, --inference_type {sync,async} + Optional. Type of inference for single model. + -l, --loop Optional. Enable reading the input in a loop. + --no_show Optional. Disables showing inference results on UI. + -d {CPU,GPU}, --device {CPU,GPU} + Optional. Device to infer the model. + --output OUTPUT Optional. Output path to save input data with predictions. + ``` + +2. As a `model` parameter the default value `../model` will be used. Or you can specify the other path to the model directory from generated zip. You can pass as `input` a single image, a folder of images, a video file, or a web camera id. So you can use the following command to do inference with a pre-trained model: + + ```bash + python3 demo.py -i /inputVideo.mp4 + ``` + + You can press `Q` to stop inference during demo running. + + > **NOTE**: If you provide a single image as input, the demo processes and renders it quickly, then exits. To continuously + > visualize inference results on the screen, apply the `--loop` option, which enforces processing a single image in a loop. + > In this case, you can stop the demo by pressing `Q` button or killing the process in the terminal (`Ctrl+C` for Linux). + > + > **NOTE**: Default configuration contains info about pre- and post processing for inference and is guaranteed to be correct. + > Also you can change `config.json` that specifies the confidence threshold and color for each class visualization, but any + > changes should be made with caution. + +3. To save inferenced results with predictions on it, you can specify the folder path, using `--output`. + It works for images, videos, image folders and web cameras. To prevent issues, do not specify it together with a `--loop` parameter. + + ```bash + python3 demo.py \ + --input /inputImage.jpg \ + --models ../model \ + --output resulted_images + ``` + +4. To run a demo on a web camera, you need to know its ID. + You can check a list of camera devices by running this command line on Linux system: + + ```bash + sudo apt-get install v4l-utils + v4l2-ctl --list-devices + ``` + + The output will look like this: + + ```bash + Integrated Camera (usb-0000:00:1a.0-1.6): + /dev/video0 + ``` + + After that, you can use this `/dev/video0` as a camera ID for `--input`. + +## Troubleshooting + +1. If you have access to the Internet through the proxy server only, please use pip with proxy call as demonstrated by command below: + + ```bash + python -m pip install --proxy http://:@: + ``` + +1. If you use Anaconda environment, you should consider that OpenVINO has limited [Conda support](https://docs.openvino.ai/2021.4/openvino_docs_install_guides_installing_openvino_conda.html) for Python 3.6 and 3.7 versions only. But the demo package requires python 3.8. So please use other tools to create the environment (like `venv` or `virtualenv`) and use `pip` as a package manager. + +1. If you have problems when you try to use `pip install` command, please update pip version by following command: + + ```bash + python -m pip install --upgrade pip + ``` diff --git a/src/otx/api/usecases/exportable_code/demo/__init__.py b/src/otx/api/usecases/exportable_code/demo/__init__.py new file mode 100644 index 00000000000..6547d6e5b2d --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/__init__.py @@ -0,0 +1,4 @@ +"""OTX Exportable code.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/api/usecases/exportable_code/demo/demo.py b/src/otx/api/usecases/exportable_code/demo/demo.py new file mode 100644 index 00000000000..3f13c165e57 --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/demo.py @@ -0,0 +1,133 @@ +"""Demo based on ModelAPI.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import sys +from argparse import SUPPRESS, ArgumentParser +from pathlib import Path + +os.environ["FEATURE_FLAGS_OTX_ACTION_TASKS"] = "1" + +# pylint: disable=no-name-in-module, import-error +from otx.api.usecases.exportable_code.demo.demo_package import ( + AsyncExecutor, + ChainExecutor, + ModelContainer, + SyncExecutor, + create_visualizer, +) + + +def build_argparser(): + """Parses command line arguments.""" + parser = ArgumentParser(add_help=False) + args = parser.add_argument_group("Options") + args.add_argument( + "-h", + "--help", + action="help", + default=SUPPRESS, + help="Show this help message and exit.", + ) + args.add_argument( + "-i", + "--input", + required=True, + help="Required. An input to process. The input must be a single image, " + "a folder of images, video file or camera id.", + ) + args.add_argument( + "-m", + "--models", + help="Optional. Path to directory with trained model and configuration file. " + "If you provide several models you will start the task chain pipeline with " + "the provided models in the order in which they were specified. Default value " + "points to deployed model folder '../model'.", + nargs="+", + default=[Path("../model")], + type=Path, + ) + args.add_argument( + "-it", + "--inference_type", + help="Optional. Type of inference for single model.", + choices=["sync", "async"], + default="sync", + type=str, + ) + args.add_argument( + "-l", + "--loop", + help="Optional. Enable reading the input in a loop.", + default=False, + action="store_true", + ) + args.add_argument( + "--no_show", + help="Optional. Disables showing inference results on UI.", + default=False, + action="store_true", + ) + args.add_argument( + "-d", + "--device", + help="Optional. Device to infer the model.", + choices=["CPU", "GPU"], + default="CPU", + type=str, + ) + args.add_argument( + "--output", + default=None, + type=str, + help="Optional. Output path to save input data with predictions.", + ) + + return parser + + +EXECUTORS = { + "sync": SyncExecutor, + "async": AsyncExecutor, + "chain": ChainExecutor, +} + + +def get_inferencer_class(type_inference, models): + """Return class for inference of models.""" + if len(models) > 1: + type_inference = "chain" + print("You started the task chain pipeline with the provided models in the order in which they were specified") + return EXECUTORS[type_inference] + + +def main(): + """Main function that is used to run demo.""" + args = build_argparser().parse_args() + + if args.loop and args.output: + raise ValueError("--loop and --output cannot be both specified") + + # create models + models = [] + for model_dir in args.models: + model = ModelContainer(model_dir, device=args.device) + models.append(model) + + inferencer = get_inferencer_class(args.inference_type, models) + + # create visualizer + visualizer = create_visualizer(models[-1].task_type, no_show=args.no_show, output=args.output) + + if len(models) == 1: + models = models[0] + + # create inferencer and run + demo = inferencer(models, visualizer) + demo.run(args.input, args.loop and not args.no_show) + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/src/otx/api/usecases/exportable_code/demo/demo_package/__init__.py b/src/otx/api/usecases/exportable_code/demo/demo_package/__init__.py new file mode 100644 index 00000000000..1d83e822bfe --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/demo_package/__init__.py @@ -0,0 +1,18 @@ +"""Initialization of demo package.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .executors import AsyncExecutor, ChainExecutor, SyncExecutor +from .model_container import ModelContainer +from .utils import create_output_converter, create_visualizer + +__all__ = [ + "SyncExecutor", + "AsyncExecutor", + "ChainExecutor", + "create_output_converter", + "create_visualizer", + "ModelContainer", +] diff --git a/src/otx/api/usecases/exportable_code/demo/demo_package/executors/__init__.py b/src/otx/api/usecases/exportable_code/demo/demo_package/executors/__init__.py new file mode 100644 index 00000000000..397ad7d9232 --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/demo_package/executors/__init__.py @@ -0,0 +1,15 @@ +"""Initialization of executors.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .asynchronous import AsyncExecutor +from .sync_pipeline import ChainExecutor +from .synchronous import SyncExecutor + +__all__ = [ + "SyncExecutor", + "AsyncExecutor", + "ChainExecutor", +] diff --git a/src/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py b/src/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py new file mode 100644 index 00000000000..50b1b0c4707 --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py @@ -0,0 +1,88 @@ +"""Async executor based on ModelAPI.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import time +from typing import Any, Tuple, Union + +import numpy as np +from openvino.model_api.pipelines import AsyncPipeline + +from otx.api.usecases.exportable_code.demo.demo_package.model_container import ( + ModelContainer, +) +from otx.api.usecases.exportable_code.demo.demo_package.utils import ( + create_output_converter, +) +from otx.api.usecases.exportable_code.streamer import get_streamer +from otx.api.usecases.exportable_code.visualizers import Visualizer +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import DetectionToAnnotationConverter +from otx.api.utils.vis_utils import dump_frames + + +class AsyncExecutor: + """Async inferencer. + + Args: + model: model for inference + visualizer: visualizer of inference results + """ + + def __init__(self, model: ModelContainer, visualizer: Visualizer) -> None: + self.model = model.core_model + self.visualizer = visualizer + self.converter = create_output_converter(model.task_type, model.labels, model.model_parameters) + self.async_pipeline = AsyncPipeline(self.model) + + def run(self, input_stream: Union[int, str], loop: bool = False) -> None: + """Async inference for input stream (image, video stream, camera).""" + streamer = get_streamer(input_stream, loop) + next_frame_id = 0 + next_frame_id_to_show = 0 + stop_visualization = False + saved_frames = [] + + for frame in streamer: + results = self.async_pipeline.get_result(next_frame_id_to_show) + while results: + start_time = time.perf_counter() + output = self.render_result(results) + next_frame_id_to_show += 1 + self.visualizer.show(output) + if self.visualizer.output: + saved_frames.append(output) + if self.visualizer.is_quit(): + stop_visualization = True + # visualize video not faster than the original FPS + self.visualizer.video_delay(time.perf_counter() - start_time, streamer) + results = self.async_pipeline.get_result(next_frame_id_to_show) + if stop_visualization: + break + self.async_pipeline.submit_data(frame, next_frame_id, {"frame": frame}) + next_frame_id += 1 + self.async_pipeline.await_all() + for next_frame_id_to_show in range(next_frame_id_to_show, next_frame_id): + start_time = time.perf_counter() + results = self.async_pipeline.get_result(next_frame_id_to_show) + output = self.render_result(results) + self.visualizer.show(output) + if self.visualizer.output: + saved_frames.append(output) + # visualize video not faster than the original FPS + self.visualizer.video_delay(time.perf_counter() - start_time, streamer) + dump_frames(saved_frames, self.visualizer.output, input_stream, streamer) + + def render_result(self, results: Tuple[Any, dict]) -> np.ndarray: + """Render for results of inference.""" + predictions, frame_meta = results + if isinstance(self.converter, DetectionToAnnotationConverter): + # Predictions for the detection task + predictions = np.array( + [[pred.id, pred.score, *[pred.xmin, pred.ymin, pred.xmax, pred.ymax]] for pred in predictions.objects] + ) + predictions.shape = len(predictions), 6 + annotation_scene = self.converter.convert_to_annotation(predictions, frame_meta) + current_frame = frame_meta["frame"] + output = self.visualizer.draw(current_frame, annotation_scene, frame_meta) + return output diff --git a/src/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py b/src/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py new file mode 100644 index 00000000000..4a395f87882 --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py @@ -0,0 +1,97 @@ +"""Sync pipeline executor based on ModelAPI.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import time +from typing import List, Tuple, Union + +import numpy as np + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.usecases.exportable_code.demo.demo_package.model_container import ( + ModelContainer, +) +from otx.api.usecases.exportable_code.demo.demo_package.utils import ( + create_output_converter, +) +from otx.api.usecases.exportable_code.streamer import get_streamer +from otx.api.usecases.exportable_code.visualizers import Visualizer +from otx.api.utils.shape_factory import ShapeFactory +from otx.api.utils.vis_utils import dump_frames + + +class ChainExecutor: + """Sync executor for task-chain inference. + + Args: + models: list of models for inference + visualizer: visualizer of inference results + """ + + def __init__( + self, + models: List[ModelContainer], + visualizer: Visualizer, + ) -> None: + self.models = models + self.visualizer = visualizer + self.converters = [] + for model in self.models: + self.converters.append(create_output_converter(model.task_type, model.labels, model.model_parameters)) + + # pylint: disable=too-many-locals + def single_run(self, input_image: np.ndarray) -> AnnotationSceneEntity: + """Inference for single image.""" + current_objects = [(input_image, Annotation(Rectangle(0, 0, 1, 1), labels=[]))] + result_scene = AnnotationSceneEntity([], AnnotationSceneKind.PREDICTION) + for index, model in enumerate(self.models): + new_objects = [] + for item, parent_annotation in current_objects: + predictions, frame_meta = model.core_model(item) + annotation_scene = self.converters[index].convert_to_annotation(predictions, frame_meta) + for annotation in annotation_scene.annotations: + new_item, item_annotation = self.crop(item, parent_annotation, annotation) + new_objects.append((new_item, item_annotation)) + if model.task_type.is_global: + for label in item_annotation.get_labels(): + parent_annotation.append_label(label) + else: + result_scene.append_annotation(item_annotation) + current_objects = new_objects + return result_scene + + @staticmethod + def crop( + item: np.ndarray, parent_annotation: Annotation, item_annotation: Annotation + ) -> Tuple[np.ndarray, Annotation]: + """Crop operation between chain stages.""" + new_item = ShapeFactory.shape_as_rectangle(item_annotation.shape).crop_numpy_array(item) + item_annotation.shape = item_annotation.shape.normalize_wrt_roi_shape( + ShapeFactory.shape_as_rectangle(parent_annotation.shape) + ) + return new_item, item_annotation + + def run(self, input_stream: Union[int, str], loop: bool = False) -> None: + """Run demo using input stream (image, video stream, camera).""" + streamer = get_streamer(input_stream, loop) + saved_frames = [] + + for frame in streamer: + # getting result for single image + start_time = time.perf_counter() + annotation_scene = self.single_run(frame) + output = self.visualizer.draw(frame, annotation_scene, {}) + self.visualizer.show(output) + if self.visualizer.output: + saved_frames.append(output) + if self.visualizer.is_quit(): + break + # visualize video not faster than the original FPS + self.visualizer.video_delay(time.perf_counter() - start_time, streamer) + dump_frames(saved_frames, self.visualizer.output, input_stream, streamer) diff --git a/src/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py b/src/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py new file mode 100644 index 00000000000..cb19bdea011 --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py @@ -0,0 +1,52 @@ +"""Synchronous Executor based on ModelAPI.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import time +from typing import Union + +from otx.api.usecases.exportable_code.demo.demo_package.model_container import ( + ModelContainer, +) +from otx.api.usecases.exportable_code.demo.demo_package.utils import ( + create_output_converter, +) +from otx.api.usecases.exportable_code.streamer import get_streamer +from otx.api.usecases.exportable_code.visualizers import Visualizer +from otx.api.utils.vis_utils import dump_frames + + +class SyncExecutor: + """Synchronous executor for model inference. + + Args: + model (ModelContainer): model for inference + visualizer (Visualizer): visualizer of inference results. Defaults to None. + """ + + def __init__(self, model: ModelContainer, visualizer: Visualizer) -> None: + self.model = model + self.visualizer = visualizer + self.converter = create_output_converter(model.task_type, model.labels, model.model_parameters) + + def run(self, input_stream: Union[int, str], loop: bool = False) -> None: + """Run demo using input stream (image, video stream, camera).""" + streamer = get_streamer(input_stream, loop) + saved_frames = [] + + for frame in streamer: + # getting result include preprocessing, infer, postprocessing for sync infer + start_time = time.perf_counter() + predictions, frame_meta = self.model(frame) + annotation_scene = self.converter.convert_to_annotation(predictions, frame_meta) + output = self.visualizer.draw(frame, annotation_scene, frame_meta) + self.visualizer.show(output) + if self.visualizer.output: + saved_frames.append(output) + if self.visualizer.is_quit(): + break + # visualize video not faster than the original FPS + self.visualizer.video_delay(time.perf_counter() - start_time, streamer) + + dump_frames(saved_frames, self.visualizer.output, input_stream, streamer) diff --git a/src/otx/api/usecases/exportable_code/demo/demo_package/model_container.py b/src/otx/api/usecases/exportable_code/demo/demo_package/model_container.py new file mode 100644 index 00000000000..4723de654de --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/demo_package/model_container.py @@ -0,0 +1,164 @@ +"""ModelContainer class used for loading the model in the model wrapper.""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import importlib +import json +from pathlib import Path +from typing import Any, Optional, Tuple, Union + +import numpy as np +from openvino.model_api.adapters import OpenvinoAdapter, create_core +from openvino.model_api.models import ImageModel, Model +from openvino.model_api.tilers import DetectionTiler, InstanceSegmentationTiler + +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model_template import TaskType +from otx.api.serialization.label_mapper import LabelSchemaMapper +from otx.api.utils.detection_utils import detection2array +from otx.api.utils.tiler import Tiler + +from .utils import get_model_path, get_parameters + + +class ModelContainer: + """Class for storing the model wrapper based on Model API and needed parameters of model. + + Args: + model_dir (Path): path to model directory + """ + + def __init__(self, model_dir: Path, device="CPU") -> None: + model_adapter = OpenvinoAdapter(create_core(), get_model_path(model_dir / "model.xml"), device=device) + + try: + config_data = model_adapter.model.get_rt_info(["otx_config"]) + if type(config_data) != str: + # OV 2023.0 return OVAny which needs to be casted with astype() + config_data = config_data.astype(str) + self.parameters = json.loads(config_data) + except RuntimeError: + self.parameters = get_parameters(model_dir / "config.json") + + self._labels = LabelSchemaMapper.backward(self.parameters["model_parameters"]["labels"]) + self._task_type = TaskType[self.parameters["converter_type"]] + + self.segm = bool( + self._task_type is TaskType.ROTATED_DETECTION or self._task_type is TaskType.INSTANCE_SEGMENTATION + ) + + # labels for modelAPI wrappers can be empty, because unused in pre- and postprocessing + self.model_parameters = self.parameters["model_parameters"] + + if self._task_type in ( + TaskType.ANOMALY_CLASSIFICATION, + TaskType.ANOMALY_DETECTION, + TaskType.ANOMALY_SEGMENTATION, + ): + # The anomaly task requires non-empty labels. + # modelapi_labels key is used as a workaround as labels key is used for labels in OTX SDK format + self.model_parameters["labels"] = ( + self.model_parameters.pop("modelapi_labels") + if "modelapi_labels" in self.model_parameters + else ["Normal", "Anomaly"] + ) + else: + # model already contains correct labels + self.model_parameters.pop("labels") + + self._initialize_wrapper() + self.core_model = Model.create_model( + model_adapter, + self.parameters["type_of_model"], + self.model_parameters, + preload=True, + ) + + self.tiler = self.setup_tiler(model_dir, device) + + def setup_tiler(self, model_dir, device) -> Optional[Union[DetectionTiler, InstanceSegmentationTiler]]: + """Setup tiler for model. + + Args: + model_dir (str): model directory + device (str): device to run model on + Returns: + Optional: Tiler object or None + """ + if not self.parameters.get("tiling_parameters") or not self.parameters["tiling_parameters"]["enable_tiling"]: + return None + + classifier = None + if self.parameters["tiling_parameters"].get("enable_tile_classifier", False): + adapter = OpenvinoAdapter(create_core(), get_model_path(model_dir / "tile_classifier.xml"), device=device) + classifier = ImageModel(inference_adapter=adapter, configuration={}, preload=True) + + tiler_config = { + "tile_size": int( + self.parameters["tiling_parameters"]["tile_size"] + * self.parameters["tiling_parameters"]["tile_ir_scale_factor"] + ), + "tiles_overlap": self.parameters["tiling_parameters"]["tile_overlap"] + / self.parameters["tiling_parameters"]["tile_ir_scale_factor"], + "max_pred_number": self.parameters["tiling_parameters"]["tile_max_number"], + } + + if self.segm: + return InstanceSegmentationTiler(self.core_model, tiler_config, tile_classifier_model=classifier) + else: + return DetectionTiler(self.core_model, tiler_config) + + @property + def task_type(self) -> TaskType: + """Task type property.""" + return self._task_type + + @property + def labels(self) -> LabelSchemaEntity: + """Labels property.""" + return self._labels + + @staticmethod + def _initialize_wrapper() -> None: + """Load the model class.""" + try: + importlib.import_module("model_wrappers") + except ModuleNotFoundError: + print("Using model wrapper from ModelAPI") + + def infer(self, frame): + """Infer with original image. + + Args: + frame (np.ndarray): image + Returns: + annotation_scene (AnnotationScene): prediction + frame_meta (Dict): dict with original shape + """ + # getting result include preprocessing, infer, postprocessing for sync infer + predictions = self.core_model(frame) + frame_meta = {"original_shape": frame.shape} + + # MaskRCNN returns tuple so no need to process + if self._task_type == TaskType.DETECTION: + predictions = detection2array(predictions.objects) + return predictions, frame_meta + + def infer_tile(self, frame): + """Infer by patching full image to tiles. + + Args: + frame (np.ndarray): image + Returns: + annotation_scene (AnnotationScene): prediction + frame_meta (Dict): dict with original shape + """ + + detections = self.tiler(frame) + return detections, {"original_shape": frame.shape} + + def __call__(self, input_data: np.ndarray) -> Tuple[Any, dict]: + """Infer entry wrapper.""" + if self.tiler: + return self.infer_tile(input_data) + return self.infer(input_data) diff --git a/src/otx/api/usecases/exportable_code/demo/demo_package/utils.py b/src/otx/api/usecases/exportable_code/demo/demo_package/utils.py new file mode 100644 index 00000000000..2c48b5a87ba --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/demo_package/utils.py @@ -0,0 +1,55 @@ +"""Utils for demo.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import json +from pathlib import Path +from typing import Any, Dict, Optional + +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model_template import TaskType, task_type_to_label_domain +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( + create_converter, +) +from otx.api.usecases.exportable_code.visualizers import Visualizer + + +def get_model_path(path: Optional[Path]) -> Path: + """Get path to model.""" + model_path = path + if model_path is None: + model_path = Path(__file__).parent / "model.xml" + if not model_path.exists(): + raise IOError("The path to the model was not found.") + + return model_path + + +def get_parameters(path: Optional[Path]) -> dict: + """Get hyper parameters to creating model.""" + parameters_path = path + if parameters_path is None: + parameters_path = Path(__file__).parent / "config.json" + if not parameters_path.exists(): + raise IOError("The path to the config was not found.") + + with open(parameters_path, "r", encoding="utf8") as file: + parameters = json.load(file) + + return parameters + + +def create_output_converter(task_type: TaskType, labels: LabelSchemaEntity, model_params: Dict[Any, Any]): + """Create annotation converter according to kind of task.""" + + converter_type = task_type_to_label_domain(task_type) + return create_converter(converter_type, labels, model_params) + + +def create_visualizer(_task_type: TaskType, no_show: bool = False, output: Optional[str] = None): + """Create visualizer according to kind of task.""" + + # TODO: use anomaly-specific visualizer for anomaly tasks + + return Visualizer(window_name="Result", no_show=no_show, output=output) diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt new file mode 100644 index 00000000000..f0f7085ae2f --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -0,0 +1,4 @@ +openvino==2023.3.0 +openvino-model-api==0.1.9 +otx @ git+https://github.com/openvinotoolkit/training_extensions/@74c47db3200000fedb0f0bcf52a3efc22e8dde6c#egg=otx +numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime diff --git a/src/otx/api/usecases/exportable_code/demo/setup.py b/src/otx/api/usecases/exportable_code/demo/setup.py new file mode 100644 index 00000000000..63b9f17524d --- /dev/null +++ b/src/otx/api/usecases/exportable_code/demo/setup.py @@ -0,0 +1,31 @@ +"""setup file for demo package.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from pathlib import Path + +from setuptools import find_packages, setup + +SETUP_DIR = Path(__file__).resolve().parent + +with open(SETUP_DIR / "requirements.txt", "r", encoding="utf8") as f: + required = f.read().splitlines() + +packages = find_packages(str(SETUP_DIR)) +package_dir = {packages[0]: str(SETUP_DIR / packages[0])} + +setup( + name=packages[0], + version="0.0", + author="Intel® Corporation", + license="Copyright (c) 2021-2022 Intel Corporation. " "SPDX-License-Identifier: Apache-2.0", + description="Demo based on ModelAPI classes", + packages=packages, + package_dir=package_dir, + package_data={ + packages[0]: ["*.json"], + }, + install_requires=required, +) diff --git a/src/otx/api/usecases/exportable_code/inference/__init__.py b/src/otx/api/usecases/exportable_code/inference/__init__.py new file mode 100644 index 00000000000..ef93bd6edfe --- /dev/null +++ b/src/otx/api/usecases/exportable_code/inference/__init__.py @@ -0,0 +1,13 @@ +"""Initialization of inference interfaces.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .inference import ( + IInferencer, +) + +__all__ = [ + "IInferencer", +] diff --git a/src/otx/api/usecases/exportable_code/inference/inference.py b/src/otx/api/usecases/exportable_code/inference/inference.py new file mode 100644 index 00000000000..8578388c44b --- /dev/null +++ b/src/otx/api/usecases/exportable_code/inference/inference.py @@ -0,0 +1,33 @@ +"""Interface for inferencer.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +from typing import Any, Tuple, Union + +from openvino.model_api.models import Model + +import numpy as np + +from otx.api.entities.annotation import AnnotationSceneEntity + +__all__ = [ + "IInferencer", +] + + +class IInferencer(metaclass=abc.ABCMeta): + """Base interface class for the inference task. + + This class could be used by both the analyse method in the task, and the exportable code inference. + + """ + + model: Model + + @abc.abstractmethod + def predict(self, image: np.ndarray) -> Union[AnnotationSceneEntity, Tuple[Any, ...]]: + """This method performs a prediction.""" + raise NotImplementedError diff --git a/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py b/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py new file mode 100644 index 00000000000..f84c2b0facf --- /dev/null +++ b/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py @@ -0,0 +1,621 @@ +"""Converters for output of inferencers.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +from typing import Any, Dict, List, Optional, Union + +import cv2 +import numpy as np +from openvino.model_api.models import utils +from openvino.model_api.models.utils import ( + AnomalyResult, + ClassificationResult, + DetectionResult, + ImageResultWithSoftPrediction, + InstanceSegmentationResult, +) + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.id import ID +from otx.api.entities.label import Domain +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.utils.detection_utils import detection2array +from otx.api.utils.labels_utils import get_empty_label +from otx.api.utils.segmentation_utils import create_annotation_from_segmentation_map +from otx.api.utils.time_utils import now + + +def convert_bbox_to_ellipse(x1, y1, x2, y2) -> Ellipse: + """Convert bbox to ellipse.""" + return Ellipse(x1, y1, x2, y2) + + +class IPredictionToAnnotationConverter(metaclass=abc.ABCMeta): + """Interface for converter.""" + + @abc.abstractmethod + def convert_to_annotation(self, predictions: Any, metadata: Dict) -> AnnotationSceneEntity: + """Convert raw predictions to AnnotationScene format. + + Args: + predictions (Any): raw predictions from inferencer + metadata (Dict): metadata from inferencer + + Returns: + AnnotationSceneEntity: annotation object containing the shapes obtained from the raw predictions. + """ + raise NotImplementedError + + +class DetectionToAnnotationConverter(IPredictionToAnnotationConverter): + """Converts Object Detections to Annotations. + + Args: + labels (List[LabelEntity]): list of labels + """ + + def __init__(self, labels: Union[LabelSchemaEntity, List], configuration: Optional[Dict[str, Any]] = None): + self.labels = labels.get_labels(include_empty=False) if isinstance(labels, LabelSchemaEntity) else labels + self.label_map = dict(enumerate(self.labels)) + self.use_ellipse_shapes = False + self.confidence_threshold = 0.0 + if configuration is not None: + if "use_ellipse_shapes" in configuration: + self.use_ellipse_shapes = configuration["use_ellipse_shapes"] + if "confidence_threshold" in configuration: + self.confidence_threshold = configuration["confidence_threshold"] + + def convert_to_annotation( + self, predictions: Union[DetectionResult, np.ndarray], metadata: Optional[Dict[str, np.ndarray]] = None + ) -> AnnotationSceneEntity: + """Convert predictions to annotation format. + + Args: + predictions (DetectionResult|np.ndarray): detection represented in ModelAPI format or + array with shape [num_predictions, 6] or + [num_predictions, 7] + Supported detection formats are + + * [label, confidence, x1, y1, x2, y2] + * [_, label, confidence, x1, y1, x2, y2] + .. note:: + `label` can be any integer that can be mapped to `self.labels` + `confidence` should be a value between 0 and 1 + `x1`, `x2`, `y1` and `y2` are expected to be normalized. + metadata (Optional[Dict]): Additional information + + Returns: + AnnotationScene: AnnotationScene Object containing the boxes obtained from the prediction. + """ + if isinstance(predictions, DetectionResult): + detections = detection2array(predictions.objects) + else: + detections = predictions + if metadata: + detections[:, 2:] /= np.tile(metadata["original_shape"][1::-1], 2) + annotations = self.__convert_to_annotations(detections) + # media_identifier = ImageIdentifier(image_id=ID()) + annotation_scene = AnnotationSceneEntity( + id=ID(), + kind=AnnotationSceneKind.PREDICTION, + editor="otx", + creation_date=now(), + annotations=annotations, + ) + + return annotation_scene + + def __convert_to_annotations(self, predictions: np.ndarray) -> List[Annotation]: + """Converts a list of Detections to OTX Annotation objects. + + Args: + predictions (np.ndarray): A list of predictions with shape [num_prediction, 6] or + [num_predictions, 7] + + Returns: + List[Annotation]: A list of Annotation objects with Rectangle shapes + + Raises: + ValueError: This error is raised if the shape of prediction is not + (n, 7) or (n, 6) + """ + annotations = [] + if len(predictions) and predictions.shape[1:] < (6,) or predictions.shape[1:] > (7,): + raise ValueError( + f"Shape of prediction is not expected, expected (n, 7) or (n, 6) but got {predictions.shape}" + ) + + for prediction in predictions: + if prediction.shape == (7,): + # Some OpenVINO models use an output shape of [7,] + # If this is the case, skip the first value as it is not used + prediction = prediction[1:] + + label = int(prediction[0]) + confidence = prediction[1] + scored_label = ScoredLabel(self.label_map[label], confidence) + coords = prediction[2:] + shape: Union[Ellipse, Rectangle] + + if confidence < self.confidence_threshold: + continue + + if self.use_ellipse_shapes: + shape = convert_bbox_to_ellipse(coords[0], coords[1], coords[2], coords[3]) + else: + shape = Rectangle(coords[0], coords[1], coords[2], coords[3]) + + annotations.append( + Annotation( + shape, + labels=[scored_label], + ) + ) + + return annotations + + +def create_converter( + converter_type: Domain, labels: LabelSchemaEntity, configuration: Optional[Dict[str, Any]] = None +) -> IPredictionToAnnotationConverter: + """Simple factory for converters based on type of tasks. + + Args: + converter_type (Domain): type of converter + labels (LabelSchemaEntity): label schema entity + """ + + converter: IPredictionToAnnotationConverter + if converter_type == Domain.DETECTION: + converter = DetectionToAnnotationConverter(labels, configuration) + elif converter_type == Domain.SEGMENTATION: + converter = SegmentationToAnnotationConverter(labels) + elif converter_type == Domain.CLASSIFICATION: + converter = ClassificationToAnnotationConverter(labels) + elif converter_type == Domain.ANOMALY_CLASSIFICATION: + converter = AnomalyClassificationToAnnotationConverter(labels) + elif converter_type == Domain.ANOMALY_DETECTION: + converter = AnomalyDetectionToAnnotationConverter(labels) + elif converter_type == Domain.ANOMALY_SEGMENTATION: + converter = AnomalySegmentationToAnnotationConverter(labels) + elif converter_type == Domain.INSTANCE_SEGMENTATION: + converter = MaskToAnnotationConverter(labels, configuration) + elif converter_type == Domain.ROTATED_DETECTION: + converter = RotatedRectToAnnotationConverter(labels, configuration) + else: + raise ValueError(f"Unknown converter type: {converter_type}") + + return converter + + +class DetectionBoxToAnnotationConverter(IPredictionToAnnotationConverter): + """Converts DetectionBox Predictions ModelAPI to Annotations. + + Args: + labels (LabelSchemaEntity): Label Schema containing the label info of the task + """ + + def __init__(self, labels: LabelSchemaEntity): + self.labels = labels.get_labels(include_empty=False) + + def convert_to_annotation( + self, predictions: List[utils.Detection], metadata: Dict[str, Any] + ) -> AnnotationSceneEntity: + """Convert predictions to OTX Annotation Scene using the metadata. + + Args: + predictions (tuple): Raw predictions from the model. + metadata (Dict[str, Any]): Variable containing metadata information. + + Returns: + AnnotationSceneEntity: OTX annotation scene entity object. + """ + annotations = [] + image_size = metadata["original_shape"][1::-1] + for box in predictions: + scored_label = ScoredLabel(self.labels[int(box.id)], float(box.score)) + coords = np.array([box.xmin, box.ymin, box.xmax, box.ymax], dtype=float) + if (coords[2] - coords[0]) * (coords[3] - coords[1]) < 1.0: + continue + coords /= np.tile(image_size, 2) + annotations.append( + Annotation( + Rectangle(coords[0], coords[1], coords[2], coords[3]), + labels=[scored_label], + ) + ) + + annotation_scene = AnnotationSceneEntity( + kind=AnnotationSceneKind.PREDICTION, + annotations=annotations, + ) + return annotation_scene + + +class SegmentationToAnnotationConverter(IPredictionToAnnotationConverter): + """Converts Segmentation Predictions ModelAPI to Annotations. + + Args: + labels (LabelSchemaEntity): Label Schema containing the label info of the task + """ + + def __init__(self, label_schema: LabelSchemaEntity): + labels = label_schema.get_labels(include_empty=False) + self.label_map = dict(enumerate(labels, 1)) + + def convert_to_annotation( + self, predictions: ImageResultWithSoftPrediction, metadata: Optional[Dict[str, Any]] = None + ) -> AnnotationSceneEntity: + """Convert predictions to OTX Annotation Scene using the metadata. + + Args: + predictions (tuple): Raw predictions from the model. + metadata (Dict[str, Any]): Variable containing metadata information. + + Returns: + AnnotationSceneEntity: OTX annotation scene entity object. + """ + annotations = create_annotation_from_segmentation_map( + hard_prediction=predictions.resultImage, + soft_prediction=predictions.soft_prediction, + label_map=self.label_map, + ) + + return AnnotationSceneEntity(kind=AnnotationSceneKind.PREDICTION, annotations=annotations) + + +class ClassificationToAnnotationConverter(IPredictionToAnnotationConverter): + """Converts Classification Predictions ModelAPI to Annotations. + + Args: + labels (LabelSchemaEntity): Label Schema containing the label info of the task + """ + + def __init__(self, label_schema: LabelSchemaEntity): + if len(label_schema.get_labels(False)) == 1: + self.labels = label_schema.get_labels(include_empty=True) + else: + self.labels = label_schema.get_labels(include_empty=False) + self.empty_label = get_empty_label(label_schema) + multilabel = len(label_schema.get_groups(False)) > 1 + multilabel = multilabel and len(label_schema.get_groups(False)) == len( + label_schema.get_labels(include_empty=False) + ) + self.hierarchical = not multilabel and len(label_schema.get_groups(False)) > 1 + + self.label_schema = label_schema + + def convert_to_annotation( + self, predictions: ClassificationResult, metadata: Optional[Dict] = None + ) -> AnnotationSceneEntity: + """Convert predictions to OTX Annotation Scene using the metadata. + + Args: + predictions (tuple): Raw predictions from the model. + metadata (Dict[str, Any]): Variable containing metadata information. + + Returns: + AnnotationSceneEntity: OTX annotation scene entity object. + """ + labels = [] + for label in predictions.top_labels: + labels.append(ScoredLabel(self.labels[label[0]], float(label[-1]))) + + if not labels and self.empty_label: + labels = [ScoredLabel(self.empty_label, probability=1.0)] + + annotations = [Annotation(Rectangle.generate_full_box(), labels=labels)] + return AnnotationSceneEntity(kind=AnnotationSceneKind.PREDICTION, annotations=annotations) + + +class AnomalyClassificationToAnnotationConverter(IPredictionToAnnotationConverter): + """Converts AnomalyClassification Predictions ModelAPI to Annotations. + + Args: + labels (LabelSchemaEntity): Label Schema containing the label info of the task + """ + + def __init__(self, label_schema: LabelSchemaEntity): + labels = label_schema.get_labels(include_empty=False) + self.normal_label = [label for label in labels if not label.is_anomalous][0] + self.anomalous_label = [label for label in labels if label.is_anomalous][0] + + def convert_to_annotation(self, predictions: AnomalyResult, metadata: Dict[str, Any]) -> AnnotationSceneEntity: + """Convert predictions to OTX Annotation Scene using the metadata. + + Args: + predictions (tuple): Raw predictions from the model. + metadata (Dict[str, Any]): Variable containing metadata information. + + Returns: + AnnotationSceneEntity: OTX annotation scene entity object. + """ + assert predictions.pred_score is not None + assert predictions.pred_label is not None + label = self.anomalous_label if predictions.pred_label == "Anomaly" else self.normal_label + + annotations = [ + Annotation( + Rectangle.generate_full_box(), + labels=[ScoredLabel(label=label, probability=float(predictions.pred_score))], + ) + ] + return AnnotationSceneEntity(kind=AnnotationSceneKind.PREDICTION, annotations=annotations) + + +class AnomalySegmentationToAnnotationConverter(IPredictionToAnnotationConverter): + """Converts AnomalyClassification Predictions ModelAPI to Annotations. + + Args: + labels (LabelSchemaEntity): Label Schema containing the label info of the task + """ + + def __init__(self, label_schema: LabelSchemaEntity): + labels = label_schema.get_labels(include_empty=False) + self.normal_label = [label for label in labels if not label.is_anomalous][0] + self.anomalous_label = [label for label in labels if label.is_anomalous][0] + self.label_map = {0: self.normal_label, 1: self.anomalous_label} + + def convert_to_annotation(self, predictions: AnomalyResult, metadata: Dict[str, Any]) -> AnnotationSceneEntity: + """Convert predictions to OTX Annotation Scene using the metadata. + + Args: + predictions (AnomalyResult): Raw predictions from the model. + metadata (Dict[str, Any]): Variable containing metadata information. + + Returns: + AnnotationSceneEntity: OTX annotation scene entity object. + """ + assert predictions.pred_mask is not None + assert predictions.anomaly_map is not None + annotations = create_annotation_from_segmentation_map( + predictions.pred_mask, predictions.anomaly_map / 255.0, self.label_map + ) + if len(annotations) == 0: + # TODO: add confidence to this label + annotations = [ + Annotation( + Rectangle.generate_full_box(), + labels=[ScoredLabel(label=self.normal_label, probability=1.0)], + ) + ] + return AnnotationSceneEntity(kind=AnnotationSceneKind.PREDICTION, annotations=annotations) + + +class AnomalyDetectionToAnnotationConverter(IPredictionToAnnotationConverter): + """Converts Anomaly Detection Predictions ModelAPI to Annotations. + + Args: + labels (LabelSchemaEntity): Label Schema containing the label info of the task + """ + + def __init__(self, label_schema: LabelSchemaEntity): + """Initialize AnomalyDetectionToAnnotationConverter. + + Args: + label_schema (LabelSchemaEntity): Label Schema containing the label info of the task + """ + labels = label_schema.get_labels(include_empty=False) + self.normal_label = [label for label in labels if not label.is_anomalous][0] + self.anomalous_label = [label for label in labels if label.is_anomalous][0] + self.label_map = {0: self.normal_label, 1: self.anomalous_label} + + def convert_to_annotation(self, predictions: AnomalyResult, metadata: Dict[str, Any]) -> AnnotationSceneEntity: + """Convert predictions to OTX Annotation Scene using the metadata. + + Args: + predictions (tuple): Raw predictions from the model. + metadata (Dict[str, Any]): Variable containing metadata information. + + Returns: + AnnotationSceneEntity: OTX annotation scene entity object. + """ + assert predictions.pred_boxes is not None + assert predictions.pred_score is not None + assert predictions.pred_mask is not None + annotations = [] + image_h, image_w = predictions.pred_mask.shape + for box in predictions.pred_boxes: + annotations.append( + Annotation( + Rectangle(box[0] / image_w, box[1] / image_h, box[2] / image_w, box[3] / image_h), + labels=[ScoredLabel(label=self.anomalous_label, probability=predictions.pred_score)], + ) + ) + if len(annotations) == 0: + # TODO: add confidence to this label + annotations = [ + Annotation( + Rectangle.generate_full_box(), + labels=[ScoredLabel(label=self.normal_label, probability=1.0)], + ) + ] + return AnnotationSceneEntity(kind=AnnotationSceneKind.PREDICTION, annotations=annotations) + + +class VisualPromptingToAnnotationConverter(IPredictionToAnnotationConverter): + """Converts Visual Prompting Predictions ModelAPI to Annotations. + + Args: + labels (LabelSchemaEntity): Label Schema containing the label info of the task + """ + + def convert_to_annotation(self, hard_prediction: np.ndarray, metadata: Dict[str, Any]) -> List[Annotation]: # type: ignore + """Convert predictions to OTX Annotation Scene using the metadata. + + Args: + hard_prediction (np.ndarray): Hard_prediction from the model. + metadata (Dict[str, Any]): Variable containing metadata information. + + Returns: + AnnotationSceneEntity: OTX annotation scene entity object. + """ + soft_prediction = metadata.get("soft_prediction", np.ones(hard_prediction.shape)) + # TODO (sungchul): condition to distinguish between mask and polygon + annotations = create_annotation_from_segmentation_map( + hard_prediction=hard_prediction, + soft_prediction=soft_prediction, + label_map={1: metadata["label"].label if isinstance(metadata["label"], ScoredLabel) else metadata["label"]}, + ) + + return annotations + + +class MaskToAnnotationConverter(IPredictionToAnnotationConverter): + """Converts DetectionBox Predictions ModelAPI to Annotations.""" + + def __init__(self, labels: LabelSchemaEntity, configuration: Optional[Dict[str, Any]] = None): + self.labels = labels.get_labels(include_empty=False) + self.use_ellipse_shapes = False + self.confidence_threshold = 0.0 + if configuration is not None: + if "use_ellipse_shapes" in configuration: + self.use_ellipse_shapes = configuration["use_ellipse_shapes"] + if "confidence_threshold" in configuration: + self.confidence_threshold = configuration["confidence_threshold"] + + def convert_to_annotation( + self, predictions: InstanceSegmentationResult, metadata: Dict[str, Any] + ) -> AnnotationSceneEntity: + """Convert predictions to OTX Annotation Scene using the metadata. + + Args: + predictions (tuple): Raw predictions from the model. + metadata (Dict[str, Any]): Variable containing metadata information. + + Returns: + AnnotationSceneEntity: OTX annotation scene entity object. + """ + annotations = [] + height, width, _ = metadata["original_shape"] + shape: Union[Polygon, Ellipse] + for obj in predictions.segmentedObjects: + if obj.score < self.confidence_threshold: + continue + if self.use_ellipse_shapes: + shape = convert_bbox_to_ellipse( + obj.xmin / width, obj.ymin / height, obj.xmax / width, obj.ymax / height + ) + annotations.append( + Annotation( + shape, + labels=[ScoredLabel(self.labels[int(obj.id) - 1], float(obj.score))], + ) + ) + else: + mask = obj.mask.astype(np.uint8) + contours, hierarchies = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + if hierarchies is None: + continue + for contour, hierarchy in zip(contours, hierarchies[0]): + if hierarchy[3] != -1: + continue + if len(contour) <= 2 or cv2.contourArea(contour) < 1.0: + continue + contour = list(contour) + points = [ + Point( + x=point[0][0] / width, + y=point[0][1] / height, + ) + for point in contour + ] + shape = Polygon(points=points) + annotations.append( + Annotation( + shape, + labels=[ScoredLabel(self.labels[int(obj.id) - 1], float(obj.score))], + ) + ) + annotation_scene = AnnotationSceneEntity( + kind=AnnotationSceneKind.PREDICTION, + annotations=annotations, + ) + return annotation_scene + + +class RotatedRectToAnnotationConverter(IPredictionToAnnotationConverter): + """Converts Rotated Rect (mask) Predictions ModelAPI to Annotations. + + Args: + labels (LabelSchemaEntity): Label Schema containing the label info of the task + """ + + def __init__(self, labels: LabelSchemaEntity, configuration: Optional[Dict[str, Any]] = None): + self.labels = labels.get_labels(include_empty=False) + self.use_ellipse_shapes = False + self.confidence_threshold = 0.0 + if configuration is not None: + if "use_ellipse_shapes" in configuration: + self.use_ellipse_shapes = configuration["use_ellipse_shapes"] + if "confidence_threshold" in configuration: + self.confidence_threshold = configuration["confidence_threshold"] + + def convert_to_annotation( + self, predictions: InstanceSegmentationResult, metadata: Dict[str, Any] + ) -> AnnotationSceneEntity: + """Convert predictions to OTX Annotation Scene using the metadata. + + Args: + predictions (tuple): Raw predictions from the model. + metadata (Dict[str, Any]): Variable containing metadata information. + + Returns: + AnnotationSceneEntity: OTX annotation scene entity object. + """ + annotations = [] + height, width, _ = metadata["original_shape"] + shape: Union[Polygon, Ellipse] + for obj in predictions.segmentedObjects: + if obj.score < self.confidence_threshold: + continue + if self.use_ellipse_shapes: + shape = convert_bbox_to_ellipse( + obj.xmin / width, obj.ymin / height, obj.xmax / width, obj.ymax / height + ) + annotations.append( + Annotation( + shape, + labels=[ScoredLabel(self.labels[int(obj.id) - 1], float(obj.score))], + ) + ) + else: + mask = obj.mask.astype(np.uint8) + contours, hierarchies = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + if hierarchies is None: + continue + for contour, hierarchy in zip(contours, hierarchies[0]): + if hierarchy[3] != -1: + continue + if len(contour) <= 2 or cv2.contourArea(contour) < 1.0: + continue + points = [ + Point( + x=point[0] / width, + y=point[1] / height, + ) + for point in cv2.boxPoints(cv2.minAreaRect(contour)) + ] + shape = Polygon(points=points) + annotations.append( + Annotation( + shape, + labels=[ScoredLabel(self.labels[int(obj.id) - 1], float(obj.score))], + ) + ) + annotation_scene = AnnotationSceneEntity( + kind=AnnotationSceneKind.PREDICTION, + annotations=annotations, + ) + return annotation_scene diff --git a/src/otx/api/usecases/exportable_code/streamer/__init__.py b/src/otx/api/usecases/exportable_code/streamer/__init__.py new file mode 100644 index 00000000000..c38457a5bc1 --- /dev/null +++ b/src/otx/api/usecases/exportable_code/streamer/__init__.py @@ -0,0 +1,28 @@ +"""Initialization of streamer.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.api.usecases.exportable_code.streamer.streamer import ( + CameraStreamer, + DirStreamer, + ImageStreamer, + InvalidInput, + OpenError, + ThreadedStreamer, + VideoStreamer, + BaseStreamer, + get_streamer, +) + +__all__ = [ + "CameraStreamer", + "DirStreamer", + "ImageStreamer", + "ThreadedStreamer", + "VideoStreamer", + "InvalidInput", + "OpenError", + "BaseStreamer", + "get_streamer", +] diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/streamer/streamer.py b/src/otx/api/usecases/exportable_code/streamer/streamer.py similarity index 78% rename from src/otx/core/exporter/exportable_code/demo/demo_package/streamer/streamer.py rename to src/otx/api/usecases/exportable_code/streamer/streamer.py index 904a93ddfde..d4a2d634931 100644 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/streamer/streamer.py +++ b/src/otx/api/usecases/exportable_code/streamer/streamer.py @@ -1,24 +1,35 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# """Streamer for reading input.""" -from __future__ import annotations +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# import abc -import contextlib import multiprocessing import os import queue import sys from enum import Enum -from pathlib import Path -from typing import TYPE_CHECKING, Iterator - -if TYPE_CHECKING: - import numpy as np +from typing import Iterator, Union import cv2 +import numpy as np + + +class InvalidInput(Exception): + """Exception for wrong input format.""" + + def __init__(self, message: str) -> None: + super().__init__(message) + self.message = message + + +class OpenError(Exception): + """Exception for error opening reader.""" + + def __init__(self, message: str) -> None: + super().__init__(message) + self.message = message class MediaType(Enum): @@ -33,16 +44,6 @@ class MediaType(Enum): class BaseStreamer(metaclass=abc.ABCMeta): """Base Streamer interface to implement Image, Video and Camera streamers.""" - @abc.abstractmethod - def __init__(self, input_path: str, loop: bool = False) -> None: - """Initialize the streamer object. - - Args: - input_path (str): path to the input stream - loop (bool, optional): whether to loop the stream or not. Defaults to False. - """ - raise NotImplementedError - @abc.abstractmethod def __iter__(self) -> Iterator[np.ndarray]: """Iterate through the streamer object that is a Python Generator object. @@ -61,7 +62,7 @@ def get_type(self) -> MediaType: """ raise NotImplementedError - def fps(self) -> float: + def fps(self): """Returns a frequency of getting images from source.""" raise NotImplementedError @@ -109,9 +110,11 @@ def __iter__(self) -> Iterator[np.ndarray]: process.start() try: - with contextlib.suppress(queue.Empty): - while process.is_alive() or not buffer.empty(): + while process.is_alive() or not buffer.empty(): + try: yield buffer.get(timeout=0.1) + except queue.Empty: + pass except GeneratorExit: process.terminate() finally: @@ -148,8 +151,7 @@ def __init__(self, input_path: str, loop: bool = False) -> None: self.cap = cv2.VideoCapture() status = self.cap.open(input_path) if not status: - msg = f"Can't open the video from {input_path}" - raise RuntimeError(msg) + raise InvalidInput(f"Can't open the video from {input_path}") def __iter__(self) -> Iterator[np.ndarray]: """Iterates over frames of the video. @@ -160,12 +162,13 @@ def __iter__(self) -> Iterator[np.ndarray]: status, image = self.cap.read() if status: yield cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - elif self.loop: - self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) else: - break + if self.loop: + self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + else: + break - def fps(self) -> float: + def fps(self): """Returns a frequency of getting images from source.""" return self.cap.get(cv2.CAP_PROP_FPS) @@ -188,13 +191,12 @@ class CameraStreamer(BaseStreamer): ... break """ - def __init__(self, camera_device: str = "0") -> None: + def __init__(self, camera_device: int = 0) -> None: self.media_type = MediaType.CAMERA try: self.stream = cv2.VideoCapture(int(camera_device)) - except ValueError as err: - msg = f"Can't find the camera {camera_device}" - raise ValueError(msg) from err + except ValueError as error: + raise InvalidInput(f"Can't find the camera {camera_device}") from error def __iter__(self) -> Iterator[np.ndarray]: """Read video and yield the frame. @@ -236,13 +238,11 @@ class ImageStreamer(BaseStreamer): def __init__(self, input_path: str, loop: bool = False) -> None: self.loop = loop self.media_type = MediaType.IMAGE - if not Path(input_path).is_file(): - msg = f"Can't find the image by {input_path}" - raise RuntimeError(msg) + if not os.path.isfile(input_path): + raise InvalidInput(f"Can't find the image by {input_path}") self.image = cv2.imread(input_path, cv2.IMREAD_COLOR) if self.image is None: - msg = f"Can't open the image from {input_path}" - raise RuntimeError(msg) + raise OpenError(f"Can't open the image from {input_path}") self.image = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB) def __iter__(self) -> Iterator[np.ndarray]: @@ -274,22 +274,19 @@ class DirStreamer(BaseStreamer): def __init__(self, input_path: str, loop: bool = False) -> None: self.loop = loop self.media_type = MediaType.DIR - self.dir = Path(input_path) - if not self.dir.is_dir(): - msg = f"Can't find the dir by {input_path}" - raise RuntimeError(msg) + self.dir = input_path + if not os.path.isdir(self.dir): + raise InvalidInput(f"Can't find the dir by {input_path}") self.names = sorted(os.listdir(self.dir)) if not self.names: - msg = f"The dir {input_path} is empty" - raise RuntimeError(msg) + raise OpenError(f"The dir {input_path} is empty") self.file_id = 0 for name in self.names: - filename = self.dir / name + filename = os.path.join(self.dir, name) image = cv2.imread(str(filename), cv2.IMREAD_COLOR) if image is not None: return - msg = f"Can't read the first image from {input_path}" - raise RuntimeError(msg) + raise OpenError(f"Can't read the first image from {input_path}") def __iter__(self) -> Iterator[np.ndarray]: """Iterates over the images in a directory. @@ -297,7 +294,7 @@ def __iter__(self) -> Iterator[np.ndarray]: If self.loop is True, it reiterates again from the first image in the directory. """ while self.file_id < len(self.names): - filename = self.dir / self.names[self.file_id] + filename = os.path.join(self.dir, self.names[self.file_id]) image = cv2.imread(str(filename), cv2.IMREAD_COLOR) if self.file_id < len(self.names) - 1: self.file_id = self.file_id + 1 @@ -312,35 +309,40 @@ def get_type(self) -> MediaType: def get_streamer( - input_stream: str, + input_stream: Union[int, str] = 0, loop: bool = False, threaded: bool = False, ) -> BaseStreamer: """Get streamer object based on the file path or camera device index provided. Args: - input_stream (str): Path to file or directory or index for camera. + input_stream (Union[int, str]): Path to file or directory or index for camera. loop (bool): Enable reading the input in a loop. Defaults to False. threaded (bool): Run streaming on a separate thread. Threaded streaming option. Defaults to False. Returns: BaseStreamer: Streamer object. """ - errors: list[Exception] = [] - streamer_types = (ImageStreamer, DirStreamer, VideoStreamer) - for reader in streamer_types: + # errors: Dict = {InvalidInput: [], OpenError: []} + errors = [] + streamer: BaseStreamer + for reader in (ImageStreamer, DirStreamer, VideoStreamer): try: - streamer = reader(input_stream, loop) # type: ignore [abstract] - return ThreadedStreamer(streamer) if threaded else streamer - except RuntimeError as error: # noqa: PERF203 + streamer = reader(input_stream, loop) # type: ignore + if threaded: + streamer = ThreadedStreamer(streamer) + return streamer + except (InvalidInput, OpenError) as error: errors.append(error) try: - streamer = CameraStreamer(input_stream) - return ThreadedStreamer(streamer) if threaded else streamer - except RuntimeError as error: + streamer = CameraStreamer(input_stream) # type: ignore + if threaded: + streamer = ThreadedStreamer(streamer) + return streamer + except (InvalidInput, OpenError) as error: errors.append(error) if errors: - raise RuntimeError(errors) + raise Exception(errors) sys.exit(1) diff --git a/src/otx/api/usecases/exportable_code/visualizers/__init__.py b/src/otx/api/usecases/exportable_code/visualizers/__init__.py new file mode 100644 index 00000000000..8c441c73d2d --- /dev/null +++ b/src/otx/api/usecases/exportable_code/visualizers/__init__.py @@ -0,0 +1,9 @@ +"""Initialization of visualizers.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .anomaly_visualizer import AnomalyVisualizer +from .visualizer import IVisualizer, Visualizer + +__all__ = ["AnomalyVisualizer", "IVisualizer", "Visualizer"] diff --git a/src/otx/api/usecases/exportable_code/visualizers/anomaly_visualizer.py b/src/otx/api/usecases/exportable_code/visualizers/anomaly_visualizer.py new file mode 100644 index 00000000000..2a1606cf582 --- /dev/null +++ b/src/otx/api/usecases/exportable_code/visualizers/anomaly_visualizer.py @@ -0,0 +1,74 @@ +"""Visualizer for results of anomaly task prediction.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Optional + +import cv2 +import numpy as np + +from otx.api.entities.annotation import AnnotationSceneEntity + +from .visualizer import Visualizer + + +class AnomalyVisualizer(Visualizer): + """Visualize the predicted output by drawing the annotations on the input image. + + Example: + >>> predictions = inference_model.predict(frame) + >>> annotation = prediction_converter.convert_to_annotation(predictions) + >>> output = visualizer.draw(frame, annotation.shape, annotation.get_labels()) + >>> visualizer.show(output) + """ + + def __init__( + self, + window_name: Optional[str] = None, + show_count: bool = False, + is_one_label: bool = False, + no_show: bool = False, + delay: Optional[int] = None, + ) -> None: + super().__init__(window_name, show_count, is_one_label, no_show, delay) + if not no_show: + cv2.namedWindow( + self.window_name, + cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO | cv2.WINDOW_GUI_EXPANDED, + ) + self.trackbar_name = "Opacity" + cv2.createTrackbar(self.trackbar_name, self.window_name, 0, 100, lambda x: x) + + @staticmethod + def to_heat_mask(mask: np.ndarray) -> np.ndarray: + """Create heat mask from saliency map. + + Args: + mask: saliency map + """ + heat_mask = cv2.normalize(mask, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX).astype(np.uint8) + return cv2.applyColorMap(heat_mask.astype(np.uint8), cv2.COLORMAP_JET) + + # pylint:disable=signature-differs + def draw( # type: ignore[override] + self, image: np.ndarray, annotation: AnnotationSceneEntity, meta: dict + ) -> np.ndarray: + """Draw annotations on the image. + + Args: + image: Input image + annotation: Annotations to be drawn on the input image + metadata: Metadata with saliency map + + Returns: + Output image with annotations. + """ + + heat_mask = self.to_heat_mask(1 - meta["anomaly_map"]) + alpha = cv2.getTrackbarPos(self.trackbar_name, self.window_name) / 100.0 + image = (1 - alpha) * image + alpha * heat_mask + image = cv2.cvtColor(image.astype(np.uint8), cv2.COLOR_RGB2BGR) + + return self.shape_drawer.draw(image, annotation, labels=[]) diff --git a/src/otx/api/usecases/exportable_code/visualizers/visualizer.py b/src/otx/api/usecases/exportable_code/visualizers/visualizer.py new file mode 100644 index 00000000000..aa35694a3fb --- /dev/null +++ b/src/otx/api/usecases/exportable_code/visualizers/visualizer.py @@ -0,0 +1,142 @@ +"""Visualizer for results of prediction.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +import time +from typing import Optional + +import cv2 +import numpy as np + +from otx.api.entities.annotation import AnnotationSceneEntity +from otx.api.utils.shape_drawer import ShapeDrawer +from otx.api.usecases.exportable_code.streamer import BaseStreamer + + +class IVisualizer(metaclass=abc.ABCMeta): + """Interface for converter.""" + + @abc.abstractmethod + def draw( + self, + image: np.ndarray, + annotation: AnnotationSceneEntity, + meta: dict, + ) -> np.ndarray: + """Draw annotations on the image. + + Args: + image: Input image + annotation: Annotations to be drawn on the input image + metadata: Metadata is needed to render + + Returns: + Output image with annotations. + """ + raise NotImplementedError + + @abc.abstractmethod + def show(self, image: np.ndarray) -> None: + """Show result image.""" + + raise NotImplementedError + + @abc.abstractmethod + def is_quit(self) -> bool: + """Check if user wishes to quit.""" + + raise NotImplementedError + + @abc.abstractmethod + def video_delay(self, elapsed_time: float, streamer: BaseStreamer) -> None: + """Check if video frames were inferenced faster than the original video FPS and delay visualizer if so. + + Args: + elapsed_time (float): Time spent on frame inference + streamer (BaseStreamer): Streamer object + """ + + raise NotImplementedError + + +class Visualizer(IVisualizer): + """Visualize the predicted output by drawing the annotations on the input image. + + Example: + >>> predictions = inference_model.predict(frame) + >>> annotation = prediction_converter.convert_to_annotation(predictions) + >>> output = visualizer.draw(frame, annotation.shape, annotation.get_labels()) + >>> visualizer.show(output) + """ + + def __init__( + self, + window_name: Optional[str] = None, + show_count: bool = False, + is_one_label: bool = False, + no_show: bool = False, + delay: Optional[int] = None, + output: Optional[str] = None, + ) -> None: + self.window_name = "Window" if window_name is None else window_name + self.shape_drawer = ShapeDrawer(show_count, is_one_label) + + self.delay = delay + self.no_show = no_show + if delay is None: + self.delay = 1 + self.output = output + + def draw( + self, + image: np.ndarray, + annotation: AnnotationSceneEntity, + meta: Optional[dict] = None, + ) -> np.ndarray: + """Draw annotations on the image. + + Args: + image: Input image + annotation: Annotations to be drawn on the input image + + Returns: + Output image with annotations. + """ + + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + + return self.shape_drawer.draw(image, annotation, labels=[]) + + def show(self, image: np.ndarray) -> None: + """Show result image. + + Args: + image (np.ndarray): Image to be shown. + """ + + if not self.no_show: + cv2.imshow(self.window_name, image) + + def is_quit(self) -> bool: + """Check user wish to quit.""" + if self.no_show: + return False + + return ord("q") == cv2.waitKey(self.delay) + + def video_delay(self, elapsed_time: float, streamer: BaseStreamer): + """Check if video frames were inferenced faster than the original video FPS and delay visualizer if so. + + Args: + elapsed_time (float): Time spent on frame inference + streamer (BaseStreamer): Streamer object + """ + if self.no_show: + return + if "VIDEO" in str(streamer.get_type()): + orig_frame_time = 1 / streamer.fps() + if elapsed_time < orig_frame_time: + time.sleep(orig_frame_time - elapsed_time) diff --git a/src/otx/api/usecases/reporting/__init__.py b/src/otx/api/usecases/reporting/__init__.py new file mode 100644 index 00000000000..2b3bb382475 --- /dev/null +++ b/src/otx/api/usecases/reporting/__init__.py @@ -0,0 +1,9 @@ +"""Training reporting.""" +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .callback import Callback +from .time_monitor_callback import TimeMonitorCallback + +__all__ = ["Callback", "TimeMonitorCallback"] diff --git a/src/otx/api/usecases/reporting/callback.py b/src/otx/api/usecases/reporting/callback.py new file mode 100644 index 00000000000..11b75974397 --- /dev/null +++ b/src/otx/api/usecases/reporting/callback.py @@ -0,0 +1,79 @@ +"""Callback module.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +class Callback: + """Abstract base class used to build new callbacks. + + Properties + params: dict. Training parameters + (eg. verbosity, batch size, number of epochs...). + model: instance of `keras.models.Model`. + Reference of the model being trained. + + The `logs` dictionary that callback methods + take as argument will contain keys for quantities relevant to + the current batch or epoch. + + Currently, the `.fit()` method of the `Sequential` model class + will include the following quantities in the `logs` that + it passes to its callbacks: + + on_epoch_end: logs include `acc` and `loss`, and + optionally include `val_loss` + (if validation is enabled in `fit`), and `val_acc` + (if validation and accuracy monitoring are enabled). + on_batch_begin: logs include `size`, + the number of samples in the current batch. + on_batch_end: logs include `loss`, and optionally `acc` + (if accuracy monitoring is enabled). + """ + + def set_params(self, params): + """Sets callback parameters.""" + # pylint: disable=W0201 + self.params = params + + def set_model(self, model): + """Sets callback model.""" + # pylint: disable=W0201 + self.model = model + + def on_epoch_begin(self, epoch, logs=None): + """It is called on epoch begin event.""" + + def on_epoch_end(self, epoch, logs=None): + """It is called on epoch end event.""" + + def on_batch_begin(self, batch, logs=None): + """It is called on batch begin event.""" + + def on_batch_end(self, batch, logs=None): + """It is called on batch end event.""" + + def on_train_begin(self, logs=None): + """It is called on train begin event.""" + + def on_train_end(self, logs=None): + """It is called on train end event.""" + + def on_train_batch_begin(self, batch, logs): + """It is called on train batch begin event.""" + + def on_train_batch_end(self, batch, logs): + """It is called on train batch end event.""" + + def on_test_begin(self, logs): + """It is called on test begin event.""" + + def on_test_end(self, logs): + """It is called on test end event.""" + + def on_test_batch_begin(self, batch, logs): + """It is called on test batch begin event.""" + + def on_test_batch_end(self, batch, logs): + """It is called on test batch end event.""" diff --git a/src/otx/api/usecases/reporting/time_monitor_callback.py b/src/otx/api/usecases/reporting/time_monitor_callback.py new file mode 100644 index 00000000000..8d032a6acf1 --- /dev/null +++ b/src/otx/api/usecases/reporting/time_monitor_callback.py @@ -0,0 +1,175 @@ +"""Time monitor callback module.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=too-many-instance-attributes,too-many-arguments + +import math +from otx.utils.logger import get_logger +import time +from copy import deepcopy +from typing import List + +import dill + +from otx.api.entities.train_parameters import ( + UpdateProgressCallback, + default_progress_callback, +) +from otx.api.usecases.reporting.callback import Callback + +logger = get_logger() + + +class TimeMonitorCallback(Callback): + """A callback to monitor the progress of training. + + Args: + num_epoch (int): Amount of epochs + num_train_steps (int): amount of training steps per epoch + num_val_steps (int): amount of validation steps per epoch + num_test_steps (int): amount of testing steps + epoch_history (int): Amount of previous epochs to calculate average epoch time over + step_history (int): Amount of previous steps to calculate average steps time over + update_progress_callback (UpdateProgressCallback): Callback to update progress + """ + + def __init__( + self, + num_epoch: int = 0, + num_train_steps: int = 0, + num_val_steps: int = 0, + num_test_steps: int = 0, + epoch_history: int = 5, + step_history: int = 50, + update_progress_callback: UpdateProgressCallback = default_progress_callback, + ): + + self.total_epochs = num_epoch + self.train_steps = num_train_steps + self.val_steps = num_val_steps + self.test_steps = num_test_steps + self.steps_per_epoch = self.train_steps + self.val_steps + self.total_steps = math.ceil(self.steps_per_epoch * self.total_epochs + num_test_steps) + self.current_step = 0 + self.current_epoch = 0 + + # Step time calculation + self.start_step_time = time.time() + self.past_step_duration: List[float] = [] + self.average_step = 0 + self.step_history = step_history + + # epoch time calculation + self.start_epoch_time = time.time() + self.past_epoch_duration: List[float] = [] + self.average_epoch = 0 + self.epoch_history = epoch_history + + # whether model is training flag + self.is_training = False + + self.update_progress_callback = update_progress_callback + + def __getstate__(self): + """Return state values to be pickled.""" + state = self.__dict__.copy() + # update_progress_callback is not always pickable object + # if it is not, replace it with default callback + if not dill.pickles(state["update_progress_callback"]): + state["update_progress_callback"] = default_progress_callback + return state + + def __deepcopy__(self, memo): + """Return deepcopy object.""" + + update_progress_callback = self.update_progress_callback + self.update_progress_callback = None + self.__dict__["__deepcopy__"] = None + + result = deepcopy(self, memo) + + self.__dict__.pop("__deepcopy__") + result.__dict__.pop("__deepcopy__") + result.update_progress_callback = update_progress_callback + self.update_progress_callback = update_progress_callback + + memo[id(self)] = result + return result + + def on_train_batch_begin(self, batch, logs=None): + """Set the value of current step and start the timer.""" + self.current_step += 1 + self.start_step_time = time.time() + + def on_train_batch_end(self, batch, logs=None): + """Compute average time taken to complete a step.""" + self.__calculate_average_step() + + def is_stalling(self) -> bool: + """Returns True if the training is stalling. + + Returns True if the current step has taken more than 30 seconds and + at least 20x more than the average step duration + """ + factor = 20 + min_abs_threshold = 30 # seconds + if self.is_training and self.current_step > 2: + step_duration = time.time() - self.start_step_time + if step_duration > min_abs_threshold and step_duration > factor * self.average_step: + logger.error( + f"Step {self.current_step} has taken {step_duration}s which is " + f">{min_abs_threshold}s and {factor} times " + f"more than the expected {self.average_step}s" + ) + return True + return False + + def __calculate_average_step(self): + """Compute average duration taken to complete a step.""" + self.past_step_duration.append(time.time() - self.start_step_time) + if len(self.past_step_duration) > self.step_history: + self.past_step_duration.remove(self.past_step_duration[0]) + self.average_step = sum(self.past_step_duration) / len(self.past_step_duration) + + def on_test_batch_begin(self, batch, logs): + """Set the number of current epoch and start the timer.""" + self.current_step += 1 + self.start_step_time = time.time() + + def on_test_batch_end(self, batch, logs): + """Compute average time taken to complete a step based on a running average of `step_history` steps.""" + self.__calculate_average_step() + + def on_train_begin(self, logs=None): + """Sets training to true.""" + self.is_training = True + + def on_train_end(self, logs=None): + """Handles early stopping when the total_steps is greater than the current_step.""" + # To handle cases where early stopping stops the task the progress will still be accurate + self.current_step = self.total_steps - self.test_steps + self.current_epoch = self.total_epochs + self.is_training = False + + def on_epoch_begin(self, epoch, logs=None): + """Set the number of current epoch and start the timer.""" + self.current_epoch = epoch + 1 + self.start_epoch_time = time.time() + + def on_epoch_end(self, epoch, logs=None): + """Computes the average time taken to complete an epoch based on a running average of `epoch_history` epochs.""" + self.past_epoch_duration.append(time.time() - self.start_epoch_time) + self._calculate_average_epoch() + self.update_progress_callback(self.get_progress()) + + def _calculate_average_epoch(self): + if len(self.past_epoch_duration) > self.epoch_history: + del self.past_epoch_duration[0] + self.average_epoch = sum(self.past_epoch_duration) / len(self.past_epoch_duration) + + def get_progress(self): + """Returns current progress as a percentage.""" + return (self.current_step / self.total_steps) * 100 diff --git a/src/otx/api/usecases/tasks/__init__.py b/src/otx/api/usecases/tasks/__init__.py new file mode 100644 index 00000000000..22a98697e6a --- /dev/null +++ b/src/otx/api/usecases/tasks/__init__.py @@ -0,0 +1,4 @@ +"""Task Interfaces based on Interfaces defined in the OTX API.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/api/usecases/tasks/exceptions.py b/src/otx/api/usecases/tasks/exceptions.py new file mode 100644 index 00000000000..0b2ffab50bf --- /dev/null +++ b/src/otx/api/usecases/tasks/exceptions.py @@ -0,0 +1,28 @@ +"""This module contains the exceptions for the tasks.""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +class UnrecoverableTaskException(Exception): + """Exception for when task is in an unrecoverable state.""" + + def __init__(self, message="Unrecoverable task exception"): + # pylint: disable=W0235 + super().__init__(message) + + +class OOMException(UnrecoverableTaskException): + """Exception for when task is out of memory.""" + + def __init__(self, message="Out of memory exception"): + super().__init__(message) + + +class TrainingStallException(UnrecoverableTaskException): + """Exception for when training should be stalled.""" + + def __init__(self, message="Training stalling exception"): + super().__init__(message) diff --git a/src/otx/api/usecases/tasks/image_computer_vision.py b/src/otx/api/usecases/tasks/image_computer_vision.py new file mode 100644 index 00000000000..c078438c42a --- /dev/null +++ b/src/otx/api/usecases/tasks/image_computer_vision.py @@ -0,0 +1,23 @@ +"""This module contains the base class for non-deep learning image-based tasks.""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc + +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask + + +class ImageComputerVisionTask(IInferenceTask, metaclass=abc.ABCMeta): + """A base class for a non-deep learning image-based tasks, which can only perform inference. + + This class inherits from ITask and IReporting. + + Example: + A cropping task + + >>> class CroppingTask(ImageComputerVisionTask): + ... pass + """ diff --git a/src/otx/api/usecases/tasks/image_deep_learning_task.py b/src/otx/api/usecases/tasks/image_deep_learning_task.py new file mode 100644 index 00000000000..d148112a845 --- /dev/null +++ b/src/otx/api/usecases/tasks/image_deep_learning_task.py @@ -0,0 +1,25 @@ +"""This module contains the base class for deep learning image-based tasks.""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc + +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.inference_interface import IInferenceTask +from otx.api.usecases.tasks.interfaces.training_interface import ITrainingTask + + +class ImageDeepLearningTask(IInferenceTask, ITrainingTask, IEvaluationTask, metaclass=abc.ABCMeta): + """A base class for a deep learning image-based tasks. + + This class inherits from ITask, ITraining, IComputesPerformance and IReporting. + + Example: + A YOLO detection task. + + >>> class YOLODetection(ImageDeepLearningTask): + ... pass + """ diff --git a/src/otx/api/usecases/tasks/interfaces/__init__.py b/src/otx/api/usecases/tasks/interfaces/__init__.py new file mode 100644 index 00000000000..44c912a3587 --- /dev/null +++ b/src/otx/api/usecases/tasks/interfaces/__init__.py @@ -0,0 +1,4 @@ +"""Interfaces for OTX Tasks.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/api/usecases/tasks/interfaces/deployment_interface.py b/src/otx/api/usecases/tasks/interfaces/deployment_interface.py new file mode 100644 index 00000000000..ee472f2f2b3 --- /dev/null +++ b/src/otx/api/usecases/tasks/interfaces/deployment_interface.py @@ -0,0 +1,22 @@ +"""This module contains the interface class for tasks that can deploy their models.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc + +from otx.api.entities.model import ModelEntity + + +class IDeploymentTask(metaclass=abc.ABCMeta): + """A base interface class for tasks which can deploy their models.""" + + @abc.abstractmethod + def deploy(self, output_model: ModelEntity) -> None: + """This method defines the interface for deploy. + + Args: + output_model: Output model + """ + raise NotImplementedError diff --git a/src/otx/api/usecases/tasks/interfaces/evaluate_interface.py b/src/otx/api/usecases/tasks/interfaces/evaluate_interface.py new file mode 100644 index 00000000000..662caaf7a82 --- /dev/null +++ b/src/otx/api/usecases/tasks/interfaces/evaluate_interface.py @@ -0,0 +1,31 @@ +"""This module contains the interface class for tasks that can compute performance.""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +from typing import Optional + +from otx.api.entities.resultset import ResultSetEntity + + +class IEvaluationTask(metaclass=abc.ABCMeta): + """A base interface class for tasks which can compute performance on a resultset.""" + + @abc.abstractmethod + def evaluate(self, output_resultset: ResultSetEntity, evaluation_metric: Optional[str] = None): + """Compute performance metrics for a given set of results. + + The task may use at its discretion the most appropriate metrics for the evaluation (for instance, + average precision for classification, DICE for segmentation, etc). + The performance will be stored directly to output_resultset.performance + + Args: + output_resultset (ResultSetEntity): The set of results which must be + evaluated. + evaluation_metric (Optional[str]): the evaluation metric used to compute the + performance + """ + raise NotImplementedError diff --git a/src/otx/api/usecases/tasks/interfaces/explain_interface.py b/src/otx/api/usecases/tasks/interfaces/explain_interface.py new file mode 100644 index 00000000000..212c9be939d --- /dev/null +++ b/src/otx/api/usecases/tasks/interfaces/explain_interface.py @@ -0,0 +1,32 @@ +"""This module contains the interface class for tasks.""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters + + +class IExplainTask(metaclass=abc.ABCMeta): + """A base interface for explain task.""" + + @abc.abstractmethod + def explain( + self, + dataset: DatasetEntity, + explain_parameters: ExplainParameters, + ) -> DatasetEntity: + """This is the method that is called upon explanation. + + Args: + dataset: The input dataset to perform the explain on. + explain_parameters: The parameters to use for the explain. + + Returns: + The results of the explanation. + """ + raise NotImplementedError diff --git a/src/otx/api/usecases/tasks/interfaces/export_interface.py b/src/otx/api/usecases/tasks/interfaces/export_interface.py new file mode 100644 index 00000000000..dca82c48443 --- /dev/null +++ b/src/otx/api/usecases/tasks/interfaces/export_interface.py @@ -0,0 +1,39 @@ +"""This module contains the interface class for tasks that can export their models.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +from enum import Enum, auto + +from otx.api.entities.model import ModelEntity, ModelPrecision + + +class ExportType(Enum): + """Represent the type of export format available through this interface.""" + + OPENVINO = auto() + ONNX = auto() + + +class IExportTask(metaclass=abc.ABCMeta): + """A base interface class for tasks which can export their models.""" + + @abc.abstractmethod + def export( + self, + export_type: ExportType, + output_model: ModelEntity, + precision: ModelPrecision, + dump_features: bool, + ): + """This method defines the interface for export. + + Args: + export_type (ExportType): The type of optimization. + output_model (ModelEntity): The output model entity. + precision (ModelPrecision): The precision of the ouptut model. + dump_features (bool): Flag to return "feature_vector" and "saliency_map". + """ + raise NotImplementedError diff --git a/src/otx/api/usecases/tasks/interfaces/inference_interface.py b/src/otx/api/usecases/tasks/interfaces/inference_interface.py new file mode 100644 index 00000000000..02df140385f --- /dev/null +++ b/src/otx/api/usecases/tasks/interfaces/inference_interface.py @@ -0,0 +1,61 @@ +"""This module contains the interface class for tasks.""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +from typing import Dict + +import numpy as np + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters + + +class IInferenceTask(metaclass=abc.ABCMeta): + """A base interface class for a task.""" + + @abc.abstractmethod + def infer( + self, + dataset: DatasetEntity, + inference_parameters: InferenceParameters, + ) -> DatasetEntity: + """This is the method that is called upon inference. + + This happens when the user wants to analyse a sample + or multiple samples need to be analysed. + + Args: + dataset: The input dataset to perform the analysis on. + inference_parameters: The parameters to use for the + analysis. + + Returns: + The results of the analysis. + """ + raise NotImplementedError + + +class IRawInference(metaclass=abc.ABCMeta): + """A base interface class for raw inference tasks.""" + + @abc.abstractmethod + def raw_infer( + self, + input_tensors: Dict[str, np.ndarray], + output_tensors: Dict[str, np.ndarray], + ): + """This is the method that is called to run a neural network over a set of tensors. + + This method takes as input/output the tensors which are directly fed to the neural network, + and does not include any additional pre- and post-processing of the inputs and outputs. + + Args: + input_tensors: Dictionary containing the input tensors. + output_tensors: Dictionary to be filled by the task with the + output tensors. + """ + raise NotImplementedError diff --git a/src/otx/api/usecases/tasks/interfaces/optimization_interface.py b/src/otx/api/usecases/tasks/interfaces/optimization_interface.py new file mode 100644 index 00000000000..1952cfafa92 --- /dev/null +++ b/src/otx/api/usecases/tasks/interfaces/optimization_interface.py @@ -0,0 +1,44 @@ +"""This module contains the interface class for tasks that can optimize their models.""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc +from enum import Enum, auto +from typing import Optional + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.optimization_parameters import OptimizationParameters + + +class OptimizationType(Enum): + """This class enumerates the OPENVINO optimization types.""" + + POT = auto() + NNCF = auto() + + +class IOptimizationTask(metaclass=abc.ABCMeta): + """A base interface class for tasks which can optimize their models.""" + + @abc.abstractmethod + def optimize( + self, + optimization_type: OptimizationType, + dataset: DatasetEntity, + output_model: ModelEntity, + optimization_parameters: Optional[OptimizationParameters], + ): + """This method defines the interface for optimization. + + Args: + optimization_type: The type of optimization + dataset: Optional dataset which may be used as part of the + optimization process + output_model: Output model + optimization_parameters: Additional optimization parameters + """ + raise NotImplementedError diff --git a/src/otx/api/usecases/tasks/interfaces/training_interface.py b/src/otx/api/usecases/tasks/interfaces/training_interface.py new file mode 100644 index 00000000000..7f642566e8f --- /dev/null +++ b/src/otx/api/usecases/tasks/interfaces/training_interface.py @@ -0,0 +1,61 @@ +"""This module contains the interface class for tasks that can perform training.""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.train_parameters import TrainParameters + + +class ITrainingTask(metaclass=abc.ABCMeta): + """A base interface class for tasks which can perform training.""" + + @abc.abstractmethod + def save_model(self, output_model: ModelEntity): + """Save the model currently loaded by the task to `output_model`. + + This method is for instance used to save the pre-trained weights before training + when the task has been initialised with pre-trained weights rather than an existing model. + + Args: + output_model (ModelEntity): Output model where the weights should be stored + """ + raise NotImplementedError + + @abc.abstractmethod + def train( + self, + dataset: DatasetEntity, + output_model: ModelEntity, + train_parameters: TrainParameters, + ): + """Train a new model using the model currently loaded by the task. + + If training was successful, the new model should be used for subsequent calls (e.g. `optimize` or `infer`). + + The new model weights should be saved in the object `output_model`. + + The task has two choices: + + - Set the output model weights, if the task was able to improve itself (according to own measures) + - Set the model state as failed if it failed to improve itself (according to own measures) + + Args: + dataset (DatasetEntity): Dataset containing the training and validation splits to use for training. + output_model (ModelEntity): Output model where the weights should be stored + train_parameters (TrainParameters): Training parameters + """ + raise NotImplementedError + + @abc.abstractmethod + def cancel_training(self): + """Cancels the currently running training process. + + If training is not running, do nothing. + """ + raise NotImplementedError diff --git a/src/otx/api/usecases/tasks/interfaces/unload_interface.py b/src/otx/api/usecases/tasks/interfaces/unload_interface.py new file mode 100644 index 00000000000..2a5bf08a9dc --- /dev/null +++ b/src/otx/api/usecases/tasks/interfaces/unload_interface.py @@ -0,0 +1,30 @@ +"""Unload Interface. + +This module contains the interface class for tasks to be notified when the task does not need to be loaded anymore. +""" + + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import abc + + +class IUnload(metaclass=abc.ABCMeta): + """Interface to provide unload functionality. + + This interface can be implemented by a task, if the task wants to be notified when + the task is not needed by the pipeline anymore. + This allows to clear GPU and system memory resources for example. + """ + + @abc.abstractmethod + def unload(self): + """Unload task. + + Unload any resources which have been used by the task. + + It is acceptable to restart the server as a last resort strategy if unloading the resources is too difficult. + """ + raise NotImplementedError diff --git a/src/otx/api/utils/__init__.py b/src/otx/api/utils/__init__.py new file mode 100644 index 00000000000..6ed562033a9 --- /dev/null +++ b/src/otx/api/utils/__init__.py @@ -0,0 +1,5 @@ +"""Utilities for OTX API.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/api/utils/anomaly_utils.py b/src/otx/api/utils/anomaly_utils.py new file mode 100644 index 00000000000..68587ab9d83 --- /dev/null +++ b/src/otx/api/utils/anomaly_utils.py @@ -0,0 +1,83 @@ +"""Detection Utils.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, List + +import cv2 +import numpy as np + +from otx.api.entities.annotation import Annotation +from otx.api.entities.label import LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle + + +def create_detection_annotation_from_anomaly_heatmap( + hard_prediction: np.ndarray, + soft_prediction: np.ndarray, + label_map: Dict[int, LabelEntity], +) -> List[Annotation]: + """Create box annotation from the soft predictions. + + Args: + hard_prediction: hard prediction containing the final label + index per pixel. + soft_prediction: soft prediction with shape + label_map: dictionary mapping labels to an index. It is assumed + that the first item in the dictionary corresponds to the + background label and will therefore be ignored. + + Returns: + List of annotations. + """ + # pylint: disable=too-many-locals + if hard_prediction.ndim == 3 and hard_prediction.shape[0] == 1: + hard_prediction = hard_prediction.squeeze().astype(np.uint8) + + if soft_prediction.ndim == 3 and soft_prediction.shape[0] == 1: + soft_prediction = soft_prediction.squeeze() + + image_h, image_w = hard_prediction.shape[:2] + + annotations: List[Annotation] = [] + for label_index, label in label_map.items(): + # Skip the normal label. + if label_index == 0: + continue + + # cv2.connectedComponentsWithStats returns num_labels, labels, coordinates + # and centroids. This script only needs the coordinates. + _, connected_components, coordinates, _ = cv2.connectedComponentsWithStats(hard_prediction) + + for i, coordinate in enumerate(coordinates): + # First row of the coordinates is always backround, + # so should be ignored. + if i == 0: + continue + + # Last column of the coordinates is the area of the connected component. + # It could therefore be ignored. + comp_x, comp_y, comp_w, comp_h, _ = coordinate + + # Compute the probability of each connected-component + component_hard_prediction = (connected_components == i).astype(np.uint8) + component_soft_prediction = cv2.bitwise_and( + soft_prediction, soft_prediction, mask=component_hard_prediction + ) + + # NOTE: Find the best approach to calculate the probability + probability = component_soft_prediction.reshape(-1).max() + + # NOTE: NMS could be needed here. + + # Create the annotation based on the box shape and the probability. + shape = Rectangle( + x1=comp_x / image_w, + y1=comp_y / image_h, + x2=(comp_x + comp_w) / image_w, + y2=(comp_y + comp_h) / image_h, + ) + annotations.append(Annotation(shape=shape, labels=[ScoredLabel(label, float(probability))])) + return annotations diff --git a/src/otx/api/utils/argument_checks.py b/src/otx/api/utils/argument_checks.py new file mode 100644 index 00000000000..e539b56b91f --- /dev/null +++ b/src/otx/api/utils/argument_checks.py @@ -0,0 +1,461 @@ +"""Utils for checking functions and methods arguments.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import inspect +import itertools +import typing +from abc import ABC, abstractmethod +from collections.abc import Sequence +from functools import wraps +from os.path import exists, splitext + +import yaml +from numpy import floating +from omegaconf import DictConfig + +IMAGE_FILE_EXTENSIONS = [ + ".bmp", + ".dib", + ".jpeg", + ".jpg", + ".jpe", + ".jp2", + ".png", + ".webp", + ".pbm", + ".pgm", + ".ppm", + ".pxm", + ".pnm", + ".sr", + ".ras", + ".tiff", + ".tif", + ".exr", + ".hdr", + ".pic", +] + + +def get_bases(parameter) -> set: + """Function to get set of all base classes of parameter.""" + + def __get_bases(parameter_type): + return [parameter_type.__name__] + list( + itertools.chain.from_iterable(__get_bases(t1) for t1 in parameter_type.__bases__) + ) + + return set(__get_bases(type(parameter))) + + +def get_parameter_repr(parameter) -> str: + """Function to get parameter representation.""" + try: + parameter_str = repr(parameter) + # pylint: disable=broad-except + except Exception: + parameter_str = "" + return parameter_str + + +def raise_value_error_if_parameter_has_unexpected_type(parameter, parameter_name, expected_type): + """Function raises ValueError exception if parameter has unexpected type.""" + if isinstance(expected_type, typing.ForwardRef): + expected_type = expected_type.__forward_arg__ + if isinstance(expected_type, str): + parameter_types = get_bases(parameter) + if not any(t == expected_type for t in parameter_types): + parameter_str = get_parameter_repr(parameter) + raise ValueError( + f"Unexpected type of '{parameter_name}' parameter, expected: {expected_type}, " + f"actual value: {parameter_str}" + ) + return + if expected_type == float: + expected_type = (int, float, floating) + if not isinstance(parameter, expected_type): + parameter_type = type(parameter) + parameter_str = get_parameter_repr(parameter) + raise ValueError( + f"Unexpected type of '{parameter_name}' parameter, expected: {expected_type}, actual: {parameter_type}, " + f"actual value: {parameter_str}" + ) + + +def check_nested_elements_type(iterable, parameter_name, expected_type): + """Function raises ValueError exception if one of elements in collection has unexpected type.""" + for element in iterable: + check_parameter_type( + parameter=element, + parameter_name=f"nested {parameter_name}", + expected_type=expected_type, + ) + + +def check_dictionary_keys_values_type(parameter, parameter_name, expected_key_class, expected_value_class): + """Function raises ValueError exception if dictionary key or value has unexpected type.""" + for key, value in parameter.items(): + check_parameter_type( + parameter=key, + parameter_name=f"key in {parameter_name}", + expected_type=expected_key_class, + ) + check_parameter_type( + parameter=value, + parameter_name=f"value in {parameter_name}", + expected_type=expected_value_class, + ) + + +def check_nested_classes_parameters(parameter, parameter_name, origin_class, nested_elements_class): + """Function to check type of parameters with nested elements.""" + # Checking origin class + raise_value_error_if_parameter_has_unexpected_type( + parameter=parameter, parameter_name=parameter_name, expected_type=origin_class + ) + # Checking nested elements + if origin_class == dict: + if len(nested_elements_class) != 2: + raise TypeError("length of nested expected types for dictionary should be equal to 2") + key, value = nested_elements_class + check_dictionary_keys_values_type( + parameter=parameter, + parameter_name=parameter_name, + expected_key_class=key, + expected_value_class=value, + ) + if origin_class in [list, set, tuple, Sequence]: + if origin_class == tuple: + tuple_length = len(nested_elements_class) + if tuple_length > 2: + raise NotImplementedError("length of nested expected types for Tuple should not exceed 2") + if tuple_length == 2: + if nested_elements_class[1] != Ellipsis: + raise NotImplementedError("expected homogeneous tuple annotation") + nested_elements_class = nested_elements_class[0] + else: + if len(nested_elements_class) != 1: + raise TypeError("length of nested expected types for Sequence should be equal to 1") + check_nested_elements_type( + iterable=parameter, + parameter_name=parameter_name, + expected_type=nested_elements_class, + ) + + +def check_parameter_type(parameter, parameter_name, expected_type): + """Function extracts nested expected types and raises ValueError exception if parameter has unexpected type.""" + # pylint: disable=W0212 + if expected_type in [typing.Any, inspect._empty]: # type: ignore + return + if not isinstance(expected_type, typing._GenericAlias): # type: ignore + raise_value_error_if_parameter_has_unexpected_type( + parameter=parameter, + parameter_name=parameter_name, + expected_type=expected_type, + ) + return + expected_type_dict = expected_type.__dict__ + origin_class = expected_type_dict.get("__origin__") + nested_elements_class = expected_type_dict.get("__args__") + # Union type with nested elements check + if origin_class == typing.Union: + expected_args = expected_type_dict.get("__args__") + checks_counter = 0 + errors_counter = 0 + for expected_arg in expected_args: + try: + checks_counter += 1 + check_parameter_type(parameter, parameter_name, expected_arg) + except ValueError: + errors_counter += 1 + if errors_counter == checks_counter: + actual_type = type(parameter) + raise ValueError( + f"Unexpected type of '{parameter_name}' parameter, expected: {expected_args}, " + f"actual type: {actual_type}, actual value: {parameter}" + ) + # Checking parameters with nested elements + elif issubclass(origin_class, typing.Iterable): + check_nested_classes_parameters( + parameter=parameter, + parameter_name=parameter_name, + origin_class=origin_class, + nested_elements_class=nested_elements_class, + ) + + +def check_input_parameters_type(custom_checks: typing.Optional[dict] = None): + """Decorator to check input parameters type. + + Args: + custom_checks: dictionary where key - name of parameter and + value - custom check class + """ + if custom_checks is None: + custom_checks = {} + + def _check_input_parameters_type(function): + @wraps(function) + def validate(*args, **kwargs): + # Forming expected types dictionary + signature = inspect.signature(function) + expected_types_map = signature.parameters + if len(expected_types_map) < len(args): + raise TypeError("Too many positional arguments") + # Forming input parameters dictionary + input_parameters_values_map = dict(zip(signature.parameters.keys(), args)) + for key, value in kwargs.items(): + if key in input_parameters_values_map: + raise TypeError(f"Duplication of the parameter {key} -- both in args and kwargs") + input_parameters_values_map[key] = value + # Checking input parameters type + for parameter_name in expected_types_map: + parameter = input_parameters_values_map.get(parameter_name) + if parameter_name not in input_parameters_values_map: + default_value = expected_types_map.get(parameter_name).default + # pylint: disable=protected-access + if default_value != inspect._empty: # type: ignore + parameter = default_value + if parameter_name in custom_checks: + custom_check = custom_checks[parameter_name] + if custom_check is None: + continue + custom_check(parameter, parameter_name).check() + else: + check_parameter_type( + parameter=parameter, + parameter_name=parameter_name, + expected_type=expected_types_map.get(parameter_name).annotation, + ) + return function(**input_parameters_values_map) + + return validate + + return _check_input_parameters_type + + +def check_file_extension(file_path: str, file_path_name: str, expected_extensions: list): + """Function raises ValueError exception if file has unexpected extension.""" + file_extension = splitext(file_path)[1].lower() + if file_extension not in expected_extensions: + raise ValueError( + f"Unexpected extension of {file_path_name} file. expected: {expected_extensions} actual: {file_extension}" + ) + + +def check_that_null_character_absents_in_string(parameter: str, parameter_name: str): + r"""Function raises ValueError exception if null character: '\0' is specified in path to file.""" + if "\0" in parameter: + raise ValueError(f"null char \\0 is specified in {parameter_name}: {parameter}") + + +def check_that_file_exists(file_path: str, file_path_name: str): + """Function raises ValueError exception if file not exists.""" + if not exists(file_path): + raise ValueError(f"File {file_path} specified in '{file_path_name}' parameter not exists") + + +def check_that_parameter_is_not_empty(parameter, parameter_name): + """Function raises ValueError if parameter is empty.""" + if not parameter: + raise ValueError(f"parameter {parameter_name} is empty") + + +def check_that_all_characters_printable(parameter, parameter_name, allow_crlf=False): + """Function raises ValueError if one of string-parameter characters is not printable.""" + if not allow_crlf: + all_characters_printable = all(c.isprintable() for c in parameter) + else: + all_characters_printable = all((c.isprintable() or c == "\n" or c == "\r") for c in parameter) + if not all_characters_printable: + raise ValueError(rf"parameter {parameter_name} has not printable symbols: {parameter}") + + +def check_is_parameter_like_dataset(parameter, parameter_name): + """Checks if the parameter is like a dataset. + + Function raises ValueError exception if parameter does not have __len__, __getitem__ and get_subset attributes of + DataSet-type object. + """ + for expected_attribute in ("__len__", "__getitem__", "get_subset"): + if not hasattr(parameter, expected_attribute): + parameter_type = type(parameter) + raise ValueError( + f"parameter '{parameter_name}' is not like DatasetEntity, actual type: {parameter_type} which does " + f"not have expected '{expected_attribute}' dataset attribute" + ) + + +def check_file_path(parameter, parameter_name, expected_file_extensions): + """Function to check file path string objects.""" + raise_value_error_if_parameter_has_unexpected_type( + parameter=parameter, + parameter_name=parameter_name, + expected_type=str, + ) + check_that_parameter_is_not_empty(parameter=parameter, parameter_name=parameter_name) + check_file_extension( + file_path=parameter, + file_path_name=parameter_name, + expected_extensions=expected_file_extensions, + ) + check_that_null_character_absents_in_string(parameter=parameter, parameter_name=parameter_name) + check_that_all_characters_printable(parameter=parameter, parameter_name=parameter_name) + check_that_file_exists(file_path=parameter, file_path_name=parameter_name) + + +def check_directory_path(parameter, parameter_name): + """Function to check directory path string objects.""" + raise_value_error_if_parameter_has_unexpected_type( + parameter=parameter, + parameter_name=parameter_name, + expected_type=str, + ) + check_that_parameter_is_not_empty(parameter=parameter, parameter_name=parameter_name) + check_that_null_character_absents_in_string(parameter=parameter, parameter_name=parameter_name) + check_that_all_characters_printable(parameter=parameter, parameter_name=parameter_name) + + +class BaseInputArgumentChecker(ABC): + """Abstract class to check input arguments.""" + + @abstractmethod + def check(self): + """Abstract method to check input arguments.""" + raise NotImplementedError("The check is not implemented") + + +class InputConfigCheck(BaseInputArgumentChecker): + """Class to check input config_parameters.""" + + def __init__(self, parameter, parameter_name): + self.parameter = parameter + self.parameter_name = parameter_name + + def check(self): + """Method raises ValueError exception if "input_config" parameter is not equal to expected.""" + raise_value_error_if_parameter_has_unexpected_type( + parameter=self.parameter, + parameter_name=self.parameter_name, + expected_type=(str, DictConfig, dict), + ) + check_that_parameter_is_not_empty(parameter=self.parameter, parameter_name=self.parameter_name) + if isinstance(self.parameter, str): + check_that_null_character_absents_in_string(parameter=self.parameter, parameter_name=self.parameter_name) + # yaml-format string is specified + if isinstance(yaml.safe_load(self.parameter), dict): + check_that_all_characters_printable( + parameter=self.parameter, + parameter_name=self.parameter_name, + allow_crlf=True, + ) + # Path to file is specified + else: + check_file_extension( + file_path=self.parameter, + file_path_name=self.parameter_name, + expected_extensions=[".yaml"], + ) + check_that_all_characters_printable(parameter=self.parameter, parameter_name=self.parameter_name) + check_that_file_exists(file_path=self.parameter, file_path_name=self.parameter_name) + + +class FilePathCheck(BaseInputArgumentChecker): + """Class to check file_path-like parameters.""" + + def __init__(self, parameter, parameter_name, expected_file_extension): + self.parameter = parameter + self.parameter_name = parameter_name + self.expected_file_extensions = expected_file_extension + + def check(self): + """Method raises ValueError exception if file path parameter is not equal to expected.""" + check_file_path(self.parameter, self.parameter_name, self.expected_file_extensions) + + +class OptionalFilePathCheck(BaseInputArgumentChecker): + """Class to check optional file_path-like parameters.""" + + def __init__(self, parameter, parameter_name, expected_file_extension): + self.parameter = parameter + self.parameter_name = parameter_name + self.expected_file_extensions = expected_file_extension + + def check(self): + """Method raises ValueError exception if file path parameter is not equal to expected.""" + if self.parameter is not None: + check_file_path(self.parameter, self.parameter_name, self.expected_file_extensions) + + +class DatasetParamTypeCheck(BaseInputArgumentChecker): + """Class to check DatasetEntity-type parameters.""" + + def __init__(self, parameter, parameter_name): + self.parameter = parameter + self.parameter_name = parameter_name + + def check(self): + """Method raises ValueError exception if parameter is not equal to Dataset.""" + check_is_parameter_like_dataset(parameter=self.parameter, parameter_name=self.parameter_name) + + +class OptionalImageFilePathCheck(OptionalFilePathCheck): + """Class to check optional image file path parameters.""" + + def __init__(self, parameter, parameter_name): + super().__init__( + parameter=parameter, + parameter_name=parameter_name, + expected_file_extension=IMAGE_FILE_EXTENSIONS, + ) + + +class YamlFilePathCheck(FilePathCheck): + """Class to check optional yaml file path parameters.""" + + def __init__(self, parameter, parameter_name): + super().__init__( + parameter=parameter, + parameter_name=parameter_name, + expected_file_extension=[".yaml"], + ) + + +class JsonFilePathCheck(FilePathCheck): + """Class to check optional yaml file path parameters.""" + + def __init__(self, parameter, parameter_name): + super().__init__( + parameter=parameter, + parameter_name=parameter_name, + expected_file_extension=[".json"], + ) + + +class DirectoryPathCheck(BaseInputArgumentChecker): + """Class to check directory path parameters.""" + + def __init__(self, parameter, parameter_name): + self.parameter = parameter + self.parameter_name = parameter_name + + def check(self): + """Method raises ValueError exception if directory path parameter is not equal to expected.""" + check_directory_path(parameter=self.parameter, parameter_name=self.parameter_name) + + +class OptionalDirectoryPathCheck(BaseInputArgumentChecker): + """Class to check optional directory path parameters.""" + + def __init__(self, parameter, parameter_name): + self.parameter = parameter + self.parameter_name = parameter_name + + def check(self): + """Method raises ValueError exception if directory path parameter is not equal to expected.""" + if self.parameter is not None: + check_directory_path(parameter=self.parameter, parameter_name=self.parameter_name) diff --git a/src/otx/api/utils/async_pipeline.py b/src/otx/api/utils/async_pipeline.py new file mode 100644 index 00000000000..a15f7ae9a08 --- /dev/null +++ b/src/otx/api/utils/async_pipeline.py @@ -0,0 +1,49 @@ +"""OTX AsyncPipeline of OTX Detection.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. +import copy +from time import perf_counter + +from openvino.model_api.pipelines import AsyncPipeline + + +class OTXDetectionAsyncPipeline(AsyncPipeline): + """OTX AsyncPipeline of OTX Detection.""" + + def get_result(self, id): # pylint: disable=redefined-builtin + """Get result of inference by index. + + Args: + id (int): index of inference + + Returns: + result (tuple): tuple of inference result and meta information + """ + result = self.get_raw_result(id) + if result: + raw_result, meta, preprocess_meta, infer_start_time = result + self.inference_metrics.update(infer_start_time) + + postprocessing_start_time = perf_counter() + result = self.model.postprocess(raw_result, preprocess_meta), {**meta, **preprocess_meta} + self.postprocess_metrics.update(postprocessing_start_time) + features = (None, None) + if "feature_vector" in raw_result or "saliency_map" in raw_result: + features = ( + copy.deepcopy(raw_result["feature_vector"].reshape(-1)), + copy.deepcopy(raw_result["saliency_map"][0]), + ) + return *result, features + return None diff --git a/src/otx/api/utils/dataset_utils.py b/src/otx/api/utils/dataset_utils.py new file mode 100644 index 00000000000..7f0cddd6a74 --- /dev/null +++ b/src/otx/api/utils/dataset_utils.py @@ -0,0 +1,274 @@ +"""Dataset utils.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from typing import List, Optional, Tuple, Union + +import numpy as np + +from otx.api.entities.annotation import AnnotationSceneEntity +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import LabelEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.result_media import ResultMediaEntity +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.utils.vis_utils import get_actmap + + +def get_fully_annotated_idx(dataset: DatasetEntity) -> List[int]: + """Find the indices of the fully annotated items in a dataset. + + A dataset item is fully annotated if local annotations are available, or if the item has the `normal` label. + + Args: + dataset (DatasetEntity): Dataset that may contain both partially and fully annotated items. + + Returns: + List[int]: List of indices of the fully annotated dataset items. + """ + local_idx = [] + for idx, gt_item in enumerate(dataset): + local_annotations = [ + annotation for annotation in gt_item.get_annotations() if not Rectangle.is_full_box(annotation.shape) + ] + if not any(label.is_anomalous for label in gt_item.get_shapes_labels()) or len(local_annotations) > 0: + local_idx.append(idx) + return local_idx + + +def get_local_subset( + dataset: DatasetEntity, + fully_annotated_idx: Optional[List[int]] = None, + include_normal: bool = True, +) -> DatasetEntity: + """Extract a subset that contains only those dataset items that have local annotations. + + Args: + dataset (DatasetEntity): Dataset from which we want to extract the locally annotated subset. + fully_annotated_idx (Optional[List[int]]): The indices of the fully annotated dataset items. If not provided, + the function will compute the indices before creating the subset. + include_normal (bool): When true, global normal annotations will be included in the local dataset. + + Returns: + DatasetEntity: Output dataset with only local annotations + """ + local_items = [] + if fully_annotated_idx is None: + fully_annotated_idx = get_fully_annotated_idx(dataset) + for idx in fully_annotated_idx: + item = dataset[idx] + + local_annotations = [ + annotation for annotation in item.get_annotations() if not Rectangle.is_full_box(annotation.shape) + ] + # annotations with the normal label are considered local + if include_normal: + local_annotations.extend( + [ + annotation + for annotation in item.get_annotations() + if not any(label.label.is_anomalous for label in annotation.get_labels()) + ] + ) + local_items.append( + DatasetItemEntity( + media=item.media, + annotation_scene=AnnotationSceneEntity( + local_annotations, + kind=item.annotation_scene.kind, + ), + metadata=item.get_metadata(), + subset=item.subset, + roi=item.roi, + ignored_labels=item.ignored_labels, + ) + ) + return DatasetEntity(local_items, purpose=dataset.purpose) + + +def get_global_subset(dataset: DatasetEntity) -> DatasetEntity: + """Extract a subset that contains only the global annotations. + + Args: + dataset (DatasetEntity): Dataset from which we want to extract the globally annotated subset. + + Returns: + DatasetEntity: Output dataset with only global annotations + """ + global_items = [] + for item in dataset: + global_annotations = [ + annotation for annotation in item.get_annotations() if Rectangle.is_full_box(annotation.shape) + ] + global_items.append( + DatasetItemEntity( + media=item.media, + annotation_scene=AnnotationSceneEntity(global_annotations, kind=item.annotation_scene.kind), + metadata=item.get_metadata(), + subset=item.subset, + roi=item.roi, + ignored_labels=item.ignored_labels, + ) + ) + return DatasetEntity(global_items, purpose=dataset.purpose) + + +def split_local_global_dataset( + dataset: DatasetEntity, +) -> Tuple[DatasetEntity, DatasetEntity]: + """Split a dataset into the globally and locally annotated subsets. + + Args: + dataset (DatasetEntity): Input dataset + + Returns: + Tuple[DatasetEntity, DatasetEntity]: Tuple of the globally and locally annotated subsets. + """ + global_dataset = get_global_subset(dataset) + local_dataset = get_local_subset(dataset) + return global_dataset, local_dataset + + +def split_local_global_resultset( + resultset: ResultSetEntity, +) -> Tuple[ResultSetEntity, ResultSetEntity]: + """Split a resultset into the globally and locally annotated resultsets. + + Args: + resultset (ResultSetEntity): Input resultset + + Returns: + Tuple[ResultSetEntity, ResultSetEntity]: Tuple of the globally and locally annotated resultsets. + """ + global_gt_dataset = get_global_subset(resultset.ground_truth_dataset) + local_gt_dataset = get_local_subset(resultset.ground_truth_dataset, include_normal=False) + local_idx = get_fully_annotated_idx(resultset.ground_truth_dataset) + global_pred_dataset = get_global_subset(resultset.prediction_dataset) + local_pred_dataset = get_local_subset(resultset.prediction_dataset, local_idx, include_normal=False) + + global_resultset = ResultSetEntity( + model=resultset.model, + ground_truth_dataset=global_gt_dataset, + prediction_dataset=global_pred_dataset, + purpose=resultset.purpose, + ) + local_resultset = ResultSetEntity( + model=resultset.model, + ground_truth_dataset=local_gt_dataset, + prediction_dataset=local_pred_dataset, + purpose=resultset.purpose, + ) + return global_resultset, local_resultset + + +def contains_anomalous_images(dataset: DatasetEntity) -> bool: + """Check if a dataset contains any items with the anomalous label. + + Args: + dataset (DatasetEntity): Dataset to check for anomalous items. + + Returns: + bool: True if the dataset contains anomalous items, False otherwise. + """ + for item in dataset: + labels = item.get_shapes_labels() + if any(label.is_anomalous for label in labels): + return True + return False + + +# pylint: disable-msg=too-many-locals +def add_saliency_maps_to_dataset_item( + dataset_item: DatasetItemEntity, + saliency_map: Union[List[Optional[np.ndarray]], np.ndarray], + model: Optional[ModelEntity], + labels: List[LabelEntity], + predicted_scored_labels: Optional[List[ScoredLabel]] = None, + explain_predicted_classes: bool = True, + process_saliency_maps: bool = False, +): + """Add saliency maps (2D array for class-agnostic saliency map, + 3D array or list or 2D arrays for class-wise saliency maps) to a single dataset item.""" + if isinstance(saliency_map, list): + class_wise_saliency_map = True + elif isinstance(saliency_map, np.ndarray): + if saliency_map.ndim == 2: + class_wise_saliency_map = False + elif saliency_map.ndim == 3: + class_wise_saliency_map = True + else: + raise ValueError(f"Saliency map has to be 2 or 3-dimensional array, " f"but got {saliency_map.ndim} dims.") + else: + raise TypeError("Check saliency_map, it has to be list or np.ndarray.") + + if class_wise_saliency_map: + # Multiple saliency maps per image (class-wise saliency map), support e.g. ReciproCAM + if explain_predicted_classes: + # Explain only predicted classes + if predicted_scored_labels is None: + raise ValueError("To explain only predictions, list of predicted scored labels have to be provided.") + + explain_targets = set() + for scored_label in predicted_scored_labels: + if scored_label.label is not None: # Check for an empty label + explain_targets.add(scored_label.label) + else: + # Explain all classes + explain_targets = set(labels) + + for class_id, class_wise_saliency_map in enumerate(saliency_map): + label = labels[class_id] + if class_wise_saliency_map is not None and label in explain_targets: + if process_saliency_maps: + class_wise_saliency_map = get_actmap( + class_wise_saliency_map, (dataset_item.width, dataset_item.height) + ) + saliency_media = ResultMediaEntity( + name=label.name, + type="saliency_map", + annotation_scene=dataset_item.annotation_scene, + numpy=class_wise_saliency_map, + roi=dataset_item.roi, + label=label, + ) + dataset_item.append_metadata_item(saliency_media, model=model) + else: + # Single saliency map per image, support e.g. ActivationMap + if process_saliency_maps: + saliency_map = get_actmap(saliency_map, (dataset_item.width, dataset_item.height)) + saliency_media = ResultMediaEntity( + name="Saliency Map", + type="saliency_map", + annotation_scene=dataset_item.annotation_scene, + numpy=saliency_map, + roi=dataset_item.roi, + ) + dataset_item.append_metadata_item(saliency_media, model=model) + + +def non_linear_normalization(saliency_map: np.ndarray) -> np.ndarray: + """Use non-linear normalization y=x**1.5 for 2D saliency maps.""" + + min_soft_score = np.min(saliency_map) + # make merged_map distribution positive to perform non-linear normalization y=x**1.5 + saliency_map = (saliency_map - min_soft_score) ** 1.5 + + max_soft_score = np.max(saliency_map) + saliency_map = 255.0 / (max_soft_score + 1e-12) * saliency_map + + return np.uint8(np.floor(saliency_map)) diff --git a/src/otx/api/utils/detection_utils.py b/src/otx/api/utils/detection_utils.py new file mode 100644 index 00000000000..511854e66c2 --- /dev/null +++ b/src/otx/api/utils/detection_utils.py @@ -0,0 +1,45 @@ +"""Detection utils.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from typing import List + +import numpy as np + + +def detection2array(detections: List) -> np.ndarray: + """Convert list of OpenVINO Detection to a numpy array. + + Args: + detections (List): List of OpenVINO Detection containing score, id, xmin, ymin, xmax, ymax + + Returns: + np.ndarray: numpy array with [label, confidence, x1, y1, x2, y2] + """ + scores = np.empty((0, 1), dtype=np.float32) + labels = np.empty((0, 1), dtype=np.uint32) + boxes = np.empty((0, 4), dtype=np.float32) + for det in detections: + if (det.xmax - det.xmin) * (det.ymax - det.ymin) < 1.0: + continue + scores = np.append(scores, [[det.score]], axis=0) + labels = np.append(labels, [[det.id]], axis=0) + boxes = np.append( + boxes, + [[float(det.xmin), float(det.ymin), float(det.xmax), float(det.ymax)]], + axis=0, + ) + detections = np.concatenate((labels, scores, boxes), -1) + return detections diff --git a/src/otx/api/utils/importing.py b/src/otx/api/utils/importing.py new file mode 100644 index 00000000000..8ef5a7d981d --- /dev/null +++ b/src/otx/api/utils/importing.py @@ -0,0 +1,28 @@ +"""Utils for dynamically importing stuff.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +import importlib + + +def get_impl_class(impl_path): + """Returns a class by its path in package.""" + + task_impl_module_name, task_impl_class_name = impl_path.rsplit(".", 1) + task_impl_module = importlib.import_module(task_impl_module_name) + task_impl_class = getattr(task_impl_module, task_impl_class_name) + + return task_impl_class diff --git a/src/otx/api/utils/labels_utils.py b/src/otx/api/utils/labels_utils.py new file mode 100644 index 00000000000..3154269536b --- /dev/null +++ b/src/otx/api/utils/labels_utils.py @@ -0,0 +1,20 @@ +"""This module implements utilities for labels.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Optional + +from otx.api.entities.label import LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity + + +def get_empty_label(label_schema: LabelSchemaEntity) -> Optional[LabelEntity]: + """Get first empty label from label_schema.""" + empty_candidates = list( + set(label_schema.get_labels(include_empty=True)) - set(label_schema.get_labels(include_empty=False)) + ) + if empty_candidates: + return empty_candidates[0] + return None diff --git a/src/otx/api/utils/nms.py b/src/otx/api/utils/nms.py new file mode 100644 index 00000000000..eb010926b4c --- /dev/null +++ b/src/otx/api/utils/nms.py @@ -0,0 +1,76 @@ +"""NMS Module.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np + + +def nms(boxes, scores, thresh): + """Adapted NMS implementation from OMZ: model_zoo/model_api/models/utils.py#L181.""" + # pylint: disable=too-many-locals + + x1, y1, x2, y2 = boxes.T + areas = (x2 - x1) * (y2 - y1) + order = scores.argsort()[::-1] + + keep = [] + while order.size > 0: + i = order[0] + keep.append(i) + + xx1 = np.maximum(x1[i], x1[order[1:]]) + yy1 = np.maximum(y1[i], y1[order[1:]]) + xx2 = np.minimum(x2[i], x2[order[1:]]) + yy2 = np.minimum(y2[i], y2[order[1:]]) + + width = np.maximum(0.0, xx2 - xx1) + height = np.maximum(0.0, yy2 - yy1) + intersection = width * height + + union = areas[i] + areas[order[1:]] - intersection + overlap = np.divide( + intersection, + union, + out=np.zeros_like(intersection, dtype=float), + where=union != 0, + ) + + order = order[np.where(overlap <= thresh)[0] + 1] + + return keep + + +def multiclass_nms( + detections: np.ndarray, + iou_threshold=0.45, + max_num=200, +): + """Multi-class NMS. + + strategy: in order to perform NMS independently per class, + we add an offset to all the boxes. The offset is dependent + only on the class idx, and is large enough so that boxes + from different classes do not overlap + + Args: + detections (np.ndarray): labels, scores and boxes + iou_threshold (float, optional): IoU threshold. Defaults to 0.45. + max_num (int, optional): Max number of objects filter. Defaults to 200. + + Returns: + tuple: (dets, indices), Dets are boxes with scores. Indices are indices of kept boxes. + """ + labels = detections[:, 0] + scores = detections[:, 1] + boxes = detections[:, 2:] + max_coordinate = boxes.max() + offsets = labels.astype(boxes.dtype) * (max_coordinate + 1) + boxes_for_nms = boxes + offsets[:, None] + keep = nms(boxes_for_nms, scores, iou_threshold) + if max_num > 0: + keep = keep[:max_num] + keep = np.array(keep) + det = detections[keep] + return det, keep diff --git a/src/otx/api/utils/segmentation_utils.py b/src/otx/api/utils/segmentation_utils.py new file mode 100644 index 00000000000..96b37d58701 --- /dev/null +++ b/src/otx/api/utils/segmentation_utils.py @@ -0,0 +1,285 @@ +"""This module implements segmentation related utilities.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import warnings +from copy import copy +from typing import List, Optional, Sequence, Tuple, cast + +import cv2 +import numpy as np +from bson import ObjectId + +from otx.api.entities.annotation import Annotation +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.id import ID +from otx.api.entities.label import LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.utils.shape_factory import ShapeFactory + + +def mask_from_dataset_item( + dataset_item: DatasetItemEntity, labels: List[LabelEntity], use_otx_adapter: bool = True +) -> np.ndarray: + """Creates a mask from dataset item. + + The mask will be two dimensional, and the value of each pixel matches the class index with offset 1. The background + class index is zero. labels[0] matches pixel value 1, etc. The class index is + determined based on the order of 'labels'. + + Args: + dataset_item: Item to make mask for + labels: The labels to use for creating the mask. The order of + the labels determines the class index. + + Returns: + Numpy array of mask + """ + # todo: cache this so that it does not have to be redone for all the same media + if use_otx_adapter: + mask = mask_from_annotation(dataset_item.get_annotations(), labels, dataset_item.width, dataset_item.height) + else: + mask = mask_from_file(dataset_item) + return mask + + +def mask_from_file(dataset_item: DatasetItemEntity) -> np.ndarray: + """Loads masks directly from annotation image. + + Only Common Sematic Segmentation format is supported. + """ + + mask_form_file = dataset_item.media.path + if mask_form_file is None: + raise ValueError("Mask file doesn't exist or corrupted") + mask_form_file = mask_form_file.replace("images", "masks") + mask = cv2.imread(mask_form_file, cv2.IMREAD_GRAYSCALE) + mask = np.expand_dims(mask, axis=2) + return mask + + +def mask_from_annotation( + annotations: List[Annotation], labels: List[LabelEntity], width: int, height: int +) -> np.ndarray: + """Generate a segmentation mask of a numpy image, and a list of shapes. + + The mask is will be two dimensional and the value of each pixel matches the class + index with offset 1. The background class index is zero. labels[0] matches pixel + value 1, etc. The class index is determined based on the order of `labels`: + + Args: + annotations: List of annotations to plot in mask + labels: List of labels. The index position of the label + determines the class number in the segmentation mask. + width: Width of the mask + height: Height of the mask + + Returns: + 2d numpy array of mask + """ + + mask = np.zeros(shape=(height, width), dtype=np.uint8) + for annotation in annotations: + shape = annotation.shape + if not isinstance(shape, Polygon): + shape = ShapeFactory.shape_as_polygon(annotation.shape) + known_labels = [ + label for label in annotation.get_labels() if isinstance(label, ScoredLabel) and label.get_label() in labels + ] + if len(known_labels) == 0: + # Skip unknown shapes + continue + + label_to_compare = known_labels[0].get_label() + + class_idx = labels.index(label_to_compare) + 1 + contour = [] + for point in shape.points: + contour.append([int(point.x * width), int(point.y * height)]) + + mask = cv2.drawContours(mask, np.asarray([contour]), 0, (class_idx, class_idx, class_idx), -1) + + mask = np.expand_dims(mask, axis=2) + + return mask + + +def create_hard_prediction_from_soft_prediction( + soft_prediction: np.ndarray, soft_threshold: float, blur_strength: int = 5 +) -> np.ndarray: + """Creates a hard prediction containing the final label index per pixel. + + Args: + soft_prediction: Output from segmentation network. Assumes + floating point values, between 0.0 and 1.0. Can be a + 2d-array of shape (height, width) or per-class segmentation + logits of shape (height, width, num_classes) + soft_threshold: minimum class confidence for each pixel. The + higher the value, the more strict the segmentation is + (usually set to 0.5) + blur_strength: The higher the value, the smoother the + segmentation output will be, but less accurate + + Returns: + Numpy array of the hard prediction + """ + soft_prediction_blurred = cv2.blur(soft_prediction, (blur_strength, blur_strength)) + if len(soft_prediction.shape) == 3: + # Apply threshold to filter out `unconfident` predictions, then get max along + # class dimension + soft_prediction_blurred[soft_prediction_blurred < soft_threshold] = 0 + hard_prediction = np.argmax(soft_prediction_blurred, axis=2) + elif len(soft_prediction.shape) == 2: + # In the binary case, simply apply threshold + hard_prediction = soft_prediction_blurred > soft_threshold + else: + raise ValueError( + f"Invalid prediction input of shape {soft_prediction.shape}. " f"Expected either a 2D or 3D array." + ) + return hard_prediction + + +Contour = List[Tuple[float, float]] + + +def get_subcontours(contour: Contour) -> List[Contour]: + """Splits contour into subcontours that do not have self intersections.""" + + ContourInternal = List[Optional[Tuple[float, float]]] + + def find_loops(points: ContourInternal) -> List[Sequence[int]]: + """For each consecutive pair of equivalent rows in the input matrix returns their indices.""" + _, inverse, count = np.unique(points, axis=0, return_inverse=True, return_counts=True) + duplicates = np.where(count > 1)[0] + indices = [] + for x in duplicates: + y = np.nonzero(inverse == x)[0] + for i, _ in enumerate(y[:-1]): + indices.append(y[i : i + 2]) + return indices + + base_contour = cast(ContourInternal, copy(contour)) + + # Make sure that contour is closed. + if not np.array_equal(base_contour[0], base_contour[-1]): + base_contour.append(base_contour[0]) + + subcontours: List[Contour] = [] + loops = sorted(find_loops(base_contour), key=lambda x: x[0], reverse=True) + for loop in loops: + i, j = loop + subcontour = base_contour[i:j] + subcontour = list(x for x in subcontour if x is not None) + subcontours.append(cast(Contour, subcontour)) + base_contour[i:j] = [None] * (j - i) + + subcontours = [i for i in subcontours if len(i) > 2] + return subcontours + + +def create_annotation_from_segmentation_map( + hard_prediction: np.ndarray, soft_prediction: np.ndarray, label_map: dict +) -> List[Annotation]: + """Creates polygons from the soft predictions. + + Background label will be ignored and not be converted to polygons. + + Args: + hard_prediction: hard prediction containing the final label + index per pixel. See function + `create_hard_prediction_from_soft_prediction`. + soft_prediction: soft prediction with shape H x W x N_labels, + where soft_prediction[:, :, 0] is the soft prediction for + background. If soft_prediction is of H x W shape, it is + assumed that this soft prediction will be applied for all + labels. + label_map: dictionary mapping labels to an index. It is assumed + that the first item in the dictionary corresponds to the + background label and will therefore be ignored. + + Returns: + List of shapes + """ + # pylint: disable=too-many-locals + height, width = hard_prediction.shape[:2] + img_class = hard_prediction.swapaxes(0, 1) + + # pylint: disable=too-many-nested-blocks + annotations: List[Annotation] = [] + for label_index, label in label_map.items(): + # Skip background + if label_index == 0: + continue + + # obtain current label soft prediction + if len(soft_prediction.shape) == 3: + current_label_soft_prediction = soft_prediction[:, :, label_index] + else: + current_label_soft_prediction = soft_prediction + + obj_group = img_class == label_index + label_index_map = (obj_group.T.astype(int) * 255).astype(np.uint8) + + # Contour retrieval mode CCOMP (Connected components) creates a two-level + # hierarchy of contours + contours, hierarchies = cv2.findContours(label_index_map, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE) + + if hierarchies is not None: + for contour, hierarchy in zip(contours, hierarchies[0]): + if len(contour) <= 2 or cv2.contourArea(contour) < 1.0: + continue + + if hierarchy[3] == -1: + # In this case a contour does not represent a hole + contour = list((point[0][0], point[0][1]) for point in contour) + + # Split contour into subcontours that do not have self intersections. + subcontours = get_subcontours(contour) + for subcontour in subcontours: + # compute probability of the shape + mask = np.zeros(hard_prediction.shape, dtype=np.uint8) + cv2.drawContours( + mask, + np.asarray([[[x, y]] for x, y in subcontour]), + contourIdx=-1, + color=1, + thickness=-1, + ) + probability = cv2.mean(current_label_soft_prediction, mask)[0] + + # convert the list of points to a closed polygon + points = [Point(x=x / (width - 1), y=y / (height - 1)) for x, y in subcontour] + polygon = Polygon(points=points) + + if polygon.get_area() > 0: + # Contour is a closed polygon with area > 0 + annotations.append( + Annotation( + shape=polygon, + labels=[ScoredLabel(label, probability)], + id=ID(ObjectId()), + ) + ) + else: + # Contour is a closed polygon with area == 0 + warnings.warn( + "The geometry of the segmentation map you are converting " + "is not fully supported. Polygons with a area of zero " + "will be removed.", + UserWarning, + ) + else: + # If contour hierarchy[3] != -1 then contour has a parent and + # therefore is a hole + # Do not allow holes in segmentation masks to be filled silently, + # but trigger warning instead + warnings.warn( + "The geometry of the segmentation map you are converting is " + "not fully supported. A hole was found and will be filled.", + UserWarning, + ) + + return annotations diff --git a/src/otx/api/utils/shape_drawer.py b/src/otx/api/utils/shape_drawer.py new file mode 100644 index 00000000000..965e627e674 --- /dev/null +++ b/src/otx/api/utils/shape_drawer.py @@ -0,0 +1,690 @@ +"""This module implements helpers for drawing shapes.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-locals +import abc +from typing import ( + Callable, + Generic, + List, + NewType, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, +) + +import cv2 +import numpy as np + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + NullAnnotationSceneEntity, +) +from otx.api.entities.coordinate import Coordinate +from otx.api.entities.label import LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.shapes.shape import ShapeEntity + +CvTextSize = NewType("CvTextSize", Tuple[Tuple[int, int], int]) + +_Any = TypeVar("_Any") + + +class DrawerEntity(Generic[_Any]): + """An interface to draw a shape of type ``T`` onto an image.""" + + supported_types: Sequence[Type[ShapeEntity]] = [] + + @abc.abstractmethod + def draw(self, image: np.ndarray, entity: _Any, labels: List[ScoredLabel]) -> np.ndarray: + """Draw an entity to a given frame. + + Args: + image (np.ndarray): The image to draw the entity on. + entity (T): The entity to draw. + labels (List[ScoredLabel]): Labels of the shapes to draw + + Returns: + np.ndarray: frame with shape drawn on it + """ + raise NotImplementedError + + +class Helpers: + """Contains variables which are used by all subclasses. + + Contains functions which help with generating coordinates, text and text scale. + These functions are use by the DrawerEntity Classes when drawing to an image. + """ + + def __init__(self) -> None: + # Same alpha value that the UI uses for Labels + self.alpha_shape = 100 / 256 + self.alpha_labels = 153 / 256 + self.assumed_image_width_for_text_scale = 1280 # constant number for size of classification/counting overlay + self.top_margin = 0.07 # part of the top screen reserved for top left classification/counting overlay + self.content_padding = 3 + self.top_left_box_thickness = 1 + self.content_margin = 2 + self.label_offset_box_shape = 0 + self.black = (0, 0, 0) + self.white = (255, 255, 255) + self.yellow = (255, 255, 0) + + self.cursor_pos = Coordinate(0, 0) + self.line_height = 0 + + @staticmethod + def draw_transparent_rectangle( + img: np.ndarray, + x1: int, + y1: int, + x2: int, + y2: int, + color: Tuple[int, int, int], + alpha: float, + ) -> np.ndarray: + """Draw a rectangle on an image. + + Args: + img (np.ndarray): Image + x1 (int): Left side + y1 (int): Top side + x2 (int): Right side + y2 (int): Bottom side + color (Tuple[int, int, int]): Color + alpha (float): Alpha value between 0 and 1 + """ + x1 = np.clip(x1, 0, img.shape[1] - 1) + y1 = np.clip(y1, 0, img.shape[0] - 1) + x2 = np.clip(x2 + 1, 0, img.shape[1] - 1) + y2 = np.clip(y2 + 1, 0, img.shape[0] - 1) + rect = img[y1:y2, x1:x2] + rect[:] = (alpha * np.array(color))[np.newaxis, np.newaxis] + (1 - alpha) * rect + return img + + def generate_text_scale(self, image: np.ndarray) -> float: + """Calculates the scale of the text. + + Args: + image (np.ndarray): Image to calculate the text scale for. + + Returns: + scale for the text + """ + return round(image.shape[1] / self.assumed_image_width_for_text_scale, 1) + + @staticmethod + def generate_text_for_label( + label: Union[LabelEntity, ScoredLabel], show_labels: bool, show_confidence: bool + ) -> str: + """Return a string representing a given label and its associated probability if label is a ScoredLabel. + + The exact format of the string depends on the function parameters described below. + + Args: + label (Union[LabelEntity, ScoredLabel]): Label + show_labels (bool): Whether to render the labels above the shape + show_confidence (bool): Whether to render the confidence above the + shape + + Returns: + str: Formatted string (e.g. `"Cat 58%"`) + """ + text = "" + if show_labels: + text += label.name + if show_confidence and isinstance(label, ScoredLabel): + if len(text) > 0: + text += " " + text += f"{label.probability:.0%}" + return text + + def generate_draw_command_for_labels( + self, + labels: Sequence[Union[LabelEntity, ScoredLabel]], + image: np.ndarray, + show_labels: bool, + show_confidence: bool, + ) -> Tuple[Callable[[np.ndarray], np.ndarray], int, int]: + """Generate draw function and content width and height for labels. + + Generates a function which can be called to draw a list of labels onto an image relatively to the + cursor position. + The width and height of the content is also returned and can be determined to compute + the best position for content before actually drawing it. + + Args: + labels (Sequence[Union[LabelEntity, ScoredLabel]]): List of labels + image (np.ndarray): Image (used to compute font size) + show_labels (bool): Whether to show the label name + show_confidence (bool): Whether to show the confidence probability + + Returns: + A tuple containing the drawing function, the content width, + and the content height + """ + draw_commands = [] + content_width = 0 + content_height = 0 + + # Loop through the list of labels and create a function which can be used to draw the label. + for label in labels: + text = self.generate_text_for_label(label, show_labels, show_confidence) + text_scale = self.generate_text_scale(image) + thickness = int(text_scale / 2) + color = label.color.bgr_tuple + + item_command, item_width, item_height = self.generate_draw_command_for_text( + text, text_scale, thickness, color + ) + + draw_commands.append(item_command) + + content_width += item_width + content_height = max(content_height, item_height) + + def draw_command(img: np.ndarray) -> np.ndarray: + for command in draw_commands: + img = command(img) + return img + + return draw_command, content_width, content_height + + def generate_draw_command_for_text( + self, text: str, text_scale: float, thickness: int, color: Tuple[int, int, int] + ) -> Tuple[Callable[[np.ndarray], np.ndarray], int, int]: + """Generate function to draw text on image relative to cursor position. + + Generate a function which can be called to draw the given text onto an image + relatively to the cursor position. + + The width and height of the content is also returned and can be determined to compute + the best position for content before actually drawing it. + + Args: + text (str): Text to draw + text_scale (float): Font size + thickness (int): Thickness of the text + color (Tuple[int, int, int]): Color of the text + + Returns: + A tuple containing the drawing function, the content width, + and the content height + """ + + padding = self.content_padding + margin = self.content_margin + + label_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, fontScale=text_scale, thickness=thickness) + + baseline = label_size[1] + text_width = label_size[0][0] + text_height = label_size[0][1] + + width = text_width + 2 * padding + height = text_height + baseline + 2 * padding + content_width = width + margin + + if (color[0] + color[1] + color[2]) / 3 > 200: + text_color = self.black + else: + text_color = self.white + + def draw_command(img: np.ndarray) -> np.ndarray: + cursor_pos = Coordinate(int(self.cursor_pos.x), int(self.cursor_pos.y)) + self.draw_transparent_rectangle( + img, + int(cursor_pos.x), + int(cursor_pos.y), + int(cursor_pos.x + width), + int(cursor_pos.y + height), + color, + self.alpha_labels, + ) + + img = cv2.putText( + img=img, + text=text, + org=( + cursor_pos.x + padding, + cursor_pos.y + height - padding - baseline, + ), + fontFace=cv2.FONT_HERSHEY_SIMPLEX, + fontScale=text_scale, + color=text_color, + thickness=thickness, + lineType=cv2.LINE_AA, + ) + + self.cursor_pos.x += content_width + self.line_height = height + + return img + + return draw_command, content_width, height + + @staticmethod + def draw_flagpole( + image: np.ndarray, + flagpole_start_point: Coordinate, + flagpole_end_point: Coordinate, + ): + """Draw a small flagpole between two points. + + Args: + image: Image + flagpole_start_point: Start of the flagpole + flagpole_end_point: End of the flagpole + + Returns: + Image + """ + return cv2.line( + image, + flagpole_start_point.as_int_tuple(), + flagpole_end_point.as_int_tuple(), + color=[0, 0, 0], + thickness=2, + ) + + def newline(self): + """Move the cursor to the next line.""" + self.cursor_pos.x = 0 + self.cursor_pos.y += self.line_height + self.content_margin + + def set_cursor_pos(self, cursor_pos: Optional[Coordinate] = None): + """Move the cursor to a new position. + + Args: + cursor_pos (Optional[Coordinate]): New position of the cursor; (0,0) if not specified. + """ + if cursor_pos is None: + cursor_pos = Coordinate(0, 0) + + self.cursor_pos = cursor_pos + + +class ShapeDrawer(DrawerEntity[AnnotationSceneEntity]): + """ShapeDrawer to draw any shape on a numpy array. Will overlay the shapes in the same way that the UI does. + + Args: + show_count: Whether or not to render the amount of objects on + screen in the top left. + is_one_label: Whether there is only one label present in the + project. + """ + + # TODO Connect show_count,is_is_one_label to the UI for toggling. + def __init__(self, show_count, is_one_label): + super().__init__() + self.show_labels = True + self.show_confidence = True + self.show_count = show_count + self.is_one_label = is_one_label + + if self.is_one_label and not self.show_count: + self.show_labels = False + + self.shape_drawers = [ + self.RectangleDrawer(self.show_labels, self.show_confidence), + self.PolygonDrawer(self.show_labels, self.show_confidence), + self.EllipseDrawer(self.show_labels, self.show_confidence), + ] + + # Always show global labels, especially if shape labels are disabled (because of is_one_label). + self.top_left_drawer = self.TopLeftDrawer(True, self.show_confidence, self.is_one_label) + + def draw( + self, + image: np.ndarray, + entity: AnnotationSceneEntity, + labels: List[ScoredLabel], + ) -> np.ndarray: + """Use a compatible drawer to draw all shapes of an annotation to the corresponding image. + + Also render a label in the top left if we need to. + + Args: + image: Numpy image, one frame of a video on which to draw + something + entity: AnnotationSceneEntity entity corresponding to this + particular frame of the video + labels: Can be passed as an empty list since they are + already present in annotation_scene + + Returns: + Modified image. + """ + + num_annotations = 0 + + self.top_left_drawer.set_cursor_pos() + + if not isinstance(entity, NullAnnotationSceneEntity): + for annotation in entity.annotations: + if ( + isinstance(annotation.shape, Rectangle) + and annotation.shape.x1 == 0 + and annotation.shape.y1 == 0 + and annotation.shape.x2 == 1 + and annotation.shape.y2 == 1 + ): + # If is_one_label is activated, don't draw the labels here + # because we will draw them again outside the loop. + if not self.is_one_label: + image = self.top_left_drawer.draw(image, annotation, labels=[]) + else: + num_annotations += 1 + for drawer in self.shape_drawers: + if type(annotation.shape) in drawer.supported_types and len(annotation.get_labels()) > 0: + image = drawer.draw(image, annotation.shape, labels=annotation.get_labels()) + if self.is_one_label: + image = self.top_left_drawer.draw_labels(image, entity.get_labels()) + if self.show_count: + image = self.top_left_drawer.draw_annotation_count(image, num_annotations) + return image + + class TopLeftDrawer(Helpers, DrawerEntity[Annotation]): + """Draws labels in an image's top left corner.""" + + def __init__(self, show_labels, show_confidence, is_one_label): + super().__init__() + self.show_labels = show_labels + self.show_confidence = show_confidence + self.is_one_label = is_one_label + + def draw(self, image: np.ndarray, entity: Annotation, labels: List[ScoredLabel]) -> np.ndarray: + """Draw the labels of a shape in the image top left corner. + + Args: + image (np.ndarray): Image + entity (Annotation): Annotation + labels (List[ScoredLabels]): (Unused) labels to be drawn on the image + + Returns: + np.ndarray: Image with label on top. + """ + return self.draw_labels(image, entity.get_labels()) + + def draw_labels(self, image: np.ndarray, labels: Sequence[Union[LabelEntity, ScoredLabel]]) -> np.ndarray: + """Draw the labels in the image top left corner. + + Args: + image (np.ndarray): Image + labels (Sequence[Union[LabelEntity, ScoredLabel]]): Sequence of labels + + Returns: + np.ndarray: Image with label on top. + """ + show_confidence = self.show_confidence if not self.is_one_label else False + + draw_command, _, _ = self.generate_draw_command_for_labels(labels, image, self.show_labels, show_confidence) + + image = draw_command(image) + + if len(labels) > 0: + self.newline() + + return image + + def draw_annotation_count(self, image: np.ndarray, num_annotations: int) -> np.ndarray: + """Draw the number of annotations to the top left corner of the image. + + Args: + image (np.ndarray): Image + num_annotations (int): Number of annotations + + Returns: + np.ndarray: Image with annotation count on top. + """ + text = f"Count: {num_annotations}" + color = self.yellow + + text_scale = self.generate_text_scale(image) + draw_command, _, _ = self.generate_draw_command_for_text( + text, text_scale, self.top_left_box_thickness, color + ) + image = draw_command(image) + + self.newline() + + return image + + class RectangleDrawer(Helpers, DrawerEntity[Rectangle]): + """Draws rectangles.""" + + supported_types = [Rectangle] + + def __init__(self, show_labels, show_confidence): + super().__init__() + self.show_labels = show_labels + self.show_confidence = show_confidence + + def draw(self, image: np.ndarray, entity: Rectangle, labels: List[ScoredLabel]) -> np.ndarray: + """Draws a rectangle on the image along with labels. + + Args: + image (np.ndarray): Image to draw on. + entity (Rectangle): Rectangle to draw. + labels (List[ScoredLabel]): List of labels. + + Returns: + np.ndarray: Image with rectangle drawn on it. + """ + base_color = labels[0].color.bgr_tuple + + # Draw the rectangle on the image + x1, y1 = int(entity.x1 * image.shape[1]), int(entity.y1 * image.shape[0]) + x2, y2 = int(entity.x2 * image.shape[1]), int(entity.y2 * image.shape[0]) + image = self.draw_transparent_rectangle(image, x1, y1, x2, y2, base_color, self.alpha_shape) + image = cv2.rectangle(img=image, pt1=(x1, y1), pt2=(x2, y2), color=base_color, thickness=2) + + ( + draw_command, + content_width, + content_height, + ) = self.generate_draw_command_for_labels(labels, image, self.show_labels, self.show_confidence) + + # Generate a command to draw the list of labels + # and compute the actual size of the list of labels. + y_coord = y1 - self.label_offset_box_shape - content_height + x_coord = x1 + + # put label inside if it is out of bounds at the top of the shape, and shift label to left if needed + if y_coord < self.top_margin * image.shape[0]: + y_coord = y1 + self.label_offset_box_shape + if x_coord + content_width > image.shape[1]: + x_coord = x2 - content_width + + # Draw the list of labels. + self.set_cursor_pos(Coordinate(x_coord, y_coord)) + image = draw_command(image) + return image + + class EllipseDrawer(Helpers, DrawerEntity[Ellipse]): + """Draws ellipses.""" + + supported_types = [Ellipse] + + def __init__(self, show_labels, show_confidence): + super().__init__() + self.show_labels = show_labels + self.show_confidence = show_confidence + + def draw(self, image: np.ndarray, entity: Ellipse, labels: List[ScoredLabel]) -> np.ndarray: + """Draw the ellipse on the image. + + Args: + image (np.ndarray): Image to draw on. + entity (Ellipse): Ellipse to draw. + labels (List[ScoredLabel]): Labels to draw. + + Returns: + np.ndarray: Image with the ellipse drawn on it. + """ + base_color = labels[0].color.bgr_tuple + if entity.width > entity.height: + axes = ( + int(entity.major_axis * image.shape[1]), + int(entity.minor_axis * image.shape[0]), + ) + else: + axes = ( + int(entity.major_axis * image.shape[0]), + int(entity.minor_axis * image.shape[1]), + ) + center = ( + int(entity.x_center * image.shape[1]), + int(entity.y_center * image.shape[0]), + ) + # Draw the shape on the image + alpha = self.alpha_shape + overlay = cv2.ellipse( + img=image.copy(), + center=center, + axes=axes, + angle=0, + startAngle=0, + endAngle=360, + color=base_color, + thickness=cv2.FILLED, + ) + result_without_border = cv2.addWeighted(overlay, alpha, image, 1 - alpha, 0) + result_with_border = cv2.ellipse( + img=result_without_border, + center=center, + axes=axes, + angle=0, + startAngle=0, + endAngle=360, + color=base_color, + lineType=cv2.LINE_AA, + ) + + # Generate a command to draw the list of labels + # and compute the actual size of the list of labels. + ( + draw_command, + content_width, + content_height, + ) = self.generate_draw_command_for_labels(labels, image, self.show_labels, self.show_confidence) + + # get top left corner of imaginary bbox around circle + offset = self.label_offset_box_shape + x_coord = entity.x1 * image.shape[1] + y_coord = entity.y1 * image.shape[0] - offset - content_height + + flagpole_end_point = Coordinate(int(x_coord + 1), int(entity.y_center * image.shape[0])) + + # put label at bottom if it is out of bounds at the top of the shape, and shift label to left if needed + if y_coord < self.top_margin * image.shape[0]: + y_coord = (entity.y1 * image.shape[0]) + (entity.y2 * image.shape[0]) + offset + flagpole_start_point = Coordinate(x_coord + 1, y_coord) + else: + flagpole_start_point = Coordinate(x_coord + 1, y_coord + content_height) + + if x_coord + content_width > result_with_border.shape[1]: + # The list of labels is too close to the right side of the image. + # Move it slightly to the left. + x_coord = result_with_border.shape[1] - content_width + + # Draw the list of labels and a small flagpole. + self.set_cursor_pos(Coordinate(x_coord, y_coord)) + image = draw_command(result_with_border) + image = self.draw_flagpole(image, flagpole_start_point, flagpole_end_point) + + return image + + class PolygonDrawer(Helpers, DrawerEntity[Polygon]): + """Draws polygons.""" + + supported_types = [Polygon] + + def __init__(self, show_labels, show_confidence): + super().__init__() + self.show_labels = show_labels + self.show_confidence = show_confidence + + def draw(self, image: np.ndarray, entity: Polygon, labels: List[ScoredLabel]) -> np.ndarray: + """Draw polygon and labels on image. + + Args: + image (np.ndarray): Image to draw on. + entity (Polygon): Polygon to draw. + labels (List[ScoredLabel]): List of labels to draw. + + Returns: + np.ndarray: Image with polygon drawn on it. + """ + base_color = labels[0].color.bgr_tuple + + # Draw the shape on the image + alpha = self.alpha_shape + contours = np.array( + [[point.x * image.shape[1], point.y * image.shape[0]] for point in entity.points], + dtype=np.int32, + ) + overlay = cv2.drawContours( + image=image.copy(), + contours=[contours], + contourIdx=-1, + color=base_color, + thickness=cv2.FILLED, + ) + result_without_border = cv2.addWeighted(overlay, alpha, image, 1 - alpha, 0) + result_with_border = cv2.drawContours( + image=result_without_border, + contours=[contours], + contourIdx=-1, + color=base_color, + thickness=2, + lineType=cv2.LINE_AA, + ) + + # Generate a command to draw the list of labels + # and compute the actual size of the list of labels. + ( + draw_command, + content_width, + content_height, + ) = self.generate_draw_command_for_labels(labels, image, self.show_labels, self.show_confidence) + + # get top left corner of imaginary bbox around polygon + x_coord = min(point[0] for point in contours) + y_coord = min(point[1] for point in contours) - self.label_offset_box_shape - content_height + + # end point = Y in polygon where X is lowest, x offset to make line flush with text rectangle + _, idx = min((val, idx) for (idx, val) in enumerate([point[0] for point in contours])) + flagpole_end_point = Coordinate(x_coord + 1, [point[1] for point in contours][idx]) + + if y_coord < self.top_margin * image.shape[0]: + # The polygon is too close to the top of the image. + # Draw the labels underneath the polygon instead. + y_coord = max(point[1] for point in contours) + self.label_offset_box_shape + flagpole_start_point = Coordinate(x_coord + 1, y_coord) + else: + flagpole_start_point = Coordinate(x_coord + 1, y_coord + content_height) + + if x_coord + content_width > result_with_border.shape[1]: + # The list of labels is too close to the right side of the image. + # Move it slightly to the left. + x_coord = result_with_border.shape[1] - content_width + + # Draw the list of labels and a small flagpole. + self.set_cursor_pos(Coordinate(x_coord, y_coord)) + image = draw_command(result_with_border) + image = self.draw_flagpole(image, flagpole_start_point, flagpole_end_point) + + return image diff --git a/src/otx/api/utils/shape_factory.py b/src/otx/api/utils/shape_factory.py new file mode 100644 index 00000000000..b27eacb9871 --- /dev/null +++ b/src/otx/api/utils/shape_factory.py @@ -0,0 +1,195 @@ +"""This module implements helpers for converting shape entities.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.shapes.shape import ShapeEntity + + +class ShapeFactory: + """Helper class converting between shape types.""" + + @staticmethod + def shape_as_rectangle(shape: ShapeEntity) -> Rectangle: + """Get the outer-fitted rectangle representation of the shape. + + `media_width` and `media_height` are the width and height of the media in + which the shape is expressed. + + Example: + + Let's assume a DatasetItem `dataset_item`. + + To obtain the shapes inside the full annotation as rectangles, one could call: + + >>> from otx.api.entities.dataset_item import DatasetItem + >>> from otx.api.entities.image import NullImage + >>> from otx.api.entities.annotation_scene import NullAnnotationScene + >>> dataset_item = DatasetItem(media=NullImage(), + >>> annotation=NullAnnotationScene()) + >>> rectangles = [ShapeFactory.shape_as_rectangle(shape, + >>> dataset_item.media.width, dataset_item.media.height) for shape + ... in dataset_item.annotation_scene.shapes] + + To obtain the shapes inside the dataset item (note that dataset item can have + roi), one should call: + + >>> rectangles = [ShapeFactory.shape_as_rectangle(shape) for shape in + >>> dataset_item.get_annotations()] + + Since the shapes in the first call come from annotation directly, this means + they are expressed in the media coordinate system. Therefore, + dataset_item.media.width and dataset_item.media.height are passed. + While in the second call, the shapes come from denormalization results wrt. + dataset_item.roi and therefore expressed inside the roi. In this case, + dataset_item.width and dataset_item.height are passed. + + Converting Ellipse to rectangle + + >>> height = 240 + >>> width = 480 + >>> rectangle = Rectangle(x1=0.375, y1=0.25, x2=0.625, y2=0.75, + ... labels=[]) # a square of 120 x 120 pixels at the center of the image + >>> ellipse = Ellipse(x1=0.5, y1=0.5, x2=0.625, y2=0.56125, + ... labels=[]) # an ellipse of radius 60 pixels (x2 is wrt width) at the + center of the image + >>> ellipse_as_rectangle = ShapeFactory.shape_as_rectangle(ellipse) # get the + fitted rectangle for the ellipse + >>> str(rectangle) == str(ellipse_as_rectangle) + True + + Converting triangle to rectangle + + >>> points = [Point(x=0.5, y=0.25), Point(x=0.375, y=0.75), + >>> Point(x=0.625, y=0.75)] + >>> triangle = Polygon(points=points, labels=[]) + >>> triangle_as_rectangle = ShapeFactory.shape_as_rectangle(triangle) + >>> str(triangle_as_rectangle) == str(rectangle) + True + + Args: + shape (ShapeEntity): the shape to convert to rectangle + + Returns: + Rectangle: bounding box of the shape + """ + if isinstance(shape, Rectangle): + return shape + + if isinstance(shape, Ellipse): + x1 = shape.x1 + y1 = shape.y1 + x2 = shape.x2 + y2 = shape.y2 + elif isinstance(shape, Polygon): + x1 = shape.min_x + x2 = shape.max_x + y1 = shape.min_y + y2 = shape.max_y + else: + raise NotImplementedError(f"Conversion of a {type(shape)} to a rectangle is not implemented yet: {shape}") + + new_shape = Rectangle(x1=x1, y1=y1, x2=x2, y2=y2) + return new_shape + + @staticmethod + def shape_as_polygon(shape: ShapeEntity) -> Polygon: + """Return a shape converted as polygon. + + For a rectangle, a polygon will be constructed with a point in each corner. + For a ellipse, 360 points will be made. Otherwise, the original shape will be returned. + The width/height for the parent need to be specified to make sure the aspect ratio is maintained. + + Args: + shape (ShapeEntity): the shape to convert to polygon. + + Returns: + Polygon: the polygon representation of the shape. + + Raises: + NotImplementedError: if the shape is not a rectangle or ellipse. + """ + if isinstance(shape, Polygon): + new_shape = shape + elif isinstance(shape, Rectangle): + points = [ + Point(x=shape.x1, y=shape.y1), + Point(x=shape.x2, y=shape.y1), + Point(x=shape.x2, y=shape.y2), + Point(x=shape.x1, y=shape.y2), + Point(x=shape.x1, y=shape.y1), + ] + new_shape = Polygon(points=points) + elif isinstance(shape, Ellipse): + coordinates = shape.get_evenly_distributed_ellipse_coordinates() + points = [Point(x=point[0], y=point[1]) for point in coordinates] + new_shape = Polygon(points=points) + else: + raise NotImplementedError(f"Conversion of a {type(shape)} to a polygon is not implemented yet: " f"{shape}") + return new_shape + + @staticmethod + def shape_as_ellipse(shape: ShapeEntity) -> Ellipse: + """Returns the inner-fitted ellipse for a given shape. + + Args: + shape (ShapeEntity): Shape to convert. + + Returns: + Ellipse: Ellipse representation of the shape. + + Raises: + NotImplementedError: If the shape is not a rectangle or polygon. + """ + if isinstance(shape, Ellipse): + return shape + + if isinstance(shape, Rectangle): + x1 = shape.x1 + x2 = shape.x2 + y1 = shape.y1 + y2 = shape.y2 + elif isinstance(shape, Polygon): + x1 = shape.min_x + x2 = shape.max_x + y1 = shape.min_y + y2 = shape.max_y + else: + raise NotImplementedError(f"Conversion of a {type(shape)} to an ellipse is not implemented yet: {shape}") + return Ellipse(x1=x1, y1=y1, x2=x2, y2=y2) + + @staticmethod + def shape_produces_valid_crop(shape: ShapeEntity, media_width: int, media_height: int) -> bool: + """Check if crop is valid. + + Checks if the shape produces a valid crop based on the image width and height, + regardless of the contents of the image. + + Args: + shape (ShapeEntity): Shape to check + media_width (int): Width of the image + media_height (int): Height of the image + + Returns: + bool: True if the shape produces a valid crop, False otherwise + """ + is_valid = True + try: + shape_as_rectangle = ShapeFactory.shape_as_rectangle(shape=shape) + except ValueError: + # Thrown if the resulting bounding box is invalid + return False + + # The min and max are to handle out-of-bounds coordinates + x1 = min(max(round(shape_as_rectangle.x1 * media_width), 0), media_width) + x2 = min(max(round(shape_as_rectangle.x2 * media_width), 0), media_width) + y1 = min(max(round(shape_as_rectangle.y1 * media_height), 0), media_height) + y2 = min(max(round(shape_as_rectangle.y2 * media_height), 0), media_height) + + if x1 == x2 or y1 == y2: + is_valid = False + return is_valid diff --git a/src/otx/api/utils/tiler.py b/src/otx/api/utils/tiler.py new file mode 100644 index 00000000000..f645b2ace77 --- /dev/null +++ b/src/otx/api/utils/tiler.py @@ -0,0 +1,419 @@ +"""Tiling Module.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import cv2 +from itertools import product +from typing import Dict, List, Optional, Tuple, Union + +import numpy as np +from openvino.model_api.models import Model, ImageModel + +from openvino.model_api.models.utils import DetectionResult + +from otx.api.utils.async_pipeline import OTXDetectionAsyncPipeline +from otx.api.utils.detection_utils import detection2array +from otx.api.utils.nms import multiclass_nms +from otx.api.utils.dataset_utils import non_linear_normalization + + +class Tiler: + """Tile Image into (non)overlapping Patches. Images are tiled in order to efficiently process large images. + + Args: + tile_size: Tile dimension for each patch + overlap: Overlap between adjacent tile + max_number: max number of prediction per image + detector: OpenVINO adaptor model + classifier: Tile classifier OpenVINO adaptor model + segm: enable instance segmentation mask output + mode: async or sync mode + """ + + def __init__( + self, + tile_size: int, + overlap: float, + max_number: int, + detector: Model, + classifier: Optional[ImageModel] = None, + segm: bool = False, + mode: str = "async", + num_classes: int = 0, + ): # pylint: disable=too-many-arguments + self.tile_size = tile_size + self.overlap = overlap + self.max_number = max_number + self.model = detector + self.classifier = classifier + # needed to create saliency maps for IRs for Mask RCNN + self.num_classes = num_classes + self.segm = segm + if self.segm: + self.model.disable_mask_resizing() + if mode == "async": + self.async_pipeline = OTXDetectionAsyncPipeline(self.model) + + def tile(self, image: np.ndarray) -> List[List[int]]: + """Tiles an input image to either overlapping, non-overlapping or random patches. + + Args: + image: Input image to tile. + + Returns: + Tiles coordinates + """ + height, width = image.shape[:2] + + coords = [[0, 0, width, height]] + for (loc_j, loc_i) in product( + range(0, width, int(self.tile_size * (1 - self.overlap))), + range(0, height, int(self.tile_size * (1 - self.overlap))), + ): + x2 = min(loc_j + self.tile_size, width) + y2 = min(loc_i + self.tile_size, height) + coords.append([loc_j, loc_i, x2, y2]) + return coords + + def filter_tiles_by_objectness( + self, image: np.ndarray, tile_coords: List[List[int]], confidence_threshold: float = 0.35 + ): + """Filter tiles by objectness score by running tile classifier. + + Args: + image (np.ndarray): full size image + tile_coords (List[List[int]]): tile coordinates + + Returns: + keep_coords: tile coordinates to keep + """ + keep_coords = [] + for i, coord in enumerate(tile_coords): + tile_img = self.crop_tile(image, coord) + tile_dict, _ = self.model.preprocess(tile_img) + objectness_score = self.classifier.infer_sync(tile_dict) + if i == 0 or objectness_score["tile_prob"] > confidence_threshold: + keep_coords.append(coord) + return keep_coords + + def predict(self, image: np.ndarray, mode: str = "async"): + """Predict by cropping full image to tiles. + + Args: + image (np.ndarray): full size image + + Returns: + detection: prediction results + features: saliency map and feature vector + """ + tile_coords = self.tile(image) + if self.classifier is not None: + tile_coords = self.filter_tiles_by_objectness(image, tile_coords) + + if mode == "sync": + return self.predict_sync(image, tile_coords) + return self.predict_async(image, tile_coords) + + def predict_sync(self, image: np.ndarray, tile_coords: List[List[int]]): + """Predict by cropping full image to tiles synchronously. + + Args: + image (np.ndarray): full size image + tile_coords (List[List[int]]): tile coordinates + + Returns: + detection: prediction results + features: saliency map and feature vector + """ + features = [] + tile_results = [] + + for coord in tile_coords: + tile_img = self.crop_tile(image, coord) + tile_dict, tile_meta = self.model.preprocess(tile_img) + raw_predictions = self.model.infer_sync(tile_dict) + predictions = self.model.postprocess(raw_predictions, tile_meta) + tile_result = self.postprocess_tile(predictions, *coord[:2]) + # cache each tile feature vector and saliency map + if "feature_vector" in raw_predictions or "saliency_map" in raw_predictions: + tile_meta.update({"coord": coord}) + features.append( + ( + (raw_predictions["feature_vector"].reshape(-1), raw_predictions["saliency_map"][0]), + tile_meta, + ) + ) + + tile_results.append(tile_result) + + merged_results = self.merge_results(tile_results, image.shape) + merged_features = self.merge_features(features, merged_results) + return merged_results, merged_features + + def predict_async(self, image: np.ndarray, tile_coords: List[List[int]]): + """Predict by cropping full image to tiles asynchronously. + + Args: + image (np.ndarray): full size image + tile_coords (List[List[int]]): tile coordinates + + Returns: + detection: prediction results + features: saliency map and feature vector + """ + num_tiles = len(tile_coords) + + processed_tiles = 0 + tile_results = [] + features = [] + for i, coord in enumerate(tile_coords): + pred = self.async_pipeline.get_result(processed_tiles) + while pred: + tile_prediction, meta, feats = pred + if isinstance(feats[0], np.ndarray): + features.append((feats, meta)) + tile_result = self.postprocess_tile(tile_prediction, *meta["coord"][:2]) + tile_results.append(tile_result) + processed_tiles += 1 + pred = self.async_pipeline.get_result(processed_tiles) + self.async_pipeline.submit_data(self.crop_tile(image, coord), i, {"coord": coord, "tile_i": i}) + + self.async_pipeline.await_all() + for j in range(processed_tiles, num_tiles): + tile_prediction, meta, feats = self.async_pipeline.get_result(j) + if isinstance(feats[0], np.ndarray): + features.append((feats, meta)) + tile_result = self.postprocess_tile(tile_prediction, *meta["coord"][:2]) + tile_results.append(tile_result) + assert j == num_tiles - 1, "Number of tiles processed does not match number of tiles" + merged_results = self.merge_results(tile_results, image.shape) + merged_features = self.merge_features(features, merged_results) + return merged_results, merged_features + + def postprocess_tile(self, predictions: DetectionResult, offset_x: int, offset_y: int) -> Dict[str, List]: + """Postprocess single tile prediction. + + Args: + predictions (Union[List, Tuple]): predictions from model + offset_x (int): tile offset in x direction + offset_y (int): tile offset in y direction + + Returns: + Dict[str, List]: postprocessed predictions - bboxes and masks + """ + output_dict: dict = {"bboxes": [], "masks": []} + if self.segm: + tile_scores, tile_labels, tile_boxes, tile_masks = predictions + tile_boxes += np.tile([offset_x, offset_y], 2) + out = np.concatenate( + ( + tile_labels[:, np.newaxis], + tile_scores[:, np.newaxis], + tile_boxes, + ), + -1, + ) + output_dict["masks"] = tile_masks + else: + assert isinstance(predictions.objects, list) + out = detection2array(predictions.objects) + out[:, 2:] += np.tile([offset_x, offset_y], 2) + output_dict["bboxes"] = out + return output_dict + + def crop_tile(self, image: np.ndarray, coord: List[int]) -> np.ndarray: + """Crop tile from full image. + + Args: + image (np.ndarray): full-res image + coord (List): tile coordinates + + Returns: + np.ndarray: cropped tile + """ + x1, y1, x2, y2 = coord + return image[y1:y2, x1:x2] + + @staticmethod + def detection2tuple(detections: np.ndarray): + """Convert detection to tuple. + + Args: + detections (np.ndarray): prediction results in numpy array + + Returns: + scores (np.ndarray): scores between 0-1 + labels (np.ndarray): label indices + boxes (np.ndarray): boxes + """ + labels = detections[:, 0] + scores = detections[:, 1] + boxes = detections[:, 2:] + return scores, labels, boxes + + def merge_results(self, results: List[Dict], shape: List[int]): + """Merge results from tiles. + + Args: + results (List[Dict]): list of tile results + shape (List[int]): original full-res image shape + """ + + detections = np.empty((0, 6), dtype=np.float32) + masks = [] + for result in results: + if len(result["bboxes"]): + detections = np.concatenate((detections, result["bboxes"])) + if self.segm: + masks.extend(result["masks"]) + + if np.prod(detections.shape): + detections, keep = multiclass_nms(detections, max_num=self.max_number) + if self.segm: + masks = [masks[keep_idx] for keep_idx in keep] + self.resize_masks(masks, detections, shape) + detections = *Tiler.detection2tuple(detections), masks + return detections + + def merge_features( + self, features: List, predictions: Union[Tuple, np.ndarray] + ) -> Union[Tuple[None, None], List[np.ndarray]]: + """Merge tile-level feature vectors to image-level features. + + Args: + features: tile-level features. + predictions: predictions with masks for whole image. + + Returns: + image_vector (np.ndarray): Merged feature vector for entire image. + image_saliency_map (List): Merged saliency map for entire image + """ + if len(features) == 0: + return (None, None) + image_vector = self.merge_vectors(features) + + (_, image_saliency_map), _ = features[0] + if isinstance(image_saliency_map, np.ndarray): + image_saliency_map = self.merge_maps(features) + else: + # if saliency maps weren't return from hook (Mask RCNN case) + image_saliency_map = self.get_tiling_saliency_map_from_segm_masks(predictions) + + return image_vector, image_saliency_map + + def merge_vectors(self, features: List) -> np.ndarray: + """Merge tile-level feature vectors to image-level feature vector. + + Args: + features: tile-level features. + + Returns: + merged_vectors (np.ndarray): Merged vectors for entire image. + """ + vectors = [vector for (vector, _), _ in features] + return np.average(vectors, axis=0) + + def merge_maps(self, features: List) -> np.ndarray: + """Merge tile-level saliency maps to image-level saliency map. + + Args: + features: tile-level features ((vector, map: np.array), tile_meta). + Each saliency map is a list of maps for each detected class or None if class wasn't detected. + + Returns: + merged_maps (np.ndarray): Merged saliency maps for entire image. + """ + (_, image_saliency_map), image_meta = features[0] + + num_classes, feat_h, feat_w = image_saliency_map.shape + dtype = image_saliency_map[0][0].dtype + + image_h, image_w, _ = image_meta["original_shape"] + ratio = np.array([feat_h / min(self.tile_size, image_h), feat_w / min(self.tile_size, image_w)]) + + image_map_h = int(image_h * ratio[0]) + image_map_w = int(image_w * ratio[1]) + # happens because of the bug then tile_size for IR in a few times more than original image + if image_map_h == 0 or image_map_w == 0: + return [None] * num_classes + merged_map = [np.zeros((image_map_h, image_map_w)) for _ in range(num_classes)] + + for (_, saliency_map), meta in features[1:]: + x_1, y_1, x_2, y_2 = meta["coord"] + y_1, x_1 = ((y_1, x_1) * ratio).astype(np.uint16) + y_2, x_2 = ((y_2, x_2) * ratio).astype(np.uint16) + + map_h, map_w = saliency_map[0].shape + # resize feature map if it got from the tile which width and height is less the tile_size + if (map_h > y_2 - y_1 > 0) and (map_w > x_2 - x_1 > 0): + saliency_map = np.array([cv2.resize(cls_map, (x_2 - x_1, y_2 - y_1)) for cls_map in saliency_map]) + # cut the rest of the feature map that went out of the image borders + map_h, map_w = y_2 - y_1, x_2 - x_1 + + for ci, hi, wi in [(c_, h_, w_) for c_ in range(num_classes) for h_ in range(map_h) for w_ in range(map_w)]: + map_pixel = saliency_map[ci, hi, wi] + # on tile overlap add 0.5 value of each tile + if merged_map[ci][y_1 + hi, x_1 + wi] != 0: + merged_map[ci][y_1 + hi, x_1 + wi] = 0.5 * (map_pixel + merged_map[ci][y_1 + hi, x_1 + wi]) + else: + merged_map[ci][y_1 + hi, x_1 + wi] = map_pixel + + for class_idx in range(num_classes): + image_map_cls = image_saliency_map[class_idx] + # resize the feature map for whole image to add it to merged saliency maps + image_map_cls = cv2.resize(image_map_cls, (image_map_w, image_map_h)) + merged_map[class_idx] += (0.5 * image_map_cls).astype(dtype) + merged_map[class_idx] = non_linear_normalization(merged_map[class_idx]) + return merged_map + + def get_tiling_saliency_map_from_segm_masks(self, detections: Union[Tuple, np.ndarray]) -> List: + """Post process function for saliency map of OTX MaskRCNN model for tiling.""" + + # No detection case + if isinstance(detections, np.ndarray) and detections.size == 0: + return [None] + # Exportable demo case + if self.num_classes == 0: + return [None] + + classes = [int(cls) - 1 for cls in detections[1]] + saliency_maps: List = [None for _ in range(self.num_classes)] + scores = detections[0].reshape(-1, 1, 1) + masks = detections[3] + weighted_masks = masks * scores + for mask, cls in zip(weighted_masks, classes): + if saliency_maps[cls] is None: + saliency_maps[cls] = [mask] + else: + saliency_maps[cls].append(mask) + saliency_maps = self._merge_and_normalize(saliency_maps, self.num_classes) + return saliency_maps + + @staticmethod + def _merge_and_normalize(saliency_maps: List, num_classes: int) -> List: + for i in range(num_classes): + if saliency_maps[i] is not None: + # combine masks for all objects within one class + saliency_maps[i] = np.max(np.array(saliency_maps[i]), axis=0) + + for i in range(num_classes): + per_class_map = saliency_maps[i] + if per_class_map is not None: + max_values = np.max(per_class_map) + per_class_map = 255 * (per_class_map) / (max_values + 1e-12) + per_class_map = per_class_map.astype(np.uint8) + saliency_maps[i] = per_class_map + return saliency_maps + + def resize_masks(self, masks: List, dets: np.ndarray, shape: List[int]): + """Resize Masks. + + Args: + masks (List): list of raw np.ndarray masks + dets (np.ndarray): detections including labels, scores, and boxes + shape (List[int]): original full-res image shape + """ + for i, (det, mask) in enumerate(zip(dets, masks)): + masks[i] = self.model.segm_postprocess(det[2:], mask, *shape[:-1]) diff --git a/src/otx/api/utils/time_utils.py b/src/otx/api/utils/time_utils.py new file mode 100644 index 00000000000..fade0c05a0d --- /dev/null +++ b/src/otx/api/utils/time_utils.py @@ -0,0 +1,149 @@ +"""This module implements time related utility functions.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import datetime +import functools +import time +from typing import Optional + + +def now() -> datetime.datetime: + """Return the current UTC creation_date and time up to a millisecond accuracy. + + This function is preferable over the Python datetime.datetime.now() function + because it uses the same accuracy (milliseconds) as MongoDB rather than microsecond accuracy. + + Returns: + Date and time up to a millisecond precision. + """ + date = datetime.datetime.now(datetime.timezone.utc) + return date.replace(microsecond=(date.microsecond // 1000) * 1000) + + +# Debug tools +def timeit(func): + """This function can be used as a decorator as @timeit. + + It will print out how long the function took to execute. + + Args: + func: The decorated function + + Returns: + The wrapped function + """ + + @functools.wraps(func) + def new_func(*args, **kwargs): + start_time = time.time() + res = func(*args, **kwargs) + elapsed_time = time.time() - start_time + print(f"function [{func.__name__}] finished in {float(elapsed_time * 1000)} ms") + return res + + return new_func + + +class TimeEstimator: + """The time estimator. + + Estimate the remaining time given the progress, and the progress changes. The estimator starts estimation at a + starting progress that is not necessarily 0. This choice is motivated by the fact that the first percent of + progress often takes a much longer time than the following percents. + + Args: + smoothing_factor (float): Smoothing factor for the exponentially + weighted moving average. There's a great explanation at + https://www.wallstreetmojo.com/ewma/ + inflation_factor (float): The factor by which the initial total time + estimation is inflated to ensure decreasing + update_window (float): Last update happened at progress1, next update + will happen at (progress1 + update window) + starting_progress (float): The progress at which the time_remaining + estimation starts time estimation + """ + + # pylint: disable=too-many-instance-attributes + def __init__( + self, + smoothing_factor: float = 0.02, + inflation_factor: float = 1.1, + update_window: float = 1.0, + starting_progress: float = 1.0, + ): + self.estimated_total_time: Optional[float] = None + self.estimated_end_time: Optional[float] = None + self.first_update_progress = starting_progress + self.first_update_time: Optional[float] = None + self.last_update_progress: Optional[float] = None + self.estimated_remaining_time: Optional[float] = None + self.starting_progress = starting_progress + self.smoothing_factor = smoothing_factor + self.inflation_factor = inflation_factor + self.update_window = update_window + + def time_remaining_from_progress(self, progress: float) -> float: + """Updates the current progress, and returns the estimated remaining time in seconds (float). + + Args: + progress (float): The new progress (floating point percentage, 0.0 - 100.0) + + Returns: + The expected remaining time in seconds (float) + """ + estimation = -1.0 + if progress is not None and progress > 0: + self.update(progress) + estimation = self.get_time_remaining() + return estimation + + def get_time_remaining(self): + """If the new estimation is higher than the previous one by up to 2 seconds, return old estimation. + + Returns: + Estimated remaining time in seconds (float) + """ + new_estimation = self.estimated_end_time - time.time() if self.estimated_end_time is not None else -1.0 + if self.estimated_remaining_time is None or not 0.0 < new_estimation - self.estimated_remaining_time < 2.0: + self.estimated_remaining_time = new_estimation + return self.estimated_remaining_time + + def update(self, progress: float): + """Update the estimator with a new progress (floating point percentage, between 0.0 - 100.0). + + Args: + progress (float): Progress of the process + + Returns: + None + """ + if progress >= self.first_update_progress and self.last_update_progress is None: + self.first_update_progress = progress + self.last_update_progress = progress + self.first_update_time = time.time() + + if self.last_update_progress is not None and progress - self.last_update_progress >= self.update_window: + + if self.first_update_time is None or self.first_update_progress is None: + raise AssertionError( + "first_update_time and first_update_progress both can not be None when calling " + "TimeEstimator.update()." + ) + + self.last_update_progress = progress + # normalized progress since starting point for estimation + normalized_progress = (progress - self.first_update_progress) / (100 - self.first_update_progress) + time_elapsed = time.time() - self.first_update_time + estimated_total = time_elapsed / normalized_progress + if self.estimated_total_time is None: + # inflating the initial estimation to ensure decreasing time remaining + self.estimated_total_time = estimated_total * self.inflation_factor + else: + self.estimated_total_time = ( + self.smoothing_factor * self.estimated_total_time + (1 - self.smoothing_factor) * estimated_total + ) + self.estimated_end_time = self.first_update_time + self.estimated_total_time diff --git a/src/otx/api/utils/vis_utils.py b/src/otx/api/utils/vis_utils.py new file mode 100644 index 00000000000..9244d8c8d94 --- /dev/null +++ b/src/otx/api/utils/vis_utils.py @@ -0,0 +1,77 @@ +"""This module implements activation map.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path +from typing import Union + +import cv2 +import numpy as np + + +def get_actmap( + saliency_map: np.ndarray, + output_res: Union[tuple, list], +) -> np.ndarray: + """Get activation map (heatmap) from saliency map. + + It will return activation map from saliency map + + Args: + saliency_map (np.ndarray): Saliency map with pixel values from 0-255 + output_res (Union[tuple, list]): Output resolution + + Returns: + saliency_map (np.ndarray): [H, W, 3] colormap, more red means more salient + + """ + if len(saliency_map.shape) == 3: + saliency_map = saliency_map[0] + + saliency_map = cv2.resize(saliency_map, output_res) + saliency_map = cv2.applyColorMap(saliency_map, cv2.COLORMAP_JET) + return saliency_map + + +def get_input_names_list(input_path: Union[str, int], capture): + """Lists the filenames of all inputs for demo.""" + + # Web camera input + if isinstance(input_path, int): + return [] + if "DIR" in str(capture.get_type()): + return [f.name for f in Path(input_path).iterdir() if f.is_file()] + else: + return [Path(input_path).name] + + +def dump_frames(saved_frames: list, output: str, input_path: Union[str, int], capture): + """Saves images/videos with predictions from saved_frames to output folder with proper names.""" + + if not saved_frames: + return + + output_path = Path(output) + if not output_path.exists(): + output_path.mkdir(parents=True) + + filenames = get_input_names_list(input_path, capture) + + if "VIDEO" in str(capture.get_type()): + filename = filenames[0] + w, h, _ = saved_frames[0].shape + video_path = str(output_path / filename) + codec = cv2.VideoWriter_fourcc(*"mp4v") + out = cv2.VideoWriter(video_path, codec, capture.fps(), (h, w)) + for frame in saved_frames: + out.write(frame) + out.release() + print(f"Video was saved to {video_path}") + else: + if len(filenames) != len(saved_frames): + filenames = [f"output_{i}.jpeg" for i, _ in enumerate(saved_frames)] + for filename, frame in zip(filenames, saved_frames): + image_path = str(output_path / filename) + cv2.imwrite(image_path, frame) + print(f"Image was saved to {image_path}") diff --git a/src/otx/cli/__init__.py b/src/otx/cli/__init__.py index 6cd99560005..67cfc327a1f 100644 --- a/src/otx/cli/__init__.py +++ b/src/otx/cli/__init__.py @@ -1,23 +1,15 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""CLI entrypoints.""" -from datetime import timedelta -from time import time - -from otx.cli.cli import OTXCLI +"""OTX CLI.""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 -def main() -> None: - """Entry point for OTX CLI. - - This function is a single entry point for all OTX CLI related operations: - """ - start = time() - OTXCLI() - dt = timedelta(seconds=time() - start) - print(f"Elapsed time: {dt}") +import os +# This must be called before the first TaskType is imported to take effect. +# How to make an Action Template invisible in Geti +# Check FEATURE_FLAGS_OTX_ACTION_TASKS in the API to determine whether to use the Action +# Always 1 in the OTX CLI +os.environ["FEATURE_FLAGS_OTX_ACTION_TASKS"] = "1" -if __name__ == "__main__": - main() +# Same logic is applied to visual prompting task to be invisible in Geti +os.environ["FEATURE_FLAGS_OTX_VISUAL_PROMPTING_TASKS"] = "1" diff --git a/src/otx/cli/builder/__init__.py b/src/otx/cli/builder/__init__.py new file mode 100644 index 00000000000..6a2cc160f14 --- /dev/null +++ b/src/otx/cli/builder/__init__.py @@ -0,0 +1,21 @@ +"""Model templates builder.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .builder import Builder + +__all__ = [ + "Builder", +] diff --git a/src/otx/cli/builder/builder.py b/src/otx/cli/builder/builder.py new file mode 100644 index 00000000000..e343e2ed97d --- /dev/null +++ b/src/otx/cli/builder/builder.py @@ -0,0 +1,241 @@ +"""Builder Class for training template. + +For user's various use cases and convenient CLI, +It is an internal Builder class used in the otx build command +that enables the configuration of the basic workspace of OTX +and supports the replacement of the backbone of the model. +""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import inspect +from pathlib import Path +from typing import Any, Dict, Union + +import mmcv +import torch +from mmcv.utils import Registry, build_from_cfg +from torch import nn + +from otx.algorithms import TRANSFORMER_BACKBONES +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.api.entities.model_template import TaskType +from otx.cli.utils.importing import ( + get_backbone_list, + get_backbone_registry, + get_module_args, +) + +# pylint: disable=too-many-locals, too-many-statements, too-many-branches + + +def get_backbone_out_channels(backbone: nn.Module): + """Get output channels of backbone using fake data.""" + out_channels = [] + input_size = backbone.input_size if hasattr(backbone, "input_size") else 64 + fake_data = torch.rand(2, 3, input_size, input_size) + outputs = backbone(fake_data) + for out in outputs: + out_channels.append(out.shape[1]) + return out_channels + + +def update_backbone_args(backbone_config: dict, registry: Registry, backend: str): + """Update Backbone required arguments. + + This function checks the init parameters of the corresponding backbone function (or class) + and identifies the required arguments. + Also, it distinguishes the argment needed for the build to add convenience to the user. + """ + backbone_module = registry.get(backbone_config["type"]) + if not backbone_module: + raise ValueError(f"{backbone_config['type']} is not supported backbone") + required_args, default_args = get_module_args(backbone_module) + for arg_key, default_value in default_args.items(): + if arg_key not in backbone_config: + backbone_config[arg_key] = default_value + + missing_args = [] + for arg in required_args: + if arg not in backbone_config: + missing_args.append(arg) + if len(missing_args) > 0: + print( + f"[*] {backbone_config['type']} requires the argument : {missing_args}" + f"\n[*] Please refer to {inspect.getfile(backbone_module)}" + ) + if "out_indices" in backbone_config: + backbone_config["use_out_indices"] = True + else: + backbone_config["use_out_indices"] = False + + updated_missing_args = [] + backbone_type = backbone_config["type"] + backbone_list = get_backbone_list(backend) + if backbone_type not in backbone_list: + return missing_args + backbone_data = backbone_list[backbone_type] + # Patch missing_args + for arg in missing_args: + if "options" in backbone_data and arg in backbone_data["options"]: + backbone_config[arg] = backbone_data["options"][arg][0] + print( + f"[*] '{arg}' can choose between: {backbone_data['options'][arg]}" + f"\n[*] '{arg}' default value: {backbone_config[arg]}" + ) + else: + backbone_config[arg] = "!!!!!!!!!!!INPUT_HERE!!!!!!!!!!!" + updated_missing_args.append(arg) + return updated_missing_args + + +def update_channels(model_config: OTXConfig, out_channels: Any): + """Update in_channel of head or neck.""" + if hasattr(model_config.model, "neck") and model_config.model.neck: + if model_config.model.neck.get("type", None) == "GlobalAveragePooling": + model_config.model.neck.pop("in_channels", None) + else: + print(f"\tUpdate model.neck.in_channels: {out_channels}") + model_config.model.neck.in_channels = out_channels + + elif hasattr(model_config.model, "decode_head"): + head_in_index = model_config.model.decode_head.get("in_index", None) + if head_in_index and len(out_channels) != len(head_in_index): + updated_in_index = list(range(len(out_channels))) + print(f"\tUpdate model.decode_head.in_index: {updated_in_index}") + model_config.model.decode_head.in_index = updated_in_index + print(f"\tUpdate model.decode_head.in_channels: {out_channels}") + model_config.model.decode_head.in_channels = out_channels + + elif hasattr(model_config.model, "head"): + print(f"\tUpdate model.head.in_channels: {out_channels}") + model_config.model.head.in_channels = out_channels + else: + raise NotImplementedError("This architecture currently does not support public backbone.") + + +class Builder: + """Class that implements a model templates registry.""" + + def build_backbone_config(self, backbone_type: str, output_path: Union[Path, str]): + """Build Backbone configs from backbone type. + + This is a function that makes the configuration + of the usable backbone found by the user through otx find. + backbone_type: The type of backbone want to get - {backend.backbone_type} (str) + output_path: new backbone configuration file output path (Union[Path, str]) + """ + print(f"[*] Backbone Config: {backbone_type}") + output_path = output_path if isinstance(output_path, Path) else Path(output_path) + + backend, backbone_class = Registry.split_scope_key(backbone_type) + backbone_config: Dict[str, Any] = dict(type=backbone_type) + if backbone_class == "MMOVBackbone": + backend = f"omz.{backend}" + backbone_config["verify_shape"] = False + backbone_registry, _ = get_backbone_registry(backend) + missing_args = update_backbone_args(backbone_config, backbone_registry, backend) + if str(output_path).endswith((".yml", ".yaml", ".json")): + mmcv.dump({"backbone": backbone_config}, str(output_path.absolute())) + print(f"[*] Save backbone configuration: {str(output_path.absolute())}") + else: + raise ValueError("The backbone config support file format is as follows: (.yml, .yaml, .json)") + return missing_args + + def merge_backbone( + self, + model_config_path: Union[Path, str], + backbone_config_path: Union[Path, str], + output_path: Union[Path, str] = None, + ): + """Build model & update backbone configs. + + This is a function that updates the existing model to be able to build + through the backbone configuration file or backbone type. + model_config_path: model configuration file path (Union[Path, str]) + backbone_config_path: backbone configuration file path (Union[Path, str]) + output_path: new model.py output path (Union[Path, str]) + """ + print(f"[*] Update {model_config_path} with {backbone_config_path}") + model_config_path = model_config_path if isinstance(model_config_path, Path) else Path(model_config_path) + backbone_config_path = ( + backbone_config_path if isinstance(backbone_config_path, Path) else Path(backbone_config_path) + ) + + # Get Model config from model config file + if model_config_path.exists(): + model_config = OTXConfig.fromfile(str(model_config_path)) + print(f"\tTarget Model: {model_config.model.type}") + else: + raise ValueError(f"[*] The model is not properly defined or not found: {model_config_path}") + + # Get Backbone config from config file + if backbone_config_path.exists(): + backbone_config = mmcv.load(str(backbone_config_path)) + else: + raise ValueError(f"[*] The backbone is not found: {str(backbone_config_path)}") + + if "backbone" in backbone_config: + backbone_config = backbone_config["backbone"] + + # Get Backbone configuration + backend, backbone_class = Registry.split_scope_key(backbone_config["type"]) + backend = f"omz.{backend}" if backbone_class == "MMOVBackbone" else backend + print(f"\tTarget Backbone: {backbone_config['type']}") + otx_registry, custom_imports = get_backbone_registry(backend) + + # Update out_indices of backbone + if backbone_config["use_out_indices"]: + model_in_indices = [] + if "backbone" in model_config.model: + model_in_indices = model_config.model.backbone.get("out_indices", []) + backbone_out_indices = backbone_config.get("out_indices", None) + if not backbone_out_indices and model_in_indices: + # Check out_indices vs num_stage + backbone_config["out_indices"] = model_in_indices + backbone_config.pop("use_out_indices", None) + + # Build Backbone + backbone = build_from_cfg(backbone_config, otx_registry, None) + if model_config.model.get("task", None) == str(TaskType.CLASSIFICATION).lower(): + # Update model layer's in/out configuration in ClsStage.configure_model + out_channels = -1 + if hasattr(model_config.model, "head"): + model_config.model.head.in_channels = -1 + # TODO: This is a hard coded part of the Transformer backbone and needs to be refactored. + if backend == "mmcls" and backbone_class in TRANSFORMER_BACKBONES: + if hasattr(model_config.model, "neck"): + model_config.model.neck = None + if hasattr(model_config.model, "head"): + model_config.model.head["type"] = "VisionTransformerClsHead" + else: + # Need to update in/out channel configuration here + out_channels = get_backbone_out_channels(backbone) + update_channels(model_config, out_channels) + + # Update Model Configuration + if backend in ("torchvision"): + backbone_config["init_cfg"] = {"Pretrained": True} + print(f"\tBackbone config: {backbone_config}") + model_config.model.backbone = backbone_config + model_config.load_from = None + + if custom_imports: + model_config["custom_imports"] = dict(imports=custom_imports, allow_failed_imports=False) + + # Dump or create model config file + if output_path is None: + output_path = model_config_path + model_config.dump(str(output_path)) + print(f"[*] Save model configuration: {str(output_path)}") diff --git a/src/otx/cli/builder/supported_backbone/__init__.py b/src/otx/cli/builder/supported_backbone/__init__.py new file mode 100644 index 00000000000..9abd8675e60 --- /dev/null +++ b/src/otx/cli/builder/supported_backbone/__init__.py @@ -0,0 +1,4 @@ +"""Supported backbone lists.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/cli/builder/supported_backbone/mmcls.json b/src/otx/cli/builder/supported_backbone/mmcls.json new file mode 100644 index 00000000000..71f10692aa5 --- /dev/null +++ b/src/otx/cli/builder/supported_backbone/mmcls.json @@ -0,0 +1,347 @@ +{ + "version": "0.25.0", + "backbones": { + "mmcls.AlexNet": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "mmcls.Conformer": { + "required": ["arch"], + "options": { + "arch": ["tiny", "small", "base"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.ConvMixer": { + "required": ["arch"], + "options": { + "arch": ["768/32", "1024/20", "1536/20"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.ConvNeXt": { + "required": ["arch"], + "options": { + "arch": ["tiny", "small", "base", "large", "xlarge"] + }, + "available": [] + }, + "mmcls.CSPDarkNet": { + "required": ["depth"], + "options": { + "depth": [53] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.CSPResNet": { + "required": ["depth"], + "options": { + "depth": [50] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.CSPResNeXt": { + "required": ["depth"], + "options": { + "depth": [50] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.DistilledVisionTransformer": { + "required": [], + "options": {}, + "available": [] + }, + "mmcls.DenseNet": { + "required": ["arch"], + "options": { + "arch": ["121", "169", "201", "161"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.EfficientFormer": { + "required": ["arch"], + "options": { + "arch": ["l1", "l3", "l7"] + }, + "available": [] + }, + "mmcls.EfficientNet": { + "required": ["arch"], + "options": { + "arch": [ + "b0", + "b1", + "b2", + "b3", + "b4", + "b5", + "b6", + "b7", + "b8", + "es", + "em", + "el" + ] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.HorNet": { + "required": ["arch"], + "options": { + "arch": [ + "tiny", + "tiny-gf", + "small", + "small-gf", + "base", + "base-gf", + "base-gf384", + "large", + "large-gf", + "large-gf384" + ] + }, + "available": [] + }, + "mmcls.HRNet": { + "required": ["arch"], + "options": { + "arch": ["w32", "w18", "w30", "w40", "w44", "w48", "w64"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.LeNet5": { + "required": [], + "options": {}, + "available": [] + }, + "mmcls.MlpMixer": { + "required": ["arch"], + "options": { + "arch": ["base", "small", "large"] + }, + "available": [] + }, + "mmcls.MobileNetV2": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "mmcls.MobileNetV3": { + "required": ["arch"], + "options": { + "arch": ["small", "large"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.MViT": { + "required": ["arch"], + "options": { + "arch": ["base", "tiny", "small", "large"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.PoolFormer": { + "required": ["arch"], + "options": { + "arch": ["s12", "s24", "s36", "m36", "m48"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.RegNet": { + "required": ["arch"], + "options": { + "arch": [ + "regnetx_400mf", + "regnetx_800mf", + "regnetx_1.6gf", + "regnetx_3.2gf", + "regnetx_4.0gf", + "regnetx_6.4gf", + "regnetx_8.0gf", + "regnetx_12gf" + ] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.RepMLPNet": { + "required": ["arch"], + "options": { + "arch": ["base"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.RepVGG": { + "required": ["arch"], + "options": { + "arch": [ + "A0", + "A1", + "A2", + "B0", + "B1", + "B1g2", + "B1g4", + "B2", + "B2g2", + "B2g4", + "B3", + "B3g2", + "B3g4", + "D2se", + "yolox-pai-small" + ] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.Res2Net": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.ResNeSt": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152, 200, 269] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.ResNet": { + "required": ["depth"], + "options": { + "depth": [18, 34, 50, 101, 152] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.ResNetV1c": { + "required": ["depth"], + "options": { + "depth": [18, 34, 50, 101, 152] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.ResNetV1d": { + "required": ["depth"], + "options": { + "depth": [18, 34, 50, 101, 152] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.ResNet_CIFAR": { + "required": ["depth"], + "options": { + "depth": [18, 34, 50, 101, 152] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.ResNeXt": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.SEResNet": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.SEResNeXt": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.ShuffleNetV1": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "mmcls.ShuffleNetV2": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "mmcls.SwinTransformer": { + "required": ["arch"], + "options": { + "arch": ["tiny", "small", "base", "large"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.SwinTransformerV2": { + "required": ["arch", "pad_small_map"], + "options": { + "arch": ["tiny", "small", "base", "large", "huge", "giant"], + "pad_small_map": [true, false] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.T2T_ViT": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "mmcls.TIMMBackbone": { + "required": ["model_name"], + "options": {}, + "available": [] + }, + "mmcls.TNT": { + "required": ["arch"], + "options": { + "arch": ["base", "small"] + }, + "available": [] + }, + "mmcls.PCPVT": { + "required": ["arch"], + "options": { + "arch": ["base", "small", "large"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.SVT": { + "required": ["arch"], + "options": { + "arch": ["base", "small", "large"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.VAN": { + "required": ["arch"], + "options": { + "arch": ["b0", "b1", "b2", "b3", "b4", "b5", "b6"] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.VGG": { + "required": ["depth"], + "options": { + "depth": [11, 13, 16, 19] + }, + "available": ["CLASSIFICATION"] + }, + "mmcls.VisionTransformer": { + "required": ["arch"], + "options": { + "arch": [ + "base", + "small", + "large", + "deit-tiny", + "deit-small", + "deit-base" + ] + }, + "available": ["CLASSIFICATION"] + } + } +} diff --git a/src/otx/cli/builder/supported_backbone/mmdet.json b/src/otx/cli/builder/supported_backbone/mmdet.json new file mode 100644 index 00000000000..ee663e9f8a1 --- /dev/null +++ b/src/otx/cli/builder/supported_backbone/mmdet.json @@ -0,0 +1,227 @@ +{ + "version": "2.28.1", + "backbones": { + "mmdet.CSPDarknet": { + "required": ["arch"], + "options": { + "arch": ["P5", "P6"] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.Darknet": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.DetectoRS_ResNet": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.DetectoRS_ResNeXt": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.EfficientNet": { + "required": ["arch"], + "options": { + "arch": [ + "b0", + "b1", + "b2", + "b3", + "b4", + "b5", + "b6", + "b7", + "b8", + "es", + "em", + "el" + ] + }, + "available": ["CLASSIFICATION"] + }, + "mmdet.HourglassNet": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.HRNet": { + "required": ["extra"], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.MobileNetV2": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.PyramidVisionTransformer": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "DETECTION", "SEGMENTATION"] + }, + "mmdet.PyramidVisionTransformerV2": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "DETECTION", "SEGMENTATION"] + }, + "mmdet.RegNet": { + "required": ["arch"], + "options": { + "arch": [ + "regnetx_400mf", + "regnetx_800mf", + "regnetx_1.6gf", + "regnetx_3.2gf", + "regnetx_4.0gf", + "regnetx_6.4gf", + "regnetx_8.0gf", + "regnetx_12gf" + ] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.Res2Net": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.ResNeSt": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152, 200] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.ResNet": { + "required": ["depth"], + "options": { + "depth": [18, 34, 50, 101, 152] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.ResNetV1d": { + "required": ["depth"], + "options": { + "depth": [18, 34, 50, 101, 152] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.ResNeXt": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.SSDVGG": { + "required": ["input_size", "depth"], + "options": { + "input_size": [300, 512], + "depth": [11, 16, 19] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmdet.SwinTransformer": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "DETECTION", "SEGMENTATION"] + }, + "mmdet.TridentResNet": { + "required": [ + "depth", + "num_branch", + "test_branch_idx", + "trident_dilations" + ], + "options": { + "depth": [18, 34, 50, 101, 152] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + } + } +} diff --git a/src/otx/cli/builder/supported_backbone/mmseg.json b/src/otx/cli/builder/supported_backbone/mmseg.json new file mode 100644 index 00000000000..cb059a3d385 --- /dev/null +++ b/src/otx/cli/builder/supported_backbone/mmseg.json @@ -0,0 +1,202 @@ +{ + "version": "0.30.0", + "backbones": { + "mmseg.BEiT": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "mmseg.BiSeNetV1": { + "required": ["backbone_cfg"], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmseg.BiSeNetV2": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "DETECTION", "SEGMENTATION"] + }, + "mmseg.CGNet": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmseg.ERFNet": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "mmseg.FastSCNN": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "DETECTION", "SEGMENTATION"] + }, + "mmseg.HRNet": { + "required": ["extra"], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmseg.ICNet": { + "required": ["backbone_cfg"], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmseg.MAE": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "mmseg.MixVisionTransformer": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "DETECTION", "SEGMENTATION"] + }, + "mmseg.MobileNetV2": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmseg.MobileNetV3": { + "required": ["arch"], + "options": { + "arch": ["small", "large"] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmseg.ResNeSt": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152, 200] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmseg.ResNet": { + "required": ["depth"], + "options": { + "depth": [18, 34, 50, 101, 152] + }, + "available": ["CLASSIFICATION", "INSTANCE_SEGMENTATION", "SEGMENTATION"] + }, + "mmseg.ResNetV1c": { + "required": ["depth"], + "options": { + "depth": [18, 34, 50, 101, 152] + }, + "available": ["CLASSIFICATION", "INSTANCE_SEGMENTATION", "SEGMENTATION"] + }, + "mmseg.ResNetV1d": { + "required": ["depth"], + "options": { + "depth": [18, 34, 50, 101, 152] + }, + "available": ["CLASSIFICATION", "INSTANCE_SEGMENTATION", "SEGMENTATION"] + }, + "mmseg.ResNeXt": { + "required": ["depth"], + "options": { + "depth": [50, 101, 152] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmseg.STDCContextPathNet": { + "required": ["backbone_cfg"], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmseg.STDCNet": { + "required": [ + "stdc_type", + "in_channels", + "channels", + "bottleneck_type", + "norm_cfg", + "act_cfg" + ], + "options": { + "stdc_type": ["STDCNet1", "STDCNet2"], + "bottleneck_type": ["add", "cat"] + }, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "mmseg.SwinTransformer": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "DETECTION", "SEGMENTATION"] + }, + "mmseg.TIMMBackbone": { + "required": ["model_name"], + "options": {}, + "available": [] + }, + "mmseg.PCPVT": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "DETECTION", "SEGMENTATION"] + }, + "mmseg.SVT": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "DETECTION", "SEGMENTATION"] + }, + "mmseg.UNet": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "DETECTION", "SEGMENTATION"] + }, + "mmseg.VisionTransformer": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + } + } +} diff --git a/src/otx/cli/builder/supported_backbone/omz.mmcls.json b/src/otx/cli/builder/supported_backbone/omz.mmcls.json new file mode 100644 index 00000000000..da74f1da8d3 --- /dev/null +++ b/src/otx/cli/builder/supported_backbone/omz.mmcls.json @@ -0,0 +1,55 @@ +{ + "version": "1.0.0", + "backbones": { + "mmcls.MMOVBackbone": { + "required": ["model_path_or_model"], + "options": { + "model_path_or_model": [ + "omz://alexnet", + "omz://caffenet", + "omz://densenet-121", + "omz://densenet-121-tf", + "omz://dla-34", + "omz://efficientnet-b0", + "omz://efficientnet-b0-pytorch", + "omz://efficientnet-v2-b0", + "omz://efficientnet-v2-s", + "omz://hbonet-1.0", + "omz://hbonet-0.25", + "omz://googlenet-v1", + "omz://googlenet-v1-tf", + "omz://googlenet-v2", + "omz://googlenet-v2-tf", + "omz://googlenet-v3", + "omz://googlenet-v3-pytorch", + "omz://googlenet-v4-tf", + "omz://inception-resnet-v2-tf", + "omz://mixnet-l", + "omz://mobilenet-v1-0.25-128", + "omz://mobilenet-v1-1.0-224", + "omz://mobilenet-v1-1.0-224-tf", + "omz://mobilenet-v2", + "omz://mobilenet-v2-1.0-224", + "omz://mobilenet-v2-pytorch", + "omz://mobilenet-v2-1.4-224", + "omz://mobilenet-v3-small-1.0-224-tf", + "omz://mobilenet-v3-large-1.0-224-tf", + "omz://octave-resnet-26-0.25", + "omz://resnet-18-pytorch", + "omz://resnet-34-pytorch", + "omz://resnet-50-pytorch", + "omz://resnet-50-tf", + "omz://se-inception", + "omz://se-resnet-50", + "omz://se-resnext-50", + "omz://shufflenet-v2-x0.5", + "omz://squeezenet1.0", + "omz://squeezenet1.1", + "omz://vgg16", + "omz://vgg19" + ] + }, + "available": ["CLASSIFICATION"] + } + } +} diff --git a/src/otx/cli/builder/supported_backbone/otx.json b/src/otx/cli/builder/supported_backbone/otx.json new file mode 100644 index 00000000000..b787b0fe4eb --- /dev/null +++ b/src/otx/cli/builder/supported_backbone/otx.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0", + "backbones": { + "otx.OTXEfficientNet": { + "required": ["version"], + "options": { + "version": ["b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8"] + }, + "available": ["CLASSIFICATION"] + }, + "otx.OTXEfficientNetV2": { + "required": ["version"], + "options": { + "version": ["s_21k", "s_1k", "m_21k", "b0"] + }, + "available": ["CLASSIFICATION"] + }, + "otx.OTXMobileNetV3": { + "required": ["mode"], + "options": { + "mode": ["small", "large"] + }, + "available": ["CLASSIFICATION"] + } + } +} diff --git a/src/otx/cli/builder/supported_backbone/torchvision.json b/src/otx/cli/builder/supported_backbone/torchvision.json new file mode 100644 index 00000000000..0c0ec861d8f --- /dev/null +++ b/src/otx/cli/builder/supported_backbone/torchvision.json @@ -0,0 +1,645 @@ +{ + "version": "0.14.1", + "backbones": { + "torchvision.alexnet": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION", "INSTANCE_SEGMENTATION", "SEGMENTATION"] + }, + "torchvision.convnext_tiny": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.convnext_small": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.convnext_large": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.densenet121": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.densenet161": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.densenet169": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.densenet201": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_b0": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_b1": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_b2": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_b3": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_b4": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_b5": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_b6": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_b7": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_v2_s": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_v2_m": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.efficientnet_v2_l": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.googlenet": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.inception_v3": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.mnasnet0_5": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.mnasnet0_75": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.mnasnet1_0": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.mnasnet1_3": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.mobilenet_v2": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.mobilenet_v3_large": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.mobilenet_v3_small": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.regnet_y_400mf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_y_800mf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_y_1_6gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_y_3_2gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_y_8gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_y_16gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_y_32gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_y_128gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_x_400mf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_x_800mf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_x_1_6gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_x_3_2gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_x_8gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_x_16gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.regnet_x_32gf": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.resnet18": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.resnet34": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.resnet50": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.resnet101": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.resnet152": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.resnext50_32x4d": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.resnext101_32x8d": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.resnext101_64x4d": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.wide_resnet50_2": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.wide_resnet101_2": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.shufflenet_v2_x0_5": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.shufflenet_v2_x1_0": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.shufflenet_v2_x1_5": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.shufflenet_v2_x2_0": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.squeezenet1_0": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.squeezenet1_1": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.vgg11": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.vgg11_bn": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.vgg13": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.vgg13_bn": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.vgg16": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.vgg16_bn": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.vgg19": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.vgg19_bn": { + "required": [], + "options": {}, + "available": [ + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION" + ] + }, + "torchvision.vit_b_16": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.vit_b_32": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.vit_l_16": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.vit_l_32": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.vit_h_14": { + "required": [], + "options": {}, + "available": [] + }, + "torchvision.swin_t": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "torchvision.swin_s": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "torchvision.swin_b": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "torchvision.swin_v2_t": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "torchvision.swin_v2_s": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "torchvision.swin_v2_b": { + "required": [], + "options": {}, + "available": ["CLASSIFICATION"] + }, + "torchvision.maxvit_t": { + "required": [], + "options": {}, + "available": [] + } + } +} diff --git a/src/otx/cli/cli.py b/src/otx/cli/cli.py deleted file mode 100644 index 1dfaaf180a1..00000000000 --- a/src/otx/cli/cli.py +++ /dev/null @@ -1,580 +0,0 @@ -"""CLI entrypoints.""" - -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - - -from __future__ import annotations - -import dataclasses -import sys -from pathlib import Path -from typing import TYPE_CHECKING, Any, Optional -from warnings import warn - -import yaml -from jsonargparse import ActionConfigFile, ArgumentParser, Namespace, namespace_to_dict -from rich.console import Console - -from otx import OTX_LOGO, __version__ -from otx.cli.utils import absolute_path -from otx.cli.utils.help_formatter import CustomHelpFormatter -from otx.cli.utils.jsonargparse import add_list_type_arguments, get_short_docstring, patch_update_configs -from otx.cli.utils.workspace import Workspace -from otx.core.types.task import OTXTaskType -from otx.core.utils.imports import get_otx_root_path - -if TYPE_CHECKING: - from jsonargparse._actions import _ActionSubCommands - - from otx.core.metrics import MetricCallable - -_ENGINE_AVAILABLE = True -try: - from otx.core.config import register_configs - from otx.engine import Engine - - register_configs() -except ImportError: - _ENGINE_AVAILABLE = False - - -class OTXCLI: - """OTX CLI entrypoint.""" - - def __init__(self, args: list[str] | None = None, run: bool = True) -> None: - """Initialize OTX CLI.""" - self.console = Console() - self._subcommand_method_arguments: dict[str, list[str]] = {} - with patch_update_configs(): - self.parser = self.init_parser() - self.add_subcommands() - self.config = self.parser.parse_args(args=args, _skip_check=True) - - self.subcommand = self.config["subcommand"] - if run: - self.run() - - def init_parser(self) -> ArgumentParser: - """Initialize the argument parser for the OTX CLI. - - Returns: - ArgumentParser: The initialized argument parser. - """ - parser = ArgumentParser( - description="OpenVINO Training-Extension command line tool", - env_prefix="otx", - parser_mode="omegaconf", - formatter_class=CustomHelpFormatter, - ) - parser.add_argument( - "-v", - "--version", - action="version", - version=f"%(prog)s {__version__}", - help="Display OTX version number.", - ) - return parser - - @staticmethod - def engine_subcommand_parser(subcommand: str, **kwargs) -> tuple[ArgumentParser, list]: - """Creates an ArgumentParser object for the engine subcommand. - - Args: - **kwargs: Additional keyword arguments to be passed to the ArgumentParser constructor. - - Returns: - ArgumentParser: The created ArgumentParser object. - """ - parser = ArgumentParser( - formatter_class=CustomHelpFormatter, - parser_mode="omegaconf", - **kwargs, - ) - parser.add_argument( - "-v", - "--verbose", - action="count", - help="Verbose mode. This shows a configuration argument that allows for more specific overrides. \ - Multiple -v options increase the verbosity. The maximum is 2.", - ) - parser.add_argument( - "-c", - "--config", - action=ActionConfigFile, - help="Path to a configuration file in json or yaml format.", - ) - parser.add_argument( - "--data_root", - type=absolute_path, - help="Path to dataset root.", - ) - parser.add_argument( - "--work_dir", - type=absolute_path, - default=absolute_path(Path.cwd()), - help="Path to work directory. The default is created as otx-workspace.", - ) - parser.add_argument( - "--task", - type=str, - help="Task Type.", - ) - parser.add_argument( - "--seed", - type=int, - help="Sets seed for pseudo-random number generators in: pytorch, numpy, python.random.", - ) - parser.add_argument( - "--callback_monitor", - type=str, - help="The metric to monitor the model performance during training callbacks.", - ) - parser.add_argument( - "--disable-infer-num-classes", - help="OTX automatically infers num_classes from the given dataset " - "and applies it to the model initialization." - "Consequently, there might be a mismatch with the provided model configuration during runtime. " - "Setting this option to true will disable this behavior.", - action="store_true", - ) - engine_skip = {"model", "datamodule", "optimizer", "scheduler", "work_dir"} - parser.add_class_arguments( - Engine, - "engine", - fail_untyped=False, - sub_configs=True, - instantiate=False, - skip=engine_skip, - ) - # Model Settings - from otx.core.model.entity.base import OTXModel - - model_kwargs: dict[str, Any] = {"fail_untyped": False} - - parser.add_subclass_arguments( - OTXModel, - "model", - required=False, - **model_kwargs, - ) - # Datamodule Settings - from otx.core.data.module import OTXDataModule - - parser.add_class_arguments( - OTXDataModule, - "data", - fail_untyped=False, - sub_configs=True, - ) - # Optimizer & Scheduler Settings - from lightning.pytorch.cli import LRSchedulerTypeUnion, ReduceLROnPlateau - from torch.optim import Optimizer - from torch.optim.lr_scheduler import LRScheduler - - add_list_type_arguments( - parser, - baseclass=(Optimizer, list[Optimizer]), - nested_key="optimizer", - skip={"params"}, - ) - add_list_type_arguments( - parser, - baseclass=(LRScheduler, ReduceLROnPlateau, list[LRSchedulerTypeUnion]), - nested_key="scheduler", - skip={"optimizer"}, - ) - - parser.add_class_arguments(Workspace, "workspace") - parser.link_arguments("work_dir", "workspace.work_dir") - - parser.link_arguments("data_root", "engine.data_root") - parser.link_arguments("data_root", "data.config.data_root") - parser.link_arguments("engine.device", "data.config.device") - - added_arguments = parser.add_method_arguments( - Engine, - subcommand, - skip=set(OTXCLI.engine_subcommands()[subcommand]), - fail_untyped=False, - ) - - if "callbacks" in added_arguments: - parser.link_arguments("callback_monitor", "callbacks.init_args.monitor") - parser.link_arguments("workspace.work_dir", "callbacks.init_args.dirpath", apply_on="instantiate") - if "logger" in added_arguments: - parser.link_arguments("workspace.work_dir", "logger.init_args.save_dir", apply_on="instantiate") - parser.link_arguments("workspace.work_dir", "logger.init_args.log_dir", apply_on="instantiate") - if "checkpoint" in added_arguments and "--checkpoint" in sys.argv: - # This is code for an OVModel that uses checkpoint in model.model_name. - parser.link_arguments("checkpoint", "model.init_args.model_name") - - # Load default subcommand config file - default_config_file = get_otx_root_path() / "recipe" / "_base_" / f"{subcommand}.yaml" - if default_config_file.exists(): - with Path(default_config_file).open() as f: - default_config = yaml.safe_load(f) - parser.set_defaults(**default_config) - - return parser, added_arguments - - @staticmethod - def engine_subcommands() -> dict[str, set[str]]: - """Returns dictionary the subcommands of engine, and whose value is the argument to be skipped in the CLI. - - This allows the CLI to skip duplicate keys when creating the Engine and when running the subcommand. - - Returns: - A dictionary where the keys are the subcommands and the values are sets of skipped arguments. - """ - device_kwargs = {"accelerator", "devices"} - return { - "train": {"seed"}.union(device_kwargs), - "test": {"datamodule"}.union(device_kwargs), - "predict": {"datamodule"}.union(device_kwargs), - "export": device_kwargs, - "optimize": {"datamodule"}.union(device_kwargs), - "explain": {"datamodule"}.union(device_kwargs), - } - - def add_subcommands(self) -> None: - """Adds subcommands to the CLI parser. - - This method initializes and configures subcommands for the OTX CLI parser. - It iterates over the available subcommands, adds arguments specific to each subcommand, - and registers them with the parser. - - Returns: - None - """ - self._subcommand_parsers: dict[str, ArgumentParser] = {} - parser_subcommands = self.parser.add_subcommands() - self._set_extension_subcommands_parser(parser_subcommands) - if not _ENGINE_AVAILABLE: - # If environment is not configured to use Engine, do not add a subcommand for Engine. - return - for subcommand in self.engine_subcommands(): - # If already have a workspace or run it from the root of a workspace, utilize config and checkpoint in cache - root_dir = Path(sys.argv[sys.argv.index("--work_dir") + 1]) if "--work_dir" in sys.argv else Path.cwd() - self.cache_dir = root_dir / ".latest" / "train" # The config and checkpoint used in the latest training. - - parser_kwargs = self._set_default_config() - sub_parser, added_arguments = self.engine_subcommand_parser(subcommand=subcommand, **parser_kwargs) - if "checkpoint" in added_arguments and self.cache_dir.exists(): - self._load_cache_ckpt(parser=sub_parser) - - fn = getattr(Engine, subcommand) - description = get_short_docstring(fn) - - self._subcommand_method_arguments[subcommand] = added_arguments - self._subcommand_parsers[subcommand] = sub_parser - parser_subcommands.add_subcommand(subcommand, sub_parser, help=description) - - def _load_cache_ckpt(self, parser: ArgumentParser) -> None: - checkpoint_dir = self.cache_dir / "checkpoints" - if not checkpoint_dir.exists(): - return - ckpt_files = list(checkpoint_dir.glob("epoch_*.ckpt")) - if not ckpt_files: - return - latest_checkpoint = max(ckpt_files, key=lambda p: p.stat().st_mtime) - parser.set_defaults(checkpoint=str(latest_checkpoint)) - if "--print_config" not in sys.argv: - warn(f"Load default checkpoint from {latest_checkpoint}.", stacklevel=0) - - def _set_default_config(self) -> dict: - parser_kwargs = {} - if (self.cache_dir / "configs.yaml").exists(): - parser_kwargs["default_config_files"] = [str(self.cache_dir / "configs.yaml")] - if "--print_config" not in sys.argv: - warn(f"Load default config from {self.cache_dir / 'configs.yaml'}.", stacklevel=0) - return parser_kwargs - - # If don't use cache, use the default config from auto configuration. - data_root = None - task = None - if "--data_root" in sys.argv: - data_root = sys.argv[sys.argv.index("--data_root") + 1] - if "--task" in sys.argv: - task = sys.argv[sys.argv.index("--task") + 1] - enable_auto_config = data_root is not None and "--config" not in sys.argv - if enable_auto_config: - from otx.engine.utils.auto_configurator import DEFAULT_CONFIG_PER_TASK, AutoConfigurator - - auto_configurator = AutoConfigurator( - data_root=data_root, - task=OTXTaskType(task) if task is not None else task, - ) - config_file_path = DEFAULT_CONFIG_PER_TASK[auto_configurator.task] - parser_kwargs["default_config_files"] = [str(config_file_path)] - return parser_kwargs - - def _set_extension_subcommands_parser(self, parser_subcommands: _ActionSubCommands) -> None: - from otx.cli.install import add_install_parser - - add_install_parser(parser_subcommands) - - if _ENGINE_AVAILABLE: - # `otx find` arguments - find_parser = ArgumentParser(formatter_class=CustomHelpFormatter) - find_parser.add_argument( - "--task", - help="Value for filtering by task. Default is None, which shows all recipes.", - type=Optional[OTXTaskType], - ) - find_parser.add_argument( - "--pattern", - help="This allows you to filter the model name of the recipe. \ - For example, if you want to find all models that contain the word 'efficient', \ - you can use '--pattern efficient'", - type=Optional[str], - ) - parser_subcommands.add_subcommand("find", find_parser, help="This shows the model provided by OTX.") - - def instantiate_classes(self, instantiate_engine: bool = True) -> None: - """Instantiate the necessary classes based on the subcommand. - - This method checks if the subcommand is one of the engine subcommands. - If it is, it instantiates the necessary classes such as config, datamodule, model, and engine. - - Args: - instantiate_engine (bool, optional): Whether to instantiate the engine. Defaults to True. - """ - if self.subcommand in self.engine_subcommands(): - # For num_classes update, Model and Metric are instantiated separately. - model_config = self.config[self.subcommand].pop("model") - metric_config = self.config[self.subcommand].get("metric") - - # Instantiate the things that don't need to special handling - self.config_init = self.parser.instantiate_classes(self.config) - self.workspace = self.get_config_value(self.config_init, "workspace") - self.datamodule = self.get_config_value(self.config_init, "data") - - # Instantiate the model and needed components - self.model, self.optimizer, self.scheduler = self.instantiate_model(model_config=model_config) - - # Instantiate the metric with changing the num_classes - metric = self.instantiate_metric(metric_config) - if metric: - self.config_init[self.subcommand]["metric"] = metric - - if instantiate_engine: - self.engine = self.instantiate_engine() - - def instantiate_engine(self) -> Engine: - """Instantiate an Engine object with the specified parameters. - - Returns: - An instance of the Engine class. - """ - engine_kwargs = self.get_config_value(self.config_init, "engine") - return Engine( - model=self.model, - optimizer=self.optimizer, - scheduler=self.scheduler, - datamodule=self.datamodule, - work_dir=self.workspace.work_dir, - **engine_kwargs, - ) - - def instantiate_metric(self, metric_config: Namespace) -> MetricCallable | None: - """Instantiate the metric based on the metric_config. - - It also pathces the num_classes according to the model classes information. - - Args: - metric_config (Namespace): The metric configuration. - """ - from otx.core.utils.instantiators import partial_instantiate_class - - if metric_config and self.subcommand in ["train", "test"]: - metric_kwargs = self.get_config_value(metric_config, "metric", namespace_to_dict(metric_config)) - metric = partial_instantiate_class(metric_kwargs) - return metric[0] if isinstance(metric, list) else metric - - msg = "The configuration of metric is None." - warn(msg, stacklevel=2) - return None - - def instantiate_model(self, model_config: Namespace) -> tuple: - """Instantiate the model based on the subcommand. - - This method checks if the subcommand is one of the engine subcommands. - If it is, it instantiates the model. - - Args: - model_config (Namespace): The model configuration. - - Returns: - tuple: The model and optimizer and scheduler. - """ - from otx.core.model.entity.base import OTXModel - - # Update num_classes - if not self.get_config_value(self.config_init, "disable_infer_num_classes", False): - num_classes = self.datamodule.label_info.num_classes - if hasattr(model_config.init_args, "num_classes") and num_classes != model_config.init_args.num_classes: - warning_msg = ( - f"The `num_classes` in dataset is {num_classes} " - f"but, the `num_classes` of model is {model_config.init_args.num_classes}. " - f"So, Update `model.num_classes` to {num_classes}." - ) - warn(warning_msg, stacklevel=0) - model_config.init_args.num_classes = num_classes - - # Hlabel classification - from otx.core.data.dataset.classification import HLabelInfo - - if isinstance(self.datamodule.label_info, HLabelInfo): - hlabel_info = self.datamodule.label_info - model_config.init_args.num_multiclass_heads = hlabel_info.num_multiclass_heads - model_config.init_args.num_multilabel_classes = hlabel_info.num_multilabel_classes - - # Parses the OTXModel separately to update num_classes. - model_parser = ArgumentParser() - model_parser.add_subclass_arguments(OTXModel, "model", required=False, fail_untyped=False) - model = model_parser.instantiate_classes(Namespace(model=model_config)).get("model") - self.config_init[self.subcommand]["model"] = model - - # Update tile config due to adaptive tiling - if self.datamodule.config.tile_config.enable_tiler: - if not hasattr(model, "tile_config"): - msg = "The model does not have a tile_config attribute. Please check if the model supports tiling." - raise AttributeError(msg) - model.tile_config = self.datamodule.config.tile_config - self.config[self.subcommand].data.config.tile_config.update( - Namespace(dataclasses.asdict(model.tile_config)), - ) - # TODO(Eugene): Need to find a better way to configure image size for OV Models - # https://github.com/openvinotoolkit/training_extensions/pull/2925 - model.image_size = model.tile_image_size - - # Update self.config with model - self.config[self.subcommand].update(Namespace(model=model_config)) - - from otx.core.utils.instantiators import partial_instantiate_class - - optimizer_kwargs = self.get_config_value(self.config_init, "optimizer", {}) - optimizer_kwargs = optimizer_kwargs if isinstance(optimizer_kwargs, list) else [optimizer_kwargs] - optimizers = partial_instantiate_class([_opt for _opt in optimizer_kwargs if _opt]) - if optimizers: - # Updates the instantiated optimizer. - self.config_init[self.subcommand]["optimizer"] = optimizers - - scheduler_kwargs = self.get_config_value(self.config_init, "scheduler", {}) - scheduler_kwargs = scheduler_kwargs if isinstance(scheduler_kwargs, list) else [scheduler_kwargs] - schedulers = partial_instantiate_class([_sch for _sch in scheduler_kwargs if _sch]) - if schedulers: - # Updates the instantiated scheduler. - self.config_init[self.subcommand]["scheduler"] = schedulers - - return model, optimizers, schedulers - - def get_config_value(self, config: Namespace, key: str, default: Any = None) -> Any: # noqa: ANN401 - """Retrieves the value of a configuration key from the given config object. - - Args: - config (Namespace): The config object containing the configuration values. - key (str): The key of the configuration value to retrieve. - default (Any, optional): The default value to return if the key is not found. Defaults to None. - - Returns: - Any: The value of the configuration key, or the default value if the key is not found. - if the value is a Namespace, it is converted to a dictionary. - """ - result = config.get(str(self.subcommand), config).get(key, default) - return namespace_to_dict(result) if isinstance(result, Namespace) else result - - def get_subcommand_parser(self, subcommand: str | None) -> ArgumentParser: - """Returns the argument parser for the specified subcommand. - - Args: - subcommand (str | None): The name of the subcommand. If None, returns the main parser. - - Returns: - ArgumentParser: The argument parser for the specified subcommand. - """ - if subcommand is None: - return self.parser - # return the subcommand parser for the subcommand passed - return self._subcommand_parsers[subcommand] - - def prepare_subcommand_kwargs(self, subcommand: str) -> dict[str, Any]: - """Prepares the keyword arguments to pass to the subcommand to run.""" - return { - k: v for k, v in self.config_init[subcommand].items() if k in self._subcommand_method_arguments[subcommand] - } - - def save_config(self, work_dir: Path) -> None: - """Save the configuration for the specified subcommand. - - Args: - work_dir (Path): The working directory where the configuration file will be saved. - - The configuration is saved as a YAML file in the engine's working directory. - """ - self.config[self.subcommand].pop("workspace", None) - self.get_subcommand_parser(self.subcommand).save( - cfg=self.config.get(str(self.subcommand), self.config), - path=work_dir / "configs.yaml", - overwrite=True, - multifile=False, - skip_check=True, - ) - # if train -> Update `.latest` folder - self.update_latest(work_dir=work_dir) - - def update_latest(self, work_dir: Path) -> None: - """Update the latest cache directory with the latest configurations and checkpoint file. - - Args: - work_dir (Path): The working directory where the configurations and checkpoint files are located. - """ - latest_dir = work_dir.parent / ".latest" - latest_dir.mkdir(exist_ok=True) - cache_dir = latest_dir / self.subcommand - if cache_dir.exists(): - cache_dir.unlink() - cache_dir.symlink_to(work_dir) - - def set_seed(self) -> None: - """Set the random seed for reproducibility. - - This method retrieves the seed value from the argparser and uses it to set the random seed. - If a seed value is provided, it will be used to set the random seed using the - `seed_everything` function from the `lightning` module. - """ - seed = self.get_config_value(self.config, "seed", None) - if seed is not None: - from lightning import seed_everything - - seed_everything(seed, workers=True) - - def run(self) -> None: - """Executes the specified subcommand. - - Raises: - ValueError: If the subcommand is not recognized. - """ - self.console.print(f"[blue]{OTX_LOGO}[/blue] ver.{__version__}", justify="center") - if self.subcommand == "install": - from otx.cli.install import otx_install - - otx_install(**self.config["install"]) - elif self.subcommand == "find": - from otx.engine.utils.api import list_models - - list_models(print_table=True, **self.config[self.subcommand]) - elif self.subcommand in self.engine_subcommands(): - self.set_seed() - self.instantiate_classes() - fn_kwargs = self.prepare_subcommand_kwargs(self.subcommand) - fn = getattr(self.engine, self.subcommand) - try: - fn(**fn_kwargs) - except Exception: - self.console.print_exception(width=self.console.width) - self.save_config(work_dir=Path(self.engine.work_dir)) - else: - msg = f"Unrecognized subcommand: {self.subcommand}" - raise ValueError(msg) diff --git a/src/otx/cli/install.py b/src/otx/cli/install.py deleted file mode 100644 index 37523539b87..00000000000 --- a/src/otx/cli/install.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""OTX CLI Installation.""" - -from __future__ import annotations - -import logging -import os -from typing import TYPE_CHECKING - -from jsonargparse import ArgumentParser -from pkg_resources import Requirement -from rich.console import Console -from rich.logging import RichHandler - -from otx.cli.utils.installation import ( - get_mmcv_install_args, - get_requirements, - get_torch_install_args, - mim_installation, - parse_requirements, - patch_mmaction2, -) - -if TYPE_CHECKING: - from jsonargparse._actions import _ActionSubCommands - -logger = logging.getLogger("pip") -logger.setLevel(logging.WARNING) # setLevel: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET -console = Console() -handler = RichHandler( - console=console, - show_level=False, - show_path=False, -) -logger.addHandler(handler) - - -def add_install_parser(subcommands_action: _ActionSubCommands) -> None: - """Add subparser for install command. - - Args: - subcommands_action (_ActionSubCommands): Sub-Command in CLI. - - Returns: - None - """ - parser = ArgumentParser() - parser.add_argument( - "--option", - help="Install the mmlab library or optional-dependencies.", - default="full", - type=str, - ) - parser.add_argument( - "-v", - "--verbose", - help="Set Logger level to INFO", - action="store_true", - ) - parser.add_argument( - "--do-not-install-torch", - help="Do not install PyTorch. Choose this option if you already install PyTorch.", - action="store_true", - ) - subcommands_action.add_subcommand("install", parser, help="Install OTX requirements.") - - -def otx_install(option: str | None = None, verbose: bool = False, do_not_install_torch: bool = False) -> int: - """Install OTX requirements. - - Args: - option (str): Optional-dependency to install requirements for. - verbose (bool): Set pip logger level to INFO - - Raises: - ValueError: When the task is not supported. - - Returns: - int: Status code of the pip install command. - """ - from pip._internal.commands import create_command - - requirements_dict = get_requirements("otx") - # Add base and openvino requirements. - requirements = requirements_dict["base"] - if option == "full": - for extra in requirements_dict: - requirements.extend(requirements_dict[extra]) - elif option in requirements_dict: - requirements.extend(requirements_dict[option]) - elif option is not None: - requirements.append(Requirement.parse(option)) - - # Parse requirements into torch, mmcv and other requirements. - # This is done to parse the correct version of torch (cpu/cuda) and mmcv (mmcv/mmcv-full). - torch_requirement, mmcv_requirements, other_requirements = parse_requirements(requirements) - - install_args: list[str] = [] - - # Combine torch and other requirements. - install_args = ( - # Get install args for torch to install it from a specific index-url - other_requirements + get_torch_install_args(torch_requirement) - if not do_not_install_torch - else other_requirements - ) - - # Parse mmX requirements if the task requires mmX packages. - mmcv_install_args = [] - if mmcv_requirements: - mmcv_install_args = get_mmcv_install_args(torch_requirement, mmcv_requirements) - install_args += ["openmim"] - - # Install requirements. - with console.status("[bold green]Working on installation...\n") as status: - if verbose: - logger.setLevel(logging.INFO) - status.stop() - console.log(f"Installation list: [yellow]{install_args}[/yellow]") - status_code = create_command("install").main(install_args) - if status_code == 0: - console.log(f"Installation Complete: {install_args}") - - # https://github.com/Madoshakalaka/pipenv-setup/issues/101 - os.environ["SETUPTOOLS_USE_DISTUTILS"] = "stdlib" - - # Install mmX requirements if the task requires mmX packages using mim. - if mmcv_install_args and status_code == 0: - console.log(f"Installation list: [yellow]{mmcv_install_args}[/yellow]") - status_code = mim_installation(mmcv_install_args) - if status_code == 0: - console.log(f"MMLab Installation Complete: {mmcv_install_args}") - - # Patch MMAction2 with src/otx/cli/patches/mmaction2.patch - patch_mmaction2() - - if status_code == 0: - console.print("OTX Installation [bold green]Complete.[/bold green]") - - return status_code diff --git a/src/otx/cli/manager/__init__.py b/src/otx/cli/manager/__init__.py new file mode 100644 index 00000000000..2a80e642a50 --- /dev/null +++ b/src/otx/cli/manager/__init__.py @@ -0,0 +1,9 @@ +"""Configuraion Manager for OTX CLI.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .config_manager import ConfigManager + +__all__ = ["ConfigManager"] diff --git a/src/otx/cli/manager/config_manager.py b/src/otx/cli/manager/config_manager.py new file mode 100644 index 00000000000..67528710090 --- /dev/null +++ b/src/otx/cli/manager/config_manager.py @@ -0,0 +1,727 @@ +"""Configuration Manager .""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os +import shutil +from collections import defaultdict +from datetime import datetime +from pathlib import Path +from typing import Any, DefaultDict, Dict, List, Optional + +from datumaro.components.dataset import Dataset +from datumaro.components.dataset_base import IDataset +from omegaconf import OmegaConf + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.helper import create +from otx.api.entities.model_template import ModelTemplate, parse_model_template +from otx.cli.registry import Registry as OTXRegistry +from otx.cli.utils.config import configure_dataset, override_parameters +from otx.cli.utils.errors import ( + CliException, + ConfigValueError, + FileNotExistError, + NotSupportedError, +) +from otx.cli.utils.importing import get_otx_root_path +from otx.cli.utils.multi_gpu import is_multigpu_child_process +from otx.cli.utils.parser import gen_param_help, gen_params_dict_from_args +from otx.core.data.manager.dataset_manager import DatasetManager +from otx.utils.logger import get_logger +from otx.utils.utils import add_suffix_to_filename + +logger = get_logger() + +DEFAULT_MODEL_TEMPLATE_ID = { + "CLASSIFICATION": "Custom_Image_Classification_EfficinetNet-B0", + "DETECTION": "Custom_Object_Detection_Gen3_ATSS", + "INSTANCE_SEGMENTATION": "Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50", + "ROTATED_DETECTION": "Custom_Rotated_Detection_via_Instance_Segmentation_MaskRCNN_ResNet50", + "SEGMENTATION": "Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR", + "ACTION_CLASSIFICATION": "Custom_Action_Classification_X3D", + "ACTION_DETECTION": "Custom_Action_Detection_X3D_FAST_RCNN", + "VISUAL_PROMPTING": "Visual_Prompting_SAM_ViT_B", + "ANOMALY_CLASSIFICATION": "ote_anomaly_classification_padim", + "ANOMALY_DETECTION": "ote_anomaly_detection_padim", + "ANOMALY_SEGMENTATION": "ote_anomaly_segmentation_padim", +} + +AUTOSPLIT_SUPPORTED_FORMAT = [ + "imagenet", + "coco", + "cityscapes", + "voc", +] + +TASK_TYPE_TO_SUPPORTED_FORMAT = { + "CLASSIFICATION": ["imagenet", "datumaro"], + "DETECTION": ["coco", "voc", "yolo"], + "SEGMENTATION": ["cityscapes", "common_semantic_segmentation", "voc", "ade20k2017", "ade20k2020"], + "ACTION_CLASSIFICATION": ["multi-cvat"], + "ACTION_DETECTION": ["multi-cvat"], + "VISUAL_PROMPTING": ["coco", "voc", "common_semantic_segmentation"], + "ANOMALY_CLASSIFICATION": ["mvtec"], + "ANOMALY_DETECTION": ["mvtec"], + "ANOMALY_SEGMENTATION": ["mvtec"], + "INSTANCE_SEGMENTATION": ["coco", "voc"], + "ROTATED_DETECTION": ["coco", "voc"], +} + +TASK_TYPE_TO_SUB_DIR_NAME = { + "Incremental": "", + "Semisupervised": "semisl", + "Selfsupervised": "selfsl", +} + + +def set_workspace(task: str, root: str = None, name: str = "otx-workspace"): + """Set workspace path according to arguments.""" + path = f"{root}/{name}-{task}" if root else f"./{name}-{task}" + return path + + +class ConfigManager: # pylint: disable=too-many-instance-attributes + """Auto configuration manager that could set the proper configuration. + + Currently, it only supports the small amount of functions. + * Data format detection + * Task type detection + * Write the data to the workspace + * Write the data configuration to the workspace + + However, it will supports lots of things in the near future. + * Automatic train type detection (Supervised, Self, Semi) + * Automatic resource allocation (num_workers, HPO) + + """ + + def __init__(self, args, workspace_root: Optional[str] = None, mode: str = "train"): + # Currently, Datumaro.auto_split() can support below 3 tasks + # Classification, Detection, Segmentation + self.otx_root = get_otx_root_path() + self.workspace_root = Path(workspace_root) if workspace_root else Path(".") + self.mode = mode + self.rebuild: bool = False + self.create_date: str = datetime.now().strftime("%Y%m%d_%H%M%S") + + self.args = args + self.template = args.template + self.task_type: str = "" + self.train_type: str = "" + self.model: str = "" + + self.dataset_manager = DatasetManager() + self.data_format: str = "" + self.data_config: DefaultDict[str, dict] = defaultdict(dict) + + @property + def data_config_file_path(self) -> Path: + """The path of the data configuration yaml to use for the task. + + Raises: + FileNotFoundError: If data is received as args from otx train and the file does not exist, Error. + + Returns: + Path: Path of target data configuration file. + """ + if "data" in self.args and self.args.data: + if Path(self.args.data).exists(): + return Path(self.args.data) + raise FileNotExistError(f"Not found: {self.args.data}") + return self.workspace_root / "data.yaml" + + @property + def output_path(self) -> Path: + """The path of output directory for workspace. + + Returns: + Path: Path of output directory. + """ + if "output" in self.args and self.args.output: + output_path = Path(self.args.output) + else: + output_path = self.workspace_root / "outputs" / f"{self.create_date}_{self.mode}" + if not output_path.exists(): + output_path.mkdir(exist_ok=True, parents=True) + return output_path + + def check_workspace(self) -> bool: + """Check that the class's workspace_root is an actual workspace folder. + + Returns: + bool: true for workspace else false + """ + has_template_yaml = (self.workspace_root / "template.yaml").exists() + has_data_yaml = self.data_config_file_path.exists() + return has_template_yaml and has_data_yaml + + def configure_template(self, model: str = None) -> None: + """Update the template appropriate for the situation.""" + if self.check_workspace(): + # Workspace -> template O + self.template = parse_model_template(str(self.workspace_root / "template.yaml")) + if self.mode == "build" and self._check_rebuild(): + self.rebuild = True + model = model if model else self.template.name + self.template = self._get_template(str(self.task_type), model=model) + self.train_type = self._get_train_type() + else: + # FIXME: Inside the workspace, ignore the --train-type args. + self.train_type = self._get_train_type(ignore_args=True) + elif self.template and Path(self.template).exists(): + # No workspace -> template O + self.template = parse_model_template(self.template) + self.train_type = self._get_train_type() + else: + task_type = self.task_type + if not task_type and not model: + if self.mode in ["train", "build"]: + if not hasattr(self.args, "train_data_roots"): + raise ConfigValueError("Can't find the argument 'train_data_roots'") + task_type = self.auto_task_detection(self.args.train_data_roots) + else: + raise ConfigValueError("No appropriate template or task-type was found.") + self.template = self._get_template(task_type, model=model) + self.train_type = self._get_train_type() + self.task_type = self.template.task_type + self.model = self.template.name + + def _check_rebuild(self): + """Checking for Rebuild status.""" + if self.args.task and str(self.template.task_type) != self.args.task.upper(): + raise NotSupportedError("Task Update is not yet supported.") + result = False + if self.args.model and self.template.name != self.args.model.upper(): + print(f"[*] Rebuild model: {self.template.name} -> {self.args.model.upper()}") + result = True + template_train_type = self._get_train_type(ignore_args=True) + if self.args.train_type and template_train_type != self.args.train_type: + self.train_type = self.args.train_type + print(f"[*] Rebuild train-type: {template_train_type} -> {self.train_type}") + result = True + return result + + def configure_data_config(self, update_data_yaml: bool = True) -> None: + """Configure data_config according to the situation and create data.yaml.""" + data_yaml_path = self.data_config_file_path + data_yaml = configure_dataset(self.args, data_yaml_path=data_yaml_path) + if self.mode in ("train", "build", "optimize"): + use_auto_split = data_yaml["data"]["train"]["data-roots"] and not data_yaml["data"]["val"]["data-roots"] + # FIXME: Hardcoded for Self-Supervised Learning + if use_auto_split and str(self.train_type).upper() != "SELFSUPERVISED": + splitted_dataset = self.auto_split_data( + data_yaml["data"]["train"]["data-roots"], str(self.task_type), self.args.train_ann_files + ) + default_data_folder_name = "splitted_dataset" + data_yaml = self._get_arg_data_yaml() + self._save_data(splitted_dataset, default_data_folder_name, data_yaml) + if (str(self.task_type).upper() == "VISUAL_PROMPTING") and (self.mode == "train"): + # TODO (sungchul): find proper way to update data_yaml + # data_yaml is related to OmegaConf.to_yaml and it doesn't support defaultdict + if "options" not in data_yaml: + data_yaml["options"] = {} + data_yaml["options"]["use_mask"] = getattr(self.args, "params.learning_parameters.dataset.use_mask", False) + if update_data_yaml: + self._export_data_cfg(data_yaml, str(data_yaml_path)) + print(f"[*] Update data configuration file to: {str(data_yaml_path)}") + self.update_data_config(data_yaml) + + def _get_train_type(self, ignore_args: bool = False) -> str: + """Check and return the train_type received as input args. + + If value passed to args.train_type -> return this train type. + Configure train type if None in args. + If ignore_args passed -> use value in model template + """ + + if not ignore_args: + if hasattr(self.args, "train_type") and self.mode in ("build", "train", "optimize"): + self._configure_train_type() + if self.train_type not in TASK_TYPE_TO_SUB_DIR_NAME: + raise NotSupportedError(f"{self.train_type} is not currently supported by otx.") + return self.train_type + + # if ignore_args -> use train type from template file + algo_backend = self.template.hyper_parameters.parameter_overrides.get("algo_backend", False) + if algo_backend: + train_type = algo_backend.get("train_type", {"default_value": "Incremental"}) + return train_type.get("default_value", "Incremental") + return "Incremental" + + def auto_task_detection(self, data_roots: str) -> str: + """Detect task type automatically.""" + if not data_roots: + raise CliException("Workspace must already exist or one of {task or model or train-data-roots} must exist.") + self.data_format = self.dataset_manager.get_data_format(data_roots) + return self._get_task_type_from_data_format(self.data_format) + + def _configure_train_type(self): + """Auto train type detection. + + If self.args.train_type is not None -> use args.train_type + If train_data_roots contains only set of images -> Self-SL + If unlabeled-data-roots were passed to CLI -> use Semi-SL + If unlabeled_images presented in dataset structure and it is sufficient to start Semi-SL -> Semi-SL + Overwise set Incremental training type. + """ + + def _count_imgs_in_dir(dir, recursive=False): + """Count number of images in directory recursively.""" + import glob + + valid_suff = ["jpg", "png", "jpeg", "gif"] + num_valid_imgs = 0 + for files in glob.iglob(f"{dir}/**", recursive=recursive): + suff = files.split(".")[-1] + if suff.lower() in valid_suff: + num_valid_imgs += 1 + + return num_valid_imgs + + def _check_semisl_requirements(unlabeled_dir): + """Check if quantity of unlabeled images is sufficient for Semi-SL learning.""" + if unlabeled_dir is None: + return False + + if not os.path.isdir(unlabeled_dir) or not os.listdir(unlabeled_dir): + raise ValueError( + "unlabeled-data-roots isn't a directory, it doesn't exist or it is empty. " + "Please, check command line and directory path." + ) + + all_unlabeled_images = _count_imgs_in_dir(unlabeled_dir, recursive=True) + # check if number of unlabeled images is more than relative thershold + if all_unlabeled_images > 1: + return unlabeled_dir + + logger.warning( + "WARNING: There are none or too litle images to start Semi-SL training. " + "It should be more than relative threshold (at least 7% of labeled images) " + "Start Supervised training instead." + ) + + # if user explicitly passed train type via args + if self.args.train_type is not None: + self.train_type = self.args.train_type + return + + if self.mode == "build" and self.args.train_data_roots is None: + # Case, when we want to build environment with tempate without dataset path + # Set train_type to Incremental by default + self.train_type = "Incremental" + return + + if ( + self.args.train_data_roots is None + or not os.path.isdir(self.args.train_data_roots) + or not os.listdir(self.args.train_data_roots) + ): + raise ValueError( + "train-data-roots isn't a directory, it doesn't exist or it is empty. " + "Please, check command line and directory path." + ) + + if _count_imgs_in_dir(self.args.train_data_roots): + # If train folder with images only was passed to args + # Then we start self-supervised training + print("[*] Selfsupervised training type detected") + self.train_type = "Selfsupervised" + return + + # if user explicitly passed unlabeled images folder + valid_unlabeled_path = _check_semisl_requirements(self.args.unlabeled_data_roots) + if valid_unlabeled_path: + print(f"[*] Semisupervised training type detected with unlabeled data: {valid_unlabeled_path}") + self.train_type = "Semisupervised" + return + + self.train_type = "Incremental" + + def _get_task_type_from_data_format(self, data_format: str) -> str: + """Detect task type. + + For some datasets (i.e. COCO, VOC, MVTec), can't be fully automated. + Because those datasets have several format at the same time. + (i.e. for the COCO case, object detection and instance segmentation annotations coexist) + In this case, the task_type will be selected to default value. + + For action tasks, currently action_classification is default. + + If Datumaro supports the Kinetics, AVA datasets, MVTec, _is_cvat_format(), _is_mvtec_format() + functions will be deleted. + """ + + for task_key, data_value in TASK_TYPE_TO_SUPPORTED_FORMAT.items(): + if data_format in data_value: + self.task_type = task_key + print(f"[*] Detected task type: {self.task_type}") + return task_key + raise ConfigValueError(f"Can't find proper task. we are not support {data_format} format, yet.") + + def auto_split_data(self, data_roots: str, task: str, ann_file: Optional[str] = None): + """Automatically Split train data --> train/val dataset.""" + self.data_format = self.dataset_manager.get_data_format(data_roots) + dataset = self.dataset_manager.import_dataset(data_root=data_roots, data_format=self.data_format) + train_dataset = self.dataset_manager.get_train_dataset(dataset) + if ann_file is not None: + train_dataset = self.dataset_manager.import_dataset(ann_file, data_format=self.data_format, subset="train") + val_dataset = self.dataset_manager.get_val_dataset(dataset) + splitted_dataset = None + if self.data_format in AUTOSPLIT_SUPPORTED_FORMAT: + if val_dataset is None: + splitted_dataset = self.dataset_manager.auto_split( + task=task, + dataset=train_dataset, + split_ratio=[("train", 0.8), ("val", 0.2)], + ) + else: + print(f"[*] Found validation data in your dataset in {data_roots}. It'll be used as validation data.") + splitted_dataset = {"train": train_dataset, "val": val_dataset} + else: + print(f"[*] Current auto-split can't support the {self.data_format} format.") + return splitted_dataset + + def _get_arg_data_yaml(self): + # TODO: This should modify data yaml format to data_config format. + """Save the splitted dataset and data.yaml to the workspace.""" + data_yaml = self._create_empty_data_cfg() + if self.mode in ("train", "optimize"): + if self.args.train_data_roots: + data_yaml["data"]["train"]["data-roots"] = self.args.train_data_roots + if self.args.train_ann_files: + data_yaml["data"]["train"]["ann-files"] = self.args.train_ann_files + if self.args.val_data_roots: + data_yaml["data"]["val"]["data-roots"] = self.args.val_data_roots + if self.args.val_ann_files: + data_yaml["data"]["val"]["ann-files"] = self.args.val_ann_files + if self.args.unlabeled_data_roots: + data_yaml["data"]["unlabeled"]["data-roots"] = self.args.unlabeled_data_roots + if self.args.unlabeled_file_list: + data_yaml["data"]["unlabeled"]["file-list"] = self.args.unlabeled_file_list + elif self.mode == "test": + if self.args.test_data_roots: + data_yaml["data"]["test"]["data-roots"] = self.args.test_data_roots + if self.args.test_ann_files: + data_yaml["data"]["test"]["ann-files"] = self.args.test_ann_files + return data_yaml + + def _save_data( + self, + splitted_dataset: Dict[str, IDataset], + default_data_folder_name: str, + data_config: Dict[str, Dict[str, Dict[str, Any]]], + ) -> None: + """Save the data for the classification task. + + Args: + splitted_dataset (dict): A dictionary containing split datasets + default_data_folder_name (str): the name of splitted dataset folder + data_config (dict): dictionary that has information about data path + """ + for phase, dataset in splitted_dataset.items(): + dst_dir_path = self.workspace_root / default_data_folder_name / phase + data_config["data"][phase]["data-roots"] = str(dst_dir_path.absolute()) + # Convert Datumaro class: DatasetFilter(IDataset) --> Dataset + if isinstance(dataset, Dataset): + datum_dataset = dataset + else: + datum_dataset = Dataset.from_extractors(dataset) + # Write the data + # TODO: consider the way that reduces disk stroage + # Currently, saving all images to the workspace. + # It might needs quite large disk storage. + self.dataset_manager.export_dataset( + dataset=datum_dataset, output_dir=str(dst_dir_path), data_format=self.data_format, save_media=True + ) + + if data_config["data"]["unlabeled"]["data-roots"] is not None: + data_config["data"]["unlabeled"]["data-roots"] = str( + Path(data_config["data"]["unlabeled"]["data-roots"]).absolute() + ) + if data_config["data"]["unlabeled"]["file-list"] is not None: + data_config["data"]["unlabeled"]["file-list"] = str( + Path(data_config["data"]["unlabeled"]["file-list"]).absolute() + ) + + def _create_empty_data_cfg(self) -> Dict[str, Dict[str, Dict[str, Any]]]: + """Create default dictionary to represent the dataset.""" + data_config: Dict[str, Dict[str, Any]] = {"data": {}} + for subset in ["train", "val", "test"]: + data_subset = {"ann-files": None, "data-roots": None} + data_config["data"][subset] = data_subset + data_config["data"]["unlabeled"] = {"file-list": None, "data-roots": None} + return data_config + + def _export_data_cfg(self, data_cfg: Dict[str, Dict[str, Dict[str, Any]]], output_path: str) -> None: + """Export the data configuration file to output_path.""" + Path(output_path).write_text(OmegaConf.to_yaml(data_cfg), encoding="utf-8") + + def get_hyparams_config(self, override_param: Optional[List] = None) -> ConfigurableParameters: + """Separates the input params received from args and updates them..""" + hyper_parameters = self.template.hyper_parameters.data + type_hint = gen_param_help(hyper_parameters) + updated_hyper_parameters = gen_params_dict_from_args( + self.args, override_param=override_param, type_hint=type_hint + ) + override_parameters(updated_hyper_parameters, hyper_parameters) + return create(hyper_parameters) + + def get_dataset_config(self, subsets: List[str], hyper_parameters: Optional[ConfigurableParameters] = None) -> dict: + """Returns dataset_config in a format suitable for each subset. + + Args: + subsets (list, str): Defaults to ["train", "val", "unlabeled"]. + hyper_parameters (ConfigurableParameters): Set of hyper parameters. + + Returns: + dict: dataset_config + """ + if str(self.train_type).upper() == "INCREMENTAL" and "unlabeled" in subsets: + subsets.remove("unlabeled") + dataset_config: Dict[str, Any] = { + "task_type": self.task_type, + "train_type": self.train_type, + "encryption_key": self.encryption_key, + } + for subset in subsets: + if f"{subset}_subset" in self.data_config: + if self.data_config[f"{subset}_subset"]["data_roots"]: + dataset_config.update({f"{subset}_data_roots": self.data_config[f"{subset}_subset"]["data_roots"]}) + if "ann_files" in self.data_config[f"{subset}_subset"]: + dataset_config.update({f"{subset}_ann_files": self.data_config[f"{subset}_subset"]["ann_files"]}) + if "file_list" in self.data_config[f"{subset}_subset"]: + dataset_config.update({f"{subset}_file_list": self.data_config[f"{subset}_subset"]["file_list"]}) + if "options" in self.data_config: + dataset_config.update(self.data_config["options"]) + if hyper_parameters is not None: + dataset_config["cache_config"] = {} + algo_backend = getattr(hyper_parameters, "algo_backend", None) + if algo_backend: + storage_cache_scheme = getattr(algo_backend, "storage_cache_scheme", None) + if storage_cache_scheme is not None: + storage_cache_scheme = str(storage_cache_scheme) + dataset_config["cache_config"]["scheme"] = storage_cache_scheme + + learning_parameters = getattr(hyper_parameters, "learning_parameters", None) + if learning_parameters: + num_workers = getattr(learning_parameters, "num_workers", 0) + dataset_config["cache_config"]["num_workers"] = num_workers + + if str(self.task_type).upper() == "SEGMENTATION" and str(self.train_type).upper() == "SELFSUPERVISED": + # FIXME: manually set a path to save pseudo masks in workspace + train_type_rel_path = TASK_TYPE_TO_SUB_DIR_NAME[self.train_type] + train_type_dir = self.workspace_root / train_type_rel_path + dataset_config["pseudo_mask_dir"] = train_type_dir / "detcon_mask" + return dataset_config + + def update_data_config(self, data_yaml: dict) -> None: + # TODO: This also requires uniformity in the format. + """Convert the data yaml format to the data_config format consumed by the task. + + Args: + data_yaml (dict): data.yaml format + """ + if "data-roots" in data_yaml["data"]["train"]: + self.data_config["train_subset"] = {"data_roots": data_yaml["data"]["train"]["data-roots"]} + if "ann-files" in data_yaml["data"]["train"]: + self.data_config["train_subset"]["ann_files"] = data_yaml["data"]["train"]["ann-files"] + if "data-roots" in data_yaml["data"]["val"]: + self.data_config["val_subset"] = {"data_roots": data_yaml["data"]["val"]["data-roots"]} + if "ann-files" in data_yaml["data"]["val"]: + self.data_config["val_subset"]["ann_files"] = data_yaml["data"]["val"]["ann-files"] + if "data-roots" in data_yaml["data"]["test"]: + self.data_config["test_subset"] = {"data_roots": data_yaml["data"]["test"]["data-roots"]} + if "ann-files" in data_yaml["data"]["test"]: + self.data_config["test_subset"]["ann_files"] = data_yaml["data"]["test"]["ann-files"] + if "unlabeled" in data_yaml["data"] and data_yaml["data"]["unlabeled"]["data-roots"]: + self.data_config["unlabeled_subset"] = { + "data_roots": data_yaml["data"]["unlabeled"]["data-roots"], + "file_list": data_yaml["data"]["unlabeled"]["file-list"], + } + # FIXME: Hardcoded for Self-Supervised Learning + if self.mode in ("train", "optimize") and str(self.train_type).upper() == "SELFSUPERVISED": + self.data_config["val_subset"] = {"data_roots": None} + + if str(self.task_type).upper() == "VISUAL_PROMPTING": + self.data_config["options"]["use_mask"] = data_yaml["options"]["use_mask"] + + def _get_template(self, task_type: str, model: Optional[str] = None) -> ModelTemplate: + """Returns the appropriate template for each situation. + + Args: + task_type (str): The task_type registered in the registry. Used for filtering. + model (str, optional): The task_type registered in the registry. Used for filtering. Defaults to None. + + Returns: + ModelTemplate: Selected model template. + """ + otx_registry = OTXRegistry(self.otx_root).filter(task_type=task_type if task_type else None) + if model: + template_lst = [temp for temp in otx_registry.templates if temp.name.lower() == model.lower()] + if not template_lst: + raise NotSupportedError( + f"[*] {model} is not a type supported by OTX {task_type}." + f"\n[*] Please refer to 'otx find --template --task {task_type}'" + ) + template = template_lst[0] + else: + template = otx_registry.get(DEFAULT_MODEL_TEMPLATE_ID[task_type.upper()]) + return template + + def build_workspace(self, new_workspace_path: Optional[str] = None) -> None: + """Create OTX workspace with Template configs from task type. + + This function provides a user-friendly OTX workspace and provides more intuitive + and create customizable templates to help users use all the features of OTX. + + Args: + new_workspace_path (Optional[str]): Workspace dir name for build + """ + + # Create OTX-workspace + if is_multigpu_child_process(): + return + # Check whether the workspace is existed or not + if self.check_workspace() and not self.rebuild: + return + if self.rebuild: + print(f"[*] \t- Rebuild: model-{self.model} / train type-{self.train_type}") + if new_workspace_path: + self.workspace_root = Path(new_workspace_path) + elif not self.check_workspace(): + self.workspace_root = Path(set_workspace(task=self.task_type)) + self.workspace_root.mkdir(exist_ok=True, parents=True) + print(f"[*] Workspace Path: {self.workspace_root}") + print(f"[*] Load Model Template ID: {self.template.model_template_id}") + print(f"[*] Load Model Name: {self.template.name}") + + template_dir = Path(self.template.model_template_path).parent + + # Copy task base configuration file + task_configuration_path = template_dir / self.template.hyper_parameters.base_path + shutil.copyfile(task_configuration_path, str(self.workspace_root / "configuration.yaml")) + # Load Model Template + template_config = OmegaConf.load(self.template.model_template_path) + template_config.hyper_parameters.base_path = "./configuration.yaml" + + # Configuration of Train Type value + train_type_rel_path = TASK_TYPE_TO_SUB_DIR_NAME[self.train_type] + + # FIXME: Hardcoded solution for supcon + enable_supcon = gen_params_dict_from_args(self.args).get("learning_parameters", {}) + enable_supcon = enable_supcon.get("enable_supcon", {"value": False}) + if enable_supcon.get("value", False): + train_type_rel_path = "supcon" + + model_dir = template_dir.absolute() / train_type_rel_path + if not model_dir.exists(): + raise NotSupportedError(f"[*] {self.train_type} is not a type supported by OTX {self.task_type}") + train_type_dir = self.workspace_root / train_type_rel_path + train_type_dir.mkdir(exist_ok=True) + + # Update Hparams + if (model_dir / "hparam.yaml").exists(): + template_config = OmegaConf.merge(template_config, OmegaConf.load(str(model_dir / "hparam.yaml"))) + + # Copy config files + config_files = [ + (model_dir, "model.py", train_type_dir), + (model_dir, "model_multilabel.py", train_type_dir), + (model_dir, "data_pipeline.py", train_type_dir), + (template_dir, "tile_pipeline.py", self.workspace_root), + (template_dir, "deployment.py", self.workspace_root), + (template_dir, "hpo_config.yaml", self.workspace_root), + (template_dir, "model_hierarchical.py", self.workspace_root), + ] + for target_dir, file_name, dest_dir in config_files: + self._copy_config_files(target_dir, file_name, dest_dir) + + # check xpu file exists + xpu_file = add_suffix_to_filename(target_dir / file_name, "_xpu") + if xpu_file.exists(): + self._copy_config_files(xpu_file.parent, xpu_file.name, dest_dir) + + (self.workspace_root / "template.yaml").write_text(OmegaConf.to_yaml(template_config)) + + # Copy deployment_tile_classifier for Instance Segmentation + if (model_dir / "deployment_tile_classifier.py").exists(): + shutil.copyfile( + str(model_dir / "deployment_tile_classifier.py"), + str(train_type_dir / "deployment_tile_classifier.py"), + ) + print(f"[*] \t- Updated: {str(train_type_dir / 'deployment_tile_classifier.py')}") + + # Copy compression_config.json + if (model_dir / "compression_config.json").exists(): + shutil.copyfile( + str(model_dir / "compression_config.json"), + str(train_type_dir / "compression_config.json"), + ) + print(f"[*] \t- Updated: {str(train_type_dir / 'compression_config.json')}") + + # copy PTQ config + if (model_dir / "ptq_optimization_config.py").exists(): + shutil.copyfile( + str(model_dir / "ptq_optimization_config.py"), + str(train_type_dir / "ptq_optimization_config.py"), + ) + print(f"[*] \t- Updated: {str(train_type_dir / 'ptq_optimization_config.py')}") + + if not (self.workspace_root / "data.yaml").exists(): + data_yaml = self._get_arg_data_yaml() + self._export_data_cfg(data_yaml, str((self.workspace_root / "data.yaml"))) + + self.template = parse_model_template(str(self.workspace_root / "template.yaml")) + + def _copy_config_files(self, target_dir: Path, file_name: str, dest_dir: Path) -> None: + """Copy Configuration files for workspace.""" + if (target_dir / file_name).exists(): + if file_name.endswith(".py"): + try: + from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + OTXConfig, + ) + + config = OTXConfig.fromfile(str(target_dir / file_name)) + self._patch_cli_configs(config) + config.dump(str(dest_dir / file_name)) + except Exception as exc: + raise CliException(f"{self.task_type} requires mmcv-full to be installed.") from exc + elif file_name.endswith((".yml", ".yaml")): + config = OmegaConf.load(str(target_dir / file_name)) + (dest_dir / file_name).write_text(OmegaConf.to_yaml(config)) + print(f"[*] \t- Updated: {str(dest_dir / file_name)}") + + def _patch_cli_configs(self, config): + """Patch for CLI configurations.""" + if config.get("ignore", None): + # FIXME: In the CLI, there is currently no case for using the ignore label. + # so the workspace's model patches ignore to False. + config.ignore = False + print("In the CLI, Update ignore to false in model configuration.") + if hasattr(config, "deterministic") and hasattr(self.args, "deterministic"): + config.deterministic = self.args.deterministic + if hasattr(config, "seed") and hasattr(self.args, "seed") and self.args.seed: + config.seed = self.args.seed + + @property + def encryption_key(self): + """Get encryption key from CLI argument or OS environment variables. If it is not specified, return None.""" + key_from_args = getattr(self.args, "encryption_key", None) + key_from_envs = os.environ.get("ENCRYPTION_KEY", None) + + if key_from_args is not None and key_from_envs is not None: + raise ValueError( + "You have to choose either one of the two, whether encryption_key is " + "specified as a CLI argument (--encryption-key=) or specified in " + "an environment variable (ENCRYPTION_KEY=). " + ) + + if key_from_args is not None: + return key_from_args + if key_from_envs is not None: + return key_from_envs + + return None diff --git a/src/otx/cli/patches/NOTICE b/src/otx/cli/patches/NOTICE deleted file mode 100644 index bc5946ebdb7..00000000000 --- a/src/otx/cli/patches/NOTICE +++ /dev/null @@ -1 +0,0 @@ -mmaction2.patch file in this directory follows Apache-2.0 license (source repository: https://github.com/open-mmlab/mmaction2) diff --git a/src/otx/cli/patches/mmaction2.patch b/src/otx/cli/patches/mmaction2.patch deleted file mode 100644 index b757dedddb0..00000000000 --- a/src/otx/cli/patches/mmaction2.patch +++ /dev/null @@ -1,1132 +0,0 @@ -diff -Naru ../orig/mmaction/models/localizers/drn/drn.py mmaction/models/localizers/drn/drn.py ---- ../orig/mmaction/models/localizers/drn/drn.py 1970-01-01 09:00:00.000000000 +0900 -+++ mmaction/models/localizers/drn/drn.py 2023-12-18 08:13:04.000000000 +0900 -@@ -0,0 +1,260 @@ -+# Copyright (c) OpenMMLab. All rights reserved. -+from typing import Sequence -+ -+import numpy as np -+import torch -+import torch.nn as nn -+from mmengine.model import BaseModel -+ -+from mmaction.registry import MODELS -+from mmaction.utils import OptConfigType -+from ..utils import soft_nms -+from .drn_utils import FPN, Backbone, FCOSModule, QueryEncoder -+ -+ -+@MODELS.register_module() -+class DRN(BaseModel): -+ """Dense Regression Network for Video Grounding. -+ -+ Please refer `Dense Regression Network for Video Grounding -+ `_. -+ Code Reference: https://github.com/Alvin-Zeng/DRN -+ -+ Args: -+ vocab_size (int): number of all possible words in the query. -+ Defaults to 1301. -+ hidden_dim (int): the hidden dimension of the LSTM in the -+ language model. Defaults to 512. -+ embed_dim (int): the embedding dimension of the query. Defaults -+ to 300. -+ bidirection (bool): if True, use bi-direction LSTM in the -+ language model. Defaults to True. -+ first_output_dim (int): the output dimension of the first layer -+ in the backbone. Defaults to 256. -+ fpn_feature_dim (int): the output dimension of the FPN. Defaults -+ to 512. -+ feature_dim (int): the dimension of the video clip feature. -+ lstm_layers (int): the number of LSTM layers in the language model. -+ Defaults to 1. -+ fcos_pre_nms_top_n (int): value of Top-N in the FCOS module before -+ nms. Defaults to 32. -+ fcos_inference_thr (float): threshold in the FOCS inference. BBoxes -+ with scores higher than this threshold are regarded as positive. -+ Defaults to 0.05. -+ fcos_prior_prob (float): A prior probability of the positive bboexes. -+ Used to initialized the bias of the classification head. -+ Defaults to 0.01. -+ focal_alpha (float):Focal loss hyper-parameter alpha. -+ Defaults to 0.25. -+ focal_gamma (float): Focal loss hyper-parameter gamma. -+ Defaults to 2.0. -+ fpn_stride (Sequence[int]): the strides in the FPN. Defaults to -+ [1, 2, 4]. -+ fcos_nms_thr (float): NMS threshold in the FOCS module. -+ Defaults to 0.6. -+ fcos_conv_layers (int): number of convolution layers in FCOS. -+ Defaults to 1. -+ fcos_num_class (int): number of classes in FCOS. -+ Defaults to 2. -+ is_first_stage (bool): if true, the model is in the first stage -+ training. -+ is_second_stage (bool): if true, the model is in the second stage -+ training. -+ """ -+ -+ def __init__(self, -+ vocab_size: int = 1301, -+ hidden_dim: int = 512, -+ embed_dim: int = 300, -+ bidirection: bool = True, -+ first_output_dim: int = 256, -+ fpn_feature_dim: int = 512, -+ feature_dim: int = 4096, -+ lstm_layers: int = 1, -+ fcos_pre_nms_top_n: int = 32, -+ fcos_inference_thr: float = 0.05, -+ fcos_prior_prob: float = 0.01, -+ focal_alpha: float = 0.25, -+ focal_gamma: float = 2.0, -+ fpn_stride: Sequence[int] = [1, 2, 4], -+ fcos_nms_thr: float = 0.6, -+ fcos_conv_layers: int = 1, -+ fcos_num_class: int = 2, -+ is_first_stage: bool = False, -+ is_second_stage: bool = False, -+ init_cfg: OptConfigType = None, -+ **kwargs) -> None: -+ super(DRN, self).__init__(init_cfg) -+ -+ self.query_encoder = QueryEncoder( -+ vocab_size=vocab_size, -+ hidden_dim=hidden_dim, -+ embed_dim=embed_dim, -+ num_layers=lstm_layers, -+ bidirection=bidirection) -+ -+ channels_list = [ -+ (feature_dim + 256, first_output_dim, 3, 1), -+ (first_output_dim, first_output_dim * 2, 3, 2), -+ (first_output_dim * 2, first_output_dim * 4, 3, 2), -+ ] -+ self.backbone_net = Backbone(channels_list) -+ -+ self.fpn = FPN( -+ in_channels_list=[256, 512, 1024], out_channels=fpn_feature_dim) -+ -+ self.fcos = FCOSModule( -+ in_channels=fpn_feature_dim, -+ fcos_num_class=fcos_num_class, -+ fcos_conv_layers=fcos_conv_layers, -+ fcos_prior_prob=fcos_prior_prob, -+ fcos_inference_thr=fcos_inference_thr, -+ fcos_pre_nms_top_n=fcos_pre_nms_top_n, -+ fcos_nms_thr=fcos_nms_thr, -+ test_detections_per_img=32, -+ fpn_stride=fpn_stride, -+ focal_alpha=focal_alpha, -+ focal_gamma=focal_gamma, -+ is_first_stage=is_first_stage, -+ is_second_stage=is_second_stage) -+ -+ self.prop_fc = nn.Linear(feature_dim, feature_dim) -+ self.position_transform = nn.Linear(3, 256) -+ -+ qInput = [] -+ for t in range(len(channels_list)): -+ if t > 0: -+ qInput += [nn.Linear(1024, channels_list[t - 1][1])] -+ else: -+ qInput += [nn.Linear(1024, feature_dim)] -+ self.qInput = nn.ModuleList(qInput) -+ -+ self.is_second_stage = is_second_stage -+ -+ def forward(self, inputs, data_samples, mode, **kwargs): -+ props_features = torch.stack(inputs) -+ batch_size = props_features.shape[0] -+ device = props_features.device -+ proposals = torch.stack([ -+ sample.proposals['proposals'] for sample in data_samples -+ ]).to(device) -+ gt_bbox = torch.stack([ -+ sample.gt_instances['gt_bbox'] for sample in data_samples -+ ]).to(device) -+ -+ video_info = [i.metainfo for i in data_samples] -+ query_tokens_ = [i['query_tokens'] for i in video_info] -+ query_length = [i['query_length'] for i in video_info] -+ query_length = torch.from_numpy(np.array(query_length)) -+ -+ max_query_len = max([i.shape[0] for i in query_tokens_]) -+ query_tokens = torch.zeros(batch_size, max_query_len) -+ for idx, query_token in enumerate(query_tokens_): -+ query_len = query_token.shape[0] -+ query_tokens[idx, :query_len] = query_token -+ -+ query_tokens = query_tokens.to(device).long() -+ query_length = query_length.to(device).long() # should be on CPU! -+ -+ sort_index = query_length.argsort(descending=True) -+ box_lists, loss_dict = self._forward(query_tokens[sort_index], -+ query_length[sort_index], -+ props_features[sort_index], -+ proposals[sort_index], -+ gt_bbox[sort_index]) -+ if mode == 'loss': -+ return loss_dict -+ elif mode == 'predict': -+ # only support batch size = 1 -+ bbox = box_lists[0] -+ -+ per_vid_detections = bbox['detections'] -+ per_vid_scores = bbox['scores'] -+ -+ props_pred = torch.cat( -+ (per_vid_detections, per_vid_scores.unsqueeze(-1)), dim=-1) -+ -+ props_pred = props_pred.cpu().numpy() -+ props_pred = sorted(props_pred, key=lambda x: x[-1], reverse=True) -+ props_pred = np.array(props_pred) -+ -+ props_pred = soft_nms( -+ props_pred, -+ alpha=0.4, -+ low_threshold=0.5, -+ high_threshold=0.9, -+ top_k=5) -+ result = { -+ 'vid_name': data_samples[0].metainfo['vid_name'], -+ 'gt': gt_bbox[0].cpu().numpy(), -+ 'predictions': props_pred, -+ } -+ return [result] -+ -+ raise ValueError(f'Unsupported mode {mode}!') -+ -+ def nms_temporal(self, start, end, score, overlap=0.45): -+ pick = [] -+ assert len(start) == len(score) -+ assert len(end) == len(score) -+ if len(start) == 0: -+ return pick -+ -+ union = end - start -+ # sort and get index -+ intervals = [ -+ i[0] for i in sorted(enumerate(score), key=lambda x: x[1]) -+ ] -+ -+ while len(intervals) > 0: -+ i = intervals[-1] -+ pick.append(i) -+ -+ xx1 = [max(start[i], start[j]) for j in intervals[:-1]] -+ xx2 = [min(end[i], end[j]) for j in intervals[:-1]] -+ inter = [max(0., k2 - k1) for k1, k2 in zip(xx1, xx2)] -+ o = [ -+ inter[u] / (union[i] + union[intervals[u]] - inter[u]) -+ for u in range(len(intervals) - 1) -+ ] -+ I_new = [] -+ for j in range(len(o)): -+ if o[j] <= overlap: -+ I_new.append(intervals[j]) -+ intervals = I_new -+ return np.array(pick) -+ -+ def _forward(self, query_tokens, query_length, props_features, -+ props_start_end, gt_bbox): -+ -+ position_info = [props_start_end, props_start_end] -+ position_feats = [] -+ query_features = self.query_encoder(query_tokens, query_length) -+ for i in range(len(query_features)): -+ query_features[i] = self.qInput[i](query_features[i]) -+ if i > 1: -+ position_info.append( -+ torch.cat([ -+ props_start_end[:, ::2 * (i - 1), [0]], -+ props_start_end[:, 1::2 * (i - 1), [1]] -+ ], -+ dim=-1)) -+ props_duration = position_info[i][:, :, 1] - position_info[i][:, :, -+ 0] -+ props_duration = props_duration.unsqueeze(-1) -+ position_feat = torch.cat((position_info[i], props_duration), -+ dim=-1).float() -+ position_feats.append( -+ self.position_transform(position_feat).permute(0, 2, 1)) -+ -+ props_features = self.prop_fc(props_features) -+ -+ inputs = props_features.permute(0, 2, 1) -+ outputs = self.backbone_net(inputs, query_features, position_feats) -+ outputs = self.fpn(outputs) -+ -+ if self.is_second_stage: -+ outputs = [_.detach() for _ in outputs] -+ box_lists, loss_dict = self.fcos(outputs, gt_bbox.float()) -+ -+ return box_lists, loss_dict -diff -Naru ../orig/mmaction/models/localizers/drn/drn_utils/backbone.py mmaction/models/localizers/drn/drn_utils/backbone.py ---- ../orig/mmaction/models/localizers/drn/drn_utils/backbone.py 1970-01-01 09:00:00.000000000 +0900 -+++ mmaction/models/localizers/drn/drn_utils/backbone.py 2023-12-18 08:13:04.000000000 +0900 -@@ -0,0 +1,48 @@ -+# Copyright (c) OpenMMLab. All rights reserved. -+from typing import List, Tuple -+ -+import torch -+from torch import Tensor, nn -+ -+ -+def conv_block(in_channels: int, -+ out_channels: int, -+ kernel_size: int = 3, -+ stride: int = 1) -> nn.Module: -+ module = nn.Sequential( -+ nn.Conv1d( -+ in_channels, -+ out_channels, -+ kernel_size=kernel_size, -+ stride=stride, -+ padding=(kernel_size - 1) // 2, -+ bias=False), nn.BatchNorm1d(out_channels), nn.ReLU()) -+ return module -+ -+ -+class Backbone(nn.Module): -+ -+ def __init__(self, channels_list: List[tuple]) -> None: -+ super(Backbone, self).__init__() -+ -+ self.num_layers = len(channels_list) -+ layers = [] -+ for idx, channels_config in enumerate(channels_list): -+ layer = conv_block(*channels_config) -+ layers.append(layer) -+ self.layers = nn.ModuleList(layers) -+ -+ def forward(self, x: Tensor, query_fts: Tensor, -+ position_fts: Tensor) -> Tuple[Tensor]: -+ results = [] -+ -+ for idx in range(self.num_layers): -+ query_ft = query_fts[idx].unsqueeze(1).permute(0, 2, 1) -+ position_ft = position_fts[idx] -+ x = query_ft * x -+ if idx == 0: -+ x = torch.cat([x, position_ft], dim=1) -+ x = self.layers[idx](x) -+ results.append(x) -+ -+ return tuple(results) -diff -Naru ../orig/mmaction/models/localizers/drn/drn_utils/fcos.py mmaction/models/localizers/drn/drn_utils/fcos.py ---- ../orig/mmaction/models/localizers/drn/drn_utils/fcos.py 1970-01-01 09:00:00.000000000 +0900 -+++ mmaction/models/localizers/drn/drn_utils/fcos.py 2023-12-18 08:13:04.000000000 +0900 -@@ -0,0 +1,192 @@ -+# Copyright (c) OpenMMLab. All rights reserved. -+import math -+ -+import torch -+from torch import nn -+ -+from .inference import make_fcos_postprocessor -+from .loss import make_fcos_loss_evaluator -+ -+ -+class Scale(nn.Module): -+ -+ def __init__(self, init_value=1.0): -+ super(Scale, self).__init__() -+ self.scale = nn.Parameter(torch.FloatTensor([init_value])) -+ -+ def forward(self, x): -+ return x * self.scale -+ -+ -+class FCOSHead(torch.nn.Module): -+ -+ def __init__(self, in_channels: int, fcos_num_class: int, -+ fcos_conv_layers: int, fcos_prior_prob: float, -+ is_second_stage: bool) -> None: -+ super(FCOSHead, self).__init__() -+ num_classes = fcos_num_class - 1 -+ -+ cls_tower = [] -+ bbox_tower = [] -+ for i in range(fcos_conv_layers): -+ cls_tower.append( -+ nn.Conv1d( -+ in_channels, -+ in_channels, -+ kernel_size=3, -+ stride=1, -+ padding=1)) -+ cls_tower.append(nn.BatchNorm1d(in_channels)) -+ cls_tower.append(nn.ReLU()) -+ bbox_tower.append( -+ nn.Conv1d( -+ in_channels, -+ in_channels, -+ kernel_size=3, -+ stride=1, -+ padding=1)) -+ bbox_tower.append(nn.BatchNorm1d(in_channels)) -+ bbox_tower.append(nn.ReLU()) -+ -+ self.cls_tower = nn.Sequential(*cls_tower) -+ self.bbox_tower = nn.Sequential(*bbox_tower) -+ self.cls_logits = nn.Conv1d( -+ in_channels, num_classes, kernel_size=3, stride=1, padding=1) -+ -+ self.bbox_pred = nn.Conv1d( -+ in_channels, 2, kernel_size=3, stride=1, padding=1) -+ -+ self.mix_fc = nn.Sequential( -+ nn.Conv1d(2 * in_channels, in_channels, kernel_size=1, stride=1), -+ nn.BatchNorm1d(in_channels), nn.ReLU()) -+ -+ self.iou_scores = nn.Sequential( -+ nn.Conv1d( -+ in_channels, -+ in_channels // 2, -+ kernel_size=3, -+ stride=1, -+ padding=1), -+ nn.BatchNorm1d(in_channels // 2), -+ nn.ReLU(), -+ nn.Conv1d(in_channels // 2, 1, kernel_size=1, stride=1), -+ ) -+ -+ # initialization -+ for module in self.modules(): -+ if isinstance(module, nn.Conv1d): -+ torch.nn.init.normal_(module.weight, std=0.01) -+ torch.nn.init.constant_(module.bias, 0) -+ -+ # initialize the bias for focal loss -+ bias_value = -math.log((1 - fcos_prior_prob) / fcos_prior_prob) -+ torch.nn.init.constant_(self.cls_logits.bias, bias_value) -+ -+ self.scales = nn.ModuleList([Scale(init_value=1.0) for _ in range(3)]) -+ self.is_second_stage = is_second_stage -+ -+ def forward(self, x): -+ logits = [] -+ bbox_reg = [] -+ iou_scores = [] -+ for idx, feature in enumerate(x): -+ cls_tower = self.cls_tower(feature) -+ box_tower = self.bbox_tower(feature) -+ logits.append(self.cls_logits(cls_tower)) -+ -+ bbox_reg_ = torch.exp(self.scales[idx](self.bbox_pred(box_tower))) -+ if self.is_second_stage: -+ bbox_reg_ = bbox_reg_.detach() -+ bbox_reg.append(bbox_reg_) -+ -+ mix_feature = torch.cat([cls_tower, box_tower], dim=1) -+ if self.is_second_stage: -+ mix_feature = mix_feature.detach() -+ mix_feature = self.mix_fc(mix_feature) -+ iou_scores.append(self.iou_scores(mix_feature)) -+ return logits, bbox_reg, iou_scores -+ -+ -+class FCOSModule(torch.nn.Module): -+ -+ def __init__(self, in_channels: int, fcos_num_class: int, -+ fcos_conv_layers: int, fcos_prior_prob: float, -+ fcos_inference_thr: float, fcos_pre_nms_top_n: int, -+ fcos_nms_thr: float, test_detections_per_img: int, -+ fpn_stride: int, focal_alpha: float, focal_gamma: float, -+ is_first_stage: bool, is_second_stage: bool) -> None: -+ super(FCOSModule, self).__init__() -+ -+ head = FCOSHead( -+ in_channels=in_channels, -+ fcos_num_class=fcos_num_class, -+ fcos_conv_layers=fcos_conv_layers, -+ fcos_prior_prob=fcos_prior_prob, -+ is_second_stage=is_second_stage) -+ -+ self.is_first_stage = is_first_stage -+ self.is_second_stage = is_second_stage -+ box_selector_test = make_fcos_postprocessor(fcos_num_class, -+ fcos_inference_thr, -+ fcos_pre_nms_top_n, -+ fcos_nms_thr, -+ test_detections_per_img, -+ is_first_stage) -+ loss_evaluator = make_fcos_loss_evaluator(focal_alpha, focal_gamma) -+ self.head = head -+ self.box_selector_test = box_selector_test -+ self.loss_evaluator = loss_evaluator -+ self.fpn_strides = fpn_stride -+ -+ def forward(self, features, targets=None): -+ box_cls, box_regression, iou_scores = self.head(features) -+ locations = self.compute_locations(features) -+ -+ if self.training: -+ return self._forward_train(locations, box_cls, box_regression, -+ targets, iou_scores) -+ else: -+ return self._forward_test(locations, box_cls, box_regression, -+ targets, iou_scores) -+ -+ def _forward_train(self, locations, box_cls, box_regression, targets, -+ iou_scores): -+ loss_box_cls, loss_box_reg, loss_iou = self.loss_evaluator( -+ locations, box_cls, box_regression, targets, iou_scores, -+ self.is_first_stage) -+ -+ if self.is_second_stage: -+ loss_box_cls = loss_box_cls.detach() -+ loss_box_reg = loss_box_reg.detach() -+ if self.is_first_stage: -+ loss_iou = loss_iou.detach() -+ -+ losses = { -+ 'loss_cls': loss_box_cls, -+ 'loss_reg': loss_box_reg, -+ 'loss_iou': loss_iou -+ } -+ return None, losses -+ -+ def _forward_test(self, locations, box_cls, box_regression, targets, -+ iou_scores): -+ boxes = self.box_selector_test(locations, box_cls, box_regression, -+ iou_scores) -+ losses = None -+ return boxes, losses -+ -+ def compute_locations(self, features): -+ locations = [] -+ for level, feature in enumerate(features): -+ t = feature.size(-1) -+ locations_per_level = self.compute_locations_per_level( -+ t, self.fpn_strides[level], feature.device) -+ locations.append(locations_per_level) -+ return locations -+ -+ def compute_locations_per_level(self, t, stride, device): -+ shifts_t = torch.arange( -+ 0, t * stride, step=stride, dtype=torch.float32, device=device) -+ shifts_t = shifts_t.reshape(-1) -+ locations = shifts_t + stride / 2 -+ return locations -diff -Naru ../orig/mmaction/models/localizers/drn/drn_utils/FPN.py mmaction/models/localizers/drn/drn_utils/FPN.py ---- ../orig/mmaction/models/localizers/drn/drn_utils/FPN.py 1970-01-01 09:00:00.000000000 +0900 -+++ mmaction/models/localizers/drn/drn_utils/FPN.py 2023-12-18 08:13:04.000000000 +0900 -@@ -0,0 +1,44 @@ -+# Copyright (c) OpenMMLab. All rights reserved. -+from typing import List, Tuple -+ -+import torch.nn.functional as F -+from torch import Tensor, nn -+ -+from .backbone import conv_block -+ -+ -+class FPN(nn.Module): -+ -+ def __init__(self, in_channels_list: List, out_channels: int) -> None: -+ super(FPN, self).__init__() -+ -+ inner_blocks = [] -+ layer_blocks = [] -+ for idx, in_channels in enumerate(in_channels_list, 1): -+ inner_block = conv_block(in_channels, out_channels, 1, 1) -+ layer_block = conv_block(out_channels, out_channels, 3, 1) -+ -+ inner_blocks.append(inner_block) -+ layer_blocks.append(layer_block) -+ -+ self.inner_blocks = nn.ModuleList(inner_blocks) -+ self.layer_blocks = nn.ModuleList(layer_blocks) -+ -+ def forward(self, x: Tensor) -> Tuple[Tensor]: -+ # process the last lowest resolution feat and -+ # first feed it into 1 x 1 conv -+ last_inner = self.inner_blocks[-1](x[-1]) -+ results = [self.layer_blocks[-1](last_inner)] -+ -+ for feature, inner_block, layer_block in zip( -+ x[:-1][::-1], self.inner_blocks[:-1][::-1], -+ self.layer_blocks[:-1][::-1]): -+ if not inner_block: -+ continue -+ inner_top_down = F.interpolate( -+ last_inner, scale_factor=2, mode='nearest') -+ inner_lateral = inner_block(feature) -+ last_inner = inner_lateral + inner_top_down -+ results.insert(0, layer_block(last_inner)) -+ -+ return tuple(results) -diff -Naru ../orig/mmaction/models/localizers/drn/drn_utils/inference.py mmaction/models/localizers/drn/drn_utils/inference.py ---- ../orig/mmaction/models/localizers/drn/drn_utils/inference.py 1970-01-01 09:00:00.000000000 +0900 -+++ mmaction/models/localizers/drn/drn_utils/inference.py 2023-12-18 08:13:04.000000000 +0900 -@@ -0,0 +1,212 @@ -+# Copyright (c) OpenMMLab. All rights reserved. -+"""Copied from https://github.com/Alvin-Zeng/DRN/""" -+ -+import torch -+ -+ -+class FCOSPostProcessor(torch.nn.Module): -+ """Performs post-processing on the outputs of the RetinaNet boxes. -+ -+ This is only used in the testing. -+ """ -+ -+ def __init__(self, pre_nms_thresh, pre_nms_top_n, nms_thresh, -+ fpn_post_nms_top_n, min_size, num_classes, is_first_stage): -+ """ -+ Arguments: -+ pre_nms_thresh (float) -+ pre_nms_top_n (int) -+ nms_thresh (float) -+ fpn_post_nms_top_n (int) -+ min_size (int) -+ num_classes (int) -+ box_coder (BoxCoder) -+ """ -+ super(FCOSPostProcessor, self).__init__() -+ self.pre_nms_thresh = pre_nms_thresh -+ self.pre_nms_top_n = pre_nms_top_n -+ self.nms_thresh = nms_thresh -+ self.fpn_post_nms_top_n = fpn_post_nms_top_n -+ self.min_size = min_size -+ self.num_classes = num_classes -+ self.innerness_threshold = 0.15 -+ self.downsample_scale = 32 -+ self.is_first_stage = is_first_stage -+ -+ def forward_for_single_feature_map(self, locations, box_cls, -+ box_regression, level, iou_scores): -+ """ -+ Arguments: -+ anchors: list[BoxList] -+ box_cls: tensor of size N, A * C, H, W -+ box_regression: tensor of size N, A * 4, H, W -+ """ -+ N, C, T = box_cls.shape -+ -+ # put in the same format as locations -+ box_cls = box_cls.permute(0, 2, 1).contiguous().sigmoid() -+ iou_scores = iou_scores.permute(0, 2, 1).contiguous().sigmoid() -+ box_regression = box_regression.permute(0, 2, 1) -+ -+ # centerness = centerness.permute(0, 2, 1) -+ # centerness = centerness.reshape(N, -1).sigmoid() -+ # inner = inner.squeeze().sigmoid() -+ -+ candidate_inds = (box_cls > self.pre_nms_thresh) -+ pre_nms_top_n = candidate_inds.view(N, -1).sum(1) -+ pre_nms_top_n = pre_nms_top_n.clamp(max=self.pre_nms_top_n) -+ -+ # multiply the classification scores with centerness scores -+ # box_cls = box_cls * centerness[:, :, None] -+ # box_cls = box_cls + centerness[:, :, None] -+ if not self.is_first_stage: -+ box_cls = box_cls * iou_scores -+ -+ results = [] -+ for i in range(N): -+ -+ # per_centerness = centerness[i] -+ -+ per_box_cls = box_cls[i] -+ per_candidate_inds = candidate_inds[i] -+ per_box_cls = per_box_cls[per_candidate_inds] -+ -+ per_candidate_nonzeros = per_candidate_inds.nonzero() -+ per_box_loc = per_candidate_nonzeros[:, 0] -+ per_class = per_candidate_nonzeros[:, 1] + 1 -+ -+ per_box_regression = box_regression[i] -+ per_box_regression = per_box_regression[per_box_loc] -+ per_locations = locations[per_box_loc] -+ -+ # per_centerness = per_centerness[per_box_loc] -+ -+ per_pre_nms_top_n = pre_nms_top_n[i] -+ -+ if per_candidate_inds.sum().item() > per_pre_nms_top_n.item(): -+ per_box_cls, top_k_indices = \ -+ per_box_cls.topk(per_pre_nms_top_n, sorted=False) -+ per_class = per_class[top_k_indices] -+ per_box_regression = per_box_regression[top_k_indices] -+ per_locations = per_locations[top_k_indices] -+ -+ # per_centerness = per_centerness[top_k_indices] -+ -+ detections = torch.stack([ -+ per_locations - per_box_regression[:, 0], -+ per_locations + per_box_regression[:, 1], -+ ], -+ dim=1) / self.downsample_scale -+ -+ detections[:, 0].clamp_(min=0, max=1) -+ detections[:, 1].clamp_(min=0, max=1) -+ -+ # remove small boxes -+ p_start, p_end = detections.unbind(dim=1) -+ duration = p_end - p_start -+ keep = (duration >= self.min_size).nonzero().squeeze(1) -+ detections = detections[keep] -+ -+ temp_dict = {} -+ temp_dict['detections'] = detections -+ temp_dict['labels'] = per_class -+ temp_dict['scores'] = torch.sqrt(per_box_cls) -+ temp_dict['level'] = [level] -+ # temp_dict['centerness'] = per_centerness -+ temp_dict['locations'] = per_locations / 32 -+ -+ results.append(temp_dict) -+ -+ return results -+ -+ def forward(self, locations, box_cls, box_regression, iou_scores): -+ """ -+ Arguments: -+ anchors: list[list[BoxList]] -+ box_cls: list[tensor] -+ box_regression: list[tensor] -+ image_sizes: list[(h, w)] -+ Returns: -+ boxlists (list[BoxList]): the post-processed anchors, after -+ applying box decoding and NMS -+ """ -+ sampled_boxes = [] -+ for i, (l, o, b, iou_s) in enumerate( -+ zip(locations, box_cls, box_regression, iou_scores)): -+ sampled_boxes.append( -+ self.forward_for_single_feature_map(l, o, b, i, iou_s)) -+ -+ boxlists = list(zip(*sampled_boxes)) -+ # boxlists = [cat_boxlist(boxlist) for boxlist in boxlists] -+ boxlists = self.select_over_all_levels(boxlists) -+ -+ return boxlists -+ -+ # TODO very similar to filter_results from PostProcessor -+ # but filter_results is per image -+ # TODO Yang: solve this issue in the future. No good solution -+ # right now. -+ def select_over_all_levels(self, boxlists): -+ num_images = len(boxlists) -+ results = [] -+ for i in range(num_images): -+ dicts = boxlists[i] -+ per_vid_scores = [] -+ per_vid_detections = [] -+ per_vid_labels = [] -+ # add level number -+ per_vid_level = [] -+ per_vid_locations = [] -+ # per_vid_centerness = [] -+ for per_scale_dict in dicts: -+ if len(per_scale_dict['detections']) != 0: -+ per_vid_detections.append(per_scale_dict['detections']) -+ if len(per_scale_dict['scores']) != 0: -+ per_vid_scores.append(per_scale_dict['scores']) -+ if len(per_scale_dict['level']) != 0: -+ per_vid_level.append(per_scale_dict['level'] * -+ len(per_scale_dict['detections'])) -+ -+ if len(per_scale_dict['locations']) != 0: -+ per_vid_locations.append(per_scale_dict['locations']) -+ -+ # if len(per_scale_dict['centerness']) != 0: -+ # per_vid_centerness.append(per_scale_dict['centerness']) -+ if len(per_vid_detections) == 0: -+ per_vid_detections = torch.Tensor([0, 1]).unsqueeze(0) -+ per_vid_scores = torch.Tensor([1]) -+ per_vid_level = [[-1]] -+ per_vid_locations = torch.Tensor([0.5]) -+ # per_vid_centerness = torch.Tensor([0.5]).cuda() -+ else: -+ per_vid_detections = torch.cat(per_vid_detections, dim=0) -+ per_vid_scores = torch.cat(per_vid_scores, dim=0) -+ per_vid_level = per_vid_level -+ per_vid_locations = torch.cat(per_vid_locations, dim=0) -+ # per_vid_centerness = torch.cat(per_vid_centerness, dim=0) -+ -+ temp_dict = {} -+ temp_dict['detections'] = per_vid_detections -+ temp_dict['labels'] = per_vid_labels -+ temp_dict['scores'] = per_vid_scores -+ temp_dict['level'] = per_vid_level -+ # temp_dict['centerness'] = per_vid_centerness -+ temp_dict['locations'] = per_vid_locations -+ results.append(temp_dict) -+ -+ return results -+ -+ -+def make_fcos_postprocessor(fcos_num_class, fcos_inference_thr, -+ fcos_pre_nms_top_n, fcos_nms_thr, -+ test_detections_per_img, is_first_stage): -+ box_selector = FCOSPostProcessor( -+ pre_nms_thresh=fcos_inference_thr, -+ pre_nms_top_n=fcos_pre_nms_top_n, -+ nms_thresh=fcos_nms_thr, -+ fpn_post_nms_top_n=test_detections_per_img, -+ min_size=0, -+ num_classes=fcos_num_class, -+ is_first_stage=is_first_stage) -+ -+ return box_selector -diff -Naru ../orig/mmaction/models/localizers/drn/drn_utils/__init__.py mmaction/models/localizers/drn/drn_utils/__init__.py ---- ../orig/mmaction/models/localizers/drn/drn_utils/__init__.py 1970-01-01 09:00:00.000000000 +0900 -+++ mmaction/models/localizers/drn/drn_utils/__init__.py 2023-12-18 08:13:04.000000000 +0900 -@@ -0,0 +1,7 @@ -+# Copyright (c) OpenMMLab. All rights reserved. -+from .backbone import Backbone -+from .fcos import FCOSModule -+from .FPN import FPN -+from .language_module import QueryEncoder -+ -+__all__ = ['Backbone', 'FPN', 'QueryEncoder', 'FCOSModule'] -diff -Naru ../orig/mmaction/models/localizers/drn/drn_utils/language_module.py mmaction/models/localizers/drn/drn_utils/language_module.py ---- ../orig/mmaction/models/localizers/drn/drn_utils/language_module.py 1970-01-01 09:00:00.000000000 +0900 -+++ mmaction/models/localizers/drn/drn_utils/language_module.py 2023-12-18 08:13:04.000000000 +0900 -@@ -0,0 +1,92 @@ -+# Copyright (c) OpenMMLab. All rights reserved. -+from typing import List -+ -+import torch -+from torch import Tensor, nn -+from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence -+ -+ -+class QueryEncoder(nn.Module): -+ -+ def __init__(self, -+ vocab_size: int, -+ hidden_dim: int = 512, -+ embed_dim: int = 300, -+ num_layers: int = 1, -+ bidirection: bool = True) -> None: -+ super(QueryEncoder, self).__init__() -+ self.hidden_dim = hidden_dim -+ self.embed_dim = embed_dim -+ self.embedding = nn.Embedding( -+ num_embeddings=vocab_size + 1, -+ embedding_dim=embed_dim, -+ padding_idx=0) -+ # self.embedding.weight.data.copy_(torch.load('glove_weights')) -+ self.biLSTM = nn.LSTM( -+ input_size=embed_dim, -+ hidden_size=self.hidden_dim, -+ num_layers=num_layers, -+ dropout=0.0, -+ batch_first=True, -+ bidirectional=bidirection) -+ -+ self.W3 = nn.Linear(hidden_dim * 4, hidden_dim) -+ self.W2 = nn.ModuleList( -+ [nn.Linear(hidden_dim, hidden_dim * 2) for _ in range(3)]) -+ self.W1 = nn.Linear(hidden_dim * 2, 1) -+ -+ def extract_textual(self, q_encoding: Tensor, lstm_outputs: Tensor, -+ q_length: Tensor, t: int): -+ q_cmd = self.W3(q_encoding).relu() -+ q_cmd = self.W2[t](q_cmd) -+ q_cmd = q_cmd[:, None, :] * lstm_outputs -+ raw_att = self.W1(q_cmd).squeeze(-1) -+ -+ raw_att = apply_mask1d(raw_att, q_length) -+ att = raw_att.softmax(dim=-1) -+ cmd = torch.bmm(att[:, None, :], lstm_outputs).squeeze(1) -+ return cmd -+ -+ def forward(self, query_tokens: Tensor, -+ query_length: Tensor) -> List[Tensor]: -+ self.biLSTM.flatten_parameters() -+ -+ query_embedding = self.embedding(query_tokens) -+ -+ # output denotes the forward and backward hidden states in Eq 2. -+ query_embedding = pack_padded_sequence( -+ query_embedding, query_length.cpu(), batch_first=True) -+ output, _ = self.biLSTM(query_embedding) -+ output, _ = pad_packed_sequence(output, batch_first=True) -+ -+ # q_vector denotes the global representation `g` in Eq 2. -+ q_vector_list = [] -+ -+ for i, length in enumerate(query_length): -+ h1 = output[i][0] -+ hs = output[i][length - 1] -+ q_vector = torch.cat((h1, hs), dim=-1) -+ q_vector_list.append(q_vector) -+ q_vector = torch.stack(q_vector_list) -+ # outputs denotes the query feature in Eq3 in 3 levels. -+ outputs = [] -+ for cmd_t in range(3): -+ query_feat = self.extract_textual(q_vector, output, query_length, -+ cmd_t) -+ outputs.append(query_feat) -+ -+ # Note: the output here is zero-padded -+ # we need slice the non-zero items for the following operations. -+ return outputs -+ -+ -+def apply_mask1d(attention: Tensor, image_locs: Tensor) -> Tensor: -+ batch_size, num_loc = attention.size() -+ tmp1 = torch.arange( -+ num_loc, dtype=attention.dtype, device=attention.device) -+ tmp1 = tmp1.expand(batch_size, num_loc) -+ -+ tmp2 = image_locs.unsqueeze(dim=1).expand(batch_size, num_loc) -+ mask = tmp1 >= tmp2.to(tmp1.dtype) -+ attention = attention.masked_fill(mask, -1e30) -+ return attention -diff -Naru ../orig/mmaction/models/localizers/drn/drn_utils/loss.py mmaction/models/localizers/drn/drn_utils/loss.py ---- ../orig/mmaction/models/localizers/drn/drn_utils/loss.py 1970-01-01 09:00:00.000000000 +0900 -+++ mmaction/models/localizers/drn/drn_utils/loss.py 2023-12-18 08:13:04.000000000 +0900 -@@ -0,0 +1,240 @@ -+# Copyright (c) OpenMMLab. All rights reserved. -+"""Adapted from https://github.com/Alvin-Zeng/DRN/""" -+ -+import torch -+import torchvision -+from torch import nn -+ -+INF = 100000000 -+ -+ -+def SigmoidFocalLoss(alpha, gamma): -+ -+ def loss_fn(inputs, targets): -+ loss = torchvision.ops.sigmoid_focal_loss( -+ inputs=inputs, -+ targets=targets, -+ alpha=alpha, -+ gamma=gamma, -+ reduction='sum') -+ return loss -+ -+ return loss_fn -+ -+ -+def IOULoss(): -+ -+ def loss_fn(pred, target): -+ pred_left = pred[:, 0] -+ pred_right = pred[:, 1] -+ -+ target_left = target[:, 0] -+ target_right = target[:, 1] -+ -+ intersect = torch.min(pred_right, target_right) + torch.min( -+ pred_left, target_left) -+ target_area = target_left + target_right -+ pred_area = pred_left + pred_right -+ union = target_area + pred_area - intersect -+ -+ losses = -torch.log((intersect + 1e-8) / (union + 1e-8)) -+ return losses.mean() -+ -+ return loss_fn -+ -+ -+class FCOSLossComputation(object): -+ """This class computes the FCOS losses.""" -+ -+ def __init__(self, focal_alpha, focal_gamma): -+ self.cls_loss_fn = SigmoidFocalLoss(focal_alpha, focal_gamma) -+ self.box_reg_loss_fn = IOULoss() -+ self.centerness_loss_fn = nn.BCEWithLogitsLoss() -+ self.iou_loss_fn = nn.SmoothL1Loss() -+ -+ def prepare_targets(self, points, targets): -+ object_sizes_of_interest = [ -+ [-1, 6], -+ [5.6, 11], -+ [11, INF], -+ ] -+ expanded_object_sizes_of_interest = [] -+ for idx, points_per_level in enumerate(points): -+ object_sizes_of_interest_per_level = \ -+ points_per_level.new_tensor(object_sizes_of_interest[idx]) -+ expanded_object_sizes_of_interest.append( -+ object_sizes_of_interest_per_level[None].expand( -+ len(points_per_level), -1)) -+ -+ expanded_object_sizes_of_interest = torch.cat( -+ expanded_object_sizes_of_interest, dim=0) -+ num_points_per_level = [ -+ len(points_per_level) for points_per_level in points -+ ] -+ points_all_level = torch.cat(points, dim=0) -+ labels, reg_targets = self.compute_targets_for_locations( -+ points_all_level, targets, expanded_object_sizes_of_interest) -+ -+ for i in range(len(labels)): -+ labels[i] = torch.split(labels[i], num_points_per_level, dim=0) -+ reg_targets[i] = torch.split( -+ reg_targets[i], num_points_per_level, dim=0) -+ -+ labels_level_first = [] -+ reg_targets_level_first = [] -+ for level in range(len(points)): -+ labels_level_first.append( -+ torch.cat([labels_per_im[level] for labels_per_im in labels], -+ dim=0)) -+ reg_targets_level_first.append( -+ torch.cat([ -+ reg_targets_per_im[level] -+ for reg_targets_per_im in reg_targets -+ ], -+ dim=0)) -+ -+ return labels_level_first, reg_targets_level_first -+ -+ def compute_targets_for_locations(self, locations, targets, -+ object_sizes_of_interest): -+ labels = [] -+ reg_targets = [] -+ ts = locations -+ -+ for im_i in range(len(targets)): -+ targets_per_im = targets[im_i] -+ bboxes = targets_per_im * 32 -+ -+ left = ts[:, None] - bboxes[None, 0] -+ right = bboxes[None, 1] - ts[:, None] -+ reg_targets_per_im = torch.cat([left, right], dim=1) -+ -+ is_in_boxes = reg_targets_per_im.min(dim=1)[0] > 0 -+ max_reg_targets_per_im = reg_targets_per_im.max(dim=1)[0] -+ is_cared_in_the_level = \ -+ (max_reg_targets_per_im >= object_sizes_of_interest[:, 0]) & \ -+ (max_reg_targets_per_im <= object_sizes_of_interest[:, 1]) -+ -+ locations_to_gt_area = bboxes[1] - bboxes[0] -+ locations_to_gt_area = locations_to_gt_area.repeat( -+ len(locations), 1) -+ locations_to_gt_area[is_in_boxes == 0] = INF -+ locations_to_gt_area[is_cared_in_the_level == 0] = INF -+ -+ _ = locations_to_gt_area.min(dim=1) -+ locations_to_min_area, locations_to_gt_inds = _ -+ -+ labels_per_im = reg_targets_per_im.new_ones( -+ len(reg_targets_per_im)) -+ labels_per_im[locations_to_min_area == INF] = 0 -+ -+ labels.append(labels_per_im) -+ reg_targets.append(reg_targets_per_im) -+ -+ return labels, reg_targets -+ -+ def __call__(self, -+ locations, -+ box_cls, -+ box_regression, -+ targets, -+ iou_scores, -+ is_first_stage=True): -+ N = box_cls[0].size(0) -+ num_classes = box_cls[0].size(1) -+ labels, reg_targets = self.prepare_targets(locations, targets) -+ -+ box_cls_flatten = [] -+ box_regression_flatten = [] -+ # centerness_flatten = [] -+ labels_flatten = [] -+ reg_targets_flatten = [] -+ -+ for idx in range(len(labels)): -+ box_cls_flatten.append(box_cls[idx].permute(0, 2, 1).reshape( -+ -1, num_classes)) -+ box_regression_flatten.append(box_regression[idx].permute( -+ 0, 2, 1).reshape(-1, 2)) -+ labels_flatten.append(labels[idx].reshape(-1)) -+ reg_targets_flatten.append(reg_targets[idx].reshape(-1, 2)) -+ -+ if not is_first_stage: -+ # [batch, 56, 2] -+ merged_box_regression = torch.cat( -+ box_regression, dim=-1).transpose(2, 1) -+ # [56] -+ merged_locations = torch.cat(locations, dim=0) -+ # [batch, 56] -+ full_locations = merged_locations[None, :].expand( -+ merged_box_regression.size(0), -1).contiguous() -+ pred_start = full_locations - merged_box_regression[:, :, 0] -+ pred_end = full_locations + merged_box_regression[:, :, 1] -+ # [batch, 56, 2] -+ predictions = torch.cat( -+ [pred_start.unsqueeze(-1), -+ pred_end.unsqueeze(-1)], dim=-1) / 32 -+ # TODO: make sure the predictions are legal. (e.g. start < end) -+ predictions.clamp_(min=0, max=1) -+ # gt: [batch, 2] -+ gt_box = targets[:, None, :] -+ -+ iou_target = segment_tiou(predictions, gt_box) -+ iou_pred = torch.cat(iou_scores, dim=-1).squeeze().sigmoid() -+ iou_pos_ind = iou_target > 0.9 -+ pos_iou_target = iou_target[iou_pos_ind] -+ -+ pos_iou_pred = iou_pred[iou_pos_ind] -+ -+ if iou_pos_ind.sum().item() == 0: -+ iou_loss = torch.tensor([0.]).to(iou_pos_ind.device) -+ else: -+ iou_loss = self.iou_loss_fn(pos_iou_pred, pos_iou_target) -+ -+ box_cls_flatten = torch.cat(box_cls_flatten, dim=0) -+ box_regression_flatten = torch.cat(box_regression_flatten, dim=0) -+ labels_flatten = torch.cat(labels_flatten, dim=0) -+ reg_targets_flatten = torch.cat(reg_targets_flatten, dim=0) -+ -+ pos_inds = torch.nonzero(labels_flatten > 0).squeeze(1) -+ cls_loss = self.cls_loss_fn( -+ box_cls_flatten, labels_flatten.unsqueeze(1)) / ( -+ pos_inds.numel() + N) # add N to avoid dividing by a zero -+ -+ box_regression_flatten = box_regression_flatten[pos_inds] -+ reg_targets_flatten = reg_targets_flatten[pos_inds] -+ -+ if pos_inds.numel() > 0: -+ reg_loss = self.box_reg_loss_fn( -+ box_regression_flatten, -+ reg_targets_flatten, -+ ) -+ else: -+ reg_loss = box_regression_flatten.sum() -+ -+ if not is_first_stage: -+ return cls_loss, reg_loss, iou_loss -+ -+ return cls_loss, reg_loss, torch.tensor([0.]).to(cls_loss.device) -+ -+ -+def segment_tiou(box_a, box_b): -+ -+ # gt: [batch, 1, 2], detections: [batch, 56, 2] -+ # calculate interaction -+ inter_max_xy = torch.min(box_a[:, :, -1], box_b[:, :, -1]) -+ inter_min_xy = torch.max(box_a[:, :, 0], box_b[:, :, 0]) -+ inter = torch.clamp((inter_max_xy - inter_min_xy), min=0) -+ -+ # calculate union -+ union_max_xy = torch.max(box_a[:, :, -1], box_b[:, :, -1]) -+ union_min_xy = torch.min(box_a[:, :, 0], box_b[:, :, 0]) -+ union = torch.clamp((union_max_xy - union_min_xy), min=0) -+ -+ iou = inter / (union + 1e-6) -+ -+ return iou -+ -+ -+def make_fcos_loss_evaluator(focal_alpha, focal_gamma): -+ loss_evaluator = FCOSLossComputation(focal_alpha, focal_gamma) -+ return loss_evaluator -diff -Naru ../orig/mmaction/models/localizers/drn/__init__.py mmaction/models/localizers/drn/__init__.py ---- ../orig/mmaction/models/localizers/drn/__init__.py 1970-01-01 09:00:00.000000000 +0900 -+++ mmaction/models/localizers/drn/__init__.py 2023-12-18 08:13:04.000000000 +0900 -@@ -0,0 +1 @@ -+# Copyright (c) OpenMMLab. All rights reserved. diff --git a/src/otx/cli/registry/__init__.py b/src/otx/cli/registry/__init__.py new file mode 100644 index 00000000000..46dabf04e6e --- /dev/null +++ b/src/otx/cli/registry/__init__.py @@ -0,0 +1,22 @@ +"""Model templates registry.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .registry import Registry, find_and_parse_model_template + +__all__ = [ + "find_and_parse_model_template", + "Registry", +] diff --git a/src/otx/cli/registry/registry.py b/src/otx/cli/registry/registry.py new file mode 100644 index 00000000000..7359431269d --- /dev/null +++ b/src/otx/cli/registry/registry.py @@ -0,0 +1,135 @@ +"""Model templates registry.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import copy +import glob +import os +from pathlib import Path +from typing import Optional + +import yaml + +from otx.api.entities.model_template import parse_model_template +from otx.cli.utils.importing import get_backbone_list, get_otx_root_path + + +class Registry: + """Class that implements a model templates registry.""" + + def __init__(self, templates_dir=None, templates=None, experimental=False): + if templates is None: + if templates_dir is None: + templates_dir = os.getenv("TEMPLATES_DIR") + + if templates_dir is None: + raise RuntimeError("The templates_dir is not set.") + + template_filenames = glob.glob(os.path.join(templates_dir, "**", "template.yaml"), recursive=True) + if experimental: + template_filenames.extend( + glob.glob( + os.path.join(templates_dir, "**", "template_experimental.yaml"), + recursive=True, + ) + ) + template_filenames = [os.path.abspath(p) for p in template_filenames] + + self.templates = [] + + for template_file in template_filenames: + self.templates.append(parse_model_template(template_file)) + else: + self.templates = copy.deepcopy(templates) + + self.task_types = self.__collect_task_types(self.templates) + + @staticmethod + def __collect_task_types(templates): + return {template.task_type for template in templates} + + def filter(self, framework=None, task_type=None): + """Filters registry by framework and/or task type and returns filtered registry.""" + + templates = copy.deepcopy(self.templates) + if framework is not None: + templates = [template for template in templates if template.framework.lower() == framework.lower()] + if task_type is not None: + templates = [template for template in templates if str(template.task_type).lower() == task_type.lower()] + return Registry(templates=templates) + + def get(self, template_id, skip_error=False): + """Returns a model template with specified template_id or template.name.""" + + templates = [ + template + for template in self.templates + if str(template_id).upper() in (str(template.model_template_id).upper(), str(template.name).upper()) + ] + if not templates: + if skip_error: + return None + raise ValueError(f"Could not find a template with {template_id} in registry.") + return templates[0] + + def get_backbones(self, backend_list): + """Returns list of backbones for a given template.""" + backbone_list = {} + for backend in backend_list: + backbone_list[backend] = get_backbone_list(backend) + return backbone_list + + def __str__(self): + """Returns the string representation of the registry.""" + templates_infos = [ + { + "name": t.name, + "id": t.model_template_id, + "path": t.model_template_path, + "task_type": str(t.task_type), + } + for t in self.templates + ] + return yaml.dump(templates_infos) + + +def find_and_parse_model_template(path_or_id): + """In first function attempts to read a model template from disk under assumption that a path is passed. + + If the attempt is failed, it tries to find template in registry under assumption that an ID or name is passed. + """ + # Return None Type + if not path_or_id: + return path_or_id + + # 1. Find from path + if is_template(path_or_id): + return parse_model_template(path_or_id) + # 2. Find from id or Name + return Registry(get_otx_root_path()).get(path_or_id, skip_error=True) + + +def is_template(template_path: Optional[str]) -> bool: + """A function that determines whether the corresponding template path is a template. + + Args: + template_path (str): The path of the file you want to know if it is a template. + + Returns: + bool: True if template_path is template file else False. + """ + if template_path and Path(template_path).is_file() and "template" in Path(template_path).name: + return True + return False diff --git a/src/otx/cli/tools/__init__.py b/src/otx/cli/tools/__init__.py new file mode 100644 index 00000000000..70216d44bfb --- /dev/null +++ b/src/otx/cli/tools/__init__.py @@ -0,0 +1,15 @@ +"""OTX cli tools.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/cli/tools/build.py b/src/otx/cli/tools/build.py new file mode 100644 index 00000000000..2357534cc85 --- /dev/null +++ b/src/otx/cli/tools/build.py @@ -0,0 +1,140 @@ +"""OTX building command 'otx build'. + +This command allows you to build an OTX workspace, provide usable backbone configurations, +and build models with new backbone replacements. +""" +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from otx.cli.manager.config_manager import TASK_TYPE_TO_SUB_DIR_NAME, ConfigManager +from otx.cli.utils.parser import get_parser_and_hprams_data +from otx.utils.logger import config_logger + +SUPPORTED_TASKS = ( + "CLASSIFICATION", + "DETECTION", + "INSTANCE_SEGMENTATION", + "ROTATED_DETECTION", + "SEGMENTATION", + "ACTION_CLASSIFICATION", + "ACTION_DETECTION", + "VISUAL_PROMPTING", + "ANOMALY_CLASSIFICATION", + "ANOMALY_DETECTION", + "ANOMALY_SEGMENTATION", +) + + +def get_args(): + """Parses command line arguments.""" + parser, _, _ = get_parser_and_hprams_data() + + parser.add_argument( + "--train-data-roots", + help="Comma-separated paths to training data folders.", + ) + parser.add_argument("--train-ann-files", help="Comma-separated paths to train annotation files.") + parser.add_argument( + "--val-data-roots", + help="Comma-separated paths to validation data folders.", + ) + parser.add_argument("--val-ann-files", help="Comma-separated paths to train annotation files.") + parser.add_argument( + "--test-data-roots", + help="Comma-separated paths to test data folders.", + ) + parser.add_argument("--test-ann-files", help="Comma-separated paths to train annotation files.") + parser.add_argument( + "--unlabeled-data-roots", + help="Comma-separated paths to unlabeled data folders", + ) + parser.add_argument( + "--unlabeled-file-list", + help="Comma-separated paths to unlabeled file list", + ) + parser.add_argument("--task", help=f"The currently supported options: {SUPPORTED_TASKS}.", default="") + parser.add_argument( + "--train-type", + help=f"The currently supported options: {TASK_TYPE_TO_SUB_DIR_NAME.keys()}.", + type=str, + default=None, + ) + parser.add_argument( + "--workspace", + help="Path to the workspace where the command will run.", + default=None, + ) + parser.add_argument( + "--model", help="Enter the name of the model you want to use. (Ex. EfficientNet-B0).", default="" + ) + parser.add_argument( + "--backbone", + help="Available Backbone Type can be found using 'otx find --backbone {framework}'.\n" + "If there is an already created backbone configuration yaml file, enter the corresponding path.", + ) + parser.add_argument( + "--deterministic", + action="store_true", + help="Set deterministic to True, default=False.", + ) + parser.add_argument( + "--seed", + type=int, + help="Set seed for configuration.", + ) + + return parser.parse_args() + + +def main(): + """Main function for model or backbone or task building.""" + + args = get_args() + config_manager = ConfigManager(args, workspace_root=args.workspace, mode="build") + config_logger(config_manager.output_path / "otx.log", "INFO") + if args.task: + config_manager.task_type = args.task.upper() + + # Auto-Configuration for model template + config_manager.configure_template(model=args.model) + + config_manager.build_workspace(new_workspace_path=args.workspace) + + # Auto-Configuration for Dataset configuration + config_manager.configure_data_config() + + # Build Backbone related + if args.backbone: + from otx.cli.builder import Builder + + builder = Builder() + missing_args = [] + if not args.backbone.endswith((".yml", ".yaml", ".json")): + backbone_config_file = str(config_manager.workspace_root / "backbone.yaml") + missing_args = builder.build_backbone_config(args.backbone, backbone_config_file) + else: + backbone_config_file = args.backbone + if missing_args: + print( + f"[!] {args.backbone} backbone has inputs that the user must enter.\n" + f"[!] Edit {backbone_config_file} and run 'otx build --backbone backbone.yaml'." + ) + else: + builder.merge_backbone(config_manager.workspace_root / "model.py", backbone_config_file) + + return dict(retcode=0, task_type=args.task) + + +if __name__ == "__main__": + main() diff --git a/src/otx/cli/tools/cli.py b/src/otx/cli/tools/cli.py new file mode 100644 index 00000000000..86eb689d02d --- /dev/null +++ b/src/otx/cli/tools/cli.py @@ -0,0 +1,94 @@ +"""OTX CLI entry point.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import argparse +import sys + +from otx.cli.utils import telemetry + +from .build import main as otx_build +from .demo import main as otx_demo +from .deploy import main as otx_deploy +from .eval import main as otx_eval +from .explain import main as otx_explain +from .export import main as otx_export +from .find import main as otx_find +from .optimize import main as otx_optimize +from .train import main as otx_train + +__all__ = [ + "otx_demo", + "otx_deploy", + "otx_eval", + "otx_explain", + "otx_export", + "otx_find", + "otx_train", + "otx_optimize", + "otx_build", +] + + +def parse_args(): + """Parses command line arguments.""" + + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("operation", choices=[x[4:] for x in __all__], type=str) + + return parser.parse_known_args()[0] + + +def main(): + """Entry point for OTX CLI. + + This function is a single entry point for all OTX CLI related operations: + - build + - demo + - deploy + - eval + - explain + - export + - find + - train + - optimize + """ + + name = parse_args().operation + sys.argv[0] = f"otx {name}" + + del sys.argv[1] + + tm_session = telemetry.init_telemetry_session() + results = {} + try: + results = globals()[f"otx_{name}"]() + if results is None: + results = dict(retcode=0) + except Exception as error: + results["retcode"] = -1 + results["exception"] = repr(error) + telemetry.send_cmd_results(tm_session, name, results) + raise + else: + telemetry.send_cmd_results(tm_session, name, results) + finally: + telemetry.close_telemetry_session(tm_session) + + return results.get("retcode", 0) + + +if __name__ == "__main__": + main() diff --git a/src/otx/cli/tools/demo.py b/src/otx/cli/tools/demo.py new file mode 100644 index 00000000000..a1363c8966b --- /dev/null +++ b/src/otx/cli/tools/demo.py @@ -0,0 +1,192 @@ +"""Model inference demonstration tool.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import time +from collections import deque + +import cv2 +import numpy as np + +# Update environment variables for CLI use +import otx.cli # noqa: F401 +from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.datasets import DatasetEntity, DatasetItemEntity +from otx.api.entities.image import Image +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.utils.vis_utils import dump_frames +from otx.cli.manager import ConfigManager +from otx.cli.tools.utils.demo.images_capture import open_images_capture +from otx.cli.tools.utils.demo.visualization import draw_predictions, put_text_on_rect_bg +from otx.cli.utils.importing import get_impl_class +from otx.cli.utils.io import read_label_schema, read_model +from otx.cli.utils.parser import ( + add_hyper_parameters_sub_parser, + get_override_param, + get_parser_and_hprams_data, +) + +ESC_BUTTON = 27 + + +def get_args(): + """Parses command line arguments.""" + parser, hyper_parameters, params = get_parser_and_hprams_data() + + parser.add_argument( + "-i", + "--input", + required=True, + help="Source of input data: images folder, image, webcam and video.", + ) + parser.add_argument( + "--load-weights", + required=True, + help="Load model weights from previously saved checkpoint." + "It could be a trained/optimized model (POT only) or exported model.", + ) + parser.add_argument( + "--fit-to-size", + nargs=2, + type=int, + help="Width and Height space-separated values. " + "Fits displayed images to window with specified Width and Height. " + "This options applies to result visualisation only.", + ) + parser.add_argument("--loop", action="store_true", help="Enable reading the input in a loop.") + parser.add_argument( + "--delay", + type=int, + default=3, + help="Frame visualization time in ms. Negative delay value disables visualization", + ) + parser.add_argument( + "--display-perf", + action="store_true", + help="This option enables writing performance metrics on displayed frame. " + "These metrics take into account not only model inference time, but also " + "frame reading, pre-processing and post-processing.", + ) + parser.add_argument( + "--output", + default=None, + type=str, + help="Output path to save input data with predictions.", + ) + + add_hyper_parameters_sub_parser(parser, hyper_parameters, modes=("INFERENCE",)) + override_param = get_override_param(params) + + return parser.parse_args(), override_param + + +def get_predictions(task, frame): + """Returns list of predictions made by task on frame and time spent on doing prediction.""" + + empty_annotation = AnnotationSceneEntity(annotations=[], kind=AnnotationSceneKind.PREDICTION) + + item = DatasetItemEntity( + media=Image(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)), + annotation_scene=empty_annotation, + ) + + dataset = DatasetEntity(items=[item]) + + start_time = time.perf_counter() + predicted_validation_dataset = task.infer( + dataset, + InferenceParameters(is_evaluation=False), + ) + elapsed_time = time.perf_counter() - start_time + item = predicted_validation_dataset[0] + return item.get_annotations(), elapsed_time + + +def main(): + """Main function that is used for model demonstration.""" + + # Dynamically create an argument parser based on override parameters. + args, override_param = get_args() + + if args.loop and args.output: + raise ValueError("--loop and --output cannot be both specified") + + config_manager = ConfigManager(args, mode="demo") + # Auto-Configuration for model template + config_manager.configure_template() + + # Update Hyper Parameter Configs + hyper_parameters = config_manager.get_hyparams_config(override_param) + + # Get classes for Task, ConfigurableParameters and Dataset. + template = config_manager.template + if any(args.load_weights.endswith(x) for x in (".bin", ".xml", ".zip")): + task_class = get_impl_class(template.entrypoints.openvino) + elif args.load_weights.endswith(".pth"): + task_class = get_impl_class(template.entrypoints.base) + else: + raise ValueError(f"Unsupported file: {args.load_weights}") + + environment = TaskEnvironment( + model=None, + hyper_parameters=hyper_parameters, + label_schema=read_label_schema(args.load_weights), + model_template=template, + ) + + environment.model = read_model(environment.get_model_configuration(), args.load_weights, None) + + task = task_class(task_environment=environment) + + capture = open_images_capture(args.input, args.loop) + + elapsed_times = deque(maxlen=10) + saved_frames = [] + while True: + frame = capture.read() + if frame is None: + break + + predictions, elapsed_time = get_predictions(task, frame) + elapsed_times.append(elapsed_time) + elapsed_time = np.mean(elapsed_times) + + frame = draw_predictions(template.task_type, predictions, frame, args.fit_to_size) + if args.display_perf: + put_text_on_rect_bg( + frame, + f"time: {elapsed_time:.4f} sec.", + (0, frame.shape[0] - 30), + color=(255, 255, 255), + ) + + if args.delay > 0: + cv2.imshow("frame", frame) + if cv2.waitKey(args.delay) == ESC_BUTTON: + break + else: + print(f"Frame: {elapsed_time=}, {len(predictions)=}") + + if args.output: + saved_frames.append(frame) + + dump_frames(saved_frames, args.output, args.input, capture) + + return dict(retcode=0, template=template.name) + + +if __name__ == "__main__": + main() diff --git a/src/otx/cli/tools/deploy.py b/src/otx/cli/tools/deploy.py new file mode 100644 index 00000000000..a4321e0cd0f --- /dev/null +++ b/src/otx/cli/tools/deploy.py @@ -0,0 +1,96 @@ +"""Model deployment tool.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from pathlib import Path + +# Update environment variables for CLI use +import otx.cli # noqa: F401 +from otx.api.configuration.helper import create +from otx.api.entities.model import ModelEntity +from otx.api.entities.task_environment import TaskEnvironment +from otx.cli.manager import ConfigManager +from otx.cli.utils.importing import get_impl_class +from otx.cli.utils.io import read_label_schema, read_model +from otx.cli.utils.parser import get_parser_and_hprams_data +from otx.utils.logger import config_logger + + +def get_args(): + """Parses command line arguments.""" + parser, _, _ = get_parser_and_hprams_data() + + parser.add_argument( + "--load-weights", + help="Load model weights from previously saved checkpoint.", + ) + parser.add_argument( + "-o", + "--output", + help="Location where openvino.zip will be stored.", + ) + + return parser.parse_args() + + +def main(): + """Main function that is used for model evaluation.""" + + # Parses input arguments. + args = get_args() + config_manager = ConfigManager(args, mode="deploy") + config_logger(config_manager.output_path / "otx.log", "INFO") + # Auto-Configuration for model template + config_manager.configure_template() + + # Reads model template file. + template = config_manager.template + + # Get hyper parameters schema. + hyper_parameters = template.hyper_parameters.data + assert hyper_parameters + + if not args.load_weights: + raise RuntimeError("No appropriate OpenVINO exported model was found.") + + # Get classes for Task, ConfigurableParameters and Dataset. + if not args.load_weights.endswith(".bin") and not args.load_weights.endswith(".xml"): + raise RuntimeError("Only OpenVINO-exported models are supported.") + + task_class = get_impl_class(template.entrypoints.openvino) + + environment = TaskEnvironment( + model=None, + hyper_parameters=create(hyper_parameters), + label_schema=read_label_schema(args.load_weights), + model_template=template, + ) + environment.model = read_model(environment.get_model_configuration(), args.load_weights, None) + + task = task_class(task_environment=environment) + + deployed_model = ModelEntity(None, environment.get_model_configuration()) + + output_path = Path(args.output) if args.output else config_manager.output_path + output_path.mkdir(exist_ok=True, parents=True) + task.deploy(deployed_model) + with open(output_path / "openvino.zip", "wb") as write_file: + write_file.write(deployed_model.exportable_code) + + return dict(retcode=0, template=template.name) + + +if __name__ == "__main__": + main() diff --git a/src/otx/cli/tools/eval.py b/src/otx/cli/tools/eval.py new file mode 100644 index 00000000000..f609f5d3ded --- /dev/null +++ b/src/otx/cli/tools/eval.py @@ -0,0 +1,171 @@ +"""Model quality evaluation tool.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import json +from pathlib import Path + +# Update environment variables for CLI use +import otx.cli # noqa: F401 +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model_template import TaskType +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.cli.manager import ConfigManager +from otx.cli.utils.importing import get_impl_class +from otx.cli.utils.io import read_model +from otx.cli.utils.nncf import is_checkpoint_nncf +from otx.cli.utils.parser import ( + add_hyper_parameters_sub_parser, + get_override_param, + get_parser_and_hprams_data, +) +from otx.core.data.adapter import get_dataset_adapter +from otx.utils.logger import config_logger + +# pylint: disable=too-many-locals + + +def get_args(): + """Parses command line arguments.""" + parser, hyper_parameters, params = get_parser_and_hprams_data() + + parser.add_argument( + "--test-data-roots", + help="Comma-separated paths to test data folders.", + ) + parser.add_argument( + "--load-weights", + help="Load model weights from previously saved checkpoint. " + "It could be a trained/optimized model (with PTQ only) or exported model.", + ) + parser.add_argument( + "-o", + "--output", + help="Location where the intermediate output of the task will be stored.", + ) + parser.add_argument( + "--workspace", + help="Path to the workspace where the command will run.", + default=None, + ) + parser.add_argument( + "--data", + type=str, + default=None, + help="The data.yaml path want to use in train task.", + ) + + add_hyper_parameters_sub_parser(parser, hyper_parameters, modes=("INFERENCE",)) + override_param = get_override_param(params) + + return parser.parse_args(), override_param + + +def check_label_schemas(label_schema_a, label_schema_b): + """Checks that both passed label schemas have labels with the same names. + + If it is False that it raises RuntimeError. + """ + + for model_label, snapshot_label in zip(label_schema_a.get_labels(False), label_schema_b.get_labels(False)): + if model_label.name != snapshot_label.name: + raise RuntimeError( + "Labels schemas from model and dataset are different: " f"\n{label_schema_a} \n\tvs\n{label_schema_b}" + ) + + +def main(): + """Main function that is used for model evaluation.""" + + # Dynamically create an argument parser based on override parameters. + args, override_param = get_args() + + config_manager = ConfigManager(args, workspace_root=args.workspace, mode="eval") + config_logger(config_manager.output_path / "otx.log", "INFO") + # Auto-Configuration for model template + config_manager.configure_template() + + if not args.load_weights and config_manager.check_workspace(): + latest_model_path = ( + config_manager.workspace_root / "outputs" / "latest_trained_model" / "models" / "weights.pth" + ) + args.load_weights = str(latest_model_path) + + # Update Hyper Parameter Configs + hyper_parameters = config_manager.get_hyparams_config(override_param) + + # Get classes for Task, ConfigurableParameters and Dataset. + template = config_manager.template + if any(args.load_weights.endswith(x) for x in (".bin", ".xml", ".zip")): + task_class = get_impl_class(template.entrypoints.openvino) + elif args.load_weights.endswith(".pth"): + if is_checkpoint_nncf(args.load_weights): + task_class = get_impl_class(template.entrypoints.nncf) + else: + task_class = get_impl_class(template.entrypoints.base) + else: + raise ValueError(f"Unsupported file: {args.load_weights}") + + # Auto-Configuration for Dataset configuration + config_manager.configure_data_config(update_data_yaml=config_manager.check_workspace()) + dataset_config = config_manager.get_dataset_config(subsets=["test"]) + dataset_adapter = get_dataset_adapter(**dataset_config) + dataset, label_schema = dataset_adapter.get_otx_dataset(), dataset_adapter.get_label_schema() + + environment = TaskEnvironment( + model=None, + hyper_parameters=hyper_parameters, + label_schema=label_schema, + model_template=template, + ) + + environment.model = read_model(environment.get_model_configuration(), args.load_weights, None) + + task = task_class(task_environment=environment) + + validation_dataset = dataset.get_subset(Subset.TESTING) + predicted_validation_dataset = task.infer( + # temp (sungchul): remain annotation for visual prompting + validation_dataset + if getattr(task, "task_type", None) == TaskType.VISUAL_PROMPTING + else validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=False), + ) + + resultset = ResultSetEntity( + model=environment.model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + task.evaluate(resultset) + assert resultset.performance is not None + + output_path = Path(args.output) if args.output else config_manager.output_path + performance = {resultset.performance.score.name: resultset.performance.score.value} + if hasattr(resultset.performance, "additional_scores"): + for metric in resultset.performance.additional_scores: + performance[metric.name] = metric.value + if hasattr(task, "avg_time_per_image"): + performance["avg_time_per_image"] = task.avg_time_per_image + with open(output_path / "performance.json", "w", encoding="UTF-8") as write_file: + json.dump(performance, write_file) + + return dict(retcode=0, template=template.name) + + +if __name__ == "__main__": + main() diff --git a/src/otx/cli/tools/explain.py b/src/otx/cli/tools/explain.py new file mode 100644 index 00000000000..7cb276eed2f --- /dev/null +++ b/src/otx/cli/tools/explain.py @@ -0,0 +1,213 @@ +"""Model explain demonstration tool.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from pathlib import Path + +# Update environment variables for CLI use +import otx.cli # noqa: F401 +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.task_environment import TaskEnvironment +from otx.cli.manager import ConfigManager +from otx.cli.utils.importing import get_impl_class +from otx.cli.utils.io import ( + get_explain_dataset_from_filelist, + get_image_files, + read_label_schema, + read_model, + save_saliency_output, +) +from otx.cli.utils.nncf import is_checkpoint_nncf +from otx.cli.utils.parser import ( + add_hyper_parameters_sub_parser, + get_override_param, + get_parser_and_hprams_data, +) +from otx.utils.logger import config_logger, get_logger + +logger = get_logger() + +ESC_BUTTON = 27 +SUPPORTED_EXPLAIN_ALGORITHMS = ["activationmap", "eigencam", "classwisesaliencymap"] + +# pylint: disable=too-many-locals + + +def get_args(): + """Parses command line arguments.""" + parser, hyper_parameters, params = get_parser_and_hprams_data() + + parser.add_argument( + "-i", + "--input", + required=True, + help="Comma-separated paths to explain data folders.", + ) + parser.add_argument( + "-o", + "--output", + default="saliency_dump", + help="Output path for explanation images.", + ) + parser.add_argument( + "--load-weights", + required=True, + help="Load model weights from previously saved checkpoint.", + ) + parser.add_argument( + "--explain-algorithm", + default="ClassWiseSaliencyMap", + help=f"Explain algorithm name, currently support {SUPPORTED_EXPLAIN_ALGORITHMS}." + "For Openvino task, default method will be selected.", + ) + parser.add_argument( + "--process-saliency-maps", + action="store_true", + help="Processing of saliency map includes (1) resizing to input image resolution and (2) applying a colormap." + "Depending on the number of targets to explain, this might take significant time.", + ) + parser.add_argument( + "--explain-all-classes", + action="store_true", + help="Provides explanations for all classes. Otherwise, explains only predicted classes." + "This feature is supported by algorithms that can generate explanations per each class.", + ) + parser.add_argument( + "--overlay-weight", + type=float, + default=0.5, + help="Weight of the saliency map when overlaying the input image with saliency map.", + ) + add_hyper_parameters_sub_parser(parser, hyper_parameters, modes=("INFERENCE",)) + override_param = get_override_param(params) + + return parser.parse_args(), override_param + + +def _log_prior_to_saving(args, num_images): + logger.info("Explain report:") + if args.process_saliency_maps: + logger.info( + "Postprocessing applied. (1) saliency maps resized to the input image resolution " + "and (2) color map applied." + ) + else: + logger.info( + "No postprocessing applied. Raw low-resolution saliency maps saved as .tiff format images. " + "Use --process-saliency-maps to apply postprocessing to saliency maps." + ) + + if args.explain_all_classes: + logger.info(f"Saliency maps generated for each class, per each of {num_images} images.") + else: + logger.info( + "Saliency maps generated ONLY for predicted class(es), if any. " + "Use --explain-all-classes flag to generate explanations for all classes." + ) + + +def _log_after_saving(explain_predicted_classes, explained_image_counter, args, num_images): + if explain_predicted_classes and explained_image_counter == 0: + logger.info( + "No predictions were made for provided model-data pair -> no saliency maps generated. " + "Please adjust training pipeline or use different model-data pair." + ) + if explained_image_counter > 0: + logger.info(f"Saliency maps saved to {args.output} for {explained_image_counter} out of {num_images} images.") + + +def main(): + """Main function that is used for model explanation.""" + + args, override_param = get_args() + + config_manager = ConfigManager(args, mode="explain") + config_logger(config_manager.output_path / "otx.log", "INFO") + # Auto-Configuration for model template + config_manager.configure_template() + + # Update Hyper Parameter Configs + hyper_parameters = config_manager.get_hyparams_config(override_param) + + # Get classes for Task, ConfigurableParameters and Dataset. + template = config_manager.template + if any(args.load_weights.endswith(x) for x in (".bin", ".xml", ".zip")): + task_class = get_impl_class(template.entrypoints.openvino) + elif args.load_weights.endswith(".pth"): + if is_checkpoint_nncf(args.load_weights): + task_class = get_impl_class(template.entrypoints.nncf) + else: + task_class = get_impl_class(template.entrypoints.base) + else: + raise ValueError(f"Unsupported file: {args.load_weights}") + + environment = TaskEnvironment( + model=None, + hyper_parameters=hyper_parameters, + label_schema=read_label_schema(args.load_weights), + model_template=template, + ) + + environment.model = read_model(environment.get_model_configuration(), args.load_weights, None) + task = task_class(task_environment=environment) + + if args.explain_algorithm.lower() not in SUPPORTED_EXPLAIN_ALGORITHMS: + raise NotImplementedError( + f"{args.explain_algorithm} currently not supported. \ + Currently only support {SUPPORTED_EXPLAIN_ALGORITHMS}" + ) + if not Path(args.output).exists(): + Path(args.output).mkdir(parents=True) + + image_files = get_image_files(args.input) + dataset_to_explain = get_explain_dataset_from_filelist(image_files) + explain_predicted_classes = not args.explain_all_classes + explain_parameters = ExplainParameters( + explainer=args.explain_algorithm, + process_saliency_maps=args.process_saliency_maps, + explain_predicted_classes=explain_predicted_classes, + ) + explained_dataset = task.explain( + dataset_to_explain.with_empty_annotations(), + explain_parameters, + ) + assert len(explained_dataset) == len(image_files) + + _log_prior_to_saving(args, len(image_files)) + explained_image_counter = 0 + for explained_data, (_, filename) in zip(explained_dataset, image_files): + metadata_list = explained_data.get_metadata() + if len(metadata_list) > 0: + explained_image_counter += 1 + elif explain_predicted_classes: # Explain only predictions + logger.info(f"No saliency maps generated for {filename} - due to lack of confident predictions.") + for metadata in metadata_list: + saliency_data = metadata.data + fname = f"{Path(Path(filename).name).stem}_{saliency_data.name}".replace(" ", "_") + save_saliency_output( + process_saliency_maps=explain_parameters.process_saliency_maps, + img=explained_data.numpy, + saliency_map=saliency_data.numpy, + save_dir=args.output, + fname=fname, + weight=args.overlay_weight, + ) + _log_after_saving(explain_predicted_classes, explained_image_counter, args, len(image_files)) + + return dict(retcode=0, template=template.name) + + +if __name__ == "__main__": + main() diff --git a/src/otx/cli/tools/export.py b/src/otx/cli/tools/export.py new file mode 100644 index 00000000000..019c855c9ba --- /dev/null +++ b/src/otx/cli/tools/export.py @@ -0,0 +1,138 @@ +"""Model exporting tool.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from pathlib import Path + +# Update environment variables for CLI use +import otx.cli # noqa: F401 +from otx.api.entities.model import ModelEntity, ModelOptimizationType, ModelPrecision +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.adapters.model_adapter import ModelAdapter +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.cli.manager import ConfigManager +from otx.cli.utils.importing import get_impl_class +from otx.cli.utils.io import read_binary, read_label_schema, save_model_data +from otx.cli.utils.nncf import is_checkpoint_nncf +from otx.cli.utils.parser import add_hyper_parameters_sub_parser, get_override_param, get_parser_and_hprams_data +from otx.utils.logger import config_logger + + +def get_args(): + """Parses command line arguments.""" + parser, hyper_parameters, params = get_parser_and_hprams_data() + + parser.add_argument( + "--load-weights", + help="Load model weights from previously saved checkpoint.", + ) + parser.add_argument( + "-o", + "--output", + help="Location where exported model will be stored.", + ) + parser.add_argument( + "--workspace", + help="Path to the workspace where the command will run.", + default=None, + ) + parser.add_argument( + "--dump-features", + action="store_true", + help="Whether to return feature vector and saliency map for explanation purposes.", + ) + parser.add_argument( + "--half-precision", + action="store_true", + help="This flag indicated if model is exported in half precision (FP16).", + ) + parser.add_argument( + "--export-type", + help="Type of the resulting model (OpenVINO or ONNX).", + default="openvino", + ) + + add_hyper_parameters_sub_parser(parser, hyper_parameters, modes=("INFERENCE",)) + override_param = get_override_param(params) + + return parser.parse_args(), override_param + + +def main(): + """Main function that is used for model exporting.""" + args, override_param = get_args() + config_manager = ConfigManager(args, mode="export", workspace_root=args.workspace) + config_logger(config_manager.output_path / "otx.log", "INFO") + # Auto-Configuration for model template + config_manager.configure_template() + + # Load template.yaml file. + template = config_manager.template + + # Get class for Task. + if not args.load_weights and config_manager.check_workspace(): + latest_model_path = ( + config_manager.workspace_root / "outputs" / "latest_trained_model" / "models" / "weights.pth" + ) + args.load_weights = str(latest_model_path) + + is_nncf = is_checkpoint_nncf(args.load_weights) + task_class = get_impl_class(template.entrypoints.nncf if is_nncf else template.entrypoints.base) + + # Get hyper parameters schema. + hyper_parameters = config_manager.get_hyparams_config(override_param) + assert hyper_parameters + + environment = TaskEnvironment( + model=None, + hyper_parameters=hyper_parameters, + label_schema=read_label_schema(args.load_weights), + model_template=template, + ) + + model_adapters = {"weights.pth": ModelAdapter(read_binary(args.load_weights))} + model = ModelEntity( + configuration=environment.get_model_configuration(), + model_adapters=model_adapters, + train_dataset=None, + optimization_type=ModelOptimizationType.NNCF if is_nncf else ModelOptimizationType.NONE, + ) + environment.model = model + + (config_manager.output_path / "logs").mkdir(exist_ok=True, parents=True) + task = task_class(task_environment=environment, output_path=str(config_manager.output_path / "logs")) + + exported_model = ModelEntity(None, environment.get_model_configuration()) + + export_precision = ModelPrecision.FP16 if args.half_precision else ModelPrecision.FP32 + + if args.export_type.lower() not in ["openvino", "onnx"]: + raise ValueError("Unsupported export type") + export_type = ExportType.OPENVINO if "openvino" == args.export_type.lower() else ExportType.ONNX + task.export(export_type, exported_model, export_precision, args.dump_features) + + if not args.output: + output_path = config_manager.output_path + output_path = output_path / "openvino" + else: + output_path = Path(args.output) + output_path.mkdir(exist_ok=True, parents=True) + save_model_data(exported_model, str(output_path)) + + return dict(retcode=0, template=template.name) + + +if __name__ == "__main__": + main() diff --git a/src/otx/cli/tools/find.py b/src/otx/cli/tools/find.py new file mode 100644 index 00000000000..bbc8e17e360 --- /dev/null +++ b/src/otx/cli/tools/find.py @@ -0,0 +1,139 @@ +"""OTX searching command 'otx find'. + +Through this command, you can check the tasks, templates, and backbones available in OTX. +""" +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import argparse +import os +from textwrap import fill + +from prettytable import PrettyTable + +from otx.cli.registry import Registry +from otx.cli.utils.importing import SUPPORTED_BACKBONE_BACKENDS, get_otx_root_path + +# pylint: disable=too-many-locals + +SUPPORTED_TASKS = ( + "CLASSIFICATION", + "DETECTION", + "ROTATED_DETECTION", + "INSTANCE_SEGMENTATION", + "SEGMENTATION", + "ACTION_CLASSIFICATION", + "ACTION_DETECTION", + "VISUAL_PROMPTING", + "ANOMALY_CLASSIFICATION", + "ANOMALY_DETECTION", + "ANOMALY_SEGMENTATION", +) + + +def parse_args(): + """Parses command line arguments.""" + + parser = argparse.ArgumentParser() + parser.add_argument("--task", help=f"The currently supported options: {SUPPORTED_TASKS}.") + parser.add_argument( + "--template", action="store_true", help="Shows a list of templates that can be used immediately." + ) + parser.add_argument( + "--backbone", + nargs="+", + help=f"The currently supported options: {list(SUPPORTED_BACKBONE_BACKENDS.keys())}.", + ) + + return parser.parse_args() + + +def generate_backbone_rows(index: int, backbone_type: str, meta_data: dict): + """Generate table row for backbone json format. + + It expects a json file format from src/otx/cli/builder/supported_backbone. + index: The index of each backbone (int) + backbone_type: The backbone type want to add (str) + meta_data: This is the metadata of the backbone type (dict) + Metadata keys expect required, options, and available. + """ + max_row_width = 40 + rows = [] + required_args = meta_data["required"] if meta_data["required"] else [""] + required_options = meta_data["options"] + use_backbone_type = False + for arg in required_args: + row = [] + options = list(map(str, required_options[arg])) if arg in required_options else [] + if use_backbone_type: + row.append("") # Index + row.append("") # backbone_type + else: + row.append(str(index)) # Index + row.append(backbone_type) # backbone_type + use_backbone_type = True + row.append(arg) # Required-Args + option_str = ", ".join(options) if options else "" + row.append(fill(option_str, width=max_row_width)) # Options + rows.append(row) + return rows + + +def main(): + """Main function for model templates & backbone searching. + + When the template argment is input, the templates based on the otx folder are displayed. + Given a backbone argument as input, + it displays a list of backbones available in the backend of the relevant task. + """ + + args = parse_args() + + otx_root = get_otx_root_path() + otx_registry = Registry(otx_root).filter(task_type=args.task) + + if not args.backbone or args.template: + template_table = PrettyTable(["TASK", "ID", "NAME", "BASE PATH"]) + templates = sorted(otx_registry.templates, key=lambda x: x.name) + for template in templates: + relpath = os.path.relpath(template.model_template_path, os.path.abspath(".")) + template_table.add_row( + [ + template.task_type, + template.model_template_id, + template.name, + relpath, + ] + ) + print(template_table) + + if args.backbone: + all_backbones = otx_registry.get_backbones(args.backbone) + backbone_table = PrettyTable(["Index", "Backbone Type", "Required-Args", "Options"]) + row_index = 1 + for _, backbone_meta in all_backbones.items(): + for backbone_type, meta_data in backbone_meta.items(): + available_task = meta_data.get("available", []) + if not available_task or (args.task and args.task.upper() not in available_task): + continue + rows = generate_backbone_rows(row_index, backbone_type, meta_data) + backbone_table.add_rows(rows) + row_index += 1 + print(backbone_table) + + return dict(retcode=0, task_type=args.task) + + +if __name__ == "__main__": + main() diff --git a/src/otx/cli/tools/optimize.py b/src/otx/cli/tools/optimize.py new file mode 100644 index 00000000000..c94c723243d --- /dev/null +++ b/src/otx/cli/tools/optimize.py @@ -0,0 +1,180 @@ +"""Model optimization tool.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import json +from pathlib import Path + +# Update environment variables for CLI use +import otx.cli # noqa: F401 +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.cli.manager import ConfigManager +from otx.cli.utils.importing import get_impl_class +from otx.cli.utils.io import read_model, save_model_data +from otx.cli.utils.parser import ( + add_hyper_parameters_sub_parser, + get_override_param, + get_parser_and_hprams_data, +) +from otx.core.data.adapter import get_dataset_adapter +from otx.utils.logger import config_logger + +# pylint: disable=too-many-locals + + +def get_args(): + """Parses command line arguments. + + It dynamically generates help for hyper-parameters which are specific to particular model template. + """ + parser, hyper_parameters, params = get_parser_and_hprams_data() + + parser.add_argument( + "--train-data-roots", + help="Comma-separated paths to training data folders.", + ) + parser.add_argument( + "--val-data-roots", + help="Comma-separated paths to validation data folders.", + ) + parser.add_argument( + "--unlabeled-data-roots", + help="Comma-separated paths to unlabeled data folders.", + ) + parser.add_argument( + "--load-weights", + help="Load weights of trained model", + ) + parser.add_argument( + "-o", + "--output", + help="Location where optimized model will be stored.", + ) + parser.add_argument( + "--workspace", + help="Location where the intermediate output of the task will be stored.", + default=None, + ) + + add_hyper_parameters_sub_parser(parser, hyper_parameters) + override_param = get_override_param(params) + + return parser.parse_args(), override_param + + +def main(): + """Main function that is used for model training.""" + + # Dynamically create an argument parser based on override parameters. + args, override_param = get_args() + + config_manager = ConfigManager(args, workspace_root=args.workspace, mode="optimize") + config_logger(config_manager.output_path / "otx.log", "INFO") + # Auto-Configuration for model template + config_manager.configure_template() + + # The default in the workspace is the model weight of the OTX train. + if not args.load_weights and config_manager.check_workspace(): + latest_model_path = ( + config_manager.workspace_root / "outputs" / "latest_trained_model" / "models" / "weights.pth" + ) + args.load_weights = str(latest_model_path) + + is_ptq = False + if args.load_weights.endswith(".bin") or args.load_weights.endswith(".xml"): + is_ptq = True + + template = config_manager.template + if not is_ptq and template.entrypoints.nncf is None: + raise RuntimeError(f"Optimization by NNCF is not available for template {args.template}") + + # Update Hyper Parameter Configs + hyper_parameters = config_manager.get_hyparams_config(override_param) + + # Get classes for Task, ConfigurableParameters and Dataset. + task_class = get_impl_class(template.entrypoints.openvino if is_ptq else template.entrypoints.nncf) + + # Auto-Configuration for Dataset configuration + config_manager.configure_data_config(update_data_yaml=config_manager.check_workspace()) + dataset_config = config_manager.get_dataset_config(subsets=["train", "val", "unlabeled"]) + dataset_adapter = get_dataset_adapter(**dataset_config) + dataset, label_schema = dataset_adapter.get_otx_dataset(), dataset_adapter.get_label_schema() + + environment = TaskEnvironment( + model=None, + hyper_parameters=hyper_parameters, + label_schema=label_schema, + model_template=template, + ) + + environment.model = read_model(environment.get_model_configuration(), args.load_weights, None) + + task = task_class(task_environment=environment) + + output_model = ModelEntity(dataset, environment.get_model_configuration()) + + task.optimize( + OptimizationType.POT if is_ptq else OptimizationType.NNCF, + dataset, + output_model, + OptimizationParameters(), + ) + + opt_method = "ptq" if is_ptq else "nncf" + if not args.output: + output_path = config_manager.output_path + output_path = output_path / opt_method + else: + output_path = Path(args.output) + output_path.mkdir(exist_ok=True, parents=True) + save_model_data(output_model, output_path) + + validation_dataset = dataset.get_subset(Subset.VALIDATION) + predicted_validation_dataset = task.infer( + # temp (sungchul): remain annotation for visual prompting + validation_dataset + if getattr(task, "task_type", None) == TaskType.VISUAL_PROMPTING + else validation_dataset.with_empty_annotations(), + InferenceParameters(is_evaluation=False), + ) + + resultset = ResultSetEntity( + model=output_model, + ground_truth_dataset=validation_dataset, + prediction_dataset=predicted_validation_dataset, + ) + task.evaluate(resultset) + assert resultset.performance is not None + print(resultset.performance) + + performance_file_path = config_manager.output_path / f"{opt_method}_performance.json" + with open(performance_file_path, "w", encoding="UTF-8") as write_file: + json.dump( + {resultset.performance.score.name: resultset.performance.score.value}, + write_file, + ) + + return dict(retcode=0, template=template.name) + + +if __name__ == "__main__": + main() diff --git a/src/otx/cli/tools/train.py b/src/otx/cli/tools/train.py new file mode 100644 index 00000000000..5cf62ec2118 --- /dev/null +++ b/src/otx/cli/tools/train.py @@ -0,0 +1,345 @@ +"""Model training tool.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=too-many-locals + +import datetime +import time +from contextlib import ExitStack +from pathlib import Path +from typing import Optional + +# Update environment variables for CLI use +import otx.cli # noqa: F401 +from otx.api.entities.model import ModelEntity +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.train_parameters import TrainParameters +from otx.api.serialization.label_mapper import label_schema_to_bytes +from otx.api.usecases.adapters.model_adapter import ModelAdapter +from otx.cli.manager import ConfigManager +from otx.cli.manager.config_manager import TASK_TYPE_TO_SUB_DIR_NAME +from otx.cli.utils.experiment import ResourceTracker +from otx.cli.utils.hpo import run_hpo +from otx.cli.utils.importing import get_impl_class +from otx.cli.utils.io import read_binary, read_label_schema, save_model_data +from otx.cli.utils.multi_gpu import MultiGPUManager, is_multigpu_child_process +from otx.cli.utils.parser import ( + MemSizeAction, + add_hyper_parameters_sub_parser, + get_override_param, + get_parser_and_hprams_data, +) +from otx.cli.utils.report import get_otx_report +from otx.core.data.adapter import get_dataset_adapter +from otx.utils.logger import config_logger + + +def get_args(): + """Parses command line arguments.""" + parser, hyper_parameters, params = get_parser_and_hprams_data() + + parser.add_argument( + "--train-data-roots", + help="Comma-separated paths to training data folders.", + ) + parser.add_argument("--train-ann-files", help="Comma-separated paths to train annotation files.") + parser.add_argument( + "--val-data-roots", + help="Comma-separated paths to validation data folders.", + ) + parser.add_argument("--val-ann-files", help="Comma-separated paths to train annotation files.") + parser.add_argument( + "--unlabeled-data-roots", + help="Comma-separated paths to unlabeled data folders", + ) + parser.add_argument( + "--unlabeled-file-list", + help="Comma-separated paths to unlabeled file list", + ) + parser.add_argument( + "--train-type", + help=f"The currently supported options: {TASK_TYPE_TO_SUB_DIR_NAME.keys()}. " + "Will be difined automatically if no value passed.", + type=str, + default=None, + ) + parser.add_argument( + "--load-weights", + help="Load model weights from previously saved checkpoint.", + ) + parser.add_argument( + "--resume-from", + help="Resume training from previously saved checkpoint", + ) + parser.add_argument( + "-o", + "--output", + help="Location where outputs (model & logs) will be stored.", + ) + parser.add_argument( + "--workspace", + help="Location where the intermediate output of the training will be stored.", + default=None, + ) + parser.add_argument( + "--enable-hpo", + action="store_true", + help="Execute hyper parameters optimization (HPO) before training.", + ) + parser.add_argument( + "--hpo-time-ratio", + default=4, + type=float, + help="Expected ratio of total time to run HPO to time taken for full fine-tuning.", + ) + parser.add_argument( + "--gpus", + type=str, + help="Comma-separated indices of GPU. \ + If there are more than one available GPU, then model is trained with multi GPUs.", + ) + parser.add_argument( + "--rdzv-endpoint", + type=str, + default="localhost:0", + help="Rendezvous endpoint for multi-node training.", + ) + parser.add_argument( + "--base-rank", + type=int, + default=0, + help="Base rank of the current node workers.", + ) + parser.add_argument( + "--world-size", + type=int, + default=0, + help="Total number of workers in a worker group.", + ) + parser.add_argument( + "--mem-cache-size", + action=MemSizeAction, + dest="params.algo_backend.mem_cache_size", + type=str, + required=False, + help="Size of memory pool for caching decoded data to load data faster. " + "For example, you can use digits for bytes size (e.g. 1024) or a string with size units " + "(e.g. 7KiB = 7 * 2^10, 3MB = 3 * 10^6, and 2G = 2 * 2^30).", + ) + parser.add_argument( + "--deterministic", + action="store_true", + help="Set deterministic to True, default=False.", + ) + parser.add_argument( + "--seed", + type=int, + help="Set seed for training.", + ) + parser.add_argument( + "--data", + type=str, + default=None, + help="The data.yaml path want to use in train task.", + ) + parser.add_argument( + "--encryption-key", + type=str, + default=None, + help="Encryption key required to train the encrypted dataset. It is not required the non-encrypted dataset", + ) + parser.add_argument( + "--track-resource-usage", + type=str, + default=None, + help="Track resources utilization and max memory usage and save values at the output path. " + "The possible options are 'cpu', 'gpu' or you can set to a comma-separated list of resource types. " + "And 'all' is also available for choosing all resource types.", + ) + + sub_parser = add_hyper_parameters_sub_parser(parser, hyper_parameters, return_sub_parser=True) + # TODO: Temporary solution for cases where there is no template input + override_param = get_override_param(params) + if not hyper_parameters and "params" in params: + if "params" in params: + params = params[params.index("params") :] + for param in params: + if param == "--help": + print("Without template configuration, hparams information is unknown.") + elif param.startswith("--"): + sub_parser.add_argument( + f"{param}", + dest=f"params.{param[2:]}", + ) + return parser.parse_args(), override_param + + +def main(): + """Main function that invoke train function with ExitStack.""" + with ExitStack() as exit_stack: + return train(exit_stack) + + +def train(exit_stack: Optional[ExitStack] = None): # pylint: disable=too-many-branches, too-many-statements + """Function that is used for model training.""" + start_time = time.time() + mode = "train" + args, override_param = get_args() + + config_manager = ConfigManager(args, workspace_root=args.workspace, mode=mode) + config_logger(config_manager.output_path / "otx.log", "INFO") + # Auto-Configuration for model template + config_manager.configure_template() + + # Creates a workspace if it doesn't exist. + if not config_manager.check_workspace(): + config_manager.build_workspace(new_workspace_path=args.workspace) + + # Update Hyper Parameter Configs + hyper_parameters = config_manager.get_hyparams_config(override_param=override_param) + + # Auto-Configuration for Dataset configuration + config_manager.configure_data_config(update_data_yaml=config_manager.check_workspace()) + dataset_config = config_manager.get_dataset_config( + subsets=["train", "val", "unlabeled"], + hyper_parameters=hyper_parameters, + ) + dataset_adapter = get_dataset_adapter(**dataset_config) + dataset, label_schema = dataset_adapter.get_otx_dataset(), dataset_adapter.get_label_schema() + # Get classes for Task, ConfigurableParameters and Dataset. + template = config_manager.template + task_class = get_impl_class(template.entrypoints.base) + + environment = TaskEnvironment( + model=None, + hyper_parameters=hyper_parameters, + label_schema=label_schema, + model_template=template, + ) + + if args.load_weights or args.resume_from: + ckpt_path = args.resume_from if args.resume_from else args.load_weights + model_adapters = { + "path": ckpt_path, + "weights.pth": ModelAdapter(read_binary(ckpt_path)), + "resume": bool(args.resume_from), + } + + if (Path(ckpt_path).parent / "label_schema.json").exists(): + model_adapters.update( + {"label_schema.json": ModelAdapter(label_schema_to_bytes(read_label_schema(ckpt_path)))} + ) + + environment.model = ModelEntity( + train_dataset=dataset, + configuration=environment.get_model_configuration(), + model_adapters=model_adapters, # type: ignore + ) + + if args.enable_hpo: + environment = run_hpo( + args.hpo_time_ratio, config_manager.output_path, environment, dataset, config_manager.data_config + ) + + (config_manager.output_path / "logs").mkdir(exist_ok=True, parents=True) + + if args.gpus: + multigpu_manager = MultiGPUManager( + train, + args.gpus, + args.rdzv_endpoint, + args.base_rank, + args.world_size, + datetime.datetime.fromtimestamp(start_time), + ) + if ( + multigpu_manager.is_available() + and not template.task_type.is_anomaly # anomaly tasks don't use this way for multi-GPU training + ): + multigpu_manager.setup_multi_gpu_train( + str(config_manager.output_path), hyper_parameters if args.enable_hpo else None + ) + if exit_stack is not None: + exit_stack.callback(multigpu_manager.finalize) + else: + print( + "Warning: due to abstract of ExitStack context, " + "if main process raises an error, all processes can be stuck." + ) + + task = task_class(task_environment=environment, output_path=str(config_manager.output_path / "logs")) + + output_model = ModelEntity(dataset, environment.get_model_configuration()) + + resource_tracker = None + if args.track_resource_usage and not is_multigpu_child_process(): + resource_tracker = ResourceTracker( + config_manager.output_path / "resource_usage.yaml", args.track_resource_usage, args.gpus + ) + resource_tracker.start() + if exit_stack is not None: + exit_stack.callback(resource_tracker.stop) + + task.train( + dataset, output_model, train_parameters=TrainParameters(), seed=args.seed, deterministic=args.deterministic + ) + + if resource_tracker is not None and exit_stack is None: + resource_tracker.stop() + + model_path = config_manager.output_path / "models" + save_model_data(output_model, str(model_path)) + + end_time = time.time() + sec = end_time - start_time + total_time = str(datetime.timedelta(seconds=sec)) + print("otx train time elapsed: ", total_time) + model_results = { + "time elapsed": total_time, + "score": output_model.performance, + "model_path": str(model_path.absolute()), + } + + if args.gpus and exit_stack is None: + multigpu_manager.finalize() + elif is_multigpu_child_process(): + return + + get_otx_report( + model_template=config_manager.template, + task_config=task.config, + data_config=config_manager.data_config, + results=model_results, + output_path=config_manager.output_path / "cli_report.log", + ) + print(f"otx train CLI report has been generated: {config_manager.output_path / 'cli_report.log'}") + + # Latest model folder symbolic link to models + latest_path = config_manager.workspace_root / "outputs" / "latest_trained_model" + if latest_path.exists(): + latest_path.unlink() + elif not latest_path.parent.exists(): + latest_path.parent.mkdir(exist_ok=True, parents=True) + latest_path.symlink_to(config_manager.output_path.resolve()) + + if not is_multigpu_child_process(): + task.cleanup() + + return dict(retcode=0, template=template.name) + + +if __name__ == "__main__": + main() diff --git a/src/otx/cli/tools/utils/__init__.py b/src/otx/cli/tools/utils/__init__.py new file mode 100644 index 00000000000..86c94e1f5b1 --- /dev/null +++ b/src/otx/cli/tools/utils/__init__.py @@ -0,0 +1,15 @@ +"""CLI tools for OTX.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/cli/tools/utils/demo/__init__.py b/src/otx/cli/tools/utils/demo/__init__.py new file mode 100644 index 00000000000..eaaf6d4c18f --- /dev/null +++ b/src/otx/cli/tools/utils/demo/__init__.py @@ -0,0 +1,15 @@ +"""Helpers for exportable code demo.py.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/cli/tools/utils/demo/images_capture.py b/src/otx/cli/tools/utils/demo/images_capture.py new file mode 100644 index 00000000000..23002e85762 --- /dev/null +++ b/src/otx/cli/tools/utils/demo/images_capture.py @@ -0,0 +1,217 @@ +"""Images capturing module.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import copy +import os +import sys + +import cv2 + +# Taken from here: +# https://github.com/openvinotoolkit/open_model_zoo/blob/develop/demos/common/python/images_capture.py + + +class InvalidInput(Exception): + """Exception for wrong input format.""" + + def __init__(self, message): + super().__init__() + self.message = message + + +class OpenError(Exception): + """Exception for error opening reader.""" + + def __init__(self, message): + super().__init__() + self.message = message + + +class ImagesCapture: + """Images capturing base class.""" + + def read(self): + """Returns captured image.""" + raise NotImplementedError + + def fps(self): + """Returns a frequency of getting images from source.""" + raise NotImplementedError + + def get_type(self): + """Returns type of image capture.""" + raise NotImplementedError + + +class ImreadWrapper(ImagesCapture): + """Class for reading an image from file.""" + + def __init__(self, source, loop): + self.loop = loop + if not os.path.isfile(source): + raise InvalidInput(f"Can't find the image by {source}") + self.image = cv2.imread(source, cv2.IMREAD_COLOR) + if self.image is None: + raise OpenError(f"Can't open the image from {source}") + self.can_read = True + + def read(self): + """Returns captured image.""" + if self.loop: + return copy.deepcopy(self.image) + if self.can_read: + self.can_read = False + return copy.deepcopy(self.image) + return None + + def fps(self): + """Returns a frequency of getting images from source.""" + return 1.0 + + def get_type(self): + """Returns type of image capture.""" + return "IMAGE" + + +class DirReader(ImagesCapture): + """Class for reading images from directory.""" + + def __init__(self, source, loop): + self.loop = loop + self.dir = source + if not os.path.isdir(self.dir): + raise InvalidInput(f"Can't find the dir by {source}") + self.names = sorted(os.listdir(self.dir)) + if not self.names: + raise OpenError(f"The dir {source} is empty") + self.file_id = 0 + for name in self.names: + filename = os.path.join(self.dir, name) + image = cv2.imread(filename, cv2.IMREAD_COLOR) + if image is not None: + return + raise OpenError(f"Can't read the first image from {source}") + + def read(self): + """Returns captured image.""" + while self.file_id < len(self.names): + filename = os.path.join(self.dir, self.names[self.file_id]) + image = cv2.imread(filename, cv2.IMREAD_COLOR) + self.file_id += 1 + if image is not None: + return image + if self.loop: + self.file_id = 0 + while self.file_id < len(self.names): + filename = os.path.join(self.dir, self.names[self.file_id]) + image = cv2.imread(filename, cv2.IMREAD_COLOR) + self.file_id += 1 + if image is not None: + return image + return None + + def fps(self): + """Returns a frequency of getting images from source.""" + return 1.0 + + def get_type(self): + """Returns type of image capture.""" + return "DIR" + + +class VideoCapWrapper(ImagesCapture): + """Class for capturing images from video.""" + + def __init__(self, source, loop): + self.loop = loop + self.cap = cv2.VideoCapture() + status = self.cap.open(source) + if not status: + raise InvalidInput(f"Can't open the video from {source}") + + def read(self): + """Returns captured image.""" + status, image = self.cap.read() + if not status: + if not self.loop: + return None + self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + status, image = self.cap.read() + if not status: + return None + return image + + def fps(self): + """Returns a frequency of getting images from source.""" + return self.cap.get(cv2.CAP_PROP_FPS) + + def get_type(self): + """Returns type of image capture.""" + return "VIDEO" + + +class CameraCapWrapper(ImagesCapture): + """Class for capturing images from camera.""" + + def __init__(self, source, camera_resolution): + self.cap = cv2.VideoCapture() + try: + status = self.cap.open(int(source)) + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, camera_resolution[0]) + self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, camera_resolution[1]) + self.cap.set(cv2.CAP_PROP_FPS, 30) + self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 1) + self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG")) + if not status: + raise OpenError(f"Can't open the camera from {source}") + except ValueError as ex: + raise InvalidInput(f"Can't find the camera {source}") from ex + + def read(self): + """Returns captured image.""" + status, image = self.cap.read() + if not status: + return None + return image + + def fps(self): + """Returns a frequency of getting images from source.""" + return self.cap.get(cv2.CAP_PROP_FPS) + + def get_type(self): + """Returns type of image capture.""" + return "CAMERA" + + +def open_images_capture(source, loop, camera_resolution=(1280, 720)): + """Opens images capture.""" + + errors = {InvalidInput: [], OpenError: []} + for reader in (ImreadWrapper, DirReader, VideoCapWrapper): + try: + return reader(source, loop) + except (InvalidInput, OpenError) as ex: + errors[type(ex)].append(ex.message) + try: + return CameraCapWrapper(source, camera_resolution) + except (InvalidInput, OpenError) as ex: + errors[type(ex)].append(ex.message) + if not errors[OpenError]: + print(*errors[InvalidInput], file=sys.stderr, sep="\n") + else: + print(*errors[OpenError], file=sys.stderr, sep="\n") + sys.exit(1) diff --git a/src/otx/cli/tools/utils/demo/visualization.py b/src/otx/cli/tools/utils/demo/visualization.py new file mode 100644 index 00000000000..e405d61ff7c --- /dev/null +++ b/src/otx/cli/tools/utils/demo/visualization.py @@ -0,0 +1,163 @@ +"""Visualisation module.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +from typing import List, Tuple +from warnings import warn + +import cv2 +import numpy as np +from cv2 import Mat + +from otx.api.entities.annotation import Annotation +from otx.api.entities.model_template import TaskType +from otx.api.entities.shapes.polygon import Polygon +from otx.api.entities.shapes.rectangle import Rectangle + + +def put_text_on_rect_bg(frame: Mat, message: str, position: Tuple[int, int], color=(255, 255, 0)): + """Puts a text message on a black rectangular aread in specified position of a frame.""" + + font_face = cv2.FONT_HERSHEY_COMPLEX + font_scale = 1 + thickness = 1 + color_bg = (0, 0, 0) + x, y = position + text_size, _ = cv2.getTextSize(message, font_face, font_scale, thickness) + text_w, text_h = text_size + cv2.rectangle(frame, position, (x + text_w + 1, y + text_h + 1), color_bg, -1) + cv2.putText( + frame, + message, + (x, y + text_h + font_scale - 1), + font_face, + font_scale, + color, + thickness, + ) + return text_size + + +def draw_masks(frame: Mat, predictions, put_object_count: bool = False): + """Converts predictions to masks and draw them on frame.""" + + frame = frame.copy() + height, width = frame.shape[0], frame.shape[1] + segments_image = frame.copy() + aggregated_mask = np.zeros(frame.shape[:2], dtype=np.uint8) + aggregated_colored_mask = np.zeros(frame.shape, dtype=np.uint8) + for prediction in predictions: + if not isinstance(prediction.shape, Polygon): + continue + contours = np.array([[(int(p.x * width), int(p.y * height)) for p in prediction.shape.points]]) + assert len(prediction.get_labels()) == 1 + label = prediction.get_labels()[0] + color = tuple(getattr(label.color, x) for x in ("blue", "green", "red")) + mask = np.zeros(shape=(height, width), dtype=np.uint8) + cv2.drawContours(mask, contours, -1, 255, -1) + cv2.drawContours(frame, contours, -1, color, 1) + rect = cv2.boundingRect(contours[0]) + cv2.rectangle(frame, (rect[0], rect[1]), (rect[0] + rect[2], rect[1] + rect[3]), color, 1) + put_text_on_rect_bg(frame, f"{label.name} {label.probability*100:.1f}%", (rect[0], rect[1]), color=color) + cv2.bitwise_or(aggregated_mask, mask, dst=aggregated_mask) + cv2.bitwise_or( + aggregated_colored_mask, + np.asarray(color, dtype=np.uint8), + dst=aggregated_colored_mask, + mask=mask, + ) + # Fill the area occupied by all instances with a colored instances mask image. + cv2.bitwise_and( + segments_image, + np.zeros(3, dtype=np.uint8), + dst=segments_image, + mask=aggregated_mask, + ) + cv2.bitwise_or( + segments_image, + aggregated_colored_mask, + dst=segments_image, + mask=aggregated_mask, + ) + # Blend original image with the one, where instances are colored. + # As a result instances masks become transparent. + cv2.addWeighted(frame, 0.5, segments_image, 0.5, 0, dst=frame) + + if put_object_count: + put_text_on_rect_bg(frame, f"Obj. count: {len(predictions)}", (0, 0)) + return frame + + +def put_labels(frame: Mat, predictions: List[Annotation]): + """Converts predictions to text labels and puts them to the top left corner of a frame.""" + + frame = frame.copy() + assert len(predictions) == 1 + # TODO (ilya-krylov): handle multi-label classification + assert len(predictions[0].get_labels()) == 1 + label = predictions[0].get_labels()[0] + color = tuple(getattr(label.color, x) for x in ("blue", "green", "red")) + put_text_on_rect_bg(frame, f"{label.name} {label.probability*100:.1f}%", (0, 0), color=color) + return frame + + +def draw_bounding_boxes(frame: Mat, predictions: List[Annotation], put_object_count: bool): + """Converts predictions to bounding boxes and draws them on a frame.""" + + frame = frame.copy() + height, width = frame.shape[0], frame.shape[1] + for prediction in predictions: + if isinstance(prediction.shape, Rectangle): + x1 = int(prediction.shape.x1 * width) + x2 = int(prediction.shape.x2 * width) + y1 = int(prediction.shape.y1 * height) + y2 = int(prediction.shape.y2 * height) + assert len(prediction.get_labels()) == 1 + label = prediction.get_labels()[0] + color = tuple(getattr(label.color, x) for x in ("blue", "green", "red")) + cv2.rectangle(frame, (x1, y1), (x2, y2), color, thickness=2) + put_text_on_rect_bg(frame, f"{label.name} {label.probability*100:.1f}%", (x1, y1), color=color) + else: + warn( + f"Predictions called on Annotations with shape {type(prediction.shape)}." + "Expected shape to be of type Rectangle." + ) + + if put_object_count: + put_text_on_rect_bg(frame, f"Obj. count: {len(predictions)}", (0, 0)) + return frame + + +def draw_predictions(task_type: TaskType, predictions: List[Annotation], frame: Mat, fit_to_size: Tuple[int, int]): + """Converts predictions to visual representations depending on task type and draws them on a frame.""" + + width, height = frame.shape[1], frame.shape[0] + if fit_to_size: + ratio_x = fit_to_size[0] / width + ratio_y = fit_to_size[1] / height + ratio = min(ratio_x, ratio_y) + frame = cv2.resize(frame, None, fx=ratio, fy=ratio) + if task_type in {TaskType.DETECTION, TaskType.ANOMALY_DETECTION}: + frame = draw_bounding_boxes(frame, predictions, put_object_count=True) + elif task_type in {TaskType.CLASSIFICATION, TaskType.ANOMALY_CLASSIFICATION}: + frame = put_labels(frame, predictions) + elif task_type in {TaskType.INSTANCE_SEGMENTATION, TaskType.ROTATED_DETECTION}: + frame = draw_masks(frame, predictions, put_object_count=True) + elif task_type in {TaskType.SEGMENTATION, TaskType.ANOMALY_SEGMENTATION}: + frame = draw_masks(frame, predictions, put_object_count=False) + else: + raise ValueError(f"Unknown task type: {task_type}") + return frame diff --git a/src/otx/cli/utils/__init__.py b/src/otx/cli/utils/__init__.py index 653e2b12b9b..e5c1b432263 100644 --- a/src/otx/cli/utils/__init__.py +++ b/src/otx/cli/utils/__init__.py @@ -1,21 +1,5 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""CLI Utils.""" - -from __future__ import annotations - -from pathlib import Path - +"""OTX cli utils.""" -def absolute_path(path: str | Path | None) -> str | None: - """Returns the absolute path of the given path. - - Args: - path (str | Path | None): The path to be resolved. - - Returns: - str | None: The absolute path of the given path, or None if the path is None. - - """ - return str(Path(path).resolve()) if path is not None else None +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/cli/utils/config.py b/src/otx/cli/utils/config.py new file mode 100644 index 00000000000..c0b003979f6 --- /dev/null +++ b/src/otx/cli/utils/config.py @@ -0,0 +1,66 @@ +"""Utils for working with Configurable parameters.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from pathlib import Path + +import yaml + + +def override_parameters(overrides, parameters): + """Overrides parameters values by overrides.""" + + allowed_keys = {"default_value", "value"} + for k, val in overrides.items(): + if isinstance(val, dict): + if k in parameters.keys(): + override_parameters(val, parameters[k]) + else: + raise ValueError(f'The "{k}" is not in original parameters.') + elif k in allowed_keys: + parameters[k] = val + else: + raise ValueError(f'The "{k}" is not in allowed_keys: {allowed_keys}') + + +def configure_dataset(args, data_yaml_path=None): + """Configure dataset args.""" + + # Create instances of Task, ConfigurableParameters and Dataset. + data_subset_format = {"ann-files": None, "data-roots": None} + data_config = {"data": {subset: data_subset_format.copy() for subset in ("train", "val", "test")}} + data_config["data"]["unlabeled"] = {"file-list": None, "data-roots": None} + if data_yaml_path and Path(data_yaml_path).exists(): + with open(Path(data_yaml_path), "r", encoding="UTF-8") as stream: + data_config = yaml.safe_load(stream) + + # The command's args are overridden and use first + if "train_ann_files" in args and args.train_ann_files: + data_config["data"]["train"]["ann-files"] = str(Path(args.train_ann_files).absolute()) + if "train_data_roots" in args and args.train_data_roots: + data_config["data"]["train"]["data-roots"] = str(Path(args.train_data_roots).absolute()) + if "val_ann_files" in args and args.val_ann_files: + data_config["data"]["val"]["ann-files"] = str(Path(args.val_ann_files).absolute()) + if "val_data_roots" in args and args.val_data_roots: + data_config["data"]["val"]["data-roots"] = str(Path(args.val_data_roots).absolute()) + if "unlabeled_file_list" in args and args.unlabeled_file_list: + data_config["data"]["unlabeled"]["file-list"] = str(Path(args.unlabeled_file_list).absolute()) + if "unlabeled_data_roots" in args and args.unlabeled_data_roots: + data_config["data"]["unlabeled"]["data-roots"] = str(Path(args.unlabeled_data_roots).absolute()) + if "test_ann_files" in args and args.test_ann_files: + data_config["data"]["test"]["ann-files"] = str(Path(args.test_ann_files).absolute()) + if "test_data_roots" in args and args.test_data_roots: + data_config["data"]["test"]["data-roots"] = str(Path(args.test_data_roots).absolute()) + return data_config diff --git a/src/otx/cli/utils/errors.py b/src/otx/cli/utils/errors.py new file mode 100644 index 00000000000..3b5bc9657b7 --- /dev/null +++ b/src/otx/cli/utils/errors.py @@ -0,0 +1,30 @@ +"""Utils for CLI errors.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +class CliException(Exception): + """Custom exception class for CLI.""" + + +class ConfigValueError(CliException): + """Configuration value is not suitable for CLI.""" + + def __init__(self, message): + super().__init__(message) + + +class NotSupportedError(CliException): + """Not supported error.""" + + def __init__(self, message): + super().__init__(message) + + +class FileNotExistError(CliException): + """Not exist given configuration.""" + + def __init__(self, message): + super().__init__(message) diff --git a/src/otx/cli/utils/experiment.py b/src/otx/cli/utils/experiment.py new file mode 100644 index 00000000000..a642aea6ca3 --- /dev/null +++ b/src/otx/cli/utils/experiment.py @@ -0,0 +1,251 @@ +"""Utils function for experiments.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import multiprocessing as mp +import os +import time +from abc import ABC, abstractmethod +from pathlib import Path +from statistics import mean +from typing import Any, Dict, List, Optional, Union, no_type_check + +import psutil +import yaml + +from otx.utils.logger import get_logger + +try: + import pynvml +except ImportError: + pynvml = None + +logger = get_logger() +GIB = 1024**3 +AVAILABLE_RESOURCE_TYPE = ["cpu", "gpu"] + + +class ResourceTracker: + """Class to track resources usage. + + Args: + output_path (Union[str, Path]): Output file path to save CPU & GPU utilization and max meory usage values. + resource_type (str, optional): Which resource to track. Available values are cpu, gpu or all now. + Defaults to "all". + gpu_ids (Optional[str]): GPU indices to record. + """ + + def __init__(self, output_path: Union[str, Path], resource_type: str = "all", gpu_ids: Optional[str] = None): + if isinstance(output_path, str): + output_path = Path(output_path) + self.output_path = output_path + if resource_type == "all": + self._resource_type = AVAILABLE_RESOURCE_TYPE + else: + self._resource_type = [val for val in resource_type.split(",")] + + gpu_ids_arr = None + if gpu_ids is not None: + gpu_ids_arr = [int(idx) for idx in gpu_ids.split(",")] + gpu_ids_arr[0] = 0 + + self._gpu_ids: Union[List[int], None] = gpu_ids_arr + self._mem_check_proc: Union[mp.Process, None] = None + self._queue: Union[mp.Queue, None] = None + + def start(self): + """Run a process which tracks resources usage.""" + if self._mem_check_proc is not None: + logger.warning("Resource tracker started already. Please execute start after executing stop.") + return + + self._queue = mp.Queue() + self._mem_check_proc = mp.Process( + target=_check_resource, args=(self._queue, self._resource_type, self._gpu_ids) + ) + self._mem_check_proc.start() + + def stop(self): + """Terminate a process to record resources usage.""" + if self._mem_check_proc is None or not self._mem_check_proc.is_alive(): + return + + self._queue.put(self.output_path) + self._mem_check_proc.join(10) + if self._mem_check_proc.exitcode is None: + self._mem_check_proc.terminate() + self._mem_check_proc.close() + + self._mem_check_proc = None + self._queue = None + + +def _check_resource(queue: mp.Queue, resource_types: Optional[List[str]] = None, gpu_ids: Optional[List[int]] = None): + if resource_types is None: + resource_types = [] + + trackers: Dict[str, ResourceRecorder] = {} + for resource_type in resource_types: + if resource_type == "cpu": + trackers[resource_type] = CpuUsageRecorder() + elif resource_type == "gpu": + if pynvml is None: + logger.warning("GPU can't be found. Tracking GPU usage is skipped.") + continue + trackers[resource_type] = GpuUsageRecorder(gpu_ids) + else: + raise ValueError( + "Resource type {} isn't supported now. Current available types are cpu and gpu.".format(resource_type) + ) + + if not trackers: + logger.warning("There is no resource to record.") + return + + while True: + for tracker in trackers.values(): + tracker.record() + + if not queue.empty(): + break + + time.sleep(0.01) + + output_path = Path(queue.get()) + + resource_record = {resource_type: tracker.report() for resource_type, tracker in trackers.items()} + with output_path.open("w") as f: + yaml.dump(resource_record, f, default_flow_style=False) + + +class ResourceRecorder(ABC): + """Base calss for each resource recorder.""" + + @abstractmethod + def record(self): + """Record a resource usage.""" + raise NotImplementedError + + @abstractmethod + def report(self): + """Aggregate all resource usages.""" + raise NotImplementedError + + +class CpuUsageRecorder(ResourceRecorder): + """CPU usage recorder class. + + Args: + target_process Optional[psutil.Process]: Process to track. + """ + + def __init__(self): + self._record_count: int = 0 + self._max_mem: Union[int, float] = 0 + self._avg_util: Union[int, float] = 0 + self._first_record = True + + def record(self): + """Record CPU usage.""" + # cpu mem + memory_info = psutil.virtual_memory() + cpu_mem = (memory_info.total - memory_info.available) / GIB + if self._max_mem < cpu_mem: + self._max_mem = cpu_mem + + # cpu util + cpu_percent = psutil.cpu_percent() + if self._first_record: + self._first_record = False + else: + self._avg_util += cpu_percent + self._record_count += 1 + + def report(self) -> Dict[str, str]: + """Aggregate CPU usage.""" + if self._record_count == 0: + return {} + + return { + "max_memory_usage": f"{round(self._max_mem, 2)} GiB", + "avg_util": f"{round(self._avg_util / self._record_count, 2)} %", + } + + +class GpuUsageRecorder(ResourceRecorder): + """GPU usage recorder class. + + Args: + gpu_ids Optional[List[int]]: GPU indices to record. If not given, first GPU is recorded. + """ + + def __init__(self, gpu_ids: Optional[List[int]] = None): + if gpu_ids is None: + gpu_ids = [0] + + self._record: Dict[int, Dict[str, Union[int, List[int]]]] = {} + self._gpu_handlers: Dict[int, Any] = {} + + pynvml.nvmlInit() + gpu_to_track = self._get_gpu_to_track(gpu_ids) + for gpu_idx in gpu_to_track: + self._record[gpu_idx] = {"max_mem": 0, "util_record": []} + self._gpu_handlers[gpu_idx] = pynvml.nvmlDeviceGetHandleByIndex(gpu_idx) + + def _get_gpu_to_track(self, gpu_ids: List[int]) -> List[int]: + if "CUDA_VISIBLE_DEVICES" in os.environ: + avaiable_gpus = [int(idx) for idx in os.environ["CUDA_VISIBLE_DEVICES"].split(",")] + else: + avaiable_gpus = list(range(pynvml.nvmlDeviceGetCount())) + return [avaiable_gpus[gpu_idx] for gpu_idx in gpu_ids] + + def record(self): + """Record GPU usage.""" + for gpu_idx, record in self._record.items(): + # gpu util + gpu_info = pynvml.nvmlDeviceGetUtilizationRates(self._gpu_handlers[gpu_idx]) + record["util_record"].append(gpu_info.gpu) + + # gpu mem + gpu_mem = pynvml.nvmlDeviceGetMemoryInfo(self._gpu_handlers[gpu_idx]) + mem_used = gpu_mem.used / GIB + if record["max_mem"] < mem_used: + record["max_mem"] = mem_used + + @no_type_check + def report(self) -> Dict[str, str]: + """Aggregate GPU usage.""" + if not list(self._record.values())[0]["util_record"]: # record isn't called + return {} + + total_max_mem = 0 + total_avg_util = 0 + gpus_record = self._record.copy() + for gpu_idx in list(gpus_record.keys()): + max_mem = gpus_record[gpu_idx]["max_mem"] + if total_max_mem < max_mem: + total_max_mem = max_mem + + # Count utilization after it becomes bigger than 20% of max utilization + max_util = max(gpus_record[gpu_idx]["util_record"]) + for idx, util in enumerate(gpus_record[gpu_idx]["util_record"]): + if util * 5 > max_util: + break + avg_util = mean(gpus_record[gpu_idx]["util_record"][idx:]) + total_avg_util += avg_util + + gpus_record[f"gpu_{gpu_idx}"] = { + "avg_util": f"{round(avg_util, 2)} %", + "max_mem": f"{round(max_mem, 2)} GiB", + } + del gpus_record[gpu_idx] + + gpus_record["total_avg_util"] = f"{round(total_avg_util / len(gpus_record), 2)} %" + gpus_record["total_max_mem"] = f"{round(total_max_mem, 2)} GiB" + + return gpus_record + + def __del__(self): + """Shutdown nvml.""" + pynvml.nvmlShutdown() diff --git a/src/otx/cli/utils/help_formatter.py b/src/otx/cli/utils/help_formatter.py deleted file mode 100644 index 6da48f2eca5..00000000000 --- a/src/otx/cli/utils/help_formatter.py +++ /dev/null @@ -1,247 +0,0 @@ -"""Custom Help Formatters for OTX CLI.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import re -import sys -from typing import TYPE_CHECKING, Iterable - -from jsonargparse import DefaultHelpFormatter -from rich.markdown import Markdown -from rich.panel import Panel -from rich_argparse import RichHelpFormatter - -if TYPE_CHECKING: - import argparse - - from rich.console import RenderableType - - -BASE_ARGUMENTS = {"config", "print_config", "help", "engine", "model", "model.help", "task"} -ENGINE_ARGUMENTS = {"data_root", "engine.help", "engine.device", "work_dir"} -REQUIRED_ARGUMENTS = { - "train": { - "data", - "optimizer", - "scheduler", - "engine.checkpoint", - *BASE_ARGUMENTS, - *ENGINE_ARGUMENTS, - }, - "test": { - "data", - "checkpoint", - *BASE_ARGUMENTS, - *ENGINE_ARGUMENTS, - }, - "predict": { - "data", - "checkpoint", - "return_predictions", - *BASE_ARGUMENTS, - *ENGINE_ARGUMENTS, - }, - "export": { - "checkpoint", - "export_format", - "export_precision", - *BASE_ARGUMENTS, - *ENGINE_ARGUMENTS, - }, - "explain": { - "data", - "checkpoint", - "explain_config", - *BASE_ARGUMENTS, - *ENGINE_ARGUMENTS, - }, -} - - -def get_verbosity_subcommand() -> dict: - """Parse command line arguments and returns a dictionary of key-value pairs. - - Returns: - A dictionary containing the parsed command line arguments. - - Examples: - >>> import sys - >>> sys.argv = ['otx', 'train', '-h', '-v'] - >>> get_verbosity_subcommand() - {'subcommand': 'train', 'help': True, 'verbosity': 1} - """ - arguments: dict = {"subcommand": None, "help": False, "verbosity": 2} - if len(sys.argv) >= 2 and sys.argv[1] not in ("--help", "-h"): - arguments["subcommand"] = sys.argv[1] - if "--help" in sys.argv or "-h" in sys.argv: - arguments["help"] = True - if arguments["subcommand"] in REQUIRED_ARGUMENTS: - arguments["verbosity"] = 0 - if "-v" in sys.argv or "--verbose" in sys.argv: - arguments["verbosity"] = 1 - if "-vv" in sys.argv: - arguments["verbosity"] = 2 - return arguments - - -INTRO_MARKDOWN = ( - "# OpenVINO™ Training Extensions CLI Guide\n\n" - "Github Repository: [https://github.com/openvinotoolkit/training_extensions](https://github.com/openvinotoolkit/training_extensions)." - "\n\n" - "A better guide is provided by the [documentation](https://openvinotoolkit.github.io/training_extensions/stable/)." -) - -VERBOSE_USAGE = ( - "To get more overridable argument information, run the command below.\n" - "```python\n" - "# Verbosity Level 1\n" - "otx {subcommand} [optional_arguments] -h -v\n" - "# Verbosity Level 2\n" - "otx {subcommand} [optional_arguments] -h -vv\n" - "```" -) - -CLI_USAGE_PATTERN = r"CLI Usage:(.*?)(?=\n{2,}|\Z)" - - -def get_cli_usage_docstring(component: object | None) -> str | None: - r"""Get the cli usage from the docstring. - - Args: - component (Optional[object]): The component to get the docstring from - - Returns: - Optional[str]: The quick-start guide as Markdown format. - - Example: - component.__doc__ = ''' - - - CLI Usage: - 1. First Step. - 2. Second Step. - - - ''' - >>> get_cli_usage_docstring(component) - "1. First Step.\n2. Second Step." - """ - if component is None or component.__doc__ is None or "CLI Usage" not in component.__doc__: - return None - match = re.search(CLI_USAGE_PATTERN, component.__doc__, re.DOTALL) - if match: - contents = match.group(1).strip().split("\n") - return "\n".join([content.strip() for content in contents]) - return None - - -def render_guide(subcommand: str | None = None) -> list: - """Render a guide for the specified subcommand. - - Args: - subcommand (Optional[str]): The subcommand to render the guide for. - - Returns: - list: A list of contents to be displayed in the guide. - """ - if subcommand is None or subcommand in ("install"): - return [] - from otx.engine import Engine - - contents: list[Panel | Markdown] = [Markdown(INTRO_MARKDOWN)] - target_command = getattr(Engine, subcommand) - cli_usage = get_cli_usage_docstring(target_command) - if cli_usage is not None: - cli_usage += f"\n{VERBOSE_USAGE.format(subcommand=subcommand)}" - quick_start = Panel(Markdown(cli_usage), border_style="dim", title="Quick-Start", title_align="left") - contents.append(quick_start) - return contents - - -class CustomHelpFormatter(RichHelpFormatter, DefaultHelpFormatter): - """A custom help formatter for OTX CLI. - - This formatter extends the RichHelpFormatter and DefaultHelpFormatter classes to provide - a more detailed and customizable help output for OTX CLI. - - Attributes: - verbosity_level : int - The level of verbosity for the help output. - subcommand : str | None - The subcommand to render the guide for. - - Methods: - add_usage(usage, actions, *args, **kwargs) - Add usage information to the help output. - add_argument(action) - Add an argument to the help output. - format_help() - Format the help output. - """ - - verbosity_dict = get_verbosity_subcommand() - verbosity_level = verbosity_dict["verbosity"] - subcommand = verbosity_dict["subcommand"] - - def add_usage(self, usage: str | None, actions: Iterable[argparse.Action], *args, **kwargs) -> None: - """Add usage information to the formatter. - - Args: - usage (str | None): A string describing the usage of the program. - actions (Iterable[argparse.Action]): An list of argparse.Action objects. - *args (Any): Additional positional arguments to pass to the superclass method. - **kwargs (Any): Additional keyword arguments to pass to the superclass method. - - Returns: - None - """ - actions = [] if self.verbosity_level == 0 else actions - if self.subcommand in REQUIRED_ARGUMENTS and self.verbosity_level == 1: - actions = [action for action in actions if action.dest in REQUIRED_ARGUMENTS[self.subcommand]] - - super().add_usage(usage, actions, *args, **kwargs) - - def add_argument(self, action: argparse.Action) -> None: - """Add an argument to the help formatter. - - If the verbose level is set to 0, the argument is not added. - If the verbose level is set to 1 and the argument is not in the non-skip list, the argument is not added. - - Args: - action (argparse.Action): The action to add to the help formatter. - """ - if self.subcommand in REQUIRED_ARGUMENTS: - if self.verbosity_level == 0: - return - if self.verbosity_level == 1 and action.dest not in REQUIRED_ARGUMENTS[self.subcommand]: - return - super().add_argument(action) - - def format_help(self) -> str: - """Format the help message for the current command and returns it as a string. - - The help message includes information about the command's arguments and options, - as well as any additional information provided by the command's help guide. - - Returns: - str: A string containing the formatted help message. - """ - with self.console.capture() as capture: - section = self._root_section - rendered_content: RenderableType = section - if self.subcommand in REQUIRED_ARGUMENTS and self.verbosity_level in (0, 1) and len(section.rich_items) > 1: - contents = render_guide(self.subcommand) - for content in contents: - self.console.print(content) - if self.verbosity_level > 0: - if len(section.rich_items) > 1: - rendered_content = Panel(section, border_style="dim", title="Arguments", title_align="left") - self.console.print(rendered_content, highlight=False, soft_wrap=True) - help_msg = capture.get() - - if help_msg: - help_msg = self._long_break_matcher.sub("\n\n", help_msg).rstrip() + "\n" - return help_msg diff --git a/src/otx/cli/utils/hpo.py b/src/otx/cli/utils/hpo.py new file mode 100644 index 00000000000..3511ebf3e15 --- /dev/null +++ b/src/otx/cli/utils/hpo.py @@ -0,0 +1,973 @@ +"""Utils for HPO with hpopt.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import json +import os +import re +import shutil +import time +from copy import deepcopy +from enum import Enum +from functools import partial +from inspect import isclass +from math import floor +from pathlib import Path +from threading import Thread +from typing import Any, Callable, Dict, List, Optional, Union + +import torch +import yaml + +from otx.algorithms.common.utils import is_xpu_available +from otx.api.configuration.helper import create +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.train_parameters import TrainParameters, UpdateProgressCallback +from otx.cli.utils.importing import get_impl_class +from otx.cli.utils.io import read_model, save_model_data +from otx.core.data.adapter import get_dataset_adapter +from otx.hpo import HyperBand, TrialStatus, run_hpo_loop +from otx.hpo.hpo_base import HpoBase +from otx.utils.logger import get_logger + +logger = get_logger() + + +def _check_hpo_enabled_task(task_type): + return task_type in [ + TaskType.CLASSIFICATION, + TaskType.DETECTION, + TaskType.SEGMENTATION, + TaskType.INSTANCE_SEGMENTATION, + TaskType.ROTATED_DETECTION, + TaskType.ANOMALY_CLASSIFICATION, + TaskType.ANOMALY_DETECTION, + TaskType.ANOMALY_SEGMENTATION, + ] + + +class TaskManager: + """Task utility class to give common interface from different task. + + Args: + task_type (TaskType): otx task type + """ + + def __init__(self, task_type: TaskType): + self._task_type = task_type + + @property + def task_type(self): + """Task_type property.""" + return self._task_type + + def is_mmcv_framework_task(self) -> bool: + """Check task is run on mmcv. + + Returns: + bool: whether task is run on mmcv + """ + return self.is_cls_framework_task() or self.is_det_framework_task() or self.is_seg_framework_task() + + def is_cls_framework_task(self) -> bool: + """Check that task is run on mmcls framework. + + Returns: + bool: whether task is run on mmcls + """ + return self._task_type == TaskType.CLASSIFICATION + + def is_det_framework_task(self) -> bool: + """Check that task is one of a task run on mmdet framework. + + Returns: + bool: whether task is run on mmdet + """ + return self._task_type in [ + TaskType.DETECTION, + TaskType.INSTANCE_SEGMENTATION, + TaskType.ROTATED_DETECTION, + ] + + def is_seg_framework_task(self) -> bool: + """Check that task is run on mmseg framework. + + Returns: + bool: whether tasks is run on mmseg + """ + return self._task_type == TaskType.SEGMENTATION + + def is_anomaly_framework_task(self) -> bool: + """Check taht task is run on anomalib. + + Returns: + bool: whether task is run on anomalib + """ + return self._task_type in [ + TaskType.ANOMALY_CLASSIFICATION, + TaskType.ANOMALY_DETECTION, + TaskType.ANOMALY_SEGMENTATION, + ] + + def get_batch_size_name(self) -> str: + """Give an proper batch size name depending on framework. + + Returns: + str: batch size name + """ + if self.is_mmcv_framework_task(): + batch_size_name = "learning_parameters.batch_size" + elif self.is_anomaly_framework_task(): + batch_size_name = "learning_parameters.train_batch_size" + else: + raise RuntimeError(f"There is no information about {self._task_type} batch size name") + + return batch_size_name + + def get_epoch_name(self) -> str: + """Give an proper epoch name depending on framework. + + Returns: + str: epoch name + """ + if self.is_mmcv_framework_task(): + epoch_name = "num_iters" + elif self.is_anomaly_framework_task(): + epoch_name = "max_epochs" + else: + raise RuntimeError(f"There is no information about {self._task_type} epoch name") + + return epoch_name + + def copy_weight(self, src: Union[str, Path], det: Union[str, Path]): + """Copy all model weights from work directory. + + Args: + src (Union[str, Path]): path where model weights are saved + det (Union[str, Path]): path to save model weights + """ + src = Path(src) + det = Path(det) + if self.is_mmcv_framework_task(): + for weight_candidate in src.rglob("*epoch*.pth"): + if not (weight_candidate.is_symlink() or (det / weight_candidate.name).exists()): + shutil.copy(weight_candidate, det) + # TODO need to implement after anomaly task supports resume + + def get_latest_weight(self, workdir: Union[str, Path]) -> Optional[str]: + """Get latest model weight from all weights. + + Args: + workdir (Union[str, Path]): path where model weights are saved + + Returns: + Optional[str]: latest model weight path. If not found, than return None value. + """ + latest_weight = None + workdir = Path(workdir) + if self.is_mmcv_framework_task(): + pattern = re.compile(r"(\d+)\.pth") + current_latest_epoch = -1 + latest_weight = None + + for weight_name in workdir.rglob("epoch_*.pth"): + ret = pattern.search(str(weight_name)) + if ret is not None: + epoch = int(ret.group(1)) + if current_latest_epoch < epoch: + current_latest_epoch = epoch + latest_weight = str(weight_name) + # TODO need to implement after anomaly task supports resume + + return latest_weight + + +class TaskEnvironmentManager: + """OTX environment utility class to set or get a value from environment class. + + Args: + environment (TaskEnvironment): OTX task environment + """ + + def __init__(self, environment: TaskEnvironment): + self._environment = environment + self.task = TaskManager(environment.model_template.task_type) + + @property + def environment(self): + """Environment property.""" + return self._environment + + def get_task(self) -> TaskType: + """Get task type of environment. + + Returns: + TaskType: task type + """ + return self._environment.model_template.task_type + + def get_model_template(self): + """Get model template.""" + return self._environment.model_template + + def get_model_template_path(self) -> str: + """Get model template path. + + Returns: + str: path of model template + """ + return self._environment.model_template.model_template_path + + def set_hyper_parameter_using_str_key(self, hyper_parameter: Dict[str, Any]): + """Set hyper parameter to environment using string key hyper_parameter. + + Set hyper parameter to environment. Argument `hyper_parameter` is a dictionary which has string key. + For example, hyper_parameter has a key "a.b.c", then value is set at env_hp.a.b.c. + + Args: + hyper_parameter (Dict[str, Any]): hyper parameter to set which has a string format + """ + env_hp = self._environment.get_hyper_parameters() # type: ignore + + for param_key, param_val in hyper_parameter.items(): + splited_param_key = param_key.split(".") + + target = env_hp + for val in splited_param_key[:-1]: + target = getattr(target, val) + setattr(target, splited_param_key[-1], param_val) + + def get_dict_type_hyper_parameter(self) -> Dict[str, Any]: + """Get dictionary type hyper parmaeter of environment. + + Returns: + Dict[str, Any]: dictionary type hyper parameter of environment + """ + learning_parameters = self._environment.get_hyper_parameters().learning_parameters # type: ignore + learning_parameters = self._convert_parameter_group_to_dict(learning_parameters) + hyper_parameter = {f"learning_parameters.{key}": val for key, val in learning_parameters.items()} + return hyper_parameter + + def _convert_parameter_group_to_dict(self, parameter_group) -> Dict[str, Any]: + """Convert parameter group to dictionary. + + Args: + parameter_group : parameter gruop + + Returns: + Dict[str, Any]: parameter group converted to dictionary + """ + groups = getattr(parameter_group, "groups", None) + parameters = getattr(parameter_group, "parameters", None) + + total_arr = [] + for val in [groups, parameters]: + if val is not None: + total_arr.extend(val) + if not total_arr: + return parameter_group + + ret = {} + for key in total_arr: + val = self._convert_parameter_group_to_dict(getattr(parameter_group, key)) + if not (isclass(val) or isinstance(val, Enum)): + ret[key] = val + + return ret + + def get_max_epoch(self) -> int: + """Get max epoch from environment. + + Returns: + int: max epoch of environment + """ + return getattr( + self._environment.get_hyper_parameters().learning_parameters, self.task.get_epoch_name() # type: ignore + ) + + def save_initial_weight(self, save_path: Union[Path, str]) -> bool: + """Save an initial model weight. + + Args: + save_path (Union[str, Path]): path to save initial model weight + + Returns: + bool: whether model weight is saved successfully + """ + save_path = Path(save_path) + dir_path = save_path.parent + if self._environment.model is None: + # if task isn't anomaly, then save model weight during first trial + if self.task.is_anomaly_framework_task(): + task = self.get_train_task() + model = self.get_new_model_entity() + task.save_model(model) + save_model_data(model, str(dir_path)) + (dir_path / "weights.pth").rename(save_path) + return True + else: + save_model_data(self._environment.model, str(dir_path)) + (dir_path / "weights.pth").rename(save_path) + return True + return False + + def get_train_task(self): + """Get OTX train task instance. + + Returns: + OTX task: OTX train task instance + """ + impl_class = get_impl_class(self._environment.model_template.entrypoints.base) + return impl_class(task_environment=self._environment) + + def get_batch_size_name(self) -> str: + """Get proper batch size name depending on task. + + Returns: + str: batch size name + """ + return self.task.get_batch_size_name() + + def load_model_weight(self, model_weight_path: str, dataset: DatasetEntity): + """Set model weight on environment to load the weight during training. + + Args: + model_weight_path (str): model weight to load during training + dataset (DatasetEntity): dataset for training a model + """ + self._environment.model = read_model(self._environment.get_model_configuration(), model_weight_path, dataset) + + def resume_model_weight(self, model_weight_path: str, dataset: DatasetEntity): + """Set model weight on environment to resume the weight during training. + + Args: + model_weight_path (str): model weight to resume during training + dataset (DatasetEntity): dataset for training a model + """ + self.load_model_weight(model_weight_path, dataset) + self._environment.model.model_adapters["resume"] = True # type: ignore + + def get_new_model_entity(self, dataset=None) -> ModelEntity: + """Get new model entity using environment. + + Args: + dataset (Optional[DatasetEntity]): OTX dataset + + Returns: + ModelEntity: new model entity + """ + return ModelEntity( + dataset, + self._environment.get_model_configuration(), + ) + + def set_epoch(self, epoch: int): + """Set epoch on environment. + + Args: + epoch (int): epoch to set + """ + hyper_parameter = {f"learning_parameters.{self.task.get_epoch_name()}": epoch} + self.set_hyper_parameter_using_str_key(hyper_parameter) + + +class HpoRunner: + """Class which is in charge of preparing and running HPO. + + Args: + environment (TaskEnvironment): OTX environment + train_dataset_size (int): train dataset size + val_dataset_size (int): validation dataset size + hpo_workdir (Union[str, Path]): work directory for HPO + hpo_time_ratio (int, optional): time ratio to use for HPO compared to training time. Defaults to 4. + progress_updater_callback (Optional[Callable[[Union[int, float]], None]]): callback to update progress + """ + + # pylint: disable=too-many-instance-attributes + + def __init__( + self, + environment: TaskEnvironment, + train_dataset_size: int, + val_dataset_size: int, + hpo_workdir: Union[str, Path], + hpo_time_ratio: int = 4, + progress_updater_callback: Optional[Callable[[Union[int, float]], None]] = None, + ): + if train_dataset_size <= 0: + raise ValueError(f"train_dataset_size should be bigger than 0. Your value is {train_dataset_size}") + if val_dataset_size <= 0: + raise ValueError(f"val_dataset_size should be bigger than 0. Your value is {val_dataset_size}") + if hpo_time_ratio < 1: + raise ValueError(f"hpo_time_ratio shouldn't be smaller than 1. Your value is {hpo_time_ratio}") + + self._environment = TaskEnvironmentManager(environment) + self._hpo_workdir: Path = Path(hpo_workdir) + self._hpo_time_ratio = hpo_time_ratio + self._hpo_config: Dict = self._set_hpo_config() + self._train_dataset_size = train_dataset_size + self._val_dataset_size = val_dataset_size + self._fixed_hp: Dict[str, Any] = {} + self._initial_weight_name = "initial_weight.pth" + self._progress_updater_callback = progress_updater_callback + + self._align_batch_size_search_space_to_dataset_size() + + def _set_hpo_config(self): + hpo_config_path = Path(self._environment.get_model_template_path()).parent / "hpo_config.yaml" + with hpo_config_path.open("r") as f: + hpopt_cfg = yaml.safe_load(f) + + return hpopt_cfg + + def _align_batch_size_search_space_to_dataset_size(self): + batch_size_name = self._environment.get_batch_size_name() + + if batch_size_name in self._hpo_config["hp_space"]: + if "range" in self._hpo_config["hp_space"][batch_size_name]: + max_val = self._hpo_config["hp_space"][batch_size_name]["range"][1] + min_val = self._hpo_config["hp_space"][batch_size_name]["range"][0] + step = 1 + if self._hpo_config["hp_space"][batch_size_name]["param_type"] in ["quniform", "qloguniform"]: + step = self._hpo_config["hp_space"][batch_size_name]["range"][2] + if max_val > self._train_dataset_size: + max_val = self._train_dataset_size + self._hpo_config["hp_space"][batch_size_name]["range"][1] = max_val + else: + max_val = self._hpo_config["hp_space"][batch_size_name]["max"] + min_val = self._hpo_config["hp_space"][batch_size_name]["min"] + step = self._hpo_config["hp_space"][batch_size_name].get("step", 1) + + if max_val > self._train_dataset_size: + max_val = self._train_dataset_size + self._hpo_config["hp_space"][batch_size_name]["max"] = max_val + + # If trainset size is lower than min batch size range, + # fix batch size to trainset size + reason_to_fix_bs = "" + if min_val >= max_val: + reason_to_fix_bs = "Train set size is equal or lower than batch size range." + elif max_val - min_val < step: + reason_to_fix_bs = "Difference between min and train set size is lesser than step." + if reason_to_fix_bs: + logger.info(f"{reason_to_fix_bs} Batch size is fixed to train set size.") + del self._hpo_config["hp_space"][batch_size_name] + self._fixed_hp[batch_size_name] = self._train_dataset_size + self._environment.set_hyper_parameter_using_str_key(self._fixed_hp) + + def run_hpo(self, train_func: Callable, data_roots: Dict[str, Dict]) -> Union[Dict[str, Any], None]: + """Run HPO and provides optimized hyper parameters. + + Args: + train_func (Callable): training model function + data_roots (Dict[str, Dict]): dataset path of each dataset type + + Returns: + Union[Dict[str, Any], None]: Optimized hyper parameters. If there is no best hyper parameter, return None. + """ + self._environment.save_initial_weight(self._get_initial_model_weight_path()) + hpo_algo = self._get_hpo_algo() + + if self._progress_updater_callback is not None: + progress_updater_thread = Thread(target=self._update_hpo_progress, args=[hpo_algo], daemon=True) + progress_updater_thread.start() + + if torch.cuda.is_available(): + resource_type = "gpu" + elif is_xpu_available(): + resource_type = "xpu" + else: + resource_type = "cpu" + + run_hpo_loop( + hpo_algo, + partial( + train_func, + model_template=self._environment.get_model_template(), + data_roots=data_roots, + task_type=self._environment.get_task(), + hpo_workdir=self._hpo_workdir, + initial_weight_name=self._initial_weight_name, + metric=self._hpo_config["metric"], + ), + resource_type, # type: ignore + ) + best_config = hpo_algo.get_best_config() + if best_config is not None: + self._restore_fixed_hp(best_config["config"]) + hpo_algo.print_result() + + return best_config + + def _restore_fixed_hp(self, hyper_parameter: Dict[str, Any]): + for key, val in self._fixed_hp.items(): + hyper_parameter[key] = val + + def _get_hpo_algo(self): + hpo_algo_type = self._hpo_config.get("search_algorithm", "asha") + + if hpo_algo_type == "asha": + hpo_algo = self._prepare_asha() + elif hpo_algo_type == "smbo": + hpo_algo = self._prepare_smbo() + else: + raise ValueError(f"Supported HPO algorithms are asha and smbo. your value is {hpo_algo_type}.") + + return hpo_algo + + def _prepare_asha(self): + if is_xpu_available(): + asynchronous_sha = torch.xpu.device_count() != 1 + else: + asynchronous_sha = torch.cuda.device_count() != 1 + + args = { + "search_space": self._hpo_config["hp_space"], + "save_path": str(self._hpo_workdir), + "maximum_resource": self._hpo_config.get("maximum_resource"), + "minimum_resource": self._hpo_config.get("minimum_resource"), + "mode": self._hpo_config.get("mode", "max"), + "num_workers": 1, + "num_full_iterations": self._environment.get_max_epoch(), + "full_dataset_size": self._train_dataset_size, + "non_pure_train_ratio": self._val_dataset_size / (self._train_dataset_size + self._val_dataset_size), + "metric": self._hpo_config.get("metric", "mAP"), + "expected_time_ratio": self._hpo_time_ratio, + "prior_hyper_parameters": self._get_default_hyper_parameters(), + "asynchronous_bracket": True, + "asynchronous_sha": asynchronous_sha, + } + + logger.debug(f"ASHA args = {args}") + + return HyperBand(**args) + + def _prepare_smbo(self): + raise NotImplementedError + + def _get_default_hyper_parameters(self): + default_hyper_parameters = {} + hp_from_env = self._environment.get_dict_type_hyper_parameter() + + for key, val in hp_from_env.items(): + if key in self._hpo_config["hp_space"]: + default_hyper_parameters[key] = val + + if not default_hyper_parameters: + return None + return default_hyper_parameters + + def _get_initial_model_weight_path(self): + return self._hpo_workdir / self._initial_weight_name + + def _update_hpo_progress(self, hpo_algo: HpoBase): + """Function for a thread to report a HPO progress regularly. + + Args: + hpo_algo (HpoBase): HPO algorithm class + """ + + while True: + if hpo_algo.is_done(): + break + self._progress_updater_callback(hpo_algo.get_progress() * 100) + time.sleep(1) + + +def run_hpo( + hpo_time_ratio: int, + output: Path, + environment: TaskEnvironment, + dataset: DatasetEntity, + data_roots: Dict[str, Dict], + progress_updater_callback: Optional[Callable[[Union[int, float]], None]] = None, +) -> Optional[TaskEnvironment]: + """Run HPO and load optimized hyper parameter and best HPO model weight. + + Args: + hpo_time_ratio(int): expected ratio of total time to run HPO to time taken for full fine-tuning + output(Path): directory where HPO output is saved + environment (TaskEnvironment): otx task environment + dataset (DatasetEntity): dataset to use for training + data_roots (Dict[str, Dict]): dataset path of each dataset type + progress_updater_callback (Optional[Callable[[Union[int, float]], None]]): callback to update progress + """ + task_type = environment.model_template.task_type + if not _check_hpo_enabled_task(task_type): + logger.warning( + "Currently supported task types are classification, detection, segmentation and anomaly" + f"{task_type} is not supported yet." + ) + return environment + + if "TORCHELASTIC_RUN_ID" in os.environ: + logger.warning("OTX is trained by torchrun. HPO isn't available.") + return environment + + hpo_save_path = (output / "hpo").absolute() + hpo_runner = HpoRunner( + environment, + len(dataset.get_subset(Subset.TRAINING)), + len(dataset.get_subset(Subset.VALIDATION)), + hpo_save_path, + hpo_time_ratio, + progress_updater_callback, + ) + + logger.info("started hyper-parameter optimization") + best_config = hpo_runner.run_hpo(run_trial, data_roots) + logger.info("completed hyper-parameter optimization") + + env_manager = TaskEnvironmentManager(environment) + best_hpo_weight = None + + if best_config is not None: + env_manager.set_hyper_parameter_using_str_key(best_config["config"]) + best_hpo_weight = get_best_hpo_weight(hpo_save_path, best_config["id"]) + if best_hpo_weight is None: + logger.warning("Can not find the best HPO weight. Best HPO wegiht won't be used.") + else: + logger.debug(f"{best_hpo_weight} will be loaded as best HPO weight") + env_manager.load_model_weight(best_hpo_weight, dataset) + + _remove_unused_model_weights(hpo_save_path, best_hpo_weight) + return env_manager.environment + + +def _remove_unused_model_weights(hpo_save_path: Path, best_hpo_weight: Optional[str] = None): + for weight in hpo_save_path.rglob("*.pth"): + if best_hpo_weight is not None and str(weight) == best_hpo_weight: + continue + weight.unlink() + + +def get_best_hpo_weight(hpo_dir: Union[str, Path], trial_id: Union[str, Path]) -> Optional[str]: + """Get best model weight path of the HPO trial. + + Args: + hpo_dir (Union[str, Path]): HPO work directory path + trial_id (Union[str, Path]): trial id + + Returns: + Optional[str]: best HPO model weight + """ + hpo_dir = Path(hpo_dir) + trial_output_files = list(hpo_dir.rglob(f"{trial_id}.json")) + if not trial_output_files: + return None + trial_output_file = trial_output_files[0] + + with trial_output_file.open("r") as f: + trial_output = json.load(f) + + best_epochs = [] + best_score = None + for eph, score in trial_output["score"].items(): + if best_score is None: + best_score = score + best_epochs.append(eph) + elif best_score < score: + best_score = score + best_epochs = [eph] + elif best_score == score: + best_epochs.append(eph) + + best_weight = None + for best_epoch in best_epochs: + best_weight_path = list(hpo_dir.glob(f"weight/{trial_id}/*epoch*{best_epoch}*")) + if best_weight_path: + best_weight = str(best_weight_path[0]) + + return best_weight + + +class Trainer: + """Class which prepares and trains a model given hyper parameters. + + Args: + hp_config (Dict[str, Any]): hyper parameter to use on training + report_func (Callable): function to report score + model_template: model template + data_roots (Dict[str, Dict]): dataset path of each dataset type + task_type (TaskType): OTX task type + hpo_workdir (Union[str, Path]): work directory for HPO + initial_weight_name (str): initial model weight name for each trials to load + metric (str): metric name + """ + + # pylint: disable=too-many-arguments, too-many-instance-attributes + + def __init__( + self, + hp_config: Dict[str, Any], + report_func: Callable, + model_template, + data_roots: Dict[str, Dict], + task_type: TaskType, + hpo_workdir: Union[str, Path], + initial_weight_name: str, + metric: str, + ): + self._hp_config = hp_config + self._report_func = report_func + self._model_template = model_template + self._data_roots = data_roots + self._task = TaskManager(task_type) + self._hpo_workdir: Path = Path(hpo_workdir) + self._initial_weight_name = initial_weight_name + self._metric = metric + self._epoch = floor(self._hp_config["configuration"]["iterations"]) + del self._hp_config["configuration"]["iterations"] + + def run(self): + """Run each training of each trial with given hyper parameters.""" + hyper_parameters = self._prepare_hyper_parameter() + dataset_adapter = self._prepare_dataset_adapter() + + dataset = dataset_adapter.get_otx_dataset() + dataset = HpoDataset(dataset, self._hp_config) + + label_schema = dataset_adapter.get_label_schema() + + environment = self._prepare_environment(hyper_parameters, label_schema) + self._set_hyper_parameter(environment) + + need_to_save_initial_weight = False + resume_weight_path = self._get_resume_weight_path() + if resume_weight_path is not None: + ret = re.search(r"(\d+)\.pth", resume_weight_path) + if ret is not None: + resume_epoch = int(ret.group(1)) + if self._epoch <= resume_epoch: # given epoch is already done + self._report_func(0, 0, done=True) + return + environment.resume_model_weight(resume_weight_path, dataset) + else: + initial_weight = self._load_fixed_initial_weight() + if initial_weight is not None: + environment.load_model_weight(str(initial_weight), dataset) + else: + need_to_save_initial_weight = True + + task = environment.get_train_task() + if need_to_save_initial_weight: + self._add_initial_weight_saving_hook(task) + + output_model = environment.get_new_model_entity(dataset) + score_report_callback = self._prepare_score_report_callback(task) + task.train(dataset=dataset, output_model=output_model, train_parameters=score_report_callback) + self._finalize_trial(task) + self._delete_unused_model_weight() + + def _prepare_hyper_parameter(self): + return create(self._model_template.hyper_parameters.data) + + def _prepare_dataset_adapter(self): + dataset_adapter = get_dataset_adapter( + self._task.task_type, + self._model_template.hyper_parameters.parameter_overrides["algo_backend"]["train_type"]["default_value"], + train_data_roots=self._data_roots["train_subset"]["data_roots"], + val_data_roots=self._data_roots["val_subset"]["data_roots"] if "val_subset" in self._data_roots else None, + unlabeled_data_roots=self._data_roots["unlabeled_subset"]["data_roots"] + if "unlabeled_subset" in self._data_roots + else None, + ) + + return dataset_adapter + + def _set_hyper_parameter(self, environment: TaskEnvironmentManager): + environment.set_hyper_parameter_using_str_key(self._hp_config["configuration"]) + if self._task.is_mmcv_framework_task(): + environment.set_hyper_parameter_using_str_key({"learning_parameters.auto_decrease_batch_size": "None"}) + environment.set_hyper_parameter_using_str_key({"learning_parameters.auto_adapt_batch_size": "None"}) + environment.set_epoch(self._epoch) + + def _prepare_environment(self, hyper_parameters, label_schema): + enviroment = TaskEnvironment( + model=None, + hyper_parameters=hyper_parameters, + label_schema=label_schema, + model_template=self._model_template, + ) + + return TaskEnvironmentManager(enviroment) + + def _get_resume_weight_path(self): + trial_work_dir = self._get_weight_dir_path() + if not trial_work_dir.exists(): + return None + return self._task.get_latest_weight(trial_work_dir) + + def _load_fixed_initial_weight(self): + initial_weight_path = self._get_initial_weight_path() + if initial_weight_path.exists(): + return initial_weight_path + return None + + def _add_initial_weight_saving_hook(self, task): + initial_weight_path = self._get_initial_weight_path() + task.update_override_configurations( + { + "custom_hooks": [ + dict( + type="SaveInitialWeightHook", + save_path=initial_weight_path.parent, + file_name=initial_weight_path.name, + ) + ] + } + ) + + def _prepare_score_report_callback(self, task) -> TrainParameters: + return TrainParameters(False, HpoCallback(self._report_func, self._metric, self._epoch, task)) + + def _get_initial_weight_path(self) -> Path: + return self._hpo_workdir / self._initial_weight_name + + def _finalize_trial(self, task): + weight_dir_path = self._get_weight_dir_path() + weight_dir_path.mkdir(parents=True, exist_ok=True) + self._task.copy_weight(task.project_path, weight_dir_path) + self._report_func(0, 0, done=True) + + def _get_weight_dir_path(self) -> Path: + return self._hpo_workdir / "weight" / self._hp_config["id"] + + def _delete_unused_model_weight(self): + """Delete model weights except best and latest model weight.""" + for json_file in self._hpo_workdir.rglob("*.json"): + if not json_file.stem.isnumeric(): + continue + trial_num = json_file.stem + weight_dir = self._hpo_workdir / "weight" / trial_num + if not weight_dir.exists(): + continue + latest_model_weight = self._task.get_latest_weight(weight_dir) + best_model_weight = get_best_hpo_weight(self._hpo_workdir, trial_num) + for each_model_weight in weight_dir.iterdir(): + if str(each_model_weight) not in [latest_model_weight, best_model_weight]: + each_model_weight.unlink() + + +def run_trial( + hp_config: Dict[str, Any], + report_func: Callable, + model_template, + data_roots: Dict[str, Dict], + task_type: TaskType, + hpo_workdir: Union[str, Path], + initial_weight_name: str, + metric: str, +): + """Function to train a model given hyper parameters. + + Args: + hp_config (Dict[str, Any]): hyper parameter to use on training + report_func (Callable): function to report score + model_template: model template + data_roots (Dict[str, Dict]): dataset path of each dataset type + task_type (TaskType): OTX task type + hpo_workdir (Union[str, Path]): work directory for HPO + initial_weight_name (str): initial model weight name for each trials to load + metric (str): metric name + """ + # pylint: disable=too-many-arguments + trainer = Trainer( + hp_config, report_func, model_template, data_roots, task_type, hpo_workdir, initial_weight_name, metric + ) + trainer.run() + + +class HpoCallback(UpdateProgressCallback): + """Callback class to report score to HPO. + + Args: + report_func (Callable): function to report score + metric (str): metric name + max_epoch (int): max_epoch + task: OTX train task + """ + + def __init__(self, report_func: Callable, metric: str, max_epoch: int, task): + if max_epoch <= 0: + raise ValueError(f"max_epoch should be bigger than 0. Current value is {max_epoch}.") + + super().__init__() + self._report_func = report_func + self.metric = metric + self._max_epoch = max_epoch + self._task = task + + def __call__(self, progress: Union[int, float], score: Optional[float] = None): + """When callback is called, report a score to HPO algorithm.""" + if score is not None: + epoch = round(self._max_epoch * progress / 100) + logger.debug(f"In hpo callback : {score} / {progress} / {epoch}") + if self._report_func(score=score, progress=epoch) == TrialStatus.STOP: + self._task.cancel_training() + + def __deepcopy__(self, memo): + """Prevent repot_func from deepcopied.""" + args = [self.metric, self._max_epoch, self._task] + copied_args = deepcopy(args, memo) + return self.__class__(self._report_func, *copied_args) + + +class HpoDataset: + """Wrapper class for DatasetEntity of dataset. It's used to make subset during HPO. + + Args: + fullset: full dataset + config (Optional[Dict[str, Any]], optional): hyper parameter trial config + indices (Optional[List[int]]): dataset index. Defaults to None. + """ + + def __init__(self, fullset, config: Optional[Dict[str, Any]] = None, indices: Optional[List[int]] = None): + self.fullset = fullset + self.indices = indices + if config is not None: + subset_ratio = config["train_environment"]["subset_ratio"] + self.subset_ratio = 1 if subset_ratio is None else subset_ratio + + def __len__(self) -> int: + """Get length of subset.""" + if self.indices is None: + return len(self.fullset) + return len(self.indices) + + def __getitem__(self, indx) -> dict: + """Get dataset at index.""" + if self.indices is None: + return self.fullset[indx] + return self.fullset[self.indices[indx]] + + def __getattr__(self, name): + """When trying to get other attributes, not dataset, get values from fullset.""" + if name == "__setstate__": + raise AttributeError(name) + return getattr(self.fullset, name) + + def get_subset(self, subset: Subset): + """Get subset according to subset_ratio if training dataset is requested. + + Args: + subset (Subset): which subset to get + + Returns: + HpoDataset: subset wrapped by HpoDataset + """ + dataset = self.fullset.get_subset(subset) + if subset != Subset.TRAINING or self.subset_ratio > 0.99: + return dataset + + indices = torch.randperm(len(dataset), generator=torch.Generator().manual_seed(42)) + indices = indices.tolist() # type: ignore + indices = indices[: int(len(dataset) * self.subset_ratio)] + + return HpoDataset(dataset, config=None, indices=indices) diff --git a/src/otx/cli/utils/importing.py b/src/otx/cli/utils/importing.py new file mode 100644 index 00000000000..5fbf45eaddf --- /dev/null +++ b/src/otx/cli/utils/importing.py @@ -0,0 +1,116 @@ +"""Utils for dynamically importing stuff.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +import importlib +import inspect +import json +import os + +# TODO: To avoid error during importing yapf dynamically. After the bug is fixed, code should be removed. +try: + import yapf # noqa: F401 +except ImportError: + pass + +# pylint: disable=protected-access + +SUPPORTED_BACKBONE_BACKENDS = { + "otx": "otx.algorithms.common.adapters.mmcv.models", + "mmcls": "mmcls.models", + "mmdet": "mmdet.models", + "mmseg": "mmseg.models", + "torchvision": "otx.algorithms.common.adapters.mmcv.models", + "pytorchcv": "mmdet.models", + "omz.mmcls": "otx.algorithms.classification.adapters.mmcls.models.backbones.mmov_backbone", +} + + +def get_impl_class(impl_path): + """Returns a class by its path in package.""" + + task_impl_module_name, task_impl_class_name = impl_path.rsplit(".", 1) + task_impl_module = importlib.import_module(task_impl_module_name) + task_impl_class = getattr(task_impl_module, task_impl_class_name) + + return task_impl_class + + +def get_backbone_list(backend): + """Gather available backbone list from json file & imported lib.""" + available_backbone_path = os.path.join(get_otx_root_path(), f"cli/builder/supported_backbone/{backend}.json") + available_backbones = {} + if os.path.exists(available_backbone_path): + with open(available_backbone_path, "r", encoding="UTF-8") as f: + available_backbones = json.load(f) + available_backbones = available_backbones["backbones"] + elif backend == "pytorchcv" and importlib.util.find_spec(backend): + backbone_list = importlib.import_module(f"{backend}.model_provider")._models + backbone_format = {"required": [], "options": {}, "available": []} + for backbone in backbone_list: + backbone_type = f"mmdet.{backbone}" + available_backbones[backbone_type] = backbone_format + else: + raise ValueError(f"{backend} cannot be imported or supported.") + return available_backbones + + +def get_backbone_registry(backend=None): + """Gather backbone list from backends.""" + if backend not in SUPPORTED_BACKBONE_BACKENDS: + raise ValueError(f"{backend} is an unsupported backbone backend.") + + custom_imports = [] + backend_import_path = SUPPORTED_BACKBONE_BACKENDS[backend] + mm_backbones = importlib.import_module(backend_import_path) + mm_registry = mm_backbones.BACKBONES + custom_imports.append(backend_import_path) + return mm_registry, custom_imports + + +def get_module_args(module): + """Gather module's Required Args.""" + if module is None: + return [] + required_args = [] + default_args = {} + args_signature = inspect.signature(module) + for arg_key, arg_value in args_signature.parameters.items(): + if arg_value.default is inspect.Parameter.empty: + required_args.append(arg_key) + continue + default_args[arg_key] = arg_value.default + # Get args from parents + parent_module = module.__bases__ + while len(parent_module): + parent_args_signature = inspect.signature(parent_module[0]) + for arg_key, arg_value in parent_args_signature.parameters.items(): + if arg_key == "depth" and "arch" in required_args: + continue + if arg_value.default is inspect.Parameter.empty and arg_key not in required_args: + required_args.append(arg_key) + continue + parent_module = parent_module[0].__bases__ + required_args = [arg for arg in required_args if arg not in ("args", "kwargs", "self")] + return required_args, default_args + + +def get_otx_root_path(): + """Get otx root path from importing otx.""" + otx_module = importlib.import_module("otx") + if otx_module: + return os.path.dirname(inspect.getfile(otx_module)) + return None diff --git a/src/otx/cli/utils/installation.py b/src/otx/cli/utils/installation.py deleted file mode 100644 index 1e758ad9be0..00000000000 --- a/src/otx/cli/utils/installation.py +++ /dev/null @@ -1,551 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""OTX installation util functions.""" - -from __future__ import annotations - -import json -import os -import platform -import re -import subprocess -from importlib.metadata import requires -from importlib.util import find_spec -from pathlib import Path -from warnings import warn - -import pkg_resources -from importlib_resources import files -from pkg_resources import Requirement - -AVAILABLE_TORCH_VERSIONS = { - "1.13.0": {"torchvision": "0.14.0", "cuda": ("11.6", "11.7")}, - "1.13.1": {"torchvision": "0.14.1", "cuda": ("11.6", "11.7")}, - "2.0.0": {"torchvision": "0.15.1", "cuda": ("11.7", "11.8")}, - "2.0.1": {"torchvision": "0.15.2", "cuda": ("11.7", "11.8")}, - "2.1.1": {"torchvision": "0.16.1", "cuda": ("11.8", "12.1")}, -} - -MM_REQUIREMENTS = [ - "mmcv", - "mmcv-full", - "mmengine", - "mmdet", - "mmsegmentation", - "mmpretrain", - "mmdeploy", -] - - -def get_requirements(module: str = "otx") -> dict[str, list[Requirement]]: - """Get requirements of module from importlib.metadata. - - This function returns list of required packages from importlib_metadata. - - Example: - >>> get_requirements("otx") - { - "api": ["attrs>=21.2.0", ...], - "anomaly": ["anomalib==0.5.1", ...], - ... - } - - Returns: - dict[str, list[Requirement]]: List of required packages for each optional-extras. - """ - requirement_list: list[str] | None = requires(module) - extra_requirement: dict[str, list[Requirement]] = {} - if requirement_list is None: - return extra_requirement - for requirement in requirement_list: - extra = "api" - requirement_extra: list[str] = requirement.replace(" ", "").split(";") - if isinstance(requirement_extra, list) and len(requirement_extra) > 1: - extra = requirement_extra[-1].split("==")[-1].strip("'\"") - _requirement_name = requirement_extra[0] - _requirement = Requirement.parse(_requirement_name) - if extra in extra_requirement: - extra_requirement[extra].append(_requirement) - else: - extra_requirement[extra] = [_requirement] - return extra_requirement - - -def parse_requirements( - requirements: list[Requirement], -) -> tuple[str, list[str], list[str]]: - """Parse requirements and returns torch, mmcv and task requirements. - - Args: - requirements (list[Requirement]): List of requirements. - - Raises: - ValueError: If torch requirement is not found. - - Examples: - >>> requirements = [ - ... Requirement.parse("torch==1.13.0"), - ... Requirement.parse("mmcv-full==1.7.0"), - ... Requirement.parse("mmcls==0.12.0"), - ... Requirement.parse("onnx>=1.8.1"), - ... ] - >>> parse_requirements(requirements=requirements) - (Requirement.parse("torch==1.13.0"), - [Requirement.parse("mmcv-full==1.7.0"), Requirement.parse("mmcls==0.12.0")], - Requirement.parse("onnx>=1.8.1")) - - Returns: - tuple[str, list[str], list[str]]: Tuple of torch, mmcv and other requirements. - """ - torch_requirement: str | None = None - mm_requirements: list[str] = [] - other_requirements: list[str] = [] - - for requirement in requirements: - if requirement.unsafe_name == "torch": - torch_requirement = str(requirement) - if len(requirement.specs) > 1: - warn( - "requirements.txt contains. Please remove other versions of torch from requirements.", - stacklevel=2, - ) - - elif requirement.unsafe_name in MM_REQUIREMENTS: - mm_requirements.append(str(requirement)) - - # Rest of the requirements are task requirements. - # Other torch-related requirements such as `torchvision` are to be excluded. - # This is because torch-related requirements are already handled in torch_requirement. - else: - # if not requirement.unsafe_name.startswith("torch"): - other_requirements.append(str(requirement)) - - if not torch_requirement: - msg = "Could not find torch requirement. OTX depends on torch. Please add torch to your requirements." - raise ValueError( - msg, - ) - - # Get the unique list of the requirements. - mm_requirements = list(set(mm_requirements)) - other_requirements = list(set(other_requirements)) - - return torch_requirement, mm_requirements, other_requirements - - -def get_cuda_version() -> str | None: - """Get CUDA version installed on the system. - - Examples: - >>> # Assume that CUDA version is 11.2 - >>> get_cuda_version() - "11.2" - - >>> # Assume that CUDA is not installed on the system - >>> get_cuda_version() - None - - Returns: - str | None: CUDA version installed on the system. - """ - # 1. Check CUDA_HOME Environment variable - cuda_home = os.environ.get("CUDA_HOME", "/usr/local/cuda") - - if Path(cuda_home).exists(): - # Check $CUDA_HOME/version.json file. - version_file = Path(cuda_home) / "version.json" - if version_file.is_file(): - with Path(version_file).open() as file: - data = json.load(file) - cuda_version = data.get("cuda", {}).get("version", None) - if cuda_version is not None: - cuda_version_parts = cuda_version.split(".") - return ".".join(cuda_version_parts[:2]) - # 2. 'nvcc --version' check & without version.json case - try: - result = subprocess.run(args=["nvcc", "--version"], capture_output=True, text=True, check=False) - output = result.stdout - - cuda_version_pattern = r"cuda_(\d+\.\d+)" - cuda_version_match = re.search(cuda_version_pattern, output) - - if cuda_version_match is not None: - return cuda_version_match.group(1) - except Exception: - msg = "Could not find cuda-version. Instead, the CPU version of torch will be installed." - warn(msg, stacklevel=2) - return None - - -def update_cuda_version_with_available_torch_cuda_build(cuda_version: str, torch_version: str) -> str: - """Update the installed CUDA version with the highest supported CUDA version by PyTorch. - - Args: - cuda_version (str): The installed CUDA version. - torch_version (str): The PyTorch version. - - Raises: - Warning: If the installed CUDA version is not supported by PyTorch. - - Examples: - >>> update_cuda_version_with_available_torch_cuda_builds("11.1", "1.13.0") - "11.6" - - >>> update_cuda_version_with_available_torch_cuda_builds("11.7", "1.13.0") - "11.7" - - >>> update_cuda_version_with_available_torch_cuda_builds("11.8", "1.13.0") - "11.7" - - >>> update_cuda_version_with_available_torch_cuda_builds("12.1", "2.0.1") - "11.8" - - Returns: - str: The updated CUDA version. - """ - max_supported_cuda = max(AVAILABLE_TORCH_VERSIONS[torch_version]["cuda"]) - min_supported_cuda = min(AVAILABLE_TORCH_VERSIONS[torch_version]["cuda"]) - bounded_cuda_version = max(min(cuda_version, max_supported_cuda), min_supported_cuda) - - if cuda_version != bounded_cuda_version: - warn( - f"Installed CUDA version is v{cuda_version}. \n" - f"v{min_supported_cuda} <= Supported CUDA version <= v{max_supported_cuda}.\n" - f"This script will use CUDA v{bounded_cuda_version}.\n" - f"However, this may not be safe, and you are advised to install the correct version of CUDA.\n" - f"For more details, refer to https://pytorch.org/get-started/locally/", - stacklevel=2, - ) - cuda_version = bounded_cuda_version - - return cuda_version - - -def get_cuda_suffix(cuda_version: str) -> str: - """Get CUDA suffix for PyTorch or mmX versions. - - Args: - cuda_version (str): CUDA version installed on the system. - - Note: - The CUDA version of PyTorch is not always the same as the CUDA version - that is installed on the system. For example, the latest PyTorch - version (1.10.0) supports CUDA 11.3, but the latest CUDA version - that is available for download is 11.2. Therefore, we need to use - the latest available CUDA version for PyTorch instead of the CUDA - version that is installed on the system. Therefore, this function - shoudl be regularly updated to reflect the latest available CUDA. - - Examples: - >>> get_cuda_suffix(cuda_version="11.2") - "cu112" - - >>> get_cuda_suffix(cuda_version="11.8") - "cu118" - - Returns: - str: CUDA suffix for PyTorch or mmX version. - """ - return f"cu{cuda_version.replace('.', '')}" - - -def get_hardware_suffix(with_available_torch_build: bool = False, torch_version: str | None = None) -> str: - """Get hardware suffix for PyTorch or mmX versions. - - Args: - with_available_torch_build (bool): Whether to use the latest available - PyTorch build or not. If True, the latest available PyTorch build - will be used. If False, the installed PyTorch build will be used. - Defaults to False. - torch_version (str | None): PyTorch version. This is only used when the - ``with_available_torch_build`` is True. - - Examples: - >>> # Assume that CUDA version is 11.2 - >>> get_hardware_suffix() - "cu112" - - >>> # Assume that CUDA is not installed on the system - >>> get_hardware_suffix() - "cpu" - - Assume that that installed CUDA version is 12.1. - However, the latest available CUDA version for PyTorch v2.0 is 11.8. - Therefore, we use 11.8 instead of 12.1. This is because PyTorch does not - support CUDA 12.1 yet. In this case, we could correct the CUDA version - by setting `with_available_torch_build` to True. - - >>> cuda_version = get_cuda_version() - "12.1" - >>> get_hardware_suffix(with_available_torch_build=True, torch_version="2.0.1") - "cu118" - - Returns: - str: Hardware suffix for PyTorch or mmX version. - """ - cuda_version = get_cuda_version() - if cuda_version: - if with_available_torch_build: - if torch_version is None: - msg = "``torch_version`` must be provided when with_available_torch_build is True." - raise ValueError(msg) - cuda_version = update_cuda_version_with_available_torch_cuda_build(cuda_version, torch_version) - hardware_suffix = get_cuda_suffix(cuda_version) - else: - hardware_suffix = "cpu" - - return hardware_suffix - - -def add_hardware_suffix_to_torch( - requirement: Requirement, - hardware_suffix: str | None = None, - with_available_torch_build: bool = False, -) -> str: - """Add hardware suffix to the torch requirement. - - Args: - requirement (Requirement): Requirement object comprising requirement - details. - hardware_suffix (str | None): Hardware suffix. If None, it will be set - to the correct hardware suffix. Defaults to None. - with_available_torch_build (bool): To check whether the installed - CUDA version is supported by the latest available PyTorch build. - Defaults to False. - - Examples: - >>> from pkg_resources import Requirement - >>> req = "torch>=1.13.0, <=2.0.1" - >>> requirement = Requirement.parse(req) - >>> requirement.name, requirement.specs - ('torch', [('>=', '1.13.0'), ('<=', '2.0.1')]) - - >>> add_hardware_suffix_to_torch(requirement) - 'torch>=1.13.0+cu121, <=2.0.1+cu121' - - ``with_available_torch_build=True`` will use the latest available PyTorch build. - >>> req = "torch==2.0.1" - >>> requirement = Requirement.parse(req) - >>> add_hardware_suffix_to_torch(requirement, with_available_torch_build=True) - 'torch==2.0.1+cu118' - - It is possible to pass the ``hardware_suffix`` manually. - >>> req = "torch==2.0.1" - >>> requirement = Requirement.parse(req) - >>> add_hardware_suffix_to_torch(requirement, hardware_suffix="cu121") - 'torch==2.0.1+cu111' - - Raises: - ValueError: When the requirement has more than two version criterion. - - Returns: - str: Updated torch package with the right cuda suffix. - """ - name = requirement.unsafe_name - updated_specs: list[str] = [] - - for operator, version in requirement.specs: - hardware_suffix = hardware_suffix or get_hardware_suffix(with_available_torch_build, version) - updated_version = version + f"+{hardware_suffix}" if version not in ("2.1.0", "2.1.1") else version - - # ``specs`` contains operators and versions as follows: - # These are to be concatenated again for the updated version. - updated_specs.append(operator + updated_version) - - updated_requirement: str = "" - - if updated_specs: - # This is the case when specs are e.g. ['<=1.9.1+cu111'] - if len(updated_specs) == 1: - updated_requirement = name + updated_specs[0] - # This is the case when specs are e.g., ['<=1.9.1+cu111', '>=1.8.1+cu111'] - elif len(updated_specs) == 2: - updated_requirement = name + updated_specs[0] + ", " + updated_specs[1] - else: - msg = ( - "Requirement version can be a single value or a range. \n" - "For example it could be torch>=1.8.1 " - "or torch>=1.8.1, <=1.9.1\n" - f"Got {updated_specs} instead." - ) - raise ValueError(msg) - return updated_requirement - - -def get_torch_install_args(requirement: str | Requirement) -> list[str]: - """Get the install arguments for Torch requirement. - - This function will return the install arguments for the Torch requirement - and its corresponding torchvision requirement. - - Args: - requirement (str | Requirement): The torch requirement. - - Raises: - RuntimeError: If the OS is not supported. - - Example: - >>> from pkg_resources import Requirement - >>> requriment = "torch>=1.13.0" - >>> get_torch_install_args(requirement) - ['--extra-index-url', 'https://download.pytorch.org/whl/cpu', - 'torch==1.13.0+cpu', 'torchvision==0.14.0+cpu'] - - Returns: - list[str]: The install arguments. - """ - if isinstance(requirement, str): - requirement = Requirement.parse(requirement) - - # NOTE: This does not take into account if the requirement has multiple versions - # such as torch<2.0.1,>=1.13.0 - if len(requirement.specs) < 1: - return [str(requirement)] - operator, version = requirement.specs[0] - install_args: list[str] = [] - - if platform.system() in ("Linux", "Windows"): - # Get the hardware suffix (eg., +cpu, +cu116 and +cu118 etc.) - hardware_suffix = get_hardware_suffix(with_available_torch_build=True, torch_version=version) - - # Create the PyTorch Index URL to download the correct wheel. - index_url = f"https://download.pytorch.org/whl/{hardware_suffix}" - - # Create the PyTorch version depending on the CUDA version. For example, - # If CUDA version is 11.2, then the PyTorch version is 1.8.0+cu112. - # If CUDA version is None, then the PyTorch version is 1.8.0+cpu. - torch_version = add_hardware_suffix_to_torch(requirement, hardware_suffix, with_available_torch_build=True) - - # Get the torchvision version depending on the torch version. - torchvision_version = AVAILABLE_TORCH_VERSIONS[version]["torchvision"] - torchvision_requirement = f"torchvision{operator}{torchvision_version}" - if torchvision_version not in ("0.16.0", "0.16.1"): - torchvision_requirement += f"+{hardware_suffix}" - - # Return the install arguments. - install_args += [ - "--extra-index-url", - # "--index-url", - index_url, - torch_version, - torchvision_requirement, - ] - elif platform.system() in ("macos", "Darwin"): - torch_version = str(requirement) - install_args += [torch_version] - else: - msg = f"Unsupported OS: {platform.system()}" - raise RuntimeError(msg) - - return install_args - - -def get_mmcv_install_args(torch_requirement: str | Requirement, mmcv_requirements: list[str]) -> list[str]: - """Get the install arguments for MMCV. - - Args: - torch_requirement (str | Requirement): Torch requirement. - mmcv_requirements (list[str]): MMCV requirements. - - Raises: - NotImplementedError: Not implemented for MacOS. - RuntimeError: If the OS is not supported. - - Returns: - list[str]: List of mmcv install arguments. - """ - if isinstance(torch_requirement, str): - torch_requirement = Requirement.parse(torch_requirement) - - if platform.system() in ("Linux", "Windows", "Darwin", "macos"): - # Get the hardware suffix (eg., +cpu, +cu116 and +cu118 etc.) - _, version = torch_requirement.specs[0] - hardware_suffix = get_hardware_suffix(with_available_torch_build=True, torch_version=version) - - # MMCV builds are only available for major.minor.0 torch versions. - major, minor, _ = version.split(".") - mmcv_torch_version = f"{major}.{minor}.0" - mmcv_index_url = ( - f"https://download.openmmlab.com/mmcv/dist/{hardware_suffix}/torch{mmcv_torch_version}/index.html" - ) - - # Return the install arguments. - return ["--find-links", mmcv_index_url, *mmcv_requirements] - - msg = f"Unsupported OS: {platform.system()}" - raise RuntimeError(msg) - - -def mim_installation(requirements: list[str]) -> int: - """Installing libraries with mim api. - - Args: - requirements (list[str]): List of MMCV-related libraries. - - Raises: - ModuleNotFoundError: Raise an error if mim import is not possible. - """ - if not find_spec("mim"): - msg = "The mmX library installation requires mim. mim is not currently installed." - raise ModuleNotFoundError(msg) - from mim import install - - return install(requirements) - - -def get_module_version(module_name: str) -> str | None: - """Return the version of the specified Python module. - - Args: - module_name (str): The name of the module to get the version of. - - Returns: - str | None: The version of the module, or None if the module is not installed. - """ - try: - module_version = pkg_resources.get_distribution(module_name).version - except pkg_resources.DistributionNotFound: - module_version = None - - return module_version - - -def patch_mmaction2() -> None: - """Patch MMAction2==1.2.0 with the custom code. - - The patch is at `src/otx/cli/patches/mmaction2.patch`. - The reason why we need is that `__init__.py` is missing in - https://github.com/open-mmlab/mmaction2/tree/v1.2.0/mmaction/models/localizers/drn - """ - dir_patches: Path = files("otx") / "cli" / "patches" - file_mmaction2_patch = dir_patches / "mmaction2.patch" - - if not file_mmaction2_patch.exists(): - msg = f"Cannot find `mmaction2.patch` file from {dir_patches}" - raise RuntimeError(msg) - - if (spec := find_spec("mmaction")) is None: - msg = "Cannot find mmaction spec" - raise RuntimeError(msg) - - if (spec_origin := spec.origin) is None: - msg = "Cannot find mmaction spec origin" - raise RuntimeError(msg) - - dir_mmaction_parent = Path(spec_origin).parent.parent - - proc = subprocess.Popen( - args=["patch", "-p0", "--forward"], - cwd=dir_mmaction_parent, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - stdout, stderr = proc.communicate(input=file_mmaction2_patch.read_bytes(), timeout=1) - - if proc.returncode > 1: - msg = f"Cannot patch. Error code: {proc.returncode}, stdout: {stdout.decode()} and stderr: {stderr.decode()}" - raise RuntimeError(msg) - if proc.returncode == 1: - warn("MMAction2 is already patched. Skip patching it.", stacklevel=1) diff --git a/src/otx/cli/utils/io.py b/src/otx/cli/utils/io.py new file mode 100644 index 00000000000..e747a93b42b --- /dev/null +++ b/src/otx/cli/utils/io.py @@ -0,0 +1,281 @@ +"""Utils for model io operations.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import json +import os +import os.path as osp +import re +import struct +import tempfile +from pathlib import Path +from typing import List, Optional, Tuple +from zipfile import ZipFile + +import cv2 +import numpy as np + +from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ( + ModelConfiguration, + ModelEntity, + ModelOptimizationType, +) +from otx.api.serialization.label_mapper import LabelSchemaMapper +from otx.api.usecases.adapters.model_adapter import ModelAdapter +from otx.cli.utils.nncf import is_checkpoint_nncf + +model_adapter_keys = ( + "confidence_threshold", + "metadata", + "config.json", + "tile_classifier.xml", + "tile_classifier.bin", + "visual_prompting_image_encoder.xml", + "visual_prompting_image_encoder.bin", + "visual_prompting_prompt_getter.xml", + "visual_prompting_prompt_getter.bin", + "visual_prompting_decoder.xml", + "visual_prompting_decoder.bin", + "image_threshold", # NOTE: used for compatibility with with OTX 1.2.x. Remove when all Geti projects are upgraded. + "pixel_threshold", # NOTE: used for compatibility with with OTX 1.2.x. Remove when all Geti projects are upgraded. + "min", # NOTE: used for compatibility with with OTX 1.2.x. Remove when all Geti projects are upgraded. + "max", # NOTE: used for compatibility with with OTX 1.2.x. Remove when all Geti projects are upgraded. +) + + +def save_model_data(model: ModelEntity, folder: str) -> None: + """Saves model data to folder. Folder is created if it does not exist. + + Args: + model (ModelEntity): The model to save. + folder (str): Path to output folder. + """ + + os.makedirs(folder, exist_ok=True) + for filename, model_adapter in model.model_adapters.items(): + with open(osp.join(folder, filename), "wb") as write_file: + write_file.write(model_adapter.data) + + +def read_binary(path: str) -> bytes: + """Loads binary data stored at path. + + Args: + path (str): A path where to load data from. + + Returns: + bytes: Binary data. + """ + try: + with open(path, "rb") as read_file: + return read_file.read() + except FileNotFoundError: + return b"" + + +def read_model(model_configuration: ModelConfiguration, path: str, train_dataset: DatasetEntity) -> ModelEntity: + """Creates ModelEntity based on model_configuration and data stored at path. + + Args: + model_configuration (ModelConfiguration): ModelConfiguration object. + path (str): Path to the model data. + train_dataset (DatasetEntity): DatasetEntity object. + + Returns: + ModelEntity: ModelEntity object. + """ + + if path.endswith(".bin") or path.endswith(".xml"): + return read_openvino_model(model_configuration, path, train_dataset) + if path.endswith(".pth"): + return read_pytorch_model(model_configuration, path, train_dataset) + if path.endswith(".zip"): + return read_deployed_model(model_configuration, path, train_dataset) + raise ValueError(f"Unknown file type: {path}") + + +def read_openvino_model( + model_configuration: ModelConfiguration, path: str, train_dataset: DatasetEntity +) -> ModelEntity: + """Reads an OpenVINO model from disk and returns a ModelEntity object.""" + + model_adapters = { + "openvino.xml": ModelAdapter(read_binary(path[:-4] + ".xml")), + "openvino.bin": ModelAdapter(read_binary(path[:-4] + ".bin")), + } + for key in model_adapter_keys: + full_path = osp.join(osp.dirname(path), key) + model_adapters[key] = ModelAdapter(read_binary(full_path)) + + model = ModelEntity( + configuration=model_configuration, + model_adapters=model_adapters, + train_dataset=train_dataset, + ) + + return model + + +def read_pytorch_model(model_configuration: ModelConfiguration, path: str, train_dataset: DatasetEntity) -> ModelEntity: + """Reads a PyTorch model from disk and returns a ModelEntity object.""" + optimization_type = ModelOptimizationType.NONE + + model_adapters = {"weights.pth": ModelAdapter(read_binary(path))} + + if is_checkpoint_nncf(path): + optimization_type = ModelOptimizationType.NNCF + + # Weights of auxiliary models + for key in os.listdir(osp.dirname(path)): + if re.match(r"aux_model_[0-9]+\.pth", key): + full_path = osp.join(osp.dirname(path), key) + model_adapters[key] = ModelAdapter(read_binary(full_path)) + + model = ModelEntity( + configuration=model_configuration, + model_adapters=model_adapters, + train_dataset=train_dataset, + optimization_type=optimization_type, + ) + + return model + + +def read_deployed_model( + model_configuration: ModelConfiguration, path: str, train_dataset: DatasetEntity +) -> ModelEntity: + """Reads a deployed model from disk and returns a ModelEntity object.""" + + with tempfile.TemporaryDirectory() as temp_dir: + with ZipFile(path) as myzip: + myzip.extractall(temp_dir) + + model_path = osp.join(temp_dir, "model") + model_adapters = { + "openvino.xml": ModelAdapter(read_binary(osp.join(model_path, "model.xml"))), + "openvino.bin": ModelAdapter(read_binary(osp.join(model_path, "model.bin"))), + } + + config_path = osp.join(model_path, "config.json") + with open(config_path, encoding="UTF-8") as f: + model_parameters = json.load(f)["model_parameters"] + model_adapters["config.json"] = ModelAdapter(read_binary(config_path)) + + for key in model_adapter_keys: + if key in model_parameters: + if key == "metadata": # anomaly tasks now use metadata for storing all parameters + model_adapters[key] = ModelAdapter(json.dumps(model_parameters[key]).encode()) + else: + model_adapters[key] = ModelAdapter(struct.pack("f", model_parameters[key])) + if key.endswith(".xml") or key.endswith(".bin"): + model_adapters[key] = ModelAdapter(read_binary(osp.join(model_path, key))) + + model = ModelEntity( + configuration=model_configuration, + model_adapters=model_adapters, + train_dataset=train_dataset, + ) + return model + + +def read_label_schema(path: str) -> LabelSchemaEntity: + """Reads serialized LabelSchema and returns deserialized LabelSchema. + + Args: + path (str): Path to model. It assmues that the `label_schema.json` is at the same location as the model. + + Returns: + LabelSchemaEntity: Desetialized LabelSchemaEntity. + """ + + if any(path.endswith(extension) for extension in (".xml", ".bin", ".pth")): + with open(osp.join(osp.dirname(path), "label_schema.json"), encoding="UTF-8") as read_file: + serialized_label_schema = json.load(read_file) + elif path.endswith(".zip"): + with ZipFile(path) as read_zip_file: + with read_zip_file.open(osp.join("model", "config.json")) as read_file: + serialized_label_schema = json.load(read_file)["model_parameters"]["labels"] + return LabelSchemaMapper().backward(serialized_label_schema) + + +def get_image_files(root_dir: str) -> Optional[List[Tuple[str, str]]]: + """Recursively get all image file paths from given root_dir.""" + img_data_formats = ( + ".jpg", + ".JPG", + ".jpeg", + ".JPEG", + ".gif", + ".GIF", + ".bmp", + ".BMP", + ".tif", + ".TIF", + ".tiff", + ".TIFF", + ".png", + ".PNG", + ) + # single image path + if root_dir.endswith(img_data_formats): + return [("./", root_dir)] + + img_files = [] + for root, _, _ in os.walk(root_dir): + for format_ in img_data_formats: + img_files.extend([(root, file.name) for file in Path(root).glob(f"*{format_}")]) + return img_files if img_files else None + + +def save_saliency_output( + process_saliency_maps: bool, + img: np.array, + saliency_map: np.array, + save_dir: str, + fname: str, + weight: float = 0.3, +) -> None: + """Saves processed saliency map (with image overlay) or raw saliency map.""" + if process_saliency_maps: + # Saves processed saliency map + overlay = img * weight + saliency_map * (1 - weight) + overlay[overlay > 255] = 255 + overlay = overlay.astype(np.uint8) + + cv2.imwrite(f"{osp.join(save_dir, fname)}_saliency_map.png", saliency_map) + cv2.imwrite(f"{osp.join(save_dir, fname)}_overlay_img.png", overlay) + else: + # Saves raw, low-resolution saliency map + cv2.imwrite(f"{osp.join(save_dir, fname)}_saliency_map.tiff", saliency_map) + + +def get_explain_dataset_from_filelist(image_files: list): + """Get explain dataset with empty annotation.""" + empty_annotation = AnnotationSceneEntity(annotations=[], kind=AnnotationSceneKind.PREDICTION) + items = [] + for root_dir, filename in image_files: + frame = cv2.imread(osp.join(root_dir, filename)) + item = DatasetItemEntity( + media=Image(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)), + annotation_scene=empty_annotation, + ) + items.append(item) + explain_dataset = DatasetEntity(items=items) + return explain_dataset diff --git a/src/otx/cli/utils/jsonargparse.py b/src/otx/cli/utils/jsonargparse.py deleted file mode 100644 index 751217fe8f7..00000000000 --- a/src/otx/cli/utils/jsonargparse.py +++ /dev/null @@ -1,422 +0,0 @@ -"""Functions related to jsonargparse.""" - -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import ast -import logging -from contextlib import contextmanager -from pathlib import Path -from typing import Any, Iterator, TypeVar, Union - -import docstring_parser -from jsonargparse import ActionConfigFile, ArgumentParser, Namespace, dict_to_namespace, namespace_to_dict - -from otx.core.types import PathLike - -logger = logging.getLogger() - - -def get_short_docstring(component: TypeVar) -> str: - """Get the short description from the docstring. - - Args: - component (TypeVar): The component to get the docstring from - - Returns: - str: The short description - """ - if component.__doc__ is None: - return "" - docstring = docstring_parser.parse(component.__doc__) - return docstring.short_description - - -def flatten_dict(config: dict, parent_key: str = "", sep: str = ".") -> dict: - """Flatten a nested dictionary into a single-level dictionary. - - Args: - d (dict): The dictionary to be flattened. - parent_key (str): The parent key to be used for nested keys. - sep (str): The separator to be used between parent and child keys. - - Returns: - dict: The flattened dictionary. - - """ - items: list = [] - for k, v in config.items(): - new_key = f"{parent_key}{sep}{k}" if parent_key else k - if isinstance(v, dict): - items.extend(flatten_dict(v, new_key, sep=sep).items()) - else: - items.append((new_key, v)) - return dict(items) - - -# [FIXME]: Overriding Namespce.update to match mmengine.Config (DictConfig | dict) -# and prevent int, float types from being converted to str -# https://github.com/omni-us/jsonargparse/issues/236 -def update( - self: Namespace, - value: Any, # noqa: ANN401 - key: str | None = None, - only_unset: bool = False, -) -> Namespace: - """Sets or replaces all items from the given nested namespace. - - Args: - value: A namespace to update multiple values or other type to set in a single key. - key: Branch key where to set the value. Required if value is not namespace. - only_unset: Whether to only set the value if not set in namespace. - """ - is_value_dict = False - if isinstance(value, dict): - # Dict -> Nested Namespace for overriding - is_value_dict = True - value = dict_to_namespace(value) - if not isinstance(value, (Namespace, dict)): - if not key: - msg = "Key is required if value not a Namespace." - raise KeyError(msg) - if not only_unset or key not in self: - if key not in self or value is not None: - if isinstance(value, str) and (value.isnumeric() or value in ("True", "False")): - value = ast.literal_eval(value) - self[key] = value - elif value is None: - del self[key] - else: - prefix = key + "." if key else "" - for _key, val in value.items(): - if not only_unset or prefix + _key not in self: - self.update(val, prefix + _key) - if is_value_dict and key is not None: - # Dict or Namespace -> Dict - self[key] = dict_to_namespace(self[key]).as_dict() - return self - - -# To provide overriding of the Config file -def apply_config(self: ActionConfigFile, parser: ArgumentParser, cfg: Namespace, dest: str, value: str) -> None: # noqa: ARG001 - """Applies the configuration to the parser. - - Args: - parser: The parser object. - cfg: The configuration object. - dest: The destination attribute. - value: The value to be applied. - - Returns: - None - """ - from jsonargparse._actions import _ActionSubCommands, previous_config_context - from jsonargparse._link_arguments import skip_apply_links - from jsonargparse._loaders_dumpers import get_loader_exceptions, load_value - from jsonargparse._optionals import get_config_read_mode - - with _ActionSubCommands.not_single_subcommand(), previous_config_context(cfg), skip_apply_links(): - kwargs = {"env": False, "defaults": False, "_skip_check": True, "_fail_no_subcommand": False} - try: - cfg_path: Path | None = Path(value, mode=get_config_read_mode()) - except TypeError: - try: - if isinstance(load_value(value), str): - raise - cfg_path = None - cfg_file = parser.parse_string(value, **kwargs) - except (TypeError, *get_loader_exceptions()) as ex_str: - msg = f'Parser key "{dest}": {ex_str}' - raise TypeError(msg) from ex_str - else: - cfg_file = parser.parse_path(value, **kwargs) - cfg_merged = parser.merge_config(cfg_file, cfg) - cfg.__dict__.update(cfg_merged.__dict__) - overrides = cfg.__dict__.pop("overrides", None) - if overrides is not None: - # This is a feature to handle the callbacks & logger override for user-convinience - list_override(configs=cfg, key="callbacks", overrides=overrides.pop("callbacks", [])) - list_override(configs=cfg, key="logger", overrides=overrides.pop("logger", [])) - cfg.update(overrides) - if cfg.get(dest) is None: - cfg[dest] = [] - cfg[dest].append(cfg_path) - - -def list_override(configs: Namespace, key: str, overrides: list) -> None: - """Overrides the nested list type in the given configs with the provided override_list. - - Args: - configs (Namespace): The configuration object containing the key. - key (str): key of the configs want to override. - overrides (list): The list of dictionary item to override the existing ones. - - Example: - >>> configs = [ - ... ... - ... Namespace( - ... class_path='lightning.pytorch.callbacks.EarlyStopping', - ... init_args=Namespace(patience=10, ...), - ... ), - ... ... - ... ] - >>> override_callbacks = [ - ... ... - ... { - ... 'class_path': 'lightning.pytorch.callbacks.EarlyStopping', - ... 'init_args': {'patience': 3}, - ... }, - ... ... - ... ] - >>> list_override(configs=configs, key="callbacks", overrides=override_callbacks) - >>> configs = [ - ... ... - ... Namespace( - ... class_path='lightning.pytorch.callbacks.EarlyStopping', - ... init_args=Namespace(patience=3, ...), - ... ), - ... ... - ... ] - """ - if key not in configs or configs[key] is None: - return - for target in overrides: - class_path = target.get("class_path", None) - if class_path is None: - msg = "class_path is required in the override list." - raise ValueError(msg) - - item = next((item for item in configs[key] if item["class_path"] == class_path), None) - if item is not None: - Namespace(item).update(target) - else: - configs[key].append(dict_to_namespace(target)) - - -# [FIXME] harimkang: have to see if there's a better way to do it. (For now, Added 2 lines to existing function) -# The thing called `overrides` is only available in OTXCLI via `apply_config`. -# Currently, default_config_files in jsonargparse is loading the default config file without using the ActionConfigFile, -# and it's not updating the overrides properly in the process. -# So this function patches to allow configs to come in via `default_config_files` with `overrides` applied. -def get_defaults_with_overrides(self: ArgumentParser, skip_check: bool = False) -> Namespace: - """Returns a namespace with all default values. - - Args: - skip_check: Whether to skip check if configuration is valid. - - Returns: - An object with all default values as attributes. - """ - import argparse - - from jsonargparse._actions import _ActionPrintConfig, filter_default_actions - from jsonargparse._common import parser_context - from jsonargparse._namespace import recreate_branches - from jsonargparse._parameter_resolvers import UnknownDefault - from jsonargparse._typehints import ActionTypeHint - from jsonargparse._util import argument_error, change_to_path_dir - - cfg = Namespace() - for action in filter_default_actions(self._actions): - if ( - action.default != argparse.SUPPRESS - and action.dest != argparse.SUPPRESS - and not isinstance(action.default, UnknownDefault) - ): - cfg[action.dest] = recreate_branches(action.default) - - self._logger.debug("Loaded parser defaults: %s", cfg) - - default_config_files = self._get_default_config_files() - for key, default_config_file in default_config_files: - with change_to_path_dir(default_config_file), parser_context(parent_parser=self): - cfg_file = self._load_config_parser_mode(default_config_file.get_content(), key=key) - cfg = self.merge_config(cfg_file, cfg) - overrides = cfg.__dict__.pop("overrides", {}) - list_override(configs=cfg, key="callbacks", overrides=overrides.pop("callbacks", [])) - list_override(configs=cfg, key="logger", overrides=overrides.pop("logger", [])) - if overrides is not None: - cfg.update(overrides) - try: - with _ActionPrintConfig.skip_print_config(): - cfg = self._parse_common( - cfg=cfg, - env=False, - defaults=False, - with_meta=None, - skip_check=skip_check, - skip_required=True, - ) - except (TypeError, KeyError, argparse.ArgumentError) as ex: - msg = f'Problem in default config file "{default_config_file}": {ex.args[0]}' - raise argument_error(msg) from ex - meta = cfg.get("__default_config__") - if isinstance(meta, list): - meta.append(default_config_file) - elif isinstance(meta, Path): - cfg["__default_config__"] = [meta, default_config_file] - else: - cfg["__default_config__"] = default_config_file - self._logger.debug("Parsed default configuration from path: %s", default_config_file) - - ActionTypeHint.add_sub_defaults(self, cfg) - - return cfg - - -# Workaround for https://github.com/omni-us/jsonargparse/issues/456 -def add_list_type_arguments( - parser: ArgumentParser, - baseclass: tuple[type, ...], - nested_key: str, - skip: set[str] | None = None, -) -> None: - """Add list type arguments to the given ArgumentParser. - - From python >= 3.11, add_subclass_arguments no longer allows adding arguments of the form list[Class]. - Modify it to bypass class checking, allowing you to use the list argument. - Copy from jsonargparse._signatures.SignatureArguments.add_subclass_arguments. - - Args: - parser (ArgumentParser): The ArgumentParser to add the arguments to. - baseclass (tuple[type, ...]): A tuple of base classes for the subclasses. - nested_key (str): The nested key for the arguments. - skip (set[str] | None, optional): A set of arguments to skip. Defaults to None. - """ - from argparse import SUPPRESS - - from jsonargparse._parameter_resolvers import ParamData - from jsonargparse._util import get_import_path, iter_to_set_str - - group = parser._create_group_if_requested( # noqa: SLF001 - baseclass, - nested_key, - True, - None, - config_load=False, - required=False, - instantiate=False, - ) - added_args: list[str] = [] - if skip is not None: - skip = {f"{nested_key}.init_args." + s for s in skip} - param = ParamData(name=nested_key, annotation=Union[baseclass], component=baseclass) - str_baseclass = iter_to_set_str(get_import_path(x) for x in baseclass) - kwargs = { - "metavar": "CONFIG | CLASS_PATH_OR_NAME | .INIT_ARG_NAME VALUE", - "help": ( - f"One or more arguments specifying 'class_path' and 'init_args' for any subclass of {str_baseclass}s." - ), - } - kwargs["default"] = SUPPRESS - parser._add_signature_parameter( # noqa: SLF001 - group, - None, - param, - added_args, - skip, - sub_configs=True, - instantiate=False, - fail_untyped=False, - ) - - -@contextmanager -def patch_update_configs() -> Iterator[None]: - """Patch the update and apply_config methods of the given namespace and action_config_file objects.""" - original_update = Namespace.update - original_apply_config = ActionConfigFile.apply_config - original_get_defaults = ArgumentParser.get_defaults - - try: - Namespace.update = update - ActionConfigFile.apply_config = apply_config - ArgumentParser.get_defaults = get_defaults_with_overrides - yield - finally: - Namespace.update = original_update - ActionConfigFile.apply_config = original_apply_config - ArgumentParser.get_defaults = original_get_defaults - - -def get_configuration(config_path: str | Path, subcommand: str = "train", **kwargs) -> dict: - """Get the configuration from the given path. - - Args: - config_path (str | Path): The path to the configuration file. - - Returns: - dict: The configuration dictionary. - """ - from otx.cli.cli import OTXCLI - - with patch_update_configs(): - parser, _ = OTXCLI.engine_subcommand_parser(subcommand=subcommand) - if kwargs: - parser.set_defaults(**kwargs) - - args = parser.parse_args(args=["--config", str(config_path)], _skip_check=True) - - config = namespace_to_dict(args) - logger.info(f"{config_path} is loaded.") - - # Remove unnecessary cli arguments for API usage - cli_args = [ - "verbose", - "data_root", - "task", - "seed", - "callback_monitor", - "resume", - "disable_infer_num_classes", - "workspace", - ] - logger.warning(f"The corresponding keys in config are not used.: {cli_args}") - for arg in cli_args: - config.pop(arg, None) - return config - - -def get_instantiated_classes( - config: PathLike, - work_dir: PathLike | None, - data_root: PathLike | None, - **kwargs, -) -> tuple[dict, dict]: - """Get the instantiated classes for training. - - Args: - config (PathLike): Path to the configuration file. - work_dir (PathLike): Path to the working directory. - data_root (PathLike): Path to the data root directory. - - Returns: - dict: The instantiated classes for training. - """ - from otx.cli import OTXCLI - - cli_args = [ - "train", - "--config", - str(config), - "--workspace.use_sub_dir", - "false", - ] - if work_dir is not None: - cli_args.extend(["--work_dir", str(work_dir)]) - if data_root is not None: - cli_args.extend(["--data_root", str(data_root)]) - for key, value in kwargs.items(): - cli_args.extend([f"--{key}", str(value)]) - otx_cli = OTXCLI( - args=cli_args, - run=False, - ) - - otx_cli.set_seed() - otx_cli.instantiate_classes(instantiate_engine=False) - instantiated_config = namespace_to_dict(otx_cli.config_init["train"]) - - return instantiated_config, otx_cli.prepare_subcommand_kwargs("train") diff --git a/src/otx/cli/utils/multi_gpu.py b/src/otx/cli/utils/multi_gpu.py new file mode 100644 index 00000000000..834a9aa6087 --- /dev/null +++ b/src/otx/cli/utils/multi_gpu.py @@ -0,0 +1,360 @@ +"""Multi GPU training utility.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import datetime +import os +import signal +import socket +import sys +import threading +import time +from contextlib import closing +from typing import Callable, List, Optional, Union + +import psutil +import torch +import torch.distributed as dist +import torch.multiprocessing as mp + +from otx.api.configuration import ConfigurableParameters +from otx.utils.logger import get_logger + +logger = get_logger() + + +def _get_free_port(): + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + sock.bind(("", 0)) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return sock.getsockname()[1] + + +def get_gpu_ids(gpus: str) -> List[int]: + """Get proper GPU indices form `--gpu` arguments. + + Given `--gpus` argument, exclude inappropriate indices and transform to list of int format. + + Args: + gpus (str): GPU indices to use. Format should be Comma-separated indices. + + Returns: + List[int]: + list including proper GPU indices. + """ + num_available_gpu = torch.cuda.device_count() + gpu_ids = [] + for gpu_id in gpus.split(","): + if not gpu_id.isnumeric(): + raise ValueError("--gpus argument should be numbers separated by ','.") + gpu_ids.append(int(gpu_id)) + + wrong_gpus = [] + for gpu_idx in gpu_ids: + if gpu_idx >= num_available_gpu: + wrong_gpus.append(gpu_idx) + + for wrong_gpu in wrong_gpus: + gpu_ids.remove(wrong_gpu) + + if wrong_gpus: + logger.warning(f"Wrong gpu indices are excluded. {','.join([str(val) for val in gpu_ids])} GPU will be used.") + + return gpu_ids + + +def set_arguments_to_argv(keys: Union[str, List[str]], value: Optional[str] = None, after_params: bool = False): + """Add arguments at proper position in `sys.argv`. + + Args: + keys (str or List[str]): arguement keys. + value (str or None): argument value. + after_params (bool): whether argument should be after `param` or not. + """ + if not isinstance(keys, list): + keys = [keys] + for key in keys: + if key in sys.argv: + if value is not None: + sys.argv[sys.argv.index(key) + 1] = value + return + + key = keys[0] + if not after_params and "params" in sys.argv: + sys.argv.insert(sys.argv.index("params"), key) + if value is not None: + sys.argv.insert(sys.argv.index("params"), value) + else: + if after_params and "params" not in sys.argv: + sys.argv.append("params") + if value is not None: + sys.argv.extend([key, value]) + else: + sys.argv.append(key) + + +def is_multigpu_child_process(): + """Check current process is a child process for multi GPU training.""" + return (dist.is_initialized() or "TORCHELASTIC_RUN_ID" in os.environ) and os.environ["LOCAL_RANK"] != "0" + + +class MultiGPUManager: + """Class to manage multi GPU training. + + Args: + train_func (Callable): model training function. + gpu_ids (str): GPU indices to use. Format should be Comma-separated indices. + rdzv_endpoint (str): Rendezvous endpoint for multi-node training. + base_rank (int): Base rank of the worker. + world_size (int): Total number of workers in a worker group. + start_time (Optional[datetime.datetime]): Time when process starts. + This value is used to decide timeout argument of distributed training. + """ + + # pylint: disable=too-many-instance-attributes + + def __init__( + self, + train_func: Callable, + gpu_ids: str, + rdzv_endpoint: str = "localhost:0", + base_rank: int = 0, + world_size: int = 0, + start_time: Optional[datetime.datetime] = None, + ): + if ":" not in rdzv_endpoint: + raise ValueError("rdzv_endpoint must be in form :.") + host, port = rdzv_endpoint.split(":") + if port == "0": + assert host in ["localhost", "127.0.0.1"] + port = _get_free_port() + rdzv_endpoint = f"{host}:{port}" + + self._train_func = train_func + self._gpu_ids = get_gpu_ids(gpu_ids) + self._rdzv_endpoint = rdzv_endpoint + self._base_rank = base_rank + if world_size == 0: + world_size = len(self._gpu_ids) + self._world_size = world_size + self._main_pid = os.getpid() + self._processes: List[mp.Process] = [] + + if start_time is not None: + elapsed_time = datetime.datetime.now() - start_time + if elapsed_time > datetime.timedelta(seconds=40): + os.environ["TORCH_DIST_TIMEOUT"] = str(int(elapsed_time.total_seconds() * 1.5)) + + def is_available(self) -> bool: + """Check multi GPU training is available. + + Returns: + bool: + whether multi GPU training is available. + """ + return ( + len(self._gpu_ids) > 1 + and "TORCHELASTIC_RUN_ID" + not in os.environ # If otx is executed by torchrun, then otx multi gpu interface is disabled. + ) + + def setup_multi_gpu_train( + self, + output_path: str, + optimized_hyper_parameters: Optional[ConfigurableParameters] = None, + ): + """Carry out what should be done to run multi GPU training. + + Args: + output_path (str): output path where task output are saved. + optimized_hyper_parameters (ConfigurableParameters or None): hyper parameters reflecting HPO result. + + Returns: + str: + If output_path is None, make a temporary directory and return it. + """ + if optimized_hyper_parameters is not None: # if HPO is executed, optimized HPs are applied to child processes + self._set_optimized_hp_for_child_process(optimized_hyper_parameters) + + self._processes = self._spawn_multi_gpu_processes(output_path) + + signal.signal(signal.SIGINT, self._terminate_signal_handler) + signal.signal(signal.SIGTERM, self._terminate_signal_handler) + + self.initialize_multigpu_train(self._rdzv_endpoint, self._base_rank, 0, self._gpu_ids, self._world_size) + + threading.Thread(target=self._check_child_processes_alive, daemon=True).start() + + def finalize(self): + """Join all child processes.""" + for p in self._processes: + if p.join(30) is None and p.exitcode is None: + p.kill() + + @staticmethod + def initialize_multigpu_train( + rdzv_endpoint: str, + rank: int, + local_rank: int, + gpu_ids: List[int], + world_size: int, + ): + """Initilization for multi GPU training. + + Args: + rdzv_endpoint (str): Rendezvous endpoint for multi-node training. + rank (int): The rank of worker within a worker group. + local_rank (int): The rank of worker within a local worker group. + gpu_ids (List[int]): list including which GPU indeces will be used. + world_size (int): Total number of workers in a worker group. + """ + + host, port = rdzv_endpoint.split(":") + os.environ["MASTER_ADDR"] = host + os.environ["MASTER_PORT"] = port + os.environ["LOCAL_WORLD_SIZE"] = str(len(gpu_ids)) + os.environ["WORLD_SIZE"] = str(world_size) + os.environ["LOCAL_RANK"] = str(local_rank) + os.environ["RANK"] = str(rank) + + @staticmethod + def run_child_process( + train_func: Callable, + output_path: str, + rdzv_endpoint: str, + rank: int, + local_rank: int, + gpu_ids: List[int], + world_size: int, + ): + """Function for multi GPU child process to execute. + + Args: + train_func (Callable): model training function. + output_path (str): output path where task output are saved. + rdzv_endpoint (str): Rendezvous endpoint for multi-node training. + rank (int): The rank of worker within a worker group. + local_rank (int): The rank of worker within a local worker group. + gpu_ids (List[int]): list including which GPU indeces will be used. + world_size (int): Total number of workers in a worker group. + """ + + # initialize start method + mp.set_start_method(method=None, force=True) + + gpus_arg_idx = sys.argv.index("--gpus") + for _ in range(2): + sys.argv.pop(gpus_arg_idx) + if "--enable-hpo" in sys.argv: + sys.argv.remove("--enable-hpo") + set_arguments_to_argv(["-o", "--output"], output_path) + set_arguments_to_argv("--rdzv-endpoint", rdzv_endpoint) + + MultiGPUManager.initialize_multigpu_train(rdzv_endpoint, rank, local_rank, gpu_ids, world_size) + + threading.Thread(target=MultiGPUManager.check_parent_processes_alive, daemon=True).start() + + train_func() + + @staticmethod + def check_parent_processes_alive(): + """Check parent process is alive and if not, exit by itself.""" + cur_process = psutil.Process() + parent = cur_process.parent() + while True: + time.sleep(1) + if not parent.is_running(): + break + + logger.warning("Parent process is terminated abnormally. Process exits.") + cur_process.kill() + + def _spawn_multi_gpu_processes(self, output_path: str) -> List[mp.Process]: + processes = [] + ctx = mp.get_context("spawn") + + # set CUDA_VISIBLE_DEVICES to make child process use proper GPU + origin_cuda_visible_devices = os.environ.get("CUDA_VISIBLE_DEVICES") + if origin_cuda_visible_devices is not None: + cuda_visible_devices = origin_cuda_visible_devices.split(",") + else: + cuda_visible_devices = [str(i) for i in range(torch.cuda.device_count())] + os.environ["CUDA_VISIBLE_DEVICES"] = ",".join([cuda_visible_devices[gpu_idx] for gpu_idx in self._gpu_ids]) + + for rank in range(1, len(self._gpu_ids)): + task_p = ctx.Process( + target=MultiGPUManager.run_child_process, + args=( + self._train_func, + output_path, + self._rdzv_endpoint, + self._base_rank + rank, + rank, + self._gpu_ids, + self._world_size, + ), + ) + task_p.start() + processes.append(task_p) + + if origin_cuda_visible_devices is None: + del os.environ["CUDA_VISIBLE_DEVICES"] + else: + os.environ["CUDA_VISIBLE_DEVICES"] = origin_cuda_visible_devices + + return processes + + def _terminate_signal_handler(self, signum, _frame): + # This code prevents child processses from being killed unintentionally by proccesses forked from main process + if self._main_pid != os.getpid(): + sys.exit() + + self._kill_child_process() + + singal_name = {2: "SIGINT", 15: "SIGTERM"} + logger.warning(f"{singal_name[signum]} is sent. process exited.") + + sys.exit(1) + + def _kill_child_process(self): + for process in self._processes: + if process.is_alive(): + logger.warning(f"Kill child process {process.pid}") + process.kill() + + def _set_optimized_hp_for_child_process(self, hyper_parameters: ConfigurableParameters): + set_arguments_to_argv( + "--learning_parameters.learning_rate", + str(hyper_parameters.learning_parameters.learning_rate), # type: ignore[attr-defined] + True, + ) + set_arguments_to_argv( + "--learning_parameters.batch_size", + str(hyper_parameters.learning_parameters.batch_size), # type: ignore[attr-defined] + True, + ) + + def _check_child_processes_alive(self): + child_is_running = True + while child_is_running: + time.sleep(1) + for p in self._processes: + if not p.is_alive() and p.exitcode != 0: + child_is_running = False + break + + logger.warning("Some of child processes are terminated abnormally. process exits.") + self._kill_child_process() + os.kill(self._main_pid, signal.SIGKILL) diff --git a/src/otx/cli/utils/nncf.py b/src/otx/cli/utils/nncf.py new file mode 100644 index 00000000000..65b693799df --- /dev/null +++ b/src/otx/cli/utils/nncf.py @@ -0,0 +1,47 @@ +"""NNCF-related utils.""" + +# Copyright (C) 2021-2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import torch + + +def is_checkpoint_nncf(path: str) -> bool: + """Check if checkpoint is NNCF checkpoint. + + The function uses metadata stored in a checkpoint to check if the + checkpoint was the result of training of NNCF-compressed model. + """ + state = torch.load(path, map_location="cpu") + is_nncf = bool(state.get("meta", {}).get("nncf_enable_compression")) or "nncf_metainfo" in state + return is_nncf + + +def get_number_of_fakequantizers_in_xml(path_to_xml: str) -> int: + """Return number of FakeQuantize layers. + + Return number of FakeQuantize layers in the model by parsing file without loading model. + + Args: + path_to_xml (str): Path to xml file. + + Returns: + int: Number of FakeQuantize layers. + """ + num_fq = 0 + with open(path_to_xml, "r", encoding="UTF-8") as stream: + for line in stream.readlines(): + if 'type="FakeQuantize"' in line: + num_fq += 1 + return num_fq diff --git a/src/otx/cli/utils/parser.py b/src/otx/cli/utils/parser.py new file mode 100644 index 00000000000..1d58d5b6308 --- /dev/null +++ b/src/otx/cli/utils/parser.py @@ -0,0 +1,266 @@ +"""Utils for parsing command line arguments.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import argparse +import re +import sys +from argparse import RawTextHelpFormatter +from pathlib import Path +from typing import Dict, List, Optional, Union + +from otx.api.entities.model_template import ModelTemplate, parse_model_template +from otx.cli.registry import find_and_parse_model_template + + +class MemSizeAction(argparse.Action): + """Parser add on to parse memory size string.""" + + def __init__(self, option_strings, dest, nargs=None, **kwargs): + if nargs is not None: + raise ValueError("nargs not allowed") + expected_dest = "params.algo_backend.mem_cache_size" + if dest != expected_dest: + raise ValueError(f"dest should be {expected_dest}, but dest={dest}.") + super().__init__(option_strings, dest, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + """Parse and set the attribute of namespace.""" + setattr(namespace, self.dest, self._parse_mem_size_str(values)) + + @staticmethod + def _parse_mem_size_str(mem_size: str) -> int: + assert isinstance(mem_size, str) + + match = re.match(r"^([\d\.]+)\s*([a-zA-Z]{0,3})$", mem_size.strip()) + + if match is None: + raise ValueError(f"Cannot parse {mem_size} string.") + + units = { + "": 1, + "B": 1, + "KIB": 2**10, + "MIB": 2**20, + "GIB": 2**30, + "KB": 10**3, + "MB": 10**6, + "GB": 10**9, + "K": 2**10, + "M": 2**20, + "G": 2**30, + } + + number, unit = int(match.group(1)), match.group(2).upper() + + if unit not in units: + raise ValueError(f"{mem_size} has disallowed unit ({unit}).") + + return number * units[unit] + + +def gen_param_help(hyper_parameters: Dict) -> Dict: + """Generates help for hyper parameters section.""" + + type_map = {"FLOAT": float, "INTEGER": int, "BOOLEAN": bool, "SELECTABLE": str} + + help_keys = ("header", "type", "default_value", "max_value", "min_value") + + def _gen_param_help(prefix: str, cur_params: Dict) -> Dict: + cur_help = {} + for k, val in cur_params.items(): + if not isinstance(val, dict): + continue + + if "default_value" not in val.keys(): + x = _gen_param_help(prefix + f"{k}.", val) + cur_help.update(x) + else: + assert isinstance(val["default_value"], (int, float, str)) + help_str = "\n".join([f"{kk}: {val[kk]}" for kk in help_keys if kk in val.keys()]) + assert "." not in k + + cur_help.update( + { + prefix + + f"{k}": { + "default": val["default_value"], + "help": help_str, + "type": type_map[val["type"]], + "affects_outcome_of": val["affects_outcome_of"], + } + } + ) + return cur_help + + return _gen_param_help("", hyper_parameters) + + +def gen_params_dict_from_args( + args, override_param: Optional[List] = None, type_hint: Optional[dict] = None +) -> Dict[str, dict]: + """Generates hyper parameters dict from parsed command line arguments.""" + + def _get_leaf_node(curr_dict: Dict[str, dict], curr_key: str): + split_key = curr_key.split(".") + node_key = split_key[0] + + if len(split_key) == 1: + # It is leaf node + return curr_dict, node_key + + # Dive deeper + curr_key = ".".join(split_key[1:]) + if node_key not in curr_dict: + curr_dict[node_key] = {} + return _get_leaf_node(curr_dict[node_key], curr_key) + + _prefix = "params." + params_dict: Dict[str, dict] = {} + for param_name in dir(args): + value = getattr(args, param_name) + + if not param_name.startswith(_prefix) or value is None: + continue + if override_param and param_name not in override_param: + continue + + # param_name.removeprefix(_prefix) + origin_key = param_name[len(_prefix) :] + value_type = None + if type_hint is not None: + value_type = type_hint.get(origin_key, {}).get("type", None) + # FIXME[HARIM]: There's no template in args, and it's not inside the workspace, but with --workspace, + # the template is not found in args, so params, which are all bools, go into str. + # This is a temporary solution. + if isinstance(value, str) and value.lower() in ("true", "false"): + value_type = str2bool + + leaf_node_dict, node_key = _get_leaf_node(params_dict, origin_key) + leaf_node_dict[node_key] = {"value": value_type(value) if value_type else value} + + return params_dict + + +def str2bool(val: Union[str, bool]) -> bool: + """If input type is string, convert it to boolean. + + Args: + val (Union[str, bool]): value to convert to boolean. + + Raises: + argparse.ArgumentTypeError: If type is neither string and boolean, raise an error. + + Returns: + bool: return converted boolean value. + """ + if isinstance(val, bool): + return val + if isinstance(val, str): + if val.lower() in ("true", "1"): + return True + if val.lower() in ("false", "0"): + return False + raise argparse.ArgumentTypeError("Boolean value expected.") + + +class ShortDefaultsHelpFormatter(argparse.RawTextHelpFormatter): + """Text Help Formatter that shortens.""" + + def _get_default_metavar_for_optional(self, action): + return action.dest.split(".")[-1].upper() + + +def add_hyper_parameters_sub_parser( + parser, config, modes=None, return_sub_parser=False +) -> Optional[argparse.ArgumentParser]: + """Adds hyper parameters sub parser.""" + + default_modes = ("TRAINING", "INFERENCE") + if modes is None: + modes = default_modes + assert isinstance(modes, tuple) + for mode in modes: + assert mode in default_modes + + params = gen_param_help(config) + + subparsers = parser.add_subparsers(help="sub-command help") + parser_a = subparsers.add_parser( + "params", + help="Hyper parameters defined in template file.", + formatter_class=ShortDefaultsHelpFormatter, + ) + for k, val in params.items(): + param_type = val["type"] + if val["affects_outcome_of"] not in modes: + continue + if param_type == bool: + param_type = str2bool + parser_a.add_argument( + f"--{k}", + default=val["default"], + help=val["help"], + dest=f"params.{k}", + type=param_type, + ) + if return_sub_parser: + return parser_a + return None + + +def get_parser_and_hprams_data(): + """A function to distinguish between when there is template input and when there is no template input. + + Inspect the template using pre_parser to get the template's hyper_parameters information. + Finally, it returns the parser used in the actual main. + """ + # TODO: Declaring pre_parser to get the template + pre_parser = argparse.ArgumentParser(add_help=False) + pre_parser.add_argument("template", nargs="?", default=None) + parsed, _ = pre_parser.parse_known_args() + params = [] + if "params" in sys.argv: + params = sys.argv[sys.argv.index("params") :] + + template = parsed.template + hyper_parameters = {} + parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter) + template_config = find_and_parse_model_template(template) + template_help_str = ( + "Enter the path or ID or name of the template file. \n" + "This can be omitted if you have train-data-roots or run inside a workspace." + ) + + if isinstance(template_config, ModelTemplate): + sys.argv[sys.argv.index(template)] = template_config.model_template_path + hyper_parameters = template_config.hyper_parameters.data + parser.add_argument("template", help=template_help_str) + elif Path("./template.yaml").exists(): + # Workspace Environments + template_config = parse_model_template("./template.yaml") + hyper_parameters = template_config.hyper_parameters.data + parser.add_argument("template", nargs="?", default="./template.yaml", help=template_help_str) + # TODO: Need fix for how to get hyper_parameters when no template is given and ./template.yaml doesn't exist + # Ex. When using --workspace outside of a workspace, but cannot access --workspace from this function. + else: + parser.add_argument("template", nargs="?", default=None, help=template_help_str) + + return parser, hyper_parameters, params + + +def get_override_param(params): + """Get override param list from params.""" + return [f"params.{param[2:].split('=')[0]}" for param in params if param.startswith("--")] diff --git a/src/otx/cli/utils/report.py b/src/otx/cli/utils/report.py new file mode 100644 index 00000000000..6baf796a9c3 --- /dev/null +++ b/src/otx/cli/utils/report.py @@ -0,0 +1,152 @@ +"""Report Generating for OTX CLI.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import sys +from collections import defaultdict +from pathlib import Path +from pprint import pformat +from typing import Any, Dict, Union + +import torch + +import otx +from otx.algorithms.common.utils import is_xpu_available +from otx.api.entities.model_template import ModelTemplate + + +def get_otx_report( + model_template: ModelTemplate, + task_config: Dict[str, Any], + data_config: Dict[str, Dict[str, str]], + results: Dict[str, Any], + output_path: Union[str, Path], +): + """Generate CLI reports.""" + dash_line = "-" * 60 + "\n\n" + # Header + report_str = get_otx_cli_ascii_banner() + report_str += dash_line + report_str += f"Current path: {Path.cwd()}\n" + report_str += f"sys.argv: {sys.argv}\n" + report_str += f"OTX: {otx.__version__}\n" + # 1. Machine Environment + report_str += sub_title_to_str("Running Environments") + report_str += env_info_to_str() + + # 2. Task Information (Task, Train-type, Etc.) + if model_template and task_config: + report_str += sub_title_to_str("Template Information") + report_str += template_info_to_str(model_template) + + # 3. Dataset Configuration + if data_config: + report_str += sub_title_to_str("Dataset Information") + report_str += data_config_to_str(data_config) + + # 4. Configurations + report_str += sub_title_to_str("Configurations") + report_str += task_config_to_str(task_config) + # 5. Result Summary + report_str += sub_title_to_str("Results") + for key, value in results.items(): + report_str += f"\t{key}: {pformat(value)}\n" + + Path(output_path).write_text(report_str, encoding="UTF-8") + + +def sub_title_to_str(title: str): + """Add sub title for report.""" + dash_line = "-" * 60 + report_str = "" + report_str += dash_line + "\n\n" + report_str += title + "\n\n" + report_str += dash_line + "\n" + return report_str + + +def env_info_to_str(): + """Get Environments.""" + report_str = "" + env_info = {} + try: + from mmcv.utils.env import collect_env + + env_info = collect_env() + if "PyTorch compiling details" in env_info: + env_info.pop("PyTorch compiling details") + except ModuleNotFoundError: + env_info["sys.platform"] = sys.platform + env_info["Python"] = sys.version.replace("\n", "") + + cuda_available = torch.cuda.is_available() + env_info["CUDA available"] = cuda_available + + if cuda_available: + devices = defaultdict(list) + for k in range(torch.cuda.device_count()): + devices[torch.cuda.get_device_name(k)].append(str(k)) + for name, device_ids in devices.items(): + env_info["GPU " + ",".join(device_ids)] = name + env_info["PyTorch"] = torch.__version__ + + if is_xpu_available(): + devices = defaultdict(list) + for k in range(torch.xpu.device_count()): + devices[torch.xpu.get_device_name(k)].append(str(k)) + for name, device_ids in devices.items(): + env_info["GPU " + ",".join(device_ids)] = name + + for key, value in env_info.items(): + report_str += f"\t{key}: {value}\n" + return report_str + + +def template_info_to_str(model_template: ModelTemplate): + """Get Template information.""" + report_str = "" + for key, value in model_template.__dict__.items(): + report_str += f"\t{key}: {pformat(value)}\n" + return report_str + + +def data_config_to_str(data_config: Dict[str, Dict[str, str]]): + """Get Dataset configuration.""" + report_str = "" + for subset_key, subset_value in data_config.items(): + report_str += f"{subset_key}:\n" + for key, value in subset_value.items(): + report_str += f"\t{key}: {value}\n" + return report_str + + +def task_config_to_str(task_config: Dict[str, Any]): + """Get Task configuration.""" + report_str = "" + not_target = ["log_config"] + for target, value in task_config.items(): + # Remove otx_dataset from the report as it is unnecessary. + if target == "data" and isinstance(value, dict): + for item in value.values(): + if isinstance(item, dict) and "otx_dataset" in item: + del item["otx_dataset"] + + if target not in not_target: + report_str += target + ": " + model_str = pformat(value) + report_str += model_str + "\n" + return report_str + + +def get_otx_cli_ascii_banner(): + """Get OTX ASCII banner.""" + return """ + + ██████╗ ████████╗ ██╗ ██╗ +██╔═══██╗ ╚══██╔══╝ ╚██╗██╔╝ +██║ ██║ ██║ ╚███╔╝ +██║ ██║ ██║ ██╔██╗ +╚██████╔╝ ██║ ██╔╝ ██╗ + ╚═════╝ ╚═╝ ╚═╝ ╚═╝ + +""" diff --git a/src/otx/cli/utils/telemetry.py b/src/otx/cli/utils/telemetry.py new file mode 100644 index 00000000000..0e98a0da6c9 --- /dev/null +++ b/src/otx/cli/utils/telemetry.py @@ -0,0 +1,88 @@ +"""Utilities for OpenVINO telemetry.""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=broad-exception-caught +import json + +import openvino_telemetry as tm + +from otx import __version__ + +__TM_CATEGORY_OTX = "otx" +__TM_MEASUREMENT_ID = "UA-17808594-29" +# __TM_MEASUREMENT_ID_FOR_TESTING = "UA-254359572-1" +# __TM_MEASUREMENT_ID = __TM_MEASUREMENT_ID_FOR_TESTING + +__TM_ACTION_VERSION = "version" +__TM_ACTION_CMD_SUCCESS = "success" +__TM_ACTION_CMD_FAILURE = "failure" +__TM_ACTION_CMD_EXCEPTION = "exception" +__TM_ACTION_ERROR = "error" + + +def init_telemetry_session(): + """Init session.""" + telemetry = tm.Telemetry(app_name=__TM_CATEGORY_OTX, app_version=str(__version__), tid=__TM_MEASUREMENT_ID) + telemetry.start_session(__TM_CATEGORY_OTX) + send_version(telemetry) + + return telemetry + + +def close_telemetry_session(telemetry): + """Close session.""" + if not isinstance(telemetry, tm.Telemetry): + raise RuntimeError(f"Invalid argument. required {type(tm.Telemetry)} but passed {type(telemetry)}") + telemetry.end_session(__TM_CATEGORY_OTX) + telemetry.force_shutdown(1.0) + + +def send_version(telemetry): + """Send application version.""" + if not isinstance(telemetry, tm.Telemetry): + raise RuntimeError(f"Invalid argument. required {type(tm.Telemetry)} but passed {type(telemetry)}") + __send_event(telemetry, __TM_ACTION_VERSION, str(__version__)) + + +def send_cmd_results(telemetry, cmd, results): + """Send cli telemetry data.""" + if not isinstance(telemetry, tm.Telemetry): + raise RuntimeError(f"Invalid argument. required {type(tm.Telemetry)} but passed {type(telemetry)}") + action = __TM_ACTION_ERROR + + if not isinstance(results, dict): + raise RuntimeError(f"Invalid argument. required {dict} but passed {type(results)}") + + retcode = results.pop("retcode", None) + if retcode is not None: + label = dict(cmd=cmd, **results) + if retcode >= 0: + action = __TM_ACTION_CMD_FAILURE + if retcode == 0: + action = __TM_ACTION_CMD_SUCCESS + else: + action = __TM_ACTION_CMD_EXCEPTION + label = dict(cmd=cmd, **results) + + if action == __TM_ACTION_ERROR: + __send_error(telemetry, f"Invalid results for sending cmd result: {results}") + else: + __send_event(telemetry, action, label) + + +def __send_event(telemetry, action, label, **kwargs): + """Wrapper of the openvino-telemetry.send_event().""" + try: + telemetry.send_event(__TM_CATEGORY_OTX, action, json.dumps(label), **kwargs) + except Exception as error: + print(f"An error while calling otm.send_event(): \n{repr(error)}") + + +def __send_error(telemetry, err_msg, **kwargs): + """Wrapper of the openvino-telemetry.send_error().""" + try: + telemetry.send_error(__TM_CATEGORY_OTX, err_msg, **kwargs) + except Exception as error: + print(f"An error while calling otm.send_error(): \n{repr(error)}") diff --git a/src/otx/cli/utils/workspace.py b/src/otx/cli/utils/workspace.py deleted file mode 100644 index 9d397df54bf..00000000000 --- a/src/otx/cli/utils/workspace.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Class modules that manage Workspace.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from datetime import datetime, timezone -from pathlib import Path - - -class Workspace: - """Represents a workspace for the OTX application. - - Args: - work_dir (Path | str | None, optional): The path to the workspace directory. Defaults to None. - use_sub_dir (bool, optional): Whether to use a subdirectory within the workspace. Defaults to True. - """ - - def __init__(self, work_dir: Path | str = Path.cwd(), use_sub_dir: bool = True): # noqa: B008 - work_dir = Path(work_dir) - self.work_dir = ( - work_dir / "otx-workspace" - # Without work_dir input & no .latest directory in root - if work_dir == Path.cwd() and not (work_dir / ".latest").exists() - else work_dir - ) - if use_sub_dir: - timestamp = datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S") - self.work_dir = self.work_dir / f"{timestamp}" - Path(self.work_dir).mkdir(parents=True, exist_ok=True) diff --git a/src/otx/config/data/openvino.yaml b/src/otx/config/data/openvino.yaml deleted file mode 100644 index 6ab639fbd74..00000000000 --- a/src/otx/config/data/openvino.yaml +++ /dev/null @@ -1,21 +0,0 @@ -defaults: - - default - -image_color_channel: BGR -stack_images: False - -train_subset: - transform_lib_type: TORCHVISION - transforms: - - _target_: torchvision.transforms.v2.ToImage - batch_size: 1 -val_subset: - transform_lib_type: TORCHVISION - transforms: - - _target_: torchvision.transforms.v2.ToImage - batch_size: 1 -test_subset: - transform_lib_type: TORCHVISION - transforms: - - _target_: torchvision.transforms.v2.ToImage - batch_size: 1 diff --git a/src/otx/core/__init__.py b/src/otx/core/__init__.py index a35295ff2c7..5e6cce56853 100644 --- a/src/otx/core/__init__.py +++ b/src/otx/core/__init__.py @@ -1,4 +1,15 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 +"""OTX Core.""" + +# Copyright (C) 2022 Intel Corporation # -"""OpenVINO Training Extension.""" +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/core/config/__init__.py b/src/otx/core/config/__init__.py deleted file mode 100644 index a17d8a25607..00000000000 --- a/src/otx/core/config/__init__.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Config data type objects.""" -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, _SpecialForm - -import yaml - -if TYPE_CHECKING: - from torch import dtype - - -def as_int_tuple(*args) -> tuple[int, ...]: - """Resolve YAML list into Python integer tuple. - - Example: - YAML file example:: - - ```yaml - mem_cache_img_max_size: ${as_int_tuple:500,500} - ``` - """ - return tuple(int(arg) for arg in args) - - -def as_torch_dtype(arg: str) -> dtype: - """Resolve YAML string into PyTorch dtype. - - Example: - YAML file example:: - - ```yaml - transforms: - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - ``` - """ - import torch - - mapping = { - "float32": torch.float32, - "float": torch.float, - "float64": torch.float64, - "double": torch.double, - "float16": torch.float16, - "bfloat16": torch.bfloat16, - "half": torch.half, - "uint8": torch.uint8, - "int8": torch.int8, - "int16": torch.int16, - "short": torch.short, - "int32": torch.int32, - "int": torch.int, - "int64": torch.int64, - "long": torch.long, - "complex32": torch.complex32, - "complex64": torch.complex64, - "cfloat": torch.cfloat, - "complex128": torch.complex128, - "cdouble": torch.cdouble, - "quint8": torch.quint8, - "qint8": torch.qint8, - "qint32": torch.qint32, - "bool": torch.bool, - "quint4x2": torch.quint4x2, - "quint2x4": torch.quint2x4, - } - prefix = "torch." - if not arg.startswith(prefix): - msg = f"arg={arg} should start with the `torch.` prefix" - raise ValueError(msg) - key = arg[len(prefix) :] - return mapping[key] - - -def dtype_representer(dumper: yaml.Dumper | yaml.representer.SafeRepresenter, data: dtype) -> yaml.ScalarNode: - """Custom representer for converting dtype object to YAML sequence node. - - Args: - dumper (yaml.Dumper): The YAML dumper object. - data (dtype): The dtype object to be converted. - - Returns: - yaml.Node: The converted YAML node. - """ - return dumper.represent_str("${as_torch_dtype:" + str(data) + "}") - - -def any_representer(dumper: yaml.Dumper | yaml.representer.SafeRepresenter, data: Any) -> yaml.ScalarNode: # noqa: ANN401 - """Representer function that converts any data to a YAML node. - - Args: - dumper (yaml.Dumper | yaml.representer.SafeRepresenter): The YAML dumper or safe representer. - data (Any): The data to be represented. - - Returns: - yaml.Node: The YAML node representing the data. - """ - return dumper.represent_none(data) - - -def ignore_aliases(self: yaml.representer.SafeRepresenter, data: Any) -> bool: # noqa: ARG001, ANN401 - """Determine whether to ignore aliases in YAML representation. - - Args: - data: The data to check. - - Returns: - bool | None: True if aliases should be ignored, None otherwise. - """ - from torch import dtype - - if data is None: - return True - if isinstance(data, tuple) and data == (): - return True - if isinstance(data, (str, bytes, bool, int, float, dtype)): - return True - return None - - -def register_configs() -> None: - """Register custom resolvers.""" - from omegaconf import OmegaConf - - OmegaConf.register_new_resolver("as_int_tuple", as_int_tuple, replace=True) - OmegaConf.register_new_resolver("as_torch_dtype", as_torch_dtype, replace=True) - - from torch import dtype - - yaml.add_representer(dtype, dtype_representer) # For lightnig_logs - # For jsonargparse's SafeDumper - yaml.SafeDumper.add_representer(dtype, dtype_representer) - yaml.SafeDumper.add_representer(_SpecialForm, any_representer) # typing.Any for DictConfig - yaml.SafeDumper.ignore_aliases = ignore_aliases # type: ignore # noqa: PGH003 - - -register_configs() diff --git a/src/otx/core/config/data.py b/src/otx/core/config/data.py deleted file mode 100644 index e431dec5c59..00000000000 --- a/src/otx/core/config/data.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Config data type objects for data.""" -# NOTE: omegaconf would fail to parse dataclass with `from __future__ import annotations` in Python 3.8, 3.9 -# ruff: noqa: FA100 - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any, Optional - -from otx.core.types.device import DeviceType -from otx.core.types.image import ImageColorChannel -from otx.core.types.transformer_libs import TransformLibType - - -@dataclass -class SubsetConfig: - """DTO for dataset subset configuration. - - Attributes: - batch_size (int): Batch size produced. - subset_name (str): Datumaro Dataset's subset name for this subset config. - It can differ from the actual usage (e.g., 'val' for the validation subset config). - transforms (list[dict[str, Any] | Transform] | Compose): List of actually used transforms. - It accepts a list of `torchvision.transforms.v2.*` Python objects - or `torchvision.transforms.v2.Compose` for `TransformLibType.TORCHVISION`. - Otherwise, it takes a Python dictionary that fits the configuration style used in mmcv - (`TransformLibType.MMCV`, `TransformLibType.MMPRETRAIN`, ...). - transform_lib_type (TransformLibType): Transform library type used by this subset. - num_workers (int): Number of workers for the dataloader of this subset. - - Example: - ```python - train_subset_config = SubsetConfig( - batch_size=64, - subset_name="train", - transforms=v2.Compose( - [ - v2.RandomResizedCrop(size=(224, 224), antialias=True), - v2.RandomHorizontalFlip(p=0.5), - v2.ToDtype(torch.float32, scale=True), - v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), - ], - ) - transform_lib_type=TransformLibType.TORCHVISION, - num_workers=2, - ) - ``` - """ - - batch_size: int - subset_name: str - - # TODO (vinnamki): Revisit data configuration objects to support a union type in structured config # noqa: TD003 - # Omegaconf does not allow to have a union type, https://github.com/omry/omegaconf/issues/144 - transforms: list[dict[str, Any]] - - transform_lib_type: TransformLibType = TransformLibType.TORCHVISION - num_workers: int = 2 - sampler: SamplerConfig = field(default_factory=lambda: SamplerConfig()) - - -@dataclass -class TileConfig: - """DTO for tiler configuration.""" - - enable_tiler: bool = False - enable_adaptive_tiling: bool = True - tile_size: tuple[int, int] = (400, 400) - overlap: float = 0.2 - iou_threshold: float = 0.45 - max_num_instances: int = 1500 - object_tile_ratio: float = 0.03 - sampling_ratio: float = 1.0 - - -@dataclass -class VisualPromptingConfig: - """DTO for visual prompting data module configuration.""" - - use_bbox: bool = False - use_point: bool = False - - -@dataclass -class DataModuleConfig: - """DTO for data module configuration.""" - - data_format: str - data_root: str - - train_subset: SubsetConfig - val_subset: SubsetConfig - test_subset: SubsetConfig - - tile_config: TileConfig = field(default_factory=lambda: TileConfig()) - vpm_config: VisualPromptingConfig = field(default_factory=lambda: VisualPromptingConfig()) - - mem_cache_size: str = "1GB" - mem_cache_img_max_size: Optional[tuple[int, int]] = None - image_color_channel: ImageColorChannel = ImageColorChannel.RGB - stack_images: bool = True - - include_polygons: bool = False - unannotated_items_ratio: float = 0.0 - - auto_num_workers: bool = False - device: DeviceType = DeviceType.auto - - -@dataclass -class SamplerConfig: - """Configuration class for defining the sampler used in the data loading process. - - This is passed in the form of a dataclass, which is instantiated when the dataloader is created. - - [TODO]: Need to replace this with a proper Sampler class. - Currently, SamplerConfig, which belongs to the sampler of SubsetConfig, - belongs to the nested dataclass of dataclass, which is not easy to instantiate from the CLI. - So currently replace sampler with a corresponding dataclass that resembles the configuration of another object, - providing limited functionality. - """ - - class_path: str = "torch.utils.data.RandomSampler" - init_args: dict[str, Any] = field(default_factory=dict) diff --git a/src/otx/core/config/device.py b/src/otx/core/config/device.py deleted file mode 100644 index 7829ff59377..00000000000 --- a/src/otx/core/config/device.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Config data type objects for device.""" - -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass - -from otx.core.types.device import DeviceType - - -@dataclass -class DeviceConfig: - """Configuration class for the engine.""" - - accelerator: DeviceType - devices: int = 1 diff --git a/src/otx/core/config/explain.py b/src/otx/core/config/explain.py deleted file mode 100644 index dedd837a5f3..00000000000 --- a/src/otx/core/config/explain.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Config data type objects for export method.""" -from __future__ import annotations - -from dataclasses import dataclass - -from otx.core.types.explain import TargetExplainGroup - - -@dataclass -class ExplainConfig: - """Data Transfer Object (DTO) for explain configuration.""" - - target_explain_group: TargetExplainGroup = TargetExplainGroup.ALL - postprocess: bool = False diff --git a/src/otx/core/config/hpo.py b/src/otx/core/config/hpo.py deleted file mode 100644 index 87efdcda849..00000000000 --- a/src/otx/core/config/hpo.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Config objects for HPO.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Literal - -import torch - - -@dataclass -class HpoConfig: - """DTO for HPO configuration.""" - - search_space: dict[str, dict[str, Any]] | None = None - save_path: str | None = None - mode: Literal["max", "min"] = "max" - num_trials: int | None = None - num_workers: int = 1 - expected_time_ratio: int | float | None = 4 - maximum_resource: int | float | None = None - subset_ratio: float | int | None = None - min_subset_size: int = 500 - prior_hyper_parameters: dict | list[dict] | None = None - acceptable_additional_time_ratio: float | int = 1.0 - minimum_resource: int | float | None = None - reduction_factor: int = 3 - asynchronous_bracket: bool = True - asynchronous_sha: bool = torch.cuda.device_count() != 1 diff --git a/src/otx/core/data/__init__.py b/src/otx/core/data/__init__.py index ab47aac0310..bb8cb0d57c9 100644 --- a/src/otx/core/data/__init__.py +++ b/src/otx/core/data/__init__.py @@ -1,9 +1,16 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for data related objects, such as OTXDataset, OTXDataModule, and Transforms.""" - -from .factory import OTXDatasetFactory, TransformLibFactory -from .module import OTXDataModule +"""OTX Core Data.""" -__all__ = ["OTXDataModule", "OTXDatasetFactory", "TransformLibFactory"] +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. +# diff --git a/src/otx/core/data/adapter/__init__.py b/src/otx/core/data/adapter/__init__.py new file mode 100644 index 00000000000..81579505bbf --- /dev/null +++ b/src/otx/core/data/adapter/__init__.py @@ -0,0 +1,160 @@ +"""OTX Core Data Adapter.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +# pylint: disable=too-many-return-statements, too-many-arguments +import importlib +import os + +from otx.algorithms.common.configs.training_base import TrainType +from otx.api.entities.model_template import TaskType + +ADAPTERS = { + TaskType.CLASSIFICATION: { + "Incremental": { + "module_name": "classification_dataset_adapter", + "class": "ClassificationDatasetAdapter", + }, + "Selfsupervised": { + "module_name": "classification_dataset_adapter", + "class": "SelfSLClassificationDatasetAdapter", + }, + }, + TaskType.DETECTION: { + "Incremental": { + "module_name": "detection_dataset_adapter", + "class": "DetectionDatasetAdapter", + } + }, + TaskType.ROTATED_DETECTION: { + "Incremental": { + "module_name": "detection_dataset_adapter", + "class": "DetectionDatasetAdapter", + } + }, + TaskType.INSTANCE_SEGMENTATION: { + "Incremental": { + "module_name": "detection_dataset_adapter", + "class": "DetectionDatasetAdapter", + } + }, + TaskType.SEGMENTATION: { + "Incremental": { + "module_name": "segmentation_dataset_adapter", + "class": "SegmentationDatasetAdapter", + }, + "Selfsupervised": { + "module_name": "segmentation_dataset_adapter", + "class": "SelfSLSegmentationDatasetAdapter", + }, + }, + TaskType.ANOMALY_CLASSIFICATION: { + "Incremental": { + "module_name": "anomaly_dataset_adapter", + "class": "AnomalyClassificationDatasetAdapter", + } + }, + TaskType.ANOMALY_DETECTION: { + "Incremental": { + "module_name": "anomaly_dataset_adapter", + "class": "AnomalyDetectionDatasetAdapter", + } + }, + TaskType.ANOMALY_SEGMENTATION: { + "Incremental": { + "module_name": "anomaly_dataset_adapter", + "class": "AnomalySegmentationDatasetAdapter", + } + }, +} +if os.getenv("FEATURE_FLAGS_OTX_ACTION_TASKS", "0") == "1": + ADAPTERS.update( + { + TaskType.ACTION_CLASSIFICATION: { + "Incremental": { + "module_name": "action_dataset_adapter", + "class": "ActionClassificationDatasetAdapter", + } + }, + TaskType.ACTION_DETECTION: { + "Incremental": { + "module_name": "action_dataset_adapter", + "class": "ActionDetectionDatasetAdapter", + } + }, + } + ) +# TODO: update to real template +if os.getenv("FEATURE_FLAGS_OTX_VISUAL_PROMPTING_TASKS", "0") == "1": + ADAPTERS.update( + { + TaskType.VISUAL_PROMPTING: { + "Incremental": { + "module_name": "visual_prompting_dataset_adapter", + "class": "VisualPromptingDatasetAdapter", + } + }, + } + ) + + +def get_dataset_adapter( + task_type: TaskType, + train_type: TrainType, + train_data_roots: str = None, + train_ann_files: str = None, + val_data_roots: str = None, + val_ann_files: str = None, + test_data_roots: str = None, + test_ann_files: str = None, + unlabeled_data_roots: str = None, + unlabeled_file_list: str = None, + **kwargs, +): + """Returns a dataset class by task type. + + Args: + task_type: A task type such as ANOMALY_CLASSIFICATION, ANOMALY_DETECTION, ANOMALY_SEGMENTATION, + CLASSIFICATION, INSTANCE_SEGMENTATION, DETECTION, CLASSIFICATION, ROTATED_DETECTION, SEGMENTATION. + train_type: train type such as Incremental and Selfsupervised. + Selfsupervised is only supported for SEGMENTATION. + train_data_roots: the path of data root for training data + train_ann_files: the path of annotation file for training data + val_data_roots: the path of data root for validation data + val_ann_files: the path of annotation file for validation data + test_data_roots: the path of data root for test data + test_ann_files: the path of annotation file for test data + unlabeled_data_roots: the path of data root for unlabeled data + unlabeled_file_list: the path of unlabeled file list + kwargs: optional kwargs + """ + + train_type_to_be_called = str( + train_type if train_type == TrainType.Selfsupervised.value else TrainType.Incremental.value + ) + module_root = "otx.core.data.adapter." + module = importlib.import_module(module_root + ADAPTERS[task_type][train_type_to_be_called]["module_name"]) + return getattr(module, ADAPTERS[task_type][train_type_to_be_called]["class"])( + task_type=task_type, + train_data_roots=train_data_roots, + train_ann_files=train_ann_files, + val_data_roots=val_data_roots, + val_ann_files=val_ann_files, + test_data_roots=test_data_roots, + test_ann_files=test_ann_files, + unlabeled_data_roots=unlabeled_data_roots, + unlabeled_file_list=unlabeled_file_list, + **kwargs, + ) diff --git a/src/otx/core/data/adapter/action_dataset_adapter.py b/src/otx/core/data/adapter/action_dataset_adapter.py new file mode 100644 index 00000000000..34e3bbc3edc --- /dev/null +++ b/src/otx/core/data/adapter/action_dataset_adapter.py @@ -0,0 +1,198 @@ +"""Action Base / Classification / Detection Dataset Adapter.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name, too-many-locals, no-member, too-many-arguments +import os +import os.path as osp +from typing import Dict, List, Optional + +from datumaro.components.annotation import AnnotationType +from datumaro.components.annotation import Bbox as DatumBbox +from datumaro.components.dataset import Dataset as DatumDataset + +from otx.api.entities.annotation import Annotation +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.metadata import MetadataItemEntity, VideoMetadata +from otx.api.entities.subset import Subset +from otx.core.data.adapter.base_dataset_adapter import BaseDatasetAdapter + + +class ActionBaseDatasetAdapter(BaseDatasetAdapter): + """BaseDataset Adpater for Action tasks inherited by BaseDatasetAdapter.""" + + VIDEO_FRAME_SEP = "##" + EMPTY_FRAME_LABEL_NAME = "EmptyFrame" + + def _import_datasets( + self, + train_data_roots: Optional[str] = None, + train_ann_files: Optional[str] = None, + val_data_roots: Optional[str] = None, + val_ann_files: Optional[str] = None, + test_data_roots: Optional[str] = None, + test_ann_files: Optional[str] = None, + unlabeled_data_roots: Optional[str] = None, + unlabeled_file_list: Optional[str] = None, + encryption_key: Optional[str] = None, + ) -> Dict[Subset, DatumDataset]: + """Import multiple videos that have CVAT format annotation. + + Args: + train_data_roots (Optional[str]): Path for training data + train_ann_files (Optional[str]): Path for training annotation file + val_data_roots (Optional[str]): Path for validation data + val_ann_files (Optional[str]): Path for validation annotation file + test_data_roots (Optional[str]): Path for test data + test_ann_files (Optional[str]): Path for test annotation file + unlabeled_data_roots (Optional[str]): Path for unlabeled data + unlabeled_file_list (Optional[str]): Path of unlabeled file list + encryption_key (Optional[str]): Encryption key to load an encrypted dataset + (only required for DatumaroBinary format) + + Returns: + DatumDataset: Datumaro Dataset + """ + dataset = {} + if train_data_roots is None and test_data_roots is None: + raise ValueError("At least 1 data_root is needed to train/test.") + + # Construct dataset for training, validation, testing + if train_data_roots is not None: + dataset[Subset.TRAINING] = self._prepare_cvat_pair_data(train_data_roots) + if val_data_roots: + dataset[Subset.VALIDATION] = self._prepare_cvat_pair_data(val_data_roots) + self.is_train_phase = True + if test_data_roots is not None and train_data_roots is None: + dataset[Subset.TESTING] = self._prepare_cvat_pair_data(test_data_roots) + self.is_train_phase = False + + return dataset + + def _prepare_cvat_pair_data(self, path: str) -> DatumDataset: + """Preparing a list of DatumaroDataset.""" + cvat_dataset_list = [] + for video_name in os.listdir(path): + cvat_data_path = osp.join(path, video_name) + dataset = DatumDataset.import_from(cvat_data_path, "cvat") + for item in dataset: + item.id = f"{video_name}{self.VIDEO_FRAME_SEP}{item.id}" + cvat_dataset_list.append(dataset) + + dataset = DatumDataset.from_extractors(*cvat_dataset_list, merge_policy="union") + # set source path for storage cache + dataset._source_path = path + + # make sure empty frame label has the last label index + categories = [category.name for category in dataset.categories()[AnnotationType.label]] + categories.sort() + dst_labels = [ + (float("inf"), category) if category == self.EMPTY_FRAME_LABEL_NAME else (label, category) + for label, category in enumerate(categories) + ] + dst_labels.sort() + dst_labels = [name for _, name in dst_labels] + dataset.transform("project_labels", dst_labels=dst_labels) + + return dataset + + def get_otx_dataset(self) -> DatasetEntity: + """Get DatasetEntity. + + Args: + datumaro_dataset (dict): A Dictionary that includes subset dataset(DatasetEntity) + + Returns: + DatasetEntity: + """ + raise NotImplementedError() + + +class ActionClassificationDatasetAdapter(ActionBaseDatasetAdapter): + """Action classification adapter inherited by ActionBaseDatasetAdapter and BaseDatasetAdapter.""" + + def get_otx_dataset(self) -> DatasetEntity: + """Convert DatumaroDataset to DatasetEntity for Acion Classification.""" + label_information = self._prepare_label_information(self.dataset) + self.label_entities = label_information["label_entities"] + + dataset_items: List[DatasetItemEntity] = [] + for subset, subset_data in self.dataset.items(): + for _, datumaro_items in subset_data.subsets().items(): + for datumaro_item in datumaro_items: + image = self.datum_media_2_otx_media(datumaro_item.media) + assert isinstance(image, Image) + shapes: List[Annotation] = [] + for annotation in datumaro_item.annotations: + if annotation.type == AnnotationType.label: + shapes.append(self._get_label_entity(annotation)) + + video_name, frame_idx = datumaro_item.id.split(self.VIDEO_FRAME_SEP) + metadata_item = MetadataItemEntity( + data=VideoMetadata( + video_id=video_name, + frame_idx=int(frame_idx), + is_empty_frame=False, + ) + ) + + dataset_item = DatasetItemEntity( + image, self._get_ann_scene_entity(shapes), subset=subset, metadata=[metadata_item] + ) + dataset_items.append(dataset_item) + + return DatasetEntity(items=dataset_items) + + +class ActionDetectionDatasetAdapter(ActionBaseDatasetAdapter): + """Action Detection adapter inherited by ActionBaseDatasetAdapter and BaseDatasetAdapter.""" + + # pylint: disable=too-many-nested-blocks + def get_otx_dataset(self) -> DatasetEntity: + """Convert DatumaroDataset to DatasetEntity for Acion Detection.""" + label_information = self._prepare_label_information(self.dataset) + self.label_entities = label_information["label_entities"] + + # Detection use index 0 as a background category + for label_entity in self.label_entities: + label_entity.id = ID(int(label_entity.id) + 1) + + dataset_items: List[DatasetItemEntity] = [] + for subset, subset_data in self.dataset.items(): + for _, datumaro_items in subset_data.subsets().items(): + for datumaro_item in datumaro_items: + image = self.datum_media_2_otx_media(datumaro_item.media) + assert isinstance(image, Image) + shapes: List[Annotation] = [] + is_empty_frame = False + for annotation in datumaro_item.annotations: + if isinstance(annotation, DatumBbox): + if self.label_entities[annotation.label].name == self.EMPTY_FRAME_LABEL_NAME: + is_empty_frame = True + shapes.append(self._get_label_entity(annotation)) + else: + shapes.append(self._get_original_bbox_entity(annotation)) + + video_name, frame_name = datumaro_item.id.split(self.VIDEO_FRAME_SEP) + metadata_item = MetadataItemEntity( + data=VideoMetadata( + video_id=video_name, + frame_idx=int(frame_name.split("_")[-1]), + is_empty_frame=is_empty_frame, + ) + ) + dataset_item = DatasetItemEntity( + image, self._get_ann_scene_entity(shapes), subset=subset, metadata=[metadata_item] + ) + dataset_items.append(dataset_item) + + found = [i for i, entity in enumerate(self.label_entities) if entity.name == self.EMPTY_FRAME_LABEL_NAME] + if found: + self.label_entities.pop(found[0]) + + return DatasetEntity(items=dataset_items) diff --git a/src/otx/core/data/adapter/anomaly_dataset_adapter.py b/src/otx/core/data/adapter/anomaly_dataset_adapter.py new file mode 100644 index 00000000000..277d96862ee --- /dev/null +++ b/src/otx/core/data/adapter/anomaly_dataset_adapter.py @@ -0,0 +1,237 @@ +"""Anomaly Classification / Detection / Segmentation Dataset Adapter.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name, too-many-locals, no-member, too-many-arguments +import os +from typing import Dict, List, Optional + +import cv2 +import numpy as np +from datumaro.components.dataset import Dataset as DatumaroDataset + +from otx.algorithms.common.utils.mask_to_bbox import mask2bbox +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, + NullAnnotationSceneEntity, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.utils.segmentation_utils import create_annotation_from_segmentation_map +from otx.core.data.adapter.base_dataset_adapter import BaseDatasetAdapter + + +class AnomalyBaseDatasetAdapter(BaseDatasetAdapter): + """BaseDataset Adpater for Anomaly tasks inherited from BaseDatasetAdapter.""" + + def _import_datasets( + self, + train_data_roots: Optional[str] = None, + train_ann_files: Optional[str] = None, + val_data_roots: Optional[str] = None, + val_ann_files: Optional[str] = None, + test_data_roots: Optional[str] = None, + test_ann_files: Optional[str] = None, + unlabeled_data_roots: Optional[str] = None, + unlabeled_file_list: Optional[str] = None, + encryption_key: Optional[str] = None, + ) -> Dict[Subset, DatumaroDataset]: + """Import MVTec dataset. + + Args: + train_data_roots (Optional[str]): Path for training data + train_ann_files (Optional[str]): Path for training annotation file + val_data_roots (Optional[str]): Path for validation data + val_ann_files (Optional[str]): Path for validation annotation file + test_data_roots (Optional[str]): Path for test data + test_ann_files (Optional[str]): Path for test annotation file + unlabeled_data_roots (Optional[str]): Path for unlabeled data + unlabeled_file_list (Optional[str]): Path of unlabeled file list + encryption_key (Optional[str]): Encryption key to load an encrypted dataset + (only required for DatumaroBinary format) + + Returns: + DatumaroDataset: Datumaro Dataset + """ + # Construct dataset for training, validation, unlabeled + # TODO: currently, only MVTec dataset format can be used + dataset = {} + if train_data_roots is None and test_data_roots is None: + raise ValueError("At least 1 data_root is needed to train/test.") + + if train_data_roots: + dataset[Subset.TRAINING] = DatumaroDataset.import_from(train_data_roots, format="image_dir") + if val_data_roots: + dataset[Subset.VALIDATION] = DatumaroDataset.import_from(val_data_roots, format="image_dir") + else: + raise NotImplementedError("Anomaly task needs validation dataset.") + if test_data_roots: + dataset[Subset.TESTING] = DatumaroDataset.import_from(test_data_roots, format="image_dir") + return dataset + + def _prepare_anomaly_label_information(self) -> List[LabelEntity]: + """Prepare LabelEntity List.""" + normal_label = LabelEntity(id=ID(0), name="Normal", domain=self.domain) + abnormal_label = LabelEntity( + id=ID(1), + name="Anomalous", + domain=self.domain, + is_anomalous=True, + ) + return [normal_label, abnormal_label] + + def get_otx_dataset(self) -> DatasetEntity: + """Get DatasetEntity.""" + raise NotImplementedError() + + +class AnomalyClassificationDatasetAdapter(AnomalyBaseDatasetAdapter): + """Anomaly classification adapter inherited from AnomalyBaseDatasetAdapter.""" + + def get_otx_dataset(self) -> DatasetEntity: + """Convert DatumaroDataset to DatasetEntity for Anomaly classification.""" + normal_label, abnormal_label = self._prepare_anomaly_label_information() + self.label_entities = [normal_label, abnormal_label] + + # Prepare + dataset_items: List[DatasetItemEntity] = [] + for subset, subset_data in self.dataset.items(): + for _, datumaro_items in subset_data.subsets().items(): + for datumaro_item in datumaro_items: + image = Image(file_path=datumaro_item.media.path) + label = normal_label if os.path.dirname(datumaro_item.id) == "good" else abnormal_label + shapes = [ + Annotation( + Rectangle.generate_full_box(), + labels=[ScoredLabel(label=label, probability=1.0)], + ) + ] + annotation_scene: Optional[AnnotationSceneEntity] = None + # Unlabeled dataset + if len(shapes) == 0: + annotation_scene = NullAnnotationSceneEntity() + else: + annotation_scene = AnnotationSceneEntity( + kind=AnnotationSceneKind.ANNOTATION, annotations=shapes + ) + dataset_item = DatasetItemEntity(image, annotation_scene, subset=subset) + dataset_items.append(dataset_item) + + return DatasetEntity(items=dataset_items) + + +class AnomalyDetectionDatasetAdapter(AnomalyBaseDatasetAdapter): + """Anomaly detection adapter inherited from AnomalyBaseDatasetAdapter.""" + + def get_otx_dataset(self) -> DatasetEntity: + """Conver DatumaroDataset to DatasetEntity for Anomaly detection.""" + normal_label, abnormal_label = self._prepare_anomaly_label_information() + self.label_entities = [normal_label, abnormal_label] + + # Prepare + dataset_items: List[DatasetItemEntity] = [] + for subset, subset_data in self.dataset.items(): + for _, datumaro_items in subset_data.subsets().items(): + for datumaro_item in datumaro_items: + image = Image(file_path=datumaro_item.media.path) + label = normal_label if os.path.dirname(datumaro_item.id) == "good" else abnormal_label + shapes = [ + Annotation( + Rectangle.generate_full_box(), + labels=[ScoredLabel(label=label, probability=1.0)], + ) + ] + # TODO: avoid hard coding, plan to enable MVTec to Datumaro + mask_file_path = os.path.join( + "/".join(datumaro_item.media.path.split("/")[:-3]), + "ground_truth", + str(datumaro_item.id) + "_mask.png", + ) + if os.path.exists(mask_file_path): + mask = (cv2.imread(mask_file_path, cv2.IMREAD_GRAYSCALE) / 255).astype(np.uint8) + bboxes = mask2bbox(mask) + for bbox in bboxes: + x1, y1, x2, y2 = bbox + shapes.append( + Annotation( + Rectangle( + x1=x1 / image.width, + y1=y1 / image.height, + x2=x2 / image.width, + y2=y2 / image.height, + ), + labels=[ScoredLabel(label=abnormal_label)], + ) + ) + annotation_scene: Optional[AnnotationSceneEntity] = None + # Unlabeled dataset + if len(shapes) == 0: + annotation_scene = NullAnnotationSceneEntity() + else: + annotation_scene = AnnotationSceneEntity( + kind=AnnotationSceneKind.ANNOTATION, annotations=shapes + ) + dataset_item = DatasetItemEntity(image, annotation_scene, subset=subset) + dataset_items.append(dataset_item) + + return DatasetEntity(items=dataset_items) + + +class AnomalySegmentationDatasetAdapter(AnomalyBaseDatasetAdapter): + """Anomaly segmentation adapter inherited by AnomalyBaseDatasetAdapter and BaseDatasetAdapter.""" + + def get_otx_dataset(self) -> DatasetEntity: + """Conver DatumaroDataset to DatasetEntity for Anomaly segmentation.""" + normal_label, abnormal_label = self._prepare_anomaly_label_information() + self.label_entities = [normal_label, abnormal_label] + + # Prepare + dataset_items: List[DatasetItemEntity] = [] + for subset, subset_data in self.dataset.items(): + for _, datumaro_items in subset_data.subsets().items(): + for datumaro_item in datumaro_items: + image = Image(file_path=datumaro_item.media.path) + label = normal_label if os.path.dirname(datumaro_item.id) == "good" else abnormal_label + shapes = [ + Annotation( + Rectangle.generate_full_box(), + labels=[ScoredLabel(label=label, probability=1.0)], + ) + ] + # TODO: avoid hard coding, plan to enable MVTec to Datumaro + mask_file_path = os.path.join( + "/".join(datumaro_item.media.path.split("/")[:-3]), + "ground_truth", + str(datumaro_item.id) + "_mask.png", + ) + if os.path.exists(mask_file_path): + mask = (cv2.imread(mask_file_path, cv2.IMREAD_GRAYSCALE) / 255).astype(np.uint8) + shapes.extend( + create_annotation_from_segmentation_map( + hard_prediction=mask, + soft_prediction=np.ones_like(mask), + label_map={0: normal_label, 1: abnormal_label}, + ) + ) + annotation_scene: Optional[AnnotationSceneEntity] = None + # Unlabeled dataset + if len(shapes) == 0: + annotation_scene = NullAnnotationSceneEntity() + else: + annotation_scene = AnnotationSceneEntity( + kind=AnnotationSceneKind.ANNOTATION, annotations=shapes + ) + dataset_item = DatasetItemEntity(image, annotation_scene, subset=subset) + dataset_items.append(dataset_item) + + return DatasetEntity(items=dataset_items) diff --git a/src/otx/core/data/adapter/base_dataset_adapter.py b/src/otx/core/data/adapter/base_dataset_adapter.py new file mode 100644 index 00000000000..af87695bd66 --- /dev/null +++ b/src/otx/core/data/adapter/base_dataset_adapter.py @@ -0,0 +1,439 @@ +"""Base Class for Dataset Adapter.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name, too-many-locals, too-many-instance-attributes, unused-argument, too-many-arguments + +import abc +import os +from abc import abstractmethod +from copy import deepcopy +from difflib import get_close_matches +from typing import Any, Dict, List, Optional, Union + +import cv2 +import datumaro +import numpy as np +from datumaro.components.annotation import Annotation as DatumAnnotation +from datumaro.components.annotation import AnnotationType as DatumAnnotationType +from datumaro.components.annotation import Categories as DatumCategories +from datumaro.components.dataset import Dataset as DatumDataset +from datumaro.components.dataset import DatasetSubset as DatumDatasetSubset +from datumaro.components.dataset import eager_mode +from datumaro.components.media import Image as DatumImage +from datumaro.components.media import MediaElement as DatumMediaElement + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, + NullAnnotationSceneEntity, +) +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.media import IMediaEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.core.data.caching.storage_cache import init_arrow_cache + + +class BaseDatasetAdapter(metaclass=abc.ABCMeta): + """Base dataset adapter for all of downstream tasks to use Datumaro. + + Mainly, BaseDatasetAdapter detect and import the dataset by using the function implemented in Datumaro. + And it could prepare common variable, function (EmptyLabelSchema, LabelSchema, ..) commonly consumed under all tasks + + Args: + task_type [TaskType]: type of the task + train_data_roots (Optional[str]): Path for training data + train_ann_files (Optional[str]): Path for training annotation file + val_data_roots (Optional[str]): Path for validation data + val_ann_files (Optional[str]): Path for validation annotation file + test_data_roots (Optional[str]): Path for test data + test_ann_files (Optional[str]): Path for test annotation file + unlabeled_data_roots (Optional[str]): Path for unlabeled data + unlabeled_file_list (Optional[str]): Path of unlabeled file list + encryption_key (Optional[str]): Encryption key to load an encrypted dataset + (only required for DatumaroBinary format) + + Since all adapters can be used for training and validation, + the default value of train/val/test_data_roots was set to None. + + i.e) + For the training/validation phase, test_data_roots is not used. + For the test phase, train_data_roots and val_data_root are not used. + """ + + def __init__( + self, + task_type: TaskType, + train_data_roots: Optional[str] = None, + train_ann_files: Optional[str] = None, + val_data_roots: Optional[str] = None, + val_ann_files: Optional[str] = None, + test_data_roots: Optional[str] = None, + test_ann_files: Optional[str] = None, + unlabeled_data_roots: Optional[str] = None, + unlabeled_file_list: Optional[str] = None, + cache_config: Optional[Dict[str, Any]] = None, + encryption_key: Optional[str] = None, + **kwargs, + ): + self.task_type = task_type + self.domain = task_type.domain + self.data_type: str + self.is_train_phase: bool + + self.dataset = self._import_datasets( + train_data_roots=train_data_roots, + train_ann_files=train_ann_files, + val_data_roots=val_data_roots, + val_ann_files=val_ann_files, + test_data_roots=test_data_roots, + test_ann_files=test_ann_files, + unlabeled_data_roots=unlabeled_data_roots, + unlabeled_file_list=unlabeled_file_list, + encryption_key=encryption_key, + **kwargs, + ) + + cache_config = cache_config if cache_config is not None else {} + for subset, dataset in self.dataset.items(): + # cache these subsets only + if subset in (Subset.TRAINING, Subset.VALIDATION, Subset.UNLABELED, Subset.PSEUDOLABELED): + self.dataset[subset] = init_arrow_cache(dataset, **cache_config) + + self.category_items: List[DatumCategories] + self.label_groups: List[str] + self.label_entities: List[LabelEntity] + self.label_schema: LabelSchemaEntity + + def _import_datasets( + self, + train_data_roots: Optional[str] = None, + train_ann_files: Optional[str] = None, + val_data_roots: Optional[str] = None, + val_ann_files: Optional[str] = None, + test_data_roots: Optional[str] = None, + test_ann_files: Optional[str] = None, + unlabeled_data_roots: Optional[str] = None, + unlabeled_file_list: Optional[str] = None, + encryption_key: Optional[str] = None, + ) -> Dict[Subset, DatumDataset]: + """Import datasets by using Datumaro.import_from() method. + + Args: + train_data_roots (Optional[str]): Path for training data + train_ann_files (Optional[str]): Path for training annotation files + val_data_roots (Optional[str]): Path for validation data + val_ann_files (Optional[str]): Path for validation annotation files + test_data_roots (Optional[str]): Path for test data + test_ann_files (Optional[str]): Path for test annotation files + unlabeled_data_roots (Optional[str]): Path for unlabeled data + unlabeled_file_list (Optional[str]): Path for unlabeled file list + encryption_key (Optional[str]): Encryption key to load an encrypted dataset + (DatumaroBinary format) + + Returns: + DatumDataset: Datumaro Dataset + """ + dataset = {} + if train_data_roots is None and test_data_roots is None: + raise ValueError("At least 1 data_root is needed to train/test.") + + # Construct dataset for training, validation, testing, unlabeled + if train_data_roots is not None: + train_dataset = self._import_dataset(train_data_roots, train_ann_files, encryption_key, Subset.TRAINING) + dataset[Subset.TRAINING] = self._get_subset_data("train", train_dataset) + self.is_train_phase = True + + # If validation is manually defined --> set the validation data according to user's input + if val_data_roots: + val_dataset = self._import_dataset(val_data_roots, val_ann_files, encryption_key, Subset.VALIDATION) + dataset[Subset.VALIDATION] = self._get_subset_data("val", val_dataset) + elif "val" in train_dataset.subsets(): + dataset[Subset.VALIDATION] = self._get_subset_data("val", train_dataset) + + if test_data_roots is not None and train_data_roots is None: + test_dataset = self._import_dataset(test_data_roots, test_ann_files, encryption_key, Subset.TESTING) + dataset[Subset.TESTING] = self._get_subset_data("test", test_dataset) + self.is_train_phase = False + + if unlabeled_data_roots is not None: + dataset[Subset.UNLABELED] = DatumDataset.import_from(unlabeled_data_roots, format="image_dir") + if unlabeled_file_list is not None: + self._filter_unlabeled_data(dataset[Subset.UNLABELED], unlabeled_file_list) + return dataset + + def _import_dataset(self, data_roots: str, ann_files: str, encryption_key: Optional[str], mode: Subset): + # Find self.data_type and task_type + mode_to_str = {Subset.TRAINING: "train", Subset.VALIDATION: "val", Subset.TESTING: "test"} + str_mode = mode_to_str[mode] + + self.data_type_candidates = self._detect_dataset_format(path=data_roots) + self.data_type = self._select_data_type(self.data_type_candidates) + + dataset_kwargs = {"path": data_roots, "format": self.data_type} + if ann_files is not None: + if self.data_type not in ("coco"): + raise NotImplementedError( + f"Specifying '--{str_mode}-ann-files' is not supported for data type '{self.data_type}'" + ) + dataset_kwargs["path"] = ann_files + dataset_kwargs["subset"] = str_mode + + if encryption_key is not None: + dataset_kwargs["encryption_key"] = encryption_key + + if self.task_type == TaskType.VISUAL_PROMPTING: + if self.data_type in ["coco"]: + dataset_kwargs["merge_instance_polygons"] = self.use_mask # type: ignore[attr-defined] + + dataset = DatumDataset.import_from(**dataset_kwargs) + + return dataset + + @abstractmethod + def get_otx_dataset(self) -> DatasetEntity: + """Get DatasetEntity.""" + raise NotImplementedError + + def get_label_schema(self) -> LabelSchemaEntity: + """Get Label Schema.""" + return self._generate_default_label_schema(self.label_entities) + + def _get_subset_data(self, subset: str, dataset: DatumDataset) -> DatumDatasetSubset: + """Get subset dataset according to subset.""" + with eager_mode(True, dataset): + subsets = list(dataset.subsets().keys()) + + for s in [subset, "default"]: + if subset == "val" and s != "default": + s = "valid" + exact_subset = get_close_matches(s, subsets, cutoff=0.5) + + if exact_subset: + return dataset.subsets()[exact_subset[0]].as_dataset() + elif subset == "test": + # If there is not test dataset in data.yml, then validation set will be test dataset + s = "valid" + exact_subset = get_close_matches(s, subsets, cutoff=0.5) + if exact_subset: + return dataset.subsets()[exact_subset[0]].as_dataset() + + raise ValueError("Can't find proper dataset.") + + def _detect_dataset_format(self, path: str) -> str: + """Detect dataset format (ImageNet, COCO, ...).""" + return datumaro.Environment().detect_dataset(path=path) + + def _generate_empty_label_entity(self) -> LabelGroup: + """Generate Empty Label Group for H-label, Multi-label Classification.""" + empty_label = LabelEntity(name="Empty label", is_empty=True, domain=self.domain) + empty_group = LabelGroup(name="empty", labels=[empty_label], group_type=LabelGroupType.EMPTY_LABEL) + return empty_group + + def _generate_default_label_schema(self, label_entities: List[LabelEntity]) -> LabelSchemaEntity: + """Generate Default Label Schema for Multi-class Classification, Detecion, Etc.""" + label_schema = LabelSchemaEntity() + main_group = LabelGroup( + name="labels", + labels=label_entities, + group_type=LabelGroupType.EXCLUSIVE, + ) + label_schema.add_group(main_group) + return label_schema + + def _prepare_label_information( + self, + datumaro_dataset: Dict[Subset, DatumDataset], + ) -> Dict[str, Any]: + # Get datumaro category information + if self.is_train_phase: + label_categories_list = datumaro_dataset[Subset.TRAINING].categories().get(DatumAnnotationType.label, None) + else: + label_categories_list = datumaro_dataset[Subset.TESTING].categories().get(DatumAnnotationType.label, None) + category_items = label_categories_list.items + label_groups = label_categories_list.label_groups + + # LabelEntities + label_entities = [ + LabelEntity(name=class_name.name, domain=self.domain, is_empty=False, id=ID(i)) + for i, class_name in enumerate(category_items) + ] + + return {"category_items": category_items, "label_groups": label_groups, "label_entities": label_entities} + + def _is_normal_polygon(self, annotation: DatumAnnotationType.polygon, width: int, height: int) -> bool: + """To filter out the abnormal polygon.""" + x_points = annotation.points[::2] # Extract x-coordinates + y_points = annotation.points[1::2] # Extract y-coordinates + + return ( + min(x_points) < max(x_points) < width + and min(y_points) < max(y_points) < height + and annotation.get_area() > 0 + ) + + def _is_normal_bbox(self, x1: float, y1: float, x2: float, y2: float) -> bool: + """To filter out the abrnormal bbox.""" + return x1 < x2 and y1 < y2 + + def _select_data_type(self, data_candidates: Union[List[str], str]) -> str: + """Select specific type among candidates. + + Args: + data_candidates (Union[List[str], str]): Type candidates made by Datumaro.Environment().detect_dataset() + + Returns: + str: Selected data type + """ + return data_candidates[0] + + def _get_ann_scene_entity(self, shapes: List[Annotation]) -> AnnotationSceneEntity: + annotation_scene: Optional[AnnotationSceneEntity] = None + if len(shapes) == 0: + annotation_scene = NullAnnotationSceneEntity() + else: + annotation_scene = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=shapes) + return annotation_scene + + def _get_label_entity(self, annotation: DatumAnnotation) -> Annotation: + """Get label entity.""" + return Annotation( + Rectangle.generate_full_box(), labels=[ScoredLabel(label=self.label_entities[annotation.label])] + ) + + def _get_normalized_bbox_entity(self, annotation: DatumAnnotation, width: int, height: int) -> Annotation: + """Get bbox entity w/ normalization.""" + x1, y1, x2, y2 = annotation.points + return Annotation( + Rectangle( + x1=x1 / width, + y1=y1 / height, + x2=x2 / width, + y2=y2 / height, + ), + labels=[ScoredLabel(label=self.label_entities[annotation.label])], + ) + + def _get_original_bbox_entity(self, annotation: DatumAnnotation) -> Annotation: + """Get bbox entity w/o normalization.""" + return Annotation( + Rectangle( + x1=annotation.points[0], + y1=annotation.points[1], + x2=annotation.points[2], + y2=annotation.points[3], + ), + labels=[ScoredLabel(label=self.label_entities[annotation.label])], + ) + + def _get_polygon_entity( + self, annotation: DatumAnnotation, width: int, height: int, num_polygons: int = -1 + ) -> Annotation: + """Get polygon entity.""" + polygon = Polygon( + points=[ + Point(x=annotation.points[i] / width, y=annotation.points[i + 1] / height) + for i in range(0, len(annotation.points), 2) + ] + ) + step = 1 if num_polygons == -1 else len(polygon.points) // num_polygons + points = [polygon.points[i] for i in range(0, len(polygon.points), step)] + + return Annotation( + Polygon(points), + labels=[ScoredLabel(label=self.label_entities[annotation.label])], + ) + + def _get_ellipse_entity( + self, annotation: DatumAnnotation, width: int, height: int, num_polygons: int = -1 + ) -> Annotation: + """Get ellipse entity.""" + ellipse = Ellipse( + annotation.x1 / (width - 1), + annotation.y1 / (height - 1), + annotation.x2 / (width - 1), + annotation.y2 / (height - 1), + ) + return Annotation( + ellipse, + labels=[ScoredLabel(label=self.label_entities[annotation.label])], + ) + + def _get_mask_entity(self, annotation: DatumAnnotation) -> Annotation: + """Get mask entity.""" + mask = Image(data=annotation.image, size=annotation.image.shape) + return Annotation( + mask, labels=[ScoredLabel(label=self.label_entities[annotation.label])] # type: ignore[arg-type] + ) + + def remove_unused_label_entities(self, used_labels: List): + """Remove unused label from label entities. + + Because label entities will be used to make Label Schema, + If there is unused label in Label Schema, it will hurts the model performance. + So, remove the unused label from label entities. + + Args: + used_labels (List): list for index of used label + """ + clean_label_entities = [] + + for used_label in used_labels: + clean_label_entities.append(self.label_entities[used_label]) + self.label_entities = clean_label_entities + + def _filter_unlabeled_data(self, unlabeled_dataset: DatumDataset, unlabeled_file_list: str): + """Filter out unlabeled dataset which isn't included in unlabeled file list.""" + allowed_extensions = ["jpg", "png", "jpeg"] + file_list = [] + with open(unlabeled_file_list, "r", encoding="utf-8") as f: + for line in f.readlines(): + file_ext = line.rstrip().split(".")[-1] + file_list.append(line.split(".")[0]) + + if file_ext.lower() not in allowed_extensions: + raise ValueError(f"{file_ext} is not supported type for unlabeled data.") + + copy_dataset = deepcopy(unlabeled_dataset) + for item in copy_dataset: + if item.id not in file_list: + unlabeled_dataset.remove(item.id, item.subset) + + @staticmethod + def datum_media_2_otx_media(datumaro_media: DatumMediaElement) -> IMediaEntity: + """Convert Datumaro media to OTX media.""" + if isinstance(datumaro_media, DatumImage): + path = getattr(datumaro_media, "path", None) + size = datumaro_media._size # pylint: disable=protected-access + + if path and os.path.exists(path) and not datumaro_media.is_encrypted: + return Image(file_path=path, size=size) + + def helper(): + data = datumaro_media.data # pylint: disable=protected-access + # OTX expects unint8 data type + data = data.astype(np.uint8) + # OTX expects RGB format + if len(data.shape) == 2: + return cv2.cvtColor(data, cv2.COLOR_GRAY2RGB) + if len(data.shape) == 3: + if data.shape[-1] == 3: + return cv2.cvtColor(data, cv2.COLOR_BGR2RGB) + if data.shape[-1] == 4: + return cv2.cvtColor(data, cv2.COLOR_BGRA2RGB) + raise NotImplementedError + + return Image(data=helper, size=size) + raise NotImplementedError diff --git a/src/otx/core/data/adapter/classification_dataset_adapter.py b/src/otx/core/data/adapter/classification_dataset_adapter.py new file mode 100644 index 00000000000..5adfa8b5cd2 --- /dev/null +++ b/src/otx/core/data/adapter/classification_dataset_adapter.py @@ -0,0 +1,157 @@ +"""Classification Dataset Adapter.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name, too-many-locals, no-member +from typing import List, Union + +from datumaro.components.annotation import AnnotationType as DatumAnnotationType +from datumaro.components.annotation import LabelCategories as DatumLabelCategories + +from otx.api.entities.annotation import Annotation +from otx.api.entities.dataset_item import DatasetItemEntityWithID +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.core.data.adapter.base_dataset_adapter import BaseDatasetAdapter + + +class ClassificationDatasetAdapter(BaseDatasetAdapter): + """Classification adapter inherited from BaseDatasetAdapter. + + It converts DatumaroDataset -> DatasetEntity + for multi-class, multi-label, and hierarchical-label classification tasks + """ + + def _get_dataset_items(self, fake_ann=False): + # Set the DatasetItemEntityWithID + dataset_items: List[DatasetItemEntityWithID] = [] + for subset, subset_data in self.dataset.items(): + for _, datumaro_items in subset_data.subsets().items(): + for datumaro_item in datumaro_items: + image = self.datum_media_2_otx_media(datumaro_item.media) + assert isinstance(image, Image) + if not fake_ann: + datumaro_labels = [] + for ann in datumaro_item.annotations: + if ann.type == DatumAnnotationType.label: + datumaro_labels.append(ann.label) + else: + datumaro_labels = [0] # fake label + + shapes = self._get_cls_shapes(datumaro_labels) + dataset_item = DatasetItemEntityWithID( + image, self._get_ann_scene_entity(shapes), subset=subset, id_=datumaro_item.id + ) + + dataset_items.append(dataset_item) + return dataset_items + + def get_otx_dataset(self) -> DatasetEntity: + """Convert DatumaroDataset to DatasetEntity for Classification.""" + # Prepare label information + label_information = self._prepare_label_information(self.dataset) + self.category_items = label_information["category_items"] + self.label_groups = label_information["label_groups"] + self.label_entities = label_information["label_entities"] + dataset_items = self._get_dataset_items() + return DatasetEntity(items=dataset_items) + + def _get_cls_shapes(self, datumaro_labels: List[int]) -> List[Annotation]: + """Converts a list of datumaro labels to Annotation object.""" + otx_labels = [] + for d_label in datumaro_labels: + otx_labels.append(ScoredLabel(label=self.label_entities[d_label], probability=1.0)) + + return [Annotation(Rectangle.generate_full_box(), labels=otx_labels)] + + def get_label_schema(self) -> LabelSchemaEntity: + """Get Label Schema.""" + return self._generate_classification_label_schema( + self.category_items, + self.label_groups, + self.label_entities, + ) + + def _generate_classification_label_schema( + self, + category_items: List[DatumLabelCategories.Category], + label_groups: List[DatumLabelCategories.LabelGroup], + label_entities: List[LabelEntity], + ) -> LabelSchemaEntity: + """Generate LabelSchema for Classification.""" + label_schema = LabelSchemaEntity() + + # construct label group + if len(label_groups) > 0: + for label_group in label_groups: + group_label_entity_list = [] + for label in label_group.labels: + label_entity = [le for le in label_entities if le.name == label] + group_label_entity_list.append(label_entity[0]) + + label_schema.add_group( + LabelGroup( + name=label_group.name, labels=group_label_entity_list, group_type=LabelGroupType.EXCLUSIVE + ) + ) + label_schema.add_group(self._generate_empty_label_entity()) + else: + label_schema = self._generate_default_label_schema(label_entities) + + # construct label tree + for category_item in category_items: + me = [i for i in label_entities if i.name == category_item.name] + parent = [i for i in label_entities if i.name == category_item.parent] + if len(me) != 1: + raise ValueError( + f"Label name must be unique but {len(me)} labels found for label name '{category_item.name}'." + ) + if len(parent) == 0: + label_schema.label_tree.add_node(me[0]) + elif len(parent) == 1: + label_schema.add_child(parent[0], me[0]) + else: + raise ValueError( + f"Label name must be unique but {len(parent)} labels found for label name '{category_item.parent}'." + ) + + return label_schema + + def _select_data_type(self, data_candidates: Union[list, str]) -> str: + return "imagenet" if "imagenet" in data_candidates else data_candidates[0] + + +class SelfSLClassificationDatasetAdapter(ClassificationDatasetAdapter): + """SelfSLClassification adapter inherited from ClassificationDatasetAdapter. + + It creates fake annotations to work with DatumaroDataset w/o labels + and converts it to DatasetEntity for Self-SL classification pretraining + """ + + def get_otx_dataset(self) -> DatasetEntity: + """Convert DatumaroDataset to DatasetEntity for Self-SL Classification.""" + # Prepare label information + if not self.dataset[Subset.TRAINING].categories(): + label_information = self._prepare_fake_label_information() + self.category_items = label_information["category_items"] + self.label_groups = label_information["label_groups"] + self.label_entities = label_information["label_entities"] + dataset_items = self._get_dataset_items(fake_ann=True) + return DatasetEntity(items=dataset_items) + return super().get_otx_dataset() + + def _prepare_fake_label_information(self): + label_categories_list = DatumLabelCategories.from_iterable(["fake_label"]) + category_items = label_categories_list.items + label_groups = label_categories_list.label_groups + # LabelEntities + label_entities = [LabelEntity(name="fake_label", domain=self.domain, is_empty=False, id=ID(0))] + return {"category_items": category_items, "label_groups": label_groups, "label_entities": label_entities} diff --git a/src/otx/core/data/adapter/detection_dataset_adapter.py b/src/otx/core/data/adapter/detection_dataset_adapter.py new file mode 100644 index 00000000000..963b7dafd73 --- /dev/null +++ b/src/otx/core/data/adapter/detection_dataset_adapter.py @@ -0,0 +1,70 @@ +"""Detection Dataset Adapter.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name, too-many-locals, no-member, too-many-nested-blocks +from typing import List + +from datumaro.components.annotation import AnnotationType as DatumAnnotationType + +from otx.api.entities.dataset_item import DatasetItemEntityWithID +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.model_template import TaskType +from otx.api.entities.subset import Subset +from otx.core.data.adapter.base_dataset_adapter import BaseDatasetAdapter + + +class DetectionDatasetAdapter(BaseDatasetAdapter): + """Detection adapter inherited from BaseDatasetAdapter. + + It converts DatumaroDataset --> DatasetEntity for object detection, and instance segmentation tasks + """ + + def get_otx_dataset(self) -> DatasetEntity: + """Convert DatumaroDataset to DatasetEntity for Detection.""" + # Prepare label information + label_information = self._prepare_label_information(self.dataset) + self.label_entities = label_information["label_entities"] + dataset_items: List[DatasetItemEntityWithID] = [] + used_labels: List[int] = [] + for subset, subset_data in self.dataset.items(): + for _, datumaro_items in subset_data.subsets().items(): + for datumaro_item in datumaro_items: + image = self.datum_media_2_otx_media(datumaro_item.media) + assert isinstance(image, Image) + shapes = [] + for ann in datumaro_item.annotations: + if ( + self.task_type in (TaskType.INSTANCE_SEGMENTATION, TaskType.ROTATED_DETECTION) + and ann.type == DatumAnnotationType.polygon + ): + if self._is_normal_polygon(ann, image.width, image.height): + shapes.append(self._get_polygon_entity(ann, image.width, image.height)) + elif ann.type == DatumAnnotationType.ellipse: + shapes.append(self._get_ellipse_entity(ann, image.width, image.height)) + elif self.task_type is TaskType.DETECTION: + if ann.type == DatumAnnotationType.bbox and self._is_normal_bbox( + ann.points[0], ann.points[1], ann.points[2], ann.points[3] + ): + shapes.append(self._get_normalized_bbox_entity(ann, image.width, image.height)) + + if ann.label not in used_labels: + used_labels.append(ann.label) + + if ( + len(shapes) > 0 + or subset == Subset.UNLABELED + or (subset != Subset.TRAINING and len(datumaro_item.annotations) == 0) + ): + dataset_item = DatasetItemEntityWithID( + image, + self._get_ann_scene_entity(shapes), + subset=subset, + id_=datumaro_item.id, + ) + dataset_items.append(dataset_item) + self.remove_unused_label_entities(used_labels) + return DatasetEntity(items=dataset_items) diff --git a/src/otx/core/data/adapter/segmentation_dataset_adapter.py b/src/otx/core/data/adapter/segmentation_dataset_adapter.py new file mode 100644 index 00000000000..2ccca25e24c --- /dev/null +++ b/src/otx/core/data/adapter/segmentation_dataset_adapter.py @@ -0,0 +1,241 @@ +"""Segmentation Dataset Adapter.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import json +import os +from pathlib import Path +from typing import Dict, List, Optional + +import cv2 +import numpy as np +import tqdm +from datumaro.components.annotation import AnnotationType as DatumAnnotationType +from datumaro.components.annotation import Mask +from datumaro.components.dataset import Dataset as DatumDataset +from datumaro.plugins.data_formats.common_semantic_segmentation import ( + CommonSemanticSegmentationBase, + make_categories, +) +from datumaro.plugins.transforms import MasksToPolygons +from datumaro.util.meta_file_util import parse_meta_file +from skimage.segmentation import felzenszwalb + +from otx.api.entities.annotation import Annotation +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.subset import Subset +from otx.core.data.adapter.base_dataset_adapter import BaseDatasetAdapter +from otx.utils.logger import get_logger + +# pylint: disable=invalid-name, too-many-locals, no-member, too-many-nested-blocks, too-many-branches, +# pylint: too-many-arguments + + +class SegmentationDatasetAdapter(BaseDatasetAdapter): + """Segmentation adapter inherited from BaseDatasetAdapter. + + It converts DatumaroDataset --> DatasetEntity for semantic segmentation task + """ + + def get_otx_dataset(self) -> DatasetEntity: + """Convert DatumaroDataset to DatasetEntity for Segmentation.""" + # Prepare label information + label_information = self._prepare_label_information(self.dataset) + self.label_entities = label_information["label_entities"] + + dataset_items: List[DatasetItemEntity] = [] + used_labels: List[int] = [] + self.updated_label_id: Dict[int, int] = {} + + if hasattr(self, "data_type_candidates"): + if "voc" in self.data_type_candidates[0]: + self.set_voc_labels() + elif self.data_type_candidates[0] == "common_semantic_segmentation": + self.set_common_labels() + + else: + # For datasets used for self-sl. + # They are not included in any data type and `data_type_candidates` is not set, + # so they must be handled independently. But, setting `self.updated_label_id` is compatible + # with "common_semantic_segmentation", so we can use it. + self.set_common_labels() + + for subset, subset_data in self.dataset.items(): + for _, datumaro_items in subset_data.subsets().items(): + for datumaro_item in datumaro_items: + image = self.datum_media_2_otx_media(datumaro_item.media) + assert isinstance(image, Image) + shapes: List[Annotation] = [] + for ann in datumaro_item.annotations: + if ann.type == DatumAnnotationType.mask: + # TODO: consider case -> didn't include the background information + datumaro_polygons = MasksToPolygons.convert_mask(ann) + for d_polygon in datumaro_polygons: + new_label = self.updated_label_id.get(d_polygon.label, None) + if new_label is not None: + d_polygon.label = new_label + else: + continue + + shapes.append(self._get_polygon_entity(d_polygon, image.width, image.height)) + if d_polygon.label not in used_labels: + used_labels.append(d_polygon.label) + + if len(shapes) > 0 or subset == Subset.UNLABELED: + dataset_item = DatasetItemEntity(image, self._get_ann_scene_entity(shapes), subset=subset) + dataset_items.append(dataset_item) + + self.remove_unused_label_entities(used_labels) + return DatasetEntity(items=dataset_items) + + def set_voc_labels(self): + """Set labels for common_semantic_segmentation dataset.""" + # Remove background & ignored label in VOC from datumaro + self._remove_labels(["background", "ignored"]) + + def set_common_labels(self): + """Set labels for common_semantic_segmentation dataset.""" + # Remove background if in label_entities + is_removed = self._remove_labels(["background"]) + + # Shift label id since datumaro always extracts bg polygon with label 0 + if is_removed is False: + self.updated_label_id = {k + 1: v for k, v in self.updated_label_id.items()} + + def _remove_labels(self, label_names: List): + """Remove background label in label entity set.""" + is_removed = False + new_label_entities = [] + for i, entity in enumerate(self.label_entities): + if entity.name not in label_names: + new_label_entities.append(entity) + else: + is_removed = True + + self.label_entities = new_label_entities + + for i, entity in enumerate(self.label_entities): + self.updated_label_id[int(entity.id)] = i + entity.id = ID(i) + + return is_removed + + +class SelfSLSegmentationDatasetAdapter(SegmentationDatasetAdapter): + """Self-SL for segmentation adapter inherited from SegmentationDatasetAdapter.""" + + # pylint: disable=protected-access + def _import_datasets( + self, + train_data_roots: Optional[str] = None, + train_ann_files: Optional[str] = None, + val_data_roots: Optional[str] = None, + val_ann_files: Optional[str] = None, + test_data_roots: Optional[str] = None, + test_ann_files: Optional[str] = None, + unlabeled_data_roots: Optional[str] = None, + unlabeled_file_list: Optional[str] = None, + encryption_key: Optional[str] = None, + pseudo_mask_dir: Path = None, + ) -> Dict[Subset, DatumDataset]: + """Import custom Self-SL dataset for using DetCon. + + Self-SL for semantic segmentation using DetCon uses pseudo masks as labels, + but Datumaro cannot load this custom data structure because it is not in Datumaro format. + So, it is required to manually load and set annotations. + + Args: + train_data_roots (Optional[str]): Path for training data. + train_ann_files (Optional[str]): Path for training annotation file + val_data_roots (Optional[str]): Path for validation data + val_ann_files (Optional[str]): Path for validation annotation file + test_data_roots (Optional[str]): Path for test data. + test_ann_files (Optional[str]): Path for test annotation file + unlabeled_data_roots (Optional[str]): Path for unlabeled data. + unlabeled_file_list (Optional[str]): Path of unlabeled file list + encryption_key (Optional[str]): Encryption key to load an encrypted dataset + (only required for DatumaroBinary format) + pseudo_mask_dir (Path): Directory to save pseudo masks. Defaults to None. + + Returns: + DatumaroDataset: Datumaro Dataset + """ + if pseudo_mask_dir is None: + raise ValueError("pseudo_mask_dir must be set.") + if train_data_roots is None: + raise ValueError("train_data_root must be set.") + + logger = get_logger() + logger.warning(f"Please check if {train_data_roots} is data roots only for images, not annotations.") + dataset = {} + dataset[Subset.TRAINING] = DatumDataset.import_from(train_data_roots, format="image_dir") + self.is_train_phase = True + + # Load pseudo masks + total_labels = [] + os.makedirs(pseudo_mask_dir, exist_ok=True) + print("[*] Generating pseudo masks for DetCon algorithm. It can take some time...") + for item in tqdm.tqdm(dataset[Subset.TRAINING]): + img_path = item.media.path + pseudo_mask_path = pseudo_mask_dir / os.path.basename(img_path) + if pseudo_mask_path.suffix == ".jpg": + pseudo_mask_path = pseudo_mask_path.with_name(f"{pseudo_mask_path.stem}.png") + + if not os.path.isfile(pseudo_mask_path): + # Create pseudo mask + pseudo_mask = self.create_pseudo_masks(item.media.data, str(pseudo_mask_path)) # type: ignore + else: + # Load created pseudo mask + pseudo_mask = cv2.imread(str(pseudo_mask_path), cv2.IMREAD_GRAYSCALE) + + # Set annotations into each item + annotations = [] + labels = np.unique(pseudo_mask) + for label_id in labels: + if label_id not in total_labels: + # Stack label_id to save dataset_meta.json + total_labels.append(label_id) + annotations.append( + Mask(image=CommonSemanticSegmentationBase._lazy_extract_mask(pseudo_mask, label_id), label=label_id) + ) + item.annotations = annotations + + if not os.path.isfile(os.path.join(pseudo_mask_dir, "dataset_meta.json")): + # Save dataset_meta.json for newly created pseudo masks + # FIXME: Because background class is ignored when generating polygons, meta is set with len(labels)-1. + # It must be considered to set the whole labels later. + # (-> {i: f"target{i+1}" for i in range(max(total_labels)+1)}) + meta = {"label_map": {i + 1: f"target{i+1}" for i in range(max(total_labels))}} + with open(os.path.join(pseudo_mask_dir, "dataset_meta.json"), "w", encoding="UTF-8") as f: + json.dump(meta, f, indent=4) + + # Make categories for pseudo masks + label_map = parse_meta_file(os.path.join(pseudo_mask_dir, "dataset_meta.json")) + dataset[Subset.TRAINING].define_categories(make_categories(label_map)) + + return dataset + + def create_pseudo_masks(self, img: np.ndarray, pseudo_mask_path: str, mode: str = "FH") -> None: + """Create pseudo masks for self-sl for semantic segmentation using DetCon. + + Args: + img (np.ndarray) : A sample to create a pseudo mask. + pseudo_mask_path (Path): The path to save a pseudo mask. + mode (str): The mode to create a pseudo mask. Defaults to "FH". + + Returns: + np.array: a created pseudo mask for item. + """ + if mode == "FH": + pseudo_mask = felzenszwalb(img, scale=1000, min_size=1000) + else: + raise ValueError((f'{mode} is not supported to create pseudo masks for DetCon. Choose one of ["FH"].')) + + cv2.imwrite(pseudo_mask_path, pseudo_mask.astype(np.uint8)) + + return pseudo_mask diff --git a/src/otx/core/data/adapter/visual_prompting_dataset_adapter.py b/src/otx/core/data/adapter/visual_prompting_dataset_adapter.py new file mode 100644 index 00000000000..7a5c235f792 --- /dev/null +++ b/src/otx/core/data/adapter/visual_prompting_dataset_adapter.py @@ -0,0 +1,89 @@ +"""Visual Prompting Dataset Adapter.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name, too-many-locals, no-member, too-many-nested-blocks +from typing import Dict, List + +from datumaro.components.annotation import AnnotationType as DatumAnnotationType +from datumaro.plugins.transforms import MasksToPolygons + +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.core.data.adapter.segmentation_dataset_adapter import SegmentationDatasetAdapter + + +class VisualPromptingDatasetAdapter(SegmentationDatasetAdapter): + """Visual prompting adapter inherited from SegmentationDatasetAdapter. + + It converts DatumaroDataset --> DatasetEntity for visual prompting tasks. + To handle masks, this adapter is inherited from SegmentationDatasetAdapter. + """ + + def __init__(self, use_mask: bool = False, *args, **kwargs): + self.use_mask = use_mask + super().__init__(*args, **kwargs) + + def get_otx_dataset(self) -> DatasetEntity: + """Convert DatumaroDataset to DatasetEntity for Visual Prompting.""" + # Prepare label information + label_information = self._prepare_label_information(self.dataset) + self.label_entities = label_information["label_entities"] + + dataset_items: List[DatasetItemEntity] = [] + used_labels: List[int] = [] + self.updated_label_id: Dict[int, int] = {} + + if hasattr(self, "data_type_candidates"): + if self.data_type == "voc": + self.set_voc_labels() + + if self.data_type == "common_semantic_segmentation": + self.set_common_labels() + + for subset, subset_data in self.dataset.items(): + for _, datumaro_items in subset_data.subsets().items(): + for datumaro_item in datumaro_items: + image = self.datum_media_2_otx_media(datumaro_item.media) + assert isinstance(image, Image) + shapes = [] + for ann in datumaro_item.annotations: + if ann.type == DatumAnnotationType.polygon: + # save polygons as-is, they will be converted to masks. + if self._is_normal_polygon(ann, image.width, image.height): + shapes.append(self._get_polygon_entity(ann, image.width, image.height)) + + if ann.type == DatumAnnotationType.mask: + if self.use_mask: + # use masks loaded in datumaro as-is + if self.data_type == "common_semantic_segmentation": + if (new_label := self.updated_label_id.get(ann.label, None)) is not None: + ann.label = new_label + else: + continue + shapes.append(self._get_mask_entity(ann)) + + else: + # convert masks to polygons, they will be converted to masks again + datumaro_polygons = MasksToPolygons.convert_mask(ann) + for d_polygon in datumaro_polygons: + if (new_label := self.updated_label_id.get(d_polygon.label, None)) is not None: + d_polygon.label = new_label + else: + continue + + shapes.append(self._get_polygon_entity(d_polygon, image.width, image.height)) + if d_polygon.label not in used_labels: + used_labels.append(d_polygon.label) + + if ann.label not in used_labels and ann.type != DatumAnnotationType.mask: + used_labels.append(ann.label) + + if len(shapes) > 0: + dataset_item = DatasetItemEntity(image, self._get_ann_scene_entity(shapes), subset=subset) + dataset_items.append(dataset_item) + self.remove_unused_label_entities(used_labels) + return DatasetEntity(items=dataset_items) diff --git a/src/otx/core/data/caching/__init__.py b/src/otx/core/data/caching/__init__.py new file mode 100644 index 00000000000..8834e5764a8 --- /dev/null +++ b/src/otx/core/data/caching/__init__.py @@ -0,0 +1,9 @@ +"""Module for data caching.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .mem_cache_handler import MemCacheHandlerError, MemCacheHandlerSingleton +from .storage_cache import init_arrow_cache + +__all__ = ["MemCacheHandlerSingleton", "MemCacheHandlerError", "init_arrow_cache"] diff --git a/src/otx/core/data/caching/mem_cache_handler.py b/src/otx/core/data/caching/mem_cache_handler.py new file mode 100644 index 00000000000..02138b7b481 --- /dev/null +++ b/src/otx/core/data/caching/mem_cache_handler.py @@ -0,0 +1,223 @@ +"""Memory cache handler implementations and singleton class to call them.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import ctypes as ct +import multiprocessing as mp +from multiprocessing.managers import DictProxy +from typing import Any, Dict, Optional, Tuple, Union + +import numpy as np +import psutil +from multiprocess.synchronize import Lock + +from otx.utils.logger import get_logger + +logger = get_logger() +GIB = 1024**3 + + +class _DummyLock: + def __enter__(self, *args, **kwargs): + pass + + def __exit__(self, *args, **kwargs): + pass + + +class MemCacheHandlerBase: + """Base class for memory cache handler. + + It will be combined with LoadImageFromOTXDataset to store/retrieve the samples in memory. + """ + + def __init__(self, mem_size: int): + self._init_data_structs(mem_size) + + def _init_data_structs(self, mem_size: int): + self._arr = (ct.c_uint8 * mem_size)() + self._cur_page = ct.c_size_t(0) + self._cache_addr: Union[Dict, DictProxy] = {} + self._lock: Union[Lock, _DummyLock] = _DummyLock() + self._freeze = ct.c_bool(False) + + def __len__(self): + """Get the number of cached items.""" + return len(self._cache_addr) + + @property + def mem_size(self) -> int: + """Get the reserved memory pool size (bytes).""" + return len(self._arr) + + def get(self, key: Any) -> Tuple[Optional[np.ndarray], Optional[Dict]]: + """Try to look up the cached item with the given key. + + Args: + key (Any): A key for looking up the cached item + + Returns: + If succeed return (np.ndarray, Dict), otherwise return (None, None) + """ + if self.mem_size == 0 or key not in self._cache_addr: + return None, None + + addr = self._cache_addr[key] + + offset, count, dtype, shape, strides, meta = addr + + data = np.frombuffer(self._arr, dtype=dtype, count=count, offset=offset) + return np.lib.stride_tricks.as_strided(data, shape, strides), meta + + def put(self, key: Any, data: np.ndarray, meta: Optional[Dict] = None) -> Optional[int]: + """Try to store np.ndarray and metadata with a key to the reserved memory pool. + + Args: + key (Any): A key to store the cached item + data (np.ndarray): A data sample to store + meta (Optional[Dict]): A metadata of the data sample + + Returns: + Optional[int]: If succeed return the address of cached item in memory pool + """ + if self._freeze.value: + return None + + data_bytes = data.size * data.itemsize + + with self._lock: + new_page = self._cur_page.value + data_bytes + + if key in self._cache_addr or new_page > self.mem_size: + return None + + offset = ct.byref(self._arr, self._cur_page.value) + ct.memmove(offset, data.ctypes.data, data_bytes) + + self._cache_addr[key] = ( + self._cur_page.value, + data.size, + data.dtype, + data.shape, + data.strides, + meta, + ) + self._cur_page.value = new_page + return new_page + + def __repr__(self): + """Representation for the current handler status.""" + perc = 100.0 * self._cur_page.value / self.mem_size if self.mem_size > 0 else 0.0 + return ( + f"{self.__class__.__name__} " + f"uses {self._cur_page.value} / {self.mem_size} ({perc:.1f}%) memory pool and " + f"store {len(self)} items." + ) + + def freeze(self): + """If frozen, it is impossible to store a new item anymore.""" + self._freeze.value = True + + def unfreeze(self): + """If unfrozen, it is possible to store a new item.""" + self._freeze.value = False + + +class MemCacheHandlerForSP(MemCacheHandlerBase): + """Memory caching handler for single processing. + + Use if PyTorch's DataLoader.num_workers == 0. + """ + + +class MemCacheHandlerForMP(MemCacheHandlerBase): + """Memory caching handler for multi processing. + + Use if PyTorch's DataLoader.num_workers > 0. + """ + + def _init_data_structs(self, mem_size: int): + self._arr = mp.Array(ct.c_uint8, mem_size, lock=False) + self._cur_page = mp.Value(ct.c_size_t, 0, lock=False) + + self._manager = mp.Manager() + self._cache_addr: DictProxy = self._manager.dict() + self._lock = mp.Lock() + self._freeze = mp.Value(ct.c_bool, False, lock=False) + + def __del__(self): + """When deleting, manager should also be shutdowned.""" + self._manager.shutdown() + + +class MemCacheHandlerError(Exception): + """Exception class for MemCacheHandler.""" + + +class MemCacheHandlerSingleton: + """A singleton class to create, delete and get MemCacheHandlerBase.""" + + instance: MemCacheHandlerBase + CPU_MEM_LIMITS_GIB: int = 30 + + @classmethod + def get(cls) -> MemCacheHandlerBase: + """Get the created MemCacheHandlerBase. + + If no one is created before, raise RuntimeError. + """ + if not hasattr(cls, "instance"): + cls_name = cls.__class__.__name__ + raise MemCacheHandlerError(f"Before calling {cls_name}.get(), you should call {cls_name}.create() first.") + + return cls.instance + + @classmethod + def create(cls, mode: str, mem_size: int) -> MemCacheHandlerBase: + """Create a new MemCacheHandlerBase instance. + + Args: + mode (str): There are two options: null, multiprocessing or singleprocessing. + mem_size (int): The size of memory pool (bytes). + """ + + # COPY FROM mmcv.runner.get_dist_info + from torch import distributed + + if distributed.is_available() and distributed.is_initialized(): + world_size = distributed.get_world_size() + else: + world_size = 1 + + # Prevent CPU OOM issue + memory_info = psutil.virtual_memory() + available_cpu_mem = memory_info.available / GIB + + if world_size > 1: + mem_size = mem_size // world_size + available_cpu_mem = available_cpu_mem // world_size + logger.info(f"Since world_size={world_size} > 1, each worker a {mem_size} size memory pool.") + + logger.info(f"Try to create a {mem_size} size memory pool.") + if available_cpu_mem < ((mem_size / GIB) + cls.CPU_MEM_LIMITS_GIB): + logger.warning("No available CPU memory left, mem_size will be set to 0.") + mem_size = 0 + + if mode == "null" or mem_size == 0: + cls.instance = MemCacheHandlerBase(mem_size=0) + cls.instance.freeze() + elif mode == "multiprocessing": + cls.instance = MemCacheHandlerForMP(mem_size) + elif mode == "singleprocessing": + cls.instance = MemCacheHandlerForSP(mem_size) + else: + raise MemCacheHandlerError(f"{mode} is unknown mode.") + + return cls.instance + + @classmethod + def delete(cls) -> None: + """Delete the existing MemCacheHandlerBase instance.""" + if hasattr(cls, "instance"): + del cls.instance diff --git a/src/otx/core/data/caching/storage_cache.py b/src/otx/core/data/caching/storage_cache.py new file mode 100644 index 00000000000..a0c919c42cc --- /dev/null +++ b/src/otx/core/data/caching/storage_cache.py @@ -0,0 +1,117 @@ +"""A thin wrapper for storage cache using datumaro arrow format exporter.""" +# copyright (c) 2023 intel corporation +# spdx-license-identifier: apache-2.0 +# + +import hashlib +import os +import shutil +import stat +from typing import List, Optional + +from datumaro.components.dataset import Dataset as DatumDataset +from datumaro.components.progress_reporting import SimpleProgressReporter + +from otx.core.file import OTX_CACHE + +DATASET_CACHE = os.path.join(OTX_CACHE, "dataset") + + +def arrow_cache_helper( + dataset: DatumDataset, + scheme: str, + num_workers: int = 0, + cache_dir: str = DATASET_CACHE, + force: bool = False, +) -> List[str]: + """A helper for dumping Datumaro arrow format. + + Args: + dataset: Datumaro dataset to export in apache arrow. + scheme: Datumaro apache arrow image encoding scheme. + num_workers: The number of workers to build arrow format. + cache_dir: The directory to save. + force: If true, rebuild arrow even if cache is hit. + """ + + def get_hash(dataset, scheme): + source_path = dataset.data_path + _hash = hashlib.sha256() + _hash.update(f"{source_path}".encode("utf-8")) + _hash.update(f"{len(dataset)}".encode("utf-8")) + _hash.update(f"{scheme}".encode("utf-8")) + if source_path: + _hash.update(str(os.stat(source_path)[stat.ST_MTIME]).encode("utf-8")) + if os.path.isdir(source_path): + for root, dirs, files in os.walk(source_path): + for file in files: + file = os.path.join(root, file) + _hash.update(str(os.stat(file)[stat.ST_MTIME]).encode("utf-8")) + for _dir in dirs: + _dir = os.path.join(root, _dir) + _hash.update(str(os.stat(_dir)[stat.ST_MTIME]).encode("utf-8")) + return _hash.hexdigest() + + def get_file_hash(file): + _hash = hashlib.sha256() + _hash.update(str(file).encode("utf-8")) + _hash.update(str(os.stat(file)[stat.ST_MTIME]).encode("utf-8")) + return _hash.hexdigest() + + cache_dir = os.path.join(cache_dir, get_hash(dataset, scheme)) + if os.path.exists(cache_dir) and force: + shutil.rmtree(cache_dir) + os.makedirs(cache_dir, exist_ok=True) + + cache_paths = [] + cache_hit = [] + for cache_path in os.listdir(cache_dir): + if not cache_path.endswith(".arrow"): + continue + cache_hit.append(False) + cache_path = os.path.join(cache_dir, cache_path) + cache_paths.append(cache_path) + hash_path = f"{cache_path}.hash" + if os.path.exists(cache_path) and os.path.exists(hash_path): + with open(hash_path, "r", encoding="utf-8") as f: + if get_file_hash(cache_path) == f.read(): + cache_hit[-1] = True + + if cache_hit and all(cache_hit): + return cache_paths + + dataset.export( + cache_dir, + "arrow", + save_media=True, + image_ext=scheme, + num_workers=num_workers, + progress_reporter=SimpleProgressReporter(0, 10), + ) + + cache_paths = [] + for cache_path in os.listdir(cache_dir): + if not cache_path.endswith(".arrow"): + continue + cache_path = os.path.join(cache_dir, cache_path) + cache_paths.append(cache_path) + hash_path = f"{cache_path}.hash" + with open(hash_path, "w", encoding="utf-8") as f: + f.write(get_file_hash(cache_path)) + + return cache_paths + + +def init_arrow_cache(dataset: DatumDataset, scheme: Optional[str] = None, **kwargs) -> DatumDataset: + """Init arrow format cache from Datumaro. + + Args: + dataset: Datumaro dataset + scheme: Datumaro apache arrow image encoding scheme. + kwargs: kwargs passed to 'arrow_cache_helper' + """ + if scheme is None or scheme == "NONE": + return dataset + cache_paths = arrow_cache_helper(dataset, scheme, **kwargs) + dataset = DatumDataset.import_from(os.path.dirname(cache_paths[0]), "arrow") + return dataset diff --git a/src/otx/core/data/dataset/__init__.py b/src/otx/core/data/dataset/__init__.py deleted file mode 100644 index 5f218797c6f..00000000000 --- a/src/otx/core/data/dataset/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module defines OTXDatasets.""" diff --git a/src/otx/core/data/dataset/action_classification.py b/src/otx/core/data/dataset/action_classification.py deleted file mode 100644 index a6fda02cf77..00000000000 --- a/src/otx/core/data/dataset/action_classification.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTXActionClsDataset.""" - -from __future__ import annotations - -from functools import partial -from typing import Callable - -import torch -from datumaro import Label - -from otx.core.data.dataset.base import OTXDataset -from otx.core.data.entity.action_classification import ActionClsBatchDataEntity, ActionClsDataEntity -from otx.core.data.entity.base import ImageInfo - - -class OTXActionClsDataset(OTXDataset[ActionClsDataEntity]): - """OTXDataset class for action classification task.""" - - def _get_item_impl(self, idx: int) -> ActionClsDataEntity | None: - item = self.dm_subset.get(id=self.ids[idx], subset=self.dm_subset.name) - - label_anns = [ann for ann in item.annotations if isinstance(ann, Label)] - - entity = ActionClsDataEntity( - video=item.media, - image=[], - img_info=ImageInfo( - img_idx=idx, - img_shape=(0, 0), - ori_shape=(0, 0), - image_color_channel=self.image_color_channel, - ), - labels=torch.as_tensor([ann.label for ann in label_anns]), - ) - - return self._apply_transforms(entity) - - @property - def collate_fn(self) -> Callable: - """Collection function to collect ActionClsDataEntity into ActionClsBatchDataEntity.""" - return partial(ActionClsBatchDataEntity.collate_fn, stack_images=self.stack_images) diff --git a/src/otx/core/data/dataset/action_detection.py b/src/otx/core/data/dataset/action_detection.py deleted file mode 100644 index cb689acf8e7..00000000000 --- a/src/otx/core/data/dataset/action_detection.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTXActionDetDataset.""" - -from __future__ import annotations - -import pickle -from functools import partial -from pathlib import Path -from typing import Callable - -import numpy as np -import torch -from datumaro import Bbox, Image -from datumaro.components.annotation import AnnotationType -from torchvision import tv_tensors - -from otx.core.data.dataset.base import OTXDataset -from otx.core.data.entity.action_detection import ActionDetBatchDataEntity, ActionDetDataEntity -from otx.core.data.entity.base import ImageInfo - - -class OTXActionDetDataset(OTXDataset[ActionDetDataEntity]): - """OTXDataset class for action detection task.""" - - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - self.num_classes = len(self.dm_subset.categories()[AnnotationType.label]) - - def _get_item_impl(self, idx: int) -> ActionDetDataEntity | None: - item = self.dm_subset.get(id=self.ids[idx], subset=self.dm_subset.name) - img = item.media_as(Image) - img_data, img_shape = self._get_img_data_and_shape(img) - - bbox_anns = [ann for ann in item.annotations if isinstance(ann, Bbox)] - bboxes = ( - np.stack([ann.points for ann in bbox_anns], axis=0).astype(np.float32) - if len(bbox_anns) > 0 - else np.zeros((0, 4), dtype=np.float32) - ) - - entity = ActionDetDataEntity( - image=img_data, - img_info=ImageInfo( - img_idx=idx, - img_shape=img_shape, - ori_shape=img_shape, - image_color_channel=self.image_color_channel, - ), - bboxes=tv_tensors.BoundingBoxes( - bboxes, - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_shape, - ), - labels=torch.nn.functional.one_hot( - torch.as_tensor([ann.label for ann in bbox_anns]), - self.num_classes, - ).to(torch.float), - frame_path=item.media.path, - proposals=self._get_proposals( - item.media.path, - self.dm_subset.infos().get(f"{self.dm_subset.name}_proposals", None), - ), - ) - - return self._apply_transforms(entity) - - @staticmethod - def _get_proposals(frame_path: str, proposal_file: str | None) -> np.ndarray: - """Get proposal from frame path and proposal file name. - - Datumaro AVA dataset expect data structure as - - data_root/ - - frames/ - - video0 - - video0_0001.jpg - - vdieo0_0002.jpg - - annotations/ - - train.csv - - val.csv - - train.pkl - - val.pkl - """ - if proposal_file is None: - return np.array([[0, 0, 1, 1]], dtype=np.float64) - - annotation_dir = Path(frame_path).parent.parent.parent - proposal_file_path = annotation_dir / "annotations" / proposal_file - if not proposal_file_path.exists(): - return np.array([[0, 0, 1, 1]], dtype=np.float64) - with Path.open(proposal_file_path, "rb") as f: - info = pickle.load(f) # noqa: S301 - return ( - info[",".join(Path(frame_path).stem.rsplit("_", 1))][:, :4] - if ",".join(Path(frame_path).stem.rsplit("_", 1)) in info - else np.array([[0, 0, 1, 1]], dtype=np.float32) - ) - - @property - def collate_fn(self) -> Callable: - """Collection function to collect ActionClsDataEntity into ActionClsBatchDataEntity.""" - return partial(ActionDetBatchDataEntity.collate_fn, stack_images=self.stack_images) diff --git a/src/otx/core/data/dataset/anomaly/__init__.py b/src/otx/core/data/dataset/anomaly/__init__.py deleted file mode 100644 index c1aaf65924c..00000000000 --- a/src/otx/core/data/dataset/anomaly/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Datasets for Anomaly Tasks.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from .dataset import AnomalyDataset - -__all__ = ["AnomalyDataset"] diff --git a/src/otx/core/data/dataset/anomaly/dataset.py b/src/otx/core/data/dataset/anomaly/dataset.py deleted file mode 100644 index 1d9d6bc443a..00000000000 --- a/src/otx/core/data/dataset/anomaly/dataset.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Anomaly Classification Dataset.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from pathlib import Path -from typing import Callable - -import torch -from anomalib.data.utils import masks_to_boxes -from datumaro import DatasetSubset, Image -from torchvision import io -from torchvision.tv_tensors import BoundingBoxes, BoundingBoxFormat, Mask - -from otx.core.data.dataset.base import OTXDataset, Transforms -from otx.core.data.entity.anomaly import ( - AnomalyClassificationDataBatch, - AnomalyClassificationDataItem, - AnomalyDetectionDataBatch, - AnomalyDetectionDataItem, - AnomalySegmentationDataBatch, - AnomalySegmentationDataItem, -) -from otx.core.data.entity.base import ImageInfo -from otx.core.data.mem_cache import NULL_MEM_CACHE_HANDLER, MemCacheHandlerBase -from otx.core.types.image import ImageColorChannel -from otx.core.types.task import OTXTaskType - - -class AnomalyDataset(OTXDataset): - """OTXDataset class for anomaly classification task.""" - - def __init__( - self, - task_type: OTXTaskType, - dm_subset: DatasetSubset, - transforms: Transforms, - mem_cache_handler: MemCacheHandlerBase = NULL_MEM_CACHE_HANDLER, - mem_cache_img_max_size: tuple[int, int] | None = None, - max_refetch: int = 1000, - image_color_channel: ImageColorChannel = ImageColorChannel.RGB, - stack_images: bool = True, - ) -> None: - self.task_type = task_type - super().__init__( - dm_subset, - transforms, - mem_cache_handler, - mem_cache_img_max_size, - max_refetch, - image_color_channel, - stack_images, - ) - - def _get_item_impl( - self, - index: int, - ) -> AnomalyClassificationDataItem | AnomalySegmentationDataBatch | AnomalyDetectionDataBatch: - datumaro_item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = datumaro_item.media_as(Image) - # returns image in RGB format if self.image_color_channel is RGB - img_data, img_shape = self._get_img_data_and_shape(img) - # Note: This assumes that the dataset is in MVTec format. - # We can't use datumaro label id as it returns some number like 3 for good from which it is hard to infer - # whether the image is Anomalous or Normal. Because it leads to other questions like what do numbers 0,1,2 mean? - label: torch.LongTensor = ( - torch.tensor(0.0, dtype=torch.long) if "good" in datumaro_item.id else torch.tensor(1.0, dtype=torch.long) - ) - item: AnomalyClassificationDataItem | AnomalySegmentationDataItem | AnomalyDetectionDataItem - if self.task_type == OTXTaskType.ANOMALY_CLASSIFICATION: - item = AnomalyClassificationDataItem( - image=img_data, - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - image_color_channel=self.image_color_channel, - ), - label=label, - ) - elif self.task_type == OTXTaskType.ANOMALY_SEGMENTATION: - # Note: this part of code is brittle. Ideally Datumaro should return masks - # Another major problem with this is that it assumes that the dataset passed is in MVTec format - mask_file_path = ( - Path("/".join(datumaro_item.media.path.split("/")[:-3])) - / "ground_truth" - / f"{('/'.join(datumaro_item.media.path.split('/')[-2:])).replace('.png','_mask.png')}" - ) - mask = torch.zeros(1, img_shape[0], img_shape[1], dtype=torch.uint8) - if mask_file_path.exists(): - # read and convert to binary mask - mask = (io.read_image(str(mask_file_path), mode=io.ImageReadMode.GRAY) / 255).to(torch.uint8) - item = AnomalySegmentationDataItem( - image=img_data, - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - image_color_channel=self.image_color_channel, - ), - label=label, - mask=Mask(mask), - ) - elif self.task_type == OTXTaskType.ANOMALY_DETECTION: - # Note: this part of code is brittle. Ideally Datumaro should return masks - mask_file_path = ( - Path("/".join(datumaro_item.media.path.split("/")[:-3])) - / "ground_truth" - / f"{('/'.join(datumaro_item.media.path.split('/')[-2:])).replace('.png','_mask.png')}" - ) - mask = torch.zeros(1, img_shape[0], img_shape[1], dtype=torch.uint8) - if mask_file_path.exists(): - # read and convert to binary mask - mask = (io.read_image(str(mask_file_path), mode=io.ImageReadMode.GRAY) / 255).to(torch.uint8) - boxes, _ = masks_to_boxes(mask) - item = AnomalyDetectionDataItem( - image=img_data, - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - image_color_channel=self.image_color_channel, - ), - label=label, - boxes=BoundingBoxes(boxes[0], format=BoundingBoxFormat.XYXY, canvas_size=img_shape), - # mask is used for pixel-level metric computation. We can't assume that this will always be available - mask=Mask(mask), - ) - else: - msg = f"Task {self.task_type} is not supported yet." - raise NotImplementedError(msg) - - # without ignore the following error is returned - # Incompatible return value type (got "Any | None", expected - # "AnomalyClassificationDataItem | AnomalySegmentationDataBatch | AnomalyDetectionDataBatch") - return self._apply_transforms(item) # type: ignore[return-value] - - @property - def collate_fn(self) -> Callable: - """Collection function to collect SegDataEntity into SegBatchDataEntity in data loader.""" - if self.task_type == OTXTaskType.ANOMALY_CLASSIFICATION: - return AnomalyClassificationDataBatch.collate_fn - if self.task_type == OTXTaskType.ANOMALY_SEGMENTATION: - return AnomalySegmentationDataBatch.collate_fn - if self.task_type == OTXTaskType.ANOMALY_DETECTION: - return AnomalyDetectionDataBatch.collate_fn - msg = f"Task {self.task_type} is not supported yet." - raise NotImplementedError(msg) diff --git a/src/otx/core/data/dataset/base.py b/src/otx/core/data/dataset/base.py deleted file mode 100644 index 02f6441692b..00000000000 --- a/src/otx/core/data/dataset/base.py +++ /dev/null @@ -1,264 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Base class for OTXDataset.""" -from __future__ import annotations - -from abc import abstractmethod -from collections.abc import Iterable -from contextlib import contextmanager -from dataclasses import dataclass -from typing import TYPE_CHECKING, Callable, Generic, Iterator, List, Union - -import cv2 -import numpy as np -from datumaro.components.annotation import AnnotationType -from datumaro.components.media import ImageFromFile -from datumaro.util.image import _IMAGE_BACKEND, _IMAGE_BACKENDS, IMAGE_COLOR_SCALE, ImageColorScale -from torch.utils.data import Dataset -from torchvision.transforms.v2 import Compose - -from otx.core.data.entity.base import T_OTXDataEntity -from otx.core.data.mem_cache import NULL_MEM_CACHE_HANDLER -from otx.core.types.image import ImageColorChannel - -if TYPE_CHECKING: - from datumaro import DatasetSubset, Image, LabelCategories - - from otx.core.data.mem_cache import MemCacheHandlerBase - -Transforms = Union[Compose, Callable, List[Callable]] - - -@contextmanager -def image_decode_context() -> Iterator[None]: - """Change Datumaro image decode context. - - Use PIL Image decode because of performance issues. - With this context, `dm.Image.data` will return BGR numpy image tensor. - """ - ori_image_backend = _IMAGE_BACKEND.get() - ori_image_color_scale = IMAGE_COLOR_SCALE.get() - - _IMAGE_BACKEND.set(_IMAGE_BACKENDS.PIL) - IMAGE_COLOR_SCALE.set(ImageColorScale.COLOR) - - yield - - _IMAGE_BACKEND.set(ori_image_backend) - IMAGE_COLOR_SCALE.set(ori_image_color_scale) - - -@dataclass -class LabelInfo: - """Object to represent label information.""" - - label_names: list[str] - label_groups: list[list[str]] - - @property - def num_classes(self) -> int: - """Return number of labels.""" - return len(self.label_names) - - @classmethod - def from_num_classes(cls, num_classes: int) -> LabelInfo: - """Create this object from the number of classes. - - Args: - num_classes: Number of classes - - Returns: - LabelInfo( - label_names=["label_0", ...], - label_groups=[["label_0", ...]] - ) - """ - label_names = [f"label_{idx}" for idx in range(num_classes)] - - return LabelInfo( - label_names=label_names, - label_groups=[label_names], - ) - - @classmethod - def from_dm_label_groups(cls, dm_label_categories: LabelCategories) -> LabelInfo: - """Create this object from the datumaro label groups. - - Args: - dm_label_categories (LabelCategories): The label category information from Datumaro. - - Returns: - LabelInfo( - label_names=["Heart_King", "Heart_Queen", "Spade_King", "Spade_Jack"] - label_groups=[["Heart_King", "Heart_Queen"], ["Spade_King", "Spade_Jack"]] - ) - - """ - label_names = [item.name for item in dm_label_categories.items] - label_groups = [label_group.labels for label_group in dm_label_categories.label_groups] - if len(label_groups) == 0: # Single-label classification - label_groups = [label_names] - - return LabelInfo( - label_names=label_names, - label_groups=label_groups, - ) - - -class OTXDataset(Dataset, Generic[T_OTXDataEntity]): - """Base OTXDataset. - - Defines basic logic for OTX datasets. - - Args: - dm_subset: Datumaro subset of a dataset - transforms: Transforms to apply on images - mem_cache_handler: Handler of the images cache - mem_cache_img_max_size: Max size of images to put in cache - max_refetch: Maximum number of images to fetch in cache - image_color_channel: Color channel of images - stack_images: Whether or not to stack images in collate function in OTXBatchData entity. - - """ - - def __init__( - self, - dm_subset: DatasetSubset, - transforms: Transforms, - mem_cache_handler: MemCacheHandlerBase = NULL_MEM_CACHE_HANDLER, - mem_cache_img_max_size: tuple[int, int] | None = None, - max_refetch: int = 1000, - image_color_channel: ImageColorChannel = ImageColorChannel.RGB, - stack_images: bool = True, - ) -> None: - self.dm_subset = dm_subset - self.ids = [item.id for item in dm_subset] - self.transforms = transforms - self.mem_cache_handler = mem_cache_handler - self.mem_cache_img_max_size = mem_cache_img_max_size - self.max_refetch = max_refetch - self.image_color_channel = image_color_channel - self.stack_images = stack_images - self.label_info = LabelInfo.from_dm_label_groups(self.dm_subset.categories()[AnnotationType.label]) - - def __len__(self) -> int: - return len(self.ids) - - def _sample_another_idx(self) -> int: - return np.random.default_rng().integers(0, len(self)) - - def _apply_transforms(self, entity: T_OTXDataEntity) -> T_OTXDataEntity | None: - if isinstance(self.transforms, Compose): - entity = entity.to_tv_image() - return self.transforms(entity) - if isinstance(self.transforms, Iterable): - return self._iterable_transforms(entity) - if callable(self.transforms): - return self.transforms(entity) - - raise TypeError(self.transforms) - - def _iterable_transforms(self, item: T_OTXDataEntity) -> T_OTXDataEntity | None: - if not isinstance(self.transforms, list): - raise TypeError(item) - - results = item - for transform in self.transforms: - results = transform(results) - # MMCV transform can produce None. Please see - # https://github.com/open-mmlab/mmengine/blob/26f22ed283ae4ac3a24b756809e5961efe6f9da8/mmengine/dataset/base_dataset.py#L59-L66 - if results is None: - return None - - return results - - def __getitem__(self, index: int) -> T_OTXDataEntity: - for _ in range(self.max_refetch): - results = self._get_item_impl(index) - - if results is not None: - return results - - index = self._sample_another_idx() - - msg = f"Reach the maximum refetch number ({self.max_refetch})" - raise RuntimeError(msg) - - def _get_img_data_and_shape(self, img: Image) -> tuple[np.ndarray, tuple[int, int]]: - key = img.path if isinstance(img, ImageFromFile) else id(img) - - if (img_data := self.mem_cache_handler.get(key=key)[0]) is not None: - return img_data, img_data.shape[:2] - - with image_decode_context(): - img_data = ( - cv2.cvtColor(img.data, cv2.COLOR_BGR2RGB) - if self.image_color_channel == ImageColorChannel.RGB - else img.data - ) - - if img_data is None: - msg = "Cannot get image data" - raise RuntimeError(msg) - - img_data = self._cache_img(key=key, img_data=img_data.astype(np.uint8)) - - return img_data, img_data.shape[:2] - - def _cache_img(self, key: str | int, img_data: np.ndarray) -> np.ndarray: - """Cache an image after resizing. - - If there is available space in the memory pool, the input image is cached. - Before caching, the input image is resized if it is larger than the maximum image size - specified by the memory caching handler. - Otherwise, the input image is directly cached. - After caching, the processed image data is returned. - - Args: - key: The key associated with the image. - img_data: The image data to be cached. - - Returns: - The resized image if it was resized. Otherwise, the original image. - """ - if self.mem_cache_handler.frozen: - return img_data - - if self.mem_cache_img_max_size is None: - self.mem_cache_handler.put(key=key, data=img_data, meta=None) - return img_data - - height, width = img_data.shape[:2] - max_height, max_width = self.mem_cache_img_max_size - - if height <= max_height and width <= max_width: - self.mem_cache_handler.put(key=key, data=img_data, meta=None) - return img_data - - # Preserve the image size ratio and fit to max_height or max_width - # e.g. (1000 / 2000 = 0.5, 1000 / 1000 = 1.0) => 0.5 - # h, w = 2000 * 0.5 => 1000, 1000 * 0.5 => 500, bounded by max_height - min_scale = min(max_height / height, max_width / width) - new_height, new_width = int(min_scale * height), int(min_scale * width) - resized_img = cv2.resize( - src=img_data, - dsize=(new_width, new_height), - interpolation=cv2.INTER_LINEAR, - ) - - self.mem_cache_handler.put( - key=key, - data=resized_img, - meta=None, - ) - return resized_img - - @abstractmethod - def _get_item_impl(self, idx: int) -> T_OTXDataEntity | None: - pass - - @property - @abstractmethod - def collate_fn(self) -> Callable: - """Collection function to collect OTXDataEntity into OTXBatchDataEntity in data loader.""" diff --git a/src/otx/core/data/dataset/classification.py b/src/otx/core/data/dataset/classification.py deleted file mode 100644 index 27da3009fa7..00000000000 --- a/src/otx/core/data/dataset/classification.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTXClassificationDatasets.""" - -from __future__ import annotations - -from dataclasses import dataclass -from functools import partial -from typing import TYPE_CHECKING, Any, Callable - -import torch -from datumaro import Image, Label -from datumaro.components.annotation import AnnotationType -from torch.nn import functional - -from otx.core.data.dataset.base import LabelInfo, OTXDataset -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.classification import ( - HlabelClsBatchDataEntity, - HlabelClsDataEntity, - MulticlassClsBatchDataEntity, - MulticlassClsDataEntity, - MultilabelClsBatchDataEntity, - MultilabelClsDataEntity, -) - -if TYPE_CHECKING: - from datumaro import LabelCategories - - -@dataclass -class HLabelInfo(LabelInfo): - """The label information represents the hierarchy. - - All params should be kept since they're also used at the Model API side. - - :param num_multiclass_heads: the number of the multiclass heads - :param num_multilabel_classes: the number of multilabel classes - :param head_to_logits_range: the logit range of each heads - :param num_single_label_classes: the number of single label classes - :param class_to_group_idx: represents the head index and label index - :param all_groups: represents information of all groups - :param label_to_idx: index of each label - :param empty_multiclass_head_indices: the index of head that doesn't include any label - due to the label removing - - i.e. - Single-selection group information (Multiclass, Exclusive) - { - "Shape": ["Rigid", "Non-Rigid"], - "Rigid": ["Rectangle", "Triangle"], - "Non-Rigid": ["Circle"] - } - - Multi-selection group information (Multilabel) - { - "Animal": ["Lion", "Panda"] - } - - In the case above, HlabelInfo will be generated as below. - NOTE, If there was only one label in the multiclass group, it will be handeled as multilabel(Circle). - - num_multiclass_heads: 2 (Shape, Rigid) - num_multilabel_classes: 3 (Circle, Lion, Panda) - head_to_logits_range: {'0': (0, 2), '1': (2, 4)} (Each multiclass head have 2 labels) - num_single_label_classes: 4 (Rigid, Non-Rigid, Rectangle, Triangle) - class_to_group_idx: { - 'Non-Rigid': (0, 0), 'Rigid': (0, 1), - 'Rectangle': (1, 0), 'Triangle': (1, 1), - 'Circle': (2, 0), 'Lion': (2,1), 'Panda': (2,2) - } (head index, label index for each head) - all_groups: [['Non-Rigid', 'Rigid'], ['Rectangle', 'Triangle'], ['Circle'], ['Lion'], ['Panda']] - label_to_idx: { - 'Rigid': 0, 'Rectangle': 1, - 'Triangle': 2, 'Non-Rigid': 3, 'Circle': 4 - 'Lion': 5, 'Panda': 6 - } - label_tree_edges: [ - ["Rectangle", "Rigid"], ["Triangle", "Rigid"], ["Circle", "Non-Rigid"], - ] # NOTE, label_tree_edges format could be changed. - empty_multiclass_head_indices: [] - - All of the member variables should be considered for the Model API. - https://github.com/openvinotoolkit/training_extensions/blob/develop/src/otx/algorithms/classification/utils/cls_utils.py#L97 - """ - - num_multiclass_heads: int - num_multilabel_classes: int - head_idx_to_logits_range: dict[str, tuple[int, int]] - num_single_label_classes: int - class_to_group_idx: dict[str, tuple[int, int]] - all_groups: list[list[str]] - label_to_idx: dict[str, int] - label_tree_edges: list[list[str]] - empty_multiclass_head_indices: list[int] - - @classmethod - def from_dm_label_groups(cls, dm_label_categories: LabelCategories) -> HLabelInfo: - """Generate HLabelData from the Datumaro LabelCategories. - - Args: - dm_label_categories (LabelCategories): the label categories of datumaro. - """ - - def get_exclusive_group_info(all_groups: list[Label | list[Label]]) -> dict[str, Any]: - """Get exclusive group information.""" - exclusive_groups = [g for g in all_groups if len(g) > 1] - - last_logits_pos = 0 - num_single_label_classes = 0 - head_idx_to_logits_range = {} - class_to_idx = {} - - for i, group in enumerate(exclusive_groups): - head_idx_to_logits_range[str(i)] = (last_logits_pos, last_logits_pos + len(group)) - last_logits_pos += len(group) - for j, c in enumerate(group): - class_to_idx[c] = (i, j) - num_single_label_classes += 1 - - return { - "num_multiclass_heads": len(exclusive_groups), - "head_idx_to_logits_range": head_idx_to_logits_range, - "class_to_idx": class_to_idx, - "num_single_label_classes": num_single_label_classes, - } - - def get_single_label_group_info( - all_groups: list[Label | list[Label]], - num_exclusive_groups: int, - ) -> dict[str, Any]: - """Get single label group information.""" - single_label_groups = [g for g in all_groups if len(g) == 1] - - class_to_idx = {} - - for i, group in enumerate(single_label_groups): - class_to_idx[group[0]] = (num_exclusive_groups, i) - - return { - "num_multilabel_classes": len(single_label_groups), - "class_to_idx": class_to_idx, - } - - def merge_class_to_idx( - exclusive_ctoi: dict[str, tuple[int, int]], - single_label_ctoi: dict[str, tuple[int, int]], - ) -> dict[str, tuple[int, int]]: - """Merge the class_to_idx information from exclusive and single_label groups.""" - - def put_key_values(src: dict, dst: dict) -> None: - """Put key and values from src to dst.""" - for k, v in src.items(): - dst[k] = v - - class_to_idx: dict[str, tuple[int, int]] = {} - put_key_values(exclusive_ctoi, class_to_idx) - put_key_values(single_label_ctoi, class_to_idx) - return class_to_idx - - def get_label_tree_edges(dm_label_items: list[LabelCategories]) -> list[list[str]]: - """Get label tree edges information. Each edges represent [child, parent].""" - return [[item.name, item.parent] for item in dm_label_items if item.parent != ""] - - all_groups = [label_group.labels for label_group in dm_label_categories.label_groups] - - exclusive_group_info = get_exclusive_group_info(all_groups) - single_label_group_info = get_single_label_group_info(all_groups, exclusive_group_info["num_multiclass_heads"]) - - merged_class_to_idx = merge_class_to_idx( - exclusive_group_info["class_to_idx"], - single_label_group_info["class_to_idx"], - ) - - return HLabelInfo( - label_names=[item.name for item in dm_label_categories.items], - label_groups=all_groups, - num_multiclass_heads=exclusive_group_info["num_multiclass_heads"], - num_multilabel_classes=single_label_group_info["num_multilabel_classes"], - head_idx_to_logits_range=exclusive_group_info["head_idx_to_logits_range"], - num_single_label_classes=exclusive_group_info["num_single_label_classes"], - class_to_group_idx=merged_class_to_idx, - all_groups=all_groups, - label_to_idx=dm_label_categories._indices, # noqa: SLF001 - label_tree_edges=get_label_tree_edges(dm_label_categories.items), - empty_multiclass_head_indices=[], # consider the label removing case - ) - - -class OTXMulticlassClsDataset(OTXDataset[MulticlassClsDataEntity]): - """OTXDataset class for multi-class classification task.""" - - def _get_item_impl(self, index: int) -> MulticlassClsDataEntity | None: - item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = item.media_as(Image) - img_data, img_shape = self._get_img_data_and_shape(img) - - label_anns = [ann for ann in item.annotations if isinstance(ann, Label)] - if len(label_anns) > 1: - msg = f"Multi-class Classification can't use the multi-label, currently len(labels) = {len(label_anns)}" - raise ValueError(msg) - - entity = MulticlassClsDataEntity( - image=img_data, - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - image_color_channel=self.image_color_channel, - ), - labels=torch.as_tensor([ann.label for ann in label_anns]), - ) - - return self._apply_transforms(entity) - - @property - def collate_fn(self) -> Callable: - """Collection function to collect MulticlassClsDataEntity into MulticlassClsBatchDataEntity in data loader.""" - return partial(MulticlassClsBatchDataEntity.collate_fn, stack_images=self.stack_images) - - -class OTXMultilabelClsDataset(OTXDataset[MultilabelClsDataEntity]): - """OTXDataset class for multi-label classification task.""" - - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - self.num_classes = len(self.dm_subset.categories()[AnnotationType.label]) - - def _get_item_impl(self, index: int) -> MultilabelClsDataEntity | None: - item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = item.media_as(Image) - ignored_labels: list[int] = [] # This should be assigned form item - img_data, img_shape = self._get_img_data_and_shape(img) - - label_anns = [ann for ann in item.annotations if isinstance(ann, Label)] - labels = torch.as_tensor([ann.label for ann in label_anns]) - - entity = MultilabelClsDataEntity( - image=img_data, - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - image_color_channel=self.image_color_channel, - ignored_labels=ignored_labels, - ), - labels=self._convert_to_onehot(labels, ignored_labels), - ) - - return self._apply_transforms(entity) - - def _convert_to_onehot(self, labels: torch.tensor, ignored_labels: list[int]) -> torch.tensor: - """Convert label to one-hot vector format.""" - onehot = functional.one_hot(labels, self.num_classes).sum(0).clamp_max_(1) - if ignored_labels: - for ignore_label in ignored_labels: - onehot[ignore_label] = -1 - return onehot - - @property - def collate_fn(self) -> Callable: - """Collection function to collect MultilabelClsDataEntity into MultilabelClsBatchDataEntity in data loader.""" - return partial(MultilabelClsBatchDataEntity.collate_fn, stack_images=self.stack_images) - - -class OTXHlabelClsDataset(OTXDataset[HlabelClsDataEntity]): - """OTXDataset class for H-label classification task.""" - - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - self.dm_categories = self.dm_subset.categories()[AnnotationType.label] - - # Hlabel classification used HLabelInfo to insert the HLabelData. - self.label_info = HLabelInfo.from_dm_label_groups(self.dm_categories) - if self.label_info.num_multiclass_heads == 0: - msg = "The number of multiclass heads should be larger than 0." - raise ValueError(msg) - - for dm_item in self.dm_subset: - self._add_ancestors(dm_item.annotations) - - def _add_ancestors(self, label_anns: list[Label]) -> None: - """Add ancestors recursively if some label miss the ancestor information. - - If the label tree likes below, - object - vehicle -- car - |- bus - |- truck - And annotation = ['car'], it should be ['car', 'vehicle', 'object'], to include the ancestor. - - This function add the ancestors to the annotation if missing. - """ - - def _label_idx_to_name(idx: int) -> str: - return self.label_info.label_names[idx] - - def _label_name_to_idx(name: str) -> int: - indices = [idx for idx, val in enumerate(self.label_info.label_names) if val == name] - return indices[0] - - def _get_label_group_idx(label_name: str) -> int: - if isinstance(self.label_info, HLabelInfo): - return self.label_info.class_to_group_idx[label_name][0] - msg = f"self.label_info should have HLabelInfo type, got {type(self.label_info)}" - raise ValueError(msg) - - def _find_ancestor_recursively(label_name: str, ancestors: list) -> list[str]: - _, dm_label_category = self.dm_categories.find(label_name) - parent_name = dm_label_category.parent - - if parent_name != "": - ancestors.append(parent_name) - _find_ancestor_recursively(parent_name, ancestors) - return ancestors - - def _get_all_label_names_in_anns(anns: list[Label]) -> list[str]: - return [_label_idx_to_name(ann.label) for ann in anns] - - all_label_names = _get_all_label_names_in_anns(label_anns) - ancestor_dm_labels = [] - for ann in label_anns: - label_idx = ann.label - label_name = _label_idx_to_name(label_idx) - ancestors = _find_ancestor_recursively(label_name, []) - - for i, ancestor in enumerate(ancestors): - if ancestor not in all_label_names: - ancestor_dm_labels.append( - Label( - label=_label_name_to_idx(ancestor), - id=len(label_anns) + i, - group=_get_label_group_idx(ancestor), - ), - ) - label_anns.extend(ancestor_dm_labels) - - def _get_item_impl(self, index: int) -> HlabelClsDataEntity | None: - item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = item.media_as(Image) - ignored_labels: list[int] = [] # This should be assigned form item - img_data, img_shape = self._get_img_data_and_shape(img) - - label_anns = [ann for ann in item.annotations if isinstance(ann, Label)] - hlabel_labels = self._convert_label_to_hlabel_format(label_anns, ignored_labels) - - entity = HlabelClsDataEntity( - image=img_data, - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - image_color_channel=self.image_color_channel, - ignored_labels=ignored_labels, - ), - labels=torch.as_tensor(hlabel_labels), - ) - - return self._apply_transforms(entity) - - def _convert_label_to_hlabel_format(self, label_anns: list[Label], ignored_labels: list[int]) -> list[int]: - """Convert format of the label to the h-label. - - It converts the label format to h-label format. - - i.e. - Let's assume that we used the same dataset with example of the definition of HLabelData - and the original labels are ["Rigid", "Panda", "Lion"]. - - Then, h-label format will be [1, -1, 0, 1, 1]. - The first N-th indices represent the label index of multiclass heads (N=num_multiclass_heads), - others represent the multilabel labels. - - [Multiclass Heads: [1, -1]] - 0-th index = 1 -> ["Non-Rigid"(X), "Rigid"(O)] <- First multiclass head - 1-st index = -1 -> ["Rectangle"(X), "Triangle"(X)] <- Second multiclass head - - [Multilabel Head: [0, 1, 1]] - 2, 3, 4 indices = [0, 1, 1] -> ["Circle"(X), "Lion"(O), "Panda"(O)] - """ - if not isinstance(self.label_info, HLabelInfo): - msg = f"The type of label_info should be HLabelInfo, got {type(self.label_info)}." - raise TypeError(msg) - - num_multiclass_heads = self.label_info.num_multiclass_heads - num_multilabel_classes = self.label_info.num_multilabel_classes - - class_indices = [0] * (num_multiclass_heads + num_multilabel_classes) - for i in range(num_multiclass_heads): - class_indices[i] = -1 - - for ann in label_anns: - ann_name = self.dm_categories.items[ann.label].name - group_idx, in_group_idx = self.label_info.class_to_group_idx[ann_name] - - if group_idx < num_multiclass_heads: - class_indices[group_idx] = in_group_idx - elif not ignored_labels or ann.label not in ignored_labels: - class_indices[num_multiclass_heads + in_group_idx] = 1 - else: - class_indices[num_multiclass_heads + in_group_idx] = -1 - - return class_indices - - @property - def collate_fn(self) -> Callable: - """Collection function to collect HlabelClsDataEntity into HlabelClsBatchDataEntity in data loader.""" - return partial(HlabelClsBatchDataEntity.collate_fn, stack_images=self.stack_images) diff --git a/src/otx/core/data/dataset/detection.py b/src/otx/core/data/dataset/detection.py deleted file mode 100644 index 3eb3d319f14..00000000000 --- a/src/otx/core/data/dataset/detection.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTXDetectionDataset.""" - -from __future__ import annotations - -from functools import partial -from typing import Callable - -import numpy as np -import torch -from datumaro import Bbox, Image -from torchvision import tv_tensors - -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.detection import DetBatchDataEntity, DetDataEntity - -from .base import OTXDataset - - -class OTXDetectionDataset(OTXDataset[DetDataEntity]): - """OTXDataset class for detection task.""" - - def _get_item_impl(self, index: int) -> DetDataEntity | None: - item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = item.media_as(Image) - ignored_labels: list[int] = [] # This should be assigned form item - img_data, img_shape = self._get_img_data_and_shape(img) - - bbox_anns = [ann for ann in item.annotations if isinstance(ann, Bbox)] - - bboxes = ( - np.stack([ann.points for ann in bbox_anns], axis=0).astype(np.float32) - if len(bbox_anns) > 0 - else np.zeros((0, 4), dtype=np.float32) - ) - - entity = DetDataEntity( - image=img_data, - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - image_color_channel=self.image_color_channel, - ignored_labels=ignored_labels, - ), - bboxes=tv_tensors.BoundingBoxes( - bboxes, - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_shape, - ), - labels=torch.as_tensor([ann.label for ann in bbox_anns]), - ) - - return self._apply_transforms(entity) - - @property - def collate_fn(self) -> Callable: - """Collection function to collect DetDataEntity into DetBatchDataEntity in data loader.""" - return partial(DetBatchDataEntity.collate_fn, stack_images=self.stack_images) diff --git a/src/otx/core/data/dataset/instance_segmentation.py b/src/otx/core/data/dataset/instance_segmentation.py deleted file mode 100644 index 77aa8057261..00000000000 --- a/src/otx/core/data/dataset/instance_segmentation.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTXInstanceSegDataset.""" - -from __future__ import annotations - -from functools import partial -from typing import Callable - -import numpy as np -import torch -from datumaro import DatasetSubset, Image, Polygon -from torchvision import tv_tensors - -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.instance_segmentation import InstanceSegBatchDataEntity, InstanceSegDataEntity -from otx.core.utils.mask_util import polygon_to_bitmap - -from .base import OTXDataset, Transforms - - -class OTXInstanceSegDataset(OTXDataset[InstanceSegDataEntity]): - """OTXDataset class for instance segmentation. - - Args: - dm_subset (DatasetSubset): The subset of the dataset. - transforms (Transforms): Data transformations to be applied. - include_polygons (bool): Flag indicating whether to include polygons in the dataset. - If set to False, polygons will be converted to bitmaps, and bitmaps will be used for training. - **kwargs: Additional keyword arguments passed to the base class. - """ - - def __init__(self, dm_subset: DatasetSubset, transforms: Transforms, include_polygons: bool, **kwargs) -> None: - super().__init__(dm_subset, transforms, **kwargs) - self.include_polygons = include_polygons - - def _get_item_impl(self, index: int) -> InstanceSegDataEntity | None: - item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = item.media_as(Image) - ignored_labels: list[int] = [] - img_data, img_shape = self._get_img_data_and_shape(img) - - gt_bboxes, gt_labels, gt_masks, gt_polygons = [], [], [], [] - - for annotation in item.annotations: - if isinstance(annotation, Polygon): - bbox = np.array(annotation.get_bbox(), dtype=np.float32) - gt_bboxes.append(bbox) - gt_labels.append(annotation.label) - - if self.include_polygons: - gt_polygons.append(annotation) - else: - gt_masks.append(polygon_to_bitmap([annotation], *img_shape)[0]) - - # convert xywh to xyxy format - bboxes = np.array(gt_bboxes, dtype=np.float32) - bboxes[:, 2:] += bboxes[:, :2] - - masks = np.stack(gt_masks, axis=0) if gt_masks else np.zeros((0, *img_shape), dtype=bool) - labels = np.array(gt_labels, dtype=np.int64) - - entity = InstanceSegDataEntity( - image=img_data, - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - image_color_channel=self.image_color_channel, - ignored_labels=ignored_labels, - ), - bboxes=tv_tensors.BoundingBoxes( - bboxes, - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_shape, - ), - masks=tv_tensors.Mask(masks, dtype=torch.uint8), - labels=torch.as_tensor(labels), - polygons=gt_polygons, - ) - - return self._apply_transforms(entity) - - @property - def collate_fn(self) -> Callable: - """Collection function to collect InstanceSegDataEntity into InstanceSegDataEntity in dataloader.""" - return partial(InstanceSegBatchDataEntity.collate_fn, stack_images=self.stack_images) diff --git a/src/otx/core/data/dataset/segmentation.py b/src/otx/core/data/dataset/segmentation.py deleted file mode 100644 index bebb806c61e..00000000000 --- a/src/otx/core/data/dataset/segmentation.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTXSegmentationDataset.""" - -from __future__ import annotations - -import warnings -from dataclasses import dataclass -from functools import partial -from typing import TYPE_CHECKING, Callable - -import numpy as np -import torch -from datumaro.components.annotation import Image, Mask -from torchvision import tv_tensors - -from otx.core.data.dataset.base import LabelInfo, Transforms -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.segmentation import SegBatchDataEntity, SegDataEntity -from otx.core.data.mem_cache import NULL_MEM_CACHE_HANDLER, MemCacheHandlerBase -from otx.core.types.image import ImageColorChannel - -from .base import OTXDataset - -if TYPE_CHECKING: - from datumaro import DatasetSubset - - -@dataclass -class SegLabelInfo(LabelInfo): - """Meta information of Semantic Segmentation.""" - - def __init__(self, label_names: list[str], label_groups: list[list[str]]) -> None: - if not any(word.lower() == "background" for word in label_names): - msg = ( - "Currently, no background label exists for `label_names`. " - "Segmentation requires a background label. " - "To do this, `Background` is added at index 0 of `label_names`." - ) - warnings.warn(msg, stacklevel=2) - label_names.insert(0, "Background") - super().__init__(label_names, label_groups) - - -class OTXSegmentationDataset(OTXDataset[SegDataEntity]): - """OTXDataset class for segmentation task.""" - - def __init__( - self, - dm_subset: DatasetSubset, - transforms: Transforms, - mem_cache_handler: MemCacheHandlerBase = NULL_MEM_CACHE_HANDLER, - mem_cache_img_max_size: tuple[int, int] | None = None, - max_refetch: int = 1000, - image_color_channel: ImageColorChannel = ImageColorChannel.RGB, - stack_images: bool = True, - ) -> None: - super().__init__( - dm_subset, - transforms, - mem_cache_handler, - mem_cache_img_max_size, - max_refetch, - image_color_channel, - stack_images, - ) - self.label_info = SegLabelInfo( - label_names=self.label_info.label_names, - label_groups=self.label_info.label_groups, - ) - - def _get_item_impl(self, index: int) -> SegDataEntity | None: - item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = item.media_as(Image) - num_classes = self.label_info.num_classes - ignored_labels: list[int] = [] - img_data, img_shape = self._get_img_data_and_shape(img) - - # create 2D class mask. We use np.sum() since Datumaro returns 3D masks (one for each class) - mask_anns = np.sum( - [ann.as_class_mask() for ann in item.annotations if isinstance(ann, Mask)], - axis=0, - dtype=np.uint8, - ) - mask = torch.as_tensor(mask_anns, dtype=torch.long) - # assign possible ignored labels from dataset to max label class + 1. - # it is needed to compute mDice metric. - mask[mask == 255] = num_classes - entity = SegDataEntity( - image=img_data, - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - image_color_channel=self.image_color_channel, - ignored_labels=ignored_labels, - ), - gt_seg_map=tv_tensors.Mask( - mask, - ), - ) - return self._apply_transforms(entity) - - @property - def collate_fn(self) -> Callable: - """Collection function to collect SegDataEntity into SegBatchDataEntity in data loader.""" - return partial(SegBatchDataEntity.collate_fn, stack_images=self.stack_images) diff --git a/src/otx/core/data/dataset/tile.py b/src/otx/core/data/dataset/tile.py deleted file mode 100644 index b92619c9d4c..00000000000 --- a/src/otx/core/data/dataset/tile.py +++ /dev/null @@ -1,442 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""OTX tile dataset.""" - -from __future__ import annotations - -import logging as log -from itertools import product -from typing import TYPE_CHECKING, Callable - -import numpy as np -import torch -from datumaro import Bbox, DatasetItem, DatasetSubset, Image, Polygon -from datumaro import Dataset as DmDataset -from datumaro.plugins.tiling import Tile -from datumaro.plugins.tiling.util import ( - clip_x1y1x2y2, - cxcywh_to_x1y1x2y2, - x1y1x2y2_to_cxcywh, - x1y1x2y2_to_xywh, -) -from torchvision import tv_tensors - -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.detection import DetDataEntity -from otx.core.data.entity.instance_segmentation import InstanceSegDataEntity -from otx.core.data.entity.tile import ( - TileBatchDetDataEntity, - TileBatchInstSegDataEntity, - TileDetDataEntity, - TileInstSegDataEntity, -) -from otx.core.types.task import OTXTaskType -from otx.core.utils.mask_util import polygon_to_bitmap - -from .base import OTXDataset - -if TYPE_CHECKING: - from datumaro.components.media import BboxIntCoords - - from otx.core.config.data import TileConfig - from otx.core.data.dataset.detection import OTXDetectionDataset - from otx.core.data.dataset.instance_segmentation import OTXInstanceSegDataset - from otx.core.data.entity.base import OTXDataEntity - -# ruff: noqa: SLF001 -# NOTE: Disable private-member-access (SLF001). -# This is a workaround so we could apply the same transforms to tiles as the original dataset. - - -class OTXTileTransform(Tile): - """OTX tile transform. - - Different from the original Datumaro Tile transform, - OTXTileTransform takes tile_size and overlap as input instead of grid size - - Args: - extractor (DatasetSubset): Dataset subset to extract tiles from. - tile_size (tuple[int, int]): Tile size. - overlap (tuple[float, float]): Overlap ratio. - threshold_drop_ann (float): Threshold to drop annotations. - """ - - def __init__( - self, - extractor: DatasetSubset, - tile_size: tuple[int, int], - overlap: tuple[float, float], - threshold_drop_ann: float, - ) -> None: - super().__init__( - extractor, - (0, 0), - overlap=overlap, - threshold_drop_ann=threshold_drop_ann, - ) - self._tile_size = tile_size - - def _extract_rois(self, image: Image) -> list[BboxIntCoords]: - """Extracts Tile ROIs from the given image. - - Args: - image (Image): Full image. - - Returns: - list[BboxIntCoords]: list of ROIs. - """ - if image.size is None: - msg = "Image size is None" - raise ValueError(msg) - - img_h, img_w = image.size - tile_h, tile_w = self._tile_size - h_ovl, w_ovl = self._overlap - - rois: list[BboxIntCoords] = [] - cols = range(0, img_w, int(tile_w * (1 - w_ovl))) - rows = range(0, img_h, int(tile_h * (1 - h_ovl))) - - for offset_x, offset_y in product(cols, rows): - x2 = min(offset_x + tile_w, img_w) - y2 = min(offset_y + tile_h, img_h) - c_x, c_y, w, h = x1y1x2y2_to_cxcywh(offset_x, offset_y, x2, y2) - x1, y1, x2, y2 = cxcywh_to_x1y1x2y2(c_x, c_y, w, h) - x1, y1, x2, y2 = clip_x1y1x2y2(x1, y1, x2, y2, img_w, img_h) - x1, y1, x2, y2 = (int(v) for v in [x1, y1, x2, y2]) - rois += [x1y1x2y2_to_xywh(x1, y1, x2, y2)] - - log.info(f"image: {img_h}x{img_w} ~ tile_size: {self._tile_size}") - log.info(f"{len(rows)}x{len(cols)} tiles -> {len(rois)} tiles") - return rois - - -class OTXTileDatasetFactory: - """OTX tile dataset factory.""" - - @classmethod - def create( - cls, - task: OTXTaskType, - dataset: OTXDataset, - tile_config: TileConfig, - ) -> OTXTileDataset: - """Create a tile dataset based on the task type and subset type. - - NOte: All task utilize the same OTXTileTrainDataset for training. - In testing, we use different tile dataset for different task - type due to different annotation format and data entity. - - Args: - task (OTXTaskType): OTX task type. - dataset (OTXDataset): OTX dataset. - tile_config (TilerConfig): Tile configuration. - - Returns: - OTXTileDataset: Tile dataset. - """ - if dataset.dm_subset.name == "train": - return OTXTileTrainDataset(dataset, tile_config) - - if task == OTXTaskType.DETECTION: - return OTXTileDetTestDataset(dataset, tile_config) - if task in [OTXTaskType.ROTATED_DETECTION, OTXTaskType.INSTANCE_SEGMENTATION]: - return OTXTileInstSegTestDataset(dataset, tile_config) - msg = f"Unsupported task type: {task} for tiling" - raise NotImplementedError(msg) - - -class OTXTileDataset(OTXDataset): - """OTX tile dataset base class. - - Args: - dataset (OTXDataset): OTX dataset. - tile_config (TilerConfig): Tile configuration. - """ - - def __init__(self, dataset: OTXDataset, tile_config: TileConfig) -> None: - super().__init__( - dataset.dm_subset, - dataset.transforms, - dataset.mem_cache_handler, - dataset.mem_cache_img_max_size, - dataset.max_refetch, - ) - self.tile_config = tile_config - self._dataset = dataset - - def __len__(self) -> int: - return len(self._dataset.ids) - - @property - def collate_fn(self) -> Callable: - """Collate function from the original dataset.""" - return self._dataset.collate_fn - - def _get_item_impl(self, index: int) -> OTXDataEntity | None: - """Get item implementation from the original dataset.""" - return self._dataset._get_item_impl(index) - - def _convert_entity(self, image: np.ndarray, dataset_item: DatasetItem) -> OTXDataEntity: - """Convert a tile dataset item to OTXDataEntity.""" - msg = "Method _convert_entity is not implemented." - raise NotImplementedError(msg) - - def get_tiles(self, image: np.ndarray, item: DatasetItem) -> tuple[list[OTXDataEntity], list[dict]]: - """Retrieves tiles from the given image and dataset item. - - Args: - image (np.ndarray): The input image. - item (DatasetItem): The dataset item. - - Returns: - A tuple containing two lists: - - tile_entities (list[OTXDataEntity]): List of tile entities. - - tile_attrs (list[dict]): List of tile attributes. - """ - tile_ds = DmDataset.from_iterable([item]) - tile_ds = tile_ds.transform( - OTXTileTransform, - tile_size=self.tile_config.tile_size, - overlap=(self.tile_config.overlap, self.tile_config.overlap), - threshold_drop_ann=0.5, - ) - - if self.dm_subset.name == "val": - # NOTE: filter validation tiles with annotations only to avoid evaluation on empty tiles. - tile_ds = tile_ds.filter("/item/annotation", filter_annotations=True, remove_empty=True) - - tile_entities: list[OTXDataEntity] = [] - tile_attrs: list[dict] = [] - for tile in tile_ds: - tile_entity = self._convert_entity(image, tile) - # apply the same transforms as the original dataset - transformed_tile = self._apply_transforms(tile_entity) - if transformed_tile is None: - msg = "Transformed tile is None" - raise RuntimeError(msg) - tile_entities.append(transformed_tile) - tile_attrs.append(tile.attributes) - return tile_entities, tile_attrs - - -class OTXTileTrainDataset(OTXTileDataset): - """OTX tile train dataset. - - Args: - dataset (OTXDataset): OTX dataset. - tile_config (TilerConfig): Tile configuration. - """ - - def __init__(self, dataset: OTXDataset, tile_config: TileConfig) -> None: - dm_dataset = dataset.dm_subset.as_dataset() - dm_dataset = dm_dataset.transform( - OTXTileTransform, - tile_size=tile_config.tile_size, - overlap=(tile_config.overlap, tile_config.overlap), - threshold_drop_ann=0.5, - ) - dm_dataset = dm_dataset.filter("/item/annotation", filter_annotations=True, remove_empty=True) - # Include original dataset for training - dm_dataset.update(dataset.dm_subset.as_dataset()) - dm_subset = DatasetSubset(dm_dataset, dataset.dm_subset.name) - dataset.dm_subset = dm_subset - dataset.ids = [item.id for item in dm_subset] - super().__init__(dataset, tile_config) - - -class OTXTileDetTestDataset(OTXTileDataset): - """OTX tile detection test dataset. - - OTXTileDetTestDataset wraps a list of tiles (DetDataEntity) into a single TileDetDataEntity for testing/predicting. - - Args: - dataset (OTXDetDataset): OTX detection dataset. - tile_config (TilerConfig): Tile configuration. - """ - - def __init__(self, dataset: OTXDetectionDataset, tile_config: TileConfig) -> None: - super().__init__(dataset, tile_config) - - @property - def collate_fn(self) -> Callable: - """Collate function for tile detection test dataset.""" - return TileBatchDetDataEntity.collate_fn - - def _get_item_impl(self, index: int) -> TileDetDataEntity: # type: ignore[override] - """Get item implementation. - - Transform a single dataset item to multiple tiles using Datumaro tiling plugin, and - wrap tiles into a single TileDetDataEntity. - - Args: - index (int): Index of the dataset item. - - Returns: - TileDetDataEntity: tile detection data entity that wraps a list of detection data entities. - - Note: - Ignoring [override] check is necessary here since OTXDataset._get_item_impl exclusively permits - the return of OTXDataEntity. Nevertheless, in instances involving tiling, it becomes - imperative to encapsulate tiles within a unified entity, namely TileDetDataEntity. - """ - item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = item.media_as(Image) - img_data, img_shape = self._get_img_data_and_shape(img) - - bbox_anns = [ann for ann in item.annotations if isinstance(ann, Bbox)] - - bboxes = ( - np.stack([ann.points for ann in bbox_anns], axis=0).astype(np.float32) - if len(bbox_anns) > 0 - else np.zeros((0, 4), dtype=np.float32) - ) - labels = torch.as_tensor([ann.label for ann in bbox_anns]) - - tile_entities, tile_attrs = self.get_tiles(img_data, item) - - return TileDetDataEntity( - num_tiles=len(tile_entities), - entity_list=tile_entities, - tile_attr_list=tile_attrs, - ori_img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - ), - ori_bboxes=tv_tensors.BoundingBoxes( - bboxes, - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_shape, - ), - ori_labels=labels, - ) - - def _convert_entity(self, image: np.ndarray, dataset_item: DatasetItem) -> DetDataEntity: - """Convert a tile datumaro dataset item to DetDataEntity.""" - x1, y1, w, h = dataset_item.attributes["roi"] - tile_img = image[y1 : y1 + h, x1 : x1 + w] - tile_shape = tile_img.shape[:2] - img_info = ImageInfo( - img_idx=dataset_item.attributes["id"], - img_shape=tile_shape, - ori_shape=tile_shape, - ) - return DetDataEntity( - image=tile_img, - img_info=img_info, - # we don't need tile-level annotations - bboxes=tv_tensors.BoundingBoxes( - [], - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=tile_shape, - ), - labels=torch.as_tensor([]), - ) - - -class OTXTileInstSegTestDataset(OTXTileDataset): - """OTX tile inst-seg test dataset. - - OTXTileDetTestDataset wraps a list of tiles (InstanceSegDataEntity) into a single TileDetDataEntity - for testing/predicting. - - Args: - dataset (OTXInstanceSegDataset): OTX inst-seg dataset. - tile_config (TilerConfig): Tile configuration. - """ - - def __init__(self, dataset: OTXInstanceSegDataset, tile_config: TileConfig) -> None: - super().__init__(dataset, tile_config) - - @property - def collate_fn(self) -> Callable: - """Collate function for tile inst-seg test dataset.""" - return TileBatchInstSegDataEntity.collate_fn - - def _get_item_impl(self, index: int) -> TileInstSegDataEntity: # type: ignore[override] - """Get item implementation. - - Transform a single dataset item to multiple tiles using Datumaro tiling plugin, and - wrap tiles into a single TileInstSegDataEntity. - - Args: - index (int): Index of the dataset item. - - Returns: - TileInstSegDataEntity: tile inst-seg data entity that wraps a list of inst-seg data entities. - - Note: - Ignoring [override] check is necessary here since OTXDataset._get_item_impl exclusively permits - the return of OTXDataEntity. Nevertheless, in instances involving tiling, it becomes - imperative to encapsulate tiles within a unified entity, namely TileInstSegDataEntity. - """ - item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = item.media_as(Image) - img_data, img_shape = self._get_img_data_and_shape(img) - - gt_bboxes, gt_labels, gt_masks, gt_polygons = [], [], [], [] - - for annotation in item.annotations: - if isinstance(annotation, Polygon): - bbox = np.array(annotation.get_bbox(), dtype=np.float32) - gt_bboxes.append(bbox) - gt_labels.append(annotation.label) - - if self._dataset.include_polygons: - gt_polygons.append(annotation) - else: - gt_masks.append(polygon_to_bitmap([annotation], *img_shape)[0]) - - # convert xywh to xyxy format - bboxes = np.array(gt_bboxes, dtype=np.float32) - bboxes[:, 2:] += bboxes[:, :2] - - masks = np.stack(gt_masks, axis=0) if gt_masks else np.zeros((0, *img_shape), dtype=bool) - labels = np.array(gt_labels, dtype=np.int64) - - tile_entities, tile_attrs = self.get_tiles(img_data, item) - - return TileInstSegDataEntity( - num_tiles=len(tile_entities), - entity_list=tile_entities, - tile_attr_list=tile_attrs, - ori_img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - ), - ori_bboxes=tv_tensors.BoundingBoxes( - bboxes, - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_shape, - ), - ori_labels=torch.as_tensor(labels), - ori_masks=tv_tensors.Mask(masks, dtype=torch.uint8), - ori_polygons=gt_polygons, - ) - - def _convert_entity(self, image: np.ndarray, dataset_item: DatasetItem) -> InstanceSegDataEntity: - """Convert a tile dataset item to InstanceSegDataEntity.""" - x1, y1, w, h = dataset_item.attributes["roi"] - tile_img = image[y1 : y1 + h, x1 : x1 + w] - tile_shape = tile_img.shape[:2] - img_info = ImageInfo( - img_idx=dataset_item.attributes["id"], - img_shape=tile_shape, - ori_shape=tile_shape, - ) - return InstanceSegDataEntity( - image=tile_img, - img_info=img_info, - # we don't need tile-level annotations - bboxes=tv_tensors.BoundingBoxes( - [], - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=tile_shape, - ), - labels=torch.as_tensor([]), - masks=tv_tensors.Mask(np.zeros((0, *tile_shape), dtype=bool)), - polygons=[], - ) diff --git a/src/otx/core/data/dataset/visual_prompting.py b/src/otx/core/data/dataset/visual_prompting.py deleted file mode 100644 index 98663681531..00000000000 --- a/src/otx/core/data/dataset/visual_prompting.py +++ /dev/null @@ -1,256 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTXVisualPromptingDataset.""" - -from __future__ import annotations - -from collections import defaultdict -from functools import partial -from typing import Callable - -import torch -import torchvision.transforms.v2.functional as F # noqa: N812 -from datumaro import Bbox as dmBbox -from datumaro import DatasetSubset -from datumaro import Image as dmImage -from datumaro import Mask as dmMask -from datumaro import Points as dmPoints -from datumaro import Polygon as dmPolygon -from torchvision import tv_tensors - -from otx.core.data.entity.base import ImageInfo, Points -from otx.core.data.entity.visual_prompting import ( - VisualPromptingBatchDataEntity, - VisualPromptingDataEntity, - ZeroShotVisualPromptingBatchDataEntity, - ZeroShotVisualPromptingDataEntity, -) -from otx.core.utils.mask_util import polygon_to_bitmap - -from .base import OTXDataset, Transforms - - -class OTXVisualPromptingDataset(OTXDataset[VisualPromptingDataEntity]): - """OTXDataset class for visual prompting. - - Args: - dm_subset (DatasetSubset): The subset of the dataset. - transforms (Transforms): Data transformations to be applied. - **kwargs: Additional keyword arguments passed to the base class. - """ - - def __init__( - self, - dm_subset: DatasetSubset, - transforms: Transforms, - use_bbox: bool = True, - use_point: bool = False, - stack_images: bool = True, - **kwargs, - ) -> None: - super().__init__(dm_subset, transforms, stack_images=stack_images, **kwargs) - if not use_bbox and not use_point: - # if both are False, use bbox as default - use_bbox = True - self.prob = 1.0 # if using only bbox prompt - if use_bbox and use_point: - # if using both prompts, divide prob into both - self.prob = 0.5 - if not use_bbox and use_point: - # if using only point prompt - self.prob = 0.0 - - def _get_item_impl(self, index: int) -> VisualPromptingDataEntity | None: - item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = item.media_as(dmImage) - img_data, img_shape = self._get_img_data_and_shape(img) - - gt_bboxes, gt_points = [], [] - gt_masks = defaultdict(list) - gt_polygons = defaultdict(list) - gt_labels = defaultdict(list) - - for annotation in item.annotations: - if isinstance(annotation, dmPolygon): - mask = tv_tensors.Mask(polygon_to_bitmap([annotation], *img_shape)[0]) - mask_points = torch.nonzero(mask) - if len(mask_points[0]) == 0: - # skip very small region - continue - - if torch.rand(1) < self.prob: - # get bbox - bbox = tv_tensors.BoundingBoxes( - annotation.get_bbox(), - format=tv_tensors.BoundingBoxFormat.XYWH, - canvas_size=img_shape, - dtype=torch.float32, - ) - bbox = F._meta.convert_bounding_box_format( # noqa: SLF001 - bbox, - new_format=tv_tensors.BoundingBoxFormat.XYXY, - ) - gt_bboxes.append(bbox) - gt_labels["bboxes"].append(annotation.label) - gt_masks["bboxes"].append(mask) - gt_polygons["bboxes"].append(annotation) - else: - # get point - if self.dm_subset.name == "train": - # get random point from the mask - idx_chosen = torch.randperm(len(mask_points[0]))[0] - point = Points( - (mask_points[1][idx_chosen], mask_points[0][idx_chosen]), - canvas_size=img_shape, - dtype=torch.float32, - ) - else: - # get center point - point = Points( - torch.tensor(annotation.get_points()).mean(dim=0), - canvas_size=img_shape, - dtype=torch.float32, - ) - gt_points.append(point) - gt_labels["points"].append(annotation.label) - gt_masks["points"].append(mask) - gt_polygons["points"].append(annotation) - - assert ( # noqa: S101 - len(gt_bboxes) > 0 or len(gt_points) > 0 - ), "At least one of both #bounding box and #point prompts must be greater than 0." - - bboxes = tv_tensors.wrap(torch.cat(gt_bboxes, dim=0), like=gt_bboxes[0]) if len(gt_bboxes) > 0 else None - points = tv_tensors.wrap(torch.stack(gt_points, dim=0), like=gt_points[0]) if len(gt_points) > 0 else None - labels = {prompt_type: torch.as_tensor(values, dtype=torch.int64) for prompt_type, values in gt_labels.items()} - masks = tv_tensors.Mask( - torch.stack(gt_masks.get("bboxes", []) + gt_masks.get("points", []), dim=0), - dtype=torch.uint8, - ) - polygons = gt_polygons.get("bboxes", []) + gt_polygons.get("points", []) - - # set entity without masks to avoid resizing masks - entity = VisualPromptingDataEntity( - image=img_data, - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - ), - masks=None, - labels=labels, - polygons=polygons, - points=points, - bboxes=bboxes, - ) - transformed_entity = self._apply_transforms(entity) - - # insert masks to transformed_entity - transformed_entity.masks = masks # type: ignore[union-attr] - return transformed_entity - - @property - def collate_fn(self) -> Callable: - """Collection function to collect VisualPromptingDataEntity into VisualPromptingBatchDataEntity in data loader.""" # noqa: E501 - return partial(VisualPromptingBatchDataEntity.collate_fn, stack_images=self.stack_images) - - -class OTXZeroShotVisualPromptingDataset(OTXDataset[ZeroShotVisualPromptingDataEntity]): - """OTXDataset class for zero-shot visual prompting. - - Args: - dm_subset (DatasetSubset): The subset of the dataset. - transforms (Transforms): Data transformations to be applied. - **kwargs: Additional keyword arguments passed to the base class. - """ - - def __init__( - self, - dm_subset: DatasetSubset, - transforms: Transforms, - use_bbox: bool = True, - use_point: bool = False, - stack_images: bool = True, - **kwargs, - ) -> None: - super().__init__(dm_subset, transforms, stack_images=stack_images, **kwargs) - if not use_bbox and not use_point: - # if both are False, use bbox as default - use_bbox = True - self.prob = 1.0 # if using only bbox prompt - if use_bbox and use_point: - # if using both prompts, divide prob into both - self.prob = 0.5 - if not use_bbox and use_point: - # if using only point prompt - self.prob = 0.0 - - def _get_item_impl(self, index: int) -> ZeroShotVisualPromptingDataEntity | None: - item = self.dm_subset.get(id=self.ids[index], subset=self.dm_subset.name) - img = item.media_as(dmImage) - img_data, img_shape = self._get_img_data_and_shape(img) - - gt_prompts, gt_masks, gt_polygons, gt_labels = [], [], [], [] - for annotation in item.annotations: - if isinstance(annotation, dmPolygon): - # generate prompts from polygon - mask = tv_tensors.Mask(polygon_to_bitmap([annotation], *img_shape)[0]) - mask_points = torch.nonzero(mask) - if len(mask_points[0]) == 0: - # skip very small region - continue - - if torch.rand(1) < self.prob: - # get bbox - bbox = tv_tensors.BoundingBoxes( - annotation.get_bbox(), - format=tv_tensors.BoundingBoxFormat.XYWH, - canvas_size=img_shape, - dtype=torch.float32, - ) - bbox = F._meta.convert_bounding_box_format( # noqa: SLF001 - bbox, - new_format=tv_tensors.BoundingBoxFormat.XYXY, - ) - gt_prompts.append(bbox) - else: - # get center point - point = Points( - torch.tensor(annotation.get_points()).mean(dim=0), - canvas_size=img_shape, - dtype=torch.float32, - ) - gt_prompts.append(point) - - gt_labels.append(annotation.label) - gt_masks.append(mask) - gt_polygons.append(annotation) - - # TODO(sungchul): for mask, bounding box, and point annotation # noqa: TD003 - elif isinstance(annotation, (dmBbox, dmMask, dmPoints)): - pass - - assert len(gt_prompts) > 0, "#prompts must be greater than 0." # noqa: S101 - - labels = torch.as_tensor(gt_labels, dtype=torch.int64) - masks = tv_tensors.Mask(torch.stack(gt_masks, dim=0), dtype=torch.uint8) - - # set entity without masks to avoid resizing masks - return ZeroShotVisualPromptingDataEntity( - image=F.to_image(img_data), - img_info=ImageInfo( - img_idx=index, - img_shape=img_shape, - ori_shape=img_shape, - ), - masks=masks, - labels=labels, - polygons=gt_polygons, - prompts=gt_prompts, - ) - - @property - def collate_fn(self) -> Callable: - """Collection function to collect ZeroShotVisualPromptingDataEntity into ZeroShotVisualPromptingBatchDataEntity in data loader.""" # noqa: E501 - return partial(ZeroShotVisualPromptingBatchDataEntity.collate_fn, stack_images=self.stack_images) diff --git a/src/otx/core/data/entity/__init__.py b/src/otx/core/data/entity/__init__.py deleted file mode 100644 index b135e076e24..00000000000 --- a/src/otx/core/data/entity/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX data entities.""" diff --git a/src/otx/core/data/entity/action_classification.py b/src/otx/core/data/entity/action_classification.py deleted file mode 100644 index a10ac00c48c..00000000000 --- a/src/otx/core/data/entity/action_classification.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX action data entities.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from otx.core.data.entity.base import ( - OTXBatchDataEntity, - OTXBatchPredEntity, - OTXBatchPredEntityWithXAI, - OTXDataEntity, - OTXPredEntity, - OTXPredEntityWithXAI, -) -from otx.core.data.entity.utils import register_pytree_node -from otx.core.types.task import OTXTaskType - -if TYPE_CHECKING: - from datumaro.components.media import Video - from torch import LongTensor - - -@register_pytree_node -@dataclass -class ActionClsDataEntity(OTXDataEntity): - """Data entity for action classification task. - - Args: - video: Video object. - labels: Video's action labels. - """ - - video: Video - labels: LongTensor - - def to_tv_image(self) -> ActionClsDataEntity: - """Convert `self.image` to TorchVision Image if it is a Numpy array (inplace operation). - - Action classification data do not have image, so this will return itself. - """ - return self - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.ACTION_CLASSIFICATION - - -@dataclass -class ActionClsPredEntity(ActionClsDataEntity, OTXPredEntity): - """Data entity to represent the action classification model's output prediction.""" - - -@dataclass -class ActionClsPredEntityWithXAI(ActionClsDataEntity, OTXPredEntityWithXAI): - """Data entity to represent the detection model output prediction with explanations.""" - - -@dataclass -class ActionClsBatchDataEntity(OTXBatchDataEntity[ActionClsDataEntity]): - """Batch data entity for action classification. - - Args: - labels(list[LongTensor]): A list of labels of videos. - """ - - labels: list[LongTensor] - - @property - def task(self) -> OTXTaskType: - """OTX task type definition.""" - return OTXTaskType.ACTION_CLASSIFICATION - - @classmethod - def collate_fn( - cls, - entities: list[ActionClsDataEntity], - stack_images: bool = True, - ) -> ActionClsBatchDataEntity: - """Collection function to collect `ActionClsDataEntity` into `ActionClsBatchDataEntity`.""" - batch_data = super().collate_fn(entities, stack_images=stack_images) - return ActionClsBatchDataEntity( - batch_size=batch_data.batch_size, - images=batch_data.images, - imgs_info=batch_data.imgs_info, - labels=[entity.labels for entity in entities], - ) - - def pin_memory(self) -> ActionClsBatchDataEntity: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.labels = [label.pin_memory() for label in self.labels] - return self - - -@dataclass -class ActionClsBatchPredEntity(ActionClsBatchDataEntity, OTXBatchPredEntity): - """Data entity to represent model output predictions for action classification task.""" - - -@dataclass -class ActionClsBatchPredEntityWithXAI(ActionClsBatchDataEntity, OTXBatchPredEntityWithXAI): - """Data entity to represent model output predictions for multi-class classification task with explanations.""" diff --git a/src/otx/core/data/entity/action_detection.py b/src/otx/core/data/entity/action_detection.py deleted file mode 100644 index 74b753977dc..00000000000 --- a/src/otx/core/data/entity/action_detection.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX action data entities.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from torchvision import tv_tensors - -from otx.core.data.entity.base import ( - OTXBatchDataEntity, - OTXBatchPredEntity, - OTXDataEntity, - OTXPredEntity, -) -from otx.core.data.entity.utils import register_pytree_node -from otx.core.types.task import OTXTaskType - -if TYPE_CHECKING: - from torch import LongTensor - - -@register_pytree_node -@dataclass -class ActionDetDataEntity(OTXDataEntity): - """Data entity for action classification task. - - Args: - bboxes: 2D bounding boxes for actors. - labels: One-hot vector of video's action labels. - frame_path: Data media's file path for getting proper meta information. - proposals: Pre-calculated actor proposals. - """ - - bboxes: tv_tensors.BoundingBoxes - labels: LongTensor - frame_path: str - proposals: tv_tensors.BoundingBoxes - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.ACTION_DETECTION - - -@dataclass -class ActionDetPredEntity(ActionDetDataEntity, OTXPredEntity): - """Data entity to represent the action classification model's output prediction.""" - - -@dataclass -class ActionDetBatchDataEntity(OTXBatchDataEntity[ActionDetDataEntity]): - """Batch data entity for action classification. - - Args: - bboxes(list[tv_tensors.BoundingBoxes]): A list of bounding boxes of videos. - labels(list[LongTensor]): A list of labels of videos. - """ - - bboxes: list[tv_tensors.BoundingBoxes] - labels: list[LongTensor] - proposals: list[tv_tensors.BoundingBoxes] - - @property - def task(self) -> OTXTaskType: - """OTX task type definition.""" - return OTXTaskType.ACTION_DETECTION - - @classmethod - def collate_fn( - cls, - entities: list[ActionDetDataEntity], - stack_images: bool = True, - ) -> ActionDetBatchDataEntity: - """Collection function to collect `ActionClsDataEntity` into `ActionClsBatchDataEntity`.""" - batch_data = super().collate_fn(entities, stack_images=stack_images) - return ActionDetBatchDataEntity( - batch_size=batch_data.batch_size, - images=batch_data.images, - imgs_info=batch_data.imgs_info, - bboxes=[entity.bboxes for entity in entities], - labels=[entity.labels for entity in entities], - proposals=[entity.proposals for entity in entities], - ) - - def pin_memory(self) -> ActionDetBatchDataEntity: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.bboxes = [tv_tensors.wrap(bbox.pin_memory(), like=bbox) for bbox in self.bboxes] - self.labels = [label.pin_memory() for label in self.labels] - self.proposals = [tv_tensors.wrap(proposal.pin_memory(), like=proposal) for proposal in self.proposals] - return self - - -@dataclass -class ActionDetBatchPredEntity(ActionDetBatchDataEntity, OTXBatchPredEntity): - """Data entity to represent model output predictions for action classification task.""" diff --git a/src/otx/core/data/entity/anomaly/__init__.py b/src/otx/core/data/entity/anomaly/__init__.py deleted file mode 100644 index c38c954359e..00000000000 --- a/src/otx/core/data/entity/anomaly/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -"""OTX Anomaly Dataset Item and Batch Class Definitions.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - - -from .classification import ( - AnomalyClassificationBatchPrediction, - AnomalyClassificationDataBatch, - AnomalyClassificationDataItem, - AnomalyClassificationPrediction, -) -from .detection import ( - AnomalyDetectionBatchPrediction, - AnomalyDetectionDataBatch, - AnomalyDetectionDataItem, - AnomalyDetectionPrediction, -) -from .segmentation import ( - AnomalySegmentationBatchPrediction, - AnomalySegmentationDataBatch, - AnomalySegmentationDataItem, - AnomalySegmentationPrediction, -) - -__all__ = [ - # Anomaly Classification - "AnomalyClassificationDataBatch", - "AnomalyClassificationBatchPrediction", - "AnomalyClassificationDataItem", - "AnomalyClassificationPrediction", - # Anomaly Segmentation - "AnomalySegmentationBatchPrediction", - "AnomalySegmentationDataBatch", - "AnomalySegmentationDataItem", - "AnomalySegmentationPrediction", - # Anomaly Detection - "AnomalyDetectionDataItem", - "AnomalyDetectionDataBatch", - "AnomalyDetectionBatchPrediction", - "AnomalyDetectionPrediction", -] diff --git a/src/otx/core/data/entity/anomaly/classification.py b/src/otx/core/data/entity/anomaly/classification.py deleted file mode 100644 index cacdeb792c5..00000000000 --- a/src/otx/core/data/entity/anomaly/classification.py +++ /dev/null @@ -1,80 +0,0 @@ -"""OTX Anomaly Classification Dataset Item and Batch Class Definitions.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass - -import torch -from torchvision import tv_tensors - -from otx.core.data.entity.base import ( - OTXBatchDataEntity, - OTXBatchPredEntity, - OTXDataEntity, - OTXPredEntity, -) -from otx.core.data.entity.utils import register_pytree_node -from otx.core.types.task import OTXTaskType - - -@register_pytree_node # Ideally, we should not use this decorator -@dataclass -class AnomalyClassificationDataItem(OTXDataEntity): - """Anomaly classification dataset item.""" - - @property - def task(self) -> OTXTaskType: - """Task type is anomaly classification.""" - return OTXTaskType.ANOMALY_CLASSIFICATION - - label: torch.LongTensor - - -@dataclass -class AnomalyClassificationDataBatch(OTXBatchDataEntity): - """Anomaly classification batch.""" - - labels: list[torch.LongTensor] - - # This is redundant. Task is already defined in AnomalyClassificationDatasetItem - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.ANOMALY_CLASSIFICATION - - @classmethod - def collate_fn( - cls, - entities: list[AnomalyClassificationDataItem], - stack_images: bool = True, - ) -> AnomalyClassificationDataBatch: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader.""" - batch = super().collate_fn(entities) - images = tv_tensors.Image(data=torch.stack(tuple(batch.images), dim=0)) if stack_images else batch.images - return AnomalyClassificationDataBatch( - batch_size=batch.batch_size, - images=images, - imgs_info=batch.imgs_info, - labels=[entity.label for entity in entities], - ) - - def pin_memory(self) -> AnomalyClassificationDataBatch: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.labels = [label.pin_memory() for label in self.labels] - return self - - -@dataclass -class AnomalyClassificationPrediction(AnomalyClassificationDataItem, OTXPredEntity): - """Anomaly classification Prediction item.""" - - -@dataclass -class AnomalyClassificationBatchPrediction(AnomalyClassificationDataBatch, OTXBatchPredEntity): - """Anomaly classification batch prediction.""" - - anomaly_maps: torch.Tensor diff --git a/src/otx/core/data/entity/anomaly/detection.py b/src/otx/core/data/entity/anomaly/detection.py deleted file mode 100644 index 08a5e63805a..00000000000 --- a/src/otx/core/data/entity/anomaly/detection.py +++ /dev/null @@ -1,89 +0,0 @@ -"""OTX Anomaly Detection Dataset Item and Batch Class Definitions.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass - -import torch -from torchvision import tv_tensors - -from otx.core.data.entity.base import ( - OTXBatchDataEntity, - OTXBatchPredEntity, - OTXDataEntity, - OTXPredEntity, -) -from otx.core.data.entity.utils import register_pytree_node -from otx.core.types.task import OTXTaskType - - -@register_pytree_node -@dataclass -class AnomalyDetectionDataItem(OTXDataEntity): - """Anomaly Detection dataset item.""" - - @property - def task(self) -> OTXTaskType: - """Task type is anomaly detection.""" - return OTXTaskType.ANOMALY_DETECTION - - label: torch.LongTensor - boxes: torch.Tensor - mask: torch.Tensor - - -@dataclass -class AnomalyDetectionDataBatch(OTXBatchDataEntity): - """Anomaly Detection batch.""" - - labels: list[torch.LongTensor] - boxes: torch.Tensor - masks: torch.Tensor - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.ANOMALY_DETECTION - - @classmethod - def collate_fn( - cls, - entities: list[AnomalyDetectionDataItem], - stack_images: bool = True, - ) -> AnomalyDetectionDataBatch: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader.""" - batch = super().collate_fn(entities) - images = tv_tensors.Image(data=torch.stack(tuple(batch.images), dim=0)) if stack_images else batch.images - return AnomalyDetectionDataBatch( - batch_size=batch.batch_size, - images=images, - imgs_info=batch.imgs_info, - labels=[entity.label for entity in entities], - masks=torch.vstack([entity.mask for entity in entities]), - boxes=[entity.boxes for entity in entities], - ) - - def pin_memory(self) -> AnomalyDetectionDataBatch: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.labels = [label.pin_memory() for label in self.labels] - self.masks = self.masks.pin_memory() - self.boxes = [box.pin_memory() for box in self.boxes] - return self - - -@dataclass -class AnomalyDetectionPrediction(AnomalyDetectionDataItem, OTXPredEntity): - """Anomaly Detection Prediction item.""" - - -@dataclass -class AnomalyDetectionBatchPrediction(AnomalyDetectionDataBatch, OTXBatchPredEntity): - """Anomaly classification batch prediction.""" - - anomaly_maps: torch.Tensor - box_scores: list[torch.Tensor] - box_labels: list[torch.Tensor] diff --git a/src/otx/core/data/entity/anomaly/segmentation.py b/src/otx/core/data/entity/anomaly/segmentation.py deleted file mode 100644 index 0ec64845cd8..00000000000 --- a/src/otx/core/data/entity/anomaly/segmentation.py +++ /dev/null @@ -1,83 +0,0 @@ -"""OTX Anomaly Segmentation Dataset Item and Batch Class Definitions.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass - -import torch -from torchvision import tv_tensors - -from otx.core.data.entity.base import ( - OTXBatchDataEntity, - OTXBatchPredEntity, - OTXDataEntity, - OTXPredEntity, -) -from otx.core.data.entity.utils import register_pytree_node -from otx.core.types.task import OTXTaskType - - -@register_pytree_node -@dataclass -class AnomalySegmentationDataItem(OTXDataEntity): - """Anomaly segmentation dataset item.""" - - @property - def task(self) -> OTXTaskType: - """Task type is anomaly segmentation.""" - return OTXTaskType.ANOMALY_SEGMENTATION - - label: torch.LongTensor - mask: torch.Tensor - - -@dataclass -class AnomalySegmentationDataBatch(OTXBatchDataEntity): - """Anomaly Segmentation batch.""" - - labels: list[torch.LongTensor] - masks: torch.Tensor - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.ANOMALY_SEGMENTATION - - @classmethod - def collate_fn( - cls, - entities: list[AnomalySegmentationDataItem], - stack_images: bool = True, - ) -> AnomalySegmentationDataBatch: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader.""" - batch = super().collate_fn(entities) - images = tv_tensors.Image(data=torch.stack(tuple(batch.images), dim=0)) if stack_images else batch.images - return AnomalySegmentationDataBatch( - batch_size=batch.batch_size, - images=images, - imgs_info=batch.imgs_info, - labels=[entity.label for entity in entities], - masks=torch.vstack([entity.mask for entity in entities]), - ) - - def pin_memory(self) -> AnomalySegmentationDataBatch: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.labels = [label.pin_memory() for label in self.labels] - self.masks = self.masks.pin_memory() - return self - - -@dataclass -class AnomalySegmentationPrediction(AnomalySegmentationDataItem, OTXPredEntity): - """Anomaly Segmentation Prediction item.""" - - -@dataclass -class AnomalySegmentationBatchPrediction(AnomalySegmentationDataBatch, OTXBatchPredEntity): - """Anomaly classification batch prediction.""" - - anomaly_maps: torch.Tensor diff --git a/src/otx/core/data/entity/base.py b/src/otx/core/data/entity/base.py deleted file mode 100644 index b56ff866e04..00000000000 --- a/src/otx/core/data/entity/base.py +++ /dev/null @@ -1,676 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX base data entities.""" - - -from __future__ import annotations - -import warnings -from collections.abc import Mapping -from dataclasses import dataclass, fields -from typing import TYPE_CHECKING, Any, Dict, Generic, Iterator, TypeVar - -import torch -import torchvision.transforms.v2.functional as F # noqa: N812 -from torch import Tensor, stack -from torch.utils._pytree import tree_flatten -from torchvision import tv_tensors - -from otx.core.data.entity.utils import clamp_points, register_pytree_node -from otx.core.types.image import ImageColorChannel, ImageType -from otx.core.types.task import OTXTaskType - -if TYPE_CHECKING: - import numpy as np - - -def custom_wrap(wrappee: Tensor, *, like: tv_tensors.TVTensor, **kwargs) -> tv_tensors.TVTensor: - """Add `Points` in tv_tensors.wrap. - - If `like` is - - tv_tensors.BoundingBoxes : the `format` and `canvas_size` of `like` are assigned to `wrappee` - - Points : the `canvas_size` of `like` is assigned to `wrappee` - Unless, they are passed as `kwargs`. - - Args: - wrappee (Tensor): The tensor to convert. - like (tv_tensors.TVTensor): The reference. `wrappee` will be converted into the same subclass as `like`. - kwargs: Can contain "format" and "canvas_size" if `like` is a tv_tensor.BoundingBoxes, - or "canvas_size" if `like` is a `Points`. Ignored otherwise. - """ - if isinstance(like, tv_tensors.BoundingBoxes): - return tv_tensors.BoundingBoxes._wrap( # noqa: SLF001 - wrappee, - format=kwargs.get("format", like.format), - canvas_size=kwargs.get("canvas_size", like.canvas_size), - ) - elif isinstance(like, Points): # noqa: RET505 - return Points._wrap(wrappee, canvas_size=kwargs.get("canvas_size", like.canvas_size)) # noqa: SLF001 - else: - return wrappee.as_subclass(type(like)) - - -tv_tensors.wrap = custom_wrap - - -class ImageInfo(tv_tensors.TVTensor): - """Meta info for image. - - Attributes: - img_id: Image id - img_shape: Image shape (heigth, width) after preprocessing - ori_shape: Image shape (heigth, width) right after loading it - padding: Number of pixels to pad all borders (left, top, right, bottom) - scale_factor: Scale factor (height, width) if the image is resized during preprocessing. - Default value is `(1.0, 1.0)` when there is no resizing. However, if the image is cropped, - it will lose the scaling information and be `None`. - normalized: If true, this image is normalized with `norm_mean` and `norm_std` - norm_mean: Mean vector used to normalize this image - norm_std: Standard deviation vector used to normalize this image - image_color_channel: Color channel type of this image, RGB or BGR. - ignored_labels: Label that should be ignored in this image. Default to None. - """ - - img_idx: int - img_shape: tuple[int, int] - ori_shape: tuple[int, int] - padding: tuple[int, int, int, int] = (0, 0, 0, 0) - scale_factor: tuple[float, float] | None = (1.0, 1.0) - normalized: bool = False - norm_mean: tuple[float, float, float] = (0.0, 0.0, 0.0) - norm_std: tuple[float, float, float] = (1.0, 1.0, 1.0) - image_color_channel: ImageColorChannel = ImageColorChannel.RGB - ignored_labels: list[int] - - @classmethod - def _wrap( - cls, - dummy_tensor: Tensor, - *, - img_idx: int, - img_shape: tuple[int, int], - ori_shape: tuple[int, int], - padding: tuple[int, int, int, int] = (0, 0, 0, 0), - scale_factor: tuple[float, float] | None = (1.0, 1.0), - normalized: bool = False, - norm_mean: tuple[float, float, float] = (0.0, 0.0, 0.0), - norm_std: tuple[float, float, float] = (1.0, 1.0, 1.0), - image_color_channel: ImageColorChannel = ImageColorChannel.RGB, - ignored_labels: list[int] | None = None, - ) -> ImageInfo: - image_info = dummy_tensor.as_subclass(cls) - image_info.img_idx = img_idx - image_info.img_shape = img_shape - image_info.ori_shape = ori_shape - image_info.padding = padding - image_info.scale_factor = scale_factor - image_info.normalized = normalized - image_info.norm_mean = norm_mean - image_info.norm_std = norm_std - image_info.image_color_channel = image_color_channel - image_info.ignored_labels = ignored_labels if ignored_labels else [] - return image_info - - def __new__( # noqa: D102 - cls, - img_idx: int, - img_shape: tuple[int, int], - ori_shape: tuple[int, int], - padding: tuple[int, int, int, int] = (0, 0, 0, 0), - scale_factor: tuple[float, float] | None = (1.0, 1.0), - normalized: bool = False, - norm_mean: tuple[float, float, float] = (0.0, 0.0, 0.0), - norm_std: tuple[float, float, float] = (1.0, 1.0, 1.0), - image_color_channel: ImageColorChannel = ImageColorChannel.RGB, - ignored_labels: list[int] | None = None, - ) -> ImageInfo: - return cls._wrap( - dummy_tensor=Tensor(), - img_idx=img_idx, - img_shape=img_shape, - ori_shape=ori_shape, - padding=padding, - scale_factor=scale_factor, - normalized=normalized, - norm_mean=norm_mean, - norm_std=norm_std, - image_color_channel=image_color_channel, - ignored_labels=ignored_labels, - ) - - @classmethod - def _wrap_output( - cls, - output: Tensor, - args: tuple[()] = (), - kwargs: Mapping[str, Any] | None = None, - ) -> ImageType: - """Wrap an output (`torch.Tensor`) obtained from PyTorch function. - - For example, this function will be called when - - >>> img_info = ImageInfo(img_idx=0, img_shape=(10, 10), ori_shape=(10, 10)) - >>> `_wrap_output()` will be called after the PyTorch function `to()` is called - >>> img_info = img_info.to(device=torch.cuda) - """ - flat_params, _ = tree_flatten(args + (tuple(kwargs.values()) if kwargs else ())) - - if isinstance(output, Tensor) and not isinstance(output, ImageInfo): - image_info = next(x for x in flat_params if isinstance(x, ImageInfo)) - output = ImageInfo._wrap( - dummy_tensor=output, - img_idx=image_info.img_idx, - img_shape=image_info.img_shape, - ori_shape=image_info.ori_shape, - padding=image_info.padding, - scale_factor=image_info.scale_factor, - normalized=image_info.normalized, - norm_mean=image_info.norm_mean, - norm_std=image_info.norm_std, - image_color_channel=image_info.image_color_channel, - ignored_labels=image_info.ignored_labels, - ) - elif isinstance(output, (tuple, list)): - image_infos = [x for x in flat_params if isinstance(x, ImageInfo)] - output = type(output)( - ImageInfo._wrap( - dummy_tensor=dummy_tensor, - img_idx=image_info.img_idx, - img_shape=image_info.img_shape, - ori_shape=image_info.ori_shape, - padding=image_info.padding, - scale_factor=image_info.scale_factor, - normalized=image_info.normalized, - norm_mean=image_info.norm_mean, - norm_std=image_info.norm_std, - image_color_channel=image_info.image_color_channel, - ignored_labels=image_info.ignored_labels, - ) - for dummy_tensor, image_info in zip(output, image_infos) - ) - return output - - @property - def pad_shape(self) -> tuple[int, int]: - """Image shape after padding.""" - h_img, w_img = self.img_shape - left, top, right, bottom = self.padding - return (h_img + top + bottom, w_img + left + right) - - @pad_shape.setter - def pad_shape(self, pad_shape: tuple[int, int] | tuple[int, int, int]) -> None: - """Set padding from the given pad shape. - - Args: - pad_shape: Padded image shape (height, width) or (height, width, channel) - which should be larger than this image shape. - In addition, the padded image should be padded - only for the right or bottom borders. - """ - h_img, w_img = self.img_shape - h_pad, w_pad = pad_shape[0], pad_shape[1] - - if h_pad < h_img or w_pad < w_img: - raise ValueError(pad_shape) - - left = top = 0 - right = w_pad - w_img - bottom = h_pad - h_img - self.padding = (left, top, right, bottom) - - def __repr__(self) -> str: - return ( - "ImageInfo(" - f"img_idx={self.img_idx}, " - f"img_shape={self.img_shape}, " - f"ori_shape={self.ori_shape}, " - f"padding={self.padding}, " - f"scale_factor={self.scale_factor}, " - f"normalized={self.normalized}, " - f"norm_mean={self.norm_mean}, " - f"norm_std={self.norm_std}, " - f"image_color_channel={self.image_color_channel}, " - f"ignored_labels={self.ignored_labels})" - ) - - -@F.register_kernel(functional=F.resize, tv_tensor_cls=ImageInfo) -def _resize_image_info(image_info: ImageInfo, size: list[int], **kwargs) -> ImageInfo: # noqa: ARG001 - """Register ImageInfo to TorchVision v2 resize kernel.""" - if len(size) == 2: - image_info.img_shape = (size[0], size[1]) - elif len(size) == 1: - image_info.img_shape = (size[0], size[0]) - else: - raise ValueError(size) - - ori_h, ori_w = image_info.ori_shape - new_h, new_w = image_info.img_shape - image_info.scale_factor = (new_h / ori_h, new_w / ori_w) - return image_info - - -@F.register_kernel(functional=F.crop, tv_tensor_cls=ImageInfo) -def _crop_image_info( - image_info: ImageInfo, - height: int, - width: int, - **kwargs, # noqa: ARG001 -) -> ImageInfo: - """Register ImageInfo to TorchVision v2 resize kernel.""" - image_info.img_shape = (height, width) - image_info.scale_factor = None - return image_info - - -@F.register_kernel(functional=F.resized_crop, tv_tensor_cls=ImageInfo) -def _resized_crop_image_info( - image_info: ImageInfo, - size: list[int], - **kwargs, # noqa: ARG001 -) -> ImageInfo: - """Register ImageInfo to TorchVision v2 resize kernel.""" - if len(size) == 2: - image_info.img_shape = (size[0], size[1]) - elif len(size) == 1: - image_info.img_shape = (size[0], size[0]) - else: - raise ValueError(size) - - image_info.scale_factor = None - return image_info - - -@F.register_kernel(functional=F.center_crop, tv_tensor_cls=ImageInfo) -def _center_crop_image_info( - image_info: ImageInfo, - output_size: list[int], - **kwargs, # noqa: ARG001 -) -> ImageInfo: - """Register ImageInfo to TorchVision v2 resize kernel.""" - img_shape = F._geometry._center_crop_parse_output_size(output_size) # noqa: SLF001 - image_info.img_shape = (img_shape[0], img_shape[1]) - - image_info.scale_factor = None - return image_info - - -@F.register_kernel(functional=F.pad, tv_tensor_cls=ImageInfo) -def _pad_image_info( - image_info: ImageInfo, - padding: int | list[int], - **kwargs, # noqa: ARG001 -) -> ImageInfo: - """Register ImageInfo to TorchVision v2 resize kernel.""" - left, right, top, bottom = F._geometry._parse_pad_padding(padding) # noqa: SLF001 - image_info.padding = (left, top, right, bottom) - return image_info - - -@F.register_kernel(functional=F.normalize, tv_tensor_cls=ImageInfo) -def _normalize_image_info( - image_info: ImageInfo, - mean: list[float], - std: list[float], - **kwargs, # noqa: ARG001 -) -> ImageInfo: - image_info.normalized = True - image_info.norm_mean = (mean[0], mean[1], mean[2]) - image_info.norm_std = (std[0], std[1], std[2]) - return image_info - - -class Points(tv_tensors.TVTensor): - """`torch.Tensor` subclass for points. - - Attributes: - data: Any data that can be turned into a tensor with `torch.as_tensor`. - canvas_size (two-tuple of ints): Height and width of the corresponding image or video. - dtype (torch.dtype, optional): Desired data type of the point. If omitted, will be inferred from `data`. - device (torch.device, optional): Desired device of the point. If omitted and `data` is a - `torch.Tensor`, the device is taken from it. Otherwise, the point is constructed on the CPU. - requires_grad (bool, optional): Whether autograd should record operations on the point. If omitted and - `data` is a `torch.Tensor`, the value is taken from it. Otherwise, defaults to `False`. - """ - - canvas_size: tuple[int, int] - - @classmethod - def _wrap(cls, tensor: Tensor, *, canvas_size: tuple[int, int]) -> Points: - points = tensor.as_subclass(cls) - points.canvas_size = canvas_size - return points - - def __new__( # noqa: D102 - cls, - data: Any, # noqa: ANN401 - *, - canvas_size: tuple[int, int], - dtype: torch.dtype | None = None, - device: torch.device | str | int | None = None, - requires_grad: bool | None = None, - ) -> Points: - tensor = cls._to_tensor(data, dtype=dtype, device=device, requires_grad=requires_grad) - return cls._wrap(tensor, canvas_size=canvas_size) - - @classmethod - def _wrap_output( - cls, - output: Tensor, - args: tuple[()] = (), - kwargs: Mapping[str, Any] | None = None, - ) -> Points: - flat_params, _ = tree_flatten(args + (tuple(kwargs.values()) if kwargs else ())) - first_point_from_args = next(x for x in flat_params if isinstance(x, Points)) - canvas_size = first_point_from_args.canvas_size - - if isinstance(output, Tensor) and not isinstance(output, Points): - output = Points._wrap(output, canvas_size=canvas_size) - elif isinstance(output, (tuple, list)): - output = type(output)(Points._wrap(part, canvas_size=canvas_size) for part in output) - return output - - def __repr__(self, *, tensor_contents: Any = None) -> str: # noqa: ANN401 - return self._make_repr(canvas_size=self.canvas_size) - - -def resize_points( - points: torch.Tensor, - canvas_size: tuple[int, int], - size: list[int], - max_size: int | None = None, -) -> tuple[torch.Tensor, tuple[int, int]]: - """Resize points.""" - old_height, old_width = canvas_size - new_height, new_width = F._geometry._compute_resized_output_size( # noqa: SLF001 - canvas_size, - size=size, - max_size=max_size, - ) - - if (new_height, new_width) == (old_height, old_width): - return points, canvas_size - - w_ratio = new_width / old_width - h_ratio = new_height / old_height - ratios = torch.tensor([w_ratio, h_ratio], device=points.device) - return ( - points.mul(ratios).to(points.dtype), - (new_height, new_width), - ) - - -@F.register_kernel(functional=F.resize, tv_tensor_cls=Points) -def _resize_points_dispatch( - inpt: Points, - size: list[int], - max_size: int | None = None, - **kwargs, # noqa: ARG001 -) -> Points: - output, canvas_size = resize_points( - inpt.as_subclass(torch.Tensor), - inpt.canvas_size, - size, - max_size=max_size, - ) - return tv_tensors.wrap(output, like=inpt, canvas_size=canvas_size) - - -def pad_points( - points: torch.Tensor, - canvas_size: tuple[int, int], - padding: list[int], - padding_mode: str = "constant", -) -> tuple[torch.Tensor, tuple[int, int]]: - """Pad points.""" - if padding_mode not in ["constant"]: - # TODO(sungchul): add support of other padding modes # noqa: TD003 - raise ValueError(f"Padding mode '{padding_mode}' is not supported with bounding boxes") # noqa: EM102, TRY003 - - left, right, top, bottom = F._geometry._parse_pad_padding(padding) # noqa: SLF001 - - pad = [left, top] - points = points + torch.tensor(pad, dtype=points.dtype, device=points.device) - - height, width = canvas_size - height += top + bottom - width += left + right - canvas_size = (height, width) - - return clamp_points(points, canvas_size=canvas_size), canvas_size - - -@F.register_kernel(functional=F.pad, tv_tensor_cls=Points) -def _pad_bounding_boxes_dispatch( - inpt: Points, - padding: list[int], - padding_mode: str = "constant", - **kwargs, # noqa: ARG001 -) -> Points: - output, canvas_size = pad_points( - inpt.as_subclass(torch.Tensor), - canvas_size=inpt.canvas_size, - padding=padding, - padding_mode=padding_mode, - ) - return tv_tensors.wrap(output, like=inpt, canvas_size=canvas_size) - - -@F.register_kernel(functional=F.get_size, tv_tensor_cls=Points) -def get_size_points(point: Points) -> list[int]: - """Get size of points.""" - return list(point.canvas_size) - - -T_OTXDataEntity = TypeVar( - "T_OTXDataEntity", - bound="OTXDataEntity", -) - - -@register_pytree_node -@dataclass -class OTXDataEntity(Mapping): - """Base data entity for OTX. - - This entity is the output of each OTXDataset, - which can be go through the input preprocessing tranforms. - - :param task: OTX task definition - :param image: Image tensor or list of Image tensor which can have different type according to `image_type` - 1) `image_type=ImageType.NUMPY`: H x W x C numpy image tensor - 2) `image_type=ImageType.TV_IMAGE`: C x H x W torchvision image tensor - 3) `image_type=ImageType.NUMPY_LIST`: List of H x W x C numpy image tensors - 3) `image_type=ImageType.TV_IMAGE_LIST`: List of C x H x W torchvision image tensors - :param imgs_info: Meta information for images - """ - - image: np.ndarray | tv_tensors.Image | list[np.ndarray] | list[tv_tensors.Image] - img_info: ImageInfo - - @property - def task(self) -> OTXTaskType: - """OTX task type definition.""" - msg = "OTXTaskType is not defined." - raise RuntimeError(msg) - - @property - def image_type(self) -> ImageType: - """Image type definition.""" - return ImageType.get_image_type(self.image) - - def to_tv_image(self: T_OTXDataEntity) -> T_OTXDataEntity: - """Convert `self.image` to TorchVision Image if it is a Numpy array (inplace operation).""" - if isinstance(self.image, tv_tensors.Image): - return self - - self.image = F.to_image(self.image) - return self - - def __iter__(self) -> Iterator[str]: - for field in fields(self): - yield field.name - - def __getitem__(self, key: str) -> Any: # noqa: ANN401 - return getattr(self, key) - - def __len__(self) -> int: - """Get the number of fields in this data entity.""" - return len(fields(self)) - - -@dataclass -class OTXPredEntity(OTXDataEntity): - """Data entity to represent the model output prediction.""" - - score: np.ndarray | Tensor - - -@dataclass -class OTXPredEntityWithXAI(OTXPredEntity): - """Data entity to represent model output prediction with explanations.""" - - saliency_map: np.ndarray | Tensor - feature_vector: np.ndarray | list - - -T_OTXBatchDataEntity = TypeVar( - "T_OTXBatchDataEntity", - bound="OTXBatchDataEntity", -) - - -@dataclass -class OTXBatchDataEntity(Generic[T_OTXDataEntity]): - """Base Batch data entity for OTX. - - This entity is the output of PyTorch DataLoader, - which is the direct input of OTXModel. - - :param images: List of B numpy RGB image tensors (C x H x W) or - An image tensor stacked with B RGB image tensors (B x C x H x W) - :param imgs_info: Meta information for images - """ - - batch_size: int - images: list[tv_tensors.Image] | tv_tensors.Image - imgs_info: list[ImageInfo] - - @property - def task(self) -> OTXTaskType: - """OTX task type definition.""" - msg = "OTXTaskType is not defined." - raise RuntimeError(msg) - - @property - def images_type(self) -> ImageType: - """Images type definition.""" - return ImageType.get_image_type(self.images) - - @classmethod - def collate_fn( - cls, - entities: list[T_OTXDataEntity], - stack_images: bool = True, - ) -> OTXBatchDataEntity: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader. - - Args: - entities: List of OTX data entities. - stack_images: If True, return 4D B x C x H x W image tensor. - Otherwise return a list of 3D C x H x W image tensor. - - Returns: - Collated OTX batch data entity - """ - if (batch_size := len(entities)) == 0: - msg = "collate_fn() input should have > 0 entities" - raise RuntimeError(msg) - - task = entities[0].task - - if not all(task == entity.task for entity in entities): - msg = "collate_fn() input should include a single OTX task" - raise RuntimeError(msg) - - images = [entity.image for entity in entities] - like = next(iter(images)) - - if stack_images and not all(like.shape == entity.image.shape for entity in entities): # type: ignore[union-attr] - msg = ( - "You set stack_images as True, but not all images in the batch has same shape. " - "In this case, we cannot stack images. Some tasks, e.g., detection, " - "can have different image shapes among samples in the batch. However, if it is not your intention, " - "consider setting stack_images as False in the config." - ) - warnings.warn(msg, stacklevel=1) - stack_images = False - - return OTXBatchDataEntity( - batch_size=batch_size, - images=tv_tensors.wrap(stack(images, dim=0), like=like) if stack_images else images, - imgs_info=[entity.img_info for entity in entities], - ) - - @property - def stacked_images(self) -> tv_tensors.Image: - """A stacked image tensor (B x C x H x W). - - if its `images` field is a list of image tensors, - convert it to a 4D image tensor and return it. - Otherwise, return it as is. - """ - if isinstance(self.images, tv_tensors.Image): - return self.images - - like = next(iter(self.images)) - return tv_tensors.wrap(stack(self.images, dim=0), like=like) - - def pin_memory(self: T_OTXBatchDataEntity) -> T_OTXBatchDataEntity: - """Pin memory for member tensor variables.""" - # TODO(vinnamki): Keep track this issue - # https://github.com/pytorch/pytorch/issues/116403 - self.images = ( - [tv_tensors.wrap(image.pin_memory(), like=image) for image in self.images] - if isinstance(self.images, list) - else tv_tensors.wrap(self.images.pin_memory(), like=self.images) - ) - return self - - -T_OTXBatchPredEntity = TypeVar( - "T_OTXBatchPredEntity", - bound="OTXBatchPredEntity", -) - - -T_OTXBatchPredEntityWithXAI = TypeVar( - "T_OTXBatchPredEntityWithXAI", - bound="OTXBatchPredEntityWithXAI", -) - - -@dataclass -class OTXBatchPredEntity(OTXBatchDataEntity): - """Data entity to represent model output predictions.""" - - scores: list[np.ndarray] | list[Tensor] - - -@dataclass -class OTXBatchPredEntityWithXAI(OTXBatchPredEntity): - """Data entity to represent model output predictions with explanations.""" - - saliency_maps: list[np.ndarray] | list[Tensor] - feature_vectors: list[np.ndarray] | list[Tensor] - - -T_OTXBatchLossEntity = TypeVar( - "T_OTXBatchLossEntity", - bound="OTXBatchLossEntity", -) - - -class OTXBatchLossEntity(Dict[str, Tensor]): - """Data entity to represent model output losses.""" diff --git a/src/otx/core/data/entity/classification.py b/src/otx/core/data/entity/classification.py deleted file mode 100644 index 878fc44dc0a..00000000000 --- a/src/otx/core/data/entity/classification.py +++ /dev/null @@ -1,235 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX classification data entities.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from otx.core.data.entity.base import ( - OTXBatchDataEntity, - OTXBatchPredEntity, - OTXBatchPredEntityWithXAI, - OTXDataEntity, - OTXPredEntity, - OTXPredEntityWithXAI, -) -from otx.core.data.entity.utils import register_pytree_node -from otx.core.types.task import OTXTaskType - -if TYPE_CHECKING: - from torch import LongTensor - - -@register_pytree_node -@dataclass -class MulticlassClsDataEntity(OTXDataEntity): - """Data entity for multi-class classification task. - - :param labels: labels as integer indices - """ - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.MULTI_CLASS_CLS - - labels: LongTensor - - -@dataclass -class MulticlassClsPredEntity(MulticlassClsDataEntity, OTXPredEntity): - """Data entity to represent the multi-class classification model output prediction.""" - - -@dataclass -class MulticlassClsPredEntityWithXAI(MulticlassClsDataEntity, OTXPredEntityWithXAI): - """Data entity to represent the multi-class classification model output prediction with explanations.""" - - -@dataclass -class MulticlassClsBatchDataEntity(OTXBatchDataEntity[MulticlassClsDataEntity]): - """Data entity for multi-class classification task. - - :param labels: A list of bbox labels as integer indices - """ - - labels: list[LongTensor] - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.MULTI_CLASS_CLS - - @classmethod - def collate_fn( - cls, - entities: list[MulticlassClsDataEntity], - stack_images: bool = True, - ) -> MulticlassClsBatchDataEntity: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader.""" - batch_data = super().collate_fn(entities, stack_images=stack_images) - return MulticlassClsBatchDataEntity( - batch_size=batch_data.batch_size, - images=batch_data.images, - imgs_info=batch_data.imgs_info, - labels=[entity.labels for entity in entities], - ) - - def pin_memory(self) -> MulticlassClsBatchDataEntity: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.labels = [label.pin_memory() for label in self.labels] - return self - - -@dataclass -class MulticlassClsBatchPredEntity(MulticlassClsBatchDataEntity, OTXBatchPredEntity): - """Data entity to represent model output predictions for multi-class classification task.""" - - -@dataclass -class MulticlassClsBatchPredEntityWithXAI(MulticlassClsBatchDataEntity, OTXBatchPredEntityWithXAI): - """Data entity to represent model output predictions for multi-class classification task with explanations.""" - - -@register_pytree_node -@dataclass -class MultilabelClsDataEntity(OTXDataEntity): - """Data entity for multi-label classification task. - - :param labels: Multi labels represented as an one-hot vector. - """ - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.MULTI_LABEL_CLS - - labels: LongTensor - - -@dataclass -class MultilabelClsPredEntity(MultilabelClsDataEntity, OTXPredEntity): - """Data entity to represent the multi-label classification model output prediction.""" - - -@dataclass -class MultilabelClsPredEntityWithXAI(MultilabelClsDataEntity, OTXPredEntityWithXAI): - """Data entity to represent the multi-label classification model output prediction with explanations.""" - - -@dataclass -class MultilabelClsBatchDataEntity(OTXBatchDataEntity[MultilabelClsDataEntity]): - """Data entity for multi-label classification task. - - :param labels: A list of labels as integer indices - """ - - labels: list[LongTensor] - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.MULTI_LABEL_CLS - - @classmethod - def collate_fn( - cls, - entities: list[MultilabelClsDataEntity], - stack_images: bool = True, - ) -> MultilabelClsBatchDataEntity: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader.""" - batch_data = super().collate_fn(entities, stack_images=stack_images) - return MultilabelClsBatchDataEntity( - batch_size=batch_data.batch_size, - images=batch_data.images, - imgs_info=batch_data.imgs_info, - labels=[entity.labels for entity in entities], - ) - - def pin_memory(self) -> MultilabelClsBatchDataEntity: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.labels = [label.pin_memory() for label in self.labels] - return self - - -@dataclass -class MultilabelClsBatchPredEntity(MultilabelClsBatchDataEntity, OTXBatchPredEntity): - """Data entity to represent model output predictions for multi-label classification task.""" - - -@dataclass -class MultilabelClsBatchPredEntityWithXAI(MultilabelClsBatchDataEntity, OTXBatchPredEntityWithXAI): - """Data entity to represent model output predictions for multi-label classification task with explanations.""" - - -@register_pytree_node -@dataclass -class HlabelClsDataEntity(OTXDataEntity): - """Data entity for H-label classification task. - - :param labels: labels as integer indices - :param label_group: the group of the label - """ - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.H_LABEL_CLS - - labels: LongTensor - - -@dataclass -class HlabelClsPredEntity(HlabelClsDataEntity, OTXPredEntity): - """Data entity to represent the H-label classification model output prediction.""" - - -@dataclass -class HlabelClsPredEntityWithXAI(HlabelClsDataEntity, OTXPredEntityWithXAI): - """Data entity to represent the H-label classification model output prediction with explanation.""" - - -@dataclass -class HlabelClsBatchDataEntity(OTXBatchDataEntity[HlabelClsDataEntity]): - """Data entity for H-label classification task. - - :param labels: A list of labels as integer indices - :param label_groups: A list of label group - """ - - labels: list[LongTensor] - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.H_LABEL_CLS - - @classmethod - def collate_fn( - cls, - entities: list[HlabelClsDataEntity], - stack_images: bool = True, - ) -> HlabelClsBatchDataEntity: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader.""" - batch_data = super().collate_fn(entities, stack_images=stack_images) - return HlabelClsBatchDataEntity( - batch_size=batch_data.batch_size, - images=batch_data.images, - imgs_info=batch_data.imgs_info, - labels=[entity.labels for entity in entities], - ) - - -@dataclass -class HlabelClsBatchPredEntity(HlabelClsBatchDataEntity, OTXBatchPredEntity): - """Data entity to represent model output predictions for H-label classification task.""" - - -@dataclass -class HlabelClsBatchPredEntityWithXAI(HlabelClsBatchDataEntity, OTXBatchPredEntityWithXAI): - """Data entity to represent model output predictions for H-label classification task with explanations.""" diff --git a/src/otx/core/data/entity/detection.py b/src/otx/core/data/entity/detection.py deleted file mode 100644 index c72dd91f9d5..00000000000 --- a/src/otx/core/data/entity/detection.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX detection data entities.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from torchvision import tv_tensors - -from otx.core.data.entity.base import ( - OTXBatchDataEntity, - OTXBatchPredEntity, - OTXBatchPredEntityWithXAI, - OTXDataEntity, - OTXPredEntity, - OTXPredEntityWithXAI, -) -from otx.core.data.entity.utils import register_pytree_node -from otx.core.types.task import OTXTaskType - -if TYPE_CHECKING: - from torch import LongTensor - - -@register_pytree_node -@dataclass -class DetDataEntity(OTXDataEntity): - """Data entity for detection task. - - :param bboxes: Bbox annotations as top-left-bottom-right - (x1, y1, x2, y2) format with absolute coordinate values - :param labels: Bbox labels as integer indices - """ - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.DETECTION - - bboxes: tv_tensors.BoundingBoxes - labels: LongTensor - - -@dataclass -class DetPredEntity(DetDataEntity, OTXPredEntity): - """Data entity to represent the detection model output prediction.""" - - -@dataclass -class DetPredEntityWithXAI(DetDataEntity, OTXPredEntityWithXAI): - """Data entity to represent the detection model output prediction with explanations.""" - - -@dataclass -class DetBatchDataEntity(OTXBatchDataEntity[DetDataEntity]): - """Data entity for detection task. - - :param bboxes: A list of bbox annotations as top-left-bottom-right - (x1, y1, x2, y2) format with absolute coordinate values - :param labels: A list of bbox labels as integer indices - """ - - bboxes: list[tv_tensors.BoundingBoxes] - labels: list[LongTensor] - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.DETECTION - - @classmethod - def collate_fn( - cls, - entities: list[DetDataEntity], - stack_images: bool = True, - ) -> DetBatchDataEntity: - """Collection function to collect `DetDataEntity` into `DetBatchDataEntity` in data loader. - - Args: - entities: List of `DetDataEntity`. - stack_images: If True, return 4D B x C x H x W image tensor. - Otherwise return a list of 3D C x H x W image tensor. - - Returns: - Collated `DetBatchDataEntity` - """ - batch_data = super().collate_fn(entities, stack_images=stack_images) - return DetBatchDataEntity( - batch_size=batch_data.batch_size, - images=batch_data.images, - imgs_info=batch_data.imgs_info, - bboxes=[entity.bboxes for entity in entities], - labels=[entity.labels for entity in entities], - ) - - def pin_memory(self) -> DetBatchDataEntity: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.bboxes = [tv_tensors.wrap(bbox.pin_memory(), like=bbox) for bbox in self.bboxes] - self.labels = [label.pin_memory() for label in self.labels] - return self - - -@dataclass -class DetBatchPredEntity(DetBatchDataEntity, OTXBatchPredEntity): - """Data entity to represent model output predictions for detection task.""" - - -@dataclass -class DetBatchPredEntityWithXAI(DetBatchDataEntity, OTXBatchPredEntityWithXAI): - """Data entity to represent model output predictions for detection task with explanations.""" diff --git a/src/otx/core/data/entity/instance_segmentation.py b/src/otx/core/data/entity/instance_segmentation.py deleted file mode 100644 index 89729639f41..00000000000 --- a/src/otx/core/data/entity/instance_segmentation.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX instance segmentation data entities.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from torchvision import tv_tensors - -from otx.core.types.task import OTXTaskType - -from .base import OTXBatchDataEntity, OTXBatchPredEntity, OTXBatchPredEntityWithXAI, OTXDataEntity, OTXPredEntity - -if TYPE_CHECKING: - from datumaro import Polygon - from torch import LongTensor - - -@dataclass -class InstanceSegDataEntity(OTXDataEntity): - """Data entity for instance segmentation task. - - Attributes: - bboxes (tv_tensors.BoundingBoxes): The bounding boxes of the instances. - masks (tv_tensors.Mask): The masks of the instances. - labels (LongTensor): The labels of the instances. - polygons (list[Polygon]): The polygons of the instances. - """ - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.INSTANCE_SEGMENTATION - - bboxes: tv_tensors.BoundingBoxes - masks: tv_tensors.Mask - labels: LongTensor - polygons: list[Polygon] - - -@dataclass -class InstanceSegPredEntity(InstanceSegDataEntity, OTXPredEntity): - """Data entity to represent the detection model output prediction.""" - - -@dataclass -class InstanceSegPredEntityWithXAI(InstanceSegDataEntity, OTXBatchPredEntityWithXAI): - """Data entity to represent the detection model output prediction with explanation.""" - - -@dataclass -class InstanceSegBatchDataEntity(OTXBatchDataEntity[InstanceSegDataEntity]): - """Batch entity for InstanceSegDataEntity. - - Attributes: - bboxes (list[tv_tensors.BoundingBoxes]): List of bounding boxes. - masks (list[tv_tensors.Mask]): List of masks. - labels (list[LongTensor]): List of labels. - polygons (list[list[Polygon]]): List of polygons. - """ - - bboxes: list[tv_tensors.BoundingBoxes] - masks: list[tv_tensors.Mask] - labels: list[LongTensor] - polygons: list[list[Polygon]] - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.INSTANCE_SEGMENTATION - - @classmethod - def collate_fn( - cls, - entities: list[InstanceSegDataEntity], - stack_images: bool = True, - ) -> InstanceSegBatchDataEntity: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader. - - Args: - entities (list[InstanceSegDataEntity]): List of InstanceSegDataEntity objects. - stack_images: If True, return 4D B x C x H x W image tensor. - Otherwise return a list of 3D C x H x W image tensor. - - Returns: - InstanceSegBatchDataEntity: The collated batch data entity. - """ - batch_data = super().collate_fn(entities, stack_images=stack_images) - return InstanceSegBatchDataEntity( - batch_size=batch_data.batch_size, - images=batch_data.images, - imgs_info=batch_data.imgs_info, - bboxes=[entity.bboxes for entity in entities], - masks=[entity.masks for entity in entities], - labels=[entity.labels for entity in entities], - polygons=[entity.polygons for entity in entities], - ) - - def pin_memory(self) -> InstanceSegBatchDataEntity: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.bboxes = [tv_tensors.wrap(bbox.pin_memory(), like=bbox) for bbox in self.bboxes] - self.masks = [tv_tensors.wrap(mask.pin_memory(), like=mask) for mask in self.masks] - self.labels = [label.pin_memory() for label in self.labels] - return self - - -@dataclass -class InstanceSegBatchPredEntity(InstanceSegBatchDataEntity, OTXBatchPredEntity): - """Data entity to represent model output predictions for instance segmentation task.""" - - -@dataclass -class InstanceSegBatchPredEntityWithXAI(InstanceSegBatchDataEntity, OTXBatchPredEntityWithXAI): - """Data entity to represent model output predictions for instance segmentation task with explanations.""" diff --git a/src/otx/core/data/entity/segmentation.py b/src/otx/core/data/entity/segmentation.py deleted file mode 100644 index 457bc26989a..00000000000 --- a/src/otx/core/data/entity/segmentation.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX segmentation data entities.""" - -from __future__ import annotations - -from dataclasses import dataclass - -from torchvision import tv_tensors - -from otx.core.data.entity.base import ( - OTXBatchDataEntity, - OTXBatchPredEntity, - OTXBatchPredEntityWithXAI, - OTXDataEntity, - OTXPredEntity, - OTXPredEntityWithXAI, -) -from otx.core.data.entity.utils import register_pytree_node -from otx.core.types.task import OTXTaskType - - -@register_pytree_node -@dataclass -class SegDataEntity(OTXDataEntity): - """Data entity for segmentation task. - - :param gt_seg_map: mask annotations - """ - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.SEMANTIC_SEGMENTATION - - gt_seg_map: tv_tensors.Mask - - -@dataclass -class SegPredEntity(SegDataEntity, OTXPredEntity): - """Data entity to represent the segmentation model output prediction.""" - - -@dataclass -class SegPredEntityWithXAI(SegDataEntity, OTXPredEntityWithXAI): - """Data entity to represent the segmentation model output prediction with explanation.""" - - -@dataclass -class SegBatchDataEntity(OTXBatchDataEntity[SegDataEntity]): - """Data entity for segmentation task. - - :param masks: A list of annotations - """ - - masks: list[tv_tensors.Mask] - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.SEMANTIC_SEGMENTATION - - @classmethod - def collate_fn( - cls, - entities: list[SegDataEntity], - stack_images: bool = True, - ) -> SegBatchDataEntity: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader.""" - batch_data = super().collate_fn(entities, stack_images=stack_images) - return SegBatchDataEntity( - batch_size=batch_data.batch_size, - images=batch_data.images, - imgs_info=batch_data.imgs_info, - masks=[entity.gt_seg_map for entity in entities], - ) - - def pin_memory(self) -> SegBatchDataEntity: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.masks = [tv_tensors.wrap(mask.pin_memory(), like=mask) for mask in self.masks] - return self - - -@dataclass -class SegBatchPredEntity(SegBatchDataEntity, OTXBatchPredEntity): - """Data entity to represent model output predictions for segmentation task.""" - - -@dataclass -class SegBatchPredEntityWithXAI(SegBatchDataEntity, OTXBatchPredEntityWithXAI): - """Data entity to represent model output predictions for segmentation task with explanations.""" diff --git a/src/otx/core/data/entity/tile.py b/src/otx/core/data/entity/tile.py deleted file mode 100644 index e407328ed53..00000000000 --- a/src/otx/core/data/entity/tile.py +++ /dev/null @@ -1,251 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX tile data entities.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING, Generic, Sequence, TypeVar - -from otx.core.types.task import OTXTaskType - -from .base import ImageInfo, T_OTXBatchDataEntity, T_OTXDataEntity -from .detection import DetBatchDataEntity, DetDataEntity -from .instance_segmentation import InstanceSegBatchDataEntity, InstanceSegDataEntity - -if TYPE_CHECKING: - from datumaro import Polygon - from torch import LongTensor - from torchvision import tv_tensors - - -T_OTXTileBatchDataEntity = TypeVar( - "T_OTXTileBatchDataEntity", - bound="OTXTileBatchDataEntity", -) - - -@dataclass -class TileDataEntity(Generic[T_OTXDataEntity]): - """Base data entity for tile task. - - Attributes: - num_tiles (int): The number of tiles. - entity_list (Sequence[OTXDataEntity]): A list of OTXDataEntity. - tile_attr_list (list[dict[str, int | str]]): The tile attributes including tile index and tile RoI information. - ori_img_info (ImageInfo): The image information about the original image. - """ - - num_tiles: int - entity_list: Sequence[T_OTXDataEntity] - tile_attr_list: list[dict[str, int | str]] - ori_img_info: ImageInfo - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - raise NotImplementedError - - -@dataclass -class TileDetDataEntity(TileDataEntity): - """Data entity for detection tile task. - - Attributes: - ori_bboxes (tv_tensors.BoundingBoxes): The bounding boxes of the original image. - ori_labels (LongTensor): The labels of the original image. - """ - - ori_bboxes: tv_tensors.BoundingBoxes - ori_labels: LongTensor - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.DETECTION - - -@dataclass -class OTXTileBatchDataEntity(Generic[T_OTXBatchDataEntity]): - """Base batch data entity for tile task. - - Attributes: - batch_size (int): The size of the batch. - batch_tiles (list[list[tv_tensors.Image]]): The batch of tile images. - batch_tile_img_infos (list[list[ImageInfo]]): The batch of tiles image information. - batch_tile_attr_list (list[list[dict[str, int | str]]]): - The batch of tile attributes including tile index and tile RoI information. - imgs_info (list[ImageInfo]): The image information about the original image. - """ - - batch_size: int - batch_tiles: list[list[tv_tensors.Image]] - batch_tile_img_infos: list[list[ImageInfo]] - batch_tile_attr_list: list[list[dict[str, int | str]]] - imgs_info: list[ImageInfo] - - def unbind(self) -> list[T_OTXBatchDataEntity]: - """Unbind batch data entity.""" - raise NotImplementedError - - -@dataclass -class TileBatchDetDataEntity(OTXTileBatchDataEntity): - """Batch data entity for detection tile task. - - Attributes: - bboxes (list[tv_tensors.BoundingBoxes]): The bounding boxes of the original image. - labels (list[LongTensor]): The labels of the original image. - """ - - bboxes: list[tv_tensors.BoundingBoxes] - labels: list[LongTensor] - - def unbind(self) -> list[tuple[list[dict[str, int | str]], DetBatchDataEntity]]: - """Unbind batch data entity for detection task.""" - tiles = [tile for tiles in self.batch_tiles for tile in tiles] - tile_infos = [tile_info for tile_infos in self.batch_tile_img_infos for tile_info in tile_infos] - tile_attr_list = [tile_attr for tile_attrs in self.batch_tile_attr_list for tile_attr in tile_attrs] - - batch_tile_attr_list = [ - tile_attr_list[i : i + self.batch_size] for i in range(0, len(tile_attr_list), self.batch_size) - ] - - batch_data_entities = [ - DetBatchDataEntity( - batch_size=self.batch_size, - images=tiles[i : i + self.batch_size], - imgs_info=tile_infos[i : i + self.batch_size], - bboxes=[[] for _ in range(self.batch_size)], - labels=[[] for _ in range(self.batch_size)], - ) - for i in range(0, len(tiles), self.batch_size) - ] - return list(zip(batch_tile_attr_list, batch_data_entities)) - - @classmethod - def collate_fn(cls, batch_entities: list[TileDetDataEntity]) -> TileBatchDetDataEntity: - """Collate function to collect TileDetDataEntity into TileBatchDetDataEntity in data loader.""" - if (batch_size := len(batch_entities)) == 0: - msg = "collate_fn() input should have > 0 entities" - raise RuntimeError(msg) - - task = batch_entities[0].task - - for tile_entity in batch_entities: - for entity in tile_entity.entity_list: - if entity.task != task: - msg = "collate_fn() input should include a single OTX task" - raise RuntimeError(msg) - - if not isinstance(entity, DetDataEntity): - msg = "All entities should be DetDataEntity before collate_fn()" - raise TypeError(msg) - - return TileBatchDetDataEntity( - batch_size=batch_size, - batch_tiles=[[entity.image for entity in tile_entity.entity_list] for tile_entity in batch_entities], - batch_tile_img_infos=[ - [entity.img_info for entity in tile_entity.entity_list] for tile_entity in batch_entities - ], - batch_tile_attr_list=[tile_entity.tile_attr_list for tile_entity in batch_entities], - imgs_info=[tile_entity.ori_img_info for tile_entity in batch_entities], - bboxes=[tile_entity.ori_bboxes for tile_entity in batch_entities], - labels=[tile_entity.ori_labels for tile_entity in batch_entities], - ) - - -@dataclass -class TileInstSegDataEntity(TileDataEntity): - """Data entity for instance segmentation tile task. - - Attributes: - ori_bboxes (tv_tensors.BoundingBoxes): The bounding boxes of the original image. - ori_labels (LongTensor): The labels of the original image. - ori_masks (tv_tensors.Mask): The masks of the original image. - ori_polygons (list[Polygon]): The polygons of the original image. - """ - - ori_bboxes: tv_tensors.BoundingBoxes - ori_labels: LongTensor - ori_masks: tv_tensors.Mask - ori_polygons: list[Polygon] - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.INSTANCE_SEGMENTATION - - -@dataclass -class TileBatchInstSegDataEntity(OTXTileBatchDataEntity): - """Batch data entity for instance segmentation tile task. - - Attributes: - bboxes (list[tv_tensors.BoundingBoxes]): The bounding boxes of the original image. - labels (list[LongTensor]): The labels of the original image. - masks (list[tv_tensors.Mask]): The masks of the original image. - polygons (list[list[Polygon]]): The polygons of the original image. - """ - - bboxes: list[tv_tensors.BoundingBoxes] - labels: list[LongTensor] - masks: list[tv_tensors.Mask] - polygons: list[list[Polygon]] - - def unbind(self) -> list[tuple[list[dict[str, int | str]], InstanceSegBatchDataEntity]]: - """Unbind batch data entity for instance segmentation task.""" - tiles = [tile for tiles in self.batch_tiles for tile in tiles] - tile_infos = [tile_info for tile_infos in self.batch_tile_img_infos for tile_info in tile_infos] - tile_attr_list = [tile_attr for tile_attrs in self.batch_tile_attr_list for tile_attr in tile_attrs] - - batch_tile_attr_list = [ - tile_attr_list[i : i + self.batch_size] for i in range(0, len(tile_attr_list), self.batch_size) - ] - batch_data_entities = [ - InstanceSegBatchDataEntity( - batch_size=self.batch_size, - images=tiles[i : i + self.batch_size], - imgs_info=tile_infos[i : i + self.batch_size], - bboxes=[[] for _ in range(self.batch_size)], - labels=[[] for _ in range(self.batch_size)], - masks=[[] for _ in range(self.batch_size)], - polygons=[[] for _ in range(self.batch_size)], - ) - for i in range(0, len(tiles), self.batch_size) - ] - return list(zip(batch_tile_attr_list, batch_data_entities)) - - @classmethod - def collate_fn(cls, batch_entities: list[TileInstSegDataEntity]) -> TileBatchInstSegDataEntity: - """Collate function to collect TileInstSegDataEntity into TileBatchInstSegDataEntity in data loader.""" - if (batch_size := len(batch_entities)) == 0: - msg = "collate_fn() input should have > 0 entities" - raise RuntimeError(msg) - - task = batch_entities[0].task - - for tile_entity in batch_entities: - for entity in tile_entity.entity_list: - if entity.task != task: - msg = "collate_fn() input should include a single OTX task" - raise RuntimeError(msg) - - if not isinstance(entity, InstanceSegDataEntity): - msg = "All entities should be InstanceSegDataEntity before collate_fn()" - raise TypeError(msg) - - return TileBatchInstSegDataEntity( - batch_size=batch_size, - batch_tiles=[[entity.image for entity in tile_entity.entity_list] for tile_entity in batch_entities], - batch_tile_img_infos=[ - [entity.img_info for entity in tile_entity.entity_list] for tile_entity in batch_entities - ], - batch_tile_attr_list=[tile_entity.tile_attr_list for tile_entity in batch_entities], - imgs_info=[tile_entity.ori_img_info for tile_entity in batch_entities], - bboxes=[tile_entity.ori_bboxes for tile_entity in batch_entities], - labels=[tile_entity.ori_labels for tile_entity in batch_entities], - masks=[tile_entity.ori_masks for tile_entity in batch_entities], - polygons=[tile_entity.ori_polygons for tile_entity in batch_entities], - ) diff --git a/src/otx/core/data/entity/utils.py b/src/otx/core/data/entity/utils.py deleted file mode 100644 index 7d911d9b2ab..00000000000 --- a/src/otx/core/data/entity/utils.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility functions for OTX data entities.""" -from __future__ import annotations - -from typing import TYPE_CHECKING - -import torch -import torch.utils._pytree as pytree -import torchvision.transforms.v2.functional as F # noqa: N812 -from torch import Tensor -from torchvision import tv_tensors -from torchvision.utils import _log_api_usage_once - -if TYPE_CHECKING: - from otx.core.data.entity.base import Points, T_OTXDataEntity # noqa: TCH004 - - -def register_pytree_node(cls: type[T_OTXDataEntity]) -> type[T_OTXDataEntity]: - """Decorator to register an OTX data entity with PyTorch's PyTree. - - This decorator should be applied to every OTX data entity, as TorchVision V2 transforms - use the PyTree to flatten and unflatten the data entity during runtime. - - Example: - `MulticlassClsDataEntity` example :: - - @register_pytree_node - @dataclass - class MulticlassClsDataEntity(OTXDataEntity): - ... - """ - flatten_fn = lambda obj: (list(obj.values()), list(obj.keys())) # noqa: E731 - unflatten_fn = lambda values, context: cls(**dict(zip(context, values))) # noqa: E731 - pytree._register_pytree_node( # noqa: SLF001 - typ=cls, - flatten_fn=flatten_fn, - unflatten_fn=unflatten_fn, - ) - return cls - - -def _clamp_points(points: Tensor, canvas_size: tuple[int, int]) -> Tensor: - # TODO (sungchul): Tracking torchvision.transforms.v2.functional._meta._clamp_bounding_boxes - # https://github.com/pytorch/vision/blob/main/torchvision/transforms/v2/functional/_meta.py#L234-L249 - in_dtype = points.dtype - points = points.clone() if points.is_floating_point() else points.float() - points[..., 0].clamp_(min=0, max=canvas_size[1]) - points[..., 1].clamp_(min=0, max=canvas_size[0]) - return points.to(in_dtype) - - -def clamp_points(inpt: Tensor, canvas_size: tuple[int, int] | None = None) -> Tensor: - """Clamp point range.""" - # TODO (sungchul): Tracking torchvision.transforms.v2.functional._meta.clamp_bounding_boxes - # https://github.com/pytorch/vision/blob/main/torchvision/transforms/v2/functional/_meta.py#L252-L274 - if not torch.jit.is_scripting(): - _log_api_usage_once(clamp_points) - - if torch.jit.is_scripting() or F._utils.is_pure_tensor(inpt): # noqa: SLF001 - if canvas_size is None: - raise ValueError("For pure tensor inputs, `canvas_size` has to be passed.") # noqa: EM101, TRY003 - return _clamp_points(inpt, canvas_size=canvas_size) - elif isinstance(inpt, Points): # noqa: RET505 - if canvas_size is not None: - raise ValueError("For point tv_tensor inputs, `canvas_size` must not be passed.") # noqa: EM101, TRY003 - output = _clamp_points(inpt.as_subclass(Tensor), canvas_size=inpt.canvas_size) - return tv_tensors.wrap(output, like=inpt) - else: - raise TypeError( # noqa: TRY003 - f"Input can either be a plain tensor or a point tv_tensor, but got {type(inpt)} instead.", # noqa: EM102 - ) diff --git a/src/otx/core/data/entity/visual_prompting.py b/src/otx/core/data/entity/visual_prompting.py deleted file mode 100644 index ee90f045daa..00000000000 --- a/src/otx/core/data/entity/visual_prompting.py +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX visual prompting data entities.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from torchvision import tv_tensors - -from otx.core.data.entity.base import OTXBatchDataEntity, OTXBatchPredEntity, OTXDataEntity, OTXPredEntity, Points -from otx.core.data.entity.utils import register_pytree_node -from otx.core.types.task import OTXTaskType - -if TYPE_CHECKING: - from datumaro import Polygon - from torch import LongTensor - - -@register_pytree_node -@dataclass -class VisualPromptingDataEntity(OTXDataEntity): - """Data entity for visual prompting task. - - Attributes: - masks (tv_tensors.Mask): The masks of the instances. - labels (LongTensor): The labels of the instances. - polygons (list[Polygon]): The polygons of the instances. - bboxes (tv_tensors.BoundingBoxes): The bounding boxes of the instances. - points (Points): The points of the instances. - """ - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.VISUAL_PROMPTING - - masks: tv_tensors.Mask - labels: dict[str, LongTensor] - polygons: list[Polygon] - bboxes: tv_tensors.BoundingBoxes - points: Points - - -@dataclass -class VisualPromptingPredEntity(VisualPromptingDataEntity, OTXPredEntity): - """Data entity to represent the visual prompting model output prediction.""" - - -@dataclass -class VisualPromptingBatchDataEntity(OTXBatchDataEntity[VisualPromptingDataEntity]): - """Data entity for visual prompting task. - - Attributes: - masks (list[tv_tensors.Mask]): List of masks. - labels (list[LongTensor]): List of labels. - polygons (list[list[Polygon]]): List of polygons. - bboxes (list[tv_tensors.BoundingBoxes]): List of bounding boxes. - points (list[Points]): List of points. - """ - - masks: list[tv_tensors.Mask] - labels: list[dict[str, LongTensor]] - polygons: list[list[Polygon]] - bboxes: list[tv_tensors.BoundingBoxes] - points: list[Points] - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.VISUAL_PROMPTING - - @classmethod - def collate_fn( - cls, - entities: list[VisualPromptingDataEntity], - stack_images: bool = True, - ) -> VisualPromptingBatchDataEntity: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader. - - Args: - entities (list[VisualPromptingDataEntity]): List of VisualPromptingDataEntity objects. - - Returns: - VisualPromptingBatchDataEntity: The collated batch data entity. - """ - batch_data = super().collate_fn(entities, stack_images=stack_images) - return VisualPromptingBatchDataEntity( - batch_size=batch_data.batch_size, - images=batch_data.images, - imgs_info=batch_data.imgs_info, - masks=[entity.masks for entity in entities], - labels=[entity.labels for entity in entities], - polygons=[entity.polygons for entity in entities], - points=[entity.points for entity in entities], - bboxes=[entity.bboxes for entity in entities], - ) - - def pin_memory(self) -> VisualPromptingBatchDataEntity: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.points = [ - tv_tensors.wrap(point.pin_memory(), like=point) if point is not None else point for point in self.points - ] - self.bboxes = [ - tv_tensors.wrap(bbox.pin_memory(), like=bbox) if bbox is not None else bbox for bbox in self.bboxes - ] - self.masks = [tv_tensors.wrap(mask.pin_memory(), like=mask) for mask in self.masks] - self.labels = [ - {prompt_type: values.pin_memory() for prompt_type, values in labels.items()} for labels in self.labels - ] - return self - - -@dataclass -class VisualPromptingBatchPredEntity(VisualPromptingBatchDataEntity, OTXBatchPredEntity): - """Data entity to represent model output predictions for visual prompting task.""" - - -@register_pytree_node -@dataclass -class ZeroShotVisualPromptingDataEntity(OTXDataEntity): - """Data entity for zero-shot visual prompting task. - - Attributes: - masks (tv_tensors.Mask): The masks of the instances. - labels (LongTensor): The labels of the instances. - polygons (list[Polygon]): The polygons of the instances. - prompts (list[tv_tensors.TVTensor]): The prompts of the instances. - """ - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING - - masks: tv_tensors.Mask - labels: list[LongTensor] - polygons: list[Polygon] - prompts: list[tv_tensors.TVTensor] - - -@dataclass -class ZeroShotVisualPromptingBatchDataEntity(OTXBatchDataEntity[ZeroShotVisualPromptingDataEntity]): - """Data entity for zero-shot visual prompting task. - - Attributes: - masks (list[tv_tensors.Mask]): List of masks. - labels (list[LongTensor]): List of labels. - polygons (list[list[Polygon]]): List of polygons. - prompts (list[list[tv_tensors.TVTensor]]): List of prompts. - """ - - masks: list[tv_tensors.Mask] - labels: list[LongTensor] - polygons: list[list[Polygon]] - prompts: list[list[tv_tensors.TVTensor]] - - @property - def task(self) -> OTXTaskType: - """OTX Task type definition.""" - return OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING - - @classmethod - def collate_fn( - cls, - entities: list[ZeroShotVisualPromptingDataEntity], - stack_images: bool = True, - ) -> ZeroShotVisualPromptingBatchDataEntity: - """Collection function to collect `OTXDataEntity` into `OTXBatchDataEntity` in data loader. - - Args: - entities (list[ZeroShotVisualPromptingDataEntity]): List of ZeroShotVisualPromptingDataEntity objects. - - Returns: - ZeroShotVisualPromptingBatchDataEntity: The collated batch data entity. - """ - batch_data = super().collate_fn(entities, stack_images=stack_images) - return ZeroShotVisualPromptingBatchDataEntity( - batch_size=batch_data.batch_size, - images=batch_data.images, - imgs_info=batch_data.imgs_info, - masks=[entity.masks for entity in entities], - labels=[entity.labels for entity in entities], - polygons=[entity.polygons for entity in entities], - prompts=[entity.prompts for entity in entities], - ) - - def pin_memory(self) -> ZeroShotVisualPromptingBatchDataEntity: - """Pin memory for member tensor variables.""" - super().pin_memory() - self.prompts = [ - [tv_tensors.wrap(prompt.pin_memory(), like=prompt) if prompt is not None else prompt for prompt in prompts] - for prompts in self.prompts - ] - self.masks = [tv_tensors.wrap(mask.pin_memory(), like=mask) for mask in self.masks] - self.labels = [label.pin_memory() for label in self.labels] - return self - - -@dataclass -class ZeroShotVisualPromptingBatchPredEntity(ZeroShotVisualPromptingBatchDataEntity, OTXBatchPredEntity): - """Data entity to represent model output predictions for zero-shot visual prompting task.""" - - prompts: list[Points] # type: ignore[assignment] diff --git a/src/otx/core/data/factory.py b/src/otx/core/data/factory.py deleted file mode 100644 index 22332e89a7e..00000000000 --- a/src/otx/core/data/factory.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Factory classes for dataset and transforms.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from otx.core.types.task import OTXTaskType -from otx.core.types.transformer_libs import TransformLibType - -from .dataset.base import OTXDataset, Transforms - -if TYPE_CHECKING: - from datumaro import DatasetSubset - - from otx.core.config.data import DataModuleConfig, SubsetConfig - from otx.core.data.mem_cache import MemCacheHandlerBase - - -__all__ = ["TransformLibFactory", "OTXDatasetFactory"] - - -class TransformLibFactory: - """Factory class for transform.""" - - @classmethod - def generate(cls: type[TransformLibFactory], config: SubsetConfig) -> Transforms: - """Create transforms from factory.""" - if config.transform_lib_type == TransformLibType.TORCHVISION: - from .transform_libs.torchvision import TorchVisionTransformLib - - return TorchVisionTransformLib.generate(config) - - if config.transform_lib_type == TransformLibType.MMCV: - from .transform_libs.mmcv import MMCVTransformLib - - return MMCVTransformLib.generate(config) - - if config.transform_lib_type == TransformLibType.MMPRETRAIN: - from .transform_libs.mmpretrain import MMPretrainTransformLib - - return MMPretrainTransformLib.generate(config) - - if config.transform_lib_type == TransformLibType.MMDET: - from .transform_libs.mmdet import MMDetTransformLib - - return MMDetTransformLib.generate(config) - - if config.transform_lib_type == TransformLibType.MMSEG: - from .transform_libs.mmseg import MMSegTransformLib - - return MMSegTransformLib.generate(config) - - if config.transform_lib_type == TransformLibType.MMACTION: - from .transform_libs.mmaction import MMActionTransformLib - - return MMActionTransformLib.generate(config) - - raise NotImplementedError(config.transform_lib_type) - - -class OTXDatasetFactory: - """Factory class for OTXDataset.""" - - @classmethod - def create( # noqa: PLR0911 # ignore too many return statements - cls: type[OTXDatasetFactory], - task: OTXTaskType, - dm_subset: DatasetSubset, - mem_cache_handler: MemCacheHandlerBase, - cfg_subset: SubsetConfig, - cfg_data_module: DataModuleConfig, - ) -> OTXDataset: - """Create OTXDataset.""" - transforms = TransformLibFactory.generate(cfg_subset) - common_kwargs = { - "dm_subset": dm_subset, - "transforms": transforms, - "mem_cache_handler": mem_cache_handler, - "mem_cache_img_max_size": cfg_data_module.mem_cache_img_max_size, - "image_color_channel": cfg_data_module.image_color_channel, - "stack_images": cfg_data_module.stack_images, - } - - if task in ( - OTXTaskType.ANOMALY_CLASSIFICATION, - OTXTaskType.ANOMALY_DETECTION, - OTXTaskType.ANOMALY_SEGMENTATION, - ): - from .dataset.anomaly import AnomalyDataset - - return AnomalyDataset(task_type=task, **common_kwargs) - - if task == OTXTaskType.MULTI_CLASS_CLS: - from .dataset.classification import OTXMulticlassClsDataset - - return OTXMulticlassClsDataset(**common_kwargs) - - if task == OTXTaskType.MULTI_LABEL_CLS: - from .dataset.classification import OTXMultilabelClsDataset - - return OTXMultilabelClsDataset(**common_kwargs) - - if task == OTXTaskType.H_LABEL_CLS: - from .dataset.classification import OTXHlabelClsDataset - - return OTXHlabelClsDataset(**common_kwargs) - - if task == OTXTaskType.DETECTION: - from .dataset.detection import OTXDetectionDataset - - return OTXDetectionDataset(**common_kwargs) - - if task in [OTXTaskType.ROTATED_DETECTION, OTXTaskType.INSTANCE_SEGMENTATION]: - from .dataset.instance_segmentation import OTXInstanceSegDataset - - # NOTE: DataModuleConfig does not have include_polygons attribute - include_polygons = getattr(cfg_data_module, "include_polygons", False) - return OTXInstanceSegDataset(include_polygons=include_polygons, **common_kwargs) - - if task == OTXTaskType.SEMANTIC_SEGMENTATION: - from .dataset.segmentation import OTXSegmentationDataset - - return OTXSegmentationDataset(**common_kwargs) - - if task == OTXTaskType.ACTION_CLASSIFICATION: - from .dataset.action_classification import OTXActionClsDataset - - return OTXActionClsDataset(**common_kwargs) - - if task == OTXTaskType.ACTION_DETECTION: - from .dataset.action_detection import OTXActionDetDataset - - return OTXActionDetDataset(**common_kwargs) - - if task == OTXTaskType.VISUAL_PROMPTING: - from .dataset.visual_prompting import OTXVisualPromptingDataset - - use_bbox = getattr(cfg_data_module.vpm_config, "use_bbox", False) - use_point = getattr(cfg_data_module.vpm_config, "use_point", False) - return OTXVisualPromptingDataset(use_bbox=use_bbox, use_point=use_point, **common_kwargs) - - if task == OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING: - from .dataset.visual_prompting import OTXZeroShotVisualPromptingDataset - - use_bbox = getattr(cfg_data_module.vpm_config, "use_bbox", False) - use_point = getattr(cfg_data_module.vpm_config, "use_point", False) - return OTXZeroShotVisualPromptingDataset(use_bbox=use_bbox, use_point=use_point, **common_kwargs) - - raise NotImplementedError(task) diff --git a/src/otx/core/data/manager/__init__.py b/src/otx/core/data/manager/__init__.py new file mode 100644 index 00000000000..f24ad9bc0dd --- /dev/null +++ b/src/otx/core/data/manager/__init__.py @@ -0,0 +1,16 @@ +"""OTX Core Data Utils.""" + +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. +# diff --git a/src/otx/core/data/manager/dataset_manager.py b/src/otx/core/data/manager/dataset_manager.py new file mode 100644 index 00000000000..2902558a00e --- /dev/null +++ b/src/otx/core/data/manager/dataset_manager.py @@ -0,0 +1,151 @@ +"""Datumaro Helper.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=invalid-name +import os +from typing import List, Optional, Tuple, Union + +import datumaro +from datumaro.components.dataset import Dataset, DatasetSubset +from datumaro.components.dataset_base import DatasetItem +from datumaro.plugins.splitter import Split + + +class DatasetManager: + """The aim of DatasetManager is support datumaro functions at easy use. + + All kind of functions implemented in Datumaro are supported by this Manager. + Since DatasetManager just wraps Datumaro's function, + All methods are implemented as static method. + """ + + @staticmethod + def get_train_dataset(dataset: Dataset) -> DatasetSubset: + """Returns train dataset.""" + subsets = dataset.subsets() + train_dataset = subsets.get("train", None) + + if train_dataset is not None: + return train_dataset + + for k, v in subsets.items(): + if "train" in k or "default" in k: + return v + raise ValueError("Can't find training data.") + + @staticmethod + def get_val_dataset(dataset: Dataset) -> Union[DatasetSubset, None]: + """Returns validation dataset.""" + subsets = dataset.subsets() + val_dataset = subsets.get("val", None) + + if val_dataset is not None: + return val_dataset + + for k, v in subsets.items(): + if "val" in k: + return v + return None + + @staticmethod + def get_data_format(data_root: str) -> str: + """Find the format of dataset.""" + data_root = os.path.abspath(data_root) + + data_format: str = "" + + # TODO # + # Currently, below `if/else` statements is mandatory + # because Datumaro can't detect the multi-cvat and mvtec. + # After, the upgrade of Datumaro, below codes will be changed. + if DatasetManager.is_cvat_format(data_root): + data_format = "multi-cvat" + elif DatasetManager.is_mvtec_format(data_root): + data_format = "mvtec" + else: + data_formats = datumaro.Environment().detect_dataset(data_root) + # TODO: how to avoid hard-coded part + data_format = data_formats[0] if "imagenet" not in data_formats else "imagenet" + print(f"[*] Detected dataset format: {data_format}") + return data_format + + @staticmethod + def get_image_path(data_item: DatasetItem) -> Optional[str]: + """Returns the path of image.""" + if hasattr(data_item.media, "path"): + return data_item.media.path + return None + + @staticmethod + def export_dataset(dataset: Dataset, output_dir: str, data_format: str, save_media=True): + """Export the Datumaro Dataset.""" + return dataset.export(output_dir, data_format, save_media=save_media) + + @staticmethod + def import_dataset(data_root: str, data_format: str, subset: Optional[str] = None) -> dict: + """Import dataset.""" + return Dataset.import_from(data_root, format=data_format, subset=subset) + + @staticmethod + def auto_split(task: str, dataset: Dataset, split_ratio: List[Tuple[str, float]]) -> dict: + """Automatically split the dataset: train --> train/val.""" + splitter = Split(dataset, task.lower(), split_ratio) + return splitter.subsets() + + @staticmethod + def is_cvat_format(path: str) -> bool: + """Detect whether data path is CVAT format or not. + + Currently, we used multi-video CVAT format for Action tasks. + + This function can detect the multi-video CVAT format. + + Multi-video CVAT format + root + |--video_0 + |--images + |--frame0001.png + |--annotations.xml + |--video_1 + |--video_2 + + will be deprecated soon. + """ + + cvat_format = sorted(["images", "annotations.xml"]) + for sub_folder in os.listdir(path): + # video_0, video_1, ... + sub_folder_path = os.path.join(path, sub_folder) + # files must be same with cvat_format + if os.path.isdir(sub_folder_path): + files = sorted(os.listdir(sub_folder_path)) + if files != cvat_format: + return False + return True + + @staticmethod + def is_mvtec_format(path: str) -> bool: + """Detect whether data path is MVTec format or not. + + Check the first-level architecture folder, to know whether the dataset is MVTec or not. + + MVTec default structure like as below: + root + |--ground_truth + |--train + |--test + + will be deprecated soon. + """ + + mvtec_format = sorted(["ground_truth", "train", "test"]) + folder_list = [] + for sub_folder in os.listdir(path): + sub_folder_path = os.path.join(path, sub_folder) + # only use the folder name. + if os.path.isdir(sub_folder_path): + folder_list.append(sub_folder) + return sorted(folder_list) == mvtec_format diff --git a/src/otx/core/data/mem_cache.py b/src/otx/core/data/mem_cache.py deleted file mode 100644 index 92dafe44e22..00000000000 --- a/src/otx/core/data/mem_cache.py +++ /dev/null @@ -1,334 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Memory cache handler implementations and singleton class to call them.""" - -from __future__ import annotations - -import ctypes as ct -import logging -import multiprocessing as mp -import re -import signal -from typing import TYPE_CHECKING, Any, ClassVar - -import numpy as np -import psutil - -from otx.utils import append_signal_handler - -if TYPE_CHECKING: - from multiprocessing.managers import DictProxy - from multiprocessing.synchronize import Lock - -logger = logging.getLogger() - -GIB = 1024**3 - -__all__ = [ - "MemCacheHandlerSingleton", - "MemCacheHandlerBase", - "NULL_MEM_CACHE_HANDLER", - "MemCacheHandlerError", - "parse_mem_cache_size_to_int", -] - - -def parse_mem_cache_size_to_int(mem_cache_size: str) -> int: - """Parse memory size string to integer. - - For example, "2GB" => 2,000,000,000 or "3MIB" => 3 * 1024 * 1024. - """ - match = re.match(r"^([\d\.]+)\s*([a-zA-Z]{0,3})$", mem_cache_size.strip()) - - if match is None: - msg = f"Cannot parse {mem_cache_size} string." - raise ValueError(msg) - - units = { - "": 1, - "B": 1, - "KIB": 2**10, - "MIB": 2**20, - "GIB": 2**30, - "KB": 10**3, - "MB": 10**6, - "GB": 10**9, - "K": 2**10, - "M": 2**20, - "G": 2**30, - } - - number, unit = int(match.group(1)), match.group(2).upper() - - if unit not in units: - msg = f"{mem_cache_size} has disallowed unit ({unit})." - raise ValueError(msg) - - return number * units[unit] - - -class _DummyLock: - def __enter__(self, *args, **kwargs): - pass - - def __exit__(self, *args, **kwargs): - pass - - -class MemCacheHandlerBase: - """Base class for memory cache handler. - - It will be combined with LoadImageFromOTXDataset to store/retrieve the samples in memory. - """ - - def __init__(self, mem_size: int): - self._mem_size = mem_size - self._init_data_structs(mem_size) - - def _init_data_structs(self, mem_size: int) -> None: - self._arr = (ct.c_uint8 * mem_size)() - self._cur_page = ct.c_size_t(0) - self._cache_addr: ( - dict[Any, tuple[Any, ...]] - | DictProxy[ - Any, - tuple[Any, ...], - ] - ) = {} - self._lock: Lock | _DummyLock = _DummyLock() - self._freeze = ct.c_bool(False) - - def __len__(self) -> int: - """Get the number of cached items.""" - return len(self._cache_addr) - - @property - def mem_size(self) -> int: - """Get the reserved memory pool size (bytes).""" - return len(self._arr) - - def get(self, key: Any) -> tuple[np.ndarray | None, dict | None]: # noqa: ANN401 - """Try to look up the cached item with the given key. - - Args: - key (Any): A key for looking up the cached item - - Returns: - If succeed return (np.ndarray, Dict), otherwise return (None, None) - """ - try: - if self.mem_size == 0 or (addr := self._cache_addr.get(key, None)) is None: - return None, None - - offset, count, dtype, shape, strides, meta = addr - - data = np.frombuffer(self._arr, dtype=dtype, count=count, offset=offset) - return np.lib.stride_tricks.as_strided(data, shape, strides), meta - - except BrokenPipeError: - # It is possible that the manager is dead but - # the multi-processing worker in DataLoader is alive. - # In this case, we need to handle this error. - return None, None - - def put( - self, - key: Any, # noqa: ANN401 - data: np.ndarray, - meta: dict | None = None, - ) -> int | None: - """Try to store np.ndarray and metadata with a key to the reserved memory pool. - - Args: - key (Any): A key to store the cached item - data (np.ndarray): A data sample to store - meta (Optional[Dict]): A metadata of the data sample - - Returns: - Optional[int]: If succeed return the address of cached item in memory pool - """ - if self._freeze.value: - return None - - try: - if (addr := self._cache_addr.get(key, None)) is not None: - return addr[0] - - data_bytes = data.size * data.itemsize - - with self._lock: - new_page = self._cur_page.value + data_bytes - - if new_page > self.mem_size: - self.freeze() - msg = "Memory pool reaches it's limit. Cannot cache more. Freeze it." - logger.warning(msg) - return None - - offset = ct.byref(self._arr, self._cur_page.value) - ct.memmove(offset, data.ctypes.data, data_bytes) - - self._cache_addr[key] = ( - self._cur_page.value, - data.size, - data.dtype, - data.shape, - data.strides, - meta, - ) - self._cur_page.value = new_page - return new_page - except BrokenPipeError: - # It is possible that the manager is dead but - # the multi-processing worker in DataLoader is alive. - # In this case, we need to handle this error. - return None - - def __repr__(self) -> str: - """Representation for the current handler status.""" - perc = 100.0 * self._cur_page.value / self.mem_size if self.mem_size > 0 else 0.0 - return ( - f"{self.__class__.__name__} " - f"uses {self._cur_page.value} / {self.mem_size} ({perc:.1f}%) memory pool and " - f"store {len(self)} items." - ) - - def __reduce__(self): - """Dump just mem_size and re-initialize with that value when unpickled.""" - return (self.__class__, (self._mem_size,)) - - @property - def frozen(self) -> bool: - """True if this handler is frozen, otherwise return False.""" - return self._freeze.value - - def freeze(self) -> None: - """If frozen, it is impossible to store a new item anymore.""" - self._freeze.value = True - - def unfreeze(self) -> None: - """If unfrozen, it is possible to store a new item.""" - self._freeze.value = False - - def shutdown(self) -> None: - """Shutdown mem caching handler. - - It is effective only for the multiprocessing handler. - """ - - -class MemCacheHandlerForSP(MemCacheHandlerBase): - """Memory caching handler for single processing. - - Use if PyTorch's DataLoader.num_workers == 0. - """ - - -class MemCacheHandlerForMP(MemCacheHandlerBase): - """Memory caching handler for multi processing. - - Use if PyTorch's DataLoader.num_workers > 0. - """ - - def _init_data_structs(self, mem_size: int) -> None: - self._arr = mp.Array(ct.c_uint8, mem_size, lock=False) - self._cur_page = mp.Value(ct.c_size_t, 0, lock=False) - - self._manager = mp.Manager() - self._cache_addr: DictProxy = self._manager.dict() - self._lock = mp.Lock() - self._freeze = mp.Value(ct.c_bool, False, lock=False) - - def shutdown(self) -> None: - """Shutdown mem caching handler. - - It is effective only for the multiprocessing handler. - """ - self._manager.shutdown() - - -class MemCacheHandlerError(Exception): - """Exception class for MemCacheHandler.""" - - -NULL_MEM_CACHE_HANDLER = MemCacheHandlerBase(mem_size=0) -NULL_MEM_CACHE_HANDLER.freeze() - - -class MemCacheHandlerSingleton: - """A helper class to create MemCacheHandler.""" - - instances: ClassVar[list[MemCacheHandlerBase]] = [] - CPU_MEM_LIMITS_GIB: int = 30 - - @classmethod - def create(cls, mode: str, mem_size: int) -> MemCacheHandlerBase: - """Create a new MemCacheHandlerBase instance. - - Args: - mode (str): There are two options: null, multiprocessing or singleprocessing. - mem_size (int): The size of memory pool (bytes). - """ - # COPY FROM mmcv.runner.get_dist_info - from torch import distributed - - world_size = distributed.get_world_size() if distributed.is_available() and distributed.is_initialized() else 1 - - # Prevent CPU OOM issue - memory_info = psutil.virtual_memory() - available_cpu_mem = memory_info.available / GIB - - if world_size > 1: - mem_size = mem_size // world_size - available_cpu_mem = available_cpu_mem // world_size - logger.info( - f"Since world_size={world_size} > 1, each worker a {mem_size} size memory pool.", - ) - - logger.info(f"Try to create a {mem_size} size memory pool.") - if not cls.check_system_memory(mem_size, available_cpu_mem): - logger.warning("No available CPU memory left, mem_size will be set to 0.") - mem_size = 0 - - if mode == "null" or mem_size == 0: - instance = NULL_MEM_CACHE_HANDLER - elif mode == "multiprocessing": - instance = MemCacheHandlerForMP(mem_size) - elif mode == "singleprocessing": - instance = MemCacheHandlerForSP(mem_size) - else: - msg = f"{mode} is unknown mode." - raise MemCacheHandlerError(msg) - - # Should delete if receive sigint to gracefully terminate - def _new_handler(signum_, frame_) -> None: # noqa: ARG001, ANN001 - instance.shutdown() - - append_signal_handler(signal.SIGINT, _new_handler) - append_signal_handler(signal.SIGTERM, _new_handler) - - cls.instances.append(instance) - - return instance - - @classmethod - def check_system_memory(cls, mem_size: int, available_cpu_mem: int) -> bool: - """Check there is enough system memory to maintain memory caching pool. - - Parameters: - mem_size: Requested memory size (bytes) for the memory cahcing pool - available_cpu_mem: Memory capacity (bytes) of this system - Returns: - Return true if there is enough system memory. Otherwise, return false. - """ - expected_mem_usage = (mem_size / GIB) + cls.CPU_MEM_LIMITS_GIB - return available_cpu_mem >= expected_mem_usage - - @classmethod - def delete(cls) -> None: - """Shutdown and delete the created instance meantime.""" - for instance in cls.instances: - instance.shutdown() - - cls.instances = [] diff --git a/src/otx/core/data/module.py b/src/otx/core/data/module.py deleted file mode 100644 index d4161755744..00000000000 --- a/src/otx/core/data/module.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""LightningDataModule extension for OTX.""" - -from __future__ import annotations - -import logging as log -from typing import TYPE_CHECKING - -from datumaro import Dataset as DmDataset -from lightning import LightningDataModule -from omegaconf import DictConfig, OmegaConf -from torch.utils.data import DataLoader, RandomSampler - -from otx.core.data.dataset.base import LabelInfo -from otx.core.data.dataset.tile import OTXTileDatasetFactory -from otx.core.data.factory import OTXDatasetFactory -from otx.core.data.mem_cache import ( - MemCacheHandlerSingleton, - parse_mem_cache_size_to_int, -) -from otx.core.data.pre_filtering import pre_filtering -from otx.core.data.tile_adaptor import adapt_tile_config -from otx.core.types.device import DeviceType -from otx.core.types.task import OTXTaskType -from otx.core.utils.instantiators import instantiate_sampler -from otx.core.utils.utils import get_adaptive_num_workers - -if TYPE_CHECKING: - from lightning.pytorch.utilities.parsing import AttributeDict - - from otx.core.config.data import DataModuleConfig - from otx.core.data.dataset.base import OTXDataset - - -class OTXDataModule(LightningDataModule): - """LightningDataModule extension for OTX pipeline.""" - - def __init__( - self, - task: OTXTaskType, - config: DataModuleConfig, - ) -> None: - """Constructor.""" - super().__init__() - self.task = task - self.config = config - self.subsets: dict[str, OTXDataset] = {} - self.save_hyperparameters() - - # TODO (Jaeguk): This is workaround for a bug in Datumaro. - # These lines should be removed after next datumaro release. - # https://github.com/openvinotoolkit/datumaro/pull/1223/files - from datumaro.plugins.data_formats.video import VIDEO_EXTENSIONS - - VIDEO_EXTENSIONS.append(".mp4") - - dataset = DmDataset.import_from(self.config.data_root, format=self.config.data_format) - if self.task != "H_LABEL_CLS": - dataset = pre_filtering(dataset, self.config.data_format, self.config.unannotated_items_ratio) - if config.tile_config.enable_tiler and config.tile_config.enable_adaptive_tiling: - adapt_tile_config(config.tile_config, dataset=dataset) - - config_mapping = { - self.config.train_subset.subset_name: self.config.train_subset, - self.config.val_subset.subset_name: self.config.val_subset, - self.config.test_subset.subset_name: self.config.test_subset, - } - - if self.config.auto_num_workers: - if self.config.device not in [DeviceType.gpu, DeviceType.auto]: - log.warning( - "Only GPU device type support auto_num_workers. " - f"Current deveice type is {self.config.device!s}. auto_num_workers is skipped.", - ) - elif (num_workers := get_adaptive_num_workers()) is not None: - for subset_name, subset_config in config_mapping.items(): - log.info( - f"num_workers of {subset_name} subset is changed : " - f"{subset_config.num_workers} -> {num_workers}", - ) - subset_config.num_workers = num_workers - - mem_size = parse_mem_cache_size_to_int(config.mem_cache_size) - mem_cache_mode = ( - "singleprocessing" - if all(config.num_workers == 0 for config in config_mapping.values()) - else "multiprocessing" - ) - mem_cache_handler = MemCacheHandlerSingleton.create( - mode=mem_cache_mode, - mem_size=mem_size, - ) - - label_infos: list[LabelInfo] = [] - for name, dm_subset in dataset.subsets().items(): - if name not in config_mapping: - log.warning(f"{name} is not available. Skip it") - continue - - dataset = OTXDatasetFactory.create( - task=self.task, - dm_subset=dm_subset, - mem_cache_handler=mem_cache_handler, - cfg_subset=config_mapping[name], - cfg_data_module=config, - ) - - if config.tile_config.enable_tiler: - dataset = OTXTileDatasetFactory.create( - task=self.task, - dataset=dataset, - tile_config=config.tile_config, - ) - self.subsets[name] = dataset - - label_infos += [self.subsets[name].label_info] - log.info(f"Add name: {name}, self.subsets: {self.subsets}") - - if self._is_meta_info_valid(label_infos) is False: - msg = "All data meta infos of subsets should be the same." - raise ValueError(msg) - - self.label_info = next(iter(label_infos)) - - def _is_meta_info_valid(self, label_infos: list[LabelInfo]) -> bool: - """Check whether there are mismatches in the metainfo for the all subsets.""" - if all(label_info == label_infos[0] for label_info in label_infos): - return True - return False - - def _get_dataset(self, subset: str) -> OTXDataset: - if (dataset := self.subsets.get(subset)) is None: - msg = f"Dataset has no '{subset}'. Available subsets = {list(self.subsets.keys())}" - raise KeyError(msg) - return dataset - - def train_dataloader(self) -> DataLoader: - """Get train dataloader.""" - config = self.config.train_subset - dataset = self._get_dataset(config.subset_name) - sampler = instantiate_sampler(config.sampler, dataset=dataset, batch_size=config.batch_size) - - common_args = { - "dataset": dataset, - "batch_size": config.batch_size, - "num_workers": config.num_workers, - "pin_memory": True, - "collate_fn": dataset.collate_fn, - "persistent_workers": config.num_workers > 0, - "sampler": sampler, - "shuffle": sampler is None, - } - - tile_config = self.config.tile_config - if tile_config.enable_tiler and tile_config.sampling_ratio < 1: - num_samples = max(1, int(len(dataset) * tile_config.sampling_ratio)) - log.info(f"Using tiled sampling with {num_samples} samples") - common_args.update( - { - "shuffle": False, - "sampler": RandomSampler(dataset, num_samples=num_samples), - }, - ) - return DataLoader(**common_args) - - def val_dataloader(self) -> DataLoader: - """Get val dataloader.""" - config = self.config.val_subset - dataset = self._get_dataset(config.subset_name) - - return DataLoader( - dataset=dataset, - batch_size=config.batch_size, - shuffle=False, - num_workers=config.num_workers, - pin_memory=True, - collate_fn=dataset.collate_fn, - persistent_workers=config.num_workers > 0, - ) - - def test_dataloader(self) -> DataLoader: - """Get test dataloader.""" - config = self.config.test_subset - dataset = self._get_dataset(config.subset_name) - - return DataLoader( - dataset=dataset, - batch_size=config.batch_size, - shuffle=False, - num_workers=config.num_workers, - pin_memory=True, - collate_fn=dataset.collate_fn, - persistent_workers=config.num_workers > 0, - ) - - def predict_dataloader(self) -> DataLoader: - """Get test dataloader.""" - config = self.config.test_subset - dataset = self._get_dataset(config.subset_name) - - return DataLoader( - dataset=dataset, - batch_size=config.batch_size, - shuffle=False, - num_workers=config.num_workers, - pin_memory=True, - collate_fn=dataset.collate_fn, - persistent_workers=config.num_workers > 0, - ) - - def setup(self, stage: str) -> None: - """Setup for each stage.""" - - def teardown(self, stage: str) -> None: - """Teardown for each stage.""" - # clean up after fit or test - # called on every process in DDP - - @property - def hparams_initial(self) -> AttributeDict: - """The collection of hyperparameters saved with `save_hyperparameters()`. It is read-only. - - The reason why we override is that we have some custom resolvers for `DictConfig`. - Some resolved Python objects has not a primitive type, so that is not loggable without errors. - Therefore, we need to unresolve it this time. - """ - hp = super().hparams_initial - for key, value in hp.items(): - if isinstance(value, DictConfig): - # It should be unresolved to make it loggable - hp[key] = OmegaConf.to_container(value, resolve=False) - - return hp - - def __reduce__(self): - """Re-initialize object when unpickled.""" - return (self.__class__, (self.task, self.config)) diff --git a/src/otx/core/data/noisy_label_detection/__init__.py b/src/otx/core/data/noisy_label_detection/__init__.py new file mode 100644 index 00000000000..ff58974fc20 --- /dev/null +++ b/src/otx/core/data/noisy_label_detection/__init__.py @@ -0,0 +1,8 @@ +"""Module for noisy label detection features.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from .base import LossDynamicsTracker, LossDynamicsTrackingMixin + +__all__ = ["LossDynamicsTracker", "LossDynamicsTrackingMixin"] diff --git a/src/otx/core/data/noisy_label_detection/base.py b/src/otx/core/data/noisy_label_detection/base.py new file mode 100644 index 00000000000..114ad728854 --- /dev/null +++ b/src/otx/core/data/noisy_label_detection/base.py @@ -0,0 +1,72 @@ +"""Hook module to track loss dynamics during training and export these statistics to Datumaro format.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import List, Optional + +import datumaro as dm + +from otx.api.entities.dataset_item import DatasetItemEntityWithID +from otx.api.entities.datasets import DatasetEntity + +__all__ = ["LossDynamicsTracker", "LossDynamicsTrackingMixin"] + + +class LossDynamicsTracker: + """Class to track loss dynamics and export it to Datumaro format.""" + + TASK_NAME: Optional[str] = None + + def __init__(self) -> None: + self.initialized = False + + def init_with_otx_dataset(self, otx_dataset: DatasetEntity) -> None: + """DatasetEntity should be injected to the tracker for the initialization.""" + otx_labels = otx_dataset.get_labels() + label_categories = dm.LabelCategories.from_iterable([label_entity.name for label_entity in otx_labels]) + self.otx_label_map = {label_entity.id_: idx for idx, label_entity in enumerate(otx_labels)} + + self._export_dataset = dm.Dataset.from_iterable( + [ + dm.DatasetItem( + id=item.id_, + subset="train", + media=dm.Image.from_file(path=item.media.path, size=(item.media.height, item.media.width)) + if item.media.path + else dm.Image.from_numpy( + data=getattr(item.media, "_Image__data"), size=(item.media.height, item.media.width) + ), + annotations=self._convert_anns(item), + ) + for item in otx_dataset + ], + infos={"purpose": "noisy_label_detection", "task": self.TASK_NAME}, + categories={dm.AnnotationType.label: label_categories}, + ) + + self.initialized = True + + def _convert_anns(self, item: DatasetItemEntityWithID) -> List[dm.Annotation]: + raise NotImplementedError() + + def accumulate(self, outputs, iter) -> None: + """Accumulate training loss dynamics for each training step.""" + raise NotImplementedError() + + def export(self, output_path: str) -> None: + """Export loss dynamics statistics to Datumaro format.""" + raise NotImplementedError() + + +class LossDynamicsTrackingMixin: + """Mix-in to track loss dynamics during training.""" + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self._loss_dyns_tracker = LossDynamicsTracker() + + @property + def loss_dyns_tracker(self) -> LossDynamicsTracker: + """Get tracker.""" + return self._loss_dyns_tracker diff --git a/src/otx/core/data/pipelines/__init__.py b/src/otx/core/data/pipelines/__init__.py new file mode 100644 index 00000000000..699c2577892 --- /dev/null +++ b/src/otx/core/data/pipelines/__init__.py @@ -0,0 +1,3 @@ +"""OTX data pipelines.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/core/data/pre_filtering.py b/src/otx/core/data/pre_filtering.py deleted file mode 100644 index 0573481ae9e..00000000000 --- a/src/otx/core/data/pre_filtering.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Pre filtering data for OTX.""" - -from __future__ import annotations - -import warnings -from random import sample -from typing import TYPE_CHECKING - -from datumaro.components.annotation import Annotation, Bbox, Polygon -from datumaro.components.dataset import Dataset as DmDataset - -if TYPE_CHECKING: - from datumaro.components.dataset_base import DatasetItem - - -def pre_filtering(dataset: DmDataset, data_format: str, unannotated_items_ratio: float) -> DmDataset: - """Pre-filtering function to filter the dataset based on certain criteria. - - Args: - dataset (DmDataset): The input dataset to be filtered. - data_format (str): The format of the dataset. - unannotated_items_ratio (float): The ratio of background unannotated items to be used. - This must be a float between 0 and 1. - - Returns: - DmDataset: The filtered dataset. - """ - used_background_items = set() - msg = f"There are empty annotation items in train set, Of these, only {unannotated_items_ratio*100}% are used." - warnings.warn(msg, stacklevel=2) - if unannotated_items_ratio > 0: - empty_items = [item.id for item in dataset if item.subset == "train" and len(item.annotations) == 0] - used_background_items = set(sample(empty_items, int(len(empty_items) * unannotated_items_ratio))) - - dataset = DmDataset.filter( - dataset, - lambda item: not ( - item.subset == "train" and len(item.annotations) == 0 and item.id not in used_background_items - ), - ) - dataset = DmDataset.filter(dataset, is_valid_annot, filter_annotations=True) - - return remove_unused_labels(dataset, data_format) - - -def is_valid_annot(item: DatasetItem, annotation: Annotation) -> bool: # noqa: ARG001 - """Return whether DatasetItem's annotation is valid.""" - if isinstance(annotation, Bbox): - x1, y1, x2, y2 = annotation.points - if x1 < x2 and y1 < y2: - return True - msg = "There are bounding box which is not `x1 < x2 and y1 < y2`, they will be filtered out before training." - warnings.warn(msg, stacklevel=2) - return False - if isinstance(annotation, Polygon): - # TODO(JaegukHyun): This process is computationally intensive. # noqa: TD003 - # We should make pre-filtering user-configurable. - x_points = [annotation.points[i] for i in range(0, len(annotation.points), 2)] - y_points = [annotation.points[i + 1] for i in range(0, len(annotation.points), 2)] - if min(x_points) < max(x_points) and min(y_points) < max(y_points) and annotation.get_area() > 0: - return True - msg = "There are invalid polygon, they will be filtered out before training." - return False - return True - - -def remove_unused_labels(dataset: DmDataset, data_format: str) -> DmDataset: - """Remove unused labels in Datumaro dataset.""" - original_categories: list[str] = dataset.get_label_cat_names() - used_labels: list[int] = list({ann.label for item in dataset for ann in item.annotations}) - if data_format == "ava": - used_labels = [0, *used_labels] - elif data_format == "common_semantic_segmentation_with_subset_dirs": - if 0 in used_labels: - used_labels = [label - 1 for label in used_labels[1:]] - else: - used_labels = [label - 1 for label in used_labels] - if len(used_labels) == len(original_categories): - return dataset - msg = "There are unused labels in dataset, they will be filtered out before training." - warnings.warn(msg, stacklevel=2) - mapping = {original_categories[idx]: original_categories[idx] for idx in used_labels} - return dataset.transform("remap_labels", mapping=mapping, default="delete") diff --git a/src/otx/core/data/tile_adaptor.py b/src/otx/core/data/tile_adaptor.py deleted file mode 100644 index dedbe890423..00000000000 --- a/src/otx/core/data/tile_adaptor.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Tile Adaptor for OTX.""" -from __future__ import annotations - -import logging as log -from typing import Any - -import numpy as np -from datumaro import Bbox, Dataset, DatasetSubset, Polygon - -from otx.core.config.data import TileConfig - - -def compute_robust_statistics(values: np.array) -> dict[str, float]: - """Computes robust statistics of given samples. - - Args: - values (np.array): Array of samples - - Returns: - dict[str, float]: Robust avg, min, max values - """ - stat: dict = {} - if values.size == 0: - return stat - - avg_value = np.mean(values) - std_value = np.std(values) - avg_3std_min_value = avg_value - 3 * std_value - avg_3std_max_value = avg_value + 3 * std_value - min_value = np.min(values) - max_value = np.max(values) - - # Refine min/max to reduce outlier effect - robust_min_value = max(min_value, avg_3std_min_value) - robust_max_value = min(max_value, avg_3std_max_value) - - stat["avg"] = float(avg_value) - stat["std"] = float(std_value) - stat["min"] = float(min_value) - stat["max"] = float(max_value) - stat["robust_min"] = float(robust_min_value) - stat["robust_max"] = float(robust_max_value) - return stat - - -def compute_robust_scale_statistics(values: np.array) -> dict[str, float]: - """Computes robust statistics of scale values. - - Average of 0.5x scale and 2x scale should be 1x - - Args: - values (np.array): Array of positive scale values - - Returns: - dict[str, float]: Robust avg, min, max values - """ - # Compute stat in log scale & convert back to original scale - if values.size == 0: - return {} - - stat = compute_robust_statistics(np.log(values)) - stat = {k: float(np.exp(v)) for k, v in stat.items()} - # Normal scale std is easier to understand - stat["std"] = float(np.std(values)) - return stat - - -def compute_robust_dataset_statistics( - dataset: DatasetSubset, - ann_stat: bool = False, - max_samples: int = 1000, -) -> dict[str, Any]: - """Computes robust statistics of image & annotation sizes. - - Args: - dataset (DatasetSubset): Input dataset. - ann_stat (bool, optional): Whether to compute annotation size statistics. Defaults to False. - max_samples (int, optional): Maximum number of dataset subsamples to analyze. Defaults to 1000. - - Returns: - Dict[str, Any]: Robust avg, min, max values for images, and annotations optionally. - ex) stat = { - "image": {"avg": ...}, - "annotation": { - "num_per_image": {"avg": ...}, - "size_of_shape": {"avg": ...}, - } - } - """ - stat: dict = {} - if len(dataset) == 0 or max_samples <= 0: - return stat - - data_ids = [item.id for item in dataset] - max_image_samples = min(max_samples, len(dataset)) - # NOTE: current OTX does not set seed globally - rng = np.random.default_rng(42) - data_ids = rng.choice(data_ids, max_image_samples, replace=False)[:max_image_samples] - - image_sizes = [] - for idx in data_ids: - data = dataset.get(id=idx, subset=dataset.name) - height, width = data.media.size - image_sizes.append(np.sqrt(width * height)) - stat["image"] = compute_robust_scale_statistics(np.array(image_sizes)) - - if ann_stat: - stat["annotation"] = {} - num_per_images: list[int] = [] - size_of_box_shapes: list[float] = [] - size_of_polygon_shapes: list[float] = [] - for idx in data_ids: - data = dataset.get(id=idx, subset=dataset.name) - annotations: dict[str, list] = {"boxes": [], "polygons": []} - for ann in data.annotations: - if isinstance(ann, Bbox): - annotations["boxes"].append(ann) - elif isinstance(ann, Polygon): - annotations["polygons"].append(ann) - - num_per_images.append(max(len(annotations["boxes"]), len(annotations["polygons"]))) - - if len(size_of_box_shapes) >= max_samples or len(size_of_polygon_shapes) >= max_samples: - continue - - size_of_box_shapes.extend( - filter(lambda x: x >= 1, [np.sqrt(anno.get_area()) for anno in annotations["boxes"]]), - ) - size_of_polygon_shapes.extend( - filter(lambda x: x >= 1, [np.sqrt(anno.get_area()) for anno in annotations["polygons"]]), - ) - - stat["annotation"]["num_per_image"] = compute_robust_statistics(np.array(num_per_images)) - stat["annotation"]["size_of_shape"] = compute_robust_scale_statistics( - np.array(size_of_polygon_shapes) if len(size_of_polygon_shapes) else np.array(size_of_box_shapes), - ) - - return stat - - -def adapt_tile_config(tile_config: TileConfig, dataset: Dataset) -> None: - """Config tile parameters. - - Adapt based on annotation statistics. - i.e. tile size, tile overlap, ratio and max objects per sample - - Args: - tile_config (TileConfig): tiling parameters of the model - dataset (Dataset): Datumaro dataset including all subsets - """ - if (train_dataset := dataset.subsets().get("train")) is not None: - stat = compute_robust_dataset_statistics(train_dataset, ann_stat=True) - max_num_objects = round(stat["annotation"]["num_per_image"]["max"]) - avg_size = stat["annotation"]["size_of_shape"]["avg"] - min_size = stat["annotation"]["size_of_shape"]["robust_min"] - max_size = stat["annotation"]["size_of_shape"]["robust_max"] - log.info(f"----> [stat] scale avg: {avg_size}") - log.info(f"----> [stat] scale min: {min_size}") - log.info(f"----> [stat] scale max: {max_size}") - - log.info("[Adaptive tiling pararms]") - object_tile_ratio = tile_config.object_tile_ratio - tile_size = int(avg_size / object_tile_ratio) - tile_overlap = max_size / tile_size - log.info(f"----> avg_object_size: {avg_size}") - log.info(f"----> max_object_size: {max_size}") - log.info(f"----> object_tile_ratio: {object_tile_ratio}") - log.info(f"----> tile_size: {avg_size} / {object_tile_ratio} = {tile_size}") - log.info(f"----> tile_overlap: {max_size} / {tile_size} = {tile_overlap}") - - if tile_overlap >= 0.9: - # Use the average object area if the tile overlap is too large to prevent 0 stride. - tile_overlap = avg_size / tile_size - log.info(f"----> (too big) tile_overlap: {avg_size} / {tile_size} = {tile_overlap}") - - # TODO(Eugene): how to validate lower/upper_bound? dataclass? pydantic? - # https://github.com/openvinotoolkit/training_extensions/pull/2903 - tile_config.tile_size = (tile_size, tile_size) - tile_config.max_num_instances = max_num_objects - tile_config.overlap = tile_overlap diff --git a/src/otx/core/data/transform_libs/__init__.py b/src/otx/core/data/transform_libs/__init__.py deleted file mode 100644 index 542dd2d23d8..00000000000 --- a/src/otx/core/data/transform_libs/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Helpers to support data transform functions from various frameworks.""" diff --git a/src/otx/core/data/transform_libs/mmaction.py b/src/otx/core/data/transform_libs/mmaction.py deleted file mode 100644 index f1f2f6994f1..00000000000 --- a/src/otx/core/data/transform_libs/mmaction.py +++ /dev/null @@ -1,249 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Helper to support MMAction data transform functions.""" - -from __future__ import annotations - -import os -from copy import deepcopy -from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable - -import mmcv -import numpy as np -from mmaction.datasets.transforms import PackActionInputs as MMPackActionInputs -from mmaction.datasets.transforms import RawFrameDecode as MMRawFrameDecode -from mmaction.registry import TRANSFORMS -from mmengine.fileio import FileClient -from torchvision import tv_tensors - -from otx.core.data.entity.action_classification import ActionClsDataEntity -from otx.core.data.entity.action_detection import ActionDetDataEntity -from otx.core.utils.config import convert_conf_to_mmconfig_dict - -if TYPE_CHECKING: - from mmengine.registry import Registry - - from otx.core.config.data import SubsetConfig - - -@TRANSFORMS.register_module() -class LoadVideoForClassification: - """Class to convert OTXDataEntity to dict for MMAction framework.""" - - def __call__(self, entity: ActionClsDataEntity) -> dict: - """Transform ActionClsDataEntity to MMAction data dictionary format.""" - results: dict[str, Any] = {} - results["filename"] = entity.video.path - results["start_index"] = 0 - results["modality"] = "RGB" - results["__otx__"] = entity - - return results - - -@TRANSFORMS.register_module() -class LoadVideoForDetection: - """Class to convert OTXDataEntity to dict for MMAction framework.""" - - fps: int = 30 - timestamp_start: int = 900 - - def __call__(self, entity: ActionDetDataEntity) -> dict: - """Transform ActionClsDataEntity to MMAction data dictionary format.""" - results: dict[str, Any] = {} - results["modality"] = "RGB" - results["fps"] = self.fps - results["timestamp_start"] = self.timestamp_start - results["filename_tmpl"] = "{video}_{idx:04d}.{ext}" - results["ori_shape"] = entity.img_info.ori_shape - - if entity.frame_path is not None: - frame_dir, extension, shot_info, timestamp = self._get_meta_info(entity.frame_path) - results["timestamp"] = timestamp - results["shot_info"] = shot_info - results["frame_dir"] = frame_dir - results["extension"] = extension - - results["__otx__"] = entity - - return results - - @staticmethod - def _get_meta_info(frame_path: str) -> tuple[Path, str, tuple[int, int], int]: - frame_dir = Path(frame_path).parent - extension = Path(frame_path).suffix[1:] - shot_info = (1, len(os.listdir(frame_dir))) - timestamp = int(Path(frame_path).stem.split("_")[-1]) - - return frame_dir, extension, shot_info, timestamp - - -@TRANSFORMS.register_module() -class LoadAnnotations: - """Load annotation infomation such as ground truth bounding boxes and proposals.""" - - def __call__(self, results: dict) -> dict: - """Get ground truth information from data entity.""" - if (otx_data_entity := results.get("__otx__")) is None: - msg = "__otx__ key should be passed from the previous pipeline (LoadImageFromFile)" - raise RuntimeError(msg) - - results["gt_bboxes"] = otx_data_entity.bboxes.numpy() - results["gt_labels"] = otx_data_entity.labels.numpy() - results["proposals"] = otx_data_entity.proposals - - return results - - -@TRANSFORMS.register_module(force=True) -class RawFrameDecode(MMRawFrameDecode): - """Load and decode frames with given indices. - - This Custom RawFrameDecode pipeline is for Datumaro ava dataset format. - """ - - file_client: FileClient | None - - def transform(self, results: dict) -> dict: - """Perform the ``RawFrameDecode`` to pick frames given indices. - - Args: - results (dict): The resulting dict to be modified and passed - to the next transform in pipeline. - """ - mmcv.use_backend(self.decoding_backend) - - directory = results["frame_dir"] - filename_tmpl = results["filename_tmpl"] - video = Path(results["frame_dir"]).name - ext = results["extension"] - - if self.file_client is None: - self.file_client = FileClient(self.io_backend, **self.kwargs) - - imgs: list[np.ndarray] = [] - - if results["frame_inds"].ndim != 1: - results["frame_inds"] = np.squeeze(results["frame_inds"]) - - offset = results.get("offset", 0) - - cache: dict[int, int] = {} - for i, frame_idx in enumerate(results["frame_inds"]): - # Avoid loading duplicated frames - if frame_idx in cache: - imgs.append(deepcopy(imgs[cache[frame_idx]])) - continue - cache[frame_idx] = i - - frame_idx_with_offset = frame_idx + offset - filepath = Path(directory) / filename_tmpl.format(video=video, idx=frame_idx_with_offset, ext=ext) - img_bytes = self.file_client.get(filepath) - # Get frame with channel order RGB directly. - cur_frame = mmcv.imfrombytes(img_bytes, channel_order="rgb") - imgs.append(cur_frame) - - results["imgs"] = imgs - results["original_shape"] = imgs[0].shape[:2] - results["img_shape"] = imgs[0].shape[:2] - - # we resize the gt_bboxes and proposals to their real scale - if "gt_bboxes" in results: - h, w = results["img_shape"] - scale_factor = np.array([w, h, w, h]) - gt_bboxes = results["gt_bboxes"] - gt_bboxes = (gt_bboxes * scale_factor).astype(np.float32) - results["gt_bboxes"] = gt_bboxes - if "proposals" in results and results["proposals"] is not None: - proposals = results["proposals"] - proposals = (proposals * scale_factor).astype(np.float32) - results["proposals"] = proposals - - return results - - -@TRANSFORMS.register_module(force=True) -class PackActionInputs(MMPackActionInputs): - """Class to override PackActionInputs. - - Transfrom output dictionary from MMAction to ActionClsDataEntity or ActionDetDataEntity. - """ - - def transform(self, results: dict) -> ActionClsDataEntity | ActionDetDataEntity: - """Transform function.""" - transformed = super().transform(results) - image = tv_tensors.Image(transformed.get("inputs")) - data_samples = transformed["data_samples"] - - ori_shape = results["original_shape"] - img_shape = data_samples.img_shape - pad_shape = data_samples.metainfo.get("pad_shape", img_shape) - scale_factor = data_samples.metainfo.get("scale_factor", (1.0, 1.0)) - - data_entity: ActionClsDataEntity | ActionDetDataEntity = results["__otx__"] - - image_info = deepcopy(data_entity.img_info) - image_info.img_shape = img_shape - image_info.ori_shape = ori_shape - image_info.scale_factor = scale_factor - image_info.pad_shape = pad_shape - - labels = data_entity.labels - - if "gt_bboxes" in results: - proposals = tv_tensors.BoundingBoxes( - data_samples.proposals.bboxes.float(), - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_shape, - ) - bboxes = tv_tensors.BoundingBoxes( - data_samples.gt_instances.bboxes.float(), - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_shape, - ) - - return ActionDetDataEntity( - image=image, - img_info=image_info, - bboxes=bboxes, - labels=labels, - proposals=proposals, - frame_path=results["__otx__"].frame_path, - ) - - return ActionClsDataEntity( - video=results["__otx__"].video, - image=image, - img_info=image_info, - labels=labels, - ) - - -class MMActionTransformLib: - """Helper to support MMCV transforms in OTX.""" - - @classmethod - def get_builder(cls) -> Registry: - """Transform builder obtained from MMCV.""" - return TRANSFORMS - - @classmethod - def _check_mandatory_transforms( - cls, - transforms: list[Callable], - mandatory_transforms: set, - ) -> None: - for transform in transforms: - t_transform = type(transform) - mandatory_transforms.discard(t_transform) - - if len(mandatory_transforms) != 0: - msg = f"{mandatory_transforms} should be included" - raise RuntimeError(msg) - - @classmethod - def generate(cls, config: SubsetConfig) -> list[Callable]: - """Generate MMCV transforms from the configuration.""" - return [cls.get_builder().build(convert_conf_to_mmconfig_dict(cfg)) for cfg in config.transforms] diff --git a/src/otx/core/data/transform_libs/mmcv.py b/src/otx/core/data/transform_libs/mmcv.py deleted file mode 100644 index f763aee3be4..00000000000 --- a/src/otx/core/data/transform_libs/mmcv.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Helper to support MMCV data transform functions.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Callable - -import numpy as np -from mmcv.transforms import LoadImageFromFile as MMCVLoadImageFromFile -from mmcv.transforms.builder import TRANSFORMS - -from otx.core.data.entity.base import OTXDataEntity -from otx.core.utils.config import convert_conf_to_mmconfig_dict - -if TYPE_CHECKING: - from mmengine.registry import Registry - - from otx.core.config.data import SubsetConfig - - -@TRANSFORMS.register_module(force=True) -class LoadImageFromFile(MMCVLoadImageFromFile): - """Class to override MMCV LoadImageFromFile.""" - - def transform(self, entity: OTXDataEntity) -> dict | None: - """Transform OTXDataEntity to MMCV data entity format.""" - img: np.ndarray = entity.image - - if self.to_float32: - img = img.astype(np.float32) - - results = {} - results["img"] = img - results["img_shape"] = img.shape[:2] - results["ori_shape"] = img.shape[:2] - - results["__otx__"] = entity - - return results - - -class MMCVTransformLib: - """Helper to support MMCV transforms in OTX.""" - - @classmethod - def get_builder(cls) -> Registry: - """Transform builder obtained from MMCV.""" - return TRANSFORMS - - @classmethod - def _check_mandatory_transforms( - cls, - transforms: list[Callable], - mandatory_transforms: set, - ) -> None: - for transform in transforms: - t_transform = type(transform) - mandatory_transforms.discard(t_transform) - - if len(mandatory_transforms) != 0: - msg = f"{mandatory_transforms} should be included" - raise RuntimeError(msg) - - @classmethod - def generate(cls, config: SubsetConfig) -> list[Callable]: - """Generate MMCV transforms from the configuration.""" - transforms = [cls.get_builder().build(convert_conf_to_mmconfig_dict(cfg)) for cfg in config.transforms] - - cls._check_mandatory_transforms( - transforms, - mandatory_transforms={LoadImageFromFile}, - ) - - return transforms diff --git a/src/otx/core/data/transform_libs/mmdet.py b/src/otx/core/data/transform_libs/mmdet.py deleted file mode 100644 index 68353b4c4d8..00000000000 --- a/src/otx/core/data/transform_libs/mmdet.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Helper to support MMDET data transform functions.""" - -from __future__ import annotations - -import logging as log -from copy import deepcopy -from typing import TYPE_CHECKING, Callable - -import numpy as np -import torch -from datumaro import Polygon -from mmcv.transforms import BaseTransform -from mmdet.datasets.transforms import LoadAnnotations as MMDetLoadAnnotations -from mmdet.datasets.transforms import PackDetInputs as MMDetPackDetInputs -from mmdet.registry import TRANSFORMS as MMDET_TRANSFORMS -from mmdet.structures.mask import BitmapMasks, PolygonMasks -from mmengine.registry import Registry -from torchvision import tv_tensors - -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.detection import DetDataEntity -from otx.core.data.entity.instance_segmentation import InstanceSegDataEntity -from otx.core.data.entity.visual_prompting import VisualPromptingDataEntity - -from .mmcv import MMCVTransformLib - -if TYPE_CHECKING: - from mmdet.structures.det_data_sample import DetDataSample - - from otx.core.config.data import SubsetConfig - -TRANSFORMS = Registry( # to make mmdeploy use mmdet pipeline module - "transform", - scope="otx", - parent=MMDET_TRANSFORMS, - locations=["otx.core.data.transform_libs.mmdet"], -) - - -@TRANSFORMS.register_module(force=True) -class LoadAnnotations(MMDetLoadAnnotations): - """Class to override MMDet LoadAnnotations.""" - - def __init__(self, with_point: bool = False, **kwargs): - super().__init__(**kwargs) - if with_point: - # TODO(sungchul): add point prompts in mmx # noqa: TD003 - log.info("with_point for mmx is not supported yet, changed to False.") - with_point = False - self.with_point = with_point - - def transform(self, results: dict) -> dict: - """Transform OTXDataEntity to MMDet annotation data entity format.""" - if (otx_data_entity := results.get("__otx__")) is None: - msg = "__otx__ key should be passed from the previous pipeline (LoadImageFromFile)" - raise RuntimeError(msg) - - if self.with_bbox and isinstance( - otx_data_entity, - (DetDataEntity, InstanceSegDataEntity, VisualPromptingDataEntity), - ): - gt_bboxes = otx_data_entity.bboxes.numpy() - results["gt_bboxes"] = gt_bboxes - if self.with_label and isinstance( - otx_data_entity, - (DetDataEntity, InstanceSegDataEntity, VisualPromptingDataEntity), - ): - gt_bboxes_labels = otx_data_entity.labels.numpy() # type: ignore[union-attr] - results["gt_bboxes_labels"] = gt_bboxes_labels - results["gt_ignore_flags"] = np.zeros_like(gt_bboxes_labels, dtype=np.bool_) - if self.with_mask and isinstance(otx_data_entity, (InstanceSegDataEntity, VisualPromptingDataEntity)): - height, width = results["ori_shape"] - gt_masks = self._generate_gt_masks(otx_data_entity, height, width) - results["gt_masks"] = gt_masks - if self.with_point and isinstance(otx_data_entity, (VisualPromptingDataEntity)): - # TODO(sungchul): add point prompts in mmx # noqa: TD003 - # gt_points = otx_data_entity.points.numpy() - # results["gt_points"] = gt_points - pass - return results - - def _generate_gt_masks( - self, - otx_data_entity: InstanceSegDataEntity | VisualPromptingDataEntity, - height: int, - width: int, - ) -> BitmapMasks | PolygonMasks: - """Generate ground truth masks based on the given otx_data_entity. - - Args: - otx_data_entity (OTXDataEntity): The data entity containing the masks or polygons. - height (int): The height of the masks. - width (int): The width of the masks. - - Returns: - gt_masks (BitmapMasks or PolygonMasks): The generated ground truth masks. - """ - if len(otx_data_entity.masks): - gt_masks = BitmapMasks(otx_data_entity.masks.numpy(), height, width) - else: - gt_masks = PolygonMasks( - [[np.array(polygon.points)] for polygon in otx_data_entity.polygons], - height, - width, - ) - return gt_masks - - -@TRANSFORMS.register_module(force=True) -class PackDetInputs(MMDetPackDetInputs): - """Class to override PackDetInputs LoadAnnotations.""" - - def transform(self, results: dict) -> DetDataEntity | InstanceSegDataEntity | VisualPromptingDataEntity: - """Pack MMDet data entity into DetDataEntity, InstanceSegDataEntity, or VisualPromptingDataEntity.""" - otx_data_entity = results["__otx__"] - - if isinstance(otx_data_entity, DetDataEntity): - return self.pack_det_inputs(results) - if isinstance(otx_data_entity, InstanceSegDataEntity): - return self.pack_inst_inputs(results) - if isinstance(otx_data_entity, VisualPromptingDataEntity): - return self.pack_visprompt_inputs(results) - msg = "Unsupported data entity type" - raise TypeError(msg) - - def pack_det_inputs(self, results: dict) -> DetDataEntity: - """Pack MMDet data entity into DetDataEntity.""" - transformed = super().transform(results) - data_samples = transformed["data_samples"] - image_info = self.create_image_info(src_image_info=results["__otx__"].img_info, data_samples=data_samples) - - bboxes = self.convert_bboxes(data_samples.gt_instances.bboxes, image_info.img_shape) - labels = data_samples.gt_instances.labels - - return DetDataEntity( - image=tv_tensors.Image(transformed.get("inputs")), - img_info=image_info, - bboxes=bboxes, - labels=labels, - ) - - def pack_inst_inputs(self, results: dict) -> InstanceSegDataEntity: - """Pack MMDet data entity into InstanceSegDataEntity.""" - transformed = super().transform(results) - data_samples = transformed["data_samples"] - image_info = self.create_image_info(src_image_info=results["__otx__"].img_info, data_samples=data_samples) - - bboxes = self.convert_bboxes(data_samples.gt_instances.bboxes, image_info.img_shape) - labels = data_samples.gt_instances.labels - - masks, polygons = self.convert_masks_and_polygons(data_samples.gt_instances.masks) - - return InstanceSegDataEntity( - image=tv_tensors.Image(transformed.get("inputs")), - img_info=image_info, - bboxes=bboxes, - masks=masks, - labels=labels, - polygons=polygons, - ) - - def pack_visprompt_inputs(self, results: dict) -> VisualPromptingDataEntity: - """Pack MMDet data entity into VisualPromptingDataEntity.""" - transformed = super().transform(results) - data_samples = transformed["data_samples"] - image_info = self.create_image_info(src_image_info=results["__otx__"].img_info, data_samples=data_samples) - - bboxes = self.convert_bboxes(data_samples.gt_instances.bboxes, image_info.img_shape) - labels = data_samples.gt_instances.labels - - return VisualPromptingDataEntity( - image=tv_tensors.Image(transformed.get("inputs")), - img_info=image_info, - bboxes=bboxes, - points=None, # type: ignore[arg-type] - masks=None, - labels=labels, - polygons=None, # type: ignore[arg-type] - ) - - def create_image_info( - self, - src_image_info: ImageInfo, - data_samples: DetDataSample, - ) -> ImageInfo: - """Create ImageInfo instance from data_samples.""" - # Some MM* transforms return (H, W, C), not (H, W) - img_shape = data_samples.img_shape if len(data_samples.img_shape) == 2 else data_samples.img_shape[:2] - ori_shape = data_samples.ori_shape if len(data_samples.ori_shape) == 2 else data_samples.ori_shape[:2] - pad_shape = data_samples.metainfo.get("pad_shape", img_shape) - if len(pad_shape) == 3: - pad_shape = pad_shape[:2] - scale_factor = data_samples.metainfo.get("scale_factor", (1.0, 1.0)) - - image_info = deepcopy(src_image_info) - image_info.img_shape = img_shape - image_info.ori_shape = ori_shape - image_info.scale_factor = scale_factor - image_info.pad_shape = pad_shape - - return image_info - - def convert_bboxes(self, original_bboxes: torch.Tensor, img_shape: tuple[int, int]) -> tv_tensors.BoundingBoxes: - """Convert bounding boxes to tv_tensors.BoundingBoxes format.""" - return tv_tensors.BoundingBoxes( - original_bboxes.float(), - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_shape, - ) - - def convert_masks_and_polygons(self, masks: BitmapMasks | PolygonMasks) -> tuple[tv_tensors.Mask, list[Polygon]]: - """Convert masks and polygons to the desired format.""" - if isinstance(masks, BitmapMasks): - masks_tensor = tv_tensors.Mask(masks.to_ndarray(), dtype=torch.int8) - else: - masks_tensor = tv_tensors.Mask(torch.empty(0)) - - polygons = [Polygon(polygon[0]) for polygon in masks.masks] if isinstance(masks, PolygonMasks) else [] - - return masks_tensor, polygons - - -@TRANSFORMS.register_module() -class PerturbBoundingBoxes(BaseTransform): - """Perturb bounding boxes with random offset values. - - Args: - offset (int): Offset value to be used for bounding boxes perturbation. - """ - - def __init__(self, offset: int): - self.offset = offset - - def transform(self, results: dict) -> dict: - """Insert random perturbation into bounding boxes.""" - height, width = results["img_shape"] - perturbed_bboxes: list[np.ndarray] = [] - for bbox in results["gt_bboxes"]: - perturbed_bbox = self.get_perturbed_bbox(bbox, width, height, self.offset) - perturbed_bboxes.append(perturbed_bbox) - results["gt_bboxes"] = np.stack(perturbed_bboxes, axis=0) - return results - - def get_perturbed_bbox( - self, - bbox: np.ndarray, - width: int, - height: int, - offset_bbox: int = 0, - ) -> list[int]: - """Generate bounding box. - - Args: - bbox (np.ndarray): Bounding box coordinates. - width (int): Width of image. - height (int): Height of image. - offset_bbox (int): Offset to apply to the bounding box, defaults to 0. - - Returns: - List[int]: Generated bounding box. - """ - - def get_randomness(length: int) -> int: - if offset_bbox == 0: - return 0 - return np.random.normal(0, min(length * 0.1, offset_bbox)) # noqa: NPY002 - - x1, y1, x2, y2 = bbox - return np.array( - [ - max(0, x1 + get_randomness(width)), - max(0, y1 + get_randomness(height)), - min(width, x2 + get_randomness(width)), - min(height, y2 + get_randomness(height)), - ], - ) - - -class MMDetTransformLib(MMCVTransformLib): - """Helper to support MMDET transforms in OTX.""" - - @classmethod - def get_builder(cls) -> Registry: - """Transform builder obtained from MMDet.""" - return TRANSFORMS - - @classmethod - def generate(cls, config: SubsetConfig) -> list[Callable]: - """Generate MMDET transforms from the configuration.""" - transforms = super().generate(config) - - cls._check_mandatory_transforms( - transforms, - mandatory_transforms={LoadAnnotations, PackDetInputs}, - ) - - return transforms diff --git a/src/otx/core/data/transform_libs/mmpretrain.py b/src/otx/core/data/transform_libs/mmpretrain.py deleted file mode 100644 index bbd02e38ef7..00000000000 --- a/src/otx/core/data/transform_libs/mmpretrain.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Helper to support MMPretrain data transform functions.""" - -from __future__ import annotations - -from copy import deepcopy -from typing import TYPE_CHECKING, Any, Callable - -from mmpretrain.datasets.transforms import ( - PackInputs as MMPretrainPackInputs, -) -from mmpretrain.registry import TRANSFORMS -from torchvision import tv_tensors - -from otx.core.data.entity.classification import HlabelClsDataEntity, MulticlassClsDataEntity, MultilabelClsDataEntity - -from .mmcv import LoadImageFromFile, MMCVTransformLib - -TRANSFORMS.register_module(module=LoadImageFromFile, force=True) - -if TYPE_CHECKING: - from mmengine.registry import Registry - - from otx.core.config.data import SubsetConfig - - -@TRANSFORMS.register_module(force=True) -class PackInputs(MMPretrainPackInputs): - """Class to override PackInputs.""" - - def transform(self, results: dict) -> MulticlassClsDataEntity | MultilabelClsDataEntity | HlabelClsDataEntity: - """Pack MMPretrain data entity into MulticlassClsDataEntity.""" - otx_data_entity = results["__otx__"] - - if isinstance(otx_data_entity, MulticlassClsDataEntity): - return self._pack_multiclass_inputs(results) - if isinstance(otx_data_entity, MultilabelClsDataEntity): - return self._pack_multilabel_inputs(results) - if isinstance(otx_data_entity, HlabelClsDataEntity): - return self._pack_hlabel_inputs(results) - - msg = "Unsupported data entity type" - raise TypeError(msg) - - def _pack_common_inputs(self, results: dict) -> dict[str, Any]: - transformed = super().transform(results) - image = tv_tensors.Image(transformed.get("inputs")) - data_samples = transformed["data_samples"] - - # Some MM* transforms return (H, W, C), not (H, W) - img_shape = data_samples.img_shape if len(data_samples.img_shape) == 2 else data_samples.img_shape[:2] - ori_shape = data_samples.ori_shape - pad_shape = data_samples.metainfo.get("pad_shape", img_shape) - scale_factor = data_samples.metainfo.get("scale_factor", (1.0, 1.0)) - - data_entity: MulticlassClsDataEntity | MultilabelClsDataEntity | HlabelClsDataEntity = results["__otx__"] - - image_info = deepcopy(data_entity.img_info) - image_info.img_shape = img_shape - image_info.ori_shape = ori_shape - image_info.scale_factor = scale_factor - image_info.pad_shape = pad_shape - - return { - "image": image, - "image_info": image_info, - "labels": results["__otx__"].labels, - } - - def _pack_multiclass_inputs(self, results: dict) -> MulticlassClsDataEntity: - """Pack multiclass classification inputs.""" - packed_common_inputs = self._pack_common_inputs(results) - - return MulticlassClsDataEntity( - image=packed_common_inputs["image"], - img_info=packed_common_inputs["image_info"], - labels=packed_common_inputs["labels"], - ) - - def _pack_multilabel_inputs(self, results: dict) -> MultilabelClsDataEntity: - """Pack multilabel classification inputs. - - NOTE, - Currently, this function is the same with multiclass case. - However, it should have different functionality if we consider the ignore_label. - That's the reason why I reamined same function. - """ - packed_common_inputs = self._pack_common_inputs(results) - - return MultilabelClsDataEntity( - image=packed_common_inputs["image"], - img_info=packed_common_inputs["image_info"], - labels=packed_common_inputs["labels"], - ) - - def _pack_hlabel_inputs(self, results: dict) -> HlabelClsDataEntity: - """Pack hlabel classification inputs.""" - packed_common_inputs = self._pack_common_inputs(results) - - return HlabelClsDataEntity( - image=packed_common_inputs["image"], - img_info=packed_common_inputs["image_info"], - labels=packed_common_inputs["labels"], - ) - - -class MMPretrainTransformLib(MMCVTransformLib): - """Helper to support MMPretrain transforms in OTX.""" - - @classmethod - def get_builder(cls) -> Registry: - """Transform builder obtained from MMPretrain.""" - return TRANSFORMS - - @classmethod - def generate(cls, config: SubsetConfig) -> list[Callable]: - """Generate MMPretrain transforms from the configuration.""" - transforms = super().generate(config) - - cls._check_mandatory_transforms( - transforms, - mandatory_transforms={PackInputs}, - ) - - return transforms diff --git a/src/otx/core/data/transform_libs/mmseg.py b/src/otx/core/data/transform_libs/mmseg.py deleted file mode 100644 index 71e52b90c2b..00000000000 --- a/src/otx/core/data/transform_libs/mmseg.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Helper to support MMPretrain data transform functions.""" - -from __future__ import annotations - -from copy import deepcopy -from typing import TYPE_CHECKING, Callable - -from mmseg.datasets.transforms import ( - LoadAnnotations as MMSegLoadAnnotations, -) -from mmseg.datasets.transforms import ( - PackSegInputs as MMSegPackInputs, -) -from mmseg.registry import TRANSFORMS -from torchvision import tv_tensors - -from otx.core.data.entity.segmentation import SegDataEntity - -from .mmcv import MMCVTransformLib - -if TYPE_CHECKING: - from mmengine.registry import Registry - - from otx.core.config.data import SubsetConfig - - -@TRANSFORMS.register_module(force=True) -class LoadAnnotations(MMSegLoadAnnotations): - """Class to override MMSeg LoadAnnotations.""" - - def transform(self, results: dict) -> dict: - """Transform OTXDataEntity to MMSeg annotation data entity format.""" - if (otx_data_entity := results.get("__otx__")) is None: - msg = "__otx__ key should be passed from the previous pipeline (LoadImageFromFile)" - raise RuntimeError(msg) - if isinstance(otx_data_entity, SegDataEntity): - gt_masks = otx_data_entity.gt_seg_map.numpy() - results["gt_seg_map"] = gt_masks - # we need this to properly handle seg maps during transforms - results["seg_fields"] = ["gt_seg_map"] - - return results - - -@TRANSFORMS.register_module(force=True) -class PackSegInputs(MMSegPackInputs): - """Class to override PackInputs.""" - - def transform(self, results: dict) -> SegDataEntity: - """Pack MMSeg data entity into SegDataEntity.""" - transformed = super().transform(results) - image = tv_tensors.Image(transformed.get("inputs")) - data_samples = transformed["data_samples"] - - img_shape = data_samples.img_shape - ori_shape = data_samples.ori_shape - pad_shape = data_samples.metainfo.get("pad_shape", img_shape) - scale_factor = data_samples.metainfo.get("scale_factor", (1.0, 1.0)) - - image_info = deepcopy(results["__otx__"].img_info) - image_info.img_shape = img_shape - image_info.ori_shape = ori_shape - image_info.scale_factor = scale_factor - image_info.pad_shape = pad_shape - - masks = data_samples.gt_sem_seg.data - - data_entity = SegDataEntity( - image=image, - img_info=image_info, - gt_seg_map=masks, - ) - data_entity.img_info.pad_shape = pad_shape - return data_entity - - -class MMSegTransformLib(MMCVTransformLib): - """Helper to support MMSeg transforms in OTX.""" - - @classmethod - def get_builder(cls) -> Registry: - """Transform builder obtained from MMSeg.""" - return TRANSFORMS - - @classmethod - def generate(cls, config: SubsetConfig) -> list[Callable]: - """Generate MMSeg transforms from the configuration.""" - transforms = super().generate(config) - - cls._check_mandatory_transforms( - transforms, - mandatory_transforms={LoadAnnotations, PackSegInputs}, - ) - - return transforms diff --git a/src/otx/core/data/transform_libs/torchvision.py b/src/otx/core/data/transform_libs/torchvision.py deleted file mode 100644 index a99cff29cdb..00000000000 --- a/src/otx/core/data/transform_libs/torchvision.py +++ /dev/null @@ -1,329 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Helper to support TorchVision data transform functions.""" - -from __future__ import annotations - -from inspect import isclass -from typing import TYPE_CHECKING, Any - -import numpy as np -import PIL.Image -import torch -import torchvision.transforms.v2 as tvt_v2 -from datumaro.components.media import Video -from lightning.pytorch.cli import instantiate_class -from omegaconf import DictConfig -from torchvision import tv_tensors -from torchvision._utils import sequence_to_str -from torchvision.transforms.v2 import functional as F # noqa: N812 - -from otx.core.data.entity.action_classification import ActionClsDataEntity -from otx.core.data.entity.base import Points - -if TYPE_CHECKING: - from torchvision.transforms.v2 import Compose - - from otx.core.config.data import SubsetConfig - - -def custom_query_size(flat_inputs: list[Any]) -> tuple[int, int]: # noqa: D103 - sizes = { - tuple(F.get_size(inpt)) - for inpt in flat_inputs - if tvt_v2._utils.check_type( # noqa: SLF001 - inpt, - ( - F.is_pure_tensor, - tv_tensors.Image, - PIL.Image.Image, - tv_tensors.Video, - tv_tensors.Mask, - tv_tensors.BoundingBoxes, - Points, - ), - ) - } - if not sizes: - raise TypeError("No image, video, mask, bounding box, or point was found in the sample") # noqa: EM101, TRY003 - elif len(sizes) > 1: # noqa: RET506 - raise ValueError(f"Found multiple HxW dimensions in the sample: {sequence_to_str(sorted(sizes))}") # noqa: EM102, TRY003 - h, w = sizes.pop() - return h, w - - -tvt_v2._utils.query_size = custom_query_size # noqa: SLF001 - - -class PerturbBoundingBoxes(tvt_v2.Transform): - """Perturb bounding boxes with random offset value.""" - - def __init__(self, offset: int) -> None: - super().__init__() - self.offset = offset - - def _transform(self, inpt: Any, params: dict[str, Any]) -> Any: # noqa: ANN401 - output = self._perturb_bounding_boxes(inpt, self.offset) - return tv_tensors.wrap(output, like=inpt) - - def _perturb_bounding_boxes(self, inpt: torch.Tensor, offset: int) -> torch.Tensor: - mean = torch.zeros_like(inpt) - repeated_size = torch.tensor(inpt.canvas_size).repeat(len(inpt), 2) - std = torch.minimum(repeated_size * 0.1, torch.tensor(offset)) - noise = torch.normal(mean, std) - return (inpt + noise).clamp(mean, repeated_size - 1) - - -class PadtoSquare(tvt_v2.Transform): - """Pad skewed image to square with zero padding.""" - - def _get_params(self, flat_inputs: list[Any]) -> dict[str, Any]: - height, width = tvt_v2._utils.query_size(flat_inputs) # noqa: SLF001 - max_dim = max(width, height) - pad_w = max_dim - width - pad_h = max_dim - height - padding = (0, 0, pad_w, pad_h) - return {"padding": padding} - - def _transform(self, inpt: Any, params: dict[str, Any]) -> Any: # noqa: ANN401 - return self._call_kernel(F.pad, inpt, padding=params["padding"], fill=0, padding_mode="constant") - - -class ResizetoLongestEdge(tvt_v2.Transform): - """Resize image along with the longest edge.""" - - def __init__( - self, - size: int, - interpolation: F.InterpolationMode | int = F.InterpolationMode.BILINEAR, - antialias: str | bool = "warn", - ) -> None: - super().__init__() - - self.size = size - self.interpolation = F._geometry._check_interpolation(interpolation) # noqa: SLF001 - self.antialias = antialias - - def _get_params(self, flat_inputs: list[Any]) -> dict[str, Any]: - height, width = tvt_v2._utils.query_size(flat_inputs) # noqa: SLF001 - target_size = self._get_preprocess_shape(height, width, self.size) - return {"target_size": target_size} - - def _transform(self, inpt: Any, params: dict[str, Any]) -> Any: # noqa: ANN401 - return self._call_kernel( - F.resize, - inpt, - params["target_size"], - interpolation=self.interpolation, - max_size=None, - antialias=self.antialias, - ) - - def _get_preprocess_shape(self, oldh: int, oldw: int, long_side_length: int) -> tuple[int, int]: - scale = long_side_length * 1.0 / max(oldh, oldw) - newh, neww = oldh * scale, oldw * scale - neww = int(neww + 0.5) - newh = int(newh + 0.5) - return (newh, neww) - - -class DecodeVideo(tvt_v2.Transform): - """Sample video frames from original data video.""" - - def __init__( - self, - test_mode: bool, - clip_len: int = 8, - frame_interval: int = 4, - num_clips: int = 1, - out_of_bound_opt: str = "loop", - ) -> None: - super().__init__() - self.test_mode = test_mode - self.clip_len = clip_len - self.frame_interval = frame_interval - self.num_clips = num_clips - self.out_of_bound_opt = out_of_bound_opt - self._transformed_types = [Video] - - def _transform(self, inpt: Video, params: dict) -> tv_tensors.Video: - total_frames = self._get_total_frames(inpt) - fps_scale_ratio = 1.0 - ori_clip_len = self._get_ori_clip_len(fps_scale_ratio) - clip_offsets = self._sample_clips(total_frames, ori_clip_len) - - frame_inds = clip_offsets[:, None] + np.arange(self.clip_len)[None, :] * self.frame_interval - frame_inds = np.concatenate(frame_inds) - - frame_inds = frame_inds.reshape((-1, self.clip_len)) - if self.out_of_bound_opt == "loop": - frame_inds = np.mod(frame_inds, total_frames) - elif self.out_of_bound_opt == "repeat_last": - safe_inds = frame_inds < total_frames - unsafe_inds = 1 - safe_inds - last_ind = np.max(safe_inds * frame_inds, axis=1) - new_inds = safe_inds * frame_inds + (unsafe_inds.T * last_ind).T - frame_inds = new_inds - else: - msg = "Illegal out_of_bound optio." - raise ValueError(msg) - - start_index = 0 - frame_inds = np.concatenate(frame_inds) + start_index - - outputs = torch.stack([torch.tensor(inpt[idx].data) for idx in frame_inds], dim=0) - outputs = outputs.permute(0, 3, 1, 2) - outputs = tv_tensors.Video(outputs) - inpt.close() - - return outputs - - @staticmethod - def _get_total_frames(inpt: Video) -> int: - length = 0 - for _ in inpt: - length += 1 - return length - - def _get_train_clips(self, num_frames: int, ori_clip_len: float) -> np.array: - """Get clip offsets in train mode. - - It will calculate the average interval for selected frames, - and randomly shift them within offsets between [0, avg_interval]. - If the total number of frames is smaller than clips num or origin - frames length, it will return all zero indices. - - Args: - num_frames (int): Total number of frame in the video. - ori_clip_len (float): length of original sample clip. - - Returns: - np.ndarray: Sampled frame indices in train mode. - """ - avg_interval = (num_frames - ori_clip_len + 1) // self.num_clips - - if avg_interval > 0: - base_offsets = np.arange(self.num_clips) * avg_interval - clip_offsets = base_offsets + np.random.default_rng().integers(avg_interval, size=self.num_clips) - elif num_frames > max(self.num_clips, ori_clip_len): - clip_offsets = np.sort(np.random.default_rng().integers(num_frames - ori_clip_len + 1, size=self.num_clips)) - elif avg_interval == 0: - ratio = (num_frames - ori_clip_len + 1.0) / self.num_clips - clip_offsets = np.around(np.arange(self.num_clips) * ratio) - else: - clip_offsets = np.zeros((self.num_clips,), dtype=np.int32) - - return clip_offsets - - def _get_test_clips(self, num_frames: int, ori_clip_len: float) -> np.array: - """Get clip offsets in test mode. - - If the total number of frames is - not enough, it will return all zero indices. - - Args: - num_frames (int): Total number of frame in the video. - ori_clip_len (float): length of original sample clip. - - Returns: - np.ndarray: Sampled frame indices in test mode. - """ - max_offset = max(num_frames - ori_clip_len, 0) - if self.num_clips > 1: - num_segments = self.num_clips - 1 - # align test sample strategy with `PySlowFast` repo - offset_between = max_offset / float(num_segments) - clip_offsets = np.arange(self.num_clips) * offset_between - clip_offsets = np.round(clip_offsets) - else: - clip_offsets = np.array([max_offset // 2]) - return clip_offsets - - def _sample_clips(self, num_frames: int, ori_clip_len: float) -> np.array: - """Choose clip offsets for the video in a given mode. - - Args: - num_frames (int): Total number of frame in the video. - - Returns: - np.ndarray: Sampled frame indices. - """ - if self.test_mode: - clip_offsets = self._get_test_clips(num_frames, ori_clip_len) - else: - clip_offsets = self._get_train_clips(num_frames, ori_clip_len) - - return clip_offsets - - def _get_ori_clip_len(self, fps_scale_ratio: float) -> float: - """Calculate length of clip segment for different strategy. - - Args: - fps_scale_ratio (float): Scale ratio to adjust fps. - """ - if self.test_mode: - ori_clip_len = (self.clip_len - 1) * self.frame_interval + 1 - else: - ori_clip_len = self.clip_len * self.frame_interval - - return ori_clip_len - - -class PackVideo(tvt_v2.Transform): - """Pack video for batch entity.""" - - def forward(self, *inputs: ActionClsDataEntity) -> ActionClsDataEntity: - """Replace ActionClsDataEntity's image to ActionClsDataEntity's video.""" - inputs[0].image = inputs[0].video - inputs[0].video = [] - - return inputs[0] - - -tvt_v2.PerturbBoundingBoxes = PerturbBoundingBoxes -tvt_v2.PadtoSquare = PadtoSquare -tvt_v2.ResizetoLongestEdge = ResizetoLongestEdge - - -class TorchVisionTransformLib: - """Helper to support TorchVision transforms (only V2) in OTX.""" - - @classmethod - def list_available_transforms(cls) -> list[type[tvt_v2.Transform]]: - """List available TorchVision transform (only V2) classes.""" - return [ - obj - for name in dir(tvt_v2) - if (obj := getattr(tvt_v2, name)) and isclass(obj) and issubclass(obj, tvt_v2.Transform) - ] - - @classmethod - def generate(cls, config: SubsetConfig) -> Compose: - """Generate TorchVision transforms from the configuration.""" - if isinstance(config.transforms, tvt_v2.Compose): - return config.transforms - - transforms = [] - for cfg_transform in config.transforms: - transform = cls._dispatch_transform(cfg_transform) - transforms.append(transform) - - return tvt_v2.Compose(transforms) - - @classmethod - def _dispatch_transform(cls, cfg_transform: DictConfig | dict | tvt_v2.Transform) -> tvt_v2.Transform: - if isinstance(cfg_transform, (DictConfig, dict)): - transform = instantiate_class(args=(), init=cfg_transform) - - elif isinstance(cfg_transform, tvt_v2.Transform): - transform = cfg_transform - else: - msg = ( - "TorchVisionTransformLib accepts only three types " - "for config.transforms: DictConfig | dict | tvt_v2.Transform. " - f"However, its type is {type(cfg_transform)}." - ) - raise TypeError(msg) - - return transform diff --git a/src/otx/core/exporter/__init__.py b/src/otx/core/exporter/__init__.py deleted file mode 100644 index ec7734a39b8..00000000000 --- a/src/otx/core/exporter/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for model exporters.""" diff --git a/src/otx/core/exporter/base.py b/src/otx/core/exporter/base.py deleted file mode 100644 index b29946c78b4..00000000000 --- a/src/otx/core/exporter/base.py +++ /dev/null @@ -1,275 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for base model exporter used in OTX.""" - -from __future__ import annotations - -import json -import os -import tempfile -from abc import abstractmethod -from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal -from zipfile import ZipFile - -from otx.core.exporter.exportable_code import demo -from otx.core.types.export import OTXExportFormatType -from otx.core.types.precision import OTXPrecisionType - -if TYPE_CHECKING: - import onnx - import openvino - import torch - - -class OTXModelExporter: - """Base class for the model exporters used in OTX. - - Args: - input_size (tuple[int, ...]): Input shape. - mean (tuple[float, float, float], optional): Mean values of 3 channels. Defaults to (0.0, 0.0, 0.0). - std (tuple[float, float, float], optional): Std values of 3 channels. Defaults to (1.0, 1.0, 1.0). - resize_mode (Literal["crop", "standard", "fit_to_window", "fit_to_window_letterbox"], optional): - A resize type for model preprocess. "standard" resizes images without keeping ratio. - "fit_to_window" resizes images while keeping ratio. - "fit_to_window_letterbox" resizes images and pads images to fit the size. Defaults to "standard". - pad_value (int, optional): Padding value. Defaults to 0. - swap_rgb (bool, optional): Whether to convert the image from BGR to RGB Defaults to False. - metadata (dict[tuple[str, str],str] | None, optional): metadata to embed to the exported model. - output_names (list[str] | None, optional): Names for model's outputs, which would be - embedded into resulting model. - """ - - def __init__( - self, - input_size: tuple[int, ...], - mean: tuple[float, float, float] = (0.0, 0.0, 0.0), - std: tuple[float, float, float] = (1.0, 1.0, 1.0), - resize_mode: Literal["crop", "standard", "fit_to_window", "fit_to_window_letterbox"] = "standard", - pad_value: int = 0, - swap_rgb: bool = False, - metadata: dict[tuple[str, str], str] | None = None, - output_names: list[str] | None = None, - ) -> None: - self.input_size = input_size - self.mean = mean - self.std = std - self.resize_mode = resize_mode - self.pad_value = pad_value - self.swap_rgb = swap_rgb - self.metadata = metadata - self.output_names = output_names - - def export( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - export_format: OTXExportFormatType = OTXExportFormatType.OPENVINO, - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> Path: - """Exports input model to the specified deployable format, such as OpenVINO IR or ONNX. - - Args: - model (torch.nn.Module): pytorch model top export - output_dir (Path): path to the directory to store export artifacts - base_model_name (str, optional): exported model name - format (OTXExportFormatType): final format of the exported model - precision (OTXExportPrecisionType, optional): precision of the exported model's weights - - Returns: - Path: path to the exported model - """ - if export_format == OTXExportFormatType.OPENVINO: - return self.to_openvino(model, output_dir, base_model_name, precision) - if export_format == OTXExportFormatType.ONNX: - return self.to_onnx(model, output_dir, base_model_name, precision) - if export_format == OTXExportFormatType.EXPORTABLE_CODE: - return self.to_exportable_code(model, output_dir, base_model_name, precision) - - msg = f"Unsupported export format: {export_format}" - raise ValueError(msg) - - @abstractmethod - def to_openvino( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> Path: - """Export to OpenVINO Intermediate Representation format. - - Args: - model (torch.nn.Module): pytorch model top export - output_dir (Path): path to the directory to store export artifacts - base_model_name (str, optional): exported model name - precision (OTXExportPrecisionType, optional): precision of the exported model's weights - - Returns: - Path: path to the exported model. - """ - - @abstractmethod - def to_onnx( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - embed_metadata: bool = True, - ) -> Path: - """Abstract method for ONNX export. - - Converts the given torch model to ONNX format and saves it to the specified output directory. - - Args: - model (torch.nn.Module): The input PyTorch model to be converted. - output_dir (Path): The directory where the ONNX model will be saved. - base_model_name (str, optional): The name of the exported ONNX model. Defaults to "exported_model". - precision (OTXPrecisionType, optional): The precision type for the exported model. - Defaults to OTXPrecisionType.FP32. - embed_metadata (bool, optional): Flag to embed metadata in the exported ONNX model. Defaults to True. - - Returns: - Path: The file path where the ONNX model is saved. - """ - - def to_exportable_code( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> Path: - """Export to zip folder final OV IR model with runable demo. - - Args: - model (torch.nn.Module): pytorch model top export - output_dir (Path): path to the directory to store export artifacts - base_model_name (str, optional): exported model name - precision (OTXExportPrecisionType, optional): precision of the exported model's weights - - Returns: - Path: path to the exported model. - """ - work_dir = Path(demo.__file__).parent - parameters: dict[str, Any] = {} - if self.metadata is not None: - parameters["type_of_model"] = self.metadata.get(("model_info", "task_type"), "") - parameters["converter_type"] = self.metadata.get(("model_info", "model_type"), "") - parameters["model_parameters"] = { - "labels": self.metadata.get(("model_info", "labels"), ""), - "labels_ids": self.metadata.get(("model_info", "label_ids"), ""), - } - - output_zip_path = output_dir / "exportable_code.zip" - Path.mkdir(output_dir, exist_ok=True) - with tempfile.TemporaryDirectory() as temp_dir, ZipFile(output_zip_path, "x") as arch: - # model files - path_to_model = self.to_openvino(model, Path(temp_dir), base_model_name, precision) - arch.write(str(path_to_model), Path("model") / "model.xml") - arch.write(path_to_model.with_suffix(".bin"), Path("model") / "model.bin") - - arch.writestr( - str(Path("model") / "config.json"), - json.dumps(parameters, ensure_ascii=False, indent=4), - ) - # python files - arch.write( - work_dir / "requirements.txt", - Path("python") / "requirements.txt", - ) - arch.write(work_dir.parents[5] / "LICENSE", Path("python") / "LICENSE") - arch.write(work_dir / "demo.py", Path("python") / "demo.py") - arch.write(work_dir / "README.md", Path("./") / "README.md") - arch.write(work_dir / "setup.py", Path("python") / "setup.py") - # write demo_package - demo_package = work_dir / "demo_package" - for root, _, files in os.walk(demo_package): - if root.endswith("__pycache__"): - continue - for file in files: - file_path = Path(root) / file - archive_path = file_path.relative_to(demo_package) - arch.write(file_path, Path("python") / "demo_package" / archive_path) - return output_zip_path - - @staticmethod - def _embed_onnx_metadata(onnx_model: onnx.ModelProto, metadata: dict[tuple[str, str], Any]) -> onnx.ModelProto: - """Embeds metadata to ONNX model.""" - for item in metadata: - meta = onnx_model.metadata_props.add() - attr_path = " ".join(map(str, item)) - meta.key = attr_path.strip() - meta.value = str(metadata[item]) - - return onnx_model - - @staticmethod - def _embed_openvino_ir_metadata(ov_model: openvino.Model, metadata: dict[tuple[str, str], Any]) -> openvino.Model: - """Embeds metadata to OpenVINO model.""" - for k, data in metadata.items(): - ov_model.set_rt_info(data, list(k)) - - return ov_model - - def _extend_model_metadata(self, metadata: dict[tuple[str, str], str]) -> dict[tuple[str, str], str]: - """Extends metadata coming from model with preprocessing-specific parameters. - - Model's original metadata has priority over exporter's extra metadata - - Args: - metadata (dict[tuple[str, str], str]): existing metadata for export - - Returns: - dict[tuple[str, str] ,str]: updated metadata - """ - mean_str = " ".join(map(str, self.mean)) - std_str = " ".join(map(str, self.std)) - - extra_data = { - ("model_info", "mean_values"): mean_str.strip(), - ("model_info", "scale_values"): std_str.strip(), - ("model_info", "resize_type"): self.resize_mode, - ("model_info", "pad_value"): str(self.pad_value), - ("model_info", "reverse_input_channels"): str(self.swap_rgb), - } - extra_data.update(metadata) - - return extra_data - - def _postprocess_openvino_model(self, exported_model: openvino.Model) -> openvino.Model: - if self.output_names is not None: - if len(self.output_names) != len(exported_model.outputs): - msg = "The number of outputs in the exported model doesn't match with exporter parameters" - raise RuntimeError(msg) - for i, name in enumerate(self.output_names): - exported_model.outputs[i].tensor.set_names({name}) - elif len(exported_model.outputs) == 1 and len(exported_model.outputs[0].get_names()) == 0: - # workaround for OVC's bug: single output doesn't have a name in OV model - exported_model.outputs[0].tensor.set_names({"output1"}) - - if self.metadata is not None: - export_metadata = self._extend_model_metadata(self.metadata) - exported_model = self._embed_openvino_ir_metadata(exported_model, export_metadata) - - return exported_model - - def _postprocess_onnx_model( - self, - onnx_model: onnx.ModelProto, - embed_metadata: bool, - precision: OTXPrecisionType, - ) -> onnx.ModelProto: - if embed_metadata: - metadata = {} if self.metadata is None else self._extend_model_metadata(self.metadata) - onnx_model = self._embed_onnx_metadata(onnx_model, metadata) - - if precision == OTXPrecisionType.FP16: - from onnxconverter_common import float16 - - onnx_model = float16.convert_float_to_float16(onnx_model) - - return onnx_model diff --git a/src/otx/core/exporter/exportable_code/__init__.py b/src/otx/core/exporter/exportable_code/__init__.py deleted file mode 100644 index c6441f18400..00000000000 --- a/src/otx/core/exporter/exportable_code/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Methods for exportable code.""" diff --git a/src/otx/core/exporter/exportable_code/demo/README.md b/src/otx/core/exporter/exportable_code/demo/README.md deleted file mode 100644 index 12b94a05401..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/README.md +++ /dev/null @@ -1,164 +0,0 @@ -# Exportable code - -Exportable code is a .zip archive that contains simple demo to get and visualize result of model inference. - -## Structure of generated zip - -- `README.md` -- model - - `model.xml` - - `model.bin` - - `config.json` -- python - - demo_package - - `__init__.py` - - executors - - `__init__.py` - - `asynchronous.py` - - `synchronous.py` - - inference - - `__init__.py` - - `inference.py` - - streamer - - `__init__.py` - - `streamer.py` - - visualizers - - `__init__.py` - - `visualizer.py` - - `vis_utils.py` - - `LICENSE` - - `demo.py` - - `requirements.txt` - - `setup.py` - -## Prerequisites - -- [Python 3.10](https://www.python.org/downloads/) -- [Git](https://git-scm.com/) - -## Install requirements to run demo - -1. Install [prerequisites](#prerequisites). You may also need to [install pip](https://pip.pypa.io/en/stable/installation/). For example, on Ubuntu execute the following command to get pip installed: - - ```bash - sudo apt install python3-pip - ``` - -1. Create clean virtual environment: - - One of the possible ways for creating a virtual environment is to use `virtualenv`: - - ```bash - python -m pip install virtualenv - python -m virtualenv - ``` - - Before starting to work inside virtual environment, it should be activated: - - On Linux and macOS: - - ```bash - source /bin/activate - ``` - - On Windows: - - ```bash - .\\Scripts\activate - ``` - - Please make sure that the environment contains [wheel](https://pypi.org/project/wheel/) by calling the following command: - - ```bash - python -m pip install wheel - ``` - - > **NOTE**: On Linux and macOS, you may need to type `python3` instead of `python`. - -1. Install requirements in the environment: - - ```bash - cd python - python setup.py install - ``` - -## Usecase - -1. Running the `demo.py` application with the `-h` option yields the following usage message: - - ```bash - usage: demo.py [-h] -i INPUT -m MODEL [MODEL ...] [-it {sync,async}] [-l] [--no_show] [-d {CPU,GPU}] [--output OUTPUT] - - Options: - -h, --help Show this help message and exit. - -i INPUT, --input INPUT - Required. An input to process. The input must be a single image, a folder of images, video file or camera id. - -m MODEL [MODEL ...], --model MODELS [MODELS ...] - Optional. Path to directory with trained model and configuration file. Default value points to deployed model folder '../model'. - -it {sync,async}, --inference_type {sync,async} - Optional. Type of inference for single model. - -l, --loop Optional. Enable reading the input in a loop. - --no_show Optional. Disables showing inference results on UI. - -d {CPU,GPU}, --device {CPU,GPU} - Optional. Device to infer the model. - --output OUTPUT Optional. Output path to save input data with predictions. - ``` - -2. As a `model` parameter the default value `../model` will be used. Or you can specify the other path to the model directory from generated zip. You can pass as `input` a single image, a folder of images, a video file, or a web camera id. So you can use the following command to do inference with a pre-trained model: - - ```bash - python3 demo.py -i /inputVideo.mp4 - ``` - - You can press `Q` to stop inference during demo running. - - > **NOTE**: If you provide a single image as input, the demo processes and renders it quickly, then exits. To continuously - > visualize inference results on the screen, apply the `--loop` option, which enforces processing a single image in a loop. - > In this case, you can stop the demo by pressing `Q` button or killing the process in the terminal (`Ctrl+C` for Linux). - > - > **NOTE**: Default configuration contains info about pre- and post processing for inference and is guaranteed to be correct. - > Also you can change `config.json` that specifies the confidence threshold and color for each class visualization, but any - > changes should be made with caution. - -3. To save inferenced results with predictions on it, you can specify the folder path, using `--output`. - It works for images, videos, image folders and web cameras. To prevent issues, do not specify it together with a `--loop` parameter. - - ```bash - python3 demo.py \ - --input /inputImage.jpg \ - --models ../model \ - --output resulted_images - ``` - -4. To run a demo on a web camera, you need to know its ID. - You can check a list of camera devices by running this command line on Linux system: - - ```bash - sudo apt-get install v4l-utils - v4l2-ctl --list-devices - ``` - - The output will look like this: - - ```bash - Integrated Camera (usb-0000:00:1a.0-1.6): - /dev/video0 - ``` - - After that, you can use this `/dev/video0` as a camera ID for `--input`. - -## Troubleshooting - -1. If you have access to the Internet through the proxy server only, please use pip with proxy call as demonstrated by command below: - - ```bash - python -m pip install --proxy http://:@: - ``` - -1. If you use Anaconda environment, you should consider that OpenVINO has limited [Conda support](https://docs.openvino.ai/2021.4/openvino_docs_install_guides_installing_openvino_conda.html) for Python 3.6 and 3.7 versions only. But the demo package requires python 3.8. So please use other tools to create the environment (like `venv` or `virtualenv`) and use `pip` as a package manager. - -1. If you have problems when you try to use `pip install` command, please update pip version by following command: - - ```bash - python -m pip install --upgrade pip - ``` diff --git a/src/otx/core/exporter/exportable_code/demo/__init__.py b/src/otx/core/exporter/exportable_code/demo/__init__.py deleted file mode 100644 index e0056bd3187..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""OTX Exportable code.""" diff --git a/src/otx/core/exporter/exportable_code/demo/demo.py b/src/otx/core/exporter/exportable_code/demo/demo.py deleted file mode 100644 index 234b75eb709..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Demo based on ModelAPI.""" - -import sys -from argparse import SUPPRESS, ArgumentParser -from pathlib import Path - -from demo_package import AsyncExecutor, ModelWrapper, SyncExecutor, create_visualizer - - -def build_argparser() -> ArgumentParser: - """Returns an ArgumentParser for parsing command line arguments.""" - parser = ArgumentParser(add_help=False) - args = parser.add_argument_group("Options") - args.add_argument( - "-h", - "--help", - action="help", - default=SUPPRESS, - help="Show this help message and exit.", - ) - args.add_argument( - "-i", - "--input", - required=True, - help="Required. An input to process. The input must be a single image, " - "a folder of images, video file or camera id.", - ) - args.add_argument( - "-m", - "--model", - help="Optional. Path to directory with trained model and configuration file. " - "Default value points to deployed model folder '../model'.", - default=Path("../model"), - type=Path, - ) - args.add_argument( - "-it", - "--inference_type", - help="Optional. Type of inference for single model.", - choices=["sync", "async"], - default="async", - type=str, - ) - args.add_argument( - "-l", - "--loop", - help="Optional. Enable reading the input in a loop.", - default=False, - action="store_true", - ) - args.add_argument( - "--no_show", - help="Optional. Disables showing inference results on UI.", - default=False, - action="store_true", - ) - args.add_argument( - "-d", - "--device", - help="Optional. Device to infer the model.", - choices=["CPU", "GPU"], - default="CPU", - type=str, - ) - args.add_argument( - "--output", - default="./outputs/model_visualization", - type=str, - help="Optional. Output path to save input data with predictions.", - ) - - return parser - - -EXECUTORS = { - "sync": SyncExecutor, - "async": AsyncExecutor, -} - - -def main() -> int: - """Main function that is used to run demo.""" - args = build_argparser().parse_args() - - if args.loop and args.output: - msg = "--loop and --output cannot be both specified" - raise ValueError(msg) - - # create models - model = ModelWrapper(args.model, device=args.device) - inferencer = EXECUTORS[args.inference_type] - - # create visualizer - visualizer = create_visualizer(model.task_type, model.labels, no_show=args.no_show, output=args.output) - - # create inferencer and run - demo = inferencer(model, visualizer) - demo.run(args.input, args.loop and not args.no_show) - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/__init__.py b/src/otx/core/exporter/exportable_code/demo/demo_package/__init__.py deleted file mode 100644 index 3afe9c9f203..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Initialization of demo package.""" - -from .executors import AsyncExecutor, SyncExecutor -from .model_wrapper import ModelWrapper -from .utils import create_visualizer - -__all__ = [ - "SyncExecutor", - "AsyncExecutor", - "create_visualizer", - "ModelWrapper", -] diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/executors/__init__.py b/src/otx/core/exporter/exportable_code/demo/demo_package/executors/__init__.py deleted file mode 100644 index 075ea409b64..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/executors/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Initialization of executors.""" - -from .asynchronous import AsyncExecutor -from .synchronous import SyncExecutor - -__all__ = [ - "SyncExecutor", - "AsyncExecutor", -] diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/executors/asynchronous.py b/src/otx/core/exporter/exportable_code/demo/demo_package/executors/asynchronous.py deleted file mode 100644 index fc0cd328131..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/executors/asynchronous.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Async executor based on ModelAPI.""" - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING, Any - -from openvino.model_api.pipelines import AsyncPipeline - -if TYPE_CHECKING: - import numpy as np - from demo_package.model_wrapper import ModelWrapper - -from demo_package.streamer import get_streamer -from demo_package.visualizers import BaseVisualizer, dump_frames - - -class AsyncExecutor: - """Async inferencer. - - Args: - model: model for inference - visualizer: visualizer of inference results - """ - - def __init__(self, model: ModelWrapper, visualizer: BaseVisualizer) -> None: - self.model = model - self.visualizer = visualizer - self.async_pipeline = AsyncPipeline(self.model.core_model) - - def run(self, input_stream: int | str, loop: bool = False) -> None: - """Async inference for input stream (image, video stream, camera).""" - streamer = get_streamer(input_stream, loop) - next_frame_id = 0 - next_frame_id_to_show = 0 - stop_visualization = False - saved_frames = [] - - for frame in streamer: - results = self.async_pipeline.get_result(next_frame_id_to_show) - while results: - start_time = time.perf_counter() - output = self.render_result(results) - next_frame_id_to_show += 1 - self.visualizer.show(output) - if self.visualizer.output: - saved_frames.append(output) - stop_visualization = self.visualizer.is_quit() - # visualize video not faster than the original FPS - self.visualizer.video_delay(time.perf_counter() - start_time, streamer) - results = self.async_pipeline.get_result(next_frame_id_to_show) - if stop_visualization: - break - self.async_pipeline.submit_data(frame, next_frame_id, {"frame": frame}) - next_frame_id += 1 - self.async_pipeline.await_all() - for next_id in range(next_frame_id_to_show, next_frame_id): - start_time = time.perf_counter() - results = self.async_pipeline.get_result(next_id) - if not results: - msg = "Async pipeline returned None results" - raise RuntimeError(msg) - output = self.render_result(results) - self.visualizer.show(output) - if self.visualizer.output: - saved_frames.append(output) - # visualize video not faster than the original FPS - self.visualizer.video_delay(time.perf_counter() - start_time, streamer) - dump_frames(saved_frames, self.visualizer.output, input_stream, streamer) - - def render_result(self, results: tuple[Any, dict]) -> np.ndarray: - """Render for results of inference.""" - predictions, frame_meta = results - current_frame = frame_meta["frame"] - return self.visualizer.draw(current_frame, predictions) diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/executors/synchronous.py b/src/otx/core/exporter/exportable_code/demo/demo_package/executors/synchronous.py deleted file mode 100644 index ea280841aad..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/executors/synchronous.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Synchronous Executor based on ModelAPI.""" - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from demo_package.model_wrapper import ModelWrapper - from demo_package.visualizers import BaseVisualizer - -from demo_package.streamer import get_streamer -from demo_package.visualizers import dump_frames - - -class SyncExecutor: - """Synchronous executor for model inference. - - Args: - model (ModelContainer): model for inference - visualizer (Visualizer): visualizer of inference results. Defaults to None. - """ - - def __init__(self, model: ModelWrapper, visualizer: BaseVisualizer) -> None: - self.model = model - self.visualizer = visualizer - - def run(self, input_stream: int | str, loop: bool = False) -> None: - """Run demo using input stream (image, video stream, camera).""" - streamer = get_streamer(input_stream, loop) - saved_frames = [] - - for frame in streamer: - # getting result include preprocessing, infer, postprocessing for sync infer - start_time = time.perf_counter() - predictions, _ = self.model(frame) - output = self.visualizer.draw(frame, predictions) - self.visualizer.show(output) - if output is not None: - saved_frames.append(output) - if self.visualizer.is_quit(): - break - # visualize video not faster than the original FPS - self.visualizer.video_delay(time.perf_counter() - start_time, streamer) - - dump_frames(saved_frames, self.visualizer.output, input_stream, streamer) diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/model_wrapper.py b/src/otx/core/exporter/exportable_code/demo/demo_package/model_wrapper.py deleted file mode 100644 index 76f1394034c..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/model_wrapper.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""ModelContainer class used for loading the model in the model wrapper.""" - -from __future__ import annotations - -from enum import Enum -from typing import TYPE_CHECKING, Any, NamedTuple - -from openvino.model_api.adapters import OpenvinoAdapter, create_core -from openvino.model_api.models import Model - -from .utils import get_model_path, get_parameters - -if TYPE_CHECKING: - from pathlib import Path - - import numpy as np - from openvino.model_api.tilers import DetectionTiler, InstanceSegmentationTiler - - -class TaskType(str, Enum): - """OTX task type definition.""" - - CLASSIFICATION = "CLASSIFICATION" - DETECTION = "DETECTION" - INSTANCE_SEGMENTATION = "INSTANCE_SEGMENTATION" - SEGMENTATION = "SEGMENTATION" - - -class ModelWrapper: - """Class for storing the model wrapper based on Model API and needed parameters of model. - - Args: - model_dir (Path): path to model directory - """ - - def __init__(self, model_dir: Path, device: str = "CPU") -> None: - model_adapter = OpenvinoAdapter(create_core(), get_model_path(model_dir / "model.xml"), device=device) - if not (model_dir / "config.json").exists(): - msg = "config.json doesn't exist in the model directory." - raise RuntimeError(msg) - self.parameters = get_parameters(model_dir / "config.json") - self._labels = self.parameters["model_parameters"]["labels"] - self._task_type = TaskType[self.parameters["converter_type"].upper()] - - # labels for modelAPI wrappers can be empty, because unused in pre- and postprocessing - self.model_parameters = self.parameters["model_parameters"] - - # model already contains correct labels - self.model_parameters.pop("labels") - - self.core_model = Model.create_model( - model_adapter, - self.parameters["type_of_model"], - self.model_parameters, - preload=True, - ) - self.tiler = self.setup_tiler(model_dir, device) - - def setup_tiler( - self, - model_dir: Path, - device: str, - ) -> DetectionTiler | InstanceSegmentationTiler | None: - """Set up tiler for model. - - Args: - model_dir (str): model directory - device (str): device to run model on - Returns: - Optional: type of tiler or None - """ - if not self.parameters.get("tiling_parameters") or not self.parameters["tiling_parameters"]["enable_tiling"]: - return None - - msg = "Tiling has not been implemented yet" - raise NotImplementedError(msg) - - @property - def task_type(self) -> TaskType: - """Task type property.""" - return self._task_type - - @property - def labels(self) -> dict: - """Labels property.""" - return self._labels - - def infer(self, frame: np.ndarray) -> tuple[NamedTuple, dict]: - """Infer with original image. - - Args: - frame: np.ndarray, input image - Returns: - predictions: NamedTuple, prediction - frame_meta: Dict, dict with original shape - """ - # getting result include preprocessing, infer, postprocessing for sync infer - predictions = self.core_model(frame) - frame_meta = {"original_shape": frame.shape} - - return predictions, frame_meta - - def infer_tile(self, frame: np.ndarray) -> tuple[NamedTuple, dict]: - """Infer by patching full image to tiles. - - Args: - frame: np.ndarray - input image - Returns: - Tuple[NamedTuple, Dict]: prediction and original shape - """ - if self.tiler is None: - msg = "Tiler is not set" - raise RuntimeError(msg) - detections = self.tiler(frame) - return detections, {"original_shape": frame.shape} - - def __call__(self, input_data: np.ndarray) -> tuple[Any, dict]: - """Call the ModelWrapper class. - - Args: - input_data (np.ndarray): The input image. - - Returns: - Tuple[Any, dict]: A tuple containing predictions and the meta information. - """ - if self.tiler is not None: - return self.infer_tile(input_data) - return self.infer(input_data) diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/streamer/__init__.py b/src/otx/core/exporter/exportable_code/demo/demo_package/streamer/__init__.py deleted file mode 100644 index 235a81c4671..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/streamer/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Initialization of streamer.""" - -from .streamer import ( - BaseStreamer, - CameraStreamer, - DirStreamer, - ImageStreamer, - ThreadedStreamer, - VideoStreamer, - get_streamer, -) - -__all__ = [ - "CameraStreamer", - "DirStreamer", - "ImageStreamer", - "ThreadedStreamer", - "VideoStreamer", - "BaseStreamer", - "get_streamer", -] diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/utils.py b/src/otx/core/exporter/exportable_code/demo/demo_package/utils.py deleted file mode 100644 index 4cbcb0a6861..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/utils.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utils for demo.""" - -from __future__ import annotations - -import json -from pathlib import Path - -from .visualizers import ( - BaseVisualizer, - ClassificationVisualizer, - InstanceSegmentationVisualizer, - ObjectDetectionVisualizer, - SemanticSegmentationVisualizer, -) - - -def get_model_path(path: Path | None) -> Path: - """Get path to model.""" - model_path = path - if model_path is None: - model_path = Path(__file__).parent / "openvino.xml" - if not model_path.exists(): - msg = "The path to the model was not found." - raise OSError(msg) - - return model_path - - -def get_parameters(path: Path | None) -> dict: - """Get hyper parameters to creating model.""" - parameters_path = path - if parameters_path is None: - parameters_path = Path(__file__).parent / "config.json" - if not parameters_path.exists(): - msg = "The path to the config was not found." - raise OSError(msg) - - with Path.open(parameters_path, encoding="utf8") as file: - return json.load(file) - - -def create_visualizer( - task_type: str, - labels: list, - no_show: bool = False, - output: str = "./outputs", -) -> BaseVisualizer | None: - """Create visualizer according to kind of task.""" - if task_type == "CLASSIFICATION": - return ClassificationVisualizer(window_name="Result", no_show=no_show, output=output) - if task_type == "SEGMENTATION": - return SemanticSegmentationVisualizer(window_name="Result", labels=labels, no_show=no_show, output=output) - if task_type == "INSTANCE_SEGMENTATION": - return InstanceSegmentationVisualizer(window_name="Result", labels=labels, no_show=no_show, output=output) - if task_type == "DETECTION": - return ObjectDetectionVisualizer(window_name="Result", labels=labels, no_show=no_show, output=output) - msg = "Visualizer for f{task_type} is not implemented" - raise NotImplementedError(msg) diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/__init__.py b/src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/__init__.py deleted file mode 100644 index 8d74b8b20d9..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Initialization of visualizers.""" - -from .vis_utils import dump_frames -from .visualizer import ( - BaseVisualizer, - ClassificationVisualizer, - InstanceSegmentationVisualizer, - ObjectDetectionVisualizer, - SemanticSegmentationVisualizer, -) - -__all__ = [ - "BaseVisualizer", - "dump_frames", - "ClassificationVisualizer", - "SemanticSegmentationVisualizer", - "InstanceSegmentationVisualizer", - "ObjectDetectionVisualizer", -] diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/vis_utils.py b/src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/vis_utils.py deleted file mode 100644 index 0edea1cbb94..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/vis_utils.py +++ /dev/null @@ -1,190 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""This module implements activation map.""" - -from __future__ import annotations - -import colorsys -import random -from pathlib import Path - -import cv2 -import numpy as np - - -def get_actmap( - saliency_map: np.ndarray, - output_res: tuple | list, -) -> np.ndarray: - """Get activation map (heatmap) from saliency map. - - It will return activation map from saliency map - - Args: - saliency_map (np.ndarray): Saliency map with pixel values from 0-255 - output_res (Union[tuple, list]): Output resolution - - Returns: - saliency_map (np.ndarray): [H, W, 3] colormap, more red means more salient - - """ - if len(saliency_map.shape) == 3: - saliency_map = saliency_map[0] - - saliency_map = cv2.resize(saliency_map, output_res) - return cv2.applyColorMap(saliency_map, cv2.COLORMAP_JET) - - -def get_input_names_list(input_path: str | int, capture: cv2.VideoCapture) -> list[str]: - """Lists the filenames of all inputs for demo.""" - # Web camera input - if isinstance(input_path, int): - return [] - if "DIR" in str(capture.get_type()): - return [f.name for f in Path(input_path).iterdir() if f.is_file()] - return [Path(input_path).name] - - -def dump_frames(saved_frames: list, output: str, input_path: str | int, capture: cv2.VideoCapture) -> None: - """Saves images/videos with predictions from saved_frames to output folder with proper names.""" - # If no frames are saved, return - if not saved_frames: - return - - # Create the output folder if it doesn't exist - output_path = Path(output) - if not output_path.exists(): - output_path.mkdir(parents=True) - - # Get the list of input names - filenames = get_input_names_list(input_path, capture) - - # If the input is a video, save it as video - if "VIDEO" in str(capture.get_type()): - filename = filenames[0] - w, h, _ = saved_frames[0].shape - video_path = str(output_path / filename) - codec = cv2.VideoWriter_fourcc(*"mp4v") - out = cv2.VideoWriter(video_path, codec, capture.fps(), (h, w)) - for frame in saved_frames: - out.write(frame) - out.release() - print(f"Video was saved to {video_path}") - # If the input is not a video, save each frame as an image - else: - if len(filenames) != len(saved_frames): - filenames = [f"output_{i}.jpeg" for i, _ in enumerate(saved_frames)] - for filename, frame in zip(filenames, saved_frames): - image_path = str(output_path / filename) - cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) - cv2.imwrite(image_path, frame) - print(f"Image was saved to {image_path}") - - -class ColorPalette: - """Represents a palette of colors.""" - - def __init__(self, num_classes: int, rng: random.Random | None = None) -> None: - """Initialize the ColorPalette. - - Args: - - num_classes (int): The number of classes. - - rng (Optional[random.Random]): The random number generator. - - Returns: - None - """ - if num_classes == 0: - msg = "ColorPalette accepts only the positive number of colors" - raise ValueError(msg) - if rng is None: - rng = random.Random(0xACE) # nosec B311 # disable random check - - candidates_num = 100 - hsv_colors = [(1.0, 1.0, 1.0)] - for _ in range(1, num_classes): - colors_candidates = [ - (rng.random(), rng.uniform(0.8, 1.0), rng.uniform(0.5, 1.0)) for _ in range(candidates_num) - ] - min_distances = [self._min_distance(hsv_colors, c) for c in colors_candidates] - arg_max = np.argmax(min_distances) - hsv_colors.append(colors_candidates[arg_max]) - - self.palette = [self.hsv2rgb(*hsv) for hsv in hsv_colors] - - @staticmethod - def _dist(c1: tuple[float, float, float], c2: tuple[float, float, float]) -> float: - """Calculate the distance between two colors in 3D space. - - Args: - - c1 (Tuple[float, float, float]): Tuple representing the first RGB color. - - c2 (Tuple[float, float, float]): Tuple representing the second RGB color. - - Returns: - float: The distance between the two colors. - """ - dh = min(abs(c1[0] - c2[0]), 1 - abs(c1[0] - c2[0])) * 2 - ds = abs(c1[1] - c2[1]) - dv = abs(c1[2] - c2[2]) - return dh * dh + ds * ds + dv * dv - - @classmethod - def _min_distance( - cls, - colors_set: list[tuple[float, float, float]], - color_candidate: tuple[float, float, float], - ) -> float: - """Calculate the minimum distance between color_candidate and colors_set. - - Args: - - colors_set: List of tuples representing RGB colors. - - color_candidate: Tuple representing an RGB color. - - Returns: - - float: The minimum distance between color_candidate and colors_set. - """ - distances = [cls._dist(o, color_candidate) for o in colors_set] - return min(distances) - - def to_numpy_array(self) -> np.ndarray: - """Convert the palette to a NumPy array. - - Returns: - np.ndarray: The palette as a NumPy array. - """ - return np.array(self.palette) - - @staticmethod - def hsv2rgb(h: float, s: float, v: float) -> tuple[int, int, int]: - """Convert HSV color to RGB color. - - Args: - - h (float): Hue. - - s (float): Saturation. - - v (float): Value. - - Returns: - Tuple[int, int, int]: RGB color. - """ - r, g, b = colorsys.hsv_to_rgb(h, s, v) - return int(r * 255), int(g * 255), int(b * 255) - - def __getitem__(self, n: int) -> tuple[int, int, int]: - """Get the color at index n. - - Args: - - n (int): Index. - - Returns: - Tuple[int, int, int]: RGB color. - """ - return self.palette[n % len(self.palette)] - - def __len__(self) -> int: - """Returns the number of colors in the palette. - - Returns: - int: The number of colors in the palette. - """ - return len(self.palette) diff --git a/src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/visualizer.py b/src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/visualizer.py deleted file mode 100644 index 764eb23cf1d..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/demo_package/visualizers/visualizer.py +++ /dev/null @@ -1,398 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Visualizer for results of prediction.""" - -from __future__ import annotations - -import logging as log -import time -from typing import TYPE_CHECKING, NamedTuple - -import cv2 -import numpy as np -from openvino.model_api.performance_metrics import put_highlighted_text - -from .vis_utils import ColorPalette - -if TYPE_CHECKING: - from demo_package.streamer import BaseStreamer - from openvino.model_api.models.utils import ( - ClassificationResult, - DetectionResult, - InstanceSegmentationResult, - SegmentedObject, - ) - - -class BaseVisualizer: - """Base class for visualizators.""" - - def __init__( - self, - window_name: str | None = None, - no_show: bool = False, - delay: int | None = None, - output: str = "./outputs", - ) -> None: - """Base class for visualizators. - - Args: - window_name (str]): The name of the window. Defaults to None. - no_show (bool): Flag to indicate whether to show the window. Defaults to False. - delay (int]): The delay in seconds. Defaults to None. - output (str]): The output directory. Defaults to "./outputs". - - Returns: - None - """ - self.window_name = "Window" if window_name is None else window_name - - self.delay = delay - self.no_show = no_show - if delay is None: - self.delay = 1 - self.output = output - - def draw( - self, - frame: np.ndarray, - predictions: NamedTuple, - ) -> np.ndarray: - """Draw annotations on the image. - - Args: - frame: Input image - predictions: Annotations to be drawn on the input image - - Returns: - Output image with annotations. - """ - raise NotImplementedError - - def show(self, image: np.ndarray) -> None: - """Show result image. - - Args: - image (np.ndarray): Image to be shown. - """ - if not self.no_show: - cv2.imshow(self.window_name, image) - - def is_quit(self) -> bool: - """Check user wish to quit.""" - if self.no_show: - return False - - return ord("q") == cv2.waitKey(self.delay) - - def video_delay(self, elapsed_time: float, streamer: BaseStreamer) -> None: - """Check if video frames were inferenced faster than the original video FPS and delay visualizer if so. - - Args: - elapsed_time (float): Time spent on frame inference - streamer (BaseStreamer): Streamer object - """ - if self.no_show: - return - if "VIDEO" in str(streamer.get_type()): - fps_num = streamer.fps() - orig_frame_time = 1 / fps_num - if elapsed_time < orig_frame_time: - time.sleep(orig_frame_time - elapsed_time) - - -class ClassificationVisualizer(BaseVisualizer): - """Visualize the predicted classification labels by drawing the annotations on the input image. - - Example: - >>> predictions = inference_model.predict(frame) - >>> output = visualizer.draw(frame, predictions) - >>> visualizer.show(output) - """ - - def draw( - self, - frame: np.ndarray, - predictions: ClassificationResult, - ) -> np.ndarray: - """Draw classification annotations on the image. - - Args: - image: Input image - annotation: Annotations to be drawn on the input image - - Returns: - Output image with annotations. - """ - predictions = predictions.top_labels - class_label = predictions[0][1] - font_scale = 0.7 - label_height = cv2.getTextSize(class_label, cv2.FONT_HERSHEY_COMPLEX, font_scale, 2)[0][1] - initial_labels_pos = frame.shape[0] - label_height * (int(1.5 * len(predictions)) + 1) - - if initial_labels_pos < 0: - initial_labels_pos = label_height - log.warning("Too much labels to display on this frame, some will be omitted") - offset_y = initial_labels_pos - - header = "Label: Score:" - label_width = cv2.getTextSize(header, cv2.FONT_HERSHEY_COMPLEX, font_scale, 2)[0][0] - put_highlighted_text( - frame, - header, - (frame.shape[1] - label_width, offset_y), - cv2.FONT_HERSHEY_COMPLEX, - font_scale, - (255, 0, 0), - 2, - ) - - for idx, class_label, score in predictions: - label = f"{idx}. {class_label} {score:.2f}" - label_width = cv2.getTextSize(label, cv2.FONT_HERSHEY_COMPLEX, font_scale, 2)[0][0] - offset_y += int(label_height * 1.5) - put_highlighted_text( - frame, - label, - (frame.shape[1] - label_width, offset_y), - cv2.FONT_HERSHEY_COMPLEX, - font_scale, - (255, 0, 0), - 2, - ) - return frame - - -class SemanticSegmentationVisualizer(BaseVisualizer): - """Visualize the predicted segmentation labels by drawing the annotations on the input image. - - Example: - >>> masks = inference_model.predict(frame) - >>> output = visualizer.draw(frame, masks) - >>> visualizer.show(output) - """ - - def __init__( - self, - labels: list[str], - window_name: str | None = None, - no_show: bool = False, - delay: int | None = None, - output: str = "./outputs", - ) -> None: - """Semantic segmentation visualizer. - - Draws the segmentation masks on the input image. - - Parameters: - labels (List[str]): List of labels. - window_name (str | None): Name of the window (default is None). - no_show (bool): Flag indicating whether to show the window (default is False). - delay (int | None): Delay in milliseconds (default is None). - output (str): Output path (default is "./outputs"). - - Returns: - None - """ - super().__init__(window_name, no_show, delay, output) - self.color_palette = ColorPalette(len(labels)).to_numpy_array() - self.color_map = self._create_color_map() - - def _create_color_map(self) -> np.ndarray: - classes = self.color_palette[:, ::-1] # RGB to BGR - color_map = np.zeros((256, 1, 3), dtype=np.uint8) - classes_num = len(classes) - color_map[:classes_num, 0, :] = classes - color_map[classes_num:, 0, :] = np.random.uniform(0, 255, size=(256 - classes_num, 3)) # noqa: NPY002 - return color_map - - def _apply_color_map(self, input_2d_mask: np.ndarray) -> np.ndarray: - input_3d = cv2.merge([input_2d_mask, input_2d_mask, input_2d_mask]) - return cv2.LUT(input_3d.astype(np.uint8), self.color_map) - - def draw(self, frame: np.ndarray, masks: SegmentedObject) -> np.ndarray: - """Draw segmentation annotations on the image. - - Args: - frame: Input image - masks: Mask annotations to be drawn on the input image - - Returns: - Output image with annotations. - """ - masks = masks.resultImage - output = self._apply_color_map(masks) - return cv2.addWeighted(frame, 0.5, output, 0.5, 0) - - -class ObjectDetectionVisualizer(BaseVisualizer): - """Visualizes object detection annotations on an input image.""" - - def __init__( - self, - labels: list[str], - window_name: str | None = None, - no_show: bool = False, - delay: int | None = None, - output: str = "./outputs", - ) -> None: - """Object detection visualizer. - - Draws the object detection annotations on the input image. - - Parameters: - labels (List[str]): The list of labels. - window_name (str | None): The name of the window. Defaults to None. - no_show (bool): Flag to control whether to show the window. Defaults to False. - delay (int | None): The delay in milliseconds. Defaults to None. - output (str): The output directory. Defaults to "./outputs". - - Returns: - None - """ - super().__init__(window_name, no_show, delay, output) - self.labels = labels - self.color_palette = ColorPalette(len(labels)) - - def draw( - self, - frame: np.ndarray, - predictions: DetectionResult, - ) -> np.ndarray: - """Draw instance segmentation annotations on the image. - - Args: - image: Input image - annotation: Annotations to be drawn on the input image - - Returns: - Output image with annotations. - """ - for detection in predictions.objects: - class_id = int(detection.id) - color = self.color_palette[class_id] - det_label = self.color_palette[class_id] if self.labels and len(self.labels) >= class_id else f"#{class_id}" - xmin, ymin, xmax, ymax = detection.xmin, detection.ymin, detection.xmax, detection.ymax - cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), color, 2) - cv2.putText( - frame, - f"{det_label} {detection.score:.1%}", - (xmin, ymin - 7), - cv2.FONT_HERSHEY_COMPLEX, - 0.6, - color, - 1, - ) - - return frame - - -class InstanceSegmentationVisualizer(BaseVisualizer): - """Visualizes Instance Segmentation annotations on an input image.""" - - def __init__( - self, - labels: list[str], - window_name: str | None = None, - no_show: bool = False, - delay: int | None = None, - output: str = "./outputs", - ) -> None: - """Instance segmentation visualizer. - - Draws the instance segmentation annotations on the input image. - - Args: - labels (List[str]): The list of labels. - window_name (str]): The name of the window. Defaults to None. - no_show (bool): A flag to indicate whether to show the window. Defaults to False. - delay (int]): The delay in milliseconds. Defaults to None. - output (str]): The path to the output directory. Defaults to "./outputs". - - Returns: - None - """ - super().__init__(window_name, no_show, delay, output) - self.labels = labels - colors_num = len(labels) if labels else 80 - self.show_boxes = False - self.show_scores = True - self.palette = ColorPalette(colors_num) - - def draw( - self, - frame: np.ndarray, - predictions: InstanceSegmentationResult, - ) -> np.ndarray: - """Draw the instance segmentation results on the input frame. - - Args: - frame: np.ndarray - The input frame on which to draw the instance segmentation results. - predictions: InstanceSegmentationResult - The instance segmentation results to be drawn. - - Returns: - np.ndarray - The input frame with the instance segmentation results drawn on it. - """ - result = frame.copy() - output_objects = predictions.segmentedObjects - bboxes = [[output.xmin, output.ymin, output.xmax, output.ymax] for output in output_objects] - scores = [output.score for output in output_objects] - masks = [output.mask for output in output_objects] - label_names = [output.str_label for output in output_objects] - - result = self._overlay_masks(result, masks) - return self._overlay_labels(result, bboxes, label_names, scores) - - def _overlay_masks(self, image: np.ndarray, masks: list[np.ndarray]) -> np.ndarray: - segments_image = image.copy() - aggregated_mask = np.zeros(image.shape[:2], dtype=np.uint8) - aggregated_colored_mask = np.zeros(image.shape, dtype=np.uint8) - all_contours = [] - - for i, mask in enumerate(masks): - contours = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[-2] - if contours: - all_contours.append(contours[0]) - - mask_color = self.palette[i] - cv2.bitwise_or(aggregated_mask, mask, dst=aggregated_mask) - cv2.bitwise_or(aggregated_colored_mask, mask_color, dst=aggregated_colored_mask, mask=mask) - - # Fill the area occupied by all instances with a colored instances mask image - cv2.bitwise_and(segments_image, (0, 0, 0), dst=segments_image, mask=aggregated_mask) - cv2.bitwise_or(segments_image, aggregated_colored_mask, dst=segments_image, mask=aggregated_mask) - - cv2.addWeighted(image, 0.5, segments_image, 0.5, 0, dst=image) - cv2.drawContours(image, all_contours, -1, (0, 0, 0)) - return image - - def _overlay_boxes(self, image: np.ndarray, boxes: list[np.ndarray], classes: list[int]) -> np.ndarray: - for box, class_id in zip(boxes, classes): - color = self.palette[class_id] - top_left, bottom_right = box[:2], box[2:] - image = cv2.rectangle(image, top_left, bottom_right, color, 2) - return image - - def _overlay_labels( - self, - image: np.ndarray, - boxes: list[np.ndarray], - classes: list[str], - scores: list[float], - ) -> np.ndarray: - template = "{}: {:.2f}" if self.show_scores else "{}" - - for box, score, label in zip(boxes, scores, classes): - text = template.format(label, score) - textsize = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)[0] - cv2.putText( - image, - text, - (box[0], box[1] + int(textsize[0] / 3)), - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - (255, 255, 255), - 1, - ) - return image diff --git a/src/otx/core/exporter/exportable_code/demo/requirements.txt b/src/otx/core/exporter/exportable_code/demo/requirements.txt deleted file mode 100644 index 9287e3be9eb..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -openvino==2023.3.0 -openvino-model-api==0.1.8 -numpy==1.26.1 diff --git a/src/otx/core/exporter/exportable_code/demo/setup.py b/src/otx/core/exporter/exportable_code/demo/setup.py deleted file mode 100644 index be23c6a1040..00000000000 --- a/src/otx/core/exporter/exportable_code/demo/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""setup file for demo package.""" - -from pathlib import Path - -from setuptools import find_packages, setup - -SETUP_DIR = Path(__file__).resolve().parent - -with Path.open(SETUP_DIR / "requirements.txt", encoding="utf8") as f: - required = f.read().splitlines() - -packages = find_packages(str(SETUP_DIR)) -package_dir = {packages[0]: str(SETUP_DIR / packages[0])} - -setup( - name=packages[0], - version="0.0", - author="Intel® Corporation", - license="Copyright (c) 2024 Intel Corporation. SPDX-License-Identifier: Apache-2.0", - description="Demo based on ModelAPI classes", - packages=packages, - package_dir=package_dir, - package_data={ - packages[0]: ["*.json"], - }, - install_requires=required, -) diff --git a/src/otx/core/exporter/mmdeploy.py b/src/otx/core/exporter/mmdeploy.py deleted file mode 100644 index 709ec356764..00000000000 --- a/src/otx/core/exporter/mmdeploy.py +++ /dev/null @@ -1,265 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for mmdeploy exporter used in OTX.""" - -from __future__ import annotations - -import importlib -import logging as log -from contextlib import contextmanager -from copy import copy -from pathlib import Path -from typing import TYPE_CHECKING, Callable, Iterator, Literal - -import numpy as np -import onnx -import openvino -import torch -from mmdeploy.apis import build_task_processor, torch2onnx -from mmdeploy.utils import get_partition_config -from mmengine.config import Config as MMConfig -from mmengine.registry.default_scope import DefaultScope - -from otx.core.exporter.base import OTXModelExporter -from otx.core.types.precision import OTXPrecisionType -from otx.core.utils.config import convert_conf_to_mmconfig_dict, to_tuple - -if TYPE_CHECKING: - from omegaconf import DictConfig - - -class MMdeployExporter(OTXModelExporter): - """Exporter that uses mmdeploy and OpenVINO conversion tools. - - Args: - model_builder (Callable): A function to build a model. - model_cfg (DictConfig): Model config for mm framework. - deploy_cfg (str | MMConfig): Deployment config module path or MMEngine Config object. - test_pipeline (list[dict]): A pipeline for test dataset. - input_size (tuple[int, ...]): Input shape. - mean (tuple[float, float, float], optional): Mean values of 3 channels. Defaults to (0.0, 0.0, 0.0). - std (tuple[float, float, float], optional): Std values of 3 channels. Defaults to (1.0, 1.0, 1.0). - resize_mode (Literal["crop", "standard", "fit_to_window", "fit_to_window_letterbox"], optional): - A resize type for model preprocess. "standard" resizes images without keeping ratio. - "fit_to_window" resizes images while keeping ratio. - "fit_to_window_letterbox" resizes images and pads images to fit the size. Defaults to "standard". - pad_value (int, optional): Padding value. Defaults to 0. - swap_rgb (bool, optional): Whether to convert the image from BGR to RGB Defaults to False. - max_num_detections (int, optional): Maximum number of detections per image. Defaults to 0. - output_names (list[str], optional): Additional names for the output nodes in addition to ones in "ir_config" . - """ - - def __init__( - self, - model_builder: Callable, - model_cfg: DictConfig, - deploy_cfg: str | MMConfig, - test_pipeline: list[dict], - input_size: tuple[int, ...], - mean: tuple[float, float, float] = (0.0, 0.0, 0.0), - std: tuple[float, float, float] = (1.0, 1.0, 1.0), - resize_mode: Literal["crop", "standard", "fit_to_window", "fit_to_window_letterbox"] = "standard", - pad_value: int = 0, - swap_rgb: bool = False, - metadata: dict[tuple[str, str], str] | None = None, - max_num_detections: int = 0, - output_names: list[str] | None = None, - ) -> None: - super().__init__(input_size, mean, std, resize_mode, pad_value, swap_rgb, metadata, output_names) - self._model_builder = model_builder - model_cfg = convert_conf_to_mmconfig_dict(model_cfg, "list") - self._model_cfg = MMConfig({"model": model_cfg, "test_pipeline": list(map(to_tuple, test_pipeline))}) - self._deploy_cfg = deploy_cfg if isinstance(deploy_cfg, MMConfig) else load_mmconfig_from_pkg(deploy_cfg) - - patch_input_shape(self._deploy_cfg, input_size[3], input_size[2]) - if output_names is not None: - self._deploy_cfg.ir_config.output_names.extend(output_names) - self.output_names = self._deploy_cfg.ir_config.output_names - - if max_num_detections > 0: - self._set_max_num_detections(max_num_detections) - - def _set_max_num_detections(self, max_num_detections: int) -> None: - log.info(f"Export max_num_detections: {max_num_detections}") - post_proc_cfg = self._deploy_cfg["codebase_config"]["post_processing"] - post_proc_cfg["max_output_boxes_per_class"] = max_num_detections - post_proc_cfg["keep_top_k"] = max_num_detections - post_proc_cfg["pre_top_k"] = max_num_detections * 10 - - def to_openvino( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> Path: - """Export to OpenVINO Intermediate Representation format. - - Args: - model (torch.nn.Module): pytorch model top export - output_dir (Path): path to the directory to store export artifacts - base_model_name (str, optional): exported model name - precision (OTXPrecisionType, optional): precision of the exported model's weights - metadata (dict[tuple[str, str], str] | None, optional): metadata to embed to the exported model. - - Returns: - Path: path to the exported model. - """ - onnx_path = self._cvt2onnx(model, output_dir, base_model_name) - exported_model = openvino.convert_model( - str(onnx_path), - input=(openvino.runtime.PartialShape(self.input_size),), - ) - exported_model = self._postprocess_openvino_model(exported_model) - - save_path = output_dir / (base_model_name + ".xml") - openvino.save_model(exported_model, save_path, compress_to_fp16=(precision == OTXPrecisionType.FP16)) - onnx_path.unlink() - log.info("Converting to OpenVINO is done.") - - return Path(save_path) - - def to_onnx( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - embed_metadata: bool = True, - ) -> Path: - """Export to ONNX format. - - Args: - model (torch.nn.Module): pytorch model top export - output_dir (Path): path to the directory to store export artifacts - base_model_name (str, optional): exported model name - precision (OTXPrecisionType, optional): precision of the exported model's weights - metadata (dict[tuple[str, str],str] | None, optional): metadata to embed to the exported model. - - Returns: - Path: path to the exported model. - """ - deploy_cfg = self._prepare_onnx_cfg() - save_path = self._cvt2onnx(model, output_dir, base_model_name, deploy_cfg) - - onnx_model = onnx.load(str(save_path)) - onnx_model = self._postprocess_onnx_model(onnx_model, embed_metadata, precision) - - onnx.save(onnx_model, str(save_path)) - log.info("Converting to ONNX is done.") - - return save_path - - def _prepare_onnx_cfg(self) -> MMConfig: - cfg = copy(self._deploy_cfg) - cfg["backend_config"] = {"type": "onnxruntime"} - return cfg - - def _cvt2onnx( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str, - deploy_cfg: MMConfig | None = None, - ) -> Path: - onnx_file_name = base_model_name + ".onnx" - model_weight_file = output_dir / "mmdeploy_fmt_model.pth" - torch.save(model.state_dict(), model_weight_file) - - log.debug(f"mmdeploy torch2onnx: \n\tmodel_cfg: {self._model_cfg}\n\tdeploy_cfg: {self._deploy_cfg}") - with use_temporary_default_scope(): - self._register_model_builder() - torch2onnx( - np.zeros((128, 128, 3), dtype=np.uint8), - str(output_dir), - onnx_file_name, - deploy_cfg=self._deploy_cfg if deploy_cfg is None else deploy_cfg, - model_cfg=self._model_cfg, - model_checkpoint=str(model_weight_file), - device="cpu", - ) - - partition_cfgs = get_partition_config(self._deploy_cfg) - if partition_cfgs is not None: - self.cvt_torch2onnx_partition(self._deploy_cfg, partition_cfgs) - - model_weight_file.unlink() - - return output_dir / onnx_file_name - - def _register_model_builder(self) -> None: - task_processor = build_task_processor(self._model_cfg, self._deploy_cfg, "cpu") - - def helper(*args, **kwargs) -> torch.nn.Module: - return mmdeploy_init_model_helper(*args, **kwargs, model_builder=self._model_builder) - - task_processor.__class__.init_pytorch_model = helper - - def cvt_torch2onnx_partition(self, deploy_cfg: MMConfig, partition_cfgs: MMConfig) -> None: - """Partition onnx conversion.""" - raise NotImplementedError # NOTE need for exporting tiling model. - - -def mmdeploy_init_model_helper(*_, **kwargs) -> torch.nn.Module: - """Helper function for initializing a model for inference using the 'mmdeploy' library.""" - model_builder = kwargs.pop("model_builder") - model = model_builder() - - # NOTE: Need to investigate it why - # NNCF compressed model lost trace context from time to time with no reason - # even with 'torch.no_grad()'. Explicitly setting 'requires_grad' to'False' - # makes things easier. - for i in model.parameters(): - i.requires_grad = False - - return model - - -def patch_input_shape(deploy_cfg: MMConfig, width: int, height: int) -> None: - """Update backend configuration with input shape information. - - Args: - deploy_cfg (MMConfig): Config object containing test pipeline and other configurations. - width (int): Width of image. - height (int): Height of image. - """ - deploy_cfg.ir_config.input_shape = (width, height) - - -def patch_ir_scale_factor(deploy_cfg: MMConfig, hyper_parameters) -> None: # noqa: ANN001, ARG001 - """Patch IR scale factor inplace from hyper parameters to deploy config. - - Args: - deploy_cfg (ConfigDict): mmcv deploy config. - hyper_parameters (DetectionConfig): OTX detection hyper parameters> - """ - raise NotImplementedError # NOTE need to implement for tiling - - -def load_mmconfig_from_pkg(cfg: str) -> MMConfig: - """Load configuration from package path as MMEngine Config format. - - Args: - cfg (str): Package path of configuraiton. - - Returns: - MMConfig: MMEngine Config. - """ - config_module = importlib.import_module(cfg) - return MMConfig.fromfile(config_module.__file__) - - -@contextmanager -def use_temporary_default_scope() -> Iterator[None]: - """Use temporary mm registry scope. After block is exited, DefaultScope is reverted as before entering block. - - DefaultScope is registered when executing mmdeploy. It doesn't make a problem normally but when making - a new mm model after mmdeploy is done, it can be problematic because registry tries to find a object from default - scope. This context manager is useful for that case. - """ - try: - ori_instance_dict = copy(DefaultScope._instance_dict) # noqa: SLF001 - yield - finally: - DefaultScope._instance_dict = ori_instance_dict # noqa: SLF001 diff --git a/src/otx/core/exporter/native.py b/src/otx/core/exporter/native.py deleted file mode 100644 index aba87dbabed..00000000000 --- a/src/otx/core/exporter/native.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for native model exporter used in OTX.""" - -from __future__ import annotations - -import logging as log -import tempfile -from pathlib import Path -from typing import Any, Literal - -import onnx -import openvino -import torch - -from otx.core.exporter.base import OTXModelExporter -from otx.core.types.precision import OTXPrecisionType - - -class OTXNativeModelExporter(OTXModelExporter): - """Exporter that uses native torch and OpenVINO conversion tools.""" - - def __init__( - self, - input_size: tuple[int, ...], - mean: tuple[float, float, float] = (0.0, 0.0, 0.0), - std: tuple[float, float, float] = (1.0, 1.0, 1.0), - resize_mode: Literal["crop", "standard", "fit_to_window", "fit_to_window_letterbox"] = "standard", - pad_value: int = 0, - swap_rgb: bool = False, - metadata: dict[tuple[str, str], str] | None = None, - via_onnx: bool = False, - onnx_export_configuration: dict[str, Any] | None = None, - output_names: list[str] | None = None, - ) -> None: - super().__init__(input_size, mean, std, resize_mode, pad_value, swap_rgb, metadata, output_names) - self.via_onnx = via_onnx - self.onnx_export_configuration = onnx_export_configuration if onnx_export_configuration is not None else {} - if output_names is not None: - self.onnx_export_configuration.update({"output_names": output_names}) - - def to_openvino( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> Path: - """Export to OpenVINO Intermediate Representation format. - - In this implementation the export is done only via standard OV/ONNX tools. - """ - dummy_tensor = torch.rand(self.input_size).to(next(model.parameters()).device) - - if self.via_onnx: - with tempfile.TemporaryDirectory() as tmpdirname: - tmp_dir = Path(tmpdirname) - - self.to_onnx( - model, - tmp_dir, - base_model_name, - OTXPrecisionType.FP32, - False, - ) - exported_model = openvino.convert_model( - tmp_dir / (base_model_name + ".onnx"), - input=(openvino.runtime.PartialShape(self.input_size),), - ) - else: - exported_model = openvino.convert_model( - model, - example_input=dummy_tensor, - input=(openvino.runtime.PartialShape(self.input_size),), - ) - exported_model = self._postprocess_openvino_model(exported_model) - - save_path = output_dir / (base_model_name + ".xml") - openvino.save_model(exported_model, save_path, compress_to_fp16=(precision == OTXPrecisionType.FP16)) - log.info("Converting to OpenVINO is done.") - - return Path(save_path) - - def to_onnx( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - embed_metadata: bool = True, - ) -> Path: - """Export the given PyTorch model to ONNX format and save it to the specified output directory. - - Args: - model (torch.nn.Module): The PyTorch model to be exported. - output_dir (Path): The directory where the ONNX model will be saved. - base_model_name (str, optional): The base name for the exported model. Defaults to "exported_model". - precision (OTXPrecisionType, optional): The precision type for the exported model. - Defaults to OTXPrecisionType.FP32. - embed_metadata (bool, optional): Whether to embed metadata in the ONNX model. Defaults to True. - - Returns: - Path: The path to the saved ONNX model. - """ - dummy_tensor = torch.rand(self.input_size).to(next(model.parameters()).device) - save_path = str(output_dir / (base_model_name + ".onnx")) - - torch.onnx.export(model, dummy_tensor, save_path, **self.onnx_export_configuration) - - onnx_model = onnx.load(save_path) - onnx_model = self._postprocess_onnx_model(onnx_model, embed_metadata, precision) - - onnx.save(onnx_model, save_path) - log.info("Converting to ONNX is done.") - - return Path(save_path) diff --git a/src/otx/core/exporter/visual_prompting.py b/src/otx/core/exporter/visual_prompting.py deleted file mode 100644 index 67d3de5d74c..00000000000 --- a/src/otx/core/exporter/visual_prompting.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for visual prompting model exporter used in OTX.""" - -from __future__ import annotations - -import logging as log -import tempfile -from pathlib import Path -from typing import Any, Literal - -import onnx -import openvino -import torch - -from otx.core.exporter.native import OTXNativeModelExporter -from otx.core.types.export import OTXExportFormatType -from otx.core.types.precision import OTXPrecisionType - - -class OTXVisualPromptingModelExporter(OTXNativeModelExporter): - """Exporter for visual prompting models that uses native torch and OpenVINO conversion tools.""" - - def export( # type: ignore[override] - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - export_format: OTXExportFormatType = OTXExportFormatType.OPENVINO, - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> dict[str, Path]: - """Exports input model to the specified deployable format, such as OpenVINO IR or ONNX. - - Args: - model (torch.nn.Module): pytorch model top export - output_dir (Path): path to the directory to store export artifacts - base_model_name (str, optional): exported model name - format (OTXExportFormatType): final format of the exported model - precision (OTXExportPrecisionType, optional): precision of the exported model's weights - - Returns: - dict[str, Path]: paths to the exported models - """ - models: dict[str, torch.nn.Module] = { - "image_encoder": model.image_encoder, - "decoder": model, - } - - if export_format == OTXExportFormatType.OPENVINO: - fn = self.to_openvino - elif export_format == OTXExportFormatType.ONNX: - fn = self.to_onnx - elif export_format == OTXExportFormatType.EXPORTABLE_CODE: - msg = "exportable code will be supported soon." - raise NotImplementedError(msg) - else: - msg = f"Unsupported export format: {export_format}" - raise ValueError(msg) - - return { # type: ignore[return-value] - module: fn(models[module], output_dir, f"{base_model_name}_{module}", precision) - for module in ["image_encoder", "decoder"] - } - - def to_openvino( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> Path: - """Export to OpenVINO Intermediate Representation format. - - In this implementation the export is done only via standard OV/ONNX tools. - """ - if not self.via_onnx: - log.info("openvino export on OTXVisualPromptingModelExporter supports only via_onnx, set to True.") - - with tempfile.TemporaryDirectory() as tmpdirname: - tmp_dir = Path(tmpdirname) - - self.to_onnx( - model, - tmp_dir, - base_model_name, - OTXPrecisionType.FP32, - False, - ) - exported_model = openvino.convert_model(tmp_dir / (base_model_name + ".onnx")) - - exported_model = self._postprocess_openvino_model(exported_model) - - save_path = output_dir / (base_model_name + ".xml") - openvino.save_model(exported_model, save_path, compress_to_fp16=(precision == OTXPrecisionType.FP16)) - log.info("Converting to OpenVINO is done.") - - return Path(save_path) - - def to_onnx( - self, - model: torch.nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - embed_metadata: bool = True, - ) -> Path: - """Export the given PyTorch model to ONNX format and save it to the specified output directory. - - Args: - model (torch.nn.Module): The PyTorch model to be exported. - output_dir (Path): The directory where the ONNX model will be saved. - base_model_name (str, optional): The base name for the exported model. Defaults to "exported_model". - precision (OTXPrecisionType, optional): The precision type for the exported model. - Defaults to OTXPrecisionType.FP32. - embed_metadata (bool, optional): Whether to embed metadata in the ONNX model. Defaults to True. - - Returns: - Path: The path to the saved ONNX model. - """ - save_path = str(output_dir / (base_model_name + ".onnx")) - - torch.onnx.export( - model=model, - f=save_path, - **self.get_onnx_dummy_inputs(base_model_name, model), # type: ignore[arg-type] - **self.onnx_export_configuration, - ) - - onnx_model = onnx.load(save_path) - onnx_model = self._postprocess_onnx_model(onnx_model, embed_metadata, precision) - - onnx.save(onnx_model, save_path) - log.info("Converting to ONNX is done.") - - return Path(save_path) - - def get_onnx_dummy_inputs( - self, - base_model_name: Literal["exported_model_image_encoder", "exported_model_decoder"], - model: torch.nn.Module, - ) -> dict[str, Any]: - """Get onnx dummy inputs.""" - if base_model_name == "exported_model_image_encoder": - dummy_inputs = {"images": torch.randn(self.input_size, dtype=torch.float32)} - output_names = ["image_embeddings"] - dynamic_axes = None - else: - dummy_inputs = { - "image_embeddings": torch.zeros( - 1, - model.embed_dim, - model.image_embedding_size, - model.image_embedding_size, - dtype=torch.float32, - ), - "point_coords": torch.randint(low=0, high=1024, size=(1, 2, 2), dtype=torch.float32), - "point_labels": torch.randint(low=0, high=4, size=(1, 2), dtype=torch.float32), - "mask_input": torch.randn( - 1, - 1, - 4 * model.image_embedding_size, - 4 * model.image_embedding_size, - dtype=torch.float32, - ), - "has_mask_input": torch.tensor([[1]], dtype=torch.float32), - "orig_size": torch.randint(low=256, high=2048, size=(1, 2), dtype=torch.int64), - } - output_names = ["upscaled_masks", "iou_predictions", "low_res_masks"] - dynamic_axes = {"point_coords": {1: "num_points"}, "point_labels": {1: "num_points"}} - - return { - "args": tuple(dummy_inputs.values()), - "input_names": list(dummy_inputs.keys()), - "output_names": output_names, - "dynamic_axes": dynamic_axes, - } diff --git a/src/otx/core/file.py b/src/otx/core/file.py new file mode 100644 index 00000000000..588f5558922 --- /dev/null +++ b/src/otx/core/file.py @@ -0,0 +1,14 @@ +"""File system related utilities.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +OTX_CACHE = os.path.expanduser( + os.getenv( + "OTX_CACHE", + os.path.join(os.getenv("XDG_CACHE_HOME", "~/.cache"), "otx"), + ) +) +os.makedirs(OTX_CACHE, exist_ok=True) diff --git a/src/otx/core/metrics/__init__.py b/src/otx/core/metrics/__init__.py deleted file mode 100644 index 1269260a61b..00000000000 --- a/src/otx/core/metrics/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX custom metrices.""" - -from typing import Callable, Union - -from torchmetrics import Metric - -MetricCallable = Union[Callable[[], Metric], Callable[[int], Metric]] diff --git a/src/otx/core/metrics/accuracy.py b/src/otx/core/metrics/accuracy.py deleted file mode 100644 index b6b206e8fb7..00000000000 --- a/src/otx/core/metrics/accuracy.py +++ /dev/null @@ -1,328 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX accuracy metric used for classification tasks.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence - -import torch -from torch import nn -from torchmetrics import ConfusionMatrix, Metric -from torchmetrics.classification.accuracy import Accuracy as TorchmetricAcc -from torchmetrics.classification.accuracy import MultilabelAccuracy as TorchmetricMultilabelAcc - -if TYPE_CHECKING: - from torch import Tensor - - from otx.core.data.dataset.base import LabelInfo - - -class NamedConfusionMatrix(ConfusionMatrix): - """Named Confusion Matrix to add row, col label names.""" - - def __new__( - cls, - col_names: list[str], - row_names: list[str], - task: Literal["binary", "multiclass", "multilabel"], - threshold: float = 0.5, - num_classes: int | None = None, - num_labels: int | None = None, - normalize: Literal["true", "pred", "all", "none"] | None = None, - ignore_index: int | None = None, - validate_args: bool = True, - **kwargs: Any, # noqa: ANN401 - ) -> NamedConfusionMatrix: - """Construct the NamedConfusionMatrix.""" - confusion_metric = super().__new__( - cls, - task=task, - threshold=threshold, - num_classes=num_classes, - num_labels=num_labels, - normalize=normalize, - ignore_index=ignore_index, - validate_args=validate_args, - **kwargs, - ) - - confusion_metric.col_names = col_names - confusion_metric.row_names = row_names - return confusion_metric - - @property - def col_names(self) -> list[str]: - """The names of colum.""" - return self.col_names - - @property - def row_names(self) -> list[str]: - """The names of row.""" - return self.row_names - - -class AccuracywithLabelGroup(Metric): - """Base accuracy class for the OTX classification tasks with lable group. - - It calculates the accuracy with the label_groups information, not class. - It means that average will be applied to the results from the each label groups. - """ - - def __init__(self, average: Literal["MICRO", "MACRO"] = "MICRO", threshold: float = 0.5): - super().__init__() - self.average = average - self.threshold = threshold - self._label_info: LabelInfo - - self.preds: list[Tensor] = [] - self.targets: list[Tensor] = [] - - @property - def label_info(self) -> LabelInfo: - """Get the member `AccuracywithLabelGroup` label information.""" - return self._label_info - - @label_info.setter - def label_info(self, label_info: LabelInfo) -> None: - self._label_info = label_info - - def update(self, preds: Tensor, target: Tensor) -> None: - """Update state with predictions and targets.""" - self.preds.extend(preds) - self.targets.extend(target) - - def _compute_unnormalized_confusion_matrices(self) -> list[NamedConfusionMatrix]: - raise NotImplementedError - - def _compute_accuracy_from_conf_matrices(self, conf_matrices: list[NamedConfusionMatrix]) -> Tensor: - """Compute the accuracy from the confusion matrix.""" - correct_per_label_group = torch.stack([torch.trace(conf_matrix) for conf_matrix in conf_matrices]) - total_per_label_group = torch.stack([torch.sum(conf_matrix) for conf_matrix in conf_matrices]) - - if self.average == "MICRO": - return torch.sum(correct_per_label_group) / torch.sum(total_per_label_group) - if self.average == "MACRO": - return torch.nanmean(torch.divide(correct_per_label_group, total_per_label_group)) - - msg = f"Average should be MICRO or MACRO, got {self.average}" - raise ValueError(msg) - - def compute(self) -> Tensor | dict[str, Any]: - """Compute the metric.""" - conf_matrices = self._compute_unnormalized_confusion_matrices() - - return { - "conf_matrix": conf_matrices, - "accuracy": self._compute_accuracy_from_conf_matrices(conf_matrices), - } - - -class MulticlassAccuracywithLabelGroup(AccuracywithLabelGroup): - """Accuracy class for the multi-class classification with label group. - - For the multi-class classification, the number of label_groups should be 1. - So, the results always the same regardless of average method. - """ - - def _compute_unnormalized_confusion_matrices(self) -> list[NamedConfusionMatrix]: - """Compute an unnormalized confusion matrix for every label group.""" - conf_matrices = [] - for label_group in self.label_info.label_groups: - label_to_idx = {label: index for index, label in enumerate(self.label_info.label_names)} - group_indices = [label_to_idx[label] for label in label_group] - - mask = torch.tensor([t.item() in group_indices for t in self.targets]) - valid_preds = torch.tensor(self.preds)[mask] - valid_targets = torch.tensor(self.targets)[mask] - - for i, index in enumerate(group_indices): - valid_preds[valid_preds == index] = i - valid_targets[valid_targets == index] = i - - num_classes = len(label_group) - confmat = NamedConfusionMatrix( - task="multiclass", - num_classes=num_classes, - row_names=label_group, - col_names=label_group, - ) - conf_matrices.append(confmat(valid_preds, valid_targets)) - return conf_matrices - - -class MultilabelAccuracywithLabelGroup(AccuracywithLabelGroup): - """Accuracy class for the multi-label classification with label_group. - - For the multi-label classification, the number of label_groups should be the same with number of labels. - All lable_group represents whether the label exist or not (binary classification). - """ - - def _compute_unnormalized_confusion_matrices(self) -> list[NamedConfusionMatrix]: - """Compute an unnormalized confusion matrix for every label group.""" - preds = torch.stack(self.preds) - targets = torch.stack(self.targets) - - conf_matrices = [] - for i, label_group in enumerate(self.label_info.label_groups): - label_preds = (preds[:, i] >= self.threshold).long() - label_targets = targets[:, i] - - valid_mask = label_targets >= 0 - if valid_mask.any(): - valid_preds = label_preds[valid_mask] - valid_targets = label_targets[valid_mask] - else: - continue - - data_name = [label_group[0], "~" + label_group[0]] - confmat = NamedConfusionMatrix(task="binary", num_classes=2, row_names=data_name, col_names=data_name).to( - self.device, - ) - conf_matrices.append(confmat(valid_preds, valid_targets)) - return conf_matrices - - -class HlabelAccuracy(AccuracywithLabelGroup): - """Accuracy class for the hierarchical-label classification. - - H-label Classification is the combination version of multi-class and multi-label classification. - It could have multiple heads for the multi-class classification to classify complex hierarchy architecture. - For the multi-label part, it's the same with the CusotmMultilabelAccuracy. - """ - - def _is_multiclass_group(self, label_group: list[str]) -> bool: - return len(label_group) != 1 - - def _compute_unnormalized_confusion_matrices(self) -> list[NamedConfusionMatrix]: - """Compute an unnormalized confusion matrix for every label group.""" - preds = torch.stack(self.preds) - targets = torch.stack(self.targets) - - conf_matrices = [] - for i, label_group in enumerate(self.label_info.label_groups): - label_preds = preds[:, i] - label_targets = targets[:, i] - - valid_mask = label_targets >= 0 - if valid_mask.any(): - valid_preds = label_preds[valid_mask] - valid_targets = label_targets[valid_mask] - else: - continue - - if self._is_multiclass_group(label_group): - num_classes = len(label_group) - confmat = NamedConfusionMatrix( - task="multiclass", - num_classes=num_classes, - row_names=label_group, - col_names=label_group, - ).to(self.device) - conf_matrices.append(confmat(valid_preds, valid_targets)) - else: - label_preds = (label_preds >= self.threshold).long() - data_name = [label_group[0], "~" + label_group[0]] - confmat = NamedConfusionMatrix( - task="binary", - num_classes=2, - row_names=data_name, - col_names=data_name, - ).to(self.device) - conf_matrices.append(confmat(valid_preds, valid_targets)) - return conf_matrices - - -class MixedHLabelAccuracy(Metric): - """Mixed accuracy metric for h-label classification. - - It only used multi-class and multi-label metrics from torchmetrics. - This is different from the CustomHlabelAccuracy since MixedHLabelAccuracy doesn't use label_groups info. - It makes large gap to the results since CusotmHlabelAccuracy averages the results by using the label_groups info. - - Args: - num_multiclass_heads (int): Number of multi-class heads. - num_multilabel_classes (int): Number of multi-label classes. - head_idx_to_logits_range (dict[str, tuple[int, int]]): The range of logits which represents - the number of classes for each heads. - threshold_multilabel (float): Predictions with scores under the thresholds - are considered as negative. Defaults to 0.5. - """ - - def __init__( - self, - num_multiclass_heads: int, - num_multilabel_classes: int, - head_logits_info: dict[str, tuple[int, int]], - threshold_multilabel: float = 0.5, - ): - super().__init__() - - self.num_multiclass_heads = num_multiclass_heads - if num_multiclass_heads == 0: - msg = "The number of multiclass heads should be larger than 0" - raise ValueError(msg) - - self.num_multilabel_classes = num_multilabel_classes - self.threshold_multilabel = threshold_multilabel - - # Multiclass classification accuracy - self.multiclass_head_accuracy: list[TorchmetricAcc] = [ - TorchmetricAcc( - task="multiclass", - num_classes=int(head_range[1] - head_range[0]), - ) - for head_range in head_logits_info.values() - ] - - # Multilabel classification accuracy metrics - if self.num_multilabel_classes > 0: - self.multilabel_accuracy = TorchmetricMultilabelAcc( - num_labels=self.num_multilabel_classes, - threshold=0.5, - average="macro", - ) - - def _apply(self, fn: Callable, exclude_state: Sequence[str] = "") -> nn.Module: - self.multiclass_head_accuracy = [acc._apply(fn, exclude_state) for acc in self.multiclass_head_accuracy] # noqa: SLF001 - if self.num_multilabel_classes > 0: - self.multilabel_accuracy = self.multilabel_accuracy._apply(fn, exclude_state) # noqa: SLF001 - return self - - def update(self, preds: torch.Tensor, target: torch.Tensor) -> None: - """Update state with predictions and targets.""" - # Split preds into multiclass and multilabel parts - for head_idx in range(self.num_multiclass_heads): - preds_multiclass = preds[:, head_idx] - target_multiclass = target[:, head_idx] - multiclass_mask = target_multiclass > 0 - - is_all_multiclass_ignored = not multiclass_mask.any() - if not is_all_multiclass_ignored: - self.multiclass_head_accuracy[head_idx].update( - preds_multiclass[multiclass_mask], - target_multiclass[multiclass_mask], - ) - - if self.num_multilabel_classes > 0: - # Split preds into multiclass and multilabel parts - preds_multilabel = preds[:, self.num_multiclass_heads :] - target_multilabel = target[:, self.num_multiclass_heads :] - # Multilabel update - self.multilabel_accuracy.update(preds_multilabel, target_multilabel) - - def compute(self) -> torch.Tensor: - """Compute the final statistics.""" - multiclass_accs = torch.mean( - torch.stack( - [acc.compute() for acc in self.multiclass_head_accuracy], - ), - ) - - if self.num_multilabel_classes > 0: - multilabel_acc = self.multilabel_accuracy.compute() - - return (multiclass_accs + multilabel_acc) / 2 - - return multiclass_accs diff --git a/src/otx/core/metrics/fmeasure.py b/src/otx/core/metrics/fmeasure.py deleted file mode 100644 index 19be72844dc..00000000000 --- a/src/otx/core/metrics/fmeasure.py +++ /dev/null @@ -1,755 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX custom f1 metrices.""" - -from __future__ import annotations - -import logging - -import numpy as np -from torch import Tensor -from torchmetrics import Metric - -logger = logging.getLogger() -ALL_CLASSES_NAME = "All Classes" - - -def intersection_box( - box1: tuple, - box2: tuple, -) -> tuple[float, float, float, float]: - """Calculate the intersection box of two bounding boxes. - - Args: - box1 (tuple): (x1, y1, x2, y2, class, score) - box2 (tuple): (x1, y1, x2, y2, class, score) - - Returns: - tuple[float, float, float, float]: (x_left, x_right, y_bottom, y_top) - """ - x_left = max(box1[0], box2[0]) - y_top = max(box1[1], box2[1]) - x_right = min(box1[2], box2[2]) - y_bottom = min(box1[3], box2[3]) - return (x_left, x_right, y_bottom, y_top) - - -def bounding_box_intersection_over_union( - box1: tuple, - box2: tuple, -) -> float: - """Calculate the Intersection over Union (IoU) of two bounding boxes. - - Args: - box1 (tuple): (x1, y1, x2, y2, class, score) - box2 (tuple): (x1, y1, x2, y2, class, score) - - Raises: - ValueError: In case the IoU is outside of [0.0, 1.0] - - Returns: - float: Intersection-over-union of box1 and box2. - """ - x_left, x_right, y_bottom, y_top = intersection_box(box1, box2) - - if x_right <= x_left or y_bottom <= y_top: - iou = 0.0 - else: - intersection_area = (x_right - x_left) * (y_bottom - y_top) - bb1_area = (box1[2] - box1[0]) * (box1[3] - box1[1]) - bb2_area = (box2[2] - box2[0]) * (box2[3] - box2[1]) - union_area = float(bb1_area + bb2_area - intersection_area) - iou = 0.0 if union_area == 0 else intersection_area / union_area - if iou < 0.0 or iou > 1.0: - msg = f"intersection over union should be in range [0,1], actual={iou}" - raise ValueError(msg) - return iou - - -def get_iou_matrix( - ground_truth: list[tuple], - predicted: list[tuple], -) -> np.ndarray: - """Constructs an iou matrix of shape [num_ground_truth_boxes, num_predicted_boxes]. - - Each cell(x,y) in the iou matrix contains the intersection over union of ground truth box(x) and predicted box(y) - An iou matrix corresponds to a single image - - Args: - ground_truth (list[tuple]): list of ground truth boxes. - Each box is a list of (x,y) coordinates and a label. - a box: [x1: float, y1, x2, y2, class: str, score: float] - boxes_per_image: [box1, box2, …] - boxes1: [boxes_per_image_1, boxes_per_image_2, boxes_per_image_3, …] - predicted (list[tuple]): list of predicted boxes. - Each box is a list of (x,y) coordinates and a label. - a box: [x1: float, y1, x2, y2, class: str, score: float] - boxes_per_image: [box1, box2, …] - boxes2: [boxes_per_image_1, boxes_per_image_2, boxes_per_image_3, …] - - Returns: - np.ndarray: IoU matrix of shape [ground_truth_boxes, predicted_boxes] - """ - return np.array( - [[bounding_box_intersection_over_union(gts, preds) for preds in predicted] for gts in ground_truth], - ) - - -def get_n_false_negatives(iou_matrix: np.ndarray, iou_threshold: float) -> int: - """Get the number of false negatives inside the IoU matrix for a given threshold. - - The first loop accounts for all the ground truth boxes which do not have a high enough iou with any predicted - box (they go undetected) - The second loop accounts for the much rarer case where two ground truth boxes are detected by the same predicted - box. The principle is that each ground truth box requires a unique prediction box - - Args: - iou_matrix (np.ndarray): IoU matrix of shape [ground_truth_boxes, predicted_boxes] - iou_threshold (float): IoU threshold to use for the false negatives. - - Returns: - int: Number of false negatives - """ - n_false_negatives = 0 - for row in iou_matrix: - if max(row) < iou_threshold: - n_false_negatives += 1 - for column in np.rot90(iou_matrix): - indices = np.where(column > iou_threshold) - n_false_negatives += max(len(indices[0]) - 1, 0) - return n_false_negatives - - -class _Metrics: - """This class collects the metrics related to detection. - - Args: - f_measure (float): F-measure of the model. - precision (float): Precision of the model. - recall (float): Recall of the model. - """ - - def __init__(self, f_measure: float, precision: float, recall: float): - self.f_measure = f_measure - self.precision = precision - self.recall = recall - - -class _ResultCounters: - """This class collects the number of prediction, TP and FN. - - Args: - n_false_negatives (int): Number of false negatives. - n_true (int): Number of true positives. - n_predictions (int): Number of predictions. - """ - - def __init__(self, n_false_negatives: int, n_true: int, n_predicted: int): - self.n_false_negatives = n_false_negatives - self.n_true = n_true - self.n_predicted = n_predicted - - def calculate_f_measure(self) -> _Metrics: - """Calculates and returns precision, recall, and f-measure. - - Returns: - _Metrics: _Metrics object with Precision, recall, and f-measure. - """ - n_true_positives = self.n_true - self.n_false_negatives - - if self.n_predicted == 0: - precision = 1.0 - recall = 0.0 - elif self.n_true == 0: - precision = 0.0 - recall = 1.0 - else: - precision = n_true_positives / self.n_predicted - recall = n_true_positives / self.n_true - - f_measure = (2 * precision * recall) / (precision + recall + np.finfo(float).eps) - return _Metrics(f_measure, precision, recall) - - -class _AggregatedResults: - """This class collects the aggregated results for F-measure. - - The result contains: - - f_measure_curve - - precision_curve - - recall_curve - - all_classes_f_measure_curve - - best_f_measure - - best_threshold - - Args: - classes (list[str]): list of classes. - """ - - def __init__(self, classes: list[str]): - self.f_measure_curve: dict[str, list[float]] = {class_name: [] for class_name in classes} - self.precision_curve: dict[str, list[float]] = {class_name: [] for class_name in classes} - self.recall_curve: dict[str, list[float]] = {class_name: [] for class_name in classes} - self.all_classes_f_measure_curve: list[float] = [] - self.best_f_measure: float = 0.0 - self.best_threshold: float = 0.0 - - -class _OverallResults: - """This class collects the overall results that is computed by the F-measure performance provider. - - Args: - per_confidence (_AggregatedResults): _AggregatedResults object for each confidence level. - per_nms (_AggregatedResults | None): _AggregatedResults object for each NMS threshold. - best_f_measure_per_class (dict[str, float]): Best f-measure per class. - best_f_measure (float): Best f-measure. - """ - - def __init__( - self, - per_confidence: _AggregatedResults, - per_nms: _AggregatedResults | None, - best_f_measure_per_class: dict[str, float], - best_f_measure: float, - ): - self.per_confidence = per_confidence - self.per_nms = per_nms - self.best_f_measure_per_class = best_f_measure_per_class - self.best_f_measure = best_f_measure - - -class _FMeasureCalculator: - """This class contains the functions to calculate FMeasure. - - Args: - ground_truth_boxes_per_image (list[list[tuple]]): - a box: [x1: float, y1, x2, y2, class: str, score: float] - boxes_per_image: [box1, box2, …] - ground_truth_boxes_per_image: [boxes_per_image_1, boxes_per_image_2, boxes_per_image_3, …] - prediction_boxes_per_image (list[list[tuple]]): - a box: [x1: float, y1, x2, y2, class: str, score: float] - boxes_per_image: [box1, box2, …] - predicted_boxes_per_image: [boxes_per_image_1, boxes_per_image_2, boxes_per_image_3, …] - """ - - def __init__( - self, - ground_truth_boxes_per_image: list[list[tuple]], - prediction_boxes_per_image: list[list[tuple]], - ): - self.ground_truth_boxes_per_image = ground_truth_boxes_per_image - self.prediction_boxes_per_image = prediction_boxes_per_image - self.confidence_range = [0.025, 1.0, 0.025] - self.nms_range = [0.1, 1, 0.05] - self.default_confidence_threshold = 0.35 - - def evaluate_detections( - self, - classes: list[str], - iou_threshold: float = 0.5, - result_based_nms_threshold: bool = False, - cross_class_nms: bool = False, - ) -> _OverallResults: - """Evaluates detections by computing f_measures across multiple confidence thresholds and iou thresholds. - - By default, this function evaluates 39 confidence thresholds, finds the best confidence threshold and appends - it to the result dict - Each one of the (default 39+20) pairs of confidence and nms thresholds is used to evaluate the f-measure for - each class, then the intermediate metrics are summed across classes to compute an all_classes f_measure. - Finally, the best results across all evaluations are appended to the result dictionary along with the thresholds - used to achieve them. - - Args: - classes (list[str]): Names of classes to be evaluated. - iou_threshold (float): IOU threshold. Defaults to 0.5. - result_based_nms_threshold (bool): Boolean that determines whether multiple nms threshold are examined. - Defaults to False. - cross_class_nms (bool): Set to True to perform NMS between boxes with different classes. Defaults to False. - - Returns: - _OverallResults: _OverallResults object with the result statistics (e.g F-measure). - """ - best_f_measure_per_class = {} - - results_per_confidence = self.get_results_per_confidence( - classes=classes, - confidence_range=self.confidence_range, - iou_threshold=iou_threshold, - ) - - best_f_measure = results_per_confidence.best_f_measure - - for class_name in classes: - best_f_measure_per_class[class_name] = max(results_per_confidence.f_measure_curve[class_name]) - - results_per_nms: _AggregatedResults | None = None - - if result_based_nms_threshold: - results_per_nms = self.get_results_per_nms( - classes=classes, - iou_threshold=iou_threshold, - min_f_measure=results_per_confidence.best_f_measure, - cross_class_nms=cross_class_nms, - ) - - for class_name in classes: - best_f_measure_per_class[class_name] = max(results_per_nms.f_measure_curve[class_name]) - - return _OverallResults( - results_per_confidence, - results_per_nms, - best_f_measure_per_class, - best_f_measure, - ) - - def get_results_per_confidence( - self, - classes: list[str], - confidence_range: list[float], - iou_threshold: float, - ) -> _AggregatedResults: - """Returns the results for confidence threshold in range confidence_range. - - Varies confidence based on confidence_range, the results are appended in a dictionary and returned, it also - returns the best f_measure found and the confidence threshold used to get said f_measure - - Args: - classes (list[str]): Names of classes to be evaluated. - confidence_range (list[float]): list of confidence thresholds to be evaluated. - iou_threshold (float): IoU threshold to use for false negatives. - - Returns: - _AggregatedResults: _AggregatedResults object with the result statistics (e.g F-measure). - """ - result = _AggregatedResults(classes) - result.best_threshold = 0.1 - - for confidence_threshold in np.arange(*confidence_range): - result_point = self.evaluate_classes( - classes=classes.copy(), - iou_threshold=iou_threshold, - confidence_threshold=confidence_threshold, - ) - all_classes_f_measure = result_point[ALL_CLASSES_NAME].f_measure - result.all_classes_f_measure_curve.append(all_classes_f_measure) - - for class_name in classes: - result.f_measure_curve[class_name].append(result_point[class_name].f_measure) - result.precision_curve[class_name].append(result_point[class_name].precision) - result.recall_curve[class_name].append(result_point[class_name].recall) - if all_classes_f_measure > 0.0 and all_classes_f_measure >= result.best_f_measure: - result.best_f_measure = all_classes_f_measure - result.best_threshold = confidence_threshold - return result - - def get_results_per_nms( - self, - classes: list[str], - iou_threshold: float, - min_f_measure: float, - cross_class_nms: bool = False, - ) -> _AggregatedResults: - """Returns results for nms threshold in range nms_range. - - First, we calculate the critical nms of each box, meaning the nms_threshold - that would cause it to be disappear - This is an expensive O(n**2) operation, however, doing this makes filtering for every single nms_threshold much - faster at O(n) - - Args: - classes (list[str]): list of classes - iou_threshold (float): IoU threshold - min_f_measure (float): the minimum F-measure required to select a NMS threshold - cross_class_nms (bool): set to True to perform NMS between boxes with different classes. Defaults to False. - - Returns: - _AggregatedResults: Object containing the results for each NMS threshold value - """ - result = _AggregatedResults(classes) - result.best_f_measure = min_f_measure - result.best_threshold = 0.5 - - critical_nms_per_image = self.__get_critical_nms(self.prediction_boxes_per_image, cross_class_nms) - - for nms_threshold in np.arange(*self.nms_range): - predicted_boxes_per_image_per_nms = self.__filter_nms( - self.prediction_boxes_per_image, - critical_nms_per_image, - nms_threshold, - ) - boxes_pair_for_nms = _FMeasureCalculator( - self.ground_truth_boxes_per_image, - predicted_boxes_per_image_per_nms, - ) - result_point = boxes_pair_for_nms.evaluate_classes( - classes=classes.copy(), - iou_threshold=iou_threshold, - confidence_threshold=self.default_confidence_threshold, - ) - all_classes_f_measure = result_point[ALL_CLASSES_NAME].f_measure - result.all_classes_f_measure_curve.append(all_classes_f_measure) - - for class_name in classes: - result.f_measure_curve[class_name].append(result_point[class_name].f_measure) - result.precision_curve[class_name].append(result_point[class_name].precision) - result.recall_curve[class_name].append(result_point[class_name].recall) - - if all_classes_f_measure > 0.0 and all_classes_f_measure >= result.best_f_measure: - result.best_f_measure = all_classes_f_measure - result.best_threshold = nms_threshold - return result - - def evaluate_classes( - self, - classes: list[str], - iou_threshold: float, - confidence_threshold: float, - ) -> dict[str, _Metrics]: - """Returns dict of f_measure, precision and recall for each class. - - Args: - classes (list[str]): list of classes to be evaluated. - iou_threshold (float): IoU threshold to use for false negatives. - confidence_threshold (float): Confidence threshold to use for false negatives. - - Returns: - dict[str, _Metrics]: The metrics (e.g. F-measure) for each class. - """ - result: dict[str, _Metrics] = {} - - all_classes_counters = _ResultCounters(0, 0, 0) - - if ALL_CLASSES_NAME in classes: - classes.remove(ALL_CLASSES_NAME) - for class_name in classes: - metrics, counters = self.get_f_measure_for_class( - class_name=class_name, - iou_threshold=iou_threshold, - confidence_threshold=confidence_threshold, - ) - result[class_name] = metrics - all_classes_counters.n_false_negatives += counters.n_false_negatives - all_classes_counters.n_true += counters.n_true - all_classes_counters.n_predicted += counters.n_predicted - - # for all classes - result[ALL_CLASSES_NAME] = all_classes_counters.calculate_f_measure() - return result - - def get_f_measure_for_class( - self, - class_name: str, - iou_threshold: float, - confidence_threshold: float, - ) -> tuple[_Metrics, _ResultCounters]: - """Get f_measure for specific class, iou threshold, and confidence threshold. - - In order to reduce the number of redundant iterations and allow for cleaner, more general code later on, - all boxes are filtered at this stage by class and predicted boxes are filtered by confidence threshold - - Args: - class_name (str): Name of the class for which the F measure is computed - iou_threshold (float): IoU threshold - confidence_threshold (float): Confidence threshold - - Returns: - tuple[_Metrics, _ResultCounters]: a structure containing the statistics (e.g. f_measure) and a structure - containing the intermediated counters used to derive the stats (e.g. num. false positives) - """ - class_ground_truth_boxes_per_image = self.__filter_class(self.ground_truth_boxes_per_image, class_name) - confidence_predicted_boxes_per_image = self.__filter_confidence( - self.prediction_boxes_per_image, - confidence_threshold, - ) - class_predicted_boxes_per_image = self.__filter_class(confidence_predicted_boxes_per_image, class_name) - if len(class_ground_truth_boxes_per_image) > 0: - boxes_pair_per_class = _FMeasureCalculator( - ground_truth_boxes_per_image=class_ground_truth_boxes_per_image, - prediction_boxes_per_image=class_predicted_boxes_per_image, - ) - result_counters = boxes_pair_per_class.get_counters(iou_threshold=iou_threshold) - result_metrics = result_counters.calculate_f_measure() - results = (result_metrics, result_counters) - else: - logger.warning("No ground truth images supplied for f-measure calculation.") - # [f_measure, precision, recall, n_false_negatives, n_true, n_predicted] - results = (_Metrics(0.0, 0.0, 0.0), _ResultCounters(0, 0, 0)) - return results - - @staticmethod - def __get_critical_nms( - boxes_per_image: list[list[tuple]], - cross_class_nms: bool = False, - ) -> list[list[float]]: - """Return list of critical NMS values for each box in each image. - - Maps each predicted box to the highest nms-threshold which would suppress that box, aka the smallest - nms_threshold before the box disappears. - Having these values allows us to later filter by nms-threshold in O(n) rather than O(n**2) - Highest losing iou, holds the value of the highest iou that a box has with any - other box of the same class and higher confidence score. - - Args: - boxes_per_image (list[list[tuple]]): list of predicted boxes per - image. - a box: [x1: float, y1, x2, y2, class: str, score: float] - boxes_per_image: [box1, box2, …] - cross_class_nms (bool): Whether to use cross class NMS. - - Returns: - list[list[float]]: list of critical NMS values for each box in each image. - """ - critical_nms_per_image = [] - for boxes in boxes_per_image: - critical_nms_per_box = [] - for box1 in boxes: - highest_losing_iou = 0.0 - for box2 in boxes: - iou = bounding_box_intersection_over_union(box1, box2) - if ( - (cross_class_nms or box1[4] == box2[4]) - and box1[5] < box2[5] # type: ignore[operator] - and iou > highest_losing_iou - ): - highest_losing_iou = iou - critical_nms_per_box.append(highest_losing_iou) - critical_nms_per_image.append(critical_nms_per_box) - return critical_nms_per_image - - @staticmethod - def __filter_nms( - boxes_per_image: list[list[tuple]], - critical_nms: list[list[float]], - nms_threshold: float, - ) -> list[list[tuple]]: - """Filters out predicted boxes whose critical nms is higher than the given nms_threshold. - - Args: - boxes_per_image (list[list[tuple]]): list of boxes per image. - a box: [x1: float, y1, x2, y2, class: str, score: float] - boxes_per_image: [box1, box2, …] - critical_nms (list[list[float]]): list of list of critical nms for each box in each image - nms_threshold (float): NMS threshold used for filtering - - Returns: - list[list[tuple]]: list of list of filtered boxes in each image - """ - new_boxes_per_image = [] - for boxes, boxes_nms in zip(boxes_per_image, critical_nms): - new_boxes = [] - for box, nms in zip(boxes, boxes_nms): - if nms < nms_threshold: - new_boxes.append(box) - new_boxes_per_image.append(new_boxes) - return new_boxes_per_image - - @staticmethod - def __filter_class( - boxes_per_image: list[list[tuple]], - class_name: str, - ) -> list[list[tuple]]: - """Filters boxes to only keep members of one class. - - Args: - boxes_per_image (list[list[tuple]]): a list of lists of boxes - class_name (str): Name of the class for which the boxes are filtered - - Returns: - list[list[tuple]]: a list of lists of boxes - """ - filtered_boxes_per_image = [] - for boxes in boxes_per_image: - filtered_boxes = [box for box in boxes if box[4].lower() == class_name.lower()] - filtered_boxes_per_image.append(filtered_boxes) - return filtered_boxes_per_image - - @staticmethod - def __filter_confidence( - boxes_per_image: list[list[tuple]], - confidence_threshold: float, - ) -> list[list[tuple]]: - """Filters boxes to only keep ones with higher confidence than a given confidence threshold. - - Args: - boxes_per_image (list[list[tuple]]): - a box: [x1: float, y1, x2, y2, class: str, score: float] - boxes_per_image: [box1, box2, …] - confidence_threshold (float): Confidence threshold - - Returns: - list[list[tuple]]: Boxes with higher confidence than the given - threshold. - """ - filtered_boxes_per_image = [] - for boxes in boxes_per_image: - filtered_boxes = [box for box in boxes if float(box[5]) > confidence_threshold] - filtered_boxes_per_image.append(filtered_boxes) - return filtered_boxes_per_image - - def get_counters(self, iou_threshold: float) -> _ResultCounters: - """Return counts of true positives, false positives and false negatives for a given iou threshold. - - For each image (the loop), compute the number of false negatives, the number of predicted boxes, and the number - of ground truth boxes, then add each value to its corresponding counter - - Args: - iou_threshold (float): IoU threshold - - Returns: - _ResultCounters: Structure containing the number of false negatives, true positives and predictions. - """ - n_false_negatives = 0 - n_true = 0 - n_predicted = 0 - for ground_truth_boxes, predicted_boxes in zip( - self.ground_truth_boxes_per_image, - self.prediction_boxes_per_image, - ): - n_true += len(ground_truth_boxes) - n_predicted += len(predicted_boxes) - if len(predicted_boxes) > 0: - if len(ground_truth_boxes) > 0: - iou_matrix = get_iou_matrix(ground_truth_boxes, predicted_boxes) - n_false_negatives += get_n_false_negatives(iou_matrix, iou_threshold) - else: - n_false_negatives += len(ground_truth_boxes) - return _ResultCounters(n_false_negatives, n_true, n_predicted) - - -class FMeasure(Metric): - """Computes the f-measure (also known as F1-score) for a resultset. - - The f-measure is typically used in detection (localization) tasks to obtain a single number that balances precision - and recall. - - To determine whether a predicted box matches a ground truth box an overlap measured - is used based on a minimum - intersection-over-union (IoU), by default a value of 0.5 is used. - - In addition spurious results are eliminated by applying non-max suppression (NMS) so that two predicted boxes with - IoU > threshold are reduced to one. This threshold can be determined automatically by setting `vary_nms_threshold` - to True. - - Args: - num_classes (int): The number of classes. - best_confidence_threshold (float | None): Pre-defined best confidence threshold. If this value is None, then - FMeasure will find best confidence threshold. Defaults to None. - vary_nms_threshold (bool): if True the maximal F-measure is determined by optimizing for different NMS threshold - values. Defaults to False. - cross_class_nms (bool): Whether non-max suppression should be applied cross-class. If True this will eliminate - boxes with sufficient overlap even if they are from different classes. Defaults to False. - """ - - def __init__( - self, - num_classes: int, - best_confidence_threshold: float | None = None, - vary_nms_threshold: bool = False, - cross_class_nms: bool = False, - ): - super().__init__() - self.vary_nms_threshold = vary_nms_threshold - self.cross_class_nms = cross_class_nms - self.preds: list[list[tuple]] = [] - self.targets: list[list[tuple]] = [] - self.num_classes: int = num_classes - - self._f_measure_per_confidence: dict | None = None - self._f_measure_per_nms: dict | None = None - self._best_confidence_threshold: float | None = best_confidence_threshold - self._best_nms_threshold: float | None = None - - def update(self, preds: list[dict[str, Tensor]], target: list[dict[str, Tensor]]) -> None: - """Update total predictions and targets from given batch predicitons and targets.""" - for pred, tget in zip(preds, target): - self.preds.append( - [ - (*box, self.classes[label], score) - for box, label, score in zip( - pred["boxes"].tolist(), - pred["labels"].tolist(), - pred["scores"].tolist(), - ) - ], - ) - self.targets.append( - [ - (*box, self.classes[label], 0.0) - for box, label in zip(tget["boxes"].tolist(), tget["labels"].tolist()) - ], - ) - - def compute(self) -> dict: - """Compute f1 score metric.""" - boxes_pair = _FMeasureCalculator(self.targets, self.preds) - result = boxes_pair.evaluate_detections( - result_based_nms_threshold=self.vary_nms_threshold, - classes=self.classes, - cross_class_nms=self.cross_class_nms, - ) - self._f_measure_per_label = {label: result.best_f_measure_per_class[label] for label in self.classes} - - if self.best_confidence_threshold is not None: - (index,) = np.where( - np.isclose(list(np.arange(*boxes_pair.confidence_range)), self.best_confidence_threshold), - ) - self._f_measure = result.per_confidence.all_classes_f_measure_curve[int(index)] - else: - self._f_measure_per_confidence = { - "xs": list(np.arange(*boxes_pair.confidence_range)), - "ys": result.per_confidence.all_classes_f_measure_curve, - } - self._best_confidence_threshold = result.per_confidence.best_threshold - self._f_measure = result.best_f_measure - - if self.vary_nms_threshold and result.per_nms is not None: - self._f_measure_per_nms = { - "xs": list(np.arange(*boxes_pair.nms_range)), - "ys": result.per_nms.all_classes_f_measure_curve, - } - self._best_nms_threshold = result.per_nms.best_threshold - return {"f1-score": Tensor([self.f_measure])} - - @property - def f_measure(self) -> float: - """Returns the f-measure.""" - return self._f_measure - - @property - def f_measure_per_label(self) -> dict[str, float]: - """Returns the f-measure per label as dictionary (Label -> Score).""" - return self._f_measure_per_label - - @property - def f_measure_per_confidence(self) -> None | dict: - """Returns the curve for f-measure per confidence as dictionary if exists.""" - return self._f_measure_per_confidence - - @property - def best_confidence_threshold(self) -> None | float: - """Returns best confidence threshold as ScoreMetric if exists.""" - return self._best_confidence_threshold - - @best_confidence_threshold.setter - def best_confidence_threshold(self, value: float) -> None: - """Setter for best_confidence_threshold.""" - self._best_confidence_threshold = value - - @property - def f_measure_per_nms(self) -> None | dict: - """Returns the curve for f-measure per nms threshold as CurveMetric if exists.""" - return self._f_measure_per_nms - - @property - def best_nms_threshold(self) -> None | float: - """Returns the best NMS threshold as ScoreMetric if exists.""" - return self._best_nms_threshold - - @property - def classes(self) -> list[str]: - """Class information of dataset.""" - if self.num_classes is None: - msg = "classes is called before num_classes is set." - raise ValueError(msg) - return [str(idx) for idx in range(self.num_classes)] diff --git a/src/otx/core/model/__init__.py b/src/otx/core/model/__init__.py deleted file mode 100644 index 57d80523589..00000000000 --- a/src/otx/core/model/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for base model and lightning module classes used in OTX.""" diff --git a/src/otx/core/model/entity/__init__.py b/src/otx/core/model/entity/__init__.py deleted file mode 100644 index 82e7cb4208b..00000000000 --- a/src/otx/core/model/entity/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for model entities used in OTX.""" diff --git a/src/otx/core/model/entity/action_classification.py b/src/otx/core/model/entity/action_classification.py deleted file mode 100644 index ae9c5122383..00000000000 --- a/src/otx/core/model/entity/action_classification.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for action_classification model entity used in OTX.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -import numpy as np -import torch - -from otx.core.data.entity.action_classification import ( - ActionClsBatchDataEntity, - ActionClsBatchPredEntity, - ActionClsBatchPredEntityWithXAI, -) -from otx.core.data.entity.base import OTXBatchLossEntity, T_OTXBatchPredEntityWithXAI -from otx.core.data.entity.tile import T_OTXTileBatchDataEntity -from otx.core.exporter.native import OTXNativeModelExporter -from otx.core.model.entity.base import OTXModel, OVModel -from otx.core.utils.config import inplace_num_classes -from otx.core.utils.utils import get_mean_std_from_data_processing - -if TYPE_CHECKING: - from omegaconf import DictConfig - from openvino.model_api.models.utils import ClassificationResult - from torch import nn - - from otx.core.exporter.base import OTXModelExporter - - -class OTXActionClsModel( - OTXModel[ActionClsBatchDataEntity, ActionClsBatchPredEntity, T_OTXBatchPredEntityWithXAI, T_OTXTileBatchDataEntity], -): - """Base class for the action classification models used in OTX.""" - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parameters = super()._export_parameters - parameters["metadata"].update( - { - ("model_info", "model_type"): "Action Classification", - ("model_info", "task_type"): "action classification", - }, - ) - return parameters - - -class MMActionCompatibleModel(OTXActionClsModel): - """Action classification model compitible for MMAction. - - It can consume MMAction model configuration translated into OTX configuration - (please see otx.tools.translate_mmrecipe) and create the OTX Action classification model - compatible for OTX pipelines. - """ - - def __init__(self, num_classes: int, config: DictConfig) -> None: - config = inplace_num_classes(cfg=config, num_classes=num_classes) - self.config = config - self.load_from = config.pop("load_from", None) - self.image_size = (1, 1, 3, 8, 224, 224) - super().__init__(num_classes=num_classes) - - def _create_model(self) -> nn.Module: - from .utils.mmaction import create_model - - model, self.classification_layers = create_model(self.config, self.load_from) - return model - - def _customize_inputs(self, entity: ActionClsBatchDataEntity) -> dict[str, Any]: - """Convert ActionClsBatchDataEntity into mmaction model's input.""" - from mmaction.structures import ActionDataSample - - mmaction_inputs: dict[str, Any] = {} - - mmaction_inputs["inputs"] = entity.images - mmaction_inputs["data_samples"] = [ - ActionDataSample( - metainfo={ - "img_id": img_info.img_idx, - "img_shape": img_info.img_shape, - "ori_shape": img_info.ori_shape, - "pad_shape": img_info.pad_shape, - "scale_factor": img_info.scale_factor, - }, - gt_label=labels, - ) - for img_info, labels in zip(entity.imgs_info, entity.labels) - ] - - mmaction_inputs = self.model.data_preprocessor(data=mmaction_inputs, training=self.training) - mmaction_inputs["mode"] = "loss" if self.training else "predict" - return mmaction_inputs - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: ActionClsBatchDataEntity, - ) -> ActionClsBatchPredEntity | OTXBatchLossEntity: - from mmaction.structures import ActionDataSample - - if self.training: - if not isinstance(outputs, dict): - raise TypeError(outputs) - - losses = OTXBatchLossEntity() - for k, v in outputs.items(): - losses[k] = v - return losses - - scores = [] - labels = [] - - for output in outputs: - if not isinstance(output, ActionDataSample): - raise TypeError(output) - - scores.append(output.pred_score) - labels.append(output.pred_label) - - return ActionClsBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - labels=labels, - ) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - export_params = super()._export_parameters - export_params.update(get_mean_std_from_data_processing(self.config)) - export_params["resize_mode"] = "standard" - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - export_params["via_onnx"] = False - export_params["input_size"] = self.image_size - export_params["onnx_export_configuration"] = None - - return export_params - - @property - def _exporter(self) -> OTXModelExporter: - """Creates OTXModelExporter object that can export the model.""" - return OTXNativeModelExporter(**self._export_parameters) - - -class OVActionClsModel( - OVModel[ActionClsBatchDataEntity, ActionClsBatchPredEntity, ActionClsBatchPredEntityWithXAI], -): - """Action Classification model compatible for OpenVINO IR inference. - - It can consume OpenVINO IR model path or model name from Intel OMZ repository - and create the OTX classification model compatible for OTX testing pipeline. - """ - - def __init__( - self, - num_classes: int, - model_name: str, - model_type: str = "Action Classification", - async_inference: bool = True, - max_num_requests: int | None = None, - use_throughput_mode: bool = False, - model_api_configuration: dict[str, Any] | None = None, - ) -> None: - super().__init__( - num_classes, - model_name, - model_type, - async_inference, - max_num_requests, - use_throughput_mode, - model_api_configuration, - ) - - def _customize_inputs(self, entity: ActionClsBatchDataEntity) -> dict[str, Any]: - # restore original numpy image - images = [np.transpose(im.cpu().numpy(), (0, 2, 3, 1)) for im in entity.images] - return {"inputs": images} - - def _customize_outputs( - self, - outputs: list[ClassificationResult], - inputs: ActionClsBatchDataEntity, - ) -> ActionClsBatchPredEntity: - pred_labels = [torch.tensor(out.top_labels[0][0], dtype=torch.long) for out in outputs] - pred_scores = [torch.tensor(out.top_labels[0][2]) for out in outputs] - - return ActionClsBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=pred_scores, - labels=pred_labels, - ) - - @property - def model_adapter_parameters(self) -> dict: - """Model parameters for export.""" - return {"input_layouts": "?NCTHW"} diff --git a/src/otx/core/model/entity/action_detection.py b/src/otx/core/model/entity/action_detection.py deleted file mode 100644 index ee244b0f778..00000000000 --- a/src/otx/core/model/entity/action_detection.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for action_detection model entity used in OTX.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from torchvision import tv_tensors - -from otx.core.data.entity.action_detection import ActionDetBatchDataEntity, ActionDetBatchPredEntity -from otx.core.data.entity.base import OTXBatchLossEntity, T_OTXBatchPredEntityWithXAI -from otx.core.data.entity.tile import T_OTXTileBatchDataEntity -from otx.core.model.entity.base import OTXModel -from otx.core.utils.config import inplace_num_classes - -if TYPE_CHECKING: - from omegaconf import DictConfig - from torch import nn - - -class OTXActionDetModel( - OTXModel[ActionDetBatchDataEntity, ActionDetBatchPredEntity, T_OTXBatchPredEntityWithXAI, T_OTXTileBatchDataEntity], -): - """Base class for the action detection models used in OTX.""" - - -class MMActionCompatibleModel(OTXActionDetModel): - """Action detection model compitible for MMAction. - - It can consume MMAction model configuration translated into OTX configuration - (please see otx.tools.translate_mmrecipe) and create the OTX Action classification model - compatible for OTX pipelines. - """ - - def __init__(self, num_classes: int, config: DictConfig) -> None: - config = inplace_num_classes(cfg=config, num_classes=num_classes) - self.config = config - self.load_from = config.pop("load_from", None) - super().__init__(num_classes=num_classes) - - def _create_model(self) -> nn.Module: - from .utils.mmaction import create_model - - model, self.classification_layers = create_model(self.config, self.load_from) - return model - - def _customize_inputs(self, entity: ActionDetBatchDataEntity) -> dict[str, Any]: - """Convert ActionClsBatchDataEntity into mmaction model's input.""" - from mmaction.structures import ActionDataSample - from mmengine.structures import InstanceData - - mmaction_inputs: dict[str, Any] = {} - - mmaction_inputs["inputs"] = entity.images - entity_proposals = entity.proposals if entity.proposals else [None] * len(entity.bboxes) - mmaction_inputs["data_samples"] = [ - ActionDataSample( - metainfo={ - "img_id": img_info.img_idx, - "img_shape": img_info.img_shape, - "ori_shape": img_info.ori_shape, - "pad_shape": img_info.pad_shape, - "scale_factor": img_info.scale_factor, - }, - gt_instances=InstanceData( - bboxes=bboxes, - labels=labels, - ), - proposals=InstanceData(bboxes=proposals) if proposals is not None else None, - ) - for img_info, bboxes, labels, proposals in zip( - entity.imgs_info, - entity.bboxes, - entity.labels, - entity_proposals, - ) - ] - - mmaction_inputs = self.model.data_preprocessor(data=mmaction_inputs, training=self.training) - mmaction_inputs["mode"] = "loss" if self.training else "predict" - return mmaction_inputs - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: ActionDetBatchDataEntity, - ) -> ActionDetBatchPredEntity | OTXBatchLossEntity: - from mmaction.structures import ActionDataSample - - if self.training: - if not isinstance(outputs, dict): - raise TypeError(outputs) - - losses = OTXBatchLossEntity() - for k, v in outputs.items(): - losses[k] = v - return losses - - scores = [] - labels = [] - bboxes = [] - proposals = [] - - for output in outputs: - if not isinstance(output, ActionDataSample): - raise TypeError(output) - - output_scores, output_labels = output.pred_instances.scores.max(-1) - output.pred_instances.bboxes[:, 0::2] *= output.img_shape[1] - output.pred_instances.bboxes[:, 1::2] *= output.img_shape[0] - scores.append(output_scores) - bboxes.append( - tv_tensors.BoundingBoxes( - output.pred_instances.bboxes, - format="XYXY", - canvas_size=output.img_shape, - ), - ) - labels.append(output_labels) - proposals.append(output.proposals.bboxes) - - return ActionDetBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - bboxes=bboxes, - labels=labels, - proposals=proposals, - ) diff --git a/src/otx/core/model/entity/base.py b/src/otx/core/model/entity/base.py deleted file mode 100644 index 113bdc38edc..00000000000 --- a/src/otx/core/model/entity/base.py +++ /dev/null @@ -1,525 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for base model entity used in OTX.""" - -from __future__ import annotations - -import contextlib -import json -import warnings -from abc import abstractmethod -from typing import TYPE_CHECKING, Any, Callable, Generic, NamedTuple - -import numpy as np -import openvino -from jsonargparse import ArgumentParser -from openvino.model_api.models import Model -from torch import nn - -from otx.core.data.dataset.base import LabelInfo -from otx.core.data.entity.base import ( - OTXBatchLossEntity, - T_OTXBatchDataEntity, - T_OTXBatchPredEntity, - T_OTXBatchPredEntityWithXAI, -) -from otx.core.data.entity.tile import OTXTileBatchDataEntity, T_OTXTileBatchDataEntity -from otx.core.exporter.base import OTXModelExporter -from otx.core.types.export import OTXExportFormatType -from otx.core.types.precision import OTXPrecisionType -from otx.core.utils.build import get_default_num_async_infer_requests - -if TYPE_CHECKING: - from pathlib import Path - - import torch - from lightning import Trainer - - from otx.core.data.module import OTXDataModule - - -class OTXModel( - nn.Module, - Generic[T_OTXBatchDataEntity, T_OTXBatchPredEntity, T_OTXBatchPredEntityWithXAI, T_OTXTileBatchDataEntity], -): - """Base class for the models used in OTX. - - Args: - num_classes: Number of classes this model can predict. - """ - - _OPTIMIZED_MODEL_BASE_NAME: str = "optimized_model" - - def __init__(self, num_classes: int) -> None: - super().__init__() - - self._label_info = LabelInfo.from_num_classes(num_classes) - self.classification_layers: dict[str, dict[str, Any]] = {} - self.model = self._create_model() - self.original_model_forward = None - self._explain_mode = False - - def setup_callback(self, trainer: Trainer) -> None: - """Callback for setup OTX Model. - - Args: - trainer(Trainer): Lightning trainer contains OTXLitModule and OTXDatamodule. - """ - - @property - def label_info(self) -> LabelInfo: - """Get this model label information.""" - return self._label_info - - @label_info.setter - def label_info(self, label_info: LabelInfo | list[str]) -> None: - """Set this model label information.""" - if isinstance(label_info, list): - label_info = LabelInfo(label_names=label_info, label_groups=[label_info]) - - old_num_classes = self._label_info.num_classes - new_num_classes = label_info.num_classes - - if old_num_classes != new_num_classes: - msg = ( - f"Given LabelInfo has the different number of classes " - f"({old_num_classes}!={new_num_classes}). " - "The model prediction layer is reset to the new number of classes " - f"(={new_num_classes})." - ) - warnings.warn(msg, stacklevel=0) - self._reset_prediction_layer(num_classes=label_info.num_classes) - - self._label_info = label_info - - @property - def num_classes(self) -> int: - """Returns model's number of classes. Can be redefined at the model's level.""" - return self.label_info.num_classes - - @property - def explain_mode(self) -> bool: - """Get model explain mode.""" - return self._explain_mode - - @explain_mode.setter - def explain_mode(self, explain_mode: bool) -> None: - """Set model explain mode.""" - self._explain_mode = explain_mode - - @abstractmethod - def _create_model(self) -> nn.Module: - """Create a PyTorch model for this class.""" - - def _customize_inputs(self, inputs: T_OTXBatchDataEntity) -> dict[str, Any]: - """Customize OTX input batch data entity if needed for your model.""" - raise NotImplementedError - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: T_OTXBatchDataEntity, - ) -> T_OTXBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Customize OTX output batch data entity if needed for model.""" - raise NotImplementedError - - def forward( - self, - inputs: T_OTXBatchDataEntity, - ) -> T_OTXBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Model forward function.""" - # If customize_inputs is overridden - if isinstance(inputs, OTXTileBatchDataEntity): - return self.forward_tiles(inputs) - - outputs = ( - self.model(**self._customize_inputs(inputs)) - if self._customize_inputs != OTXModel._customize_inputs - else self.model(inputs) - ) - - return ( - self._customize_outputs(outputs, inputs) - if self._customize_outputs != OTXModel._customize_outputs - else outputs - ) - - def forward_explain( - self, - inputs: T_OTXBatchDataEntity, - ) -> T_OTXBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Model forward explain function.""" - raise NotImplementedError - - def get_explain_fn(self) -> Callable: - """Returns explain function.""" - raise NotImplementedError - - def _reset_model_forward(self) -> None: - pass - - def _restore_model_forward(self) -> None: - pass - - def forward_tiles( - self, - inputs: T_OTXTileBatchDataEntity, - ) -> T_OTXBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Model forward function for tile task.""" - raise NotImplementedError - - def register_load_state_dict_pre_hook(self, model_classes: list[str], ckpt_classes: list[str]) -> None: - """Register load_state_dict_pre_hook. - - Args: - model_classes (list[str]): Class names from training data. - ckpt_classes (list[str]): Class names from checkpoint state dictionary. - """ - self.model_classes = model_classes - self.ckpt_classes = ckpt_classes - self._register_load_state_dict_pre_hook(self.load_state_dict_pre_hook) - - def load_state_dict_pre_hook(self, state_dict: dict[str, torch.Tensor], prefix: str, *args, **kwargs) -> None: - """Modify input state_dict according to class name matching before weight loading.""" - model2ckpt = self.map_class_names(self.model_classes, self.ckpt_classes) - - for param_name, info in self.classification_layers.items(): - model_param = self.state_dict()[param_name].clone() - ckpt_param = state_dict[prefix + param_name] - stride = info.get("stride", 1) - num_extra_classes = info.get("num_extra_classes", 0) - for model_dst, ckpt_dst in enumerate(model2ckpt): - if ckpt_dst >= 0: - model_param[(model_dst) * stride : (model_dst + 1) * stride].copy_( - ckpt_param[(ckpt_dst) * stride : (ckpt_dst + 1) * stride], - ) - if num_extra_classes > 0: - num_ckpt_class = len(self.ckpt_classes) - num_model_class = len(self.model_classes) - model_param[(num_model_class) * stride : (num_model_class + 1) * stride].copy_( - ckpt_param[(num_ckpt_class) * stride : (num_ckpt_class + 1) * stride], - ) - - # Replace checkpoint weight by mixed weights - state_dict[prefix + param_name] = model_param - - @staticmethod - def map_class_names(src_classes: list[str], dst_classes: list[str]) -> list[int]: - """Computes src to dst index mapping. - - src2dst[src_idx] = dst_idx - # according to class name matching, -1 for non-matched ones - assert(len(src2dst) == len(src_classes)) - ex) - src_classes = ['person', 'car', 'tree'] - dst_classes = ['tree', 'person', 'sky', 'ball'] - -> Returns src2dst = [1, -1, 0] - """ - src2dst = [] - for src_class in src_classes: - if src_class in dst_classes: - src2dst.append(dst_classes.index(src_class)) - else: - src2dst.append(-1) - return src2dst - - def optimize(self, output_dir: Path, data_module: OTXDataModule, ptq_config: dict[str, Any] | None = None) -> Path: - """Runs quantization of the model with NNCF.PTQ on the passed data. Works only for OpenVINO models. - - PTQ performs int-8 quantization on the input model, so the resulting model - comes in mixed precision (some operations, however, remain in FP32). - - Args: - output_dir (Path): working directory to save the optimized model. - data_module (OTXDataModule): dataset for calibration of quantized layers. - ptq_config (dict[str, Any] | None): config for NNCF.PTQ. - - Returns: - Path: path to the resulting optimized OpenVINO model. - """ - msg = "Optimization is not implemented for torch models" - raise NotImplementedError(msg) - - def export( - self, - output_dir: Path, - base_name: str, - export_format: OTXExportFormatType, - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> Path: - """Export this model to the specified output directory. - - Args: - output_dir (Path): directory for saving the exported model - base_name: (str): base name for the exported model file. Extension is defined by the target export format - export_format (OTXExportFormatType): format of the output model - precision (OTXExportPrecisionType): precision of the output model - - Returns: - Path: path to the exported model. - """ - self._reset_model_forward() - exported_model_path = self._exporter.export( - self.model, - output_dir, - base_name, - export_format, - precision, - ) - self._restore_model_forward() - return exported_model_path - - @property - def _exporter(self) -> OTXModelExporter: - msg = ( - "To export this OTXModel, you should implement an appropriate exporter for it. " - "You can try to reuse ones provided in `otx.core.exporter.*`." - ) - raise NotImplementedError(msg) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation. - - To export OTXModel, you should define an appropriate parameters." - "This is used in the constructor of `self._exporter`. " - "For example, `self._exporter = SomeExporter(**self.export_parameters)`. " - "Please refer to `otx.core.exporter.*` for detailed examples." - Returns: - dict[str, Any]: parameters of exporter. - """ - parameters = {} - all_labels = "" - all_label_ids = "" - for lbl in self.label_info.label_names: - all_labels += lbl.replace(" ", "_") + " " - all_label_ids += lbl.replace(" ", "_") + " " - - # not every model requires ptq_config - optimization_config = self._optimization_config - parameters["metadata"] = { - ("model_info", "labels"): all_labels.strip(), - ("model_info", "label_ids"): all_label_ids.strip(), - ("model_info", "optimization_config"): json.dumps(optimization_config), - } - - return parameters - - def _reset_prediction_layer(self, num_classes: int) -> None: - """Reset its prediction layer with a given number of classes. - - Args: - num_classes: Number of classes - """ - raise NotImplementedError - - @property - def _optimization_config(self) -> dict[str, str]: - return {} - - -class OVModel(OTXModel, Generic[T_OTXBatchDataEntity, T_OTXBatchPredEntity, T_OTXBatchPredEntityWithXAI]): - """Base class for the OpenVINO model. - - This is a base class representing interface for interacting with OpenVINO - Intermediate Representation (IR) models. OVModel can create and validate - OpenVINO IR model directly from provided path locally or from - OpenVINO OMZ repository. (Only PyTorch models are supported). - OVModel supports synchronous as well as asynchronous inference type. - - Args: - num_classes: Number of classes this model can predict. - """ - - def __init__( - self, - num_classes: int, - model_name: str, - model_type: str, - async_inference: bool = True, - max_num_requests: int | None = None, - use_throughput_mode: bool = True, - model_api_configuration: dict[str, Any] | None = None, - ) -> None: - self.model_name = model_name - self.model_type = model_type - self.async_inference = async_inference - self.num_requests = max_num_requests if max_num_requests is not None else get_default_num_async_infer_requests() - self.use_throughput_mode = use_throughput_mode - self.model_api_configuration = model_api_configuration if model_api_configuration is not None else {} - super().__init__(num_classes) - - tile_enabled = False - with contextlib.suppress(RuntimeError): - if isinstance(self.model, Model): - tile_enabled = "tile_size" in self.model.inference_adapter.get_rt_info(["model_info"]).astype(dict) - - if tile_enabled: - self._setup_tiler() - - def _setup_tiler(self) -> None: - """Setup tiler for tile task.""" - raise NotImplementedError - - def _create_model(self) -> Model: - """Create a OV model with help of Model API.""" - from openvino.model_api.adapters import OpenvinoAdapter, create_core, get_user_config - - plugin_config = get_user_config("AUTO", str(self.num_requests), "AUTO") - if self.use_throughput_mode: - plugin_config["PERFORMANCE_HINT"] = "THROUGHPUT" - - model_adapter = OpenvinoAdapter( - create_core(), - self.model_name, - max_num_requests=self.num_requests, - plugin_config=plugin_config, - model_parameters=self.model_adapter_parameters, - ) - return Model.create_model(model_adapter, model_type=self.model_type, configuration=self.model_api_configuration) - - def _customize_inputs(self, entity: T_OTXBatchDataEntity) -> dict[str, Any]: - # restore original numpy image - images = [np.transpose(im.cpu().numpy(), (1, 2, 0)) for im in entity.images] - return {"inputs": images} - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: T_OTXBatchDataEntity, - ) -> T_OTXBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Customize OTX output batch data entity if needed for model.""" - raise NotImplementedError - - def forward( - self, - inputs: T_OTXBatchDataEntity, - ) -> T_OTXBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Model forward function.""" - - def _callback(result: NamedTuple, idx: int) -> None: - output_dict[idx] = result - - numpy_inputs = self._customize_inputs(inputs)["inputs"] - if self.async_inference: - output_dict: dict[int, NamedTuple] = {} - self.model.set_callback(_callback) - for idx, im in enumerate(numpy_inputs): - if not self.model.is_ready(): - self.model.await_any() - self.model.infer_async(im, user_data=idx) - self.model.await_all() - outputs = [out[1] for out in sorted(output_dict.items())] - else: - outputs = [self.model(im) for im in numpy_inputs] - - return self._customize_outputs(outputs, inputs) - - def optimize( - self, - output_dir: Path, - data_module: OTXDataModule, - ptq_config: dict[str, Any] | None = None, - ) -> Path: - """Runs NNCF quantization.""" - import nncf - - output_model_path = output_dir / (self._OPTIMIZED_MODEL_BASE_NAME + ".xml") - - def check_if_quantized(model: openvino.Model) -> bool: - """Checks if OpenVINO model is already quantized.""" - nodes = model.get_ops() - return any(op.get_type_name() == "FakeQuantize" for op in nodes) - - ov_model = openvino.Core().read_model(self.model_name) - - if check_if_quantized(ov_model): - msg = "Model is already optimized by PTQ" - raise RuntimeError(msg) - - def transform_fn(data_batch: T_OTXBatchDataEntity) -> np.array: - np_data = self._customize_inputs(data_batch) - image = np_data["inputs"][0] - resized_image = self.model.resize(image, (self.model.w, self.model.h)) - resized_image = self.model.input_transform(resized_image) - return self.model._change_layout(resized_image) # noqa: SLF001 - - train_dataset = data_module.train_dataloader() - - ptq_config_from_ir = self._read_ptq_config_from_ir(ov_model) - if ptq_config is not None: - ptq_config_from_ir.update(ptq_config) - ptq_config = ptq_config_from_ir - else: - ptq_config = ptq_config_from_ir - - quantization_dataset = nncf.Dataset(train_dataset, transform_fn) # type: ignore[attr-defined] - - compressed_model = nncf.quantize( # type: ignore[attr-defined] - ov_model, - quantization_dataset, - **ptq_config, - ) - - openvino.save_model(compressed_model, output_model_path) - - return output_model_path - - def _read_ptq_config_from_ir(self, ov_model: Model) -> dict[str, Any]: - """Generates the PTQ (Post-Training Quantization) configuration from the meta data of the given OpenVINO model. - - Args: - ov_model (Model): The OpenVINO model in which the PTQ configuration is embedded. - - Returns: - dict: The PTQ configuration as a dictionary. - """ - from nncf import IgnoredScope # type: ignore[attr-defined] - from nncf.common.quantization.structs import QuantizationPreset # type: ignore[attr-defined] - from nncf.parameters import ModelType - from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters - - if "optimization_config" not in ov_model.rt_info["model_info"]: - return {} - - initial_ptq_config = json.loads(ov_model.rt_info["model_info"]["optimization_config"].value) - if not initial_ptq_config: - return {} - argparser = ArgumentParser() - if "advanced_parameters" in initial_ptq_config: - argparser.add_class_arguments(AdvancedQuantizationParameters, "advanced_parameters") - if "preset" in initial_ptq_config: - initial_ptq_config["preset"] = QuantizationPreset(initial_ptq_config["preset"]) - argparser.add_argument("--preset", type=QuantizationPreset) - if "model_type" in initial_ptq_config: - initial_ptq_config["model_type"] = ModelType(initial_ptq_config["model_type"]) - argparser.add_argument("--model_type", type=ModelType) - if "ignored_scope" in initial_ptq_config: - argparser.add_class_arguments(IgnoredScope, "ignored_scope", as_positional=True) - - initial_ptq_config = argparser.parse_object(initial_ptq_config) - - return argparser.instantiate_classes(initial_ptq_config).as_dict() - - def _reset_prediction_layer(self, num_classes: int) -> None: - return - - @property - def model_adapter_parameters(self) -> dict: - """Model parameters for export.""" - return {} - - @property - def label_info(self) -> LabelInfo: - """Get this model label information.""" - return self._label_info - - @label_info.setter - def label_info(self, label_info: LabelInfo | list[str]) -> None: - """Set this model label information.""" - - @property - def num_classes(self) -> int: - """Returns model's number of classes. Can be redefined at the model's level.""" - return self.label_info.num_classes diff --git a/src/otx/core/model/entity/classification.py b/src/otx/core/model/entity/classification.py deleted file mode 100644 index 9cfcce01fed..00000000000 --- a/src/otx/core/model/entity/classification.py +++ /dev/null @@ -1,884 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for classification model entity used in OTX.""" - -from __future__ import annotations - -import json -import types -from typing import TYPE_CHECKING, Any, Callable - -import numpy as np -import torch - -from otx.algo.hooks.recording_forward_hook import feature_vector_fn -from otx.core.data.dataset.classification import HLabelInfo -from otx.core.data.entity.base import ( - OTXBatchLossEntity, - T_OTXBatchDataEntity, - T_OTXBatchPredEntity, - T_OTXBatchPredEntityWithXAI, -) -from otx.core.data.entity.classification import ( - HlabelClsBatchDataEntity, - HlabelClsBatchPredEntity, - HlabelClsBatchPredEntityWithXAI, - MulticlassClsBatchDataEntity, - MulticlassClsBatchPredEntity, - MulticlassClsBatchPredEntityWithXAI, - MultilabelClsBatchDataEntity, - MultilabelClsBatchPredEntity, - MultilabelClsBatchPredEntityWithXAI, -) -from otx.core.data.entity.tile import T_OTXTileBatchDataEntity -from otx.core.exporter.base import OTXModelExporter -from otx.core.exporter.native import OTXNativeModelExporter -from otx.core.model.entity.base import OTXModel, OVModel -from otx.core.utils.config import inplace_num_classes -from otx.core.utils.utils import get_mean_std_from_data_processing - -if TYPE_CHECKING: - from mmpretrain.models import ImageClassifier - from mmpretrain.models.utils import ClsDataPreprocessor - from mmpretrain.structures import DataSample - from omegaconf import DictConfig - from openvino.model_api.models.utils import ClassificationResult - from torch import nn - - -class ExplainableOTXClsModel( - OTXModel[T_OTXBatchDataEntity, T_OTXBatchPredEntity, T_OTXBatchPredEntityWithXAI, T_OTXTileBatchDataEntity], -): - """OTX classification model which can attach a XAI hook.""" - - @property - def has_gap(self) -> bool: - """Defines if GAP is used right after backbone. Can be redefined at the model's level.""" - return True - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - export_params = super()._export_parameters - export_params["output_names"] = ["logits", "feature_vector", "saliency_map"] if self.explain_mode else None - return export_params - - @torch.no_grad() - def head_forward_fn(self, x: torch.Tensor) -> torch.Tensor: - """Performs model's neck and head forward. Can be redefined at the model's level.""" - if (neck := getattr(self.model, "neck", None)) is None: - raise ValueError - if (head := getattr(self.model, "head", None)) is None: - raise ValueError - - output = neck(x) - return head([output]) - - def forward_explain( - self, - inputs: T_OTXBatchDataEntity, - ) -> T_OTXBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Model forward function.""" - self.model.feature_vector_fn = feature_vector_fn - self.model.explain_fn = self.get_explain_fn() - - # If customize_inputs is overridden - outputs = ( - self._forward_explain_image_classifier(self.model, **self._customize_inputs(inputs)) - if self._customize_inputs != ExplainableOTXClsModel._customize_inputs - else self._forward_explain_image_classifier(self.model, inputs) - ) - - return ( - self._customize_outputs(outputs, inputs) - if self._customize_outputs != ExplainableOTXClsModel._customize_outputs - else outputs["predictions"] - ) - - @staticmethod - def _forward_explain_image_classifier( - self: ImageClassifier, - inputs: torch.Tensor, - data_samples: list[DataSample] | None = None, - mode: str = "tensor", - ) -> dict[str, torch.Tensor]: - """Forward func of the ImageClassifier instance, which located in ExplainableOTXClsModel().model.""" - x = self.backbone(inputs) - backbone_feat = x - - feature_vector = self.feature_vector_fn(backbone_feat) - saliency_map = self.explain_fn(backbone_feat) - - if self.with_neck: - x = self.neck(x) - - if mode == "tensor": - logits = self.head(x) if self.with_head else x - elif mode == "predict": - logits = self.head.predict(x, data_samples) - else: - msg = f'Invalid mode "{mode}".' - raise RuntimeError(msg) - - return { - "logits": logits, - "feature_vector": feature_vector, - "saliency_map": saliency_map, - } - - def get_explain_fn(self) -> Callable: - """Returns explain function.""" - from otx.algo.hooks.recording_forward_hook import ReciproCAMHook - - explainer = ReciproCAMHook( - self.head_forward_fn, - num_classes=self.num_classes, - optimize_gap=self.has_gap, - ) - return explainer.func - - def _reset_model_forward(self) -> None: - if not self.explain_mode: - return - - self.model.feature_vector_fn = feature_vector_fn - self.model.explain_fn = self.get_explain_fn() - forward_with_explain = self._forward_explain_image_classifier - - self.original_model_forward = self.model.forward - - func_type = types.MethodType - self.model.forward = func_type(forward_with_explain, self.model) - - def _restore_model_forward(self) -> None: - if not self.explain_mode: - return - - if not self.original_model_forward: - msg = "Original model forward was not saved." - raise RuntimeError(msg) - - func_type = types.MethodType - self.model.forward = func_type(self.original_model_forward, self.model) - self.original_model_forward = None - - @property - def _exporter(self) -> OTXModelExporter: - """Creates OTXModelExporter object that can export the model.""" - return OTXNativeModelExporter(**self._export_parameters) - - -class OTXMulticlassClsModel( - ExplainableOTXClsModel[ - MulticlassClsBatchDataEntity, - MulticlassClsBatchPredEntity, - MulticlassClsBatchPredEntityWithXAI, - T_OTXTileBatchDataEntity, - ], -): - """Base class for the classification models used in OTX.""" - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parameters = super()._export_parameters - parameters["metadata"].update( - { - ("model_info", "model_type"): "Classification", - ("model_info", "task_type"): "classification", - ("model_info", "multilabel"): str(False), - ("model_info", "hierarchical"): str(False), - }, - ) - return parameters - - -class MMPretrainMulticlassClsModel(OTXMulticlassClsModel): - """Multi-class Classification model compatible for MMPretrain. - - It can consume MMPretrain model configuration translated into OTX configuration - (please see otx.tools.translate_mmrecipe) and create the OTX classification model - compatible for OTX pipelines. - """ - - def __init__(self, num_classes: int, config: DictConfig) -> None: - config = inplace_num_classes(cfg=config, num_classes=num_classes) - self.config = config - self.load_from = config.pop("load_from", None) - self.image_size = (1, 3, 224, 224) - super().__init__(num_classes=num_classes) - - def _create_model(self) -> nn.Module: - from .utils.mmpretrain import create_model - - model, self.classification_layers = create_model(self.config, self.load_from) - return model - - def _customize_inputs(self, entity: MulticlassClsBatchDataEntity) -> dict[str, Any]: - from mmpretrain.structures import DataSample - - mmpretrain_inputs: dict[str, Any] = {} - - mmpretrain_inputs["inputs"] = entity.images # B x C x H x W PyTorch tensor - mmpretrain_inputs["data_samples"] = [ - DataSample( - metainfo={ - "img_id": img_info.img_idx, - "img_shape": img_info.img_shape, - "ori_shape": img_info.ori_shape, - "pad_shape": img_info.pad_shape, - "scale_factor": img_info.scale_factor, - }, - gt_label=labels, - ) - for img_info, labels in zip( - entity.imgs_info, - entity.labels, - ) - ] - preprocessor: ClsDataPreprocessor = self.model.data_preprocessor - - mmpretrain_inputs = preprocessor(data=mmpretrain_inputs, training=self.training) - - mmpretrain_inputs["mode"] = "loss" if self.training else "predict" - return mmpretrain_inputs - - def _customize_outputs( - self, - outputs: dict[str, Any], - inputs: MulticlassClsBatchDataEntity, - ) -> MulticlassClsBatchPredEntity | MulticlassClsBatchPredEntityWithXAI | OTXBatchLossEntity: - from mmpretrain.structures import DataSample - - if self.training: - if not isinstance(outputs, dict): - raise TypeError(outputs) - - losses = OTXBatchLossEntity() - for k, v in outputs.items(): - losses[k] = v - return losses - - predictions = outputs["logits"] if isinstance(outputs, dict) else outputs - scores = [] - labels = [] - - for output in predictions: - if not isinstance(output, DataSample): - raise TypeError(output) - - scores.append(output.pred_score) - labels.append(output.pred_label) - - if self.explain_mode: - if not isinstance(outputs, dict): - msg = f"Model output should be a dict, but got {type(outputs)}." - raise ValueError(msg) - - if "feature_vector" not in outputs: - msg = "No feature vector in the model output." - raise ValueError(msg) - - if "saliency_map" not in outputs: - msg = "No saliency maps in the model output." - raise ValueError(msg) - - feature_vectors = outputs["feature_vector"].detach().cpu().numpy() - saliency_maps = outputs["saliency_map"].detach().cpu().numpy() - - return MulticlassClsBatchPredEntityWithXAI( - batch_size=len(predictions), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - labels=labels, - feature_vectors=list(feature_vectors), - saliency_maps=list(saliency_maps), - ) - - return MulticlassClsBatchPredEntity( - batch_size=len(predictions), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - labels=labels, - ) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - export_params = super()._export_parameters - export_params.update(get_mean_std_from_data_processing(self.config)) - export_params["resize_mode"] = "standard" - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - export_params["via_onnx"] = False - export_params["input_size"] = self.image_size - export_params["onnx_export_configuration"] = None - - return export_params - - -### NOTE, currently, although we've made the separate Multi-cls, Multi-label classes -### It'll be integrated after H-label classification integration with more advanced design. - - -class OTXMultilabelClsModel( - ExplainableOTXClsModel[ - MultilabelClsBatchDataEntity, - MultilabelClsBatchPredEntity, - MultilabelClsBatchPredEntityWithXAI, - T_OTXTileBatchDataEntity, - ], -): - """Multi-label classification models used in OTX.""" - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parameters = super()._export_parameters - parameters["metadata"].update( - { - ("model_info", "model_type"): "Classification", - ("model_info", "task_type"): "classification", - ("model_info", "multilabel"): str(True), - ("model_info", "hierarchical"): str(False), - ("model_info", "confidence_threshold"): str(0.5), - }, - ) - return parameters - - -class MMPretrainMultilabelClsModel(OTXMultilabelClsModel): - """Multi-label Classification model compatible for MMPretrain. - - It can consume MMPretrain model configuration translated into OTX configuration - (please see otx.tools.translate_mmrecipe) and create the OTX classification model - compatible for OTX pipelines. - """ - - def __init__(self, num_classes: int, config: DictConfig) -> None: - config = inplace_num_classes(cfg=config, num_classes=num_classes) - self.config = config - self.load_from = config.pop("load_from", None) - self.image_size = (1, 3, 224, 224) - super().__init__(num_classes=num_classes) - - def _create_model(self) -> nn.Module: - from .utils.mmpretrain import create_model - - model, classification_layers = create_model(self.config, self.load_from) - self.classification_layers = classification_layers - return model - - def _customize_inputs(self, entity: MultilabelClsBatchDataEntity) -> dict[str, Any]: - from mmpretrain.structures import DataSample - - mmpretrain_inputs: dict[str, Any] = {} - - mmpretrain_inputs["inputs"] = entity.images # B x C x H x W PyTorch tensor - mmpretrain_inputs["data_samples"] = [ - DataSample( - metainfo={ - "img_id": img_info.img_idx, - "img_shape": img_info.img_shape, - "ori_shape": img_info.ori_shape, - "pad_shape": img_info.pad_shape, - "scale_factor": img_info.scale_factor, - "ignored_labels": img_info.ignored_labels, - }, - gt_score=labels, - ) - for img_info, labels in zip( - entity.imgs_info, - entity.labels, - ) - ] - preprocessor: ClsDataPreprocessor = self.model.data_preprocessor - - mmpretrain_inputs = preprocessor(data=mmpretrain_inputs, training=self.training) - - mmpretrain_inputs["mode"] = "loss" if self.training else "predict" - return mmpretrain_inputs - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: MultilabelClsBatchDataEntity, - ) -> MultilabelClsBatchPredEntity | MultilabelClsBatchPredEntityWithXAI | OTXBatchLossEntity: - from mmpretrain.structures import DataSample - - if self.training: - if not isinstance(outputs, dict): - raise TypeError(outputs) - - losses = OTXBatchLossEntity() - for k, v in outputs.items(): - losses[k] = v - return losses - - predictions = outputs["logits"] if isinstance(outputs, dict) else outputs - scores = [] - labels = [] - - for output in predictions: - if not isinstance(output, DataSample): - raise TypeError(output) - - scores.append(output.pred_score) - labels.append(output.pred_label) - - if self.explain_mode: - if not isinstance(outputs, dict): - msg = f"Model output should be a dict, but got {type(outputs)}." - raise ValueError(msg) - - if "feature_vector" not in outputs: - msg = "No feature vector in the model output." - raise ValueError(msg) - - if "saliency_map" not in outputs: - msg = "No saliency maps in the model output." - raise ValueError(msg) - - feature_vectors = outputs["feature_vector"].detach().cpu().numpy() - saliency_maps = outputs["saliency_map"].detach().cpu().numpy() - - return MultilabelClsBatchPredEntityWithXAI( - batch_size=len(predictions), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - labels=labels, - feature_vectors=list(feature_vectors), - saliency_maps=list(saliency_maps), - ) - - return MultilabelClsBatchPredEntity( - batch_size=len(predictions), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - labels=labels, - ) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - export_params = super()._export_parameters - export_params.update(get_mean_std_from_data_processing(self.config)) - export_params["resize_mode"] = "standard" - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - export_params["via_onnx"] = False - export_params["input_size"] = self.image_size - export_params["onnx_export_configuration"] = None - - return export_params - - -class OTXHlabelClsModel( - ExplainableOTXClsModel[ - HlabelClsBatchDataEntity, - HlabelClsBatchPredEntity, - HlabelClsBatchPredEntityWithXAI, - T_OTXTileBatchDataEntity, - ], -): - """H-label classification models used in OTX.""" - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parameters = super()._export_parameters - hierarchical_config: dict = {} - - label_info: HLabelInfo = self.label_info # type: ignore[assignment] - hierarchical_config["cls_heads_info"] = { - "num_multiclass_heads": label_info.num_multiclass_heads, - "num_multilabel_classes": label_info.num_multilabel_classes, - "head_idx_to_logits_range": label_info.head_idx_to_logits_range, - "num_single_label_classes": label_info.num_single_label_classes, - "class_to_group_idx": label_info.class_to_group_idx, - "all_groups": label_info.all_groups, - "label_to_idx": label_info.label_to_idx, - "empty_multiclass_head_indices": label_info.empty_multiclass_head_indices, - } - hierarchical_config["label_tree_edges"] = label_info.label_tree_edges - - parameters["metadata"].update( - { - ("model_info", "model_type"): "Classification", - ("model_info", "task_type"): "classification", - ("model_info", "multilabel"): str(False), - ("model_info", "hierarchical"): str(True), - ("model_info", "confidence_threshold"): str(0.5), - ("model_info", "hierarchical_config"): json.dumps(hierarchical_config), - }, - ) - return parameters - - -class MMPretrainHlabelClsModel(OTXHlabelClsModel): - """H-label Classification model compatible for MMPretrain. - - It can consume MMPretrain model configuration translated into OTX configuration - (please see otx.tools.translate_mmrecipe) and create the OTX classification model - compatible for OTX pipelines. - """ - - def __init__(self, num_classes: int, config: DictConfig) -> None: - config = inplace_num_classes(cfg=config, num_classes=num_classes) - self.config = config - self.load_from = config.pop("load_from", None) - self.image_size = (1, 3, 224, 224) - super().__init__(num_classes=num_classes) - - def _create_model(self) -> nn.Module: - from .utils.mmpretrain import create_model - - model, classification_layers = create_model(self.config, self.load_from) - self.classification_layers = classification_layers - return model - - def set_hlabel_info(self, hierarchical_info: HLabelInfo) -> None: - """Set hierarchical information in model head. - - Args: - hierarchical_info: the label information represents the hierarchy. - """ - self.model.head.set_hlabel_info(hierarchical_info) - - def _customize_inputs(self, entity: HlabelClsBatchDataEntity) -> dict[str, Any]: - from mmpretrain.structures import DataSample - - mmpretrain_inputs: dict[str, Any] = {} - - mmpretrain_inputs["inputs"] = entity.images # B x C x H x W PyTorch tensor - mmpretrain_inputs["data_samples"] = [ - DataSample( - metainfo={ - "img_id": img_info.img_idx, - "img_shape": img_info.img_shape, - "ori_shape": img_info.ori_shape, - "pad_shape": img_info.pad_shape, - "scale_factor": img_info.scale_factor, - "ignored_labels": img_info.ignored_labels, - }, - gt_label=labels, - ) - for img_info, labels in zip( - entity.imgs_info, - entity.labels, - ) - ] - preprocessor: ClsDataPreprocessor = self.model.data_preprocessor - - mmpretrain_inputs = preprocessor(data=mmpretrain_inputs, training=self.training) - - mmpretrain_inputs["mode"] = "loss" if self.training else "predict" - return mmpretrain_inputs - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: HlabelClsBatchDataEntity, - ) -> HlabelClsBatchPredEntity | HlabelClsBatchPredEntityWithXAI | OTXBatchLossEntity: - from mmpretrain.structures import DataSample - - if self.training: - if not isinstance(outputs, dict): - raise TypeError(outputs) - - losses = OTXBatchLossEntity() - for k, v in outputs.items(): - losses[k] = v - return losses - - predictions = outputs["logits"] if isinstance(outputs, dict) else outputs - scores = [] - labels = [] - - for output in predictions: - if not isinstance(output, DataSample): - raise TypeError(output) - - scores.append(output.pred_score) - labels.append(output.pred_label) - - if self.explain_mode: - if not isinstance(outputs, dict): - msg = f"Model output should be a dict, but got {type(outputs)}." - raise ValueError(msg) - - if "feature_vector" not in outputs: - msg = "No feature vector in the model output." - raise ValueError(msg) - - if "saliency_map" not in outputs: - msg = "No saliency maps in the model output." - raise ValueError(msg) - - feature_vectors = outputs["feature_vector"].detach().cpu().numpy() - saliency_maps = outputs["saliency_map"].detach().cpu().numpy() - - return HlabelClsBatchPredEntityWithXAI( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - labels=labels, - feature_vectors=list(feature_vectors), - saliency_maps=list(saliency_maps), - ) - - return HlabelClsBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - labels=labels, - ) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - export_params = super()._export_parameters - export_params.update(get_mean_std_from_data_processing(self.config)) - export_params["resize_mode"] = "standard" - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - export_params["via_onnx"] = False - export_params["input_size"] = self.image_size - export_params["onnx_export_configuration"] = None - - return export_params - - -class OVMulticlassClassificationModel( - OVModel[MulticlassClsBatchDataEntity, MulticlassClsBatchPredEntity, MulticlassClsBatchPredEntityWithXAI], -): - """Classification model compatible for OpenVINO IR inference. - - It can consume OpenVINO IR model path or model name from Intel OMZ repository - and create the OTX classification model compatible for OTX testing pipeline. - """ - - def __init__( - self, - num_classes: int, - model_name: str, - model_type: str = "Classification", - async_inference: bool = True, - max_num_requests: int | None = None, - use_throughput_mode: bool = False, - model_api_configuration: dict[str, Any] | None = None, - ) -> None: - super().__init__( - num_classes, - model_name, - model_type, - async_inference, - max_num_requests, - use_throughput_mode, - model_api_configuration, - ) - - def _customize_outputs( - self, - outputs: list[ClassificationResult], - inputs: MulticlassClsBatchDataEntity, - ) -> MulticlassClsBatchPredEntity | MulticlassClsBatchPredEntityWithXAI: - pred_labels = [torch.tensor(out.top_labels[0][0], dtype=torch.long) for out in outputs] - pred_scores = [torch.tensor(out.top_labels[0][2]) for out in outputs] - - if outputs and outputs[0].saliency_map.size != 0: - # Squeeze dim 4D => 3D, (1, num_classes, H, W) => (num_classes, H, W) - predicted_s_maps = [out.saliency_map[0] for out in outputs] - - # Squeeze dim 2D => 1D, (1, internal_dim) => (internal_dim) - predicted_f_vectors = [out.feature_vector[0] for out in outputs] - return MulticlassClsBatchPredEntityWithXAI( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=pred_scores, - labels=pred_labels, - saliency_maps=predicted_s_maps, - feature_vectors=predicted_f_vectors, - ) - - return MulticlassClsBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=pred_scores, - labels=pred_labels, - ) - - -class OVHlabelClassificationModel( - OVModel[HlabelClsBatchDataEntity, HlabelClsBatchPredEntity, HlabelClsBatchPredEntityWithXAI], -): - """Hierarchical classification model compatible for OpenVINO IR inference. - - It can consume OpenVINO IR model path or model name from Intel OMZ repository - and create the OTX classification model compatible for OTX testing pipeline. - """ - - def __init__( - self, - num_classes: int, - model_name: str, - model_type: str = "Classification", - async_inference: bool = True, - max_num_requests: int | None = None, - use_throughput_mode: bool = True, - model_api_configuration: dict[str, Any] | None = None, - num_multiclass_heads: int = 1, - num_multilabel_classes: int = 0, - ) -> None: - self.num_multiclass_heads = num_multiclass_heads - self.num_multilabel_classes = num_multilabel_classes - model_api_configuration = model_api_configuration if model_api_configuration else {} - model_api_configuration.update({"hierarchical": True, "output_raw_scores": True}) - super().__init__( - num_classes, - model_name, - model_type, - async_inference, - max_num_requests, - use_throughput_mode, - model_api_configuration, - ) - - def set_hlabel_info(self, hierarchical_info: HLabelInfo) -> None: - """Set hierarchical information in model head. - - Since OV IR model consist of all required hierarchy information, - this method serves as placeholder - """ - if not hasattr(self.model, "hierarchical_info") or not self.model.hierarchical_info: - msg = "OpenVINO IR model should have hierarchical config embedded in rt_info of the model" - raise ValueError(msg) - - def _customize_outputs( - self, - outputs: list[ClassificationResult], - inputs: HlabelClsBatchDataEntity, - ) -> HlabelClsBatchPredEntity | HlabelClsBatchPredEntityWithXAI: - all_pred_labels = [] - all_pred_scores = [] - for output in outputs: - logits = output.raw_scores - predicted_labels = [] - predicted_scores = [] - cls_heads_info = self.model.hierarchical_info["cls_heads_info"] - for i in range(cls_heads_info["num_multiclass_heads"]): - logits_begin, logits_end = cls_heads_info["head_idx_to_logits_range"][str(i)] - head_logits = logits[logits_begin:logits_end] - j = np.argmax(head_logits) - predicted_labels.append(j) - predicted_scores.append(head_logits[j]) - - if cls_heads_info["num_multilabel_classes"]: - logits_begin = cls_heads_info["num_single_label_classes"] - head_logits = logits[logits_begin:] - - for i in range(head_logits.shape[0]): - predicted_scores.append(head_logits[i]) - if head_logits[i] > self.model.confidence_threshold: - predicted_labels.append(1) - else: - predicted_labels.append(0) - - all_pred_labels.append(torch.tensor(predicted_labels, dtype=torch.long)) - all_pred_scores.append(torch.tensor(predicted_scores)) - - if outputs and outputs[0].saliency_map.size != 0: - # Squeeze dim 4D => 3D, (1, num_classes, H, W) => (num_classes, H, W) - predicted_s_maps = [out.saliency_map[0] for out in outputs] - - # Squeeze dim 2D => 1D, (1, internal_dim) => (internal_dim) - predicted_f_vectors = [out.feature_vector[0] for out in outputs] - return HlabelClsBatchPredEntityWithXAI( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=all_pred_scores, - labels=all_pred_labels, - saliency_maps=predicted_s_maps, - feature_vectors=predicted_f_vectors, - ) - - return HlabelClsBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=all_pred_scores, - labels=all_pred_labels, - ) - - -class OVMultilabelClassificationModel( - OVModel[MultilabelClsBatchDataEntity, MultilabelClsBatchPredEntity, MultilabelClsBatchPredEntityWithXAI], -): - """Multilabel classification model compatible for OpenVINO IR inference. - - It can consume OpenVINO IR model path or model name from Intel OMZ repository - and create the OTX classification model compatible for OTX testing pipeline. - """ - - def __init__( - self, - num_classes: int, - model_name: str, - model_type: str = "Classification", - async_inference: bool = True, - max_num_requests: int | None = None, - use_throughput_mode: bool = True, - model_api_configuration: dict[str, Any] | None = None, - ) -> None: - model_api_configuration = model_api_configuration if model_api_configuration else {} - model_api_configuration.update({"multilabel": True, "confidence_threshold": 0.0}) - super().__init__( - num_classes, - model_name, - model_type, - async_inference, - max_num_requests, - use_throughput_mode, - model_api_configuration, - ) - - def _customize_outputs( - self, - outputs: list[ClassificationResult], - inputs: MultilabelClsBatchDataEntity, - ) -> MultilabelClsBatchPredEntity | MultilabelClsBatchPredEntityWithXAI: - pred_scores = [torch.tensor([top_label[2] for top_label in out.top_labels]) for out in outputs] - - if outputs and outputs[0].saliency_map.size != 0: - # Squeeze dim 4D => 3D, (1, num_classes, H, W) => (num_classes, H, W) - predicted_s_maps = [out.saliency_map[0] for out in outputs] - - # Squeeze dim 2D => 1D, (1, internal_dim) => (internal_dim) - predicted_f_vectors = [out.feature_vector[0] for out in outputs] - return MultilabelClsBatchPredEntityWithXAI( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=pred_scores, - labels=[], - saliency_maps=predicted_s_maps, - feature_vectors=predicted_f_vectors, - ) - - return MultilabelClsBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=pred_scores, - labels=[], - ) diff --git a/src/otx/core/model/entity/detection.py b/src/otx/core/model/entity/detection.py deleted file mode 100644 index 3a849a21103..00000000000 --- a/src/otx/core/model/entity/detection.py +++ /dev/null @@ -1,523 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for detection model entity used in OTX.""" - -from __future__ import annotations - -import copy -import json -import logging as log -import types -from typing import TYPE_CHECKING, Any, Callable - -import torch -from openvino.model_api.models import Model -from openvino.model_api.tilers import DetectionTiler -from torchvision import tv_tensors - -from otx.core.config.data import TileConfig -from otx.core.data.entity.base import OTXBatchLossEntity -from otx.core.data.entity.detection import DetBatchDataEntity, DetBatchPredEntity, DetBatchPredEntityWithXAI -from otx.core.data.entity.tile import TileBatchDetDataEntity -from otx.core.exporter.base import OTXModelExporter -from otx.core.model.entity.base import OTXModel, OVModel -from otx.core.utils.config import inplace_num_classes -from otx.core.utils.tile_merge import DetectionTileMerge -from otx.core.utils.utils import get_mean_std_from_data_processing - -if TYPE_CHECKING: - from mmdet.models.data_preprocessors import DetDataPreprocessor - from mmdet.models.detectors import SingleStageDetector - from mmdet.structures import OptSampleList - from omegaconf import DictConfig - from openvino.model_api.models.utils import DetectionResult - from torch import nn - - -class OTXDetectionModel( - OTXModel[DetBatchDataEntity, DetBatchPredEntity, DetBatchPredEntityWithXAI, TileBatchDetDataEntity], -): - """Base class for the detection models used in OTX.""" - - def __init__(self, *arg, **kwargs) -> None: - super().__init__(*arg, **kwargs) - self.tile_config = TileConfig() - self.test_meta_info: dict[str, Any] = {} - - def forward_tiles(self, inputs: TileBatchDetDataEntity) -> DetBatchPredEntity | DetBatchPredEntityWithXAI: - """Unpack detection tiles. - - Args: - inputs (TileBatchDetDataEntity): Tile batch data entity. - - Returns: - DetBatchPredEntity: Merged detection prediction. - """ - tile_preds: list[DetBatchPredEntity | DetBatchPredEntityWithXAI] = [] - tile_attrs: list[list[dict[str, int | str]]] = [] - merger = DetectionTileMerge( - inputs.imgs_info, - self.tile_config.iou_threshold, - self.tile_config.max_num_instances, - ) - for batch_tile_attrs, batch_tile_input in inputs.unbind(): - output = self.forward(batch_tile_input) - if isinstance(output, OTXBatchLossEntity): - msg = "Loss output is not supported for tile merging" - raise TypeError(msg) - tile_preds.append(output) - tile_attrs.append(batch_tile_attrs) - pred_entities = merger.merge(tile_preds, tile_attrs) - - return DetBatchPredEntity( - batch_size=inputs.batch_size, - images=[pred_entity.image for pred_entity in pred_entities], - imgs_info=[pred_entity.img_info for pred_entity in pred_entities], - scores=[pred_entity.score for pred_entity in pred_entities], - bboxes=[pred_entity.bboxes for pred_entity in pred_entities], - labels=[pred_entity.labels for pred_entity in pred_entities], - ) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parameters = super()._export_parameters - parameters["metadata"].update( - { - ("model_info", "model_type"): "ssd", - ("model_info", "task_type"): "detection", - ("model_info", "confidence_threshold"): str(0.0), # it was able to be set in OTX 1.X - ("model_info", "iou_threshold"): str(0.5), - ("model_info", "test_meta_info"): json.dumps(self.test_meta_info), - }, - ) - if self.tile_config.enable_tiler: - parameters["metadata"].update( - { - ("model_info", "tile_size"): str(self.tile_config.tile_size[0]), - ("model_info", "tiles_overlap"): str(self.tile_config.overlap), - ("model_info", "max_pred_number"): str(self.tile_config.max_num_instances), - }, - ) - - return parameters - - -class ExplainableOTXDetModel(OTXDetectionModel): - """OTX detection model which can attach a XAI hook.""" - - def forward_explain( - self, - inputs: DetBatchDataEntity, - ) -> DetBatchPredEntity | DetBatchPredEntityWithXAI | OTXBatchLossEntity: - """Model forward function.""" - from otx.algo.hooks.recording_forward_hook import feature_vector_fn - - self.model.feature_vector_fn = feature_vector_fn - self.model.explain_fn = self.get_explain_fn() - - # If customize_inputs is overridden - outputs = ( - self._forward_explain_detection(self.model, **self._customize_inputs(inputs)) - if self._customize_inputs != ExplainableOTXDetModel._customize_inputs - else self._forward_explain_detection(self.model, inputs) - ) - - return ( - self._customize_outputs(outputs, inputs) - if self._customize_outputs != ExplainableOTXDetModel._customize_outputs - else outputs["predictions"] - ) - - @staticmethod - def _forward_explain_detection( - self: SingleStageDetector, - inputs: torch.Tensor, - data_samples: OptSampleList | None = None, - mode: str = "tensor", - ) -> dict[str, torch.Tensor]: - """Forward func of the BaseDetector instance, which located in is in ExplainableOTXDetModel().model.""" - # Workaround to remove grads for model parameters, since after class patching - # convolutions are failing since thay can't process gradients - for param in self.parameters(): - param.requires_grad = False - - backbone_feat = self.extract_feat(inputs) - bbox_head_feat = self.bbox_head.forward(backbone_feat) - - # Process the first output form bbox detection head: classification scores - feature_vector = self.feature_vector_fn(backbone_feat) - saliency_map = self.explain_fn(bbox_head_feat[0]) - - if mode == "predict": - results_list = self.bbox_head.predict(backbone_feat, data_samples) - if isinstance(results_list, tuple): - # Export case - predictions = results_list - else: - # Predict case, InstanceData or List[InstanceData] - predictions = self.add_pred_to_datasample(data_samples, results_list) - - elif mode == "tensor": - predictions = bbox_head_feat - elif mode == "loss": - # Temporary condition to pass undetermined "test_forward_train" test, values aren't used - predictions = self.bbox_head.loss(backbone_feat, data_samples)["loss_cls"] - else: - msg = f'Invalid mode "{mode}".' - raise RuntimeError(msg) - - return { - "predictions": predictions, - "feature_vector": feature_vector, - "saliency_map": saliency_map, - } - - def get_explain_fn(self) -> Callable: - """Returns explain function.""" - from otx.algo.detection.heads.custom_ssd_head import CustomSSDHead - from otx.algo.hooks.recording_forward_hook import DetClassProbabilityMapHook - - # SSD-like heads also have background class - background_class = isinstance(self.model.bbox_head, CustomSSDHead) - explainer = DetClassProbabilityMapHook( - num_classes=self.num_classes + background_class, - num_anchors=self.get_num_anchors(), - ) - return explainer.func - - def _reset_model_forward(self) -> None: - if not self.explain_mode: - return - - self.model.explain_fn = self.get_explain_fn() - forward_with_explain = self._forward_explain_detection - - self.original_model_forward = self.model.forward - - func_type = types.MethodType - # Patch class method - model_class = type(self.model) - model_class.forward = func_type(forward_with_explain, self.model) - - def _restore_model_forward(self) -> None: - if not self.explain_mode: - return - - if not self.original_model_forward: - msg = "Original model forward was not saved." - raise RuntimeError(msg) - - func_type = types.MethodType - self.model.forward = func_type(self.original_model_forward, self.model) - self.original_model_forward = None - - def get_num_anchors(self) -> list[int]: - """Gets the anchor configuration from model.""" - if anchor_generator := getattr(self.model.bbox_head, "prior_generator", None): - return ( - anchor_generator.num_base_anchors - if hasattr(anchor_generator, "num_base_anchors") - else anchor_generator.num_base_priors - ) - - return [1] * 10 - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parameters = super()._export_parameters - parameters["output_names"] = ["feature_vector", "saliency_map"] if self.explain_mode else None - return parameters - - -class MMDetCompatibleModel(ExplainableOTXDetModel): - """Detection model compatible for MMDet. - - It can consume MMDet model configuration translated into OTX configuration - (please see otx.tools.translate_mmrecipe) and create the OTX detection model - compatible for OTX pipelines. - """ - - def __init__(self, num_classes: int, config: DictConfig) -> None: - config = inplace_num_classes(cfg=config, num_classes=num_classes) - self.config = config - self.load_from = config.pop("load_from", None) - self.image_size: tuple[int, int, int, int] | None = None - super().__init__(num_classes=num_classes) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - if self.image_size is None: - error_msg = "self.image_size shouldn't be None to use mmdeploy." - raise ValueError(error_msg) - - export_params = super()._export_parameters - export_params.update(get_mean_std_from_data_processing(self.config)) - export_params["model_builder"] = self._create_model - export_params["model_cfg"] = copy.copy(self.config) - export_params["test_pipeline"] = self._make_fake_test_pipeline() - - return export_params - - def _create_model(self) -> nn.Module: - from .utils.mmdet import create_model - - model, self.classification_layers = create_model(self.config, self.load_from) - return model - - def _make_fake_test_pipeline(self) -> list[dict[str, Any]]: - return [ - {"type": "LoadImageFromFile"}, - {"type": "Resize", "scale": [self.image_size[3], self.image_size[2]], "keep_ratio": True}, # type: ignore[index] - {"type": "LoadAnnotations", "with_bbox": True}, - { - "type": "PackDetInputs", - "meta_keys": ["ori_filenamescale_factor", "ori_shape", "filename", "img_shape", "pad_shape"], - }, - ] - - def _customize_inputs(self, entity: DetBatchDataEntity) -> dict[str, Any]: - from mmdet.structures import DetDataSample - from mmengine.structures import InstanceData - - mmdet_inputs: dict[str, Any] = {} - - mmdet_inputs["inputs"] = entity.images # B x C x H x W PyTorch tensor - mmdet_inputs["data_samples"] = [ - DetDataSample( - metainfo={ - "img_id": img_info.img_idx, - "img_shape": img_info.img_shape, - "ori_shape": img_info.ori_shape, - "pad_shape": img_info.pad_shape, - "scale_factor": img_info.scale_factor, - "ignored_labels": img_info.ignored_labels, - }, - gt_instances=InstanceData( - bboxes=bboxes, - labels=labels, - ), - ) - for img_info, bboxes, labels in zip( - entity.imgs_info, - entity.bboxes, - entity.labels, - ) - ] - preprocessor: DetDataPreprocessor = self.model.data_preprocessor - - mmdet_inputs = preprocessor(data=mmdet_inputs, training=self.training) - - mmdet_inputs["mode"] = "loss" if self.training else "predict" - - return mmdet_inputs - - def _customize_outputs( - self, - outputs: dict[str, Any], - inputs: DetBatchDataEntity, - ) -> DetBatchPredEntity | DetBatchPredEntityWithXAI | OTXBatchLossEntity: - from mmdet.structures import DetDataSample - - if self.training: - if not isinstance(outputs, dict): - raise TypeError(outputs) - - losses = OTXBatchLossEntity() - for k, v in outputs.items(): - if isinstance(v, list): - losses[k] = sum(v) - elif isinstance(v, torch.Tensor): - losses[k] = v - else: - msg = "Loss output should be list or torch.tensor but got {type(v)}" - raise TypeError(msg) - return losses - - scores = [] - bboxes = [] - labels = [] - - predictions = outputs["predictions"] if isinstance(outputs, dict) else outputs - for output in predictions: - if not isinstance(output, DetDataSample): - raise TypeError(output) - scores.append(output.pred_instances.scores) - bboxes.append( - tv_tensors.BoundingBoxes( - output.pred_instances.bboxes, - format="XYXY", - canvas_size=output.img_shape, - ), - ) - labels.append(output.pred_instances.labels) - - if self.explain_mode: - if not isinstance(outputs, dict): - msg = f"Model output should be a dict, but got {type(outputs)}." - raise ValueError(msg) - - if "feature_vector" not in outputs: - msg = "No feature vector in the model output." - raise ValueError(msg) - - if "saliency_map" not in outputs: - msg = "No saliency maps in the model output." - raise ValueError(msg) - - saliency_maps = outputs["saliency_map"].detach().cpu().numpy() - feature_vectors = outputs["feature_vector"].detach().cpu().numpy() - - return DetBatchPredEntityWithXAI( - batch_size=len(predictions), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - bboxes=bboxes, - labels=labels, - saliency_maps=saliency_maps, - feature_vectors=feature_vectors, - ) - - return DetBatchPredEntity( - batch_size=len(predictions), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - bboxes=bboxes, - labels=labels, - ) - - @property - def _exporter(self) -> OTXModelExporter: - """Creates OTXModelExporter object that can export the model.""" - from otx.core.exporter.mmdeploy import MMdeployExporter - - return MMdeployExporter(**self._export_parameters) - - -class OVDetectionModel(OVModel[DetBatchDataEntity, DetBatchPredEntity, DetBatchPredEntityWithXAI]): - """Object detection model compatible for OpenVINO IR inference. - - It can consume OpenVINO IR model path or model name from Intel OMZ repository - and create the OTX detection model compatible for OTX testing pipeline. - """ - - def __init__( - self, - num_classes: int, - model_name: str, - model_type: str = "SSD", - async_inference: bool = True, - max_num_requests: int | None = None, - use_throughput_mode: bool = True, - model_api_configuration: dict[str, Any] | None = None, - ) -> None: - self.test_meta_info: dict[str, Any] = {} - super().__init__( - num_classes, - model_name, - model_type, - async_inference, - max_num_requests, - use_throughput_mode, - model_api_configuration, - ) - - def _setup_tiler(self) -> None: - """Setup tiler for tile task.""" - execution_mode = "async" if self.async_inference else "sync" - # Note: Disable async_inference as tiling has its own sync/async implementation - self.async_inference = False - self.model = DetectionTiler(self.model, execution_mode=execution_mode) - log.info( - f"Enable tiler with tile size: {self.model.tile_size} \ - and overlap: {self.model.tiles_overlap}", - ) - - def _create_model(self) -> Model: - """Create a OV model with help of Model API.""" - from openvino.model_api.adapters import OpenvinoAdapter, create_core, get_user_config - - plugin_config = get_user_config("AUTO", str(self.num_requests), "AUTO") - if self.use_throughput_mode: - plugin_config["PERFORMANCE_HINT"] = "THROUGHPUT" - - model_adapter = OpenvinoAdapter( - create_core(), - self.model_name, - max_num_requests=self.num_requests, - plugin_config=plugin_config, - model_parameters=self.model_adapter_parameters, - ) - for name, info in model_adapter.model.rt_info["model_info"].items(): - if name == "test_meta_info": - for key, value in json.loads(info.value).items(): - self.test_meta_info[key] = value - return Model.create_model(model_adapter, model_type=self.model_type, configuration=self.model_api_configuration) - - def _customize_outputs( - self, - outputs: list[DetectionResult], - inputs: DetBatchDataEntity, - ) -> DetBatchPredEntity | DetBatchPredEntityWithXAI | OTXBatchLossEntity: - # add label index - bboxes = [] - scores = [] - labels = [] - - # some OMZ model requires to shift labels - first_label = ( - self.model.model.get_label_name(0) - if isinstance(self.model, DetectionTiler) - else self.model.get_label_name(0) - ) - - label_shift = 1 if first_label == "background" else 0 - if label_shift: - log.warning(f"label_shift: {label_shift}") - - for output in outputs: - output_objects = output.objects - if len(output_objects): - bbox = [[output.xmin, output.ymin, output.xmax, output.ymax] for output in output_objects] - else: - bbox = torch.empty(size=(0, 0)) - bboxes.append( - tv_tensors.BoundingBoxes( - bbox, - format="XYXY", - canvas_size=inputs.imgs_info[-1].img_shape, - ), - ) - scores.append(torch.tensor([output.score for output in output_objects])) - labels.append(torch.tensor([output.id - label_shift for output in output_objects])) - - if outputs and outputs[0].saliency_map.size > 1: - # Squeeze dim 4D => 3D, (1, num_classes, H, W) => (num_classes, H, W) - predicted_s_maps = [out.saliency_map[0] for out in outputs] - - # Squeeze dim 2D => 1D, (1, internal_dim) => (internal_dim) - predicted_f_vectors = [out.feature_vector[0] for out in outputs] - return DetBatchPredEntityWithXAI( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - bboxes=bboxes, - labels=labels, - saliency_maps=predicted_s_maps, - feature_vectors=predicted_f_vectors, - ) - - return DetBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - bboxes=bboxes, - labels=labels, - ) diff --git a/src/otx/core/model/entity/instance_segmentation.py b/src/otx/core/model/entity/instance_segmentation.py deleted file mode 100644 index 164a39100e7..00000000000 --- a/src/otx/core/model/entity/instance_segmentation.py +++ /dev/null @@ -1,546 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for instance segmentation model entity used in OTX.""" - -from __future__ import annotations - -import json -import logging as log -import types -from copy import copy -from typing import TYPE_CHECKING, Any, Callable - -import numpy as np -import torch -from mmengine.structures.instance_data import InstanceData -from openvino.model_api.models import Model -from openvino.model_api.tilers import InstanceSegmentationTiler -from torchvision import tv_tensors - -from otx.algo.hooks.recording_forward_hook import MaskRCNNRecordingForwardHook, feature_vector_fn -from otx.core.config.data import TileConfig -from otx.core.data.entity.base import ( - OTXBatchLossEntity, -) -from otx.core.data.entity.instance_segmentation import ( - InstanceSegBatchDataEntity, - InstanceSegBatchPredEntity, - InstanceSegBatchPredEntityWithXAI, -) -from otx.core.data.entity.tile import TileBatchInstSegDataEntity -from otx.core.exporter.base import OTXModelExporter -from otx.core.model.entity.base import OTXModel, OVModel -from otx.core.utils.config import inplace_num_classes -from otx.core.utils.tile_merge import InstanceSegTileMerge -from otx.core.utils.utils import get_mean_std_from_data_processing - -if TYPE_CHECKING: - from mmdet.models.data_preprocessors import DetDataPreprocessor - from mmdet.models.detectors.base import TwoStageDetector - from mmdet.structures import OptSampleList - from omegaconf import DictConfig - from openvino.model_api.models.utils import InstanceSegmentationResult - from torch import nn - - -class OTXInstanceSegModel( - OTXModel[ - InstanceSegBatchDataEntity, - InstanceSegBatchPredEntity, - InstanceSegBatchPredEntityWithXAI, - TileBatchInstSegDataEntity, - ], -): - """Base class for the Instance Segmentation models used in OTX.""" - - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - self.tile_config = TileConfig() - self.test_meta_info: dict[str, Any] = {} - - def forward_tiles(self, inputs: TileBatchInstSegDataEntity) -> InstanceSegBatchPredEntity: - """Unpack instance segmentation tiles. - - Args: - inputs (TileBatchInstSegDataEntity): Tile batch data entity. - - Returns: - InstanceSegBatchPredEntity: Merged instance segmentation prediction. - """ - tile_preds: list[InstanceSegBatchPredEntity | InstanceSegBatchPredEntityWithXAI] = [] - tile_attrs: list[list[dict[str, int | str]]] = [] - merger = InstanceSegTileMerge( - inputs.imgs_info, - self.tile_config.iou_threshold, - self.tile_config.max_num_instances, - ) - for batch_tile_attrs, batch_tile_input in inputs.unbind(): - output = self.forward(batch_tile_input) - if isinstance(output, OTXBatchLossEntity): - msg = "Loss output is not supported for tile merging" - raise TypeError(msg) - tile_preds.append(output) - tile_attrs.append(batch_tile_attrs) - pred_entities = merger.merge(tile_preds, tile_attrs) - - return InstanceSegBatchPredEntity( - batch_size=inputs.batch_size, - images=[pred_entity.image for pred_entity in pred_entities], - imgs_info=[pred_entity.img_info for pred_entity in pred_entities], - scores=[pred_entity.score for pred_entity in pred_entities], - bboxes=[pred_entity.bboxes for pred_entity in pred_entities], - labels=[pred_entity.labels for pred_entity in pred_entities], - masks=[pred_entity.masks for pred_entity in pred_entities], - polygons=[pred_entity.polygons for pred_entity in pred_entities], - ) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parameters = super()._export_parameters - parameters["metadata"].update( - { - ("model_info", "model_type"): "MaskRCNN", - ("model_info", "task_type"): "instance_segmentation", - ("model_info", "confidence_threshold"): str(0.0), # it was able to be set in OTX 1.X - ("model_info", "iou_threshold"): str(0.5), - ("model_info", "test_meta_info"): json.dumps(self.test_meta_info), - }, - ) - - # Instance segmentation needs to add empty label - all_labels = "otx_empty_lbl " - all_label_ids = "None " - for lbl in self.label_info.label_names: - all_labels += lbl.replace(" ", "_") + " " - all_label_ids += lbl.replace(" ", "_") + " " - - parameters["metadata"][("model_info", "labels")] = all_labels.strip() - parameters["metadata"][("model_info", "label_ids")] = all_label_ids.strip() - - if self.tile_config.enable_tiler: - parameters["metadata"].update( - { - ("model_info", "tile_size"): str(self.tile_config.tile_size[0]), - ("model_info", "tiles_overlap"): str(self.tile_config.overlap), - ("model_info", "max_pred_number"): str(self.tile_config.max_num_instances), - }, - ) - - return parameters - - -class ExplainableOTXInstanceSegModel(OTXInstanceSegModel): - """OTX Instance Segmentation model which can attach a XAI hook.""" - - def forward_explain( - self, - inputs: InstanceSegBatchDataEntity, - ) -> InstanceSegBatchPredEntity | InstanceSegBatchPredEntityWithXAI | OTXBatchLossEntity: - """Model forward function.""" - self.model.feature_vector_fn = feature_vector_fn - self.model.explain_fn = self.get_explain_fn() - - # If customize_inputs is overridden - outputs = ( - self._forward_explain_inst_seg(self.model, **self._customize_inputs(inputs)) - if self._customize_inputs != ExplainableOTXInstanceSegModel._customize_inputs - else self._forward_explain_inst_seg(self.model, inputs) - ) - - return ( - self._customize_outputs(outputs, inputs) - if self._customize_outputs != ExplainableOTXInstanceSegModel._customize_outputs - else outputs["predictions"] - ) - - @staticmethod - def _forward_explain_inst_seg( - self: TwoStageDetector, - inputs: torch.Tensor, - data_samples: OptSampleList = None, - mode: str = "tensor", # noqa: ARG004 - ) -> dict[str, torch.Tensor]: - """Forward func of the BaseDetector instance, which located in is in ExplainableOTXInstanceSegModel().model.""" - # Workaround to remove grads for model parameters, since after class patching - # convolutions are failing since thay can't process gradients - for param in self.parameters(): - param.requires_grad = False - - x = self.extract_feat(inputs) - - feature_vector = self.feature_vector_fn(x) - - rpn_results_list = self.rpn_head.predict(x, data_samples, rescale=False) - results_list = self.roi_head.predict(x, rpn_results_list, data_samples, rescale=True) - - if isinstance(results_list, tuple) and isinstance(results_list[0], torch.Tensor): # rewrite - # Export case, consists of tensors - predictions = results_list - # For OV task saliency map are generated on MAPI side - saliency_map = torch.empty(1, dtype=torch.uint8) - - elif isinstance(results_list, list) and isinstance(results_list[0], InstanceData): # rewrite - # Predict case, consists of InstanceData - predictions = self.add_pred_to_datasample(data_samples, results_list) - - features_for_sal_map = [data_sample.pred_instances for data_sample in data_samples] - saliency_map = self.explain_fn(features_for_sal_map) - - return { - "predictions": predictions, - "feature_vector": feature_vector, - "saliency_map": saliency_map, - } - - def get_explain_fn(self) -> Callable: - """Returns explain function.""" - explainer = MaskRCNNRecordingForwardHook(num_classes=self.num_classes) - return explainer.func - - def _reset_model_forward(self) -> None: - if not self.explain_mode: - return - - self.model.explain_fn = self.get_explain_fn() - forward_with_explain = self._forward_explain_inst_seg - - self.original_model_forward = self.model.forward - - func_type = types.MethodType - # Patch class method - model_class = type(self.model) - model_class.forward = func_type(forward_with_explain, self.model) - - def _restore_model_forward(self) -> None: - if not self.explain_mode: - return - - if not self.original_model_forward: - msg = "Original model forward was not saved." - raise RuntimeError(msg) - - func_type = types.MethodType - self.model.forward = func_type(self.original_model_forward, self.model) - self.original_model_forward = None - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parameters = super()._export_parameters - parameters["output_names"] = ["feature_vector", "saliency_map"] if self.explain_mode else None - return parameters - - -class MMDetInstanceSegCompatibleModel(ExplainableOTXInstanceSegModel): - """Instance Segmentation model compatible for MMDet.""" - - def __init__(self, num_classes: int, config: DictConfig) -> None: - config = inplace_num_classes(cfg=config, num_classes=num_classes) - self.config = config - self.load_from = self.config.pop("load_from", None) - self.image_size: tuple[int, int, int, int] | None = None - super().__init__(num_classes=num_classes) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Parameters for an exporter.""" - if self.image_size is None: - error_msg = "self.image_size shouldn't be None to use mmdeploy." - raise ValueError(error_msg) - - export_params = super()._export_parameters - export_params.update(get_mean_std_from_data_processing(self.config)) - export_params["model_builder"] = self._create_model - export_params["model_cfg"] = copy(self.config) - export_params["test_pipeline"] = self._make_fake_test_pipeline() - - return export_params - - def _create_model(self) -> nn.Module: - from .utils.mmdet import create_model - - model, self.classification_layers = create_model(self.config, self.load_from) - return model - - def _make_fake_test_pipeline(self) -> list[dict[str, Any]]: - return [ - {"type": "LoadImageFromFile", "backend_args": None}, - {"type": "Resize", "scale": [self.image_size[3], self.image_size[2]], "keep_ratio": True}, # type: ignore[index] - {"type": "LoadAnnotations", "with_bbox": True, "with_mask": True}, - { - "type": "PackDetInputs", - "meta_keys": ["img_idimg_path", "ori_shape", "img_shape", "scale_factor"], - }, - ] - - def _customize_inputs(self, entity: InstanceSegBatchDataEntity) -> dict[str, Any]: - from mmdet.structures import DetDataSample - from mmdet.structures.mask import BitmapMasks, PolygonMasks - from mmengine.structures import InstanceData - - mmdet_inputs: dict[str, Any] = {} - - mmdet_inputs["inputs"] = entity.images # B x C x H x W PyTorch tensor - mmdet_inputs["data_samples"] = [] - - for img_info, bboxes, masks, polygons, labels in zip( - entity.imgs_info, - entity.bboxes, - entity.masks, - entity.polygons, - entity.labels, - ): - # NOTE: ground-truth masks are resized in training, but not in inference - height, width = img_info.img_shape if self.training else img_info.ori_shape - if len(masks): - mmdet_masks = BitmapMasks(masks.data.cpu().numpy(), height, width) - else: - mmdet_masks = PolygonMasks( - [[np.array(polygon.points)] for polygon in polygons], - height, - width, - ) - - data_sample = DetDataSample( - metainfo={ - "img_id": img_info.img_idx, - "img_shape": img_info.img_shape, - "ori_shape": img_info.ori_shape, - "pad_shape": img_info.pad_shape, - "scale_factor": img_info.scale_factor, - "ignored_labels": img_info.ignored_labels, - }, - gt_instances=InstanceData( - bboxes=bboxes, - masks=mmdet_masks, - labels=labels, - ), - ) - mmdet_inputs["data_samples"].append(data_sample) - - preprocessor: DetDataPreprocessor = self.model.data_preprocessor - - mmdet_inputs = preprocessor(data=mmdet_inputs, training=self.training) - - mmdet_inputs["mode"] = "loss" if self.training else "predict" - - return mmdet_inputs - - def _customize_outputs( - self, - outputs: dict[str, Any], - inputs: InstanceSegBatchDataEntity, - ) -> InstanceSegBatchPredEntity | InstanceSegBatchPredEntityWithXAI | OTXBatchLossEntity: - from mmdet.structures import DetDataSample - - if self.training: - if not isinstance(outputs, dict): - raise TypeError(outputs) - - losses = OTXBatchLossEntity() - for loss_name, loss_value in outputs.items(): - if isinstance(loss_value, torch.Tensor): - losses[loss_name] = loss_value - elif isinstance(loss_value, list): - losses[loss_name] = sum(_loss.mean() for _loss in loss_value) - return losses - - scores: list[torch.Tensor] = [] - bboxes: list[tv_tensors.BoundingBoxes] = [] - labels: list[torch.LongTensor] = [] - masks: list[tv_tensors.Mask] = [] - - predictions = outputs["predictions"] if isinstance(outputs, dict) else outputs - for output in predictions: - if not isinstance(output, DetDataSample): - raise TypeError(output) - - scores.append(output.pred_instances.scores) - bboxes.append( - tv_tensors.BoundingBoxes( - output.pred_instances.bboxes, - format="XYXY", - canvas_size=output.ori_shape, - ), - ) - output_masks = tv_tensors.Mask( - output.pred_instances.masks, - dtype=torch.bool, - ) - masks.append(output_masks) - labels.append(output.pred_instances.labels) - - if self.explain_mode: - if not isinstance(outputs, dict): - msg = f"Model output should be a dict, but got {type(outputs)}." - raise ValueError(msg) - - if "feature_vector" not in outputs: - msg = "No feature vector in the model output." - raise ValueError(msg) - - if "saliency_map" not in outputs: - msg = "No saliency maps in the model output." - raise ValueError(msg) - - saliency_maps = outputs["saliency_map"].detach().cpu().numpy() - feature_vectors = outputs["feature_vector"].detach().cpu().numpy() - - return InstanceSegBatchPredEntityWithXAI( - batch_size=len(predictions), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - bboxes=bboxes, - masks=masks, - polygons=[], - labels=labels, - saliency_maps=list(saliency_maps), - feature_vectors=list(feature_vectors), - ) - - return InstanceSegBatchPredEntity( - batch_size=len(predictions), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - bboxes=bboxes, - masks=masks, - polygons=[], - labels=labels, - ) - - @property - def _exporter(self) -> OTXModelExporter: - """Creates OTXModelExporter object that can export the model.""" - from otx.core.exporter.mmdeploy import MMdeployExporter - - return MMdeployExporter(**self._export_parameters) - - -class OVInstanceSegmentationModel( - OVModel[InstanceSegBatchDataEntity, InstanceSegBatchPredEntity, InstanceSegBatchPredEntityWithXAI], -): - """Instance segmentation model compatible for OpenVINO IR inference. - - It can consume OpenVINO IR model path or model name from Intel OMZ repository - and create the OTX detection model compatible for OTX testing pipeline. - """ - - def __init__( - self, - num_classes: int, - model_name: str, - model_type: str = "MaskRCNN", - async_inference: bool = True, - max_num_requests: int | None = None, - use_throughput_mode: bool = True, - model_api_configuration: dict[str, Any] | None = None, - ) -> None: - self.test_meta_info: dict[str, Any] = {} - super().__init__( - num_classes, - model_name, - model_type, - async_inference, - max_num_requests, - use_throughput_mode, - model_api_configuration, - ) - - def _setup_tiler(self) -> None: - """Setup tiler for tile task.""" - execution_mode = "async" if self.async_inference else "sync" - # Note: Disable async_inference as tiling has its own sync/async implementation - self.async_inference = False - self.model = InstanceSegmentationTiler(self.model, execution_mode=execution_mode) - log.info( - f"Enable tiler with tile size: {self.model.tile_size} \ - and overlap: {self.model.tiles_overlap}", - ) - - def _create_model(self) -> Model: - """Create a OV model with help of Model API.""" - from openvino.model_api.adapters import OpenvinoAdapter, create_core, get_user_config - - plugin_config = get_user_config("AUTO", str(self.num_requests), "AUTO") - if self.use_throughput_mode: - plugin_config["PERFORMANCE_HINT"] = "THROUGHPUT" - - model_adapter = OpenvinoAdapter( - create_core(), - self.model_name, - max_num_requests=self.num_requests, - plugin_config=plugin_config, - model_parameters=self.model_adapter_parameters, - ) - for name, info in model_adapter.model.rt_info["model_info"].items(): - if name == "test_meta_info": - for key, value in json.loads(info.value).items(): - self.test_meta_info[key] = value - return Model.create_model(model_adapter, model_type=self.model_type, configuration=self.model_api_configuration) - - def _customize_outputs( - self, - outputs: list[InstanceSegmentationResult], - inputs: InstanceSegBatchDataEntity, - ) -> InstanceSegBatchPredEntity | InstanceSegBatchPredEntityWithXAI | OTXBatchLossEntity: - # add label index - bboxes = [] - scores = [] - labels = [] - masks = [] - for output in outputs: - output_objects = output.segmentedObjects - if len(output_objects): - bbox = [[output.xmin, output.ymin, output.xmax, output.ymax] for output in output_objects] - else: - bbox = torch.empty(size=(0, 0)) - bboxes.append( - tv_tensors.BoundingBoxes( - bbox, - format="XYXY", - canvas_size=inputs.imgs_info[-1].img_shape, - ), - ) - # NOTE: OTX 1.5 filter predictions with result_based_confidence_threshold, - # but OTX 2.0 doesn't have it in configuration. - _masks = [output.mask for output in output_objects] - _masks = np.stack(_masks) if len(_masks) else [] - scores.append(torch.tensor([output.score for output in output_objects])) - masks.append(torch.tensor(_masks)) - labels.append(torch.tensor([output.id - 1 for output in output_objects])) - - if outputs and outputs[0].saliency_map: - predicted_s_maps = [] - for out in outputs: - image_map = np.array([s_map for s_map in out.saliency_map if s_map.ndim > 1]) - predicted_s_maps.append(image_map) - - # Squeeze dim 2D => 1D, (1, internal_dim) => (internal_dim) - predicted_f_vectors = [out.feature_vector[0] for out in outputs] - return InstanceSegBatchPredEntityWithXAI( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - bboxes=bboxes, - masks=masks, - polygons=[], - labels=labels, - saliency_maps=predicted_s_maps, - feature_vectors=predicted_f_vectors, - ) - - return InstanceSegBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - bboxes=bboxes, - masks=masks, - polygons=[], - labels=labels, - ) diff --git a/src/otx/core/model/entity/rotated_detection.py b/src/otx/core/model/entity/rotated_detection.py deleted file mode 100644 index 420b7cc01d7..00000000000 --- a/src/otx/core/model/entity/rotated_detection.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for rotated detection model entity used in OTX.""" - - -from otx.core.model.entity.instance_segmentation import ( - MMDetInstanceSegCompatibleModel, - OTXInstanceSegModel, - OVInstanceSegmentationModel, -) - - -class OTXRotatedDetModel(OTXInstanceSegModel): - """Base class for the rotated detection models used in OTX.""" - - -class MMDetRotatedDetModel(OTXRotatedDetModel, MMDetInstanceSegCompatibleModel): - """Rotated Detection model compaible for MMDet.""" - - -class OVRotatedDetectionModel(OVInstanceSegmentationModel): - """Rotated Detection model compatible for OpenVINO IR Inference. - - It can consume OpenVINO IR model path or model name from Intel OMZ repository - and create the OTX detection model compatible for OTX testing pipeline. - """ diff --git a/src/otx/core/model/entity/segmentation.py b/src/otx/core/model/entity/segmentation.py deleted file mode 100644 index c313c8a42fb..00000000000 --- a/src/otx/core/model/entity/segmentation.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for detection model entity used in OTX.""" - -from __future__ import annotations - -import copy -from typing import TYPE_CHECKING, Any - -from torchvision import tv_tensors - -from otx.core.data.entity.base import OTXBatchLossEntity -from otx.core.data.entity.segmentation import SegBatchDataEntity, SegBatchPredEntity, SegBatchPredEntityWithXAI -from otx.core.data.entity.tile import T_OTXTileBatchDataEntity -from otx.core.exporter.base import OTXModelExporter -from otx.core.exporter.native import OTXNativeModelExporter -from otx.core.model.entity.base import OTXModel, OVModel -from otx.core.utils.config import inplace_num_classes -from otx.core.utils.utils import get_mean_std_from_data_processing - -if TYPE_CHECKING: - from mmseg.models.data_preprocessor import SegDataPreProcessor - from omegaconf import DictConfig - from openvino.model_api.models.utils import ImageResultWithSoftPrediction - from torch import nn - - -class OTXSegmentationModel( - OTXModel[SegBatchDataEntity, SegBatchPredEntity, SegBatchPredEntityWithXAI, T_OTXTileBatchDataEntity], -): - """Base class for the detection models used in OTX.""" - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - parameters = super()._export_parameters - hierarchical_config: dict = {} - hierarchical_config["cls_heads_info"] = {} - hierarchical_config["label_tree_edges"] = [] - - parameters["metadata"].update( - { - ("model_info", "model_type"): "Segmentation", - ("model_info", "task_type"): "segmentation", - ("model_info", "return_soft_prediction"): str(True), - ("model_info", "soft_threshold"): str(0.5), - ("model_info", "blur_strength"): str(-1), - }, - ) - return parameters - - -class MMSegCompatibleModel(OTXSegmentationModel): - """Segmentation model compatible for MMSeg. - - It can consume MMSeg model configuration translated into OTX configuration - (please see otx.tools.translate_mmrecipe) and create the OTX detection model - compatible for OTX pipelines. - """ - - def __init__(self, num_classes: int, config: DictConfig) -> None: - config = inplace_num_classes(cfg=config, num_classes=num_classes) - self.config = config - self.load_from = self.config.pop("load_from", None) - self.image_size = (1, 3, 544, 544) - super().__init__(num_classes=num_classes) - - def _create_model(self) -> nn.Module: - from .utils.mmseg import create_model - - model, self.classification_layers = create_model(self.config, self.load_from) - return model - - def _customize_inputs(self, entity: SegBatchDataEntity) -> dict[str, Any]: - from mmengine.structures import PixelData - from mmseg.structures import SegDataSample - - mmseg_inputs: dict[str, Any] = {} - mmseg_inputs["inputs"] = entity.images # B x C x H x W PyTorch tensor - mmseg_inputs["data_samples"] = [ - SegDataSample( - metainfo={ - "img_id": img_info.img_idx, - "img_shape": img_info.img_shape, - "ori_shape": img_info.ori_shape, - "pad_shape": img_info.pad_shape, - "scale_factor": img_info.scale_factor, - "ignored_labels": img_info.ignored_labels, - }, - gt_sem_seg=PixelData( - data=masks, - ), - ) - for img_info, masks in zip( - entity.imgs_info, - entity.masks, - ) - ] - preprocessor: SegDataPreProcessor = self.model.data_preprocessor - mmseg_inputs = preprocessor(data=mmseg_inputs, training=self.training) - mmseg_inputs["mode"] = "loss" if self.training else "predict" - - return mmseg_inputs - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: SegBatchDataEntity, - ) -> SegBatchPredEntity | SegBatchPredEntityWithXAI | OTXBatchLossEntity: - from mmseg.structures import SegDataSample - - if self.training: - if not isinstance(outputs, dict): - raise TypeError(outputs) - - losses = OTXBatchLossEntity() - for k, v in outputs.items(): - if "loss" in k: - losses[k] = v - return losses - - masks = [] - - for output in outputs: - if not isinstance(output, SegDataSample): - raise TypeError(output) - masks.append(output.pred_sem_seg.data) - - if hasattr(self, "explain_hook"): - hook_records = self.explain_hook.records - explain_results = copy.deepcopy(hook_records[-len(outputs) :]) - - return SegBatchPredEntityWithXAI( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=[], - masks=masks, - saliency_maps=explain_results, - feature_vectors=[], - ) - - return SegBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=[], - masks=masks, - ) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - export_params = super()._export_parameters - export_params.update(get_mean_std_from_data_processing(self.config)) - export_params["resize_mode"] = "standard" - export_params["pad_value"] = 0 - export_params["swap_rgb"] = False - export_params["via_onnx"] = False - export_params["input_size"] = self.image_size - export_params["onnx_export_configuration"] = None - - return export_params - - @property - def _exporter(self) -> OTXModelExporter: - """Creates OTXModelExporter object that can export the model.""" - return OTXNativeModelExporter(**self._export_parameters) - - -class OVSegmentationModel(OVModel[SegBatchDataEntity, SegBatchPredEntity, SegBatchPredEntityWithXAI]): - """Semantic segmentation model compatible for OpenVINO IR inference. - - It can consume OpenVINO IR model path or model name from Intel OMZ repository - and create the OTX segmentation model compatible for OTX testing pipeline. - """ - - def __init__( - self, - num_classes: int, - model_name: str, - model_type: str = "Segmentation", - async_inference: bool = True, - max_num_requests: int | None = None, - use_throughput_mode: bool = True, - model_api_configuration: dict[str, Any] | None = None, - ) -> None: - super().__init__( - num_classes, - model_name, - model_type, - async_inference, - max_num_requests, - use_throughput_mode, - model_api_configuration, - ) - - def _customize_outputs( - self, - outputs: list[ImageResultWithSoftPrediction], - inputs: SegBatchDataEntity, - ) -> SegBatchPredEntity | SegBatchPredEntityWithXAI | OTXBatchLossEntity: - if outputs and outputs[0].saliency_map.size != 1: - predicted_s_maps = [out.saliency_map for out in outputs] - predicted_f_vectors = [out.feature_vector for out in outputs] - return SegBatchPredEntityWithXAI( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=[], - masks=[tv_tensors.Mask(mask.resultImage) for mask in outputs], - saliency_maps=predicted_s_maps, - feature_vectors=predicted_f_vectors, - ) - - return SegBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=[], - masks=[tv_tensors.Mask(mask.resultImage) for mask in outputs], - ) diff --git a/src/otx/core/model/entity/utils/__init__.py b/src/otx/core/model/entity/utils/__init__.py deleted file mode 100644 index 3b425fcaa60..00000000000 --- a/src/otx/core/model/entity/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility files for model entities used in OTX.""" diff --git a/src/otx/core/model/entity/utils/mmaction.py b/src/otx/core/model/entity/utils/mmaction.py deleted file mode 100644 index e2482927bfd..00000000000 --- a/src/otx/core/model/entity/utils/mmaction.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility files for the mmaction package.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from mmaction.models.data_preprocessors import ( - ActionDataPreprocessor as _ActionDataPreprocessor, -) -from mmaction.registry import MODELS -from mmengine.registry import MODELS as MMENGINE_MODELS - -from otx.core.utils.build import build_mm_model, get_classification_layers - -if TYPE_CHECKING: - from omegaconf import DictConfig - from torch import device, nn - - -# NOTE: For the history of this monkey patching, please see -# https://github.com/openvinotoolkit/training_extensions/issues/2743 -@MMENGINE_MODELS.register_module(force=True) -class ActionDataPreprocessor(_ActionDataPreprocessor): - """Class for monkey patching preprocessor. - - NOTE: For the history of this monkey patching, please see - https://github.com/openvinotoolkit/training_extensions/issues/2743 - """ - - @property - def device(self) -> device: - """Which device being used.""" - try: - buf = next(self.buffers()) - except StopIteration: - return super().device - else: - return buf.device - - -def create_model(config: DictConfig, load_from: str | None) -> tuple[nn.Module, dict[str, dict[str, int]]]: - """Create a model from mmaction Model registry. - - Args: - config (DictConfig): Model configuration. - load_from (str | None): Model weight file path. - - Returns: - tuple[nn.Module, dict[str, dict[str, int]]]: Model instance and classification layers. - """ - classification_layers = get_classification_layers(config, MODELS, "model.") - return build_mm_model(config, MODELS, load_from), classification_layers diff --git a/src/otx/core/model/entity/utils/mmdet.py b/src/otx/core/model/entity/utils/mmdet.py deleted file mode 100644 index 2ff192ac8db..00000000000 --- a/src/otx/core/model/entity/utils/mmdet.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility files for the mmdet package.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from mmdet.models.data_preprocessors import ( - DetDataPreprocessor as _DetDataPreprocessor, -) -from mmdet.registry import MODELS -from mmengine.registry import MODELS as MMENGINE_MODELS - -from otx.core.utils.build import build_mm_model, get_classification_layers - -if TYPE_CHECKING: - from omegaconf import DictConfig - from torch import device, nn - - -@MMENGINE_MODELS.register_module(force=True) -class DetDataPreprocessor(_DetDataPreprocessor): - """Class for monkey patching preprocessor. - - NOTE: For the history of this monkey patching, please see - https://github.com/openvinotoolkit/training_extensions/issues/2743 - """ - - @property - def device(self) -> device: - """Which device being used.""" - try: - buf = next(self.buffers()) - except StopIteration: - return super().device - else: - return buf.device - - -def create_model(config: DictConfig, load_from: str | None) -> tuple[nn.Module, dict[str, dict[str, int]]]: - """Create a model from mmdet Model registry. - - Args: - config (DictConfig): Model configuration. - load_from (str | None): Model weight file path. - - Returns: - tuple[nn.Module, dict[str, dict[str, int]]]: Model instance and classification layers. - """ - classification_layers = get_classification_layers(config, MODELS, "model.") - return build_mm_model(config, MODELS, load_from), classification_layers diff --git a/src/otx/core/model/entity/utils/mmpretrain.py b/src/otx/core/model/entity/utils/mmpretrain.py deleted file mode 100644 index deaed816e25..00000000000 --- a/src/otx/core/model/entity/utils/mmpretrain.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility files for the mmpretrain package.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from mmpretrain.models.utils import ClsDataPreprocessor as _ClsDataPreprocessor -from mmpretrain.registry import MODELS - -from otx.core.utils.build import build_mm_model, get_classification_layers - -if TYPE_CHECKING: - from omegaconf import DictConfig - from torch import device, nn - - -@MODELS.register_module(force=True) -class ClsDataPreprocessor(_ClsDataPreprocessor): - """Class for monkey patching preprocessor. - - NOTE: For the history of this monkey patching, please see - https://github.com/openvinotoolkit/training_extensions/issues/2743 - """ - - @property - def device(self) -> device: - """Which device being used.""" - try: - buf = next(self.buffers()) - except StopIteration: - return super().device - else: - return buf.device - - -def create_model(config: DictConfig, load_from: str | None = None) -> tuple[nn.Module, dict[str, dict[str, int]]]: - """Create a model from mmpretrain Model registry. - - Args: - config (DictConfig): Model configuration. - load_from (str | None): Model weight file path. - - Returns: - tuple[nn.Module, dict[str, dict[str, int]]]: Model instance and classification layers. - """ - classification_layers = get_classification_layers(config, MODELS, "model.") - return build_mm_model(config, MODELS, load_from), classification_layers diff --git a/src/otx/core/model/entity/utils/mmseg.py b/src/otx/core/model/entity/utils/mmseg.py deleted file mode 100644 index 1e40378e55a..00000000000 --- a/src/otx/core/model/entity/utils/mmseg.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# Copyright (c) OpenMMLab. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Please note that parts of source code here are -# borrowed from https://github.com/open-mmlab/mmsegmentation -# -"""Utility files for the mmseg package.""" -# ruff: noqa -from __future__ import annotations - -from typing import TYPE_CHECKING - -import numpy as np -import torch -import torch.nn.functional as F -from mmengine.registry import MODELS as MMENGINE_MODELS -from mmseg.models.data_preprocessor import SegDataPreProcessor as _SegDataPreProcessor -from mmseg.registry import MODELS - -from otx.core.utils.build import build_mm_model, get_classification_layers - -if TYPE_CHECKING: - from mmseg.utils.typing_utils import SampleList - from omegaconf import DictConfig - from torch import device, nn - - -def stack_batch( - inputs: list[torch.Tensor] | torch.Tensor, - data_samples: SampleList | None = None, - size: tuple | None = None, - size_divisor: int | None = None, - pad_val: int | float = 0, - seg_pad_val: int | float = 255, -) -> torch.Tensor: - """Stack multiple inputs to form a batch and pad the images and gt_sem_segs - to the max shape use the right bottom padding mode. - - Args: - inputs (List[Tensor] | torch.Tensor): The input multiple tensors. each is a - CHW 3D-tensor. - data_samples (list[:obj:`SegDataSample`]): The list of data samples. - It usually includes information such as `gt_sem_seg`. - size (tuple, optional): Fixed padding size. - size_divisor (int, optional): The divisor of padded size. - pad_val (int, float): The padding value. Defaults to 0 - seg_pad_val (int, float): The padding value. Defaults to 255 - - Returns: - Tensor: The 4D-tensor. - List[:obj:`SegDataSample`]: After the padding of the gt_seg_map. - """ - assert isinstance(inputs, list) or ( - isinstance(inputs, torch.Tensor) and inputs.ndim == 4 - ), f"Expected input type to be a list or 4D torch.Tensor, but got {type(inputs)}" - assert ( - len({tensor.ndim for tensor in inputs}) == 1 - ), f"Expected the dimensions of all inputs must be the same, but got {[tensor.ndim for tensor in inputs]}" - assert inputs[0].ndim == 3, f"Expected tensor dimension to be 3, but got {inputs[0].ndim}" - assert ( - len({tensor.shape[0] for tensor in inputs}) == 1 - ), f"Expected the channels of all inputs must be the same, but got {[tensor.shape[0] for tensor in inputs]}" - - # only one of size and size_divisor should be valid - assert (size is not None) ^ (size_divisor is not None), "only one of size and size_divisor should be valid" - - padded_inputs = [] - padded_samples = [] - inputs_sizes = [(img.shape[-2], img.shape[-1]) for img in inputs] - max_size = np.stack(inputs_sizes).max(0) - if size_divisor is not None and size_divisor > 1: - # the last two dims are H,W, both subject to divisibility requirement - max_size = (max_size + (size_divisor - 1)) // size_divisor * size_divisor - - for i in range(len(inputs)): - tensor = inputs[i] - if size is not None: - width = max(size[-1] - tensor.shape[-1], 0) - height = max(size[-2] - tensor.shape[-2], 0) - # (padding_left, padding_right, padding_top, padding_bottom) - padding_size = (0, width, 0, height) - elif size_divisor is not None: - width = max(max_size[-1] - tensor.shape[-1], 0) - height = max(max_size[-2] - tensor.shape[-2], 0) - padding_size = (0, width, 0, height) - else: - padding_size = (0, 0, 0, 0) - - # pad img - pad_img = F.pad(tensor, padding_size, value=pad_val) - padded_inputs.append(pad_img) - # pad gt_sem_seg - if data_samples is not None: - data_sample = data_samples[i] - pad_shape = None - if "gt_sem_seg" in data_sample: - gt_sem_seg = data_sample.gt_sem_seg.data - del data_sample.gt_sem_seg.data - data_sample.gt_sem_seg.data = F.pad(gt_sem_seg, padding_size, value=seg_pad_val) - pad_shape = data_sample.gt_sem_seg.shape - if "gt_edge_map" in data_sample: - gt_edge_map = data_sample.gt_edge_map.data - del data_sample.gt_edge_map.data - data_sample.gt_edge_map.data = F.pad(gt_edge_map, padding_size, value=seg_pad_val) - pad_shape = data_sample.gt_edge_map.shape - if "gt_depth_map" in data_sample: - gt_depth_map = data_sample.gt_depth_map.data - del data_sample.gt_depth_map.data - data_sample.gt_depth_map.data = F.pad(gt_depth_map, padding_size, value=seg_pad_val) - pad_shape = data_sample.gt_depth_map.shape - data_sample.set_metainfo( - {"img_shape": tensor.shape[-2:], "pad_shape": pad_shape, "padding_size": padding_size}, - ) - padded_samples.append(data_sample) - else: - padded_samples.append(dict(img_padding_size=padding_size, pad_shape=pad_img.shape[-2:])) - - return torch.stack(padded_inputs, dim=0), padded_samples - - -# NOTE: For the history of this monkey patching, please see -# https://github.com/openvinotoolkit/training_extensions/issues/2743 -@MMENGINE_MODELS.register_module(force=True) -class SegDataPreProcessor(_SegDataPreProcessor): - """Class for monkey patching preprocessor. - - NOTE: For the history of this monkey patching, please see - https://github.com/openvinotoolkit/training_extensions/issues/2743 - """ - - @property - def device(self) -> device: - """Which device being used.""" - try: - buf = next(self.buffers()) - except StopIteration: - return super().device - else: - return buf.device - - def forward(self, data: dict, training: bool = False) -> dict: - """Perform normalization、padding and bgr2rgb conversion based on - ``BaseDataPreprocessor``. - - Args: - data (dict): data sampled from dataloader. - training (bool): Whether to enable training time augmentation. - - Returns: - Dict: Data in the same format as the model input. - """ - data = self.cast_data(data) # type: ignore - inputs = data["inputs"] - data_samples = data.get("data_samples", None) - - # NOTE: We slightly changed this code for small optimization. - # The original code flow is - # 1) Change image tensor's dtype from uint8 to float32 and normalize => 2) Pad and stack images and masks - # However, we reverse it as 2) => 1) to perform pad operation on low bits (uint8) rather than high bits (float32) - - # Pad and stack images and masks - if training: - assert data_samples is not None, ("During training, ", "`data_samples` must be define.") - # Call stack_batch only if the image is not 4D tensor and there is a mismatch in the ground truth masks - if not ( - isinstance(inputs, torch.Tensor) - and inputs.ndim == 4 - and all("gt_sem_seg" in data_sample for data_sample in data_samples) - and len(set(data_sample.gt_sem_seg.shape for data_sample in data_samples)) == 1 - ): - inputs, data_samples = stack_batch( - inputs=inputs, - data_samples=data_samples, - size=self.size, - size_divisor=self.size_divisor, - pad_val=self.pad_val, - seg_pad_val=self.seg_pad_val, - ) - - if self.batch_augments is not None: - inputs, data_samples = self.batch_augments(inputs, data_samples) - else: - img_size = inputs[0].shape[1:] - assert all( - input_.shape[1:] == img_size for input_ in inputs - ), "The image size in a batch should be the same." - # pad images when testing - if self.test_cfg: - inputs, padded_samples = stack_batch( - inputs=inputs, - size=self.test_cfg.get("size", None), - size_divisor=self.test_cfg.get("size_divisor", None), - pad_val=self.pad_val, - seg_pad_val=self.seg_pad_val, - ) - for data_sample, pad_info in zip(data_samples, padded_samples): - data_sample.set_metainfo({**pad_info}) - else: - inputs = torch.stack(inputs, dim=0) - - # Change image tensor's dtype from uint8 to float32 and normalize - # TODO: whether normalize should be after stack_batch - if self.channel_conversion and inputs[0].size(0) == 3: - inputs = ( - inputs[:, [2, 1, 0], ...] - if isinstance(inputs, torch.Tensor) - else [_input[[2, 1, 0], ...] for _input in inputs] - ) - - if self._enable_normalize: - inputs = ( - (inputs.float() - self.mean) / self.std - if isinstance(inputs, torch.Tensor) - else [(_input.float() - self.mean) / self.std for _input in inputs] - ) - - return dict(inputs=inputs, data_samples=data_samples) - - -def create_model(config: DictConfig, load_from: str | None) -> tuple[nn.Module, dict[str, dict[str, int]]]: - """Create a model from mmseg Model registry. - - Args: - config (DictConfig): Model configuration. - load_from (str | None): Model weight file path. - - Returns: - tuple[nn.Module, dict[str, dict[str, int]]]: Model instance and classification layers. - """ - classification_layers = get_classification_layers(config, MODELS, "model.") - return build_mm_model(config, MODELS, load_from), classification_layers diff --git a/src/otx/core/model/entity/visual_prompting.py b/src/otx/core/model/entity/visual_prompting.py deleted file mode 100644 index 9c59cab39cc..00000000000 --- a/src/otx/core/model/entity/visual_prompting.py +++ /dev/null @@ -1,993 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for visual prompting model entity used in OTX.""" - -from __future__ import annotations - -import logging as log -import os -import pickle -from collections import defaultdict -from copy import deepcopy -from functools import partial -from itertools import product -from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal - -import cv2 -import numpy as np -import torch -from torchvision import tv_tensors - -from otx.core.data.entity.base import OTXBatchLossEntity, Points, T_OTXBatchPredEntityWithXAI -from otx.core.data.entity.tile import T_OTXTileBatchDataEntity -from otx.core.data.entity.visual_prompting import ( - VisualPromptingBatchDataEntity, - VisualPromptingBatchPredEntity, - ZeroShotVisualPromptingBatchDataEntity, - ZeroShotVisualPromptingBatchPredEntity, -) -from otx.core.exporter.base import OTXModelExporter -from otx.core.exporter.visual_prompting import OTXVisualPromptingModelExporter -from otx.core.model.entity.base import OTXModel, OVModel - -if TYPE_CHECKING: - from openvino.model_api.models import Model - - from otx.core.data.module import OTXDataModule - - -class OTXVisualPromptingModel( - OTXModel[ - VisualPromptingBatchDataEntity | ZeroShotVisualPromptingBatchDataEntity, - VisualPromptingBatchPredEntity | ZeroShotVisualPromptingBatchPredEntity, - T_OTXBatchPredEntityWithXAI, - T_OTXTileBatchDataEntity, - ], -): - """Base class for the visual prompting models used in OTX.""" - - def __init__(self, num_classes: int = 0) -> None: - super().__init__(num_classes=num_classes) - - @property - def _exporter(self) -> OTXModelExporter: - """Creates OTXModelExporter object that can export the model.""" - return OTXVisualPromptingModelExporter(via_onnx=True, **self._export_parameters) - - @property - def _export_parameters(self) -> dict[str, Any]: - """Defines parameters required to export a particular model implementation.""" - export_params = super()._export_parameters - export_params["metadata"].update( - { - ("model_info", "model_type"): "Visual_Prompting", - ("model_info", "task_type"): "visual_prompting", - }, - ) - export_params["input_size"] = (1, 3, self.model.image_size, self.model.image_size) - export_params["resize_mode"] = "fit_to_window" - export_params["mean"] = (123.675, 116.28, 103.53) - export_params["std"] = (58.395, 57.12, 57.375) - return export_params - - @property - def _optimization_config(self) -> dict[str, Any]: - """PTQ config for visual prompting models.""" - return { - "model_type": "transformer", - "advanced_parameters": { - "activations_range_estimator_params": { - "min": { - "statistics_type": "QUANTILE", - "aggregator_type": "MIN", - "quantile_outlier_prob": "1e-4", - }, - "max": { - "statistics_type": "QUANTILE", - "aggregator_type": "MAX", - "quantile_outlier_prob": "1e-4", - }, - }, - }, - } - - def _reset_prediction_layer(self, num_classes: int) -> None: - return - - -class OVVisualPromptingModel( - OVModel[ - VisualPromptingBatchDataEntity | ZeroShotVisualPromptingBatchDataEntity, - VisualPromptingBatchPredEntity | ZeroShotVisualPromptingBatchPredEntity, - T_OTXBatchPredEntityWithXAI, - ], -): - """Visual prompting model compatible for OpenVINO IR inference. - - It can only consume OpenVINO IR model path and create the OTX visual prompting model compatible - for OTX testing pipeline. - """ - - def __init__( - self, - num_classes: int, - model_name: str, - model_type: str = "Visual_Prompting", - async_inference: bool = False, - max_num_requests: int | None = None, - use_throughput_mode: bool = True, - model_api_configuration: dict[str, Any] | None = None, - ) -> None: - if async_inference: - log.warning( - "Async inference is not supported for visual prompting models. Setting async_inference to False.", - ) - async_inference = False - - basename: str = Path(model_name).name - model_type_name: str = "_".join(basename.split("_")[:2]) - self.model_names: dict[str, str] = { - module: model_name.replace(basename, f"{model_type_name}_{module}.xml") - for module in ["image_encoder", "decoder"] - } - super().__init__( - num_classes, - model_name, - model_type, - async_inference, - max_num_requests, - use_throughput_mode, - model_api_configuration, - ) - - def _create_model(self) -> dict[str, Model]: - """Create a OV model with help of Model API.""" - from openvino.model_api.adapters import OpenvinoAdapter, create_core, get_user_config - from openvino.model_api.models import Model - - ov_models: dict[str, Model] = {} - - plugin_config = get_user_config("AUTO", str(self.num_requests), "AUTO") - if self.use_throughput_mode: - plugin_config["PERFORMANCE_HINT"] = "THROUGHPUT" - - model_parameters = {"decoder": {"input_layouts": "image_embeddings:NCHW"}} - for module in ["image_encoder", "decoder"]: - model_adapter = OpenvinoAdapter( - core=create_core(), - model=self.model_names.get(module), - model_parameters=model_parameters.get(module, {}), - max_num_requests=self.num_requests, - plugin_config=plugin_config, - ) - ov_models[module] = Model.create_model(model_adapter, module, configuration=self.model_api_configuration) - return ov_models - - def forward( - self, - inputs: VisualPromptingBatchDataEntity, # type: ignore[override] - ) -> VisualPromptingBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Model forward function.""" - if self.async_inference: - log.warning( - ( - "Async inference is not supported for visual prompting models yet. " - "Running synchronous inference instead.", - ), - ) - - images, metas, batch_prompts = self._customize_inputs(inputs) - outputs: list[dict[str, Any]] = [] - for image, meta, prompts in zip(images, metas, batch_prompts): - # forward image encoder - image_embeddings = self.model["image_encoder"].infer_sync(image) - - # forward decoder - for prompt in prompts: - label = prompt.pop("label") - prompt.update(**image_embeddings) - - # forward decoder to get predicted mask - prediction = self.model["decoder"].infer_sync(prompt) - prediction["scores"] = prediction["iou_predictions"] - prediction["labels"] = label - processed_prediction = self.model["decoder"].postprocess(prediction, meta) - outputs.append(processed_prediction) - - return self._customize_outputs(outputs, inputs) - - def _customize_inputs( # type: ignore[override] - self, - entity: VisualPromptingBatchDataEntity, - ) -> tuple[list[np.ndarray], list[dict[str, Any]], list[list[dict[str, Any]]]]: - """Customize OTX input batch data entity.""" - images: list[np.ndarray] = [] - metas: list[dict[str, Any]] = [] - prompts: list[list[dict[str, Any]]] = [] - for image, bbox, point, label, imgs_info in zip( - entity.images, - entity.bboxes, - entity.points, - entity.labels, - entity.imgs_info, - ): - # preprocess image encoder inputs - numpy_image = image.cpu().numpy().transpose(1, 2, 0) - processed_image, meta = self.model["image_encoder"].preprocess(numpy_image) - images.append(processed_image) - metas.append(meta) - - # preprocess decoder inputs - processed_prompts = self.model["decoder"].preprocess( - { - "bboxes": bbox.cpu().numpy() if bbox is not None else bbox, - "points": point.cpu().numpy() if point is not None else point, - "labels": {k: v.cpu().numpy() for k, v in label.items()}, - "orig_size": imgs_info.ori_shape, - }, - ) - prompts.append(processed_prompts) - - return images, metas, prompts - - def _customize_outputs( - self, - outputs: Any, # noqa: ANN401 - inputs: VisualPromptingBatchDataEntity, # type: ignore[override] - ) -> VisualPromptingBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Customize OTX output batch data entity if needed for model.""" - masks: list[tv_tensors.Mask] = [] - scores: list[torch.Tensor] = [] - for output in outputs: - masks.append(torch.as_tensor(output["hard_prediction"])) - scores.append(torch.as_tensor(output["scores"])) - - return VisualPromptingBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=[torch.cat(scores, dim=0)], - masks=[tv_tensors.Mask(torch.cat(masks, dim=0))], - polygons=[], - points=[], - bboxes=[], - labels=[torch.cat(list(labels.values())) for labels in inputs.labels], - ) - - def optimize( # type: ignore[override] - self, - output_dir: Path, - data_module: OTXDataModule, - ptq_config: dict[str, Any] | None = None, - ) -> dict[str, Path]: - """Runs NNCF quantization.""" - import nncf - import openvino - - def check_if_quantized(model: openvino.Model) -> bool: - """Checks if OpenVINO model is already quantized.""" - nodes = model.get_ops() - return any(op.get_type_name() == "FakeQuantize" for op in nodes) - - def transform_fn( - data_batch: VisualPromptingBatchDataEntity | ZeroShotVisualPromptingBatchDataEntity, - module: Literal["image_encoder", "decoder"], - ) -> np.ndarray | dict[str, Any]: - images, _, prompts = self._customize_inputs(data_batch) # type: ignore[arg-type] - - image = images[0]["images"] # use only the first image - if module == "image_encoder": - # resize - resized_image = self.model["image_encoder"].resize( - image[0], - (self.model["image_encoder"].w, self.model["image_encoder"].h), - ) - - # pad image if necessary because `fit_to_window` resize for python in modelapi doesn't support pad - pad_w = max(0, self.model["image_encoder"].w - resized_image.shape[1]) - pad_h = max(0, self.model["image_encoder"].h - resized_image.shape[0]) - resized_image = np.pad( - resized_image, - ((0, pad_h), (0, pad_w), (0, 0)), - mode="constant", - constant_values=0, - ) - - # normalization - resized_image = self.model["image_encoder"].input_transform(resized_image) - - # change layout from HWC to NCHW - return self.model["image_encoder"]._change_layout(resized_image) # noqa: SLF001 - - # obtain image embeddings from image encoder - image_embeddings = self.model["image_encoder"].infer_sync(image) - # use only the first prompt - prompt_for_optim = next(iter(prompts[0].values()))[0] if isinstance(prompts[0], dict) else prompts[0][0] # type: ignore[attr-defined] - prompt_for_optim.pop("label") - prompt_for_optim.update(**image_embeddings) - return prompt_for_optim - - output_model_paths: dict[str, Path] = {} - for module in ["image_encoder", "decoder"]: - output_model_path = output_dir / (self._OPTIMIZED_MODEL_BASE_NAME + f"_{module}.xml") - - ov_model = openvino.Core().read_model(self.model_names[module]) - if check_if_quantized(ov_model): - msg = "Model is already optimized by PTQ" - raise RuntimeError(msg) - - train_dataset = data_module.train_dataloader() - - ptq_config_from_ir = self._read_ptq_config_from_ir(ov_model) - if ptq_config is not None: - ptq_config_from_ir.update(ptq_config) - ptq_config = ptq_config_from_ir - else: - ptq_config = ptq_config_from_ir - - quantization_dataset = nncf.Dataset(train_dataset, partial(transform_fn, module=module)) # type: ignore[attr-defined] - - compressed_model = nncf.quantize( # type: ignore[attr-defined] - ov_model, - quantization_dataset, - **ptq_config, - ) - - openvino.save_model(compressed_model, output_model_path) - output_model_paths[module] = output_model_path - - return output_model_paths - - -class OVZeroShotVisualPromptingModel(OVVisualPromptingModel): - """Zero-shot visual prompting model compatible for OpenVINO IR inference. - - It can only consume OpenVINO IR model path and create the OTX zero-shot visual prompting model compatible - for OTX testing pipeline. - """ - - def __init__( - self, - num_classes: int, - model_name: str, - model_type: str = "Zero_Shot_Visual_Prompting", - async_inference: bool = False, - max_num_requests: int | None = None, - use_throughput_mode: bool = True, - model_api_configuration: dict[str, Any] | None = None, - root_reference_info: str = "vpm_zsl_reference_infos", - save_outputs: bool = True, - ) -> None: - super().__init__( - num_classes, - model_name, - model_type, - async_inference, - max_num_requests, - use_throughput_mode, - model_api_configuration, - ) - self.root_reference_info: Path = Path(root_reference_info) - self.save_outputs: bool = save_outputs - - self.point_labels_box = np.array([[2, 3]], dtype=np.float32) - self.has_mask_inputs = [np.array([[0.0]]), np.array([[1.0]])] - - self.initialize_reference_info() - - def learn( - self, - inputs: ZeroShotVisualPromptingBatchDataEntity, - reset_feat: bool = False, - default_threshold_reference: float = 0.3, - ) -> tuple[dict[str, np.ndarray], list[np.ndarray]]: - """`Learn` for reference features.""" - if reset_feat or self.reference_feats is None: - self.initialize_reference_info() - - images, metas, processed_prompts = self._customize_inputs(inputs) - largest_label: int = max(sum([[int(p) for p in prompt] for prompt in processed_prompts], [])) - self.expand_reference_info(largest_label) - - reference_masks: list[np.ndarray] = [] - for image, meta, prompts in zip(images, metas, processed_prompts): - original_shape = np.array(meta["original_shape"][:2]) - - # forward image encoder - image_embeddings = self.model["image_encoder"].infer_sync(image) - processed_embedding = image_embeddings["image_embeddings"].squeeze().transpose(1, 2, 0) - - # get reference masks - ref_masks: np.ndarray = np.zeros((largest_label + 1, *original_shape), dtype=np.uint8) - for label, input_prompts in prompts.items(): - ref_mask: np.ndarray = np.zeros(original_shape, dtype=np.uint8) - for inputs_decoder in input_prompts: - label = inputs_decoder.pop("label") # noqa: PLW2901 - if "point_coords" in inputs_decoder: - # bboxes and points - inputs_decoder.update(image_embeddings) - prediction = self._predict_masks(inputs_decoder, original_shape, is_cascade=False) - masks = prediction["upscaled_masks"] - else: - log.warning("annotation and polygon will be supported.") - continue - ref_mask[masks] += 1 - ref_mask = np.clip(ref_mask, 0, 1) - - ref_feat: np.ndarray | None = None - cur_default_threshold_reference = deepcopy(default_threshold_reference) - while ref_feat is None: - log.info(f"[*] default_threshold_reference : {cur_default_threshold_reference:.4f}") - ref_feat = self._generate_masked_features( - feats=processed_embedding, - masks=ref_mask, - threshold_mask=cur_default_threshold_reference, - image_size=self.model["image_encoder"].image_size, - ) - cur_default_threshold_reference -= 0.05 - - self.reference_feats[label] = ref_feat - self.used_indices: np.ndarray = np.concatenate((self.used_indices, label)) - ref_masks[label] = ref_mask - reference_masks.append(ref_masks) - self.used_indices = np.unique(self.used_indices) - return {"reference_feats": self.reference_feats, "used_indices": self.used_indices}, reference_masks - - def infer( - self, - inputs: ZeroShotVisualPromptingBatchDataEntity, - reference_feats: np.ndarray, - used_indices: np.ndarray, - is_cascade: bool = False, - threshold: float = 0.0, - num_bg_points: int = 1, - default_threshold_target: float = 0.65, - image_size: int = 1024, - downsizing: int = 64, - ) -> list[list[defaultdict[int, list]]]: - """`Infer` for target predictions.""" - images, metas, _ = self._customize_inputs(inputs) - total_results: list[list[defaultdict[int, list]]] = [] - for image, meta in zip(images, metas): - original_shape = np.array(meta["original_shape"][:2]) - - # forward image encoder - image_embeddings = self.model["image_encoder"].infer_sync(image) - - # get point candidates - total_points_scores, total_bg_coords = self._get_prompt_candidates( - image_embeddings=image_embeddings["image_embeddings"], - reference_feats=reference_feats, - used_indices=used_indices, - original_shape=original_shape, - threshold=threshold, - num_bg_points=num_bg_points, - default_threshold_target=default_threshold_target, - image_size=image_size, - downsizing=downsizing, - ) - - predicted_masks: defaultdict[int, list] = defaultdict(list) - used_points: defaultdict[int, list] = defaultdict(list) - for label in total_points_scores: - points_scores = total_points_scores[label] - bg_coords = total_bg_coords[label] - for points_score in points_scores: - if points_score[-1] in [-1.0, 0.0]: - continue - - x, y = points_score[:2] - is_done = False - for pm in predicted_masks.get(label, []): - # check if that point is already assigned - if pm[int(y), int(x)] > 0: - is_done = True - break - if is_done: - continue - - point_coords = np.concatenate((np.array([[x, y]]), bg_coords), axis=0, dtype=np.float32) - point_coords = self.model["decoder"].apply_coords(point_coords, original_shape) - point_labels = np.array([1] + [0] * len(bg_coords), dtype=np.float32) - inputs_decoder = { - "point_coords": point_coords[None], - "point_labels": point_labels[None], - "orig_size": original_shape[None], - } - inputs_decoder.update(image_embeddings) - - prediction = self._predict_masks(inputs_decoder, original_shape, is_cascade) - prediction.update({"scores": points_score[-1]}) - - predicted_masks[label].append(prediction[self.model["decoder"].output_blob_name]) - used_points[label].append(points_score) - - # check overlapping area between different label masks - self._inspect_overlapping_areas(predicted_masks, used_points) - total_results.append([predicted_masks, used_points]) - return total_results - - def forward( # type: ignore[override] - self, - inputs: ZeroShotVisualPromptingBatchDataEntity, # type: ignore[override] - ) -> ZeroShotVisualPromptingBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Model forward function.""" - kwargs: dict[str, Any] = {} - fn = self.learn if self.training else self.infer - if not self.training: - kwargs.update( - { - "reference_feats": self.reference_feats, - "used_indices": self.used_indices, - }, - ) - - if self.async_inference: - log.warning( - ( - "Async inference is not supported for visual prompting models yet. " - "Running synchronous inference instead.", - ), - ) - - return self._customize_outputs(fn(inputs, **kwargs), inputs) # type: ignore[operator] - - def _customize_inputs( # type: ignore[override] - self, - entity: ZeroShotVisualPromptingBatchDataEntity, # type: ignore[override] - ) -> tuple[list[np.ndarray], list[dict[str, Any]], list[dict[int, list[Any]]]]: - """Customize OTX input batch data entity.""" - images: list[np.ndarray] = [] - metas: list[dict[str, Any]] = [] - processed_prompts: list[list[dict[str, Any]]] = [] - for image, prompts, labels, imgs_info in zip( - entity.images, - entity.prompts, - entity.labels, - entity.imgs_info, - ): - # preprocess image encoder inputs - numpy_image = image.cpu().numpy().transpose(1, 2, 0) - processed_image, meta = self.model["image_encoder"].preprocess(numpy_image) - images.append(processed_image) - metas.append(meta) - - if self.training: - points: list[np.ndarray] = [] - bboxes: list[np.ndarray] = [] - _labels: dict[str, list[int]] = defaultdict(list) - for prompt, label in zip(prompts, labels): - if isinstance(prompt, tv_tensors.BoundingBoxes): - bboxes.append(prompt.cpu().numpy()) - _labels["bboxes"].append(label.cpu().numpy()) - elif isinstance(prompt, Points): - points.append(prompt.cpu().numpy()) - _labels["points"].append(label.cpu().numpy()) - - # preprocess decoder inputs - processed_prompts.append( - self.model["decoder"].preprocess( - { - "bboxes": bboxes, - "points": points, - "labels": _labels, - "orig_size": imgs_info.ori_shape, - }, - ), - ) - processed_prompts_w_labels = self._gather_prompts_with_labels(processed_prompts) - return images, metas, processed_prompts_w_labels - - def _customize_outputs( # type: ignore[override] - self, - outputs: Any, # noqa: ANN401 - inputs: ZeroShotVisualPromptingBatchDataEntity, # type: ignore[override] - ) -> ZeroShotVisualPromptingBatchPredEntity | T_OTXBatchPredEntityWithXAI | OTXBatchLossEntity: - """Customize OTX output batch data entity if needed for model.""" - if self.training: - return outputs - - masks: list[tv_tensors.Mask] = [] - prompts: list[Points] = [] - scores: list[torch.Tensor] = [] - labels: list[torch.LongTensor] = [] - for output in outputs: - predicted_masks, used_points = output - for label, predicted_mask in predicted_masks.items(): - if len(predicted_mask) == 0: - continue - masks.append( - tv_tensors.Mask( - torch.stack([torch.as_tensor(m) for m in predicted_mask], dim=0), - dtype=torch.float32, - ), - ) - prompts.append( - Points( - torch.stack([torch.as_tensor(p[:2]) for p in used_points[label]], dim=0), - canvas_size=inputs.imgs_info[0].ori_shape, - dtype=torch.float32, - ), - ) - scores.append(torch.stack([torch.as_tensor(p[2]) for p in used_points[label]], dim=0)) - labels.append(torch.stack([torch.LongTensor([label]) for _ in range(len(scores[-1]))], dim=0)) - - return ZeroShotVisualPromptingBatchPredEntity( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - scores=scores, - prompts=prompts, - masks=masks, - polygons=[], - labels=labels, - ) - - ###################################### - # Preprocess # - ###################################### - def _gather_prompts_with_labels( - self, - batch_prompts: list[list[dict[str, Any]]], - ) -> list[dict[int, list[np.ndarray]]]: - """Gather prompts according to labels.""" - total_processed_prompts: list[dict[int, list[np.ndarray]]] = [] - for prompts in batch_prompts: - processed_prompts: defaultdict[int, list[np.ndarray]] = defaultdict(list) - for prompt in prompts: - processed_prompts[int(prompt["label"])].append(prompt) - total_processed_prompts.append(dict(sorted(processed_prompts.items(), key=lambda x: x))) - return total_processed_prompts - - ###################################### - # Common # - ###################################### - def _predict_masks( - self, - inputs: dict[str, np.ndarray], - original_size: np.ndarray, - is_cascade: bool = False, - ) -> dict[str, np.ndarray]: - """Process function of OpenVINO Visual Prompting Inferencer.""" - masks: np.ndarray - logits: np.ndarray - scores: np.ndarray - num_iter = 3 if is_cascade else 1 - for i in range(num_iter): - if i == 0: - # First-step prediction - mask_input = np.zeros( - (1, 1, *(x * 4 for x in inputs["image_embeddings"].shape[2:])), - dtype=np.float32, - ) - has_mask_input = self.has_mask_inputs[0] - - elif i == 1: - # Cascaded Post-refinement-1 - mask_input, masks = self._decide_masks(masks, logits, scores, is_single=True) # noqa: F821 - if masks.sum() == 0: - return {"upscaled_masks": masks} - - has_mask_input = self.has_mask_inputs[1] - - elif i == 2: - # Cascaded Post-refinement-2 - mask_input, masks = self._decide_masks(masks, logits, scores) # noqa: F821 - if masks.sum() == 0: - return {"upscaled_masks": masks} - - has_mask_input = self.has_mask_inputs[1] - y, x = np.nonzero(masks) - box_coords = self.model["decoder"].apply_coords( - np.array([[[x.min(), y.min()], [x.max(), y.max()]]], dtype=np.float32), - original_size[0], - ) - inputs.update( - { - "point_coords": np.concatenate((inputs["point_coords"], box_coords), axis=1), - "point_labels": np.concatenate((inputs["point_labels"], self.point_labels_box), axis=1), - }, - ) - - inputs.update({"mask_input": mask_input, "has_mask_input": has_mask_input}) - prediction = self.model["decoder"].infer_sync(inputs) - upscaled_masks, scores, logits = ( - prediction["upscaled_masks"], - prediction["iou_predictions"], - prediction["low_res_masks"], - ) - masks = upscaled_masks > self.model["decoder"].mask_threshold - - _, masks = self._decide_masks(masks, logits, scores) - return {"upscaled_masks": masks} - - def _decide_masks( - self, - masks: np.ndarray, - logits: np.ndarray, - scores: np.ndarray, - is_single: bool = False, - ) -> tuple[np.ndarray, ...]: - """Post-process logits for resized masks according to best index based on scores.""" - if is_single: - best_idx = 0 - else: - # skip the first index components - scores, masks, logits = (x[:, 1:] for x in (scores, masks, logits)) - - # filter zero masks - while len(scores[0]) > 0 and masks[0, (best_idx := np.argmax(scores[0]))].sum() == 0: - scores, masks, logits = ( - np.concatenate((x[:, :best_idx], x[:, best_idx + 1 :]), axis=1) for x in (scores, masks, logits) - ) - - if len(scores[0]) == 0: - # all predicted masks were zero masks, ignore them. - return None, np.zeros(masks.shape[-2:]) - - best_idx = np.argmax(scores[0]) - return logits[:, [best_idx]], masks[0, best_idx] - - ###################################### - # Learn # - ###################################### - def initialize_reference_info(self) -> None: - """Initialize reference information.""" - self.reference_feats = np.zeros((0, 1, self.model["decoder"].embed_dim), dtype=np.float32) - self.used_indices = np.array([], dtype=np.int64) - - def expand_reference_info(self, new_largest_label: int) -> None: - """Expand reference info dimensions if newly given processed prompts have more lables.""" - if new_largest_label > (cur_largest_label := len(self.reference_feats) - 1): - diff = new_largest_label - cur_largest_label - self.reference_feats = np.pad(self.reference_feats, ((0, diff), (0, 0), (0, 0)), constant_values=0.0) - - def _generate_masked_features( - self, - feats: np.ndarray, - masks: np.ndarray, - threshold_mask: float, - image_size: int = 1024, - ) -> tuple[np.ndarray, ...] | None: - """Generate masked features. - - Args: - feats (np.ndarray): Raw reference features. It will be filtered with masks. - masks (np.ndarray): Reference masks used to filter features. - threshold_mask (float): Threshold to control masked region. - image_size (int): Input image size. - - Returns: - (np.ndarray): Masked features. - """ - target_shape = image_size / max(masks.shape) * np.array(masks.shape) - target_shape = target_shape[::-1].astype(np.int32) - - # Post-process masks - masks = cv2.resize(masks, target_shape, interpolation=cv2.INTER_LINEAR) - masks = self._pad_to_square(masks, image_size) - masks = cv2.resize(masks, feats.shape[:2][::-1], interpolation=cv2.INTER_LINEAR) - - # Target feature extraction - if (masks > threshold_mask).sum() == 0: - # (for stability) there is no area to be extracted - return None - - masked_feat = feats[masks > threshold_mask] - masked_feat = masked_feat.mean(0)[None] - return masked_feat / np.linalg.norm(masked_feat, axis=-1, keepdims=True) - - def _pad_to_square(self, x: np.ndarray, image_size: int = 1024) -> np.ndarray: - """Pad to a square input. - - Args: - x (np.ndarray): Mask to be padded. - - Returns: - (np.ndarray): Padded mask. - """ - h, w = x.shape[-2:] - padh = image_size - h - padw = image_size - w - return np.pad(x, ((0, padh), (0, padw)), constant_values=0.0) - - ###################################### - # Infer # - ###################################### - def _find_latest_reference_info(self, root: Path) -> str | None: - """Find latest reference info to be used.""" - if not Path.is_dir(root): - return None - if len(stamps := sorted(os.listdir(root), reverse=True)) > 0: - return stamps[0] - return None - - def load_latest_reference_info(self, *args, **kwargs) -> bool: - """Load latest reference info to be used.""" - if (latest_stamp := self._find_latest_reference_info(self.root_reference_info)) is not None: - latest_reference_info: Path = self.root_reference_info / latest_stamp / "reference_info.pickle" - reference_info: dict[str, np.ndarray] = pickle.load(Path.open(latest_reference_info, "rb")) # noqa: S301 - self.reference_feats = reference_info.get( - "reference_feats", - np.zeros((0, 1, self.model["decoder"].embed_dim), dtype=np.float32), - ) - self.used_indices = reference_info.get("used_indices", np.array([], dtype=np.int64)) - log.info(f"reference info saved at {latest_reference_info} was successfully loaded.") - return True - return False - - def _get_prompt_candidates( - self, - image_embeddings: np.ndarray, - reference_feats: np.ndarray, - used_indices: np.ndarray, - original_shape: np.ndarray, - threshold: float = 0.0, - num_bg_points: int = 1, - default_threshold_target: float = 0.65, - image_size: int = 1024, - downsizing: int = 64, - ) -> tuple[dict[int, np.ndarray], dict[int, np.ndarray]]: - """Get prompt candidates.""" - target_feat = image_embeddings.squeeze() - c_feat, h_feat, w_feat = target_feat.shape - target_feat = target_feat / np.linalg.norm(target_feat, axis=0, keepdims=True) - target_feat = target_feat.reshape(c_feat, h_feat * w_feat) - - total_points_scores: dict[int, np.ndarray] = {} - total_bg_coords: dict[int, np.ndarray] = {} - for label in used_indices: - sim = reference_feats[label] @ target_feat - sim = sim.reshape(h_feat, w_feat) - sim = self._resize_to_original_shape(sim, image_size, original_shape) - - threshold = (threshold == 0) * default_threshold_target + threshold - points_scores, bg_coords = self._point_selection( - mask_sim=sim, - original_shape=original_shape, - threshold=threshold, - num_bg_points=num_bg_points, - image_size=image_size, - downsizing=downsizing, - ) - - if points_scores is not None: - total_points_scores[label] = points_scores - total_bg_coords[label] = bg_coords - return total_points_scores, total_bg_coords - - def _point_selection( - self, - mask_sim: np.ndarray, - original_shape: np.ndarray, - threshold: float = 0.0, - num_bg_points: int = 1, - image_size: int = 1024, - downsizing: int = 64, - ) -> tuple[np.ndarray, np.ndarray]: - """Select point used as point prompts.""" - _, w_sim = mask_sim.shape - - # Top-first point selection - point_coords = np.where(mask_sim > threshold) - fg_coords_scores = np.stack(point_coords[::-1] + (mask_sim[point_coords],), axis=0).T - - ## skip if there is no point coords - if len(fg_coords_scores) == 0: - return None, None - - ratio = image_size / original_shape.max() - width = (original_shape[1] * ratio).astype(np.int64) - n_w = width // downsizing - - ## get grid numbers - idx_grid = fg_coords_scores[:, 1] * ratio // downsizing * n_w + fg_coords_scores[:, 0] * ratio // downsizing - idx_grid_unique = np.unique(idx_grid.astype(np.int64)) - - ## get matched indices - matched_matrix = np.expand_dims(idx_grid, axis=-1) == idx_grid_unique # (totalN, uniqueN) - - ## sample fg_coords_scores matched by matched_matrix - matched_grid = np.expand_dims(fg_coords_scores, axis=1) * np.expand_dims(matched_matrix, axis=-1) - - ## sample the highest score one of the samples that are in the same grid - matched_indices = self._topk_numpy(matched_grid[..., -1], k=1, axis=0, largest=True)[1][0].astype(np.int64) - points_scores = matched_grid[matched_indices].diagonal().T - - ## sort by the highest score - sorted_points_scores_indices = np.flip(np.argsort(points_scores[:, -1]), axis=-1).astype(np.int64) - points_scores = points_scores[sorted_points_scores_indices] - - # Top-last point selection - bg_indices = self._topk_numpy(mask_sim.flatten(), num_bg_points, largest=False)[1] - bg_x = np.expand_dims(bg_indices // w_sim, axis=0) - bg_y = bg_indices - bg_x * w_sim - bg_coords = np.concatenate((bg_y, bg_x), axis=0).transpose(1, 0) - bg_coords = bg_coords.astype(np.float32) - - return points_scores, bg_coords - - def _resize_to_original_shape(self, masks: np.ndarray, image_size: int, original_shape: np.ndarray) -> np.ndarray: - """Resize feature size to original shape.""" - # resize feature size to input size - masks = cv2.resize(masks, (image_size, image_size), interpolation=cv2.INTER_LINEAR) - - # remove pad - prepadded_size = self._get_prepadded_size(original_shape, image_size) - masks = masks[..., : prepadded_size[0], : prepadded_size[1]] - - # resize unpadded one to original shape - original_shape = original_shape.astype(np.int64) - h, w = original_shape[0], original_shape[1] - return cv2.resize(masks, (w, h), interpolation=cv2.INTER_LINEAR) - - def _get_prepadded_size(self, original_shape: int, image_size: int) -> np.ndarray: - """Get pre-padded size.""" - scale = image_size / np.max(original_shape) - transformed_size = scale * original_shape - return np.floor(transformed_size + 0.5).astype(np.int64) - - def _inspect_overlapping_areas( - self, - predicted_masks: dict[int, list[np.ndarray]], - used_points: dict[int, list[np.ndarray]], - threshold_iou: float = 0.8, - ) -> None: - def _calculate_mask_iou(mask1: np.ndarray, mask2: np.ndarray) -> tuple[float, np.ndarray | None]: - assert mask1.ndim == 2 # noqa: S101 - assert mask2.ndim == 2 # noqa: S101 - # Avoid division by zero - if (union := np.logical_or(mask1, mask2).sum().item()) == 0: - return 0.0, None - intersection = np.logical_and(mask1, mask2) - return intersection.sum().item() / union, intersection - - for (label, masks), (other_label, other_masks) in product(predicted_masks.items(), predicted_masks.items()): - if other_label <= label: - continue - - overlapped_label = [] - overlapped_other_label = [] - for (im, mask), (jm, other_mask) in product(enumerate(masks), enumerate(other_masks)): - _mask_iou, _intersection = _calculate_mask_iou(mask, other_mask) - if _mask_iou > threshold_iou: - if used_points[label][im][2] > used_points[other_label][jm][2]: - overlapped_other_label.append(jm) - else: - overlapped_label.append(im) - elif _mask_iou > 0: - # refine the slightly overlapping region - overlapped_coords = np.where(_intersection) - if used_points[label][im][2] > used_points[other_label][jm][2]: - other_mask[overlapped_coords] = 0.0 - else: - mask[overlapped_coords] = 0.0 - - for im in sorted(set(overlapped_label), reverse=True): - masks.pop(im) - used_points[label].pop(im) - - for jm in sorted(set(overlapped_other_label), reverse=True): - other_masks.pop(jm) - used_points[other_label].pop(jm) - - def _topk_numpy(self, x: np.ndarray, k: int, axis: int = -1, largest: bool = True) -> np.ndarray: - """Top-k function for numpy same with torch.topk.""" - if largest: - k = -k - indices = range(k, 0) - else: - indices = range(k) - partitioned_ind = np.argpartition(x, k, axis=axis).take(indices=indices, axis=axis) - partitioned_scores = np.take_along_axis(x, partitioned_ind, axis=axis) - sorted_trunc_ind = np.argsort(partitioned_scores, axis=axis) - if largest: - sorted_trunc_ind = np.flip(sorted_trunc_ind, axis=axis) - ind = np.take_along_axis(partitioned_ind, sorted_trunc_ind, axis=axis) - scores = np.take_along_axis(partitioned_scores, sorted_trunc_ind, axis=axis) - return scores, ind - - def _reset_prediction_layer(self, num_classes: int) -> None: - return diff --git a/src/otx/core/model/module/__init__.py b/src/otx/core/model/module/__init__.py deleted file mode 100644 index a10543e4585..00000000000 --- a/src/otx/core/model/module/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for base lightning module classes used in OTX.""" diff --git a/src/otx/core/model/module/action_classification.py b/src/otx/core/model/module/action_classification.py deleted file mode 100644 index cd48a2e1913..00000000000 --- a/src/otx/core/model/module/action_classification.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for action classification lightning module used in OTX.""" -from __future__ import annotations - -from typing import TYPE_CHECKING - -import torch -from torch import Tensor -from torchmetrics import Metric -from torchmetrics.classification.accuracy import Accuracy - -from otx.core.data.entity.action_classification import ( - ActionClsBatchDataEntity, - ActionClsBatchPredEntity, -) -from otx.core.model.entity.action_classification import OTXActionClsModel -from otx.core.model.module.base import OTXLitModule - -if TYPE_CHECKING: - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - - from otx.core.metrics import MetricCallable - - -class OTXActionClsLitModule(OTXLitModule): - """Base class for the lightning module used in OTX detection task.""" - - def __init__( - self, - otx_model: OTXActionClsModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: MetricCallable = lambda: Accuracy(task="multiclass"), - ): - super().__init__( - otx_model=otx_model, - torch_compile=torch_compile, - optimizer=optimizer, - scheduler=scheduler, - metric=metric, - ) - - def _log_metrics(self, meter: Metric, key: str) -> None: - results = meter.compute() - if results is None: - msg = f"{meter} has no data to compute metric or there is an error computing metric" - raise RuntimeError(msg) - - self.log(f"{key}/accuracy", results.item(), sync_dist=True, prog_bar=True) - - def validation_step(self, inputs: ActionClsBatchDataEntity, batch_idx: int) -> None: - """Perform a single validation step on a batch of data from the validation set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, ActionClsBatchPredEntity): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) - - def _convert_pred_entity_to_compute_metric( - self, - preds: ActionClsBatchPredEntity, - inputs: ActionClsBatchDataEntity, - ) -> dict[str, list[dict[str, Tensor]]]: - pred = torch.tensor(preds.labels) - target = torch.tensor(inputs.labels) - return { - "preds": pred, - "target": target, - } - - def test_step(self, inputs: ActionClsBatchDataEntity, batch_idx: int) -> None: - """Perform a single test step on a batch of data from the test set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, ActionClsBatchPredEntity): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) diff --git a/src/otx/core/model/module/action_detection.py b/src/otx/core/model/module/action_detection.py deleted file mode 100644 index 040b500c4a0..00000000000 --- a/src/otx/core/model/module/action_detection.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for action detection lightning module used in OTX.""" -from __future__ import annotations - -import logging as log -from typing import TYPE_CHECKING - -import torch -from torch import Tensor -from torchmetrics import Metric -from torchmetrics.detection.mean_ap import MeanAveragePrecision - -from otx.core.data.entity.action_detection import ( - ActionDetBatchDataEntity, - ActionDetBatchPredEntity, -) -from otx.core.model.entity.action_detection import OTXActionDetModel -from otx.core.model.module.base import OTXLitModule - -if TYPE_CHECKING: - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - - from otx.core.metrics import MetricCallable - - -class OTXActionDetLitModule(OTXLitModule): - """Base class for the lightning module used in OTX detection task.""" - - def __init__( - self, - otx_model: OTXActionDetModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: MetricCallable = lambda: MeanAveragePrecision(), - ): - super().__init__( - otx_model=otx_model, - torch_compile=torch_compile, - optimizer=optimizer, - scheduler=scheduler, - metric=metric, - ) - - def _log_metrics(self, meter: Metric, key: str) -> None: - results = meter.compute() - if results is None: - msg = f"{meter} has no data to compute metric or there is an error computing metric" - raise RuntimeError(msg) - - for k, v in results.items(): - if not isinstance(v, Tensor): - log.debug("Cannot log item which is not Tensor") - continue - if v.numel() != 1: - log.debug("Cannot log Tensor which is not scalar") - continue - - self.log( - f"{key}/{k}", - v, - sync_dist=True, - prog_bar=True, - ) - - def validation_step(self, inputs: ActionDetBatchDataEntity, batch_idx: int) -> None: - """Perform a single validation step on a batch of data from the validation set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - inputs.labels = [label.argmax(-1) for label in inputs.labels] - - if not isinstance(preds, ActionDetBatchPredEntity): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) - - def _convert_pred_entity_to_compute_metric( - self, - preds: ActionDetBatchPredEntity, - inputs: ActionDetBatchDataEntity, - ) -> dict[str, list[dict[str, Tensor]]]: - return { - "preds": [ - { - "boxes": bboxes.data, - "scores": scores, - "labels": labels, - } - for bboxes, scores, labels in zip( - preds.bboxes, - preds.scores, - preds.labels, - ) - ], - "target": [ - { - "boxes": bboxes.data, - "labels": labels, - } - for bboxes, labels in zip(inputs.bboxes, inputs.labels) - ], - } - - def test_step(self, inputs: ActionDetBatchDataEntity, batch_idx: int) -> None: - """Perform a single test step on a batch of data from the test set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - inputs.labels = [label.argmax(-1) for label in inputs.labels] - - if not isinstance(preds, ActionDetBatchPredEntity): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) diff --git a/src/otx/core/model/module/anomaly/__init__.py b/src/otx/core/model/module/anomaly/__init__.py deleted file mode 100644 index 4da5b47d100..00000000000 --- a/src/otx/core/model/module/anomaly/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Anomaly models.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from .anomaly_lightning import OTXAnomaly - -__all__ = ["OTXAnomaly"] diff --git a/src/otx/core/model/module/anomaly/anomaly_lightning.py b/src/otx/core/model/module/anomaly/anomaly_lightning.py deleted file mode 100644 index 01fca867d92..00000000000 --- a/src/otx/core/model/module/anomaly/anomaly_lightning.py +++ /dev/null @@ -1,473 +0,0 @@ -"""Anomaly Lightning OTX model.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING, Any, TypeAlias - -import onnx -import openvino -import torch -from anomalib import TaskType as AnomalibTaskType -from anomalib.callbacks.metrics import _MetricsCallback -from anomalib.callbacks.normalization.min_max_normalization import _MinMaxNormalizationCallback -from anomalib.callbacks.post_processor import _PostProcessorCallback -from anomalib.callbacks.thresholding import _ThresholdCallback -from torch import nn - -from otx.core.data.dataset.base import LabelInfo -from otx.core.data.entity.anomaly import ( - AnomalyClassificationBatchPrediction, - AnomalyClassificationDataBatch, - AnomalyDetectionBatchPrediction, - AnomalyDetectionDataBatch, - AnomalySegmentationBatchPrediction, - AnomalySegmentationDataBatch, -) -from otx.core.exporter.base import OTXModelExporter -from otx.core.types.export import OTXExportFormatType -from otx.core.types.precision import OTXPrecisionType -from otx.core.types.task import OTXTaskType - -if TYPE_CHECKING: - from collections import OrderedDict - - from anomalib.metrics import AnomalibMetricCollection - from anomalib.metrics.threshold import BaseThreshold - from lightning.pytorch import Trainer - from lightning.pytorch.callbacks.callback import Callback - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - from lightning.pytorch.utilities.types import STEP_OUTPUT - from torchmetrics import Metric - from torchvision.transforms.v2 import Transform - - -AnomalyModelInputs: TypeAlias = ( - AnomalyClassificationDataBatch | AnomalySegmentationDataBatch | AnomalyDetectionDataBatch -) -AnomalyModelOutputs: TypeAlias = ( - AnomalyClassificationBatchPrediction | AnomalySegmentationBatchPrediction | AnomalyDetectionBatchPrediction -) - - -class _AnomalyModelExporter(OTXModelExporter): - def __init__( - self, - image_shape: tuple[int, int] = (256, 256), - image_threshold: float = 0.5, - pixel_threshold: float = 0.5, - task: AnomalibTaskType = AnomalibTaskType.CLASSIFICATION, - # the actual values for mean and scale should be in range 0-255 - mean_values: tuple[float, float, float] = (0.0, 0.0, 0.0), - scale_values: tuple[float, float, float] = (1.0, 1.0, 1.0), - normalization_scale: float = 1.0, - ) -> None: - self.orig_height, self.orig_width = image_shape - metadata = { - ("model_info", "image_threshold"): image_threshold, - ("model_info", "pixel_threshold"): pixel_threshold, - ("model_info", "normalization_scale"): normalization_scale, - ("model_info", "orig_height"): image_shape[0], - ("model_info", "orig_width"): image_shape[1], - ("model_info", "image_shape"): image_shape, - ("model_info", "labels"): "Normal Anomaly", - ("model_info", "model_type"): "AnomalyDetection", - ("model_info", "task"): task.value, - } - super().__init__( - input_size=(1, 3, *image_shape), - mean=mean_values, - std=scale_values, - swap_rgb=False, # default value. Ideally, modelAPI should pass RGB inputs after the pre-processing step - metadata=metadata, - ) - - def to_openvino( - self, - model: nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> Path: - save_path = str(output_dir / f"{base_model_name}.xml") - exported_model = openvino.convert_model( - input_model=model, - example_input=torch.rand(self.input_size), - input=(openvino.runtime.PartialShape(self.input_size)), - ) - exported_model = self._postprocess_openvino_model(exported_model) - openvino.save_model(exported_model, save_path, compress_to_fp16=(precision == OTXPrecisionType.FP16)) - return Path(save_path) - - def to_onnx( - self, - model: nn.Module, - output_dir: Path, - base_model_name: str = "exported_model", - precision: OTXPrecisionType = OTXPrecisionType.FP32, - embed_metadata: bool = True, - ) -> Path: - save_path = str(output_dir / f"{base_model_name}.onnx") - torch.onnx.export( - model=model, - args=(torch.rand(1, 3, self.orig_height, self.orig_width)).to( - next(model.parameters()).device, - ), - f=save_path, - opset_version=14, - dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, - input_names=["input"], - output_names=["output"], - ) - onnx_model = onnx.load(save_path) - onnx_model = self._postprocess_onnx_model(onnx_model, embed_metadata, precision) - onnx.save(onnx_model, save_path) - return Path(save_path) - - -class OTXAnomaly: - """Methods used to make OTX model compatible with the Anomalib model.""" - - def __init__(self) -> None: - self.optimizer: list[OptimizerCallable] | OptimizerCallable = None - self.scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = None - self._input_size: tuple[int, int] = (256, 256) - self.mean_values: tuple[float, float, float] = (0.0, 0.0, 0.0) - self.scale_values: tuple[float, float, float] = (1.0, 1.0, 1.0) - self.trainer: Trainer - self.model: nn.Module - self.image_threshold: BaseThreshold - self.pixel_threshold: BaseThreshold - - self.normalization_metrics: Metric - - self.image_metrics: AnomalibMetricCollection - self.pixel_metrics: AnomalibMetricCollection - - @property - def input_size(self) -> tuple[int, int]: - """Returns the input size of the model. - - Returns: - tuple[int, int]: The input size of the model as a tuple of (height, width). - """ - return self._input_size - - @input_size.setter - def input_size(self, value: tuple[int, int]) -> None: - self._input_size = value - - @property - def task(self) -> AnomalibTaskType: - """Return the task type of the model.""" - if self._task_type: - return self._task_type - msg = "``self._task_type`` is not assigned" - raise AttributeError(msg) - - @task.setter - def task(self, value: OTXTaskType) -> None: - if value == OTXTaskType.ANOMALY_CLASSIFICATION: - self._task_type = AnomalibTaskType.CLASSIFICATION - elif value == OTXTaskType.ANOMALY_DETECTION: - self._task_type = AnomalibTaskType.DETECTION - elif value == OTXTaskType.ANOMALY_SEGMENTATION: - self._task_type = AnomalibTaskType.SEGMENTATION - else: - msg = f"Unexpected task type: {value}" - raise ValueError(msg) - - @property - def label_info(self) -> LabelInfo: - """Get this model label information.""" - return self._label_info - - @label_info.setter - def label_info(self, value: LabelInfo | list[str]) -> None: - """Set this model label information. - - It changes the number of classes to 2 and sets the labels as Normal and Anomaly. - This is because Datumaro returns multiple classes from the dataset. If self.label_info != 2, - then It will call self._reset_prediction_layer() to reset the prediction layer. Which is not required. - - This overrides the OTXModel's label_info setter. - """ - if isinstance(value, list): - # value can be greater than 2 as datumaro returns all anomalous categories separately - self._label_info = LabelInfo(label_names=["Normal", "Anomaly"], label_groups=[["Normal", "Anomaly"]]) - else: - self._label_info = value - - def _extract_mean_scale_from_transforms(self, transforms: list[Transform]) -> None: - """Extract mean and scale values from transforms.""" - for transform in transforms: - name = transform.__class__.__name__ - if "Resize" in name: - self.input_size = transform.size * 2 # transform.size has value [size], so *2 gives (size, size) - elif "Normalize" in name: - self.mean_values = transform.mean - self.scale_values = transform.std - - @property - def trainable_model(self) -> str | None: - """Use this to return the name of the model that needs to be trained. - - This might not be the cleanest solution. - - Some models have multiple architectures and only one of them needs to be trained. - However the optimizer is configured in the Anomalib's lightning model. This can be used - to inform the OTX lightning model which model to train. - """ - return None - - def setup(self, stage: str | None = None) -> None: - """Setup the model.""" - super().setup(stage) # type: ignore[misc] - if hasattr(self.trainer, "datamodule") and hasattr(self.trainer.datamodule, "config"): - if hasattr(self.trainer.datamodule.config, "test_subset"): - self._extract_mean_scale_from_transforms(self.trainer.datamodule.config.test_subset.transforms) - elif hasattr(self.trainer.datamodule.config, "val_subset"): - self._extract_mean_scale_from_transforms(self.trainer.datamodule.config.val_subset.transforms) - - def configure_callbacks(self) -> list[Callback]: - """Get all necessary callbacks required for training and post-processing on Anomalib models.""" - image_metrics = ["AUROC", "F1Score"] - pixel_metrics = image_metrics if self.task != AnomalibTaskType.CLASSIFICATION else None - return [ - _PostProcessorCallback(), - _MinMaxNormalizationCallback(), # ModelAPI only supports min-max normalization as of now - _ThresholdCallback(threshold="F1AdaptiveThreshold"), - _MetricsCallback( - task=self.task, - image_metrics=image_metrics, - pixel_metrics=pixel_metrics, - ), - ] - - def training_step( - self, - inputs: AnomalyModelInputs | dict, - batch_idx: int = 0, - ) -> STEP_OUTPUT: - """Call training step of the anomalib model.""" - if not isinstance(inputs, dict): - inputs = self._customize_inputs(inputs) - return super().training_step(inputs, batch_idx) # type: ignore[misc] - - def validation_step( - self, - inputs: AnomalyModelInputs | dict, - batch_idx: int = 0, - ) -> STEP_OUTPUT: - """Call validation step of the anomalib model.""" - if not isinstance(inputs, dict): - inputs = self._customize_inputs(inputs) - return super().validation_step(inputs, batch_idx) # type: ignore[misc] - - def test_step( - self, - inputs: AnomalyModelInputs | dict, - batch_idx: int = 0, - **kwargs, - ) -> STEP_OUTPUT: - """Call test step of the anomalib model.""" - if not isinstance(inputs, dict): - inputs = self._customize_inputs(inputs) - return super().test_step(inputs, batch_idx, **kwargs) # type: ignore[misc] - - def on_test_batch_end( - self, - outputs: dict, - batch: AnomalyModelInputs | dict, - batch_idx: int, - dataloader_idx: int = 0, - ) -> None: - """Called in the predict loop after the batch. - - Args: - outputs: The outputs of predict_step(x) - batch: The batched data as it is returned by the prediction DataLoader. - batch_idx: the index of the batch - dataloader_idx: the index of the dataloader - - """ - if not isinstance(batch, dict): - batch = self._customize_inputs(batch) - super().on_test_batch_end(outputs, batch, batch_idx, dataloader_idx) # type: ignore[misc] - - def predict_step( - self, - inputs: AnomalyModelInputs | dict, - batch_idx: int = 0, - **kwargs, - ) -> dict: - """Return predictions from the anomalib model.""" - if not isinstance(inputs, dict): - inputs = self._customize_inputs(inputs) - return super().predict_step(inputs, batch_idx, **kwargs) # type: ignore[misc] - - def on_predict_batch_end( - self, - outputs: dict, - batch: AnomalyModelInputs, - batch_idx: int, - dataloader_idx: int = 0, - ) -> None: - """Wrap the outputs to OTX format. - - Since outputs need to be replaced inplace, we can't change the datatype of outputs. - That's why outputs is cleared and replaced with the new outputs. The problem with this is that - Instead of ``engine.test()`` returning [BatchPrediction,...], it returns - [{prediction: BatchPrediction}, {...}, ...] - """ - _outputs = self._customize_outputs(outputs, batch) - outputs.clear() - outputs.update({"prediction": _outputs}) - - def configure_optimizers(self) -> tuple[list[torch.optim.Optimizer], list[torch.optim.Optimizer]] | None: # type: ignore[override] - """Configure optimizers for Anomalib models. - - If the anomalib lightning model supports optimizers, return the optimizer. - If ``self.trainable_model`` is None then the model does not support training. - Else don't return optimizer even if it is configured in the OTX model. - """ - # [TODO](ashwinvaidya17): Revisit this method - if self.optimizer and self.trainable_model: - optimizer = self.optimizer - if isinstance(optimizer, list): - if len(optimizer) > 1: - msg = "Only one optimizer should be passed" - raise ValueError(msg) - optimizer = optimizer[0] - params = getattr(self.model, self.trainable_model).parameters() - return optimizer(params=params) - return super().configure_optimizers() # type: ignore[misc] - - def state_dict(self) -> dict[str, Any]: - """Return state dictionary of model entity with meta information. - - Returns: - A dictionary containing datamodule state. - - """ - state_dict = super().state_dict() # type: ignore[misc] - # This is defined in OTXModel - state_dict["label_info"] = self.label_info # type: ignore[attr-defined] - return state_dict - - def load_state_dict(self, ckpt: OrderedDict[str, Any], *args, **kwargs) -> None: - """Pass the checkpoint to the anomaly model.""" - ckpt = ckpt.get("state_dict", ckpt) - ckpt.pop("label_info", None) # [TODO](ashwinvaidya17): Revisit this method when OTXModel is the lightning model - return super().load_state_dict(ckpt, *args, **kwargs) # type: ignore[misc] - - def forward( - self, - inputs: AnomalyModelInputs, - ) -> AnomalyModelOutputs: - """Wrap forward method of the Anomalib model.""" - _inputs: dict = self._customize_inputs(inputs) - outputs = self.model.model.forward(_inputs) - return self._customize_outputs(outputs=outputs, inputs=inputs) - - def _customize_inputs( - self, - inputs: AnomalyModelInputs, - ) -> dict[str, Any]: - """Customize inputs for the model.""" - if isinstance(inputs, AnomalyClassificationDataBatch): - return {"image": inputs.images, "label": torch.vstack(inputs.labels).squeeze()} - if isinstance(inputs, AnomalySegmentationDataBatch): - return {"image": inputs.images, "label": torch.vstack(inputs.labels).squeeze(), "mask": inputs.masks} - if isinstance(inputs, AnomalyDetectionDataBatch): - return { - "image": inputs.images, - "label": torch.vstack(inputs.labels).squeeze(), - "mask": inputs.masks, - "boxes": inputs.boxes, - } - msg = f"Unsupported input type {type(inputs)}" - raise ValueError(msg) - - def _customize_outputs( - self, - outputs: dict, - inputs: AnomalyModelInputs, - ) -> AnomalyModelOutputs: - if self.task == AnomalibTaskType.CLASSIFICATION: - return AnomalyClassificationBatchPrediction( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - labels=outputs["label"], - # Note: this is the anomalous score. It should be inverted to report Normal score - scores=outputs["pred_scores"], - anomaly_maps=outputs["anomaly_maps"], - ) - if self.task == AnomalibTaskType.SEGMENTATION: - return AnomalySegmentationBatchPrediction( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - labels=outputs["label"], - # Note: this is the anomalous score. It should be inverted to report Normal score - scores=outputs["pred_scores"], - anomaly_maps=outputs["anomaly_maps"], - masks=outputs["mask"], - ) - if self.task == AnomalibTaskType.DETECTION: - return AnomalyDetectionBatchPrediction( - batch_size=len(outputs), - images=inputs.images, - imgs_info=inputs.imgs_info, - labels=outputs["label"], - # Note: this is the anomalous score. It should be inverted to report Normal score - scores=outputs["pred_scores"], - anomaly_maps=outputs["anomaly_maps"], - masks=outputs["mask"], - boxes=outputs["pred_boxes"], - box_scores=outputs["box_scores"], - box_labels=outputs["box_labels"], - ) - msg = f"Unsupported task type {self.task}" - raise ValueError(msg) - - def export( - self, - output_dir: Path, - base_name: str, - export_format: OTXExportFormatType, - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> Path: - """Export this model to the specified output directory. - - Args: - output_dir (Path): directory for saving the exported model - base_name: (str): base name for the exported model file. Extension is defined by the target export format - export_format (OTXExportFormatType): format of the output model - precision (OTXExportPrecisionType): precision of the output model - Returns: - Path: path to the exported model. - """ - min_val = self.normalization_metrics.state_dict()["min"].cpu().numpy().tolist() - max_val = self.normalization_metrics.state_dict()["max"].cpu().numpy().tolist() - image_shape = (256, 256) if self.input_size is None else self.input_size - exporter = _AnomalyModelExporter( - image_shape=image_shape, - image_threshold=self.image_threshold.value.cpu().numpy().tolist(), - pixel_threshold=self.pixel_threshold.value.cpu().numpy().tolist(), - task=self.task, - mean_values=self.mean_values, - scale_values=self.scale_values, - normalization_scale=max_val - min_val, - ) - return exporter.export( - model=self.model, - output_dir=output_dir, - base_model_name=base_name, - export_format=export_format, - precision=precision, - ) diff --git a/src/otx/core/model/module/base.py b/src/otx/core/model/module/base.py deleted file mode 100644 index c573cad2cbd..00000000000 --- a/src/otx/core/model/module/base.py +++ /dev/null @@ -1,274 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for base lightning module used in OTX.""" -from __future__ import annotations - -import inspect -import logging -import warnings -from functools import partial -from typing import TYPE_CHECKING, Any - -import torch -from lightning import LightningModule -from torch import Tensor -from torchmetrics import Metric - -from otx.core.data.entity.base import ( - OTXBatchDataEntity, - OTXBatchLossEntity, - OTXBatchPredEntity, -) -from otx.core.model.entity.base import OTXModel, OVModel -from otx.core.types.export import OTXExportFormatType -from otx.core.types.precision import OTXPrecisionType -from otx.core.utils.utils import is_ckpt_for_finetuning, is_ckpt_from_otx_v1 - -if TYPE_CHECKING: - from pathlib import Path - - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - - from otx.core.data.dataset.base import LabelInfo - from otx.core.metrics import MetricCallable - - -class OTXLitModule(LightningModule): - """Base class for the lightning module used in OTX.""" - - def __init__( - self, - *, - otx_model: OTXModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: MetricCallable = lambda: Metric(), - ): - super().__init__() - - self.model = otx_model - self.optimizer = optimizer - self.scheduler = scheduler - self.torch_compile = torch_compile - self.metric_callable = metric - - # this line allows to access init params with 'self.hparams' attribute - # also ensures init params will be stored in ckpt - self.save_hyperparameters(logger=False, ignore=["otx_model"]) - - def training_step(self, inputs: OTXBatchDataEntity, batch_idx: int) -> Tensor: - """Step for model training.""" - train_loss = self.model(inputs) - - if isinstance(train_loss, Tensor): - self.log( - "train/loss", - train_loss, - on_step=True, - on_epoch=False, - prog_bar=True, - ) - return train_loss - if isinstance(train_loss, dict): - for k, v in train_loss.items(): - self.log( - f"train/{k}", - v, - on_step=True, - on_epoch=False, - prog_bar=True, - ) - - total_train_loss = sum(train_loss.values()) - self.log( - "train/loss", - total_train_loss, - on_step=True, - on_epoch=False, - prog_bar=True, - ) - return total_train_loss - - raise TypeError(train_loss) - - def on_validation_start(self) -> None: - """Called at the beginning of validation.""" - self.configure_metric() - - def on_test_start(self) -> None: - """Called at the beginning of testing.""" - self.configure_metric() - - def on_validation_epoch_start(self) -> None: - """Callback triggered when the validation epoch starts.""" - if isinstance(self.metric, Metric): - self.metric.reset() - - def on_test_epoch_start(self) -> None: - """Callback triggered when the test epoch starts.""" - if isinstance(self.metric, Metric): - self.metric.reset() - - def on_validation_epoch_end(self) -> None: - """Callback triggered when the validation epoch ends.""" - self._log_metrics(self.metric, "val") - - def on_test_epoch_end(self) -> None: - """Callback triggered when the test epoch ends.""" - self._log_metrics(self.metric, "test") - - def setup(self, stage: str) -> None: - """Lightning hook that is called at the beginning of fit (train + validate), validate, test, or predict. - - This is a good hook when you need to build models dynamically or adjust something about - them. This hook is called on every process when using DDP. - - :param stage: Either `"fit"`, `"validate"`, `"test"`, or `"predict"`. - """ - if self.torch_compile and stage == "fit": - self.model = torch.compile(self.model) - - self.model.setup_callback(self.trainer) - - def configure_optimizers(self) -> tuple[list[torch.optim.Optimizer], list[dict]]: - """Choose what optimizers and learning-rate schedulers to use in your optimization. - - Normally you'd need one. But in the case of GANs or similar you might have multiple. - - Examples: - https://lightning.ai/docs/pytorch/latest/common/lightning_module.html#configure-optimizers - - :return: A dict containing the configured optimizers and learning-rate schedulers to be used for training. - """ - - def ensure_list(item: Any) -> list: # noqa: ANN401 - return item if isinstance(item, list) else [item] - - optimizers = [ - optimizer(params=self.parameters()) if callable(optimizer) else optimizer - for optimizer in ensure_list(self.hparams.optimizer) - ] - - lr_schedulers = [] - for scheduler_config in ensure_list(self.hparams.scheduler): - scheduler = scheduler_config(optimizers[0]) if callable(scheduler_config) else scheduler_config - lr_scheduler_config = {"scheduler": scheduler} - if hasattr(scheduler, "interval"): - lr_scheduler_config["interval"] = scheduler.interval - if hasattr(scheduler, "monitor"): - lr_scheduler_config["monitor"] = scheduler.monitor - lr_schedulers.append(lr_scheduler_config) - - return optimizers, lr_schedulers - - def configure_metric(self) -> None: - """Configure the metric.""" - if isinstance(self.metric_callable, partial): - num_classes_augmented_params = { - name: param.default if name != "num_classes" else self.model.num_classes - for name, param in inspect.signature(self.metric_callable).parameters.items() - if name != "kwargs" - } - self.metric = self.metric_callable(**num_classes_augmented_params) - - if isinstance(self.metric_callable, Metric): - self.metric = self.metric_callable - - if not isinstance(self.metric, Metric): - msg = "Metric should be the instance of torchmetrics.Metric." - raise TypeError(msg) - self.metric.to(self.device) - - def register_load_state_dict_pre_hook(self, model_classes: list[str], ckpt_classes: list[str]) -> None: - """Register self.model's load_state_dict_pre_hook. - - Args: - model_classes (list[str]): Class names from training data. - ckpt_classes (list[str]): Class names from checkpoint state dictionary. - """ - self.model.register_load_state_dict_pre_hook(model_classes, ckpt_classes) - - def state_dict(self) -> dict[str, Any]: - """Return state dictionary of model entity with meta information. - - Returns: - A dictionary containing datamodule state. - - """ - state_dict = super().state_dict() - state_dict["label_info"] = self.label_info - return state_dict - - def load_state_dict(self, ckpt: dict[str, Any], *args, **kwargs) -> None: - """Load state dictionary from checkpoint state dictionary. - - It successfully loads the checkpoint from OTX v1.x and for finetune and for resume. - - If checkpoint's label_info and OTXLitModule's label_info are different, - load_state_pre_hook for smart weight loading will be registered. - """ - if is_ckpt_from_otx_v1(ckpt): - msg = "The checkpoint comes from OTXv1, checkpoint keys will be updated automatically." - warnings.warn(msg, stacklevel=2) - state_dict = self.model.load_from_otx_v1_ckpt(ckpt) - elif is_ckpt_for_finetuning(ckpt): - state_dict = ckpt["state_dict"] - else: - state_dict = ckpt - - ckpt_label_info = state_dict.pop("label_info", None) - - if ckpt_label_info and self.label_info is None: - msg = ( - "`state_dict` to load has `label_info`, but the current model has no `label_info`. " - "It is recommended to set proper `label_info` for the incremental learning case." - ) - warnings.warn(msg, stacklevel=2) - if ckpt_label_info and self.label_info and ckpt_label_info != self.label_info: - logger = logging.getLogger() - logger.info( - f"Data classes from checkpoint: {ckpt_label_info.label_names} -> " - f"Data classes from training data: {self.label_info.label_names}", - ) - self.register_load_state_dict_pre_hook( - self.label_info.label_names, - ckpt_label_info.label_names, - ) - return super().load_state_dict(state_dict, *args, **kwargs) - - @property - def label_info(self) -> LabelInfo: - """Get the member `OTXModel` label information.""" - return self.model.label_info - - @label_info.setter - def label_info(self, label_info: LabelInfo | list[str]) -> None: - """Set the member `OTXModel` label information.""" - self.model.label_info = label_info # type: ignore[assignment] - - def forward(self, *args, **kwargs) -> OTXBatchPredEntity | OTXBatchLossEntity: - """Model forward pass.""" - if self.model.explain_mode and not isinstance(self.model, OVModel): - return self.model.forward_explain(*args, **kwargs) - return self.model.forward(*args, **kwargs) - - def export( - self, - output_dir: Path, - base_name: str, - export_format: OTXExportFormatType, - precision: OTXPrecisionType = OTXPrecisionType.FP32, - ) -> Path: - """Export this model to the specified output directory. - - Args: - output_dir (Path): directory for saving the exported model - base_name: (str): base name for the exported model file. Extension is defined by the target export format - export_format (OTXExportFormatType): format of the output model - precision (OTXExportPrecisionType): precision of the output model - Returns: - Path: path to the exported model. - """ - return self.model.export(output_dir, base_name, export_format, precision) diff --git a/src/otx/core/model/module/classification.py b/src/otx/core/model/module/classification.py deleted file mode 100644 index 4f7eedd9d02..00000000000 --- a/src/otx/core/model/module/classification.py +++ /dev/null @@ -1,356 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for classification lightning module used in OTX.""" -from __future__ import annotations - -import inspect -from functools import partial -from typing import TYPE_CHECKING - -import torch -from torch import Tensor -from torchmetrics import Metric -from torchmetrics.classification.accuracy import Accuracy - -from otx.core.data.dataset.classification import HLabelInfo -from otx.core.data.entity.classification import ( - HlabelClsBatchDataEntity, - HlabelClsBatchPredEntity, - HlabelClsBatchPredEntityWithXAI, - MulticlassClsBatchDataEntity, - MulticlassClsBatchPredEntity, - MulticlassClsBatchPredEntityWithXAI, - MultilabelClsBatchDataEntity, - MultilabelClsBatchPredEntity, - MultilabelClsBatchPredEntityWithXAI, -) -from otx.core.metrics.accuracy import AccuracywithLabelGroup, MixedHLabelAccuracy -from otx.core.model.entity.classification import OTXHlabelClsModel, OTXMulticlassClsModel, OTXMultilabelClsModel -from otx.core.model.module.base import OTXLitModule - -if TYPE_CHECKING: - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - - from otx.core.data.dataset.base import LabelInfo - from otx.core.metrics import MetricCallable - - -class OTXMulticlassClsLitModule(OTXLitModule): - """Base class for the lightning module used in OTX multi-class classification task.""" - - def __init__( - self, - otx_model: OTXMulticlassClsModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: MetricCallable = lambda num_classes: Accuracy(task="multiclass", num_classes=num_classes), - ): - super().__init__( - otx_model=otx_model, - torch_compile=torch_compile, - optimizer=optimizer, - scheduler=scheduler, - metric=metric, - ) - - def configure_metric(self) -> None: - """Configure the metric.""" - super().configure_metric() - if isinstance(self.metric, AccuracywithLabelGroup): - self.metric.label_info = self.model.label_info - - def _log_metrics(self, meter: Metric, key: str) -> None: - results = meter.compute() - if results is None: - msg = f"{meter} has no data to compute metric or there is an error computing metric" - raise RuntimeError(msg) - - # Custom Accuracy returns the dictionary, and accuracy value is in the `accuracy` key. - if isinstance(results, dict): - results = torch.tensor(results["accuracy"]) - - self.log(f"{key}/accuracy", results.item(), sync_dist=True, prog_bar=True) - - def validation_step(self, inputs: MulticlassClsBatchDataEntity, batch_idx: int) -> None: - """Perform a single validation step on a batch of data from the validation set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, (MulticlassClsBatchPredEntity, MulticlassClsBatchPredEntityWithXAI)): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) - - def _convert_pred_entity_to_compute_metric( - self, - preds: MulticlassClsBatchPredEntity | MulticlassClsBatchPredEntityWithXAI, - inputs: MulticlassClsBatchDataEntity, - ) -> dict[str, list[dict[str, Tensor]]]: - pred = torch.tensor(preds.labels) - target = torch.tensor(inputs.labels) - return { - "preds": pred, - "target": target, - } - - def test_step(self, inputs: MulticlassClsBatchDataEntity, batch_idx: int) -> None: - """Perform a single test step on a batch of data from the test set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, (MulticlassClsBatchPredEntity, MulticlassClsBatchPredEntityWithXAI)): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) - - -class OTXMultilabelClsLitModule(OTXLitModule): - """Base class for the lightning module used in OTX multi-label classification task.""" - - def __init__( - self, - otx_model: OTXMultilabelClsModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: MetricCallable = lambda num_labels: Accuracy(task="multilabel", num_labels=num_labels), - ): - super().__init__( - otx_model=otx_model, - torch_compile=torch_compile, - optimizer=optimizer, - scheduler=scheduler, - metric=metric, - ) - - def configure_metric(self) -> None: - """Configure the metric.""" - if isinstance(self.metric_callable, partial): - num_classes_augmented_params = { - name: param.default if name != "num_labels" else self.model.num_classes - for name, param in inspect.signature(self.metric_callable).parameters.items() - if name != "kwargs" - } - self.metric = self.metric_callable(**num_classes_augmented_params) - - if isinstance(self.metric_callable, Metric): - self.metric = self.metric_callable - - if not isinstance(self.metric, Metric): - msg = "Metric should be the instance of torchmetrics.Metric." - raise TypeError(msg) - - self.metric.to(self.device) - if isinstance(self.metric, AccuracywithLabelGroup): - self.metric.label_info = self.model.label_info - - def _log_metrics(self, meter: Metric, key: str) -> None: - results = meter.compute() - - # Custom Accuracy returns the dictionary, and accuracy value is in the `accuracy` key. - if isinstance(results, dict): - results = torch.tensor(results["accuracy"]) - - self.log(f"{key}/accuracy", results.item(), sync_dist=True, prog_bar=True) - - def validation_step(self, inputs: MultilabelClsBatchDataEntity, batch_idx: int) -> None: - """Perform a single validation step on a batch of data from the validation set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, (MultilabelClsBatchPredEntity, MultilabelClsBatchPredEntityWithXAI)): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) - - def _convert_pred_entity_to_compute_metric( - self, - preds: MultilabelClsBatchPredEntity | MultilabelClsBatchPredEntityWithXAI, - inputs: MultilabelClsBatchDataEntity, - ) -> dict[str, list[dict[str, Tensor]]]: - return { - "preds": torch.stack(preds.scores), - "target": torch.stack(inputs.labels), - } - - def test_step(self, inputs: MultilabelClsBatchDataEntity, batch_idx: int) -> None: - """Perform a single test step on a batch of data from the test set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, (MultilabelClsBatchPredEntity, MultilabelClsBatchPredEntityWithXAI)): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) - - -class OTXHlabelClsLitModule(OTXLitModule): - """Base class for the lightning module used in OTX H-label classification task.""" - - def __init__( - self, - otx_model: OTXHlabelClsModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: MetricCallable = partial( # noqa: B008 - MixedHLabelAccuracy, - num_multiclass_heads=2, - num_multilabel_classes=2, - head_logits_info={"default": (0, 2)}, - ), # lambda: MixedHLabelAccuracy() doesn't return the partial class. So, use the partial() directly. - ): - super().__init__( - otx_model=otx_model, - torch_compile=torch_compile, - optimizer=optimizer, - scheduler=scheduler, - metric=metric, - ) - - self.label_info: HLabelInfo - self.num_labels: int - self.num_multiclass_heads: int - self.num_multilabel_classes: int - self.num_singlelabel_classes: int - - def configure_metric(self) -> None: - """Configure the metric.""" - if isinstance(self.metric_callable, partial): - sig = inspect.signature(self.metric_callable) - param_dict = {} - for name, param in sig.parameters.items(): - if name in ["num_multiclass_heads", "num_multilabel_classes"]: - param_dict[name] = getattr(self.model, name) - elif name == "head_logits_info" and isinstance(self.label_info, HLabelInfo): - param_dict[name] = self.label_info.head_idx_to_logits_range - else: - param_dict[name] = param.default - param_dict.pop("kwargs", {}) - self.metric = self.metric_callable(**param_dict) - elif isinstance(self.metric_callable, Metric): - self.metric = self.metric_callable - - if not isinstance(self.metric, Metric): - msg = "Metric should be the instance of torchmetrics.Metric." - raise TypeError(msg) - - self.metric.to(self.device) - if isinstance(self.metric, AccuracywithLabelGroup): - self.metric.label_info = self.model.label_info - - def _set_hlabel_setup(self) -> None: - if not isinstance(self.label_info, HLabelInfo): - msg = f"The type of self.label_info should be HLabelInfo, got {type(self.label_info)}." - raise TypeError(msg) - - # Set the OTXHlabelClsModel params to make proper hlabel setup. - self.model.set_hlabel_info(self.label_info) - - # Set the OTXHlabelClsLitModule params. - self.num_labels = len(self.label_info.label_names) - self.num_multiclass_heads = self.label_info.num_multiclass_heads - self.num_multilabel_classes = self.label_info.num_multilabel_classes - self.num_singlelabel_classes = self.num_labels - self.num_multilabel_classes - - def _log_metrics(self, meter: Metric, key: str) -> None: - results = meter.compute() - - # Custom Accuracy returns the dictionary, and accuracy value is in the `accuracy` key. - if isinstance(results, dict): - results = torch.tensor(results["accuracy"]) - - self.log(f"{key}/accuracy", results.item(), sync_dist=True, prog_bar=True) - - def validation_step(self, inputs: HlabelClsBatchDataEntity, batch_idx: int) -> None: - """Perform a single validation step on a batch of data from the validation set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, (HlabelClsBatchPredEntity, HlabelClsBatchPredEntityWithXAI)): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) - - def _convert_pred_entity_to_compute_metric( - self, - preds: HlabelClsBatchPredEntity | HlabelClsBatchPredEntityWithXAI, - inputs: HlabelClsBatchDataEntity, - ) -> dict[str, list[dict[str, Tensor]]]: - if self.num_multilabel_classes > 0: - preds_multiclass = torch.stack(preds.labels)[:, : self.num_multiclass_heads] - preds_multilabel = torch.stack(preds.scores)[:, self.num_multiclass_heads :] - pred_result = torch.cat([preds_multiclass, preds_multilabel], dim=1) - else: - pred_result = torch.stack(preds.labels) - return { - "preds": pred_result, - "target": torch.stack(inputs.labels), - } - - def test_step(self, inputs: HlabelClsBatchDataEntity, batch_idx: int) -> None: - """Perform a single test step on a batch of data from the test set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, (HlabelClsBatchPredEntity, HlabelClsBatchPredEntityWithXAI)): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) - - @property - def label_info(self) -> LabelInfo: - """Meta information of OTXLitModule.""" - if self._meta_info is None: - err_msg = "label_info is referenced before assignment" - raise TypeError(err_msg) - return self._meta_info - - @label_info.setter - def label_info(self, label_info: LabelInfo) -> None: - self._meta_info = label_info - self._set_hlabel_setup() diff --git a/src/otx/core/model/module/detection.py b/src/otx/core/model/module/detection.py deleted file mode 100644 index 8d5ea504ad0..00000000000 --- a/src/otx/core/model/module/detection.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for detection lightning module used in OTX.""" -from __future__ import annotations - -import logging as log -from typing import TYPE_CHECKING, Any - -import torch -from torch import Tensor -from torchmetrics import Metric -from torchmetrics.detection.mean_ap import MeanAveragePrecision - -from otx.core.data.entity.detection import ( - DetBatchDataEntity, - DetBatchPredEntity, - DetBatchPredEntityWithXAI, -) -from otx.core.model.entity.detection import ExplainableOTXDetModel -from otx.core.model.module.base import OTXLitModule - -if TYPE_CHECKING: - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - - from otx.core.metrics import MetricCallable - - -class OTXDetectionLitModule(OTXLitModule): - """Base class for the lightning module used in OTX detection task.""" - - def __init__( - self, - otx_model: ExplainableOTXDetModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: MetricCallable = lambda: MeanAveragePrecision(), - ): - super().__init__( - otx_model=otx_model, - torch_compile=torch_compile, - optimizer=optimizer, - scheduler=scheduler, - metric=metric, - ) - self.test_meta_info: dict[str, Any] = self.model.test_meta_info if hasattr(self.model, "test_meta_info") else {} - - def load_state_dict(self, ckpt: dict[str, Any], *args, **kwargs) -> None: - """Load state_dict from checkpoint. - - For detection, it is need to update confidence threshold information when - the metric is FMeasure. - """ - if "confidence_threshold" in ckpt: - self.test_meta_info["best_confidence_threshold"] = ckpt["confidence_threshold"] - self.test_meta_info["vary_confidence_threshold"] = False - elif "confidence_threshold" in ckpt["hyper_parameters"]: - self.test_meta_info["best_confidence_threshold"] = ckpt["hyper_parameters"]["confidence_threshold"] - self.test_meta_info["vary_confidence_threshold"] = False - super().load_state_dict(ckpt, *args, **kwargs) - - def configure_metric(self) -> None: - """Configure the metric.""" - super().configure_metric() - for key, value in self.test_meta_info.items(): - if hasattr(self.metric, key): - setattr(self.metric, key, value) - - def _log_metrics(self, meter: Metric, key: str) -> None: - results = meter.compute() - if results is None: - msg = f"{meter} has no data to compute metric or there is an error computing metric" - raise RuntimeError(msg) - - for k, v in results.items(): - if not isinstance(v, Tensor): - log.debug("Cannot log item which is not Tensor") - continue - if v.numel() != 1: - log.debug("Cannot log Tensor which is not scalar") - continue - - self.log( - f"{key}/{k}", - v, - sync_dist=True, - prog_bar=True, - ) - if hasattr(meter, "best_confidence_threshold"): - self.hparams["confidence_threshold"] = meter.best_confidence_threshold - - def validation_step(self, inputs: DetBatchDataEntity, batch_idx: int) -> None: - """Perform a single validation step on a batch of data from the validation set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, (DetBatchPredEntity, DetBatchPredEntityWithXAI)): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) - - def _convert_pred_entity_to_compute_metric( - self, - preds: DetBatchPredEntity | DetBatchPredEntityWithXAI, - inputs: DetBatchDataEntity, - ) -> dict[str, list[dict[str, Tensor]]]: - return { - "preds": [ - { - "boxes": bboxes.data, - "scores": scores, - "labels": labels, - } - for bboxes, scores, labels in zip( - preds.bboxes, - preds.scores, - preds.labels, - ) - ], - "target": [ - { - "boxes": bboxes.data, - "labels": labels, - } - for bboxes, labels in zip(inputs.bboxes, inputs.labels) - ], - } - - def test_step(self, inputs: DetBatchDataEntity, batch_idx: int) -> None: - """Perform a single test step on a batch of data from the test set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, (DetBatchPredEntity, DetBatchPredEntityWithXAI)): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) diff --git a/src/otx/core/model/module/instance_segmentation.py b/src/otx/core/model/module/instance_segmentation.py deleted file mode 100644 index 60aff90d0ca..00000000000 --- a/src/otx/core/model/module/instance_segmentation.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for instance segmentation lightning module used in OTX.""" -from __future__ import annotations - -import logging as log -from typing import TYPE_CHECKING, Any - -import torch -from torch import Tensor -from torchmetrics import Metric - -from otx.algo.instance_segmentation.otx_instseg_evaluation import ( - OTXMaskRLEMeanAveragePrecision, -) -from otx.core.data.entity.instance_segmentation import ( - InstanceSegBatchDataEntity, - InstanceSegBatchPredEntity, - InstanceSegBatchPredEntityWithXAI, -) -from otx.core.model.entity.instance_segmentation import ExplainableOTXInstanceSegModel -from otx.core.model.module.base import OTXLitModule -from otx.core.utils.mask_util import encode_rle, polygon_to_rle - -if TYPE_CHECKING: - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - - from otx.core.metrics import MetricCallable - - -class OTXInstanceSegLitModule(OTXLitModule): - """Base class for the lightning module used in OTX instance segmentation task.""" - - def __init__( - self, - otx_model: ExplainableOTXInstanceSegModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: MetricCallable = lambda: OTXMaskRLEMeanAveragePrecision(), - ): - super().__init__( - otx_model=otx_model, - torch_compile=torch_compile, - optimizer=optimizer, - scheduler=scheduler, - metric=metric, - ) - self.test_meta_info: dict[str, Any] = self.model.test_meta_info if hasattr(self.model, "test_meta_info") else {} - - def load_state_dict(self, ckpt: dict[str, Any], *args, **kwargs) -> None: - """Load state_dict from checkpoint. - - For detection, it is need to update confidence threshold information when - the metric is FMeasure. - """ - if "confidence_threshold" in ckpt: - self.test_meta_info["best_confidence_threshold"] = ckpt["confidence_threshold"] - self.test_meta_info["vary_confidence_threshold"] = False - elif "confidence_threshold" in ckpt["hyper_parameters"]: - self.test_meta_info["best_confidence_threshold"] = ckpt["hyper_parameters"]["confidence_threshold"] - self.test_meta_info["vary_confidence_threshold"] = False - super().load_state_dict(ckpt, *args, **kwargs) - - def configure_metric(self) -> None: - """Configure the metric.""" - super().configure_metric() - for key, value in self.test_meta_info.items(): - if hasattr(self.metric, key): - setattr(self.metric, key, value) - - def on_validation_epoch_end(self) -> None: - """Callback triggered when the validation epoch ends.""" - if isinstance(self.metric, Metric): - self._log_metrics(self.metric, "val") - self.metric.reset() - - def on_test_epoch_end(self) -> None: - """Callback triggered when the test epoch ends.""" - if isinstance(self.metric, Metric): - self._log_metrics(self.metric, "test") - self.metric.reset() - - def _log_metrics(self, meter: Metric, subset_name: str) -> None: - results = meter.compute() - if results is None: - msg = f"{meter} has no data to compute metric or there is an error computing metric" - raise RuntimeError(msg) - - for metric, value in results.items(): - if not isinstance(value, Tensor): - log.debug("Cannot log item which is not Tensor") - continue - if value.numel() != 1: - log.debug("Cannot log Tensor which is not scalar") - continue - - self.log( - f"{subset_name}/{metric}", - value, - sync_dist=True, - prog_bar=True, - ) - if hasattr(meter, "best_confidence_threshold"): - self.hparams["confidence_threshold"] = meter.best_confidence_threshold - - def validation_step(self, inputs: InstanceSegBatchDataEntity, batch_idx: int) -> None: - """Perform a single validation step on a batch of data from the validation set. - - Args: - inputs (InstanceSegBatchDataEntity): The input data for the validation step. - batch_idx (int): The index of the current batch. - - Raises: - TypeError: If the predictions are not of type InstanceSegBatchPredEntity. - - Returns: - None - """ - preds = self.model(inputs) - - if not isinstance(preds, (InstanceSegBatchPredEntity, InstanceSegBatchPredEntityWithXAI)): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) - - def _convert_pred_entity_to_compute_metric( - self, - preds: InstanceSegBatchPredEntity | InstanceSegBatchPredEntityWithXAI, - inputs: InstanceSegBatchDataEntity, - ) -> dict[str, list[dict[str, Tensor]]]: - """Convert the prediction entity to the format that the metric can compute and cache the ground truth. - - This function will convert mask to RLE format and cache the ground truth for the current batch. - - Args: - preds (InstanceSegBatchPredEntity): Current batch predictions. - inputs (InstanceSegBatchDataEntity): Current batch ground-truth inputs. - - Returns: - dict[str, list[dict[str, Tensor]]]: The converted predictions and ground truth. - """ - pred_info = [] - target_info = [] - - for bboxes, masks, scores, labels in zip( - preds.bboxes, - preds.masks, - preds.scores, - preds.labels, - ): - pred_info.append( - { - "boxes": bboxes.data, - "masks": [encode_rle(mask) for mask in masks.data], - "scores": scores, - "labels": labels, - }, - ) - - for imgs_info, bboxes, masks, polygons, labels in zip( - inputs.imgs_info, - inputs.bboxes, - inputs.masks, - inputs.polygons, - inputs.labels, - ): - rles = ( - [encode_rle(mask) for mask in masks.data] - if len(masks) - else polygon_to_rle(polygons, *imgs_info.ori_shape) - ) - target_info.append( - { - "boxes": bboxes.data, - "masks": rles, - "labels": labels, - }, - ) - return {"preds": pred_info, "target": target_info} - - def test_step(self, inputs: InstanceSegBatchDataEntity, batch_idx: int) -> None: - """Perform a single test step on a batch of data from the test set. - - Args: - inputs (InstanceSegBatchDataEntity): The input data for the test step. - batch_idx (int): The index of the current batch. - - Raises: - TypeError: If the predictions are not of type InstanceSegBatchPredEntity. - """ - preds = self.model(inputs) - - if not isinstance(preds, (InstanceSegBatchPredEntity, InstanceSegBatchPredEntityWithXAI)): - raise TypeError(preds) - - if isinstance(self.metric, Metric): - self.metric.update( - **self._convert_pred_entity_to_compute_metric(preds, inputs), - ) diff --git a/src/otx/core/model/module/rotated_detection.py b/src/otx/core/model/module/rotated_detection.py deleted file mode 100644 index cb879d17570..00000000000 --- a/src/otx/core/model/module/rotated_detection.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for rotated detection lightning module used in OTX.""" -from __future__ import annotations - -from typing import TYPE_CHECKING - -import cv2 -import torch -from datumaro import Polygon -from torchvision import tv_tensors - -from otx.algo.instance_segmentation.otx_instseg_evaluation import ( - OTXMaskRLEMeanAveragePrecision, -) -from otx.core.data.entity.instance_segmentation import ( - InstanceSegBatchPredEntity, -) -from otx.core.model.entity.rotated_detection import OTXRotatedDetModel -from otx.core.model.module.instance_segmentation import OTXInstanceSegLitModule - -if TYPE_CHECKING: - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - - from otx.core.metrics import MetricCallable - - -class OTXRotatedDetLitModule(OTXInstanceSegLitModule): - """Base class for the lightning module used in OTX rotated detection task.""" - - def __init__( - self, - otx_model: OTXRotatedDetModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: MetricCallable = lambda: OTXMaskRLEMeanAveragePrecision(), - ): - super().__init__( - otx_model=otx_model, - torch_compile=torch_compile, - optimizer=optimizer, - scheduler=scheduler, - metric=metric, - ) - - def predict_step(self, *args: torch.Any, **kwargs: torch.Any) -> InstanceSegBatchPredEntity: - """Predict step for rotated detection task. - - Note: This method is overridden to convert masks to rotated bounding boxes. - - Returns: - InstanceSegBatchPredEntity: The predicted polygons (rboxes), scores, labels, masks. - """ - preds = super().predict_step(*args, **kwargs) - - batch_scores: list[torch.Tensor] = [] - batch_bboxes: list[tv_tensors.BoundingBoxes] = [] - batch_labels: list[torch.LongTensor] = [] - batch_polygons: list[list[Polygon]] = [] - batch_masks: list[tv_tensors.Mask] = [] - - for img_info, pred_bboxes, pred_scores, pred_labels, pred_masks in zip( - preds.imgs_info, - preds.bboxes, - preds.scores, - preds.labels, - preds.masks, - ): - boxes = [] - scores = [] - labels = [] - masks = [] - polygons = [] - - for bbox, score, label, mask in zip(pred_bboxes, pred_scores, pred_labels, pred_masks): - if mask.sum() == 0: - continue - np_mask = mask.detach().cpu().numpy().astype(int) - contours, hierarchies = cv2.findContours(np_mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) - if hierarchies is None: - continue - rbox_polygons = [] - for contour, hierarchy in zip(contours, hierarchies[0]): - # skip inner contours - if hierarchy[3] != -1 or len(contour) <= 2: - continue - rbox_points = Polygon(cv2.boxPoints(cv2.minAreaRect(contour)).reshape(-1)) - rbox_polygons.append((rbox_points, rbox_points.get_area())) - - # select the largest polygon - if len(rbox_polygons) > 0: - rbox_polygons.sort(key=lambda x: x[1], reverse=True) - polygons.append(rbox_polygons[0][0]) - scores.append(score) - boxes.append(bbox) - labels.append(label) - masks.append(mask) - - if len(boxes): - scores = torch.stack(scores) - boxes = tv_tensors.BoundingBoxes(torch.stack(boxes), format="XYXY", canvas_size=img_info.ori_shape) - labels = torch.stack(labels) - masks = torch.stack(masks) - - batch_scores.append(scores) - batch_bboxes.append(boxes) - batch_labels.append(labels) - batch_polygons.append(polygons) - batch_masks.append(masks) - - return InstanceSegBatchPredEntity( - batch_size=preds.batch_size, - images=preds.images, - imgs_info=preds.imgs_info, - scores=batch_scores, - bboxes=batch_bboxes, - masks=batch_masks, - polygons=batch_polygons, - labels=batch_labels, - ) diff --git a/src/otx/core/model/module/segmentation.py b/src/otx/core/model/module/segmentation.py deleted file mode 100644 index e9f776e119a..00000000000 --- a/src/otx/core/model/module/segmentation.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for segmentation lightning module used in OTX.""" -from __future__ import annotations - -import inspect -import logging as log -from functools import partial -from typing import TYPE_CHECKING - -import torch -from torch import Tensor -from torchmetrics import Dice, Metric - -from otx.core.data.entity.segmentation import ( - SegBatchDataEntity, - SegBatchPredEntity, - SegBatchPredEntityWithXAI, -) -from otx.core.model.entity.segmentation import OTXSegmentationModel -from otx.core.model.module.base import OTXLitModule - -if TYPE_CHECKING: - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - - from otx.core.metrics import MetricCallable - - -class OTXSegmentationLitModule(OTXLitModule): - """Base class for the lightning module used in OTX segmentation task.""" - - def __init__( - self, - otx_model: OTXSegmentationModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: MetricCallable = lambda: Dice(), - ): - super().__init__( - otx_model=otx_model, - torch_compile=torch_compile, - optimizer=optimizer, - scheduler=scheduler, - metric=metric, - ) - - def configure_metric(self) -> None: - """Configure the metric.""" - if isinstance(self.metric_callable, partial): - sig = inspect.signature(self.metric_callable) - param_dict = {} - for name, param in sig.parameters.items(): - if name == "num_classes": - param_dict[name] = self.model.num_classes + 1 - elif name == "ignore_index": - param_dict[name] = self.model.num_classes - else: - param_dict[name] = param.default - param_dict.pop("kwargs", {}) - self.metric = self.metric_callable(**param_dict) - elif isinstance(self.metric_callable, Metric): - self.metric = self.metric_callable - - if not isinstance(self.metric, Metric): - msg = "Metric should be the instance of torchmetrics.Metric." - raise TypeError(msg) - - # Since the metric is not initialized at the init phase, - # Need to manually correct the device setting. - self.metric.to(self.device) - - def _log_metrics(self, meter: Metric, key: str) -> None: - results = meter.compute() - if results is None: - msg = f"{meter} has no data to compute metric or there is an error computing metric" - raise RuntimeError(msg) - - if isinstance(results, Tensor): - if results.numel() != 1: - log.debug("Cannot log Tensor which is not scalar") - return - self.log( - f"{key}/{type(meter).__name__}", - results, - sync_dist=True, - prog_bar=True, - ) - else: - log.debug("Cannot log item which is not Tensor") - - def validation_step(self, inputs: SegBatchDataEntity, batch_idx: int) -> None: - """Perform a single validation step on a batch of data from the validation set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - - if not isinstance(preds, (SegBatchPredEntity, SegBatchPredEntityWithXAI)): - raise TypeError(preds) - - predictions = self._convert_pred_entity_to_compute_metric(preds, inputs) - if isinstance(self.metric, Metric): - for prediction in predictions: - self.metric.update(**prediction) - - def _convert_pred_entity_to_compute_metric( - self, - preds: SegBatchPredEntity | SegBatchPredEntityWithXAI, - inputs: SegBatchDataEntity, - ) -> list[dict[str, Tensor]]: - return [ - { - "preds": pred_mask, - "target": target_mask, - } - for pred_mask, target_mask in zip(preds.masks, inputs.masks) - ] - - def test_step(self, inputs: SegBatchDataEntity, batch_idx: int) -> None: - """Perform a single test step on a batch of data from the test set. - - :param batch: A batch of data (a tuple) containing the input tensor of images and target - labels. - :param batch_idx: The index of the current batch. - """ - preds = self.model(inputs) - if not isinstance(preds, (SegBatchPredEntity, SegBatchPredEntityWithXAI)): - raise TypeError(preds) - predictions = self._convert_pred_entity_to_compute_metric(preds, inputs) - if isinstance(self.metric, Metric): - for prediction in predictions: - self.metric.update(**prediction) diff --git a/src/otx/core/model/module/visual_prompting.py b/src/otx/core/model/module/visual_prompting.py deleted file mode 100644 index 05840add07c..00000000000 --- a/src/otx/core/model/module/visual_prompting.py +++ /dev/null @@ -1,393 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Class definition for visual prompting lightning module used in OTX.""" -from __future__ import annotations - -import logging as log -import pickle -import time -from pathlib import Path -from typing import TYPE_CHECKING - -import torch -from torch import Tensor -from torchmetrics.aggregation import MeanMetric -from torchmetrics.classification import BinaryF1Score, BinaryJaccardIndex, Dice -from torchmetrics.collections import MetricCollection -from torchmetrics.detection.mean_ap import MeanAveragePrecision -from torchvision import tv_tensors - -from otx.core.data.entity.visual_prompting import ( - VisualPromptingBatchDataEntity, - VisualPromptingBatchPredEntity, - ZeroShotVisualPromptingBatchDataEntity, - ZeroShotVisualPromptingBatchPredEntity, -) -from otx.core.model.entity.visual_prompting import OTXVisualPromptingModel -from otx.core.model.module.base import OTXLitModule -from otx.core.utils.mask_util import polygon_to_bitmap - -if TYPE_CHECKING: - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - from torchmetrics import Metric - - -class OTXVisualPromptingLitModule(OTXLitModule): - """Base class for the lightning module used in OTX visual prompting task.""" - - def __init__( - self, - otx_model: OTXVisualPromptingModel, - torch_compile: bool, - optimizer: list[OptimizerCallable] | OptimizerCallable = lambda p: torch.optim.SGD(p, lr=0.01), - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable = torch.optim.lr_scheduler.ConstantLR, - metric: Metric = MeanMetric, # TODO (sungmanc): dictionary metric will be supported # noqa: TD003 - ): - super().__init__( - otx_model=otx_model, - torch_compile=torch_compile, - optimizer=optimizer, - scheduler=scheduler, - metric=metric, - ) - - self.train_metric = MetricCollection( - { - "loss": MeanMetric(), - "loss_dice": MeanMetric(), - "loss_focal": MeanMetric(), - "loss_iou": MeanMetric(), - }, - ) - - def configure_metric(self) -> None: - """Configure metrics.""" - self.val_metric = MetricCollection( - { - "IoU": BinaryJaccardIndex(), - "F1": BinaryF1Score(), - "Dice": Dice(), - "mAP": MeanAveragePrecision(iou_type="segm"), - }, - ) - self.val_metric.to(self.device) - - self.test_metric = MetricCollection( - { - "IoU": BinaryJaccardIndex(), - "F1": BinaryF1Score(), - "Dice": Dice(), - "mAP": MeanAveragePrecision(iou_type="segm"), - }, - ) - self.test_metric.to(self.device) - - def on_train_epoch_start(self) -> None: - """Callback triggered when the train epoch starts.""" - self.train_metric.reset() - - def on_validation_epoch_start(self) -> None: - """Callback triggered when the validation epoch starts.""" - self.val_metric.reset() - - def on_test_epoch_start(self) -> None: - """Callback triggered when the test epoch starts.""" - self.test_metric.reset() - - def on_train_epoch_end(self) -> None: - """Callback triggered when the train epoch ends.""" - self._log_metrics(self.train_metric, "train") - self.train_metric.reset() - - def on_validation_epoch_end(self) -> None: - """Callback triggered when the validation epoch ends.""" - self._log_metrics(self.val_metric, "val") - self.val_metric.reset() - - def on_test_epoch_end(self) -> None: - """Callback triggered when the test epoch ends.""" - self._log_metrics(self.test_metric, "test") - self.test_metric.reset() - - def _log_metrics(self, meter: MetricCollection, subset_name: str) -> None: - results = meter.compute() - for metric, value in results.items(): - if not isinstance(value, Tensor): - log.debug("Cannot log item which is not Tensor") - continue - if value.numel() != 1: - log.debug("Cannot log Tensor which is not scalar") - continue - - self.log( - f"{subset_name}/{metric}", - value, - sync_dist=True, - prog_bar=True, - ) - - def training_step( - self, - inputs: VisualPromptingBatchDataEntity | ZeroShotVisualPromptingBatchDataEntity, # type: ignore[override] - batch_idx: int, - ) -> Tensor: - """Step for model training.""" - train_loss = self.model(inputs) - - if isinstance(train_loss, Tensor): - self.train_metric["Loss"].update(train_loss) - - elif isinstance(train_loss, dict): - for k, v in train_loss.items(): - if k in self.train_metric: - self.train_metric[k].update(v) - - else: - raise TypeError(train_loss) - - self._log_metrics(self.train_metric, "train") - - return train_loss - - def validation_step(self, inputs: VisualPromptingBatchDataEntity, batch_idx: int) -> None: - """Perform a single validation step on a batch of data from the validation set. - - Args: - inputs (VisualPromptingBatchDataEntity): The input data for the validation step. - batch_idx (int): The index of the current batch. - - Raises: - TypeError: If the predictions are not of type VisualPromptingBatchPredEntity. - - Returns: - None - """ - self._inference_step(self.val_metric, inputs, batch_idx) - - def test_step(self, inputs: VisualPromptingBatchDataEntity, batch_idx: int) -> None: - """Perform a single test step on a batch of data from the test set. - - Args: - inputs (VisualPromptingBatchDataEntity): The input data for the test step. - batch_idx (int): The index of the current batch. - - Raises: - TypeError: If the predictions are not of type VisualPromptingBatchPredEntity. - """ - self._inference_step(self.test_metric, inputs, batch_idx) - - def _convert_pred_entity_to_compute_metric( - self, - preds: VisualPromptingBatchPredEntity | ZeroShotVisualPromptingBatchPredEntity, - inputs: VisualPromptingBatchDataEntity | ZeroShotVisualPromptingBatchDataEntity, - ) -> dict[str, list[dict[str, Tensor]]]: - """Convert the prediction entity to the format required by the compute metric function.""" - pred_info = [] - target_info = [] - - for masks, scores, labels in zip( - preds.masks, - preds.scores, - preds.labels, - ): - pred_info.append( - { - "masks": masks.data, - "scores": scores, - "labels": labels, - }, - ) - - for imgs_info, masks, polygons, labels in zip( - inputs.imgs_info, - inputs.masks, - inputs.polygons, - inputs.labels, - ): - bit_masks = masks if len(masks) else polygon_to_bitmap(polygons, *imgs_info.ori_shape) - target_info.append( - { - "masks": tv_tensors.Mask(bit_masks, dtype=torch.bool).data, - "labels": torch.cat(list(labels.values())) if isinstance(labels, dict) else labels, - }, - ) - - return {"preds": pred_info, "target": target_info} - - def _inference_step( - self, - metric: MetricCollection, - inputs: VisualPromptingBatchDataEntity | ZeroShotVisualPromptingBatchDataEntity, - batch_idx: int, - ) -> None: - """Perform a single inference step on a batch of data from the inference set.""" - preds = self.model(inputs) - - if not isinstance(preds, VisualPromptingBatchPredEntity): - raise TypeError(preds) - - converted_entities = self._convert_pred_entity_to_compute_metric(preds, inputs) - for _name, _metric in metric.items(): - if _name == "mAP": - # MeanAveragePrecision - _preds = [ - {k: v > 0.5 if k == "masks" else v.squeeze(1) if k == "scores" else v for k, v in ett.items()} - for ett in converted_entities["preds"] - ] - _target = converted_entities["target"] - _metric.update(preds=_preds, target=_target) - elif _name in ["IoU", "F1", "Dice"]: - # BinaryJaccardIndex, BinaryF1Score, Dice - for cvt_preds, cvt_target in zip(converted_entities["preds"], converted_entities["target"]): - _metric.update(cvt_preds["masks"], cvt_target["masks"]) - - -class OTXZeroShotVisualPromptingLitModule(OTXVisualPromptingLitModule): - """Base class for the lightning module used in OTX zero-shot visual prompting task.""" - - def configure_metric(self) -> None: - """Configure metrics.""" - self.test_metric = MetricCollection( - { - "IoU": BinaryJaccardIndex().to(self.device), - "F1": BinaryF1Score().to(self.device), - "Dice": Dice().to(self.device), - "mAP": MeanAveragePrecision(iou_type="segm").to(self.device), - }, - ) - - def on_train_start(self) -> None: - """Initialize reference infos before learn.""" - self.model.initialize_reference_info() - - def on_test_start(self) -> None: - """Load previously saved reference info.""" - super().on_test_start() - if not self.model.load_latest_reference_info(self.device): - # TODO (sungchul): check fit_loop for OVModel # noqa: TD003 - log.warning("No reference info found. `Learn` will be automatically excuted first.") - self.trainer.lightning_module.automatic_optimization = False - self.trainer.fit_loop.run() - # to use infer logic - self.training = False - self.model.training = False - # to set _combined_loader - self.trainer._evaluation_loop.setup_data() # noqa: SLF001 - self.trainer._evaluation_loop.reset() # noqa: SLF001 - self.model.load_latest_reference_info(self.device) - - def on_predict_start(self) -> None: - """Load previously saved reference info.""" - if not self.model.load_latest_reference_info(self.device): - # TODO (sungchul): check fit_loop for OVModel # noqa: TD003 - log.warning("No reference info found. `Learn` will be automatically excuted first.") - self.trainer.lightning_module.automatic_optimization = False - self.trainer.fit_loop.run() - # to use infer logic - self.training = False - self.model.training = False - # to set _combined_loader - self.trainer._evaluation_loop.setup_data() # noqa: SLF001 - self.trainer._evaluation_loop.reset() # noqa: SLF001 - self.model.load_latest_reference_info(self.device) - - def on_train_epoch_start(self) -> None: - """Skip on_train_epoch_start unused in zero-shot visual prompting.""" - - def on_train_epoch_end(self) -> None: - """Skip on_train_epoch_end unused in zero-shot visual prompting.""" - if self.model.save_outputs: - reference_info = { - "reference_feats": self.model.reference_feats, - "used_indices": self.model.used_indices, - } - # save reference info - path_reference_info: Path = ( - self.model.root_reference_info / time.strftime("%Y%m%d_%H%M%S") / "reference_info.pt" - ) - Path.mkdir(Path(path_reference_info).parent, parents=True, exist_ok=True) - if isinstance(self.model, OTXVisualPromptingModel): - torch.save(reference_info, path_reference_info) - pickle.dump( - {k: v.numpy() for k, v in reference_info.items()}, - Path.open(Path(str(path_reference_info).replace(".pt", ".pickle")), "wb"), - ) - else: - torch.save({k: torch.as_tensor(v) for k, v in reference_info.items()}, path_reference_info) - pickle.dump(reference_info, Path.open(Path(str(path_reference_info).replace(".pt", ".pickle")), "wb")) - log.info(f"Saved reference info at {path_reference_info}.") - - def on_validation_epoch_start(self) -> None: - """Skip on_validation_epoch_start unused in zero-shot visual prompting.""" - - def on_validation_epoch_end(self) -> None: - """Skip on_validation_epoch_end unused in zero-shot visual prompting.""" - - def configure_optimizers(self) -> None: # type: ignore[override] - """Skip configure_optimizers unused in zero-shot visual prompting.""" - - def training_step( - self, - inputs: VisualPromptingBatchDataEntity | ZeroShotVisualPromptingBatchDataEntity, # type: ignore[override] - batch_idx: int, - ) -> Tensor: - """Skip training_step unused in zero-shot visual prompting.""" - self.model(inputs) - - def validation_step( - self, - inputs: VisualPromptingBatchDataEntity | ZeroShotVisualPromptingBatchDataEntity, - batch_idx: int, - ) -> None: - """Skip validation_step unused in zero-shot visual prompting.""" - - def _inference_step( - self, - metric: MetricCollection, - inputs: VisualPromptingBatchDataEntity | ZeroShotVisualPromptingBatchDataEntity, - batch_idx: int, - ) -> None: - """Perform a single inference step on a batch of data from the inference set.""" - preds = self.model(inputs) - - if not isinstance(preds, ZeroShotVisualPromptingBatchPredEntity): - raise TypeError(preds) - - converted_entities = self._convert_pred_entity_to_compute_metric(preds, inputs) - for _name, _metric in metric.items(): - if _name == "mAP": - # MeanAveragePrecision - _preds = [ - { - k: v > 0.5 if k == "masks" else v.squeeze(1).to(self.device) if k == "labels" else v - for k, v in ett.items() - } - for ett in converted_entities["preds"] - ] - _target = converted_entities["target"] - - # match #_preds and #_target - if len(_preds) > len(_target): - # interpolate _target - num_diff = len(_preds) - len(_target) - for idx in range(num_diff): - _target.append(_target[idx]) - elif len(_preds) < len(_target): - num_diff = len(_target) - len(_preds) - pad_prediction = { - "masks": torch.zeros_like(_target[0]["masks"], dtype=_target[0]["masks"].dtype), - "labels": torch.zeros_like(_target[0]["labels"], dtype=_target[0]["labels"].dtype), - "scores": torch.zeros(len(_target[0]["labels"]), dtype=torch.float32), - } # for empty prediction - for idx in range(num_diff): - _preds.append(_preds[idx] if idx < len(_preds) else pad_prediction) - - _metric.update(preds=_preds, target=_target) - elif _name in ["IoU", "F1", "Dice"]: - # BinaryJaccardIndex, BinaryF1Score, Dice - for cvt_preds, cvt_target in zip(converted_entities["preds"], converted_entities["target"]): - _metric.update( - cvt_preds["masks"].sum(dim=0).clamp(0, 1), - cvt_target["masks"].sum(dim=0).clamp(0, 1), - ) diff --git a/src/otx/core/ov/__init__.py b/src/otx/core/ov/__init__.py new file mode 100644 index 00000000000..402621932cc --- /dev/null +++ b/src/otx/core/ov/__init__.py @@ -0,0 +1,9 @@ +"""Module for otx.core.ov.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +# flake8: noqa +from .graph import * +from .models import * +from .ops import * diff --git a/src/otx/core/ov/graph/__init__.py b/src/otx/core/ov/graph/__init__.py new file mode 100644 index 00000000000..56374442183 --- /dev/null +++ b/src/otx/core/ov/graph/__init__.py @@ -0,0 +1,9 @@ +"""Module for otx.core.ov.graph.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +# TODO: Need to remove comment with ignore mypy and fix mypy issues +from .graph import Graph # type: ignore[attr-defined] + +__all__ = ["Graph"] diff --git a/src/otx/core/ov/graph/graph.py b/src/otx/core/ov/graph/graph.py new file mode 100644 index 00000000000..e51e1a431ad --- /dev/null +++ b/src/otx/core/ov/graph/graph.py @@ -0,0 +1,645 @@ +# type: ignore +# TODO: Need to remove line 1 (ignore mypy) and fix mypy issues +"""Modules for otx.core.ov.graph.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import inspect +from collections import OrderedDict +from copy import deepcopy +from dataclasses import asdict +from typing import Any, Dict, Generator, List, Optional, Tuple, Union + +import _collections_abc +import networkx as nx +from openvino.runtime import Model + +from otx.utils.logger import get_logger + +from ..ops.op import Operation +from ..ops.utils import convert_op_to_torch +from ..utils import get_op_name + +# pylint: disable=too-many-locals, too-many-nested-blocks, arguments-renamed, too-many-branches, too-many-statements + +logger = get_logger() + + +class SortedDictKeysView(_collections_abc.KeysView): + """SortedDictKeysView class.""" + + def __repr__(self): + """Function repr of SortedDictKeysView.""" + return f"{self.__class__.__name__}({list(self._mapping)})" + + def __reversed__(self): + """Function reversed of SortedDictKeysView.""" + yield from reversed(self._mapping) + + +class SortedDictValuesView(_collections_abc.ValuesView): + """SortedDictValuesView class.""" + + def __repr__(self): + """Sorteddictvaluesview's repr function.""" + return f"{self.__class__.__name__}({[self._mapping[i] for i in self._mapping]})" + + def __reversed__(self): + """Sorteddictvaluesview's reversed function.""" + for key in reversed(self._mapping): + yield self._mapping[key] + + +class SortedDictItemsView(_collections_abc.ItemsView): + """SortedDictItemsView class.""" + + def __repr__(self): + """Sorteddictitemsview's repr function.""" + return f"{self.__class__.__name__}({[(i, self._mapping[i]) for i in self._mapping]})" + + def __reversed__(self): + """Sorteddictitemsview's reversed function.""" + for key in reversed(self._mapping): + yield (key, self._mapping[key]) + + +class NOOP: + """NOOP class.""" + + pass # pylint: disable=unnecessary-pass + + +class SortedDict(dict): + """SortedDict class.""" + + def __init__(self, sort_key, *args, **kwargs): + self._sort_key = sort_key + self._sorted_keys = [] + super().__init__(self, *args, **kwargs) + + def __setitem__(self, key, value): + """Sorteddict's setitem function.""" + assert len(value) == 1 + edge_key, edge_attr = next(iter(value.items())) + sort_value = float("inf") if self._sort_key not in edge_attr else edge_attr[self._sort_key] + self._sorted_keys.append([sort_value, key, edge_key]) + self._sorted_keys.sort(key=lambda x: x[0]) + if key in self: + assert edge_key not in self[key] + self[key].update(value) + else: + super().__setitem__(key, value) + + def __delitem__(self, key): + """Sorteddict's delitem function.""" + super().__delitem__(key) + for i, (_, key_in, _) in enumerate(self._sorted_keys): + if key_in == key: + break + self._sorted_keys.pop(i) # pylint: disable=undefined-loop-variable + + def __iter__(self): + """Sorteddict's iter function.""" + for _, key, _ in self._sorted_keys: + yield key + + def __reversed__(self): + """Sorteddict's reversed function.""" + for _, key, _ in self._sorted_keys[::-1]: + yield key + + def __repr__(self): + """Sorteddict's repr function.""" + if not len(self): # pylint: disable=use-implicit-booleaness-not-len + return "{}" + repr_ = "{" + for _, key, _ in self._sorted_keys: + repr_ += f"{key}: {self[key]}, " + repr_ = repr_[:-2] + repr_ += "}" + return repr_ + + def __deepcopy__(self, memo): + """Sorteddict's deepcopy function.""" + cls = self.__class__ + result = cls(self._sort_key) + memo[id(self)] = result + for key, value in self.items(): + result[key] = deepcopy(value, memo) + return result + + def clear(self): + """Sorteddict's clear function.""" + super().clear() + self._sorted_keys = [] + + def pop(self, key, default=NOOP()): + """Sorteddict's pop function.""" + if isinstance(default, NOOP): + value = super().pop(key) + else: + value = super().pop(key, default) + + for i, (_, key_in, _) in enumerate(self._sorted_keys): + if key_in == key: + break + self._sorted_keys.pop(i) # pylint: disable=undefined-loop-variable + + return value + + def popitem(self): + """Sorteddict's popitem function.""" + raise NotImplementedError + + @staticmethod + def fromkeys(iterable, value=None): + """Sorteddict's fromkeys function.""" + raise NotImplementedError + + def keys(self): + """Sorteddict's keys function.""" + return SortedDictKeysView(self) + + def values(self): + """Sorteddict's values function.""" + return SortedDictValuesView(self) + + def items(self): + """Sorteddict's items function.""" + return SortedDictItemsView(self) + + +class SortedDictHelper(dict): + """SortedDictHelper class.""" + + def __init__(self, sort_key=None, *args, **kwargs): # pylint: disable=keyword-arg-before-vararg + self._sort_key = sort_key + super().__init__(*args, **kwargs) + + def __setitem__(self, key, value): + """Sorteddicthelper's setitem function.""" + super().__setitem__(key, SortedDict(self._sort_key)) + for v_key, v_value in value.items(): + self[key][v_key] = v_value + + +class Graph(nx.MultiDiGraph): + """Graph class.""" + + adjlist_outer_dict_factory = SortedDictHelper + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._adj = self.adjlist_outer_dict_factory("out_port") + self._pred = self.adjlist_outer_dict_factory("in_port") + self._succ = self._adj + + self._normalize_nodes = [] + + @staticmethod + def from_ov(ov_model: Model) -> "Graph": + """Graph's from_ov function.""" + graph = Graph() + + ov_ops = ov_model.get_ordered_ops() + ops_dict = OrderedDict() + parents_dict: Dict[str, List[Optional[List]]] = {} + children_dict: Dict[str, List[Optional[List]]] = {} + + for ov_op in ov_ops: + op_name = get_op_name(ov_op) + node = convert_op_to_torch(ov_op) + + graph.add_node( + node, + name=op_name, + type=node.type, + version=node.version, + attrs=node.attrs, + ) + ops_dict[op_name] = node + + children_dict[op_name] = [] + for out_port in ov_op.outputs(): + out_port_id = out_port.get_index() + for in_port in out_port.get_target_inputs(): + in_port_id = in_port.get_index() + children_dict[op_name].append([out_port_id, in_port_id, get_op_name(in_port.get_node())]) + + parents_dict[op_name] = [] + for in_port in ov_op.inputs(): + in_port_id = in_port.get_index() + out_port = in_port.get_source_output() + out_port_id = out_port.get_index() + parents_dict[op_name].append([out_port_id, in_port_id, get_op_name(out_port.get_node())]) + + # validate graph + for node, children in children_dict.items(): + for _, _, child in children: + assert node in [i[-1] for i in parents_dict[child]], f"{node} is not a parent of {child}" + for node, parents in parents_dict.items(): + for _, _, parent in parents: + assert node in [i[-1] for i in children_dict[parent]], f"{node} is not a child of {parent}" + + # add edges + for src, tgts in children_dict.items(): + for out_port_id, in_port_id, tgt in tgts: + graph.add_edge( + ops_dict[src], + ops_dict[tgt], + in_port=in_port_id, + out_port=out_port_id, + ) + + # freeze normalization nodes + graph._freeze_normalize_nodes() # pylint: disable=protected-access + + return graph + + def get_edge_data(self, node_from: Operation, node_to: Operation, default=None) -> Optional[List[Dict[Any, Any]]]: + """Graph's get_edge_data function.""" + edge_data = super().get_edge_data(node_from, node_to, None, default) + if edge_data is not None: + return list(edge_data.values()) + return None + + def remove_node(self, node: Operation, keep_connect: bool = False): + """Graph's remove_node function.""" + edges_to_keep = [] + if keep_connect: + predecessors = [ + predecessor + for predecessor in self.predecessors(node) + if hasattr(predecessor, "type") and predecessor.type != "Constant" + ] + if predecessors: + assert len(predecessors) == 1 + predecessor = predecessors[0] + + for successor in self.successors(node): + for predecessor_ in self.predecessors(successor): + edges_attrs = self.get_edge_data(predecessor_, successor) + assert len(edges_attrs) == 1 + if predecessor_ == node: + for edge_attrs in edges_attrs: + edges_to_keep.append([predecessor, successor, edge_attrs]) + + super().remove_node(node) + for edge in edges_to_keep: + node_from, node_to, attrs = edge + self.add_edge(node_from, node_to, **attrs) + + def replace_node(self, old_node: Operation, new_node: Operation): + """Graph's replace_node function.""" + edges = [] + for successor in self.successors(old_node): + for edge_attrs in self.get_edge_data(old_node, successor): + edges.append((new_node, successor, edge_attrs)) + for predecessor in self.predecessors(old_node): + for edge_attrs in self.get_edge_data(predecessor, old_node): + edges.append((predecessor, new_node, edge_attrs)) + + self.remove_node(old_node) + + for edge in edges: + node_from, node_to, attrs = edge + self.add_edge(node_from, node_to, **attrs) + + def add_edge( + self, + node_from: Operation, + node_to: Operation, + out_port: Optional[int] = None, + in_port: Optional[int] = None, + **kwargs, + ): + """Graph's add_edge function.""" + if node_from not in self: + self.add_node(node_from) + + if node_to not in self: + self.add_node(node_to) + + if out_port is None: + out_port = 0 + + if in_port is None: + occupied = [ + edge["in_port"] + for predecessor in self.predecessors(node_to) + for edge in self.get_edge_data(predecessor, node_to) + ] + assert len(occupied) == len(set(occupied)) + if occupied: + for i in range(max(occupied)): + if i not in occupied: + in_port = i + break + if in_port is None: + in_port = len(occupied) + + # validate in_port + spec = inspect.getfullargspec(node_to.forward) + if spec.varargs is None: + valid_range = list(range(len(spec.args[1:]))) + if in_port not in valid_range: + raise ValueError(f"in_port {in_port} is not in valid range {valid_range} " f"for {node_to.name}.") + occupied = [] + predecessors = list(self.predecessors(node_to)) + if predecessors: + occupied = [ + edge["in_port"] for predecessor in predecessors for edge in self.get_edge_data(predecessor, node_to) + ] + assert len(occupied) == len(set(occupied)) + if occupied: + if in_port in occupied: + raise ValueError(f"in_port {in_port} is occupied for {node_to.name}.") + + # out_port validation is not able to do + + # add edge + key = f"{in_port}{out_port}" + super().add_edge(node_from, node_to, key=key, in_port=in_port, out_port=out_port, **kwargs) + + def predecessors( + self, + node: Operation, + with_edge_data: bool = False, + ) -> Generator[Union[Tuple[Operation, Optional[List]], Operation], None, None]: + """Graph's predecessors function.""" + for predecessor in super().predecessors(node): + if with_edge_data: + yield (predecessor, self.get_edge_data(predecessor, node)) + else: + yield predecessor + + def successors( + self, + node: Operation, + with_edge_data: bool = False, + ) -> Generator[Union[Tuple[Operation, Optional[List]], Operation], None, None]: + """Graph's successors function.""" + for successor in super().successors(node): + if with_edge_data: + yield (successor, self.get_edge_data(node, successor)) + else: + yield successor + + def get_nodes_by_types(self, types: List[str]) -> List[Operation]: + """Graph's get_nodes_by_types function.""" + found = [] + for node in self.topological_sort(): + if node.type in types: + found.append(node) + return found + + def bfs( + self, node: Operation, reverse: bool = False, depth_limit: Optional[int] = None + ) -> Generator[Union[Tuple[Operation, Operation], Tuple[Operation, Tuple[Operation]]], None, None]: + """Graph's bfs function.""" + if reverse: + for s_value, t_value in nx.bfs_edges(self, node, reverse=True, depth_limit=depth_limit): + yield (t_value, s_value) + else: + parent = node + children = [] + for p, c in nx.bfs_edges(self, node, depth_limit=depth_limit): + if p == parent: + children.append(c) + continue + yield (parent, tuple(children)) + children = [c] + parent = p + if children: + yield (parent, tuple(children)) + + # def dfs(self, node: Operation, forward=True, depth_limit=None): + # if forward: + # return nx.dfs_successors(self, node, depth_limit) + # else: + # return nx.dfs_predecessors(self, node, depth_limit) + + def get_nodes_by_type_pattern(self, pattern: List[str], start_node: Optional[Operation] = None, reverse=False): + """Graph's get_nodes_by_type_pattern function.""" + if len(pattern) < 1: + raise ValueError(f"pattern must be longer than 2 but {len(pattern)} is given") + pattern_pairs = [pattern[i : i + 2] for i in range(len(pattern) - 1)] + + if start_node is None: + if reverse: + start_node = list(self.topological_sort())[-1] + else: + start_node = list(self.topological_sort())[0] + + founds = [] + start_nodes = [start_node] + for pattern_pair in pattern_pairs: + found_ = {start_node: None for start_node in start_nodes} + for start_node_ in start_nodes: + for s_value, ts_ in self.bfs(start_node_, reverse, 1): + if not isinstance(ts_, tuple): + ts_ = (ts_,) + for t in ts_: + if [s_value.type, t.type] == pattern_pair: + if reverse: + found_[t] = s_value + else: + found_[s_value] = t + if founds: + pop_indices = [] + for i, found in enumerate(founds): + last_node = found[-1] + if last_node not in found_.keys(): + pop_indices.append(i) + else: + founds[i].append(found_[last_node]) + for pop_idx in pop_indices[::-1]: + founds.pop(pop_idx) + else: + founds = [[s, t] for s, t in found_.items() if s is not None and t is not None] + start_nodes = [found[-1] for found in founds] + return founds + + def _freeze_normalize_nodes(self): # noqa: C901 + """Graph's _freeze_normalize_nodes function.""" + invariant_types = ["Transpose", "Convert"] + + def test_constant(node): + """Graph's test_constant function.""" + constant_nodes = [node_ for node_ in self.predecessors(node) if node_.type == "Constant"] + if len(constant_nodes) != 1: + return False + constant_value = constant_nodes[0].data.squeeze() + if constant_value.dim() > 1 or constant_value.numel() not in [1, 3]: + return False + return True + + def get_nodes_by_type_from_node( + node, + types, + ignore_types=None, + reverse=False, + depth_limit=-1, + ): + """Graph's get_nodes_by_type_from_node function.""" + ignore_types = ignore_types if ignore_types else [] + func = self.successors + if reverse: + func = self.predecessors + + candidates = [(i, 1) for i in func(node)] + found = [] + for candidate, cur_depth in candidates: + if cur_depth > depth_limit > -1: + break + if candidate.type == types: + found.append(candidate) + elif candidate.type in ignore_types: + candidates.extend([(i, cur_depth + 1) for i in func(candidate)]) + return found + + def find_multiply_add(node): + """Graph's find_multiply_add function.""" + scale_node = None + mean_node = None + + scale_nodes = get_nodes_by_type_from_node(node, "Multiply", invariant_types) + if scale_nodes and len(scale_nodes) == 1: + scale_node = scale_nodes[0] + if not test_constant(scale_node): + scale_node = None + + node = scale_node if scale_node is not None else node + mean_nodes = get_nodes_by_type_from_node(node, "Add", invariant_types) + if mean_nodes and len(mean_nodes) == 1: + mean_node = mean_nodes[0] + if not test_constant(mean_node): + mean_node = None + + return (scale_node, mean_node) + + def find_subtract_divide(node): + """Graph's find_subtract_divide function.""" + mean_node = None + scale_node = None + + mean_nodes = get_nodes_by_type_from_node(node, "Subtract", invariant_types) + if mean_nodes and len(mean_nodes) == 1: + mean_node = mean_nodes[0] + if not test_constant(mean_node): + mean_node = None + + node = mean_node if mean_node is not None else node + scale_nodes = get_nodes_by_type_from_node(node, "Divide", invariant_types) + if scale_nodes and len(scale_nodes) == 1: + scale_node = scale_nodes[0] + if not test_constant(scale_node): + scale_node = None + + return (mean_node, scale_node) + + def find_subtract_multiply(node): + """Graph's find_subtract_multiply function.""" + mean_node = None + scale_node = None + + mean_nodes = get_nodes_by_type_from_node(node, "Subtract", invariant_types) + if mean_nodes and len(mean_nodes) == 1: + mean_node = mean_nodes[0] + if not test_constant(mean_node): + mean_node = None + + node = mean_node if mean_node is not None else node + scale_nodes = get_nodes_by_type_from_node(node, "Multiply", invariant_types) + if scale_nodes and len(scale_nodes) == 1: + scale_node = scale_nodes[0] + if not test_constant(scale_node): + scale_node = None + + return (mean_node, scale_node) + + for node in self: + if node.type != "Parameter": + continue + + # others + found = find_multiply_add(node) + if not any(found): + # onnx, paddle + found = find_subtract_divide(node) + found_ = find_subtract_multiply(node) + if len([i for i in found if i is not None]) < len([i for i in found_ if i is not None]): + found = found_ + + if not all(i is not None for i in found): + continue + + self._normalize_nodes.append(found) + for normalize_node in found: + if normalize_node is not None: + constant_node = [node_ for node_ in self.predecessors(normalize_node) if node_.type == "Constant"][ + 0 + ] + attrs = constant_node.attrs + attrs = asdict(attrs) + attrs["is_parameter"] = False + new_constant_node = constant_node.__class__( + constant_node.name, + data=constant_node.data.data, + **attrs, + ) + self.replace_node(constant_node, new_constant_node) + + def remove_normalize_nodes(self): + """Graph's remove_normalize_nodes function.""" + for nodes in self._normalize_nodes: + first_node, second_node = nodes + + if first_node is None: + first_node = second_node + elif second_node is None: + second_node = first_node + self.remove_node(first_node, keep_connect=True) + logger.info(f"Remove normalize node {first_node.name}") + try: + self.remove_node(second_node, keep_connect=True) + logger.info(f"Remove normalize node {second_node.name}") + except Exception: # pylint: disable=broad-exception-caught + pass + self._normalize_nodes = [] + + def topological_sort(self): + """Graph's topological_sort function.""" + return nx.topological_sort(self) + + def has_path(self, node_from: Operation, node_to: Operation): + """Graph's has_path function.""" + return nx.has_path(self, node_from, node_to) + + def clean_up( + self, + nodes_to_keep: List[Operation] = None, + remove_sub_components: bool = True, + ): + """Graph's clean_up function.""" + nodes_to_keep = nodes_to_keep if nodes_to_keep else [] + if remove_sub_components: + # clean up sub components + components = list(nx.connected_components(self.to_undirected())) + if nodes_to_keep: + for component in components: + if not set(nodes_to_keep).intersection(component): + super().remove_nodes_from(list(component)) + else: + components.sort(key=len, reverse=True) + # keep largest one only + components.pop(0) + for component in components: + super().remove_nodes_from(list(component)) + + # clean up isolated node + for node in nx.isolates(self): + if node not in nodes_to_keep: + super().remove_node(node) diff --git a/src/otx/core/ov/graph/parsers/__init__.py b/src/otx/core/ov/graph/parsers/__init__.py new file mode 100644 index 00000000000..7e45cd24ab9 --- /dev/null +++ b/src/otx/core/ov/graph/parsers/__init__.py @@ -0,0 +1,8 @@ +"""Module for otx.core.ov.graph.parser.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from .builder import PARSERS + +__all__ = ["PARSERS"] diff --git a/src/otx/core/ov/graph/parsers/builder.py b/src/otx/core/ov/graph/parsers/builder.py new file mode 100644 index 00000000000..79b1d3eddde --- /dev/null +++ b/src/otx/core/ov/graph/parsers/builder.py @@ -0,0 +1,8 @@ +"""Builder module for otx.core.ov.graph.parsers.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from otx.core.ov.registry import Registry + +PARSERS = Registry("ov graph parsers") diff --git a/src/otx/core/ov/graph/parsers/cls/__init__.py b/src/otx/core/ov/graph/parsers/cls/__init__.py new file mode 100644 index 00000000000..7cbd2f62383 --- /dev/null +++ b/src/otx/core/ov/graph/parsers/cls/__init__.py @@ -0,0 +1,8 @@ +"""Module for otx.core.ov.graph.parsers.cls.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from .cls_base_parser import cls_base_parser + +__all__ = ["cls_base_parser"] diff --git a/src/otx/core/ov/graph/parsers/cls/cls_base_parser.py b/src/otx/core/ov/graph/parsers/cls/cls_base_parser.py new file mode 100644 index 00000000000..0f0ceadb2ae --- /dev/null +++ b/src/otx/core/ov/graph/parsers/cls/cls_base_parser.py @@ -0,0 +1,105 @@ +"""Class base parser for otx.core.ov.graph.parsers.cls.cls_base_parser.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, List, Optional + +from otx.utils.logger import get_logger + +from ..builder import PARSERS +from ..parser import parameter_parser + +# pylint: disable=too-many-return-statements, too-many-branches + +logger = get_logger() + +NECK_INPUT_TYPES = ["ReduceMean", "MaxPool", "AvgPool"] +NECK_TYPES = [ + "Reshape", + "Squeeze", + "Unsqueeze", + "Concat", + "Convert", + "ShapeOf", + "StridedSlice", + "Transpose", +] + + +@PARSERS.register() +def cls_base_parser(graph, component: str = "backbone") -> Optional[Dict[str, List[str]]]: + """Class base parser for OMZ models.""" + assert component in ["backbone", "neck", "head"] + + result_nodes = graph.get_nodes_by_types(["Result"]) + if len(result_nodes) != 1: + logger.debug("More than one reulst nodes are found.") + return None + result_node = result_nodes[0] + + neck_input = None + for _, node_to in graph.bfs(result_node, True, 20): + if node_to.type in NECK_INPUT_TYPES: + logger.debug(f"Found neck_input: {node_to.name}") + neck_input = node_to + break + + if neck_input is None: + # logger.debug("Can not determine the output of backbone.") + return None + + neck_output = neck_input + for node_from, node_to in graph.bfs(neck_input, False, 10): + done = False + for node_to_ in node_to: + if node_to_.type not in NECK_TYPES: + done = True + break + neck_output = node_from + if done: + break + + if component == "backbone": + outputs = [node.name for node in graph.predecessors(neck_input) if node.type != "Constant"] + if len(outputs) != 1: + logger.debug(f"neck_input {neck_input.name} has more than one predecessors.") + return None + + inputs = parameter_parser(graph) + if len(inputs) != 1: + logger.debug("More than on parameter nodes are found.") + return None + + return dict( + inputs=inputs, + outputs=outputs, + ) + + if component == "neck": + return dict( + inputs=[neck_input.name], + outputs=[neck_output.name], + ) + + if component == "head": + head_inputs = list(graph.successors(neck_output)) + + outputs = graph.get_nodes_by_types(["Result"]) + if len(outputs) != 1: + logger.debug("More than one network output is found.") + return None + for node_from, node_to in graph.bfs(outputs[0], True, 5): + if node_to.type == "Softmax": + outputs = [node_from] + break + + if not graph.has_path(head_inputs[0], outputs[0]): + logger.debug(f"input({head_inputs[0].name}) and output({outputs[0].name}) are reversed") + return None + + return dict( + inputs=[input_.name for input_ in head_inputs], + outputs=[output.name for output in outputs], + ) + return None diff --git a/src/otx/core/ov/graph/parsers/parser.py b/src/otx/core/ov/graph/parsers/parser.py new file mode 100644 index 00000000000..650f3bf2e51 --- /dev/null +++ b/src/otx/core/ov/graph/parsers/parser.py @@ -0,0 +1,25 @@ +"""Parser modules for otx.core.ov.graph.parsers.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import List + + +def type_parser(graph, types) -> List[str]: + """Type Parser from graph, types.""" + found = [] + for node in graph: + if node.type in types: + found.append(node.name) + return found + + +def result_parser(graph) -> List[str]: + """Result Parser from graph.""" + return type_parser(graph, ["Result"]) + + +def parameter_parser(graph) -> List[str]: + """Parameter Parser from graph.""" + return type_parser(graph, ["Parameter"]) diff --git a/src/otx/core/ov/graph/utils.py b/src/otx/core/ov/graph/utils.py new file mode 100644 index 00000000000..c6bf2b69675 --- /dev/null +++ b/src/otx/core/ov/graph/utils.py @@ -0,0 +1,290 @@ +"""Utils for otx.core.ov.graph.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any, List + +import torch + +from otx.core.ov.graph import Graph +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.infrastructures import ConstantV0 +from otx.core.ov.ops.op import Operation +from otx.utils.logger import get_logger + +# pylint: disable=too-many-locals, protected-access, too-many-branches, too-many-statements, too-many-nested-blocks +logger = get_logger() + + +def get_constant_input_nodes(graph: Graph, node: Operation) -> List[Operation]: + """Getter constant input nodes from graph, node.""" + found = [] + for node_ in graph.predecessors(node): + if node_.type == "Constant": + found.append(node_) + return found + + +def handle_merging_into_batchnorm(graph, type_patterns=None, type_mappings=None): # noqa: C901 + """Merge function graph into batchnorm.""" + type_patterns = type_patterns if type_patterns else [["Multiply", "Add"]] + type_mappings = type_mappings if type_mappings else [{"gamma": 0, "beta": 1}] + assert len(type_patterns) == len(type_mappings) + batchnorm_cls = OPS.get_by_type_version("BatchNormInference", "opset1") + constant_cls = OPS.get_by_type_version("Constant", "opset1") + + for node in list(graph.nodes.keys()): + if node not in graph: + continue + nodes = [] + type_mapping = {} + for pattern_idx, type_pattern in enumerate(type_patterns): + type_mapping = type_mappings[pattern_idx] + nodes = graph.get_nodes_by_type_pattern(type_pattern, node) + if nodes and len(nodes) == 1: + nodes = nodes[0] + break + if not nodes: + continue + + is_normalize = False + for normalize_nodes in graph._normalize_nodes: + if set(nodes).intersection(normalize_nodes): + is_normalize = True + break + if is_normalize: + logger.info( + f"Skip merging {[i.name for i in nodes]} " + f"becuase they are part of normalization (preprocessing of IR)" + ) + continue + + shapes = [] + constants = [] + is_valid = True + for node in nodes: + constant = get_constant_input_nodes(graph, node) + if len(constant) != 1: + is_valid = False + break + constant = constant[0] + shapes.append(constant.shape) + constants.append(constant) + if not is_valid: + logger.info( + f"Skip merging {[i.name for i in nodes]} " f"becuase it has more than one weights for node {node.name}." + ) + continue + + if len(set(shapes)) != 1: + logger.info( + f"Skip merging {[i.name for i in nodes]} " f"becuase shape of weights are not the same. ({shapes})" + ) + continue + + if len(set(shapes[0][2:])) != 1 or shapes[0][2] != 1: + logger.info(f"Skip merging {[i.name for i in nodes]} " f"becuase shape of weights are not 1. ({shapes})") + continue + + channel_dim = shapes[0][1] + + name = nodes[0].name + "/merged_bn" + batchnorm = batchnorm_cls(name, shape=node.shape, epsilon=1e-10) + + gamma = ( + constants[type_mapping["gamma"]].data.squeeze() if "gamma" in type_mapping else torch.ones([channel_dim]) + ) + gamma = constant_cls( + gamma, + batchnorm.name + "/gamma", + shape=((channel_dim,),), + is_parameter=True, + ) + + beta = constants[type_mapping["beta"]].data.squeeze() if "beta" in type_mapping else torch.zeros([channel_dim]) + beta = constant_cls( + beta, + batchnorm.name + "/beta", + shape=((channel_dim,),), + is_parameter=True, + ) + + running_mean = ( + constants[type_mapping["running_mean"]].data.squeeze() + if "running_mean" in type_mapping + else torch.zeros([channel_dim]) + ) + running_mean = constant_cls( + running_mean, + batchnorm.name + "/running_mean", + shape=((channel_dim,),), + is_parameter=False, + ) + + running_variance = ( + constants[type_mapping["running_variance"]].data.squeeze() + if "running_variance" in type_mapping + else torch.ones([channel_dim]) + ) + running_variance = constant_cls( + running_variance, + batchnorm.name + "/running_variance", + shape=((channel_dim,),), + is_parameter=False, + ) + + logger.info(f"Merge {[i.name for i in nodes]} into batch normalization.") + edges = [] + for predecessor in graph.predecessors(nodes[0]): + if predecessor.type != "Constant": + edges_attrs = graph.get_edge_data(predecessor, nodes[0]) + assert len(edges_attrs) == 1 + for edge_attrs in edges_attrs: + edges.append({"node_from": predecessor, "node_to": batchnorm, **edge_attrs}) + for successor in graph.successors(nodes[-1]): + edges_attrs = graph.get_edge_data(nodes[-1], successor) + assert len(edges_attrs) == 1 + for edge_attrs in edges_attrs: + edges.append({"node_from": batchnorm, "node_to": successor, **edge_attrs}) + for node in nodes: + graph.remove_node(node) + for edge in edges: + graph.add_edge(**edge) + graph.add_edge(gamma, batchnorm) + graph.add_edge(beta, batchnorm) + graph.add_edge(running_mean, batchnorm) + graph.add_edge(running_variance, batchnorm) + + +def handle_paired_batchnorm(graph, replace: bool = False, types: List[str] = None): + """Handle function paired batchnorm.""" + types = types if types else ["Convolution", "GroupConvolution"] + batchnorm_cls = OPS.get_by_type_version("BatchNormInference", "opset1") + constant_cls = OPS.get_by_type_version("Constant", "opset1") + + for node in list(graph.nodes.keys()): + if node.type not in types: + continue + + # if input is 1x1x... this node is probably in Squeeze-and-exitation network + input_node, edge = list(graph.predecessors(node, True))[0] + assert len(edge) == 1 + edge = edge[0] + input_shape = input_node.shape[edge["out_port"]][2:] + if len(set(input_shape)) == 1 and input_shape[0] == 1: + logger.info( + f"Skip a paired batch normalization for {node.name} " f"becuase input shape to it is {input_shape}." + ) + continue + + bias_node_list: List[Any] = [n for n in graph.successors(node) if n.type == "Add"] + if len(bias_node_list) == 1: + bias_node = bias_node_list[0] + else: + bias_node = None + + # if bias node is not found we do not need to add batchnorm + if bias_node is None: + logger.info(f"Skip a paired batch normalization for {node.name} " "becuase it has no bias add node.") + continue + # if add node is not bias add node + if not isinstance(list(graph.predecessors(bias_node))[1], ConstantV0): + logger.info( + f"Skip a pared batch normalization for {node.name} " f"because {bias_node.name} is not a bias add node." + ) + continue + + node_name = node.name + channel_dim = node.attrs.shape[0][1] + + batchnorm = batchnorm_cls(node_name + "/paried_bn", shape=node.shape, epsilon=1e-10) + + gamma = torch.ones([channel_dim]) + gamma = constant_cls( + batchnorm.name + "/gamma", + data=gamma, + shape=((channel_dim,),), + is_parameter=True, + ) + if replace and bias_node is not None: + beta = list(graph.predecessors(bias_node))[1].data.squeeze() + else: + beta = torch.zeros([channel_dim]) + beta = constant_cls( + batchnorm.name + "/beta", + data=beta, + shape=((channel_dim,),), + is_parameter=True, + ) + running_mean = torch.zeros([channel_dim]) + running_mean = constant_cls( + batchnorm.name + "/running_mean", + data=running_mean, + shape=((channel_dim,),), + is_parameter=False, + ) + running_variance = torch.ones([channel_dim]) + running_variance = constant_cls( + batchnorm.name + "/running_variance", + data=running_variance, + shape=((channel_dim,),), + is_parameter=False, + ) + + if replace and bias_node is not None: + logger.info(f"Replace {bias_node.name} with a paired batch normalization.") + edges = [] + for successor in graph.successors(bias_node): + edges_attrs = graph.get_edge_data(bias_node, successor) + assert len(edges_attrs) == 1 + for edge_attrs in edges_attrs: + edges.append({"node_from": batchnorm, "node_to": successor, **edge_attrs}) + for predecessor in graph.predecessors(bias_node): + if predecessor.type != "Constant": + edges_attrs = graph.get_edge_data(predecessor, bias_node) + assert len(edges_attrs) == 1 + for edge_attrs in edges_attrs: + edges.append( + { + "node_from": predecessor, + "node_to": batchnorm, + **edge_attrs, + } + ) + graph.remove_node(bias_node) + for edge in edges: + graph.add_edge(**edge) + else: + logger.info(f"Append a paired batch normalization after {node.name}") + edges = [] + for successor in graph.successors(node): + edges_attrs = graph.get_edge_data(node, successor) + assert len(edges_attrs) == 1 + for edge_attrs in edges_attrs: + edges.append({"node_from": batchnorm, "node_to": successor, **edge_attrs}) + graph.remove_edge(node, successor) + for edge in edges: + graph.add_edge(**edge) + graph.add_edge(node, batchnorm) + + graph.add_edge(gamma, batchnorm) + graph.add_edge(beta, batchnorm) + graph.add_edge(running_mean, batchnorm) + graph.add_edge(running_variance, batchnorm) + + +def handle_reshape(graph): + """Reshape function.""" + for result in graph.get_nodes_by_types(["Result"]): + for node in graph.predecessors(result): + # some models, for example, dla-34, have reshape node as its predecessor + # of result node and the reshape node reshapes the tensor to [1, -1] + if node.type == "Reshape": + input_node, shape = list(graph.predecessors(node)) + if torch.equal(shape.data, torch.tensor([1, -1])): + for shape_ in input_node.shape[0][::-1]: + if shape_ != 1: + break + logger.info(f"Change reshape to [-1, {shape_}]") # pylint: disable=undefined-loop-variable + shape.data = torch.tensor([-1, shape_]) # pylint: disable=undefined-loop-variable diff --git a/src/otx/core/ov/models/__init__.py b/src/otx/core/ov/models/__init__.py new file mode 100644 index 00000000000..c439bc2ade1 --- /dev/null +++ b/src/otx/core/ov/models/__init__.py @@ -0,0 +1,14 @@ +"""Module for otx.core.ov.models.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from .mmov_model import MMOVModel +from .ov_model import OVModel # type: ignore[attr-defined] +from .parser_mixin import ParserMixin # type: ignore[attr-defined] + +__all__ = [ + "MMOVModel", + "OVModel", + "ParserMixin", +] diff --git a/src/otx/core/ov/models/mmov_model.py b/src/otx/core/ov/models/mmov_model.py new file mode 100644 index 00000000000..8abf27ef548 --- /dev/null +++ b/src/otx/core/ov/models/mmov_model.py @@ -0,0 +1,68 @@ +"""MMOVModel for otx.core.ov.models.mmov_model.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, List, Optional, Union + +import openvino.runtime as ov +import torch + +# TODO: Need to remove line 1 (ignore mypy) and fix mypy issues +from .ov_model import OVModel # type: ignore[attr-defined] +from .parser_mixin import ParserMixin # type: ignore[attr-defined] + +# TODO: Need to fix pylint issues +# pylint: disable=keyword-arg-before-vararg + + +class MMOVModel(OVModel, ParserMixin): + """MMOVModel for OMZ model type.""" + + def __init__( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + outputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + *args, + **kwargs + ): + parser = kwargs.pop("parser", None) + parser_kwargs = kwargs.pop("parser_kwargs", {}) + inputs, outputs = super().parse( + model_path_or_model=model_path_or_model, + weight_path=weight_path, + inputs=inputs, + outputs=outputs, + parser=parser, + **parser_kwargs, + ) + + super().__init__( + model_path_or_model=model_path_or_model, + weight_path=weight_path, + inputs=inputs, + outputs=outputs, + *args, + **kwargs, + ) + + def forward(self, inputs, gt_label=None): + """Function forward.""" + if isinstance(inputs, torch.Tensor): + inputs = (inputs,) + assert len(inputs) == len(self.inputs) + feed_dict = dict() + for key, input_ in zip(self.inputs, inputs): + feed_dict[key] = input_ + + if gt_label is not None: + assert "gt_label" not in self.features + self.features["gt_label"] = gt_label + + outputs = super().forward(**feed_dict) + outputs = tuple(outputs.values()) + if len(outputs) == 1: + outputs = outputs[0] + return outputs diff --git a/src/otx/core/ov/models/ov_model.py b/src/otx/core/ov/models/ov_model.py new file mode 100644 index 00000000000..aeca7db0397 --- /dev/null +++ b/src/otx/core/ov/models/ov_model.py @@ -0,0 +1,476 @@ +# type: ignore +# TODO: Need to remove line 1 (ignore mypy) and fix mypy issues +"""Modules for otx.core.ov.models.ov_model.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import math +import os +import tempfile +from collections import OrderedDict +from copy import deepcopy +from typing import Callable, List, Optional, Union + +import openvino.runtime as ov +import torch +from torch.nn import init + +from otx.utils.logger import get_logger + +from ..graph import Graph +from ..graph.utils import ( + handle_merging_into_batchnorm, + handle_paired_batchnorm, + handle_reshape, +) +from ..ops.builder import OPS +from ..utils import load_ov_model, normalize_name + +CONNECTION_SEPARATOR = "||" + +# pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements +logger = get_logger() + + +class OVModel(torch.nn.Module): # pylint: disable=too-many-instance-attributes + """OVModel class.""" + + def __init__( # noqa: C901 + self, + model_path_or_model: Union[str, ov.Model] = None, + weight_path: Optional[str] = None, + inputs: Optional[Union[str, List[str]]] = None, + outputs: Optional[Union[str, List[str]]] = None, + features_to_keep: Optional[List] = None, + remove_normalize: bool = False, + merge_bn: bool = True, + paired_bn: bool = True, + init_weight: Union[bool, Callable] = False, + verify_shape: bool = True, + ): + super().__init__() + self._model_path_or_model = model_path_or_model + self._weight_path = weight_path + self._remove_normalize = remove_normalize + self._features_to_keep = features_to_keep + self._merge_bn = merge_bn + self._paired_bn = paired_bn + self._init_weight = init_weight + self._verify_shape = verify_shape + + self._inputs: List[str] = [] + self._outputs: List[str] = [] + self._feature_dict = OrderedDict() + + # build graph + graph = self.build_graph(model_path_or_model, weight_path) + self._graph = graph + if remove_normalize: + graph.remove_normalize_nodes() + + # handle inputs + if inputs: + inputs = inputs if isinstance(inputs, list) else [inputs] + assert all(isinstance(i, str) for i in inputs), f"input must be string but {inputs} is given" + inputs = self.build_custom_inputs(graph, deepcopy(inputs)) + else: + inputs = [node.name for node in graph.get_nodes_by_types(["Parameter"])] + self._inputs = inputs + + # handle outputs + if outputs: + outputs = outputs if isinstance(outputs, list) else [outputs] + assert all(isinstance(i, str) for i in outputs), f"input must be string but {outputs} is given" + outputs = self.build_custom_outputs(graph, deepcopy(outputs)) + else: + outputs = [node.name for node in graph.get_nodes_by_types(["Result"])] + self._outputs = outputs + + # clean up graph + self.clean_up(graph, inputs, outputs) + + handle_reshape(graph) + if merge_bn: + handle_merging_into_batchnorm(graph) + if paired_bn: + handle_paired_batchnorm(graph, replace=True) + + # clean up graph + self.clean_up(graph, inputs, outputs) + + # build torch module + self.model = self.build_torch_module(graph) + + if init_weight: + if not isinstance(init_weight, Callable): + + # internal init weight + def init_weight(module, graph): # pylint: disable=function-redefined + from ..ops.op import Operation + + if not isinstance(module, Operation): + return + + if module.TYPE == "BatchNormInference": + _, gamma, beta, mean, var = list(graph.predecessors(module)) + init.ones_(gamma.data) + init.zeros_(beta.data) + mean.data.zero_() + var.data.fill_(1) + logger.info(f"Initialize {module.TYPE} -> {module.name}") + elif module.TYPE in [ + "Convolution", + "GroupConvolution", + "MatMul", + ]: + for weight in graph.predecessors(module): + if weight.TYPE == "Constant" and isinstance(weight.data, torch.nn.parameter.Parameter): + init.kaiming_uniform_(weight.data, a=math.sqrt(5)) + logger.info(f"Initialize {module.TYPE} -> {module.name}") + elif module.TYPE in [ + "Multiply", + "Divide", + "Add", + "Subtract", + ]: + for weight in graph.predecessors(module): + if weight.TYPE == "Constant" and isinstance(weight.data, torch.nn.parameter.Parameter): + fan_in, _ = init._calculate_fan_in_and_fan_out( # pylint: disable=protected-access + weight.data + ) + bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0 + init.uniform_(weight.data, -bound, bound) + logger.info(f"Initialize {module.TYPE} -> {module.name}") + + self.model.apply(lambda m: init_weight(m, graph)) + + for node in self._graph.get_nodes_by_types(["Parameter"]): + node.attrs.verify_shape = verify_shape + + input_shapes = {} + output_shapes = {} + for node in self._graph.get_nodes_by_types(["Parameter", "Result"]): + if node.name in self._inputs: + input_shapes[node.name] = node.shape[0] + elif node.name in self._outputs: + output_shapes[node.name] = node.shape[0] + self._input_shapes = OrderedDict() + self._output_shapes = OrderedDict() + for input_ in self._inputs: + self._input_shapes[input_] = input_shapes[input_] + for output in self._outputs: + self._output_shapes[output] = output_shapes[output] + + @property + def inputs(self): + """Property inputs.""" + return self._inputs + + @property + def outputs(self): + """Property outputs.""" + return self._outputs + + @property + def features(self): + """Property features.""" + return self._feature_dict + + @property + def input_shapes(self): + """Property input_shapes.""" + return self._input_shapes + + @property + def output_shapes(self): + """Property output_shapes.""" + return self._output_shapes + + @staticmethod + def build_graph(model_path_or_model, weight_path=None): + """Function build_graph.""" + with tempfile.TemporaryDirectory() as tempdir: + if isinstance(model_path_or_model, ov.Model): + assert weight_path is None, "if openvino model is given 'weight_path' must be None" + ov.serialize( + model_path_or_model, + os.path.join(tempdir, "model.xml"), + os.path.join(tempdir, "model.bin"), + ) + model_path_or_model = os.path.join(tempdir, "model.xml") + weight_path = os.path.join(tempdir, "model.bin") + # TODO: reshape decompose ir graph + ov_model = load_ov_model(model_path_or_model, weight_path, False) + graph = Graph.from_ov(ov_model) + return graph + + @staticmethod + def build_custom_outputs(graph, outputs): # noqa: C901 + """Function build_custom_outputs.""" + cls_result = OPS.get_by_type_version("Result", "opset1") + node_dict = OrderedDict((i.name, i) for i in graph.topological_sort()) + + if not isinstance(outputs, list): + outputs = [outputs] + + nodes_to_remove = [] + edges_to_add = {} + for i, output in enumerate(outputs): + output = normalize_name(output) + output = output.split(CONNECTION_SEPARATOR) + explicit_tgt = False + + if len(output) == 1: + src = output[0] + tgt = None + elif len(output) == 2: + src, tgt = output + explicit_tgt = True + else: + raise ValueError() + + src = node_dict[src] + if src.type == "Result": + continue + + if explicit_tgt: + tgt = node_dict[tgt] + else: + tgt = list(graph.successors(src))[0] + + output_result = f"{src.name}/result_{i}" + outputs[i] = output_result + + if src not in edges_to_add: + edges_to_add[src] = [] + + for successor in graph.successors(src): + if tgt == successor: + edges_attrs = graph.get_edge_data(src, successor) + assert len(edges_attrs) == 1 + + output_result = cls_result(output_result, shape=src.shape) + for edge_attrs in edges_attrs: + edges_to_add[src].append({"node_from": src, "node_to": output_result, **edge_attrs}) + if explicit_tgt and tgt != successor: + continue + nodes_to_remove.append(successor) + + # handle duplicated successors + merge_candidates = [k for k, v in edges_to_add.items() if len(v) > 1] + if merge_candidates: + for merge_candidate in merge_candidates: + edges = edges_to_add[merge_candidate] + seen = {} + out_ports = [edge["out_port"] for edge in edges] + for idx in reversed(range(len(out_ports))): + out_port = out_ports[idx] + if out_port in seen: + edge = edges.pop(idx) + outputs.pop(outputs.index(edge["node_to"].name)) + else: + seen[out_port] = edges[idx] + + if edges_to_add: + for edges in edges_to_add.values(): + for edge in edges: + edge["in_port"] = 0 + assert {len(edges) for edges in edges_to_add.values()} == {1} + edges_to_add = [edge for edges in edges_to_add.values() for edge in edges] + else: + edges_to_add = [] + + for node in set(nodes_to_remove): + graph.remove_node(node) + for edge in edges_to_add: + graph.add_edge(**edge) + return outputs + + @staticmethod + def build_custom_inputs(graph, inputs: Union[str, List[str]]): # noqa: C901 + """Function build_custom_inputs.""" + cls_param = OPS.get_by_type_version("Parameter", "opset1") + node_dict = OrderedDict((i.name, i) for i in graph.topological_sort()) + + if not isinstance(inputs, list): + inputs = [inputs] + + edges_to_add = {} + nodes_to_remove = [] + for i, input_ in enumerate(inputs): + input_ = normalize_name(input_) + input_ = input_.split(CONNECTION_SEPARATOR) + explicit_src = False + + if len(input_) == 1: + src = None + tgt = input_[0] + elif len(input_) == 2: + src, tgt = input_ + explicit_src = True + else: + raise ValueError() + + tgt = node_dict[tgt] + if tgt.type == "Parameter": + continue + + if explicit_src: + src = node_dict[src] + else: + src = list(graph.predecessors(tgt))[0] + + input_parameter = f"{tgt.name}/parameter_{i}" + inputs[i] = input_parameter + + if src not in edges_to_add: + edges_to_add[src] = [] + + for predecessor in graph.predecessors(tgt): + if src == predecessor: + edges_attrs = graph.get_edge_data(predecessor, tgt) + assert len(edges_attrs) == 1 + + # TODO: here, we force the batch dim to be dynamic + # it is assumed to be dim 0 + new_shape = [] + for shape in predecessor.shape: + new_shape.append([-1 if j == 0 else k for j, k in enumerate(shape)]) + new_shape = tuple(tuple(shape) for shape in new_shape) + input_parameter = cls_param(input_parameter, shape=new_shape) + for edge_attrs in edges_attrs: + edges_to_add[src].append({"node_from": input_parameter, "node_to": tgt, **edge_attrs}) + if (explicit_src and src != predecessor) or predecessor.type == "Constant": + continue + nodes_to_remove.append(predecessor) + + # handle duplicated predecessors + merge_candidates = [k for k, v in edges_to_add.items() if len(v) > 1] + if merge_candidates: + for merge_candidate in merge_candidates: + ctr = 0 + edges = edges_to_add[merge_candidate] + seen = {} + out_ports = [edge["out_port"] for edge in edges] + for idx in reversed(range(len(out_ports))): + out_port = out_ports[idx] + if out_port in seen: + edge = edges.pop(idx) + inputs.pop(inputs.index(edge["node_from"].name)) + edge["node_from"] = seen[out_port]["node_from"] + edges_to_add[f"{merge_candidate.name}_{ctr}"] = [edge] + ctr += 1 + else: + seen[out_port] = edges[idx] + + if edges_to_add: + for edges in edges_to_add.values(): + for edge in edges: + edge["out_port"] = 0 + assert {len(edges) for edges in edges_to_add.values()} == {1} + edges_to_add = [edge for edges in edges_to_add.values() for edge in edges] + else: + edges_to_add = [] + + for node in set(nodes_to_remove): + graph.remove_node(node) + for edge in edges_to_add: + graph.add_edge(**edge) + return inputs + + @staticmethod + def clean_up(graph, inputs=None, outputs=None): + """Function clean_up.""" + inputs = inputs if inputs else [] + outputs = outputs if outputs else [] + nodes = list(graph.topological_sort()) + nodes_to_keep = [] + for node in nodes: + if node.name in inputs or node.name in outputs: + nodes_to_keep.append(node) + + def get_nodes_without_successors(graph, ignores=None): + ignores = ignores if ignores else [] + outputs = [] + for node in reversed(list(graph.topological_sort())): + if not list(graph.successors(node)) and node not in ignores: + outputs.append(node) + return outputs + + nodes = get_nodes_without_successors(graph, nodes_to_keep) + while nodes: + graph.remove_nodes_from(nodes) + nodes = get_nodes_without_successors(graph, nodes_to_keep) + + graph.clean_up(nodes_to_keep) + + @staticmethod + def build_torch_module(graph): + """Function build_torch_module.""" + node_dict = OrderedDict((i.name, i) for i in graph.topological_sort()) + return torch.nn.ModuleDict(list(node_dict.items())) + + def _build_forward_inputs(self, *args, **kwargs): + """Function _build_forward_inputs.""" + inputs = {} + if args: + for key, arg in zip(self._inputs, args): + inputs[key] = arg + if kwargs: + for key, arg in kwargs.items(): + if key in inputs: + raise ValueError + inputs[key] = arg + return inputs + + def forward(self, *args, **kwargs): + """Function forward.""" + self._feature_dict.clear() + inputs = self._build_forward_inputs(*args, **kwargs) + + done = {} + for node_name, node in self.model.items(): + done[node_name] = {node.name: False for node in self._graph.successors(node)} + + for node_name, node in self.model.items(): + predecessors_with_edge = list(self._graph.predecessors(node, with_edge_data=True)) + if not predecessors_with_edge: + if node.type == "Parameter": + self._feature_dict[node_name] = node(inputs[node_name]) + elif node.type == "Constant": + self._feature_dict[node_name] = node() + else: + raise ValueError( + f"Broken graph. Node {node_name} is a type of {node.type} " "but it has no in edges." + ) + else: + input_nodes, edges = list(map(list, zip(*predecessors_with_edge))) + input_node_names = [input_node.name for input_node in input_nodes] + + input_features = [edge["in_port"] for edges_ in edges for edge in edges_] + assert len(input_features) == len(set(input_features)) + input_features = [None for _ in input_features] + for idx, input_node_name in enumerate(input_node_names): + if self._features_to_keep is not None and input_node_name in self._features_to_keep: + input_feature = self._feature_dict.get(input_node_name) + else: + input_feature = self._feature_dict.pop(input_node_name) + done[input_node_name][node_name] = True + if not all(done[input_node_name].values()): + self._feature_dict[input_node_name] = input_feature + + if isinstance(input_feature, tuple): + for edges_ in edges[idx]: + input_features[edges_["in_port"]] = input_feature[edges_["out_port"]] + else: + for edges_ in edges[idx]: + input_features[edges_["in_port"]] = input_feature + assert all(input_feature is not None for input_feature in input_features) + self._feature_dict[node_name] = node(*input_features) + + outputs = OrderedDict() + for output_name in self._outputs: + outputs[output_name] = self._feature_dict[output_name] + + return outputs diff --git a/src/otx/core/ov/models/parser_mixin.py b/src/otx/core/ov/models/parser_mixin.py new file mode 100644 index 00000000000..bb49e0ae36f --- /dev/null +++ b/src/otx/core/ov/models/parser_mixin.py @@ -0,0 +1,62 @@ +# type: ignore +# TODO: Need to remove line 1 (ignore mypy) and fix mypy issues +"""Parser mixin modules for otx.core.ov.models.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Callable, Dict, List, Optional, Tuple, Union + +import openvino.runtime as ov + +from otx.utils.logger import get_logger + +from ..graph.parsers.builder import PARSERS +from .ov_model import OVModel + +logger = get_logger() + + +class ParserMixin: + """ParserMixin class.""" + + def parse( + self, + model_path_or_model: Union[str, ov.Model], + weight_path: Optional[str] = None, + inputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + outputs: Optional[Union[Dict[str, Union[str, List[str]]], List[str], str]] = None, + parser: Optional[Union[str, Callable]] = None, + **kwargs, + ) -> Tuple[Union[str, List[str]], Union[str, List[str]]]: + """Parse function of ParserMixin class.""" + parser = self.parser if parser is None else parser + if isinstance(parser, str): + parser = PARSERS.get(parser) + + if not inputs or not outputs: + graph = OVModel.build_graph(model_path_or_model, weight_path) + parsed = parser(graph, **kwargs) + + if not isinstance(parsed, dict) or ("inputs" not in parsed and "outputs" not in parsed): + raise ValueError(f"parser {parser} failed to find inputs and outputs of model. ") + if isinstance(parsed["inputs"], dict) != isinstance(parsed["outputs"], dict): + raise ValueError(f"output of parser ({parser}) is not consistent") + if isinstance(parsed["inputs"], dict) and isinstance(parsed["outputs"], dict): + if set(parsed["inputs"].keys()) != set(parsed["outputs"].keys()): + raise ValueError( + f"input keys {parsed['inputs'].keys()} and " + f"output keys {parsed['outputs'].keys()} are different." + ) + + inputs = parsed["inputs"] if not inputs else inputs + outputs = parsed["outputs"] if not outputs else outputs + logger.info(f"inputs: {inputs}") + logger.info(f"outputs: {outputs}") + + return inputs, outputs + + @staticmethod + def parser(graph, **kwargs) -> Dict[str, Union[List[str], Dict[str, List[str]]]]: # pylint: disable=unused-argument + """Function parser.""" + return dict(inputs=[], outputs=[]) diff --git a/src/otx/core/ov/omz_wrapper.py b/src/otx/core/ov/omz_wrapper.py new file mode 100644 index 00000000000..6567f87b5fb --- /dev/null +++ b/src/otx/core/ov/omz_wrapper.py @@ -0,0 +1,400 @@ +"""OMZ wrapper-related code for otx.core.ov.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import hashlib +import os +import shutil +import string +import sys +import time +from pathlib import Path +from typing import Dict, List + +import requests +from openvino.model_zoo import _common, _reporting +from openvino.model_zoo._configuration import load_models +from openvino.model_zoo.download_engine.downloader import Downloader +from openvino.model_zoo.download_engine.postprocessing import PostprocUnpackArchive +from openvino.model_zoo.omz_converter import ModelOptimizerProperties, convert_to_onnx +from requests.exceptions import HTTPError + +from otx.core.file import OTX_CACHE + +# pylint: disable=too-many-locals, too-many-branches +OMZ_CACHE = os.path.join(OTX_CACHE, "omz") +os.makedirs(OMZ_CACHE, exist_ok=True) + + +OMZ_PUBLIC_MODELS: Dict[str, List[str]] = dict( + cls=[ + "alexnet", + "caffenet", + # "convnext-tiny", # omz_downloader does not support + "densenet-121", + "densenet-121-tf", + "dla-34", + "efficientnet-b0", + "efficientnet-b0-pytorch", + "efficientnet-v2-b0", + "efficientnet-v2-s", + "hbonet-1.0", + "hbonet-0.25", + "googlenet-v1", + "googlenet-v1-tf", + "googlenet-v2", + "googlenet-v2-tf", + "googlenet-v3", + "googlenet-v3-pytorch", + "googlenet-v4-tf", + "inception-resnet-v2-tf", + # "levit-128s", # IR has hard-codeded batch size of 1 + "mixnet-l", + "mobilenet-v1-0.25-128", + "mobilenet-v1-1.0-224", + "mobilenet-v1-1.0-224-tf", + "mobilenet-v2", + "mobilenet-v2-1.0-224", + "mobilenet-v2-pytorch", + "mobilenet-v2-1.4-224", + "mobilenet-v3-small-1.0-224-tf", + "mobilenet-v3-large-1.0-224-tf", + # "nfnet-f0", # mo 2022.2 bug + # "regnetx-3.2gf", # omz_converter does not support + "octave-resnet-26-0.25", + # "repvgg-a0", # trainig and inference architecture are difference + # "repvgg-b1", # trainig and inference architecture are difference + # "repvgg-b3", # trainig and inference architecture are difference + # "resnest-50-pytorch", # IR has hard-coded batch size of 1 + "resnet-18-pytorch", + "resnet-34-pytorch", + "resnet-50-pytorch", + "resnet-50-tf", + # "rexnet-v1-x1.0", # IR has hard-coded batch size of 1 + "se-inception", + "se-resnet-50", + "se-resnext-50", + "shufflenet-v2-x0.5", + # "shufflenet-v2-x1.0", # IR has hard-coded batch size of 1 + "squeezenet1.0", + "squeezenet1.1", + # "swin-tiny-patch4-window7-224", # IR has hard-coded batch size of 1 + # "t2t-vit-14", # IR has hard-coded batch size of 1 + "vgg16", + "vgg19", + ], + det=[], + seg=[], +) + + +AVAILABLE_OMZ_MODELS: List[str] = [] +for models_ in OMZ_PUBLIC_MODELS.values(): + for model_ in models_: + AVAILABLE_OMZ_MODELS.append(model_) + + +class NameSpace: + """NameSpace class for otx.core.ov.omz_wrapper.""" + + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + +def _get_etag(url): + """Getter etag function from url.""" + try: + response = requests.head(url, allow_redirects=True, timeout=100) + if response.status_code != 200: + return None + return response.headers.get("ETag", None) + except HTTPError: + return None + + +def _get_ir_path(directory): + """Getter IR path function from directory path.""" + directory = Path(directory) + model_path = list(directory.glob("**/*.xml")) + weight_path = list(directory.glob("**/*.bin")) + if model_path and weight_path: + assert len(model_path) == 1 and len(weight_path) == 1 + return dict(model_path=model_path[0], weight_path=weight_path[0]) + return None + + +def _run_pre_convert(reporter, model, output_dir, args): + """Run pre-converting function.""" + script = _common.MODEL_ROOT / model.subdirectory_ori / "pre-convert.py" + if not script.exists(): + return True + + reporter.print_section_heading( + "{}Running pre-convert script for {}", + "(DRY RUN) " if args.dry_run else "", + model.name, + ) + + cmd = [ + str(args.python), + "--", + str(script), + "--", + str(args.download_dir / model.subdirectory), + str(output_dir / model.subdirectory), + ] + + reporter.print("Pre-convert command: {}", _common.command_string(cmd)) + reporter.print(flush=True) + + success = True if args.dry_run else reporter.job_context.subprocess(cmd) + reporter.print() + + return success + + +def _update_model(model): + """Update model configs for omz_wrapper.""" + m_hash = hashlib.sha256() + for file in model.files: + url = file.source.url + etag = _get_etag(url) + if etag is not None: + m_hash.update(bytes(etag, "utf-8")) + model.subdirectory_ori = model.subdirectory + model.subdirectory = Path(m_hash.hexdigest()) + + # FIXME: a bug from openvino-dev==2022.3.0 + # It has been fixed on master branch. + # After upgrading openvino-dev, we can remove this temporary patch + if getattr(model, "conversion_to_onnx_args") and not [ + arg for arg in model.conversion_to_onnx_args if arg.startswith("--model-path") + ]: + model.conversion_to_onnx_args.append("--model-path=") + + +def get_model_configuration(model_name): + """Getter function of model configuration from name.""" + model_configurations = load_models(_common.MODEL_ROOT, {}) + for model in model_configurations: + if model.name == model_name: + _update_model(model) + return model + return None + + +def download_model(model, download_dir=OMZ_CACHE, precisions=None, force=False): + """Function for downloading model from directory.""" + download_dir = Path("") if download_dir is None else Path(download_dir) + precisions = precisions if precisions else {"FP32"} + + # TODO: need delicate cache management + if not force and (download_dir / model.subdirectory).exists(): + target_file_names = [] + for postprocessing in model.postprocessing: + if isinstance(postprocessing, PostprocUnpackArchive): + target_file_names.append(postprocessing.file) + + done = [False for _ in model.files] + for i, file in enumerate(model.files): + filename = file.name + if filename in target_file_names: + # TODO + # here, we assume unarchive is done + done[i] = True + continue + if os.path.exists(download_dir / model.subdirectory / filename): + done[i] = True + + if all(done): + return + + reporter = Downloader.make_reporter("text") + downloader = Downloader(precisions, download_dir) + failed_models = downloader.bulk_download_model([model], reporter, 1, "text") + if failed_models: + reporter.print("FAILED:") + for failed_model_name in failed_models: + reporter.print(failed_model_name) + sys.exit(1) + + +def _convert(reporter, model, output_dir, namespace, mo_props, requested_precisions): + """Convert function for OMZ wrapper.""" + if model.mo_args is None: + reporter.print_section_heading("Skipping {} (no conversions defined)", model.name) + reporter.print() + return True + + model_precisions = requested_precisions & model.precisions + if not model_precisions: + reporter.print_section_heading("Skipping {} (all conversions skipped)", model.name) + reporter.print() + return True + + (output_dir / model.subdirectory).mkdir(parents=True, exist_ok=True) + + if not _run_pre_convert(reporter, model, output_dir, namespace): + return False + + model_format = model.framework + mo_extension_dir = mo_props.base_dir / "extensions" + if not mo_extension_dir.exists(): + mo_extension_dir = mo_props.base_dir + + template_variables = { + "config_dir": _common.MODEL_ROOT / model.subdirectory_ori, + "conv_dir": output_dir / model.subdirectory, + "dl_dir": namespace.download_dir / model.subdirectory, + "mo_dir": mo_props.base_dir, + "mo_ext_dir": mo_extension_dir, + } + + if model.conversion_to_onnx_args: + if not convert_to_onnx(reporter, model, output_dir, namespace, template_variables): + return False + model_format = "onnx" + + expanded_mo_args = [string.Template(arg).substitute(template_variables) for arg in model.mo_args] + + for model_precision in sorted(model_precisions): + data_type = model_precision.split("-")[0] + layout_string = ",".join(f"{input.name}({input.layout})" for input in model.input_info if input.layout) + shape_string = ",".join(str(input.shape) for input in model.input_info if input.shape) + + if layout_string: + expanded_mo_args.append(f"--layout={layout_string}") + if shape_string: + expanded_mo_args.append(f"--input_shape={shape_string}") + + mo_cmd = [ + *mo_props.cmd_prefix, + f"--framework={model_format}", + f"--output_dir={output_dir / model.subdirectory / model_precision}", + f"--model_name={model.name}", + f"--input={','.join(input.name for input in model.input_info)}".format(), + *expanded_mo_args, + *mo_props.extra_args, + ] + if "FP16" in data_type: + mo_cmd.append("--compress_to_fp16") + + reporter.print_section_heading( + "{}Converting {} to IR ({})", + "(DRY RUN) " if namespace.dry_run else "", + model.name, + model_precision, + ) + + reporter.print("Conversion command: {}", _common.command_string(mo_cmd)) + + if not namespace.dry_run: + reporter.print(flush=True) + + if not reporter.job_context.subprocess(mo_cmd): + # NOTE: mo returns non zero return code (245) even though it successfully generate IR + cur_time = time.time() + time_threshold = 5 + xml_path = output_dir / model.subdirectory / model_precision / f"{model.name}.xml" + bin_path = output_dir / model.subdirectory / model_precision / f"{model.name}.bin" + if not ( + os.path.exists(xml_path) + and os.path.exists(bin_path) + and os.path.getmtime(xml_path) - cur_time < time_threshold + and os.path.getmtime(bin_path) - cur_time < time_threshold + ): + return False + + reporter.print() + + return True + + +def convert_model( + model, + download_dir=OMZ_CACHE, + output_dir=OMZ_CACHE, + precisions=None, + force=False, + *args, +): # pylint: disable=keyword-arg-before-vararg + """Converting model for OMZ wrapping.""" + download_dir = Path("") if download_dir is None else Path(download_dir) + output_dir = Path("") if output_dir is None else Path(output_dir) + precisions = precisions if precisions else {"FP32"} + + out = _get_ir_path(output_dir / model.subdirectory) + if out and not force: + return out + + namespace = NameSpace( + python=shutil.which("python"), + dry_run=False, + download_dir=download_dir, + ) + + mo_executable = shutil.which("mo") + + if mo_executable: + mo_path = Path(mo_executable) + else: + try: + mo_path = Path(os.environ["INTEL_OPENVINO_DIR"]) / "tools/mo/openvino/tools/mo/mo.py" + if not mo_path.exists(): + mo_path = Path(os.environ["INTEL_OPENVINO_DIR"]) / "tools/model_optimizer/mo.py" + except KeyError: + sys.exit( + "Unable to locate Model Optimizer. " + + "Use --mo or run setupvars.sh/setupvars.bat from the OpenVINO toolkit." + ) + + mo_path = mo_path.resolve() + mo_cmd_prefix = [namespace.python, "--", str(mo_path)] + + if str(mo_path).lower().endswith(".py"): + mo_dir = mo_path.parent + else: + mo_package_path, stderr = _common.get_package_path(namespace.python, "openvino.tools.mo") + mo_dir = mo_package_path + + if mo_package_path is None: + mo_package_path, stderr = _common.get_package_path(args.python, "mo") + if mo_package_path is None: + sys.exit(f"Unable to load Model Optimizer. Errors occurred: {stderr}") + mo_dir = mo_package_path.parent + + reporter = _reporting.Reporter(_reporting.DirectOutputContext()) + mo_props = ModelOptimizerProperties( + cmd_prefix=mo_cmd_prefix, + extra_args=[], + base_dir=mo_dir, + ) + shared_convert_args = (output_dir, namespace, mo_props, precisions) + + results = [] + models = [] + if model.model_stages: + for model_stage in model.model_stages: + results.append(_convert(reporter, model_stage, *shared_convert_args)) + models.append(model_stage) + else: + results.append(_convert(reporter, model, *shared_convert_args)) + models.append(model) + + failed_models = [model.name for model, successful in zip(models, results) if not successful] + + if failed_models: + reporter.print("FAILED:") + for failed_model_name in failed_models: + reporter.print(failed_model_name) + sys.exit(1) + + return _get_ir_path(output_dir / model.subdirectory) + + +def get_omz_model(model_name, download_dir=OMZ_CACHE, output_dir=OMZ_CACHE, force=False): + """Get OMZ model from name and download_dir.""" + model = get_model_configuration(model_name) + download_model(model, download_dir=download_dir, force=force) + return convert_model(model, download_dir=download_dir, output_dir=output_dir, force=force) diff --git a/src/otx/core/ov/ops/__init__.py b/src/otx/core/ov/ops/__init__.py new file mode 100644 index 00000000000..de96fffb4d9 --- /dev/null +++ b/src/otx/core/ov/ops/__init__.py @@ -0,0 +1,140 @@ +"""Module of otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from .activations import ( + ClampV0, + EluV0, + ExpV0, + GeluV7, + HardSigmoidV0, + HSigmoidV5, + HSwishV4, + MishV4, + PReluV0, + ReluV0, + SeluV0, + SigmoidV0, + SoftMaxV0, + SoftMaxV1, + SwishV4, + TanhV0, +) +from .arithmetics import AddV1, DivideV1, MultiplyV1, SubtractV1, TanV0 +from .builder import OPS, OperationRegistry +from .convolutions import ConvolutionV1, GroupConvolutionV1 +from .generation import RangeV4 +from .image_processings import InterpolateV4 +from .infrastructures import ConstantV0, ParameterV0, ResultV0 +from .matmuls import EinsumV7, MatMulV0 +from .movements import ( + BroadcastV3, + ConcatV0, + GatherV0, + GatherV1, + PadV1, + ScatterNDUpdateV3, + ScatterUpdateV3, + ShuffleChannelsV0, + SplitV1, + StridedSliceV1, + TileV0, + TransposeV1, + VariadicSplitV1, +) +from .normalizations import ( + MVNV6, + BatchNormalizationV0, + LocalResponseNormalizationV0, + NormalizeL2V0, +) +from .object_detections import ( + DetectionOutputV0, + PriorBoxClusteredV0, + PriorBoxV0, + ProposalV4, + RegionYoloV0, + ROIPoolingV0, +) +from .op import Attribute, Operation +from .poolings import AvgPoolV1, MaxPoolV0 +from .reductions import ReduceMeanV1, ReduceMinV1, ReduceProdV1, ReduceSumV1 +from .shape_manipulations import ReshapeV1, ShapeOfV0, ShapeOfV3, SqueezeV0, UnsqueezeV0 +from .sorting_maximization import NonMaxSuppressionV5, NonMaxSuppressionV9, TopKV3 +from .type_conversions import ConvertV0 + +__all__ = [ + "SoftMaxV0", + "SoftMaxV1", + "ReluV0", + "SwishV4", + "SigmoidV0", + "ClampV0", + "PReluV0", + "TanhV0", + "EluV0", + "SeluV0", + "MishV4", + "HSwishV4", + "HSigmoidV5", + "ExpV0", + "HardSigmoidV0", + "GeluV7", + "MultiplyV1", + "DivideV1", + "AddV1", + "SubtractV1", + "TanV0", + "OPS", + "OperationRegistry", + "ConvolutionV1", + "GroupConvolutionV1", + "RangeV4", + "InterpolateV4", + "ParameterV0", + "ResultV0", + "ConstantV0", + "MatMulV0", + "EinsumV7", + "PadV1", + "ConcatV0", + "TransposeV1", + "GatherV0", + "GatherV1", + "StridedSliceV1", + "SplitV1", + "VariadicSplitV1", + "ShuffleChannelsV0", + "BroadcastV3", + "ScatterNDUpdateV3", + "ScatterUpdateV3", + "TileV0", + "BatchNormalizationV0", + "LocalResponseNormalizationV0", + "NormalizeL2V0", + "MVNV6", + "ProposalV4", + "ROIPoolingV0", + "DetectionOutputV0", + "RegionYoloV0", + "PriorBoxV0", + "PriorBoxClusteredV0", + "Operation", + "Attribute", + "MaxPoolV0", + "AvgPoolV1", + "ReduceMeanV1", + "ReduceProdV1", + "ReduceMinV1", + "ReduceSumV1", + "SqueezeV0", + "UnsqueezeV0", + "ReshapeV1", + "ShapeOfV0", + "ShapeOfV3", + "TopKV3", + "NonMaxSuppressionV5", + "NonMaxSuppressionV9", + "ConvertV0", +] diff --git a/src/otx/core/ov/ops/activations.py b/src/otx/core/ov/ops/activations.py new file mode 100644 index 00000000000..908e1c95743 --- /dev/null +++ b/src/otx/core/ov/ops/activations.py @@ -0,0 +1,356 @@ +"""Activation-related modules for otx.core.ov.ops.activations.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import math +from dataclasses import dataclass, field + +import torch +from torch.nn import functional as F + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.op import Attribute, Operation + + +@dataclass +class SoftMaxV0Attribute(Attribute): + """SoftMaxV0Attribute class.""" + + axis: int = field(default=1) + + +@OPS.register() +class SoftMaxV0(Operation[SoftMaxV0Attribute]): + """SoftMaxV0 class.""" + + TYPE = "Softmax" + VERSION = "opset1" + ATTRIBUTE_FACTORY = SoftMaxV0Attribute + + def forward(self, inputs): + """SoftMaxV0's forward function.""" + return F.softmax(input=inputs, dim=self.attrs.axis) + + +@dataclass +class SoftMaxV1Attribute(Attribute): + """SoftMaxV1Attribute class.""" + + axis: int = field(default=1) + + +@OPS.register() +class SoftMaxV1(Operation[SoftMaxV1Attribute]): + """SoftMaxV1 class.""" + + TYPE = "Softmax" + VERSION = "opset8" + ATTRIBUTE_FACTORY = SoftMaxV1Attribute + + def forward(self, inputs): + """SoftMaxV1's forward function.""" + return F.softmax(input=inputs, dim=self.attrs.axis) + + +@dataclass +class ReluV0Attribute(Attribute): + """ReluV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class ReluV0(Operation[ReluV0Attribute]): + """ReluV0 class.""" + + TYPE = "Relu" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ReluV0Attribute + + def forward(self, inputs): + """ReluV0's forward function.""" + return F.relu(inputs) + + +@dataclass +class SwishV4Attribute(Attribute): + """SwishV4Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class SwishV4(Operation[SwishV4Attribute]): + """SwishV4 class.""" + + TYPE = "Swish" + VERSION = "opset1" + ATTRIBUTE_FACTORY = SwishV4Attribute + + def forward(self, inputs, beta=1.0): + """SwishV4's forward function.""" + return inputs * torch.sigmoid(inputs * beta) + + +@dataclass +class SigmoidV0Attribute(Attribute): + """SigmoidV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class SigmoidV0(Operation[SigmoidV0Attribute]): + """SigmoidV0 class.""" + + TYPE = "Sigmoid" + VERSION = "opset1" + ATTRIBUTE_FACTORY = SigmoidV0Attribute + + def forward(self, inputs): + """SigmoidV0's forward function.""" + return torch.sigmoid(inputs) + + +@dataclass +class ClampV0Attribute(Attribute): + """ClampV0Attribute class.""" + + min: float + max: float + + +@OPS.register() +class ClampV0(Operation[ClampV0Attribute]): + """ClampV0 class.""" + + TYPE = "Clamp" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ClampV0Attribute + + def forward(self, inputs): + """ClampV0's forward function.""" + return inputs.clamp(min=self.attrs.min, max=self.attrs.max) + + +@dataclass +class PReluV0Attribute(Attribute): + """PReluV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class PReluV0(Operation[PReluV0Attribute]): + """PReluV0 class.""" + + TYPE = "PRelu" + VERSION = "opset1" + ATTRIBUTE_FACTORY = PReluV0Attribute + + def forward(self, inputs, slope): + """PReluV0's forward function.""" + return F.prelu(input=inputs, weight=slope) + + +@dataclass +class TanhV0Attribute(Attribute): + """TanhV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class TanhV0(Operation[TanhV0Attribute]): + """TanhV0 class.""" + + TYPE = "Tanh" + VERSION = "opset1" + ATTRIBUTE_FACTORY = TanhV0Attribute + + def forward(self, inputs): + """TanhV0's forward function.""" + return F.tanh(inputs) + + +@dataclass +class EluV0Attribute(Attribute): + """EluV0Attribute class.""" + + alpha: float + + +@OPS.register() +class EluV0(Operation[EluV0Attribute]): + """EluV0 class.""" + + TYPE = "Elu" + VERSION = "opset1" + ATTRIBUTE_FACTORY = EluV0Attribute + + def forward(self, inputs): + """EluV0's forward function.""" + return F.elu(input=inputs, alpha=self.attrs.alpha) + + +@dataclass +class SeluV0Attribute(Attribute): + """SeluV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class SeluV0(Operation[SeluV0Attribute]): + """SeluV0 class.""" + + TYPE = "Selu" + VERSION = "opset1" + ATTRIBUTE_FACTORY = SeluV0Attribute + + def forward(self, inputs, alpha, lambda_): + """SeluV0's forward function.""" + return lambda_ * F.elu(input=inputs, alpha=alpha) + + +@dataclass +class MishV4Attribute(Attribute): + """MishV4Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class MishV4(Operation[MishV4Attribute]): + """MishV4 class.""" + + TYPE = "Mish" + VERSION = "opset1" + ATTRIBUTE_FACTORY = MishV4Attribute + + def forward(self, inputs): + """MishV4's forward function.""" + # NOTE: pytorch 1.8.2 does not have mish function + # return F.mish(input=input) + return inputs * F.tanh(F.softplus(inputs)) + + +@dataclass +class HSwishV4Attribute(Attribute): + """HSwishV4Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class HSwishV4(Operation[HSwishV4Attribute]): + """HSwishV4 class.""" + + TYPE = "HSwish" + VERSION = "opset1" + ATTRIBUTE_FACTORY = HSwishV4Attribute + + def forward(self, inputs): + """HSwishV4's forward function.""" + return F.hardswish(input=inputs) + + +@dataclass +class HSigmoidV5Attribute(Attribute): + """HSigmoidV5Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class HSigmoidV5(Operation[HSigmoidV5Attribute]): + """HSigmoidV5 class.""" + + TYPE = "HSigmoid" + VERSION = "opset1" + ATTRIBUTE_FACTORY = HSigmoidV5Attribute + + def forward(self, inputs): + """HSigmoidV5's forward function.""" + return F.hardsigmoid(input=inputs) + + +@dataclass +class ExpV0Attribute(Attribute): + """ExpV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class ExpV0(Operation[ExpV0Attribute]): + """ExpV0 class.""" + + TYPE = "Exp" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ExpV0Attribute + + def forward(self, inputs): + """ExpV0's forward function.""" + return torch.exp(inputs) + + +@dataclass +class HardSigmoidV0Attribute(Attribute): + """HardSigmoidV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class HardSigmoidV0(Operation[HardSigmoidV0Attribute]): + """HardSigmoidV0 class.""" + + TYPE = "HardSigmoid" + VERSION = "opset1" + ATTRIBUTE_FACTORY = HardSigmoidV0Attribute + + def forward(self, inputs, alpha, beta): + """HardSigmoidV0's forward function.""" + return torch.maximum( + torch.zeros_like(inputs), + torch.minimum(torch.ones_like(inputs), inputs * alpha + beta), + ) + + +@dataclass +class GeluV7Attribute(Attribute): + """GeluV7Attribute class.""" + + approximation_mode: str = field(default="ERF") + + def __post_init__(self): + """GeluV7Attribute's post init function.""" + super().__post_init__() + valid_approximation_mode = ["ERF", "tanh"] + if self.approximation_mode not in valid_approximation_mode: + raise ValueError( + f"Invalid approximation_mode {self.approximation_mode}. " + f"It must be one of {valid_approximation_mode}." + ) + + +@OPS.register() +class GeluV7(Operation[GeluV7Attribute]): + """GeluV7 class.""" + + TYPE = "Gelu" + VERSION = "opset1" + ATTRIBUTE_FACTORY = GeluV7Attribute + + def forward(self, inputs): + """GeluV7's forward function.""" + mode = self.attrs.approximation_mode + if mode == "ERF": + return F.gelu(input=inputs) + if mode == "tanh": + return ( + inputs * 0.5 * (1 + F.tanh(torch.sqrt(2 / torch.tensor(math.pi)) * (inputs + 0.044715 * inputs**3))) + ) + return None diff --git a/src/otx/core/ov/ops/arithmetics.py b/src/otx/core/ov/ops/arithmetics.py new file mode 100644 index 00000000000..8fd7c2825e3 --- /dev/null +++ b/src/otx/core/ov/ops/arithmetics.py @@ -0,0 +1,147 @@ +"""Arithmetics-related codes for otx.core.ov.ops.arithmetics.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass, field + +import torch + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.op import Attribute, Operation + + +@dataclass +class MultiplyV1Attribute(Attribute): + """MultiplyV1Attribute class.""" + + auto_broadcast: str = field(default="numpy") + + +@OPS.register() +class MultiplyV1(Operation[MultiplyV1Attribute]): + """MultiplyV1 class.""" + + TYPE = "Multiply" + VERSION = "opset1" + ATTRIBUTE_FACTORY = MultiplyV1Attribute + + def forward(self, input_0, input_1): + """MultiplyV1's forward function.""" + broadcast = self.attrs.auto_broadcast + + if broadcast == "none": + assert input_0.shape == input_1.shape + return input_0 * input_1 + if broadcast == "numpy": + return input_0 * input_1 + raise NotImplementedError + + +@dataclass +class DivideV1Attribute(Attribute): + """DivideV1Attribute class.""" + + m_pythondiv: bool = field(default=True) + auto_broadcast: str = field(default="numpy") + + +@OPS.register() +class DivideV1(Operation[DivideV1Attribute]): + """DivideV1 class.""" + + TYPE = "Divide" + VERSION = "opset1" + ATTRIBUTE_FACTORY = DivideV1Attribute + + def forward(self, input_0, input_1): + """DivideV1's forward function.""" + broadcast = self.attrs.auto_broadcast + + if broadcast == "none": + assert input_0.shape == input_1.shape + output = input_0 / input_1 + elif broadcast == "numpy": + output = input_0 / input_1 + else: + raise NotImplementedError + + non_integer_types = [torch.float16, torch.float32, torch.float64, torch.bool] + if self.attrs.m_pythondiv and input_0.dtype not in non_integer_types and input_1.dtype not in non_integer_types: + output = output.type(input_0.dtype) + + return output + + +@dataclass +class AddV1Attribute(Attribute): + """AddV1Attribute class.""" + + auto_broadcast: str = field(default="numpy") + + +@OPS.register() +class AddV1(Operation[AddV1Attribute]): + """AddV1 class.""" + + TYPE = "Add" + VERSION = "opset1" + ATTRIBUTE_FACTORY = AddV1Attribute + + def forward(self, input_0, input_1): + """AddV1's forward function.""" + broadcast = self.attrs.auto_broadcast + + if broadcast == "none": + assert input_0.shape == input_1.shape + return input_0 + input_1 + if broadcast == "numpy": + return input_0 + input_1 + raise NotImplementedError + + +@dataclass +class SubtractV1Attribute(Attribute): + """SubtractV1Attribute class.""" + + auto_broadcast: str = field(default="numpy") + + +@OPS.register() +class SubtractV1(Operation[SubtractV1Attribute]): + """SubtractV1 class.""" + + TYPE = "Subtract" + VERSION = "opset1" + ATTRIBUTE_FACTORY = SubtractV1Attribute + + def forward(self, input_0, input_1): + """SubtractV1's forward function.""" + broadcast = self.attrs.auto_broadcast + + if broadcast == "none": + assert input_0.shape == input_1.shape + return input_0 - input_1 + if broadcast == "numpy": + return input_0 - input_1 + raise NotImplementedError + + +@dataclass +class TanV0Attribute(Attribute): + """TanV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class TanV0(Operation[TanV0Attribute]): + """TanV0 class.""" + + TYPE = "Tan" + VERSION = "opset1" + ATTRIBUTE_FACTORY = TanV0Attribute + + def forward(self, inputs): + """TanV0's forward function.""" + return torch.tan(inputs) diff --git a/src/otx/core/ov/ops/builder.py b/src/otx/core/ov/ops/builder.py new file mode 100644 index 00000000000..faff366fff2 --- /dev/null +++ b/src/otx/core/ov/ops/builder.py @@ -0,0 +1,57 @@ +"""OPS (OperationRegistry) module for otx.core.ov.ops.builder.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any, Optional + +from otx.core.ov.registry import Registry + + +class OperationRegistry(Registry): + """OperationRegistry class.""" + + def __init__(self, name, add_name_as_attr=False): + super().__init__(name, add_name_as_attr) + self._registry_dict_by_type = {} + + def register(self, name: Optional[Any] = None): + """Register function from name.""" + + def wrap(obj): + layer_name = name + if layer_name is None: + layer_name = obj.__name__ + layer_type = obj.TYPE + layer_version = obj.VERSION + assert layer_type and layer_version + if self._add_name_as_attr: + setattr(obj, self.REGISTERED_NAME_ATTR, layer_name) + self._register(obj, layer_name, layer_type, layer_version) + return obj + + return wrap + + def _register(self, obj, name, types, version): + """Register function from obj and obj name.""" + super()._register(obj, name) + if types not in self._registry_dict_by_type: + self._registry_dict_by_type[types] = {} + if version in self._registry_dict_by_type[types]: + raise KeyError(f"{version} is already registered in {types}") + self._registry_dict_by_type[types][version] = obj + + def get_by_name(self, name): + """Get obj from name.""" + return self.get(name) + + def get_by_type_version(self, types, version): + """Get obj from type and version.""" + if types not in self._registry_dict_by_type: + raise KeyError(f"type {types} is not registered in {self._name}") + if version not in self._registry_dict_by_type[types]: + raise KeyError(f"version {version} is not registered in {types} of {self._name}") + return self._registry_dict_by_type[types][version] + + +OPS = OperationRegistry("ov ops") diff --git a/src/otx/core/ov/ops/convolutions.py b/src/otx/core/ov/ops/convolutions.py new file mode 100644 index 00000000000..96c233aa2ae --- /dev/null +++ b/src/otx/core/ov/ops/convolutions.py @@ -0,0 +1,129 @@ +"""Convolutions-related module for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass, field +from typing import Callable, List + +from torch.nn import functional as F + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.movements import get_torch_padding +from otx.core.ov.ops.op import Attribute, Operation + + +@dataclass +class ConvolutionV1Attribute(Attribute): + """ConvolutionV1Attribute class.""" + + strides: List[int] + pads_begin: List[int] + pads_end: List[int] + dilations: List[int] + auto_pad: str = field(default="explicit") + + def __post_init__(self): + """ConvolutionV1Attribute's post-init function.""" + super().__post_init__() + valid_auto_pad = ["explicit", "same_upper", "same_Lower", "valid"] + if self.auto_pad not in valid_auto_pad: + raise ValueError(f"Invalid auto_pad {self.auto_pad}. " f"It must be one of {valid_auto_pad}.") + + +@OPS.register() +class ConvolutionV1(Operation[ConvolutionV1Attribute]): + """ConvolutionV1 class.""" + + TYPE = "Convolution" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ConvolutionV1Attribute + + def forward(self, inputs, weight): + """ConvolutionV1's forward function.""" + if weight.dim() == 3: + func = F.conv1d + elif weight.dim() == 4: + func = F.conv2d + elif weight.dim() == 5: + func = F.conv3d + else: + raise NotImplementedError + + padding = get_torch_padding( + self.attrs.pads_begin, + self.attrs.pads_end, + self.attrs.auto_pad, + list(inputs.shape[2:]), + list(weight.shape[2:]), + self.attrs.strides, + self.attrs.dilations, + ) + if isinstance(padding, Callable): + inputs = padding(input=inputs) + padding = 0 + + return func( + input=inputs, + weight=weight, + bias=None, + stride=self.attrs.strides, + padding=padding, + dilation=self.attrs.dilations, + ) + + +@dataclass +class GroupConvolutionV1Attribute(ConvolutionV1Attribute): + """GroupConvolutionV1Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class GroupConvolutionV1(Operation[GroupConvolutionV1Attribute]): + """GroupConvolutionV1 class.""" + + TYPE = "GroupConvolution" + VERSION = "opset1" + ATTRIBUTE_FACTORY = GroupConvolutionV1Attribute + + def forward(self, inputs, weight): + """GroupConvolutionV1's forward function.""" + if weight.dim() == 4: + func = F.conv1d + elif weight.dim() == 5: + func = F.conv2d + elif weight.dim() == 6: + func = F.conv3d + else: + raise NotImplementedError + + n_groups = weight.shape[0] + # merge groups and out dimension + weight = weight.view(-1, *weight.shape[2:]) + + padding = get_torch_padding( + self.attrs.pads_begin, + self.attrs.pads_end, + self.attrs.auto_pad, + list(inputs.shape[2:]), + list(weight.shape[2:]), + self.attrs.strides, + self.attrs.dilations, + ) + if isinstance(padding, Callable): + inputs = padding(input=inputs) + padding = 0 + + output = func( + input=inputs, + weight=weight, + bias=None, + stride=self.attrs.strides, + padding=padding, + dilation=self.attrs.dilations, + groups=n_groups, + ) + + return output diff --git a/src/otx/core/ov/ops/generation.py b/src/otx/core/ov/ops/generation.py new file mode 100644 index 00000000000..fa364fb703e --- /dev/null +++ b/src/otx/core/ov/ops/generation.py @@ -0,0 +1,40 @@ +"""Generation-related module for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass + +import torch + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.op import Attribute, Operation +from otx.core.ov.ops.type_conversions import _ov_to_torch + + +@dataclass +class RangeV4Attribute(Attribute): + """RangeV4Attribute class.""" + + output_type: str + + +@OPS.register() +class RangeV4(Operation[RangeV4Attribute]): + """RangeV4 class.""" + + TYPE = "Range" + VERSION = "opset1" + ATTRIBUTE_FACTORY = RangeV4Attribute + + def forward(self, start, stop, step): + """RangeV4's forward function.""" + dtype = _ov_to_torch[self.attrs.output_type] + return torch.arange( + start=start, + end=stop, + step=step, + dtype=dtype, + device=start.device, + requires_grad=False, + ) diff --git a/src/otx/core/ov/ops/image_processings.py b/src/otx/core/ov/ops/image_processings.py new file mode 100644 index 00000000000..11634921848 --- /dev/null +++ b/src/otx/core/ov/ops/image_processings.py @@ -0,0 +1,142 @@ +"""Image Processings-related code for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass, field +from typing import List + +import numpy as np +from torch.nn import functional as F + +from .builder import OPS +from .movements import PadV1 +from .op import Attribute, Operation + +# pylint: disable=too-many-instance-attributes, too-many-branches + + +@dataclass +class InterpolateV4Attribute(Attribute): + """InterpolateV4Attribute class.""" + + mode: str + shape_calculation_mode: str + coordinate_transformation_mode: str = field(default="half_pixel") + nearest_mode: str = field(default="round_prefer_floor") + antialias: bool = field(default=False) + pads_begin: List[int] = field(default_factory=lambda: [0]) + pads_end: List[int] = field(default_factory=lambda: [0]) + cube_coeff: float = field(default=-0.75) + + def __post_init__(self): + """InterpolateV4Attribute's post-init function.""" + super().__post_init__() + valid_mode = ["nearest", "linear", "linear_onnx", "cubic"] + if self.mode not in valid_mode: + raise ValueError(f"Invalid mode {self.mode}. " f"It must be one of {valid_mode}.") + valid_shape_calculation_mode = ["sizes", "scales"] + if self.shape_calculation_mode not in valid_shape_calculation_mode: + raise ValueError( + f"Invalid shape_calculation_mode {self.shape_calculation_mode}. " + f"It must be one of {valid_shape_calculation_mode}." + ) + valid_coordinate_transformation_mode = [ + "half_pixel", + "pytorch_half_pixel", + "asymmetric", + "tf_half_pixel_for_nn", + "align_corners", + ] + if self.coordinate_transformation_mode not in valid_coordinate_transformation_mode: + raise ValueError( + f"Invalid coordinate_transformation_mode {self.coordinate_transformation_mode}. " + f"It must be one of {valid_coordinate_transformation_mode}." + ) + valid_nearest_mode = [ + "round_prefer_floor", + "round_prefer_ceil", + "floor", + "ceil", + "simple", + ] + if self.nearest_mode not in valid_nearest_mode: + raise ValueError(f"Invalid nearest_mode {self.nearest_mode}. " f"It must be one of {valid_nearest_mode}.") + + +@OPS.register() +class InterpolateV4(Operation[InterpolateV4Attribute]): + """InterpolateV4 class.""" + + TYPE = "Interpolate" + VERSION = "opset1" + ATTRIBUTE_FACTORY = InterpolateV4Attribute + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.pad = PadV1("tmp", shape=self.shape, pad_mode="constant") + + def forward(self, inputs, sizes, scales, axes=None): + """InterpolateV4's forward function.""" + # TODO list + # - handle 'linear_onnx' mode + # - coordinate_transformation_mode + # - nearest_mode + # - cube_coeff + # - antialias + + if axes is None: + axes = list(range(inputs.dim())) + else: + axes = axes.detach().cpu().tolist() + + output = self.pad(inputs, self.attrs.pads_begin, self.attrs.pads_end, 0) + + mode = self.attrs.mode + if mode in ("linear", "linear_onnx"): + align_corners = False + if output.dim() == 3: + pass + elif output.dim() == 4: + mode = "bilinear" + elif output.dim() == 5: + mode = "trilinear" + elif mode == "cubic": + align_corners = False + if output.dim() == 3: + raise NotImplementedError + if output.dim() == 4: + mode = "bicubic" + elif output.dim() == 5: + raise NotImplementedError + elif mode == "nearest": + align_corners = None + pass # pylint: disable=unnecessary-pass + else: + raise NotImplementedError + + if self.attrs.shape_calculation_mode == "sizes": + sizes = sizes.detach().cpu().numpy() + sizes = sizes[np.argsort(axes)].tolist() + if output.dim() == len(sizes): + sizes = sizes[2:] + + return F.interpolate( + input=output, + size=sizes, + scale_factor=None, + mode=mode, + align_corners=align_corners, + ) + scales = scales.detach().cpu().numpy() + scales = scales[np.argsort(axes)].tolist() + if output.dim() == len(scales): + scales = scales[2:] + + return F.interpolate( + input=output, + size=None, + scale_factor=scales, + mode=mode, + align_corners=align_corners, + ) diff --git a/src/otx/core/ov/ops/infrastructures.py b/src/otx/core/ov/ops/infrastructures.py new file mode 100644 index 00000000000..58ad862be3d --- /dev/null +++ b/src/otx/core/ov/ops/infrastructures.py @@ -0,0 +1,263 @@ +"""Infrastructure-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from collections import OrderedDict +from dataclasses import dataclass, field +from typing import Optional, Tuple + +import numpy as np +import torch + +from otx.utils.logger import get_logger + +from ..utils import get_op_name # type: ignore[attr-defined] +from .builder import OPS +from .op import Attribute, Operation +from .type_conversions import ConvertV0 +from .utils import get_dynamic_shape + +NODE_TYPES_WITH_WEIGHT = set( + [ + "Convolution", + "GroupConvolution", + "MatMul", + "BatchNormInference", + "Multiply", + "Divide", + "Add", + "Subtract", + ] +) + +logger = get_logger() + + +@dataclass +class ParameterV0Attribute(Attribute): + """ParameterV0Attribute class.""" + + element_type: Optional[str] = field(default=None) + + layout: Optional[Tuple[str]] = field(default=None) + permute: Optional[Tuple[int]] = field(default=None) + verify_shape: bool = field(default=True) + + def __post_init__(self): + """ParameterV0Attribute's post-init function.""" + super().__post_init__() + # fmt: off + valid_element_type = [ + None, + "u1", "u4", "u8", "u16", "u32", "u64", + "i4", "i8", "i16", "i32", "i64", "f16", "f32", "boolean", "bf16" + ] + # fmt: on + if self.element_type not in valid_element_type: + raise ValueError(f"Invalid element_type {self.element_type}. " f"It must be one of {valid_element_type}.") + + +@OPS.register() +class ParameterV0(Operation[ParameterV0Attribute]): + """ParameterV0 class.""" + + TYPE = "Parameter" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ParameterV0Attribute + + def forward(self, inputs): + """ParameterV0's forward function.""" + # TODO: validate shape + # need to handle new generated op from reshaped model + if self.attrs.verify_shape: + assert self.shape is not None + ov_shape = self.shape[0] + torch_shape = list(inputs.shape) + for ov_shape_, torch_shape_ in zip(ov_shape, torch_shape): + if ov_shape_ == -1: + continue + assert ov_shape_ == torch_shape_, f"input shape {torch_shape} does not match with ov shape {ov_shape}" + + if self.attrs.permute: + inputs = inputs.permute(self.attrs.permute) + + return inputs + + @classmethod + def from_ov(cls, ov_op): + """ParameterV0's from_ov function.""" + op_type = ov_op.get_type_name() + op_name = get_op_name(ov_op) + op_version = ov_op.get_type_info().version_id + assert cls.TYPE and cls.VERSION + assert op_type == cls.TYPE + assert op_version == cls.VERSION + + attrs = ov_op.get_attributes() + if "shape" not in attrs: + shapes = [] + for output in ov_op.outputs(): + shapes.append(get_dynamic_shape(output)) + shapes = tuple(tuple(shape) for shape in shapes) + attrs["shape"] = shapes + + layout = ov_op.get_layout() + if not layout.empty: + layout = layout.to_string()[1:-1].split(",") + attrs["layout"] = tuple(layout) + + # N, C, H, W + input_layout = OrderedDict( + { + "N": 0, + "C": 1, + "H": 2, + "W": 3, + } + ) + if not set(layout).symmetric_difference(input_layout.keys()): + permute = [] + for layout_ in layout: + # N, H, W, C + permute.append(input_layout[layout_]) + attrs["permute"] = tuple(permute) + + # TODO: here, we force the batch dim to be dynamic + # but this should be done when loading ov model + i = layout.index("N") + new_shape = [] + for shape in attrs["shape"]: + new_shape.append([-1 if j == i else k for j, k in enumerate(shape)]) + new_shape = [tuple(shape) for shape in new_shape] + attrs["shape"] = tuple(new_shape) + + # change shape and layout based on permute + if "permute" in attrs and attrs["permute"] != (0, 1, 2, 3): + assert len(attrs["shape"]) == 1 + permute = [] + for layout_ in input_layout.keys(): + permute.append(layout.index(layout_)) + new_shape = [] + for shape in attrs["shape"]: + new_shape.append([shape[i] for i in permute]) + attrs["shape"] = tuple(tuple(shape) for shape in new_shape) + attrs["layout"] = tuple(attrs["layout"][i] for i in permute) + + return cls(name=op_name, **attrs) + + +@dataclass +class ResultV0Attribute(Attribute): + """ResultV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class ResultV0(Operation[ResultV0Attribute]): + """ResultV0 class.""" + + TYPE = "Result" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ResultV0Attribute + + def forward(self, inputs): + """ResultV0's forward function.""" + return inputs + + +@dataclass +class ConstantV0Attribute(Attribute): + """ConstantV0Attribute class.""" + + element_type: str + offset: int = field(default=0) + size: int = field(default=0) + + is_parameter: bool = field(default=False) + + def __post_init__(self): + """ConstantV0Attribute's post-init function.""" + super().__post_init__() + # fmt: off + valid_element_type = [ + "u1", "u4", "u8", "u16", "u32", "u64", + "i4", "i8", "i16", "i32", "i64", "f16", "f32", "boolean", "bf16" + ] + # fmt: on + if self.element_type not in valid_element_type: + raise ValueError(f"Invalid element_type {self.element_type}. " f"It must be one of {valid_element_type}.") + + +@OPS.register() +class ConstantV0(Operation[ConstantV0Attribute]): + """ConstantV0 class.""" + + TYPE = "Constant" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ConstantV0Attribute + + def __init__(self, *args, **kwargs): + data = kwargs.pop("data", None) + if data is None: + raise KeyError("data is not provided") + assert isinstance(data, torch.Tensor) + kwargs["element_type"] = ConvertV0.convert_torch_type(data.dtype) + super().__init__(*args, **kwargs) + if self.attrs.is_parameter: + self.data = torch.nn.Parameter(data) + else: + self.register_buffer("data", data) + + def forward(self): + """ConstantV0's forward function.""" + return self.data + + @classmethod + def from_ov(cls, ov_op): + """ConstantV0's from_ov function.""" + op_type = ov_op.get_type_name() + op_name = get_op_name(ov_op) + op_version = ov_op.get_type_info().version_id + assert cls.TYPE and cls.VERSION + assert op_type == cls.TYPE + assert op_version == cls.VERSION + + attrs = ov_op.get_attributes() + attrs["shape"] = tuple(attrs["shape"]) + + data = ov_op.get_data() + if data.dtype == np.uint64: + data_ = data.astype(np.int64) + if not np.array_equal(data, data_): + logger.warning(f"Overflow detected in {op_name}") + data = torch.from_numpy(data_) + elif data.dtype == np.uint16: + data = torch.from_numpy(data.astype(np.int32)) + else: + data = torch.from_numpy(data) + + in_port_indices = [] + op_node_types = [] + for out_port in ov_op.outputs(): + for in_port in list(out_port.get_target_inputs()): + in_port_index = in_port.get_index() + in_port_indices.append(in_port_index) + node = in_port.get_node() + op_node_types.append(node.get_type_name()) + + # FIXME: need a better way to distinguish if it is parameter or not + is_parameter = False + # pylint: disable=too-many-boolean-expressions + if ( + set(op_node_types).intersection(NODE_TYPES_WITH_WEIGHT) + and len(in_port_indices) == 1 + and in_port_indices[0] != 0 + and data.numel() > 1 + and (data.is_floating_point() or data.is_complex()) + ): + is_parameter = True + attrs["is_parameter"] = is_parameter + + return cls(name=op_name, data=data, **attrs) diff --git a/src/otx/core/ov/ops/matmuls.py b/src/otx/core/ov/ops/matmuls.py new file mode 100644 index 00000000000..7c707dda97a --- /dev/null +++ b/src/otx/core/ov/ops/matmuls.py @@ -0,0 +1,56 @@ +"""MatMul-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass, field + +import torch + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.op import Attribute, Operation + + +@dataclass +class MatMulV0Attribute(Attribute): + """MatMulV0Attribute class.""" + + transpose_a: bool = field(default=False) + transpose_b: bool = field(default=False) + + +@OPS.register() +class MatMulV0(Operation[MatMulV0Attribute]): + """MatMulV0 class.""" + + TYPE = "MatMul" + VERSION = "opset1" + ATTRIBUTE_FACTORY = MatMulV0Attribute + + def forward(self, input_a, input_b): + """MatMulV0's forward function.""" + if self.attrs.transpose_a: + input_a = torch.transpose(input_a, -1, -2) + if self.attrs.transpose_b: + input_b = torch.transpose(input_b, -1, -2) + return torch.matmul(input_a, input_b) + + +@dataclass +class EinsumV7Attribute(Attribute): + """EinsumV7Attribute class.""" + + equation: str + + +@OPS.register() +class EinsumV7(Operation[EinsumV7Attribute]): + """EinsumV7 class.""" + + TYPE = "Einsum" + VERSION = "opset1" + ATTRIBUTE_FACTORY = EinsumV7Attribute + + def forward(self, *inputs): + """EinsumV7's forward function.""" + return torch.einsum(self.attrs.equation, *inputs) diff --git a/src/otx/core/ov/ops/modules/__init__.py b/src/otx/core/ov/ops/modules/__init__.py new file mode 100644 index 00000000000..6dbe0509de9 --- /dev/null +++ b/src/otx/core/ov/ops/modules/__init__.py @@ -0,0 +1,8 @@ +"""Module for otx.core.ov.pos.modules.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from .op_module import OperationModule + +__all__ = ["OperationModule"] diff --git a/src/otx/core/ov/ops/modules/op_module.py b/src/otx/core/ov/ops/modules/op_module.py new file mode 100644 index 00000000000..b0003708052 --- /dev/null +++ b/src/otx/core/ov/ops/modules/op_module.py @@ -0,0 +1,104 @@ +"""Operation module for otx.core.ov.ops.modeuls.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import inspect +from typing import Dict, List, Optional, Union + +import torch +from openvino.runtime import Node + +from ..op import Operation +from ..utils import convert_op_to_torch + + +class OperationModule(torch.nn.Module): + """OperationModule class.""" + + def __init__( + self, + op_v: Operation, + dependent_ops: Union[List[Operation], Dict[str, Optional[Operation]]], + ): + super().__init__() + + self.op_v = op_v + self._dependent_ops = torch.nn.ModuleDict() + + spec = inspect.getfullargspec(op_v.forward) + kwargs = spec.args[1:] + + self._dependents_with_defaults = [] + if spec.defaults: + self._dependents_with_defaults = spec.args[-len(spec.defaults) :] + + if isinstance(dependent_ops, list): + assert len(dependent_ops) == len(kwargs) + for op_, kwarg in zip(dependent_ops, kwargs): + self._dependent_ops[kwarg] = op_ + elif isinstance(dependent_ops, dict): + for kwarg in kwargs: + self._dependent_ops[kwarg] = dependent_ops[kwarg] + else: + raise NotImplementedError + + def forward(self, *args, **kwargs): + """Operationmodule's forward function.""" + inputs = {k: v() if v is not None else None for k, v in self._dependent_ops.items()} + + if args: + empty_input_keys = [k for k, v in self._dependent_ops.items() if v is None] + for key, val in zip(empty_input_keys, args): + inputs[key] = val + if kwargs: + for key, val in kwargs.items(): + if inputs[key] is not None: + raise ValueError(f"duplicated key {key}") + inputs[key] = val + + assert all(v is not None for v in inputs.values() if v not in self._dependents_with_defaults) + + return self.op_v(**inputs) + + @property + def type(self): # pylint: disable=invalid-overridden-method + """Operationmodule's type property.""" + return self.op_v.type + + @property + def version(self): + """Operationmodule's version property.""" + return self.op_v.version + + @property + def name(self): + """Operationmodule's name property.""" + return self.op_v.name + + @property + def shape(self): + """Operationmodule's shape property.""" + return self.op_v.shape + + @property + def attrs(self): + """Operationmodule's attrs property.""" + return self.op_v.attrs + + +def convert_op_to_torch_module(target_op: Node): + """Convert op Node to torch module.""" + dependent_modules = [] + for in_port in target_op.inputs(): + out_port = in_port.get_source_output() + parent = out_port.get_node() + + parent_type = parent.get_type_name() + if parent_type == "Constant": + dependent_modules.append(convert_op_to_torch(parent)) + else: + dependent_modules.append(None) + module = convert_op_to_torch(target_op) + module = OperationModule(module, dependent_modules) + return module diff --git a/src/otx/core/ov/ops/movements.py b/src/otx/core/ov/ops/movements.py new file mode 100644 index 00000000000..d77fd5a44b6 --- /dev/null +++ b/src/otx/core/ov/ops/movements.py @@ -0,0 +1,517 @@ +"""Movement-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import math +from dataclasses import dataclass, field +from functools import partial +from typing import List + +import torch +from torch.nn import functional as F + +from .builder import OPS +from .op import Attribute, Operation + +# pylint: disable=too-many-branches + + +@dataclass +class PadV1Attribute(Attribute): + """PadV1Attribute class.""" + + pad_mode: str + + def __post_init__(self): + """PadV1Attribute's post-init function.""" + super().__post_init__() + valid_pad_mode = ["constant", "edge", "reflect", "symmetric"] + if self.pad_mode not in valid_pad_mode: + raise ValueError(f"Invalid pad_mode {self.pad_mode}. " f"It must be one of {valid_pad_mode}.") + + +@OPS.register() +class PadV1(Operation[PadV1Attribute]): + """PadV1 class.""" + + TYPE = "Pad" + VERSION = "opset1" + ATTRIBUTE_FACTORY = PadV1Attribute + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._pad_mode = self.get_torch_pad_mode(self.attrs.pad_mode) + + @staticmethod + def get_torch_pad_mode(pad_mode): + """PadV1's get_torch_pad_mode function.""" + if pad_mode == "constant": + return "constant" + if pad_mode == "edge": + return "replicate" + if pad_mode == "reflect": + return "reflect" + raise NotImplementedError + + @staticmethod + def get_torch_pad_dim(pads_begin, pads_end): + """PadV1's get_torch_pad_dim function.""" + # reverse padding + return [val for tup in zip(pads_begin[::-1], pads_end[::-1]) for val in tup] + + def forward(self, inputs, pads_begin, pads_end, pad_value=0): + """PadV1's forward function.""" + pads_begin = pads_begin if isinstance(pads_begin, list) else pads_begin.detach().cpu().tolist() + pads_end = pads_end if isinstance(pads_end, list) else pads_end.detach().cpu().tolist() + pad = self.get_torch_pad_dim(pads_begin, pads_end) + pad = list(map(math.ceil, pad)) + return F.pad(input=inputs, pad=pad, mode=self._pad_mode, value=pad_value) + + +@dataclass +class ConcatV0Attribute(Attribute): + """ConcatV0Attribute class.""" + + axis: int + + +@OPS.register() +class ConcatV0(Operation[ConcatV0Attribute]): + """ConcatV0 class.""" + + TYPE = "Concat" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ConcatV0Attribute + + def forward(self, *inputs): + """ConcatV0's forward function.""" + return torch.cat(inputs, self.attrs.axis) + + +@dataclass +class TransposeV1Attribute(Attribute): + """TransposeV1Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class TransposeV1(Operation[TransposeV1Attribute]): + """TransposeV1 class.""" + + TYPE = "Transpose" + VERSION = "opset1" + ATTRIBUTE_FACTORY = TransposeV1Attribute + + def forward(self, inputs, order): + """TransposeV1's forward function.""" + if order.numel() == 0: + order = list(range(inputs.dim()))[::-1] + elif isinstance(order, torch.Tensor): + order = order.detach().cpu().tolist() + return inputs.permute(order) + + +@dataclass +class GatherV0Attribute(Attribute): + """GatherV0Attribute class.""" + + batch_dims: int = field(default=0) + + +@OPS.register() +class GatherV0(Operation[GatherV0Attribute]): + """GatherV0 class.""" + + TYPE = "Gather" + VERSION = "opset1" + ATTRIBUTE_FACTORY = GatherV0Attribute + + def forward(self, inputs, indices, axis): + """GatherV0's forward function.""" + assert axis.numel() == 1 + axis = axis.squeeze() + squeeze_axis = indices.dim() == 0 + + batch_dims = self.attrs.batch_dims + if batch_dims < 0: + batch_dims = indices.dim() + batch_dims + + indices_shape = torch.tensor(indices.shape) + if batch_dims < axis: + indices = indices.reshape(*indices_shape[:batch_dims], -1) + indices_shape = indices_shape[batch_dims:] + + if indices.dim() != inputs.dim(): + if indices.dim() != 0: + while indices.dim() - 1 < axis: + indices = indices.unsqueeze(batch_dims) + while indices.dim() < inputs.dim(): + indices = indices.unsqueeze(-1) + + repeat = [] + for i, (j, k) in enumerate(zip(inputs.shape, indices.shape)): + if i == axis: + repeat.append(1) + else: + assert j % k == 0 + repeat.append(j // k) + indices = indices.repeat(repeat) + output = torch.gather(input=inputs, dim=axis, index=indices.type(torch.int64)) + + if squeeze_axis: + output = output.squeeze(axis) + + return output + + +@dataclass +class GatherV1Attribute(Attribute): + """GatherV1Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class GatherV1(Operation[GatherV1Attribute]): + """GatherV1 class.""" + + TYPE = "Gather" + VERSION = "opset2" + ATTRIBUTE_FACTORY = GatherV1Attribute + + def forward(self, inputs, indices, axis): + """GatherV1's forward function.""" + return torch.gather(input=inputs, dim=axis, index=indices) + + +@dataclass +class StridedSliceV1Attribute(Attribute): + """StridedSliceV1Attribute class.""" + + begin_mask: List[int] + end_mask: List[int] + new_axis_mask: List[int] = field(default_factory=lambda: [0]) + shrink_axis_mask: List[int] = field(default_factory=lambda: [0]) + ellipsis_mask: List[int] = field(default_factory=lambda: [0]) + + +@OPS.register() +class StridedSliceV1(Operation[StridedSliceV1Attribute]): + """StridedSliceV1 class.""" + + TYPE = "StridedSlice" + VERSION = "opset1" + ATTRIBUTE_FACTORY = StridedSliceV1Attribute + + def forward(self, inputs, begin, end, stride=None): + """StridedSliceV1's forward function.""" + if sum(self.attrs.ellipsis_mask) > 0: + raise NotImplementedError + + for i, mask in enumerate(self.attrs.begin_mask): + if mask == 1: + begin[i] = 0 + for i, mask in enumerate(self.attrs.end_mask): + if mask == 1: + end[i] = inputs.size(i) + + if stride is None: + stride = torch.tensor([1 for _ in begin], dtype=begin.dtype) + + output = inputs + for i, (b, e, stride_0) in enumerate(zip(begin, end, stride)): + length = inputs.size(i) + + # begin index is inclusive + b = torch.clamp(b, -length, length - 1) + # end index is exclusive + e = torch.clamp(e, -length - 1, length) + + if stride_0 > 0: + b = b + length if b < 0 else b + e = e + length if e < 0 else e + indices = torch.arange(b, e, stride_0, device=inputs.device) + else: + b = b - length if b >= 0 else b + e = e - length if e >= 0 else e + indices = torch.arange(b, e, stride_0, device=inputs.device) + indices += length + + output = torch.index_select(output, i, indices) + + for i, mask in enumerate(self.attrs.new_axis_mask[::-1]): + if mask == 1: + i = abs(i - len(self.attrs.new_axis_mask) + 1) + output = output.unsqueeze(i) + + for i, mask in enumerate(self.attrs.shrink_axis_mask[::-1]): + if mask == 1: + i = abs(i - len(self.attrs.new_axis_mask) + 1) + if output.size(i) != 1: + raise NotImplementedError + output = output.squeeze(i) + + return output + + +@dataclass +class SplitV1Attribute(Attribute): + """SplitV1Attribute class.""" + + num_splits: int + + +@OPS.register() +class SplitV1(Operation[SplitV1Attribute]): + """SplitV1 class.""" + + TYPE = "Split" + VERSION = "opset1" + ATTRIBUTE_FACTORY = SplitV1Attribute + + def forward(self, inputs, axis): + """SplitV1's forward function.""" + split_size = inputs.shape[axis] // self.attrs.num_splits + return torch.split(tensor=inputs, split_size_or_sections=split_size, dim=axis) + + +@dataclass +class VariadicSplitV1Attribute(Attribute): + """VariadicSplitV1Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class VariadicSplitV1(Operation[VariadicSplitV1Attribute]): + """VariadicSplitV1 class.""" + + TYPE = "VariadicSplit" + VERSION = "opset1" + ATTRIBUTE_FACTORY = VariadicSplitV1Attribute + + def forward(self, inputs, axis, split_lengths): + """VariadicSplitV1's forward function.""" + idx = [i for i, j in enumerate(split_lengths) if j == -1] + if idx: + assert len(idx) == 1 + idx = idx[0] + split_lengths[idx] = inputs.size(axis) - sum(split_lengths) - 1 + assert inputs.size(axis) == sum(split_lengths) + outputs = [] + start_idx = 0 + for length in split_lengths: + outputs.append( + torch.index_select( + inputs, + axis, + torch.arange(start_idx, start_idx + length, device=inputs.device), + ) + ) + start_idx += length + return tuple(outputs) + + +@dataclass +class ShuffleChannelsV0Attribute(Attribute): + """ShuffleChannelsV0Attribute class.""" + + axis: int = field(default=1) + group: int = field(default=1) + + +@OPS.register() +class ShuffleChannelsV0(Operation[ShuffleChannelsV0Attribute]): + """ShuffleChannelsV0 class.""" + + TYPE = "ShuffleChannels" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ShuffleChannelsV0Attribute + + def forward(self, inputs): + """ShuffleChannelsV0's forward function.""" + # n, c, h, w = input.shape + assert inputs.dim() == 4 + origin_shape = inputs.shape + origin_dim = inputs.dim() + assert origin_shape[self.attrs.axis] % self.attrs.group == 0 + + axis = self.attrs.axis + axis = axis if axis >= 0 else axis + inputs.dim() + + target_shape = [ + 0, + self.attrs.group, + int(origin_shape[axis] / self.attrs.group), + 0, + ] + if axis == 0: + target_shape[0] = 1 + target_shape[-1] = math.prod([origin_shape[i] for i in range(axis + 1, origin_dim)]) + elif axis == inputs.dim() - 1: + target_shape[0] = math.prod([origin_shape[i] for i in range(0, axis)]) + target_shape[-1] = 1 + else: + target_shape[0] = math.prod([origin_shape[i] for i in range(0, axis)]) + target_shape[-1] = math.prod([origin_shape[i] for i in range(axis + 1, origin_dim)]) + + output = inputs.reshape(target_shape) + output = output.permute([0, 2, 1, 3]) + output = output.reshape(origin_shape) + return output + + +@dataclass +class BroadcastV3Attribute(Attribute): + """BroadcastV3Attribute class.""" + + mode: str = field(default="numpy") + + def __post_init__(self): + """BroadcastV3Attribute's post-init function.""" + super().__post_init__() + valid_mode = ["numpy", "explicit", "bidirectional"] + if self.mode not in valid_mode: + raise ValueError(f"Invalid mode {self.mode}. " f"It must be one of {valid_mode}.") + + +@OPS.register() +class BroadcastV3(Operation[BroadcastV3Attribute]): + """BroadcastV3 class.""" + + TYPE = "Broadcast" + VERSION = "opset1" + ATTRIBUTE_FACTORY = BroadcastV3Attribute + + def forward(self, inputs, target_shape, axes_mapping=None): + """BroadcastV3's forward function.""" + if self.attrs.mode == "numpy": + return inputs.expand(*target_shape) + if self.attrs.mode == "bidirectional": + return torch.ones(*target_shape, device=inputs.device) * inputs + assert axes_mapping is not None + prev = -1 + for axes in axes_mapping: + prev += 1 + while axes - prev > 0: + inputs = inputs.unsqueeze(axes - 1) + prev += 1 + while inputs.dim() < len(target_shape): + inputs = inputs.unsqueeze(-1) + return inputs.expand(*target_shape) + + +@dataclass +class ScatterNDUpdateV3Attribute(Attribute): + """ScatterNDUpdateV3Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class ScatterNDUpdateV3(Operation[ScatterNDUpdateV3Attribute]): + """ScatterNDUpdateV3 class.""" + + TYPE = "ScatterNDUpdate" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ScatterNDUpdateV3Attribute + + def forward(self, inputs, indicies, updates): + """ScatterNDUpdateV3's forward function.""" + # TODO: need to verify + if updates.numel() == 1: + raise NotImplementedError + + # FIXME: hard-coded + last_dim = indicies.shape[-1] + assert last_dim == 2 + assert indicies[..., -2].sum() == 0 + inputs.shape[indicies.shape[-1] :] # pylint: disable=pointless-statement + index = indicies[..., -1] + for i in inputs.shape[indicies.shape[-1] :]: + index = index.unsqueeze(-1).tile((i,)) + output = torch.scatter(inputs, 1, index, updates) + + return output + + +@dataclass +class ScatterUpdateV3Attribute(Attribute): + """ScatterUpdateV3Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class ScatterUpdateV3(Operation[ScatterUpdateV3Attribute]): + """ScatterUpdateV3 class.""" + + TYPE = "ScatterUpdate" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ScatterUpdateV3Attribute + + def forward(self, inputs, indicies, updates, axis): + """ScatterUpdateV3's forward function.""" + # TODO: need to verify + axis = axis.item() + + if inputs.dtype != updates.dtype: + updates = updates.type(inputs.dtype) + + if indicies.dim() == 0: + assert axis == 0 + output = inputs + output[indicies] = updates + + output = torch.scatter(inputs, axis, indicies, updates) + + return output + + +@dataclass +class TileV0Attribute(Attribute): + """TileV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class TileV0(Operation[TileV0Attribute]): + """TileV0 class.""" + + TYPE = "Tile" + VERSION = "opset1" + ATTRIBUTE_FACTORY = TileV0Attribute + + def forward(self, inputs, repeats): + """TileV0's forward function.""" + return torch.tile(inputs, repeats.tolist()) + + +def get_torch_padding(pads_begin, pads_end, auto_pad, input_size, weight_size, stride, dilation=None): + """Getter function for torch padding.""" + if dilation is None: + dilation = [1 for _ in input_size] + + if auto_pad == "valid": + return 0 + if auto_pad in ("same_upper", "same_lower"): + assert len(set(dilation)) == 1 and dilation[0] == 1 + pads_begin = [] + pads_end = [] + for input_size_, weight_size_, stride_, _ in zip(input_size, weight_size, stride, dilation): + out_size = math.ceil(input_size_ / stride_) + padding_needed = max(0, (out_size - 1) * stride_ + weight_size_ - input_size_) + padding_lhs = int(padding_needed / 2) + padding_rhs = padding_needed - padding_lhs + + pads_begin.append(padding_lhs if auto_pad == "same_upper" else padding_rhs) + pads_end.append(padding_rhs if auto_pad == "same_upper" else padding_lhs) + pad = PadV1.get_torch_pad_dim(pads_begin, pads_end) + return partial(F.pad, pad=pad, mode="constant", value=0) + if auto_pad == "explicit": + pad = PadV1.get_torch_pad_dim(pads_begin, pads_end) + return partial(F.pad, pad=pad, mode="constant", value=0) + raise NotImplementedError diff --git a/src/otx/core/ov/ops/normalizations.py b/src/otx/core/ov/ops/normalizations.py new file mode 100644 index 00000000000..d86f9032ad3 --- /dev/null +++ b/src/otx/core/ov/ops/normalizations.py @@ -0,0 +1,207 @@ +"""Normalization-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass, field + +import torch +from torch.nn import functional as F + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.op import Attribute, Operation +from otx.core.ov.ops.poolings import AvgPoolV1 + + +@dataclass +class BatchNormalizationV0Attribute(Attribute): + """BatchNormalizationV0Attribute class.""" + + epsilon: float + max_init_iter: int = field(default=2) + + +@OPS.register() +class BatchNormalizationV0(Operation[BatchNormalizationV0Attribute]): + """BatchNormalizationV0 class.""" + + TYPE = "BatchNormInference" + VERSION = "opset1" + ATTRIBUTE_FACTORY = BatchNormalizationV0Attribute + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_buffer("_num_init_iter", torch.tensor(0)) + + def forward(self, inputs, gamma, beta, mean, variance): + """BatchNormalizationV0's forward function.""" + + output = F.batch_norm( + input=inputs, + running_mean=mean, + running_var=variance, + weight=gamma, + bias=beta, + training=self.training, + momentum=0.1, + eps=self.attrs.epsilon, + ) + + if self.training and self._num_init_iter < self.attrs.max_init_iter: + # no parameters update for adaptive phase + with torch.no_grad(): + n_dims = inputs.dim() - 2 + gamma = gamma.unsqueeze(0) + beta = beta.unsqueeze(0) + for _ in range(n_dims): + gamma = gamma.unsqueeze(-1) + beta = beta.unsqueeze(-1) + output = inputs * gamma + beta + self._num_init_iter += 1 + if self._num_init_iter >= self.attrs.max_init_iter: + # Adapt weight & bias using the first batch statistics + # to undo normalization approximately + gamma.data = gamma.data * mean + beta.data = beta.data + (mean / (variance + self.attrs.epsilon)) + + return output + + +@dataclass +class LocalResponseNormalizationV0Attribute(Attribute): + """LocalResponseNormalizationV0Attribute class.""" + + alpha: float + beta: float + bias: float + size: int + + +@OPS.register() +class LocalResponseNormalizationV0(Operation[LocalResponseNormalizationV0Attribute]): + """LocalResponseNormalizationV0 class.""" + + TYPE = "LRN" + VERSION = "opset1" + ATTRIBUTE_FACTORY = LocalResponseNormalizationV0Attribute + + def forward(self, inputs, axes): + """LocalResponseNormalizationV0's forward function.""" + dim = inputs.dim() + + axes = axes.detach().cpu().tolist() + assert all(ax >= 1 for ax in axes) + + axes = [ax - 1 for ax in axes] + kernel = [1 for _ in range(dim - 1)] + stride = [1 for _ in range(dim - 1)] + pads_begin = [0 for _ in range(dim - 1)] + pads_end = [0 for _ in range(dim - 1)] + for axe in axes: + kernel[axe] = self.attrs.size + pads_begin[axe] = self.attrs.size // 2 + pads_end[axe] = (self.attrs.size - 1) // 2 + + avg_attrs = { + "auto_pad": "explicit", + "strides": stride, + "kernel": kernel, + "pads_begin": pads_begin, + "pads_end": pads_end, + "exclude-pad": True, + "shape": self.shape, + } + avg_pool = AvgPoolV1("temp", **avg_attrs) + + div = inputs.mul(inputs).unsqueeze(1) + div = avg_pool(div) + div = div.squeeze(1) + div = div.mul(self.attrs.alpha).add(self.attrs.bias).pow(self.attrs.beta) + output = inputs / div + return output + + +@dataclass +class NormalizeL2V0Attribute(Attribute): + """NormalizeL2V0Attribute class.""" + + eps: float + eps_mode: str + + def __post_init__(self): + """NormalizeL2V0Attribute post-init function.""" + super().__post_init__() + valid_eps_mode = ["add", "max"] + if self.eps_mode not in valid_eps_mode: + raise ValueError(f"Invalid eps_mode {self.eps_mode}. " f"It must be one of {valid_eps_mode}.") + + +@OPS.register() +class NormalizeL2V0(Operation[NormalizeL2V0Attribute]): + """NormalizeL2V0 class.""" + + TYPE = "NormalizeL2" + VERSION = "opset1" + ATTRIBUTE_FACTORY = NormalizeL2V0Attribute + + def forward(self, inputs, axes): + """NormalizeL2V0's forward function.""" + eps = self.attrs.eps + eps_mode = self.attrs.eps_mode + + if isinstance(axes, torch.Tensor): + axes = axes.detach().cpu().tolist() + if not isinstance(axes, (list, tuple)): + axes = [axes] + + # normalization layer convert to FP32 in FP16 training + input_float = inputs.float() + if axes: + norm = input_float.pow(2).sum(axes, keepdim=True) + else: + norm = input_float + + if eps_mode == "add": + norm = norm + eps + elif eps_mode == "max": + norm = torch.clamp(norm, max=eps) + + return (input_float / norm.sqrt()).type_as(inputs) + + +@dataclass +class MVNV6Attribute(Attribute): + """MVNV6Attribute class.""" + + normalize_variance: bool + eps: float + eps_mode: str + + def __post_init__(self): + """MVNV6Attribute's post-init function.""" + super().__post_init__() + valid_eps_mode = ["INSIDE_SQRT", "OUTSIDE_SQRT"] + if self.eps_mode not in valid_eps_mode: + raise ValueError(f"Invalid eps_mode {self.eps_mode}. " f"It must be one of {valid_eps_mode}.") + + +@OPS.register() +class MVNV6(Operation[MVNV6Attribute]): + """MVNV6 class.""" + + TYPE = "MVN" + VERSION = "opset1" + ATTRIBUTE_FACTORY = MVNV6Attribute + + def forward(self, inputs, axes): + """MVNV6's forward function.""" + output = inputs - inputs.mean(axes.tolist(), keepdim=True) + if self.attrs.normalize_variance: + eps_mode = self.attrs.eps_mode + eps = self.attrs.eps + var = torch.square(output).mean(axes.tolist(), keepdim=True) + if eps_mode == "INSIDE_SQRT": + output = output / torch.sqrt(var + eps) + elif eps_mode == "OUTSIDE_SQRT": + output = output / (torch.sqrt(var) + eps) + return output diff --git a/src/otx/core/ov/ops/object_detections.py b/src/otx/core/ov/ops/object_detections.py new file mode 100644 index 00000000000..acb4a943835 --- /dev/null +++ b/src/otx/core/ov/ops/object_detections.py @@ -0,0 +1,212 @@ +"""Object-detection-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass, field +from typing import List, Optional + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.op import Attribute, Operation + +# pylint: disable=too-many-instance-attributes + + +@dataclass +class ProposalV4Attribute(Attribute): + """ProposalV4Attribute class.""" + + base_size: int + pre_nms_topn: int + post_nms_topn: int + nms_thresh: float + feat_stride: int + min_size: int + ratio: List[float] + scale: List[float] + clip_before_nms: bool = field(default=True) + clip_after_nms: bool = field(default=False) + normalize: bool = field(default=False) + box_size_scale: float = field(default=1.0) + box_coordinate_scale: float = field(default=1.0) + framework: str = field(default="") + + def __post_init__(self): + """ProposalV4Attribute's post-init function.""" + super().__post_init__() + valid_framework = ["", "tensorflow"] + if self.framework not in valid_framework: + raise ValueError(f"Invalid framework {self.framework}. " f"It must be one of {valid_framework}.") + + +@OPS.register() +class ProposalV4(Operation[ProposalV4Attribute]): + """ProposalV4 class.""" + + TYPE = "Proposal" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ProposalV4Attribute + + def forward(self, class_probs, bbox_deltas, image_shape): + """ProposalV4's forward function.""" + raise NotImplementedError + + +@dataclass +class ROIPoolingV0Attribute(Attribute): + """ROIPoolingV0Attribute class.""" + + pooled_h: int + pooled_w: int + spatial_scale: float + method: str = field(default="max") + output_size: List[int] = field(default_factory=lambda: []) + + def __post_init__(self): + """ROIPoolingV0Attribute's post-init function.""" + super().__post_init__() + valid_method = ["max", "bilinear"] + if self.method not in valid_method: + raise ValueError(f"Invalid method {self.method}. " f"It must be one of {valid_method}.") + + +@OPS.register() +class ROIPoolingV0(Operation[ROIPoolingV0Attribute]): + """ROIPoolingV0 class.""" + + TYPE = "ROIPooling" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ROIPoolingV0Attribute + + def forward(self, inputs, boxes): + """ROIPoolingV0's forward function.""" + raise NotImplementedError + + +@dataclass +class DetectionOutputV0Attribute(Attribute): + """DetectionOutputV0Attribute class.""" + + keep_top_k: List[int] + nms_threshold: float + background_label_id: int = field(default=0) + top_k: int = field(default=-1) + variance_encoded_in_target: bool = field(default=False) + code_type: str = field(default="caffe.PriorBoxParameter.CORNER") + share_location: bool = field(default=True) + confidence_threshold: float = field(default=0) + clip_after_nms: bool = field(default=False) + clip_before_nms: bool = field(default=False) + decrease_label_id: bool = field(default=False) + normalized: bool = field(default=False) + input_height: int = field(default=1) + input_width: int = field(default=1) + objectness_score: float = field(default=0) + + def __post_init__(self): + """DetectionOutputV0Attribute's post-init function.""" + super().__post_init__() + valid_code_type = [ + "caffe.PriorBoxParameter.CORNER", + "caffe.PriorBoxParameter.CENTER_SIZE", + ] + if self.code_type not in valid_code_type: + raise ValueError(f"Invalid code_type {self.code_type}. " f"It must be one of {valid_code_type}.") + + +@OPS.register() +class DetectionOutputV0(Operation[DetectionOutputV0Attribute]): + """DetectionOutputV0 class.""" + + TYPE = "DetectionOutput" + VERSION = "opset1" + ATTRIBUTE_FACTORY = DetectionOutputV0Attribute + + def forward(self, loc_data, conf_data, prior_data, arm_conf_data=None, arm_loc_data=None): + """DetectionOutputV0's forward.""" + raise NotImplementedError + + +@dataclass +class RegionYoloV0Attribute(Attribute): + """RegionYoloV0Attribute class.""" + + axis: int + coords: int + classes: int + end_axis: int + num: int + anchors: Optional[List[float]] = field(default=None) + do_softmax: bool = field(default=True) + mask: List[int] = field(default_factory=lambda: []) + + +@OPS.register() +class RegionYoloV0(Operation[RegionYoloV0Attribute]): + """RegionYoloV0 class.""" + + TYPE = "RegionYolo" + VERSION = "opset1" + ATTRIBUTE_FACTORY = RegionYoloV0Attribute + + def forward(self, inputs): + """RegionYoloV0's forward function.""" + raise NotImplementedError + + +@dataclass +class PriorBoxV0Attribute(Attribute): + """PriorBoxV0Attribute class.""" + + offset: float + min_size: List[float] = field(default_factory=lambda: []) + max_size: List[float] = field(default_factory=lambda: []) + aspect_ratio: List[float] = field(default_factory=lambda: []) + flip: bool = field(default=False) + clip: bool = field(default=False) + step: float = field(default=0) + variance: List[float] = field(default_factory=lambda: []) + scale_all_sizes: bool = field(default=True) + fixed_ratio: List[float] = field(default_factory=lambda: []) + fixed_size: List[float] = field(default_factory=lambda: []) + density: List[float] = field(default_factory=lambda: []) + + +@OPS.register() +class PriorBoxV0(Operation[PriorBoxV0Attribute]): + """PriorBoxV0 class.""" + + TYPE = "PriorBox" + VERSION = "opset1" + ATTRIBUTE_FACTORY = PriorBoxV0Attribute + + def forward(self, output_size, image_size): + """PriorBoxV0's forward function.""" + raise NotImplementedError + + +@dataclass +class PriorBoxClusteredV0Attribute(Attribute): + """PriorBoxClusteredV0Attribute class.""" + + offset: float + width: List[float] = field(default_factory=lambda: [1.0]) + height: List[float] = field(default_factory=lambda: [1.0]) + clip: bool = field(default=False) + step: float = field(default=0.0) + step_w: float = field(default=0.0) + step_h: float = field(default=0.0) + variance: List[float] = field(default_factory=lambda: []) + + +@OPS.register() +class PriorBoxClusteredV0(Operation[PriorBoxClusteredV0Attribute]): + """PriorBoxClusteredV0 class.""" + + TYPE = "PriorBoxClustered" + VERSION = "opset1" + ATTRIBUTE_FACTORY = PriorBoxClusteredV0Attribute + + def forward(self, output_size, image_size): + """PriorBoxClusteredV0's forward function.""" + raise NotImplementedError diff --git a/src/otx/core/ov/ops/op.py b/src/otx/core/ov/ops/op.py new file mode 100644 index 00000000000..21d4017b159 --- /dev/null +++ b/src/otx/core/ov/ops/op.py @@ -0,0 +1,100 @@ +"""Operation-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import re +from dataclasses import dataclass, fields +from typing import Generic, Optional, Tuple, Type, TypeVar, Union + +import torch + +from ..utils import get_op_name # type: ignore[attr-defined] +from .utils import get_dynamic_shape + + +@dataclass +class Attribute: + """Attribute class.""" + + shape: Optional[Union[Tuple[Tuple[int]], Tuple[int]]] + + def __post_init__(self): + """Attribute's post-init function.""" + if self.shape is not None and not isinstance(self.shape, tuple): + raise ValueError("shape must be a tuple of ints or a tuple of tuples of ints.") + + +_T = TypeVar("_T", bound=Attribute) + + +class Operation(torch.nn.Module, Generic[_T]): # pylint: disable=abstract-method, invalid-overridden-method + """Operation class.""" + + TYPE = "" + VERSION = "" + ATTRIBUTE_FACTORY: Type[Attribute] = Attribute + + def __init__(self, name: str, **kwargs): + super().__init__() + self._name = name + self._attrs = self.ATTRIBUTE_FACTORY(**kwargs) + + @classmethod + def from_ov(cls, ov_op): + """Operation's from_ov function.""" + op_type = ov_op.get_type_name() + op_version = ov_op.get_type_info().version_id + op_name = get_op_name(ov_op) + assert cls.TYPE and cls.VERSION + assert op_type == cls.TYPE + assert op_version == cls.VERSION + + attrs = ov_op.get_attributes() + if "shape" not in attrs: + shapes = [] + for output in ov_op.outputs(): + shapes.append(get_dynamic_shape(output)) + shapes = tuple(tuple(shape) for shape in shapes) + attrs["shape"] = shapes + + return cls(name=op_name, **attrs) + + @property + def type(self) -> str: # pylint: disable=invalid-overridden-method + """Operation's type property.""" + return self.TYPE + + @property + def version(self) -> str: + """Operation's version property.""" + return self.VERSION + + @property + def name(self) -> str: + """Operation's name property.""" + return self._name + + @property + def attrs(self): + """Operation's attrs property.""" + return self._attrs + + @property + def shape(self) -> Optional[Union[Tuple[Tuple[int]], Tuple[int]]]: + """Operation's shape property.""" + return self.attrs.shape + + def __repr__(self): + """Operation's __repr__ function.""" + repr_str = f"{self.__class__.__name__}(" + repr_str += f"name={self.name}, " + for field in fields(self.attrs): + key = field.name + if key == "shape": + continue + value = getattr(self.attrs, key) + repr_str += f"{key}={value}, " + repr_str = re.sub(", $", "", repr_str) + repr_str += ")" + return repr_str diff --git a/src/otx/core/ov/ops/poolings.py b/src/otx/core/ov/ops/poolings.py new file mode 100644 index 00000000000..d64afdfbf30 --- /dev/null +++ b/src/otx/core/ov/ops/poolings.py @@ -0,0 +1,167 @@ +"""Pooling-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass, field +from typing import Callable, List + +from torch.nn import functional as F + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.movements import get_torch_padding +from otx.core.ov.ops.op import Attribute, Operation + +# pylint: disable=too-many-instance-attributes + + +@dataclass +class MaxPoolV0Attribute(Attribute): + """MaxPoolV0Attribute class.""" + + strides: List[int] + pads_begin: List[int] + pads_end: List[int] + kernel: List[int] + rounding_type: str = field(default="floor") + auto_pad: str = field(default="explicit") + dilations: List[int] = field(default_factory=lambda: []) + index_element_type: str = field(default="i64") + axis: int = field(default=0) + + def __post_init__(self): + """MaxPoolV0Attribute's post-init functions.""" + super().__post_init__() + valid_auto_pad = ["explicit", "same_upper", "same_Lower", "valid"] + if self.auto_pad not in valid_auto_pad: + raise ValueError(f"Invalid auto_pad {self.auto_pad}. " f"It must be one of {valid_auto_pad}.") + valid_rounding_type = ["ceil", "floor"] + if self.rounding_type not in valid_rounding_type: + raise ValueError( + f"Invalid rounding_type {self.rounding_type}. " f"It must be one of {valid_rounding_type}." + ) + valid_index_element_type = ["i32", "i64"] + if self.index_element_type not in valid_index_element_type: + raise ValueError( + f"Invalid index_element_type {self.index_element_type}. " + f"It must be one of {valid_index_element_type}." + ) + + if not self.dilations: + self.dilations = [1 for _ in self.strides] + + if self.axis != 0: + raise NotImplementedError + + +@OPS.register() +class MaxPoolV0(Operation[MaxPoolV0Attribute]): + """MaxPoolV0 class.""" + + TYPE = "MaxPool" + VERSION = "opset8" + ATTRIBUTE_FACTORY = MaxPoolV0Attribute + + def forward(self, inputs): + """MaxPoolV0's forward function.""" + if inputs.dim() == 3: + func = F.max_pool1d + elif inputs.dim() == 4: + func = F.max_pool2d + elif inputs.dim() == 5: + func = F.max_pool3d + else: + raise NotImplementedError + + padding = get_torch_padding( + self.attrs.pads_begin, + self.attrs.pads_end, + self.attrs.auto_pad, + list(inputs.shape[2:]), + self.attrs.kernel, + self.attrs.strides, + ) + if isinstance(padding, Callable): + inputs = padding(input=inputs) + padding = 0 + + return func( + input=inputs, + kernel_size=self.attrs.kernel, + stride=self.attrs.strides, + padding=padding, + dilation=self.attrs.dilations, + ceil_mode=self.attrs.rounding_type == "ceil", + return_indices=True, + ) + + +@dataclass +class AvgPoolV1Attribute(Attribute): + """AvgPoolV1Attribute class.""" + + exclude_pad: bool + strides: List[int] + pads_begin: List[int] + pads_end: List[int] + kernel: List[int] + rounding_type: str = field(default="floor") + auto_pad: str = field(default="explicit") + + def __post_init__(self): + """AvgPoolV1Attribute's post-init function.""" + super().__post_init__() + valid_auto_pad = ["explicit", "same_upper", "same_Lower", "valid"] + if self.auto_pad not in valid_auto_pad: + raise ValueError(f"Invalid auto_pad {self.auto_pad}. " f"It must be one of {valid_auto_pad}.") + valid_rounding_type = ["ceil", "floor"] + if self.rounding_type not in valid_rounding_type: + raise ValueError( + f"Invalid rounding_type {self.rounding_type}. " f"It must be one of {valid_rounding_type}." + ) + + +@OPS.register() +class AvgPoolV1(Operation[AvgPoolV1Attribute]): + """AvgPoolV1 class.""" + + TYPE = "AvgPool" + VERSION = "opset1" + ATTRIBUTE_FACTORY = AvgPoolV1Attribute + + def __init__(self, *args, **kwargs): + if "exclude-pad" in kwargs: + kwargs["exclude_pad"] = kwargs.pop("exclude-pad") + super().__init__(*args, **kwargs) + + def forward(self, inputs): + """AvgPoolV1's forward function.""" + if inputs.dim() == 3: + func = F.avg_pool1d + elif inputs.dim() == 4: + func = F.avg_pool2d + elif inputs.dim() == 5: + func = F.avg_pool3d + else: + raise NotImplementedError + + padding = get_torch_padding( + self.attrs.pads_begin, + self.attrs.pads_end, + self.attrs.auto_pad, + list(inputs.shape[2:]), + self.attrs.kernel, + self.attrs.strides, + ) + if isinstance(padding, Callable): + inputs = padding(input=inputs) + padding = 0 + + return func( + input=inputs, + kernel_size=self.attrs.kernel, + stride=self.attrs.strides, + padding=padding, + ceil_mode=self.attrs.rounding_type == "ceil", + count_include_pad=not self.attrs.exclude_pad, + ) diff --git a/src/otx/core/ov/ops/reductions.py b/src/otx/core/ov/ops/reductions.py new file mode 100644 index 00000000000..4b52328f42f --- /dev/null +++ b/src/otx/core/ov/ops/reductions.py @@ -0,0 +1,132 @@ +"""Redunction-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass, field + +import torch + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.op import Attribute, Operation + + +@dataclass +class ReduceMeanV1Attribute(Attribute): + """ReduceMeanV1Attribute class.""" + + keep_dims: bool = field(default=False) + + +@OPS.register() +class ReduceMeanV1(Operation[ReduceMeanV1Attribute]): + """ReduceMeanV1 class.""" + + TYPE = "ReduceMean" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ReduceMeanV1Attribute + + def forward(self, inputs, axes): + """ReduceMeanV1's forward function.""" + if isinstance(axes, torch.Tensor): + axes = axes.tolist() + if not axes: + return inputs + + if not isinstance(axes, (list, tuple)): + axes = [axes] + + return torch.mean(input=inputs, dim=axes, keepdim=self.attrs.keep_dims) + + +@dataclass +class ReduceProdV1Attribute(Attribute): + """ReduceMeanV1Attribute class.""" + + keep_dims: bool = field(default=False) + + +@OPS.register() +class ReduceProdV1(Operation[ReduceProdV1Attribute]): + """ReduceMeanV1Attribute class.""" + + TYPE = "ReduceProd" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ReduceProdV1Attribute + + def forward(self, inputs, axes): + """ReduceMeanV1Attribute's forward function.""" + if isinstance(axes, torch.Tensor): + axes = axes.tolist() + if not axes: + return inputs + + if not isinstance(axes, (list, tuple)): + axes = [axes] + + output = inputs + for axe in axes: + output = torch.prod(input=output, dim=axe, keepdim=True) + if not self.attrs.keep_dims: + output = torch.squeeze(output) + + return output + + +@dataclass +class ReduceMinV1Attribute(Attribute): + """ReduceMinV1Attribute class.""" + + keep_dims: bool = field(default=False) + + +@OPS.register() +class ReduceMinV1(Operation[ReduceMinV1Attribute]): + """ReduceMinV1 class.""" + + TYPE = "ReduceMin" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ReduceMinV1Attribute + + def forward(self, inputs, axes): + """ReduceMinV1's forward function.""" + if isinstance(axes, torch.Tensor): + axes = axes.tolist() + if not axes: + return inputs + + if not isinstance(axes, (list, tuple)): + axes = [axes] + + output = inputs + for axe in axes: + output = torch.min(input=output, dim=axe, keepdim=True)[0] + if not self.attrs.keep_dims: + output = torch.squeeze(output) + + return output + + +@dataclass +class ReduceSumV1Attribute(Attribute): + """ReduceSumV1Attribute class.""" + + keep_dims: bool = field(default=False) + + +@OPS.register() +class ReduceSumV1(Operation[ReduceSumV1Attribute]): + """ReduceSumV1 class.""" + + TYPE = "ReduceSum" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ReduceSumV1Attribute + + def forward(self, inputs, axes): + """ReduceSumV1's forward function.""" + if isinstance(axes, torch.Tensor): + axes = axes.tolist() + if not axes: + return inputs + + return torch.sum(input=inputs, dim=axes, keepdim=self.attrs.keep_dims) diff --git a/src/otx/core/ov/ops/shape_manipulations.py b/src/otx/core/ov/ops/shape_manipulations.py new file mode 100644 index 00000000000..358218b9ee1 --- /dev/null +++ b/src/otx/core/ov/ops/shape_manipulations.py @@ -0,0 +1,163 @@ +"""Shape-mainpulation-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass, field + +import torch + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.op import Attribute, Operation +from otx.core.ov.ops.type_conversions import ConvertV0 + + +@dataclass +class SqueezeV0Attribute(Attribute): + """SqueezeV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class SqueezeV0(Operation[SqueezeV0Attribute]): + """SqueezeV0 class.""" + + TYPE = "Squeeze" + VERSION = "opset1" + ATTRIBUTE_FACTORY = SqueezeV0Attribute + + def forward(self, inputs, dims=None): + """SqueezeV0's forward function.""" + if dims is None: + return torch.squeeze(inputs) + + if dims.dim() == 0: + dims = torch.unsqueeze(dims, 0) + + max_dim = inputs.dim() + dims = dims.detach().cpu().tolist() + for i, dim in enumerate(dims): + if dim < 0: + dims[i] = max_dim + dim + + output = inputs + for dim in sorted(dims, reverse=True): + output = torch.squeeze(output, dim) + + return output + + +@dataclass +class UnsqueezeV0Attribute(Attribute): + """UnsqueezeV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class UnsqueezeV0(Operation[UnsqueezeV0Attribute]): + """UnsqueezeV0 class.""" + + TYPE = "Unsqueeze" + VERSION = "opset1" + ATTRIBUTE_FACTORY = UnsqueezeV0Attribute + + def forward(self, inputs, dims): + """UnsqueezeV0's forward function.""" + if dims.dim() == 0: + dims = torch.unsqueeze(dims, 0) + + max_dim = inputs.dim() + dims = dims.detach().cpu().tolist() + if len(dims) > 1: + for i, dim in enumerate(dims): + if dim < 0: + dims[i] = max_dim + dim + + output = inputs + for dim in sorted(dims, reverse=True): + output = torch.unsqueeze(output, dim) + + return output + + +@dataclass +class ReshapeV1Attribute(Attribute): + """ReshapeV1Attribute class.""" + + special_zero: bool + + +@OPS.register() +class ReshapeV1(Operation[ReshapeV1Attribute]): + """ReshapeV1 class.""" + + TYPE = "Reshape" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ReshapeV1Attribute + + def forward(self, inputs, shape): + """ReshapeV1's forward function.""" + target_shape = shape.detach().cpu().tolist() + origin_shape = list(inputs.shape) + for i, (origin_dim, target_dim) in enumerate(zip(origin_shape, target_shape)): + if target_dim == 0 and self.attrs.special_zero: + target_shape[i] = origin_dim + elif target_dim == -1: + break + for i, (origin_dim, target_dim) in enumerate(zip(origin_shape[::-1], target_shape[::-1])): + if target_dim == 0 and self.attrs.special_zero: + target_shape[i] = origin_dim + elif target_dim == -1: + break + return torch.reshape(inputs, target_shape) + + +@dataclass +class ShapeOfV0Attribute(Attribute): + """ShapeOfV0Attribute class.""" + + pass # pylint: disable=unnecessary-pass + + +@OPS.register() +class ShapeOfV0(Operation[ShapeOfV0Attribute]): + """ShapeOfV0 class.""" + + TYPE = "ShapeOf" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ShapeOfV0Attribute + + def forward(self, inputs): + """ShapeOfV0's forward function.""" + return torch.tensor(inputs.shape, device=inputs.device) + + +@dataclass +class ShapeOfV3Attribute(Attribute): + """ShapeOfV3Attribute class.""" + + output_type: str = field(default="i64") + + def __post_init__(self): + """ShapeOfV3Attribute's post-init function.""" + super().__post_init__() + valid_output_type = ["i64", "i32"] + if self.output_type not in valid_output_type: + raise ValueError(f"Invalid output_type {self.output_type}. " f"It must be one of {valid_output_type}.") + + +@OPS.register() +class ShapeOfV3(Operation[ShapeOfV3Attribute]): + """ShapeOfV3 class.""" + + TYPE = "ShapeOf" + VERSION = "opset3" + ATTRIBUTE_FACTORY = ShapeOfV3Attribute + + def forward(self, inputs): + """ShapeOfV3's forward function.""" + return ConvertV0("temp", shape=self.shape, destination_type=self.attrs.output_type)( + torch.tensor(inputs.shape, device=inputs.device) + ) diff --git a/src/otx/core/ov/ops/sorting_maximization.py b/src/otx/core/ov/ops/sorting_maximization.py new file mode 100644 index 00000000000..28c8906df61 --- /dev/null +++ b/src/otx/core/ov/ops/sorting_maximization.py @@ -0,0 +1,110 @@ +"""Sorting-maximization-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass, field + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.op import Attribute, Operation + + +@dataclass +class TopKV3Attribute(Attribute): + """TopKV3Attribute class.""" + + axis: int + mode: str + sort: str + index_element_type: str = field(default="i32") + + def __post_init__(self): + """TopKV3Attribute's post-init function.""" + super().__post_init__() + valid_mode = ["min", "max"] + if self.mode not in valid_mode: + raise ValueError(f"Invalid mode {self.mode}. " f"It must be one of {valid_mode}.") + + valid_sort = ["value", "index", "none"] + if self.sort not in valid_sort: + raise ValueError(f"Invalid sort {self.sort}. " f"It must be one of {valid_sort}.") + + valid_index_element_type = ["i32", "i64"] + if self.index_element_type not in valid_index_element_type: + raise ValueError( + f"Invalid index_element_type {self.index_element_type}. " + f"It must be one of {valid_index_element_type}." + ) + + +@OPS.register() +class TopKV3(Operation[TopKV3Attribute]): + """TopKV3 class.""" + + TYPE = "TopK" + VERSION = "opset1" + ATTRIBUTE_FACTORY = TopKV3Attribute + + def forward(self, inputs, k): + """TopKV3's forward function.""" + raise NotImplementedError + + +@dataclass +class NonMaxSuppressionV5Attribute(Attribute): + """NonMaxSuppressionV5Attribute class.""" + + box_encoding: str = field(default="corner") + sort_result_descending: bool = field(default=True) + output_type: str = field(default="i64") + + +@OPS.register() +class NonMaxSuppressionV5(Operation[NonMaxSuppressionV5Attribute]): + """NonMaxSuppressionV5 class.""" + + TYPE = "NonMaxSuppression" + VERSION = "opset5" + ATTRIBUTE_FACTORY = NonMaxSuppressionV5Attribute + + def forward( + self, + boxes, + scores, + max_output_boxes_per_class, + iou_threshold=0, + score_threshold=0, + soft_nms_sigma=0, + ): + """NonMaxSuppressionV5's forward function.""" + raise NotImplementedError + + +@dataclass +class NonMaxSuppressionV9Attribute(Attribute): + """NonMaxSuppressionV9Attribute class.""" + + box_encoding: str = field(default="corner") + sort_result_descending: bool = field(default=True) + output_type: str = field(default="i64") + + +@OPS.register() +class NonMaxSuppressionV9(Operation[NonMaxSuppressionV9Attribute]): + """NonMaxSuppressionV9 class.""" + + TYPE = "NonMaxSuppression" + VERSION = "opset1" + ATTRIBUTE_FACTORY = NonMaxSuppressionV9Attribute + + def forward( + self, + boxes, + scores, + max_output_boxes_per_class, + iou_threshold=0, + score_threshold=0, + soft_nms_sigma=0, + ): + """NonMaxSuppressionV9's forward function.""" + raise NotImplementedError diff --git a/src/otx/core/ov/ops/type_conversions.py b/src/otx/core/ov/ops/type_conversions.py new file mode 100644 index 00000000000..267ae7ea37d --- /dev/null +++ b/src/otx/core/ov/ops/type_conversions.py @@ -0,0 +1,73 @@ +"""Type-conversion-related modules for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass + +import torch + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.op import Attribute, Operation + +_torch_to_ov = { + torch.uint8: ["u1", "u4", "u8"], + torch.int8: ["i4", "i8"], + torch.int16: ["i16"], + torch.int32: ["i32"], + torch.int64: ["i64"], + torch.float16: ["f16"], + torch.float32: ["f32"], + torch.bool: ["boolean"], +} + +_ov_to_torch = { + "u1": torch.uint8, # no type in torch + "u4": torch.uint8, # no type in torch + "u8": torch.uint8, + "u16": torch.int32, # no type in torch + "u32": torch.int32, # no type in torch + "u64": torch.int64, # no type in torch + "i4": torch.int8, # no type in torch + "i8": torch.int8, + "i16": torch.int16, + "i32": torch.int32, + "i64": torch.int64, + "f16": torch.float16, + "f32": torch.float32, + "boolean": torch.bool, +} + + +@dataclass +class ConvertV0Attribute(Attribute): + """ConvertV0Attribute class.""" + + destination_type: str + + +@OPS.register() +class ConvertV0(Operation[ConvertV0Attribute]): + """ConvertV0 class.""" + + TYPE = "Convert" + VERSION = "opset1" + ATTRIBUTE_FACTORY = ConvertV0Attribute + + @staticmethod + def convert_ov_type(ov_type): + """ConvertV0's convert_ov_type function.""" + if ov_type not in _ov_to_torch: + raise NotImplementedError + return _ov_to_torch[ov_type] + + @staticmethod + def convert_torch_type(torch_type): + """ConvertV0's convert_torch_type function.""" + if torch_type not in _torch_to_ov: + raise NotImplementedError + return _torch_to_ov[torch_type][-1] + + def forward(self, inputs): + """ConvertV0's forward function.""" + return inputs.type(self.convert_ov_type(self.attrs.destination_type)) diff --git a/src/otx/core/ov/ops/utils.py b/src/otx/core/ov/ops/utils.py new file mode 100644 index 00000000000..b74d37bbb02 --- /dev/null +++ b/src/otx/core/ov/ops/utils.py @@ -0,0 +1,33 @@ +"""Utils function for otx.core.ov.ops.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from openvino.runtime import Node + +from .builder import OPS + + +def get_dynamic_shape(output): + """Getter function for dynamic shape.""" + shape = [str(i) for i in output.get_partial_shape()] + for i, shape_ in enumerate(shape): + try: + shape_ = int(shape_) + except ValueError: + shape_ = -1 + shape[i] = shape_ + return shape + + +def convert_op_to_torch(op_node: Node): + """Convert op Node to torch.""" + op_type = op_node.get_type_name() + + op_version = op_node.get_type_info().version_id + try: + torch_module = OPS.get_by_type_version(op_type, op_version).from_ov(op_node) + except Exception as e: + raise e + + return torch_module diff --git a/src/otx/core/ov/registry.py b/src/otx/core/ov/registry.py new file mode 100644 index 00000000000..f732d8afff0 --- /dev/null +++ b/src/otx/core/ov/registry.py @@ -0,0 +1,56 @@ +"""Registry Class for otx.core.ov.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any, Dict, Optional + + +class Registry: + """Registry Class for OMZ model.""" + + REGISTERED_NAME_ATTR = "_registered_name" + + def __init__(self, name, add_name_as_attr=False): + self._name = name + self._registry_dict = {} + self._add_name_as_attr = add_name_as_attr + + @property + def registry_dict(self) -> Dict[Any, Any]: + """Dictionary of registered module.""" + return self._registry_dict + + def _register(self, obj: Any, name: Any): + """Register obj with name.""" + if name in self._registry_dict: + raise KeyError(f"{name} is already registered in {self._name}") + self._registry_dict[name] = obj + + def register(self, name: Optional[Any] = None): + """Register from name.""" + + def wrap(obj): + cls_name = name + if cls_name is None: + cls_name = obj.__name__ + if self._add_name_as_attr: + setattr(obj, self.REGISTERED_NAME_ATTR, cls_name) + self._register(obj, cls_name) + return obj + + return wrap + + def get(self, key: Any) -> Any: + """Get from module name (key).""" + if key not in self._registry_dict: + self._key_not_found(key) + return self._registry_dict[key] + + def _key_not_found(self, key: Any): + """Raise KeyError when key not founded.""" + raise KeyError(f"{key} is not found in {self._name}") + + def __contains__(self, item): + """Check containing of item.""" + return item in self._registry_dict.values() diff --git a/src/otx/core/ov/utils.py b/src/otx/core/ov/utils.py new file mode 100644 index 00000000000..35413e293bf --- /dev/null +++ b/src/otx/core/ov/utils.py @@ -0,0 +1,127 @@ +# type: ignore +# TODO: Need to remove line 1 (ignore mypy) and fix mypy issues +"""Utils for otx.core.ov.""" +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import errno +import os +from typing import Optional + +from openvino.runtime import Core, Model, Node + +from .omz_wrapper import AVAILABLE_OMZ_MODELS, get_omz_model + +# pylint: disable=too-many-locals + + +def to_dynamic_model(ov_model: Model) -> Model: + """Convert ov_model to dynamic Model.""" + assert isinstance(ov_model, Model) + + shapes = {} + target_layouts = {} + for input_node in ov_model.inputs: + target_layout = { + "batch": ["N", None, None], + "height": ["H", None, None], + "width": ["W", None, None], + } + + any_name = input_node.any_name + parameter_node = input_node.get_node() + layout = parameter_node.get_layout() + if layout.empty: + continue + layout = layout.to_string()[1:-1].split(",") + shape = [str(i) for i in input_node.get_partial_shape()] + for i, (layout_name, shape_) in enumerate(zip(layout, shape)): + try: + shape_ = int(shape_) + except ValueError: + shape_ = -1 + + for target_layout_ in target_layout.values(): + target_layout_name = target_layout_[0] + if layout_name == target_layout_name: + target_layout_[1] = i + target_layout_[2] = shape_ + shape_ = -1 + break + shape[i] = shape_ + + shapes[any_name] = shape + target_layouts[any_name] = target_layout + + def reshape_model(ov_model, shapes): + try: + ov_model.reshape(shapes) + return True + except Exception: # pylint: disable=broad-exception-caught + return False + + pop_targets = [["height", "width"], ["batch"]] + pop_targets = pop_targets[::-1] + while not reshape_model(ov_model, shapes): + for key, shape in shapes.items(): + target_layout = target_layouts[key] + + targets = pop_targets.pop() + for target in targets: + target_idx, target_origin = target_layout[target][1:] + if target_idx is not None: + shape[target_idx] = target_origin + + if len(pop_targets) == 0: + reshape_model(ov_model, shapes) + break + + return ov_model + + +def load_ov_model(model_path: str, weight_path: Optional[str] = None, convert_dynamic: bool = False) -> Model: + """Load ov_model from model_path.""" + model_path = str(model_path) + if model_path.startswith("omz://"): + model_path = model_path.replace("omz://", "") + assert model_path in AVAILABLE_OMZ_MODELS + ov_ir_path = get_omz_model(model_path) + model_path = ov_ir_path["model_path"] + weight_path = ov_ir_path["weight_path"] + + if weight_path is None: + weight_path = os.path.splitext(model_path)[0] + ".bin" + + if not os.path.exists(model_path): + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), model_path) + if not os.path.exists(weight_path): + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), weight_path) + + ie_core = Core() + ov_model = ie_core.read_model(model=model_path, weights=weight_path) + + if convert_dynamic: + ov_model = to_dynamic_model(ov_model) + + return ov_model + + +def normalize_name(name: str) -> str: + """Normalize name string.""" + # ModuleDict does not allow '.' in module name string + name = name.replace(".", "#") + return f"{name}" + + +def unnormalize_name(name: str) -> str: + """Unnormalize name string.""" + name = name.replace("#", ".") + return name + + +def get_op_name(op_node: Node) -> str: + """Get op name string.""" + op_name = op_node.get_friendly_name() + op_name = normalize_name(op_name) + return op_name diff --git a/src/otx/core/patcher.py b/src/otx/core/patcher.py new file mode 100644 index 00000000000..7849a042a1d --- /dev/null +++ b/src/otx/core/patcher.py @@ -0,0 +1,211 @@ +"""Simple monkey patch helper.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# pylint: disable=unnecessary-dunder-call,invalid-name + +import ctypes +import importlib +import inspect +from collections import OrderedDict +from functools import partial, partialmethod +from typing import Callable + + +class Patcher: + """Simple monkey patch helper.""" + + def __init__(self): + self._patched = OrderedDict() + + def patch( # noqa: C901 + self, + obj_cls, + wrapper: Callable, + *, + force: bool = True, + ): + """Do monkey patch.""" + if isinstance(obj_cls, (tuple, list)): + assert len(obj_cls) == 2 + obj_cls, fn_name = obj_cls + assert getattr(obj_cls, fn_name) + else: + obj_cls, fn_name = self.import_obj(obj_cls) + + # wrap only if function does exist + n_args = len(inspect.getfullargspec(obj_cls.__getattribute__)[0]) + if n_args == 1: + try: + fn = obj_cls.__getattribute__(fn_name) + except AttributeError: + return + self._patch_module_fn(obj_cls, fn_name, fn, wrapper, force) + elif inspect.isclass(obj_cls): + try: + fn = obj_cls.__getattribute__(obj_cls, fn_name) # type: ignore + except AttributeError: + return + self._patch_class_fn(obj_cls, fn_name, fn, wrapper, force) + else: + try: + fn = obj_cls.__getattribute__(fn_name) + except AttributeError: + return + self._patch_instance_fn(obj_cls, fn_name, fn, wrapper, force) + + def unpatch(self, obj_cls=None, depth=0): + """Undo monkey patch.""" + + def _unpatch(obj, fn_name, key, depth): + if depth == 0: + depth = len(self._patched[key]) + keep = len(self._patched[key]) - depth + origin_fn = self._patched[key].pop(-depth)[0] + while self._patched[key] and len(self._patched[key]) > keep: + self._patched[key].pop() + if not self._patched[key]: + self._patched.pop(key) + + if isinstance(obj, int): + obj = ctypes.cast(obj, ctypes.py_object).value + setattr(obj, fn_name, origin_fn) + + if obj_cls is not None: + obj_cls, fn_name = self.import_obj(obj_cls) + n_args = len(inspect.getfullargspec(obj_cls.__getattribute__)[0]) + if n_args == 1: + key = (obj_cls.__name__, fn_name) + elif inspect.isclass(obj_cls): + obj_cls_path = obj_cls.__module__ + "." + obj_cls.__name__ + key = (obj_cls_path, fn_name) + else: + key = (id(obj_cls), fn_name) + _unpatch(obj_cls, fn_name, key, depth) + return + + for key in list(self._patched.keys()): + obj, fn_name = key + if isinstance(obj, int): + obj = ctypes.cast(obj, ctypes.py_object).value + else: + obj, fn_name = self.import_obj(".".join([obj, fn_name])) + _unpatch(obj, fn_name, key, depth) + + def import_obj(self, obj_cls): # noqa: C901 + """Object import helper.""" + if isinstance(obj_cls, str): + fn_name = obj_cls.split(".")[-1] + obj_cls = ".".join(obj_cls.split(".")[:-1]) + else: + if "_partialmethod" in obj_cls.__dict__: + while "_partialmethod" in obj_cls.__dict__: + obj_cls = obj_cls._partialmethod.keywords["__fn"] # pylint: disable=protected-access + while isinstance(obj_cls, (partial, partialmethod)): + obj_cls = obj_cls.keywords["__fn"] + + if inspect.ismodule(obj_cls): + fn = obj_cls.keywords["__fn"] + fn_name = fn.__name__ + obj_cls = fn = obj_cls.keywords["__obj_cls"] + elif inspect.ismethod(obj_cls): + fn_name = obj_cls.__name__ + obj_cls = obj_cls.__self__ + elif isinstance(obj_cls, (staticmethod, classmethod)): + obj_cls = obj_cls.__func__ + fn_name = obj_cls.__name__ + obj_cls = ".".join([obj_cls.__module__] + obj_cls.__qualname__.split(".")[:-1]) + else: + fn_name = obj_cls.__name__ + obj_cls = ".".join([obj_cls.__module__] + obj_cls.__qualname__.split(".")[:-1]) + + if isinstance(obj_cls, str): + try: + obj_cls = importlib.import_module(obj_cls) + except ModuleNotFoundError: + module = ".".join(obj_cls.split(".")[:-1]) + obj_cls = obj_cls.split(".")[-1] + obj_cls = getattr(importlib.import_module(module), obj_cls) + return obj_cls, fn_name + + def _patch_module_fn(self, obj_cls, fn_name, fn, wrapper, force): + def helper(*args, **kwargs): # type: ignore + obj_cls = kwargs.pop("__obj_cls") + fn = kwargs.pop("__fn") + wrapper = kwargs.pop("__wrapper") + return wrapper(obj_cls, fn, *args, **kwargs) + + assert len(inspect.getfullargspec(obj_cls.__getattribute__)[0]) == 1 + obj_cls_path = obj_cls.__name__ + key = (obj_cls_path, fn_name) + fn_ = self._initialize(key, force) + if fn_ is not None: + fn = fn_ + setattr(obj_cls, fn_name, partial(helper, __wrapper=wrapper, __fn=fn, __obj_cls=obj_cls)) + self._patched[key].append((fn, wrapper)) + + def _patch_class_fn(self, obj_cls, fn_name, fn, wrapper, force): + + if isinstance(fn, (staticmethod, classmethod)): + + def helper(*args, **kwargs): # type: ignore + wrapper = kwargs.pop("__wrapper") + fn = kwargs.pop("__fn") + obj_cls = kwargs.pop("__obj_cls") + if isinstance(args[0], obj_cls): + return wrapper(args[0], fn.__get__(args[0]), *args[1:], **kwargs) + return wrapper(obj_cls, fn.__get__(obj_cls), *args, **kwargs) + + elif isinstance(fn, type(all.__call__)): + + def helper(self, *args, **kwargs): # type: ignore + kwargs.pop("__obj_cls") + wrapper = kwargs.pop("__wrapper") + fn = kwargs.pop("__fn") + return wrapper(self, fn, *args, **kwargs) + + else: + + def helper(self, *args, **kwargs): # type: ignore + kwargs.pop("__obj_cls") + wrapper = kwargs.pop("__wrapper") + fn = kwargs.pop("__fn") + return wrapper(self, fn.__get__(self), *args, **kwargs) + + assert len(inspect.getfullargspec(obj_cls.__getattribute__)[0]) == 2 + obj_cls_path = obj_cls.__module__ + "." + obj_cls.__name__ + key = (obj_cls_path, fn_name) + fn_ = self._initialize(key, force) + if fn_ is not None: + fn = fn_ + setattr( + obj_cls, + fn_name, + partialmethod(helper, __wrapper=wrapper, __fn=fn, __obj_cls=obj_cls), + ) + self._patched[key].append((fn, wrapper)) + + def _patch_instance_fn(self, obj_cls, fn_name, fn, wrapper, force): + def helper(ctx, *args, **kwargs): # type: ignore + fn = kwargs.pop("__fn") + wrapper = kwargs.pop("__wrapper") + return wrapper(ctx, fn, *args, **kwargs) + + assert len(inspect.getfullargspec(obj_cls.__getattribute__)[0]) == 2 + obj_cls_path = id(obj_cls) + key = (obj_cls_path, fn_name) + fn_ = self._initialize(key, force) + if fn_ is not None: + fn = fn_ + setattr(obj_cls, fn_name, partialmethod(helper, __wrapper=wrapper, __fn=fn).__get__(obj_cls)) + self._patched[key].append((fn, wrapper)) + + def _initialize(self, key, force): + fn = None + if key not in self._patched: + self._patched[key] = [] + if force: + while self._patched[key]: + fn, *_ = self._patched[key].pop() + return fn diff --git a/src/otx/core/types/__init__.py b/src/otx/core/types/__init__.py deleted file mode 100644 index 350ab222cf0..00000000000 --- a/src/otx/core/types/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module reserved for definitions used in OTX.""" - -import os -from pathlib import Path -from typing import Union - -from typing_extensions import TypeAlias - -PathLike: TypeAlias = Union[str, Path, os.PathLike] diff --git a/src/otx/core/types/device.py b/src/otx/core/types/device.py deleted file mode 100644 index 0d11e0393f7..00000000000 --- a/src/otx/core/types/device.py +++ /dev/null @@ -1,21 +0,0 @@ -"""OTX Device type definition.""" - -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from enum import Enum - - -class DeviceType(str, Enum): - """OTX Device type definition.""" - # ("cpu", "gpu", "tpu", "ipu", "hpu", "mps", "auto") - - auto = "auto" - gpu = "gpu" - cpu = "cpu" - tpu = "tpu" - ipu = "ipu" - hpu = "hpu" - mps = "mps" diff --git a/src/otx/core/types/explain.py b/src/otx/core/types/explain.py deleted file mode 100644 index 5ed8b94bea8..00000000000 --- a/src/otx/core/types/explain.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""OTX explain type definition.""" - -from __future__ import annotations - -from enum import Enum - - -class TargetExplainGroup(str, Enum): - """OTX target explain group definition. - - Enum contains the following values: - IMAGE - This implies that single global saliency map will be generated for input image. - ALL - This implies that saliency maps will be generated for all possible targets. - PREDICTIONS - This implies that saliency map will be generated per each prediction. - """ - - IMAGE = "IMAGE" - ALL = "ALL" - PREDICTIONS = "PREDICTIONS" diff --git a/src/otx/core/types/export.py b/src/otx/core/types/export.py deleted file mode 100644 index af439c6ff96..00000000000 --- a/src/otx/core/types/export.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""OTX export-related types definition.""" - -from __future__ import annotations - -from enum import Enum - - -class OTXExportFormatType(str, Enum): - """OTX export format type definition.""" - - ONNX = "ONNX" - OPENVINO = "OPENVINO" - EXPORTABLE_CODE = "EXPORTABLE_CODE" diff --git a/src/otx/core/types/image.py b/src/otx/core/types/image.py deleted file mode 100644 index 8aca0d02245..00000000000 --- a/src/otx/core/types/image.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""OTX image type definition.""" - -from __future__ import annotations - -from enum import Enum, IntEnum, auto - -import numpy as np -from torchvision import tv_tensors - - -class ImageColorChannel(str, Enum): - """ImageColorChannel definition.""" - - RGB = "RGB" - BGR = "BGR" - - -class ImageType(IntEnum): - """Enum to indicate the image type in `ImageInfo` class.""" - - NUMPY = auto() - TV_IMAGE = auto() - NUMPY_LIST = auto() - TV_IMAGE_LIST = auto() - - @classmethod - def get_image_type( - cls, - image: np.ndarray | tv_tensors.Image | list[np.ndarray] | list[tv_tensors.Image], - ) -> ImageType: - """Infer the image type from the given image object.""" - if isinstance(image, np.ndarray): - return ImageType.NUMPY - if isinstance(image, tv_tensors.Image): - return ImageType.TV_IMAGE - if isinstance(image, list): - image = next(iter(image)) - if isinstance(image, np.ndarray): - return ImageType.NUMPY_LIST - if isinstance(image, tv_tensors.Image): - return ImageType.TV_IMAGE_LIST - raise TypeError(image) diff --git a/src/otx/core/types/precision.py b/src/otx/core/types/precision.py deleted file mode 100644 index 3ed742c24ac..00000000000 --- a/src/otx/core/types/precision.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""OTX precision type definition.""" - -from enum import Enum - - -class OTXPrecisionType(str, Enum): - """OTX precision type definition.""" - - FP16 = "FP16" - FP32 = "FP32" diff --git a/src/otx/core/types/task.py b/src/otx/core/types/task.py deleted file mode 100644 index 55c64f50bf6..00000000000 --- a/src/otx/core/types/task.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""OTX task type definition.""" - -from __future__ import annotations - -from enum import Enum - - -class OTXTaskType(str, Enum): - """OTX task type definition.""" - - # Action Recognition - ACTION_CLASSIFICATION = "ACTION_CLASSIFICATION" - ACTION_DETECTION = "ACTION_DETECTION" - - # Anomaly Detection - ANOMALY_CLASSIFICATION = "ANOMALY_CLASSIFICATION" - ANOMALY_DETECTION = "ANOMALY_DETECTION" - ANOMALY_SEGMENTATION = "ANOMALY_SEGMENTATION" - - # Classification - MULTI_CLASS_CLS = "MULTI_CLASS_CLS" - MULTI_LABEL_CLS = "MULTI_LABEL_CLS" - H_LABEL_CLS = "H_LABEL_CLS" - - # Detection - DETECTION = "DETECTION" - ROTATED_DETECTION = "ROTATED_DETECTION" - DETECTION_SEMI_SL = "DETECTION_SEMI_SL" - - # Segmentation - INSTANCE_SEGMENTATION = "INSTANCE_SEGMENTATION" - SEMANTIC_SEGMENTATION = "SEMANTIC_SEGMENTATION" - - # Visual Promting Tasks. - VISUAL_PROMPTING = "VISUAL_PROMPTING" - ZERO_SHOT_VISUAL_PROMPTING = "ZERO_SHOT_VISUAL_PROMPTING" diff --git a/src/otx/core/types/transformer_libs.py b/src/otx/core/types/transformer_libs.py deleted file mode 100644 index 41066908558..00000000000 --- a/src/otx/core/types/transformer_libs.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Transform library types used in OTX.""" - -from __future__ import annotations - -from enum import Enum - - -class TransformLibType(str, Enum): - """Transform library types used in OTX.""" - - TORCHVISION = "TORCHVISION" - MMCV = "MMCV" - MMPRETRAIN = "MMPRETRAIN" - MMDET = "MMDET" - MMSEG = "MMSEG" - MMACTION = "MMACTION" diff --git a/src/otx/core/utils/__init__.py b/src/otx/core/utils/__init__.py deleted file mode 100644 index b47a7c18303..00000000000 --- a/src/otx/core/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for utility functions in OTX.""" diff --git a/src/otx/core/utils/build.py b/src/otx/core/utils/build.py deleted file mode 100644 index ac7177d36ca..00000000000 --- a/src/otx/core/utils/build.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility functions for mmX build function.""" - -from __future__ import annotations - -import warnings -from copy import deepcopy -from typing import TYPE_CHECKING - -from omegaconf import DictConfig - -from otx.core.utils.config import convert_conf_to_mmconfig_dict - -if TYPE_CHECKING: - from mmengine.registry import Registry - from torch import nn - - -def build_mm_model(config: DictConfig, model_registry: Registry, load_from: str | None = None) -> nn.Module: - """Build a model by using the registry.""" - from mmengine.logging import MMLogger - from mmengine.runner import load_checkpoint - - from otx import algo # noqa: F401 - - try: - model = model_registry.build(convert_conf_to_mmconfig_dict(config, to="tuple")) - except AssertionError: - model = model_registry.build(convert_conf_to_mmconfig_dict(config, to="list")) - - mm_logger = MMLogger.get_current_instance() - mm_logger_level = mm_logger.level - mm_logger.setLevel("WARNING") - model.init_weights() - mm_logger.setLevel(mm_logger_level) - if load_from is not None: - load_checkpoint(model, load_from, map_location="cpu") - - return model - - -def get_default_num_async_infer_requests() -> int: - """Returns a default number of infer request for OV models.""" - import os - - number_requests = os.cpu_count() - number_requests = max(1, int(number_requests / 2)) if number_requests is not None else 1 - msg = f"""Set the default number of OpenVINO inference requests to {number_requests}. - You can specify the value in config.""" - warnings.warn(msg, stacklevel=1) - return number_requests - - -def get_classification_layers( - config: DictConfig, - model_registry: Registry, - prefix: str = "", -) -> dict[str, dict[str, int]]: - """Return classification layer names by comparing two different number of classes models. - - Args: - config (DictConfig): Config for building model. - model_registry (Registry): Registry for building model. - prefix (str): Prefix of model param name. - Normally it is "model." since OTXModel set it's nn.Module model as self.model - - Return: - dict[str, dict[str, int]] - A dictionary contain classification layer's name and information. - Stride means dimension of each classes, normally stride is 1, but sometimes it can be 4 - if the layer is related bbox regression for object detection. - Extra classes is default class except class from data. - Normally it is related with background classes. - """ - sample_config = deepcopy(config) - modify_num_classes(sample_config, 5) - sample_model_dict = build_mm_model(sample_config, model_registry, None).state_dict() - - modify_num_classes(sample_config, 6) - incremental_model_dict = build_mm_model(sample_config, model_registry, None).state_dict() - - classification_layers = {} - for key in sample_model_dict: - if sample_model_dict[key].shape != incremental_model_dict[key].shape: - sample_model_dim = sample_model_dict[key].shape[0] - incremental_model_dim = incremental_model_dict[key].shape[0] - stride = incremental_model_dim - sample_model_dim - num_extra_classes = 6 * sample_model_dim - 5 * incremental_model_dim - classification_layers[prefix + key] = {"stride": stride, "num_extra_classes": num_extra_classes} - return classification_layers - - -def modify_num_classes(config: DictConfig, num_classes: int) -> None: - """Modify num_classes of config.""" - for key, value in config.items(): - if key == "num_classes": - config[key] = num_classes - elif isinstance(value, (DictConfig, dict)): - modify_num_classes(value, num_classes) - elif isinstance(value, list): - for item in value: - if isinstance(item, (DictConfig, dict)): - modify_num_classes(item, num_classes) diff --git a/src/otx/core/utils/cache.py b/src/otx/core/utils/cache.py deleted file mode 100644 index 5d9a6e5d1f8..00000000000 --- a/src/otx/core/utils/cache.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Cache Class for Trainer kwargs.""" - -from __future__ import annotations - -import inspect -import logging -from typing import Any - -logger = logging.getLogger(__name__) - - -class TrainerArgumentsCache: - """Cache arguments. - - Since the Engine class accepts PyTorch Lightning Trainer arguments, we store these arguments using this class - before the trainer is instantiated. - - Args: - (**kwargs): Trainer arguments that are cached - - Example: - >>> conf = OmegaConf.load("config.yaml") - >>> cache = TrainerArgumentsCache(**conf) - >>> cache.args - { - ... - 'max_epochs': 100, - 'val_check_interval': 0 - } - >>> config = {"max_epochs": 1, "val_check_interval": 1.0} - >>> cache.update(config) - Overriding max_epochs from 100 with 1 - Overriding val_check_interval from 0 with 1.0 - >>> cache.args - { - ... - 'max_epochs': 1, - 'val_check_interval': 1.0 - } - """ - - def __init__(self, **kwargs) -> None: - self._cached_args = {**kwargs} - - def update(self, **kwargs) -> None: - """Replace cached arguments with arguments retrieved from the model.""" - for key, value in kwargs.items(): - if value is None: - continue - if key in self._cached_args and self._cached_args[key] != value: - logger.info( - f"Overriding {key} from {self._cached_args[key]} with {value}", - ) - self._cached_args[key] = value - - def requires_update(self, **kwargs) -> bool: - """Checks if the cached arguments need to be updated based on the provided keyword arguments. - - Args: - **kwargs: The keyword arguments to compare with the cached arguments. - - Returns: - bool: True if any of the cached arguments need to be updated, False otherwise. - """ - return any(key in self._cached_args and self._cached_args[key] != value for key, value in kwargs.items()) - - @property - def args(self) -> dict[str, Any]: - """Returns the cached arguments. - - Returns: - dict[str, Any]: The cached arguments. - """ - return self._cached_args - - @staticmethod - def get_trainer_constructor_args() -> set[str]: - """Get the set of arguments accepted by the Trainer class constructor. - - Returns: - set[str]: A set of argument names accepted by the Trainer class constructor. - """ - from lightning import Trainer - - sig = inspect.signature(Trainer.__init__) - return set(sig.parameters.keys()) diff --git a/src/otx/core/utils/config.py b/src/otx/core/utils/config.py deleted file mode 100644 index 249e50d5661..00000000000 --- a/src/otx/core/utils/config.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility functions for config files.""" - -from __future__ import annotations - -from numbers import Number -from typing import TYPE_CHECKING, Any, Literal - -from omegaconf import DictConfig, ListConfig, OmegaConf - -if TYPE_CHECKING: - from mmengine.config import Config as MMConfig - - -def to_tuple(dict_: dict) -> dict: - """Find and replace tuple or list values in dict to tuple recursively.""" - # MMDET Mosaic asserts if "img_shape" is not tuple - # File "mmdet/datasets/transforms/transforms.py", line 2324, in __init__ - - for k, v in dict_.items(): - if isinstance(v, (tuple, list)) and all(isinstance(elem, Number) for elem in v): - dict_[k] = tuple(v) - elif isinstance(v, dict): - to_tuple(v) - - return dict_ - - -def to_list(dict_: dict) -> dict: - """Find and replace tuple or list values in dict to list recursively.""" - # MMDET FPN asserts if "in_channels" is not list - # File "mmdet/models/necks/fpn.py", line 88, in __init__ - - for k, v in dict_.items(): - if isinstance(v, (tuple, list)) and all(isinstance(elem, Number) for elem in v): - dict_[k] = list(v) - elif isinstance(v, dict): - to_list(v) - - return dict_ - - -def convert_conf_to_mmconfig_dict( - cfg: DictConfig | dict, - to: Literal["tuple", "list"] = "tuple", -) -> MMConfig: - """Convert OTX format config object to MMEngine config object.""" - from mmengine.config import Config as MMConfig - - cfg = cfg if isinstance(cfg, DictConfig) else OmegaConf.create(cfg) - dict_cfg = OmegaConf.to_container(cfg) - - if to == "tuple": - return MMConfig(cfg_dict=to_tuple(dict_cfg)) - if to == "list": - return MMConfig(cfg_dict=to_list(dict_cfg)) - - raise ValueError(to) - - -def mmconfig_dict_to_dict(obj: MMConfig | list[MMConfig]) -> list | dict: - """Convert MMEngine config object to Python dictionary.""" - if isinstance(obj, list): - return [mmconfig_dict_to_dict(x) for x in obj] - if hasattr(obj, "to_dict"): - return {k: mmconfig_dict_to_dict(v) for k, v in obj.to_dict().items()} - - return obj - - -def inplace_num_classes( - cfg: DictConfig | ListConfig | Any, # noqa: ANN401 - num_classes: int, -) -> DictConfig | ListConfig | Any: # noqa: ANN401 - """Inplace the number of classes values in a given config object. - - Args: - cfg: Config object to inplace the number of classes values - num_classes: Number of classes to inplace - Returns: - Inplaced config object - """ - if isinstance(cfg, DictConfig): - for key in cfg: - if key == "num_classes" and isinstance(cfg[key], int): - cfg[key] = num_classes - else: - cfg[key] = inplace_num_classes(cfg[key], num_classes) - - if isinstance(cfg, ListConfig): - for idx in range(len(cfg)): - cfg[idx] = inplace_num_classes(cfg[idx], num_classes) - - return cfg diff --git a/src/otx/core/utils/imports.py b/src/otx/core/utils/imports.py deleted file mode 100644 index c6e04fe142e..00000000000 --- a/src/otx/core/utils/imports.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for utility functions in OTX.""" - -import importlib -import inspect -from pathlib import Path - - -def get_otx_root_path() -> Path: - """Return the root path of the otx module. - - Returns: - str: The root path of the otx module. - - Raises: - ModuleNotFoundError: If the otx module is not found. - """ - otx_module = importlib.import_module("otx") - if otx_module: - file_path = inspect.getfile(otx_module) - return Path(file_path).parent - msg = "Cannot found otx." - raise ModuleNotFoundError(msg) diff --git a/src/otx/core/utils/instantiators.py b/src/otx/core/utils/instantiators.py deleted file mode 100644 index daf3be15ca6..00000000000 --- a/src/otx/core/utils/instantiators.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Instantiator functions for OTX engine components.""" - -from __future__ import annotations - -import inspect -from functools import partial -from typing import TYPE_CHECKING - -from lightning.pytorch.cli import instantiate_class - -from . import pylogger - -if TYPE_CHECKING: - from lightning import Callback - from lightning.pytorch.loggers import Logger - from torch.utils.data import Dataset, Sampler - - from otx.core.config.data import SamplerConfig - - -log = pylogger.get_pylogger(__name__) - - -def instantiate_callbacks(callbacks_cfg: list) -> list[Callback]: - """Instantiate a list of callbacks based on the provided configuration. - - Args: - callbacks_cfg (list): A list of callback configurations. - - Returns: - list[Callback]: A list of instantiated callbacks. - """ - callbacks: list[Callback] = [] - - if not callbacks_cfg: - log.warning("No callback configs found! Skipping..") - return callbacks - - for cb_conf in callbacks_cfg: - if isinstance(cb_conf, dict) and "class_path" in cb_conf: - log.info(f"Instantiating callback <{cb_conf['class_path']}>") - callbacks.append(instantiate_class(args=(), init=cb_conf)) - - return callbacks - - -def instantiate_loggers(logger_cfg: list | None) -> list[Logger]: - """Instantiate loggers based on the provided logger configuration. - - Args: - logger_cfg (list | None): The logger configuration. - - Returns: - list[Logger]: The list of instantiated loggers. - """ - logger: list[Logger] = [] - - if not logger_cfg: - log.warning("No logger configs found! Skipping...") - return logger - - for lg_conf in logger_cfg: - if isinstance(lg_conf, dict) and "class_path" in lg_conf: - log.info(f"Instantiating logger <{lg_conf['class_path']}>") - logger.append(instantiate_class(args=(), init=lg_conf)) - - return logger - - -def partial_instantiate_class(init: list | dict | None) -> list[partial] | None: - """Partially instantiates a class with the given initialization arguments. - - Copy from lightning.pytorch.cli.instantiate_class and modify it to use partial. - - Args: - init (list | dict | None): A dictionary containing the initialization arguments. - It should have the following each keys: - - "init_args" (dict): A dictionary of keyword arguments to be passed to the class constructor. - - "class_path" (str): The fully qualified path of the class to be instantiated. - - Returns: - list[partial] | None: A partial object representing the partially instantiated class. - """ - if not init: - return None - if not isinstance(init, list): - init = [init] - items: list[partial] = [] - for item in init: - kwargs = item.get("init_args", {}) - class_module, class_name = item["class_path"].rsplit(".", 1) - module = __import__(class_module, fromlist=[class_name]) - args_class = getattr(module, class_name) - items.append(partial(args_class, **kwargs)) - return items - - -def instantiate_sampler(sampler_config: SamplerConfig, dataset: Dataset, **kwargs) -> Sampler: - """Instantiate a sampler object based on the provided configuration. - - Args: - sampler_config (SamplerConfig): The configuration object for the sampler. - dataset (Dataset): The dataset object to be sampled. - **kwargs: Additional keyword arguments to be passed to the sampler's constructor. - - Returns: - Sampler: The instantiated sampler object. - """ - class_module, class_name = sampler_config.class_path.rsplit(".", 1) - module = __import__(class_module, fromlist=[class_name]) - sampler_class = getattr(module, class_name) - init_signature = list(inspect.signature(sampler_class.__init__).parameters.keys()) - if "batch_size" not in init_signature: - kwargs.pop("batch_size", None) - sampler_kwargs = {**sampler_config.init_args, **kwargs} - - return sampler_class(dataset, **sampler_kwargs) diff --git a/src/otx/core/utils/mask_util.py b/src/otx/core/utils/mask_util.py deleted file mode 100644 index bd5b70da1b5..00000000000 --- a/src/otx/core/utils/mask_util.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility functions for mask operations.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pycocotools.mask as mask_utils -import torch - -if TYPE_CHECKING: - import numpy as np - from datumaro import Polygon - - -def polygon_to_bitmap( - polygons: list[Polygon], - height: int, - width: int, -) -> np.ndarray: - """Convert a list of polygons to a bitmap mask. - - Args: - polygons (list[Polygon]): List of Datumaro Polygon objects. - height (int): bitmap height - width (int): bitmap width - - Returns: - np.ndarray: bitmap masks - """ - polygons = [polygon.points for polygon in polygons] - rles = mask_utils.frPyObjects(polygons, height, width) - return mask_utils.decode(rles).astype(bool).transpose((2, 0, 1)) - - -def polygon_to_rle( - polygons: list[Polygon], - height: int, - width: int, -) -> list[dict]: - """Convert a list of polygons to a list of RLE masks. - - Args: - polygons (list[Polygon]): List of Datumaro Polygon objects. - height (int): bitmap height - width (int): bitmap width - - Returns: - list[dict]: List of RLE masks. - """ - polygons = [polygon.points for polygon in polygons] - return mask_utils.frPyObjects(polygons, height, width) - - -def encode_rle(mask: torch.Tensor) -> dict: - """Encodes a mask into RLE format. - - Rewrite of https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocotools/mask.py - - Example: - Given M=[0 0 1 1 1 0 1] the RLE counts is [2 3 1 1]. - Or for M=[1 1 1 1 1 1 0] the RLE counts is [0 6 1]. - - Args: - mask (torch.Tensor): A binary mask (0 or 1) of shape (H, W). - - Returns: - dict: A dictionary with keys "counts" and "size". - """ - device = mask.device - vector = mask.t().ravel() - diffs = torch.diff(vector) - next_diffs = torch.where(diffs != 0)[0] + 1 - - counts = torch.diff( - torch.cat( - ( - torch.tensor([0], device=device), - next_diffs, - torch.tensor([len(vector)], device=device), - ), - ), - ) - - # odd counts are always the numbers of zeros - if vector[0] == 1: - counts = torch.cat((torch.tensor([0], device=device), counts)) - - return {"counts": counts.tolist(), "size": list(mask.shape)} diff --git a/src/otx/core/utils/pylogger.py b/src/otx/core/utils/pylogger.py deleted file mode 100644 index ace8b3bda89..00000000000 --- a/src/otx/core/utils/pylogger.py +++ /dev/null @@ -1,48 +0,0 @@ -# MIT License - -# Copyright (c) 2023 Intel Corporation -# Copyright (c) 2021 ashleve - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# -# This source code is borrowed from https://github.com/ashleve/lightning-hydra-template -# -"""Pylogger for OTX logger.""" - -import logging - -from lightning.pytorch.utilities import rank_zero_only - - -def get_pylogger(name: str = __name__) -> logging.Logger: - """Initializes a multi-GPU-friendly python command line logger. - - :param name: The name of the logger, defaults to ``__name__``. - - :return: A logger object. - """ - logger = logging.getLogger(name) - - # this ensures all logging levels get marked with the rank zero decorator - # otherwise logs would get multiplied for each GPU process in multi-GPU setup - logging_levels = ("debug", "info", "warning", "error", "exception", "fatal", "critical") - for level in logging_levels: - setattr(logger, level, rank_zero_only(getattr(logger, level))) - - return logger diff --git a/src/otx/core/utils/tile_merge.py b/src/otx/core/utils/tile_merge.py deleted file mode 100644 index 97f19660981..00000000000 --- a/src/otx/core/utils/tile_merge.py +++ /dev/null @@ -1,294 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""OTX tile merge module.""" - -from __future__ import annotations - -from abc import abstractmethod -from collections import defaultdict -from typing import Generic - -import torch -from torchvision import tv_tensors -from torchvision.ops import batched_nms - -from otx.core.data.entity.base import ImageInfo, T_OTXBatchPredEntity, T_OTXDataEntity -from otx.core.data.entity.detection import DetBatchPredEntity, DetBatchPredEntityWithXAI, DetPredEntity -from otx.core.data.entity.instance_segmentation import ( - InstanceSegBatchPredEntity, - InstanceSegBatchPredEntityWithXAI, - InstanceSegPredEntity, -) - - -class TileMerge(Generic[T_OTXDataEntity, T_OTXBatchPredEntity]): - """Base class for tile merge. - - Args: - img_infos (list[ImageInfo]): Original image information before tiling. - iou_threshold (float, optional): IoU threshold for non-maximum suppression. Defaults to 0.45. - max_num_instances (int, optional): Maximum number of instances to keep. Defaults to 500. - - """ - - def __init__( - self, - img_infos: list[ImageInfo], - iou_threshold: float = 0.45, - max_num_instances: int = 500, - ) -> None: - self.img_infos = img_infos - self.iou_threshold = iou_threshold - self.max_num_instances = max_num_instances - - @abstractmethod - def _merge_entities(self, img_info: ImageInfo, entities: list[T_OTXDataEntity]) -> T_OTXDataEntity: - """Merge tile predictions to one single full-size prediction data entity. - - Args: - img_info (ImageInfo): Image information about the original image before tiling. - entities (list[T_OTXDataEntity]): List of tile prediction entities. - - Returns: - T_OTXDataEntity: Merged prediction entity. - """ - raise NotImplementedError - - @abstractmethod - def merge( - self, - batch_tile_preds: list[T_OTXBatchPredEntity], - batch_tile_attrs: list[list[dict]], - ) -> list[T_OTXDataEntity]: - """Merge batch tile predictions to a list of full-size prediction data entities. - - Args: - batch_tile_preds (list): list of tile predictions. - batch_tile_attrs (list): list of tile attributes. - """ - raise NotImplementedError - - def nms_postprocess( - self, - bboxes: torch.Tensor, - scores: torch.Tensor, - labels: torch.Tensor, - masks: None | list[torch.Tensor] = None, - ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, None | torch.Tensor]: - """Non-maximum suppression and post-process.""" - keep = batched_nms(bboxes, scores, labels, self.iou_threshold) - if len(keep) > self.max_num_instances: - keep = keep[: self.max_num_instances] - bboxes = bboxes[keep] - labels = labels[keep] - scores = scores[keep] - if masks is not None and len(masks) > 0: - # coalesce sparse tensors to prevent them from growing too large. - masks = torch.stack([masks[idx] for idx in keep]).coalesce().to_dense() - return bboxes, labels, scores, masks - - -class DetectionTileMerge(TileMerge): - """Detection tile merge.""" - - def merge( - self, - batch_tile_preds: list[DetBatchPredEntity | DetBatchPredEntityWithXAI], - batch_tile_attrs: list[list[dict]], - ) -> list[DetPredEntity]: - """Merge batch tile predictions to a list of full-size prediction data entities. - - Args: - batch_tile_preds (list): detection tile predictions. - batch_tile_attrs (list): detection tile attributes. - - """ - entities_to_merge = defaultdict(list) - img_ids = [] - - for tile_preds, tile_attrs in zip(batch_tile_preds, batch_tile_attrs): - for tile_attr, tile_img_info, tile_bboxes, tile_labels, tile_scores in zip( - tile_attrs, - tile_preds.imgs_info, - tile_preds.bboxes, - tile_preds.labels, - tile_preds.scores, - ): - offset_x, offset_y, _, _ = tile_attr["roi"] - tile_bboxes[:, 0::2] += offset_x - tile_bboxes[:, 1::2] += offset_y - - tile_id = tile_attr["tile_id"] - if tile_id not in img_ids: - img_ids.append(tile_id) - tile_img_info.padding = tile_attr["roi"] - - entities_to_merge[tile_id].append( - DetPredEntity( - image=torch.empty(tile_img_info.ori_shape), - img_info=tile_img_info, - bboxes=tile_bboxes, - labels=tile_labels, - score=tile_scores, - ), - ) - return [ - self._merge_entities(image_info, entities_to_merge[img_id]) - for img_id, image_info in zip(img_ids, self.img_infos) - ] - - def _merge_entities(self, img_info: ImageInfo, entities: list[DetPredEntity]) -> DetPredEntity: - """Merge tile predictions to one single prediction. - - Args: - img_info (ImageInfo): Image information about the original image before tiling. - entities (list[DetPredEntity]): List of tile prediction entities. - - Returns: - DetPredEntity: Merged prediction entity. - """ - bboxes: list | torch.Tensor = [] - labels: list | torch.Tensor = [] - scores: list | torch.Tensor = [] - img_size = img_info.ori_shape - for tile_entity in entities: - num_preds = len(tile_entity.bboxes) - if num_preds > 0: - bboxes.extend(tile_entity.bboxes) - labels.extend(tile_entity.labels) - scores.extend(tile_entity.score) - - bboxes = torch.stack(bboxes) if len(bboxes) > 0 else torch.empty((0, 4), device=img_info.device) - labels = torch.stack(labels) if len(labels) > 0 else torch.empty((0,), device=img_info.device) - scores = torch.stack(scores) if len(scores) > 0 else torch.empty((0,), device=img_info.device) - - bboxes, labels, scores, _ = self.nms_postprocess( - bboxes, - scores, - labels, - ) - - return DetPredEntity( - image=torch.empty(img_size), - img_info=img_info, - score=scores, - bboxes=tv_tensors.BoundingBoxes( - bboxes, - canvas_size=img_size, - format="XYXY", - ), - labels=labels, - ) - - -class InstanceSegTileMerge(TileMerge): - """Instance segmentation tile merge.""" - - def merge( - self, - batch_tile_preds: list[InstanceSegBatchPredEntity | InstanceSegBatchPredEntityWithXAI], - batch_tile_attrs: list[list[dict]], - ) -> list[InstanceSegPredEntity]: - """Merge inst-seg tile predictions to one single prediction. - - Args: - batch_tile_preds (list): instance-seg tile predictions. - batch_tile_attrs (list): instance-seg tile attributes. - - """ - entities_to_merge = defaultdict(list) - img_ids = [] - - for tile_preds, tile_attrs in zip(batch_tile_preds, batch_tile_attrs): - for tile_attr, tile_img_info, tile_bboxes, tile_labels, tile_scores, tile_masks in zip( - tile_attrs, - tile_preds.imgs_info, - tile_preds.bboxes, - tile_preds.labels, - tile_preds.scores, - tile_preds.masks, - ): - keep_indices = tile_masks.to_sparse().sum((1, 2)).to_dense() > 0 - keep_indices = keep_indices.nonzero(as_tuple=True)[0] - _bboxes = tile_bboxes[keep_indices] - _labels = tile_labels[keep_indices] - _scores = tile_scores[keep_indices] - _masks = tile_masks[keep_indices] - - offset_x, offset_y, _, _ = tile_attr["roi"] - _bboxes[:, 0::2] += offset_x - _bboxes[:, 1::2] += offset_y - - tile_id = tile_attr["tile_id"] - if tile_id not in img_ids: - img_ids.append(tile_id) - tile_img_info.padding = tile_attr["roi"] - - entities_to_merge[tile_id].append( - InstanceSegPredEntity( - image=torch.empty(tile_img_info.ori_shape), - img_info=tile_img_info, - bboxes=_bboxes, - labels=_labels, - score=_scores, - masks=_masks.to_sparse(), - polygons=[], - ), - ) - - return [ - self._merge_entities(image_info, entities_to_merge[img_id]) - for img_id, image_info in zip(img_ids, self.img_infos) - ] - - def _merge_entities(self, img_info: ImageInfo, entities: list[InstanceSegPredEntity]) -> InstanceSegPredEntity: - """Merge tile predictions to one single prediction. - - Args: - img_info (ImageInfo): Image information about the original image before tiling. - entities (list[InstanceSegPredEntity]): List of tile prediction entities. - - Returns: - InstanceSegPredEntity: Merged prediction entity. - """ - bboxes: list | torch.Tensor = [] - labels: list | torch.Tensor = [] - scores: list | torch.Tensor = [] - masks: list | torch.Tensor = [] - img_size = img_info.ori_shape - for tile_entity in entities: - num_preds = len(tile_entity.bboxes) - if num_preds > 0: - bboxes.extend(tile_entity.bboxes) - labels.extend(tile_entity.labels) - scores.extend(tile_entity.score) - - offset_x, offset_y, _, _ = tile_entity.img_info.padding - mask_indices = tile_entity.masks.indices() - mask_values = tile_entity.masks.values() - mask_indices[1] += offset_y - mask_indices[2] += offset_x - masks.extend( - torch.sparse_coo_tensor(mask_indices, mask_values, (num_preds, *img_size)), - ) - - bboxes = torch.stack(bboxes) if len(bboxes) > 0 else torch.empty((0, 4), device=img_info.device) - labels = torch.stack(labels) if len(labels) > 0 else torch.empty((0,), device=img_info.device) - scores = torch.stack(scores) if len(scores) > 0 else torch.empty((0,), device=img_info.device) - masks = masks if len(masks) > 0 else torch.empty((0, *img_size)) - - bboxes, labels, scores, masks = self.nms_postprocess(bboxes, scores, labels, masks) - return InstanceSegPredEntity( - image=torch.empty(img_size), - img_info=img_info, - score=scores, - bboxes=tv_tensors.BoundingBoxes( - bboxes, - canvas_size=img_size, - format="XYXY", - ), - labels=labels, - masks=tv_tensors.Mask(masks, dtype=bool), - polygons=[], - ) diff --git a/src/otx/core/utils/utils.py b/src/otx/core/utils/utils.py deleted file mode 100644 index 006279e5cbb..00000000000 --- a/src/otx/core/utils/utils.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility functions.""" - -from __future__ import annotations - -from collections import defaultdict -from multiprocessing import cpu_count -from typing import TYPE_CHECKING, Any - -import torch -from datumaro.components.annotation import AnnotationType, LabelCategories - -if TYPE_CHECKING: - from datumaro import Dataset as DmDataset - from omegaconf import DictConfig - - -def is_ckpt_from_otx_v1(ckpt: dict) -> bool: - """Check the checkpoint where it comes from. - - Args: - ckpt (dict): the checkpoint file - - Returns: - bool: True means the checkpoint comes from otx1 - """ - return "model" in ckpt and ckpt["VERSION"] == 1 - - -def is_ckpt_for_finetuning(ckpt: dict) -> bool: - """Check the checkpoint will be used to finetune. - - Args: - ckpt (dict): the checkpoint file - - Returns: - bool: True means the checkpoint will be used to finetune. - """ - return "state_dict" in ckpt - - -def get_mean_std_from_data_processing(config: DictConfig) -> dict[str, Any]: - """Get mean and std value from data_processing. - - Args: - config (DictConfig): MM framework model config. - - Returns: - dict[str, Any]: Dictionary with mean and std value. - """ - return { - "mean": config["data_preprocessor"]["mean"], - "std": config["data_preprocessor"]["std"], - } - - -def get_adaptive_num_workers(num_dataloader: int = 1) -> int | None: - """Measure appropriate num_workers value and return it.""" - num_gpus = torch.cuda.device_count() - if num_gpus == 0: - return None - return min(cpu_count() // (num_dataloader * num_gpus), 8) # max available num_workers is 8 - - -def get_idx_list_per_classes(dm_dataset: DmDataset, use_string_label: bool = False) -> dict[int | str, list[int]]: - """Compute class statistics.""" - stats: dict[int | str, list[int]] = defaultdict(list) - labels = dm_dataset.categories().get(AnnotationType.label, LabelCategories()) - for item_idx, item in enumerate(dm_dataset): - for ann in item.annotations: - if use_string_label: - stats[labels.items[ann.label].name].append(item_idx) - else: - stats[ann.label].append(item_idx) - return stats diff --git a/src/otx/data/__init__.py b/src/otx/data/__init__.py deleted file mode 100644 index 3e47216f290..00000000000 --- a/src/otx/data/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""OTX Datamodules.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from .anomaly import AnomalyDataModule - -__all__ = ["AnomalyDataModule"] diff --git a/src/otx/data/anomaly/__init__.py b/src/otx/data/anomaly/__init__.py deleted file mode 100644 index ee3857a9464..00000000000 --- a/src/otx/data/anomaly/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""OTX Anomaly Datamodules.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - - -from .anomaly import AnomalyDataModule - -__all__ = ["AnomalyDataModule"] diff --git a/src/otx/data/anomaly/anomaly.py b/src/otx/data/anomaly/anomaly.py deleted file mode 100644 index c9b94a4ef99..00000000000 --- a/src/otx/data/anomaly/anomaly.py +++ /dev/null @@ -1,86 +0,0 @@ -"""OTX Anomaly Datamodules.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from typing import Any - -from otx.core.config.data import DataModuleConfig, SubsetConfig, TileConfig -from otx.core.data import OTXDataModule -from otx.core.types.task import OTXTaskType -from otx.core.types.transformer_libs import TransformLibType - - -class AnomalyDataModule(OTXDataModule): - """Anomaly DataModule.""" - - def __init__( - self, - task_type: OTXTaskType, - data_dir: str, - data_format: str = "mvtec", - # Train args. - train_batch_size: int = 32, - train_num_workers: int = 8, - train_transforms: list[dict[str, Any]] | None = None, - train_transform_lib_type: TransformLibType = TransformLibType.TORCHVISION, - # Validation args. - val_batch_size: int = 32, - val_num_workers: int = 8, - val_transforms: list[dict[str, Any]] | None = None, - val_transform_lib_type: TransformLibType = TransformLibType.TORCHVISION, - # Test args. - test_batch_size: int = 32, - test_num_workers: int = 8, - test_transforms: list[dict[str, Any]] | None = None, - test_transform_lib_type: TransformLibType = TransformLibType.TORCHVISION, - # Tiler args. - enable_tiler: bool = False, - tile_size: tuple[int, int] = (400, 400), - overlap: float = 0.0, - ) -> None: - # Create the train subset. - train_subset_config = SubsetConfig( - batch_size=train_batch_size, - subset_name="train", - transforms=train_transforms, # type: ignore[arg-type] - transform_lib_type=train_transform_lib_type, - num_workers=train_num_workers, - ) - # Create the validation subset. - val_subset_config = SubsetConfig( - batch_size=val_batch_size, - subset_name="test", # use test as validation - transforms=val_transforms, # type: ignore[arg-type] - transform_lib_type=val_transform_lib_type, - num_workers=val_num_workers, - ) - - # Create the test subset. - test_subset_config = SubsetConfig( - batch_size=test_batch_size, - subset_name="test", - transforms=test_transforms, # type: ignore[arg-type] - transform_lib_type=test_transform_lib_type, - num_workers=test_num_workers, - ) - - # Create the tiler config. - tiler_config = TileConfig( - enable_tiler=enable_tiler, - tile_size=tile_size, - overlap=overlap, - ) - - # Create the datamodule config. - datamodule_config = DataModuleConfig( - data_format=data_format, - data_root=data_dir, - train_subset=train_subset_config, - val_subset=val_subset_config, - test_subset=test_subset_config, - tile_config=tiler_config, - ) - super().__init__(task=task_type, config=datamodule_config) diff --git a/src/otx/engine/__init__.py b/src/otx/engine/__init__.py deleted file mode 100644 index 86f7406c917..00000000000 --- a/src/otx/engine/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""API for OTX Entry-Point User.""" - -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from .engine import Engine - -__all__ = ["Engine"] diff --git a/src/otx/engine/engine.py b/src/otx/engine/engine.py deleted file mode 100644 index 48daf0b760b..00000000000 --- a/src/otx/engine/engine.py +++ /dev/null @@ -1,841 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Module for OTX engine components.""" - -from __future__ import annotations - -import inspect -from pathlib import Path -from typing import TYPE_CHECKING, Any, Iterable, Literal -from warnings import warn - -import torch -from lightning import Trainer, seed_everything - -from otx.core.config.device import DeviceConfig -from otx.core.config.explain import ExplainConfig -from otx.core.config.hpo import HpoConfig -from otx.core.data.module import OTXDataModule -from otx.core.model.entity.base import OTXModel, OVModel -from otx.core.model.module.base import OTXLitModule -from otx.core.types import PathLike -from otx.core.types.device import DeviceType -from otx.core.types.export import OTXExportFormatType -from otx.core.types.precision import OTXPrecisionType -from otx.core.types.task import OTXTaskType -from otx.core.utils.cache import TrainerArgumentsCache - -from .hpo import execute_hpo, update_hyper_parameter -from .utils.auto_configurator import AutoConfigurator - -if TYPE_CHECKING: - from lightning import Callback - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - from lightning.pytorch.loggers import Logger - from lightning.pytorch.utilities.types import EVAL_DATALOADERS, TRAIN_DATALOADERS - from pytorch_lightning.trainer.connectors.accelerator_connector import _PRECISION_INPUT - from torchmetrics import Metric - - from otx.core.metrics import MetricCallable - - -LITMODULE_PER_TASK = { - OTXTaskType.MULTI_CLASS_CLS: "otx.core.model.module.classification.OTXMulticlassClsLitModule", - OTXTaskType.MULTI_LABEL_CLS: "otx.core.model.module.classification.OTXMultilabelClsLitModule", - OTXTaskType.H_LABEL_CLS: "otx.core.model.module.classification.OTXHlabelClsLitModule", - OTXTaskType.DETECTION: "otx.core.model.module.detection.OTXDetectionLitModule", - OTXTaskType.ROTATED_DETECTION: "otx.core.model.module.rotated_detection.OTXRotatedDetLitModule", - OTXTaskType.INSTANCE_SEGMENTATION: "otx.core.model.module.instance_segmentation.OTXInstanceSegLitModule", - OTXTaskType.SEMANTIC_SEGMENTATION: "otx.core.model.module.segmentation.OTXSegmentationLitModule", - OTXTaskType.ACTION_CLASSIFICATION: "otx.core.model.module.action_classification.OTXActionClsLitModule", - OTXTaskType.ACTION_DETECTION: "otx.core.model.module.action_detection.OTXActionDetLitModule", - OTXTaskType.VISUAL_PROMPTING: "otx.core.model.module.visual_prompting.OTXVisualPromptingLitModule", - OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING: "otx.core.model.module.visual_prompting.OTXZeroShotVisualPromptingLitModule", # noqa: E501 -} - - -class Engine: - """OTX Engine. - - This class defines the Engine for OTX, which governs each step of the OTX workflow. - - Example: - The following examples show how to use the Engine class. - - Auto-Configuration with data_root - >>> engine = Engine( - ... data_root=, - ... ) - - Create Engine with Custom OTXModel - >>> engine = Engine( - ... data_root=, - ... model=OTXModel(...), - ... checkpoint=, - ... ) - - Create Engine with Custom OTXDataModule - >>> engine = Engine( - ... model = OTXModel(...), - ... datamodule = OTXDataModule(...), - ... ) - """ - - def __init__( - self, - *, - data_root: PathLike | None = None, - task: OTXTaskType | None = None, - work_dir: PathLike = "./otx-workspace", - datamodule: OTXDataModule | None = None, - model: OTXModel | str | None = None, - optimizer: list[OptimizerCallable] | OptimizerCallable | None = None, - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable | None = None, - checkpoint: PathLike | None = None, - device: DeviceType = DeviceType.auto, - **kwargs, - ): - """Initializes the OTX Engine. - - Args: - data_root (PathLike | None, optional): Root directory for the data. Defaults to None. - task (OTXTaskType | None, optional): The type of OTX task. Defaults to None. - work_dir (PathLike, optional): Working directory for the engine. Defaults to "./otx-workspace". - datamodule (OTXDataModule | None, optional): The data module for the engine. Defaults to None. - model (OTXModel | str | None, optional): The model for the engine. Defaults to None. - optimizer (list[OptimizerCallable] | OptimizerCallable | None, optional): The optimizer for the engine. - Defaults to None. - scheduler (list[LRSchedulerCallable] | LRSchedulerCallable | None, optional): - The learning rate scheduler for the engine. Defaults to None. - checkpoint (PathLike | None, optional): Path to the checkpoint file. Defaults to None. - device (DeviceType, optional): The device type to use. Defaults to DeviceType.auto. - **kwargs: Additional keyword arguments for pl.Trainer. - """ - self._cache = TrainerArgumentsCache(**kwargs) - self.checkpoint = checkpoint - self.work_dir = work_dir - self.device = device # type: ignore[assignment] - self._auto_configurator = AutoConfigurator( - data_root=data_root, - task=datamodule.task if datamodule is not None else task, - model_name=None if isinstance(model, OTXModel) else model, - ) - - self._datamodule: OTXDataModule | None = ( - datamodule if datamodule is not None else self._auto_configurator.get_datamodule() - ) - self.task = task if task is not None else self._auto_configurator.task - - self._trainer: Trainer | None = None - self._model: OTXModel = ( - model - if isinstance(model, OTXModel) - else self._auto_configurator.get_model( - label_info=self._datamodule.label_info if self._datamodule is not None else None, - ) - ) - self.optimizer: list[OptimizerCallable] | OptimizerCallable | None = ( - optimizer if optimizer is not None else self._auto_configurator.get_optimizer() - ) - self.scheduler: list[LRSchedulerCallable] | LRSchedulerCallable | None = ( - scheduler if scheduler is not None else self._auto_configurator.get_scheduler() - ) - - _EXPORTED_MODEL_BASE_NAME = "exported_model" - - # ------------------------------------------------------------------------ # - # General OTX Entry Points - # ------------------------------------------------------------------------ # - - def train( - self, - max_epochs: int = 10, - seed: int | None = None, - deterministic: bool | Literal["warn"] = False, - precision: _PRECISION_INPUT | None = "32", - val_check_interval: int | float | None = None, - callbacks: list[Callback] | Callback | None = None, - logger: Logger | Iterable[Logger] | bool | None = None, - resume: bool = False, - metric: Metric | MetricCallable | None = None, - run_hpo: bool = False, - hpo_config: HpoConfig | None = None, - **kwargs, - ) -> dict[str, Any]: - """Trains the model using the provided LightningModule and OTXDataModule. - - Args: - max_epochs (int | None, optional): The maximum number of epochs. Defaults to None. - seed (int | None, optional): The random seed. Defaults to None. - deterministic (bool | Literal["warn"]): Whether to enable deterministic behavior. - Also, can be set to `warn` to avoid failures, because some operations don't - support deterministic mode. Defaults to False. - precision (_PRECISION_INPUT | None, optional): The precision of the model. Defaults to 32. - val_check_interval (int | float | None, optional): The validation check interval. Defaults to None. - callbacks (list[Callback] | Callback | None, optional): The callbacks to be used during training. - logger (Logger | Iterable[Logger] | bool | None, optional): The logger(s) to be used. Defaults to None. - resume (bool, optional): If True, tries to resume training from existing checkpoint. - metric (Metric | MetricCallable | None): The metric for the validation and test. - It could be None at export, predict, etc. - run_hpo (bool, optional): If True, optimizer hyper parameters before training a model. - hpo_config (HpoConfig | None, optional): Configuration for HPO. - **kwargs: Additional keyword arguments for pl.Trainer configuration. - - Returns: - dict[str, Any]: A dictionary containing the callback metrics from the trainer. - - Example: - >>> engine.train( - ... max_epochs=3, - ... seed=1234, - ... deterministic=False, - ... precision="32", - ... ) - - CLI Usage: - 1. you can train with data_root only. then OTX will provide default model. - ```python - otx train --data_root - ``` - 2. you can pick a model or datamodule as Config file or Class. - ```python - otx train - --data_root - --model --data - ``` - 3. Of course, you can override the various values with commands. - ```python - otx train - --data_root - --max_epochs --checkpoint - ``` - 4. If you have a complete configuration file, run it like this. - ```python - otx train --data_root --config - ``` - """ - metric = metric if metric is not None else self._auto_configurator.get_metric() - if run_hpo: - if hpo_config is None: - hpo_config = HpoConfig() - best_config, best_trial_weight = execute_hpo(engine=self, **locals()) - if best_config is not None: - update_hyper_parameter(self, best_config) - if best_trial_weight is not None: - self.checkpoint = best_trial_weight - resume = True - - lit_module = self._build_lightning_module( - model=self.model, - optimizer=self.optimizer, - scheduler=self.scheduler, - metric=metric, - ) - lit_module.label_info = self.datamodule.label_info - - if seed is not None: - seed_everything(seed, workers=True) - - self._build_trainer( - logger=logger, - callbacks=callbacks, - precision=precision, - max_epochs=max_epochs, - deterministic=deterministic, - val_check_interval=val_check_interval, - **kwargs, - ) - fit_kwargs: dict[str, Any] = {} - if resume: - fit_kwargs["ckpt_path"] = self.checkpoint - elif self.checkpoint is not None: - loaded_checkpoint = torch.load(self.checkpoint) - # loaded checkpoint have keys (OTX1.5): model, config, labels, input_size, VERSION - lit_module.load_state_dict(loaded_checkpoint) - - self.trainer.fit( - model=lit_module, - datamodule=self.datamodule, - **fit_kwargs, - ) - self.checkpoint = self.trainer.checkpoint_callback.best_model_path - return self.trainer.callback_metrics - - def test( - self, - checkpoint: PathLike | None = None, - datamodule: EVAL_DATALOADERS | OTXDataModule | None = None, - metric: Metric | MetricCallable | None = None, - **kwargs, - ) -> dict: - """Run the testing phase of the engine. - - Args: - datamodule (EVAL_DATALOADERS | OTXDataModule | None, optional): The data module containing the test data. - checkpoint (PathLike | None, optional): Path to the checkpoint file to load the model from. - Defaults to None. - metric (Metric | MetricCallable | None): The metric for the validation and test. - It could be None at export, predict, etc. - **kwargs: Additional keyword arguments for pl.Trainer configuration. - - Returns: - dict: Dictionary containing the callback metrics from the trainer. - - Example: - >>> engine.test( - ... datamodule=OTXDataModule(), - ... checkpoint=, - ... ) - - CLI Usage: - 1. you can pick a model. - ```python - otx test - --model --data_root - --checkpoint - ``` - 2. If you have a ready configuration file, run it like this. - ```python - otx test --config --checkpoint - ``` - """ - model = self.model - checkpoint = checkpoint if checkpoint is not None else self.checkpoint - datamodule = datamodule if datamodule is not None else self.datamodule - - is_ir_ckpt = Path(str(checkpoint)).suffix in [".xml", ".onnx"] - if is_ir_ckpt and not isinstance(model, OVModel): - datamodule = self._auto_configurator.get_ov_datamodule() - model = self._auto_configurator.get_ov_model(model_name=str(checkpoint), label_info=datamodule.label_info) - - metric = metric if metric is not None else self._auto_configurator.get_metric() - lit_module = self._build_lightning_module( - model=model, - optimizer=self.optimizer, - scheduler=self.scheduler, - metric=metric, - ) - lit_module.label_info = datamodule.label_info - - # NOTE, trainer.test takes only lightning based checkpoint. - # So, it can't take the OTX1.x checkpoint. - if checkpoint is not None and not is_ir_ckpt: - loaded_checkpoint = torch.load(checkpoint) - lit_module.load_state_dict(loaded_checkpoint) - - self._build_trainer(**kwargs) - - self.trainer.test( - model=lit_module, - dataloaders=datamodule, - ) - - return self.trainer.callback_metrics - - def predict( - self, - checkpoint: PathLike | None = None, - datamodule: EVAL_DATALOADERS | OTXDataModule | None = None, - return_predictions: bool | None = None, - explain: bool = False, - explain_config: ExplainConfig | None = None, - **kwargs, - ) -> list | None: - """Run predictions using the specified model and data. - - Args: - datamodule (EVAL_DATALOADERS | OTXDataModule | None, optional): The data module to use for predictions. - checkpoint (PathLike | None, optional): The path to the checkpoint file to load the model from. - return_predictions (bool | None, optional): Whether to return the predictions or not. - explain (bool): Whether to dump "saliency_map" and "feature_vector" or not. - explain_config (ExplainConfig): Explain configuration (used for saliency map post-processing). - **kwargs: Additional keyword arguments for pl.Trainer configuration. - - Returns: - list | None: The predictions if `return_predictions` is True, otherwise None. - - Example: - >>> engine.predict( - ... datamodule=OTXDataModule(), - ... checkpoint=, - ... return_predictions=True, - ... explain=True, - ... ) - - CLI Usage: - 1. you can pick a model. - ```python - otx predict - --config --data_root - --checkpoint - ``` - 2. If you have a ready configuration file, run it like this. - ```python - otx predict --config --checkpoint - ``` - """ - from otx.algo.utils.xai_utils import process_saliency_maps_in_pred_entity - - model = self.model - - checkpoint = checkpoint if checkpoint is not None else self.checkpoint - datamodule = datamodule if datamodule is not None else self.datamodule - - is_ir_ckpt = checkpoint is not None and Path(checkpoint).suffix in [".xml", ".onnx"] - if is_ir_ckpt and not isinstance(model, OVModel): - datamodule = self._auto_configurator.get_ov_datamodule() - model = self._auto_configurator.get_ov_model(model_name=str(checkpoint), label_info=datamodule.label_info) - - lit_module = self._build_lightning_module( - model=model, - optimizer=self.optimizer, - scheduler=self.scheduler, - ) - lit_module.label_info = datamodule.label_info - - if checkpoint is not None and not is_ir_ckpt: - loaded_checkpoint = torch.load(checkpoint) - lit_module.load_state_dict(loaded_checkpoint) - - lit_module.model.explain_mode = explain - - self._build_trainer(**kwargs) - - predict_result = self.trainer.predict( - model=lit_module, - dataloaders=datamodule, - return_predictions=return_predictions, - ) - - if explain: - if explain_config is None: - explain_config = ExplainConfig() - - predict_result = process_saliency_maps_in_pred_entity(predict_result, explain_config) - - lit_module.model.explain_mode = False - return predict_result - - def export( - self, - checkpoint: str | Path | None = None, - export_format: OTXExportFormatType = OTXExportFormatType.OPENVINO, - export_precision: OTXPrecisionType = OTXPrecisionType.FP32, - explain: bool = False, - ) -> Path: - """Export the trained model to OpenVINO Intermediate Representation (IR) or ONNX formats. - - Args: - checkpoint (str | Path | None, optional): Checkpoint to export. Defaults to None. - export_config (ExportConfig | None, optional): Config that allows to set export - format and precision. Defaults to None. - explain (bool): Whether to get "saliency_map" and "feature_vector" or not. - - Returns: - Path: Path to the exported model. - - Example: - >>> engine.export( - ... checkpoint=, - ... export_format=OTXExportFormatType.OPENVINO, - ... export_precision=OTXExportPrecisionType.FP32, - ... explain=True, - ... ) - - CLI Usage: - 1. To export a model with default setting (OPENVINO, FP32), run - ```python - otx export - --config --data_root - --checkpoint - ``` - 2. To export a model with precision FP16 and format ONNX, run - ```python - otx export - --config --data_root - --checkpoint --export_precision FP16 --export_format ONNX - ``` - """ - ckpt_path = str(checkpoint) if checkpoint is not None else self.checkpoint - - if ckpt_path is None: - msg = "To make export, checkpoint must be specified." - raise RuntimeError(msg) - - self.model.eval() - lit_module = self._build_lightning_module( - model=self.model, - optimizer=self.optimizer, - scheduler=self.scheduler, - ) - loaded_checkpoint = torch.load(ckpt_path) - lit_module.label_info = loaded_checkpoint["state_dict"]["label_info"] - self.model.label_info = lit_module.label_info - - lit_module.load_state_dict(loaded_checkpoint) - - self.model.explain_mode = explain - - exported_model_path = lit_module.export( - output_dir=Path(self.work_dir), - base_name=self._EXPORTED_MODEL_BASE_NAME, - export_format=export_format, - precision=export_precision, - ) - - self.model.explain_mode = False - return exported_model_path - - def optimize( - self, - checkpoint: PathLike | None = None, - datamodule: TRAIN_DATALOADERS | OTXDataModule | None = None, - max_data_subset_size: int | None = None, - ) -> Path: - """Applies NNCF.PTQ to the underlying models (now works only for OV models). - - PTQ performs int-8 quantization on the input model, so the resulting model - comes in mixed precision (some operations, however, remain in FP32). - - Args: - checkpoint (str | Path | None, optional): Checkpoint to optimize. Defaults to None. - datamodule (TRAIN_DATALOADERS | OTXDataModule | None, optional): The data module to use for optimization. - max_data_subset_size (int | None): The maximum size of the train subset from `datamodule` that would be - used for model optimization. If not set, NNCF.PTQ will select subset size according to it's - default settings. - - Returns: - Path: path to the optimized model. - - Example: - >>> engine.optimize( - ... checkpoint=, - ... datamodule=OTXDataModule(), - ... checkpoint=, - ... ) - CLI Usage: - To optimize a model, run - ```python - otx optimize - --checkpoint - --model --data_root - --model.model_name= - ``` - """ - checkpoint = checkpoint if checkpoint is not None else self.checkpoint - optimize_datamodule = datamodule if datamodule is not None else self.datamodule - - is_ir_ckpt = checkpoint is not None and Path(checkpoint).suffix in [".xml", ".onnx"] - if not is_ir_ckpt: - msg = "Engine.optimize() supports only OV IR or ONNX checkpoints" - raise RuntimeError(msg) - - model = self.model - if not isinstance(model, OVModel): - datamodule = self._auto_configurator.get_ov_datamodule() - model = self._auto_configurator.get_ov_model( - model_name=str(checkpoint), - label_info=optimize_datamodule.label_info, - ) - - ptq_config = {} - if max_data_subset_size is not None: - ptq_config["subset_size"] = max_data_subset_size - - return model.optimize( - Path(self.work_dir), - optimize_datamodule, - ptq_config, - ) - - def explain( - self, - checkpoint: PathLike | None = None, - datamodule: EVAL_DATALOADERS | OTXDataModule | None = None, - explain_config: ExplainConfig | None = None, - dump: bool | None = False, - **kwargs, - ) -> list | None: - """Run XAI using the specified model and data (test subset). - - Args: - checkpoint (PathLike | None, optional): The path to the checkpoint file to load the model from. - datamodule (EVAL_DATALOADERS | OTXDataModule | None, optional): The data module to use for predictions. - explain_config (ExplainConfig | None, optional): Config used to handle saliency maps. - dump (bool): Whether to dump "saliency_map" or not. - **kwargs: Additional keyword arguments for pl.Trainer configuration. - - Returns: - list: Saliency maps. - - Example: - >>> engine.explain( - ... datamodule=OTXDataModule(), - ... checkpoint=, - ... explain_config=ExplainConfig(), - ... dump=True, - ... ) - - CLI Usage: - 1. To run XAI using the specified model, run - ```python - otx explain - --config --data_root - --checkpoint - ``` - """ - from otx.algo.utils.xai_utils import dump_saliency_maps, process_saliency_maps_in_pred_entity - - model = self.model - - checkpoint = checkpoint if checkpoint is not None else self.checkpoint - datamodule = datamodule if datamodule is not None else self.datamodule - - is_ir_ckpt = checkpoint is not None and Path(checkpoint).suffix in [".xml", ".onnx"] - if is_ir_ckpt and not isinstance(model, OVModel): - datamodule = self._auto_configurator.get_ov_datamodule() - model = self._auto_configurator.get_ov_model(model_name=str(checkpoint), label_info=datamodule.label_info) - - lit_module = self._build_lightning_module( - model=model, - optimizer=self.optimizer, - scheduler=self.scheduler, - ) - lit_module.label_info = datamodule.label_info - - if checkpoint is not None and not is_ir_ckpt: - loaded_checkpoint = torch.load(checkpoint) - lit_module.load_state_dict(loaded_checkpoint) - - lit_module.model.explain_mode = True - - self._build_trainer(**kwargs) - - predict_result = self.trainer.predict( - model=lit_module, - datamodule=datamodule, - ) - - if explain_config is None: - explain_config = ExplainConfig() - - predict_result = process_saliency_maps_in_pred_entity(predict_result, explain_config) - if dump: - dump_saliency_maps( - predict_result, - explain_config, - datamodule, - output_dir=Path(self.work_dir), - ) - lit_module.model.explain_mode = False - return predict_result - - @classmethod - def from_config( - cls, - config_path: PathLike, - data_root: PathLike | None = None, - work_dir: PathLike | None = None, - **kwargs, - ) -> Engine: - """Builds the engine from a configuration file. - - Args: - config_path (PathLike): The configuration file path. - data_root (PathLike | None): Root directory for the data. - Defaults to None. If data_root is None, use the data_root from the configuration file. - work_dir (PathLike | None, optional): Working directory for the engine. - Defaults to None. If work_dir is None, use the work_dir from the configuration file. - kwargs: Arguments that can override the engine's arguments. - - Returns:s - Engine: An instance of the Engine class. - - Example: - >>> engine = Engine.from_config( - ... config="config.yaml", - ... ) - """ - from otx.cli.utils.jsonargparse import get_instantiated_classes - - # For the Engine argument, prepend 'engine.' for CLI parser - filter_kwargs = ["device", "checkpoint", "task"] - for key in filter_kwargs: - if key in kwargs: - kwargs[f"engine.{key}"] = kwargs.pop(key) - instantiated_config, train_kwargs = get_instantiated_classes( - config=config_path, - data_root=data_root, - work_dir=work_dir, - **kwargs, - ) - engine_kwargs = {**instantiated_config.get("engine", {}), **train_kwargs} - - # Remove any input that is not currently available in Engine and print a warning message. - set_valid_args = TrainerArgumentsCache.get_trainer_constructor_args().union( - set(inspect.signature(Engine.__init__).parameters.keys()), - ) - removed_args = [] - for engine_key in list(engine_kwargs.keys()): - if engine_key not in set_valid_args: - engine_kwargs.pop(engine_key) - removed_args.append(engine_key) - if removed_args: - msg = ( - f"Warning: {removed_args} -> not available in Engine constructor. " - "It will be ignored. Use what need in the right places." - ) - warn(msg, stacklevel=1) - - return cls( - work_dir=instantiated_config.get("work_dir", work_dir), - datamodule=instantiated_config.get("data"), - model=instantiated_config.get("model"), - optimizer=instantiated_config.get("optimizer"), - scheduler=instantiated_config.get("scheduler"), - **engine_kwargs, - ) - - # ------------------------------------------------------------------------ # - # Property and setter functions provided by Engine. - # ------------------------------------------------------------------------ # - - @property - def work_dir(self) -> PathLike: - """Work directory.""" - return self._work_dir - - @work_dir.setter - def work_dir(self, work_dir: PathLike) -> None: - self._work_dir = work_dir - self._cache.update(default_root_dir=work_dir) - - @property - def device(self) -> DeviceConfig: - """Device engine uses.""" - return self._device - - @device.setter - def device(self, device: DeviceType) -> None: - self._device = DeviceConfig(accelerator=device) - self._cache.update(accelerator=self._device.accelerator, devices=self._device.devices) - - @property - def trainer(self) -> Trainer: - """Returns the trainer object associated with the engine. - - To get this property, you should execute `Engine.train()` function first. - - Returns: - Trainer: The trainer object. - """ - if self._trainer is None: - msg = "Please run train() first" - raise RuntimeError(msg) - return self._trainer - - def _build_trainer(self, **kwargs) -> None: - """Instantiate the trainer based on the model parameters.""" - if self._cache.requires_update(**kwargs) or self._trainer is None: - self._cache.update(**kwargs) - kwargs = self._cache.args - self._trainer = Trainer(**kwargs) - self.work_dir = self._trainer.default_root_dir - - @property - def trainer_params(self) -> dict: - """Returns the parameters used for training the model. - - Returns: - dict: A dictionary containing the training parameters. - """ - return self._cache.args - - @property - def model(self) -> OTXModel: - """Returns the model object associated with the engine. - - Returns: - OTXModel: The OTXModel object. - """ - return self._model - - @model.setter - def model(self, model: OTXModel | str) -> None: - """Sets the model for the engine. - - Args: - model (OTXModel | str): The model to be set. - - Returns: - None - """ - if isinstance(model, str): - model = self._auto_configurator.get_model(model, label_info=self.datamodule.label_info) - self._model = model - - @property - def datamodule(self) -> OTXDataModule: - """Returns the datamodule object associated with the engine. - - Returns: - OTXDataModule: The OTXDataModule object. - """ - if self._datamodule is None: - msg = "Please include the `data_root` or `datamodule` when creating the Engine." - raise RuntimeError(msg) - return self._datamodule - - def _build_lightning_module( - self, - model: OTXModel, - optimizer: list[OptimizerCallable] | OptimizerCallable | None, - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable | None, - metric: Metric | MetricCallable | None = None, - ) -> OTXLitModule: - """Builds a LightningModule for engine workflow. - - Args: - model (OTXModel): The OTXModel instance. - optimizer (list[OptimizerCallable] | OptimizerCallable | None): The optimizer callable. - scheduler (list[LRSchedulerCallable] | LRSchedulerCallable | None): The learning rate scheduler callable. - metric (Metric | MetricCallable | None): The metric for the validation and test. - It could be None at export, predict, etc. - - Returns: - OTXLitModule | OTXModel: The built LightningModule instance. - """ - if self.task in ( - OTXTaskType.ANOMALY_CLASSIFICATION, - OTXTaskType.ANOMALY_DETECTION, - OTXTaskType.ANOMALY_SEGMENTATION, - ): - model = self._get_anomaly_model(model, optimizer, scheduler) - else: - class_module, class_name = LITMODULE_PER_TASK[self.task].rsplit(".", 1) - module = __import__(class_module, fromlist=[class_name]) - lightning_module = getattr(module, class_name) - lightning_kwargs = { - "otx_model": model, - "optimizer": optimizer, - "scheduler": scheduler, - "torch_compile": False, - } - if metric: - lightning_kwargs["metric"] = metric - - model = lightning_module(**lightning_kwargs) - return model - - def _get_anomaly_model( - self, - model: OTXModel, - optimizer: list[OptimizerCallable] | OptimizerCallable | None, - scheduler: list[LRSchedulerCallable] | LRSchedulerCallable | None, - ) -> OTXModel: - # [TODO](ashwinvaidya17): Need to revisit how task, optimizer, and scheduler are assigned to the model - model.task = self.task - model.optimizer = optimizer - model.scheduler = scheduler - return model diff --git a/src/otx/engine/hpo/__init__.py b/src/otx/engine/hpo/__init__.py deleted file mode 100644 index d82e8c52e64..00000000000 --- a/src/otx/engine/hpo/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Functions and Classes to run HPO in the engine.""" - -from .hpo_api import execute_hpo -from .hpo_trial import update_hyper_parameter - -__all__ = ["execute_hpo", "update_hyper_parameter"] diff --git a/src/otx/engine/hpo/hpo_api.py b/src/otx/engine/hpo/hpo_api.py deleted file mode 100644 index bcafb6039ae..00000000000 --- a/src/otx/engine/hpo/hpo_api.py +++ /dev/null @@ -1,259 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Components to run HPO.""" - -from __future__ import annotations - -import dataclasses -import logging -import time -from functools import partial -from pathlib import Path -from threading import Thread -from typing import TYPE_CHECKING, Any, Callable - -import torch -from lightning.pytorch.cli import OptimizerCallable - -from otx.core.config.hpo import HpoConfig -from otx.core.types.task import OTXTaskType -from otx.hpo import HyperBand, run_hpo_loop -from otx.utils.utils import get_decimal_point, get_using_dot_delimited_key, remove_matched_files - -from .hpo_trial import run_hpo_trial -from .utils import find_trial_file, get_best_hpo_weight, get_hpo_weight_dir - -if TYPE_CHECKING: - from otx.engine.engine import Engine - from otx.hpo.hpo_base import HpoBase - -logger = logging.getLogger(__name__) - -AVAILABLE_HP_NAME_MAP = { - "data.config.train_subset.batch_size": "datamodule.config.train_subset.batch_size", - "optimizer": "optimizer.keywords", - "scheduler": "scheduler.keywords", -} - - -def execute_hpo( - engine: Engine, - max_epochs: int, - hpo_config: HpoConfig | None = None, - progress_update_callback: Callable[[int | float], None] | None = None, - **train_args, -) -> tuple[dict[str, Any] | None, Path | None]: - """Execute HPO. - - Args: - engine (Engine): engine instnace. - max_epochs (int): max epochs to train. - hpo_config (HpoConfig | None, optional): Configuration for HPO. - progress_update_callback (Callable[[int | float], None] | None, optional): - callback to update progress. If it's given, it's called with progress every second. Defaults to None. - - Returns: - tuple[dict[str, Any] | None, Path | None]: - best hyper parameters and model weight trained with best hyper parameters. If it doesn't exist, - return None. - """ - if engine.task == OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING: # type: ignore[has-type] - logger.warning("Zero shot visual prompting task doesn't support HPO.") - return None, None - - hpo_workdir = Path(engine.work_dir) / "hpo" - hpo_workdir.mkdir(exist_ok=True) - hpo_configurator = HPOConfigurator( - engine, - max_epochs, - hpo_workdir, - hpo_config, - ) - if (hpo_algo := hpo_configurator.get_hpo_algo()) is None: - logger.warning("HPO is skipped.") - return None, None - - if progress_update_callback is not None: - Thread(target=_update_hpo_progress, args=[progress_update_callback, hpo_algo], daemon=True).start() - - run_hpo_loop( - hpo_algo, - partial( - run_hpo_trial, - hpo_workdir=hpo_workdir, - engine=engine, - max_epochs=max_epochs, - **_adjust_train_args(train_args), - ), - "gpu" if torch.cuda.is_available() else "cpu", - ) - - best_trial = hpo_algo.get_best_config() - if best_trial is None: - best_config = None - best_hpo_weight = None - else: - best_config = best_trial["configuration"] - if (trial_file := find_trial_file(hpo_workdir, best_trial["id"])) is not None: - best_hpo_weight = get_best_hpo_weight(get_hpo_weight_dir(hpo_workdir, best_trial["id"]), trial_file) - - hpo_algo.print_result() - _remove_unused_model_weights(hpo_workdir, best_hpo_weight) - - return best_config, best_hpo_weight - - -class HPOConfigurator: - """HPO configurator. Prepare a configuration and provide an HPO algorithm based on the configuration. - - Args: - engine (Engine): engine instance. - max_epoch (int): max epochs to train. - hpo_workdir (Path | None, optional): HPO work directory. Defaults to None. - hpo_config (HpoConfig | None, optional): Configuration for HPO. - """ - - def __init__( - self, - engine: Engine, - max_epoch: int, - hpo_workdir: Path | None = None, - hpo_config: HpoConfig | None = None, - ) -> None: - self._engine = engine - self._max_epoch = max_epoch - self._hpo_workdir = hpo_workdir if hpo_workdir is not None else Path(engine.work_dir) / "hpo" - self.hpo_config: dict[str, Any] = hpo_config # type: ignore[assignment] - - @property - def hpo_config(self) -> dict[str, Any]: - """Configuration for HPO algorithm.""" - return self._hpo_config - - @hpo_config.setter - def hpo_config(self, hpo_config: HpoConfig | None) -> None: - train_dataset_size = len(self._engine.datamodule.subsets["train"]) - val_dataset_size = len(self._engine.datamodule.subsets["val"]) - - self._hpo_config: dict[str, Any] = { # default setting - "save_path": str(self._hpo_workdir), - "num_full_iterations": self._max_epoch, - "full_dataset_size": train_dataset_size, - "non_pure_train_ratio": val_dataset_size / (train_dataset_size + val_dataset_size), - } - - if hpo_config is not None: - self._hpo_config.update( - {key: val for key, val in dataclasses.asdict(hpo_config).items() if val is not None}, - ) - - if "search_space" not in self._hpo_config: - self._hpo_config["search_space"] = self._get_default_search_space() - else: - self._align_hp_name(self._hpo_config["search_space"]) - - if ( # align batch size to train set size - "datamodule.config.train_subset.batch_size" in self._hpo_config["search_space"] - and self._hpo_config["search_space"]["datamodule.config.train_subset.batch_size"]["max"] - > train_dataset_size - ): - logger.info( - "Max value of batch size in HPO search space is lower than train dataset size. " - "Decrease it to train dataset size.", - ) - self._hpo_config["search_space"]["datamodule.config.train_subset.batch_size"]["max"] = train_dataset_size - - self._remove_wrong_search_space(self._hpo_config["search_space"]) - - if "prior_hyper_parameters" not in self._hpo_config: # default hyper parameters are tried first - self._hpo_config["prior_hyper_parameters"] = { - hp: get_using_dot_delimited_key(hp, self._engine) - for hp in self._hpo_config["search_space"].keys() # noqa: SIM118 - } - - def _get_default_search_space(self) -> dict[str, Any]: - """Set learning rate and batch size as search space.""" - search_space = {} - - if isinstance(self._engine.optimizer, list): - for i, optimizer in enumerate(self._engine.optimizer): - search_space[f"optimizer.{i}.keywords.lr"] = self._make_lr_search_space(optimizer) - elif isinstance(self._engine.optimizer, OptimizerCallable): - search_space["optimizer.keywords.lr"] = self._make_lr_search_space(self._engine.optimizer) - - cur_bs = self._engine.datamodule.config.train_subset.batch_size - search_space["datamodule.config.train_subset.batch_size"] = { - "type": "qloguniform", - "min": cur_bs // 2, - "max": cur_bs * 2, - "step": 2, - } - - return search_space - - @staticmethod - def _make_lr_search_space(optimizer: OptimizerCallable) -> dict[str, Any]: - cur_lr = optimizer.keywords["lr"] # type: ignore[union-attr] - min_lr = cur_lr / 10 - return { - "type": "qloguniform", - "min": min_lr, - "max": min(cur_lr * 10, 0.1), - "step": 10 ** -get_decimal_point(min_lr), - } - - @staticmethod - def _align_hp_name(search_space: dict[str, Any]) -> None: - for hp_name in list(search_space.keys()): - for valid_hp in AVAILABLE_HP_NAME_MAP: - if valid_hp in hp_name: - new_hp_name = hp_name.replace(valid_hp, AVAILABLE_HP_NAME_MAP[valid_hp]) - search_space[new_hp_name] = search_space.pop(hp_name) - break - else: - error_msg = ( - "Given hyper parameter can't be optimized by HPO. " - f"Please choose one from {','.join(AVAILABLE_HP_NAME_MAP)}." - ) - raise ValueError(error_msg) - - @staticmethod - def _remove_wrong_search_space(search_space: dict[str, dict[str, Any]]) -> None: - for hp_name, config in list(search_space.items()): - if config["type"] == "choice": - if not config["choice_list"]: - search_space.pop(hp_name) - logger.warning(f"choice_list is empty. {hp_name} is excluded from HPO serach space.") - elif config["max"] < config["min"] + config.get("step", 0): - search_space.pop(hp_name) - if "step" in config: - reason_to_exclude = "max is smaller than sum of min and step" - else: - reason_to_exclude = "max is smaller than min" - logger.warning(f"{reason_to_exclude}. {hp_name} is excluded from HPO serach space.") - - def get_hpo_algo(self) -> HpoBase | None: - """Get HPO algorithm based on prepared configuration.""" - if not self.hpo_config["search_space"]: - logger.warning("There is no hyper parameter to optimize.") - return None - return HyperBand(**self.hpo_config) - - -def _update_hpo_progress(progress_update_callback: Callable[[int | float], None], hpo_algo: HpoBase) -> None: - while not hpo_algo.is_done(): - progress_update_callback(hpo_algo.get_progress() * 100) - time.sleep(1) - - -def _adjust_train_args(train_args: dict[str, Any]) -> dict[str, Any]: - train_args.update(train_args.pop("kwargs", {})) - train_args.pop("self", None) - train_args.pop("run_hpo", None) - - return train_args - - -def _remove_unused_model_weights(hpo_workdir: Path, best_hpo_weight: Path | None = None) -> None: - remove_matched_files(hpo_workdir, "*.ckpt", best_hpo_weight) diff --git a/src/otx/engine/hpo/hpo_trial.py b/src/otx/engine/hpo/hpo_trial.py deleted file mode 100644 index 6b519af504c..00000000000 --- a/src/otx/engine/hpo/hpo_trial.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Components to run HPO trial.""" - -from __future__ import annotations - -from pathlib import Path -from tempfile import TemporaryDirectory -from typing import TYPE_CHECKING, Any, Callable - -from lightning import Callback -from lightning.pytorch.callbacks.model_checkpoint import ModelCheckpoint - -from otx.algo.callbacks.adaptive_train_scheduling import AdaptiveTrainScheduling -from otx.hpo import TrialStatus -from otx.utils.utils import find_file_recursively, remove_matched_files, set_using_dot_delimited_key - -from .utils import find_trial_file, get_best_hpo_weight, get_hpo_weight_dir - -if TYPE_CHECKING: - from lightning import LightningModule, Trainer - - from otx.engine.engine import Engine - - -def update_hyper_parameter(engine: Engine, hyper_parameter: dict[str, Any]) -> None: - """Update hyper parameter in the engine.""" - for key, val in hyper_parameter.items(): - set_using_dot_delimited_key(key, val, engine) - - -class HPOCallback(Callback): - """HPO callback class which reports a score to HPO algorithm every epoch.""" - - def __init__(self, report_func: Callable[[float | int, float | int], TrialStatus], metric: str) -> None: - super().__init__() - self._report_func = report_func - self.metric = metric - - def on_train_epoch_end(self, trainer: Trainer, pl_module_: LightningModule) -> None: - """Report scores if score exists at the end of each epoch.""" - score = trainer.callback_metrics.get(self.metric) - if score is not None and self._report_func(score.item(), trainer.current_epoch + 1) == TrialStatus.STOP: - trainer.should_stop = True - - -def run_hpo_trial( - hp_config: dict[str, Any], - report_func: Callable[[int | float, int | float, bool], None], - hpo_workdir: Path, - engine: Engine, - callbacks: list[Callback] | Callback | None = None, - **train_args, -) -> None: - """Run HPO trial. After it's done, best weight and last weight are saved for later use. - - Args: - hp_config (dict[str, Any]): trial's hyper parameter. - report_func (Callable): function to report score. - hpo_workdir (Path): HPO work directory. - engine (Engine): engine instance. - callbacks (list[Callback] | Callback | None, optional): callbacks used during training. Defaults to None. - train_args: Arugments for 'engine.train'. - """ - trial_id = hp_config["id"] - hpo_weight_dir = get_hpo_weight_dir(hpo_workdir, trial_id) - - _set_trial_hyper_parameter(hp_config["configuration"], engine, train_args) - - if (checkpoint := _find_last_weight(hpo_weight_dir)) is not None: - engine.checkpoint = checkpoint - train_args["resume"] = True - - callbacks = _register_hpo_callback(report_func, callbacks) - _set_to_validate_every_epoch(callbacks, train_args) - - with TemporaryDirectory(prefix="OTX-HPO-") as temp_dir: - _change_work_dir(temp_dir, callbacks, engine) - engine.train(callbacks=callbacks, **train_args) - - _keep_best_and_last_weight(Path(temp_dir), hpo_workdir, trial_id) - - report_func(0, 0, done=True) # type: ignore[call-arg] - - -def _set_trial_hyper_parameter(hyper_parameter: dict[str, Any], engine: Engine, train_args: dict[str, Any]) -> None: - train_args["max_epochs"] = round(hyper_parameter.pop("iterations")) - update_hyper_parameter(engine, hyper_parameter) - - -def _find_last_weight(weight_dir: Path) -> Path | None: - return find_file_recursively(weight_dir, "last.ckpt") - - -def _register_hpo_callback(report_func: Callable, callbacks: list[Callback] | Callback | None) -> list[Callback]: - if isinstance(callbacks, Callback): - callbacks = [callbacks] - elif callbacks is None: - callbacks = [] - callbacks.append(HPOCallback(report_func, _get_metric(callbacks))) - return callbacks - - -def _get_metric(callbacks: list[Callback]) -> str: - for callback in callbacks: - if isinstance(callback, ModelCheckpoint): - return callback.monitor - error_msg = "Failed to find a metric. There is no ModelCheckpoint in callback list." - raise RuntimeError(error_msg) - - -def _set_to_validate_every_epoch(callbacks: list[Callback], train_args: dict[str, Any]) -> None: - for callback in callbacks: - if isinstance(callback, AdaptiveTrainScheduling): - callback.max_interval = 1 - break - else: - train_args["check_val_every_n_epoch"] = 1 - - -def _change_work_dir(work_dir: str, callbacks: list[Callback], engine: Engine) -> None: - for callback in callbacks: - if isinstance(callback, ModelCheckpoint): - callback.dirpath = work_dir - break - engine.work_dir = work_dir - - -def _keep_best_and_last_weight(trial_work_dir: Path, hpo_workdir: Path, trial_id: str) -> None: - weight_dir = get_hpo_weight_dir(hpo_workdir, trial_id) - _move_all_ckpt(trial_work_dir, weight_dir) - if (trial_file := find_trial_file(hpo_workdir, trial_id)) is not None: - best_weight = get_best_hpo_weight(weight_dir, trial_file) - remove_matched_files(weight_dir, "epoch_*.ckpt", best_weight) - - -def _move_all_ckpt(src: Path, dest: Path) -> None: - for ckpt_file in src.rglob("*.ckpt"): - ckpt_file.replace(dest / ckpt_file.name) diff --git a/src/otx/engine/hpo/utils.py b/src/otx/engine/hpo/utils.py deleted file mode 100644 index b2c43846f8d..00000000000 --- a/src/otx/engine/hpo/utils.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Util functions to run HPO.""" - -from __future__ import annotations - -import json -from typing import TYPE_CHECKING - -from otx.utils.utils import find_file_recursively - -if TYPE_CHECKING: - from pathlib import Path - - -def find_trial_file(hpo_workdir: Path, trial_id: str) -> Path | None: - """Find a trial file which store trial record. - - Args: - hpo_workdir (Path): HPO work directory. - trial_id (str): trial id. - - Returns: - Path | None: trial file. If it doesn't exist, return None. - """ - return find_file_recursively(hpo_workdir, f"{trial_id}.json") - - -def get_best_hpo_weight(weight_dir: Path, trial_file: Path) -> Path | None: - """Get best model weight path of the HPO trial. - - Args: - weight_dir (Path): directory where model weights are saved. - trial_file (Path): json format trial file which stores trial record. - - Returns: - Path | None: best HPO model weight. If it doesn't exist, return None. - """ - if not trial_file.exists(): - return None - - with trial_file.open("r") as f: - trial_output = json.load(f) - - best_epochs = [] - best_score = None - for epoch, score in trial_output["score"].items(): - eph = str(int(epoch) - 1) # lightning uses index starting from 0 - if best_score is None: - best_score = score - best_epochs.append(eph) - elif best_score < score: - best_score = score - best_epochs = [eph] - elif best_score == score: - best_epochs.append(eph) - - best_epochs.sort(key=int, reverse=True) - for best_epoch in best_epochs: - if (best_weight_path := find_file_recursively(weight_dir, f"epoch_*{best_epoch}.ckpt")) is not None: - return best_weight_path - - return None - - -def get_hpo_weight_dir(hpo_workdir: Path, trial_id: str) -> Path: - """Get HPO weight directory. If it doesn't exist, directory is made. - - Args: - hpo_workdir (Path): HPO work directory. - trial_id (str): trial id. - - Returns: - Path: HPO weight directory path. - """ - hpo_weight_dir: Path = hpo_workdir / "weight" / trial_id - if not hpo_weight_dir.exists(): - hpo_weight_dir.mkdir(parents=True) - return hpo_weight_dir diff --git a/src/otx/engine/utils/__init__.py b/src/otx/engine/utils/__init__.py deleted file mode 100644 index c8efaed1fd4..00000000000 --- a/src/otx/engine/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Util API for OTX Engine.""" diff --git a/src/otx/engine/utils/api.py b/src/otx/engine/utils/api.py deleted file mode 100644 index 98fe31e228e..00000000000 --- a/src/otx/engine/utils/api.py +++ /dev/null @@ -1,76 +0,0 @@ -"""OTX APIs for User-friendliness.""" - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import fnmatch -import textwrap -from pathlib import Path - -from otx.core.types.task import OTXTaskType -from otx.core.utils.imports import get_otx_root_path - -RECIPE_PATH = get_otx_root_path() / "recipe" - - -def list_models(task: OTXTaskType | None = None, pattern: str | None = None, print_table: bool = False) -> list[str]: - """Returns a list of available models for training. - - Args: - task (OTXTaskType | None, optional): Recipe Filter by Task. - pattern (Optional[str], optional): A string pattern to filter the list of available models. Defaults to None. - print_table (bool, optional): Output the recipe information as a Rich.Table. - This is primarily used for `otx find` in the CLI. - - Returns: - list[str]: A list of available models for pretraining. - - Example: - # Return all available model list. - >>> models = list_models() - >>> models - ['atss_mobilenetv2', 'atss_r50_fpn', ...] - - # Return INSTANCE_SEGMENTATION model list. - >>> models = list_models(task="INSTANCE_SEGMENTATION") - >>> models - ['maskrcnn_efficientnetb2b', 'maskrcnn_r50', 'maskrcnn_swint', 'openvino_model'] - - # Return all available model list that matches the pattern. - >>> models = list_models(task="MULTI_CLASS_CLS", pattern="*efficient") - >>> models - ['otx_efficientnet_b0', 'efficientnet_v2_light', 'efficientnet_b0_light', ...] - - # Print the recipe information as a Rich.Table (include task, model name, recipe path) - >>> models = list_models(task="MULTI_CLASS_CLS", pattern="*efficient", print_table=True) - """ - task_type = OTXTaskType(task).name.lower() if task is not None else "**" - recipe_list = [str(recipe) for recipe in RECIPE_PATH.glob(f"**/{task_type}/*.yaml") if "_base_" not in recipe.parts] - - if pattern is not None: - # Always match keys with any postfix. - recipe_list = list(set(fnmatch.filter(recipe_list, f"*{pattern}*"))) - - if print_table: - from rich.console import Console - from rich.table import Table - - console = Console() - table = Table(title="OTX Recipes", show_header=True, header_style="bold magenta") - table.add_column("Task") - table.add_column("Model Name") - table.add_column("Recipe Path") - for recipe in recipe_list: - recipe_path = ( - textwrap.fill(recipe, width=int(console.width / 2)) if len(recipe) > console.width / 2 else recipe - ) - table.add_row( - recipe.split("/")[-2].upper(), - Path(recipe).stem, - recipe_path, - ) - console.print(table, width=console.width, justify="center") - - return list({Path(recipe).stem for recipe in recipe_list}) diff --git a/src/otx/engine/utils/auto_configurator.py b/src/otx/engine/utils/auto_configurator.py deleted file mode 100644 index bf72ec09d1d..00000000000 --- a/src/otx/engine/utils/auto_configurator.py +++ /dev/null @@ -1,351 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Auto-Configurator class & util functions for OTX Auto-Configuration.""" - -from __future__ import annotations - -import logging -from copy import deepcopy -from pathlib import Path -from typing import TYPE_CHECKING - -import datumaro -from lightning.pytorch.cli import instantiate_class - -from otx.core.config.data import DataModuleConfig, SamplerConfig, SubsetConfig, TileConfig -from otx.core.data.dataset.base import LabelInfo -from otx.core.data.module import OTXDataModule -from otx.core.model.entity.base import OVModel -from otx.core.types import PathLike -from otx.core.types.task import OTXTaskType -from otx.core.utils.imports import get_otx_root_path -from otx.core.utils.instantiators import partial_instantiate_class - -if TYPE_CHECKING: - from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable - from torchmetrics import Metric - - from otx.core.model.entity.base import OTXModel - - -logger = logging.getLogger() -RECIPE_PATH = get_otx_root_path() / "recipe" - -DEFAULT_CONFIG_PER_TASK = { - OTXTaskType.MULTI_CLASS_CLS: RECIPE_PATH / "classification" / "multi_class_cls" / "otx_efficientnet_b0.yaml", - OTXTaskType.MULTI_LABEL_CLS: RECIPE_PATH / "classification" / "multi_label_cls" / "efficientnet_b0_light.yaml", - OTXTaskType.H_LABEL_CLS: RECIPE_PATH / "classification" / "h_label_cls" / "efficientnet_b0_light.yaml", - OTXTaskType.DETECTION: RECIPE_PATH / "detection" / "atss_mobilenetv2.yaml", - OTXTaskType.ROTATED_DETECTION: RECIPE_PATH / "rotated_detection" / "maskrcnn_r50.yaml", - OTXTaskType.SEMANTIC_SEGMENTATION: RECIPE_PATH / "semantic_segmentation" / "litehrnet_18.yaml", - OTXTaskType.INSTANCE_SEGMENTATION: RECIPE_PATH / "instance_segmentation" / "maskrcnn_r50.yaml", - OTXTaskType.ACTION_CLASSIFICATION: RECIPE_PATH / "action" / "action_classification" / "x3d.yaml", - OTXTaskType.ACTION_DETECTION: RECIPE_PATH / "action" / "action_detection" / "x3d_fastrcnn.yaml", - OTXTaskType.ANOMALY_CLASSIFICATION: RECIPE_PATH / "anomaly_classification" / "padim.yaml", - OTXTaskType.ANOMALY_SEGMENTATION: RECIPE_PATH / "anomaly_segmentation" / "padim.yaml", - OTXTaskType.ANOMALY_DETECTION: RECIPE_PATH / "anomaly_detection" / "padim.yaml", - OTXTaskType.VISUAL_PROMPTING: RECIPE_PATH / "visual_prompting" / "sam_tiny_vit.yaml", - OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING: RECIPE_PATH / "zero_shot_visual_prompting" / "sam_tiny_vit.yaml", -} - -TASK_PER_DATA_FORMAT = { - "imagenet_with_subset_dirs": [OTXTaskType.MULTI_CLASS_CLS, OTXTaskType.H_LABEL_CLS], - "datumaro": [OTXTaskType.MULTI_LABEL_CLS], - "coco_instances": [ - OTXTaskType.DETECTION, - OTXTaskType.ROTATED_DETECTION, - OTXTaskType.INSTANCE_SEGMENTATION, - OTXTaskType.VISUAL_PROMPTING, - ], - "coco": [ - OTXTaskType.DETECTION, - OTXTaskType.ROTATED_DETECTION, - OTXTaskType.INSTANCE_SEGMENTATION, - OTXTaskType.VISUAL_PROMPTING, - ], - "common_semantic_segmentation_with_subset_dirs": [OTXTaskType.SEMANTIC_SEGMENTATION], - "kinetics": [OTXTaskType.ACTION_CLASSIFICATION], - "ava": [OTXTaskType.ACTION_DETECTION], - "mvtec": [OTXTaskType.ANOMALY_CLASSIFICATION, OTXTaskType.ANOMALY_DETECTION, OTXTaskType.ANOMALY_SEGMENTATION], -} - -OVMODEL_PER_TASK = { - OTXTaskType.MULTI_CLASS_CLS: "otx.core.model.entity.classification.OVMulticlassClassificationModel", - OTXTaskType.MULTI_LABEL_CLS: "otx.core.model.entity.classification.OVMultilabelClassificationModel", - OTXTaskType.H_LABEL_CLS: "otx.core.model.entity.classification.OVHlabelClassificationModel", - OTXTaskType.DETECTION: "otx.core.model.entity.detection.OVDetectionModel", - OTXTaskType.ROTATED_DETECTION: "otx.core.model.entity.rotated_detection.OVRotatedDetectionModel", - OTXTaskType.INSTANCE_SEGMENTATION: "otx.core.model.entity.instance_segmentation.OVInstanceSegmentationModel", - OTXTaskType.SEMANTIC_SEGMENTATION: "otx.core.model.entity.segmentation.OVSegmentationModel", - OTXTaskType.VISUAL_PROMPTING: "otx.core.model.entity.visual_prompting.OVVisualPromptingModel", - OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING: "otx.core.model.entity.visual_prompting.OVZeroShotVisualPromptingModel", - OTXTaskType.ACTION_CLASSIFICATION: "otx.core.model.entity.action_classification.OVActionClsModel", -} - - -def configure_task(data_root: PathLike) -> OTXTaskType: - """Configures the task based on the given data root. - - Args: - data_root (PathLike): The root directory of the data. - - Returns: - OTXTaskType: The configured task type, or None if data_root is None. - - Raises: - ValueError: If the data format is not supported. - """ - data_root = Path(data_root).resolve() - - data_format = datumaro.Environment().detect_dataset(str(data_root)) - if len(data_format) > 1: - logger.warning(f"Found multiple data formats: {data_format}. We will use the first one.") - data_format = data_format[0] - if data_format not in TASK_PER_DATA_FORMAT: - msg = f"Can't find proper task. we are not support {data_format} format, yet." - raise ValueError(msg) - if len(TASK_PER_DATA_FORMAT[data_format]) > 1: - logger.warning( - f"Found multiple tasks with {data_format}: {TASK_PER_DATA_FORMAT[data_format]}. We will use the first one.", - ) - return TASK_PER_DATA_FORMAT[data_format][0] - - -class AutoConfigurator: - """This Class is used to configure the OTXDataModule, OTXModel, Optimizer, and Scheduler with OTX Default. - - Args: - data_root (PathLike | None, optional): The root directory for data storage. Defaults to None. - task (OTXTaskType | None, optional): The current task. Defaults to None. - model_name (str | None, optional): Name of the model to use as the default. - If None, the default model will be used. Defaults to None. - - Example: - The following examples show how to use the AutoConfigurator class. - - >>> auto_configurator = AutoConfigurator( - ... data_root=, - ... task=, - ... ) - - # If task is None, the task will be configured based on the data root. - >>> auto_configurator = AutoConfigurator( - ... data_root=, - ... ) - """ - - def __init__( - self, - data_root: PathLike | None = None, - task: OTXTaskType | None = None, - model_name: str | None = None, - ) -> None: - self.data_root = data_root - self._task = task - self._config: dict | None = None - self.model_name: str | None = model_name - - @property - def task(self) -> OTXTaskType: - """Returns the current task. - - Raises: - RuntimeError: If there are no ready tasks. - - Returns: - OTXTaskType: The current task. - """ - if self._task is not None: - return self._task - if self.data_root is not None: - self._task = configure_task(self.data_root) - return self._task - msg = "There are no ready task" - raise RuntimeError(msg) - - @property - def config(self) -> dict: - """Retrieves the configuration for the auto configurator. - - If the configuration has not been loaded yet, it will be loaded using the default configuration - based on the model name. - - Returns: - dict: The configuration as a dict object. - """ - if self._config is None: - self._config = self._load_default_config(self.model_name) - return self._config - - def _load_default_config(self, model_name: str | None = None) -> dict: - """Load the default configuration for the specified model. - - Args: - model_name (str | None): The name of the model. If provided, the configuration - file name will be modified to use the specified model. - - Returns: - dict: The loaded configuration. - - Raises: - ValueError: If the task doesn't supported for auto-configuration. - """ - config_file = DEFAULT_CONFIG_PER_TASK.get(self.task, None) - if config_file is None: - msg = f"{self.task} doesn't support Auto-Configuration." - raise ValueError(msg) - if model_name is not None: - model_path = str(config_file).split("/") - model_path[-1] = f"{model_name}.yaml" - config_file = Path("/".join(model_path)) - from otx.cli.utils.jsonargparse import get_configuration - - return get_configuration(config_file) - - def get_datamodule(self) -> OTXDataModule | None: - """Returns an instance of OTXDataModule with the configured data root. - - Returns: - OTXDataModule | None: An instance of OTXDataModule. - """ - if self.data_root is None: - return None - self.config["data"]["config"]["data_root"] = self.data_root - data_config = deepcopy(self.config["data"]["config"]) - train_config = data_config.pop("train_subset") - val_config = data_config.pop("val_subset") - test_config = data_config.pop("test_subset") - return OTXDataModule( - task=self.config["data"]["task"], - config=DataModuleConfig( - train_subset=SubsetConfig(sampler=SamplerConfig(**train_config.pop("sampler", {})), **train_config), - val_subset=SubsetConfig(sampler=SamplerConfig(**val_config.pop("sampler", {})), **val_config), - test_subset=SubsetConfig(sampler=SamplerConfig(**test_config.pop("sampler", {})), **test_config), - tile_config=TileConfig(**data_config.pop("tile_config", {})), - **data_config, - ), - ) - - def get_model(self, model_name: str | None = None, label_info: LabelInfo | None = None) -> OTXModel: - """Retrieves the OTXModel instance based on the provided model name and meta information. - - Args: - model_name (str | None): The name of the model to retrieve. If None, the default model will be used. - label_info (LabelInfo | None): The meta information about the labels. If provided, the number of classes - will be updated in the model's configuration. - - Returns: - OTXModel: The instantiated OTXModel instance. - - Example: - The following examples show how to get the OTXModel class. - - # If model_name is None, the default model will be used from task. - >>> auto_configurator.get_model( - ... label_info=, - ... ) - - # If model_name is str, the default config file is changed. - >>> auto_configurator.get_model( - ... model_name=, - ... label_info=, - ... ) - """ - if model_name is not None: - self._config = self._load_default_config(self.model_name) - if label_info is not None: - num_classes = label_info.num_classes - self.config["model"]["init_args"]["num_classes"] = num_classes - - from otx.core.data.dataset.classification import HLabelInfo - - if isinstance(label_info, HLabelInfo): - init_args = self.config["model"]["init_args"] - init_args["num_multiclass_heads"] = label_info.num_multiclass_heads - init_args["num_multilabel_classes"] = label_info.num_multilabel_classes - - logger.warning(f"Set Default Model: {self.config['model']}") - return instantiate_class(args=(), init=self.config["model"]) - - def get_optimizer(self) -> list[OptimizerCallable] | None: - """Returns the optimizer callable based on the configuration. - - Returns: - list[OptimizerCallable] | None: The optimizer callable. - """ - optimizer_config = self.config.get("optimizer", None) - logger.warning(f"Set Default Optimizer: {optimizer_config}") - return partial_instantiate_class(init=optimizer_config) - - def get_scheduler(self) -> list[LRSchedulerCallable] | None: - """Returns the instantiated scheduler based on the configuration. - - Returns: - list[LRSchedulerCallable] | None: The instantiated scheduler. - """ - scheduler_config = self.config.get("scheduler", None) - logger.warning(f"Set Default Scheduler: {scheduler_config}") - return partial_instantiate_class(init=scheduler_config) - - def get_metric(self) -> Metric | None: - """Returns the instantiated metric based on the configuration. - - Returns: - Metric | None: The instantiated metric. - """ - if self.task in DEFAULT_CONFIG_PER_TASK: - metric_config = self.config.get("metric", None) - logger.warning(f"Set Default Metric: {metric_config}") - - # Currently, single metric only available. - if metric_config: - metric = partial_instantiate_class(init=metric_config) - return metric[0] if isinstance(metric, list) else metric - - return None - - def get_ov_model(self, model_name: str, label_info: LabelInfo) -> OVModel: - """Retrieves the OVModel instance based on the given model name and label information. - - Args: - model_name (str): The name of the model. - label_info (LabelInfo): The label information. - - Returns: - OVModel: The OVModel instance. - - Raises: - NotImplementedError: If the OVModel for the given task is not supported. - """ - class_path = OVMODEL_PER_TASK.get(self.task, None) - if class_path is None: - msg = f"{self.task} is not support OVModel." - raise NotImplementedError(msg) - class_module, class_name = class_path.rsplit(".", 1) - module = __import__(class_module, fromlist=[class_name]) - ov_model = getattr(module, class_name) - return ov_model( - model_name=model_name, - num_classes=label_info.num_classes, - ) - - def get_ov_datamodule(self) -> OTXDataModule: - """Returns an instance of OTXDataModule configured with the specified data root and data module configuration. - - Returns: - OTXDataModule: An instance of OTXDataModule. - """ - config = self._load_default_config(model_name="openvino_model") - config["data"]["config"]["data_root"] = self.data_root - data_config = config["data"]["config"].copy() - return OTXDataModule( - task=config["data"]["task"], - config=DataModuleConfig( - train_subset=SubsetConfig(**data_config.pop("train_subset")), - val_subset=SubsetConfig(**data_config.pop("val_subset")), - test_subset=SubsetConfig(**data_config.pop("test_subset")), - tile_config=TileConfig(**data_config.pop("tile_config", {})), - **data_config, - ), - ) diff --git a/src/otx/hpo/__init__.py b/src/otx/hpo/__init__.py index 72301826f16..8726914c5f3 100644 --- a/src/otx/hpo/__init__.py +++ b/src/otx/hpo/__init__.py @@ -1,8 +1,19 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# """HPO package.""" +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + from .hpo_base import TrialStatus from .hpo_runner import run_hpo_loop from .hyperband import HyperBand diff --git a/src/otx/hpo/hpo_base.py b/src/otx/hpo/hpo_base.py index f0e03452a57..17ebc9da4be 100644 --- a/src/otx/hpo/hpo_base.py +++ b/src/otx/hpo/hpo_base.py @@ -1,25 +1,30 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# """HPO algorithm abstract class.""" -from __future__ import annotations +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. import json -import logging import tempfile from abc import ABC, abstractmethod from enum import IntEnum -from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal +from typing import Any, Dict, List, Optional, Union from otx.hpo.search_space import SearchSpace from otx.hpo.utils import check_mode_input, check_positive +from otx.utils.logger import get_logger -if TYPE_CHECKING: - from collections.abc import Hashable - -logger = logging.getLogger(__name__) +logger = get_logger() class HpoBase(ABC): @@ -29,71 +34,73 @@ class HpoBase(ABC): Only common methods are implemented but not core algorithm of HPO. Args: - search_space (dict[str, dict[str, Any]]): hyper parameter search space to find. - save_path (str | None, optional): path where result of HPO is saved. - mode ("max" | "min", optional): One of {min, max}. Determines whether objective is - minimizing or maximizing the score. - num_trials (int | None, optional): How many training to conduct for HPO. - num_workers (int, optional): How many trains are executed in parallel. - num_full_iterations (int, optional): epoch for traninig after HPO. - non_pure_train_ratio (float, optional): ratio of validation time to (train time + validation time) - full_dataset_size (int, optional): train dataset size - expected_time_ratio (int | float | None, optional): Time to use for HPO. - If HPO is configured automatically, - HPO use time about exepected_time_ratio * - train time after HPO times. - maximum_resource (int | float | None, optional): Maximum resource to use for training each trial. - subset_ratio (float | int | None, optional): ratio to how many train dataset to use for each trial. + search_space (Dict[str, Dict[str, Any]]): hyper parameter search space to find. + save_path (Optional[str]): path where result of HPO is saved. + mode (str, optinal): One of {min, max}. Determines whether objective is + minimizing or maximizing the metric attribute. + num_trials (Optional[int]): How many training to conduct for HPO. + num_workers (int): How many trains are executed in parallel. + num_full_iterations (int): epoch for traninig after HPO. + non_pure_train_ratio (float): ratio of validation time to (train time + validation time) + full_dataset_size (int): train dataset size + metric (str): Which score metric to use. + expected_time_ratio (Optional[Union[int, float]]): Time to use for HPO. + If HPO is configured automatically, + HPO use time about exepected_time_ratio * + train time after HPO times. + maximum_resource (Optional[Union[int, float]]): Maximum resource to use for training each trial. + subset_ratio (Optional[Union[float, int]]): ratio to how many train dataset to use for each trial. The lower value is, the faster the speed is. But If it's too low, HPO can be unstable. - min_subset_size (int, optional) : Minimum size of subset. Default value is 500. - resume (bool, optional): resume flag decide to use previous HPO results. - If HPO completed, you can just use optimized hyper parameters. - If HPO stopped in middle, you can resume in middle. - prior_hyper_parameters (dict | list[dict] | None, optional) = Hyper parameters to try first. - acceptable_additional_time_ratio (float | int, optional) = Decide how much additional time can be acceptable. + min_subset_size (int) : Minimum size of subset. Default value is 500. + resume (bool): resume flag decide to use previous HPO results. + If HPO completed, you can just use optimized hyper parameters. + If HPO stopped in middle, you can resume in middle. + prior_hyper_parameters (Optional[Union[Dict, List[Dict]]]) = Hyper parameters to try first. + acceptable_additional_time_ratio (Union[float, int]) = Decide how much additional time can be acceptable. """ # pylint: disable=too-many-instance-attributes def __init__( self, - search_space: dict[str, dict[str, Any]], - save_path: str | None = None, - mode: Literal["max", "min"] = "max", - num_trials: int | None = None, + search_space: Dict[str, Dict[str, Any]], + save_path: Optional[str] = None, + mode: str = "max", + num_trials: Optional[int] = None, num_workers: int = 1, - num_full_iterations: int | float = 1, + num_full_iterations: Union[int, float] = 1, non_pure_train_ratio: float = 0.2, full_dataset_size: int = 0, - expected_time_ratio: int | float | None = None, - maximum_resource: int | float | None = None, - subset_ratio: float | int | None = None, + metric: str = "mAP", + expected_time_ratio: Optional[Union[int, float]] = None, + maximum_resource: Optional[Union[int, float]] = None, + subset_ratio: Optional[Union[float, int]] = None, min_subset_size: int = 500, resume: bool = False, - prior_hyper_parameters: dict | list[dict] | None = None, - acceptable_additional_time_ratio: float | int = 1.0, - ) -> None: + prior_hyper_parameters: Optional[Union[Dict, List[Dict]]] = None, + acceptable_additional_time_ratio: Union[float, int] = 1.0, + ): # pylint: disable=too-many-arguments, too-many-locals check_mode_input(mode) check_positive(full_dataset_size, "full_dataset_size") check_positive(num_full_iterations, "num_full_iterations") if not 0 < non_pure_train_ratio <= 1: - error_msg = ( + raise ValueError( "non_pure_train_ratio should be greater than 0 and lesser than or equal to 1." - f"Your value is {subset_ratio}" + f" Your value is {subset_ratio}" ) - raise ValueError(error_msg) if maximum_resource is not None: check_positive(maximum_resource, "maximum_resource") if num_trials is not None: check_positive(num_trials, "num_trials") check_positive(num_workers, "num_workers") - if subset_ratio is not None and not 0 < subset_ratio <= 1: - error_msg = ( - f"subset_ratio should be greater than 0 and lesser than or equal to 1. Your value is {subset_ratio}" - ) - raise ValueError(error_msg) + if subset_ratio is not None: + if not 0 < subset_ratio <= 1: + raise ValueError( + "subset_ratio should be greater than 0 and lesser than or equal to 1." + f" Your value is {subset_ratio}" + ) if save_path is None: save_path = tempfile.mkdtemp(prefix="OTX-hpo-") @@ -106,11 +113,12 @@ def __init__( self.non_pure_train_ratio = non_pure_train_ratio self.full_dataset_size = full_dataset_size self.expected_time_ratio = expected_time_ratio - self.maximum_resource: int | float | None = maximum_resource + self.maximum_resource: Optional[Union[int, float]] = maximum_resource self.subset_ratio = subset_ratio self.min_subset_size = min_subset_size self.resume = resume self.hpo_status: dict = {} + self.metric = metric self.acceptable_additional_time_ratio = acceptable_additional_time_ratio if prior_hyper_parameters is None: prior_hyper_parameters = [] @@ -119,42 +127,42 @@ def __init__( self.prior_hyper_parameters = prior_hyper_parameters @abstractmethod - def print_result(self) -> None: + def print_result(self): """Print a HPO algorithm result.""" raise NotImplementedError @abstractmethod - def save_results(self) -> None: + def save_results(self): """Save a HPO algorithm result.""" raise NotImplementedError @abstractmethod - def is_done(self) -> bool: + def is_done(self): """Check whether HPO algorithm is done.""" raise NotImplementedError @abstractmethod - def get_next_sample(self) -> Trial | None: + def get_next_sample(self): """Get next sample to train.""" raise NotImplementedError @abstractmethod - def auto_config(self): # noqa: ANN201 + def auto_config(self): """Configure HPO algorithm automatically.""" raise NotImplementedError @abstractmethod - def get_progress(self) -> int | float: + def get_progress(self): """Get current progress of HPO algorithm.""" raise NotImplementedError @abstractmethod - def report_score(self, score: float | int, resource: float | int, trial_id: Hashable, done: bool) -> TrialStatus: + def report_score(self, score, resource, trial_id, done): """Report a score to HPO algorithm.""" raise NotImplementedError @abstractmethod - def get_best_config(self) -> dict[str, Any] | None: + def get_best_config(self): """Get best config of HPO algorithm.""" raise NotImplementedError @@ -164,36 +172,36 @@ class Trial: Args: trial_id (Any): Trial id. - configuration (dict): Configuration to train with. - train_environment (dict | None, optional): Train environment for the trial. Defaults to None. + configuration (Dict): Configuration to train with. + train_environment (Optional[Dict], optional): Train environment for the trial. Defaults to None. """ - def __init__(self, trial_id: Hashable, configuration: dict, train_environment: dict | None = None) -> None: + def __init__(self, trial_id: Any, configuration: Dict, train_environment: Optional[Dict] = None): self._id = trial_id self._configuration = configuration - self.score: dict[float | int, float | int] = {} + self.score: Dict[Union[float, int], Union[float, int]] = {} self._train_environment = train_environment - self._iteration: int | float | None = None + self._iteration = None self.status: TrialStatus = TrialStatus.READY self._done = False @property - def id(self) -> Hashable: # noqa: A003 + def id(self): """Trial id.""" return self._id @property - def configuration(self) -> dict: + def configuration(self): """Configuration to train with.""" return self._configuration @property - def iteration(self) -> int | float | None: + def iteration(self): """Iteration to use for training.""" return self._iteration @iteration.setter - def iteration(self, val: int | float) -> None: + def iteration(self, val): """Setter for iteration.""" check_positive(val, "iteration") self._iteration = val @@ -201,47 +209,44 @@ def iteration(self, val: int | float) -> None: self._done = False @property - def train_environment(self) -> dict | None: + def train_environment(self): """Train environment for the trial.""" return self._train_environment - def get_train_configuration(self) -> dict[str, Any]: + def get_train_configuration(self) -> Dict[str, Any]: """Get configurations needed to trian.""" self._configuration["iterations"] = self.iteration return {"id": self.id, "configuration": self.configuration, "train_environment": self.train_environment} - def register_score(self, score: int | float, resource: int | float) -> None: + def register_score(self, score: Union[int, float], resource: Union[int, float]): """Register score to the trial. Args: - score (int | float): Score to register. - resource (int | float): Resource used to get score. It should be positive. + score (Union[int, float]): Score to register. + resource (Union[int, float]): Resource used to get score. It should be positive. """ check_positive(resource, "resource") self.score[resource] = score def get_best_score( - self, - mode: Literal["max", "min"] = "max", - resource_limit: float | int | None = None, - ) -> float | int | None: + self, mode: str = "max", resource_limit: Optional[Union[float, int]] = None + ) -> Optional[Union[float, int]]: """Get best score of the trial. Args: - mode ("max" | "min", optional): - Decide which is better between highest score or lowest score. Defaults to "max". - resource_limit (float | int | None, optional): Find a best score among the score at resource - lower than this value. Defaults to None. + mode (str, optional): Decide which is better between highest score or lowest score. Defaults to "max". + resource_limit (Optional[Union[float, int]], optional): Find a best score among the score at resource + lower than this value. Defaults to None. Returns: - float | int | None: Best score. If there is no score, return None. + Optional[Union[float, int]]: Best score. If there is no score, return None. """ check_mode_input(mode) if resource_limit is None: scores = self.score.values() else: - scores = [val for key, val in self.score.items() if key <= resource_limit] # type: ignore[assignment, index] + scores = [val for key, val in self.score.items() if key <= resource_limit] # type: ignore if len(scores) == 0: return None @@ -250,17 +255,17 @@ def get_best_score( return max(scores) return min(scores) - def get_progress(self) -> float | int: + def get_progress(self) -> Union[float, int]: """Get a progress of the trial. Returns: - float | int: How many resource is used for the trial. + Union[float, int]: How many resource is used for the trial. """ if len(self.score) == 0: return 0 return max(self.score.keys()) - def save_results(self, save_path: str) -> None: + def save_results(self, save_path: str): """Save a result in the 'save_path'. Args: @@ -273,21 +278,19 @@ def save_results(self, save_path: str) -> None: "score": self.score, } - with Path(save_path).open("w", encoding="utf-8") as f: + with open(save_path, "w", encoding="utf-8") as f: json.dump(results, f) - def finalize(self) -> None: + def finalize(self): """Set done as True.""" if not self.score: - error_msg = f"Trial{self.id} didn't report any score but tries to be done." - raise RuntimeError(error_msg) + raise RuntimeError(f"Trial{self.id} didn't report any score but tries to be done.") self._done = True - def is_done(self) -> bool: + def is_done(self): """Check the trial is done.""" if self.iteration is None: - error_msg = "iteration isn't set yet." - raise ValueError(error_msg) + raise ValueError("iteration isn't set yet.") return self._done or self.get_progress() >= self.iteration diff --git a/src/otx/hpo/hpo_runner.py b/src/otx/hpo/hpo_runner.py index 2a936ff1a49..3736221989c 100644 --- a/src/otx/hpo/hpo_runner.py +++ b/src/otx/hpo/hpo_runner.py @@ -1,30 +1,35 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# """HPO runner and resource manager class.""" -from __future__ import annotations +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. -import logging import multiprocessing import os import queue import signal +import sys import time from copy import deepcopy from dataclasses import dataclass from functools import partial -from typing import TYPE_CHECKING, Callable, Literal +from typing import Any, Callable, Dict, Literal, Optional, Union from otx.hpo.hpo_base import HpoBase, Trial, TrialStatus from otx.hpo.resource_manager import get_resource_manager -from otx.utils import append_main_proc_signal_handler - -if TYPE_CHECKING: - from collections.abc import Hashable - from signal import Signals +from otx.utils.logger import get_logger -logger = logging.getLogger(__name__) +logger = get_logger() @dataclass @@ -42,44 +47,43 @@ class HpoLoop: Args: hpo_algo (HpoBase): HPO algorithms. train_func (Callable): Function to train a model. - resource_type (Literal['gpu', 'cpu'], optional): Which type of resource to use. + resource_type (Literal['gpu', 'cpu', 'xpu'], optional): Which type of resource to use. If can be changed depending on environment. Defaults to "gpu". - num_parallel_trial (int | None, optional): How many trials to run in parallel. - It's used for CPUResourceManager. Defaults to None. - num_gpu_for_single_trial (int | None, optional): How many GPUs are used for a single trial. - It's used for GPUResourceManager. Defaults to None. - available_gpu (str | None, optional): How many GPUs are available. It's used for GPUResourceManager. - Defaults to None. + num_parallel_trial (Optional[int], optional): How many trials to run in parallel. + It's used for CPUResourceManager. Defaults to None. + num_devices_per_trial (Optional[int], optional): Number of devices used for a single trial. + It's used for GPUResourceManager and XPUResourceManager. + Defaults to None. + available_devices (Optional[str], optional): Number of devices available. + It's used for GPUResourceManager and XPUResourceManager. + Defaults to None. """ def __init__( self, hpo_algo: HpoBase, train_func: Callable, - resource_type: Literal["gpu", "cpu"] = "gpu", - num_parallel_trial: int | None = None, - num_gpu_for_single_trial: int | None = None, - available_gpu: str | None = None, - ) -> None: + resource_type: Literal["gpu", "cpu", "xpu"] = "gpu", + num_parallel_trial: Optional[int] = None, + num_devices_per_trial: Optional[int] = None, + available_devices: Optional[str] = None, + ): self._hpo_algo = hpo_algo self._train_func = train_func - self._running_trials: dict[int, RunningTrial] = {} + self._running_trials: Dict[int, RunningTrial] = {} self._mp = multiprocessing.get_context("spawn") self._report_queue = self._mp.Queue() self._uid_index = 0 self._trial_fault_count = 0 self._resource_manager = get_resource_manager( - resource_type, - num_parallel_trial, - num_gpu_for_single_trial, - available_gpu, + resource_type, num_parallel_trial, num_devices_per_trial, available_devices ) self._main_pid = os.getpid() - append_main_proc_signal_handler(signal.SIGINT, self._terminate_signal_handler) - append_main_proc_signal_handler(signal.SIGTERM, self._terminate_signal_handler) + signal.signal(signal.SIGINT, self._terminate_signal_handler) + signal.signal(signal.SIGTERM, self._terminate_signal_handler) - def run(self) -> None: + def run(self): """Run a HPO loop.""" logger.info("HPO loop starts.") try: @@ -95,7 +99,7 @@ def run(self) -> None: time.sleep(1) except Exception as e: self._terminate_all_running_processes() - raise e # noqa: TRY201 + raise e logger.info("HPO loop is done.") if self._trial_fault_count >= 3: @@ -104,7 +108,7 @@ def run(self) -> None: self._get_reports() self._join_all_processes() - def _start_trial_process(self, trial: Trial) -> None: + def _start_trial_process(self, trial: Trial): logger.info(f"{trial.id} trial is now running.") logger.debug(f"{trial.id} hyper paramter => {trial.configuration}") @@ -123,22 +127,14 @@ def _start_trial_process(self, trial: Trial) -> None: args=( self._train_func, trial.get_train_configuration(), - partial( - _report_score, - recv_queue=trial_queue, - send_queue=self._report_queue, - uid=uid, - trial_id=trial.id, - ), + partial(_report_score, recv_queue=trial_queue, send_queue=self._report_queue, uid=uid), ), ) - self._running_trials[uid] = RunningTrial(process, trial, trial_queue) # type: ignore[arg-type] + os.environ = origin_env + self._running_trials[uid] = RunningTrial(process, trial, trial_queue) # type: ignore process.start() - os.environ.clear() - for key, val in origin_env.items(): - os.environ[key] = val - def _remove_finished_process(self) -> None: + def _remove_finished_process(self): trial_to_remove = [] for uid, trial in self._running_trials.items(): if not trial.process.is_alive(): @@ -153,21 +149,18 @@ def _remove_finished_process(self) -> None: self._resource_manager.release_resource(uid) del self._running_trials[uid] - def _get_reports(self) -> None: + def _get_reports(self): while not self._report_queue.empty(): report = self._report_queue.get_nowait() + trial = self._running_trials[report["uid"]] trial_status = self._hpo_algo.report_score( - report["score"], - report["progress"], - report["trial_id"], - report["done"], + report["score"], report["progress"], trial.trial.id, report["done"] ) - if report["uid"] in self._running_trials: - self._running_trials[report["uid"]].queue.put_nowait(trial_status) + trial.queue.put_nowait(trial_status) self._hpo_algo.save_results() - def _join_all_processes(self) -> None: + def _join_all_processes(self): for val in self._running_trials.values(): val.queue.close() @@ -181,51 +174,44 @@ def _get_uid(self) -> int: self._uid_index += 1 return uid - def _terminate_all_running_processes(self) -> None: + def _terminate_all_running_processes(self): for trial in self._running_trials.values(): trial.queue.close() process = trial.process if process.is_alive(): logger.info(f"Kill child process {process.pid}") - process.terminate() + process.kill() + + def _terminate_signal_handler(self, signum, _frame): + # This code prevents child processses from being killed unintentionally by proccesses forked from main process + if self._main_pid != os.getpid(): + sys.exit() - def _terminate_signal_handler(self, signum: Signals, frame_) -> None: # noqa: ANN001 self._terminate_all_running_processes() singal_name = {2: "SIGINT", 15: "SIGTERM"} logger.warning(f"{singal_name[signum]} is sent. process exited.") + sys.exit(1) + -def _run_train(train_func: Callable, hp_config: dict, report_func: Callable) -> None: +def _run_train(train_func: Callable, hp_config: Dict, report_func: Callable): # set multi process method as default - multiprocessing.set_start_method(None, True) + multiprocessing.set_start_method(None, True) # type: ignore train_func(hp_config, report_func) def _report_score( - score: int | float, - progress: int | float, + score: Union[int, float], + progress: Union[int, float], recv_queue: multiprocessing.Queue, send_queue: multiprocessing.Queue, - uid: Hashable, - trial_id: Hashable, + uid: Any, done: bool = False, -) -> TrialStatus: - logger.debug( - f"score : {score}, progress : {progress}, uid : {uid}, trial_id : {trial_id}, " - f"pid : {os.getpid()}, done : {done}", - ) +): + logger.debug(f"score : {score}, progress : {progress}, uid : {uid}, pid : {os.getpid()}, done : {done}") try: - send_queue.put_nowait( - { - "score": score, - "progress": progress, - "uid": uid, - "trial_id": trial_id, - "pid": os.getpid(), - "done": done, - }, - ) + send_queue.put_nowait({"score": score, "progress": progress, "uid": uid, "pid": os.getpid(), "done": done}) except ValueError: return TrialStatus.STOP @@ -244,24 +230,28 @@ def _report_score( def run_hpo_loop( hpo_algo: HpoBase, train_func: Callable, - resource_type: Literal["gpu", "cpu"] = "gpu", - num_parallel_trial: int | None = None, - num_gpu_for_single_trial: int | None = None, - available_gpu: str | None = None, -) -> None: + resource_type: Literal["gpu", "cpu", "xpu"] = "gpu", + num_parallel_trial: Optional[int] = None, + num_devices_per_trial: Optional[int] = None, + available_devices: Optional[str] = None, +): """Run the HPO loop. Args: hpo_algo (HpoBase): HPO algorithms. train_func (Callable): Function to train a model. - resource_type ('gpu' | 'cpu', optional): Which type of resource to use. + resource_type (Literal['gpu', 'cpu', 'xpu'], optional): Which type of resource to use. If can be changed depending on environment. Defaults to "gpu". - num_parallel_trial (int | None, optional): How many trials to run in parallel. - It's used for CPUResourceManager. Defaults to None. - num_gpu_for_single_trial (int | None, optional): How many GPUs are used for a single trial. - It's used for GPUResourceManager. Defaults to None. - available_gpu (str | None, optional): How many GPUs are available. It's used for GPUResourceManager. - Defaults to None. + num_parallel_trial (Optional[int], optional): How many trials to run in parallel. + It's used for CPUResourceManager. Defaults to None. + num_devices_per_trial (Optional[int], optional): Number of devices used for a single trial. + It's used for GPUResourceManager and XPUResourceManager. + Defaults to None. + available_devices (Optional[str], optional): Number of devices available. + It's used for GPUResourceManager and XPUResourceManager. + Defaults to None. """ - hpo_loop = HpoLoop(hpo_algo, train_func, resource_type, num_parallel_trial, num_gpu_for_single_trial, available_gpu) + hpo_loop = HpoLoop( + hpo_algo, train_func, resource_type, num_parallel_trial, num_devices_per_trial, available_devices + ) hpo_loop.run() diff --git a/src/otx/hpo/hyperband.py b/src/otx/hpo/hyperband.py index 82eb46d0036..49e5b5003ed 100644 --- a/src/otx/hpo/hyperband.py +++ b/src/otx/hpo/hyperband.py @@ -1,16 +1,24 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# """Hyperband implementation.""" -from __future__ import annotations +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. import json -import logging import math -from copy import copy -from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal +import os +from os import path as osp +from typing import Any, Dict, List, Literal, Optional, Union from scipy.stats.qmc import LatinHypercube @@ -21,17 +29,14 @@ check_positive, left_vlaue_is_better, ) +from otx.utils.logger import get_logger -if TYPE_CHECKING: - from collections.abc import Hashable +logger = get_logger() -logger = logging.getLogger(__name__) - -def _check_reduction_factor_value(reduction_factor: int) -> None: +def _check_reduction_factor_value(reduction_factor: int): if reduction_factor < 2: - error_msg = f"reduction_factor should be greater than 2.\nyour value : {reduction_factor}" - raise ValueError(error_msg) + raise ValueError("reduction_factor should be greater than 2.\n" f"your value : {reduction_factor}") class AshaTrial(Trial): @@ -39,40 +44,40 @@ class AshaTrial(Trial): Args: trial_id (Any): Id of the trial. - configuration (dict): Configuration for the trial. - train_environment (dict | None): Train environment for the trial. - For, example, subset ratio can be included. Defaults to None. + configuration (Dict): Configuration for the trial. + train_environment (Optional[Dict]): Train environment for the trial. + For, example, subset ratio can be included. Defaults to None. """ - def __init__(self, trial_id: Hashable, configuration: dict, train_environment: dict | None = None) -> None: + def __init__(self, trial_id: Any, configuration: Dict, train_environment: Optional[Dict] = None): super().__init__(trial_id, configuration, train_environment) - self._rung: int | None = None - self._bracket: int | None = None + self._rung: Optional[int] = None + self._bracket: Optional[int] = None self.estimating_max_resource: bool = False @property - def rung(self) -> int | None: + def rung(self): """Rung where the trial is included.""" return self._rung @rung.setter - def rung(self, val: int) -> None: + def rung(self, val: int): """Setter for rung.""" check_not_negative(val, "rung") self._rung = val @property - def bracket(self) -> int | None: + def bracket(self): """Bracket where the trial is inlcuded.""" return self._bracket @bracket.setter - def bracket(self, val: int) -> None: + def bracket(self, val: int): """Setter for bracket.""" check_not_negative(val, "bracket") self._bracket = val - def save_results(self, save_path: str | Path) -> None: + def save_results(self, save_path: str): """Save a result of the trial at 'save_path'.""" results = { "id": self.id, @@ -82,8 +87,7 @@ def save_results(self, save_path: str | Path) -> None: "score": self.score, } - save_path = Path(save_path) - with save_path.open("w", encoding="utf-8") as f: + with open(save_path, "w", encoding="utf-8") as f: json.dump(results, f) @@ -93,7 +97,7 @@ class Rung: Rung is in charge of selecting a trial to train and deciding which trial to promote to next rung in the bracket. Args: - resource (int | float): Resource to use for training a trial. + resource (Union[int, float]): Resource to use for training a trial. For example, something like epoch or iteration. num_required_trial (int): Necessary trials for the rung. reduction_factor (int): Decicdes how many trials to promote. @@ -103,7 +107,7 @@ class Rung: def __init__( self, - resource: int | float, + resource: Union[int, float], num_required_trial: int, reduction_factor: int, rung_idx: int, @@ -116,25 +120,25 @@ def __init__( self._reduction_factor = reduction_factor self._num_required_trial = num_required_trial self._resource = resource - self._trials: list[AshaTrial] = [] + self._trials: List[AshaTrial] = [] self._rung_idx = rung_idx @property - def num_required_trial(self) -> int: + def num_required_trial(self): """Number of required trials for the rung.""" return self._num_required_trial @property - def resource(self) -> int | float: + def resource(self): """Resource to use for training a trial.""" return self._resource @property - def rung_idx(self) -> int: + def rung_idx(self): """Current rung index.""" return self._rung_idx - def add_new_trial(self, trial: AshaTrial) -> None: + def add_new_trial(self, trial: AshaTrial): """Add a new trial to the rung. Args: @@ -144,22 +148,21 @@ def add_new_trial(self, trial: AshaTrial) -> None: RuntimeError: If no more trial is needed, raise an error. """ if not self.need_more_trials(): - error_msg = f"{self.rung_idx} rung has already sufficient trials." - raise RuntimeError(error_msg) + raise RuntimeError(f"{self.rung_idx} rung has already sufficient trials.") trial.iteration = self.resource trial.rung = self.rung_idx trial.status = TrialStatus.READY self._trials.append(trial) - def get_best_trial(self, mode: Literal["max", "min"] = "max") -> AshaTrial | None: + def get_best_trial(self, mode: str = "max") -> Optional[AshaTrial]: """Get best trial in the rung. Args: - mode ("max" | "min", optional): Decide which trial is better between having highest score or lowest score. - Defaults to "max". + mode (str, optional): Decide which trial is better between having highest score or lowest score. + Defaults to "max". Returns: - AshaTrial | None: Best trial. If there is no trial, return None. + Optional[AshaTrial]: Best trial. If there is no trial, return None. """ check_mode_input(mode) best_score = None @@ -186,22 +189,21 @@ def is_done(self) -> bool: """Check that the rung is done.""" if self.need_more_trials(): return False - return all(trial.is_done() for trial in self._trials) + for trial in self._trials: + if not trial.is_done(): + return False + return True - def get_trial_to_promote( - self, - asynchronous_sha: bool = False, - mode: Literal["max", "min"] = "max", - ) -> AshaTrial | None: + def get_trial_to_promote(self, asynchronous_sha: bool = False, mode: str = "max") -> Optional[AshaTrial]: """Get a trial to promote. Args: asynchronous_sha (bool, optional): Whether to operate SHA asynchronously. Defaults to False. - mode ("max" | "min", optional): Decide which trial is better between having highest score or lowest score. + mode (str, optional): Decide which trial is better between having highest score or lowest score. Defaults to "max". Returns: - AshaTrial | None: Trial to prmote. If there is no trial to promote, return None. + Optional[AshaTrial]: Trial to prmote. If there is no trial to promote, return None. """ num_finished_trial = 0 num_promoted_trial = 0 @@ -227,11 +229,11 @@ def get_trial_to_promote( return None - def get_next_trial(self) -> AshaTrial | None: + def get_next_trial(self) -> Optional[AshaTrial]: """Get next trial to trian. Returns: - AshaTrial | None: Next trial to train. If there is no left trial to train, then return None. + Optional[AshaTrial]: Next trial to train. If there is no left trial to train, then return None. """ for trial in self._trials: if not trial.is_done() and trial.status != TrialStatus.RUNNING: @@ -244,13 +246,13 @@ class Bracket: Args: bracket_id (int): Bracket id. - minimum_resource (float | int): Maximum resource to use for training a trial. - maximum_resource (float | int): Minimum resource to use for training a trial. - hyper_parameter_configurations (list[AshaTrial]): Hyper parameter configuration to try. + minimum_resource (Union[float, int]): Maximum resource to use for training a trial. + maximum_resource (Union[float, int]): Minimum resource to use for training a trial. + hyper_parameter_configurations (List[AshaTrial]): Hyper parameter configuration to try. reduction_factor (int): Decicdes how many trials to promote to next rung. Only top 1 / reduction_factor of rung trials can be promoted. - mode (Literal["max", "min"], optional): Decide which trial is better between having highest score - or lowest score. Defaults to "max". + mode (str, optional): Decide which trial is better between having highest score or lowest score. + Defaults to "max". asynchronous_sha (bool, optional): Whether to operate SHA asynchronously. Defaults to True. """ @@ -259,11 +261,11 @@ class Bracket: def __init__( self, bracket_id: int, - minimum_resource: float | int, - maximum_resource: float | int, - hyper_parameter_configurations: list[AshaTrial], + minimum_resource: Union[float, int], + maximum_resource: Union[float, int], + hyper_parameter_configurations: List[AshaTrial], reduction_factor: int = 3, - mode: Literal["max", "min"] = "max", + mode: str = "max", asynchronous_sha: bool = True, ): # pylint: disable=too-many-arguments @@ -273,54 +275,51 @@ def __init__( self._id = bracket_id self._minimum_resource = minimum_resource - self._maximum_resource = maximum_resource + self.maximum_resource = maximum_resource self._reduction_factor = reduction_factor self._mode = mode self._asynchronous_sha = asynchronous_sha - self._trials: dict[Hashable, AshaTrial] = {} - self._rungs: list[Rung] = self._initialize_rungs(hyper_parameter_configurations) + self._trials: Dict[int, AshaTrial] = {} + self._rungs: List[Rung] = self._initialize_rungs(hyper_parameter_configurations) @property - def id(self) -> int: # noqa: A003 + def id(self): """Bracket id.""" return self._id @property - def maximum_resource(self) -> float | int: + def maximum_resource(self): """Maximum resource to use for training a trial.""" return self._maximum_resource @maximum_resource.setter - def maximum_resource(self, val: float | int) -> None: + def maximum_resource(self, val: Union[float, int]): """Setter for maximum_resource.""" check_positive(val, "maximum_resource") if val < self._minimum_resource: - error_msg = ( + raise ValueError( "maxnum_resource should be greater than minimum_resource.\n" - f"value to set : {val}, minimum_resource : {self._minimum_resource}", + f"value to set : {val}, minimum_resource : {self._minimum_resource}" ) - raise ValueError(error_msg) if val == self._minimum_resource: logger.warning("maximum_resource is same with the minimum_resource.") self._maximum_resource = val @property - def max_rung(self) -> int: + def max_rung(self): """Number of rungs the bracket has.""" return self.calcuate_max_rung_idx(self._minimum_resource, self.maximum_resource, self._reduction_factor) @staticmethod def calcuate_max_rung_idx( - minimum_resource: float | int, - maximum_resource: float | int, - reduction_factor: int, + minimum_resource: Union[float, int], maximum_resource: Union[float, int], reduction_factor: int ) -> int: """Calculate the number of rungs the bracket needs. Args: - minimum_resource (float | int]): Minimum resource to use for training a trial. - maximum_resource (float | int): Maximum resource to use for training a trial. + minimum_resource (Union[float, int]): Minimum resource to use for training a trial. + maximum_resource (Union[float, int]): Maximum resource to use for training a trial. reduction_factor (int): Decicdes how many trials to promote to next rung. Only top 1 / reduction_factor of rung trials can be promoted. @@ -334,25 +333,23 @@ def calcuate_max_rung_idx( check_positive(maximum_resource, "maximum_resource") check_positive(reduction_factor, "reduction_factor") if minimum_resource > maximum_resource: - error_msg = ( + raise ValueError( "maximum_resource should be bigger than minimum_resource. " - f"but minimum_resource : {minimum_resource} / maximum_resource : {maximum_resource}", + f"but minimum_resource : {minimum_resource} / maximum_resource : {maximum_resource}" ) - raise ValueError(error_msg) return math.ceil(math.log(maximum_resource / minimum_resource, reduction_factor)) - def _initialize_rungs(self, hyper_parameter_configurations: list[AshaTrial]) -> list[Rung]: + def _initialize_rungs(self, hyper_parameter_configurations: List[AshaTrial]): num_trials = len(hyper_parameter_configurations) minimum_num_trials = self._reduction_factor**self.max_rung if minimum_num_trials > num_trials: - error_msg = ( + raise ValueError( "number of hyper_parameter_configurations is not enough. " f"minimum number is {minimum_num_trials}, but current number is {num_trials}. " "if you want to let them be, you can decrease needed number " - "by increasing reduction factor or minimum resource.", + "by increasing reduction factor or minimum resource." ) - raise ValueError(error_msg) rungs = [ Rung( @@ -371,7 +368,7 @@ def _initialize_rungs(self, hyper_parameter_configurations: list[AshaTrial]) -> return rungs - def _promote_trial_if_available(self, rung_idx: int) -> AshaTrial | None: + def _promote_trial_if_available(self, rung_idx: int): check_not_negative(rung_idx, "rung_idx") if self.max_rung <= rung_idx: @@ -383,11 +380,11 @@ def _promote_trial_if_available(self, rung_idx: int) -> AshaTrial | None: return best_trial - def get_next_trial(self) -> AshaTrial | None: + def get_next_trial(self) -> Optional[AshaTrial]: """Get next trial to train. Returns: - AshaTrial | None: Next trial to train. There is no trial to train, then return None. + Optional[AshaTrial]: Next trial to train. There is no trial to train, then return None. """ current_rung = self.max_rung while current_rung >= 0: @@ -415,7 +412,7 @@ def is_done(self) -> bool: """ return self._rungs[-1].is_done() - def get_best_trial(self) -> AshaTrial | None: + def get_best_trial(self) -> Optional[AshaTrial]: """Get best trial in the bracket. Returns: @@ -433,21 +430,20 @@ def get_best_trial(self) -> AshaTrial | None: return trial - def save_results(self, save_path: str | Path) -> None: + def save_results(self, save_path: str): """Save a bracket result to 'save_path'. Args: save_path (str): Path where to save a bracket result. """ result = self._get_result() - save_path = Path(save_path) - with (save_path / "rung_status.json").open("w", encoding="utf-8") as f: + with open(osp.join(save_path, "rung_status.json"), "w", encoding="utf-8") as f: json.dump(result, f) for trial_id, trial in self._trials.items(): - trial.save_results(save_path / f"{trial_id}.json") + trial.save_results(osp.join(save_path, f"{trial_id}.json")) - def print_result(self) -> None: + def print_result(self): """Print a bracket result.""" print("*" * 20, f"{self.id} bracket", "*" * 20) result = self._get_result() @@ -461,7 +457,7 @@ def print_result(self) -> None: return print( f"best trial:\n" - f"id : {best_trial.id} / score : {best_trial.get_best_score()} / config : {best_trial.configuration}", + f"id : {best_trial.id} / score : {best_trial.get_best_score()} / config : {best_trial.configuration}" ) print("all trials:") @@ -469,7 +465,7 @@ def print_result(self) -> None: print(f"id : {trial.id} / score : {trial.get_best_score()} / config : {trial.configuration}") print() - def _get_result(self) -> dict[str, Any]: + def _get_result(self): return { "minimum_resource": self._minimum_resource, "maximum_resource": self.maximum_resource, @@ -502,7 +498,7 @@ class HyperBand(HpoBase): https://arxiv.org/abs/1810.05934 Args: - minimum_resource (float | int | None, optional): Minimum resource to use for training a trial. Defaults to None. + minimum_resource (Union[float, int]): Minimum resource to use for training a trial. Defaults to None. reduction_factor (int, optional): Decicdes how many trials to promote to next rung. Only top 1 / reduction_factor of rung trials can be promoted. Defaults to 3. asynchronous_sha (bool, optional): Whether to operate SHA asynchronously. Defaults to True. @@ -514,12 +510,12 @@ class HyperBand(HpoBase): def __init__( self, - minimum_resource: int | float | None = None, + minimum_resource: Optional[Union[int, float]] = None, reduction_factor: int = 3, asynchronous_sha: bool = True, asynchronous_bracket: bool = False, **kwargs, - ) -> None: + ): super().__init__(**kwargs) if minimum_resource is not None: @@ -531,8 +527,8 @@ def __init__( self._minimum_resource = minimum_resource self._asynchronous_sha = asynchronous_sha self._asynchronous_bracket = asynchronous_bracket - self._trials: dict[Hashable, AshaTrial] = {} - self._brackets: dict[int, Bracket] = {} + self._trials: Dict[str, AshaTrial] = {} + self._brackets: Dict[int, Bracket] = {} if not self._need_to_find_resource_value(): self._brackets = self._make_brackets() @@ -540,14 +536,14 @@ def __init__( def _need_to_find_resource_value(self) -> bool: return self.maximum_resource is None or self._minimum_resource is None - def _make_brackets(self) -> dict[int, Bracket]: + def _make_brackets(self) -> Dict[int, Bracket]: if self.expected_time_ratio is None: brackets_config = self._make_default_brackets_setting() else: brackets_config = self.auto_config() return self._make_brackets_as_config(brackets_config) - def _calculate_bracket_resource(self, num_max_rung_trials: int, bracket_index: int) -> int | float: + def _calculate_bracket_resource(self, num_max_rung_trials: int, bracket_index: int) -> Union[int, float]: """Calculate how much resource is needed for the bracket given that resume is available.""" num_trial = self._calculate_num_bracket_trials(num_max_rung_trials, bracket_index) minimum_resource = self.maximum_resource * (self._reduction_factor**-bracket_index) @@ -555,9 +551,7 @@ def _calculate_bracket_resource(self, num_max_rung_trials: int, bracket_index: i total_resource = 0 num_rungs = ( Bracket.calcuate_max_rung_idx( - minimum_resource, - self.maximum_resource, # type: ignore[arg-type] - self._reduction_factor, + minimum_resource, self.maximum_resource, self._reduction_factor # type: ignore ) + 1 ) @@ -582,18 +576,21 @@ def _get_num_max_rung_trials(self, bracket_idx: int) -> int: def _calculate_s_max(self) -> int: return math.floor( - math.log(self.maximum_resource / self._minimum_resource, self._reduction_factor), # type: ignore[operator] + math.log(self.maximum_resource / self._minimum_resource, self._reduction_factor) # type: ignore ) - def _make_default_brackets_setting(self) -> list[dict[str, Any]]: + def _make_default_brackets_setting(self) -> List[Dict[str, Any]]: # Bracket order is the opposite of order of paper's. # This is for running default hyper parmeters with abundant resource. - return [ - {"bracket_index": idx, "num_trials": self._calculate_origin_num_trial_for_bracket(idx)} - for idx in range(self._calculate_s_max() + 1) - ] + brackets_setting = [] + for idx in range(self._calculate_s_max() + 1): + brackets_setting.append( + {"bracket_index": idx, "num_trials": self._calculate_origin_num_trial_for_bracket(idx)} + ) + + return brackets_setting - def _make_brackets_as_config(self, brackets_settings: list[dict[str, Any]]) -> dict[int, Bracket]: + def _make_brackets_as_config(self, brackets_settings: List[Dict]) -> Dict[int, Bracket]: brackets = {} total_num_trials = 0 for bracket_setting in brackets_settings: @@ -627,7 +624,7 @@ def _make_brackets_as_config(self, brackets_settings: list[dict[str, Any]]) -> d bracket = Bracket( bracket_idx, minimum_resource, - self.maximum_resource, # type: ignore[arg-type] + self.maximum_resource, # type: ignore bracket_configurations, self._reduction_factor, self.mode, @@ -637,10 +634,10 @@ def _make_brackets_as_config(self, brackets_settings: list[dict[str, Any]]) -> d return brackets - def _make_new_hyper_parameter_configs(self, num: int) -> list[AshaTrial]: + def _make_new_hyper_parameter_configs(self, num: int) -> List[AshaTrial]: check_not_negative(num, "num") - hp_configs: list[AshaTrial] = [] + hp_configs: List[AshaTrial] = [] if num == 0: return hp_configs @@ -650,7 +647,7 @@ def _make_new_hyper_parameter_configs(self, num: int) -> list[AshaTrial]: return hp_configs - def _get_prior_hyper_parameters(self, num_samples: int) -> list[AshaTrial]: + def _get_prior_hyper_parameters(self, num_samples: int) -> List[AshaTrial]: hp_configs = [] num_samples = min([num_samples, len(self.prior_hyper_parameters)]) for _ in range(num_samples): @@ -659,19 +656,19 @@ def _get_prior_hyper_parameters(self, num_samples: int) -> list[AshaTrial]: return hp_configs - def _get_random_hyper_parameter(self, num_samples: int) -> list[AshaTrial]: + def _get_random_hyper_parameter(self, num_samples: int) -> List[AshaTrial]: hp_configs = [] latin_hypercube = LatinHypercube(len(self.search_space)) configurations = latin_hypercube.random(num_samples) for config in configurations: config_with_key = {key: config[idx] for idx, key in enumerate(self.search_space)} hp_configs.append( - self._make_trial(self.search_space.convert_from_zero_one_scale_to_real_space(config_with_key)), + self._make_trial(self.search_space.convert_from_zero_one_scale_to_real_space(config_with_key)) ) return hp_configs - def _make_trial(self, hyper_parameter: dict) -> AshaTrial: + def _make_trial(self, hyper_parameter: Dict) -> AshaTrial: trial_id = self._get_new_trial_id() trial = AshaTrial(trial_id, hyper_parameter, self._get_train_environment()) self._trials[trial_id] = trial @@ -682,14 +679,15 @@ def _get_new_trial_id(self) -> str: self._next_trial_id += 1 return str(trial_id) - def _get_train_environment(self) -> dict: - return {"subset_ratio": self.subset_ratio} + def _get_train_environment(self) -> Dict: + train_environment = {"subset_ratio": self.subset_ratio} + return train_environment - def get_next_sample(self) -> AshaTrial | None: + def get_next_sample(self) -> Optional[AshaTrial]: """Get next trial to train. Returns: - AshaTrial | None: Next trial to train. If there is no trial to train, then return None. + Optional[AshaTrial]: Next trial to train. If there is no trial to train, then return None. """ if not self._brackets: return self._make_trial_to_estimate_resource() @@ -720,21 +718,21 @@ def _make_trial_to_estimate_resource(self) -> AshaTrial: trial.iteration = self.maximum_resource return trial - def save_results(self) -> None: + def save_results(self): """Save a ASHA result.""" for idx, bracket in self._brackets.items(): - save_path = Path(self.save_path) / str(idx) - save_path.mkdir(parents=True, exist_ok=True) - bracket.save_results(str(save_path)) + save_path = osp.join(self.save_path, str(idx)) + os.makedirs(save_path, exist_ok=True) + bracket.save_results(save_path) - def auto_config(self) -> list[dict[str, Any]]: + def auto_config(self) -> List[Dict[str, Any]]: """Configure ASHA automatically aligning with possible resource. Configure ASHA automatically. If resource is lesser than full ASHA, decrease ASHA scale. In contrast, resource is more than full ASHA, increase ASHA scale. Returns: - list[dict[str, Any]]: ASHA configuration. It's used to make brackets. + List[Dict[str, Any]]: ASHA configuration. It's used to make brackets. """ if self._trials: self._adjust_minimum_resource() @@ -742,13 +740,13 @@ def auto_config(self) -> list[dict[str, Any]]: return self._decrease_hyperband_scale() return self._increase_hyperband_scale() - def _adjust_minimum_resource(self) -> None: + def _adjust_minimum_resource(self): """Set meaningful minimum resource. - Purpose of this function is to avoid setting minimum resource too low + Goal of this function is to avoid setting minimum resource too low to distinguish which trial is better. """ - if self.maximum_resource < self._reduction_factor: # type: ignore[operator] + if self.maximum_resource < self._reduction_factor: logger.debug("maximum_resource is lesser than reduction factor. adjusting minimum resource is skipped.") return @@ -760,11 +758,11 @@ def _adjust_minimum_resource(self) -> None: logger.debug("There is no finished trial. adjusting minimum resource is skipped.") return - cur_score: int | float = 0 - best_score: int | float = 0 - minimum_resource: int | float = 0 + cur_score = 0 + best_score = 0 + minimum_resource = 0 for resource, score in trial.score.items(): - if resource > self.maximum_resource // self._reduction_factor: # type: ignore[operator] + if resource > self.maximum_resource // self._reduction_factor: break cur_score = cur_score * 0.5 + score * 0.5 if not left_vlaue_is_better(best_score, cur_score, self.mode): @@ -775,11 +773,11 @@ def _adjust_minimum_resource(self) -> None: minimum_resource = 0 if minimum_resource == 0: - minimum_resource = self.maximum_resource // self._reduction_factor # type: ignore[operator] + minimum_resource = self.maximum_resource // self._reduction_factor self._minimum_resource = minimum_resource - def _get_full_asha_resource(self) -> int | float: - total_resource: int | float = 0 + def _get_full_asha_resource(self) -> Union[int, float]: + total_resource: Union[int, float] = 0 for idx in range(self._calculate_s_max() + 1): num_max_rung_trials = self._get_num_max_rung_trials(idx) total_resource += self._calculate_bracket_resource(num_max_rung_trials, idx) @@ -793,22 +791,22 @@ def _need_to_dcrease_hyerpband_scale(self) -> bool: return self._get_full_asha_resource() > self._get_expected_total_resource() - def _decrease_hyperband_scale(self) -> list[dict[str, Any]]: + def _decrease_hyperband_scale(self) -> List[Dict[str, Any]]: """Decrease Hyperband scale. From bracket which has biggest number of rung, check that it's resource exceeds expected_time_ratio if bracket is added. If not, bracket is added. If it does, check that number of trials for bracket can be reduced. if not, skip that bracket and check that next bracket can be added by same method. """ - brackets_setting: list[dict[str, Any]] = [] - total_resource: int | float = 0 + brackets_setting: List = [] + total_resource: Union[int, float] = 0 resource_upper_bound = self._get_expected_total_resource() - reserved_resource: int | float = 0 + reserved_resource: Union[int, float] = 0 if self._trials: # reserve resources for trials which should be run on bracket 0 for trial in self._trials.values(): if trial.bracket == 0: - reserved_resource += self.maximum_resource # type: ignore[operator] + reserved_resource += self.maximum_resource # type: ignore total_resource += reserved_resource for idx in range(self._calculate_s_max(), -1, -1): @@ -827,10 +825,9 @@ def _decrease_hyperband_scale(self) -> list[dict[str, Any]]: return brackets_setting - def _get_expected_total_resource(self) -> float | int: + def _get_expected_total_resource(self): if self.expected_time_ratio is None: - error_msg = "expected time ratio should be set to get expceted total resource" - raise ValueError(error_msg) + raise ValueError("expected time ratio should be set to get expceted total resource") return ( self.num_full_iterations * self.expected_time_ratio @@ -838,13 +835,13 @@ def _get_expected_total_resource(self) -> float | int: * self.num_workers ) - def _increase_hyperband_scale(self) -> list[dict[str, Any]]: - total_resource: int | float = 0 + def _increase_hyperband_scale(self) -> List[Dict[str, Any]]: + total_resource: Union[int, float] = 0 bracket_status = {} s_max = self._calculate_s_max() # If all brackets can run more than one, then multiply number of trials on each bracket as many as possible - sum_unit_resource: int | float = 0 + sum_unit_resource: Union[int, float] = 0 for idx in range(s_max + 1): num_max_rung_trials = self._get_num_max_rung_trials(idx) unit_resource = self._calculate_bracket_resource(1, idx) @@ -872,25 +869,27 @@ def _increase_hyperband_scale(self) -> list[dict[str, Any]]: break # set brackets setting - return [ - { - "bracket_index": idx, - "num_trials": self._calculate_num_bracket_trials( - bracket_status[idx]["num_max_rung_trials"], # type: ignore[arg-type] - idx, - ), - } - for idx in range(s_max + 1) - ] + brackets_setting = [] + for idx in range(s_max + 1): + brackets_setting.append( + { + "bracket_index": idx, + "num_trials": self._calculate_num_bracket_trials( + bracket_status[idx]["num_max_rung_trials"], idx # type: ignore + ), + } + ) + + return brackets_setting - def _get_used_resource(self) -> int | float: - used_resource: int | float = 0 + def _get_used_resource(self) -> Union[int, float]: + used_resource: Union[int, float] = 0 for trial in self._trials.values(): used_resource += trial.get_progress() return used_resource - def get_progress(self) -> int | float: + def get_progress(self) -> Union[int, float]: """Get current progress of ASHA.""" if self.is_done(): return 1 @@ -905,17 +904,13 @@ def get_progress(self) -> int | float: return min(progress, 0.99) def report_score( - self, - score: float | int, - resource: float | int, - trial_id: Hashable, - done: bool = False, - ) -> TrialStatus: + self, score: Union[float, int], resource: Union[float, int], trial_id: str, done: bool = False + ) -> Literal[TrialStatus.STOP, TrialStatus.RUNNING]: """Report a score to ASHA. Args: - score (float | int): Score to report. - resource (float | int): Resource used to get score. + score (Union[float, int]): Score to report. + resource (Union[float, int]): Resource used to get score. trial_id (str): Trial id. done (bool, optional): Whether training trial is done. Defaults to False. @@ -949,13 +944,16 @@ def is_done(self) -> bool: """ if not self._brackets: return False - return all(bracket.is_done() for bracket in self._brackets.values()) + for bracket in self._brackets.values(): + if not bracket.is_done(): + return False + return True - def get_best_config(self) -> dict[str, Any] | None: + def get_best_config(self) -> Optional[Dict[str, Any]]: """Get best configuration in ASHA. Returns: - dict[str, Any] | None: Best configuration in ASHA. If there is no trial to select, return None. + Optional[Dict[str, Any]]: Best configuration in ASHA. If there is no trial to select, return None. """ best_score = None best_trial = None @@ -968,18 +966,15 @@ def get_best_config(self) -> dict[str, Any] | None: if best_trial is None: return None - config = copy(best_trial.configuration) - if "iterations" in config: - config.pop("iterations") - return {"id": best_trial.id, "configuration": config} + return {"id": best_trial.id, "config": best_trial.configuration} - def print_result(self) -> None: + def print_result(self): """Print a ASHA result.""" print( "HPO(ASHA) result summary\n" f"Best config : {self.get_best_config()}.\n" f"Hyper band runs {len(self._brackets)} brackets.\n" - "Brackets summary:", + "Brackets summary:" ) for bracket in self._brackets.values(): bracket.print_result() diff --git a/src/otx/hpo/resource_manager.py b/src/otx/hpo/resource_manager.py index 653da358b97..3971613d170 100644 --- a/src/otx/hpo/resource_manager.py +++ b/src/otx/hpo/resource_manager.py @@ -1,40 +1,47 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# """Resource manager class for HPO runner.""" -from __future__ import annotations +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. -import logging import os from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Literal +from typing import Any, Dict, List, Literal, Optional import torch +from otx.algorithms.common.utils import is_xpu_available from otx.hpo.utils import check_positive +from otx.utils.logger import get_logger -if TYPE_CHECKING: - from collections.abc import Hashable - -logger = logging.getLogger(__name__) +logger = get_logger() class BaseResourceManager(ABC): """Abstract class for resource manager class.""" @abstractmethod - def reserve_resource(self, trial_id: Hashable) -> dict | None: + def reserve_resource(self, trial_id): """Reserve a resource.""" raise NotImplementedError @abstractmethod - def release_resource(self, trial_id: Hashable) -> None: + def release_resource(self, trial_id): """Release a resource.""" raise NotImplementedError @abstractmethod - def have_available_resource(self) -> bool: + def have_available_resource(self): """Check that there is available resource.""" raise NotImplementedError @@ -46,13 +53,13 @@ class CPUResourceManager(BaseResourceManager): num_parallel_trial (int, optional): How many trials to run in parallel. Defaults to 4. """ - def __init__(self, num_parallel_trial: int = 4) -> None: + def __init__(self, num_parallel_trial: int = 4): check_positive(num_parallel_trial, "num_parallel_trial") self._num_parallel_trial = num_parallel_trial - self._usage_status: list[Any] = [] + self._usage_status: List = [] - def reserve_resource(self, trial_id: Hashable) -> dict | None: + def reserve_resource(self, trial_id: Any) -> Optional[Dict]: """Reserve a resource under 'trial_id'. Args: @@ -64,14 +71,13 @@ def reserve_resource(self, trial_id: Hashable) -> dict | None: if not self.have_available_resource(): return None if trial_id in self._usage_status: - error_msg = f"{trial_id} already has reserved resource." - raise RuntimeError(error_msg) + raise RuntimeError(f"{trial_id} already has reserved resource.") logger.debug(f"{trial_id} reserved.") self._usage_status.append(trial_id) return {} - def release_resource(self, trial_id: Hashable) -> None: + def release_resource(self, trial_id: Any): """Release a resource under 'trial_id'. Args: @@ -83,47 +89,31 @@ def release_resource(self, trial_id: Hashable) -> None: self._usage_status.remove(trial_id) logger.debug(f"{trial_id} released.") - def have_available_resource(self) -> bool: + def have_available_resource(self): """Check that there is available resource.""" return len(self._usage_status) < self._num_parallel_trial -class GPUResourceManager(BaseResourceManager): - """Resource manager class for GPU. +class AcceleratorManager(BaseResourceManager): + """Abstract Resource manager class for accelerators. Args: - num_gpu_for_single_trial (int, optional): How many GPUs is used for a single trial. Defaults to 1. - available_gpu (str | None, optional): How many GPUs are available. Defaults to None. + num_devices_per_trial (int, optional): Number of devices used for a single trial. Defaults to 1. + available_devices (Optional[str], optional): Number of devices available. Defaults to None. """ - def __init__(self, num_gpu_for_single_trial: int = 1, available_gpu: str | None = None) -> None: - check_positive(num_gpu_for_single_trial, "num_gpu_for_single_trial") + def __init__(self, num_devices_per_trial: int = 1, available_devices: Optional[str] = None): + check_positive(num_devices_per_trial, "num_devices_per_trial") - self._num_gpu_for_single_trial = num_gpu_for_single_trial - self._available_gpu = self._set_available_gpu(available_gpu) - self._usage_status: dict[Any, list] = {} + self._num_devices_per_trial = num_devices_per_trial + self._available_devices = self._set_available_devices(available_devices) + self._usage_status: Dict[Any, List] = {} - def _set_available_gpu(self, available_gpu: str | None = None) -> list[int]: - if available_gpu is None: - cuda_visible_devices = os.getenv("CUDA_VISIBLE_DEVICES") - if cuda_visible_devices is not None: - available_gpu_arr = self._transform_gpu_format_from_string_to_arr(cuda_visible_devices) - else: - num_gpus = torch.cuda.device_count() - available_gpu_arr = list(range(num_gpus)) - else: - available_gpu_arr = self._transform_gpu_format_from_string_to_arr(available_gpu) - - return available_gpu_arr - - def _transform_gpu_format_from_string_to_arr(self, gpu: str) -> list[int]: - for val in gpu.split(","): - if not val.isnumeric(): - error_msg = f"gpu format is wrong. gpu should only have numbers delimited by ','.\nyour value is {gpu}" - raise ValueError(error_msg) - return [int(val) for val in gpu.split(",")] + @abstractmethod + def _set_available_devices(self, available_devices: Optional[str] = None) -> List[int]: + raise NotImplementedError - def reserve_resource(self, trial_id: Hashable) -> dict | None: + def reserve_resource(self, trial_id: Any) -> Optional[Dict]: """Reserve a resource under 'trial_id'. Args: @@ -133,21 +123,24 @@ def reserve_resource(self, trial_id: Hashable) -> dict | None: RuntimeError: If there is already resource reserved by 'trial_id', then raise an error. Returns: - dict | None: Training environment to use. + Optional[Dict]: Training environment to use. """ if not self.have_available_resource(): return None if trial_id in self._usage_status: - error_msg = f"{trial_id} already has reserved resource." - raise RuntimeError(error_msg) + raise RuntimeError(f"{trial_id} already has reserved resource.") - resource = list(self._available_gpu[: self._num_gpu_for_single_trial]) - self._available_gpu = self._available_gpu[self._num_gpu_for_single_trial :] + resource = list(self._available_devices[: self._num_devices_per_trial]) + self._available_devices = self._available_devices[self._num_devices_per_trial :] self._usage_status[trial_id] = resource - return {"CUDA_VISIBLE_DEVICES": ",".join([str(val) for val in resource])} + return self._make_env_var_for_train(resource) - def release_resource(self, trial_id: Hashable) -> None: + @abstractmethod + def _make_env_var_for_train(self, device_arr: List[int]) -> Dict[str, str]: + raise NotImplementedError + + def release_resource(self, trial_id: Any): """Release a resource under 'trial_id'. Args: @@ -156,56 +149,112 @@ def release_resource(self, trial_id: Hashable) -> None: if trial_id not in self._usage_status: logger.warning(f"{trial_id} trial don't use resource now.") else: - self._available_gpu.extend(self._usage_status[trial_id]) + self._available_devices.extend(self._usage_status[trial_id]) del self._usage_status[trial_id] - def have_available_resource(self) -> bool: + def have_available_resource(self): """Check that there is available resource.""" - return len(self._available_gpu) >= self._num_gpu_for_single_trial + return len(self._available_devices) >= self._num_devices_per_trial + + +class GPUResourceManager(AcceleratorManager): + """Resource manager class for GPU.""" + + def _set_available_devices(self, available_devices: Optional[str] = None) -> List[int]: + if available_devices is None: + cuda_visible_devices = os.getenv("CUDA_VISIBLE_DEVICES") + if cuda_visible_devices is not None: + available_devices_arr = _cvt_comma_delimited_str_to_list(cuda_visible_devices) + else: + num_gpus = torch.cuda.device_count() + available_devices_arr = list(range(num_gpus)) + else: + available_devices_arr = _cvt_comma_delimited_str_to_list(available_devices) + + return available_devices_arr + + def _make_env_var_for_train(self, device_arr: List[int]) -> Dict[str, str]: + return {"CUDA_VISIBLE_DEVICES": ",".join([str(val) for val in device_arr])} + + +class XPUResourceManager(AcceleratorManager): + """Resource manager class for XPU.""" + + def _set_available_devices(self, available_devices: Optional[str] = None) -> List[int]: + if available_devices is None: + visible_devices = os.getenv("ONEAPI_DEVICE_SELECTOR", "").split(":") + if len(visible_devices) > 1: + available_devices_arr = _cvt_comma_delimited_str_to_list(visible_devices[1]) + else: + num_gpus = torch.xpu.device_count() + available_devices_arr = list(range(num_gpus)) + else: + available_devices_arr = _cvt_comma_delimited_str_to_list(available_devices) + + return available_devices_arr + + def _make_env_var_for_train(self, device_arr: List[int]) -> Dict[str, str]: + return {"ONEAPI_DEVICE_SELECTOR": "level_zero:" + ",".join([str(val) for val in device_arr])} def get_resource_manager( - resource_type: Literal["gpu", "cpu"], - num_parallel_trial: int | None = None, - num_gpu_for_single_trial: int | None = None, - available_gpu: str | None = None, + resource_type: Literal["gpu", "cpu", "xpu"], + num_parallel_trial: Optional[int] = None, + num_devices_per_trial: Optional[int] = None, + available_devices: Optional[str] = None, ) -> BaseResourceManager: """Get an appropriate resource manager depending on current environment. Args: - resource_type (Literal["gpu", "cpu"]): Which type of resource to use. + resource_type (Literal["gpu", "cpu", "xpu"]): Which type of resource to use. If can be changed depending on environment. - num_parallel_trial (int | None, optional): How many trials to run in parallel. It's used for CPUResourceManager. + num_parallel_trial (Optional[int]): How many trials to run in parallel. It's used for CPUResourceManager. + Defaults to None. + num_devices_per_trial (Optional[int]): How many GPUs is used for a single trial. + It's used for GPUResourceManager. Defaults to None. + available_devices (Optional[str]): How many GPUs are available. It's used for GPUResourceManager. Defaults to None. - num_gpu_for_single_trial (int | None, optional): How many GPUs is used for a single trial. - It's used for GPUResourceManager. Defaults to None. - available_gpu (str | None, optional): How many GPUs are available. It's used for GPUResourceManager. - Defaults to None. Raises: - ValueError: If resource_type is neither 'gpu' nor 'cpu', then raise an error. + ValueError: If resource_type is neither 'gpu', 'cpu', nor 'xpu' then raise an error. Returns: BaseResourceManager: Resource manager to use. """ - if resource_type == "gpu" and not torch.cuda.is_available(): - logger.warning("GPU can't be used now. resource type is modified to cpu.") + if (resource_type == "gpu" and not torch.cuda.is_available()) or ( + resource_type == "xpu" and not is_xpu_available() + ): + logger.warning("{} can't be used now. resource type is modified to cpu.".format(resource_type)) resource_type = "cpu" if resource_type == "cpu": args = {"num_parallel_trial": num_parallel_trial} args = _remove_none_from_dict(args) - return CPUResourceManager(**args) # type: ignore[arg-type] + return CPUResourceManager(**args) # type: ignore if resource_type == "gpu": - args = {"num_gpu_for_single_trial": num_gpu_for_single_trial, "available_gpu": available_gpu} # type: ignore[dict-item] + args = {"num_devices_per_trial": num_devices_per_trial, "available_devices": available_devices} # type: ignore + args = _remove_none_from_dict(args) + return GPUResourceManager(**args) # type: ignore + if resource_type == "xpu": + args = {"num_devices_per_trial": num_devices_per_trial, "available_devices": available_devices} # type: ignore args = _remove_none_from_dict(args) - return GPUResourceManager(**args) # type: ignore[arg-type] - error_msg = f"Available resource type is cpu, gpu. Your value is {resource_type}." - raise ValueError(error_msg) + return XPUResourceManager(**args) # type: ignore + raise ValueError(f"Available resource type is cpu, gpu. Your value is {resource_type}.") -def _remove_none_from_dict(dict_val: dict) -> dict: +def _remove_none_from_dict(dict_val: Dict): key_to_remove = [key for key, val in dict_val.items() if val is None] for key in key_to_remove: del dict_val[key] return dict_val + + +def _cvt_comma_delimited_str_to_list(string: str): + for val in string.split(","): + if not val.isnumeric(): + raise ValueError( + "string format is wrong. " + "string should only have numbers delimited by ','.\n" + f"your value is {string}" + ) + return [int(val) for val in string.split(",")] diff --git a/src/otx/hpo/search_space.py b/src/otx/hpo/search_space.py index 912e7efccf6..acdcf657f0b 100644 --- a/src/otx/hpo/search_space.py +++ b/src/otx/hpo/search_space.py @@ -1,18 +1,28 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# """Search space class for HPO.""" -from __future__ import annotations +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + -import logging import math import typing -from typing import Any, Generator, Literal +from typing import Any, Dict, List, Optional, Tuple, Union from otx.hpo.utils import check_positive +from otx.utils.logger import get_logger -logger = logging.getLogger() +logger = get_logger() AVAILABLE_SEARCH_SPACE_TYPE = ["uniform", "quniform", "loguniform", "qloguniform", "choice"] @@ -24,27 +34,28 @@ class SingleSearchSpace: in addition to categorical type. Quantized type has step which is unit for change. Args: - type ("uniform" | "loguniform" | "quniform" | "qloguniform" | "choice"): type of hyper parameter. - min (float | int | None, optional): upper bounding of search space. - If type isn't choice, this value is required. - max (float | int | None, optional): lower bounding of search space - If type isn't choice, this value is required. - step (int | None, optional): unit for change. If type is quniform or qloguniform, - This value is required. - log_base (int | None, optional): base of logarithm. Default value is 2. - choice_list (list | tuple | None, optional): candidiates for choice type. If task is choice, - this value is required. + type (str): type of hyper parameter in search space. + supported types: uniform, loguniform, quniform, qloguniform, choice + min (float or int, optional): upper bounding of search space. + If type isn't choice, this value is required. + max (float or int, optional): lower bounding of search space + If type isn't choice, this value is required. + step (int, optional): unit for change. If type is quniform or qloguniform, + This value is required. + log_base (int, optional): base of logarithm. Default value is 2. + choice_list (list, optional): candidiates for choice type. If task is choice, + this value is required. """ def __init__( self, - type: Literal["uniform", "loguniform", "quniform", "qloguniform", "choice"], # noqa: A002 - min: float | int | None = None, # noqa: A002 - max: float | int | None = None, # noqa: A002 - step: float | int | None = None, - log_base: int | None = 2, - choice_list: list | tuple | None = None, - ) -> None: + type: str, + min: Optional[Union[float, int]] = None, + max: Optional[Union[float, int]] = None, + step: Optional[Union[float, int]] = None, + log_base: Optional[int] = 2, + choice_list: Optional[Union[List, Tuple]] = None, + ): # pylint: disable=redefined-builtin self._type = type if self.is_categorical(): @@ -58,44 +69,44 @@ def __init__( self._check_all_value_is_right() @property - def type(self) -> Literal["uniform", "loguniform", "quniform", "qloguniform", "choice"]: # noqa: A003 + def type(self): """Type of hyper parameter in search space.""" return self._type @property - def min(self) -> float | int | None: # noqa: A003 + def min(self): """Lower bounding of search space.""" return self._min @property - def max(self) -> float | int | None: # noqa: A003 + def max(self): """Upper bounding of search space.""" return self._max @property - def step(self) -> float | int | None: + def step(self): """Unit for change.""" return self._step @property - def log_base(self) -> int | None: + def log_base(self): """Base of logarithm.""" return self._log_base @property - def choice_list(self) -> list | tuple | None: + def choice_list(self): """Candidiates for choice type.""" return self._choice_list def set_value( self, - type: Literal["uniform", "loguniform", "quniform", "qloguniform", "choice"] | None = None, # noqa: A002 - min: float | int | None = None, # noqa: A002 - max: float | int | None = None, # noqa: A002 - step: float | int | None = None, - log_base: int | None = None, - choice_list: list | tuple | None = None, - ) -> None: + type: Optional[str] = None, + min: Optional[Union[float, int]] = None, + max: Optional[Union[float, int]] = None, + step: Optional[Union[float, int]] = None, + log_base: Optional[int] = None, + choice_list: Optional[Union[List, Tuple]] = None, + ): # pylint: disable=redefined-builtin """Set attributes of the class. @@ -105,18 +116,17 @@ def set_value( To prevent it, this function priovides a way to set all necessary values at a time. Args: - type ("uniform" | "loguniform" | "quniform" | "qloguniform" | "choice" | None, optional): - type of hyper parameter in search space. supported types: uniform, loguniform, quniform, - qloguniform, choice - min (float | int | None, optional): upper bounding of search space. - If type isn't choice, this value is required. - max (float | int | None, optional): lower bounding of search space - If type isn't choice, this value is required. - step (int | None, optional): unit for change. If type is quniform or qloguniform, - This value is required. - log_base (int | None, optional): base of logarithm. Default value is 2. - choice_list (list | tuple, optional): candidiates for choice type. If task is choice, - this value is required. + type (str): type of hyper parameter in search space. + supported types: uniform, loguniform, quniform, qloguniform, choice + min (float or int, optional): upper bounding of search space. + If type isn't choice, this value is required. + max (float or int, optional): lower bounding of search space + If type isn't choice, this value is required. + step (int, optional): unit for change. If type is quniform or qloguniform, + This value is required. + log_base (int, optional): base of logarithm. Default value is 2. + choice_list (list, optional): candidiates for choice type. If task is choice, + this value is required. """ if type is not None: self._type = type @@ -135,71 +145,57 @@ def set_value( self._check_all_value_is_right() - def _align_min_max_to_choice_list_if_categorical(self) -> None: + def _align_min_max_to_choice_list_if_categorical(self): if self.is_categorical(): self._min = 0 - self._max = len(self._choice_list) - 1 # type: ignore[arg-type] + self._max = len(self._choice_list) - 1 - def _check_all_value_is_right(self) -> None: + def _check_all_value_is_right(self): # pylint: disable=too-many-branches if self._type not in AVAILABLE_SEARCH_SPACE_TYPE: - error_msg = ( - f"type should be one of {', '.join(AVAILABLE_SEARCH_SPACE_TYPE)}. But your argument is {self._type}" + raise ValueError( + f"type should be one of {', '.join(AVAILABLE_SEARCH_SPACE_TYPE)}. " f"But your argument is {self._type}" ) - raise ValueError(error_msg) if self.is_categorical(): - if self._choice_list is None or len(self._choice_list) <= 1: - error_msg = "If type is choice, choice_list should have more than one element" - raise ValueError(error_msg) + if len(self._choice_list) <= 1: + raise ValueError("If type is choice, choice_list should have more than one element") if self._min != 0: - error_msg = "if type is categorical, min should be 0." - raise ValueError(error_msg) + raise ValueError("if type is categorical, min should be 0.") if self._max != len(self._choice_list) - 1: - error_msg = "if type is categorical, max should be last index number of choice_list." - raise ValueError(error_msg) + raise ValueError("if type is categorical, max should be last index number of choice_list.") else: - if self._min is None: - error_msg = "If type isn't choice, you should set min value of search space." - raise ValueError(error_msg) - if self._max is None: - error_msg = "If type isn't choice, you should set max value of search space." - raise ValueError(error_msg) + if min is None: + raise ValueError("If type isn't choice, you should set min value of search space.") + if max is None: + raise ValueError("If type isn't choice, you should set max value of search space.") if self._min >= self._max: - error_msg = ( - f"max value should be greater than min value.\nmax value : {self._max} / min value : {self._min}" + raise ValueError( + "max value should be greater than min value.\n" f"max value : {self._max} / min value : {self._min}" ) - raise ValueError(error_msg) if self.use_log_scale(): - if self._log_base is None: - error_msg = "Type loguniform and qloguniform need log_base." - raise ValueError(error_msg) if self._log_base <= 1: - error_msg = f"log base should be greater than 1.\nyour log base value is {self._log_base}." - raise ValueError(error_msg) + raise ValueError("log base should be greater than 1.\n" f"your log base value is {self._log_base}.") if self._min <= 0: - error_msg = ( - f"If you use log scale, min value should be greater than 0.\nyour min value is {self._min}" + raise ValueError( + "If you use log scale, min value should be greater than 0.\n" f"your min value is {self._min}" ) - raise ValueError(error_msg) if self.use_quantized_step(): if self._step is None: - error_msg = f"The {self._type} type requires step value. But it doesn't exists" - raise ValueError(error_msg) + raise ValueError(f"The {self._type} type requires step value. But it doesn't exists") check_positive(self._step, "step") if self._step > self._max - self._min: - error_msg = ( + raise ValueError( "Difference between min and max is greater than step.\n" f"Current value is min : {self._min}, max : {self._max}, step : {self._step}" ) - raise ValueError(error_msg) - def __repr__(self) -> str: + def __repr__(self): """Print serach space status.""" if self.is_categorical(): - return f"type: {self._type}, candidiate : {', '.join(self._choice_list)}" # type: ignore[arg-type] + return f"type: {self._type}, candidiate : {', '.join(self._choice_list)}" rep = f"type: {self._type}, search space : {self._min} ~ {self._max}" if self.use_quantized_step(): rep += f", step : {self._step}" @@ -207,32 +203,32 @@ def __repr__(self) -> str: rep += f", log base : {self._log_base}" return rep - def is_categorical(self) -> bool: + def is_categorical(self): """Check current instance is categorical type.""" return self._type == "choice" - def use_quantized_step(self) -> bool: + def use_quantized_step(self): """Check current instance is one of type to use `step`.""" return self._type in ("quniform", "qloguniform") - def use_log_scale(self) -> bool: + def use_log_scale(self): """Check current instance is one of type to use `log scale`.""" return self._type in ("loguniform", "qloguniform") - def lower_space(self) -> float | int | None: + def lower_space(self): """Get lower bound value considering log scale if necessary.""" if self.use_log_scale(): - return math.log(self._min, self._log_base) # type: ignore[arg-type] + return math.log(self._min, self._log_base) return self._min - def upper_space(self) -> float | int | None: + def upper_space(self): """Get upper bound value considering log scale if necessary.""" if self.use_log_scale(): - return math.log(self._max, self._log_base) # type: ignore[arg-type] + return math.log(self._max, self._log_base) return self._max @typing.no_type_check - def space_to_real(self, number: int | float) -> int | float: + def space_to_real(self, number: Union[int, float]) -> Union[int, float]: """Convert search space from HPO perspective to human perspective. Args: @@ -251,7 +247,7 @@ def space_to_real(self, number: int | float) -> int | float: number = round((number - gap) / self._step) * self._step + gap return number - def real_to_space(self, number: int | float) -> int | float: + def real_to_space(self, number: Union[int, float]) -> Union[int, float]: """Convert search space from human perspective to HPO perspective. Args: @@ -261,7 +257,7 @@ def real_to_space(self, number: int | float) -> int | float: Union[int, float]: value converted to HPO perspective. """ if self.use_log_scale(): - return math.log(number, self._log_base) # type: ignore[arg-type] + return math.log(number, self._log_base) # type: ignore return number @@ -276,7 +272,7 @@ class SearchSpace: arguemnt format is as bellow. { "some_hyper_parameter_name" : { - "type": type of search space of hyper parameter. + "param_type": type of search space of hyper parameter. supported types: uniform, loguniform, quniform, qloguniform or choice # At this point, there are two available formats. @@ -305,45 +301,81 @@ class SearchSpace: def __init__( self, - search_space: dict[str, dict[str, Any]], - ) -> None: - self.search_space: dict[str, SingleSearchSpace] = {} + search_space: Dict[str, Dict[str, Any]], + ): + self.search_space: Dict[str, SingleSearchSpace] = {} for key, val in search_space.items(): # pylint: disable=too-many-nested-blocks - self.search_space[key] = SingleSearchSpace(**val) - - def __getitem__(self, key: str) -> SingleSearchSpace: + if "range" not in val: + val["type"] = val.pop("param_type") + self.search_space[key] = SingleSearchSpace(**val) + else: + args = {"type": val["param_type"]} + if val["param_type"] == "choice": + args["choice_list"] = val["range"] + else: + if len(val) != 2: + logger.warning("If there is the range in keys, then other values are ignored.") + try: + args["min"] = val["range"][0] + args["max"] = val["range"][1] + if args["type"] == "quniform": + args["step"] = val["range"][2] + elif args["type"] == "loguniform": + if len(val["range"]) == 3: + args["log_base"] = val["range"][2] + elif args["type"] == "qloguniform": + args["step"] = val["range"][2] + if len(val["range"]) == 4: + args["log_base"] = val["range"][3] + except IndexError as exc: + raise ValueError( + "You should give all necessary value depending on search space type." + "which values are needed depending on type are as bellow." + " - uniform : min value, max value" + " - quniform : min value, max value, step" + " - loguniform : min value, max value, log base(default 2)" + " - qloguniform : min value, max value, step, log baes(default 2)" + "But your value is:" + f" - {val['param_type']} : {', '.join([str(element) for element in val['range']])}" + ) from exc + self.search_space[key] = SingleSearchSpace(**args) + + def __getitem__(self, key): """Get search space by key.""" try: return self.search_space[key] except KeyError as exc: - error_msg = f"There is no search space named {key}." - raise KeyError(error_msg) from exc + raise KeyError(f"There is no search space named {key}.") from exc - def __repr__(self) -> str: + def __repr__(self): """Print all search spaces.""" return "\n".join(f"{key} => {val}" for key, val in self.search_space.items()) - def __iter__(self) -> Generator[str, Any, None]: + def __iter__(self): """Iterate search spaces.""" return self._search_space_generator() - def __len__(self) -> int: + def __len__(self): """Number of search spaces.""" return len(self.search_space) - def _search_space_generator(self) -> Generator[str, Any, None]: - yield from self.search_space + def _search_space_generator(self): + for key in self.search_space: + yield key - def has_categorical_param(self) -> bool: + def has_categorical_param(self): """Check there is a search space whose type is choice.""" - return any(param.is_categorical() for param in self.search_space.values()) + for param in self.search_space.values(): + if param.is_categorical(): + return True + return False - def get_real_config(self, config: dict) -> dict: + def get_real_config(self, config: Dict) -> Dict: """Convert search space of each config from HPO perspective to human perspective. Args: - config (dict): config to convert + config (Dict): config to convert Returns: Dict: config converted to human perspective. @@ -353,11 +385,11 @@ def get_real_config(self, config: dict) -> dict: real_config[param] = self[param].space_to_real(value) return real_config - def get_space_config(self, config: dict) -> dict: + def get_space_config(self, config: Dict) -> Dict: """Convert search space of each config from human perspective to HPO perspective. Args: - config (dict): config to convert + config (Dict): config to convert Returns: Dict: config converted to human perspective. @@ -367,7 +399,7 @@ def get_space_config(self, config: dict) -> dict: space_config[param] = self[param].real_to_space(value) return space_config - def get_bayeopt_search_space(self) -> dict: + def get_bayeopt_search_space(self) -> Dict: """Return hyper parameter serach sapce as bayeopt library format.""" bayesopt_space = {} for key, val in self.search_space.items(): @@ -375,19 +407,19 @@ def get_bayeopt_search_space(self) -> dict: return bayesopt_space - def convert_from_zero_one_scale_to_real_space(self, config: dict) -> dict: + def convert_from_zero_one_scale_to_real_space(self, config: Dict) -> Dict: """Convert search space of each config from zero one scale to human perspective. Args: - config (dict): config to convert + config (Dict): config to convert Returns: - dict: config converted to human perspective. + Dict: config converted to human perspective. """ for key, val in config.items(): lower = self.search_space[key].lower_space() upper = self.search_space[key].upper_space() - real_val = (upper - lower) * val + lower # type: ignore[operator] - config[key] = real_val + val = (upper - lower) * val + lower + config[key] = val return self.get_real_config(config) diff --git a/src/otx/hpo/utils.py b/src/otx/hpo/utils.py index 034b3f50626..886cfb44853 100644 --- a/src/otx/hpo/utils.py +++ b/src/otx/hpo/utils.py @@ -1,14 +1,23 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# """Collections of Utils for HPO.""" -from __future__ import annotations +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. -from typing import Literal +from typing import Literal, Optional -def left_vlaue_is_better(val1: int | float, val2: int | float, mode: Literal["max", "min"]) -> bool: +def left_vlaue_is_better(val1, val2, mode: Literal["max", "min"]) -> bool: """Check left value is better than right value. Whether check it's greather or lesser is changed depending on 'model'. @@ -27,13 +36,13 @@ def left_vlaue_is_better(val1: int | float, val2: int | float, mode: Literal["ma return val1 < val2 -def check_positive(value: int | float, variable_name: str | None = None, error_message: str | None = None) -> None: +def check_positive(value, variable_name: Optional[str] = None, error_message: Optional[str] = None): """Validate that value is positivle. Args: - value (int | float): value to validate. - variable_name (str | None, optional): name of value. It's used for error message. Defaults to None. - error_message (str | None, optional): Error message to use when type is different. Defaults to None. + value (Any): value to validate. + variable_name (Optional[str], optional): name of value. It's used for error message. Defaults to None. + error_message (Optional[str], optional): Error message to use when type is different. Defaults to None. Raises: ValueError: If value isn't positive, the error is raised. @@ -42,19 +51,19 @@ def check_positive(value: int | float, variable_name: str | None = None, error_m if error_message is not None: message = error_message elif variable_name: - message = f"{variable_name} should be positive.\nyour value : {value}" + message = f"{variable_name} should be positive.\n" f"your value : {value}" else: raise ValueError raise ValueError(message) -def check_not_negative(value: int | float, variable_name: str | None = None, error_message: str | None = None) -> None: +def check_not_negative(value, variable_name: Optional[str] = None, error_message: Optional[str] = None): """Validate that value isn't negative. Args: - value (int | float): value to validate. - variable_name (str | None, optional): name of value. It's used for error message. Defaults to None. - error_message (str | None, optional): Error message to use when type is different. Defaults to None. + value (Any): value to validate. + variable_name (Optional[str], optional): name of value. It's used for error message. Defaults to None. + error_message (Optional[str], optional): Error message to use when type is different. Defaults to None. Raises: ValueError: If value is negative, the error is raised. @@ -63,13 +72,13 @@ def check_not_negative(value: int | float, variable_name: str | None = None, err if error_message is not None: message = error_message elif variable_name: - message = f"{variable_name} should be positive.\nyour value : {value}" + message = f"{variable_name} should be positive.\n" f"your value : {value}" else: raise ValueError raise ValueError(message) -def check_mode_input(mode: str) -> None: +def check_mode_input(mode: str): """Validate that mode is 'max' or 'min'. Args: @@ -79,5 +88,4 @@ def check_mode_input(mode: str) -> None: ValueError: If 'mode' is not both 'max' and 'min', the error is raised. """ if mode not in ["max", "min"]: - error_msg = f"mode should be max or min.\nYour value : {mode}" - raise ValueError(error_msg) + raise ValueError("mode should be max or min.\n" f"Your value : {mode}") diff --git a/src/otx/recipe/__init__.py b/src/otx/recipe/__init__.py deleted file mode 100644 index 6e14bfbb517..00000000000 --- a/src/otx/recipe/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""YAML file recipes for the models OTX provides. - -this file is needed here to include configs when building project as a package -""" diff --git a/src/otx/recipe/_base_/data/mmaction_base.yaml b/src/otx/recipe/_base_/data/mmaction_base.yaml deleted file mode 100644 index 1bea8af89b5..00000000000 --- a/src/otx/recipe/_base_/data/mmaction_base.yaml +++ /dev/null @@ -1,105 +0,0 @@ -task: ACTION_CLASSIFICATION -config: - data_format: kinetics - mem_cache_size: 1GB - mem_cache_img_max_size: - - 500 - - 500 - image_color_channel: RGB - stack_images: False - unannotated_items_ratio: 0.0 - train_subset: - subset_name: train - transform_lib_type: MMACTION - batch_size: 8 - num_workers: 2 - transforms: - - type: LoadVideoForClassification - - type: DecordInit - - type: SampleFrames - clip_len: 8 - frame_interval: 4 - num_clips: 1 - - type: DecordDecode - - type: Resize - scale: - - -1 - - 256 - - type: RandomResizedCrop - - type: Resize - scale: - - 224 - - 224 - keep_ratio: false - - type: Flip - flip_ratio: 0.5 - - type: FormatShape - input_format: NCTHW - - type: PackActionInputs - meta_keys: - - img_shape - - ori_shape - - pad_shape - - scale_factor - sampler: - class_path: torch.utils.data.RandomSampler - val_subset: - subset_name: val - transform_lib_type: MMACTION - batch_size: 8 - num_workers: 2 - transforms: - - type: LoadVideoForClassification - - type: DecordInit - - type: SampleFrames - clip_len: 8 - frame_interval: 4 - num_clips: 1 - test_mode: true - - type: DecordDecode - - type: Resize - scale: - - -1 - - 256 - - type: CenterCrop - crop_size: 224 - - type: FormatShape - input_format: NCTHW - - type: PackActionInputs - meta_keys: - - img_shape - - ori_shape - - pad_shape - - scale_factor - sampler: - class_path: torch.utils.data.RandomSampler - test_subset: - subset_name: test - transform_lib_type: MMACTION - batch_size: 8 - num_workers: 2 - transforms: - - type: LoadVideoForClassification - - type: DecordInit - - type: SampleFrames - clip_len: 8 - frame_interval: 4 - num_clips: 1 - test_mode: true - - type: DecordDecode - - type: Resize - scale: - - -1 - - 256 - - type: CenterCrop - crop_size: 224 - - type: FormatShape - input_format: NCTHW - - type: PackActionInputs - meta_keys: - - img_shape - - ori_shape - - pad_shape - - scale_factor - sampler: - class_path: torch.utils.data.RandomSampler diff --git a/src/otx/recipe/_base_/data/mmdet_base.yaml b/src/otx/recipe/_base_/data/mmdet_base.yaml deleted file mode 100644 index 0b4233bba2a..00000000000 --- a/src/otx/recipe/_base_/data/mmdet_base.yaml +++ /dev/null @@ -1,76 +0,0 @@ -task: DETECTION -config: - mem_cache_size: 1GB - mem_cache_img_max_size: null - image_color_channel: RGB - data_format: coco_instances - include_polygons: false - unannotated_items_ratio: 0.0 - train_subset: - subset_name: train - batch_size: 8 - num_workers: 2 - transform_lib_type: MMDET - transforms: - - backend_args: null - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - keep_ratio: true - scale: - - 1333 - - 800 - type: Resize - - prob: 0.5 - type: RandomFlip - - type: PackDetInputs - sampler: - class_path: otx.algo.samplers.balanced_sampler.BalancedSampler - val_subset: - subset_name: val - num_workers: 2 - batch_size: 1 - transform_lib_type: MMDET - transforms: - - backend_args: null - type: LoadImageFromFile - - keep_ratio: true - scale: - - 1333 - - 800 - type: Resize - - type: LoadAnnotations - with_bbox: true - - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - type: PackDetInputs - sampler: - class_path: torch.utils.data.RandomSampler - test_subset: - subset_name: test - num_workers: 2 - batch_size: 1 - transform_lib_type: MMDET - transforms: - - backend_args: null - type: LoadImageFromFile - - keep_ratio: true - scale: - - 1333 - - 800 - type: Resize - - type: LoadAnnotations - with_bbox: true - - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - type: PackDetInputs - sampler: - class_path: torch.utils.data.RandomSampler diff --git a/src/otx/recipe/_base_/data/mmpretrain_base.yaml b/src/otx/recipe/_base_/data/mmpretrain_base.yaml deleted file mode 100644 index d7b1bc8337c..00000000000 --- a/src/otx/recipe/_base_/data/mmpretrain_base.yaml +++ /dev/null @@ -1,52 +0,0 @@ -task: MULTI_CLASS_CLS -config: - data_format: imagenet_with_subset_dirs - mem_cache_img_max_size: - - 500 - - 500 - mem_cache_size: 1GB - image_color_channel: RGB - include_polygons: false - unannotated_items_ratio: 0.0 - train_subset: - subset_name: train - num_workers: 2 - batch_size: 64 - transform_lib_type: MMPRETRAIN - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - type: PackInputs - sampler: - class_path: otx.algo.samplers.balanced_sampler.BalancedSampler - val_subset: - subset_name: val - num_workers: 2 - batch_size: 64 - transform_lib_type: MMPRETRAIN - transforms: - - type: LoadImageFromFile - - backend: cv2 - edge: short - scale: 256 - type: ResizeEdge - - crop_size: 224 - type: CenterCrop - - type: PackInputs - sampler: - class_path: torch.utils.data.RandomSampler - test_subset: - subset_name: test - num_workers: 2 - batch_size: 64 - transform_lib_type: MMPRETRAIN - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs - sampler: - class_path: torch.utils.data.RandomSampler diff --git a/src/otx/recipe/_base_/data/mmseg_base.yaml b/src/otx/recipe/_base_/data/mmseg_base.yaml deleted file mode 100644 index dbbd05c34f9..00000000000 --- a/src/otx/recipe/_base_/data/mmseg_base.yaml +++ /dev/null @@ -1,74 +0,0 @@ -task: SEMANTIC_SEGMENTATION -config: - mem_cache_size: 1GB - mem_cache_img_max_size: null - image_color_channel: RGB - data_format: common_semantic_segmentation_with_subset_dirs - include_polygons: true - unannotated_items_ratio: 0.0 - train_subset: - subset_name: train - batch_size: 8 - num_workers: 4 - transform_lib_type: MMSEG - transforms: - - type: LoadImageFromFile - - reduce_zero_label: true - type: LoadAnnotations - - keep_ratio: false - ratio_range: - - 0.5 - - 2.0 - scale: - - 544 - - 544 - type: RandomResize - - cat_max_ratio: 0.75 - crop_size: - - 512 - - 512 - type: RandomCrop - - prob: 0.5 - type: RandomFlip - - type: PhotoMetricDistortion - - type: Pad - size: - - 512 - - 512 - - type: PackSegInputs - sampler: - class_path: torch.utils.data.RandomSampler - val_subset: - subset_name: val - num_workers: 4 - batch_size: 8 - transform_lib_type: MMSEG - transforms: - - type: LoadImageFromFile - - keep_ratio: false - scale: - - 544 - - 544 - type: Resize - - reduce_zero_label: true - type: LoadAnnotations - - type: PackSegInputs - sampler: - class_path: torch.utils.data.RandomSampler - test_subset: - subset_name: test - num_workers: 4 - batch_size: 8 - transform_lib_type: MMSEG - transforms: - - type: LoadImageFromFile - - keep_ratio: false - scale: - - 544 - - 544 - type: Resize - - reduce_zero_label: true - type: LoadAnnotations - - type: PackSegInputs - sampler: - class_path: torch.utils.data.RandomSampler diff --git a/src/otx/recipe/_base_/data/torchvision_base.yaml b/src/otx/recipe/_base_/data/torchvision_base.yaml deleted file mode 100644 index e2e92cad872..00000000000 --- a/src/otx/recipe/_base_/data/torchvision_base.yaml +++ /dev/null @@ -1,35 +0,0 @@ -task: MULTI_CLASS_CLS -config: - mem_cache_size: 1GB - mem_cache_img_max_size: null - image_color_channel: RGB - stack_images: False - data_format: imagenet_with_subset_dirs - unannotated_items_ratio: 0.0 - train_subset: - subset_name: train - transform_lib_type: TORCHVISION - transforms: - - class_path: torchvision.transforms.v2.ToImage - batch_size: 1 - num_workers: 2 - sampler: - class_path: torch.utils.data.RandomSampler - val_subset: - subset_name: val - transform_lib_type: TORCHVISION - transforms: - - class_path: torchvision.transforms.v2.ToImage - batch_size: 1 - num_workers: 2 - sampler: - class_path: torch.utils.data.RandomSampler - test_subset: - subset_name: test - transform_lib_type: TORCHVISION - transforms: - - class_path: torchvision.transforms.v2.ToImage - batch_size: 1 - num_workers: 2 - sampler: - class_path: torch.utils.data.RandomSampler diff --git a/src/otx/recipe/_base_/test.yaml b/src/otx/recipe/_base_/test.yaml deleted file mode 100644 index 22c157f8811..00000000000 --- a/src/otx/recipe/_base_/test.yaml +++ /dev/null @@ -1,26 +0,0 @@ -callbacks: - - class_path: lightning.pytorch.callbacks.RichProgressBar - init_args: - refresh_rate: 1 - leave: false - - class_path: otx.algo.callbacks.iteration_timer.IterationTimer - init_args: - prog_bar: true - on_step: false - on_epoch: true - - class_path: lightning.pytorch.callbacks.RichModelSummary - init_args: - max_depth: 1 -logger: - - class_path: lightning.pytorch.loggers.csv_logs.CSVLogger - init_args: - save_dir: "" - name: "csv/" - prefix: "" - - class_path: lightning.pytorch.loggers.tensorboard.TensorBoardLogger - init_args: - save_dir: "" - name: "tensorboard/" - log_graph: false - default_hp_metric: true - prefix: "" diff --git a/src/otx/recipe/_base_/train.yaml b/src/otx/recipe/_base_/train.yaml deleted file mode 100644 index b49d89261ca..00000000000 --- a/src/otx/recipe/_base_/train.yaml +++ /dev/null @@ -1,58 +0,0 @@ -max_epochs: 200 -min_epochs: 1 -callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - monitor: null - mode: max - patience: 10 - check_on_train_epoch_end: false - min_delta: 0.001 - - class_path: lightning.pytorch.callbacks.RichProgressBar - init_args: - refresh_rate: 1 - leave: false - - class_path: lightning.pytorch.callbacks.ModelCheckpoint - init_args: - dirpath: "" - monitor: null - mode: max - save_top_k: 1 - save_last: true - auto_insert_metric_name: false - filename: "checkpoints/epoch_{epoch:03d}" - - class_path: otx.algo.callbacks.iteration_timer.IterationTimer - init_args: - prog_bar: true - on_step: false - on_epoch: true - - class_path: lightning.pytorch.callbacks.RichModelSummary - init_args: - max_depth: 1 - - class_path: lightning.pytorch.callbacks.LearningRateMonitor - init_args: - logging_interval: epoch - log_momentum: true - - class_path: otx.algo.callbacks.adaptive_train_scheduling.AdaptiveTrainScheduling - init_args: - max_interval: 5 - decay: -0.025 -logger: - - class_path: lightning.pytorch.loggers.csv_logs.CSVLogger - init_args: - save_dir: "" - name: "csv/" - prefix: "" - - class_path: lightning.pytorch.loggers.tensorboard.TensorBoardLogger - init_args: - save_dir: "" - name: "tensorboard/" - log_graph: false - default_hp_metric: true - prefix: "" -deterministic: false -seed: null -precision: 16 -check_val_every_n_epoch: 1 -gradient_clip_val: null -num_sanity_val_steps: 0 diff --git a/src/otx/recipe/action/action_classification/movinet.yaml b/src/otx/recipe/action/action_classification/movinet.yaml deleted file mode 100644 index 2417abf91ec..00000000000 --- a/src/otx/recipe/action/action_classification/movinet.yaml +++ /dev/null @@ -1,32 +0,0 @@ -model: - class_path: otx.algo.action_classification.movinet.MoViNet - init_args: - num_classes: 400 - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.0003 - weight_decay: 0.0001 - -scheduler: - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.5 - patience: 2 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: ACTION_CLASSIFICATION - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmaction_base.yaml diff --git a/src/otx/recipe/action/action_classification/openvino_model.yaml b/src/otx/recipe/action/action_classification/openvino_model.yaml deleted file mode 100644 index 1ca8f715f2d..00000000000 --- a/src/otx/recipe/action/action_classification/openvino_model.yaml +++ /dev/null @@ -1,39 +0,0 @@ -model: - class_path: otx.core.model.entity.action_classification.OVActionClsModel - init_args: - num_classes: 400 - model_name: x3d - async_inference: True - use_throughput_mode: True - model_type: Action Classification - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 1e-3 - weight_decay: 0.0 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - -engine: - task: ACTION_CLASSIFICATION - device: cpu - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml -overrides: - data: - task: ACTION_CLASSIFICATION - config: - image_color_channel: BGR - data_format: kinetics - test_subset: - batch_size: 8 - num_workers: 2 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.DecodeVideo - init_args: - test_mode: true - - class_path: otx.core.data.transform_libs.torchvision.PackVideo diff --git a/src/otx/recipe/action/action_classification/x3d.yaml b/src/otx/recipe/action/action_classification/x3d.yaml deleted file mode 100644 index 7c2dfd77c98..00000000000 --- a/src/otx/recipe/action/action_classification/x3d.yaml +++ /dev/null @@ -1,32 +0,0 @@ -model: - class_path: otx.algo.action_classification.x3d.X3D - init_args: - num_classes: 400 - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.0001 - weight_decay: 0.0001 - -scheduler: - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: ACTION_CLASSIFICATION - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmaction_base.yaml diff --git a/src/otx/recipe/action/action_detection/x3d_fastrcnn.yaml b/src/otx/recipe/action/action_detection/x3d_fastrcnn.yaml deleted file mode 100644 index 8a680bcda95..00000000000 --- a/src/otx/recipe/action/action_detection/x3d_fastrcnn.yaml +++ /dev/null @@ -1,102 +0,0 @@ -model: - class_path: otx.algo.action_detection.x3d_fastrcnn.X3DFastRCNN - init_args: - num_classes: 81 - topk: 3 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.005 - momentum: 0.9 - weight_decay: 0.00001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: ACTION_DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../../_base_/data/mmaction_base.yaml -overrides: - precision: 32 - data: - task: ACTION_DETECTION - config: - data_format: ava - train_subset: - transforms: - - type: LoadVideoForDetection - - type: LoadAnnotations - - type: SampleAVAFrames - clip_len: 32 - frame_interval: 2 - - type: RawFrameDecode - io_backend: disk - - type: RandomRescale - scale_range: - - 256 - - 320 - - type: RandomCrop - size: 256 - - type: Flip - flip_ratio: 0.5 - - type: FormatShape - input_format: NCTHW - collapse: true - - type: PackActionInputs - val_subset: - batch_size: 1 - transforms: - - type: LoadVideoForDetection - - type: LoadAnnotations - - type: SampleAVAFrames - clip_len: 32 - frame_interval: 2 - test_mode: true - - type: RawFrameDecode - io_backend: disk - - type: Resize - scale: - - -1 - - 256 - - type: FormatShape - input_format: NCTHW - collapse: true - - type: PackActionInputs - test_subset: - batch_size: 1 - transforms: - - type: LoadVideoForDetection - - type: LoadAnnotations - - type: SampleAVAFrames - clip_len: 32 - frame_interval: 2 - test_mode: true - - type: RawFrameDecode - io_backend: disk - - type: Resize - scale: - - -1 - - 256 - - type: FormatShape - input_format: NCTHW - collapse: true - - type: PackActionInputs diff --git a/src/otx/recipe/anomaly_classification/padim.yaml b/src/otx/recipe/anomaly_classification/padim.yaml deleted file mode 100644 index 75e037f5517..00000000000 --- a/src/otx/recipe/anomaly_classification/padim.yaml +++ /dev/null @@ -1,81 +0,0 @@ -model: - class_path: otx.algo.anomaly.padim.Padim - init_args: - layers: ["layer1", "layer2", "layer3"] - backbone: "resnet18" - pre_trained: True - n_features: null - -engine: - task: ANOMALY_CLASSIFICATION - device: auto - -callback_monitor: step # this has no effect as Padim does not need to be trained - -data: ../_base_/data/torchvision_base.yaml -overrides: - precision: 32 - max_epochs: 1 - limit_val_batches: 0 # this is set to 0 as the default dataloader does not have validation set. But this also means that the model will not give correct performance numbers - callbacks: - - class_path: otx.algo.callbacks.adaptive_train_scheduling.AdaptiveTrainScheduling - init_args: - max_interval: 1 - data: - task: ANOMALY_CLASSIFICATION - config: - data_format: mvtec - vpm_config: - use_bbox: True - use_point: False - train_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - val_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - test_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] diff --git a/src/otx/recipe/anomaly_classification/stfpm.yaml b/src/otx/recipe/anomaly_classification/stfpm.yaml deleted file mode 100644 index f518883423e..00000000000 --- a/src/otx/recipe/anomaly_classification/stfpm.yaml +++ /dev/null @@ -1,86 +0,0 @@ -model: - class_path: otx.algo.anomaly.stfpm.Stfpm - init_args: - layers: ["layer1", "layer2", "layer3"] - backbone: "resnet18" - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.4 - momentum: 0.9 - dampening: 0 - weight_decay: 0.001 - -engine: - task: ANOMALY_CLASSIFICATION - device: auto - -callback_monitor: train_loss_epoch # val loss is not available as there is no validation set from default dataloader - -data: ../_base_/data/torchvision_base.yaml -overrides: - max_epochs: 100 - limit_val_batches: 0 # this is set to 0 as the default dataloader does not have validation set. But this also means that the model will not give correct performance numbers - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 5 - - class_path: otx.algo.callbacks.adaptive_train_scheduling.AdaptiveTrainScheduling - init_args: - max_interval: 1 - data: - task: ANOMALY_CLASSIFICATION - config: - data_format: mvtec - train_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - val_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - test_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] diff --git a/src/otx/recipe/anomaly_detection/padim.yaml b/src/otx/recipe/anomaly_detection/padim.yaml deleted file mode 100644 index fc62ee39d19..00000000000 --- a/src/otx/recipe/anomaly_detection/padim.yaml +++ /dev/null @@ -1,81 +0,0 @@ -model: - class_path: otx.algo.anomaly.padim.Padim - init_args: - layers: ["layer1", "layer2", "layer3"] - backbone: "resnet18" - pre_trained: True - n_features: null - -engine: - task: ANOMALY_DETECTION - device: auto - -callback_monitor: step # this has no effect as Padim does not need to be trained - -data: ../_base_/data/torchvision_base.yaml -overrides: - precision: 32 - max_epochs: 1 - limit_val_batches: 0 # this is set to 0 as the default dataloader does not have validation set. But this also means that the model will not give correct performance numbers - callbacks: - - class_path: otx.algo.callbacks.adaptive_train_scheduling.AdaptiveTrainScheduling - init_args: - max_interval: 1 - data: - task: ANOMALY_DETECTION - config: - data_format: mvtec - vpm_config: - use_bbox: True - use_point: False - train_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - val_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - test_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] diff --git a/src/otx/recipe/anomaly_detection/stfpm.yaml b/src/otx/recipe/anomaly_detection/stfpm.yaml deleted file mode 100644 index 221865aab97..00000000000 --- a/src/otx/recipe/anomaly_detection/stfpm.yaml +++ /dev/null @@ -1,86 +0,0 @@ -model: - class_path: otx.algo.anomaly.stfpm.Stfpm - init_args: - layers: ["layer1", "layer2", "layer3"] - backbone: "resnet18" - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.4 - momentum: 0.9 - dampening: 0 - weight_decay: 0.001 - -engine: - task: ANOMALY_DETECTION - device: auto - -callback_monitor: train_loss_epoch # val loss is not available as there is no validation set from default dataloader - -data: ../_base_/data/torchvision_base.yaml -overrides: - max_epochs: 100 - limit_val_batches: 0 # this is set to 0 as the default dataloader does not have validation set. But this also means that the model will not give correct performance numbers - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 5 - - class_path: otx.algo.callbacks.adaptive_train_scheduling.AdaptiveTrainScheduling - init_args: - max_interval: 1 - data: - task: ANOMALY_DETECTION - config: - data_format: mvtec - train_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - val_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - test_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] diff --git a/src/otx/recipe/anomaly_segmentation/padim.yaml b/src/otx/recipe/anomaly_segmentation/padim.yaml deleted file mode 100644 index 3ed2934eb8e..00000000000 --- a/src/otx/recipe/anomaly_segmentation/padim.yaml +++ /dev/null @@ -1,81 +0,0 @@ -model: - class_path: otx.algo.anomaly.padim.Padim - init_args: - layers: ["layer1", "layer2", "layer3"] - backbone: "resnet18" - pre_trained: True - n_features: null - -engine: - task: ANOMALY_SEGMENTATION - device: auto - -callback_monitor: step # this has no effect as Padim does not need to be trained - -data: ../_base_/data/torchvision_base.yaml -overrides: - precision: 32 - max_epochs: 1 - limit_val_batches: 0 # this is set to 0 as the default dataloader does not have validation set. But this also means that the model will not give correct performance numbers - callbacks: - - class_path: otx.algo.callbacks.adaptive_train_scheduling.AdaptiveTrainScheduling - init_args: - max_interval: 1 - data: - task: ANOMALY_SEGMENTATION - config: - data_format: mvtec - vpm_config: - use_bbox: True - use_point: False - train_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - val_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - test_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] diff --git a/src/otx/recipe/anomaly_segmentation/stfpm.yaml b/src/otx/recipe/anomaly_segmentation/stfpm.yaml deleted file mode 100644 index a045fdd3486..00000000000 --- a/src/otx/recipe/anomaly_segmentation/stfpm.yaml +++ /dev/null @@ -1,86 +0,0 @@ -model: - class_path: otx.algo.anomaly.stfpm.Stfpm - init_args: - layers: ["layer1", "layer2", "layer3"] - backbone: "resnet18" - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.4 - momentum: 0.9 - dampening: 0 - weight_decay: 0.001 - -engine: - task: ANOMALY_SEGMENTATION - device: auto - -callback_monitor: train_loss_epoch # val loss is not available as there is no validation set from default dataloader - -data: ../_base_/data/torchvision_base.yaml -overrides: - max_epochs: 100 - limit_val_batches: 0 # this is set to 0 as the default dataloader does not have validation set. But this also means that the model will not give correct performance numbers - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 5 - - class_path: otx.algo.callbacks.adaptive_train_scheduling.AdaptiveTrainScheduling - init_args: - max_interval: 1 - data: - task: ANOMALY_SEGMENTATION - config: - data_format: mvtec - train_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - val_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - test_subset: - batch_size: 32 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 256 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] diff --git a/src/otx/recipe/classification/h_label_cls/efficientnet_b0_light.yaml b/src/otx/recipe/classification/h_label_cls/efficientnet_b0_light.yaml deleted file mode 100644 index 4c2597f26a0..00000000000 --- a/src/otx/recipe/classification/h_label_cls/efficientnet_b0_light.yaml +++ /dev/null @@ -1,43 +0,0 @@ -model: - class_path: otx.algo.classification.efficientnet_b0.EfficientNetB0ForHLabelCls - init_args: - num_classes: 1000 - num_multiclass_heads: 0 - num_multilabel_classes: 0 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0049 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: otx.core.metrics.accuracy.MixedHLabelAccuracy - init_args: - num_multiclass_heads: 0 - num_multilabel_classes: 0 - -engine: - task: H_LABEL_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - task: H_LABEL_CLS - config: - data_format: datumaro diff --git a/src/otx/recipe/classification/h_label_cls/efficientnet_v2_light.yaml b/src/otx/recipe/classification/h_label_cls/efficientnet_v2_light.yaml deleted file mode 100644 index f147fe05a66..00000000000 --- a/src/otx/recipe/classification/h_label_cls/efficientnet_v2_light.yaml +++ /dev/null @@ -1,55 +0,0 @@ -model: - class_path: otx.algo.classification.efficientnet_v2.EfficientNetV2ForHLabelCls - init_args: - num_classes: 1000 - num_multiclass_heads: 0 - num_multilabel_classes: 0 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0071 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: otx.core.metrics.accuracy.MixedHLabelAccuracy - init_args: - num_multiclass_heads: 0 - num_multilabel_classes: 0 - -engine: - task: H_LABEL_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - task: H_LABEL_CLS - config: - data_format: datumaro - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - direction: horizontal - prob: 0.5 - type: RandomFlip - - type: PackInputs diff --git a/src/otx/recipe/classification/h_label_cls/mobilenet_v3_large_light.yaml b/src/otx/recipe/classification/h_label_cls/mobilenet_v3_large_light.yaml deleted file mode 100644 index 03fe33572c3..00000000000 --- a/src/otx/recipe/classification/h_label_cls/mobilenet_v3_large_light.yaml +++ /dev/null @@ -1,58 +0,0 @@ -model: - class_path: otx.algo.classification.mobilenet_v3_large.MobileNetV3ForHLabelCls - init_args: - num_classes: 1000 - num_multiclass_heads: 0 - num_multilabel_classes: 0 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0058 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 10 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: otx.core.metrics.accuracy.MixedHLabelAccuracy - init_args: - num_multiclass_heads: 0 - num_multilabel_classes: 0 - -engine: - task: H_LABEL_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - task: H_LABEL_CLS - config: - data_format: datumaro - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - direction: horizontal - prob: 0.5 - type: RandomFlip - - type: PackInputs diff --git a/src/otx/recipe/classification/h_label_cls/openvino_model.yaml b/src/otx/recipe/classification/h_label_cls/openvino_model.yaml deleted file mode 100644 index 37a7db17449..00000000000 --- a/src/otx/recipe/classification/h_label_cls/openvino_model.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# @package _global_ -model: - class_path: otx.core.model.entity.classification.OVHlabelClassificationModel - init_args: - model_name: openvino.xml - async_inference: True - use_throughput_mode: False - model_type: Classification - num_classes: 7 - num_multiclass_heads: 0 - num_multilabel_classes: 0 - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 1e-3 - weight_decay: 0.0 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: min - factor: 0.1 - patience: 1 - monitor: train/loss - -engine: - task: H_LABEL_CLS - device: cpu - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml - -overrides: - data: - task: H_LABEL_CLS - config: - image_color_channel: RGB - data_format: datumaro - test_subset: - batch_size: 128 diff --git a/src/otx/recipe/classification/h_label_cls/otx_deit_tiny.yaml b/src/otx/recipe/classification/h_label_cls/otx_deit_tiny.yaml deleted file mode 100644 index 4d0744b56be..00000000000 --- a/src/otx/recipe/classification/h_label_cls/otx_deit_tiny.yaml +++ /dev/null @@ -1,47 +0,0 @@ -model: - class_path: otx.algo.classification.deit_tiny.DeitTinyForHLabelCls - init_args: - num_classes: 1000 - num_multiclass_heads: 0 - num_multilabel_classes: 0 - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.0001 - weight_decay: 0.05 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 10 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: otx.core.metrics.accuracy.MixedHLabelAccuracy - init_args: - num_multiclass_heads: 0 - num_multilabel_classes: 0 - -engine: - task: H_LABEL_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - task: H_LABEL_CLS - config: - data_format: datumaro diff --git a/src/otx/recipe/classification/multi_class_cls/efficientnet_b0_light.yaml b/src/otx/recipe/classification/multi_class_cls/efficientnet_b0_light.yaml deleted file mode 100644 index f943fa1485e..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/efficientnet_b0_light.yaml +++ /dev/null @@ -1,41 +0,0 @@ -model: - class_path: otx.algo.classification.efficientnet_b0.EfficientNetB0ForMulticlassCls - init_args: - num_classes: 1000 - light: True - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0049 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml - -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 diff --git a/src/otx/recipe/classification/multi_class_cls/efficientnet_v2_light.yaml b/src/otx/recipe/classification/multi_class_cls/efficientnet_v2_light.yaml deleted file mode 100644 index b22c154a3f7..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/efficientnet_v2_light.yaml +++ /dev/null @@ -1,52 +0,0 @@ -model: - class_path: otx.algo.classification.efficientnet_v2.EfficientNetV2ForMulticlassCls - init_args: - num_classes: 1000 - light: True - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0071 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - direction: horizontal - prob: 0.5 - type: RandomFlip - - type: PackInputs diff --git a/src/otx/recipe/classification/multi_class_cls/mobilenet_v3_large_light.yaml b/src/otx/recipe/classification/multi_class_cls/mobilenet_v3_large_light.yaml deleted file mode 100644 index 577431d1b9a..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/mobilenet_v3_large_light.yaml +++ /dev/null @@ -1,55 +0,0 @@ -model: - class_path: otx.algo.classification.mobilenet_v3_large.MobileNetV3ForMulticlassCls - init_args: - num_classes: 1000 - light: True - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0058 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 10 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - direction: horizontal - prob: 0.5 - type: RandomFlip - - type: PackInputs diff --git a/src/otx/recipe/classification/multi_class_cls/openvino_model.yaml b/src/otx/recipe/classification/multi_class_cls/openvino_model.yaml deleted file mode 100644 index 906bab5ee40..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/openvino_model.yaml +++ /dev/null @@ -1,38 +0,0 @@ -model: - class_path: otx.core.model.entity.classification.OVMulticlassClassificationModel - init_args: - num_classes: 1000 - model_name: efficientnet-b0-pytorch - async_inference: True - use_throughput_mode: False - model_type: Classification - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 1e-3 - weight_decay: 0.0 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: min - factor: 0.1 - patience: 1 - monitor: train/loss - -engine: - task: MULTI_CLASS_CLS - device: cpu - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml - -overrides: - data: - task: MULTI_CLASS_CLS - config: - image_color_channel: RGB - test_subset: - batch_size: 128 diff --git a/src/otx/recipe/classification/multi_class_cls/otx_deit_tiny.yaml b/src/otx/recipe/classification/multi_class_cls/otx_deit_tiny.yaml deleted file mode 100644 index 5fcf8540abe..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/otx_deit_tiny.yaml +++ /dev/null @@ -1,42 +0,0 @@ -model: - class_path: otx.algo.classification.deit_tiny.DeitTinyForMulticlassCls - init_args: - num_classes: 1000 - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.0001 - weight_decay: 0.05 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 10 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml - -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 diff --git a/src/otx/recipe/classification/multi_class_cls/otx_dino_v2.yaml b/src/otx/recipe/classification/multi_class_cls/otx_dino_v2.yaml deleted file mode 100644 index 314692b9d7f..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/otx_dino_v2.yaml +++ /dev/null @@ -1,106 +0,0 @@ -model: - class_path: otx.algo.classification.otx_dino_v2.DINOv2RegisterClassifier - init_args: - num_classes: 1000 - config: - class_path: omegaconf.dictconfig.DictConfig - init_args: - content: - backbone: - name: dinov2_vits14_reg - frozen: false - head: - in_channels: 384 - num_classes: 1000 - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 1e-5 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: min - factor: 0.1 - patience: 9 - monitor: train/loss - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - transforms: - - type: LoadImageFromFile - to_float32: true - - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: true - type: Normalize - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - type: PackInputs - val_subset: - transforms: - - type: LoadImageFromFile - to_float32: true - - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: true - type: Normalize - - backend: cv2 - edge: short - scale: 256 - type: ResizeEdge - - crop_size: 224 - type: CenterCrop - - type: PackInputs - test_subset: - transforms: - - type: LoadImageFromFile - to_float32: true - - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: true - type: Normalize - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs diff --git a/src/otx/recipe/classification/multi_class_cls/otx_dino_v2_linear_probe.yaml b/src/otx/recipe/classification/multi_class_cls/otx_dino_v2_linear_probe.yaml deleted file mode 100644 index ac60adea80c..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/otx_dino_v2_linear_probe.yaml +++ /dev/null @@ -1,108 +0,0 @@ -model: - class_path: otx.algo.classification.otx_dino_v2.DINOv2RegisterClassifier - init_args: - num_classes: 1000 - config: - class_path: omegaconf.dictconfig.DictConfig - init_args: - content: - backbone: - name: dinov2_vits14_reg - frozen: true - head: - in_channels: 384 - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.007 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: min - factor: 0.1 - patience: 1 - monitor: train/loss - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - transforms: - - type: LoadImageFromFile - to_float32: true - - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: true - type: Normalize - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - type: PackInputs - val_subset: - transforms: - - type: LoadImageFromFile - to_float32: true - - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: true - type: Normalize - - backend: cv2 - edge: short - scale: 256 - type: ResizeEdge - - crop_size: 224 - type: CenterCrop - - type: PackInputs - test_subset: - transforms: - - type: LoadImageFromFile - to_float32: true - - mean: - - 123.675 - - 116.28 - - 103.53 - std: - - 58.395 - - 57.12 - - 57.375 - to_rgb: true - type: Normalize - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs diff --git a/src/otx/recipe/classification/multi_class_cls/otx_efficientnet_b0.yaml b/src/otx/recipe/classification/multi_class_cls/otx_efficientnet_b0.yaml deleted file mode 100644 index 7a50c6eed9f..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/otx_efficientnet_b0.yaml +++ /dev/null @@ -1,41 +0,0 @@ -model: - class_path: otx.algo.classification.efficientnet_b0.EfficientNetB0ForMulticlassCls - init_args: - num_classes: 1000 - light: false - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0049 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml - -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 diff --git a/src/otx/recipe/classification/multi_class_cls/otx_efficientnet_v2.yaml b/src/otx/recipe/classification/multi_class_cls/otx_efficientnet_v2.yaml deleted file mode 100644 index bca4bf51c93..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/otx_efficientnet_v2.yaml +++ /dev/null @@ -1,52 +0,0 @@ -model: - class_path: otx.algo.classification.efficientnet_v2.EfficientNetV2ForMulticlassCls - init_args: - num_classes: 1000 - light: false - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0071 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - direction: horizontal - prob: 0.5 - type: RandomFlip - - type: PackInputs diff --git a/src/otx/recipe/classification/multi_class_cls/otx_mobilenet_v3_large.yaml b/src/otx/recipe/classification/multi_class_cls/otx_mobilenet_v3_large.yaml deleted file mode 100644 index ffe1e120301..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/otx_mobilenet_v3_large.yaml +++ /dev/null @@ -1,55 +0,0 @@ -model: - class_path: otx.algo.classification.mobilenet_v3_large.MobileNetV3ForMulticlassCls - init_args: - num_classes: 1000 - light: false - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0058 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 10 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: RandomResizedCrop - - direction: horizontal - prob: 0.5 - type: RandomFlip - - type: PackInputs diff --git a/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b0.yaml b/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b0.yaml deleted file mode 100644 index 8aa19e79e22..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b0.yaml +++ /dev/null @@ -1,116 +0,0 @@ -model: - class_path: otx.algo.classification.torchvision_model.OTXTVModel - init_args: - backbone: efficientnet_b0 - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.01 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: torch.optim.lr_scheduler.CosineAnnealingLR - init_args: - T_max: 100000 - eta_min: 0 - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml - -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.RandomResizedCrop - init_args: - size: - - 224 - - 224 - antialias: True - - class_path: torchvision.transforms.v2.RandomHorizontalFlip - init_args: - p: 0.5 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - sampler: - class_path: otx.algo.samplers.balanced_sampler.BalancedSampler - val_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - test_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 diff --git a/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b1.yaml b/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b1.yaml deleted file mode 100644 index 456539265c9..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b1.yaml +++ /dev/null @@ -1,116 +0,0 @@ -model: - class_path: otx.algo.classification.torchvision_model.OTXTVModel - init_args: - backbone: efficientnet_b1 - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.01 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: torch.optim.lr_scheduler.CosineAnnealingLR - init_args: - T_max: 100000 - eta_min: 0 - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml - -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.RandomResizedCrop - init_args: - size: - - 224 - - 224 - antialias: True - - class_path: torchvision.transforms.v2.RandomHorizontalFlip - init_args: - p: 0.5 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - sampler: - class_path: otx.algo.samplers.balanced_sampler.BalancedSampler - val_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - test_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 diff --git a/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b3.yaml b/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b3.yaml deleted file mode 100644 index a39a58bf3fd..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b3.yaml +++ /dev/null @@ -1,116 +0,0 @@ -model: - class_path: otx.algo.classification.torchvision_model.OTXTVModel - init_args: - backbone: efficientnet_b3 - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.01 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: torch.optim.lr_scheduler.CosineAnnealingLR - init_args: - T_max: 100000 - eta_min: 0 - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml - -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.RandomResizedCrop - init_args: - size: - - 224 - - 224 - antialias: True - - class_path: torchvision.transforms.v2.RandomHorizontalFlip - init_args: - p: 0.5 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - sampler: - class_path: otx.algo.samplers.balanced_sampler.BalancedSampler - val_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - test_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 diff --git a/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b4.yaml b/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b4.yaml deleted file mode 100644 index 0ac4a129893..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_b4.yaml +++ /dev/null @@ -1,116 +0,0 @@ -model: - class_path: otx.algo.classification.torchvision_model.OTXTVModel - init_args: - backbone: efficientnet_b4 - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.01 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: torch.optim.lr_scheduler.CosineAnnealingLR - init_args: - T_max: 100000 - eta_min: 0 - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml - -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.RandomResizedCrop - init_args: - size: - - 224 - - 224 - antialias: True - - class_path: torchvision.transforms.v2.RandomHorizontalFlip - init_args: - p: 0.5 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - sampler: - class_path: otx.algo.samplers.balanced_sampler.BalancedSampler - val_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - test_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 diff --git a/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_v2_l.yaml b/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_v2_l.yaml deleted file mode 100644 index 43883ad9b15..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/tv_efficientnet_v2_l.yaml +++ /dev/null @@ -1,116 +0,0 @@ -model: - class_path: otx.algo.classification.torchvision_model.OTXTVModel - init_args: - backbone: efficientnet_v2_l - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.01 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: torch.optim.lr_scheduler.CosineAnnealingLR - init_args: - T_max: 100000 - eta_min: 0 - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml - -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.RandomResizedCrop - init_args: - size: - - 224 - - 224 - antialias: True - - class_path: torchvision.transforms.v2.RandomHorizontalFlip - init_args: - p: 0.5 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - sampler: - class_path: otx.algo.samplers.balanced_sampler.BalancedSampler - val_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - test_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 diff --git a/src/otx/recipe/classification/multi_class_cls/tv_mobilenet_v3_small.yaml b/src/otx/recipe/classification/multi_class_cls/tv_mobilenet_v3_small.yaml deleted file mode 100644 index 4bfc03f4e4e..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/tv_mobilenet_v3_small.yaml +++ /dev/null @@ -1,116 +0,0 @@ -model: - class_path: otx.algo.classification.torchvision_model.OTXTVModel - init_args: - backbone: mobilenet_v3_small - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.01 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: torch.optim.lr_scheduler.CosineAnnealingLR - init_args: - T_max: 100000 - eta_min: 0 - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml - -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.RandomResizedCrop - init_args: - size: - - 224 - - 224 - antialias: True - - class_path: torchvision.transforms.v2.RandomHorizontalFlip - init_args: - p: 0.5 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - sampler: - class_path: otx.algo.samplers.balanced_sampler.BalancedSampler - val_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - test_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 diff --git a/src/otx/recipe/classification/multi_class_cls/tv_resnet_50.yaml b/src/otx/recipe/classification/multi_class_cls/tv_resnet_50.yaml deleted file mode 100644 index 9c779a6bc7e..00000000000 --- a/src/otx/recipe/classification/multi_class_cls/tv_resnet_50.yaml +++ /dev/null @@ -1,116 +0,0 @@ -model: - class_path: otx.algo.classification.torchvision_model.OTXTVModel - init_args: - backbone: resnet50 - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.01 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: torch.optim.lr_scheduler.CosineAnnealingLR - init_args: - T_max: 100000 - eta_min: 0 - -metric: - class_path: torchmetrics.classification.accuracy.Accuracy - init_args: - task: multiclass - num_classes: 1000 - -engine: - task: MULTI_CLASS_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml - -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - config: - train_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.RandomResizedCrop - init_args: - size: - - 224 - - 224 - antialias: True - - class_path: torchvision.transforms.v2.RandomHorizontalFlip - init_args: - p: 0.5 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - sampler: - class_path: otx.algo.samplers.balanced_sampler.BalancedSampler - val_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 - test_subset: - batch_size: 96 - transforms: - - class_path: torchvision.transforms.v2.Resize - init_args: - size: - - 224 - - 224 - - class_path: torchvision.transforms.v2.ToImage - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: True - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: - - 0.485 - - 0.456 - - 0.406 - std: - - 0.229 - - 0.224 - - 0.225 diff --git a/src/otx/recipe/classification/multi_label_cls/efficientnet_b0_light.yaml b/src/otx/recipe/classification/multi_label_cls/efficientnet_b0_light.yaml deleted file mode 100644 index 152bbf3df85..00000000000 --- a/src/otx/recipe/classification/multi_label_cls/efficientnet_b0_light.yaml +++ /dev/null @@ -1,63 +0,0 @@ -model: - class_path: otx.algo.classification.efficientnet_b0.EfficientNetB0ForMultilabelCls - init_args: - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0049 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.MultilabelAccuracy - init_args: - num_labels: 1000 - threshold: 0.5 - average: micro - -engine: - task: MULTI_LABEL_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - task: MULTI_LABEL_CLS - config: - data_format: datumaro - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs - val_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs - test_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs diff --git a/src/otx/recipe/classification/multi_label_cls/efficientnet_v2_light.yaml b/src/otx/recipe/classification/multi_label_cls/efficientnet_v2_light.yaml deleted file mode 100644 index 1b2d94d0d16..00000000000 --- a/src/otx/recipe/classification/multi_label_cls/efficientnet_v2_light.yaml +++ /dev/null @@ -1,68 +0,0 @@ -model: - class_path: otx.algo.classification.efficientnet_v2.EfficientNetV2ForMultilabelCls - init_args: - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0071 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.MultilabelAccuracy - init_args: - num_labels: 1000 - threshold: 0.5 - average: micro - -engine: - task: MULTI_LABEL_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - task: MULTI_LABEL_CLS - config: - data_format: datumaro - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - direction: horizontal - prob: 0.5 - type: RandomFlip - - type: PackInputs - val_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs - test_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs diff --git a/src/otx/recipe/classification/multi_label_cls/mobilenet_v3_large_light.yaml b/src/otx/recipe/classification/multi_label_cls/mobilenet_v3_large_light.yaml deleted file mode 100644 index 4ac46e98674..00000000000 --- a/src/otx/recipe/classification/multi_label_cls/mobilenet_v3_large_light.yaml +++ /dev/null @@ -1,71 +0,0 @@ -model: - class_path: otx.algo.classification.mobilenet_v3_large.MobileNetV3ForMultilabelCls - init_args: - num_classes: 1000 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0058 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 10 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.MultilabelAccuracy - init_args: - num_labels: 1000 - threshold: 0.5 - average: micro - -engine: - task: MULTI_LABEL_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - task: MULTI_LABEL_CLS - config: - data_format: datumaro - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - direction: horizontal - prob: 0.5 - type: RandomFlip - - type: PackInputs - val_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs - test_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs diff --git a/src/otx/recipe/classification/multi_label_cls/openvino_model.yaml b/src/otx/recipe/classification/multi_label_cls/openvino_model.yaml deleted file mode 100644 index ed5f1c239d9..00000000000 --- a/src/otx/recipe/classification/multi_label_cls/openvino_model.yaml +++ /dev/null @@ -1,39 +0,0 @@ -model: - class_path: otx.core.model.entity.classification.OVMultilabelClassificationModel - init_args: - num_classes: 1000 - model_name: openvino.xml - async_inference: True - use_throughput_mode: False - model_type: Classification - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 1e-3 - weight_decay: 0.0 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: min - factor: 0.1 - patience: 1 - monitor: train/loss - -engine: - task: MULTI_LABEL_CLS - device: cpu - -callback_monitor: val/accuracy - -data: ../../_base_/data/torchvision_base.yaml - -overrides: - data: - task: MULTI_LABEL_CLS - config: - image_color_channel: RGB - data_format: datumaro - test_subset: - batch_size: 128 diff --git a/src/otx/recipe/classification/multi_label_cls/otx_deit_tiny.yaml b/src/otx/recipe/classification/multi_label_cls/otx_deit_tiny.yaml deleted file mode 100644 index 97e28e4253a..00000000000 --- a/src/otx/recipe/classification/multi_label_cls/otx_deit_tiny.yaml +++ /dev/null @@ -1,67 +0,0 @@ -model: - class_path: otx.algo.classification.deit_tiny.DeitTinyForMultilabelCls - init_args: - num_classes: 1000 - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.0001 - weight_decay: 0.05 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 10 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 1 - monitor: val/accuracy - -metric: - class_path: torchmetrics.classification.accuracy.MultilabelAccuracy - init_args: - num_labels: 1000 - threshold: 0.5 - average: micro - -engine: - task: MULTI_LABEL_CLS - device: auto - -callback_monitor: val/accuracy - -data: ../../_base_/data/mmpretrain_base.yaml -overrides: - max_epochs: 90 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - task: MULTI_LABEL_CLS - config: - data_format: datumaro - train_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs - val_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs - test_subset: - transforms: - - type: LoadImageFromFile - - backend: cv2 - scale: 224 - type: Resize - - type: PackInputs diff --git a/src/otx/recipe/detection/atss_mobilenetv2.yaml b/src/otx/recipe/detection/atss_mobilenetv2.yaml deleted file mode 100644 index 2626a833bcc..00000000000 --- a/src/otx/recipe/detection/atss_mobilenetv2.yaml +++ /dev/null @@ -1,112 +0,0 @@ -model: - class_path: otx.algo.detection.atss.ATSS - init_args: - num_classes: 1000 - variant: mobilenetv2 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.004 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - train_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - type: MinIoURandomCrop - min_ious: - - 0.1 - - 0.3 - - 0.5 - - 0.7 - - 0.9 - min_crop_size: 0.3 - - type: Resize - scale: - - 992 - - 736 - keep_ratio: false - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 992 - - 736 - keep_ratio: false - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 992 - - 736 - keep_ratio: false - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/atss_mobilenetv2_tile.yaml b/src/otx/recipe/detection/atss_mobilenetv2_tile.yaml deleted file mode 100644 index bc8197fdccb..00000000000 --- a/src/otx/recipe/detection/atss_mobilenetv2_tile.yaml +++ /dev/null @@ -1,115 +0,0 @@ -model: - class_path: otx.algo.detection.atss.ATSS - init_args: - num_classes: 1000 - variant: mobilenetv2 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.004 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - tile_config: - enable_tiler: true - enable_adaptive_tiling: true - train_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - type: MinIoURandomCrop - min_ious: - - 0.1 - - 0.3 - - 0.5 - - 0.7 - - 0.9 - min_crop_size: 0.3 - - type: Resize - scale: - - 992 - - 736 - keep_ratio: false - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 992 - - 736 - keep_ratio: false - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 992 - - 736 - keep_ratio: false - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/atss_r50_fpn.yaml b/src/otx/recipe/detection/atss_r50_fpn.yaml deleted file mode 100644 index 4aa71b6d416..00000000000 --- a/src/otx/recipe/detection/atss_r50_fpn.yaml +++ /dev/null @@ -1,93 +0,0 @@ -model: - class_path: otx.algo.detection.atss.ATSSR50FPN - init_args: - num_classes: 1000 - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 1e-3 - weight_decay: 0.0 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml - -overrides: - gradient_clip_val: 35.0 - data: - config: - train_subset: - batch_size: 2 - transforms: - - backend_args: null - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - keep_ratio: true - scale: - - 1333 - - 800 - type: Resize - - prob: 0.5 - type: RandomFlip - - type: PackDetInputs - val_subset: - batch_size: 1 - transforms: - - backend_args: null - type: LoadImageFromFile - - keep_ratio: true - scale: - - 1333 - - 800 - type: Resize - - type: LoadAnnotations - with_bbox: true - - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - type: PackDetInputs - test_subset: - batch_size: 1 - transforms: - - backend_args: null - type: LoadImageFromFile - - keep_ratio: true - scale: - - 1333 - - 800 - type: Resize - - type: LoadAnnotations - with_bbox: true - - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - type: PackDetInputs diff --git a/src/otx/recipe/detection/atss_resnext101.yaml b/src/otx/recipe/detection/atss_resnext101.yaml deleted file mode 100644 index 8e77b5c5866..00000000000 --- a/src/otx/recipe/detection/atss_resnext101.yaml +++ /dev/null @@ -1,112 +0,0 @@ -model: - class_path: otx.algo.detection.atss.ATSS - init_args: - num_classes: 1000 - variant: resnext101 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.004 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - train_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - type: MinIoURandomCrop - min_ious: - - 0.1 - - 0.3 - - 0.5 - - 0.7 - - 0.9 - min_crop_size: 0.3 - - type: Resize - scale: - - 992 - - 736 - keep_ratio: false - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 992 - - 736 - keep_ratio: false - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 992 - - 736 - keep_ratio: false - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/openvino_model.yaml b/src/otx/recipe/detection/openvino_model.yaml deleted file mode 100644 index 31f5a10aa7b..00000000000 --- a/src/otx/recipe/detection/openvino_model.yaml +++ /dev/null @@ -1,38 +0,0 @@ -model: - class_path: otx.core.model.entity.detection.OVDetectionModel - init_args: - num_classes: 80 - model_name: ssd300 - use_throughput_mode: True - model_type: "SSD" - async_inference: True - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 1e-3 - weight_decay: 0.0 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: min - factor: 0.1 - patience: 9 - monitor: train/loss - -engine: - task: DETECTION - device: cpu - -callback_monitor: val/map_50 - -data: ../_base_/data/torchvision_base.yaml -overrides: - data: - task: DETECTION - config: - image_color_channel: RGB - data_format: coco_instances - test_subset: - batch_size: 64 diff --git a/src/otx/recipe/detection/rtmdet_tiny.yaml b/src/otx/recipe/detection/rtmdet_tiny.yaml deleted file mode 100644 index ea62d8ca6cc..00000000000 --- a/src/otx/recipe/detection/rtmdet_tiny.yaml +++ /dev/null @@ -1,146 +0,0 @@ -model: - class_path: otx.algo.detection.rtmdet.RTMDet - init_args: - num_classes: 80 - variant: tiny - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 1e-3 - weight_decay: 0.0 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: min - factor: 0.1 - patience: 9 - monitor: train/loss - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - precision: 32 - data: - config: - train_subset: - batch_size: 32 - transforms: - - backend_args: null - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - img_scale: - - 640 - - 640 - max_cached_images: 20 - pad_val: 114.0 - random_pop: false - type: CachedMosaic - - keep_ratio: true - ratio_range: - - 0.5 - - 2.0 - scale: - - 1280 - - 1280 - type: RandomResize - - crop_size: - - 640 - - 640 - type: RandomCrop - - type: YOLOXHSVRandomAug - - prob: 0.5 - type: RandomFlip - - pad_val: - img: - - 114 - - 114 - - 114 - size: - - 640 - - 640 - type: Pad - - img_scale: - - 640 - - 640 - max_cached_images: 10 - pad_val: - - 114 - - 114 - - 114 - prob: 0.5 - random_pop: false - ratio_range: - - 1.0 - - 1.0 - type: CachedMixUp - - type: PackDetInputs - val_subset: - batch_size: 5 - transforms: - - backend_args: null - type: LoadImageFromFile - - keep_ratio: true - scale: - - 640 - - 640 - type: Resize - - pad_val: - img: - - 114 - - 114 - - 114 - size: - - 640 - - 640 - type: Pad - - type: LoadAnnotations - with_bbox: true - - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - type: PackDetInputs - test_subset: - batch_size: 5 - transforms: - - backend_args: null - type: LoadImageFromFile - - keep_ratio: true - scale: - - 640 - - 640 - type: Resize - - pad_val: - img: - - 114 - - 114 - - 114 - size: - - 640 - - 640 - type: Pad - - type: LoadAnnotations - with_bbox: true - - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - type: PackDetInputs diff --git a/src/otx/recipe/detection/ssd_mobilenetv2.yaml b/src/otx/recipe/detection/ssd_mobilenetv2.yaml deleted file mode 100644 index 5e9115d2727..00000000000 --- a/src/otx/recipe/detection/ssd_mobilenetv2.yaml +++ /dev/null @@ -1,118 +0,0 @@ -model: - class_path: otx.algo.detection.ssd.SSD - init_args: - num_classes: 80 - variant: mobilenetv2 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.01 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - train_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - type: PhotoMetricDistortion - brightness_delta: 32 - contrast_range: - - 0.5 - - 1.5 - hue_delta: 18 - - type: MinIoURandomCrop - min_ious: - - 0.1 - - 0.3 - - 0.5 - - 0.7 - - 0.9 - min_crop_size: 0.3 - - type: Resize - scale: - - 864 - - 864 - keep_ratio: false - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 864 - - 864 - keep_ratio: false - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 864 - - 864 - keep_ratio: false - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/ssd_mobilenetv2_tile.yaml b/src/otx/recipe/detection/ssd_mobilenetv2_tile.yaml deleted file mode 100644 index b4c16b90b9a..00000000000 --- a/src/otx/recipe/detection/ssd_mobilenetv2_tile.yaml +++ /dev/null @@ -1,121 +0,0 @@ -model: - class_path: otx.algo.detection.ssd.SSD - init_args: - num_classes: 80 - variant: mobilenetv2 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.01 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - tile_config: - enable_tiler: true - enable_adaptive_tiling: true - train_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - type: PhotoMetricDistortion - brightness_delta: 32 - contrast_range: - - 0.5 - - 1.5 - hue_delta: 18 - - type: MinIoURandomCrop - min_ious: - - 0.1 - - 0.3 - - 0.5 - - 0.7 - - 0.9 - min_crop_size: 0.3 - - type: Resize - scale: - - 864 - - 864 - keep_ratio: false - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 864 - - 864 - keep_ratio: false - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 864 - - 864 - keep_ratio: false - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/yolox_l.yaml b/src/otx/recipe/detection/yolox_l.yaml deleted file mode 100644 index 9a969f92c72..00000000000 --- a/src/otx/recipe/detection/yolox_l.yaml +++ /dev/null @@ -1,143 +0,0 @@ -model: - class_path: otx.algo.detection.yolox.YoloX - init_args: - num_classes: 80 - variant: l - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.001 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - image_color_channel: BGR - train_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - type: CachedMosaic - random_pop: false - pad_val: 114.0 - max_cached_images: 20 - img_scale: - - 640 - - 640 - - type: RandomAffine - scaling_ratio_range: - - 0.1 - - 2.0 - border: - - -320 - - -320 - - type: CachedMixUp - img_scale: - - 640 - - 640 - ratio_range: - - 1.0 - - 1.0 - prob: 0.5 - random_pop: false - pad_val: - - 114 - - 114 - - 114 - max_cached_images: 10 - - type: YOLOXHSVRandomAug - - type: Resize - scale: - - 640 - - 640 - keep_ratio: true - - type: RandomFlip - prob: 0.5 - - type: Pad - pad_to_square: true - pad_val: 114 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 640 - - 640 - keep_ratio: true - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 640 - - 640 - keep_ratio: true - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/yolox_l_tile.yaml b/src/otx/recipe/detection/yolox_l_tile.yaml deleted file mode 100644 index 9ae71acacb2..00000000000 --- a/src/otx/recipe/detection/yolox_l_tile.yaml +++ /dev/null @@ -1,124 +0,0 @@ -model: - class_path: otx.algo.detection.yolox.YoloX - init_args: - num_classes: 80 - variant: l - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.001 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - tile_config: - enable_tiler: true - enable_adaptive_tiling: true - image_color_channel: BGR - train_subset: - num_workers: 4 - batch_size: 8 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: LoadAnnotations - with_bbox: true - - type: YOLOXHSVRandomAug - - type: Resize - scale: - - 640 - - 640 - keep_ratio: false - - type: RandomFlip - prob: 0.5 - - type: Pad - pad_to_square: true - pad_val: 114 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - num_workers: 4 - batch_size: 8 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: Resize - scale: - - 640 - - 640 - keep_ratio: false - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - num_workers: 4 - batch_size: 8 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: Resize - scale: - - 640 - - 640 - keep_ratio: false - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/yolox_s.yaml b/src/otx/recipe/detection/yolox_s.yaml deleted file mode 100644 index 825673fb5a2..00000000000 --- a/src/otx/recipe/detection/yolox_s.yaml +++ /dev/null @@ -1,143 +0,0 @@ -model: - class_path: otx.algo.detection.yolox.YoloX - init_args: - num_classes: 80 - variant: s - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.001 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - image_color_channel: BGR - train_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - type: CachedMosaic - random_pop: false - pad_val: 114.0 - max_cached_images: 20 - img_scale: - - 640 - - 640 - - type: RandomAffine - scaling_ratio_range: - - 0.1 - - 2.0 - border: - - -320 - - -320 - - type: CachedMixUp - img_scale: - - 640 - - 640 - ratio_range: - - 1.0 - - 1.0 - prob: 0.5 - random_pop: false - pad_val: - - 114 - - 114 - - 114 - max_cached_images: 10 - - type: YOLOXHSVRandomAug - - type: Resize - scale: - - 640 - - 640 - keep_ratio: True - - type: RandomFlip - prob: 0.5 - - type: Pad - pad_to_square: true - pad_val: 114 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 640 - - 640 - keep_ratio: True - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 640 - - 640 - keep_ratio: True - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/yolox_s_tile.yaml b/src/otx/recipe/detection/yolox_s_tile.yaml deleted file mode 100644 index 2a3efb24412..00000000000 --- a/src/otx/recipe/detection/yolox_s_tile.yaml +++ /dev/null @@ -1,124 +0,0 @@ -model: - class_path: otx.algo.detection.yolox.YoloX - init_args: - num_classes: 80 - variant: s - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.001 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - tile_config: - enable_tiler: true - enable_adaptive_tiling: true - image_color_channel: BGR - train_subset: - num_workers: 4 - batch_size: 8 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: LoadAnnotations - with_bbox: true - - type: YOLOXHSVRandomAug - - type: Resize - scale: - - 640 - - 640 - keep_ratio: false - - type: RandomFlip - prob: 0.5 - - type: Pad - pad_to_square: true - pad_val: 114 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - num_workers: 4 - batch_size: 8 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: Resize - scale: - - 640 - - 640 - keep_ratio: false - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - num_workers: 4 - batch_size: 8 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: Resize - scale: - - 640 - - 640 - keep_ratio: false - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/yolox_tiny.yaml b/src/otx/recipe/detection/yolox_tiny.yaml deleted file mode 100644 index af53c9061aa..00000000000 --- a/src/otx/recipe/detection/yolox_tiny.yaml +++ /dev/null @@ -1,135 +0,0 @@ -model: - class_path: otx.algo.detection.yolox.YoloXTiny - init_args: - num_classes: 80 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0002 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - train_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - type: CachedMosaic - random_pop: false - pad_val: 114.0 - max_cached_images: 20 - img_scale: - - 640 - - 640 - - type: RandomAffine - scaling_ratio_range: - - 0.5 - - 1.5 - border: - - -320 - - -320 - - type: PhotoMetricDistortion - brightness_delta: 32 - contrast_range: - - 0.5 - - 1.5 - saturation_range: - - 0.5 - - 1.5 - hue_delta: 18 - - type: Resize - scale: - - 640 - - 640 - keep_ratio: True - - type: RandomFlip - prob: 0.5 - - type: Pad - pad_to_square: true - pad_val: 114 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 416 - - 416 - keep_ratio: True - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - batch_size: 8 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 416 - - 416 - keep_ratio: True - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/yolox_tiny_tile.yaml b/src/otx/recipe/detection/yolox_tiny_tile.yaml deleted file mode 100644 index ab915e6a810..00000000000 --- a/src/otx/recipe/detection/yolox_tiny_tile.yaml +++ /dev/null @@ -1,131 +0,0 @@ -model: - class_path: otx.algo.detection.yolox.YoloX - init_args: - num_classes: 80 - variant: tiny - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.0002 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - tile_config: - enable_tiler: true - enable_adaptive_tiling: true - train_subset: - num_workers: 4 - batch_size: 8 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: LoadAnnotations - with_bbox: true - - type: PhotoMetricDistortion - brightness_delta: 32 - contrast_range: - - 0.5 - - 1.5 - saturation_range: - - 0.5 - - 1.5 - hue_delta: 18 - - type: Resize - scale: - - 640 - - 640 - keep_ratio: false - - type: RandomFlip - prob: 0.5 - - type: Pad - pad_to_square: true - pad_val: 114 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - num_workers: 4 - batch_size: 8 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: Resize - scale: - - 416 - - 416 - keep_ratio: false - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - num_workers: 4 - batch_size: 8 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: Resize - scale: - - 416 - - 416 - keep_ratio: false - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/yolox_x.yaml b/src/otx/recipe/detection/yolox_x.yaml deleted file mode 100644 index e6482900130..00000000000 --- a/src/otx/recipe/detection/yolox_x.yaml +++ /dev/null @@ -1,143 +0,0 @@ -model: - class_path: otx.algo.detection.yolox.YoloX - init_args: - num_classes: 80 - variant: x - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.001 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - image_color_channel: BGR - train_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - - type: LoadAnnotations - with_bbox: true - - type: CachedMosaic - random_pop: false - pad_val: 114.0 - max_cached_images: 20 - img_scale: - - 640 - - 640 - - type: RandomAffine - scaling_ratio_range: - - 0.5 - - 1.5 - border: - - -320 - - -320 - - type: CachedMixUp - img_scale: - - 640 - - 640 - ratio_range: - - 1.0 - - 1.0 - prob: 0.5 - random_pop: false - pad_val: - - 114 - - 114 - - 114 - max_cached_images: 10 - - type: YOLOXHSVRandomAug - - type: Resize - scale: - - 640 - - 640 - keep_ratio: True - - type: RandomFlip - prob: 0.5 - - type: Pad - pad_to_square: true - pad_val: 114 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 640 - - 640 - keep_ratio: True - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - - type: Resize - scale: - - 640 - - 640 - keep_ratio: True - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/detection/yolox_x_tile.yaml b/src/otx/recipe/detection/yolox_x_tile.yaml deleted file mode 100644 index edb87c2f3fc..00000000000 --- a/src/otx/recipe/detection/yolox_x_tile.yaml +++ /dev/null @@ -1,124 +0,0 @@ -model: - class_path: otx.algo.detection.yolox.YoloX - init_args: - num_classes: 80 - variant: x - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.001 - momentum: 0.9 - weight_decay: 0.0001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: torchmetrics.detection.mean_ap.MeanAveragePrecision - init_args: - box_format: xyxy - iou_type: bbox - -engine: - task: DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - gradient_clip_val: 35.0 - data: - config: - tile_config: - enable_tiler: true - enable_adaptive_tiling: true - image_color_channel: BGR - train_subset: - num_workers: 4 - batch_size: 4 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: LoadAnnotations - with_bbox: true - - type: YOLOXHSVRandomAug - - type: Resize - scale: - - 640 - - 640 - keep_ratio: True - - type: RandomFlip - prob: 0.5 - - type: Pad - pad_to_square: true - pad_val: 114 - - type: PackDetInputs - meta_keys: - - ori_filename - - flip_direction - - scale_factor - - gt_ann_ids - - flip - - ignored_labels - - ori_shape - - filename - - img_shape - - pad_shape - val_subset: - num_workers: 4 - batch_size: 4 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: Resize - scale: - - 640 - - 640 - keep_ratio: True - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape - test_subset: - num_workers: 4 - batch_size: 4 - transforms: - - type: LoadImageFromFile - to_float32: true - - type: Resize - scale: - - 640 - - 640 - keep_ratio: True - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - - type: PackDetInputs - meta_keys: - - ori_filename - - scale_factor - - ori_shape - - filename - - img_shape - - pad_shape diff --git a/src/otx/recipe/instance_segmentation/maskrcnn_efficientnetb2b.yaml b/src/otx/recipe/instance_segmentation/maskrcnn_efficientnetb2b.yaml deleted file mode 100644 index 0445d364220..00000000000 --- a/src/otx/recipe/instance_segmentation/maskrcnn_efficientnetb2b.yaml +++ /dev/null @@ -1,105 +0,0 @@ -model: - class_path: otx.algo.instance_segmentation.maskrcnn.MaskRCNN - init_args: - num_classes: 80 - variant: efficientnetb2b - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.007 - momentum: 0.9 - weight_decay: 0.001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: otx.algo.instance_segmentation.otx_instseg_evaluation.OTXMaskRLEMeanAveragePrecision - init_args: - box_format: xyxy - iou_type: segm - -engine: - task: INSTANCE_SEGMENTATION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - max_epochs: 100 - data: - task: INSTANCE_SEGMENTATION - config: - include_polygons: true - train_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: Pad - size_divisor: 32 - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - val_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - test_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor diff --git a/src/otx/recipe/instance_segmentation/maskrcnn_efficientnetb2b_tile.yaml b/src/otx/recipe/instance_segmentation/maskrcnn_efficientnetb2b_tile.yaml deleted file mode 100644 index 93030f8cf59..00000000000 --- a/src/otx/recipe/instance_segmentation/maskrcnn_efficientnetb2b_tile.yaml +++ /dev/null @@ -1,112 +0,0 @@ -model: - class_path: otx.algo.instance_segmentation.maskrcnn.MaskRCNN - init_args: - num_classes: 80 - variant: efficientnetb2b - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.007 - momentum: 0.9 - weight_decay: 0.001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: otx.algo.instance_segmentation.otx_instseg_evaluation.OTXMaskRLEMeanAveragePrecision - init_args: - box_format: xyxy - iou_type: segm - -engine: - task: INSTANCE_SEGMENTATION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - max_epochs: 100 - gradient_clip_val: 35.0 - data: - task: INSTANCE_SEGMENTATION - config: - tile_config: - enable_tiler: true - enable_adaptive_tiling: true - include_polygons: true - train_subset: - num_workers: 4 - batch_size: 4 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: Resize - keep_ratio: false - scale: - - 512 - - 512 - - type: Pad - size_divisor: 32 - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - val_subset: - num_workers: 4 - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: false - scale: - - 512 - - 512 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - test_subset: - num_workers: 4 - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: false - scale: - - 512 - - 512 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor diff --git a/src/otx/recipe/instance_segmentation/maskrcnn_r50.yaml b/src/otx/recipe/instance_segmentation/maskrcnn_r50.yaml deleted file mode 100644 index fc70a4c51fd..00000000000 --- a/src/otx/recipe/instance_segmentation/maskrcnn_r50.yaml +++ /dev/null @@ -1,106 +0,0 @@ -model: - class_path: otx.algo.instance_segmentation.maskrcnn.MaskRCNN - init_args: - num_classes: 80 - variant: r50 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.007 - momentum: 0.9 - weight_decay: 0.001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: otx.algo.instance_segmentation.otx_instseg_evaluation.OTXMaskRLEMeanAveragePrecision - init_args: - box_format: xyxy - iou_type: segm - -engine: - task: INSTANCE_SEGMENTATION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - max_epochs: 100 - gradient_clip_val: 35.0 - data: - task: INSTANCE_SEGMENTATION - config: - include_polygons: true - train_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: Pad - size_divisor: 32 - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - val_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - test_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor diff --git a/src/otx/recipe/instance_segmentation/maskrcnn_r50_tile.yaml b/src/otx/recipe/instance_segmentation/maskrcnn_r50_tile.yaml deleted file mode 100644 index 047b18e67e2..00000000000 --- a/src/otx/recipe/instance_segmentation/maskrcnn_r50_tile.yaml +++ /dev/null @@ -1,112 +0,0 @@ -model: - class_path: otx.algo.instance_segmentation.maskrcnn.MaskRCNN - init_args: - num_classes: 80 - variant: r50 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.007 - momentum: 0.9 - weight_decay: 0.001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: otx.algo.instance_segmentation.otx_instseg_evaluation.OTXMaskRLEMeanAveragePrecision - init_args: - box_format: xyxy - iou_type: segm - -engine: - task: INSTANCE_SEGMENTATION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - max_epochs: 100 - gradient_clip_val: 35.0 - data: - task: INSTANCE_SEGMENTATION - config: - tile_config: - enable_tiler: true - enable_adaptive_tiling: true - include_polygons: true - train_subset: - num_workers: 4 - batch_size: 4 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: Resize - keep_ratio: false - scale: - - 512 - - 512 - - type: Pad - size_divisor: 32 - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - val_subset: - num_workers: 4 - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: false - scale: - - 512 - - 512 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - test_subset: - num_workers: 4 - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: false - scale: - - 512 - - 512 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor diff --git a/src/otx/recipe/instance_segmentation/maskrcnn_swint.yaml b/src/otx/recipe/instance_segmentation/maskrcnn_swint.yaml deleted file mode 100644 index 640a6ec7d2e..00000000000 --- a/src/otx/recipe/instance_segmentation/maskrcnn_swint.yaml +++ /dev/null @@ -1,103 +0,0 @@ -model: - class_path: otx.algo.instance_segmentation.maskrcnn.MaskRCNNSwinT - init_args: - num_classes: 80 - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.0001 - weight_decay: 0.05 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: otx.algo.instance_segmentation.otx_instseg_evaluation.OTXMaskRLEMeanAveragePrecision - init_args: - box_format: xyxy - iou_type: segm - -engine: - task: INSTANCE_SEGMENTATION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - max_epochs: 100 - data: - task: INSTANCE_SEGMENTATION - config: - include_polygons: true - train_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: Resize - keep_ratio: true - scale: - - 1344 - - 1344 - - type: Pad - size_divisor: 32 - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - val_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: true - scale: - - 1344 - - 1344 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - test_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: true - scale: - - 1344 - - 1344 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor diff --git a/src/otx/recipe/instance_segmentation/maskrcnn_swint_tile.yaml b/src/otx/recipe/instance_segmentation/maskrcnn_swint_tile.yaml deleted file mode 100644 index ed9d02e26f2..00000000000 --- a/src/otx/recipe/instance_segmentation/maskrcnn_swint_tile.yaml +++ /dev/null @@ -1,106 +0,0 @@ -model: - class_path: otx.algo.instance_segmentation.maskrcnn.MaskRCNNSwinT - init_args: - num_classes: 80 - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.0001 - weight_decay: 0.05 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/map_50 - -metric: - class_path: otx.algo.instance_segmentation.otx_instseg_evaluation.OTXMaskRLEMeanAveragePrecision - init_args: - box_format: xyxy - iou_type: segm - -engine: - task: INSTANCE_SEGMENTATION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - max_epochs: 100 - data: - task: INSTANCE_SEGMENTATION - config: - tile_config: - enable_tiler: true - enable_adaptive_tiling: true - include_polygons: true - train_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: Resize - keep_ratio: false - scale: - - 512 - - 512 - - type: Pad - size_divisor: 32 - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - val_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: false - scale: - - 512 - - 512 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - test_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: false - scale: - - 512 - - 512 - - type: Pad - size_divisor: 32 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor diff --git a/src/otx/recipe/instance_segmentation/openvino_model.yaml b/src/otx/recipe/instance_segmentation/openvino_model.yaml deleted file mode 100644 index ce4f21cdea7..00000000000 --- a/src/otx/recipe/instance_segmentation/openvino_model.yaml +++ /dev/null @@ -1,38 +0,0 @@ -model: - class_path: otx.core.model.entity.instance_segmentation.OVInstanceSegmentationModel - init_args: - num_classes: 80 - model_name: openvino.xml - model_type: MaskRCNN - async_inference: True - use_throughput_mode: True - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.01 - -scheduler: - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: min - factor: 0.1 - patience: 9 - monitor: train/loss - -engine: - task: INSTANCE_SEGMENTATION - device: cpu - -callback_monitor: val/map_50 - -data: ../_base_/data/torchvision_base.yaml -overrides: - data: - task: INSTANCE_SEGMENTATION - config: - include_polygons: true - image_color_channel: RGB - data_format: coco_instances - test_subset: - batch_size: 2 diff --git a/src/otx/recipe/instance_segmentation/rtmdet_inst_tiny.yaml b/src/otx/recipe/instance_segmentation/rtmdet_inst_tiny.yaml deleted file mode 100644 index 486dbed5f63..00000000000 --- a/src/otx/recipe/instance_segmentation/rtmdet_inst_tiny.yaml +++ /dev/null @@ -1,146 +0,0 @@ -model: - class_path: otx.algo.instance_segmentation.rtmdet_inst.RTMDetInst - init_args: - num_classes: 80 - variant: tiny - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.004 - weight_decay: 0.05 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 20 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 9 - monitor: val/map_50 - min_lr: 4e-06 - -metric: - class_path: otx.algo.instance_segmentation.otx_instseg_evaluation.OTXMaskRLEMeanAveragePrecision - init_args: - box_format: xyxy - iou_type: segm - -engine: - task: INSTANCE_SEGMENTATION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - precision: 32 # 16/"16-true" does not work - max_epochs: 100 - gradient_clip_val: 35.0 - data: - task: INSTANCE_SEGMENTATION - config: - include_polygons: true - train_subset: - batch_size: 4 - num_workers: 10 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: CachedMosaic - img_scale: - - 640 - - 640 - pad_val: 114.0 - max_cached_images: 20 - random_pop: false - - type: RandomResize - scale: - - 1280 - - 1280 - ratio_range: - - 0.5 - - 2.0 - keep_ratio: true - - type: RandomCrop - crop_size: - - 640 - - 640 - - type: YOLOXHSVRandomAug - - type: RandomFlip - prob: 0.5 - - type: Pad - pad_to_square: true - pad_val: 114 - - type: CachedMixUp - img_scale: - - 640 - - 640 - ratio_range: - - 1.0 - - 1.0 - max_cached_images: 10 - random_pop: false - pad_val: - - 114 - - 114 - - 114 - prob: 0.5 - - type: FilterAnnotations - min_gt_bbox_wh: - - 1 - - 1 - - type: PackDetInputs - val_subset: - batch_size: 2 - num_workers: 10 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - scale: - - 640 - - 640 - keep_ratio: true - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - test_subset: - batch_size: 2 - num_workers: 10 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - scale: - - 640 - - 640 - keep_ratio: true - - type: Pad - pad_to_square: true - pad_val: 114 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor diff --git a/src/otx/recipe/rotated_detection/maskrcnn_efficientnetb2b.yaml b/src/otx/recipe/rotated_detection/maskrcnn_efficientnetb2b.yaml deleted file mode 100644 index 4ea4375cd68..00000000000 --- a/src/otx/recipe/rotated_detection/maskrcnn_efficientnetb2b.yaml +++ /dev/null @@ -1,99 +0,0 @@ -model: - class_path: otx.algo.instance_segmentation.maskrcnn.MaskRCNN - init_args: - num_classes: 80 - variant: efficientnetb2b - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.007 - momentum: 0.9 - weight_decay: 0.001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 9 - monitor: val/map_50 - -metric: - class_path: otx.algo.instance_segmentation.otx_instseg_evaluation.OTXMaskRLEMeanAveragePrecision - init_args: - box_format: xyxy - iou_type: segm - -engine: - task: ROTATED_DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - max_epochs: 100 - data: - task: ROTATED_DETECTION - config: - include_polygons: true - train_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - val_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - test_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor diff --git a/src/otx/recipe/rotated_detection/maskrcnn_r50.yaml b/src/otx/recipe/rotated_detection/maskrcnn_r50.yaml deleted file mode 100644 index 39de84411d3..00000000000 --- a/src/otx/recipe/rotated_detection/maskrcnn_r50.yaml +++ /dev/null @@ -1,99 +0,0 @@ -model: - class_path: otx.algo.instance_segmentation.maskrcnn.MaskRCNN - init_args: - num_classes: 80 - variant: r50 - -optimizer: - class_path: torch.optim.SGD - init_args: - lr: 0.007 - momentum: 0.9 - weight_decay: 0.001 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 9 - monitor: val/map_50 - -metric: - class_path: otx.algo.instance_segmentation.otx_instseg_evaluation.OTXMaskRLEMeanAveragePrecision - init_args: - box_format: xyxy - iou_type: segm - -engine: - task: ROTATED_DETECTION - device: auto - -callback_monitor: val/map_50 - -data: ../_base_/data/mmdet_base.yaml -overrides: - max_epochs: 100 - data: - task: ROTATED_DETECTION - config: - include_polygons: true - train_subset: - batch_size: 4 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: RandomFlip - prob: 0.5 - - type: PackDetInputs - val_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor - test_subset: - batch_size: 1 - transforms: - - type: LoadImageFromFile - backend_args: null - - type: Resize - keep_ratio: true - scale: - - 1024 - - 1024 - - type: LoadAnnotations - with_bbox: true - with_mask: true - - type: PackDetInputs - meta_keys: - - img_id - - img_path - - ori_shape - - img_shape - - scale_factor diff --git a/src/otx/recipe/semantic_segmentation/dino_v2.yaml b/src/otx/recipe/semantic_segmentation/dino_v2.yaml deleted file mode 100644 index 83916043a58..00000000000 --- a/src/otx/recipe/semantic_segmentation/dino_v2.yaml +++ /dev/null @@ -1,80 +0,0 @@ -model: - class_path: otx.algo.segmentation.dino_v2_seg.DinoV2Seg - init_args: - num_classes: 2 - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.001 - betas: - - 0.9 - - 0.999 - weight_decay: 0.0001 - -scheduler: - class_path: torch.optim.lr_scheduler.PolynomialLR - init_args: - total_iters: 100 - power: 0.9 - last_epoch: -1 - -metric: - class_path: torchmetrics.Dice - init_args: - num_classes: 2 - -engine: - task: SEMANTIC_SEGMENTATION - device: auto - -callback_monitor: val/Dice - -data: ../_base_/data/mmseg_base.yaml -overrides: - data: - config: - train_subset: - transforms: - - type: LoadImageFromFile - - reduce_zero_label: true - type: LoadAnnotations - - keep_ratio: false - ratio_range: - - 0.5 - - 2.0 - scale: - - 640 - - 640 - type: RandomResize - - cat_max_ratio: 0.75 - crop_size: - - 560 - - 560 - type: RandomCrop - - prob: 0.5 - type: RandomFlip - - type: PhotoMetricDistortion - - type: PackSegInputs - val_subset: - transforms: - - type: LoadImageFromFile - - keep_ratio: false - scale: - - 560 - - 560 - type: Resize - - reduce_zero_label: true - type: LoadAnnotations - - type: PackSegInputs - test_subset: - transforms: - - type: LoadImageFromFile - - keep_ratio: false - scale: - - 560 - - 560 - type: Resize - - reduce_zero_label: true - type: LoadAnnotations - - type: PackSegInputs diff --git a/src/otx/recipe/semantic_segmentation/litehrnet_18.yaml b/src/otx/recipe/semantic_segmentation/litehrnet_18.yaml deleted file mode 100644 index dab8e719c34..00000000000 --- a/src/otx/recipe/semantic_segmentation/litehrnet_18.yaml +++ /dev/null @@ -1,41 +0,0 @@ -model: - class_path: otx.algo.segmentation.litehrnet.LiteHRNet - init_args: - num_classes: 2 - variant: 18 - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 0.001 - betas: - - 0.9 - - 0.999 - weight_decay: 0.0 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/Dice - -metric: - class_path: torchmetrics.Dice - init_args: - num_classes: 2 - -engine: - task: SEMANTIC_SEGMENTATION - device: auto - -callback_monitor: val/Dice - -data: ../_base_/data/mmseg_base.yaml - -overrides: - max_epochs: 300 diff --git a/src/otx/recipe/semantic_segmentation/litehrnet_s.yaml b/src/otx/recipe/semantic_segmentation/litehrnet_s.yaml deleted file mode 100644 index 34ef5a95524..00000000000 --- a/src/otx/recipe/semantic_segmentation/litehrnet_s.yaml +++ /dev/null @@ -1,41 +0,0 @@ -model: - class_path: otx.algo.segmentation.litehrnet.LiteHRNet - init_args: - num_classes: 2 - variant: s - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 0.001 - betas: - - 0.9 - - 0.999 - weight_decay: 0.0 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/Dice - -metric: - class_path: torchmetrics.Dice - init_args: - num_classes: 2 - -engine: - task: SEMANTIC_SEGMENTATION - device: auto - -callback_monitor: val/Dice - -data: ../_base_/data/mmseg_base.yaml - -overrides: - max_epochs: 300 diff --git a/src/otx/recipe/semantic_segmentation/litehrnet_x.yaml b/src/otx/recipe/semantic_segmentation/litehrnet_x.yaml deleted file mode 100644 index d385e0eb7a7..00000000000 --- a/src/otx/recipe/semantic_segmentation/litehrnet_x.yaml +++ /dev/null @@ -1,41 +0,0 @@ -model: - class_path: otx.algo.segmentation.litehrnet.LiteHRNet - init_args: - num_classes: 2 - variant: x - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 0.001 - betas: - - 0.9 - - 0.999 - weight_decay: 0.0 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 100 - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - mode: max - factor: 0.1 - patience: 4 - monitor: val/Dice - -metric: - class_path: torchmetrics.Dice - init_args: - num_classes: 2 - -engine: - task: SEMANTIC_SEGMENTATION - device: auto - -callback_monitor: val/Dice - -data: ../_base_/data/mmseg_base.yaml - -overrides: - max_epochs: 300 diff --git a/src/otx/recipe/semantic_segmentation/openvino_model.yaml b/src/otx/recipe/semantic_segmentation/openvino_model.yaml deleted file mode 100644 index 90004e3c9b9..00000000000 --- a/src/otx/recipe/semantic_segmentation/openvino_model.yaml +++ /dev/null @@ -1,33 +0,0 @@ -model: - class_path: otx.core.model.entity.segmentation.OVSegmentationModel - init_args: - num_classes: 19 - model_name: drn-d-38 - async_inference: True - use_throughput_mode: True - model_type: "Segmentation" - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 1e-3 - weight_decay: 0.0 - -scheduler: - class_path: torch.optim.lr_scheduler.PolynomialLR - -engine: - task: SEMANTIC_SEGMENTATION - device: cpu - -callback_monitor: val/Dice - -data: ../_base_/data/torchvision_base.yaml -overrides: - data: - task: SEMANTIC_SEGMENTATION - config: - image_color_channel: RGB - data_format: common_semantic_segmentation_with_subset_dirs - test_subset: - batch_size: 64 diff --git a/src/otx/recipe/semantic_segmentation/segnext_b.yaml b/src/otx/recipe/semantic_segmentation/segnext_b.yaml deleted file mode 100644 index 72b08f030ad..00000000000 --- a/src/otx/recipe/semantic_segmentation/segnext_b.yaml +++ /dev/null @@ -1,39 +0,0 @@ -model: - class_path: otx.algo.segmentation.segnext.SegNext - init_args: - num_classes: 2 - variant: b - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.00006 - betas: - - 0.9 - - 0.999 - weight_decay: 0.01 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 20 - - class_path: torch.optim.lr_scheduler.PolynomialLR - init_args: - total_iters: 100 - power: 0.9 - last_epoch: -1 - -metric: - class_path: torchmetrics.Dice - init_args: - num_classes: 2 - -engine: - task: SEMANTIC_SEGMENTATION - device: auto - -callback_monitor: val/Dice - -data: ../_base_/data/mmseg_base.yaml -overrides: - max_epochs: 170 diff --git a/src/otx/recipe/semantic_segmentation/segnext_s.yaml b/src/otx/recipe/semantic_segmentation/segnext_s.yaml deleted file mode 100644 index fc016177d50..00000000000 --- a/src/otx/recipe/semantic_segmentation/segnext_s.yaml +++ /dev/null @@ -1,39 +0,0 @@ -model: - class_path: otx.algo.segmentation.segnext.SegNext - init_args: - num_classes: 2 - variant: s - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.00006 - betas: - - 0.9 - - 0.999 - weight_decay: 0.01 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 20 - - class_path: torch.optim.lr_scheduler.PolynomialLR - init_args: - total_iters: 100 - power: 0.9 - last_epoch: -1 - -metric: - class_path: torchmetrics.Dice - init_args: - num_classes: 2 - -engine: - task: SEMANTIC_SEGMENTATION - device: auto - -callback_monitor: val/Dice - -data: ../_base_/data/mmseg_base.yaml -overrides: - max_epochs: 170 diff --git a/src/otx/recipe/semantic_segmentation/segnext_t.yaml b/src/otx/recipe/semantic_segmentation/segnext_t.yaml deleted file mode 100644 index 00ff9c9fd41..00000000000 --- a/src/otx/recipe/semantic_segmentation/segnext_t.yaml +++ /dev/null @@ -1,39 +0,0 @@ -model: - class_path: otx.algo.segmentation.segnext.SegNext - init_args: - num_classes: 2 - variant: t - -optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.00006 - betas: - - 0.9 - - 0.999 - weight_decay: 0.01 - -scheduler: - - class_path: otx.algo.schedulers.warmup_schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 20 - - class_path: torch.optim.lr_scheduler.PolynomialLR - init_args: - total_iters: 100 - power: 0.9 - last_epoch: -1 - -metric: - class_path: torchmetrics.Dice - init_args: - num_classes: 2 - -engine: - task: SEMANTIC_SEGMENTATION - device: auto - -callback_monitor: val/Dice - -data: ../_base_/data/mmseg_base.yaml -overrides: - max_epochs: 170 diff --git a/src/otx/recipe/visual_prompting/openvino_model.yaml b/src/otx/recipe/visual_prompting/openvino_model.yaml deleted file mode 100644 index dd1ac23a79b..00000000000 --- a/src/otx/recipe/visual_prompting/openvino_model.yaml +++ /dev/null @@ -1,37 +0,0 @@ -model: - class_path: otx.core.model.entity.visual_prompting.OVVisualPromptingModel - init_args: - num_classes: 0 - model_name: segment_anything - model_type: Visual_Prompting - async_inference: False - use_throughput_mode: True - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 0.00001 - -scheduler: - class_path: torch.optim.lr_scheduler.ConstantLR - init_args: - factor: 1 - total_iters: -1 - -engine: - task: VISUAL_PROMPTING - device: cpu - -callback_monitor: val/Dice - -data: ../_base_/data/torchvision_base.yaml -overrides: - data: - task: VISUAL_PROMPTING - config: - data_format: coco_instances - vpm_config: - use_bbox: True - use_point: False - test_subset: - batch_size: 1 diff --git a/src/otx/recipe/visual_prompting/sam_tiny_vit.yaml b/src/otx/recipe/visual_prompting/sam_tiny_vit.yaml deleted file mode 100644 index ddd149bff47..00000000000 --- a/src/otx/recipe/visual_prompting/sam_tiny_vit.yaml +++ /dev/null @@ -1,96 +0,0 @@ -model: - class_path: otx.algo.visual_prompting.segment_anything.OTXSegmentAnything - init_args: - backbone: tiny_vit - num_classes: 0 - freeze_image_encoder: True - freeze_prompt_encoder: True - freeze_mask_decoder: False - # options - use_stability_score: False - return_single_mask: True - return_extra_metrics: False - stability_score_offset: 1. - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 0.00001 - -scheduler: - class_path: torch.optim.lr_scheduler.ConstantLR - init_args: - factor: 1 - total_iters: -1 - -engine: - task: VISUAL_PROMPTING - device: auto - -callback_monitor: val/Dice - -data: ../_base_/data/torchvision_base.yaml -overrides: - max_epochs: 100 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - task: VISUAL_PROMPTING - config: - data_format: coco_instances - vpm_config: - use_bbox: True - use_point: False - train_subset: - batch_size: 2 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 1024 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - val_subset: - batch_size: 1 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 1024 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - test_subset: - batch_size: 1 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 1024 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] diff --git a/src/otx/recipe/visual_prompting/sam_vit_b.yaml b/src/otx/recipe/visual_prompting/sam_vit_b.yaml deleted file mode 100644 index f164b14739e..00000000000 --- a/src/otx/recipe/visual_prompting/sam_vit_b.yaml +++ /dev/null @@ -1,96 +0,0 @@ -model: - class_path: otx.algo.visual_prompting.segment_anything.OTXSegmentAnything - init_args: - backbone: vit_b - num_classes: 0 - freeze_image_encoder: True - freeze_prompt_encoder: True - freeze_mask_decoder: False - # options - use_stability_score: False - return_single_mask: True - return_extra_metrics: False - stability_score_offset: 1. - -optimizer: - class_path: torch.optim.Adam - init_args: - lr: 0.00001 - -scheduler: - class_path: torch.optim.lr_scheduler.ConstantLR - init_args: - factor: 1 - total_iters: -1 - -engine: - task: VISUAL_PROMPTING - device: auto - -callback_monitor: val/Dice - -data: ../_base_/data/torchvision_base.yaml -overrides: - max_epochs: 100 - callbacks: - - class_path: lightning.pytorch.callbacks.EarlyStopping - init_args: - patience: 3 - data: - task: VISUAL_PROMPTING - config: - data_format: coco_instances - vpm_config: - use_bbox: True - use_point: False - train_subset: - batch_size: 2 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 1024 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - val_subset: - batch_size: 1 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 1024 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] - test_subset: - batch_size: 1 - num_workers: 4 - transforms: - - class_path: otx.core.data.transform_libs.torchvision.ResizetoLongestEdge - init_args: - size: 1024 - antialias: True - - class_path: otx.core.data.transform_libs.torchvision.PadtoSquare - - class_path: torchvision.transforms.v2.ToDtype - init_args: - dtype: ${as_torch_dtype:torch.float32} - scale: False - - class_path: torchvision.transforms.v2.Normalize - init_args: - mean: [123.675, 116.28, 103.53] - std: [58.395, 57.12, 57.375] diff --git a/src/otx/recipe/zero_shot_visual_prompting/openvino_model.yaml b/src/otx/recipe/zero_shot_visual_prompting/openvino_model.yaml deleted file mode 100644 index 3339b7b9a94..00000000000 --- a/src/otx/recipe/zero_shot_visual_prompting/openvino_model.yaml +++ /dev/null @@ -1,30 +0,0 @@ -model: - class_path: otx.core.model.entity.visual_prompting.OVZeroShotVisualPromptingModel - init_args: - num_classes: 0 - model_name: segment_anything - model_type: Zero_Shot_Visual_Prompting - async_inference: False - use_throughput_mode: True - root_reference_info: vpm_zsl_reference_infos - save_outputs: True - -engine: - task: ZERO_SHOT_VISUAL_PROMPTING - device: cpu - -callback_monitor: step - -data: ../_base_/data/torchvision_base.yaml -overrides: - max_epochs: 1 - limit_val_batches: 0 - data: - task: ZERO_SHOT_VISUAL_PROMPTING - config: - data_format: coco_instances - vpm_config: - use_bbox: True - use_point: False - test_subset: - batch_size: 1 diff --git a/src/otx/recipe/zero_shot_visual_prompting/sam_tiny_vit.yaml b/src/otx/recipe/zero_shot_visual_prompting/sam_tiny_vit.yaml deleted file mode 100644 index 459dad65802..00000000000 --- a/src/otx/recipe/zero_shot_visual_prompting/sam_tiny_vit.yaml +++ /dev/null @@ -1,45 +0,0 @@ -model: - class_path: otx.algo.visual_prompting.zero_shot_segment_anything.OTXZeroShotSegmentAnything - init_args: - backbone: tiny_vit - num_classes: 0 - freeze_image_encoder: True - freeze_prompt_encoder: True - freeze_mask_decoder: True - default_threshold_reference: 0.3 - default_threshold_target: 0.65 - is_cascade: False - save_outputs: True - root_reference_info: vpm_zsl_reference_infos - # options - use_stability_score: False - return_single_mask: False - return_extra_metrics: False - stability_score_offset: 1. - -engine: - task: ZERO_SHOT_VISUAL_PROMPTING - device: auto - -callback_monitor: step - -data: ../_base_/data/torchvision_base.yaml -overrides: - max_epochs: 1 - limit_val_batches: 0 - data: - task: ZERO_SHOT_VISUAL_PROMPTING - config: - data_format: coco_instances - vpm_config: - use_bbox: True - use_point: False - train_subset: - batch_size: 1 - num_workers: 4 - val_subset: - batch_size: 1 - num_workers: 4 - test_subset: - batch_size: 1 - num_workers: 4 diff --git a/src/otx/recipe/zero_shot_visual_prompting/sam_vit_b.yaml b/src/otx/recipe/zero_shot_visual_prompting/sam_vit_b.yaml deleted file mode 100644 index b7707139541..00000000000 --- a/src/otx/recipe/zero_shot_visual_prompting/sam_vit_b.yaml +++ /dev/null @@ -1,45 +0,0 @@ -model: - class_path: otx.algo.visual_prompting.zero_shot_segment_anything.OTXZeroShotSegmentAnything - init_args: - backbone: vit_b - num_classes: 0 - freeze_image_encoder: True - freeze_prompt_encoder: True - freeze_mask_decoder: True - default_threshold_reference: 0.3 - default_threshold_target: 0.65 - is_cascade: False - save_outputs: True - root_reference_info: vpm_zsl_reference_infos - # options - use_stability_score: False - return_single_mask: False - return_extra_metrics: False - stability_score_offset: 1. - -engine: - task: ZERO_SHOT_VISUAL_PROMPTING - device: auto - -callback_monitor: step - -data: ../_base_/data/torchvision_base.yaml -overrides: - max_epochs: 1 - limit_val_batches: 0 - data: - task: ZERO_SHOT_VISUAL_PROMPTING - config: - data_format: coco_instances - vpm_config: - use_bbox: True - use_point: False - train_subset: - batch_size: 1 - num_workers: 4 - val_subset: - batch_size: 1 - num_workers: 4 - test_subset: - batch_size: 1 - num_workers: 4 diff --git a/src/otx/recipes/__init__.py b/src/otx/recipes/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/__init__.py b/src/otx/recipes/stages/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/__init__.py b/src/otx/recipes/stages/_base_/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/data/__init__.py b/src/otx/recipes/stages/_base_/data/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/data/coco.py b/src/otx/recipes/stages/_base_/data/coco.py new file mode 100644 index 00000000000..58ebd90014c --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/coco.py @@ -0,0 +1,34 @@ +_base_ = ["./data.py", "./pipelines/coco_resize_hflip_pad.py"] + +__dataset_type = "CocoDataset" +__data_root = "data/coco/" + +__train_pipeline = {{_base_.train_pipeline}} +__test_pipeline = {{_base_.test_pipeline}} + +__samples_per_gpu = 2 + +data = dict( + samples_per_gpu=__samples_per_gpu, + workers_per_gpu=2, + train=dict( + type=__dataset_type, + ann_file=__data_root + "annotations/instances_train2017.json", + img_prefix=__data_root + "train2017/", + pipeline=__train_pipeline, + ), + val=dict( + type=__dataset_type, + ann_file=__data_root + "annotations/instances_val2017.json", + img_prefix=__data_root + "val2017/", + test_mode=True, + pipeline=__test_pipeline, + ), + test=dict( + type=__dataset_type, + ann_file=__data_root + "annotations/instances_val2017.json", + img_prefix=__data_root + "val2017/", + test_mode=True, + pipeline=__test_pipeline, + ), +) diff --git a/src/otx/recipes/stages/_base_/data/coco_inst_seg.py b/src/otx/recipes/stages/_base_/data/coco_inst_seg.py new file mode 100644 index 00000000000..37bc125f45e --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/coco_inst_seg.py @@ -0,0 +1,34 @@ +_base_ = ["./data.py", "./pipelines/coco_inst_seg_pipeline.py"] + +__dataset_type = "CocoDataset" +__data_root = "data/coco/" + +__train_pipeline = {{_base_.train_pipeline}} +__test_pipeline = {{_base_.test_pipeline}} + +__samples_per_gpu = 4 + +data = dict( + samples_per_gpu=__samples_per_gpu, + workers_per_gpu=2, + train=dict( + type=__dataset_type, + ann_file=__data_root + "annotations/instances_train2017.json", + img_prefix=__data_root + "train2017/", + pipeline=__train_pipeline, + ), + val=dict( + type=__dataset_type, + ann_file=__data_root + "annotations/instances_val2017.json", + img_prefix=__data_root + "val2017/", + test_mode=True, + pipeline=__test_pipeline, + ), + test=dict( + type=__dataset_type, + ann_file=__data_root + "annotations/instances_val2017.json", + img_prefix=__data_root + "val2017/", + test_mode=True, + pipeline=__test_pipeline, + ), +) diff --git a/src/otx/recipes/stages/_base_/data/coco_otx.py b/src/otx/recipes/stages/_base_/data/coco_otx.py new file mode 100644 index 00000000000..b80189f73bf --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/coco_otx.py @@ -0,0 +1,34 @@ +_base_ = ["./data.py", "./pipelines/coco_otx_pipeline.py"] + +__dataset_type = "CocoDataset" +__data_root = "data/coco/" + +__train_pipeline = {{_base_.train_pipeline}} +__test_pipeline = {{_base_.test_pipeline}} + +__samples_per_gpu = 2 + +data = dict( + samples_per_gpu=__samples_per_gpu, + workers_per_gpu=2, + train=dict( + type=__dataset_type, + ann_file=__data_root + "annotations/instances_train2017.json", + img_prefix=__data_root + "train2017/", + pipeline=__train_pipeline, + ), + val=dict( + type=__dataset_type, + ann_file=__data_root + "annotations/instances_val2017.json", + img_prefix=__data_root + "val2017/", + test_mode=True, + pipeline=__test_pipeline, + ), + test=dict( + type=__dataset_type, + ann_file=__data_root + "annotations/instances_val2017.json", + img_prefix=__data_root + "val2017/", + test_mode=True, + pipeline=__test_pipeline, + ), +) diff --git a/src/otx/recipes/stages/_base_/data/coco_ubt.py b/src/otx/recipes/stages/_base_/data/coco_ubt.py new file mode 100644 index 00000000000..d6f4676bc19 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/coco_ubt.py @@ -0,0 +1,46 @@ +_base_ = [ + "./data.py", + "./pipelines/ubt.py", +] + +__dataset_type = "CocoDataset" +__data_root_path = "data/coco/" + +__train_pipeline = {{_base_.train_pipeline}} +# __unlabeled_pipeline = __train_pipeline.copy().pop(1) # Removing 'LoadAnnotations' op +__unlabeled_pipeline = {{_base_.unlabeled_pipeline}} +__test_pipeline = {{_base_.test_pipeline}} + +__samples_per_gpu = 2 + +data = dict( + samples_per_gpu=__samples_per_gpu, + workers_per_gpu=2, + train=dict( + type=__dataset_type, + ann_file=__data_root_path + "annotations/instances_train2017.json", + img_prefix=__data_root_path + "train2017/", + pipeline=__train_pipeline, + ), + unlabeled=dict( + type=__dataset_type, + img_file=None, + img_prefix=__data_root_path + "train2017/", + filter_empty_gt=False, + pipeline=__unlabeled_pipeline, + ), + val=dict( + type=__dataset_type, + ann_file=__data_root_path + "annotations/instances_val2017.json", + img_prefix=__data_root_path + "val2017/", + test_mode=True, + pipeline=__test_pipeline, + ), + test=dict( + type=__dataset_type, + ann_file=__data_root_path + "annotations/instances_val2017.json", + img_prefix=__data_root_path + "val2017/", + test_mode=True, + pipeline=__test_pipeline, + ), +) diff --git a/src/otx/recipes/stages/_base_/data/custom_seg.py b/src/otx/recipes/stages/_base_/data/custom_seg.py new file mode 100644 index 00000000000..cf40f978820 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/custom_seg.py @@ -0,0 +1,32 @@ +_base_ = ["./data_seg.py"] + +__dataset_type = "CustomDataset" +__data_root = "tests/assets/common_semantic_segmentation_dataset" + +data = dict( + train=dict( + type="RepeatDataset", + times=1, + dataset=dict( + type=__dataset_type, + data_root=__data_root, + img_dir="train/images", + ann_dir="train/masks", + classes=["background", "person"], + ), + ), + val=dict( + type=__dataset_type, + data_root=__data_root, + img_dir="val/images", + ann_dir="val/masks", + classes=["background", "person"], + ), + test=dict( + type=__dataset_type, + data_root=__data_root, + img_dir="val/images", + ann_dir="val/masks", + classes=["background", "person"], + ), +) diff --git a/src/otx/recipes/stages/_base_/data/data.py b/src/otx/recipes/stages/_base_/data/data.py new file mode 100644 index 00000000000..a0f5de5a981 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/data.py @@ -0,0 +1,3 @@ +data_root_path = "data/" + +data = dict(samples_per_gpu=2, workers_per_gpu=2, train=dict(), val=dict(), test=dict()) diff --git a/src/otx/recipes/stages/_base_/data/data_seg.py b/src/otx/recipes/stages/_base_/data/data_seg.py new file mode 100644 index 00000000000..6ac161493d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/data_seg.py @@ -0,0 +1,29 @@ +_base_ = ["./pipelines/incr_seg.py"] + +__dataset_type = "" +__data_root = "" +__train_pipeline = {{_base_.train_pipeline}} +__test_pipeline = {{_base_.test_pipeline}} + +data = dict( + samples_per_gpu=4, + workers_per_gpu=2, + train=dict( + type="RepeatDataset", + times=1, + dataset=dict( + type=__dataset_type, + data_root=__data_root, + img_dir=None, + ann_dir=None, + split=None, + pipeline=__train_pipeline, + ), + ), + val=dict( + type=__dataset_type, data_root=__data_root, img_dir=None, ann_dir=None, split=None, pipeline=__test_pipeline + ), + test=dict( + type=__dataset_type, data_root=__data_root, img_dir=None, ann_dir=None, split=None, pipeline=__test_pipeline + ), +) diff --git a/src/otx/recipes/stages/_base_/data/pipelines/__init__.py b/src/otx/recipes/stages/_base_/data/pipelines/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/pipelines/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/data/pipelines/coco_inst_seg_pipeline.py b/src/otx/recipes/stages/_base_/data/pipelines/coco_inst_seg_pipeline.py new file mode 100644 index 00000000000..2df191d77e7 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/pipelines/coco_inst_seg_pipeline.py @@ -0,0 +1,32 @@ +dataset_type = "CocoDataset" +img_size = (1024, 1024) + +img_norm_cfg = dict(mean=(103.53, 116.28, 123.675), std=(1.0, 1.0, 1.0), to_rgb=False) + +train_pipeline = [ + dict(type="LoadImageFromFile"), + dict(type="LoadAnnotations", with_bbox=True, with_mask=True, poly2mask=False), + dict(type="Resize", img_scale=img_size, keep_ratio=False), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img", "gt_bboxes", "gt_labels", "gt_masks"]), +] + +test_pipeline = [ + dict(type="LoadImageFromFile"), + dict( + type="MultiScaleFlipAug", + img_scale=img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] diff --git a/src/otx/recipes/stages/_base_/data/pipelines/coco_otx_pipeline.py b/src/otx/recipes/stages/_base_/data/pipelines/coco_otx_pipeline.py new file mode 100644 index 00000000000..8115bc060d5 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/pipelines/coco_otx_pipeline.py @@ -0,0 +1,33 @@ +img_size = (992, 736) +img_norm_cfg = dict(mean=[0, 0, 0], std=[255, 255, 255], to_rgb=True) + +train_pipeline = [ + dict(type="LoadImageFromFile"), + dict(type="LoadAnnotations", with_bbox=True), + dict(type="MinIoURandomCrop", min_ious=(0.1, 0.3, 0.5, 0.7, 0.9), min_crop_size=0.3), + dict( + type="Resize", + img_scale=[(992, 736), (896, 736), (1088, 736), (992, 672), (992, 800)], + multiscale_mode="value", + keep_ratio=False, + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **img_norm_cfg), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img", "gt_bboxes", "gt_labels"]), +] +test_pipeline = [ + dict(type="LoadImageFromFile"), + dict( + type="MultiScaleFlipAug", + img_scale=img_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] diff --git a/src/otx/recipes/stages/_base_/data/pipelines/coco_resize_hflip_pad.py b/src/otx/recipes/stages/_base_/data/pipelines/coco_resize_hflip_pad.py new file mode 100644 index 00000000000..099fdf91c15 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/pipelines/coco_resize_hflip_pad.py @@ -0,0 +1,29 @@ +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +train_pipeline = [ + dict(type="LoadImageFromFile"), + dict(type="LoadAnnotations", with_bbox=True), + dict(type="Resize", img_scale=(1333, 800), keep_ratio=True), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img", "gt_bboxes", "gt_labels"]), +] + +test_pipeline = [ + dict(type="LoadImageFromFile"), + dict( + type="MultiScaleFlipAug", + img_scale=(1333, 800), + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] diff --git a/src/otx/recipes/stages/_base_/data/pipelines/incr_seg.py b/src/otx/recipes/stages/_base_/data/pipelines/incr_seg.py new file mode 100644 index 00000000000..f38ee96a4af --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/pipelines/incr_seg.py @@ -0,0 +1,31 @@ +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) +__img_scale = (544, 544) +__crop_size = (512, 512) + +train_pipeline = [ + dict(type="LoadImageFromFile"), + dict(type="LoadAnnotations"), + dict(type="Resize", img_scale=__img_scale, ratio_range=(0.5, 2.0), keep_ratio=False), + dict(type="RandomCrop", crop_size=__crop_size, cat_max_ratio=0.75), + dict(type="RandomFlip", prob=0.5, direction="horizontal"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size=__crop_size, pad_val=0, seg_pad_val=255), + dict(type="RandomRotate", prob=0.5, degree=30, pad_val=0, seg_pad_val=255), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img", "gt_semantic_seg"]), +] +test_pipeline = [ + dict(type="LoadImageFromFile"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_scale, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] diff --git a/src/otx/recipes/stages/_base_/data/pipelines/twocrop_pipeline.py b/src/otx/recipes/stages/_base_/data/pipelines/twocrop_pipeline.py new file mode 100644 index 00000000000..e607fb89fb3 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/pipelines/twocrop_pipeline.py @@ -0,0 +1,27 @@ +img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) +__resize_target_size = 224 + + +train_pipeline = [ + dict( + type="TwoCropTransform", + pipeline=[ + dict(type="Resize", size=__resize_target_size), + dict(type="RandomFlip", flip_prob=0.5, direction="horizontal"), + dict(type="AugMixAugment", config_str="augmix-m5-w3"), + dict(type="RandomRotate", p=0.35, angle=(-10, 10)), + dict(type="ToNumpy"), + dict(type="Normalize", **img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="ToTensor", keys=["gt_label"]), + dict(type="Collect", keys=["img", "gt_label"]), + ], + ) +] + +test_pipeline = [ + dict(type="Resize", size=__resize_target_size), + dict(type="Normalize", **img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] diff --git a/src/otx/recipes/stages/_base_/data/pipelines/ubt.py b/src/otx/recipes/stages/_base_/data/pipelines/ubt.py new file mode 100644 index 00000000000..96188ba9900 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/pipelines/ubt.py @@ -0,0 +1,126 @@ +__img_scale = (992, 736) +__img_norm_cfg = dict(mean=[0, 0, 0], std=[255, 255, 255], to_rgb=True) + +common_pipeline = [ + dict( + type="Resize", + img_scale=[ + (992, 736), + (896, 736), + (1088, 736), + (992, 672), + (992, 800), + ], + multiscale_mode="value", + keep_ratio=False, + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="BranchImage", key_map=dict(img="img0")), + dict(type="NDArrayToPILImage", keys=["img"]), + dict( + type="RandomApply", + transform_cfgs=[ + dict( + type="ColorJitter", + brightness=0.4, + contrast=0.4, + saturation=0.4, + hue=0.1, + ) + ], + p=0.8, + ), + dict(type="RandomGrayscale", p=0.2), + dict( + type="RandomApply", + transform_cfgs=[ + dict( + type="RandomGaussianBlur", + sigma_min=0.1, + sigma_max=2.0, + ) + ], + p=0.5, + ), + dict(type="PILImageToNDArray", keys=["img"]), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="NDArrayToTensor", keys=["img", "img0"]), + dict( + type="RandomErasing", + p=0.7, + scale=[0.05, 0.2], + ratio=[0.3, 3.3], + value="random", + ), + dict( + type="RandomErasing", + p=0.5, + scale=[0.02, 0.2], + ratio=[0.10, 6.0], + value="random", + ), + dict( + type="RandomErasing", + p=0.3, + scale=[0.02, 0.2], + ratio=[0.05, 8.0], + value="random", + ), +] + +train_pipeline = [ + dict(type="LoadImageFromFile"), + dict(type="LoadAnnotations", with_bbox=True), + dict(type="MinIoURandomCrop", min_ious=(0.1, 0.3, 0.5, 0.7, 0.9), min_crop_size=0.3), + *common_pipeline, + dict(type="ToTensor", keys=["gt_bboxes", "gt_labels"]), + dict( + type="ToDataContainer", + fields=[ + dict(key="img", stack=True), + dict(key="img0", stack=True), + dict(key="gt_bboxes"), + dict(key="gt_labels"), + ], + ), + dict( + type="Collect", + keys=["img", "img0", "gt_bboxes", "gt_labels"], + ), +] + +unlabeled_pipeline = [ + dict(type="LoadImageFromFile"), + *common_pipeline, + dict( + type="ToDataContainer", + fields=[ + dict(key="img", stack=True), + dict(key="img0", stack=True), + ], + ), + dict( + type="Collect", + keys=[ + "img", + "img0", + ], + ), +] + +test_pipeline = [ + dict(type="LoadImageFromFile"), + dict( + type="MultiScaleFlipAug", + img_scale=__img_scale, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="Normalize", **__img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ), +] diff --git a/src/otx/recipes/stages/_base_/data/seg_semisl.py b/src/otx/recipes/stages/_base_/data/seg_semisl.py new file mode 100644 index 00000000000..41263c0caaa --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/seg_semisl.py @@ -0,0 +1,12 @@ +__dataset_type = "" +__data_root = "" +__pipeline = "" + +data = dict( + train=dict( + type="RepeatDataset", times=1, dataset=dict(type=__dataset_type, data_root=__data_root, pipeline=__pipeline) + ), + val=dict(type=__dataset_type, data_root=__data_root, pipeline=__pipeline), + test=dict(type=__dataset_type, data_root=__data_root, pipeline=__pipeline), + unlabeled=dict(type=__dataset_type, data_root=__data_root, pipeline=__pipeline), +) diff --git a/src/otx/recipes/stages/_base_/data/selfsl_cls_data.py b/src/otx/recipes/stages/_base_/data/selfsl_cls_data.py new file mode 100644 index 00000000000..7dc6c00034f --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/selfsl_cls_data.py @@ -0,0 +1,50 @@ +"""Base Self-SL dataset.""" + +__resize_target_size = 224 +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +__train_pipeline_v0 = [ + dict(type="RandomResizedCrop", size=__resize_target_size), + dict(type="RandomFlip"), + dict( + type="RandomAppliedTrans", + transforms=[ + dict(type="OTXColorJitter", brightness=0.4, contrast=0.4, saturation=0.2, hue=0.1), + ], + p=0.8, + ), + dict(type="RandomGrayscale", gray_prob=0.2), + dict(type="GaussianBlur", sigma_min=0.1, sigma_max=2.0), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] +__train_pipeline_v1 = [ + dict(type="RandomResizedCrop", size=__resize_target_size), + dict(type="RandomFlip"), + dict( + type="RandomAppliedTrans", + transforms=[ + dict(type="OTXColorJitter", brightness=0.4, contrast=0.4, saturation=0.2, hue=0.1), + ], + p=0.8, + ), + dict(type="RandomGrayscale", gray_prob=0.2), + dict(type="RandomAppliedTrans", transforms=[dict(type="GaussianBlur", sigma_min=0.1, sigma_max=2.0)], p=0.1), + dict(type="RandomAppliedTrans", transforms=[dict(type="Solarize", thr=128)], p=0.2), + dict(type="Normalize", **__img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), +] + +data = dict( + samples_per_gpu=64, + workers_per_gpu=2, + train=dict( + type="SelfSLDataset", + pipeline=dict( + view0=__train_pipeline_v0, + view1=__train_pipeline_v1, + ), + ), +) diff --git a/src/otx/recipes/stages/_base_/data/selfsl_seg_data.py b/src/otx/recipes/stages/_base_/data/selfsl_seg_data.py new file mode 100644 index 00000000000..369850f47b8 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/selfsl_seg_data.py @@ -0,0 +1,40 @@ +"""Base Self-SL dataset.""" + +__resize_target_size = 224 +__img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + +__train_pipeline = [ + dict(type="LoadImageFromFile"), + dict(type="LoadAnnotations"), + dict( + type="TwoCropTransform", + view0=[ + dict(type="RandomResizedCrop", size=__resize_target_size), + dict(type="RandomFlip", prob=0.5, direction="horizontal"), + dict( + type="ProbCompose", + transforms=[dict(type="ColorJitter", brightness=0.4, contrast=0.4, saturation=0.2, hue=0.1)], + probs=[0.8], + ), + dict(type="RandomGrayscale", p=0.2), + dict(type="GaussianBlur", kernel_size=23), + dict(type="Normalize", **__img_norm_cfg), + ], + view1=[ + dict(type="RandomResizedCrop", size=__resize_target_size), + dict(type="RandomFlip", prob=0.5, direction="horizontal"), + dict( + type="ProbCompose", + transforms=[dict(type="ColorJitter", brightness=0.4, contrast=0.4, saturation=0.2, hue=0.1)], + probs=[0.8], + ), + dict(type="RandomGrayscale", p=0.2), + dict(type="ProbCompose", transforms=[dict(type="GaussianBlur", kernel_size=23)], probs=[0.1]), + dict(type="ProbCompose", transforms=[dict(type="Solarization", threshold=128)], probs=[0.2]), + dict(type="Normalize", **__img_norm_cfg), + ], + ), + dict(type="Collect", keys=["img", "gt_semantic_seg"]), +] + +data = dict(samples_per_gpu=16, workers_per_gpu=2, train=dict(type="OTXSegDataset", pipeline=__train_pipeline)) diff --git a/src/otx/recipes/stages/_base_/data/twocrop_data.py b/src/otx/recipes/stages/_base_/data/twocrop_data.py new file mode 100644 index 00000000000..be39eaefe57 --- /dev/null +++ b/src/otx/recipes/stages/_base_/data/twocrop_data.py @@ -0,0 +1,16 @@ +_base_ = ["./pipelines/twocrop_pipeline.py"] + +__dataset_type = "OTXClsDataset" + +__train_pipeline = {{_base_.train_pipeline}} +__test_pipeline = {{_base_.test_pipeline}} + +__samples_per_gpu = 16 + +data = dict( + samples_per_gpu=__samples_per_gpu, + workers_per_gpu=2, + train=dict(type=__dataset_type, pipeline=__train_pipeline), + val=dict(type=__dataset_type, test_mode=True, pipeline=__test_pipeline), + test=dict(type=__dataset_type, test_mode=True, pipeline=__test_pipeline), +) diff --git a/src/otx/recipes/stages/_base_/default.py b/src/otx/recipes/stages/_base_/default.py new file mode 100644 index 00000000000..2d94d2375dc --- /dev/null +++ b/src/otx/recipes/stages/_base_/default.py @@ -0,0 +1,10 @@ +_base_ = ["./dist/dist.py"] + +cudnn_benchmark = True + +seed = 5 +deterministic = False + +hparams = dict(dummy=0) + +task_adapt = dict(op="REPLACE") diff --git a/src/otx/recipes/stages/_base_/dist/__init__.py b/src/otx/recipes/stages/_base_/dist/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/dist/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/dist/dist.py b/src/otx/recipes/stages/_base_/dist/dist.py new file mode 100644 index 00000000000..e47a458bd3a --- /dev/null +++ b/src/otx/recipes/stages/_base_/dist/dist.py @@ -0,0 +1 @@ +dist_params = dict(backend="nccl", linear_scale_lr=True) diff --git a/src/otx/recipes/stages/_base_/logs/__init__.py b/src/otx/recipes/stages/_base_/logs/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/logs/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/logs/log.py b/src/otx/recipes/stages/_base_/logs/log.py new file mode 100644 index 00000000000..6460a4b29b6 --- /dev/null +++ b/src/otx/recipes/stages/_base_/logs/log.py @@ -0,0 +1 @@ +log_level = "INFO" diff --git a/src/otx/recipes/stages/_base_/logs/tensorboard_logger.py b/src/otx/recipes/stages/_base_/logs/tensorboard_logger.py new file mode 100644 index 00000000000..752f06f8357 --- /dev/null +++ b/src/otx/recipes/stages/_base_/logs/tensorboard_logger.py @@ -0,0 +1,7 @@ +_base_ = "./log.py" + +# yapf:disable +log_config = dict( + interval=100, hooks=[dict(type="TextLoggerHook", ignore_last=False), dict(type="TensorboardLoggerHook")] +) +# yapf:enable diff --git a/src/otx/recipes/stages/_base_/logs/text_logger.py b/src/otx/recipes/stages/_base_/logs/text_logger.py new file mode 100644 index 00000000000..75ff5010ba1 --- /dev/null +++ b/src/otx/recipes/stages/_base_/logs/text_logger.py @@ -0,0 +1,10 @@ +_base_ = "./log.py" + +# yapf:disable +log_config = dict( + interval=100, + hooks=[ + dict(type="TextLoggerHook"), + ], +) +# yapf:enable diff --git a/src/otx/recipes/stages/_base_/models/__init__.py b/src/otx/recipes/stages/_base_/models/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/models/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/models/classifiers/__init__.py b/src/otx/recipes/stages/_base_/models/classifiers/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/models/classifiers/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/models/classifiers/classifier.py b/src/otx/recipes/stages/_base_/models/classifiers/classifier.py new file mode 100644 index 00000000000..c5cf56893bb --- /dev/null +++ b/src/otx/recipes/stages/_base_/models/classifiers/classifier.py @@ -0,0 +1,11 @@ +_base_ = "../model.py" + +model = dict( + type="ImageClassifier", + task="classification", + pretrained=None, + backbone=dict(), + head=dict(in_channels=-1, loss=dict(type="CrossEntropyLoss", loss_weight=1.0), topk=(1, 5)), +) + +checkpoint_config = dict(type="CheckpointHookWithValResults") diff --git a/src/otx/recipes/stages/_base_/models/cls_semisl.py b/src/otx/recipes/stages/_base_/models/cls_semisl.py new file mode 100644 index 00000000000..44010d8b113 --- /dev/null +++ b/src/otx/recipes/stages/_base_/models/cls_semisl.py @@ -0,0 +1,10 @@ +_base_ = ["./classifiers/classifier.py"] + +model = dict( + type="SemiSLClassifier", + pretrained=None, + backbone=dict(), + head=dict( + type="SemiSLClsHead", + ), +) diff --git a/src/otx/recipes/stages/_base_/models/cls_supcon.py b/src/otx/recipes/stages/_base_/models/cls_supcon.py new file mode 100644 index 00000000000..030c57a8a23 --- /dev/null +++ b/src/otx/recipes/stages/_base_/models/cls_supcon.py @@ -0,0 +1,23 @@ +_base_ = "./model.py" + +model = dict( + type="SupConClassifier", + task="classification", + pretrained=None, + backbone=dict(), + neck=dict(type="GlobalAveragePooling"), + head=dict( + type="SupConClsHead", + num_classes=10, + in_channels=-1, + aux_mlp=dict(hid_channels=0, out_channels=1024), + loss=dict(type="CrossEntropyLoss", loss_weight=1.0), + aux_loss=dict( + type="BarlowTwinsLoss", + off_diag_penality=1.0 / 128.0, + loss_weight=1.0, + ), + ), +) + +checkpoint_config = dict(type="CheckpointHookWithValResults") diff --git a/src/otx/recipes/stages/_base_/models/detectors/__init__.py b/src/otx/recipes/stages/_base_/models/detectors/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/models/detectors/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/models/detectors/detector.py b/src/otx/recipes/stages/_base_/models/detectors/detector.py new file mode 100644 index 00000000000..19b9cb11f55 --- /dev/null +++ b/src/otx/recipes/stages/_base_/models/detectors/detector.py @@ -0,0 +1,5 @@ +_base_ = "../model.py" + +task = "detection" + +model = dict(train_cfg=dict(), test_cfg=dict()) diff --git a/src/otx/recipes/stages/_base_/models/model.py b/src/otx/recipes/stages/_base_/models/model.py new file mode 100644 index 00000000000..b9bc7b44a50 --- /dev/null +++ b/src/otx/recipes/stages/_base_/models/model.py @@ -0,0 +1,8 @@ +# base model settings +model = dict() + +load_from = None + +resume_from = None + +checkpoint_config = dict(interval=1, max_keep_ckpts=1) diff --git a/src/otx/recipes/stages/_base_/models/segmentors/__init__.py b/src/otx/recipes/stages/_base_/models/segmentors/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/models/segmentors/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/models/segmentors/segmentor.py b/src/otx/recipes/stages/_base_/models/segmentors/segmentor.py new file mode 100644 index 00000000000..6917ad8aa04 --- /dev/null +++ b/src/otx/recipes/stages/_base_/models/segmentors/segmentor.py @@ -0,0 +1,5 @@ +_base_ = "../model.py" + +task = "segmentation" + +model = dict(train_cfg=dict(mix_loss=dict(enable=False, weight=0.1)), test_cfg=dict(mode="whole")) diff --git a/src/otx/recipes/stages/_base_/optimizers/__init__.py b/src/otx/recipes/stages/_base_/optimizers/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/optimizers/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/optimizers/adam.py b/src/otx/recipes/stages/_base_/optimizers/adam.py new file mode 100644 index 00000000000..6e3dac8473b --- /dev/null +++ b/src/otx/recipes/stages/_base_/optimizers/adam.py @@ -0,0 +1,3 @@ +_base_ = "./optimizer.py" + +optimizer = dict(type="Adam", lr=0.003) diff --git a/src/otx/recipes/stages/_base_/optimizers/lars.py b/src/otx/recipes/stages/_base_/optimizers/lars.py new file mode 100644 index 00000000000..7de932d385a --- /dev/null +++ b/src/otx/recipes/stages/_base_/optimizers/lars.py @@ -0,0 +1,3 @@ +_base_ = "./optimizer.py" + +optimizer = dict(type="LARS", lr=0.3, momentum=0.9) diff --git a/src/otx/recipes/stages/_base_/optimizers/optimizer.py b/src/otx/recipes/stages/_base_/optimizers/optimizer.py new file mode 100644 index 00000000000..1a728f639fa --- /dev/null +++ b/src/otx/recipes/stages/_base_/optimizers/optimizer.py @@ -0,0 +1,4 @@ +# base optimizer settings +optimizer = dict() + +optimizer_config = dict(grad_clip=None) diff --git a/src/otx/recipes/stages/_base_/optimizers/sgd.py b/src/otx/recipes/stages/_base_/optimizers/sgd.py new file mode 100644 index 00000000000..860aadd51cf --- /dev/null +++ b/src/otx/recipes/stages/_base_/optimizers/sgd.py @@ -0,0 +1,3 @@ +_base_ = "./optimizer.py" + +optimizer = dict(type="SGD", lr=0.03, momentum=0.9) diff --git a/src/otx/recipes/stages/_base_/runners/__init__.py b/src/otx/recipes/stages/_base_/runners/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/runners/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/runners/epoch_runner.py b/src/otx/recipes/stages/_base_/runners/epoch_runner.py new file mode 100644 index 00000000000..6af0e0f141d --- /dev/null +++ b/src/otx/recipes/stages/_base_/runners/epoch_runner.py @@ -0,0 +1,5 @@ +_base_ = "./runner.py" + +runner = dict(type="EpochBasedRunner", max_epochs=1) + +workflow = [("train", 1)] diff --git a/src/otx/recipes/stages/_base_/runners/epoch_runner_cancel.py b/src/otx/recipes/stages/_base_/runners/epoch_runner_cancel.py new file mode 100644 index 00000000000..cf4939a3289 --- /dev/null +++ b/src/otx/recipes/stages/_base_/runners/epoch_runner_cancel.py @@ -0,0 +1,3 @@ +_base_ = "./epoch_runner.py" + +runner = dict(type="EpochRunnerWithCancel", max_epochs=300) diff --git a/src/otx/recipes/stages/_base_/runners/iter_runner.py b/src/otx/recipes/stages/_base_/runners/iter_runner.py new file mode 100644 index 00000000000..07b586124e8 --- /dev/null +++ b/src/otx/recipes/stages/_base_/runners/iter_runner.py @@ -0,0 +1,5 @@ +_base_ = "./runner.py" + +runner = dict(type="IterBasedRunner", max_iters=10000) + +workflow = [("train", 1)] diff --git a/src/otx/recipes/stages/_base_/runners/runner.py b/src/otx/recipes/stages/_base_/runners/runner.py new file mode 100644 index 00000000000..65e5fa4f793 --- /dev/null +++ b/src/otx/recipes/stages/_base_/runners/runner.py @@ -0,0 +1,4 @@ +# base runner setting +runner = dict() + +workflow = [] diff --git a/src/otx/recipes/stages/_base_/schedules/1cycle.py b/src/otx/recipes/stages/_base_/schedules/1cycle.py new file mode 100644 index 00000000000..63e19f34dc7 --- /dev/null +++ b/src/otx/recipes/stages/_base_/schedules/1cycle.py @@ -0,0 +1,3 @@ +_base_ = "./schedule.py" + +lr_config = dict(policy="OneCycle", pct_start=0.200001, div_factor=100, final_div_factor=1000) diff --git a/src/otx/recipes/stages/_base_/schedules/__init__.py b/src/otx/recipes/stages/_base_/schedules/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/_base_/schedules/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/_base_/schedules/cos_anneal.py b/src/otx/recipes/stages/_base_/schedules/cos_anneal.py new file mode 100644 index 00000000000..9d45904ac86 --- /dev/null +++ b/src/otx/recipes/stages/_base_/schedules/cos_anneal.py @@ -0,0 +1,3 @@ +_base_ = "./schedule.py" + +lr_config = dict(policy="CosineAnnealing", warmup=None, warmup_iters=0, min_lr_ratio=0.00001) diff --git a/src/otx/recipes/stages/_base_/schedules/plateau.py b/src/otx/recipes/stages/_base_/schedules/plateau.py new file mode 100644 index 00000000000..fdc46ba9951 --- /dev/null +++ b/src/otx/recipes/stages/_base_/schedules/plateau.py @@ -0,0 +1,13 @@ +_base_ = "./schedule.py" + +lr_config = dict( + policy="ReduceLROnPlateau", + metric="bbox_mAP", + patience=30, + iteration_patience=0, + interval=1, + min_lr=1e-06, + warmup="linear", + warmup_iters=200, + warmup_ratio=0.3333333333333333, +) diff --git a/src/otx/recipes/stages/_base_/schedules/schedule.py b/src/otx/recipes/stages/_base_/schedules/schedule.py new file mode 100644 index 00000000000..46bbc94ddce --- /dev/null +++ b/src/otx/recipes/stages/_base_/schedules/schedule.py @@ -0,0 +1,2 @@ +# base schedule setting +lr_config = dict(policy="fixed") diff --git a/src/otx/recipes/stages/classification/__init__.py b/src/otx/recipes/stages/classification/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/classification/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/classification/finetune.yaml b/src/otx/recipes/stages/classification/finetune.yaml new file mode 100644 index 00000000000..26a59adb079 --- /dev/null +++ b/src/otx/recipes/stages/classification/finetune.yaml @@ -0,0 +1,25 @@ +_base_: ["./train.yaml", "../_base_/models/classifiers/classifier.py"] + +model: + type: SAMImageClassifier + head: + type: "LinearClsHead" + in_channels: -1 + num_classes: -1 + loss: + type: "CrossEntropyLoss" + loss_weight: 1.0 + topk: !!python/tuple [1, 5] + +optimizer: + lr: 0.03 + momentum: 0.9 + weight_decay: 0.0005 + +optimizer_config: + type: SAMOptimizerHook + +custom_hooks: + - type: NoBiasDecayHook +deterministic: True +seed: 1234 diff --git a/src/otx/recipes/stages/classification/incremental.yaml b/src/otx/recipes/stages/classification/incremental.yaml new file mode 100644 index 00000000000..b40a5fdfd5b --- /dev/null +++ b/src/otx/recipes/stages/classification/incremental.yaml @@ -0,0 +1,52 @@ +_base_: + [ + "./train.yaml", + "../_base_/models/classifiers/classifier.py", + ] + +runner: + max_epochs: 20 + +optimizer: + type: SGD + lr: 0.01 + momentum: 0.9 + weight_decay: 0.0001 + +evaluation: + metric: ["accuracy", "class_accuracy"] + +lr_config: + policy: ReduceLROnPlateau + min_lr: 0.000001 + interval: 1 + metric: accuracy + factor: 0.5 + patience: 1 + iteration_patience: 0 + warmup: linear + warmup_iters: 1 + warmup_ratio: 0.333 + +task_adapt: + type: "default_task_adapt" + op: "REPLACE" + +custom_hooks: [ + { + type: LazyEarlyStoppingHook, + interval: 1, + metric: accuracy, + patience: 3, + iteration_patience: 0, + start: 3, + min_delta_ratio: 0.01, + priority: 75, + } +] + +ignore: True +adaptive_validation_interval: + max_interval: 5 + enable_adaptive_interval_hook: True + enable_eval_before_run: True diff --git a/src/otx/recipes/stages/classification/multilabel/__init__.py b/src/otx/recipes/stages/classification/multilabel/__init__.py new file mode 100644 index 00000000000..9007dfdd7e4 --- /dev/null +++ b/src/otx/recipes/stages/classification/multilabel/__init__.py @@ -0,0 +1,5 @@ +"""Recipes for multi-label classification.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/classification/multilabel/incremental.yaml b/src/otx/recipes/stages/classification/multilabel/incremental.yaml new file mode 100644 index 00000000000..03a87dc5591 --- /dev/null +++ b/src/otx/recipes/stages/classification/multilabel/incremental.yaml @@ -0,0 +1,59 @@ +_base_: + [ + "./train.yaml", + "../../_base_/models/classifiers/classifier.py", + ] + +runner: + max_epochs: 50 + +optimizer_config: + type: SAMOptimizerHook + +optimizer: + type: SGD + lr: 0.007 + momentum: 0.9 + weight_decay: 0.0005 + +evaluation: + metric: ["accuracy", "class_accuracy"] + +lr_config: + _delete_: True + policy: ReduceLROnPlateau + min_lr: 0.000001 + interval: 1 + metric: accuracy + factor: 0.5 + patience: 1 + iteration_patience: 0 + warmup: linear + warmup_iters: 1 + warmup_ratio: 0.333 + +task_adapt: + type: "default_task_adapt" + op: "REPLACE" + +ignore: True + +custom_hooks: [ + { + type: ModelEmaV2Hook + }, + { + type: LazyEarlyStoppingHook, + interval: 1, + metric: accuracy, + patience: 3, + iteration_patience: 0, + start: 3, + min_delta_ratio: 0.01, + priority: 75, + }, + { + type: AdaptiveRepeatDataHook, + priority: ABOVE_NORMAL + } +] diff --git a/src/otx/recipes/stages/classification/multilabel/semisl.yaml b/src/otx/recipes/stages/classification/multilabel/semisl.yaml new file mode 100644 index 00000000000..e32c0bc67af --- /dev/null +++ b/src/otx/recipes/stages/classification/multilabel/semisl.yaml @@ -0,0 +1,18 @@ +_base_: ["./train.yaml", "../../_base_/models/cls_semisl.py"] + +optimizer: + lr: 0.001 + momentum: 0.9 + weight_decay: 0.0005 + +optimizer_config: + type: SAMOptimizerHook + +custom_hooks: + - type: NoBiasDecayHook + - type: ModelEmaV2Hook + priority: ABOVE_NORMAL + +task_adapt: + type: "default_task_adapt" + op: "REPLACE" diff --git a/src/otx/recipes/stages/classification/multilabel/train.yaml b/src/otx/recipes/stages/classification/multilabel/train.yaml new file mode 100644 index 00000000000..4666a957e36 --- /dev/null +++ b/src/otx/recipes/stages/classification/multilabel/train.yaml @@ -0,0 +1,18 @@ +_base_: + [ + "../../_base_/default.py", + "../../_base_/logs/tensorboard_logger.py", + "../../_base_/optimizers/sgd.py", + "../../_base_/runners/epoch_runner_cancel.py", + "../../_base_/schedules/plateau.py", + ] + +optimizer: + lr: 0.007 + momentum: 0.9 + +runner: + max_epochs: 60 + +evaluation: + metric: ["accuracy"] diff --git a/src/otx/recipes/stages/classification/selfsl.yaml b/src/otx/recipes/stages/classification/selfsl.yaml new file mode 100644 index 00000000000..daad801976d --- /dev/null +++ b/src/otx/recipes/stages/classification/selfsl.yaml @@ -0,0 +1,45 @@ +_base_: [ + # remove default.py to disenable task_adapt + "../_base_/dist/dist.py", + "../_base_/logs/tensorboard_logger.py", + "../_base_/optimizers/lars.py", + "../_base_/runners/iter_runner.py", + "../_base_/schedules/cos_anneal.py", + "../_base_/data/selfsl_cls_data.py", + ] + +# in default.py settings +cudnn_benchmark: true +hparams: + dummy: 0 + +model: + base_momentum: 0.97 + +optimizer: + lr: 0.45 + weight_decay: 1.e-6 + mode: selfsl + exclude_bn_from_weight_decay: true + +lr_config: + warmup: linear + warmup_iters: 50 + warmup_ratio: 0.0001 + warmup_by_epoch: false + +runner: + max_iters: 5000 + +checkpoint_config: + by_epoch: false + interval: 100 + max_keep_ckpts: 1 + +log_config: + interval: 10 + ignore_last: false + +custom_hooks: + - type: EMAMomentumUpdateHook + end_momentum: 1.0 diff --git a/src/otx/recipes/stages/classification/semisl.yaml b/src/otx/recipes/stages/classification/semisl.yaml new file mode 100644 index 00000000000..0a685cef6e8 --- /dev/null +++ b/src/otx/recipes/stages/classification/semisl.yaml @@ -0,0 +1,22 @@ +_base_: ["./train.yaml", "../_base_/models/cls_semisl.py"] + +runner: + max_epochs: 50 + +optimizer: + lr: 0.001 + momentum: 0.9 + weight_decay: 0.0005 + +optimizer_config: + type: SAMOptimizerHook + +custom_hooks: + - type: NoBiasDecayHook + - type: ModelEmaV2Hook + priority: ABOVE_NORMAL + - type: SemiSLClsHook + +task_adapt: + type: "default_task_adapt" + op: "REPLACE" diff --git a/src/otx/recipes/stages/classification/supcon.yaml b/src/otx/recipes/stages/classification/supcon.yaml new file mode 100644 index 00000000000..01981fbd2b6 --- /dev/null +++ b/src/otx/recipes/stages/classification/supcon.yaml @@ -0,0 +1,25 @@ +_base_: + [ + "./train.yaml", + "../_base_/data/twocrop_data.py", + "../_base_/models/cls_supcon.py", + ] + +runner: + max_epochs: 20 + +optimizer_config: + type: SAMOptimizerHook + +optimizer: + type: SGD + lr: 0.005 + momentum: 0.9 + weight_decay: 0.0005 + +evaluation: + metric: ["accuracy", "class_accuracy"] + +task_adapt: + type: "default_task_adapt" + op: "REPLACE" diff --git a/src/otx/recipes/stages/classification/train.yaml b/src/otx/recipes/stages/classification/train.yaml new file mode 100644 index 00000000000..14193d79ca2 --- /dev/null +++ b/src/otx/recipes/stages/classification/train.yaml @@ -0,0 +1,31 @@ +_base_: + [ + "../_base_/default.py", + "../_base_/logs/tensorboard_logger.py", + "../_base_/optimizers/sgd.py", + "../_base_/runners/epoch_runner_cancel.py", + "../_base_/schedules/plateau.py", + ] + +optimizer: + lr: 0.03 + momentum: 0.9 + +runner: + max_epochs: 10 + +evaluation: + metric: ["accuracy"] + +channel_last: True + +custom_hooks: [ + { + type: AdaptiveTrainSchedulingHook, + enable_adaptive_interval_hook: False, + enable_eval_before_run: True, + }, + { + type: LoggerReplaceHook + }, +] diff --git a/src/otx/recipes/stages/detection/__init__.py b/src/otx/recipes/stages/detection/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/detection/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/detection/finetune.py b/src/otx/recipes/stages/detection/finetune.py new file mode 100644 index 00000000000..9b2cf178841 --- /dev/null +++ b/src/otx/recipes/stages/detection/finetune.py @@ -0,0 +1,20 @@ +_base_ = ["./train.py", "../_base_/data/coco_ubt.py", "../_base_/models/detectors/detector.py"] + +model = dict(super_type="UnbiasedTeacher") # Used as general framework + +custom_hooks = [ + dict( + type="DualModelEMAHook", + epoch_momentum=0.4, + start_epoch=2, + ), + dict( + type="LazyEarlyStoppingHook", + start=3, + patience=10, + iteration_patience=0, + metric="bbox_mAP", + interval=1, + priority=75, + ), +] diff --git a/src/otx/recipes/stages/detection/incremental.py b/src/otx/recipes/stages/detection/incremental.py new file mode 100644 index 00000000000..9ddd2e28e55 --- /dev/null +++ b/src/otx/recipes/stages/detection/incremental.py @@ -0,0 +1,48 @@ +_base_ = ["./train.py", "../_base_/models/detectors/detector.py"] + +task_adapt = dict( + type="default_task_adapt", + op="REPLACE", + efficient_mode=False, + use_adaptive_anchor=True, +) + +runner = dict(max_epochs=30) + +evaluation = dict(interval=1, metric="mAP", save_best="mAP") + +custom_hooks = [ + dict( + type="LazyEarlyStoppingHook", + start=3, + patience=10, + iteration_patience=0, + metric="mAP", + interval=1, + priority=75, + ), + dict( + type="EMAHook", + priority="ABOVE_NORMAL", + momentum=0.1, + ), +] + +lr_config = dict( + policy="ReduceLROnPlateau", + metric="mAP", + patience=5, + iteration_patience=0, + interval=1, + min_lr=1e-06, + warmup="linear", + warmup_iters=200, + warmup_ratio=0.3333333333333333, +) + +ignore = True +adaptive_validation_interval = dict( + max_interval=5, + enable_adaptive_interval_hook=True, + enable_eval_before_run=True, +) diff --git a/src/otx/recipes/stages/detection/semisl.py b/src/otx/recipes/stages/detection/semisl.py new file mode 100644 index 00000000000..52b727084a5 --- /dev/null +++ b/src/otx/recipes/stages/detection/semisl.py @@ -0,0 +1,35 @@ +_base_ = ["./train.py", "../_base_/data/coco_ubt.py", "../_base_/models/detectors/detector.py"] + +task_adapt = dict( + type="default_task_adapt", + op="REPLACE", + efficient_mode=False, + use_adaptive_anchor=True, +) + +custom_hooks = [ + dict(type="MeanTeacherHook", epoch_momentum=0.1, start_epoch=2), + dict( + type="LazyEarlyStoppingHook", + start=3, + patience=5, + iteration_patience=1000, + metric="bbox_mAP", + interval=1, + priority=75, + ), +] + +lr_config = dict( + policy="ReduceLROnPlateau", + metric="mAP", + patience=5, + iteration_patience=0, + interval=1, + min_lr=1e-06, + warmup="linear", + warmup_iters=200, + warmup_ratio=0.3333333333333333, +) + +find_unused_parameters = True diff --git a/src/otx/recipes/stages/detection/train.py b/src/otx/recipes/stages/detection/train.py new file mode 100644 index 00000000000..ef6973e51ef --- /dev/null +++ b/src/otx/recipes/stages/detection/train.py @@ -0,0 +1,43 @@ +_base_ = [ + "../_base_/default.py", + "../_base_/logs/tensorboard_logger.py", + "../_base_/optimizers/sgd.py", + "../_base_/runners/epoch_runner_cancel.py", + "../_base_/schedules/plateau.py", +] + +optimizer = dict( + lr=0.001, + momentum=0.9, + weight_decay=0.0001, +) + +optimizer_config = dict( + _delete_=True, + type="OptimizerHook", + # type="SAMOptimizerHook", + grad_clip=dict(max_norm=35, norm_type=2), +) + +lr_config = dict(min_lr=1e-06) + +evaluation = dict(interval=1, metric="mAP", save_best="mAP") +early_stop_metric = "mAP" + +custom_hooks = [ + dict( + type="LazyEarlyStoppingHook", + start=3, + patience=10, + iteration_patience=0, + metric="bbox_mAP", + interval=1, + priority=75, + ), + dict( + type="AdaptiveTrainSchedulingHook", + enable_adaptive_interval_hook=False, + enable_eval_before_run=True, + ), + dict(type="LoggerReplaceHook"), +] diff --git a/src/otx/recipes/stages/instance_segmentation/__init__.py b/src/otx/recipes/stages/instance_segmentation/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/instance_segmentation/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/instance_segmentation/incremental.py b/src/otx/recipes/stages/instance_segmentation/incremental.py new file mode 100644 index 00000000000..93cda5428e7 --- /dev/null +++ b/src/otx/recipes/stages/instance_segmentation/incremental.py @@ -0,0 +1,24 @@ +_base_ = ["./train.py", "../_base_/models/detectors/detector.py"] + +task = "instance-segmentation" + +evaluation = dict( + interval=1, metric="mAP", save_best="mAP", iou_thr=[0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95] +) + +task_adapt = dict( + type="default_task_adapt", + op="REPLACE", + efficient_mode=False, +) + +runner = dict(max_epochs=300) + +optimizer_config = dict(_delete_=True, grad_clip=dict(max_norm=35, norm_type=2)) + +ignore = True +adaptive_validation_interval = dict( + max_interval=5, + enable_adaptive_interval_hook=True, + enable_eval_before_run=True, +) diff --git a/src/otx/recipes/stages/instance_segmentation/semisl.py b/src/otx/recipes/stages/instance_segmentation/semisl.py new file mode 100644 index 00000000000..c7d83f073b2 --- /dev/null +++ b/src/otx/recipes/stages/instance_segmentation/semisl.py @@ -0,0 +1,26 @@ +_base_ = ["./train.py", "../_base_/models/detectors/detector.py"] + +task = "instance-segmentation" + +task_adapt = dict( + type="default_task_adapt", + op="REPLACE", + efficient_mode=False, +) + +runner = dict(max_epochs=300) + +optimizer_config = dict(_delete_=True, grad_clip=None) + +ignore = True +find_unused_parameters = True + +adaptive_validation_interval = dict( + max_interval=5, + enable_adaptive_interval_hook=True, + enable_eval_before_run=True, +) + +custom_hooks = [ + dict(type="MeanTeacherHook", epoch_momentum=0.1, start_epoch=5), +] diff --git a/src/otx/recipes/stages/instance_segmentation/train.py b/src/otx/recipes/stages/instance_segmentation/train.py new file mode 100644 index 00000000000..0ac963fa94d --- /dev/null +++ b/src/otx/recipes/stages/instance_segmentation/train.py @@ -0,0 +1,51 @@ +_base_ = [ + "../_base_/default.py", + "../_base_/logs/tensorboard_logger.py", + "../_base_/optimizers/sgd.py", + "../_base_/runners/epoch_runner_cancel.py", + "../_base_/schedules/plateau.py", +] + +optimizer = dict( + lr=0.001, + momentum=0.9, + weight_decay=0.0001, +) + +lr_config = dict( + policy="ReduceLROnPlateau", + metric="mAP", + patience=5, + iteration_patience=0, + interval=1, + min_lr=0.000001, + warmup="linear", + warmup_iters=200, + warmup_ratio=1.0 / 3, +) + +evaluation = dict(interval=1, metric="mAP", save_best="mAP") +early_stop_metric = "mAP" + +custom_hooks = [ + dict( + type="LazyEarlyStoppingHook", + start=3, + patience=10, + iteration_patience=0, + metric="mAP", + interval=1, + priority=75, + ), + dict( + type="AdaptiveTrainSchedulingHook", + enable_adaptive_interval_hook=False, + enable_eval_before_run=True, + ), + dict(type="LoggerReplaceHook"), + dict( + type="CustomModelEMAHook", + priority="ABOVE_NORMAL", + epoch_momentum=0.4, + ), +] diff --git a/src/otx/recipes/stages/segmentation/__init__.py b/src/otx/recipes/stages/segmentation/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/src/otx/recipes/stages/segmentation/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/src/otx/recipes/stages/segmentation/finetune.py b/src/otx/recipes/stages/segmentation/finetune.py new file mode 100644 index 00000000000..26359fb69b5 --- /dev/null +++ b/src/otx/recipes/stages/segmentation/finetune.py @@ -0,0 +1 @@ +_base_ = ["./train.py", "../_base_/models/segmentors/segmentor.py"] diff --git a/src/otx/recipes/stages/segmentation/incremental.py b/src/otx/recipes/stages/segmentation/incremental.py new file mode 100644 index 00000000000..c1fcba0f4b2 --- /dev/null +++ b/src/otx/recipes/stages/segmentation/incremental.py @@ -0,0 +1,36 @@ +_base_ = ["./train.py", "../_base_/models/segmentors/segmentor.py"] + +optimizer = dict(_delete_=True, type="Adam", lr=1e-3, eps=1e-08, weight_decay=0.0) + +optimizer_config = dict( + _delete_=True, + grad_clip=dict( + # method='adaptive', + # clip=0.2, + # method='default', + max_norm=40, + norm_type=2, + ), +) + +log_config = dict( + interval=10, + hooks=[ + dict(type="TextLoggerHook", by_epoch=True, ignore_last=False), + # dict(type='TensorboardLoggerHook') + ], +) + +runner = dict(type="EpochRunnerWithCancel", max_epochs=300) + +checkpoint_config = dict(by_epoch=True, interval=1) + +seed = 42 +find_unused_parameters = False + +task_adapt = dict( + type="default_task_adapt", + op="REPLACE", +) + +ignore = True diff --git a/src/otx/recipes/stages/segmentation/incremental_poly.py b/src/otx/recipes/stages/segmentation/incremental_poly.py new file mode 100644 index 00000000000..009a552da45 --- /dev/null +++ b/src/otx/recipes/stages/segmentation/incremental_poly.py @@ -0,0 +1,22 @@ +_base_ = ["./incremental.py"] + +optimizer = dict( + _delete_=True, + type="AdamW", + lr=0.0005, + betas=(0.9, 0.999), + weight_decay=0.0001, + paramwise_cfg={"bias_decay_mult ": 0.0, "norm_decay_mult ": 0.0}, +) +optimizer_config = dict(_delete_=True) +# learning policy +lr_config = dict( + _delete_=True, + policy="poly", + warmup="linear", + warmup_iters=300, + warmup_ratio=1e-6, + power=0.9, + min_lr=1e-6, + by_epoch=False, +) diff --git a/src/otx/recipes/stages/segmentation/selfsl.py b/src/otx/recipes/stages/segmentation/selfsl.py new file mode 100644 index 00000000000..89b678b3e4d --- /dev/null +++ b/src/otx/recipes/stages/segmentation/selfsl.py @@ -0,0 +1,33 @@ +_base_ = [ + "../_base_/logs/tensorboard_logger.py", + "../_base_/optimizers/sgd.py", + "../_base_/runners/epoch_runner.py", +] + + +# doesn't inherit default.py to disenable task_adapt +cudnn_benchmark = True + +seed = 5 +deterministic = False + +hparams = dict(dummy=0) + +optimizer = dict(type="SGD", lr=0.001, momentum=0.9, weight_decay=10e-4) + +lr_config = dict(policy="step", by_epoch=True, gamma=1, step=10) + +optimizer_config = dict(_delete_=True, grad_clip=dict(max_norm=40, norm_type=2)) + +log_config = dict( + interval=1, + hooks=[ + dict(type="TextLoggerHook", by_epoch=True, ignore_last=False), + ], +) + +runner = dict(type="EpochBasedRunner", max_epochs=10) + +checkpoint_config = dict(by_epoch=True, interval=1) + +find_unused_parameters = False diff --git a/src/otx/recipes/stages/segmentation/semisl.py b/src/otx/recipes/stages/segmentation/semisl.py new file mode 100644 index 00000000000..0f6a95ffa17 --- /dev/null +++ b/src/otx/recipes/stages/segmentation/semisl.py @@ -0,0 +1,44 @@ +_base_ = [ + "../_base_/models/segmentors/segmentor.py", + "./train.py", +] + +optimizer = dict(_delete_=True, type="Adam", lr=1e-3, eps=1e-08, weight_decay=0.0) + +optimizer_config = dict(_delete_=True, grad_clip=dict(max_norm=40, norm_type=2)) + +custom_hooks = [ + dict( + type="DualModelEMAHook", + momentum=0.99, + start_epoch=1, + src_model_name="model_s", + dst_model_name="model_t", + ), +] + +log_config = dict( + interval=10, + hooks=[ + dict(type="TextLoggerHook", by_epoch=True), + ], +) + +runner = dict(type="EpochRunnerWithCancel", max_epochs=300) + +checkpoint_config = dict( + by_epoch=True, + interval=1, +) + +evaluation = dict(interval=1, metric="mDice", save_best="mDice", rule="greater", show_log=True) +early_stop_metric = "mDice" + +task_adapt = dict( + op="REPLACE", +) +ignore = True + + +find_unused_parameters = True +seed = 42 diff --git a/src/otx/recipes/stages/segmentation/semisl_poly.py b/src/otx/recipes/stages/segmentation/semisl_poly.py new file mode 100644 index 00000000000..ead4ca64488 --- /dev/null +++ b/src/otx/recipes/stages/segmentation/semisl_poly.py @@ -0,0 +1,22 @@ +_base_ = ["./semisl.py"] + +optimizer = dict( + _delete_=True, + type="AdamW", + lr=0.0005, + betas=(0.9, 0.999), + weight_decay=0.0001, + paramwise_cfg={"bias_decay_mult ": 0.0, "norm_decay_mult ": 0.0}, +) +optimizer_config = dict(_delete_=True) +# learning policy +lr_config = dict( + _delete_=True, + policy="poly", + warmup="linear", + warmup_iters=300, + warmup_ratio=1e-6, + power=0.9, + min_lr=1e-6, + by_epoch=False, +) diff --git a/src/otx/recipes/stages/segmentation/supcon.py b/src/otx/recipes/stages/segmentation/supcon.py new file mode 100644 index 00000000000..d2686f4db4e --- /dev/null +++ b/src/otx/recipes/stages/segmentation/supcon.py @@ -0,0 +1,17 @@ +_base_ = ["./incremental.py"] + +custom_hooks = [ + dict( + type="LazyEarlyStoppingHook", + patience=8, + iteration_patience=0, + metric="mDice", + interval=1, + priority=75, + start=1, + ), + dict(type="TwoCropTransformHook", interval=5), +] + +# added to support multi-gpu training +find_unused_parameters = True diff --git a/src/otx/recipes/stages/segmentation/train.py b/src/otx/recipes/stages/segmentation/train.py new file mode 100644 index 00000000000..b535a189a3a --- /dev/null +++ b/src/otx/recipes/stages/segmentation/train.py @@ -0,0 +1,46 @@ +_base_ = [ + "../_base_/default.py", + "../_base_/logs/tensorboard_logger.py", + "../_base_/optimizers/sgd.py", + "../_base_/runners/epoch_runner_cancel.py", + "../_base_/schedules/plateau.py", +] + +optimizer = dict( + lr=0.001, + momentum=0.9, + weight_decay=0.0001, +) + +lr_config = dict( + policy="ReduceLROnPlateau", + metric="mDice", + patience=5, + iteration_patience=0, + interval=1, + min_lr=0.000001, + warmup="linear", + warmup_iters=80, + warmup_ratio=1.0 / 3, +) + +evaluation = dict(interval=1, metric="mDice", save_best="mDice", rule="greater", show_log=True) +early_stop_metric = "mDice" + +custom_hooks = [ + dict( + type="LazyEarlyStoppingHook", + patience=8, + iteration_patience=0, + metric="mDice", + interval=1, + priority=75, + start=1, + ), + dict( + type="AdaptiveTrainSchedulingHook", + enable_adaptive_interval_hook=False, + enable_eval_before_run=True, + ), + dict(type="LoggerReplaceHook"), +] diff --git a/src/otx/tools/__init__.py b/src/otx/tools/__init__.py deleted file mode 100644 index 7c49380bea8..00000000000 --- a/src/otx/tools/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Runnable Python script collection to provide useful tools.""" diff --git a/src/otx/tools/translate_mmrecipe.py b/src/otx/tools/translate_mmrecipe.py deleted file mode 100644 index fb868103261..00000000000 --- a/src/otx/tools/translate_mmrecipe.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Python script to translate MMx recipe to OTX YAML recipe file.""" -from argparse import ArgumentParser -from pathlib import Path - -from mmengine.config import Config -from omegaconf import OmegaConf - -from otx.core.utils.config import mmconfig_dict_to_dict - -parser = ArgumentParser() -parser.add_argument("-n", "--recipe-name", type=str, required=True) -parser.add_argument("-o", "--output-dir", type=Path, required=True) -parser.add_argument("-i", "--input-path", type=Path, required=True) - -override = parser.add_argument_group("override") -override.add_argument("--base", type=str, default="detection") -override.add_argument("--data", type=str, default="mmdet") -override.add_argument("--model", type=str, default="mmdet") - - -if __name__ == "__main__": - args = parser.parse_args() - - config = Config.fromfile(args.input_path) - config = mmconfig_dict_to_dict(config) - - omega_conf = OmegaConf.create( - { - "defaults": [ - {"override /base": args.base}, - {"override /data": args.data}, - {"override /model": args.model}, - ], - "data": { - "subsets": { - "train": { - "batch_size": config["train_dataloader"]["batch_size"], - "transforms": config["train_dataloader"]["dataset"]["pipeline"], - }, - "val": { - "batch_size": config["val_dataloader"]["batch_size"], - "transforms": config["val_dataloader"]["dataset"]["pipeline"], - }, - }, - }, - "model": {"otx_model": {"config": config["model"]}}, - }, - ) - - print(omega_conf) - output_path: Path = args.output_dir / f"{args.recipe_name}.yaml" - with output_path.open("w") as fp: - fp.write("# @package _global_\n") - OmegaConf.save(omega_conf, fp) diff --git a/src/otx/utils/__init__.py b/src/otx/utils/__init__.py index 2d433f25bee..1a7b41db1f0 100644 --- a/src/otx/utils/__init__.py +++ b/src/otx/utils/__init__.py @@ -1,8 +1,15 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Utility files.""" - -from .signal import append_main_proc_signal_handler, append_signal_handler +"""Collection of tools to run common OTX algorithms.""" -__all__ = ["append_signal_handler", "append_main_proc_signal_handler"] +# Copyright (C) 2022 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/src/otx/utils/logger.py b/src/otx/utils/logger.py new file mode 100644 index 00000000000..4977943cbe0 --- /dev/null +++ b/src/otx/utils/logger.py @@ -0,0 +1,154 @@ +"""Module for defining custom logger.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +# ruff: noqa: PLW0603 + +import functools +import logging +import os +import sys +from typing import Callable + +import torch.distributed as dist + +# __all__ = ['config_logger', 'get_log_dir', 'get_logger'] +__all__ = ["config_logger", "get_log_dir"] + +_LOGGING_FORMAT = "%(asctime)s | %(levelname)s : %(message)s" +_LOG_DIR = None +_FILE_HANDLER = None +_CUSTOM_LOG_LEVEL = 31 + +LEVEL = logging.INFO + +logging.addLevelName(_CUSTOM_LOG_LEVEL, "LOG") + + +def _get_logger(): + logger = logging.getLogger("otx") + logger.propagate = False + + def logger_print(message, *args, **kws): + if logger.isEnabledFor(_CUSTOM_LOG_LEVEL): + logger.log(_CUSTOM_LOG_LEVEL, message, *args, **kws) + + logger.print = logger_print + + logger.setLevel(LEVEL) + console = logging.StreamHandler(sys.stdout) + console.setFormatter(logging.Formatter(_LOGGING_FORMAT)) + + logger.addHandler(console) + + return logger + + +_logger = _get_logger() + +# # to expose supported APIs +# _override_methods = ['setLevel', 'addHandler', 'addFilter', 'info', +# 'warning', 'error', 'critical', 'print'] +# for fn in _override_methods: +# locals()[fn] = getattr(_logger, fn) +# __all__.append(fn) + + +def config_logger(log_file, level="WARNING"): + """A function that configures the logging system. + + :param log_file: str, a string representing the path to the log file. + :param level: str, a string representing the log level. Default is "WARNING". + :return: None + """ + global _LOG_DIR, _FILE_HANDLER # pylint: disable=global-statement + if _FILE_HANDLER is not None: + _logger.removeHandler(_FILE_HANDLER) + del _FILE_HANDLER + + _LOG_DIR = os.path.dirname(log_file) + os.makedirs(_LOG_DIR, exist_ok=True) + file = logging.FileHandler(log_file, mode="w", encoding="utf-8") + file.setFormatter(logging.Formatter(_LOGGING_FORMAT)) + _FILE_HANDLER = file + _logger.addHandler(file) + _logger.setLevel(_get_log_level(level)) + + +def _get_log_level(level): + # sanity checks + if level is None: + return None + + # get level number + level_number = logging.getLevelName(level.upper()) + if level_number not in [0, 10, 20, 30, 40, 50, _CUSTOM_LOG_LEVEL]: + msg = f"Log level must be one of DEBUG/INFO/WARN/ERROR/CRITICAL/LOG, but {level} is given." + raise ValueError(msg) + + return level_number + + +def get_log_dir(): + """A function that retrieves the directory path of the log file. + + :return: str, a string representing the directory path of the log file. + """ + return _LOG_DIR + + +class _DummyLogger(logging.Logger): + def debug(self, message, *args, **kws): + pass + + def info(self, message, *args, **kws): + pass + + def warning(self, message, *args, **kws): + pass + + def critical(self, message, *args, **kws): + pass + + def error(self, message, *args, **kws): + pass + + +def local_master_only(func: Callable) -> Callable: + """A decorator that allows a function to be executed only by the local master process in distributed training setup. + + Args: + func: the function to be decorated. + + Returns: + A wrapped function that can only be executed by the local master process. + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements + local_rank = 0 + if dist.is_available() and dist.is_initialized(): + local_rank = int(os.environ["LOCAL_RANK"]) + if local_rank == 0: + return func(*args, **kwargs) + + return wrapper + + +# apply decorator @local_master_only to the lower severity logging functions +_logging_methods = ["print", "debug", "info", "warning"] +for fn in _logging_methods: + setattr(_logger, fn, local_master_only(getattr(_logger, fn))) + + +def get_logger(): + """Return logger.""" + # if dist.is_available() and dist.is_initialized(): + # rank = dist.get_rank() + # else: + # rank = 0 + # if rank == 0: + # return _logger + # return _DummyLogger('dummy') + return _logger diff --git a/src/otx/utils/signal.py b/src/otx/utils/signal.py deleted file mode 100644 index 1ca3311be89..00000000000 --- a/src/otx/utils/signal.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""Functions to append a signal handler.""" - -from __future__ import annotations - -import os -import signal -from dataclasses import dataclass -from typing import TYPE_CHECKING, Callable - -if TYPE_CHECKING: - from types import FrameType - - -@dataclass -class SigHandler: - """Signal handler dataclass having handler function and pid which registers the handler.""" - - handler: Callable - pid: int - - -_SIGNAL_HANDLERS: dict[int, list] = {} - - -def append_signal_handler(sig_num: int, sig_handler: Callable) -> None: - """Append the handler for a signal. The function appended at last is called first. - - Args: - sig_num (signal.Signals): Signal number to add a handler to. - sig_handler (Callable): Callable function to be executed when the signal is sent. - """ - _register_signal_handler(sig_num, sig_handler, -1) - - -def append_main_proc_signal_handler(sig_num: int, sig_handler: Callable) -> None: - """Append the handler for a signal triggered only by main process. The function appended at last is called first. - - It's almost same as append_signal_handler except that handler will be executed only by signal to - process which registers handler. - - Args: - sig_num (signal.Signals): Signal number to add a handler to. - sig_handler (Callable): Callable function to be executed when the signal is sent. - """ - _register_signal_handler(sig_num, sig_handler, os.getpid()) - - -def _register_signal_handler(sig_num: int, sig_handler: Callable, pid: int) -> None: - if sig_num not in _SIGNAL_HANDLERS: - old_sig_handler = signal.getsignal(sig_num) - _SIGNAL_HANDLERS[sig_num] = [old_sig_handler] - signal.signal(sig_num, _run_signal_handlers) - - _SIGNAL_HANDLERS[sig_num].insert(0, SigHandler(sig_handler, pid)) - - -def _run_signal_handlers(sig_num: int, frame: FrameType | None) -> None: - pid = os.getpid() - for handler in _SIGNAL_HANDLERS[sig_num]: - if handler == signal.SIG_DFL: - signal.signal(sig_num, signal.SIG_DFL) - signal.raise_signal(sig_num) - elif isinstance(handler, SigHandler): - if handler.pid < 0 or handler.pid == pid: - handler.handler(sig_num, frame) - else: - handler(sig_num, frame) diff --git a/src/otx/utils/utils.py b/src/otx/utils/utils.py index 89cf03a2c79..0c0cfe66e31 100644 --- a/src/otx/utils/utils.py +++ b/src/otx/utils/utils.py @@ -1,116 +1,22 @@ -# Copyright (C) 2024 Intel Corporation +"""Utility functions collection.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +# -"""OTX utility functions.""" +from pathlib import Path +from typing import Union -from __future__ import annotations -from decimal import Decimal -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from pathlib import Path - - -def get_using_dot_delimited_key(key: str, target: Any) -> Any: # noqa: ANN401 - """Get values of attribute in target object using dot delimited key. - - For example, if key is "a.b.c", then get a value of 'target.a.b.c'. - Target should be object having attributes, dictionary or list. - To get an element in a list, an integer that is the index of corresponding value can be set as a key. - - Args: - key (str): dot delimited key. - val (Any): value to set. - target (Any): target to set value to. - """ - splited_key = key.split(".") - for each_key in splited_key: - if isinstance(target, dict): - target = target[each_key] - elif isinstance(target, list): - if not each_key.isdigit(): - error_msg = f"Key should be integer but '{each_key}'." - raise ValueError(error_msg) - target = target[int(each_key)] - else: - target = getattr(target, each_key) - return target - - -def set_using_dot_delimited_key(key: str, val: Any, target: Any) -> None: # noqa: ANN401 - """Set values to attribute in target object using dot delimited key. - - For example, if key is "a.b.c", then value is set at 'target.a.b.c'. - Target should be object having attributes, dictionary or list. - To get an element in a list, an integer that is the index of corresponding value can be set as a key. - - Args: - key (str): dot delimited key. - val (Any): value to set. - target (Any): target to set value to. - """ - splited_key = key.split(".") - for each_key in splited_key[:-1]: - if isinstance(target, dict): - target = target[each_key] - elif isinstance(target, list): - if not each_key.isdigit(): - error_msg = f"Key should be integer but '{each_key}'." - raise ValueError(error_msg) - target = target[int(each_key)] - else: - target = getattr(target, each_key) - - if isinstance(target, dict): - target[splited_key[-1]] = val - elif isinstance(target, list): - if not splited_key[-1].isdigit(): - error_msg = f"Key should be integer but '{splited_key[-1]}'." - raise ValueError(error_msg) - target[int(splited_key[-1])] = val - else: - setattr(target, splited_key[-1], val) - - -def get_decimal_point(num: int | float) -> int: - """Find a decimal point from the given float. - - Args: - num (int | float): float to find a decimal point from. - - Returns: - int: decimal point. - """ - if isinstance((exponent := Decimal(str(num)).as_tuple().exponent), int): - return abs(exponent) - error_msg = f"Can't get an exponent from {num}." - raise ValueError(error_msg) - - -def find_file_recursively(directory: Path, file_name: str) -> Path | None: - """Find the file from the direcotry recursively. If multiple files have a same name, return one of them. +def add_suffix_to_filename(file_path: Union[str, Path], suffix: str) -> Path: + """Add suffix to file name. Args: - directory (Path): directory where to find. - file_name (str): file name to find. + file_path (Union[str, Path]): File path to add suffix to. + suffix (str): Suffix to add. Returns: - Path | None: Found file. If it's failed to find a file, return None. - """ - if found_file := list(directory.rglob(file_name)): - return found_file[0] - return None - - -def remove_matched_files(directory: Path, pattern: str, file_to_leave: Path | None = None) -> None: - """Remove all files matched to pattern except file_to_leave. - - Args: - directory (Path): direcetory to find files to remove. - pattern (str): pattern to match a file name. - file_not_to_remove (Path | None, optional): files to leave. Defaults to None. + Path: Suffix added path. """ - for weight in directory.rglob(pattern): - if weight != file_to_leave: - weight.unlink() + if isinstance(file_path, str): + file_path = Path(file_path) + return file_path.parent / f"{file_path.stem}{suffix}{file_path.suffix}" diff --git a/tests/__init__.py b/tests/__init__.py index 6a16273c024..c371fbe5a1f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,2 +1,18 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2022-2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import os + +os.environ["FEATURE_FLAGS_OTX_ACTION_TASKS"] = "1" +os.environ["FEATURE_FLAGS_OTX_VISUAL_PROMPTING_TASKS"] = "1" diff --git a/tests/assets/action_classification_dataset/test.csv b/tests/assets/action_classification_dataset/test.csv deleted file mode 100644 index 2ebf0b55a2f..00000000000 --- a/tests/assets/action_classification_dataset/test.csv +++ /dev/null @@ -1,3 +0,0 @@ -label,youtube_id,time_start,time_end,split,is_cc -0,0,0,9,test,0 -1,1,0,9,test,0 diff --git a/tests/assets/action_classification_dataset/test/0.mp4 b/tests/assets/action_classification_dataset/test/0.mp4 deleted file mode 100644 index a3e2be4e009754dcef81d1e8b8582db80d1cff6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10404 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 diff --git a/tests/assets/action_classification_dataset/test/1.mp4 b/tests/assets/action_classification_dataset/test/1.mp4 deleted file mode 100644 index 9faae1442d48ebc2af5abbfbc58c69897ab019fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10241 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 diff --git a/tests/assets/action_classification_dataset/train/1.mp4 b/tests/assets/action_classification_dataset/train/1.mp4 deleted file mode 100644 index 9faae1442d48ebc2af5abbfbc58c69897ab019fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10241 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 diff --git a/tests/assets/action_classification_dataset/val/1.mp4 b/tests/assets/action_classification_dataset/val/1.mp4 deleted file mode 100644 index 9faae1442d48ebc2af5abbfbc58c69897ab019fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10241 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 diff --git a/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0001.jpg b/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0001.jpg deleted file mode 100644 index 6c8e6fc1f93a4d833cea86b7a780cef527a479f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 918 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zcX-Me<`TXz2D*HXvkm;BZ( z<+iMO@i0UB+>K3JCZ6i3arnXgG5KL$Z|)<0*&o5bxjpt=d3f>dy8jH@Ys^>voHh06 z+P81a?x$b;+UxYv{MO6wlYU-u+MqvqIz%ss9Y=>ziJ`;Qr4b9QbeZkNN)@1lj*7Ha(7iQ~!3I$bW|5`mJ#( z8|uUVo&V1ew13-whRuJc?|rlXn*5sIzIEGAznedyB+#|qS^n{aKvxk4AZGl369C*3 BVm$x= diff --git a/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0002.jpg b/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0002.jpg deleted file mode 100644 index 940b331ba33f698c5a8d582f4ebf3221922e1398..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 943 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z;6ai3p3+W{$2fDe#8EW>B^V;>gWHR zUHzXy?q2!SC;xt*{;c%bDqrH?+5J*A%n#4HAKj<>A^n?w`NB$BKTNFf`_J&^ zKSTcht(SG}FPfY!`}8Z`?ANDT{CA=~pZ=VeGF#D8#ee<&2XpH`N7TQYU;plXyieVY z`cHZBg7xnL|1(^9eOUC*=Oc2ug>s>f*7C6(ez3{&scG>(sne+)Yuk>WHViSIWf|%d z@;FLzs*TZVzlc|-`PEwrx_a$1{ZU=BeubpV_&=(XJ|e^3 zCTCu_R%X7=xgA0MY^O~>TsM0BM$WZ;<@858v?b^6*)Q(hTJ$sR%HksiN3O4omikb! chb1eZPyWNbE{*jslK&KOX)J00Vf+6#0p-PF>Hq)$ diff --git a/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0003.jpg b/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0003.jpg deleted file mode 100644 index b4fdef8984e78d796bc3dfbe63436f372b46ae6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 923 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zA%ULyZB#*2b#>>nA8Ec@{z*4sTD;mV$>r1YP9#lo3$fe(o1y+>@<;u@lWo)gGX%|N3xC;u zZ2eE=`C|2NX8!p6pCK?eZ2!E9&qvoOKdiGo^kG|k(@E{rulEAaM@zOHzj93fE!V`> zyr?P0I<~B*W`7Gmyf6IY=e7Lb%wJ6ZZCfM$@%fAS^DEvS{p0_rF8b)r=#Rp$ewOn- zkI`&?c=A8PstBzabGF}3`0U4Z&e*cL@}N$+TA}|fr&sM)=PublZkx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zf%+QWXQf2*7SZ8tanmfEI6uY-60XYjuAZq1vdPiu1C zrT_hVrY?Q@)8qTn`^>-V|42T6%lKdC{eQ9dSpPFTo7aEi^FKfHhu8m}z5lD}$GrVT z+CQHkT`#tyzu`we>*OatbS4Y89eFY%=Tz~T4L5#Wy;^nS&uMNAr-+!-?7l%EA4=vV zz4Xc|T5;E1T;llO-oNwqZ@zx8e(Ch4KmL!*JMQ>Ds`H-vWS{!;7}3^4AHqrx%`6KI z);hf-xM}k(i5^#;?@c@U#cd8%>{C7-ygF>2)ikMytOyThPOc4WqxOHccNGc$V))OJ NRV2^>gysL=1OU{eM(_Xt diff --git a/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0005.jpg b/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0005.jpg deleted file mode 100644 index 480ca41e55bf237e9754df37bbe1f8055ba359ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1038 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z!MnOn9lQNjJm_FH`}|Uyz{=~! z&MeW}&*~Jew)iFfPvY~pp8pJs%KtM=FK?{>xqQ9x5AFX9s{Iem|4up2_@Cib@Y3)v zn(@sQdmr@+6tE>ve()(r&cAc%jD(F6vo4+a@Z;81WvkGu8ms)dXCy3^u-JXl&p7wh zl40aBv1LHa>pN!lZJN`k4lDlPP_D==YE^f9o-r=Yt3nPYkf+L z-|;_+x>_i>W=iIH{&-&B;7cLP^=tm+Y+WlK^x@s+)fM#-+kVTfUzkg-e-*syV*J*p zGiJ@Wvt9pKXh3gF96M*{N~tWbIa%R{FRwOb)%5e?3eM6BnldG4&TOF888Z2k9#&-c z{Mg-IU}`8@5Xmn5{HGq@I=zxtNvj?ft+_7Vc=*bZbG=$3=^b;W1g2(QTIe=sm8C_{ WaNv)TP1v|0Vz%8H=j` diff --git a/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0006.jpg b/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0006.jpg deleted file mode 100644 index 63e6c2a9f45fef29692ede2111f2163c1578b84b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1006 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z;~heErPVzY^9T`IGoj+2+IVzp?+09ZI!W|F-^Z z{hI#_Spmnwp8RV6mcH?ue8};Hot67`hu`|`pMCbU|Kv%Rp8sd?-=FrM;lk=e=a=#S zaf|=aS^1x#LsI^a^Zp<0^*8pv(#t-)GyU5BYxloC|Ly%d_;-D4#q8U^`M;(!ewANU z>wfchPWtv==eI2WTK`=9m>u6o`Gy+z6+gU>?kfN0-%&DuQ+-C}e}?Q0`+wWtexqAz z?f+%3(W_6#;?0hIx>bKC=H%0#^I~SJc`7^YJ^k(Qe}>7uKf3=j6!rgGuuo#q{=kQ8 zPVFl`JjZ@c(n7C!>ZT`+)|?jPu}PN_Sl@Rb;AmK@#Fdp{OETS-Mrsw#jhY+~7_`z- zUHy;Z<8s#2IH?~OAH8q46jL@h`eY8{qk8#*F#oQzs~)a?wqwrf){6CJCpP_YOOI!7 vIgqHGCc0q;m#N{_f*mq)Qp?mr)tx!pmppcpH@@g9`gZ;dR*^sl`TsWo0!WJE diff --git a/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0007.jpg b/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0007.jpg deleted file mode 100644 index 8a31f0bbeda483e26a91d525328240f0ad8692e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1035 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zh*$- zrR?*wZGwK98yvQY-sUS(q^g-H2xs}Z_%yLI_F2<<)7OyPEYx9 zW;XA%q^xHWcig156kA!Wy3nH;5SsK-N6(y9>enqpgT}8gyU(COG^^5C=+b@fc z%~LLT-YI!sCS``4z{8r=)~B;RxTQ{bx?|UmJ!QE%tWgy&pWiV(vL)-`V^_`KHn}#b zMJv~=I%oIrhv!G;hueGW#9o`-lmB?H`)}*&dzlkni(Q+4%gS&0v&`+{;oAc4{9Ww6 z|B7yK?X~%p;*(WQTm7`YrP}8)_tdZT-~I!GW76@5>wl|Nng3gAXS`_N?WWCp;srmf zbDx*I^5M&*&o(@UrY|2V21!(eOnEN0;?UNpugTo5k$zh}X1~^ft4A!a%_=^edc!ME ZOEbvV?4R_aF6~$RhnTuF7V!VS2>`Q1lNkU2 diff --git a/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0008.jpg b/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0008.jpg deleted file mode 100644 index 19ab461b7f819990730ea8207783d17c01d4e1f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1076 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zE>ahrJZXX1J*X2*%&lOx%KFSS6Zu<@lEQP8|mm2D!lB;V~zg| z<#p%d7yV~o_^Y!g<54|djoaBDhgDA+#q$`iGd=WZhql4VqBqx;Wlt}RXH5w)_|f(A zJ^x2Jp~{*MbKLt@hE28M)$!SAvSD(Yp2)(e-ue^v9sAk$Dc7j~JN}=cwejKoH~eq^ zGrZ-0Gyliz7g6hP{b$&;@9?+(3_qD(RZXAxq&_pU{`mRH&wk$D`PpV?&8B@%{xdXn z)t{C7&#*XO><+^I}@%<(Vr%b5@1t#y6>89eO;|HwXAbGXCUPx8TkhOhR&jb)Eo ztl2F+@A`j+2n+7k9m}P^z6lGSnliICK6OTV#V(8Z)lVzpd}M8Gt@hp8qm`ufFa4sc Q=(ppa83J8J82{e{04^4;U;qFB diff --git a/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0009.jpg b/tests/assets/action_detection_dataset/frames/train_video0/train_video0_0009.jpg deleted file mode 100644 index cc9e52b80076541ea1bb587597bc6a3e25efda3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1038 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zCH1|ZR@o;G`DI;I*+TTTkc`r&O@QkJ@wpHtIYd2?f5tQ3&AhG9<2Ag zFJJVZf$PV2<3~M<+c(zeAK7`=C!Hg?XbxMkOq_4~!f0+kiJs>w=Rauk2ZgGvJ``E9 z%1cwLG}C(f&!ZoiALt+LZ{20Oy~C#bq5m6QrO(0b?%Q_%yZhnZ@@HA$V%xWGxLrHn z{hBHFlKa`q?|DD>9(#(58LY&;3mq!Cyie6ml bi4N7eT9*BZuS;Y7MgGSV7IkSb|Gxkx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zjv*-Dj^TK7L=SM)IM)*vtF3?#W%<|4`}Qh0Fi0*C&4K-%#Nnd*!YDr7x#fTff+| z`n7fDe}=+h`I%3D7Uj1->HW_Ty8mIneY*WG_4=RX6|es@%&Pj+{KNL6_&-tmBl&!P z9R3~ucmLmkf7kyrbozg5{}z7hKf~4%U4PgA4E_HZHoS^+pZIs_xA{Ll%|837X8zru z^K$Bo>JJ~af7M_A%<(_N2c`Y>6Z^b-;rnd7^fkI~| diff --git a/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0001.jpg b/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0001.jpg deleted file mode 100644 index 22fc77d28c877ff149c3b7ef7a9017dc26d25ee9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 924 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zRi)zLa|Z?ftEHC6#M)ziRwv*lc6ICR^-3L)xrwrvtY7 zevhg#Jp1hC*HiE5p8w^J7qkx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z?%-?tA0K{uD}Vm; z{|sq=XaBQL{Iuu2_4j%4Q`PUx|L|`AuQm2R`}hAbUq7Kf?Y_j1;sf?lHIk3+i@mbH zX(xB7{-M&p3s>w;*4KZl-~Pit_Oh=1MU&Ifr(f3GetkN_zA#sP`m@SOIjt%a?L+rJ z-0+_v-Ts%x{GS;=UjJv9+4ZmTV|Is4=&2q3tnCl7B=5#7t5lQPrWBda8+a~5=UBv_ zQ=*G)LVZ@tYL$jb%@aJ$x7yW>Z?1pZ+;GkR3>)`vzJ5r5fxi3?{Ubk*Us`PT;V|PP zG3g_JUhABE@o|0c&FO7B*X%Is=PC%()?e9vVna>sp)DILtIvI$<17`qWz(;#+K&>u f5AMnJc%bae|0Ca3B>aovKTB4TKnD<(|9=wz-H>Hw diff --git a/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0003.jpg b/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0003.jpg deleted file mode 100644 index 6462147b2e144c8588772b7ae9a6e85999aaa38a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 928 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zBZ0rC{)^Ec{+kycWUWc>BIZw{;;p9Xb!t< zqN{ZNpHTnSSG$h>T{m;tt-p&8&QsdD{kQMeHyff3KELbFyTvYN^|SLP0n1*hxY}?0 z&+uVa9bf&$$RFwd8CsvW?*Ew`f8I{F@V#8YcEJam{)r#@U9)eSXWOx``QKc9YBx?b z{MpMj@!N&&z^yWNvcGj7pFR8S;=g16&c`o`zi55DpY_M=?my+{_iy^KTkoT@)x$lz z`TsKr3bP)v`D5Aq(2wiPhtmC<3q=#H{@k2bIQ#OiPcd69?bgjVlf3=&^`b8AUlRWr PWSY7(7BB%Z|Nol+E>mGe diff --git a/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0004.jpg b/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0004.jpg deleted file mode 100644 index 2811e448ac02e21e9be9eae02ab4aa922607382c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 878 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zf%+QWXQf2*7SZ8tanmfEI6uY-60XYjuAZq1vdPiu1C zrT_hVrY?Q@)8qTn`^>-V|42T6%lKdC{eQ9dSpPFTo7aEi^FKfHhu8m}z5lD}$GrVT z+CQHkT`#tyzu`we>*OatbS4Y89eFY%=Tz~T4L5#Wy;^nS&uMNAr-+!-?7l%EA4=vV zz4Xc|T5;E1T;llO-oNwqZ@zx8e(Ch4KmL!*JMQ>Ds`H-vWS{!;7}3^4AHqrx%`6KI z);hf-xM}k(i5^#;?@c@U#cd8%>{C7-ygF>2)ikMytOyThPOc4WqxOHccNGc$V))OJ NRV2^>gysL=1OU{eM(_Xt diff --git a/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0005.jpg b/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0005.jpg deleted file mode 100644 index 9f0102112f5d1ea9ffbec411a1b93a3be6ad03ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1042 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z0!KKEN~>f#%*SyfZ7-+VCb)#EqU%mV#O?I&ilTsN)^ zG@r_1bmn(*?~QW%4@>tM{%5$T_G9LMh8Fp*`k$e3Gk?p~pFH-*>OaHI-TxT^;)Soj zbFJfj)Zcw`Izu5FzuLhkKXeXFQ&&IytY~fL`nahc=eC&}9NOh&GP%1Yxv6K$ymi5o zs#NR0PJf~J+iHjY%i@RPfuAs?=!?S1Jhf99`9VEmr>PcANB{5tAn^fv+b*BBZ__mH6*j*kpKY6cYHH05wQ1saW%`!y{Lj#O`D6dT zuJcFE|IxPz{?BkaN@JalU*xpMSz0^J>}Ty>EumGovhz^D`XGa#DN{sVU7j;VWT~dc z@>EUFrDZq2N~%kLT+W-Al~7UK{bRAhfvZRS#lzVrrT^R(+PboPrC+b)qpZXa7P~~! hJLc-xas{u|u$r{osYhl`=0W!}FBWyZwTtkx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z0BqeNMcNOEilATK+r!b)+xP<=6JN z{xbwT`F7(>nedHj`8Q|s&GoPBUB30(#%JI5%sX3D?Ely9muTJD{|rCo$a5S2XAt_& z@W|*t!x8I$hd(l#K8Sx4|D(PBb^f*en=Q&)i~lpc^?$4X^?6%u`}Y3~k^5GDm0uC| ztv>MEe}ZIH?KgT`PVfKX>v!$bv3Rp%pPupGzwP<-=e(3^HIFC$MgJMv4*y+j|6)=7)5Q>XK+@@rOuC}-w z6cD^J$V)4Da{clT;l1}vDzc|08R_%Y#D27IF!G5@3^zNP@<;Ueo$y0D!_-aJw;f7& y{j8#AztY(a5?(i3bJUobTUYn4bc>iZBS%iC<&{-Lm0|mtFM+Pr>Cf2z-vj`Fs);-R diff --git a/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0007.jpg b/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0007.jpg deleted file mode 100644 index c42ab6a80c6d886c11c79cec70e9d2b1397a0f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1045 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zdw;|&kEPJt#_Jgad~amvBXeysg)AK9Ye zG-vvj(1NWg_jMlTKVR3SvHp7h=Oypzq-z|m)rtMM?)ve5``az2&&E6byZBpm+eY52 z$7ZX4n>V?B@txXb(%N_JUVe*uCG5#RwUY1VHMM#p(~9}8_W#KL&(Kr{aL1LxKipy k)}@NB9Wtx+4*Rau*cIXB>1wL3-}Yir*RSRe&Hrx#06R;d3;+NC diff --git a/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0008.jpg b/tests/assets/action_detection_dataset/frames/train_video1/train_video1_0008.jpg deleted file mode 100644 index 8f8b9164a9fb9fdb35f34d534fb60ef28c9cb5fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1079 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z z-7oQf=j}78)!D&r{!#kyJoOKURZkkl8Jw}u7HxeT^SbNPn`_Imr>E?(pLhL8;)|3R z(IXX}Pd^_jlw+H#wbk&-Mx!H_jvlJ;2x^tO`k#UK#iFjiF8>5=jURkJ68|IQKZD>N z>yOO8<$u@zX8s-jPu2dG{LT36e^)PTwH5zo|L^e1N;&s$({I&3K9z5N@>6~Jv&xNs zPyT1%7x~Yy%Jx6QO!of_2f8%YcRupx+hN|l@ycdBMWFN+!z~6=KA5bo&q&+h7o)3n z^`pSjHykx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z5u)zeVR29AC@=TI2NzG`BLirxA(W+l~k_H{i^YwVY7|-nryNE3~95z zoetRQ`#q}0@a(gjXLqqp4L&__QpjJ~{|s#mKP>+PFc9ExwvOi@V6aN*IU;uTJ-Od`#1ZGuU}|B zet)UF>9d{fhxPgoYvTD+WdeI2@}&B_?<}}!re%4KS?i?n92d=p*?}vyW|THQ;@K6j zZpmV;No#$zFO~mkUeu-i#rVgAum2eu_6wNgx9%6S(R{pLAUdmjmRsxJ-QRZS%1KA} zdilS#=Y1_@yZ`3Gy?ZaejZeL{kY{q`_6ctrQ&T>xgmnIAaIb&3{~urdg{>cr|1+@M z1N!~hxi0O`=X5_DZmqa(v~bq*DTP}ND{4e-_RT(=oZi0tYgR;$pBm>O!_4!y9<7cm z-`(ByTXX&mph@eO$M^4d)8jt#gZt2)!>u3Y*v|gwX5RgA%IAGjhtn-qrd#}3t#Wds p@ba#;QFB&4yvN@D>BXU@?w%~StuF6FZ-3(J(pZ1t{Nv{THvyK@pW^@k diff --git a/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0000.jpg b/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0000.jpg deleted file mode 100644 index 9f7fe9cbfc294584164af61c4fdd48cfdf50b714..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1074 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z!_H!d9yUK`G>zxLS<8D9Tv9@`qz$f8R}u1vAiK6F(?X6K@%ob~>Hd2K`= z#xHTd%U}P@XO){=o=T#>*8WcX&v0^Qjq87g zo$mh`g4go|#;LdL$({A_IbU*d&tv}S4>wxmyceE!NHSN)SBkGyhAq0Q=ebOnzGUFl zQp2esOI97@6I?dcR8`aa%YTMjqF>@4$S=z8l`(vHzTI-aM0%uJ>BBbfV>{M_OFfg+ zOP+Q4Y_Xj7kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zCH1|ZR@o;G`DI;I*+TTTkc`r&O@QkJ@wpHtIYd2?f5tQ3&AhG9<2Ag zFJJVZf$PV2<3~M<+c(zeAK7`=C!Hg?XbxMkOq_4~!f0+kiJs>w=Rauk2ZgGvJ``E9 z%1cwLG}C(f&!ZoiALt+LZ{20Oy~C#bq5m6QrO(0b?%Q_%yZhnZ@@HA$V%xWGxLrHn z{hBHFlKa`q?|DD>9(#(58LY&;3mq!Cyie6ml bi4N7eT9*BZuS;Y7MgGSV7IkSb|Gxkx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zE>ahrJZXX1J*X2*%&lOx%KFSS6Zu<@lEQP8|mm2D!lB;V~zg| z<#p%d7yV~o_^Y!g<54|djoaBDhgDA+#q$`iGd=WZhql4VqBqx;Wlt}RXH5w)_|f(A zJ^x2Jp~{*MbKLt@hE28M)$!SAvSD(Yp2)(e-ue^v9sAk$Dc7j~JN}=cwejKoH~eq^ zGrZ-0Gyliz7g6hP{b$&;@9?+(3_qD(RZXAxq&_pU{`mRH&wk$D`PpV?&8B@%{xdXn z)t{C7&#*XO><+^I}@%<(Vr%b5@1t#y6>89eO;|HwXAbGXCUPx8TkhOhR&jb)Eo ztl2F+@A`j+2n+7k9m}P^z6lGSnliICK6OTV#V(8Z)lVzpd}M8Gt@hp8qm`ufFa4sc Q=(ppa83J8J82{e{04^4;U;qFB diff --git a/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0003.jpg b/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0003.jpg deleted file mode 100644 index 8a31f0bbeda483e26a91d525328240f0ad8692e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1035 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zh*$- zrR?*wZGwK98yvQY-sUS(q^g-H2xs}Z_%yLI_F2<<)7OyPEYx9 zW;XA%q^xHWcig156kA!Wy3nH;5SsK-N6(y9>enqpgT}8gyU(COG^^5C=+b@fc z%~LLT-YI!sCS``4z{8r=)~B;RxTQ{bx?|UmJ!QE%tWgy&pWiV(vL)-`V^_`KHn}#b zMJv~=I%oIrhv!G;hueGW#9o`-lmB?H`)}*&dzlkni(Q+4%gS&0v&`+{;oAc4{9Ww6 z|B7yK?X~%p;*(WQTm7`YrP}8)_tdZT-~I!GW76@5>wl|Nng3gAXS`_N?WWCp;srmf zbDx*I^5M&*&o(@UrY|2V21!(eOnEN0;?UNpugTo5k$zh}X1~^ft4A!a%_=^edc!ME ZOEbvV?4R_aF6~$RhnTuF7V!VS2>`Q1lNkU2 diff --git a/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0004.jpg b/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0004.jpg deleted file mode 100644 index 63e6c2a9f45fef29692ede2111f2163c1578b84b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1006 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z;~heErPVzY^9T`IGoj+2+IVzp?+09ZI!W|F-^Z z{hI#_Spmnwp8RV6mcH?ue8};Hot67`hu`|`pMCbU|Kv%Rp8sd?-=FrM;lk=e=a=#S zaf|=aS^1x#LsI^a^Zp<0^*8pv(#t-)GyU5BYxloC|Ly%d_;-D4#q8U^`M;(!ewANU z>wfchPWtv==eI2WTK`=9m>u6o`Gy+z6+gU>?kfN0-%&DuQ+-C}e}?Q0`+wWtexqAz z?f+%3(W_6#;?0hIx>bKC=H%0#^I~SJc`7^YJ^k(Qe}>7uKf3=j6!rgGuuo#q{=kQ8 zPVFl`JjZ@c(n7C!>ZT`+)|?jPu}PN_Sl@Rb;AmK@#Fdp{OETS-Mrsw#jhY+~7_`z- zUHy;Z<8s#2IH?~OAH8q46jL@h`eY8{qk8#*F#oQzs~)a?wqwrf){6CJCpP_YOOI!7 vIgqHGCc0q;m#N{_f*mq)Qp?mr)tx!pmppcpH@@g9`gZ;dR*^sl`TsWo0!WJE diff --git a/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0005.jpg b/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0005.jpg deleted file mode 100644 index 480ca41e55bf237e9754df37bbe1f8055ba359ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1038 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z!MnOn9lQNjJm_FH`}|Uyz{=~! z&MeW}&*~Jew)iFfPvY~pp8pJs%KtM=FK?{>xqQ9x5AFX9s{Iem|4up2_@Cib@Y3)v zn(@sQdmr@+6tE>ve()(r&cAc%jD(F6vo4+a@Z;81WvkGu8ms)dXCy3^u-JXl&p7wh zl40aBv1LHa>pN!lZJN`k4lDlPP_D==YE^f9o-r=Yt3nPYkf+L z-|;_+x>_i>W=iIH{&-&B;7cLP^=tm+Y+WlK^x@s+)fM#-+kVTfUzkg-e-*syV*J*p zGiJ@Wvt9pKXh3gF96M*{N~tWbIa%R{FRwOb)%5e?3eM6BnldG4&TOF888Z2k9#&-c z{Mg-IU}`8@5Xmn5{HGq@I=zxtNvj?ft+_7Vc=*bZbG=$3=^b;W1g2(QTIe=sm8C_{ WaNv)TP1v|0Vz%8H=j` diff --git a/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0006.jpg b/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0006.jpg deleted file mode 100644 index 2811e448ac02e21e9be9eae02ab4aa922607382c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 878 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zf%+QWXQf2*7SZ8tanmfEI6uY-60XYjuAZq1vdPiu1C zrT_hVrY?Q@)8qTn`^>-V|42T6%lKdC{eQ9dSpPFTo7aEi^FKfHhu8m}z5lD}$GrVT z+CQHkT`#tyzu`we>*OatbS4Y89eFY%=Tz~T4L5#Wy;^nS&uMNAr-+!-?7l%EA4=vV zz4Xc|T5;E1T;llO-oNwqZ@zx8e(Ch4KmL!*JMQ>Ds`H-vWS{!;7}3^4AHqrx%`6KI z);hf-xM}k(i5^#;?@c@U#cd8%>{C7-ygF>2)ikMytOyThPOc4WqxOHccNGc$V))OJ NRV2^>gysL=1OU{eM(_Xt diff --git a/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0007.jpg b/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0007.jpg deleted file mode 100644 index b4fdef8984e78d796bc3dfbe63436f372b46ae6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 923 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zA%ULyZB#*2b#>>nA8Ec@{z*4sTD;mV$>r1YP9#lo3$fe(o1y+>@<;u@lWo)gGX%|N3xC;u zZ2eE=`C|2NX8!p6pCK?eZ2!E9&qvoOKdiGo^kG|k(@E{rulEAaM@zOHzj93fE!V`> zyr?P0I<~B*W`7Gmyf6IY=e7Lb%wJ6ZZCfM$@%fAS^DEvS{p0_rF8b)r=#Rp$ewOn- zkI`&?c=A8PstBzabGF}3`0U4Z&e*cL@}N$+TA}|fr&sM)=PublZkx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z;6ai3p3+W{$2fDe#8EW>B^V;>gWHR zUHzXy?q2!SC;xt*{;c%bDqrH?+5J*A%n#4HAKj<>A^n?w`NB$BKTNFf`_J&^ zKSTcht(SG}FPfY!`}8Z`?ANDT{CA=~pZ=VeGF#D8#ee<&2XpH`N7TQYU;plXyieVY z`cHZBg7xnL|1(^9eOUC*=Oc2ug>s>f*7C6(ez3{&scG>(sne+)Yuk>WHViSIWf|%d z@;FLzs*TZVzlc|-`PEwrx_a$1{ZU=BeubpV_&=(XJ|e^3 zCTCu_R%X7=xgA0MY^O~>TsM0BM$WZ;<@858v?b^6*)Q(hTJ$sR%HksiN3O4omikb! chb1eZPyWNbE{*jslK&KOX)J00Vf+6#0p-PF>Hq)$ diff --git a/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0009.jpg b/tests/assets/action_detection_dataset/frames/train_video2/train_video2_0009.jpg deleted file mode 100644 index 6c8e6fc1f93a4d833cea86b7a780cef527a479f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 918 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zcX-Me<`TXz2D*HXvkm;BZ( z<+iMO@i0UB+>K3JCZ6i3arnXgG5KL$Z|)<0*&o5bxjpt=d3f>dy8jH@Ys^>voHh06 z+P81a?x$b;+UxYv{MO6wlYU-u+MqvqIz%ss9Y=>ziJ`;Qr4b9QbeZkNN)@1lj*7Ha(7iQ~!3I$bW|5`mJ#( z8|uUVo&V1ew13-whRuJc?|rlXn*5sIzIEGAznedyB+#|qS^n{aKvxk4AZGl369C*3 BVm$x= diff --git a/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0000.jpg b/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0000.jpg deleted file mode 100644 index 9b113abfcb1648386f42934714f345b8110b8c93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1079 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z^X&>BIZ^|1)f}-(bJ}Kf~7l43BdE&i}jmoBx}&+`-rOKR*2S zR{s3w{~6N$&i-eg_-W63>+kd8r>ftX|KZ*KUu*1t_V52=zP|I3zhH%VOW~DG_Y}|B zZ~nOT!<3I#R{Up3f3{;^j&9V|4aVu{ zF_V7-^s}q*e+FH7+4|3huA)E7r7OG- dC@N>^&>A4R#TIB4T7R`~mV-BknU#pCt zzNq9mi#6LN1Fx1EP7PVI>KLEkvZ#-&-qxq V)XF5IPWqy&=&#N{O7;J50s!1Ft>^#% diff --git a/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0001.jpg b/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0001.jpg deleted file mode 100644 index 78228fbbad3c475efd80bfabf83232321d3da6ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1052 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z5u)zeVR29AC@=TI2NzG`BLirxA(W+l~k_H{i^YwVY7|-nryNE3~95z zoetRQ`#q}0@a(gjXLqqp4L&__QpjJ~{|s#mKP>+PFc9ExwvOi@V6aN*IU;uTJ-Od`#1ZGuU}|B zet)UF>9d{fhxPgoYvTD+WdeI2@}&B_?<}}!re%4KS?i?n92d=p*?}vyW|THQ;@K6j zZpmV;No#$zFO~mkUeu-i#rVgAum2eu_6wNgx9%6S(R{pLAUdmjmRsxJ-QRZS%1KA} zdilS#=Y1_@yZ`3Gy?ZaejZeL{kY{q`_6ctrQ&T>xgmnIAaIb&3{~urdg{>cr|1+@M z1N!~hxi0O`=X5_DZmqa(v~bq*DTP}ND{4e-_RT(=oZi0tYgR;$pBm>O!_4!y9<7cm z-`(ByTXX&mph@eO$M^4d)8jt#gZt2)!>u3Y*v|gwX5RgA%IAGjhtn-qrd#}3t#Wds p@ba#;QFB&4yvN@D>BXU@?w%~StuF6FZ-3(J(pZ1t{Nv{THvyK@pW^@k diff --git a/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0002.jpg b/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0002.jpg deleted file mode 100644 index 8f8b9164a9fb9fdb35f34d534fb60ef28c9cb5fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1079 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z z-7oQf=j}78)!D&r{!#kyJoOKURZkkl8Jw}u7HxeT^SbNPn`_Imr>E?(pLhL8;)|3R z(IXX}Pd^_jlw+H#wbk&-Mx!H_jvlJ;2x^tO`k#UK#iFjiF8>5=jURkJ68|IQKZD>N z>yOO8<$u@zX8s-jPu2dG{LT36e^)PTwH5zo|L^e1N;&s$({I&3K9z5N@>6~Jv&xNs zPyT1%7x~Yy%Jx6QO!of_2f8%YcRupx+hN|l@ycdBMWFN+!z~6=KA5bo&q&+h7o)3n z^`pSjHykx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zdw;|&kEPJt#_Jgad~amvBXeysg)AK9Ye zG-vvj(1NWg_jMlTKVR3SvHp7h=Oypzq-z|m)rtMM?)ve5``az2&&E6byZBpm+eY52 z$7ZX4n>V?B@txXb(%N_JUVe*uCG5#RwUY1VHMM#p(~9}8_W#KL&(Kr{aL1LxKipy k)}@NB9Wtx+4*Rau*cIXB>1wL3-}Yir*RSRe&Hrx#06R;d3;+NC diff --git a/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0004.jpg b/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0004.jpg deleted file mode 100644 index fce8092cd9aae2a187a4fef473260767adcdc57c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1010 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z0BqeNMcNOEilATK+r!b)+xP<=6JN z{xbwT`F7(>nedHj`8Q|s&GoPBUB30(#%JI5%sX3D?Ely9muTJD{|rCo$a5S2XAt_& z@W|*t!x8I$hd(l#K8Sx4|D(PBb^f*en=Q&)i~lpc^?$4X^?6%u`}Y3~k^5GDm0uC| ztv>MEe}ZIH?KgT`PVfKX>v!$bv3Rp%pPupGzwP<-=e(3^HIFC$MgJMv4*y+j|6)=7)5Q>XK+@@rOuC}-w z6cD^J$V)4Da{clT;l1}vDzc|08R_%Y#D27IF!G5@3^zNP@<;Ueo$y0D!_-aJw;f7& y{j8#AztY(a5?(i3bJUobTUYn4bc>iZBS%iC<&{-Lm0|mtFM+Pr>Cf2z-vj`Fs);-R diff --git a/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0005.jpg b/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0005.jpg deleted file mode 100644 index 9f0102112f5d1ea9ffbec411a1b93a3be6ad03ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1042 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z0!KKEN~>f#%*SyfZ7-+VCb)#EqU%mV#O?I&ilTsN)^ zG@r_1bmn(*?~QW%4@>tM{%5$T_G9LMh8Fp*`k$e3Gk?p~pFH-*>OaHI-TxT^;)Soj zbFJfj)Zcw`Izu5FzuLhkKXeXFQ&&IytY~fL`nahc=eC&}9NOh&GP%1Yxv6K$ymi5o zs#NR0PJf~J+iHjY%i@RPfuAs?=!?S1Jhf99`9VEmr>PcANB{5tAn^fv+b*BBZ__mH6*j*kpKY6cYHH05wQ1saW%`!y{Lj#O`D6dT zuJcFE|IxPz{?BkaN@JalU*xpMSz0^J>}Ty>EumGovhz^D`XGa#DN{sVU7j;VWT~dc z@>EUFrDZq2N~%kLT+W-Al~7UK{bRAhfvZRS#lzVrrT^R(+PboPrC+b)qpZXa7P~~! hJLc-xas{u|u$r{osYhl`=0W!}FBWyZwTtkx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zf%+QWXQf2*7SZ8tanmfEI6uY-60XYjuAZq1vdPiu1C zrT_hVrY?Q@)8qTn`^>-V|42T6%lKdC{eQ9dSpPFTo7aEi^FKfHhu8m}z5lD}$GrVT z+CQHkT`#tyzu`we>*OatbS4Y89eFY%=Tz~T4L5#Wy;^nS&uMNAr-+!-?7l%EA4=vV zz4Xc|T5;E1T;llO-oNwqZ@zx8e(Ch4KmL!*JMQ>Ds`H-vWS{!;7}3^4AHqrx%`6KI z);hf-xM}k(i5^#;?@c@U#cd8%>{C7-ygF>2)ikMytOyThPOc4WqxOHccNGc$V))OJ NRV2^>gysL=1OU{eM(_Xt diff --git a/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0007.jpg b/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0007.jpg deleted file mode 100644 index 6462147b2e144c8588772b7ae9a6e85999aaa38a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 928 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zBZ0rC{)^Ec{+kycWUWc>BIZw{;;p9Xb!t< zqN{ZNpHTnSSG$h>T{m;tt-p&8&QsdD{kQMeHyff3KELbFyTvYN^|SLP0n1*hxY}?0 z&+uVa9bf&$$RFwd8CsvW?*Ew`f8I{F@V#8YcEJam{)r#@U9)eSXWOx``QKc9YBx?b z{MpMj@!N&&z^yWNvcGj7pFR8S;=g16&c`o`zi55DpY_M=?my+{_iy^KTkoT@)x$lz z`TsKr3bP)v`D5Aq(2wiPhtmC<3q=#H{@k2bIQ#OiPcd69?bgjVlf3=&^`b8AUlRWr PWSY7(7BB%Z|Nol+E>mGe diff --git a/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0008.jpg b/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0008.jpg deleted file mode 100644 index de5dd70ec092541ada090fa76e1e8e701e7e3d2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 944 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 z?%-?tA0K{uD}Vm; z{|sq=XaBQL{Iuu2_4j%4Q`PUx|L|`AuQm2R`}hAbUq7Kf?Y_j1;sf?lHIk3+i@mbH zX(xB7{-M&p3s>w;*4KZl-~Pit_Oh=1MU&Ifr(f3GetkN_zA#sP`m@SOIjt%a?L+rJ z-0+_v-Ts%x{GS;=UjJv9+4ZmTV|Is4=&2q3tnCl7B=5#7t5lQPrWBda8+a~5=UBv_ zQ=*G)LVZ@tYL$jb%@aJ$x7yW>Z?1pZ+;GkR3>)`vzJ5r5fxi3?{Ubk*Us`PT;V|PP zG3g_JUhABE@o|0c&FO7B*X%Is=PC%()?e9vVna>sp)DILtIvI$<17`qWz(;#+K&>u f5AMnJc%bae|0Ca3B>aovKTB4TKnD<(|9=wz-H>Hw diff --git a/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0009.jpg b/tests/assets/action_detection_dataset/frames/train_video3/train_video3_0009.jpg deleted file mode 100644 index 22fc77d28c877ff149c3b7ef7a9017dc26d25ee9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 924 zcmex=kx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~ zblLJ1D_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq& z`TFhqkDtFl{$gZch6fqMV`%;o1p14Kg@u`g9po=Yrg9)=7Gz;nG-MNU3}jC%6jm~7 zRi)zLa|Z?ftEHC6#M)ziRwv*lc6ICR^-3L)xrwrvtY7 zevhg#Jp1hC*HiE5p8w^J7qiF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO{080}C^w5(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA385lSYFSuyG jMb5-xD%bP0l+XkKo$3>i literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2017_dataset/dataset/validation/2.jpg b/tests/assets/ade20k2017_dataset/dataset/validation/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3bd4d24c4983eab4132416da238c6a601cc70091 GIT binary patch literal 631 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO{080}C^w5(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA385npDEjasc n=2!O0iG3RSmjs`>T0LUS5@F-Ld(mzRP%VR}tDnm{r-UW|EWj44 literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2017_dataset/dataset/validation/2_parts_2.png b/tests/assets/ade20k2017_dataset/dataset/validation/2_parts_2.png new file mode 100644 index 0000000000000000000000000000000000000000..0c9ac07629edfca6911f5274f360ba6a3c02c2b1 GIT binary patch literal 88 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c6!2~3&r&&$}Qu3ZIjv*YfyayS185lU273_Yy m{|o6bzv#NBQ}2;)Nnz$pHWrn;pWJ{-89ZJ6T-G@yGywqSR25tR literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2017_dataset/dataset/validation/2_seg.png b/tests/assets/ade20k2017_dataset/dataset/validation/2_seg.png new file mode 100644 index 0000000000000000000000000000000000000000..3d5e9f0a11e473d4f3671762724b99e9fa221fee GIT binary patch literal 85 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c6!2~3&r&&$}QZk+{jv*YfyayQ>85lSYFSu;~ jMdXd*Nx4aH6kjq-_{qs7p7yQ=sE)zY)z4*}Q$iB}$xanB literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2017_dataset/dataset_with_meta_file/dataset_meta.json b/tests/assets/ade20k2017_dataset/dataset_with_meta_file/dataset_meta.json new file mode 100644 index 00000000000..feb0a37bb2f --- /dev/null +++ b/tests/assets/ade20k2017_dataset/dataset_with_meta_file/dataset_meta.json @@ -0,0 +1,3 @@ +{ + "label_map": { "0": "sky", "1": "person", "2": "license plate", "3": "rim" } +} diff --git a/tests/assets/ade20k2017_dataset/dataset_with_meta_file/training/street/1.jpg b/tests/assets/ade20k2017_dataset/dataset_with_meta_file/training/street/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3bd4d24c4983eab4132416da238c6a601cc70091 GIT binary patch literal 631 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO{080}C^w5(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA385lSYFSuyG jMb5-xD%bP0l+XkKo$3>i literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset/training/street/1.jpg b/tests/assets/ade20k2020_dataset/dataset/training/street/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..da0b2003e68e6c79fde1717f2481352d4f2de98c GIT binary patch literal 631 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO{0011mG55(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA3mdKI;Vst0Q4CS>;M1& literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset/training/street/1/instance_001_ADE_train_1.png b/tests/assets/ade20k2020_dataset/dataset/training/street/1/instance_001_ADE_train_1.png new file mode 100644 index 0000000000000000000000000000000000000000..7054099dff8a59238bca4518b1a682f206131bf2 GIT binary patch literal 83 zcmeAS@N?(olHy`uVBq!ia0vp^tRTz*Bp6n(R9XTlDNh&25Dr<^gN%$oUX#Kf`&|mX dCYL;vdzrm!S=mmg9R#Uj@O1TaS?83{1OQDm5orJb literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset/training/street/1/instance_002_ADE_train_1.png b/tests/assets/ade20k2020_dataset/dataset/training/street/1/instance_002_ADE_train_1.png new file mode 100644 index 0000000000000000000000000000000000000000..1a54e43a1b188039c103c2a7f50a0ce59b1fcfcf GIT binary patch literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^tRTz*Bp6n(R9XTl2~QWt5Dr<^gN%$oUW342ds#&g YU}o6Iz-@bY(Myn8Pgg&ebxsLQ0Q?>gOaK4? literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset/training/street/1_parts_1.png b/tests/assets/ade20k2020_dataset/dataset/training/street/1_parts_1.png new file mode 100644 index 0000000000000000000000000000000000000000..dd50d05d8784d1884027eddd1ef5899566539e8e GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^tRT$61SFYwH*Nw_DxNNmAsn)%2Mrk+7f literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset/validation/2.jpg b/tests/assets/ade20k2020_dataset/dataset/validation/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..da0b2003e68e6c79fde1717f2481352d4f2de98c GIT binary patch literal 631 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO{0011mG55(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA3mdKI;Vst0Q4CS>;M1& literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset/validation/2/instance_001_ADE_val_2.png b/tests/assets/ade20k2020_dataset/dataset/validation/2/instance_001_ADE_val_2.png new file mode 100644 index 0000000000000000000000000000000000000000..7054099dff8a59238bca4518b1a682f206131bf2 GIT binary patch literal 83 zcmeAS@N?(olHy`uVBq!ia0vp^tRTz*Bp6n(R9XTlDNh&25Dr<^gN%$oUX#Kf`&|mX dCYL;vdzrm!S=mmg9R#Uj@O1TaS?83{1OQDm5orJb literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset/validation/2/instance_002_ADE_val_2.png b/tests/assets/ade20k2020_dataset/dataset/validation/2/instance_002_ADE_val_2.png new file mode 100644 index 0000000000000000000000000000000000000000..1a54e43a1b188039c103c2a7f50a0ce59b1fcfcf GIT binary patch literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^tRTz*Bp6n(R9XTl2~QWt5Dr<^gN%$oUW342ds#&g YU}o6Iz-@bY(Myn8Pgg&ebxsLQ0Q?>gOaK4? literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset/validation/2/instance_003_ADE_val_2.png b/tests/assets/ade20k2020_dataset/dataset/validation/2/instance_003_ADE_val_2.png new file mode 100644 index 0000000000000000000000000000000000000000..62cee3758433f7881ac9553ab1d625c2a28a6337 GIT binary patch literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^tRTz*Bp6n(R9XTl2~QWt5Dr=1gN%$oUekgf^;QQD Z0l_1NoebQ(y8FRuJzf1=);T3K0RUg360!gQ literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset/validation/2_parts_1.png b/tests/assets/ade20k2020_dataset/dataset/validation/2_parts_1.png new file mode 100644 index 0000000000000000000000000000000000000000..dd50d05d8784d1884027eddd1ef5899566539e8e GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^tRT$61SFYwH*Nw_DxNNmAsn)%2Mrk+7cxAsn*42NfBCyh8?Y{6GIC lZpe8Oabn|yWX0o7eW}^v%;J?wWlMq544$rjF6*2UngBjM7T5p) literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset/validation/2_seg.png b/tests/assets/ade20k2020_dataset/dataset/validation/2_seg.png new file mode 100644 index 0000000000000000000000000000000000000000..abbc546cd29198dfaa420120de7b09c6dc809bd1 GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^tRT$61SFYwH*Nw_dY&$hAsn)@2N`)87&s1Z@IAZq z0qfG~u1?wp8@2rtlEUX5II_;Bu`{-kNqF}szTkx%275Af literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset_with_meta_file/dataset_meta.json b/tests/assets/ade20k2020_dataset/dataset_with_meta_file/dataset_meta.json new file mode 100644 index 00000000000..e84d973f926 --- /dev/null +++ b/tests/assets/ade20k2020_dataset/dataset_with_meta_file/dataset_meta.json @@ -0,0 +1,3 @@ +{ + "label_map": { "0": "car", "1": "person", "2": "door", "3": "rim" } +} diff --git a/tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1.jpg b/tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..da0b2003e68e6c79fde1717f2481352d4f2de98c GIT binary patch literal 631 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO{0011mG55(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA3mdKI;Vst0Q4CS>;M1& literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1/instance_001_ADE_train_1.png b/tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1/instance_001_ADE_train_1.png new file mode 100644 index 0000000000000000000000000000000000000000..7054099dff8a59238bca4518b1a682f206131bf2 GIT binary patch literal 83 zcmeAS@N?(olHy`uVBq!ia0vp^tRTz*Bp6n(R9XTlDNh&25Dr<^gN%$oUX#Kf`&|mX dCYL;vdzrm!S=mmg9R#Uj@O1TaS?83{1OQDm5orJb literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1/instance_002_ADE_train_1.png b/tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1/instance_002_ADE_train_1.png new file mode 100644 index 0000000000000000000000000000000000000000..1a54e43a1b188039c103c2a7f50a0ce59b1fcfcf GIT binary patch literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^tRTz*Bp6n(R9XTl2~QWt5Dr<^gN%$oUW342ds#&g YU}o6Iz-@bY(Myn8Pgg&ebxsLQ0Q?>gOaK4? literal 0 HcmV?d00001 diff --git a/tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1_parts_1.png b/tests/assets/ade20k2020_dataset/dataset_with_meta_file/training/street/1_parts_1.png new file mode 100644 index 0000000000000000000000000000000000000000..dd50d05d8784d1884027eddd1ef5899566539e8e GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^tRT$61SFYwH*Nw_DxNNmAsn)%2Mrk+7f literal 0 HcmV?d00001 diff --git a/tests/assets/anomaly/classification/test.json b/tests/assets/anomaly/classification/test.json new file mode 100644 index 00000000000..a16ae69665a --- /dev/null +++ b/tests/assets/anomaly/classification/test.json @@ -0,0 +1,41 @@ +{ + "image_path": { + "0": "test/good/25.jpg", + "1": "test/good/05.jpg", + "2": "test/good/28.jpg", + "3": "test/colour/06.jpg", + "4": "test/colour/03.jpg", + "5": "test/colour/07.jpg", + "6": "test/colour/14.jpg", + "7": "test/colour/01.jpg", + "8": "test/colour/12.jpg", + "9": "test/colour/11.jpg", + "10": "test/colour/09.jpg", + "11": "test/colour/08.jpg" + }, + "label": { + "0": "good", + "1": "good", + "2": "good", + "3": "abnormal", + "4": "abnormal", + "5": "abnormal", + "6": "abnormal", + "7": "abnormal", + "8": "abnormal", + "9": "abnormal", + "10": "abnormal", + "11": "abnormal" + }, + "masks": { + "3": "mask/colour/06.png", + "4": "mask/colour/03.png", + "5": "mask/colour/07.png", + "6": "mask/colour/14.png", + "7": "mask/colour/01.png", + "8": "mask/colour/12.png", + "9": "mask/colour/11.png", + "10": "mask/colour/09.png", + "11": "mask/colour/08.png" + } +} diff --git a/tests/assets/anomaly/classification/train.json b/tests/assets/anomaly/classification/train.json new file mode 100644 index 00000000000..26c75cf333c --- /dev/null +++ b/tests/assets/anomaly/classification/train.json @@ -0,0 +1,63 @@ +{ + "image_path": { + "0": "train/good/06.jpg", + "1": "train/good/32.jpg", + "2": "train/good/00.jpg", + "3": "train/good/33.jpg", + "4": "train/good/21.jpg", + "5": "train/good/03.jpg", + "6": "train/good/07.jpg", + "7": "train/good/14.jpg", + "8": "train/good/17.jpg", + "9": "train/good/02.jpg", + "10": "train/good/30.jpg", + "11": "train/good/26.jpg", + "12": "train/good/19.jpg", + "13": "train/good/16.jpg", + "14": "train/good/01.jpg", + "15": "train/good/15.jpg", + "16": "train/good/31.jpg", + "17": "train/good/27.jpg", + "18": "train/good/29.jpg", + "19": "train/good/10.jpg", + "20": "train/good/20.jpg", + "21": "train/good/12.jpg", + "22": "train/good/11.jpg", + "23": "train/good/24.jpg", + "24": "train/good/09.jpg", + "25": "train/good/08.jpg", + "26": "train/good/22.jpg", + "27": "train/good/18.jpg" + }, + "label": { + "0": "good", + "1": "good", + "2": "good", + "3": "good", + "4": "good", + "5": "good", + "6": "good", + "7": "good", + "8": "good", + "9": "good", + "10": "good", + "11": "good", + "12": "good", + "13": "good", + "14": "good", + "15": "good", + "16": "good", + "17": "good", + "18": "good", + "19": "good", + "20": "good", + "21": "good", + "22": "good", + "23": "good", + "24": "good", + "25": "good", + "26": "good", + "27": "good" + }, + "masks": {} +} diff --git a/tests/assets/anomaly/classification/val.json b/tests/assets/anomaly/classification/val.json new file mode 100644 index 00000000000..51dff55bea2 --- /dev/null +++ b/tests/assets/anomaly/classification/val.json @@ -0,0 +1,38 @@ +{ + "image_path": { + "0": "test/good/13.jpg", + "1": "test/good/23.jpg", + "2": "test/good/04.jpg", + "3": "test/colour/13.jpg", + "4": "test/colour/00.jpg", + "5": "test/colour/05.jpg", + "6": "test/colour/02.jpg", + "7": "test/colour/16.jpg", + "8": "test/colour/15.jpg", + "9": "test/colour/10.jpg", + "10": "test/colour/04.jpg" + }, + "label": { + "0": "good", + "1": "good", + "2": "good", + "3": "abnormal", + "4": "abnormal", + "5": "abnormal", + "6": "abnormal", + "7": "abnormal", + "8": "abnormal", + "9": "abnormal", + "10": "abnormal" + }, + "masks": { + "3": "mask/colour/13.png", + "4": "mask/colour/00.png", + "5": "mask/colour/05.png", + "6": "mask/colour/02.png", + "7": "mask/colour/16.png", + "8": "mask/colour/15.png", + "9": "mask/colour/10.png", + "10": "mask/colour/04.png" + } +} diff --git a/tests/assets/anomaly/detection/test.json b/tests/assets/anomaly/detection/test.json new file mode 100644 index 00000000000..d347897c4da --- /dev/null +++ b/tests/assets/anomaly/detection/test.json @@ -0,0 +1,62 @@ +{ + "image_path": { + "0": "test/good/25.jpg", + "1": "test/good/05.jpg", + "2": "test/good/28.jpg", + "3": "test/colour/06.jpg", + "4": "test/colour/03.jpg", + "5": "test/colour/07.jpg", + "6": "test/colour/14.jpg", + "7": "test/colour/01.jpg", + "8": "test/colour/12.jpg", + "9": "test/colour/11.jpg", + "10": "test/colour/09.jpg", + "11": "test/colour/08.jpg" + }, + "label": { + "0": "good", + "1": "good", + "2": "good", + "3": "abnormal", + "4": "abnormal", + "5": "abnormal", + "6": "abnormal", + "7": "abnormal", + "8": "abnormal", + "9": "abnormal", + "10": "abnormal", + "11": "abnormal" + }, + "bboxes": { + "3": [[0.3125, 0.4296875, 0.7109375, 0.69140625]], + "4": [ + [0.37109375, 0.240234375, 0.630859375, 0.333984375], + [0.375, 0.421875, 0.6875, 0.736328125] + ], + "5": [ + [0.509765625, 0.341796875, 0.751953125, 0.48046875], + [0.34375, 0.345703125, 0.64453125, 0.609375] + ], + "6": [[0.384765625, 0.55859375, 0.63671875, 0.712890625]], + "7": [[0.341796875, 0.33203125, 0.578125, 0.552734375]], + "8": [ + [0.28125, 0.240234375, 0.708984375, 0.62109375], + [0.724609375, 0.494140625, 0.75, 0.521484375] + ], + "9": [ + [0.2734375, 0.41796875, 0.404296875, 0.568359375], + [0.439453125, 0.43359375, 0.60546875, 0.52734375], + [0.330078125, 0.580078125, 0.44921875, 0.697265625], + [0.46875, 0.6640625, 0.484375, 0.68359375] + ], + "10": [ + [0.5859375, 0.310546875, 0.66015625, 0.408203125], + [0.705078125, 0.3125, 0.744140625, 0.349609375], + [0.345703125, 0.390625, 0.625, 0.623046875] + ], + "11": [ + [0.501953125, 0.267578125, 0.70703125, 0.6953125], + [0.2421875, 0.494140625, 0.279296875, 0.62109375] + ] + } +} diff --git a/tests/assets/anomaly/detection/train.json b/tests/assets/anomaly/detection/train.json new file mode 100644 index 00000000000..22c842cb67d --- /dev/null +++ b/tests/assets/anomaly/detection/train.json @@ -0,0 +1,63 @@ +{ + "image_path": { + "0": "train/good/06.jpg", + "1": "train/good/32.jpg", + "2": "train/good/00.jpg", + "3": "train/good/33.jpg", + "4": "train/good/21.jpg", + "5": "train/good/03.jpg", + "6": "train/good/07.jpg", + "7": "train/good/14.jpg", + "8": "train/good/17.jpg", + "9": "train/good/02.jpg", + "10": "train/good/30.jpg", + "11": "train/good/26.jpg", + "12": "train/good/19.jpg", + "13": "train/good/16.jpg", + "14": "train/good/01.jpg", + "15": "train/good/15.jpg", + "16": "train/good/31.jpg", + "17": "train/good/27.jpg", + "18": "train/good/29.jpg", + "19": "train/good/10.jpg", + "20": "train/good/20.jpg", + "21": "train/good/12.jpg", + "22": "train/good/11.jpg", + "23": "train/good/24.jpg", + "24": "train/good/09.jpg", + "25": "train/good/08.jpg", + "26": "train/good/22.jpg", + "27": "train/good/18.jpg" + }, + "label": { + "0": "good", + "1": "good", + "2": "good", + "3": "good", + "4": "good", + "5": "good", + "6": "good", + "7": "good", + "8": "good", + "9": "good", + "10": "good", + "11": "good", + "12": "good", + "13": "good", + "14": "good", + "15": "good", + "16": "good", + "17": "good", + "18": "good", + "19": "good", + "20": "good", + "21": "good", + "22": "good", + "23": "good", + "24": "good", + "25": "good", + "26": "good", + "27": "good" + }, + "bboxes": {} +} diff --git a/tests/assets/anomaly/detection/val.json b/tests/assets/anomaly/detection/val.json new file mode 100644 index 00000000000..7f31dc68a1c --- /dev/null +++ b/tests/assets/anomaly/detection/val.json @@ -0,0 +1,82 @@ +{ + "image_path": { + "0": "test/good/13.jpg", + "1": "test/good/23.jpg", + "2": "test/good/04.jpg", + "3": "test/colour/13.jpg", + "4": "test/colour/00.jpg", + "5": "test/colour/05.jpg", + "6": "test/colour/02.jpg", + "7": "test/colour/16.jpg", + "8": "test/colour/15.jpg", + "9": "test/colour/10.jpg", + "10": "test/colour/04.jpg" + }, + "label": { + "0": "good", + "1": "good", + "2": "good", + "3": "abnormal", + "4": "abnormal", + "5": "abnormal", + "6": "abnormal", + "7": "abnormal", + "8": "abnormal", + "9": "abnormal", + "10": "abnormal" + }, + "bboxes": { + "3": [ + [0.560546875, 0.384765625, 0.619140625, 0.4609375], + [0.41796875, 0.458984375, 0.630859375, 0.640625], + [0.423828125, 0.701171875, 0.484375, 0.734375], + [0.400390625, 0.7578125, 0.455078125, 0.794921875] + ], + "4": [ + [0.466796875, 0.443359375, 0.705078125, 0.603515625], + [0.48828125, 0.451171875, 0.517578125, 0.470703125], + [0.388671875, 0.521484375, 0.453125, 0.552734375], + [0.39453125, 0.599609375, 0.474609375, 0.65625], + [0.35546875, 0.62109375, 0.390625, 0.640625] + ], + "5": [ + [0.591796875, 0.412109375, 0.712890625, 0.595703125], + [0.21484375, 0.447265625, 0.2421875, 0.548828125], + [0.25, 0.447265625, 0.337890625, 0.552734375] + ], + "6": [ + [0.349609375, 0.23828125, 0.583984375, 0.314453125], + [0.265625, 0.384765625, 0.37890625, 0.568359375], + [0.369140625, 0.478515625, 0.50390625, 0.603515625], + [0.234375, 0.482421875, 0.263671875, 0.5390625], + [0.298828125, 0.580078125, 0.357421875, 0.626953125], + [0.328125, 0.72265625, 0.380859375, 0.767578125] + ], + "7": [ + [0.421875, 0.427734375, 0.580078125, 0.525390625], + [0.623046875, 0.453125, 0.67578125, 0.513671875] + ], + "8": [ + [0.5859375, 0.375, 0.751953125, 0.58203125], + [0.513671875, 0.39453125, 0.62890625, 0.501953125], + [0.337890625, 0.45703125, 0.47265625, 0.515625], + [0.626953125, 0.607421875, 0.642578125, 0.626953125] + ], + "9": [ + [0.361328125, 0.287109375, 0.396484375, 0.33203125], + [0.25390625, 0.392578125, 0.28125, 0.4375], + [0.447265625, 0.40234375, 0.74609375, 0.6328125], + [0.396484375, 0.474609375, 0.431640625, 0.505859375], + [0.4921875, 0.650390625, 0.513671875, 0.66796875], + [0.533203125, 0.669921875, 0.56640625, 0.693359375] + ], + "10": [ + [0.396484375, 0.41015625, 0.427734375, 0.4375], + [0.51953125, 0.482421875, 0.576171875, 0.6328125], + [0.357421875, 0.484375, 0.462890625, 0.64453125], + [0.541015625, 0.5078125, 0.6953125, 0.693359375], + [0.263671875, 0.51953125, 0.3125, 0.57421875], + [0.43359375, 0.708984375, 0.46484375, 0.728515625] + ] + } +} diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/00.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/00.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/00.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/00.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/01.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/01.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/01.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/01.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/02.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/02.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/02.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/02.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/03.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/03.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/03.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/03.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/04.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/04.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/04.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/04.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/05.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/05.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/05.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/05.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/06.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/06.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/06.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/06.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/07.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/07.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/07.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/07.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/08.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/08.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/08.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/08.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/09.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/09.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/09.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/09.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/10.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/10.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/10.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/10.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/11.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/11.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/11.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/11.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/12.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/12.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/12.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/12.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/13.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/13.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/13.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/13.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/14.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/14.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/14.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/14.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/15.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/15.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/15.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/15.png diff --git a/tests/assets/anomaly_hazelnut/ground_truth/colour/16.png b/tests/assets/anomaly/hazelnut/ground_truth/colour/16.png similarity index 100% rename from tests/assets/anomaly_hazelnut/ground_truth/colour/16.png rename to tests/assets/anomaly/hazelnut/ground_truth/colour/16.png diff --git a/tests/assets/anomaly_hazelnut/test/colour/00.jpg b/tests/assets/anomaly/hazelnut/test/colour/00.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/00.jpg rename to tests/assets/anomaly/hazelnut/test/colour/00.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/01.jpg b/tests/assets/anomaly/hazelnut/test/colour/01.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/01.jpg rename to tests/assets/anomaly/hazelnut/test/colour/01.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/02.jpg b/tests/assets/anomaly/hazelnut/test/colour/02.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/02.jpg rename to tests/assets/anomaly/hazelnut/test/colour/02.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/03.jpg b/tests/assets/anomaly/hazelnut/test/colour/03.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/03.jpg rename to tests/assets/anomaly/hazelnut/test/colour/03.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/04.jpg b/tests/assets/anomaly/hazelnut/test/colour/04.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/04.jpg rename to tests/assets/anomaly/hazelnut/test/colour/04.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/05.jpg b/tests/assets/anomaly/hazelnut/test/colour/05.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/05.jpg rename to tests/assets/anomaly/hazelnut/test/colour/05.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/06.jpg b/tests/assets/anomaly/hazelnut/test/colour/06.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/06.jpg rename to tests/assets/anomaly/hazelnut/test/colour/06.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/07.jpg b/tests/assets/anomaly/hazelnut/test/colour/07.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/07.jpg rename to tests/assets/anomaly/hazelnut/test/colour/07.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/08.jpg b/tests/assets/anomaly/hazelnut/test/colour/08.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/08.jpg rename to tests/assets/anomaly/hazelnut/test/colour/08.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/09.jpg b/tests/assets/anomaly/hazelnut/test/colour/09.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/09.jpg rename to tests/assets/anomaly/hazelnut/test/colour/09.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/10.jpg b/tests/assets/anomaly/hazelnut/test/colour/10.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/10.jpg rename to tests/assets/anomaly/hazelnut/test/colour/10.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/11.jpg b/tests/assets/anomaly/hazelnut/test/colour/11.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/11.jpg rename to tests/assets/anomaly/hazelnut/test/colour/11.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/12.jpg b/tests/assets/anomaly/hazelnut/test/colour/12.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/12.jpg rename to tests/assets/anomaly/hazelnut/test/colour/12.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/13.jpg b/tests/assets/anomaly/hazelnut/test/colour/13.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/13.jpg rename to tests/assets/anomaly/hazelnut/test/colour/13.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/14.jpg b/tests/assets/anomaly/hazelnut/test/colour/14.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/14.jpg rename to tests/assets/anomaly/hazelnut/test/colour/14.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/15.jpg b/tests/assets/anomaly/hazelnut/test/colour/15.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/15.jpg rename to tests/assets/anomaly/hazelnut/test/colour/15.jpg diff --git a/tests/assets/anomaly_hazelnut/test/colour/16.jpg b/tests/assets/anomaly/hazelnut/test/colour/16.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/colour/16.jpg rename to tests/assets/anomaly/hazelnut/test/colour/16.jpg diff --git a/tests/assets/anomaly_hazelnut/test/good/04.jpg b/tests/assets/anomaly/hazelnut/test/good/04.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/good/04.jpg rename to tests/assets/anomaly/hazelnut/test/good/04.jpg diff --git a/tests/assets/anomaly_hazelnut/test/good/05.jpg b/tests/assets/anomaly/hazelnut/test/good/05.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/good/05.jpg rename to tests/assets/anomaly/hazelnut/test/good/05.jpg diff --git a/tests/assets/anomaly_hazelnut/test/good/13.jpg b/tests/assets/anomaly/hazelnut/test/good/13.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/good/13.jpg rename to tests/assets/anomaly/hazelnut/test/good/13.jpg diff --git a/tests/assets/anomaly_hazelnut/test/good/23.jpg b/tests/assets/anomaly/hazelnut/test/good/23.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/good/23.jpg rename to tests/assets/anomaly/hazelnut/test/good/23.jpg diff --git a/tests/assets/anomaly_hazelnut/test/good/25.jpg b/tests/assets/anomaly/hazelnut/test/good/25.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/good/25.jpg rename to tests/assets/anomaly/hazelnut/test/good/25.jpg diff --git a/tests/assets/anomaly_hazelnut/test/good/28.jpg b/tests/assets/anomaly/hazelnut/test/good/28.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/test/good/28.jpg rename to tests/assets/anomaly/hazelnut/test/good/28.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/00.jpg b/tests/assets/anomaly/hazelnut/train/good/00.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/00.jpg rename to tests/assets/anomaly/hazelnut/train/good/00.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/01.jpg b/tests/assets/anomaly/hazelnut/train/good/01.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/01.jpg rename to tests/assets/anomaly/hazelnut/train/good/01.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/02.jpg b/tests/assets/anomaly/hazelnut/train/good/02.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/02.jpg rename to tests/assets/anomaly/hazelnut/train/good/02.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/03.jpg b/tests/assets/anomaly/hazelnut/train/good/03.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/03.jpg rename to tests/assets/anomaly/hazelnut/train/good/03.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/06.jpg b/tests/assets/anomaly/hazelnut/train/good/06.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/06.jpg rename to tests/assets/anomaly/hazelnut/train/good/06.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/07.jpg b/tests/assets/anomaly/hazelnut/train/good/07.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/07.jpg rename to tests/assets/anomaly/hazelnut/train/good/07.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/08.jpg b/tests/assets/anomaly/hazelnut/train/good/08.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/08.jpg rename to tests/assets/anomaly/hazelnut/train/good/08.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/09.jpg b/tests/assets/anomaly/hazelnut/train/good/09.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/09.jpg rename to tests/assets/anomaly/hazelnut/train/good/09.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/10.jpg b/tests/assets/anomaly/hazelnut/train/good/10.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/10.jpg rename to tests/assets/anomaly/hazelnut/train/good/10.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/11.jpg b/tests/assets/anomaly/hazelnut/train/good/11.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/11.jpg rename to tests/assets/anomaly/hazelnut/train/good/11.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/12.jpg b/tests/assets/anomaly/hazelnut/train/good/12.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/12.jpg rename to tests/assets/anomaly/hazelnut/train/good/12.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/14.jpg b/tests/assets/anomaly/hazelnut/train/good/14.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/14.jpg rename to tests/assets/anomaly/hazelnut/train/good/14.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/15.jpg b/tests/assets/anomaly/hazelnut/train/good/15.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/15.jpg rename to tests/assets/anomaly/hazelnut/train/good/15.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/16.jpg b/tests/assets/anomaly/hazelnut/train/good/16.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/16.jpg rename to tests/assets/anomaly/hazelnut/train/good/16.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/17.jpg b/tests/assets/anomaly/hazelnut/train/good/17.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/17.jpg rename to tests/assets/anomaly/hazelnut/train/good/17.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/18.jpg b/tests/assets/anomaly/hazelnut/train/good/18.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/18.jpg rename to tests/assets/anomaly/hazelnut/train/good/18.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/19.jpg b/tests/assets/anomaly/hazelnut/train/good/19.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/19.jpg rename to tests/assets/anomaly/hazelnut/train/good/19.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/20.jpg b/tests/assets/anomaly/hazelnut/train/good/20.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/20.jpg rename to tests/assets/anomaly/hazelnut/train/good/20.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/21.jpg b/tests/assets/anomaly/hazelnut/train/good/21.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/21.jpg rename to tests/assets/anomaly/hazelnut/train/good/21.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/22.jpg b/tests/assets/anomaly/hazelnut/train/good/22.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/22.jpg rename to tests/assets/anomaly/hazelnut/train/good/22.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/24.jpg b/tests/assets/anomaly/hazelnut/train/good/24.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/24.jpg rename to tests/assets/anomaly/hazelnut/train/good/24.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/26.jpg b/tests/assets/anomaly/hazelnut/train/good/26.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/26.jpg rename to tests/assets/anomaly/hazelnut/train/good/26.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/27.jpg b/tests/assets/anomaly/hazelnut/train/good/27.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/27.jpg rename to tests/assets/anomaly/hazelnut/train/good/27.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/29.jpg b/tests/assets/anomaly/hazelnut/train/good/29.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/29.jpg rename to tests/assets/anomaly/hazelnut/train/good/29.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/30.jpg b/tests/assets/anomaly/hazelnut/train/good/30.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/30.jpg rename to tests/assets/anomaly/hazelnut/train/good/30.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/31.jpg b/tests/assets/anomaly/hazelnut/train/good/31.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/31.jpg rename to tests/assets/anomaly/hazelnut/train/good/31.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/32.jpg b/tests/assets/anomaly/hazelnut/train/good/32.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/32.jpg rename to tests/assets/anomaly/hazelnut/train/good/32.jpg diff --git a/tests/assets/anomaly_hazelnut/train/good/33.jpg b/tests/assets/anomaly/hazelnut/train/good/33.jpg similarity index 100% rename from tests/assets/anomaly_hazelnut/train/good/33.jpg rename to tests/assets/anomaly/hazelnut/train/good/33.jpg diff --git a/tests/assets/anomaly/segmentation/test.json b/tests/assets/anomaly/segmentation/test.json new file mode 100644 index 00000000000..3090b7e125d --- /dev/null +++ b/tests/assets/anomaly/segmentation/test.json @@ -0,0 +1,2833 @@ +{ + "image_path": { + "0": "test/good/25.jpg", + "1": "test/good/05.jpg", + "2": "test/good/28.jpg", + "3": "test/colour/06.jpg", + "4": "test/colour/03.jpg", + "5": "test/colour/07.jpg", + "6": "test/colour/14.jpg", + "7": "test/colour/01.jpg", + "8": "test/colour/12.jpg", + "9": "test/colour/11.jpg", + "10": "test/colour/09.jpg", + "11": "test/colour/08.jpg" + }, + "label": { + "0": "good", + "1": "good", + "2": "good", + "3": "abnormal", + "4": "abnormal", + "5": "abnormal", + "6": "abnormal", + "7": "abnormal", + "8": "abnormal", + "9": "abnormal", + "10": "abnormal", + "11": "abnormal" + }, + "masks": { + "3": [ + [ + [0.50390625, 0.4296875], + [0.5, 0.43359375], + [0.5, 0.435546875], + [0.498046875, 0.4375], + [0.498046875, 0.443359375], + [0.49609375, 0.4453125], + [0.49609375, 0.451171875], + [0.4921875, 0.455078125], + [0.4921875, 0.45703125], + [0.490234375, 0.458984375], + [0.48828125, 0.458984375], + [0.484375, 0.462890625], + [0.482421875, 0.462890625], + [0.48046875, 0.46484375], + [0.478515625, 0.46484375], + [0.4765625, 0.466796875], + [0.46875, 0.466796875], + [0.466796875, 0.46875], + [0.451171875, 0.46875], + [0.44921875, 0.466796875], + [0.435546875, 0.466796875], + [0.43359375, 0.46484375], + [0.37890625, 0.46484375], + [0.376953125, 0.466796875], + [0.365234375, 0.466796875], + [0.36328125, 0.46875], + [0.357421875, 0.46875], + [0.35546875, 0.470703125], + [0.3515625, 0.470703125], + [0.349609375, 0.47265625], + [0.345703125, 0.47265625], + [0.34375, 0.474609375], + [0.341796875, 0.474609375], + [0.33984375, 0.4765625], + [0.337890625, 0.4765625], + [0.333984375, 0.48046875], + [0.33203125, 0.48046875], + [0.322265625, 0.490234375], + [0.322265625, 0.4921875], + [0.3203125, 0.494140625], + [0.3203125, 0.49609375], + [0.318359375, 0.498046875], + [0.318359375, 0.5], + [0.31640625, 0.501953125], + [0.31640625, 0.505859375], + [0.314453125, 0.5078125], + [0.314453125, 0.513671875], + [0.3125, 0.515625], + [0.3125, 0.544921875], + [0.314453125, 0.546875], + [0.314453125, 0.5546875], + [0.31640625, 0.556640625], + [0.31640625, 0.560546875], + [0.318359375, 0.5625], + [0.318359375, 0.56640625], + [0.3203125, 0.568359375], + [0.3203125, 0.5703125], + [0.322265625, 0.572265625], + [0.322265625, 0.57421875], + [0.32421875, 0.576171875], + [0.32421875, 0.578125], + [0.330078125, 0.583984375], + [0.333984375, 0.583984375], + [0.3359375, 0.5859375], + [0.357421875, 0.5859375], + [0.359375, 0.583984375], + [0.365234375, 0.583984375], + [0.3671875, 0.58203125], + [0.369140625, 0.58203125], + [0.37109375, 0.580078125], + [0.373046875, 0.580078125], + [0.375, 0.578125], + [0.3828125, 0.578125], + [0.384765625, 0.580078125], + [0.38671875, 0.580078125], + [0.388671875, 0.58203125], + [0.390625, 0.58203125], + [0.404296875, 0.595703125], + [0.40625, 0.595703125], + [0.408203125, 0.59765625], + [0.412109375, 0.59765625], + [0.4140625, 0.599609375], + [0.427734375, 0.599609375], + [0.4296875, 0.6015625], + [0.451171875, 0.6015625], + [0.453125, 0.603515625], + [0.45703125, 0.603515625], + [0.458984375, 0.60546875], + [0.4609375, 0.60546875], + [0.462890625, 0.607421875], + [0.46484375, 0.607421875], + [0.474609375, 0.6171875], + [0.474609375, 0.619140625], + [0.48046875, 0.625], + [0.48046875, 0.626953125], + [0.48828125, 0.634765625], + [0.48828125, 0.63671875], + [0.4921875, 0.640625], + [0.494140625, 0.640625], + [0.5, 0.646484375], + [0.501953125, 0.646484375], + [0.50390625, 0.6484375], + [0.5078125, 0.6484375], + [0.509765625, 0.650390625], + [0.515625, 0.650390625], + [0.517578125, 0.65234375], + [0.52734375, 0.65234375], + [0.529296875, 0.654296875], + [0.59375, 0.654296875], + [0.595703125, 0.65234375], + [0.6015625, 0.65234375], + [0.603515625, 0.650390625], + [0.60546875, 0.650390625], + [0.607421875, 0.6484375], + [0.609375, 0.6484375], + [0.61328125, 0.64453125], + [0.615234375, 0.64453125], + [0.619140625, 0.640625], + [0.630859375, 0.640625], + [0.640625, 0.650390625], + [0.640625, 0.66796875], + [0.638671875, 0.669921875], + [0.638671875, 0.681640625], + [0.640625, 0.68359375], + [0.640625, 0.685546875], + [0.64453125, 0.689453125], + [0.6484375, 0.689453125], + [0.654296875, 0.68359375], + [0.654296875, 0.681640625], + [0.658203125, 0.677734375], + [0.658203125, 0.67578125], + [0.662109375, 0.671875], + [0.662109375, 0.669921875], + [0.66796875, 0.6640625], + [0.66796875, 0.662109375], + [0.673828125, 0.65625], + [0.673828125, 0.654296875], + [0.67578125, 0.65234375], + [0.67578125, 0.650390625], + [0.677734375, 0.6484375], + [0.677734375, 0.646484375], + [0.6796875, 0.64453125], + [0.6796875, 0.642578125], + [0.68359375, 0.638671875], + [0.68359375, 0.63671875], + [0.685546875, 0.634765625], + [0.685546875, 0.6328125], + [0.6875, 0.630859375], + [0.6875, 0.62890625], + [0.69140625, 0.625], + [0.69140625, 0.623046875], + [0.693359375, 0.62109375], + [0.693359375, 0.619140625], + [0.6953125, 0.6171875], + [0.6953125, 0.615234375], + [0.697265625, 0.61328125], + [0.697265625, 0.611328125], + [0.69921875, 0.609375], + [0.69921875, 0.603515625], + [0.701171875, 0.6015625], + [0.701171875, 0.595703125], + [0.703125, 0.59375], + [0.703125, 0.5859375], + [0.705078125, 0.583984375], + [0.705078125, 0.568359375], + [0.70703125, 0.56640625], + [0.70703125, 0.548828125], + [0.708984375, 0.546875], + [0.708984375, 0.513671875], + [0.70703125, 0.51171875], + [0.70703125, 0.509765625], + [0.705078125, 0.5078125], + [0.705078125, 0.505859375], + [0.703125, 0.50390625], + [0.703125, 0.501953125], + [0.701171875, 0.501953125], + [0.69921875, 0.5], + [0.697265625, 0.501953125], + [0.6953125, 0.501953125], + [0.693359375, 0.50390625], + [0.69140625, 0.50390625], + [0.689453125, 0.505859375], + [0.685546875, 0.505859375], + [0.68359375, 0.5078125], + [0.681640625, 0.5078125], + [0.6796875, 0.509765625], + [0.671875, 0.509765625], + [0.669921875, 0.51171875], + [0.619140625, 0.51171875], + [0.6171875, 0.513671875], + [0.6171875, 0.515625], + [0.615234375, 0.517578125], + [0.615234375, 0.5234375], + [0.61328125, 0.525390625], + [0.61328125, 0.52734375], + [0.609375, 0.53125], + [0.607421875, 0.53125], + [0.60546875, 0.533203125], + [0.6015625, 0.533203125], + [0.599609375, 0.53515625], + [0.59375, 0.53515625], + [0.591796875, 0.537109375], + [0.5859375, 0.537109375], + [0.583984375, 0.5390625], + [0.564453125, 0.5390625], + [0.5546875, 0.529296875], + [0.5546875, 0.52734375], + [0.552734375, 0.525390625], + [0.552734375, 0.51171875], + [0.5546875, 0.509765625], + [0.5546875, 0.505859375], + [0.556640625, 0.50390625], + [0.556640625, 0.498046875], + [0.55859375, 0.49609375], + [0.55859375, 0.4921875], + [0.556640625, 0.490234375], + [0.556640625, 0.486328125], + [0.552734375, 0.482421875], + [0.552734375, 0.48046875], + [0.529296875, 0.45703125], + [0.529296875, 0.455078125], + [0.52734375, 0.453125], + [0.52734375, 0.451171875], + [0.525390625, 0.44921875], + [0.525390625, 0.447265625], + [0.5234375, 0.4453125], + [0.5234375, 0.443359375], + [0.521484375, 0.44140625], + [0.521484375, 0.439453125], + [0.51953125, 0.4375], + [0.51953125, 0.435546875], + [0.515625, 0.431640625], + [0.513671875, 0.431640625], + [0.51171875, 0.4296875] + ], + [ + [0.595703125, 0.5625], + [0.59765625, 0.560546875], + [0.6171875, 0.560546875], + [0.619140625, 0.5625], + [0.625, 0.5625], + [0.626953125, 0.564453125], + [0.63671875, 0.564453125], + [0.638671875, 0.56640625], + [0.642578125, 0.56640625], + [0.6484375, 0.572265625], + [0.6484375, 0.57421875], + [0.650390625, 0.576171875], + [0.650390625, 0.583984375], + [0.65234375, 0.5859375], + [0.65234375, 0.599609375], + [0.650390625, 0.6015625], + [0.650390625, 0.607421875], + [0.6484375, 0.609375], + [0.6484375, 0.611328125], + [0.64453125, 0.615234375], + [0.638671875, 0.615234375], + [0.63671875, 0.6171875], + [0.603515625, 0.6171875], + [0.6015625, 0.619140625], + [0.59375, 0.619140625], + [0.591796875, 0.62109375], + [0.583984375, 0.62109375], + [0.58203125, 0.623046875], + [0.572265625, 0.623046875], + [0.5703125, 0.625], + [0.552734375, 0.625], + [0.55078125, 0.623046875], + [0.546875, 0.623046875], + [0.544921875, 0.62109375], + [0.54296875, 0.62109375], + [0.541015625, 0.619140625], + [0.541015625, 0.6171875], + [0.5390625, 0.615234375], + [0.5390625, 0.611328125], + [0.541015625, 0.609375], + [0.541015625, 0.607421875], + [0.54296875, 0.60546875], + [0.54296875, 0.599609375], + [0.544921875, 0.59765625], + [0.544921875, 0.58984375], + [0.546875, 0.587890625], + [0.546875, 0.5859375], + [0.55078125, 0.58203125], + [0.5546875, 0.58203125], + [0.556640625, 0.580078125], + [0.568359375, 0.580078125], + [0.5703125, 0.578125], + [0.572265625, 0.578125], + [0.57421875, 0.576171875], + [0.576171875, 0.576171875], + [0.5859375, 0.56640625], + [0.587890625, 0.56640625], + [0.58984375, 0.564453125], + [0.591796875, 0.564453125], + [0.59375, 0.5625] + ] + ], + "4": [ + [ + [0.580078125, 0.421875], + [0.578125, 0.423828125], + [0.576171875, 0.423828125], + [0.5703125, 0.4296875], + [0.5703125, 0.431640625], + [0.568359375, 0.43359375], + [0.568359375, 0.443359375], + [0.56640625, 0.4453125], + [0.56640625, 0.458984375], + [0.564453125, 0.4609375], + [0.564453125, 0.46484375], + [0.5625, 0.466796875], + [0.5625, 0.470703125], + [0.548828125, 0.484375], + [0.546875, 0.484375], + [0.544921875, 0.486328125], + [0.54296875, 0.486328125], + [0.5390625, 0.490234375], + [0.537109375, 0.490234375], + [0.53515625, 0.4921875], + [0.53515625, 0.494140625], + [0.533203125, 0.49609375], + [0.533203125, 0.498046875], + [0.53125, 0.5], + [0.53125, 0.509765625], + [0.529296875, 0.51171875], + [0.529296875, 0.529296875], + [0.52734375, 0.53125], + [0.52734375, 0.5390625], + [0.525390625, 0.541015625], + [0.525390625, 0.54296875], + [0.5234375, 0.544921875], + [0.5234375, 0.546875], + [0.505859375, 0.564453125], + [0.50390625, 0.564453125], + [0.501953125, 0.56640625], + [0.4921875, 0.56640625], + [0.486328125, 0.560546875], + [0.486328125, 0.55859375], + [0.484375, 0.556640625], + [0.484375, 0.552734375], + [0.482421875, 0.55078125], + [0.482421875, 0.541015625], + [0.48046875, 0.5390625], + [0.48046875, 0.53515625], + [0.478515625, 0.53515625], + [0.4765625, 0.533203125], + [0.462890625, 0.533203125], + [0.4609375, 0.53125], + [0.45703125, 0.53125], + [0.455078125, 0.529296875], + [0.453125, 0.529296875], + [0.451171875, 0.52734375], + [0.451171875, 0.51953125], + [0.453125, 0.517578125], + [0.453125, 0.5078125], + [0.44921875, 0.50390625], + [0.4453125, 0.50390625], + [0.443359375, 0.505859375], + [0.44140625, 0.505859375], + [0.439453125, 0.5078125], + [0.4375, 0.5078125], + [0.435546875, 0.509765625], + [0.43359375, 0.509765625], + [0.4296875, 0.513671875], + [0.427734375, 0.513671875], + [0.419921875, 0.521484375], + [0.41796875, 0.521484375], + [0.4140625, 0.525390625], + [0.4140625, 0.52734375], + [0.40625, 0.53515625], + [0.40625, 0.537109375], + [0.396484375, 0.546875], + [0.396484375, 0.548828125], + [0.390625, 0.5546875], + [0.390625, 0.556640625], + [0.388671875, 0.55859375], + [0.388671875, 0.560546875], + [0.38671875, 0.5625], + [0.38671875, 0.564453125], + [0.3828125, 0.568359375], + [0.3828125, 0.5703125], + [0.380859375, 0.572265625], + [0.380859375, 0.57421875], + [0.37890625, 0.576171875], + [0.37890625, 0.578125], + [0.376953125, 0.580078125], + [0.376953125, 0.58203125], + [0.375, 0.583984375], + [0.375, 0.607421875], + [0.376953125, 0.609375], + [0.376953125, 0.615234375], + [0.37890625, 0.6171875], + [0.37890625, 0.62109375], + [0.380859375, 0.623046875], + [0.380859375, 0.626953125], + [0.3828125, 0.62890625], + [0.3828125, 0.630859375], + [0.384765625, 0.6328125], + [0.384765625, 0.634765625], + [0.38671875, 0.63671875], + [0.38671875, 0.638671875], + [0.388671875, 0.640625], + [0.388671875, 0.642578125], + [0.390625, 0.64453125], + [0.390625, 0.646484375], + [0.39453125, 0.650390625], + [0.39453125, 0.65234375], + [0.400390625, 0.658203125], + [0.400390625, 0.66015625], + [0.404296875, 0.6640625], + [0.404296875, 0.666015625], + [0.40625, 0.66796875], + [0.40625, 0.671875], + [0.408203125, 0.673828125], + [0.408203125, 0.677734375], + [0.41015625, 0.6796875], + [0.41015625, 0.6875], + [0.412109375, 0.689453125], + [0.412109375, 0.6953125], + [0.4140625, 0.697265625], + [0.4140625, 0.701171875], + [0.416015625, 0.703125], + [0.416015625, 0.705078125], + [0.41796875, 0.70703125], + [0.41796875, 0.708984375], + [0.421875, 0.712890625], + [0.421875, 0.71484375], + [0.42578125, 0.71875], + [0.42578125, 0.720703125], + [0.4375, 0.732421875], + [0.439453125, 0.732421875], + [0.44140625, 0.734375], + [0.451171875, 0.734375], + [0.453125, 0.732421875], + [0.455078125, 0.732421875], + [0.45703125, 0.73046875], + [0.45703125, 0.728515625], + [0.458984375, 0.7265625], + [0.458984375, 0.72265625], + [0.4609375, 0.720703125], + [0.4609375, 0.705078125], + [0.462890625, 0.703125], + [0.462890625, 0.69921875], + [0.466796875, 0.6953125], + [0.46875, 0.6953125], + [0.470703125, 0.693359375], + [0.478515625, 0.693359375], + [0.48046875, 0.6953125], + [0.482421875, 0.6953125], + [0.484375, 0.697265625], + [0.486328125, 0.697265625], + [0.48828125, 0.69921875], + [0.490234375, 0.69921875], + [0.4921875, 0.701171875], + [0.494140625, 0.701171875], + [0.498046875, 0.705078125], + [0.501953125, 0.705078125], + [0.50390625, 0.70703125], + [0.51171875, 0.70703125], + [0.513671875, 0.708984375], + [0.533203125, 0.708984375], + [0.53515625, 0.70703125], + [0.5390625, 0.70703125], + [0.541015625, 0.705078125], + [0.54296875, 0.705078125], + [0.548828125, 0.69921875], + [0.548828125, 0.6953125], + [0.55078125, 0.693359375], + [0.55078125, 0.65625], + [0.552734375, 0.654296875], + [0.552734375, 0.646484375], + [0.5546875, 0.64453125], + [0.5546875, 0.642578125], + [0.55859375, 0.638671875], + [0.560546875, 0.638671875], + [0.5625, 0.63671875], + [0.578125, 0.63671875], + [0.580078125, 0.638671875], + [0.603515625, 0.638671875], + [0.607421875, 0.642578125], + [0.607421875, 0.650390625], + [0.609375, 0.65234375], + [0.609375, 0.65625], + [0.611328125, 0.658203125], + [0.611328125, 0.66015625], + [0.61328125, 0.662109375], + [0.615234375, 0.662109375], + [0.6171875, 0.6640625], + [0.619140625, 0.6640625], + [0.62109375, 0.666015625], + [0.646484375, 0.666015625], + [0.6484375, 0.6640625], + [0.65234375, 0.6640625], + [0.654296875, 0.662109375], + [0.658203125, 0.662109375], + [0.66015625, 0.66015625], + [0.6640625, 0.66015625], + [0.666015625, 0.658203125], + [0.669921875, 0.658203125], + [0.671875, 0.65625], + [0.673828125, 0.65625], + [0.6796875, 0.650390625], + [0.6796875, 0.6484375], + [0.681640625, 0.646484375], + [0.681640625, 0.642578125], + [0.68359375, 0.640625], + [0.68359375, 0.62890625], + [0.681640625, 0.626953125], + [0.681640625, 0.623046875], + [0.671875, 0.61328125], + [0.669921875, 0.61328125], + [0.66796875, 0.611328125], + [0.666015625, 0.611328125], + [0.6640625, 0.609375], + [0.65625, 0.609375], + [0.654296875, 0.607421875], + [0.650390625, 0.607421875], + [0.642578125, 0.599609375], + [0.642578125, 0.59765625], + [0.640625, 0.595703125], + [0.640625, 0.591796875], + [0.642578125, 0.58984375], + [0.642578125, 0.587890625], + [0.646484375, 0.583984375], + [0.6484375, 0.583984375], + [0.650390625, 0.58203125], + [0.65234375, 0.58203125], + [0.654296875, 0.580078125], + [0.6640625, 0.580078125], + [0.666015625, 0.58203125], + [0.669921875, 0.58203125], + [0.671875, 0.583984375], + [0.67578125, 0.583984375], + [0.677734375, 0.5859375], + [0.681640625, 0.5859375], + [0.681640625, 0.583984375], + [0.68359375, 0.58203125], + [0.68359375, 0.564453125], + [0.685546875, 0.5625], + [0.685546875, 0.5546875], + [0.68359375, 0.552734375], + [0.68359375, 0.525390625], + [0.681640625, 0.5234375], + [0.681640625, 0.509765625], + [0.6796875, 0.5078125], + [0.6796875, 0.501953125], + [0.677734375, 0.5], + [0.677734375, 0.498046875], + [0.671875, 0.4921875], + [0.666015625, 0.4921875], + [0.66015625, 0.498046875], + [0.65234375, 0.498046875], + [0.650390625, 0.49609375], + [0.63671875, 0.49609375], + [0.634765625, 0.498046875], + [0.6328125, 0.498046875], + [0.630859375, 0.5], + [0.630859375, 0.501953125], + [0.62890625, 0.50390625], + [0.62890625, 0.5078125], + [0.623046875, 0.513671875], + [0.6015625, 0.513671875], + [0.599609375, 0.51171875], + [0.595703125, 0.51171875], + [0.59375, 0.509765625], + [0.591796875, 0.509765625], + [0.583984375, 0.501953125], + [0.583984375, 0.498046875], + [0.5859375, 0.49609375], + [0.5859375, 0.494140625], + [0.587890625, 0.4921875], + [0.587890625, 0.490234375], + [0.59375, 0.484375], + [0.59375, 0.482421875], + [0.59765625, 0.478515625], + [0.59765625, 0.4765625], + [0.599609375, 0.474609375], + [0.599609375, 0.47265625], + [0.6015625, 0.470703125], + [0.6015625, 0.466796875], + [0.603515625, 0.46484375], + [0.603515625, 0.4609375], + [0.6015625, 0.458984375], + [0.6015625, 0.4375], + [0.599609375, 0.435546875], + [0.599609375, 0.431640625], + [0.595703125, 0.427734375], + [0.595703125, 0.42578125], + [0.59375, 0.42578125], + [0.591796875, 0.423828125], + [0.58984375, 0.423828125], + [0.587890625, 0.421875] + ], + [ + [0.515625, 0.240234375], + [0.513671875, 0.2421875], + [0.501953125, 0.2421875], + [0.5, 0.244140625], + [0.4921875, 0.244140625], + [0.490234375, 0.24609375], + [0.482421875, 0.24609375], + [0.48046875, 0.248046875], + [0.474609375, 0.248046875], + [0.47265625, 0.25], + [0.46875, 0.25], + [0.466796875, 0.251953125], + [0.462890625, 0.251953125], + [0.4609375, 0.25390625], + [0.458984375, 0.25390625], + [0.45703125, 0.255859375], + [0.455078125, 0.255859375], + [0.453125, 0.2578125], + [0.451171875, 0.2578125], + [0.44921875, 0.259765625], + [0.447265625, 0.259765625], + [0.44140625, 0.265625], + [0.44140625, 0.279296875], + [0.4375, 0.283203125], + [0.4375, 0.28515625], + [0.431640625, 0.291015625], + [0.4296875, 0.291015625], + [0.42578125, 0.294921875], + [0.423828125, 0.294921875], + [0.421875, 0.296875], + [0.41796875, 0.296875], + [0.416015625, 0.298828125], + [0.412109375, 0.298828125], + [0.41015625, 0.30078125], + [0.408203125, 0.30078125], + [0.40625, 0.302734375], + [0.40234375, 0.302734375], + [0.400390625, 0.3046875], + [0.396484375, 0.3046875], + [0.39453125, 0.306640625], + [0.392578125, 0.306640625], + [0.390625, 0.30859375], + [0.38671875, 0.30859375], + [0.384765625, 0.310546875], + [0.3828125, 0.310546875], + [0.37109375, 0.322265625], + [0.37109375, 0.32421875], + [0.373046875, 0.326171875], + [0.375, 0.326171875], + [0.376953125, 0.328125], + [0.37890625, 0.328125], + [0.380859375, 0.330078125], + [0.384765625, 0.330078125], + [0.38671875, 0.33203125], + [0.40625, 0.33203125], + [0.408203125, 0.330078125], + [0.4140625, 0.330078125], + [0.416015625, 0.328125], + [0.421875, 0.328125], + [0.423828125, 0.326171875], + [0.427734375, 0.326171875], + [0.4296875, 0.32421875], + [0.43359375, 0.32421875], + [0.435546875, 0.322265625], + [0.439453125, 0.322265625], + [0.44140625, 0.3203125], + [0.443359375, 0.3203125], + [0.4453125, 0.318359375], + [0.44921875, 0.318359375], + [0.451171875, 0.31640625], + [0.455078125, 0.31640625], + [0.45703125, 0.314453125], + [0.4609375, 0.314453125], + [0.462890625, 0.3125], + [0.46484375, 0.3125], + [0.466796875, 0.310546875], + [0.470703125, 0.310546875], + [0.47265625, 0.30859375], + [0.474609375, 0.30859375], + [0.4765625, 0.306640625], + [0.478515625, 0.306640625], + [0.48046875, 0.3046875], + [0.482421875, 0.3046875], + [0.484375, 0.302734375], + [0.486328125, 0.302734375], + [0.490234375, 0.298828125], + [0.4921875, 0.298828125], + [0.49609375, 0.294921875], + [0.498046875, 0.294921875], + [0.50390625, 0.2890625], + [0.505859375, 0.2890625], + [0.5078125, 0.287109375], + [0.509765625, 0.287109375], + [0.51171875, 0.28515625], + [0.513671875, 0.28515625], + [0.515625, 0.283203125], + [0.51953125, 0.283203125], + [0.521484375, 0.28125], + [0.5390625, 0.28125], + [0.541015625, 0.283203125], + [0.552734375, 0.283203125], + [0.5546875, 0.28515625], + [0.5625, 0.28515625], + [0.564453125, 0.287109375], + [0.57421875, 0.287109375], + [0.576171875, 0.2890625], + [0.5859375, 0.2890625], + [0.587890625, 0.291015625], + [0.625, 0.291015625], + [0.626953125, 0.2890625], + [0.62890625, 0.2890625], + [0.62890625, 0.287109375], + [0.626953125, 0.28515625], + [0.626953125, 0.283203125], + [0.62109375, 0.27734375], + [0.619140625, 0.27734375], + [0.6171875, 0.275390625], + [0.61328125, 0.275390625], + [0.611328125, 0.2734375], + [0.60546875, 0.2734375], + [0.603515625, 0.271484375], + [0.59765625, 0.271484375], + [0.595703125, 0.26953125], + [0.591796875, 0.26953125], + [0.58984375, 0.267578125], + [0.587890625, 0.267578125], + [0.5859375, 0.265625], + [0.583984375, 0.265625], + [0.58203125, 0.263671875], + [0.580078125, 0.263671875], + [0.576171875, 0.259765625], + [0.57421875, 0.259765625], + [0.5703125, 0.255859375], + [0.568359375, 0.255859375], + [0.564453125, 0.251953125], + [0.5625, 0.251953125], + [0.55859375, 0.248046875], + [0.556640625, 0.248046875], + [0.5546875, 0.24609375], + [0.552734375, 0.24609375], + [0.55078125, 0.244140625], + [0.548828125, 0.244140625], + [0.546875, 0.2421875], + [0.544921875, 0.2421875], + [0.54296875, 0.240234375] + ] + ], + "5": [ + [ + [0.37890625, 0.345703125], + [0.376953125, 0.34765625], + [0.375, 0.34765625], + [0.375, 0.349609375], + [0.36328125, 0.361328125], + [0.361328125, 0.361328125], + [0.35546875, 0.3671875], + [0.353515625, 0.3671875], + [0.345703125, 0.375], + [0.345703125, 0.376953125], + [0.34375, 0.37890625], + [0.34375, 0.392578125], + [0.345703125, 0.39453125], + [0.345703125, 0.400390625], + [0.34765625, 0.40234375], + [0.34765625, 0.408203125], + [0.349609375, 0.41015625], + [0.349609375, 0.4140625], + [0.3515625, 0.416015625], + [0.3515625, 0.41796875], + [0.353515625, 0.419921875], + [0.353515625, 0.423828125], + [0.35546875, 0.42578125], + [0.35546875, 0.427734375], + [0.357421875, 0.4296875], + [0.357421875, 0.431640625], + [0.361328125, 0.435546875], + [0.361328125, 0.4375], + [0.3671875, 0.443359375], + [0.3671875, 0.4453125], + [0.37109375, 0.44921875], + [0.37109375, 0.451171875], + [0.373046875, 0.453125], + [0.373046875, 0.455078125], + [0.375, 0.45703125], + [0.375, 0.458984375], + [0.376953125, 0.4609375], + [0.376953125, 0.462890625], + [0.37890625, 0.46484375], + [0.37890625, 0.466796875], + [0.380859375, 0.46875], + [0.380859375, 0.470703125], + [0.3828125, 0.47265625], + [0.3828125, 0.474609375], + [0.384765625, 0.4765625], + [0.384765625, 0.478515625], + [0.38671875, 0.48046875], + [0.38671875, 0.482421875], + [0.388671875, 0.484375], + [0.388671875, 0.48828125], + [0.390625, 0.490234375], + [0.390625, 0.4921875], + [0.392578125, 0.494140625], + [0.392578125, 0.49609375], + [0.39453125, 0.498046875], + [0.39453125, 0.5], + [0.396484375, 0.501953125], + [0.396484375, 0.50390625], + [0.3984375, 0.505859375], + [0.3984375, 0.5078125], + [0.400390625, 0.509765625], + [0.400390625, 0.51171875], + [0.40234375, 0.513671875], + [0.40234375, 0.515625], + [0.404296875, 0.517578125], + [0.404296875, 0.51953125], + [0.408203125, 0.5234375], + [0.408203125, 0.525390625], + [0.41015625, 0.52734375], + [0.41015625, 0.529296875], + [0.4140625, 0.533203125], + [0.4140625, 0.53515625], + [0.416015625, 0.537109375], + [0.416015625, 0.5390625], + [0.421875, 0.544921875], + [0.421875, 0.546875], + [0.427734375, 0.552734375], + [0.427734375, 0.5546875], + [0.447265625, 0.57421875], + [0.44921875, 0.57421875], + [0.458984375, 0.583984375], + [0.4609375, 0.583984375], + [0.46484375, 0.587890625], + [0.466796875, 0.587890625], + [0.46875, 0.58984375], + [0.470703125, 0.58984375], + [0.47265625, 0.591796875], + [0.474609375, 0.591796875], + [0.4765625, 0.59375], + [0.478515625, 0.59375], + [0.48046875, 0.595703125], + [0.484375, 0.595703125], + [0.486328125, 0.59765625], + [0.490234375, 0.59765625], + [0.4921875, 0.599609375], + [0.49609375, 0.599609375], + [0.498046875, 0.6015625], + [0.505859375, 0.6015625], + [0.5078125, 0.603515625], + [0.525390625, 0.603515625], + [0.52734375, 0.60546875], + [0.55078125, 0.60546875], + [0.552734375, 0.603515625], + [0.57421875, 0.603515625], + [0.576171875, 0.60546875], + [0.591796875, 0.60546875], + [0.59375, 0.607421875], + [0.626953125, 0.607421875], + [0.62890625, 0.60546875], + [0.6328125, 0.60546875], + [0.640625, 0.59765625], + [0.640625, 0.595703125], + [0.642578125, 0.59375], + [0.642578125, 0.580078125], + [0.640625, 0.578125], + [0.640625, 0.57421875], + [0.638671875, 0.572265625], + [0.638671875, 0.5703125], + [0.6328125, 0.564453125], + [0.630859375, 0.564453125], + [0.626953125, 0.560546875], + [0.625, 0.560546875], + [0.623046875, 0.55859375], + [0.62109375, 0.55859375], + [0.619140625, 0.556640625], + [0.6171875, 0.556640625], + [0.615234375, 0.5546875], + [0.61328125, 0.5546875], + [0.611328125, 0.552734375], + [0.607421875, 0.552734375], + [0.60546875, 0.55078125], + [0.599609375, 0.55078125], + [0.59765625, 0.548828125], + [0.58984375, 0.548828125], + [0.587890625, 0.546875], + [0.580078125, 0.546875], + [0.578125, 0.544921875], + [0.568359375, 0.544921875], + [0.56640625, 0.54296875], + [0.560546875, 0.54296875], + [0.55859375, 0.541015625], + [0.5546875, 0.541015625], + [0.552734375, 0.5390625], + [0.55078125, 0.5390625], + [0.548828125, 0.537109375], + [0.546875, 0.537109375], + [0.54296875, 0.533203125], + [0.541015625, 0.533203125], + [0.537109375, 0.529296875], + [0.53515625, 0.529296875], + [0.53125, 0.525390625], + [0.529296875, 0.525390625], + [0.525390625, 0.521484375], + [0.5234375, 0.521484375], + [0.51953125, 0.517578125], + [0.517578125, 0.517578125], + [0.513671875, 0.513671875], + [0.51171875, 0.513671875], + [0.509765625, 0.51171875], + [0.5078125, 0.51171875], + [0.505859375, 0.509765625], + [0.50390625, 0.509765625], + [0.501953125, 0.5078125], + [0.5, 0.5078125], + [0.498046875, 0.505859375], + [0.494140625, 0.505859375], + [0.4921875, 0.50390625], + [0.48828125, 0.50390625], + [0.486328125, 0.505859375], + [0.484375, 0.505859375], + [0.482421875, 0.5078125], + [0.482421875, 0.509765625], + [0.48046875, 0.51171875], + [0.48046875, 0.513671875], + [0.474609375, 0.51953125], + [0.466796875, 0.51953125], + [0.46484375, 0.517578125], + [0.4609375, 0.517578125], + [0.458984375, 0.515625], + [0.45703125, 0.515625], + [0.4453125, 0.50390625], + [0.4453125, 0.501953125], + [0.44140625, 0.498046875], + [0.44140625, 0.49609375], + [0.439453125, 0.494140625], + [0.439453125, 0.490234375], + [0.4375, 0.48828125], + [0.4375, 0.484375], + [0.435546875, 0.482421875], + [0.435546875, 0.439453125], + [0.43359375, 0.4375], + [0.43359375, 0.421875], + [0.431640625, 0.419921875], + [0.431640625, 0.41015625], + [0.4296875, 0.408203125], + [0.4296875, 0.400390625], + [0.427734375, 0.3984375], + [0.427734375, 0.392578125], + [0.42578125, 0.390625], + [0.42578125, 0.388671875], + [0.423828125, 0.38671875], + [0.423828125, 0.384765625], + [0.419921875, 0.380859375], + [0.419921875, 0.37890625], + [0.404296875, 0.36328125], + [0.40234375, 0.36328125], + [0.39453125, 0.35546875], + [0.392578125, 0.35546875], + [0.388671875, 0.3515625], + [0.38671875, 0.3515625], + [0.3828125, 0.34765625], + [0.380859375, 0.34765625] + ], + [ + [0.525390625, 0.341796875], + [0.5234375, 0.34375], + [0.517578125, 0.34375], + [0.515625, 0.345703125], + [0.513671875, 0.345703125], + [0.51171875, 0.34765625], + [0.51171875, 0.349609375], + [0.509765625, 0.3515625], + [0.509765625, 0.357421875], + [0.51171875, 0.359375], + [0.51171875, 0.361328125], + [0.513671875, 0.36328125], + [0.513671875, 0.365234375], + [0.517578125, 0.369140625], + [0.51953125, 0.369140625], + [0.5234375, 0.373046875], + [0.525390625, 0.373046875], + [0.52734375, 0.375], + [0.529296875, 0.375], + [0.53125, 0.376953125], + [0.53515625, 0.376953125], + [0.537109375, 0.37890625], + [0.576171875, 0.37890625], + [0.578125, 0.380859375], + [0.587890625, 0.380859375], + [0.58984375, 0.3828125], + [0.59375, 0.3828125], + [0.595703125, 0.384765625], + [0.599609375, 0.384765625], + [0.6015625, 0.38671875], + [0.603515625, 0.38671875], + [0.60546875, 0.388671875], + [0.607421875, 0.388671875], + [0.609375, 0.390625], + [0.611328125, 0.390625], + [0.615234375, 0.39453125], + [0.6171875, 0.39453125], + [0.6328125, 0.41015625], + [0.634765625, 0.41015625], + [0.642578125, 0.41796875], + [0.64453125, 0.41796875], + [0.646484375, 0.419921875], + [0.6484375, 0.419921875], + [0.65234375, 0.423828125], + [0.654296875, 0.423828125], + [0.65625, 0.42578125], + [0.658203125, 0.42578125], + [0.66015625, 0.427734375], + [0.662109375, 0.427734375], + [0.6640625, 0.4296875], + [0.666015625, 0.4296875], + [0.66796875, 0.431640625], + [0.671875, 0.431640625], + [0.673828125, 0.43359375], + [0.6796875, 0.43359375], + [0.681640625, 0.435546875], + [0.689453125, 0.435546875], + [0.69140625, 0.4375], + [0.6953125, 0.4375], + [0.697265625, 0.439453125], + [0.701171875, 0.439453125], + [0.703125, 0.44140625], + [0.705078125, 0.44140625], + [0.716796875, 0.453125], + [0.716796875, 0.455078125], + [0.720703125, 0.458984375], + [0.720703125, 0.4609375], + [0.72265625, 0.462890625], + [0.72265625, 0.46484375], + [0.724609375, 0.466796875], + [0.724609375, 0.46875], + [0.728515625, 0.47265625], + [0.728515625, 0.474609375], + [0.73046875, 0.4765625], + [0.732421875, 0.4765625], + [0.734375, 0.478515625], + [0.744140625, 0.478515625], + [0.748046875, 0.474609375], + [0.748046875, 0.47265625], + [0.75, 0.470703125], + [0.75, 0.45703125], + [0.748046875, 0.455078125], + [0.748046875, 0.44921875], + [0.74609375, 0.447265625], + [0.74609375, 0.443359375], + [0.744140625, 0.44140625], + [0.744140625, 0.439453125], + [0.7421875, 0.4375], + [0.7421875, 0.43359375], + [0.740234375, 0.431640625], + [0.740234375, 0.427734375], + [0.73828125, 0.42578125], + [0.73828125, 0.421875], + [0.736328125, 0.419921875], + [0.736328125, 0.41796875], + [0.732421875, 0.4140625], + [0.732421875, 0.412109375], + [0.71484375, 0.39453125], + [0.71484375, 0.392578125], + [0.703125, 0.380859375], + [0.703125, 0.37890625], + [0.69921875, 0.375], + [0.697265625, 0.375], + [0.693359375, 0.37109375], + [0.69140625, 0.37109375], + [0.6875, 0.3671875], + [0.685546875, 0.3671875], + [0.68359375, 0.365234375], + [0.6796875, 0.365234375], + [0.677734375, 0.36328125], + [0.673828125, 0.36328125], + [0.671875, 0.361328125], + [0.66796875, 0.361328125], + [0.666015625, 0.359375], + [0.658203125, 0.359375], + [0.65625, 0.357421875], + [0.623046875, 0.357421875], + [0.62109375, 0.359375], + [0.595703125, 0.359375], + [0.59375, 0.357421875], + [0.591796875, 0.357421875], + [0.587890625, 0.353515625], + [0.5859375, 0.353515625], + [0.58203125, 0.349609375], + [0.580078125, 0.349609375], + [0.578125, 0.34765625], + [0.576171875, 0.34765625], + [0.57421875, 0.345703125], + [0.572265625, 0.345703125], + [0.5703125, 0.34375], + [0.56640625, 0.34375], + [0.564453125, 0.341796875] + ] + ], + "6": [ + [ + [0.57421875, 0.55859375], + [0.572265625, 0.560546875], + [0.568359375, 0.560546875], + [0.56640625, 0.5625], + [0.564453125, 0.5625], + [0.5625, 0.564453125], + [0.560546875, 0.564453125], + [0.55859375, 0.56640625], + [0.556640625, 0.56640625], + [0.552734375, 0.5703125], + [0.55078125, 0.5703125], + [0.548828125, 0.572265625], + [0.546875, 0.572265625], + [0.544921875, 0.57421875], + [0.54296875, 0.57421875], + [0.541015625, 0.576171875], + [0.53515625, 0.576171875], + [0.533203125, 0.578125], + [0.53125, 0.578125], + [0.529296875, 0.576171875], + [0.521484375, 0.576171875], + [0.51953125, 0.57421875], + [0.513671875, 0.57421875], + [0.51171875, 0.572265625], + [0.5, 0.572265625], + [0.498046875, 0.57421875], + [0.494140625, 0.57421875], + [0.490234375, 0.578125], + [0.48828125, 0.578125], + [0.482421875, 0.583984375], + [0.482421875, 0.5859375], + [0.48046875, 0.587890625], + [0.48046875, 0.58984375], + [0.478515625, 0.591796875], + [0.478515625, 0.65234375], + [0.4765625, 0.654296875], + [0.4765625, 0.66015625], + [0.474609375, 0.662109375], + [0.474609375, 0.6640625], + [0.46484375, 0.673828125], + [0.462890625, 0.673828125], + [0.4609375, 0.67578125], + [0.458984375, 0.67578125], + [0.45703125, 0.677734375], + [0.451171875, 0.677734375], + [0.44921875, 0.6796875], + [0.435546875, 0.6796875], + [0.43359375, 0.677734375], + [0.42578125, 0.677734375], + [0.423828125, 0.67578125], + [0.419921875, 0.67578125], + [0.41796875, 0.673828125], + [0.416015625, 0.673828125], + [0.4140625, 0.671875], + [0.412109375, 0.671875], + [0.41015625, 0.669921875], + [0.40625, 0.669921875], + [0.404296875, 0.66796875], + [0.40234375, 0.66796875], + [0.400390625, 0.666015625], + [0.390625, 0.666015625], + [0.384765625, 0.671875], + [0.384765625, 0.69140625], + [0.38671875, 0.693359375], + [0.38671875, 0.697265625], + [0.390625, 0.701171875], + [0.390625, 0.703125], + [0.392578125, 0.703125], + [0.39453125, 0.705078125], + [0.396484375, 0.705078125], + [0.3984375, 0.70703125], + [0.40234375, 0.70703125], + [0.404296875, 0.708984375], + [0.41796875, 0.708984375], + [0.419921875, 0.7109375], + [0.44140625, 0.7109375], + [0.443359375, 0.708984375], + [0.447265625, 0.708984375], + [0.44921875, 0.70703125], + [0.451171875, 0.70703125], + [0.453125, 0.705078125], + [0.45703125, 0.705078125], + [0.458984375, 0.703125], + [0.4609375, 0.703125], + [0.462890625, 0.701171875], + [0.470703125, 0.701171875], + [0.47265625, 0.69921875], + [0.484375, 0.69921875], + [0.486328125, 0.697265625], + [0.49609375, 0.697265625], + [0.498046875, 0.6953125], + [0.50390625, 0.6953125], + [0.505859375, 0.693359375], + [0.51171875, 0.693359375], + [0.513671875, 0.69140625], + [0.515625, 0.69140625], + [0.517578125, 0.689453125], + [0.521484375, 0.689453125], + [0.5234375, 0.6875], + [0.525390625, 0.6875], + [0.52734375, 0.685546875], + [0.529296875, 0.685546875], + [0.53125, 0.68359375], + [0.533203125, 0.68359375], + [0.53515625, 0.681640625], + [0.537109375, 0.681640625], + [0.546875, 0.671875], + [0.546875, 0.669921875], + [0.548828125, 0.66796875], + [0.548828125, 0.666015625], + [0.55078125, 0.6640625], + [0.55078125, 0.662109375], + [0.552734375, 0.66015625], + [0.552734375, 0.654296875], + [0.5546875, 0.65234375], + [0.5546875, 0.646484375], + [0.556640625, 0.64453125], + [0.556640625, 0.638671875], + [0.55859375, 0.63671875], + [0.55859375, 0.634765625], + [0.560546875, 0.6328125], + [0.560546875, 0.630859375], + [0.564453125, 0.626953125], + [0.564453125, 0.625], + [0.572265625, 0.6171875], + [0.57421875, 0.6171875], + [0.578125, 0.61328125], + [0.580078125, 0.61328125], + [0.583984375, 0.609375], + [0.5859375, 0.609375], + [0.587890625, 0.607421875], + [0.591796875, 0.607421875], + [0.59375, 0.60546875], + [0.595703125, 0.60546875], + [0.59765625, 0.603515625], + [0.6015625, 0.603515625], + [0.603515625, 0.6015625], + [0.607421875, 0.6015625], + [0.609375, 0.599609375], + [0.615234375, 0.599609375], + [0.6171875, 0.59765625], + [0.623046875, 0.59765625], + [0.625, 0.595703125], + [0.626953125, 0.595703125], + [0.6328125, 0.58984375], + [0.6328125, 0.587890625], + [0.634765625, 0.5859375], + [0.634765625, 0.576171875], + [0.6328125, 0.57421875], + [0.6328125, 0.572265625], + [0.630859375, 0.572265625], + [0.62890625, 0.5703125], + [0.625, 0.5703125], + [0.623046875, 0.572265625], + [0.619140625, 0.572265625], + [0.6171875, 0.57421875], + [0.61328125, 0.57421875], + [0.611328125, 0.576171875], + [0.6015625, 0.576171875], + [0.599609375, 0.57421875], + [0.59765625, 0.57421875], + [0.58984375, 0.56640625], + [0.58984375, 0.564453125], + [0.587890625, 0.5625], + [0.5859375, 0.5625], + [0.583984375, 0.560546875], + [0.58203125, 0.560546875], + [0.580078125, 0.55859375] + ] + ], + "7": [ + [ + [0.36328125, 0.33203125], + [0.361328125, 0.333984375], + [0.357421875, 0.333984375], + [0.35546875, 0.3359375], + [0.3515625, 0.3359375], + [0.349609375, 0.337890625], + [0.34765625, 0.337890625], + [0.341796875, 0.34375], + [0.341796875, 0.349609375], + [0.361328125, 0.349609375], + [0.36328125, 0.34765625], + [0.375, 0.34765625], + [0.37890625, 0.3515625], + [0.37890625, 0.36328125], + [0.375, 0.3671875], + [0.375, 0.369140625], + [0.361328125, 0.3828125], + [0.359375, 0.3828125], + [0.357421875, 0.384765625], + [0.357421875, 0.38671875], + [0.353515625, 0.390625], + [0.353515625, 0.392578125], + [0.3515625, 0.39453125], + [0.3515625, 0.396484375], + [0.349609375, 0.3984375], + [0.349609375, 0.439453125], + [0.3515625, 0.44140625], + [0.3515625, 0.4453125], + [0.353515625, 0.447265625], + [0.353515625, 0.44921875], + [0.361328125, 0.45703125], + [0.36328125, 0.45703125], + [0.365234375, 0.458984375], + [0.380859375, 0.458984375], + [0.3828125, 0.4609375], + [0.384765625, 0.4609375], + [0.392578125, 0.46875], + [0.392578125, 0.470703125], + [0.39453125, 0.47265625], + [0.39453125, 0.486328125], + [0.396484375, 0.48828125], + [0.396484375, 0.490234375], + [0.3984375, 0.4921875], + [0.3984375, 0.494140625], + [0.40234375, 0.498046875], + [0.40234375, 0.5], + [0.40625, 0.50390625], + [0.40625, 0.505859375], + [0.408203125, 0.5078125], + [0.408203125, 0.509765625], + [0.41015625, 0.51171875], + [0.41015625, 0.513671875], + [0.412109375, 0.515625], + [0.412109375, 0.517578125], + [0.41796875, 0.5234375], + [0.419921875, 0.5234375], + [0.421875, 0.525390625], + [0.42578125, 0.525390625], + [0.427734375, 0.5234375], + [0.4296875, 0.5234375], + [0.43359375, 0.51953125], + [0.435546875, 0.51953125], + [0.4375, 0.517578125], + [0.44921875, 0.517578125], + [0.453125, 0.521484375], + [0.455078125, 0.521484375], + [0.466796875, 0.533203125], + [0.46875, 0.533203125], + [0.48046875, 0.544921875], + [0.482421875, 0.544921875], + [0.48828125, 0.55078125], + [0.498046875, 0.55078125], + [0.498046875, 0.548828125], + [0.5, 0.546875], + [0.5, 0.544921875], + [0.501953125, 0.54296875], + [0.501953125, 0.53515625], + [0.50390625, 0.533203125], + [0.50390625, 0.529296875], + [0.513671875, 0.51953125], + [0.515625, 0.51953125], + [0.517578125, 0.517578125], + [0.51953125, 0.517578125], + [0.521484375, 0.515625], + [0.53125, 0.515625], + [0.533203125, 0.517578125], + [0.53515625, 0.517578125], + [0.541015625, 0.5234375], + [0.54296875, 0.5234375], + [0.544921875, 0.525390625], + [0.548828125, 0.525390625], + [0.55078125, 0.52734375], + [0.55859375, 0.52734375], + [0.560546875, 0.529296875], + [0.56640625, 0.529296875], + [0.568359375, 0.52734375], + [0.5703125, 0.52734375], + [0.57421875, 0.5234375], + [0.57421875, 0.517578125], + [0.576171875, 0.515625], + [0.576171875, 0.5078125], + [0.57421875, 0.505859375], + [0.57421875, 0.50390625], + [0.572265625, 0.501953125], + [0.572265625, 0.5], + [0.5703125, 0.498046875], + [0.568359375, 0.498046875], + [0.56640625, 0.49609375], + [0.55078125, 0.49609375], + [0.548828125, 0.494140625], + [0.537109375, 0.494140625], + [0.52734375, 0.484375], + [0.52734375, 0.474609375], + [0.529296875, 0.47265625], + [0.529296875, 0.470703125], + [0.533203125, 0.466796875], + [0.53515625, 0.466796875], + [0.541015625, 0.4609375], + [0.54296875, 0.4609375], + [0.544921875, 0.458984375], + [0.546875, 0.458984375], + [0.552734375, 0.453125], + [0.552734375, 0.451171875], + [0.5546875, 0.44921875], + [0.5546875, 0.423828125], + [0.552734375, 0.421875], + [0.552734375, 0.41796875], + [0.548828125, 0.4140625], + [0.548828125, 0.412109375], + [0.529296875, 0.392578125], + [0.529296875, 0.390625], + [0.525390625, 0.38671875], + [0.525390625, 0.384765625], + [0.521484375, 0.380859375], + [0.521484375, 0.37890625], + [0.51953125, 0.376953125], + [0.51953125, 0.375], + [0.517578125, 0.373046875], + [0.517578125, 0.37109375], + [0.515625, 0.369140625], + [0.515625, 0.3671875], + [0.513671875, 0.365234375], + [0.513671875, 0.36328125], + [0.5078125, 0.357421875], + [0.505859375, 0.357421875], + [0.50390625, 0.35546875], + [0.501953125, 0.35546875], + [0.5, 0.353515625], + [0.49609375, 0.353515625], + [0.494140625, 0.3515625], + [0.466796875, 0.3515625], + [0.46484375, 0.349609375], + [0.451171875, 0.349609375], + [0.44921875, 0.34765625], + [0.439453125, 0.34765625], + [0.4375, 0.345703125], + [0.431640625, 0.345703125], + [0.4296875, 0.34375], + [0.423828125, 0.34375], + [0.421875, 0.341796875], + [0.41796875, 0.341796875], + [0.416015625, 0.33984375], + [0.412109375, 0.33984375], + [0.41015625, 0.337890625], + [0.404296875, 0.337890625], + [0.40234375, 0.3359375], + [0.3984375, 0.3359375], + [0.396484375, 0.333984375], + [0.390625, 0.333984375], + [0.388671875, 0.33203125] + ], + [ + [0.412109375, 0.3671875], + [0.42578125, 0.3671875], + [0.427734375, 0.369140625], + [0.4296875, 0.369140625], + [0.439453125, 0.37890625], + [0.439453125, 0.380859375], + [0.44140625, 0.3828125], + [0.44140625, 0.384765625], + [0.443359375, 0.38671875], + [0.443359375, 0.390625], + [0.4453125, 0.392578125], + [0.4453125, 0.396484375], + [0.447265625, 0.3984375], + [0.447265625, 0.404296875], + [0.44921875, 0.40625], + [0.44921875, 0.412109375], + [0.451171875, 0.4140625], + [0.451171875, 0.419921875], + [0.453125, 0.421875], + [0.453125, 0.431640625], + [0.455078125, 0.43359375], + [0.455078125, 0.4453125], + [0.453125, 0.447265625], + [0.453125, 0.44921875], + [0.447265625, 0.455078125], + [0.443359375, 0.455078125], + [0.44140625, 0.45703125], + [0.43359375, 0.45703125], + [0.431640625, 0.458984375], + [0.412109375, 0.458984375], + [0.41015625, 0.45703125], + [0.408203125, 0.45703125], + [0.40625, 0.455078125], + [0.40625, 0.453125], + [0.404296875, 0.451171875], + [0.404296875, 0.447265625], + [0.40234375, 0.4453125], + [0.40234375, 0.43359375], + [0.400390625, 0.431640625], + [0.400390625, 0.42578125], + [0.3984375, 0.423828125], + [0.3984375, 0.419921875], + [0.396484375, 0.41796875], + [0.396484375, 0.412109375], + [0.39453125, 0.41015625], + [0.39453125, 0.384765625], + [0.396484375, 0.3828125], + [0.396484375, 0.380859375], + [0.40625, 0.37109375], + [0.408203125, 0.37109375] + ] + ], + "8": [ + [ + [0.73046875, 0.494140625], + [0.728515625, 0.49609375], + [0.7265625, 0.49609375], + [0.7265625, 0.498046875], + [0.724609375, 0.5], + [0.724609375, 0.509765625], + [0.7265625, 0.51171875], + [0.7265625, 0.513671875], + [0.73046875, 0.517578125], + [0.732421875, 0.517578125], + [0.734375, 0.51953125], + [0.740234375, 0.51953125], + [0.74609375, 0.513671875], + [0.74609375, 0.51171875], + [0.748046875, 0.509765625], + [0.748046875, 0.501953125], + [0.74609375, 0.5], + [0.74609375, 0.498046875], + [0.744140625, 0.49609375], + [0.7421875, 0.49609375], + [0.740234375, 0.494140625] + ], + [ + [0.44140625, 0.240234375], + [0.439453125, 0.2421875], + [0.4375, 0.2421875], + [0.43359375, 0.24609375], + [0.43359375, 0.248046875], + [0.431640625, 0.25], + [0.431640625, 0.263671875], + [0.43359375, 0.265625], + [0.43359375, 0.267578125], + [0.4375, 0.271484375], + [0.4375, 0.2734375], + [0.439453125, 0.275390625], + [0.44140625, 0.275390625], + [0.4453125, 0.279296875], + [0.447265625, 0.279296875], + [0.44921875, 0.28125], + [0.451171875, 0.28125], + [0.45703125, 0.287109375], + [0.45703125, 0.2890625], + [0.458984375, 0.291015625], + [0.45703125, 0.29296875], + [0.45703125, 0.302734375], + [0.455078125, 0.3046875], + [0.455078125, 0.30859375], + [0.453125, 0.310546875], + [0.453125, 0.314453125], + [0.451171875, 0.31640625], + [0.451171875, 0.318359375], + [0.44921875, 0.3203125], + [0.44921875, 0.322265625], + [0.447265625, 0.32421875], + [0.447265625, 0.326171875], + [0.431640625, 0.341796875], + [0.4296875, 0.341796875], + [0.42578125, 0.345703125], + [0.423828125, 0.345703125], + [0.419921875, 0.349609375], + [0.41796875, 0.349609375], + [0.4140625, 0.353515625], + [0.412109375, 0.353515625], + [0.41015625, 0.35546875], + [0.408203125, 0.35546875], + [0.404296875, 0.359375], + [0.40234375, 0.359375], + [0.3984375, 0.36328125], + [0.396484375, 0.36328125], + [0.37890625, 0.380859375], + [0.37890625, 0.3828125], + [0.376953125, 0.384765625], + [0.376953125, 0.38671875], + [0.375, 0.388671875], + [0.375, 0.392578125], + [0.373046875, 0.39453125], + [0.373046875, 0.412109375], + [0.37890625, 0.41796875], + [0.380859375, 0.41796875], + [0.3828125, 0.419921875], + [0.384765625, 0.419921875], + [0.38671875, 0.421875], + [0.390625, 0.421875], + [0.392578125, 0.423828125], + [0.39453125, 0.423828125], + [0.3984375, 0.427734375], + [0.400390625, 0.427734375], + [0.404296875, 0.431640625], + [0.404296875, 0.43359375], + [0.40625, 0.435546875], + [0.40625, 0.4453125], + [0.404296875, 0.447265625], + [0.404296875, 0.44921875], + [0.392578125, 0.4609375], + [0.388671875, 0.4609375], + [0.38671875, 0.462890625], + [0.384765625, 0.4609375], + [0.380859375, 0.4609375], + [0.375, 0.455078125], + [0.373046875, 0.455078125], + [0.37109375, 0.453125], + [0.361328125, 0.453125], + [0.35546875, 0.458984375], + [0.35546875, 0.4609375], + [0.353515625, 0.462890625], + [0.353515625, 0.46484375], + [0.3515625, 0.466796875], + [0.3515625, 0.47265625], + [0.349609375, 0.474609375], + [0.349609375, 0.484375], + [0.34765625, 0.486328125], + [0.34765625, 0.509765625], + [0.345703125, 0.51171875], + [0.345703125, 0.517578125], + [0.34375, 0.51953125], + [0.34375, 0.5234375], + [0.341796875, 0.525390625], + [0.341796875, 0.529296875], + [0.33984375, 0.53125], + [0.33984375, 0.533203125], + [0.337890625, 0.53515625], + [0.337890625, 0.537109375], + [0.3359375, 0.5390625], + [0.3359375, 0.541015625], + [0.333984375, 0.54296875], + [0.333984375, 0.544921875], + [0.32421875, 0.5546875], + [0.322265625, 0.5546875], + [0.3203125, 0.556640625], + [0.318359375, 0.556640625], + [0.31640625, 0.55859375], + [0.3125, 0.55859375], + [0.310546875, 0.556640625], + [0.310546875, 0.537109375], + [0.3046875, 0.537109375], + [0.30078125, 0.541015625], + [0.298828125, 0.541015625], + [0.294921875, 0.544921875], + [0.294921875, 0.546875], + [0.291015625, 0.55078125], + [0.291015625, 0.552734375], + [0.2890625, 0.5546875], + [0.2890625, 0.556640625], + [0.287109375, 0.55859375], + [0.287109375, 0.5625], + [0.28515625, 0.564453125], + [0.28515625, 0.568359375], + [0.283203125, 0.5703125], + [0.283203125, 0.580078125], + [0.28125, 0.58203125], + [0.28125, 0.59765625], + [0.283203125, 0.599609375], + [0.283203125, 0.6015625], + [0.28515625, 0.603515625], + [0.28515625, 0.60546875], + [0.287109375, 0.607421875], + [0.2890625, 0.607421875], + [0.291015625, 0.609375], + [0.30078125, 0.609375], + [0.302734375, 0.607421875], + [0.306640625, 0.607421875], + [0.30859375, 0.60546875], + [0.310546875, 0.60546875], + [0.3125, 0.603515625], + [0.314453125, 0.603515625], + [0.31640625, 0.6015625], + [0.328125, 0.6015625], + [0.330078125, 0.599609375], + [0.361328125, 0.599609375], + [0.36328125, 0.6015625], + [0.37109375, 0.6015625], + [0.373046875, 0.603515625], + [0.37890625, 0.603515625], + [0.380859375, 0.60546875], + [0.4140625, 0.60546875], + [0.416015625, 0.603515625], + [0.41796875, 0.603515625], + [0.419921875, 0.6015625], + [0.421875, 0.6015625], + [0.423828125, 0.599609375], + [0.42578125, 0.599609375], + [0.427734375, 0.59765625], + [0.439453125, 0.59765625], + [0.44140625, 0.599609375], + [0.443359375, 0.599609375], + [0.447265625, 0.603515625], + [0.44921875, 0.603515625], + [0.453125, 0.607421875], + [0.455078125, 0.607421875], + [0.458984375, 0.611328125], + [0.4609375, 0.611328125], + [0.462890625, 0.61328125], + [0.46484375, 0.61328125], + [0.466796875, 0.615234375], + [0.46875, 0.615234375], + [0.470703125, 0.6171875], + [0.47265625, 0.6171875], + [0.474609375, 0.619140625], + [0.494140625, 0.619140625], + [0.49609375, 0.6171875], + [0.5, 0.6171875], + [0.50390625, 0.61328125], + [0.505859375, 0.61328125], + [0.509765625, 0.609375], + [0.509765625, 0.607421875], + [0.513671875, 0.603515625], + [0.513671875, 0.6015625], + [0.515625, 0.599609375], + [0.515625, 0.59765625], + [0.517578125, 0.595703125], + [0.517578125, 0.59375], + [0.51953125, 0.591796875], + [0.51953125, 0.58984375], + [0.521484375, 0.587890625], + [0.521484375, 0.5859375], + [0.53515625, 0.572265625], + [0.537109375, 0.572265625], + [0.5390625, 0.5703125], + [0.541015625, 0.5703125], + [0.54296875, 0.568359375], + [0.544921875, 0.568359375], + [0.546875, 0.56640625], + [0.55078125, 0.56640625], + [0.552734375, 0.564453125], + [0.55859375, 0.564453125], + [0.560546875, 0.5625], + [0.564453125, 0.5625], + [0.56640625, 0.560546875], + [0.5703125, 0.560546875], + [0.572265625, 0.55859375], + [0.576171875, 0.55859375], + [0.578125, 0.556640625], + [0.580078125, 0.556640625], + [0.58203125, 0.5546875], + [0.583984375, 0.5546875], + [0.5859375, 0.552734375], + [0.587890625, 0.552734375], + [0.58984375, 0.55078125], + [0.591796875, 0.55078125], + [0.59375, 0.548828125], + [0.595703125, 0.548828125], + [0.59765625, 0.546875], + [0.599609375, 0.546875], + [0.603515625, 0.54296875], + [0.60546875, 0.54296875], + [0.609375, 0.5390625], + [0.611328125, 0.5390625], + [0.61328125, 0.537109375], + [0.615234375, 0.537109375], + [0.619140625, 0.533203125], + [0.62109375, 0.533203125], + [0.625, 0.529296875], + [0.626953125, 0.529296875], + [0.62890625, 0.52734375], + [0.630859375, 0.52734375], + [0.6328125, 0.525390625], + [0.63671875, 0.525390625], + [0.638671875, 0.5234375], + [0.646484375, 0.5234375], + [0.6484375, 0.521484375], + [0.662109375, 0.521484375], + [0.6640625, 0.51953125], + [0.67578125, 0.51953125], + [0.677734375, 0.517578125], + [0.6796875, 0.517578125], + [0.681640625, 0.515625], + [0.68359375, 0.515625], + [0.6953125, 0.50390625], + [0.6953125, 0.501953125], + [0.697265625, 0.5], + [0.697265625, 0.498046875], + [0.69921875, 0.49609375], + [0.69921875, 0.494140625], + [0.701171875, 0.4921875], + [0.701171875, 0.490234375], + [0.703125, 0.48828125], + [0.703125, 0.486328125], + [0.705078125, 0.484375], + [0.705078125, 0.48046875], + [0.70703125, 0.478515625], + [0.70703125, 0.474609375], + [0.705078125, 0.474609375], + [0.703125, 0.47265625], + [0.67578125, 0.47265625], + [0.673828125, 0.470703125], + [0.671875, 0.470703125], + [0.669921875, 0.46875], + [0.66796875, 0.46875], + [0.6640625, 0.46484375], + [0.662109375, 0.46484375], + [0.658203125, 0.4609375], + [0.65625, 0.4609375], + [0.65234375, 0.45703125], + [0.650390625, 0.45703125], + [0.6484375, 0.455078125], + [0.646484375, 0.455078125], + [0.64453125, 0.453125], + [0.642578125, 0.453125], + [0.640625, 0.451171875], + [0.63671875, 0.451171875], + [0.634765625, 0.44921875], + [0.626953125, 0.44921875], + [0.625, 0.451171875], + [0.62109375, 0.451171875], + [0.619140625, 0.453125], + [0.6171875, 0.453125], + [0.61328125, 0.45703125], + [0.611328125, 0.45703125], + [0.607421875, 0.4609375], + [0.60546875, 0.4609375], + [0.599609375, 0.466796875], + [0.59765625, 0.466796875], + [0.587890625, 0.4765625], + [0.5859375, 0.4765625], + [0.578125, 0.484375], + [0.576171875, 0.484375], + [0.572265625, 0.48828125], + [0.5703125, 0.48828125], + [0.56640625, 0.4921875], + [0.564453125, 0.4921875], + [0.5625, 0.494140625], + [0.560546875, 0.494140625], + [0.55859375, 0.49609375], + [0.556640625, 0.49609375], + [0.5546875, 0.498046875], + [0.55078125, 0.498046875], + [0.548828125, 0.5], + [0.54296875, 0.5], + [0.541015625, 0.501953125], + [0.529296875, 0.501953125], + [0.52734375, 0.50390625], + [0.5, 0.50390625], + [0.498046875, 0.501953125], + [0.484375, 0.501953125], + [0.482421875, 0.5], + [0.478515625, 0.5], + [0.46875, 0.490234375], + [0.46875, 0.48828125], + [0.466796875, 0.486328125], + [0.466796875, 0.4765625], + [0.46875, 0.474609375], + [0.46875, 0.470703125], + [0.470703125, 0.46875], + [0.470703125, 0.466796875], + [0.47265625, 0.46484375], + [0.47265625, 0.462890625], + [0.474609375, 0.4609375], + [0.474609375, 0.458984375], + [0.4765625, 0.45703125], + [0.4765625, 0.451171875], + [0.478515625, 0.44921875], + [0.478515625, 0.4375], + [0.4765625, 0.435546875], + [0.4765625, 0.4296875], + [0.474609375, 0.427734375], + [0.474609375, 0.42578125], + [0.47265625, 0.423828125], + [0.47265625, 0.421875], + [0.470703125, 0.419921875], + [0.470703125, 0.41796875], + [0.46875, 0.416015625], + [0.46875, 0.4140625], + [0.46484375, 0.41015625], + [0.46484375, 0.408203125], + [0.4609375, 0.404296875], + [0.4609375, 0.40234375], + [0.458984375, 0.400390625], + [0.458984375, 0.3984375], + [0.45703125, 0.396484375], + [0.45703125, 0.39453125], + [0.455078125, 0.392578125], + [0.455078125, 0.390625], + [0.453125, 0.388671875], + [0.453125, 0.3828125], + [0.451171875, 0.380859375], + [0.451171875, 0.365234375], + [0.453125, 0.36328125], + [0.453125, 0.359375], + [0.46484375, 0.34765625], + [0.466796875, 0.34765625], + [0.47265625, 0.341796875], + [0.474609375, 0.341796875], + [0.48046875, 0.3359375], + [0.48046875, 0.333984375], + [0.482421875, 0.33203125], + [0.482421875, 0.330078125], + [0.484375, 0.328125], + [0.484375, 0.32421875], + [0.486328125, 0.322265625], + [0.486328125, 0.31640625], + [0.48828125, 0.314453125], + [0.48828125, 0.3046875], + [0.490234375, 0.302734375], + [0.490234375, 0.296875], + [0.4921875, 0.294921875], + [0.4921875, 0.291015625], + [0.505859375, 0.27734375], + [0.5078125, 0.27734375], + [0.509765625, 0.275390625], + [0.51171875, 0.275390625], + [0.513671875, 0.2734375], + [0.515625, 0.2734375], + [0.517578125, 0.271484375], + [0.521484375, 0.271484375], + [0.5234375, 0.26953125], + [0.52734375, 0.26953125], + [0.529296875, 0.267578125], + [0.53125, 0.267578125], + [0.537109375, 0.26171875], + [0.537109375, 0.259765625], + [0.5390625, 0.2578125], + [0.5390625, 0.25390625], + [0.541015625, 0.251953125], + [0.541015625, 0.248046875], + [0.537109375, 0.244140625], + [0.533203125, 0.244140625], + [0.53125, 0.2421875], + [0.51953125, 0.2421875], + [0.517578125, 0.244140625], + [0.51171875, 0.244140625], + [0.509765625, 0.24609375], + [0.50390625, 0.24609375], + [0.501953125, 0.248046875], + [0.494140625, 0.248046875], + [0.4921875, 0.25], + [0.478515625, 0.25], + [0.4765625, 0.248046875], + [0.46875, 0.248046875], + [0.466796875, 0.24609375], + [0.462890625, 0.24609375], + [0.4609375, 0.244140625], + [0.458984375, 0.244140625], + [0.45703125, 0.2421875], + [0.453125, 0.2421875], + [0.451171875, 0.240234375] + ] + ], + "9": [ + [ + [0.470703125, 0.6640625], + [0.46875, 0.666015625], + [0.46875, 0.67578125], + [0.474609375, 0.681640625], + [0.478515625, 0.681640625], + [0.48046875, 0.6796875], + [0.48046875, 0.677734375], + [0.482421875, 0.67578125], + [0.482421875, 0.66796875], + [0.478515625, 0.6640625] + ], + [ + [0.341796875, 0.580078125], + [0.33984375, 0.58203125], + [0.337890625, 0.58203125], + [0.333984375, 0.5859375], + [0.333984375, 0.587890625], + [0.33203125, 0.58984375], + [0.33203125, 0.591796875], + [0.330078125, 0.59375], + [0.330078125, 0.607421875], + [0.33203125, 0.609375], + [0.33203125, 0.611328125], + [0.333984375, 0.61328125], + [0.333984375, 0.615234375], + [0.3359375, 0.6171875], + [0.3359375, 0.619140625], + [0.337890625, 0.62109375], + [0.337890625, 0.625], + [0.33984375, 0.626953125], + [0.33984375, 0.6328125], + [0.341796875, 0.634765625], + [0.341796875, 0.63671875], + [0.34375, 0.638671875], + [0.34375, 0.640625], + [0.345703125, 0.642578125], + [0.345703125, 0.64453125], + [0.34765625, 0.646484375], + [0.34765625, 0.6484375], + [0.349609375, 0.650390625], + [0.349609375, 0.65234375], + [0.3515625, 0.654296875], + [0.3515625, 0.65625], + [0.353515625, 0.658203125], + [0.353515625, 0.66015625], + [0.35546875, 0.662109375], + [0.35546875, 0.6640625], + [0.357421875, 0.666015625], + [0.357421875, 0.66796875], + [0.3671875, 0.677734375], + [0.369140625, 0.677734375], + [0.37109375, 0.6796875], + [0.373046875, 0.6796875], + [0.375, 0.681640625], + [0.37890625, 0.681640625], + [0.380859375, 0.68359375], + [0.390625, 0.68359375], + [0.392578125, 0.685546875], + [0.396484375, 0.685546875], + [0.3984375, 0.6875], + [0.40234375, 0.6875], + [0.404296875, 0.689453125], + [0.40625, 0.689453125], + [0.408203125, 0.69140625], + [0.41015625, 0.69140625], + [0.412109375, 0.693359375], + [0.416015625, 0.693359375], + [0.41796875, 0.6953125], + [0.439453125, 0.6953125], + [0.44140625, 0.693359375], + [0.443359375, 0.693359375], + [0.4453125, 0.69140625], + [0.4453125, 0.689453125], + [0.447265625, 0.6875], + [0.447265625, 0.677734375], + [0.43359375, 0.6640625], + [0.43359375, 0.662109375], + [0.431640625, 0.66015625], + [0.431640625, 0.626953125], + [0.42578125, 0.62109375], + [0.423828125, 0.62109375], + [0.421875, 0.619140625], + [0.416015625, 0.619140625], + [0.4140625, 0.62109375], + [0.41015625, 0.62109375], + [0.40625, 0.625], + [0.404296875, 0.625], + [0.40234375, 0.626953125], + [0.400390625, 0.626953125], + [0.3984375, 0.625], + [0.3984375, 0.6171875], + [0.396484375, 0.6171875], + [0.39453125, 0.615234375], + [0.380859375, 0.615234375], + [0.376953125, 0.611328125], + [0.376953125, 0.6015625], + [0.375, 0.599609375], + [0.375, 0.595703125], + [0.37109375, 0.591796875], + [0.37109375, 0.58984375], + [0.3671875, 0.5859375], + [0.365234375, 0.5859375], + [0.361328125, 0.58203125], + [0.357421875, 0.58203125], + [0.35546875, 0.580078125] + ], + [ + [0.515625, 0.43359375], + [0.513671875, 0.435546875], + [0.5078125, 0.435546875], + [0.505859375, 0.4375], + [0.501953125, 0.4375], + [0.5, 0.439453125], + [0.49609375, 0.439453125], + [0.494140625, 0.44140625], + [0.4921875, 0.44140625], + [0.490234375, 0.443359375], + [0.48828125, 0.443359375], + [0.486328125, 0.4453125], + [0.484375, 0.4453125], + [0.482421875, 0.447265625], + [0.48046875, 0.447265625], + [0.478515625, 0.44921875], + [0.4765625, 0.44921875], + [0.47265625, 0.453125], + [0.470703125, 0.453125], + [0.466796875, 0.45703125], + [0.46484375, 0.45703125], + [0.458984375, 0.462890625], + [0.45703125, 0.462890625], + [0.447265625, 0.47265625], + [0.447265625, 0.474609375], + [0.4453125, 0.4765625], + [0.4453125, 0.478515625], + [0.443359375, 0.48046875], + [0.443359375, 0.484375], + [0.44140625, 0.486328125], + [0.44140625, 0.4921875], + [0.439453125, 0.494140625], + [0.439453125, 0.51171875], + [0.44140625, 0.513671875], + [0.44140625, 0.517578125], + [0.447265625, 0.5234375], + [0.44921875, 0.5234375], + [0.451171875, 0.525390625], + [0.462890625, 0.525390625], + [0.46484375, 0.5234375], + [0.47265625, 0.5234375], + [0.474609375, 0.521484375], + [0.484375, 0.521484375], + [0.486328125, 0.51953125], + [0.49609375, 0.51953125], + [0.498046875, 0.517578125], + [0.509765625, 0.517578125], + [0.51171875, 0.515625], + [0.541015625, 0.515625], + [0.54296875, 0.513671875], + [0.55078125, 0.513671875], + [0.552734375, 0.51171875], + [0.560546875, 0.51171875], + [0.5625, 0.509765625], + [0.568359375, 0.509765625], + [0.5703125, 0.5078125], + [0.57421875, 0.5078125], + [0.576171875, 0.505859375], + [0.578125, 0.505859375], + [0.580078125, 0.50390625], + [0.58203125, 0.50390625], + [0.583984375, 0.501953125], + [0.5859375, 0.501953125], + [0.59375, 0.494140625], + [0.59375, 0.4921875], + [0.59765625, 0.48828125], + [0.59765625, 0.486328125], + [0.599609375, 0.484375], + [0.599609375, 0.48046875], + [0.6015625, 0.478515625], + [0.6015625, 0.470703125], + [0.603515625, 0.46875], + [0.603515625, 0.458984375], + [0.6015625, 0.45703125], + [0.6015625, 0.451171875], + [0.599609375, 0.44921875], + [0.599609375, 0.447265625], + [0.59765625, 0.4453125], + [0.59765625, 0.443359375], + [0.59375, 0.439453125], + [0.591796875, 0.439453125], + [0.58984375, 0.4375], + [0.587890625, 0.4375], + [0.5859375, 0.435546875], + [0.5625, 0.435546875], + [0.560546875, 0.43359375] + ], + [ + [0.302734375, 0.41796875], + [0.30078125, 0.419921875], + [0.30078125, 0.423828125], + [0.298828125, 0.42578125], + [0.298828125, 0.43359375], + [0.296875, 0.435546875], + [0.296875, 0.46484375], + [0.294921875, 0.466796875], + [0.294921875, 0.46875], + [0.29296875, 0.470703125], + [0.29296875, 0.47265625], + [0.291015625, 0.474609375], + [0.291015625, 0.4765625], + [0.287109375, 0.48046875], + [0.287109375, 0.482421875], + [0.28515625, 0.484375], + [0.28515625, 0.486328125], + [0.283203125, 0.48828125], + [0.283203125, 0.490234375], + [0.28125, 0.4921875], + [0.28125, 0.494140625], + [0.279296875, 0.49609375], + [0.279296875, 0.5], + [0.27734375, 0.501953125], + [0.27734375, 0.505859375], + [0.275390625, 0.5078125], + [0.275390625, 0.513671875], + [0.2734375, 0.515625], + [0.2734375, 0.51953125], + [0.275390625, 0.521484375], + [0.275390625, 0.5234375], + [0.279296875, 0.52734375], + [0.283203125, 0.52734375], + [0.28515625, 0.529296875], + [0.287109375, 0.529296875], + [0.2890625, 0.53125], + [0.291015625, 0.53125], + [0.30078125, 0.541015625], + [0.30078125, 0.54296875], + [0.30859375, 0.55078125], + [0.30859375, 0.552734375], + [0.31640625, 0.560546875], + [0.318359375, 0.560546875], + [0.3203125, 0.5625], + [0.322265625, 0.5625], + [0.32421875, 0.564453125], + [0.326171875, 0.564453125], + [0.328125, 0.56640625], + [0.34375, 0.56640625], + [0.349609375, 0.560546875], + [0.349609375, 0.556640625], + [0.3515625, 0.5546875], + [0.3515625, 0.546875], + [0.349609375, 0.544921875], + [0.3515625, 0.54296875], + [0.3515625, 0.533203125], + [0.35546875, 0.529296875], + [0.35546875, 0.52734375], + [0.359375, 0.5234375], + [0.361328125, 0.5234375], + [0.365234375, 0.51953125], + [0.3671875, 0.51953125], + [0.369140625, 0.517578125], + [0.375, 0.517578125], + [0.376953125, 0.515625], + [0.3984375, 0.515625], + [0.40234375, 0.51171875], + [0.40234375, 0.501953125], + [0.400390625, 0.5], + [0.400390625, 0.498046875], + [0.380859375, 0.478515625], + [0.37890625, 0.478515625], + [0.3671875, 0.466796875], + [0.3671875, 0.46484375], + [0.36328125, 0.4609375], + [0.36328125, 0.458984375], + [0.361328125, 0.45703125], + [0.361328125, 0.455078125], + [0.35546875, 0.44921875], + [0.333984375, 0.44921875], + [0.33203125, 0.447265625], + [0.326171875, 0.447265625], + [0.32421875, 0.4453125], + [0.322265625, 0.4453125], + [0.314453125, 0.4375], + [0.314453125, 0.435546875], + [0.3125, 0.43359375], + [0.3125, 0.431640625], + [0.310546875, 0.4296875], + [0.310546875, 0.42578125], + [0.30859375, 0.423828125], + [0.30859375, 0.421875], + [0.306640625, 0.419921875], + [0.3046875, 0.419921875] + ] + ], + "10": [ + [ + [0.56640625, 0.390625], + [0.564453125, 0.392578125], + [0.5625, 0.392578125], + [0.560546875, 0.39453125], + [0.55859375, 0.39453125], + [0.5546875, 0.3984375], + [0.552734375, 0.3984375], + [0.55078125, 0.400390625], + [0.548828125, 0.400390625], + [0.546875, 0.40234375], + [0.544921875, 0.40234375], + [0.54296875, 0.404296875], + [0.52734375, 0.404296875], + [0.525390625, 0.40234375], + [0.5234375, 0.40234375], + [0.521484375, 0.400390625], + [0.51953125, 0.400390625], + [0.517578125, 0.3984375], + [0.515625, 0.3984375], + [0.513671875, 0.396484375], + [0.51171875, 0.396484375], + [0.509765625, 0.39453125], + [0.48828125, 0.39453125], + [0.486328125, 0.396484375], + [0.484375, 0.396484375], + [0.482421875, 0.3984375], + [0.48046875, 0.3984375], + [0.48046875, 0.400390625], + [0.478515625, 0.40234375], + [0.478515625, 0.404296875], + [0.4765625, 0.40625], + [0.4765625, 0.443359375], + [0.474609375, 0.4453125], + [0.474609375, 0.447265625], + [0.47265625, 0.44921875], + [0.47265625, 0.451171875], + [0.466796875, 0.45703125], + [0.45703125, 0.45703125], + [0.453125, 0.453125], + [0.451171875, 0.453125], + [0.447265625, 0.44921875], + [0.4453125, 0.44921875], + [0.443359375, 0.447265625], + [0.431640625, 0.447265625], + [0.4296875, 0.44921875], + [0.421875, 0.44921875], + [0.419921875, 0.451171875], + [0.412109375, 0.451171875], + [0.41015625, 0.44921875], + [0.40234375, 0.44921875], + [0.400390625, 0.447265625], + [0.3984375, 0.447265625], + [0.396484375, 0.4453125], + [0.39453125, 0.4453125], + [0.392578125, 0.443359375], + [0.390625, 0.443359375], + [0.388671875, 0.44140625], + [0.38671875, 0.44140625], + [0.384765625, 0.439453125], + [0.3828125, 0.439453125], + [0.380859375, 0.4375], + [0.357421875, 0.4375], + [0.357421875, 0.439453125], + [0.36328125, 0.4453125], + [0.36328125, 0.447265625], + [0.375, 0.458984375], + [0.375, 0.4609375], + [0.376953125, 0.462890625], + [0.376953125, 0.46484375], + [0.375, 0.466796875], + [0.359375, 0.466796875], + [0.357421875, 0.46875], + [0.35546875, 0.46875], + [0.349609375, 0.474609375], + [0.349609375, 0.4765625], + [0.34765625, 0.478515625], + [0.34765625, 0.482421875], + [0.345703125, 0.484375], + [0.345703125, 0.53515625], + [0.34765625, 0.537109375], + [0.34765625, 0.541015625], + [0.349609375, 0.54296875], + [0.349609375, 0.544921875], + [0.3515625, 0.546875], + [0.353515625, 0.546875], + [0.35546875, 0.548828125], + [0.357421875, 0.548828125], + [0.359375, 0.546875], + [0.361328125, 0.546875], + [0.37109375, 0.537109375], + [0.376953125, 0.537109375], + [0.380859375, 0.541015625], + [0.3828125, 0.541015625], + [0.38671875, 0.544921875], + [0.388671875, 0.544921875], + [0.390625, 0.546875], + [0.4140625, 0.546875], + [0.416015625, 0.548828125], + [0.419921875, 0.548828125], + [0.423828125, 0.552734375], + [0.423828125, 0.560546875], + [0.421875, 0.5625], + [0.421875, 0.564453125], + [0.419921875, 0.56640625], + [0.419921875, 0.5703125], + [0.41796875, 0.572265625], + [0.41796875, 0.583984375], + [0.419921875, 0.5859375], + [0.421875, 0.5859375], + [0.423828125, 0.587890625], + [0.42578125, 0.587890625], + [0.427734375, 0.58984375], + [0.4375, 0.58984375], + [0.439453125, 0.591796875], + [0.443359375, 0.591796875], + [0.4453125, 0.59375], + [0.447265625, 0.59375], + [0.453125, 0.599609375], + [0.455078125, 0.599609375], + [0.4609375, 0.60546875], + [0.462890625, 0.60546875], + [0.466796875, 0.609375], + [0.46875, 0.609375], + [0.470703125, 0.611328125], + [0.474609375, 0.611328125], + [0.4765625, 0.61328125], + [0.478515625, 0.61328125], + [0.48046875, 0.615234375], + [0.486328125, 0.615234375], + [0.48828125, 0.6171875], + [0.494140625, 0.6171875], + [0.49609375, 0.619140625], + [0.5078125, 0.619140625], + [0.509765625, 0.62109375], + [0.56640625, 0.62109375], + [0.568359375, 0.619140625], + [0.572265625, 0.619140625], + [0.57421875, 0.6171875], + [0.578125, 0.6171875], + [0.580078125, 0.615234375], + [0.58203125, 0.615234375], + [0.5859375, 0.611328125], + [0.587890625, 0.611328125], + [0.591796875, 0.607421875], + [0.59375, 0.607421875], + [0.61328125, 0.587890625], + [0.61328125, 0.5859375], + [0.615234375, 0.583984375], + [0.615234375, 0.58203125], + [0.6171875, 0.580078125], + [0.6171875, 0.578125], + [0.619140625, 0.576171875], + [0.619140625, 0.57421875], + [0.62109375, 0.572265625], + [0.62109375, 0.564453125], + [0.623046875, 0.5625], + [0.623046875, 0.5234375], + [0.62109375, 0.521484375], + [0.62109375, 0.498046875], + [0.619140625, 0.49609375], + [0.619140625, 0.470703125], + [0.6171875, 0.46875], + [0.6171875, 0.4609375], + [0.615234375, 0.458984375], + [0.615234375, 0.455078125], + [0.61328125, 0.453125], + [0.61328125, 0.44921875], + [0.611328125, 0.447265625], + [0.611328125, 0.4453125], + [0.607421875, 0.44140625], + [0.607421875, 0.439453125], + [0.599609375, 0.431640625], + [0.59765625, 0.431640625], + [0.59375, 0.427734375], + [0.591796875, 0.427734375], + [0.587890625, 0.423828125], + [0.5859375, 0.423828125], + [0.58203125, 0.419921875], + [0.580078125, 0.419921875], + [0.57421875, 0.4140625], + [0.57421875, 0.412109375], + [0.572265625, 0.41015625], + [0.572265625, 0.40625], + [0.5703125, 0.404296875], + [0.5703125, 0.39453125] + ], + [ + [0.712890625, 0.3125], + [0.7109375, 0.314453125], + [0.708984375, 0.314453125], + [0.708984375, 0.31640625], + [0.70703125, 0.318359375], + [0.70703125, 0.3203125], + [0.705078125, 0.322265625], + [0.705078125, 0.32421875], + [0.70703125, 0.326171875], + [0.70703125, 0.330078125], + [0.7109375, 0.333984375], + [0.7109375, 0.3359375], + [0.71484375, 0.33984375], + [0.716796875, 0.33984375], + [0.720703125, 0.34375], + [0.72265625, 0.34375], + [0.724609375, 0.345703125], + [0.728515625, 0.345703125], + [0.73046875, 0.34765625], + [0.736328125, 0.34765625], + [0.73828125, 0.345703125], + [0.740234375, 0.345703125], + [0.740234375, 0.34375], + [0.7421875, 0.341796875], + [0.7421875, 0.33984375], + [0.740234375, 0.337890625], + [0.740234375, 0.333984375], + [0.73828125, 0.33203125], + [0.73828125, 0.330078125], + [0.734375, 0.326171875], + [0.734375, 0.32421875], + [0.724609375, 0.314453125], + [0.72265625, 0.314453125], + [0.720703125, 0.3125] + ], + [ + [0.619140625, 0.310546875], + [0.6171875, 0.3125], + [0.611328125, 0.3125], + [0.609375, 0.314453125], + [0.60546875, 0.314453125], + [0.603515625, 0.31640625], + [0.599609375, 0.31640625], + [0.59765625, 0.318359375], + [0.595703125, 0.318359375], + [0.59375, 0.3203125], + [0.591796875, 0.3203125], + [0.5859375, 0.326171875], + [0.5859375, 0.3359375], + [0.587890625, 0.337890625], + [0.587890625, 0.33984375], + [0.58984375, 0.341796875], + [0.58984375, 0.345703125], + [0.591796875, 0.34765625], + [0.591796875, 0.38671875], + [0.59375, 0.388671875], + [0.59375, 0.390625], + [0.595703125, 0.392578125], + [0.595703125, 0.39453125], + [0.59765625, 0.39453125], + [0.6015625, 0.3984375], + [0.60546875, 0.3984375], + [0.607421875, 0.400390625], + [0.611328125, 0.400390625], + [0.61328125, 0.40234375], + [0.619140625, 0.40234375], + [0.62109375, 0.404296875], + [0.62890625, 0.404296875], + [0.630859375, 0.40625], + [0.64453125, 0.40625], + [0.646484375, 0.404296875], + [0.6484375, 0.404296875], + [0.6484375, 0.40234375], + [0.650390625, 0.400390625], + [0.650390625, 0.3828125], + [0.65234375, 0.380859375], + [0.65234375, 0.37109375], + [0.654296875, 0.369140625], + [0.654296875, 0.365234375], + [0.65625, 0.36328125], + [0.65625, 0.357421875], + [0.658203125, 0.35546875], + [0.658203125, 0.33984375], + [0.65625, 0.337890625], + [0.65625, 0.3359375], + [0.642578125, 0.322265625], + [0.640625, 0.322265625], + [0.630859375, 0.3125], + [0.62890625, 0.3125], + [0.626953125, 0.310546875] + ] + ], + "11": [ + [ + [0.255859375, 0.494140625], + [0.25390625, 0.49609375], + [0.25390625, 0.498046875], + [0.251953125, 0.5], + [0.251953125, 0.501953125], + [0.25, 0.50390625], + [0.25, 0.505859375], + [0.248046875, 0.5078125], + [0.248046875, 0.51171875], + [0.24609375, 0.513671875], + [0.24609375, 0.521484375], + [0.244140625, 0.5234375], + [0.244140625, 0.546875], + [0.2421875, 0.548828125], + [0.2421875, 0.55078125], + [0.244140625, 0.552734375], + [0.244140625, 0.5703125], + [0.24609375, 0.572265625], + [0.24609375, 0.576171875], + [0.248046875, 0.578125], + [0.248046875, 0.58203125], + [0.25, 0.583984375], + [0.25, 0.5859375], + [0.251953125, 0.587890625], + [0.251953125, 0.591796875], + [0.25390625, 0.59375], + [0.25390625, 0.599609375], + [0.255859375, 0.6015625], + [0.255859375, 0.60546875], + [0.2578125, 0.607421875], + [0.2578125, 0.609375], + [0.259765625, 0.611328125], + [0.259765625, 0.615234375], + [0.263671875, 0.619140625], + [0.265625, 0.6171875], + [0.267578125, 0.6171875], + [0.271484375, 0.61328125], + [0.271484375, 0.611328125], + [0.2734375, 0.609375], + [0.2734375, 0.607421875], + [0.275390625, 0.60546875], + [0.275390625, 0.6015625], + [0.27734375, 0.599609375], + [0.27734375, 0.55078125], + [0.275390625, 0.548828125], + [0.275390625, 0.53515625], + [0.2734375, 0.533203125], + [0.2734375, 0.52734375], + [0.271484375, 0.525390625], + [0.271484375, 0.51953125], + [0.26953125, 0.517578125], + [0.26953125, 0.513671875], + [0.267578125, 0.51171875], + [0.267578125, 0.5078125], + [0.265625, 0.505859375], + [0.265625, 0.50390625], + [0.263671875, 0.501953125], + [0.263671875, 0.5], + [0.259765625, 0.49609375], + [0.259765625, 0.494140625] + ], + [ + [0.546875, 0.267578125], + [0.544921875, 0.26953125], + [0.541015625, 0.26953125], + [0.5390625, 0.271484375], + [0.53515625, 0.271484375], + [0.533203125, 0.2734375], + [0.53125, 0.2734375], + [0.529296875, 0.275390625], + [0.52734375, 0.275390625], + [0.525390625, 0.27734375], + [0.5234375, 0.27734375], + [0.51953125, 0.28125], + [0.517578125, 0.28125], + [0.5078125, 0.291015625], + [0.5078125, 0.29296875], + [0.505859375, 0.294921875], + [0.505859375, 0.296875], + [0.50390625, 0.298828125], + [0.50390625, 0.30078125], + [0.501953125, 0.302734375], + [0.501953125, 0.3203125], + [0.50390625, 0.322265625], + [0.50390625, 0.32421875], + [0.505859375, 0.326171875], + [0.505859375, 0.328125], + [0.51953125, 0.341796875], + [0.521484375, 0.341796875], + [0.552734375, 0.373046875], + [0.552734375, 0.375], + [0.55859375, 0.380859375], + [0.55859375, 0.3828125], + [0.5625, 0.38671875], + [0.5625, 0.388671875], + [0.56640625, 0.392578125], + [0.56640625, 0.39453125], + [0.568359375, 0.396484375], + [0.568359375, 0.3984375], + [0.5703125, 0.400390625], + [0.5703125, 0.40234375], + [0.57421875, 0.40625], + [0.57421875, 0.408203125], + [0.576171875, 0.41015625], + [0.576171875, 0.412109375], + [0.578125, 0.4140625], + [0.578125, 0.416015625], + [0.580078125, 0.41796875], + [0.580078125, 0.419921875], + [0.58203125, 0.421875], + [0.58203125, 0.42578125], + [0.583984375, 0.427734375], + [0.583984375, 0.431640625], + [0.5859375, 0.43359375], + [0.5859375, 0.439453125], + [0.587890625, 0.44140625], + [0.587890625, 0.4453125], + [0.58984375, 0.447265625], + [0.58984375, 0.451171875], + [0.591796875, 0.453125], + [0.591796875, 0.45703125], + [0.59375, 0.458984375], + [0.59375, 0.462890625], + [0.595703125, 0.46484375], + [0.595703125, 0.466796875], + [0.59765625, 0.46875], + [0.59765625, 0.47265625], + [0.599609375, 0.474609375], + [0.599609375, 0.478515625], + [0.6015625, 0.48046875], + [0.6015625, 0.484375], + [0.603515625, 0.486328125], + [0.603515625, 0.48828125], + [0.60546875, 0.490234375], + [0.60546875, 0.494140625], + [0.607421875, 0.49609375], + [0.607421875, 0.501953125], + [0.609375, 0.50390625], + [0.609375, 0.51171875], + [0.611328125, 0.513671875], + [0.611328125, 0.5234375], + [0.61328125, 0.525390625], + [0.61328125, 0.546875], + [0.615234375, 0.548828125], + [0.615234375, 0.572265625], + [0.6171875, 0.57421875], + [0.6171875, 0.583984375], + [0.615234375, 0.5859375], + [0.615234375, 0.595703125], + [0.61328125, 0.59765625], + [0.61328125, 0.6015625], + [0.611328125, 0.603515625], + [0.611328125, 0.60546875], + [0.609375, 0.607421875], + [0.609375, 0.611328125], + [0.60546875, 0.615234375], + [0.60546875, 0.6171875], + [0.603515625, 0.619140625], + [0.603515625, 0.62109375], + [0.6015625, 0.623046875], + [0.6015625, 0.625], + [0.59765625, 0.62890625], + [0.59765625, 0.630859375], + [0.59375, 0.634765625], + [0.59375, 0.63671875], + [0.58984375, 0.640625], + [0.58984375, 0.642578125], + [0.587890625, 0.64453125], + [0.587890625, 0.646484375], + [0.5859375, 0.6484375], + [0.5859375, 0.650390625], + [0.583984375, 0.65234375], + [0.583984375, 0.654296875], + [0.58203125, 0.65625], + [0.58203125, 0.658203125], + [0.580078125, 0.66015625], + [0.580078125, 0.6640625], + [0.578125, 0.666015625], + [0.578125, 0.671875], + [0.576171875, 0.673828125], + [0.576171875, 0.6875], + [0.58203125, 0.693359375], + [0.609375, 0.693359375], + [0.611328125, 0.69140625], + [0.615234375, 0.69140625], + [0.6171875, 0.689453125], + [0.619140625, 0.689453125], + [0.625, 0.68359375], + [0.625, 0.681640625], + [0.626953125, 0.6796875], + [0.626953125, 0.677734375], + [0.62890625, 0.67578125], + [0.62890625, 0.671875], + [0.630859375, 0.669921875], + [0.630859375, 0.66796875], + [0.6328125, 0.666015625], + [0.6328125, 0.662109375], + [0.634765625, 0.66015625], + [0.634765625, 0.654296875], + [0.63671875, 0.65234375], + [0.63671875, 0.6484375], + [0.638671875, 0.646484375], + [0.638671875, 0.64453125], + [0.640625, 0.642578125], + [0.640625, 0.638671875], + [0.64453125, 0.634765625], + [0.64453125, 0.6328125], + [0.646484375, 0.630859375], + [0.646484375, 0.62890625], + [0.650390625, 0.625], + [0.650390625, 0.623046875], + [0.654296875, 0.619140625], + [0.654296875, 0.6171875], + [0.658203125, 0.61328125], + [0.658203125, 0.611328125], + [0.6640625, 0.60546875], + [0.6640625, 0.603515625], + [0.669921875, 0.59765625], + [0.669921875, 0.595703125], + [0.673828125, 0.591796875], + [0.673828125, 0.58984375], + [0.6796875, 0.583984375], + [0.6796875, 0.58203125], + [0.681640625, 0.580078125], + [0.681640625, 0.576171875], + [0.68359375, 0.57421875], + [0.68359375, 0.572265625], + [0.685546875, 0.5703125], + [0.685546875, 0.56640625], + [0.6875, 0.564453125], + [0.6875, 0.55859375], + [0.689453125, 0.556640625], + [0.689453125, 0.552734375], + [0.69140625, 0.55078125], + [0.69140625, 0.544921875], + [0.693359375, 0.54296875], + [0.693359375, 0.53515625], + [0.6953125, 0.533203125], + [0.6953125, 0.525390625], + [0.697265625, 0.5234375], + [0.697265625, 0.515625], + [0.69921875, 0.513671875], + [0.69921875, 0.50390625], + [0.701171875, 0.501953125], + [0.701171875, 0.490234375], + [0.703125, 0.48828125], + [0.703125, 0.4765625], + [0.705078125, 0.474609375], + [0.705078125, 0.443359375], + [0.703125, 0.44140625], + [0.703125, 0.4375], + [0.701171875, 0.435546875], + [0.701171875, 0.431640625], + [0.69921875, 0.4296875], + [0.69921875, 0.42578125], + [0.697265625, 0.423828125], + [0.697265625, 0.421875], + [0.6953125, 0.419921875], + [0.6953125, 0.416015625], + [0.693359375, 0.4140625], + [0.693359375, 0.412109375], + [0.69140625, 0.41015625], + [0.69140625, 0.408203125], + [0.689453125, 0.40625], + [0.689453125, 0.404296875], + [0.6875, 0.40234375], + [0.6875, 0.400390625], + [0.68359375, 0.396484375], + [0.68359375, 0.39453125], + [0.681640625, 0.392578125], + [0.681640625, 0.390625], + [0.6796875, 0.388671875], + [0.6796875, 0.38671875], + [0.67578125, 0.3828125], + [0.67578125, 0.380859375], + [0.673828125, 0.37890625], + [0.673828125, 0.376953125], + [0.669921875, 0.373046875], + [0.669921875, 0.37109375], + [0.666015625, 0.3671875], + [0.666015625, 0.365234375], + [0.66015625, 0.359375], + [0.66015625, 0.357421875], + [0.65625, 0.353515625], + [0.65625, 0.3515625], + [0.6484375, 0.34375], + [0.6484375, 0.341796875], + [0.61328125, 0.306640625], + [0.611328125, 0.306640625], + [0.57421875, 0.26953125], + [0.572265625, 0.26953125], + [0.5703125, 0.267578125] + ] + ] + } +} diff --git a/tests/assets/anomaly/segmentation/train.json b/tests/assets/anomaly/segmentation/train.json new file mode 100644 index 00000000000..26c75cf333c --- /dev/null +++ b/tests/assets/anomaly/segmentation/train.json @@ -0,0 +1,63 @@ +{ + "image_path": { + "0": "train/good/06.jpg", + "1": "train/good/32.jpg", + "2": "train/good/00.jpg", + "3": "train/good/33.jpg", + "4": "train/good/21.jpg", + "5": "train/good/03.jpg", + "6": "train/good/07.jpg", + "7": "train/good/14.jpg", + "8": "train/good/17.jpg", + "9": "train/good/02.jpg", + "10": "train/good/30.jpg", + "11": "train/good/26.jpg", + "12": "train/good/19.jpg", + "13": "train/good/16.jpg", + "14": "train/good/01.jpg", + "15": "train/good/15.jpg", + "16": "train/good/31.jpg", + "17": "train/good/27.jpg", + "18": "train/good/29.jpg", + "19": "train/good/10.jpg", + "20": "train/good/20.jpg", + "21": "train/good/12.jpg", + "22": "train/good/11.jpg", + "23": "train/good/24.jpg", + "24": "train/good/09.jpg", + "25": "train/good/08.jpg", + "26": "train/good/22.jpg", + "27": "train/good/18.jpg" + }, + "label": { + "0": "good", + "1": "good", + "2": "good", + "3": "good", + "4": "good", + "5": "good", + "6": "good", + "7": "good", + "8": "good", + "9": "good", + "10": "good", + "11": "good", + "12": "good", + "13": "good", + "14": "good", + "15": "good", + "16": "good", + "17": "good", + "18": "good", + "19": "good", + "20": "good", + "21": "good", + "22": "good", + "23": "good", + "24": "good", + "25": "good", + "26": "good", + "27": "good" + }, + "masks": {} +} diff --git a/tests/assets/anomaly/segmentation/val.json b/tests/assets/anomaly/segmentation/val.json new file mode 100644 index 00000000000..28676231a4f --- /dev/null +++ b/tests/assets/anomaly/segmentation/val.json @@ -0,0 +1,2431 @@ +{ + "image_path": { + "0": "test/good/13.jpg", + "1": "test/good/23.jpg", + "2": "test/good/04.jpg", + "3": "test/colour/13.jpg", + "4": "test/colour/00.jpg", + "5": "test/colour/05.jpg", + "6": "test/colour/02.jpg", + "7": "test/colour/16.jpg", + "8": "test/colour/15.jpg", + "9": "test/colour/10.jpg", + "10": "test/colour/04.jpg" + }, + "label": { + "0": "good", + "1": "good", + "2": "good", + "3": "abnormal", + "4": "abnormal", + "5": "abnormal", + "6": "abnormal", + "7": "abnormal", + "8": "abnormal", + "9": "abnormal", + "10": "abnormal" + }, + "masks": { + "3": [ + [ + [0.41796875, 0.7578125], + [0.416015625, 0.759765625], + [0.404296875, 0.759765625], + [0.400390625, 0.763671875], + [0.400390625, 0.775390625], + [0.40234375, 0.77734375], + [0.40234375, 0.779296875], + [0.41015625, 0.787109375], + [0.412109375, 0.787109375], + [0.4140625, 0.7890625], + [0.416015625, 0.7890625], + [0.41796875, 0.791015625], + [0.421875, 0.791015625], + [0.423828125, 0.79296875], + [0.439453125, 0.79296875], + [0.44140625, 0.791015625], + [0.4453125, 0.791015625], + [0.451171875, 0.78515625], + [0.451171875, 0.783203125], + [0.453125, 0.78125], + [0.453125, 0.765625], + [0.451171875, 0.763671875], + [0.451171875, 0.76171875], + [0.447265625, 0.7578125] + ], + [ + [0.447265625, 0.701171875], + [0.4453125, 0.703125], + [0.4375, 0.703125], + [0.435546875, 0.705078125], + [0.43359375, 0.705078125], + [0.431640625, 0.70703125], + [0.4296875, 0.70703125], + [0.4296875, 0.708984375], + [0.427734375, 0.7109375], + [0.427734375, 0.712890625], + [0.42578125, 0.71484375], + [0.42578125, 0.71875], + [0.423828125, 0.720703125], + [0.423828125, 0.73046875], + [0.42578125, 0.732421875], + [0.4765625, 0.732421875], + [0.482421875, 0.7265625], + [0.482421875, 0.71484375], + [0.48046875, 0.712890625], + [0.48046875, 0.7109375], + [0.478515625, 0.708984375], + [0.4765625, 0.708984375], + [0.474609375, 0.70703125], + [0.47265625, 0.70703125], + [0.470703125, 0.705078125], + [0.466796875, 0.705078125], + [0.46484375, 0.703125], + [0.455078125, 0.703125], + [0.453125, 0.701171875] + ], + [ + [0.47265625, 0.458984375], + [0.470703125, 0.4609375], + [0.462890625, 0.4609375], + [0.4609375, 0.462890625], + [0.45703125, 0.462890625], + [0.455078125, 0.46484375], + [0.453125, 0.46484375], + [0.451171875, 0.466796875], + [0.44921875, 0.466796875], + [0.447265625, 0.46875], + [0.4453125, 0.46875], + [0.443359375, 0.470703125], + [0.44140625, 0.470703125], + [0.4296875, 0.482421875], + [0.4296875, 0.484375], + [0.427734375, 0.486328125], + [0.427734375, 0.48828125], + [0.42578125, 0.490234375], + [0.42578125, 0.4921875], + [0.423828125, 0.494140625], + [0.423828125, 0.498046875], + [0.421875, 0.5], + [0.421875, 0.50390625], + [0.419921875, 0.505859375], + [0.419921875, 0.51171875], + [0.41796875, 0.513671875], + [0.41796875, 0.546875], + [0.419921875, 0.548828125], + [0.419921875, 0.556640625], + [0.421875, 0.55859375], + [0.421875, 0.560546875], + [0.423828125, 0.5625], + [0.423828125, 0.564453125], + [0.4296875, 0.5703125], + [0.435546875, 0.5703125], + [0.4375, 0.568359375], + [0.44140625, 0.568359375], + [0.443359375, 0.56640625], + [0.44921875, 0.56640625], + [0.451171875, 0.564453125], + [0.455078125, 0.564453125], + [0.45703125, 0.5625], + [0.458984375, 0.5625], + [0.4609375, 0.560546875], + [0.462890625, 0.560546875], + [0.46484375, 0.55859375], + [0.466796875, 0.55859375], + [0.470703125, 0.5546875], + [0.47265625, 0.5546875], + [0.474609375, 0.552734375], + [0.4765625, 0.552734375], + [0.478515625, 0.55078125], + [0.494140625, 0.55078125], + [0.498046875, 0.5546875], + [0.5, 0.5546875], + [0.501953125, 0.556640625], + [0.501953125, 0.55859375], + [0.505859375, 0.5625], + [0.505859375, 0.5703125], + [0.50390625, 0.572265625], + [0.50390625, 0.576171875], + [0.501953125, 0.578125], + [0.501953125, 0.580078125], + [0.5, 0.58203125], + [0.5, 0.5859375], + [0.498046875, 0.587890625], + [0.498046875, 0.591796875], + [0.49609375, 0.59375], + [0.49609375, 0.59765625], + [0.494140625, 0.599609375], + [0.494140625, 0.603515625], + [0.4921875, 0.60546875], + [0.4921875, 0.62109375], + [0.494140625, 0.623046875], + [0.494140625, 0.626953125], + [0.49609375, 0.62890625], + [0.49609375, 0.630859375], + [0.501953125, 0.63671875], + [0.50390625, 0.63671875], + [0.505859375, 0.638671875], + [0.513671875, 0.638671875], + [0.515625, 0.63671875], + [0.521484375, 0.63671875], + [0.5234375, 0.634765625], + [0.52734375, 0.634765625], + [0.529296875, 0.6328125], + [0.533203125, 0.6328125], + [0.53515625, 0.630859375], + [0.5390625, 0.630859375], + [0.541015625, 0.62890625], + [0.54296875, 0.62890625], + [0.544921875, 0.626953125], + [0.546875, 0.626953125], + [0.55078125, 0.623046875], + [0.55078125, 0.62109375], + [0.552734375, 0.619140625], + [0.552734375, 0.6171875], + [0.5546875, 0.615234375], + [0.5546875, 0.61328125], + [0.55859375, 0.609375], + [0.55859375, 0.607421875], + [0.5703125, 0.595703125], + [0.572265625, 0.595703125], + [0.576171875, 0.591796875], + [0.578125, 0.591796875], + [0.580078125, 0.58984375], + [0.583984375, 0.58984375], + [0.5859375, 0.587890625], + [0.587890625, 0.587890625], + [0.58984375, 0.5859375], + [0.59375, 0.5859375], + [0.595703125, 0.583984375], + [0.599609375, 0.583984375], + [0.6015625, 0.58203125], + [0.60546875, 0.58203125], + [0.607421875, 0.580078125], + [0.611328125, 0.580078125], + [0.61328125, 0.578125], + [0.615234375, 0.578125], + [0.6171875, 0.576171875], + [0.619140625, 0.576171875], + [0.626953125, 0.568359375], + [0.626953125, 0.564453125], + [0.62890625, 0.5625], + [0.62890625, 0.548828125], + [0.626953125, 0.546875], + [0.626953125, 0.51953125], + [0.619140625, 0.51171875], + [0.6171875, 0.51171875], + [0.615234375, 0.509765625], + [0.58984375, 0.509765625], + [0.587890625, 0.51171875], + [0.572265625, 0.51171875], + [0.5703125, 0.513671875], + [0.546875, 0.513671875], + [0.544921875, 0.51171875], + [0.5390625, 0.51171875], + [0.53515625, 0.5078125], + [0.533203125, 0.5078125], + [0.525390625, 0.5], + [0.525390625, 0.498046875], + [0.521484375, 0.494140625], + [0.521484375, 0.4921875], + [0.51953125, 0.490234375], + [0.51953125, 0.486328125], + [0.517578125, 0.484375], + [0.517578125, 0.48046875], + [0.515625, 0.478515625], + [0.515625, 0.4765625], + [0.513671875, 0.474609375], + [0.513671875, 0.47265625], + [0.51171875, 0.470703125], + [0.51171875, 0.46875], + [0.5078125, 0.46484375], + [0.505859375, 0.46484375], + [0.50390625, 0.462890625], + [0.501953125, 0.462890625], + [0.5, 0.4609375], + [0.498046875, 0.4609375], + [0.49609375, 0.458984375] + ], + [ + [0.56640625, 0.384765625], + [0.564453125, 0.38671875], + [0.564453125, 0.392578125], + [0.5625, 0.39453125], + [0.5625, 0.400390625], + [0.560546875, 0.40234375], + [0.560546875, 0.439453125], + [0.5625, 0.44140625], + [0.5625, 0.443359375], + [0.5703125, 0.451171875], + [0.572265625, 0.451171875], + [0.57421875, 0.453125], + [0.576171875, 0.453125], + [0.578125, 0.455078125], + [0.580078125, 0.455078125], + [0.58203125, 0.45703125], + [0.5859375, 0.45703125], + [0.587890625, 0.458984375], + [0.60546875, 0.458984375], + [0.607421875, 0.45703125], + [0.609375, 0.45703125], + [0.61328125, 0.453125], + [0.61328125, 0.451171875], + [0.615234375, 0.44921875], + [0.615234375, 0.4453125], + [0.6171875, 0.443359375], + [0.6171875, 0.416015625], + [0.615234375, 0.4140625], + [0.615234375, 0.408203125], + [0.61328125, 0.40625], + [0.61328125, 0.40234375], + [0.611328125, 0.400390625], + [0.611328125, 0.3984375], + [0.6015625, 0.388671875], + [0.599609375, 0.388671875], + [0.59765625, 0.38671875], + [0.595703125, 0.38671875], + [0.59375, 0.384765625] + ] + ], + "4": [ + [ + [0.357421875, 0.62109375], + [0.35546875, 0.623046875], + [0.35546875, 0.625], + [0.357421875, 0.626953125], + [0.357421875, 0.62890625], + [0.359375, 0.630859375], + [0.361328125, 0.630859375], + [0.36328125, 0.6328125], + [0.365234375, 0.6328125], + [0.3671875, 0.634765625], + [0.369140625, 0.634765625], + [0.37109375, 0.63671875], + [0.37890625, 0.63671875], + [0.380859375, 0.638671875], + [0.3828125, 0.638671875], + [0.384765625, 0.63671875], + [0.38671875, 0.63671875], + [0.388671875, 0.634765625], + [0.388671875, 0.62890625], + [0.384765625, 0.625], + [0.3828125, 0.625], + [0.380859375, 0.623046875], + [0.375, 0.623046875], + [0.373046875, 0.62109375] + ], + [ + [0.400390625, 0.599609375], + [0.396484375, 0.603515625], + [0.396484375, 0.60546875], + [0.39453125, 0.607421875], + [0.39453125, 0.611328125], + [0.396484375, 0.61328125], + [0.396484375, 0.6171875], + [0.3984375, 0.619140625], + [0.3984375, 0.623046875], + [0.40234375, 0.626953125], + [0.40234375, 0.62890625], + [0.40625, 0.6328125], + [0.40625, 0.634765625], + [0.416015625, 0.64453125], + [0.41796875, 0.64453125], + [0.423828125, 0.650390625], + [0.42578125, 0.650390625], + [0.427734375, 0.65234375], + [0.431640625, 0.65234375], + [0.43359375, 0.654296875], + [0.447265625, 0.654296875], + [0.453125, 0.6484375], + [0.455078125, 0.6484375], + [0.470703125, 0.6328125], + [0.470703125, 0.62890625], + [0.47265625, 0.626953125], + [0.47265625, 0.625], + [0.470703125, 0.623046875], + [0.44921875, 0.623046875], + [0.447265625, 0.62109375], + [0.4453125, 0.62109375], + [0.443359375, 0.619140625], + [0.44140625, 0.619140625], + [0.4375, 0.615234375], + [0.435546875, 0.615234375], + [0.42578125, 0.60546875], + [0.423828125, 0.60546875], + [0.419921875, 0.6015625], + [0.41796875, 0.6015625], + [0.416015625, 0.599609375] + ], + [ + [0.4296875, 0.521484375], + [0.427734375, 0.5234375], + [0.421875, 0.5234375], + [0.419921875, 0.525390625], + [0.41796875, 0.525390625], + [0.416015625, 0.52734375], + [0.4140625, 0.52734375], + [0.412109375, 0.529296875], + [0.41015625, 0.529296875], + [0.408203125, 0.53125], + [0.40625, 0.53125], + [0.404296875, 0.533203125], + [0.40234375, 0.533203125], + [0.3984375, 0.537109375], + [0.396484375, 0.537109375], + [0.39453125, 0.5390625], + [0.392578125, 0.5390625], + [0.388671875, 0.54296875], + [0.388671875, 0.544921875], + [0.390625, 0.546875], + [0.392578125, 0.546875], + [0.39453125, 0.548828125], + [0.3984375, 0.548828125], + [0.400390625, 0.55078125], + [0.40625, 0.55078125], + [0.408203125, 0.548828125], + [0.4140625, 0.548828125], + [0.416015625, 0.546875], + [0.419921875, 0.546875], + [0.421875, 0.544921875], + [0.4296875, 0.544921875], + [0.431640625, 0.54296875], + [0.443359375, 0.54296875], + [0.4453125, 0.541015625], + [0.447265625, 0.541015625], + [0.451171875, 0.537109375], + [0.451171875, 0.529296875], + [0.44921875, 0.52734375], + [0.44921875, 0.525390625], + [0.447265625, 0.525390625], + [0.4453125, 0.5234375], + [0.443359375, 0.5234375], + [0.44140625, 0.521484375] + ], + [ + [0.49609375, 0.451171875], + [0.490234375, 0.45703125], + [0.490234375, 0.458984375], + [0.48828125, 0.4609375], + [0.48828125, 0.462890625], + [0.490234375, 0.46484375], + [0.49609375, 0.46484375], + [0.498046875, 0.466796875], + [0.5078125, 0.466796875], + [0.509765625, 0.46875], + [0.51171875, 0.466796875], + [0.513671875, 0.466796875], + [0.515625, 0.46484375], + [0.515625, 0.458984375], + [0.509765625, 0.453125], + [0.5078125, 0.453125], + [0.505859375, 0.451171875] + ], + [ + [0.671875, 0.443359375], + [0.669921875, 0.4453125], + [0.66796875, 0.4453125], + [0.662109375, 0.451171875], + [0.66015625, 0.451171875], + [0.66015625, 0.453125], + [0.64453125, 0.46875], + [0.64453125, 0.470703125], + [0.63671875, 0.478515625], + [0.634765625, 0.478515625], + [0.6328125, 0.48046875], + [0.630859375, 0.48046875], + [0.62890625, 0.482421875], + [0.61328125, 0.482421875], + [0.611328125, 0.48046875], + [0.609375, 0.48046875], + [0.599609375, 0.470703125], + [0.599609375, 0.466796875], + [0.59765625, 0.46484375], + [0.595703125, 0.46484375], + [0.59375, 0.466796875], + [0.58984375, 0.466796875], + [0.587890625, 0.46875], + [0.5859375, 0.46875], + [0.583984375, 0.470703125], + [0.58203125, 0.470703125], + [0.580078125, 0.47265625], + [0.578125, 0.47265625], + [0.576171875, 0.474609375], + [0.57421875, 0.474609375], + [0.572265625, 0.4765625], + [0.5703125, 0.4765625], + [0.5625, 0.484375], + [0.560546875, 0.484375], + [0.5546875, 0.490234375], + [0.552734375, 0.490234375], + [0.548828125, 0.494140625], + [0.546875, 0.494140625], + [0.544921875, 0.49609375], + [0.541015625, 0.49609375], + [0.5390625, 0.498046875], + [0.537109375, 0.498046875], + [0.53515625, 0.5], + [0.53125, 0.5], + [0.529296875, 0.501953125], + [0.525390625, 0.501953125], + [0.5234375, 0.50390625], + [0.51953125, 0.50390625], + [0.517578125, 0.505859375], + [0.51171875, 0.505859375], + [0.509765625, 0.5078125], + [0.501953125, 0.5078125], + [0.5, 0.509765625], + [0.4921875, 0.509765625], + [0.490234375, 0.51171875], + [0.486328125, 0.51171875], + [0.484375, 0.513671875], + [0.48046875, 0.513671875], + [0.478515625, 0.515625], + [0.4765625, 0.515625], + [0.474609375, 0.517578125], + [0.47265625, 0.517578125], + [0.46875, 0.521484375], + [0.46875, 0.525390625], + [0.466796875, 0.52734375], + [0.466796875, 0.53125], + [0.46875, 0.533203125], + [0.46875, 0.53515625], + [0.490234375, 0.53515625], + [0.4921875, 0.537109375], + [0.51953125, 0.537109375], + [0.521484375, 0.5390625], + [0.53515625, 0.5390625], + [0.537109375, 0.541015625], + [0.552734375, 0.541015625], + [0.5546875, 0.54296875], + [0.560546875, 0.54296875], + [0.5625, 0.544921875], + [0.564453125, 0.544921875], + [0.56640625, 0.546875], + [0.568359375, 0.546875], + [0.5703125, 0.548828125], + [0.5703125, 0.55078125], + [0.572265625, 0.552734375], + [0.572265625, 0.56640625], + [0.5703125, 0.568359375], + [0.5703125, 0.572265625], + [0.568359375, 0.57421875], + [0.568359375, 0.587890625], + [0.5703125, 0.58984375], + [0.5703125, 0.591796875], + [0.576171875, 0.59765625], + [0.578125, 0.59765625], + [0.580078125, 0.599609375], + [0.583984375, 0.599609375], + [0.5859375, 0.6015625], + [0.6015625, 0.6015625], + [0.603515625, 0.599609375], + [0.607421875, 0.599609375], + [0.609375, 0.59765625], + [0.611328125, 0.59765625], + [0.61328125, 0.595703125], + [0.615234375, 0.595703125], + [0.6171875, 0.59375], + [0.619140625, 0.59375], + [0.62109375, 0.591796875], + [0.623046875, 0.591796875], + [0.625, 0.58984375], + [0.626953125, 0.58984375], + [0.630859375, 0.5859375], + [0.630859375, 0.583984375], + [0.62890625, 0.58203125], + [0.62890625, 0.580078125], + [0.626953125, 0.578125], + [0.626953125, 0.576171875], + [0.619140625, 0.568359375], + [0.619140625, 0.56640625], + [0.61328125, 0.560546875], + [0.61328125, 0.55078125], + [0.62109375, 0.54296875], + [0.623046875, 0.54296875], + [0.625, 0.541015625], + [0.6328125, 0.541015625], + [0.634765625, 0.5390625], + [0.642578125, 0.5390625], + [0.64453125, 0.541015625], + [0.658203125, 0.541015625], + [0.66015625, 0.54296875], + [0.6875, 0.54296875], + [0.689453125, 0.541015625], + [0.693359375, 0.541015625], + [0.6953125, 0.5390625], + [0.697265625, 0.5390625], + [0.69921875, 0.537109375], + [0.69921875, 0.53515625], + [0.701171875, 0.533203125], + [0.701171875, 0.53125], + [0.703125, 0.529296875], + [0.703125, 0.5], + [0.701171875, 0.498046875], + [0.701171875, 0.48828125], + [0.69921875, 0.486328125], + [0.69921875, 0.478515625], + [0.697265625, 0.4765625], + [0.697265625, 0.470703125], + [0.6953125, 0.46875], + [0.6953125, 0.46484375], + [0.693359375, 0.462890625], + [0.693359375, 0.458984375], + [0.69140625, 0.45703125], + [0.69140625, 0.455078125], + [0.689453125, 0.453125], + [0.689453125, 0.451171875], + [0.685546875, 0.447265625], + [0.685546875, 0.4453125], + [0.68359375, 0.4453125], + [0.681640625, 0.443359375] + ] + ], + "5": [ + [ + [0.279296875, 0.447265625], + [0.27734375, 0.44921875], + [0.2734375, 0.44921875], + [0.271484375, 0.451171875], + [0.26953125, 0.451171875], + [0.259765625, 0.4609375], + [0.259765625, 0.462890625], + [0.2578125, 0.46484375], + [0.2578125, 0.466796875], + [0.255859375, 0.46875], + [0.255859375, 0.47265625], + [0.25390625, 0.474609375], + [0.25390625, 0.478515625], + [0.251953125, 0.48046875], + [0.251953125, 0.48828125], + [0.25, 0.490234375], + [0.25, 0.51953125], + [0.251953125, 0.521484375], + [0.251953125, 0.525390625], + [0.25390625, 0.52734375], + [0.25390625, 0.529296875], + [0.255859375, 0.53125], + [0.255859375, 0.533203125], + [0.2578125, 0.53515625], + [0.2578125, 0.537109375], + [0.26171875, 0.541015625], + [0.26171875, 0.54296875], + [0.265625, 0.546875], + [0.267578125, 0.546875], + [0.26953125, 0.548828125], + [0.271484375, 0.548828125], + [0.2734375, 0.55078125], + [0.29296875, 0.55078125], + [0.294921875, 0.548828125], + [0.296875, 0.548828125], + [0.298828125, 0.546875], + [0.30078125, 0.546875], + [0.302734375, 0.544921875], + [0.3046875, 0.544921875], + [0.306640625, 0.54296875], + [0.30859375, 0.54296875], + [0.322265625, 0.529296875], + [0.322265625, 0.52734375], + [0.326171875, 0.5234375], + [0.326171875, 0.521484375], + [0.330078125, 0.517578125], + [0.330078125, 0.515625], + [0.33203125, 0.513671875], + [0.33203125, 0.509765625], + [0.333984375, 0.5078125], + [0.333984375, 0.50390625], + [0.3359375, 0.501953125], + [0.3359375, 0.484375], + [0.333984375, 0.482421875], + [0.333984375, 0.478515625], + [0.33203125, 0.4765625], + [0.33203125, 0.474609375], + [0.328125, 0.470703125], + [0.328125, 0.46875], + [0.31640625, 0.45703125], + [0.314453125, 0.45703125], + [0.3125, 0.455078125], + [0.310546875, 0.455078125], + [0.30859375, 0.453125], + [0.3046875, 0.453125], + [0.302734375, 0.451171875], + [0.298828125, 0.451171875], + [0.296875, 0.44921875], + [0.29296875, 0.44921875], + [0.291015625, 0.447265625] + ], + [ + [0.224609375, 0.447265625], + [0.22265625, 0.44921875], + [0.22265625, 0.453125], + [0.220703125, 0.455078125], + [0.220703125, 0.4609375], + [0.21875, 0.462890625], + [0.21875, 0.470703125], + [0.216796875, 0.47265625], + [0.216796875, 0.490234375], + [0.21484375, 0.4921875], + [0.21484375, 0.498046875], + [0.216796875, 0.5], + [0.216796875, 0.515625], + [0.21875, 0.517578125], + [0.21875, 0.5234375], + [0.220703125, 0.525390625], + [0.220703125, 0.53125], + [0.22265625, 0.533203125], + [0.22265625, 0.537109375], + [0.224609375, 0.5390625], + [0.224609375, 0.541015625], + [0.23046875, 0.546875], + [0.234375, 0.546875], + [0.236328125, 0.544921875], + [0.236328125, 0.53125], + [0.23828125, 0.529296875], + [0.23828125, 0.513671875], + [0.240234375, 0.51171875], + [0.240234375, 0.470703125], + [0.23828125, 0.46875], + [0.23828125, 0.46484375], + [0.236328125, 0.462890625], + [0.236328125, 0.4609375], + [0.232421875, 0.45703125], + [0.232421875, 0.455078125], + [0.23046875, 0.453125], + [0.23046875, 0.451171875], + [0.2265625, 0.447265625] + ], + [ + [0.671875, 0.412109375], + [0.669921875, 0.4140625], + [0.6640625, 0.4140625], + [0.662109375, 0.416015625], + [0.66015625, 0.416015625], + [0.658203125, 0.41796875], + [0.654296875, 0.41796875], + [0.65234375, 0.419921875], + [0.650390625, 0.419921875], + [0.642578125, 0.427734375], + [0.642578125, 0.4296875], + [0.640625, 0.431640625], + [0.640625, 0.43359375], + [0.638671875, 0.435546875], + [0.638671875, 0.4375], + [0.63671875, 0.439453125], + [0.63671875, 0.44140625], + [0.634765625, 0.443359375], + [0.634765625, 0.447265625], + [0.6328125, 0.44921875], + [0.6328125, 0.453125], + [0.630859375, 0.455078125], + [0.630859375, 0.458984375], + [0.62890625, 0.4609375], + [0.62890625, 0.466796875], + [0.626953125, 0.46875], + [0.626953125, 0.47265625], + [0.625, 0.474609375], + [0.625, 0.48046875], + [0.623046875, 0.482421875], + [0.623046875, 0.486328125], + [0.62109375, 0.48828125], + [0.62109375, 0.490234375], + [0.619140625, 0.4921875], + [0.619140625, 0.494140625], + [0.6171875, 0.49609375], + [0.6171875, 0.498046875], + [0.615234375, 0.5], + [0.615234375, 0.50390625], + [0.61328125, 0.505859375], + [0.61328125, 0.509765625], + [0.611328125, 0.51171875], + [0.611328125, 0.513671875], + [0.609375, 0.515625], + [0.609375, 0.517578125], + [0.607421875, 0.51953125], + [0.607421875, 0.521484375], + [0.60546875, 0.5234375], + [0.60546875, 0.525390625], + [0.603515625, 0.52734375], + [0.603515625, 0.529296875], + [0.6015625, 0.53125], + [0.6015625, 0.533203125], + [0.59765625, 0.537109375], + [0.59765625, 0.5390625], + [0.595703125, 0.541015625], + [0.595703125, 0.54296875], + [0.59375, 0.544921875], + [0.59375, 0.546875], + [0.591796875, 0.548828125], + [0.591796875, 0.5625], + [0.595703125, 0.56640625], + [0.6015625, 0.56640625], + [0.603515625, 0.568359375], + [0.607421875, 0.568359375], + [0.609375, 0.5703125], + [0.611328125, 0.5703125], + [0.615234375, 0.57421875], + [0.6171875, 0.57421875], + [0.623046875, 0.580078125], + [0.623046875, 0.58203125], + [0.6328125, 0.591796875], + [0.634765625, 0.591796875], + [0.63671875, 0.59375], + [0.650390625, 0.59375], + [0.65234375, 0.591796875], + [0.65625, 0.591796875], + [0.658203125, 0.58984375], + [0.66015625, 0.58984375], + [0.662109375, 0.587890625], + [0.6640625, 0.587890625], + [0.666015625, 0.5859375], + [0.66796875, 0.5859375], + [0.6796875, 0.57421875], + [0.6796875, 0.572265625], + [0.681640625, 0.5703125], + [0.681640625, 0.56640625], + [0.68359375, 0.564453125], + [0.68359375, 0.560546875], + [0.685546875, 0.55859375], + [0.685546875, 0.556640625], + [0.6875, 0.5546875], + [0.6875, 0.552734375], + [0.69140625, 0.548828125], + [0.69140625, 0.546875], + [0.70703125, 0.53125], + [0.70703125, 0.529296875], + [0.708984375, 0.52734375], + [0.708984375, 0.521484375], + [0.7109375, 0.51953125], + [0.7109375, 0.4921875], + [0.708984375, 0.490234375], + [0.708984375, 0.486328125], + [0.70703125, 0.484375], + [0.70703125, 0.478515625], + [0.705078125, 0.4765625], + [0.705078125, 0.453125], + [0.70703125, 0.451171875], + [0.70703125, 0.43359375], + [0.705078125, 0.431640625], + [0.705078125, 0.42578125], + [0.703125, 0.423828125], + [0.703125, 0.421875], + [0.6953125, 0.4140625], + [0.693359375, 0.4140625], + [0.69140625, 0.412109375] + ] + ], + "6": [ + [ + [0.330078125, 0.72265625], + [0.328125, 0.724609375], + [0.328125, 0.734375], + [0.330078125, 0.736328125], + [0.330078125, 0.73828125], + [0.33203125, 0.740234375], + [0.33203125, 0.7421875], + [0.34375, 0.75390625], + [0.345703125, 0.75390625], + [0.349609375, 0.7578125], + [0.3515625, 0.7578125], + [0.35546875, 0.76171875], + [0.357421875, 0.76171875], + [0.359375, 0.763671875], + [0.361328125, 0.763671875], + [0.36328125, 0.765625], + [0.369140625, 0.765625], + [0.37109375, 0.763671875], + [0.373046875, 0.763671875], + [0.375, 0.76171875], + [0.375, 0.759765625], + [0.376953125, 0.7578125], + [0.376953125, 0.755859375], + [0.37890625, 0.75390625], + [0.37890625, 0.74609375], + [0.376953125, 0.744140625], + [0.376953125, 0.740234375], + [0.373046875, 0.736328125], + [0.37109375, 0.736328125], + [0.369140625, 0.734375], + [0.365234375, 0.734375], + [0.36328125, 0.732421875], + [0.357421875, 0.732421875], + [0.35546875, 0.73046875], + [0.349609375, 0.73046875], + [0.34765625, 0.728515625], + [0.34375, 0.728515625], + [0.341796875, 0.7265625], + [0.337890625, 0.7265625], + [0.3359375, 0.724609375], + [0.333984375, 0.724609375], + [0.33203125, 0.72265625] + ], + [ + [0.34765625, 0.580078125], + [0.345703125, 0.58203125], + [0.341796875, 0.58203125], + [0.326171875, 0.59765625], + [0.32421875, 0.59765625], + [0.322265625, 0.599609375], + [0.3203125, 0.599609375], + [0.318359375, 0.6015625], + [0.314453125, 0.6015625], + [0.3125, 0.603515625], + [0.30859375, 0.603515625], + [0.306640625, 0.60546875], + [0.3046875, 0.60546875], + [0.298828125, 0.611328125], + [0.298828125, 0.619140625], + [0.30078125, 0.62109375], + [0.30078125, 0.623046875], + [0.302734375, 0.625], + [0.30859375, 0.625], + [0.310546875, 0.623046875], + [0.318359375, 0.623046875], + [0.3203125, 0.62109375], + [0.328125, 0.62109375], + [0.330078125, 0.619140625], + [0.337890625, 0.619140625], + [0.33984375, 0.6171875], + [0.34375, 0.6171875], + [0.345703125, 0.615234375], + [0.34765625, 0.615234375], + [0.3515625, 0.611328125], + [0.3515625, 0.609375], + [0.353515625, 0.607421875], + [0.353515625, 0.60546875], + [0.35546875, 0.603515625], + [0.35546875, 0.583984375], + [0.353515625, 0.58203125], + [0.3515625, 0.58203125], + [0.349609375, 0.580078125] + ], + [ + [0.251953125, 0.482421875], + [0.25, 0.484375], + [0.248046875, 0.484375], + [0.24609375, 0.486328125], + [0.244140625, 0.486328125], + [0.240234375, 0.490234375], + [0.240234375, 0.4921875], + [0.236328125, 0.49609375], + [0.236328125, 0.5], + [0.234375, 0.501953125], + [0.234375, 0.521484375], + [0.236328125, 0.5234375], + [0.236328125, 0.52734375], + [0.240234375, 0.53125], + [0.240234375, 0.533203125], + [0.2421875, 0.53515625], + [0.244140625, 0.53515625], + [0.24609375, 0.537109375], + [0.255859375, 0.537109375], + [0.26171875, 0.53125], + [0.26171875, 0.498046875], + [0.259765625, 0.49609375], + [0.259765625, 0.4921875], + [0.2578125, 0.490234375], + [0.2578125, 0.484375], + [0.255859375, 0.482421875] + ], + [ + [0.46875, 0.478515625], + [0.466796875, 0.48046875], + [0.4609375, 0.48046875], + [0.458984375, 0.482421875], + [0.453125, 0.482421875], + [0.451171875, 0.484375], + [0.4453125, 0.484375], + [0.443359375, 0.486328125], + [0.435546875, 0.486328125], + [0.43359375, 0.48828125], + [0.427734375, 0.48828125], + [0.42578125, 0.490234375], + [0.419921875, 0.490234375], + [0.41796875, 0.4921875], + [0.416015625, 0.4921875], + [0.412109375, 0.49609375], + [0.41015625, 0.49609375], + [0.40625, 0.5], + [0.404296875, 0.5], + [0.388671875, 0.515625], + [0.388671875, 0.517578125], + [0.380859375, 0.525390625], + [0.380859375, 0.52734375], + [0.375, 0.533203125], + [0.375, 0.53515625], + [0.373046875, 0.537109375], + [0.373046875, 0.5390625], + [0.37109375, 0.541015625], + [0.37109375, 0.54296875], + [0.369140625, 0.544921875], + [0.369140625, 0.55859375], + [0.37109375, 0.560546875], + [0.37109375, 0.5625], + [0.373046875, 0.564453125], + [0.375, 0.564453125], + [0.376953125, 0.56640625], + [0.37890625, 0.56640625], + [0.380859375, 0.568359375], + [0.39453125, 0.568359375], + [0.396484375, 0.5703125], + [0.40234375, 0.5703125], + [0.404296875, 0.572265625], + [0.40625, 0.572265625], + [0.408203125, 0.57421875], + [0.41015625, 0.57421875], + [0.427734375, 0.591796875], + [0.427734375, 0.59375], + [0.431640625, 0.59765625], + [0.43359375, 0.59765625], + [0.435546875, 0.599609375], + [0.4453125, 0.599609375], + [0.447265625, 0.6015625], + [0.45703125, 0.6015625], + [0.458984375, 0.599609375], + [0.46484375, 0.599609375], + [0.47265625, 0.591796875], + [0.47265625, 0.58984375], + [0.474609375, 0.587890625], + [0.474609375, 0.583984375], + [0.4765625, 0.58203125], + [0.4765625, 0.55859375], + [0.478515625, 0.556640625], + [0.478515625, 0.548828125], + [0.48046875, 0.546875], + [0.48046875, 0.544921875], + [0.482421875, 0.54296875], + [0.482421875, 0.541015625], + [0.486328125, 0.537109375], + [0.486328125, 0.53515625], + [0.494140625, 0.52734375], + [0.494140625, 0.525390625], + [0.498046875, 0.521484375], + [0.498046875, 0.51953125], + [0.5, 0.517578125], + [0.5, 0.513671875], + [0.501953125, 0.51171875], + [0.501953125, 0.494140625], + [0.5, 0.4921875], + [0.5, 0.48828125], + [0.4921875, 0.48046875], + [0.48828125, 0.48046875], + [0.486328125, 0.478515625] + ], + [ + [0.33203125, 0.384765625], + [0.330078125, 0.38671875], + [0.328125, 0.38671875], + [0.322265625, 0.392578125], + [0.322265625, 0.39453125], + [0.31640625, 0.400390625], + [0.31640625, 0.40234375], + [0.314453125, 0.404296875], + [0.3125, 0.404296875], + [0.30859375, 0.408203125], + [0.3046875, 0.408203125], + [0.302734375, 0.40625], + [0.298828125, 0.40625], + [0.294921875, 0.40234375], + [0.29296875, 0.40234375], + [0.291015625, 0.400390625], + [0.279296875, 0.400390625], + [0.27734375, 0.40234375], + [0.27734375, 0.404296875], + [0.2734375, 0.408203125], + [0.2734375, 0.41015625], + [0.271484375, 0.412109375], + [0.271484375, 0.4140625], + [0.26953125, 0.416015625], + [0.26953125, 0.41796875], + [0.267578125, 0.419921875], + [0.267578125, 0.423828125], + [0.265625, 0.42578125], + [0.265625, 0.43359375], + [0.267578125, 0.435546875], + [0.267578125, 0.4375], + [0.26953125, 0.4375], + [0.271484375, 0.439453125], + [0.2734375, 0.439453125], + [0.275390625, 0.44140625], + [0.27734375, 0.44140625], + [0.279296875, 0.443359375], + [0.279296875, 0.455078125], + [0.27734375, 0.45703125], + [0.27734375, 0.4609375], + [0.275390625, 0.462890625], + [0.275390625, 0.466796875], + [0.2734375, 0.46875], + [0.2734375, 0.525390625], + [0.275390625, 0.52734375], + [0.275390625, 0.54296875], + [0.27734375, 0.544921875], + [0.27734375, 0.546875], + [0.279296875, 0.548828125], + [0.279296875, 0.55078125], + [0.283203125, 0.5546875], + [0.283203125, 0.556640625], + [0.28515625, 0.55859375], + [0.287109375, 0.55859375], + [0.291015625, 0.5625], + [0.29296875, 0.5625], + [0.294921875, 0.564453125], + [0.296875, 0.564453125], + [0.298828125, 0.56640625], + [0.306640625, 0.56640625], + [0.30859375, 0.564453125], + [0.310546875, 0.564453125], + [0.3125, 0.5625], + [0.314453125, 0.5625], + [0.32421875, 0.552734375], + [0.32421875, 0.55078125], + [0.328125, 0.546875], + [0.328125, 0.54296875], + [0.330078125, 0.541015625], + [0.330078125, 0.537109375], + [0.33203125, 0.53515625], + [0.33203125, 0.529296875], + [0.333984375, 0.52734375], + [0.333984375, 0.462890625], + [0.33203125, 0.4609375], + [0.33203125, 0.44921875], + [0.330078125, 0.447265625], + [0.330078125, 0.435546875], + [0.328125, 0.43359375], + [0.328125, 0.427734375], + [0.33984375, 0.416015625], + [0.341796875, 0.416015625], + [0.34375, 0.4140625], + [0.34765625, 0.4140625], + [0.349609375, 0.412109375], + [0.35546875, 0.412109375], + [0.357421875, 0.41015625], + [0.373046875, 0.41015625], + [0.376953125, 0.40625], + [0.376953125, 0.400390625], + [0.375, 0.3984375], + [0.375, 0.396484375], + [0.373046875, 0.396484375], + [0.37109375, 0.3984375], + [0.35546875, 0.3984375], + [0.353515625, 0.396484375], + [0.3515625, 0.396484375], + [0.345703125, 0.390625], + [0.34375, 0.390625], + [0.33984375, 0.38671875], + [0.337890625, 0.38671875], + [0.3359375, 0.384765625] + ], + [ + [0.455078125, 0.23828125], + [0.453125, 0.240234375], + [0.447265625, 0.240234375], + [0.4453125, 0.2421875], + [0.44140625, 0.2421875], + [0.439453125, 0.244140625], + [0.4375, 0.244140625], + [0.435546875, 0.24609375], + [0.431640625, 0.24609375], + [0.4296875, 0.248046875], + [0.427734375, 0.248046875], + [0.42578125, 0.25], + [0.423828125, 0.25], + [0.421875, 0.251953125], + [0.419921875, 0.251953125], + [0.416015625, 0.255859375], + [0.4140625, 0.255859375], + [0.41015625, 0.259765625], + [0.408203125, 0.259765625], + [0.390625, 0.27734375], + [0.390625, 0.279296875], + [0.38671875, 0.283203125], + [0.380859375, 0.283203125], + [0.37890625, 0.28515625], + [0.375, 0.28515625], + [0.373046875, 0.287109375], + [0.37109375, 0.287109375], + [0.3671875, 0.291015625], + [0.365234375, 0.291015625], + [0.3515625, 0.3046875], + [0.3515625, 0.306640625], + [0.349609375, 0.30859375], + [0.3515625, 0.310546875], + [0.353515625, 0.310546875], + [0.35546875, 0.3125], + [0.373046875, 0.3125], + [0.375, 0.310546875], + [0.380859375, 0.310546875], + [0.3828125, 0.30859375], + [0.38671875, 0.30859375], + [0.388671875, 0.306640625], + [0.392578125, 0.306640625], + [0.39453125, 0.3046875], + [0.3984375, 0.3046875], + [0.400390625, 0.302734375], + [0.404296875, 0.302734375], + [0.40625, 0.30078125], + [0.41015625, 0.30078125], + [0.412109375, 0.298828125], + [0.41796875, 0.298828125], + [0.419921875, 0.296875], + [0.423828125, 0.296875], + [0.42578125, 0.294921875], + [0.431640625, 0.294921875], + [0.43359375, 0.29296875], + [0.451171875, 0.29296875], + [0.453125, 0.291015625], + [0.509765625, 0.291015625], + [0.51171875, 0.29296875], + [0.521484375, 0.29296875], + [0.5234375, 0.294921875], + [0.53125, 0.294921875], + [0.533203125, 0.296875], + [0.5390625, 0.296875], + [0.541015625, 0.298828125], + [0.546875, 0.298828125], + [0.548828125, 0.30078125], + [0.5546875, 0.30078125], + [0.556640625, 0.302734375], + [0.576171875, 0.302734375], + [0.58203125, 0.296875], + [0.58203125, 0.275390625], + [0.580078125, 0.2734375], + [0.580078125, 0.267578125], + [0.578125, 0.265625], + [0.578125, 0.26171875], + [0.576171875, 0.259765625], + [0.576171875, 0.2578125], + [0.572265625, 0.25390625], + [0.572265625, 0.251953125], + [0.5703125, 0.25], + [0.568359375, 0.25], + [0.56640625, 0.248046875], + [0.564453125, 0.248046875], + [0.5625, 0.24609375], + [0.5234375, 0.24609375], + [0.521484375, 0.244140625], + [0.509765625, 0.244140625], + [0.5078125, 0.2421875], + [0.49609375, 0.2421875], + [0.494140625, 0.240234375], + [0.48046875, 0.240234375], + [0.478515625, 0.23828125] + ] + ], + "7": [ + [ + [0.63671875, 0.453125], + [0.6328125, 0.45703125], + [0.630859375, 0.45703125], + [0.626953125, 0.4609375], + [0.626953125, 0.462890625], + [0.625, 0.46484375], + [0.625, 0.466796875], + [0.623046875, 0.46875], + [0.623046875, 0.478515625], + [0.625, 0.48046875], + [0.625, 0.498046875], + [0.626953125, 0.5], + [0.626953125, 0.50390625], + [0.630859375, 0.5078125], + [0.6328125, 0.5078125], + [0.634765625, 0.509765625], + [0.638671875, 0.509765625], + [0.640625, 0.51171875], + [0.6640625, 0.51171875], + [0.666015625, 0.509765625], + [0.66796875, 0.509765625], + [0.669921875, 0.5078125], + [0.669921875, 0.505859375], + [0.671875, 0.50390625], + [0.671875, 0.501953125], + [0.673828125, 0.5], + [0.673828125, 0.48828125], + [0.671875, 0.486328125], + [0.671875, 0.482421875], + [0.669921875, 0.48046875], + [0.669921875, 0.4765625], + [0.66796875, 0.474609375], + [0.66796875, 0.47265625], + [0.666015625, 0.470703125], + [0.666015625, 0.46875], + [0.66015625, 0.462890625], + [0.66015625, 0.4609375], + [0.658203125, 0.458984375], + [0.65625, 0.458984375], + [0.65234375, 0.455078125], + [0.650390625, 0.455078125], + [0.6484375, 0.453125] + ], + [ + [0.51953125, 0.427734375], + [0.517578125, 0.4296875], + [0.51171875, 0.4296875], + [0.509765625, 0.431640625], + [0.5078125, 0.431640625], + [0.505859375, 0.43359375], + [0.501953125, 0.43359375], + [0.5, 0.435546875], + [0.498046875, 0.435546875], + [0.49609375, 0.4375], + [0.494140625, 0.4375], + [0.4921875, 0.439453125], + [0.490234375, 0.439453125], + [0.48828125, 0.44140625], + [0.486328125, 0.44140625], + [0.484375, 0.443359375], + [0.482421875, 0.443359375], + [0.48046875, 0.4453125], + [0.478515625, 0.4453125], + [0.4765625, 0.447265625], + [0.474609375, 0.447265625], + [0.47265625, 0.44921875], + [0.470703125, 0.44921875], + [0.46875, 0.451171875], + [0.466796875, 0.451171875], + [0.4609375, 0.45703125], + [0.4609375, 0.458984375], + [0.458984375, 0.4609375], + [0.458984375, 0.46484375], + [0.45703125, 0.466796875], + [0.45703125, 0.46875], + [0.455078125, 0.470703125], + [0.455078125, 0.47265625], + [0.453125, 0.474609375], + [0.453125, 0.4765625], + [0.44921875, 0.48046875], + [0.447265625, 0.48046875], + [0.44140625, 0.486328125], + [0.439453125, 0.486328125], + [0.4375, 0.48828125], + [0.435546875, 0.48828125], + [0.431640625, 0.4921875], + [0.4296875, 0.4921875], + [0.423828125, 0.498046875], + [0.423828125, 0.5], + [0.421875, 0.501953125], + [0.421875, 0.513671875], + [0.423828125, 0.515625], + [0.42578125, 0.515625], + [0.427734375, 0.517578125], + [0.453125, 0.517578125], + [0.455078125, 0.515625], + [0.4609375, 0.515625], + [0.462890625, 0.517578125], + [0.4765625, 0.517578125], + [0.478515625, 0.51953125], + [0.4921875, 0.51953125], + [0.494140625, 0.521484375], + [0.5078125, 0.521484375], + [0.509765625, 0.5234375], + [0.537109375, 0.5234375], + [0.5390625, 0.521484375], + [0.54296875, 0.521484375], + [0.544921875, 0.51953125], + [0.546875, 0.51953125], + [0.548828125, 0.517578125], + [0.55078125, 0.517578125], + [0.556640625, 0.51171875], + [0.556640625, 0.509765625], + [0.560546875, 0.505859375], + [0.560546875, 0.50390625], + [0.564453125, 0.5], + [0.564453125, 0.498046875], + [0.57421875, 0.48828125], + [0.57421875, 0.486328125], + [0.576171875, 0.484375], + [0.576171875, 0.482421875], + [0.578125, 0.48046875], + [0.578125, 0.462890625], + [0.576171875, 0.4609375], + [0.576171875, 0.458984375], + [0.572265625, 0.455078125], + [0.572265625, 0.453125], + [0.5625, 0.443359375], + [0.560546875, 0.443359375], + [0.5546875, 0.4375], + [0.552734375, 0.4375], + [0.548828125, 0.43359375], + [0.546875, 0.43359375], + [0.544921875, 0.431640625], + [0.54296875, 0.431640625], + [0.541015625, 0.4296875], + [0.537109375, 0.4296875], + [0.53515625, 0.427734375] + ] + ], + "8": [ + [ + [0.634765625, 0.607421875], + [0.62890625, 0.61328125], + [0.62890625, 0.6171875], + [0.626953125, 0.619140625], + [0.626953125, 0.623046875], + [0.62890625, 0.625], + [0.630859375, 0.625], + [0.6328125, 0.623046875], + [0.63671875, 0.623046875], + [0.638671875, 0.62109375], + [0.638671875, 0.619140625], + [0.640625, 0.6171875], + [0.640625, 0.61328125], + [0.638671875, 0.611328125], + [0.638671875, 0.609375], + [0.63671875, 0.609375] + ], + [ + [0.439453125, 0.45703125], + [0.4375, 0.458984375], + [0.435546875, 0.458984375], + [0.43359375, 0.4609375], + [0.4296875, 0.4609375], + [0.427734375, 0.462890625], + [0.423828125, 0.462890625], + [0.421875, 0.46484375], + [0.419921875, 0.46484375], + [0.41796875, 0.466796875], + [0.4140625, 0.466796875], + [0.412109375, 0.46875], + [0.40625, 0.46875], + [0.404296875, 0.470703125], + [0.388671875, 0.470703125], + [0.38671875, 0.47265625], + [0.376953125, 0.47265625], + [0.375, 0.470703125], + [0.361328125, 0.470703125], + [0.359375, 0.46875], + [0.357421875, 0.470703125], + [0.349609375, 0.470703125], + [0.34765625, 0.47265625], + [0.345703125, 0.47265625], + [0.33984375, 0.478515625], + [0.33984375, 0.482421875], + [0.337890625, 0.484375], + [0.337890625, 0.490234375], + [0.33984375, 0.4921875], + [0.33984375, 0.494140625], + [0.345703125, 0.5], + [0.34765625, 0.5], + [0.3515625, 0.50390625], + [0.353515625, 0.50390625], + [0.35546875, 0.505859375], + [0.357421875, 0.505859375], + [0.359375, 0.5078125], + [0.36328125, 0.5078125], + [0.365234375, 0.509765625], + [0.390625, 0.509765625], + [0.392578125, 0.51171875], + [0.4140625, 0.51171875], + [0.416015625, 0.513671875], + [0.451171875, 0.513671875], + [0.453125, 0.51171875], + [0.45703125, 0.51171875], + [0.466796875, 0.501953125], + [0.466796875, 0.498046875], + [0.46875, 0.49609375], + [0.46875, 0.4921875], + [0.470703125, 0.490234375], + [0.470703125, 0.4765625], + [0.46875, 0.474609375], + [0.46875, 0.470703125], + [0.466796875, 0.46875], + [0.466796875, 0.466796875], + [0.462890625, 0.462890625], + [0.462890625, 0.4609375], + [0.4609375, 0.4609375], + [0.45703125, 0.45703125] + ], + [ + [0.5703125, 0.39453125], + [0.568359375, 0.396484375], + [0.56640625, 0.396484375], + [0.564453125, 0.3984375], + [0.5625, 0.3984375], + [0.5546875, 0.40625], + [0.5546875, 0.408203125], + [0.548828125, 0.4140625], + [0.548828125, 0.416015625], + [0.537109375, 0.427734375], + [0.537109375, 0.4296875], + [0.52734375, 0.439453125], + [0.52734375, 0.44140625], + [0.5234375, 0.4453125], + [0.5234375, 0.447265625], + [0.51953125, 0.451171875], + [0.51953125, 0.453125], + [0.517578125, 0.455078125], + [0.517578125, 0.458984375], + [0.515625, 0.4609375], + [0.515625, 0.466796875], + [0.513671875, 0.46875], + [0.513671875, 0.486328125], + [0.515625, 0.48828125], + [0.515625, 0.490234375], + [0.517578125, 0.4921875], + [0.51953125, 0.4921875], + [0.521484375, 0.494140625], + [0.54296875, 0.494140625], + [0.544921875, 0.49609375], + [0.552734375, 0.49609375], + [0.5546875, 0.498046875], + [0.5625, 0.498046875], + [0.564453125, 0.5], + [0.576171875, 0.5], + [0.578125, 0.498046875], + [0.580078125, 0.498046875], + [0.583984375, 0.494140625], + [0.5859375, 0.494140625], + [0.59765625, 0.482421875], + [0.599609375, 0.482421875], + [0.603515625, 0.478515625], + [0.60546875, 0.478515625], + [0.607421875, 0.4765625], + [0.609375, 0.4765625], + [0.611328125, 0.474609375], + [0.61328125, 0.474609375], + [0.625, 0.462890625], + [0.625, 0.458984375], + [0.626953125, 0.45703125], + [0.626953125, 0.4296875], + [0.625, 0.427734375], + [0.625, 0.42578125], + [0.623046875, 0.423828125], + [0.623046875, 0.421875], + [0.61328125, 0.412109375], + [0.611328125, 0.412109375], + [0.60546875, 0.40625], + [0.603515625, 0.40625], + [0.6015625, 0.404296875], + [0.599609375, 0.404296875], + [0.595703125, 0.400390625], + [0.59375, 0.400390625], + [0.591796875, 0.3984375], + [0.587890625, 0.3984375], + [0.5859375, 0.396484375], + [0.583984375, 0.396484375], + [0.58203125, 0.39453125] + ], + [ + [0.703125, 0.375], + [0.697265625, 0.380859375], + [0.697265625, 0.384765625], + [0.6953125, 0.38671875], + [0.6953125, 0.388671875], + [0.693359375, 0.390625], + [0.693359375, 0.392578125], + [0.685546875, 0.400390625], + [0.68359375, 0.400390625], + [0.681640625, 0.40234375], + [0.677734375, 0.40234375], + [0.67578125, 0.404296875], + [0.67578125, 0.423828125], + [0.677734375, 0.42578125], + [0.677734375, 0.431640625], + [0.67578125, 0.43359375], + [0.67578125, 0.443359375], + [0.673828125, 0.4453125], + [0.673828125, 0.451171875], + [0.671875, 0.453125], + [0.671875, 0.45703125], + [0.669921875, 0.458984375], + [0.669921875, 0.462890625], + [0.66796875, 0.46484375], + [0.66796875, 0.466796875], + [0.666015625, 0.46875], + [0.666015625, 0.47265625], + [0.6640625, 0.474609375], + [0.6640625, 0.4765625], + [0.662109375, 0.478515625], + [0.662109375, 0.48046875], + [0.66015625, 0.482421875], + [0.66015625, 0.484375], + [0.658203125, 0.486328125], + [0.658203125, 0.48828125], + [0.654296875, 0.4921875], + [0.654296875, 0.494140625], + [0.65234375, 0.49609375], + [0.65234375, 0.498046875], + [0.646484375, 0.50390625], + [0.646484375, 0.505859375], + [0.6328125, 0.51953125], + [0.630859375, 0.51953125], + [0.626953125, 0.5234375], + [0.625, 0.5234375], + [0.623046875, 0.525390625], + [0.6171875, 0.525390625], + [0.615234375, 0.52734375], + [0.607421875, 0.52734375], + [0.60546875, 0.529296875], + [0.6015625, 0.529296875], + [0.599609375, 0.53125], + [0.59765625, 0.53125], + [0.58984375, 0.5390625], + [0.58984375, 0.54296875], + [0.587890625, 0.544921875], + [0.587890625, 0.552734375], + [0.5859375, 0.5546875], + [0.587890625, 0.556640625], + [0.587890625, 0.56640625], + [0.58984375, 0.568359375], + [0.58984375, 0.5703125], + [0.591796875, 0.572265625], + [0.59375, 0.572265625], + [0.595703125, 0.57421875], + [0.59765625, 0.57421875], + [0.599609375, 0.576171875], + [0.6171875, 0.576171875], + [0.619140625, 0.578125], + [0.626953125, 0.578125], + [0.62890625, 0.580078125], + [0.638671875, 0.580078125], + [0.640625, 0.578125], + [0.642578125, 0.578125], + [0.64453125, 0.576171875], + [0.646484375, 0.576171875], + [0.669921875, 0.552734375], + [0.669921875, 0.55078125], + [0.673828125, 0.546875], + [0.673828125, 0.544921875], + [0.677734375, 0.541015625], + [0.677734375, 0.5390625], + [0.6796875, 0.537109375], + [0.6796875, 0.53515625], + [0.681640625, 0.533203125], + [0.681640625, 0.53125], + [0.68359375, 0.529296875], + [0.68359375, 0.52734375], + [0.685546875, 0.525390625], + [0.685546875, 0.5234375], + [0.6875, 0.521484375], + [0.6875, 0.51953125], + [0.689453125, 0.517578125], + [0.689453125, 0.513671875], + [0.69140625, 0.51171875], + [0.69140625, 0.509765625], + [0.693359375, 0.5078125], + [0.693359375, 0.505859375], + [0.6953125, 0.50390625], + [0.6953125, 0.501953125], + [0.697265625, 0.5], + [0.697265625, 0.498046875], + [0.701171875, 0.494140625], + [0.701171875, 0.4921875], + [0.705078125, 0.48828125], + [0.705078125, 0.486328125], + [0.720703125, 0.470703125], + [0.72265625, 0.470703125], + [0.74609375, 0.447265625], + [0.74609375, 0.4453125], + [0.748046875, 0.443359375], + [0.748046875, 0.44140625], + [0.75, 0.439453125], + [0.75, 0.423828125], + [0.748046875, 0.421875], + [0.748046875, 0.416015625], + [0.74609375, 0.4140625], + [0.74609375, 0.41015625], + [0.744140625, 0.408203125], + [0.744140625, 0.40625], + [0.7421875, 0.404296875], + [0.7421875, 0.40234375], + [0.740234375, 0.400390625], + [0.740234375, 0.3984375], + [0.73828125, 0.396484375], + [0.73828125, 0.39453125], + [0.736328125, 0.392578125], + [0.736328125, 0.390625], + [0.724609375, 0.37890625], + [0.72265625, 0.37890625], + [0.720703125, 0.376953125], + [0.71875, 0.376953125], + [0.716796875, 0.375] + ] + ], + "9": [ + [ + [0.5546875, 0.669921875], + [0.552734375, 0.671875], + [0.548828125, 0.671875], + [0.546875, 0.673828125], + [0.544921875, 0.673828125], + [0.54296875, 0.67578125], + [0.5390625, 0.67578125], + [0.537109375, 0.677734375], + [0.53515625, 0.677734375], + [0.533203125, 0.6796875], + [0.53515625, 0.681640625], + [0.53515625, 0.68359375], + [0.537109375, 0.685546875], + [0.537109375, 0.6875], + [0.5390625, 0.689453125], + [0.541015625, 0.689453125], + [0.54296875, 0.69140625], + [0.560546875, 0.69140625], + [0.564453125, 0.6875], + [0.564453125, 0.673828125], + [0.5625, 0.671875], + [0.560546875, 0.671875], + [0.55859375, 0.669921875] + ], + [ + [0.498046875, 0.650390625], + [0.49609375, 0.65234375], + [0.494140625, 0.65234375], + [0.494140625, 0.654296875], + [0.4921875, 0.65625], + [0.4921875, 0.66015625], + [0.49609375, 0.6640625], + [0.498046875, 0.6640625], + [0.5, 0.666015625], + [0.505859375, 0.666015625], + [0.509765625, 0.662109375], + [0.509765625, 0.66015625], + [0.51171875, 0.658203125], + [0.51171875, 0.65625], + [0.505859375, 0.650390625] + ], + [ + [0.400390625, 0.474609375], + [0.396484375, 0.478515625], + [0.396484375, 0.486328125], + [0.3984375, 0.48828125], + [0.3984375, 0.4921875], + [0.40234375, 0.49609375], + [0.40234375, 0.498046875], + [0.404296875, 0.5], + [0.40625, 0.5], + [0.408203125, 0.501953125], + [0.41015625, 0.501953125], + [0.412109375, 0.50390625], + [0.421875, 0.50390625], + [0.423828125, 0.501953125], + [0.42578125, 0.501953125], + [0.42578125, 0.5], + [0.427734375, 0.498046875], + [0.427734375, 0.49609375], + [0.4296875, 0.494140625], + [0.4296875, 0.490234375], + [0.42578125, 0.486328125], + [0.423828125, 0.486328125], + [0.421875, 0.484375], + [0.419921875, 0.484375], + [0.4140625, 0.478515625], + [0.412109375, 0.478515625], + [0.41015625, 0.4765625], + [0.408203125, 0.4765625], + [0.40625, 0.474609375] + ], + [ + [0.6328125, 0.40234375], + [0.630859375, 0.404296875], + [0.626953125, 0.404296875], + [0.625, 0.40625], + [0.623046875, 0.40625], + [0.62109375, 0.408203125], + [0.619140625, 0.408203125], + [0.6171875, 0.41015625], + [0.61328125, 0.41015625], + [0.611328125, 0.412109375], + [0.609375, 0.412109375], + [0.607421875, 0.4140625], + [0.60546875, 0.4140625], + [0.603515625, 0.416015625], + [0.599609375, 0.416015625], + [0.59765625, 0.41796875], + [0.59375, 0.41796875], + [0.591796875, 0.419921875], + [0.58984375, 0.419921875], + [0.587890625, 0.421875], + [0.5859375, 0.421875], + [0.580078125, 0.427734375], + [0.580078125, 0.4296875], + [0.578125, 0.431640625], + [0.578125, 0.4375], + [0.580078125, 0.439453125], + [0.580078125, 0.44140625], + [0.58203125, 0.443359375], + [0.583984375, 0.443359375], + [0.5859375, 0.4453125], + [0.587890625, 0.4453125], + [0.58984375, 0.447265625], + [0.59765625, 0.447265625], + [0.599609375, 0.4453125], + [0.6015625, 0.4453125], + [0.60546875, 0.44140625], + [0.607421875, 0.44140625], + [0.615234375, 0.43359375], + [0.6171875, 0.43359375], + [0.619140625, 0.431640625], + [0.62890625, 0.431640625], + [0.630859375, 0.43359375], + [0.6328125, 0.43359375], + [0.640625, 0.44140625], + [0.640625, 0.45703125], + [0.642578125, 0.458984375], + [0.642578125, 0.4609375], + [0.650390625, 0.46875], + [0.65234375, 0.46875], + [0.6640625, 0.48046875], + [0.6640625, 0.490234375], + [0.66015625, 0.494140625], + [0.66015625, 0.49609375], + [0.6484375, 0.5078125], + [0.646484375, 0.5078125], + [0.64453125, 0.509765625], + [0.638671875, 0.509765625], + [0.63671875, 0.51171875], + [0.62890625, 0.51171875], + [0.626953125, 0.509765625], + [0.62109375, 0.509765625], + [0.619140625, 0.5078125], + [0.6171875, 0.5078125], + [0.61328125, 0.50390625], + [0.611328125, 0.50390625], + [0.607421875, 0.5], + [0.603515625, 0.5], + [0.6015625, 0.498046875], + [0.58984375, 0.498046875], + [0.587890625, 0.49609375], + [0.5859375, 0.49609375], + [0.58203125, 0.4921875], + [0.58203125, 0.490234375], + [0.580078125, 0.48828125], + [0.580078125, 0.486328125], + [0.578125, 0.484375], + [0.578125, 0.482421875], + [0.576171875, 0.48046875], + [0.576171875, 0.4765625], + [0.57421875, 0.474609375], + [0.57421875, 0.470703125], + [0.572265625, 0.46875], + [0.572265625, 0.451171875], + [0.5703125, 0.44921875], + [0.5703125, 0.431640625], + [0.568359375, 0.4296875], + [0.568359375, 0.42578125], + [0.56640625, 0.423828125], + [0.56640625, 0.419921875], + [0.55859375, 0.412109375], + [0.544921875, 0.412109375], + [0.54296875, 0.4140625], + [0.53515625, 0.4140625], + [0.533203125, 0.416015625], + [0.525390625, 0.416015625], + [0.5234375, 0.41796875], + [0.51953125, 0.41796875], + [0.517578125, 0.419921875], + [0.513671875, 0.419921875], + [0.51171875, 0.421875], + [0.5078125, 0.421875], + [0.505859375, 0.423828125], + [0.50390625, 0.423828125], + [0.5, 0.427734375], + [0.498046875, 0.427734375], + [0.49609375, 0.4296875], + [0.49609375, 0.431640625], + [0.4921875, 0.435546875], + [0.4921875, 0.4375], + [0.490234375, 0.439453125], + [0.490234375, 0.44140625], + [0.48828125, 0.443359375], + [0.48828125, 0.453125], + [0.486328125, 0.455078125], + [0.486328125, 0.458984375], + [0.482421875, 0.462890625], + [0.482421875, 0.46484375], + [0.48046875, 0.466796875], + [0.478515625, 0.466796875], + [0.474609375, 0.470703125], + [0.458984375, 0.470703125], + [0.45703125, 0.47265625], + [0.455078125, 0.47265625], + [0.453125, 0.474609375], + [0.451171875, 0.474609375], + [0.451171875, 0.4765625], + [0.44921875, 0.478515625], + [0.44921875, 0.48046875], + [0.447265625, 0.482421875], + [0.447265625, 0.494140625], + [0.44921875, 0.49609375], + [0.453125, 0.49609375], + [0.455078125, 0.498046875], + [0.458984375, 0.498046875], + [0.462890625, 0.501953125], + [0.462890625, 0.51171875], + [0.4609375, 0.513671875], + [0.4609375, 0.515625], + [0.458984375, 0.517578125], + [0.458984375, 0.51953125], + [0.45703125, 0.521484375], + [0.45703125, 0.5234375], + [0.455078125, 0.525390625], + [0.455078125, 0.52734375], + [0.453125, 0.529296875], + [0.453125, 0.533203125], + [0.451171875, 0.53515625], + [0.451171875, 0.546875], + [0.453125, 0.548828125], + [0.453125, 0.5546875], + [0.455078125, 0.556640625], + [0.455078125, 0.560546875], + [0.45703125, 0.5625], + [0.45703125, 0.564453125], + [0.4609375, 0.568359375], + [0.4609375, 0.5703125], + [0.46484375, 0.57421875], + [0.46484375, 0.576171875], + [0.478515625, 0.58984375], + [0.48046875, 0.58984375], + [0.486328125, 0.595703125], + [0.48828125, 0.595703125], + [0.490234375, 0.59765625], + [0.4921875, 0.59765625], + [0.494140625, 0.599609375], + [0.498046875, 0.599609375], + [0.5, 0.6015625], + [0.509765625, 0.6015625], + [0.51171875, 0.599609375], + [0.517578125, 0.599609375], + [0.51953125, 0.59765625], + [0.5234375, 0.59765625], + [0.525390625, 0.595703125], + [0.52734375, 0.595703125], + [0.529296875, 0.59375], + [0.53515625, 0.59375], + [0.537109375, 0.591796875], + [0.544921875, 0.591796875], + [0.546875, 0.59375], + [0.55078125, 0.59375], + [0.552734375, 0.595703125], + [0.5546875, 0.595703125], + [0.556640625, 0.59765625], + [0.55859375, 0.59765625], + [0.560546875, 0.599609375], + [0.5625, 0.599609375], + [0.56640625, 0.603515625], + [0.568359375, 0.603515625], + [0.572265625, 0.607421875], + [0.57421875, 0.607421875], + [0.580078125, 0.61328125], + [0.58203125, 0.61328125], + [0.5859375, 0.6171875], + [0.587890625, 0.6171875], + [0.591796875, 0.62109375], + [0.59375, 0.62109375], + [0.59765625, 0.625], + [0.599609375, 0.625], + [0.6015625, 0.626953125], + [0.60546875, 0.626953125], + [0.607421875, 0.62890625], + [0.615234375, 0.62890625], + [0.6171875, 0.630859375], + [0.623046875, 0.630859375], + [0.625, 0.62890625], + [0.634765625, 0.62890625], + [0.63671875, 0.626953125], + [0.642578125, 0.626953125], + [0.64453125, 0.625], + [0.6484375, 0.625], + [0.650390625, 0.623046875], + [0.65234375, 0.623046875], + [0.654296875, 0.62109375], + [0.658203125, 0.62109375], + [0.66015625, 0.619140625], + [0.662109375, 0.619140625], + [0.6640625, 0.6171875], + [0.66796875, 0.6171875], + [0.669921875, 0.615234375], + [0.673828125, 0.615234375], + [0.67578125, 0.61328125], + [0.6796875, 0.61328125], + [0.681640625, 0.611328125], + [0.6875, 0.611328125], + [0.689453125, 0.609375], + [0.693359375, 0.609375], + [0.6953125, 0.607421875], + [0.697265625, 0.607421875], + [0.708984375, 0.595703125], + [0.708984375, 0.59375], + [0.71484375, 0.587890625], + [0.71484375, 0.5859375], + [0.720703125, 0.580078125], + [0.720703125, 0.578125], + [0.728515625, 0.5703125], + [0.728515625, 0.568359375], + [0.732421875, 0.564453125], + [0.732421875, 0.5625], + [0.736328125, 0.55859375], + [0.736328125, 0.556640625], + [0.73828125, 0.5546875], + [0.73828125, 0.552734375], + [0.740234375, 0.55078125], + [0.740234375, 0.548828125], + [0.7421875, 0.546875], + [0.7421875, 0.544921875], + [0.744140625, 0.54296875], + [0.744140625, 0.525390625], + [0.7421875, 0.5234375], + [0.7421875, 0.521484375], + [0.740234375, 0.51953125], + [0.740234375, 0.517578125], + [0.73828125, 0.515625], + [0.73828125, 0.513671875], + [0.736328125, 0.51171875], + [0.736328125, 0.5078125], + [0.734375, 0.505859375], + [0.734375, 0.501953125], + [0.732421875, 0.5], + [0.732421875, 0.49609375], + [0.728515625, 0.4921875], + [0.724609375, 0.4921875], + [0.72265625, 0.490234375], + [0.720703125, 0.490234375], + [0.705078125, 0.474609375], + [0.705078125, 0.47265625], + [0.69921875, 0.466796875], + [0.69921875, 0.46484375], + [0.6953125, 0.4609375], + [0.6953125, 0.458984375], + [0.69140625, 0.455078125], + [0.69140625, 0.453125], + [0.689453125, 0.451171875], + [0.689453125, 0.44921875], + [0.6875, 0.447265625], + [0.6875, 0.4453125], + [0.68359375, 0.44140625], + [0.68359375, 0.439453125], + [0.681640625, 0.4375], + [0.681640625, 0.435546875], + [0.6796875, 0.43359375], + [0.6796875, 0.431640625], + [0.677734375, 0.4296875], + [0.677734375, 0.427734375], + [0.673828125, 0.423828125], + [0.673828125, 0.421875], + [0.669921875, 0.41796875], + [0.669921875, 0.416015625], + [0.662109375, 0.408203125], + [0.66015625, 0.408203125], + [0.658203125, 0.40625], + [0.65625, 0.40625], + [0.654296875, 0.404296875], + [0.650390625, 0.404296875], + [0.6484375, 0.40234375] + ], + [ + [0.2734375, 0.392578125], + [0.271484375, 0.39453125], + [0.26953125, 0.39453125], + [0.267578125, 0.396484375], + [0.265625, 0.396484375], + [0.2578125, 0.404296875], + [0.2578125, 0.40625], + [0.255859375, 0.408203125], + [0.255859375, 0.4140625], + [0.25390625, 0.416015625], + [0.25390625, 0.42578125], + [0.255859375, 0.427734375], + [0.255859375, 0.4296875], + [0.2578125, 0.431640625], + [0.2578125, 0.43359375], + [0.259765625, 0.43359375], + [0.26171875, 0.435546875], + [0.267578125, 0.435546875], + [0.275390625, 0.427734375], + [0.275390625, 0.42578125], + [0.27734375, 0.423828125], + [0.27734375, 0.419921875], + [0.279296875, 0.41796875], + [0.279296875, 0.39453125], + [0.27734375, 0.392578125] + ], + [ + [0.3828125, 0.287109375], + [0.380859375, 0.2890625], + [0.37890625, 0.2890625], + [0.375, 0.29296875], + [0.373046875, 0.29296875], + [0.36328125, 0.302734375], + [0.36328125, 0.3046875], + [0.361328125, 0.306640625], + [0.361328125, 0.31640625], + [0.36328125, 0.318359375], + [0.36328125, 0.3203125], + [0.3671875, 0.32421875], + [0.369140625, 0.32421875], + [0.37109375, 0.326171875], + [0.373046875, 0.326171875], + [0.375, 0.328125], + [0.37890625, 0.328125], + [0.380859375, 0.330078125], + [0.384765625, 0.330078125], + [0.38671875, 0.328125], + [0.388671875, 0.328125], + [0.390625, 0.326171875], + [0.390625, 0.32421875], + [0.392578125, 0.322265625], + [0.392578125, 0.3125], + [0.39453125, 0.310546875], + [0.39453125, 0.291015625], + [0.392578125, 0.2890625], + [0.390625, 0.2890625], + [0.388671875, 0.287109375] + ] + ], + "10": [ + [ + [0.439453125, 0.708984375], + [0.4375, 0.7109375], + [0.435546875, 0.7109375], + [0.435546875, 0.712890625], + [0.43359375, 0.71484375], + [0.43359375, 0.716796875], + [0.435546875, 0.71875], + [0.435546875, 0.720703125], + [0.4375, 0.72265625], + [0.439453125, 0.72265625], + [0.44140625, 0.724609375], + [0.443359375, 0.724609375], + [0.4453125, 0.7265625], + [0.45703125, 0.7265625], + [0.4609375, 0.72265625], + [0.4609375, 0.720703125], + [0.462890625, 0.71875], + [0.4609375, 0.716796875], + [0.4609375, 0.71484375], + [0.455078125, 0.708984375] + ], + [ + [0.27734375, 0.51953125], + [0.267578125, 0.529296875], + [0.267578125, 0.533203125], + [0.265625, 0.53515625], + [0.265625, 0.541015625], + [0.263671875, 0.54296875], + [0.263671875, 0.546875], + [0.265625, 0.548828125], + [0.265625, 0.5546875], + [0.26953125, 0.55859375], + [0.26953125, 0.560546875], + [0.271484375, 0.5625], + [0.2734375, 0.5625], + [0.27734375, 0.56640625], + [0.279296875, 0.56640625], + [0.28125, 0.568359375], + [0.283203125, 0.568359375], + [0.28515625, 0.5703125], + [0.287109375, 0.5703125], + [0.2890625, 0.572265625], + [0.298828125, 0.572265625], + [0.30078125, 0.5703125], + [0.302734375, 0.5703125], + [0.3046875, 0.568359375], + [0.3046875, 0.56640625], + [0.306640625, 0.564453125], + [0.306640625, 0.5625], + [0.30859375, 0.560546875], + [0.30859375, 0.556640625], + [0.310546875, 0.5546875], + [0.310546875, 0.53515625], + [0.30859375, 0.533203125], + [0.30859375, 0.53125], + [0.306640625, 0.529296875], + [0.306640625, 0.52734375], + [0.302734375, 0.5234375], + [0.30078125, 0.5234375], + [0.298828125, 0.521484375], + [0.294921875, 0.521484375], + [0.29296875, 0.51953125] + ], + [ + [0.673828125, 0.5078125], + [0.671875, 0.509765625], + [0.66015625, 0.509765625], + [0.658203125, 0.51171875], + [0.650390625, 0.51171875], + [0.6484375, 0.513671875], + [0.640625, 0.513671875], + [0.638671875, 0.515625], + [0.634765625, 0.515625], + [0.6328125, 0.517578125], + [0.62890625, 0.517578125], + [0.626953125, 0.51953125], + [0.625, 0.51953125], + [0.62109375, 0.5234375], + [0.619140625, 0.5234375], + [0.60546875, 0.537109375], + [0.603515625, 0.537109375], + [0.583984375, 0.556640625], + [0.583984375, 0.55859375], + [0.58203125, 0.560546875], + [0.58203125, 0.5625], + [0.580078125, 0.564453125], + [0.580078125, 0.568359375], + [0.578125, 0.5703125], + [0.578125, 0.57421875], + [0.576171875, 0.576171875], + [0.576171875, 0.62109375], + [0.572265625, 0.625], + [0.572265625, 0.626953125], + [0.564453125, 0.634765625], + [0.564453125, 0.63671875], + [0.556640625, 0.64453125], + [0.556640625, 0.646484375], + [0.552734375, 0.650390625], + [0.552734375, 0.65234375], + [0.55078125, 0.654296875], + [0.55078125, 0.65625], + [0.548828125, 0.658203125], + [0.548828125, 0.66015625], + [0.546875, 0.662109375], + [0.546875, 0.6640625], + [0.544921875, 0.666015625], + [0.544921875, 0.66796875], + [0.54296875, 0.669921875], + [0.54296875, 0.67578125], + [0.541015625, 0.677734375], + [0.541015625, 0.6875], + [0.544921875, 0.69140625], + [0.5546875, 0.69140625], + [0.556640625, 0.689453125], + [0.55859375, 0.689453125], + [0.56640625, 0.681640625], + [0.56640625, 0.677734375], + [0.568359375, 0.67578125], + [0.568359375, 0.671875], + [0.5703125, 0.669921875], + [0.5703125, 0.662109375], + [0.572265625, 0.66015625], + [0.572265625, 0.65625], + [0.57421875, 0.654296875], + [0.57421875, 0.65234375], + [0.580078125, 0.646484375], + [0.580078125, 0.64453125], + [0.5859375, 0.638671875], + [0.5859375, 0.63671875], + [0.595703125, 0.626953125], + [0.59765625, 0.626953125], + [0.599609375, 0.625], + [0.6015625, 0.625], + [0.603515625, 0.623046875], + [0.60546875, 0.623046875], + [0.607421875, 0.62109375], + [0.6171875, 0.62109375], + [0.619140625, 0.619140625], + [0.634765625, 0.619140625], + [0.63671875, 0.6171875], + [0.64453125, 0.6171875], + [0.646484375, 0.615234375], + [0.65234375, 0.615234375], + [0.654296875, 0.61328125], + [0.65625, 0.61328125], + [0.658203125, 0.611328125], + [0.66015625, 0.611328125], + [0.6640625, 0.607421875], + [0.666015625, 0.607421875], + [0.67578125, 0.59765625], + [0.67578125, 0.595703125], + [0.681640625, 0.58984375], + [0.681640625, 0.587890625], + [0.68359375, 0.5859375], + [0.68359375, 0.583984375], + [0.685546875, 0.58203125], + [0.685546875, 0.578125], + [0.6875, 0.576171875], + [0.6875, 0.57421875], + [0.689453125, 0.572265625], + [0.689453125, 0.56640625], + [0.69140625, 0.564453125], + [0.69140625, 0.544921875], + [0.693359375, 0.54296875], + [0.693359375, 0.509765625], + [0.69140625, 0.5078125] + ], + [ + [0.447265625, 0.484375], + [0.4453125, 0.486328125], + [0.4375, 0.486328125], + [0.435546875, 0.48828125], + [0.4296875, 0.48828125], + [0.427734375, 0.490234375], + [0.423828125, 0.490234375], + [0.421875, 0.4921875], + [0.419921875, 0.4921875], + [0.41796875, 0.494140625], + [0.416015625, 0.494140625], + [0.404296875, 0.505859375], + [0.404296875, 0.5078125], + [0.3984375, 0.513671875], + [0.3984375, 0.515625], + [0.396484375, 0.517578125], + [0.39453125, 0.517578125], + [0.388671875, 0.5234375], + [0.38671875, 0.5234375], + [0.3828125, 0.52734375], + [0.380859375, 0.52734375], + [0.37890625, 0.529296875], + [0.376953125, 0.529296875], + [0.375, 0.53125], + [0.373046875, 0.53125], + [0.37109375, 0.533203125], + [0.369140625, 0.533203125], + [0.365234375, 0.537109375], + [0.36328125, 0.537109375], + [0.359375, 0.541015625], + [0.359375, 0.54296875], + [0.357421875, 0.544921875], + [0.357421875, 0.556640625], + [0.359375, 0.55859375], + [0.359375, 0.560546875], + [0.365234375, 0.56640625], + [0.365234375, 0.568359375], + [0.3671875, 0.5703125], + [0.369140625, 0.5703125], + [0.380859375, 0.58203125], + [0.380859375, 0.583984375], + [0.384765625, 0.587890625], + [0.384765625, 0.59375], + [0.38671875, 0.595703125], + [0.38671875, 0.619140625], + [0.388671875, 0.62109375], + [0.388671875, 0.623046875], + [0.390625, 0.625], + [0.390625, 0.626953125], + [0.39453125, 0.630859375], + [0.39453125, 0.6328125], + [0.396484375, 0.634765625], + [0.3984375, 0.634765625], + [0.40234375, 0.638671875], + [0.404296875, 0.638671875], + [0.40625, 0.640625], + [0.41015625, 0.640625], + [0.412109375, 0.642578125], + [0.421875, 0.642578125], + [0.423828125, 0.640625], + [0.4296875, 0.640625], + [0.43359375, 0.63671875], + [0.435546875, 0.63671875], + [0.44140625, 0.630859375], + [0.44140625, 0.62890625], + [0.443359375, 0.626953125], + [0.443359375, 0.625], + [0.4453125, 0.623046875], + [0.4453125, 0.6171875], + [0.447265625, 0.615234375], + [0.447265625, 0.55078125], + [0.44921875, 0.548828125], + [0.44921875, 0.53515625], + [0.451171875, 0.533203125], + [0.451171875, 0.525390625], + [0.453125, 0.5234375], + [0.453125, 0.51953125], + [0.455078125, 0.517578125], + [0.455078125, 0.513671875], + [0.45703125, 0.51171875], + [0.45703125, 0.5078125], + [0.458984375, 0.505859375], + [0.458984375, 0.501953125], + [0.4609375, 0.5], + [0.4609375, 0.490234375], + [0.455078125, 0.484375] + ], + [ + [0.5703125, 0.482421875], + [0.568359375, 0.484375], + [0.56640625, 0.484375], + [0.552734375, 0.498046875], + [0.552734375, 0.5], + [0.548828125, 0.50390625], + [0.548828125, 0.505859375], + [0.546875, 0.5078125], + [0.546875, 0.509765625], + [0.544921875, 0.51171875], + [0.544921875, 0.513671875], + [0.54296875, 0.515625], + [0.54296875, 0.517578125], + [0.541015625, 0.51953125], + [0.541015625, 0.5234375], + [0.5390625, 0.525390625], + [0.5390625, 0.52734375], + [0.537109375, 0.529296875], + [0.537109375, 0.533203125], + [0.53515625, 0.53515625], + [0.53515625, 0.5390625], + [0.533203125, 0.541015625], + [0.533203125, 0.544921875], + [0.53125, 0.546875], + [0.53125, 0.55078125], + [0.529296875, 0.552734375], + [0.529296875, 0.556640625], + [0.52734375, 0.55859375], + [0.52734375, 0.564453125], + [0.525390625, 0.56640625], + [0.525390625, 0.5703125], + [0.5234375, 0.572265625], + [0.5234375, 0.580078125], + [0.521484375, 0.58203125], + [0.521484375, 0.60546875], + [0.51953125, 0.607421875], + [0.51953125, 0.623046875], + [0.525390625, 0.62890625], + [0.52734375, 0.62890625], + [0.529296875, 0.630859375], + [0.54296875, 0.630859375], + [0.544921875, 0.62890625], + [0.546875, 0.62890625], + [0.548828125, 0.626953125], + [0.548828125, 0.625], + [0.55078125, 0.623046875], + [0.55078125, 0.62109375], + [0.552734375, 0.619140625], + [0.552734375, 0.615234375], + [0.5546875, 0.61328125], + [0.5546875, 0.6015625], + [0.556640625, 0.599609375], + [0.556640625, 0.58984375], + [0.55859375, 0.587890625], + [0.55859375, 0.58203125], + [0.560546875, 0.580078125], + [0.560546875, 0.576171875], + [0.5625, 0.57421875], + [0.5625, 0.572265625], + [0.564453125, 0.5703125], + [0.564453125, 0.56640625], + [0.56640625, 0.564453125], + [0.56640625, 0.560546875], + [0.568359375, 0.55859375], + [0.568359375, 0.5546875], + [0.5703125, 0.552734375], + [0.5703125, 0.54296875], + [0.572265625, 0.541015625], + [0.572265625, 0.509765625], + [0.57421875, 0.5078125], + [0.57421875, 0.484375], + [0.572265625, 0.482421875] + ], + [ + [0.404296875, 0.41015625], + [0.40234375, 0.412109375], + [0.400390625, 0.412109375], + [0.3984375, 0.4140625], + [0.3984375, 0.416015625], + [0.396484375, 0.41796875], + [0.396484375, 0.421875], + [0.3984375, 0.423828125], + [0.3984375, 0.42578125], + [0.40234375, 0.4296875], + [0.404296875, 0.4296875], + [0.40625, 0.431640625], + [0.408203125, 0.431640625], + [0.41015625, 0.43359375], + [0.4140625, 0.43359375], + [0.416015625, 0.435546875], + [0.423828125, 0.435546875], + [0.42578125, 0.43359375], + [0.42578125, 0.423828125], + [0.423828125, 0.421875], + [0.423828125, 0.419921875], + [0.416015625, 0.412109375], + [0.412109375, 0.412109375], + [0.41015625, 0.41015625] + ] + ] + } +} diff --git a/tests/assets/car_tree_bug/annotations/instances_test.json b/tests/assets/car_tree_bug/annotations/instances_test.json deleted file mode 100644 index c99563c75f1..00000000000 --- a/tests/assets/car_tree_bug/annotations/instances_test.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "licenses": [{ "name": "", "id": 0, "url": "" }], - "info": { - "contributor": "", - "date_created": "", - "description": "", - "url": "", - "version": "", - "year": "" - }, - "categories": [ - { "id": 1, "name": "car", "supercategory": "" }, - { "id": 2, "name": "tree", "supercategory": "" }, - { "id": 3, "name": "bug", "supercategory": "" } - ], - "images": [ - { - "id": 7, - "width": 1280, - "height": 720, - "file_name": "Slide3.PNG", - "license": 0, - "flickr_url": "", - "coco_url": "", - "date_captured": 0 - }, - { - "id": 8, - "width": 1280, - "height": 720, - "file_name": "Slide20.PNG", - "license": 0, - "flickr_url": "", - "coco_url": "", - "date_captured": 0 - } - ], - "annotations": [ - { - "id": 19, - "image_id": 7, - "category_id": 1, - "segmentation": [ - [184.09, 131.61, 338.06, 129.89, 339.78, 457.63, 183.23, 461.08] - ], - "area": 51030.0, - "bbox": [183.23, 129.89, 156.55, 331.19], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 20, - "image_id": 7, - "category_id": 2, - "segmentation": [ - [832.69, 104.09, 1018.49, 102.37, 1017.63, 226.24, 825.81, 233.98] - ], - "area": 23933.0, - "bbox": [825.81, 102.37, 192.68, 131.61], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 21, - "image_id": 7, - "category_id": 3, - "segmentation": [ - [898.92, 490.32, 1195.7, 487.74, 1209.46, 673.55, 913.55, 670.11] - ], - "area": 54157.0, - "bbox": [898.92, 487.74, 310.54, 185.81], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 22, - "image_id": 8, - "category_id": 3, - "segmentation": [ - [341.51, 373.33, 502.4, 456.8, 341.5, 709.7, 188.39, 612.47] - ], - "area": 52814.0, - "bbox": [188.39, 373.33, 314.01, 336.37], - "iscrowd": 0, - "attributes": { "occluded": false } - } - ] -} diff --git a/tests/assets/car_tree_bug/annotations/instances_train_5_imgs.json b/tests/assets/car_tree_bug/annotations/instances_train_5_imgs.json new file mode 100644 index 00000000000..c891204a4fc --- /dev/null +++ b/tests/assets/car_tree_bug/annotations/instances_train_5_imgs.json @@ -0,0 +1,248 @@ +{ + "licenses": [{ "name": "", "id": 0, "url": "" }], + "info": { + "contributor": "", + "date_created": "", + "description": "", + "url": "", + "version": "", + "year": "" + }, + "categories": [ + { "id": 1, "name": "car", "supercategory": "" }, + { "id": 2, "name": "tree", "supercategory": "" }, + { "id": 3, "name": "bug", "supercategory": "" } + ], + "images": [ + { + "id": 1, + "width": 1280, + "height": 720, + "file_name": "Slide9.PNG", + "license": 0, + "flickr_url": "", + "coco_url": "", + "date_captured": 0 + }, + { + "id": 2, + "width": 1280, + "height": 720, + "file_name": "Slide8.PNG", + "license": 0, + "flickr_url": "", + "coco_url": "", + "date_captured": 0 + }, + { + "id": 3, + "width": 1280, + "height": 720, + "file_name": "Slide7.PNG", + "license": 0, + "flickr_url": "", + "coco_url": "", + "date_captured": 0 + }, + { + "id": 4, + "width": 1280, + "height": 720, + "file_name": "Slide6.PNG", + "license": 0, + "flickr_url": "", + "coco_url": "", + "date_captured": 0 + }, + { + "id": 5, + "width": 1280, + "height": 720, + "file_name": "Slide5.PNG", + "license": 0, + "flickr_url": "", + "coco_url": "", + "date_captured": 0 + } + ], + "annotations": [ + { + "id": 1, + "image_id": 1, + "category_id": 3, + "segmentation": [ + [17.2, 166.88, 203.87, 7.74, 410.32, 43.87, 117.85, 331.18] + ], + "area": 58273.0, + "bbox": [17.2, 7.74, 393.12, 323.44], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 2, + "image_id": 1, + "category_id": 1, + "segmentation": [ + [294.19, 281.29, 643.44, 300.22, 628.82, 469.68, 277.85, 449.03] + ], + "area": 59331.0, + "bbox": [277.85, 281.29, 365.59, 188.39], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 3, + "image_id": 1, + "category_id": 2, + "segmentation": [ + [114.41, 499.79, 30.97, 670.11, 151.4, 705.38, 240.86, 536.77] + ], + "area": 24033.0, + "bbox": [30.97, 499.79, 209.89, 205.59], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 4, + "image_id": 2, + "category_id": 1, + "segmentation": [[165.16, 2.58, 344.95, 41.29, 27.5, 363.0, 9.46, 147.1]], + "area": 53173.0, + "bbox": [9.46, 2.58, 335.49, 360.42], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 5, + "image_id": 2, + "category_id": 2, + "segmentation": [ + [524.73, 378.49, 648.6, 227.96, 762.15, 298.49, 627.96, 458.49] + ], + "area": 26526.0, + "bbox": [524.73, 227.96, 237.42, 230.53], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 6, + "image_id": 2, + "category_id": 3, + "segmentation": [ + [946.24, 652.9, 1191.4, 356.13, 1274.8, 576.3, 1092.5, 715.7] + ], + "area": 55317.0, + "bbox": [946.24, 356.13, 328.56, 359.57], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 7, + "image_id": 3, + "category_id": 2, + "segmentation": [ + [584.95, 221.94, 715.7, 223.66, 706.24, 411.18, 583.23, 413.76] + ], + "area": 24074.0, + "bbox": [583.23, 221.94, 132.47, 191.82], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 8, + "image_id": 3, + "category_id": 1, + "segmentation": [ + [826.67, 222.8, 966.9, 176.3, 1081.29, 489.46, 931.61, 542.8] + ], + "area": 51362.0, + "bbox": [826.67, 176.3, 254.62, 366.5], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 9, + "image_id": 3, + "category_id": 3, + "segmentation": [ + [698.49, 384.52, 864.52, 390.54, 872.26, 688.17, 683.01, 683.01] + ], + "area": 52982.0, + "bbox": [683.01, 384.52, 189.25, 303.65], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 10, + "image_id": 4, + "category_id": 1, + "segmentation": [ + [69.68, 11.18, 67.1, 336.34, 213.33, 338.92, 222.8, 10.32] + ], + "area": 48945.0, + "bbox": [67.1, 10.32, 155.7, 328.6], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 11, + "image_id": 4, + "category_id": 2, + "segmentation": [ + [569.46, 70.54, 688.17, 70.54, 683.01, 262.37, 559.14, 263.23] + ], + "area": 23273.0, + "bbox": [559.14, 70.54, 129.03, 192.69], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 12, + "image_id": 4, + "category_id": 3, + "segmentation": [ + [972.04, 116.13, 1265.38, 95.48, 1274.84, 295.05, 974.62, 292.47] + ], + "area": 55841.0, + "bbox": [972.04, 95.48, 302.8, 199.57], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 13, + "image_id": 5, + "category_id": 3, + "segmentation": [ + [200.43, 336.34, 385.38, 334.62, 382.8, 635.7, 206.45, 638.28] + ], + "area": 54478.0, + "bbox": [200.43, 334.62, 184.95, 303.66], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 14, + "image_id": 5, + "category_id": 2, + "segmentation": [ + [594.41, 523.01, 779.35, 523.87, 778.49, 643.44, 590.97, 645.16] + ], + "area": 22525.0, + "bbox": [590.97, 523.01, 188.38, 122.15], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 15, + "image_id": 5, + "category_id": 1, + "segmentation": [ + [1101.1, 304.6, 1230.1, 389.6, 1058.1, 665.8, 929.03, 581.51] + ], + "area": 50271.0, + "bbox": [929.03, 304.6, 301.07, 361.2], + "iscrowd": 0, + "attributes": { "occluded": false } + } + ] +} diff --git a/tests/assets/car_tree_bug/annotations/instances_val.json b/tests/assets/car_tree_bug/annotations/instances_val.json index c99563c75f1..3231021483d 100644 --- a/tests/assets/car_tree_bug/annotations/instances_val.json +++ b/tests/assets/car_tree_bug/annotations/instances_val.json @@ -28,7 +28,17 @@ "id": 8, "width": 1280, "height": 720, - "file_name": "Slide20.PNG", + "file_name": "Slide4.PNG", + "license": 0, + "flickr_url": "", + "coco_url": "", + "date_captured": 0 + }, + { + "id": 9, + "width": 1280, + "height": 720, + "file_name": "Slide5.PNG", "license": 0, "flickr_url": "", "coco_url": "", diff --git a/tests/assets/car_tree_bug/annotations/instances_val_1_imgs.json b/tests/assets/car_tree_bug/annotations/instances_val_1_imgs.json new file mode 100644 index 00000000000..13458f3b5c3 --- /dev/null +++ b/tests/assets/car_tree_bug/annotations/instances_val_1_imgs.json @@ -0,0 +1,86 @@ +{ + "licenses": [{ "name": "", "id": 0, "url": "" }], + "info": { + "contributor": "", + "date_created": "", + "description": "", + "url": "", + "version": "", + "year": "" + }, + "categories": [ + { "id": 1, "name": "car", "supercategory": "" }, + { "id": 2, "name": "tree", "supercategory": "" }, + { "id": 3, "name": "bug", "supercategory": "" } + ], + "images": [ + { + "id": 7, + "width": 1280, + "height": 720, + "file_name": "Slide3.PNG", + "license": 0, + "flickr_url": "", + "coco_url": "", + "date_captured": 0 + }, + { + "id": 8, + "width": 1280, + "height": 720, + "file_name": "Slide4.PNG", + "license": 0, + "flickr_url": "", + "coco_url": "", + "date_captured": 0 + }, + { + "id": 9, + "width": 1280, + "height": 720, + "file_name": "Slide5.PNG", + "license": 0, + "flickr_url": "", + "coco_url": "", + "date_captured": 0 + } + ], + "annotations": [ + { + "id": 19, + "image_id": 7, + "category_id": 1, + "segmentation": [ + [184.09, 131.61, 338.06, 129.89, 339.78, 457.63, 183.23, 461.08] + ], + "area": 51030.0, + "bbox": [183.23, 129.89, 156.55, 331.19], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 20, + "image_id": 7, + "category_id": 2, + "segmentation": [ + [832.69, 104.09, 1018.49, 102.37, 1017.63, 226.24, 825.81, 233.98] + ], + "area": 23933.0, + "bbox": [825.81, 102.37, 192.68, 131.61], + "iscrowd": 0, + "attributes": { "occluded": false } + }, + { + "id": 21, + "image_id": 7, + "category_id": 3, + "segmentation": [ + [898.92, 490.32, 1195.7, 487.74, 1209.46, 673.55, 913.55, 670.11] + ], + "area": 54157.0, + "bbox": [898.92, 487.74, 310.54, 185.81], + "iscrowd": 0, + "attributes": { "occluded": false } + } + ] +} diff --git a/tests/assets/car_tree_bug/images/test/Slide20.PNG b/tests/assets/car_tree_bug/images/test/Slide20.PNG deleted file mode 100644 index 22960a010347823b0149f7cefe9c434e187a8167..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6123 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 Oy=7=}z3eLb(SHGVSUFPw diff --git a/tests/assets/car_tree_bug/images/test/Slide3.PNG b/tests/assets/car_tree_bug/images/test/Slide3.PNG deleted file mode 100644 index 6cd6e10f7021d538cd38ccd3a5603db5ee0c0166..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21103 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 Oy=7=}z3eLb(SHGVSUFPw diff --git a/tests/assets/car_tree_bug_zero_shot/images/test/Slide4.PNG b/tests/assets/car_tree_bug/images/val/Slide4.PNG similarity index 100% rename from tests/assets/car_tree_bug_zero_shot/images/test/Slide4.PNG rename to tests/assets/car_tree_bug/images/val/Slide4.PNG diff --git a/tests/assets/car_tree_bug/images/val/Slide5.PNG b/tests/assets/car_tree_bug/images/val/Slide5.PNG new file mode 100644 index 0000000000000000000000000000000000000000..2f1d60994449f5a6816761445594e9d33870de31 GIT binary patch literal 10729 zcmeI&F-t;G6u|Lw)$(arxr!g4HVIsVXHWEnKICab4N?%aMPNcxNt=uH1*8uUC{C{F z2XL{?*&#@GA$lFZL?HYxT+TU{3+LY7U2nVM6%z4OTtpIX(eXu+8Ijn#71otScd*c4 z8h#-sy-E5@i^z>#wnYXnJ3m)jT5nfMwR{i+nx2iPj~YVvzE=%oKFpkI6}u~*H$;v; z*3%qZU(7@f>#k$h9_rsCV{}XF^|!yYLYgrUKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0V6z2o_jWD4!G2`*)I8xcz-B)tr$+z*1Q0*~fqxZfMYG3EF>jsE O)Vi;m_ncnt^7#+F4lqXm literal 0 HcmV?d00001 diff --git a/tests/assets/car_tree_bug_zero_shot/annotations/instances_test.json b/tests/assets/car_tree_bug_zero_shot/annotations/instances_test.json deleted file mode 100644 index bdbfe9331bf..00000000000 --- a/tests/assets/car_tree_bug_zero_shot/annotations/instances_test.json +++ /dev/null @@ -1,316 +0,0 @@ -{ - "licenses": [{ "name": "", "id": 0, "url": "" }], - "info": { - "contributor": "", - "date_created": "", - "description": "", - "url": "", - "version": "", - "year": "" - }, - "categories": [ - { "id": 1, "name": "car", "supercategory": "" }, - { "id": 2, "name": "tree", "supercategory": "" }, - { "id": 3, "name": "bug", "supercategory": "" } - ], - "images": [ - { - "id": 7, - "width": 1280, - "height": 720, - "file_name": "Slide3.PNG", - "license": 0, - "flickr_url": "", - "coco_url": "", - "date_captured": 0 - }, - { - "id": 8, - "width": 1280, - "height": 720, - "file_name": "Slide4.PNG", - "license": 0, - "flickr_url": "", - "coco_url": "", - "date_captured": 0 - }, - { - "id": 1, - "width": 1280, - "height": 720, - "file_name": "Slide9.PNG", - "license": 0, - "flickr_url": "", - "coco_url": "", - "date_captured": 0 - }, - { - "id": 2, - "width": 1280, - "height": 720, - "file_name": "Slide8.PNG", - "license": 0, - "flickr_url": "", - "coco_url": "", - "date_captured": 0 - }, - { - "id": 3, - "width": 1280, - "height": 720, - "file_name": "Slide7.PNG", - "license": 0, - "flickr_url": "", - "coco_url": "", - "date_captured": 0 - }, - { - "id": 4, - "width": 1280, - "height": 720, - "file_name": "Slide6.PNG", - "license": 0, - "flickr_url": "", - "coco_url": "", - "date_captured": 0 - }, - { - "id": 5, - "width": 1280, - "height": 720, - "file_name": "Slide5.PNG", - "license": 0, - "flickr_url": "", - "coco_url": "", - "date_captured": 0 - } - ], - "annotations": [ - { - "id": 19, - "image_id": 7, - "category_id": 1, - "segmentation": [ - [184.09, 131.61, 338.06, 129.89, 339.78, 457.63, 183.23, 461.08] - ], - "area": 51030.0, - "bbox": [183.23, 129.89, 156.55, 331.19], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 20, - "image_id": 7, - "category_id": 2, - "segmentation": [ - [832.69, 104.09, 1018.49, 102.37, 1017.63, 226.24, 825.81, 233.98] - ], - "area": 23933.0, - "bbox": [825.81, 102.37, 192.68, 131.61], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 21, - "image_id": 7, - "category_id": 3, - "segmentation": [ - [898.92, 490.32, 1195.7, 487.74, 1209.46, 673.55, 913.55, 670.11] - ], - "area": 54157.0, - "bbox": [898.92, 487.74, 310.54, 185.81], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 22, - "image_id": 8, - "category_id": 3, - "segmentation": [ - [341.51, 373.33, 502.4, 456.8, 341.5, 709.7, 188.39, 612.47] - ], - "area": 52814.0, - "bbox": [188.39, 373.33, 314.01, 336.37], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 1, - "image_id": 1, - "category_id": 3, - "segmentation": [ - [17.2, 166.88, 203.87, 7.74, 410.32, 43.87, 117.85, 331.18] - ], - "area": 58273.0, - "bbox": [17.2, 7.74, 393.12, 323.44], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 2, - "image_id": 1, - "category_id": 1, - "segmentation": [ - [294.19, 281.29, 643.44, 300.22, 628.82, 469.68, 277.85, 449.03] - ], - "area": 59331.0, - "bbox": [277.85, 281.29, 365.59, 188.39], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 3, - "image_id": 1, - "category_id": 2, - "segmentation": [ - [114.41, 499.79, 30.97, 670.11, 151.4, 705.38, 240.86, 536.77] - ], - "area": 24033.0, - "bbox": [30.97, 499.79, 209.89, 205.59], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 4, - "image_id": 2, - "category_id": 1, - "segmentation": [[165.16, 2.58, 344.95, 41.29, 27.5, 363.0, 9.46, 147.1]], - "area": 53173.0, - "bbox": [9.46, 2.58, 335.49, 360.42], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 5, - "image_id": 2, - "category_id": 2, - "segmentation": [ - [524.73, 378.49, 648.6, 227.96, 762.15, 298.49, 627.96, 458.49] - ], - "area": 26526.0, - "bbox": [524.73, 227.96, 237.42, 230.53], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 6, - "image_id": 2, - "category_id": 3, - "segmentation": [ - [946.24, 652.9, 1191.4, 356.13, 1274.8, 576.3, 1092.5, 715.7] - ], - "area": 55317.0, - "bbox": [946.24, 356.13, 328.56, 359.57], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 7, - "image_id": 3, - "category_id": 2, - "segmentation": [ - [584.95, 221.94, 715.7, 223.66, 706.24, 411.18, 583.23, 413.76] - ], - "area": 24074.0, - "bbox": [583.23, 221.94, 132.47, 191.82], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 8, - "image_id": 3, - "category_id": 1, - "segmentation": [ - [826.67, 222.8, 966.9, 176.3, 1081.29, 489.46, 931.61, 542.8] - ], - "area": 51362.0, - "bbox": [826.67, 176.3, 254.62, 366.5], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 9, - "image_id": 3, - "category_id": 3, - "segmentation": [ - [698.49, 384.52, 864.52, 390.54, 872.26, 688.17, 683.01, 683.01] - ], - "area": 52982.0, - "bbox": [683.01, 384.52, 189.25, 303.65], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 10, - "image_id": 4, - "category_id": 1, - "segmentation": [ - [69.68, 11.18, 67.1, 336.34, 213.33, 338.92, 222.8, 10.32] - ], - "area": 48945.0, - "bbox": [67.1, 10.32, 155.7, 328.6], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 11, - "image_id": 4, - "category_id": 2, - "segmentation": [ - [569.46, 70.54, 688.17, 70.54, 683.01, 262.37, 559.14, 263.23] - ], - "area": 23273.0, - "bbox": [559.14, 70.54, 129.03, 192.69], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 12, - "image_id": 4, - "category_id": 3, - "segmentation": [ - [972.04, 116.13, 1265.38, 95.48, 1274.84, 295.05, 974.62, 292.47] - ], - "area": 55841.0, - "bbox": [972.04, 95.48, 302.8, 199.57], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 13, - "image_id": 5, - "category_id": 3, - "segmentation": [ - [200.43, 336.34, 385.38, 334.62, 382.8, 635.7, 206.45, 638.28] - ], - "area": 54478.0, - "bbox": [200.43, 334.62, 184.95, 303.66], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 14, - "image_id": 5, - "category_id": 2, - "segmentation": [ - [594.41, 523.01, 779.35, 523.87, 778.49, 643.44, 590.97, 645.16] - ], - "area": 22525.0, - "bbox": [590.97, 523.01, 188.38, 122.15], - "iscrowd": 0, - "attributes": { "occluded": false } - }, - { - "id": 15, - "image_id": 5, - "category_id": 1, - "segmentation": [ - [1101.1, 304.6, 1230.1, 389.6, 1058.1, 665.8, 929.03, 581.51] - ], - "area": 50271.0, - "bbox": [929.03, 304.6, 301.07, 361.2], - "iscrowd": 0, - "attributes": { "occluded": false } - } - ] -} diff --git a/tests/assets/car_tree_bug_zero_shot/images/test/Slide3.PNG b/tests/assets/car_tree_bug_zero_shot/images/test/Slide3.PNG deleted file mode 100644 index 6cd6e10f7021d538cd38ccd3a5603db5ee0c0166..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21103 zcmeHvXHb+~)8-(e0t!kHktB}-Dgu%vBdDlEMM1Ji1_6O>;3VR56m6T>Aw2v?yJubsHUQDhTtl#zr~NZRdiS-C4?be{@FW)yi+<%}NPk(K53IY#m2s(G&YG7DcC-NVc>D(WGM zf1W-Jl03GB7F(AYU!J`=%Jjf#SKB?mb75af+W1^}_27hSl82rZe$Wex1ZxXBcI*NS z=KbVsE%@Q959|aCc8UJ@P4GkJC2z2^`(z}i!7i_#dJcZ|_%@6b+(Pl6TmECQ|3iSG zs88~&EL|J=fsJi&mUe2Tr@Ss>IxjQ!eL8UTC)c`%y8=3t1j1*a zbg?caN{Y1IJ#F*md*~PXCPwnN4m^SRCe3fsaf3*f)vw;$iV*H?u`-MDn{UhN8Z!Ht zWxqU+&bLHWn&NxZ7P~K>c<%>j;F9WY^+87&!;?X<74S~2x!7;yCi5Fdn5>o~v6u|!F1l^$x9#13T|Jki+4OD~IzF-ehp-XI6WF)~(|+*V<3xfqic zD7|q|S3bLDpOvw5P}jn(OMkindP;Qc%Vz;hN$(^?@Y`N*36LX`B z_#zwB_S;C!XDk;fqM?0ZqVUEV{lzupRo#!Qpm$`)3s1&L+_QLoRSkLub8Fwa@6!cS zGGn{8hBB(YTD8v*-pZeh?Zh*lIZgHu+OwdHo88@=Vt9idB9{Ej*!R~j(8X_#u|u8+ z&Xo|+7b-!AR|YLrAVcF?*Cg`i(+h={qwlG*UOi5H#ZZy$VA!>^)M&cqxXvblnRP}X z2g}8D0~vNve0T#6apP>bvhHJC@YuFD{SxBzLgMEB1G0l&a^e|1% zxuw+H$9LhT-CCHr0<0NAgLx!a16Azf*TbH}CNW8w<*v2WsyrW@PKG^)ULWO7_&xQp zm!Ocvi^osuR!k?nUsiOgslGgNP4+S5FYUEm1)H2ittfimYYC4iPl}(r7_JIAN$z06 zGh-SXzKJ>Al2zxR0?f!)gEKC+^1ed@0AAd+A{=35t32M)$tIk7p8A5sHq$zr81pL2 zC0A%uJO!SZY$UF1jf%i3^|Ti&y@%85raBg`06m)~B05B6D%eW~-pIqp<|OZR@(p_d zbXR++88XjS56RtLCXM~ZXxCcXBr0`bmGhF}@td2Kh%NN2D*73Tw~Wpf%0ZI=M}5$i zoNV!{|1y63$_E%HoShG&za9F36`)@M+BmQx2yU~IWODmq&xJFIBcoDp*ki$?Xlr3= z&yw(S6;q;>m~{n}@v%Zb8SL%iBJV5bYJY+K%Np(*lnwUr(bTeuMd#hhFx7Vbw+}x| z$h>x04hc5YkhIf@VOrZCeP`a~QKGF}2N|#~1s3X)I?+P@!|f z0$;Wc7anPleR1mfG?zEYB?9on4B!E5$7+3zt2O3a*{8wwi)w&eVz75jjH0Ms#&s)x zS!L*Cyda%Bjn!vRm0AC?9vz}5a+7<%ty^6*1Hp7m{uueu(`*5g-U5VYnI5Srg3hci zF+h#xygv9vzNq7FmHghHl|kR#Y3D=FN6JJ>JUgw^3W#?F36}e(NtAHF^s~3`Bf%y# z#=9y342gvGz2tc#Qt)cu`rTf)Gf6#qC(PPSFo~LWp3MgeR0{5F_fH(%TAZnbrNYD4 zMOym!6;HNIQg^(IZ`C_-`oU%Iz+~77ZxJYvU-qJ<+%5|4H8!3um31(^*D0|M7&1to z{1W4lKf2BjRqAOw)AM&z8K9G0pwP009_N}dq!7-O|@?QB|VYV`5HtDNoH9nT+7;?4(_ z(FmP$y~lfl$wRN~QBZt!i$ixC#Qgxm-PyS)*7gxU+w~NFdMd$gRrMT+Q_?EX^tG3 z1Di}{=d(FvLMl(1u{3f2*oE+WFj^T9COl4nMXhYE#k5+tWoz#lR=&g-%B2#TerLg@R=?Qh0Ukkj1%^Q2zDjN?_ubhIqndfvRvaG>T$D5>7# zJpO96*#1j2#a}-Z*AB2S@x^IBe#fCDRz3L6)w4+CJo1B{$i`B`GHtPJNyqHu{@H-to;F@2(*@_|~4ZnS~k@l^_kM;C} z(RA2rzu3LyPncQXNGH$le)IMqb@0EQ)e0C~6gPiN2eWtnS?U8BqMae8#W~M{d96#GY+wM%SEHR8qEk4V6|UZONG7o559aWYNr5!!FjZm}Ko|h&jxgnq68@J(oAN4BkZIiS z7Y1$vl<}o!s0&tNHioO~*+4SMN9-qm0dD%kYRr{T?&BYq!2E31z zkqzj>Dl5k)30Q%zBE@W=@mS)9?g`b!35Pw=w%_G*P)$_*QdZ#9^DS%4+PJFpzOlS~ z{cLl@$uk!n2DHo)52INS*jV0bX|JBAcxl@Cx+$+RSDa=cjnKroTG*8)z0giZ{(M_? z6MFOqeSy!f-T22&EJ7s=;0pvw1{WF_w16q1IuAvpS$F8TFi>omuXXUhS^nGbu5@aoL;Xeh5gbE}IGeh>^L?{wz%GWaJ&oOzh2Y zyJs=b2)Vf0m&Pi(41R;wai;P64pX6K)s4jNPF{AS@ezb0`%8?@kHb3NC zZC?N_W4IAWhzyD8;*MHFK%T;L)@yx7Z&J>7x2l#+wsIkPe3nMN2}|&`v*rq;}p-)W3O5=-X#S zY014H8W-NtjHa+#^*6b$s*zO zLA)f`QLIL9V&L6*fP{9CsLjS5GQOvGb$YRue}B30X${zv%gTKKSm7dxlFiCc!AxnRO5ZqV|e2n1S}|EA<(E?uXm_1GX1Ppsd{d=Iu#p_!OnVyDT9D z0Esm9f*d9ApecxmC&Fl!vZ_n#0ewHj`#!B1MV+4={}5UY&pmdA{0g{pYip}2aUXXZ zY-+DmpH>lAN7ib9kHt>$wutd+f;1b#>4^ZBgM0hR1igA_CcOU0Q&xKL{kcSO$M-LU zs8~kXG_gpPmWKmlA+QsdVjJ5lCgs;<<7b<7@l1#DB~nwgmb+h=+%!6`;`g5Bha@Sk zNt^vlsOwfF1D;BM%IZ4%u~dJ1|Cz&D*V>4^bo|p?F=k+10qgOC%`cf8ekGrt%(Yir zE_mz(+tW%w#VLRdh?dro!%f=5ergT#pidHRW6#f*}grJw>xn3h^{Ltcdj=!*BfBD<|bq!~o?U2;QTM)Z-pONCuz=-em zCWR(-+jqEc&T*v+kfhK)I3!y=v=q z@QYUULysV%c(Ftt&Q`VAaxmcoaKRM3YV&Z34Xyn8sm8<@xBU$A>{7qrL!DO-tS3zS zrb+14D?u1)T7_hbvPvcONVvH|WM8b-1IW<+G zKr&oc?~A16nro=7+f7iOK}-DQ4C0!KMf^^8?9J*!P7LNa2q`*W&_UNY?Q_)I@+$r= z?@F-<=v>m<#Iv#yTpDk%JOgEk-(V8UW3f(6fq;`DuMM?O6C0Q8-EUNYeh4^~w)*DL zl5G#?ZNH2uC8@BCRJ-oPYLSh$fUu(rt7J{K@9yh4KUT_BM*H{=mZciyE2p;Qd8ljh zbneQBR*a4~Dd#I;L^Lq7c~j^7k9A^wHB|O|qTBa1&~7l0c`Zr-8C3ISu<~7?p$hP|(G_reh`AXlxF;!C{wNVd4R<+%%+)<{lI9+)_az>?6C@{?DeWj1kmF-L2ND~_csFB0{Z5+P zBSmCt2E5KY`+WZ$&^WP@C>15bv5Sw)IiG3NmA(ed(ZC*2*MG*OK0UI(eNo2k0i|X$ zMJ^Z@_r6AXnf@ zC-&yzb`%xbK3L~2epLp>802GPTYYiq^2Z<{7XD902HbZ%-r5=9D2)W{IccPXSYN6T!x#D7e5#K4 zwh}%}fC}?&R+8|Cz+@4e%I|@nT2y4jvY+dSfu2yh5tQiJOf8MPii?3=DscZ}PzZ4- zMma3?U9)VzlO?{a{wG2=HU+rk0_fV_*MCVq(rg5{WaLxM)(VYM@>PY+`h8Yi8Cpx< zmPvm4Jr|@FSTd@k$%YZGjwseU?Xj8gW{?+gWrR$2njG-fY=mmaYe83)TQzz%>u>1p z!ANTh%%6!B%tt|L&M$Su^I8<6fea+};okhBtu4M2K3LbXPw@ARH8h(#H zF1R!RIyAnx9{0`~z3zyU6mMYZ9IP&rZ;X@3#xv{fGoPke* zl5VQq0@=Y~t$e`=6n9G$Fo0zsR;gn7Q2p} zq*_D3_xF}8@Vw;*2|3|6en9HT$D&R~!I@-dAWFHp$&%327LTosS4P-#yo#O~m3m5; z%X!t_58}|g;AAo)8)%$MKpo{UA!!H33Q*>*1kxxZJV|kXz6so6()0DfgInh+JS%5f z2)mzhEZe7`dxN3q0BR;*S8vKP!|87}SKW7`m%nh4(g~0#Wix}S?So*)JNzA#x-6uSIk)Etw+JgpS|LEdfQFsu#BRQDEcLuaMP| z=>Y|WP@s4uJS&@;0G)egWv;;%HQ6BFbtdr=G=PxZh#(y;i4xVzi7K(Ypf{DwNFvJm zv&GneEz1kHS6K$f__GSx%QeYDwZy+WVZ9a241PE^HKJ~lHpsyun5ZQ;YRS10&OWuM zNf^uY93RdwX|U7*N!)vE1k?whmii17NT~bTG5a8B7X2YV;9_)*8XDBPrNC^6gGz)|rJ~PhnZz3CQ9SyybD)O|?5>PDE{|A& zIJUH5Rs&6|2+x!fW&r6nF=^$Fdp<{>x8sLCTRS@eCLQ zC(0Kl9VNE?laY+Sd-*slknXjR`cDR=r1|XF7Le5dUwwM|ohRmOb!znK3$CNJ6*?zO zDlkBcvj5G{12TYy)5?$?2!{D3wGGfqcx|dDvN0bzYgxo4liS6Y%*k^V&xsK#q^AH$ zU8Ng!MqnqmorQA}o(bkwE&0PQo1L+28yv+3gOLPnD0P4-RWejSj8`>Yt}YRcq>g=z z9p|{LDQOo!Qype{ET(h^@CSS++dvA`KH`o`bR{qCi*21jJ2pz_f6&BNwLxgkYWDys z*&F23GHzch)RfwH%^TkmZklb7A(yfIf==_IKQkbKEOvT)e>Vh0NwUc4-5k&!FDfb| zxOy%LlnL`pe_;iSP{}i5;}piVPB&l1>jaPuq5n0457-cv$pOg}|9QcGO!R*pMfB=0 zhF^CMSOBttq>A`LKIs#)q5B0-1%q89>fiqxsC>`qT{IB)aOJ-q^MYbB$DNAf)qlYK zpZNa|MgGr89^rFp(kQko3gmhCNtpL1pw9ArHPU#+Y5nu^2UczvOzr!T#0`^WJ({?| zyd^>9Fbb;6WRI<8e}Dhx2rlZ2La3ag4skr61bjzO&&~7jYc|%Cv+Zp?!Df3e-^=rAMH>2<~~7pUU|x!An!CL`ckhpk#X7hC`4 zRS|{?Oi?oavN@;OxHx(!rh$DRjVcgf=^+25*46x2f+LB9nn zDBNMP=b$1fSaf2jz^Lh}jy;d@2Et>G4_Sq6%eiOW3#DJRbnJs_borjyZD$Gcs>&rMEC#xa{wHwN19?+cyVn8v~olDtDL&JlWdz ze=CyorzcKdI4f*Erm{L#v#R0?L?7aIc`&u4)V~}T*H0$)l;cYlJVdy4?f+IR>2oST zO8zDtF*U2QT4u0Mt+g5#@z+p1XmF>)Vh_i|PGb~Mh&?f?e|n^&wRT0k$r4PF+3+tr;>t6=$p-Ute-1~rNYZ)8>%>%Jq zvIu0Aa=TL=E1BV7nf&kfyaaN*Qpb3xG^(%TsHr;x5F*-DXw;j86jBs= zRaV0OG~-7r*xA_u*Z0fdqG|s~2E-u!t1Mv~k%YT{WSJx5M__`(0S z>{6qq5MI5CJGrrvUc+hltQJK3k$myT8@;MBQZ4PXU^FluubCYC?+c*;O2iTX_>%;3 z!AVN4KbqwcD1Jd|LR+i1>f+cTq$d2kC*>f~hc59!59sWPqK|=&%%9RZ-LD3}YU{i< zCbraJi~@?y7D9^SNChU=+v$Fwxras{3X8F)A@0e<^8Gh3j)Ay$0n?g0JB(#&2*Oq* zqK#ruH~6oy%YpoV*kFLWOZVAs8Egr1#=n0ad6L!Tvtl&wqqmJ2V9c{r=_UT}u~p)K znvrJR@)^VjjRwxH+e?B)S;7JTip%B6y0>NtcJ~6PSiZ2fSAve(Hb{q!Me1*3aMEdg z0>7y0CBD>+6E85TJQ+aH`*%*TsoMBSxi53B!al%>$a^`Z4S(r0f&5HDCs~1biOi8a zbJVvIn~-SGxNVAhZHq!PeWJ_Nu)1Q!wFgn;>T^=p79ZjTy6csPWo=v_G z{K3C7qV)@)Af&vpPpX3~izC37cDV`c+-jq4e>qEM7_>@Z?DDT2#T|e|;dn3>2+8!^ z%G*G-J93b<^+Z*e$ z1DtAF* z$2DNEE!m^dHEj0H(S#QEoAl_U2Q2L5(XbSDiTZz*TcMLPFqrGnIfwu6AA|^tZcauO zfTO`=U<~&E;$hh^u2Cn}cZbLApd*X2(m8w0ObfVWY`^OIdujhFa7YR!8?S@dNmN4^ zCJmu0&1q*7A%Khw*kY=j>%KC`<9XfocFbvi5f2MnqsB2IPgI95EIDsYYkpBaOAFz= zII>>h(4k>0HwCVsiex@yw)TsG;P*Lq>@~7f4Z)z~wwYTU04^sc#^ONmc8VSM!>65u z7o%+8*U(u;SUGxY`G9AK?xo8dCIwzEi5^=6uT_Quqob+>qt-BcCI$N#UN)L;lDv>N z%xdj(9NJSDQ$Q%r0*!7@!REzO3b#{A3-cU!?D#}~Q3i8=5Uj6_z*6OQ{B|kHE?7TI zYb)%$Y_sRuf*}RNP7>IqlWV$|cr{OGjb-i=K_Y2AW5T0?2X%#=SHd7R9nglX<7p^T zwRK>weq9PI>pSg@1Em*G`C}ewEkW%&OF0$2<(DsqHr;(|lKkKXv>To>XTnsQk80Ui z8PCY4hqkLypR{(x<__Kf4IKuK-a(7Tweh52ZyZQY`zMc5Ux5PC`+Qu*N{O0hlxK}w zo#HMj8Dw=?MaXL@*bc;!YpZks{`tNY1$4CA+tf;XR}9Av9v^FxT>sT$fBE#u+6X8> zWw&uoo$j%7A)2_^V6f_H92VYcz_z+X#;3>coPAnF09s@F*)7hZmF+&0{JZgw7lDVa zbK0JQcFq{9URN(o?#?-KXA5%VEe){T#rNep@(Ul5%Q@OxSppWM-LUiD*=`&pd=#U{ z;j)vw_IL2zoHP%>+iMFd=4z}{z@gBWW0izg#n&P^`QRKyYzb>jIN)%B3?G?-Mdxl# zpz?!@`L*!qytYR*z>;_3A6s-)mv(Pvx76%@OY_`!1x!|mr;p3W> zZQ6LanT(Jtrus7E%ps--1_X`uKf@4zd-x#NT>0!jbryEU+Iy%a50= zFjO64S}@qb@s;A)NA9pgz}xsZHYRMeR5C`Kd8o4t?{E!{`5I^AjQ{|M=@WSWCCmY!eRdm*0K0~YfJwKnv2!i%E z&cIt@5YIXC$p+fpuD?k#C3NJ~n^4=XC%C&N^KySVl?MdpchMUMx?RxLaVKNrE46oB z0kC%EBe+EHyeq*sqda-dp*T3?Yn3)Lj`A#J6NroQ)CWd4r}cx}CHvK4c;OS<9E7I1 zRs%C35I?P+qhFNih)50wi%o^D;PXoCcDTMrZ(k-(Uj-T&80?k(;BUYaW?&&EDrEUd zh24dn#Ph#lP5}`P_Jf*bgf%mSTWTb2&3*J-+-0F8ljP7SfEVD}LjcfjPKpGuqf4t> z_V$BHae3NhCKD|TKXnBUx`PnTwmXChv)_^2Etd1iu2UaG;=C>vUGWGyH<&|#$F68oFk z8BdyG+p56ZcD*Ouu*^AIn=XA}!z9fyTcFn&JyK{FYMhSS$`D-aAG?;kr_{^PthA0j zh_AUjA0KtvKv(?b?E!fT>MbTs>5DZ#W%Nqm*hD_b&0g!2VCU6+BXmxW*=PqTF!@7Z za&L6Y*jPzJtF*1_YJ1oF6h}V9R9zkIy5&-H($j4@OQ(G8glnB}BlrdFVnSM|iGZ+} z;Pam|Q*ZnN1~U%dH1g0U^JMCrWA*5zCEr#xiZZ5R$=5F9c(R#-2_ifmsy?7LQz0u` zvEL*rtxqVUJgFKgaVg4!ZE!S>UonoRz-$huv}_tOZ8}oel>u$bnmI``)KF2;pU=jv zhU2Ndzn*93r~S}?>r@YLyEUZU6W1s}Yy=R1hxZ^4_n*H+T6Qb1foS3}V2mvFWmwG1$gAAH&Y zM4CH>Ekxwe9imHmi&wS-CdPJFJx|J^^?|$7OETS_@SvXBYXWQcYE&iPip>sDTwYt{ ze`yigV(uLIq&*YAmKEDN7%=|Bb78`~?7Rmx;9J&W;VF5lAxMxAC7AbD@bFZMsZT&jC)TV0 zzAVwr`JO0B>xI;mFCUTy3`lSG#4N9^Yw&0Dy9IPCgC!!nt#qoWikjnOFdJEGPlGC{ zY{u$=F*${Dsy%o7dGf0nt67`RPMn_NImi0S#r;nF)q4;e=qxJAmwyWL>51W|Y>-%c zj0Vq|)N4n=rc4Je(jxU*rLmsHc=dR8bb=ng#LBJ>ebGI_D87|#gzmtiXT8Sx%3rd+ z`WSj#@lfe@t&HBjE-HZ}b=_)ZqJ=EQ{iOb`v3g2ul`=t*ZCTID>)n(WZu{fZ?VX_X zHZNRYa^tJflt0;eiXCL6Gmt+T*1HZApp6dlmRn8wa$!TXbRL)WY);H7AquE5S=ACn z{u*WPwP-I6fEeYl&3=FciGAY&V1F8DB`)h^rysZ9Tb@}vm9G~cwXu}p?6+<;ya2{LK7=LZVspH_n)o>}XoOwWJJ1JHT8T)HCj>Z)6sP6{BiyVM9)>wn0Io1XzF zJeUURT|)> z&+l5V<8J@zy0G6}z}jlH6BEv#cPq2yj_qVsgZ4fnlIK;;G4$a_i=bq2FnR-lWIs75q&!3a^RlKhn*f*bE0+Lf zPuC3{cWSL&XlyL|W*G2>0h4pnFntk^lZmtg@MuKyB!IO5BW}%I;ojKB_J$^SxEORb zJNb^pH~1q=3mGY=B-~;9LWJ<66V!==t2#xNdF~=F$c< zT8qy4p^VON!ctibyKI=$MQ8Y4N!`}~kXAn5#I0!BetOces8xLjA2+YE|I6&*@|slT z3n#619KgdJ+iN{g!?gR3nV+ zOJ&(=BGPwyy>OxK8hqr8x(DM=N1fM#Cyc+og>u;W3ehwe0|f{gt-IKD7_v}<3h;e}EMSEGDg~ zBZoUf#shpSvwM_ZVVCVz-*;5=M&p`(`F8-E^6Fg8{W(?9q{~vqDuCqF=VfZFI@wa! zTOJV~L${Tx(C?FDuo>Lk`6<|a7FYtA@Z&}RW4YycRCtTnxPKe@V*JkP>s#zuOsc`G z;(W%h_CmTb^8|?8t~>%*dmJPazh_$O+vE;%oPi)>XPE~o2~srkZh=s$wp`~$;1+Xi z`|hbfE)(!*B0poLcfmslGQ)IL^}6?VvhoV;ixT6#@SZHOT#HFpZiC+m+hc^DrotMy zjw(P<#hF;aZ}5*MU*d__S_n+Gp==K;GcsxP==-GV!P#5OQz_NsxDBV7Xs%vf@5C0m z#&5{?6}dfdIbDF9yTuOeG*A$jc|4$_75j7|<{78Zt4_l*>MVo4oaHyv;P84+rd1_X zQlu{}qg$vh3v2V&*yS%LqhCNQ_^aG>Hx=Y9V?2$mlCC;@zjvIQf!UpOUFMZJDw4ZG zqm38jwt?vPR)Ak`32hWNsNSXK&n3Q;_Sg?le`z7=m}qB=FG49|@0a^!zxy>CTP9#J z%OHPf8C;oIlyJ+nPl1Haz+RW*KP>GA^4A-GNkFB?!CiZq!#mSqO@M0@?$43XvUXw@ z9>f3!4mhEI@Jq3~UK#2;Wv7*52`-^aqeXCMQO?~((fI&2wlMb4N{u!ZQ@m+9z&eOg zZG?pa`kgwYXolSaDNRj#=G6BG(`Z1k?hPx{dUWExcE4w4ku}+1)#KMqOAZek{q7U5 zF3X?jO6|Gc#TlqBy&0?DA9T1Mz7q6Co+cpcv}?l7escorQ%Xxj<#jid1u=3GlJQHy zf_w=dXSeSw$8l8&0{fPLNbNq@>5M*vdycV4WuNzx?7+?%tAKp^Gj!;3XU#>aH+@84 zsr(KER0@2NxT90aPaX@R0Fxh!bU5qM)8=T#bmvUe5D=g4huK_#H9wnIaM_#7~WZ zu(4riZ^w@M#7qFz&yFd90wuYE_qW%ZT3=y?Q#rG;bN+t;;|fe?_TV7d9p|wJ5o@lHEqcRw%1_9UTlhjk; zju2T3L7C;aKqjG3?W>iaM!FjwcJs-3|L!A;EMG&z|Vk;ppD!76l>^bDd=opiXW$&(ytmnOl ze>-~jq&1*gK%7!p=h1A0nH+=VV2dCNNwSLhJb(i!P{=TWp0Hc>$&p##>bW02bM1eh z_tq?OU_5>&zM5Yw8I*zVU(%z1n?Y|Sgtfb_57FMV-NDiszyUj1`m<`^Eh<6n)iz9D zmLz4YKd=jo71sc+f>E@AxL~0Wu@>hLZgtlclBj9|=t6@U-yM_yUy{EZ?OQHZz78xA zWSA?<44y{Xf}Zzsp)*dDYX=s4>rgd;9X$rVQX+jmsRKkU10F|5G=tqpjs5yND-Az` zWl)dU-79Wx_7#0RSXY$J1GgMZlJPIo0Nhb1og$lo`Q4xP)c#9y!WZakrB_W=aSo$0 z2Co&&BLR0Xv~P;jw)$dMABp8}Wzh#r$k0q^gQn*FyvltJVP}1K{o7c!*lFUOaL|+V zAxsZJVF!9N$I9ouK8s$9Mj#VX?Now-xGjDOlq61U;xfy<6m~yV@3>f6{D9qpXzQyS zDhkxgqIu_}UfLn-WLhj?M$)D&yjl!2b^v*1A$ZdqWbsTJr5Rn_@Q%q0>w{*p+BZ;8 z-97YNsIkvCmz<>+_yO{^7RU3plNrdN0Kl5UDTb@vJ50XRt(Nl)by{5@rLGxB$hc`7 zQ3J32;7e)*ny~Ghw5Y*fsW4X+R;Th_giRRWzO(98qr&(lXAm2d_#3E=d1}|>@a<$$ z2x1k?(pwqby`ljIc-tS#{h;(+0&UB42qUz>Rj5nt?0nb-xw&?>bq6hZ9Ax4*z zp%ky$uDADqDC`=&1oWgONRI>Es>j}{7bN`TA3i!^AXN6%#^iTW>UQl{@CA46UhCg0 zwz_Uj04}Mb?Fv`3W03&LIbQl#p?s1gzu=GYLK{Dpg~;w+sLc@Vu}Pq#)RdIjJwk@n zf$Veh8@wA(=he*eB4C!#wc`;Wtjp}tImpIa)x zi@4i|<3Fa6@}xlFn*(yn-S|Tf&+6qhta241e>cH*7x z*uGPx`Dfp1W(@_RFeg6>2lXEqcKgSUd!j^lKS50_U1O3bzX7Spy@UdgFwA%j3_$zc zdD1s}mub5`wY828iDoUwf>Z8+0fvQNpJF1t2p)8kKie1!8ISI7SAxNxTBn5rda}b#|Vs=_HncaZjvdX#9 z_1$8I6gF}>yNo623MMnR57)z3h_%NUAk)ozie?7C@UUn5%oQO++zLkEz5@3U z0klbXLM2QfYv9(QuFw4KcD~!;+#p6FJ_Izg0QlGi#kEr$%;^i?B|u5NZu&DYZsRrN z9I?@T(3|IiWQJ;JEdeB_1FNzyNGm20hOCbEQmkC5u zhz<;XABE7HbUCq)^lgKXAs2?Fx)k${J~oj6x+L`PqWpV&O8QWn!4YZy&@#$Y>bncq zN<`jUrhHeMHRYuD#A)_#(BtaeTx?LIBr3jr;!yY0r}=IwKQyT0iPT-xKlZ=DtJlm?sL8vwk zF1{>d?@|uub^<9iS41H67@Qv zZxg^+#9-F>DTjo;J1MP4@W)_-z-Lw|8{RD0H7E&=+Ey;U^;EeZFTl=n}TJ<<3W3Ype#CQ4{Ytoix~&n zOltDo2_m0ZM)~XYIW5EKiBnU;?fdBv=fZ#3H6Rq?M}|fZsEpnAwwPF%pRnnPjk%PL>7_10#`frfCSG7B74Y80p+3o6oplw+MPKz$Q&6N& zMXWVHF^C}+1vjX!MT$Ly+OH6*r0JM2EXdT&%riVJrI)sURcf}eg4hQs4w62b;M)_? z2FPUBbMfw0*$I@?m0vAYLRl0Mj@A?WmLerz;ql@(WntsfK;lDDI49*b@uDi)gfr3m!Otv3+#=x&yc@B z=#Ak}J`)>zczGSAhp154VTqNbqKy;$WM&#-lfkUgJGd^oJC(jZYBbc-1^2Z4LR%F- zZ)Ftis{n6bQK&CE^)Y$-C8+3J?z>9F<7fQ1ZlxGny9`KOUkG5BMkNlkkXKefO_o3S z;(-%9$@bvMGoqt@bXE7Oi}+W>RGPXMXPyO)y?-jV?c5B1ZxUrF!6zHe{Ez+KSNE1s zaR*%8qFwjc*)|1L(8~f%!vXEfqm>*H3;bx}S(47^*59%*Uf-r$BmHD;giD_yl&8-f z{c3aw1h|QcuRfUknX8ZKj~`Z^#Tj-wfXUV7iEbk{o6Wg8JfCNdX1m@vp^9}#l#7=# z|A&2&OcLb3&^`wii`QG3T5)SHalg`6XanVaOvU#@psi^x z*^9V?xh7E>=cvt-@=7S(%d}Ux8}Life;j^;e+;NhtrrLDYD7I=V^V^-f&=o;I@Qo7 zg?=QysR(2)cFFK4)uz0~7xTwFabob7V_bR?xc0h%rS9lre;$3-AR@8F+p6w;(DTcVrR()<%6Vi?SkQ+<| zC8kmqoD|||t`&_8)5?rHp;LC!wLq)9H;PiXI0tb@^5<*G)76iT5S!m9{ zaJ>IoSzOc(mDf8FqIw4l>7T#{2s+bW?+UUgTa^rJMjVs+D8VJ5u$7(h(%~Nm3gR@^ ze@6#NzE3AmC`-+r2WGBQn+$7#V`BX9`Ka%-P!x1Gef5VdKYtA!WFg4)T2*gX8*PV$ z#?$ln_prI^cjE7#gsjm{TT%9zn%y7Qn(!Ns@|WsPN0WC-t-N{P%mgNP%zvb3p;5p& z(PF;OKA(wgMugR|`==@|3jT57WE#qdSnSCv8ebGJe)VQT%Nd$B&LCc_4WMSw!=PFL z*fHcfnv@eOU0F(E4JRF(R8xMI?*tsoM`Lk1n-QX4loyMpB9RPeLc`~BD2j^JQjl60 zhS5V{3gYCohilu5zp-;{I`6&(^tZx3&q3AJ?R+j>6t{MnzRNTebBV7ODv)5D0kuCc zZIhkeN;zqIl+R;7_*n@q==vd2Lkjdn2QyUk!g3?ER9(O&@34XMF70@NK8v7`805_kU7^{!rXXkNWBS+9Od^~%U6ffRR2EO^*u=wV`ET58vo z-mofEgY9ojX7IZ6U~hTGt*z>Qj<&bI0qtq_-n`JrUkhJn1~YdDaDt(J#*4`3SjbL4 zJ;}t*LyLdSY0oT-69stjMW3Ork=5L8iG+r*PI7y;X#NIBVVCV#8TYzn&+G`c`9+z` z1Q?$}bkhExDxcWaayB;`=iGxz7t7P?d9GZ zm0G_WfA#2Et8BL%Zv{cxkD+p@*iJRxWqa)H9TLGR;HiNS1Dk)iU2 zHwzlX{(Q@3SKp}~?==;u365Sk$-S8vd>=}DGbU~=Q~h1g^#B~@4ihU5WN!eVZE%jF zp7oUayNtqZqJ&u-*!HD`%>Jf#P|;{+YxXiNJ_=EiH+ zYaL9v`EWkrRKE87lidyRd+}FaLDm|1c=a2|$qXo-k3K#%;bL5IDkcUHJJR|o0ZYNy z*1+ij3e3ZKT~{z`h`l^@H0%SXbD=qTT%Iu%KeP42VK6vT0DK7o zLzmxGeu(^Nj&2C49JNgrEeIhboik{fn92Ewf8K8IIFtUqCnurDLw&3*Zo)#EMjsq= zg$@WnB=Q?^tU66y#g|K8%EYK8)R#dVLXSsbm{$s1{?LymL?o@VHGkdtO4Vm5<|ku( zms-c@ErW4B7=^dz?!Z{cv;k3vq4RO0_H33zRMJ}Bw0b@En8w`*}DKv zjM!hPl7$}hnGu{>xEkRFHD+L|0ZF5Y(1V~O4uB1ML}v|tPlAnLz!8iz@F;fTql)}C zDEFi1);_#YF_73I2nfhFL0Tzk6cGUd=?*~}>5jFq=#W;VL68QKPNf8-yOnMv z7rlP-(H-XXoF{`&sfdyGBS-ea(yxbHcyIp=j<_wsotBThtco&beH5lP&?E003q z!+#!WKY0W`MCc^(-~-QAUi=O!yOm}R{^yv{ZRy)6R9?{O{YS{3p0c{HW{W}**C7Am zHCVoXj6ykUOWeJ!=%l?ga!OIrF&2wU)!&f7Cda)oDs@f439d&|jc4B_+)MNz)pvC9 zdG0O0_|_v{@4>@MCTAm!Kaw9Kx0W71PoZZb((yZGz_nUW8A8@i&j%&FJGp(N?mmb+ z#638;qhcX)`}+LoZiUW_t<6{R@1q*A>AOxC`zqYR3Op7H#o%#-5czy~1fLT5j6X)e zj(m!oBGW}aJxCdb zQx^~%_$LnjS%ZJ#;Qxm{vAxlhP5TDjd-RQ*D~H&j&o>MG-O=l_8v(4J1|p7K&Ge00 zosNFixz8d_q7r-XBlPrI5Yoiv-XLIaKXrF*q!gQE;^%iX@ienopP*ZT<*@R?HhmQ> z`=g-kmyTbrt(Ix0>_;f??z_ZLGh@li$d_(*KihGm{!yqT zm(mvA*<03Cvcu=??9jEYOW zCC7|pqm0glYaBYk-`+}OYgLNlW{bof)qp2kDlim(X`HMboP+<4u6sj?x=#4r7x!Ts zZ2K^*T#48@c;zIdp`qI=J*6BrXU(4z-Z;MI$~aK9yo8yT!0Iwi2E%~hOU??9b0LK^E@j) zr4p|o=kc^`z;;%Vg3Z-}sfIq%*)xaGpY@iKtJ*RR+?#N@Vn?nn&;9&|;~E6HU*1tY z54y6+!pVDJz0*BSt+_w@7?xO?4sG+v{XsUYr!+-@%H2~~AN3i1_4<(n*ae#EmzT@# zc4WPoAMQkJT;e|0u*D+dGCut2$y!DS6+^Yif=rM0Ii)U+H6rQL1(wfJ$`{U7-0rqn ze`;Ei1W&*oKTWN3j=t=IG*x_BC5y?I^KQ%qRmNXdTKtL!zMQXay^VKVUCwc?`HWA7 z;N@QL&uMA#F5VD&u=YVRm}`y{aFKiK4diVUbsK4Ib{!kq(yfL9hcb6Cwu*GqSL_K0 z*yHUtT@U84_3rJSVG0PnYVY$hH1NhssU+RqBZczey<;>T`EKAG{tnDyV6=?haHAakEd?d*@HAasf^OZ zhZRBW^mDf%&oHXFg~R%bAK<|_1r^%z+$dEQ9i5SxwLNc*s%nU0EU&ID%ad0E>ve?i zegQ>JaX{myRDXiWYrdfO3)vS-m|wu0+Flhk&h_$I>N?eP&L>-kdV=(G^;4(UjJ{&Woo&dV4}`w;x91#= z%kEr{f>JcjU(!7G^A^}?aq@;O*yBu-+?bFEUI`UbCs`*RGTm>1$&m!Gy~e)po{m!# zIjb^LJcZ!0oBtfF1rHUK)*3ZF=u^V}Jwo_ppsK!fXB|61zB35uG<6YfBf@i7Jie3o zT4?hPmSY<$bO{Z&8HBATtU4SB@#wBdtdC2tU=Bjq4Ze1(@%OWdJ#$C7xO#fi-pn(r zW0!9kOHq2)jfGb%MD_T8#aKQFqtU^ro5s~D%BR;s$cf6#4IN+VNgYnv&|8pr6MwEXcrL-j-T zr8=fW)QOa(uI~F2^*j~}#WUQEHOLzXmh%Xw-zt<9?bVfD?^f!L4!w)#*$IV!9RC)8 zRySfh_wuEJ}SMVeg~ ziDE8JI3B-C@ddewNS|rts)6-~g^n=1sPwd%=oTk8OLHBu=_|;00I>x>uW`|Kwo^qt zb~x1<;dKI@D|_=#2>CekZEVl)l5GgQ#~_p==QlLzs1pzRFBAOeS@0e0gU6~4{2Mgv zq>?5#@>}(AvgU2U4HMq*ttd!)4dd7p(er!}a@F>cm0{ul^d77~p~A>3oPOT;ho!he zM1Pgt;QfXRXC5Fc;AVBNTTI%E)6P?x@g33`*j4-)78d1oZ4oqa0`cvWll=>E_yp`> zTi@v^?rPa3mx=79lI0x(B#QTg*!Tqwsn0PhZ+0IrAAj?nk!$?p&(@ls(%g86Kt)qZg#^jJ0-2iwKEwsXk-FLy@5Cdv%wqy^Up z0OTsv8#*J^F}LDxtjOwQ$Q#~EU#=l_79b8dLii4v00)v5+MDQ9%p@f6 zHExIe)epSsZF!1GtZ0ib=QYW?QZMmHUkFM!h}lm&opFF%4%fvozsXj>HbuOd9Y*&6}Vv8 zU*G~4aDWXvl(~`DF^!%zYwzI=bUBFV1J6Z8r+1w&RevXE^JwsQ+q*euhioTT8IT?U z{<@)f>bAmY&xVA4-G{>+*r&!Mq_#hC8yWtH8+bJ3bNs@-#;<50KaMj6%&+3%)OI9k zL0~*Bte#*Y4)(FM-e-w-+B??1E1-0HXL}M4fRl+Ue9cJ_kZk!^k+>h+r&_mU(fn59=(gM=s|yrH%%#>7rTb6B757>_ z?}T>k_}>O}4~s_ksx4whysoY}b&KL1PgW?1WNG^rWs!#4zIv|IYx?FP^Z<5OKltKsVmkc-!(Dclxrpr+X^E@VE^_^1)etRuQ9ANGn z9n_kDO^I*u8_#1WBG^{-NaqIg59kYh&b6b*?@@4a@~x>O z*X3zo%FujHv3K>H>F68P@i_U(3m=1Y`k zou}Ym_}bXD7DT1I9>C@osuh$%*bdiBzG(KNavSYs__Aq2juM};T^-s77Ea6lWcil$ z;!#r0DJ>}VNvWBUdql`KMa*QnT)Ks9Qp?-W75d-pa~k>$EI#=K2+^mRN-N7f7~6f2 zM0|LlZwd`pT!y_7#I^qsLSw)IVpnv&W36cnhO(bYFUE5F=CV6b{iU6C>obD)*fs)& z(w&Vum20UP$BLhfeU*sPZ|9ZxkIC{Lz~$o|9NqD)z|q+YQF{P@lNT1gexJYuS3x`l zUcLFK5T1;-LPmF(S3-w}X9?q=>YVuW1TNz078{hwflpQEp_{djAUV2_e{`?@3He?j zbPOdqe4+UV{{4+p{AN=)SWWRf_X&s9Yggb+H~B-i8oX^oWX#)cAobazyRNN)o`ivu zS1av%#0^q>O5;!f9w%R{zniFvSwbC?SAPZyw zh^Hh)pId&(8110W8hc~MSsxX;QCDIL%a+awOPJMWH$dJ~W-1*aZ~=0>j9=5aSCF%h zLr?nQ?(LSYBCYYb{X(<*%hAu|OE4}rOBq8Fe(AL`fl;f6KDH`T$|%$~hR)Qlj;9-! z!g$0EuZxXq%wc5YeDH9W!JX^LMWQAOi5QgzCB9@}h6JfwgQ!wp>6L83qGY zm(D~P2|Z-NO32DYm+_g_@97DLo>S7=n=c%OCXJ>pfV%G&81(KQNTl8^FR`I3)AX_p zvEbI`7T@HJ7=%X4ERHS=-aBG+@40LDXs+Y<_apWd$GWJ=@2ry`hP z$}}rRA2;s_j9%z~Qo z^lR*!iSr>G2!LpWwMsTeCy%{0oHLT?-18&#Ln@kymco50<+{@fud9k+z z_S;GBo;{~k%8=go2c&${fT%rMxP0)1-t7&P3eozNZUM)kONk~L5rS2(RXV#f0aEyh zd(|AcjQDMP@K9C{{=%5Ta1=JwD20cNu`v~&Rfa6YHhwf3B<_|5wX75Aq_i2deO`+xh^QdvQni4TBc3Vx zZd?9Y`D3|Mp2=i%@{OE+YZ>{!!6vLnMz;H`aPZ3>!;sKW%hB@dB!puy-C!pamkgSH zjoZHPcwT$MZ!OcAEAdXKgIZJ6#;cBO5{CKR!u?>E&&2?jWO)KcJ!d}+JT=+N9Lw(K z?6nO!KhN59FndSI?1Qh~Zm-dx)ep<1_2);T(Zh2T=&F_PUUUwQjyX9wkqyTQ*nj-K zMZ-rPTA(CwyA0mKR~`2|R*bB|@ym7$v?hKl+&263&_s65L>YY_R4Jlo-MPQWF%gJD zS(#I`#bGHc+IcK8Hs3~M`&3L{;flVcZfuh-gKJ$1`dAmOPI}tA8mK#$2XV@RL2{v5Aml<9`sE{4c z$m=@$YQg;!MZWSZRXMEg=yyKW;vg1X`UPh(#hqEACT~JHxg8w(zDn$tmYCr3YnskJ zxwTznI$n*zd|VgaaCIaR+-Oe<5^~vERCA6tO+pyAnZ20)~GV>U}VUkmn1Bs5H z(*~(j0=YGH*&XwaNuL{18kM?MUr762UCm#^Vw^=+;@WkG&!ABA{gu{bPD!M|@z`Hq z+O?aCPKIvf>lwj?lp)jAb)z?|?wGCnsl!VpNu`b}qxnW{_4W6h-xDOEYAhIK4hxdU zL-y(PO^wTAH{=tXp8W6`O5ACXTdf+$uMysPGFF_WEw>;xw;4FffPNDGB*N9$-F5+& z1#{?SQWd)F?v<*qv_gjz^|W}XcyA^mqRNXozK(1xD^C_*TaGdhxXE-<%%r!-xus6?G?;+B#A$K_#3u15mh6U?QYL+XZX_TUbCi}`_w#3NzX%O3bbI2ibGRH z{L1jk#Wk*ucX2w3$u+XLo6FFdze|Q*=3EGM7&>>V!goY}fmdQmt!>e-===CgiR^)w zdE(Z3yC}(3div(0%H;Tz4*5GVrz4nrlMKr(i={rD?v3!9(SaJ)+ty4H%a3(E*}`F9 z-rLg>N9A}7TPYxem_bwv@)3EInIMT ze(a&!Tt-@RkVb4Pq1|<-`j84(8g3V*lhMUvmzgnl$LAFaw&>Vuk?c^kov7aoo-lNM znNxtqBxZd4Dj^xmUZya8APNY zwY(B5Y?91@OGB@?RJ+vqnE+NomlSwLT^3lJf6OPKL@Ba0E5b`Y46fOw3`u=TJh)b& zID0;-4?q9g^OSPenO-%0o28zoKUe8lpo((Iakecf=8tHN#G!qC8TpN7EU*xN0#|EP z@1W1zW~lh%m2NMKky0X>TMsW(xiD0>pNY{SsI*-wk{Yy>a!ceX_L_D$P&bALKVBh5 zg+djRBwaFcewmev=1If}e2Gr}S8^bRl2-e$;NjLKc6`d+M0iLkZW;%o#-(vNC&%og z;+cSZ%8qkVC3J;-H<#~6V}6EZ;1q90?8(;UCd|T0rW-%~^_jj$Y>vh<@;yAO z(CS`zv~y9RcgRJ^s<}9+WkvJVTbMY`kB1sM`)i%!oEnL6{M}PZWL`V++v0vL?o2`c zt%0R=;gKczAAK#-dvUbNvBou@sxHl;aoKTm;^#+7?KO%lVgzWB8~3TR;tR)E*Rp11 zCams;p2`ro+^Z^F;ouIV?IvZ>eqA|fiAKy4RLT}DjQalXKNmHAiMq&dzQ2xcbAEVO zjg!a0`SGNk9RC7`R$vMtbX-c4DI@0Q2LUaAPB~ACx7CtY+BS+02ks0gwUg=2j)Z3v zowLZ!6DY7hc=v6Fg53E9`LTC&=FTn!D*K^h=W-sL5SrZ&Z}jIsf)X+4t1KR;UQ2h} zxVOM_!P5_@ZES~M#?2$@O-3z>Gr$IwO+>fR4qqeL)X<5}fVLq`GXMKGo#4Tl6OGwv zDyG@bQcyLNfOuR8*f=aSNqur9jwxNwNl0_eiql@ATy2^>TL~*rI$YKIV<$YNe+NT0 z7LB0$`|IvaZbh#&1@~=n?lY~a81b3OtKz1N#-hF7^wSPbOUpoeuAQ~lp!8xb(+CJ0 zht>Qq>ECb>VhRm1u>#j+eQz>Uv-_Lrj4Z=l6ODlQ=wD5$8k21ga}<=uhf#WpRVbH( zB8LNkw{TiU7ROVq7or{)qMIKTJ9(1(->guKn@UNGj{s^!vs()~(5~sJ@mI*C!MKWy zU2F>*wWSA&IdQlHuMZz?Hjf>%f9UauUCZJ6_S?F)9hWll+8E3?!bYw9q&v$~M}HO$ z-!kio-7nVpf;CHSIiV-~b@UQue|W`GC(Ok~DZbm}5#L~bvslIP0h^~d#ELGw@^wnb zpt+lgYvs8Zp69CFr+2y~+iHeFLVv7xO8+R-4|Id-L0?D^v`Ue^)qvR`>5DE z23p)V;kUFEgfv=SnmoUIg4K6^05E7>qtJT~vmeSF<+52rcW5>J_05{)b7ZL)hqMSH z{Nn80m`(z;J!-RzC>ydy!T|MWr?hD7{QOa^lUOFvN%w-Ry0$2{TM0S|jcJDI`G#cy z};{ZEQa5pH3hoU?+TFdwsjMBx8-|!q;a7Q+#pHDP9-0UA^VnmY`r0 zG}PiUEGJQSwS*k@+emSe83wdoOLjtvW`BP{1$354Y*xx4eq_v~_uSzX*SrgkcGG%B#PYC8^){>f&Osfg-Ny@6 zs|wgPm;JqH@^i0si7W7Hw5&EBg(e?iM2DV4;jnFjk2|_E#4k8s;pf)?Y(0)o87R7; zE!A+gq)O`3y^?)1AIo9xyrLzl(_fJy)_zIIB;w*>36AyD{gDNraZSc4rDx2!A`OdQ zUv`P0-{>$eviCg1CVeoTCwF{$oNxt?4Zlr8Yx%QmwOqhDsx4_0@D~Wn10<+zF zo?h*O_IuTQtXQikiFJ@__1qT@a|BGJzne3i7{n6&kG%oy|dE|Fail;g5_OA156MFned+dRXSmJ?im#8Z1cUfM1ACFXJ;M-4_xo4bi zm~o~8%M#j^r^wHYwrIp0_@OP2jbv#QwY=qNuA)N<#3>_I?IQnba<$@;={@W-0&iCK zWp>npU-i!?9^+Y=TKtvo`RLP+GXKu^n?sV;b~%afV_`t!8Y;VqEN;q#L3=C`^G7qJv!>KO82lune8e5ltXyS6p zlRGY0NWSu=?^%6iXKP&>oy@EX^!ab*GY3ZNu7fC7meOjs(EH@etBY_OsN@fukAkf4s|Vzh~XVXb$C1~T5I=x4^i{yXF6QwJ;} z_xBq{4CcDu6pgw_9UeR+#HVEZP)Vk#>YywTcRYEy03E`m=UHp%V#eI_V;Fu3u~i zB956CsR%XO?ex(JGH0Y(Fu3lh#G^z_7NlDDg2q_Wlct?}y%FU3n~IGu-%eHoE#!EN z?IVd_orLt+24i1?j1tb8IuO|e5`qlN-19daaaUbai5bwpt_U@hR5s1C@*vzEnz=WP%09iX?q zhzhqiOU??qZhfh`B$)`0>BHd&(B37`bDk#xb1AFBNeodJ9`+QlWp8_y&yyEuuUmXl zT@4tT7ILxP{Fo3WG+-(t%>RQWR6!|9@C2?fd@~`aamz#n`Wz`)*+`6-BGjx>(4PHD zZ*eweI;V5lm0wn2QPp4#`VH`YekU!bn98k59~9f{B-^!+%UlK@vK9e|NxL-i<%A`$ zdyadfL>b~i7Y+{(A*en>Q1y9^gv-8|Jin`OV=PGLbD+oU5sR1jD37Lj*$b|GR}?;q z!|+t^o-sjQX%ja0&Vl&Qoh?l^p>fWw{`VFKt*J=&!;-f#l2+K|5!|{kF2Ax;`;Z)= z_~r>@bSyXQZ;#1BYv1>3_$Xa;@QG6}T(2YAB_c>{X+-k%x_+e-sAuIFFKz%WrSzB) z=7;916w$)-9j;?Hd1WSB!$}R(gAAi=pEablJG&vhg0;U2Xz)GJ9w< zos>6auXhRz@rl6pU8zuH2s#Vn>~!rGjqP2wX0qX^2XV#kO~wU-K&*#8Gzv>t+5#+t z#YNtH(guy9hO*>ar7CiYZJwE*qL%m`^wd5T##v*%%6{-6FDAC9v|q0R%+$l~5F)RU z2G%aP(>J~IbC+wYpaTlSAL+`H^-w54Gb~Ads9DBWLi$v?2(6v68K3j&4yVvut z6~yP|e6(F%_d2^v9u;kx!e>^vzzy@f@Two3z4VE5J=j#?0}~~s@clhJ5L>t|Sr>r( z(UPB~UwiX6r-jj=$pk2h1~2Zl*qv&1psP@|U3tIG=2Y?e)g^Q@lUqRRivLuc{FN<* zFo%T$OQub>;zfU_#ryKYEeOgUg6!|bm%w8aWC;cUK9gNXMY2&L6p z)7d;H#~|4aYNV0tfID$P=Bt<2AO;HRq859*R)#JqXsjNMl-1%6O^0cUUeQ#&ppD`o z;gk3a6pBWd%95|7V3)tdMr@9HHbeV}-cF$FhYFs!>x(hgW{^yKna-J1rE{j`!*L4b9fYKEj8w zq^R7vU7}o{d&i6QedB&B>2jfs+nUFnPzC#4krs!f$VvJaO1PZlcnw%)Y* zCL**kbPjGx7Fy7ASubY#R9~>38h!Frlm&pU%yJ~OrmJKSiwpal_%Pc#aG++@Q_LJ^iTbNFP##Kj0 zM^3ZMe!R$$RdAk+aL_Lps~&vL0KtH`hehZi#YJ@!%a<;$rWE8n$jey_x~TBVlG zd1~%6%f|@iwolr-5us-jyavZ!uvDn*e;ykX78b@$x4c~&N+1G-4H*M# z&8mU^ib~Zw;b%Z@2ghShwD)UN(5>s0vMiS2{tL545zP8q0`zKc8pt;i3!>$*7zUomG_81?0=LG7Tl-N% zqH%~77v7a?&cT;j$pYF8a{+q?&(mg_EX>6-r(0l#Pg@nA2<34}`-@YZ;NwQYl_lm; z>h0PmXS8@&O`}qbmL)Oum9`;A=$B!0l1gkhwLb8FFp_v+*>dmb)K4L(DCh0lNzs1K zB=MlukC)U}*)Tg&tA5Vw)8XOaH-={qA9^H2gS|szndE!_BXUk)7FV7>U-6Y0I&5>m zPhQ?6(CC z+yM19V?UDrfqGo?<1+w7VmI6wZMr5!y;;TaJk_}uv+amFMt|>+I~#_6SRTE|YbSX@ z07ygTb?sL4rCT$H%jSupyb@rz@u_4%>opS(3)Xd1_$Io5jyYfvrubmCiobOSbo8Cy z5}QHDjG&U*a=B->ts@aYo#P$OHl0@uhKUm!a}9K5$Jx-;QUzd^xUH{aVlNZ&zM(KS zbdq)_YI|-;bASK)3-lijiKTmu1O5$lhkcd#w}oEho*-cFPHa8l!2)G%7wj%;-|$c# zM|a}BL<-ik^p$UniT~u%U9{{*X2!3@Vm>vO=BiC!aSqfnObQCxNhKRm%i0=Q9#Njx zu^OOS?GH{yN5Lw?i0Zz2K-@wQ9z?hJ;t1hO;Kr}SK3=kS6)n44uIVFcNJ$DiXt>$Q zpAiW{m5KK53V)(sX;9-*6R(8haa>>vdDhTw!^qXEH!2*(w=&x}=f91me>|LiFQl^2 z3Y+Hu;dh7mtj7}nM7EzkmFLGtSYg9VNQ$H~AVp-qJd@Gsc4*_3xS$_FD1FoHu-m-> zj56D?iMkrN_`)@b-T#R`)85+jg)?y7Em62`g!!#S6UKId9X>a=#9rwrqli=xX(@Hw zI+?K#HAcEQK=v`}x6x>mAb2oDUl~4aYOC?D{mN7MpF->xSK>o43h00?{%ir&bDR(D zx4~HP_)OziYn%rLcV5(PA(v)J}IkC-wr5__(8zm2&X6)TL`fh?XaL#JwNQz~EME1-p3eL3N$$0b$QD`-kbRd|2$nE*ITs&lj5 ztqo>hJCDlcm}xIR39k0pTe(RuVP3^SKUXRAg1ISwZ?-N4us_CGA$ zQ=~wyOXpHsyZE94qRtnT)H)7(`cbCOBG#u{G+>sG1-?L6wN2I?N%iMBA{H1(5;T`Z zD|Y5e01-y6ykvehIvGrMTH-Uty((i@%PzY{)Nb^eb3P>gX&%sD%|}B%`x2EgiIH>* z;4_iteCgY!1iR2Z?59tTV&BOE6xZ)qE({w0h~|yzAAaLS}Xq(ezAZouIj zCI6VxMu?zkHPv8tA~@ue^cHKgb@mf)&9!h-?WwYxAUbeP_!Ua;(&298xcIBtM4s+O zQk%_*>-y*}$>`Ppl-qacpiy}lBfr`b)iK!xRuHIi+9wq`derhq7tQ}tKfvYam| z(+dTg0a7=0Ay>V6%M_V8q9`2qQeb%v1=pXZRqbvT4~)fyPC`F2>Tj}~-1A@Zc?0tK zD$Vvcf2Y=hj&di_T7E;sH|GqFH4p1dZ5N|+!Nl}x!?rRZD*Xh_Q(pYfF%8o~6g+uT zX3Mc5uSX#mdX3IQq$#QYDF%z46hsET9gRSjHw2k+AC&ySPc&{nJ8y6Ci3H^ltSNvQ zrGNYsyoD`HW$4aWUI|b~4@4=9g}8^6g(T=6B7zIudn6L19ihncLIeKPqLc`8P4$cF_Yecz<7lli4+;v@4 zi)|Or9}&4OwW!1Q90x{(>^w@x0eF5nmc-$rqXuRy&_NR&EXorOL`YKC6 z!K;(F(W|i>a&)!A(UZSNng@yl5U_t3EiS%*O7?vlsF^Y6)%$3Tbpon@fpTCb}vLx*a^W71Pe;(xtMXEXYdN$qo6O# z+g6)lsJ3#0fy#!cu}6T5iL%)}7a@+;6RCmIuUl|!YsNv`U~qd0oi?lKf{*uQS~5zW zj#C#&)7-hInVxdBFdQ4Q`-fIx&Lat(vfn|@UhZAk0Vbdv#=wi~J9cC+1n{SBp*?M| zd98f5=4n&2tP#zbGRN(0oylt`RJQ|Cb{_#R*r!?*NX-h~qw#&Tn@bkVj{m4tGj(KRp{!>^W&MvKK7C#+QpoFSHt!`N|+cia7tO zw+-!aqvwT67JWL9A;Qd8Pvd&vvJzR3Lic@WL6i0F;6|)ld8jw--{56|#Ih=8SUE>q zA-7(hY52#~*5+3+WiG5gkU@K50eR*y$6w4qN2u@rkH_Is8beX(SF>vr{Ll!cC>bXk7o zXfNBCXLI|Nd?~9f7*Kl2>Xa35&zf7k8v?IvcEL)6A$P@Z#nD*#z1x1gE1B*sp;a>G zSZ^-b7SnpRN8-A6PX*W<{dYD!IR=UFF-X>3f-R~Y%m&E4#FIh-9zd%?aS-r}UQc&{@t-ArfB z;yjFuTjX&94T79JU<&Nb8L5}7ejzv>Q=j6a&ncGIqZxH^P`_>Mg;3PeaY;2jT&Kxp zs(mwNGTr_r&;UR|W7-kk%C}z;2mT?-$S8b5h`WuI=a~hSBd6_Bqec<#TG0`dhvn@V1*7g?X3K%Vzowf`hxbSn{%HyjLnXhSMxc!7!wfO2UjdKGMQSE)qUFLa5QG zsm4Ok229fzTv6y%YGPNUPrjW&6Zc+eJo-z1my!cbX8!(0^pe{h=;E@VIPC$+K)dPE zyijmvg1V^3ePcY`A0;hk&J`!LupiNw#@O=yIR>N-P_&i7ctvv2AFN?-iy?Y4{94sw zi9nbB5*YbR=PT2y?#Wyww?v(|!eZ6f7}h*DqpT@$gy#2978Kqj$G*A>m8qayo0=6Q zbIo760_4L0?LhvOlp@2Vr#81rFxDTa{o(?^p-jzZy==O;FYG&WxA~oJ8MB+g$=qw; zB1OiafxH`_p`Bo)A|gSFFae7Q1}GA$s;=G5L90~)jT=oH^86=om66@4Fz*QIzcMV7 zlZT4+%c*toMFQvr8oj_cQ7!N0@w7c~so}9OYtHAG`yg?@ud-Cox>0Xx12f;t*6Zo_mu>+0c5h>}=s^&z)E{j)Sk;-3HvD8#0xF5=qb9+*Gw#JqZ1^Xx zW|GP_vBUvXkD^&G6N7BcPwQL z2a|Pq3PWM|_^h^=oS;}hH>4ipwT&^_8Mpl4cTet#;oYM+^A5XNF~fqz`>C}o5=r0G z5SC_&1r}~Rx?8*alI$xY?-NA^!yOQLlVHO6q4kHRQ2!(!{SNZuQt$B>t(G(6Ga&5% zKd>=a@S2BcJ7HTap^E>3)ulm@y`W2_rhRCfsO> zci!A92v@h(M)QU#qijuH7=`jYOCva>KWwuQX;Ncg9HLg9)6X&07nRs=`k$M2f3T?W z_wTp9%q}6*1Z{IQnFbC^Y;thrjWB#0S?SS0A4BNI;<6qol`-eYkiwiP{6Q&qK&00*>~WX6h**>1#N?bMr1I8x{xa1XD@pZ~+&4loYHZ0~eX@f|2Q^ z&bcHH%aOyO;my=KM?*w9T{`nOaIm%i0uHU-{{|cjD2D1>4iINg^Ym%%rE{#vlmm3! z!x;y#jML^{zh;&pAWrhtWw;}*)$Jkz9NdVwpeT+A<|;<|Vv3G!dCiwDr<$qjY5cop zUg0`GAQe7kscw?gC!`aKf=(#M$J=taH?=+|VrKS}8~R=2&2Nl)t$85YHG_R9o(P%+ zERk~$|GfP|q+yL%d88FxMKR-L5d|`hmee-BDg&EzNe-BtV8{o}8Qd_C70;-MGF!A{s%td;IGE1r)TP;Ousm z{DPm8I%Wr9(sw~lMXb{^vuGdjhPC?!*_q43FBjn8OuFd1sqw2$;IHY;Y4-F}>GzdO zi5%qJ##ST?!R{i*3n2Kx(65JE5MmAnqyw|eNQup6#H?b3QU8f@GG>BYmG0e$88bbf zscdqh0Bn_B38b-;2U3@*ljx30BQY3Z9c1EytrU!qDaWMrIHCsZ5&8$AjF)Jm!TcF(1c%x*d%C zRK7>3Jgza=p8P<6Z>Ie9tHkTRPu~6HC}qL^2`}S&>cFvqcg+0p=a-_;>sEp6?x!={ zkow><$Jj>TX#ZNKG&7_R1HlikIJ@FX^&tMz6-+p^aku{kD8&B10EHD9sN7nB)T?8G z5v+0^?h}RT!q#V{?&6Kw&V~s{FWG`AP$80z0;4SOTOs8jAq@P~w1UmENmSt9t=sd` znrlOvt~AUi1EX4f@sM4ULh669I}4$$ekaqVtajHSAeK}W!?WPh{=TDuMPHFs;D->s zZXy@H{T$I~;*&scTO(vbOewol{UZN2Iv7EC!f6`MN5c*8bmcq_NF7v#-OqwwLwO1e zU$R{lszLmu(yEA+aLJ+k!#r5az+%BY%V@kZ8z7{AuJ`dS^5i>L+(kf@<#Ufm$_>v5 z=m-lyE$@8$ln3?Z3T*}1B3;4f+0f(WWMi`hwgi$>CZ!p^p_qdGvgfzHBai;=b~zt? zpZ>-jY+mzGd8~RB>w-YU&Y1=I22;3x38(1ChFAk1cdj9#un)C|@4D{Y-0qW;^LQix zwak$wGgL$4a%|@H>a2Pu(}Rz7b!(gj($S{PeQd*;whLQhNCB{_VvQ9RW_5^g@eZ^T zYqAQ|AV^kJV#|kN;^C{QFL>)Fdan58`X0B^oOa0SgdL4Z01=pm6 zWUTJOR;$)ER;dfU-LUUT&>-#|@|CsRUW!}M=>0ex_Y^@BO9>|MGE zD}JBWZpe{gWjKcy=xPxvpfGk-CT;3@ZxSWqnF^>-P$cEtBJX=e&EukL()|TgifNawTKSEPd*! zZ_K>j)||54LN`5U2ls{$OivIEq}=OV9m!vF^;9Pf3vv zJv3VWK~j3K zCV>P+Lkp^X8c3+3+WR8w<}&J}KU(g(HkY`#uQ)f?BPgi>oPhg)HLxrr%lbC-4Jkjz zE}yvyAb;Ic{JocT5{KV?_>wPL+MnEcH0!VVO=-Z zYGDecHGUI1NmZG`G;_rntddl}>;+Kl=;!{j7X*Zq&ancYm)g=iCE;}hUR`zg*aGXB zR0FH~%lT5mq*Z9XiIYfuYfl9l$k9S~r-EWbCTJ?|OW(V|9+WX84(S zq{;c;d|XgpTf_`SQ#`8Xk=)PVt!w1PswD}i0nc~x!_3qG7|V1Ag}?^j7peyd{Yyq1 z(#Y=+`OB(J03FgJt%}jRcos|%N06@0`2*@!>4Z@b?XJ_x^iig1i&z2scWGH)-7ZVG zNt;Zuu5R{OYKLPeJz{@a?doO92U7oAu@=TDlnA6Ewtzr?NLw(e)SM+!Rvef!OB6NQ z^G6R{=B8(0byWTO;o_-`BOzmyqspWzC%kifT27Pc&~f)UcCTo~v7FRU`7nEM)?nk2 zh)Q8ao?5GfB=97R7`jy{W-?2{|C*5sr|N)S`BJN}0|~<=yLuPMs$0o^L`FOjtZ$1s zRXImrFMdyKx|T)F?Y#G8gBBYXwppXU?o~hIE3_DyYy9|jNBQ8c!rBa8ZboZV=#|68 zvq`qv5AJu6)fLLqjASsRr@jwHTF<%{hhY7((Yy*1-kVJyWIXwnzG&J`PDt}LxUn0x#v*Nr1wN* zb|2(H6R0o$o7uyaJRh2zdk#iMtH-xbWt18k$9{(cJ2awGsVfHA0kj-L8XKdhsGo7V z_c-KnKI$sJc)g@8OnKR~(p_r#Hk4P_Z+~AuAXUZ}_;v{lZrcSliTOt_P484yvZbyL z>P#Nu<$6I%30Z3l^3p?1iwBEQ*lb!aVLGns#1VKej0DjT!SBntaCVal$nLmLIMMn#9w*UyOg02z+| z4W9sKvW*DRkt-u4&e2^!mi9$v2Whj;c1l&mK?m z10Ou!HNh3HRmQ8*lP@SGH;Qdy2Ctf$#Kcnr?Na`t+3@ z7*NZJPJ*b+$7f_M?~e?2KS@MaghUiqI8KI;h*WSC@YYQ^kC7UcqK`tIq^nKhc*pIQ zcUv;pvSfXBpJN86>A6Na8Qy%{OnV*e(Or%!>94hXp6lSY5ErU&tRuI+61K z#TDRYFpQBiN1BkQXBn{Sr#H@^{rv$Wo9q5fiXar zvpH(97SxV%yx(|lZAu`Kl8CJ|a9RB8cvs+(1|08_#e)N{`k-ojzRaxCb>Qv&4%OUs zOvK3H0$Ts8`O2!NP@t%a`&Vk< zuKiDDVD*E56Z|SzgQ3vFrE^iKbLhJW?EvS@Wf#U8iFDYzi<@JInv%0*>!yfZR|{6{ zbgP%|UUaiWD}q_$zWkUJ#~^c2gZ!RPj)|;nuMrr*N%t_z-?*Lot9cL@@M3E7{dw8P zFh-H-ig+mlc^{z(gErgT_T1@j}aaAgbOx9mtB@V2luuM%n4keCK(ven3-spiUae zzc@vdw)O?DUjrP<;1o6toE^A1hlS}rk-W&op#Qfs2BXlme{?PpxKm94^&}I{88cKv zQX2of=!g5ES1esn|n z;Buv$*;)}zI9y7QQ+rQpyFB6gLeK8(3YNoULE(|Ddr+L>=?cN*Lhx= zp(5lwFG)Mn5JkUt);_}oG3WGh#&@q zGJN^&`^fRoyY>|L+TBjh0wUB9xmMFhd*JLt)UJ3f?-`?ev6oR=*@cJQ#U(<%Z0JH6 zH;eY{%hXc?c8T{&{HfCo4ZgY!M3Y1+4rDEF2XCv%QxrYZs?TI5LM>%hbMs zl@f_`MJpvoIif9?&T=0E+-(xmt!9PTVw7>^sD^qGQiYAxql2k#-Z6c8@Q;!pqUG2y=}(Q2XP;r%zV zxWeE`9?#TCq04P#s3HD5!3LU+jQ>H5Kgz}DL-3F%HNKleFCgVuIt@70WCL_q+K4?^ zyPcNU67t=zJ#m***uO7zNoP~o{ky(%C91y@vb_p?FW8N*NyA_4>Vrs){&OK=hn>p>l0?kb+TV)(9 z%-?m37SeTM2zU_~hLA^!o9#K5AUP6$!nu5_)$OdgHqvw548`c0O+0;OdA7 zSczjet7Ds%C*yXIkN>FAlCTfCmycy2C4WPojSF7C%zw@zYFNk>f{zSQJL5Exayarw$rKQ2 z0AIulMAH$^{;>SC$q-0&GF@kF^J!V5N=mK?_tlA;D#~M}2RtR@>|sr^V^V5sWk2vm zLwg6d6v_ZaM$$gn3P2lejFuXxWhG9}623}z3Kf%z9F;b&`gv43OLF0_9}I|}fOq>G zE1C)xWjGUMriI_*Z#i6e#;1_>h^F;g4kDxqn|+@?KgdRy9&e1m`b60ePbFA4I-W5p z&PPI&%r}KFwW9>RJV#;{|1(6K#;25opZ(cs4T$gBQd7CjH9=c(!&x~&fQ|d1ymjN+ zbDP5ToP74=1)dzCqMMia8iX9*`!=ngj#=pF7&7D49zVT*+IPCq3;L(+(vmd0!7po9 zj{Z!>70&ccXHkW4;B&H)Syf=4C(s>KVNZX{0hW1$Qzs)ik8y+%XD{v#HtEbv@S=Xcc=D=36c-ZVO5B; z@Oq4?eFtxIQ2&8;#qcr8rbGRgk8BT0Z3b*LBt|wK&uRv}*93p2eMsR>BMd`pPw{?K zLpN{(Irp>B`Kb!R*Y#ki4Ku9uarnXrS~soiiSzcw9sER+)V+hU?fl-cbfKJ2Z8t}u3v{_9r!=MogT#Tei*Z%i8oY7nbOF^M%DW)p|GcXgUVfq7p z;iRt7f*E};6`UdDW)6x@;IbHse+KF=J%JVc3d>ruohRs>+_p;+t1V)CS%`iDkqhYe zvSqVph~84{1aZv5nWE1+`C2a@j0)YHG$7gefpTI8SsTRMm^Ncuduw__6~&cGVb|JD zeY#ig1pLmIGku8c$F<=US%XeEv4bsV9Gr}Iv8N@4jkQiHA;Pw+akjwa801WCDyuMS z<@$T7mADcoEem|YDZLuCE+$cP!G+mFWU*hVExV|Ok8DzHx~lA)#B)ONP_wNB+6qOO zuGh7#q;8N{?dH8JbGRf$M0{b~ZGBqW`F{0t;MvJla(H}>EW(+1`-_S!FqqzxRby-W za`fyH55iLyp0N!)*y&WUVbWy?=sP5>_?@9rq)J6ip_haNJs|yQ?~a1UQyID7x5_kq zITy`xQ*s>8tGYr}omX6Pwqx_waSUjdq9dN2ZhOj-%N*9tFkDQnnt8qaY7@6TeX_-&ff%3C z$qMX=V5dMF$GfI8({aCsBZUt zU-x;P%lG@a2EoNT-;UpaeM?z&W#RF48+evsBLzcoo-Cs<7lQHEUaEwHUP~Uqm6CyP znpD`lh)J4FeRNLCZ_6;1(Ypt~ z3PkKJ5QopzwtV^uu~A_{IqfP8<$Y>J6Lo%j>H81e{QRjDX3y~Gq>x7^*HtYTZ8+1- z6p6&V^r&sj0fLp6|CIHB_cYkg~tG%}8x%khR7qhdMN?2Ks!`~eea zXtO~C6{n~R-cm_Doy`idDI>(_3%YEDtG_mdO9l%rpz|SDC{LKAX;<9YxGx?#(qH=G zs#$j6#|Lkm22Yl~a-7hCl^$1Y-bKR1H~Pgt>gMo=lJ7D_c`$`B@g9=x#huAk_zFhZ z=48ED)AV^6OHD_GC-#xVw6*%rT_IsmT`Q@lP-_U`uQLW zYJi0+J?q4SJP-fU1qdc4z9X*JA5KiIJgaD-^OLu)RcATR1Ud+tC!>ZkJ=ek#H2T*u z#2St!HXR>ir+V9_1eT#N6@L;bFrEf@K3X6vomwZvv1B%a>t=G&*{}UP z2@#os?%!i~J=+zfc6}CZL@5r8lscvlL!qM~@USX;Z_5o_LL6(mC9;d-PTNH7X_694 zIquMt>A_MgeTZNDvIfp@Z7g7(j-ip`S74&*Ag?C$uu@K&I*vTkq>i?em5>9kni2D|o z5UvI#i5Uu0-UH@;>0H<_+Zgv5u63NV2XzVUTP0JGuT&!Xy?7v>A~h`gK=@NQJ8PUGi3qk2ek2`;U`-L)gBlSz&g;5Hx$EWSw*5b1f|^ z97z%G51B@*aQAa=`LA!9&vWJ8yG2Hb9O%cdydE#JsAu17P#*Y<%dhxpbV1-V-!)w+ z9poArM`bc(KU*RIEV!G|sd)k;&z&YcnCyiJnnWGU0mVm$Yt>|Mf|)y*HqWHzrt*E* zyOQO1$j7MGz+F7S(qSKH$_ISTf&ohkJfzPh!9?9K=S8eW&9r zq2`)_PKWf;dvMoCN=p8F(Zeuv@?GBw216TW&(}R*RT@&41r4xFMQMTI;lQp6zkHDk z2|#R_ib`bg$tB(}{id&85Onp+2RxD(T&%v-uIyIVnHA_29PwUA2}jZMn{^RPFR*nW_g|%G z;meJUvISiq9OLSPD(lU5AAaSZ80cO0MR#v!y2QZO-Aw_E-z=By3*f=MCQHeBGy!uvEGf0#j$TP2K3*j`?_qYsKHCnuzm|OS6ol}HE z56|zsPrsWP_aj+;qKNYvEA@Jjrs7Rhr_ZhBcBt^B>{|9Cmdf#@0n_s=v<)21ZTj8= zALue)lAF_ahg}A43QV4&)H*$+f6(^S4EU9P?bndOD55-R3t8?OBOYW}6uJ>lF6Zil zoQ9Lim(+2qyL)YMhT;l^L8zqwSu$|^N@Uu; zV;?$hjH-^T8B^P0^vcS5fB;bp9)s&2>VU!pBf)wCiUk~TFYK8LK)T7AZXm|^F_XCy z239pMmge!~6WT`KlTXc&pqi>TD;6WtGr!95=!ZFgiAj`4B$B6{ed5I_;voyK&)Bl@ zi8G$cx0}Arb^_{4f9;q7}%FN&X&f6@6=MhLn~ObhzWf|oZLG06U?5uVi#>ke+G@3k_x+P@#+$K4mjmpx|1EZc_uyZG7GEuEYy7l}{HjqT zPcSW+RI0?#T@v6sO&^XdzYE>eR4e2H4ySHQ5-L33atqKLjl~pyix_Wq>`EJ6d=O!? z|14ZbZNnR%*BwcWV2v9Qsx7`r3K6VoVz~=9)n^I~Ola^T7H!6sR%E4rU(-rN`wc!r zHqe+~x0M9Kj9<)zhXiM#aJER$w*eympuJDML zLMg7L*Wt~F`l~;7Dzo~Z?-akkfh>nJwOts1BHUc{FFYK9-h0bRXEXEiS zpWQ3$Sdw}|PE%wBhec-Cv}xJv0ulRlyOJVSss?vBjz?wf%fW;+xedM4?L+@vJun^J zufEl52vK5tdj+s&-w@-vTSlC}a-F+z zJe(kvgkorAt2<0WnprP=>~8d6JGBT24A{YE>IDq@J3%YOWCU#i_()ZE7gJLE;B6ol zk=^nl-c59nnvO+Bu7p@B#CZ%Vo;=R*WGZz@sLGsC6h3l31_CZWmx@dhyU1w3J174a zgHm&aQx+oV$V;uJ^R{%z7Ck^(ZgBxc9Aq2M$<}tDCoAt}76vWuopj$$!ssT#gi%mN zQBP%b+jSc@N>Uyk@I~owbP*s;$}ITPtRjhG^9RB22&}QEpWev*fQ$`c1(o6hkopbm z$O-kb*&yiloFD=v*yok0HRHP|)&$I7gO%(^5MpP?zl7y>beJ(W=i%5X2&vYM8%jF8 z*pU>zfPz{8k7HvKgXulAM-B~R;TF!tcgmXluzG&jnDF(8TR8*60k60FM45|+iJdT| z$V}VFeEZvz@k4Fu`SSwBY|J=lah8^r<^$=bVAAB-S;XZG6CvK`2sF_9wUim)P5^PJ zIQ!2*zpJRur%^l#*ooy3#U zGL-CMWZq_+&6RuXkmU>eUfsoW>aksEX5lz}wns)5cYo7y~UDrOG zWSi{OvSfTkA&&}zcg(xLy9G%yQ?nq!usi(RL!6!ov@XzIlx3#S(HD@6h?-hv4v)ji zpo1AM*<6=kAahRWgPZD;nJx2d?HY9HM9sxDS-LAl|2J7mPxR5rZX=)j6cJWtwq3ae ztH6QiN^^n6j7C6g!C=a1?>5#Tj&^bDVD(!Tum8s5|t?LY?1k+DXU>F$~F3pce+h;A9e-27|xZrz4IS<~39IWG-h+b!a7?NeZY zEo4|3Z5o)XMw9&(-YM82rO}-kGv@gGMTP;39m{OufXXyG%#HWiD=O^#IU#;#*EvX9 zlDy9KUt0~c451f^$xMqbGA|UpLP#rxo@sKf6ZWIZ?tn@HGAF;cBW>iD*O^c>CTFtg z90w5AsQ?bdE3LFW7lC+v8+aO_Yum>LpVx)R<&7#gCx@38L%|jU@fN&2H7fLtim5M2 zUxwGtSy|`ksW$Nw8@tx)8`9szb1i-M3^Fv5zP;ILowT4(UAJ-tn^K;z+M=qkUy?<= zCYVVV5%m9Rs`~&Ucm?Si2~g7FCOdM} z%=edCrLl>Byg0lXv)z}`A$au4Z~>#Qqx-k-N0!aGM72!<@8D|qA{q-1hkl&F)#Z5a zGh!1Kj1fQ|I)Copa?n85`;{#LmqW4Z3r1sa_bS96b*I5KeY=achyY%z79za2C#A;Bt&68I~C5bOncxQJm}SVG2v@jJ?ChHbU|28SkEGv2JaVX>tO z#${W;jL*2`Sjrz;+ABOSt<}(MU$Du7i3(4pRMjn&L=@)5y+S4mGF0KCq;iJ)syyMQ zb6TA714p(CR=w?ex;0+(-Z!L+N-FJtqdwFzn{PeP#zwi$FN$VAFUei#Q0cJ+1`#L! zclD=N(rJ5Jn%}KFnC>mvh`?>yD6WKQBg}MP0B|Q8uD|DU=zPaEz?pXn#*2ki)L~8d zK48?vYuQH-x@jH>d3o?RPo5sboDvldZaEKcd|G<)C8h=pKe5ad)Aq_vMV08>uw>mt ztBejML>S9e%wM)?3OME&Sb3I1>-s`vT>SlF4n$*tW<7u6zxLL6>XB?9(JL+8uDM7_ z0ta^MM$Vrr~yh9{Y@Jla&QZ-zDli~u@lu?cC6#Ng}Ul&($;a0_5c z1jUQiNV5*os+IIPn1j2cEg*NjxBfK@4f^L9iW2F13!lx)40ufiXxNhktSe^?D+#sL zDBa6{;;0<|W&HKmMOOBaFqD=m<}!` zHuhVLeLHN@1&5kM;aCT#e{aRSKa6NMT{NeGcKtPo(dtn0^OE8t($3Ry?Em<*a+~Na zgOuxj{Q{p1_2Z8=g+qu+rLI>(FCKILAb?h1Y9BI=x9zKJxG_?BB!uBv@M1<)RTa=) z!cJ|VX&xhD#$O_^iz&FZ=$UTmb=qbE?HF#!WkyukhMCtHR0ZAlhFq^BRLeyjQni4m z)?}FJkOh5a7WAm=y8gDCh7jQ;N2{%lnCrd&yKj)NX z{=#C8abAP&5bzRL@~vD7fvt*I@tGZ~5hw4ix`AG!^KJ4$KL_AFpA_3RY&RpXrut4t z#co!`b)CfFj(!&Oj-o!QL&sz)2s&P{`gT_!I>r)9Q#EedFzxqpk-l6L!Z9M?HlpeG zh^{hl`~7^sEfMyEJw#@c7t5g9M-vi6BX&s8BcM~QI-KGkzN{o38 z40fJ0RC_rri4yLvGwAD*Zj=}4!de44I2Txq_w5?)7f&9+a*$?)vbI2X(eLEL-*G$Z zr+k}?zGuQu=FAhYVe-jW6Op|sov!&u;DCnSF+}WfnL$kR(A%-sMRhPkNG`(R=1wQt z0|L|>4h-=Cv@~EF$CAy_dS8N)TOVo~i#rx}pGEn5Zcb(`v(BG|hmu)X`iQ#R@f=il z^1bH54J?V7fSdj;_%MFiYH1+t{i<^W%@e-0pd|T-ekY)tJ1Ib=JR#eqlzQpMC6a7u z)*B$(gT%-kHF0HsrO58a+zTy#V_RnJ7s>|@(!E##JPE91pp!w34#x|89*bQ^{OA4c zr4R%?$#eri6Xl=1VG-(d$rT7QT0&FV@aFcO*o0}sARC=THYAT#;#fJ4ydwVg>{a6& zS-vvZ9s%wMP539N%Va5=%y)s~M{ZODRh;TvEV6Pyf(?R`J|1>4{HG!eT4y^(r%h1>Kso+dUxwd6GvRFQ3wq-CG@vUZ_YvF0O%SD(16+H z1Li)aPwYAiKeYdHsZWI_)aIg;4&%%H{8St#zn=%SO!H$)X$7C3AuuGYy~pG(F6#BX zcHc%9P+x!BbpRsgNklOqXSrO)^ief$*V#<|t*0yfGLMt9sPeKYl44SHtmVbJq^l^mWqUAC5V)Z>& zsGFig{OgwucxF)EjL-77f5$pa=zO)=*?0kdq~)sRss6K<1>Up5lMCG^=F2E?5=gL# z;FAWb4S=6pn%kI=_BYVX;4F+FNrLM30Ti?QB%9_2qnt)t!{rYR7?8(J6hH461+=U?^AnI7>AHP1hJuf-0E4!AUAqlCs}}k_6n)ub(O@ zIBrXHiZ_N9d1`;?6M%u6 ztthS^fg7#*3lxy-8MMlON$tz6)2R~SUEXMmgdu?1;UjqM@6uk+9TRY7n<2;`4tL55 zIjrS0|7xG6mF69Qx%hs2$JWCq3fYZ->*9$uUKt{vEDZG%1*%-5hL zovB!v08}>!;b7cX+Hu=>`*>L&=>9SSSHC_u%a{taO>*}mkPmHCe)49woB|Y$uxGg2 z;1!C`GdLVWNp9=)rVZHbKy3pP9t{&2%^wm=vCUdK7fZ(zN(LAoZqb{6_1D%{xhBxf zHvy_3FI$2a6Y(n=bpRAEnP>G}*#q5ALPPf1;u@(RN7mx9fn7oGh3%ResxcoKRlnld zOBB(iMc;)NdN3NjSJ4g89~Q2ligZWnFL(YH4XkC*1tUj&mZ4xVn&?W2WAY=?qLXbM z9p7s|&ph>)^JnVh9!_!T)~{PTt=M6Go;ul0i+vk$a+ycXBv5!w>)(GaA$BG<$WLXw zps+OAas0#12upbpN9(>9LpF%sYK0qLat`37{|Vq%==m6tBT4UZoH%XcjcGTd77Y26 zDz(PRn{%(e8Q!B+yIlv(joK}S#6xgZw(yy@q_dYH?n8sdc2=B^9r?#9MeXiB>1#tz zqtAgt=mfc`h4UViaR-Sa5}5o3${~GWr68SYPLw%{bpE=@t71>fjLaEZmI-z$38ep;juTdItO?LngTNV8 z#t|Y`1SMkA{bR#S$Hjt9xjvvlX&kfek1I6rH)1?K6p_2wD;=;66Nf9?S5Xh=r()k) z;51#l{>SH(0v{);PZvIkA!xW6ag*izI$pue-0-@qe{J&=^tI=ly7Khp?6%@4HT@Hj zEEgXsIr-aN>hHI-o4IWb!ebVu!Z#uE>c-{(v>K7GiOlORi=6Mu*D;qJ706n; NtfsB{S=s9D{{iZvAZh>r diff --git a/tests/assets/car_tree_bug_zero_shot/images/test/Slide6.PNG b/tests/assets/car_tree_bug_zero_shot/images/test/Slide6.PNG deleted file mode 100644 index cc5e63a173f3e372d11e115e567ccd2b47b19037..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21277 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 opC|sKIRAHw^8`n4D0-n1;LjV8( diff --git a/tests/assets/car_tree_bug_zero_shot/images/test/Slide7.PNG b/tests/assets/car_tree_bug_zero_shot/images/test/Slide7.PNG deleted file mode 100644 index 93e61c87f489d494bafe9d2d77897306a21d1be7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32317 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

    lQQievQIfGHG4G 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 diff --git a/tests/assets/car_tree_bug_zero_shot/images/test/Slide8.PNG b/tests/assets/car_tree_bug_zero_shot/images/test/Slide8.PNG deleted file mode 100644 index 954d34d32e3ce15086be66af38f56fde818c2e45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22874 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 Y7+8KVJQc0h?*u9HboFyt=akR{04LoMdjJ3c literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/dataset/gtFine/test/defaultcity/defaultcity_000001_000032_gtFine_instanceIds.png b/tests/assets/cityscapes_dataset/dataset/gtFine/test/defaultcity/defaultcity_000001_000032_gtFine_instanceIds.png new file mode 100644 index 0000000000000000000000000000000000000000..56c008eac13067d18945da44505d2462602153bc GIT binary patch literal 76 zcmeAS@N?(olHy`uVBq!ia0vp^tU$~t03;ZaS)bFS$!n&hBxL+Q^I>iy YBZELaGi#|D%K?xwPgg&ebxsLQ08Wk)i2wiq literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/dataset/gtFine/train/defaultcity/defaultcity_000002_000045_gtFine_instanceIds.png b/tests/assets/cityscapes_dataset/dataset/gtFine/train/defaultcity/defaultcity_000002_000045_gtFine_instanceIds.png new file mode 100644 index 0000000000000000000000000000000000000000..e658ec33cda661d28014cdf6891a175b988e7cff GIT binary patch literal 76 zcmeAS@N?(olHy`uVBq!ia0vp^tU$~t03;ZaS)bFS$q5N*34OLF4xBl_ Y!@y?6sC^-1(E^Y%Pgg&ebxsLQ06=^akN^Mx literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/dataset/gtFine/val/defaultcity/defaultcity_000001_000019_gtFine_instanceIds.png b/tests/assets/cityscapes_dataset/dataset/gtFine/val/defaultcity/defaultcity_000001_000019_gtFine_instanceIds.png new file mode 100644 index 0000000000000000000000000000000000000000..d2ccdd1f1a7ced01bd18144787ccba00b5db98a2 GIT binary patch literal 76 zcmeAS@N?(olHy`uVBq!ia0vp^tU$~t03;ZaS)bFS$q5N*i3y27GJ%bO WA&WsMXzu$7AZ4DeelF{r5}E)R;}Hu0 literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/dataset/leftImg8bit/test/defaultcity/defaultcity_000001_000031_leftImg8bit.png b/tests/assets/cityscapes_dataset/dataset/leftImg8bit/test/defaultcity/defaultcity_000001_000031_leftImg8bit.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/dataset/leftImg8bit/test/defaultcity/defaultcity_000001_000032_leftImg8bit.png b/tests/assets/cityscapes_dataset/dataset/leftImg8bit/test/defaultcity/defaultcity_000001_000032_leftImg8bit.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/dataset/leftImg8bit/train/defaultcity/defaultcity_000002_000045_leftImg8bit.png b/tests/assets/cityscapes_dataset/dataset/leftImg8bit/train/defaultcity/defaultcity_000002_000045_leftImg8bit.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/dataset/leftImg8bit/val/defaultcity/defaultcity_000001_000019_leftImg8bit.png b/tests/assets/cityscapes_dataset/dataset/leftImg8bit/val/defaultcity/defaultcity_000001_000019_leftImg8bit.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000019_gtFine_instanceIds.png b/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000019_gtFine_instanceIds.png new file mode 100644 index 0000000000000000000000000000000000000000..d2ccdd1f1a7ced01bd18144787ccba00b5db98a2 GIT binary patch literal 76 zcmeAS@N?(olHy`uVBq!ia0vp^tU$~t03;ZaS)bFS$q5N*i3y27GJ%bO WA&WsMXzu$7AZ4DeelF{r5}E)R;}Hu0 literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000019_gtFine_labelTrainIds.png b/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000019_gtFine_labelTrainIds.png new file mode 100644 index 0000000000000000000000000000000000000000..9aa2057dab3fa0e85206da420eb8e57ed921a02c GIT binary patch literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j0VEiVr&{X+DIQN3#}E$LWRWxmCI*IV2GI$& QwR#|VPgg&ebxsLQ08Cm7IsgCw literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000031_gtFine_instanceIds.png b/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000031_gtFine_instanceIds.png new file mode 100644 index 0000000000000000000000000000000000000000..9a2cb23ffa86206a2ab160a7130f677f754e7ec7 GIT binary patch literal 76 zcmeAS@N?(olHy`uVBq!ia0vp^tU$~t03;ZaS)bFS$tekG2`6-t6H*cw Y7+8KVJQc0h?*u9HboFyt=akR{04LoMdjJ3c literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000031_gtFine_labelTrainIds.png b/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000031_gtFine_labelTrainIds.png new file mode 100644 index 0000000000000000000000000000000000000000..ecae3adbec3ff1db3d746884b8ea369f17125e05 GIT binary patch literal 71 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j0VEiVr&{X+DLzjZ#}E$LWRbKVXAdwiFvlbP0l+XkKmDvry literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000032_gtFine_instanceIds.png b/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000032_gtFine_instanceIds.png new file mode 100644 index 0000000000000000000000000000000000000000..56c008eac13067d18945da44505d2462602153bc GIT binary patch literal 76 zcmeAS@N?(olHy`uVBq!ia0vp^tU$~t03;ZaS)bFS$!n&hBxL+Q^I>iy YBZELaGi#|D%K?xwPgg&ebxsLQ08Wk)i2wiq literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000032_gtFine_labelTrainIds.png b/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000001_000032_gtFine_labelTrainIds.png new file mode 100644 index 0000000000000000000000000000000000000000..a459022b94d706a65478a7ad768d4ad26e510a23 GIT binary patch literal 71 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j0VEiVr&{X+DLzjZ#}E$LWRZj)ItLgSm_r#w T=WIAR7o@_|)z4*}Q$iB}mH-Yw literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000002_000045_gtFine_instanceIds.png b/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000002_000045_gtFine_instanceIds.png new file mode 100644 index 0000000000000000000000000000000000000000..e658ec33cda661d28014cdf6891a175b988e7cff GIT binary patch literal 76 zcmeAS@N?(olHy`uVBq!ia0vp^tU$~t03;ZaS)bFS$q5N*34OLF4xBl_ Y!@y?6sC^-1(E^Y%Pgg&ebxsLQ06=^akN^Mx literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000002_000045_gtFine_labelTrainIds.png b/tests/assets/cityscapes_dataset/train_dataset/gtFine/train/defaultcity/defaultcity_000002_000045_gtFine_labelTrainIds.png new file mode 100644 index 0000000000000000000000000000000000000000..3ad5c050e60d597336d8188f5d36ecf66d05cab2 GIT binary patch literal 71 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j0VEiVr&{X+DLzjZ#}E$LWRbKV2M;nZFefvL T^1qV)15)AX>gTe~DWM4fo0<+t literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000001_000019_leftImg8bit.png b/tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000001_000019_leftImg8bit.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000001_000031_leftImg8bit.png b/tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000001_000031_leftImg8bit.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000001_000032_leftImg8bit.png b/tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000001_000032_leftImg8bit.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000002_000045_leftImg8bit.png b/tests/assets/cityscapes_dataset/train_dataset/leftImg8bit/train/defaultcity/defaultcity_000002_000045_leftImg8bit.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/classification_dataset/test/0/11.jpg b/tests/assets/classification_dataset/0/11.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/11.jpg rename to tests/assets/classification_dataset/0/11.jpg diff --git a/tests/assets/classification_dataset/test/0/14.jpg b/tests/assets/classification_dataset/0/14.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/14.jpg rename to tests/assets/classification_dataset/0/14.jpg diff --git a/tests/assets/classification_dataset/test/0/16.jpg b/tests/assets/classification_dataset/0/16.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/16.jpg rename to tests/assets/classification_dataset/0/16.jpg diff --git a/tests/assets/classification_dataset/test/0/17.jpg b/tests/assets/classification_dataset/0/17.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/17.jpg rename to tests/assets/classification_dataset/0/17.jpg diff --git a/tests/assets/classification_dataset/test/0/18.jpg b/tests/assets/classification_dataset/0/18.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/18.jpg rename to tests/assets/classification_dataset/0/18.jpg diff --git a/tests/assets/classification_dataset/test/0/23.jpg b/tests/assets/classification_dataset/0/23.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/23.jpg rename to tests/assets/classification_dataset/0/23.jpg diff --git a/tests/assets/classification_dataset/test/0/26.jpg b/tests/assets/classification_dataset/0/26.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/26.jpg rename to tests/assets/classification_dataset/0/26.jpg diff --git a/tests/assets/classification_dataset/test/0/3.jpg b/tests/assets/classification_dataset/0/3.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/3.jpg rename to tests/assets/classification_dataset/0/3.jpg diff --git a/tests/assets/classification_dataset/test/0/30.jpg b/tests/assets/classification_dataset/0/30.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/30.jpg rename to tests/assets/classification_dataset/0/30.jpg diff --git a/tests/assets/classification_dataset/test/0/4.jpg b/tests/assets/classification_dataset/0/4.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/4.jpg rename to tests/assets/classification_dataset/0/4.jpg diff --git a/tests/assets/classification_dataset/test/0/5.jpg b/tests/assets/classification_dataset/0/5.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/5.jpg rename to tests/assets/classification_dataset/0/5.jpg diff --git a/tests/assets/classification_dataset/test/0/6.jpg b/tests/assets/classification_dataset/0/6.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/6.jpg rename to tests/assets/classification_dataset/0/6.jpg diff --git a/tests/assets/classification_dataset/test/0/7.jpg b/tests/assets/classification_dataset/0/7.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/7.jpg rename to tests/assets/classification_dataset/0/7.jpg diff --git a/tests/assets/classification_dataset/test/0/8.jpg b/tests/assets/classification_dataset/0/8.jpg similarity index 100% rename from tests/assets/classification_dataset/test/0/8.jpg rename to tests/assets/classification_dataset/0/8.jpg diff --git a/tests/assets/classification_dataset/test/1/0.jpg b/tests/assets/classification_dataset/1/0.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/0.jpg rename to tests/assets/classification_dataset/1/0.jpg diff --git a/tests/assets/classification_dataset/test/1/1.jpg b/tests/assets/classification_dataset/1/1.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/1.jpg rename to tests/assets/classification_dataset/1/1.jpg diff --git a/tests/assets/classification_dataset/test/1/10.jpg b/tests/assets/classification_dataset/1/10.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/10.jpg rename to tests/assets/classification_dataset/1/10.jpg diff --git a/tests/assets/classification_dataset/test/1/12.jpg b/tests/assets/classification_dataset/1/12.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/12.jpg rename to tests/assets/classification_dataset/1/12.jpg diff --git a/tests/assets/classification_dataset/test/1/15.jpg b/tests/assets/classification_dataset/1/15.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/15.jpg rename to tests/assets/classification_dataset/1/15.jpg diff --git a/tests/assets/classification_dataset/test/1/19.jpg b/tests/assets/classification_dataset/1/19.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/19.jpg rename to tests/assets/classification_dataset/1/19.jpg diff --git a/tests/assets/classification_dataset/test/1/2.jpg b/tests/assets/classification_dataset/1/2.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/2.jpg rename to tests/assets/classification_dataset/1/2.jpg diff --git a/tests/assets/classification_dataset/test/1/21.jpg b/tests/assets/classification_dataset/1/21.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/21.jpg rename to tests/assets/classification_dataset/1/21.jpg diff --git a/tests/assets/classification_dataset/test/1/27.jpg b/tests/assets/classification_dataset/1/27.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/27.jpg rename to tests/assets/classification_dataset/1/27.jpg diff --git a/tests/assets/classification_dataset/test/1/28.jpg b/tests/assets/classification_dataset/1/28.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/28.jpg rename to tests/assets/classification_dataset/1/28.jpg diff --git a/tests/assets/classification_dataset/test/1/29.jpg b/tests/assets/classification_dataset/1/29.jpg similarity index 100% rename from tests/assets/classification_dataset/test/1/29.jpg rename to tests/assets/classification_dataset/1/29.jpg diff --git a/tests/assets/classification_dataset/val/0/11.jpg b/tests/assets/classification_dataset/val/0/11.jpg deleted file mode 100644 index d381e4ece59532e16a893c5211f8969b39c6560a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 730 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-s5d>+Y96VISWY?Cv-rQ)eGF@3H&-t&PiO z@orpaRKD_TQxfZT}2pxnDPHj0LQidp8x;= diff --git a/tests/assets/classification_dataset/val/0/14.jpg b/tests/assets/classification_dataset/val/0/14.jpg deleted file mode 100644 index b272c3dabf88f5c3ea2b0df92e3bf879c50233cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 747 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+Pk}rJKT;v&aTjQ0a-`6Md$2PCNu;u diff --git a/tests/assets/classification_dataset/val/0/16.jpg b/tests/assets/classification_dataset/val/0/16.jpg deleted file mode 100644 index 3696014a7b5809e1c35083b82904c58934a46e95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 732 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-sBPyI21i>TfQ(zgc=;g5kWm{#zp^J6~DY z`LsGmNi)s*dV{^x)errD_SGM-|CV*<%Kj(f<^LIC;{P)^nm)gF^qlBUp|A_TWp=l3 m*fD2k(sIpWQ}xAR7q{L%z4Q5&K-Vhv^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R8xvOj>Rq*|Eo>$uON!vc*z4bN`~Uom z{?BlK>3;^UXX`sdqI~0?@0{|a)L6r-U{+Y!mCv`d-DCPJN@urpiVEZ&T5&*LW6}1O jor{*<`BZ2U%XN9t-Jj9%#T)0kiiAqEiUc~y|Gx^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R-~GjE0bnm_x=p6sWOtxw7pzu;dQxcXMv zYP*@werifr&N%nM-{+U$>&UCmpVrKtTOq&tcg5eVKeO)Le!Ki}lCQJN&dUjr%PM9@ zZfj_nJV*FWb)ehAwRc_x=oqVbt-9Ul;CPPXUG0md-nWYWeJ^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+RFKYCQS@+-i^Kx-_*_r%=oxksGoxjdg z=GNK6M_#=sdpPBZ{FE@mRd>VvZkO6x?W|F3XwwbZJ$bu+=)T=Q%U-1ix{7Xm#wrr% K0K)SBZvp^LBK64t diff --git a/tests/assets/classification_dataset/val/0/26.jpg b/tests/assets/classification_dataset/val/0/26.jpg deleted file mode 100644 index 1ac24d776b4761e71442b27a0d3f2aa459a2f23d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 759 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+RH!f^hVQRmn^1RZ|>cS0;*X_U-nS{jMTm$7e-uxuUnm z{O_#T*&la5Ww&^FrTSv|irDx!zkPOpOV2;WW_*3Yw?$p^JU$mk2iLC+{}y+A?x(Wd S+iSPo585gc7?M!`|0V$Wa~LZC diff --git a/tests/assets/classification_dataset/val/0/3.jpg b/tests/assets/classification_dataset/val/0/3.jpg deleted file mode 100644 index 04f4a13b4d03328f0731feb89a14357ed73f7bdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 782 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+86e3&Z&QX>i)mK`ai<$lm%AYdiC9Q z&T_SRspn6s%0=ftum5E8t?TAT9YMzx$2J_9xUxk^&_VL#mqlBb+f}U8TRy+%u#IkI zz~tTC8+&8T|ES*o^7uc)#QNV-8Y@@2iRPYLpBy8W+I6tFHZEq*wSAp$x4rys_%+u3 qx8KXOPkYv`5SHFvu73Gmm&WRzkZ_$c%hlIc#w}hQc+bE1|4jhrIU$h% diff --git a/tests/assets/classification_dataset/val/0/30.jpg b/tests/assets/classification_dataset/val/0/30.jpg deleted file mode 100644 index 81b0373220b7f84d3e34ed9ad8f7fc08919f8b73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 766 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-uu@{?mizE^M6`8D6O8A#p;seQ(BphC|x5 z{~2s^|1-Q__Md_4+~u#FB{y$ngi6jVoA6*_PxFo6Vf}wp!|#7Sb^qVb{EzFR_D4MX zdv{Is&iZfe8~-z8KYRY^P4anRN1F*7Sr;cQ>6^(e5@@s5=9A9>wu#*WRw;8X#%g`L Z@-eIE^FH5Qf)`yyPH<@~YOw!*6967)7OMaN diff --git a/tests/assets/classification_dataset/val/0/4.jpg b/tests/assets/classification_dataset/val/0/4.jpg deleted file mode 100644 index fab3ec5a61b0d67102003fd29f19a4157746f4f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 723 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+Dra3wA5YxE&X<0%DwcPf9~W-oV)X@ zWX-G4UhCpg>w?PKoOJU|zqQ_9c=lpZ*M&!1SApuA-oKGg{`BzEpY__-euX_%Ul(H3fkH diff --git a/tests/assets/classification_dataset/val/0/5.jpg b/tests/assets/classification_dataset/val/0/5.jpg deleted file mode 100644 index 0689bda8e296dc757c1a661b557bcd871138561b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R-a@_)G0zx@55L9zZ@=$Y#a>oX(%Gn{|B z|L@=UkLz?&y(T>UR+oPH!!vHyu5bIJJ#Ssxon3uDZBHN9Gx^PTefi$ri=J}MaE?r% w|Nhikx0h3!wSArl6mgtfwCmCBuAHB@KK5jtbWgb%*QK$_h)ZKpgZ=-T00v+S!T^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+SM)>b+>Ap&q|*fnWDucz-GW-_&)N= zN~ywU6R$TN3_W)Bn@PDr#=2GS4~G>*o;NyenejM{VZ$+p^GiimhV_P4%u4=r>j$UC z^{w~Qi>I#b)sFplW@|>>*=O&He%{}{L2llaz|f#MS$dJWJOw*;-SC=st$E8n|Ia#i yek^_~w%Pfc?Y<^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+N&>q?dFcvl1Q7M)4f|cSthmfjkeZR uRX_8dyei2&zG-@^mxrxdHfitO-D^#=f4OVAii9nnClct&0K)bEZvp_MT<5v~ diff --git a/tests/assets/classification_dataset/val/0/8.jpg b/tests/assets/classification_dataset/val/0/8.jpg deleted file mode 100644 index 257140c8bab7946f4bd87499a8f9819df6b60cb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 785 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-s5ZPyKb5?kO$2Z=qB1ApKX^`aeqB?|;5_ z|KG3t538(fkA&@7W9)M~IrgT7x@3@bc=&$?PPXa)8SLYK^6o#hW20%dQ`xe0d-+$K zG_Xul=6l{Vt7rW+&&<@z?=5bh_SDI{cQ0t^Wl61yC+WW~2EP^;jLq8Y%bnM|?ZJbd shYs&uOM`O{TISXK(-V2vF-Oz-sM+*b{bfs~G#0IS&ZV)a!T$eE07w2DSpWb4 diff --git a/tests/assets/classification_dataset/val/1/0.jpg b/tests/assets/classification_dataset/val/1/0.jpg deleted file mode 100644 index f0fd22622f052d23b376035dcb3ee47b555c3370..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 797 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!w2u`%y-WtbQey_oNb-l>uq@8*>dA{tC zZR*ov)&45G>YTfFj;Zm=zsRnwzmL5NowzQhjeUi(O_#=ssTQ7h7pZy)l~3}W`6bXbGX4Ke E0QyWGssI20 diff --git a/tests/assets/classification_dataset/val/1/1.jpg b/tests/assets/classification_dataset/val/1/1.jpg deleted file mode 100644 index 7df9200ef65d73c61cc15c36c9458deb43aa2615..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 791 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+RcRs9duYag^Rdv(v@SEG5Qtv-~e@@cP zaN^(hm-C-30Hu-rM8M&H@BK?aj9$b9vTX*?L)fw|? zg=Z!%-F3v}6yu3B+s?-mT}97+jjFrh($ diff --git a/tests/assets/classification_dataset/val/1/10.jpg b/tests/assets/classification_dataset/val/1/10.jpg deleted file mode 100644 index 8c06ad5cdecca45f8bea7e716d5b86aaed2a1ba6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 825 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+BdeuGyf=h?{s-x`sM9sO}7+TSAOEz z7~pvzKkD(Z%)!}NtwSr)bovF#Ej+{P2aj~R$TpMn`gVF^*gQ3wzlq_6LyWw zLMdX(mi$Q~491^JA9ZQHPJ9{e8PfYEPMO^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+BdeuGyf=h?{s-x`sM9sO}7+TSAOEz z7~pvzKkD(Z~ zIe$;uIkV*V!ml#c7ToA~q^Y&x=J(&dA7-pbT5)WPu;=$rWfoOmb=4pF^(HQkxOqCa zE~V+7#f&wXO`#4JKX(n*dEvZt!{Xkq-n@3p$MZ!ae$0L&pValpe)G@S pf1U=;y^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+E=#BZ@XpsuQ$0;_w0{J)fG<@HmNbD zvOIqJ@@&zgjZ-$P^jaR=E#1GkscUN9@@W^tO|v83n9hD}(Jk>{$0SvqO2vrA`E?rW zW}NiY)LR~-vRqX4%JR6@V6Yo*Q7?JEvq(*I~id#hsp^ z;oH~0D1Njd#4yusPu}#|@e>bcnb+TvT6*{0dHuJ-HoJ}I?w&CxeSeh3qU}8)K^`xF KMAuFE|2F}__9;gI diff --git a/tests/assets/classification_dataset/val/1/19.jpg b/tests/assets/classification_dataset/val/1/19.jpg deleted file mode 100644 index c736d1da246b073bc038d82fd520f17439582b20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 741 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+IQ?9CRD`#YpVaLum7JxIL6{mXpFu7 z=4H>T-(LQZK6^%Tc4B4iRP~_mw|+(4oqXrRFYQ$y|7}cM`+7sn%gaA6tle}hI$2M` zSSGRIjjQNorzNX`gQrwRzr6AKaOl*iz?BxCwqN}6r!TBaW3BI@E{z5J|8D{SQ)>*s diff --git a/tests/assets/classification_dataset/val/1/2.jpg b/tests/assets/classification_dataset/val/1/2.jpg deleted file mode 100644 index 6fe0e3c69d0295a0ad069be2c61ac094012cf024..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 814 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+86gQKCG3FzOa1fm7G5>r#)TF`)t=k zrn{io*wg@lAagefzeyyw)Sz z+4ASgj>S4Y*eJ`nXgPaNn;zd%lS_WLCN~_vd_1@`Q*Bb&^{3B%&kGONEzR{hq%1mb zVQIc(?oq{=6NFzkRDIA`v}>uTYNqzwqL#F^s)g63uB|R-b}U|b?<{w6Ns-@~^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+Pgo*H+{J;_~LS0oPWtA)x$0ezI{??TCV}OCbPY{U&f-Y Y9zDKkq8685r8gZ2Jm`9?KluMm0N5xiH2?qr diff --git a/tests/assets/classification_dataset/val/1/27.jpg b/tests/assets/classification_dataset/val/1/27.jpg deleted file mode 100644 index 9b997c560ea189611beff0573b40134253215d6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 830 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R-a*2*7{TDN_v^NQOskt)V{w>DX`NhB?| z2)+}p9ry6Ve};c9z1IJ3{bzV_Y`wAR(U8M|!OQo){b79U+{dj26{SJb-roLD|D@vG z#-;v?vNmU4*OxxaclWHMs$Yn~F^4x9YwJ7YKUu%B|91L6gX>z++ikN~x(QEvtNuf- zPiV=ihcB-H+20q>T7B);wbhwS2Xj~6JIwVwr|{gV#p>z|@=+RVRV7YFc|E(-lD1a0 oXzS54-;1|=H}*6++k465=!^#mDf6>57R~b1R$MY^ll}jj0N}qmKmY&$ diff --git a/tests/assets/classification_dataset/val/1/28.jpg b/tests/assets/classification_dataset/val/1/28.jpg deleted file mode 100644 index 951c69ae0b2821a3bbe73d651a6c626e896fd267..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 785 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!^izxKg$0J^o3q89rv?$5(1$&&K- z{%RK=k>~r*uwjdJulMiZ^PkH8y?^t&cpCTh!mnRe*)d$xG4U;{p6gbxCbli(a8BB3 zvEUvJ+s1XPkI5NaD2?a3cQZ1+a=TE@jd|Yd3SRlEI=tYm*fHzIiO0!YnV(|geQmyO p{Cwk#TcXS%fhF@d@5z66H-Fu>zy8{by0mZEwP-BrVgTX)Hvt`Y9b5na diff --git a/tests/assets/classification_dataset/val/1/29.jpg b/tests/assets/classification_dataset/val/1/29.jpg deleted file mode 100644 index 3fc3b963e5e51bf7071e57b3840613216d60453e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 803 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R-a{xfj>kp3r>s~3I$t^DSk8AmstxIag0 z&hvtQ-(SYGme+BATsHgRS*dL66MFO9cVs%Yiu8yE-fg#$dUQrL&LqG6$NqKC=g#sL zm9egf?|Z%Z&zbpRKfM15=5PGZu)KElTkHP}Pp6&TUG|@0OY!&pURu}tYC@AA^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!v|6Knzvy&r3cPhpP2ry(*EzKtZL20?RDRd z753R}&3`-XKf~wCUrfL3tP7t0$l&7YXn}?5)vJ&DM!h^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+fReRQT9h}UPzH-&p&Fp`wkNn&e$m%7U-R1p3eaXQ%R?q2fP-mG+_atX!MAZklCnbtwsS NO)*&1rNR9FCIH<(^t}K8 literal 0 HcmV?d00001 diff --git a/tests/assets/classification_dataset_class_incremental/2/22.jpg b/tests/assets/classification_dataset_class_incremental/2/22.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9390fc912b1491e5c27fae0624fa808f113d1a6b GIT binary patch literal 773 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R({74+w}wdu&*?w3rS5qCe+HZX48d{N zq9;5m=JH#s>8kQ}b?xLA^NTI729%}wit3)yDd$;buvkLARrG22VlB~Bujr#&zgvsV zy?Uh0UoEsIU3PQW{w@1oPXA~7pW)h~uD1uD{Wv2dYh&|f_P&#^Kl^VqS}tk5Y0ag| gOPe1{zMZA9Xh+u7sgb9a+`XNDmO0QD#r(*OVf literal 0 HcmV?d00001 diff --git a/tests/assets/classification_dataset_class_incremental/2/24.jpg b/tests/assets/classification_dataset_class_incremental/2/24.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d6e3a633de2c61a177c9b05083f22d2b9540e6b5 GIT binary patch literal 751 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+D^wzDkL^(%ii@kpBVcyk168Uze9TG z&i*~(SF`F_t?2S{k?fqO$pIBl_^Y&iuItW=jSQT#zO>dh>MxV<^KfIWyK65w3Wo7` z=6rM&J>PWf>cP-&SIT7;Em?E?PMo_APuCjG^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R-qw{83F_I`feowBozK?e&ye_QE!ZUmy dZ^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+RT6fIiO1%E18Q;z=eel8b2H$XvB?$+@-f<;zd|6MCjhy%@B0&3}f+ z7wzSryNbU5(Ej_&e})7987yWd*$M3V&(QDw$9}Fzpu6~ghW*ZexIa%w{3rbXCIFKp B41)jw literal 0 HcmV?d00001 diff --git a/tests/assets/classification_dataset_class_incremental/2/9.jpg b/tests/assets/classification_dataset_class_incremental/2/9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ac59b824f0df445598aa3bde948e85eb063a4d2c GIT binary patch literal 711 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+R+Rkq-{GD8Pe)j2eMOBaX@~}mR%btI8 zD`wTZ@BbOpT$U}~Ip0+zd|pIY`>n*^YG?m5m{0FX_iT@z{{Kw?=i2-! literal 0 HcmV?d00001 diff --git a/tests/assets/classification_dataset_class_incremental/3/.gitignore b/tests/assets/classification_dataset_class_incremental/3/.gitignore new file mode 100644 index 00000000000..5e7d2734cfc --- /dev/null +++ b/tests/assets/classification_dataset_class_incremental/3/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/tests/assets/common_semantic_segmentation_dataset/supervised/val/dataset_meta.json b/tests/assets/common_semantic_segmentation_dataset/supervised/val/dataset_meta.json deleted file mode 100644 index 8de5762d2d2..00000000000 --- a/tests/assets/common_semantic_segmentation_dataset/supervised/val/dataset_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label_map": { - "1": "Rectangle" - } -} diff --git a/tests/assets/common_semantic_segmentation_dataset/supervised/val/images/0001.png b/tests/assets/common_semantic_segmentation_dataset/supervised/val/images/0001.png deleted file mode 100644 index 02d1fe884f7fd5778da85fbbddc1b443a7bc6a85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^U}W=jaSW+oe0$K45h&QK@HW26 uu!gNjbLW&8FFn=ile|wZeG)Y6xy;V<85$cF<@^DL0fVQjpUXO@geCy&JcBX- diff --git a/tests/assets/common_semantic_segmentation_dataset/supervised/val/images/0002.png b/tests/assets/common_semantic_segmentation_dataset/supervised/val/images/0002.png deleted file mode 100644 index 02d1fe884f7fd5778da85fbbddc1b443a7bc6a85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^U}W=jaSW+oe0$K45h&QK@HW26 uu!gNjbLW&8FFn=ile|wZeG)Y6xy;V<85$cF<@^DL0fVQjpUXO@geCy&JcBX- diff --git a/tests/assets/common_semantic_segmentation_dataset/supervised/val/masks/0001.png b/tests/assets/common_semantic_segmentation_dataset/supervised/val/masks/0001.png deleted file mode 100644 index 4d6037074201621f7afd108c703522b6854beecd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 420 zcmeAS@N?(olHy`uVBq!ia0vp^4Is<`Bp9BB+KDqTFlKwYIEGX(9zE#D#bC(8ym97# zd#mbKCj&TQH>jirsbN8%94>88=2zjhrhpMXsm&Q^3SB7slT(*G^e07r>mdKI;Vst0HSQIOaK4? diff --git a/tests/assets/common_semantic_segmentation_dataset/supervised/val/masks/0002.png b/tests/assets/common_semantic_segmentation_dataset/supervised/val/masks/0002.png deleted file mode 100644 index c410b755b543abda8103b97d40281de37080b3c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 652 zcmeAS@N?(olHy`uVBq!ia0vp^4Is<`Bp9BB+KDqTFa>+MIEGX(9zD2`lfjUO<>1|a z-TU~@xI4Ku9x(ni!$z(DWHM<~xRG#OQ0u&ylRiH@MXQQbjUTFqzBoDcOw0U`h3cb{ zz(6>?gv&Nk4Hzg!E1mqO0Rw51&#DWPk3TW2^pTrwrYdAj + + 1.1 + + + 1 + v1 + 10 + interpolation + 2 + + + + 0 + 9 + 1 + True + + + + + + 24 + 24 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/assets/cvat_dataset/action_classification/train/0/images/00020.jpg b/tests/assets/cvat_dataset/action_classification/train/0/images/00020.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ca2f7df2c8cd72b5e6ad4ded7da83474ce6c8a47 GIT binary patch literal 735 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-w0oa{pej&XO>9yLs^5Vhf9RS=xK;UL8)q zm2`8zbzMSn-(LQ`WpeWW879bX|HJ*C!O3*yqxV0H|1&iHXLz&qpuebP__ti`*njNT qk~Ybh%B)OI)+{bM`flyo^_Tuu&p9g+82kKWt4N>&D-g^7zX<@@s{%3r literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/0/images/00021.jpg b/tests/assets/cvat_dataset/action_classification/train/0/images/00021.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a983e07e46d8d3ad38535922e90040314ea1c75e GIT binary patch literal 728 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-w6zdf$F2>+Z3>V0NjT@%*FO7rQUL&q)%x z@&={>{$& jck0)BnZ-r-OY=|tXXrc;=qh@zW=5c^2m=r^{=W$TS+4^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-sBPyI21i>TfQ(zgc=;g5kWm{#(5Z7vH~i z;Ic2KD@~+@YplkVjHIYD91`w|Qe-i*rmjdPh literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/0/images/00023.jpg b/tests/assets/cvat_dataset/action_classification/train/0/images/00023.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fa5fa2b628034e1571546d2441d06d9e89b0c802 GIT binary patch literal 727 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-s5d>+Y96VISWY?Cv-rQ)eHwRjqxcv+${J zH(Y|wl&@!~GtED=|C7k{e>>*?<;-4oW&acL^8XAm@&6edO%KM4T9sA`h1qS^K2dOE h5A%^LQ>IM)mUXM9-*!=#_MV>xi@G!zfSCFJO#r^S{)+$r literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/0/images/00024.jpg b/tests/assets/cvat_dataset/action_classification/train/0/images/00024.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6da595f5029f1058dbd8cb57bacd32a3e1753d8 GIT binary patch literal 713 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!w2u`%Dm+fiLsZ=3XIqajPI?lKV-kE{`~rX zF7Y21AO6qqNoL!BhQ|6AYP)AWIGVKFE6dP3`L&TuJMWCdrD1E9%$gi!ded&@cbjuv T8tXnkIn<@GfC-5C|K9`v@38nX literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/0/images/00025.jpg b/tests/assets/cvat_dataset/action_classification/train/0/images/00025.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3f709fd2a536d15cf0b7ad55bef03bd6218920fb GIT binary patch literal 739 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!v~4Sl}kM)q~2{>@l|b0{X_Yy_Rp{XXHe4r zaW&TbpVPGYUvJufQH*PkiCE+FV#}ghH^)hdpQVcDrd^%8W&5j{&o;SzIQ7xu@M6)# x7p|gqOaC({dhh@B=0C$P!9}|sy%#w@do3&1q^1|Qv;$p5-4b0z0@(lG1OV<>2kHO- literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/0/images/00026.jpg b/tests/assets/cvat_dataset/action_classification/train/0/images/00026.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2a8b09d4f39aa66109889624d2cdb25e8570b4c3 GIT binary patch literal 732 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+P{xdw3ziR*d`hNx`{U6s-t5OReeH2=p z7q|T4hkyRgM&IV$+TdpByW?|WqEBaAI~%>7SU7P);lz1YLf;G2pD23%kNH2t of-dc4bLK2q@@w(YV}ZRqzFvX1jI!UA8EY)++B^eDF#Nv>030F)761SM literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/0/images/00027.jpg b/tests/assets/cvat_dataset/action_classification/train/0/images/00027.jpg new file mode 100644 index 0000000000000000000000000000000000000000..48da9d8fed35dbfb489e81648fb39b9699b6f12e GIT binary patch literal 743 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+Pk}rJKT;v&aTjQ0a-`6Md$2PCNu;ujp^$wfk;}5?A zqxNb2XPB@g{_BzY7rw5!Zh0kF<_DiWQ*hzXqvdYWuD4^)SZOTUV$sy4v4H>oO#s1s B4K)A& literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/0/images/00028.jpg b/tests/assets/cvat_dataset/action_classification/train/0/images/00028.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0f509735bc8d3e0c23772900a5683cfcf47c653a GIT binary patch literal 751 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R-~qhEag9-wpK;4ZUsd|xFG9xo2b-udA_ z!=Hw?`ya&r5?q%PniDS?x02*`K#R zW4CyH;rGSpeR}^HrrZCJ|99FoPHyo^U*9=D_wh;Cq^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R-~qB6_3UzE&!c&~BWCksSA-dO6kIx}*w`JZ6R(|h&49aGdjVlA-kLOJvK z2em8K^&gG@#NGXGH~(MO6-(Pzy}JBtLV5&Og-q~b&uwew?f%Cp(<%}e%RWgY(3PS7 G|4jhEzzog+ literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/1/annotations.xml b/tests/assets/cvat_dataset/action_classification/train/1/annotations.xml new file mode 100644 index 00000000000..71288527b47 --- /dev/null +++ b/tests/assets/cvat_dataset/action_classification/train/1/annotations.xml @@ -0,0 +1,61 @@ + + + 1.1 + + + 1 + v1 + 10 + interpolation + 2 + + + + 0 + 9 + 1 + True + + + + + + 24 + 24 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/assets/cvat_dataset/action_classification/train/1/images/00020.jpg b/tests/assets/cvat_dataset/action_classification/train/1/images/00020.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3f75798fdf30140bcf52c539fe24d24f3be4982e GIT binary patch literal 748 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R-~qB6_3UzE&!c&~BWCksSA-dO6kIx}*w`JZ6R(|h&49aGdjVlA-kLOJvK z2em8K^&gG@#NGXGH~(MO6-(Pzy}JBtLV5&Og-q~b&uwew?f%Cp(<%}e%RWgY(3PS7 G|4jhEzzog+ literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/1/images/00021.jpg b/tests/assets/cvat_dataset/action_classification/train/1/images/00021.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0f509735bc8d3e0c23772900a5683cfcf47c653a GIT binary patch literal 751 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R-~qhEag9-wpK;4ZUsd|xFG9xo2b-udA_ z!=Hw?`ya&r5?q%PniDS?x02*`K#R zW4CyH;rGSpeR}^HrrZCJ|99FoPHyo^U*9=D_wh;Cq^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+Pk}rJKT;v&aTjQ0a-`6Md$2PCNu;ujp^$wfk;}5?A zqxNb2XPB@g{_BzY7rw5!Zh0kF<_DiWQ*hzXqvdYWuD4^)SZOTUV$sy4v4H>oO#s1s B4K)A& literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/1/images/00023.jpg b/tests/assets/cvat_dataset/action_classification/train/1/images/00023.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2a8b09d4f39aa66109889624d2cdb25e8570b4c3 GIT binary patch literal 732 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!_R+P{xdw3ziR*d`hNx`{U6s-t5OReeH2=p z7q|T4hkyRgM&IV$+TdpByW?|WqEBaAI~%>7SU7P);lz1YLf;G2pD23%kNH2t of-dc4bLK2q@@w(YV}ZRqzFvX1jI!UA8EY)++B^eDF#Nv>030F)761SM literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/1/images/00024.jpg b/tests/assets/cvat_dataset/action_classification/train/1/images/00024.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3f709fd2a536d15cf0b7ad55bef03bd6218920fb GIT binary patch literal 739 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!v~4Sl}kM)q~2{>@l|b0{X_Yy_Rp{XXHe4r zaW&TbpVPGYUvJufQH*PkiCE+FV#}ghH^)hdpQVcDrd^%8W&5j{&o;SzIQ7xu@M6)# x7p|gqOaC({dhh@B=0C$P!9}|sy%#w@do3&1q^1|Qv;$p5-4b0z0@(lG1OV<>2kHO- literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/1/images/00025.jpg b/tests/assets/cvat_dataset/action_classification/train/1/images/00025.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6da595f5029f1058dbd8cb57bacd32a3e1753d8 GIT binary patch literal 713 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!w2u`%Dm+fiLsZ=3XIqajPI?lKV-kE{`~rX zF7Y21AO6qqNoL!BhQ|6AYP)AWIGVKFE6dP3`L&TuJMWCdrD1E9%$gi!ded&@cbjuv T8tXnkIn<@GfC-5C|K9`v@38nX literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/1/images/00026.jpg b/tests/assets/cvat_dataset/action_classification/train/1/images/00026.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fa5fa2b628034e1571546d2441d06d9e89b0c802 GIT binary patch literal 727 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-s5d>+Y96VISWY?Cv-rQ)eHwRjqxcv+${J zH(Y|wl&@!~GtED=|C7k{e>>*?<;-4oW&acL^8XAm@&6edO%KM4T9sA`h1qS^K2dOE h5A%^LQ>IM)mUXM9-*!=#_MV>xi@G!zfSCFJO#r^S{)+$r literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/1/images/00027.jpg b/tests/assets/cvat_dataset/action_classification/train/1/images/00027.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8685452a16571ee7f6618a882ec92f565f7a4c5d GIT binary patch literal 732 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-sBPyI21i>TfQ(zgc=;g5kWm{#(5Z7vH~i z;Ic2KD@~+@YplkVjHIYD91`w|Qe-i*rmjdPh literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_classification/train/1/images/00028.jpg b/tests/assets/cvat_dataset/action_classification/train/1/images/00028.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a983e07e46d8d3ad38535922e90040314ea1c75e GIT binary patch literal 728 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-w6zdf$F2>+Z3>V0NjT@%*FO7rQUL&q)%x z@&={>{$& jck0)BnZ-r-OY=|tXXrc;=qh@zW=5c^2m=r^{=W$TS+4^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!-w0oa{pej&XO>9yLs^5Vhf9RS=xK;UL8)q zm2`8zbzMSn-(LQ`WpeWW879bX|HJ*C!O3*yqxV0H|1&iHXLz&qpuebP__ti`*njNT qk~Ybh%B)OI)+{bM`flyo^_Tuu&p9g+82kKWt4N>&D-g^7zX<@@s{%3r literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train.pkl b/tests/assets/cvat_dataset/action_detection/train.pkl new file mode 100644 index 0000000000000000000000000000000000000000..86610a1a4500522457044767d09d31f4457d0bfb GIT binary patch literal 3185 zcmbuAJ4gdT5QY;q2r39-t?d;Od_O?B!a^|+&{`0pIbxxPkQ6F{jW0Mz;VNirZ)IU+ zAvS_S5F{WLK2T0=Y^?PzlR5o$Pw&<(oCwLs?9O~M%aJXkE>t>8LNlvdTUwnX%!b+A zOnN>#IhRRB?T^XK^rSFrCydP8Od@9($&A<(+O|eN4J|hA58W;t==5))9B<0eD#uq{ zn2{3Kj#JtBbkc^H6-KYvSYH!kLW@j2p9j&n|3 zSaB~sP*c433+n_JzHmWzPzaH>|C!*4L)}Ckzcx zz9wac1}GPRU}%7HYY+?#P;Od+p#jP|5eyAb7M);dfbs|;7#g5F90`U7_#XD(MQ@iF zzy&}kPYd24lBq#3G=O1vT}#JZ;0Z$m7>0$gble4=Ff@Q+*dmsWyTB8M1~3ex^=%h; z!sG^^R+f&tz!QcBa7HLRSCgYB3=Loyjv}rm?*dO48o)3dl3Y#R1)eZ8fMGbkxthES zB(RQO3{Zs04G4Tq%1mxR-~td#Zb0DHAeh{Mz)edqxdB{NiavBAnB0KCq7zJRK;RKX SFu4JNha + + 1.1 + + + 0 + v0 + 10 + interpolation + 2 + + + + 0 + 9 + 1 + True + + + + + + + 24 + 24 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000000.png b/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000000.png new file mode 100644 index 0000000000000000000000000000000000000000..8eea7beb6e04b3f7e6de0a74765bc25c0997121f GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9s-7;6Ar*6y6C_xj`3;;_{6C^` rhO#0J8paup{S3j3^P6;VZoG@;IWA5R31DVmNO;I7a^uLt4L}_Xp00i_>zopr00kEr>i_@% literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000004.png b/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000004.png new file mode 100644 index 0000000000000000000000000000000000000000..7dacfb37dbfb653962c6d2ab6180145c1ac9bd95 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9nw~C>Ar*6y6C_xj`3;h|{;G#E rgMskVJW11xD+;e9O}N|`O&J(sA2NzKmR&mr)WqQF>gTe~DWM4fouL*I literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000005.png b/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000005.png new file mode 100644 index 0000000000000000000000000000000000000000..870d00485975a7b06f77a0f92f01b85356aadb11 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9DxNNmAr*6y6C_xj`3;gp|N8He qQ#*CnDC@!s$1BDeB0Wr53=B~Z8Abl*?#~0NXYh3Ob6Mw<&;$UyNEpNb literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000006.png b/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000006.png new file mode 100644 index 0000000000000000000000000000000000000000..0042daeda6464d1979058d5dc6cfa12c03ee4319 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9DxNNmAr*6y6C_xj`3)XT`r*IG qOzqTNqpS-n9IqH>i1aXJF))NbWE3eE(_acy&*16m=d#Wzp$P!edl-QL literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000007.png b/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000007.png new file mode 100644 index 0000000000000000000000000000000000000000..f670119132856680c6bddf4d63cd99eb61c3e8e8 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9DxNNmAr*6y6C_xj`3)XT{O#Z6 qr*`VDQPzbOj#rE`M0%LA7#KnxGKzds@RSCsXYh3Ob6Mw<&;$V0C>WIh literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000008.png b/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000008.png new file mode 100644 index 0000000000000000000000000000000000000000..110d70e8ce6706abc285e29ff45afc96d1085ea5 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9DxNNmAr*6y6C_xj`3)XT|L4CY qP3_cOqpS-n9IqH>i1aXJF)#!^WE82^+o%Rq&*16m=d#Wzp$P!r`WWW` literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000009.png b/tests/assets/cvat_dataset/action_detection/train/0/images/frame_000009.png new file mode 100644 index 0000000000000000000000000000000000000000..678bb535cd5f97e1703758307d7f8a6c8afe9ff4 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9DxNNmAr*6y6C_xj`3;0ZKF+uJ q>oa9{>Xik7%`4I`Xh^VLVPNok$S88__4T + + 1.1 + + + 0 + v0 + 10 + interpolation + 2 + + + + 0 + 9 + 1 + True + + + + + + + 24 + 24 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000000.png b/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000000.png new file mode 100644 index 0000000000000000000000000000000000000000..5fdaee4438315faf0c74f6b0abc37023db52803b GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9YMw5RAr*6y6C_xj`3-~?zpxi{ sGYEVq))v_8#=B^iaT?}v;crg3NOfI1jFUHx3vIVCg!0H;(I&;S4c literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000001.png b/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000001.png new file mode 100644 index 0000000000000000000000000000000000000000..678bb535cd5f97e1703758307d7f8a6c8afe9ff4 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9DxNNmAr*6y6C_xj`3;0ZKF+uJ q>oa9{>Xik7%`4I`Xh^VLVPNok$S88__4Ti1aXJF)#!^WE82^+o%Rq&*16m=d#Wzp$P!r`WWW` literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000003.png b/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000003.png new file mode 100644 index 0000000000000000000000000000000000000000..f670119132856680c6bddf4d63cd99eb61c3e8e8 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9DxNNmAr*6y6C_xj`3)XT{O#Z6 qr*`VDQPzbOj#rE`M0%LA7#KnxGKzds@RSCsXYh3Ob6Mw<&;$V0C>WIh literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000004.png b/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000004.png new file mode 100644 index 0000000000000000000000000000000000000000..0042daeda6464d1979058d5dc6cfa12c03ee4319 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9DxNNmAr*6y6C_xj`3)XT`r*IG qOzqTNqpS-n9IqH>i1aXJF))NbWE3eE(_acy&*16m=d#Wzp$P!edl-QL literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000005.png b/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000005.png new file mode 100644 index 0000000000000000000000000000000000000000..870d00485975a7b06f77a0f92f01b85356aadb11 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9DxNNmAr*6y6C_xj`3;gp|N8He qQ#*CnDC@!s$1BDeB0Wr53=B~Z8Abl*?#~0NXYh3Ob6Mw<&;$UyNEpNb literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000006.png b/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000006.png new file mode 100644 index 0000000000000000000000000000000000000000..7dacfb37dbfb653962c6d2ab6180145c1ac9bd95 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9nw~C>Ar*6y6C_xj`3;h|{;G#E rgMskVJW11xD+;e9O}N|`O&J(sA2NzKmR&mr)WqQF>gTe~DWM4fouL*I literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000007.png b/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000007.png new file mode 100644 index 0000000000000000000000000000000000000000..a7c8136d1288adb5905134e96cc926bc44795a21 GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9YMw5RAr*6y6C_xj`3)9*{6Be# s6sJMpyJ>;VZoG@;IWA5R31DVmNO;I7a^uLt4L}_Xp00i_>zopr00kEr>i_@% literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000008.png b/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000008.png new file mode 100644 index 0000000000000000000000000000000000000000..ca2f05eec3c720231f329045184ec3a7c056a388 GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9YMw5RAr*6y6C_xj`3;nc{|T=b sVKxYSH!ZN)jd#&J$HfUE0n7{x$qyMtLd^J20(CHWy85}Sb4q9e0IZP~;{X5v literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000009.png b/tests/assets/cvat_dataset/action_detection/train/1/images/frame_000009.png new file mode 100644 index 0000000000000000000000000000000000000000..a548094f29f3ad63dbbfef30864e9816e950f5f1 GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9YMw5RAr*6y6C_xj`3(+5{_hNF sXEq3YH!ZN)jd#&J$HfUE0n7{xX%87ie9u^!0(CHWy85}Sb4q9e0N#-pApigX literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/2/annotations.xml b/tests/assets/cvat_dataset/action_detection/train/2/annotations.xml new file mode 100644 index 00000000000..a40a71ca339 --- /dev/null +++ b/tests/assets/cvat_dataset/action_detection/train/2/annotations.xml @@ -0,0 +1,65 @@ + + + 1.1 + + + 0 + v0 + 10 + interpolation + 2 + + + + 0 + 9 + 1 + True + + + + + + + 24 + 24 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/assets/cvat_dataset/action_detection/train/2/images/frame_000000.png b/tests/assets/cvat_dataset/action_detection/train/2/images/frame_000000.png new file mode 100644 index 0000000000000000000000000000000000000000..610bcf0e187930a0d134ddcb144586fad424fc5d GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9nw~C>Ar*6y6C`pP1cP7LuauGi tff*X*r&l;GW(z#OAkcuzjj@)2Vfq|KktrKx&jK|uc)I$ztaD0e0st3T80`Q6 literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/2/images/frame_000001.png b/tests/assets/cvat_dataset/action_detection/train/2/images/frame_000001.png new file mode 100644 index 0000000000000000000000000000000000000000..b76ae6cbfcca544ead2b819e642a50c9aa5634ab GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9nw~C>Ar*6y6C}1Z2nN5fUnwO4 s0y8wqPp@!X%occlL7)Mb8)F;;gQp>*i08C3Q9w-$p00i_>zopr022imH2?qr literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/2/images/frame_000002.png b/tests/assets/cvat_dataset/action_detection/train/2/images/frame_000002.png new file mode 100644 index 0000000000000000000000000000000000000000..6f118d530b127230637c71a5171b02d8916a806d GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9nw~C>Ar*6y6C_w$7?hV?s1GtV r0E57H(*m2_co)rcT$~`nAr*6y6C_wmn0sb8{r9|j r01RAqM!WG!nRZmM^)>|>tYBbRAr*6y6C_xj`3;h|{;G#E rgMskVJW11xD+;e9O}N|`O&J(sA2NzKmR&mr)WqQF>gTe~DWM4fouL*I literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/2/images/frame_000005.png b/tests/assets/cvat_dataset/action_detection/train/2/images/frame_000005.png new file mode 100644 index 0000000000000000000000000000000000000000..11bc7997f2e552163537eedc259520cfb42028ee GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz98lEnWAr*6y6C_xbB@B|d{;G#E qgMskVJW11xD+;e9O}N}l7#R4=7)3Y&4m1PxFnGH9xvXAr*6y6C_v{HxwP{(EKt# tgqH^de5UM9y|N&%c}4mK4T&xWh68^XMbsiW8-bb_JYD@<);T3K0RT?}8Ych% literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/2/images/frame_000007.png b/tests/assets/cvat_dataset/action_detection/train/2/images/frame_000007.png new file mode 100644 index 0000000000000000000000000000000000000000..7a334a1bf1d3864947816c4851f8768a39ccc52b GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9nw~C>Ar*6y6C_v{CkQaQ`7Hdu rBr5?77TwWZ%+@RAV%zM-D9gxDG?7un^3L}(pe6=SS3j3^P6Ar*6y6C_v{Cs;7K`7Hdu rBr5?77TwWZ%+@RAV%zM-n8?7OqQ)pAr*6y6C_v{Cx|>??3v;8 r-}CALFmTx!?Zzu*+EK;Eo6Nv)bq%8km)q%bpe6=SS3j3^P6 + + 1.1 + + + 0 + v0 + 10 + interpolation + 2 + + + + 0 + 9 + 1 + True + + + + + + + 24 + 24 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/assets/cvat_dataset/action_detection/train/3/images/frame_000000.png b/tests/assets/cvat_dataset/action_detection/train/3/images/frame_000000.png new file mode 100644 index 0000000000000000000000000000000000000000..7f7c8b160078dd0b05e36fc0bb99bd3e62ab842b GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9nw~C>Ar*6y6C_v{Cx~2NHb~<7 rs~*Y>2EtGCBuz7}D7=z1`N6=@?#L*j@b}#epe6=SS3j3^P6Ar*6y6C_v{Cx|>??3v;8 r-}CALFmTx!?Zzu*+EK;Eo6Nv)bq%8km)q%bpe6=SS3j3^P6Ar*6y6C_v{Cs;7K`7Hdu rBr5?77TwWZ%+@RAV%zM-n8?7OqQ)pAr*6y6C_v{CkQaQ`7Hdu rBr5?77TwWZ%+@RAV%zM-D9gxDG?7un^3L}(pe6=SS3j3^P6Ar*6y6C_v{HxwP{(EKt# tgqH^de5UM9y|N&%c}4mK4T&xWh68^XMbsiW8-bb_JYD@<);T3K0RT?}8Ych% literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/3/images/frame_000005.png b/tests/assets/cvat_dataset/action_detection/train/3/images/frame_000005.png new file mode 100644 index 0000000000000000000000000000000000000000..11bc7997f2e552163537eedc259520cfb42028ee GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz98lEnWAr*6y6C_xbB@B|d{;G#E qgMskVJW11xD+;e9O}N}l7#R4=7)3Y&4m1PxFnGH9xvXAr*6y6C_xj`3;h|{;G#E rgMskVJW11xD+;e9O}N|`O&J(sA2NzKmR&mr)WqQF>gTe~DWM4fouL*I literal 0 HcmV?d00001 diff --git a/tests/assets/cvat_dataset/action_detection/train/3/images/frame_000007.png b/tests/assets/cvat_dataset/action_detection/train/3/images/frame_000007.png new file mode 100644 index 0000000000000000000000000000000000000000..fc454a4d3dddbf481678240bfefe5a161371b0f0 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9nw~C>Ar*6y6C_wmn0sb8{r9|j r01RAqM!WG!nRZmM^)>|>tYBbRAr*6y6C_w$7?hV?s1GtV r0E57H(*m2_co)rcT$~`nAr*6y6C}1Z2nN5fUnwO4 s0y8wqPp@!X%occlL7)Mb8)F;;gQp>*i08C3Q9w-$p00i_>zopr022imH2?qr literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/annotations/train.json b/tests/assets/datumaro_h-label/annotations/train.json new file mode 100644 index 00000000000..f641fbb2352 --- /dev/null +++ b/tests/assets/datumaro_h-label/annotations/train.json @@ -0,0 +1,489 @@ +{ + "info": {}, + "categories": { + "label": { + "label_groups": [ + { + "name": "shape", + "group_type": "exclusive", + "labels": ["blue", "green"] + }, + { + "name": "blue", + "group_type": "exclusive", + "labels": ["blue_rectangle", "blue_circle", "blue_triangle"] + }, + { + "name": "green", + "group_type": "exclusive", + "labels": ["green_rectangle", "green_circle", "green_triangle"] + } + ], + "labels": [ + { + "name": "blue", + "parent": "", + "attribute": [] + }, + { + "name": "green", + "parent": "", + "attribute": [] + }, + { + "name": "blue_rectangle", + "parent": "blue", + "attributes": [] + }, + { + "name": "blue_circle", + "parent": "blue", + "attributes": [] + }, + { + "name": "blue_triangle", + "parent": "blue", + "attributes": [] + }, + { + "name": "green_rectangle", + "parent": "green", + "attributes": [] + }, + { + "name": "green_circle", + "parent": "green", + "attributes": [] + }, + { + "name": "green_triangle", + "parent": "green", + "attributes": [] + } + ], + "attributes": [] + } + }, + "items": [ + { + "id": "00", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + }, + { + "id": 5, + "type": "label", + "group": 1, + "label_id": 5 + } + ], + "image": { + "path": "00.jpg" + } + }, + { + "id": "01", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + }, + { + "id": 5, + "type": "label", + "group": 1, + "label_id": 5 + } + ], + "image": { + "path": "01.jpg" + } + }, + { + "id": "02", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + }, + { + "id": 5, + "type": "label", + "group": 1, + "label_id": 5 + } + ], + "image": { + "path": "02.jpg" + } + }, + { + "id": "03", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "03.jpg" + } + }, + { + "id": "04", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "04.jpg" + } + }, + { + "id": "05", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "05.jpg" + } + }, + { + "id": "06", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "06.jpg" + } + }, + { + "id": "07", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "07.jpg" + } + }, + { + "id": "08", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "08.jpg" + } + }, + { + "id": "09", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "09.jpg" + } + }, + { + "id": "10", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "10.jpg" + } + }, + { + "id": "11", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "11.jpg" + } + } + ] +} diff --git a/tests/assets/datumaro_h-label/annotations/valid.json b/tests/assets/datumaro_h-label/annotations/valid.json new file mode 100644 index 00000000000..f641fbb2352 --- /dev/null +++ b/tests/assets/datumaro_h-label/annotations/valid.json @@ -0,0 +1,489 @@ +{ + "info": {}, + "categories": { + "label": { + "label_groups": [ + { + "name": "shape", + "group_type": "exclusive", + "labels": ["blue", "green"] + }, + { + "name": "blue", + "group_type": "exclusive", + "labels": ["blue_rectangle", "blue_circle", "blue_triangle"] + }, + { + "name": "green", + "group_type": "exclusive", + "labels": ["green_rectangle", "green_circle", "green_triangle"] + } + ], + "labels": [ + { + "name": "blue", + "parent": "", + "attribute": [] + }, + { + "name": "green", + "parent": "", + "attribute": [] + }, + { + "name": "blue_rectangle", + "parent": "blue", + "attributes": [] + }, + { + "name": "blue_circle", + "parent": "blue", + "attributes": [] + }, + { + "name": "blue_triangle", + "parent": "blue", + "attributes": [] + }, + { + "name": "green_rectangle", + "parent": "green", + "attributes": [] + }, + { + "name": "green_circle", + "parent": "green", + "attributes": [] + }, + { + "name": "green_triangle", + "parent": "green", + "attributes": [] + } + ], + "attributes": [] + } + }, + "items": [ + { + "id": "00", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + }, + { + "id": 5, + "type": "label", + "group": 1, + "label_id": 5 + } + ], + "image": { + "path": "00.jpg" + } + }, + { + "id": "01", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + }, + { + "id": 5, + "type": "label", + "group": 1, + "label_id": 5 + } + ], + "image": { + "path": "01.jpg" + } + }, + { + "id": "02", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + }, + { + "id": 5, + "type": "label", + "group": 1, + "label_id": 5 + } + ], + "image": { + "path": "02.jpg" + } + }, + { + "id": "03", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "03.jpg" + } + }, + { + "id": "04", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "04.jpg" + } + }, + { + "id": "05", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "05.jpg" + } + }, + { + "id": "06", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "06.jpg" + } + }, + { + "id": "07", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "07.jpg" + } + }, + { + "id": "08", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "08.jpg" + } + }, + { + "id": "09", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "09.jpg" + } + }, + { + "id": "10", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "10.jpg" + } + }, + { + "id": "11", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 0, + "label_id": 5 + } + ], + "image": { + "path": "11.jpg" + } + } + ] +} diff --git a/tests/assets/datumaro_h-label/images/train/00.jpg b/tests/assets/datumaro_h-label/images/train/00.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b7d22cc8d3170d4262856c04bacc50712068049e GIT binary patch literal 5285 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 Xp)KTfYJ>*refisb`cHUQ7##f%Ov|%$ literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/train/01.jpg b/tests/assets/datumaro_h-label/images/train/01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ad51fd024e058e55ec9f2ec9a9741c1862c8fa30 GIT binary patch literal 5431 zcmcIn2UJu|mVVG=ktA7yK$ArzG+9uv8=EL7U=u|$h$Kk@Hi8H=2nZ5H0VPPLB@0MK za*!-Zg5=yJ8*I9#`Tv>S*|W1dvvbbYJ73j%=T+6Y->q9;-6DJ^%mEBqFgOe#Aprmq z;sX$-01beYVsIf**PC`^YRPI$}1|Xsz2A%wzRf=ZSUyp z>K+;%866w{J~27J@N;o#d1ZBN9kaK8aCr0!dwg=niv$4uNsCzjLtczTUZiJhKzYWC zgw&S^5F;5mpA-et4FgJRj|=?L�KyUwtTTrWTMf++}&-IY`3_mYo;GoKgFO*}q0C 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 ONdB5z{5vfrVd5{OY2K#* literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/train/02.jpg b/tests/assets/datumaro_h-label/images/train/02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..973df2648f7ad66ffcfaeb9e42eb9cde169d2c69 GIT binary patch literal 5341 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/train/03.jpg b/tests/assets/datumaro_h-label/images/train/03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7990d475faba894ee45558c31f431319f174ea8e GIT binary patch literal 4541 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 FzX2Rx7Lxz~ literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/train/04.jpg b/tests/assets/datumaro_h-label/images/train/04.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f0255007c30a745157b40f8d3e4a605ccb854c55 GIT binary patch literal 4335 zcmds4cT`i$y5Av$e!x%^1&u;zg3?PgR}M;OIe=Jb3IUWN5$U}|K?MU62_2NKh%^xp zDH0?If)r^YK_mpECej3?B{z7^dGEZn?tS;Z_s?7FeS7_8X0Mq&`}g~1zHjzsePU68 zJthXm1^@^I03h}SuttD000(H-c1w<3;{@-P5Kc}oCl`c^>nFH*dAPWFxw*J__<4Bw zc8%Q<;O7(Az1f}Q_eVJ(U@(M_n~VGBkpEG^Y674<02QEuK~ew*6a=x}yFzzKy2A5qog60vaPmhu)=yOog3 zBYnE!jo7)~MYy_?Pb4qj-hJZx56H;M$txVy(A3h_L7X_Fk1{YcGB!DX;i9FLwT{rm$0gMve%Zr`~Z9di#)OiW5nNli=7$jdKy{G{+{(X$toRn;$RUe(q$ zlUv@lw!Lfb=<6RC{5bS!m@-bAnEX05Ju^GEw7l|tb#48}#^$ar5CHyN7Q6h1x}a=b z9J_14y{ikv5y%D@$_Y85$|bC4!R_cRBBgeVNAz?;Zp9m3Y4vl9VopB2e0$*<<1$OT z(taxY_k>0Muay0Vu>aIW0R+Gx_Tqt|fG)tq&E(+$`Tqlq^=g=^t^9_k(T03q(jU=v zW8zy&SwORP9t-G6W$Jq_<&$yCkZ2}^1>_!hjroLBCnL3E#v8^tplUi+O01o6kV7
  1. gudQzs;aLX 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/train/05.jpg b/tests/assets/datumaro_h-label/images/train/05.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fab630e59574ddf7bc4f2ee0814e59c77260a672 GIT binary patch literal 4496 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 D7|c^F literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/train/06.jpg b/tests/assets/datumaro_h-label/images/train/06.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a7bc6010ff5f5f9828f0ff6dd40db042fcffcc25 GIT binary patch literal 4697 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/train/07.jpg b/tests/assets/datumaro_h-label/images/train/07.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b133a1fa94f56b6c3cf3852815a53c0f74bf7ce2 GIT binary patch literal 4530 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/train/09.jpg b/tests/assets/datumaro_h-label/images/train/09.jpg new file mode 100644 index 0000000000000000000000000000000000000000..100f4ba5b98af49331552e6cdb5a84cf008a2849 GIT binary patch literal 2963 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 J(1(WK{vB_vz?c94 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/valid/00.jpg b/tests/assets/datumaro_h-label/images/valid/00.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b7d22cc8d3170d4262856c04bacc50712068049e GIT binary patch literal 5285 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 Xp)KTfYJ>*refisb`cHUQ7##f%Ov|%$ literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/valid/01.jpg b/tests/assets/datumaro_h-label/images/valid/01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ad51fd024e058e55ec9f2ec9a9741c1862c8fa30 GIT binary patch literal 5431 zcmcIn2UJu|mVVG=ktA7yK$ArzG+9uv8=EL7U=u|$h$Kk@Hi8H=2nZ5H0VPPLB@0MK za*!-Zg5=yJ8*I9#`Tv>S*|W1dvvbbYJ73j%=T+6Y->q9;-6DJ^%mEBqFgOe#Aprmq z;sX$-01beYVsIf**PC`^YRPI$}1|Xsz2A%wzRf=ZSUyp z>K+;%866w{J~27J@N;o#d1ZBN9kaK8aCr0!dwg=niv$4uNsCzjLtczTUZiJhKzYWC zgw&S^5F;5mpA-et4FgJRj|=?L�KyUwtTTrWTMf++}&-IY`3_mYo;GoKgFO*}q0C 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 ONdB5z{5vfrVd5{OY2K#* literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/valid/02.jpg b/tests/assets/datumaro_h-label/images/valid/02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..973df2648f7ad66ffcfaeb9e42eb9cde169d2c69 GIT binary patch literal 5341 zcmb_g2|Sct+rKSif5;X^rWhh*Uz?heu|5bP#DuJqY}tn?T8GFULX1K}wqzY;dz2+x zLNbP|W8WEbf1~GlzVCUz-}`>=^L@YX+`sFbbN|kDpZoe>*SW6ioCAIZiNGNP9eo{u zjt&6mXb%9602cvzx`T31(;pN@hJzZy$jHFR1Yu(ONzAM)Ow6pzOiV27EUat?h1TO> zXX7|{Ihf@4Pw62H3=lSECgz_k|EmgY1h`oMB0yxI69wqG=@_`_z-9nSlN0h&Jesh- z7dmG#L**fw_zVm*p&>MhY20D7ae$EakY!)%C z()avkJZe%Zp(>Wa0E({*PT zS2uSLzgzwRfkD9`k$3OifAH`TEI$h{bO%m|G<~QuS3J*q>0HNQ`0lEb1SQB>l>R}+dI1lxaa_e-(k_}e~OEnhKv5d z2FwSz=;#A!#K6r6IibMBbHS9^!G~8=@eT{0R$O-JdsZ=x$ zk^Mbj5&tJ-{|W5Ba18?-40JT(F>nJ2fFjbPY^1pMD~?1O^m|LrAR`{ev=)KD2elj! z=uV+(d#&J`u&a;z$CE0gYa4T;SHBG>0cGKJMubivOsO73QGqNlR`cqa1{yy7gXA;CaChxsW+`t z9h@Sw+4Mgc zzk}+mGilOzz&7Q@inFq9HFVdlA@3FSExsNr(W#JbN;`q%v+7ER@R^?fS9DD7!4lb_ zR}_c-E$%+KP>&9(P?$9w;6(O{yJa|ID0l@BKXQ_(+CZ`-W`a|`dF8%Dl1G1{Ft3g3QegPY zZ4BIdHh0PI`EpW3+l}GKg0T~)9>6_8U?aBo?Wu>M!aSuURc02`*Kp(jqQnF$ zrTP&~TdSoDw2A+vy8cw-vE*kRyBJQgq2T3s6BzOGK+?Eje{vi7#(aZ>y8RCm4NCjI zv?ZY`2&ITmU)~hrHyTUhZm6n=uSf{%GlFW^aAsH|X+N^xd18v>11wKQr$&rCcdAH% zxkdzm0I#k8$8Fq(oWH7DhBddAv|RY~h6F)j1+9pW7-t-Zm5u(XDDQZ z%FWG|s|mo~nf&yNJ!h9pEz$5{CJzLbW^7aOBA91(dt zQ#5zs4ldUdt&_e2Wv`NxjCQrR?WxXNw_?O;RiSnu&9rFubw=I=YK!QG$Q)z}biR?(V8F<@N)XG-pZh4s`S|+7VrFwDv=;p;Gr8uwXZ{p@a z08;C2mwAyGdk@M@g&7nOvzW$Ec!Qqwe9{QLX7Or~lB4n&J#`nxNzPo?UNYetA$rTw zd(`B0P{`2aBkP5{hNpGf(TR>v%HlPTsrT zv1%SD25EC1x*kiv}8R{ zP#KJdvxlK?#>vFn>qNMTag6XOmmWrnzZ=$r$7mecr_V3B@p?mAqhdj)S2`;6VVVXp zQ+`%dzDZh&7{SUY!lIJN)eI*+Ms7T8_37QUrlj>P=>&1{E6wp+eaU8v>)rJwoKbl( zmWCBubHwdpHxu?_m1oyyF<#4PUgH>a~&q z#T)aGOoyTfWyl?8|xL)4N#6Mzr*ZK9XV@j_$?}?Y@koImheJJ4n7u zbpJqMO?$M&(q0o3Nc|21>t!D6HIzXp3EE#e;7waN1lS9qwP8w8;Yi%@CPK8M?lQHj z1MLu=*GZn192>UzV`P|2>B}KpXi&zsA6s$Vk5U7HUE__NHI!k~)r-_Op9{=~e0?h# zPNPRrW#YnZ$uEY(jXaCw=2i^a?8@AI_*52 zwVJN!Ia--9DaE^D)}kSe!qa2(%f*@H6)I@0pKiu>&O-j`-Ek>!hN!5_| zN#}9z3xYDADj5-HnC|I&;<*>*@38LI)w>NJj*{(D6yn)L20chG*BI z1w``4m#EBM{k=-4nB7hV>elQ1(=}PHjT)6#3m>`V?hhB4$1im(Kx-_+Q*2c+)->xa zO>eLqgv-AW-4+)a3jz|e<70cFAkf=h*gHSPbsGdKr<+js#@Lig=;wDOs#Qi~v!?Z3 zJ=t{vyJ3-#7v?>!zRViMwN#-c;?h35y#xp}p21QPsJpJhYMFzbrTp&WlM_;%d7~Pt zsg#~9?^qs^Ux$Uq6>Ig6xx*nI&(cl8^VbQFp{u$WYY=ek1OaM7eK=;YKkE?Iv-{pT zndQUPmV=XC;_ChI$D-hRgSb{rN+-5V`V1ktqnedo$Lm~4 zjZI+i^6WUTej@x5o<+S&fAix`0+n@VBG_`xZrjTT_Bxhl&eBcH?%{wjw^f@$IFJG7c?c8H~{=JZ(?{-|{ zty#o$YjNE{uP9?}4^v_!T9GH%dINQmL$27e55q zJnfSBV1A$4tGI()L%dg-#7X85@UipqIXFq+-w=bz`v#dW7*qrZ_=059sYdQ3$1{Ut1Wu}GkQFKA-nA9%T{+6UdxN!@H)HZDpoSEkT=0o&J08Iz zu6J^+p{C^S*b%trvXgzn)#b2$?W@QiqhCi0;R1lGyFa6Ffrcd`cf8X5YvC88)SX?w z2KDDISPG$BY7srVTQ%FpYQ&A^uWkd+`_X*_+0A&W+csac- z*}I8!)>w|y!`D!>GC5mH{`T`XKHe5iK*`qIjK+NCAF^}FR&PU+o@vHp1oF4kHygru z-ygpfC+KW_#rp!|Azj9!6ceH3iPhnTZeFAJJ4I0ZP>J#|B4bTBPw69C^U6oF$Tb^; zZn?dCg&~rEDvKM7hDO>m_T`1j@!O(pB1VGWCLg`r;L*{X#O89ohjzwk|0eJx?-a+l zyn06%%Ju%sDN54T(ok?c$Fjyvgi;atP2vj;_p+OVOFeRG*8>~GS58L~7ntUoze@~= zmcJ>F>Zq=Mx{QA=C>*a3NVdan)4|xbj6`EWy|U78?OqkK(Cr z2&&vWmfa8*+3cuNB#xHNsYMlr*^deY?n4^u1Wq5zld+WgUN`W%(li#madWu7=5$e_ zDKe~jSMo36o1gqlnM?d-a_(v z*LUM_Wo(i;Pyh5&`ak^j@3m4E)?Y=XWQ+lZc=y~%auuCq0SJ_<$n7$lfIzq{MUvXl zPnbD|*x&J5E(QVevq36`HbR%O!`;m}6M@UxQ!)dA=eIy$>j&XOy6nAmN$0e#%ccWq zia~4V`rrlBkg$GBuo}BVeX(lznmze&SxU~r6uGXO{xMt93pc!p`4{r~`^pha z23|{1eQ8G1N+aK+DlL8ST>^Wn*wuR?+Z(SIJIHollTXqtpqxZ$YiT&{3O85NC%9_u zYRO?&2oYRZ1c?BmS;Uve0Tn#ZK==YCkjs4rRd8lBTi4m zsCB+WtKJu2XE9KWNU6&m{gzv!@18S*IST?D*gYf&^qc{KkM(&Va12is1cA*c5XcDx zfuv9q>KT;x;BAcG{##`z7ZPzloEvVFw1D8202(?1kOr22~{gISKiV7^L+)IT`^Wf8tj@FM#axkvs>g7bs+ z5-X|&vOo4G8=N~k+LF%#jYQfVkj~E=6^o<$2)0s#yWwS|>A@uB1}jg>Qt_rOwCq~4 zs=C|$O;dl(RSEn|f^SHmmmT>n%{OZ}pXdDh>~WA?7-&n}2?Tc337jA>GDeN-MA=b{ zw`++ecQqB!BB}$FSVT~HB~EeeYE3aIAHOV=&c-h3a)f{R!5z3)2}WZ@TSR`jBTjt( zwYh9Xf^MD8^+zPRie)`cy)K>ezlk0H@^=tgwYWYzxnG}gk=q7SfoDuZ@p_mAkdq7T zLTw9nmbjM@yPeI-D;9IUJ&#omr!)^IX4HziR1U0HS$wB>?dQV-SJFFrR`!#!5y{w6 zVj|+{Y(kBGK%xGc@WQxG{~5#66H|$%D2s^3aYeEw{(eNX6>_JcXs6zKvpU@N&c`=} iTz6k)LwdqJ7smn?$1G_(0RN2azZ*3EANiQUq5lB($gso! literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/valid/03.jpg b/tests/assets/datumaro_h-label/images/valid/03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7990d475faba894ee45558c31f431319f174ea8e GIT binary patch literal 4541 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 FzX2Rx7Lxz~ literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/valid/04.jpg b/tests/assets/datumaro_h-label/images/valid/04.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f0255007c30a745157b40f8d3e4a605ccb854c55 GIT binary patch literal 4335 zcmds4cT`i$y5Av$e!x%^1&u;zg3?PgR}M;OIe=Jb3IUWN5$U}|K?MU62_2NKh%^xp zDH0?If)r^YK_mpECej3?B{z7^dGEZn?tS;Z_s?7FeS7_8X0Mq&`}g~1zHjzsePU68 zJthXm1^@^I03h}SuttD000(H-c1w<3;{@-P5Kc}oCl`c^>nFH*dAPWFxw*J__<4Bw zc8%Q<;O7(Az1f}Q_eVJ(U@(M_n~VGBkpEG^Y674<02QEuK~ew*6a=x}yFzzKy2A5qog60vaPmhu)=yOog3 zBYnE!jo7)~MYy_?Pb4qj-hJZx56H;M$txVy(A3h_L7X_Fk1{YcGB!DX;i9FLwT{rm$0gMve%Zr`~Z9di#)OiW5nNli=7$jdKy{G{+{(X$toRn;$RUe(q$ zlUv@lw!Lfb=<6RC{5bS!m@-bAnEX05Ju^GEw7l|tb#48}#^$ar5CHyN7Q6h1x}a=b z9J_14y{ikv5y%D@$_Y85$|bC4!R_cRBBgeVNAz?;Zp9m3Y4vl9VopB2e0$*<<1$OT z(taxY_k>0Muay0Vu>aIW0R+Gx_Tqt|fG)tq&E(+$`Tqlq^=g=^t^9_k(T03q(jU=v zW8zy&SwORP9t-G6W$Jq_<&$yCkZ2}^1>_!hjroLBCnL3E#v8^tplUi+O01o6kV7

  2. gudQzs;aLX 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/valid/05.jpg b/tests/assets/datumaro_h-label/images/valid/05.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fab630e59574ddf7bc4f2ee0814e59c77260a672 GIT binary patch literal 4496 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 D7|c^F literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/valid/06.jpg b/tests/assets/datumaro_h-label/images/valid/06.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a7bc6010ff5f5f9828f0ff6dd40db042fcffcc25 GIT binary patch literal 4697 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/valid/07.jpg b/tests/assets/datumaro_h-label/images/valid/07.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b133a1fa94f56b6c3cf3852815a53c0f74bf7ce2 GIT binary patch literal 4530 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/valid/09.jpg b/tests/assets/datumaro_h-label/images/valid/09.jpg new file mode 100644 index 0000000000000000000000000000000000000000..100f4ba5b98af49331552e6cdb5a84cf008a2849 GIT binary patch literal 2963 zcmd5-do+~m8h^(agS4?3JKdDDDLds-l3S&rNp@xwU1+wFl1lC(Gwl@H?2^kCQA94O z?A&597>w9aZsU?%G8lu%xDPY)edn86>zsAgI)6Cltaa9T-t~Ll@BO~#eb?{zJ@5Ox z@CUdbC>^pUSOXLa02I;y+zqUN6iQ+eFC{T)w8UeirP0zd7#W$bz{<(WVCAqfGO{aV z<>V!XJSnV@SC9-Oi+r0Zg+Ze+@>m({*N}h9!IeN+7W9LDG)falDWlNJD7Xe_AUrW& z=^;w)T$B4_zNQ1A{}{Vcy8-*wpmQ?A-iQY8@ zNl9{mmFPlA1tLHzOJg?b%Y46&h;{W=(KPr~R&{?&T7H$>CPNY*cgCkhUQNr0vw2D) z?W?lo3A_0pDf_#yf9U!M6woLnd1z%|2}HQ&B3(OD%b$8$(L17_%3AB+2)P%`fPrjy zG7QXlFsMin>F^^&VPuGrD#my^GIIpRVY|s1)q=y;o8;tvwzn$@DG3}7qTj(^=!hjX z*TdkJl^qOHW?=Bi7Q!_;c{`g^LI=L=i*uurmTt?kdGilDK6{<_Tyhcf9Hnv(mGv|V zDQS3Br$Oz2N47`#=JK$Ljd%pz!0Zp;mUZ^GB8T?hp6ZQi%0t zYb|x=Q>zmMrZ6~=%CZz_mcU>@w|_>@toj^%De(j`?rK2k1rghLYCHN5_hVOmS@l1E z*J*bmHPy&z#8|Y28pfO@hzwzHQtLH(@&C8!%CiBr|l&Toqo`9<@whJE?ivqL^M=sMdoyo6=WaTk(rH8Zx`C zBXHq&2=`8P(kfViyIyarxhf97=Bpl#TbbAran$t=k2ZHia7nR?4bikAk{%2M?G)vj z2Nsvf<~2t%BOa!6pV-G8Y001(c?VdfoC!+6IYy~ve0z&8JoIk1RK$Ksr*iTv&mURR zNnJdM9D~7Q{JBuCh(v{e6`w7SXX=n+ds@89Ur%~whPY_>b55U1tlX9d_H|f(x{Fv+o%d>$IFo<%d(-Y+RN9qH5S_4Tx>3@c5l0K_!;(- z{>0lQr-(;a?}xIA2F+??v)>o+vbtyQxDB{+JO~AYLdSKN1?KT;qrErmm12*8>+K7Dt9Gt8sWvW|w47~gboLS!Rgja6FJ@)G zIO*Xb9PZudoMDv%1JsF+FKMN$Od&;ZuUX2{VRG(;jeCIJxUEVoVJJ!LbjR#eRljQ9 z>z&Wq+V><^TXLX49{-Y<>*wvtH%KUVz<(&r`TRrh6KJuxcFh*05zCtPlR7VXcPOrR z-|PF-42+qy-Jf7E1#*722Z|2WJzr(5%%v5`>!j1`5qD!*uz#oe_-s-1!6s=K#Cqst zCI4Ou(rH_esuoNhpA;e`p;`!ifB}sq)D~)w!(c{nM)b+}Jq)rUOV*Pse7kXPqiQ)m z<3HuEiu9@8#V8CZx*uuGaQW`1I{hl{Zj&?lByRQ^moA1&MiHU>2O|Ud(l@U5ekmOsCAx&*8wb=loO-Hz8Cl0 ztEJ~oObmEvr-UkKTGI^Py}PmlATxnHpS;#%y;AesFuP1;P|m+wE0WOW#$MfQnsV@{ zyZeT#2HgacL7+cd&?D4+KRkFVIlg4UJ$8|vN!9I5sc@+2(hDKkSGM_bd#CL-kM)ok zBQFDUiw$(rY4eIfhzHAo?x>eQn<{G5SWtvPchX{3MT~^|>8{aY%FZ_bHDAf+$6n(_ zBjojL4fXp0N-G86rh6_XbCO1>$kk;VIYrr+t*2O%DQ*&*phrtNkl}R-U%Sf1gjsS0 zYI)XhsdaS8OnNb4?F1%KO7k#p_(~f2!Wo9=Ule1ghx*yflz$ioO}i{KQ#5kiqSPJ4 z?R|s7{_VZNi>34PGo-R)V%rriolcL>NVztoH*LDM&~QCL3)!M^bk=^R`_NnQuhT8wPqqN(~DI@27-y)YY%lpk9X1 zA|BUZn?1r;=5|jNsr`bApGtjDsur4Nb@|WrG~KxudMYb#rmZ&G@B~*t?hyA)<_M!! zU~Vac+&bSqRrT(x^L_8$ck3bD zNIzKYOmm_E3saQoEPEmz`CwvY0uN?9OXefbx(2v6y0;~oGr-4DZfQa%`{7Mfc z`^_*|oPr{rfYMM!1F9FJ^l>(VmT6#w2wXA|0Hw$kS-7YSMphn^IsxGwD!Rtjd=sHu+E-=Y zCoKMll>JlKzjXBgH5>*_9!>*L01OQmumI@?F^*1zrJMHfgpl;iPWVxZOtCb(BBc=l zQ;Xar;}nr@MK~vwy%zyjcu;@RKy-xq9F$3?>deRWXw#|SdvOe!%oyQJKZ6zqp{5kE zi>Pm&PmxV=KJ6LQkw8-FM@u*Y>Q@p-5{YeZyN>tn@aPB|`*i7-OBUi3PK?Q8i;TQByvSpwlWj} z%^?U#q{s>oFr0&cLBCD}Tg#gv6kZJ@hkMJJdaCJ{Pn-No|%rNlU_uxXeh(>O*sc@`+B&$cjz{L^HtkqTo z+?>;yGKsz1pjTsN%S$({bKX%s{$Z`dZ_@U+Vmtdr;ZM$YGcy~Vc+MR=(6`VzG{+*d zH;UTy&EE@RVXAivG}#9&O9T>08&=bFYKJ;w%t=Fp=kyYbl8r{oPA4O`NeXkW=n z&As@g4Xty@*1zIeq_h`hOvzSmo76lE@*hX!Z}(5LW!?tnf&M}Xl*ao0zP zt0Typmu*J?7Ok+=WKLDc2`FhkyIkjCNdv#!$|B}`TWa83fY(w* z)jmedIiR%p7{a@?2%k+!>x>p87|EN33_)aTk{vkCIZ_h&7vAz*_zJg5&qD zdtK!s1hnW9#Y^CX!)fKu7e#gctz&;M}cT^2ax;C2K7p0Rm+*TWeH-pv~6Tqr5- z+!a)cZUzU<5B064ecO%(Ubjt64XBuagWIMX!c^Vt3;Pd!BxMEc>tt?g#rusq zJf`HirsY7lKYa0E{(keNRrTg4{)Jool9onPW)4Za_f&{NX42o7O#5S}T*rHOFSyy8 z@Y=GGF9-mrpeY>t$C_@Hl-Q2|4eQanJ#wJ#z9mb<5eFim#~fz%!JcCj z@fdNk839%PGA)j@v0tkA4->2*Dy8ECn zxqj!llHylfM~`R|n;MyBaQCS><3_Jtw*!h_x&`Sq)(&5NMD`^WJjT6IAr+kdE{HhY z2!LJ98#uFbXwzbNXL%RGsHUl-vC9!q%aIvnvCHP*ZJCvlIf~3A*Gv;_8{}jJRC-cG zi7^dS21ToWomh+iQ*u`Gg=)yC{o|UkfMj;py3z+FBq@IASv*fKk$Q?L9B@&+cPu9;FD#OH_k>Mjz%sTsiYcFZ{tj6kgs# z$+0<*?n9fMQ!?#c1bd^KY-hTd>>VX8kx-=hH3q|CFlF;EHt11>ep-HB z(V?+qMW!v5?=NPoTd`^Sqni&Uk2+9RAC>p#LxNh6J$dU@RrSb;jb(Z51yf#o!(z}~ zRv98$T)-lsMf#sn4q+qu{KO2wIs5(mYlbchyKns#YMK&}+^WU(5ADCS>~eN;+$GwU zmV$zE+vCN&9BM>Wr(1CD@XKob-7Jqnx|yD7R2qZO!Qp;7LDvV5G_A08f2Vk&uBWy0eIZ2F)E_Tyl3eGc3URCdc6v5=${gQg=9r||D3SMVB@rr5&0(GHS+Baou6L#@G zQua?_|I*b5WY8#NcxX951}i^w8d&Si9H$Qs^1{m|vnR>P=`BStXfR^J;MK2?X^1eV zk@gjn1YuxsU-_v=ud#lku~D+1M!}^*OE0saE2t_os9dV7Kdd@vqTd+b!_wkf*;2dP z>RuZp|LmKkeO%A9Ex#>Tis&-RD(N8)|0xUxZbTRyHsm-CL3@2}1v}sJ^p3xew`{O3 zFxR!_7iCnApVmCk;MO59BRCFmq9|qrutuP`i?LA4HdRtyP0E(w7f<5kW|S*e^YdWv z+I++EO5KoKgu=3{^AOgD;qxRktfBtli8T43_XQ7%XG-dgB{hw92Xz!vy3Z|ijhWV7 zDoKcwpk#!e$z;+OKkprqvEAYo;80|FxZW@)c!4?*lGMnN&9Yg*lbE%}<4BJCzlHW^ zVlNCJ>#oMS`2`r{UU$spjaI;5Bb1xYN)nYWsgwA3cq2jX@6vfCONu^&`SX$9uIuK0 z4DmZ)4sricDdYK#W-Ex+3v-~OUsB1F&UG;0Qj1?@iQJV3iF1hwR(D=rzmY2pcb{0j zHn*~)+2fOga*>DkSrwG>$eQXsw#0SrH|fltY298QU_Tpn`$ET=M=(fSUOWf`6JAQ) zv@9P6S9k&?&ib*^nV-#EqN4Y|2oG4WvCg9z_`TcFRB?Mo!G)n{qgS}^PFB7~7ysd* zJwp~N*Z7LGUN#K|559QZHJsCIBKC(1Y*URfkD^>{reTn5H*}J_JLq)u)OM+2wc~WB zloYg@AGjS^%=q+qaluipblkw5S?bImlaUwD4%Z;#9-u4h(Sw8{2k(1k7-@EI_`li7xQL^xX*#Ja9%*DY%r z!r-nv44(Nwrs0iM+nM=LTX)^~HuCCH$aLOI<RtFZ%`|!Z$5W%a7q}KQN-DZyDI1 z`q(3MX+U(1Vy4h=VvV*2_9Bm!OUSeIsfspka<6c{F_dRNuu)udrHsE0enMGjU6{WByYt2qO#py+6LQV_g@_6EjER@TQ~HE zoVk3rhF6XGEqA;+x_XcPvmBG$02fkM_gwX%y+M(DP5;+v=?6u!H#!29i$?R7AE`A6 zlX)r@wTA+$*#|dAs;9K7eYIAn=?X)Vm0l6rxgl>~*IrHKuY^pI>(BjU>y@aOjfvrh zGU?g_9rxBc-~7Jz`7<{^@B939czxQ?{sVaw7YfTR!Kpj`3A4+cWv_AF!6*C3shVzn zMAJ=oUFBiY0;kA+&sGfmY1&K*0EjvR2q-*oRW6Bh=5q_Z01`1_TTJ_Ik{{E zwZj0j5;N^#F7!(^R+@im@@0zXUjcPiUWx6$MTxb)N@^(BD%N>-(AjC#Q(GHDpd9x! zBb-330XQyyU0vV?iN7kXW*clH68owqqQ{nhkhY5UU$~ylJyyyfXZnTcP*wUA$~VX^ zW(%nt+UC;CH^xD%B?y`58G@%unR!q%#bUr{pHsq#wovUXW{n&6Fk@u#MMZ)6oaRo; z)(M_|aOY&{T#W0Fzvu|k^0hf{TDp=912Zyj=d#?|%W~cZzR9XB`shU`75j@euGJ=g J(1(WK{vB_vz?c94 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/annotations/train.json b/tests/assets/datumaro_h-label_class_decremental/annotations/train.json new file mode 100644 index 00000000000..dbd6dfa0702 --- /dev/null +++ b/tests/assets/datumaro_h-label_class_decremental/annotations/train.json @@ -0,0 +1,430 @@ +{ + "info": {}, + "categories": { + "label": { + "label_groups": [ + { + "name": "shape", + "group_type": "exclusive", + "labels": ["blue", "green"] + }, + { + "name": "blue", + "group_type": "exclusive", + "labels": ["blue_rectangle", "blue_circle", "blue_triangle"] + }, + { + "name": "green", + "group_type": "exclusive", + "labels": ["green_rectangle", "green_circle"] + } + ], + "labels": [ + { + "name": "blue", + "parent": "", + "attribute": [] + }, + { + "name": "green", + "parent": "", + "attribute": [] + }, + { + "name": "blue_rectangle", + "parent": "blue", + "attributes": [] + }, + { + "name": "blue_circle", + "parent": "blue", + "attributes": [] + }, + { + "name": "blue_triangle", + "parent": "blue", + "attributes": [] + }, + { + "name": "green_rectangle", + "parent": "green", + "attributes": [] + }, + { + "name": "green_circle", + "parent": "green", + "attributes": [] + } + ], + "attributes": [] + } + }, + "items": [ + { + "id": "00", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + } + ], + "image": { + "path": "00.jpg" + } + }, + { + "id": "01", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + } + ], + "image": { + "path": "01.jpg" + } + }, + { + "id": "02", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + } + ], + "image": { + "path": "02.jpg" + } + }, + { + "id": "03", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "03.jpg" + } + }, + { + "id": "04", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "04.jpg" + } + }, + { + "id": "05", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "05.jpg" + } + }, + { + "id": "06", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + } + ], + "image": { + "path": "06.jpg" + } + }, + { + "id": "07", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + } + ], + "image": { + "path": "07.jpg" + } + }, + { + "id": "08", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + } + ], + "image": { + "path": "08.jpg" + } + }, + { + "id": "09", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + } + ], + "image": { + "path": "09.jpg" + } + }, + { + "id": "10", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + } + ], + "image": { + "path": "10.jpg" + } + }, + { + "id": "11", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + } + ], + "image": { + "path": "11.jpg" + } + } + ] +} diff --git a/tests/assets/datumaro_h-label_class_decremental/annotations/valid.json b/tests/assets/datumaro_h-label_class_decremental/annotations/valid.json new file mode 100644 index 00000000000..dbd6dfa0702 --- /dev/null +++ b/tests/assets/datumaro_h-label_class_decremental/annotations/valid.json @@ -0,0 +1,430 @@ +{ + "info": {}, + "categories": { + "label": { + "label_groups": [ + { + "name": "shape", + "group_type": "exclusive", + "labels": ["blue", "green"] + }, + { + "name": "blue", + "group_type": "exclusive", + "labels": ["blue_rectangle", "blue_circle", "blue_triangle"] + }, + { + "name": "green", + "group_type": "exclusive", + "labels": ["green_rectangle", "green_circle"] + } + ], + "labels": [ + { + "name": "blue", + "parent": "", + "attribute": [] + }, + { + "name": "green", + "parent": "", + "attribute": [] + }, + { + "name": "blue_rectangle", + "parent": "blue", + "attributes": [] + }, + { + "name": "blue_circle", + "parent": "blue", + "attributes": [] + }, + { + "name": "blue_triangle", + "parent": "blue", + "attributes": [] + }, + { + "name": "green_rectangle", + "parent": "green", + "attributes": [] + }, + { + "name": "green_circle", + "parent": "green", + "attributes": [] + } + ], + "attributes": [] + } + }, + "items": [ + { + "id": "00", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + } + ], + "image": { + "path": "00.jpg" + } + }, + { + "id": "01", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + } + ], + "image": { + "path": "01.jpg" + } + }, + { + "id": "02", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + } + ], + "image": { + "path": "02.jpg" + } + }, + { + "id": "03", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "03.jpg" + } + }, + { + "id": "04", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "04.jpg" + } + }, + { + "id": "05", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 1, + "label_id": 3 + }, + { + "id": 3, + "type": "label", + "group": 2, + "label_id": 4 + } + ], + "image": { + "path": "05.jpg" + } + }, + { + "id": "06", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + } + ], + "image": { + "path": "06.jpg" + } + }, + { + "id": "07", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + } + ], + "image": { + "path": "07.jpg" + } + }, + { + "id": "08", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 4 + } + ], + "image": { + "path": "08.jpg" + } + }, + { + "id": "09", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + } + ], + "image": { + "path": "09.jpg" + } + }, + { + "id": "10", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + } + ], + "image": { + "path": "10.jpg" + } + }, + { + "id": "11", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 2 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 3 + } + ], + "image": { + "path": "11.jpg" + } + } + ] +} diff --git a/tests/assets/datumaro_h-label_class_decremental/images/train/00.jpg b/tests/assets/datumaro_h-label_class_decremental/images/train/00.jpg new file mode 100644 index 0000000000000000000000000000000000000000..642d2394403abcd92457fc1565f744d260a8fb84 GIT binary patch literal 4679 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/train/03.jpg b/tests/assets/datumaro_h-label_class_decremental/images/train/03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f886d08bd5d86a6a9438b7a3d928cae6a79a5214 GIT binary patch literal 4393 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 k0ty9xFThml#Y=6 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 rqcj$e$j#3c6%8Vq=?C-iXz28C<%n*1e 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 GCjSc}=_gnK literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/train/08.jpg b/tests/assets/datumaro_h-label_class_decremental/images/train/08.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0cae45f63ac4770be39442c1902b95b10cbcf578 GIT binary patch literal 4041 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/train/09.jpg b/tests/assets/datumaro_h-label_class_decremental/images/train/09.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4600fdc14714f5a52bb93a00195606f01685b8a2 GIT binary patch literal 2160 zcmds%Yfuwc7>3Vgb0b1SASkAwH3R{%V^I(VC87i>)r0_E(WZz9O1&aVQP{yMa;a5O zgeo>QC{Yoj7(lTKLIg>z2#A2UBp`?mt%QpP2sz!rjQ!Ie==4XY-_HBZ+1YpZyw99H z2R1`BF!%Re>!lTyiaS-+ zcWadO4G$Y1H8r<9*0i?0XzzI0`RcW{r?;>F?K|DTppgp!=qMK6|B;J2z1q=as_WY0{bG8Y~_bLF;)-K@ZFt93i-XyZAq8hfn~ZG>!WVEaCb z>^<1uT#tb%ir|ZfG5{ACuue%TaQtW$$v>wx1k0n+QaGlZX#~a(-%M(Ielt6beQ!?M z7c1wn_?zAH3JX<19z~)4HucRll2WeD$5l1(hCAF>rt#CWSzGn*#D+mpx2za~WL}l1 zhORfWQHd%CvfG(LQI_o)9y0(LtJY55&wasyp#LcZoz838cP5xaa3l(Xrf3L$$xje# zCg@KHAs|A)*&YVLGsA{_e3nLO^0q!94@1NOOd${a-(=PA(B;=XRQ~mJUC{YWUQXS< zM=z{#NDe>b@X)n6H^!Stbc_gGP~%`bQ;@&6M6}jWF7EM*nK3jm7lQgdak@V}@&ZqI zJ1t!571`Cbm!9Hw@qs4AKRa9G8pE%}?CK%Ve5JRAKqAnH^^6b*v`nlbQ|AeRkGPVn zW0yeC>YOVnvlujZ8WMhkfPi&ip4i3>+}=SZqUVf^h>{Z*4&4>Jn&Yh|jdRukH z;R7nOt#Z0;Y+jdpbX59NKYDXSrb||6K+7TP7+>rAv9j#*vKj8mZ7q+?GbH-(wmn-kT%6P%D$h4GStiVJojiG1BgY zNtM^x>8q83Q;`K_>96bV96INrbV+sVH(Or2WNDK{_%%h;zG~H^jQgA7o^|Bzy7BE{ zc_g1ce^}t3H@)y)$EvdtVU1ZYPHx_&#gYfAE$0SEhc0n@94=+PQ;B%eg7$!lpq#wM T1NaGa+BS5o9RH$^0$ct9yw4Ur literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/train/10.jpg b/tests/assets/datumaro_h-label_class_decremental/images/train/10.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5d3a166a1b9b0fea1f7e262fd9e06cbf024bbb9f GIT binary patch literal 2417 zcmd^EUygaB?t!Jt5u1_TsG5f!%}s0e}z5pg48DahuE z1OqBU6iuQm1{Kgi!3854HxSf-f{6sk{`#Ub?U#O_{nF0#y!oGbciy>o{=awT9{4%j z3C0EZ`TGGB3IG)H0Qe>F1$ro5=(3(J^wGME(bq@oV=-9lH$jF5SjZ5x-ML!ogfxB*xpJ~7|yA!fe^ zN)N4%!9s`)W2AsH4zZ6$Bc?HkW28C-`404PnDN%G3$YWZQIO3J{H(O>Vgp;BYj+62 zk0o|)>vkS6G%_=vIBBx|)M*aW9o~T@!Qt|5-m1QPuc5K& z{sTetllG^=XC2R9h`Qc&_q_k`QQX@n{W>Tc8Xl1=lsYaHK!3+V@?Ub{5H3Ak4E zN^dtJG)^C5?TQ`0kP1cZm|!z2%>eI{U3~41p{-l6gs^VsV4(%J+=)eyA zBeFlh{^Sw?6Eq5G9vTP8KuZ@huWYG~QC|4)=JlhQOB-%^H7)o&|5F*1-q0;|z1Q{b z{p`7RyqMtJD2qCVER3zTnOvYAN!KdlBqdthm_Y5`J1|J`l!v#|FV2gG!GaJ~fub;l zW~zSB%w*lZo`1KuVrsNgp&;bGE%_Ofo!=1kDav(1am2jq={vRzoJyqiw6+I_FQnkZ zY>gb4`$r|AuE5kgkazkq12*akkf+Ek`!)51nn0W-x4N+ zsB%{f=1Hfi50}DVKMce+7lmKO(;sY&lbdP}I3B0tq?^=vUNBHsgMkB(EvqQpzr8NIU*VPyLp{)D8RO+fI>BLO?&CiNG4eu|1C)%PQU7ipS z@LL@W45xBnK;|pP(C=o+{Yh+v1_lpkT7n;AnE$M8Itg=Mc#p?0_-+|ao zwP!1xt>U?}wnH(ArBq5%!ewDYXkJ9s&fL@6c!@8C+&2}x@|eeVdB0LAgu=TFhx2;> zMb%jhuao(;HrM1ysk=F((oFWP| z$K2?>l)9u^%F&8L0-})RywcBe=`L?NL%Jfv9%zSDr}nxO|0~(a~ZCO!HHv1b4@k9*OK%hPt_C@ z#@bhA8dg%vGVoCG@Gr@%s>wq;X8&Nu2~Q35BN=RVoD!Mx*eXsr5x) zS8!ZZW*cGfWY9FsQ3Qu`xpmwcM;Jz_E( zz`v^j;upC^8owjx3jM}jmuSK8Y<6p_Ml5=Dd-tuDj0Vjb(VCB@l--0W!q=Qk@06#- z=3as!b~U?`Wl5S8T5-~$$+5rM->!z+_rmt%#u+sZsbtAmXT}p@t$%!~iCKLkPJiY@ zr}-N1^tTloJGzsb%!zlpr#OwZ?l_=|wanlTluh@jea-2md5S}F-&+V)Ns&KO%N+In zt6_9~3JjClx5{U(LgN>WyEpIZh)ijk%2Adbv5d@<{9g1Uh#&AVwsIh@bZ~&>{JbfMwV1N6 z125$!G$!yx_AUN%o6e*XUhqE@oE}gt54l!hLYiZ|RCNd$cR%hzhTekzi>%-mzXQzj B$`$|s literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/train/11.jpg b/tests/assets/datumaro_h-label_class_decremental/images/train/11.jpg new file mode 100644 index 0000000000000000000000000000000000000000..95746a0bb58fd969332fd611ac67cd335889f505 GIT binary patch literal 2380 zcmd6oX;2eq7{}k-5KssNL@`wfiW)7493C|pwV*(&283u)6buSjjws3%NJTLcA_^!T zcoG%yqM(Q(CDKq3S}lm-3P%zJ2?Bzg!6n;`&a_|pA@zgPXXpRS?(Bbe|G#;kcNT7f z<-pj(&D{;4PynEi1Hdw{2B1;e(ss1Auo!K}VX+u29*4(&6hU7PPtYge@p=Y&`b2FZ zXNCquL+yq3C7&Ke<1iQ;k$@+BwEU+AKLI2?AO~^`$_k)KC=3Y&SAbbaPTa?MNZ3z> zLSwKvJOPPefEbF5k@y%45*mjjM%)R=eSjt5Oy=0H!kaP!2v!`j!@=|`deqegqHyf^vyvNmt|VJACi*Y4nu z(0x(+4@Aer#vMAGe1v~AMUats@>JI8GiT4`h^}6{o_izjX5qd2MGuM}J}UWDQt?}5 z)zj*l7xgct4UJ9BEehqU*PUH&x>Y@c?}vs*M#sk06Iw15z#4f>Irp9Sk_vNkJC&*Dl=JH( z;nR15<7Ze-+gK@#*9I;S0S2qhY8r<+gfU#YWlDFYkjyGbEp(?1z3h#96d#@{=GEcI z%M$%_!-?RSRAUE%@0F|*I^4c(iZxz^ja-$-PQHv zIUVCE`XB8KnflG!ThHbNi@rM{T2uq7XB&>q_N639KJ4ICm&BxM+8D!yGsfe}WXVG% zk2L7bf)mWp_0Iktwj+!+@^)%|g?j6t&yLHlTrB6c*QJ(;mBjfuWx|Qt9Kn2`qcFpk zaxs+Np$~(mdjT-Gs@mqny(1UP9&1QOgb+GHn9LeI-usJ#!G`<1@E?b`52Y?)@29r* zJ$K+-ynY`B3gxVkb&U6N81%2QP(5e39 zxiM9*Oruuu9uT1H+@s>YdKh@l$>-l{rQ0PoUm7UVP(tYd`DnzbQL&n#;>o^>I9{rvy_f|0*=~BhW1Opco1Bn)1n7MTsyQ}T)#q$N*Ed8!0-cxjWo@K`__OR&TX6~Pv zQR<&&$13&n%k4tuFd=sAppKhH*Uj~1eP&Ws%Ylm%+^M5>S$28W6%Olh34FppqK(kr zDY)+DWqGF5CH`^LvO$YvzI##p`D_U_FdFYU|CR>?f}=2;f)$Pv&g9FgJsNhkG2iPd-+3>vX8sCDH?3v5#~ ze+EpsmK4-5mz7?H>7=Y&pOCPku_w`jW9}o_EcB;Q=swltj0P{R%ymC>+Ba=c*n$S8 zkePoY^g&!;_`}S_mDH`~^zq@l`@9?8IV~xYu@gkABV*t7#QAIq%G&MljJo2h+f~-} zd-$o)+UHBp)+969kZDg}$rhJQOe|g*z3_<11&aS|K AQ~&?~ literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/valid/00.jpg b/tests/assets/datumaro_h-label_class_decremental/images/valid/00.jpg new file mode 100644 index 0000000000000000000000000000000000000000..642d2394403abcd92457fc1565f744d260a8fb84 GIT binary patch literal 4679 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/valid/03.jpg b/tests/assets/datumaro_h-label_class_decremental/images/valid/03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f886d08bd5d86a6a9438b7a3d928cae6a79a5214 GIT binary patch literal 4393 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 k0ty9xFThml#Y=6 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 rqcj$e$j#3c6%8Vq=?C-iXz28C<%n*1e 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 GCjSc}=_gnK literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/valid/08.jpg b/tests/assets/datumaro_h-label_class_decremental/images/valid/08.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0cae45f63ac4770be39442c1902b95b10cbcf578 GIT binary patch literal 4041 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/valid/09.jpg b/tests/assets/datumaro_h-label_class_decremental/images/valid/09.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4600fdc14714f5a52bb93a00195606f01685b8a2 GIT binary patch literal 2160 zcmds%Yfuwc7>3Vgb0b1SASkAwH3R{%V^I(VC87i>)r0_E(WZz9O1&aVQP{yMa;a5O zgeo>QC{Yoj7(lTKLIg>z2#A2UBp`?mt%QpP2sz!rjQ!Ie==4XY-_HBZ+1YpZyw99H z2R1`BF!%Re>!lTyiaS-+ zcWadO4G$Y1H8r<9*0i?0XzzI0`RcW{r?;>F?K|DTppgp!=qMK6|B;J2z1q=as_WY0{bG8Y~_bLF;)-K@ZFt93i-XyZAq8hfn~ZG>!WVEaCb z>^<1uT#tb%ir|ZfG5{ACuue%TaQtW$$v>wx1k0n+QaGlZX#~a(-%M(Ielt6beQ!?M z7c1wn_?zAH3JX<19z~)4HucRll2WeD$5l1(hCAF>rt#CWSzGn*#D+mpx2za~WL}l1 zhORfWQHd%CvfG(LQI_o)9y0(LtJY55&wasyp#LcZoz838cP5xaa3l(Xrf3L$$xje# zCg@KHAs|A)*&YVLGsA{_e3nLO^0q!94@1NOOd${a-(=PA(B;=XRQ~mJUC{YWUQXS< zM=z{#NDe>b@X)n6H^!Stbc_gGP~%`bQ;@&6M6}jWF7EM*nK3jm7lQgdak@V}@&ZqI zJ1t!571`Cbm!9Hw@qs4AKRa9G8pE%}?CK%Ve5JRAKqAnH^^6b*v`nlbQ|AeRkGPVn zW0yeC>YOVnvlujZ8WMhkfPi&ip4i3>+}=SZqUVf^h>{Z*4&4>Jn&Yh|jdRukH z;R7nOt#Z0;Y+jdpbX59NKYDXSrb||6K+7TP7+>rAv9j#*vKj8mZ7q+?GbH-(wmn-kT%6P%D$h4GStiVJojiG1BgY zNtM^x>8q83Q;`K_>96bV96INrbV+sVH(Or2WNDK{_%%h;zG~H^jQgA7o^|Bzy7BE{ zc_g1ce^}t3H@)y)$EvdtVU1ZYPHx_&#gYfAE$0SEhc0n@94=+PQ;B%eg7$!lpq#wM T1NaGa+BS5o9RH$^0$ct9yw4Ur literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/valid/10.jpg b/tests/assets/datumaro_h-label_class_decremental/images/valid/10.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5d3a166a1b9b0fea1f7e262fd9e06cbf024bbb9f GIT binary patch literal 2417 zcmd^EUygaB?t!Jt5u1_TsG5f!%}s0e}z5pg48DahuE z1OqBU6iuQm1{Kgi!3854HxSf-f{6sk{`#Ub?U#O_{nF0#y!oGbciy>o{=awT9{4%j z3C0EZ`TGGB3IG)H0Qe>F1$ro5=(3(J^wGME(bq@oV=-9lH$jF5SjZ5x-ML!ogfxB*xpJ~7|yA!fe^ zN)N4%!9s`)W2AsH4zZ6$Bc?HkW28C-`404PnDN%G3$YWZQIO3J{H(O>Vgp;BYj+62 zk0o|)>vkS6G%_=vIBBx|)M*aW9o~T@!Qt|5-m1QPuc5K& z{sTetllG^=XC2R9h`Qc&_q_k`QQX@n{W>Tc8Xl1=lsYaHK!3+V@?Ub{5H3Ak4E zN^dtJG)^C5?TQ`0kP1cZm|!z2%>eI{U3~41p{-l6gs^VsV4(%J+=)eyA zBeFlh{^Sw?6Eq5G9vTP8KuZ@huWYG~QC|4)=JlhQOB-%^H7)o&|5F*1-q0;|z1Q{b z{p`7RyqMtJD2qCVER3zTnOvYAN!KdlBqdthm_Y5`J1|J`l!v#|FV2gG!GaJ~fub;l zW~zSB%w*lZo`1KuVrsNgp&;bGE%_Ofo!=1kDav(1am2jq={vRzoJyqiw6+I_FQnkZ zY>gb4`$r|AuE5kgkazkq12*akkf+Ek`!)51nn0W-x4N+ zsB%{f=1Hfi50}DVKMce+7lmKO(;sY&lbdP}I3B0tq?^=vUNBHsgMkB(EvqQpzr8NIU*VPyLp{)D8RO+fI>BLO?&CiNG4eu|1C)%PQU7ipS z@LL@W45xBnK;|pP(C=o+{Yh+v1_lpkT7n;AnE$M8Itg=Mc#p?0_-+|ao zwP!1xt>U?}wnH(ArBq5%!ewDYXkJ9s&fL@6c!@8C+&2}x@|eeVdB0LAgu=TFhx2;> zMb%jhuao(;HrM1ysk=F((oFWP| z$K2?>l)9u^%F&8L0-})RywcBe=`L?NL%Jfv9%zSDr}nxO|0~(a~ZCO!HHv1b4@k9*OK%hPt_C@ z#@bhA8dg%vGVoCG@Gr@%s>wq;X8&Nu2~Q35BN=RVoD!Mx*eXsr5x) zS8!ZZW*cGfWY9FsQ3Qu`xpmwcM;Jz_E( zz`v^j;upC^8owjx3jM}jmuSK8Y<6p_Ml5=Dd-tuDj0Vjb(VCB@l--0W!q=Qk@06#- z=3as!b~U?`Wl5S8T5-~$$+5rM->!z+_rmt%#u+sZsbtAmXT}p@t$%!~iCKLkPJiY@ zr}-N1^tTloJGzsb%!zlpr#OwZ?l_=|wanlTluh@jea-2md5S}F-&+V)Ns&KO%N+In zt6_9~3JjClx5{U(LgN>WyEpIZh)ijk%2Adbv5d@<{9g1Uh#&AVwsIh@bZ~&>{JbfMwV1N6 z125$!G$!yx_AUN%o6e*XUhqE@oE}gt54l!hLYiZ|RCNd$cR%hzhTekzi>%-mzXQzj B$`$|s literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label_class_decremental/images/valid/11.jpg b/tests/assets/datumaro_h-label_class_decremental/images/valid/11.jpg new file mode 100644 index 0000000000000000000000000000000000000000..95746a0bb58fd969332fd611ac67cd335889f505 GIT binary patch literal 2380 zcmd6oX;2eq7{}k-5KssNL@`wfiW)7493C|pwV*(&283u)6buSjjws3%NJTLcA_^!T zcoG%yqM(Q(CDKq3S}lm-3P%zJ2?Bzg!6n;`&a_|pA@zgPXXpRS?(Bbe|G#;kcNT7f z<-pj(&D{;4PynEi1Hdw{2B1;e(ss1Auo!K}VX+u29*4(&6hU7PPtYge@p=Y&`b2FZ zXNCquL+yq3C7&Ke<1iQ;k$@+BwEU+AKLI2?AO~^`$_k)KC=3Y&SAbbaPTa?MNZ3z> zLSwKvJOPPefEbF5k@y%45*mjjM%)R=eSjt5Oy=0H!kaP!2v!`j!@=|`deqegqHyf^vyvNmt|VJACi*Y4nu z(0x(+4@Aer#vMAGe1v~AMUats@>JI8GiT4`h^}6{o_izjX5qd2MGuM}J}UWDQt?}5 z)zj*l7xgct4UJ9BEehqU*PUH&x>Y@c?}vs*M#sk06Iw15z#4f>Irp9Sk_vNkJC&*Dl=JH( z;nR15<7Ze-+gK@#*9I;S0S2qhY8r<+gfU#YWlDFYkjyGbEp(?1z3h#96d#@{=GEcI z%M$%_!-?RSRAUE%@0F|*I^4c(iZxz^ja-$-PQHv zIUVCE`XB8KnflG!ThHbNi@rM{T2uq7XB&>q_N639KJ4ICm&BxM+8D!yGsfe}WXVG% zk2L7bf)mWp_0Iktwj+!+@^)%|g?j6t&yLHlTrB6c*QJ(;mBjfuWx|Qt9Kn2`qcFpk zaxs+Np$~(mdjT-Gs@mqny(1UP9&1QOgb+GHn9LeI-usJ#!G`<1@E?b`52Y?)@29r* zJ$K+-ynY`B3gxVkb&U6N81%2QP(5e39 zxiM9*Oruuu9uT1H+@s>YdKh@l$>-l{rQ0PoUm7UVP(tYd`DnzbQL&n#;>o^>I9{rvy_f|0*=~BhW1Opco1Bn)1n7MTsyQ}T)#q$N*Ed8!0-cxjWo@K`__OR&TX6~Pv zQR<&&$13&n%k4tuFd=sAppKhH*Uj~1eP&Ws%Ylm%+^M5>S$28W6%Olh34FppqK(kr zDY)+DWqGF5CH`^LvO$YvzI##p`D_U_FdFYU|CR>?f}=2;f)$Pv&g9FgJsNhkG2iPd-+3>vX8sCDH?3v5#~ ze+EpsmK4-5mz7?H>7=Y&pOCPku_w`jW9}o_EcB;Q=swltj0P{R%ymC>+Ba=c*n$S8 zkePoY^g&!;_`}S_mDH`~^zq@l`@9?8IV~xYu@gkABV*t7#QAIq%G&MljJo2h+f~-} zd-$o)+UHBp)+969kZDg}$rhJQOe|g*z3_<11&aS|K AQ~&?~ literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/annotations/train.json b/tests/assets/datumaro_multilabel/annotations/train.json new file mode 100644 index 00000000000..f4622d990de --- /dev/null +++ b/tests/assets/datumaro_multilabel/annotations/train.json @@ -0,0 +1,302 @@ +{ + "info": {}, + "categories": { + "label": { + "label_groups": [ + { + "name": "___rectangle", + "group_type": "exclusive", + "labels": ["rectangle"] + }, + { + "name": "___circle", + "group_type": "exclusive", + "labels": ["circle"] + }, + { + "name": "___triangle", + "group_type": "exclusive", + "labels": ["triangle"] + } + ], + "labels": [ + { + "name": "rectangle", + "parent": "", + "attributes": [] + }, + { + "name": "circle", + "parent": "", + "attributes": [] + }, + { + "name": "triangle", + "parent": "", + "attributes": [] + } + ], + "attributes": [] + } + }, + "items": [ + { + "id": "00", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 1, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "00.jpg" + } + }, + { + "id": "01", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "01.jpg" + } + }, + { + "id": "02", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "02.jpg" + } + }, + { + "id": "03", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "03.jpg" + } + }, + { + "id": "04", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "04.jpg" + } + }, + { + "id": "05", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "05.jpg" + } + }, + { + "id": "06", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "06.jpg" + } + }, + { + "id": "07", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "07.jpg" + } + }, + { + "id": "08", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "08.jpg" + } + }, + { + "id": "09", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "09.jpg" + } + }, + { + "id": "10", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "10.jpg" + } + }, + { + "id": "11", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "11.jpg" + } + } + ] +} diff --git a/tests/assets/datumaro_multilabel/annotations/valid.json b/tests/assets/datumaro_multilabel/annotations/valid.json new file mode 100644 index 00000000000..d5f0b5b8dc6 --- /dev/null +++ b/tests/assets/datumaro_multilabel/annotations/valid.json @@ -0,0 +1,302 @@ +{ + "info": {}, + "categories": { + "label": { + "label_groups": [ + { + "name": "___rectangle", + "group_type": "exclusive", + "labels": ["rectangle"] + }, + { + "name": "___circle", + "group_type": "exclusive", + "labels": ["circle"] + }, + { + "name": "___triangle", + "group_type": "exclusive", + "labels": ["triangle"] + } + ], + "labels": [ + { + "name": "rectangle", + "parent": "", + "attributes": [] + }, + { + "name": "circle", + "parent": "", + "attributes": [] + }, + { + "name": "triangle", + "parent": "", + "attributes": [] + } + ], + "attributes": [] + } + }, + "items": [ + { + "id": "00", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "00.jpg" + } + }, + { + "id": "01", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "01.jpg" + } + }, + { + "id": "02", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "02.jpg" + } + }, + { + "id": "03", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "03.jpg" + } + }, + { + "id": "04", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "04.jpg" + } + }, + { + "id": "05", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "05.jpg" + } + }, + { + "id": "06", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "06.jpg" + } + }, + { + "id": "07", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "07.jpg" + } + }, + { + "id": "08", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "08.jpg" + } + }, + { + "id": "09", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "09.jpg" + } + }, + { + "id": "10", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "10.jpg" + } + }, + { + "id": "11", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "11.jpg" + } + } + ] +} diff --git a/tests/assets/datumaro_multilabel/images/train/00.jpg b/tests/assets/datumaro_multilabel/images/train/00.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9331f386dc8f2cd9aa2d6fc5069769ca81bd9ba8 GIT binary patch literal 3482 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/train/02.jpg b/tests/assets/datumaro_multilabel/images/train/02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af7ddd489613b39dda026a523210afab92e32593 GIT binary patch literal 3414 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 O3jS{Q|ASa*&hW4JA)Gq^ literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/train/03.jpg b/tests/assets/datumaro_multilabel/images/train/03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..77ed1d56b09c5dcdbb4fc3b5ade3acfd22da01a9 GIT binary patch literal 3044 zcmeH`c~BE+8pb;W5`ut6h{|P9KyC$r;ZVY5lt2k`42L*DhysxjG%9BV0RfFD8YD#oCPgrAYoSc7ladmS)jXQVV z$JY;k;mXx(LBS!RVKFyjL1JvuX)c(U!qSBTRGK=5Bl=K{V2ya4{*e+%LA(go;wC`m9 z8CdlHMD`D`e{u}~;$V4d_*-J>nMl7c z4En|Pe7c6Uw3o9a^a9sxD3K5N+OMicuDI6lfsxV(1|OIYNLSzk74!`)>YIb~jFQ4> zquexaFY%LTgd<+XSecyk+GY*eR%_8Dvn0Q^+b;cJAx^ezr%etEb25i)f2>-m1*ICn zfvrdO^a=f))q#N4Nd7@{8^ARih2u(m6Z z4=g^JUKpcW@c4jSLJMcw7VhYD!RAXx4SuYDH8Hk7&P=D?TZQyO<1WUXY3I_N`&o?g zp1I&q3AVyJv zzLt`#C$wO?mP<-d*qbbBz#kZ%$@UpOQ1Io~jHa+!7iN%dbU%^tc8%%AQCB!@D86CL zj^`kwM+hH6^IOGU4JRemC4BH9^tP$A;zY9Txpj|;sWwGcCjkOcQZWAm=T+%n%a3G_7Ls}Ud?d1 zxyJ16$_()XIxM4^9m^ZEGE2}G?J>r=P&V0$S|rG6^kTr*$lX|ac$?;oj@y9S6pDp3 z=+Ee=wQn?z7aI$Nkz74mZ1E?mXV!|kxrO~`X;-g>OtkbV(7zG1kN%q8G#gj9cUj`; z#3~%=KI4YfPK@$nb7vt6COm1eG9gK=W;#$}$!S3n{<;6LXiXJK}uVc12uq{L!WQ zv-Q4dk{kB5y^l7#bSz8ohMHox6?|Rpt9@~nR+O4Bl)r9Y6rL&)>clAU$Ez;;Z-iw~ zLf#~Iwp>apCSW&U=)4{lIR<-gl5X^pLG3~#{(#QEazIN1e<9GK*-c;ScthAP9qk>; zWPGPS`N+=p76WB+7`5xA9LM$6@3dMSqNz%g*= ziVwWTu450b^pz+`>0^M=YR2is0v`KLgi`sOYQk80%dmRU?^$k&6s@89cer#upr_Ny zxuKh$H_+M{h;hys+L>i>mB_ zyK`pZd2x4ZUPv}YM#%`vuJq9Sum^f;^#}o_+KwJUfpt4ZJ)=j45AiQO9a zOx2qQ{JH)m0*u87@SG4G=1kdJdxpmf#(Pi?tz?I*UM;(WHPnC(n!?DbO?s_ z-~|OY!DKz#w3(hgv4{oVGqTmq$+ML^>6w$0d8j%lGK%GJCU?9gBfh7+sn`d!}gX!bK_b*55zON)^BX1>F z$-4&Uts^Uk-XBE|X=TM^^x#}vSo#BxBMrE;wa_Az>r{m1x=lInQ(7I8b)KF70q%N^ zfMOP+=9KO)8#{cCGmQxuGzlH6O63Ol;FTZT!6+4LwcBBom-GkLjzWYeLW#=P0)q!L v+G~Gle#F&o-z;$_G?-YLwIQ)ZOOe;e)`F?xqMWzR)!+Ji4gMc9!0-PPf43jg literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/train/04.jpg b/tests/assets/datumaro_multilabel/images/train/04.jpg new file mode 100644 index 0000000000000000000000000000000000000000..77ed1d56b09c5dcdbb4fc3b5ade3acfd22da01a9 GIT binary patch literal 3044 zcmeH`c~BE+8pb;W5`ut6h{|P9KyC$r;ZVY5lt2k`42L*DhysxjG%9BV0RfFD8YD#oCPgrAYoSc7ladmS)jXQVV z$JY;k;mXx(LBS!RVKFyjL1JvuX)c(U!qSBTRGK=5Bl=K{V2ya4{*e+%LA(go;wC`m9 z8CdlHMD`D`e{u}~;$V4d_*-J>nMl7c z4En|Pe7c6Uw3o9a^a9sxD3K5N+OMicuDI6lfsxV(1|OIYNLSzk74!`)>YIb~jFQ4> zquexaFY%LTgd<+XSecyk+GY*eR%_8Dvn0Q^+b;cJAx^ezr%etEb25i)f2>-m1*ICn zfvrdO^a=f))q#N4Nd7@{8^ARih2u(m6Z z4=g^JUKpcW@c4jSLJMcw7VhYD!RAXx4SuYDH8Hk7&P=D?TZQyO<1WUXY3I_N`&o?g zp1I&q3AVyJv zzLt`#C$wO?mP<-d*qbbBz#kZ%$@UpOQ1Io~jHa+!7iN%dbU%^tc8%%AQCB!@D86CL zj^`kwM+hH6^IOGU4JRemC4BH9^tP$A;zY9Txpj|;sWwGcCjkOcQZWAm=T+%n%a3G_7Ls}Ud?d1 zxyJ16$_()XIxM4^9m^ZEGE2}G?J>r=P&V0$S|rG6^kTr*$lX|ac$?;oj@y9S6pDp3 z=+Ee=wQn?z7aI$Nkz74mZ1E?mXV!|kxrO~`X;-g>OtkbV(7zG1kN%q8G#gj9cUj`; z#3~%=KI4YfPK@$nb7vt6COm1eG9gK=W;#$}$!S3n{<;6LXiXJK}uVc12uq{L!WQ zv-Q4dk{kB5y^l7#bSz8ohMHox6?|Rpt9@~nR+O4Bl)r9Y6rL&)>clAU$Ez;;Z-iw~ zLf#~Iwp>apCSW&U=)4{lIR<-gl5X^pLG3~#{(#QEazIN1e<9GK*-c;ScthAP9qk>; zWPGPS`N+=p76WB+7`5xA9LM$6@3dMSqNz%g*= ziVwWTu450b^pz+`>0^M=YR2is0v`KLgi`sOYQk80%dmRU?^$k&6s@89cer#upr_Ny zxuKh$H_+M{h;hys+L>i>mB_ zyK`pZd2x4ZUPv}YM#%`vuJq9Sum^f;^#}o_+KwJUfpt4ZJ)=j45AiQO9a zOx2qQ{JH)m0*u87@SG4G=1kdJdxpmf#(Pi?tz?I*UM;(WHPnC(n!?DbO?s_ z-~|OY!DKz#w3(hgv4{oVGqTmq$+ML^>6w$0d8j%lGK%GJCU?9gBfh7+sn`d!}gX!bK_b*55zON)^BX1>F z$-4&Uts^Uk-XBE|X=TM^^x#}vSo#BxBMrE;wa_Az>r{m1x=lInQ(7I8b)KF70q%N^ zfMOP+=9KO)8#{cCGmQxuGzlH6O63Ol;FTZT!6+4LwcBBom-GkLjzWYeLW#=P0)q!L v+G~Gle#F&o-z;$_G?-YLwIQ)ZOOe;e)`F?xqMWzR)!+Ji4gMc9!0-PPf43jg literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/train/05.jpg b/tests/assets/datumaro_multilabel/images/train/05.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a965731053eac85e4787ed9cbd457600fe0aca2 GIT binary patch literal 3074 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 oWOesa4p=HX(kxJJ>oFRoJ^cGSh$zivSf=G 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/train/08.jpg b/tests/assets/datumaro_multilabel/images/train/08.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0aa5f25c7c284fe87aba1f4c872d7f7b5a4af701 GIT binary patch literal 3027 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/valid/00.jpg b/tests/assets/datumaro_multilabel/images/valid/00.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9331f386dc8f2cd9aa2d6fc5069769ca81bd9ba8 GIT binary patch literal 3482 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/valid/02.jpg b/tests/assets/datumaro_multilabel/images/valid/02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af7ddd489613b39dda026a523210afab92e32593 GIT binary patch literal 3414 zcmds32~bm67X2Y10Y$=+D2p_$KqHIIt|CYTIw)~i0>~N+7#2|x2_ON*G>FhFvIGP{ z8f255G_o~27-VY%0TBUN0tg7AVGj_J4tCYdRP}Uq&(u^+)!bj_*8Bh6|K2(O-TL=( z-f|`ZA&il+5dZ=K0El}5oKe6K-~sK~p6A&!2zbx)LLguWloty90vJCZ6vhvOLizUd z@$cI+?nq$&K7qZPy+wX|m4_D$=G_N_!oGz3TaMEV2=f7xz$6%S6yOmCfrUYwRzRA| zllKcfF4^A}<1#UXf8W7FV&W2V-^d?RP}I~qaq^V*X~XZ(M(55OV=S#MTHDy# z*%Mq`-Q2HuT=ntwyX8*`2)q}1KP>!*h)7C8;=`omM=7aU**UpS^YRM{pOsfsR#pF8 zQ~R9O`l7A9qqD1j;Pv2}p|`^$^r`8Y_p@{J9~PEZR@c@yHkn)7d%8dX_%~Tx|1Wh3 zb9M3TodIl57l_B73$QSR_lO!)L>~)t_7**=9?XX@h|er*;g`{{U?49O`}Q4_)uhWU z?@9Zj?5_z6`Hz(SUD!W#jQ|2*5clxF!T<_jljpM{fn)yz>^!E+KR3I7I{A5qz}3^S zG!-}wha&}c8P8>)gU8QZ5h~X}bjnhUbt%`LaDe+N41ab6DvZoPdvSnPECbIz|2dQc ze6;(C1N8frrZ%N6zx{Z%Z4gu3s1TGlww2M55<_@&PPO=<2g_o|tZr48u#wLsFotLo zpvon}S?2A`w_~&o{5K1|#$gJgC(R3JiXrO!FsTUS@?a2c$BEooONwHTq1K*BFaw+H z7#k(_!^e%S61+yvjIq1n?{t-^4#Ko`S8G$VkzcGwcZ5D6i|84=q=Xj(d~1%{>8hhH zHRL4O8F%H|51J=k%U(%TGCnu2lICzbqL@DW=0?}oT{&?z&F0?Ux2L)PB%kKAla%t* z_R1S!k3h#l8bqn;)W8$z*PF5{#+}qBFMa%i@AjnDa{vh0lmQRFcM$c5RHC{~(E8f}Mm=BGB2E>70#gx2_u7*gBJL0c}v{|&9 zUp%tvn8Kp9vb199*ZXaCO&bVu71@2IqjsgSwdJk|tfPA`F9Lh^k05=SrM6qB2PWO7 zQ^P9`5ZdpWjKzd|c=^FM90#t)A3?izwd%J(Ttt+7*plE4Uk;EJ!2uGZn%G)+I2O>fDBYlNTeAFEr4El{_cw;mz>>i~7z{0ip z648J1;gb7-W1#3t`i0Q=*Lauu58b#{mq8#kFDBq`nsH zrKMB(CC6_c#}@@QE>$70XqP|`8etB050QLyX=D~j(MA-<*M=15IE8RO0E#qiXm)^E z#Q}^WE)pV%&Q_NV+M3k`(uv6L^`;9J@%lsYcVDUgtx>t6=8CP+HnFh7YDrm^cvdg_ zX7B63p(fg3t^gsg|H3kU0CjXZK{-2u{xjkD=l;|;6K?iecOzNU_p>(>vFM-zRx_6j z@Mj1*4q?1|o_u0JIldz^J6;UwD)V*7=}12!9?HTBHqeh%45g$lnqU$1Y~Xt53gaBa zQ*6B}Z9~3<+U>0=7e-aI7(*?7M%Ak0`osJa``WR0m(J(SZ&PjS@@5}l(a4B}FS$h8 zZ11Eex5c$1x?Ubki#l4wgRQPOjXu`pDP$(f3BB+i4G5xgGbaf0YUSO!-H66UGGkwCt5FTgQJic0_ zxiq5IFfrcmc=Dq=cmECd)niyR0{9bviY9vxPB2w)CT?w7aY(h^{c99;eWeV#E)P8a zlRCVX>C+l$6IqeJ{@|X9Cc zi^Cwk&T*%-VaOS`025tu(;<)2hFx;^u4Hh%{tHF|DX`2>b;bXELmJ*ja&z&J%Xqc@ z#Qe5Ww=Zf$&}j-KGOuV=*8N6V|6)6skdTnR6AsHeyx6|H+Hxc2rF(uRx~u4Hicg%X z>TZC1ngF=xCcMk4OGAQKUG6ntxyh3L;hlJNZcmwaY+^!`V<5Hb!^cXUK&9Sw_z3)L zjL?Yny#UR}jjlhtL>Nr2#yO;?5td6OUo?r_!A{HCYDqo}DhkPx zwxTowmA+aF2x}-n6v@x{VE)8Y(@Cq_$Ean`ojsi93w_^YJpR->+xT_ws-Rr=`v+~9 zl|b35XzRX97~=Rr-owb+t*)aC3)L`FKSxru1mBhecFk zPQS5I)p&$#rDubPfv4Op^~eSez&BpW0rc=&!sNDCrqMceYlQ=JM6tC#kqhrwa)7hQ zCWaLU*b+r~8Ez$yM`~XD{LfJbW|M>R3L9pQfDfzm8t8Oc z%~IAq?G9Ybm3Nife8}xUmLKp$9((eaKqYyx4b3?~bJLor)z!@~4lttyXYJ^!3qhoG1+_ozQk|prnSg_%V|8Qb1yY4-rBYqVA)gD@%ipQqgQgia z@~LlFE-kWZ{fZUlQfJ-L1NQ42E|+jcaw||h{zq?XnGA81_Z`)kxY(?P&CMUjs~U=9 z-5%`oMP@ZRqY*aUl7Cc(e)rti!%r)5YL*9_uVXCUrZ4>_JF7DDQk6u-HH(DktO1i7 zss1JbPrpg9Cd{Q@OGyowQT|Bm_DIB9TI6B|r7#a_PTm?Xsjl-Xa-v{5xSd`wH}(Fo O3jS{Q|ASa*&hW4JA)Gq^ literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/valid/03.jpg b/tests/assets/datumaro_multilabel/images/valid/03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..77ed1d56b09c5dcdbb4fc3b5ade3acfd22da01a9 GIT binary patch literal 3044 zcmeH`c~BE+8pb;W5`ut6h{|P9KyC$r;ZVY5lt2k`42L*DhysxjG%9BV0RfFD8YD#oCPgrAYoSc7ladmS)jXQVV z$JY;k;mXx(LBS!RVKFyjL1JvuX)c(U!qSBTRGK=5Bl=K{V2ya4{*e+%LA(go;wC`m9 z8CdlHMD`D`e{u}~;$V4d_*-J>nMl7c z4En|Pe7c6Uw3o9a^a9sxD3K5N+OMicuDI6lfsxV(1|OIYNLSzk74!`)>YIb~jFQ4> zquexaFY%LTgd<+XSecyk+GY*eR%_8Dvn0Q^+b;cJAx^ezr%etEb25i)f2>-m1*ICn zfvrdO^a=f))q#N4Nd7@{8^ARih2u(m6Z z4=g^JUKpcW@c4jSLJMcw7VhYD!RAXx4SuYDH8Hk7&P=D?TZQyO<1WUXY3I_N`&o?g zp1I&q3AVyJv zzLt`#C$wO?mP<-d*qbbBz#kZ%$@UpOQ1Io~jHa+!7iN%dbU%^tc8%%AQCB!@D86CL zj^`kwM+hH6^IOGU4JRemC4BH9^tP$A;zY9Txpj|;sWwGcCjkOcQZWAm=T+%n%a3G_7Ls}Ud?d1 zxyJ16$_()XIxM4^9m^ZEGE2}G?J>r=P&V0$S|rG6^kTr*$lX|ac$?;oj@y9S6pDp3 z=+Ee=wQn?z7aI$Nkz74mZ1E?mXV!|kxrO~`X;-g>OtkbV(7zG1kN%q8G#gj9cUj`; z#3~%=KI4YfPK@$nb7vt6COm1eG9gK=W;#$}$!S3n{<;6LXiXJK}uVc12uq{L!WQ zv-Q4dk{kB5y^l7#bSz8ohMHox6?|Rpt9@~nR+O4Bl)r9Y6rL&)>clAU$Ez;;Z-iw~ zLf#~Iwp>apCSW&U=)4{lIR<-gl5X^pLG3~#{(#QEazIN1e<9GK*-c;ScthAP9qk>; zWPGPS`N+=p76WB+7`5xA9LM$6@3dMSqNz%g*= ziVwWTu450b^pz+`>0^M=YR2is0v`KLgi`sOYQk80%dmRU?^$k&6s@89cer#upr_Ny zxuKh$H_+M{h;hys+L>i>mB_ zyK`pZd2x4ZUPv}YM#%`vuJq9Sum^f;^#}o_+KwJUfpt4ZJ)=j45AiQO9a zOx2qQ{JH)m0*u87@SG4G=1kdJdxpmf#(Pi?tz?I*UM;(WHPnC(n!?DbO?s_ z-~|OY!DKz#w3(hgv4{oVGqTmq$+ML^>6w$0d8j%lGK%GJCU?9gBfh7+sn`d!}gX!bK_b*55zON)^BX1>F z$-4&Uts^Uk-XBE|X=TM^^x#}vSo#BxBMrE;wa_Az>r{m1x=lInQ(7I8b)KF70q%N^ zfMOP+=9KO)8#{cCGmQxuGzlH6O63Ol;FTZT!6+4LwcBBom-GkLjzWYeLW#=P0)q!L v+G~Gle#F&o-z;$_G?-YLwIQ)ZOOe;e)`F?xqMWzR)!+Ji4gMc9!0-PPf43jg literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/valid/04.jpg b/tests/assets/datumaro_multilabel/images/valid/04.jpg new file mode 100644 index 0000000000000000000000000000000000000000..77ed1d56b09c5dcdbb4fc3b5ade3acfd22da01a9 GIT binary patch literal 3044 zcmeH`c~BE+8pb;W5`ut6h{|P9KyC$r;ZVY5lt2k`42L*DhysxjG%9BV0RfFD8YD#oCPgrAYoSc7ladmS)jXQVV z$JY;k;mXx(LBS!RVKFyjL1JvuX)c(U!qSBTRGK=5Bl=K{V2ya4{*e+%LA(go;wC`m9 z8CdlHMD`D`e{u}~;$V4d_*-J>nMl7c z4En|Pe7c6Uw3o9a^a9sxD3K5N+OMicuDI6lfsxV(1|OIYNLSzk74!`)>YIb~jFQ4> zquexaFY%LTgd<+XSecyk+GY*eR%_8Dvn0Q^+b;cJAx^ezr%etEb25i)f2>-m1*ICn zfvrdO^a=f))q#N4Nd7@{8^ARih2u(m6Z z4=g^JUKpcW@c4jSLJMcw7VhYD!RAXx4SuYDH8Hk7&P=D?TZQyO<1WUXY3I_N`&o?g zp1I&q3AVyJv zzLt`#C$wO?mP<-d*qbbBz#kZ%$@UpOQ1Io~jHa+!7iN%dbU%^tc8%%AQCB!@D86CL zj^`kwM+hH6^IOGU4JRemC4BH9^tP$A;zY9Txpj|;sWwGcCjkOcQZWAm=T+%n%a3G_7Ls}Ud?d1 zxyJ16$_()XIxM4^9m^ZEGE2}G?J>r=P&V0$S|rG6^kTr*$lX|ac$?;oj@y9S6pDp3 z=+Ee=wQn?z7aI$Nkz74mZ1E?mXV!|kxrO~`X;-g>OtkbV(7zG1kN%q8G#gj9cUj`; z#3~%=KI4YfPK@$nb7vt6COm1eG9gK=W;#$}$!S3n{<;6LXiXJK}uVc12uq{L!WQ zv-Q4dk{kB5y^l7#bSz8ohMHox6?|Rpt9@~nR+O4Bl)r9Y6rL&)>clAU$Ez;;Z-iw~ zLf#~Iwp>apCSW&U=)4{lIR<-gl5X^pLG3~#{(#QEazIN1e<9GK*-c;ScthAP9qk>; zWPGPS`N+=p76WB+7`5xA9LM$6@3dMSqNz%g*= ziVwWTu450b^pz+`>0^M=YR2is0v`KLgi`sOYQk80%dmRU?^$k&6s@89cer#upr_Ny zxuKh$H_+M{h;hys+L>i>mB_ zyK`pZd2x4ZUPv}YM#%`vuJq9Sum^f;^#}o_+KwJUfpt4ZJ)=j45AiQO9a zOx2qQ{JH)m0*u87@SG4G=1kdJdxpmf#(Pi?tz?I*UM;(WHPnC(n!?DbO?s_ z-~|OY!DKz#w3(hgv4{oVGqTmq$+ML^>6w$0d8j%lGK%GJCU?9gBfh7+sn`d!}gX!bK_b*55zON)^BX1>F z$-4&Uts^Uk-XBE|X=TM^^x#}vSo#BxBMrE;wa_Az>r{m1x=lInQ(7I8b)KF70q%N^ zfMOP+=9KO)8#{cCGmQxuGzlH6O63Ol;FTZT!6+4LwcBBom-GkLjzWYeLW#=P0)q!L v+G~Gle#F&o-z;$_G?-YLwIQ)ZOOe;e)`F?xqMWzR)!+Ji4gMc9!0-PPf43jg literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/valid/05.jpg b/tests/assets/datumaro_multilabel/images/valid/05.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a965731053eac85e4787ed9cbd457600fe0aca2 GIT binary patch literal 3074 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 oWOesa4p=HX(kxJJ>oFRoJ^cGSh$zivSf=G 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel/images/valid/08.jpg b/tests/assets/datumaro_multilabel/images/valid/08.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0aa5f25c7c284fe87aba1f4c872d7f7b5a4af701 GIT binary patch literal 3027 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 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/annotations/train.json b/tests/assets/datumaro_multilabel_class_decremental/annotations/train.json new file mode 100644 index 00000000000..0ebab7ae01d --- /dev/null +++ b/tests/assets/datumaro_multilabel_class_decremental/annotations/train.json @@ -0,0 +1,238 @@ +{ + "info": {}, + "categories": { + "label": { + "label_groups": [ + { + "name": "___rectangle", + "group_type": "exclusive", + "labels": ["rectangle"] + }, + { + "name": "___circle", + "group_type": "exclusive", + "labels": ["circle"] + } + ], + "labels": [ + { + "name": "rectangle", + "parent": "", + "attributes": [] + }, + { + "name": "circle", + "parent": "", + "attributes": [] + } + ], + "attributes": [] + } + }, + "items": [ + { + "id": "00", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "00.jpg" + } + }, + { + "id": "01", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "01.jpg" + } + }, + { + "id": "02", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "02.jpg" + } + }, + { + "id": "03", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "03.jpg" + } + }, + { + "id": "04", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "04.jpg" + } + }, + { + "id": "05", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "05.jpg" + } + }, + { + "id": "06", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "06.jpg" + } + }, + { + "id": "07", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "07.jpg" + } + }, + { + "id": "08", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "08.jpg" + } + }, + { + "id": "09", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + } + ], + "image": { + "path": "09.jpg" + } + }, + { + "id": "10", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + } + ], + "image": { + "path": "10.jpg" + } + }, + { + "id": "11", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + } + ], + "image": { + "path": "11.jpg" + } + } + ] +} diff --git a/tests/assets/datumaro_multilabel_class_decremental/annotations/valid.json b/tests/assets/datumaro_multilabel_class_decremental/annotations/valid.json new file mode 100644 index 00000000000..0ebab7ae01d --- /dev/null +++ b/tests/assets/datumaro_multilabel_class_decremental/annotations/valid.json @@ -0,0 +1,238 @@ +{ + "info": {}, + "categories": { + "label": { + "label_groups": [ + { + "name": "___rectangle", + "group_type": "exclusive", + "labels": ["rectangle"] + }, + { + "name": "___circle", + "group_type": "exclusive", + "labels": ["circle"] + } + ], + "labels": [ + { + "name": "rectangle", + "parent": "", + "attributes": [] + }, + { + "name": "circle", + "parent": "", + "attributes": [] + } + ], + "attributes": [] + } + }, + "items": [ + { + "id": "00", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "00.jpg" + } + }, + { + "id": "01", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "01.jpg" + } + }, + { + "id": "02", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "02.jpg" + } + }, + { + "id": "03", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "03.jpg" + } + }, + { + "id": "04", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "04.jpg" + } + }, + { + "id": "05", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + }, + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "05.jpg" + } + }, + { + "id": "06", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "06.jpg" + } + }, + { + "id": "07", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "07.jpg" + } + }, + { + "id": "08", + "annotations": [ + { + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "08.jpg" + } + }, + { + "id": "09", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + } + ], + "image": { + "path": "09.jpg" + } + }, + { + "id": "10", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + } + ], + "image": { + "path": "10.jpg" + } + }, + { + "id": "11", + "annotations": [ + { + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 + } + ], + "image": { + "path": "11.jpg" + } + } + ] +} diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/train/00.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/train/00.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1bdbbe8b4594aa0c2762cc6c8190c909b19f8e96 GIT binary patch literal 3078 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 O|N7PDfBEkiqkjii-9Ul> literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/train/01.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/train/01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..733f6083fb771fe87269b8cd4881aae366992368 GIT binary patch literal 3118 zcmeHJSx{437QG>XkT7T@fYM+TgCx!ZK0(1K7$hiQ7}Ox6%qTJ{gCK}9${;~h5Jiwd zL5K*5AQB-!L>UZWkf0Dm0cD<}hTNBERrk-U_qtzy^iS_wYu7!s_qny!UHeoW@Eynn zRvH_U3;_rP03gB#0Q&#~Knx-ZQ5F*g3@XavFc=gjAub{D6L5s21RMdEkdTy;L?A^W ze3OEiPm%><$yw0T@br)%u+WB;?4Ba8+Nl#-)^O$qo9Y zb@Jw{X{_cczeog9L2>mOCAE#}IQ%B9UBB(t*4bllkZ5?wh-7SGX?5)Q35vCovx}>n zyN9QLz`65*LBS!HuS7-1#Ky%_Q?IApxOpo*BjUTrw0=rEZy=P>9exC<-6|g7DFtcmVgeQO?J3K>)@3(QsZMH+}cc;tTz$@ZJgj zr{s&BZri#0r{-23r|)-#nT#$xQ}7MFAb*WVnwRy*l@d_2a=JC+vW znTN*{$Jx~v6W3NC=Y6%(tToK9_U30?Omwf0#}&)y$5$2bF4%Wv@7{APCfHx;X*adM z_k)3hva@VWa@(V3uDz0B?8KnS!8k>~i889QcuHy}u9wezsM`2zz2&5gW3K|cu0Cwb164SZX5 ziaz#UxVjOPc^Cw+!WW#$J)^Cl`7GCh zz^76V`r;M_2;?csf823kS4*>(mlO$`prFknxt{U?pbu%INeb*3pMgOT(EMH*nR-)- z+cA)G@l4fVyR2vJrYS^qg(*orX(x5l5+fPT;7-XfPODWHe|n9ry#{$!HR7S8BX#FKTv_gY_j0gK zzVAsCnVmBs-bcIhBm9Wq4cAO&c03Diw&oVwYNrN?WBm0_55w&~^(*Im$f46u;&C+C zvcu)09rLZ$D6hgVZ*7{2tDK%i*%~+`aFc4X+G={31ZXYOYa5OG&E>j!3+F;&YXbHB zZi`C^_-1Y0)QB*{`0;wD8Et%jX88mwYj=Zl>&0 zy)7WIT}-#rc;bZGSkHyMMIXe`WLx!UG?@te5#+0;2bka6>!*|`L1X)$KFye9w#+)! z7`nIdIGWcU72MBsNb)@qaO~Ve3!1TI;pQe%{_}$ShE}8I4XiY!!WpVZ$(N#CbBCUV zGs5_%Kp^`xgTRCPf&gL81!!vnOs`RC5C zHQ-sW6^ouHCWX?d-w_L3=%hcX(TGuH!oPZO<$JF#Z-tH4W0+vBN+0 z6;tn4b-=WarO>g`gfrC9i)n+4lt`Py5q0$li-aNf`SP%_(`=LK@sJ7OJZEL~9U!8Q zLWGbAysD)&qaHmKU}&|~7lQIWg*MXBHyVLN5Mm7Mb&s><=Z<`oPw(a!1~D*O?}(M! zOdG+tKb)6M!rWv`P30X~lx5+$z=rGb2BY$n4dE+qzECsYx=?OoJ;!=&AAXC2%c;G` zSN^j@DynTR=HVY1Lzpd2?C(YN0W2s+vp!Nna}_OOkU(AJJgE42)vX z**@!p2Pr*XEvJ*IWQMdeH*k$k9sGr7&u|64E$tBEY&oIwaZt@Z0>Vu0K);+ZY01P^ zI9x{qqR{01X#Rmm27}A1rmQpZC*76y)7I@DhfAftO^wiN4IQz&Gc(ZSt)w0~u4axU zs|f(CbYxs6zaX)}LD_2r)^n?;&qttO=~I$hnf2-Evf1Y;#ksGPyW!IEia~>HGrnIL z)h>=0l3)BjCw_B30gJ@tMEn(g(^VWYIbOHtL8sQ@fE?GRPt(!We)N#`*?#V|u-o-^ zgz14q)XhWHF6oK^H_GyR<^2DvdzErVU1y|W) zmA+4de=vY(>w7e3Fx^Y_O=fCp+Sn^)2M1YAYAC{Ic$c0UhRILvc^K5i&?pGGzFEB& zw`(?ZY{qB_$qn_NDKlnI>C9N4sv$%fIn1c4otk z?uGP8^kZ*)7AhJFTv>h8cXV%-bsNn9&glF?GOek~a;a>mb3+4H1u+wu;-#&2b9`a$p8QV literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/train/02.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/train/02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ac21e9e870c22ded9790435eac5665c7d7921499 GIT binary patch literal 3009 zcmeH{c{r5q9>-)Ly-*dA@SyRBFi-yLA z00;yCAoc^WCIEea1GFFenPWd7;Qb7RK)?_f6bAb)I2R`j&IO0TIC(g^xc7rS;^pDy z-GA9%nSU7#E+EoRXTBp7HE?UVcGgQSr;)N?un~R#n&3zNu?&X>DtN*U{NE z@L_Ohcw}^JoHG4oW_E6V;p-x8b!~lPbBn&cv(E(rz(26q`QLIO*jya@X8_;l0&#?} z1x7%i$4|q4(Zj-B{Q1R|A~})gp5~U5xg?Y=r~+35`nd(sDilfDKH7J({|@Z_|3&ss zuzzul1H51m`|!XB00S^)>BiJ#idiAGxaDv{KDE1LL`X#oPDlxxNu;=R12T?De>!wRDp#VpZ9F;uBrO+Mi^eY&Cx!D@cY^v=490hjpmJ%e(7 zT`vgt_1;}Iz~6H6+Zz>Iy_qe}0&2{eCt?~zNyB&@qII>2g%x^YGchEh0=&h?BR|~g z$F#LdPTqVyRK3@#741d$AUmRILCrcsW*&qyV~S3*aT>ziqF?;fg>MAG%J5oL?MaOh z-PVCh>sKeA-BS7ut>}joaXvjSyv#L^(w!~xOgpUlz|Jw<>lK;%&jghP_=ZlWc2a=% zg0jOWXQzrH5{4}Hp&m>bGLwDO{sm2mHR~Fl`CO*fmTGQbL@?WW9?_$(nJyr!$j`E%qy_=lnTOK zQCz^>mF&iZ`Q$9RPxI{_Ayl8G$AIO)Ma+tSV*!tGErYmbYwDI{nYERqMd5^R=mxuQ_HoK`CU(S_Pf1?rJqZXXBkoH{c zN=v3j6y^PqW*3oyLPg)Oo=*j(rRnZ{TYL{1M5_Dn!k+3i4;>s^rE61_Okz6E8n0Wx zG{HF*=B5S$G=U`(gq@Y-+Yn4&CQ&gr#l6zc68F|jPTs_^8@D(~dg~L7#M+A9!x|ug zv8k9jq3So$ZV3kR=$kKyvZ!NGgSg&F^}Y`+cNH4@^Q>&9Gc&AEPs(5MMoDKzJM+~p zhK=;ynsKV+lp zIF^u?<&aKpMCy}5FDj_cU904*}(9zi~0s1s1E6Sw5OCSuCvLhMSZG2hwk zT_L2aB@Qco-7Z364X%bo?zfVo4sA8momOzVO{v15SiGa8DaB{S z@6JI^%sKgylfDX-OBz)?ooDnRrfAar;1WV|WakvFx8cNySZg-=kKwV5veuclKhCVvklZ!8c^9GTn4nco1uF=NY4m zZ3Fn3yy(hpHG4cG{I=zf-Jq8Oo0(|BlZ*3ob0BI_ix9QaAVL#Ooi3BpalKIcnV;Gf zdVQkq)$#>8;)xz9%j@q?OBd@7EG{0_%~h!HPRy2SH`F5^MY$b3sMmH|IQv&~v2ShT z-lPbpHH_hu?;(FYdQ!CX$|dcrZC?+ep?2m@+n8F}aLtZx{Mu2z&8Q6Z#MV0Q)DwZK zj}BS+H;vBB%r{WTU*Boc778Q!Y!jcy701oAgkb(GnM^;p(4MpB<-=FHng~Y9#v95M9_8lJWG< Un)t8zpa1B8?a_}t5@U`118sGi%K!iX literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/train/03.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/train/03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..72c7fdccb7a0f148fff2a3a1188800a90bb3bc76 GIT binary patch literal 3082 zcmeH`do+~$8pq!;S8B&-#-`Ym5lYE5icz_Royk2^3Pri5$bE9zgkeJ1ZlhdMxlE37 zE0jwHxiuJ}G~+U&Vq8bzJu|h|IqRHt&f5E&zxVU5@3Y?DdY|8KeLwH-dB7p?10Z5* zWMTwBAOHa2JpecY=mP=}e(2v>g+K}-5eU>4l%NnlcyC*` z2yNvr_=o)1DgcK<;X+6R@_Whul)x7N8U=g+K0qP60Rc1wiiUtKfHco1{JTA#*&hKB zfWqJiB+tecUcpllo_#2kXBy6P%&QLLeFtD@xacmGV~Fh*7m&NJVpJoN@1bOmSG>ep z_RZteoc$sNg~Waqm)IeKG zXzd+u==}qOjG?!~OxF0s

    }Q;|ynEacOyFmAm$p&jkUXKd^ZDKXRdYTmt+%K=QdD z0ylVqqG9k|DhSbI7RU=%x9?VsKw*w2->Y~jD63{Uk9GFz6B5IzGvzpZwC`kp4lMHj zBKrr}Ke>j1txyQ>@}Ouy4_G(d$OQp6BG-vHBF#BXbOiy|jl9hZo5bv?wZ^4B&GF9F z?hxmC0D-xaSe1_N&*R= z^V~N(``J&v8D~^>Eljw+tCsf;U`uHASUDAFtZ_I6n`S>J=zY4>2m)_nsVn0@iGC2n>Nd6*pD0QH^(D_;W z?8VipC6oGX`ug*EaXKfM*H7*U`Ax~L%ev*l&31S9UHTk~Vhq0en?u`$-uNRj+og$~v2Rl2=rb6k4!Q>WdRC0kvQdg-a&|$bO58JPa7^>@1mi95^gKb7pDm(G2-TO-78GxmeqVqlDBl zDWh5Lov!8#rKInEheCc`4@F2J|K@)aJ{<%iZ-BsSCJ6X~z(y^#sAMs_xj~&nj@Fsu z8m!wt{77^kSs{O#U6WZ1V0(X#_I@=^T54Y|Q|6wP7H^y`4a^1sWVQ7A+3>4*zDE1B zx*j<*9aJo<67k7qOL}`)KHDm4SAy)M%%3|KcNUn>^pS}AOTRSY@#@5OXO7zji2(v% zPc0Ffrn#*kVCd2^#?`F>0oH6@ef{%(>Yjf8oweQv4#-`Wrvz7mlA|y z6g7?Rzr|cPG)>#9cn$Z2(3m;v>jY=jIALwe93F*o%nSxf=lX}aUQ8S(BVLwXoNSA- z>#4KtKBsXJj!7RX`2i41-OEA-zO@X8IRyIgc!j$H&)voe z(W%XwxXq!ec`3j~1I%wL=5m!GHtIm{($9s@nn9ssQ_nc=e zPW`H|*M$s=B*=VGFwzyq*VlS!+qBW+zvR6uSw}BQdXS1Dm`Yv_*&*7-Y>jp)5U9rm zVKEj?1$nK<3^Au5e;|17PP^y5{*GZTgs@7-4k^_WNGasFM`-5;aDBxqQDYU z^69=Whg#NEDLXX3lpaBE!dyi>*bW2p8KmV#_}J5x#J$P8KW7OZQ=uJk70{NT$wVZ6{i;F>nR=KBa9%~d>;t4i0)d>riHHQ2#c4{oW1pL-sufl^@D$b3f zsttYIzcr`$LVSg&4dzGL$eWiEg@)q%=m|bFx=NSdXns^#V%d(X410B|cW1zQ+Q@X` z2EpoaU#Z-HhFMVz%IAMKAzYwm<`2p@RZHoQ6Ndm0ind z@y3%e{Yk>Gtmc>R=Zv)-7#{g{0glUIqPz)O>FNAFLHsHD6G9Sn7;3s*ue)$5DwlBq la+z`MILiHOLyV{9XzqI^uQg=&4f&V;`Un5k15oho-vL@_K)3(^ literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/train/04.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/train/04.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fd251b0cff309b20617e765eaf9f05629f0c0acf GIT binary patch literal 2886 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 B8x#Nl literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/train/06.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/train/06.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c08c7dea829b232015e3ea8c0dea2fbfc10c4c4d GIT binary patch literal 2697 zcmd6oXHZk=8pls^Qb<4{kRVM#0l~Hmp$Leuz*0ppAWfEzAkt9~Sc;&6M5H8iT#(*n zDZ+w^G^J*_^rA=zNE0PA30aohL}%{(a%bFozj&WB|7YGa=Q;2D|DET|Jgf;872q?{ zGt>hh5CDL%FM#z1&;g(jPH-}m6EKjI;V>8oL%AqT)e2y^04hKQA;$nH1_ELrtWH3j?GygR9^35KfIvYQ z9D!up;AR&**}92H2&Vy+}_do>UCFlPw&w1 z$mrPk#N^a0ZEpVk!s62M%KFCU*7oNe#x9e?1p&aXSnT{CxiD-lD5nQV4i^M^i!BfX zgCCVg@M{r}PW}SN@KIb??c}_gHk71-86D>wFo+hEQk<1u=b(Kd`+H!~{}b82!2ZoO z1@M3ncJn|Cpb6|pF69#e+3%`m)7Z#^YkU5YFZy&!+9#TtKvL+c@wV?jK=OoGg=JA} z;mYM8gS9?DKaGc;`Ggn<^@;aW9ib)w z#bdnRS%nlfmIwD)+e@w5soU|&Z8Mhp&5RElwK7Z(Glr#ANUzUc)^*%aWQgBdTQZo62 zgPys2v}lI?)B=vAimgm;j;<_pjOHcSn9+=l($^%iHkw(0!ZUGd@dYz7Gv|1r^k1KB zx`ukR_CD>qQxvU=!+vDQc*C<-_R&PU!Ob6Xer|c+$^tA+rRXL@CO5i%EZLR8)i zgKT~0r(vA&IFI(4mn^_kgHj(ki(N{*r(wA)YHzVn17`emt z2VE6^%M`0KlzV6CEi;7)s{I&p!r?_haaIaJH*Eb+K7uY5uu2Z|qq{6$X+P)8-Vf-#`#;==SJSNCyGkQR}6u#W1jEo>AM4f>1LI}z(6&mOsZ+e#xobgNqb z9VB0H3pmDYw${Md~)4;pW7jwrc9(8V!@_q?zKUEo^^y|wzu zX(RBe^@M@Qf&QPpPsO%^Vn>=(RlBJ!xklB;7|18miE%$D8ro>jv}J4*){xYbAYzdE zCeJ3IW)ucvS-HRoXE|6=Yo6AEw-*ut+t&urpVwEU)t;6v&5sSZ3A}GJwIPd9Z7n% zAzGqS7Xn*Pp2?fxMqTb~KULw7IlnkjZhe|8U|&)1V&cmJq{DYwdICd_9AcleTte^W zeC=-tqLEoPvoyYbJNV%yd^Ajo~t_NluKKL;%Jya`9mF3?{y70rA}G!>i*puF&{snklmEpnd%|4*sjCR z_g?nxqnL8BkGvSB;wOV2JU?%H!*=y){m`CNT9xfBY~o|tC%)~P*f2R-xAudgy5_f~ z-abVZZf;^Q)!o+tDvKjL@ms0%?NW=50aBsYz)Jix#lFoje5d0|c~ZFf9AJyVaG#BQB;4@XGnx{W5*XSJUCc@NQq{OFsG}MSS)7 z6RV<|i|4JBT6#_1t%Np?7J1|5*}vzOJ1tKadkOM)WL3FGpxLQSdTp@)jDr^1-SyIR zbNT!{6^!aRUOH$lt9^aQN;&U-r&9?<&qvO)c(}29G`q`Q@z;)*#BDz9^1bwg0sv4! zR*U5nZUkI(GgTi~I8W)PNRi|{q=KX~<9eiWinDcP%}PvXiT3x0F$u3}EFjU+{KJ~z YiwH0Fu&GP=z7ywrPrU#C^I4Ps1QXs*egFUf literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/train/07.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/train/07.jpg new file mode 100644 index 0000000000000000000000000000000000000000..860bc522fec770beb693bb3d5a1d23afcc13e6d2 GIT binary patch literal 2689 zcmeH`cTiL58pcmZhX@7<0*eHtSE);)5=0Gk5kp@@x=2~Y0HFxT0ud!yDpF6wv?(AQ8?tA8W<~wuV@0;g$-uZS$cBX*c z7AEE<00aU65byzZ#)0F20EAzB7vL8R%6B*n28AKu2*ej5(I^BGjYJ?&LMXH#zrYnn zND#w+;UDsKs{kAdg$p7P$S*DbmF+YGSQIb?OhF+B00ArniiPa719&he{7XDA?AL+_ zKw)qM5{w}PHdO5f<3pifXgHV{?2ZP%128OHbicMSVh_awd4MXW6PuEck~>z`g0t!8 z%IkUt#i0eo_ew}gDJUu_tElSfe``P>9y)%4Y+`C=ZeeR@@8Ib4y|b71S)X&he*VEB zw9v5dh)ePGD+yPxB{EXe(r;vBX5GwY75sANZeh{A;_`~hs_L5By870(_KwcSUENP! z3=9sv93B}Ro1S6MzJ4>unO|61URnLPw!X2s#pi+m(63ma|649B$R)s^0g}%J5eNeX z#lqnGwGpDm6r=}r&jFoSl-RM9{IV9boURQQ=NZ&5C@!xzt+2#L`$G1gfyMnLvVVg8 zi)#$PKq27ppjf~N;KguQ3_$ts&;vKjhFW{BW|k>y{^?S}xI)i%nUWmQYvKYDs*&{L z@GrhGz_x+s%$Yz+?&o;?WYMyh8WT9GP%c8m9HzdVn^`#`vuiA9xO`{_Xx_|oYugrT zzio;4yK=`rrq`ecpZR^na$#WYvZ~so+=eOly6! z5w_`yj$V{eA&0tZY-r3_i?Aj*udB>LdE6G^QEUbChi-MEG;@^*U~<20V3HIQRHaw% z&^|{$ezVzVe#@JXSHkAh2hB(4W&1B!J~Jr2bw!MFI`ajV9aVJ!Un%kU9jgNJG&CP7f7!!%tP!eAfx;{c*7rkqKaa0*iVjO*Z zGwGcgbHA>q$aU<5Y^r1=iT=uYir1Jb^LukKYpjXUy1L}GK zsLBS-A&)rRxU^rN$UA?uI2Da~2vQ;kXf=6UPmV?=>O}WW}V0lU$ z%8*QRU!q;44Vf9a>5p&;X;*(eVtKVS(;9D+eR`bQFuLzntk2KYLg%e$9#GAwoX{0Qu`iq>$`9e&Ygz3Zt;@OhnXX@(RL z8|z@BgOV$Hcw@$U2cYc$ftRm-c!hX^Uwr%UedC8$A-tO7MJ&dq?fMjpp$ve6b~X7* zD7uL|yHu*nMHB74RYNmGiL!V~?YRZR0D9@~>{)IP+sun`*5~?3a*BPd^7zEmmh|q+ zbVsZk>#X_x#+*<1PM2t9OJ-TA(R1D>HJ;H$3K_@2%ZgFRz#j>>7=zfR2scbCR}SCI zerrV$n=Sx;=w9ZU!p=*4d~$nDsYA`NC0C<~>sp)rj2R%S>Xo~9Ufw&-(Wo^XhNnz; z;|=s%q9X;__(cDvo;jZB72z*?4b5XDnJXt7?^v?3*=je-R`5xFCrvt#ph*v)q^8TKSslKc`%H_) zP_>FZ2wyZXW!28z&SX~vE zYD_r_85TrfXDl|m7qd%>OfNhv=T(!=T_@y0TkGt~?6c?micIg{Ilq@RaC6o5yi!+q zg-eWRcHN@S%PA+R&j?G$yu(d+PEMS;?bbaR zFjQisJL0vmIaOooQ!#jo_1gaD@9MfFBrgA!_8bA3BOt2tKN)kdt>0Mm1uCY6J*~l{ zD7(l62?Y-@FYC{)I%0w_b%!f|f4WuQs7cMSh$_2>YFrTH)lXg)H`qSCq|_m!Lyo7^ zIl0@nIgh<}nwTTHn@@!A3zwJ@4vr<`-r5vc(1|J~2jy0_+q571)Mj4mw4*6*bx&?x YiU6;dv$Qc;>=fkx`k(s;$eq!D011ZfQvd(} literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/train/08.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/train/08.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c08c7dea829b232015e3ea8c0dea2fbfc10c4c4d GIT binary patch literal 2697 zcmd6oXHZk=8pls^Qb<4{kRVM#0l~Hmp$Leuz*0ppAWfEzAkt9~Sc;&6M5H8iT#(*n zDZ+w^G^J*_^rA=zNE0PA30aohL}%{(a%bFozj&WB|7YGa=Q;2D|DET|Jgf;872q?{ zGt>hh5CDL%FM#z1&;g(jPH-}m6EKjI;V>8oL%AqT)e2y^04hKQA;$nH1_ELrtWH3j?GygR9^35KfIvYQ z9D!up;AR&**}92H2&Vy+}_do>UCFlPw&w1 z$mrPk#N^a0ZEpVk!s62M%KFCU*7oNe#x9e?1p&aXSnT{CxiD-lD5nQV4i^M^i!BfX zgCCVg@M{r}PW}SN@KIb??c}_gHk71-86D>wFo+hEQk<1u=b(Kd`+H!~{}b82!2ZoO z1@M3ncJn|Cpb6|pF69#e+3%`m)7Z#^YkU5YFZy&!+9#TtKvL+c@wV?jK=OoGg=JA} z;mYM8gS9?DKaGc;`Ggn<^@;aW9ib)w z#bdnRS%nlfmIwD)+e@w5soU|&Z8Mhp&5RElwK7Z(Glr#ANUzUc)^*%aWQgBdTQZo62 zgPys2v}lI?)B=vAimgm;j;<_pjOHcSn9+=l($^%iHkw(0!ZUGd@dYz7Gv|1r^k1KB zx`ukR_CD>qQxvU=!+vDQc*C<-_R&PU!Ob6Xer|c+$^tA+rRXL@CO5i%EZLR8)i zgKT~0r(vA&IFI(4mn^_kgHj(ki(N{*r(wA)YHzVn17`emt z2VE6^%M`0KlzV6CEi;7)s{I&p!r?_haaIaJH*Eb+K7uY5uu2Z|qq{6$X+P)8-Vf-#`#;==SJSNCyGkQR}6u#W1jEo>AM4f>1LI}z(6&mOsZ+e#xobgNqb z9VB0H3pmDYw${Md~)4;pW7jwrc9(8V!@_q?zKUEo^^y|wzu zX(RBe^@M@Qf&QPpPsO%^Vn>=(RlBJ!xklB;7|18miE%$D8ro>jv}J4*){xYbAYzdE zCeJ3IW)ucvS-HRoXE|6=Yo6AEw-*ut+t&urpVwEU)t;6v&5sSZ3A}GJwIPd9Z7n% zAzGqS7Xn*Pp2?fxMqTb~KULw7IlnkjZhe|8U|&)1V&cmJq{DYwdICd_9AcleTte^W zeC=-tqLEoPvoyYbJNV%yd^Ajo~t_NluKKL;%Jya`9mF3?{y70rA}G!>i*puF&{snklmEpnd%|4*sjCR z_g?nxqnL8BkGvSB;wOV2JU?%H!*=y){m`CNT9xfBY~o|tC%)~P*f2R-xAudgy5_f~ z-abVZZf;^Q)!o+tDvKjL@ms0%?NW=50aBsYz)Jix#lFoje5d0|c~ZFf9AJyVaG#BQB;4@XGnx{W5*XSJUCc@NQq{OFsG}MSS)7 z6RV<|i|4JBT6#_1t%Np?7J1|5*}vzOJ1tKadkOM)WL3FGpxLQSdTp@)jDr^1-SyIR zbNT!{6^!aRUOH$lt9^aQN;&U-r&9?<&qvO)c(}29G`q`Q@z;)*#BDz9^1bwg0sv4! zR*U5nZUkI(GgTi~I8W)PNRi|{q=KX~<9eiWinDcP%}PvXiT3x0F$u3}EFjU+{KJ~z YiwH0Fu&GP=z7ywrPrU#C^I4Ps1QXs*egFUf literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/train/09.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/train/09.jpg new file mode 100644 index 0000000000000000000000000000000000000000..848f6ad7da02a83bacb009df18bed0571fa398fd GIT binary patch literal 1872 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 C2PN+S literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/valid/00.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/valid/00.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1bdbbe8b4594aa0c2762cc6c8190c909b19f8e96 GIT binary patch literal 3078 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 O|N7PDfBEkiqkjii-9Ul> literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/valid/01.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/valid/01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..733f6083fb771fe87269b8cd4881aae366992368 GIT binary patch literal 3118 zcmeHJSx{437QG>XkT7T@fYM+TgCx!ZK0(1K7$hiQ7}Ox6%qTJ{gCK}9${;~h5Jiwd zL5K*5AQB-!L>UZWkf0Dm0cD<}hTNBERrk-U_qtzy^iS_wYu7!s_qny!UHeoW@Eynn zRvH_U3;_rP03gB#0Q&#~Knx-ZQ5F*g3@XavFc=gjAub{D6L5s21RMdEkdTy;L?A^W ze3OEiPm%><$yw0T@br)%u+WB;?4Ba8+Nl#-)^O$qo9Y zb@Jw{X{_cczeog9L2>mOCAE#}IQ%B9UBB(t*4bllkZ5?wh-7SGX?5)Q35vCovx}>n zyN9QLz`65*LBS!HuS7-1#Ky%_Q?IApxOpo*BjUTrw0=rEZy=P>9exC<-6|g7DFtcmVgeQO?J3K>)@3(QsZMH+}cc;tTz$@ZJgj zr{s&BZri#0r{-23r|)-#nT#$xQ}7MFAb*WVnwRy*l@d_2a=JC+vW znTN*{$Jx~v6W3NC=Y6%(tToK9_U30?Omwf0#}&)y$5$2bF4%Wv@7{APCfHx;X*adM z_k)3hva@VWa@(V3uDz0B?8KnS!8k>~i889QcuHy}u9wezsM`2zz2&5gW3K|cu0Cwb164SZX5 ziaz#UxVjOPc^Cw+!WW#$J)^Cl`7GCh zz^76V`r;M_2;?csf823kS4*>(mlO$`prFknxt{U?pbu%INeb*3pMgOT(EMH*nR-)- z+cA)G@l4fVyR2vJrYS^qg(*orX(x5l5+fPT;7-XfPODWHe|n9ry#{$!HR7S8BX#FKTv_gY_j0gK zzVAsCnVmBs-bcIhBm9Wq4cAO&c03Diw&oVwYNrN?WBm0_55w&~^(*Im$f46u;&C+C zvcu)09rLZ$D6hgVZ*7{2tDK%i*%~+`aFc4X+G={31ZXYOYa5OG&E>j!3+F;&YXbHB zZi`C^_-1Y0)QB*{`0;wD8Et%jX88mwYj=Zl>&0 zy)7WIT}-#rc;bZGSkHyMMIXe`WLx!UG?@te5#+0;2bka6>!*|`L1X)$KFye9w#+)! z7`nIdIGWcU72MBsNb)@qaO~Ve3!1TI;pQe%{_}$ShE}8I4XiY!!WpVZ$(N#CbBCUV zGs5_%Kp^`xgTRCPf&gL81!!vnOs`RC5C zHQ-sW6^ouHCWX?d-w_L3=%hcX(TGuH!oPZO<$JF#Z-tH4W0+vBN+0 z6;tn4b-=WarO>g`gfrC9i)n+4lt`Py5q0$li-aNf`SP%_(`=LK@sJ7OJZEL~9U!8Q zLWGbAysD)&qaHmKU}&|~7lQIWg*MXBHyVLN5Mm7Mb&s><=Z<`oPw(a!1~D*O?}(M! zOdG+tKb)6M!rWv`P30X~lx5+$z=rGb2BY$n4dE+qzECsYx=?OoJ;!=&AAXC2%c;G` zSN^j@DynTR=HVY1Lzpd2?C(YN0W2s+vp!Nna}_OOkU(AJJgE42)vX z**@!p2Pr*XEvJ*IWQMdeH*k$k9sGr7&u|64E$tBEY&oIwaZt@Z0>Vu0K);+ZY01P^ zI9x{qqR{01X#Rmm27}A1rmQpZC*76y)7I@DhfAftO^wiN4IQz&Gc(ZSt)w0~u4axU zs|f(CbYxs6zaX)}LD_2r)^n?;&qttO=~I$hnf2-Evf1Y;#ksGPyW!IEia~>HGrnIL z)h>=0l3)BjCw_B30gJ@tMEn(g(^VWYIbOHtL8sQ@fE?GRPt(!We)N#`*?#V|u-o-^ zgz14q)XhWHF6oK^H_GyR<^2DvdzErVU1y|W) zmA+4de=vY(>w7e3Fx^Y_O=fCp+Sn^)2M1YAYAC{Ic$c0UhRILvc^K5i&?pGGzFEB& zw`(?ZY{qB_$qn_NDKlnI>C9N4sv$%fIn1c4otk z?uGP8^kZ*)7AhJFTv>h8cXV%-bsNn9&glF?GOek~a;a>mb3+4H1u+wu;-#&2b9`a$p8QV literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/valid/02.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/valid/02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ac21e9e870c22ded9790435eac5665c7d7921499 GIT binary patch literal 3009 zcmeH{c{r5q9>-)Ly-*dA@SyRBFi-yLA z00;yCAoc^WCIEea1GFFenPWd7;Qb7RK)?_f6bAb)I2R`j&IO0TIC(g^xc7rS;^pDy z-GA9%nSU7#E+EoRXTBp7HE?UVcGgQSr;)N?un~R#n&3zNu?&X>DtN*U{NE z@L_Ohcw}^JoHG4oW_E6V;p-x8b!~lPbBn&cv(E(rz(26q`QLIO*jya@X8_;l0&#?} z1x7%i$4|q4(Zj-B{Q1R|A~})gp5~U5xg?Y=r~+35`nd(sDilfDKH7J({|@Z_|3&ss zuzzul1H51m`|!XB00S^)>BiJ#idiAGxaDv{KDE1LL`X#oPDlxxNu;=R12T?De>!wRDp#VpZ9F;uBrO+Mi^eY&Cx!D@cY^v=490hjpmJ%e(7 zT`vgt_1;}Iz~6H6+Zz>Iy_qe}0&2{eCt?~zNyB&@qII>2g%x^YGchEh0=&h?BR|~g z$F#LdPTqVyRK3@#741d$AUmRILCrcsW*&qyV~S3*aT>ziqF?;fg>MAG%J5oL?MaOh z-PVCh>sKeA-BS7ut>}joaXvjSyv#L^(w!~xOgpUlz|Jw<>lK;%&jghP_=ZlWc2a=% zg0jOWXQzrH5{4}Hp&m>bGLwDO{sm2mHR~Fl`CO*fmTGQbL@?WW9?_$(nJyr!$j`E%qy_=lnTOK zQCz^>mF&iZ`Q$9RPxI{_Ayl8G$AIO)Ma+tSV*!tGErYmbYwDI{nYERqMd5^R=mxuQ_HoK`CU(S_Pf1?rJqZXXBkoH{c zN=v3j6y^PqW*3oyLPg)Oo=*j(rRnZ{TYL{1M5_Dn!k+3i4;>s^rE61_Okz6E8n0Wx zG{HF*=B5S$G=U`(gq@Y-+Yn4&CQ&gr#l6zc68F|jPTs_^8@D(~dg~L7#M+A9!x|ug zv8k9jq3So$ZV3kR=$kKyvZ!NGgSg&F^}Y`+cNH4@^Q>&9Gc&AEPs(5MMoDKzJM+~p zhK=;ynsKV+lp zIF^u?<&aKpMCy}5FDj_cU904*}(9zi~0s1s1E6Sw5OCSuCvLhMSZG2hwk zT_L2aB@Qco-7Z364X%bo?zfVo4sA8momOzVO{v15SiGa8DaB{S z@6JI^%sKgylfDX-OBz)?ooDnRrfAar;1WV|WakvFx8cNySZg-=kKwV5veuclKhCVvklZ!8c^9GTn4nco1uF=NY4m zZ3Fn3yy(hpHG4cG{I=zf-Jq8Oo0(|BlZ*3ob0BI_ix9QaAVL#Ooi3BpalKIcnV;Gf zdVQkq)$#>8;)xz9%j@q?OBd@7EG{0_%~h!HPRy2SH`F5^MY$b3sMmH|IQv&~v2ShT z-lPbpHH_hu?;(FYdQ!CX$|dcrZC?+ep?2m@+n8F}aLtZx{Mu2z&8Q6Z#MV0Q)DwZK zj}BS+H;vBB%r{WTU*Boc778Q!Y!jcy701oAgkb(GnM^;p(4MpB<-=FHng~Y9#v95M9_8lJWG< Un)t8zpa1B8?a_}t5@U`118sGi%K!iX literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/valid/03.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/valid/03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..72c7fdccb7a0f148fff2a3a1188800a90bb3bc76 GIT binary patch literal 3082 zcmeH`do+~$8pq!;S8B&-#-`Ym5lYE5icz_Royk2^3Pri5$bE9zgkeJ1ZlhdMxlE37 zE0jwHxiuJ}G~+U&Vq8bzJu|h|IqRHt&f5E&zxVU5@3Y?DdY|8KeLwH-dB7p?10Z5* zWMTwBAOHa2JpecY=mP=}e(2v>g+K}-5eU>4l%NnlcyC*` z2yNvr_=o)1DgcK<;X+6R@_Whul)x7N8U=g+K0qP60Rc1wiiUtKfHco1{JTA#*&hKB zfWqJiB+tecUcpllo_#2kXBy6P%&QLLeFtD@xacmGV~Fh*7m&NJVpJoN@1bOmSG>ep z_RZteoc$sNg~Waqm)IeKG zXzd+u==}qOjG?!~OxF0s

    }Q;|ynEacOyFmAm$p&jkUXKd^ZDKXRdYTmt+%K=QdD z0ylVqqG9k|DhSbI7RU=%x9?VsKw*w2->Y~jD63{Uk9GFz6B5IzGvzpZwC`kp4lMHj zBKrr}Ke>j1txyQ>@}Ouy4_G(d$OQp6BG-vHBF#BXbOiy|jl9hZo5bv?wZ^4B&GF9F z?hxmC0D-xaSe1_N&*R= z^V~N(``J&v8D~^>Eljw+tCsf;U`uHASUDAFtZ_I6n`S>J=zY4>2m)_nsVn0@iGC2n>Nd6*pD0QH^(D_;W z?8VipC6oGX`ug*EaXKfM*H7*U`Ax~L%ev*l&31S9UHTk~Vhq0en?u`$-uNRj+og$~v2Rl2=rb6k4!Q>WdRC0kvQdg-a&|$bO58JPa7^>@1mi95^gKb7pDm(G2-TO-78GxmeqVqlDBl zDWh5Lov!8#rKInEheCc`4@F2J|K@)aJ{<%iZ-BsSCJ6X~z(y^#sAMs_xj~&nj@Fsu z8m!wt{77^kSs{O#U6WZ1V0(X#_I@=^T54Y|Q|6wP7H^y`4a^1sWVQ7A+3>4*zDE1B zx*j<*9aJo<67k7qOL}`)KHDm4SAy)M%%3|KcNUn>^pS}AOTRSY@#@5OXO7zji2(v% zPc0Ffrn#*kVCd2^#?`F>0oH6@ef{%(>Yjf8oweQv4#-`Wrvz7mlA|y z6g7?Rzr|cPG)>#9cn$Z2(3m;v>jY=jIALwe93F*o%nSxf=lX}aUQ8S(BVLwXoNSA- z>#4KtKBsXJj!7RX`2i41-OEA-zO@X8IRyIgc!j$H&)voe z(W%XwxXq!ec`3j~1I%wL=5m!GHtIm{($9s@nn9ssQ_nc=e zPW`H|*M$s=B*=VGFwzyq*VlS!+qBW+zvR6uSw}BQdXS1Dm`Yv_*&*7-Y>jp)5U9rm zVKEj?1$nK<3^Au5e;|17PP^y5{*GZTgs@7-4k^_WNGasFM`-5;aDBxqQDYU z^69=Whg#NEDLXX3lpaBE!dyi>*bW2p8KmV#_}J5x#J$P8KW7OZQ=uJk70{NT$wVZ6{i;F>nR=KBa9%~d>;t4i0)d>riHHQ2#c4{oW1pL-sufl^@D$b3f zsttYIzcr`$LVSg&4dzGL$eWiEg@)q%=m|bFx=NSdXns^#V%d(X410B|cW1zQ+Q@X` z2EpoaU#Z-HhFMVz%IAMKAzYwm<`2p@RZHoQ6Ndm0ind z@y3%e{Yk>Gtmc>R=Zv)-7#{g{0glUIqPz)O>FNAFLHsHD6G9Sn7;3s*ue)$5DwlBq la+z`MILiHOLyV{9XzqI^uQg=&4f&V;`Un5k15oho-vL@_K)3(^ literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/valid/04.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/valid/04.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fd251b0cff309b20617e765eaf9f05629f0c0acf GIT binary patch literal 2886 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 B8x#Nl literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/valid/06.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/valid/06.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c08c7dea829b232015e3ea8c0dea2fbfc10c4c4d GIT binary patch literal 2697 zcmd6oXHZk=8pls^Qb<4{kRVM#0l~Hmp$Leuz*0ppAWfEzAkt9~Sc;&6M5H8iT#(*n zDZ+w^G^J*_^rA=zNE0PA30aohL}%{(a%bFozj&WB|7YGa=Q;2D|DET|Jgf;872q?{ zGt>hh5CDL%FM#z1&;g(jPH-}m6EKjI;V>8oL%AqT)e2y^04hKQA;$nH1_ELrtWH3j?GygR9^35KfIvYQ z9D!up;AR&**}92H2&Vy+}_do>UCFlPw&w1 z$mrPk#N^a0ZEpVk!s62M%KFCU*7oNe#x9e?1p&aXSnT{CxiD-lD5nQV4i^M^i!BfX zgCCVg@M{r}PW}SN@KIb??c}_gHk71-86D>wFo+hEQk<1u=b(Kd`+H!~{}b82!2ZoO z1@M3ncJn|Cpb6|pF69#e+3%`m)7Z#^YkU5YFZy&!+9#TtKvL+c@wV?jK=OoGg=JA} z;mYM8gS9?DKaGc;`Ggn<^@;aW9ib)w z#bdnRS%nlfmIwD)+e@w5soU|&Z8Mhp&5RElwK7Z(Glr#ANUzUc)^*%aWQgBdTQZo62 zgPys2v}lI?)B=vAimgm;j;<_pjOHcSn9+=l($^%iHkw(0!ZUGd@dYz7Gv|1r^k1KB zx`ukR_CD>qQxvU=!+vDQc*C<-_R&PU!Ob6Xer|c+$^tA+rRXL@CO5i%EZLR8)i zgKT~0r(vA&IFI(4mn^_kgHj(ki(N{*r(wA)YHzVn17`emt z2VE6^%M`0KlzV6CEi;7)s{I&p!r?_haaIaJH*Eb+K7uY5uu2Z|qq{6$X+P)8-Vf-#`#;==SJSNCyGkQR}6u#W1jEo>AM4f>1LI}z(6&mOsZ+e#xobgNqb z9VB0H3pmDYw${Md~)4;pW7jwrc9(8V!@_q?zKUEo^^y|wzu zX(RBe^@M@Qf&QPpPsO%^Vn>=(RlBJ!xklB;7|18miE%$D8ro>jv}J4*){xYbAYzdE zCeJ3IW)ucvS-HRoXE|6=Yo6AEw-*ut+t&urpVwEU)t;6v&5sSZ3A}GJwIPd9Z7n% zAzGqS7Xn*Pp2?fxMqTb~KULw7IlnkjZhe|8U|&)1V&cmJq{DYwdICd_9AcleTte^W zeC=-tqLEoPvoyYbJNV%yd^Ajo~t_NluKKL;%Jya`9mF3?{y70rA}G!>i*puF&{snklmEpnd%|4*sjCR z_g?nxqnL8BkGvSB;wOV2JU?%H!*=y){m`CNT9xfBY~o|tC%)~P*f2R-xAudgy5_f~ z-abVZZf;^Q)!o+tDvKjL@ms0%?NW=50aBsYz)Jix#lFoje5d0|c~ZFf9AJyVaG#BQB;4@XGnx{W5*XSJUCc@NQq{OFsG}MSS)7 z6RV<|i|4JBT6#_1t%Np?7J1|5*}vzOJ1tKadkOM)WL3FGpxLQSdTp@)jDr^1-SyIR zbNT!{6^!aRUOH$lt9^aQN;&U-r&9?<&qvO)c(}29G`q`Q@z;)*#BDz9^1bwg0sv4! zR*U5nZUkI(GgTi~I8W)PNRi|{q=KX~<9eiWinDcP%}PvXiT3x0F$u3}EFjU+{KJ~z YiwH0Fu&GP=z7ywrPrU#C^I4Ps1QXs*egFUf literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/valid/07.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/valid/07.jpg new file mode 100644 index 0000000000000000000000000000000000000000..860bc522fec770beb693bb3d5a1d23afcc13e6d2 GIT binary patch literal 2689 zcmeH`cTiL58pcmZhX@7<0*eHtSE);)5=0Gk5kp@@x=2~Y0HFxT0ud!yDpF6wv?(AQ8?tA8W<~wuV@0;g$-uZS$cBX*c z7AEE<00aU65byzZ#)0F20EAzB7vL8R%6B*n28AKu2*ej5(I^BGjYJ?&LMXH#zrYnn zND#w+;UDsKs{kAdg$p7P$S*DbmF+YGSQIb?OhF+B00ArniiPa719&he{7XDA?AL+_ zKw)qM5{w}PHdO5f<3pifXgHV{?2ZP%128OHbicMSVh_awd4MXW6PuEck~>z`g0t!8 z%IkUt#i0eo_ew}gDJUu_tElSfe``P>9y)%4Y+`C=ZeeR@@8Ib4y|b71S)X&he*VEB zw9v5dh)ePGD+yPxB{EXe(r;vBX5GwY75sANZeh{A;_`~hs_L5By870(_KwcSUENP! z3=9sv93B}Ro1S6MzJ4>unO|61URnLPw!X2s#pi+m(63ma|649B$R)s^0g}%J5eNeX z#lqnGwGpDm6r=}r&jFoSl-RM9{IV9boURQQ=NZ&5C@!xzt+2#L`$G1gfyMnLvVVg8 zi)#$PKq27ppjf~N;KguQ3_$ts&;vKjhFW{BW|k>y{^?S}xI)i%nUWmQYvKYDs*&{L z@GrhGz_x+s%$Yz+?&o;?WYMyh8WT9GP%c8m9HzdVn^`#`vuiA9xO`{_Xx_|oYugrT zzio;4yK=`rrq`ecpZR^na$#WYvZ~so+=eOly6! z5w_`yj$V{eA&0tZY-r3_i?Aj*udB>LdE6G^QEUbChi-MEG;@^*U~<20V3HIQRHaw% z&^|{$ezVzVe#@JXSHkAh2hB(4W&1B!J~Jr2bw!MFI`ajV9aVJ!Un%kU9jgNJG&CP7f7!!%tP!eAfx;{c*7rkqKaa0*iVjO*Z zGwGcgbHA>q$aU<5Y^r1=iT=uYir1Jb^LukKYpjXUy1L}GK zsLBS-A&)rRxU^rN$UA?uI2Da~2vQ;kXf=6UPmV?=>O}WW}V0lU$ z%8*QRU!q;44Vf9a>5p&;X;*(eVtKVS(;9D+eR`bQFuLzntk2KYLg%e$9#GAwoX{0Qu`iq>$`9e&Ygz3Zt;@OhnXX@(RL z8|z@BgOV$Hcw@$U2cYc$ftRm-c!hX^Uwr%UedC8$A-tO7MJ&dq?fMjpp$ve6b~X7* zD7uL|yHu*nMHB74RYNmGiL!V~?YRZR0D9@~>{)IP+sun`*5~?3a*BPd^7zEmmh|q+ zbVsZk>#X_x#+*<1PM2t9OJ-TA(R1D>HJ;H$3K_@2%ZgFRz#j>>7=zfR2scbCR}SCI zerrV$n=Sx;=w9ZU!p=*4d~$nDsYA`NC0C<~>sp)rj2R%S>Xo~9Ufw&-(Wo^XhNnz; z;|=s%q9X;__(cDvo;jZB72z*?4b5XDnJXt7?^v?3*=je-R`5xFCrvt#ph*v)q^8TKSslKc`%H_) zP_>FZ2wyZXW!28z&SX~vE zYD_r_85TrfXDl|m7qd%>OfNhv=T(!=T_@y0TkGt~?6c?micIg{Ilq@RaC6o5yi!+q zg-eWRcHN@S%PA+R&j?G$yu(d+PEMS;?bbaR zFjQisJL0vmIaOooQ!#jo_1gaD@9MfFBrgA!_8bA3BOt2tKN)kdt>0Mm1uCY6J*~l{ zD7(l62?Y-@FYC{)I%0w_b%!f|f4WuQs7cMSh$_2>YFrTH)lXg)H`qSCq|_m!Lyo7^ zIl0@nIgh<}nwTTHn@@!A3zwJ@4vr<`-r5vc(1|J~2jy0_+q571)Mj4mw4*6*bx&?x YiU6;dv$Qc;>=fkx`k(s;$eq!D011ZfQvd(} literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/valid/08.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/valid/08.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c08c7dea829b232015e3ea8c0dea2fbfc10c4c4d GIT binary patch literal 2697 zcmd6oXHZk=8pls^Qb<4{kRVM#0l~Hmp$Leuz*0ppAWfEzAkt9~Sc;&6M5H8iT#(*n zDZ+w^G^J*_^rA=zNE0PA30aohL}%{(a%bFozj&WB|7YGa=Q;2D|DET|Jgf;872q?{ zGt>hh5CDL%FM#z1&;g(jPH-}m6EKjI;V>8oL%AqT)e2y^04hKQA;$nH1_ELrtWH3j?GygR9^35KfIvYQ z9D!up;AR&**}92H2&Vy+}_do>UCFlPw&w1 z$mrPk#N^a0ZEpVk!s62M%KFCU*7oNe#x9e?1p&aXSnT{CxiD-lD5nQV4i^M^i!BfX zgCCVg@M{r}PW}SN@KIb??c}_gHk71-86D>wFo+hEQk<1u=b(Kd`+H!~{}b82!2ZoO z1@M3ncJn|Cpb6|pF69#e+3%`m)7Z#^YkU5YFZy&!+9#TtKvL+c@wV?jK=OoGg=JA} z;mYM8gS9?DKaGc;`Ggn<^@;aW9ib)w z#bdnRS%nlfmIwD)+e@w5soU|&Z8Mhp&5RElwK7Z(Glr#ANUzUc)^*%aWQgBdTQZo62 zgPys2v}lI?)B=vAimgm;j;<_pjOHcSn9+=l($^%iHkw(0!ZUGd@dYz7Gv|1r^k1KB zx`ukR_CD>qQxvU=!+vDQc*C<-_R&PU!Ob6Xer|c+$^tA+rRXL@CO5i%EZLR8)i zgKT~0r(vA&IFI(4mn^_kgHj(ki(N{*r(wA)YHzVn17`emt z2VE6^%M`0KlzV6CEi;7)s{I&p!r?_haaIaJH*Eb+K7uY5uu2Z|qq{6$X+P)8-Vf-#`#;==SJSNCyGkQR}6u#W1jEo>AM4f>1LI}z(6&mOsZ+e#xobgNqb z9VB0H3pmDYw${Md~)4;pW7jwrc9(8V!@_q?zKUEo^^y|wzu zX(RBe^@M@Qf&QPpPsO%^Vn>=(RlBJ!xklB;7|18miE%$D8ro>jv}J4*){xYbAYzdE zCeJ3IW)ucvS-HRoXE|6=Yo6AEw-*ut+t&urpVwEU)t;6v&5sSZ3A}GJwIPd9Z7n% zAzGqS7Xn*Pp2?fxMqTb~KULw7IlnkjZhe|8U|&)1V&cmJq{DYwdICd_9AcleTte^W zeC=-tqLEoPvoyYbJNV%yd^Ajo~t_NluKKL;%Jya`9mF3?{y70rA}G!>i*puF&{snklmEpnd%|4*sjCR z_g?nxqnL8BkGvSB;wOV2JU?%H!*=y){m`CNT9xfBY~o|tC%)~P*f2R-xAudgy5_f~ z-abVZZf;^Q)!o+tDvKjL@ms0%?NW=50aBsYz)Jix#lFoje5d0|c~ZFf9AJyVaG#BQB;4@XGnx{W5*XSJUCc@NQq{OFsG}MSS)7 z6RV<|i|4JBT6#_1t%Np?7J1|5*}vzOJ1tKadkOM)WL3FGpxLQSdTp@)jDr^1-SyIR zbNT!{6^!aRUOH$lt9^aQN;&U-r&9?<&qvO)c(}29G`q`Q@z;)*#BDz9^1bwg0sv4! zR*U5nZUkI(GgTi~I8W)PNRi|{q=KX~<9eiWinDcP%}PvXiT3x0F$u3}EFjU+{KJ~z YiwH0Fu&GP=z7ywrPrU#C^I4Ps1QXs*egFUf literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_multilabel_class_decremental/images/valid/09.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/valid/09.jpg new file mode 100644 index 0000000000000000000000000000000000000000..848f6ad7da02a83bacb009df18bed0571fa398fd GIT binary patch literal 1872 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 C2PN+S literal 0 HcmV?d00001 diff --git a/tests/assets/hlabel_classification/annotations/test.json b/tests/assets/hlabel_classification/annotations/test.json deleted file mode 100644 index 5112381310b..00000000000 --- a/tests/assets/hlabel_classification/annotations/test.json +++ /dev/null @@ -1,721 +0,0 @@ -{ - "dm_format_version": "1.0", - "media_type": 2, - "infos": { - "ScExtractorVersion": "1.0", - "GetiProjectTask": "classification", - "GetiAnomalyLabels": [], - "GetiTaskTypeLabels": [ - [ - "classification", - [ - "Rigid", - "Rectangle", - "Triangle", - "Non-Rigid", - "Circle", - "Lion", - "Panda" - ] - ] - ] - }, - "categories": { - "label": { - "labels": [ - { - "name": "Rigid", - "parent": "", - "attributes": [] - }, - { - "name": "Rectangle", - "parent": "Rigid", - "attributes": [] - }, - { - "name": "Triangle", - "parent": "Rigid", - "attributes": [] - }, - { - "name": "Non-Rigid", - "parent": "", - "attributes": [] - }, - { - "name": "Circle", - "parent": "Non-Rigid", - "attributes": [] - }, - { - "name": "Lion", - "parent": "", - "attributes": [] - }, - { - "name": "Panda", - "parent": "", - "attributes": [] - } - ], - "label_groups": [ - { - "name": "Shape", - "group_type": "exclusive", - "labels": ["Non-Rigid", "Rigid"] - }, - { - "name": "Object___Rigid Group", - "group_type": "exclusive", - "labels": ["Rectangle", "Triangle"] - }, - { - "name": "Object___Non-Rigid Group", - "group_type": "exclusive", - "labels": ["Circle"] - }, - { - "name": "Animal___Lion", - "group_type": "exclusive", - "labels": ["Lion"] - }, - { - "name": "Animal___Panda", - "group_type": "exclusive", - "labels": ["Panda"] - } - ], - "attributes": [] - }, - "mask": { - "colormap": [ - { - "label_id": 0, - "r": 241, - "g": 91, - "b": 133 - }, - { - "label_id": 1, - "r": 201, - "g": 230, - "b": 73 - }, - { - "label_id": 2, - "r": 7, - "g": 105, - "b": 132 - }, - { - "label_id": 3, - "r": 255, - "g": 125, - "b": 0 - }, - { - "label_id": 4, - "r": 255, - "g": 86, - "b": 98 - }, - { - "label_id": 5, - "r": 0, - "g": 245, - "b": 212 - }, - { - "label_id": 6, - "r": 233, - "g": 97, - "b": 21 - } - ] - } - }, - "items": [ - { - "id": "5", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 2 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "5.jpg", - "size": [1368, 1872] - } - }, - { - "id": "4", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 2, - "label_id": 4 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "4.jpg", - "size": [1327, 1494] - } - }, - { - "id": "3", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 1 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "3.jpg", - "size": [897, 1175] - } - }, - { - "id": "6", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 1 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "6.jpg", - "size": [955, 3309] - } - }, - { - "id": "8", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 2 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "8.jpg", - "size": [1368, 3523] - } - }, - { - "id": "1", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "1.jpg", - "size": [358, 478] - } - }, - { - "id": "7", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 1 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "7.jpg", - "size": [2200, 3995] - } - }, - { - "id": "9", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 2 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "9.jpg", - "size": [2265, 3933] - } - }, - { - "id": "0", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "0.jpg", - "size": [272, 427] - } - }, - { - "id": "2", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "2.jpg", - "size": [1231, 3561] - } - }, - { - "id": "11", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 2, - "label_id": 4 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "11.jpg", - "size": [2265, 3667] - } - }, - { - "id": "10", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 2, - "label_id": 4 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "10.jpg", - "size": [1466, 3424] - } - }, - { - "id": "14", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "14.jpg", - "size": [1405, 1946] - } - }, - { - "id": "13", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "13.jpg", - "size": [1555, 2000] - } - }, - { - "id": "12", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "12.jpg", - "size": [1273, 1959] - } - }, - { - "id": "17", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "17.jpg", - "size": [1314, 3582] - } - }, - { - "id": "16", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "16.jpg", - "size": [1863, 2779] - } - }, - { - "id": "18", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "18.jpg", - "size": [1035, 4232] - } - }, - { - "id": "15", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "15.jpg", - "size": [2118, 4065] - } - }, - { - "id": "19", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "19.jpg", - "size": [1158, 1427] - } - }, - { - "id": "20", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "20.jpg", - "size": [1158, 2935] - } - } - ] -} diff --git a/tests/assets/hlabel_classification/annotations/train.json b/tests/assets/hlabel_classification/annotations/train.json deleted file mode 100644 index 1cca640b2d2..00000000000 --- a/tests/assets/hlabel_classification/annotations/train.json +++ /dev/null @@ -1,700 +0,0 @@ -{ - "dm_format_version": "1.0", - "media_type": 2, - "infos": { - "ScExtractorVersion": "1.0", - "GetiProjectTask": "classification", - "GetiAnomalyLabels": [], - "GetiTaskTypeLabels": [ - [ - "classification", - [ - "Rigid", - "Rectangle", - "Triangle", - "Non-Rigid", - "Circle", - "Lion", - "Panda" - ] - ] - ] - }, - "categories": { - "label": { - "labels": [ - { - "name": "Rigid", - "parent": "", - "attributes": [] - }, - { - "name": "Rectangle", - "parent": "Rigid", - "attributes": [] - }, - { - "name": "Triangle", - "parent": "Rigid", - "attributes": [] - }, - { - "name": "Non-Rigid", - "parent": "", - "attributes": [] - }, - { - "name": "Circle", - "parent": "Non-Rigid", - "attributes": [] - }, - { - "name": "Lion", - "parent": "", - "attributes": [] - }, - { - "name": "Panda", - "parent": "", - "attributes": [] - } - ], - "label_groups": [ - { - "name": "Shape", - "group_type": "exclusive", - "labels": ["Non-Rigid", "Rigid"] - }, - { - "name": "Object___Rigid Group", - "group_type": "exclusive", - "labels": ["Rectangle", "Triangle"] - }, - { - "name": "Object___Non-Rigid Group", - "group_type": "exclusive", - "labels": ["Circle"] - }, - { - "name": "Animal___Lion", - "group_type": "exclusive", - "labels": ["Lion"] - }, - { - "name": "Animal___Panda", - "group_type": "exclusive", - "labels": ["Panda"] - } - ], - "attributes": [] - }, - "mask": { - "colormap": [ - { - "label_id": 0, - "r": 241, - "g": 91, - "b": 133 - }, - { - "label_id": 1, - "r": 201, - "g": 230, - "b": 73 - }, - { - "label_id": 2, - "r": 7, - "g": 105, - "b": 132 - }, - { - "label_id": 3, - "r": 255, - "g": 125, - "b": 0 - }, - { - "label_id": 4, - "r": 255, - "g": 86, - "b": 98 - }, - { - "label_id": 5, - "r": 0, - "g": 245, - "b": 212 - }, - { - "label_id": 6, - "r": 233, - "g": 97, - "b": 21 - } - ] - } - }, - "items": [ - { - "id": "5", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 2 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "5.jpg", - "size": [1368, 1872] - } - }, - { - "id": "4", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 2, - "label_id": 4 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "4.jpg", - "size": [1327, 1494] - } - }, - { - "id": "3", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 1 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "3.jpg", - "size": [897, 1175] - } - }, - { - "id": "6", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 1 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "6.jpg", - "size": [955, 3309] - } - }, - { - "id": "8", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 2 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "8.jpg", - "size": [1368, 3523] - } - }, - { - "id": "1", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "1.jpg", - "size": [358, 478] - } - }, - { - "id": "7", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 1 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "7.jpg", - "size": [2200, 3995] - } - }, - { - "id": "9", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 2 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "9.jpg", - "size": [2265, 3933] - } - }, - { - "id": "0", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "0.jpg", - "size": [272, 427] - } - }, - { - "id": "2", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "2.jpg", - "size": [1231, 3561] - } - }, - { - "id": "11", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 2, - "label_id": 4 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "11.jpg", - "size": [2265, 3667] - } - }, - { - "id": "10", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 2, - "label_id": 4 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "10.jpg", - "size": [1466, 3424] - } - }, - { - "id": "14", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "14.jpg", - "size": [1405, 1946] - } - }, - { - "id": "13", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "13.jpg", - "size": [1555, 2000] - } - }, - { - "id": "12", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "12.jpg", - "size": [1273, 1959] - } - }, - { - "id": "17", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "17.jpg", - "size": [1314, 3582] - } - }, - { - "id": "16", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "16.jpg", - "size": [1863, 2779] - } - }, - { - "id": "18", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "18.jpg", - "size": [1035, 4232] - } - }, - { - "id": "15", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "15.jpg", - "size": [2118, 4065] - } - }, - { - "id": "19", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "19.jpg", - "size": [1158, 1427] - } - }, - { - "id": "20", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "20.jpg", - "size": [1158, 2935] - } - } - ] -} diff --git a/tests/assets/hlabel_classification/annotations/val.json b/tests/assets/hlabel_classification/annotations/val.json deleted file mode 100644 index 5112381310b..00000000000 --- a/tests/assets/hlabel_classification/annotations/val.json +++ /dev/null @@ -1,721 +0,0 @@ -{ - "dm_format_version": "1.0", - "media_type": 2, - "infos": { - "ScExtractorVersion": "1.0", - "GetiProjectTask": "classification", - "GetiAnomalyLabels": [], - "GetiTaskTypeLabels": [ - [ - "classification", - [ - "Rigid", - "Rectangle", - "Triangle", - "Non-Rigid", - "Circle", - "Lion", - "Panda" - ] - ] - ] - }, - "categories": { - "label": { - "labels": [ - { - "name": "Rigid", - "parent": "", - "attributes": [] - }, - { - "name": "Rectangle", - "parent": "Rigid", - "attributes": [] - }, - { - "name": "Triangle", - "parent": "Rigid", - "attributes": [] - }, - { - "name": "Non-Rigid", - "parent": "", - "attributes": [] - }, - { - "name": "Circle", - "parent": "Non-Rigid", - "attributes": [] - }, - { - "name": "Lion", - "parent": "", - "attributes": [] - }, - { - "name": "Panda", - "parent": "", - "attributes": [] - } - ], - "label_groups": [ - { - "name": "Shape", - "group_type": "exclusive", - "labels": ["Non-Rigid", "Rigid"] - }, - { - "name": "Object___Rigid Group", - "group_type": "exclusive", - "labels": ["Rectangle", "Triangle"] - }, - { - "name": "Object___Non-Rigid Group", - "group_type": "exclusive", - "labels": ["Circle"] - }, - { - "name": "Animal___Lion", - "group_type": "exclusive", - "labels": ["Lion"] - }, - { - "name": "Animal___Panda", - "group_type": "exclusive", - "labels": ["Panda"] - } - ], - "attributes": [] - }, - "mask": { - "colormap": [ - { - "label_id": 0, - "r": 241, - "g": 91, - "b": 133 - }, - { - "label_id": 1, - "r": 201, - "g": 230, - "b": 73 - }, - { - "label_id": 2, - "r": 7, - "g": 105, - "b": 132 - }, - { - "label_id": 3, - "r": 255, - "g": 125, - "b": 0 - }, - { - "label_id": 4, - "r": 255, - "g": 86, - "b": 98 - }, - { - "label_id": 5, - "r": 0, - "g": 245, - "b": 212 - }, - { - "label_id": 6, - "r": 233, - "g": 97, - "b": 21 - } - ] - } - }, - "items": [ - { - "id": "5", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 2 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "5.jpg", - "size": [1368, 1872] - } - }, - { - "id": "4", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 2, - "label_id": 4 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "4.jpg", - "size": [1327, 1494] - } - }, - { - "id": "3", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 1 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "3.jpg", - "size": [897, 1175] - } - }, - { - "id": "6", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 1 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "6.jpg", - "size": [955, 3309] - } - }, - { - "id": "8", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 2 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "8.jpg", - "size": [1368, 3523] - } - }, - { - "id": "1", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "1.jpg", - "size": [358, 478] - } - }, - { - "id": "7", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 1 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "7.jpg", - "size": [2200, 3995] - } - }, - { - "id": "9", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 1, - "label_id": 2 - }, - { - "id": 1, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "9.jpg", - "size": [2265, 3933] - } - }, - { - "id": "0", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "0.jpg", - "size": [272, 427] - } - }, - { - "id": "2", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "2.jpg", - "size": [1231, 3561] - } - }, - { - "id": "11", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 2, - "label_id": 4 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "11.jpg", - "size": [2265, 3667] - } - }, - { - "id": "10", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 2, - "label_id": 4 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "10.jpg", - "size": [1466, 3424] - } - }, - { - "id": "14", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "14.jpg", - "size": [1405, 1946] - } - }, - { - "id": "13", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "13.jpg", - "size": [1555, 2000] - } - }, - { - "id": "12", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "12.jpg", - "size": [1273, 1959] - } - }, - { - "id": "17", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "17.jpg", - "size": [1314, 3582] - } - }, - { - "id": "16", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "16.jpg", - "size": [1863, 2779] - } - }, - { - "id": "18", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "18.jpg", - "size": [1035, 4232] - } - }, - { - "id": "15", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 0 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 3, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "15.jpg", - "size": [2118, 4065] - } - }, - { - "id": "19", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "19.jpg", - "size": [1158, 1427] - } - }, - { - "id": "20", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 3 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 4, - "label_id": 6 - } - ], - "attr": { - "has_empty_label": false - }, - "image": { - "path": "20.jpg", - "size": [1158, 2935] - } - } - ] -} diff --git a/tests/assets/hlabel_classification/images/test/0.jpg b/tests/assets/hlabel_classification/images/test/0.jpg deleted file mode 100644 index 4634f9e0cf4f124054952645e015fa32f9a4b0f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19539 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
    lI*eNKkifywmB&63&XcnnDaj) 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 diff --git a/tests/assets/hlabel_classification/images/test/1.jpg b/tests/assets/hlabel_classification/images/test/1.jpg deleted file mode 100644 index 515127e50bcd338a7884269f53decc466fee0dc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29933 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 diff --git a/tests/assets/hlabel_classification/images/test/10.jpg b/tests/assets/hlabel_classification/images/test/10.jpg deleted file mode 100644 index d0118ec6c79604fc8f7adbec296b12aa61a0fdf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222211 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
    XnU949xtK>(Ji$SG3$>sI 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

    V}i| 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%0D_lxzl}3RU!Mh!- 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

    a7u`JHpzKT=ty58E0;LKoqFGdI&y?KM zcJ^eK#H(JC9xrMOrAkeKR8L$onx%|UDs+7BU|j92D5@g*arP_V!^d_8;)w|08P}q8 zSt>{&JEL<*;xEO6K+HsaL-R!Pf^g#QW=q<{8X2a%-62;E+%&U6^qa6`Sw%m4vl+JF zm${TwH*}vttrD5!Cld){tFgD`D>Hq^9|b?>B7W^|hflrWz3(3V&tz)}YYyogR+ z&#|Xtv=Fep!QP***us=MR9M&PD!?>YpT9FBI8<4LGDXc%?2oqrS2q~9;-W8rm`jI^ z=sAWtn{d`!o(Q0C%~hgU>@VG;&9tzd973-L5!$V7kySwY0@~#-hHhGzT5pf8`GE#N zVFJ9^E@k7y(kOD9L_h%}(mHg`kD3Huhf5 z=XtuQ<}NxEH6Z(QIWw?7Lgcj!aOKe&8FkVsrAtF2G8J@1G(V-X@Jg#@n}^+j7<*H`r6l7GRWm|=+29%sLRQq$K~5x&WrZuTL(YV)IiWR5nufNdyK&0M z=av?Q)nW4yzzoVrlcl53`L3srmO&6N_1o8Q^%iqfRgdb}pxiQhfI?TG8V!0*QWW0_ zuhcJff#73H+CE8({-wxIl-snBgF97LAZg_3@d6rroAg+}U?$=A_?+!|Ib~*8bBzL~ z?_}12>wULjC*~hNp&HL1*<+`Su7%+i#tC|H9x|!u7K}Q1233=tY@!`{rFhS_W(yGa zw1(tVmbsx_98sQ+Gl#%QDk4p=VGjCmCFgM&(>J*@lOp6aYXip(wvQ$ycmzjJxA!yA zEL9$&Vd?--5TUy#+Ka%Po|&uBWs)OG*Kf#oDiCz;eWup-;?OhCAy0SoB7Ry^aPn8e zHGL9YWRjiAIt-UR7lc4;mBkMl!uA5`#-Pn`vP`3lAj-(_wFOaz=5lY^Cfs}3Y(6_aT)4QP6`0N>BHbtothw9F9>KEAK zdj_WfJ^P&ITAu0$mZPtYD2AjxZ5s33*ug<0ST(e&jdZsg%yS{YLEulvn5N0b0$0NwS)tgtedwFvYpvN+~8X_55e;-e+@|79$^ylw_ z-Bw*f^D2yD9kGBPT~owqeHVKR?Phw7S0f-Y3f)~6g>PZq>|r}bj81Y*8OaNwVDSO0 zSps8DLu978k@#$=Se>%4j}upEdIOykDH;vIMN8s_g6o+4>Vxb%0!PYE9Of)NZ-GTcnGKnRBeL zkwm(@T$bdhrZSCPH>Lu+q>j(Blw_CK$i@nNt61ijf+jasy70gDOe8c}=gM?`g__uU z^Md%D25TJ)T}<-eDb(fkIq58%Z${E8ccXHG__fo?{$|tH2J2Wc|?Jh^3YXjlq;*MuM6$tVfCWY#m%&slYWvv-pcesp>=e)K2iq(vy*= z%*oC|jTy~~%O^cin$>AxnZyH;>65QkJsPF*%achP&9dT(TmIc(D!F7-kBj8hMU`5Q z{;i@G1*rlWNo*8AYr zBBx`$F+1M^>lJPO zF3|70$#GYjU0dbiP^?1A9E&T+@nuspnrpt*#LC0UN5r&}oJBncT0|XDLj|OYIxXQO<4>wWKmI$U3gy2$ zrd6wa(juDv(>?x+|JX%?`kc?4Nh=EjEsxRwt5XT zwEweM9X+_bTojUMwZOZ>y0oq5Y$M<>I>49u?ckh=T!0ga_+LkP|MUDmmIgzF-q!vu zx!*kap%-;_^Opdbp#tTKIkkqiVx!h?J~IqGudR{x2?}4+^^=+2g5}p86nT1SzmnpJML66dL;H zAOnRJenRqMYPeE&;xrkr^0-d~@5=doO^yg~^L(g|xoC5vPFj$-U z-AfBn0gpKicVulRL`9C#+P=n31@7KL4%iEzNykDZV2&w}zKXtsRF>B8lNU_t$HR*C zkPYhA4(_4|_=@Xb7Fp#3gW0RU2re??M!#$y#!g> z*c|D5O8IEY=yXcG;ut)&0Y9PhY69nHX)mt$&ufAS}jyzVFw`A=Ifnb|O|ii11|$Pzuka z8N3|nMS$CiMcoWL>mEZCdu3o9Il1z*0DPb$;G2LiPh!0+1abm>fHEz9^mc;V5(eSD z5yx+2L1kE|ze0aZ!Ur5SmWGbpfzEe4b#HSslXuFK#Q0`~gprGaZW#E+%~;6KvdrM3 z4d%{O^!_)I@G^#K)}k2atc72S=Z}H}{nEU@z}#v17TXZwt`Bqs^@e{Kw3rR=f6=TPEBj)L{?$4fuyw>@hKWz26w|RRyd^WT{ zW+eX>?M!Iel4rZf9DW$| zAt!Ky4&Rv)Av~bEaH!EAW)mNt1YI$TuCh79YYXpQ=gAp~)%sqp$A6Z1mQ+lm@(?1{ zF$(^y)K*&!U45W?z$9h=uvQIBteNE#q^q1%W-5fBm0h)HBhBGKZ&!3=7@i^3rynQ0 z4=uw?(68KBLw&8P)vbygApvpL*>Ovv1jhvk`O%N}>)&sxe zWv83xO+wOh=f9&B0J|CdrC9$6#qY>;hCMy=l&orY?UFZV9O+e`yL@>9Hqr~yza~q5 zYnZmkyX`E|qkw`MWTwMN3ANVk8rA7Wyeq6rcW^g}ez&bSc(;8LX=E%`VE$y#z0zu>lPX1Qb+Vhe(u?3Mf7J+PXtnsMkkTmn!vLdr0&r?( zwxZb}@JwhW%(A{l6NGkcKJpq)QA2B8BHkBRI@%b69mPm#yIbHA+}FgE20_}af8l9k z7N+J`Z4Bj%5XyzviA6M)q3>1xLY0QZ3*-Zh397^f@h;f&FW+UCH9g5gACG$|1+(YS z`uG;6!=O<>Ip9#?T?M~Td;me+`C*@koH84OfxtE~1%v3o#7Gn)2CcMi2)%C31mm{U zbD%WVU6!C?4ZlsW8ujKM0W+BZJ07Qq!6IUKaE6O(j16*0&UZi-!wxL|hgA2hX=~H1 zD9^NWJ2BLnywBwke3#|HUIr0oYh5oXRFQJmavC7!7qLWBS(YV^?a~%iBtbM1v4MW$ z3gD|&102rg<7RG#Q?K6aDSU=PlwRaTX+e_*Psp^t2N}TdoBBgSc6j=Fi_foaWS9;G zX1%My2{IO;A-qHEao#J|WoNcvv^-}BHtETF1hhK4#3|+%zks=o?$tmm+b;XzI_nR^ zG_wQbuim6r?Yf}q?)~*ysPt%GYL54J{~HsAnu@7Gp~;Fv&agvv`k_Y(ScB%9sXr17 zqlI8NM5o@aH0e#Y+$j@ce?c%KDBXj9;ng*5(SF-O8g0>Rjrbb~iQhqo1$n60eCx&~quRs)8^0uSbAa?k;v+gh$7{`Rj6l@7nah>ah z#=)|D6>&b3unS*R`P19{?p)e;7h8>7L%quoRcwb-#)Y$d#j{xP)87bFpH?M>1ps zpY|;QfUb8^rBU9;5NG{?PGb`d`r@+Exry<$XsG%0)rlxAB?9*K2{j${NT{ujezors zP3yg`m$d`*LtRBu3g9x$Huv>TK2m3~mUR}fE2@xS-i3ec_pdVC#|ox~jVc*wazw>< z5Qj=E2O_koJu|XgAF3qOW=t@(4#$fSL@g`amb(4E%ht!&0UUeWu&RqmSL*sZi(bZI zL2iPG(m2LUkopG?vO$Xz7`j_}zi}a<(li5IbwpR&2+Ou>tkMhFHf4zgKg^7+?I7GjqGL@~VmXH#cvzX^us+0+Ae}`QessGN>^|mWwgWK&Tg|-J( zYAT_ii)*QU*4)8K5eu?*5O`pU5*@x~P9|_2G`@_8gyrL<^+P_}d3}VR(t-iGh4|A$ zt-j&dxd|gJtDLAYGSPoaH$4)TG8lJk7eZjavW*EsS!N{NM*LE{Erm|B&IvPrY{=}j zixKXh*B90H6ts0IW${l$HmQiRj)nDdNvI-*AI}PU;CO+&$ks3CrThH;uoXA~6st9g z%hGQQaOE~aa&im2=i0PidFi7c#Vx$4$We#VK=Ei$%*VUmOGgSp#f@oCCMaP?Su$&v z&syv}9k`viaiF5iHSv8<6xxb6c1#9d15_i}G|d}T{%QGMcI=eNjNP|sil+K=kd!!L zdPP)$0un#)3Dqe%yv-|>>uPRISEVGU&vg!eW!wRi z5qEk5C*q8=DSudWGL2mK2?SEPDPXU;p4|YKtG*m$v#9fh&s8j~pv)&hEQQPBG$ywo zd<6}$E`T5q$N8Uso(C*zN)~C6H);U)@VXWqBY?;kq~N)COJR`To4LA7?~MU{NF}@Z z$tCsBs{GVAy|399gSA4PA6Sujbf_GH{g;wBSwG&d=mlc9LdzR{%X>9E;(3X<6FaXZ zp4%X7^DLX@Ey?ZV1?0o)K&!hi!*{zD=5`ExYpN?@6(xNc??MtcbUWj3tB8to4_%;r znVFaN?c;pPwGk zr1D^dfzuw(m>g?64XQNsslTG01^d-bop5yl%+Bb`;IIXj=lmqX5KPyxg&rohZhK74 zn635>vN^sINW_7y?GnegzLHnDqvOMbe`(`3PWX1~LrQkpvMzwn0fo*ypKot6D#xS_ zd54dFQLL9UZ9B@a4trQ|p5FWoHy_W!Y{famm|Q7}vRGH#w<;L05a+yC>7x8ffxIL* zy6y2Ft-~z4uMc=haJ&+E^JN(I*;2TgQ=$7cIM6LjD6%fz#;el7(LdYsO9lC6-_9~r zq9Dvqe8(|u^WLS^i`R%ZVb^U6FX~ja9}Yeo?wu66Yv|W9)u1=Dwn`Qo^z8n3u0#Hh zI=4$RP)asa(!w;;P_?s5FI<5QOh2JJ}1^ z|E2iT0(&{9GFYX@r1vWhZ}==^Q9Av`N%pVO;EOu^+4n=GOcnirOEOFa5Lx;kefa-H zf2Vw1z6!HjZw=axyQVi%r4`&IkrBok8uON`{~sTYtU)V^WYSF9AJv>sdGe&e&3pT> z!0k(nkD&(yX%Jt2fU;1X+n)G2U^Rt5k8JUQHpt~JI{SXM=DMX7>ZQ;}C9CJd%{yLJ z-a2!eZ44XOx_bLsDMJ>D)u4q6OqtC#TD`F(pKlEb$ILJ|MHZfOZOhSKyo7Xojd__A z-HJZr{w)2R9So>?CFNkiMe$uB@M)+UL;euLxQ@#Eio61jW|%#O#@`m=wI^xj!AtN6 zRHX77ZY32G3*k`&Q->)7Dhr4ENFhJO zqodDvS#XXVKdiuE?WR%#WXSP|<@o08y3%x((Wc+o+GFkOJjB$fG~i!~hq7O*>0IiQ zIOZtAf~nNc$06_1(kjJ!v7?WrWZS~}r$lYOyC!qx4l0UU$XPSY-RJ3lb$7_E*q2jM zB#(X960=IlIC=7yB81dRq9^{Pklz?GYl$D1Nqn#SMSZpd=oacN1l;p76qC|@D;*8} z;Ka~b^}_uYRLH~s)+&Gdcn9m!$M)%|Xz-kB+FQKEGwPNi%PJxBSkTSn;kP*+YJt_O ze<`jkzyorVA>!X#8q^W<$;%64HA}6UHPBt}Uuq0i)Rs-wr!YMRS3hKKPbhl3aT|NL=yo#$g(_Qapb#?-SGt19U zg0_Dw@?gGX4icY@W|DSn^f;MzHN%%7!Z_vnr-z}})g+dO{{oI7Z8(dMJLr5ASg zp5l*1S3igDwoQf%BO;UYXrZwyhd_qqhEP1Y_N4z_-8vG6|9F`&L`Q!ia!2a9XOT(X zva((knD}{po@F(EB%jfQi_wqb(Bwhk4@=<+xRDg}rfreSTn8XN@4>A^lSw6Xhc=BU zoxry;QsayyYv^+yK6A)gC)U8$jGd$GqqF$d>7qhy>;58P05TY6+U3=82!(7giNsa= zHra?nc2q4f6Id?i`{RtkY+<@>9AH^u&09HIRw1EOe%>ov9zd>~Z=n_e;Cb;=u@ZY~ zW1*49n~4cFIf zQZ3W)TFspd8><#E-DgPsabqQ>XCUbxGgax4M%%3pdf^ZHWsFjv?d{aR{gS5KhJx_V zKv|?kaRzE$aA0i-ym^%)6-5Zhg^&~^br93N$;#=}s!osTe3DfR4dJ-HA}Q89sJUSg zxvi4{ykCTtkii&y!ad&=)e|q#9AA7`4N|8;*0gjoT*)vqO%d#ADr$q5$@_HMX|{oU zi$&YdePhuy{;65_{Y^RD%*ViU;E1pWp>#&m;z{e1$BGc*>qz z`w(yNwW|@RP0RWznu=JR9zJ3T4?RHG;N*FtVI)|795i&kHg{Q<6q9raHAJ4~V_Rr@nCV%4q-FM6fPG%x)ObO94ng3&05UkR4Dx`djI&>16w@8 z_MZ?DPF9R^QE_a6Vmc4kQR~E&2p>SXS4D-j0>CRn0LEa}c%n`M!WvZ1r)(FoQ< zi|M05wql$XO2pfGAvYIt2gId&TsJ<*N>*HCmmKIW?mAmMv9j>VUszpg7a8&kd&$7F zxd0TJ%H;KCL1mb3AoTb)7kRmr9-2cmCv~75<#J;uxa)evavK`Ox)VKw2^|O4Ova^I z@;Noox5#`mt^^UpuT)0wLq&!5Qyn<7APDo<8RYIi);xvX4^1nx-I24PZiSPu(TdPUB^96@v0c)XHEA}tq<0zKRHJ0vu)RH70Hm*^ARqi7g;F!ax1^UMi+Kb5z1%<;}0zu^?+N0;NL_qoM+)YNjG+ zJ#jJNC%V_i7xVF2shGo1)p(D?ynJIJ>deZ|i&^V!;?DO^(NSzk(K@PGwWQ+A`JZ-* z#a)nZTx?Ej9)5zF0|}D;i8E0{C%Q|b!EUj5nM;yJ@Nm#eHrC(L<)i<5lo}~ffaq5u}ApvNC^T}-?ok~ z6z8ShZD$p40h{^=bUwUq4jUSMS|IPTaIAb`B#p4~Sk0Zaf+dqGTU$h*qacnw+R03! zYg%^xx^$$K#$z5Hh~+}%oT9Lih|qjbVMH{sb?c2eg_DT%OvH@6c4!dRPRQps1xe7{V{?7v1$+PxY8)!{&=C#Y5nZW$fo0)to4=%EvA4 zSGu&*r8gHsw{BdgP)f9-DzOV&5xIJU?9bRc%GBBgz_@lAzR?-I-ecHO#L8~)-Q@Wq zWr<=dv~8R#hSG!&+1L8?|;w zF}F%80a75UqA3eKnNgIrXusT_UspO;Kd?T_i67CAm9}Xe)7Zt!j+Vt_>z0GloJnfW z{^d9cU(UOY7f>=Z(|yr$?U794>BOkFYp0J`3I4#)#kJ`dsQ4M!A-jo{@?A0ifgWYn zy+b?IVlUoa91jBtV@Kezi*cQ(Nd~YE+p?6tqI*~G_HiNf9w5o>aUKP6Vemy4N0rcs z;3r-~8#{GLMR3$+Ll|a`Ic&yBB4mfv)V_A`|1Glz=&d|p`1wfCJ^2bp@W>0S!(7J7 zJ*H&KICYSIAP?_YQD{z@-7H>kUc6cN);*@T#D>LUxKg=5<)qkyp^uA~X_dik!5Q|e zn3>x*Ou}lvHN3cCW06*{vrs)gL4MtUg8yNh{Acb5h+9LQ_)BHxXyvr*7r|@u;*ctTrI>1cLG=sC(j#Vn zm$3A$X4ce-ye{oX{r`h1O7;E^;{eZ}``tFW*UxDm{L6LFv>xI;{<#Pn{(JM8+P7l_ zJfji1o<}xIoBswa$bM!q6r#-RI1G2!diZj&u>brE4^E*brmH{boV*LOS1fIWz)q3q z069rOHFiJY^gh}RK#Xs@*Q@*BS3qdG;@u`)#^u?2Ko>~h)T`@cE{fzjft|?+?$n76 zTA`DT+KtIg@DWjNvIt~U!Jb6*capc3liS$_dD21EGk+!mY4%6OQ@>G9{&Pfo?slpg zZY3*mg7uyrU!v)*qW6A)|4U(IP`l5D_3C8xt^lR~^ZXLmcf*dgo<7yut@gJaVsTZ5`NCJ7gIAVDV-Q-(uM@hEL1el= zM1ln5`t+BgX+`3SxzNl`$QYejAL(^;;4q{2Ky|%v1()VNsyGgdRP+2=Gm2TF^FlNU z>WBzm0^r+p@9IvKB&XmEnH!(exhvr#4-|G{{PMCmB1MtB-?^(i?}9z*OA2@zn|}7= zl0r%zBy*h^C?7VeIic*t)8bhq*{nv0wNi3V=CSeScw}}rK4a-uRd+sK8I(iOoMk%A zTVSRwng#smJX4nc{ET8g;~%9BcUmey^?v6Ilw(bX|F=JTJZ;>xS_rcoS)@9Qx+NyX|hO`dnGlr~PGo};#v~k^s z)y`y)7hvCS1aTGv^CH-Ge%XD6a__upkO3XmjZFjE*#|vTS`< z_+e=%V^qMzYv_@WmiBSXsQiS|W9-5)h2n^I%qr!oQz%&G@?;S6>9`RkJ8oD-w$nB!B;(S-Kd z-NZf73V*F3=lAm1Z?KZGPOP(Wj`gL7?m3{g^0Ky771=rGDRzSWOA$t?@L8`>_enR@jxdcX+HVl8n*Mtk2D{yKU~&SWF1ra-G$)aXNjIaq7+0qtiUzmeLib@UurPN4Fv-Vo@9ny)!fKgXcT%e zm4+H-nB)!k^Id~S^dzKwh4$_w;*ZPV7bF+Roh9`K^c!94r`5WJhetVQ3eY^MQ_gc= zspKml=m*0ysUf@exgg<7BxFV)?X!jAjqF;VYK#WDD8`7gdVTa^R86iQ*{f_rg{qdN zNK6$=cEbtagn5(q`QBA|9z)IeP(v0W`SSYp3^*^qk9n5;_>n}sl*b}iFgXfxnR4&7 z9*gzLwXI=M4cD&68CHNTby`5MQ>$%^;qZ)_SB6!Q<;8%5T{?$GyIxGv zM`qL(t`e8E+gWNsQiZm*R@l9l$i==9q}GDA`jf^=%Yu;Gv$b zx65iYbzlXA9M14D=?v&U7lIrk36))>-yd77gv|Q}QE8hlrEOFM{ms9{VtV9cw zT0U;{S3eDQ-B-_$6JAGTG7wSo6>jy5a*toc)+RlW=f_G}xA^#ePJDm!vNpEH^P`pk zf5ZB*?Taid^g3f(W1Qk>*{*Qe=1rS)m=P`%3_U$?mXP*n60DAwLFqcUa$c#NTc~)4 zSoILN=@O~;8n$~|k-HR3*Pi#Ys;kDieVUM1K%8V+CeK#3&@W-AQzu2UTs^ub)UV#*^|S}NP}f+!TKVw~`YoYqAP!WlWuNGD~^8)B`- zuJ{muno}HYZS_!$ua*=>g7=LU7{RC-K+%PEY6_Id*QS0lS8etSJ&|5lHAAc)0cOal z$)Y(wv0a$frD_@fE=zi!c-B6@YedBr4$Yn#*d>o8mB;XY_3toa->dIo$t_GJe-neQ zBx=Ia_gj41^9g>KHS}J2kYyEkwLY-yoj*1>C94Kg-x_}bNd8e6^K-$-PF?3S**dsL z&HP2nkeAfJiSs1L1PGyluY$uAwI+j1?4!cwvtVti&|M7LX#g7lskD6YR7ZI3ZfJ4# zHbmv%WD4nh5JI7>iviVICcLXkmvd>=2vp%G=ry)j9wN0JXR-XYGH4!iz;jRfl`P}* z+WrMy@`!STyxX4Oa<$c2q3%@Qnn?JK8x0G>WJk1xRDtN&I5vsNY+&=aE$ z>IecTbr@e6r4bEPGj&!Na)-e@lO5(!Fg<=AuEMnJ`p|Iajul~f&wWd8zTj6t%Z}3i zL6*-4+~Bc#LHk7 zL;tf|wqI?4Gs%e*V~Hm=)02#L+IK~R+Y%RRuGEi-xrA3m@HufkzGa3HNshMIT?>U8 zxE8F+BZXZPAU=#kxjDIN{0LaY6e9^<-Htdt|y_wCv`_$O>@LDkZ(JN}*Z^nL$oGy_qk!pe|c!)Ed3hon>uuTrMeLzqjN#{)|P z_|VUfsYp>0T}3nn@BF&tgu9T-Xr&OFG6;DsooW~{SvkvpGXzs;I|srMN!{GQ_RKY( zVU*1oz!}||QuT_3;YQfHflH*CzQKegMLwk_$gL09DgPnca!^ra4V_X%*8dd5gF2^&V>BbcMGyaT`l_!&>KBM(}HUXCw zb9kgj)T@@re;_=fW5Q=DaE4FEB;+uQB)D2eOzxj)ec3VW>@+Z$)QImjWtD`Ge?5n zH@hW*4%T+n50(RfatBWF_#16CaUWzE(8cf3)U2*zWG;IT!@TZKv`Zgv z0VeDZq{_1yN3r~a1hUkGF5>FKgn`2PI|ZE4_;GO;alQBOF3*CrchD1um}9x^0GzGa zm&sc!A=qnp_M5*?MDNX0-O80sRs*cm>ba?Q*}vr>1>H+C+H z)#ZjvBeuUkT`z1z>XU+*(;5AKM26B4s5$*%t&{#H*9LAQ`l303mLe`9LL!l2_t(QG zAX;A?W5GtWWDyMLSDCBQoVx3>kj9#&j;JwoBBk1S3ei6##mi_@u+wU)6{f@YpA1td z#e0sultz2DYNk!?ywmCn-EZ{=YpyB1DEU6TP^esvlsjUHiv{%3PZcE}n49S|zIzZX ziJ6hxc1%a;lfKzKIZ9PZ z=a|*x->w0`{`?fg?JrxsY}I{7->?EVoHL3S*H%&e@YJ!w`=*NVKUUPW2cEdu(bc`I z^oP@kLd%8gql+>{-M;Y;)6a2EHe5DP?Vpo=$dMsZVUSKBz1%_yunCx{x1FV@1=!_{ z{CC;fe|y<~{}%phj%lW2U&Ha7fUtPI-&z5GDZG{Z880y(;(VW#58irxj2QV#aYz5# z^;cK_Qov6^xebFp#?GPx!@k1TjKS|$k&WxZBVj{#7*<5&-P*rRUyHv+-39TIh*?{n ztBg#w(SlC^TAP?}`X|=OX1A%GS%!LRGA7Tr(mt&JgqxKqCDrZ+bxWu>;(FZ;vR6<9 zevf_b&>o9;!o2JURjDS#b(PyYs{4Fa<_x+ahvs>@+ETg<*2hrbW!L=q7yN`LK`&Hc zNf%T240+VDJxf(#2pZ!YvOc|gvv^#_dSpmMn2H~_YA-nM?pvGA-^4eWfE}*E16yIlpn3J98VQcPHVf2g74^zs`DeFL{AOw)RexAt-Xrn8YnT z7$I7LKDyCQs=FU+fHSZ#`*ejV*g`6o>j|C*t9<0=MKEwiWC$5BiE{T^IyyznHgaAx z@m5&cayz(O#ym;1?8&5&HaI4 zTQ4b|>{ZBPf?;-jBRe}+A0`d*ff zv6ycV+fDMGLGQhpzNL36449XXonRa!TJGd6y^6He)@+4Fjk&alr0Qbse*8<(vQbc= zFkKnI#;z%R6;c%6F6GRFUXd0oFX(Pe{lgbu&RRucB))>Ryi0si5>GqWvmkNlv{)SD;5vT7PwZJ!d^uM%N1U>tLIG zR=3Yvp)O8cO71TOwA0Id=M?TwouXLI0=@MC5a*ekr}N@5am~#8_qyC}pLXF8&p_iy zrb-X}Yw-i^UvtBu05j6)c(7TAb@O7q>{BbOg`H~fv%w+@UvQV&dn8ZIC_cp3F zsf>HNIY~8G;_i@{&`_<*wSqbw6Z&<;p~jw$h0vU_5cXOpQ&nN8tZFs0O1WX_w5Q-kXhx5Zk7Q%j+20wdvFsv`jjeNwN^iXQeb z99xHoZwf!E;nvQM&?87bqS!aSBT8>9p9n7)C3M;~7+#I3-J|>UzxaZY@09oVu%wBF zHAC;?S4B3_xJBunXa)qb+nH;(ELLK-vQc*)pE7>M(AEYbfma9hM!PC$OJlVNq$EEC zbMO^(S5CuYdV-vr(RcNyK?y(RI*tPR$@CKn3+X=K9dRceKPPBAO_hJN(e_enTTIxz zr-1f0z5fAsj8&HOulW-;aoU7f3IO*i%^pBgcLofL`tp_xCWMHuR7l+@)-v>U$mFk~ zLDVz;nztPXD<=+nqFqWtUwo`G_QInQKj`JEwF9UZy0*ZYx;vG4r*%Eb^1Tk05*1GpAbjPp)cKP=>4u} zX(sv;eEw3n#X_;*h}5KOQM}!utfyaV_a84=Rcg<)n6Tep*%;#Xqc%o`0iUE3)oodj z>KltY+cOSYoK@-V=8sJ#Lu>D|PdpdBwxc*K+@#e?sR(|Je8&0=WrH)guOCrv4m`-S zY+s!cy<2PB@`VOjDEdxmo;mkcU=^)S4GfqtWZo99gid{&4G?U71D*ARb6&Ki?6Qb> zIt8S=Rd;qy1k~_%-Tm<|GkWEiGPTXzpFJ?zU7A7)%BFLrviZn5UE)vnn2QCpk)O%` zamk#B){s-gn`>cnM%8D5HEu0@=;>UROjDHgr_3*7cWCPM8}2Fy)vd~01%%z(t0V(L z?;}18nW(&e#|IgFC68zaq(Ai=3?g?5x3`{1cxwf6In(yx5N8)wt~yxczB$0rP>Sg* zg{UfYpxw&5s!PRCtDkt6pJvF!x*O@@A^^tnM)k=vgZNj?tVipc53gtr8Hfu!dNSZG zi89s04!n12+!6ABS7|8wRl>CT;Tn)sSkkej3a5Owa!MX~QGR&L#CYVmn? z-8v;&w(EKIu8^pW7xjQQfx@W*zUx=cy1LG{5xI?1&3O-nUzd#}r?po!gCcH4o2$g8 zf(+i@YkyM2v#93fEOl26{ANbmijTf3Pn|nL3U(peGc@gLDcQSMg2er$cHfZV(b3#6G%cFpN zPsMm|?Ip%WZRc4{u=XM`k{8SAp04$|okGdO{v^SwoglhHXFVUiZkeaO|@t)uMRfXsVMQBNM0^9ur?= z?1V2oU$p(nx1_}`y#BWR$x+bD8-)PUkG3>_>cdh0cJN_B&2P+8uF*vdo2}oYU3@gZ z&h_$PYckucJol<4x3yu#OH%bS=LuhYIp3UTg#zYt@md6Y?gLw!gbIj7+%Y%YxmSbA zAD*1@i@5Nzj@hZ@+tS^H&CXu?9d3?G3xnT3Ka&M<$WqAKmlRpx7ow69t5q%xH8 z()#^>wfEgoO>NuWv7)G;VgOM<0jUCl1f@nmy7biR%V$OqDOZ^kh6 z&7ud9Tu(7`4>P{;U)|@a4QkNU-znwM*hoP%Rxa{1_wvisRTEB0OMyF8CJi!TmLZY+ z!9ugI!43W<&!bde)i$Ji}h2Q=_Z|VQ(yG>1fq4~wNt03XA2wOWnH!2 z?I$0A__qluahVx>cVN0QL{UU2$Aw`+X-&c@RtV5kcH&VJ#E{YK^Z41tU8mjQ?Cm~& zNa&G_Pq1BY$$l;}BqqJd?eT(Peo=i#Vt>?>%XH$@5~P-vV@{{g4E>3kXn1iuT(*g! zF%_{q18poWsiu}*{?cNu2YHp#(E2h)9kPG5EgGdiMXu8#!Msb+lnMpze&Pq9p;knw z;VY(~8_vG2G^Fahm88il%_G_?UyP%+?zwhmKRN(CJ57dA3<1h@ofIhr905N1! zU+Z4Zhp?v((}_o_MFwnPOc@ahp&$ly@Byg&@QGr&;Puy}!P=*XYr(lleyS46toC(X zo6R2_>)@k+1UX~o!{kXSOVs-{Ku7wr1gw`c=sToV05Mwq8eynPU0}j^uCy}Z=%C=%mZ;@^uv(5Cs8y<+^xM$!q%P?vyR++BdmqObM^PaFsQl@e0x{re-j zNnfgNV?{@k5z7kzTUhn4%R9J-{<`lnud7e=^h@Qo`6Q*LI~BJS?lN@rk>n=^#y`Uo z&C`4ktzi2|+HZ8aX?e@;ZKgtSymBKQ3{QLRP5qNSfVXiOjf_1=Tt-!Xu zc@y)-TH2IBe^ptCKnwn+TJHq3F)?7t@bO|v8jbO=)c4u!XmyvFP*+3$;&(DkROoCh zVTNKO*HK(5eaFTcn;pIE610owb}WP`S61{-J}OCk`P>HWyt*Bvb}@NT_sszaLnK3- zyuu$$k=e`4w$GJQt`%U_tPX9#ofiuHPfG1G!Nx#cSI*YTz(ZKGr zWeylO&-;J5#POVKsHQHqvmROE%0UWrBO&?^nS+VFTger@Pzh|};g1R8BhTjxb3|el zAYC1gGr#MuJdu4@u;ZR0zRwp;f(xp6DsY7A0&bq8G$go{vk3zw+4zdAhQh}*q0g9w zE6`~ki(ll;RKii|qUEB8J}~f0U|@%dciG|((KZsuW*_Znc$JhX#E9kq@ucmWIb97D}pE9gRX&p6Wh}L+6?;xP(*1E`8Xhzbq=4qd*uZ& z=d4FMdlXBwCi+WcuP*L-0U{g&ytA{8P!g2HlV@;ivjUKnt?HA3wYfKBwoo*~B_U-+ z%cUci%|VNVV?>vE%Z4r2|mA;b&mcS|r<( z<>^xZ*E1h(o#FdW^R^Uznz(7pSI83g6lPD-%bSQp0my;*U}D+4ROC9sOBNC6tND7e z$~SJ8Lr_e~mFJMHeWkpD!aLYyI-+1W(a07(KY{qalsk8;ii5fG0i|DU8|@bx6|^ne zRhWnTf>Zs}gK~mfroX$Gz@c)_h~1>J$PN?Wp2pEIbFS))ubxDrIQ-A8VQ&h_YfZqu&O>cF?g7O_rK{L3vCq7QK8O-lSe&>43%I|uVB zuFB3g2<*K+kJGgi-Hggk(QQBseL{q)a0I(WL8p0tWviGNY{5>XGwX%dl+sSF24h2m z0PEQOO!5d~l_D@^W<2>g$2MM$uQ3GW0L#y;$UYCsck6eHKumL4{lc59#6h{c_+Ne% z&$DEu@ovj?=V7Gr%xGCp;gLK%xj0+gzF4EgVp%keXe_(2ry=JFlVD1>h+G6T2}WBgs-;&D%4(&1hrW{db%mfu%4R|JQvPhzAzEiYLw9@&V{(N8ruu=q};Vp`~U z!vmSVMbMvI86%S^-3(v8A$kH9H-cijw*X_8WckLj_sipmvI*^I-NG|~cbG7?wSj0% zg}|A%HpjMh5RI8JrYZMP0S0T7J1TfS)KrFDMK+IDl=wEqZqn5f@n-{?-7!|VU=SkbG;>#+|vi_ z2J>e9;hVyjdGC-BzYeY`Q^jhe-b9~k*dSMVG)_WI3S+nC%1`Qt#Q-PiZ0SY}3{+09x~q*^??TK@cPaVn?_gDZkm6 zZ~&6JqP9l{gx20n1^WHx$J)q3eU@cPpC9L~2*U2*-KP4~fy|>rx}Sk1B1i`yBgELu zp%P{@PF9>r?qjbR2HGM3#jeX|YRjRD08a^QoN+-+&GPRp>E;+EPMX_B(YmvOmCo#` zO;$bivjk^@$qC_!yEs2H3@gr)AkV5=Ztmk?D(uLW!4(yj9FnfuMEY8|X z?mWjesy4s+7v+#kJ^YMRA1g4t%$4i|FeRnxWMSCY%@>+UapBK&GV4kNFY>*EZgy*{ zdBfC5PXN8^hg_IHkzVKBg1R+u{bSzw(ZR5C^>AP!(u_*Y?zQF2n|NU3r*1-`PUJA* z??G-a#?c>u!ikQ1yY*8#W12ck9dyU|W?B`?Qm$GYsm2?jl01@l#P9pw z&7G<#yYSS3D+hdfJLqU*|9ofjZL#NLC{(0<&I38!t z7L?OMWVAN=+w!HeBOfG-D7%BS3m6^ILHngmwzh%r3C4k*g$j_ z9_H94xK*Vl)J&rj#OR;6)ZXIxKW+X{qGX9SB4eDXXJD6!mfEktyXT55YX_^*TN26~U9Au@TAkGN(|GWk z4W6T;{ihIUmvYyIna(zpi^O}gRg@GDn0L;nH*odi(VQk{%lftN)cd4MtDe?Z$dV5M zN<9yJ-BB^i$30Uc)uq%Jq+STJUK>UX=>SB-mEhg*P~mrvC5!pDakuz|{PoARIpD$! z@1ym4l@QHJUC<3cTNFA#jIQ5l@pX{TiA6NN*Mnr1glc&;!t(A#3!l@8*Dt;`Ze|pH z05Sm-P%Q@a7#mmf)X$1}NHj|d?(5>9%7???Ieq^+c-rq ztp{N@IqIPge(N>;*%V@YF+nJ{aapz5&jvu^)~i&Z)3N0Kf?dB%;ImK+sp2TAwB_?) zN_vW&b)J+s)%5IVjGi{7W3fsVc%-xNaK=iAP_^0t zPr1yF%?9>LREJ5n>t%5*2>*FoyL?vs&7}RImrz2bh`W)N2+P{h+x;EPQ-*9U_KbmP zSu!3wcDl)Nh58XW@Hf2>;;A=|ID4)Ag&2fkIbxDgRQ`m!nDDM@_HN&dMU^NAQpEn% zRU2?yYcQWemrsGXOxAt`b~7{8xv$G?6$A4##|>ZPf5<9vKZ67_rtg#2p>CB_%JlZ> zX+JWRXdRuukhV6YS!}kIt+5XmGsUgXCe!nyjeXv?BGA_3d$fsSDtyGQ7hDo>sihan zvz)UPel*4f!Pumn!>=^VikVOi5a9^=slB60r>GwWx|%=CY~CkzrX=XxgSf9}W>JVw zQg3s`+*8B=uog{%<=3(Fv>GHwIt)ddt24vBv^XpfXcgm9|cMUaML0!U%WnO1G7`3GLiLw5upT#>Y=4r zfY}JAQgk^*aO=H|Gm$3??XP_5&bH4gDSHc+;0l?*jYXCA8No;Clpj*nW*!R*T!XIW z^?3z-ew$J?`^G>1(5B^4F4*txTmFK41Q*jTrMnobrqK(`=NvG9T^Je^lU1YgRPumBzrE(V$``U+$gvt|B zBgPM5gCgJs;aMJp)ye*%`_Qa(1M#fsfv((^_$x`ziymrwRF%ES?acUDnc$VtYHGHE z{Ajh_L-5fLlz~hH9B~7#zc=5VZhepNl0YXC5_VE;1Ug|-_9}lr8C}|DO|u1@Xrcxp zel?3FCHpeg*WZv*z~PP-z=?i4nXIKpx=~RQt~t%=m7#P7YXbIeW7YFvh@gQzJyj|> zd8I8|oR*$^5@$vAExOSKxc-D!Uu-Sidou8^ z|I0D}pEITQ4hITRZhl**Gu~4apjq zvES_I4)fRxhY>Xh_x0q?<_n(sW;!F9mtG~l@bOC+pcFL!eT9_s?VmF@?%J7Y;aiuj z?WjJuTg-C_M^!WEF}u2g-=7oeD5W4S@gND1xJq^d)n>gFIcOJq0t&Y5)aV;H#IWDM zSQ}L95`Ra{oo{ZOx~lf2w|tkl{J^qP|S zk17@c6yf`^oIRX2Eo{oFJ{Q3G~ zR@Gy3Mqw+>s6+|m>8XkktB}tbZY=LH)QSYAF&d+?`Jr!(h^~{>Vmt$mZ!b!ZedAip z9<6PKOlf$h+AUNRbzIUgN_hFVZ9A^(kc(={#(eqQ>BFC0nHv+MGtg`Qm1b=5yzQz( z%uM)*6t4dQ3f94zuyTd%r^xVWXd`MdF@wU^%2CjBp4yRI>iYe`*2M%i5svjuEGzg` zeO1_^5HE_>H^IToEi~Ef*xQ_(^cuntJ|*O^)%d-Cb~N+Hz2Dz<;_pr?x%cQA;Z|m7+5_4UH9fB2oPR?H;o%4+m5XdPNUyLy}4a8s4 zFtPso!9B=`h4jit9XUmgXU2qFtkBTgk{cHDmMddwQUw?Ef^^HZSFAe6yBa!o(Z_`V`DJieS zXf1}56O}kIH17f30Q&Lwg9d*j5AbNVX^H4#PBq;-08JUV=}0^9&1)i~aO)C0W{;0H zIW;OM>=?UfBQ#7NxUd!GM;2H?YDqPvnJ@Sq}fjf+77~t)LDI6?h6k38{ zsCVHWbt@1p5`nain$>oyKPj|%HoOlzy<%1A*DLJYP4zq-d$Ei~=kmPP`*eU}u8DtN zjP~D^Q}}WJ_qrhe;d8&C37iK|#066!F>?!m2#3i>8f3erH?!hzj zSGJ8vJgr34J{|6(#x9@_&i$*HB}svgUz72Vx$JyKz)cH2>v64l9dkyTA6uAZ_IdKk zssdN8v+-R8i+#tRtC(A1=K_4O7(OswYiXWE~syF_Bw89Q9{ z5(9>3Ktr@Xo>0T@h4i?RkbY%qKLd~NIy$W%JS#hm*C}=hVz$ubazzMlVc^$xiMJhw@xbCQYuljr_JSH zOk*y-Kd-zre>({-uTrut8BtnAc_PH;h zW3>xXsXmJ|b@vy_&}Y66V|e4q7}kiy+WI;AGHqXF){&6M=^$5BAa1Uu4}p~2;6D;G z{j0~~!YnY*c~E9VDSiOUs}p#aju`Jmd&o*iW|z8%W)-vT_$D|7#D*f?ely;1zK{i{ zRF#{}nB=boM{Ej4l3u#p8O)Kl6Noccom7)q@g=wuGH&igN-A{GugKSGVp;0=w0szG z^PDS^2Jch0n!h9w5@f+&D!czvK-rIV{B>L?U?Dm!BVh{{3U`TfGO|L0_+xsaJd6ep zkUhzm0Uu zq|rrkVgjzE|Gy1pL4T=hi7SQgJRjzXuq7YS_aw94Z}))L*3V_QF-wi6yR)9~&^Dhb zEOvH1m+x=6cHGSW0Hicq%6vhQyU56z{^?BDDC@2sGzT_T%PSswb2MRr3=VSjg|u9M zljkAT2gKh^?Vormq`3XGZ;;`cm$#x%wI$NF2>_4#QQ{a=xtmRuSv{u4i%PyJk;t%* zvX`_k{gA=Gjtu@M?iF`2E6{o4^E--@CCjCDpmI>!X43{ZUe?%#bLl~PX61-&)%xaq zl3C}|bzKRH!etYQxLo)XQ0QjMe&hI|3$HmnRsV{cs`gfsjA2<-Q89PBu&|diAPZ>I zmuFoK6=rgS>T)&=c_Io{&oxk{OY^ZTDb6u9R*6Zd61M#pA3)t47-~h`GbF-9v|^NF zG-U1mf_9G8D6y<8L0i%-U|ans$*e3*4Lu9R+ECTlSU;?h;S7tUTOHDiS<84d1Ks8H zzUx*P{|eWK0}!!a#2HoXd~}I<)Ir6G(3mqHmgwcNbq8>dDEj+W)|pL&`uHEM+(WAx zgbjp*N`!NexT66R#`+f%rY7%Ah)PO9COzQp&}aCE_Uhj@UiHJ*@vr0w8Yy>3we_8A zo?6`mbQ$_e>_ux1@=ptC3;Hp-5hy^ryv?kf7nWqHc)^*pwaOF(6_%_erkCKB`R}b| zUO50&3`4vx0iY+jahht;hoLV(@AXuN))j=83((7g^6s83YgE>i!o?CjMvKWaZ?vrl zlYB#AYUA*?#f1EopQ=f@)IZfTM`D%06Sw2EXK3V*oaon|e#eQ_KPSiHALVk$S;^j9 z0?TWvg7SU4E*MG0KE0fVbE;3dpH08gs^Ms1UCa+@Q#Q^kPq{|EB<{~%=_hZL_)w*N zf>q*z7jwLpKF#6|{j*chQHQaY&N@wUqO9rk9$TUk^2VtXrxP_UL5H=T0;e8_Le27q zu%vVKZ}Y$2@J@U3+MrfNu%`7*LPttO27P#xUThm+@8J44fkZm#e|w_+BVzuz{;y@` zKZpLOS^f)w2=CU*R+w!w9Dp5u7dLi z3_iZBRpZ&|kL3}(Wo4c(dfo{}5-n*-gWL~TgN!IMa?W>pPec!FG*2t4i#uSl-)Tjv zV(%0Yz{Xd614Fugihh0AbO($Lu{7qzNi}Un2kf|ErBnGX4o}BkzQ2$I6g`d2oIS3^ z_icrVdPbL+E3_e}0{J%!`AFTpECkT1-*6-rBcCu^L8jl?_t9mX3tq`QH?#a4s?pL! zC`|E^&wX#(k|MsPZirc!(Po;9-qz(E3(;^XE2M>v`BTgo`w_5AlS6~vr>Ez36FZVL zD`FzhGr3E(2+?s=_ySE9y~8Z|UB1w36?}`i=rsu_6+T1~&66yB9KPtyUObPL^a^QT z4CQRlwf4BVWDu^Ys&eC!?#X9=Qq8WvU%38%pN;v0R{w-{S|7b7Dvn%TCNE5DU(!-R zt0&gYHZW8P9JS&^)QEuVt*|C?&(f1sm@`OYK{*(fScAv_R`8?Ys|@qm$mFw`a)J_a zLk$IzCmK{Plh;U}NpQn6IedCC#~R{_9zMXH@!6j+DQJ*3QT5i05`LWX zuJZY~_2oz)1PkG{OWC{zICIVaC5emN=*A3Nc$%==al|PRXhc3LjYh7LwmS|w3MdwX z)_m4#E@?GON4!yrQg0uN0qz$0LJR4o^H$YN`_r8*Q^WKAI$~15DTB{R`cjdXtir|1 zkCDhaEm~LTmmJTE+1R>H`ll19w=boQ^H2f@2jW!uR)Q(gsnQ}aFcaxnEpowI;)^N3 zccTM;o+Yd~un@?|s%dW=abIA0-I}%)>U$C;pY- z`p3I}7f?6SGskDk?0qPrZpKq`XNbsKb6ncGa}7ViMms`7<%;t+ zFI$G}RcwbXSZm+}&h2hsSbmFEIiA&?wBi=9$r{>Dp*qoU0x>9)2AF(x*;cOMd=t0# z3FG9_=^oe}fpPDor%RtjNIv+1jTfw2g|{S8o$#{MN5gY*bcKuSdMSfMf4$vTy-z)O zpY1Sw4QPmWUgehO+}O(|k-=Zj8nZp(z9_FJYaugfBtNMgbsMM^e#-rfSontCh3JK3 z=4Qfb?rFRBVasyaM9%d5i1hzozUqjGw^8?wRpzdP71pBVwZv9<-vOUO`ymT{}B! z;Ujl@cO7$)Xb?J(;9zRaSY)&&!TqA@W9Ri?KQqz^mo8>-VU1JAibpvUN5r(z2Nbz4 znH(GHjZtBraQ1yFnx5?cywaAR?*_fM5#z19T{G%>;0Jcoj#tGs3fT)8lI5@5-QWN# znJCR_gicVTpY;a{TY=lSi{rJdPQ6pSsmK=P&DN_SYpwqWg*pERq5mJDupv)Kny9{bntKdz?!rfnHERm+{nHNQppnNOF#BY7!mt zJa7QK?9obSS~t)>ZduZ_sKllR*B>vRGa_H&UHz*Hd_lNDF&9FQD2^mU`mTQ55b!c> zLdm*2-r3EaLTu?XB-UUgOlcd}-8nEKN3l}q6L$kQ`k0r&%xJeJMvBVl&x%Z;vd&Ik zfm@e>j@I&5pCUB$SXU(gud=~$74iZrPT*nd4U30s8@iR ze~~DnCTLd|S;DMo%vQw^qjMjB({is|1Mwk=xig=wM{$cy?f~Te45g`Nt&+$rdr9Zi z?+^B;`=Y$aq*eaC&J*lQf`;6$hzw8s93l=t)zPH9>TTpl$%RAM36x)OzgBLc9z74& zFeasPN|7=6@M=l!{L~xOj+IvxBwwV>sjpt@*No+2)HyzG*-s4(=4%=>2+{9b1XlXB zN$ZVaXEMa3)Vp&zmm#3p7>HDqas4=Wk274h+mae;yoVM@N znqD~*oCMKMe-#Du6ttl_f8jM}9+tJ)Wu@Iue>O?ha(Z;BBx(R4Y2?cv?)iIv1d{nN z#$ON-EAertgA(sifKEcdm--%`vCyhMPIK+jYa}8ZodzuYRhKe;Od1m<4`Y`vj&wA?-llwq~ z(&xbeNOYPLrJtK_LSJ0*s^Jbgjc?iV80Ki&f-YTyClS{$rh|p1m*{`RZo8N=ain8I zR_c-TYTXz1VN#PJ2qd1mCm1V6N0S3mxpwU7F$PIIaB5!y_$t(O*cG~;9XXX6A+9Ml z$oxr-Tzc0loo}-XsK!D$wZ3IjEa!34dpdaw;!viKj2rIg4Z%^<3~fC(Yu7vgeNq8x zQHWcUg0}m4W%UTKGhgp7)=#h(AAnM|$TFnIK)KSyF0waJ6yynzyrGMfELN9@QALq2 z!qs-BpupLxX!1p0vz@6sz`vC+&fSiCn;N37lU`y*Ejvit)%$*EBk~>)W7LBvDigtC z;a~61cOengHajgqoy?prBs=-a<0gCv=8jIB!XR&y;GD>y^Mb(`3^+5F3X~Qavo4O~ zPWPx(dTl7SxeA%sE0l&FBSUDsg47+tUac#QkJ?(&dou?;(U<*XnKKU6w=NpGps186AV_aPP#{52P^5QANP$qK3!zAFR=R}HdqAXwl8{7tM*-;& zl#l>Y6zR&?YI}gwY9OLBV;^a8S#l^+VeT?UXAn%Fe$4?0J3!D-Z6A?czCL$^- zA)_KMA$37oR8--z;)P49U@%x*UQ0()Of9eaJ!mW@RUz{euvA0U2opByazY=HmKSXkNEIXJnFaq}EM>d<@|z{&pd!NJac)cfAibpSgb2fw814Nd{5E0lD+rZU)ip#?Lt|5OOKV$uN8gM7fx)5Skx}{-gE>7j`+Dx(^2+Mk z`o`wg_TK)%=ff{wzkUDl4_qt&wtse;pg z*;%GGwAtdHW4f>pjcHD;7JPOK8$}w*S9m7%2fXHsSx~v(D9aP}lKU!akmLt^64Of( zlv|6XS`=AjUiWgX3YG&UU+afBxE{c{#m_|)^#l>;E))NrY^f(5=j2g;w>jkw=k6Yq ztA-jhRVGZjTE}dCaPn0o`Rs7?hiYlNMkH+r*m_$5fV%2aqGT^hnx+~X*pm;Q<|FRz z;VWc&*(j*XT#zCL7LOkc8_+x8pU)WOq>lYt6Ae*x*mgTlP+TP{(bu900rd zo_f}sdS-ejK#SrXCdxt!4pz_Y*+qs)Q?$)^LsT*Le?XmLpL8}?)q^ySd*xa})0Qsn zz_@#KyjP`jxC#d!r1bGkk!Ux7qNks-qZ``LJV|s$iK;p0dw<)}tKHJe<;HqzyrsK9M%VRjh@A$n zWoU>b=^M4iU<+p&fkQ+Udl7mEP=!|8iw190&v#)neI8vyXBu7=vCGAH)*vfw4IdW3 zN9eV2u-YKF(kZzDOAccElw1!}xLUwcu7Ip{3W8C=6lg+%*@{s*2A(pGI7GYq?_#^?kttUC@H0ckJP+<_Oxp)Ye!Hxh&(MQ(n18ib5hhUA_#S0GW(;zn=n zB~5mOyNS%4uLx=Bb-Q2%P*RKhC_rlm9sd3~uz#y%oPg}aWx}*zuZIfh z9Yk9%m^w~|=b`Q!FJFuafHelAu>oY1?s`oE3GCSEriUK|-(Q)%w1sYIS(z9FiH1p; z>#4K!R2%bgHsfbqf-Fu0qHWi-=!}@t%S9J%0mIx`Gb`9_*RDv7o4RR{E}QSq+_=Ez zv)mBiwquwYmFD&C@R{GeElw)g-cnyYSeE+;w6{Mnvwv>EW@7CLA}A} zvH*BlpsFc0&kMwpuPHbgW09rHp>H_Ao7q!?i}N8hF1Twmg>UP{(Yz)}dT*^R`95jS zP#4Mi@>OfSVSEi=k;6RWE{tKcTn)0X<3jz$(EHBO5e^ra+HT=hq2UZe%W4%Ly z96Mm%Dh$b&9H|Xycf(K@@Krv1f^hPsoNvoCd-Hq0+%g+7@uk_eLjmKyrr=-UwHb4u zFIDlcpYUd{B5@ixRg`qHp;al*q_9bhizJAT`z`9AL~-Zzj)fGqT%e_b?=-WzS&tcf zHq2b#n@P_HAiw(=+jWF%U>P~fS7?QeXYB&rF$eDr%}spUm_iwS?Naj0>o;8Tb*mh9%?0nelD|e9UD9h z!q!9FwDE*nQrnu<>R3uvIiCJ%oRHM%ReK#Qtav#VBb;sE$z28Zz&2d+*)GK=nx<-V zK_Ibw3bkKPpMZz&z0ZITVAN~rL!(N8diK7@&F``}DR;7#ca(DId-o%*7RlRKhGNbF z(T4(Kkk(6+YuBXXW0`|`@s3X@(c;Nc3pDKVN~_dTaReEpmf2a2GXV8xad4J5pK~L9 z4PFvi&SDUtoq=>@B5p~?%~B5ibSuO^nyJj~y)`n1FlP=E%lud2`FsJ+xws6ni{I7l z1qD;VjW}L9WzH}Q1e47xZFP&S{;AG}qAVkBShi&fbYt|-iGP^EWAF4^HN);OZ32Bo zwm&rnVmwT#Ux$D3P-GrXt4;JaU`AiIg;7dBtLLp|wbkSVKt$7R(W$#ia);pLfH$0S zO=lLdZWQ}D)(Y(NFM$=6{#<2~0*~()ae^w!bcFZdF8N|6kqu=#UdoqZh{{o)m{sp{3KRx*uaEG7x%llwdp}f1IO`#kM_;34fAN>FAf%g&EKvy{X zn?tEF{D#Fy`6pLvUIgEPts)Itl$+d3%nSy#)j~tbULNZv_3LD#wyHspb8Nlb*9%5J zF+rEbd^Ja~(97Nar`OsW1q;!M_c6z#2H!N9U-`joEmO9`Jbme)VNS0z?EAQb{(?F_ z1Q~(~HzEy2rxD{$zP2=^bFQY9taubX0GTEe5*=PLr8Fx!z8X`=pX&s8MxWw9&R+a2nWlA#_qUnuahQF$- zdKH}g=v$>CHrHeTGV9jp9WIi@NgljGy84NEA^5=7rT&4pR7IAmxoBZ^{cx393RN-Y z4ugJ3fbLH<_sxh1Q8b-^AE6@%54m(N^B5vtj zF-8|YRee+cKKZ!05lUch2o=%We5B7!U|>uMuh1konUZljQY0T_Z4DngiQVsUsO zq)i9oXfQE-{L>xgw-3D^9X-1zSMZF)-K7xQ;Tw#hz+*e6+HWtS2NOTOd7?alqCog3 zd4CTwpu%u9i)oo76vhU{MwW8LS@B}i!Sq=$i^jGd^W?S@qp!b^K)VcX z;)v5`c7|`kizs#p_4cO1rXqW-f(LRSEG=d_v-&GpmQC28d&4#_nkI0oThrX`iWh%O z`IHC3v_V@l#OO^pV_>z~Mlm}KTbe9G1qpNUcr{kM>m zYmOv%m5}8>ZeD;`vfeXo^zZyMwL@HQ0n6*s`=C zjz=u8P=t)$SvH#I%BM>;Y@~B`C`ye^JB{l^k_jVz{r7pz`IaAFxI1-rO|U{LDyg0{ z5k)LmFKi4VVOHAohQb17OLDVMXn&^o-pt^UHe2>;J%_1vF~vvbuaO!@(tN3cCiOBC z+`wz^R*^T_78WsH?fsfGzpYIb+furtKcvE6?RPDZwiRKgPeV6#(>|!bsSjin8tXTD zvGY7yCQ$};*9oUG%`?`6>gBdCvq4n}yfpJ<<=NaG1z$Dq25Vqtr;?4D2SfFazS#RC z?qlN8yM@AofT}jL45~RRfWz5F6`omd%N@A`9Y-YFKdc!qh4a%s!q?h|qO~5iRK-~V zb`bLIO9%FJ=ap_-{DIbboJqNF3e_5td?y-FkGKC2Yz;|`lDt(uIBBr&c=n@((R!_P zG-KE&INx1QI0$J_NZuT&4f4uY7_u+{TIpFj6@6eD0eu8Bp)+6^Er_Z!#(*{P+^IyK zZgz=FnlKcSH(h99Q0AH@9l@JFuO8L$JbZ$D#c}Ccd=df zJxTN`mQoRgu7aAbUn2J3NLKwKiQ^ur9Bp5T6>Z-;rp?-k6<%_sy^T(5Clx_DH{a-B6pL`OkGCd$)>!|Npue|Jr~ zkm+yHk6-twdo8I+{SaovS@&_pkR6-&0evZHxPZJfw8Y#=>eVFI=Fx7ALZ3+AWxCOw zaG*vU-tZnyDZq)#y?M$Z($<0Z?<5=Y53e7HG|uufW&Gd_lSE$=uKY{` z;Q^GXc?~Dou&1gdel`Th{H=2>2od{%a#f-32D!LdMc&klwX}NC0>}$DZIMS;L`CFE zLnbXZF@=JyQEuv{!bfACD2!&*zqnNHcTSn|3`ue{ZiRHz^X3*t7UL(b7Ph!ItS%E; zn_najTpxA&t%Q8TvWCM>`|8=2eDp5K^vC;wtue=NaS6hVB`6RC-49>P@mnxNU`1v@i!~q z8GBood1D7opD!)WL=s1KlY`BPde~_vVSU~M{_Y+}k@_>)0cslB@t;Swz9UEi9v$^Y zyuj%jWm-b%)^4Z5^&Z%w%03-_a+GZttbWqPwNG)6E0UbSYQ`tpbHXmWPJsvvqI-y3 zc;7##n~5wD?!12A4z+3eC)ZW`;_si&d*uHDoSV~F{gRT60eGjWEFMx2v*_uX^`GIk zKfi7k{sr)S27jW}{5-IDtH<@3IP+pI`q21x?UOAuxjkdgxHwg4$t&>G-HS)Vl;G9a z4#xYVK`Xy}>1Pe6@+%wu$UNDxRe9{utt|P=Mr6`+Gaq{k|G>lHFW~;o@dpVCbmd;I z(4^VSms`%o%KkGSdQbn5AhLpfGubvl&&v4kH@s9Av25Q87}~=`PP*wR%Uu7;j79(- zK$AWv944O+00VU{c>TiX|Ka-!_!0Qz&-duh(F#9OuWudS;n&{y!?$v@mB0T5e9R~O z1;j@lG5c!|ey;6IZdM@(LMygu_6@HYQI{GIU^!{=Yuehqy?5e1EY zj#h7j)O`cqM!%6hyaE0FgzvX!33^T~@9HgyaE=g5f8p52jjW3?jy#8)Q+^H;TeIM4nCJjDG4fKr#Uw4)x4l@ukaJ7@ekd%ZXO8t%#O zXzCx4uA7}Q%HJ}7D=E6jrPvjeL>Wd1L`HvJ8E>)&Y>nb8t|c02&vQ9O9Me7O;wqPd zSe`&)7qd-Uyq2!GxIBC?SbF8sLXZ|)3+noGL+H_UH;+lw6SH<`X8rG+zkpQTFDQjT z*Wd@Qh5sOm;`25uWqxEp&2GSMu=DkDW)Vuuu?m3yV#XGzO0yP*$Y+%opXTY67BXDL*$D}5t0Z3JB^*6!JbH57)^LD7fd|C>mNm0# znKUNn3%@ORL#CK{HEEf$bcU2)$}-?9U{@Gyo=7asBewruv*v{w;m7tFx0P?SEvGdV zBp;6`yUAH5GnJhUdqwrWA&XudfF@Gd227}G`j*HZLdKNV0GuZUIDzR@vD8ZOdMxrm zdpb4E!o*$XGv$t5n%#fZyIC~a(|x;H}$+;X)8zRt>Jw#gfN zGPW#>5sj>`3GFM3cb6&))uQts#VQqPgp~Q)-g|r5 ztnrc4H9w@9hemLtygasAp0hPfD1&Y7kX*Cmb-MsAXE++AwS|;)TC6p&FLlg>Ho2Yx zHi-=ud@BQ9V@+#5VPO;kuWSnnY1KxBfxR3x2LcN< zZBM;sTBnMCdQ1Wz>-O_E(KI2omb)$qpEVkM`^Q&uN>m}II?69?K6SmpqwR6On#W0B zBk9cP_EYb9sb!V{`r#IUINFK6KuEzU^sVR$u@|k;b-2N!y(5=mPJfI|Rew_%z!5TP zbix&C^NfSv#LzYiNsY3g%M8>-zmoY%GMvS$9cC9!@<$Pt8$NhRLkKl?AvV^2g!2lv za&rZ03!+SWdAX+x^ACrxgjd)}i)n=l83KbN;sE6t&x4cT!7x%87GJOWOlrvyLODp@ zuIRfj>fP8UK(kKe6$!evC$6C#`x}JMVX2G;)uW@YugSKL*h4%5L!IyePWUv)cn`^1 zWi3tL^}~c=j6DAWnqqEr_mJ$X>V>LOk}u3Gh>W#$D3@UD&6XQCTxsYlQjg>^>#Q&- zP8z%L53e;V(-wtRu}zqa;ZPtDF@OUy^J zt(+y3wLB`fLrU6gvj?Ca>mBD(T%vhscWgT=^|jVVK}mG-yaC!q74_aPf-ln{ufx%u z>o!DjJ49!+r6rOLI~~hg8iZ2b?GeT1*)avDTSdhDdV|o(n%s^bixF)Q4@ZrO=3;_( zW~I*XA-QO*lw^f@{CVdV(9&QFa;y2DPVp*=g@jW%{@}8uvBW*NZ9(XKK1J!@em-BF+8NH00GxRizIFA`G z+E?6)v85_$oz~0TU9Uu~=MK-?#2vtO5;KXu2Ye2BBMF@KpVvUg_;N z%5=Up*PI!mV!GaRv7knidf!KA1iMA}tPqvGPPwOHUFa(X;S#1eSSSjS3JMwhH_~br zhT2ol9Eh^mW}L%63eZ>5_dW$tdzj>}Vs8vYo3@B%J85*Vr|^&SSwrF;L>1%cZ?`2_ z*-+XCUG!oRT2JR$n?i9&vq!i~CLFxu-*X1oxJHuoR7e}BS5U&g`y*#2{-S03`o{YQ zpSh@ykH}k6Wzb*1=@$=Nbgh5=zW68h^QQ6FnvMtj+H|bKQri8t%8{sB>Nv;ieu z&G1Ir0>1Hl=CQ%A;nGx1&dzUlue+XoW1R69(DL>j_BpV!OMJUDRflQhTO}O5?S8>_ zGf?A)@Pk8*6Fpx+CckJH{de>00Kqg_F}BOrV~|;>lWL!yoK;uh#L&C@`-}H^o=j7Q z-i-3jAUcgU~p(Q=7CP|asH!EF!&_L_hG zPJ$p5F7fYVaV+VA!6!^b>iz<}y+|X3S@W+HckvIY&qt|~u;r3XpXT~>`NZ8PAHelb zyoP^cil3bg`a@c?Zd6eslsASdK1%>)Iaz5uIQfNHuE1OGBdL%zQ+`fJ#vC)SzX7$cc5cZ$Vi`ky;r&nt0T4ScqI5XAk zexkr0Q6I{(#o9mDW;#4EW2C*9qYSyfcNi!zL#fdhRw`9jl;%1(8doY&__2~4!ua@E z&hjBhErfa%A)mrk>GqmnFCvE-)#X(^%SjnD0!>|Umn>)S(JQiE&NaFB3NDw@Z^Md= z&b_{4Ye;>=Y;)r*_vWhP4EDkpWAmHsFRe6R9wmDjj;=0O+#0a=jwF5LqedtR5)9}f zxGc;~g&($gtap$Zg-ssF-W~XDYhc5t)7?s-59&1EagRBDA{PjcB4Z z6lV?1v}D8Aiv%G8*CmzAD)0rBmjifN#;t*N?-47tY?bE6Gc~J0zjDZm314tFSPNfy zh>R)C@VOkhN6k>9&*FJNLG5N2;!fxmYEqLtueClryi7EoZgL7#x)#ukD|{&_Rjaj3 z8pPb)vu~8sJdg2MCnc^|r`E&FFZfq;)bz)eP!;@!Gh{W5O~@eKzRJ3`sq==$Umjo@ z2m&yGV#rxxLCeeGZ8}X(DDmjT`tpbW|osMB%v*CF_OOU047Cx%Vau948+vS8CN_##fl`iB++Xu zp4kte3(E_+1^W~t2GqxC7=kxJ678x}uo75({B&%C-$*Ck zrjOTtzvTqe0eQjD$NZd7FL@!rT6shY2S%``)Zy{Gy+P1L#p9@Bvuy@1JO2XUmLX?v zs^d4Qv)|>~iE+Zb0ht|-WW-f4cFXJ{nWw*lXGesAJ|Z&40zw;JMZ>-)m|!Gr>G;laI#Qwmg<;TSj;ZMA%!Ng)b&lS z4zG2KtwJ%*GGxGHT$8l_95@CJ>F|ug6B|9IsIt8f)2a9WbV>jC@|*8M3B)~{JAqWr?P{yXMa2ZMSaoj7i9W;;UKWrC}-5hWcyyKwt+ z5GlMb>>|2;w)Gq$0YenCvo&#y$ISwrO@pj6+eolxu#eD*JU^zEC_XnvE zw`Om4#%i-zn>*GmQ=_ihR%+}bhRbx zbXfI-ce#W8Wr(Bj{?4^mXCPDoK8BWvjN8q7Q;whXW;i?UtO-FfMI~s(@P$&7YtFgO zk;V|JJB!j4mk1F-+`L+4%S0sk`--B|u@oiRtv7nAbfN`qxGxx^5`vORDFI!V*{BR9 zdV5!vl~r96^KF}|`pyyfMFBAo)^X=_d3{b?!Sg6No0+RYBam**3sv<2*}{ZE<$|wl zarRk<6yJoUX%8aJJ6V#Ga0=@+WpV?k%&)ULTs0W{dBT)fW?cY>luou+r5T0>)$z^f zxM(alTl}=8gftTk&VD!sNl-ku{eEpCgyz}!5wlg*#L7_4QbIGi2R!HW1`Ml=rA(*1!dq*kJ$>-%MF(B!8w1yDOa|=uiron2_l7d< zOdKox{UfSOsMnWMpXa`-_4*5_9uWVwGE?DTz^96R_e`bcykPUtufG8KKQXd`4<03P ze3Ty8EXN*N{Z|Y+{bxVy|MmR2h{v&{Joi^D*(gc+2bo?MY3FNSZ;p~wgI{wuwJO3$ zjdcjvcr-xGH~g(>{3RBVTPuJczc~WXkL!VmZx7M{V80 zla*CH4-&3(UUsNZTI*#1N5q*w!U=%^az3ZZf0Z{Y?d2jIPoTZa%j)G4E{PmZ4==ha&}s~_v8CT@T7 zfmLpqXi$HryP@G{XpB{PE6!akGu!`zAk@$%7OPOo`hU=lt;H(N_|;3Nf60eDzMiz} zHG=R?|H=EC7c&V}Fmfo*!OyPCe;OalW*=UeOq{Z7?T~q7+TM8W1J@yN6O0t->odxv z$V3_7ukigO+KGQ@r@^wV+SBF77P!K+UcQ^|_FA!MXdlU1RR6uU^d~nr=v};)#=((1 z#dGih)GPaTdW#2%B5<&_Qp8qKkAcLdX@unFfpI$Oxh8OTdsHh6&x6VIe7 zzPlw*4k;hM0$S+sAJ@OimJZkYeT50*$vP&Hb__DHJiSh=uFHWMN*=L$U5@`?J3lH3 z)DBEHOTCGIe7JnLMPjS9s& zRPl~5EYoDdR$6sE257~h0w9A-(t;cHOPInfFEL#Jhy+e#Ipf)E<8_!h~9%HPcJPutZ(}w z19A(mqiGdf{4&JkK})grA@8p~9N4-R%Q+jMD2D_-k!E>nJ%_zbtF zQn5-Z!m6m{?4uy+ORY0^>o8hN3(eZMAktOH`runvvsF{;kTf%bJ76**hqGpDAN(JJ5V*%vA2}dFMcMW^J8z}(ii{ahN}z-1XR0d)+S%hQ^jsoe0to210?N}$3F5d z8Y-|@8YE{a7zRd6$KUScd?KGjm1yQy!(-oY*-hM7uFF!*x&SU!)}^m9phGfKg2L5f z$8~32ZG{#KXFX{9(~zlmwN`7Na+5;j>}LEmMFxahckSS+CI)vbo%io9?7=;(Rqi!U z=OymxlJvcrIA-#1BVGe^F~#)raHK6*)`xdImz1(Lk3?j-tWKBS+iA!#^}KyO4o^-0 zxjRLD)sH!M^Nl%nJKkb!{XSDFgmyz)RusOm8ZT1Kzw7vLZ@T4kltIWX$f$8R%b>5> z#8+QdUU!8;Gx^AX-XVx+zGxUVT4}jI1a;!ObMu&{P}`}td@T~MJovjP{vi&(o^xWs zbR`GdD!Lj$I@avU1&oW4!#eIwXW8#7U8PCZTluJg1j9sx!R>P8y!bRDS>hyax;}Xl za6cD@SYm#$0uja@`g#hKNh=T0U`Co%&)-xJ(JT84rTXt-jNe%rilgs739t9=Gn8LT z)!VTRb2nM;P~0_BtRp~aW_<~45U)x76`-c5tvm3NDC{!M>;s6Lf?C9MQefaU(6R5t z6&#E&&I&nz7BD2>pc<@j^NZaYK5ETs>VDLhi9}#4PZjNe*%{Oc%=XaG)^D%Xn_hq! zo{g>}pGNv6LZ*1J(XyI%Hy|Ko)0`M-Vyx|CWcB; zEYePboQROY@0J{*XkHjTb9}2<*$#HXqNbd!jge_&j(8#w4WBenw+}f-uy0KF2Htzi z$2d*pFj>lk7y1aJaHbF=>0~8mhv1%KWJo{sNQZ8jfh1|`;%!ylD3*9binP{yN)hCg zjUGt#g<>Rvo?o*bp*7E_B0k4QWWR^?7YGqsX5gM>7=!Sw<|E;Te2ksx3uJfp=bCIW z+T+f=hL0-)7=z$uW2m5E+B*B>DuP>Ty?NCUOP-ANEtl{rxJOJq)r^UfrsF@Qn+c8Q zCau>(4P!yKmAEP``A=Y#OUE?Enrt_M1*euBdD8ADw$CxV;IF+1t*sXomMMg*Z!3Kr z>^W>^DW%{m)5D-R>~ykrGwIW%3DBmnvm#tsDs^p8aOPr-#kgf?emy_0i{pIO5O(xx zUU^7}^i>fDC!G>q6H1xK@lcAdvX0ToxowMSKB=kD#nK=rV0X(aPQ@>IN7*!Q@>OHA zLX%d0_Eyd(O|oW=3N$aO?mLk*W`w8z6gKu z=gCnDRySI~?9j~&T>>=}L^b&xF%iIj+yCqXm?3*(%j~QAFYonGv&S|mCZVyq>Za+4 zn|l<>b)zi(KgoW9(d}E`@o$8pj!30jTClRlI`nZ2S8ehA$G&>JQnJ^(lohoBn$(Gz z7obZsu)OOFR$LnnXTBh(<4)^6x)=0vW4fCs>VduJ)IhkFk`C(!=)ME_SkxHr^J?_# zmQT#C$jkiT>Q{Fwe)$Gt-zT2uJJ4D~T40ZRE9d*EN}c**jLyRcIbzi3A`uJQRI()}_`8#0hNjKzp1FIbw+@A^3%6lHVk7 z@CI>(eM;}Zc97$0qk)d=BGTPmXpTe5ja;=5VzwLMZ6Ncdwq>jR;ec)>hjX)1mk4k+ zL@femz@eG2|J5s{<-4IxTQS)fAI>}IWF@#H@-m7IVV+Xaw#t0aw9##D(Byo;O8@?P zxhCp05qMy4Ld)lZfD|D1B2`8Mp7%^Va~{F_BtRXeuWByr#^iZl zZbw>`Gc>mnzJ3%#G$SDW)h>)kqZ`W0^+0`@mU6JRw(`dVqDbPIgl&b_cy%kmVop$% z;vYNk;%rr6kgZGGiZ{i6recJh1!XyLm3eLi@hTf$q^J>s@|KekdC}3xV@mm6a9fkS zQ!}PG5PGB0yTzq5DvWE+=e+khG{^{HpZGpDAF_G$4@ zg!$7``d2yUG$|BDp7FM$}1*tg+gM&59`?38f~W=^?ZA-w->ZZLhT)yvB5_UN_l^j z(t*Q;Em3L6Ns9jAwu_~|oG7cWN3j8~>wvIY>=k71 z?L`*55&mK4VBRZQ$3+spgTEy~#?&=Z_EZe*-~c$^c~BseCasYO1{5dmEm~} z?YV~%^VzV$wwj)G0vC|r;m>J_o4)sYE)euO8;Er6T5pD3k(NqM38Lw^u@a<#=dLOE z)htRWs1CLl&WOP$P=)r8+FjUN zufK1>$-_RUsdnp69exqu<|jepKG+E#q48G<4WL&KHA#VcC}=IG%WRB>C{Mk}dIGK$ zaz##|E(sK5_JWnb##(Vdm;cqMVZ^tCTuR77V z+pt`jm8IM2BrmWSxCv6t(;Vu@EYk7%fx^evH>Hs*EU*^Q!SAdUB&X2v$1KpHPj?U0r zf}A{Yp3NV2 z<&oX4?0B<3*gDDo;4k31?_WTvPWUhzZbocRRqtEy!e7AD++V=vDb$VoU*v-B?`xBe zlmi}{|B(;-|N3*)w*2ro2woYx`6cOkF5GQ(H-63`hcvEnR^Z`i5&SPeAjlyctydVt zei8gQS$OEPIC{(P7x;Z*XSVmB<=_Wbb}Hj?zFsPkG+0w^dK3ewwUpF8@!*CN@JUiY zdd03*RfVd0Z{^g z4dB9~Ky4c^Ho%>`9axS9#s6m!*#ED2r9mCJo+qknj*@VQnWWu~kw-$}ze}Fv`a*60 zN9c6r_TB3dJjMNu1Am5JZaKam%oKi^Td)wWjXgB0ji^SyNq=i_{K4J87kktzZ3<)V z-fkib`-;08t-_&KA9QH}IievOi5n@(e#?kITkoFt%&12YjMstQbKG1msy|RgC~gJ6 zaJ`Pqmy<{;`1||)`H0d#vVWS9?V^rOg$+KtM|b{zJQgbU4IQ~$*aiCtq`8kmMKN5& z<5S$Ut0G3SUcmFbc1rk&+)Vt1n~FTDoK5~!64-KE;00qJ@@xKNhTY34{HDJb`$=D1 zQxSWJL(7aE8$Qu(IaH-L;g(|tOJ&+h@FA&{e*-MMkGZvQjhj^ND3TRy3PEu}ic{g1 z$Ev>8)|qG9^Q|Jr!X;+FlWIn7cNuZ`>T|2i;dZj4g-~UP%@qW#z3K`)IQNWA=yItN zYEiT1MCeq#WsVc3vC;<|#x{T+)Wr>@m3i-2mVc?iU1Lsh`Uogyte1O0$kVUi+jO@! zhZ@W&jLjKpnF4*@4d|hOG2r8}CU74FF9IIb zyQ%S+zh1aMvEH!g5>`!FdWn8lG zVBx!{B&#&$eO=IPNRkk--2VkWNcRqcfKudXiDs0M)Z#@l4ORD3>dyOk4+y@pQ#Dc; z5UOIq{^JuMr?6#?Hv>2^0t5@Ha=|Qe?+}&gA?t)$#!`~#D>)-^mjM&9C|u28sPclz zm<3XzbIQGg{SpxOMzGbVL6=(5Y_M&*#FUdu3*YH$S!ZiHvD{U0^Aj!!=4TYXLGNA* z*2FxGsCHQBe-Pf89*sesRa_#zYl%78FaL=efuzUF7MyxQ@BZMVrX;a!Ao`hl7V)XS z^{l_<&BYRCc?LPkvOR-UMu^mESe!cqT-<(h-i13=HLVLJe?iy6ReD|UgA&kB3NDIo z4TM(oVA344q@2~^cZn7SGOv_)l-V>wkQGoNQAS1|LaXMnxhRhXuaXan4d1%%frOvQ zis{rwG-Zn`lB>)w91`7uqIVfRZtWm-tuuV}uq0Ahz4a-_EH8`Z&b1nGJlme`dg(|i zYYvX5&=yP)2Wn3%UlsK5vt6$w~Lunsgz*=F<;*)*}NCNc_^wd1w?zH|0Eo?M1NtQxtLD06?jB6OJ zvq5B80Z#0PAs|u@vzLxdUal5OQy>*CqQh(HM2_Ctz!s&*igL~_T^6JuQ%GiUz!6;) zP^h>CEomZ59S^aiom}0+wqTEupl!85*C8|+(<_G9%>3ilda9<1EWVI-LoIR6>Xy58 zB!Q7c;)V6^Ly##}Va6O4N&bm2S7b43ehX<(^CpHD+B&YSwaM%`L3)>_Whc}y9$u3# z$9yvBjR6;a*H(IRk}OZ_)Selxj{`o@0sYcgLe%poY*{P_GOA#1lu{ki>X}F#g;tj! zbVkC!?K7By8@C^VUO2=yeU^Y1C4xM>L%!yHldA#kLR!OFN#q>a1$G{dlgK&7!Z~G?VZiSvi%S7fCuwz>F@a+bX-> z$_YXr_^duRd+u5ld{W6EbO<(`?VlhFd^K=p67inWuN#CC?yE#YtX_EJNR{|*4y@@tx+fbl+d<(Zu5w}7{Qp8d0Q zY?pA(GUz7%^Hs;zzksLBM^!iHsY!c$m+t@2Zuy7oboakLq5HQ=LjQOA{R?UQ3+Vh; ztdVJc3Bg{k2?&GSpEt|109TLQwA%QkbjxZo<=MQQi1CMw)|FQ%sOP)kiWiHm1#cIe z1s|k_PxhLj8%wGrmD7B!=;!rY=nM6m6Ay@ga_RQpFyI$XBVB(1L7L)^1EgfU`2;~} zF-CxD6w#XZn6i}?GUw5|ScTIvP}0S696Sf=#CgL|(Dw9E+!OF$+2EJY)H%tGEThh^ zWPXM~1o6*SwB}#(hWVOaUpn=hHw5*=N=7M+%~<|x68j$=-e?_Tu&SexdmbcAr@*V7 z(1q&-|8X1QiErHXz~N`a)~z`-LJ1W zBm6|m>gTOG@-{9TqT8l6T1N&yi}ydDCe55nNol-nsdJ<_AYA%)Dr5h(B;)^GC&kt_ zQaM84f=CVF`-XTEarj6*pWYzr6C7%E;Wcry3^@)R547(ka3NC4vnpY|m}VmC)tFhF ziD5PLF_Z)#;V_<-ti7Ysfz-|@Q*ZJ(`SBByM{4`w&41aO9+2MGbSFN?$i+%h~tOi!8 zjxN(MH^(uf z*urSGGURp$MwWni8ED5-A{=zaSF!p8fH=JxbqB*RTZ5E#oNI?rIOssp8EIkv*EDA= z3VQTMNyS!A#%<|Udc1CP6DF(EIJE#juf`k;LaIU!#MPa%G$C>aS)rv#_37_3u~x9P zxPzGG+uSj)!yq+Re4uJJ(I2D1!YERX>?~l;`k!%=qZ(Plt#g<=VFy=`!Nw3hgdE{L?07JTNlQO&`o!9u zLb~LUpH3(t%7q6(@~u_nzJi`WHy;B7<_jTPIt4v1TmWba7Evhdn}I~nm_`lH5oSz% z-W96oy|au!bH&;7*-tm&cq`Z*)_=;$%9T?;qK9SQZb%!d6#P?c`kAIIrP*^%yCF2BrlVVG^o5so=!eDya=eSecUxQ%+;(qqZDH` z1JO%_U18iu=TG>g2%P-9ni}UpUsK2)6NgscpNJ zrl!cVoOHfv`{|m-FUCL6Zi`h^%P-U}G`0d!12b-3yb2orex zacKzFXlg~n!9;k#DOUOU9ehaH|3%(=M>VyF+oE(80R?=xbOXuM`QlDDuQGjI&HKS(u z4(h?gH~YIJcp<@5?1xRc^0(=Nq26byN~P@nm0@We=cBi$Mb1JPrWXDh)W?O#(DOR# zF6EXkW{S;2QpA_=Hb|p(gL+H{yd{)K#^Hl{BUYvzX~& z#M83DQ=6*QOZFU~8Rw2zz}Tfw-FNU$H0#egL8+X!Gn)b~#_sniFZA7)>X?KP%Ieoi zcpiRZ^DdNbmXw>#LT9Lh^g>mpqf1$W^!P}T+U=bN-55Ge$zuu<832($*j~pcsmF)M8-q~*lA@$tg zeyJ!(xrANSNvM^hXI`_iMQ~e~-aMNmD$(Ml8=li|r?jD0x3~>e8`1V~vUV(tU>G@p zFH{h<34@d44n-${#uJG5s9Tc^gs0l0ZCQkv(>3fJ-wYE>!$n=v<>4ho>$}xm6isX= zNOl(G(?Bj!#?*%pnVd8;#PcZC_D~b6Ts%%_8t)NO)qMyE%2R~ijHVQ#CH7J3`$Cff z5MswnKDQ#(VL_E=MKaTt%uP%Co6A8_lizPbG~K9nPR}7w9xQPkBmGhwA8BylY3Co~ zvB{>|XoB&zKw~l^Xt4yUcvp#PjCp8B5~SO!nSdqOw{#=fkK>44y??!AQ+#<#kf(Z< zG%gR6CzPK!ug30T3_L-z^;YtBA>IOz$_1j1*PZHP(Cp~g19>cCH6xK3q8Fir7~_a% zChik{vYv!LrW#*dI|EG*>M~o^K!lgL!3?2Xh2k#VI=krZfFA1MZN<(FDeu|G`_Mke zfJn2g-S=5#JD%}k0{%V#u%KH}hZZehb6)(?1NvnHiIr>yO5#B>o{>Ltbm;Te}Q?^W;&Nj(d zPFLqxIlUhF)UKbW;T1zby*0v5$(7-((X*6`T=EQbSP1_zegs&jmwLg={{hLF3&E4O z9>@TtrcB`w`OCreYEOW6>~~zbb8yEk&{&Jto%d7j(6BhUw2zzZrt@6YfpFU&&uPo% z(*%d@7`aQKFM!vS7D1!C9MvYIFWR$HVsfRz>^hxs)v6@nCiTf*XV3gf`TXpK<=}z$ z`GPyfvb?{l-T&q?$KF3VY|;3keyWM1*PMF)zi9#e#}?RsC&vj#J!v9J7i{9s^6TB1 z;f+_IpuS)IB5AmI^Xr}OC{lHCYMGr>!`b$tyZ$~tH*ZR{t87G^JYYJX&!*isggM7n zlj7v^uQ;;|U>ys*%2kVh{=D)VE%wYFi~q6mBWK{JbMkWLVd{qHW9d4P0Yupzd}!#X z8$OPt&^}Uh%67)p#;l&F$_mH|;4olX|KX5&ewboUE^&s^S_@PG5PGKKzdXL?%lw;b z|IXb_T=_$m`($vrYUU;Hz`wbk`QCC_T{cgUAKMTyNiL|Rp?ho{!y*J(`P&7np3V^y zH1{79qs^aR6K>vA$&TRoJl;CEd2U24b$4c z^C;qUzkSvQZ+-E5otK2VFZMD~I92;0aif;)hnj0xITJ#RT##{gq%Nx5Y+JS0-m9dj z#7k>+Qv|zVOX9^bkxs$^SaFFGd=*JKuGyVW9!9%8tLF-S?)amco~kjmu!lBa1=})X zpYgq=Wev|;60QunQ{7G|ccbxU=vqN@j&#d*S~m!p5EIhjU;(#qC4`J~!Fvu@@k7ztrPjX8-unx<%6T%Py-6sf z-S!4NXaey;lV6D@eEPNoJx4!BDyn81$_MT>Y-Fmww=$Oq3@>qyp`k@3BFm`$0vzRK z+KxFdka8)Ci0F3+q4@^8bPYenW{|!#TAgQh%i+$s-7SH~=jm_vBZ{RwUU5VFF+^At zuiwjOu!cnMUjFiXF4H@?dWI&B66}PN56+jxC!fx^2=?V5zodq*F=}Qihqio#B#JK7 z@SQA!r*>g(FNb6R)OuaLibmRK^RkHBT+uL8S>9YYpso-;ugH=GVYo;PpK)ETXzY(9S6>_@%#V)r^IRapGq7HvuRG9=;3bFy|>X=H@b) z!hqZ;P%4@|AC!Gd&MM0I#SQrW;l` z06EH9+?}9eeL*@~`q|BDPsxmi#g?Qw{pYx#g%ry6IRht@{^M4 zIXk9i8q($ca=ji*UG7y4RIC&m>xra>@fdzn(z@_9N(HR;i)4y;Az1O%a82NVhu&3d{}cM)nT6yvN{)YXM~p@1RzEn2o>@TG|S}xL8N&)u*>-;QF5UQayDt0;e3LPo+0IG^vTBF^N1G6M_tI7E}S(l-e zOW10fByw^HZvjStsU=GBO`bdNT%Z`9x-8k?19<&DP$t?1rU_|QUy|^>Zag2W>WI`S z#)i4HlRzF*!Hdpzz0$g(_%ul;ew?SJfDS|+*C|45yRaz&9;F97R_vlGxaEU4hMG-W zCfX(LNakhYS~`p3MarIBtFc~1(KFaTh30yNc9io%;1I%mOUfGKvgo9eLy70Jk4HN3 zHx*W2h2E&I6jWUEiuE`V5N$7Cehn;}wk4?;vX2&B4o*fkwe&rl59dcRAQ_XN(?hXA zS%(>36$8z+-RHpTK9ds^pMeh^)Jq-cH_I!@@{sCBHzxw3eMFCzcXa?bR)V}iyA2J^ zdxk*P%4%em>_S?v-0O)smqxw4Y@N?4Jxt4#QWfMw=i;suE+ph5kMv->UTo}Gdpp6_ zHV}E{t~hH@A7mv-C->w^7*o|3Bk9L`Wx$Jf<{9#+ zzm=H2?L+I`G~_^IOI?L}3VyJoCB*xouE!UR9>RPte5qyoMv0@2)a{uA!d1t%d9we| z!c}V9oP#Fzaf~6m)k<@ht=MbQPFU$E>;XzB?Bc9Bk$&a6gcbpbn){_K)&$F#lGRH&V$ceh*$TU`G)o-n_-li10K7LrRKYC#4hCW@#~ppz7Pb_Ye|54A)VFz$WL{LykJp9Q!5?*3Ib^o*ubrYUnY7)N8 z3~{2K_WtWeJ=rx+4I=HeUvrI7P|H`JNd|sA{UvMf6L4mSU@?C6fo;#P(`V0%d(GB@ zI*sOBwf8R<3hd=O2f6*+BhU7o)!QY_c|BeXEcpcNZG3(2PmlP&-n@9Ol==zS=u4NC z4d~yD4aGD|y`-x=&++$!6rK$(N4J+FJr)?Mi>gR*^v-Vq4bySy3 zZ2M0B>i4N5F<_o~`u7Z}&gO(QnX3Vb=lc#hH|={fpO+#|bdT{C-)w#5+wJi^GgWvh zsCyXx{g0!6WJEn11VM&(8nbY*<5^(aQ)z~$SnjT5jJoWTSkZhJt=3=3_<`OI!ea5` znE2?~51(39Nm6F}8CHsPfU5`LU-Pe$&&TVcW~{+!_DrvQUkG8Ff6UVwe73HPxL&Ul zYc=jrhCtjNXp*qI{tGN4Fi1|PDxKB+gJW69t-zWhdp;nPu-!5!$J=(6#UdDfkP%T& zRYlQkk%)2}r!P>(j3RZ>%3d0nW_j znBE&_&}jmU>^jywZbZ?}Md(Ew7CYyu?eZktV^gs0`o32ihxfO=!0yX9sj*I3DEU5r zC+mcN%RN;kMkKkQa4l&jUXnbq^7e3R5|hV93m~#D!$kIM3y0%dL7ywRDi|q24QQ&HX z3!;k_UF~63kYdi486MPDhX<|l;MMA0`EmC0e<$ck{4NOEIABTWf69>+NW(k8XC*mD z4s(u}aekji@GukLGJJ{~(;;R9XbIzKyd4iZ+f7IU@_kp=ISqzfOaZHwihx!fUVV<> z7Sj*5J6N+`Vxc5oy7#PJ=oSS&L9c%<5Dtw^g-;Tb2?KKRv0p`IZ5rIl6 z+^m-22`P4cu~}MY@x|1>0Du zRD9!v!OL8|xD3kj`(eSk_CBE=rpE4(K)6K(y zYPZrB!s3F2&>QkR8mN@cpA=XnS7E$CS zC5+*-%E+$hp&q>wGaTf$-*QYA)g|#jn{ing2k@MIxs(qHlT~Y3%rbZPzqVc%LOzDE zZOG1n8;d(0wu*)26$6C8CsL9TyI+YR0>vTWQ#}QOIFS!goG}74RD0z5nBMaS$J;ZY z0|;qWNyRE?@KS}byK}?JE6zt7KGzYb0MY*>m)ZUNOmWAV`frYK>*mwyy9tEuORAIb zzLVPPb(hs392;zl`)RT z@h~0hWO`-BM>yTAI|@S!iYw)X?pFTcNdAzq_WJFjd`!n$VLw)?l-yh*ey=lF8PV6f zb_UJv^ZU`2^~YY%?+kGITW*~NgB%xH!I8hY8aPRU=Gf-*!&ZY=M%RBEi}PzZ1=7D0 z(&+!x`{)hz`(haTkx}g&`>9c~*F-_TyoCyYJN!_BGk3i733f`f58Jkhf&w;M&*_(` z!0m4u^@8b{MUTjDacym^{Wm8zeOL)a_y6YlOkq8KSxFC7@wV!+K6m%k!c|yud~4X? z=3mZO&eb&x6dl258FOhn*Y$h2pQEgP`#QolW^gj-+5}>WfT%*je~C($Q6Jdu*z~wPeS4rldsE{6~CV8wSl)7r~A^I zUQb`-d3l)q>#N*#`V(xjh4n9=IWMoTLB1P{&q}fedv^1eJ#Kso1zCCA?<}6re4`_u z3+_+a*LyQXoL%IsHx~aIJe3J?jnI{A)(kU%8dNc&L9!1gt4IvXNP*;e&tN7>GZsyX zjv-JCId)ey#rBy|a(!uL?X05hj8SCis^~)b5gl;AoeY0NN!y@Ee(xL&R&$H#{7T#k zeyg}Wx5&7bA(TlHb{?@3Cp@$!ogM_hD0df-v0v+Z_Hq8}dSC)!g zlMJcCVoIj7@pHd?LAly?TTQvHOeegf`x+CSv0L2RJY6`lg0)Y&+FTJehR>#svF~F> zMzV`xF?OGuFDdF&&b6MHz&EO2;Rd$74SBAmoQjy<-$StrgdE+9Vx6{*g}}kNyY|uW zYLHrws6|;M7ACSQtjCfxtMww3EyXe{9HAQCM|9MfHcrGsF)|NHgpMw!Y7>s%XHz?$ zY%L&+$e#Jy1kFwZph0oDMUr%6y&3E?%B1V6oXFU3@-Msdb+z463$3vZl zf1MmEtaVckxZgEH4KSx40%CyfH4Ixc86r_1P`sSAvIWH0t2Oc*X#_GaYR60VYB241 zzBGKQ%MS=YkDc`hBmrEb3-A4!`!r=l=2xgm5Ojsv+YLs~m6mq(T5^Dmi693FxwQ>( zfYAT|KI6bwhqpTv9Y!S8a8w8XEnim!z}4kyr^c;DBI^CtAUxr4QJ~+*=>MLlTobyZ-z^Ge4_~3hBtVjdHo%vL$od?)1}Qv?ES+fiHmMX?9YZws&#* zi?=S=U8ZU0Deq-yF1hlfmBEC*J4vq|dBfVa!|EL1z}fCqF{U!FYh1;4&yMndgoPG& zd~TBF7Aypp7?YhCQ2$D#eK&aSPxX!xd{9%c5d%w%A8A%VF>L|T=10&`>&+W+;($|) zfq+!pz5`5PLbonpn zc9c&ne5CKd2bCS(aYnI=Bx6$MbRy_}NREWS7PY;8_|P~VyUgc+!k41=`=@U4szMIs z^#Fs2csfz;Y_n06jh%OP1x$*+QXbQ)DbZM!JTH}uxNkdF@rA~$MET)S*LBeysvGdZ zB_F%q!4e_BTzAvg+)-sg=(DG?grZs8XtboDs(K|lcM|? zQc7?1zW0_sW9?9c63(dLi}umzGYOQU&Wct=huc*{R01H{>-SAExdJ+KPd&sKWo2tD zE&8G8{hf+wR#uVmf%u*;ShAQv^jvbsdN74?<67xb}I2-Wrco`zXDB+ivpxeIU$x)a6>^})&{(in0)3mUK} z$_SQyH>#0LiwoB6$D(U<^Kr$*H?j-HTD9MRvUmX8Ym`R>YtozY&U1vNGMW0K8pQD@ zc}y9^ktNL0#___T5;NSiCNFYa&SKr@M0_BLg_$0Xx9&MyT>= zJL=ljePUcdoqf&{_OAABMNjn_Ss)_ud7OjlPEe;ZaoEZ6#lnglZs~Ygf2Gu98#q6{l`?`Ya zngH+LT+a3!S3*A$_+uDxvadkD4iA|#{>?ITs;mDRKUK03kQ?UZrrhuqG%W6v;9=jl zHaz#wp0N$}7w)=3e8N;XIr%4$4%TU!+;+8`>SUEg&E~z0zVYVRXWApj_Xn`-OWcV4 zld+#aQ5{We(gqHl@4NG3^YV98*%dvlqO`e}WSAcEd=+*{pa~cR*-bo|0Q8|EsRoq#yEi|Ixd&KZ;sC zTk`BL;NR&CW;J~JCGq{4@KtF-pA#OrnqW%&@9`^_3FT!KLb0dIbnf=&KicsA`5qRz zr3^oNEbvpSJPBmJn=pj6yzFWf~3mTr{-FHu6W+FlA$G#pqVlrg4x9b}t87l=g;{hW_qgLsttj zq9+8$5T_j#^n`R_Fap|)r^*qq%f)X(*K>Q|!uMW8E#zCf<;lx)5x+W8RL$X(Sl8P1 zj30B{e!2MUi@0cb`bT8o1c8hGXHhrbfqPeli{q`93&hd0;Fr;YX^n&nCOD1jy9+yT zqr>NA{Dl6~F)JZa;wHY?xMOd zz$r11nXYT@Q%EVI?mZ#FS;;wxkMDA&Z^y#5$CD_6F<|mKDer4dU;!FLJg^Q^j>p70 zGNoz`%{Vd*-MC1^x+Z=S2l)IyZ)5z#x?)ZwE3t;B%7LjF#y#g2YyIE0T<`2*1T}69 zMy=)${QoOeKw6DB;Mf&V<;a8_=A4lg*(}Na(NL2mw+&SYXN?G8(j`Ced}EyFl_W)W zv9J!yYHOGelSd%T=;c4tj)BWwMDnfI+^2g!<3mMA*_|*=d&|&xsc?%hmazdJaBsp@ z%i44%`1Z7F_s){KD^lEYpPBB&8J)J9Y*iY)xMd3Gk3SFf`eCB%RS}Jxtm|!*k%WCr z#RI+DfJZWvf25I+;`dhP_PEmVjH%%(SESxW>?5mJzoTB~LY2@w#;Br-7Rc&$uBD3r z#24o+QdXrC0iBg|${ODknj8A`EF7W!x$=v#l8GYAo&$HE=wDf|{$34AB3o=j2UkI@ zBCl8#ydl4XV=Qq;q;!_b58}C4d9y{RhbgduGz5Sm^N^Jh<9fTE_j4(cV8zrTcpQ8JDeI zU*ZpJ@bPi~N`A3|yW8(g%pEr-P_`fbYUqM%bWT(0Rx#(WEc=uVYE<&&DUUc>-O@@J z6)+hT=4395@_s_~S@noTk{k%KlLRn$b3TLHNypR*PVN^Fd+z!c|>-$9^xQLA^xhUBRbuqi2sEO5< zZ7B*W9-=Ttdl+#ze5|uGXwcuiDlOn%P$wcM)uS6m&r6~=lJ5kM(xTD{rJ?bG^?R>kD9-l6=_FzK8JJ~Hdn@Sd?hzR;4C_#TNoY(sJ&yV4 zAb``~&kCSpp*?G*8B;3x7DM7{0@ykdCYwvszr z$?Lb?aQUb`HY%++WY5$49^RcV*0cCj2<4!X&@6y1n_N~-(~{_Q&OTaj%cFFCw;g#i#}OryWZkq8^^} z4Jm9|PyD=fqkPhXXD0rVGZoQlIe%FoMX!83=te=sF%_H7z$edlZmN1kY0ou&b!P8dr>u8$CId&pLps#iKr-lXzWsEa6yKiKN zQ2tz^;wF%_^Zg`&JDB41YBc6LqRI&)Jd=}#Yn$ux=aI|t)~ARMGY4Cg=_f#tiznV8 z*CtR`H8&632l%n;50NjNeW?rwy1)q&& zYH(=)DmeRH@F3SBj*6%P&|AY#YG6(CuvqYE?*G9I{Wpfld>wDSV|EyQ8%^+%!{lx( z+_+)buhp-0F9|3WJuOY)HmG3Q%Sbm6Zvn3z=NCvV*mI0#7{2dAm}f&Z_%p*JmdK@? zyapRUPgA=oQG8e>xArwoVDIt!o)9%ieE=Y&evQou?Z;hEIa)t%Y9os(GH!l}f618I zK8ss{UJ`oJq#OCp+nW07aCiccW?b02jcSE9jPDCwmOq7aVv`ti9y? z1oD7DPt*d}eFO)+Wy%h)Mi#Lb8VVu29QUHj{JjzqcK93makp|D)hlF-hbjkPhgl~! zX|6xdq*|9jETwRkaU(XuWUu4Sz*KPIB5fN<-%ItbVmvr<3|MyrU>RIa={gh_{#bCI zosQK-TW&{=7^e1v)&)LMqp@c}L$k}IxA2O>Q=(BIYMu|7Y%bH0Joy944z**4-`gxY~}lN@iV?4buzK(%dSHR<<{W zRKTJ8NQt;u@6RJ`L6U25^z*lJ7(ELcYg!FLcT|u-`m^;G)m&xa%7P9si9>oGs68TEz$ve)hHed#j1BY| zm->WY*%ah#{z+gBOH2z~&zRb+UA#foQ}(OIblAGA-{pOa6$42WLyxlR2xjT{E~!K8M5CileM`JqRVuoAYGiGL9E{KU~(GmpnNd-g4>C zo!kWyO%=T^z5j6bS$`Zsow32b?=O%MYiY*24Q#|5hp?J-*ISqDw0&e3q=Ua|ARgJx z<}aD;AR~g{$qR*k{Fq}&qN?uYP*;wpQ5$U_CWJ!lmsp@=J@3Xu2?srm=ID*AKRk5v z-k<9!p(%~aJT-~S zmE2!$e*&-~mU!c(WSt@VdG-c1v~)+-cn1-~UNwj^zIK-1gLU~<(G7{ca?=NQ*|_f6 zPm7no-fLtk{Fq9<`-`6J;jMK2z;Ms~UKW+~dS*$j*hPNhS;t@JN9bX7{fP`KPg#w7 zZ#fsV`QN?2?_Wrh{4W#>N|j>*difMv8@%gQbLl;_+sg7~L(6z>rO(XPa%M$eJ7USW z8j%~i_s%1{B^Oyr5vj?Q{@m|uo8#iMccW6|+RqU0TiAZ>>Rqmuf-?$5xD^{9N! z*Kg~6&Pp*|;f_aF#sdre3Pyj-#ce|)r2g43KH1a4j?BbB?;Wzs86Dj5Zh6^b)juh=ENT?;+rvvO*xI0fDXa^S$uUUP6vCN&hoDI68Hq-^RmV_SmOZnGbvn zn)fmEmF(e~R4+rI=HaFmM91cQ1s4xhswYoK+peI*rE>j_%+6 zKNIzYEFASVU}ht;H|^|PZTbJ9&%+VEaF;k=a7O17?F5`s5JR=-QfaPehDCH}Z=G4T=0|9VKO_f!GC?7QVg(+;afGJJn-0{i&+= zaW$#6uh_BYe;V0*Z~4^fDesx4&Bfh51sT2UXl<+F;YLc%#Ir`5J)|v+_qA0-d4|{N z!b2kh*o?~Y;~;3lm}f83cFbyMR13V-W`Zq^A9Mx=>DmkcgsxoMb%j-a#Bj!uf(GD% zQ8oMw!^;h`8CnvYZ`nWS0{~rVcrIc6QkFEA!wdn$p>U4W>#@SPR0)oJv#^>&Aa>&h z5Fw9^SaspeZW^m0VDz{$@(iLmJ2gkR*DHm!nSx3mAuxuq;PP;@N+Ye87(Qxs(50K z5cjwjX*&3A{&w)a(oN1_HO-MF2SQBQX?&QP_!jgcb9tnOQc}Scj=0m)oOAFH8$XUO z*UpC-Hge`%H;s`~2AXKjLlDE{MDR z;QjnsU)4ZsZE#mcsHF-|skJ#Oc@+c^X9VR)ba>bxVr%-PcX1zv@}=28 z56h;QxeV%Cyido$1Jc!?R~WztG=Z_|Ns?I*e{uN>QeV5APt?z3JE$ zMi~v*+afnE&Pd@D5mL*zki>Itj8ItCAZ^sfqPAbiexpr#uzDLOkHfh}46OI=_$6HD z5337}@n}71D(+s_Q+Cr08POtJ^pUWT$w=8jj%ye75?eWJ+33*Hi4 zy#LI@m+tfSWAgmb>`|ceQx(b)1&_JnfX7q>GNhW6a)L6A{`qpfcQeD!c9f^#C?g=jGB%S()bDgv|AwiKSctH9eU*Q3#>;&6stVP1TI(g^OTYZ@#TKY!3InA;Rd?+Suc#xc`x7|Aa1 zGfVH>cPBcAb?`ERok;>`!F!Hs-kPz=K4f>eRf6;%<7u4{UDLUS)D_${3hUt|pl7ex zlH72m^w1-jc}g8&wJ@>e!5uiT1N7>hs@0U>AH{F}!ZlBa%4mBSXViM;aJ%beOY87o zam`QfT`cO0cDo>jqIf+Fc&0F1_dN5wB6{kB_x@Jz*m{HdvifkmDZAsH zELSx8+0kz3&BRs09$w#pD0_>VqX!qT3LL3OUTZH%wJgiAYU}aGuXXWRL(W02EgDh{ z-La`2wnU%oJSQ2R=2`~qA9pIG^ zVEpPASo4LD*U9hKe&Z=L*q%EYMNM~3{a>ch^JbDAgE_#f9WT~^jk}d8hwo z;jsVKEFlo}!#VAjTyd_}51KI*T~`95Xg5Z_%&n)Ez_xZK_snQ3P?jTboGPMq*BzE{J z(Wq(jY9(?a4R9QyeTPjIQqE2b7QT>ulMFZS25j*fkwesDIK#569CHPGrE?LriYLLh zckn#}+(=bF2(6;-)OCJTLgU04AZ+y+tWHLE{s_9UteJ9PZmGUH7fjxAGzjLEvBfNM z09Awnper*qkD&qRO0ffl)$rAosC7v{1H zz}#iJeZ;r3i8*Z|RtQgmQrl(n08-`_)i&!nv+U>NX;fa>8;1*OgFwrvcOT_H2Rw7Q z-4j+?9457#7;{qBC9lBYSq~#Klosai9?o!BGQR0N|=1s@@@72;+t|l-N4z5xC-$8ZR}$>f^W^ z@0Y|`-M^*B^^*nvojWi{5~v`!gz=bR2IkCW2Wt+oi53hCLNcApp5Q&_MNV+x>#c{% z?;1?=-C%{>Mwi5K4(PhR<-C-=Ls5bz$N+Bg(uGg)EZqi?k@p~C58wZ2fnau5?Ha^V z_3EpQy)i1(FF`aN+0y5!GmyiUucr5Z@UnMpD(CG_>I-Wcj73wOm+JK2K{l(THvi?Btxn+QlO95`h%|5D)`pMIr`rH zL)i+|dcvo6_Tb~3NQQz5+1aYp^@I(NH))$0}osW#oX>N zl4$NYK&`TCc{SiOQ2~}c(E4LM6mY6reJXruUP?kT7b%k>f*mZT*d-tigeRHhT8`GI zC{%fLwc-W|s&vGpwEQHtERb@}ovxPO88Zhv-t0CscL%eAv$bu=x(^R2``YCB9tUoL zouKwcd;LlN_{=DbBf+NS&pfna=Wjb2W()~Qvp=hX7lAWAung$b^L1<5YpO{0$mnQd zZkfu%YRO!DS9_}4$hvXY6ky~01U}# z$)}sYJw~;x2;7dC_i<;}PpV_a7JAvkGn60!tH=xKksNh$mST6rVkz*(?czDkkWcj_ zC|Gs38~E^wDX0n!_Pvgm(CA|57b>3mBmA~s)K5@di(ihe zKC8b;^7kHpFne{{7KnP`81{k`*JjpXtpoa-%g-g*{c>Z_;ZctYg%;d5IWID8+}?a^ z1DKc8a4E8E>MwR!?$IN%%qd*!>XFCabj2j+pMXyK*d8tN5ou$@dY9517F+GA zN65D#N+qa!>Ov@$x@riTlPc&GHltgAkBjQbOL*E@S_rW+qM=NR)kqv`ur55Ssa+#C zFNyM=WFhN4|GI=^a(v0u9x`pjxurM!FUye25JhNDAT z4F$G@F)@V`Y_Id&1(yQ7Gq&!aAI0bXweTymA8*U@ST+LaXbm3fkC7h1M;wW}s54&> zUG;FJZxtm9Ws1}Cu7phq;`{&bi+EZg?_l{i*9)k1U$zxvscNi&mWEd&|F#(YQ{g-J zyOzWz8LIA-YTi=QebeV~D|C_*_|)9{k?I>Ci%c&Yt^n=RFwZ@L2b+pD)#`bJ_hQzD zYM{k4SyNW(2|^+q3%w0Or2Ocl2wjEo;_U(9Nq3BuBSfr+hLI~$^Lf+OZZbv>$Pm^zUYS?`R-iifDQJPnFji~7&WnDgL3)&9D57i>e z0=#@$3&DrrkUGt+VcL3aF!_#A6BCJ75@nYT>l1JfmuX+r4Vemg(V2S7y47K-g~JRL zK2}Iht{Rc48rDA$_uG-g#o4mhDYBrUTEFeLEQLb(FrMief>k642B7gdV2&Ta{iKT} z2Tf<96#T0(fhGRzma*f!ip>I!UsVg=_@rJP;|+LlNdzXQC6e+BIJt8-GzL(_aj2C9 ze!kPKXDN zjy056Hf|4nY6KeA4MyMi;P4b`w5+K7xrO}pUi)C(V9Zy&-N|3G*>3!qyK()mi|)_( z^)`U!-6s_i;)$@_n_I*a8gP7pp>OgmZ*gKxaFG-6aFn&lT=Rg!Q#98#)!nos2@d8v zsJoFL@LYaaZ`KqraI{%iSCH!wK^L*lR(}C?czWRH)+R0vBUtj2J#Fg-G%*PXz9LxP zjGuZY>EwB!$61)nt0wNC+*UxFB{D}GetuFH5gBv$JuSaAl-s;+K{&C@f`}-TW;xln%W)m}gy#E*IE+Ma8}#+UDiMVCH=j`S&W&Wt7Fj2mvU9lT$W zpI+_409s=WSO^fgAlzC!`O?s-o1eX237?WY{kEw`OnBOv!2 zQqs<0;T!S%fgq_^kK6Y^rjv-^W}W$xin?ZxV}wMh=H_|hvld0(UsOdy?sgPi#9NmD zVueMPK_sm18NIEmEDh~=RzTHqU5ph*?of=>{dprvc0OZe%by#`4oFUnW(Y=xu$$Xj8;8||FOT)O1zi=P zOK%FcMk{e)^9{#0R4em8A6hAKX%J0wUvH?1zC z3h{T4s_QSCibm#sc zFgrFfL9DK~S#mOiq|zJ)i_Jy2ERVhw5RQ`$DWs-Nc*hhmRYPnVyB-^oMasvRacLul zC8>x;^J&Bni_-@4FVAN1Id(lYS`nj`zmE6^0_Xo1q|;iedwkB04( zmG#uVL~R38{hO0+1XF6eOKrP0M0~~_8lxixUkZ{p-*6PhLP#mMbo`5D2ryR12I63& zJlAOrg9?(}ybrtw(1Q@f+Y_heyZ%65QPi1jl+h`Aj4Lc`#BT~YM4o$>{2AAO&S-EI zBDi6Z?a|Q+eLY*1V-2k89yg@mL>4FkGxdh9ZZ_e=AHkb%(Lv?5(P-Dt3=2Mf6XaG7 zFZ4#)jqQNM6VT|ub+Yw}mKYaM#xjKEwkqfcKdg4=0J9W4Vpzj(2gg zM?d||H520=%Z^y*%Ju=&Ct&f7OQ{ zG%TKYtE$Zz$V~Nd;i#wvfNx%DHG@BzaxCTXWIzc1MMf6fc`&-^DpvnrC3vw#R4 zzcSlIJS0O6dlbAnPTId%@a0;;*Jm ze)Qq|*X^&rpLZu`XwCO@u;9W>r3w)Hkx9My!20^$9~Ma56U_eraR20ZQQjrXZ)BAB z0;l1#(85rOArocauSj&TBqO7t3^0W}#*05nII^^6Xjh0tEOd4p)DT`zbnOe@A{Eq-qL z5{r^oVw$<6qlX7ay@QZRlEX3J<$Umwja3K&dMb0X`g6QdHUpk1D^eC?_`#gSgv3vQ zceto7-FyM9Vi)E*L4a*DT~AgZp+Dw{K&SF)!t+AQG$%T*+vaJ#L1wt8uWJ!v&1rX5 z?QoN?0^T9mMXSp#YmlK&%EbU~xaH~1<=Q#8kwG=$fPiv5XDe!NbxM5&$V-4}!5Ap@ z$8TrT4cu&x@XSTpHfEn7IkX>1hVyhql(@S@X}iF%qdNR1&Yr4wf)(nX2y-Ydz;`$6 zhat&xw@@cwQBsI?YKUjx;pADhk$YwPk@LL+(4Axf>Ys}XT1GrQUq5}iE_ro+@hg1X z1eR{A1>){h=^OnOHU+7D|Ep0UiYO0(PnjHT8onxq3z>M_HjvO9N9MI-z3EfJ_sWz* zW%b7-N5G%d%1(Ktr|7yPGr>2V%1piTxSWzXkt}up&_rw$jNmB8*}1d296k8Adc8>w z4vlVL=d#4DhJSYvIZea2r&Lo6WZhsj`k<_!Hu33sx5&|0Lf1h`64dV3FT?dIOji7d zlge|tyX`e2ue_(~sc-V`(W6iNQYp1vOpX`bDc-Yv67s>*{n?I~#B<1aq^X+@?5@_r zwRbtEEA+F4d5o#{jPs%Q4X<;5rsnZ7E__INYWD%hyx?_Hxfn+Mn@bRTGAJO}uqO1r z{W_mlKrfpXCY?6rgzn4V^X3z4WndDo4A&Wq{CfB)^9rDoCkixO)W&Y>S~K&mYd2zI z?v%N3>1D!X?si>4l3{1T_|bH>9PYvr%W{k?#g~L)dhnGHLN-M(_9AoBV;41hnhQ=c zQAM3D;{z~+qL%4HtZn|Evh!YJ7itC@JXiA`tET98D0p|CQ}`pn_$Uz;mc6iF{3&Fi$==T~prn80SiQ1&E{#Hm@HrvpRpUHdGNS?2!U zMU%<`I#zU{fOLHRfMMedr+EDZgDu#LTl@S^U1a@S8TtM}m%xU#fge=?->zbDiTmwC z#mS7i81rd!kRp3GgK#yJ;hsVBvV1l3`31>I#(M3FA&E^-$4hF=?*8VJ*xAqWJ?2tu zUIKNGp|~QO2MQ&`h#17Y3FJc9c>_YEzoL2+#oN7H3iF}B;Db=Bjr9xsto9vLwgTLk z{u<=Q%6x=)Sfoz#Beib2h6DX3c?5&1o?v+WN)sDfh_m0x= z_3VSh1yG4%g|4NvxG+q#*Gv>&vKzv9jZhkbn_Up6Lgg48)MtKM@T@cXHV%;Ll1YEy ztxCK-#)RO!d~wpFTT(&$xHACZH%JR{xTe*p_kO0SM49~h&Q+jyVh{bZt2P-$M_05I zXNAfg_qgUUn<%Ft<2JXV-_uF<>rOFbI`I>Ad4N)SivmsT6n4y8aaHkDmg(53eyest z%<9I+_K!L_AgQp6=z@g>pA^hTAN#zu^6)SK7ckO?4OpHabq!>cZzn7@^F;d*6vt^ll_(K-LsNiGG@&%OO@>|CHGZhg(J3{2RZ+6lRNAct#E0?@n+-LQf%q2vG#H+zT|^pC)C5lzw^29?gF8P0^1#}(O%9G~1f zuSr#f)=RxSo(Id@Srh4bZSbz-0o1gr_SSKl#nEb`8~6ekg@ffgeg+)XiOMFBfbx|| zXoz-q)?Q@J48u<2iWdDkmq87z%fzOS-<$GAo*=F1CW@O9l zDU60N*$x7EHUHs8#VNr7bBp%JkEpYB?SHNQ*mc_mbWuVgN81Erd4l?y2fbB688R(rq z^q-ROzabwN6s-M2Hk^iDvCDkhR%M}nx}d+nb9`;_IiTw8K-aGkJ<6RO@n59B_;2z} z;P!N3|4ilcNBtbjJqp$wK4Jd5_HRJdA;a%$)AFQ|3gY9KHl}Kuz7x3kJLS7)OQYIQ zXhYDohJUu>|3^$cmTizB;gk#O%#a!lX6)eiSpzeXM$pzltcUSVT7_am&x+(M0O4LU z<*lIZY`@{}Qgqj=RiqJ`rNpg6_3aeh&u+=cq+1jlS`_FYic)Ur>XD0AHX}Rizf(^n zk8NFJb9Z@hLI$*!)u2#%)8Ql#>KWcp7axhzt5Q?m73^MXw=~B2g}rDD04A7a(G4N+ z;jH_wvf%CFRM%9AUa=a+)vvf>Gv ziE;Cnu&2R#_ha&R1kinLqMa>i?<-*JG0yXur2;OG>RfWo<*SVs{pnox!&{Z*VI8rA+**)GUU%5zS`<-! zTU?-PZM7&$iL2y)AJz(Ei!tOQFw^3A1htF z|M3aM1Evf1>**975R^3u(d1*!menVn)(tAu4f@{?PHf3I-Fqoc^DMekq4t{7=By#k zUDNj9rH~JbIin{hNr-iOswx@l9ZW(&R*JwOsqmS2e@goH2AQ0da|d<% z(}+R^B4E-kCA!J#a!|(bokKT0rRy3tonp6f2e7fLKJw|Q2(-y_6W8wN=onV`*7;N6 z!!5@IDczXvcTU>-Xo0Ugij0QoL(B9n&hc<0w9gQ0b8nVEsukzE{Yae*AL1? z&?i}+r1U&(dxga1Z?%wj>64d|R32^tr&GlT~Wo)=CNN z5|?#Hy&`D#eAlH>S$EKnrRYo7jceErAD>X;1o8MPZLixU>o}F=ch_@>*030Qv3NO=>-Bh@9q@sCH&6K0(Iw2p z^y)7oPWepkpSJe&X=jR104v1Ho&CP-T+YWT714gb^I13NYD0Q{yYXI(LP@Af%PSA( z38IvWY|4;nBP3g<&B+03M%7E_ip&MOP_0IO!}Pd`e*?5lFH9F$Dkmvx-J1DY5szbO zvF=K7O~Rn%3=F}vPofts?BKDOt>94ZW7W)vLdypOciMIBYGbJsqb-g!$dyM)jjS`*UC#>W)kfyFV`rX26P3A;60UFn5LhvWW8}$sfTv<^s`n=|TWM zL-KWt^$H$7f7Y6i?S8N@_CtYIG{~x}sWlqG09Wd)wfW>_9bP%E7rz+h+8;yVn-ok_ zq%VbZiwv#5A*qGjh<5QUA=SKxf6EoC@0S4(%tBZ#uCA=rhIDVis&`;T$D!K!);W_N z-_K*a8C=-0dr~3=i>T|3(r^?{^=z^a6ud zeMDSVdg)?}?*z$C4POn1G{N&uwz910ig3dX16`<-I*2c*+3;WmzW+{hpOXUEiYAaA0)nZEX ziopcyK&-+uB_?yG+3Fm_xz{lhf?07yx@5LEt$|0l&JQIaC|ltcDssC?aw$sTEN~iH zS*E^!{4H7X8=gdTm3U4ZZ5~>lRYcZ&8Q4DbmFA47YhqcffV@c!}iRBJ2s6i zJKzg%+Tn^Yi(jFZK*1uH6gF3w#YZ|y3)@&vg#KuVA$oG_(a+{;!_!LRXpT}w$5dUK zoDg^T{tZOYsZ^1cmm+aB(0SG6jx?-k6IyiFs}OOv>wDAr1MD@WybR;F=*%CFxQbT^ zOe(k|=F;Ba6Z2$KZ*<0wR<7UjvrHJ+qnUq7jM{7d1|;4)`PMRrNuv5B?fV}pn*=Iq zn!K(t7_k1Gbf~LzTks@>auc7>0JzQgtVW(}Pl+4hyY8;=YwYg03;bKQQe9w2%Gn>4 z<7Qv3tdQCfC7<|g)2@y_a`EmEK5>MLb|X!m#3znDfKNrL+qjYV;&>-XK0jrc|!onCrm{L5{sN*NuxZ@8Ru;mnQA`m~)*tszaz zjoY8*!6(GN>|{B<5^#RrgHEQ$b*fL;=-HoHG0FWK;69h1`Etp2DB zqh((Q0gJ+C%|0lHoR$c~R=q=OGFsklu+0{O0SzSQY2oHhP%X(;u~r&g);V+V8(u3t zUV35DKW+qe>&5J?iT8!bW@v!d%xTTMlR#_nQEKcG&;c!{qTC*62WHV$F~r4Oo4w;? zKvWgGuK--Wx)jNU+58f2-0Gm}B|jW}5wzimeQW@ZSiTq1Jt-RcVgS1kniv^n>^x-T9vuNRchU9Q0G^QiCmUlY*Nt7uYxJ3$mfz4~? z(ntOWKWA4e*G7`k^F~Ha77l^?R{w%pkpYI(fLDRK8=Ai;Pa=h&0B2^Foby)Ec1kQC z*}SN(mQRaW3@FIr!+s*dH=KLwH-Q_e4_7c%Z@+y0usd7UIvm#&-p~?J#1b>-s-bd> zevJ8YBljUNGqCq|?<0Rr|eU+2 z|4?#NwZy*v6xwJ;eb^;QyftO^Am2JD!@b{wZ%i|To^qgq+T}X+o`{1vVCqpM`$GTw zIX97)EE_HBQzWHSw-66Ra{iBq-@Q2XD76W>7E9h$!bnWm`HS9~=Ul2a_ zq>3497q(=q-HC3_k|rWW{tY%?7&<)<*3`oF- zoUGP8d1^2^EDa~FFx&aa$wRA|@ho~eKC?@in`{c){v%5N{@VCxe73r}a^sx)2Ov7a z5?svrB&?y4Fmj_Xt9q?E>!wOICrJBSHD1Kyz@V{gKeW1yS>z>`W34q?@hYm(5h!1Q z3G_5Sd!>Rd*?Cgj3w~T1Bn9UoZicPZSPeuQ^1C=#D(-pxHRH!2>lS{uep%CieAQ{{ zL$3Nd=B3qog~QEpLq;)E|JfC^SQ~FZ6J!4 z@>P?JOl~3XSXlYv(?E|u&aU-EM|FPrO3VFiw4nA<-K`T8gj}1YTuorIGQ>SKz*P|H z32fkR`9_k1Zbq2PziG%gSr_sO<%nuIVvZl!qB^pZm8`pjXoM#4hRp%a zBX=7_Us7xF+}Zx0ycIQHN5jwUI;^L-U%(izOQUNl;&pCT95s+^ugAEuy=j==+P6Ir zL_(=Z^z6w5s zu!cgyWGYT-cA3TBn=-QS#T0b#R{IH3e%NZ+lah9TZGKo2#04Jc`z88|UhNw-YN`97 z1!2UQr)!QMny+6PO5mG0eT}!9oRGT4k*v#|f^b3cN|HP~dff(fRdVy1kCx%rI;kN9 z0&hTX0+w>BQGDbKSS42^={NnXv!yDEm6K}XRp`6}Ru74FnmFc}wACbyxXQQ_OZ_(x zd4(f6I2;if)7Xbr-u_Ua+If1V&DE~T-#OX}Mx~j!iGX}yMn2B5JlamtUi#G7M<+b* z&TZfQc=F2qaxX{f>6O)TCgRueUwazimw#lqZhonK!_-Cpv6_!qOw~;R1&IJRNZl5Z zxdgZpeU@u!X$v0LE1%@>yDc|Ax`OT9rz?Ll>oQetZMl8lz4Rr2%R60|Z)0~1c^F2_ zyb*sd^r%K@bNt-fdeS81W~d@D*Q9s*C39Ly_CKCE`@a_8OWe6?Cz1Bdg*EQi{kg^V z5PXqqap`pEii{_I{xsiqs$GTldLgp_iFo0V|EA4>>&lDg229yl+jU4j;nKE9iz!{a z^K~oblef%yf%m()&@{M`zcxmNER6eq)9**%qF|Es2flcs*|jf@zYvzl{m<7yoT+WO z$i{t;Hgdk`uE<1jDN-Ev|+!W7(J<4xYaJ%mS$=p*kPM1lF6mCX>@ zunV@F!SX!>F>7b9Kly?NL*%gmFgK_`LxO2zf5E07X|fQGcAJXL|CqJY`AGsdHj?O4 z5jxJ^BlnHz?jMeOW^}M2y_$S#kLcAb0vzGTcX!4Tpj-V0yREu9=&84$yc zg>a%_J2r(LI;+G3Gu?H`aOKj`W3Dg!IzWXiL!3NTTwG+?H5KO97?9YI zQIu5(1u2MRy;EJwn;=%-aOIcds`Ik7mLwU>=g;OtL=ya`$PhFmUSzYA`v!bIip$2- zQYKIF75qgY#ZF!r^W1MMkDHt`Vvh;ak(Hfd%e7acL{`BUA&vssO0~C|qSX4XhzRjn zC%dITIsp}YykaGAae$Gy;x`|aNG^`s6eK=>je601Po_}^5xyE6c9-8)LU~%FiwijA zsg=sx5FByJ5?Eid`GRT&nLn4w2OlcZ8Uxk!6(;dteZc$Pg$+8-$AalQq7{P#Wj7IZREl;h_Y)<)eKUD4ZB=DBi(o^C^=a-b>g4B3>|yrhq}|I97-ZZ zfcB!Gf+WF1yy-FM+~LJb*KA$}@ab3y@Lv+ASvc^E3bsDV!*0eC9yh+gZq-LFQ?8F= zBA*uDLFMIK_3T3M#d>jUYzG<7?i_A)x%HDT9*N(28H1Y07AuyH#fD8~a6*D!8NGa@ z)zpc^GFLRA?KfUy=A8>Y^&B<6{dp8AfwuGQ=t{S$xs1H?}oBHtS; z)N{?%g1QW!1k*R~J5JMs25csD2FT&rV@>{phEJ@&-oL*HMu5=8QyP#j4_>JZi$1wF z^=*D~iowk~*7OZ0_7Hpb%T7)7 zR1qRO!GU_hVGYOWS6j{y!7LvdcUf8Ao2UlH+bZi&J0er3SNqw``2PK zU5~R^;`Z!c9M*W-zjAMdZ?gMLLw@VqQbYZ$-d8Z0U~!@s6$)k{ritKj?(fe>5Ps8v z*Wve3>FK#1Q|B+zg?jaopTEt)P1%B;*4p)i$hxi!k>@OC945pGdN89@Bm8SWJksm_ zAUL{Iq9Cv7I^NPI?Ic*v*aaeQSei^#gy~&yv1@8z;9+a>7vLhQmG@DO?PqKxTy-bY zx0o#mZr$AmTVA`Q^X0*;rb*Ua8*YYw@$K8(oSs1)TYfId94-Q8U>?0agQ-G4$5^ zimqDoMg8&=Vl4o(8CR-c{&t>8eJ(J=P&ipJN>s4jTkV$0%>xHbE<0CT)^tjm)NIQG z0--uTK0rj+>#uEg<(~3Ssf+0RnIMiFH3!$R#F?4l;_k))dsU7_^3r0-Ah778*H+eW z%Ya=9X8RlAYO#ab1<5lNQf~JC+!j)9QeSI|XrAt-HXeB_j%oKukZ8?8q;qwU55qP8 z2s74IdVo+a1lv!vkcS|OO4_)jPB091niQ6)7xELKR~xLtCEjo*MId69_##$P!d|>0oge@=G$jHXJd={whMp;ctWp&&G z5(y>!185xGEhNnCY;*ymURy&hqx@~>w|M|HV9~;s_Z{WRW0%4Sg2Zc%Ow6kp#R#;A z;!uCn=^;{;nv9R({rul!hj=06u!T&>2PIRz0#2W|l;Z^%k~LNLHC>6lWrMN^ zd{L$rt(#yDeCIAYW;f`-Q!!Xt`0sVH`vh)0bMS-24pG*D6Zt;4u9VFLX&4+M9jauufi-U1IH-^51}= z&Pe8p3i@ArzXg`GsJ{Us!jb9Wxqn^!r*4ySa?Qdu@Yn0>8qC4HH{1U+tS;v0-@gjp z{!JW1LOZ-;k6X$9(ky=z{s56v%gf8^{tXDjk7{ib-|Biw`OCv=e!ZGo?1}nz#afJx zAI_F$vl&}jnETU>{}Vqs#f+}`SSEOp-?#eB^(@O&4UL%B1}XGEVvzmkXCMIJn@>Gf z9kIr?rHTfDgS>ZN_6+}@!E(bSFibD-rNg-oxmOp;L#^Q#U2ad$$DsfCB-dkpx{Wh|NJHPY9LxBa)E_pd{|AxJ*5)yGW9HL2OJuEt5Nac; z<%z-ChB804O6g7G6f0<1Ark3!nUbk>Ie)6Y;LOTeiFol1dckGo&;xmTPQ>oq**Z8z zI@fcKSJD`9D{mcJCts-hZR___H(T`WG^shxhu*e+9qQ!KQUJZ)dAa{)_YJ!3QWl=k zxMbsO2nLQGa}uAW2Y&93buy2c@v5ZRC#`l97Y*ylq0nw*SaYN_=w<>RXyU$nYmlBH zpCf@JSZKmpnnSKuK%m37CsUs`bb-P@w93aj(DQUuccF$ycSS<>p7!#Dt3MGgDzn3_)pn;0F+G(CGc{W(1YZ&Tm^ghb}u4 z5K&Vp1xlnUcKm>u;z65BhC)G-LqK|HPkd_swV|4)hZf~uahUn`&S&}1H)1fh=eH#m zuGsf-{&aJxorl8VKgegydJ0~hh_e;`bcLGA_`u0TiYCuMe0fz)q+sK)+A42rvFWsc zEY^+L_G^utVy??UrjsRz5k7b!PLZ282W@{P$hX~(oXK44ux{3@?zp>4|0r0!fvB;R zAZvmQ<+*k-nIaeUk5#o7wyZ_MF3?{)Z#lfM58F5k4zPA=l#Eh^TY;@kNVbBM>qJgr zedG@bDv3?2Q7Cwr3(M&Bdd9s_9vr}44#!0AveV?4=#?2jyUR7$2PS$BHi-c^90s51 zX+&t#bt+rlPbgc|roeFqsKhS#`)To8uk!6IT>b_O_MSoQpOF}Ex8kARsH(nIlBXGI zvv*bUZDzjgWg7fJ_Z!O#dGm(EUjwlfyy}k&3#ptD5%i#p zuN3`kU&ciM;piHb8#Mn|;N|%0K(VoSu-8oNE%0>hB{kmh&ON@oB;!}PB-=|_2;Jj~ zM*}35g!A`rnDc*;Pl12Ab*X;-$u7JzjvqkpRKx*%H@PG{N}h-0|#+WcblVU~k{fH>gL zk8>~hMb8czkf$aS>iLy*Em_x4PhZJb3+DZ53SaiC&PwANXWmy5z&l~nrLlH?Dh<<+ zbD>%+KuO+0-6?EJ zc{k~@B2wg=)0#@z`GZ`uMRMn>W?G-H0j(^He=M!iNONU3xM;IaU0hlu>_#82b3W^Q zPWcsHet8*l{C&+awV}PV%jI4ya^T9Fkb@tQ0bEhQTA5Bo+Z5OEJZO!c)HGQg>3}F? z^K_U~EZJa9B+oh0JAf>Nku{f)1rtK7!wg?e7r0ziW~Fu;YWj4}T6*BU+UPgQ&m3>a zYh#}cq-I33e|Rd_-|dBKlvfkZEqJ+f?YhQkOYz?D%)8*S_ByXnR{Q@NU;+c;ym>v0 zA4Q8T{^Y)T6o+R&_6lle8gl;Ub{&mp(YAd(Gv8Il3qX0WY#J}=&1O*cQ=^PEid=^i z7qC1ys+N_qUKq93Zm;E$jRl{eDxM9$;FU~sdxs%gyqK}g;+47eo!D?_;4f~obd%u^O;%TwYBS+U0O+MAkM6}k~$Lc`Gl_A#};*gmaXhzPX%$ni671m00 zdfPjR9CXBN!qRLvN*RebcQsiV{itd_f`+!en5f)joxbHkoyQHynx?m}`sPqdT~l1l z2WRoUC1v(%>@D0Wxsa`T86AOiV=8!@g|I)o2Um5uw0fo#U8(2 z=f=xdN{uK~^~!lGC5Vn#IAVc|@+V1LD{J>F50ps%9`XsUJac=yu;flL!uC4ztJBVl zb?=$`8=#M*a18v8^*VrlT&DjDJz~M;_yy|5X>}lKi?=JTed-he5BWPVv`hc$&Gx_; z!xQVg4?_dl;Zpm47Y@A6Qk`>8EHNF>zX41OboH2!`h%#odNcCpUye*JP1uDylLGhm zPOX1)hTJ^c{2xMCJr6h-Z$<_z4RDiB#Q%Jkm0 zwEkCAHU0nTf2eLFcp8ynIcz!3ef-yV2|lAl-ta2<7_B!)PaKFQckK>p^`oAA5xn*O zn#}XG7{5kPes`Zl1$nS!5{ay<8qnwY?*99g^hr|CT(Ut|ug-K9nasRPt(ZRa|ApJn z`d|JQm{mL`n2WrJTkT;dQnTDDMmkxV?w@l5!HUE zi&zF0Y2Lg5@0yOrSDh)WLk238RlWN_x^hlRdlLMdHuc0Ek}ie7mn(Ru z?QYsXaI)0fJq+Q0*&3i1G+Z%*>H=3Qdeh;Bz(U}lMNG8I1wMG<_&B080MQ&+?^FTc z4_v-RdQhX>7NN0(-A)19y{_QH)(`QJBr~jN&H+X(SMq}JSz2Te&uMAULQ)#dOpa%^ zqdQRl-Im>(FD}NPww!lQPPlf+aT-mv`6kwhe*R09Y~0d5vuqKnEJ3`Ed2x!4mwiW-FV%gahr~sS|y$}{9|2~dv zg9=8yX}x2&|$euMuB&V}@5~)gzH^Slsou20_Yi(6r{-fg|XL!!s?+Au0N%^O?8e_##{d3j_o8 zxB8HCs4j|*x_k2lNoi`N@-KW)r>t)xRhKWV-P2jr^&U#i(=FYB3+T6^D8O^Eay;ge`3kzjJpo{HQ8&mwM1VSX3$ite=f9!!YBzn38`;8lu zH(}3Y^teN=H)X9D5Mse_uA6I_EWB2xhPczR`pEc~;ZlejP@0BjcQ~0Rn&#;?%tvQC zg%HO-9X+Sg?A4W|MRgY>`bRZI3Ee?a7+)kqn!Q@2=GVc@YOflRoY1xR=Q%SuMmP~J z$&S$APjHLj0i0)=jXFoO-htQm2fFU@nQq!DMv(B#q;=NX(jcy@&i3+-4{ur4^IwAJ zyZV}u6Bs|;mq7w*vlrz;?1e_+PUlcIoQBC)qc(0#`?#-sc&+oD5NXkw7R?akk3~!~ zK1GJjeiQwoPH1U?4crhd-&B$0Cf5wOv4@i_&8IgOyTj(9eIqwn@?p;1p^9~uoa7)! zn|p6}4QBY@lwL~{kZH*BO6U^EGH@y1Iil&ZltBY071EPOv1N@`R&5*4pqUlgRfuL` z)Ri+XwewvkX1h5DXC&Lk5L0IZj(s2&>(%4BG1#KrK;`Q6ESN^q{vIFNxr1TsEPnMVxp;S7TjD zyYVoo*#{Clk%Ec{8qt(@?15f0Nd-4g33*d8-$V)?*BnRY?Ea8vc4&oLiGD)xluM>a zc*)Qog~yzcnk5?esDfh?`BYxVHBFgXZz21ks>IXa!teIt(8c-mE#%jM;Xaj4I4#st zqPRg~Ry*Hz^l0=oVx_MI!amz8E93w6 zhRdRyL{Kr#eH`q)duVY5T~&Lk^0oo&Ix#jz|FGuIr|SmbY{gAaQuGKcEbi3snZ6KD zW~Q){J+P)$ll;utWmr@>KPJ}o++I7KaYs#Wrwi;niKSNjwP994x1g|pBTRRW+X|dS z?W9bsXtM}R+O%GekFOota^39opK#W)&eq;F3B4TcoPnuALA;n1hT7f2ebx~%5b%3t zqNlkT&>5N^RHT;1Sz8fAWXYf+3b9i!hO4}hq<&5alyCt56+MN8K={#NImN7teooQI z7kn`79APxyX*0EFeALd5umv_*#ZQao>-FRXKK$?}Otp37|Q4j2uH zaYK}vfRfD{fHPh%m{y0~?0vRZ8UM2+b+xT?5>W(V3_kruq9x<+~L z%@HZiZRgkJi1{0^ul8S}YwuK8*ZE9zE-#Xq8t&iyDCB0(_3L%0<&TU#tV&H^EdAfV zq^N_RTXhXAea{PM^F<$({qg-p+3fzEcJ0}jX^W}bWcaY$G0X1lyS&sX-B;@Qi~23o+#nllg)x5j9uoA__XE&X?NE>a17btEFS`0gZQ zXs~)$?Llv+-gJ9#1Ke<1ZT`FlY@hzVuvXw?^=I^W{1ub&GPW%HQwHCeMJf{e(Oq|1 zjG7N`=wO)+;u|CL|DHx$k;HaNl>(j4sV=p6@k{MIwEOL1Jc?~#NR+6z5OG;c8mfvI zIwhku2b8M$0oWTT#>?ytKHJ$bj_R0wciyHgECr}rdcyv+Ft6v;@;Z<9j#mPpSMMqA zoiF4mb_;^zr%t$cVobr)4lKx5&zx9DF6Rbyq8g%`sP>_Dk6Rk~{0f6&AlmM{aXP>V z`->1N+`oM`>_dJZTF;8n@!pWN`{wBlH|vlEXOA}Jk#i|PXaGq_8`Ms4n$%ZOc|~sr zp?F-_z#MzgM*#@)8 z&MYUG_`f>kc@?UkhsOUBamuQ@?v{Rtn4VWv?$fvH^ZYnhMNtoIB>%1lU{~5oR9~>q z+J2Xxh| z9wt8KQupndUVuZiJTYTWXN)T*jJpMP*4-2>)qo6v>`;0KAry}V9fsL<%m z-gTWkd#A44jpaTRn}Tj}9iO)!0Bw;GY@*sVKFqJKnm`+_a5LRgG%?lfzbRA^21J~y zJW5!42;?!pze(~?^0?1RIQH~ZF2-0SOnZF+q;yau6yqeXgV|$cuat`8+3zhe&Kf=z zo9SGVRW~4Ht8)W}Xs-JARy!I$IW46h z2XA>xXJpqU;{8qZz;MUHI=L{{1YD+XRabr6fs=OlLLD2JSI^$VEZ@ps%h92zE`$xJ zk$nQfY-(jbr0ltMFxiX6V@$wgla+bfjN@6Lq_vZA1zeHUUJ0i2IgC+q9hI8fwN{ym zvXo_Zn+=Mf7KmMe`wy|%hi6KYArHFo7pLZtA)S6K)L_dS5@vt#KLU8!%+-s{JEf(JLCOep)}PA>5_JjkEa-RYgF@$$Bk7K9ggtN-d>A zqUZkY>oaf9xtHtC|B)Z$VX}o{r_%v1J~^7N$aLAC9hi9QMRo_EpWL=lrSlqGvf*^v zOV7gQ4g`6GK!qOdUowfbjFj~d+z<~T`EW`u4rk2r?3kVk@jf89{ z`e5@bQ5j-U5EJ0#XphYSQEB7c^O99`9vAC1dDG zq_YjJJqXWJhRtsNtX9?C8RftcpZjb3i3YZKOm$dU$9Ua^*$YBOZl4y2QJaaE6fxZL z>Zq+x;$iJ-da;2uxPR;j1{1QstvKk2q{7{m7wNAI+^MzXR^N6+GJq6Y@-T4_`~*0* zI@`7hQfc;QpJ^EYDMg_md914Q^;dG{qQ+DMlbXJQZhEGjNMm**21Ly9lb;1O~QmVqcwQON*aB^c4v?j|FZ6N*n|EEia{Qb$i3VB2{HTW z#B66yt)3hUE#xvvlH%*}mPs?{6sSqQfmxC5Zs6#VWz=EgvJJo`i`e}BCO7@GnfEi+ z^M3D8M%MjA!$U-jTfFw(LNcSpE!G~hj+ete^UZ_Xrpm$_1$P>3MOWQP?(U>OIXB=m za=b`tPOn7-HQiAz2AmWNF_H}IgnJeZHBiq%E<)K68XgafbSiqX(wW6bRDw<7HOhs> zX~vc(6gnBu>>24yb^kNQ5sT@L6oZHpZa(yFidYNVj3Ak7tv$x5W2a}tBESxO5ya2I zF#&CSeX8$*78cv(2Kp_U_(Mg|LIvbqm78}_Ekdi?{Q$c5XiEf>gr$&$X?1l1h=`V| z7}S_1xMEa4ciTvwFCECY25a;^bB}hh73H@NGxhbbF4*wCXOd}Au#R~cwcznkXUCrw zYxSme=+(4RC#;+5g89ZaOO{NUWV8lpU(=2VAGiUBbR72}a73{wD!2a>78;;wmKE)` zE(x*8^NFkPTJp5Gj5Jld_oMySQ#B?x=kU@uF7g=cnj6oa-J>5+jpv{CnAK%u;-x*m zf0W=ph^Yi?Xa3zQ!MQ0CZ>~xIa-ykaB(l~Y6%h>hi@N#zW$%-dp*5jtuO16hUuH!) z{tZC(d~NsIjOC?^z_Tk)JN)9W|8VA6Vad@qU>yYbWdH;kI*{z`WnPa)v;R<=LjSd; z`~QQZdzI|;(;s)}LVxzP4p>(1{-k%BiHn(l&T<$m6pP37R#`S7!|s}}EWW$RR?U=v zv*Fh+FiJW)`FeFC&x1@%2$|iqe^G37He<1syL`P#Uh*n{76>qV>i&|Q-U?EPvo>JH2cW| z?xKJI@cr9di*;`~LU5?E;c;PR%HHH#)~wdmJ~8&#;wCy_ZnZni5LgpfJ1RQ%J}U`j zsIhVRfX(~)rPMp4NnwA1Y7?baly|zCx^d2%YJ;5V7|UNZeG-xPvl_rWUmF(ExwjxA zEiXMyKQaped}~gv8_#YIU@q|1EJ_BGPMjVrQvX|5@x{%an0DXS)Tb76$ON6C@Tur*>f=FY~cOwm}cG=w0d( zY)`R*nMEw7h7Fh&-WsBkIYs3$PmGrH)Dg35wWOdViRoQE=Gd_cVOGuyTiMGo^`jTY ziKSk`^oK5soQxRMQ81r%7JEz@>^?Z5OwBPvLQ8A zg4}r&=sO3r;{9IWIhe1?+Fj{jWazrn--N7c{61yFS@uM=t)vV>1<3(;R-S>faRxZ~cEE2p>EC+(ppj7opApv_m+eYpe z3SiflQ~3URv$|gEk}zI~yoW*_KA(%VNxjYM;+TV)k5%*FQS4YE%{9mqD#*vOX4Bh3UF4^K0RpqOn;L zY3u60n%qZH^0DTob=l9HRHb8SqJX#URj3anQ#V6KlX0EW04guDZ3xg5V{oGD8(vJ@ zjpS5i#{f-o-{`Dzd`P>j63F~5kJ4Q(F+q`Li8&_%;BxF6bO+~7eVdWKxe|qbTHyFv ztb7q`BXnn$5PouaRL9i;AJ%YFlgd1WeXmh5t)@AWYCAv}n=jqoLv$SF^roW5|<&j=-A^qvpUV* z+xIDeD>)uGlkIJx%(BGBO|6A}tF@MRjd*Dr0t*H(-B+?Sa#p@88+l|f{~49yG+U|w zppQSOIv%rr%rCrVOMtAfCz`2Z2vfWC`sb}1sp?+An7UQ9=W4H7gklnAzk$ww-WadOz4&Tx3u)_o?w1rn z^O|0jOoyP+KXo8@tH0Xd#8ugEc>fF17ZqQSeuY4x9KN9+&45$L+@ffhb1kO`Z!@nj z%|)DQAPnQY$4$MY=IXCFQ>*);jSA!jgDAEVr#Z{c!L?UFPVb#TnW+nIf`N*?snPk^ zobBL{@rO1)B^e2t z0aG$2yCtPu%W0|kRs`q9mzbFM6J=SOl)=fCmD+M>|^($jgirFX2{Z^K750cOVo6Q0s4{P%n zRK=4nH=xZeVtU|;)dJXbXn`cYPIJ+Ub++GTTZ=_J9awO_ENXw1DZHsSbUVZ$5z;4- z`SIehN%mL5)hQQEfNd3s?}AG=-$;IunDWdR{yorS@*wR7B4EEgAZyOVnvdQUvMdGX zCbgZ5b}jf^1|Z*?WP?fqsd_LmdC&zI9~;d(kOi%Gr@lyEtTo(3HUHTF1DiClRaN|s z<1tOFZfLFHfTv0CZve;#+nlB2MHj8VZOfgu_~f#nGYy{Zq~-E-9mq4ks8ZsYmKERh zE&|3)bLuIoIPl0d%sM34N$8%ddppQklazP8iSx_bqN~i;-3KibRD%<4sBFJ%))#yjww9)<2#CE%60d(rYF$SCamBP!1q=M*WI&7`C&o2Dd|~hOIEO=(wd-G;^HC5&y4gNt+*J$+cGzUnz}$0~-Fd;@uNs zJ`I`j{%M?mQF-F@?Z21u{nxbb-}X`MvQ5W=rsAZ$PtA>+m}&Vfzl$0Z-uj0oPJ6iWFy~TbbxB=hI}Tyf8g6-+K-%9=o3Rs z2H~5-&*!<@QeU1oPuXYfrVmmORd&98b07S+JJ~WHX#FRl*Hi7qR4q&Z&&Lrbbk(gqk_7Q5} zm?jz#Bt|5p<0h}E1#{LTUNnU(^RqIMzxt}$ws?;@kr6}LVPB(;n#eB4bvjoga+h!8R$8D&CD0j%Xfa)=_mqbwmAagVD> z)3cbcS6P#!^newfUd(JU2V8??992x4OM%RsF?oo4ipV@yG>jJz&%_#q12i=Bm^$^J zXc`$>hx>=r&Gb17O?gb0szOz9QWG3mWRTtxHmiUG>dr_~9!_>WR|ltg_(i*a{|pUb zQ$Tdzxz*-Hn-^9Uia`hM-cD}w%08-r#pV=o6Rt(%vn6}258RnzW_S;=5AxOsH9Xh9 zAj;IAXgu3cf;e{`T%7E1?7bpn-tv;!$4Y;Zld$v&seD<(OGozgZBR|k(C^#ZCE`y% z9yd;i1+3(N$nqP4K;N!t$C-NfY8l734TDjN82pG>g5m$h-g`zh*|_VTNS7upNS7u8 z0i=Z@h_uja2mwNoE(uaX2ho>aLJgfzq(gv&-cdmbJxC25l`0|@5Ea{d^6r_vX3orB zGv};zX4c+o{lDc&o)5{Btd;Az@9VmLDg=mBnyZ1t-x)0PtaI!y zfQvBD7mTES5X+PP=BErLOJ*536rVRc$`2yGu2XN%v{>mI8%4Z!@+<8^J6;l5=~%_} zr0JoEL9UD`?~jFgwr~3_di6MXLS!7qfjoS-5f_a)zq&pl9mW%^=xgZ9SdGUIDk#|7 z8v3mIMCxSGCXZKx^|$<;msnNlWpb%i!)cfQ>YS7vjBRYasnx>&%s)Kc^Pu8+d7_c> z!vgI`KqkK>$8Wicj(SHJP}(`hr? z=-O7Q@7A(va6+uG^FpbOkKkmiGu*IPK7w^h*-><3w5U<>IhN`?o3WErWl*y)@LNlZ zqd?22^B1r-{WibC1E#fcKAJj7hC8jkCnzrC=3GB)&%!#7DESH9hu&;iygO1vwoG2e z5o~5=M~iZ@HmHO-LrzZYJ^OcRePY2x0Wj>UmMwg5DegYiE!$2z_s3C0OA8gzH@JwM zYLX<<(fIL9&C9h}B+KHp1Ky@|dS|Dd-xlDIAoGXFp`hI-ob~pAx|u!Op9sUf#lvT4 z;~)!NIZOy;2W#b~Nw(Ff%^>Ni@6)39Kln3i>7R%I98}W3x@1(y` z86+FPNP}H9+U!oCzE4CtO+*5W$ynL^$@;EHTWmLH0Cy)bsTwtK%iqgwwo;>cJ)|0g zZI0lhT?Y3lV4*A0dG`W}I2gypI=U~<9n!a15~dWfX{|Hk_|So=vHS9+DD|ePM9RRh z7bK=n42B{NNBq6?Sk1YgKhVnjtmfA_<}XO_aq-c-%rx!kyj`5Of40U z1J)S4Y&>WohJJ8HaT#?0){`Rya{ardvdN6BNu!hGVx04%Du&reLjBGc;|A>EM+Z&| zZ?zyr&)AE~oq=>c9x7nPnd%dSen=u;IZ@efN)})}SgQE-_uyBDf zG2;tRUZY9ZXITAXL`!pj47q_vjYF*+o_%uwzwzP;815vCA%<2%4cd)7fG9Ch83CYM zEi7@+PFmHUv1Ilq>n)AbTg$-7wsEX~S1FE|_}aNb4{XP_(X^Hwy$J4Anc2K78Uow> ztJtTx$wlcjWM|gSr9>8AD`QPhHEsQAXjFw2e1sAD32mh z!Maud8gY!77Gqi&l=^rY0RLzs?0Ar6MxAE()hz>M=Qbc|*{%2W?7?}1!~Uk7gk4ts z0%X$Glvxh!eQiQ!*9Zfz1@irI{6e_8GXVJ1-Tv=_^vP9Uz;KKU%^wcUyFdO|{iZf0 zW4~cSpQ=PYp=h!3>i^1)NezzLpw!y-w{+qB@}-9)K-=F`umt&sgMY%3YMp)`CtrKt zdVc3{`-y;l`%j^qh-*)Z4g%d*?Q7zzW`ogTlyKn|HO9&~pUJaL-!_%G?*R-{u6e#=A3)%q&T;+1l)h&Od6 ztyCVjRyusMM8KKR{CzdPrDTPcyD}M^TZW*@;)v2wvBlsmAV~Wk8~$_&}J-#r(VLZX}rW4Uf+>QLRB#!gfO%mI!eMYJ9j^XPORRtWnW2$Nd_ zdd?sn!jy)z(P29=ZxGJkoTHvMF~d^9rIpQe=tS!#>gTBW5e@wAm9)640Ti5$c`8$| zFEyVK*#68uOgr>TQ@rXcovZrN@Tq4TB*gf&X_EERG-ItfhO5oG5^JZ;xz9R6Y1 z=wa0}tpmfK^@LLB&y9X=Fi<(_K_s3_G!xCh!t{H+Klt;|C3^uJT$jZQrqwH{!AxU^ z;)&>DG|{r-3sVH%&p5Mq(9?Ip_32%8Lh%f_2BzwG@oRiIPhM%=S}j2c1&}1KA%@&)D*`2X0-}>U5?|yqI{>T zzsu->t}pa$Ptg zFvING&B%3Wp@M3vFuckF8M=v758X#hL8NATf^(UUM2o)ZgYqJL{112m0XKfHa$oxE z1l9B!iHBOFg zZ)kX%qe&kM=sWhVue{I!<$ik^@Ka16r5kay4jnkVB$FdWqkZsh7fQcPf87ds{EPXG z#>w58@>^#lr0-MatSP%keT1*eoV?GIXsBCqx4PnaY4zKhIi}DG%Vk$SD#Kv6e=WE< ztnq5LJMw4f*-xR#mkZXBEua0hLLgI$+OtMTj5r7}B80<{t1@^v!9X3<0)9Nhc6MFI zCRr&3+dhz{xL&Lb>*2UAFMSmxh9>1FC)Hw%ozdm6viT3ZL`LriK@NfornOwQjT|)A zJU1m3iHHzs7DKyZGl{u^aqbJb5s05W)NL=krG7@j=UwoPQlbw@sfmo=stl9c?sw5W z(1S`7J!VboTDx!o-Kw8p{i78&A`_tc&OaZqQ-;R=dzD;{H&c*d?+>Gh_PxO*Qi!>L ztRa0tsGs{}q)Iry~%o$06RzAf|6~_9+ zC?z~FmncKcR6(~X!*GL_=-lDFGaxlNjdCzKQEN3F>st)|n<{+pdLnm|y@J#v;Y$I! zJ;l?5BN-gpW|(%xbc&_D+rephqpPju5XhYh5{RvQy`rI7X^4hF4}O%|Z~D{?-L$#=$q-T0!A{|d ze}l?KTs%{&0DE;kOasrdY5i3f-EwqhH>WGObiYPro{bI#fXA~N?-WPk#PSf$h5__t z3Q|JSy^H`$Y0*AsO3&3yofQZn7R6GEGo=uFBEr|iL9LO)38at+BPC(vJ+FIGtJzTz z_}@y}V0T?Uio`Fna;bzNasoHln`Fj5`Q7#!ZfM!=u6DaVI(Qc!bPZ7Z>W!bxzfVTP zd*zCD`tG0^{z#Vw{`mE81Zk^RV959Ry)GMYL`CfX1V-+Eq?!8vw{vWFGV2RGt%k;_ zOW;2YcrzQs3oq=C{`^(oq8>6eN{Qzj>Q^2$-(EtU{GJg}e(*QdkK>+}-~7|>VNARp zCffHXjElL}>ME;Th60H50%e8VtNb7Bk^l8vb?pD_HQ*?4VQQOd8xgMVWA(F8Gy^q1 z^z{63?dW2q5~h}Vf9R)TP{LtK-iIg0rCe4O^j!_p;!_hcMB@<#S_rJLm3|H8ru{b+ z>-n|AFD>gA7vqJn+YW>OOh>I1s{fBNf0|Wu{e{xrK+`U$M`a%Rvoqg|DMK&uC-#M= z*JJGL^FV?b&NW^+hv;6~1~-zmhGe~=OhK2lQoiYr+8OPm#;*56HDRh>ia_@)Qa{oU z*hwOI%egNn@!~c)yL6sO;T%}3k0zESZKOv?wuM_EWv~)wZ073;lDAJrEjoO6+C2*x zyr4JCLY8*Ph8MCHQg@@3e3pC}NhWQ!mCksxj$UI@!PFuja?&)dur%$u4}cF>292OK+Z)!JzYb*tM`tBu~$TmNi79zv{5guxD_ILLMve58jel zPA-4yO^d?TW{P2osu1>y+bMiwek)x92wLP_yl&lF>lT?|mKA+}8XYP-Jg4EjVRxUX zPC$XO2|ruevaiIAWSlT4AOtQE$PMZ>4`LODH@!(wX)jMfXCW>lC46<2j8^UV)Q@1b zAUmSha4f+}avm zT@skAGt|IyT(6KGMyG^HA(S7kY1}elbQxG@RoHUB4 zb;&GCIP0z+TMUakTFz3 z+3giS*S17v^oD4Rv5FQFZhGoa+W$VVG$cSSSfwxGaJy}~leNkvOQIo;9gee;rw4=m zDyv?C80fw3EZIh)sBlPB-PGPkz82sXkB@DeeciTBA{R0EN5)H@aCt!1|nKm>O z{BKPf1AyBz^sXBB=Ec4JgIEdig9a+;*bjS)L@G^cEci~h${elNB2(c`*|LP2|73lv zhm*oV<2xC1*_4IW>2P*8Sj+-nrlRn}rnie)NmaZW^$4uprmb|5I{jcnFHNpY(ufq> zaudf`q8FfR%E3S zL-qCChS>zk`X}wZm~@%U?z{-U2yfBL0&WkWz$l#B+y{*t7Q7u zYe2Nv!e)dBG;TtB87D`2)6NxT6eXkxjwMzs<8yE=gC71LX0pE_zvQ`YKKm%<{vAT{ zB3+$PH0Bb068Rx{|HKjHH3fw(e2mSv!I-B4{I^JEa*7MI474E--2w4JEgLZ)>)xq! z5AlQ7T9!0RfmfDaP9J`0;YI=XM!a@D_EBOIS>J_FA{f*th_c{Zv)BLhK7~RqeH{IT zRds5cM88Z$mc3fVdkH=7M4Bcyjc&ejTHXB!sjm_!qj9dDJ+chYzI{tdN6i(@{63`G}^>Rw=vC}y3&0JTdM_GI59KO~N0H~HIfxlOAr>iZfR_}XH=R2K=jU423{KwCJa1EjLhMzaXfyZPAG{_>{h8-=d<74=GBD{cEb ziYiT)w3hSyvz9qhaV`DEo!8Ss76?Yn7#p~xF)83hK;5M}AGrAp2M4)CZ3{2G!vwr# z&V`mGLCw=+*P32v9CS;j@>j|DH`&M;=v)-4dGuR`jNLqK`t4RClUsLv^}ArIbd3ny-OUzn=HbC&>A{ zQ)fVziq}c#vt8Q3WAa;8_ePo#>f>+^1YdNbL4rd)*C6&NAZVCu-gv2U3eJq+JjB3? zWhPXYjrZ1(Rw`Eo$#ce8o^U|`Ky(+|8gM~P=!#0k_PU>O6XZud@kW480p}%6nxc)e z(`^jcoPk>^`5=vKLAwV~9~$&L&*42&doJI)gMn|g50W<~8%VnXcrU0$(5 zv`*BY-uep&2^szH?d0z}FU|jJWc9(KVuJ<02#t8q>P6O8MO$ zyGp8N%M!AUu0Y<6J*0GaMg4cQ(g)IcAnp{g;@UjOdkOoDcf7$Rrw7di66C~rkLJXK z+(yA#1RK&QwaKnA2bwpgRqa<_HdgJf<+R@QUheDbo$6$fmrkmm{H;+gsKR8l#L+1p zEb-cQPZQQbkQB|lAr8${7l>+3maX$X`Z&;2RM2w0KVuh{8Co1&Y87KFV>WS$WDd+V z?JJ(T;fU1LwcY7#slU)6`6duU!MrA{V%6@DJac3-pHeeAsSw|BP@}li7iS-+vc`q( z-QU4}K4`;o`9wFogc*pmnpzYi%dY?~bC;BTU3*l3uV2ezlVOd0;*UV9HC3GZZc83y z4@TOS*}HH{c}!Aw%C>lBiBjr*EL2hzZoyLDrg88333YRJMu0pN57bDIA;qTiX2Xdr zRiSqphF08UiED*rY(}>c#&d)tyKzYFCbi>2vcu@_B_GcA%|Q(>+YHLZZgIN) z@uob=vGaQk+`ZlX{zI8O4$IqAYvVmOc?SIVIZfhtNxtyOzoPpql$1}LRQZFGCk2idb&JQ|TS74s5uAiRv#Tq9L5( z&dpfl%i3fLv5hBC3T5d-F_ikq7?Z9L6O}g(u?SAR7oczsSv0Skolh5ARCUyhN3-0| zt3*YfN0a**t$^%@VjrY_p`V2{s+f>GTwU~KI0Xai#h!h=jz9AXn{jK|0y1w8 zfeCj%3ApIB`-$;zg*6vu?Dt7F5s`u)@S2g~y8z=t=On?hgKoV4UU96l_4-Dq zh&uiR#Ch`x(MC-&#av7lLo(y$6c4LY%eua}v409jv5V&Zv&L|{)rsrde}t&ZiZ$LM5;v>6va!G9IIn1k0ZXjuc>^{CV@ zE#qH90~G+WHA?3!K#aAnxNv=|vj+-I5an3tOSkC-+`t+|6Ef%=z_3O)4WB@G(Tqe; zRO6IXeREPkH;Uc~9h|^|Wav}W2)`r8_I7#&J2jXKo5~3%PmprA-&u%AN{U=DJtTzT zTr%9V{B)^)_P<_=fcw{?E(Vj|%^I55aH;IFg<$ay>!^~RlQVmBuZmpFuR zU)q#`mwc`^!XS~7Js^RYT$F>{>21%D@^{Y0y*+*qvRt>}=tp9}$Q2rN8k>UnPwBjF zGq&s}oju+DmArzmx-OCM7QT62?)NI~JZ$T0!X@j@Ur2c?4U z7UGdgd**d1q{9?c7-dWD{vb*#2!qbO!Lep46$&AThL#gk z7lyPT{dr281@)IL-*tuv&qEnr;oZe60>)1A`^5$r+sJ=YC56C@BMI@G2I|dyc?w|; z`f8@_^=k?2>J4|RZqzv~Ix*7G^j9H#8IW_4dgBYF0Xp2h${xx(rHX;Y)P4#%BnXnL ze5`R?jik#4-tT3lRCC%1;c{kF=K`tj`(O=JwT(ZiH<>4PyPOq_!#$Me!}b4$AJ@Bc zc^yMBVov@|RlSFozWI>9J{v$uD$h&(ztw90XMA1S&TOD5qf_vb3H+8hT>~QZ!rJXx z2*pqOC(JrqrA1m3ST3B>koKV}fiFw3fA&_~AIZU{@lPiW7!M!!vl+^nmxO>GPt&jb zGfaj3+c0&2CH`OQ`~Ri(1^NM5Oo#eZn@F~e>w^}=317uhMu%XPcx%G}a31*hvs^t) zvs*~p=ug2ky1s$A)aJ2lwqi8_Y0XPmi@mseaE**xd1(?nC|n(3Pto}~&;R%D=Zbrp z(sG#CMKsi!PR@S#67X=-@#V}tXK;!D^c7jE32=YJg|9Q~U5Q84;7nhjZ zAwkoz5vChyeFUEc4>r2M4MiDNcRAjQ+FoXi5hAr0lYJc|5fauC0455z{z%WSFlpq&b* z-;b@G7(9Z38c2PSlJGnmadKJV>nyOHU(hY+YG1X=9vPp066B{XCD=nnNHJe5nYfP3 zz2%mbXF0INk4MG~hM*h#eK3ZMl#Ue2U+)o`D{_`j-0^M1(au(n<3qUnLoCF0Oi127|q(^<6@&JFHI?8sV^(HG`=F5A}Cw(`640Y@#H> zNXV=cV5xy^xqKN=YvNhbZeg^EGRv6bz6rvyLGIi2*5jsuHKX#u@97tOsqs(b!BbqV1`9 zkacn+CW^MxeWBHwHd31}u=YY}ZW!la86Q{mdI^DBOdaXRru*Cu0~Pfxx=Pe)8OsTR z2Ej-M;}m`Hu=gIOO@N!Jab{3MaQSVu$*l#m`>`lv^XGjQI-n@b z?;xJBUR?5_DTAgRJ@T4p!+tb>+YdaYmZT@ZCe_>QbVlA|q zMd>1|XSYpsqAuaDC-G7tE_uu=1|EPkyv{a|OLKAi7Z008d2oCal?>qfc53_uyrF{7 z63y@>cP1>A>zB_~*2RZ-O=ca|6AP6cu-*xpijic~KX5ubRbJ~3t+mTV4H6KQV(eNC zVdetwkCK~-oV>bUTbl77bG1)=#qcL@-QyhNKYH$wGY~ZGY=+2!C54M(h@v^z@8{oC zT}Iu}D|hJXUc5?{I3b0$uQor<_aWTw?&7(a*p^Oh8P=y786k%BwwIQeW}Qn`JwQD} z9&#Uhd&Es#;8Jn?%ygp3CQbN2&FKSrO)KQ~sd%6BbOsu?uU_PP?K-uOpW$f6f)$7H zDMcrk8E!LWNq>XA_5T#T;8iTGbm6Vja@cWKp^wsyuQyMnG9Tb2Aw|-c6J%@l9Xk%! zT!aNmLp=uMY($d%J94f(fyGa4AH|wekee!v+VlQ>OlzHsCIP;S@3HGtfXHUaH*bzC zE*I@wzB-8xO(kWf03erH2$%5++O9IGLRUCmpEoOr)08c%e~TLWf>-~+0_}mTh)8!) z(<2s~KJFv)Oxo487EeAJzsa;1OJ|HpDBOdXg~{{qC(er=4mrh-#u~y867JPcMUPE zZHTr~GCZ+@-oP6tvA_%PTPgGvO*{p)A~D^yx&=vH=v~Qf$;DAhdVLU}!)G!HxOb8# z<9`A#=@hd7?J!98Td}{CtCc8JCybrG)2r`03Aj24g8>rP@)WyBcJ;IYpuzia{anA? ztSLXISPn~NmoRL{^V%$wGr;ksb=DsRF;8dj&EovHW0!(69zE6&rS;}1lk*l#ZknGF z-Pe`7w0A^y+Pa#(brfCrgA%w)hQzEck5G4uIHRZIOijKz5H&lW7A#6<8z^%#f{>^NnE?!O!@5d|SMUGEPFvtX(Z&C&T;X00WJyc>gqBywsNu=Z^!GC)kzL6y2i)O zxRSMZdja2@EMI3X1_>_V&R|+fs&BCCT_b|a0wWmH1^{hHv6Uo#s|~8&lJjP7-8N_%Q!s(js)kX^KL5DNR-{YASN=5b~Lb|e$ z3>p05*7aGQkv*Y>rwryRbe~iIrXq?XDBbXsId?S$<;xtOp0UF5*(2m;629op>5r7Z zsRW}3lz#81B}5oy&4U3)<^LtKJua5nZnJLPG%SaQ;=LgkSN5HMCyG7jf+*D-1itv2 zsulg^Cd>MM-GKYz-reKhz(G2*3VV&uT>C}_sf}o~LX!eRWnDG;o0S4ZX1o1YHq`!S zeD46j5_6cJ1nd0DHLK`Cw8dTB3eV>C-rRRDIQ7QGk5+#Dw7=t;v}2|y^tvbj;=SjY zk#*$@fobQa*pBhVg%#hIle>4)8zz+(tF(PF6z=Loz`uXW2Xvj~=S?ZnjzLDrcz_$v zkm@XR0pTjhqy|*Er`TW=aIM?aJw!Su&hgZ#)1u5CdS7`xt}biPuc<7#B;AjO1y<{; zw_N4Z{&ndbIs1>)fQID@^h9*3wg(+sDG&-_#Gf-t=u{AvP-@HUTCFcq4Yp zrPVSnN-hA$CJ}rSVBJcxpP)kyN;=y)^cg&9fTy5{;i^21m{7p%5lS!%Sev;<#r-}^ zPa;DBWufwCXo16gOHA1UB+C6TQDe}~(nUf(>fuzA3a44}b#k+`I8L0JkOlB=a1 zI|z1AqItThJ@*$ld#PlGT>*fJsT}ObDIlaZ>MYPc?y;R> zDwWUHG!%GLmfE9f@;a#apfm#;$JAniD+rd#q%(`9 z1IF>C^4nQ=trT&;R6~P#SkTqY)1eHeaeJN%H5{E42&(!w#pgKj%-WB7dd-tK@Td)2 zb-|c1`}13i!W5oM?cY?P2i5t}HYkosDUjn7R)W>~BxbQMN$LtRu<&Y`?J~UVt?9Ce zvixGy4oq@1mPyUvr4B zQ#Bp)y~3u<A<6I@Zlx%T%-aP{C_Tqi#0YS}q@W;vMrk!emfbLo9JV0)P3gyTK3TJYou*s?*I zO33n*kJ_6hD93bH=85Pq9(2m)gL8iIcRd4s2pj~DTCOLeVrqktgMWVQ3A2KOamqB; zK-?W&DEC_n_cigaW32WYDIg~ZF)p=b3o{AmPw@!iQW%Ymc*(Zl1`+^?5&qGT_B zZSH5iI;$E`m;6ayx5+Pv<$=jTv!dHCn14Q|EibCWvs>FsG+Aay-8iA5TgJKO9Hb_F zwQ_cz#AWHJu2PuWtgUS8GMc#HDcwES2!CUR%r&N02iU!Fi{wr>=x4QLR}KZa=u0;% zl6rBq3O~hLqYKs1=M9%%d}0+1mUbw#I*`fAf1!RO&ipO~RKrOJQe_{VUixEIIQUW} zNiVcM&8$47&e>WKACo?|p1NIA265S-QmSs>=N#Llfz7ND=F-LihP!68T-Gn5#DXl~ zKOMuy{IY5@N-**j=vSe(g&Q<+_O5D}k#T@VD(yFd^k7Nf=3$`wg@xPDjTu76xe}|V zK+H2!6Rs(<25>A8WxEeBof4`y0<7-zQOuYR zA<*39=C2l`?~?xxpFLB>5~92(y}Z5V{4j56c_4N zQ|^!Ed%m0y#MojhPD3=#)u`SV+x)=hCm%l2Y&iqAd92Z7n8vwLf69s^G62fG*Ly{2 z{6h_v#d=71dy!J#M#*6i=WSn1i%F}zQgUZy%#?Q5&xQ*N`!4qbmgUuX<<2gbuaysM zU4<$ayVjs%&9D#9`|6FpF=H$Ze%QD&+t$nWAne&S_+)id`qXReVi(0cnh-Pt1PNJ{-y@obIBs*bqe)DBpaE&%F<9iW> z7sTe2{o}zumd^Ss*uAG>W#1oN`SF8t{O;{v9Q=(EQ3aGGW54eIB~g6oKmT!4-Ikr9 zV-{5*BTXH(E0KASi?&GHYQMAFmKl@^x7Sur_z@8M5VvW8fP?0T^2wJj+{Az?+)4F!1=j$6v|$zN3G! z-Y~&R1bqvyoltN7YF%JM47Gh*R~%EI60IqdU6}3TSXe?+2M)EIEqFRWX_p8uMb!-6 z*s7b*S7#olz<*qs;{~ust)WKSkKj{%^L4rfwr2UH>h?jL_y7eOR1ox6;&N&iF69$e z)*`*9mloqh&OH<)3-WN*D*-LJL%AJf14kq^NDgW=fHXl;nNO<$9}&TAmgfDD8tovj zHA#1mB!KR%*LlB-9^^N?fvBw%wFoHgqwnaNFg2_tx0b9YR7)wM^`xU0tP$>sw#GPC zg*h`mv>xJ|tBfDQNsW}OlPn%<_pVn=G6ZX3-CNe|d0`dlIsh>=yEBH)lnw;G186b! zu%@@5FVwd1HU3Q0Qk8B|v{ClJ80^1Oy}u-BYH`s-4Zd^R@wQCuU(xuAe5&WGjnk0! z#|&X*2ee=f^pUSwsfkU`kpsHx^EY+$QHy?eMXOH6-A~3xrh)BP5IA6 zSfpcA^CtoW$+D!Hc^$_%o28dRyS$X6(4+*AJ6&o|Zla#gptJ|EqvO>yhe4I4K{6Pz zvHAvEG;+6F`pEp!V*r;aof2f(ZrUeSF6!6f(EDYUZgq`H=A<+0XP~Pk=27k2v4Es| z&LdALRa+Umf9?TmyBRV?^WN#ksuvU)J)!57=w6`A6s;B%=BH8_w7q> zFYLZ3z6Wx3s5o$WodXQ9{;AO8T0->UY`)}u1RnKxm;11o$qC|>5_M$bQserRK^;_e zwSX?WF!H`_A3FF|lonH#X1ll-`Qh62$rHP$3*thEO~a+*0|lV%gh9`?YY>u962Xlr z-}7li+K*lJNI^%?Ix3}e`;)N4gP$4E`WT{*>+vpWL!9i>A4W^oth}QEkSoOaL04c0 zYtMVwfV0(43tw3ym^qaxqb^|1+J@;LeR->GTt5tyRbOOtTBEWd&2!m|UD07u#PInt z(PNqj%o@RMUYxy&Y{Gf1ZNh5{d48@QG34Hb*!y`r&IkRl7L+|-Pz0A9 zM6lO1>LCeMnGLGe%e_YX0Bk-^{kRjk9OcAq!ftvR$3cjI49Hl4C|hUh{uZ-2FH*31 zbN*jl=t*+7T$po)Pygg=x6z&QfSF83=UL-p{=YPkWrc$q>h+G3eJteMSmXNbWS*t0 z?L}WfXa~5iLF<7c`v- z?^>vk?%3()m-ZGmT=DamE5cx5G-w8YA6HRKhrY^F7!+Z zbIw{s$6sZV8DB3jf_HNnk9xJy0g8f)K{eMW0?us~XFy2FDKW<^u)o17{AF)?fXp{A zzB;{avQ)Ch)Z_}uN|)%*tyDMv97}Ti)!W?rvD?BUW!qBbJ%eK{(h*}1S`Iw(Y*k*k zC-z zrjR?{CkzxjeHk7}h;5iALCY`Ecej;g={Z?sE|g!LCL*`=G+Jk||???GDFm z9K-L2H~+++uXd?}CSnXMo?fGvA{Q6_@ju#`-Wk%j9c(`crPv>R;{Njsz5l*_$No!k z?-rlN^D9fA|A6#*zoh$|vR^uz*+eD0z4P*kpP|;G0W>^q{d>U1R0m?>qAox?fie@s zt~b6;@;}*4iWW=40gpt59crVLA|fa|&X%12Q(T1VUyuLSp~(pmqvTib0w_N`_h-d_ zy65dIEH_Tzq4SptHwu1af$zbz=6w4!O%L;s98vZiVJ~7{Py#)_W0v8o%Wq~(6a;Z2 z!|(rkdF*h07Unsv8a)@0N>S`3|J!h|gu3y_Jn^I% zSx2Sh^_OG~Gn>Ml_wqECq|~E4nX!Go?e~bBC)FEwTp+qs)R><*oO1di*p! zdR4ZBZ0XzYldTl&-Rl#&0xFT$C)`(KJ!(&tNtUW7jjI()=>}e&Xnj)zs~ACL(L#OE zLiDn=`Xuh1D0k8GxmeWNa-O+lZ9xIm5on^L;CT80#jM^U~Y3hQi_c zp!@(&%Z>)xgp?n(=v6t-!pL|75V5I! z$POa_e=CK{Y!^p^PDGcG^n9rp=*yqhG!-RK|fREtA6AD zO4`*DsB|8mklYyf@z?32*~qUgt2=|a=1)_71@?LV+m3}ZJna=Ja1?0}Q2utm8pe=fVo7xy2cyTonE4EsrXH!V?Ue2@Lbv*fa-+Rx_9Vvygs+#ngR z3Ob9SfJVt6-Acx4F_Um?*g}+q|DN5A;Ou@2*Z6lgdj3+`Z>J!OD!|4OG-uIvzO~9M z6~OBj(@1>Z;~OAcNEi#SUER~1F`$-1T4axGyFK_+0SU?DW1jrRsxl<(cBI24;Q^-o zY+IX7wP8mf?8dX}wrrhQ{T$V@?VvIJ@w(#^QbRr*K{`fPE`CnFEi*KbVO}qIig1v1 z{j?PSE=uid6SjN209QP65n|q1jQCk5=+hR4*Pp@E+MZeSsDv>mf_dE?dbu)_66_VD z2m>ZPMj-Wp@b0s;3zSa3kxOLNmy39oQ$It0M?&ZuX$suoiPw_>$k6K*D@Efk3N-{N zHrvu?;e9!6$T)c+!^K?b`SyDn%@|$2Jv5VXjr1A&uWpR&b?34b3oT;`>YnZB@(u15 zIM~3sZ22c7WTDAOfQIDf$T&$g@9~~xFj>($>XIc2yZQ5KiLf~r2`T``El)2$R9Hzqd`D?`y zb=~W?3>gH;xVzUC)!_Dg6}$MHMHKW5($5I+mI?X99(e$th1GU87P(R zeQOlbT<+w8TJSYuSZYaG%W#-v>rfbtArNcbUux(yM(os|vO~e?L5O zoBZ*{%wJn7gv~X~jS`~9q`531A=njFwrHk!Ct%VwDNyMPV?jgxfcC%Gn=$!!MuBVx z%p+w!>Bcvm$l1-tt~2jyFjQk;iIHpHlRU0>e|(}*`D8p=yIJI`_gI|T!#7sO?0-|S zw-m*H+b$4&@~!6G!WEx@7xx?2SYbUGVJ+&MCss3-GOy3F@pmXJ3jxmA1P1J6e~vuZ zrBGuvqGco9WVNE}AzXi7%0$van=kpbG+VjGZC35ust>nVLCF|->a844g5{sRanXZ; zi>|>!{Sktv&0N6Ry#zlX_y zDKMnMA&(+iwEZnc5mw=MSw7NZ(&WxKVOHaxydf^u>Fw2qQ65g%9Fr~^O|g3b-4FCA zSe{Vo$25#Fz>vJwLGhNhxO*SlpPbYPVyvv&%Nr2h0R~UcUm>#FsR+?ZseZ1rZz732 z?o$+Ct3$i<`8uNmc2(X{y~V;*LqMHmCDP9Xc~^0ly1CrFJ;P3s=3+cjy`hWw$Wa}s zpW5Nt)rH}7(x{I@YC_vz!!YSL^2uGr1P#1Ht7?dY!W5meN&aUSMRVCu_E%Ax(3z?v zPp2sy=`MPYD}y}AAT@cCo)G77bxw(k7~b;cX??ssmHWM50~55cx$ygIpQ@p| zag51Y4n}X%xQmNZtion}bjSqZ){~o~ zU%&gx>m26?9^Ao4?SJtBu3ehFyvcUkZ)Dtdzu=|W6<{@`tIPSuKf$s~Us2xY^_)+; zbteAQ7cbnwJ6v^W#p@9tBJ+&@)K?>}EzQyG*pr(ldv7CgnBPbqC9dt*n?R}CetKI) zS$?##73X5pVi78r7L~@Pmtc1L1FAvD;wO1a3w-52u~g}kjL{2;!d;VmagEbhst#_p zFO_lIxDx&vCABA12zuczfTKFeoA1A|cV0nFzFoTyAiYcPy%#BoQl<9}AykzP2}OFd z&=EtCUIHQl0Rn^`Iw-wHB%w$ZsRAO>RmA_!`_1gxM|;mc_`ZX8<~;yH9_AjnpP7g2 zzSdg5>(LxO`%C7j836k1wL$bFak>kGqr|IfnGa7sl4ASMH!u#~SkqQ2K$I!m9v#sE zl%7KL_)QMGWwJ@v1Nav%db!x(NS|J+g~V886j3kSbUvBNfCZxGziQ3SsV!k(Oe>HE z9I^o74c%dPx#uukVJLSYAl^a{q388J4GyhFxT4${3_9m}`H?BH$~B@n+2TYwSgdwV z=3I}-++z;MN-K|vPO!ED)=G%`%xzLrUV0g)#t*zilcV2U^|rHe{b(v#VLL^cSL z>HG|oE_&b!3a3tYx8i)l=}8nw{Q6byM^ZI9N$nkbBZu16mOmJ_f@G|LfbxjpI5=ClvJzzF!R=PGn_i6NO@%rbgFRLs<_Vhi~ z_>3DFg9H@0QTs-~+sf;2f-@&m191}xxVDXS+j|kUxA>Jv5X}@JuZ+oz)K-$^!Nq8c zD8Vw82OcyML|nINl~x%#M@*Yv@JMieF3nB0%J$-)_e`pOs>mRDUc+!^yRx@=zm3R^ zD=0uo8I`^wKDwNID~tS?*8GY=aw87WX2Nk^g_|%Tl=HK3)sEP95q01e*ziwAD|40JKP2hA=AXbXH zZHR;4h!vMUlg#b-MkfiCCHqc4Wh;_q9vmMl%JkS7Q71iYNX!jS{%hm!7qxBe6CgX+ z8UhE*pXO@v^-JA836cGCj^;Xi>zBy%z9IOTme*5>GFq!>(KOvBwo-)){Py=i+P-gE z#R|Q-B!Y8-)sCa#;SILuwhG)$CH;v_25!|b zgYt3oB!0)YT$Zwd$#%iZjnzQdt4E#Ka#^fUU0l4Y{$N*W5LFLtP+PIyo{t(@pB#u9 zs1Y-OGjWtvrui1TN8JsM1U38vkS*Az>#SWU(;e=!=vBP7w0@cTm|92H@Yu=P+_P=z z$imk%D367Anx~CiBG*lPUd?X^ z>ZwgsbZ2_!Zuh06sfPwK$Z?Q)k;|KVVIIC}Xh}FTa4LACpehAR-xf)fSkP=Q|1`&! z*@+>Yt23KB9GgQtrB5YNkviHc98X=Pb%I5GQXdvuS${yn(8;+%n)n8TKBveH2r&!b zi8V2a>hZB1f42)p&`H91GphouE>R|YPZ|pOvOg=BgR5q|gLEXiY~1BTpmQx8v9EjI zo}O5V;7iY`c02Vhkf$TwW2(Ft!ODuJcEYEq6TKJJ6(lyY^yY2f44K($N>$!W!2(m} zI#aNbMn|^GtG^*%V|nMk`1R)%$Ej5-`F{$5o2BFZL*IGmEEnID_615gTA2iIF%`?d zF?J`!k5)Wk+_&{?n$dDv76~r&96vHDxkQWMi`6BCT(&SKR&+Y@xjDEzeN{+Y8~+yb zv!qvA)>{nQ{OK={rb2c+5{K0j`w%z$PEAlsFf*6AgQ`tMlYO*Iz)*G9k+qSVUCmp4 z{&LsUpqpj=*qCq(_U(Dax5UfZotMtZTu$`ganykt$sPUgEnqs;Z;X}QxRdz_vm&MK z2{_D1syC8s;sxwqZ$A1ocUkpX-3jBHs*hsD_KEML1Q#ZlKGL#|@s_07fGjSthgW<{ zTeaTS-j*kADW!tPKTuHCNQ7*+-@XG&^7E9j`8KPT%swl`!we4I*Q6s2qju*%G{2@) z*Ap|MUMf+Ajy+!ut2>Fb60zJvHDsM%(7wjF-K8L&QxShSy&Lm|UR$!apHjbJ&BHOs z6C?{~YgN1Y%aBNpwBSpy^6d}^G0J1PKR_-paVK$JPvXZ%;vT?$2lHC(7+k$DxWu%S z_4@3gXoa_Yb#kNtfvDkYZX=^7QVeBs=mxhHbX!yPZi z)oCprS1_y7hU;psCF#!&s}LEq(%JCxXi#JIf{^4qQa*V+657cXOGz2`{V6;$C0l(A za6Ll-u^#ZW5yo$l4+NdfN!x_JLMwFc=Y3`uR8PA1?ZmsRHeDdNMCEImDknmnry~@! zoErIzRNW<2ozxGNn!A%@$WJHfb6#Gp)KFG$tU?8qcSko?zqq1&B(o*)86eK z1URofUDe5$zd!9<1!!8aM&~(aAMii1EU@x=<#pfYO@C5F4#1B5M8-ZcV3jb?bI12z zH2LV^*)c_L;@-2(a|yo0Bjb$cs|2G_U_v|T^%(n7DS>weNq+lpX0DR0XECzwT>km6 z`Xr8!{E3bBuOwc^i5vV7CqG>8M3dp4?NkcWh;lUJGh$vxgdMxQouV!c5VXtuuTnbc z|3De}KWqM<_5|jz)R;DCh&Z1@`qq!#Uc7zk4Y({CrTpg|I(h&QPcaVKL_^Y z#@+!KlM;}~aK8?b1^hJQ3?6lnDA%7Mi{qnP(vxZ zKwvTDz9u_<-?vnf?;8@-%D)^{X)R77bS+>3(`aVCL~m2U2Frh2k}KZC)5Cmys8{W5 zyFmw%$`6Fu9RWL|vANYcBxMaRE$P~B##P{eoku1TA8nyBJWn{ku@A2$**%H;OvvTl ze%e9)7VWPf>ZTxNxPP$5ieCEk%OK0pw9of($^gnq0$25lNM6enN0iki#^OFj8VrTM zg*tR@W{$JFqN+;Pp#RdS|0PKU(m_n%O0b_Yfy@X^t`scPGa*(q1D;A$f`Rf~?`s|y zDc2D}vlVJk#4Ij-Q1%zKXUbx50kH^>VYw@rd3eu|c*xe_{1kt~*MgTZ{1Y1WSk7oFpC?dj8{#BWgF_Il)$R{TQwT^!K7U-$&bS ze=ddPihq2Hh2(EVRVoZ>i*Y%nffXk|lH7h(uuwmAMxQ%pGoIAja^Kz1)80j2^=0|4 z&Mj|mn8%L^a%`z2`*&8G<}P5emzsopRfjJAABqb4#Mjp2=3MH~UT=c(X?5iR8_$QXMKLV2Fmy zSJz|Q^jP7~67whgq+bG}Yp4FtD5?Gr_9$4JQTWmil8Cz7S;3tF@(#1kwYIPB&G7Y9 zkt)YR#OaxJu?(vEP?47jXBHWuDXt>`uSaCvlkRAHT*ZU?tgC^r!COpGiBB;F=(T{L z?H}qjB#O>+!dXlDgdoQr*4dr#AtJ;((TVZ_K2S(>iOuK;V+vWU;%EoxZ~4LtRiaVf za3z=a7t$M?1FbEr_@Y9Z-wPTkVMRj4ZptIXUj2=YT$Z{9pg2*a$Tz(ew3lAsqRd&z zTvcdN`(+hH?A@9-`9lzjr9R2fW81Z3-x!eeQU1H!7VuJfnc;Hv6KQ`Zuyr~ z@GC);6Dz@_OeP$P$lV)pKR^P4lV2|m35}OyLu+x@4=I;@FzwI(syd(h(jfYS1J?pt z--!cj%`$aR7J(`!3G=$C@V2pVYEs7fWMX@3m9{fN+ zT`xF!S34T3l|_}&*1)PLYn5jfVrM?6^8h_q57%Ug z3#&BzZcisp&3$dUj-hG2D`oC4kFTj!T*+e};cw299*b}cRlFTN3u;vilyK|tR7)_G zw!gNiChmL$zoI3K@Z=HzMhAR$=rdA$U9@zO5~E8rL5!&irVvFWsoVkd_Vn1!fi`(O-bLtN~jv1QQA!9ZG8xD=C~M9XgEN%pO@@K zC!8PMNaj7=&a0d~)LU~Vw0LH{aex0A(Y!6_jxlW;4va*_<3(N>=YmZv$TI1GL&j!69Q%+NaBBepr7?z9 z+n5QYSAp&q&Lso!v}$-ml#4#JZOaTqFh~ zXz2zKufc|NS%M`7<=ValY@#VM3Hcuq62`1*RPJxljI7d8JtM#dsFOVbX;9=pWV~uA#bLSZG|CT z)dnQrS!9x4S}VQ-kq(Pgl3^+m7~$zSO{n2ZO;5lgT`RKFIF4ZIcM{`b@Qf&%nBtx+@M9f{0h@&~dy)LrQz+p%Rwa91 zI~sgxIq0nHiWo{YFP;_?Z9DR-rXk2rukEX37UKJ`%52I4>8Q#qQ;D;Z1@K};lqEDS z2x%V&Sa^Dpnym$l1y-YtUFAhLp)~QTx*gVfkbM0-_`S8N2QWdg6+&025$g^&j#&Bb zz?GSH$8ALys=$t@4A}d0|51C}a`xKKV%QOF96uq}FvM){?%WpeC-7zAvCN21+ETdG zetGKltyxxNP~eE?Eh>9t#!{j=5lDSJtxOzHr`s&FdK$Ofws^y##ZpAOtf{c3_aOe! z5ib)){>slR*b_-^h zM<3Kpl+gum3Q-DE?P83?yeCnh<%f9#^`mgH^p}=CBs1TS8}cZqd<*(AOo}=T>3nA& z-y+uMA`MZ=rTAdnKuYYz(<`X*Hb!)bjuzVEuT+0n3@K`nh?o8O{Hyd(P@_J6Gx>-( zTe(rZqa%BXkT10K^NE#|iWCUws7l!ud9)aufwABJ`mXobDPwSVEih!n*4|`dXP&k2 zMiiny5?aSy5acIuikNE0bFL<*p}7;Fd9N@*Tm{bgeA8s>b^-m3e6O*j`lU{m?3_oP_+w;NOUov@IGS?UP(3 z4x;LQKT72DaeIS&ESr4753s!Ny3irR>;$QMSXtnGi*vDccH2$SDafZ-Q<#b(SQ)K2 zWO(i2YO7}Dl0O?;-bW;}K+z1}>IL`vl)VtbhWUepo4rV6;Ag)C?zU$CaapXR=;+9g zw&U5n5y+dbVE=99&N?3Yh+$Pam66;V4Lv|y;0#j%!hFViM=UO}G*9PeeI~enD#Sj= z+|~w4-;qs|60qr9D6#8MxY+>(FDo&UPs&3UC%%=p`cMT?dK}O`$XL6fHRBnUzhqJB zPt1tVWO?s#WWH`@h_5(j9y|mSrwrX3U}&srMqydc)+AUy-LuiU4a#R$KNz>AhR-Aw zW?=3Vh7i1a`g}Q<#vLKzN81jp=xr}j%;h`9qK18g>~ z2j47QFtO{Mg}>Zk?y__)_tR1rXC=Y4-PRj1Y{(5R>msJsScSH_F$xwt6?2uwP{(kf-4>wR-WcJ%gh}i>j*iq3-sk|PxJ%Hv@m=#G!@=nzNo4k8 zCscbvQN$O(&C}nA7jMf|NP-i7CILT-|CQXE$h`s%wv_H>en>o!)PTp;y1#9Rm@1gV z)f%egX!5XKRy`rKQTllIxd8sQU3@;&?iYIg9!9ga49)Det>rm|= z3kvbhlq*hWM6SNXrhxf|rXnJ@VuC0$lq4Z6Y%7(XT%pQ3pbz3QS!a{2T3pn5O8G`6@LMi%H zQKyyI@&jqgB9L^Pt(-Q9V2g~4a<}GZB_jH=EkOvB@JKh>i#ltgBV~dh$6Ow<)*Q|f!V6}N*6hRGg3i1}=(ZwO{Pg$-sn_zi zBd7{L4D#Ip(&RtTH8u>d|8-aKJ6Y|_V*%j=%NIVKqM1w<5yIr#RWZ;R{$=Pr${uS! ziV(PtlnEn(5PH{kZTOlPV+p?l{2^s5eD{D02q*}QqD>iN;f7_O+#znDytS1D4*23i zX#;Q55$ePwD|QP~*6u$EP49rNm>PJ*6z167$1|GxbD$ZE1-;V8iUfw{jRKG%7zI6r zUGl7}Iv1NGQ9~yhMM58FF*_nkdyhX#&2R2D@QelPF6j^iz^Sf?xCCOeYC6^jGmgj^(G~=pij#=?Y==#d zm7T%|-^0IhSQW6dXm9eCQ31BRTF=A+qzH-6M#0svoCi(#e*aJ9K3f@r99m`cT>VW5 zF)?U;tI+5t*g5*4_?vKMyEm{|zrqbmQmC+OFF1TL$H|#kK@RHwNG8FimncKCR!5y= z@b}aMKsG46S&vt@f}>IRqsQGAj}p5mOdq$D5M z%6v-us9Jy~nH2ka3NT)i(b%?u*rQKjCCnV% zV@v9y)uJ;93=`+%ZiR0>)rkoRdBIX7vzpeU`{JyAB6Q-o_!vR6`a)>bDN?ZJ;KU$| zV=Zl=m-ALlq3FwA4dDc|oLa+oO7|{)l2%Z_=z*cVf5VF9j}5u3xbChYB$plkf*-A~ zx>luT*KUVDiAu0k3Eq!Kgf807h-Z0a!>!dty(~DAEUCRfoz^-2@7+<&y1EcX9#Vcq zoG6c;l)#8{6!@Hl(dCIHvBOAL#zD)%0dFx8VE%QNa;&QrH6IQB9jKQxGbB9PPU{iRq(5@L}$o0mhZex0HWg7%}yk+|*;|=6^j^ z*tkvWvAS>VLeI@!>^EHqd~KkQwfb>kRO@sV>C;LlV}Td+~@;iFB!^ z=Ju9&6}-b3+PnTw*xLq)wWJp&A%gvENXa#mV_PgPVZ0iNi?#}9m7zvG`+85|dd@ev zH21iz5vi^{2paBBuIlIx;{$01*x*Xu^VHEU~o7+ zdL6^j|JX4dJ%g58;glU5KCEHUkVn~pEsm)sdoaNzoaOD={{SLq!&z1G=J3~pm7KXT z>nu~$P=gar+TPFWzz0bgWd~L`_WmP2!^NUFX-*$H5+hmz4tp0mavn=rspci^3V82hZ(9{2; z&Pb+V_Ka$CndA3&VBDTf`e=ayOUAZ|@68wUGntrPsh4&o{{R?KIu$T*G_KO+UjA#jkh125monFK$TR-GKN_q&^Ykf!C&2zHWLuWDcW^vLXQ^jyjQ)#t_Y1fi_CetYadVo>*@;i>803(3`csES8I^2dy=Aj)6FrQS%4MM-rs&I-xqY`Mp==gMPC_q)NsKeIB5t8pm(H=bD{A zvx%$XQb!(!IIldoht^+|Hn6dA>6%ZsGB}~Ks%^Zgd!?v>s=U_8*hTSoEz>#}oY*8c z5dNH@;_3?^K60xI&8)o_b<@;&Jy@YKxOYrw%3S_iRWOrz);TMl^kEw|9+P7I4*>f1 zuYmKNWe>h#=x5)cKl)VzA?+&hgl*<{N4Tw9{RGbSI*_a50%$KA_6j9g%Go=!6}Hj& z;{k%C1V#~a%AJQ3QmY_DUH@!jkAy6W2_2#SRdHvfE(QIYNg)fWeH?!oj!jZ(O#NQ0AY5z@F}b-K+t&DfvG4h#w;|XKiA2{z`2ibYEVRlJv9kag|$1?m=8k}9i*>Nu^A2)_0L{nDR^B`?fJ z-=~RvF(R93poVEW(__8}opF%O&k_WeCbi6t<_t7ar+sABpC@OlUem-;m>G@R8G*7d z;>vxr%HMmpPOX3*8a1#jV2mZhs>T@_we*rg*bmq!qC`u(>-V~lU5Sc8Rztq|lj3~ee1n6&;^Uv)f zpw7~|MAesNI8Hli8g*z@?nPy#JS`CZOhhV6|Z%S>)AcSwh4l+kQN4extb zJ_(LJ%4N)ijX@j#_b=#JCj+E>&IdE!F`%I3Fd-h+v|oml)Ch z^1?WH^#w|w6Vu5!Seez`So6|AsWxpu4@}^7o_`lS+MW~?pa;G76j*;cnYem;^~}2b z*P2q9tW!2caB=LnWz>OMNNp?z&$|5MmhF!c7ucFx}#1O7&@34oqBI8f3d8Qald&b>I6b=>x75Fy` z^RZ$=@U{*jn{V45rHY0TivFqNBhMrPEX&0D9>`H^ylH^dx4blm((O>J6=u#ee`Hgm z>U19iQr>!+g&^K}Vh_1Srxc;ynge%5z2Zc8xvj4;$Y9wdxmRAV|;r1 z-f98|M^1QJ?tIV$X&=4)2axXp@!Y|i2rRE!J?2uHfK?{Bj(_kNgN3F<5Jy|*GnC+d zm4gLe$9kL&H@h$*M!H4ylnx{j{T_7V$SuE z6p=6IGimh-qa%x4dS1lr(Y6xvR}Yc*ppURwA|cuJWI`eV$2z&OH0jHShME$NtkDTh zWtn^>5UJ1Ke;MNwUj%$R0h2rVZyxTsWQ_TTMf;yC)7BC<+KuumGXys&`(=o#~BcG|0HQyz5h{8SOl|uVZ(A`Ni^L z@Ch7SdXm6`mCB89A8rA9?`^j1E;5!J$1HSpejktP513C-$ImERj-lD8OA`wS@C0<0 zZNvH!*@M^y*yshieAx&%wX}W`$PSm4Dz5syCMd!UQD113{U)&4JUGSWw`%5O$i)G4 z&VQI-nVZ&(PNlU(UC+mHes{?e-dF;JEWDm*UUNER$Rz2+-hZaA@UF+zS(wpG;{*F9 z=14G;zAy)D`|?iX&R(e$an$r8^&7{PzSzQaPc4s!rgL)5vyM(e4k)WLx6bW~syFe>h-pVMzJAh6SN6mVFx#-Z&rB}Ppmwi92LbaAG z3%$azPnb;k>~jv1T&r5WE%iG2A3)%UdZpOwx$orK-HAal43#PeB(a-aQbo?^?hZTK zYgg%PjGxo}d7@k$t_qrr>8GXg9E8W#feUB!C!E?Kr*W?jdJ^*P@lwK%d9X?ZCojtg z!LA*NvVyeYbZlP!kgQiN2nN(FE!3(HjM!#x%(|}jYq}${z!2DjtWinN-pfihV1dt} zNxz2UM^BPZ|g;RVMSthjN&>jv!CENmRqgeBp59;bHRkg9uTSQyKa ztA#dRLaR-Me)=M(pU=JNxh&rZF)(-i1N_{iaphaR!iNzZzp3fu9&gFA&@sQ3`pvno z);WU&m8TJCZ@`duo{x=ne2<(OIiJ`nOTlO=*QA0pJ13s_BX!A= z?!YrzIy;t(z`^z0DB69e-4Q*x<(it#U#7OPzrp37)$0rg%Zo93Y;@MiTWrLg7VuNH z9~i=^y?X9nxk5ero2#pKL?;i``&ls>=fB_lCYcRv-XhE*5&t_?pg}-S(%q-Q0KiSm zjqQH4^g~Dv;QRaLKW0(unU#+6{HJ@of2hu{BmdO-RQ%l}{gd=W`Dtj^-ezgLFrl3g zuT1A$uW|FeKY94LPTc9e=1wNlpLfXUQZ!#N(z&rb;7`}z zyxr(=rKF~yak0}zDY;iR9aO(Lz*c%u=(Ae_ z5^igvEtibz-B$O2nbSWRUpV%og_K0p&e~AmgrnBB#kMT_Z z%gCef3=u9ESG-VK)uNVzRrqMhnZaeS)~{N0TDksxPrFWto*2kjD!Ts1cEPOz??IsEuwtyy#kNu)VOxZA%`e)JK? zST^pz!4!(M@?VtMr3bG%u;zaB(Q)E(n z9xp30|BGAB6gRx0rk$B_9YvW-`T6em3(r%{&o){f_py7W7-s*=Wd0L4AqrDy+k;Lr ze=|J}V)j*QdtP~)u}P>8D2Vr-TwBjy{$Vfx;+)#mgHQHn4z6;(<-!pP!2{JRQg*7< zYi+Hhp!pS23d&k-F+5Q4pjR_tx2A6j$|F&u*^90+b{A2Y~}M&sWKfv2kTv zkt5!wXzr5GYJ=z0ArN-T#lTA90VPWX*e0Wld%xv;ooal8WF{w}@j3vOHXZRa8J*|U z8T&ypaa`sVIY`rDciI4C?#KG8R&1UllG>K@2>CqGlJq@`!IbHl2mOq(Jvb}JB9mB7 z<)b`x9XxI@1LlJ+#7)3Hl}QsaWKpBjwlS@8>Ia@l?H{=-oXdb8XYpQf&EWCCPXo1D zzMKLT)yc-#)uDx)8CO&NhB?^%Y4mc9UZ4?L++)0qjM`^l8(inlhcp#f53!NK0Bn4H znsIOMHhUwX7_`2B6w-CIDnBdQ+UT|ct}Po^){Rl#OeLMJmXsgvcdF2#11p;(0Zu_j#U??2mb|<(DWgbzn*kpmy^!EUWCe_@l471Itft1r;-Adz#FyrlYwa6W zYI8o0&Q$9^{|6u!ZT+hP7ngq9YOGe3w+(B6@# zq!%O{3I{XCx!_c~bo|vGZa~$4bxftg0V%KsjLk zX9u(=0U`D)hUzd+wXib1tKP?s4ObS2mKVn+ZM^695kgOj)L~?$j~qL8NY-I=De?bC!icUy zH@2xYW#icO7ZV|UFx^?>#aoSK)~t3)tqKulD4yQprpO#8jxCwd>9i1Mv&_zjhDIbUVboQ0bH-U%i{r-@u;Z6+! znfc1_w~yg2($cY!R+U9?Db6aSe1wq^`AN?wHOTy05+Tc~NG*q?72t*xd;;U1m57h9 zQFByIm<}hmAs9_h2}d#ea-k3l*6&Yj=bgh*x%9@etWpC^f_PzC#9W5llx43@m_oK< zqUqcMAy0iigAvXvN6N;eZ@kihe9+!Tqkl4DT3fb|o=Yt;!lvlc>h4W33P68#rK<3$ zWz-ZLmHZZxSH7+AUcZ{XfP2n2&;s`SW{zQe$&yzEZ&cm(?92+!Ner#ZN0^Z?AWT0( zB>>6Ua0!65m60JaA{vjXMgs(@IG@SkT!47!XtDLAjy;ow)}guHc$NGz%)u~Yi@knO zBg$^a4c1ci2$2luWB&z{;0l1vTB5)rB86~>1WQ$J>BR%~{N1X2MN0tqywWyZHTW*R z+Jy~X&77tRo;JzIb(Z!vA1OrbTM4@fW!WKCnR2YmIBQE`4T8Ok@+KT9e2Cg1|DC%Y ze8zQ6V^QzayxK&HXQ(sgz6&lZCjJBPs_Xeb6MwiNkFZL@pjDqCHd z=Ft=G1)1~~$E<8hv`K3HFms40RsL11!S&YJ+=O_^pH;XOQuuP$?bH1{+BA2pq=hj< z>ZWyFtA>pCy1cn-oz0(W;yOR9CH%ld)MCBTemneE?Q~%JMW}Y?cvYg^(#{ZKV2(21 zk~CDY$@7zBW)sa91ezis&%N!1V8iE!zsP-*P!1z~{cJV$FVDinWk%#V9sOtX9~YiD zwyqV9j(}1ar8nu)b4+MwJ2P3U$dGL^(n*CumJe1_s!p{IZKk3w?TLtMMc%DAJ2#Ii zmubn$Fiop@weG#VL*(bxctCO_pE3qZml?qn1yoW2-WQvs+;xCieEUknrJXjvqP>XA z^L)<|;42@Hn9)Wwqvzrplj{y@h9cW_GkR@~G>iIO(`@$}6XkxnSd#jtnkeXBa9bNa z_WALxon-p5{z=_vly=5$`}q_&j&-Bm0tvhYY0nuMe!p4O)e+~uN=Rb02+paBWUn_0 zxOK|jYUfJHY`7}&5Yra962v_2Kg?Lby@_m_>;08xzpvR~B=zT~y5tkDc>3bBc^XXPBvWLt9duo=4kC zT-@#a;-pl2wJ1G2w%)o)_F1G8OWf7aT`n2rw>sA}>8Z?ut+Y_z{T{yCeWNm=t2_hK zPq7sk#aqrWnCC8Jj)mJ{45ETbsFFv)oyi7#G7ysp%LuA@xUFYxLjN(V z_S4jJv(An)PnC8Kt3N4p;t?l#XaqikpxsIDEviOD?2J-?D!ftU`}HQYpZ3AA_|lV) zZi+Y*k0II38ALs!-m5z-Cut@c@twUoK$!b_o)lYnp$>din1=6Pa@*S;3C@PbMcpQJ zeKL`3h5OtJPgQW=sTvU{#H34cPJUAw0F~dZ`P?0!^-=fZMra+8tLt)CV&5JITFSR6 zC`Zc&=OcK|94~$&si?qsm837q)&0?0FUOxNBhR7j=h9D4j233R@9 zvA22l_}_|Yf5O^DN~BWKBZ~7Tp+!tAT*mQb{F@aXq6FVqW0^Uk>;chw?Jr?`{wgAZ zB{jyE{m_ifw+F3r`8dB0sH?7;n)@l^NF5dwUU+EFFL!|=iiwV3Nq3O7ysB+f`91Gn z{|x#x8@ej3HeQK~N;Q*XG})#huaMKcF|I_(Rvj#dirgvqNa1-X5_!I4I# zXk1k6ye9;O6C_*?|Aqta2bh_+6CT}O=3I|iETj4_`Y#jwwJtTG<&Fx$o4Lxzj$YF? zv?;*vK+7{di9+`bjNkrX&Na|0HP^eSa>nsigN5TqUv3-HQd%}*W-s7b4@-cmwW>|& zXX4cC=`mq2nE^TaLB1^PPU~)wNR0B0&)I53orYKCwG1);wMoYLGxQBCriJSMusD%W zZs!DO=nus2=fjo1@9h316^~gz`-}vBW))ic;!Dtu7XMYwrlIF9hf8YyPpXLj&b1}R zlZ{x&6o!cq{$^gtV*x6Kh=gAuW>P(r-y%vm&6acSUAc>0uq@c!`w}|capx=Z^C-du z^1jr*+~nMhXyY)mm0o}s;S=CLAOG`vz%m?}d^?P~bCDx#>;B<`zTk(#X5EDv>uG$u z8psg0ybXW7WrH_X#*Jyb0pmNqQIm~(9Kz@G7QXvPdtJ_s0yVEHg zD@wtEgqA-t{t#$;q8ocuCTpTTSaMXq-#ezpnGcrYfK~9|Z)-6Rt8bCY{{iPu?cAF& zb#R`+!%dcqTQ+O-EBNg@GV)$Uyew^V+`9opZ?=^CpN42_dtwz9ZzsT8Yot*sN7rpX z?FZv>Lm&nIli#1y!m?6V8-vuW97+Dz*IHWartQ{aVIm^-UtshtDN5#gtVCFqYU93J z>C)t-Qqn7JFXHU@GvD{nO3`G4D5iaxEHiKF;LF~C+h(376kEZ{-5I*= zx2mV5Hu+2U(8OOpTgYM2&;L{RDKw6|`GfGd7|UB%$-UcWW*u2xE#6Pbh+F&{iW-V6 zOz|XM-!F!~r8l)*i?0<2U(_dOrGI)j-`SdKpK!-oLQb?KNt^4~)-Y~Dv}Wd+F#a4Q z-gUaTw*?%g4OjaIAY%bSk|>DHtT%5aD+Y_MN;xYP2jFoChE$TGjyF9%YXUa9&R{c`mu{NRJd(&m;aSGNz8;Jahocu`IWA&If9Kcy;(d}@ujM7sS%CRY$ zq}`3>QH$~ByTeg`*s|^r;wW!)xe>D5db|P{(d76jULdn?{=zy)Bc~^B8Hsr{mELzK z&+l5pHZba=={FPnwU8Wb*?#tLkN*lS+Hoc`^_yxECu@p8C7zQtY^wNdFZtR2-X!dI zHtwYIIhn78H8IP^WrLVQ5C7AuRQT9~u)sUuZf>+z^&>q`dFeo3KAnEi@nn$WP0<_q z?XzXVk3>k9?=l6xP7-|;-EkWj%)Qxio?3B}@BX|pqI`gtIysCyM{n4qX4!x4>FV=i z(3^VwJ1mg7V3o~|T~{>`rv$LfQBqH~GM0_;;pis=7|V-e$qUSo!?s~0@Van;oo1^M z>Al=FS8}{Xz|g0&QU|W&pz5*O%eKmBF)2@8@E%K6NG%W>FKN&6rD*f1{ARte?o<}R z^Un7iC-4cfouY`3lq!j(v5zGb-sHayV(dzsVOt{s$qX}E1kj{0?2wH{JEcZ+)0+PC3#!udBvF4O)MLKr)&Q|P>*WO3ieC(U zE72*HYPs!m2Bw(7mH?FFN;Kb$ZT6XFX^O@#Y(&pF2^H+FN(>6~eC@O^v^dr(Etju3 zwrZ*CJOsmvfz&pM`$If z299`BCzxNGc`cRewe#ccxVmb+4vH?ji!(;sw?{;`>)*9q<8G?gUyr!k1Y0K?PhMN} z-C2INzp89zeF%?Dk^w)*ADc5><4v8b;w7WOZ{5#XN?PaM_*_*71sAaM*SsXID^9zr znO;BtE^A}xFTYzC2CEN%H<0QU=wb-6bR`)uLGV}G`Ja~OFK-{Q09&`Mf`W@s{sPhM zGU2ZZ!A;}$sPdx>n4Oi4KYNjvRabIF!=p{h!rtyV2l{jdXTQ}uwEl%!ZrLu}gS&ko zl!|T4A*7MDf96c?L)Tp{s)}Uj2P(*lcIh3}1`f?P2svD~eD^?nh3Y!It>pw{37MY?JZ9H?MwxKx=jQxiC4{iOw-YfqUj%72~|JV3mtTF39 z{{bW(|JAJ}{1K!I{|NYe?M^rQ69OrrWN_xew?P+T*jd0ma<9x{!_w%trA=ekeW81$ z4TxWcQVVli3qSM7xkNSpI{VDOOym0B*B3g2VCCdE+I#t2k9-$8fP()@JYHQ^DRcf^ z$N1pSG7xnv;2ZJ};AU|2+x2Ngt~ucGWDK2Y$$g#w7{X^D8aGY-fd$D{mxjOQe!M!X zpDxJhn|Q3jzn9+i7?RLP*T8WV84VkaOgIXNB*J>A`c=Z63(0b~r)$B)5`N+%$b^g% z5uNaw{JDawf$7>NmUD~zFyq91ofq4&;vf&TtZUwqXsaW${;zBCHNw#3h_9S{R<~S^ zuogyf(ypNHD!YsNYZ5JTy2Y6{jS+jBItob)lWi^E7x=6jy6#9c7l@Ke6D!zUk~R`= z7$9N5!PCK0TkQ`E?ls&s`K4YFz$N2(?iPKwe5}xOsa_+*o0(}PeR{5%ulsbZ|2@RVE2Cst}-85;Leqz~uhXMlJy(iTspMqeu#GVsud6B=A(r^5aCKFr_U zh+cbe?AjsyFzMr+>GD>c?Xru#ulbi_q2rT0qlk};!*MwHGLDZ?QHy-GFUGV;26U5l z$atQuh-Xr`ztzvDDUnn0(V$qkmQKwIZ+I;3o?*7<(n#IDm*^XpT)km^8X=@Q{&t3f zt(m%XjJJ(-fda!&EH}9T$nM>n3A$|4*7>4<(FRC}ttCf|P`^d!mSxO2FPOGDL}W`c zEfA7>u3-y+XP@5lYAdDE3l5`FP{UexB^L+95mESibAln&Je!=x3-4R})t8^|{zA5c z(!+lbDl!G1gY$2v#jW*T^;mZ+H>H(~ot|NQQ{_BJd>Vcukk}65P0kgt2tv0V5q!&U zNN&y2Rw{SD#@*4;=|{PA@0vs4I`s@Yg;p9y3D%)+06e&4SLPj|f`TlS+F0P+>O+6sQaQOKXlybP&$lQP8b=JN2a* z_f3daOoZq6`XXr?rImUgFDmpE1+1=x<~Lec=i*ggF10;rx#7d?GnAzd2btT;a%TO4g=MmpcC#Y z@VsOzO(ArJtgFkXd??>&zML%kQ7^PnuPx|44oVI9Vm4MK*7^^iV~tj5nue1-B}Y$1 z@q!_e;GF@qP&w{tee%a%<%L-kaiq3nnSvLe7=yO%iJPK}vpsjy%Y=LbnrR8HC`#Ww z6PAs4qNt&g8nrX>ez#VV({rz53C_UigWU6%poK0ZQ=1bxbx{S;T_3UefFzFJNJ>2g6_pq*F0ZmqI>2r5sWiI?MstUQaJ z!9|3N&t$XG>#ntJH{uB%cf4+HH2Pq>TbcCJd0q?S)w|Bz@9c#vRNUFm(Q%#!2`Vx} zQi8Hf3m;>x8a?fvA3FQz&Mx={13k+s3LCWzHXJK0Mnm|!ci54BB=;}mpa!FOP05Nv zHPaDU_}v)7ho%h2vza!mXC1N6rYJn3?fP@iCVy+`0_)RQ8l+a^ z0~Z@BtpSzre~lu%?4j!yn`r)_TRkC9La|bW2!gd9A^^NV#m+hT@PaNI%Iy z)6dS!I86id4YIqvtNzhW6#nAxnPt05h(@N%QPGhe5?%@q2Y%&{t69NF4AkxmQBf8* z!l2f`&rfCV8id#C@35f~$TF*mNAW1+r*vY?iP8AJ&fsACP$;lQiFF)hs|_BeAcLRj zcb#!+ITfoN#jSqi@`{()z+za9D4;>R1L+3Tz}>|}R1ph0k}(&tLTj)y)Qu8|$CC2g zl*Pk%A3S|TnF~&1ydsZBKzyS-2Z{(!N&kG|MY>gS*DerQJzum6UmdokZLL&|tZq{i zUMzkFd$>carLth?&2$;d`-Q(imr@qfa9(A-+FZ!9@mykszE8=g;%_wDKY-0thlm9G zn#S83KJa4RPQiZwlN*>AE_93Yo_lp#sRo)yUTD^;(tU4B%zI|Ag@k3YN&0TfiivAaghCUC( z@vZwRQN@UAe9M;#%YOiHl!MWo{k&^uiiT&yq0rrX7^Stm#kI3Ht#>q)cqmDkjwi%= z@=YJ)&B+K`*(5tobr-S(Fm<)nM|QM|3(t!BnJ9w&FWt5F%{#j2BF8T^B6+mE9=U2k zd*ymmTtPCB)B?mv6r~#%ZD&33)QYz@opH7`SBAcEQ71pBn|;C_(u6GY?wI$X)UBrj z4u>eC9B66mSYDCZ8p2Ac*vpdwPt1f&9*(k+Lyg$5lV(&#U?cFBF+=T4oEEEJm8Em8 zA6z`^M~#pB3ph7VUTfirfoRLCt%X=eJE@jj(!TjBw3_S+{?YA+iCMGz)(^WoXWMb! zTjI^r=Vlz>MY&#br0;oNUzgYr7iUDXr1%?$lcqrJT+YA3dUDU6g5bih6iqm2AMN0| z|Lj%H50?x}<-bb|RbhAldOj=3G}>>0*x-Q;vNjs2lY$N&wK|+G+&Q!QBz6BsympnbGs%?Y4-doO#$2@3kipNb zkJueT=yD#*HMHaHN3_%E(HycXj>|)|@+yR66|ionQ{4@2M*|>rRB#eIX!0Tlz+nWyg_d z<`uvOsOdEHK5*S2gFyk^=WOM>Ey&u z#fxn^<5`Uae~+W^m%kR>pRuGGnHBO`a}=<4@TFw8X&wRvY!4)3CyjXBdIfY`_f)!h z>Fbt?hMQL|!apHr-gfFH*oFY1?)}1?S#{A&erxts8Om}@W^)~*K1#%n!)!lACXOzy zkuO|tXMXD;#(G<#p7uTgy^C;Zf%-cVa%-j?6&uKU!s5Sk4-1?w32O27qt@es)<*An zPDF@3e|XlnF0jBCq~gM{oJo$mK9-zwhVEwc0CHPFy53#e7^%jVXJpHL&ZiUO2W$4A z#*!TsR8TZ5#U7g;ABx-u@eTx5zNBjplzDMy4f9o`*TLQI^sGaBjH}Uak0bs-L*O;& zu~zomk0*bY;)apwsb-ei-!n}=h5m9lunBzAHO8J>Jt2E`>;62bsB%rV&qZrpft|K2 zQmp4j%dY8$275MBPkPcu9cVFIY$Kd~My};Mjdk_X4+B#TGuq<%=yrf@4zYJmJ2$zl zOw=U4g!2b^u>g}vPX}9%{EF|sW)EDg@L}$?i>w8XU&eM_f}0bGktJhP9q^7i*2ghU zs-q}39T8pSd(68wrM*$n{qOH$Y-x2gc6Sq)g?nbM{Y21EdX&=struLtiXVm#-gt_fe6%gN)5oX^6lK32B~+wEyGDf+(% z?svg~X3)~SuXkXT9LkOD68?fqrGD7p%u!)nS~yxHuOQAM|7xR&1%JY9yqT0`h-tg7 zxJIo@u<2)49S`eI;9-pmc4WN`%Fd|sKAC|gyc`{`0u9z~)bNhNlN3V9BFY9|RWS_J zi}qJ6k~Xego77}KNK`m2+9<8Ora~kHByS}`c5t!sekK#kDl$sQnM?IdWOFLU$-*a4 zXTC6`R)uj$b79az2Q(iIu>tDZYvk;~y$H+wro{Kb`gcvU87s$KHFTLEZ{jTDQqaY% zoBbYh&&|2bMLIJ?X4E(7mR@NAG57^;Wyy7KPo5KY!r#XD|^}+E=ur z)5*?^-UF0qqwvSIh_R0Th-+@2ENy;erSm-qKCO}f3e546h%`5!SjzhH7E;&q^5S&A zJ0X%Hn!vM9RXy<#*lYq|Y^*WDa+|Y5@A{H+k{=bDZ0}A3gn(N`P1XrrVP-Ez#w?GM zYH#e&*l}iv@$b2>Xb>&nk5$APP)n_64Xe`9S+!!&6Rxl-JxMuQctW@N0wFlu+Bgmy zG!YsqY5e8*J!-VyIT~IVQgLbdi#VYoUBugUj>0-ed;ceQ$ve8;Y79G_JO#G%=Df5o z=?9~0Sopf?mz%p2r=;$l@vLjDLM>mAR{jBSl^K4Xx&se^pclWGi^&#DK{n8Eqb^JCz&Nbl0tNh0(r}3zi z)BUJ#>PWQ*J6hsb4gKyP*^ZnsTUU!NRG217Y_sDw!opB={*l`ykFSIW8Vd-630S_0 z9`z$t0VSiZ$VCPJ#)r7c)aj~cK{WUFX*h1}J>h-bAokfd-TV4jcp$jobVQp$hPLsu zK_A2D-}xi~TdP;^Vl$5rG8)MA6B3| z!H)2go80a-O2+TEi>f?)W6*g7c--T64G+$zqZV_*kY!om;X2#$!-Rcn;exP8z#{8y zl&-tOs??PaEcXR@0Ppw^QC5|1qlROwG-Tzu7ylP3XwS95GT5v&c>R(9yG?(j#e0n7 zz9Y4<5(8jQKu@%aPr<*}DSd`zOjj8?Z7DRaoAl|JRoXC@>rRx)H;#+|0jydn28lQG2R_I7wZ z9J83y_ZjOs8Nqc=+KhI#M4FiGK~>ZEWbc;R4BK4IMM+4T#Vo({G%6gF9;Iex`U~4j za8EFxUR0_MPjS@UlcBpGSa&6FySDk5`r+Z&cq7LVxY$Bpto5dOSBhB>%$C`XQ(M5v zuU#`TZFzDr@8*rMW}0=YRD_$O%3sU@U5!z7ots~MK{6C8M`go8_ogy*d(QyG4E#!7 z;jh{6$0fDH#5&M-1^OD!w7EXlW)%x}X}@ws@K7!IgM8nA{s#aV&b17Z|u+=!`S#yC%Vk`^2er%{UO-(y(!#Dwc!CV|l$PznH-PwH__Vb^<*n2rfMjM3x2C zKImTAWLK8kG2&>A4b8+UIvDFJngp@zDF8pkZ-Rn;{bdC#4XKh>-xwr*T;6!T{Yugo z20o9j@G}Yg7XJ#VlePn8=|@gd7{{X%p017{+lo+7Ql+o1Zs*E+Pr*&| zv$t0pCaa{n_N9P@G6pi~3rg_>x}#Rf0Es=L(zJLgGoDKjw% z!q#^h%(KXRZ^k!$uMv z)3p8(8~me&0kK7dj&FBH?gHQ7n%8eedNJKGcxfCAEw23g{J}9KGW&A>qFd5JAoE0oaBcou_B0_ zg}g$f;)NG$R};geUj%AhxAS%hkR9K$_GpTfZOnc~=Bq2y_Knq+kp&L1J3YGQg=Td3 zT~+1lZc`&B3F(yuQ<@v-`Edk0(JW=u*UC!dYY9-UE%GvR*UVrIpuXw5|Rm zDN`mlotmC*QUU05)>-M*AHke{CU9)H$R=n8(hb4zce>5ep8rS{^k_g$l#^lGB0Xxv zd;($fDiW}J`XL%4vKE20CNvaGO7yv88ocAkGycT$@vZH4`w1t_8?;CqXFTqArzY~9 z-&XQL5R*GRJsP$3i;z`vJ+^q^J>n;8V1Fb-&Yg$8^lKFvI&Ny31mOKX@>c^`Ki;@9 zEmfRly%cux)+y4XrNq3L<3snd^`YOx!WL3B?H&_{u^2NwIX=Vws{b<5MRhwzzJ`{-p=0dQYa79{4`fbTPWk zH#h1@W2fPwY!m)#vz&@x^xf-e%;x$`CJ>_i9h04vr+uer*>Bx^ zRtZfOe;0QITiA0%t}Aooaqul%`%^Q$E(UM$N5-%y6$9}g;OiQMMY$70OlP*w8#lOdKkyg6tmzLkuB*Lt;19OOhGS4jn z2$qkw94l`G*={0z+unTbRM;pAYMHDamB06dlxZ}VT>T+0VLpf@W|dHVb`%>yFrpx zur+x{uss0$laUt8>RS|@G`C~?ZX{aWsfTGTtqX3NDiM4mah%Q?_|DHCQhnyoX?dx~ zPgS^_a-L#h*c0P;mdNdkA%?OY;i@li{{RlnH0EzwpEpKS!u@uDwraRha&Ah6dgEGr zD`iVn#KRP?=3pdq{mmmQ7)a%L5-Qm^~m#YP@HH4A_*5R^R>X>Gk$32o9kgYl!0A2%xXD zn&dQ*3hp8R7jEe-_YpC*WX0rA_Z!iVr3J`@@B_Lj_@L=lNPYM%$NYvXTMqqR?2{#4 z&TExM8Bs)^jM%k&zJCBJ5SrkKTVwksa!Qt;bA_uYk}VC~vo4bD5zEiO3AUkibNb`f z1}Y!ka96t}WR_L+FH=~Gcc<;JQ3T4p)MgCwPWzc2)D|APs`vX3S*JWI&|ZWJF80nn z9{C4wpda?+@&Osgm({wHLUya8|22Z%|A*Z`Syl9y^5Wm_+u!Rz_K*Gnr1>`e33|LG zg?lFRoGpSmfGpB}`ES|u{M*OLUeFXh#zWq<{e3BI*w(&&K}G(DP>}UZuROt@p1;!G z(Bsx&v&ji9l7@}4I~?dYJOU!U`3c)aBWfK|@P`)d<*s0Z?OOn?jO&t33Uv>FXbi(D z4Zw3;!z#2W;RurKRn@L?AYGhAfvm^vThtp9Ds#Lvtf~(;`KzfEdE+~r+UH@c^jxfV zic7;&*Q8b`N&zTynS;+gwr{A|+h|eky3&Xi7&=5;wQ-OrF{~Yp0(cKZ-ZCXCzQAeT zl%&5J!R`kguns&U)u+^uQP)lEDql_&`1uNgA46YEF(-zNg(KO1Q}9Z zd@f?JA$0{8Y!i0tL*b&_ZS;*Z)w;wd4r)N#t4B35uC#4mL7?6O;!hAA^ISjlSd)t2c^F;tKVJ8$z>l4V~K6sq2djSc5eJ$2rYY3 z`Fp!|a8jlPcY-e>0A6^)zM-z)nc3UVL8arj6}K&83LjiEVLs9vN;M`hdgkt1+&-e6 z$0q`ILn7R>u5UdDl?l|6rv_MZ)(1Z`&tH>SfR^4E+RQQs?+jB!1PEdh0r(y(L zLEEsoX(kR?As8=lF1}v-zz*eKpjti$%pJSt$lCQaJUsyb$AbpqGqdNKCY^3Bs4Ru8p&W>`=Eko~ytSiilonfZ^$)U3 zWp_0*Xyv}YY}*+IpKMh+Nx{^cw@6$%O!S;K(0{^q(c#;GDxV+rd%N1?J9kj?SW{(n z#bz7@>>}q)d}THOzrdjl*p`s(x}0~2khCK@?(#v82Hf0&*h&@o(e$uK!1~3alhY!L+8-G!|tU3GxD1VBsz=w$N z`P4@eR7-GX4fuS;E2-mJoac4Wd;lNvt+KkGlWM){?}71Mi#y9w*>nw5x1W_7iRZM3 z-^=xxh(e6ceNJyYAG-^-51Tvw{-jiovj&@Q*S|-|DwCNgG0gaTBN*$BLCaf$DMkV^ zG9+o{^)G|XMSZ0MEfRxSR&)Tyb7|eUhZ!zVR&!iPdb^3W#pSsfogM`>L$}-wCcqpvh)>8!j%{kRl64flbUnL#gZMA_u; z=Av&<@iFZCgxHo-WWR;Gh#X5j%h^HTI=fB+x*JRkb>6)yO0j1j|1kCKadYNNoV>ET z^$7rXYx1hsxd%EQub-5D5^H-qM6{JH?VDt*3=+|-8En$oFO;G|pr5A`K^Zxj=fx58 zIB-!oGPQHz_Ilo&>);Sc2DsD+)6(<)zT-R`dqYU<9UNo=Mu>SK>Y=%;f8f}^< zv7EYzHknlEwz+SFv9dTcAoUC7K44I0{07f0xMhQk7)tVFYjOg4c1}gmA<^}&6PcMN z-ZjR8p2!!<9%}HBpl+EZJct{!94nc2_-oPMd*fZZAPpH}IWb5i2h;HPPkmDz3514t zb#slKbsbSx3Dq8=mPPk-G@eOM%beu`C<%n!^W({DVNVI0i$fD~q%ieWSEJX^whss@ zm%dEsHy@(Ps|zV|#fT3`^3(H2llEX#4$31eTaiVVqpxNxOw&|@8S^{F3DL`u+3b{r zTq6Pv4z@gFuPNCD?y+Dmy9;jE%sU4%n76>)#Eq!h6)>O3Rl9E!`E?#FrWc=KHWxmW z5}iV;A?y6LCo3aSgs^<($dMl|Qh8liYj5k-LINp0G=s?<7xa0I4)LYRjDC>q$n*x8 zwJ->jJTl|no{9W84it%Sr|Z`Tzy}Pr`1d9pn)$smo_u{jPi{dl2M*Q%>wnK*1zEZ} z$wgEE&(f@69&F{-?ABE7{4ktUtnJ|E*O8Gc9D82PoJFzd+86MyG^8dTW6PwPWtA!| z(%^Cx-@g}r^x>R5Ra^O-7kbpAivjy~H~rnXX)9B~>AedW7>8qt(TXaFF_h`5Cs3~Z z!*;dzw<@bw z5>^DVwV>XgOw>HB>(Qd%yLfOvz`ssR!$$g;x@OjC^!K0Je_s&1_88T9c78iNncp>+ zydzHc4}c#|{X>-;3Y-7ms@wnFRU42VmEWEJ0Ak2@_r(7#pYhh)d}#U8Zugh}?>O1$ z{fvJAQtej2wj#OZzukz=$41Pr1Ri$&2Z_J`zx(<9|7Co3J?Fn;Nxzy-*im-R1y%8) z=k9+)iXO*iGbivpV>5TOTM7}pobP0Y;4W>?Dh^$VeQg;0${nwne$Qw1k@!FLk+U?v zqZTFrj_uh$-bJQ4GWB~=UZly27fUOA9XK*4M~q`AFs)RN7?~r-j9!q%blItq0L<5i zFFHF- z9Q;yagldD__%LVr3O=1_8$K>nP35<3Y{yH!&j#Mn#9q{9k6^y5tkqhqE5+de*4>$| z>Bxq6(UI|c)}CT@3Hqh(G3|*n@mwNeqyQ|V5mW?Q7Yep5jq-0SayU2x3V*r<#Nl@OD7+J0j-x-Bxm@zCc zl*e$8d?&G<`5*Y(Bh8cv%5OY@6f)XjqJ*N)95r?9yQU{f#6&>m_`7zkYJ)m1+O(u6 zg&={MKwCQF%V`}ou_Y-+JbC@=JeIW(B!|TT?@9`o46#wrk3S`q z$P~;|7MN55u5q$+)7^oQ((nN0W{>im$4ZC@ZnlA&KnkA5Yw)e$mr$27`bYFyjDnbg zHmqqQU_`PEw(P~i%3lTC$`V)@-9hN^!gXz7TKrNso=z}}ut{J65~-7<^2z#g9Tx>_ z`Z285??5eRr|i(Wu854ORFrh4*MO2`gDN~d#gi<9GWw$hbaH8_fC-n(Fly0(T#jgH zZ1y=Mt`?_%I^u13E1X%pvKaP^4}y)&V>!8&ebUDY>YK3KM4lcohbJeuKAahmO5;}F z@-!@bxhYmq1=o0UtGn*4q-0R-Wxm^h#5fB`EN*};ri^;s zHIP+664{NgU4(JhCtLrNbg3fM)4c1=fGY7>Hs4`3Y}e^vMy7&$ZJXOOEB6$AZ6~dw zZ;T6mu;~}#?TIkkfX^I*6S2H{hBEHqr=-a4%sNfy~Ut<7!xHpZ@QMvK_}W9arLz<8CMRpirH4A$IfWaRp;KdL8sNsa-6g#(9)o_sI#H)ti#Z z4rwiJ-5VJ^tpFB}^nin4vq>Q^#*8WlDj?++R22&EGb(PEAJFik9M7DFRCFZ-E|o~< zncjibQ<5Y!1*aL!|oBAo`lqZu*{qU1L2P{ujs)#51HDT54 zS`hCRO?$u=F-m&r?Zz;v#4kWc_R|^-m3Zd@L(C&X-__Z!5!(Nx+7u<33F#RIDX(Fc zBq+o36rC8st}b*SepNGy)B<3on+pO0NF3h*NpNvh05KHY&|rf{&$E^QP<)&Ts+7(T z#e{U1D5ci}NeO`%kZs(hS6LoDgCCe%c_ABh7bw(A`4)-;sJN~{w6AfqR=U_m zl^_vaR13TzXX2st`rbW5pLLmh$Lz^BrAP&@Q+<$oce15`_hVDp9cd2LCKdcqUng&J z=#n3b+x#Thkp_N^`GM(12;%Fqvw5LS-p{&0MJ>l)iUDfx8%ay&f=+~?8*uuq$qD2l zv)w?7M)6he2pK%kkKu3=*^HN0#f3Toza_N)*?t*5BhEhj@G1H``tt1SU{8j6&hXwr z)WW#H-!sN^WyN-`{7p?Ho@_V&GT`h^mQh!`gunna@;l zuO*9#8gSZ9X^3Bt6fJbo7~;AuE$jsDu>7ow`e<=3o>l9L0}k0Jo42L~WhHl)ISDr& zpw$Hy=p=8TC5`QCmU#-Uxlry;-s za;O8f91LZh$e09God3I|11z66ByWBnbrvp^EV;=^HRZJP>Pk?4U(1(z+|JHj8*s~b z|NE?BU|27LTP2C$wcyd%v|7qwAqhqdAB+EvtM~`dSaHF#T5@E~nQ(85|K^{Nn`85= zAGW`gBizVkR9gRv1O8?G`}$w1Ki*OpOxSSiXlh^S&h9^e{RXe!|4Rg|p{ItIow-!~ zW8zqUNZ;x8Ez0la&*KK3Uxv|?n)y$S$vG6Q5B^1&Xk=*n|C3Hm)zl3{Z4$Na z6?qeHClv5JaV47nPIkBd(%H&cP6^xW#3PPZy~I7H&*&9*mL4_S zCLd<&x|>isgXlwXVlFwCy4nx5uEqHl{|vV7rMho-SuiU5m{`RXng#$|95Un43n(R3Lf4argbC z_!kOr0Ez8gs0*?r3V2S6WlGwv)#a+*l^NG zrm%9D-g;+WDIu4%MF6@}JuWqg5FxcaM-iIu%kvzwmJDU)o3Jgf6reUg#y z*q7{n9~ZLfyLiv%fgVZlB`tubn;+Jzr&~3Naau%F<gq8}}~Wy4qPs>8`7H=}Q?TJ-3(R*4cBze7IY^OMB8SLe0;; zc}`@=LN`OeNG}bT5`!s0F2tEXy6#4*Yzbtc*AiCS_`cHaIZ?2+YLD!B)xoGibm(HD z7t0&bEr^%QOs>^2U>h$L3Gh%su@@IL29a%*4RobKoxz_t%{~>890U9EyRSK{? zMkdJ`Cu(t2fZ)ot>4?@W3U^554Md~5rQUNG6X(kp#iHy*xmC1e&icW9BN78{Z6P9g zUuzO4fqQt^Q=5Mcwtq{1pklmAG`uRF3k|Hr#7pa?o9K6=FFu`DdS`2oH%Rs>l;iaf zqFmJ$hs=?|#$16AQ-hRYW@2Rm6^ps5C|)|NG4#MF!Q;!7im_@D0k|6;Qyefr$|BcS znyo7oBtR^j)o3x8Lz4>UOuK0n0WWh*AT--c>`lO(t0sgYz*U<^zSS@93#v50J|yPamSD1blUD5K+oSdylMQ{>^proyBik5 zK5?aSOyn(Wu6kp1I()UhRn0A%VROL(YF|Sg=lFUfmHtmPS!?}`PMm#*Gtk6U65+EI z(&U)zW|2=hsBJ~yg1BRj6${VMeHDSDhI=Gqf_|=Q#WU@mv$x|84Rq^x?-UMuGOn&p;@Vvhq(5L5o1k-$e4nWKTBqPrgK$ z4AyY-Aju?e7iZUV$^9#IAXCCjKF+Qf`lR-uqO05h)`a>(1eBBM=fi4E^|Tuj%F-Yr zX-X&(-F{4kdYCND=eBSVqnER<6R(1+z}4zHcOf3Dnmd2!aI0LmdivRjM3$FwsLZ!v zt!^+b^uC2?%RVHCCcmm}X_uM#J}a~<4)QYkPSnpu7EE7wQEsM>ksF=`8++Jq z0lvMDI~|EeIjF$OJ-@ZhYpO3}O7Ha?NJCmQ6+gCQKNFq#a&NF>e^;V+K*0F#f81Sg zEpr_T zFR~a@Rxbnf&dHq$tL#ocZ|%Tt`-{ zJcSn7wBNdqSEHufNDVhEy5D%J)s{5}BHJ{3V{2)6L%k>sSOccv=+y=yIvr8QuHz;& zZ33`&&Z@u2MRxBru2_*h`WuNc*i`BJNmP$)C<4tjZiJI30{sP99r?!)&%CF7R5+_> zB~4@wbuWb`Phh90Wx#lQcik?~&5SnsV+wpY^qm|$XqQ+h=X%AaUr;D1=4VwmJJfSZ zb}4%u_`ZmX)j$$p$ z&ztC5&R@X-s4^dYGzQ1l^bxRthZboKM*Sv2g0s}r7A|NJV1XA!jFU&Od21GB#(B6l|s+1P@JP->B;pi@9EC;StYPR88>_$Bkxy0McF8r0#W# zNrgixaN1HiJ_tZ7Lw4;erCU<|Y$R}s`YiSFaIZSSgKkQsUCN)XiCrd!>*v0{+XIu= zh)ym}5$`}tcHS^hfI*Ay8YbSRSj7MKBtC2Uy9C%eu7bHF(`#29YVFzd|!V*L&0=VyTvvX_Q{ zlRD7lG7T<)$k0Bmo(r`9MO!(mYiaFKYGGk$j-GCoWY&rK;@4=jeS)(|aT+3N67IFv zsfZ)W!UEO;BmyLwdA@gU+mUuAyB(tzCZl+@*!~ouU;c<#>U+ViPXa7puMV@g(k2syZIGj6jUVY4!WbtDB$F&dQd?OfyT77q^cBSD+hET6`1m~?_ga|uxZj}!<{@9o%b%^$jgeft6Jq* z3;;E=dqtLY0bo z>>Pj4xi|)8fp$iO9!Nw?>Da8V5~-71604mm_~JC)wlkrq2i#b{UQJuSn;X#kY814* z`&HCZTM)Ve-xT)%CY_W@e@eJD2FjPdJ0o%yP+-jtLi zqI&{CwD!uq{sAxs{P1;6t(i%eDg`5HE)E*7W*`N}wGd&DN$r?NkUgia6#e0{5$~zu zO%G+{+0$ciZvtD?!dJal?P zsrd{Pn8-et(X5jHzCpv@+DTDrier*{Z4=S?qc?2ECo8GF_nWUSn7d@9&s^cf7s@W*a z9Lp2gr&7Z0J0dLyjeddHTf$%H{YxarNJ>z3@ zI_ib>@IZ19yo{;0hRw`J*1tW>Tiz!5mM3;SLv;|rn2zQvZ4O_WaEI0P@Se;ea9*JclU3|oQ>?~*Vu!aKVMERSpI?* z7V*xjyA|Vg$7JlvKL6Ff1;qNdk2C14*wa0hrgN8@Q;Swm&o?OZTH~qbwhL~5xf^|q zFic)e;h#x?qTf}gxf8g5%^8=!4LD0&rs3rzmYy*83pk$wuP2gUi4oLQ4lnMWl|884U`a2C}lL zdRPTzbigq8 zgMBw*gt4-!2v{i%PHry=>9qiotl ztpJg#IZKnR8B$#djQxpLzyZ{i;`ePV1b2Jd21tzq0qn|10IYEELlt-5_CU4gn|H$H zNqNh(F#^O7YL%}4ujbh6plahr@%2_;s5Fr#!sFb?=zYJdgc|Mb`3mqZ=%@6zBHbRK;X58d%~Lz8q@?oe}*?LEBW0R%82UY4#}5@0;{!g$}Q0JI~M@_?Jk&llsMBA~2ic`go%CHc(n+j79Ew zw%`n1hJ9&-w)Mf0s5Ng_li}NM8S=Nxo?A_>heN#bvbk!qz}79V^qE^0I7MU~;xnUp zvw2@dV36($bTu0dyPf!@X_c0Zu3G1w!bYBA0u2(X-4~=)efnKzH+g=W&sy*GQxoPK zkcub9#U{)OKBm0`IOVm^gfS6>oKU8A*7Ay^EYi6rdBoBQ zd6bm2O&I~z@C03jw!d3N^QHH;gE$Yxy$&J4B6PHn;-G)}*omcwd#B8q+YFWVn zw$BvV$4l+9luL563P==-vJIy5dt--dtW6^(EC1!|>KYfF6U~Vn?{+F*h)~vMn)dfl z_N%e7gC&vfq)#e{249tFdXSf%wv3@~%#V7Q(?_w-5T+-ZrBdC(=Ehe!)C=xoAyMgp zVw<*jt#O>1T&UQ}iN41#!;bQ`BT~DPHufPR#3g*3#7K zmj(aG$VBND!7gAb@ZnsSCeKr|mH{AAQt>4*YG9+aQO0mO3#ReJ+-G%}^ zV;@XywnMv~tm4aTQpo{w&{uiy#H#nAj}P7*rEb9wzB8>@KmWhjd+(^Gy7gUjg(?Ur zk|<3;P#_=(l28;Oq9DD68UYccgkGd70wSQ1-U)~ZArPu`up&tBp{NuEqzDKqMWuQs z?(f_8ch9*0oH6zucRSAhBMBjE%{A9t@0{~3&-=XDTP1_RRm$-|_vi*t^NW8HgxfFc zXj-t7O2;j8ct|kI8#}Au?5-koSXhG?X*c3I&5KAT@rISfqP+^qi&B45WyC+aud6$^ ztQ}r@`FD!$syv6{U?SON(nB+>&f?OuY9ALf->{uA3GI(Gjqe3KOcPn@^Dn=Kx9;>( zV}GiA+?|`ZdidROhhF4e?i!rAH9ffT2>%adP36h`v0D%G0{!;>{n9?R&nPK=RvDZZ zTfb@S`_(COQdx7|z4Fp0WMiZ3oFh3R;HDLu?);|l>#rnr1>Bmg9e2#_LW_oMUX#1p zlRc3v7qSb8!<;>*#eH|A*jT-f+!Ov7QFO6_{750ZoL{h#Z&r<$KnzfWT=JCtQAQH&*>rABhZnI~$xMqF=446v zg5kU+4Es41T~^&8Cf+{Uq=8pt(fm*h49&wGM~DuG_Ic}(CC-E#CIIyS&kE;;{$Cme z)VwuhTW!kAo#+RLJ{k}$sQR?5GS9W5sIJ-cNB11?hvKMIZV0*GCMB&2_~$ou*2WBN zLU^qbgXDmk!~Yb8RXr28Ulr&J5<+~UkDln$%4iW?;Sg|N?Y&bCL5wL6-meRXa#B(0 zdD#qnI&_AiDHIS8Imkf()Go7`_v0}vEy(PLQbwh9aKY{R?EQrcwp@jY2ObjccARf< zQtz2e#4y!`>M(kPVr-WXhbXK>C_R-XL(jgg`7}@&K$rl$4iRvgkO~qxm;^nT|4AMw zfPi`djC{vjI5dDkMV7#!34CK3vN7bS`d(z=uhW=i)E&D8`1a7a*jL$%VVbPJRSJK! zAn^`&ypGgKB*kdR2Ys4ot<9>QkEY2aR>WsjwO_mM#z)%i9UmdrayC0(7OXifis(8# zka-=ek4|dJAud~2UHUAxfR}!}m6I1U_Yme$zBJ;Li?dYsk9w0U8+ZH46S4Id88kjo zw~SBvDLyJbmF$1-rC1(1CcAY9d&_ufiaPMaL~44WN4^4`|?I!%86~tfMa}ET-XZfY})Ydi5H2m&=PqcN?Uuk z%of^K`8t-5g$1VBRIm8tbKRoR74ys&c-8v~^Ck=l&1dxEHKN0e6AU6qnWXGFcoA9A z{^8C8q3h^;f$1pc2TBU(CrlM?VC$;{{c!fhC!!9%D`9-GYE%RZv(mGh z;vjb%OFZXk`5=o_`dkFTvg>EsMUn-g#7az{NoHrkAHBe6j9Rvl#xNVnbQ2OBh0aWvSlqv~&5JXD7dV zC>B0rbMC#AF7YZht9poORXnZZFd8t*si!$ScL}f=I&ZbENo~#!>p53nBsCUPT?3=m z+^@frYx6q$T)o?RDnIdB10z>SO^rCCaQ=YH@I`w8&&wp6cP6T4C!f9g@y!6gf_{5q z3v2H!F<*OB#x&)_XJpv>jEiJe&&Qt;$MrXwJPT=0$vUe_9_HMql+4mEs4#TY*Tjf$ zERiNF^+su|qW!(NPAS1m^V6y%hKrn!G@={O!Fn}qT=34Vpo(k8%&+AR(emROiT37o zfDC?CI*P-X`JV0AnLb+LTxd70=BW_x@cDaTKKZ0wQyks(U?&mYp;5ed6miz(B|}xo z7g#goE;~R-Hdgs&?%8pLcj)c(N3Q&i=<2;KFy4!wxNI*E;~%yR9P2+GplZvn{_0w)h2Uu3BDg&q94ZqMQyeMy9E0T3Scbj7z!} zM01{bKijfF2zO!V5hc~1Vx0-@TVcin&&LxD`kFSV#=cy{1; zouQ{eRDD}EQ$|)Y2QBfIFX3K399oxOG;{~GnMZ5a^6#3h5+QGIYOW%8n(+JT{ZZA> zIraS8Vr=*(6bxC=mdEe_Z1v1|U*fH=qO0`Kr7%_b$Dl;=p@b;=D4P&C_7wubmCdAq z;4rjL2;s)7;hRd72~aFiA33NG(+bteML~C>`iWN;H82O-NNr>T%zqr!SqAEO%6{1cIW*f#KVXQ)Y$j``V4Ew59 zm@Y0Elf6%tDCN{cj}FP+r$B0>aC#cJC;+S_JbOTvTb~u=>PW}{&kSQWaHK~WLDc1J zhQ}L)>aDy3-f9cb^-Mw}s3E5_2p9qrSHKgj95H5nW9=r<1$0{slVaSeQpGrg=gOBOcBG;0Vpaq;OO~_F#fc9iy@8%u& zl+8f!frIuaHgj5SQKs9UtpU@Q4VK9ckLrl%CRJ zWN@%Ctfs8#OpbMlINPlA>!v7U1-jeXq9q9zWBd!=qfOf3#b=X}uSK61Br%gq(>4edGVCkfMqR1Q zJ8a^*qT8|0dz>6DUTdSN2#lF^@jDcwhs(NjV|pczUqZR<3*n;2Wk=qj0(hP+armZ? z)7NMHrPpYM5KZPfMQ;}*c!chTHk9FGplODA?IF}zyz4#nLD@4?e>AWC>E|s-K-KRteLgR%1Et*!PIv&Xe=D6VHkpo~N8j73H~H;_?3U z*JN@!{{zR|0ARmgK4+;I=V#1nxb6-w;vKG9P7ZxLadO1efF&%f*|DU$ChO@vShPe( zF^4;3Z4xwX==3_kMQvn>>@|2;p)~(_gW*+?66ap88b`zdtE$ek1tqSkm7?#@4%?G1 z`ztJ3dMX^(F1&rEDi~oQvp;Wot@sy&jLuddkF?p+ap>IRs9|qI(5=)+8}g^uVvovs zo2ieSP`_#`j492~rDk1YGny*Ed)%7^tjeFB7(Y>UpGle~2sR(LzT|d{vsJM23xOkS z_=&4Y+Xy9|O1#$Ha^F5zwY%J6at2glyO`>S zyGGy~a{NGi94VZ3?^yKt*%EQ~A5N8QX+Z}~3 z(XpEyuu^R6h>LF^{E)yaIkA^N7fp@xd%yeF)VG&N4uAY}gvE*x1)9rZOzkV8U&4_Aq6v20o|wZZKuj>RtSICp-|Yz<)I+-mUt(j3Si zKJz;D;9LQVGMjQinck_C8u@QpS`SWuOq=$GKgkF5C-)Ups7PhM+`P~nJK4Hkc6N}z z>qXpcbw9a`JLN8scf8vcynm4z(n77&lu7R=_dMG31MA%relsYngF2)!;6xD zM+Cplxv1L>NjeHAqVDw3P`S1HiIAqIg#m_1L$~<^bmHi|Cx#ylsTb*v*JPeCk_^aZ z3LRJPB&Zjf@Pke*>h9NRVO8YD$TJ&?G9(e9yM*XSgfyCCEt^3?vzikj*hfo=c&LMd zde%i!qz%^4!n>ldJbF}#MrMChA00iu4((vaX9=0k#yA;fQeCq5ZR8RY?RZ3Kl<@P^ zR69}5Y?_1z1WnK>oG6T50uG`%@Y^XNyrS8LF;$RF+(5Yo0kWgRmaD5mvKe}U83afZ z_d6zogEap(n=WXnLMK4w5Pg^`C-9yCyjX~VmO6k(G$scQs44^w&{!k!^?BV=eDv~I zSU#;bBU?S?P9fA@aK0DbZgboNXc*Nt!^>(@;yGcT9Yb!5hlg4!6Ut8ZKpWAgU3?kL z-cGWLy^_f+Ku6l1m1LT&8L&Lu2Gbraf7^HOz3BH}5JGL`O5)aIPop;mGRnjwgf~^g z1|G}@I`WdQ4l{ghQ=IU$oQ-yMTu>aduQw~By4{OytdhvL6S%FaZnhlrzzHiIqQj0i zDBnmaKUwX~31tqaeqV@hKK6D%N0u!O0sqeaXu?>uI@>BG$c+2su`etJELmct5)Rti zVp)$0Qep{R5ld;SfF&Tu(mLo}$-X;&hnpis5Rsng1JKk%{LSH}4n-=L?_hw8Y@qSI z$<}z*s-^iGwoEv=RQRaha=X=`} z24wfmZa$_Zi{uY?*-2ZCMPvT=gzldWx4S`*5&K!U9CPWYrmYBe{eL&TBo{8Uu1I@*)MbHboU@x zW8XQk=7hVikmirI=6VkPC^G$VLH&Geh7TTLi#!`=*;S-)e0zvf!b>fn`Mh9I;i;;* zGN*ddL+Q&xc`hWIe!Fp7P(~suz;`OWKrd2^)ts7mQ#IODTK2ftAtlE<_YMw;kH*i! z5ze|?QG=%k%AZe)yPq|Db#!qsci>sA_9?_Ge=&h4amW42Y%Y6zjc9dJzme%qur+?X zE#ACdl#p_;-qu!Iv-+2;20ePW`u@RdyEw}gCl|05Sa1-ev@@LIC0Oq1E>uLa zVo%f@kuR|H1-wHJVTKJJQ$nMmvSwV2~6H8_acI6TjvQ2je05e=#Tzcsj33ULIkGyR3(DgXZ|Fu>Cf$} zk*VRv;e^he*CK1A@oKJD&Tj-YPT46OyJxT+?&VWm)LHWm>nAxDtFOHIvlzG0rCm=B ztTN64I+Hi?q2mMN;OE+B0N-igW9>^@g9G~msj_k}tB-6At`8mA8tUB_`-8H`w7Y)n z0q5YGt-%uC()w=eVv*q_P2-wbBKx`Zx2p2!7H95=r6!;2fgxC~jiT_O$ey=o#~wBM z?S@3agP6+M!|L?@$85Rq{Vuoa92q-~^vl8|Y?V#^Wlv7D2{ue4ns$5WNGc#g?pAM; zR%d?OER5`al}i)HE>&P=oYIV$iaSW8XNWU|O-yARlhC!&MSc#!VlKgN&X&X`8F@}{ z-&iBl=?Uv13t_X5;^GEBO#>=11;8M=pW;+L&HA)b+v{+MxUDO!Rg-tQpeA!RMC2`Z zY<=@ZVWj%CQ{9l(lQ?1&2MDGNX5bq$=^?ZlQWwcNmGLA5PPhu+t(vIMW}eYGBXyqg zotc?Fo1tIz1Slb_fuTQ>q2>tlR@Z?iLG`4Z#OIKrf##V#y+KO@!(=G+2*4CS18?P8{_H zF1qK63_Hvut_N53hKh2vmrCl`$TN*KOx5pPX-Tk98@?BpA4Pz>u`GO1)Zu%C)Y*WB zw4NKvyJ|$g2%I=M}y>i%(i9Ef2ZfICyMm|-1gm17IFKZqByc}*uyi4ZsEEsl?=PWuM(ocOJ zQy_Jx^y?+uNN7|cY~Etf&BR(fe_;0-SI(Ogodzq2T9w5g;YLRUe+W*@9Tl@wPgZNb zI@;EItQTI)lxm1q{g9CV!}ukWmj4SYCPcVvZ4d7%4*Ozo5$jcqt#g;17=DnMv)`9* zC@MC6W8Oq0^Aw1!;uHD}X7HzC0_|&EP8c8a9xlI`tsIc$So=(H7u9PBOptTVjeVze zD`3OVy%?EtpGCiO4*MD%nc=_@I>@}RUR3w_I#+zAxXZcu(^LK*qN=N(JxPH!UZLl_ zd&>z9CEQu0rBdB@9beEU*7izv`?v&-^KK3&=hddsvYV#_Joug^Z7ES~BVTTG&-!t6 zU9&^)fiHn3T^AGzS1z(tJEl007>|nax+K(^jJcp=eV-neFTBWcNdTn5On2V)FK(MT zR*qv16+GlXw7yq5VXi<^ek%8vRCOhyWnExUWlZlFL%f<>CNr74imK#oJa^}^I?d2q zwi$Q`KCQLcZgGI?q(1+~rvsmyHLrJinsZZ^D)cr#DaTV%nxqPH2wmBKL8ktRw)#*0 zUgFB9mtYnne=L%+zN=Ez_8R!vh#S(N~duzo5;cz!wQg_1Wpu-8dbvL*owq$(H_~(04@g6xXwoli7-GuY$|g zN^r9`^<)?CS~D(FY(W{+%_8oVDz588(=kG(iPz^jTM*u@77u?x`o#-bD|)iarW4U` z>8=ZSh%s+;#H_fi=M={Q_H=rF^NY>Vx^2GSj0MJ@=3U_$HFR-M=jB7%IvjQCEWLrH zbD*ZH$J;jmXp=pCo#J3Dk8Pg4#o3Fm2Qb(nodMd%nWiE^;@L`9XP>06pL|4TiYT^_ zX?u$RDu%>XRrHF9_p)(sVAXO5lVF9U`{e)&>}k7j->U}dRW+rS0Tf>QG(q~a_^nq6 zjwmD5HWKZOQnG~%EtdAly@N0!M#XGEN8%WXR-McpK<35>9b!;btB_VC#=lrn%OyJQ z-`7Gq9&aF`dCQ+nXUJTv!P;z|Q1lq65FAQl_HpIs@VR*s5Zs0!3X`Q(>q9zb(=`B$ zf<8>mF(>5Us+uS&9Y8SK$+J;?w6)x7wgl}yM_7F>sxB8EqTR1Wkj60Sk^p3~stS1c zEtsVMR+1(qAqflf)T%NBu?5QrXiP}c)*{4;ysO#bN3q-H14 zh(`yqaO{6Gi+FI-S{dQe*{s%?5QVAc2qIAVDVw}NN^8{ns;WS49jp(DvDXt0MiVwgM-d9M;RUW#`+et z&4TC%>sfdj(4BOfg#oA)`8F56&-KJv_#J&(ys9L~bx%l10L~kG;O6j9$RiUh`e^z! zh%%V{M5d`k6kigQmPeBT5GNDJ2CS;QKFxR1`*$t)8(7b7R6}5h9@_0Nl3G||c0qT- zQsd!pao*tlz9EEDvQ=`!V@Ze8*En##Z{ApYw3J^Q)v>63{;C;r-M^K2v&%_+U4(Jg zsf3m(TzeRbnhhnUY(|OjbmMmWL(NlON%9ukXMy_%o&G3H3-xgBd4tXTF*5T=ox zs&9RA^#ImDRY=`S2l5;C`}Ao)pR*xPU4;TynNq~h*V069=UD~JIEF75F^#!7ZBtq8 z&M}H>=x>>jJ?9+u@|y->RBZgYbJVlp*=G-ZKSuR2>IGAJDY~O?D`q2xhdbIDc^DDl z&Ns@voc9~o2e`2@HA~RUJS|aKShk&!7D7A8?5pj{{Q<~vH4i=*w5WHEib-mSC*O?E z5)*b=)zpJG;w}7yJfD_a3=#u1fRe+~qgaf-85Z}P9aPH{eVj$QldiN*gI*)SH*Fp_ z64)G1dJ?eOsAB1OmNDk}CD=aMHwuS#zjA*nwymNF?&y{kV5m7C&@{Wj=Lq^nJ0y&e#bzQ7gUE*JziMucGM}SYuQT&mC#8@ z*w@15`1szT=@_P|M|U@fRcC{rAzKb!E^%?h;j}0!p97`HjQxC!`yKLBPi{;0tk3>} z4&O8t{D3-KarFQv!_%YQ#`o2~CfSSd-1bGUzJFTyqR8JeCV)&YdcLksaxB5i+1%Dx zo11c#VV`hG*V~&|byIGHjY~dmxHc}$%UN1zoba|L@`rpO`?<`{L}CP9#`9JFNhNbM zXB1ZjKEa%2)LCh|R0Car@Lex{`m=?qDvdB|IWJVAR;f;1>h096i0on@t$f$QaDXhX z6b-H;;=@TnBc&_Xrb5SvLUSxHh4Zy3Q$>xQ%@(v#eMdOTa2;uM7qYX@Vew*P4OqHM2h{gA*H1IP+v38?TcSo_+L7q??M9^u%_eq|7Ga zvvb$l4f4Sz(Vm}X2Rh?7ZJr}})d>q;#V~%&O%~pb>;RvSc(xpZ_~V9uXKw9(+a^Hb zx8Ib>Sz7k+@Mst->(cwasoWuU;GgCkQhNRpw<=*}Q?tmg;N9C79B(1NkXc?n5E(sf z^Ba%}wrx1{1=30Jrxlo=4CC$j591T!D=_R^>SUy?Rc^$TiPQHPL zQfK2ViLS5`{8+CbjkLx3$dRP_Ld?7SKFPx6ZbKl7P(o?rh#Ee*T*fs^IruIz@LMUS zSgw{;__HSt7lZhThF{f;BIe3sk6-iLQ4(dKOTV4yeKf7PiKseGo(q# z->r!!0mJvN z{AfcCEuC|~U{}M7f@F!k4Uoo>gwh8ajVdGoPh!Tv4f`=nsuCc9!7vNZGvO=-@WA%= z-uqdUJe$OAEzj*Eg$;)!%r2}L2gYBH(x)?ACm);5^XXRcN5Sv`@aX2#a{0A_A%ga^ zC6X-0`ccF*B@Q^uaAJG#Q$0K(x+O(P6xJo$3jf%Rca)_E$}at{%ZNT@2w za8Gj}Wi!!BNTDScARh@CAMg>x)hy#f*GIdmDubHD>{ml#@?jVj!M_j&(i()sy}i{Y zL?M3)IQTV@NL5;1B%WRZI^|@KoJxhm_>j_uHn&A-6hWE`*nnvi4G00?lnehnmFQ%F zkMmo&p~M;0*YrHvAjFf6@@eF!F%Lfq+k{ zC(}*NPcD=$SgygW+s+t0o>Mf2CA=&0)TjxOD_*9vc0=ck6lQeZgqCj8mB*^a1=wAw15Rd3mWT zogrJ!rA;C4;;N}|h)~Mx<>Y-#=)ewy*$dC^^X_i!EBFBmxC=9s;MKG|sB+?3CmTK| zXdFM<1ISP+XnAgC8W%K3+dUZsHE6wFwqsz5R1Tf)69P1Nk|9wPc1Iuimh=U~2H&*K z@1xZLsXdReGFW8L<`cA=S}QH&}@ByeRKjffF|pS>`xC*LeIVz)n0Iw zJN6-`oUMejHhVw6zbe3Di|%Npt-P&@@3bjsUe&z_^VWeK7Z7AF6FOW6UrY41i9Pir zdw@NcIb2`I>Ac`lv#pj~Drd6>{;bRaO&i&CFZn`_L_Qs-%g}r`c90z-gjSKE|LDL) z5fN6GF={*@Q{p6stZBjDEg_d8Dlfi0#7!Jmp(R8Zq@tjV-3vx^>1l+TF=XNSeW|FB zy66Z*@U4R;Hyt$?@_ifi>7J65uMgYTzx=FG_igsTw9~e?waKko$t@KB8pu4<_>8U( zQ-zz@h5un$Zrr$wqjvuHCL`4JJa>aN*B&r@?4^FB+wM<@+`N@MV^XmpXVB&u640A} z!76MrBRbV^H1YNQx}ZzHpvxPk`Kg1I<*kI6)H8viInNVA{=4xIK+@O0@AdkdL`$dq z1Yjp1UX|)WtP;jF68aa~A_9 zko$3&@cX1bf?vvlce8hD-?g}$Cjp%Y{fl{4XKoLvr}W5SsUk^-5p5riRNF)anz|M^ z3rlszTUb#9$DRT^lB8wN+wtajym8d&M=d&z&3D_IPfw=e(HGNaPmklJE8=HeGL6&# z8&pF@1+{!k?8)@nqX%)`>ihDos?F87^Aapu5&S}l>WthMG@hdDqsCwG0^BbssA@M97qJ<%4ReG96k%# z$KlnSP99E>KWTi`CgVvagRUb@gGGsPn)P6#iac5V!*Iy4=#8Qa@%nVk_(^~(q**WQ z73M)8lL)b$EZ!LAFb|S4k`k{%P{TMvv9tMg4F#2zd8iH@=nNq?Xm6*3j}L;RMKZOY zfRT+G_@>sc&R}Sip(@;uVV{j2oXXu}a*#7z&KuQwtWWD_7^YQ|RUit>9(a|v5BW+J z63u7EfOLR^1a0iR)Sh{|&1{(yBAAo+7Rd#{VF=M&xW4`*0OEB6d|k6z=3@lDPmD&C zhFjBDzFULtYA*^#&qCD zXPuJdMv7-}q+!+s@p^hlRm0GgG>T!#c?jyALmkLbJTQ&V=K&CbY$no|0AyFvph!vmVeWxN~P9v|9>ZjjC3Z#NRB(FsF(Zg$+m4G3KfCMfJ~wVh#nY zO`ah}T9e+=L%0J^^YnN_;md_$qj;RkAS>Mut-7fkwd z3mDR)N+m&Wj4T96Z1`c*3M+spHu7e?|pO>NtfvnH?svhE9Y|^eBpAFex z(9Ub5K5EK9#_wk_x=JgDYOu$9o7_xpmg_K3--pM~(4(UCX?)3k`PXK%4*Fy>FbDB? z>WIK0axN+_S}GA3MdK8KRA*y%nf(MOjwf+PC8{1J84pc8cC{Cycif)>%79Tn#1rza zdus6(>!EaM`{<8Edd*h4F+~kmCAeCc*tMA-K*3Ce1Nfn#%yte)NA)0kjjCBM@jME4 zbu2By>E!OG!NTk)IZev-S;af^82b2e1w^1hb&+JFO69fTJJe-VLyXC(P)oLSa%$BF z!gQ&K<2z5Qj7jRbJ9_P&IN3i<#k6|0Ja~9Q3jw{<#74MzN?bx-Fud6fZ z56s?+6yM%Q#~VgXsjSj^ST*lCJ(Ig}U~*us+3`((i$%LkyHn3ARk-oKe@7qx+a>`L zQ<%McBS6^gZeF2g%eSMw$u^I}RyD_V7&j0csV6Usi7mHs6phu!O+U=CpUdP2JLV!y z*#h;;6ZX|R{FJ$EvGAMYcyFDoYP2KJ>&y@DIkAJv?pQTOx7{$7%?zZTtl}opI%Jcd z^=B^`2|wNU-1*R|$%y*6c0_l3>Bm>|7CqHc($Wk?53s(soLžK&w8_KxM_}nvV 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�oD@2i_O4OuEJ6~kENa}*C@}oSJE=PODoJDmk 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 Tcse})hmijNAE8bCZ~p%Q4WTl+ diff --git a/tests/assets/hlabel_classification/images/test/12.jpg b/tests/assets/hlabel_classification/images/test/12.jpg deleted file mode 100644 index abd463df610ba518c6b1a51e35701bd454ea693e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57832 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 diff --git a/tests/assets/hlabel_classification/images/test/14.jpg b/tests/assets/hlabel_classification/images/test/14.jpg deleted file mode 100644 index d7f2d51fdfa42c69b748a2c41e390b419c03ce77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66226 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

    E-U@*NBLo7_*tW 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 Yl*x38yn1{n(y%6GDnPqf_sgsQ0XTo}k^lez diff --git a/tests/assets/hlabel_classification/images/test/15.jpg b/tests/assets/hlabel_classification/images/test/15.jpg deleted file mode 100644 index 957f8cfdccc1e1b013c6236159bd2a2ace2ca24f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 370121 zcmeFZdstFw`!>7)#l2;M8?thj0j9Zu(z3z~MFeH*hRoa#P&S(Bm{SdATCFri(Ui)rY5tdai)2CSI_46z2EoO`+djr{PhLLTI)brz`eM+ z&+9tR^SVCV`S1{!ON~#A2VgJ&fI)ZQ!#yAluty^8khb=Ac6JU9_Kwad7iT9YXYYCL zb5IK~ixw`x`1tq+hA#2-UmD=!gI$4J8Wv6<5EdWUJvlysl1OE9xT$IB8N9swg2K=E0%7U)vhpt~ zc2rjHt*PC&{{Yz3ENN*y+}1Ab?vX2$Dz!#?`b_`8;Mt*b=SQx5qrZA>^!g2>$viQ6 z`_A`wA6Xthd20RX=c#AYuV>!8{q5cFvwwUX7YsoBb6C*+p9A}UjLQug7u?1MVPp4k zTrl_-(1vicvGof_&WmN*<&?PlM^xFP)``1LU3LhFWW7S?ZX0*>2qZjOJpFNK{}|c- z*}(Sve;V2U4DA0J*ImFF0fW9ggd4CHnEgA5mBBx4pRU1wVGg+d_~`TZb+^~Pwf^`A zChfS`uw(7WcRN!`KLA-pAAl}J<@Z^e!LP4wb~#}8$N6v1`AE&zu4lY+YT673?biq; zbJ`g0$WBkIJTm^aO!rv9IQxj4RcEcLFG*U2l;>M|d@ymLs{6_?YineSX1UQjn9=(R)@QTG9EyuP;&^PV2O>J4>Xp_IP!+2Fi%%%KIm~OE) zdi0Vv20T?4nc%%>izrg*BNe#{-<+CZY_W=#-Yv#kcW4`T4rF71HkJux-XroFX(Q88 zrKuu+reWS24SXii*P?-?`Iv7+DPLF>T#MSiz=z9<))|+fGgSmYdPfHlrU6|t5xz^+H`_R~GX}kZh zoQ3^%pFmsOYyn(mG+aK%RC8#{v|ergi_(x7Ka^2hw84W0eH z6?(RVVZ%NU_*=@&j4$Grr6o;s3pQRA1?*GnE~RSyA5MJZjek1ja4iwu4MY68;U#0Yb32d<46HLv*BBIUKV;Jet$JQDeBfN{gN}y?pOnLpY zTN>KGBspJM1p?>X9Sgp{VRAc_|m)b|@l?mCIozB$_X>N|fbVkw)@-`K+0?tSRLsrK;=|_2SL4*UU!r74 z74MuUReeB&6_YY>~{tQ^7p9XmwUr3#+76hFp)o z%dOC7#FFN`U?ir_q1MKdQ2A&~@kGoXSpc%C)k4HgaUT)+&LA@GhvOr{3(lDBpI4JG z(H7m!>xzsBY|NPL&eCM~vB9H*B5yFy@kkM|RfJzT@kN$!rgt4%3gpMZt{p^*&u=CFdn zh8zC%a)ym1mfn80&o-`_wLZe!1{NU-Wfk*@L%Ck+yZ0AP_~!0#nIaeM>ZoLr7Ofg5 zqkrkhA)^?uPur(!@E@K7cBQY>pXoi!zf`XI9;`c4!Rg{ONTHzlQzU+|g(vH{K=ltPcpBys1y!jpd&a()-FMh-o7vAm3J6M!a!W6R*TmjW2VNXK<95In` z8@xE8qebAV=$_J(Iz7=-dNd6L4(vlaIrfjpN@(`s$!p8&SXDH82pe%5M-CTI4oZRQ zQM-Y#s~&w}!|Fx7K)9<*%GnAvsx+JF{9|8_+PxaF(zwijSt;2Z;W5vcf^BHj>WbEuSwf2xf$I7VyoLN>^BHEVdicIeO7 zhWNFoF<%S!H&}Z&R5c0J4OTgv>?*hg=+mLlB`BO0pV?Sv4qD2cvNw>f5bO^T46b3q z^vNSwYug@=*&2atc5?)JRxgZS!=%Lp%7RimpGzy3-6VP6^1Q_gTr-i=bCaV%`_7-2 ztGcJh_u+n*Ii6T3bYGAa%#0YbEe=m%Q$||I0IekgJYY-f2ZnlzR)Ro|mom&inecit z;p;uvX}by~8Q$z{-o23)ttyh)ud{S`bjIUIGGvjgHxaa#IsP($=*=upw`1YWA@~s5 zxDjvJ8|25UZzhH-t_t^`V&AZKxTCWb@mEl_Vy7VCMR9gvUsZGB(j8Q?zXV~`{oIbu z@_IGVg(;!P*-dK%n#;PT1n!M&V@UX;A+>ztRXP0VS;C$turS;l^ybgdh+dDyEq6?g zRz)z0R}Obu5aZ0bjB^~%4hZVdi7Bb7`rW*xwz)&ZTvvJo*?JUqg_*r{w1evqkLhm+ z<|tF72VwS)1S?GUbMPnE57g6ztSW@RX8l{i25_k-l2_MDr3Xcl_dLlFJc90j@=s>=-DIM5p?Htyu>mAik%Cu& zf~8WPEm~&PeLJx5J3efGo6bS+OJnat!LWqAECdTiazo4Y)Kn=MSSN7afwHU-9%Tn} zY(41x6EPF#nn4W|3ZAU??@WUq@uZ5O^E`PV*{X^)EGJt<{#;eW`~J7QR3C;7HdpaNun6E>5QLLl7T_=HT6M(vp>!brAX}!?nr~oX z_UaDq9{ds%j6SKO4C#Iu$l@3f0eepP&I;v8)oE-6!e10(!ZB;fq#$WR@76J z2!@lKV-0jrc!C&&g|*4`i6D)E2u}o?KLTl(MVI%JZp#{;?)`Rqi7P(ZLy;x9CL?d_ z7|XL9J0!Sc5n*ql*ZU#+sQASw7)-<|ve(gE);%1vjj!X-WZZp7-4Frh?idgyj7RS0 z9%R^_@*}(|rJVBFu4#>&qK*XU(Q8}z-NQQ#WK>#TNezlgvi-Dux(5H@Ik4c@k=SGC zjbGhv{s8E3S>KoyENT`nmoC@Fwc{y}3IF^qU?NYxN0>h)`Bx-1SH0pGJEH5@}%qKDB*f9Fl^arTwvbxbqFt*U56Y%?W)I@C6$6Q>UA-#Y_6 zsGAUl;hl1%C*QNO*S}pQ zkb4DMv@r38tT5FX6F)B>2T@DcQ$# zzm&d!PYP5$)y`We{N=~;Gf&nMdEU8eFEFpO;%o?z$O>yRrN?d#t8(1tv`XV2uiu9{wM}qmy*S}J;hSCfz?4=+azD&3bKC^B#FOK2 zSJ)?OxYu=#9h~seRC5U{!aqE*Q()wfW=QUNAQ;PbZ9!ea!vA*P)FMceHiiI>coGg! zjlt+adjxyOfEW63S9;(+FrRK9*mh31FBH)CdZE^(Vg_1H2x0Ear>64wRQp-yiqv2%H317A3 zg|Ph(ZdxESd)d`LYl0Ixg_m^mHj~*nvdD0>SgFA;m{1f$A)UKbmE(lLw1wg+k1cXs z^2oXLCk&U>HDRt03N|jp_gDX?Bv#L;l!3uld&f=kLOJdmMzAUS1*ySNu|`jYf@5l3K2h17GwUI5Br9_ZYHH`_ges$g^$#y=e#z% zkQ|jtCSw`z^ieE~I?X%@Q8C>@VBhD4)VU?hDg*5juFCnQo>gG&5A;T}Vyj6G-g>&3 zTU;zGh?d!1y=bIfG@k;u^hkkvvTKwI+jR{EgC7!pn+H}^PVAi`xoqv1oS1B6!G_tV z)`1l$Q4UXl4oAB)Y~n@d6ofZE47jZdA{Q7Raq|vG31zA4QhV|Z$pYp!KK#VtK?QZE z4y;chI5+l@V*__jlpx8=*ITM2XLk{T;!KsO7}HC71Z^^V8}YoUWSLr zjbj>NPYAhGU~ec^PrZV=|1Vsut;#ICOXEOT2@uI^Q6X!p>hwXlCoBe{BoQq0k|9sd z3I}g>_AZo6&<5ZbckmEdR~4m}QvuakX^9W}#!M{qqwMIG9+b8+nZYk)b||%UW#dhH z1jDu*?B=i5exE~MiiIEC@F%;gdh>SOPIktivk6X@IYyz=@0(l<;hqs#?OH*%XiZT! z7(5ZR=f6Mzej=7Xk?XD>fJbxQ*B{>W*75=PBVy%mvXZReR|gwBARs*m0cpwYGu9#9 z`@*q1@tGaQH$8+@WVP2;WzDdrByq5D?(A2~h2QS1V=p(05AnUA5L8#qT0zZzPAQN- z6?o(F{_H<40DgD9+i`S)7imc6(QNW9vJ&w01N_AnUt$U|$Gup4bF%lEvCX-n7LV{U zY}nnh=~5WR9rYP%hVCy4*edj(P3*x(;6^a=Vp7`9(DK1OGD3P1BP!L1@pz zIT1&@dmlr*(N6>z*QZrC{}3LPB0XLt_Zu<<-E9mBy~{87f2I>i_n>U&(2^C3- z+y;+dk@9J+K?d57<+A;ppI(mn_k3+g^13hRTO(}pGXP0vQ8lH>K@#UN#B%1-YF3d# z55zq|kj|o`P(w2Ny%m}?Fp~*-jqQ`>)M0joP_EjGRJQt%5C8=;pt?niI=~#7Nkaw* zLHJnx;t*gJVQ*no63MO8CG;^hUa))&?qLXTCI;ONC(WH**Wrl{!s>yQD@$braUDji zhQo)T6rMWaEkoLBJi&q}23#zK8^>_KL!3p58) zgeML{`>mI$I*^Bj^th&*Gn9C~LWcaMr(k8P zXs$E+O70OdfU`EEo?&4_vXAEv$@LJM@HrL1fXMuXNWmkVj^zk^ewdOfR}KL2sqJIt z+8%$=0xM_B_X_Vy%@3EjH5=a=7 zP7QJH2^=3XM4I19q$y_{&ybmmKR*rP^(fd3l+a~qzCkg`z^CogHTX}@fiJ#xh??Gb z`>%?x|Gsa*muog(T{G{i(c5K^P`T`*Q2AvT8p`Wds7^jgMv2&><4!0?M!$?bEDx{Y z#~LUJsQqh%$mo5j%ae*6eZmioHyw<%goEz{K1SGhr|`xEqe{1KV_ zy)Fsw@4`zLLVKL(-4w%hAoz_s*4j3QaGzxb0RhU0d$X4fU)^sztEDmQ2*a#3ynUC_ z^In^ytfIyB+i>4?u=K{xzpx01GioAORpK3T@@1LLy((tJL@Ro}BT$nj9hj>oSyc@X zOKXRAKFF%p2RM(}a#N%Vr{sHvD93dek39pvsNA;w8Nwjm$&v0HPHkFn;-JF+FzRX; zggkFJr^F}~-Xg!31jeDG6s7d8}JDqPIUJqFTPPEa(Br?K2fZsW;%8#XMGyn@i(R*iZH#mu&0DE2jK zwyLlyz2+KW1YhW_bR*3blcT_`SlEah*@JooB`JmNgYu7y`k=8$0}Tp@8*DD3lD%a1 z8!NPZL&P2X&{^x1HcU{`0HyV4KOcr|{37C-@VGhB&OIWehLO*SC>B=y?F9hE{i~oC zLe@asA-q-OUPP?L$I^F8U<;tNx#~btJdO`^gFJv|3n2uGWn_)PL0)VU$q^|iH=dxD z;{Zc{k;+oXKrA00JO)v1E=&+}lr9A7wUI1hJRjEoh^}%3%HW`_@4|mx3=gTmY$7=m zo=(vTJqlnYNX5eV@@|m_NOeKXHMy*81d;2>LHKnzdl?dBCX!;tZ1#Y5R31R(;Yvsj zNzgcLQ4Aa(hQsk?$nm6jEUeck#TkrJES#*djUVup*(x!+2xADGoUPU)0`z|9ARHuf z6-vnzG9;>f%bC|lPuIs=M8UuG6eSHFM!{U~F6Z3ofsQvepFroKZbRvd@-BT~`49Yi zsNIb>OmgKldnOqLeA+%;ga7axfc^O1-+Q{?-^+ab-RWc1SJs4F_yC;n z`~ZxLZx0sr;XeT0m)7pB3yE?p#~ydFLH%N66*gY8w%8H9h=e>TQ0j^gm{jd z-+U)%NZE9OdC6}N=B&3xb8WIO!Pq?aw<+!uor_U-orUo1-O!-mWDN+{NWO{wgS)L* zc;$`uP_aPtmwBr?j(wJ4b3s!IEp*7XIo(I3Uhn&dSNsGPqcavQ1JCz>0PG%h0}-VX zCNM1B%j}TC_9o`+;^tn2CEPS!Scs=p!15Kt`&@j+jADmH<{CHhJ|Q5Yt7W zH4_St#8)WyBD;lOVh+@^yiAja2f{i94>WaH)QEs%%%nK0I4s>0+}(fC%cLV~fKWaIjn;fudk3w5q^LlaW0H^$wW> z$>7JU1ictGb|9BP^dGaiq9B+KEBv05Z2#{IJVwG0gv|$OY)v>LxL!{%85R@5hm>cj zyNK7&q_`hC@qUvEUfx1T*>dD%FRwF78!m;N^7Tg5;n#bkCeeYXk*FKunlK{_H*zG2 zJlrCfz?iPfViV&9k>sm>-F2o?@^DuF^)yf%O*pgcCe4lJD)>u-d!V_WA6S|KuY0bt|>^Q-{aK<(S4gyqa*U5chq!XJbURG zi1Ue7B2thowK2#PjQUgLz0r+T zjA%Wta8!HRQ5JBExP-~flZxqKbFb3zSILy#+NN8td<&sN3?K zRoIryf4bm00&I-ZS~{>NZ^HC8*6xpyuw(N&KSK=rb}t-r1>O^+Xja4>g8pi9xW``D z!sF%SJjW6voM$-f-mJavCAJ4vROuI0cIC`)#vuV9>fX9OK`5p*XjYJJr?x5{4EC^R z*z!do!NB!E7aFpyf`A~4Bp4L!V+v zYt&BjmdaRvJ?s79czlIy=~{@wJEtVVa=FnOxI5$O4jq3t5B^Plk@6+-Xt<>-tG}x0 zahJn4RfEj4e_W(D(P$2g}mx<89rpZ{*YOhjK z`e5$`MM#|YuMN|-^|HpUssNKU_3fK z8aW`}$>A)xb%!Q}D(d7r&8tpEd#NBiBN$C|fMEO#B{#VMYszTIMfE0nbiLCXjEn*e81d9ECV0r-{I)O?4a!JR3nqc_!dY?$Ek8_~XZe-5v{H_0V4CI%+ z*mE%jLb3lHyT&t#e~(li9*JRVx{sx|5=<@>L9Hl&xNwJNgCgx$k~>b5D&s-Kx_K`f zGPAY?^x=JjZNMvfpPyZUYl%6?mzWb=Cr>;5kSz#aC8Q^Z-Jh$#w7)Ebf3Gzux;6C zJZVTfa84Je2n#-WS#C)5wRRDPD}?3G{_zeh;c~(w+HT2ky|mHCyD=v}O1;SojZ-DOoshTffZY z?Q*kpJzY=UzC(UsNNziN&#L+HI9|w_^Xh(F)^?}Asx6rD{NnTQMdm60KMT?oc(DPxMiOdHBE1vq1jcx<|d{D^&+6d z9KvwJB|jgNB;R;a<{*pmKk;%lIAZx_9XQL&j*C`5Cd}5aQz1p6I18A&5NDCw5R8H+ zoX_<@kN|g1;WLJ)iNX|NWp>p`6s!z6u>PuCSg(f3W>{M--M=QC7cHa{q%>op2tg+( zo7xf&dHsV}*Hx`T=R4yEI1*Wjerj*aviiV=LkcyP9o_l{p|-zbYMh zBno7pi^nA@0eU21uc?$Y4~nq5kex07Ed}kmL*=ed=;XsmLXvPX;T3=GYk_=RDj)!R zO%5t|T9C*S%8Q^#2-qzsr5RQcfL~0T6y9AL;BpGGJxL1v*+FS-COEr9UWzg@!pWh6 zb3FuLI_cwcoy$L-6Uz67z(XLUeDqKIc0*7^#wkHI$%Sy2O_$lZ4*068G#;YB7qrz- z7QC+)KsHH?4ERT)K~mm*cmPOk?hPx`1U_e;-~qeh{}S>bzyOX43;!H&EYsX6;A4V+ zl4crTSnK-_x)WG0Ao-jI$>-n4j6v@O@pzxMPuJi-ItLJAyXSlWM(_Pgi|7eBu>}rc z#ybcyzWTSKKAsuXQ1`RqRRii5mSb5q-%oA$2WI5x;|>W+Y4;3}yNQbeA+8_jZf%m% z0UTj^RPDqWe~tBaIzc#9hiPvnj19>V)B0Hd8FsI8I|@cC7Upj;E#BBadL)G8K_{5% z!^U7Ks2jyHhty+tdw7iuo23^a(fa!=yeBU=7(+9)vH7s)EXR6b0Sb8Q!$^}>9>NAl z0WvDe($Sx26yXvl_x~}zjJN(d$>Xv`UT2O|;0Lj8k+1Rc0|uU*zx28k&{iXl2Xkl_ z1n8-%$Wq2hO`4c;Qy+#uiwcR>`@0INA`ameRA}awB>7L1yp6_q!Hi=r!{(d{!8FzQ zK?@kN2ib{*llV(Oe%CD&@FZGOqK})yJ97vNKR&>1AQo7PNfD zG94@AHX|MQ@D!A&WQvZIyitx3VlW@!Lze5#4A)>F15@D+P2)887O#EIKf&Wm&K5qL ze~VZ&=O!ml2Uf_C|A37Y$uu)zvavlIs<}D!*Hhk-r`pf{Du6HTvK;ZVwp6kMV@$Az z?^}2&mr_yhTe!CjG`sj{MI%c;*_tJRWiH3`1?(K?+%PBAvK1$qs^oZI*2t&02dtuy zs}(vYu73=7=ozWRhbzFFT=DV;eE8BCmWqhnq3Mx1xWHR9-J*q9!k$(UMi}&uk1ZfP zF!vg}ZxLWY&mw6CEMd4@An5-nAK;hoWW#2A%GTv8+i_L-#26xbp84S7un;g`qHj8S z)C_Y&jg+uXYG3lbUc7nQq&+N4Bw-xihE9XGGVn~ujU2vJ5XnSZJ6-uOz6utL0@{1Z z2-CC>io+RcrrB@m3-hUF1QqZ5=HVO2P5p~`)l}S=%{gVO@}&jk_K-N*)23^BrI6$S#6lCMivE}@qBlOYn% zCIls6AZq@1OmVC<=R$KX*=$O>+ay9HQUzETwj1Kuvdj`leM$QlpwJfZVGpz`p0mtF zlOmhvscUOx$pdR+DS0LFScA7bOphuAfmsZQJ;9&Z^T~_#sUY{iFbC$u{=WJH5K#2v zN1?m!(53#g{qGlj0K|jO|9)}P2OvHn>yArt?wXI(85Q?-F5%T?_gH#ZH47|;i-L;? zUG$8*I0YG4ZyUuD(`B}VtH@5=qga_^%?4uOLOUXsOq}nc0!ar>x`fx@JF$e?@JjRL zZT)6+P6AZD@(y1tlmiE%d!r?Dg>8)aAONkJm=+{C!S}KZX`1ZbbSaf&5!q@`^8`nq zeM`!1vrl`feeHPu_j?>G>KkqeO&?P&Wncj&8;81L?9Tm?Vf*$nYHum?UQ+@w8w9dZ zhvZ;|kNIcWAwI&Nv&F>lnQZxH^Au;W{te7`D$$VG&{~5MZQr4rznhnf)j;Rt%)e=e zd?$Ohu$CF{LCeZQ;f+ET!K(OpLjst-C{bY5V*4CDh;j&rGtPvjduG z_NsN29K5`&7fpH~^ln^61x_UUu)}9KC*I)z8CZeTT}^%|{3RPLLpnm`oW&o24Dj@` znDcOxeka`;`FiLKIiL zv(LmNq9_e0Y}@}zkbqu}#5=6k?u`jk!Clzbk*WjdtfD2C^_;`LlXVrr41-rtoA)=; zoYGWzEO$W(&2|oJY0CyFQ<}Xo!>%{|z~x^v+J&tva!8&}3f^j)!dBzG3%0yf1~=`H z*>3qwSwA6wi`Q3?WcJ)sEIaMXJA{c+DR8lu?WYCbbF^<@#kmrT?f@=n4AB8r)dv&T zUu|@CNp@kl8&>n0y|H=8E;JjK5l%8+(8gejQ851w(*b=ru4w>>64MQFLslr0qPVPv zCP*NgTMuj;)vth}gH-}lhcS;_quOIjzT5U~tgPVZh*Ws~lp%Q~1CGg2R8~5J0C<@2 z8`=p2H2WX+*R>Lm1lVVUr`m_o)<}cz;sCIwO6S8)cU<2>2h4}?NCRR?D(dwWbW#cG zX{i)Rt29EU34shj_yycACD9Q(m^=kni z9!rf?UYk@6%N(Czwz}}}cn`S93-_EUZCVL5Bag6w^f*dwLs7v@h;%5l7O|=vdmTB(2oY?-Hee43o{xsj<)q|?_ z?yiF>XBRWc=og*E|6$emlM~^S8{^{~fZPLqQU7ueK-j4(t5A(Bo1Gu}@9Lksv3<@7 zSGTi&PmE(<;rMsDWb>0qhoUte(koJ0 zb`R(h<%;X2z=R`jw9j_v9E>9Wwql{oTX+F$(P`g95<(WPDTGJ1^oaTJ?h^*~W01T_ zstDEt>nhFDXVHisjx69XKoKc_%QTRaKC&uMVg z3p!aJ^G1er9mrkNl3Av68?%X=2wPKmDNK9g>(xNaiJKH%s^iZ7;eKZ>Iw(rcRY4lC z@Px092IYEy?yOY@J1H5tIF^DM-jm095;LI@N-haR!KJCW#PyJ+ZHY9Se>DP+n^abd z*WV-{U^k9Ph_cO%oqt`*8 z`*G0mZ;maF!`>Ad&fQ=ji;R!zSy@-hL+>AJA`8Es50*h0xWPB z-t4bcp|`M7Vp#@vnCAN@iexq_U6wD9z5eh8n?r z0Itvpz6@&_xXXuQVlvV8hvNB_08#ArI@`lKA$#cRaGElNz3(#8;3`=|N@ zj4|7msW`?Z$k*X7jE%#@(rmxFz)gQRq5N|oc`L3fYzB8{RNHks}ZLm{cDlRIl2SukoO&I)!8*a6VA=s}`KV5@0xC z4xvm{BuBUq{)P+O!p#WO2Sr$JrUq=8q|yypXPXJNo19|36mepDxy+W$yunYkw)@GD zUuY7j)iGw-G|B$W!O7aL$}!uTKl=~9(+uzIpZL?CRpW*{{Z0CCA(N78sbhh>b?zt`%Qyzl zJq$Udfh-rQOM{Ibr zue-QA5b>A*5s&vi5s#k;$Nwnd_-_&$@weZ}P_FntrfA9uuir_LmYXO3nEvW7?*(6` z2H*PT{b%%Ldr7#53=g?ex(C1>mW9NG0KPuvqZ+trS`Wkv`d{OQWB^-FaBOr0iWUA6 zwn!d6Wa+LvnSi#D029wsd#1ST-VFy8!17pq8nafnHA?#vRF9ruJmPQsMqnV##h0=o zN|MP|MLtw0zH%m+sZaDTrRwwmlwAId4;v!-OLNgsNV}5zw|-;0%yC1shACt(2nr`T z+S)0icXaJT!RAzUhPK#MoV47~f>vcRz;M%Oy;k*x7<&L|qV@~jq@xqa5O5CzJ*E2}(-zv6m8)&-^p-s-`_jjY)m;5o4 zmh;tpi)M6f1wCx>13h6N>@4bm8R8y`g(uE;VA?!dw50po0!Woay6zTklh$=ex0xmB zY!-q6nagE%kRa49Hvj?b9`)T$39O)!MRYROPLbS*1Qujewkt#P9#!a)t}DN7=gET# z9wYnK?dDaPkjK1-m5G7(dq0b^o;WwE{kB~RgXwuL+{PkJP1r(ute<9@apNKzE~z)f zFOu%OA{CV>Aw`jx?F{zX>%cIGQNC$NgZf;sJB5Qgyxc48@uF%bix68Oq?kpD0FS^&M zT>C?!A-#qok4#=+*%ysQ?ZR(7Cqv?_M*=u?7lapU+wA8MBRcUXW6aMgZ@(6_%nVf3vfg*&HeBZCV4b*rt=fHWc{s|-EqK0O1%?d#*=1KlR`8y{|3dP^yj%Z1T2@i$7W=KEI9G5nfg+2pz#4waSA<1zc zy@)HQo(dPE$XAFm4n&B8kj+pjUa{O}kJQ{P){^$WRb=(=gHTcC(o_y%YG9JY0 z?B#$_AAiMYF9#R=@i4n8$+;z(2}XN1xv#opAe|RARaJZ$!)%&P*@z9#47{2>GUaTv)i{E)FR6 zuo9=SMR=kxY-7&@1Asijf=L}22wTd4n*|D()8U;1@Bv=^wtispqeSy<2eyk$#Qt{I5-VY`(WlFU#v?kVMNYE|-Q zD4tN;7-!r$UA!+jejLW}(-@L9V#cn!#S#z@H-o)P&}-YzOY@;j#kuH=x%Na?CKqHjLqGvtI>uQ3;((XfNJsQTLrfJNE162!OzJ5x|z-W z9r^Pa%2L`Q!rROV#y4y5_cOsXZ|9q!GQ|K6lt00ACq5?+ANw5b?!$g@XY^0{hNe3( zsa(|SbssmfR;r{-r1hpo)*SZMGSN3Ri_pBO687;MC?C^;rex(Y-^`++S)5_ATfp+W zj81!@&A<9A8vfLBNBjCLVHzLyoNCmgo0 z?4T|cnW?H;b?&MLVtJGVOoPuAl8M(1?YKM3xHPx5As%kw{_KFnVya(=^pNzY2SEGKK zjAsa==YyfhEzb>UPBYwd&$k%*T+mX~WnKQh@3EL)18iEd1**{O6*YiDCoQunJphvGX zXItgeP}{C|_{D#kAKqs{7`}yncp(0Pu)tfMFJIMof_M57e!-9iH3mh}x#e!P~V5 zqBU1uqJZ1#sb7sOC)P>Z0}Q~Lm0KH)R*Hinhyc0G} zj5gd%6n&3}N9*P=pw@^+B^o}>swTM-ovT4MZaB4C? z2ZX9X8!n|jp5lH5_9hss4lbeYBNC3{F~H`(?pC=jR6!KunOddo@HX7>{&l77)6+D^ zB7LN{;&5&E8mKGXc%iImO)&fBsP?s<=G;>Ku3ay@S1;qhfXe=A@Qc9qW8mI8sgIcM zV(^4>sEu`OW-ad93V8&76a2t@qT_Phih?O;u;TPNt+>9IXS~*SCS&XJQ}PI>s&iVi zvrUEe#LUNbuwdNMs+q#RSWsK-udI6^9}hV+M9yQhu?v>(7+x*(I1gJn)_B0*zKPw} z$4%=kNUTIR5UG|oLot(T@@yF(tDoq+{qTCnByqw7-=TFWzkGgjzw4>DVeY?j<0l>8 z2!C39G53=2$j0#1g8e^lCDumIYNf($-n-xCwtF}51~$7#g4x@}&*rsw1@+x~5V`9y zrzj)|YdKD>bxS-MddYhEvi3xUaq#|xow(zba6{CBr#9yM1-}PeHt@eWQsaB}o|i>c zRYH9jy@t*DHi--}7t-lxo>~>B+UnhrF>LCk&F7aXNTcVbSi`s6PQ6G@pKt6(dm5FF zZBvPL-`U^1+j(ZC^sIm1N^S;<1^;%1jkr0j^jxqxF#OX4td8gF^f(+3XUps{a! zj%G3PwlK4U+TS$^y$;+?%lg2lIg_ zlul~(B~O^)9Ui;Sz&d+h-TW&f39_knH}|eiLEW~t?LYi0cQ0|%C8|;sn5XA{bt&~c z*!S3~gsVs8+bx}Y$Ce zyMKbhVW@;v&Jmpn~vVp zdnELN-9o9WeOI$|dmd605()l&NI z{??xevvvevJXwzF79p4eRN-=nE>PcvFdPM2DCBO%dptBrrejIgV_S=a4%MivKEoVV3{tb3e=L#C$UPo}2-f}sbxvj4v~O3jJ;eFa2R z@A@|$k6c{!@X@t{P)z&$GSrq4DzcdUmoS^agc?o^k6{&1-GtZOZR4OF4`}BHi3M~7 zq?Jv!XnJ}Pi`l7`7%o{C-iZrac;95N!P#rzYa|b$nh8EUC(ae;ELiDaAzE9-m25RkDHU9m@}UD6suKj( zEV`81K<%CLgDwPWjxMc(n&uEh|G?$21$TR{8CbtT-SFG&uYd}OpA5HXX^}L?bgPP~ z4?974RUxx~f}X3nQ`jAV0&ZD!%r{yH;D^pL;Ma{Z&66pH3D)~g*t6@|;0K^w+sPA! zY~Ws96v7^~^ftdXAy$5MX}z6Q(XsPt_kok<;R%wxTAA#!G@^t>S>Y%1Zc6zzJS`v- zl;7C-hq)@kHn{P|>poj{;oZ%t6~7-ow#cg7vhqjvPFPt9`^o!0&+j>0s>!Ik8i}w6i%%WG zJFzS|m%iNhYn*#UrD++W7}9=q5a5 zv!@wpA~hDLD$R)%`Z)M3T9av~Qlcs@8XWIePD~1ngYL|xR%hor0~wwz7>?C8m<`F- ztOj5i#t>a>5SQVdyx66VtJ*|UoLpgN4Y_i;i1{Gy65{9y9zo*T3NyW@>{PxJ#YeV1 zMk>1NxIPpP)^fevb@);N&p+=pWu*1QGzAnV!x-0d*XXT|p@~*&-i#nUehk|SpbRg# z?uMzEAT(#lN@t>YRH|4vl4&Uit8z@M5#jHQ#Mmo!1$sZSX9xB|#Io!UlA6K{N1;di zr$_GRNf8m;r_Xgg&O=It!MyC?lYw0aU7A6inq#z|-^r?D7~pCM z?1k?EG!>N$Du|vP(}`SoA){FM@RUXi-JCILPZ_TSYs3j5+5f;7`dcss}mh#=ZXaXc;%N>c7pk{^-7W}>-Ds_-g~ay8&Eo&gpg6*t_3;;P_iCrhdZ zuqL(y*2Gi)vnKw}vnD>wX-XV!HGXW&`-NcmkBG?s|9DP?r1uH4UH=H-4~)zl-PE1n z{g31dY435u+Ny=jpzF&$B6O&ylL3ZK9u-M`;eMWfth4rFilDp)B_oFEc%Ca|0w5_0 zhOPb1-%H8>uFa)!G-bG0)JQTyw3`$#lP_@0!43jjD7`O0w>edFfoHE(H_@A7&aZkfn*@7RPD|0hHZ#0>G1O_Ca`RcPUAe3|!$unxKgYdY49x zV3%>0CIyIQuDA=ZsmU4qEPZ0zZ=Zew1GM6-so1&X0H z?{KGmN800{09?OV>P6l)=2T(!))%9U!yIY8ct1aH#%L_`;!k~cA^WY)vv)_jd&{I?M z@uS!)U3cL=C@Y+jj!on{_kA6&5RcCVZ}t`%X*)4$dAG*k$jYql3)zMJ^AX+Lz{_*J zW_aiREd~_7Kt;VvxYzcYSh@Ld(ww`7gP+#(Ia|{0Vn;o;QTQG!VI${D{eI56Fp_t2 zp~tZgYXJErHGxuj*ea}Yw&BorYMv#fBz2g2q9}0DZOqeiholzi*|ftpEz^nd3!<;J z!yabf!>zBke5SZM3}%59j4Ow9sMom6P``?Y`)uUn7U~$rT?aKz?H4!SwH^KFz!v#H z7iYtl?_rY?trXtP$KnTl8%j>cWMqtn551!{A#V2SKuK~c5J^}*l#+ar@&>*}nbvdH$@=c5gEO_t3H}**tclJUW_v0%w9SuptdwVg$SZTNs;gsR9 ztNP!r=NH`5r0JcZ0GibE9JT7hMTC;#ec*%XuS^n=AGVTkMFg5+HDwUT{9&5u6qmxy zX8a*4S&ZuIla}i+Q$;~uphi0Y-USy8{6cO!9SiT|$^}(-+jt;toaDhyBKjltvs181 z6wCm)=6c&)a95s%!W*!#WQ3kj&I9@F3}K$xPw;Zp$aLJ2U4#Z$ zzU3M0;#2?!dY>XSurf{YH5@g3IMXcP4|&)Ec!H-Ct3mdXf_MjpLtSPIU3+1_u%ony z%nV%|*HM!($7il#WCERyvEa|En2W$eGcZHQkD1IXX%KQUCENg`d$&upZGkM|&qQ{? zavkDmn1waX)W9;zEjsmP4abVaC^4nyN;fsAkN zfr}1l0v8V+Ly1|YLy&$ z_a-Xnq&4-9?U>G)pS7GbTEwRLvkOBahz(glG`5>51{Ah7x+2s;1sToC_5{7ZlHG5sgK z&(n&lwYZsir6scGm9zS10aGo9-O7_9UIJOhM_ku8-Z%`~WsdDLTsAn_D|hGHNL{dt zZFn8}N!JPZt1ppFXFhK%ZJ1h@%FsoP*FEyz{T5e$Z}R?W%!^08qTgaHayTI;wvSt$ zUm7-Ims`M=Ysn{~%VP@%W;pQJ@s|7H!xnA89($Di<%aVksTb3h-&=B^ z>c*h34rp{%qJ}&tOd$HYcQ@TSwND-!>bol5)N^9r#K`yN<14Kn5%`;&iq8PW61i_88J{ABvX>yX}Smcz$mV60#^0D{tE{1-6fgI}N9WHA^16BW1V7b>pV z;R?I!H}Qd@vm5ZVzI27$b)oCoplYfhpJ)O7 zE;ZT*$vcLfje%){xvBdRAWC|KtXivWbPFH{d}chmisph4@%(m%2gi}>B+OGM8rV~j z+7Q4^r7M36h`bm2C{pVbKb`EXGaU|i%{(OlS)cf1@VK6E7 z1{MlGNzS1XtLa?=k`y0+876fBps|>ZfIbrJpeh)10hsyVw)Mp&%|~|-8c?;d_~E85U0I(2Dd`znAX_cyN=xxuw;aEc%==Kk zlU?rHTb$gjuU{R%IcdxYKUs+#+B1&-ubCEa;!tg>0%4>iB4fJb9wnUH&QvH+Jka>=ONn@MpP;Pn_=#)(HXK0@&SF~ z2PhBL6k+BIMcwaFV>P7XQXMf}fkc;k?$rIb%>Jsj5zA1|h5EN^x}@M4f$D~_N9AM5 z)eRTQLtJK*FjoS1h%`mXTJ4L?iS$18=6h}z`a`tROpEr+j{#_Y&ibKNRKxC++ECrZz@Y_aFAHwh1?`?Tc!@4)mMlK0HW@qMXrzgE8IGY`5&9 z3LMWX;}{W)w_F>Xpywu%!ZkoRo>Rk|yTANM8{e-O@(h-&EYeH|c2&JRko_jeqOG%` zGNm8`XgwlipFj|cN?OR$Y}ZMkJY;!>3~L+$lKD4=vC&A0hsLa^81@jQR%szAcNMlY|VtbBZ-fZQh`evw(p5?Pb`HZ>ry$HP;uxW>-&D5)#tp8ACanr-}W} z;mz}atT1zzqC-F{L~(6pz{zhS4jBcV&*aBc=ycQM zT21~LFcez7EmzgIUkTl_wU=|idH3#?`=P@|R;Zs^fdd#!3}XDU5`Wh%#U~#dZ!Env zwQdn>6qb+ZpuP?3FpAkP_V{|XYe&eDNo2zXVg93M^|2f5mU8f|$8j?r))$)x2PZ=f zc?KudGN1n-J#TqG&2KZ%x4L~b_9-Skmx0CMb_-;P#+_AAX7cFD%V<7Y{!|Wa&@n}2U`0V2zNl`&$@3q zgyzx*o1-)Wj-B5g-^ba5_PV^K?f)cY5bRe)89zpUOYM;TxZ5!PB#WuSfuMc5EB*%E z{3pHNRF$wxuEYP7#gf7=c;Xi;!>6emJAn*%-`a0?-~9yy07fFp8hoSyph)feT$Cm2 zyFSrzo`sbGShyKsOqsSCBY`Sy&ot2y!2(f0wcOAQS=z=3 zx*uRj3kLHX=tbdJbV95UtRXF@7C*FAO8qXh&{%m5Up^NZ z#>w-CUK4KnVC2UxZoO?ow7u&)=~}$ce_E%cfHg3&?p%t$n&(6xo{$(rKZ0MZn-w`o z91`cav?}icCKM|qZ%q}c1F4$tGgEVcHmHX;orA{hcfD}_il{!3d6$%#`(wmWsI6U~ zF|^(tkKLjWk~dmUxea;h%=Fp39KGyw_MY5$>_W}#JOUmOgmgs=*;Y$7Hiw*3*WJun zQ(Re0N%ue7BSQD3MdD;SYI>`8DSzeZd7fXuZxUdJ?i*(>gL4+H%rCQ?iqb1DP1N`7 z+6^;)FYav%a(8mxVXp{vIJD4A_qB0PbjCdluFqZbP)(K?ssbv!8^Dx=yM8whX$pl< zufO3L1mb#ksLZe*L2yOVP}{lk;IzuS3g7QqZszp*k7rN`#92L#0t1*Q+iq{3S+|46e=Ff$b0GJ9W2mc z0`$D<6)d_mD>+hIBYt*b6|h_haf|NK4QL6TK1!iVoKiN*z9W*eQ#D}Z>jC^pvs^;N zHG!Po$y|(0m)%5pYIQ~d$3UK6wZcENhS3l1CUr_fh0OSKGdU8v{ci38r6#13;W?Tp zAn;cE^ssZraDWD*(_8}GVuIL_K#2!(kh4(L$U~}>J<>phqinX>FcqNAVEHACqN@{R z*=}knRUy3{4$8X*b+P0HI~cPtS-~3DvPd>Hv($K?6+j$P0Gf(+g%ksn8j>L)wl<6h z!Ufbo*-qT8+8|v~ANKg@$O2&ufbNv`_xx7ReDbVL^;o2v z3$V38E08*&7=u)HaFhnvD4L@7y?OWx^XSR|`C2&;L0N8T1te&;J5H zdH)G=@AlA+;j7c#yh-EPUkYY7pkQ*AFYHUQlh@h1@mowSu}ZaNEQ}iT+Wub{qMZ7x zgejE?5H)@oXYp#SzVGJ{#rN^z=8xaIxtwM>X_XeJ>h@2+7LAq{I>y3 zAU=;#*PlG)HQQPgM*1;f@WuO^Ec@_iQhQOIKva|0(|318V@szgMG+d2y zh6Yzs77#zjg8!^HbGaAy!&Xzq{{mv10qVpazL{7!JM!}R<}C`^H+nX{{^HzRQ~UH+ z3ka%rorTS(TwFz`n$(GJOLddgLVPf?iJEj-Pztkb$rdVM1{7_Sb7j{snmvTPi}7fXZSoEv;wSmfpo>Mb#B@PAml9!kfH#LrqV8WP`*k&)z>8PJ-hY?4(-|j zd$-Y8BOEzevX^i!K7b6=g5DXSKF;~={ph80Yy|Z$pv2;>t^}YmO5KU8Pf3BD^fet) zJEfV42`BiAeWZ!LL%*V)VSZ%rcTWw2#~7v2_{c7+H#}Y03)I}<`AWn>O@8=^p;59tOc!wLptmqBp@4giE5JUQg=q=mVh3ak8x-c29%$)1iM>Rb%6T<(G`9g?b$<`O zWJ@R|!8J=p(fEz4=K(p5}qOoP>q3|y0j=PLT%-Kr^4=E#5y&TK@? zv30r>7i_Fh{x{!aW%!f;!=W5WnN97Yd?bslTEbn!vXb)GpAaYgO1 z+HAEK#XpJ4&KW#5OkwVmV~uX`DHS({o$z{U4Qj8T63to@H%{ykU88XJZFvkE-YUJi z6H4$d{la=W*!W`*=cj*KgsCS-G(5v!TCZc2r4icYMrz27E+qA8AfI`i)h521*mYeq zDG83+W?WYzv}*5O)Hj+CYRL{BhYG3&_G1V4Yi!NOqzDv?KA{K(;D}d6=qZiOodv_9 z!>>DQgoKI7es#J~!3#*SX-cYpoBLIj^rzzuCx;yOLssZ|qd#my9^BEWyAmoQ!(pH_G(kY|gb{pQMA)*ND0ZWEjv>7< z0t46;rz{Yt{-Re@s%@7?iy1$!97twXnM8W{B(5;55+kVtequguB0l%2nL80dht!6v zE&*nZOS>JIlrnUrW3!uJ<;+Eq;3JwF5=f|w=9M`1zGhzK&-u@ImpQU_S(+wV-+UdQ ze&{dc!Ig%=d*pFj>=0Et0t^Rwxh4d!N{ygZ9k14KM{c!$cAU8gdIo&vhnCGXoHFt2Us76kvJK7WWh63jUwlf53YH-G^UuId@vlDD?BjBK=vX zjSbP7e&Fx(A9ER><<#+-@Z?-z>rtoWIZ&E0kJorL(@Ks0-Zp;oxO`&FsXwi(8i9e% zqhf;_hs-xUI(Lt|{8#z(M)~2!4UaH0!84_6UHj8^0-CJs`Xrn;kY{x6Q_6|S*zg4> znQ0Y;tFeCpr2tDK3KRm%B8!OE-v!mv z|AV0V+Hafq=A88rX#BP-=B7C8AfBR8@9*|6sJZ{7i^xAMUj+vbijfnJp1!*N{ukuI zrl)_X!~4Z}&!v@r*lBl`J{3`&HPuYmWA%XREPDYRw$6GK;whWGI8c#!}_+v-&&QsKK4Ye#mW3Vf=@0#!hFKv|e2)TEPuy=Ji z%FVfvi{;(#@@!!?HOfnsWf>Szpdn6=Ze_nvn6eq@dVV2_wxqcUQ8ZD~bVB4J5*jGY zO5PZWBGqo36f2kD?K;F-#dNp%@ZC4=RcBB(`H$AyjvvFla1t$aQON1fOPZg<1-FNj zdJj}}9byAf8#8y=QfIwPknMXC7@;GO79fP{Rltnts-40@0=7I5CH)SZ^$8`?Kg2+p zupdXksE-I{2k+6!Pnm4r6SFvYCQR7PVu^GrO;xfLSP(O!||qz&yPjBuJw z7cM|%rp_P|t-SskTp_M^xtuh)L)asd8Z2S}tA(QJdv5SEfV^hap+tb!Q;flBx89M14{bf~u;Zi}sbCPgi_tNB0kiDPKr+UKDV|)5T9&yoWK)(WB zPhJWX^PjDjO~kv99>LUF6&=#E)fV#o;em29#vkjg(M`PG(9`wFM_EjvQJ<$`=mKT~c=;(pBvo^)|0}`D{-0wTUL440~)qC~B>^9&JzC z7xHM)?^H}zRJ_j`{8gMWFNTm&8FH4Ja^ggz6|!y|HFh|~(Eg=(SVm1R>yvWA?bC<8*YW){EkQ`hkcUkUG>FD4YL4?v=|40t9U zCNk{Rdpm_~+&vp2TyK#&OQWr=_uFY{H@xB=i(5Hy+rK-+#p|9pw@mGv;i}svYq8wT z-Fl^XTaLl2c?7hBR~l4#yA)6p%*UY&4v-L4A4K~Xj?}cqqKZj#Rku=Yd;slvv|b~R8Y;^XUCwY_|!UP z`_fxLCkYK5$||KiY)i5(6&(qhVLWPA8xV#gol*T+1r-q_oHHMEu@nR8WrKA!#(BE1 z;wAd(8T_3>>iJ-Eh9E=}Khw;Sxd`GrZa5Q`D~L8wb286n$sclkEuk5Q_EQBQ~n5E@*otx=>CA zCFkr?=xzWI)`JSDKj^@?ugc(heS$DL9hUMZ1jMz zuSTwIA3=Vj0DMI7d*2BgI;~#rcLWwV9Qpk$WF#fg$2^30@a$Bv{IK)ApWb3Hl6vwJxzEN*$5rdsm1ZV9Zbr#g0#D2(&+5DwEEoq-RrWERkZlmX!xhv00is+=Oo7D*&&R;5;LJfvmES zMRINqqYEVrV!|qO6qSJ~+PnXmqWx)BuN+?Evo6X*T)TkFy+Y8{qqFSoWo zF~hu$G>nHmJoS%NB6Ps}kEeOKu^1INtE2%i<^;FiEB;~hx09SbI2%#c Sa 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

    6LC<2+e@7x}I$j#A*xslxaO(-aznf1f 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 z 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

    ?H=avI|_gVOBoi<;};#9?R$qRjuheWb&^y zf{+DRB^F$*h=P0W!$HvSYaI$i<#A${iB)4G{gKc0aC_M;o2fXB&}XwutiEQrv+ct% z=Qx1BFyJ_+X_8|;i<5+f@44y&sAyrFRut9ZP{XH!6mUc$odQLiEM$~)i8Rkz;j=2& zYf|`Fh!SDSBIXw6Q-|d39FDg0vI;E(w?W+aK$BuFq43?C0$X)Pqa{M4&IXPPw+k4v zaHyxM>#e6%@~YKkNcDeGtc2 zUpuOH$LiT&XvFuoY zi?f-5iuR5U*px~EJbg*BG9_|!oo0uMH{teHhKNg8oe7Yb^&d4+{V!yYkIGtAUVt3h zqpPhh#*uuiL<5d$bKtLwu%TK=mEOwuj1&x?<0cxC=oDgJ3VW0w z%8d6Eil*sY*tAr51kwS~ZW5Zo`^0+IA5uzuuEhda5D7s5{JCHP9lk8b3zu_n1ygfDH-!MSOnH+%HEBTOQty(WQu z+r>$_U5Ajxea4DKxL*Bfpoqe`BhW38Uh)I^a@NJ#vb4%+8Nb zD!+KMV~YmaCAht0Ar%N+ho{e8Q3va1c&}Vl@;X)zhR5)I*$FpbxHu}qo}Y`UM>b}g z$mHmj+<7_kRM|rD3unG+>P5_Gr#LcbxW#D8ShAxbb)vhk?e*`n`9BBtJE4fat&T@h zM!THH{=#xa`7bQxG`BPB(u7~17q}C_$%ai zMD%}weI1-cvolAFz=q?``dsr>ZszYq9PSSf{d@p@8*E_BRp?qb>i^?8QeYcmUnL;kq6OMNn3--%;Z?*9JuB0;3e5fy-_Y4c4z314&$p3s^DeoIR9%G&t*I@OG-x5=PdMo~OP; zp;33yYkm&|6Tvp->8`Uk8Av|DNgH0iO*Gw!>y!1!LsX(odc;0a39fT@Aab1na+R= zu)ZG{B1_CS0=jaHsO658%xz^BZ5}Y0C01DjDHIt|*-Xlygx8BC&y&)K`WUji>kyuFmf=xqDr zE8$m1*&&a-#R;(LWYhpQvE!`vH6*&A#!;7i6#Bd!b2B_ z4GmKYLo`l+!mDA4&EhiAWLt{}-*vLqMgwZ1L%$O=BP^OJ(ItKs8Io%UgDs@v>?-%W}3^A!d*EW&Y}+?`0yjvJS2WD zc=qrXrea|-gEV~!P%NJ3E4N-}EBa+3dCXdZNi0^=beSAW-m%y9R$zkrOR^SGSJQ+j z?wMgck!au2=avaf$3(Z#LC}Ow;fTVgt`V>DOfmk;0L#*na@9m4F7o>2q+ zxb*_ld3m@)wttJnK~#!ov z=B?WkEqD=%VirY7)!$-trmP&?w0Pya*zJM#xTHidDkCE|NTBi|Y_2+>QU^B8iE6%E zQ;G9r3NI<1PG;7WQ~?J()U=}&<^XXL#IXwq+`%alE;5h)i;Pqy&dhj{nJ_wgT3cEF zwFYHArQ8HEBJ7p_u!PeC*4^xK&NH*}@qE7|uTX^UsoD8@BDceEFn>}f7?!1Npl$$E zu`hk1T*k1{RfUc`T$dcpO=+PEN{!AD5P7cTJX}MS_Sq z=g$V;0v`SW;0piwc=q}Jym^d9=S}o6`!NNu$<}&hT%^meU>4V)0x4(AZWsR`B+$q# zpx+;LvesK*fcdGKzrE8q0649!w6{9T73t=12_{>z2Pg^`k!F-lV;*pjxXR}@b)u~a>#5@W3XzVoy8!-h`^O#Bx|S%{ zxB*~OfdZ+uzpV9JqPKaqzs64+Ta>YtFzI7|IErDPl#c7eZ%J5rSO9g1^iCBWioUC_ z&N>y;TsN-hyYRr~u5$YPRsTlmZFKyoOo+1Iz(EWL7M+qYT<(&a?xapyNM1>FGk_)A zG?B;5$(?kwA}1YPZ^f-rF zQcwS8Yc|0GxKXW&TU!KkpSk+#l<&B?oI`>p0K1E=wx--oE5zFye$SKzA2-l4xWyl2plRPTM?XiH@ z9j;!4jLBI>Mw1Gf&l>8z42rH!N6jv#Cqq+Q8nNfXw4+JQ5SI>WDjqB$S)}Idgb4Wy zi!}8DbbOQA%<&8-7jmC`tWFNindc)gTzJ-5gY1VkI!3;pzif+jV~uplHn$=aagF)t zj#-}$HpIrAdp4&e=4<|J>+vNf-uHb(2o&dV9eR-##3zNM;c&s z$s1%PvR|MU&HFr^;W*-Ri&h{%BV;gF?6lkms&)vspmi>dS%FrWa+1z6Ncmq|(-wj7 z%))Mdw^t!UV?KY3{%AqCZ*)f!xq8l{y7^&d_dV%BFB*Z$1=(*;od9*#qFGOXRNiz_ zhw}j#q0@-tNit-S1n5}yXsMko865XDvpD9#nO`@U+HrG>*cLytv9RAD-KjOE91GJq zc@jms_vviZ{L@T6oMHod??3&vTqf4JJM`k*;^n*hOt20PLnCoegG}nHcoksxe)MHA znL>XsnL^^v{$vWh{*GJQ9+n-5$Q+4h`h`}O0mJ+>szfB~G3#PhA6xu{n1tcHpF1vY zZf3F^j+03b1Bce;Hets{B(7&Mg&;T=8X25A+U@ef`;LU$Y`y;RH=vgX-RV6kq)h|EzCLQNV%gnoRW2av9TF;?#fW3f0zOCg1Vy-p(6b37@@v zNZJ&@+qz_E(=5xdU5>J1*OM1t=Z61e`epH(e%UYF5SBgs+Ti)5IpsT>K-DjX1bm*U zP{$#{bxA2}d{E*SJx+%#|70U(s$r@m0{|LpS$y7fbYicaV&?8@tFKAZXU2F4>wn@f z{vX%+_rspgSeT2{2P6S(CMCUl8(<<9VBNe3}ugQjD+2Dya~ zDA`JtHh-p4vuipJ&1COl{9IhfB_3KOSYx&w9Zk|$&Usl8$8Fr715RsFFUet)G06*X zL@M3dqA^kHZfh)wc+3m?;s*^o0J=3-t+c%Ku(F~ORD&@J&$-gkCU%Oo<*Tr55b;jp z<7wbv2}5=F_dpNm`1gE%5wdG>N?tQJmY6r>4q>g{0JD#tl0 zvsm%YH5^4@zXF=9PG(2iJlvR7w89nxR1-$RwC>uopGq$kCCeh<#9T&NdUG=`cG@Ey zU!4TfhT=q21y6f?athjuX&feJoi~_yJY6NzoUIwIuOCSBWdnI8c;a90UT{G(xy9TI z0fV62N?*l%8y5gq);d|PoMNWT=G-B&FW`%+QqDq!6el}oFE`bAE>v~oK<%(Uujn$5yrVY4OK15nN_YccrQtEX)vOyD5wB`NvQK}= zAG5~S+bF&Bcl@mKigdm^kpl1n$SLl(nh&5;j-u7x7$ZsZcDLu_$~7(1uahlVDfh!N zlEvZA_vh;HiYG1eA9LQ{u$mBNi{s`XiF zVGiJ7Hn0Ff!I~MX?k6y_vpvB-qgEsH5&NI%8RpMf!L$GBcORe*mRD8(GwWkMrTWi_ zh;+4Vw5q^K<8DmzrRUIr1Wm_LeUSs#xWVIy(~r)8f!S~;QvsgYf#_{SxaK(R3J)1F zT0h+p_{~M4Ktg#rR5gEH&|VkRvm+^fb94Z%*6#@8J$G!47j&KP+Sfui)YMwMpnyx*ahjEjPda+IKt}ZJZ8Ih%FJ6)XRdG%<8nXgP~(cM1iuj)0VfL`ANK@` ziKtU)Y^rvn+!m1fGzYM*9lL#AXZ^f{twVT@DYumfvA7Hk(I6np!rmQV<`89yWGc8f? z!u3?H$i1JSV=xC@S+*@RC{rCuM^p-870G9w&%UJP0b`k9whM~SZBfXcme8>ZG_{Qs zk1KSo%$yzeKZ_%LeXTPlmmJaGYab5B-!2w-5ppS$X3ytaJEEUs(~nD$^dWBipgBYM zpaE*Zbl!j+HYZw-7klsvH)S3-oQQw1~3WP(Mcy!;B(6J}ueXbLNe9afCj%>9u1nfL}54h2^oDii4 z$6m$~c;D`chXv`p^^ZL%pv?O-!{q7x2^k!evL&lVOySIhNHP%QQ{6>2E97Il9 z)H{33wn|q&BRj$uFp0?#SB+d}o#uSo`XhoD8B{F#Y&AX7T6OIdgE+#8r)ZYam?ji~ zOQq9?RPO=RZp(YWyC3{wuXkT<%5O42ot#=b|NA;~2kB3`wy4SVq1Vh7)L~|M<-e-Q z{>%F8Km0N4tY~sSTR`yhg-gux;-KbubcDhUK3ZI#R;Fc!;?YKCp*X|hxHgXXoxmlfHORi6L#p_va zH{n~=lu>4sBS`;8EKXer#q(d7+d!-m=%(;Ezw^}MLLotHJ+xE$FDwS4n`-1uT41%K6o$`aRzZNyWx_%d} zcel1v8{M@9MKPOl=l+b$f5z>9Z2#x1fyA`>n5(5ULMzv=<{(8m4f^JMi;C9M!?T6{ z6huCX?4!Gbk`LPi<$LZpk`o$G16F4!SH$0Ft!T*Ie{Z#I`{s$B2peZsPS|?q8@5xY z#1KQnhQRFp7xiCunEL0{WN$f}@+>nYVz0pZF#&U^LR1mJ6vHwBst=bmq5DjPj53QL z#YK4$-#55abp^t8u~J^z^h<#t)j%T|=Q| z(^u4LSTwu=Y{oCvyOah7((-*fM-EA_ys)@hgWz_Fcg6M6x}Z)c%?~G!Ct$OsQ0*EL z{rFmG-SyG|!#l4j4jm`10n{KLAaOh39vrJ#B+PnDn5{-@+WRDL$X3k_HafT@ijW-% zo9lVFo?V1TSseqe6JUl|DkkD4c1EP?Ec&oMh`9L%lyl06rVm*P$KQCy=ePI`pyD02 zMdHfeq7GOr<}2K*gNM*6*k4W_1LY`OgHhkCisq+n>UCd#_`H|sdOeDxk%_NV?_jJv zt>O0!(}b<7!D1(~k{GqtB60>wps8(l$-p1sVb$K0poMMB@kBS1x-g*@-6#5xqS9Mt zN661*64}1!f!n~(+_J`Ll`d`R$oYj;7Vab*I`RGZ35$ACVMc$R3kH9)p~F$1rS&r z!18R&&Kqpp82`Uh7|4o}`sqYtI(STR z%oZnS2t0wt=lT_h)OtE+IBt^K1=qvOc$lznXvUCqA~-c*jn;6+ zwZ9bfi*oAe<%8+!&gs>BNqhbW&~b;(yIHF;y1`3;CqLgX+n#BNW|@g%lPz0Dx&O2+Xg?BUSul1!wp%$fKwk!{o&FPGT5TN4|^F}P1O ztjG;A4y>6?MwN^sy|*3g2##4OqQqof%83;_PHrs=Fhqycu{?83Sm2|&v8SE*bkp6^ zVaie&Hb%CIRH_HNXGF`%Bs|QR^5DXUr)EtTo#r`=b_sWqINg+IU>-w$k$XUo;!m)I zj%TZvs*KP|Z%@VqvOZ8Sl-`Th5U9x|%@MP&u&s??b zAb$I^tJXtS0mO7h>9Z2Gfo=S8A2o1@+TB!+DU^18g*qygbON+=y;O}uTrikFj20_| z*hYDJe+CGUI#%Hw=y3;)^o;H-$o>tQ%1-)v-SZN7mxWEmO{F)0ye{OeV-y$1@Y-J1 z4Ij8(%vq@1jR#CcNJOmxVt>1p*fj{?w0Z)1be^?Mi3ITsar zAZe{CEJFlWb~Mv|>Ize59ckxVS=x&@!NlK>lWRF>(PHU2N$AhLrI~Sc%hl=LIogIh z*RRknq~jWR@DqwR!rdgQt@#phjA91;hx*4wU;{L_+ypwEyj8dSij?lxEf6I^c8xcDoo?Y45uaDJTF@m&xw1vot+3*vyMHEuBxcL6kt;jzZaH1+UB}DG z&2nEy>sJ7XhLfU#ep|0eO2$8WT)S~x5+*3!~Xdmv9U(8Xp zp!3rp27PUV5mX3zEaxdhV4clU^sB4cp~FlTWmOAO0dT!ViIXA*8I zQ-hYHoLh5E$N(9%dd#(*QybscY3e-N%HiGPj!txb$+Vrh#~jVp{%w&z&`YZRFv8t*f1RX<0nQ6!4Ka_n!rU`!}w`dIx^z)bWUu^^Mq^e&ZXEr}vVM z=0t&2=Oqg9S0XE07`zu_w*urOA{KsKpSQxSTU9+<9+1EK;R4ei;tj~)Qe=nHB&Z!VE+e_^4G{EjA*sTFxiN^ebuK=UF4u9_zV+XZoo z^>WNm`gaw{QEIlxv6vdF$VxeBxIGJF%V0#AVTF?qRIg^eP&xp9hWL+1@9`C>X zY4Pw2A{yAEk#*-x{3fWB|9vkZ@tW(S$4{7mmBadRx+lL=+Pp=#-RQw0@Yi0+WPY|Z zO0D`Ds>0o%a{c}I=NTsxumC*$@BJ43@p^yTS7K-OBItrny#-ecq3{z^;af>sxP&mI#^I{lP+4SWG0FXiMmByb#NXpkc39(580s|XQZMCOixH`ZrjMzS1 zfXY{~L{e6plmZ1W6Jrl-eGK1Kz_ymmipp>=&()n$sAoSSZFV7>RGtXgVQ)|uQ!}#s z>SzoNO*eZr`GV+IxC*8in^Lx&$v8H5Mnu{4ZfVNMglfrb^UxK5^BqOuy9c)Ac$>+K z3V7S&)BMJC!W3SKg)>^qv5rMxZ+6OV*Ox`|OYsxyK_oPr}hRi)~P!isYMfm)XXa<&cgv z70)X?5rfX%*l@s`>8)BQk>`$Ue6X3V!e?b#Y>_X>kUkTuZYc`aQUs>ss%;F1VNn56rKg8q-hbr@&T+6 zP|jXcU6~+Z^iOA0grx95v`zsnN*~(!2>;thnGZ5m~9(tU9Q62GuV9LLyMy&>81$2~xbl zWK1TTf82M!nxaQ$*8~O0#+)bCJ zUTSK$Y=E*v1Qz#4zY(JdLZxFTmd8bxI z7c}E)1I5u~m4x=xGnF;G8C0QVAbB{)c9fonxF*_&6M$I?X&qV2k$u&jaW2LZ*xXTJ z(ad!iHI@%n^`tgKIU>Wj{aNszG9N0Q)PW zq09nojQ7?mt}mZiXg(;g&ATnEO|1gFORdP;)u9z9U2RPz- zN~~l*a);V#jD~UR&0pcu4tIOJ$onGCH>l?h7?XbrHxqbP9L+i02_rmSoS7YKu@Ebf z-XgiL{Fnn5W@_4_v)3zghdb`PFW7OCEy;2o7K_WPKW<(VqvXbAT{MhlgR}Tyr+z5w zdjqf%THleHnbmFZ=8U{V%3C>Lo;Xtf1T=TB(Y_Mx+-w8cL2L#Qs~@hH#~Gnwv~BL` zC=gdJk6^9j*$f!Lvp*#<)!M!zvT#d&e$jy33n|o@?AfZUkmFamN z;Knn>?V#O(Ub=I@ml;Z!tbk1)etc8Qq2q0!nsXm0z%IfT!KMn`P-lE%<_144ZJ$cE zU;g}8BCoe_57lhl>F51_ruNQF{gyd-1G2xUSouVg{D0D|QR!dCqDrmy9J!*8aq1dO zXNG{VK(9V$#n z$Kk=9>Fzam{&$hklB_DGvDM#~j$y)b=j4!SIN2lccQlo ziYlbeBOR8OuRBql49NjSE0F<@4`T-pq}JSTo#(7>;3s7G3;D(60Vp>K83gO-Ua7bf zw`;#4ZDVJWl3EN;4qF8w(TEJOXj}y#yE2!cQaG{2f)rpJdqmwIrywlt{ePr62Ec54_BSUh#YAT z0C)2%H`Ru3KaM6vKv69fMTG}W2_tj;DnQ>1Bltomp`s6-Q&OX?!D3ZT)OyPxoK%P1 z2YZyv)rE1Z=W>FBkRQR7KgnFeWEYcV>`{&~j{5ElXohbJdi5|G5idSho523(-(y;q ztoXjIF&S}qrV)CQRTfUt9YZ)CAZDf10*+aGcvabiZr#(-soM;$$FnrPbT_*C#J+N@ z`)k5(-TH}N7z576X!Z_P=?4q4S>JU-fssD$)_ke10@7F{FNbe;`ReA|UfrKv72^|6n5Q_8? zic$q^Ac_)-AUzZ*p(Kz{L)VQ+=%EA%ihxK{5L7_yx+i;o=iD>*-gVcRb!P6J@64UW z3hzp?-sD{=Z=V12d&+4#uA3`=*!h9Q_ zhCYp;vL3@z%BFVevhfew)X)QYKdWXn9XgjJTpmn-)P7{!nzo%0kGHFp#UwY9)j_K2 zu*VBBpF(dKlr-md&T?3QV1-89%I)hx}(ocspyS&=033Do+*#TY!+%K9i{a)ol4mt zs#8_OM5EV;*q`xrBAu>+=EWR6kc_wi%tDbDdg@WWH-BNya*SsEM%6TmvOb8<9II1I zK<%8G-@FddxC@ElmXmjS-j!d6GG9ReJH%++5$o{UnsUl1Js{#pZ7i`Vx(@pBz0I6t zbl%XO_Ld;oZ;TCf5bx>5l_@L^q_d!U-kuOb$-*Bij;^E1zwONJxtvsT&hWfyKdE) zmr+~87r5kQ7R9KrQ`JTs`j03EAC&e3p|{g>-t3_fa&LR^{*ZGn zjtUHT(<^5%=3Q}^K$RXhmq-C-AbI!8#GP2rj3!VEvh#lM0$CQ>w4;I<>7y}aFwnCZo_zvhHZ$O^$KPn!e&;kETX~Xo*Pp&@3D>vQUTi9j(WluxjJh7&nwzN{^Wq&W zv*-k$A!*%Sf6iH~Xz5fQCBpP-BPF(rt`j`y$gah_OA(li!iE4Xa>N-see8&!%;Bq1 zct3yHd}v#gA+@tCv0>;n=1!*h;b2v8nJn8VFi`RRvB&B8Z@$N#7Q!X(2*y_IJH4F0 z!ivn&8}`If2@~`#tj(fPb>R=2`5cPzuCpglq0==*1^QzoCdfk? zj+`lHK)Ug0c(`=gbGtWo7_DzYiVeTgOLzyOxMP=KufJ0*=lo})vyMG2ux z%}LvOP0)qN;TQNO1)nmNH;*j@1x{*a?Zt&B%MqU|d!pToyf*s8<+sF2HEXipUShYu zDmTyfI#hxbawYV(xk5ENCA8!zkl_JKUcvnoa`c}7fm>VN^irM{dTGk+nSK~)D*! zUR~WrTm2F~AzJ`QKBvbR%*Ly!pl@3(vuj6y`@g`+ov{ zC2tJBhFiqN!J0Qt3>$A_Z?nnX=0(}8Ep*PyYH+s4uj4k2M%P;JgP>Bk>BfZ&SQ#CL ziHuvb(}82q|DJXJbIdx=6u@zCy{3c6oP`-h*1DLkD_-uxEhDu+q#W6um9ckcyjR*n zrpcO55q-@_nJKs}A}p^NkYj5^P?P|s_1v;-7 zX{QMim-4np=3N2J@yOCAI}=ZPAPj~u%Rd1S1(U$_IIMbDDMd*XXJ+h$akiXrA}!i= z@_tFgXC1h)ZB)k*?}TQ(u%1Sh_ePQGKyYP5V@+*Hot6mB-G!S;23*iUkgoC<3u_Rw zVbP|;E?7=M3R25L=#FUJIGxlv9W8VclL+C*38W}m0QJi`QbA6k48x2l@vI0Odt<$L zx)LMX>XWy-i0V|Q_oEPrD|fvrJ5LJQ=q*G6lv2lWD#5d-AQj>i-X@j@bMN=I-K8)Y z+~R&Yu{4)*F&1+&oUW26tNkb7n#9IHt;xdOHutuCpJJ@TJm?{hGS|D#7Yd;x`x~S? zjVXb&8~H5-%pT#lbaGLa9>>o2SO}gWkxuC6xOZ*>E_@?OBA^2d%IGvIRGoc3u8t)|?$msT9ewEuxN(XaT);KU zkZ)QdTNnuWr-;Q`fj5*jjM;5%(OynWVa78^FBkd93=9ArulEgA-u!%%;AP#7 zqrgf(w<+MHxjcq&TuJrA?cmaj3BB2Fo}M>M0#;a!NDIhIgP%iRjzCZFtwc}vHi}I! zz*mge-P$^G_-(!xx)pIx=)7Ie9nDx{_yJT%<_d~mor>a6Tx0ToR0*n*e+1Dmu&@<* zA(D@lMH=fs>{ysBhR4p8moO?-nJ;xC6y`l+I0^9KC!P4P2{w$vx}$3tJ5l8c=3x`A0Y z18A)k1`X2xOg$*9izPdkV_;j5+$zT3V!LhJ%qqa6G@3CGP^cf?Tp6WPfi|H3#0p%O` zX%bdUjbpt|wlR!Q;Ns*Ni$L#}Zy;{UQ`=seMyxiiO@*ROE}qv{m1|VtBUWnGS>_av z9N>jU=-!aLZ4nl9yRJ{H#M5fmCewwvDXUYwQ6q9T0cTh!i5qk|^jduJNOvJCm`G-m ztuqnWZi20;60WFGswM<2Qc&&HPYKuV8@afOuq`Zk1yuJZi!5b#iVb7lg~t;jt=oB2 z%1cEb)rHoC#Ox!}aO;cDPuNT1A;poQTRGg}W}$;|_-#=cN7+hif~TH41Ev<(%Ub8@ z+zN~46bpXGNo%_B>O#2Dwc9ngU)Z8%dA>HB>h?Wk6({%v?D3Fb7GBCgjveQCHs`=> zcN*Qc_1Z#VVp_#HCQse<&@FP?*SHa(y70C?9B;r|u#l1JgQ8P{R!F?Uau5Eh!xRsw zjK_I?r8G&Z`P%;h~LT5`_q(Al2l^(T77 zETcB9ylT}u=vw8bva4zcjFB=WM*(tty}mbd#DEg7(^f9_=A4wCp)0w}qLgR512ft0 ztA$>uNZ4sRC!X&eZ8=_e>h2>JlA5$7AkTxJ=+muQyY4K9RF~QJ>*^+G%CdCGrpbxX zQWL6xchDAZ%PUE`x;yx4vh^S6JZ-x`a4qJpr$X~cZ`FwDol|bWLkrWd!SsRG{Kntm zCC_a(j6mv!8uU`%OPO@9>4k8Mn}-}{1q{>aCIqKNgWu>6N%0rNk~u%HF;mOeK>Wv4bM zp%eTWGPEb=^Cdn`W91y{MS4g0JSx4ej@)SRBA99W*4rCR#r)}TAZ=#>-_tQxA*QIb z2fybOMxy?k3Dxn|Z&GagGdbRN3(#t`4-o0V^{E;nr-@c-&ito-5 z&T*CAbt=RsA@SA}X1rKa0-C(lgZn zu9m7NTln42hR8+(8uNoEXx6O~1>d^WX_A{Q+?CTg^eoQf#9^cWuSXyS~I{sOwv zV~rP?$445djN17@Y6${!rj)F@BDcosG-wZLH1{o@s`6Q_?vFo>5~}WGWi54wB{&n> zflRtBr$4?aARC)1l6Q+P1uEY>F^suOe4)I+7Wtm(65DTmO3(*PP znX?Zd14OY3WpsiOfTarBE97DTUdOP@8&wqXM#jhmD(b>QaLQSX&0llPL~zx-mJK%m zN}sZ2iCqCRD1_}g?Nk!T0DB9ozT$X*wXJhh-KsG!*o6}oRmHAM-AA)D7styQ zF#{0p6Nj$x=0U0G6LQ#Pij@rKUF8F$wAPzj5~z-4)UzE~7Hr#L^U#aalnWQ=^`Ag+ z4JJ>PHt=0YIkm4bJvqzhTzT&G`3HnJeaEa!U2e6c_&a_Z+xUsJ(3j7g>}RULzarIZ zL?ZH$N-c^q16F2-*6D$!tG~i&pX_5)N|x01lNUN+!JnK9-#NyxHL!XJf?}PnHopja zAQ9{M3d8aH1H?0l+hs!Y2DY5KmFke4ibOocBe`m$70 z><$GPZs0c(3?B^_<&|EVkO=Ns%BZ3Djva{IOV4|DhP3lUfx6X^@cho^a{I$x3OJ~x zU@{FoHL%=rgb2UnBUg95ew14FnLgdR3^O9lfOPca8TakpUs(n1D`~;2nE+p!+TwAa z6Eq4UeA3TbcpQy@*gS#07lnH5Jh<+II?h3&%rN&vQTC@<rlB=%VSy zv5Gcybxc(n0^h{9#_gO3>Y?<h z?`z2HjC}?TLztZpqa};0Kzc)FUbd!anDo3^>%`*8eR#=zo6gu3&q!s!Sv8MvITLDS zH~Mxp=IfagH?X-nI=yLZQ|M)8bB@Y|_x9$c3Bv0;O`xBj>b*w_^=X3sZB2ny8(r%e z(d7v4vUT)Y_w_1bl7^h!Gqmpmg?c!}1I_ihPAHtnh9u;}82|P1D72PnPWOZkHrmi;N2Xcrn3i z^GRYqJ!=60wFtW8rC-S%ALH@hoJh0ED!4z67f$$MgGE)!PZ<ix2b`7jF?7ms3basSz{i9u8EP02(!Y3ck)cYj8I+)1i1q!Q$AM{ViuJ`Kys$J zMr@BYy`#jm6EB1zurO#9o@fN)D>bo`eA=s=P3 zc51izwqtiPwR`btWJi^gbX#9}m2l z8#1KwIb+flet+D)HS$=*-%qQ^wN5Yw9q3}E&q}8QFD&dl@+_sF((V4E1m0i!aU`c# z@Kp0Gee=38DrrS;QSQ0nViOo}`AWZ6OwPrU&n@Qor7ove#{LDj`kvu2Z(2;~)pMPD zx>}?4cMrBT#?D)9-PqHANAi|>rkEd7Dp3&qYUS45*wcuXaP`rxvwdr%bz04C2lcLn zoC%%AQ%LkKQHT(Hu?m$Gd+SDMX{gTIm&@kbVtcxZRH;nF+u-+GDStn z9q&R>evnMhjZ;^`F)^O>qMizgjU1U<$u#8bKPc+m$!U}XpDvLMJ6^kfXQwT4OU|`J zFOVENwx2`hw)nZD(0a~u(#$iTOByn<*UhS`;vQj_R~oIoGlIhYp+u22atLctI7BbX zRzfqP&VQwZ%3iOcd0M3PH80OR?!q{5m+E>gWb`)8RjL?WsIA5gKMi&A+kX*kHP}2^ z+J@=<_btrh z^TN3}E1`7rXWaI6YCSvoU@kitEZ<(Ca6||eoHcjlztU?l43&def+d+jHpBFl*H{(C zLQkkch;PpKH6R5d4ZzmljkNnZY!{CgB$O`gx>R zDk#m1R(4r1g%cq3G|xRulf?wfQEFlt%ycHxNb$HW?}GDTt8 zpU;J?pd9ubAq1ndY_WC;^z~BeUnhhFCi^;Y0h^7Pt-V4DlikgN-ukw&Dz;;&JroGt znm0C$&T|;*6{T}~1qatTILd=FnMd;NXCN9T+gr4_ZfWr}i`(0SVT({7tL?2;3f~$# zj~4xw*ADpKQF;f)kD|XNVNWa82@T%#!dxziB{L}sy>?C)`GPym%ubie>A0Ss+_CTc z)Wnl=9x%&f2HVLfuq*}I6WOXb`x{r`zGYLo5LqK}#)^~@lENNpyHN^ld@){qFQr$g z(yzf+m8I^-3OrigR`BNnUvb5z^>RF*5lFx9vl5||S`tliJLgVOtles34zFj8pkB%1UvMH~1H~ZSe+t(D~No+sE&)EB+BmK}bNp=qzd6U!GzKcJ@ zJntvLJoSjBfiQ*^v5!8D!qC^jEgpGY#@T1}fXoVE>CB*kjI~D*SIs%&ab4-}Oz_I= z9ObTWJ#fg?K3cSsIIWZ(OI_uW;tJEvci`HLd0qQIdCRy|C zR-Qd2KvsT5@6?90;GeU3-_9w#Z*7Jis#(~^iJz`bF24vYZ-;7oK7l&zh8r++z0J4OZi)`0HA!N=%8RNDofH-3(SXt_x^Unu*z(dTLP zP32K%+79p!#(kR!jShWWXS&ZtmfjzwDN*)eD6K*l^qaukiN!@xZ+B|*l5%w5DVM+) zRv_cUnwrXBz@mhWscqckTGbDiGbc7-&q=C!0kAg#HS+V)(eN!+dMKY34Zn8tpjAcB z71BD)3h@)hzkKqU9128oJ zOLpZS#<>aj)^ug@kR@4c&nz}y>mCV5CSBzi?f2RtZMTbO6oM=4LuQh(k(5gnEb|oK zl_B^zkm5ArZ`g|}Qs5>d(27@?So+j-pvUK-L~H0yz7{#(c1-AXgMKXfUBEm-XxGV6 zM8dyeSyZq@jSD>lYtB$v2!aP-1L7^EwizE>Do~=|* z_HD80t&5NrgKc}~qh}IJ*I{U2wt?&sM_;u2$C>T&;F`ya1L;f)$(iEcfueLbk$*0c@k7j zOPj)h@Qkj-w+?%5ish~kfI-5Qc}%cxi9+vH@`tXF-uwv=b+|4~U-@178#of*T6g;Q z@MiJ$4(Q=Hw9C_^3q1@H2fuf>{x(GT z{lgG}C{9Vz;(oOf#k-AoH&L~?8Nu#adn+;K;z*ykkRbw4Wct`y1cfRw+R$7ij7@yCZ7w=d^6%B}6y75i!f%Q<`^PyI;kh ztf0PYd2D#=-JJ%54|wxhu&Vi5uPkST1^z{o?1g}q*UZWrp4 z*CLLyB6aO*Xpt zFZ^4GMnQ%2{mPSQMMVi(AippC?QE7)cr0wS1Kl;ju_zvq1>d-dEDDgxI?PGL3oy97 z_gbfUZoE@{-z+ z@qG5w8-lzL;{M+Y%5vpSI_YPj0}f7+{QJKU6&9R}QI`T!!>sEC9X6P(vlqmSUpj={ z&v9RggSbo= zYvb<#xm3iCjwhHi-7u6*b^*H&c>R0X@fir9)_Qp`0CXdaYEqLa|!^b8|kq>lEe=;TQTBrPA`w$UUPy1F?od> z@|PeXN&Y-xWlly~Hc(0BD*4=EJGi15rsp2y6qXgwD92)i3N&~Q5AS7^!glm9r`rR< z)nz59U_6Qw_r#6~eC6kBVMVF>BT}}S( zY4X_Klq!{-pGvK+mTfpT(;27MjgPW4VJ#sI%@sJY>L1fd5YWCK#iIKPMI-Z^3G7t9 zCjMvVbFcRDc|e%1OmWzQR46Et4Tj#P#-5_*I0;@^q*7aXTiY_ z?Ud1%Z8F7%JKGQKlTf|aIqq)Pwc%AoTT#%R@#c|&o%+;qh;9Z)*DNnIve{jxGnNF>4OmvH_>88EEvrs0h$n>02W-&_ zpRSBF-67lH{85kDem((p9!67JHmVC_k=Me@+X}O+rM~sKw$f8&2!Mq(FSHCjFvy#bhHTfhLffrqE-rlzLv2IJ9Qps9{b^ z4e+by(s0&%kB^?Ph1cc!_m;JmwVi6c1{ZaeCMaVW!o}X-vxnDt@w2cE@Eo~8nScx1JjhNyZ7j)7tOePXZ0Qbk5gaOr~PF>DgVmL%Z zpSydIa`EVU!=C_&=j2uzep%v1f!}GeHJ#D{NZoT$Q_>%C&wvQb?+O~BQxaEY>;j8qV@_GI3SGdDVxBIP;{~%l~ zl7-rS$;HfYKR@A6dDw9%Ib~wrAM&8>ZS)D@rG4=92Y~f<@9)!_e*y^JA6wY>r^8$d z?CG3Hm)3t|kSs1229A_Ki;DQBt@Zp0G1soX?;_uHIP>wx*+b&Q-?hnHSo(eKP1df6 zprI($BhDX8QP;YiV0rBQs7#zBZ9vsh zaz0s>S_iilq9dqn?f=LT{_lPM`?Vn#Op+HHJDgA9YDHS~podABvyjlC#ZpnSXZA0PcrwLLqt*{mT+(Km|oxX9^b|S1r>UTpJnlk+PLA zjoZGiFpgfCRM%~M&!+UpRC=zIYkG^NzDsXk&}8BJa^Yb(mq^ohWk%K&*KAK{)Y#aI(&sZ2z%OossoBY3O=`#H z?264zAC-X{dTN(^CE$1Fol~TooCmG2`YSvdf!j=Kz$S)PfiFyfDbB)8OET=z!DU(wbb%DfLjw-;frg zWnYsb!OfB@W(IrCK*zN9=jWmszzjW9v2g9Dm`)kOuEy3*uYHJfps5aGnve_^#~eq- zx~NVuA?{m5TDcjxM-lap4zLMNYku#!#ju1cAPOCz5NkHuyG3SC@sEPaE^6Rtn>$Yp z=Nk%#ZYzUj@;ZHAKCrP`Fd9v{ch;s?e&|NYR`%lkiDI6sVtJTNPfe5Tm4)xY?T21f zD@l-Py%Ke|cMqIyOwRRo3rl`Az|DY1_Y`{pEspwp=oky=ptz7LT{WGC2vLxIIHLLRmL+cE9;Sx-Y&du{&j_6$tJ@+NtL$- z2f~~sr8YU?sf#~B5ihp8v+_SR+2s#k{;6@Ln_hHv4^ka;IWnTBkO6`tI9|OR<2;+{ zVJ_MiUoWfqS;(_PRqEV@ChASe#47hOf!7@`F~=|bZ~TZxBE2lxnPQY$&pIIY_%FT8 z;LGB7;1kvZ?ufBw+~HTXbfi6IvZ2ZLI3F`P%|V}MbL61B@z-29)4d3$LGci7uC}{A z(Kd|TsHQi<8zuAlaahgIv&tfY@N=Dy^`b;(8NUTWph6od)}poa+5tuarObgxrF5~` zoWJAzv`TYjh{+KL@pI-4eu11$jEr;dCMNr*#+X}>Gpze!eDV_RRpObUh*l=Z^sB_M z2-lM0x*)k&2Uc;lOeKmLM{6 z3@pDM)NzQ|hM0zsgGX{{QK0_vdE})spu$Slf|j|-ZF{e-tSuJ-LkzX+ULAB2G4~Pl z-HKXv%9x|`G-HZ_#Tg$EOJ-%EA1^8|KZ_b9Q#wR&>b<@=S@aS!S=cRhFV@xA5?M0o z;$IxRH(`ygZ)`z_tE0wq9;g=x>WL|d;1GuJKLLu%xzBPIUjQDX?`nch)8DP_c{>lt zADZASG{5K1Rh4UwnNgt5^IWt7&Q=60#1*0~J?QKhfNqdjJJE|E#7?@z^Qm&1$)+fa z-pA)pTnV$3S1??;djWM&H_^cpUS8zVhwbiHdWKo`ka>TVw&s<$iB9*1*a`y>Kq8^z zEF{9Jsi|IvW1`klP-Vqa9(M zbJ&|?mi6xGHk>k$s4NYUS>YiqT4tp%xgd8Bn^!oxFZ3V)&R_%Jg0IWZA!Zx@p-+vH z;W^N0XiK!-C~~AHw*!G4$JBfbq(TuYL+q%93?rMyyaPy>1Owa;Yuzoh4y{j0p{Ac& z6wcF!cs#%~l7h9#207%sGYA~?kzP}#(undT&oKr|krf!w{&qpdVNL3BBbvLm4oYct z>5b%6xr-d>FV06a^t_`4PuODA72HDY8X)BBb}8i7{jv0^KI-D(Xp4Ebu^?vSrmE5z z;jYo2zZ?F7wWog!^W8i$jG+rSUH|z7od1nFIeGL(DDV5NpbuGx-w^aZ5(h=&i^~qL z?gq~NT}M(Z^u^%9MXySEmgUW4-gUasM<4x{(dV+d@HUtIuh$dwgqNgHd8kybl@z+4 zr^(P)B~|8u5APfA&NgaJ)|*Pv3lF@}?mNbZ`hAsqv5qZ}_QeI;5{1OTefAMLaHcmk zx7Q2rX-NG(x^heX(Zpl1#2&}kqBwcM`WpQ5o_2TI7cJ$>?1!_nSF*2oaTQHP}sid zx96;FE_Z~_L$U=w(EE}5M=qAoQ?)lPcg}T=NA<{lkoxe6e*I8G#EmsB8Qi3-W`|L# z>B&i{`JWd~`F}Hs{{Q4yn3jYMF{RSrEeF(4j>@>zojgA8&eys~xm~zd)?z)(;%TB5 zg6chv8Qz4%=-nQr-*#EZGt2~{%GrB2h&0GbFj85+M{u5J!gn!PAuFxK!yl!&%pAfO zd)M@-ZGC~%pM$iB+4X$noMK5Ye(8KJFEC7WYV3M|Nb|cgS%qliLj|z{1q)Vij_Lck z8cIO__2+%V;NH8fHth;XargWRXT7o_) zWM=0*?hwQ@87czXT}$@vtY_`?g>g%q46`aUl6ol-E1I`sJ*Z;?>$FYfvH)iikp*QP zbn7)fQhxjbkCKR{??!$kRMYB07~RsNV%@n=UuHb^8FNWB&!KKL)p+baSze`lArJz9 zGK-^Bd$8&YDI|%^v(A`8dGQV>&;97*$}sB739I+1GPz>3rO@+OL~d%ZT_Z7O=8M&r zpJ$U_k$PbOGmNoEx(+>PzTxBhX2g|d``B?IYU`juxDRzz>e;z9>`2ZQ`SU0jsqS*f zHdi-fdiQD!7snn!wB8w0R)Bb@^@nVoS4U3{)3H)hWKh90DBq(ks(^tId5dYn$i#Eb zp52*bOpKazh)9>B{p9oI0Ce(We~=8X^qqiPiE{{@Xk)&1jSr?)X%y>n%oU+Qrvc}# zN6YzOzzWO-SD(T)FLLZquY&|=S%xVqF}%d-xP(@*CY98^!raBJUng-fU>Q@cx;p6L zWFb(~ZXols12Y$BxU5@`#>6_pkAX!FmLG>a4f5Y?~fYW@r7L z9;Aj}lDuK1I0aHx59ss8vxI9OWr)$T3aJNMfSvyErZm}HeMIC)#hcbh5|fHm9k}mS*@1igYIV7= zBzFRW%2v&%!LD99)_j!gR>&8#*z^ zinlXhLc6cWH8vho|U~4J~eP-Jrr@)Xh^kpYaKx^ zPA)MTJrgS7Lf8@vR2l*GdwMPFqTAdbdIG`q*BAU%h5N4T#aW4~6ue?gNENcCxWO}KQ>mSzy^ zG&?=5%p}(81aax^>tg-D(HSLsxhK|X1oE)(Gz-Qjx0+t!V%Gbp*WZ8!|rX z5hs4Ht*70kyQGm0?CK4`J9Fc@CBGA=l||dNFzD9_g7kQ5E?r_+ZRzlh;iuu+9#_TU zj-w1Q`1d^mmPazSy(#!s?s{l*UJr-NmW^TIvIt*p)Fmdu`28J}Q8Rg~(Rdkfp!GFkz<7r8yM2q42{yIcDaQ z22X*8Ug#DMVFH`4EXg(WSm;Fds6A{*aMx|4OOfR9LxI(1ZnmwUCa_=>mC>pKa|Zxz zpi7XfV&%*XC%zgdv$_AdR!9ycWrK^2quk4*GCXRw}g^GQAANw`wlD z?jmyAAZkk4=+X7W_!sW!*38c3Z@>nv3iwH7vRJLqiB!q@VyuF)d@+XT(^g@b?HuZL zUJ?~<`$EjSKRbkV26)RcO*0L>Re=Zr$I*yW<(N)Fnq5cAz63mt2PCciVtOuSF`e04 z^Dc7w&Vf*e1LFyCIXXhs=k%`VP6y-`ADPRtL!Ed(fG+68&**Pr-Zn_ut?d}KwQx42 z!#}_y)pkm|xfaX3`6w|WxOd(aIq7-iFW21afK{@YJt5s$Zy?;2HJrXnW`wX>E&$Pj z;KQ!V(pqXQsn${I?n|Te70@%5gf;sZkdZeh9*1VC$wsN<32tFpql$t>Afw|rjVMyW z$YDuN*i@5bczX#xKUd3KpvVN1z5{nSnVK!P)Ml?QF@q}%IIW@hvA*XQZbFOFg(AF(aA-UL|r2w!ZlL?CBSVh7V9+Ub!YeCP`6sZujkDr zPFo1P_4bIeP_6$YAk(ARO)Ahi`bJh3E#hHPw!=m)_$~$1K#-BwD`U(o8GncyB%a>r zow-Z+TwesnDS59gA}??XWcpYMh_fAyz)_+O5m`^)My$oA&) z7e|?@8cZbW&f*ts-^ESc5xgIm%*ryFztLY2dXkb#F z%X)z5T-twszFm;%`$flT5-?3ENGfd>ePnyXV<}@P@%Pck@DN8DQj?o74!gBodU1BC z)>fjfR!=cKJYe=M@#g&S#Fg&ofM1<-Y`NUscYF7$38?3@;e(-mKbY2w@}snztp#U` zQ0TwrtNnXh#F@4l_Gey5qWs?2#mxV6XEXd(eW}Da$A;a{vBVjAIdj|CzxdMed*4B8 zOojl}U!x&(UU!KX7glfSe>(8pQ`W2>XOj=3b^pS=Lg5O{Q)0@{f9yncW1#+Lkvspd za-7#dV_LAJT3}B1P5N4d6ipjwI%6fB>~Ys6DGC1}y7Gj##2ODJF|HS-moYZs6pEY@ zf^p)Oa$4R8AK-U))o8fYekP}v52;*>wXQjs2}Fz@5i>4Y;C2x^BrXbk;XYJt1+*5< zRuR!U3*p{!XXzw#HOJfh8sT0&qZq zPXbp|&cW>8qQs>o?mSX){Y;>!4WH83D2=K^7E(4SiLW>jQu&xF!qLL#;rDpE`pXU9 z%V{^}PKosk+v(!nG;~?fyV$1^zqSJNDZ!m;l_WxZu6>BXF+Em7hKXPYXFS7f$qsHl z8&u9Dt9N&yp`zLg_DVjBetw1D z(cEQMh0rQZ_YPd@x=WYKi=H~lbV|XUlo_1C4gMmCa)++Al<{)J-TT=+5YlCO09efA zQ!J~rI_7)ZE0JYp$FTaz(82SHz?IP&Ha5PNNu;7>>V~LuU#U5?lk1=evCj)bLFM~dBL0!I6h5nc4)Xfu)eQ+^5P0Eo>hi1nQ-Q^7GpC>riU z@N_5ur00XPlCTs2J(<#ku6$*PwNoet&(l&k?C3ssPHTG7kn}rP>d}G~!STT=we`M8 z;B14?eh7qL7wm32@k$v&3D=Rb9K^g0zPu{vb892~gZw27R<>YC9>Eo!GEk7zfO!j1 z+0=Y8?`8kae&?YUji4dM&X*ek)*}1?FDGYW9>MzL9P!o4hqlP9W2w2;DI&MwPx zXfMe&ta_66k|;+Zn1eS9ahbMoZL0duSoEo2ueQ#7)^b-{A{J$WjGS%=iQ(}Q^w?1f zQoNiTAm6sYgKJV{)96{kTGWv4)_)NlBk~R*ed52qaQoWy@qhoaI*^>dQEJ~?(9XyLP`yXFeVL(mF@~j>q;;FWo8%i zX+F7X%jr)~kf29;W=!v(vd^{_+8Jcl;R>?ThTiMdNju&{RXjbwEo62lCT^;^A?=c5 zgy!$<(XsI9-_nVedcMbz{%=Hefi=Y@?uYI5q3zs=ot5&1`6=+b2QmSjX|vRE$m5}( zSh0Qqi_5wm-EJOrUyJvHoX(I&BMd&0K*;HN6~Gm4Gc&7>u;Q6S^y?X;lcg{soW&Ee zjkI|8G$ON{-+LspvDmqX7J*IRxC*NTkvjK&?alMUU_eKR)UHzll3sKeiq#BRRzgQ9 z*A+a%V9j{+7lm}EFDM&+df)kAyRv|SPj>B4=JT37WAv67>lV5u%wQQ_-Fr{A&hM3H zIen%qDluO%o!(RIJQN6oQJFp!@tpD{X?7{}%eI2s%E#;#6$4eNZVaaoz~-Mk z;TDRU1bG?jEw9B}?cj8xITM;nO67G%l*sb!h6G1@9KR@8?)Rw8_CN4i{O0`=(WVC6 zCmcOPub^jA$*%LVTpN9YVm0Wu*oQ&&@SGJjIkJ=M!wJ6l=Cc0gH!NKhsqLIixmq4E zUR%`ZseP1u*VO|rh-|DjqvQFF&i(3fAX3HBOA%S5has=HxNaH5xV`a^U9c zB;>3lw5T0tiqJBA5;nu~_0T0(%Mr$kq-j9nX-fklY=%3aP&^!eT1TO3*3@DR>&HS(CsNm+;*)qw|GaO`P zv94S|!EOL!eac;%oDGvS8zI#x33SvC3#Xp)jRuU}qNoY#{T-Mo3}ji<1=0yZWw4x|u_v-~&y7e-txjUAuC`aomZtQ5~3MXA)$*FuV-=l~g zdQ8=kNcCp!zn3MJSHn}^i-&ebQ$n@~AuGKS!F_7Z+AQRlVpTH5JP>J)s$1)hd(I0i z3>Q_^@@B^DGvXah_rsZA00U5hc^I09jyiYnt6`pS9$&rDp37P|f`Mqs2437La2HvW zuvZoBCdxIR1Z8-=$CfzL;*&M5Unw#^1PuT__RA%esOgDr-$e6T43NjM}og<9EmM~I$a&ojnY z6^rpW%YsR1t9fIHH6Kp%avPri6CkwuFX2tA^j_NG>yBQDsQ<(4kDUE2XJL71z$)L_ z0=QpU_3y#6|HSt*#7Np~$S~=Y)OOs;4qHYY7O`G4k$M~ZfYz!+2tY>5xNgjt{v}~R zJ&Ag;d4bpLZ`+vm)kIE((TjsW#g5#G3Ms$eTl##RIa?U|tO+M&f98Tm@=f?F+cUI> zdJXCO$zz!)FIqQoqInXOW3HYhG$YB4rCPq_<{*?B(rsN@Z~_vPF8i zh7QDrzH;=9s6aZvJOA%4PX5s}t!YTN`JbWehdZE4Ev)JFgB5c-$n@gJtKM8!Iq5N= zbZPQnTxfYM4k2mM;=S;xhs1U~WXDQpyJvs6apJ=wylV=t_s&;e_!IDtu4(<+C)wBD zPNUG*TgRF9^K812K}iJn|7E5G|L)8B-#BgffAet+;(PSoxu_1)P%H|Bk*x4(-6T$f zaa@2GCPN0*%b1!qS8XjUa}T1L`}+eBteO(*(#q8{%MSV5ke#cxXpdTJNn9Rw`aJFh%T|nsT0wCqrYx?lU^N5C{7=47@$mqG)pe1xw z=3zRU1a|~#ZoPbc6D=~Y;O)fwx*^+6N2F2U z1ux24ZHOxs2B#QAc5~^G;}XOey%>&DA0@w*t#p66rO*|5?nK^sy9375ahdm5;Zk^T zALXpQfF(XW|KsmkS%%L$Z`S%fmB087g`L-_rV4;ZS-}r#l5Sl@6-|32YKpzc*-$Vh zqN21g`Wh7-)MjG#=XmZc+DA6eFYy8U+XYb)um~z6IwHQBAnp`;2r&;fsK>gdw0p`_M}-X@LM_hoCof_R z;c&WY`?=5{!f-y;#E>co1z=47&VUNWgvHvc((^U*W&TRx6!M*AyxMf}XrhYE8u$Dl zlOU; zf>Zo>dw248tHia1tG~4i1f+}6Bat`1e{1#1vr~@<(^0|hg{y0I(q+Z>uY3Q(wvCi0 z5FOY8b6KM)7FQnaMl(FV$xK<6FOXQYmnZaD+3Cpq&`X1U4oX>a1H39?+yW^CS zDMJ5@xl(=6zLpkXf~~zwEuVSl_z#sO-Xue!p9i=F!P z)AH6T(pOWd6;jGyP`M9y4`-gaVRmevPqX@{OscECvdJsV*m%;tNU-0jock~Ay=PRD zZQJijkuC^IkSXYVM!Mi>Po9ECX17-NYvjhm;GJUA5-3rG z-howM#DUvoSnPc>&6<1HJx-9@Wz)rz=6=161BKd7rn)8Kk@do~T{RKU4gKE{UMx7_ zo(2H4S5N251SN#?@e``@Me(vHW-$$%DAtZ9np!-N|M^!M8X$CIpBCOWzWNQP?HdmC zUo#mXZtRG))Lq}`unKvFelj#G)+;t;^dm6H1HFXa1T-0WT(c;dUF%0Ll)q{hh>D`( zHt#5WIkaC73EIXgK)zQ(x|^n6>-SSidEz+=>}QO&JMD%9A^QYhV>w!<8u^}8>-^FP z+dG6?zD4|VYc$far!JoN@7AasQ!0lp6&IEVIoi8<#E~^6zc$P&1Zo0BEv$##)(RP0 z6!dd40;f_Nw%^Flx1x3@YU>#|8$zmWXoSU<@wXQe&3h|DGlW(gjsPQ0yO|eNkHlu$au$qKV z5~qvBTZE1pI8plYUe8HJ_dcxPS5$FTg42}+CbbqC*|}1>tekM+q@DmC(DhRl_zvZz zZU|WIV{QE;ePq-22Ya8un8X}dOE!bguSvwTGRWbsJio_tBsTPCgZC6`CUszWLOVy2 z+QGYO(RH!J*g%hHku;B2zwEVF0(1#)^&Wf>c8P-VZS2&$D;sV3kEao!f6(Y6V({vVYB0UEy$+n;3M1H5 zNg1vFI#E{B*|Q?^NbS5<=lwdnc9DevV2$j_du%syxY5*z*Qgk_@@fVR-*@EwOz8Uk z@NWp}Mwb+F>n8Bhx0<7WcAjyc4}O}ZT-p{N+s7ck*xdg237r2wKSQETE}|fh4LI1{ z9z)6%0fRZF%}l2ast^-vZKc2G<(3ie+vx9T8;gF3nLPu|t;7@>53idDK8n@2_0-}t zu9e}#mtW$$k9yaThc&4Di&Bs6b;B3!I{UBn6k9%u{QO9^(;dRt%(uGNX!2%5FzXnf zIrUd>iqmDeUnhsUjM?Ek){MY^EKU?y@V)+eqp zi@d~iH_$&TJi=$%_rkG-)2@(@69<$V*^0GRCbu~-G%@wRPe{Efthw= zr^4iEBHQYuSN83!@t=z=8_R}|Xpwe*4mlbEpO7PyE|!BQy`xNpevf@~%0mvjZkPu1 z|K=N?b68s$XzII`{qQgv#1NaI`eBCjky`PK{Ld^Y|MlMgOd@NW<)L4vXdFO11u4lI zgxG1qQnr+kG(9>mUM3~QdwS_yCo@o=!=tI?7kO!C(zA94?o|smmeaH@2)i1F6&9?s z=x&>vo~3;QTX8f}-lOV>XXl#_j)Og5wTO%qn{0?>$|m^_9pwQjp59feV!1?duPW=4 zgI~TMPOm*aZr(TCWVijTAO$%|x2OqN5_-RLx3Zh)BhC}p5s$+w!EXD(GGA^6qu+85sM*4b~Rn zd=0)jhZ7Cf#h^-^AQi`A>p=WP@UjP#WH0nSwV`X)rYXp;p!`9tVV)zfZLUcyw1*HH zDEDM=uQ&Me7u)yh{Dmr=!3;i8(qU)#m&Wt`$?(!e^}4@qE1bGTKYJt~O+!-7sP4Kz z3-wx1KkaQKC2#RPA$#{j;7G3{m1sV;)UZR`iqUyDqh#c-}=sJ|Mz}gt*mtx`yD0_fiHRKxt z_!maJxL&doq^8l#cPvrtH$;qMLW1%`xA^;C+{@BB>MF$t8W~D2cQJ``yH~`_usfHE zWvx?!8iCgk(Oh|;J{Dzm8@Ap!G8uL0g{%j+m0}edD#H!i`VAI?4IB)G%YI&Pdls#M zl}H|`@?*q`_?UeDGXC|`1ZI-{LXjALNMd>jUJ9*_p3COYtQ~jCgr)%#pyTBhOBE%3-L9pag zN1)0B%(sicdJ~R&Np_8%&q`mbQnwj~zY}LE++%e(&^{MHl!_P|L&}NulMOue8uLJo zx3oReX$8YeBP9o&b}NvffEk z+U<}^Piw?-Uic>UMPJPpC3_yCt=r&UolCqRTJFN1hZ{=PE;^IA0cR3c_6Cfmg>eHM zepC7LS=@S`+FSvETwIXKpsz^m%d(^L$iF+m7C1_@ZgZl7r=xH&}!_ADHIIZ}|d&=PPhl+|c_7${6;ZyZFbAfHR5WC27>y9cBl4((fP zm1T9wW}cZcLk+s5lm=&>3yCGF4Xt^|=5Es63q_gIt@7+w$9u*ZTh9acSHzRKr&1#& z+ZD{CL?{ty$#YW(r&|LO7ac@i(9j@)EofLYnq(xT*PO~=QgRu<#u_h2R6>SrZn!HB zO#cxyIyWmeLtMuW>qIvy8h1PHy`jok(~&Z?Hmm58$)N*#3C$yDPE8_nn{m*}u#(GX zJOzMdOM%VtDbLw9I@V^h1`{`pbttMU@$!@=u>hVm{|SHvTl`K0Oey}XY`Ncrt2?qG zcZ8~KPv9hgFz~acFRB52UtYI(xN^#{8TZSS7gT5CH}c=YM#0?6mMBMG5_*n{CeHET zGWvkJDK0;uBs<>Vg6;dH7@RdV*07=HCU=TMWSN)%9^*u%r$86;HGh11o_z`(Iz7oo zXC7twy8VvYFwm3bt2A$uhN!Pcf@^e(ay>D)jm|VHw(h(3Rgfe^i*)y{bZbDrJ*LL9?{-K?oP?k6Z76?SE?6lsI7a=6!enzyRoTsleuhe1iCWv zrUUHaD?mx~FaBUIC3eH{t zbCUz0CcROM>z&3MUMG=_g^EciYs!jWw{gw~I*wkOOLEs_}|o z$tRD0vac>b=gujpm5hK^dj}XTH_pj`o&~8ODG3_5Dp5uy_zl9N?|}b3<1AVpr#zKfAd+RY#;TTJbYvb37iNZESF(x@rg+3YU)tq z|8(2@f7U?n-{(!=ozc^nGm+p32}S6Qux|Qk`<$u+_?}8)zt=wFlM9p)SDW%k1Jwlc z`2Y;0)5N-hFtkj75{bf%rE+or?uB(h_=;#|fi@Go4B;FU##F*e(YSFO-|G+%?GyW;88h4-m%VTEiq=a^hwL zxErsiSYsqisE+ zN7sZe=OH6YCccA>lb)5}Bt~=C#(HZ~*Y0BJ_DXNU?#d1i^rE1t)S`-(=&N*hA7M5R z*|}ogx^x~)H>#^oD?i^Gb9Nm3`m9;=b8Sn|=QdCrxNK*SkcIRIwi8o;jm=uVQW?P3up*E>EMCi?pC&SDi-(RM2I=kU?BM)Vmy;?+4-} zYS_~E&OMJ0SWTf3Nd}qkCUYa#d>i0zRZ2v&IgSHJ% z>+32ztkqCtXA`G$z#_)XWpJ2&rOgwJs|GenjGSXj_;7~LNzriOgdgoC)e}gKW&W` zX0Ddf!maZsC$UsT=e1YTuM1APiI#i^IYwes!Bm|eK&bIb5L*-h^bDPl7ebVRrBSis z(nL`d_DT&yfF40|GzEi{fqNKpJ8)KPS4G!EBtXjE=hC_~sO2Hks_WgYWpg?6%pHFn zWuc`C>&-=V*|nKT8&cfHh{{t(`Ms0HJd2elOf++zA#UBa0(4a;MVSSV8y9jfq8G0i zi83MOu3na4V>xd)o*uSWA~==UI_A+FgCD?_uWIsPh9Vq^CuB_pW>C=|kuD~I6n%w( z;7m+9191r-LBY|#=&n1~?Ersm@V_77S*nsXlg{xD@ltk2IN<%Le(H8l*)0OJ>!~PY z^wL123Vd|P$~)pfPw-Qw>h*!S9o8NRC!*H#5{10*eaCq4gG4dcdbM$7=H@Hk>M95q zYu=(R)tJqSrml%x@k1}PlO)XZ8ODj;5;Y%n5=xVhhaJJXj2F}M%oA3=g@5j?-6hgrcAjq6QIHa>A@ExZz*k4pO7E8DKf(LU6&Gwj= zJAqwMn7@=UprKO?X~-$BJ-I$qhjK_`RROaUtz=XP0qM5Y>|^k~wGJUTZi-ocS>^F_s9bP`jkt+5y+fv>CSM9(aC6- zbw2P0z--uRCrZ;rACIssJO#Fqj@VKM@tzNKlg=9Sd0bk0bi5Z0?}VKo_l|JOO0GQL z>2N}vAn*o*-q&v+`W+-ztQ9d?S>?Nq`{GRfXRV#@1oXvqJLw}h7(yoxip}~YzHzs@ zp`sd`nxI-!KV9@G4FG->KU2{ABdR?8?rmxs%LdDN^W1mpQTI8%o7A20mxf9dHYu?) zMTlYp>+ZUB1Gb`tn$Ixg?*Y`lpKkAkp9F@pbth&Thh%?rdENop!;K$uKR_h?RUyb< z6?Namn%$@@@$IK5q2yENF0GzWCy@P*yFsVU3G20^fw|SGfjXn^h>YQE6=;RM6IwDo zw27xl&e%%^3Lv?H^Qks;K_C9-^1g%gpni{v4?#RchaRgW2ZD%LAso*97ta3yP%7(N)BitS5l&+YCwnb^i; z%AqMFx{n$SwzR!%{xjRS!mau8=Y5wf7E-t?FpffvW>wB@*>IGDFl*=K`{zDY4vtgG zMi+z&d?g4jN?8`R6}IVCkOQNn9V|C%M6G@5R8N+I^ZXDaJPaz4&JLAN{0PV|Dorn( z`vA)|k0IQOFNH>ptU8~<=aqs=9?Kup+5hnIk{miB#){5=8VU@qEFV8XZO%b#nSe?OA?lW;vcyl0&d`V7GP z?4qmRpGQvrpwWwa|LPw!Jd%HxxuVphX3p&171Tdy4Bf8RrFp~U zN%~`77m~g^RoPr+2?@7AVIn&qfo=wr&ky$?^1m0Tf-7Q_B)V4_v6@1~U;dxEui#u& zv|6U|MUZ$U`RVgz->e(m;CMBw+C6@7R4Xk4qlqzl@$L6P3B^ATO$@~J88nK zdzeq59x=ba(}SL$ld-G+prJHt1Cqh+Qf~Z5{z$?SFqrZS+5AaC=#%zm`DJ9DK#OhCNtla65}zuc8PdxUo*UEX z+C^1b1<#ma{ZtdjL{N*CzMHf}Pl?;Y3~tHYV<2|k_DA74l**`Ci)I$D4g6@&p~Ix1 zOowqdLD3~=rE?y1R@V`jSzu?9t$aZ{fR8C|K$!-3GboARpWz^WFA9-p36KR_)wDa* zuXW?N)2;UEI$vco_^ypOX!3%VKrLzM-PxjEvJtZyk%#0=Kl4QGmCBB2%0|`~)^O9x{OlA` zh^eliD@0OjV+@k!F2X_utV&ah4q&Ox)8nY}R~`1hl%&TY0~QBR`NAn{zVY+lWl(=j^UMrJDs*b^>e!`u=V?BAah9%w+RZkdsVfK#NxEMRtYO zCV|46mRx5|QY7#F4`A5!e4iGsmjKz9x&2XC@Q(NG^M+h+q2G;;ddu$uv>|qEg%eDG zs*9fbF9oUXvNi2!X@L~mC)-B!d6uG2ICvpam$k~ONt`S22`LApvDgS||8BhmYf7_s zx3&^2eRqsDWpKmT-V8cpaKgcE$p~B66iCt}{PiLSKx4qtAnad!D~iDB0@sP}^S0-D zWQYv-lpm}?yvLL7{~9CcbY-ifD`6oHg**?wF5dejh(5u76fyW{Zfw3XJi`9yxMR^_C0;1!&}8?#VMiSY z4M3VFBC^x-6zOpeIo-2}$eL<*cKhzU9i^-RXtHL>-SxeS>>R4e!o%0R=={7sYZyNc;N%1b_Ynqm$#ZC@j|HEjxgSUo3ymJIWT1$XXG-Q5cqm3xm~G>a^1f zl=?PoY^=#wtc-yq%1Da`T+>09<|`g?wPiFd0fv?iFA4h1c-jouk)sC3jr5xa6B} zWXh)2G#Xvu%BG@-om$6s^Y$Kgjv?+oG!$xk*s*TtCW}&EyrH}LMso#Bb4ULz-`Sc@ zkU3yqnB!Z98dk(Ib%;q2ee7~ORPhz)kx`-Rz5QSP&C@d64>GFWeE;%(T_2G2R!9b9 zR@8E2nv8$`?V(h}X)rF;rpsmA={2e4W(?IM%M+JmR_zDE)eR~$0u9|3e6t*~OOJ?Z zGjhWDMKh0Wp4(%(aJWISR@Kk?fO%L}K2?eijG|#}ZFMGtBsgTYh-O!-(wqivA;_vZ zS#kMFwG0^;tH^FV`UY)IL$Bi{+gWM2kGa=dO-=#95F3XfYan3URgSb(*o3 zU0G=x`#BS6$veGwm~*FRZ~_4 zzNsuxn&d|M;-f6V*JL^6KN{Nk%3e~QI~dO_P7OHKjSDW`oGZvfl3Qo_nglIOK7xAHpAu)Xk2A>gv|AO~6drT24J@ zYCdUCrj1xHk_q(WGspxh121z5Lhcn+XVo~id^G{*xlcC>(>4h_USS8zWmbO#S@^B7 zhmqf%IkI7Xt>2FiG;43D8e+$+0V)=H?mHt3 z8j#zh2I5*7^_K8_ZP_EJ`U49ZDRlQZ-C5K3G$y3&nui$ee6G|Ai6C)a9i`j&g3Tbq zx%MrxWJUtQ7H$v$0Uiq}IvJv*Wq9OP+ww~vZaR05e% zq+e$DzrHTn@Vy71YOO?68em~V+o+(u+v)+1GwQZLwAs0aM;Fewx>Gbfj^9f2@mkJG zWugg(MqNYR*9KcVQ;SKnr^<_EClStF3!A0D@~ecQrR!e!Zpn<<@Mz3j|; zt?r`+%Yg8W6$rg1!5~4sCl=m{6M)QUG;qe31)6MlGvXYC(hsjq%{Ra=`ZkZr<~Q6$ zDTcx7SqW729%6b0(X*sy6gbA-dLONx`x7s z!b>U&;P-?JR#|h(r{CoZ-n~w}s4_74uM5pY+6!FS_o$y-1pkXvkM%G_^%yz((WQlz zZk(ab$*HRN&ihI2ds&F{$#+rz4Rhl3SOaoBrc5UVxFg2KaK2;=c z(9`&OmB)i%=&hzqLl%2{`5mA+uxJ zNZAq7i>dLFQOQlcvUd@=rOte1r({mk9`xP4XD? z!3hrGc@q7lZNc@yqN&z`D0If|C4zkx@z0!I&nRM>h?)8N>W4CmVeod})IE(kx+6fr z)|A!CsM$Nfi`R-_t|Kegffrlez5zS?=jqj=NfdsqPkb;19?cS6`80++ z0Qn$#ip3ph609pm&nk^vdWU3b?FyRe;E?s=jH|qJ1+N@vA)Y{_lSmb*#c9h}u{M`x zO0y2`p#7PU*^ZvofQmGpY5+eeFe10$92#g$TXNN4ByBQ_`~_Z6$*DUuGt;6&DBW&$q^{0}EbWLpXF( zJ6#{`QrF?QkC@hJQ^W-yI808o@mZAQPSX7IPo045O$aS-{!Ub*RiPvL4ShJ-|9Y(N z$y%7}@#^-K-j=!o(Qb`%+!fJ6pX^oq$UY(~U+61GZBsQSW}@EXH>oG2z=_Lu<4QN3 zM&s4#=b@=te0A0J$2(CCSD6k3Xqpje|5MHuB00 z)r5L7^2`T25S&P$@A582emGIj!_0G+NX_Ot=zH2~@i`sdh4;d)y`}tL9>@tuKfM#? zes9y#|7a7rR{zGmlv2w~LKiAO*t02GXt%5U9xz;k%H|IR@v3ejOm3-uuD~M{{U>Im z^)!^&?F+xF*tG@o)$+x|Arco%>{%K@@TcDNmQj(dg6M%jxko;y)*5vloP{S2d)uwM zJ^B&d<+4JL+@Hue-d2M1v~MpR$rpE>=)TVuQh#y|$uKQ^QxJaj=5?h}!<)DGw|9>} z+_ipBd^XyHa~Qc9dpiRXCY<)W3$W@@eXY!jh!B6Xcky@Nxcs?hPJ2K36hD1MVD_Ey zNL-M;tNBEjy|Sy7+DE`HaGS;U_vv>^N>>_8hbQ9ue7Exv{%k+7thT4jVAeyjHfdyT zS`n7}(1bvMLDlK8X5=a@oFV65Ul%N zM)=l_rWAKOydO6q*#KtT%9if_0NyL9o)Yf(=}cjqi6iswos@(C?dRt$-`U1f-&?IC zLn%t2#^jm|PX3PJW7D4Gu~9QzhLmZr0x@J~zY1h&U-T{U1e9J@ZOhd7&E79hKh5-O zAbx%)rqV|o=M`gU9vI9o|F#1E*xx;=Mv@7*EiW<253$OF@0OQOTAvf*ycZ1R9=b|P z_8jh4W!e;k*-uI@FUOk#dhXEBK<{5>ApC{gr7q3p6VJRU?xAocu)0_^3pskB9rCtF z6Wb1EjzNUpchttJJGSL`P1ni&aUnXcgTq}yWidnOTOn@_Y~gEvJ|SxaL$v3oS*)O4 zDmio;TRoubhY2j^+(^k@{&c}T9$=&#&LwkguVSL4@TwF&nsR9%u>84w5zb#?9AHL4OL_01=YG}VOiJp}a`Ljs z{r%^u+~ex#mvsymd>hb_>t5k#GD$w0F zI$Hc48xvmudD*p9->qei8)cu#_KHopA-{h;THhx>nGp+(3*@nB~c=%W5`yY_7Aea zA#rx7)+P6Z+LUjKie4y$MUJ%82W1}t!rqSKxQ)GTdEf=M+j6$<-TCp`<~Qx(Tr@45 zi9nzqF0A`E`G#KZ^V)$Ow;LJ4O2*tyVf)mMtKQ$CuYW!4^sb7M@M0n=vFZD7-ogrd z`+bI7_*s|?tf?sLeVoTXr~VAvkFSq-7!COu&h-jDxJNmvZT7nxpPL-<>(bEQIEh@t z2vxq5YS~L6m<|u}(Eruk-z(?9F3yA#?}1b>M)ZQmKU?kt%$}lDRa|vO zgG}pqZbi(xn~g!8SlNH8io2-$m(i8f?oPNRwaC7@u+bfC2v!(1bGq!eXI=dFQ8Z9) zHe+y`DuZ;hYtfJX41{@xHX!!*Yg`D+<&OIH`WLu{7<0QiDeTu0n*X$ZGqN608U{w$fwP99`b|AnM{6YQfMDq}Q#1_p~`D zx8IBC7Y=04^ub{=Dnbr$1VrDjf~>ko9G@$T8|&z?%Ewxu6u_4fsyET}vqmlg!t-Hd zsrrLeF*%J^&@s3W^vo;b9jz-Hhm$JG707HDjrB|lIIvG%K}2!SD|_E`)3^>shA2Ti z8558(wewf3hg*IxRVzD&EWz;Jm{$^1-<$WkC5k-p3^~@9hw~8uPTaf%OT#~OAUdKQ z@*t?ucA2Xm_F_ZSz7XSXJnmNqtTfF~!KaCz&kVLdT#EbRMjzY(yjI_s)&U++TMj0P zWit3{U5-FzYW(VaL!EOR%2aYw(h@V+3Oj|ksJ){hul7Z>i%AuRo>54=L9WPvl(Nf&B{8X3>m^z&V#HXV=x^pY9XZ+6=CB%6R?&$Tu;zu z>Xfjfj*lN(8QC3E>g1TK{afu)L{6!N0G7c-Rv*TkgV49bnMu>GWAjhvi}l!ysd*0h zzjsQ}np*VVmGoOHRT_r^eLmI60^2q}RoW{@0FK>tN@0{3_Sfn@-4|}0_Z!S9EDQ^D z`T9Edk}56XkWT-Z+!c7rdEaI`J^D-_yERwPxAjI7bj#e1Qf*4}nMdM>L-5|o-pU&5 z(u;VP%gl%H-y=RtZO4wqa&vtzj8MrQrB3ovC7=m8jy3W^u&8!=m`$dU*Y43?r&X#k zYp)`8M38V`c-QHc8ngF3dY;jkZLI4;#-DZU)45Aei5 zO&v|S+z0jQQ;UH8n->?LN5lC)JD5NGlCRBAkpiaTUr+n!33U27%dXZx22^{!1<84G z$P6yT*~&Uk(ALr0A!D$-n+T)P*!mREn_x%1P8GT$fQg<#o<-iLAiD?3uY5n=W1R!i zCSWaG6YO=@TKzvTMj+o=cjvCR36FMU)(NZ>ecN&1dizlI7;s+NO!aWa?)=!@tNPg` zk>p+i3HwVxNGW7BuKf?kL;E*J7^8 zX-X{%;u)?uJAsLJAWtc|=?J6 zj-cSl2TIYc>?*nlMcw&Xp@AHYZ6Z}Ew98aEBm z5?a2!jJv%ftbw~j8xt3Ct60Ym9}B3ej8QO?aw-ZDK0kQQ6ABxl6JZicagof(ah4k~ zY-!~BAd(6y<>GG`7}bSy}aLyX*d(ndlkML-*5^(qEEQb>&ykOYaz`=ZdW5`vb)2T17+G-_*Sxw z@{(NU%^Er%epz0-N{n#&YNCU`1W4DEeX6r7PpOsw;N!ICy@dkp{H7lS%J4~=MsIpU zbt<$S7ZJe^>`>U2j6Y5@f0?`X@U=j+9Hd#Z62MI<}r?5KR z=2xtI5)U5^dv?4CaMlyo1x^);If>>54YQVWugPA$lh4}atM1uVqb@&U7#1)P2faR8 z@>ItuNdDCgi<&S*MIQ2_9aM&w|6w^`3_x0S#Tc;rj`FBAPKB!l6ODE~bD)1bcr`#t z3e`|6-)2O{kjSEpb{9HO*rV*i-CBK0W$9DK`V}P{?c_qx#YMxu8fz59*jbw8!0gx~ z!o|$6szm7Ellqin@pE*h`3v_>X#+Tz2)%q6A>)0$!TCxozDS9K^V9bXb74+$Mx}Vu z^1CH+$8)trwTk>OqKj9-cFU21y5e3}g#e{{FOtA_H6=ArgXivEuRtSs@YrQ4p!0`) z>aIp%kmtA{v~3j?Sszq6s(}+Lf*oWI+liWRk<``W=~QgpCLKUopb5{|`VuvyP?%;k z$anl5poqm6r!9(dNU)Y(Nu#y1f0AM>;9+dlO!PO5SG~=OKRAVapp%u%+N(vav0`+{ zMZrVjNP8NrM<_eYB!poS9&tDB&Xloyq9#f3asJ+k z;9NXqH1JiU`A~VjWc8DM405~$^RL|WfArn?fATv0B8q-KJ^?WYOBn@@g!x^PexsTI z+09cm`_|BAj?Q#p%MYB?-^L9oNdq-A!8QU%7AxFKB(qNKLYrPhXL_-Z0QSaeEH-ld zA2cz*8VBmEviTih0&nAG?-~iV6J)_tq@Z_(y$@rS$X-3szd8>|@u~NIEDkEKwU=GF zB#napZX(139(Q>`ap~R(u#pM$0cQxn;LZ&}I^p?%`kpwaVjN zc^i3PvAGan8Yg_Bzcrq|LnpG+?(=EPnkC_+0iF?S10YjWJ0#sHV(O zlPZJm46X(3kZb6P%-v|WF?6n??T+nLbOE%o^~p(}V~&!pzl@DO@(NeDYzHr`BiFHg zY$xV=q02%14oTALbq3KtO^!z96@cfxEeY;DxbVu67CVGR<#%yUqiKn^f|b}O>MqWw z)Y0mVBJXO{G1LWb7a?nA0tWL8ZxF2^4H8OdgH91{lngBw;0l~uTOXTYL@D=pgJz`R zi(BYVb_bg*u!S8$BuSQ`4Afq6xyBe{TL}JcE^T#G?5u-LmW{*?+E8=dNJ+D4!f5NJ zZQ%1~rdKYKHRX7&ujo~a6F+^}EzNx&NcYvIjPrwodsVD&@|6Vm`mfeO4qJYI(^6$O zlW$QXkow#U0%-z`&>xYd*{yFsHvy+i)2!mDg|^Rdo0`8Zpqxr7Q$_ahWru|o&38waKEA|fS zrgl_gd@3*Cg4ccbe3_F62XHx^W}*eRgZ?^E$5tsCX`p0tM9s|%FV(NgTO)Eo7ktrl z%v(O11PCb|)GqoF2>u9Q1gh=n%YdA?_Tp-iKTprHmgbohKFv^0b9@V0H}^%E`!F_G zv<`D{K6D%jb-y`}muhERX3=;D*Z~X9&F-wxam4Ze2q%jVIGJ+_Ya>q|U5()_sp~`C zjvA~mm7$J6Sr^~;ef9t|IBihB>hei$-9Ys7W}DYN2aRnrl5mC(A3D9j&m(%Q3<0<$;825Eddb|><+}i__0gCMPS?MT#zxZx!e$sX zt~Vs#F)h>t#7P-lQ)*iBIU&hgbN(AdBb_MwtUI>)WI9Psw%5pGr<9!1W8YaK!~HIq zgKV}7cJJ;Mqy*(r3ALE0BIKhLcDvJq3|&hvRUYs=bUwEb*w;`ER^vCZNtUYM7g%Vm zxs*rnlQchcs&v;PsWH@#e}bB$q}&nZidqXMF0E*>q0Ibt^(ks<_ZxU=uR6o%K`hog z2cqK2*Oq5-HMQZX7|88B;={9`?@3+&=*ieuA9aeovi~Uhz1)$6XM4!ehwJv{d<{SP zZv^+TqjzdZK9{r2@d16jU~$Af?Wmwy+3FcDL5Op+Wt8e+T7t#I_-=U$-_FHfq{2xR zf^BhHVMpX~c_(T*>UtD?o-H5QS_P_*%1tGKzj=vT)b{K0@SRQEY>+s~TWW9Qm%mY#p5&elA5su#ATC5n!;Lc?Oy+Xb}`!*#_MO+L0O|^5Tkri~%y8U<-LEvDpa}83T23FfPbrwIYyadpx z-?uYTtgu&tPVUMk+utUS!p|C~%W%t-5g)IXa`I7OcoT8b_X^Y}o~=kEc%Ff+JVpPsU&fx5lqc8P=W!?(@Du zj+y%h^ry&3u#pZ~w>*YCL4o?dBpO>>Afv*W=)^-@1P@G&*^HV?fFB60d49EX$c3b#IBo zGXNb*SJLvaCZ=^OF$Dr_QCx<<&vqU{IvJ+bWnS=6Wqt?l#*B4g%&)k_sQlf0%Xhm)a1Ijn7T__lEd=pus&A zwBzpOb8b6WwgV}0zmqxo<&?~KS^Aaq)wQ*m5qA|_ zo-ZpS#xIR&Rs9#(;{Uy3@88!@x3s%nLX(^93D&`i_20GQ4bQ($q*9KZb~783_i~GR z%qU_3M|_TrVJ5QXm=TlSfa#hmtj=FM0|LJMG}EZxPLH4vIs_aAo$Pa4F=>cf1;%RT zdX+O|YKLM-RV~92%sX=mwQ{kTZFxX$=?22**M`GLgdT7+RQ2oc&6@tKy?WeD7ob@c zpV47(tVIdHWb&_U)`42i~sIY&01+8`nk1 zIPGm`_ZZqRg%V}>0d9ysWba#KqexTHJn*cJi2(XmpAHRbSlKxW}!3;cEwX?A)z+Gnl2%ev6Nkvl_~`ZFGtv(U$RiQWr#o9 zBs=qmB5u1}vg~b4t@AB2blR#&_NTF3jh~a4%H_Fz+Q4nx#BEQk=e8i6vxBm64xC<2b^IqA8;lp1efAiP*ge zG-~v#_d79+8cx778@U)XA_$dxe#<|c*U9AdZfw(Pnogn_e4#rt?vn{pRgR}e``gJ8 z-+|jYVXhRgiX4Atc)IiwXHP#9a&`~<=C=^b4+rtjl-6u_$yK-SIgxqO9<4`q@R%)w zSkJ|O&&bq&}p^B{U{`8GY3VN>s@E? zeb3T`atH%Q1|DuvbVY7OsY2$RKMj#jNFoaGv~(?y)|D! ziAq5!(^?NJz8T`T+HB1T2WtMh<+CU7pvs>L-91k<{|nao#mD#bYq)<)ru9E)hM$Mt zpR2$>Jhd@l)G#Hw{XVPAuZei$^HX&%Tvpo zd_*osl2OG87a}1~@T+N!CI+k;wsXHOuDmkt%)iie>OIKU@a6SK_kz~}(eE{bAUUB) zHo*y#r%ocul?3n?veU_6BQqe?pU-ohFJ2PwQzZxu?N<_{g?jd#nu^tAu%241*;n3o zD30d?$X(DnB^b(ZQ$f_&jEMu-ymWo2ic1?3NFQfk5^>;k!k|=bXFsVNVyLGqlRYy_ zBKHkS@dBV4I=xO@aq8POSK7`%P!)%!dwrzm7pB6;25!v+c}Laj3=ACDwJbF}QU0LL z#1vZ(+fU;-J#n}u5YF!7wyr`K{}9xs;_jO2eE?Jr$$fZpn@) z;QdyOCWjMUkBGAOFwRdwl(JG+*_vmw$dy!KCctG6UU=FOY5fj|SrA&& z*_KI8(h{dv2hSTB33blNGQTD0a@oZ>I*ilcL>yjEkgh%qEbS3C*T5sUPQbU{OFNfB z+n0vzr3G>?!OHo*-fdduOoGa{0gEQkho`LN8eI^xk^~q9A&B%bzUa(+1igF{XpFlR z%^`bB%D*;BpYLwK3IE<5%oP8j!r)3^X+M2ft^c-?|joye!EIRCvE1L39$;Nq&CP017sYDPaN;CSoAH3vWq9Vd$rq^R0XD6^NX= zfVGO5iMCgp4k?XIAAD*i?M?bPr(CG65$B8vn0IR-_q+_Bd>L8QYy{aRA5MO_F;!#F z3tRa}1@Zj6Dg?Bce|-TE@u7<Ycwi0J^6CJ32Gi2@zoSBn$af2)oV%(T*Sf(;&`bm z<%_odf=|8Ahm0$h(&^cTSa?Eoz!ErNN zln^j%lvtL5nahg^aXCe9{kcBu^?A2P{Es{7(c3KdYRVv0R~=4!Ek)k-&$H}DZ*!sf zPhVs}vUsV*@|-j^HcRub$N&8r6bcmMPIt{5h1X4YVYZ0(CQ=4Pk{eY&yYCKVjUCoeYAk_QzB*8gSWRv_YDj-h`a{1EuVz69f zRh8zuR2#2F@to^=$Nw99?;X@+`~Ufdt~5aiO+cD-P#~eB^xji|P^Bky5D;v1A@nMQ zB0WF|f+Tc&klq5)J0ew3KtNDz&*r-`yEA9b?wsHG&G($0oqhhuWO7fgO!B#}J9A&3 z>-~PeIyy??&Gl5fHivBJ-{OyW#KexWQ~vNVJqRli4eedi<$Cr+`Ce`$^dykn;;tDL zJ)f&o%HXPYsQvgT_CN~#f6e>ol(PTn^Y&*_Zu+-K*6+~fm4OfjJ^2Lv1IfDSRNIf@ zW^YSWLXfv`8GTYEWI{VlI8RI3ygTkyf&VYsp^r~KWp*|D*c?s0?0Elf;ki?6^`w3= zOaw-rGexui%$NV?`SKsB(fRimq79XHh_ow%2X9!YU%f;Y{TlQ=ae>itp=LTB!$X)z zMxwm^nM4a^a?q|Qu5QB6C0?DM6^(>x!AQX)bp7gH2{A|>9K+PJ%tpov&6 zh;6vo9=KI)n`fY%DhRY{SDqPSCI)P03vyU1`pHb=zcOqWVynR|KO5TXg(W!d=Q%Cznhr%(}eFcG8*FZY_TPpjc!L5yA~JLMwU} zsbDv(#`O}pm8;`OW^yb1dRqQFBb&{6mGWIU!VK%d*Os60aK)b}Ye^w7QDXf~_hh@E z3jZEpC)N&>qFiUVAEU;=Q2M2A_Z7Q7H~6KO$MzsI>f$paOt@NH6J{}H79#v$-;8|B zLWv5fKb*u)bv7dscmw^Yc#g_|B*77P8Ll~6yRHs>g! z(G&xdnQkuXuh`cJzfLquY2(Q)>S&V`2t^rk1<7z&LmE=97QB30MI!TdXN(2}=@Dh& z*eoVJfvP|u&$?G3#EWc;%+XoxcN5`LL5oC=5jwDhW2}^w-qkdG`L@gKYI$vmW)LRt zwj|`|XaOG9Im-kvPe_hhX)8^+CQe^7!fjeY43+8cHj}s4q(f+X%JdffBAQJLD9B17 zyu)^1zmge8>!~G{so7D*527mckH4ux3XNSZM{x5~ITVIWQ}M7Gy{_%c|u zc$3 ze9Kh%MxDD1#ieq!y_5sXwksjNCQzJmt}YX3vgP$DS$uetsa+}$ z=&3Gni8B3j>@O)mj{!uesL#Z!xKVo+H!^<(kaL4dG=U6n#3`=S&8%ICb~v-of%o0`-m$sKX&Aze894~?j!0jBGGPS490-F zlLqB+Wimc@lGcxC@RVrSd2_{R3G?2ba;O3Iae6c6rkZeIwGhanStoZR(_y!gXH%O_ zv;oC~6ZWM_%Vp%wzMg5Sr)CIWvcTMV1PjM}91qLhnP1KGd8p*YRaa*GS8i+cdbEPd zwLFMm^J&Rbp9tTUJq)sSLKDbH z2;?Dl-so98hUnc%WoNU;q~>^96h)!3_9f(Q*n<*K$aj}t{H-vvgvPL^2JDBDWC1|@ z0P@ywEDmkEXgzmJPUQBj#i4tk)F5=8sLkm%#JV!2 z8_D$6f3`HBmQ^etkv}5H4HObx&KM&;1P0MUobNrPQ^vLO`2RFJ_)AgprgF2FpCeBa ztFKUtf|shFXv%dxmrs!?7w>{YP*o%O|}fL%Q%w{8ieEDf@!E66S*b((%DUc1V5BA$;r6OH$Du$V+N zN8DXSkGbuF9k?ego!r4ryR(hku&8xQlH5^jnvYR8v&n&rgS|_MBq-g_dzGbsP9HcK zw1^rZ%pc|aEbu82#w<@43Eu`CIj}C*t3(AsWg#x$`?odY=&_-S=b)o7i2kk5#Xj^K z9HNBdq8J~EN|k-{T@p(Korc6GR@y3~s~VCAy*%ofW(pF*^nY}C7Bs*kUc%saK9cPG zKIs_0W}bz7((B#cuOc#?Rkymf({hm_dsRA_%z8MVu=^>n#jjgMhc$lKl{6{q8$Fs_ z^{YvHH~0#yri#Dc$VEOjc#aFD#qV>tM z!uVjb2LnImYW93X>$>|3x@M&K3AK~PqY19I^ovA_c!h`Z65$jB@;73+w`afE>WRK? zz07hgPD^aIpRl7t;H6zKt-xx5YnYVk&$9%_Q5+ZPx;SO!Jt?Sl%flLXMNg*;_W1|4 zNKL#?^0PbEOZ#d?U?Ff@jJB-0#*)G$Ptg%UG_2CGCB(AeLspt@pjg1Q7_54rxTy3 z-RH#8Me}Ygd$$wZCrUV(+Z(91ZvH|mzC8c)@`tSPr^ALYQK{$OTkjQquqPwWvD^2) zZ_Q0T{h<^aZjef5oa`41>akl={PXya*8nc?rY-7MgI%Pbzn)F`zbJ##j|u}(Cu{{& zx_uc2_eTHaUR=|zkZomsMhJeUx>0*V5)qT@y#4eVVjqhx;?DUvXi(Qzzvw)8zmWBa zOF~TN`#eWe`R{xVtKH>$PuqTwN!-C-P@k``mEQlo^T{5L=%jIPZDV|%klIfDgemN| zCos3Fee!da*&6Tf^XL|}tufta;2(362ir@6_XOzIY-;MI4mC<$PbjDO168E{CFY=? z^*h$x$6Ei8Pi41q*e(0#g#X7+`2VCl!20Vl@)KnHjgJ9$}30(*#L)525ELOWf|6&0bY%iTd#tDDQaa9#DR7I(KLc8t3U)doQf{&6srh zdds+dI@p!r0bLOp701EHJ~AfjW4xk^mcOcHZ&fFGf_8uH$4ifkN($h$y01ynE^D2+ z`)=UGM%Ypl)NjQ65PxM74^5$fvu{|^6rr`p&+W^2; zez(F4$e<|6nS%xtHP*crzKkByvLX!P#%gl-o$(-=%K+-8#MxiHrFjo4T3o)bp-Fa+ ztL44$I-r-xrxrjPR*S=|h(ltUf;ELSDH|h3u+?-|0oS53WnSj@`%{sBX4;Z^yeOi7 zmZ-#*3%`^cSTA=(ht;nn2TALlk<=1()6|-a+-FIz*Az?S^iw(3R%$Wj+bzrCtX%D@ zizz2IY@D+4r_7>-D>N1?H0h6s(34^H0{;}S#HMfOm-s)fsir#Vam3 zsZlEI3A%ief@e^i|24283#EOPVH5DOuu43AtV$sw*2BS4j`7{?D*YMVHA&)JySbi6 z*R=lH!HR-s<-3TAo%+sV>6T!k=X7#n>{TwI;oL|WP zmJAS~CIymKCye*h9x=0@*xqJ6lfrkWxXcUN@0#ai^0tW0xG4kMYERJ>Zp`nL@h)BT z-1QImC|zRV%YL}|GUr?cn7X~WM)f!#&!wrK+mq{dzT>Q1c;cnv@wEgY6loYu9r>Gr!QrG!*xI z-6@k=E*q|8mI|O?^XuKd!;|{@dZ}_k2I0X`XivfkMIFDU2G0EWXpZSS=rXmC)vrfG zvVUB-1q`>(h-p3s^Y%k|9uLZUiZ+F|HIjlS^Dr&1l#B;!hrCS-2CR$IBIF+_2TN)B zoh%n40~%qZ^4B5qzA?%t@J%jIGM|JRN+q$qg1Su4PC>$n_zlqAUgZRMXyH*Skwc>B9D>jLS^S6@6 zQp$Fl1+(ST@ptzpW?*<`ckc7LraCVVUxsChLo0%wRxM$Z2gr@%l#5EJDm&w{4%{ts zmGIBUVM@PI!_C;n9k@gZcmP*J$f1Xs_tJVc+R&zb8V4ZE`(m)XJ2|k#w{D1jZyRcK z&6ljXh75o9ROEYLm*qIYLfW&&+3~(1$#+#R9!xqAUjk#r3o;ch(;<4aHdWf0e8151 zt%<(uMTVa4CmC{fhaT%@_zNBnFIdz14KR6mFQ(WfE`L% zsl5f{X<@qSS(hrxB*qr{wR2vVQ}ZR?>ewawgmLs>v*r^nS?8YvjDLh3UTm zAFtWyd{3OyUM_JX+w~f3|2Jmp0sa?%cZy&p^GyM&|E6Z(0;@%|xw-{hbUY1>*Yn|- zPGT;ytExf%RQT^<6{Onx?U?s>q48RWvrnNzenTxlOwYFoTUc9PF{ z^Fn`eaInf~@kty~*6bq~nCJE_$GF9+0_dfY+zK^Tc91s;_Gk7ShtSFylV?n!lBUz* zd)uyNI?X}&zX6}KEY_+V1@?}n9~9IX@J0BWR-mVvQtHT=Lt6&7hQ{E6(*@AS1WC_$ z?-Mlo(+w%R2N!=14MM6_EIN?E+{0V4+w{9-F&qB7#8(fn>rWweS^K90!w>_BLb0F5 z=q$B255Zb~)%SW){qU^4BMe+s@HS_omhZ>5wMqmw3>B(B~8vq6#}T=UXX@}fX)pdhHvu4y#| zufY+eU|shvW;;<*Q=*RSZR=0CMU|~pWp3texWiQoYcf@bFl@#ojiHm4%jF>^yZP58 zE)U-B!`$wH5gOtY@lB>OIy@;DmwTYlYAOpSLJ#EXAWB{l(`M^i7hHrPMV)8RhZxJj zG*z%sNI=$gWO9~-?PhhVmcukROZt3qJ4=Xp2qDOB;rcyg_5=%!1A)!g{sD`7K0NNp z!X*irGT}Ed86#hR&Ql+QD=uxgP#Z3U= zxica0+Sp8+!L#;C?u;wM^}LFt`G&8bi*^(ADPVUeoJ9GKh$n9$9H4)`ig$v(<3vz zF~hZz&E*pY76vE>1AEBz4cFJM&!jhaT13IOB{{Lomp>MAq-BCHRk-op3SmXzf?%6l6-%5@?O#g3 zU@1a2ZP(zKEz+Id9`ot5XlBI(lZmTpc8%DJCJWGF!wn6ed`{Wcn~IwFF$uVDh@toU z=AsH((T0)8SL?4U1qFsddDwACwqYge>Qs%|4I*Sv)%y8;Exp()8Z`RzN0E>$$a?*d zxCW9Pj+S8gH7CJce&mNconl_qMDPW)W>&N&=-d1$jHafwRN~qN5R}dz~?i=HR%+U}q_{OVzOl6}-2Q1+U38KYs=ABW+ zdU4_hbSa__uAZy$@U_koD5#{A?3tClU?VwU_O94~qia0Q9GYdfO==IbkKy=+8ZNic zU^XeKJq%{q(^`exH!YYSyGPC-k$>bYcZPma-`RuvKp|9^m%uTL5AY%f$@C2nVBgDc zprmqp>W>stlGsGcf8KorYq<&5fVvjN^&~cAtJGy8c1_6~K6`wXSU$>7T}Ar+&n>vJeI$i5K; zWy94RW}>lZW|FlbJ$k&&foA~|IQLRA09>B*Ix6ppp1<}YVyxvk7oPPL@5k<#Funx7 zK5e*!ie#3uEw6`m+>@wdo!semc=M#rhx6&KGG3a_tMWkLb%Reu_?hr#*MecY&==W9 z3l+ApBCcg+4ufx^PJ?VG8Q2_)4@*x+2JREv)@+T~LYNK<`6PEzJ5%7;B?_lpkm3hA zQ{WadqQkD%mzD3pO5Q-?4#@L zS2UJ3JLHRCMw`?;^S!~!OEQW|#wDz+l!S&-Dlq16fMBinbD?3&-h${(AfRoC8!LR| zJ0F~6nRyogWNVQ$J%PQ?wi@-VOso`{Q_;e+^6TkHt3%uJs?m~*z|qIrD(?jT2E3{w zV|$%CE|O{6ZzdvJ`@Y=#5lOzKJg6D^mWT>}cV;YJ-mTsHZ%T}!{O5E2?Wq9&C)M;C z?gCq-BW<{bvt6clLtHb~z{p>CvPNr7NP3S7>?35nH6%Hj`E$Cnu6d$Ks+$$pr4;4h zX8AQtZivPsRc82vBY3$iS=@l0dW)4>VX&8(pCo^XeDX zX+xBe;dHb~h67f)gwOft+GsUDztu0wR``b}bru>y%jur+!y))`8eSQjY*UHl4zC=Z z+S?=pp__uyz+^)HfRCQ5t8+eN%Ph-(z${2bfWkl~ei*KJH>d~rs@nT=+;z#+q6QCM z8^`LbQF-wuV@2te9^n*5=sJWS)uXq9LArKJ*b;`D(j=H9wx@aY9FWzRplS?te+_R4 z1nM_mZhBovV1^!(Kf~lXDkzDJH#0>NIk=Ko@0x&A9 zmET3A+R#>sp-Y>n&~dZmzFMg?U}L5ajef!2z#$@Q2KqV0&)o!3cVV1OOf{^GfCiba zD!CSKAMZv9vldKn;6+5a{-Atq<94xG50{18(0c6rp(5zS;kXSnHf-qGQN$0}ia4xn z?Kab~wVbI{;|6`L`h5x^{P-uXjd_p$@s0RWN)@PAIG7g`Cmw`OV-G3bVjp^)`d;K6 z<=+65K@ojB8@Hy5kI*9#eHv6h#OOMJMxg0+-se6LjtjmG4O1%MihOq_V>qUa4Q%3A zeZ>oZveH3yW{u)pw-RD|S>^VmVRzG%{NF5UqDN>OJ40mTdH@TV8imF14vC+Yak}!u zH@h|)%3G#L+Hah5yD~BHx0yE;;Bnsmi}GelhvF8qSm8KdDFQdjoy zI?}ZPEaW_lwc+DjYP2+M)$gs>(Vgh{Bkldq{b%UXvw)j&HPsy{J+7OugE^l{|L|4;51!D*ai?>(5XegL+eYa2&q}P|TcP>@n zUKlJlh+=(wR4C$V&<(a9cf*vZf!wmS46*Y12ga|!*+Rn&5y}l&_8xl5_4ZW)Mf7kP zp}H7z)bD1hw{=ztlS0J$5BkEOdK+<7DOo^W`J%)Kux0+4C=|-N+}T8fTvRcZqgiLC zpG{lL8L4LA%klzNFi&w$R>?=0;#j6oZel45SmcYwM9rRkCkSK1@}k8DQlv)t0I$he z2%3~Vikzfd9y#s6G-FpxJc9)5#J^n$*R7muEBWyu;p1}k2e*nOo)6lw@as#ZymNjk zvMkp!c3WbOO^5Gzw|%yP?_r*v^VJC5*>;DHF#4;$i%N254ipa!YzMk33+vY|3A+#K zS#uXKCK&WVNt{)*z@#LE|ghf+W-<28>BFXKu zvnB#a)3b(9Y^4u^GL@}OV-tjOx{DHyNKZbzKx6NyzvGLlaSzw1RW?ksE_&ClQU@{k zgt#0%gWY-_jv9oz54l~p58R8ksxIo^re9EOP+_oY!^rhwCKu(DSN*!14N~S~WDR#4 zv7H^JAIvxwTK#^BzG%$6JN`*X*}=1-MEv63V~nXKJ%n^SyTRVGC$=4lU>E~rr(aq~&}Prv{! z)0`5o!paQ|fs+mfJBnt0gvy}-mxBN?WuPo4ry$z_>tGw0b*^H-jpfu>$BX@EpUh%pJ7`Y;&00@MSCg~x8TQqRkCPaABQc43s~I`F0%KR8E|Z! zq%3WBmY9&paJc_tK3KED%|+j^1|h_BS1RQd0wN%W*DHW?fO{%)X(esHp!D^Gk||zZ z_^mE*o+w_E>I>WZ^OBt`V0HYYoU&na?L^<~4|mJWum8$nn1iIhK|k+L7a^+=+YbJl z0p*|Pf1jD>pIZLEujSoI0SFg;r9UEnK?Y7!Oxoeolpx4yn z-qFl%%*VIU0b@k7!LDi}*Q1oy0b_QYdEG&pvoED4b! zkYTeXcQg)QnNPeaxRonS!ZKz@M=cPo{A^{nLGt|tre5Q zDUEm{JzQwpkf@5k5oFE_tK_^n08YJ8n}X`y;+@qm8O--)h+_ZzxH?K=BgmRlN_(-S z!9-vM7Wc>*l%<5q%t#m~R$Qe}sSDD}BbguWG)Pb|&A!zd@KhU7@UxDgI-Fwc&SXS~`bX5NZ(mJ;#A)rP(h;cy9>vg& z4M1-PsrlJ}{S9NzYwWjo?-HW&uJGSG$AWz==&yAszz)|9Zgv6hX93zpJn<|&4z=pg zF@gI7p2nI1YejR(RucYN1Kuf14@A-ysWXAK*VW++F_?#=tFp{{~il(9z2HwEs=c&+Mkzb<-5 z(=49hiR+%xUD4W@dsL80Yzc`qonCbINLMapqVO$K|I9Sh?Y<65a0%z7pKEVp(T9>@ zZNKLZI`5kSkl^v)9$`zW$oU7uCy#eJU^+qDkGD7L%<8xcz5T^Cl`Q8>WP2jmJD#@E z*TkQ+&D~fDmFSKTZ67NzBNYB%h0;0KJIkav`5!U4Ay!91@8rv^V77=aSxo|Q?5RBs zg<63R((mxkCZJFquivuJN7OSER)A>EV7GT3)zUYZGudRW$)NcOOv226BxJpAT+^}l z+MfFL*VX`(VAb{lcPqBIxJD|cmk76eeW|{nr7=|CwiCNt`UpG$wQ8pFquP;=^Hc<* zB;+9K-2S=ha5wG&KshA0m7au=&I+=By+Sbn&zj1ATplxhXXWakqzRInH!uhy6E}^z zQfPj);o|+@;b2 zeWb?-^fAX~SzKd`^Zh+l#~KO5(|(bqi9vyVM?*dP@6QWP18-9jw$LBWO;>!L zds+H~O`mSM7SI712TE!y*0uP`FVR;A{NL6ul3O=$;vh0uc)6}=kKJ)dewbP_1I5MH z{JnZ&r8pMI_Ao|6%P7&s=AK8@jd=ct0`SDfgc$~)0I*@@8N>#E9N2}4_MJ-zGC$Sk zg=ivtCL>{;cZuwQIOygQ>K$9ppCFtlFZ_|x(j|i9GCh(eL}#pG42)3AT00mI-i?z= zZmONC;MlEl;K>o^lUU4qcHUR6HE47FBCF>d@h@tTMBcx?1?-I(hm`uAD4g1PmcR|zgZ zf?;2~L_+S4ZV^r5bTZ06O|sWjwBcTQ1{hns{_DQw2bOIf^z%k+q4(;ODdU#u*mrX^ z_?jeb!U)4t$!w6xIyR=oUx?JnTcqc76Z?BlSyw!scAa$W^CU0ADG zKglVX{as|deFrmo?}%Q@i|Y@C{d-q2qF~d#wTh*F6-MW#vXA8@EjrW0_(H1b;tNl{ znu{Q29g)S@P_8{kF*54Q88j!h zGv030)mNmi3Mw0reX4b7qkAze_GQ*p!ssXQ?g-m(h}4H!C+%yaNuc87-UURf9FQMa#{cc(@|blX3QU#V!U{n0@l#Qvr0quT8Az8Q-3 z&M%2JM15E5NcyLveLL#A0ZitXgQ$Wp@nihw2TjXT{F*q|Q2|x@r{iJjb=R&=$ zK6?uRvPGUuz*dgSrP5c?1{OlfI8p#+ z?mV)j)3}@0Gkb5z%#l5|1PO#ksl5N32YRj_vfL4$YG&Sey9yBWH(=5K5yW0Q<7>mB z9@&$U=AM$C&0WeY?QKmTegR5$L=CE;pS|Gzga&M-mF!!AWJ`#P-CfdZFe6`K|ClHh z7hC>wJeG1BRz1W1714R$ZKp&}P{DY9HIBAE%epd=f?tALiAPh?q!XPn(B_$+8DGn= z;bP70n7}MiU9^fpR?3lyV!QcBMnC;IiJJTn>p{>AlREl7kdc-LiOn<>xO}QICnzh#uBuQx?qA5 zV4)*2#7AB_pdwU{_?&DL%(zAZVXU#-ho-C7j7wQCeiF)+)NBI+{6ujePf943I}^G+ z+sN}7P}lG`0IYZGojePZP1X){0~141u3~^SH0Vdp-P35wcvu%Q`gu0Ndu5n={{_d< zj6YqvF%C6(!{0wQe3CJ!M1nD}^=y`A1MQnt&zhg_(Ty4xc_n4JD(!Fn^tWJ6MT@+g zO6{#6KmI3@Vn`PiVM>gLeKav$G&<*2r;#A>qr@ei7E3!GYxj0o}}r)UyM#3rRu1kNk7yOr~KLx8c5*Nv+S+gm_=ZY%w(n^{T?(t7p3M5iUo&1>(V%U}=2~Y{YJq{JEmi6)V&eT45vuCs<*IszWKRZ) zjq?G3*QezM6QOy%y5^o4g(vH%wQ-{$j;%4Cy;s;aqJEMB;T<*PVU*J<5fi92;M_v+ zu2>~G92L9mtQS7#EkR=T5=Ds5WNxtclN?PPOl}F1^&6dY9On$LfH~h%pT6%>Vc(YP zSM)+taG$Mcd@a50=J+OCYzdSum`-kDDpz=phT^ZkmfYfn$Z&xAnbbIrP^3QvIr8G0 z&E*lSgQNR0UPkyZL;WCALU52Rp{gatwQPB6awpTV9^t(==0t>8zvB~9puVgD8NQ{~ zzY|euA0q6I}!~x!{!zdnm`2r}6 zL3vdQ$L5%Gn|e6{E*t@j)@aReh<;m|7Xfeghu9#mY=QBWqgens`1zQwrG`%LA zkupmN84JPB#76Ydq4%%v2uZbvI2^s8Zuro1=~IiI!G>+Rj4_AKej#rg6 z;e5Rwnp$n*nR!SWR4b?WQxPCOIV( zEaV6X!AuFII!4iJYu4TLrz@*j|H}N;RvH;GN_bBcuTS{P`4}qm^yPZhsM?Lc{49}V zbQy4xq4Cj-d#6KaZMZ!c}+!D>wE^Du{(=6|8^5FEY^7>Zy zC(QdjMw)|ngJ_c_N*4>KsJvg2V=Jwj*@{=G5D6u(kd)chlWEW-R+0woI#Fd%@+z~c zl}nRI3cU*yz$3Ie84G8$tuB`We(~PLN?7IA8PSQ4Pvg$agfbQ0)TkJk_R}ZXVd_cz zJ6zQR%&IfY)}kqx;C@ZqEgEBV_dcD(i$FW}Q=gLd;<)G8j`6dS!$A%MyDud&GO+A4 zDf1lx5X-UP#RF7?bR}dIg9C{$=g^?7S6hy%i-CP?^SDVi|IIwEMSeYjrt;g0fj+U{ ziNT^@@O3|&%E)qS{?4|~7^@7a`0qq-moIo6Ysz}*zYZsGsH4#0wf~t6&40%Hlgaw; zo&!{$^F`D;=FWZbo|?=D?<_h^%egIyaxQLgW(9~otvF>ef~ewWE_nuIucKh6u(W5j zmevigC4q(Uiy3U%=qL?lT}|1ZeScsZRsv4PBBgS zXwD{kWE*4Tul=n~!E7&(3E({xc&oPm*3Wr$wP}J-anvVG4yGo)kF2g4rWTR8%#rUX zE8R3CLoHS5^#%+%V(f&V<>iPB9!-gAo*+=4p&JuuCKWWzmD6WYjnTPb)r!cH=+j6X>8ku4pDp zhA8~F4``H>aU0RjUWU4rjyAAXg(oI+j7JXvIaYaR?oUMEq$~U^ZBxeSeUGh%u38(_ zL~v3~upaE-;q&rhrI@=|lMtw2e6W*0_sx#Jw({D;Tg@DM>*R%v{x1&qTXhvhr`D zYy$Nq3)lSoqNcgSmbax?P~0nQ5$zk~{qHWH_D6GJSrGyvo*ASYxUa?9jP4NWLnY&z z;lOC#a8aGNczc}(^>myR~ zv;zP9^a%OB{`s`v^%XmrSrau18@cqL_ zFgHmlFbJWT_WU9s|64T!t^WEo;MD3()ivWEu$$6<4Xe&^6u24XP*O2&w(jV5#*Qs5 zEH{0wcaML2e;|zK*^+uAvl2wFI^qJmA=WiM9JYno-A-^+bZ*WS3*dg`+Yb5W_Otr+ z@h>^Q(5o2&@;n}^r;BGwPLjw6q^Uh{ZQaLaJHxPAU_S1_Pyi@Cn9DDY4%002qwgkV z!L8yEJ|-I;k8x5=bxgL5-YZ&Xu>i4@`RZIB!5riMjWg33bs6D|T?I#7RcK==MG%^H z3F{Hq*h{NYr&>YWSc$d4gDziEA@L6h@|w~)*SAAd!=K;Ec|I~=uqmVTIg`Tad4PAz z3A3ETi)4fRE=!%vo#lyqCsHM_w;aStU2?}~u0%op((K&=!)CY4p*!uZ9+2%ovxV-7 zBxTFaGoG2*#}*d`Fsq^kzWek#*M|)z9hqMuIZAuR<@I!Q3&T|VUMDq1;K6gtPY1JR z%v%j(4!MA#()fj=XuC!MqY?{5GtXa6sByU!tOKlc{`(etR9%A&UTvIY{KsnF>T!7u zpzK8oIi3DX8wq|py;Ivk+1E2h#2ep@uK-}(i z^*z9|8YTgwc1!b{juA&qhnvlwiFwLEJ)zR1m)(NR!I7AEEv;_L4atFYLYamm2~#KN zN5}^K5PP}7m@2>gDxak0l0PKh*{S`{QqUZdh8;3EJ2O+gc$-KD1RPqWF2;GFQlSnpU+NgMtM|0`XU- z(N45QF7vK85f#`HfOrF?H>Fpxbf{^}*{$TpNtTwNEF`>lw1V3nhhd*o_!-{^-q1QU zkYq^lS%q+XS8&B#>`j*`?wAMZBs#R6QA*aQm*h;pLMK&ZPrtr!Ym0L~vlhCBsvi@m z85@2>cqHdAXRfjaxBe0u+_UL2JM*s`zB%CV*(O_ZOSC9J^c;Lmd+J?8*`uk8 zQndwT|7s1AzQ5=>h;jeVYDKsBEYwEQvv57v%+C*9 zzEw=pHqLvU*TOx|OPO-openUbSF0Ou6~}x*0m-uh89n{GhQ~sgr#H>(92Yb^dl`rH zr;WN30?Q8>^m~(TA&j9^m_JaliI%4+VoxGzZ9ZiF5+$6dCRu(cYH`s)s_<< zUvuw!Q%UY73XVB73m2U@!A=g7zJT+deY|z^4>~V7>7^RkL38~-(+T=#%s<`3fA<`C zQh42Et|SIm`xaXeC@viynHveyx(|Rl5_YzshkU>MtM_}$!Z|w zuSM<+R^RKk;K~1U4)Dvk7KzmQWG8IRPdFj>@&W$#jan(QON^28ln27|*mP@$(^(3F z5R;V|9neX}Ts;)1=G;FvYe}oOs_TifD?5w8GZZG07pv;41~=S!01GC8=N{4qcz2yT zp>tFlxTfh_JizJa9rLe!@pUq_SFerEfuyH-07M2h0boUIGz?#M89m@E6|Kb>%T+~^ zqE80_eodLt7L)gr$yz%sKVinu6=J4}A$Jo&=U9jt6&JF)lDsKS&Rj?CVtf?k6`v4# z3lFRdYq2{ulx)lrI)_+JlD-Xy-T1YCElhK__GrmqIb;x7dOLtJji(voRzmpN_6^Sl zOpcGSy2yA)vd(*)#-y1tP}jf#8r|EgX4^f{%w{=*O}kv(&F$?cU9l>fQ&|Y=sque> z&QdT!3g(}Ybnf3D@oC$ht0Itb;5%;VU~QAWA}XuEriX1-Efungn7Wi{>tQ?ST+~PB z2aB~ko3)D`4U#)k^c&XPHq_a6O7&Bu&goX|sP;M~qZ?UQb~DDCANm7K;RM#YW%nO2 zWikj!89Vj>XzDV}%^OaCOPKGIt40!C;XsIQYG)uW;V{1SMM;q67$@i0T^r>Ax6!}4 zhQZd))a9fwDAT@UjHc})vYU&)pvupSWY$*q`VdoLn`>dLBhap2;u5mnQeUsW*qaT_ z68KDyo+G{oP=77GQXTs_HN_Jn$f*-x{Fk6fbV+sd5p~9C5v-h>dii0i-{pkF6sq&C z*~=>J=1xTsby#`cKrnzr-cM(%Dd_sVwOsbnQoxT&xvo|JzNqj|q6{|A;y7}yoEzNm zsrBkG>A^hwo{Qzu8fGYNfQ`;vDs1pVa4V(omNilX;_CkA*$u4Cc4z zHJR^Cv7i(U{YA``IdMyY{fufbmuze?Jr$>t-5?}S;}R#cmKvfDDp1BVd@!7FX_{W775CtulWwmHE%yWF)OYoCS?;O6s(bg-^ zx(5tGH*bK`j~UDnt*M{jTELqHGkIFUvKbukTF$HUWL7ler4+57sH}vEkm7 z5Z>DbzLdoW3QIQlQ2spphCj{n797q6>UMroa3q)zCg^|_0a(Yl8_|_*(?>-vRA^WA ziLD0;`8^vvGcy6HO_Z(Y*O--ZoV`f4nftF8SU!i^sV-ru)Is~w$lB06Ngm?i%``xN^wJ|KL%LBWL zOtwA_8mg;gAK6>#qB$Yk%pn5Qe+5wwKqjme?E$@|!Ja*Jr>nUtKwm^_6}XgFAn);Lb_Ybol8uJbe;*~4o%NrvBd{W!}+`)ihd3eu{lw`dE0b2nF``^W$cB|!6; zzDX$d4kmR2Z#cuQr&|#MAmc->XUP7ndk?b8e-Vuk zd$UvHy+fX@b&qEL%*TZShpx&RgqZ9(r0tU5u6h>yysaAC|L{O6!zcL_#HyP#uZlrw zxizxsTdcExIeEDW?86W2Mq)Dk8-_v39&&)&9~@bIGTnFUBYuRkb6(Dw5H;n_O-akIhi4-SzBHuu)S0<-=> z-%@#p;i}aw=|6kReZ2n$bV(Jc#xHzJz4LzU*~95wEw@^h(m#SGJ7P5r9^k7{JE`iZ z>tr6xEI4>-Dgt>Rp@^+%ck2?f)4rpcF9Mv`$CITN0-bjWwoI1M-%0BB6n2e{zsT2R03Du#&4ZZ8 zd@husHt**2Xx1$92}&;{(72*o!yjlK;;`bZgqM5uo)6{AsXA428?TIF_~c--QaMS! zY2CirkIG^w?wPfjF9D031IcJn?~&hI1{X!K-&4KIIn|C2K|@wn#bye#HdlYwihWB} z`l)Rl(l#X?Kk@CS*0mtR0LyQ{-c)_(W1hg-sy`<+jSh#1eTyKg!m9Izdq2%Bj;)^| zjeZ|UbE?k2fob@RKUG<~+QsHx(Ng}Ld$kLu?t_6Pk*gi}DOI6L&m;M&NDUGE$ z)jiST)&H28|Ho$Lf9mr8Z(Zn?>;|-?Q zFSInxjc%joK#PfpyZ*?tm8?}XhAT((N_mgX^?TjQ#(&O3U3HeLSj4J0sML-BeEpyX zoAG~>dqY!0yb*{vT&PBI%z8KtzKWn%MZ340P>mycLv~8)J=G{pzR0hJsf<~KC2a{<=+ytHd?CuZ17JE(q3wvML z)K=TJ8yre$i#sjuo*)HU++6|$DGmuzf)=PCg+g$L;0__7Sa4b_cqkgYf)>Avw57DY zcb@$d-q|zn{_uRrOy*jWnKkFE9M^dsvUt6fH?)y@$cSu?sLc>{l+YIaQ_q)`J2cFR z0nV4U=Ohs6ni=zM$uZo#4KiX1N1EKDNY@5BnR(YBc@q$CEy1s%w_|}le4!**zmA&- zURl!pwa-ts>PmiYwVB!<2H3n&t!Y5qfeno+oQugFzCl9cqu(+!%0lvIzz0U{AZQ>v zi{pi@B?Xx>YU4q@N00dILKn;Fh91oz`bgQhNegbFHW!?rYF;HQJIgmh&{!#6D&lm# zCk%2DaFH29W;jN;#755h*fM;7Szm`VqP^cvipS|a^%YolSU&|1laNLiTc5{|mt(E7 zp}?uHna{|r6&d@~-}#CeHF#(9cGa>1FBfs6W`-a=@j!)hqVsnRY2C+4hr~rUXGnC{ zavA+y$m{5@j>x1qS%DDLy#8d^UdE|?g8H%bl0&aslVeD=-Hx!REkS>p$LgGT$V-bi z<7CXxV6j{hyw!Ol&H^8-4`BD0d1%Q!Nh4jDp#v`EGZ<*Bl&p&}bBf-rr_DJ&Ie!!& z7U7@I#evjMsW(=2Ys1tAN9-!A`7}HB`?Ki?EPm5pW{B``u>eXmRfjZs+3`i+14K%& z@Wei%i-oKdtVBcJExzG@ z%8cJM6)Xm*5U`gXqh^ugh^3F_FAmx4zGD1xAdN9IqAy41i$5cZ%w9=>>>q+rW&w8Y z%tf?p2V}TqkeA{k>?_NxyKT>-y;@yz{2yTzXgO|?h1XrXgrm&!rI!(9(_+6Dn(ayq zw7?7&odS#EK)d5oe;vvsA?O5i4Dc#VZ9~Nb%&+=d(90%tzjah6zeL@U#{a`OZw0zxH@*JS(tVk&zsWba73zxc&ylnR=f%nX~+jE#{AZ_m#=fE3)s-`GDjtV*$(SKHH*=08*_A#f3 zjj+1#p@z4043?apGe$4lKu%7`UyVM|%b{SsV~JGEk#&^D6)OG&FTPqheD(FN)5f=w z4O^0L_J%*5SIVycnEfI7%A9_`^1n0Q|JO6#|K9I^R#RlhHu=cH0H!@K2K&8mFuZQ`j!M)=w>OW!%E9OX&2U@)qDa@ z@lmn%W4a~9!Y7Ax&6)2I6RP~;#+jTz6=M#X@;|W6Tc2Ua$r(w6ReqWDm{MV!FC!mO zG;9$p{9x#|&a5WV)os+W@f@pa3*doe>FNkf9x7FC`OGVpDF86ytWfG}-&>l9BCYb= zfU)a0h}K&KU3vL@v&n6~DiSGfVrXren85jZixQ)1j-k9cm>=PdUjxPD*-RHd$YErf zuS@F`Sm=lm03p$i?7xq1CqNQs#l|4kT?lyGCR-6WR97}ryRbMu{gD|g!) zIn&v-7FK$Bo^tv^j0O9uU6fm1Ul%1B^L*iB!v+!3z2T!Z zw`+I{&rPLt3ERvC(^K%~keUenjFW~r>)LHX#@Is<<0T#4JpXFA)k(N&NF%+`y=&PE za|&oJ3vGKi3LHq;d$Ar3{nakVN=JlBxjy6uKMrDE{yOrdF!kw3bfoXwYTKq`|MNo# z({^9_hd0xJ=Xo;paX4N(-%NONm^9bpdLX|HBh(m9QPUg<01h-a@@(YoRWbNKmIfvH z1`D3yIk~_msa4(A^(4vud(&KqeqbeaG*2$SYG9ef6^bK{8SowCV_d%fV)R!FKRbX5 z!Q1A3zxd^eG-I(atkPhJp2F4u$sY~-wB~NtSM{Uu5tw=vIGZeCU~&bjnYD<8(S7+c zfbK0^+w585u%A#f)+yO*{Xs+w=eUt!1M8kx7?KceT@gDs37A`B1ZJ&!Olsb#6AkZ z4w!j2vppkLge{`r>+EaSF& zm4iVw6$aZAuE>zG?%H)9){fuBTW{$U95U!N#w&-qDX{r##gD|Ua7hU=aEFJwNSC!V zWsH-R@t=VQ7H$E7fWDxBF+=~I){R^elaR-&MW4Ks8!f(8Zg}YsH=b7W{$%PEZ$R&# z)=8~lnBV6G_1&6H_+svYtW4RoT+|`qYd*Oz;aj~_U<89Z?pEJ!GH7sll3AdKGw`xc zFp;{nzHVp?<+vlRK+({8&nd@q^7wU_8Cj-Dw%y` z*3o?#$2;@aH=Jp(k%{k#Z}JIktHxCODALtlFSd32i~7t~=t=!h6|JR7Bffpl6aq+tK7EX!OA4*qe>m(KBC3mt&!!h zGlkbCE~~+mwbCJ&y{>@T28z^s$wy|?@36%}t{-z8Gd~t2@7;@9peu(F<4k@u&MCR7 zGsh=>faLnw63`LGV|=xCkPIdWj+nTkK#Yp5pH z?dxm*l;{|GZJG!mwVIRJm|6i=OhtBcyDc?=Eyg%J5NA6rls)mia;rsM8>aJmiKu0( z)2FJDX({D{WoW`7ZX513z!7-_6fp|b6uE9RI=(*9_g8Dkc30V}qY!E;@(Zh*VT}*o}{0dIce%2MAy|&xrZ>3 zNPkC{=6pd-9N2t+Tf<9xpw5y||2OCTTKa+Yc@>s3C#;;-AJvmap&goJ=58LKMM%MY z70ogXHMk2)eY@FfWkkf0HQDpA8Ju})4J=>cykZYbB&uD^EhU>O)o})YD&h8}_IO?^ zbq>k;`P!;Dd*f7-wXIn8*)f!h70*vuybEPu6>qHr!^fzLAQuaOo1L)yO77zpAoZYeXx|{|o&FQ|g$9p4(FWrT2aE>p#fvz>a;p z-9m>vp2utIWOyyR;}W5hC+CyEokd3pt0|tvUbgRzQyghaa+SY`Q(fm3 zIcY-!ww-Sf*&+64%(R1eR!M%Av0M zYNL!71)@`n7erG@fD1+@z}v?2UU3Hcr6mCWpFy}81fXRMyf6k9&;i%r zwTrD8gu3z?n@tm8XoDx8O0=xK_a9H6>1$>StKdbTg}`%+?hdaIntUzS&}-s>rRY5~ zS=8h%vP=va^9-hAw-}=V2^kSvvhy0i8Sy`bgfpky6w>a(+Uco_9$`aIrW9bxd}6gF z1D+G9pC8cowE3STJ@3Nla_ID5C#@srN_AX%mFnOu^Ir53FHltX6`6kS(A{&sa(1Zc zc&rA*xm&KSa@~dGMR}$M+aq7rVwBjo+D~hU(S%EO_=1HQBW5)*yZbaa4Zeu1dk+`q z-sZgKX-8A|5(MD3m!m>l9JMX%&g~~B)8f6&l>_#vnjW1*GstJ7uOwb9fTaSSU#;FJ z2m5B!`Mx*=i})Xlb0nl^K|9WOHA~w|pXUE<%Ya&L7XF?anr~)QxqgzW=t73)hUUvr z4KQ6^yHABwQy84~jjjtbeLW_XF0KOUnWxqjLLNmE@YrKFyxY@03K@~9H|zn_^$$wc zoZ@O8eq;Z%bD-0FF9UB(dj`YM1TWs&-`9!~*LqB?=Ij40QM zYxku3-t%!Y*s)X-$IPH$%JUJT-lD|IV%TKLyFb9mG+~2x$DHC>K>W~kD;G$4!|8eT zC2HRosXyIPEXg7=s@c6`!^>fyF#f!BsU+}UfV*;f@ls*hjLH&NOZeknL1K75kL%6i z&-`1LFT>!+)D9y%Q9CBo_xG(zFSK+bT_E2=2%lstGRpY%)q3P?GaZGRW zhc>$M1$P?X06AsS)~D=EHJm2090c2hEk{Pkywf%PG4lt95rUzByA|#G_DO10E7Bdq zWx0}Jgm8h@O&O%vQP20Do4A6$^ZAbHn_bG7JK8KG<52|9KdqZtL+w}Uo-q~=5wX-9 zoSJ8A(aI4a+LCy~5dC)_#vseOtW7ple09FM@p79YWJTDP8~J-ISWL*aKuqDY#=z0G zPO=&paCY!%Cs`m9Np@(3!?f-e1zpu3l=3IFl*%nkc>R5Op8#xC&H7ZAiMXd49gv*J zIMx+(d;v2`m6I&`kGJeZ=(Njiu+GR|_<7ESt*c`rcWe33(z+76zy!p|hl98Km(5Ys ziq}&~bt2;HTGbbZk|B|k1h%3GoK^o*(WH7wDW0oIT~i$vX1u!?ZA*I^wkk@*hibn2 z3sZp7W=)6)zO|lo#@s<(t}2w$;X%FYAQp(giQ4$3ipY9*~7%d_Ad%ROrH&Sgel$8PW|@ z1-R8*fW9gg9c-it6isYbuRUR}PaUXcOqu0d6RC*`&BkbPZ4*i_f(Pqd1RRub3i=jQ zF!DNXSnAFQ8D5AyoRQC)vTdh}ssd$e)#8p9>@yQ>@T}Z1=|{~P`hio~HejBP1i$t- zVz&xybYytTN0;c30w%fK2R1n!a$-eQzZU2n9|c>%SC)9;gepP3$mlk=3jeWw6$I|R ztX^bb@tvzun-7uX0a0`CAml43+Y#~qrWa#zPSg!WpV1LeTP^)ulTnBQ!-7NcQhTm& zkVxSl=mr?Hlc}a-9l@#ws|XL+%c#P#GAG$J^0b#pV{~@As#h*gS%k)~ssgW2yUy9} zvn&#)Pax)>!%@BW3CQ)l=$4RiHE7LYWgbYZkwc0_K+=Fv+T7BRx0*`gmUBGW&1`LY z^;WQAeZ2k=?!-FDr(6gYnDe92Yy6iLU3OUIwl<$xF=YkSb*F&mV zjZKUR##Dwn?GG+1KhuhXl)7;y+%1=N?c{fI9P=!^`6_R;$iRZ>%rw!Z&@ACKWR4S7 zcoi?aij28?<9q8#XLJ4E$;C1c7@%2LF330jXVdCG*k)qH&*FbH+`WmPJ(-7p+iJdJ z?GXO^%IaT$dL$c@`hS04hw1-^XF&3uHx5@O)~4C4$+DOxs2~gCYp2PJJuBk=wK_iOlN~T)b;!+Di>gGXhk3tXes{TR-BEq zKw}&417t-@GDqyGV=T3yM<(*+_;MF|i%1_t3WTjM&XQl~H~uj3+A!YvY*j76)PkA_ zLF@AsBp9$*g@3bvbt^@Zf2mzx%Q|&slu|;`SL~8I4cB-)d5c^^FT}areGY#3zEF_b zxX1|0=W2WAmBpS*83-lRxR8uDN^^Xq`y7I?l=HAm-0EQP2;gg|9&*0CDGD!$aJfVs z1Q3Ry#PLq6fL)qK&@;>8s|kLp`WDs? z7fEr2#Z8iI_>fM6Cx_v+8*->GrX~;Bhlk+yZIk{x5*vtezNn-;QxY81zI6l6Tfa>M zhs{STF&FuV7bWOyGYzbFEpluc)BJ#m%iTLcETD9lK!4;RGQwD zQD_gJY74{?rRCkXt%M$bYH^WSY*TTY0T{>dk67wX($j4A1Mt&;EN!u3j+q;-tbyq< ztSr3)EXx_c)3Vi`r4Xyp&IP8-Wly(!@N3x!9?qo&-qXj?ZC4?wMvO=mULxhFu{791{IkL}ioLsiw$2jJZ2{??oT8A{uJE;#7tEDR~PgD_7 zvI)1(V?ZlT&jiZbzO^1GXwm||lR2-WOinz!Vt;+T5+o+cVTTEzl_#-(2>OW}KT}ph zDK6h-D%6bpUb$1Uw2*YSv^d6W;S~I{Js=Vc&JNv&3dml!SU}UhFH4LRGwKW&-I(;0 zZagnUK{hl6%-VvYwtB?NJLwr7U3ob;Fa_`Z9`5kzLUgdW9}j`=_hE`?+c9eUWcLF%sM;xwwVfjdVy{RMs-3&iP7vP9jyFDAR@Tfh8C|Yng(FLeJk&WAE&R? zse1Mv7c%CHgrcOjRA1i8Fy;XZXZD_7cKna}Go}lH=k=#zMvYpkycwU7EwD!JxPhM% z!|b)u`#j%--Pr^5VC8Eyi;8@!8s`&(^;i{}_T>B?H^b~=E0;yL?&RqeqHS6=W&Gv) z)me#u?iZiKYThR{-OmR(HNBiS%+3%?9bYZ!v!b6*^-~&_sYia}+7E@N2Oe&km{A zVi!Q8?SJIKZi^Qau>E&4t4C(^J!$Q%!Szn=Zrh>qH{QZ$woubH*>&&V%(H*{*cjobvcSog=CUjK2mMydN++VrGu0yxbh|Cb3ePn-88Fo_#&$E* z8i2J4j$Xolbk?_v9xe3AnjRLIo4zG<86f!51h(#%;teYC^%&H^+dW&Qg1q5dG)X*V zd6Ey3xg~yUZdG81kiRCbfM+t=W~yUDX>OH#&$RFG$y5PA5mlj@0Xm8&nz@rLuiV_y zCB@#X^&h-kIy2pBiJCGD)MhUG{xX)~H>Bx{r;T6~eO$=d^Zwg;~II zZM=UpkpI21Wfhf;p^%AZW-t3#CI26+nokb10+HNT5POV`H?|fHp|x>ztSt^3Tvn== z;uW2>Y4nY0x7y3J3;EBWOKjYMt&wjz@H*Zh+3Rnv#BmiHaoPJ%J~xG7hI@UZ$P~gh zlr;H`NXtkZ#tchkh>!Ruasj;aYc6mXC^Y&}ti$9%u}syox*!e~gvn6qR&uPyPs{qL zAW=nxB0|dP%_M?CH?slek!BX@Cc8;UMvTS|6re8R+3S+-JY2G0xxljuRfS#IwB!^8uSFu(;iVps2$UpdgQ5O zmUg8-sRmOhj+}3HTwql?GmaWiA=r11MJ5$@5{;<4YoDqe9E4^QT9S!8^-A|6!@*#v z_xO?if{j4H!};tp)4`yDY$5chEf>KxQM=AXKMqCOFGWaJ?V z-~=#b$?F~816fLb(2@=*+MU~%XJ~x1e5+IPnyqrAL)jAj04KCz8HbQC5aj1@jf6%@ z@EX*zRC{d|BGun8ngG3!4oplcN;Q(_i*!O_@`_5;q;1@bo1-NuDf9l-WJVlNcVb$O z&I8V_`2?VU4XwU$fQitynYckm%xwv%A_p8|(w>mG5>UI%ECcbqrNhSuH<8xgXC?!Q zGZ{KpWvN$IN?RD^Ig0`#V^COhY$1O4}UBZD7}%#y{^27c*=Ypr~Ia&UDPf4^6e&j-(!RQ}=XroC@C|8CJz z1>T8qm)Huw4FyacmGQ|K;5ecrhbci;PMWhR9AQa0sX@@&{TTI1UIZ@2yxGcuqi;3w z)TLM z8}fr{bpS@LqWy_2`^={M;RO~jf7(ABo5R*#o;MDR3qTS#JOQ}2Pp5-#DDX(nYi(*W zTepI4g9sWOQG#rPICHLT)sAm$v(K@b4;b8r8!%DVV0P8wAYZ9H>DCxgRSv)`Vx{XM z@xUx%O`~l-*-3-bO!%Gnqxp-FW9l=5m`L*1lVqi^j(){e8)Kn7-|$cSaRcZQL*k^{ zOsSe8*+~dt&jP7#D7jFsb&F2jl zp9pPOCv&L^E|VEk(ZsJnBM+!{YbfZ6(~Epm!vY9Pk)K;DIIc9g*PVQm@uQq4qL%V+ z8J`xLTLexIE}C~NbAYT9+U)@IeudnAXv_Zc_RcSx-igu(`D}!P&&&C+A4Kko znrffS3FN&$!}Py^1FpM~@i`E~drL_fAJ|rLtjx)?6T9Plak-Cj=k4vHi+0(X^vqth z8rNJ8HkF!qDVI}saaq9a5QjTD@RqyG5*|tu=;W$c&ly-dWUJO1DJ9NnYXzKiafVg1 zE!;hOTUnXCv6T182|xRrlt_y}!LBYvdIn`HyL`5kYzLN>#N!damoDOR zzt$u8)nMvzrij@O!OY^xo7)p3QU7HoVOKBa!0`naPb zav^@986EQ&6v<9f4$~gBl@j~x+8nv$G%(kzc5r8BPhC{?uqpV+STgN0!9h_clEZbe z#Eh~*we*UN>qmoMby@@}^6u+DzsZeM68aKh{Zy^Jl!iR8_!M zH+h!E#^TWKSg^&6Wztf_9M4w6W~<^RTDs$LL-pS|Q9@c!H?5pQf-=Z$sp|DP-2SBA zGZO5Hzlj_5*}tRJlWdvnLlSS=Nh2M3kr+x7{sR;B!nMjd*oe6E!P+=imeK`-A)gDs zBbQjWYFcwl=r!;*Mgy?WVtUI}Fb57LXJ>u0xZoJu{>DvOW^RuooeJ1h`Nefh%ZT`$ zsf#!z7cPKqLE@MS>)>`B!ub(NphKxv2-ycqnYXQ8%x{K%T@9@Icm2j}JbH4F zPXmh_04K*M1J25uL{aO|FT)mryKGzyH5VT+63I9X1Tw!u<1GoSw|0LEpBa zkaE3@&tO$Sbuyh4l*O1MQP)H|M~7?3r(I~9m^#}{?0e8D=%=ko8R^&yjzyvZ^aHRG z-!hT-Agv`_rhwCp!Bq__6`?+s__nm=!_5W0@VUU-02T&-juH+I4>(Z=hA*8 z{{r%th}q`L*0XI~agI+$#u3i~HnRT(NE6X%xtdB6=B`*JLNw$5wM6*8_xrEx{C`pd zQ7cQ2*XmM@9^)Jl8yzr@ZkX=!!?!25{{=AXE`PlGfSI!D+w!3;qI>ZH(#CywytAy* zovNzh&R#AR06M?BRVP##_p`CYf4{qW?Qd;#_hPCI;AW9Pxc7=VSj9sPFEh-0>O)&X z-ZSsbdC*u-^BB>c-;q5{Ce(5HM;Q@oVkz&nR2)+r?9`OvF6f(zZ5QXmgdv@hbrFjr zq2K4KgqdCW)`G451w_g@=4>}a?s23RF||HgOIE1|NE=HU9>tim?nMi3A}x%$<&7gc z7F)8iEmYi)LK|@Hg3(}0xxOqLchODb?gF6*6kT{660m9i&bd7Seq?{o`T9)&Y(nWw zpY~mNKM;RrpY3?%YnOqAM(hTHd!KQ0|_I;8OWTfpaU)>u~X z4Yu(1&NTm~<)`!upEGx<}pqkR}9Cb-eE5ye!E zYoQxv0VAdAO^)t5*1IL{tK8NcR$~&?&~Z1z+-UjTVRBq z3|tE1cCxfd`j(9Ygc!+hj53`=t$|dfVn*85DTdU_)um5B-`&}3Tyv$Hva|<56uG}7 zhGdqk91~Y@eG0+$k#2>;_Q~Njt{cZ7iFQL6CP$x3-qPt~LXvf`B_EOf_w*}VCw3bY zm4P7lYH%=8B#Ox0<5Y^jH&hgbp91zO%DdJZEujQiPf0Fwt!)wO*G4+ zHms0SavK2W&|hcfU80YYX9Z-pYe<67<`a^K9^p-dJ$e6H@dx|Nadk~U?snEZ-Q1v& zBzJGFr4m$*@VK~NkN#u$1pwc$BB6rFryY5gd24xS&~*9uveKZc zn!b75(Uwyw!#Uw)VXcp%iR5zL7~*MSC`CuV-J8g8vX(XUu%^&J&#i+ABSN{aH zIahwP*nyX9D(Y?}1~kn0*GYdMl)b8#$Y6WJUVhu5aYDKnF>)+{`-e(P^Y##d@W0tYES zU!R>j@5WGj=_GnwAVw0512vffi&v0C&?SoXVccM9ZJpe)3Y~{9d#aW+&`x)$ zscv7JJk=gD^OvskvEg)Qm+O6D%69*!fG?vj+mFe^Y~KcyPX_9Q^{_M$EuZcTbcn|< z@k-O`m(_TET}#Pp`%;hwUCEwp66kT*vV8=GB{YO8vnQ*9ahT)qFaW$O4I9(2`68*~ zWE_ZQk=Y?U4bRCb!RCgC2y~H`Ue!glKAVaq0mTcpOek07Nca_fp=*{YD`Mty<}KjP zeoRGZ*$QS13KGL^u|J6{wh>@?NT+M$n;6bZks5S9Arb6M!JfzL$gPp zUH2>%ESV6e)W}@Hb^CUR&qA6Y$kpFntq~;keJTDu&T9+Ll=;kFH_BQbIzAR-k*aMS zbVMp8V&NHF>DiKUo8FYenjw_5aq!jmH<#7DOFD%Edqv5b`#SHBeM9$s>OYyhDQDKa zdrxinwY5+$hEqN2kLsojKbaERNjv_;Er_NLNEL*!vk-!9{`nH?^}1G}*qhl&`d-sB zRPx@68cH~2Tx@83tG?z}HlkUIIUyL*s2n^*ZyEJF^%CI{ZE7Pmn}-cRd`M z_H`9(VYZQ*BPIVaXTv3w_-3hhqIlq`CeD5)^KjwhtzNKB?ALXBz>%rE=vQCCCXK&h zsc%VNF|*KC*%y@Nphm##giIBL*~!S$Eh3LOEqW>wVN8&Ch*Wv;1h(Ip~ze9;6u`=WI-&*H33~7oR z-SZK@^$c$9o)2$q(R<&|LW1EZJ12%1Nv`%O2XOviF*Tdtg=L@LqaB9#che?R|y2LIbNaFyQbjwJ>3qZAhVQLKyd8sSp$7M4kj8 z<*!TnGR|c~Ft^%i@-G+)x@*|$i8}SKjh0e!iKr0NIWj@+KIuYb`8^&LA>HV-@K;@; z>au(Wg1E#z?)&5W$RPH9zdGk*AC+go@1xR1X5Lh^reeSK-+`5$^IN}d3>`n_*n69E@K~v zrm9^3neP2DGVck~I>mK0^ImE_%l#asYQhqhpaRa!tSYdT433lC*or_I0Ay zH(N&kV*O8Trv_EQOlz+#n^gvW!)UH3M5BrDS)5MZrYXZ@N zcPIA;1%4v$p?Veh=`TSOK|_3P+t>wczXHaA>84EB8F*mfi+TDA+v$+KvnzVPF;_eb zZ8q=u+wk`rcw0g!3)IGcefJFU!QXh}cO7DEF_f|`!+nowUd=BHFmmW(SLsT$)T za&D2^C9@52DlJLx{xPoD7V-R1uk7It%QoobZBVPjzFIw5@{)V$PL5xOIInA0$3X@~ z2+=<#XP*{XBT?&XiIY`lfZEc?j?~paKgdwqm`yqE<&kvpEA90xEzF&S2kbMEWHPl> zD4*H37cieKtvv5WtLboT{%inZT=P6NA9LhIA7jU}Vb>u1CmlYAQ&3fK!`=Miy~2>8 zC{taf8W;KY`4h588);sTUVg4Uv`+AMYftB3^A&@4KU;_@6Gb$E^I2F2nN@{Kjo`Zz z*Te8s?)mrw+xcToc-)MrotJKZK<$3yt?@7~5{Gh3RCnJ*AS`u}lQFQyHLastW-R5K z!!OWyNupJ@BVwX==c*}A)5;6?Yp;Q%p&oz^w-rYz93Nv!OfQXi*>S}rzZYStQjjfMnrm>A{;y=<(?3;-6YP84Mw^g5=wO%ve~zIIBgEL&Okc3PJ7lTb5>bfzKS_iCXY^ z+Vf@1$%K(53d>jLn>3PC4An4}~a;Na26hh8-a+DUzLgl=4rT`OwiOk;g0HWMG#F|pfnWP8OK>SJ70yg)xX{M>DF@+GX339tC+ zv83B!W*IhCY47cr!U|KA6>0y0Xwa<}8%yJ784uX^P*X`v*@Sw&`!W~m-28P#Fc0T0 zR3t>=;^F4y39=GY74XhfPRutdMm&Iu9YvZ~2b+a)ZnM4SNZ9)#xYn_sxy;;BKf!6& z>Jfm0o0}381xhu5Bx~zK&SO8Sut6MKKt;gdm|9~^=OQ6WU4BNK)7(7BL5|r?~RW^c<9K!q5~q6K~3>kE^I^)8*fjPcEzB8MRL^jUxQ(UP2 zos!$iPJ=oj9H~Rp26StE#@cIgeqVk&RXX4~)mrb${Ibp49EN-I>W%O9`+SNEoZLWE zp?fP@K5=*ucxCGExn2e4PvAa4&;ad}r_K)3dJ}LPbN~?Zq z0)L_E-B7bG^%hgG{eDZ&L>#&KS>1Rz2V{&a@GlR+Obme>EJH~Qae{@YXke_aESTJ{rZxnyhOD&T_*o+aAo&3ciM^J0!t z)whs$u9VrwThSa@pDk6d9(3}wk9Obzwo&NtdXAHR*p&TV)eRMpwK!3 zJlKRHtf;DI8{dhM%JVofu{~eOwEGa$5l7M>c_l0VB~66P&9{O2Gr?qOcXoztG259> z`nGu4VrV9t5|KgRr2Cb}aZ%U`{$s7XsO%p1XxknkIlTPN(2<=^;+JkzUgR%Qt$z5h z`Ni&vL!9{`1Fh$z8VgzXJHmr32Id6cjQ(DUoj6=@oPGB6ngP!AsVZ&;YDY&Fe(%15 z_@l!^(wB~2t*3R*i@h499QK5gNnYA}^2gyUBD^kSvkjk8U&Ae~Uw0{04)_cUWY!BT zE%pxu`ATfWMqBIZ@-x}dvG&O^kf2%(vkvc?7WE)*fZY+q5&#%t_pj zITwY>B`>q9l;AbYaMR{!&`cPZo(FpS8P3=sQ?*BeviPq+7gTrR4pBuHx(eYXS|2HV z1~RzVBzn0~ny`>n`v>^{{0O(XqI0%j6B`5NIJ|?tzV2f*9PY01M(L|djDdpuEy2d} zT((MYcc+%jh;9~EBmcCk{&mTm{W*@2Z zAl7OMRBHLD2z90BGrpAP@cFJXW-*98|Cq4w$HuR}Vxn%d-~lj)jdn!|jcw2p^jA(= zIm=ex2aCmztAjk)xu4-SM)B&WB#$mhmSAx7;Vs0dD|aV8MF!LT=5#Q9??X-Sy9b~A z-4+1BITQFz|m+)qW3FEP{^3k1i3>ofHIPgC?w8-%)@Yw@K(vCOyA%PdE1??yLUEbNw_~# zVV)%Xv2EDuKIwJ9JhWFB8!W%mLraKY8hysR}4BLsvfHkKp@%-^{cZ?&9C zr%uS0%371RIS#F=eeHO+rcc?jN5RPq%1XK`RKQXb)!igVbABlPRl{6;uf}1UYm4F0 z6G%%)W@{`PgAbY18uWpST}frxtBCK>^(5Hs+6K& zkJo7Ldd&5+)sGiBa6CZkcAMt9V@YLgg!XIZ`YQKODFPd_$~yH!BEenxTCDS%H98n4 zlXgiW@-#2w#jnK2uuNjSno!1XEHx0lT5C=oy)alGQ%9!GKxU~IyRrCS{&XD=a30!5 z7-!C-&^t&hJDvI+%Oy7%GYGvcp$2d#2XLixWCnK5y>l_2%iLCM$sr}6feP*5$-ONF z#?TBf^D|D`oR~;8nq)m+Qsz!7_xKRx(vtCMSP&pM?@V6GEHQ-CmQ=g^Xf zQ2$;-Z5vNy2|hJUFwvMBAk96dSal6?nj(}NI0o(Saf^Oit#RfYT`JBBj~4jAgWX=x*M1`Ta0;Ba8N0Jw21(9LNw;U!_!J1LfHoNP zY2(j%D*Jv-ISSlH9!NCh+5N_EEN4PeDyL6^r)M!H428#=zPM<$EfxIf%u5gfReUepgsHt>6XbOAp3UM7^U z!mO$LLL}kYX3!4h<=dKJa7v|5t^2QHbNuej-e0MY4cm_Wi3=NR#~50@VZ_gI9MZS)l;ar$oKIK z(Q{2lg#zzkT~`tfHt*#tl}fg7>Go2uEvT2bi0H0i^sgKY))Z*y>)>kx-}u{`=mC#e zg>U4$N5woA)}Qi~JL!3Y-z>zLa%~^4oVXenv{ehsG-+T5}FYZ zL68JNiXdPMD5w#T5?VwEkc5P$R6#_sgld!$P>P61Q4kamyIym==bbh4VLrX})pK|MDv%^@k*SZ)#h>ioT3Kd5lUXs$T{zET$@4Z^%A9>=a#sa$FZY zo9B8baQw=W|J=yYz=2G~i(eZQ-WEg$2ZX6C_>dfhygeQ!7&_0EO`AY=(#3#_{^A||EQv=}#}kbVUb!JR7hO1j+nojLamV`s zlsPivcYA0U7l?e0dIVL>4k7Mz?b&iU-B_xyb*Nxv&qJE1yo^cbKrL6#+|KLOGrXPe zL_8eoNOi7=MP?_DWf^n*^3SbZbW^I`q5^(4W%D;xx{^^U&7^(fb_e$pUOHAIPuv?< zT=05{mvhf)lOY{$kb?{Y3p`r2jw&ImqN~Q15+E#l=!g#EaE|>)dzKkbmlANS*o1YY zh<&c%@({I2_iUR(<@Xi`QgcSxmLK4CkIcH>Tz+f@ep{kLH`O=sRk>~`Y2;3{9Arqk z;G-`q565Cv7C_5JBd!50cetCMYdC7EspIhpE>wN~G2OvQ)?i9#;SpH={l%(BYKEL2 z8(24;ezR@X`!+RrSXOy!Z{BozG6R`OQpuP7=sn}A65=&OzM_MKDBa?6BW>T+ee_Mo zrGt_-);XIY1?>!9^u7d)n8!3)+#qwAVd-j4%>F)4y(p_MrZePS<|D~hjyRm>d{Hnh zT@x{Ms>b28wLB#63NZF^G0w}}6}rT%WItz3*M|>qT$sVA<|{8J!Yd@O#@rs7VKmX- z+hw-}Hw^!JX6n^tM|DU|(B?&IW6EV18Ts^zWfAvNiR(c>vFS14Ifg$Gb*tW^D-KUWssNp5HCKKIX2LBY+`IpTm!yF@N77x>(PD z=k5)J7Bjig<9R^wnwz3dn3-NDrH={n2V(4;9i3qT)uC|eXd+*l-*)_0IkMCi`SEDm z$vdp5i*3IpqFvwtTT_Lk%Hw+}_kEakQD#jraniBz{T`p}7kQg{()Vmok3Xo*`P{Zr z)$YH-CyhJuEYDCkv!G5cBMfEt3GPq&z8mcq-w{FJZNpF9H*F&wkk$lQgSA~hKO}C26ywImnD*0>5k&yR>V;qdi zT9?*_&)cBh8on$qfzI7e0~@egho4SiEC!Fhou~?b{v0jyeaXa(I@)r-B-dQUZ}+TX z9sU?+-E0@-N#e+>rJ2nAbroHSE^bZ_xtPR+h1=-Vapm^uT_8a%6xFb|<%FMI0hwFw zK(?K26R7@v(XuAEF^nc+&7qp+w5jZAC@XsJHElL{ZlQ|;@e$~Z$Z@`U)zmqt8dmo;!r&)d{c5glT2UsE1N~g<}!KLapvvS%r+TU(%xmS_&TQq70 z9x~|}#Y-p&%SfP)>*@r<4)a6JUg21e=8KVD{Pt1=!t>D*IApSh&z(UNT8DKCRx?z* z_-Uy=Eaf(PX1!x1H2LCqf8KR`rgmLk8{nKLzQFsQB_3KW!EvxYgQmMtm-;WMYy?-IJX6SrJTlo= zH?5q+C$3i~aV0J(ENA(uO)cs%%+FGlh)YznFu<4Ve5(6)-fqv`g05jdn86ZcDR#DS z-LW};UlAuSc5SxYy6>a!j7hw}KC7x4YF*&55BUO1tb6v|!9P7Vyx$s7Z+ZpPd@*^& zKG-rn+B!T>pE34V1T|UB+`DT{#SBhZa(xpA8m~-VMg+;^5L`uX;>baT`sG;m^{J9j z)ah_#gb)J#gtwTT5j44m3GcH($LHx%mCW`d-X5ULc`_h_zGj;;92b?HqOB@FaF;HR zk`~qS%_m)?jy`0U+mT7mXCGqwnyfzh_Or$pB6=~ATA@CQ46}!pU^@|Qt~S8DmMQTN zrNy|?tLxk#TM!45jp7cB8y?u81r6?)MKX;n=*HwE$ZG0iHrwIbc5Q?@#)#|({W{&7 z%6p9S>W_if=I<$h{xfZ< z>8!KMSoQN?+q?TG7XATJZ{KtI2T*(W53tJm<5*JOx7PuNQ~U4k>Hqur|Kdviu7TA9 z;~(8X4leh#;}%-!0`Y70TJZLb``Mo%HUDAeswCEPKflrG29B8POymas92ZRRLe@(F zCml}hkFr(dz0gtlNFBRVWGf^{GIzb#Xr{zVe-=$zYBD&UCp_QQOMkD5-*agxz*C!j zpFXl3<{d;7bbN?966(eqlQt!Ca!iPbMz=;ArdWVoJTFK%|6pDQ;Agn7U31c6VLZ%G zz(4=v$yA^yrpy9tfKRcJl`j!+8ep0^$U|NB@cSH2 zV-08*+Hiq`N0FnVywxn9_)SEb0wscBbDzCtS=SDNzJMI&zDOf zwG{x+PsouccsXsmj3OZf2?KSu!q{IwSR{a*!(x|6DgXmy8e(`5I0vCAEBV8G7@Bw% zdz*tg@Q1LO;B4o$z`GP$edX-9>1UdtI%sqWe^}FdfC%c*ljL5`@h<)OQJFP2cIWJO zn#j7cuCDkmkQwb@de8uH)HIxw?P3OZgSF+f0M<_l_wd0_DYbm`2jhNA)-vUK#mR1g zMXIKlts^M)8^q;qKKJS7dvc!&b*Rs~iVm}d`R7~yj4n3$jiRr0b*mxvDW^cz^_ZW& zFk`f&o=sDdP#JJ}zPMGdY3?4%^hBQ1A-0IxgF|XoVM1D>@AOf=Jawtc?>$Q$-xBw> z^dZgUm}UOQZ5OkyGlyRiy;Qf7FE(_mRfg+z_il=p4nys@L(NKm z!Cq^>_DYQvQBbog*4V0VE8*}D4_aR)@=~J16Z{d%zWL@8CndUWCrmKY-!w)@e*vR0 z>EBl=w-jj-+G)p6nohLrT`O~iju2|2+Z9pWjEB1_6_a`$j7;WR5J0FUCt`M5{_OnS z!`V%p*KXGz9&7v5O6u(l&6{s+eNfIDk1F9~5}%dXM>UtoE?lA4W4uaD4IN@u@~*s@ zwVDWY*2-#PnxHUmVfl+v8u<%QCdst=tTER?x9G$dW~chMI{qD+``fhWQ=#VX z;Tj3|PMxY(z;;aOcAh7Y)+t!NJU3?n28<#K;JsZO04roCGlHT7#DdRDS;Npav!cJh zcSR&oM4SU)iSblq+4A~9oK&>mTOvah?+UawU^8WXcIlex@i|A{SHPv=pYLq@+d4_LfNMg=}~snIWGD=MpNmy;b_0gie|Yo_sVp1X4n(G$7rb2*W{ zFhQAek1UiLehbqF#iN{JQ~g?6RLRdLZmQU18*fnq7ImXayQJnZCRb!oz!A&SJ_Rnz z+Ec^`B*(qN_{P;8B@=78afhi9je|aNsEhE~6vlPqc)j#C{t-!`n)g4dI(DN@`4tcC zQYqK#KauF78Dbh#ev@54A`o%Q>{Otlb-R@tE4r;i(KXZJWALH*-J*f-U!07GNb{BJ zb=-LnWnx`WHqoWO@70}xxU>$x)+prAvUSAlv5vvFd0sPzrStU~H}+mWpt%C%>uP+(Ge%_O7^SA}3;%`533nN~?~Qatwc5KBru#oh@yc z;Bp5!Ohgl__2kSIs>xyZC3H%C&y+R?!{@8E;9H0p`&I!7?~`;>*{V6rZSPj!gjfP| zZX{NJ4jbp|zC=AvG!$r8&J@si_zdF^jX3_W&Sa>Cfwa@!Dcr-ZDzRK@elq<`ggyR6 zoSY=h*43VwXYcQ^YxU7scbqR95e!;x&=bFK!}(ZkgqNhL+tPJ>l49fl=C$vyhpTS) zQb&%yym4-?x9``iZMON@*33hfc+E!BzM;fZ&1YMMQ!TiMcS^M7H*ev-zcv5@3YJh; z=e@Jd4*vLXFt5S*VE3_PGUmtGus_u63z0%iPe&YJPal!tH0Zy_zjN^4yapsrj8Ly9 zS0~f__izcguH z^_YotF2?tj_W1~HW{R+#Pf=*#5=q}IY2xg6?vdH9edCBLI%MFgR~}}wlg%TwDc|1# zgCJwGJDF~~_IWQ$?^Y#NKUYWT3q_w84!eL4IAXvJ_EuDN03uT2k2hdcc4Un=Uy*Qa zP{`Uebrv1TQt`=7^Svl-{JCaekI^1?8G91Mt9Kdc`XmPdJWLEo)iz9!WGt$Y6V+{im1Q-OgKP!f zD4wT`58jsu04s{OjMO*b&}TjVC~r!sF;*)(3v1@I{~a`7?N1<2zwi81s{3y%933qm zyXqW<#atGwM}{uOR#R!_rV0LU<94R+8&&kV+*Y8-p536MAxrddx9g=p##1zWezs=r zKmI6Wo7rn~BF{cj`6G_uYqL45zNRbCi^vO1@i6AtR_`Zh1xh(s34b_sb=l~3GN;8y0a<2-a{xs^Ff`B)sLw)1tNZ^i>%(XoWTN@a0ja##7oKi z_0@!8!CJkmnHWD)FlOBFF+8axl3qDC5`qjrI$CScpYz7?3~%<-N#MkCyLwLat%jhhi7T7!(-FiL0>66?pj@4lyo5 z=DS?dbByyow+sdgJMp*qm*~0T3qFR{54t?AUQq#!i!lekI6n>7IrnQhmkmjgRb0~X@8XE!Yxx3ck z>yM-6n?1*$NkdEDa>c{x&|x?O24I!&c4)y!?j9I0M@YahXacYm;us5H&|$4WYux~y zvk&wi;IAYe!`qMx7{9ryV_Fx`1*y5Sd)rjs6$+9l%$fg8a z3TgQg3VoXk=~dJ{PoT0(HFtP;Md&)p%X#^tGmGg?ovXv_f4VFm);XGZs%NLl@2INu z)AK$jM7JPu3v=v-ZmXcuD_Ipq_r80_jjNobE;I2f6W2w^i*_xsrh&y*?#b+#_}MQg zwF%V8U5Ay#&yZhX@Q056H1~OLPwb%3pYP=H-*xXbe~x!vx8_K!2A4K`3!eI8R@n2J z=YQi>iv;LScEulX5#7_Tt2Xt2+j)%xzt^-W*ozpZOZW}jBef;EJOpS{FSe%jHN zxAdJi5?bJBEFM7hYB+om)Bm&e!DLS&EPy_8MVdXW)XpL8XU{)$@^*cE*C^eL*ncFy zEhGJ{ny+Og{3Rb*Nw8lNHu-1Gy(72{ww{qn&V!{{vQMk{r10}f`sYcS$JHicZa}?=p6Cj zzUx-A>C$_P&JoVGta_)+eomR)-lK6u{|9gA(fARo*>df_+Pz1gQ;F(fn7suY7pW8D zl}cts(@lqqChXJ5{jG+^hl@sULmM;>v7>;T1H{6Ww-#)rdCWb(b5EO9mNErgM4scb zZo_i8@gXs~F@8fzIbaV3rY>xx)K~dz29G zI+>)4LzeUOprP4%u+EB*hxHKRo$%KsJ#ikuQjNU=O)yOCqxY^hQ=Y*DNu^DP80vGA z1=ZnebCWas%$hAc3?=EaWz|rJg3EW2_E??MHE}?88+8Mc3GUUFRU~myqy2F33A>BZ znn{wvag1;rK(Lya%fq0>tEocTjA;(wXu9e~jf0J$B?XH$sv`agqr;4OL?Ko9b03|7yAd>eJ@Hdd6v88p+>HZwK9SO^o+V2(NuOUeu%(klRnL)- z=%A*FN=klws&D)+xfLAeSfo$$?L2TC+i%x3c zj>gmZ&=QdfW<>Nuf5H0p(Ke6ZdtT7v+GHX*5l~GX8DoI`-uzTB+XArI5wwVln**SY z;7y$av;_hbN%3I^^5AR6DtR!%?ttcuo2ZIq?0L;gfem`bG2sXN&Of0e@-*Hop*A*LT z(gUhxkA^rcj0Ml`2_ZTq9Dc0i{-W4rOGPI5Rn&Q#+Lr%-s@j>@j8*(>fH#@Q>{i_mC7J&2y~SUhWeaf?Y*+U}B6@!1pRR_6Qc zvesEXU%!F33aF9&zY;pXr`3+~@0}_M?7!wtJ^iTQaM1zj&N4nOks!A*q$UbMy~ppW zQ1G3p%6*Q!ATaZ!bg5-&B>c`nBkk@#=MGF(s)#PHe^yQj{8d%>^0`#Grqn_8dUnhq zt)I;9XD5a6&XQi|eXpLIyJ4-$GR2NR89knNpXQ>X)E#zVOLS%LJtg@)n!oibI)>K` zO4jT+_HX2JQgfZ`j-K~8KwLdFlu^K^iCT1h5o>O1e3Q5dL>j(WW#!=|Vwx<=r6;NoJyT^njK5uebKE@mtXPK1 zMxeO-AD?nNacRyuRkQNDH>*V`I~`JXT>B+!PCw@; z{;o*ry`JQ%!*3)ZvRIvb)7zixB8CIA(h9tK^Fjq@s)BjbrI(%ny719}8$)Vqbwm8g z&r|($7fr0M{CbSTPn*%jbQEgtX`d`m@oO&>J=E*ZB(U-)v4b=8KZhj?L%+JDgiGFo z_=l#QA3j#s{FL0m#XDynpI*BPmF}p8&)}bhZ=?Q1&I`rvm{)Mx-Y46r?`b+g;k&RG)3G`#Q(~IZ%1QNO)zL46Rkm$ z6`wvM`tMbpHV)xs5ay%;@# zUh7P|JRg+x@6rpeclVD^x>kqyN;tHfc<}N10s$#+@~(<(D+>O?{d@d72mjq`z-sq5 zyYSD|f21bQNt}`Ya^igY^$#!q0q(+OuY=A1OJ+yx-2MTCPu$P!zWmed;5zwWOW2=x zY=matKfoJ_;$fb|8-!h$V0ZO@UOf55`b0u2&dxe>uW6uERyPnfZs4(R;?$kAKG(CGDcL_~+aLkY zUEPJ1Fyk@^;_d$KS`l+e3W;sd8M6(YE}7QiZ7N#%*quL5L?f8gM0GkJSiMMLQqdY& zWj;TXc;3H%+U9g0sgF7IOfz;MPpC1OP2Zq?f}1 zl#CCviU@>)1(T_|>)qJ(c@m5lQHDVd^GTR?J3DAk0kQAh(C;09J?acP67cSV;glPI zgXgq=97mQ7^9_yMuU^WDgkmPEN)S8}9v^K081Qd6Xr7`8E@h2H(u5-Z&&T|HGLQ&I zj)439+~^V&5ecDhxsbgM5L1HTQ2o)w%DZEE322G;w*7W#9*I44A0a9*3?Rc$u1DLx z(YFRSsIgYxkwChwfVma*rUuD^HKzU!itT{S_%zz!b4W6`k(3BJiKr;2Z-OFos6%YE38Yc12L$pe57(z`<{rI}XT({Nk zmA*yq9Rh?8$yw)pGE3*{tl!0p;r@&yXeNdQkMB)CwXs_|>o;PeTy$)!2)v|`Y+XKZ zTwxFXmYWvq(g)NBzDM|Y9brUsGWKDWW(Zq%x$xVWpk%{O{mH0zX7^2{L!vomy9KA{ zi$%t`p*O7DPn3Eqs&Vr3XF$k!$1n8mf&c_v%W0j*oYDA%kX%~0`$M4qJ!*V!fBhI= z=R3md$;P3Z%zQidu^?B)UQse5F$HM;0q&hf`*Uiiv>q8g2q zEp>j!AIFlL29mqpfGI%nm*q(BGSN*${^7%!8EJzm{$oki&TyGt)bmx@W8|H1(bqkEo+i zh7KfWCw-+0kEu@1v*q?0e{Qf))g&!9f)X#Xjg>p(X19x5A8YHr?0OyVZ|!>~7a!MN ztB;ZUYD$9^nd|l_wS=9#Cr%Du5VW+AwSRdy{eE!N&cpdF(RIc00{;9HXRqEnsY-wx zZ8Q=>#ZPQ0EZr6pCXW8c2MJ46}_quAbzwd>nD%Yb{eAH`_&MP~V^7RWxWA z3%?z>Fe@15lL*gBTJikaw}~2g)w9zbULPH8HmUv(z`Xcfsi;+~1nKP%;q|YT$^Qr| zkN*jFBLAIZfP8*Fb&mDZ!8y8*VLuYq#&`Q;>*#N~Z-OH812I3};2hoeQfE+GnI`f0 zN4_&)(R?Ts2L3yAy8#=x2b$FCxShAHeXA6&o?{=klzl$)I^9#YrjB#R${|5=$BnI1 z4btK{i%+?E9Fz=?w+<6!Xl3nymB=M!C9*2WIhG3BY?X8bM9k*fe#T$gZl7Sor=G^Pe+bd^}ta z3xfRAOVZM)I_fEeyywLqT0~cNKun{hy*(|MRKQh2y@~|GaU7^Y;8iFYuYge*Di972 za}`4Vt|BQ?z1`B>&?4lZ*3xPukV!tOgFH6O$CfbHF3=|JcAObAA)Jmwi?wIBj_aU^ zDE<7klG1xI;l4hU~&VYb4Cr$z}6vMaKNr0ZaVmh$#X zGC;Cu!U)3Cdp-e{G!au*P>I-R~a0@Mc`+IFbA8b|vZt)&1 zrZ!)-BZK4YK`^_BmVH!2q?}E^*(xf`1yDy)IIdxk1n&%B!$Uug41~6OAignK(3{}* zF!q3T&;&!Xd`e&;Ui_zgZv2g*a`NdK1u$UnoHhau;k8cTf&WBG#&<_NI0`Tzy$sF) z<1#E(2}p#^fQU*64;LRfKodYxywQ@dH|{VnSb?pCAZc4Zd=J?$UUH52us{aeXWXl| zOkoTmMpr(M>Kg^_#WtP`|>7yn;I^*!k#L!_UUTm?^h>#{}@T0Q=J${%dQ` zEM-*-)PRABRt~o#SeUJNsNo?@3j$%|y_OeQlUGoh_D9X{&-iAvPY(O!R(*#_YIYCyxeXE^rVUyi(XTC}`C5I|4eMCAhJHjN)^E2l9?LS3tNq;~k=>Rm{133);IgO8 zJ_hhJm@)LrS##lUF!)%=KKxz7Xh$Rbr-e`&$4$ZE;9fjY-q56}Cu)~?{{9#6zQtu| z)8~qv&!bbm_}gVzJkxvOF8Jo`okdL8%6W;F98+uW9Y$N!(osLNY+DACSwM^LQR?1R zRg->NbSpaOR_o7hb}nfggwaOmYQ;olGR#VA3!e@C19)EdPpcFad_&&ylup)qCmsWt z5qYLJ`k_tu$CDz!c@6DGu4{%k@2bp;PiBSX z@&-@uB3wBoou4|+%k7)3Rk`Kba>D$!guBDfGn>MKZF3dLf}N|mO-6eDAzj{5ic3`K z;XbuKynjKCo}3kPzeLcT4)NIDlE9XsGT$rp{5LTg6itviwdsCwK|5FS270HN6Kd$& zn9EPWZ1MYdL?3)xy^wJlA3d;nAvLA(MvKIzC)=^b3;9MIYeHG2rLMhFU=lTeLb|tZHF$xxUy!EsfajkQJa>tfqmy$g}eRnwEkXfQRZG;$?mHK;k|2#C5hR(D~`Hl8plMH!Y0%Z~F7JX=l9FtqF? zCj5NpE?$%L+K*<5-*ZvOf~oBT;$Y%EWks?I{N?r*Yv+;4Ky7ujM{`c0Om3=Az=>gj zb*0M0rK&@&+bE;x$4KC*_bAxkA&HONEx{n$0LulAT7JoSB8ExQ-r*tP6_0q>gq9&Y zzV}X{RBnM;KZqtqz`1(>_^KyUvh~9-bX3NLY8wfOq;s_+ArgDQIst{!>IDrY!}*qI zLbbfg_F*Ae$yk107(?nJhtj0KMFi|Yx(M7bhENhY&!FtUA9INQ~ zET1~G7zX)+0CtmfU>|j<`*_otaHxzbvQhVZYY9zs0qdJh8B+!9C6w8V@JY1zE60`d zgJxcQ=C~Jmj)U=8q5uLBNfR7#b3p?PaLBP$X%2veH=5=J9aTy~2-5_WY!M4!$Pj@* zJi$eR*A$U<7LoCQ>;F}owfkEP`M>?5odsa6=|v$BB z*9-z>p^n_hY!89m2pA~?>AHJUi*bJF~<(Gx>Rod!0M9a|i013yi|BK@Oc? z$zjmG_|tXL?l+PS;{B9#o%9TCSX7&(h4J^^w)p}ffrH1&IJ;-rMLk@_h}Wbt9y_$s zdLo7>2Ky;m)b();6}*hyVjyJBwq+eYK?$JL?dnE^9M$}wL+-ivAnFj1wCW$$wbk(0 z{(M-Faav_loYE7c8q3;TyV7`Ky?M0MljVw33ej7~QWg_%Sx^3033|?F^oV}$Tn^}X z6(y~B;1g@lL{*M;k!-<+hfnZ3O##@Y?__ zO(&VBdXLlas~jKgm5#mlI0LBV_OAxZEbb|Ssgsn}M2uW`XVFRREfrSHOGotq>m^ow zRZ7w=oIgHstl;bNE!PPpMVFa|1nr^pxwli5MRxIzr|MpvyqPudRd=>T&i6po9jxV) zL74$_2gA$ube!9{xZ4y#`WTLD+sp3v(>|yhap`d-3cnfKiB4gkFxe;r{a?8iV$K}V=gYq_k?yh9I$|K*cXOO*x`pkFK|e##qf?vR z>?4?WEaH4G9`nh4@Z8X z8rUFv6LV+d1*80Q4#DE}uBCst-H&2Kv83Ye<|>bJ7tJ7zVZAM-ry^}YgN2NP-&Xya zI(8;ddp4p3N2_*!p1JVO0H;GUcxFb1*JJ*Ln*Iy7{hzP~cHEKpe12TjJrZ$ zENvr|RS!<%rpoW+{q%<`oy1}FFe^m#3TOTdPODJiIC)u?xs5WJvH0!+M%SV13y#c= z-x1$TZgZnolm~Z8#NKV`NK&k`cYegv5`XgI;OV$lrLlVv%haAur+A0RtVs{QtqPQ` zYBlWL+F%Y5AvxxfEwY#E`z65-w^V~nR-_BW8oOkT^)F>%q1v)yqRo%$I_UiNShAfn z0-{l-G8P`qs~g~e2+U!r-b-!4o)jN%maOI{1ZhBJm#$i=CF|7{;@OksaA?BbA##Jd z7VnQ+;u8Gcke$Pv(qa5yj} zIK;s~4WyWgdQaqHZ{dOS2xAYr21&^QImR_3DW&49c$uYW!T<0r0=*poGqS}0RZ)b~ z_+VfF1aK6<*u&1nm4E=MigK~TP;xLu)Nu|6dBc8K+rAP@3^^p>A>n%wcM%8eIZup! zn5kRzAq!XKFOi>Ii#`nGq|>uYbBx_{=235Ix4WJ!lMuI?YHAK&g&mu3JL2r(b9TE> zy!^;36eHUD`Y)f7fCqxPSLMuohTnI@NSG!$nUOOm?Jv570!Ls;Iw znkF*u9>M1tIxg&Liw-Q`CMODybq8F;I=v%E+>l=MNU(1%+paxw64On1bOaSI!C&B9 z9zRDV;`vpH3hU7OB5ZDd41{qs`!u45eYD-hspPQGXm#W#bCiiy)Kx7(lO(KSN(}xW zMVy@HYCOxm=z|h_+1g>}?ieMzEr0ijncR1T_{a-XO{-{ALWOHfjh*tLY+Jibm94;g z#T&-IzYFbZtEl_-40b!pVz)$=xRPaz_31jG(r599oa>r-p4xu4N_5|&o1bj59&Ru@Xw!lqmso-AU1BSi=W0V zHMOF;Z_hd_P-}OMHrv*=ZuP(JDMcDRrYsIA9)=lwW=&w1W>Ltey^_pyWlKV6* z>INCxOv0Q>0!+w$9c67=zH&9(bzSAI>uYhD7SnDlOXuf|(CL|-3+fz9y-4xf?#Ss6 zA;(T}qYw2Ogz4;$5}Y-P;oH^Lpq~MH=e=!mT>CY2al0YN@`>46g)7JZRh%FF#$XU_ zyVkvkoZxZn{?TbvS4}Prl*oig?@Y<8pPCKbklByHtCv^2aejK5`0Zox;~1MUCER4x zv$KehV|Q}U`^f7bmwxJ8W-bdIr4L zDlfj*#r&45l!-%h1GVeq$t9x`zRC&9xpfnFtt@Qqp5y6mO6_ltyFM>haT~p(83+oQ z3WnF&w^(XeFmDa*Ibm>XFuA_2a8kX_UhZwIBQg2-?6gF`N_puo%I`fZ*ceB<0896N{*xNPvyhCH|-{wJnF z=^tRs>w=yCaac zHIVQsYPx!K~g1Z^XZ(CvD8X)au4ts8rD~9NECAlY%Z&U#AkTY~Wcs=Phvz3y*wJEwSJE$bXYs^V< zHr>EG2gFr|7QkA)_{7n}n$QXdz&zX#%)_C@NQxKK99hCZMvz3kcz>G}YfE_*WOop- ziQz>Qz(RAysHQGtW3-stUy?yJ3`+)#UD}I^NDw!&tG7E}wBgfX1P^$v?=xuB!CDDd zbOkQ;iqu@`!AGem#hxVtnI4Ew@sBPCjy^G?0&NC&2k%WJJZ}1=haTeiC(b0GrRgDL zP<3I>httl(Y$*Yu*{=u>0VIT+OozBC!$7F#9jYylf&)H!a!qbJ^MQk<(**Nenr}J) zrWT;~L`O9YD6P8LUK|aWF|y+kH;6uSUXW(04{i%20Y5?*douIT5*!5v*o~k>P{{6H zkh_HnOE<{AD#QFB4TlQiG(+El&Y~c5V)UY{bTIM!xb!MooCEuX4J9c%65<@;5Cj5z zl%kX}I0`g=E*u;L7%)YE!IP=uimNLSYsp}elOlm?WCTd}T*@sYn>t5AIvCLWGB~6M zo92vobd5v{qaLyNOkczFA;%zdF_v{j>tWWu_H`m2bA&x(z8#5z*BIgI>_S$D5}z-{ zEsy%5ZG-|4_RRI%P|inA|~EMGfY()kT1SD&*s+uF2`J%@z2*WOb$-i9+V}O!p7C(w{8$Nt`;F~*g6+(F=Z(W{mJzD~)b?ui zvMOVaoK1q0*M`cPp75s4{#x0zvU^A(e@kC z#c8dHjkx*iS(1gClRcAu3Ue>);*#&~$k5C+!tT6(VdH%J6z6;VbSG0#J)*0d;OS@C zphZeaM&Ud!eH@TI?+7Zms|S)5GFsMU@9=Y~xM3Ok4pGzgXR~}onis1t>u6U}F)eer zP{%B4#G|oeV_F^B>QQFz?TocspIq1z{<<2jg_AxM8cZ9#)*H5%#9H%S5G-qoE~cJ_ zK0U^8SuQz$bb=}*D-JeeFUMog^G>5MiU^Jkq=$nS>x~q;IpBPrlWT1~e+<36%F< zem`POM1#(IW}Hq8SmWgo?f4?nMGcH+qVT;wFAmmf5jyy|QQXtFr31MeDG>$*WV+eY zs7zf<$ZSvZPD~6@TvPUH{AqQyaZR(QVjcNLXjGcP{qI-T@ITVR&$mGx6%%|S2Cwj2 z`_R@|F=i#0OtVEtO@7f&kzr*sqsigSWF?0;gK(!XHEh%Ar|@pn^p01$NAs26ku zJTnl?PjiNn@o)&7QAOHG#$jjz40;tw9@z?R@JkeIM4G!0p9fPkmL}x${;O`cYc@zF zRWEKpDO1m0M9JHsYg`!d%qg1iUeu_jgOJhJ}*Ey~)OoaB%0tVj%=L9o!bZUC=v4jh%PEB1o#_KwN?w6$r#G z!)8Wh&^x%Wr(+p_A)Et;(Y*MC=^PM^35S%>IS0X8X>#ByV@+rfG*FdK@iKn&H#3J! zQJs(*j&P{8H@N_I$%`MXX8w&wq~{A@n{We8bAFnO!Dso*cTXa z`(M~lkWr!pxO!t4Fc8}S8}WbvE5^>cXgDCNOUoR#lzM^&$uV#Juu*yAw3!)ykFoV@4M$%e zy-Jh3(BD&iAYi`U8I6Jj@gNGhMGdbswmxj?mtRJa)t4F{%cLCRL#z5y$I`HR3v059w#Nloj#<*#H0p~x zQ!=u-qgUR_8njI;K})jzgDq;YmRU=!m7d1L!e16RcZWFEu2R-|*?ZgzR0NWPCA~?;XAbj`7LpT?9hj*XuR8&4*_>Ep-p7@1!_p$kgAuxTQXUA<_3P8ShftsCd#mn2wk(Cbf)bPQUgZUwG= z7D#AK2nv6^E<}FV{V2bWft5KfR{J>qNp6iScP7vpUAGbR`%5LX4qDZrdQ5~K;UR!k zZxP*|iWuDT^>e{x`DF>qSamX=ks)fLRfle>5hO8Uo1^z0dKi0`+F?Tu9ZF_)&gcRd zL@^%~?O&QYe(L9V=pmchxh-cQs-0sboeyKgE1^$13xb_!?1yQtb^->Vl-5LZ)m=mO zC{#1lPk8Y~(qD0vSzvz}>=6Ta!m8Wdf(8JF4J8A3L}zs(xVu$1D1dq%#^8FY%Fmk5 z-BER;3;Bkt28!r7C)4=%BPjE{DF8131AE7#fAg+kL>K$CFR@5vJGVxsZXQRTb?8 z;b0Z={Ge?KEezqHjd0*oK@k2n@X6;9*M>rCBEK|7LiN4LNfz9F7QY^o0dkhlExE@a z9pCycU~5@bUe|pZE6af@l5G)ui|u1CqiJ?T^HmdigZ$nH59u005Z?>QI^LiVCz zYpo~1_B%LSGT^$y6`WhMGWfKs90+F2Fo&%G-V-%^|-579)bjn@XOM@xNEmFOd8>k@EB)pyVrH_Ja)^1jw)U?2gfX<}X7y2&N7R<+~< zv#rZFG@)sgWA3$mg0=g;_i2=Dw3sE}9{y~5NIPboqdVS3G9qdR^&xeoCTU_HHw_)U z^R7MEPkiid7FxIK*ekSHQ_q~wNLEhj*pt;e3aqROGY7yaUNIHtecvOMH);{*bwYo} z3sO!5tg5*N(DVlQ}qJH+2w7ruWqMk~Y zA}1Z~(Nxvji|Yu_Z8&wu==UY#qggLB&;Ln$sKIhjT}~qYIxQYH?JKU8;7MA-vQzwFESyFj-sp>5rVRAcAPgf1) zw5MHvFhBen&!}BN_YP6%cE{E0gF2Fc7yI?sA5%wpOS4POTDbysww;Q_LR9kcun9nzN$HLkqKQ5{{8(O%kGH59l*Q$=#WDN>Ls>(L&j=7HVi4~J!Pr?ShVj~aK0kM-d_R*M4Mz^| z8ux3liEXp0`!|bsbi-ZihIk&a~Sg%ML zKQfa7)@KD=+oafjxsuGbeN-MPrzqz$_=dl{bO!@i&rwEoH>}6o14hE8p#`i@F~O|M zU=Pd-Ab>b3DRTVZqNj)rjT(Z?U_owq=V6tO()d9EyDAV#<)8-UNv${7p03xdN1+&A z3~S4gAaK>(`))N?k;%8^V2|eGAjwo10zZv*a(W6 zV+R~h<4lf2b{HB2bIQpa9kbNzU^z{h(|(zK?Q4I3zCZig*R{VN{Q(y1dY|>Y4~zA@ z_j9=4(~bZqcujAJ%8%4~J3C6ULeO4ZXmL<Px&7S^SFy6z`O&jVbq>&vf&sRNMm`+KrZo}3QZ>)80D_M!X3!8Y5AfYTaQC0BGF#Tt& z$zbb4cM3Dfoxv`wf6&pi@LWMBdW0s|da@sUpL{1g+0%>nj_(mt;qcnSL1O#l*z%VA zyCHF(w~e<{PxeT@%Q+tZ`+-d^E7azfUZ7`NJS8_>9A9p2h(|rS(`q-PZKUo&S2>=d z=oOtsNBl@zh)+C+#+JH~s_}0&YC>E7iZ<6{-gFBeDavL)R`P$5CV52sK-O##!4I3> z_K7DiXW7jOFno_sZe~D^0|J&Taj4TiWnUS|Fl9I@v@4D?#Ffa`bPchln_V>t9i4NL zKeC%K60lB8vY@2=&PVL7cn{Uoy>ZlucE_amyoa1|@1Y$a@>tU{nJM#n=U*mMrr9^h74U5 zjW#{F$LJEho)V4$+TH`wbJm6fpfCSdn=;6G;Q#}#3?-#9!^l=_Hbn`% z)&JqLpxdveGja|Ck|R+RU`I8^5}h6hH|q@$JO=V*dihUAv}yD%S>Ul3^*cFknFqG1`v|Sg1{)KOM^m%1jejl9bjD> zmjJ4JZ;(F(dB&uwGx=Nn2b;lGdL^fGj-=gHmV0f8+LHG<9i{zxcW?Bh)1es8iNyza zr?zpv&x?h3L;TOz&~LA9PJQWVvSD2y!`QgB+AP@bSf&~n3z|V{qGrJkP*T8B$7kXkW z5eGI&(5DnT1Iiq&J9k4Eh`H)$Yk=$OH1hGI1m{o^q7KZS1{MJ(MhY7>qKtx{mzm8? zDe?PX&`4x*2=2t-51sX}%)H%^oh^w3j6UAX0(-C|KyN15aYo5j`&Qw-71E;1J6l3U zEdJwbW&z*rlGUHUU4EJIeqJUh$Hr%h#t32bhyy75wo(*XYM_e62d`vJssNQjZWdkVp8`e zygG2~(}=Q-=x!2w98+>Um$N$tc&?EDbj;|3ybp26f9D?f?3GjyHFU*m{luQWXxx_x zEfl9K&XlF+Dk??;_Ice&U#BVYBbDWykTt4*zt&sED%^Xf@#a}HqvY4*8#A%%F*~YL zF;N0OPmj?!{RFlZk^23pjq%!i$8LtT&Z;vyQ^Ptra-!2s_|+HKe&sq5GA)4>v1hG& z)goTp!-dZzB}ZvuYoasrqucL*;?ut9vX&ICo{~>9;zQ6MnMe`2OdBsLXm`D+h); z6l<%Pnk~nM=*ZtnB0M$-tG}6LWz35a?i*-swwPD-?QAF*;1*C7IsoXr+;QGC|EeA? z!rs2oP6{@ZsOiB%9%inOl!Uj2QofVU&~A}BU#~N*gi|VT6ev(g-AqS*rq?^G>BuWo z^ZF>VO*(wOz~}rs7J@{qVxFQFb?K0&h{{OpSw*@S;ixCfQgOAK2$a~p7^jWfDIOh{ zp_&{ISrHhm)WjLGbsACTmS5^+5z;(bDG)9-61H-6hK|$Mozr5wuakDyjt*w$QZ@@r z18R7o$TlDUlCFUR_9132PF+mpg1UI%fFoU)zMOq&?TG5!ePV)nJD+L7MN~S&nq%Pe0i;ex%f-aru5#nyc6c(XYJ7PgG(REE|IQ43PG*Mo91_15qFbe-* zl|_O93&$;q0@QANDj}STi$ndQ=W_?g$kz&p9mKJ4^0I&|#S-F_KP#1N!x$Gt&XUnG zXD*tUI}J&)oHV2^R_^qPID29@w`6ccW&Z=#*1?VV2<(-hA=_9>_hd4x#Qie!LPam2 zYysXk+Ue*F?S62BG_lbnIrA*{T2kOt6F6bHG@x>ZQMr9c#?FP}!j|V;{IX}dbg-Fh zcg7vR)O*uW_%GlM1g6nl3FJs1A`QU>igANOp?o;iE zzeFI|W-J_|cVw>AUlPsRlCv7Xy)yimU0%D#(uRC+UFA?)^O@$?OI_!#T)iL}sj=79 zypv`lsl#;mQP2NudDC4$@MmU^sXFYw{;EvcnQD`c&;)0!y~S7>$n!#pN;U9a#>G}! zMG}P;P`ie)IV8jt zo@I-pC@zj8c+m(AX5dx`QwJ!nCvtgVe>{X4CMv+dv=u-gz}4({Fp5G%?rQPK#mXzt zz#R=xp!h!=3&DdN1G=tdORT;Cjjr(&7_m8bST&j_#G34#qNS^&$Y>b6SpFGPY6Yy% z$X(A^XY}Co#)ZKN(7OOU4iE)E0-#c;YXeYW8~dLPk{K_eC{TnyD{V-KMZ6d+GL9ld z{DB7nzgXqQ-&OJo8>3qR5CBZ#AB80bpg;g{Cg3=vns*3{fMz3zK#7JHm~Pq}2`*Tu zqGDy1miiK->O3=WMGX~bXd^6N-(ysNaadd12sred?8i?{QQ(SR{8jLFGBBuzp?!^& z7Y|f@JEKW$7y*C$ z&z&yu2R3xc6Bh!@3-+{a8O;In!hzYYFv1uyP{Isg8bla4po4Gi1V(Tp7vdoG7*GJz z!w8}MwTJ6~^re)|MpGFd*1+`b)N?&>w7Jw@0=_iOh<*`8jqDFovH2 z4YNcUIe_yMicr4=gIbJ}wyKw6L8EFBFohd$FOXQ^w>S(2onmK#IU_#G`kQe;{I>4s zyYF5_kw11>o90It1!tB$$!3PH1cKxF={tF=$Ow>W>RCj?bW|Grzeu}2d?uTHu)%-L J27h^f@9*~L=5GK1 diff --git a/tests/assets/hlabel_classification/images/test/16.jpg b/tests/assets/hlabel_classification/images/test/16.jpg deleted file mode 100644 index 9129e9739627f8976bf9a107425098b54d9fef36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188698 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�Y&nW?J^ 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 z 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 Ln@rfMKcD^|N&kwJ diff --git a/tests/assets/hlabel_classification/images/test/17.jpg b/tests/assets/hlabel_classification/images/test/17.jpg deleted file mode 100644 index 4370fdc9b25e130c995e1b97e3146fd974ff65e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 175283 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 Uf26V+cS_)KzyCQ5V28v114qB0R{#J2 diff --git a/tests/assets/hlabel_classification/images/test/19.jpg b/tests/assets/hlabel_classification/images/test/19.jpg deleted file mode 100644 index 26f10d093ae571c5d859382d928fd9d475006989..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44669 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 diff --git a/tests/assets/hlabel_classification/images/test/2.jpg b/tests/assets/hlabel_classification/images/test/2.jpg deleted file mode 100644 index 0dc56601729fa8908564c802c204e714593c92f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 278748 zcmeFZX;_k7_cwfkib`skU}~200H!&BsacVVA&LWo8d*69q?JRZHi(&Ja(5Q3hy}U6qw(vsqnPme1*R-S_`F-uKhv|unzn1+_7rlz*G7F-9RtFvH%j`3oHg@|P) zX3LkEn3|efJK33AuCOvSwO@r=;p~FJV9e}1*Lb*kJGo(8=Pv@$*4EZppkt(~YvgKS zYT^3-`7zfE=xeFp(VbL-SOQReh?+iR?mA!wj#C5j_XGIX0|Hf3*MMniX~P$Q4LTPA zP>7luR9#I&LtPzgeE>WUsOxJiws2VwGYE^(v`j&`=9JcJS^2bILz3@)vUZD2J){jc zTw-KwVqAZL<*G_9uc{b9v7dGxQ)SNrKRuQvo|9%EBA2T zk)!#?I48==D^6BcRdX8}`2wLxENMD>uA}q(g|6<4ef`&E1A{|1ZYq?jkD-%Nd;{_^$PU*Bhb%*zD<)cz3*eEvtU|Ce0)pj=RObv1R(dAT6az2Kpy zudZR?0$aR3Ofx3Mz|u8G3*l2*-+oQo%8mR98Jl_+ZfK2pW-~P}+TW7>p9yy8|CeO{ zBiR3z>mi_{1_5uLnm(`&xEmu|tG^!cdcfh6Mn<>+ymxtury!NYrUXSLJr&(QF z&?9Zv_+b+L@tS^4kz2U?i%^Z-M%Yv}S%d@UqyZuRPN3F|Y_+if-%mD9?j|<%Nq5ta zofH%zBhzq5tCbiHJ~XzfPxOeCAk+xZL{iMwY%R@bN;sGm9k?toXg&EkC)#IZSx{f2 z5J2NSgz)X|y1>S&34^HCdgDZ_((tjN1A6?)zUB66bw>;rZeGq6;F=`NTt!E6S)#q# z#{AFg-~*MvVx+Efz(v`%i!bFsV3LP$tcbOJcdu}4B?jVe@$=!x*6xi{XFfXM4m<>o z1b1esW-EMxJ5wLDbyOKH7mv{)$mrWR7?giHb$z9(v@<2$eAd>d;=u))3 z47NjEWWAh!^PgqF@3!nPs7BhIcU<)R^&$$7_O zWX-W6*4Ro;?8@DkH}Ttc6EYB5(zKx#33Z}#Y)6xuOb&$7CeA!sX;~lH-F$xPGYXQT zc`Gy+{qPJDA>jhI6DCCGjbeN^$O9HX9oG1iA>tffj>cmkHn5yv8Q_C=GgZB0o?<{2 zK5lyF*&M&WQ*9-d0UJQQ^AO-gu2Vs9C(i~KIdM{sw!;92$vNaCfi_M@hO~X?(*apP zB~hHkbhc$>MN@q+>wz&3PckMnTAGMo?+rgEqjB|^Cphi(a^mZh@WrMSt`o}j?lD(0 z=WwY=3vC=dbCEhc+l92v@5rJq)?+DJ@f)nus~eVWzlJ=w^x%i~`lqAisrx&Q zXYzPjtusiXOKybC>L#EH1KF{%o0Jl`B#0{TkOL80bgfdEf4D2%xIpQKTR|})njBxb zhHjXAV*!=XnP6BoP%++25T2nL)W5=?V-r0BjUn{ z0o3ah+!C9l!HqlC#zh3aOrbqMHEfLH0227HVYL-uy!j@2;vvUxob491ehd=f08}-%Dxg^TviQ ziS~x4U*d4wI3=?xh?W_4li8a$i!Q0EXBbfaWfsO%^_f$s0T8?>8~&64<5ik+oMDWF z-grd70vMAd)?U_jIL=4)u!OV29zdd1(bVIkZ1angv_{jBp3+j&jAE zRa&^V+*u0vYpg~*?dV}ZEzwKp9pQ}$Jh)SbUl(?@jipc*a74Td;{XZ&Jfpt~WN=TO z_8<|38!re|W1_zEWq^B;s;dzJA;wIHF5#TO&q&_Xga!mBcr-1xkZ-@W98b;Y=IM+d zMTRqO>n0g_G%2p(j2>xWo<%HwV%zxBg{QS%q6XjlV5sq?CEK(WHNHOdon;w}y#m8- zMHiHub7!S%?v9Ggp`WxuK4ndBHM1b?0kU5`$o^t`CWZldza?zS#XX~FEL+(y2fW_7 z>Dkr%ZIovoH|-lP8`Ri0aDe|F|Cu`Yu`L(1t zTBOWAn1$=&yJq|tc@ZTb=f{s8I&c#fQ4)K?KpwD)m}JGpZ7ztX^gX@1XuOW&ySzqm zb}G;x2wtdw$9ag_JB*TsmV!rzm zn%(@>GkNf~0!m_(a`>sDO%BkgGjHyFQbNG%S=wjBZ-qIB@IDgF*?pE zG{NEUvS@fS3ekI;AwhaJJLU$_W+eIkLGaibliKy4RTc+X>WoL9I~~4=-ygk2X&bM~ z4@+gC#wS_^E-7wSV<32oZ_QyFa>>>-S0M^OPm$6kY+fq^dK@n|kD{F?=H&qxlM0r( zr=%oQy_pON3M0dTATm;LNO+6&e3t_*EXl@pUV&bC?*c)kXN zEy(hRagLEPj1-_Ih>clqZE!bm$cjwK`6*nG$pKfoWxTk6w#~RPLH&MSd)v~&x)-Y!>=}ob}UTPTlbwf{E@29qjpn{C=5|rDz9ZE`LitpeZBF=@-e7Me8 ze0X=<4(2s`*v;#)jd9_d(LIi<7yI}#y)TwKdfOO67j=mYWJGdR_;FxlX`x<~Bz6l^ z;b;i8zsR|?1VyDBX?~sB5ecWXq7lf{^r5C_yX@Pd3eM|#2D7yFOlpsuKJ=20jnSnM ztK7j8?X{?d*hN{plI})#4SI+M9*DOkk^R0zIx$vS9-$X5<&^D3L^}E1A{g?$6?qNU zQgRpxccV=BBcienv&%GppaX`EJ&c#w-X@+=enpSv;8VR0YjshJDZBTWMmxhC$!v}QRohf;^TQ~%N3)u zk|ikt=h;>=+lQ5iQw->u4v0K}1D(d{sc~FFzyqgaS%#Pey;?D%OB7oWswToo(a#e%`ui*}79SbH>aj5;?LSzQpAo9b=9aRrqMv(~_CMe- z^c!7x#s0k6mls!mEFJy6v*4zvZ86h1=gl002cbEoSFzR$SWg+@cX(PFqGkS(q#%F8b zjaJhUtr5WJJ}Ai$qKWzB5<>VcywNL)(Y3=!mXrLYC!0%JnfLwL3gB%pybnxwV?p9^ ztweXla#DbOuei|@7fH9!Wve2qe+t-(Ia!%xo zu^m?@uS<5aC-~`9i|Oc|3z$*j9^=4UvZbs&QPlKEb#1OMlGq^7#^;)jGyQ#NTkLAj^+9EwK$90%W!)6DZwe96+D^eMT**5&@X_|2uG0>v%9If#wd;id@m}pH2#9*A%EEd3dL^gjRxa*_#bkiFrwAXMAcs1XF!?4mQvdLr(%2e=^1O=E0SuQv zt&&urHiChnX&C3Ay$oZnL5MX3Dt-ZM+TpPK`$XlH(9+WkKA z^{sB&7W`De>>YzmW)qhu1|CuYQ`TP2VpF+y!7AO)g zo#W$r7;&Y6{`SU(f0!z~c1TkEAL6WicOv5wpM~bgBe(Y8hR#QiRIv;wzJ&OeZXxfX zYMP`u!FRpCzZC@sBQ|vK$O=EbF7dfs%H@bPOo(^fZbGa&HHB8B>J?8FDED8Ywr29* z2f@<$wzYT+CmMH)TQEo|C0SN2%1nQG80#$!G?~IJ$_G3_6DJxuXi#j6tWa`LK`q6C|NUZJS@RR2F@Vrc9rX~Wip!F_o z+3g=!?Z+iv`@cXeU1Y1JT2~!C94&Mn5N^px=eglrW71PtqrQ4`0ooQx&w2VMM zu2mFsMRF+vn_TGMk9nuD>v{?~bNy(p85aEsmCxS9PNSGVA~JZsgSH}HP7W(xXi4so zx!uwxqxfk71*z`nkV|N!gsLjoYHbpl!+gNw0)4-mFXy<%`UbzhqxQCmZ2Kulf-^YZpU++{!IGQDuIBf zp@x_WLQLWG5M6x4Go0@3@#gZ0maD|v%W6h2)Xz^SU0Zvj!fEP07cx22;)d;?sF}y1 z&$$&@p%x1fkH3qs{>=Q0G7N8rxK|R+Uf!-2cFTxfPnWtbaD~VF)3AT|ar5VnK=V)TLeP=)fX#rr>s|VZ= zYm-XEQ+w#bbDUj=;Z^XcP&GtsZ7@S-V~fEEnd(0LhLVB9GHD{Ff<2T!rD5FE)jVCmk<71|ma<44 z$vmxZchB=5`9B2mTk$@_P}YMjsKXSWgstb)w)jqw*R8z%t^apT`5f^4pkiT`lo@kcSHp$zfV;g(k(;wcIc<=_DNd??jdGSr?+v7ccc zPMeSAjP)h4LT8bT5HGA+C=ZU4`7M+a5<0x8mz%WYq=$n1LOCu=btiP&1mE&HZ5`_j zGB}F@)%>Y?rr^Gs5NL9u5ek&5yqAekblTXWa`fq3?D#28X$A=AMa;VeTz_NC_dyKA z={A~wOTP6zEzuYQA*{rD1f!tSlsFF1`ZnzSM2oX9jtfMdY&9QmwnN5{06lb%9hNa2 zgu>}JO@%BV!ce{BMUOiaolYn|1&6y8TG6}$ZHX=!+M_(A>=nqIjX~!i5J36y7U+1R z#^ja6lP)i&!phdOVld`VRiDZv*?HM$%@WLrq@1$o&Poo`sCiR{*fb57pU)R@lU?AQ zuH3^^4)eaYYB*Dv8;q#3z%FQ>b7YbEA}QM0?11xk-W- zdl@%0Q#esDtUfct()JYP%COx+jns}N7cvqgPzW7wqCt4oh34XT8vh3LXCx5NwH~7v zdrBS&{5D`$>|F}N#1zPZyvS&%#akeNKghX(oEUHAqBlPFh%O00v_-SFWZT9LTT~f zsdm`K3%Syv3LpyWY@^0G6ef>chDK>(AYjrcu1|@eJjvr}6mu()2ybp30vIE|7eR;` zz^Y`Yhr8{0FnyWZ<)AF8om35uV(_3R3&L<39+Z?|1VH8x!$42fap)x5uv|w$IavaG z=&pP{A385(es4qoa**R$hX{o(uUJYw(#QhYL)1hy_>I3+0bYdJSF-T$ubVK1HB2E} zpZVolFi)f51P7p_?0Fh|TTZf?k4XWiTuI=?M>Zlpg953GI0LkH{| z5e;lOis^#W#yi>oHWrh?{5AH_wO=ijtCgc{Ys4FdC{&H)TFX4bHo>n#+Su9~FGp!f zw1l7@O9MEWh$@~|f-w?yv*S7E_&hgO*y#=~0DR9ER?NTpP&n)}*j;CkkSz=Yi$`1B zrjp`hd#-*Rulr$I@&43y*};`M8E5bPzC@V=`~x;U`|XCls@)rgNB)_bhX9rJ-{U`0 z2WFJ_f&~=N-nMKUb%yT}$Phrc09MU)BgFmSmM=Wnu!Xg2Egi^{+*Pbuk$%Lp(0`#k zB-Gf{7mjHI^&yygyhLHq3O)>|J{5(uDCh9f0)%JixkfqRy zbL=&QJxzAR|0y5>6r6yy9tTH_SE0t)rXW&+FKUWYoJ^srt!s=NTSu}iX~ihOh>XLC zBaGMY=!`BxpF8j-+MCiV9u?B#5bz+180*_MrYK?-k~he8f_yp+;B9^NF`)t44dseZ z3;dcarG{tL-INmw6E~SHqf5g^U6II!6H#t*XIPXp+)6j%+iV4CP>$=R?(md^bNjh_ zAx#|*GU(;-qMbeX-Xu+KJGU7!(UN$a{gnAYJnhfuT0Bv1QjFRv$y?+p@BT)eoH}A>IH?p-CpV_zAU_*-wH5NetpJ zWr%Odw&KgY!8AE^6>?di9g)L^+k-F-9*e$+Z%&gHv1QOTo#R`L{Jl3ZK^`782Q*rr zNs(n79dIdLLeIW;DEsvdhon_gI@|Z#)q-gHx~BeL0{mN$|GxfnL;n->pgHn0&nYd8 zBFo$s-6dbj>^sS}a3INoSm1Q`dZ&JDeh zoM`mJA*1lvGuRyl91>)8E1tHqe57HPw$T$4sU2IA*pk~7p1O-QwiC}zFD;{O)WYts zky6v9!=fz*PdIQfsZHr8Z zR}ZIuZ7xl3=q2`>D!qr%8(y+bq2z`(CoBGKpb`6!{T?n*zNNqPnK*Bg!4H_JxsS9^`@ zOUotSkXzLMyg+s>aU&5=`hF(24MmiZHDta9`$s&z??zj1fiah_FCZ6p)5g+>y-r|K zqfzb=s(z?O!-AXi2@{9-46Z=lLAN1lqlN9`jURCa9}zuyU_{SucX}3h ziJH-f&^S7CsMwj}u1t|bVt1v5sA1vMH;e@f&KH%xQzbDLKuX|-q!G0Oh3r~=k&H_( zK470!8hH@4N9n7~9X8ZG!tN404!p>2GYISSQ0l6+^@AM6ur9Ps1EkAIgoOx={9;&* zuFDSv2TXP-HN4nrL73-37@%6_^d^SS({}S`f`ru%R7W=tTx}fRi?vD@uO3!^qncGY zgurr&>^ON%zG0n!A1m^I9WgtCQ3t>vu7&IWIevOsz#A)TS7f_ynr!YpjbsGLZ5Mc@i2B* zd6l4n-Mtj#H6oHXA`u-LLurAWomnO6JSXQVe?}JLEiS6W0@K5M`xjHt%^x`YXv?yx z@;KSIy01^`ez+3Aifvr zs;7`Bxj2(u7jA|9k)@2uK6!#t!pw;}@TToZ)2j(dBP&Q-;zINk$jptHe8mN%J_h9C z3A(Ehz_Vaj6$re`2n;?3vecyDtEkf-)JRhXk6W@G)RvN*wjTn>}052G@8D`Yy~L3NWjP_l6&%3n)0fM`3Lf!ewZ? zw>T}-&-Sp1vfTjbN=YaFV!^OlG3K;AtW+HB^d?kwEs(69Be~Xu-o?{iMaiY6H?VE) zAo>_`XD*=4Gb!Lgc-lD~W{oVMSSxEqf6+Xn5ch z7dXgF3z9oAATt8+jzkTsof6>xtPMfj#CcH8g-ujqfX~dscR|r~e2E~R5_c|ezO~Vh zP-eA2u4cv!3zDO`K5-yI`u)vP*z+6MW)G{FX`lyuXpeydB@~%IxTw;5LIL5DVu#i9Y}V&Qqqd-1 z8qOfcoBjap0{?Tw*_&$hk=l?`46D`kLp@bMam=TxXr&BTjmhb5^rj%dlJs~z)QJ=_ zZ@l3Y2M}3IG}!V>ro&7CW)khx-f;3Wmd zk+sfDNUHk`8f8)uytqDbq6C%+GDmU`GITvzEesA;bMh2Qjnx>o&-~|h+_3e&(dKw8$$Jhwe;3Om=r8%Di^`njy|*0 zqEvK?t}N2-rBGdWl%KD1$`tu!@_R3K+Iol^B~vnU^D<7$Aotu#qHvH4ROkjiAm#8VCNrBws^AUL&CC_L1=__m- zxg-8c68-qEl)aGf}mYBp~)NlrBA-bWtAuu+w7{gP=gK-HEq`Mkx0wx{8j91eg5Pb>?t zpjX!h#Ro|pOos_ToQL$Vus|}Whz{gl_D&bh<2n%cMRV4VafdI~19G zvQ9+a>^7sRJkhTVyWa&TE-SImvW@3}7rQ#Jb3mJKAu$(Cbyr;TKd+pQ<)FX5F_x!Y>OeMdw#VJ~C@@_>J6RKO@~9RzQ`ku2Cj+ z>Kzv>auaT>`x$-p86`WG=`jbCY_9yRCi+foUj-F~Ph2?eeCbSg@$Lp@lip+Z;REJd zKea5CI^B(Y-@!g$W%g|I>AsiS4aXNn-X+tQrnwm^iJgfvQuB3@vYo#5lLOAdrx^#^ zR(=oLwqsSg^mX+0-Pb&OJ~-_ioA{8vc@6b>NT(SgV%DMVqEcFM+cCzBAlth)Y}fYF zSI)nvp;1nKfG53pc|IrPNB-a}K)$wf$EGUFbyVbzinyD z`#YmHa{ydf9ISMa=he>v=RsksS=Sw-rf+g~_)iv}3Q?-1#IV+uocT^!drb(cy70`e znXR9iK1ZEkt?MWa{Sf0Yc6QWc?{D}`d=&u{il%%KO^B?%6tzYZzQrrv7)`iayz@7|k|tThL);djHh z{>2z&LL)qouzvjS8yv8#WzDnV-!sT&o2#9E7Ba>u`-INZl9-DR4Zg15wCCc%n(I5- zJJ&^mx^aX(yu5R*!r|lv?3YBOQ~GF@e?jjK#k-Kv$Lsn!m_KDOaY>$l%KOS@Z_gzxW(@bm0BwV~>jyz_jv3O@(zhttlV zp9AVLu71o3_T~buRb#I?aa;aWt(@H_n?K!Z&c5)YYTj1E<292b3YLvXt2-#Bj`QZ~ zG6y`d|eI7;db?QirWG-;VzN#o2lP zecBwbd>HH$+G0See`j7a@W&i*tHlgN1M!!)R-?Da=72}@#=)_YGx}T|xO-hH2AnEm zz|P#C{56@~*Mb)?2c>zEe~bpK48(h22Uj`0S~1pBe4L=je?>aC?%TEOzUB}yTjMmg?x+wi{uZ2qq*}fxN6Do^`muv zz0B^DX6|F2SgYZ2_L(ikEBsW?@AcoV&VMdCPEan~>;>xTkp?(2+zhM7c)T;{^>K%_ z#e(OB?stCAuKZ5(?2vk-xBY`m(Vx*Bw@MgEJyougqqwPUJHg&6XVI9pvv=J0%mMS- zJ9E3sws{-uToNfq`OY20;fjA`3$9D3`~Ky^9B?&m7Cltv9m?E%-@u%-cF_C5BTzL0 zZ~&KKr-ZfV4MlI^b3k6iZ8YK4wN~P#$JZb1XVN#Cebxi#|J4P*5jQY}tC<6J;JO5q z6!nSJ13lBQgMJP5Bn{=BMq%!WDO&M<@NW$XD;OJQGp~USkA4sTGWvKbVA<7MiXPN} zKk3SZ`o+yck8|woM=rk+Ka@N9JS{y3y5hdkMe4?CYU+oyC!dO$o}lBP70){kTK=Wg zjejXtEd14D_dR^_yjrJcjBK^56>6``ouinj>>;xd5LM>Qv3b?e^qd-|yVYb;-LuWB zDrG*O5|6tzxE;7n>R4v4HU}*3Svjj2@~P$4WwYR@_saL03MBZow;y62aRQ@yR9ZuSe3{x_-$w(2snzi240 zenNtHn=c3>Ke8ci{^RAR+qqBtl-ZvaulrO#yNU;y1I$UQKt!AeC6zUVCMt)F){iM% zE7LIF_pJxPEd6hU6gy#-X-5<;4K@p$N7hc8+se^%A3h#gh&^>Rjrt_baymePIDIjp zI^7Z4eWvTf0IBiy(-gt|V!_4iy!gjVZtSZab&Pp87+!E;O=ioZ z>T}#l(tU$Ozi;li8ai%mY4N9~C$XJ(X7~Am^GFs|{z73He|OJz!y$6t@Yl|H%!xl4 zp40v<5(F_RSQ?2hEqR4#l~W5Ul!>8mx5!9x&Z2!!_8GCgku-SJ zufs!L(f#B4)n^e6!z~`S>sZNEobc+hBw3}O@+iq`IQwrKjr>CL_^Ony z``8CIN_w}D%r_6c8PR^^$MPC4CTQ)?vipQ*hrgUJhbdPK{go5)MY=Hz?BMK8ON z%nd(iGQ1J?-hi}j)4aBwW<7c6WfyI^YfqMfmt#JhS{8976`^t;>hV~%ouIUed&wL< z_WK=togi)NgKt;1&1Ur%|Cn%SAMyW&yqtd+kAAMJ!1>8*|6;Uo8wPo=OjXI>(Md^>+ z=76Lnv*;8))9lNFv!H3Dg9DZ7h>*;9%;wx^%mJ(4IeWH#*QKu}*=qQG9)9Gy*ZxYOV2^Ue)xVYK z^U%HeQSd*|g5+(*uyZ$dySYE8{&ifGJ?y3Y8^K*)j~dMfXWwMmeHWm)!Ok#B7;5eK z#5O0)Df8L;kFx)UuP2HxYtTWN+|6a#mv!#h}7tzzeiZB{D}W1>8yw# zv{T+VmShvZG%l{d%HOsxw3!Njfq5`~v9)fyDc`0A?KbO@Lz(36K2>@}0hzP?<^;!Uk=zvuX!but`*KZi;3rDe>Gg(=Xnm!8M%-=r1EWG zZdy1SxZ>)kW!ZgacVG(FM2`2V4p?mc)LOqJHT=MJ(^>~j)Lw%m+3^77olOhC3Y;Xc zQ=dn^%BX4KL$j6zzme8qG3}eeo%K)y4K6tNp)&3}2r9IVAm^8^O!}8j^v6y(lh(W#l8&7&m=PSfNfJzUgSQ^U zQ-|M(*Iy2-DW1)J{|^8Wmae3KTeMRaEBx5q5&yHod>Es=7j$l2Riv8C;q+(sNdKB~-X^vcr9DAs*KOJjYV|=a)>sy<25YaW z+m&j4)OYyPD#nk@ACK(5lW#RJa4C(G&woEIo3)=Nat>|EB`!Yj$5^$`UjcVj`!8+= zA(HpIg1*dvRMGWsgb2(;v6Oe8Ke>RVlzo5)9VkA65f1#am0daiI$Myu;aQ>jO8LL0 zWVoOMEk;N5jRx=RN^`UNtzWmWcqV7E?z^_xcXCw5-u@ff>+WYoulqV__MLX)mG+-! zckVVFpqj{UZM|XPRf|P+$QSr|`A)kcV@t}|RAN?2qvEsVo19q)qvusb6hBpmjZk5Yp$LuQhpK5f!Z^ToYLad+jD^QBpdGj)6Vd{G zQaX@A@?q*OOh7L8lrVW|1Wj|f^$i)xNKm2^3R}TG#v04sMJDF%_S66@gk|K5Q?%P@ zxW(`aW_uCp3-Ad|-GVAj&1}Z$P@pY`nl4A3x=h{$W(DHOVz4w32!#rQ51WXue-q(0 z?FKf*ke~uJOYR2n=O7lNJBI$OH~?VI4)kP&!rT?Q8;b>t#V~%s{tlo&Bxs@_jbh2c zt%2cPY2!lZ5pYjS`O_m3OAqgn>P1INweQ3JCZ}5kDTSGXANxqx?;eb+jWTB^Rulc{*J6NbVP~kbut#HVxB; z)tSrG4AhIzF8$LH7NK!X7fcN}>h!{U%UG!UG>__AVXK{aan|PC7z(Fw(@Hs`33VK! zb+cSQTb$H?25Y7=K2X?b~kRQ!3-OO->~q{W_2>~x-+Vd+fvsn_9*u`CSUN+%QQh7?#VhBxhq6W8r}R8_u(Q zmY*G>_sSnV=)+^&1s9~w)1z#g!{gE&OIi6t5?Z0NHY)b%bg&wxaF7_3Fm#_**pIAg zWhLy97_S&BLZ8azp7(qk*cPghajkh>e?SL(JIEcm$vvpV%w}zzHVKO|rIK*^Sm#{M ziPJe8b#inX4kp;Oip+MQT3Qf|$X7eVW7i8c+VCDaBZRSuGd}Bia3vHiaio`-TS_j% zd0-a%@s}Axp1Bgb`-E^Kx=QY0fHTn_(w04EHxE^B`f8puvg{Q$cGWoBv0icyN!lXVXb-)ITv{47Q$COdn5y5&^M&6FR1fHc9=;(@v8)x7FW4MzzZUA_-8j(tMZO zLwEJn8qpRFV!h)_aRvr*PZl)zO?4L8zI+jCFTC{~-q}+Q(7`miZ>cZ1t&h7y*K2JE z<{Ol4B^VNR`UUV77$droR2R_^3?zCv^SgM_bU0+ZB)`fee?qzlZHG9#o?A)~-=~6? zzm=2cKg(@3oe%|Yz~~U^^F&#EHKJRb%IqC>jZ!M5eyFiw%l#Zzv<^3vWMPn4pxTJ( z27kf@TFs@s#Vw8==dPEO>yV#@YLIoI!f3R?D0_LQIdbvtW>i6_`ZlS}imO>AWC%)I z1hS+cCwH33Rt>q~bOBZBhzM|`R7!yjf7WWaWR2vhAs}iT##{1`hj`zxM#axgeLJk> zuZIOiOlJe^vnR%xQwRP{n++nt|Fd801QGim)-1w*$@e3>uc-ZPp2HbbRGROS}7ORVit@XnRI`D);sS`kji@ zk?23%zc!sEcE2U=^x4R|dyqAjm%TUYSIey)>CR0z;dE08VP(0Pj?PgPti= zy1bxpb8aB((R!@I_mK^B?Kw4T%$)jDw0)FK;S}=r#S&qYI8Y%9d#CQ%Ux(e@nH-!lt=( ziQDX#*<7BnlQOSm1(@EmOL~Qs?s#P>=p6|4O4z@xBOE^r4Pu=vx25PlwUW|{ zo-Y4-WSZ ztliG}%e*Q&JmGqkobhxhqos-M>Ze{IMF^g_H9hdCbT#urbYe%)pDRWw+zmmT zBs-7hSZ@xgGR<(@sq>U&$@coLqi2w%DTcc7F9H*+*I^^yhbH$^tH+yGMD<%^wc@xH z-TcV2Oph_AsB^KA5x&Vc!*yHs5u{aStP!8W8}UjA=NLhYnXsSJX#F#zb%@;VX!z88 zh_#Cvb!POOd7Umd`(d^sqV_9U7=jvgL-%z`VsA#b()ay=yEJ;fa`#1uJZ@;4$0^<1 zpdsXwaoM7xTwm+G!nj(t3f@ne{a)?&Cf5dK2a*L=~a^^ zK92g7*>v{LpQF3wf9_LYIo20@`pUT1x!>EZFc%`!>nGa(Ix$gEc*3zGk1NcZqN-G{ znW+_VWX}wdp*79^{z%8qj?@CFgDF>HOQM(TAE-}gM$1B0+KM=4tW>dlV?NwyOMhdo z2wd0`4N5}?Ic>s{G`)w70>sx)=1&;9MA>a*TdOU8kr4cq95{UX&E4>B;R*G(zMJho zMv_^+qfG(gSC?spi1by7m!rNm8!oXBnBKFzCpabD&U8CjvE4oY#G1wO3ma%(A)ie) zubBhVQQU&uTi7yJ#)MN@no08u$ivp)=P6Bca~b}6_33nI8Fl?k(|hF(R#W>4Mt4KW z`zMa>>n;b|E^=q$kPU6KD=y*l$1fyFXH02=*ivb{p2l{oMd-^lYS1%PY$OYBxuk}j*|H|6IxKH{FwpX# zd==gE<_2x?Yb?5QBvjN39v@;T!kaFn+i~h_~wfjegC+T+Qr5lDFS-&xeRAF;CuVtnZ zc5y)!n|S@S-t*w#dx}-fp=1_<_utr=wOqmrhRwPY2zhFM0qYjU0!zgiR>~pPSxnN|t!%klpxTfeN@K zswX79n$CyuV6{TF9as?Or@I;h{O(W&({%xVh78rfHtn=j8uIDD22)BXwZMZ<h);FJ+Rh<4iw%4cb3byf?G7tPUEzW%TVKd+IZDC z+t5nBQCCJSmXf&9;E&bmF59SvL5x>%Y*GTkt%IoQX*lfyRjI8myweNZ@%J=^kfrPu zuA&1KD8;Xb@dsD|nzwMqpNm+8%9r)`Y(>YwO$0}GVlCn1t60zQoFeEEaAy~|ujeD} zG3ya0?_`y_H#}Y%YzpTP0SQXHDvf-Lc5I@`u#wfX1bbS4q9ghhqW2_zhE-;Vd91BV{Bp|| zh^h8Teorv!+xux%<$|tIm{W=kqw)6}@gJwQ)Y|lovlrxYi%b^m6rtY>U9TU&N2dI| zc*u#+9JFpPZjtX_O{pxS0h8K6E{<^UeZ;_)7Ckc08*7dbpj+)9b9Z6b<>27`1&%Vm zK@7kd8xhyNY~pw}yA+{-&zbVof5=_FBcWI2>+vVj2=Ov=ZyIwejBUO5ai{?Yc`jfj zl)e0@ZR-Zbx%z{wFFR#`SK4QjX0`oaz2AQ}k*I-NiuY^~{x9O*JR0ghe*YdsvK40R zO18{cvdoB*#y*y@%!Z+oEi))fDcT6dShB~EEMv^7Fe6HzRK`9n%&4TJ5x<f^&+B?znGM)j#5OXh%(s#d?Zkq9XFYNP=LIaL z(w^rFjaM<(2ahQ-K0Sbb{w2ovtP6=eXS#giKU)jdi93H(RG6$_Wl(fUBZ&u+!C26h zvZzt0mhz?XfQk!nWegWdZ^L3joZ#<@^)eDj`!WNo3Z$$Mu-1*Gf`y%2CarBj5@1SkUsUBm@fDXc8f zm#S*Wj@6i_Ocr)o$SjamVTU;;<25q~I-RYP28<_fWoh4_hfz{SNzvAOfu}hQH{{~-y z9&7{IyYklPJBX=GiZiXoZN^kDkPjuL6&x%Fm@Pa+x^1ecfUC#1OIry{AGn6+Xo$U} z0|HEJfDr^BOPH*P)NILNm&TF9ope16sjuk}V0+4XWG88qHv)h0&8}@yI&MMR4r&uv z0~t0+!$vrd2LkwwxTvYSO$xW&Uyw5Z@}hc{xXJ||w~U20ZK&ibqZZ8p2g=rJOzW-qbgm#SV$S6q#o}7PoBxxE`2YO< zcDv&%htGfb-n_51gN+%dl7T7zhv%A&3AI}v>uH3e^l~J{vEz2DgmN+`oLxRIPHX5c z`>KODVv`zd3`9N;`;CRzORaMRUQhw@i79U99z=Xg;>I+O?ZUlr!_F+P+R2aU0&+`I zO}_N@8(mh~tEg{`NJm%$YjConud0PS^Xk`&#Fp;cW2K1~io5hg{9ENImYa~m!S~c< z6E3#)6Nh&z1F~Z#Z2EN1hWr{UD%o8|xU-(>VPol;2;N%|f;(Kv^*0s!VSB}`2`#$Y zjHZ);agfxdnm3JjeHU@_saVDLEcuQ~KOx{W^|tOnlQP%ZJ@!S2mO0gzSd)GEtSTDJ z53obHcu;HdNJHyjXGu%(9Jb*L#@OmLbK)69*JaAEfE!KHGR;LO9w7ZJM8qq4s8i_y zwaZ~EK9J6%+J2Om&%9L+dOe@)N|IJ?ef*0b81}EYE)E7`R&`3dlI~$(E{EagHOT)C z>Rnxs3Gdr7kBLYxt$L*Nk0p8wR`02n(6Su5Nx7WxrYd*3Nj;i|7igQ*{!?jqo6Mc~ zrH33;O7ywIb&c_IQ2MR%43T5q_wr=NP-2vm0GEsiTZH(92*D3fAabeJC z4xgEvI@43)K}V=uz*3T;F0d!v&%kbZV|aMgGfmN#!NQ@2{wb;2AyPVqgQQ?}!0Q)i z$YDwm!hupmYEP>K>j7RyJ@g>9_hSH*Y$R>eH_oBlCLceo#BC&b;CQj4GuY*T)lB;F zcExvk+goBQ5iXI`4mRbEtc`u5v*!?sFB^-yrVSoUxvjcy!aB^?V8vK{PP}~F0H>5+ z5?NyF#HqK7%MXyy&Q(JTP|HD=hGu%+# z`UzJ{7n?umaQ^S-QXax5C@IY=(Yua#k9XM#!RYRlgV@I;RY+`9VXdeTs`9Xf*T~bd-JlKDFu@E6pOZd7z_!jET!D zlBK=kN!YW2kMEaHxA1BwXg7!-=tZbUrq!IHXG-J6;)l+;>NDLt1DA3jkB?Cn%)@Q_ z=$vN_2I9={n(dTp2Hbwl5OW~e(4m;0$>r*$B@ZJ;`{_8@8}Oews&@_~+M8)|a|%BZ z21Ytg1m3ZC-Ne;AdG|WhfttG0w(S5PA0NMrHPIUiSg3P#aPz~>WB2zwaiPW9U4|$d zVEpYMA=0F2jXIwHf%rX-R#K;#nZ+c0U8(Bf531azXVL*g_1@79tHc$zenOm>q07RW znK=4aF5}G_8ox!OQpg)~DSVTYgY71lCG4BYQf}?_ug@Mub5UaBvPuQ+zzG~wF`oVC zo>u80URbFsEa?OI?J&e(IN4yZ><+Q8MCF_2?SvBRruVCu)@bJXMT9YmOGO(@Z_j3dwh)x#@uKDuZ*P?wINBh0}VxV*+(_+Nelm zc`WQ(TdYPSIkr340k{96r=T{}xAoFX>-43qLpmK7efNYXI;)%9;GgNl?{$9z^CmIV zv|Rb+j2E9%5_NTaA&d2uLOkjByVk%5fg~C8uK0lJ`gAEoXZ;czFoIGcMXD3_7${F{3fIrp( zfh7;lww+3hi~$xF$UJ-=4<9LMuI}%VaJwE~OEi+@xXh{X^HoQMyAykqGC8v)8rBb; zoTZ&duF2Zzvb6tg9d6JYIrXHzGGv12-SB-bR~U65@<_VSHT3ie&gl3&wQYOBVhZAl z?RrUs+6z^aL0UY&NTlgdn?gRL+nu%X_GXJbI9K%2 z-t~2yM~Po|gljzT`z-M*(TAK1&he-2cg)3yLdmM4 zXxletatdjq()QbRIZm8!G25lLjK1m~nF(qL#nO9%L#>bMs8U~cRW*Td0X2z{9hh#_ zf7X_WGQR}Ux*Mbs7Tv?x|EPr=mZ>B3F{Ksjv8^YEE>guq3#P6sZ;cxh8*cX5EK<=v z!KSa#n4TB#TtsCK0iHAzVSZDY1U77Y(#i%wpSCFnK1V9GQdFIk+%04-lwk{iygD0WZRdrukV*BE zw*bG#=xQnz-42j61IZl$)>hI|#0><7EFkNN8TK`j0?u|BhfHv=oA1xL!%?0T_~0aKV$c;LH}J2iQH5kMq0YrJovTm;e%Z zzaS^!c1~w++eWPW^TA^q@yb6R0Cc95>weV?00pE$)-80yFH75$F}CB5KSAeu=5 zTe$C-NCo=t8F<(UJkV_Wdm#*15PAQCIY#!fldit~swl`Ud^pV;9pQYw6}RJM$KVMi zP}VESEBJ;AV_j>+egK&{FW;+^j`U2pya5fk0E$!0@e>@e@1Hzm;K?N}Ov zXprM`=@If^YqCeuEn-{k{FE--pgc6}>__JFPvg&Id`w)IdS+pht)h1#VkFxn0TG2~PU zSE+e)3ya`cyl*M7-I|g=ga#ot!vTV5f=>HZFA%bw#7^A@39qC!nmILXoOopxdgJG} z=#Ukh|-kO4bj_wKW2!cYrNN0W$okl^&^gq zvS|0E=5$_?UX=E)(SYLDq$dX*aNDm#bD-oS?~3``$)H-qULt!V6T-|P5$m_p6=?B7 zl>|J(o|;3(Bg`~&1BKpyWoI7{1}mP_M9O|+Z^B>Zh6PrERznbeS-?s_rCliwK+Kp$ zGCAKP5sCH-u)NMWD$iekqXO4%oy<80Wu8b4@NyoWAa$mQU5Y9LaCe)OVYApGoPUsP ztxCE8S0x@k0IRxJyXAoC?7W0(Ui&b@MIEFFDfX%dC}mpVT`;R(Vk6xqx^t6)v!rPU_4I}2hwyBJ9^-<`ZS&3>;jbR1mn3 zmj+6DsfBJmY}MUBiHc)v2CHC54!%Ha!cIaX7LwzdVp-7NP!{xH4oVL2Z(MU%qD}kb1MA;(TT!PFZ*9i+O9<%id zS=h-IGSX-nI~=UW-yJLm2n9u5ChaF*PvwyuiY^*V4ZcAH?Vgn6>mtlwTF7epQV7d_ z@+u0F{+(}pew8|2UEBOu8=UH~y!Q|MKhVe;!meMgMbNTK-qW zi$!45eU2L&>h(I|^e6t8sl$JSA^Y+urH_+(O%TTS5<`+ov+#MHYn(SG6s&_u_P_X% zIoIko$R&P+zf}geBYm_^K||hifby#1;ldIwj%UI@Y^+`;%1I|I?~ZoN=9?5 z<5&T}_NCMgqoNUcU(M&`zl8AkmP201$JEh)Nqe1#)Z$gFVqrUH!y32MB%!OmVLKi=_tl&mu{w`Ne;LxMFCNPtpR>5w^Mzygs$7)Xq&bv&}c)U(w|=6)i?(2|vyx`em+Ag3muz|tFP9$nu!*>JraPzaa!yWXD_lbJ1y z!&bl(Qc{VpVx54DGWiTl>@J#!zJstC>$8&26yV|o3A6;dErV`9SqI$}&%;=wii<;T zfu)Q~M0WLrlOx(M7olN;-haVrvQM?3lUijCn@vfbq&faN#!R!XqUZ;b$Drzq@HaAF z=_tV#%IL@t)-2cRMWVY-cX&q92?7mc_>|MetUs~S=cpho-}N-+dE9ihhn2X#(16w@ zxY34BAA+sM)pWO+I&+(Nj>Q4{1Lmj#^-mT2=;8$n%L;re_)l5&iBAHkKasl z2;T4C<3{;VTJX|hCQT(Af|k5k4N2Zt3;bip&Xiqk+hxoN8AbvAJ41*;I`J0*;A~l2c#p?o9QNDxA`|H$so4|;KSp;xBH2` zz0H|=L$dxhwHAdVVxB!W+L^t9b>-qB(Zef4N^sn`xpuZ&5G76=dYoU@?hK)YNY z3l5vd3*$w$y~2=+MPU+N_5BdXP4`8GrLNA%*|C$v%WeQaz;b02*E&c=TJnmOM zhhtUM!xt?OjekUMdG8G*e08n1F$N=PDByx}RCX#cr6osr7%_G&>*(5rjS3aj1^!q9 zNtPQki@wY_EK*kE2m8fl83@5>3w`sV;t%kjo{}qR*-%E(W0Pvp3fd!CZ2@w2%}5y3 zlfA(JHyamjB$GurJw4Z0eIun8g6xpeX@||Dg-37jF=f?G9h8$Y2dYcb)pVxI8fj2+ ze5bUn=V$|_ws0@>^PmS|rRGBaaCqx@wq^Tj`ex!hyt_CQRZ^Ru-eVKv`nzkW^YEvL z6A6Lnn81E&YOG^yq#7ExgL*rqpGw0?Ipt$d+t(X#VaC)!5q$f^I)ePL2IA{x&P$rw zKu9NoORomK_KN5`)WsnFBLeS_e4RB7B|F%EJwL=~28k{^#9cz{mx-6$l)U6bLs7e_6p5Q2_5Ll<>8=zq^P>%xu<{`!2r>3UG zr2TKV?Erc3@TrR(G?I>Xbi=Ige#d&!$_3-jWA$>q8kU{Bi?LDBbg#4unNG5)hiNZ6 zLK$D5c&J~)a??hr_S0U2khPuUXDdVUTQSo_rNJNV@{Ie~`-F?vQSvBxPdy-;xmmVB z-k5r_DLR^(5P!ER9jOYhhWZ`SX_6-RbkV#)zg)Pa0cZTzq6MddO5G@hK|+G!EOZ0X;E~)Z zVE*SO({;L$>ORvP^%c<}$Ei_Cwr(l;tL4v|t(alw0~X4fPSRR0KZV!SL*NUZB!4sa~+uU~;Gr68BnFhQF>c{r#2H28OCB9Z+a8ZDs zjXdKy#Wx7t6ooaAUJ)E(*o%)}{7CNiCj`w3w3T(HR6Ec>(}@xlB}X+W@OEl>9b^At zZFV!4WFme3nbrOL;$=jJQ+5$J(ki6n+)47`Gc9c^Jq=S^_loWb&itgducXz{Gu;A7 zL(y!Ctl-2!E0NXn5`PnImVKW$U7_vzJfY|ui&MYDN+B`w^kYepjQ?%BKi`6klNqU% zfYka?Jr8KK};a&RRE5zQRMS-(Kz2h~? zDx$ELc|*L%aaBzr#2hEiHJViGl#Mx3|R= z9T}1GW-##1W~hVtRE%vB;EmT6)ze zvTmtQfT4bK8+um(6+dxqEwgbYtdrKxIpFNO%I~(!AWNnzvr7m%j8`AOF=B7(XHDZ~ zDmOXi`j;tVOL;Ym3Qeoy9i-1&6BcAMGg%XxZc_b&H_i;EnfWPzE0AxdP%mXMm(5X%66Z{Vbm z?55#RhpACVLa&+zo_(~DDZdDb-?75j5P6cUDNig|FQRgfn|P~1!`Q_u*-Z7+@S-ef zfZkM3@^6w186gb#0(92LJ@iV}{!ENpKrZ9^fTO0YNyZRnls!r8B1f)c)< z!ln+y6svG7+<+~6@d5EoHew#Y8G|OP| z26#pK{b-)a9)VmZ(TjyiNyNm;GJUtWJlX?s@PqEp5bgP#psb}u_v7lvbBB?mn2ICv zPN{DCZ)F+q_m7R=FNJJe1ame#XL|3KHX{&Jk!m4A8Yy^@iB1?m(I>-U16+STy^-L2x!H-8|o9?HTqJ`A^cmk>g}M5fd&tX7zU zGMF`XA)69bzr4JO6P&7qlf7_M^@rd$sh>i4&lpt{)PvsabU{c$O5*-Mm{PUTsYe^~+_Eq9kPx2>FH z>wDSAr{U`i7R+<^vP=>`5OZYOGmW0?bZr}o5AIoGYQ37_oi2mAT(>+KAPyM3+4JBe zC$T6n2=Zi9VPPr!gx38=%L7r1&eyu%@DZ(oq*0J8(_aU|iA5OmNW$=yAGAUR^0Poo z+!)5G}O)OmxMLLMJBWHEE<5%$;m*-DI+R4zKy0^?P6$r~iqNtkYY$!_OOD1fW5w7YY`( z(rFseO<4a%a*}6DcsLB7#Q?-pO>u)i`I`|rM?RpXJC+w%BJktl-2{Fm7)72M2_>|H zMM0^?vQ<-xXT^t;oi(ZYouL*8##hFlk-e7qDy*zhwSCpZn*_Vbrkbglo?o4m^I?KS zT`JT-{DE%b61oR&=G*a30($3l=)ltt8zeW1OJYna%c{xh-2@xS%n+-c__hdvu{sXX zJwWzaLir$C;#M`h`OP2${01j>%0Ey?r~|ZgT2iybay>LlyVW=MMuHUBlUaLk;cnCtt>VWi%CB zcrf$s&1c~t_Yda}zIj{T5!u{@ZDU;HNvF)#y%|&1rHgkVukPf2R54xmS>g2d?@Xz@ z`r_&sgAD|7G4}kZ2faEFX6oyoRmJ7HzgYPlCC9Dul3WOB#whBhoJjo0h#|R%=zrA7 zdjqb!ro=18^pI(gX{!q^Vg*_R=G2ck(dR*=sggm;jU?MdJxL@~%G+tWER02+(A@uZ zE)g7m)Oe0sft79{M-(L&-T{vve@Va5`525oY(6Zd_22%p1-LFAm$Dpm%@nSed;L@_ zynKQi*?T!g(!w7kt6JaFRd^jL=e#A-zg*z@GRa36T>d9X@0N;J^Q`=Fi$cx9U5&7l zg$k?)s8u=!es9+h$*H9EfXV7k#FM4(+mo|}DR%pdOB0Vg4<3w)?_1Q=MZOb`0Ncaf zdZX#nyW%bqedEph4$EoFRQd{$T@8~cU-*D?2MKvm<14m2)THk_XAlBS&P5bummK5) z>y%kx`81pJ%#f_xASoDRG;`L;?rAWiG;&HGgcp76E`$bgpCdFBg?~5ZEjV@p8Yu1* zPI5xvM2bKH`!A*H=)H7 zECtVlr9|&BGpIZvc9i|GTFx3{S(R_X4jF`P7X)-g^W3wvG;CW~50Ej~!jr6Bn=2Bb zx;@UO?GP&%4_!&LHGdu6;~!&mW0boc6t;>kwdK2h_O^74AHl<7OREVSZ@#&`UZ*H0 z6(ipUbVzoV46uIov29$5Iaq6~u^t4+Ctfh1hD8e#?r!9pDkIOUH4ySrijUEUx9-8` z46vZ6f^1b?f%nSkA2e%$Ii_r_J{eD4HGBu1H$gOh44Y2kj>g^Gaoa8~-Y@~>zs9zE zo@@a4lmuRoKhfj`PHb61m_inQ`DC^ro|aUeHJePNd<;|#=vO))YfUKXEOZ{nJyPdk zKU$A0LO^@uH;kNK?!_NcQ$q94~DxZD!g`$*>BZqr+4?t z+4N9_J+)KAmG}Sz*AzJUt1OQxkS??&lqNpy9ZXxmDA~{eUCBY_dF|wtvY$q z@1F<8WM6F91g$vPeK9vTlKY-Bk=aqYW}GgF^(FOwZE0L!H2l1`!Y41ykW4jTzdIUq z`yh|P!(eYHZ*P~h8gA(9RBWX@rKs)3J$fFc1{*>0HOgjDTCbA#hHY=}&sJ);EElGa zc_(Y1{^Gw#8C!jj^?30radf0e|0ihxyEZAg`4ESMxuM$tZ;V2NUW-DBNF_auv>YM@ zY)zFexT8TILilnfpD@-wK}3acz^k#t@BCK2!wM`6Qk(8~g%lRNdr~&8omzt42L}xs z;L}{mUH2&k*We%q>^P$eSA^75`rljvg@!B8s)Z(bay|$0}(1ei3^@DuqJ=Mt1zEeiVE~5@6chprNR2&fCsW0rUEjX45+-qawK@G72dgd z{1~?`TEg>{c6V|`VQGvYtS`WsRhnjp6!7+_hM%irRa~KSfvpKacF<`9 zzOV|+8dHRK`vWDZPiuy({Kz{>AWJRtYTq!D&vMh;?Rxa%s8s4i&5!MX;qd56uP}E1 zi(M|$1*-lfNV{dvtODFm@z{9Avt{(p#N=h7(QaQv(yo`R?q%ysw*l%gNrDD~Z&?@TY{|`F${_mO0YhOp{$ZD7V+R><00^?%Jsg7_F-@Tht@J zest7HpmRa%}3Q{4@~TEkhJy{)Am`4~^VtVpz+0UQjy{Xi0SX z4DBd-SvHYe485f@UElIA7O%Y*hHTm%Gec9iU*m#tU)$|FpejwK$CxdBN2C>ATdVhX z=~IwN0`qT-#Y;vaLf4^8_UXM^x`Zd+G4WjVuG-xq2k+qcmf{+k?6x~pUd69oAt99# zn|As~lk!pY0iN_HPrQuO!nRp{r3F@9o6LsYAf|n#|9xqhbNBfKn>_<#pZT}ZL9oGY zp6KNZXF?J=jKr(p{IUM@ZZkjdHOOvZ1m~&1~R1?)} zAguCBu(o0$G$58O+=kFCt}$LlS8a9HDb2a9lv5+hbs|i2+ap${^C>k0b=D~;GJ4UDeP@Z$U<0B zOH`d!EObOazqQMyhWn+6v$UI(HTi@Xfxl&ZGeFhmw6G!44ivEVd?ceIh$#MouZpG< zfWSsOlyL(42ng4(=1z;F50F*Tk>hra(1Yax9ziY?q`MD%?qqYm5e*~~F9#l}dOV$N z?KMd|QVre@^Ik$J>T{i;-05m4=>0D-*-^r@TW1X>3qWX-SUlj6up40z%QU=GF&`yt zs9@g-u#EjylC-&C6|0Bz7V9$gTH)D#C+S!KrNg(KdgxdDtyz)y`CQ4l73c8(Ku`DX z`x;yQ@!S7fl(7H*5hW3Gb*Jy#IJJK6Cn631_+T8F7f9mxc za?{r<&ShOW;(=yrUYKFRSdm2zvOjiLVo{BSGA+Vks$}c zdGf|c0wwz6L_7lu3RNoMytkQ+*cl>&;%+pWQoLUiAn!1K}R2;2KvD8>|93AMWbxhW^&z;STre3z*v4@l+b9(Qw ze~(|O59_wo?~LG<9Bt0)!abfiAro*+b}9R3AM=gpEKEN>x#vJ$gB&r%Ce)cT3u;YH zAPOw$+VH+3LDu7m=Fm#tKGZNG{*`}ko=ztTvCl!^7g6nst;&w$sexS2kWc! zr8@hJxvimGy8a3K#ddldqN|)_@RI;ANiz(K!+AQi;*PuPctsZn!&}|J2cg)TWecoG zO-+i+&ydgOW^4i|ww8{|chTmyyF(c|t23>Rb8F1CgsbtF5S90&`o{H{g0v@D;Oigv zpidP&Yu((Ciht$Z)nL$sNImblJ7K*& znojTMCfll$w=8tN*6ujnQcqTP8YwBs09YFrRgOH@=(OfVkL z_lQR?NwU-XY&%rrRXM3IOFi$`m1zygUc1-K^<87_O5u@u)u72{>04)d1;!Du+np6L z-?(8Q%5z{qV}X2G*x7QMN90+K{D|v1!$uM+&Z?uK+%ZdD=*8%?a_}`iJyS+}1;k9$H#?c`1}`Eq0u35U~x( zbsSPl6W^Q`fq_YeNIw->h!C&1T*!`RE3nAuhRxtf1gh0khm~e->4(L|>9Q0sF_idK z%uU=W=aAATlqyv%*^=0}eSsIq_`l zm*{CJY{YEHQeQWctRiMi!3Qrv;5wfcUF7&WD z2!FE+LUbq(TJ|XHq7_J!#eYe30-qby#oFB@&4JgaaKaRaVw_!@bL%>LlWmB#=PvQn zN?GQBq99^(PN1E|bv2k5dWf|tlbvE9xoNV&zjAwjcJflVW`xG3j=I;YT5L2aZ;5yc z9s_l_bS1!>tzViNLCo)Q5JZiu6F3XLI-{4Y|7k8qN?T7@;tyo((B=H5dYUmw2f@?S z=HeP=fG6d#WxYjB^1*baw_MEygD1O=w1pAAHCzPYPr(yfN_K2Bw}hQKT1%apr!dpY zV5S^btPRSRtaT|lEMpIIdFz+(o5jMn!PcdA?!!|2lb)?r1N9cz_~fo4^CyDvY>d5D z){SH}6l>AiKH-DPeTc=QqE#XxCB}fjGS_NZXjL%WHR5dC+E-*{7tzI#b}zc&=t6aZ z)>8$t?+$q~uM{lWY&%TcnU1>h9~a$F0oaS%E<^FO2CZe( zwL6s2R%$@}Ds{iKZ6$weFk``9x_btCimdoIQOUdT2NGz&Jafy(Nx($4I;R-4N@Rv=M z001@=?ci;{goC${+L|Ayhz)l#sA3~;67LO~E=xH{S323Gu#cFs&00R){}EP}cc0Sq zhWZGogxAfa%f%RAYg1AEn96b_WJFrd@`mUKZ@4b5{7&xQ54-!5C$sF?o;76R{ zLzdp;7dH=CsGo_UsD0tYEOOtspR zd&ukXi}CDCT7Uo3!T8RPJ5QWwkKR<=-bk*VxY$V2NERC`E|^o1_*G?B2j6scD7B;F z94(sr!g0HhD5rMJNE}lJn=k35vffamLZe8mOlI9l#`#shx&1vUiri((rY3^?&24^n zwB|_ZGLJ`(nHI@1GQH~IHP36a6Mc>53$#JRNAZfN&SVUBwIgS5;2sy4T(3Z*m^Jk0aaQ3s?WXuo z)Qv#AaaWd9AA5T0jp!I0XRKW8QBm>KWO=JdVVEFgBK z_lHE6oMLoqk!g0(Jl$MRc_df~iK{z_z}Wqqd0d^Qio*?7Wp|zZfXJGv{|GSnjI7%I zwzd5=Md5PrH>WPQA^yIAfHYWpGa!b*tGGh*7^+$IHHg>pv0A6ZCW*&~nxQWwoo zuC{F5@vZ5|;_Ax8srjpv*v_&Su>QIxRm=F7u=ua640KMYLnTrL8dAmfI!nBi3?e+E z1d~G}an9=uS+|gJ?K(_p)mOUw$yPh+VgPP|Y!6i-ZL=nr zNZGNe!Bs#Bs9CR+b6wJspF+-9Lh5#JZfxuMd9P5@8Z%H!26Y>)lec40e?^aMjyKfH z#zNK+qN!BJascYrLQqc3FO|Nq(>zs3DA%``5?x9@ey}E|8tBiiErpp!>YYB5AbY}@ zPD#}T8b9qtUa$wyVxCM`aj-skQHSB%wCM(}oR3$ltDHtWRoUU^rF8tDR2446nbo7mKd3ki$x_NRHT^;7Qpiw&a@(S zkP86K<8VOz|FMX9*>CbD^j9h2-2e8M9S5=}F+lc2@;{qBdG-0x!;S@ibvz1VU3~s*_b1mg`5ORGFg{tbi_L z-$PaBow@bS|E7n*6cJ}<0@TC1oju=k=o=xO`sb@hpUuV?Ob_k~9}v0~6=ozwO{1q` z5Lw|}w-aFttJI`wS>U{w9NvW>21xTp5XHhwbb@p#^i8E@SZe18SBJLlmF}xMs;Bg< zJqED>P35&3cc$7K9b@Ri)&pT$e)W1@bszghf&KU=m#F!z?7(|TP0RQzmc9~(-cIsM zd#Eng(0z$AnUKktQCaIK=*Gp_Qwyu6TO+pl<3NBL4t zG$L7JZ(OFHT|U%4=88Fa)b%t$aQo@X7wY_EBwCOlNJ9>ht30a26_K6N&QFC_mY;PjPdj{QJq?iXMN6i1cKS? zZmjqev7{H8M@>3`Z9B9$!%ObpVcOw}T@U3y zKZ#L%w6n%(jjQOg@2Gz6bzGYE`%6&XXH66fg%FWm%BT-*g1luOKB{!7N~t;^DheB- zM@u8TgrKq*z|S~qIGt&^p?~!js%uLIl~#lsiE&G6l4rWSc)?0*zG&Qj%OrbMbG>Xsi`hJ!I<&Zf zTBYz1zS|%by{XwVy8&x{BRshHIOBY87O!p?KH52P@W*}|Unc``n2WAP^IT~3y1-L8 z_XFBd%jJgPA5FU%JFkb6{cD?*M|-=W22Uo_iT97y#|7W(d*O4x)2g`zsd)!Nj$Uwa z|LGN@HrXpk*BqLzTtAfNa9~klz15lP7_sxy9!W2$-+o+Q7rV&rb9AbsS#ELS+|{i~ z8M>+ed+!IG^;YF@$Xs8dRc_dgLLAS zEjqGfXKk{`qN4j3uU5a)NsdBDqBd*SZCQUe>G*E3-mHQl

      Ei>TGJkSy(aHiTpQ9@59YCJn4TUlScpTo5$#9T*fo%sCB8>al2v$V=hsPB9N zIEbO*ByP?_>as*wE!;aY@)4Fp3L*sxk2M~ilXJP^)~Ic}R~Y?>^juu(_Pt8gATtT; ztZP1$)%JE*W34m=Rr{KwqKt?{JOA|!-PByM(QHS{JIsg_X9pI6%}e!4s%oJE7~;%T z_DFs_eKm>n7rEKjqfEuzx1@=>Q5n#muNqmFSho))Qf_o2J|sO)wNHxnW-=E=Nxf1g zU=B7qt?%Ezq}O>pzC)m3dpSTu*@`#8WeGlaB^b;VSuEbWzJ;gbnYncL7Tj>4lA5wV z&VNDsadtKM{ieA3Lap?jK=~yTd{xI|eUhvXUySvAb5*G=Al)JoIs^*`RzZq53(V5> zAyrnv#F{2efeXo3G1{CmMDWy#lfd#PW>$L%&Dw#al9$z3nMk`l7rlJSu6xuMzR$+W zi7zla=E2_%(3)kThMs>$9F|$KOBR@jTLrrJ_|NFy8qGp`(U#k@PHL;nmi^dg)|VB2 z+*LlHW)?oG=7QXJJKv6Jf5nV=jjd2i#*(&wbwZrYD>3(HzZl3N4bjzEf)?PUr5>cMFs z%mHf2=4mxw`1yqK7kRT<0r6y^=f5JJ{56I_c`cCr(u?Kg)>44a=Ok;+YnZyFhuhol zqQBkzNki~G25l{gZfHCbU9*}Z<&}l%A}k-Y0eg1r--zWoIEEKhA7x3y)54r`nJ{XK zHf<9H>S#gN9~$AV_hq<(W9IFd3@rV(m9vfTHqx#^GfgNX>I}4?==1-_jYok;1%=io ztrwb^Q^hvBk#;wWlhs@)1y#ho&v~`wFO=CF2z2;p)I=J*o66$_5z(Pv@0uuQUW`5U z8FZHS8QH_b)V5Jj>&3aK+K46P;W*sN;jHp^lG*&h>^0>y+wqoWHNgbQ^B??%8IC+e z?ewpw1pbN<;V$Jb@4hancKY~@(tj)DoJ>{R6EC2qC6Ku+{`>WtJ#;qEu(mbD|BP}2Phtb@N3niaN0Mddpey^cPiKu4Yu#E{C>3WMXX{K zlWjq)2xsXP8RGTfLbq@`(&xwF8r^HKU9sxn} zCQt?yP9&p$|90X0_QQOjKxxwS7^L}icbU!cZ7^RUn!@tFoa)DnG$PXw#ak1@y2EU{ zW00)hTK^HfpB>V#+w{O4AmD+H+R{Z@D?0BjHMnt*jnsVp5rl2~eccZCLGUh7i2jMW z#kP(_T{$pY_4R4Frif)&FK@UozxX+IfRbBI+Xo`?=3~(Q zKpd3!{*zBbp9hYHjzPJ82Y$yOywY~};Vqz$?-+E5YRialygjw;^4;!s$<^kBi$#S~ zX{$h8mHnun>-83(;?1U4ZMeH?k8|*Yhwt+8hRLiQlg3m#|jXaOtqITOST+eKBnQ3cZs$m zR9VJ}My!K!ZMjV44d)AAx_q)ck8&q!r=?yZJ&Z|PEx<#!O1$zybHk?CrA1>7C(w>g zIat&u?j+eHzlKzr&;r^O#^f)I;=T-ZLGsH?W6X!(+F`4FeiF(w?PfXr-F+DwR^x&X z?}GdqvU@O0`E;!4UPsmk&6Y3qOQgv2E7IK21`W=#XXM(_`!VynDW9bwmc-QTs?_GVl7HLP6}VWEHYd=9ZZpJIh18cZHK93 zzJm_DOpaG{79fPbY>9HLXW@@P(W1)fsP%Rs!pOJ|E7`=&yrcH;fnse|Leonhz3PkB z=8Y9;0f=FNhjExDN5&2ME~sJ$^uFyQ+z*mMn_Hr>=)jPiZJAOfE^|c_!3SBq2NJ`V16#Esyv7jqM;ic-@jLv4CI}gK&@~3;}i{wDzpAui- zDW&qM<5)+T%sMo{1}p-#9uM5HwqBern#PXlf%m#A(HB(EcRE!rZfKLY=6g?^1S_O01whqFoS4r=Fq0QVj#` zRmd&C$zhG{E^8DMVe-dPQ| zdq*x%3o2^IRLKf5GAb8dC9B5X?x^|(JJM&&8l^WcEg`$|Vz@l@t53O;6gWm?c%WC6 zJBv=46PYe}iVB7d?7EECSJ^Sb{CHkctMjz$o7i~`^`-ZR`RB&Xqdk5YMJmg@Z^E`U z#S4s?GwXiNfRf`-Za*H7AxkBn(dl)%h(l$JjLi!YPK{dNMXhN&5h`zgqWajQc^&ug zBSsDMr|j84l(5JEjn7H?}~B;(bss|I;*`4Cq^M8wwZ&widwIhUP4o;Zhg zQ*D&5M_Pg;YuhxU!Cwc=>xAnjcBicKk&bcm@t_N-UJQ3v{*;8%(-YtO^P z-ZTny(I|<3M5~-q)aaweWLSWGChw@=l1q_|SL(ssFU_$nRpez^3ovVDrUA}$PLbeF zy$1g9+o<@fjno>K(y@sL<uZ_((*vF;%zYKy4-@fifomaQ9I19pfy@Kp zjftHGdT@(;DrFSuSg}CX+sW*xDx7OdKQ!-x3O@wi3K7xOkbTgauZT6s;N->~xR@z{ zMUm_01p1-n{FUa(f*G)ubnm?SdMuTTVTFR_?|W&-R(v>NM?sxN4+ec9<5se$hyFFgvs zXeLa9n}(Xp8$A#dl!A=PQm?)|jD>WM+VE7)n6w-0xGZ;awK_CbF*CI%;O$3+=Oxx3@!Y%jUOoV|B_0Cq6Yh4RTAC&|CPr0e8eV3RZ#!w4C=Wc)ylyCX)8 zSVO}zSfbld>V7Dj^+36>S*vut$K_tq|B%K$e`N7|)J{Su_FI7QmK00o@4~zCKz$b0ybyU=#7z5p31yN; z>ucdED*|B-NEG@T*Dm4+c$(=5%sq>VnVD)(*3?SRcaM!*h|VEK-odr3X7W`jyVr&PX?Ad443=dELI!J&DNmOf9@kP=PL02b(GPhPF5#AXK#hRVzxHzvf8r3D)-;=*aM#Zg$cZEEiCoS zgje~-xM%kqo+!_4=2I15&1z0vCP--9(s?TX^Ru;#Xp`t#O?Z+ON0Jd1HzMI`C~`Qn zw5QiW6npl)r}AMMkYe}cd!NPHi|_63HTsb=TqSo^*B85RC_vFWCAmIhwKXbsMF0A1 zI<7=y9)GEKmfEwG@&4y&xM%-Llp2F3Mf&NZxN|9A?#52;$K~b9{No1<2=xDoto8rN z7`}JpF{$EULrcabJ+9}qRjsL)#e_NV>hGYii$8m;N#DCSDO`8b(W%xIh^k^O`{XQZ z8;dU9BQSMhm{a4N+pfIl=^2QLQd79uBDJ70xQBK|S@{Ih<1|qW1xN%IsvkNKOHco-&@NC4`!Ul| zw4Y<0ghfE2k~PkYS?^52-s|QL#HFb|%=RZ1fknLFbrwP8mU2!eHM4GTK%Qarw7-ev z%O?V_8}r<`(BqF!cjsnd?Ji7!h+VjNFqo&R4USE7}1J zDKq!D%VUP1KtFf)y1(@mI&pU?1s3vc`QsYW6|C2>-DX{$)%zY zxl8BV3Raq@6uj_qxZJ5r5S3b}1jm2Ho<3%HU( zqpplnKVW*%qJnk&&W)MI>S?|!_0@0c*E{nrX(-Oxf!lnnb%TjeV@?-kpnpzH-$RlK z%DCg7#SDKmh+D=hPi%-`x?f9}ID-2f&%Jy}M**=aM9YWvay)xRd|Y5yEXW_PM#nfk zTLC)j=x^v5QW}Fi^Ruf>=bi*iG!A&`+uW)JTQs?jqhhyABsFmkA(Pd}UK>II9UCYFTsk}$i#t?tvTx^HgotHwjXUM0*OY-#n zStSS(a}*u?ztZTYkHLoCZn+LxqFW1~C~di!E+WLBw8g&r?U7VP9yp z{dAswa0&zu{D@vzc|UdY8__dnZDiAa9)O##oND=HmUb3g%T{^aFpP|l$vX!yV ziTPyj;nn4JqM)V>V0+O{bisP!Y3m(^x?mQYZY3yn1;1TZ^kni=L>-P}I(0HhH5Oq0 zZj0>Wt_6?DlEy}X6CQ2cgoQGbQ7rvRYdj@J%7qt8u+Eu^U@NcZJJ7yTbexlK>IJ&O z7;S57flm+{VjSE1Ku9rEGzK3W%$j{3IOFi)^)RK6H$iL#y-07kgdpJ7S|Kr)0XD2x zgW#vMxdnz(Dxtm}oRJqs$E~9svo{1@cC?KM3~OU#O8W{w>`xIFc9KialZgsU?_AVk zjA-^7@=rkb%ue=oWG*U%JP&P+!Yc%aF#~Q6FKWSt!5!H(;=-~QS*J>mg4gO>J`yFx z)$YfBJ#ba;N8WnPzQjRIs_ow0KI7*4n%p&a?vuio8xhNQrx*X!v%$y;}F>Gs>3 z1}tkSIS8|m9tazc8$(6kFu#Fh-+5XvffeYoH1U~EsrTlsO{4@SrB39mWEr>wdxspy zlK2#6GW))-6-V9%l#8=i(!NNDXQn)Cgit~aepv4Cn1*7(A_Cr9>UEyKRd>j8H^ZBC z%uH!68+@g>^?&%HqwRUO>j~Rq^I6Cg3y&ZLJVcgDGpBt*KA{G6q99JgHZB;6!slQf zDBu4D4c~sfVD0u~lfx;`unc{~YpD5Q?mJ0odSOn}>)86lLr<3lm!!SsaIzTw8|0^pSV`p& zR+-7kmf5WN>^TPNQj(!2k6Ra9Z@*8YrcQ5pf16S(LjrU08Otdqzuj=3Ie#j?+t;pU zLeJgiXZBRZxQ)$e2g`wAoCgImT-tzZBz~R6L1tMZ;EuYJ^Ww;&R=^8P6-VCY7G!3T*;4wRv+Y&*SUE`|jh2 zUu)_chcKHZ-T_S;m-elcaCM;-$F4^zavlASw`K}H$;8lORwK?d2UF!$j1vT4ni;~og8$MppAE2_X5df|J{w4R0?4I_~_eI>Ig zvHm>ss8t+ssn{&_819*~;>K}^%ClnGgZYm~$~ zEWw9|_xr59+Vs7LXsoKQMmMGd^PQ%=KbdgKcv^Nipd~9!Ey5sH9J0M=Armv2I+~tc zu~k^`72(|=#%GEulx8w*t8f|6K8iUG9~~i}V(nV9kPy~ApF+IZan@&zym_mW;CSx7 zz1!JNVo2mv+mt@eJ9918ur{{d(JnX${m`o8tgEazHmYyMHPh03Utc*<#qguJSR3eO zR-4$pYr{A&JK;?JRKXB37g2wmBp5?`h++@Gil@Hbblg|{wwZtD!*3WVJVMRsn8%xk zB+HIjARlftugly8QX4<_bjwALFk}X3ZA8rXsP-eyitNCi!`Dd;n!M&>zvNtAmhOS9u5LgOH~8`Kd?X$SAps#H4t@@}l4fdP z<#{|)jfLk_87ug6tF191_t@$TY^xrK5#wc;Rp-)tf4nFI^9rZ-6OF#2WTUnvHGTQ( z`LTQ8Y-0Mt$t&Ni5uh3+@N2}34};JTN6Y5y0ojPki0b@noGvE)g-VaGzPooWsB>5h zK6=r8AOo`%8~72kg+WU`7f~VJ&^oLMR+##1g`ptR3jV5DiJ%{s_Zy@1)G zaNc#KaQ4YKBLd9?W(L3?zb^iQI=SxdMT@p#=RIiC=M3ilkq!1X;(gDFNdAK+hS674 znN=cadd;f&WBJA zpP5%QxZbUDF6ugp!tqcH=k9+U;91jeB>j2}RU2gj){_@$T_wnlD%rKH-4|<@Vqi1u zoefofcn8H-YA)Q--dU9~b5V8H|m5@_eX@tnR%tX^ZNUSXjBka0UA@n&)I)&$Ki#Z>yCC1FI@b7g55_)5RHrZ^f`QcO4rD-@Qk4yfYsdpj0_{Gc)B_aL&o z;La?+83_idGJBhz@dOqLY}|SfW`WLi%3Ms7KsPJb!T_+U@&dn2GpUMnAIkYWF|&mA zkXb&H*C706BALB{C%^bXC)BCe0u9HP zkUGR^!Ir*N#nYY!*;Q4A!UU?>g;+#~w-_;1s#3nylYzNjs^71**}LO>aFtVgd6wH& zNj%TFMMLVrgK}Ve(UYQjlQiCQZdlj2Ip10re8P3EKz1AQF^hzwJ(8=Rj`#+1`FTh} zoP7ucZf$iRHOsyiPl(0)S_}kF$6MPU$Kpa7ng=n&79UE`W{LDe`AAp)zA)4K!N=5@ zxPgDrROrTh`Z+Qd{+3%`M6mr2n#W`+sz`*8`0In!+OLgRYaWRE(A57%Mfxwdwa*bS zWkEY<>C2(!tTQ=uiei~BeFlWpZk3W(|5gmJL;yI7>m%HCjZ%ek# zh%M@e{18mk-Ft+kYJ{);;uDz}Xnoj^TLDcxi^e{2z7esrGn?#B>G`X9L^^)GZr$F^ z2^{s*$44$F+tlPwG?0BAe|@h-h?d!lDs1_pi$A;Ax-QE$3i!OX)epZ+)yeh$%boof z7+tSwx=XJJ0zx4ON*BQ_W}HslPx#DN2M{0(FB4={f$i*!ta};3k#n|TIq?jqv&!OF zP`KDx?JL_z%9oaWpAP#2T+qu{E;VSq z&i8ZgQB^B^bLNpxm*5n#aLOlH#(W$$i&VW+D|;v5{^_3yeJ`_5rPa;l#r9~jNL{3e z^1EKXmu$CAHoGp!Q%22ImujTHc*cLMdwfcGEjSRiJskHgMb1XoyQ1(i-u{=~_ftPz z8;T9lE-(EGcF4xXOtZF~YBQg>T{yhz3ss}<61od)_^xAoE4S;B!*gmeEX*1;gCgvD zPde62g>dIDuIqTL{w5<_kndg>bJJ**hJT~joOh|3$(yH+{lNd9tcj>^HSIPZ&pSwL z{!`ybr=uBjPQ$f_w^d80o2t_26;wRy86$1a`35xI!uq}f#VkTua2!$FzwII7n#S87 z{CCEp93IlsW7ZXXsXV{-&R(iCG1Rw%up5CXmU_&DyZXk>a1WJE_z&R?-+cqwk8af! zokz93#EhE|Bs;M6l%oxbmn;Cq-8!d}bKa!+Dav;XH}DMuDDU}^y*yErx}F{fZ9oz-$m4Rl4@G+q|l7Q zIZ|OefPE>!O-T){%yv$n0lpwcSFb_WYBnnh|76RUWzpcuby6Quojw~;Fnrb7S$nqL zvPx;p*KL$bv(7+~M`+rP%f9L+X`STIaeESjU?M;}f&zJWxKNqtKW%u4eyjW(- z@nEWgh*rx6XJM@h-%pVp=HpX^OpR&+>EAo|9$RZRFbk4g99E-`gceO6lc?l+K6U9xKR{ z)BADOJW&UCI`-hc&qT=MzFb7VQoPuie49`*8Qdq^HN2#j`%nen9s>$X>Ua< zMF5Sjr%yngZPYOH(>mIclMI#b7rR911NsJCj3Oueu-7=<%#Te{dM;WX-dU-s+i{~j zeCL$N`AV)+)339Zn|puq#y!ijU4`^4P7S@a4hS zuaaTv#(KaV;uFz8Zo0`c!(F&)r@7?X*7tYRrXaHRV1|i(Z~7qbBm8<}yM1C6@!P%6 zm2>Qvnv{xc_8Swok^ub9w4d>nA)0tD7DAINHd-PY55pK;RNdsO)#mE0N{65W48V#q z{a4PTT_I?i5E<;4Z~(1}$#*4;tz!IzQ2;kC5|cMv$I#nPb8IF4O=ZWuk!jdj z+L_gDE9MgftK7u}*}t5VD9)2VSgQ^}CUn$Y2C1odF=FKs?5q5!YBqEwr7V3x z1NU+jUR+IphFg{{DhtZZs)wNbhHz-V24%I(Y9@1hj(IJ?V>Y!vC}So>OSRw04tSNO z=2*!E%vtmZQc!wNf|zp`)8NXjW=%WZlT^lE$3czFd0B@D(;~=s$7bu%UTYqc@Mf@MG^lbj z9cC+hE_DqJ!LJ|+JNU-zmy#U3Bw|QwQTlVK>EZTxg4zf3tp;>$NU^An{TT!+ZY0ua zRM8pTaEZa;Rs_7GW~;O2o~zl;cn1O&jy5j}CQ>$eu(t%tXOGre8YExB3L`VtF*^wy zdn6_((Z3Wf$e&=*nL}x#vgH~c0s|j)$DhB^kn%2t)62DE2%l48qElFrSx^@4zwgqQ z1oyKGDA6fo`rMK8)%epi2pI{l@;tx7Zq9m&wgm2>2#*z&}yip}8E^DC0 z`R{xK(4DvNLLK?)(QEGOKLZWi=j^P68}4@9@PrSs{!|<-N1Co_ewVXYH;dk4dZX91 zEHLitixxhAX`g$fz%!2JB@Yx<%3!m z?f`{QrPANOH#FXhi=Pm+i0U%i>Aj{j zp#mD};=MGTxQcf5#Nqqim(dbOpcV!58kQ=WWs2w#c??`N zL;v|&F}fy{FnCEw#Fcg2Z`e24k#{j%ND{T^4jHL)vFXlckj@^OKH72qBH(j)lr(zn zv#DNzF>Y8An3j+G7Xu&YJ1YEgY^-f?oF3s$9`;cA*t+0;sUz9K>`z?Cm{qUj{3#V~ z^URlg_J+qsO11Yji5vc68#?f>rSWo#nw&1?Tyj!gWA)dy<7P7QoIf9*1r85e%Ox76 z$JTxmnH77A(nmQX&#?sf&I5zzahux7ev_q>5}dhmP_A&N!%~NIZm-I|&0G-RMs3%O zT=*cuUIQdlk)7W9QdkZ7qEeh>Z@K2e)SPd*sh|wA7PgQyaowuObroX68Dr072w&+R zKn-V%MdSWb#=~{B>P^iFn;p0ecIJOw=gUONe4n+SqT^E*-K+ zK2JoxyaDKiU&FWqV|3DcV1l&qs4)g5hJqfm< zLK|d%raZJ0S7VXf;G_mL&hFT1OM7C`f}cmB-$w_y=JKHH*m%8j96g%@S{%KZs8Z%9 z1aGR(zVoXsoG#cr6ZopFcLyrjmE)vR@X=34qIC9pDtay;RbvpqlcNaCx31(L-(Xd4AqT`3DBO+S(S9FNUAY4`#@EajbK@=$T) z6C7Y>t-B%n7D=7gvAiR~pSh^?RB4bLb%>C-L{mjv_2wh3a|iT}yO@d7FaqNEdTk4n zXrw_v&Llvy2|>c9!4?rF>4?cwrMSK1ahm0D&Yz-7Uy(UWYvYtyI_a6UC>zGf1MxB` z7_Kw9B4CpCRg1LyN?bqoHWO%STR=Rsrtop3r$*r zY51|rvP$M?nwWQ1$~e`IT2+HgB*N=YA@0GE ztJ>9s)=G`n*qezsNIDGq;z`>i28BqgehIq9HG;YycLEq#jt#Ri5SH844Yj;rrzyAi z#I$cwqH%ol?CA-~UTjcTVM?9dsy(?TPmJUga5OTC1_z9x+8`f=k!rju*vn@Zt&O;S zceP9I7kmXs5`$bM98`Q-Y`FC6jl&_v3HBJ%V$-;F+^CPN&Qy|>qr-vi;!U=Vo5f1c zJL}I0%|;p8f+|B-GG30qk7KBM&$*=zRHuD}%F7tUjO0&A zURKN=+;>!CLuQii*$@J19eG0fodWtzya017!r^=uK|ERLsOk}ZKv{TLufVit)6uwT z9AQpc7ggFWZw=yW+QPFuk(Tb!8(XU|#A-s_ zkY7b=_}jN8`_$jAGl}!j)f#J^@5R+#q1WQWq5Dipd_=h-flh38x@*^IX4?EdSfEYG0DAjRmU-yTB{tqAgq(P-N0!jC4qk4 zQbw}8i9e&-RN;wN80L4wIUKs}=>F*wF_=jVs8)yhm3e&2RD#fgbS?bLWg%{i8j@Vlo5G zT+^c){4I;R(kZ*O_8`2xWl8xNP@3+5)3L(=U%eMD_14QV<6=%a%dXOwVR-0_g4+8p zmN`ysmf-8FUIgWh>6%9%wqZ+{!f4k+(c#(oE%6>$s&ByM*37^gn7$RV*M!@6#X5!U|7xfAouVpTW3K)4gNX4aWnRB~X>2fDx;QLjsd$taaE_k?&) z42P4Ek2s`X%W2LJ7??K8o#f2@wyckBQb0eb5f_rbT?Q=?Di1{zRO)w*p>me2vd3BW zoWD34(~l{yg`d>`C5Ok2i3$^@gw4-%cTmY=PaxkiASxl@n5ou#9s>NM8`h!7fpCYm zAf80Sdo)0g?m5g*v6_$D*!_mfYeFSI^+NP^cJ7`qH%$G$M6?dOqZ_oHo%KET+Al`J z_T6)gdKn_Ie!|InRuAQBgMz6}ptJ-%yT)!G(V{31Sv{-N#_y@9q`j39O0 zOd|y3`h(^@rLE3dFV2O3oLW?JTXSY~(9qDls5tn2JCF@ozGg3RU*fmXVy?Nn<6Cjw zP*l>@_(38{cm`MiW5*%D9$K~BZN+AJy7IKdygjsVMwg}ldSOX z;@uIk*1-fgOdS|z!s*^ND{+r8pZ~(lqoTVRNM#plLi|3Y^xSi%){SIq&uVFm-Pg=G zbZ2y(Wr+HjH_|wvEF2;FZy-*ODzq%ezuNlykIyFyFXmHjqFNg?RP*ha3Z4@8j>fwO zzdTHGEZDz1%^aBXQ#u9kdMXi?$jT7GLaIhsnxoQJKwYXKs`uSJg4chLo)fqJZC|go z&-!Mu%N$>r>!6}+CmhK$+<$ut*X&0xKJ<^t{4~`ABVCbPPkNUIzqlkhkEUmkXYeQD zfb>e4j~P69^A<8yHJ<?@E6z@;wX@t+1F9m z*&XT6xyCOpw88a(;@^ur`gs@90RUj9a+P!BN4`cfY}b z5_KZcoot_O5eKQoed)h!e9gymvGrz8?4egti>Cdak!i|Ih;))`z}MQ?Tayvfm*9Bg zSiq&sTGpePHnm{|VU>}s$IA9}kWEyQweVfeM8)Iy!oz-2QZ1w!8okKmiEk1kw(d*5c(cbst)M3{m-aXuUvO6GfA&hUv>UZ58L z=#F^n$Z;ve6Xs zg~L$dii?vfFz!`4YXkOrCAu_;xBVs+CK14oZX-j~@CG?wOli!^D595Ab0O)X5TAL-z2hPi5H%nm#9Q`GF0J%@ZFiaA3r^mtq3@2~zBS;G>I6jUIVL8mwpeXd>0LV* z5)Tk^NTEB_;OTsAVbLupldN#5&K_JNgRP+}a|}-d^VqZhd8}%nwmj%MAm(6ESJz35 zg)B|0olGs1iquQ^{~gIB6(s2mz&sGoWCckDy1xwENvk2UAKeEsY=Zw}O-DJRnGx$=_U<*BFy z=@qE;#affw)^p|Hmtc_o(2;ka+oU%L>StL74+ z`7+xtrO2dByDKQ1owZBi+vrFTK2Mspvl%#ZO#viMbsCS$Rb4ic+jTj=3WJVd8m88& zVk03EuJD232vn006^T|M1vZtP$jUy-X8q0<38-idqQvNe`r(W;NiSMTIk2!Y{~pi1>85_AWDlQLWdtTv zalfM{LLXPf#2++$uLOB90q&aznB@&hd$azg>R4T%zM}=+A*Nuj=``+JbkpJ9*${U@rzM6*t|o;eC40fbN9p zDy6n8wj&*rG7w!MU)30W6LRd~fLSIOvw#eD?XRMQ?nkC~wu_E4s>xuw)Em#Mh1+IX z(v!h2wyb+PHC~Z4_OH4ZAcGyQ&is0ABEw#wlU=}#(}5iip@|t?R`9<1{ErIPW!IV{ z+ZSXt8eV_+v99B&i`Vz2BBIW*XHEOCC%ExM&=vH#EEjFigQk{TLsBCVjDv3?PK9L0 z)MAxSb)SJs<4YxBPi=Eswb-iftcnr^xiw3QW6aS`Lfo)}k}nH2DYtvJ(aM4Lv92t; zHPb&=P_&T+1IW^Dwr#X3TK%LmV#F=D_0gpUBg^ze@{Dl|uF7Ob;9^{5L6~g4nryQL zp95$5c-JkzruQQP^BvX;gfq;#GFFAYPn@=LTs-TKjgpDoNv)V-?wc3hbq&T%rnt?H zm}Q~{Jve^~nplNp=oNP2Uc5tLmy^9!r=;`EPZb9F2|JgIX0wm-M7JIRBNPyqv_XFLf{)yPHJ66{FjiU82>DF zEJNvCt(tH;r&p6G)qhLnQ1K9-=3QE}BVuFrPXJWRkZz1v#HLYJUfrUQ2ko?UAt+h( zb+~MpbNz&BOJ zV49#MRMSxMBbMLQAE{6Q{2ao^o^z5`@CO&Qwh^HQ>!}QN zQ0#N6x0G62nG#f@sM_5_0aJ(e(697&jhvf}KV4m*#;63^neaNs%zY20nUlUz(y#Nj z4>hjl{p8a8srP->we|hsc7LpCBprJ8k=fAbnXJz)KLfP_Bomr|fY-K0Un$T!nU9Fv zo98C~TXQ_&d&0VP9!H5yXL_mXF@m5J5n~uEN%a`FASIoby+E4$A5p>XQNV0EcKVLv z>7QG~l@)BMCyegvH~Yqm@0=tGC34Mmas!cM8=cf~^}H=Fs#g228=V$s=nX%&L-BVCU_ItO`GYt*Vff%&-ybRr8Kd44Ow9V{N+1#l*L3}DWv4#b80rY?n9m6v_s z9b^6jvmt6kmn(Uac^HF!#YnRowMF^&L;IDm{U-b9difd$-1Y-`(17*~x9cYgZb(yj zm#L^dZc=Q)%QDxLDl+zA_%jSmthKp4pAUgleJ}}*m?pk{OOwMiIS#A@Z}$)Tv~Q@D z1mQhISm$F|_&(w)D$p4%g8J2Kn9}S!>>nJ0&&r?6JvD~EzV`O#Pl3d({h4!^aU@5b zRIA-ZS(GJdg!;0VsNq$D#Rlk_tb<}DQ&kCFRU*`yyGb=g$#*M7I|F>a5cMbo=+m2- z56v-=l~Ohad0IPK6bHfvY-+Yh67iQo4oaA!N_OkCDkCXdfX)RG$atW})9EA|MT<|) z=KQVEyqIhaev-$#34wR}n?B)goIUSkm;$8*i-)Cp?Z5`W1aqEm2%lW~H_c%x8*&y; z9B%b)kz-O2$)qHKl*9qY!WE?ExRM)ZLuqq1OTD8OiyfN+%X!VO74piA5{<((9s;{3 z2WOToR$0cc7R)Ywl^Nz4y0C*VQCBd0U%I2l?P^uc)T3f>_+X4Yd_vm4{QNWJW1R<6 z_f2+c8Y>|;8K><15w$F=hQMNtdZK@7^JST zWZ3^a3V!&1_(qf}#(2EYOn;VF)Xfs84>O}n_e0Anv4qKH74p@cwUz;>^I!Y0l%;C! z%iX|+CYJ-5a}o$0#bdkQwNw@1;Mj7!$EVSsLVi6>>&MwC+ks`XQ7k?Vz$Y^;nj#mY zYSk~H8=fgO47wfu%;^Rgp*MWm3t72V!Jev-uL?D6A{{-mO(_)#&-p+e1AO7G`+TpO zwwU;FZoA4qZr~P*Dij&WJhqM8-(y2ysyj@@!)wV6xy5)Vh#NWwX(uIh*}YH(#_ zsH?s?MD?S+gP4QFY{x?oTYr*+w_w0~rF^30=7#;fsIPZW`3-XXwh&oYyu7BcoYu`y z)PgvO`vuDm!Vp~R8Ox_DyA&p8=%i1`<#051HcPcnsP#*q=q&>2=sK>Le1BowJdsEJ zBoPF+xsNHGTfPzjDLnUPK_zc$&Tln0IkCC4wtgiSi6fmqf3G@$eL zBi?ZaTRm~qu0PK+r_KL*xoq6p;rW=^*u#F;HmcKXjhraeXD6;EhoH>oDg$rc*5lJ4 z@sVD5qqohdv^n3iRt`8l1;6?rK*Yk&=MWVi-DR{FH7AA8nbvvYsG5+=$N|(8|FsjkHvk=72-f$EqRKg*-obNSpT!~sZEIjGr1MN52*Q)91gx zkNun`&!%dJXbP8d-4z(ABB+GNu|c5AZ_CHo``)muEKD=&T|*3YHe=>~exoyj(lOxt9)KlHE@H?oe%J_rXDKNQ)ok^yID&AMTVF6iC>z z@8__!R}%eg?3O!*EIsofU<2wp@qad}=D)EW<3TH5Bb zn?CO&4tMNK3?h6y_j-Tgr`|3uNzH$5`p|Sjf(r(!dkI_aoGa`?fKy7iem{DlH}GK0 zTAcBc=%XJmv#HJKk092M%IZNzKjnU*NL&PTD?`Q7lVm@Cukrq(Q;ChKe>JA_y4jP{ zdb!>`=N+Xn>-R#A=YzI3j1LqSq$tnMy`Bs(6tYmgUOBlyXH)pVv=he8n+{>jG!QgCmCcRGpj}Wdbr~hZper63|qc z;%UW+K*d-k%u2cZXX3YDdXa)jo?vb+Q+Z^4DJ8$n^Cmo0#vf2gFs624W|KsK(rX1H zW&wK+;%>84?2O-qExa5?fnAEeMRk5dNuc?D#9C6w8I1-EY9MVn#e95u)}a)jSs%*x zgKQGT>cX|zWNHO$Ko}#}AJ; zbM-4`T5V-}0QFHcnAz4zfz?d8$d_*ynw@F2FPCJVwuc7gyyEZ(9R=d_C%@H6 zQ8AzfiyfvK8D!_F3!|!DYQ4it)6dgtPb)LVlh5@IgRnp_Q1ARJC)O_)O@olZ^3W7j z6=qM+Qbi~l>XtNr_liKd(Fl_r^Z1f}#qWNLc4Rf$r4}onF%33EJ=Y81yKsn(cy28I zH9d&y*9KbN$W#~a(yzkznFF(NdX1@d*v%2})-t#xe!f1GC7>yDmP`FgzU`xf)?#^o zput+j@vR1y$F_5-m$m#n3SRCm8W}Dtc+1EM9i?S84jr=@vDSN0jPGkyElPPGv#sVL zOKo=Y5z5u7xa0&7aq?xl5F82NIZMS=PTR!Du+rQnLqDa40I#<{9F?c7?dfiR10duq z2)x4G!xV!xp0|^wSO}dqRo;ncx7J>3Gp>N9{IVg@Q2Aqf{v^1!c~?vL47ID(kra-< zrwM?{Dmg-3(b&ouuy(2jamYi0vhnN+L3tpKf(F;5BfXX2ceP6$l4pV# z1k>BTX3A1)>3e$eP9ac0CPYo%@>0K}~6j*CT9pvkvgBySb%r|Al2Re?ibP(FY zJsGW)=N`$r6}2rOqEt4xPH$RFv0sr_vFm#>=UgdP@U0#JY3Yo5hreq*JxRmJyy@cg zoG+}Zm^SS;aJ9qWP1m%RE5t2oSqL4uq|8OlZtoz? z0Bf%m8|O?p3u6Tgj*?u0^(x0kR7`JpyyyxrxY6%3KTrL#RG=^ApjVWg4cC@{ftU0h zyj|Lr6Z)a^{LQi!L54;FJ|2MxIa`GY6j3w}YYy7U&;;e>`Bn)oC7c!O%rIHC%?AED zU3=B%*P@drcdM%4f!V;>7FXKL)9IZy4LdVz+Z5}|f-SOGS`}YE(t6Ju%wUm}hsTNp)|$#VVkO&^y%Fl5yQ;lQgg!vfV&P%i!~ z0AeGf77IxMF;0Htgyu1gvQro^Lj|;&B)ONz0^>2%nG~!5r+vJ__?H2otckVj^t;g{ zbSE2d3rj9v%)^1aYr135KB%RU zTnJH57`(Y{cXw|1PbJDNxz$+OV(Y4C?Ft?JA2ha9T6aZ{8{kJD?BbIwRchB{TI;_) zss8gi3nN_dIm{<7Ualeo`r=i{cD20zE~+pOWqG@@izE3WrFvHJ*3hj2aj)p_bH;kt zxvqES_v}RE*A~b-Pui1A)qmEb{^35w#a-p+cC1|nPVCusD@fiB|W9k3F zHEqmW`*IPXZx6NyAC0$GcaWc2-)DHV8vgp-pOXdILT6F?ZFmWN`HELhYkt1`83_KXgUs}>oDY6vNUgY}F_7i=+a&ffpxID> z^3L`hsvPHz3l9FFR<=k*L$Up&O?JP-^g7Xn`EX)^pKSG^p;i_Au8elJ3}D$y87 z*$UwSEx#)L9$m_(dEy~Ec!aZQV&W3dF7J)kBZ&;N$1>}bzpBBwO$z~FU;LezLKkos;hndFfknS}Pk(Oz8a_VrMtK)`7ALqepV_f0Q1YQI zW?eo9r{g<*x6B)~Inv~L7((tIvqp;O z>D@!XLn;w7)RPcyJhftbr7yQhq>OpfZW8RXbQ2>;t2izE- z!4-oeBTB!>szzUYIOZmY$;ibASo(%3If_EkXZ`qU&R@~s`If$b=caY^(j*o+r;%io zX~z6cRRX&3m^-7eKpBX2AXa!!R{wQ=V_jf${U@3cr!!IP90EOz+ic2#OvIP@&$JR? zKG96MYlr73&3;{)8lYB7v+^xSlJoYD8r3Y>xC{d$Of5qGDzK)u<}xPj8(tNBca4f7 z#KcR3VpifDCgt1t7CY;HRnQtIo%L}r&b#96b_HCuw9I*Gauj9@5Na0gBqWa{A$TN? zHi2nzIYSEQj65EW^l~I?jWko9DhF$gR3frwLMhVR9D~rOE3@X=rD4=2tJ-(lF&swK zw%qz!XBbvn9r{e4yJD@JG-h%i4gWDi>&=fyiV>(|-XwgLf6qfJy%>Hp`%C%#P+mB( zg_~*F6rf((LFHqMY2CQIVTmp95j72(3$44+p%Zv9+}(wKs0A}K9a@!W`$ou4W&WfL zg6(vCqyO4!u5$63e2&c64U>m>r5uFXx4_9(1*>76kQ$W<_;UnOlhD>2k}lBtWF{T1 z0rKPmJA}wmr`K>~mj{epDMv;ch__f`{E(6QXU2F&r<1SnqGbrQNTh54wak+F`-PUe zuhe?%&st1&p%ywp-(rGnwfpN&T1!l@0*!)#T4B5}#=-(Y^N%h6zu#5@GfZ(?yx>ir zW`~B>Dt)+vNRfP*XZf`Jo;{QJB?XN842gn?09q}IVOAuB5?r6QSFxHH&cQC0Le0+) zYXUf3$L%^d77HrL2`S^{v*Y0K6wh?WmOw34Zl|YbL;7QHAKZU$UQ{LI*Hj>07)A8f zQq3xzB5LShCJP5FZz!+x5nrlH_;pGP3Qd18cL!%^!2%e5uJbeo7aYzaC&e@n?<1PB z+MCSxdk8Iy7cCaRoKV3qQ;VtdpyZpSOD+ZbR^q`Swm~#I&W^0DnB9~*m~=!v$izL@ zq~8$HQvTksEREtS(9W}P;Os&OsM*3Yx2i$f*}@-$5k4PaCbAeDQWv>pddgRC5O3}& zRD5znRmeHFAJW}Nw}#&45{Uv1REV`6R4xvp$3i#}Cfr~o?U*U1ab1j1no@5zBzK

      -jqi8cEjeh%tXk+g{zk*3wEKg7KHMp7#s!mS0D2nq~+uTpx z$>o?U{32@X#60Gr)_&I?kahNXJr3I+W$GPJFG%mSs5Nyrq73EY5$SHf+SXB^q>{gBiq-$~Up$GYPpMRrjObHd8*P zrqt8gTNd3f+n@wT*!{FuG-sGKkhLH4mQC+yqC>$GBH`z^<7TR;mjOiP7Pc8UTP(z~ zr_5gZrT*SSi|q$eTh<$cqjgKd^L5LJ!l1Q6&^fdKy@CEYiXv`9w%-j|#6b_krjJqT z@>?T=d$tZJUBZgfca&wlryVty5O$Z6`061F0y!>K;B+|B3xY9y!g@}EB0Szq=nKK} zEt<4XU`}sb5*SodP?_yGC|o@q5A=JnBA6?OAAeOi+8wOZSCIp?_+i$N=yaCq4rBVH zmggRf-1S?KxUS(&h8}HT;;oa=5=NJBSmh9V^P${Uuj~SXdC;x#0#b00MO4H5@V2k) z1C4&e!qGu&(4d#|cZYjgp+7*S)iIWSU{Vv5OU^+Vg4%9o(Sj+Pwq-)Ah7Tl-#5a+C zYXoyeMqc#?d@c;Lf}MBlZ3@Fw#(Y;?Pq*-*%^6EafuOgT!rh=eSo>yuKyj;idLxOX zsMA3#atsG_*D4Cx=Ltb(7LC=ofaWwjg=6<>N575vM_^=^)e*0KmuDkFtR3Os@UCN3 zPTcISkNR@5xR$7r)?eT!ZgF99*_2NbwrI6Ncn0(6AYCz{3;gU&hv#oJ`~e3Nrk)Vn4C@5tl6fG`y#KZ~{QDa5pST9-_AIxJ z6)k1%#t9^%k*yyS>PskJ4&4+7UudpYN9nD@KSpJn?AIH^5Ad|>_AnB@q)r_9Io8Ml!F2gf{x z{4dh;{)|%2>4Ad-g=C`$T%7ILR{Z>Z)M}ONt)Vlxd_PV0?0rVW0=6SKA3sQ|i>z&lmzp-SAsQPSU z)*Z*f<#SU_`9!Hv=GLnP0t>fhCv|b+ScjVtq^XwA_wuXSmh63G@K#o)0EF4H2oj$h z-x-q6j+EHubZ%-zE{9`2M6Cb}Q=-?%R=4hR1-UVqfh>@t(xf;wd<{Omi1OsXsX13Y zM2=O#c+K%b@{cJcg)zGnb%(9Tc)@VTvyDyZn8N56Fw5s-=Av&pYI~EJkUvx`9<-Az zJHw$5j0LZt0+x6vo{JdHPzI>tkfK+=uUWCgY*pW75WQIkXqwv?R`St{;O_zQa{Sgq z$as0&?`(MS8Ad1(C>8qfO*L2e7(L(HxDt!FDLW-Kpp@4tXGPi5&bP2Rb=_jkvfTOixci3m!lW#I_!)F4}R@DCox7UqS9_)Y45&qJiRs%q`Tt;(5^_pUk(|Xrg&mUOWt| zV1`&T*th7jTgX;W-xFT66le9PKltvUIR&1%R}aZqOR*XhmtkWd!XYtey^g%FQ(jK zh^=*9&GXTZaPxasjc`p>tpZNKF6jPPc)3H%A|1uxFYqwFU~RgNyKmoxt}m_<*52;k zGOLW=RS!Si>-eR>tEH;^vUzv=jYynr3si}(Ar#{69&Y6x8_kXb6>XC;36X@mjlYIU--23liO1(GnV}P|(xnn^W#sFR!jGu1I2X6X3%fg|zS0Y<{2!Tj zKVg_0OP$UMLv*$Tlby3i6XyunRuAf!`#Y~~Gr?aZ*+R^%Fib~Io&sgh!Nq2R?AjHK zHzFrmdN4E>bby~G>SPKw^53l4wq&ArVv>62?WEfu&kKYr)Zh1U6hn+cY)9 z7I&5Hw)$ccy+DIGlF(VA(M^!LPfm`&6lm9r@#CU09Y(?};BCU_S6xAk%~4DpG%{)M z4acGzU?$hV-sRz+7W9*z@-SPvAgD1);XOqWrEX_Vwo>XZ4MEoA-qAlZ*R*QM>o_wm zLEyB0Bc?@{`Z$R_U5GLs=L|z+SH1Gw{DNPcA3d$QlLUd(d#&U$3ki5{x z*BjuT8|x0uoDH6li%%kd2&dHUyN5P-x-A|Jpyu0c;q3xx_@anlVa!TdL9}M@TcKWf zo-rNeMn%g}5@LyVVvM8*{h6Di+W_{7Ze7fiFlC6J<;xlbkk5ZjlY!uxk*NyA$1T~r zt*v(F%s~&mmFW^sniZMevq*3=>kf0dz+nKcNv{#m?P+><66ZbEy1@<1%D%$ihmTrFs)}{3%%`*RR@R@|0Pnf zpA(zt-{iP7e)ZYT$wC}q-8E70P1^VAXL0Wy^jJ;c)ry}e@X~ibJO9IU(0{!|3TMxD z-=dR1+=!yy7Rlg|Q|*Z(8!j4-gW;(|;~>|vMZR;Z&0hw!H?Pp?55bXcQqjv+iNlu< zjEpbXyJ~ue=-}Q3dc3wxeaFAb>^)3-cjo~y_=Cfd&+VCGC=mEK5~~mr!$sR~Z~YMm zcgxki)CmE-N^PysHVWU6R!XH05n2A$EBxQ~5-s=CP>caL(N=FYX7cMP6jt6Dee349 zsRd+|0eRf-*;dHAz~AV`sxcA|Q=oD1NEri#i+)a<7b2RMjith@)B|dUA9$t;AU=q9 zGOlt&5q?XI%AEqvB(s+g9kL26g{a&EFGcVZ@dn|{Q+oRgkv)cn8_WHH9#OZ8M~LwC zF!`ZZekfS?cB$6`f3R*pdxzh;$uq_xZSBTUt$OHPlqIDo!~V*Q8gegrp}Iy_Fb1dF ztLfE?D@whr;3%r@WWHd{a;q*Na}3sG+D(*CXi{4*g=iu|Kv3QJ(^Wpuyz)3YYB)D< zrA5aO9dGCI3SbNY|4&|tcmr0g&K%H^iUqPSB+M`!+pbIPFAcDARn#2U$h|@j#O_azi_j?iu=_vJv$Il5YB5AMO^tYRzypRC^9|hi$WH?RV za#~(-$TaOfpt$@V|8oR zPzQW_`uTc+le$Lh4kW6>g#k(=-&IwTrsW0` zY(`i^*=k<&wDO3QYHWpCLAzA@V_~4y;_daSx(OxOX>@Tk5U$$2-4LqZ7~n0mjf#_z zZ`9t#z5^-CNXmJlM;1CF`|J^YPfmw;N|$6TaIH`xi*gXwfT-mZ%~lc#{0L6s)E2)WYL$GpwM9iCh!_ORvM zOCuAA++9K~_PX4-Ds+fOY?Wh?a1GXzKj?#47`3Xe1$&TOnvdeB7U^tFo=ZN)AsItj}C85q8qeA*gzaLri(l-ChH9M+VIf@vxwo2SWmg&F2Y!#pyn& z>8))EFZL=!Q3LUFRSPnP{SG|hxya6|FnWs7Yhcvm2Qiwrt4a{HFV;AQw;)~9`Bw?W z`xPSg1drEzOUrsePYKL>Iq8nm6q*@_x}L_=b-W4L4;Z1R8gqa(`<^5IO>v zO(l!9iOD^$PJziQD51>(@Qlm&b>T&8p||R}jhnfS(o>)xGFw3A*)Z#%fPts7$Y#Go zGjI3EO>Nc|<@XO|fvSG9{#Q-}2z?{_u9wZ)!HH3Kz1>5hxPrp$AIoh&Ytf_WYG0ms zUtlaR%;F9ZSJWc**`<*kQeSIn5!n_4$Pumeh_v;FH8R{N(R$6)D&JqG&JL5vW#yJ0 z0m-b>HUSR(g8c=^^;`M#?809F{kV^C#q7ct@o>YZ7do$f{_*Gs6RpGPY0BAj(Q9-` zbbTb9jra!^Mn~e6i&qY{2W-m!T>Kpu<`l&f$ix&Fmi%I{#AZq`au>JQO8Q(W!6@~2n#5~~t zKu`ZwKxiq7X8r>CnFVq_n!TucN9W-o;m1NJ zwS{q&@!rbC1;-wGh=J9cthEZ&=|viGK9l;!MkhN@MGk*i)86&Gi-oMe2wg~wXhAmk z1z>K|k%j3$iqig@+|z%HkNRKmvr~QjMnISK?T(9Bco@ZJJvOT_zBf=77#J*RGGb1d zm)Q==uyW8tt&pe9bBxlC(ZdcC>5dM03O6k>)Rg>&5=b|l<9G$X?0@k^+)#DGF`u>cENWVM$&P$3XbVqD17FO9TV6j-=0@$E;im)4P<1k zD`=!YJjZRVF|R*F=7r5M$YE;|&Z7}>3@oM{&uw@aK$s|G!i*I_iv*qLKjBTmF>u6I zoz9aki6z7WB`kfey3pPJE`{H99TUd)kG|l}ohaFvV9rX^wJnI$vAqB|`o6l3)P>U? zRZYYgq($gYmAz2KdkpW;o;))Ts9Xa`Vq>;*u@t{?VmbP=lAw#{nSPAXmDlZlZs_?= zR&bMMH2dgj{JvTxi|N?;xBbgAW=veDg0NI=;I+KpjRNxf=JLGfRFH%BA-r#`cn|ei zv|zA#`XdBN_ZCD+@YkyZWawNR8Dw9({Hz4U8du7R1y}(Ya%6 zbvY7X^)z$A6=zU&Y8xGb3}6_bJGUuv8Vq6@Z%``8ff?pf9Pgp+c7diGWBe@UL8S5z zK$%8QWAyCP1ZkPf1<1Rz`aFJ_3p%)HTjrj+P=PQ@PSf{ffcgZ0s%L&yjg_exG<Y6;1N(p@K0VhdJhI+u+IvAh-2VodPiDjB-b0ecm!F#BCGa^q#_%+j3H1Hz_j>S`6Ci zjg~%pmHOh~fr82!n$k1d+;MX4cBR5)=#FBQ8FP#_k`o#`X{Oom zuC313y`e_O3y$N@LsMEO(C?IZgPZ;4Y|5R?0{#L>N0>x3m7BN$=ml-|*DeER*2Yr6 z(`zbG+kNXDmvKVI6Gzcs@NimSZmuV=XWqH(FuvF3(t(=RH2_%PO*G_A=g*lFwoKhS#V_P$yh z_$)Z@eoK%U%2{`=&P5Moy0PX|-MLHVhVDtnyxG8>A4x>8D%E0P-C|6x*sAc-2oA7V z5EoJ=thWkdi=(xd$=LD5LOK0A=z$f1_g#tVI`~tPrv+8k-h`w4d+Fo}^wn7bEHwTY zgR0K(huqYJxVv$?Xu62Oo0vM=h#q@`-Tm@-TT_KAaBz zLLKy_Nyh8r*kg;c7R9g-$klQgw{<)Odk|7Mhp37fMm_JmZoXe|py4GHp7WWYWN?SvSZ2hI;!P+X^plHZ)6D%X1{~3Mk(8UWjl(fT;**Z4=g-gi$&YDL|7Hs z21>aVdUQNODaO%&J{svCRH(TzNuKy=8HoQ{p6$YXTtT*t{f(Yx#J1Zgc_JciyUt7NJG*WYeeEO`Sjk6%;(ZK6Bg*VkpK z+o$1{ibiX!0}Xi_YcnuCg5JCbMv5=^;N90Uxeq=S;^^&_n|}dj!<}kf6WhAaWa|80UdG((IH3GKL$C1k=#LDNUpmPBQy8%od@{D0A{)Xg}XrrB;nMj z`WdI}IR9hfJ0-VAMpqtMM5})EJ8s^*5nlBsqLRg_22JkkD=O@u(-S#^|6Kv=zg=s? z_|NCS-rsihUla1yGoa^c(o=gH1$(*zab&p_WyyOHe}^#!PZbI$bnu&ez7|dY7vxM0 zS+T{eCYZ(09a=U=;I^hZqPi?ADV@1xjOiek9h2S5w$u5{u!M6J&t*an4{gG^?JAxQ z3}^fEcJG>?w28$F2e5rzhq@eA3@3J*bD1E0I@C4Lk}7?#a~bnD&p`&wFNcvFw>QG->P|^&q~;9fC0Sv^G9c2@ zr&sASf|?Spnlfkr!%HTju8Q8CDwD-%-MRi?ikKohC)kx2OTNIUNnztczDcq-tP;+J z3?vqOtFPtNb=-B}m4e?ZXF8JS8ZIPQNwSioDlZl}i|8XZKjXh=;<=e*o}95r*s@R4!aZVv?Q2{8@7j0+F3BGed})o$Rb2Ird~0 z)ZnDf!YYGLzzAdWiusf_kXO_5d_GmL_PpR49Bn@2I~3R((=mD+}br2DMauC6h|J^(GBN*t?`HD6utcvSGY=6U!BqybP!BqPbvtxF>n!TZ*cW^4WG z%jFVDLr#FMiY`}&FdBbTMpo)G{voKCzlwD;yQ6G`c;)R#G1*_|5h997UrUcud9OBD zoReZbUA&&ba22U;Az0L)5mPg8IZ`OacpRcGWp_g{wLyk|y(twZna`TRpxBxaU~Kxw zE6heR?!#xo%Pr*6&;wT7>GrI{SLMaIV{!rm;5T^&=N4Y6nXo=hK9m}y4La9tp)n`Z z_GnX5tjyRlqQh8ib!_Y6+A+MNU3#d@pZU@g3oJsY?z|`j<>o5)kQ=}sXD{vh1t;`{lyrk~czW)^^r?KSpNh%rRR-ciNjnB#LX93&U(#Dk!2Yx z%F*f1iIP1mC+W(V7Ss}=ICqU2-(T^ai(%vJRl&P8tNT`~dw zB!@+JQ77`+^mC$>*%TM&jrWQp;C}x*?0EY%#k%BdWLq~AQA3^)@5cHZ?=R&We!2lr zE|}sgwQycC1a~8I8#^^(d`3=SPu>Z+-65ub%DnpS)d=+1f@aIoCmWzIU*q!5J#bTc zPYj`~yA(w+@N}0ZXlLGV((yfW6S(ADbuM$EanCZE@0DaFB+y7OrO69|i0S^;oJJOD z%})Nt&x9WA_nuOKnO$b5^bT{nn!gBBt@nEl&eEIz2#SL*95Ljg$17JRr4rq!95M%X z%Ffh@fV!MaCX3+uK2xpccFTF7z9NY%zqFOFRyEI&QH03-JY+D}`yfTq13(xXpqO+> z7HtQq9Eh>c>1!^S7i@eXT$M4JK|8QCIZJqaUMqWuD3uh8Y#w*MTbH|P+{fu#^63}6 zd@~`o32g$V6@yv`*V{vz81OekGct3XYwkRBiL0;o&H+%*KTw%nGP$DtW?_qoOX1;s zq}3*4Aill)3K}Jhb<2*4=J3L4=s<$T?vtPnqguo}^+`SXsuHCF+k^=Qia|S)Ak~xV z!ldBnpOzDY%hSKIurgZ=w9P_mJqe+{w5EbbNtw?A#_Tf}@zac2+O~l*8#Za1$-uSK zOC1q>MN4+M@MJpJjbBpHWiL{YI4jW+-eZ>TFM0}G&97aDejTEP4;1BV&1)$?umS)h z&vCoF3q}3z^9Np#t=yat_>7x1#Yx(lm=`pXb+W&S@Bl~y@jgM{4d?$noBs;{8FTM5 zYuNw~#JlC+OHnfUeNFaHqI96y&v)IIVnr{}@l#FYKeE#EeO~Hcp-!nhQvLI&8T6|4 z68E6$?~Pv%zF=oJMBVZZ^P)cQ{)zbED0HH`@{-G%VOf2JPVBt=^`J(GN|_QgwLVcRtIAGK#zMMf5ej7rl2L z+a>hiBhLbt$%13RR?PMIAjc<=Kvxt_d_{}xw#vJEgPq$cHW^TVq*1V479?c4!64%2 zkd8plYJbvqxbfF^*K*TjuE{HcQ@60^Yrzt9sY|=!Kg<38Pd$F+zV^g2Z@Y~;g_^W+ zLz|RNp`b7EVVP7mA#8vlJrX!Jz37U73dm>IDj&;P%;)b$Z%cU^*A?MVaQCRSl*$_P z@(6V3MufErv(KM7qDN;h@_Aj)Y79g#Ou68D78aA-JgNm01D(0Gg@5{E9_ms@5{}7E z0i)n1m2iL>h;U*Jad%v=F1^e?%j{#soaMA0L3T*EzPrP2B}qOnegN1~Ee{*9>U4uDy#e=5VmC1uFWg zsK_;_ynw;H`H^f5f<`da4%nO*2Jm+ra}~QKY&{dk7$e1|jg96&wZh^ALt(WtIpJb$2oi&MlyhTZ>?w2(iqX|&;URKlNCiKfPtPiI5~>} z+zp_p(<22%k@fi+JdmLQjHgE3yjjgUV}KhEMFV3=6+feBPK8_CdWgBzCF-)>4pwr6 zNm5k#qs6Z9iysW2=7hY zJF_L=29JobB2zt}R-Ne`mqo$N6JU84pxFd5)6vbUjxpkIG;)~u zXeX2)d}wogIb6<2=9&r_9y7d$(cG>nrMqz*#2=;&BR{D`0j+-oi8T^FgbpTL#)h50 z59;Zij?4B}r(1xIN}{XIK!d(j2~$Ip9dNn&d_V>qpSUHIZ1D`uklUZaH9IqpSb{!9 zJuF$M^!S5vUgc?->yf>DarE&XnoIi)LPiDr>A4(q1G^68`h#BRAzkalFr(+egRgBfJKni z27jK6P90r1xj~$s5IYHzSQV%h^W30<4euyBjf}=QX_M z-tl+qK&HP=*Qe2pw+|tv)qF}`q19I?C znFLv>A0W5hASn!oMF~gCo8{U)2%?VYqZmuT+>IY1hrKtX(CGE^<1x7N`XrQITbL_O z6CFOY^&$9TR47WTYV0|Xu6OlQY|TfH-%-l*CVRqnl%Phwe&y#Cf`O9Ost#~FayHr-;4;mImfw4e{jJe3{pRa-Ed@N8 zi&ku%qrUIHmx;QCzBJKuw%^-eM*@aRR7Raf~FCkt6mr(hz- z9i+J=<7x2pJ}CaVyOGSS+p8A(T*thzs%1^~w-@?7VRQL@w>#+i>qPW{Sn(#WRe+9Q z&7D=hN)?DdbV!(rEoSWTNY=dK;0AYp!6MUa6Us1zjWnVf2C;sKkmoIVV}>hhx9nOuo5FcbLJr6V-}ejacAi1LZr zw)5vY?_Yq8hwhQDeDWo_AEu3)JRly^zt!JXExkZ`lF;16^SZo1hbzs%JxxznBvdO7PR&32OaQ&=YqvY_ zwZYSWL(4xb#QFcbOxdn}%)%q3)cG;{8QPO4zb;s`e#(rITRZ!m#tEzgjbBYRXNQue z^6Emmw63knJ{+L=(B(F5f6Hx%E2s!#{hU&sr*VHG>mDQTmguMaurg8Ocy7X)$Lu7f$Ezxteh1+Bs+=UZ{4rvHmzA2s%im!Z=Kr- zkRWyZW=2%3!WdUiOQoF##=Ttj{CIeg$fR4cGG018*D^PAFvc6Oy z7||h7St|j`$GnZC7=v0kU4Kwtjjn7zR-t}jk|OjF&!G|7_;0Cu+YP;3J2C`!s9*j^ zN1-xF!4FfYJZ>JTa8YDdFnfe!5&V4eb9}_Ww#t%*hJet!N1l`0Sr@$@7Xs5M@fC z&Z1lR&RY+Qw<|OYX8kpMHTsYKfX}h1e+|NdH!pY^STkqB4G`SYs2NfyB0g5a-Y}?B zw>|NVz_f-my+GG;_Gz8Lv}FgE$k(wPvN35jN+EjVVhbq)7!mLp&xRbNBe3y2?pB=IV;HB@ajQqGgqFy){go~o&9d)(lfUs>LTXXw$(cHlp0RE zB*|>&Df$k(eOkF1mlbOBx#Rha#CZT(h>oL^%=U39hKtt!(e z6sP-JA(G6O5oSC$_!|A3rf6X}%#i=yHGJ5YpOx+~k`eYxO%LnY5UhXHlflPg!L)RG&4xY4+r6s8Qn$n7GVxY z;}%p6TGaw8?X-3n$WRvJaS#l389XiJweFb1;3!Q`SuIO)X@B;_XYd8WJS!2*kiUaB zXRx!KFg7m*-(40p0rG}z(y$%w^3J|xG2Osed;Yo<70@sXAGgHWd|q01;aM#_3v{^% zp}wgD@prqr*B7-!@oo26&_I=nsv!j;VBY9B<-IA}AbTpXQ_x5hHEYg%3(6_nEG>DK z#HbiWWB`h%vG1iOuVUM310t3m^VT11cka;tgyfsk=F4pl{XqSy5p$2lmKEep_fzZZ z)BqjdLeyOj$5BhcW6pZkg(A#q4m=i#)_2t%{O@)^#Y)DM(?UogQ{T1)z!~z|^&_fA zw9a1B_8G;GwvEixVN>_$Fw7gzg~^!h4&oQeYvn+}d?*i;x?qun9w`zgxw%ejzySP< zQhEOB#+qNt`rv594K9#tzz8s2jBJh#t8?Hp-yJ3N{TAWp>Ff=lvy=*7AZeS~g|pC# zQpMv8;uWn;U6?ERoL-Yv)O)YomcYsk2c0PeYVs`snq0aQ35+$ELBIGjd-XcPH00Nd zZ%tAUyaRI9+b?N+)Y=I)15OE&l(w8aAX*Xx&m|;8mR>4-!m7*Rh!3v3X<)xfr>biL z4VD^t7%@ZioHbz+Oyj;=czSt_ZFjf<0cW7(s%C;|`V=vjm)2u$TTf3O;?tzwyL1y3 znnQGwTd>gLWlykxDQs6{*eQzR?YwX}-0Y$LiNydX$*xAmx}fH+KK>KApOp;r%Sq?p z`&09HxSB;x>%6y^ zA8q|cRRjji9xA4o_?|lX3t&E%*FVb>l_5khD@Kdi09rqP{X;5-lLX=?7N3t^F9@3# zx#vbjfchw{&k=HTU@}?tAEZ$FS5-~_((^ZGr!QF4+j=T^M6x~c*WPN;*RHYt8~MMG zi-&Xr6ph7<_h?|HCcNP@xFZmW+{;|M+A7mMztc709d~y9`W}e*cR=JQ<8<;pyKgq% z?LTNN$y$AS%b$7w*{#0-k5D0bdHwta%X3f>pvJ$@V5zN0qCitLeU;1kM>xj+9iJ5) z$latSr+Hx#{Ob7``;!Juvr=Cbo-rRdv^O1KZ~RC>C>iI!KU+zSs*+@4X}9t`j9*x=aECcGFt2mLOfS&m^kjma_bm#m z5E)jf7=d0OWyJYa_a*_TQvMpAR}IL9Unj&i9mn!H4XY$lBTTm=h4d5N11aN~%@OYJ zw^~ADn!Rm1j*i0Y!yu($t-uf&85~aswJ&O{X2;kC%Ruic`4d8dDzmtyoHT@&s_S=8 zfeH?lIRly3MEqClX9DYUB)QhP^Bp{ZBB(E`D`tc-S;aUwnQc&d-&QxrTR;FCkVz6k zCjWX_uO`8uEh5i8|MlPm$;l+CVAf$jS|^dg;Wul|t_qdMrmdF@ce1F5HstnVyr$#y z8H$R-=Q%t+&kNKszjOONRq#=3WFoThO<}c8)DE~Fi$j4hFH4q4S8%OI**Z6vzs(r@sy~bOu^L#|DoaS?vtUs4Xwzgsv zGB0R@yM)71gcNe;y@Rp?TjtkC1;Cf(&=%p=2QaC*!Mli8d`+`d?*`vSMj@TP)B<75 zu2?194ukivXR!^m*3#TS12A$gGD$AZb|$=|ivhUPWR-I@b4?7UTvnzv3Bmv}GrPPT zgQ}T1k`o}U+B?6SMN8n(_}lZX4z-fz1!Qj8Cu%iLBXZ6bVXGvgwHvCZ5m`UxbJj8E zcGhvPCMDAHPzzaP&p>-WRsgkH*e0E1Q+mG(xYHFaUMCBl3mmslrGzfRfH`zh4&u5p zY3rbrL#>xIJmZ`-*vF+%6%e$Q4BF5lI2XEv_0~Nn2_^Y! zl%!qyyzgbh1g3Z_6%+&s`}JNfrsPYl*YV|!tC_Cnj4B0r-BDp={RJ2P;g&nm1Meh{ zre2q~zJvFpExr#e@)_Yx_4yw6e!bgyOUurq5EbkP0U3a|qHnBtoO_ZLbr%>LqPe4y z8eIg1+_>;z(ZXjWi)v!s_IoZ*!9$BZyC4;1%=)~Gs9?yy)w=+f-Fc30=ZdLgz6#4l zhbF4dmDg0V0HEh&&Al^^P+lNSUck}@eBU@%u3<Rmwkt)^jCoS4(~;_R98T{j{uPho;zc%qWMV)6}13j}F# zhJtK`0WgvEr*VI*lwqR{@N)-!IhcTIGm$*MKYGy`Q?xY9D{r?5M50HgWPft1G9+j5e7kxm>;XDL2!xntA-Z`yc_auf9Cq$DfF))qO4A)T$aDVOFJw zfALi1!NI$>@yp({-gl7(aSZA0*R@=%B-}E!nXZ!d?()$f{rWnazfpn$yG47O)Hao? zsO=kv{L{iT($ng%B#MzhaI4`P2UnaCOs;iX*db_W-pIZcQ>Ox#sQdW>lajq0rwLIb zm?)nMb6HgT06S~Gkauo_=$LPF_EmMQd~NPRRZ5$+Ldg^Ms^rH8PiBphC=pjG(>KNA zw_EeHc6hRt$E}eZ``H~oNiVov!t>}wj2P`L?vQ%zH`G(%akCoTV3&GI#ApWhn4Hr- z+`GI`nC5eG2@@Z&!335&f0B8Ao+RZIj=B0!nb_K1w5VS!=sE{u0w%IgLeQ?vC72jbj7@tyaNgb};9t=+5 z*8rSWcC#mu9A*tuFYWV7#<~KMV9Tz*DnNb@c&jI0PR&ZUp94)$NO4rK4vUfA)G5Sg z`RCa~ZF@=rZx;2M1M|~Hp&CDN8J7`beaRGBSHs1au@B)+moF3-$7qXTUe0$Q1)|k3 z3oUc!Ci&NU_ixK>WF%a1ss&cf?g$p*2QmR;gLbX1S{{{lfB=O+<0`vn}k3ZjR3xm+v!d@e&lpL`y>APZ-#x;#UbPQ;-?Q~=xXjoy?+(t z{A)mpxTeIogcCSPmp!>>K1ifTQ(w9M>saDaJ=e9$z&N2!f2TnH@Mon8W0@gdVY?V# zUN~#w|D9{{hFd*Ih=4*WNRU1hTmw)7v1lMFJ4)F^aDt7c(U&1M2%^Yy|9!~12$hrI zKPiBR;zUHtGdG>Cdxk`1D^$##3G#B;TTL$6obxSgZ$8+A0?2<;z;TJ}6w>YR5ba>8m|s=l=3-gu6q@ z?Bp6KFHkR69Z{UqmQVJ*(xRsjo+1R%8av9ha`@6vQtY{np?5kcBfD|Jo|MbHGsE0g zWK+mxppgJNG$7&mz6IL^z5}l;-lY8mhl{kE-y0VstS5)|AV78mjj2 zGif4AA0=hr|Lsgo07F|P5D`re(2Yt=yXtGMa6!TYd}`-oA}>* z^D8CURGT?$XS-i~p%%{Dc-gm^m#)5b$>ea*YzYE*byRnqzu~}Zc2m8agH|A^MKR$_ zbELQ!vxixp`MQ=8py&;>rm`~NpQp+v%&CB6!0N*2=HIA8a2zjM^8g$MBf&5?U1PRJ3?TB_*3H&dDHhLujRh?^1iqz~_3>nF}Pjmu@A!aP`%-dVQH{LF&N+ zWMa=(eCWyC(P8c*)D45T@h(S}0k z!j&zshi>1SxpQQtX92tRV^%+qvw8(1I7Gp7B;;eOPE#6lo4?POf!u_$fd)=mHLh$NeSqjxsaBwfO*DQ+ES zgzMPg>Lx0hw(3An^a7!rUIG3PxE94uzio`xabw;)>TG|Q?eIgBna73iXX|v=`?55* zH4R!8^PJZd&08PresnL&XiQsRq?XBxrtpL`w2*z5UZn%JJLvaK-prSM*bZZrQ9kUl zxwnz@#GPf+Ien09`7m-f&UDwoP9gSw=~Hvg;SUQ{F(w*`BX6CziYVrh^HRVsexG6qT@=c!@9~Qm<0${A>v$)beur0ot}f0l z9Mklhg}Sk56V^BNkTwE(O{EDtSi@Y!%4ILM=G4KB1~A1$fR#j8!trXSS7Z@Rm@rS< zI3koZBTbyS^c4y?+(FV~C?nXDU)07+@-JGJ(=jbR4oP3>7 zw+Oit!;abfxUD$cJXU^)!<8w-FHj8^wz4XE;f18qpfEd-e1%=39eOP@3BBLZ7bH>c z$R89su-+@AbziL6<*hAN9k#4;K7D{OPn(fw5Oxj7U6Zd8p}PgaFk#4} zx7sJU9u_I(*SKol`lUH5DB5JM^^1r~PMGzzb0Pi^yp&BAwH=8+dBK2Bf6@`-Ba3Op zuJMMRZeI8-s8jPabgU`*N}MSjV@|!%HNtdv`+z54(6p`Q_cWlcS4&8pfJASWOH7cH zyspm^V;_W(xYb8@){1j(K5)l6UG!>~dzGWvJNGP@NnxUM-2G?Z#)l3cRbCRblh?A{ zb0-V&oTL7Y2O`b@>$00J#sduJnBw}$4aB4aK zX|Dy(Ky_QbGO^Rdyzs`EWvq#9$4gneSMB+rjRgQmq3DDS$mE{+rM}cSiRiafrtSyz z$5m%$IzE=Vtf7{|n<*`!8PA?vF`NgLntyAF;X;}w7Gu_*%4Eiby#Dia@t-_`bq_Ib zpUTz0=2*G-pW$54wL>pn-=9OV(!6#aV|yw8PLm*47Dpb4!#9ly+|{Jz%6CfJ3-IUO zTpPiD@=1C`rsh56d)R=$vuQOCMV!ljU;7>TL{EOK)WW6Co9>QIZ2S*O_xpcycK@yC zqn88Y#Ilp&g`I?fly1PokJ+`K2;-nyZ4*zehMpNS7SfMXqan`=j;>8V@paohcFohj zbn#ol?iZkkd|l$VVX|v|AbiEqh0Sch@Ln37Z`+5j&fpFMCf~~iH>tiOu4G*ms@@i7 z8>yI>`AN^2AaqjFFJ%8`8QMidx(V_Ne3@Ft>G9_ZKL9pU);F%C*~ zwB`)=C8IGJ1*OwX5eUB72y_&DsMKJ-Qya4W+-uc3jd9eXNP5F*fBRz|_=a6MjKU^o zifj3Bb^HkIxh=TgL2 zW~KtfWNj$-xzPC_^-I~10)`sM1n%;*L48x zMX{bNI2<{fv~PN#Vk+uAd+3|}HAALX*cMf<`PTwumu$YYV+1NH)2sH@S#$Dfp{+0* z?pEy@t^3pt{+ab9t1X z$?-!?oos_}JnUQlD#6r_s!8Y>!C4 z80>iazMSP55DJ2-$=|N*5?;r(7QZ+&^=Yz-NXne?9pA>LbxKd%SeWd&o=@~Wc|f$f zdFy8j#42IN`oi|URsN$sbG9(dsR=7_HleNO`-Y#`?xq$<6s=)g0G?!9LfBKKPuP^T z@?uutrR?s_MK;p9htpq2;Df=(u7DE zf{0-Z>lwi~C~YoQv72lLu7e#-GSn?`&4UH3MiobZ%S(G$7k$+6woQ9D35Q*sP)JI$ z9XBBbU=l;0fg?kwoVfxgHFO&2<*d5`pm z`%uy24zKdYyvJJ2g6LtztxXc(4gGY(>gadP@9kVAa923Iddk$pRkEI*HLqlTj(-f0 zDHMB-^;4SQ`^DhI-9!lNtmlBhGPqy|POe<)h*0-KT8p>gG=@e>#w^prD0}?)~8vn1h+GR7A2!=Q{*I z*yV)R;0zi6Ef8eQ1;1IO6e)gdb&ePi^gJ42(iCLhdaT3N^4B#b?B?XToGlQ|K0Zz_T8GaOt6yq2Rux&PQkOhl~1g;*VKh9qMVWIY+I;9 z3CxBXr6?goXoJZqTW z?Z4u7TdF#w=iCKozh~?#1z9>Z~lV?b$rw zEXC}uzl0Z9j_v-y?*{$+{ZAXN_t)IZGbI1#zRutGYiJy)=OQ3ih|uRgsJUMqXSe{1>pziN&kPx&c|oPu@wL3eK85cHCYJ zHu}RNOv7DZX7WnR+CA4T-`cubq4G6M7@obw{eBsJ22sq#VOw+EX1NpXM|m=Kh%|h+ z72HVoKe|!*Kk=e`|2BwFfU@2&^mx%T~&kVDxSpqNP0g@IqUR03rwTJzu9}7+9=;MZD;&AZ)Z6dgB=>q4waar* z`w{8ZC%y&CNpqNt?&+Bg<$OsTynYa7ml+65OYxt`Q^u#rbE1uaw&4_SN^r_qzwHq- zxc$L>5;u;f6K#F?o72llgxn+1P2;{C$xdk5ISJIM@3&BG{k_SJ0Vxx};^PZzkVcMW z*|gkY;{^Q)<7aM`-E~aRdknJ4zZyIx84z}1L-sA^OF7FNEy{@Iye(D#oioeFZOjNmjf| zt^R5&xD+5jg{}o`LG9%o-Iq3ITj*ovfL&|rRljatGnTysJwK}KfFk7B&9_F^2?4&< zn`kQRrYcM+WrRu_X74_|w7)l zftd^4ENigt&KQX5gcfeJUv=VGxaR-qdjoFBuc8AhLvVh>WFq}3e5|&L!L_k*!1?ux z(@>%t-`oH-;(Np&OCh)+^|6`+SiS!F%{l4Dsx&%m+vTm6ZQFS1^hc-&CiU&nSCU~holvCdpgME4 z-_26rZSqyA0&@)B85&EYd*JEtyYfO@e&eKzdGNwdks@Tlk*@i>ijobED)wxfAuSW) zzT$V2Zj6XEEDs65QNGL?mbefDUp<+3%1BeKuS9F%?^JA?FPbUK(}78&YPotC^lnK(@r>vSapds8e>vuQ+B5^BgAEGu3tn0-)qXj4*8 zHM>Y`EU@b?lGe^+F(yDY!ze+2_hGdaZL5QD6XLt(037DF`u8CtA=a zxu7>C1Lv$#BUwGKCPTdEmg07ajt+Ssu@1LR`$@s+?|Qtqx=ze<8AEm}@^;*{G3mQi z&mHLMORM5)YMq#*@I$Kh_uE~Z3eWAfO5`NsxAH19SS8P6=Q{ZX8eLv)$tw8~;s1ttI8gX@hQpGGn_kCc&AlKA%g}ilp zZ^hVj{fIYpxp=W^)ZG*xrDa_d`iP+JA=!;m?nW8FU>!!h-^o&Bi=aT;4De>U*x0fB z1zVdFq~gQ6qpG<@0b5ZyJJ*y8YPCQv%M?ipX(A#(iRdFF=ifF=N0+;UYGcej* zdh~{n7;Su7Jo~S`{(r=A8C|e0V}d%PHEj`%Z@B@N*|sw?d+#01$2%5K3!hcH%(_7e zo868`AFKqrO=6c}cw}BJb;hyca6DgJv|a!8^SIc-GfWa@<->2jcMmrFe#0H{)9c5o z={+f%@V^0Ca~dI~&F*_4)36eSU>Qjo`yn_l83WqEF3SdiQc(;=q%^S-47RUgD2}pDk53 zOnE32)15Z|a8b@OmsiD0EL(;?OI3Oe6RiU@TVZbV(Ms=8Z-oar3fDHGD5t>brQ+I# zrN>^KR#<`(GM=XJ;w!HRv>ckJ+GkjzvuegDI@tcXSJND`dR4YzV%*c^2Cg*K6?R>A zP8s|KwugPyrK1$$%yHc2gB{0D6DBkVDvbSn}SJ9u8(qb{oxe+mRK-LI|`mD$95i9?6+cnQQu!`{hPbmlwu zSw6A%3bPp({8*T_Gu zubL$btl4ucJpK42_*be85+^%YdeHmuqLtZ|LOz|{`he^=r|~3{a3+Hfb7CBqW_EF1 zl2wVfx>&^VOC@>Nda(7s%OwxkBDlHR6|YedcqK=6)OC+H>b!$o;~{XYKHvB3dKTG; z`MSdrz4+I4Mt9l|v}L|HRWXFBN@gkzXI{NGlnRs1XUCE_-{i~leucrtOR)?h(%kp? zsoS@&-E>Lc;NNP#(R%#%kO@6K%!(!Lffc|HmKTAfF^74T&Rpe>Ve zHp@|VQB{?| zdB(Ekpe~o!(_J%GlMO-n*r(|O+-P+Yl<~13?(&e-O8vRu$S#$;#um|P|Y5g znc128CL|a(6`cjWD})xc>TO)SZRKE?rLUm7**mzIoE6v!MVD&YrhWDdw@=vGt^av1 zm-4FawYjpxzDMXYT>9ig=AHoX$_N{vYA75#4>}yo?dbkYciP*?zCt_FueHMd^zo+xz5A@Etw`?zzG!PWvq$E;^I&-WfRc2 z%}25FaMVQ^%!Nn7Z2jFWVa06w2A&^H@zO6nH;Zv;6B@H$Pt_ZdN0hl#vA&y@hW0p| z(|oyST2;K$9oJAZkq6g~#yO1wMBiG>y;B?whG+BtC#%?6Ndla{Uc8W~UqI31#5b?7Scd4&Z_CskHtE$0)HAz@eXC9rAGblI z#`)V_19xThF@K*lJ`aq)o~L>dTg9#H@0<^uTrrLK*6{wILnJp;0kkn!9j@Ooqi`A1 zkk8peG7g5@`3_)4t9t}hoZGwH2k~z^a&mb?u zp1;Q(VjpU5DS|dI6KD$!Fbw$UpsM;Vb+=XiTFNJfPcio28f$I4(kb0c&*lilz{Utj zb}8B4*)3+l^p3!bz?=BFgw+e2p%KrLXXeS9bFl3@YP@H=x2z1K|A)rv;>D~HTiMbiNjk)G>sUOtOEg0A?Q zf0)iwIJ9v@rR~v*mHxZcedosSp}dA-oIuD^C+}uR%v%nt zTsyY!cpg%%o5%rRZ% z+)S*R0Z6oI?Z)163z*V(@O>5J_2Ahmb`5EU!t9!tkW-cpy*t%#td|SIVVpsZZwJDA zuCVSUq`y7G2$e`U<;N#nJd>h14N$38&w4Kp*zSWwu(AI_AdBcwHf<1W`QJ8y{cp4> z>E8C#-%6b9-DCFvEEXLeZN*@EKr3aI{?j)^uf*Tw(A& z_boeiwAXrLvvKsa?m#sJ=`bbKD zTQLYRDxSVdP8l-r3!UGlU3|`8$IM#UE>iIO!TdHIpXw&7!C(DMa+3ArOaw#PhB40) zs@y2hbTO}XO-);_cYbTm4mmso zejd$O|@czm%=tVBXLE%C`zZd;5qm!^)Ql8pS}EXls^R zKkT6G2hUt+Y|AOqdH9iN$2e~Ye5_?R#J;b4jIHx~lI=>zvz`(S;f;9PjO=q-Yt!)wwG?dypigUUgACJ!`*{$-u8W6m4q zun?)d#;3H~vL4~UmWWLC8X?V%xH8%L3@n0Gp5>aKH#WdId09|V6^ygIZO`FT;&sd!=o<0D!E9RY>jQ1~Dv&F&VLP4b?5^u+l8w zFNhOz5u6go?@MSayS%7D4@u9^_bv?nRT;x-DApIG6DAHC|N6;#2%{hIlCRQJ7Gu{R z)=8kx=3ks7)*EiHJruOER(hZyn0iG2e3?nHVs)j?z<~h-O_1gHy;rXGM(yuXumt}Ze% z(Nw@DZQgn4Sw_<1w7IL7_FOGcwLcyP$wKhY7o4(pI{cK(+>eL2BIa1vUK|S}Keg75 zY^s%8K)*JI?udLadnK6sW%*XeA{~)?n)A-JoK|7vz1{Zt%8>hz{F`Ck52az*wQrq_ zS^Xzj?p=DknKe87wBXpjNw&?@NpS%7#zE~|R&?WI{Tvf{@qL|3Wy!Zdl8u_|`eT9# zKfkl<7&r4b(sG}#Qe_~$mdwA8n$G9M*qyKcfPE7&XY$+nmQ8cEL&Qp>@M#WHQjukG z&C%);LiiuHV>wFFbk%elebh)}?EWDnAT<<2EiKY6IWX{oysonP_9VHHI*-rZOOUx! zAKud$;k6cT?*i=ka;AB~<_tEyf6$2xJ*R;)ihuXB*HZ3R#Sq;D!NiS^Kg9WbN;@Ls zyOG{8p4!$S{u%6Ga8zsuaDT?`$-)9ma$}%ZYs{GAm)@-MdS4(-d^LthP?3qiXI>K@ z+qXE;t+Ps8ujcJ<=@qo1>Tnv8tcOxnE}C{ROFtG2&gMO6cnkSjpQ0>r2Zmh1hEp0n zPVdg0LdX2K-5+7qJT}RScy$Q%x@%ar{a>y8mdU`?&irdEz@j>|!pJ#MJFO|IcyvT% z(D&Xt!Xx=ak|9$v=2#U|w7CZ2Y zyGGS2zXy`Bp|vwMYoZKPOw{Ifi^a(p5fx|g|W{$jkr%Ncxlcgd|%CS z!Yy9nul^;8UZ!BoV)vIvFyiH`QxEJ!Uk73WIQhOhZ3XP)l6nb<3teIX!UkB)!Py<} zoPg-1Os}9huNaW<9Ba*ROzu(CN2|L+a2_WYMrLBVzHr){U!kfu zP5d(kL#J07M9032t{(hSO3n$)P{!-V>V8Ct!!zjC$BZLej z6Acyhyn5zts8nY-vAM~ybFg8bycwwVrVWD;*QfG6>Z-!BjPnlxY~xs`zlb?UYE^>; zS#~E~yo+V7{ku<_MX9hx`V4Y0jticg@4kT_dY;pwR&Q-C)TiTiV&=Jd6PGfh^de@q zvuBMTmH!J8J-%E5eJkt3o5PqOAs>dyYuUh#Z-Vjcy(-E`V#d&y;hZ{ElXlz5)g6tw zyEUs2geejnkAHVxR$lA>iik$MOKB9^?78^P&;M`0#J_|Rc-_#I#wKt7W!AZ0n)%-- zBL21cCmoHqs_^ue+uwlJd8VLM@3$siY7!o-!4KH_Ngux&*#4wVGynIGH2ybaCRHLtL(C zf-cS3_WOCgY?Q;Tg6Dn1Kj!~TD^L4Q;D+BxE43;jts68F6wr`zFY>I|2Vt&rt*|5ewJec-JSHrhw#^ zbLd%ZKBa7R-$P@gX>yiU89#A()<@KKQYY+n#Sjlf?giOCLpY*dPY~x$5w14%44JD! zVH&|+n@?p_L=y2i8DP;6^J^4gg4nr7zxe}1Lax?XafJTE!> zabfb;)B$m!3L`k=0JrcAApAS?)esQ1AU2u33S3d5 zDTWYT9yvoWjK~Dg$cFY2+X3y})zh@CvE3pQ9cDDA+|@GXY_or1+E;PWRb&#S#Ns4Q zm}SyMsR{{9`eArL!D~RnN>65YeH9Nx5tQ?XwoJKV4P2?@Yg2y+Oe{br$rp z4bC1_<2`MqnlSTl`R8-xHR$=a(P|xKos75dzT-fSaOL8KCdVOb7y~UQa5?aEFQ)P@ z#J)|HyTsBwAq{gSAAcVM+prlRdWdkU38o1~!9Z6mxBwJ=PH6$m1@`eU>%dcKoL)7i zaujZ@0~?>$nwG%j(&8u7S@}0n1ND;_c98;4sZ4Og)jQ{qL)TH=^(82^f>HYrr%x$o zUcCMv#Veo0oTNt%l^>S4=1VQk=xH-J9n-rca1V=o8^qkds`A0$PwBV4sUKOts{+P@ zF(k^)+)VNIZpo{Nj_0x*e*+Y$eyZ4V1==8JXH@)AHqKfo)v%wc8NaU(Rl&Ugj5vQ` zP}y~}H|eM*y<4)Ok)UPBf6*pn@f)$xtt5&Pwposd%>JP7vvOj)Fskyf06JW=Y%Y` zapgG4+I7Y@^3TQnapd44JBNR)&S$LwPVWnHOe+;mZmmB~O25W|Iwx;!bZO@=(zQz| zS(|h`8I=T#*>^eRTarqQWk~r)yw85WKhbp+ED&kgSS$_T5e=Ju_RHRsy;B==<^2A! z!@hKxYdnh1gd=0NKmgy7CVx0`*({8`>UVmISdZ9A-pX@{G=h*2RrF2gUIO9C9)1hd)E-~bXH{2*S&#NCe=A#{34xEuaxd4ubb($6eHpa1z<{WoYPFcq9kJU5SXsj zj9mq{gq z$?FXtx8(!hhiYpk#j@zz6|KVVUBC4>x=AC~&L}jv#mbTRd}yRVUZ%UHUj;c+R>w}u z=~4aU>V-=DPNtCT29}^ul@XiKZaBz$Gq{`iz~*5>9aVk?@qF^0Uk49+HZ{7{)SH0) z7#TC1A2xz-qXY3)H$us(75QRSG~!YYXFqZ6Icg{Gk-j_ZZ7CU7`}{m}gOf#jsh>Pe zw)D|lHV~A4WpT5c!CfYgtf#6S1R*&#I(LD`-E zpg}%bQnZ>?-WLcP9X-F1gQ*m!N@ZJ;Rzmhyjf+wl6#A|Hq=Nvzz;eNRL{#j*w)OT0piNZ_P zi7^B(wW4quC5jwU03WA`U?eQvnC^emfMqI781G{>#8SK;NQc)2$Zr)R36p8nn)6}( z#yfD^j1=Iyt|j#O?EckEK?_-?-BxHfcTOa4^z(!w@CoJm8uPmz!lesw0vdBEf6V6~ zrzYv`qBiE+S(QFyQ;=jzB{1ZIdjWQ|N9K=r6tNI~v)u<T zLSlf7$!j7W+}N$ZC>@w<@7UtM87JkSL@vC)Dgr@Rik&zt5Lm_H)Gr&nS=%eNgVgR1 zNzmM~<3BBAOUQ+a$@-_q)}REH7k9Og=zVOZH{VwG0fl}|1E>`fSF`Q;XBJvEjp)+2=3&#Yd4RlZyI&V9A! zbs8<`EB@hH*i~}_Fe2LX#*MWa(~hY|8zCYmUs=%Ffwq}Uv()aItytFt-~^>IS$g4l@OPqF(;AJ%2a2OkWk1X=e>h)O&;(}~% z`4G5wYE$gh*IKuTw^UV|V$$P7txfVI2V{iL2P{M|Qt~nJVNXoAc79@wzj{{}kwX7wUgcy{ z=+a;e;n^E0Ib|*!>=nsJ}WzfodbR+VBR0et*8p$^JS=< zY1nk)qi?+ZLB>1IpztT#uutjc?XUia-y3TN*RSLq$M@>p>Z4a5yNmwT-P^0c=~DRa zJaf97ewuQrpN0A79YQ^`J^;Hfb{z!(k4Rx}%f+_XkcRVzz$xmnb6u6|N@xWsC4j1X zYm?1{|0Y>4P9X+?-s1cVZ2!h102_Lf>VV_7#RNy!dhv)05d1KC>b$neaAFtG`kL%+ z)td3PsbspipIvy)KLlPGYN!F`Lpg+tLmZ3>jQD>R|EhWo%nb?U_ABe|ymfOJHu^#3 zl73d%X0!iJUBN&Uy9X?2oC)%j?=S9=69&776dT6*P~Ty<&Q8EZhn_^T$`@-|k*?`~ zwA)=RCHB${)kIS}lk+ikROGTyIvYvqtNg6^ZKeSi9{728>-upu<7 ztnitf$D<6;Y8+?iMFIl-qsz}(=&9_-pd z0HeR-swZ}eA!b3<3lt>KSUMG1+h{~7QB1|%We;;Wei?bQOy^EPZL(AyyK zmPhfZm!eF%Xs*=|yQTZa`;RYyWQ-It+8TqxaQLj>*xeTK2=7{41LD!h)rk75Qa+_sF^*5vma#(B$s{9Ji2O#Ka@B~Fd!627c|j`rjnu2UO-Fw{=gDDwS>!CTv3 zL2hi%H28S`{2i$$FWbaZH}AE|Aiu(L_D41I1!Vk{u+p{v>_GpU_8$Me&-<>IQ0azk z4Z$YpL59M|<*oVr5opgE;~#YyukcU2<*iy(%uvauQmC5tY#WX`4BCaVgHa5Nc@ zP^!Jeee_kDN%bn@5J~vrxAL0j_DGz8oY0g!vTDUVJ~UTyc@3nZgRFEz8qb=xuNs2M z26LL&_$2SmA%ioLUaxmGNXteb7@|VE0y|;)$6E`1Ps~485?;8Qj zGXsWEi&-<*rknPcqtH3HAUIG?LyQR&x$je04&@-{ZyNZBn+1QFJyvJ8b?k2MBIoKr_^Co)K z5LPacKD#l;^}%wM*PK%grXyG^Sa8vYw){31h;hRLd{MzDE1F}Po{6wRQFG5vv*EUi zU$d|>_c|qxf>Qx9tTmhX*|@Yhb?1i#4A?GowLPy}%d`0OB;Ah|S+?mhSRyp(#oU|< zc4s2in*Udc&KE$#9&T5g)5THck0O$XHNTcOIqcGW;2K@swW>=`@P})1-)V@aI}>^5 zR?@7w2iD%NgyAW@lSsH|vEAM1Hs)?;BZ*M`=xR_(?-WxTA7?S*W~|$(%2(3qL101F zDaG>xA8{v>g=+iIV|8q=c(>NHS`i;UEfhI88xYAm7!*@=mG9Fw2+` zf-8o-C23hQ>{pj8<9+EtI%Y%3MDB!YGbsIIF9!97LtUk$FdMVzv^Y$QoSdTY{mQa? zqztv%hFq_fBH|$iQ@zl&R=&wLw`Z_$|--93W3d71Hv+;n=c_p_CI42Q=x7!ytKoWuq!J zN*FFx#CcfLFM?WOsia;F3Vh2^o^Jok=y4p44y%46lHiz%rXp5BeaVCmiRW;xIy(QIKX4H2Zxx{33&T+)_# zC7?vvE5<3aLL7CK*WVB^s0oa>Jy&3eUCJp0lz z&KR8DynkqBrs4s@emp3Xplb{=m|1f+0m8lNT#II7qRF8I^!0f!b(z<$O%{gswOA91L?vlE% z5f7_FZq5!Fjv-v5B?&gz-hg{`EOEq!Q^OsPa_4XNHLVRW>@WcV(t9hd7L(k(3UB@h zTV*bY^@cRC@G4V_NzRo1NLN}mA|lhz#eU#2MB~m1jZU62ImkP-TZUFZe>f%aKysXS z48T%jBDpb|VWQ*U+qAEa6kn3}Q?bH{>C{0w?y%56>zHfy@{}LwpNLQf@LEd2roB^2^B>kmJ%$U1LMTc_1)N zew5SjH2r<`D%koQ@G_xCI0WTBd$4b4ZeaBqI-HaA5!vXm;_!tYy-Iyu6DA`%uf|kE zDirlTw4%7jzQNhZnPM=fHwJXUf3Bh-22>-QHw85V9woG+w=+~98e8^HSQ`GdWE+Rh%Sw&sw#9lf-A zLfdBRyBYj%Y?LuPph?U#W1l`c0meK2%s)%qIlEEN!1k8=Sit&+9#*=6lXh1||NoL3 zd&ke>&!6|+-2VRLMhQl|V`1v>^maG$!;SHn1#RPV)~w>(uf;n4la<^|J6$OK>Oz4~ z)Sr*R4>ZO-J>44#_IDUv=aU-ij?>!`J}y17`U5L_xnlWPC^3G9g$dJFEY%=Vbz*#j zd%_BJNzk@sr{cQ%&&VrV4&Tx$HV>#5EnI0HXaim=IN(3>cK1plKK+z;mJ#0!61T&C z;9nAGCF!C3mK(YQqwV6i>mQ(xS@665${+l)h-ihT7|9J%)T!(4rZo$54(G1 zLpQ!ThuMf_>oKS1>{jdIX=CCXQ)J84a)iuIu#6pjZi-mD*sJi0eBLrkMYwbsTNH1< zF6G8OXogG|lukM(D(0b0GCE!%#`)U?fUluRCkT z7d>FS@91$E^5}Wz1NG*rw1v>0_xf+BtJR83U%2iiP^YKn+pEl1(LbuUH{br^-EH*b zkoZad4ODmSidolV|0e_wqbk3dD|xq8YztD~8YPSv58bqp3y56`X~Xt-lzosc?2^4t zZu9c4U4&2(ui)siVQHbb#>Xa;m1Nh^vKhD5{A8Eh?ixd}O=DB;8ZW`m58a#0{-n^k zgxT6$llI(4Zu{51r5cVYtVZvXyQqes3txJ7Ys;jLkV&PC;x)Z1LZ3gqy$Bmc+vBAti=87*KQnhsp z)=&?UwMnOv2lSPnmNf4d2>s97@_LFmWY8x;{x7IzpLx-ziVp;;K?c*ykeN|zOIT14 z`rNxva9$oYu;ES2yg}y!h$di|_W*vV*1l4LJUJ-y$FO)b*rW{33QrBmG(W zBcc68_&syhHTD1jN+_q__pxkt4tYOfch|{z7VLQGg-hWcoYs?#UPkYtY(o+t^1lW@ z>kx49LB=0JJVL6g_x(eh@RabNw{OI&F%G_)L8PTS7YepubBl}O3XqJhhTkYA!8p-K z(p{h5qlx?0N+FsI?QAc69~R>?)>qZmuv49n#F>DGd5*~(Cyso4E+GQ7Bsu;vab#a{ zs^4EUjb%CTp&yr$vL1>l5ZqY;8tJnbv#Thv_vn*^-tL53Dq+T$K#30C0!6t}Kv@IB z=chyuc;Y2(jQ*nxP6buGLLY`^BM*I{Z6XIGbk^Q~qiiQ$h`9puQ?aCU@K&#(*Mn<4 zyM|-(lu9f4=1wt$=SSb}6`Kw0R!S2mt+}BA6s?xJeNkqT!3X$FIv~E9v&$X-rmMDh zBOTwBDA$n8jjh)n&Mu_e%~>1Ty8Iz&mS>}Te3^DS6Dk^dX1yee-Sc_Y6>WTBk<{)r zVXiq7)aYNCc$7X@oK{$ZNsH3n|7$shKQaU_7qkHm!8!y!>;@b=!5k(mx385cc2jJO zxPtx4W<_610mqFajO?q!`b<6#3Gb5SHdh>$qu*lRyFQAi=oebWrJ{Eu>hpfm)O?5@ z-#9B&#Cs;!@q&J@>e3cKh}~`0u9`qLvh`-jM^wM{WO?)>$?lco=3?XlscRl_AaO!L zYiC4)mS z=>XKd_R%FOs9LsMO9GRvXye#c>*^=(tG;hoJvuN_yKRk`zByqWMj1A?Q}Wh|Bk2hr z)i3x0{j?2S$dA=`{TE+{LMoHmf_~7HtLHZ64~+Hg>$_zNo)zE4llJpQ-FU~|r`8MJ z^|zD;@AeV8fD!tLjPm$6RKVDRj58 z-Rz=^nGJ|#*n&=RA>~ICg%O}jR2zxq7cpTM4B(n#l~6NG9NTly=&=Jteu!jZl;{Dq zUsl$?=y2q}*=2!6{;^07D7y0HN9ErD;h1k1u&WDC&)jsL^R$EhJjnh>F^aPOG1Z!- zWlS?!>6iWcDzq2>->A_3rIO5bz$HRxVb;V%BRJqc%dMia*5^-E(|8`FpTF_sZ-7g) z%(0pH`R6Lji=(&ueG!jv=-m@6^p75$bP(e1IEj!mAYahXADC=T&6Vd~_X46EugRAlwsMioSTT z?SoH@0?d1!bQcM1KHJy9z<-H&?#(}w?l?t^I~2QjxMvu8_f91X5xT+c@J0vI6U+sXx7Mu@5HJ!_ z;R%HVE3hg%5psSxdCqTl-W=hF_0r&*mlB&u(xUUWk&Lu!Mk72~neVYZf!(k{=2w5s z8?$7M4ex>i|IHFekXa>3Wqxg7mo{{qatQ~i0i7e+mZA*S;Rs)YDBcEJE7Wyzy;o0^ zD8g5^aMjh$g0L?)$ZZCyW}TVw%Elz9k<$ML$lc^M5_YAds?C9_r31g9irtX{k_GUq zlC^Wpv5mPdM(ipWj|r5AG(l|YoQG8l(H;@4?4UF`PthMhH$5wJpDU7XEyuiayXQx< zBBU(!`RaK$xM7^=Y(d`N>cQp^!>nONii(m+n1%3EKgrOu*g7p{RvhoPrt>(iiKOOE zZ2VAouA1@3W>H666oD5|j@Fw=lBuQ#$lSk;N?pXp^#p!2g(uFZ&BTegU?hWOob+01 zo$!%(nZ`&i87EeOB+I;8JZF92)fUBl>p{TE2^X!lliPM@hNB=|)~t%t1j~ygNRD+> zfe4cc#{{=9B3tWbEs!zWAcoD3F5d>a5rQaaHg#`u%n7V00hhm*?oy`r9sA;Ueb>9w z+BVE_hElxQ=0jd9rY-zY$A}Z4LkPtT*j4QH3_o>GTe+kn58_T5Hb$pV?=l&%0KHZ1 z<4)lg!CKGBMY2m9h*g7E0YT8H(28pz}56!9K`qOZ6u z(jbCSjtNXgKCb)xJ|FEYtApVHd-4;n>Z2c3iLsxOg>64K4{0J6aV(;i3|VU)k7-!x zHi}7tfy{FkE?>YvO&l5tw@NfW;fuRShT@SSmOYmASrDb82nUsGH<61^QPl?(!Tgcc zaiI2fRTxiKrxV)?XsR&aiSe6K$y|=Y4P=6HV53*QD1!qPic<*=Lxs5LWdU84z=Stb zMv}H6I@n99N^A1doIWe>55Ko4vIsO{*Ow%>=_n>)kwJ`-!kk}U*>>frRj_w4ULXpVZq}=dx1a^Maq&jmMsUvxy@>~Y=}Qg+MG0b~nQKJ@ zsGX~`=j&Q!LcFR4u=zc3>3wA0{bid0CQ=0J`xRGaEl^Xja@tYWH9#8Eog_dg(n}~x5d|9{AcS6pP^5$aAwfz) zM-d4ea{%*IOF~i2s1MoWAX>Q^Ld}=_bBRz z&@7k+7xv_IU9m0LI#IEACdy>U5+PAO3K@P}to;f*s=f~)g=!svl;V&Z0!t;yr9U8?;=>;{82OZZG>I4%`X23gnPN7 z@xxk7z;#;&HPKUrQ8&DGs<9{DI&$f8k)<(O=1DT#Twu1Nj(SnIy3;+WKCF^bW%Pl) z{ux%ahk}V{Hb`|=^hIv#JC;cV2u`$4ei}Cs1Wz5nuHRtsEAToAwtKJ9^5c5R4988) zfEx_24Of(!ZwN9mh1V0)>Qm`o171KYPXXgZ8V373+wG@pXJhn(@wc$?m9LGY6#{o) zp=9$4%bW}^YWHO}1K<(HyoR0ity4VGZi7oMV=D>J5>*Z;k9wjlODwc7$9W7A1aG{6 zP|S}DZ@2J27L=xkL0EIiG~z*MaRfb# z$65MjYg|-F%UnIRuQf-AEq36v#hGI%hi~&8m>9Zw1#G>d6gZJgc0IAPKVDj$Fg8Pu z=i1Mod~_Hy_h=F7zihpo$q&9SZ5GNRyxsC?bGM=H42e_;>nK1LjFFwj(WrE@VDEi@ zPDbZ$q-F74OgHe-y9V!$PLmV})S}$R>;997cy{C)cm7B&^QL;bN>4zYzjghc2$t<; zlISR2$H5A0>U1j_E^&-f=}~;qbmTaW7;F#HpG(l@LbY6|=ll`n2sZZ>CHPEw&v2Ll z+j-s7vwWhK^eIp><+c9OL<}!Qdd_(;w%MQ0s1;gcqOIA_Z|7{?sUHueqD(mR|NklPzt##dDF@-PWoy(mmYLI>y(ZCk ziQIAz22$zBQy#n{_NCDtv%1Fdbz3cgF;Q%|?ckpRw#zU_o0f9jc#5PK{A$FT{OILJ zW=^mj-I9TKfo0j%EK!fXV%EZ(ZAvrFBKP0?MU%#vbNaAn>1vo1AF}jLSmcw4tn6=6 zwM{L2s5z7zRU!rb_cAH}?(4K4zoS3nAx1JGFGYc^Oe@5#t{YzIH^~r{qLo&@HN=Pt z^Px*wc6xIAd^52~qnvc}x$l~;YkDQkzPHAXONdbKGM3hI7{Nn?m885545~$Y zSZ>}32kzUDBc?Z(F7QO}o0VgO<9GxrSwG-fZ^`fWR`g6xs>1)gACLE6BA1RX=JmjW zW{mN?UdWV#E2U{XI)WfQDN4#lsnZ2q^KZ9?T>CY8NVas8S?nx)v8&2Kr`6l*X<{$O zoGYbST{E{&&P8`AA35AR6~0kPZKs|26zj14{)!SCs;!9^qB2nggSSd+XWtyh;(8E$ z)~il{wRZs6WiAcEIKB3G>*$Pzb-$Zd!%&Q3Ei{P6wS9@DuzN!rgSsK?!ezHCsRqKJ zB~cct*=WfsB)ptMjR~GSk8f-8g4xZfWi^=2Em7bZ_~5aaCOgPDp#fnIzodCppoLXI z8eYjtk=9gZLMobD46!c^2d4qlkW5zZ=idQ9B+Jnz4k8Q3=*iVmJ=-VtJ^CBC_kXqcm{=asXZw(iG$sG-h(?48dHy=yVM~H>9zzQ@1T% zh-jS;Mi)vV#3VW`hO{-Z`z^z^B|&7-EhJh>ZQDp?(K)PkRT@^16}DR?{NYi9i$C96 zTVq;DJf0Ap=rR_E)2=i1x`Akw*oCqYAU6DDR7?9v`7GeRmH@z1E>qP_N*mkW)zElF*osQtydt z7|b7H5y3`kvqcnb)IJ;eWZW3di{A-LdcjQI3R5hbebnU$U_ z#t=_k&pYwNjfLjf(14&KaWuL0oUAZ0vsDoZ96!Bvu9aFBooYW8S-;MlzFR zw*WzYgw%h~#5ZUO$lk%*w3}<@><6j0&KeW&u|OSmM+Kr6=}X zr;{^CP%AED&IB~_%n|M;zm$C63DnL0wHj3FCV_5wEaUzit*+WC%$l*Ls=qXrp=FEv z-l&mf2K{lfd7kk;(Q_hD<15J4OsT@09vNfV>}(ai8|^5)Tap<#>Z0qaKNc`wprgOk zvlMMRESYolW(zwqvo~?!8cnpbRFUjOTS9Wa%^iiC0G35IVbzw$PRu@ve0OYk3#s7u zPG@#kPV1(|&eZH|7+k+V)TRZbr#%{;i_R{GhsVK&=#sCGM#D&**0&(IWLE%5D@Hgf z_D9>7aK_DFd$tGHd4(x%1&{Yl&5z#{IXt3?_Oz{JZ4TpnrD)DhYo3+y$76bp4l==I zkBj2H{>tHWR_f{pUCWJACDmxu)YYXc6vAeoDN(`7z*O&PiG3_EXToX-Jm+I8AnbPK z9-EDfS3~Voq9@ACmc&bYaf$V+f*Sb*-IZ^q~S)3!WyR$%K@x}ss0jAf7(OH<;&l5 zi3PEDy#Qpx06}#vNaZ-$-BfLU4?x{|o1v_6_anAZRoT*fPSN9dE*iQjb`!>{9y)^7 z{#M8;LwL+(ON3SgWNo@zWOUOoMPM& z3k@)zS(&)CN_%)!Ne{9J^qwq+l0(-v8v)A9sqrSDeon;$Na+ahcKNInxux zC6E|zIW|S9JU8*R&{tX04UzxYZ#v-%ydIC3J-PUT+DrP64tVd2+fo+1dFoX6`)h%J zIbSM&La6g9v@^;4F-ql;{6(|N^A}Bo(RcXY`BJsUzv*v7|0xh~|BjtYVTt&YG9)ii z|9k&;2@HvFxctFhu6K~*l(_QgE|Y=tpJHE|qBK&kqnK8x%#ysB_JjAW^~yaf4{led z9vh~2Lj#m$v)6zWnFQZbrd0)i(M&!5TMzM>{Ue4ao6)mdRP->m8)yaVzWxXO12bDl z;!6nF6`hR*9h-kOT;^>_1P1wr{;%A2-WlqYB3jAx)6Vz3{q%Joy0`GCAz_v?I;OI4 zc(o5USjU}bb%8Xk#$ar_NkprT0KMk@4^iabq|7VW~*w82ICQ8wFs zU7c@N>UCj9OY==A>F6AW)NdMDh@Ee(v*!YdE;+)Si&Z?3Jfpt$0P!=#VMB{#t|I=i z1^c=JWbVeG{P%eIJr_;-28+17wy)ms55E^XL+=zdTs9Sa|D#1bmITRJ7eal$a@X+c zH(bRN&S%;HeN3eAS#Q7wIyYXH$n&xz5!f#F2rbeaQ{+F0BNup53csi!Dih7dR`4>N zR*uC=5oT)1ltwLvTw;6~=pM)F0&lI%R)^a*?403jtli zvcvg8)4`TuBMbJylRfk5juvRT24_<)N!EN;1oFP3d*O(N{6x+C*pC)dt8b0hNm5iH z$G^aQ)Hde{m^RUA8Ix3UkqmRRZ5G>T329J~3UDrM)M>K8(4RO6qjC@?b{D42f)Ru# zpyui4mlgV}Mmv2qtW6pdQWwk<8WV2wCgt?j@@OmzMLN>1m6IfLR(K(5YY71lT_+6= zvb#S0+GlT!zPpzcZfI3_4L%oX(6?YtceyTqGJf8m{TrU;DO zV%{>cXRg;NHd4Hxr}DcObY73*k&I~E({6-?kZNhY~SC(0`LM%%eDRVIjpCA}^J z(A(#J>v%c{jnkmjdNK>YQDJa^VQFAM(sTuq%|CINUIXxuku=5I&5qoJr0QaPG#T2{ z%V^DvqG`m>NOp^wOx=NUsD*_%J*`B=6?Ez-#V8!6^>_fjCV{*x8dI)9IBQOz1Ig+M zc@!d1#Pmuh);ExKCXAMN8MJ13xl^u~#wSK;?1@IjU%bLz;jE1SY+&E^|ca-A>5%lEJsS>4O9en@>#c= z*NZD=_ec*aDdp8wk6ZI>kFkbNJbOK@?pL9|6zK@Fw;Y?YVaj(% zOhd}2$)wX6#4SRptdje7L2FUeuugMt{1sBOjY4Fsi|6c)YV=0?$a5_~y#Ba~f|bAC z{>t|KL>Hw?@)P=tg{%4KB`LOkUmkl^4Jvu<)z1PR#Wf4(+ttm>D84(@osQ4 z@RFfPUW4~|`-iXL{#N6NH^fEutitfgZy#T4WpT;j6E!OkWEQ=Ch)R3oFO0{}fv;{7 z5e3NIBdPGPtEYHx*&j0px-M)Y!ghYfr)nGeRSM{iqla{*(}%7FCf0r392*TDCzO_7 z2Mi&U0*iKH-@>z*JZO^Q2HG>HDy;Ys0mQ74T+#T_1EU@$S7g2>Eiybt|SY@EayMZ(aQSB{FmU;s(jRrx)hK7--*H zwz)V>E*ywfJ&?DqcZEXTZ)#{XFvCwis)K+nk_tDmvD0b^xb6Xouc$j$i)X@1qO>LC zB1h|Um5A!`o_L>br*Ly_^8zb^YgM}|V0cNVLa@#A+#~47)@KR=J9;Y%7SO*Dr|9q-)7sclyBq58tO;JtfT^ZG zB8^4lQ3iFbBTREn-mEz$^`O_v#;Kxq=F+S%7$JWJ~dg zZ&0se;K#C3M26pipT=H}^@z^g)inUk;BF86qkR&cZ_aWL88zu%x4Z(wb4 zi{h1}D3eAhQec=&-KzGXJx3AbT}8r2w@c9|Sa3{ckT3(-t81Tw{ronjMu&WXYtk!S zd?`WPSRpm$6F_2+Rd-3+%imE0!3-#hsr(9qf71DAzq$*{6l%&Ln5uemRVurtDEDmR z3)1lZ$aY?QDcZy?_5_kS--Q%(__Spolt8+MeBShI5u78a#sg!vLL=m6q2&1J_EJ@! zfCfFbF9shMzKy&E?l#nFhFpVlqqo~pud7Q|u6ZV3&~Cl=PUuek`eM`(5U*1zdp3$WJRddZLHgS@A zJiL?q!WG%hcC_1BC@k$c3t-4``QGFn>pU5OF`u-?c8rGG@JN;`>AOT@~{p z_&xOS_p`@;(U>G2qC^wj#J&OzsztlC|Dt(J4P?F&#TGxfo!)pWV46CIe9JNYul1Y% z!Jj!(j5hZD0`xMoLJwzDe|~&||E2ZYO=$t15w#ptz&K46_@XlCGM z+2L|KJoN||p42Cu9vO{xeJ`jkvb%c9{bj`A!pDMI&1(+ z=h}x(X1=#%^K3h`UzPj@AiT=6&8!tI?sbZ(7cRF~_4u(#=}k?9x;?sx3X>LYDqW`) zUP{lVC7JL8;x#rDTKVL4e8c9()lZ-kkumBXx8&#cc+=tjk1+D=svdK;>j%!D8nzc{ zdiO0A@=MX&^BrtF8W-fGS#xmyQ8g}^phA0sbD$z?Uz}kI`kcWqr3Js-%s8hJQ}f(so-Fa^S^lAy|C9HvI?Y-+JxvuT3yDV$NfHej=>=}UFid3_ zCO@^wD{yg%=Vo*MCrgN{Ei!+BBY5;5&k`_;*>Qf+WT~6m_5LQ?n4B0LG+nL1{wD_z z?ggmM$sR6}2GiMnB88Q8!<$L@LSe;4NX*Dw$gJET?FrNbtOi-nsFc?@HL*8w#V*xw z@pHgfw#)c=w{N%5@i*Dq?bd40lsBw}sb0+m(QKVKL68v~<Fm^n6B4_3u(tX9|e$Wmy(5!jf zYQ5#dFiF3YY)$%Lh-irWK-=iG{%{_f5FDEaXR;6;t0KDG$*S66r{ojPwI_6%vYMib zSti0ON}~I<@pnA(N7t1t{4mbZ!#afpUJy{W>I!5)J2)jZnLai1l7STw4|qPimO%x0A< zPmoC)Ps;=7W znL&p=5z0)VpW4YaLGyusUb*u5{mBD(EpDA*>B8&;$}FY}G$0(@`*(-z?Ap*PK8>3q z4%{_T>=e5np>sQ;;knP30#hQQ-wwAfaQ`Tnt)6Af%ZX2?rP51B4sV*JP${q7Xi>EXuH}4d%T!=8Z)w&8v-^T= z2hY#uG}5(S_Fo$<$>9cxGd*3Y-%ExTax{V)f)vte=KOI6^${`JO#=l_9^UVBJFYP4AKz3M4RWS)OeY& zXlI)Nc38RaH|k6^-1(jVO_)GGn#1idm?}_eJ~RMLUGK_k83zV6R47hhk2@9yI&+CU zLyn1$ALaMj9-HFSA#!;licUELkYJ9}is5k`J2TFAr)gjGFPC4;-}e`%EdKK4f%B$C_lGmyuk=zKH}tMWY*UJ{qkPE9C~8YZ|V zWN}$m24pcv{vu=5yBE9dtr(hoj&OsAZJ!~*^WJ%hhRx#dRYfDJ2_HgU=7w9Hb4+}8 z%rm*W;0}|cOIySS=)V?g_5$Q@zOJ*`+6x3HKiJ}rmwkHh=@C!G!ey;)*}mH;1T%oc z!tk-_I!B}hhF~)e=6uL6f%;OouH6D+We_FCe)_|w1X}WA6NFo5a+F}7KC@m+5J0<38t%2*Wvp*D*X!z6+jV6B#gk>2V z2riA;dM1dt3EA6uWSo~Qu;>BNA(w_uuI?W4o~QeAUZR9g$bRlYoVkDEx1CQ>7{q6V zP-D-ze@p`B)4A)9?iYU=Ch3WQU7U7ey>qM2d0Ah$DZGs*;N_yU8zxDU*X_36y=cAv zt5DJ!+@@t+r6}jraf|uk)qEX05Rrck%3*xH)!^V-n|@58Nu!3`IkB>~nN!A0zwq8c z>7`hdtDx9R2jiE2Al@-Kf*QglykLc19l0F-Bq`3ND4nuO|Goe|J}u>_^%;Ssw3}ZE%Olq(|YaaZW~Ggr-BecI&gbmscd44%(S=+6Xa^KtmmV&k&ta2Q0jg z=hS+KQ4Ku0&&{0WAAw%U4iiQ#$gp(Sn3+pLPIbu>MK+$guOx0uYc$eq-}UqOzLg>y zfFd(S_HraiL%)#Q#ZLbCJB$#bUxwKFE&J@hRMH>pkIq}XxHEo?q;f+3jRmOv-$8_H zc_-jxPq@|3M5FV)GuFwzzi9G$pTE7n=eb1T3U>OV9PpnO3imHoxBd;7WBtFP34}-@ zESsVHYVuFBr&Rx2XDBv%EuIH?r;6(w{L`GS)!@$4KjLt!{{)4@vQB3U3basRalpCm zrS;oYxAHz;VLrz9ZW-lxlpjwTU8pp1F=60(!Bp7Cbk*BLO|w^QMBC5f$3LYPTosfN zL^CMQT=_Sbd`P|t!wnNO&i)@xz5l`4|G#(c#g6Iyk9J`Tl;+|GX3iZ(wJmh34%I|9 z>H}Kxi^FvCcY4xg!39dogjxvRP2qQXS>$@2nK2(Mjv0TgyDvLHw!@!`QiU_s#D=Zvo0rfXKp*>}P1I58HMH~V%m zWFxWN!jv?0#WD6Z!=WOH86`FiFW!byJcGt6;ENfuVBeaH6ANHQRWa3UgS(@ z76gLSf}hq$2W8Pn!qw+Sqer?Jfw&XUTo0c%4k5)Ea8qTVA6sVzb%=n>$o!(8L&1*2j zPV9IB;MX{HP@2ldz&*q&`>#(hz?S!U9-P?XlNIF8LB(WMJf20+P;yTzc4ChgwPJ>O z52bDf`S>VZI)X6{Drzc@^+b+o&~#HNPc!jLdiVUp6H-bg)X8C912GmU#@oI2&Lie& z2!G4h8N?439cNcgs)?NWqj#e9PCuefsvN$Xh`*3SeZFgCc*q>Rxt)?mAzCT`JR3p` z#>3TYc2iT|+HF>+*Yy)ai!q$?AStm=Q@1X=hcFS))W4!e;&CLmRDlaaWZS2`$Dtr8 zH9gC8yJWb`^5z@F>r!b!lBrYrsJs}c>5wTR7CU>&6Wd4_{*(P-9lC2;6cM#<1`$^H&B`J&E_}v z={o_%vEFuF(Fx*BB8tpV$F_r7-e}ZYLjEiZLwt>ruMM3L9(}9vZ5YB=sbvK-kXG0C zHn#*J^u&n|G*{1>96*SjuTc zR>n8^@Wb~v)(xSMhhstC946NK^W3o>}Lj5D+B&S-g+~9Z_cxAf9j({=>#mz*IDImik^#rV?%_H zvoVpWIxVIwcO1ywa7HM3!<-woIN3%(`jNe1H5A z5y7VFs+`eS6H~T5VE`JPquXJ9bAE|8Wts;GGkve}vqpCTxFL7I80Cm*`s{=$FQPn_ zl*=Cj-nFU$@b)1JhF{xiydpdgaCZIlDn>XUEM%JgI-vb}ji-ih2Rr5Ea?>6!smO0s z;T}pZAFZ#4uv!BMR|^u1h7|o|zc=eoe{YV<@8t~r!)HD2u5)`Qu*f571obu=JvR$(05dv?FM;HSIUaMl>`1ykwE?S@HXzS=%L7PFIdNU*`2@ejB!# zmDPk2E8Dz7n%t7R58umMMZV81UXC=cXMpxf&my?a_8p78FiL%NlNXjfBDYRG?!S+Z zjN`fp$TO^|b0u9(Is5pF@0`G!3{kP=b2SJrDQI|ABw^iyM2l3LhRMNisl3HBRX<$drj&!;KSBSHJA;U6!$e{(5%J(xXy@Szvf zyjb)XGR%+_qRuG7Y~%Fyx4HuBm2xffB*a^^PfxhSAQE( zW2yQ0J-qaE{nrls|BfTze{hCbapNxvkZm7KUf_l9dB4tlef#(EmvBvF(~R>Q2hdR* zdud-vU}TI@2K7$!NTWGm%KR?r8&yYawo)89ry4mA6m8$aK6JDFMMLlWlN342`h+ig zw$%B}lz6jv3d`n08*esJQus-hdNio&IKuQW#g2CLP}V5tJ5W2_<4 zg|(1HJeTGFBXyDX`xY_p6(lm8zrjjm;{^F6-V%}S7VbXCXi91SleN&pc2PE1gJvO| zFOjuCJoP4BU6?>TS}2oD<9+q^ML%1LQ}-8SYxXb>jY*P-)V*?q{cI{5Uw2 zUN>C3?2Ce`pWtucpvs#t>>@2Bto_MWBZq?ePw;(pR7Ve8lA2|X;BPJ)**xxCCs@CI zC(-mZoe#CYpje~H(SoI_zjk&;%W<<);Bm%bJgxHeF4pca-T5teabT6p_0U676Lj;T=rHV8kDP@4oz+`FOAG(H9$;hE1s`RTc+f#$w$(v1t=6Z%FDtt1u@(;dJ;_z2;?NeEI=gt(^qDU|Dt&y46FXN-<}rRN5r0$aU>fHz#hA4!TS~4RYQ>bGJ}MUtZ-jV z#mn-N#9zBxsGXk{P5oY5LP!R(K>$2KP5JFH4qY8qYZxYz<3xxLC~XezKYqgdg8PNv z?o_#@MI7ve?annBZ2=tq@}{@u%9VPlqj!`;m+66h(eQ-cm=sqrwTiNa@8ht_>O-fmm8oZ7oCDEtm=fWiK7 zNzmSmHnC77OM3pY*7n+=tcK+@4mZ3Ob~9^Ou>=JAsyp2-bykvq#MPAgJ7Vyp3R`}C z7=z|-n|v;BK-JFZmCjGnKz!43nNzy}nW1y?^dsMSxNZHQO@03!dw2*uZJ;ZXIOE|} zwzvA(Wo}k^X4XL=Sd=&yS@~{?9Qc9qY9JgG-M;{Sg&$F&5lk~P7320EuXV44W*i1u z3Q`40d-k4jk~iw~0t1&512*>(B$BNYV&yzE)Nu9K(Uzuc^lNJv^2m#QF@2Bh`Yuf#qm9lbt3*O|7=Tg|G@Dvao*r0 z`9}-4UZY>7tIvViD2I8`2XJlEN>RvXNWt6EgQeMWkvuckk(rBT&S9K;7T?=*i9T0f z=}~TnP>s`pRtT71ti|>FSrBM23m&;#2+Rn38Nj%TyByufX5(Tz7oP2av zdp>yCgK?$`?6-_{ZEoR&_g*@_g17F}5&2+iX1DL@;s}pRMDkF2D|=6yFZG;aJ4I%m z>Fr424W3dD##OzhP|J7=HnLQDUo~v=P#*POgTFuqbHLcs`safAHA?qmDY&s4EFay+ zbw0wJQ*sF&X=KZ)XeZ*h2nR%sexqK-F;QuFXMW>kqDAvZSdtB{Jb<}!QI=}r)QDAG zTkl=0%f5*X!&-h$T1vY?R{iMwlFMo)bQdr~4C1~5&!}1V$oE$Bg{9aE#@O=%CKi<) zkQi*U>jqB3d83LH;|DCT4!CAZ{}eCuno`#4c6;jG$DzK*vXf-+_#I^jzj0T}GoiNls(6z?H`H`LziCZ{}v z&0=`OQVdbxJHGG2&7l*xYB5Y=>k*LH?!D7C?COonmZl;Ob3_7VKClVjQHQwjk>a#~ z)^^(+N6IXltR?|WArj56bY$@pzDIB4<{KV1pJS5RhUXt}hNu0}G%v>P88u_L^0(lJ zg@>hEA6wio0xI2~+B~-!^EP4rZC04hr+*nK-+bv`+&$9P*SGU?_MX`W=G9UMzlEDY z&;bT^K4Zt;od>q|GASFE9FHZ@s$Zb;FjhcR`5s%V|wa#h}hq;fQa7<{4K9HC?e-e(qUvKYG0Vd-~y zNh$u(jqTdDwr^Q-@d|>QYn$6dn0Pxfz?_9FiGyVtI)OIaW;j7#fBl3!CDEw)0#!iB zIkfpL3?X#~7I9KiNp~Bq_NM<>xbSw<-m~oHXiAGu6hGY~u%Pucrmw+b_Va)n2f{EK(RaV8fv4<+FC#Q{Gk;#UxsF$e2>{KVl{V3>d zI5vLzzF&beX3R+4AE%AfJ{AbhAD<*~=hx6fuEigF9xdsu{;)BIw>_19m8+vc%X^Id zQo?&xv72G3xN|M$C!{ri= zy?lAz5hlU!{QPq!$>w`HvCQ(JBJ6UARd!$!Vou3jDolo%7&>{kMCdjya8Q+O zxc3@8n6mQ5Uf;J{Asnpn$0luYHl-xD&3VM2xo&~2iglvSPs!FjSG)Bh^vmF)E1>|y z6;Dd6%~;<`B0EI9>&cr4;-!MHMqqs15w;xgFg!}%H@M&E(jUHxMZniOV(IVI0{q72 z^N;)q1+{0E3WKE8jxC$k=ASBW&M}2=ha-+-9v#=9XI$p4%M`PBt$ZS^f-|AW6K~P- zEk^4XAh>-^ssLTmuDbG0WCaA}p%m03bwe8Vt28?)n=&7kFPUq>&?^8B&SbR|d==dO z(=)1|hDC88lp_BdzWvkTnXgU_dHm$ia$e_Cx$#*}0;Luacb5A##Sw7O%o)6r=%&8? zOW4j#e%~UyLWeARd?@R-akr21yzu*Xh~}s0w}29^OXgBXinXtHO>#ycHZ1dPodbLy z3vc-oYKi?~cTv69Zy=Mhp@g0dHOAmqmrQ~bMG=3xw}~ilB?cV2aMsG@d5Im$Vlt+2{02dg45K5e$Ui7{X=b>I zxxf$;0Lt}J4dq3C84)5newQ)8k_pkYKhof`U|}xn zAZ?sggNNpY?xThIAQ2FJ$3MT^eOcTn4$>U@kSNhJ(J1}RgLS8FS#`dQ@{!tB*3-1& zYLV|1@@DaS1E1M^b+-c3g=W6@_bm8KVy?7bv<-L9vn75nIps~?`p2<{?oS&emuXj8 z!NY$pY~PGtbPnVP-OJ}(+(H;Ad zmS!O1b=sVY4SmJ>+mkimI^b3eDh#R-$`n5Ea8NZU-$xS4&mjxgzi5aa_^6HadKKzRTyZn(eXzIJ3D%lkeQ%AgGsP zqeIG6VU6l_u}m4~uoeJf(lM4oeT!>sf*SQVwE<)B+n|pQn^q5P0a(9IO0{O~X>QIh+J922+nARN+Q;vPd;t%D(R0PmGyi!?Q*Q)y-|4*&%@qPhu{I|qQPNHd~Bvg8M@?D$0=<|}0 z7B!V1f1K{68{w93-EMB*+%Os#5S#4UOZ^QF4w;hwDI2RO1qf1j*(62X+g|zR`Qa~` zbYmmcUskrW|7-_@Q*>*wSs}F*7K;D(`RyA23ZukuHjid;HC>@C!bSfLFP~#2s{H<8 z(61(X>TOvl8zo7}F52FcXp$0sdC!?f4Al{|Xmg)~=H`QlYw#QWW*fa+P2G5Xgz`sY zk&u;3b9=R|?g;yjj=+M`8qQX|5}#9DFGW$}IEP*Cw^NGdlR;w!`=VuD=Z&a$+!5r*u3$tZWa$=WNO%if7!vXrt97 zS%O1e@*tNvrHG1e0o?M!xkTITTilTbAogsBDJ`ExB*5-JPev@r15MY8e5XAhOj{s6#qxTVfFEW9(TUhe`Nj%CD8V3}0v4Ex)I zAtvlS)V}A9fUdHd(PbaR?5bE43!bY;GQ4NoyCde*Gosu6mxm1{Tds4*2@#ItQk zH44rx9Kv-TlXu);1yYh{;`E1JeO{{eBe8IJe68f3#>n+@ZkA{Fi55qEch7DX-D=jG zgy!*{+Zdle(x6_1>^v>F%)dO()ssWGFqW`rDOqJf6J!%4;ehBj4#-T#&u^tGl#Ccx zQib5}clLAu@DsCy{1W)bzi2ua?~XvjA9Gn6*IbleB4a9XM`Bt=7YTvj1 zZuTXi=e*vBLrh7%udk|&HZpbYEKNsFxv8Oc=AX_MK=&4<417|A^rW^0U}9y+9q_M2 zK?lC3!Fg*H(F(rDjPHV0QiK#pad{`SP$H8}e!3ud;bqNGRvP-TX;L;(leyq+SO;#@ ziE~BSE;)VPQgQzJx2ymzJLLPug}Uw{#H}V@o5poQS)LqtVcC?VgS*P1H6`f<>s|@W zNUBFpB;S71Uk#eV^z7DmKQGEgKI_I=h)eL{igUO~4h!n>m_QWG%}V#{D7^0F-zDtI z56K=hT43waG$$JDF?~O>L)BH9^(8n|$;o>NnozQe2@i!g=r3D!gF{0Mgw^jX$ap;N zZN+X(YJP-q7DT-(l&62huhtmr!2h}0{k8Yy5U1^87r#`>iQ+^v@Z&~D z*FaR(m86h) z57FG`8|`=%)7a$R!*>ntQuybhV%*|q0@1*9m=Hr9)C#r_5Iiv(B}&q^h6sl(f3ui- z2-)82@Z#ZW!b{e~Mep1Z<(LhkVDEKT*D@YT`Hl>~pFjOwza136$2IuX7HAyF)swOA zlTuh~$1|chrHIn;dl)k}7bHwUOuQ{>k)neVeqnVX9F7A;>*Y~Ch>KB|?yUm9%%&4Q;)k>HyU`gC> z6!d_w^}Y4Br`-Ten7P`YKv-|+2O~lq`uD30ia@%fnIo6cpm4qB{kI}=tMy=N4Bs+m z%vQ&EnZJD@Z3Xy}P5e{X$K8u?9piO=8%KUc_vAI#ksK>|p#rlWt5x-(fB66WW*HcC?VOn$X8e|5?6eURAO zA+~|Kg$!eysSFpV=FiR}sK?Z1KijEkpW-2=dw##VRDWRATiJgd?A+a(O}y=I8Ean0 zuRGUzGj#+i7hDaP|gxX^|S(wme-f{(LU?JL#MI<=adDh7*l5XF8@nxezT3 z+h|J0xmW%~bNzgd_k;cV{F$&9xC3A}tOR~&z)5E}ewFp54tq2I+pBQ@_2{h7Da8a+ zcv;KL^mO&0V=S#WA?xGXgPf2#s9?`0Io=<-jCZ_ERN{NZhHv{pqBI`)_(r}v$G^aB z+UA>x=b8ZSzlE0SE7eb5^Lh6(oXQ#f8A@Hx{$b0G%Z3)WaMol4Z9f1GZ!)}K-Fa}~ zAL$Q~|1cVB)6BFZ7h_K{ZC1$~yJ1`xnYr@V%jUahUj{92KeP^`c}YQq*~>^|SZE;c$tWg~F{s;MS%l_C zW3Mnwfg=VeFv?|5(q9PIRMQ%75MDqYHBGD_1g#@#q5fAGRVQ7@u54vA{aQ3%m%B6Q=~@8yGV`-wnjMaf+2f309!EI|QeN7Kh*jr$~_mcWG&n zLLsI>zuPM*4pb_?7c5Ck~zmrGMR&n`Fx-E zeLs({veeS&kb>X{=X7ok8bbdIgOKA>;m2*lL-^CRo)|wR&U8r!8hj zVLrQPdSS>6Vwuwf_cciXmElU=jwZ$UYHh(JqxmkKLPmGk-AULk0XCuM0~c+f|x>JL*t*-edL;00Hyvf*lw3Z#5Y|;a`~&Q3J)b-5^EQ%a+sGaobb}}^GxXI4YC-hfBrsGVKBAL zj&3e#R4niAHjWl8E%#-pd9&{Yg74=%RRIF5IJsgedbnttQ7I7q`$+ory*i&zIZuE@ zhNDw2{8M<}Q4I4rT%1XjXwh6Bu_jFQ$M8Y$IHBEo-5GUQngXIAW#m(Q5^T48(J3r- zf^xL0H`ndfYW`U(9Y1bTc)wk+EJJgYoumb|-CVCMrh)wR$+?w#AYCM=<{3jL@wlo- zS6#4VSgOJZ80ZGF_p#gtjxAgNDJL%4?eO-pCMTFru8)+>7x2e8wYKF^>Ymy2T{qLQ z!Nxn=wO-;Ro2RW%Adgyd1+;GU4G*xCy3|e0e7-A~DS*bUKX=baZszR7V$&;mzVK)` z_p`UFrrhW3Sz<#WT!(Pvp`ma#p)OW$8#z+rD3+fpEtC?%#SCOK8(CxZkK^+#L2`2c zoh3zGvBsp6L3^$G$z*938^Y6RG{Y&0X+-DY3 z3Gsfc&EnGY8bhI>6`M-9bE2(WRHxEJ0Felx`PMr!ddQs_`DUp}ArS7lzx8_)27=}4 z^f_4#*ie-gpr|)akNsc?oi`Ch8GXuNVVQAmQ8e;`Rg_7Vm`o9A4jZ-RV^1+?0quLR zV~v2|tP>p(u??>vAYpjx^ej8}LmO8ORoOclCp2~+`pI=_1JS?8MA|#oS!#o|6fvd9 z$Hk3hhL*G#kG`wvq{Iv?AH%e`h_{u4s>Ql_dSSSw+U9BP(qO@e`thHWpEfinqkdPHv{B$Z7(m`>R_R57o{1j$`s!Kf8k7k*eQ>eZK-& zJ=0A#jjVVQsQCt>?a6X1{BfCIiE`9-l(CvMIVS1OoLolg$9%_x5I{*V1&!oJcg+}& zg}1ZQcTKV(ncv*V!N|C6T*dE*nHP+SsYNwq40BcP?Nqh2HW|T$N1FW!``b5V5akG1 z3y|)}bogb;_k@ZsCQ4RU)x{Dk?T;3jzQ;X;CjI)D6rCB=dSLK(+*fb;mw`h?TASvf zU>Ea;RcYsE1Q8Dl)>mJ+V64TOpjl5I`l%}1R;ov zJXK-K992jncixdEY)AdZ@r9YqTxw2P3L`l;VnQRBVLBk+*M}UMu=@O}zEj7@Unk>f zNd;2cMk>C<>N#a}y|pcKLx)1yD$WAm;wNszP!uyP>uoz5?J++sD@Ci9n9H?hz7`^d zK_W>%v^%&kp9=Hvx^z{1bxtlw{$fdfZjlt`u=@D&XDPm*TdsrV8fhTj93%bKj(qrU z`=&)W6!FD9&I*{|e~-xePdzup{&jkMGx!gIYs0I~tIxOOm>>TT$k;PSu1KR>-0;0^ zcDw(_%>Di+i=ad?<}rA;{|@@*XI=AGR}SZ&-|>MFR~A}^$`lyY$v7K)zC@XyQj8?H zwIK+}!&f$5Q~vAopNO*myAe>?+d6vHf8_5Ew!gqm6=8@fOQ~q(w}LkS-h1k7_aGv# zY+rcpk0VWoD;td?{~`E!ofYgN);#(k>sP=3kDum-S*d7qbGMQ>s9;5l=zjz@^Yh9m z_3DE+)ke$8Hr+ybc;H47d}+~JEn+^jo7TE;6v!=K&qT$@Ojesx?vi0D(pYcU%E^w; z0!QC~8NxLz;tL|7j0KVS9SJc)Z!!!agw^-)9#?o}s3KXJN73EuW^ekQOO12^Svy6; z5S>!lW)UGgKlG`|gJAaBu5O*J08%}1>X);1PZGrpqDWaPHa)ENAVrSw{VTLUb}ZKQ zpvUEu1lh0%>bNlX*Bs?bG7L7@tx9EjI*MNXYe!l4>k`962hrlGP28=p%u-MlHkf54 zf*__rvT!nzS+{h!o|^1+Mf!{mQ6@}JO&l))jjB1?2;%c0yZIiDvU8B3LgO*Ecfu58 zARSuIx5K5I*e${gW$FNudz=f;KZ9rmYdrrBYI0ja|A?UACntDK6jp^kQcXEh)A!-EXn zeMHFh{B&%y-m0bqFEkDMM1PJpAc>!2b}`u)Z}(HE(GTuYho~j$?mZFu!spqTz0Sp@UdunX zSHgW-BTI-`UFmtR*Z>%&Dkdmg;ywV*1N1NDX!x82=SByDRg zp};7eVorE26&q3~`!)Jt?>I`?zbkn6~fK0P62Dz9lxs4Dm zpda$?)>6dC$@HKM25iYteVy*7v9PyH)Sne(34;`zFd8N~o$jzqhW_1>&yU9hS(x)B z4@`OqSL!itkwO<%nM%gWEaQ^oRaVxPzIrsV#Ise5DXH1yLSWq}RiT(2=%)^&Sd6h; zOZ+}G9WIs>SCbTn;%8xMu1yTC=COVbMuLGp^H~fj@VQPmg);IJ3k8f`d8DxU8+raZ zUMe+)d~qdnP0Pn2ZP=cI`>7Xh4D79WDo5u+Iy>ZL=h>>~NEk?!8l)BiCyi>VUh+ke3Tp^!FE{SX%J2 z&2poEwoHAi^U6qw@LSI|UWd(5$ES~Z5T5cn4YH<<<^3AUs1X^JoobKWfb+i25%`|M zVfzn3M8QpCgi)LOJ5sK{wh^-ou6wed6=uT`*9YoP-%Kjwr^qGQA-$E?)w#*v%Sb(Q zugmP4&OjeoA)00JHumaoS!D4g=u8>-6qkBUA_KKsG$Z^=$u-s3@H?3aBOAZ4KORjZ zcHO5?MoU$!0}WHX0yDnuj)zL~Y0>!oL%?YJh_M~tQLlgO2=r4$Tb%AwXID}lhci8D{nan`N1x7ptIO5;HMIz0b4A~hij(bM$YM`2c!(Qv z4KLQz{@(`Vzu%6OMlGM2A)YUMSTYZ(JRW<85)ZPPyjQAcMh8Ifm>ALx;WgNZ;Kw2^ zXBnHn23q+uwFjCXhfLz$LEb-&Sjgh6_PW9^65y-%O_QXdvqO=>YIpG4g7sr!n!MV; z8an$voFU3;vp|=DaShJ82%Alng6R?vMa#O0H3DBPEOC(}&poXLB5Q5~$l+Gv9_Gm9+>NKAC!+%ls+j zz*Vf-q#G%seD99_o)(_YX^>Rftks-1+c}$Jn4q{Dm~6u0*lVOWi4zDbq?>*-2^%vP zy$>t-h7uyM6sxwIaA)D~*z2()4aC|=yV>2+-#1g!O_SXs6npn}TwRz9-{l3~k}jg0 zp2PRn0I3D#chEX_7U(VOMes71%6xqhm?R^c72dryTZisDwfIR{9w-R&E=F2C&uZ$m zuZ$7y6Lrqk*0WgUu&|*+`1xj?%{C3$6_?$Em?v@%M^t=I=QbnfgL;P>)=ZGm4;QtU9>t^j3M_5|cmS-7h zn8lE~s7>2SQmUb64KK^+E)6LC?SZIpFN#O)ZY#c0Ezp$#s2D{ahnsM5g1U@*4fI}; z>)7Q=t;2o!_05Jgp;4YHurWn7cMEZ=dqjjylOj+_7`;7@yK|Ib{stU$@fGW6UkQ@r zxSrPgwK`t=YKp--5B-EjZbSpNTY$9B{|tq9@(zx!((Sfv+l>|=6S}J=49Yt7=DNGb zNzANi1BpJ7koZ=w;#)p1X=VF2+B$AKo9liZ#dOASO~XqLzixGs$ryB`@TXz(&R`_c zw{ZwU{1q*;*Y1LK2WT#igcH5hfK8(!kB!MR7HZ!rt5?zSU^*SqO5Sa=6B~ZY(DgN` zA%wt28MS&%-0SS%`a*k2P3CNz9PH_RKHcD#BXJDpYE=HglpN-4reYHj3BQy$$rl09 zDpS0J>@xPGNvi`yT|5)jQ4tWg;WM_~q;4La9%|g|v$~ATy$q`lUe*niQd>Z0Q8kj` zXxLo*h?YFDu{NtHY1VAn+?i|c@z8LzT7zvt$0$~x{X#>5ZC=&n#+@D7k=V1jofu&Z z!qSEB+7qW57TC<4f*TL&<*`VAU6o=ws6Ry zUd=qtm_<9RJG})`ljKg1DFB}xzcL;G`cPH5606MHF5BtcIWQ%CP^(7`%N^RUaD?X+ z3#57A(_Je^UUCSkK}P+gvMf*J+LVuS`?~q1nl8;&Ju@_)Pq+U4mP}}#{vd1Z7J=&2 zo6ItntG^6{4YlHUJGb8nF%Ct7k3$pFzR?SyZT#D zSe5P^sLd?#t&B(u!n};8^afhq5yu13H~AsY>EM*w>VQoG;T1cc)R=CDzR@oWIqwb| zhKoa`1snTl9PR^zc6ypDJ;Q$%ZMTMs#c1wD^}#3K z^%`(MBsqINumCmI3TP~I`i2$IxglS^G}XFR==_eh8N8(oIn z)Ffbp5+cCfaNZ|A{wr^K$FHiwG_{&i)+h_pvU_~Ugc-XT!rj@0;^|-D{VuK^1d~?Y z@!o7v#|G|HgyNeg@mY`*#1xs`-w$?OyD0bcC|pQ)p! z0^#b!p$uQ|oC>Xhc`jRZ_soYrCvO()P=-=bUm{h0W*vXJViWV!)!HsApFll^`H_>rl{&F9c1eY@8HJrZ{x-w4pznawkIBfUoxCE0{ve&z56flcI8+{uTdYkYERqx!wt1G|S9 z#J{SxkN5ti?8>9Jf=o1Z{`(B*fBM-2Lff%37eXvyWcqlt?q{>}U;ATF^6#bJKmN7{ zts@GgEt3T1v%HrayQnJ#@ba?Af^vC&Yn@1kkk6Ja2v1$UrW``m)yoO{oa05_f6OEL zkN8OcyB;I#`JzC~uI+gsjw8Oj0`9F`2HjJq`NTBCz)7z?@wVN~0W#%#S?%!HF5ttn zb5z2~gST2-Z=Z;`jhk4<3=2_7sJ#FN328NCu8r2vfQR@1=oi(I2aZ4U`~N=sYHji6 z%a`NvmF6W_H9hX<1uJ zf9-@V}a_7m_vQD(;>XF4WITw)jq%J+FS(?%ddZ7!yVpvYB&6z%Go) zpc5cUM)opJ?z#W8Ji7@z_u+&3I$W@#>+9`R+553jIAFRiQl0 zxVX@g)?oCFuXHp2BhUGTuE$QRCH~ga^v~cB%H5JeFOn#Do^c0%u&WBC)!?0I!**w( zJRkmF^A%Dra(u;aY)FXHg>%|4i~Mb_sgn9SkM!R3H^|(%^=JZYckxtttjHIT>7pPW zZr){qraSev<0__2qj`5gj-N9Z?Z6XR!s5C|<{I4LgOp%qo2i@Namp&xp~k0G=q{2l z9-ndlSGPDMqiDt{eSZdcjA$OB36h9Q8^yYOeB2s+0=E+9&D{$LLlT2&uN(s{T=VTl zj4hB_`#-MgvS0Hb1qPMTc=&tLnQgQd%P79Q2CB41mbuabo`l2v@%2D7okc>aUX|EX z(T%f)U!YiB5# zb(aDlV(9a0-pyvelOVjmznHnLdFM_zhMzR404(W(wM-z)f@Ar^cc*~7-^sAr`TlU{ zd+T|97tKSS&QjZgXK+mX=6b&Vkf+UtYxjUzx={K3`fA#u2s42-nX#uUI!e@()3W zWSNV%w=U<3OIE#QDj=nmxym1KQg-hNC(k*Or25nE8bvfyaw1gk&(Fy@isGr#NmYoP zN!5c$bQXU6wDLJQ7it}crq<9LsAFm5cfU=<=rhlY5%1RB#suxv%iU+68tH3S4|ETt zuJqu75RxP>_&h%p9?rTs66o4j!-`=seR5k<80kghxJ?ndKc6E!?i#ASL2jPjl0;9Nrrt{8 zN=7b5r&bD1vK-vF{MH63TCHbDDk!3p9xrVLp09fuT-UXuiPo*vD&Ft1dVi7aWm>m4 zTfjacEp8~R&MQzY*cFg=pOtzjD}W<=h~TO+Us1jKQQLOk#V~)FAjrH&GzzBpc1%CG z!E1!9WfcrrX+1Sr$J)%D#A|)k&u0KBTRN+Ax6|*9J7njcYt$+*v~ZW?h-=p7iuuaF zr|}4{Xy!^Dx~efoZwe`7V@=_UT!*Zcm{mjr zGAnvIs1;B*bCM=l5bq~^n}MR<`O?nM`BpD(0Gkm%ya#XfXq6lz3d{A|8lB zZyD4|Boh0h<5!;u^zp&%zf8N$<}Gs*O|Jj6ru%nIeC)JLJ+HlbUPL{>e~sO>=fV9Q z4kAPCiASY-xAUrc62j-rGk=}%71!%adqCvAP{C7Og&e&0+5`U2OalC$_#U8WJ2$ho z-!ggBUgzR(C}sz?9?h$%@%Doo;H{_6nv*-C?KwQ(;~=YtrCeR zELSh81N)d}$Vr~3|3T@T_z<*l<@3Gn ztW%<1V%2g1vETjr^*gRY_QU92e5h!{a6Kac`?ziE-IMQcf%%KEs*p|Nak!{*E_NBK z%Rb8Anw$jBRm?VVvRl+0Wst2#R(&K~$X*wyXQay2=$nu0p%mddGY&;vIP8_9bno9Y>rKX(@uYgz%+_nXd2ZI!jd$6&yLc_l7+DBVavkc%_k@4b<*3X!+wxMt za>5YY=|v_^^gh+Owc~SCGfwnUwcpM3-QYC*@o#t2;Skv&FH`0}O9*Ld3->$#Md|L5 z4i(L4^=45W<+PP4;TuyrLs21lN;MgVGSgg!@;EN3E(CXpm02hy1+yl<(62ZyCmn>0 z4}@|PK`qqqF+(RhRah=FR||FQA(%dDj1^~s8G^WyKQO?BsP*v(w$2llT57%rRrK$f z0F1(0>(RF;^xEC*&D&(yWL5}>5il;j9uo(R_5wb=Y+FKmL%*umIN3GzzD)j(GIdJC z8kG01n)_N;;CFguy6j9bWSmgis){J>1~i{yEn|MPNbhBVxa<~VZ9n0&(4J))$%aQ= zHE4+_&K^xZ4h7EINM(xN5VbWIQUsHCaTqFrJMu2Ho4(iO8yG1XlO1;7{HW)p943oW zA|wfk#g6PYH!>UfF`M}YGr|b~DEva3#~&fB>TE`$56jamoUq`pm_L_&$^f^m(_?za z1&^2^qqF-hbDT{Lu0{(BD$7)ePVZ!L;R+A243umN4P%0hYRbcT{gPG3xn$$XSrMxN6*|nZ zo2nC5(>N(k=jU%j&+83!9ad;?ICRSynbK3UZhp|877T4apRF9OcUwE zb>PUF^)GraS!-QHKnu5gmMiCTMY!Tfn1#{Yt(2Qdp7%Drc1(8M;%>P**oGg|8wwJa z|I*Hlf9xP!{Ju!c2lTlSC^q&L@98-Rwyw}B)c7w|qM`BHDY(Ovs@A z7DJO>6Mrk%0(&K7vjH6$yGN{u`k9m<{!s!DAbaWjpc{X`0cMwacbl#edA3-jj;%>@ z7Fkm4M#7EoE2ycH76#EY3=<&fdmi6uXAle8*sglR3NIi1*PiHccv2pj=3-KBI9bIR zm98>;8dWORSd(lYFO#Y?ghp7(S07@kt)E&NTK=*omdAD!f0Cp3@g{kT1Z2I`BaXIt z@{{@D@%k`8>+j*6Rf%Nt=-9hxV9HcM+ZO&e<%z5n{2b-UdioCm2+j^8`?=d7QL^FJ zR7B)KJnbW^&~CoMOMKoCYS!XSDVIV4O*2~9Yd5}7#MtyMhy^nzI8)J?M{SGsY4cUB zGP1ConXP-4`tJG2n74Z?@UQ^FfCcPh8ojhOGlb6JDtMpojeVBMi1qD>K7-d+pYRFWFi6Dnv~4k;P>3OT&E&O!5FEd6ey<2*ZnC^P%}a z=R^AJuB#)!^)*~J%T6DtCUq1?m+s8<$)5@r?xbEgOuw^YX+dG5s) ze|-HHH-DZflANaB1D>R@xo5BS?jR5_7Amzqv=tAK%!u?%XoBx5z6$WS6^U48oArXQ z-{JgTwL%*)ocgWg8Y2GZ&_|hZh4TdtA7=-t*p8TXroX^F}$#8su?f;_#D$cUK!QC3Z2P2e>`o1!-HPX z2|lyv=9@3-sK2BxSD;p&kNDi#9sieK%fbJW>#uXGZyinN6o-w38SY_3u#k12wv$#Q zU$-7i@c+w2rggE``H35^Ewve>>q1&M#M%}*|1P;oiHP(Rvp6(P_MpmzAMe#0$PL#y73zCM!Zy{@g(_HpSqe@W_6;c_G2LvI@({{Cbeq5ZWU)szUq3#@Gl(&O zOda0cc30DVfVhDG-Or3q@rh2+7|PU@4l5^wij#POB-@R+ouFAAG)zQ}kY%Fs`+_!! zMIWA;E{$u(J>{y6W!Q$NdhSm<;-cAFoKY-Vr|qQJ!)|4eDTFyDP={u|>p_?-3%=g7 z;hjhAseO_#h>ySWk;2V}-yT>q$=mrsnhrgZ-m1(v%Bfr$-j@>*ea2gS2ik zXHt^M91tr=t~kP&0+7B)J*g>Tfa%D3dJ-2ZA5qBUU*XaVsTryCfi^pyyo)e%>sphf zVI&B?_=g}&*+REH^r0ZTa7eKMpYCq6Pn8aZnC2S?kWT9hTxcHIvv(M$!P>GO@^-rq z-PT(Z=Et1fg5OToEyU_S?zGOfa(9RJT#TYE;V`R)VHLQO^ZG`8i-b@FtQ0iQrZ&*K z2<+~>J^M}zawG3bMqZ|>Cz)navi)Z!KUcNy6r8oucew&6#pU7izHUZ(Z*I<`QUb{LseH#Oq%3m%1-!p~5b{d9tVE{uTHP_CKpvM77Yl zglkR8Fzj>4PZ);sM8V5k9v%e=Z60N?)As0udM{?oqV&UfyuWq>#9wC1f0XKP#322Yb`4=!{88~QJ^qvk$zU91VA z@}%Np{bWzf{_tx`;iN}zUig2gl3ELHdg$)xDdqY?B-c-QKHq1-(EMC;T+^@6oCZfa zmN|Me`lc=%;O|p#Pp~dN@A%>kzLCT<3NFeGcH%x2fDhqZg+2o+1zm54*8!!06!Ypj zN~vwfoUJ$m4@IQ7GsyVmFms(L>#T3<;|;WyWh+7Rml?+g%5p^r0d-r42j@?zhlU#g zr7BM@Mw+%W>}Cqu&>3A@a5UvGYP<$fwn28A226{jpF}7-SIq?b2YN(xl9(%Dk}rux z1$NkqOn0c*+SidC={?QdE$&?*+45Bo5K6;UuJQXY0Q_7{*<_57O~PiAi%tRKJ|50j zGzj4}OI%qqH$Cys-Ja)V6z^#A*}OBK;IB3(N8s!OAjxFt8x3t1{ucFNy>z{;@`|r6<`vo_!29ct-l#{OCDl37SbRH9aog_&*cj!4 za7??i;`xBA9a#3|AUxf0Oc)VL88M#|gcnHn*57{lHx8Gf*v@ zshF+qeVnCACrJ>Hv-`EpTO!TlrU~y`SmTR8zVTN(&4%X3cagZl1gQZVi;nPH^Rb_c$`u|xspSEvsx;v7 zPp0_yScGg~nKWPvII2{Hco8`1){@9nOIB_hbdCPYJ#%_>y8)h@IJet8R5p(ZN7n!) zEwJq=BG>Pdhx642FqXg5w?iDU-$}j1aj_f zAabLm?M;2a=anVJbu<2V%*^~P5mZGRPH;E;nwb9`d7v3hz}j~P%{4_VDbmt6&|0kS zNd#+LyQV5@kBi~g3Gad&TNb2g@O6XN29sAjan>=WmcOq#DfAgyXVJ^NJ-vt` zx3Mdq@B)4zu2&Mt5Qy00egC~G)`VidcIU23mAo?Oq34uMzfzlH0JLod2UUP7KFvSW zsEufsD8%#L?lsUqyBDXTs-dE%{En@BB7$g5gw3sVx3S)@<1c@LyqKB(<_Ms&t~&>o&-FjdV>}SYH>gb zCQ6Tunu_LF;GNu@5dNK=OvK$6l$9LnyN#MEp5JSm?4A^M1BYJfgWj`sKEAs77evs{ zN_Xt4L1NQmIQ|5W`#Z2|S1NcNjgtN^Igap!KwrNW>Mwr%p#T~NJbsj{!57N- z4*|4;mW4EL*Df$)Uxchf7G1yZ7&0y)Qfj1EQ@iq~By#-w!;IrdkqC5wP^F;qBd{4h zYq;ot`#9kLKP&D5${Js15}Zc3JG^{z4^)u;yDn^3PFC3#A1h<|O-yCat~L?lJ_zJt zRM1bbtEbhWT5amrCT+s0*Ns2s&e}dJCO3Ui{5AFh?@J$9o0-{7bLnDMw15nV|30_! z|GeBcM9rz26>gfRB78Nk&5Ya54n0JdFL6P#J6(0ag4Tulr$VxX4XtoU{%I{wx^oVK zV48Gk7Y$LsIR8P2$$pOBh>~aBri?x#IJ2sMU2;vl&UmKA&MVK?W04UQ0hTN9lXF6w zGAEy1%<&b=rLIN+mm%MMx(M)T>}pN~z~BXg1VV)}Eu7^svs=4MiQ*y&Y}OVF?W-kwDs*;(x5mUUp;%xJHFMSnys&RL2=J{cb1DMk7Z zfz@NBi<*UPoep21N&Bt((BfoaCg-@y(kYDdh*wC4a7*? z9&0a@-0mTeP`Mn-IknI?X|EAI2oCCbv-93k0Wr-{8eFgSr3hQ|=AyGx#pwCi|c?dGq9fdSxn;Ynqd1wZcIo~+f~23i(7Wv4_yvu9&l)V;Mu zxY$VG#X@mv<&8V_9!!+Hvg)8HW)HqNw<|3P{?)#h*r?4zW)>ylvxv$4N<>6VDN61u zTOshkBNrdk9q9?n!%SDkxv`Z0)@DmgUo&y4ta@=H)`Q-FP8@Wqiokv8G`|O8O?nn9 z6MO_>^)PD(l?(PpdGO$&igWQZn=Q_TOx>rk82i43&CGj*{~9m%9IRJfixa@J@Mebt zV)^^FJ@K}A$#qlDH!iJ1Q6?o_*%5m7!@*$p2XSwOhlz3;ioN>qs{wJ5{2m<5uZ;UH z_|JfjD?Q~ZU}mf<{Pi_dxWJYfX!bIlZDbjcS9_c1+jBokRj|qK(BVooKk;r@opm;n zH&eJFN13;rS8=%Bc3%9xpT(K^Q?dO}A@;?Ir5ev2zCx_lq^d|!9yabJhSz+?A+Oz6 za(gqHeRvRSQff>m^~JJbJua8_x#AR9swah0i}Rj}F{v2$!B%$dZbudUNZbv4w{0~$bvyRRzUtj|U&nx? zv?f8|RM4~>4RrrI9mN7@Fxd4-Zkw$!0^599zjxQ_q{>m1*sNIm<0*G@@B<6RZU<;l z-cX2fb(?tW-QwNHJPdWDG(Zc^$UQQ&?WXPKyH;b2h`k7NDgmO|1+cunkQ47c`cjNp zgVv@*z?{P;Vu7rREpvj;tiNXU+3omt>`0YUBKo8&yEEcbrgYA$(zV+-{BKB!Je$ z4*N;kM_dL2(F_o)i%P!&@b!ILUwuR{DM&0}xWJ*G*4q5V-vYQg6mw2=#a|Xb|8;@o zAA+|)cZ(L!?)hOT0j{Y_+7@{Rt3s+-zR$2;`?q)1GQ3C;M!h~-G5d)Z`B z@>}n}w)5)k!hx8*CHP(-}Ey6!~ zobK?{2cC~n0IEn1`^5-?S6KOQd+raBu1!Kmu(2OakfqG9ziUQvhJkkJ zn&FMvt>-k}Qgm!ilo>^_*1hlv(8g2lzpD`&5M930RbiH`#VHiG*5o9~@fox;;m5F$ za@zhN)_3jP?qtH3>NasGyqY|aHV=?DroP#&3Vql%+I53G20n42liIDs?O83BT=k1SDmurw%OREHAMhCvf>W&oH6x43w#_(;m z$Cz{`M4P21amuEQ$2k!Eq{uma#@i?1%#`XwMEQEp!DichjY`E%cBfTqdCJ4&tKDOk|hOjGJ9H(#Ie9u^-eYKJwM89oemZlWoe1 zqBY6oE}DFqg-0avtQ`73soZfUL?r%B{5?2Za>@H$SMJdPR=8z4_r+q&RZ`e-MK^&j zC+m^V?nM@7P@~Yd(BumH+?^ik;D(j&b}$beFdOXKIlY_c)B;IFlSe6FH)8KCqN6B3 zJ^PxdmnTSbz$q4@rH|!bOD!jHZ#9 ztIP|mfqSq(e(6SCDYsX!Y=zJ}5TnP-p@yr!*ZA4h`COjtP}!4~2sq+Y+(lo|P~JRd zR$8W~<#m|iC=&dmaSY^vM3PIrT&>-Q#D#pXWjqUwcwIyzpvk$%<|?w0b)pekySiIz zBkd_5kH0-t^eIQ+N>@li>$Xi9^S{gsL_RITDOKLRegI8=v0&zdxHFVQu2`{>#BqHe zDz3!Z(1gzvFWO8ZS4czZ9zq%WLQf&QZvm{@SjM~lzYL18g6&iF)^TLQFA0=P9QOpN z?B5->-Q0S`cp9o_V5GdqsW-E@~Z!k{+P*f;>_+9h!R_)p+sK_EcRu z_-2@ACFi5b@D1@6ZL7tMxb!9l%AMM3QF5R++g82HfS(L(Ce^YlF=gdeyHDcjq zs~Jz>ZkeTgWgbsG=`G4wKLb1~y*965(9E(1{e2G|jFS4OX=7>}jOOj}t|YESwQ93| zuiY+gPM#Oi?~EX5@l`1yY|!3h|6E$8)_o9E^A7=A#JZ%;+HQ9m`;(dd^sKoP;m79- zWgNq$c)Irl-eN8z`Y$}lF+7^WBQ?8b8MYJ1TI;(5c;ZOgqN-)v>Id#;Vk2}P0wP#f zl4oBwy16TSHaGjXx;wQ}ET}#Oycj?+^UC$t4|eUwuU6Lpu2B&HOQ1*`gNd>6|0H+( zzXW{zU-dh|ak&U5Du(6f`wx23j{a@9TzntIUx!`oWj-Cwe?xKS>szg#3p|xtJM4N* zhcwo$A2OHXc-!n3=MHS<)Jd-hr2)BbvUI=N;NG}_7ltYXPaqxvr)ng=PUU; zho?L2wM!4$30!raUjgkBDfT`6`fJ(+mhneO%?i=FfL+>MR@xQ-?5N;7?`nzJZku$7 z{^d#~#ZcyFB5nA)8tF8usJ&fqR=Qk7u zbnl9$7@QXZ@}-@&2gtK#$E8g`5qW2Sw$#||C)-Zr7$J1)sCqOp=2wdA?jqZuu57rl5T8Et5;MYHSu3gm3my^<$Sa(MB#nFBPU;$NoX+RU zio(=|hi#6}oje-DL(BPm_D?-!04XC8FryWV9~)(ZBQD;mnC!Gt$LjT4!!3@XSt)z4 zRM((GNV5T;yQ1H|lZyE^RY+y}!g~q;4w24ig=I1`Z3 z(Y$Vh@$gRVpoPxYEJbG!BUN5zK-z97BJ&bk5X#zM%j2!#=o~=9NL*n?vhY>^ZcawX zIF;Aafm#<`vmdF#Eb~d*Mbl&@SwUXA^u(~!wv$)kPf(ejSo$Y^q(1vUXRYnHNAb+k z-SURDgL8Dc78Ls-!Qq#Ya+--9+%2A*{VI_KBHJN{6MyF zfE#HBzkFw>v*ZeGEek95+S7Ti$T>}3yH+aoDB>fI|Cfqu8m02WX^Exn8|J0EA_5p2 z&+3x%#*JYcQ7$;mgyr+}{>>gYxN_I~iBHabje~in5Q8HTj!4hSmIay651|ZJG`8FJ zqB&{hC~zKy=WqD+ickfLn3tob<|DCXSSPwl4eWI+%ofEa!;NAQ< zpPZ;b9524wSsVMugQ41icBeJfqr|5IF&<3?uKeCl)bKc%?smDOY=$0Cv+!LJhfO}K zzfcmF{T3;l`D0>W&F1Z_;2Z>%3;xwO;Gl@wkkU@E6p$s14Gx&Kbqq%R3_i{s>pzF5 zu3tt$e3(S#&ea%m{Vqf@ZY#@hq&U$^&D^Gz zrDkPT-^2HJe$P42b6w}ZXIy9fAvd>h<9*!-pZELyTDa<(hIdk<{U(IXWY05rSuZjm zl9203lo#Ln)_N36#k!p@-*IngHlrW_@pq#vq-@6}*&ea5ArLm9YmJ|60oXdlAT3u} zpm!V3LpjE?kf+B!POx7H+wQ}<1zv7H5)T0``j$9&o+%m_od7&^Y`{!Bje8x7NxLnt z#q#jn>)@_`OYO*4&vD11v?>LhP?v2-r7q==rI;eBX)E;u zFO4_0;IFQ~@1g?CCmX^GG8~NTI^GLaUtqe^@l2}0dQ0I|8jOYKrk9J*22s8k&QAsC z>IhbT4y;c(s*5@9D&rJyVHC-CT7t{ilO5SA8Pc^AY>z~JTAlGHYymwR$+m3U6e_Bg_?xd!5aH*I?Tc~rSi;G}&{EyFY zjp;n^JfvM(G-NjF_qHZy*jFCza6Jf*OOQgOO%7Zm!fm#s>jtKrCO1pe_~3t7Hhel> zs`g4E>M=wkpXHQI4^y+o%ZWqKqt0`4)ydASdiTT7SxVredSUJ1z&qsU(+l=#*o)ea zR#NkzSAU0OX4zG-LbFCh{ne>a^zu_-As2Y3glSS~D`GRUahYT45T$Bsql~V(v61HE5rl7NR19 zec5FWkRnSUtvpRV?5z+?j52G5pJPhVx7*@7Ml<7BSi#yj!n3PNNz^8)`iZ*-ETauq zK8QsqHy98$OAL}~W&02~j9lk^&M7g z^?eKx9ral(%`W}w9++%H5KNnay`0j_vbY^$KD;oKwVuVu2#V@EtgYzwTbR78&#`i+ zC|hxk8pfufR1}i$IwY5`R2{yer6NKt;mOam*2m*KsKP@H%L%XaX^R`u|#24;Qs;;MOVtEj1fDU;VpbJ9}0 z$V{KT_H(1YmufJ<4((82clPRdu;Bqw0GRCWGsvlOH=U z`R1WX3&Ej(*U(y_0{8dv+Fy6du}pZULi*+1&4`Bi9Ym-e^RAB4ACP$Or*MrS;vPNW zf$ahT@~lSt-Jf9K=RH7-N+Fe*#J4zw8`q zKQ)%lUfdcoiN7Xt3y-GkCe@d4gxoL%mhFlUnpQP8HmtQt#|odY1d06%Ipjut*y4uh zk3wOn$;;c{4jXT;x_W#Vy|UaszzT}});E9sgPp~8!UHnALH5je!bDUh|}s&v2QF7ImOsrzBY&g%%E{qH>wv~P@j#q zDmLZGfH38y@3ky-*JhQmn@>E`?_VwB&alY@k>6>Cd^+JZ(RH$ko$xIDI+P{S zlo?MVSh%j`lAbTqO4qYbf!>_eyce%c^LLus9CU5m$$s|~>ErdayHgr48~rm!&Kd*Q z3R~|mHEn7zcq?@|zQH3#KK6K^{93-xEKaH?S2>lf{vLCT7VmS}R>I@njIT_}>y{Dm zHI+d|m>#>-HTwPNG6b-H6~yrny+jvy=kJQplf!8}ov@wJD=FN%rz0q=@d78aj*udz zzfIv~XJyq_hA~T=e5AHxYP(XMd8>_s5j3;>_BCN#d3kS+5+Dt7%{@f`T-W`KC{*ip zb+k~x0AvHP&JEWO5dI<@!6s!mTPjPi)1K@Gwf}O6EzuL8*K__d{YBgNrGcV!JQiCXl^J@Y-F;<$*I%fjs!e zk%REhNX=43`4wE3vSZjyH$$#ca5|+NK|;1I*O?~XRN#IX9_}C9*Fft@yl#Bx(XDp@ zmN=ph+;m+|JhSk|y7{wY^o}=K{M}P>RccmJEa;%S$4WtIfcI+Y+1G{A)^F3{OpD;u zkJ!L-1J{tTCAD>#!d>L>lU}m)_%TN%Tu^k6t~^=2l;oGV0fOB`pc1;;TBdt{yx~;$ zvV0KJBK~q;vAvCKI7zvXE~&k=XJ>cH^imGh#IF71yP~3`+kVA_gL*W+5+UVw4RKD~Sr4rD_!cQPIT;i~1Swb4; z#kuBjPp)X8fglJ|PxY&(?gb*{k2NvegSu67IcyKIKKHc zz*=;*R}R0Rgzq~j$F9vv}8-kGB2ZvD#^;p$lt^sN_tI`Sy8=#`52 zt7!<}3opuWbGJx6_GE`aPAv(ygugMd$jU6rx^S0?EkER8=kW{|BVv96_>HRAzL=ZK z#bQ0J*~lv;UI{F66a;i`LY|;Cp%6eR{IEJH2@geMp*#V=A}35JK$ygqosT;cN#eD! z0NCo)u0k>0gl6F~?nv{t1O>XQLb_*q4e$+&4b4NFB8cL(1GB8TbL^#vZ|&CUX=wuB z7nUGN`~$h9%ska7v@H|j6PKbe#?q4G9dgC`4b~&$rIwu_@hVZ{M@4kroV)F{ou^Z}!-2c4T7HT-2@B z#o8=%7v}F^SHPCm!!JKI#Kd;GA~7GneyXrYIN;`<4qKJ$5!ZA_0s?!veq22hZDv&& zKN86|Fre;r3EvB5X`k}y>v~v24MBN-X5Ju&1lklR?Hhe4hr|3bHg8$*&Jf2sF4t|Q z(dRqF9!otj8z>dLXlnFr`;6G0x7Xg2JL7LloY;jQN0pwl5eI)T6qCS=dV6f6AFv$` z)une!dy+<-iF<<`_$pu4ZAh0qzNt5DDl#}06gn~eXINaZ7}4P@3^9iH%$0p|EoBu~ zJP@N>XkJuj$Ptw4E+g7qmcg;`aJZN4qBq(w6Ei!f4e#u-Erouq9Og!Tn?@NjKu(VTv+bWPBEVrQCfuQzY|E`|$FiVw@Vtc}Lh?TXmsT>f z>)t2rd;Q9YTm4aQjISA{zT>Q~7ny5obo<0jzpVDBEyuI_#^4+oZ_N~N2v1L|jn{{MRE-`0!O(iBH=Ax4}HBDjqASeW*%RzpEX zyn3>e@lMXF^l@Vq001!@`)phEgCAcLzmM;O@?!o1oW&+SeLvQ%*z-b#Av=RuSO~i5 z)lSB5&L-&{))m!ap{IWxFf@dZP_^CZbf)leW6HAnAgY?cQlMMLZRxk;aEvsdv#hx& z6DQbm-Fgqn3g_9_wZ{9o)yenmr&$O9>~M~0j&0^Cn$g1mIp#d(m|E7B1lLIIX~$!4 zhVKJwmH=0XR0gKJS7tgtijFH7NX-CL3lvTgTQi18HGDahl0C*U~`lAMGLjbt0 zZ|z4qICfQ=Ksc^kZ?jzmR;KyXA=ltQ-6PDEcjyml=SEuUf^;tzt9D#%ID4veJ9+vb~C~pJUJw-m13Hl3e{F`q?TF~Sfn;>*SQkzq`n$KhFEQg&vZ2SeZ)PmebqjgiM99hg~e5QsR~XF{K%Xd>nktgd2;MN zU8tkF>fExv9eCve9+08EgSQs@T@#p1_b0%MWZywghv#Z@)IEQCu?_q%t_FH^0Oh^@ zSTIx5LT)?TVgb!DL{%5C9q{X!s*LAO&s{e-!&Y#HmvGrB?0Pd&;^GLtz6XCfbEDt{ zmL}D>9rlqxP(}clgR5FMNI4{g?}akXV4}B$isb2BI81>FeSuhkI|J#e3Va9we4N@S zB6v*z(BR-QU_PUPBvs_{1p`2a(S25^q3ubN&P(lhghXy*MfI(dz; zAI&nAKB=JkM(meJ&QpJljgrIqjsk_F`(sDJ%bzZQ35ef6?WBmUk=p zN7z~Z7Xc4Apr5>*>$kAzt173bOiSB#oEz^W6lJ^QC0(=j(_F7zTdFhT5wR@R^mhx= z&#KGvixI7RR!E4$q@HpaPO4^trBtVu8=MB~Lc&1Y&miv((S;qgei4-sb;lpxTTr70 zmTkrO+B|QlGX%?B5kpTYDHexyp6PN9XSnl6>vwDzY(Rt_QIhdsyqI+)BGhHaMdhB` zaP4N4r3L(4OW?y6debN)-=R>f`i7@)%u2~^w9zY#B?-*!#`S%DgkRFg)!GG)6}=sc z6tfM2;j4-=L0B4-3guI%{!s5~&A79tTZGD|8t{l;lvvDa^{P9lmbkD~6f0kf`1#=Av*%>? z4~fySieu}M3W}CzqH3h#gS?-|XAgdd7`NnlT`Y{e>c$w#{6B4R_`kAiWE;!qn;$A% zGq(Qu;7?oNBG_&?nMZu>4?E#WW#UNUj}wP=-4B1=mi=XtN!}#Oj8^WokOZCGMF@qJ zTI>br>-`sW6CofBrnm$VgM zw(JhoJ$2Kg0h>1G+~n#4bRyxlv9-c==i0mU%9OpO!5#&BF}k^=Nk70yS=iF7E?}wp^us)7{`ZX}-AT6K~_U zkkai;nt01cPjo&Jc^SGmEg?>IxIq$(^{8XYz<=VjeEM^U?Gjg#F67(A7r15tT@|MT zwn*oAPd9{u+YTj;CAg%sU7I%d4Cdxti>b<|8qv%x zgSnX=ifQG>_x);yP z)ipX<)l%`)aG5fs*tt|e8sdis_VESNBw3uH?gSYgKFsMO*;?P6Pz{SkEOa>ERSDk+ zJRH@dh_1bly;2v-IJViYTPd(ead;^xr85OE{!A}ZYimn-H zQG=|nq=?ouW!!gq<}S(SB$dCN_{nu?6EvP}e~qagb>nTzkm1ICo?F|@oYLY-2=nfI zt#eKw$6eEXg zcMdJ%WCnuS+2smO7du)ioNSDWBL=QyigAU3aqLFmkDbCbdk29ezphAoj$|xCrn9EE z`(mya_qh(P^rs!}mqe2TM?zUuWCa1sj#ahQ zfjBM{tAZc^k&|B;s+cA=4UA}0BwI?9K8Bv=$Hagx`qv^+fTl`93n+F*UBzZ&&mzU% z6n_to_whyFK85hf2txT}p{u0O#^a@58`RzX{askUpzV7^N2;ug1FQN4X@jV=W~@_& z9X4`Kv{wt(4r@-U#3&IjH#fgYn0@l+QPm~%af!>AmDa$U?c`ukNALiW zw+G)}NWB#*5xEl1_s&A^n!f?lC$FEQKED@IH^f608ht!`)eD{v?cmqmFYkN5gOsfr zuJn9ZQEi}=&Fkv=FY4ASIqs>J$ftg0Q6#3(wf7scZaL{bHWldB_jMrn+2C)5q(4Y< zOuKh&rgNcM>BQJJD#gaI5+AK`Pk+x!S{-{9uP9%1rN_#)NGUDHE-zyRW-T6U*0nd6 zy=5NvoOGFotQ)H3t>J#)n1$!31m0&I0jTIo5mfv%Px8u6J?Sk67|ujBWBJU5R~q8= z%(9RwSNbKf=G7y)lH!{c-DlLHMbP_g zL&y}$w0^2v)Kj)+tqs{VeOm7eU+_BEdeeAluo!WtD8bD!pUcYDisJIgN`am=#x3`? znp^&BW0DGen!xH!Z*bGs`wP%);swv0Qwu3`PX@2F?y5ZXvnku?vec?_tPZu27V2>Y zH@?}!Gpwpt37Nm=aCSK~$3~%;isw;sEQGOHrqlNx9eps#A`8pDiBQr(=G>@w@NIA( z!uf;;qfl|{-@{}2H^=6=SJv0|gNGowFm{XmOCAZFXZB5g`?+U(c$sVLX&YJT{;HjQ z-7=TLA(*nsTdHKK;(cbtJb3)E^F@^x6<%YbRIpiS8cL}Q91uSy9c%a{;{Ah!$J&{x za$v5~xG<}uU)$oGVQpZSi}neM3*DikW(Ld;T=ahH&%n8|IV3YUv~uoy<`b@QS_s00 z{)1|%|I@iqO!fa#kPsgJN*wu-cn%4VD-vZfn5;>lkQ%IOx(pcFSZ| z(^L1W?n`{sv=hEazaOso7l7$k-Ga72wz1ifYq`d!(lJfXZxvFke)AsIov#Qpx6Z(H zwJKx@_aTv0tU=z6>4dCYzYX0w9sb8AT5_yynl}aQZjkB7ls>Ch#%h*~QmdeNG&IH6t9yMC4Q7(f9T*h+uyMe~S#5WspsTJg z-U$yVSrU(gwSrcv4_ey1oeFE1%GRbP&M>SESSr$-@c@OtT@F|(9t!3Ha&sv#Afook z)>i;bfm8-ebGCzW+JYG?>2^fbR5Fy02nkGvGCp>&zl&ucJ_Z8c$mA2;b7rt1a1xBu zy$o4GCrN5a(Ix44bdo-O1O~2knFe~(m9P$MCG9xnfsw@qW(g%7EMIPXqXA5bz}xh- zMo1#k_4cPN$KwmNeqBf_*GHw4tpKFAV*gv2k&RO{MBEFpKvG_ml zh8WXaMd`X*7Cy6UHqLmADJsPkwEdtSwY8`CvM*(n-+#!eJyQ>%LYqRgOhZ8M4H*`pLIqlh29CX4I?fu1%@ z8#{Ph%POI#Mp08TTRIoI8aJ)JGzipfs2{oMEbnwy7b~fOwtl+KP`IAy|A|hGx-*!3 zj^E$hL~-MDufq0ADYu%Z^xSYt=>ul!Nl260d%`zQGH~&-H5Dt8dPjO#$<}szK=a<%+wEFm z(e#*~+HI%08@JaQqnSBn0_!~??Fi;^&>(^*!;RM|rpdbT(|w*(_*+wYp^x7@TcAZQ zO6(yl+O>^BdOgZ}+6BUdwZ?~s!X4QSRzCAl8_W+ZlM49SRub;qSEE(vL%z&jO7zbkds_$%FCS?exKQ*llk&`TgbzAikto41BEJm3wQ9N z$B~gKORpfbSofHB$$_SQDoxy$n3nw7$LqWc0lTg^oGeFPyyqt~W8S&S})^Cg9 z6bY|qV6uU=;c}`F>DrwJxAxrl3r_E=3y7KP-7e#orE=o7;4+mYvGS;ft-ETE5-1QQ z6Ad}dv_}cnpT4(mY`LvuUby3kuDREo|84#}AGwbH^3*K?DM}ynYm+$3kqUz5w*fxGOkWrIq+7 z2MOvn8}XDBOF0)IE#^n+oz=AP9Jobzl1+Ihw#EN1uIuu!dRK*VM5szxlGS>(BSE5CV50 z{omuA`>%Y~{*6Z)ic$OD{tM8sXms|`pC_&rgd9&!;&-izSvGZum2vGKHuAlXyUqFO zV-d_5iGYyOiZ<6ZN0F?Eh=`gVAUAzWoK*J-eYW^A}+CQ{uweQM!Kkuf)Ie$)u5okB)r)0s#Ny zbPOCh z0XQfo2ctJ#=X{K6W2@V2Y{V`x9$fwH{;RGh4zBUb(nRsITIq~pzp!v$6<*iev8K+#Mns|TGmoN148T~j*{ zi{-)Q*YQvd0{ZK;H5Ic@B_q&YF-+?geVBzPrB&G=oC6vC*8mjpq zTd6a}VQQ#s^cAVi9n*cH*=Cmvfxd7;zIg(VR z4-|;ly6;%VT9J(hpv)}Ta7<2u&l$_P$h|s*vlc-8=Al!mB45g*Zk(8d+SEoiPRK$> zjNVxp3Xt5gvk(O2rsQW%Fgf@oJ62`oKsmc}VpQiGD1bXP?=0uECxFQWRIm_3{b*jQzEym|Wv^6^$ZV|$u6v8g8^T;RpFk-v^-bY}tGNI% zvj4p}_{TKO63r`z27F=AXMBqln3krKs+etg)nwR8t11{j1*Zk+br{y#+-V>TW4=+2 z$Z?atk3(k<``$LeP}-S}5tmIlR@%r(t5Qi`BcbPtD(DUs_6f`v@AL*o79-PQzwi%( zl&>0o*}8fzmS@_n|0-63>9MJ{I;9ZCFkJEE803ZzVn>T!=uqR>YKNDeSUfQEetMQ& z)B<&nIT27RKF)8$ny|=%l-FWVh@zyi@R_5d~d8x$4&EUkkWAby6 zv@~PHkz1DyP~+1Z@a(sdvtPK}0wf&5m#%%75U(OWdj9>7v!q1cM;^0jkkw@n?V>lLvDL!!w==; zfAWiB&wS1R*!?PaxW>^^N2Koc8J5jRvibCP2@B7Qsv5-;c>{$HLa*xVsn;$B_#C(n zOs}RL3Vxlgym|Oo1s^EWWTjw;RN1)eFH`Y48qzsUHo8*&A<|h%*h3;zB0YvV9?t=! ze7bHX+jX+1RJZXX`b+o6Rko(X$LyX|{?5DW-AisVTMKg>Q+WlAh)l)Zav2o)S?;#J>xYAim=8w~&+KKvZW+C{F#rJo4`p}4JACOVepHfOfkfL!K^@z(K@iOy#^q}&8cmU&BCI_^VbourbsZT zSZL23n}2YXFAwwclF}1sS~fL17^vMf#)g?`8(t-p`3TD(?5m`yKz-KYVyxSzssT+e zicX^g`a)dXa2a3bXf;TqC2ys9x#W^C=80Icm`E&lPItbTt6l@aaVDM*VRN@p%s292F&WVOr6w)dxiis}bWPjdYE@yD{eXatP1|EQI`A{vzH zlwN4f1~W0UwS(GkFqVL!rvFT8K=mLbvv}a+Gqc%33(hyZ9Eb6*nP$BBs>@9_x7>VY zRMz@@{lbjD+y4&X!p~||-Hl$MeWSE?+1HgbOGgI@{i-8&>+SGVGWZoMO6ne2$2Z4fvwA8M0z!WO2oTcy1VPT3srdE?B0X;az!z?-AwrL#2*as*Pp z*$RnBL1czpMZjiNt=Fxk4+am(TRV={uM%uD*%+>2yiTN86SDl6!OZ^Y@)#6Xv)gDm z%5e6umg_ND$M+{aH}k#v6O2WVWGId4%q$^v;3`)z+lX}2(yZ%y!iBZj+(HoiW~x?j zN>oBxwpDSC*Vos4yHG7UW{6B!4}fOpprnMkEnlv8)%>D5^4cE6)wqN@gjH3@|ea&@a%M#GO~GM6KS&R+!j4lJ$2UTR zAO*%`mxT|N1)K{DUI15N%-mo!CfLBQxK+mi&>S|mt-VXLT89C{NxW7MEf-+SN!5XsTXqCMmL=nw_JaUn;HxmeNRE?JE<(q- z8z-?DQG+MMZo{xp{yHp8k0Ds6$pS2KDI3+wJ?e{LQViXZQXba&omU+v$ zxC)#wWhzwb9D{r1PNZ6+c>$V&U8JfB=2kmGC=e%3@R~*kGm)Z#=g@Q*(D!f5z7hf# zVbnncd&VH+pBqjZV_3;#p(_$hV#qWY^#&ppRVD%6Mt72cI5{^UQGlzF)sXA5yN%_Z z=jo}+8W4`1=2o6Lzyz|k{~#Uh*jXR(ZorWhd*84{3=m>DCTs`1P?mF3PdN1zCLm+^ z&yGkfzj_Adg?H`>iGfFT9~-lz%28J66`8(GlgP-fbk8J}RXtTjciJ1tgEt_ub;CEl z5WdCV(tYVkWQE4vz~egWTH(+cE0Dyx7*~RsLDZ|@YiMrXIy6s)f=!M*DOk2JW~ho9 zs^+{;prk`W(yQ`PqwddVTGzo*LsMzUKDXFm!D0T)O~KTt98|A!GU7sz%nH%NzB_U70`i|?{^^zvH^{UN?4aLmqG!@qny_wb=)Xhto zH&I2#zDqF;AK&cyR}4L|p}D7v{{@&Weq3I!!p7$ATb^D>oX=3Qeo0aij4=|K-G2MR zyk}@>rFZ8K|3!o&T_kE$YRuvqi*?;)MXl|I|`9u&OSA2r3N?%o|%Cs}kkEUzD))74Tk?$Q2M+i(H&nXq9PNt$yA<0WFq z?}7InBH~T15pM0kj-ispvzZROt@zgVd@xJYskE8nk)9iRfH@|N=WCAjhh(^?PuWICV6(p0Tn*}m_ zzj9cA(u&j^2&29mJ{p#Tvx1HKC;~ABJ254896iqS($1yAL-ys&$-r4NjD$j zhjbYUAJDyumqFc}X$Rg9s!b;bq~@QOt7Yr0a@@@uQ1{T8uDOv)julzI+m3t1F4z0n zxj^ZRe3o03vR_=fz4I>UK00FY5~{7vyY0=J1g=^hPiclX=@YR<3OQ8^uD#K~@D-49 z|27a0^3go^g3i-L^=LctLhwr!O8Vq@Y9HMEsIHZsu#oJKahPYy5Qtbgh_7U5A~+2f zQ@TEcG4tfOg*JZqK~vK{eTqJFyDR0yQ&!iEH8p0)k;<$qO;W2YV{z{)E9liP%l3@3 zC;El}CKyIbm|+w)NBd?z$BVsb(tYq%;mp6tND8lLb!{@5Z{Ohm$`0J)0j%o*k(7c> z+0=?vx$Jk!{3c^=(GpCSMn=BPUOpWSTp9a-L+ME4#1ei6Ry?#x8r&VROjLKzgSa93 z)WsUu)H5pgwW{0Go0N%h}ENgGfj7i;a{o;9$Aj%Lz5?yEy8W8QDZuapT_mlz9f-(Fwir6*_k$+-7n|ik`yFg-necHZs~9pK0kol1dZ(6bWg`gk%e2w4#9d-T|0Tha@{I67qITwPt1hLlcP2T zl`Ex>)M_59`LB>}FI&h#G%n@W9~EjDJ*T+CUcFfO>H9aD8~BOIDljo4`Vs0j#W0rTJFH2dOYtPj;WExv8w**0iYx;t zFFSCmp&CE{T|~JY$SDTW;ULGL&a(LV4B=3Kq_0BNKt^O0#HmO zi(T+jt{-DW0$*XAN_Ku449JC2C}ZaWv#J1eVG4qkWX3=O@(b>Z6SlT6#G5KG<<8v% zGms2YMLv#;!Rcf9mrjNVVW1k$c6fCLa-pg~1;`3ITmSQQ;Ez$k)qK2aa-^y-(mQ)j zR$UjL+UK=Nl7}Q6CU!_)6x#o;#>_cQ+djl4m4c;u$_kf#IwKs|SZ656Ij zl)OxDelyuJ#o+v(@r5?Nx9aV<&6;&VYsmARlbYxqwCQN495L32Q>LTKUMh)@uVg)) z5A58zXlvcXRR0%X{uKXgru%l*Ie8%v?@Q<8KF}Doj`l3asmLX>l)^F5nXl`n3o3n` zHKe9w{B74pdZp&Y{-GYxxzcdu1c4GNZ+G8pTDQT5BQN4><_JXjv|22@NhIF1BDmiYupDU6~;`&axPgcHrmonfhPquGo z^QMfI#pzPL2rEj#y)A6ZE-~D9DV>9F?+)*{SHjT5_v(EO6(z%-ga5V6RJb$Usa6 zwC_=Vm10^b*!ghm@q61IVi?yv6+`r&gdA{`zjY0zk47oBm#b@nK;^`wHFYhomv1~4 ztRxI3uPpipwuY;Acxf;%QlcmKvG7B%f%w^EaxcvuXz;o|-5^AX$s4=$@@);9aofi% zbBQ-5m%%5n_&pDoX6d!V90hn^%tyGv4K$km`+HemP&*J>4l>q1huxk_n|s zX?wC2H|J;ik>Ijv8JflPR}qIXDotGsK@it4@~;1Ji=w+HpJMk$$thpkV2hrlXCUUe zma1(E;EHR5^kBymbs^uSmk|zL?VSvs3;6~HX`ck^O%bA=5uUV&AKGXS4X5Kc8#TtT zQbAe?+^KFAOVO>gu9m8<$7h;Dr6iqxb)5aljm*8OCq@|9voHTT)W;Yx_x}aBfVs!0 z4H$}2P?pa>F~$EC3GYAicg)#VY3t&-Utblw4jqQvUjEiOt|w$da;~V;%#HPpbi#(I zpxDp$@(TZ=+O55PcgvJv*8I};OQIOo>$ifewPdE1B!0&2nvGYgL$nrga^k@|+rFPV zdUbI0YxT9GYq;HNyD>1%>AG4hbZfiaqp8361?e@J+0ws}~!djzqJ~0IG8reY0 z%4m}!24>PO)Y0FE^X26?%YpBFGd4h&HhnVvj45_IRsUYVJ+M37as!Zx3O;^LbCtf8@e9?poEeHm+^oLRF>&WbD5{0Txb&dMX@^cWG=}< zlKul_ImgZ*Bk;;GI&xrk+9ZQgrvsq?D7PnVplrdFjMW(sQIQ*6!B~(V3SORI#<+NE zI_GKms_(;?xqS;PgClHKitk3!0NhmDYOPayPPfXq36!w--E=!ihUTKH||3K)((CYQjx61)#bUyPyb~ zK^|m$4>#IW?P35;PLRTyWX7ODmL^=&0pBg-)U-wy)xP3noOVgFO)`|Xf^k6swqOTY zfC7mqNC7O{K0$X0u1iAzzu<@#umV$6b_w1d#_VJpl#p3V)nrr}lw1k}*RTLUCNKPV zi8`0F4U_@y{H-ldn&tW+3&FY5FBu_@*XJ6r%f6%-SMU64x1V9&I4r|D4w7*j=Ge!t{iLT$d>D96 zvjY_?mR{*+fj13=svLy%)yT|bzxinR2=!vm>8`+>T7HHGBVp*(QJ6B%bO+70AJP_o z);VDRz0h~^h1z~nFnc^}-;NjS>$@u*fj}rPR_Bl8)842;xl^X;&r96*+~zkUJaNHK zXX*2Th?GV-;cT9kQYX}7t>f0xxVi{yXDe5q6;K`>MR8~`;5nH?{-mi@O+&7)+(H6kvkMMII0y`Jsw8-} zhlIUaQ^uFJSw!Xeim- z{5Pv4*O<4m;dNF0?yv71_v4As<`Jp`%Wb=ysn(phaLmN}lc~uE2o}#0r0bZHz)xUb z5=w0}HWNMB5Gr?xB$d`9sLmrow?3dZm7Izozrh#?Lqx2PqY?Ub&gZ_@iwu=^xR3pMzY=DSt?gFK_ie7cxTwEj_~&zkIDOM371eM7eu0Jm zIa{0WKR6iMkCre)Jq%<-^Bgn#m1Ln`Ti0~$mVL%$yVVrP!;t!`XC;Hjo5v68ve5#K z_EAb{SC{DBnih&#fiWQE{OSIZJvHXq!Lub69+cVj#bpMM^1sZQ;$OP5|BNk}SL{)n z?(lQ_)jiN7H^_j}b)1CI$wXzwET%hb+I)Q3?=XUOB@^%pc;g7-()|kerQO(?`;$VYF-8jmD*3!qtet|7quM?Ds<`8R0z z?r-B6d07b$PCLMXrKG@VFysFfwWcKyx=-8$O*-6=6FTLUQNLmP5tCJ%=GhVy#tX}f zJ&)>8G`zgg)F_+#3QE(I^Lij7fo`PC!=K;8pk2!k###Z*N=4-}vQ558k$#uDh{sY+)Wgk=jZktZ^JV9!CUqkXR%p|fD2 zC*ZM%B%(Mt_#bcW%D*x(WP!lY3JkVk1@3fK4FhBdCRJo|TBtCVz-?QGNDlos-w(_< zjsf>0m|hfv84a~KT;T|#Si(LG<@XOP<(P*JMFhK*vE&Apu=9YaS7DXtGH%kta4l2U zku|Q|;kjE~UK1>~P>{E`gE4XrT^3fxRz@|xrEdE_I0b-e1e%TmGn`-q@Qme{h_%Dt zRG5eIP*uS{NK}9ozSKCEMmb!9F{eW&31(1UNI^BD*%!40zhR&U+S#QRatch8+SxRC z%?3_}B$!)~lp#pDm5*bds*ol;Y_qh202biJk>Bb zXxq@HFQUpy@!LVvclf!2>R?~JIP)$(GN`wFC-dj%*N!wzat|rlbJ+^Q)NV*U8#hIs zABdOBo_tvr__(bh0E-7m&YJo~)SJxXN7dcWBuR}a<|0mq@iH4cZ>;P0(9v(T(^ZV4 z`589&_7RiibD-j+j4EvfxweWK*hvi~oM-EqgnHG^$x-73z+vu8<(;dVJZ=V!ag!jF zU|6pkZ*5#5wTbK3N>_eQXKzlt3?8s;|2baMKu_|(^>fVo7SqdYtv)?!kL8-%m}L&m zh~WW`1TX^md0+O4wBNC^g1(V z0#bZHg0e0@n~8U}HyboU|6nj*%2;%(L?Ww1pxWn-zl2m#L^GMZzSO1)WTPrU9RX4&HBk;?@M;X;z<-*UvMR5owXDDz^Dv)-^RiO@Or_!i!pMkDh&Sg}Y!%ud7;JqpV-cxzfaR+X2mFb0FvNz|AYqGze$8+j2D1HzP zhn!(@1dzw|nvf{~WApxd0mxLaG!9gI#VN~0>TbJI;N01Feu6J$iN`iH3#HGU^%5}0 zTjfXQg^*!+z^8`e>bKb;_6`yY{>Ydg2;u+M-j|0%-Twc6##*A1@swRchO&+<(NJjY z%-EU~MVL{9O0>*KRFkY@8_L93##-4)LiUI;R7wjXiLxczIQMv-=l6Yn=bv+~>s-I< z_dVx2f6V@w&%M3x`*pvU*ZY=Bfsdr<8aVju>Pnx%zA=&1){EVj6Sf0pjyR|;WFV82 zUzU2+PP4gwXsMdfnBMP`#xHuo!_Nn%qV_^&=2ON3i@4{w(oEQ#lkM#x3EL<&HzaY7 z#c?6OAYeoHbon=od56gdb?#iQ7BdSXU0%%}4ShOE`c}-3?a7#%FI-m6(mU$47rv;o zr@!bOtTI<})YJPXL*Up4N%qv6xrV0ehLaf}hVTQ^S?iZ&)&WnZoS#Qr?0EeiQH-_n zmu+pLt&dyh@-WL|2E$%@>_t>}QEQ%sL;WMQT|U zeO)+sO#ktb^ivTBzgGlrNkqTds(A~Gl=;xslWd+yl49GoD$Zxf)P@KAvrY?-r6}eDN(XdNjGj&7%|Zg*uq-pp0zvwOH_joSr}1{# z+#1aTXVDCk_9SIt!?90B9Rv?hp|V?Yk$E1Wlyou_5u=T_oz8#kN)1kp00DVbE3B_I z7~S`H=Pxw=NAy?8d8+WZz>mKxY3JAnq*Jfq%B) zK&q8PN){X@jnKdV1mwKh%0=1jPN6rW;J}Pd#xpdmu9rcB7C4|fTi}iu2`F`e$V0I# zq&-mokr-*<8u)Alg8VM$3b=D({d5Ia2CZY7;AT`-+>4YD1ZBR}5M z-c5+l(&(~@#uh>6-o0Ov4RVq>K3G+J`MpRF%j!D|_Aodvwbah|yj_1l^n!BUsW>BX zd`M#RZWfQXD1`~vK0M~F)0&2tWPr^^6Gl9$hxhigDc6dt+IB}G3Y-dc9KvdO z8;n$(x_>_qI^6=ZcnnkW;SLSqZm|v$r<{5-@HjnXhT2)H*9Cc$og&o6Sd=R1XLQqt+F)2;K0Wn(vFb(F?Zsy108xkq-3 zh#s4L&2?YoM=Y7rdIy#xx9VcTSJH4^gWU4qTppyYot*MQhj3cw(ht$Y6Bb;)n4ZX! zgrv-`n#wmlwnPq*%jJ?QyRZ^hJNlfD_>*~zbBmwj6;V&Mk@8Bqd31@C9-cGLjyQ;} zLstot3~o^elOyAB*eQuI(cqVj0tIlBIr7!iEo6wYH?`YW$2I0myyQC62^j3h%`M1T zg>5H$`PYy#ZOkh|ey4!>_TPVjwAhTwTI(F?$WHpI#yhr-j~9jU07wXO%odv9qy3y4r*jDlONlDJ~bsiB)KM9su(8{ zBpG$faA{XQ6R%Zlp;`4^Uj*~D>jJ{zuGUw^Xld2mAB}HX%(-Q+%h3u4eFGD0-80Du z!G8z6`F~XhL~nJpX+RXPaHg3;e&6mqYpV!&9DeNz5r-H|UJd`Uib~0>d~wz5!ZVS{ zh+47SwD(SDFU^K`S&tq9sI>p?b8CZ+;_?UkM_-9*Hp$|AoK;TH+q}>; z;UD!k1J+mcuzl%=JX6%ZtvM;@>k#+q+EwgQH|Zi%{&2DgaW_38o-78BkBstUa_N!s zJJeZ42oJJ=W)#&30Mihwn$9JT&mG0gpf|My*&vIe0<8F`5Hh{5MF_!_BHU~7-obR!Bq0>OtGkZ&OBVMIRYZ}$a+|+C{ zc04*pcew2)wWNXSeRQ4~wK2-yLQIXzxvI75zz z)^|7qPL9aTY|rND;h|t*Ang3xx&4wD_XFWJ!_f$+!yif$+{`XbhqOUGP6c; zC3BI)5-hZAp6BN6qVWm#HS5AaRUpK0>}imNMcBIeP&`S(93yDo44IOZ&kC4v!(l=| zUIZi&_%dL)#kjM~7n_eK_$O%$kYux@DW7y1=UCojG=bm9fDeUMJA*~duDu;5NgqWf z%A4%^8OkgRk+hQ&S5`+S^U>o-ElS%@XI?Ki@Ib~Z@sU)R4)08NbCGt_8EAM4KwpNs z(sS^V8;cNv4uo5pe;`TNz`qM9LGaJ!mPP{ltdU*$^Fv)!=m|O}!7fr;772-1>oK5Q zk`mYMOV}^Xe4oly=!USy@I+JoPz=k|n@*|DJ^YL? z$~=o@VPAjQ1sA-R%r7dhDe8_0pxW_5$73U-^D38LV&A4o5e_ZUwzzp=Xk*l>BNdr* z5>j{_=`xtY3*%;Ip1gOM>Agh)eA7N$s@D)^gpciH{1zk83BqAsbuwhb>JGAL=t4uy zkQriL5^D|drTEDA+4yjsY3Yzl_H_X`miI~FS?M1Mzd{MjVsn^8&0;XP?CwJZ*? zwmr~t^y?OqzY^A-Yt?WI3A4`>Ow~@jBp|JVId)#HsOSJi#+}%xqT<*W-D^qNwwv~EFg2U|0!~00fsD7$F6GZfTl=K!%5l#2Zo8?+N0LKV3Op=qebTj!gJ=Ua zX6WJ4i=!&10srY00KC1AdS3yiEbsq~;l3i-Q!l;cyw{;HfZqD0(j}U4b;6{{=*>VQ z>o?z@s*0FDklFv~EI1!_(a>leLRq9(p9wTp-0NSX6P7Vhy{uw-^2EixZ&f2FC!Aj0 zdzc_I;LppTn01qn&QwM4mWxc^f9c-$91|mLqyt#s^~r|D=1I#nS+$6x9iE9C(FA( z5`Lf7!R{{2O19@Wc&%4Br1IkZz-88=_gLXIi-mOI8irU!5VdwXcFrXp7F~YDE;dP< zGJQPENhQu8%@)UX=dxl}JiiCkL~&1-y5z*@{j9pZZ~9wCu>8l&@X@1!0!j9`qboXF z?C)4N46>fwA0li&`~U$%lsQU-<@cd&JIU!5wpgoJK1mE04P?5pRgKM-6{A|ShAC-y zD2-uE2h{C8BMXE%p#E5^i??JdK4{6>ONDTM56MXuP^~tIP+a2Ybo+c@ot9{@>_?J? z;80{N6IlKrhEt}fG84uc$i?DFVjq&>_a}THa40$%(l7(J5&Mut#<_E)BY1V~U^Y^S zvTqO$kd|TA3Sx8`#3Wnbpjh$4z&)sA2eF!Lx^~m}z?b$Mighf83}Kn6HR+)aCnlgZ zmS2v&(Y7J8Z)f1pGJ?h&BS`^Tl^Bg56HfNSZ>DY5!9?bj0cxrw%hIk=AnHtZRNt}SP-Sp76g5rQ_uy@P9vpkI7c6p12lP(1c$H%FH6lNNh;C1 z08V>{Ug9_}(-CFWba1#+SYD|mV5Qk2DXH`>NENST27N;UMamRco{3G4;sI z5ZfbYTGex!kDS66xnjyYX@bOMfdeC0OU2)Svzvpp};K4`l@V9GccD~>m+bu5KnteVAW4_M=qGmHbwub~Zr^HdGpYEqg+yz$W zPI9m?U$XR@cQ1F1)OnuxMBw)?^1uARlgU#4X`3B15LefEr@A3WuFteC$WbAot;pl1 zm3UMCwl*|v)3f&{?YsO3+vIOM@5W0U^s154JW&j%amE0EjUEPXu=`FvC8i z%_@0Pkim6FQFx!PyJ}8BV0AlCpOSOGIAM>8aiP&t040tg9rH_1pwHj^>X*YHG%2JS zrV<=bR#psGcZ(2~@MIKDG;&-Hv>>Fz9= zo?yAF5&cd7vkUnNHJh29ye?)&#Drq+>G_G~xsKVY-PFU|9Q~$M`j*%Y*)|D$GGToFnICSum z<(%L>df2l4ceS67ekE}fw9?{lSY1fZyHt58n;ff5ZE&Nw3+4z_}GbOoSgN zw$f1xUBpDuiKCtYb+0mc+ZVEZyRwQ-AE41L=Ie;p6o-`P&WjaYyJ`^&eEmTO^&XaM zQ$~FB2cWlRE6x(WofZ9%))brtM^i`o;n<;mp^OrC3>5vU}iS+ z5o)jYN%iJur^o}*vOCAe3Wm%CR-cL+Ib938Y`_xM()u)?v+KPMNLd_J^(hF=p9pW{ zMIc)UM#w%P!F~}#S;KPx^0&8xx6^Ap;rUADa&;gK<6p;Lb*P?s}s{qZ9#of0iK;EGjb?OfNZRO)2 z7Ww2n7G{*q6^o4k%QWDyAP{)qjRKHnZ=8)Yqw$HD*HBwx#UY9~mPsue1H@mBMUNzj zFqH*rS8ldxGy$i9-t^c^nL~YvfuGKPXCAG@r8@5w7Y&fJI=f2;Sxw{jq6qeBVQ6i@ zsHp`1jtMi`#H)&U4rsePxa)&1&6X7;xh-ac$>&rDp}XgJ39cAjpbR~>1enPOVTQ># z2x5p5^pms_j6}a+B#@6QhIx-C$qXgs3(I^EHj=3LJsaRyY^G&XnLtALMS|>zN&G*8 zoEEPpnp7w>pNR6q@z@PoHy@W(qR-B#;*+A4QK6TI_2i~6_uJwVH-NZ70-j!xU zM?%1Nr-XzeIlOJ)t3&3*&eQmMfeyB1gZ_LF-aC*nm;fuoQ1NsaRR?RHlVBvm-K-$@ z002{$q7NPD$|9x*Kr)t3__EB{Rt_Ws29US~_!&5xVR+{v1*J)=%?F~e7JZeNi}_rC zh>GzQCU)tZfgBsU4AewXiD+jy><Iz9zp zcnVrYy_8Ju6Y^(+oc5$29U$%BqHRz0HOh%S$=F5L;UdYq+~_MM-#a6Ofz^=qT@I6rxeZl`UQAKL70G*>i%4VIu|u}R*mZu+(Dt)o9fD3BE9sc(~bf5m-8Pj2$$MYr4A z-6qa?h`2v$#qk8->gET>GCf8~Z;A){%*cI4J03Oh7TO8`@?>57u^gm<(=fCup<@t zpS%!$5H)CYY06ol#$}|1P+rD2$R*_I_Wr;yjK=kAifWrwI_-s*gVlIz6L{vF6|ziC^M0G=jjogjApu`Ejm+n5jXEEoyZ4bw%%lVFi*Jw!8u|pa9%+i(83|UkDqi;ISa+eMNL;fYlhyg`GotB7ee8vptW0&)~``YW^1x+OAwouiqVqHcYia;S4F#%&j38&3HxYD;n5F z``i80HK?f4bExrTwgIsvrr=qWVg9!$0TtgBxsNNeY>!pt1HF{T?=-f&5pB=@u86{m zD3Y1iq>W=#9sN%<$&6UD+p5^BZmFP8GR_=o8jlG&L@r-T3too|>SsQ)k*^luK2exZZ$Z5yNDrsPnZW;}WSd-T4-CshX_N5Rlu2kql=dv4p^5J_q+Sz=#snk!N2 z=zZ5Kyh*Y66^=T$WgxHY8Gh1a0BnyBNO-jsoA?H9$@SrL$}O#qHxcX7vAatUWFE81 z3n!N;nupZ;1vp4w+Dxtu2@MM%cVF6}`WYv8*87CiI5qp}!{K7r;QajqDTRhHX#;Wl z6Ky^p1_VVRk1)pvpDF#x6Aov-;o%WF`v_ut39uYpy84l)bGAtu;-lC-yR`Q=tPRO3 z|4^h_D{;JQV`*jBdkV6EftOWpmUb4)RE^A_)jRoX;9DsgFTH|Vo+cNk(slJ>koG^+dL7C}l8)&?d}Htr&1 z(gauP60)Gcl#Gq-081z?kIV2K-XxOB5`ZXr+H&bE6-*I?<~7{2^*lZ^`|b}6tcyS{ zKYZ#@x>Ld0U}t)u5A1dz!e#h&E4+jmE0ErQ(@rnHw?FnWUE^J+)P2U2cl$ifPgJT^c^5hd|5Qf#jCO}(rr-iN$agg}B^SA* z@8D~+gY1eC2qr+M=t5nHoR~VJ9U^3}Icm^_yp{e<_qP(P9PC+jd9cDafa$a!Yv{~Y zhA2T+p_eD@>mUqW?~@T@xZLMN=xrcO`nTk4>%3(y(ZjMK<5jCu}F}66ROHb zQ7YCXyj1(5BWm^mP}CKc^0DCLE_ny>D}`eJi{B z)QeVU{V1IM`gafSf7!D3w_WDn_*G%Wv2}GZnZ2Sd_iZP-)}&SXlgzkdUOp!No2tVJ zYo{+k@g*5Q)NDI4)=C1_+UobPwKGgn_(Q{^F&@q<-t7D9(9Al-E;rP^-F>w!_1b~- zbtuB4*Kun6{mE{{*T3rO7Zp}-{btnL@weO-A*5RfmzERVPEN7hpthtGc#sy?OUPlH51cK z4|}HqxFd8nAfV*NtOl(^C60VqmuN7J=L=BpS~fsML*h&?8(y9FYm39Q)rFAR^D}Yp z;zj=%uwr@* z0n4}A>Sf}+W7cOHSAQ1BIO+m3Yw)jEYX?CdsdZ?Lpy#u=h4meCWeZ(Q{qfZ7+$tmE zOcmht>{y3B@|7hVJAD1J%Qi=l&s2c;cW3stn@`D(Wq7E3uauc!x;w!iJa(|*MUbv_O=^D~p-Mq?cUMJ`_g|MZ#N=Xc1_#U=XuO>UrkA5{PKfOEmWh5&gl2lpg)J7YnRSR~^d zd&MOM>Gb-SY5n4t)z;q_DQThHv+>+-;-|JD~Mk*23IIP1#WLEiF05WrNKt#ibTpx z%^!N~AE=IG`Pyx%!79C&wWJaDWlP|I7eCYmn0E#W>(DbHRp&RVxAO`%dju(7YHX7E zFtXHFU+T9GakfyXmJIn?e$8pe%#z}S^BtQt7^p_`wlxIt?`mW`$v73%TAg9)L-ced zURQew`c3)k+#eF7Ms;oN$TiB!mIsC}zo|H{NU$HEW?r%DWZrz72Qwj>xG=FI_G=qn zddyMuNsYm7vn9#OKy}1K_Hl&#kL&sxU?2%Mo2O2nVv_#a?EH9g%y{mJTzG4DAPR>K z@!q=Kf^=z2 zfJFsyQ6<$(=ce>+jpc>hmAs`}&sOi99ds$yjGO7JX6NL&etY?eu52f*BVE5n=$3yh zA8$UI@F70OG0h3LJ0DK~W|2Z`p^BMHRYU&uvpa}0$na+B;C$UW)Mm~G8CisqoAqBd z?KtU4d@V1-%yJVZ6fh@cesvMSA7LdW-vY0#fQkQ38??^2gka?E3cAAe^{P_l{WpT= zPOq`&m)0SY+HVJMOf71bvkb_9KCV$n zxbsiSES&?NV}9lhE`OI_21}^AW&3gSwI57SUxNwo33F#uN@5=Tb_98=;5sD} z&u_K{*6Hmp$HAw~t{40{SAN8&t*k>dx>g7A?mXy*&13b8^%e%N*^3jVaOl~Ej1Oyw zXCB^qW8m5kS8sX*Pgw0U?*`UuNrixq^K?y(AK)^7kfNn!fm4f?g)ES-1w>4}B)Tyv z79;hXyF?z{n_xLs#a81KC>Rw=wO5J0wNbG>(mD60CjYuaE9VYw+G|8m`~s(@BAnDO z|MPwQaXOVna67IgCh-a=UyoCMrZZ@0wpQCen_#O^Isl3@VQw2&BcdLK@^P~=2AJv%U+y1U#{3jUFO{(KkGV^eW z>Q~Ndyl^|{&yPPUUBSw2y9!K(kWziM6j)JOkN(C~WL;|F1FI-80&d4cB%~aeT3zF{ z(zblAuDEdlxOxx4*-xE0N~}ROrc}O0EPA%C@_?`RzEjolo6}nske&@*VR~~)7TT#9 zGqT33Hkk8QGq^8mNY;Nv@V@Nq1?>f`$e9l62k*>(r3TysVhD!F>zCn#NwK~Tr`N?? z7yOxFe+f(QMkgb3yipsyD(%Y|wyZ87XCO9jPUiJIeNG09I5a0Q5TgNv=r19R(WnE` zx(z&k0I7_P_YuZ!{m)q?$doQxwvz{P&khCMKYC&NdyCM@uV7k@>v1G) zXLkY+!`+FmupDX8xf05}BY6R7Z?uIYd**eV>I+Rx9!+Vg10I{38}htjQvZQ7mRxH& zr&fuy?7swvBNk^f*rC^NBuoJr1Omg6&5h1S9O^}|Q5@;p`*+$t%JP@25mk6z!*rswWfw%7pTI(n4{z&6zu4rS9dV`2AE^wA-hH^hyu17 zJpx=}#~HDw++;}f_~B#Dz7=LcoKnGYR~u=7H!Pz6|NZ}7Usx2kHN=r0eKl?XgOD)g zEHn4PZRI<&XsGegDT7pggU51Y#N46bC-Q7&Em#cdH$cXoF%-$uw)LQ>WUuL=kl>yB zt^la;eNPNjYg>ozDNl;Rg_2el+H^?Nr7x*bugYy%B44V2|EM5w)DY zjGvk0s{XV~XOnFX$o#$aLK1!7twVusZNz(Ur>C=8Uyiu_3!(c^6~u{;;G8p%=*!;p zjB=?>JJn3s=U(}pyHas9w&nYgi`mic`2SK`NwE#FZ9oVEtFxAt8TE+6XD@GwX{bdT(7tHp3i ad~0jxLRHFsxf0z5K$QMZuNU|F+y4R8J4a~% diff --git a/tests/assets/hlabel_classification/images/test/3.jpg b/tests/assets/hlabel_classification/images/test/3.jpg deleted file mode 100644 index 86483d70c5cd7e5bde85f68a6def8daf7ab53b48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25473 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

      GgvU0OE=5&!TSEJKni#* zop_PlTv2)Kt8iC0WpM}ElqUrq<)H)ZhmD80^SPnHZUTW6^?en4EwRKGcJIFP&vz9> zjoo(7Z(qD(N$El0;EWn&sw7c@dN2xjn}>C!6$xi?I18uV<>pY%om9Y4V>J5WiFQ@_ z-f}B@%7Nq6oF+dqRRD3H4CDV|vhtjthTlajC!BnTRR|H`B@ftbfx{8u8@Yn(fRddI zf;@Yr=b|9guX+TVDjVCJGm3Cc!X~#**k9A^#g06NlHC7r zm5_%{Lx?QbK4;v(v@7YNc#{%PueUP|0XHF^ruG!(&hc+ctSg zv;J8y>s!W8H_kw24}8mG9cT4&dFVqJXtNi^XLRK17!l=43ac+bW{J3|js@Bi^L#TC z$GLkDBKN|J7tCvU4sfZ&-+b0Gi*HvYHM-l5g6^+HWQl=IEoKrXH{c>lN|NbHQ&{}d=*xtugU#3rv1Ld3KfafG7tUT_GG=M zXee8M{Fhmqz*hl6N06t;JS2oYuS>7g6$?NG1zo>sDEy~V$p47vl5clE4C0&9=O(+|JAonTLB$l9e} z&?R_O{Hf6L-mL$Sm+`(7u<5IHukim{Xv@Bx{T4I(>i3=pZj?w~(zCR!+tyFE+JE|* zwUS>-SAUFgwm1{4{GVahf$Ni(ZB#h-W9nYBIrY7f{oxH$@5u5O_cX=^oDsfrEF%2E zS1av?ZP(KR_WAyN-mvB;aCghmN8VR{KeA-s_2A#s%Q7*N-HTO9r-ZS@Y4iXm_qL^e zcRT+h^u*V{c7B_TSE!pAv#Y{M&qko=y@V|5XKSEb*osrD2^z^Y>wuk+v zcHYu+dNFY72MFW6+bfpBv{dcwhhCr}=-M0vDNn{#9h{c5q_XhIf3t zxiUEyXCFQ~uWz2lg!(mGo&UWS{AZeIlw0HLWoo^_ z(TKV4sJrQ^GpbkGTiur}?^q+f+GBqR(Ac@Ab8UactoYSrKV!YYzw0~xJ&6Z4NTeoY zt1}xgRZqC_Z|bTu)e}A}U$Jgs+JA) zrMb$#?cM)f^-uecYuyH&`dsHj7VSA|nq9ysyyk${_xpcVP5-<)V#hc28=H45+A(p>E9QT3^FN8~zg^Y; z=g<7#PwHFk*E(<5+tYWg>(PIP?WXcSf7*XP^q-+s|J%Ezz*{Hne*lGB<)hluR2G-}!;|pFZ9Hegf!LyRCd@Y{91RZCoM$v#S2mtWxjCz@znx{xjSU zsm-!&zOi}dqCLNA(=rOw_Rh(B`FZ22<59i)zTRnny=&p)ge{thPtJR(Fit8AG(E%l zeOYha8rI4sv$k%Ky!T1w3j?rA-2Xf7{_jcuod0CW*{SN7^n2xc9ksf9_=kll_v=|j z^`{TmyQ=>RpS^97;?0=NC z`^O^1f9r1E{`-viPxkq9>kAwilO^9VKK{C*Z*iHUT=_pM`$L;6n{^Wd@9pn=$Ny`0 z#r40B{C|XI*0|P%%r~+)@MOZ`{&PDLOAGj1K0W{YIWR2fxbL(L7p-1y{Tb<5>)P=p z{9n@w@71^G&p7c(e&PAo5h7tmM}22$8M^I$t-*hi+u%P#2C&*-yZYRE+ae8H3ze?( zi#C@o`!!|dQ8Bq+&wTP`w%PAK{%HlLj;G3`N|j52k=yP)Rhb4{$pyLzY;cWo|GxPeDL=lv9lGfTBJTvn>_ZtvI;31mBw8u}JWIe^k z&BJ@1k6%Fil7ytxqPshPR`4F^XjXBStW+rD@F?%wl%@GuM( z9`PtLDlsWJB{eNQBQqyAFTbF$sJP@=b>JPLe*qnw14Vv0oi zwg;HGZV^*m3{A+c+QV{A={@x|yH=8e$G8WNf4~lHdt`rZVBY`L$bKEzZ{xy%Nbzuh zn}rc%{mZl)Grg@F~byQs4pb3l6X)C)mYxhCZ3@beg zGYsz-J(+;Nf7w#jbW`#ym7)X_32Wlb-Peyj2UNJ1L6q_rz{zcS%eZ_jTCa|db+395 z7*9YR2V>}PiAadwP3ic?PgENF%y6OJ88WhpKC9eteOEQu+xPmVP6{mW2>4P?{$-YN zfjH&fRyWg#0u+7>x%jvnZMKA)duG?T%d%7PC@D{XUG>=gP!=!YTr<9>BZK)Zxbuj*UL0>9GjS%X(DU4dd zqY~7r&}*6n;pdLj`#STS9GmfdM)c7wt6i#QnW5P>ap2-1wBsuaP9?JThB!q9s3P1f zi&n-AM47On7Cc{t)bmdECT8*8=e}s}{y{`Ea5@s@>w>>ii4v+t7Wwm><;;FHy>da?R~$fNgX z18n3!l1zbnt6CI-&k0Q~^2d1lvm)mou_pS1u&)6 z9FY|~*D5zTC_$w}qbNZI+I{_4bC8+Rrz)u)1VX|ux6?`nb)7BiEp=o_&E+l?j@LTR zUQJRo@lMAt$!tJBzjtW4&91+{bNtQw^``0J8r7VfvprSxd3#93iG6UB6aAMSWIqM= zSDe54IVoFwCk`R5fTRH02mZ2JCb8zL29j+WFP9_2tm<0OYz>O9)%3&O_%9+%573O?k9{Tk?ug=G+zfESIMv1N5a){k|uaBppsR1O}5&RNSbv zfO$Fk4HK2RZc=wBGv8yncP&7lN{__DW1oOIMfOOORuf=yAh&;AYQJo+>^7O&-6K8G zl$$fnO+?0v>lW#$GL-TMB$ygFs;Fb6&26)7BCwuxsnR%Zgt62L8rP_*F_j<4UVSV) z$Al=%-ErFD;SJO1qznkX7%CQZr9E^~IgaZHa5lnQyMFWs@uSrCa(Qc)db$};^9$L? zr|><0@p?}wIA;@Ptcag-<$)X(UhFEjwWX zSB6Nv5|8*`9~Qf@4s@D#bzfh!vwuPB!PbMUofH={13-c#4ph)5Hh?j^`fpP`QR6wa z^^=Fy^n7}i)02savL-I}n-qo^^R6UWTYsRgmpvc`mi*XT0h_|acuyX@BZ7awfl(Y- zklnc%g2Ykm-bKT!{vSRRxd|p!l0#O{5&E;DIHv#Is^0zicak0{PsO;*tpwo&l3x^v zLRjNzUCmAxS`_i=Dy2Upb4d9Z69vBBA(4T%7D?n19icGwxg=e{kP-D5>Bv*~tWPB* ztCA$BfIv_2@=ts0<71VqpBp)2Vw334_Tmq3@mQI7>aa(_@nKsg#-HPI!<2r(SHcZWZ@FUOBCeS z;fij)##&-0EUi3xNkvh@K5Ey#eXsT%5)^4NDjLUhRAs$l49IJ#(i96Gz4BVCGDVH+ zw!-7IumDm!VR$R|+Oa2jYEXibQrD=)_e+mQq?TSl$86WK$A!IU`%qqcB`%&9YUtbv zJaa*b{;YrIg^Nruj93cXt9M?-T3p8|v-=#8(qogm(E)ZI@OJp#8UgFaj6KY{F+}f( z(ihjk52+>O&Yw?GhIP+Pf~Aym=LVVsD6>4R4Yq(VcJlqnLx+rAZJnUNN>A3`-MW2m zTetXwdYOBUy}{|fd92_T(nJj%u*f$K&Tm=BFNuK~_c#|@dzsg_-55J^RJCy~R??zn z8bMjx$t2gCy(s+3wTN956Z-rvy!9YZ_#p%q7Z}ZV@vqcLym9a;LQE5H@|D}mXsW+< zv?`2%EiED0MhOdIlB{f7hSE{`<#{qw&|7PBX)Q}UV^IasWA5xlu`EwtxzO~As>O7> zmLG0Xloo@qJ@qCd7R~@S2>zuj_$ybt``-Vq3YND}DqA;0hJ)ve@3ffd7(P1c5O8C% zPME+ELl%A}mGtFiFd3n+yuP-Y`58M-grWrPD&gZLXU|Ty%_XrppLH-`N)VZ>D9`;0 zfuW<)a&pr^xSjKtZ*#W8#`&538=0RK3^Y|#llQ(4b|Rx~8w^u^HxEduH?^^_y#^MVA-GJCrT~VS?S`|1bXi&iBke?e(&LDhWt; zwjsPmM(T3xKOTS*^OqhtWe1PafJ8k{DhR*>$3@=U0tt~U9FZ}do_RoA*N}wDSj^fW zmt8wv8NXIu>-9QdB1$ueVY=J5w+KRKeRWJ8qU~&)_F@h^V zsSBT0y0g;~7!XnJ{6{=>)%kg!`?p$H3cr*DC#YR_Tk)+bLCEMY2_X@wPKzxKaQ}9ca3^C^CqYZ|IX3d zinnws_L46}Rs&v?vQ>h5GFt3!?vjX(qb9uj4Ft~UMZ6=mAx~>T?{hJ?H12tzFqc|> z=PmJUytC0^NgAG~<`@H(D;8(_brehR#w{9Z2RGXUZ5Cb6!4hF%Nih98%HF5I^@6+W z>mSB`B>;k*vRyHMFTE7vZkB`ysk!7ce z__dzp%c&YbCE6>hEh@y;R`{)#q*BaUCEPyv`{xMN_Ba9Ca8=^Jo3I9-(`c{gv#8)) z`?#XJXl3nlhf5590JKR4wy6|S*@ z_J=2Aml-!)OKj|6_ri)*UuiSSaJ`_?T`yXxiSrVnsVi z+=~@{ z`s@r@i_q{0`2x~i;<%ECab8+OGs`bUgw(F43`4EMunRYu-a8-jT7=V9xF*vbH|Pi` zhCTCVO=tbP&%^HD|J6cR9F-+IoR&N?hybMCju2_(T_SoZCr@`^Li?RxUf;W=*U%B& zMZIN_{NTh19rx@Wre|ll_!uKsqh^N!uLT-NkYsXzropeP^rwp>$KJ9HToM4XsaJ>p zFs1evfoJ@v{S&2(GudYw2BtvM9v0reD;eli9kXD!Jd$pt?@ey2bs|5-h4RJqA=cj7 zVNa<^aAWP@Y;h>s4{mw5PEkIb*B=sC*O*2aslz$`({94Q*Wkz6%u$vWS0ys#>E0lz z_^b`y_U4F28&b3Gpw!ZLT(y>zqBtoEc zaf)g@6R}6z?L>Mg*De#yBZ-vq=8n(r$2qcumw?)PNbDI@n2nkr=FhDtVM~wmH@A_y zfBz>jTS`4o(FfQ~$%W0WRoNoer~hcA9gy`ndSg~bOLZ?)V-aulF!f1(6Zd@Q!L-7uk#?|fVBOHNl5R7 zu6b@Na(eP~>?k|0ABNqM&8?A>MEH=jKg~3(DA-;pUfqLUxb$o8^;%sV@To3tm>xwz8fsyIQEHgvQ$67#X&ZC zeQocgippwR?Hu%6&sY%2H};o3kyRTL%-wlo+42L|TjzUEU)jSZT{j;u-SzCG+??Z_ z$61%z0(GH8J1sAU+W3Qn_^<$S=LRy$5@A|us~rIOja|F;mF$|lg_V^FMNo{|SbX{g z_6wi~&LF5v@fjolyJXTp6?IxKt(f7G6!=xRkuJ=kNFs46H95mXS)Pn znt00v@QL_!N24mYKx$}iF(om4 ziYmei$Nckq)E3Q}BliFvRbqGLed(2_dS6i4flucmnYy38?1{dR#|0^UZ0JoLEw_`R zw|K@i4nAIz@e)zpSn#QQk4iBdr`0Qdbamn`^Efu0D?Md_#dT{n&ci!K9f6Ruv`H`V zzpD@YYrWvlevikZ-R_CD9;>OW2yomyzJL<`twx}es})|Ey4NhAKGIt!2|%_jAW4k~ zSm#+%edVAA&FSz%;i+AAWnn^ahPQC^opw3EiB=@+!pFaC<_&;#D{jNOldiPyEpu$e z85JF{s#IRc>SJq3UlaE!dVe#hKY!fyOw1D4a*lkUo@-z_SbD+6+0(?)?k(GE^+Pj= zq5Q(j*JxB}R@XX!2(%5-G7m(bsZ8H^t%1^!mO~|lK-~aBv+CB7r)0iLI*-u2$~vbh zP4pZtjiM^@eH&s#r{~{<;Nj~ol7aPpn3__yI8QUz zbKL7$CR!Ulap%;ryofZF-{Z(*KF5)txb?Mnpg;ZbvH0%u2Z;37@_vW!o`sp@UurCN zTJxd11pqftm)f`o_07-YjESD{5Qc++(Dcz32#8HpMN1jdx=PHKpM+7d*uO$4M|J~i zZHE8Zx7x7%oAZ#)=*k^hN;74!v-3k^%HFBw_NE$X33z-6x(sYXp_%A&Z|Tp!^eVVi z31;jk7oXBnD(~coaKOl&BHfKC ze~T&q>Rh-h=H+CWay8n35v_TZB5E^9G-8cGM5CVhktyAeI2a@J z@MEyBKf1htk|**jE#><_ZJBsRidKTQ)}NY64+j$?K|M`}BSEDM`lk%rFXv5tK`V1a za`G^Zo=<;_1QQ>hsRW!g$dM3>}B zJO2vuJVuSGN9fY9!NZ`=iOySq8+8R*8&<+R-;)h>6?3Yn4B5N#@M{NiB9=^ci%Y9f z4M0v;VJD~CJfh>PZye5hpTtgzy+5t1T5a%p4yNG}}0$`sydW zRy*aodV8B#=moNy#k32sNm{hsuSr@y^E1f-7AlI8m3|8Y%#8UBm1QCBHad1<7Jy?9NcGy>($ zi{ak_IpJGGCi7ccZ$3eZ<>m)9y=DqdTm))Qxq0;Grwg_~rZ6uLrXkU1`PsT+cM>yd z$09V+wlQh}7ynH#i2I%VocQc%gR%nWW8;0~+aU71v5jHMGbaK@JhniqwdKs&ivSrC z1Q_8x07gE>Sd;(qUSg)?XotC7Zo^%+%S_&UOl^?V3}evRC3B+Xl?{tC6`nS>jwTt0 z?ZJS#QvymQf2uj`9}k$m{dKA;&88=@*MSu9mmF&+ZOU%b<&t&wnhNL@M@#S21B87I zCTI=L)d3_#Qlf$e>+iuWmfM+IgYC>M8s%NRk!tgnZQ)X$qJ(SO7sn2LX)MyGDvKEoAs?L2Zs*`AMR_U!3} z1>`$`Q7B5$miZQ`6mij;xcAg(-^4NSBS?B;mW64QQ#YdQ1<||V!=gaxpd-wwh0(n144bPCfv3VIp?JU2N*H(M4E(riZ5000i-Bo$^};OmFkPZvao)6nGAZKJ?meh`D3#5aLrW?6tbVk))`Epz111XC`_7T?d z%G%TWQ_4<}8W8({R8KR?9XwcUyEegZKQW`hv%u%7(YppY=Q zT}XJ7@4;RNP|qy!OD-<(jPt}~nLXP?vx<5!vU+&H<0xb>QjJ| z<>zL;o>dk=(u;ho+l7<=r>yP{%sUth^HzJ3d!Ea?!98WIF4Y5G{(dQD^vco;sd~R< zgO1fr>fodaPXOmF$&nOEN8X5bp)(_up)eko%HM4kzwgoqmElm;&&S2V4WbDi_oGRr^OaUuOMW7SK(+tfnc3qU(> zTHM)3Z)n=H>1CZN7~1VTd)VId1u%|j7fF-Z6*udov1P+k%!cIiNG{B4Mu=>}T>Zg) z0V=AYqlHe6K$=^@`+G6lfAu=jCrg|=PLtde>Kq}jOwmb9RaAl)$Tf}6y@E96MB;`X zbLTkuWJ(P-;d!*(#0z*}%hrYp>U0K+YXQ^(qW=LzbHN^oQf|6FeUiC+o{M%I(z`+IACS}ZMCTpt zp&3R7($8M7TOOpY5>~G)Gw-B=mOPNIpMQ)66e|BHxsE^N%vIG~J64frstH>|v)X-YT9KyWS?PpqfI$Kfc>#~_)mc70}WZ^MQ z=C=hx=^W(p0XWHNv7F>UxBs*MJxIB%g%$eTUG%MvI59<#+D)8e??K3iSK8x-y?Gkj z7(6OhpXDdXHeHf)Pw#5UODT?#Y}AzKa9x8M6A}`LnV8YTG6A6Xq=!60URPW)D(wh^ z${3cQ19A69i$DM9dU3fpECSFsQVL@3H1^1}0I~`u3OX86ZszZ=w3aZ6PpNgKW=cM9 zj{^(2PWBz{F>i^Q)>vU|YPdLuS_nCh+bkOwq6sHMJq8V`8a9xH9;pZbXsiNmrJK*pRzd!odZI}9%=Q5bWSY!0oF3GHQ z*D-lfqGQbq9?N6)gUHE1fQmz*)}8Xyz3PqW@l|K7o-R{Y1;hg_S?yTR+fzlz+0(LF zBQC5i5aG#kXG`wsW(mPb;M)Hu7*t7cu7U6$KE*BZIC<6BbS4SZFgB_u+Kiifqf`jT zmWgQ#1X^0e62-p*h~jg|2QCA3DnnOjf=~6lU~U8jq@ljj!je9vM4aYX`ufJCIV;K% zY#J;CY~8Q@k%jnSe}88ZBUtKmM+~OF;?~gtpvgI7?crtth|hSDI6n2klpAQZSG=|y ztrhj48QMs|viqtJAGup~@?%}$w!+8#5@AwdP58MJK=;4U! zQu!=?8{*9lv`Q%_O$4&kSupmCo-5R!v@qw69yU>OPw#%e`RHVrmH<#XAlWV*bavQW z41a%&6N~Xy!YW`S9AVHJX~YWhm|ysEam$x&qxe$S1wDn8R5(>6L3mj1u0vU0pio%Z~LF8MMMLo8iB`*MF};Cq2&eDUj0h40nPYn~_XL!E9OKJ}bm@aTCXN8L6A zZ+uL|nJtiwL5S$;;H+rnVt^ww4du2+?4nTJW1X~f zPXIv_jQ!6NgZ|NdKU~rjeHLNov)npYvDrBmg-9JfmpRz}F2GJ|3#3sFDGuImR-qDi z{hp_kC?9<=YG@lxJ}SVfGU6!o)<56NV##b%ajjs)W&OD-itq{+OukK~8uFv2!2eSo zk?8pmS+?3rmmmU%5|MC|a6z?NDtne&D*#q4tAQy5Y533=uLYfGCgA)8o3qu85N#`G zKgbx_7ySs}@;<~Ic=Vn@;KjrM(5VGQofl)kBK!AT-d`#$_`Mt_lHH73md+oGB$rPN zp(D#qd35|imlzBSB$q5cWngM#+Vk#I6-ui!?}L}A3eIR6t5=FkKipWG^p-7<4;Jw* zyKcn@>tncsg^?czVB}_9-*W%?EzU*lw4+GiIYTB-FQ20ND|!4Aprs(MF?=GF(r#vE z^F#^9LiDf^1nWVU9zO(_h;G&BEiWU3qV8rVCbFt3-XzkOL&c{N5Wi;7O4)XwrkB5z zoBC0X1LX(syF>sl645=-%sXv*o>oIiDTRh=e1^BV83cTTN^llkH8V5P8?kOrV`|LWrR@>Kop z>tNe_Y<$w5-JjIrDMM-|r=8ke`SsHWIbZ$)^2StJ z!d!$T$@QnS3EaA+Nhpd(AsLO+z!Ty~^G6QnXZ=0Zq9Ao;o@p}~zvKeXm@DFbNnFUi z?TSGtC6newR*$_SO*_sf0}GHw|ab zG%mE40F)urEI&iAaJ=vq@`PFkgKM1Fly>+%0Cf9+iP)Rze4MUzMpn#Oxf^V^#O06H z(U$|g1fS8xFLJ24O|fhvyl+xpOb6Ith|D!uQCfq*oILtj*U%r1>nKhb93)1=w44HX zP{~37yZc+=0LlB7urM-7&w1QK3gNo>IY+)rrdiWSNV@tE-&kcnKua#1f3MFE-Wt6K zI8k-Xw{UD<^BmMEzb^%-f`5~VE!Ex&5DRr&e_p2bWN`@@&2vaelE<7C(7#x9~ZF^M4<*X z^Y#;04FjCySU@=tiw3LYb#J=j8p#w3^zs?A7J_2!p09{7M!Nm=d_*6iK)pqiXMx6?E!xV(A=4g9 z2a{g&wXgq2IX=qGjD7}QbyB=#m7%t5O|`w(4_h@j5Ny9mSbicq|2J{n5Yc-;bH`_Z z^7`4Vu4Anj&r7eP3KPw6)-xGFqtw;|=)SYq-}-!Gnu_|g+QW_&w7z zfuCvy@Pd{b+4dZcx(m)WoRe-OFWPQ&ou<#zJFK8n4kKWp+J8Qbd&=s7bkY zvx`Euk=`8u(!1`bseXQ;paf@IWH&Mfy1{Tgz?I=`(c_l}7W_KW464DdOMBs_vopW) z+rWgHvuZ|Ls%+r32FmP@pjKT%hCGEiV*wF`UM0~BJku? zgh)N=v6==Rw1hw0*8Fe};=ewafAM?6sVM87(`DW0<<=lk38bL{7KrS856-`?0*p@R#!u4-!-B^a-rt8}#@fK#eYQ`X_Rxxik(+%hl zyzv$^rKGt-K}ew3n)mJrW)_R@wzf8JL&aCOstk>~IGF6{Uoa)^b_o4nxFB5qH$kIB zcND{HeY6Nssod7bvnpL&B&R+0xzZa?2{`p2oR~Hi+)x%O62{I%bZA0J0#J>Oi@vtQ98FRQn3kfJ&-(o3aQ-3Xg zqpfXo zj;y*1_t?M6npiHr+u;2NmS$vKEOmuz19^O-CoY=o6#yb90?GvHzmy5wNVAW%ax0w~ z*7pW`cX8*EKG1j(leVp?`?7ZM-8+_CatP*JXkzqsDJx6BM;1l*?z{J+J#7dQrZ8{A z5d90nfeOFpi(dx#;$MBr(u(B2z8yaXj$WK(h0{jK#j>9U=?RUEG%u`!FZzS~e+qQ> z-%d*Y0h8q~$Ity-R`+9gbZ+`3V%Q9dhMiTJX z_rpgbL{gF4G~RWMbD8pyKp3Qn{j@73{3qsM-wkyZLkH4rB)MU~ zHknHsQZl+vZzKfYVxIGB>7t%s3!Udq1Dq*oD>r4Hb>Pbh;JyJIBUidFwHi{_o$P?@ zb6>p7CtU~Mv)$vY9`-e}WdGfCU-dmIWV zgooDL^b&Z(0~TeL=K)b5#Tf`BHsz4b!q{eaq)OHr@DgHb6RpdpJd z>R)i=uK*nR5#X7>{GW>5-~4wa``D_&m~Zz9CF>c4&ZyjgE6usgim0v?)5`-`gwib# z`9y_^7r@Ntw8I*YoIXy(uN~bvIR~e8^?0DB!4CHB19*rufHq^LE9wmPP8~o{^z)+B z{wyTxuf7kSu6}$Y>0?EHY>aA195CT3&N+Z(o|m;^qX)dhl*6wU_73tPiZ$)EL`>(MEJ7& z_rgrB(p4}zYD+WG8RSZ0#tUd+9J&GD(VM*u=*^jDrB<5PE#QKHuoqyO>@{^ti=RRtp($sH;^XxkV zMh?cB$_?}RnO{l;H-Sv=4qSCN%K3R9*vgkY5 z0!p07Z`8@;v}*xG(l;f~(eVYo!H6QdW3{70Q3nHoNW}t+jg#O{Y>Ri_^B=+|aVJaq zk=!b#6*UJy>Vt&x~$CZpuB)n&1lw6|1BGg)79KBew7 zqqPtOh&lSLv`fjAH5CC4(x&7UMO3_*_gj6zRHpm00XFh!El_nu%_{o5J*48qNw_Jf zgwL70J8tfV27f6u_`fo~+do6f>$Ve=)!w*Tb+(`84WoPlGAFUfTksLXVM)b1`-~t_ z(bu((H_SROj%4INwz?wT5QWm#L8az!hv#gJj2y{c;AzMSbCjep;$yCG%|E$^A%%bb zD6raUTE6{~LwqJGdsOb!i`RQf!8x-qQx%t4vQeP;UT)FKtt~jsTRO~G`7j*Yz5;;) zEPf)o0E=I#_#9qexfjVKxbK;QLNMnjI(-=HM4jjczyIv@4~0GNz0ZE^DF*v9je5))6G`|zdbLx9gFlJg$y8o=@2CnZu~${Z9WM3aa=I!W!v<# z#D*7>+=#Fn#Y+3rdH_R%^rcAIPYm3rhbuDM&lqM|+ZWQq@nv0?p&W zr;DMFcX`pkIHBW|seVTirc9aEKNBK~aPG#sQo;9&fGgDiTxomOx3X(&qj6?S_X?kg zXxKTwiIYZb%gkc=ha51xu(6l)Ii$?m6`uzo$b1Iof~NrgB<9w`T8e_iFGf5DVL z;raS?2RP<4#vzq!!_&9>V6XPEy+~4!e#l*fV_UyKJ?^UQJ+X940IMmRRr#6hthmk+ zrT1+s7g(mfAh6xnzO!r8zApHZU`KE_=9rGc8n)S4miYnIZ-8<)08lOktVJ`>rbFHR z__QOBTd{5wM(y^Q=58j>H-nS^3iFP>!I0l=8l_Mi$)s?JVs1>hvZf0H(`(4YwTe~Z z;H+OliMMAdJ!`$Db#QWK(xRhE{~df=|Kjv7inB8^64X=C!1DU%)h~YbU?kayEJY1B z8PN6!_rG9+`MuC?-L|6cb#i(#fnbfrnQhVC6Hl$u`|kiT=@VlJ0dF$}Kh5wObcddN zaB@J>enC;f#YFFGWsM+rB25iL%Wmt(^8TV9TVk`IedVxfdSaj# zxmkr4wr222X%;aFkPZ=o##vGEx2aeRUF;&xVOuQ>f%MI80rXqU!M4IK%bKCgeEPP9SMK9;P*J$?&BvISb4GOzI2E1?|Nd?sHGTOe?t*)`nx4%|IZwE*2|0^;1_&36j|AybvmP0|0B&6rI>^ z#Q9|-&ObSyMD)msYhA`{mE-e+NK9euCk%L;UoUDK|NqugBO?D-hcmOq{mtpbZwv=mHwk*}${;vzP^ zzA2dlZ*kb*&}=lm)JVCwlL4lk>%cTYANaux`YXJsC7wZr1$d}Jb^KY8%6flyuqmo% zCNlx`JA&gD6A)ji*f+C6Oe}>6EvC`wt0@ggGcy5wryvZIP~Aw+N?eL7L3x>D!33!| zNfM*3YeM40z_o$_rj2!<)7S>CU- zQZtx%r{e`T^JS1RyD)zs%zD@pHl81WFdMKe7@*l$Hvj|V_{i%L)JiAv(i)mi7_m-g z0Yvx`$1NrSmi*Y8vvhoTg$P4va}ya^-6v@eg}*_d%1@DOb2sJDdrZsk(PDR zVQ9y1+Z(x`IFV0cyICbGhUF^P3;HZ=fzqKud+lisfHR&7%NY;N_O9;fHz8z3EQpUM z`Y)30>Wq9JT@-k*4`h#yhV<6ne157L$w#!l&!({JPL~zuNP#rKd*nvil2UV)%7=9b zA4;$-r z=vcm!ZKcgXglVvmqq?H9ISnLdOF8oBM(jyCsZ;~CSpYqPBpU|%xv_Rko@rwF_;U$$ z1_LK2eF-X6;x8YoeCgQdf8%&yL1?@0Mh1o`Iyzx-2|>G~>@htX%+`5*`BWs7EShge zL7|+bM!*`VE&^Og5YaL$keplr3nUNw6-b_m1(IVsvbKTbf4L*;)CbA7hHO;7!L6%^ zzB4to)c%DrCviOZCp46OqO-1!0A6$)1C3*MYz<%i;e?2(DM+n;;g<;}Wp83i%C7PKDLmwOkH+(++l0$>t)|LySpP z>FT&bsQ+s$xsv$aUTKy9!?=347FhMQuE%C$FGu+Zxu>O+wSmr+ffWy!M-tIWm&pB; zy^#+H@WmO=*!Qfr^pW>}>(Y1sE*|%5&OxR*?{CVwg%T$oV%0sy z9!(aoPd-h3{q`gaU9=<8vs@IYVl4o)Nb{^4Z|Lg~HncG)*x6{GVf$ub+A?-+qCHv* zrYZn_9N3!w3H*2pKs98dhQ^A?vayXXAcQ-PNf*Z|;T%SoDUZSKMi(o>6DBTB8Ui|YLef0Q6%5E;!dEb3FEn4RQC^pX2&Y*)(eXv!CGy; zS+5v+dm8pAPt1ITTB>d*eiJ;?VAfbV^0XP2Q8PLBtPg8a+m1CM3455t0$C`W+|Dn#PnXX;g!!be934YsvpWx=IPR?o}Nw7ad$l68J+ zS1Bg~D277GF)6zJmqir3d;z)D&;+wFtdI|e;O(@806~Nsc z|I;X6e6ghEx2Q<{7w4df1$dD@dUjH7&T-D;tjhp8yC{*))4ZawH)pKNZ?#~{K4<~- z5o|Kp5t|G)!E5h;#sySo0De>fZerk*m{wZIi{5v5qEpV*-@8mQ%F!e-=9xchGS2^z zX~zA;`+|%BbO@GhB#WcO!JhqVp$TtF`{3H5(+Kv6wNQSf2L)A{7VJJ*jGdg4Axu$`3FH)C410PT|e?F*G(F zD(hsUJGDy>Qfi$OzlF?M+) zF7NKv|NL(KyO^tChZzn{IU|YfDgwjXl1cy3enI#)Ieu_j`R{?`(Wv9(Y<1;OWE!;n zu#Suwi1O$PT`+pwBEo@nIslsqc3(8Nq7>G!AH4{mF7E07y6~utYdd^z!9? zj3pr*nb}dCEqZ@5s6T()^+e1P*qTdhZG}*NjioKeT{Lx1${|YD{WUiCgZ6BJaI{t3 ziUdd~MonTqk$Cso|Lkt)XC2>R*Z!EL9&0YnwU4z@YvOGH-YAb+#(VpgX~#4zvZjg`_hg;G1Vj^_7DfG>ya0cco(NhumYXvfKSxa9}TY8N1%_ zLTROlZ-xtj!0@&k)$E}diAAS6tFiSphz`so|59VI)0z+6tt}85b*Xh>P~RegvEpHb zNNnf$n@sc;Jq)I&)W@lyc{ad(xcdLu&|jDKzu4B@pa0(1{L}NH_=-g9 z;`@58AP$Ku5=Ub_;AVGD9m|WD3=ZlGURg#ewkFvH5$jhGmRqXKZg= zvcN5@5$-2n!TEx5!~8?`BTwiJCoBlg0I+PMG5V0{)B75W^{`^HXbk-M5gE4Hq)~%g z+E&3SD|3Jp`?DZJ8_{!`Ey-gn0uM(d>wrufmwbVAB{O4Rurkn2Eh`(ueTOHAhs2)) zlm=~S_r87IMR^CUmT>f+p0wTb|Gh=5)qMBHz!`amyn^x?gH>+KdDRAg_J4oYrA|S=++e*e(Cw(@O&85A z(7?%c)8BuW7;LFrw0n@>&Cu?g?5>mDXJ^-*0LF6H6zrOUT~n}Y3U*Dw?(Wg6c6Ud; z8)abq;qI>Be`Z&}HelXvPLU}~#bQ=(gio>BMVMhR`h1LarNzEkHPOEoRT0N&v#jH{ H_4@w-$d@%u diff --git a/tests/assets/hlabel_classification/images/test/6.jpg b/tests/assets/hlabel_classification/images/test/6.jpg deleted file mode 100644 index 3a179941df5ad9c0308784e04ff0ecd6fcfdde67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143396 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

      A6MerU%+8s}WN3pc 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�At492Ap1n?Nc2Q!dj?846`(r-?~i(N_G0}(V)9?$2!_0NmPa}_^W zNUbV#l@B3e;qc_&47eQ$Ta95x_@*N3!lKSYaUzB|Qn)MO1g(9~Pl$Z{d&_0Z;?Ga! zxcCf&+;cEpRG82<$y4jA(nuDm&nw@X#gB6%&+6*`O zBnAyxE7{+A6gjPahHwI~WrTUEeI^gT$h=c}NWS#EW4XG*5fEbz$ec!pcjijgLlD|M%%_eU#V+PU#SC|_ zN1a;$$~m=v*}30&x2$=L;Y+k^DO5&*F*b(+P|`5$=yNu$UH zy+a(ZkE68enMmDDT9JxbD~YwPV5x!n6fKjtjE1P;QQ>E#{<+Sc#z2p<|VA0EAobx^)dxG_i$_;%mF5-epE-4OV?b#ck)G98== z@t@6gPeK&id4`CYx8l7*-}s$*%Lsn6$qBAR5{~5#g4{P!{L-a=rU9llfAi5;S(&$0 zHC%Bw%OCmD{|bHAAJpuxt!vrppl&YXYIex0^ol|-)4Q)=WzPAfo`VP~p6MBc%w=mM zIh?Ll{%8zPj0SbN;~p(537P@Qu`?`qa2<->$03eTk>FJ-%$$K0RKnGdI9CDpeEX;+ zXgwyYTTkz|IC$!t#jYUB)SQwD3w?IWQJH9_Pqmxu9Aq}V$kK~B7_=MWah*$1R*4vI z`n@2@J+_~7?jP9Z|1Z?}jc-c7hU;C(;#OVW8~rm|LjSwpo7pXyKY5<+fBzl-C(ld4 zILOk~zxi$N-y8jtN9yNubuMr}lR_gmB8Gf9@;6nXWMtUSZ`k z{Qk^X(1+M=m)ExL!xaAjd!fdsj-fsc%MwAZBDz=UPY+UBj2ZbjmQjU~%0jdS8oKO* zXIwg5UE=QpEyg=<6qy6cV{e2d2U&X8NFo){U*BpxhX!Pxg4-hOIXx zCdp&z>`5X2jB#k{xA04E3C)Xq-m#sW^ z?)$pVbAYm|cyX;Rt)r|{BH_8DiDx#D>@oA?9Rj_n`1r|3QE{e+0og;GH^cmJ?cGIHx@KhW$UcfDVnf|k`fGUu zY8agA%$yYwFUQHrzyrHUOs*>gzdf7b1SJWX)NYB&G{EstPWh)gRsizZfRddId2t%I ziF`?Dh*mKlQ>F<#TTmZ|uPzq9)lbWX+t1I%lsYasqZ+1~)J?}npjBdzVp=VC_}kvA z`D8?k+x9*Q*kqRi-nzxp(8&nr(Y#Q zuCPDNmsCF$Ex32%bAXmz-1Z06bS!EX0{tCQkPa_)9TBsKWX^erq>Yx3DP&lia#IP9F2xKJ-lK5g@ zr8t7dMe+FyeZa5Dew{|gn;ZM4^{miY-Kc|7N|t6nN}- zf30HUtf9kKw9~`};LXodsT&WCRIloPj1`|K4TaWtvX4=>jHqG6Lc*2Y%rJdl8~EPD zdRNx*m|CD(-ei8r2<_$|;uKe`|5eMBlDIfi2^{6Hy zuH&9m22qLk4VaRBWV_zY>ak#X!T;BuzEbH6L5NL(bUl_U%IPomq8#*;*xkKDPKiRU z2WGOb9F?mh>`R=)iNYMa(X>)TSB`ZIb$7kC=h0EZbgsz=aUjpL*6tC&L){S_l*<>_ zc4hzs3J%YS(mlZ2E~}TJgZoMpI)X#PEC(+R$niM~In?pwkUU|@&ZbkK@JDL)76RKO zJXKxWDxZYu4eq9n1P{~PY)>05L|7ytM{y*aZ97ChHNpxo zV7kE3X`%yxmK1o(Z<)%hvrybXBbckVfp93Q&+0(vl>(UhZ^i@O-&r_+2B%g@q&Sm- z=py+AP?f(n>{q~jS#(0WSN+N*9-83_x-%Uq1M8|jN+A3Mh~+W-pOCyRvvD#-?9u6WyS6qN+y;x@FM#Q z_3d}}H36k@_^GgXWe_D_j-aABB*8|LeEwsB<3H*bG>Q505k93nIMkj^P_ zbH6`j(l?hrIa(+M2~yr`Tc~aniU!P0w#Mrx4);jAGsUa4@wwp+FYdLh+-u!{(rTEGd5aJCC7w z!rdQS{FI?F*~ymI^^T)t^;8iDGkKh+^GyjxE**2_@fPJ^w1fc9y-n2Vi}VqN3S{TwNU-nO7xN=8 zKOhE9?v{Hf=DFQY4(j(=5G@JrSHc(zNhL5W;B`xbW+>4ztD6lPu5Dak6S3YSb3CUn zUko@3ph=t#)kL^_=4nzjmEe}OYIp2yv4rzo8!jG6%0qD5pWCL^S$YXt9>-d9f=41p z<}50_=1$Yp14%PH4lw0_6ArNRyfSgDgm}92TSVf4W4pqsx3;DPDL8yuSE_(@0G=bX$UXqMtbYVSh53&zO|I&%8JY1Hfq@S~pPh$u@WH0dsbj!*<^%S6@WAKKhsbXu;6A`y!k54E?JKzCmYXA3)hJzi?J&E7; z3R)m#fakqy&k|0mx3}6SNnC{_)U&Hcpq_;#okS0iWB)6^YMz2W`(SR+1cF*%ajfAO zv6R~fdSuQK*L(Lh%tOLV4YSyy2jF{Svkba^v`#1b5uCuWWF_k!0bYiw!XtrxzyO1n?$Wj#(YjC(o-0X7*OTSus{udPglVybX>o1+S&EU3i#8=XA}N*m8Lce-`V9l} zJ57ZBN7g&PDtEeyuEcXwixtGVQOe;-vk437Y|#RkxRqGg0@=#r!#Yh`78$=o_Cvx& zzGi`WlU#?ZtHuP2LJHPar@lAda?IJvC<3HsI#*At7jX{Zt=hZ`osm-U9bWiHEwjSV|_5A6;)}zo^S24@YoU7xY^r>6u2aP|?-^CBk)&~~n zDt1DhvyhTI6#ofpFSh6nvyA44K|mj`tdt z&0&N?MkJh5R$qmolh4E4J3CIBN&JxKNiJDD>V3&qD<-QD_o0Y+bxgPm+UFIrO^rAS z=E3Dp!ESb`l7BG#;O14)# zUC_&ZuHPfG!l3pYP*B>+ZNcbO7pf}dkg*o{%&KGr;2hJnJdX25_fDFDu@7WEdR zu7(B}qM;M%IY?>A?8QE>zwuUf1;v?o8Kw0vGl9*C>GKwE5b$@xCOkO$)@&ngtp5RF zkR^2JkDJ{^bx4*I~S8)mFw4pT5=$OV8sP*qVw^FsFx&iTHTMUOPE|5Xt-!FLDJjNGK zt!_Z=Ew*KvbiKYn1mN11+X&x#)`IvBKD|<&3h%Dx5#@;Emn356#d(R=GU`5?u_ONu z1@j-dFqIt68{l7YtN+L1R=Mhb@BYJu8-45aKyw<~oV(q(A{Pz3y>sEQhMsam3e)+X z7R*4h$~u7<-||RxKGkD%7in>PEFB3?ldWMtoEM?w$J}`gtIdw$3 zad=e4*cEnfq$m?#)$#OMChob}^&^Rh`s?F59L--WNBDTTa0R!esnT zP2{&wlZZ0(wXZYj>`s*tlybS(4-2N1R`4%6+~WLE-Wr6oP`X|%yF7OD3yFb{dIszw z3)JRVduuwBMn#Q6bwFivqgnSV^B;`)*dgt7jH0Pqy+t_ybi)6}*p~M&9o|jcdzVbMQRDUB`$wTfB2y_Z107lF5MTVqT-ES!3ppX`<$Nn@1k z4blqPwY|1tYkbH`*Q@Y0s!KE>M4l5?sPaS-GwyN0`OZmfFeLg@8v{#mUO1!UYSrw!dR>e&r@L;9)Vt@-IQ_G+r)}y!l zDz=!Nx90zFU0kJDo1Ro?O+Nq^e`-p}b8c6LaOX!Px;X{aT@E}tNo|^$S3Ww{vtm(& zQ{z4NjF@uSMmz*^M=8gYUd=DvY<=I< zTN5$kY8^U(!C;7h0bR1USw-NtE7n$0A+RW-ay8w^uL&?08SB0o;!W5Of^rPeP``1r zR_C=eO{TlLNSKUHm5&fJQq0y#6ek3~D@(j32NF1~T=!S6H3<_q+0h7DZbD} z;q|~K<4;FUNAz@&6-+e&6ZXUARTMj{bj&Um@81<)Mf_^5PuM8c4(NY)i~<)YzFD8~ z&#}LpJz_agJ_PZ(Mv_@-4Hu{vc``Rk?JtHJZnniOgzyEB*1$3^6F_^KwY63{`=g%N zg+y3{{aUcRU;C@x4H{mWM<>J=mb$2UOC?a3cI>?sS(-{d(YD>AFaa;jujpAc*;F9x z1RXClrs;)5U&JnJHNRK=#%)Z!)$s7ecJBMl^h4)$zpo9E>gpah17#io5^>$Z0zqNe z=6(vF-$t10sFk>!DA;!DGRbicw9hUPTXF7Dv|dh5sLDkHEI0(3S;&tMRxg}t3{FZ) z*Tq)CaSx_H*jt20eGG(pj@Kr4E?TuOp0Bs-&IBX2(LoD_EvN1jfGCgC6_H2?Fp4IG zH(6p+mzP2=gG1JbRAkX(%8_utAf2dSPTy7H5l=m zy4F6d|D7O7Te`??OCNLC!)izR_6K2rp``A{C!f=_osce#@7PtU&jD)ZILZIkeV#T? z!IbvOoUjFob=85zun6KYcN;QfgV3Po?K*XBtfbuhwH3)l4^_-(3^^AjEnBn?sz*xL<&rWK1H<_@@DI zr%-L|y-PAa&0G4YVVJVlgFGRx4pnt5g3ot4`0z8$MqO5egF}CQMx?iRwyOJ){QNz; zY?MKQ2Hp`h*_+f&&emC|n}p+(JY;IBrNN~-@1z8~6pokwO1<5`}exY(s14|1OHDNQ*!hRKB?d(zRYJ*_;g z5Cyezyn2B72ba88yv~@Tv`CX+qO#A#!8|7WCd0K@6j!Rkyg7orh6d*mLzcTxn)c@9 z1vEm-F^NIb>bWbP3wh$FBH=S31{9-l{n@toKF3vlc)}-wa?+L(Qqrc5_-NXsOF40M zO3dI2H>(h zW&R$p<1t>W0pCm1sUHUEDIT9CF?cA0(y}ldn|s$m&GzmCZmrJ@U1cBzut0-h8Fy29+*-|1H0C}BeP-Ad(=Um+Gu#Q73^+V{4^{GS-Y_t|gzw^-3l zcAqYfQ#x#7wjZmM!=0vVu-8_1RB28=d1!T6lQKeNPoMmUS~rW+wIT+B+pfffml^b^ z+0S)(N7$av&T-{giy6>9=OJsy}bJHl-$LDB5CFBLUmh%`BJ>2cgYCA!Dy)O zlk7L^s6s9lng*TwJ@uFVf{rBoVz$UU-{U-nvSXOzP{4|FLAp2-Q?9VsA{-6q6rN$= ze?8N-UwKxvm{ZI--gsYrIuIov5T>KI;&zYn&Q)gcBS~3J5~@hQF{Ejc?jU+L(xNvV zu|r5N$MNf7%4VC#DzW)95P5bHB;Ve?yEcc2RXMR%Qd_{VY+tOn<~V0df+U-K=xDe+ z405KVuVw-(R8{*~4|Fd8i0SsSb5~}0BkRIe7w`#n(xkvHrVnJRECK^KNPidNUtY^0fAG6ZRMQ+6!4oM#^2fsPqqwPf8E)lK6{lO(AGITqv%*Hk9vaeb95#qlp zgcvh!d~dLEQA682xX0gZRhssD5^f9ZkUTFKENX^ZoV)05b_RdWWhxbXdvERelWG1V z#r!oHa)}k|1wzF4s*T?gnX!LLRh2|idP*h3;mdb3XJ4dnH>m%RqL#vn>l=ENYQk0v zwG@Ilc_92zx})GrhV3#Rf+s2;dWl^B6OqDDe)g=-fPo6WD$+%4KhMx&hbtJemN#{a zu|@Y&szgP>`Hw$hUM!9E^qSUd`SZ6z4Sp!4r!QzHPV1KW6uu2uu`w96z|b+Qa?^8A ztiiDDv8F53pa}EG`C>RF>%=g~>cQFw%}8k#pHnTZb+mJ=#wuQ>tEI3m_F7cswJ=r? zzTI))lZ_zEP#MP(+5&L?9gtEJM5t>+&oPGLX%7~ zetN?)rc@E&r0Z?Fe9Hl-?KHd@Emkx}aj_1@mb-}ATLcY#u1EUn2i&e17LcN)lGefM z%d8|0|M6Zy9QrxMkF8YA3w7-V#9VVpvifTHwK0{cC8Pa&Rz;iGx8SsoI)DEL&ZZJT z$|p7+$doXQ-ZH$^OcoWqpAegW&ntNoi=+T$fejf*Sc5Kxu#~wPL{;$x!NFe3Xb*5D zSfTK)mE1yIg%T?3V(fPLr_?!U0bH2b_2bq;P)64V)a%bHpy7gE&)}CvV9s~|JgNha z9sek*>7+3(*SrEQn)>g}7n*$2D6%aJ~- z&?D3}s7D#Y9ou#LSn!-e0As7+N1<(a;L1Wntbv*3jgqZ}NCh3LTkf4OB6a7s>2?U@ zV@#uerR-2pF%+V-L&HQv)moGYHDr==-N{X)zbh)WNI(R36{4%)<6roL5-=QS*o+@M6)$JY`olOYd5 z$5dAjT_ihJdQl^-KlCw%C8hrIAGM&aosTAXn%uA+%sBNd#7-7KTgnxxc-g7pzN`@K{JwP>sFxUoN=*;v5y}|2iuE zKXz1%Jd1sLjNK;I-4LkE7rQT|ZPd*1#4yp?zvkj6iDT6|*yBvsv@uJjYc3c&dvaX2 zUcOLCZMiPyj%`86`r>8Eic9=sP)?X$3GGivLDaCS2Aej4FWf~32R9EA=CytFGUur) zCK6 zQt9R=n_fT|{Iye5o=+w|e05WBtfa$C3FR;cpuEv^thHq3g}3|fG+1kanoqkZ(-g@x zc~@t&lLhtd1LQQ|Ii*^V5MZBKFvlO1sm@+a|K$8ZdX2ExBySr-b>85;09ZUG!^pRm za-9!!@wNTNYY<`IDy@&Ez*g(OooR8nzaEFm^BF0#SDU;I6E(Wlv9gHb|iqC_1IJsh8bkj zC~oc%j`NmoeJM~>Q=V}>!344D*|q}Y zKjY(1bt(JON4MTuG8AS5>;K`p0@JzW1?L)8>9opzl^eCtF;u{H{jm+NX7DTDY&0uC zClhqO^I&sA6g0SQlpvfwqd@;sax2`J_74~Lu8q>GoO1kS`!(3Xfk@ni!Ic1--G_uR z(r_HS=y;xALwXnnLoolEKYp{ekYOqf#CbwyUes}Oie&(KJ%Ai=ceY^oJ_iiz>IsuT za-)gcMY{D5-!!M>+4!-GryUH-kj*L9^A6_z12sfT(%ilt(?FmtyjT_;XTnr-OL0)y zWbJN|nFfMe%J)417m7{TH4h733R$W-C4|B5hkf+PnD?-Gx>XBwOl^vzrYVfN&?SY6 zo^6!P{!O|Fa2<`>fLfx1zEllFIN8dOP8on@fTOy~O~cQOVRyH_bggUJ6!A-5cd+L_ zy5&w+NU?jm{=msB>$mBQa46@7JdOQS?5CHlI{^4nM1YW3pX)L(N`xN`j3;uX=Djzv z>y`6(%R7oopOpFa2;K5Dq(pnQREv>}{^0_+wPfx&JC2n3<*9sreAMPNW}6u;iT-0Q zi#h&!vGBZ|#^NpCTcR^ktn|gZhZ9`E4HmchK2t0pqPh&r(&#V*-?wZ)ee|4?mt>W2 zV4`; zWj;;9cH8pngtsW4-rzMnh%lV~W`8_yxqSl#c70egCn>w|rSBc*_?B61Jr4WLr$^?z znRNmQjTW_Laa?a*kW?RLpvgYz&a*Z0N3QMm%8nzvg!yX?`TH~>F-EX)aEo!BsSI4NioL67DfrhL zW2cp!G~c@ypk2_H8=}@%!qW-ukdGJ+^@xAUY7`W>R|05=4id`(U{w;KNpK7h+I^XF zD?l)Zfj?j2yK5QT5!FC|JdjQ|Lv0?JAF>&d*y-dnW80vP+f6Q+`L`b(t+CLb05!d`zuf$MV$bq1tR4d1Yg&&>_UuETq1v8ouTZBaJUyI4yrQl7SARG8otKXN*X@+FFS-Y9CM`lzjjOiukDsPg$SHs-_T3swM#OMC-Ttyi%0xga!P%_drpDFnLPD?hryjZO@k_;T+|nrE(Pa%)nq5o z(fkHDg9q8&(zO{1dc~3rlAAtg`$xCJFdl0IG_lr;HYwPlC%pW0to21`PMi=g;kmY{ zsE3f_c|IF0jSw8qQe}yG3X0+AorJE@qon9WFiiC}1D2jD^thN704A2<-ipu}tSyBf zC!-NGuc$y7rE5)5fzr{R$pIzqB=Len}djU@`**8`+81a!-OHMAzrV z0cY@z?{Fohp*3v`-{~L@z~K3r44VM4>`yLmcv_hu@s5&mc3pv+M2DY+fP?d?87Y@Q z*#!zbEg$>#299=q_-7=ew|*@XRGhZf%uVSwKf_y2m(wXjeV%QU^5?HC95>IJ6fr2w z5sLj* z46o?SY5Y2j9}nW`^%2}_AM=BA9|K%ElV$d2ln^eZK?AEPnHcjoLms51y1m2GI=keY z# zRd^!r9HaKWJrv~_jh+pWpq_go+di2E=EA{tn4E$H{=N@xJ%u(efsM0&4upcz!d%xY z!vaI^2guG>IS*_t+rn_hQa8C+ZCIceXQ&q1|ACcRuc?cf!n?vrxT}ec`IW|aK~N8N^w_< z_S`Up@RG}Lw?`KTaq%!+57JTJvE+XSb2UUlZTZwW0lZ}RZDvl_duyl)-WL}uvrU5oB7i|$-$=U#Kg`CNHDOJTBo zVZ(4(cp$qpFjzWTWD|2qZS}x*Qfgm0;WFQE$B2P?(kFu)MwmvLk0Q-&`o*G_JwJWq zfpTgtH)W8m+N>ku<^FOd)~qJ<=aXtF@79FNO=h2#U02CD{YP1gA<*7i3_EqyRnzP8 z6ExT1N8Q=vuZ>Up|7i52yhiN#$(L&dO58@_(B}6|8L?LxHcFPx&-LR3Kg3^-Thsw+ zgb36d8vCDlCp@Liu#!F~3!RMQ6*?z)ye8@=tUj5W&`VibhTVC6f}d$uH^eJUuohX7 z8sp{Z*rzS!Fi~^!8vNip%jX7oNL6b!jS>+Ull?r$->gHLv5={M^> zU_Ia`%lUyTiPbbK83|cL+v^CMWgD%a5xMPM%n9$~AMBtK3uB!Z05W@GyqTX@O5_6m zn4+I2PX?PT>r5+jXPDW!krG|L6@D{lwP5(VjVh+!Z^UNn`D9(0_tLZsbF3)EM>Zxo zcK)(B&li#9tBN=ohY>NCY7bd<+u&%lE*ad%Zl22@?iY7K=SecW#}StbtyxvJFo*Oq z$I?uolTyfv8>K>;yzHQ`sIWM!7qv|f`Vc?KF`iqy{Ib)K1D8Lt`jN(ndT^FlfW3t} z14GE#BEVYrYXVGkL~%b7BW5Y9{o^sUTi#U)9RieTIc&aFTaumWwjAheAJX@M?h{X! zuM-Jig>_|DnJ1k*noMtfVP@;C!_+P+rc32GG-_|($CcMe&NRYBHbN$N8$^uI1(kq!U%2a%hi^3 zhUX>N+!Wl2i8LBaLWQW8uom@#!!CRh$6Ccoxk9}i(#bK-iBw;>_QK}ehw(&XePP7HbgMCK9lNuPIS28>+A+kRTO&Hq5542zC&!r@154BW9BOY zGX*h`gFLF-y-l6`P(M~B%KC-3jdN@na@uZ!Nf|SBN0(I#B?Rc+lcz=OiM!rMi4-WG zeENKZtF&Cjs}@S=S>l6|_-COw-8)FzA*a>kjJt&nL^7bCSDJk7HRU16R z?cmj?iYbuqUczxG^Mh@=nztcJMR17B&!=VF(T+B(L3F!y)2g|hrt))HQ@DQD=Uas5 z*UWOi`~NXP{Iw|ln-LlLANW{5MNfbC{g;>X|1T;0Pma?6SzgXK30eWgzmQD21A6j- z*tb2UGqy;Gdm^H#N0!vG@mDtW{S})EfMArY^(Y@*TF88DY1;NmH&3j`Kg7|8R>ro; zAwGbkWmX$d2Y3sV2Dia84rsnk@$>bzC+X(vioS7;8-Gb0MA%$^?1=Q0-oL~X=2R5m zrQCZ~72;2L#@au2lqU(O{RpnU)X=gLEX^k~~K&PedS^*~GLk73)5k$&y%FS`lHg%wr}noI`#Z z7fwgTzhY6oODJPK4e>l5q?lfAE~VX<*zND#9)=M!XZIwsuWjUEVMIpu?((sxVW%$w zSlD>|goLeF+RNj+Z!AYoCAp>JQhC@NX;iyQJs)#d8Ua@=8x6(JjEDFRG$C@OisQ$D ze3anvOi+eb*4am|-&tC$IBtGykE)j1bsvlfNfsk)eX-Fi)^xDkE;L-&vB;_ez}l}V z_mndA?RRLB%hs*g8UbBS&st}HcW?>5`y4E908}8+pJP=|VPCeSnwUu>wSd5k`FHYM30-rc7X+Okl+k$l8z$mX|BG;^+s?7AF%I{FabW5@96J z2s4Fj@*+>!gRksGy3X)fHQbXg)NQoGEchfkU4Z_DD%hMZ40GKY6BT@_Oyn(n`n2#z zT6{WMd$qM=24C?cP4OrfM(2}QsMF_$Yc0b+MvfrQXG?k zRoX`A)GPGOR=(T2&lYsZWb4_g;h~7->@4?>e=iTcX?e9s4Y2!KoS5xR_%zM6J2x$i zw8*{uc8PXkP$rpk(47wjeAKrGL6kD%=GqX$<{~P!yte!#Zw?e7 z0+tvtLA<9%?N?7A_c4cQDBMDanSk^6k~(ytl};#gX03ipv*Nym_z#5#y{L&!$*`@A zh;K3kr6+-&gmwj1k*U?D938%cHA67&MXbv&p4hEOF+nF zuLbW+O>^O5^}03_4ZJpD-me&-*I2$e0D4$~xkPpW-8GVMRYkQ%w1s&S-?T@c(h9zF zw`;TOg}mh&z4w>QjVW!|vk_z@1Z{fmK45C^$~g;ew~eg0XA97}N1Xx*L^+{)xADjs8pJVozo-rw^>75JlSH40g=Vy;8T6F1BZ)2`yuvJ0G81E1p{a_n8 zwc3^vrc&r))K&jcp5}{+Q*dza6V35Dp6-SRT2nEN7S+dO^ZKJd)4=CJFt=NxaY!~D z6o)ZL&AR}9HYazx;?T|qL@$Zj!YkiXh8ess)ob{A>9oLh8<(m!Vz3)aVmy3YFr%VJ z-Zo%(I87SKE=-1pU`c^TOfVu2kfSb1x z<`GsU=}OF@)EA5SwCWVRJcHr{3NR4i87c@|6+o~2vK zrA>ulXZS{$`2L&%S;0|r*%Z}a@zSSp#UIsSf zj4-{buif9MsJ$Grb;@_`CBw4>o^g2B&Vdoc3wN3r7PWAa^G@2g3u;e z=6wcSnW8jeY!#YHF0-$Ics0GJOrG+%gq?a)rYI{z<{Q(BY_JE7_=-#+C~Hq%qk?7g z0frR?G)F$K`gp{Tl%jFS69>u`AWP|gBYS(V@p4e`DW?VnnDP0)*&zOZ`n!exx$qBH z*4qz1cmCmO6=dT({=J_4`qhj-ia*M0&%WXyGrE}1{*(LlKf7T6Pd{^-gJGxyg9c0U zRuH6P;%1Zb4zDMVIAN}{W7SMzNv$Ol4CfPonfkQu*ATLG8%C_%x*1vvk*?^9Zc~7O z*yKwW0gj+Sb7+1J7Y^nnY%5)TLaWNCx)GSc!0Y`0i`y8Y8=j82FisCq58nm()!a*x zU9j0qwqV%dZ}Mve1R(s_9g-AR`D_H~^@zkiX|^T5q->!=HW4{+Y)YWvfz)M+PqHY> z=^4!PINh?viyC{g7f1CQR$$k-1GyOL0b77H33l0aLUIQm+_yTPr0>UWZIo-YxxnA5 zzV+6=KW%G(bofz!{+K_9@C+RphvT*KAh7h6*Eh8IjB8dV+YXjbt9(s#jv7*QGWwdF19iCp%`%D)CA+2Yn2yuoUO@3 z#hS`=pYE?y)D~x83eRe4z}%y-i6rdQc85ktd-MK7x>{y&{i_0+$(uNWA;UfYy#ly2 z(v=I?yqvULcl9yU&`9k_X3CP9t7$vZA+heAa&emD`??>VvGp^6w}{xpuG&tH-U=W^ z<_l1Ok@?_s`HG`awR62lj!-R-BbBKkRV7GfKR37GI#)|X+CaGSXGH^XFnK|))J?(E zJYu1<5oJAQ&FExVj?=dv+a1>uXudC-2iOUERxf^6ynv3peAzQ;zUvl64*2H$!nL;2 z<2iYByI{!E?fRG^(u>Faap3k+hb<_V`8Dvzu49Snq8Qb_>?cM$I$SbJ@YehSoYGYlZ%+bey=C64D|BlRdNVzo~xhCV9j^YODgUA ziBmnk{(LY+-I%Y5OmKmBP1`4>rUaV+=A96Q?XDOhrMo>UzC)%ES5>D2f4p2A0dDxMNq^U(8LtAH5aN)U}26e-0#f_(R8*cJJ7pTY}iJq{df^O%i2a>;Ay+iB}h+V zZILT^SICgTprg2eUc?g9>5v;cY|hla@|uTCHR$>#2lhJ%Jw|Q4mf?E9KYH##I%;(P zevKxV2;s4q6m?WmVhL}#HpZ|&B94^|D^wt#k$7Gs7$ULDel}kk)692DiV;vbdDFsK zc5T&1+%f)Z>cZMMKhtL#XB!Gqeq1v7LLApvEnO?LQ2d3Ght-Ar3UGGv>n;Z^pPVSY zBup$&V3!#QfEYdRB{r^vTlbENt`|(L8LfM=d&hz=HCc{5viWgHScVLH1};pxhPmA? zs5}^kIkvV53@Y@f+*N`GA5`VQW=w6|jLr??L@YN|bQ}Hf?DLAyB0aQzche9dmI@wO zwWycxQP-c_((Nt1> zX7{1j_b)6|bd_G9nUGoyGp|Kl3sZ71&{rXR*82>9m#Jes zO=nB+nf}+jkmVbmZ=IvHe$X%%y+iPN3SPgT^CQ#lET0(%v0w0*NxYSckwrHrbIykbN)7VRsp@C}GBmF0UQ4JlJ$pe}8kl(Zkb2W}S z%ZoRF+z;>zDRG3gBQ50T7i>Xo9F{UXGLJbAt(pe#Bdm&>-CfsA&1xuzS2v zANAea?J=;3i%*xbc_Et;($_n1OEEy11JzB4}1R^||_AeIee@pi|dYt{&_diem;cEQrmwcmg?+x+)Ko2|o{*ryQ zWY6;tR}}%AzWVQ(_y0G3{vv+tGR3JvdJ{FrS$c>OKCYJRem#m20o7G= z0}jmV1y3yRO8kI>{E;0^533y~Uu?Y0b8b8s{_WQ(7>Xk`r++5zXy=`utMeP)d!y*+ z17JSW(O?9>=+@n)8InDUO- zm1DaPgBm`Hsi1E9( zzgafv7h_0n;L=%5`t`$lv?}Tisr247nX7Tk81w6eO~Kmb?d&Se_PLzanZX>NX-i`{ z3~{dXMvZ;ma;t9y;Jm9G@FQG0<$Zo{I1YB*vfQKF0494|1VkYw7G%W{<&A4e3abu+ zT{-EffKq^m1q18T0hX`t@R)nCOL+mc5M(FXNt8%b%)1_?S%OXk4v<%mSxD9TAjETQ z2C80JaLl+|b9d*sU(}Fzif%}7l}MD-XGSAgGEo2qWLCtS?}&p(_c5k*qE5s@F^<$) zkjImK(l+5U<*ww**`{l~KUtXw%_$XW9X z$iX(bTXuo=cbnXN`A?rFIXKQ}<4eGh@HN!bv{X=0ts<1SUse~ zW#jE!aqyMZc)H&Xym0=&CDHm6`NR??2VJ6eJl9hYK#gs*)bllDEr$PK4p4WEv+`^- zo${u0Bj`_dYsc_Mc;m#n<|Mb`TRGFP*R1A`u3EmOM$MT+tmK^e5Wg&%M_f2D%h)9< znjEU5vYi)oCSwWGa4c_f)$oJm=i^5LNl!l%3F`axZNTQ#edcox7}~Rs=V0xq&Me%k zSBS3g(!zo2P{I}LoMuK<<-KcRo0+(n&69$dGpVTq3>E(J!R;C;lB5iG4d6Uvws}yY zI_INq#avV&WTPSlrg&;Q5AkTCCA_W-b&oy*HMKYK_ND1191c>giyD=5rD?kKrYl#~ zT=Xc!+T?HQ*6}hAY-jvB3Y?c-$*3!4fBMo|{Efr7WLhd&75{*~f1>tr?rl`5*IqR} zhW@8OC$C;g31I{gr~^D+`)X_g!}LC{fn7bvjK)HleZc$1#I|UOlew>i9X#a_khcl8 z_H*pc;wuXXzBpI?AZ>C#4|z3!+?T*7DO6U5y?H}g&B5gb{2L!%$Qt%`iRE^yC;#l` z_!`iuq2R{5zO)oREAfm3)JjS2el+)Nn*zcQY#x2COs~vamnP+DaUzbTl}&5U#F-o4 z-@ZV=NWE}o8zAWI@Y9ESBMWWb#a1^@-H6-unj!UF(+so5DCO>XDzDrzqu&VVQR?YE zPCqyD*k$Ioj)$P{=_m9xXP5{@gu=&3=v}_>48366NYMhfB#FT#xEbF6a0QSeRk6j{ zuTXgr*0N*7_M8vfAt*yEnaP$^h)l(pL);el403ra$lQ)4=PJuHRc1hWVHSx5a}E~ zG%ziTPM?ca%cNu9M>=(SwdJJe0A|EE zT5r!}NA*eE*dZ?#ta*%S1sy_^)dNgkH|tW%8Y7@y58n-SGfYD)LU7VGKR)7^jc02j zCjS?EZyL>J!>)gutESS5xoR#k)R3sT<{@T@AcmSrVy22}i=xClmQXcBM2OZ9q-Yf} zRkVhvv9}s;RUIg;PWM0i+y8mj-tYeSzH9I2!?lttD=S&+`j8ygc^=2_pp7SvqsoG0 zioIm$XZpAnTfOpR+QN*f)eM*`VUQNHocuO`Yd@@Bp1CEr?S_f_f@xh6U3|${hbGri zVWrTC?>(v_MKnz`$ClsA-vx1+TvBfuS?3F_9Mx${qU%muPIj(P^Xb+@g1^Wh+%8F= zCb8{|>Bo(M@j#w45K170s%@va&Q&uLztPjk=Q6K`k~40EQnD>kZ>j~9)qZ^CIr2u; zC6~)+s)R4##M0Pb_+bVq{i_bV&gD0Q*0Iwu6yC+9`8JuT_$T10Dcnn{0Vw^~$lQGH zk^8FGRyW0mT;Iw`j8^+t7Ox*K^GL&HHDVPjXDrloh&d+Uf09*KacxdTL)bK%4jqt} z5)ICDk9PK4VL*9si_m(=4vx*gV(Gxs+_18Kx?by^$JO(NtFRiR^F2l zp)kDvCS%&yb_}3-G)*g$=KNRdpOmV_4auXcoJ=-4sd({JZ@X@b+0NyaaRW>lxA-kh zWu5p<%oMb5IG<m7m4C8=Jl`OM3uLxW;Nt3epYsv;R*nC zXVo48Uv5r`koazjaLl9=KH^}wTBCRDFb~A2L(DJYwU^_1sC2pZTINrtW zQq)QnCI=f}^JINCap;lGsiXG)+vtwruhaOqF8p7n3r~tUfM?Z}-Ug$T_$(5ylx}YW zC%+Thw&>X=#Gjg|aQ-bBsS>CXuuR4bn_T!#xgMJ}Z&x)%X+ea!ML zwoD*QZnS>bsgj9ypVa0q^|&dPN9s5S2#4;rDDx}36d|j(x+^2fZaU5l+fgKf+sM6F zX31YyOma86tQRhuHSWPT*nu4HS2O7wtf3p7ap`_iXLQ=0$vY|+A!HLu{bF=bmA|@o z!R$l7#sKF(UPR42Rh0%NFoyA-*yQm1t|iM&h((#40WZq#d2#MJEO66}H;$hH?7a{+7NH8;5qPhX6GABDn{IytMO zqEJ~b7+*UQ4Wi+Cp9A-o@dX}ehOuOgWW@z0r39+EaJ%Xqe-sP1pEvHxi+Gw>3 z5jzGb^`5Dp=QWfr9%$^lLc_f3#el}W{2TfQlwQ-XkVW-g%W#j_NtR8b9D!Sw%7k9i zW3vx_k9P2~sg(TpE;pO0f!CLaqt=(eNv^-E(c->*Wy61sm``bcvFF_MfpYszD|I*u zwxkS$=gx6=6Hd=Wpf-MiLJO^fexqLJi<0Ex6x8kAFw>u=wjGpb?mj7g!BwrT)V+XI zsY-4?b>-ZA;83)2TDtQ%bQ!c0O+JVCbkMKY$(Qz2Rd#qH^c{xvJ8V4kX2vh0mh%S9 z_q?~9R3P!=yQ? zy2O#AAk&}uIfny#5c36CQK7Ix1v2kpui$I8H&rpr#0Z*3-MZW^==&Sr9pVsh;2B4> z#~bNEX^Ti59t|gP*8qnh&93#`sKTh*=*lmS8LDdcrU^V{Yvc}Mb+r4<+4LCFI9_%* z^6RaAq`QSU!$@P^$%y71{ss6LwfFhD#D_~|j%p9kXdyfF7+FJ-)$rLsq)b+Q@=cPB z^t^qFzBr9~i!FL-DzBnMcDY?r=gy+rOYzt}i}ZvQ#Cw-(pNg7~+|_fw<@mHWFAk1- z1h7idC+tJ7gs8Jqr3?<^inUiRK?1E|y8}r)HG<3PiL~Ig?_r)wQL!#F*4x+pyn5c^ zI&IYCm#tnpza4scNA_D=ljB^vdTaU(fHrXR>u)hBvyLP0h#LQNCJm~e>frTKuD(#w zDqTkSWV&Abpq0r0j(Pjhhde#ZeS>sMS-S#LZ!@!191+{y6E-Wwd>I?47wUd{Z)Ulp z2jPpNN=saUQ25<08N#}AnKV%^FEi2vn!%y>39Zy3qVIO5mMed7hu-7W#`0~*J4c#F(WJkKLd(nO zt@gqi<-lNC!k_8E%JDOAo8Q`mbma{LG7|5+;qu_q@Y)`2(i#t#Xx$2YxjboZmU#IZ zWFzEgHsA&IM)^;iNJOmtvpM0(Ri-w*)}gQ+8A{N*JX@8Yb0oKc^pRbFhrI5K)`*yw z&K)eX@S2($vX5d<^+IEpCt8=k)+{dl9%i(B!SW7sTYI>{G(DJCO5>7hY$u*y6NJ9} zmI7Eq7z;^{p&kd}IiEx$!q||QS&lxDB)n<2@>w~c1G;X2`N(Vg^Wg*)ZJ67rT*BRY zvneNUZJ)PEie8my(-y#ilniwR6;-FYN$1?q7|G_ZA;-uMHnX=oSHqfx1*a=y9=_~o z0?@8cor34R#}2VOkNr>cw5%>jMZ~ut!#t9pby*xFYkKlil>o&`j`@5#w<+u^sUrs# z!O|8Qb-6Gc%agnCRYT#0*VIiJ?R`Yldz2v0e6L`=&qF~jT4CB^bI>!sChXKz;|bf+ z85tY@Gvtt|KD5;o57}}nP^|HMv0|k<^8G0Rp^0c6j<5kB0siJG6VponFa?~DJoT>^ zM!Ok}jY0Z~Yw&XXAEK#oh~%nhnB)pNt_400o2ATf#0iXxqV=8kskQ=AH}H;^Du51z zPD8mBc=37w$rUS8=EQg(hfi-%6dLHtL@xl5O%xS!D#in2!a@Hq=?F{EZ8XUug&+4l zO)7c<>bq|bx*kdTE3~TwZMj(p@J4=jT@}o?byaD?U?wi67%b#B3+=QZf@(DwarWfl z1L@!~08TZ3y3DYVaFO_PR7lS>Mt3Fagnm{FadVm(TvbRjsVcH`uk3?qcwD+F2VXW8 z!@CPQqbmm>(bR2{m0Wrh@N07_)g^TLy;p;?*}GMG5=bi6j-2cNI5E*j=@2`rINB$7 z4ZwjQ2Y0C~QLRp7_pFqNX$G(U%a}wT1Ho|w{AFSx=w?#LNL4ZY`}j|<0rT+p{|o=$ zwleY$)0f}3|6%g_hv~$`%Rc$f)$av6Nfmcf7}LDXtN&kj7yNhMZ+Jt~-cD8y>TgHB zZvD)s?C4|I=nibgza@Q#93jgDkIe;h*{Ru|#}vm&+JJe&@4*$6PXb3}PQaY_wl7ZI{f7d&2y9IiJ>{_mH|Hs|cXtx@p0^^e`-uaa-5=Bh8(IF79$JY%me-8 z46p$%jAG>w@J-E7Kd(8S#O5y$p)|-dA0?6`YUPR`mFb?lJ^$2U6#{QYRheXnJwP7B z!n{;b)l<(`d|tAw;F1D4EZdFERW5GiQ`kYhJD@Zcs&B31Q}zhIswI=A>>0I!?TRzc zlvlZvR5k)33s;s?mF_1@qw;2$C$6dLL>WbNz5ChVWUIE*DD(QUD-Ix?JSVc#5meuT z$>@YvusUmU(eg(i_6)3?g$Z2N#FQh$`u(JW(oAi0d@5ojGu?{|E``af63kku0@Sgl zG%-6cxuhsEGyTUCXbPT{6MqB^z(g!F`8MUwW;C(mS$Jl1=h9{mB)dkWKlt&dH~rJs3|aDA4LhF>Qp{-th6dbF8S38!MoT(8$cJi#K4PvLC2F5hlY*dgR zt@d8m#>kySc}<}?*Z4(HMv<#l*W`rlqSiNBo>j5NtOs}P(ECSMI=|M1|9O2ij^^U8 zYr9pmScJL;9K-I9XkKi29-hRy_rmMEmYcM70INp9WV43k-Jpx9`Tt%NZR!g2#n=URoLAs5DvN<1 z2}Tl6vb*0T?wd!vIhX6g5;*yL@^$yig11na?z58ble!aX&pcu7-GeAq@6;$V|8B-V zT*ij7C^WIuYuZawL)KPHZ{HgLvDink^7E=hcm>zl23(m!e)ucc3!Ww(tz`+U_n{;Q z*Y@`#HJMufB7-9P1V_T&UI!@^qxtrodnzFx#oz0{TzG0<3padLsr2mp`Dm=ao}r9Y zmPW+G7A;S@{OTl$ih{rujTw=^-8TQ9cGTO*td@YAQ6Hil^cNm2tC}8KRl+d0Q^Hvp z384vG$@FUCfpMk($EaVd)F6ANoNq-1*vBFc#vsSD`KpV!3FU^#lL*?klnsgoc=b(N z@oq4hXft8jON?_rRxO5@555QTp$)%3uS;}0z zM#IeZ+-GBN3O!+r&&X_;<<2?duoC;TIY<BnF;d@c*z8y^VF6)3flPt+fCS<_I z6f$&&*KC^@*X)=3ai=kq^(IciIG^3aG_w}%t3S&_GR_I{6?_3~i9vy1Li4)6EVM~Y zAQG06jy!If1{BA3kaFxh&SA;84x6y{qH6eP7-KdPy)vMEs4VjCuhAc~RrdTXv0jnc zq6dD#aD|R429}jPB5uRx;us^xZnDu!Q+7cz^F({g6Ru2vUwBj>^Zd5t+1jA&@r|`T zcQTfVY>alxHy>ji@UP6s%VB*Ec+O_Pr_5^Uvcl*>OOxVm2$x(lzIJ|t13$+q9p5fgC5=*Ub&kQd|LL_!}(obyHAPYfR{zn9|stFd(B`H zVudvy?P6#cT>}&s)-aV1gVvlMZbXqPSc=vM_pH?Cb2G^76u_OgOO`r~K*9NJMFh%h z2Tbxat|uiTF(cmQmeFuw=kcbP$mZbC2cQvSUaONrM#tDr?OUGclF(+=ZMSKAw@Mvi z%hlqwP`vft+s-TYd79-IU-kNu zXzLd>y1_y?b>WS{oTW=y{=*1?F@z;6p$+wylYR;JOa?)DE-UX>bvKe zQU6Xw{4YyI+_I;*(C_9|!@(^VkBRq{e8jCcca*#@(3}iv(2#eruw_{rIW@nO6lLvO zO-Jz|n3>QD2LAg%v~gOQ_%IWP=#=6|2P-)&zwGXom4IWdGTO$MM~P-3ca$Z!A1HKv zJj2l*kt#PA4|Y-LD(gumzg`gnYI!wwy@jTr8#*aG^^_3Gt?~$N|4w;-UL@paMzGhG zQ|^{m`y)P0bi8Mwvz*yRg z5iRti2>w7TM78<3q~wvqqN%~LjD2UsnDk{ZGW@hfNyOQ5CqmvviY*4%X{^shzcEhC z?M110$-pz|OWy&r$Rtngvxow_9)^|mLs+eOPA##W&RO&#tjJPbx7p^v!S6^6i7%QF z)Di(j`avu}bfL_er1`M4Mw~L1vFG&2Fy6xSzI0tvRtUv7(44f$o^b>*JzA;8MdC;d z*!%(jsLE;ABaycccbx#hHQ+FKyLA32GU^OO5nL~w4Y``;@7MvpMhN^U**`901 z_!>wmSunr}wP)Ft@T?{%UvLE`YjWl3+~T|yNnMq>lx8`_F8$yvh9p-BzADG?TU30P z#bm_TJG2x*HkBZcmml$V^uFXny~}lCLb!>Zf6-IhJRiPq9mEA}?eQti^j1BWlxCSj zb~^H)ISR^u!MceCvs>+(&s_GihE7Pp95w1UfR5+9xTbo^N1B`@8wKKr^*K9B@xAQo zgJ$WTKph5*0LTlf^@+CE5baDEKpP>g7%nQsKrq4()aQzq6|-qO%F3qNB7fI^IufGB zo&3;SKM(vBuO#f}u%Xr3x}_*)LJI|sM)7*A)@kRu-pJXZsb4Y6>$~OJ>zkM8OoL`< zxHah1o~6iMY_8%ofc8&8?S=)JH9MGJA-pGGVy1{{IS_Mn1&oh=snQOq$P|pi|3pOh zwV`4a34wX;iB%NA*TP#$f=mUF$zH2M;Y;x)2br(LcR-2-HfL@suKy{?4qYg05gFBTG0-#QiUoAQ_i62IMPcCW;1v@zx&$}2>0 z#DZWOb(qY^O?2n=KcRhVKDm70Z3VeK|2%|^dG_Imb<2h3qofn!^N~lgBcxDX3C}}UUjw&02LghBU z=ehAn;yj~%?+Lp9H1~}{;)5ffqO?L7w_kG6Y|WwNQs)_d*hd>piG5ZOzv_}%{E)l| zg1cP=5$5|G@x)&M|AmGWgCJm*xr+Lq(wIvsG5lyUl66PR}xXEJ=-TeIh)65-^o?ud%HPp) z(}=t&DD_5P{%Kc#{_(=w;X~|;glmbjb^P^Sv+J&`EKdP!iqs(Fj4#&;?9@U#oK0^n zW)s{l4r~lD?PZ!TwkIbo@T}Bb+#Zu-g$aH#n4S9h_7RU4G&dhb$eX5)6JNz!@t|n0 zCh?R3)4xZ*17gbWiyRKhhN2QTZTQT%n`VYhimi4OnQ5TwN*+u+c&&3rSDZ z%8-PiKu-zD`T1PjbSxr{X``2dGfJdJzVDG=%7|UzwuLt`p!Zi+w2Is=+(C9b1;&vC zm{>YuwPMb%t)VT&vvov?*^qPVmn9UT(P2Ea$$Jx)Pv`S(JEA zS*c~NS8BoQ;GVs8*T~xs$MJ^W0X|DtixBN5)bI=DS?_}Rv^?SI;X56ea8xJHibZHq zm+j4b774hiwWQZ21oq=5gJHJz4Pr!wYumlw$S|7Zl$aOS(QPS5KU+M9vXNfKH`Y^f zyWjN^a1+fz;cY06LU`B%8Br-dMzP*Y#{#J`0^GTV+=!IxgRRRovmj(-K(r=;Z0RT{Lr|Hyd3gn8x+3VJ)` zG0p2{KU!2VejG2N+0`06<6>$mHe78z`v|gkIXz#)_0DkbF5rrpSP|tBQeah68P{dX z?`o}b!oZvi3(*0>GN6hv6LS=E(*)J@5=TyzWfjQ3SFx97bJDQyVHxItV( zFM{k}<=J2VAZPcYl1slVAD{&l5m&Uii_jYbF%=A7GnE?3Om2 z5blt9{YaXIEmORan2B7QN6}jd|PkAeE+nM(Op) zI`#=#Q`C4D?7O<pP?z6A*GqwmS3b+zMU;;>$qWuAgoQ@AE)g@O2< zT;CMNo!fa0nzZ0A|H3)2buI=ec&Nl#YgZuH+eigfyA&#bGfelP7o5TVvKi1W!y9SYoB(kN7g3;g z@*K0tDKUcXagGLegfMB@tJ()({7}9+WYBDIX$&$|JOuPwGqEL8pa<;z2kMbk3JGi?;PmxnJ@9svxw#*(0Fl3fT|5w(E&3ct@OpMaiRc+m(454 zb8$6xizgb*=e6uPd&Ie_C4pUNOtl1*dCJ~&rPBea;nX$g#5T#HWbb-(+LX9dd)A$P zGuH76HhUbz%&pt{GpEODrAFB#rS)eP>*U#5j>_(~cMEk#X5H6|T*;LdjlCMx*oLOH zfmy%%3J*uZAnnskS+VejfwblM(ZwTRmQ|PccLRDz%Id_<1&kUaEYd7G?FhJ#wVj@u zOur5<`LNF3ohqM z+QIsV9v5)!x97xZT{SvhV&Klr5>m`9l25>i;T+==ZI6d)$FMOx3R!KhVkqw?j z2~GH6b^;deJu`!oy6bj$b~?lOiWLNgM2?%R67uHXT>IVJf12355o7%Vr`6Q?5Mk$9 z)o5v`%y*UtDA{_Of~&jztj>KfF1bn``E4diR1uu79iqo1%ly95j^9kQGRJ@5S+<&x?a zwGS)QIy=eMV}}ZpTB(OEF%&^;eOjXp(r4>3sx>Z`=ZUG)Am8}0rxRU@vcKs-uB}e4)HmQga5&)gQiKNPRobc$pbyRDzPI{56>^l(I;Sw_ zDNN8_klW9lql6bZ-h6VSwS{E-w2Y?rxdyF_26cIj+C4T1mlexbmVK^6N(~I(4FQbqi4|!Qcb;zF)z)x&9V>CX z<4Oze$3NxXcQMgvg?CTvxlf$mazR!|lJ88u%hx5m9WR7+t3^dQj}@4XO=K3Z|$+z+1y?N3%_O{R@L|6fshx+)->exZt{0 z>zhCdyDXdI&h=b|tU9+10G(8cH)>xiFEQ6WEC1}{JSWp80O>B^{879`$vpa|SYfKm zc|iuOI@7ZC)ZWEBX22y;1MV&afWtu5_J9|7qv}7?C&Z(@xAcL`uY`x9eQ-65?m#7t_ZshP7pe6mmFgL4ZK%kz_Zs$- z=_H{4VSR0}jJb_x`nlliWYcBm$akc_WqEX4c=?@Y$<7vp>Yfv& zKL?64?Z3dUwE%`nGaWU(xQ!g^ZTyVw)wSk^8EfamH<0(0n}W4%dM;a{uha_vTEjgu z4|eSH7LkI!%oL$IN~=o7-l{b^I$Us_z9)4F_=ac`T3lTS6rSCTsCC(r9yu(1-DH^O zUSL*n_v~XkZ<#A5E_n)9Ol+6hV}j`_jxbA@q&w=@n#+WS~B z&$+ZP#p3K*OlYTN9G3>H!7oN6cJLRSrbcIQ<{JQAu||B#cO7OMyaXJmgvFjF^8fOE^-E|F`{DSbXo=RQ!XNfXf(FC=tr0R9TnmXp~c7j36vyi>2x80}PK zr=r;GMl$A8TA?YYhE8g4wCJvep1C2-22&T4Re1O$6(TM;As@Ex#CoxN{`0#M(76AF zt2Fqs08dD`3}*ITzOqq*_YtPtwz$nK+Pb6jQE4D7O{-?oG_fvx=u!pzx$`*jro1tT zr%6RJa#heOY;U1hcXC&P?s0tfULiA%gQz97@2K?%u{+(Mz0L`43EP)p8N;&s4{MJ5)M9&UBiu)FyAJ!yM!+aL-GZ(L9gn z@t@!K5DTBmM2Y4d?h|}k2+sXfnCbb`F+em+00|-B=Pm}FZ85;)yx6G~j;w~VjMUQI zc(Iipe--QCCkAUdbp`A4GMaH~G8qWQTpPW{`EZhCuBfk9Qkk(RHCQV(!aK=TeBxU^ucl`jHa4J5@rir0O7I^$z9`fo4M1)xHs-}N zy-Q49+B&+1Xl(V$IyR-8#@fN9`>tt#?bbNQ5b+JS&J&i0!Fh&5rE*t6Tw|V%?==zw zbw!C{jeC~CmK*(_oW4k$Xc47#6v%(Ml;2i?ENwAjp(}s!bM6fHBsI38)Ze=MOGGmI zoaaumxr1x1iugFJ>&h{z7pEd@FJRoT=X<3hNN2C=Nnj?hOl|h|Io7VF92;%YYW=Qk zPN-u-*w;pXiseh@(T~T@>8(*`N#&-)Y%n8ZXpzIvwvq|ThJ6%`U^}$*7y03ua8J}& z7PE+1OiaYvW<8CsJ&2J}iS_>TkS&idcDa9ak&z6LI>LSp*Z$+0ZK*@Gy*K8&iQlmS#%(CRv--1=O(y+%&#)#_d|r%?~1W2?wsYcp2{ z!yTt^Uu7>Jxl;6$D?5AvZ=F&LzrYsPELE6o6hJkwvi`!PWic|L7L*e?sVs9dW(?V4 zr{$zKZ&TUPQK^rOuBhEND+YE|)QhGD^I(*$DW)c;}PZ*jI4_#+4Q2aWzLel{5_VBL=+S_q7c*4z#iM?uzn@&%5WJ<$RmBZw z-}jK`Dzg-M2Q3_G-PV|gZUwi)r6{EgcR?~g+A^xbV8L*{E{3^jd@}gN8yz1*f-VaR z6+QW8(l^hZJnTNRri)r2mv(ExaG_YW6>5)!>y#hzH3~}8QjXWud4Pf06UxgO@J0F)KN-Bj>S)E zDz`C3bi6~gPI7{I2lj1Ww3u@Mh`e^Tz4r~^27f7m=V>S}39i9;RSzvH@A)~cXLDVf zV31?)4ngpg(h|QF$g>b*h6(Mf0WueUQL3jMi>J3t)G#(t)F!D5OaC_~V3)=Q%{ilC-7%4rK{2dY|zJrLOeCg2}X*+~Z=m z{T<+ow=fd-9V@cQN}CqtNjdVqgKdNbH+y(UjRAVtnB7ucG{^nA8^TU%)0D0Tkfd`# znbk;M#>1Q=XxQ2xX1~x#SRvVEs?YHn%-IJI=FTCvp!U||FZmOii()j9LWc8)rd&*C z&sxjzx(1i-Vlot@iA5H>I$N$G6(yMKT2c5`Pb|UCm=2Vf%{tczDe}jUtj2Iv!iz2d zG%;Oi>n${$8YN^dYG^A0b-_i0Kx2;9u+3Lwc~nq3%Ut+G)1PErtLl`atc{xL_(hhH z`s-+;RnBh|g$bNMXml^`@#Gc>(6sGs&TI0*`znqwxF=ntqua}798khUR&UGa@Xmsl zCFR6}j)|MbF>CKd+YAYW^ zxS9$NDXY2%TMT^7rZt*>$4Ln;3eW**n{3?E%AiWAA-u(-kFdSVzx&bC%8!cHQVHjo zW&eC;7}lB+Z=cNlEWtDZ@?PAY8o|`)1Z3U$u!GZ@;$Ye!@lR}IJ2QA*z=wFk?Z(a{ z*DU4ja8ryS~e;#*N z<@U0p!6V8vGPSj*9eA1`N(e3#SRe{szmj+j9Acj+(|r!YM3DuA-E&j@wCQ=%L)Tx2 z7MR^pRYCWVyCQXXD#*-ba_yuAGE#9g+og8QS(u1^0XCuw4L%yc4EJ;IL|iU1J};gq z(7&q-b*`%De=&%&-2|T6B>)inlNR!hpw~j5B4-s%I0%+eDLBQPJ34 z@bZ;~~$e5S}hdq!Cs`F38328pQ@gUQ?ZDfl+qhEE!;L!s>wmhM~>T)~D z?KJKmCaEMRBk1`Ji)R#U8z+*zze~H#c>4F}dfh@-z-Dw9d{PkdD5h`V`hImCY&Bjq8R} zmTYe0GSWNp=g&4|_gIC88r|`FQ}^PwpmY6ZiBEi|2)HnBkah#_?)g$WGof{H~eHT(; zPA%?95uaJ9x7yz~K|6D5?cefl)bqsS)PF*M;H<`@Bk|sM6K3&MH-2w=$9n#B;p9B9 zqBA~Am%VHxL}iqeC2vRz=e&AD@C)h;%BY!tOZQYY$Zvij`A1l*BjUrK$tLn8KeOcm zqPZ>~+ur$-*K?aQ_*Z7$>vMCnHDS({#trWY5`045eDP)r6q?!4H=dF8HzZCgQ{d%} ze1}?>A5Ucq5B0B{^Dma6Y-K)tQxcG~K&KS0nyx6p4`Bu*dUMYDj0Ga`vsQ% z{J}T5#j2BA0;5;AhbVu*nhUS`p1}GIWesu`{n>R({RxdahZ&s@$AxWz-%x#t14<=( zS(}ZLQ!0rMmM#X%r&6t77kqpZshx={7HZK>zt-n5S&p-jKpN{qUvH9c*w_BU)M@wU z#Xn5JZJ>-ve+xBz*WTSTm^p8rf_Gq0lJwd)`$#eWZJ>#|Pqdr=)0fO{!j^-VR3TW0 zxrbz8%Bc+r^7K&onrkXgnCU{7_8+G`dxn>!G}n1t4N8l2YN+OF;c2`+aC7c^Vi4Ow zYv;Sy9lw_=jg#?qS`WMaVY;f&J!j5kbRdhAiYJ`-Z)?dECc#!p^UKg5m~X*y0u2>U z{3Ziv3JV*RySL`woJ@DTGa=TW)p$MgVYu|@(SKbC`mB%lmsbA2OB?>Tl{V~mTz^|Z z{k>nSE${;>HhK+Gt2=t-le5O%q`$F@v+UM@ueE#$555tmBE|;d6h;lcHlLrA{9el! zYIb=*Coy~9+hx)CwvxDA`bjNcpqI^iD6B zkXdn`%oLm6Gob$9Tic_bO6O2fGll;!@iN7jKOX>2{vAuZ@q97#ZYK7Jpl?hU^AtDU zsO$LQz<#}$oMFZp3zL~p*0aH2&1q#zd*w(AVvPIKuLvfKUeT754h~sBaroAmI#ReM zhg_fwYy3FayG^rM1$)2jvV!|Y2@6q7noEv zmVC0q`%kJ1g%g}d?z4u==9v=7qjls8t>fm#)QjJ~;WS$zuPKST9Oa(ys&B-oXJtu> z<<8_ET#sXB%^^~!N<6hAA&gdqtbmBK2GBa|?3a?ro(LDM?O7K6CDQ-^i*2tf=xFzD zwFDfeT`M(S%a+HQU%HSI+W;57V26%`XUc9a`r!TN8`S zB)6}Dr7mX*r36i*Y_2r8jw&nI?#YGmmBmlUl-uH zrO9{lGk=0#On>=v(id91soF<29al@d*xY%*2aBsWeBTtr1exju3`4vV zUI-mLYV^O|jV9QXe1ft$(ZNb)DO1+8n?{!^j53}t_hx-CqF?=VMzjq-?TilCQrZ+0 z%ED?tt4uNEHHqaO$uvU!!A=O`+TWuQ@2Qu2G^Hk}nzK6lcULvWo9}&0J|XGfR&NjA zhbfQ~A{ekacAU;o2T!WA;Ba`0?lfC6Ju961(1LYaOHAU)2FWUnX&N}SuyPK9l!vFZ zyMzwvFhXgpS50hXHA=bu{+bh1m^YMGtSi!WR@}vbPs`^a@?FI;)yU-Gvoik9R0Xzx z8xbbaD$W6y)D+dmSnoVqTCT|DiS_2~e;%504%OI` zJ)X>qIqB0$l5PfKnfW&<%X@B^_EsyB&m+fM6v4W|R(l<#3d!S{vkG>J{&iWi9dkkp z>W3*7jNJjhBn&dMKoaQ!Z(M zO`1gk<+-8eFtBT;fhlrGtSTvWp<AeZ^^v$UKu_NCw;?nG((lR6*GBNU_sv0}lD8XWs({>fnOzA>=cTCoR!y z(if4mn40>6P$5NbRxEEe;6{W~4X^C7Kuu)A?GY2eGrLG0Qd1M=cWP7V*q1AI$`h^^H$F$uwZpWwVx!-Zi1L1b5 zpF>2oI&mXbF0~B*%k56Fm?Ry?^48Nd6RL55cqimgEDj0O40q6IZ@NX zJh^kZW;Rj;AJ$O#K!9uX7MFd|KTL7Z?pXm?tweo#!O!}d7Re;89FTG{9VI>K9+JoK zU6yO=!Q7Zqoh@;6cU>E~8y}PeG7|-l_>$&xBF7*q615?D{99b(HHXql>#q9*57-wk z+LE6V?sW_{_{S9!*X?Ho7nP-Ps7$Lcmu-{D>&3sdtRIZq`_hzn9WqTF5xPz>RgXIY zs<|RdYtlOH4zH}yE-wdtYbZOTwCyAM4^v_3qhm(;QN!CirsiKxZ%c}Q4|(9W!3ZBd zU|KixOLNaltT7(vZuz_a-r0Lv7kNXIf{%9 zl-8wsz^D*K`I6D7-bN)X3RXMI%*1U?b6`yJ08jhk(KabnQVbj&SqZix?E$jLJ4%W%}MkL^yJhnqObG@Z454V9qRJ~^x? zxZ&n;wOA>6!(p<}l0W$2(1=6)tZW46nb8cD(%rkeTa$8`6ShRMk7L$N?j1~C(w*iLh(v1jT@!=c9 z*Q=-bva(>0V@Jd8&S!9mhIh}{OFgK^J=#Oh|<(zfU9+FFZNX(^=uQj=Y z$9G6VI;Rdm+*Hx$GM-4~9)NWw+9K-9p?i^A0KH+5jcbgTY?PGRRB&@Lkt9&1cY`}d zQ(dXpSbA5tHKD1O|Ia7TVnjscc$H3zxw!LMdW-UvWh)7|IdB$YsBHbHlQ1SYh+)`V z6f!9MQI@d|i3anVpP&cfW@$l!%;z;*YmgFfBXUeg6f{NIKO|Q-R}%P;+8S||B}LsM zy**48y9@RVaa$&(pqrkijMzAPO*dVf%7xrB7E~n((SOb5zPY-g>Pj7*55Ua!UwvWC z^agx0Qm%N%Db`^lAiNRjsS@ilFgf(SUL^fSCBY_}PmwL|Y1~B2A}e+Qt-#&uCSU8L z63aU}aM||OtwvWli8)n?uTlEXMMX$2^@9S2C-rZVVm5~CFo)@Yoy|} z+cUXy$Jcmwrk+S_JZ#ig_H~2ucD>Pf_W+12FA#Zjr_yM_G?7%LUsfzN5&-`D%^kt^ z8f~Ogx$IX*rZU-KOaZgy%kkDH&4f19?Wa0YMJB412}OX`bJ0GDquEblLA5KW-6!!zL>OLv zlrNz%fa6&Ix6Fujj`m0L>_|Yi<%aSUDBS-*!FA$r9}1m++*rm=k-JGXGc^>OJ&{#d z<>~>C_@-%if$y)}nQk>%Lqw4`Z+}Ls>uKGo^-q81!kDwF+x<3anFw8vt5x$xE?5i# z#8TSC>JHcRUAFt^%)#$Jhzya;E@<%_Spyv3wm2sm``^>$Y$UGwb?+1jX~CI|ds|GU zwMv_~`+4T%T~P(TeqHb^oVzfUsC%XR8=gDfWR_^BD@Aw5*)8W8FBjo3{Kr=onGYaT z>E;CY;41k&S&^2g#hjGRGYuz z$0aMMOI5dQa}&4g*Cy^8UmKS0t1m`~-$YbTdJb4^F~4@nT6F$Eow82ZB_Z;%E~08A zT!f>oI{5;OCyEvt0&0%2bK^=mu?m0eFWV(kmjW(V&)x@k z(k|4Q;puGhcKl6@_utWD8eYg7UU+hsd~F&3(2en$Uo65SjVx6|7)un)DL0s{B3MOR z2Yt~-zB_u`LHiK!ov1~q{R8w3=aB5`znV0phb`EsMb+e446ZKae~tU(eh+&2d-JgK zt1m9Bq2oYd{T7hV?tp)VHeeOy7O;IP`(f^N255U&-ZPEH-ng~`is5vF`{6lU(5 z>vbE+V@cMhy zQbpOFg9h>3{tr@i?hohu&Zcgl2Id1k*U-6`DsiaT>%Jl185~jCzN{yi+2=aDak;GJ zEN*wm z5!bt+NmIu=RsAoDXPnd{qyno)_&N(?XAfhmgeQL?U(W&z8HfU36s537#;IcNbL5ycZ>QmTbb(ZvgMclxmSg;E( zUbUP0rVGiCB8R4^oI1U5+MN5Vw9#na^89RKnn#Fs}8 zh(5QB&-Id*X|EE**`_m#bkN z%5$G~ou|2m7d0#EEv|*E5!XOeoWxbd5lrN){OkWAz*JX;Jtxv5ynv&cEfhu{QSeFmB>R_OI{O^oyb#7U~b=QoFRXP!-}R&tYW z>asdVmq)`g!1bLZsN`NY{k|qWj86 zqGP-I-H!gXts=fzjJt&USl?X`VZQbRHP<@NCVkaj?G-qoa4(6lu~PcUcX|@7F6+j! z`++#|wJQr?ZbUidzCcx-=pJMj(@^&!x2i*4I;V#04R&DQ)1xS%uJ)(dFNgRt*HFSr zWay^_ZqM&1fpxx|3R1-RA!i0G#Ypwt5lig!rT`h!CP|_+*YnKf@Am*RZKz^?If5_G z!f?KK#RcsCS$BHMq1H~2Dz^bsg%3*}4k08=z9Ot64Ecw3;2n=1-Ef3On%=P+eG*Z9 z0+AVXCCb2)o=4g=ePcBjf!unrGVrCSF3`9zm$fx_qb}N!>#~BsbBWFE9N?X{8#4sI zVpJI;;3i~yA2+EpyVw8u617M}@(i!GXQP=$=O*(yHG;(LYZAvaA3|6N^}BUWb`u>v zU?F_NjOt!E@Fm>)ZgvnNOXFj`owb`#Q64eSmGc0wscG&>9-ZEr0Zh8+4WIG~HJkHz z`Etn%Ffs5G<;H1~Oo4UJ$%618{8yq-d?#*l@m z{$6?NfgoVlB+~@FcUz=*6>ZOP@p*h%l)a{^);F|rrNUZf2k{R{JO}$NjVo(ao)>Zk zNgNluwJx2*Kj>sYH0D$N7%k)DwjbZIk3gd;Qi2(dxHm32+Al(H0=?`xR&9lJ$hTFl zjek73BZK((w_m1;#QKrMmjoX(3LS7`OaV1{?zVTcT{>ntRZrV2tYj0e$7Q9rIL^gV zoFia626yfw`a5a1(ch+8{5F5Tm$Q4C0FDe56r-kHNkKZTO1_)xFlF<+&ajki{j+GAbJoUYC=fC7ii&L2-&;v(H-?N~^@PeMGz8|pt{;;FE<-JZ{4 zV6QBFHPKt*ei)rwg4~?H}}5&^bIHK18w8Mp^3RWM%S(^%9p{tCiZL2kvWhysQA% zMJ@&tc<&uiX9q_H{no4=rBuaLACXJg-7m-eKo#iGK_gL~f z(ao5+!nM*wJ^+F8vNGY}T5YoC`~viCvxKS+i>4@u_$qQguf55?HZ?bI`BPa{roo_F z$BL`G^S7cE7kUg2VYrn%9i;C>+;p#9c}g=6{C}bIXB@z-zV&%^(tM4K-g6`rK?ez9_q9bCJ7ov+0NhXEO$Fk;*ESiQ3Ks-#~kqf$r z0qqoc^PN@u2~hPItv2(lhtIAvzCw2*xOTdg_sHntjB6Vksz8<$Tp9Ne(f60|@W>+B z{%H?{V(K~Y*ZhDKKBx?C{@e0P?VHQ$Ilxq^^H;p~Y|?q{gM;#swW_Jsj_U=Ga(?oi z^9r`;+L+pszrcqFj~)!^OQIYl$kfF|_R@T2eOD zQO!-AZxAF49dFHUgEJ+up+AUWT^pKV!g*Z2`co(Met_gybHwBC70&qo$ zFygUGxGc4NdokV6!k<0M#et$8yJhjpa_e+SDos!FwnRJg-&QWyJ~yuixgfuj*r+JZUcDMPLbbj#h<@fBjGh@~8yK}>;)dASqDl~); zWY7?=8z=)4$#X(31@pn_Wz|>LoARd*aE<@k)o9ILw=0K(6naat-A3HyF~3M*);7 za=)u$mC9{eFN-TE)W5CP7$v+emx2(7#to>`!fQ(Pv%@O`VlrL9Fy*97wB#Y64EO>w z-4w)GBv(?>KojbJoxqN3KA8$cxx=wL0M&Z^g^iNvt1X5R8;IQKihEIz5(dgqi27UP z*-#>zf)ek;-|+r}oF6u`I`b^cX$7|YSrKYYvP!6_l=MX{?t5oZrwrc2o^qBen9-?} z%oi7j3a*g3<4mse*-8z5!~_w$p9?2bPtN%g5|}rsYXSoMFG6s9?bJH~8%04Vp``I= zXf2W4XMuF8<@^t%KoeG>fxTJ#RpZb0^=WtUp#|~Po4H{%=|^L$kc^e_FlO2qyUC^4 zIHe&(tWfJeBygZ^hN#KnFp^@FH*r!0J!5%Cay`7NjEt|X19xz+b*eCSDZ)Kwp#?kv zW^eKm-6b<%+dO0QgzlHFk*7(Ag&`?Hidu@nwybZ?FG|2gTgYNsXq zxbud3NaIG;iuSD`V$O8o!j%heY=jx*!wY|^007suQ&JN3Lg67F&ukJwwNDmDmPk5* z>T+F;1MDnyi#KLaMEDS1iwNZg*lkyq-vXp8g^%1Lq-6AWCOZhq4qzF1e>(sEpe_1z?6NdAJ8e#32S-l4H@YPoU!i*9;4GB@qcEw zU-!$+>MQ{_r9t1oH<0ofVzH?f7{FjHN}#ser(IPnCun~2m6Ukv7x)K`fxb<0yNI8$ zu%zERfIRtL^v-*fr1I;mUOva=>hjNtpWRoiRq1|Pi*=ajSod_b#s&T?-RS7-q9}+~(pY)6Ev8RN3-mY9pIEK)$ zfG+CM8Fe4z;bEwd)9&T5=E7vqJpl|f1wkRz*qZCs3OV|JNYoPstOdaD$I9_F*<9P5 zM*AJ%$|+*h1_;}@u_Lo#_G%F<3R4e81NRcWN%U*b+>;-~>W<}CNRRL0zIAc0Mf;=$4U0~0`TdBmpy_6hG?d2UVn<9(j37tETgnO_ zY9%UjtEVk4i9f3aHYlh_zqK_L1$6V6eZlH zjNLpYjBN)E1!)Hueh|HL#Jg}jic%dl1ps|2otwqQN0^h9Dp<^bta&;tWLlxmk}O|_ z9$22RZCau!lqnJ?LJv%?t+|24!Q9BlKZjVj6DjwXu&I}S7mX&hOl+fH(+S58y(1yU zjRowRC1$LClWoK6#J)>zKd<0)+K0$7tmR)k+t-O53=PT8d}ayj`$b4JY7ee>L6%5? zb{^!RbOspt$-U66eIHtIR<%TpJ@& zJ%;=_Yn|}#0uKg&B&DNOI5kKDHQiG zQJyq%WB;!1Qz6!U!2i^J|Lf|$U}8FsbR5K-$))GPK}qBy2fWbMiodZAuLt&d^MO!f zz(*;NmY4Qe`d>lwb50=A9WpG&KI9>RRGQAJZ zI6dnP3a^-+N?GhAtULorI23@PyjjL=iP@XI$eAOkI(*IuBxVXx;#x2t4(h+@7RpBR?u#{3L)*km!AQADvlJ*-i7JGTlW<9Cx>`t5 zVdrpldn-i)`%2CaC43IhFt2+X{((8(f#K@eQ#>?JKNEnOGvo9M!D1vnkiIa~;x~f) zE(E&0&b(y1GG(tZeUT&{g{-)f_DWzItDL>{(ueYF6H_L*hTOqJE>d%AU-09ay?qtl ztYT$`)qYl77215o!^2s9l@|gKaz@>MmTrh7*y|b{$Jof1m5xLp1Z}cty&s5aqgT>BNo~Sy_)Sq9yedMt=zX_ zG;E~+@&8%u$7lwbwj@Z8fz4y??DWXeOpC>$(vt5z72LF_T?=HeEZlWxBc`f+;=tW} zBato%x2#{HbtP2mVuP$IUv^LDluOHj+4>VnV8%;<;C!Xw0DHRcH*_-LQ2oBE@b!6y z9gc26!-=*e0_Sq$<8|wj{reudUD@H_be_T(`OX=ZLeOi2WZZVQ30m~Z0VHwT}aQ9-?)99OTO<2_qgUWR^n z;7!^Gu6@}dqt0RUbQ@g?%;Qzi0SoRj1%7u zAC(wJiM$GLWG=8U^%APSO2%jep{8~Z%9biYITqmccU)GgY8S$(Ui7G;{v}d}GwLlF za@}jo4B*CcHVI~KUi`@%IRoZx6!FK2jd`r&#U0u&w+*8{vP2Vswj4`3KJP&DZmRS2 zmCU57_!>J>VTd;Z(V22CoafD#B#`1&(iSV*Lt!?B`n7VU$KQDH{hgXuHNn}k)tMqF z#4qOo;uYmkZIPQg>Ng@UP_ecxxlj^lq(3e-X;Tjd)67e>uE31td?+9Yr?=?oLfMP} zeY%^h5(NwIhdBAG-l@&VqRxzkc%fvThk5No;Df=*u_68A1l9n6-Fur}s3rNbu@3vH z<)#by7u3;_{^p`V0^KQ;OkXPZ2MU(P6J!}<_qc^!R&29Sgv|Dp`;pD2{9(nL@am1y zsM6>Lj>8eCzKhGsJV9p?PM=#01Ktqm5fKM^q?1j677XXnDJjNj8!fDNw#Fb_^0G1i}*cpfEY`?z&kewdTi~g zr#wXHl!g1f!rE?K)IApLZQ-?v@?@W(IlsU$?h5XscQhyVHRPm-t%CB?vUBF}hF|FI zw&_1#jkZnyd{yV>`T0kOv+Nu&_0s+Carr6nu9*IX3=bA+JAMwB?rutd$B`I3eVHr^ z*^&8$#`Bq)+b9)X4`*Jkjum4chCDHcyrE)#NJeTazLsx=kWEIXZvZ^ub8*SlAZ}n`yGTtHL*Z<^F&)<>3dbyw_g_F+FFY%vVj01ly zb1m-Mn%!KH(;4-gns2`JxPhztYS?-bJz6DRoZvz&7AP`2iFo3o^PjlVPA$P$0Y&vEHG^I2wF~VtFrFcIe=2lI-Glo3NC})BoTmsVsfcZe1I636S=1pf1EsQb!_O*} z@r{SR=ZPw*LL<4i#|+;A4Q<~mw*ye}l#-%tw!U|6{zAu6ovi4tBH?b?2@1lmxhHO^ zbU*HG;qDiq6xHRCR68dV+w&Jl>)TvZtjI83ksg>0^7q-I;hTb>Mm!r4w=8MXiMbtW z?{(KuQ5%-S;Z^QgI@wQJD5^q>c#n-~$R9~c%L41#sydfqSS1c@yG-AlQ4M;i*Hx#r1`zO54t zN{Gs;yIFu8urAHCuxiH1qBHyRkwP5{CJ7DCBG*&Xwdz>~%%;IGa3t5#<$;7*kMrlf z_6q(9hio;o>lQ>fS-qH9g`p#{Y1|h08EnB2ASlH`=eoxj4(Z3cn@VyK=)NJ{NUUz= zL)tS8qE*H4ZEo6}DPlB64JUnpKzrTOF`wc^KF-*6mH|u$M~zNL=LeYzJJwtl6%QTg{f}B=#z81<~H}h$+>EWm8(!qhP4=GmljsZ(XTOZ1U~1}mNG@K zf*?X$@VuI~CakLgBS{4iS~$)iYpSNA+Lnz5Ym#0}S;3i~wmbnovgCH=y)!F0zFh^0ldHcQ^%=|GY4af zJ?rbVTpV9z)Vh?TP%!-&@&lXC-ZqjkJT2wH{v4!dr2`$)LTC`tJ<%nR#DbkIa}QALnhit4e>V?VUk8O$PJ5rw13Q;Mj)zrbo<~!M5IYJF$fsJ)+$EWGn78l6 zNIZxXrO|v0lMr?buyTDJf$2h3JAbx}UZRi`Ak!kE;BJws*-Torce-=Bcsp0gmv1T@ znV_COjJz5lg;<|fr2lmTrT;{a`j5+5&^gb!oRF$dNWbBlFYx8m(QOlXCuYF>ZhEJj zTzeKltYw7q1j;>i0`8EP+t~02%$QlFgnswFZJ6hi?`OE|mO5i`TWZEknd>(JpAuh{ zi=~_S@Ni2YC7Z^kBBUU86tUvMPs}_NE_QJiVU>ZpL(1;W>w#YU3igX{ zP<lyM?KX z2C@_Rb;AXupu`x9Tg;ZN7MkHU<#%Xje3>nT=5mH0BhFeB_cDM%vKysdag_mZ7CtDy zgrx$1Oo-?T3=~@@ns13Izv#U`ud}{N+y$|Gl&>$`z!u7jxS^Ac*F%nx{y@qk>c=@~ zRMQG^bEWjDh~Yb{>R$Ieh2_HYCqcc)>c9Tj(^t_F;lo)5U3jZgLaK@x1%4rJudPtz zR8F1gDr;Usr(kY<4WJ4W<2aHB?p>1=AB8Y(P1%k#M&mxvmGA# zNdOd2;Xt86zOVm@*-l)27VVL9U?Ed(A*VC&4si1TD#eXkPlB??vC^{+Zp+v`oY=C+9Qvrs5RA zq+?iyJ2x=HLjW8BD4xs|pF(~a+t;ibq7_XWbhVIME90Gy4}~tXRm3OIfznMPo^Qvb z2xnkOD`SRu*>(81<{alpgY+SMlo$;x`H3j@6w+Tt8BIG6k)#t|QU}c=w~$>ZL#;G2 zr3zw-Mr_Co8;7yy8_BO$b8>If5%-W)cJzyM%n4JAT9$$n4FHrnGF_5WI8#SJ4W4jd zbs<}rU8AG;4dSS`S$$-t|N?->^G}gTwvnjJt zHIC;aHbpSbGOOWb6{vkmF4C=osllj@w#Tbr^L;5 z!@A=6*rVkuL;z;FaQ*)89n8eMM1Y7Qu7%Ccq}&uxs%z|&(6#kq!n99kV7{&{Ls?qp z)EQ?xy)6l0AsdvTeB6Q(L~_~YRMmQP(9AVU6iiSC>AU*K*~{pD13o{wi9IXnc-BB-Tz@%euJ(2Y-A2cx zh(VMn?-_VjahWQ+5DC|PG!xZu$+H+J_K7b^^U1gJ7BZ4htME;mmFnPPvalyJq4Y@f z0R3y!Rz1t{oj(u{ee9I2ihC{-XNOHZ>5%-zgBGn98?^(~0xNokIB;K9mV;=F!nlJLl&Ix*Q$||Cz|Vhfl;*rH$f(f3FQkc4Zmv_emG9XbvRsZFRv$kSQLFVB-?wvu z*8^oXIs*y6(qYeq74@l@Z@vjnEN8f2{w(D4ed==$+SW67Jji0gx9{-YRxxeOjaP^6 zjJUp<9QiS7?jF=Z^Gs442?rjS=X?KH9Uwt@p*$=D^ObsU&de<;S$3knY}4lSTxhJA zT0ucdu#f&Z;qPnaJK9Z@ocl$s<|0J#=I%M6vP*$Ihku+$NXpj9tI`?KJ`p0dlOmwL`>#K?{QKA^3 zAK|a>7U5YL(xc?^X!YaYHw2BG;pY&&4_=KH#yhoc8(YxYhrz&`q&%F&yICnc_X zL6~^iFke6p-m?4kD|ITvGR|hHu^RD>N0(Sq$+6PDeVgwpIsF^Y1Z+m_uMikGlGUXBf&270mazhvB+u4X8ao}cJNUDQ%t*5${HO- zST{(yR%$R&DBcb!PRocuEnuo<($6=TlqL{FH9V~9$%yF9b6#*pR%e4x^{pw=Doc+ z0#~5zwO)U)zvgI)A|{#U*mqZ8@)qCd&rbc>et3~gMIn#Zyty!vEcZ#JX~t@=m*e`k z8xDhqk{B@KIq=i`XFC1-XNUGvz{%IWCkn8r=KKWV7MFJTzmlavI*t5fJ;(`etJZEt zHQbBYxW~u{X1>E_}!wazjO;; ze8Eef)$cCXhgY%>FdyFUB#t&S&ZI1;_EI2UKze}XgU>LMM!J>_TxV5(r!k$bcJpCi zgS1)b_kq+V>b4jIw4>EEaP~r{296yf#BJ#$7c{v*M{el~ZPND?A*pmhyMMP}-AJQ#-%e}M zUBUS93QFiB%9OB5*c=30PvWw~lLI-2uy zw;)IF1zU&blq*w=&uhc@-{C!mRPqzRIEnGxaRQR}fnf+>SU}%>j0@_Ab(}!YSw6fM z0?aB_+I}4u7}Ea0kz8e?iO+d1$TY*4u45ygjatE@926Ji%s)F!O^22ZFPXKpGd( zK6bFPy!=a)A4gC zIAr5fYz945xd-21?l)wN_bV?VR>@X(-FASQC&0?Fm2&z0kyUn5owcf{^exGI81MB3 zyuZ(f%NOibE?xSPWDJ(SwG=RkTLoqmAG_moA?@xhU(v3&Jmd^~KD#0x3doR6tpT?4c;JR9<`UA@WoupIREwRENmlmRbTXz4T( z5p(o<-bYVx!*hT-TRb&$+l<0KT`v`)ny6R& zijQ&}j3Bpl4&ahtKNORnHj!@|{&sc`Y)!+7srZAIe${{J7+#|d>QW-AG#RxPKaZpMe zzK@w`$vnsrYl?=1-06GePwCBtmJ|cH1qiwc?pyJ-!MU=yQ8W|y4>k^zs0Aib2Yq7y z`MN%RnVsr$?LMW#)Z@cN`F}`S?5=pvtH5%95{TwPwn_F?A7?e2O_<=Xq(*tpayvZ* z*uX<`8`k8lXG7|&3>NE7;VArlJKJ#*Xlug(IMXe(qhA0#uT|5J6X_$Fq3}v=%J=th zRBJ}8YSy#4IGX+Nd^>|(C=+A)x(jE8oqRKE4wMnbky5^`)VnEXLpvU>SX<+j*wUsJ zkruVvS||wj&7b0ok6g%E8dS(TSs;l5&fFOa?$u;lVD+w41Z&#R8ryhsSaQQW}fVUtnPq7imQ`VAXZz#hKbhE+-YT{1zxX&7t(J%uk&5f<=*>Q z5Vx;ks9Q1GgA8Ef_PRGMc#ccY>zV=c&u2}^nZSqTtf=MJgJ9al!lLA%f_>$rsE$|p z+jHc)@;a)hGwbxqW9l7*v+}}i|4;h{4IjTRmSp(~Li-YlO@CFpLHSIW0HJ3u9Nn;( zZL%;--r_V;OTJbsSzVDMePuMrQRDv|jiju~roA+JprxmHMH^XqID$8I^6H5HV$u1= zK{WPCVv?(W^ik_YLpAlIbo#|Omd?r~!gCJv*jo8$Ipbny?q~2L<3+1`x?BtvlsqkT zuS#v0Fb|tKqJ!3AJ8<63FF7`U&8sa05lTI$@w&5jJJH7Z z_M#}_Ux&qP|L@@p>peojz02Bz;X=VuV&ksqdj9Y1k3atSrt@-&}Ra|+C7`9RlfIX9hdGtOV^Bzg1cPrNaJSrL>m+B@{NlMLk4pQdUKwtQKA#=`Zb=`C1c7y-N1scm1X#ydK^~ zkZpgpCw^k3r6+@uBZ3IE!X?j#NP`^PCn3y70gA5Ao zU!RB%;p@yJ7Z@FWacsJMU~2`wXFZU-zbL5XVZgktSNFoWnemQ(o#D7vyQ{TV>2z!d z^LNNp%G`)E83oc7M@5;aM<0v7L5>k;Wn%VV=15K^<8sJ#D08o1DeW^vVCdw6C5$+@ zATs&P*ab!~-?Ity^XVx$v+;AwiPK;#Wj$}wjDEF|A`Kz6*@El~e}1}^3bCF=7N)V@ zVlGCP?aaO0y_I9YoCknTSLFTqCfJs)s{_5(;cLfQNXCk$=xA`2yg+b|;3@bT!Pf4T z%H(Th>z7!w=NK2h0CcX%InG7>HV6cI{||{s8FbTI!t{}q27jZ&W)p+Ca~(qJy3D3g zjtmu`X0jxxku>YXUcOpBWHO^hIz$^Qf2s;pYKb#%;}-p>6X!wGinH(1(eF%ipVWwa z@cO`H0JvgerQlu?Ps}9p?%2+MR8rgrHYBXzR!j|%x1P&PwN>a+`Fd5h1|CN`*VnZu zZoF_L=dYL#HuL$&G4l+;wt-AhJNMKJ1~f$vlH0ZP{6mrx0xy(aQA{_Xs)HG=c;ZYfhE<>}{_4WTv-l|>Erd;@yt~RR;B4>SGN%@WqV?eO8M_oPc91I*+Z38crxIr>)-KY*Km(Y zUxn8$wrlxzr(~NSc2|DxDkqV%?(|~nAS(5{AJ$wot-<+EJ}><=3qU_ki%Nt5InzW) zd-)baZ?Co)j?IB#uZ~@a(_P;tg9PG1uU;j4MF>xmMJq#e4}89|DUR0GaJFHN-murc zwl$A@$w2A#RJVZ)K>U-UrRD4IwkImoO4Mme0le=m@pE>W zl>K}6KqFgmh)f=IV5=Y|g8C*of9+(I>){jLpWeLCuPRpdW9Q`pF1hR2SWk<4P+IEK ziL9~}%}%t!l&(mTDN;xQy}}bkMhk;6mew?ce^Q+>l>AB z;_DWhnin@S_JqE9VfpVU4(U3cngC?gvQ_#^vMWEYybRz|#FxJ%jz-HOOISm-jBWiQ zqykns0@j$;YRfmzAmDpE&2tp23=E)r92}}V-+r4C3t(K>qrzL|z7UgMw?rdK zP5~xz7T6kS6}&O7!9+P(U6Q2o57vx@yLs-bDoP-vH2WT-8H>2MA<9$~R`aJ=;x3AA zExJMNp!qBlY#rEkY!Pqu7f3dQlZKTn8#6r{MlO%wS@}vrW@8MxBj)^gnh$T$_m%kw zAMfkCj)3fHn8elRyGzXV0_9A-($SK%VM%5`l3S=(X%~_MyCP--*)=zKNo(`yxd_>- zqy4o=q4&=hbZsI`(TSW+x)6f4V z^S6ko>c0Drs{7xz-KcJFJpK8%_aBnFr`No7!d-s+h|wIDa(oY%W*xO@K!E=t7S^Ifq|vX~8{aiji>dNQ(nvSe)Wn1$(b= z7RAtsQAjyUZKUO_Au-6Sy;#!hS|GywLgfO_lBc77X(&RIcTnk@=HwMJJPy)mx6-fE zdIa#a4?w%hp$MzAHO&F`z4@-CLO`WDIenj>sDnrQtHrct0)cBvOeC<_L-SLZxAj;5 z8!$}g)N4;NcEKGqifq9!<~E|vx4U?)V425AJi2R~Mcz9};)` z`WM4r?C{%lb@YONyjbi_ZxeHl@nSd6KKiW(ELfNZ=9|nrcoy<{K0ucg4ddBBaE}U& z$Y$>0JmHd8ec-JhKR;m=cM&0fc~7N35nHP*QQCa{V15UWf)9KbUjwx~05~V5>{zBD z7MgtBgZIy<(A}r?&+mlSA_ea#vQMfRt*4)tJ5|6VU%0d+OIMGag5A_gP7ka6f}@V( zm}`+iW@)Y~J)#~(Kr+luDWNqvD--3;*F{Jq?L3K(enx1JWET+ZF;BHUy+?y-iDXWPd4NJ6rSi5R8DdIp@BOY%CZ zXKz}DI>^Mwb;&{8`y+c@sAjIjhxZF%&+=1cILk>Q2t}cElbyNxk~#+BfiJ0Y)e!5w zcTvM$vrO^<4mDwkf?N@1R7@N437RTzde#~rjS1)yt&tI}e%?1S#3OGlCYEqGA}{b=7JwI8RlxD-i zEY7Y{jK9~f@{}|UuS|5LNpf$Jq%QSnd9>7>7{99sLdn-tVHrw ze=&`)&h6Z31fgZ=o5BYvH_2@!a8*thy_`CyDbn{{2&Ilu#flO4jONo2G1hDKP3q1z zTW8w)Gn932^4y@@+9y{bU*GRqKT6O!)5$#n_ToKl*r_y(B`xE(Y!7)vb6zqx{4R>9 zU)!y9JL&P#U9B(GjF_Qtl1jVg#Cr|`vY2dGCwl#)L$Imlinn%+>1=5DMz6E;tF%8% zBnG_V4nS0msisem=vQN064_UZ@NzT);TEJxRjBfp>;Az0vx#9k>4tB=bgLz6 zvyI4ZR&%^p;5~}w?m4!zs%u@cZ)YC^5WYDGrdl>V$|U*9HS5^$O!Z9GomJrdkf?7`g)(6r?Yrlj)NeymvSnB9pMmW-{6VBj9U*(37BJTWwQGETi z2xVD2n+v%tA4glbM|VF(-~+1OCy`~Eg?~IUe{e*9WLTvm&-H%3r6bL0OT|**a<1uV zSRe0xpB+sY{-IOep1PvzzP)er4%n2li0|B%0PMbi2*U3L*C7yBLV z#z~ZR@-nJ}msUD83rI-b2)CHP+_$G0K8PQA``Wehs~p?VR7JN-Ohy#Gy@BWisgrn( zpTB0b6^mtN>}~$G;S!}?Ee+8=+^XITt)3use9(MtLMJ2nyKUHO-ilACq}F{Zca8rX z3}3r*oplUvS~)V0ubR95d8%M4z4Fj!2m2OuT7D5P1=pVK+#l*>+no8}NZ&F}e4Sr=be9^=arHs3 z#dCVk_M_CLV08v9vXKiOP^YyNvmND3oZf16cW1MwB5LC<>?xw|^5|ZW&a;B=uE!+M zcMC3qiL3ib&L^RuB9EZoy8n8{^3yx-x^e)ZLZLQjsAw9=kyZ>q-|~ zZl86sJCnm{-!yu-`xEfsy|uMn<$pPNXcYgKeZ%S}G2OZD z8E+|Cgjj6;=l1{cKOlc`{NVS~hfj%E@HA^C`?i<=kbM1h{kP*ElJBwYn5*A@$9LbK z6CsO4^e38Fi4 zHo2dfg=EnxguEQQi6ZH`i0=8O&=*>pC-gL;&VKAjom1**;ko_c*pVf((m8I9lIXf? zt1zxDQb5e;FIf+KkZtT~iYBcq(kluQMY%Rr&X#yCP}U6(@<&4g8rE(&G7|gk_n;g~9-fQzy2dT8fGMvF$NhFJasKge|&Ea|7I! z7NAJ2%FWx{QDJ#d7Rwh}`2Vo?-ce2M?Yd~_QWXqUEObJXlF$SUB}fY;p@T?=gd)Ak z(tv^ziu4kS0g^y~P^32nr6bZ&s!Cmeh=72o%XQ{=_qpeu?~Z%+zW?nr&i?)g8FMDg zIS85a_rA~j6dCG)9C0nW#*Xe?#13h4)9T2|Rdex=7(@R&7^eBW+NNZ?m#z>X5W1ah z=ABH}sX(y|n$^4Qy{;Wrm>?@B}( z8cQ%X@RnX74JfJ!$+46bhj4fVjajzOxtTk()n2X>ei12@_+xt`1%$D$F3xsrvQT$j z@~-`Wt=GB+5$uoNBadENKsx1qiVyMK;PME7`A)6JE^W3LcCb}`a0!6ch_7&Jz>L*6 zvigNfLuUymx|q$z*!)`WpH^ye2Ao4x<#&$4US4a(zE1nPpmHIUprN5C^c-c)rWV=Y&bgL#DCINF*72= zX-Et0FHD}7pDWh#Fdj(@wF#g&NG+Y0%t>^d)q2-vcc(+9Pi@uyk;#34oKet|Lfv;z z96j-SD58a`A8ifEX=59t-HPOL<0#9z8c^(PoBfrk74!pD|Ms?5-z(MHXQod_ZciRK z&yKY_(bg!~QNNcSDbi4Tvx%AglM(%9R~P5#ic`akFPE(Hg7=XnOAEEPpndj}dEq-r z+SmvD2E!ZqSV}VF4}|U-VdlU;nO^c7lrD>@Cv7OYqFM81qGpVK`3fMimkv?j9zP53^H5c^5`hL8F z|HwCDOWbpwwrJ5qz~ z{RNHc#tZ45mW-1_th{87su8|+>n_*X02qV7GW_z}!b*1Ab6j+^#Xt%0SPobnDI&P40{58@=gb${F=3bXulUg;4bpW)gqpS=#!21=i+wZ*a6jcqxz9<(Z4(~gZI60k%`rzYD8)Jhk zOk8b?T_Rt7BjH!KEFNUwXKSADi)A6%6b78|aNoz+Lb2A~@u9p<=#9?W@6lWDri} zck{Zt=zL1S`@#W!@vW4(l=Sy2?-heM`huNP>_)HOs5+(%8_zbT%Z_A|nSDZ^94I`F z^pYKJdL)HkBX=%lOKsH=>uA^B1;kQ%)aw)uSmHU0r%eUKx%?)2SF@%N5aB<%vP2|N zK`|)Bg3YCZe*b>At(L({!FFp?lgyK2e=uEf=$kd$Y79=JWJ7;bI`k=(yqD~ysoDOw z-&~lEAw-=0fy8zbFIO?+*mACILRckOG^*Jj#KmLG05=C%)t9$B+X?yY$;lCyx^Ike zI{dV`iO)7G@3tFb5anhzX5r-NYwG}SI6 z^jg@t0a(LMiSkoCt^UgzD@3%J8z(S^fVE`{$;oEB7Kh>|)4Ud~*QTHN&(1XU0P;EE zEH=;3U&~lyc}!A3BCz8=YlzH9xteGd;>skUAh6()K$Tv5jP!e(q4{*%5SI&@DD5-) zO2fjX7n-wz%H1TGsB_{joWnCG4WmWHE{x>wKvqIx@SwGS&&KB;_Kkf1vhn$^ zX5%y0HFa;=c+5Z`W@;bIaevad`432E|D`y<9Qzg5j6hB&TK$-sUFp(9$J&Bk$oibT zjcO{zyz~WwsQwR|9gNm4S25)ar32Qnbe?IVuj6bzQp+vTc;e^SUi|X3l{XvB`q8Ln z7}{vZJot7(eVV%AM@?HZZp7Uyy1X16$)1(lW`+ryqzu&=^-5YZGPQjXKN5DabG;q|fK)xrO^s}}(Nfn%V9tI9+1d!Mpdly+lv+!+s5_!N<$skynd;2ks zkKXACDa5GiQ*3=?c(vh{thG;i*2g&Bvwq&n5o6Z5yTBW%G>FT8O>f<(H?u zd0%Eu)v%Me3Id6XA3WA|3>212fi;TQOSnp98SO=8EJR#6$hY3vx5Vs|{cNSphun1= zgtPE$v*bL_#ooY}>wN{<@TC(H3S*QzMBg z@SW7=!Y*vh&$M8cWqZY09`Q_tMi-3I$MMs`93`tcyZ&tCo@?KiGgOvgW;E`)l1=xg zj{GdmtKLFNa1cu_!(N+b8i$P}IUeXKsq<<#ZBD^)6U=io@t#Eee$4mCzF=KZgi5IM z#tCsufH$2uC{=P^i~#zu44vn z>@~0DWNO65e#t(NNJ@8B`sGJyoKuPJj2v~&OA^rpqJ}%S3A7362 z6f({`zMU$w19%R3*EoELPC#og*I`w^Voo0T*@*ilRjoV;Qo-xVIyhzAXX83PpTbdD-8Pq2vI8PC{xaG;i`oLyjp+UC;hf40 zV>9eEz-){*9iWHGuT3y24T`1?lI8L^Q*p;sue?(yguW2uEdL`O)Lg!3)T-lI%o*T9 zIXDcfz4sHjdItY!Q`xMRse_3#{K3_nA(sE!$}ukw(ddxCn3@IWOytX-84Gkfd$Nth zAS;=$hZ&*14jrpFWpc-dLu#eX)rcM2$DWVy&(Cq+n_xVd&e_54FES=K!k#=AK0uZ8 z-l(KM`ox9Rkg`RV)1bzibugpM=RYl(Up$Kn7+Uh9HqMpXTyom1rQ6UDsGd{yN)#%P z3W!!-S!`63$FI|F?_HlK-Bj>H2L5WRM|!xJZYqs~d$z?36s{?h2@6!*=TN_1*8(VxZj`h*f$yJi;dO^PPpDL^X9-!SDJ`1SjzdqYCI2mL6T4QqAH{qNTW0UQ>@F(T&cjTwY z*;`NAD(|@$rMBQMGnzjS%*(L#I(&csv-Nq6@I_>5ONu4^#kY`iYtXhW=&G_3ocHw+ zy3TAmHlt{|r>7qTZNWesg2z;GS0FvpDkhvDBWbBLNFi0O0^)#Dc z`o5u$X6Br#>OWWAU?5FL4_rGki;9~RYX(Aa(Kix`TRG?Yf}-YZsN{|tRqzIoZ=yHZ zX~_jyMM(>uhxC$`FrPaYp`A)7Wl_h%ykjch^5BSPbTjhe7~ZwnJUXRkrA50LT*gv+ zso0Np7?NVb@EC&c^RHRg3g`d}EW504a71ver&1L2DM!AVv$a~Eh`jR5CsMofi<}-|<>&33a~Wp0-T zn3_MxyvDR;b$74Wcc~-WLv|{#ajx-;g09Y7U3Tg)gQf?f9!L1&t+0;P_wa3(d#f+c zi4ID9c5d6E?=F&*nbS|68P+U{dW%(^5!2qyY03O(HXOu;iyCu~3q35tA1_VcLaCM< z0Dd~PmUGywrP(KwPShF#J5SSS)132Ed+0+)3%0o4D(?#}MV=G(~KxFk(&$8<7Hs07O-4STxP3Uw7#h|CcpW2+@PF;pa@Olms zFpr^ZeLA6B-@fISP>N4t7(teNSA+>12^c*((+y^#+^v%q^c*Ga_-TyK`0pk>TkCocI1c_c->Xi`JVon;g{^NIsF}e=Uf54+x^<|0RgR{YPT2|FZ;94^Cdj+-d$fcXi=B z?KW46k=wrIY+5sDf_PMbFSx4^J)@eWfj+I~{Tyr+TI7U0T#KHsLyA;^!B zgO4mk@?0nMcX1Egx2>Vpir?u3z^;wiJOQ-B^PSp9=>zQ_n$4X`D@MMcH^)GC3T-3r zc43?H{MTrfYZ_zB2*9{YH{|np(aaS$$Ji-_m{|=8hHj87^5`&%$`?l8SYk%tO`7kKT57mA*9BYIjD*k(X% zhS?cb1xkpXOlS8hWsc2~!z?onVErLFj|Jet+hw@Ry(F>g ztawBo6C^URo3`SSGvqcBqx^9HwM)J(E+xNE0*^NZ$X0m&*k+U0^tcPfuX!&XSUk<8 zjP(*(7J_C@ZoXtL#eJL}y?+X&)1Dt;1H?UY4Jh+@E6c5Q;<@r1z~^m^@a@j|sDww5 ziyGUzF8747TZD<$XP^6c0dnW%r%7I>q~%`qT88n=8(s8(bf?f3lZSy7x!Yz%m&R*$ zU-|r`RcYI5mvA&1=*IOxvk!kdge1-|A^ILQe4G+Q&FvU;8OjBBX1F|reTzxV&2SVp zVG*}0S?&{kkVVEuMf_S^j@=vV7AdwZsJg7FdpB%8c01()r*oHph zimG0?Qh6HaTJ*^-j-bzP^HR{YVzcJldIu)tZLn$G8;{utkrD6mjt+Y_R7DZW-N%mf zL32-U_G%J)y&k3ARNi6Aa=tb-@SUa2QYc;zCbwKR#h|ps%bPReEStDlDE|6$iC)Pp zUeZrcV+{)09ko^StTUU9zB7F0MDu?&IVKQdh_vo#P#I$5c!~v;bC~V5eXFwjIc0g_ zqn(L7W5mbEn(EGQ`K&pdN-^%y#ktyf(8)UfZ71p?eN$6Lp*ts3Ik|TGTvcXl=4x}T z+hKmQkMWh0Wiw`z5_f#T)zMa2^VG;-t0je5QJhTa(jbiSt-dSj@xnkN@&gQXA!TMn zH=vm#02ge`u({3v=P@4;WfJLq8g6_a_Ysd%MF)VcX`s=V!29pwqpIGRa99P1;BU|L z`S0*MN!qaBX;FWJ+BkrT+tQm)-H=R^+w_)9h5SohGpETNnic-B4YrKx^ z^I}%ZJsu(%!WAWeV>>l;u^}#D1dq(lWdyZVSRtL5UL#jiyN8 zD@xaa#vmm?;3m`rpy6x+9mCvJY!DWgL3fg>)&m?42ra2_euiMfH#0K8Y*jsGO6Y3+ z)R{Ly@=DVu#bc^KnV9K1!AknLg7p@S;esR+;yQRU-@Dv<8KvB)!HGsFUEWUAB7_Vu zi6jFU3{Ce^(GmJNJ*4^q4N~2Z=HlzMO17rY)WvsXPuP=otv-89Q9QS2XssF{iqPfBzDf z-%wrR3re6=o#VSFW)b0&Rgh~gMJ=^UZ1Z}7 zXWYFawxb;8sk@CMTnGL4XgNC{#KgaYe!P=deRdAcy)T~^dMr?PFhCv$>68iqNM& zXKql{Y@L*+#61Xcc`(UPbNZG?gChnP@wc-NgN+G+b`qtX((~qHf`D3VIeRdrBu4>b zo%YRH&hC&<#GPPnGR5a;$IcCURh&pxc+phidR!4y|L&~}ldUp?tu>IP%82bEuzEcS zt`DS|VLyb~sOB-?b6MXS`ZkEjcg`Fz+SkL!bd1w0tQ-zUdcSH~x#gOx7)8w9Reh?O z%#oi!NpI#aF#I9McNKsiQsT4zxS?g`mf3@|e*02h|DtuK4j+z3QN_;sS2N-Hk*&c? zd64RT$MSydd?nHFb7^(pB6m}T-_B%5lG%%#Hb8@H=us;i!_ z6Q~KCy(Gpx1tt{XW7@H23fbpP*EAhX$!)IGy>B)lg_n{InxdC@Qi9otuxVnu2;-=E z+%3uIrwBM6%&;#{E0r7uMKWk~Ni3eyAWEL*oTC4xHm z(GN?8&46#S0;H{O5r0d@SMdlt<+X09!hJZ8;@GVe)Z;N`9M3a3ZLzFGNuSPi(#3vI~k));o@kmkMEW0V54ceFQn3#4A z*hg1W9c!!wwJRB6`i(x4wCJCfT;Kkj(=Lv#s;PLRlbz^b%cF3KrYmkiw^9NApyCIZ z*wMQ^-Zt{6X{KkM=kmL?HyYTP74OHkc~!<@`P&KQEbTRC5lWC8tlloEKU~FgH+xMB zC0j25-jLuz($g`BZZ?IDQWEM18W$q80LR^-)ws(oB?GG)##%NHcq<>eIX*P&ws+^U zfMU^qgD`0t{UEE>SvEL7?A`Z}P1)p#>+!)Z(ZSr0#im-qWVc21_{pHK(@>cu#mlmy z`=?KHwoOBWoG-3a-wGnSjYC1h4(~(Q!f=*9D`gD?5ETx);G}1 z%u!C1i(!=My&45# z5c4!D#Ipy4J*_3aVh!(bDOq)Ww9!|)jFk)-NFOZ=LW(V)8;3613l6Q4JEoTo=}23{ zl@m!Z6q3qg-U9LK{2oZ-ks={%t5U@E`Y|X^_Iont3dx__M6bT|*9zwij7?AF>Qyyf zUr-bb;{2E=nWMy)fdStvrxjaP?ON?RscC1Rp5Xz0pHV5kD&W+QyaCbWi&=A9k_(LrL8;6=9HnQo7pg%)YCAS zo1dxkin0W!#d%9_f@i;}Ewo+iC3N}tN+b$xh|JtC{{w-zfnV?3P8B((Oa!kaC%k^6 z(^NQ4Q(%$?O$N0KdLL6VXLb53&H(|g84C^P#~ey5Oj5-_2k5gK>FYqxWv=o3JIt47 zwBOFjTRFB2$eb(mU22Bol}HI4gy$oCawW|a`BlUoVW>N_tLhukx6J1(^?C{9rMG)1 z-pYR^8+^UkbPJVi3uN)yej`?M@*JTVa>k&JFJi0I8{Gaupd;R@?@?oi6?E4#Z@Yf%IgPi{co#;9vV}GlCf4Uiy{{ROl zFxJ%mBTVBz`}OP5*)O~#`SRFxS(7&>H>cb%K}~qQv!-Lg&GJ{)L|m689>5{XN`2&_ z$RV6^Zjm-5Sstf>jYL-3qJvGeOpsjgNV zEvAZqrvZ1rhacU`L$gc}(5W{2h(mOMZPcR17;-9a-;z%>=|l|{tpH^0v}ToH*!GBW zj>>h8I`2zOR;#wOEJcUM=-hfP|NLpWpJgt_j(>7Yl+#q!t!R^jB%{)@?d5E^vo)xE zd>#Zls)s+Do4bA9UB`?>gdxp>6K+Wf_UV zU|^<1Ceh-l-1Kk62#(2CXnY9-^eX$65?Uy!l8*RsW+kX!cO@?XWV-p#xJ}4=L7J}5 zJGH5xJRBr;sATjoKpxEYs?pP(P=P5+p9U~hVv05Aa0M!1jLuzBkTl4vx`1qo@4Lor ziV7$XpmUovZn6$hI2j@jKtkd6?qXcu$RUt@1u6P-F(ROLl%PX@Ca$82qrGdb-|i+2ehI;V)kY)szG@ufYy>d?BvwV`h?JZ?nDvtmq3L3XC%E7X|J-~l=G^aX!Z z{-cyx%@hsuYlfr4Bs+E~SbHRK1s5^k$Gl}U;}H_uQsZ^S-HUNjD^Fm6^UidgxC68E zYCF}nTmJ|`rTOGaUOJRz%Z5&@7lWT#Hq2!CvIx=&F{GRqXN%h|!5&o6jP~mKDVR}l zP9)nA8A)*)^}wD?v3~OO?RD2&ZHxYWbv!a^>ej2QB0K7W(_na@Ge?oNt&XQ%zo&r7 zLaMRBN>N~xceCdYrlCZ*Z(LtS;<)4S)l1*Kwas9Cn0DMa?^-1H^7cFb%=k(!jXG2+ zLOiVU{Y36Zv4GMC_aeh@41!~p33G_0!OhBA?*gOwBI~wPA(Kd*1=AcQYuhP?bK<|g)rx2T zdW|mxtE$RjrZiIG=902TA3{#6zNGq`w9Qut&iPFPn4Wu6ah3dg z^63{Q!D^IfVUTw96<_)npDc3D=g-9t*S0fLmoSMMU!zHlGcF7%v*S$#&+Z91mv4$8 z)fKmm=0tATY_!7_&rxm&YFdrJl*=5eBPuR=xy)*k9yW^w?%$F=o9F4Nslb;82PDF( zGG!RDY)EJCSIMPR=OORI4m=h1QA~hV;D4HylNajNDg~~&k^P8B$iMK4I((`0Bie?1 z8Dg&hn2QUeV>iB4vP$iTlU8%$xAZ?fJ7YBlmPm^u-r0AMf7+UIdzlz@d>`PO%MyqF zGewL4yPx~hFN?oHSKoa&9tW}tdHdc4{F7qz#e++~ZXIcx-RuGQBEZ~UhTcE=N$Ixxec}6qt>UX+pXmP$0{k;fSQ|DTbp75gKYg|g+j;pvcHjAK zAo4e;4qi!wpTFtGfEHoK3YEi{%Wm5Zm zO0c)}+%KJK%W9DF(=du+;k~MlwdYF9CU5uyl#!}RxCCv&g$t4Z6k1mc zg;H9@aZD2aff3z=dKUl?5!aX`Mg;+qhJM8S1F1Ge0+6&yF#ajD8wmk}O21PY#-IcM z*kBlQPTa!oRROh*N|z9*9V#(^?bwv;t*(~&3?fu3F3p4KJjR%?HAT%L5(rVC`K1ds z#pmSF7yM)GuCV$aKfz6w48G)kFlgZqHR0($=^(%cNJ1Vs?Shq2%mmOy6Q^~_V9@6Y zUlNfX4`ArPg9+SDnIyoqgrZ;P#RU(m==R^_&aowU+#nK=N4Ig42m(TvPi`2x%~+CRg(A3WJTK z@?*xiy7|ByRS@?4=X>)O=db`Up+^jn(I6up{ORIZJ&n|*o% z@LSlI(Q6UU1U&jfD_#rb-I4xnJ^tPBSnh8S^=6JNxM0#Ts;@kC47L2d%d`jf<{2l} zMA;4cXBUiW^!ilZm}IA->Nfl9lv?u1)VsX-YfGun!;NN(n~#sVxkXjCEp5hVKmwB_ zdwpOp!#Tlos~L= zW3%W?t=>iw=|-`2uI>8`+vkDbequsO`44M`!=n#U2dFybAx;{=EIqC3YZodW&O3^| za#5y_7Jl+0W&E{J>~pz?o6s`_v+~e4K9dr-tSnQHVELjo@q^o+Pclr-)7M`QL)m9L zCQVIj10f@Yez?#ohQ&k+hSqjn1goRasQS+Id;vt;$Dej&ld4RW9OVMW+Fz4w zf01&usS~foJ^UZyGe~$Y%OfY5Jf*OqUk^F*$1qM#$qBu@wlil(oXfIa6HZm1tIXHc z-;ew8CEi}H8qr)6s7AT%CG9(8=&KC(sE;`&apq2acym9-Hk+fR+;`t{yyepMy#3X( zsnNHrdChhZwRYMC%M~xVch6qQWWBX%VQXO2asT3G6ajI8uy)xvVT^(a)A{8avI?%E zdGs`8N7%ydDcoB3Q8_P%k(VysS%3rryvmGeBOG|_vj6RZ&5E{cfgf|=FRT7(*>p8h zGNFX&rcMFD+S|2J3^44%pDCg6%WfTJ5fqgqORf7IM1Z)303Hcbd_hoMC-pVeCul6=kYXchh!>kFb z4Jl$(NodT}8KCV%AznhXnY05uv_9Hg)#%3BboLBHQxY9j58@_lWb-E&rV8cz9tHVH ze!)CZm!$EGEX3Dxafb3|cs(7ngRvK&HDKf85nuWk97P>?`$|kbXnInd3z-NX4Xm%H zc~kylrEzTBG)(8%t>5r$&xq}Aphe7+-i;%|^EX&QRtAE;7=@sQ*IP}01f7DIvT06% z^b@HC&t*6(nick*#SrL%$BZ6>CzY>A%Q$_RrbkQhUJSu>3Wry)Fwhtnrb+{wl{B`k zcr}PjCV9fvCVIyeXVrK{SQU>3)p&WTTuX>q2A|I6o`GTpgQjFud6hQv?VzB3@D4rD z*5Y{Z&O#=PU?GS!9T0oOq^?M0f?c#uNH83TOA!{jzvC^v;7fN#BK4B#aTmb>6;lUr zjA!qPJX@?iULqGFusPDon#!5elyjUjt8!+{h(qy44~X?N>!YxT^$5UuZj# zkwG4nvzq^KbIi=BA7tY;rp+&!kta!d8aD<~MyzIl773Z=2>nC`FlZqCA)cU;F=j{| zhaM`b6Fr_~SXHIZmuY?3tdNMfHz86pFF=__-=Ka2WusOBx}TM4OKKA+JA03kA_NMM zxB<>5GFGsep*C^ATwERmZX=P$K!!1sy26tW;cSps1SU-!AKt_nn2AdQ#fdz)02q`A zspvxEal(*S`H)qdFsC$h0;5|+_i+Y~6|q3WFH))5v*B*2YP_pig(U43cMr^1{Pt># z{QPA&xNoqBN3{4*4V#iCL(X<*S19O=8olZVS(Oyaz|HR`QcSNmCvQ zE%_yP*?_(h$YyD&H>a%S^> zRbJA#7%T`TDRBJ=T72BR|+EQe&>38!+%G`H(_)Tl_~@mGY}Qd-Z#i|+p$yJDFYc++>JEp@br0z z@7f?=l|cJ`v1xoO%W4$&;JIiRBo1(+AAHF9ym>_Jg=|IYhD!yZo+FRJbBU(^SgFQg z*36g+V|r)7&S^@tie zmW|e;f^&zeg}Q7Pn(K1KEQ8mywRYkm^H zIL0E*_D9WF)K8-Ui2szU0cwkv3~8RulA=ouM@MQ@iCkH>74~^%(nxWvHr#k@G8`}4 zncSc_{18TtjA$9*Q%O0lZ7a_-40|+InSb*!tn4ehc+bA|Oxd#=-@CC><_dP38vUx` z)|QU92kMebImGYTNQpSd*=$8<*(!Ta>hpYVp42&(%Mc_pvehwlLJR0Sy071gc3-i> z2fJs=ZlAOeZ6k`Cr&>j4dMO4sIaA^*-mm$C0gl82)4ej!3P@k2nuPlMUWQ- z4hZfF`9U!gl1ivs3Ncga9KQ53cI$+x)~>g*e7x3Lvt;qVM%*<8$mi3r-$H^@-pLD3 z>Gdels-u(OebaD1kul?T__no2ho$Ds3-90ApZPD#Quk~wtNF0o=Bi3bksVWMYd^nJ z>zy~Q%jXZjntky6|5BX)M+@}- zS&H*N3_KhY)_5U_7BNYsSu63M^*pXLeUOfse#7%~LVo>}DiANSYo@xTK~3l?*tbK? zrZ}fzh~B0LHd2Dj&FhV!C9LsD-&)%Ir z&V*hftMP)2WYG`wNG>!{2;>@YEZSQVYL)Rk#~Wa1=1VRl3^-O8GoYP4Cm7W$*dibl zU}aK=AqdDBI<*44g=+N;A@~4J?#q}Y_QK598PG59^b}MgnCBdkc@hEezOtLLLFKfY z&4!xOe}c-gppPc#r@Zk1!mW*g_yX@SHHNdAmPixBiF9^^R>7W18;q$tPCP5aat&y9 zI}%-_>7_Okz}=W06djy6-9yB~^ps32bCrzvl{(UXngT zI^(@l6>U2hn;udott|X_NNJN(mbFH_CdR8l!6vU|lVbk{rQ;if zUwWG}V;^PKaCS7&R&FGtqTWI{47&IqQ7_9h(bh*(P`W>plC)*sWU%H%k=>p&vbf}! zc%7GAc#!)i(wY!_ah+fR_iAGISDC9GRcZ?;41$Qr$KE#&w!2Miee6f~q7mJeB~5zZ zqti|;^s6w(Hm(?&vsUyfrna`9$s*WunYjRk4m4!EkvGuk$(Nh6Xpt0_VVb;KsC}k% zUeGcM>*#-RAsy_)i`sdhp!-m_+v5s*L}gann&siki+^seeC}uf36+y~O3dQa!fFfq zD`HWKyZFPXx+^y}hu2__DvvI67kq9ZOr2f(vRQTKPBQu0p)9UtPFV5wlInCO9=KU7 zdv0xu#`jq~WlkBEbk`^u$UaOv{_}^kjv2jZw_oztgw{eTU<7$=Gu@(`-i3m{wR33r zDQ9S@`HrCynZ)tvTb=EJrFH#aa}=dH$e^YQTPrKr(SN$|ZuDEtcTvdY8*V7q;PHGG&|-!(`4)@Hw*?`6Ai zsF(}nG?dUC5dL_;FVD1Guoo7xu>R#OChq-Ukbrw3>?J{&(;X3g#O3F`a3BbmOy~dX z^Hc7garMo+yvf0;P8%@(qLn6Rj+g&y-e30{pbyvlmzwoI?8W~N>%;f-%lr4NTAuPZ z>xGB^K33cr{cDHQANf}*f3x(Y`w0N7F93UDc1Cwm%0rW)CluofQ4TiWS-UxsTGejR zw4Rrs+@x-;pJOUxK#J|&Uz@Y)u>PIXpRF9T<*Ff$nVg#qQa1F>cPkhPTXz*SKh8Z# z^jY~ERI^Hc^sB5CG!%k`M;sWZa<;+De8QdX63Pz`j2KNFPpOtc))624X%rVTUsl=Q4|D!*$%oZ16hRcgx_OMEe9dKPSFmeVF77jKq52 z7h#Nuvw!O$m?r0#-2hnPpxkyZqdK2oyS zZ~^2iSng$14hFFKsD8=M=phO(k>L*+9i$?ai~tmYGuf8DAW~T-4jf+LU0@1lC$N%8 zk~n^-cNR1ov||Few@X4G1DQZ)PIqL55Aq3YQH(VW4TIQic% z2^1a_DBTYb(E=gCz`<1UXk#NLa1($ZfYEt0DC!%9agiEo$wEGF#txc}tNOIvyB(*l zv6>{Z)XeoO6ea#CO(s46PmjfwTDiwh38jFmpNtXhL%1p*?6YThukhU9$(xfMke=U7 zi+KUM{s2fz5S^8AT7T5Ocm5d}iCx%LrZA6$DfMoAl%|%pq_i&caztEzeg7yfhjvcf z>)H(8#6XoguP;s$bvZQ|X;aoS=KD7&P=oi4VGwpf5gI@6*>Nc-IQCnj zZ~XI0@sHD)8|48wPaBZ%VFi6gvw-W_ol8`X$@aF?i*h}-=f@(W8?TCJ`>8p#6Ta5X zzmMBv@V)QZ7mW)C^nsHht481KdsEg4gPKVlg`q_Z&V4T+NImb{XP_Coj#u(j z?iL{m^z$%Ywb}HgB;wF~pX}23(y5X$@ICCcz29V|Tgbe2qvdej6CZ`t1lqLGyFC2~ z&u&wO1pm$QtUCs`7ZajsTlhy6`*XM7b{$?H*~~6};~k^@E{T`)bM2_=R7b*o_3gpg zyS&SWfT#zPmuGjLpTkeb){CDyk0pHUXnBw?j6WLG=p5?_>LgYAI;47Z zD+mXLE&NPHC4O}WfgP=_)9_2`l5Mg{zuMV+XduxGPR!X5W!JFE$CkHZ<{Dd8N}p62 zG$aKiyp?IBI>?5*Ukb*Lmz-ZIk(rdMB~7pT8>7kXgC;evJ#&oWe=~5~nqdzD_N_g# z-O2Ij&BG|lxv}n6h!j1+Q*nSZV5#qU%4);)QVAnR1)F5mR%XOo>|foBvbH)UWPVP^ zq^mJ@>_Wq#ZJvb45^cZBWThh1A4DJ(Pw0vuP#F1?y?-bPz5b;n z{4b^?oRhq8Z`$n2w?FfZVTDxf3emAysCOclb`De)<-HH2f8nM;6V5gORb=ipORUzv_^^Y><$}@aj6A|N%IoSb~af{|h>VPfJ$BA>t zB!G&4hlBH)l)v?P6Jua3Zh(=|H-qAAcE?P5?+L}VxyVR(k30IZ55b|QS~g*lBq_YB zZI(!7nt2dr%EsL&P@2KaurSiQEP{Qiy1Heh$+M&hq_J$3y^pejbDz}|L)i1yXWGE7 zmW&?IvS)D!5PcDdG6`oWLhC{)cn}NpBa>>%1Y;pW#1gP($Veix01Nd91oU|l2@I|Y zD#!{;I%88)I45b}0vIVXkin>Fx;9e3u{p?+QCz4&=vVdvi-83Z-VSl&6(xhk0^R>0xB3S3EHe49abkYXO(0p z7fq+fOn(I+eWT{ff<)Hmki>#41n7E91T!Ri2c%?#l$QXh1(Q}VGp?2(q12gI8s%Wn zUo(M})jTk$lloe+08daz?A3-!EkONlW6dgCB2YIxTG{749m_2{hH2BnQ@+? zgu?kCkajYd;~c493$=<96)mq8=dczJg>#U}{fvOTMFI>;1bTHwu$3{K-2{>id=v-? z36Lfp&@rmgVouT`)`(&++ebpzBRAHvgS zH7RWjg{d@RC`eXsYQ6oeT7o%dHb2QF`wI&^^Qq%RQx5A!HNp7OT!lkX7}VLv;qsw) zTqK9>Uhx}7dS|;rmVCflZn8D=BbV?Ld&$$2_trtHcZ_6=?HO{$CVJHjW#4mz3| z?!RY7S08*!`b${oZo?wqrNJx5W7rb8jt==im5mY==dNkzJ7y*t1K1s;ui3XHjUZRogne$7S&TMpyo!%rv6L(PJc%Bp^9}gr$kQ3 zLoh+ei{0BoJw(9QM9$V|*s19ECcpco?;7BNpMG zi6L*b}Rp@?f$RUcJF-WZ4ImLx!f!l42rQ%A#&wN z_OqZcWs=%elMLE2z9(@*6zH3G2?tZpYg_w_`@`k_1b>yypADSoyvA#!;5k;=Zp+-Z z=i(rqQ7>ECab^rlj<93QuzupdDcnJCRO3`gnE9>28@g{u|kf9uMLC?-ST2ly}Mm2qP_- z0t{vafsw4G?-&?oW@RYd4+x#`O8@wW6F`nk5(>U_7)(<;k%w~{}@ow3xAxbL*~(x%sD}3qXg?qNDVH8m?guTZfZ@tV5MMtHTFR`9K0St$=AV} zK0HE~ZNphp(9}2ot-ULchjM-U&kV*MYLumf(2OE76GDwNmSHdpYC;Fm%oHV2IjRxr zG$=7y#x^l4COL>`Ij4?Nnqp?Cl*oD%TC{3=@6PXC&ilu^{PixMk3Z)A%xAXend`Zq z`?{~|`hLF`+7Y0`0A>tSltJjbKA(*PMp?#3_7fxi60^Z_3q_d z244nF2kjULb^$(Y#~{lBBy=6su@Tgu5z%K;<-;tH>6WtzB-|}6nx{XI49Pjgj8r}e zR&n%W7a&XNaCZ{>NWUY_U^s{J+K;X3ge(?;qPB`7-VF(g`uT=OouC_bMY;J>M@UO+ z{~@04MET|On!-)~`s@PFyZM;%CUcZ+*Y3fR>V2M~XUuf$e+x>W|CZKa^vK&TH!opAUy0 znH1XUtr=h1txDT*mEjoALVo+s7p;^^Ff7%C_hCxl)>w zLOxoUcS_!&Y-MMntPj-pe}%Z210Pj|IqvD19xdh237;lXkbvrV_WU>;5)j z=&f%E!&a-l$e}>`nc`#hr3GQe2Nu|7bvlkJtgf1Pw072Yy^Y9Z*Vvb)I8pBC6n+w_ z{cPlw5#%^kC{PqJFEVVtLtE&uk(u7O0y}oW(M~2S?Jiwq)8gMdBA|F#+={g8^`DA8 zeYH{gr-*I_sqAydk9Z2+4EC`wX@7@X4P&~1BK3>H(mS)x7U2oE_8#`A4g0pRaxUpR z)OqbI^ZlP8nNe}~H}I{-_V1^LxqXK;gTA_cheo%5YhM0}^Bq!De#3u7Z5)KkA7ML)R}$0SZ{ng@f|vIbWV9Vx^nM( z^3;EJEYdsn;Tw~tIpd7%+dLI^B>>8^R}6{&HP&Y zFOF^APtE&quos#4nt$_}1^ZBmd%{rbgXGN zpqK44{l88xTTZu9_ptS1`c9IicQVL_r9R98at3YbTQNVq_n;x)KCgl^z9V^(sCF-M zQV@{NotPPUGS=Vj*2>A~NIKA+5M3qJY&_j-Lme`?$)(cg9{7v@%(X2X2oK~-=~E?> zAl&X!vQG>t9oyoKF;e3;tKO969UhFl&Y4%+#{`)p$g5!zVPzr9?wCf+tT`sz8ts;4 z!Lc^NF1$$GTEhG_2W!3*=+W^pEQZkJkkAAN;LXbzqZAy-S2NHo6Tn~w>E5c0zJGLj zM2n>BxvBq(bOsSN+yn(m&P^Z@u8rdhmNCH79$(d}LN??jFAegrT@TS{@ph1%9mw`J z@#G|`N|p=7Q@~TF^j>fzu@hcau&k41xFeKMWh}F41i@Nvs?6DE%io<8T6C7D@yl(A z`(1nV?+3ADAB)#GZSyA#3VVOdUSSI;L%$&SAn`H}03I{^7Y^aNav~q*hqvGZfiey> zt!%Hc1ek1MTW;@S`T%ZG0*{*wqI=a6kyeh$K@jcjMINgF)n{^a1>PA6Jt46HY|I%M z4Njjx@*|*(p85Nt=`g%KTFqSx)SWd%au7>V>y{q7qE>7+g66A&)YvpOLsA#t2t+1?J z^->}yEiCKS*p4|@;~W-rErVF`a&@Ag$)_$Z!fkqWCk(rqU=_)%wO?RMx9-RBwDqqy zTqLeLbm!_40db|{XvpI6027E47(_er#s^s$Yn{YhGU`Tz@aS8#kfR0cZN)7*?DTES zX`cExJC0}%t}hUY(PZyQJ<8mf3hfms)HZ-wa-5F^<%F2scH5ex+9cvnjP9;x39Q>2 zMrcgv&?Pk&uVr7>fV0+=+&*nwS9+tYm-lG|8#whtb@(3B%&jlu#-*gK2eyJ5Nk-SZ z+PqJ(4#XWe6F*0wO48#p&RXryFex8CgVP{-JS@(_R}Pra;m%J+DR+-vLDX(3f1nzg z`5RgYm#yo(Y3uKtV*b8n?Xppw2SI|#_Nfo){ak~%2MaFu+Ijt?8k*bOKzuZOp*eJw zYq-LSXP9IbXEBTH#d4UI;{Y=^nU;OjT4uzeRxzo z9kzV->Gsc4N@{4@{YyXyjS2dSRlNBQ4aS{r4nKPT(9xHTB$AKQ%;NdC=B+V5H|B4L z`Nd&=J)HLr|I-dpIG&w(Axwa0 z&E|Fc=1yJcnThD_WI73MEM95VD~;&#eTY3_?lRhInP9cUI}ly8(Ej?*$~88MK5p1O zBUv!}{LCNIlcYl;7i@gN?N6!W#k()xFU79jLD$w{`)PZ-ik5tKUdG5>(|;tA!>V~R z<03p8JK9?i+cGZm!jzaM`XUl!#^Q8=%-m|Y2wo*KmB3a&>d4<1&cSG&8H6!JYLOY~ z#>2&16c3e~yJ1`>z|e@x>A^$BRqRM5AZIomPd~{G=w_hwpULbj<=rqp&nni!oZ$># z26Qgh*HxTFXkw+HMeJm(7?#=N_Y~k){;l+<$%XM>`cqZX1qd$sdU|$2AY{g9dM0O? zEoJXY8vyDJo7CV%iAWOyM6p12r=KDsIKir7o|bno9dV2fUz-AY#v3WP#dMOYavi9p#D+Ek5+2Q@dAZdGm-n+C9}sp1YN z%+N!gdKPl1s(OH6!m@_Q>>RB9b11)nO=mjc^nR;`b9Dg}W*XT?VmtZXvP{pZ${`P@ zXW1d6S%w=v*6y>-GsGnJ zJ?JX#a0ymr#g*w(JVh$_-%eJA_gv6QGGm}%?RZn@FTHc!7J<&Ff}yM86+n2xJ+m>z z0o}ywc6@jrqXvz$Kv`6_BF0cJP4QT;HamSy#lxG7_S>!iRi$UYg%aYKK(BV;X?|(y zz*H$CW6feAcd^ibV8VuWx{9)ls*wXqXbMJLiA><%qr>=IhD{}VUAO9yC>5m5^dsTg z@^?)4=)A?#LX_I}tpg_vPc28(Z@a5bgXAY4yJ8xnPGHshsanVUD^sDg{P^(D-DBai zL4~{70t>5d)iYN2@xpkZ_+fjb`H(-E?DgBKTy&*l`_ z)OVp0-j$dpKMwD6{Yj&^^KHF*Q@OzNCiCPP?dTC(TU5G#u5?=}HEJY& zWa)vrQr?namv=(72bFiiRYEq`y@{)MU1VHyf907zmYwSaHy_nymKy4JZ4f2x+Nr0{ zSIG%jG`2KE6ZSM|8~u5M{w|xLFQaUTX>kiVE%n#_(rhyaJrh^4j~)AGC6ga&Ci9Q~ zzc$eOl{RGdHrx41%-33=UH1M|lWeQVV+j9Zi*wsWquk!ex$n@J3-)eggp;c<=dts( zX6K#bbrFGIiydU-Qz_%(fuKvBGVUAJRxj+FIeOM_uAH=3`U1 z(y@};RD!e%F85rCD88+^^^mYB%UjLU6zNiH(#Mg0(Ee__NIT! z3M8v>Vxa@k%T}dR)TS0b&SP(Snc<6=!erU@(4uI#uvNszlrsRanZt&OoIz_xx~371K}g!!4lE@B;8Wm=1>H z9JvXkMrhWTgyt;}Hm~O@p?hze$|r$iA@=~u0l@mZLv!m1P(wcRRYM^mN|rU?{c}h- z1Oo+>{Aiw@JhcH&W)uc2GH(EMqB}KMg@Lz5@zk|QzGyt8F2GkXDe~%D7#0DY=#7G* z1RX{^HAIf$=N>L}gf4T&`~Z6d$m5DY-v=2AQw&5DkS5`B82AJGxXG4U1@RDu6sYZg zk!6bC7;o`cW-0~Mp|S|sDu4r5CU+71lBVQQ*-eoAW9t6gJjY;yMw;SGi#`heg_Mwu zx!#@>7Oy;n4IPb_L2)r(ZJPDdm&K)je$p!&w1ztgUCR`iJLK`Qj&1!APvhILGPZ>> zzR_+IV2>~WN%F@s-+y==3xQ11DdEd(=&(n8o4;tH<7K)|An)clq-YF405K?TDFL_wdKDX@ zdC3boz}vj%I^efF?r@L>8|*|T2%<=305AJde0#j)9%CpGr={ycaSQ|ryI?d+(Gm<_ zn-f>{)brff1aj=OyE5HYCr946cE2E~h+S?zONamJZ{fkXN2}L1zbwbLVqir5=D>@O zrYUt6%s_~zl?z4os+5=8&R%5lC2cI*%YVEgI>a7V^BA(p-*|?F(bc<{Sv!`swGjW1 zN@c59lS+IGS*w}?x25-8ar5e1eCvk5IqKL0O{0ivOp!^3mAAD!MW1_5V*RxX5skz4 ze%$HlyzaKmUCH^4{tCV_A;f!c5cXwiT!Gn{XKUO?_MK;>PxQ~)4vaJa2jS2O-Cw!Ccw^G9raNZ>9#s>>!kBWQy^tiQqDX745?Vj%@|Xp)hArNc`aRX4mO3e(RPIAd>N<;T3(h zpzqf`^+BZ_p`UjJBO;KbV6e|c{a3W_`S<_t+el?HDUFw0Q}A32(9a8Y-CaXFXLXa( zmt8m-zk&a2zEg06k8z#$N!s8zqOLy@BX*+ZJWBQ1jA%&kYHB}0Xg)ZFU>1bu6=UhL zs`9%XJ~oYAg$cI1E8zy>4uj)F<&-)l@C6Ok-r4~gKhLO+pYu_wj5PQT{mu&Ko_erZUpA9u}kt* zv#E{~9x4$Q^5KotfVU!U6j%VYyLHvjcS?jTk|*ZGm|#1%*r3_xsUVTf6pr8)EJHI% zOOV%F0h`QoIL`zu9)a*`yV+sZ;yh)RgH!O5PYpON26|`*#TSs%XaLD~i3j!I?NKmc zo}z)U1jyS+LW{i>>{K~S*~j1cJ1N+2``9_DmRs>(B`0p4lHfZ1@gl}3X?pMKL zB`h02cNul4rtmZ*c(+(nI)nr}cMzmpEX=!zhN5$uEzdvmTiJ!vdF<_SZkr8%6}UfR zWX6)a=Ci~%4LF1G9L1Nmz1bL;8Dp|jq6#UG0S>z*;GGXU5&(Lz8~sqSc|WoPvf4$C z7iYwaG=Z)jQ0X;KQ~WLi)IX!2sOK16i)Y!8$UlsG7bT-`c4Kr1H0HAFoU&l}TM31m zdxTlOXb49&W?zp-@utY~U9(_cUq3b+E)V?7jQQ4X7&+})HEgtCZrwCL^CxA!Yzch5S72gO z+!@4kdlj^9!0RE3)|X3FLk?QHn`3|P@YrTqyg1{6vHIy@fv+asQO4^9*fN-xQLcZye1faZrpzrNx?bo+DGp>Rc7TWXoI zn5XqE#MF`E^b-{kz=bxUb^ZFP6F^l+aX`LEh6?CbNm=-~=DG{A=S>bpLjwP8bXiavm9KU!=pG znv!dNJr{SU_e8kD=Z+&zVT#P!@LJ>=HcBPyDtkY9qqMWHq>IZET$x2uoe~-}>Av!4 z>`wd13yu+ejUzw1l^aY+M@`v7ut=ANyg*arpFP!it_hr(71CE%a}?pX-Mh||&2Hlk z{t`beHMwnsP%%0mdcPwm)*v@b+z}NS_~1dj>&AnM)?DA%wR^1I)xD~-zwO~f_C(`5 z)ZTom8uDM2?~U4G7L`uMgWlu4Vx!H<^J>{Hw5KXMrj+ z?E%?Rs}&oAf5|i!ZE3UAQTB`G+U{q}^qyN%Zy&9_b)Wvo3W4r3i3 zX=LkjW_GgB#+6AT*%8ptgGp-abLQ;+8mD9od-JC1#31AYzN4iP7W@0%WaMF{-?uSy zTP_gcQ?X$-!kG@DP7T4+#O@{0Z95s9e)@I+H*2TBo|9A!oUnUcPe zAR2U*ag*Eh96Bi>pAi$F3s=x4XH&`Gzbmy&(s6pUvrG%J6e8GeIr46^?Ut+Wpe;#n z`oc#FXnQ{FT6aX4_shsqN~OTy?YTK}jK*AV-ocvNq3Q&$r5O_lK_y2#^7*=;bcSqP zRu;G_+9}KM)V0^IHZ-5F+)p+(d@$D-BGp4kL0^FyEcroo$U;ozpz=V>8KC}G__l}& zi$G6E9T^i+$m&{3zRA`j(^gcNRb@)m#(jc<8Kuumu-ZCc-TmUnFR3TxTzwDAWWf>6 zNzfNVosiY09H?3R`LBc?2av&Zkr2?BX@baPc`zWv`Qg;yY&FOZ#peaXf1uud)kIqH zE@Y#>^jK7OOp>I2D+k1VENkS9vL|pFr-SMFAi2ri@DB25qaJ?lJxTgdV}MC^W*%Q( z)C_sQ+y1fUM(9Fn)t zmPnzkLG7d^HT9}VdLRnJ!qMV|lqJX%nx{AD-tb(Ha@g!7?jEu}DnYmb4xa80CotOVV z{vEXeSQ!|yPUp~22?3~Csc2ZKjyeDW6gp|Ce*XY}|EQ>GPSDcPGcYopqztG#1E8j& zp`kuOLrZ(&1ZDJn%6-5IR$8|6a@XkC%^m24{5a&pQwkY`uUEALA%p883Xc8}j7*$o zxwv^m#l$5fL5g4{Wfj#+np)aAx_bHs7El=6(#jg)!bWw3hX<2y%23uWITUX!E*wp;`O$V{FtGlOnXn2G?`hIMDVs`G+ z=lO-jFH6fCn_J(vcXs#oe;o5g1)%whEy~}&82fj8u~PV=K5>HP1pP5zRMdAUjfV9E z?Rhymwrl3}4u0%H^5F~|*Ha3s+8KovAnQO!|3M~B5ye^2jbqk+bN2s_v55aG&i;+D z|H;<`fSHDhvUoJC01d#_WnS(;#tDgRV8plm9vW_%ALJvzo{n$WBH`7q?_sZ#n2!M6 z2Btse!m=tWTYo4zseH(DF-Vl{p4XV=Zao6z)gJ+N2af;4hI_6V?Z&DJ=0tj=Tn-1l>zZWW=VlBXn! zP=G(}j|~1_)Pe6B|NPC>Wcts&muV~q3O%?DS`KqNx@+1TW>kOLe<%a`({o{ahcAx+ zm0>>xE5%;;vL6Auws(I890A5@{lHs$({mcx%MOPC3S7khfr~$I`TwlD_yZUJ1rYcL zF8(__rR0W=OR~@>@zHGf?d=cuxqci0gs1jtN2 zbUTnLwpt>p;9blMoA}(jH3AIeT27#mUu*@A0qZnzu)?!P($u-mORy++mZqDB?G+UJnATgvO4IIf-hMmed^pWWh@^x@F0Z<>^cIQh!rc@bkMN{I?J3N8Safm;c(K&|#7D#xglqFoBy39A8zO1i)%4$#3Oki_j^6&>`bk;qod#^S} zk2~v?Rh9Rb&*rNypVL1%(LFy9?W=oGN8yyhIz)iQo+@AI^v^FxfT-Ki2vY^{l?f-9 zNWhKt#1BV+0X>bi;zN$nce3yC$-nc)QWfTCT_Y6GgR`t(zc%|lu4_Mky(~fA|H1dL zV@3H=`go#{^$e1%e>yvxv1qEPE%+5%*lR-9>}ARtkG?rxV+`BOXld;j(FNY}sVa9_ z-^=mM`Vjz@|82_p^zRjb56=eGe~Ke$93E;M0a)$sCwv4f)X%)+;u{q}U>#RR)D{nc zCQU~GjnF&44#EzFC~J6W?cmqa^zH^FX^z_FXrXU4lBHVH9mOVZfKQ8QFm1OV0WSPK zI{hB#{po|&e;fUDW|&OWAi%^|V5TAzIha>cIRE(c2oNT41c)i6TrvHZE6;Aifr3N| zB^vkddCFp6UI-=SKkH0C|Nv745k00qf3!~{6Q42T(+81ba zL!j&LZrJZfTT{$f^_&)pS2Ciuv5%9Uy}lp#!*1!-Uo3^}={`%^zdIbZ_BsDR`>u#S z>UKgnv?|bQRIG5$|DM!)VZIMb(~pvl03C3<>4-Whd9AbwzOX<_wm-IZ6ro3c^3x7JqI$a614g zl$wD3E*z^PoSGOG~MI-*QfA#ji>>m%(E`Qr6*BR>{mU!VUMQ|$a z%DRULO^S{Xh?vYoS9;2NGHj0@140<33L!bYp77D>mzTir%Egz*h%A1|EjioyBfuAj^H$v}feh)gf#aFQ# zTGQ%I+r6EpXQ5lK_B87R_E$YAuRk_weG1PH{@HS01h!$n?GYSeD|Tg7&`AMtS6E;D zo8_YKN#48E=Y9O_o~7*;R7UCUdoQt%i(1H@+?-(K#-%>`<9bkrn%rG+eabLS3e!^c z%MG6V+L9zf%u^4UM6+N`GoQ3$t|;m-f}44&AFZ{MzdISW0iGxc#xVsEn7o7$Ml+@n zb)qYOgBoa09O9EU5jIVkvg5taTAGFCP_5BN1+UpxvnR-aBft$e3Q5QKXxmsKy*cb!`pu6V8~w5cK!73 za9GsEJ;!ZR`M;h3zQ=7C`522Ti3+FjbG2%0j!qx`(3rIle*|W{WbD0HL!o`>6pMAV zT{*!4((>uKszMe&{YW>3E|0(I(!0lsIlb@ukFH&d#pgiG!w;0a1OgYJ4AI-ds7c;u zePigmrw|sUqoxoVHjn>H@z#92H^+%rGso4Xihx-4w;w!m&DV)vxVI>>IL5z`^$(wN z83hllC>nj|wC^Ox7$K;^U^-9hm!q2q1-_lR*%61*6wiQ&YvaMc3YzP_Z%6S!Ycd4J ztA2j6W?k{~E7(7WC$HIF{~nLbV3VAll-yh(7pMkFQ8t^HP~^*0Eo@W67)LU&PA zUus0AXkXi2Z@>6ywi-+;MEL;p^bYq=`+vY^F$Jlc!{*majsUNY(aEyPd*9>wmf?h6 zQYB0+_@3zdBnlM&jdFJmKV|$zyGahIylPS>TT9{;a!WQ`U$lI@W;>Q&d>ko2r`r*!eB7%V z$DXuR>Bkr6E$h=_I~6u84&837KCnApqOUWEL6mVs z5*{j0pR!1zOl#kuq_Inp=>`~KN`v&fzx4OCvNm~_MqNh6>CqElkS$4~vUJgIJ%)HH&^=1VkwqoBp?O5KXcUy8Z($>4TX z6(5s&NdiZr&pZ1ePpWqYT%5noqNQ(`}&kZ#*k|77-}6^fwlS+Pqepu@G+9 zDSz8VA@8XG^*;|SRAa%sxM}(iJG0MRzt<|6q8UqTm_rZIWu<>j@}Hr8f(RbKAFnrb zbv^FJwX~43u<3CPifg{F=Adocn8tuq$H(jpse#sJzY&}nQ|G!r{iHbA17m0X*_Fmg zk5uNLauj3PUiD~@tW*lte^PqEQz*ll54-2H(X9U)0Vx{BKwLZ6K8|E^4y5o_J1Fzf z{JIounwuo$7Gd`vw?NM8K1IL8>JF?aM!7r!ja3>(owMe_@m24!Lmxch4Ev3Yf_wLI z0{g+$l-Z__@xPy&WNW>*qFa}InUDTJYSrfOf%L02%FE_Fbl;FDP|!z8`n7XRaws4j zoh_pHt0aCZdogmh^Lg0(Lf7F>ibW?MsFJ%5TE-}vMM1)j_odRg5ViEX`rOT_YCSIn zmitEM`VN2o4%Zm56zuv3#M-Bl(2wFrnumlZy0_A1?i+>m-8KEWN+I7fSPEiabF8Yy z#}gLshm_UwJ~nu4@32}Df?0pMr?Tb!>Nq}Rs`8!jHu*d&UMpZpdBF>7@8`dyG>wca zC3RZH?t1Xu(_q{len(d#;kq#9G9Q)s8T+szy@9%Zs?ey&#gam-$Z*#yteDbv4hdv z6R^0J_O~wH8NBx#MF_aUfm#ekKM_z%y5h@hkMowFT>P~_;qry=j4@$ip{1d5MeWnYvp2fn^3 zZ#8NPG^ejiSv)z*axL-1=&^s=0LuppLjuD8CL}19pQqN_Vd@QvQ>^gHgf8_J0c6*q z{XcZVa`t0o7xnRS9wWKElo`~|WE05qY=!-7L-u^%dKbchIGX==Oj(v69ooCy{QMu7 zh|yPxzvT(~WDe@0E1g5KuP%ga9b6(06G%xjzd=`KM#emJ%KC4FdjH@;{8MA_eD?|h zyII#wk&D`P3p?h5^{U1i1#IR#UadzX#Sd-%O-=mQ(jaX)B)9GCsUh$}KeyUR%q#s> zjk6TRDDed#vGP9B6c=;+sOPZE>M44l*Z4PERvVpKG#DllB7Pv)oPV(U52z>?e^_U! zX8Ui%l=r!xt-Fq?!;0H`I{ehR#d@GH>da?n^+28y?6E=wqMFCJf5QLa51#2odl!|Y ze*Hwy@>B474A;#xT>oSh<5KBmI;2Y>0Et<@Qg%x1=U?jn{l$wSvn?o|;^IYZaU}Zs zlsClnU3k{4kMv{mv64aoL*B zDDnIPCDsjdro2G%%?u@~XBe1N9QgV3FE2x?FS=W*Hvm3;*35oF7uB==!X{?6lEUxn zV~Ri;d+_FggI`u*za5_EQ024OteFu3q%R`B5_+~iyK~R&@H!;yJ^KMMFt9iHpE|o% zzO%V?4+JT>&4TX#jt~3)SKc{$V0r`)ct5>8a0GA$cD(ogm!$E_Fx~^*Ep;P}HV$)b zznmxz^Q)A+`=9oom4V$5U7()s&*>kGSXh9;|g5^;_hIEULjMS6_ zyb*^*8<440k0-~`rz6~BQo-1QWUbv@8B)#HJtT`fI2jLh&ye4+E>`;Ht~Ca{sc(&w z1(_=gqd!WMI)-G@@hcfUIK6#?XObs|bKWTlM##1J*^H(nWF*lkkFFN-dPbLhuGjSw z==EgVWRCE&6X|8Z$JGLO%tB)oVn-Z1Jtyp{*M*04B(4N zC{GDsj%JlmfCX(gwqhHpOqYRr-W3U^(Tp6b4Hha)(ME9V`bZ8|(dB+ZD>;^#FNk)| zcl)63;0Bjf)~t*skqWBx1l?+d+x^Yw3{@6t&%f$dtrR+0*1b@7vq>$$;_KT|nySPv zz!lT8H}BxNDovk^X6-uPDeZQiyB)E2Y8$kCa>X|CM#{%5u$TbRqFd@*Gv$ijF1Cx{ z7YX%UgRU=&CW)1{@myA^q@qFR>ahfH?*39Ga6MD_SUjHMZT+F=RytD8`w$^UM#*OL2JpB1%hA4jACaKsirk|u}z%^NI*N~Cf zE0;%4Wk-hlTh7%ihNhHEfm#aCjW|XrV4f!WrYMHU2v)o3R(D$`NYUbX_q#O2j^Djv zRFuqfdLYJwlM~bXxxo*iCIic;kF;Ev^#m<#pew59v|RSFUr%S?HD!68O`2{ddTRB@ z)R^%NpYxZjdWpRCB1(xQ>tZYFrgzaZXioML9M)kaj_Rql0nxW(9t+`~pT`Dj$gnyt zuqJ{ul<9I83IrF5{2G;RG-Rbp`Mh7CZd!4<9Ce%14FK(oWv-GIL_Oov^uwhp_rcWK zO5VF%QaWEq;M$Udw4C>Yb+q-en5w-wNkT5A^?HxLzCy}{XXbN>jKT{i5X(X&YM!*RW%3kM%vsWe%u5;&J+q>FyYiS9QPVl;ST^lE zvXVgUMV1EAQ02trOd@EoVX@Sd;~LT`z_!MhAAk#&Gb4t>m6;(;(jEn@=-7Gc0?&t+ zhlGejGfX(OJ$f{L*XbNGiZo4+Hs;YjowvfhP+b{#su&C;zNbgGH8zVu6fAnZIc%Gn zq(etouOP5%A0RrnJp9ojUe`YXC7+=B6A{%3%}*pw)CeGo6(5akts`+LvQVmIH=|-K zw-`Po1gvPz{I0h(T9^JDXtlNMu&4xe9@TV)aj4{8hoLLScg)bmR;*E)&JZ#=z!rA% zGBkWyrm^HBZj?Yb;<8*1C6Y8E@-|)CWaXIHq&cI+7}W4o%o|CI>*? zl_c&EtBcBB{EV(x^7qlk?OJV;oZq5V)wO&@;8Hd&HK>m*?Ez2&)I%NTDsM!I2~X{} zjr!^_wh`Mwy=-1oeA>(KWJ&suMe|!@O^o0`MywnktQP4GG|Wsx*YPZ1I>-;$Kyg#L z`$)P}yomzz^9d%S=tuH%;S$(XRl35h7*i{}=~CPrZ*L3zO(0nyCpm&(&K=G)S& z!x?nS5w-pJhb?Tbypt5s1jnOdtt~?%idoTtR#c`&Gibk7Xd^$)%HAf$RZ6aDMr2-1k`^K!ao!S70%ZrLWtGG%^HMQ%+XZU@sc0=+p#dc;JK z1J~~CnPg=6XNi^ZX4kv+pOgeDJ>k}u_vqo@CAznJ*PLL~e3WoS7PT8e=wKGs(; z;!R_%o}8Bxn;V(R(QPkA7({3c?}S<BF#VEghK z+REfplzcb{G9KPoJ<`38jpK!WVZgJ=r^%kjF@S1e{%WwXgKcF`H z>EnME>3F(-_2kf82!PL|l{&9o#!j8mk*pdcZ_$AyMMzg&=89!0@Xz+R9W$4ME5SFQ;43E#9C$)DR)y^DCOKW3Gi)iMiM+FdDQ5&QGc3h#VvA3@llru+Fj!-_X+(DH4`6TZLZVCRNchE`qo=JpbO!=s}_ir z>p^+-TltyHW4I@3quzzvMh(dY*m-RzEP_)49N+F!SIR3tdaGBYilbt{EjjjvaHv{6xxF4wob!jS3 zzFJ|5xO1-STRB|?QNqjIKbET8wU}9QpLCIZ5Bt^>h5MK%0A%6TKYQEyfMq!Pf#ZsGwxKi3{ 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�x?+=PnasoLEJyRv+SUmq^3@r?EA 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

      E|YI&ROki4cO^gQM>Y?!w-#S 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�+4&90GpSv6l1{B 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�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 z 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 B`dR=0 diff --git a/tests/assets/hlabel_classification/images/test/8.jpg b/tests/assets/hlabel_classification/images/test/8.jpg deleted file mode 100644 index ea340fb4cfffaba602f7a48895058b9102b5a083..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178732 zcmeFZ2UJtvwl5rdQAA153wJTRrF8?|Wn1@$R|r-Z9>H?mw_eviD-`HRt-x-<)f%wb#+RqYt2y zx|%wgASx;lhzj@t9ZiDNL5%eD4D@u23=9lROpMIOIa!V$J9eC#or9H=4bCmsjkfxWol%IXO82QAJe+85Jp6IhkLTP%$wv9Y1!Qi-m#C!}W&~Oq&O+`aP zO-n;ZM@tKoz6o3h(X!F8pOwBs&tc-oAn3;_b0?*cQRr&zOR(wChOn%Y|6L~LQ>VGO zA?HLy#l)dgMj@i3tb{3ce8%8Wwf$esoN1 z95yvAJtOmBRyGb_R9sS8R$ftA_oTj|v8lPGwWISDsjIuEw{Li4lrr}A-T1`E*}3_J z#iiwyPn%oYJG*;dzwLkjB^MQl=8ssw-#>!=FLJQ~a#7RL($F&el8cJ^25``@(bAoj zrf0um!r>Yh+iKo2^>BTf}F~1LY9xmhqO4Zu4z6kAy$2scbl;@3k$a>+_m`Z>;L0g z#7B<`Uk5MG3Hc?y6G~X@v%RX%uZ!0?O%P|8#qhWPI{wnZUl{oB!vH6>Hi+HmGOT#G z{wuYDkPZ3l(!1|B>D*S#bCM@`sTr6VzpUEbFIZ`Fi&~SZ`QjoVrq;m-0d5bX<21Sa*YTGQ{=&e269$Ytz273~Kx@3UJ77k-Sb!Wr%qu&O;PF~M zT=W)dvQ&p5;08#~br zMOSx*;eNVAm`2L%!;>DA79KjY+p5|>^TXW-z2TV2UZ+70- zPKFMj|5D6*1gcYoiFST>$a7S#J{!NDaNXz|^D1oWEJI6{MTWi zvSb>iX_W-3CTNd9G?T{8Kbv|Q%wPIM?x|{l%|(e$=H`_zb}>bs?;RVa=DrU5&&*Jk zKWrSl{yhH*RabX@@+DtA({XjiDC%N7b}zNI9T?wB|09Oe@}s?RK}9B^C;^g+HL!8d z!+dwv97gwleVFds{pKyS&t%-vG5rH}m*{TzBdB^_;6-xA*S#7Qio{o%>(68oWaes~ zhcpkU!CI9!c73Y`!o={;=BoZqGJn1QA=|(7@RuI`PcQ(b+^?B%-;Y^;o4K?Fwq+~1 z9$S*NL)}Ui(392Ss|syjc+d9~H*;UqR5M7lFEN>QutV@DQZVjJ zoKu1$rvvlNn=DEgw%Uc)Hm?DfZGP_`RPguVz5n5vk%;VDZ`lvm246F{?H+iQm+G+_ zaUjn0U3dw8q>J&Zz5ejmNMWzdEq_7asjo%;01Ca@kSNh9oLT42kRB2I*()v3Be$r+ zobO6aUX9L3$|q#L3=eQO`a6Hci2ubg;$LI0|CwV?aDZX`4~@xp0+Na<*NsXPNWJ4s zPhoY38Ez)DB|52woHThXVf<-j@y9N6u`2Ed{DhtFobX-z;Z10ty%3&{UV+VY7#k-J z%0D-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%0D(J^xql3SgC~D;vTaj&OaGjmJ95H?*ZL@%e7v2 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

      c-;plccKy_3{(kCzd5_3%exIv~ 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

      V}i| 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%0D_lxzl}3RU!Mh!- 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
      lI*eNKkifywmB&63&XcnnDaj) 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 diff --git a/tests/assets/hlabel_classification/images/train/1.jpg b/tests/assets/hlabel_classification/images/train/1.jpg deleted file mode 100644 index 515127e50bcd338a7884269f53decc466fee0dc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29933 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 diff --git a/tests/assets/hlabel_classification/images/train/10.jpg b/tests/assets/hlabel_classification/images/train/10.jpg deleted file mode 100644 index d0118ec6c79604fc8f7adbec296b12aa61a0fdf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222211 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
      XnU949xtK>(Ji$SG3$>sI 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

      a7u`JHpzKT=ty58E0;LKoqFGdI&y?KM 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žK&w8_KxM_}nvV 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�oD@2i_O4OuEJ6~kENa}*C@}oSJE=PODoJDmk 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 Tcse})hmijNAE8bCZ~p%Q4WTl+ diff --git a/tests/assets/hlabel_classification/images/train/12.jpg b/tests/assets/hlabel_classification/images/train/12.jpg deleted file mode 100644 index abd463df610ba518c6b1a51e35701bd454ea693e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57832 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 diff --git a/tests/assets/hlabel_classification/images/train/14.jpg b/tests/assets/hlabel_classification/images/train/14.jpg deleted file mode 100644 index d7f2d51fdfa42c69b748a2c41e390b419c03ce77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66226 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

      E-U@*NBLo7_*tW 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 Yl*x38yn1{n(y%6GDnPqf_sgsQ0XTo}k^lez diff --git a/tests/assets/hlabel_classification/images/train/15.jpg b/tests/assets/hlabel_classification/images/train/15.jpg deleted file mode 100644 index 957f8cfdccc1e1b013c6236159bd2a2ace2ca24f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 370121 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 Sa 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

      6LC<2+e@7x}I$j#A*xslxaO(-aznf1f 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 z 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

      ?H=avI|_gVOBoi<;};#9?R$qRjuheWb&^y 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 J27h^f@9*~L=5GK1 diff --git a/tests/assets/hlabel_classification/images/train/16.jpg b/tests/assets/hlabel_classification/images/train/16.jpg deleted file mode 100644 index 9129e9739627f8976bf9a107425098b54d9fef36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188698 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�Y&nW?J^ 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 z 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 Ln@rfMKcD^|N&kwJ diff --git a/tests/assets/hlabel_classification/images/train/17.jpg b/tests/assets/hlabel_classification/images/train/17.jpg deleted file mode 100644 index 4370fdc9b25e130c995e1b97e3146fd974ff65e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 175283 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 Uf26V+cS_)KzyCQ5V28v114qB0R{#J2 diff --git a/tests/assets/hlabel_classification/images/train/19.jpg b/tests/assets/hlabel_classification/images/train/19.jpg deleted file mode 100644 index 26f10d093ae571c5d859382d928fd9d475006989..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44669 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 diff --git a/tests/assets/hlabel_classification/images/train/2.jpg b/tests/assets/hlabel_classification/images/train/2.jpg deleted file mode 100644 index 0dc56601729fa8908564c802c204e714593c92f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 278748 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 ad~0jxLRHFsxf0z5K$QMZuNU|F+y4R8J4a~% diff --git a/tests/assets/hlabel_classification/images/train/3.jpg b/tests/assets/hlabel_classification/images/train/3.jpg deleted file mode 100644 index 86483d70c5cd7e5bde85f68a6def8daf7ab53b48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25473 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

        GgvU0OE=5&!TSEJKni#* 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 H_4@w-$d@%u diff --git a/tests/assets/hlabel_classification/images/train/6.jpg b/tests/assets/hlabel_classification/images/train/6.jpg deleted file mode 100644 index 3a179941df5ad9c0308784e04ff0ecd6fcfdde67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143396 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

        A6MerU%+8s}WN3pc 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�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%0A%6TKYQEyfMq!Pf#ZsGwxKi3{ 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�x?+=PnasoLEJyRv+SUmq^3@r?EA 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

        E|YI&ROki4cO^gQM>Y?!w-#S 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�+4&90GpSv6l1{B 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�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 z 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 B`dR=0 diff --git a/tests/assets/hlabel_classification/images/train/8.jpg b/tests/assets/hlabel_classification/images/train/8.jpg deleted file mode 100644 index ea340fb4cfffaba602f7a48895058b9102b5a083..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178732 zcmeFZ2UJtvwl5rdQAA153wJTRrF8?|Wn1@$R|r-Z9>H?mw_eviD-`HRt-x-<)f%wb#+RqYt2y zx|%wgASx;lhzj@t9ZiDNL5%eD4D@u23=9lROpMIOIa!V$J9eC#or9H=4bCmsjkfxWol%IXO82QAJe+85Jp6IhkLTP%$wv9Y1!Qi-m#C!}W&~Oq&O+`aP zO-n;ZM@tKoz6o3h(X!F8pOwBs&tc-oAn3;_b0?*cQRr&zOR(wChOn%Y|6L~LQ>VGO zA?HLy#l)dgMj@i3tb{3ce8%8Wwf$esoN1 z95yvAJtOmBRyGb_R9sS8R$ftA_oTj|v8lPGwWISDsjIuEw{Li4lrr}A-T1`E*}3_J z#iiwyPn%oYJG*;dzwLkjB^MQl=8ssw-#>!=FLJQ~a#7RL($F&el8cJ^25``@(bAoj zrf0um!r>Yh+iKo2^>BTf}F~1LY9xmhqO4Zu4z6kAy$2scbl;@3k$a>+_m`Z>;L0g z#7B<`Uk5MG3Hc?y6G~X@v%RX%uZ!0?O%P|8#qhWPI{wnZUl{oB!vH6>Hi+HmGOT#G z{wuYDkPZ3l(!1|B>D*S#bCM@`sTr6VzpUEbFIZ`Fi&~SZ`QjoVrq;m-0d5bX<21Sa*YTGQ{=&e269$Ytz273~Kx@3UJ77k-Sb!Wr%qu&O;PF~M zT=W)dvQ&p5;08#~br zMOSx*;eNVAm`2L%!;>DA79KjY+p5|>^TXW-z2TV2UZ+70- zPKFMj|5D6*1gcYoiFST>$a7S#J{!NDaNXz|^D1oWEJI6{MTWi zvSb>iX_W-3CTNd9G?T{8Kbv|Q%wPIM?x|{l%|(e$=H`_zb}>bs?;RVa=DrU5&&*Jk zKWrSl{yhH*RabX@@+DtA({XjiDC%N7b}zNI9T?wB|09Oe@}s?RK}9B^C;^g+HL!8d z!+dwv97gwleVFds{pKyS&t%-vG5rH}m*{TzBdB^_;6-xA*S#7Qio{o%>(68oWaes~ zhcpkU!CI9!c73Y`!o={;=BoZqGJn1QA=|(7@RuI`PcQ(b+^?B%-;Y^;o4K?Fwq+~1 z9$S*NL)}Ui(392Ss|syjc+d9~H*;UqR5M7lFEN>QutV@DQZVjJ zoKu1$rvvlNn=DEgw%Uc)Hm?DfZGP_`RPguVz5n5vk%;VDZ`lvm246F{?H+iQm+G+_ zaUjn0U3dw8q>J&Zz5ejmNMWzdEq_7asjo%;01Ca@kSNh9oLT42kRB2I*()v3Be$r+ zobO6aUX9L3$|q#L3=eQO`a6Hci2ubg;$LI0|CwV?aDZX`4~@xp0+Na<*NsXPNWJ4s zPhoY38Ez)DB|52woHThXVf<-j@y9N6u`2Ed{DhtFobX-z;Z10ty%3&{UV+VY7#k-J z%0D-Bcfey|^-KR=vR8K3N5y{PWIT_vOewU&DmKHC~irMPRbQ|03715wTAOtxJ4BU4e$lmSo8TS;jw>+DZ&NQaRwo|a{d6k&5^a2+fI2`J z@19gBGvXxfHqU>@Zf-V*D;Kq~ntzw2$BNZl`26p&&i{z>;ysB%_f0ZYEA4A-1oqv( zV`(R>ADEaU?)M&nG9$ljlt6;Uflv0|HjY3L>SY4L24edWIHC6Yg#G87kt2|V+U72B z)k3{R7yJBY@-4N6n8QnLbC-wDCu{$?WHYo5!zY4E^Y|&kKN?$(K#lQ-aK{LtYPIRw z!JiFAhtPbPb))*^0Cw!m5$K+*{Shdi^&nB;2n5!@#^_ak$%{8~(&Y#=TXDC7fx?w; zo|`8?5&W^&{HG?!a%&Xtv0IVv?Kg|(Ge1Xct{dYUMmoL^M2WlS|G?zW8D3*v)f?*< z*rTX*l})I(o(TCWT~6edQ`J1*=9Bylas*1e8aUaodiqdBtqYfKH=sB-Re`XA+#FJ1 zi=68`zyuD#xi77~i2UGn2v={FT_(D2ynZ|_DSHI+;W+TL?)cuf`=lEXWV0mjv?c;f zI07-w)Et2>@ z`)}WYhDe2cJ>l_Hwwy>)_(_P^_DhpGKjX_I2Y-LmQVCDaz9D8}k(7Mvxx*2t`|SZn zw#@dtW!vV*L%9I0W?QfaL9kI1@%On|b$8_l6({=W2Spa-=Y z$`1O!uzImBYRMu3-*41BGmcyYE~K$VZkn}i)I=bUK$qJNvf__G*rJJ%L)Wh{4Kdq7 zS@_n-Zx(sb|8LD-NcR6rE5a>rXh|aiyFY%#o~{OF5r0=3+0nMC^^-=?+-SG?u9c{mBD!LYPC}#A$)cJ7cPucokW#{%DtS4cx@A5k)$W7l@RonI`tXpvX zl^fXWjzAB^d>dIx_S^1mF~l~?^>+gH68WpI+Qu!~w!48|5|~2`JWbB#TqcMbwKCA3 zKM09T6d3<%zuo`)BH~ToD5|z}I)C5w2$amaSpq?={KEjHZkHkKAc#%igurj~oZpi# z1fX;T`u*+nWn+B3+T&e;{ja~F;&J$DG;-OGes^!trO%n@iC5IzO4Ikk^Gl(r>+eFR0nQN&4~`d((ipstry$oFT@ocsF+qoMDg6O4#Z-2hcRwOM0!tnfN zf+J8*#UY#{B;3rd@ef7G+1!v7BMN+$)H}TSM`hPH_R9aEGK;INz-5ktPwK>`-_-f0 zL1=jLcQ+MfOBJ;|LSSm5cin$90t#x3JA|LvC~XY0iu~QQ7<1eYy)WRQM1;2h^Qt=W z;UU6kvpm1^d$Bu!wz;bWgSB&C>fr%qSRp^dCQsl<+m=e?0ZQ%gAtv`Sc;TBl}|xH31I80*oo35}qYzKJrb^Ci#n% z^Wj9jCoWKDc`X`fcDqWp-ov+vlGxdQ|5eg1=xRmrwtVQa*01 zlC(-Q{`Rfq;q|@W#=mqP5u~DG1Z^D%c(1qL&1MFfS0&q1h*WP$3Ie;2F5y`ab_j6Sua`G2~tH0Bi=7&uR z>Pp~FZbQGE$lsmy(3io#Icvp-e$$*DgeA>U2J6UQI7$1#e*`)V`2`fY0I84u ziQov2w@W3z{gII^1JXAWFcZL|%h@u>0JWm~KR$B&Z6zAOeM14L(i{8+C4SjvbKTj* znxYI|&^6{c)fSahK00&9QSHvnMM#pLU?aR^`Fe|P}W8vHwA4^K`<9!98b z$pM(>?QiBXGvC;-k7YdosQ>plg%F`vT8DsZ_}yPXXTXZ`@lAsOVwL{Ui4lA?CmIEQ z7kDTa9*WcwuIZ@ywKqb!C%2u1!)(Ej1;l3N;Q@Pw9<;& z9|vxrcH)_t(u)0`Z)N~={C6z052-h0jyLAqIRe?v0sl-;n(O>tQX%MN3ou?OHZr9o zkauX^&BVfK_xOpp?hg z3+2R^jm9V3o#S)ASYm`Bdefi9cO$uW5Jh9x3 zZ#V*dut>miR6Es^|m#h zjd>9-oNqg;zey@3Shv6Bkif928WrqTcO!;LqrHr@SGec~ff4m`=TMNat9eBE+l{@P zJA306X|I3*fYP!j_-Z){1iAoP)3aAMWM->VY0=A`0uPEAgGM?VZnB&`12!qJ52=wMbV1yfE#-o_;_9-D0rKwT*GDkS z0u`K-ja9ZJz}&Ubx~{3iEbZkMuFdo0_;BW|TKZeQ-P6+Dn3KG?juwN4al0!I3}FvP zohEUgN(Au(^$~Ed@!sKDl;QWWdL$`GoR2M~iSrwErdMuKgGMTzpeMdt;k(c~4WXV7 z8;1AnRuxI}t-SEtE%)e59}3+z&c>Acd-{9HV{09H^TcT2)Jt$jam(Up?QF27M<~%J z8_e@EkD^`gT%`eNIQbH+tR6L-(*!nh2#3f&aR$G6Hw|Kd_jJuehVFhYRjd|AjxMCb zm9x&%Gr)a(?|iYbK_{tUt2-;)1>oo1+&%<3T@N362I<6p)0djkG1F-4oS<{F)p<#1Vt0VNdX^8`Mt9Mt#PzSHy5d^6jb3{dQ5KJr$pP2; zaVz%>V_$iM`P~vP^wj0G(02;dJm(hiTpKFOy$yAMe)i6Yk<@Qs$7Xn1GG&(VVZd@5 zp6|>hqDXQ$2Ve1HC`)rroMQpbQ;y`wa!Hn~g+r;qHF{Fz&TvcTk;5OK%r`!L;NF}x z173(pPiZYloKVo>&mD|w<88;jWdA^U8rmy_`}jz?jt73YYtnK%a|M0s{RObZV`?Q7 zvUuH9h)HrQ44L;n!94eRVmB6YcG2l+p^>^sLKacCNq4-l;T#nScKI}~Ii;B9^FE3F zjwIW-wSaE3Pf{3)c5To6FUxOSg__H~mi*{F;r=q{Pg6+V(vtdzNWMHJlJ z!-8tL=e?pOUYA0)EmW+EiTzTYI?KC+9h856QCH%GhP(-H60EQ>c>%0B_MX7?iMWRqw-T zi~B&kYI*Ukx+qRBXM>8nN{@i=CaT9(s?Vo|n?0_9=ksR!dR;xmRWUCKG46egTxgyn zn#g=Wg}PsAxSn`BXtX_w>}2yohY-+b;I5bI<(~@6Gth&)NlC>_UH@T*D@A3TP~c4I zAWt-Wtlw5f71uI{8Dke^KGsQ2)SrD!veia~n|(4fkBwI6D*(&3%R3bx7w_MO;Mqrs zd~I;p%PmR+v$$~3F&+GbIO&8icyy%`gI#hCl3`x{tUUpfcN_Ab)j#L2H0g`9RUKD0 z@W*AHPciP*67XY_85vf^*Fz>HbCE9bn4tiVx`I{H^76P&v%V>0OKXZ;fR6&<&6K&c z^NZ@sT`3WZeS;)rawTI|nLf@VbKO=48kB}5K++7I%**?h<%Ofhq9>f9kV?=@KDNw+@nGO;m`zA*dLR)P9%%Q5)GY%5twUyS&H zzI(hAt658Hw$XC|+LPi{qV0aM+=@1lF!1ck3BJ|0L0U*2r)T}PQ4xuEkn*OAH z=4{Q>Bm`?ECHvHLCtklJVDXYCrwr~3))q(f;u=EF_EvR8;xZJXw2&n_KXT2nTLuoE zX&*u4Ym*V925insTQZgu;ba!;hZo!%OfIZpx;i{=Rw-wh4+LkMpsJjC{j8}n*y}Z9 z9yBphg!F~@FwZHU&Z>+Vbm322_pGxDzNzKKkExk__Ifv2f-J-f!SoE}8JwJogAD7X zn9WDvJknq$vJn)dQyhkK#>;f(%_a#oQZ&=i8t&!z*iV-gi5Ky*TDNf`=(smFEuazt ztVUXMXB)#IQ)G<^N+Ip!<;v#X*++Nj0#tjwpPrkcbDObsWs$MK6e+qP(cR9<@jgHH zVa1O}zA=CYJ+(D*-(GX-5dx<K z6mJSkl4+@`>(U~-y^DYEC5JMI##+$4D#v5La}53Kq{xYUGJ^w=mo8te*uM#c;OI(e z|KO`>buE#)k6jrHFB*w+am|NY;63+r96cUa9}5cJg=c?UyV#e!mRj?A?yK{gutWG$ zyTeo1d%mF=l>y4YJcg~?e{^1$&h~2W`8CL?cHi=u=x1M0kKIm`$Pcn9a?Ss+C^J7D z8~9#ctx=;*iFJn6Wc8bc2o#&^drh+pR=Y52o>b<((3a12&hlUeNUGFWQ9c+-Ne%4G zu2%zdoZ21#y9n@K@cOYfEv*{G3t7tS%NJGbn)r?)A za?krsLh)y779%A2a)wM2M%ZQ13e{6F1Qq)=!cLqZX)LC``?7-GYdkn1-tV&=NZ8cmH28j({Udi4~_8QQHUD9`0-5(2!e)P18mX__j`>ni+pYXx+AS^L)5*sW|5kI zW{U2I4g=Bh?&tXctHj*VH#8|a?R_RnVp4;=V^tPf{i`;+t$8rh3mQQuMA+keY8bzm z$QNjZm5IZ3HN8jW+=i;*GRh-em0Dp-TQl_^n@8Vi>76RJ+$F-o59V-lMZxN0^~5EW z0A!!KyFGX@oKoLH8&r*krAm|Su&YV+Vu`Ybi;-bVaBCvg5cBz~N>FuxMS2}CWJt&| zOX>dNqRN<)UQ2oFahp+T&2i$Kb15AK^WY@Hf*GO04hwn2#&&yJmrhnOEQc5ADh(>b z`igfvs8)dld)^+(6QlaEJZ-$bm^j*=6C{FB9sT^~)T#SwaBg5A57v)MmGhaXk%$6k zaLkr%Zzzq)#Kp&j^e64SS9&tA|3&xQ*Q#^+UB+_v4tElz5GL#{l-QS;!PG7(Vbghv z={?+thd)$66j%J&;PaK^+-nxjjqSWEDoSbLAg?7f-d_Wq(OV*>ZZZE+x@Ax`! zwZ3qFMn5d4)M%6-a)YGuOk%idoQ-4e2#-8HK|!3Cv6sSl@A#&Wcos?{~0_2+pNSbgPhV5a9t zD(M)V-sSSfddlVM=iPhx#30KR3de*hf^&rOx*}5CRZz~?26ZXoxY%y1M!3u~X)5%E z+tR4plSWq3#`C75&r`7Xi>dLl5e;aoTAWQsszm*V8|8V+&O|qZprl=0z4u`^cuk4| zf?h7xu1htmjLO>110KqAi&c&XadOIz<1BaOqCr=|Ly@bVs9Hp#sifvRxWA&8Di0iU z!2ovr8L_n>C(5*&q;*jQcNv=`Yn-)(>1TFRtd!6$T&zJblCE%redVc(^PEPR*-J%L zD`~w)KOq=xIvl>6B$!R6GvD>3AkcE^O;n@9`+bjA1!oV=hYhA7zY-ATk9giw%~X)f z{l;3pj>^sydf}&6>aDd5yh2;Akb}`KIVoP`ov$K@#iu{dZ9>LOrBmVSwF|g~FCZze zf~UERe)3;BRd6JOLY1b>7N5n49lu=(Us|rulbvR?*t`n?Wv>BBT=oyWk%WJswbMc9TxifliY<^B|_^b@6O5^cts~%$# z1Nf2qwFEy~aJqw2jZ=!7+*wasEVhyQe&v&tD5Ko^g#Jb;#%eF)$J z_aJ%U=_%d4Zi1>%Zu?`{u~FMA3RT?ig-2EOWY;H!$3oTMHZ3X?P2RBwu|r{9KGpPI zwIeTF!R?d1jSE`OZ+zF1ChEVjY?Dy5nSCOf>{)%UJ8KoP8=PjdC+i_;?E#jg^qN&l z>tKDRvUTfY-J}Y`CI{i^Z@tN5Rdx%x6jq7FG+DR%^>?ydMz=qtV95y?aDVyfu|kiY zu*^9Dv4TgXLla*oRkX3qMfl9^KQ37EMCzf~3gyiV>~hXcige~;v8)13+DJhe2y z0Im;~ZC(w(CUKX}ds5MuP9r6OIp^$Ycp72~=k+LltoHo*8!j}6pNle7RrYed=h3|d z!hxPSa5paPHS^Do$Dd(aCEHbDBGJZM$tB>z1uWE?;dQ+U;^x`qe~raTrnjx`mJ6rd zW(WhNPSCeJa<-Elohzt*t3EvTDaba*XLjNFPlAxnNUryN3)#LpLX085#n@~ z1ZqZwGke7~Vr&0Y3;so(YZrae+o~Ua1cHtkQ*uhT%MtmJ^Inl2J5^;Z?7@Qil;+Aa zFDP;t&*A_>M00yZ21YEMl8)vzw9<}#`Yy`&DIGiZ znJSYQ^qlV2uK&rR3SQjM{d_(z?#tvNzfuccT#d+BW%%S+y5ep{16DgrFiLj-RDQgH zqu)Hy>0X{Len1B<+lY})^0Le!aX0B}dMOVmSN`0bA9MQoX?Ve=jWz~b?>bwdHC#-l z@>f4A@}NO3Njyuj;&HWMLV!i#E$BUG+|$##nXpXm=F0WZ3M1V+t;3hI)~3u1uaqhA znw>NBa@mo4ao4+dmN?ll&TD!(cN;yn+zdnt&hk3fPJzhw%+zMOAkJ#Ti9Y?s^w@=C zD1M1N@z;jy4H7nEQkp>ILA6tn`hJ3Vk`6it=Y6%!>2@!g3oM~~85~&PI~wXd9G|Ut zTvL#!B?3K@{-HFzE2jCO>L=jI$%yxmS;$@)f!LV z1@lavcQmi$J6Rq;^wq+|;Yru@eAdj08~2DA>Ug+*VpiBg!73f!%depo>bM4({dG00 z4TnBTksw{uaLHlu?1YCRHSr{9#aPusS{LCITEe-OE%#J>s=h1Ah}O)z^nnYqdELn1 zi-CDTgNQf>%qcqT9wdajqBe)akF0y|4Z+lzx}->6@1!txsL93|Za95=)jZQLm$^^7 z3o#`H6}gfpjwMwFCg7G*2G4FtdT8C1M$IPkqEgsQaIx!dO~OLQNN&@OvS{TO=)N{@ z@#^$I1b0ABvm!i0*=5tzR{8W9$o-Q`I0_HqM1#{g?&?g+9qN_^gEBb5S!xc8e~p`d z=t0*1Xz6FQaku{^K|1a9OjU8OcUh3{k}z{mwtQM^Yn+sj^c?l$8vJ%X%u3#E{6XKV z&8sAlF!S6~%|e1zDjr28W-d(WXAQFnJ(Z{7wh!~fmtaibaU;{Avl%)ac^D19Bm);~ zWyzNkA%vPtKCiEMt?r~`xN&12} zB{nyuuFl*1V!fi_Hg4s^qhro0Z~0u$k4GEWHoTDZSDZ~r#|QTc-svsOW>vSM_iuNg z#KxagEKliIczB1q!XgD#WJ+<8#xCV3y2LmjdCSYRLn{4icdJ8tOu*AxVX-}`n5U^m zXb*FERQ3@2Tb%S5LH|{fxcMovq2L@Y&%ZbUmO7_m?wxwcAn@UZ#4;=?Ps#|4aYbw}f~&NQQ$lzyty(!8#?ri4IQc%RJH%^PRdx7{(fb_I;K8Q*uwllUNljI z=eZ>o-mUw9v@GU8W|6)XRK05CP|I3!eIl}_6F&Mf_>?r+RfrpM`Ynit9B|_zsP-k{ zVpC4~Z7;fhZYHwRfQzMPlIQ(+v&#^iG}&!n9o=-<4_7~Q+z<6}wz{Ba@j-4ZtzfZb zW*!o!&t{e#BM*4%Ooc8C9Iga6bw??lK6@GJW|9Q0y9jm@N+Byh@=RYB66S|NLncMB zD|}b@p!Z3QpFfy2>W>d*?N^x6JGK*Mdw?WqW3W&HHu|+rcI(ZYhx?hWJmE6ZB%7uu zHpeABwLpj!G-6p-toW84bobdyKeBnNLlL$z_0hDau4Wt`RV!5(}lydTI@8cT8Q5-y%vGee6Y(c(;X z+;3`U2{I`YHS3@N)VyF*6jY)J(ljwTm+%(vQT4*~_%`^`GZ!($CWY)g8ZA9L zMUm-<=MNu=r8V_^$2a!8!Sp= zB5pt&?8BqKU!B(nij{$hUk(;&a!X*Y`$}`o4#Q-5f(|kNG{Ca?e%y;;kMdiuN6og9 zy6*xHqkrN1_^&YWg$}bVo!g56-RZci{GFn#7Z9M-r4;0xclW6egWhQ*J+Nd$D~5)@ z>~VD!{Inw1SUEoNCXK-l6*WvoWcZo=D~I4C(CQe4)1r-0OtWYEoTz9pkdLxD`_Jme zvh^@$FZLFN{culuo9X~qJ2OVCgo@I^-OOBO*OP@QlJfcWXF&X1RPV>0z@EParYEIc z?RP@!q?g07v%YKO`ZewqieVUbLt4&8I4lq&E(nBcEe8KmQ^@~;B;K)B-SyFr{7>6< zIBXS~E@cdU3sNBmkE`B@To!pssaq;>G&fJ_w+qflaBaMl;iimSv1WVs16BU{W@VvJ zDzYUFT(Lu-9$j0p7I#sF+Sjo>hfYd+FXpkc>Q*l!0!_!m%_H`j2f+#1CNfvIkzKb( za1xLRXHJo8+#JF}!=*nIgi7E%U>`1@GI7W@r%OFUgN2v$3i=!UgDzH#4&GY&keoES zn+(PeF~^d0&1L0mQgck;y8$T?B7aUB*o zuD!$%jyuNV3BBNu;s+7GDvEZz?IjY>GkB{(0>$~ra}(E_Rp;-4Bugir@x?HDH0NT| zs0l)|#q7qe!iJy@&IY@Zi0jTFq@&h3ZAkE!fbq9nW^F$$8gs!-JJ zBuc$^%ZjeVa{|J+!MHOdNeShiu-M8i@omMdw7y*w@Y)sG%9l%Xk&ASe*s3p4=n9B< zvO@VgQwFj!(IXq5!lqEnAd*3XN_i&MS-yp16zY_a4oUTe@mPawMX-j*gDx-qE8V)D zO@o-%rqbzs*ULeRu|2X*#o5P$?C359dx;8(!g5YlZlW(}me!d)KzO~YaDvHK8&vC> z!R}ZvSHzmhUcNzt)GXt|OOImc#IKiuGb&CB5v?0K&815EoLbp0nq0_XIcMm^jK^E{ zrV{%FdZ5E{+Kz~=ip)B$nY<_=9|OK@QMm~oO=8B71Qon$UJ2XPR4+#3R{vooVa!za z!D0fqRG2x8^57ybG9lcH-ld17ndS@%mO=7-$VD=4;U?P%l@DFHwTnhRZkmyFD&WD@H0znJUdRi(Eq`ckHM?y_B-tz( z9;0&q3l)FE3H91i!Q(BJFN1BuQLlVb5-6To{vEi{XaoNvkl3U_LrTiaa>uj*1-Nv_ z4q*XYea(7ii5p%4em9|5ttqap>ZPe1RUcs0C&F=Oa%KITJ_UclOWem+*c$t+OJVxY|=JF8c?Ra$jkiVMMW);BjC*K?Pq=#j8&6 zDe(ukXI@rWy>S^My();Z;!qGCOVNCQFi&u*G~lxr6(sb*T#~YiWL!Ha7cODmm_!6_^5_EEjy6l6MhwsVL?iF69%)SZm@%VRf>l zI?5Q8^$F1r$m^YqmAzx!H!3*F=$cMZ6c#B-Eqtd#(8S8aUZ7LCdT(U}Np71vHgvj! zHSfXrp{@y#OpQr>Bw5|1Q8)jb?ls*68q{=i?Mg%L{gddr{k8I4W8n$SrwN_r(|8SxXSZ6OEyWGWZZFLdG=ySQdYSR;rdkr z<(!xAQsKg=cgC}^K3mXPL_Jc8v*%uD*)fOdN{8kOvpc5m6m9NESSXULljwBSWy&ju zXYPNoYINC!I|bV9?E9LXM=vE64x;0}#C_3Hjp4L!ZJ7#@>tS*ON$ff#|P|_o9*rA=2NYEkk$7A_v;e!BPj}ifAc{#5yJO_(DnD z&4~2<0au~y=7fq$UlyMkO&Kr6!d~LafRIeF!QNOV zo)D{Ou8OEZa9T751Cwn3|9*N)>~EVa=~9M?kqZmmbphWvEEpA^a)|pAaA@c{&4c(VPC#tOB)9TopXRiQuI z23VDL_kU98#ZtqR@C`R0uy+L7p|3GA^l!}{Y0^qt0{40t51R6p+X#Gy)6mUVLr_Wm1je7WMAa;h(rM~sDxTxLSN!8*gGcPo3VjE2zwBZ+_gLwP zqP+M1J4E&Z6WtbviN#cCg=O{+DpsNvT5ECwJ!@EFJvV6O}~K2umT&{(V)cob6k zsqLcfghYv;M)E>wE2_jk75~7zytvObw8FWx3GHu;cA2iIk`bRPVV3z|DlSudFU?eo zX;62>+QAqSW_{ZOKHTZ!sA68v_QA|pJ&6)}nz|&r7i0$|wy#>SQdbNnK%x_b6MB_K zQd8~fD~yM7Vi)D!gch#c-ZmamDW0q~v`{S9DXYeKyNYFkD+8TTsa0pn-F$^VVKltW zFyh}xZoRci&-F_y&l04!b>}puJ()++k$WQuJR83O?6NA<)y{L?thcV(M1D?Hykp9_ z#Hw3v*G?=UTURqMvy%^+zE|Wp+azqKgwo+qacWFR6cUPm_T+3Y$$6^F?Cp(EH+c^1 zLJ*gHZF_ZW&$u?p^a|lvPr2@cX{gzXH3=t9!<>3SoS2M8DG*vBNXyq@n(P-~f)`14 z(YmkoyZqs4^u>%UX3_eTcdxyeN&&UNfhFFbW($P#-X^Ad3w11bap6xWB0vyDL-p5K4#HJ z(^_X~$5@87=aanN&2M;mFm+=(?dhJp)|}bZ;lzigo6B(ILLQHq*3HpB$FNc@$YL8< zZ%4Uf>*KXIzC6)Qo1?Ec7e&!O7ujyVFBO8;+|c7(QNKL2q~_tcwODr7+^%6bpI&Eg zx>Agd@9dNk4IUgef@y# zdxqq_XJMd_En^*gW)oR;?*)sjK{A;Eem$F;qa1*6NxOS_dV>{)m=*G8*Wb|ULzAKHtS3*)lj!JgnLJzRm>5I*iJ87}g zOK>?~4CvWtPs5n~;e3(2&bexM~Rj!6L&O3_i{&aq{?h*o}NVakEqIU3y zD$?n3^^^q8HYOOBXFsW-FP~c^^4xh@$w0iwo(&BoNbgjx4wogDxC%R;6;+co6@vTD zty6-yIg#jW@HB?~8Knu##Gnf7@F|{vTCCD+fJEhGxKiyD2Wp3rf~dQ2x#BZP2yNDc$-b3n0)KoL1JIq#yC7Vev}M^GfT+@G5vx6LKU4-PGI5&c7J2$`nKb8Z6CJ6U zojY65XKN%qahoKsqE)lvY+S15xk_!P>9Hr?!Ga00kca)kJA{63PPcSTSxa4X4uZ|P zb5M7@$+i+&EFLvnQ!tBji(_eaZmjiHXZP|O2}0$)oqrAeEbr1DJi6Xa)Lb^kRgawX z(JnV6U(!^q7#l0ea`n9IqI5I=H52&F6<iw(Q4V zKIt-JhufNH;qqILUg12-Pc;4WBk=DSn)W0zfc%{Rgznj*LIOAgvIs;j$R2_}i} zWjZ)75TAy7i_H>yCex$2E--*yGz#2`#UBTj+j;w0_G#1$HNH-NU+?@}W3Ab(LM6-F zoINE?^WntJ<*LDpGuZe#?}$wxIve8qr*l33Y;Nekw|r&E6g1%pFV$s^@rj&@#`$}^ zwbe_PN9bO4+_1TFdYbD)zEMF{qNu*R$C}d*a(|Pts=dEl|ThG+g0f!O~$c%w;uyYuqeuMP3^er;XPp?=g(ykNOL(BNr z9$LpCAK*UHDa8#BznmAFrrLooIA1>kdGa-fJ?HKl((N6u{Ia|KP;~6s7Yzl<2emK1a$o1t->Q0)3B4u}$YDI50bdL+UfO-| zgiYPh&#Kq~@6j846GNEE4XR|ffmCke2oN!xSJ*-;<(-tCvq;MV5D2~Wy9-6rC*?6P zfD2F3xBSpcn8QuiUw>EbL4Ig{30hwpK`CLUZr~j<)$TIL+A4*%tXiy}=B`@dp^x)m zdwMRJmeMn1;I1VKEL?%%d>%S|zrxLn^}Ek-#e^h+(0K~)7ld&2&|RJbyCjSnHl&!i zYLYd%u#d*@j@V|Msp0QzYUHuXUI?y!8p=c$B+lV~?{UQ%3JqqsV=)i58f#35*RRr~ zr@Q)rl8H{d@12czmACWcVMkqP2#2IdNWb~Kns zo&_nE1r<+;9T(q9R}m|_Eq)t&?9NPDEQb$rECrwJxLZk7;QJ)XEd zG;2jAY5j0JTHP{foa}WBM|#4(4!&yO+IyD!nDyK1(MZbx4U^T%(-#w8H8ixgTubLb zJ~Op`(W?M2!_QQ)Y*U$FtXiJV5J;YaCH`5gT`(7)GeM4n_M#aWd?|&rkCH?-rS{cl*ZZKeF!}bRSZ>uV>*3b zCYNZaDAI~uJ+s(4C}GO=kekFCYZJQ-JW0v)ENLL+Enij0r5}Fd+}*>^$6alI=#CUO zVNJFL^E-&baSK{xfsHu(qJ_C@eNP%#Jf1G!iC}J46m} z^2vr6@1>yA=EKigjDz2X_UeYP1 zK2{nolKSq2A_>k3t9y|z;}T_J^XgplYlZSNiACvABzqJW8U04xQQZ4QI_a8B1h(R) zI4zaA1eJobP9t4T?6Dfba!EqybC~A(YM)((CTz>)N5e(x35}t#`%#ORp*Zs59Ja;t zRQI&W8M#sgUX-kmTUo|7s%Nof(QU|y-3a|A5m=eY8z*B z8C2%Ne5aTsJ5b?mU{j~<+i21wr03S-}?UA2D6^OEo4WWY=sSth{pO&_}6(HG{*ftN-Q7x5`^Wi;9pcFo+_ zN()@3M2#-LV_(v3H>cuBDCi{u{79ozUxmeSgWW4^3lL$NLcfDigc<3))! ztx>d_R$GN8mVJ!9NufwELzXPbJpHlU;|gcfRKyhy<=PdaCg-QP6bE7C@KiJ~11sX4 zD*u@~7&}VqGh6G!nPC1(S9in@F%B+EG}NiuM^O>2W0OX#IPO9or#;3AN#9i{=fB7Y zomYp;YDimc%W`w4bm=U{J)j|3yE0=$3>Jy~#Ukje?cgEDuC>z(+Ik3csN#9yh zlq@Q7k>-t&Dcq$+E+Hr4a3`k}#-*F0d4VHnbXY+U|4xPq6Fqr|Mq zpa9kJ(KI?x*|IDce5J~2o+{C9RO<2|e2^UQP-V<}DMvE&8KmlEyjxbP3#9>6?IoY& zDbqNHuUg|Yt-W|D7V7U~ihQw;R>ZBpDQKIvNa7&K8J~7oA=&uKd_nytb!R;yHu}-^60; zRJtZ_I3hSvj6lZg_jbqs*>{*MdgR5l*9++%-}deIVt^YU#VTOZW$uBYngkEa(B%^! zJ8uJX{fIF6wz5!;o7HTwQwQyl$AIS%UcR6BnHiaWYpumDQg=hzjQ3o?ND(sIk^qF~ zXPf_r?2t=Uh!3a&GNjZ;pf?7=APYWxF6NEE*I)CXH?l29r)FOf$Q;0MdC~NAMQv&v z5B5%GRi0;|zeg=T&fXI&$j)NuG+QJdW%_E?t|9BLVs&)aTp7GyBRB;;mgWr3kx4h~ zoAGNR>IvP2(I3Nu7HgQc9(f9hvct+&tvGsn32#|*$Gj_bDpE{tQy$`)c&CCYubjXH zA}w`TUX3*Y3#^Lf$6O@qQSWj@TBh`cZD{0VkVV{JsR{C{^ghE3+Bl18D1)wfZOM+9 zjGwWR`JE)ZN9EjE?qu$Ah|*|C<*De_GyR&*B$9ox~(dPhw?er zMHWYN3sExKDtHY#SHSWfUExpkQ*~(l(Ch`~&5Rs_mwIf6*9?i*7-Mh6GHPz8jWHfq z7U9xy8=5y>LR`y73_o}9yz=K2Uu(w*tPulytJZAHt(VSO=LTkwaSff9sXpzJxe052 zGOFig+H9KyG15zm`5%7IO~`tW*a&_ps|DTl=%vWpqM#gvgtekA&pdl2PKay1SmGliXFW1+i07^jy7p5mIGxJS>n%7*;~2)d3*`#8_&s(y9@lh|1BjXZKm zg1uv%K{YJXVl+wakSY$HNY-+cqLAf}*V-PbyuCeZ7?5yNvG#`QKun+kV=}hhQ>tiG zw?>Wh?Miqk>0RptuazF$;WugtYbbESQSpZ(Qu>BSsZ*)wQ(2M8c^Em;aLdv-OY0H& zHYg*}<3tzi7(Jj5R*xx%`oAr2PMkR3@d=Tob!NxSzGSuFS5vptvEw}z8GC}#5U={W8!lU5>=(d2d*g1xQQ(qn(#ciAp50;OC^|ar}C?RqeNQgZ$X5J$;DyI)$+A! z(_B%iB@O=kBNrx`R1$;B@a1!u7T2qBhx521B;p9JFoLJh-$#J8EuvP-h}(P@W7k#2 zcK2)_u+&Ww1u%2qT(R`&W6A*{A+~II>=I#sYD$R1i};Rjf;1H{Q8E(#o%OyJRg215 z4k$Yplen!Z23SId?Sp;+WzYOJA=^5RF8^(Ps4 zmLnP-*1>|AyvRl999auC;6lzk>~cCoWKg1RoOC(&(ed%hcS}rOe$cH@*Lyu>Q5sL) z1`}u=PKU=*o(s6A>`io$F`Gc{ZemV)&QgOx*?ENg!08F`m#n0`oo`KcaijZNv{nz9 zvJ+;61k`smWDHCFMuPPU?t80~*D{}3!mHbd_ApW%u!y?hMc6Eo$~sSXMpM@bH*xDl znm%P;>K8yxm#>6-b>+k+*(qR9qO}bDtH#nTsXWwe&Q|Y-gi0mPn&N3q>5B(*&euve z;t7dKxSX-IA<-_fYqmAKG=(sSy8#M*moQf_&q|6x91<7`ZD)mONqP3o@KaDq^#fX@}E&`)zgo1c{Btsei0cVg52P$Y}~CG*XFUb$xCkRM;&kpuQ3 z?WAOz4Qz;wKQTfwVr4+)JA-`8(CLKhQruP0Bh@k)GJdp|6#EJ4CC_}-&hId+#sb^X zh8>CzhxDTAmpICSH#4j|BpwIN!MtCfb6X4PSYiJ{PhKMpxzQ+butR#rT{K7)})-nR0O0L!1Is<@pSAwq0hKWe3M1NqL`6=U?P@tU4}N$}8+mVktdI7eX}MrMB_ZG&QRGeUlkQP9 z`~$~Ah#Txfh@)HmW$G+U%*stvkv&}}YDigYt4c?-d%r*GiBjFSkh2#xuR^k9)xPY! z9-93b$?YEO+)^5%x>PO$3DD+LoP@S1UC_lPg?61Ur&HDP>tHVkW{` zixs>1xbux=uB`6aje$dQ`X5|9f(eZQS1BFM##-030Q=g}1&_f1qQYGNPaBF)J1ru$u?% zD@1bHe&mX?U`9-v>b9xI{pib<{}1hp@-ANamFV#oAeLEr>!xmkPb$C5EX9JypD9Jq|O>Y;aq&k=T$dl1y@)$FdQ@F?^^4Qs3 zF>YzL1Bgq99vzjPH)hA{*%GYho0e=2#qiSoObwMLJ4N0*todecW0I?}w!Pn|9Xf_i zYb`l;Lh>4)VVX$TwHxmdtpsyp3V}mNIK4t+=GXLfkQA&#zT4xp(*Wi3o+x#+KV|@ma=vnc$eW=x7X2mefbohPL1GWdjEL8(zjc=QY=r+Tko()r@IT1O1l8=g9|s74m$H zs@ynvV;a_sUh(8%D~fvpGs$jUMH=Z2tcOBBSo=6`%=(1dX2CUaEMZGcE@q1|8>Yyh zSk_jZAlEtG3Oigq4xLV;lj1}Ux*+;Oe73}#SIK<#6?uz7yZlP?3!j}V*9%ce=1M&# zc$W|BzPD%14UT|WCzK05idLVnYOwNjP~u3<>jfU$P6Ci4 z#BRhVjMon%%kU!NrA2Q$x(81;gfIHVIqA1p{BXQm{T;_^nX~HKz7{JZ!lUfp`$k)M<|9lAlkN)7lyb@2nDC>ydWDUK#sJ%zOM0lC75Gl6L?d|zM^h^DU zm-%gnrFru0FOv$01o8RPYiR;@0sCi243(seQ&`XBOkDj;eX;6w2DtWZrsa$bDY>C-YIx2rnPFjob(F~t!o@AQE5T*#XVKjT;WVGe}pW})&7 zl@0xWFp9TXTYT@~O9na%vo4I;9_%hDTmUC>AU(s3fumssbQg`;Kmi)X>_Z&Wz~pue zTo<4o>g4y*9y-l^kZl%3R&|~~qR@hzd|TeUEts*5``o4X-v0FVQ&`5g%X}y+UTvktmqh2~oFvc++=6p`I#(c{egoSb znJ)^QO%t?#)7!3F>-}IzSGxOWpdyHc4x4T5wKu96_b(hzhiZ%~ezTtR_N?W8C5aOs z={)!~+cXSI{ZymHd< zM`mMs14)=W+c&qmAKIjz-|AG>TeuEl>>Qj)_3akqoa#Co%?Mwa;3AdGa66w-vB@pZ zn!09_+NO8?X1Qv7@mxYXuhVe0XOE)6tk9QQn(II?(z~?TH^d0IUq{ryR00!8jVuRS ztubq^1g}gjOnccgI}v zbI0M8|CwU++nDpz4kdM*psL6dbDaOy%A2xo@nV$arULerhiuTmk=_Q{=gGn#-dvj< z&6XwOe=DN;A>W_W#kKkYDCfxrY&GDIh_=c$!StM;zOILMJzav^oV7f%j)JVpkP1}5 zTJg6JV}vXNYfU^dz1yoY7hQVzt7{CZ;vcV9|ElM?>Iu$P%bjQOwEgr`Oj9R)`OxxP zpp+h^@_3_<>Zek5m0i+f2a~xN=(<)IC}2!PXCX>D>ld=UTsBOd;bT8vxWFyC_}tyf zEaY2!12YbvzN1P80#lf77=^~LqyJ-zUH`v@I>pcC(<4n~#|5hn93}cQd#7)>-)Y_W z+-I2k=KF0sCZRbn_a2J5t#K;!z0(i1-0fEq%_pS~WnzyX)zbEL843)r1GN&s-~Y5& zSn^e2={@O&FQ$|V!k2NQ|IJ;{f4w96_cIfGHlF86uJ9lw79KonpD`C zr{`|4J$1K=if5GRJYbLqthp?2ylk*g-6am!b97&LY8o0g@ z$zhK7T*0JRf^(2|9qg%}vS+>dkw2f@z1&EfcAl5cN<&2XL(Oi~AhD7UqPK%noU+??^kr{OkCJnW8@^ji@Zz2f&FwJ0b`PWrwpMj!+xeiY1 z6KBYn!mc}mY#|C_tr>H~81T?uV5up=lIp@#SAnP4J#7xh%#=t-g>Q#k!h>r{qv(O+ zIrbY&f4zH;dsKN3Q7$xMj@@4iJ~R@IIzgLlRJA{hKbN!B65KVRpt;MxO;(P*z@w-F zcS$jEU!8aJdE2~XWjcmEA%>_9-eK0i|4=Yse9d3|nIa-ob3pqGVDo8OZ5rKOgJZ=%JF`RJ(g=&X?`;*IoL4_6mUVvc~Dq{b| z=o=FgJ!i87=}e|+p~Z>Qr4OV@5%Mqj?!Oh;@*GsZ&$HEb7wFu(@BJYM3nwUYceUDY3Aoo867rAct_ zChf&XW9tnFRf3m|qpC5^4qQkT3cZTef@uub7u&51sff?S)M^Kl($W_x18+10X4QgC zUpcfE(vtmyN|IVU>qd0u(t0QdyVYOL60;3N_mHw3eLHS4E*oJ~#FAH=pD1s^5OfhY z0yi)dyMuR;zm%?QT?gM=E3UO`QcuIpdR7=C$R0A<_BmgFwuzB~^*|HazC$~+o;m5t zxdlzBb8Kz4{biXBDQ0R5A{4+2-)avB%V0Nk?UV~8t;mapx96~O5V5Smwvfq?*R2_O zbxjde#;5FbW&5-DiX9EAL#?uxmRgvx4pZ+9WCS*q=S@3mX^hQsEPb@jo3J!f;yH+G zdm&v*N4ShN_SFVdc6D^3jkMsyLa#(2vDGVFgLZhU2*I0trkVSp-EEXt?v6UL>^iJH zXUn&z9Y28y4ZMddaVCd{l*l4sUH-A8$8*qgs_r5_oA_CyesnAC zc7!uW}M%5-Kby8azOKl0IL?TI0( z<@efzUjDX(hoZW@kc_ZD@o2wgKHsaNUJeC?L9@Y*om04!Rr)> zly>z!yf9y#jc{EH*dOdL^u0l_RaDt7{YbF$8#QYbintxeH0;aZ^ZmNE8{m1XL`E6b z25B3L@(ehoaiIa?nr%0Teb-7-)3}fU-f_HH&Z#&a4HH=@;gP9mQ7L34miwn+7xoU4 zupPLBU^k^g*Gg42)^&*L%5K6c! zqT;^I{tr$!%ZkH+BK5Cn-@HiO@*`+_PJ4DZg^IWc3S2(TDEPdO85?kI*Vy3Ftq$xn*)lvkl^OITe>(7;V)-$1@BS82w0{@da0^CIJhxmaLjXtMv-saVpx zFjXad4Itiar7?DHNRwFsa<2T2elr^fL{mAajv8Msa)WRC_^j`^J#9<1q-rzoj3?kC zCIAg1BYhO*HO+=SnA*r%q;NUy3On4L6rpGq%NF$BQD$dM1IgbezbWyqslxOs24*!o zO@Q)RbI7UM4XDZi5w>~E)G}&-c{sz%6d|?|f3sd5Gk??k|E_`3+`key^51YUe~T7N zW9ECbY~f2W36%w8=S#e z#pke>llf&cn^gNObC|GAqAgf8 zT?Tp-sxC@*_!;cQNXqQ2u3(#ne~6-Nae8({JKipCwzj#RuaAvI#33!_ql9@2wwbBDL2czQ8{>Z3$iE zAVbWZh8XOp3$I|yiU5X?=)`W;0W`GvV1O}LKcH71e(4Wjg^lT%2;48h7Ef|}^Pze$ z=Iht9Mr+-k1cG#H`-Zq9)HfDDg_)dYLe?VYw7k=VmXWGb!0*o@4=tyByQ&=s8?!}h zJQ+cY*L6U-Ltc_nJI?diz6Q;=)RK`6vAV+H;<-g}l25UBXWch}4N}&~A6w$^CdDFc zfszW^m?Vdd7pFilVi`&Yr&Xk1hh5KM7{SH5mEX0LDl07gL=&M0X@t9Rl-)CbD2mpd zE;qt+es3BJ1e#7(s!sVC%)ZyzuhYxfuQe4YXb3RPY#wznm=)ek>#ap!9&Es1wmj&* zL)!ID2_XE$`6F^iJJWX%hk8L8OOsLyN}n0?1pg!(%>680Wnkoe>le@${(xQ?2E!#X zn2QJHC4J^i`5%iFPwt4N$vaI-Mn5us4|6moJ84p5ifVFFPljkMwvw-wqNPJN9U)3eqQzLrecy!*`-ilp_IU?kulSZu)c;Rbi>P6}P8$E{)~oR*19=wW{L+CYz#* zdH9cO3;nOJm0CVq2SBC@+$Ov|daB@J`$=7%ThEj^GUYBRDUEP_5p2QIT^TLb|5Oex zpR+wOA=Me%u;srjsecak_ot-3)>39I@=GpG4GCWC`f<$zW6saK#E3l?_j@VV?zRaQtkqd=Os7}h zp|pJ^l5*9QRU~mfxS7nnG@kA7z0$=kKVCpfz< zoaUAuRlw9If2WjR!u7Y?c_+i9Bh!_btx^;q^++PJ=0QYcLm3w2#UB+tC+|7fUe(NH zfYMVniD)1;E2tHKJ3Ym#@!)__r>1t#Kq?+pJ&6k;g9IQs!p8iIp_! zC#{B4YL~sQ3i^&2Udi+<WpICd-Vk+;Cb9ON^lD1i`}jH@U1a)e#y zuU2CPTv!TTZeqlnSt3a`mPnI(EUkSFc@Vq~q&p*?yG9-%5#a9Q!42BX;VsSY@l zyTS-63LL?xv8qp4Cl4%)<8ZDso%`UEJQ#&CHT~3Sm`#q!6ciZpcYX2$Q^&*Hc{HpQ zs##%B&X|)^Rz_vFgqgG5-Um}^d6h>JBco_?3*K2AP1>5^k9uJoLt*JJD9-t8QF-U+r1 z8PbiMtB~k#c9Ar7;1PwKWH}q`np+u|u3uYGn>-_G9nqjGHR6+ac`fzi*8(ZhdeE_m9JD+Q{${-R~c*uGM$JM!ZPIymy%dN}-$2R|}z&(c;MBGdiusn_3 z+EJpt-c+96M}e}>j!4J#s6@r4S*CQ?CiT@hydvb(-|)6~XQH|U!W*RIp^gPX4M>8O zNR~^M9Wwjd9rP%XSgNeFE3a1v_c^F&9^tOZ9>i6$CS{HX6C|rc(JJ81YJ)BiY*W5M z>-O|U*XYK+i?hZXvU8oTjlIxpw?9q?!oF6r&E_13iOi#dVRsS_)u3pSkL$H!i9=tR^d%x_?-x zyYH)U5pqv%QBl4R#3!qz6^TI6f{aX*ZYTEXQZ)x$(L&LA#C#R{Bf|WQcMu2CMcRcN z@Y;FrC8)~E|M_OVb!8O(K-Hy4TvM ze5!+leLbyVn>9G$d6F>2Q|1K{J*D?CH_qEh-RD$OsxvP{SIv8c`?+4%GSq@E zD&{uha6LR<^=){|hB&YsC$*fkU#ImpBowf}cfB+3eVAGj_Zhj+96jyk8uOAw=>U|r zb;o&jMV*33yf}%(s~=JG^L0Nu+d1VymDA=T`qwWg27(+;t zXkyk!;Z212nvy7vq4OruB1dYA7fI3R6>>F$k5EqT8(Qm*Jb%mu$_8(q6H6d)6Keq{Fa~*GL6x|J;L|E?+oo%{d+p1}K;bz1|<$e65!R6{>?AKw(ZI7Q#?=IgL z8=3;Qb(c7(Ek$+(-1_X%;NZvWB8LNwA;7u zfwsNWcWnwKkV*@$-%@IW-0|2&?QIEueFEVLy8goi^im>o8Oi#OCSw05$IIIdXHu!o zeY`K@rkhM)t~-02JDAUyu{`-yp24c1(dr|Gf^jA7)HwzWmQ!mz7?|_bvowCI)>#84 zsnnJ1^LiGbv}~&^C+8XL=&oXCb4$~=J`$o&MklO>q761-jOLsm-^o8aYdt-BKz6j8 zTNL>FZ-o<}gts0cB|H>mlrP{k%pzL*i0Sc(+2&-X%= z{PW~bDoR$M6*No@an2Rza}Hq_F}Z_w(ib2wRpGsP@-@E4p{TulvQ=b+1)XVPNRfOS zfzrCEM-9AmI(06JtfU@d(r7k%?4)!d1K1EH=R()yD%*C;>2 zlPq21?#v07me25I1-6FpQpwqZ5z$BIf6fzjq7wua;D10OqSb1ob(vUATfYv|ux6Hc z^h0k)smkkxM0-3X?I12wNV4^dZgnit?gEgYx~&Y`BDyGBkxb5!SgN;j#5^TPt}|NJ zeCiTVAcDoeid@ag6fS`OG4;p+nB|omKvxwo$ARt89NX%^*7Em}bsA=6#Q(Q&5zYrM zV*cIega%0i%0B%#E_KGLN=30eW6a{BGQs3aQEWA9eO(X~y8cOcUqLo$1bLhu zHQlwPsO$fj!+29eZwzOc5-R@u<~jtltQzoKogQjmlJ0DgLm}=N9GbOA2e(&h1v6^e z)8sen^6Urs89;`8tHp(Ic<+?Ha*nP=51SCd*-@Ot+I8tzG7 zVn9{kUH(MKXa|PTGW%_vv|4d->lQ7;mOmI>Wvc(B^JLq#w;f@W3dh%q8{(mkqn@M3 zQQ)Qq+o_G&6iE(0MWW}Kz=ue3=T?IolNV9rDb^IqddowO_XNbwcQFd0znp=gzk<^+ z=^RY2t_zg{%Iq*@Fk`qq;*3w80d3*(!jH)Xg-tG+aqpco0?fLk8$CWk;oOZJW z;t5vro}FW+rsn3gGTy5UyY8N`ar`}-8@dOIpLVDZF5xOqn>kN^cmc3%cp1y?)LZ}V z>~JseXU}Kxs4+z|hKhWe5hUeKVl#*A;z-cou5?MzSsK2&sF`y_aA!+ilaT%Px~ z?ka_AYB~QvQ9OerjM);{ZyhxnO@jYudGWQ@@mtoZoO5rl|9*OF_0xVxk6ztea}(Li zH&45kCt@?B_NWdFyS8SfEHfVTmO0r8{Es>AU(a{{apx%fd+@Wi$m`FQ?w)FJuE@5k z)2Q(pgScH@x?|f}Dt%MSbu+s3dG((a=i1DkKS{mQdtc};X6kz4$i<<`RXXMrKj7p0 zYm%Dr3A7CJM3*|z92oTGKRT=7zb|V4t(N1=f57b7sdM9B@?Yu7smylPzaFSQU%k$3 z$+z$ruea}k+C?vA!&PKwlM5lyCyYUipw#9SS@92+Z$QE)n63S;BiV6f5 z+~;1GpHLc3sNJ!gGJK<`6zi$^6KZaA#Tu`iwABXQyIkWz_a_G*6jn@I>9d(Gfo-y{ z#rstZHf=JRAtegbCg@tOopJBmjVBu7zC~ZNFGR<~+ljIUNbTJhXQ|SOBbb0}cz+jo zoKIhq8I|x*<2*~i(V&LLPh0GbxG+g@5wCJb&T5rT%EANOG)G0`BP_Y+?hdLCd}%e* zO&wZMDbJ5<=pCwS?%snKFMDsbE2I>06JjmdXS_qVZX@A(d&Xv2>w&0`aA7%|5D94%Nyic<+OTqZq_(xJ zWi{O~seF~NP=(VU2lWQ=l{2(tpNBK4^Gf-Syb6NZo8M^9%{tD|%}g)8$(*qN)Tb&m zA9_xZu=dNaVi+XK9ZP!o|?ww{aNhfM61RCz#QLf=@{Wyt4;@gf0kKT zyp<6zRDRcPEk?r1?v28-0?TacN`HYNW+YJ(SlVm16>1jC<__op2l(x`QtOU*PKuCt6`T#9 zxd*dcCDLtJ#Dl6;ob+m>JE99b%gdeIML@twmGzTKgG!3$Boxa-3Ko+J#f-**-j2DE z4vHi zJ@|}JtD4b{E21hY7pn!W{&UYY&cjLLjf~_~$UV`PNrvHAC5K%levAW``-+v%Tz+^|mIA%#x0q!Wv8`|$lcmQ> zQ+0kKL$?r%tO&B(ZF7qa(-I&Ty1)ywFPUhe;^W@ zzBG_qCUC?6t35O4J6j^{O!`kI#ywrx)=4`WmD6%QjEs86lo5;cR-P%;5M`S6DRnlO zbb#A8A)d=7u?9lIOoFE?iP|&OX%)k;-gN5tTGa(^=Wm+pPqFR+O>Cz$yYFh`u()cv zCEROk^sA$udWx>F@hh{?=h#)sUD>U37u#}iJ&wdt7YlQLA6*g{@4gf>r%}w}depFR zJ`_!QpWarSS?|)BqYlXkc_BN?Y+HNuC|tL1>v0w+nal*l3z-rc#be(zPzm&Xc|C8v zhD&<g)CT?riG>~iUHE7&(c7k4OX7XNs7 z{O4874*1gahc3wA)uepFACo%V9HFR3(x?5Z7q9+ox|(&fcZ;&nv2QV5*_lJpZPW}u zF61j*Ea^2_X4pM_ht(kYHY&eO$McN8ee(P1?v7~@JSdu@Q^=H*r`wCGt2*tq>rLF-OFcZc-Aw>})a_Ij^vN=w@=$p^I@z{T zZ-ZgCv^R#ejb^C+bQc?4RGA>HXK6X->xfK%cdB2ICb~iv=&rD4pQ!EesQMl9aLt7L zbSQG6qf{A5P%&D-EwtILH8qsq*#`Ca^#P8mUd!IVzV86h&TY_r7CnsW-Ztyj(_BnS zac2ni@?4lukTkKFU|moXSo@W5Rb|q1C}Ii`G;CmiMnvR|mA$!yOSt9viZBFxFfXWxC8P|>7 z(u}cqiIQgHb5QX|i1DOh#w;LBXSf67k5|H~>5oqM-zgj+P2wqlDSUCGjl9)+5@i=qF_^p5Kebg>WKGCn`hRNVOdFM!iL*cfii zF4)KUETT<^+vx~9QOFGiqM^(h%0D(J^xql3SgC~D;vTaj&OaGjmJ95H?*ZL@%e7v2 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

        c-;plccKy_3{(kCzd5_3%exIv~ zVyF`R56gP{{^ez-s;2uAkzUF2?lis&lCv8_*x9Qy1+)Oi?FK_GN0-)*QcD6hc8ul} zOs;u*f=Ys`$wyI{X19SbV+XZ_GsSVPn5Zed4t8l+LV#Fsplp>06tU2nTvhBiRbO)D zS7Gb(EFjQOs1)`t@tc%v%*3(#DHwLq%28e}scWWX_K72mNhK<4GuF(O=ZK8`6V%CR zKUcudh6~=(hMW2;;;I@~4{g|6YP}g=*;V5MWM@V zR=qe2rj{A!%M{zTN^^N7iEBUtsAJ+T*=Uu{2!tS2ZAGh??Bi?q&eRzEC?_4lJEGY( zPW4tlQGJk)aBx_iO(+yk!GT_KZ=SEpsbEG$EJZEw|9V8Z5|_Zviv1|2w2x$##J_4i z$D14%v%W^o*eskmFpWi3-s?|}OAcM9ft@xU`69ncW9fIPD^gx?cNL4>>lB1*?-7 zKVwnXDD1>6YsC}D1Cm#?r*wKwxJ#6xl+72D?1P+{h%Q8RJ!L-xAzz zlacM_TKm0ALt;W7RhkYK-~sfECYO+wUP)K5)4>LD&>O8M0Y2CFu5gZdq07Y}#nGcu zPhhRm^ntvh+5f@bdj>V#w(q*33j%@?y3$($NDERF>AeOLAQY(qLI(j0B1jQJkzN8w zFKHmXC`t=GNJ;1*Qlf&0f{n-W?!5oK*6h7Mt~G1+?DBp9W|)B?zwqI@ulqcYBj>sW zhEzUU<}HQk;=Frkn{h1>v=Om`aTej{VPy$gC(Ti>g1t6vY^ZvK2CC6>3!kaII zn#fnz%n;^}^Sij=;6hv8zJ{WaMizLG6}h|Rx`@OZ3v(Mj;}9-m@0sentrkP$Y;o^J)303O30YWHPyWP2#!{Vy)Q4mXXoaUR(cz73` zmg(qkp;UST;yo`xOkt>-1r<$la3PYoO7A7-qQG$z4KH=c2JGu3#9ZKF#%Ic5Sz;E2 z{YSLD`%5QuJgM&S7po5P(6Ph&*z!R|(Dyc!e}NWW*vXSl+L^W8Z5sggp927pcnguS zRpzBarais_lNYxMD*|7gkxa7vz${*O8YaHN*@$x}zbN&pq(bd3`VmBagE}5Uy{lb2 zEW=_+)i%9_uC!$_HG7T0Gh7+2z&r|K5X47|j-$!BUKnLvp*a0AH4wRTP~35;uByP^ zBu~B}qK%MOKz4DN1d7`IYJ*FL%=;kZty6ATe)+podGjjt+CP|1E0wYrSayivPr@#G z?)j5Q2xhuNoA{w!+GoS0%e*D_??raV;${Vg^wN4Pn5ZsQ8l>4J!RlGq!^=Y}X6lTF zXZT06J5S;{hicb!=D~~TBEVqPy} zMU6IhR^PhxN&{Dltq&1?Ocug=N9JGEFjFx~wzNG+8MYvUi!-+Z^CCd(QeZL<)(C2Q zhPcF$owL-5vy`BZpef$gsqCio2q#90tR#`K&p; z#+g?8*4J>yYu*cf9UTYz&K3%bBnw>3PEC0m1V_DazhELdI7cjQl?w><3JzdjV@Di1hvs@+FaRZL-|VP!cZqJYe>Nr2@5`l!h%&j7 zuxmn?rrd;2i6GAnEaA^v><{y3dsCI5#P8{Om<-!R=eA${jfbbgUR)74u-^7rN}i)_ zTJ8BHYmT`wV!dN9b0u&|p3M+#7T*p2-x`c~DubagN zIen}h^2wfIMz&XhWy0Le1>x>_pYk)$<;$L#1g*x8b)?*fc6N46FEXCC@V9-kI|d_< zx|O0A$z{#F8h80@=_k)&Wa^)NM!D`H2bG`#wZ5Iw33vhDSkx;Xo(s$ea)0~&D}KP! z8vXOp&7CyL!=sYF*_NND=^f1bhr$p66oA0x5jiMNOT z1@L(IHF!C3`DMHoojrSwzx3hZzb!sAQv7+!+LO_ksFr;~dFj0`ZbE`I?B>zBJDbJK zM*b6$`(g|CH@=Q_Aq#P+>w3Gp{P;b<2e~uC$pf(DoBhI_?oZCsi$i)v$;hpc(F+K` zs>RJXvcJ_Gy~Umz2YahIQeRv2g1d!tg^u+v&scIAV1?qS_k)Hy9iLKBt0QwP76QIb z^OjnC>(rqgEa}w@AH#FN2CBP02~I#%;@EK*LUk)^qRN)L9%$$H7x;BT{2HYr!imZJ z{Jr=P*$;>wj@VP%g&NaHk_)wvF8EDK0*Bt~30Wl}C(iO)5ixv;i#KDN5`Q;V-@NVg ztg4VIu71_+$5&eT21S7UH_uK^g(?m@*2Phvc_euDd5HOxJ=(AR``{rasl=OX6)rB$ zQA|^$1AbGwLf-*V#)3HE$eE(p1N{nn*7&(cRAEvwJWUL9$3dUZAIf6bVg&n8$m=u} zPhqGFIdgC}^n*WWmI5}`?N>7Z5ZGurEui!)94D}`F{fhwLzJ3Z%J-g*gy%E?C3D*XiEvbrjO(5Yu?ng*SaeOG{LjzEuw7%!_oEzO{w9 zOsDV7tft|ye;aG_w6T{y`6MFmI=;IEH>9(Cu0*4Y`N)#z5{^_IZ$GMvIne|K`%NW7 zp^CN4`Y4y2Q*@dcgQ`Wa9GRZbmr3{OKw1kg`v}6cB{!&%tO0lZFxb1SPG8)7-dx5N z{gZ0sHahwAK4F*vGQKH($`yO>Fp=!;KaDhUtCMvo^@82@#q_D|qpID7*JMK~ZA~D# zOc?!@hP<(V*N zH|tY6OD_^;I*$xW=0&68i?EU0)DKRaobUF5B#lsh{n^pQZRl&VdkzV`hmMDnq(8Vu zVH#c1RH}vBRoAL>d0s$G_Y}ym$5Cz>k48;{s{(wrrIuPY%7kXs(Wz4B;%-uD6fTVIvM{IX*c`Gf&O6>R^U9sopwz3w|X_|x6-PXLZ>%5Zrrfnu;89rKS_ z9*{6`vLTX<5Ez54Ye0bzSV8kkz9m8nPzB8T=LZu!&0jZMwwCFF-57gQ{r#Lk1^&W> z1Dqv0`4;R<)uELA2#QeM@3?qkVqE;!Dbc5AiHA)RYa?*1#$qMk7WV2n&|iYf-t9ZE zI*|c7h@+P=z=mtS?YOD(*FX(R7|KGKZL%pZ4>os7Upm)YN%$t4IbcG@X-!PO`n^oY zufr_HSX^d3Yx_i<3jHV4P%_qfs7ZIsJuBV0(p^MDnCc`+iZYqMqk2Wgi?xyPi}P_vaO;D0}UFaq0#V z`|37q9P_y^0BdE+xD)PGWSL?7Cc7=8#Wvy1X=Wn7DQB#2VY@ePxY?Qnruz###u)Yj z@9yG0_2T-tfVlm(A>8_1tgo((0PizCK|JUkL$;^u!x+~d>3o!{xw-!cJWD`2j$kYo zwlT6;R~mBOOy4B4Kv8Qd2`rd?ZUCjh-^*vl~len2eK`(cjLzQ>B}MsDYy4cSIMAy@?6h)Oz())p4zrZ0)}( z>)}21U*GUUSTY$O`-^v`Mlk;TypG@C0TmN~NEm zVdEMMf}&30gV;8b(N#-8VmFtS2-R6jqxma?Vt%wk^JJY!wq+)oLvfv}9=0$7G;s^P zIs2oLNm)Redfgkht9g*RlzSn~GG{Lwh5FrCVY{n;k#sT+3*+84abx18Drlnsii;Nz z-ruuex-mmdGrw#AI>-$!q$RhF=Vl*DAd}wt78PA8s`Cb6Gg+Hbj`$e#=u!NN-$fVy zS+J7jFXam}wShUDIxSAZs4r1~7dNTYs6JGs-KPW9X-CG+c^bamJJ`^KtpDKzplp+i4AV5rfjjI*3%9{6n5cZcU+0JI47zqucW@tX;BnC3(#_Tl=~ zKf*5JmcYmDX9>JhJuvl+~18@hN3&skw2+LLXWVU zVqdGqI=!ujLAg9`O|R|xEa89V$@E0C<|9|m3mN)9h#6y{H#sI6bLb(5u7@t$MY}c{-RiD4{*KnuOEuNj`33Xev#|wMrS-toL z)rqESU`*>P48Y8NL5>SuX8P9ticIx+JF!ov4hAE_?Y7=HCAs22Y4mTT0lF)66zKBz zC#~^9n_3e za0e7k1q*1i(R<`mW&$lXC8#}G$iVG;HCv<0x@z9|+gYV!-raazbbL0+DOQ{4Q0_N~ zESJ~ivH?sX%aqkjJcfYcf(4k>u1emtU?oR3jY(wSKTZ?#R~l-9c}P6W=tDw`U`r_| zk9>z&crRB)2(tR~)~J8WoVv4;ImYB^-(5dz-Bf5ApaRn#6S&uLtMoQOWYgb!R?u0x z7FC9o`RdR;t1xvaK0%5o5q|z+J!uz`uG=Z4yF z=ciVQ%D6DG%8S{V*66j7*W&jmYbB5C`5Q_Wp1pc2tyz*JkcT<&U0yqxQ=ktxd+-5n zWw5q*>8IC-_ljTkZ(}_v)~h7Y$X*MWH6$yu*W;b8MU$DoIG`JK*Cj(8U8a1VtG7NQ zPk6i&y|zf%h6&bfnS`zwWWgr|)!_?FL&S@QGG-h{SunTt>h!z#uB^YZP|ByoV<+zc z!7&tZW6TGYUkoR#=}6VK^+>NTbu<|6QRXVASP)hrr)XalWC|8b+&v36JXl`C%de;! znDa)h;C;kO8Vq>OI-CoCca{a#XstTcyH`+u^;g@Nlai0A%b_?>y%CM#{sX1<`Pxl)Z{SBVeRM5tD-D)x;om-3KJxzmfB=0{_MHj zYy6{%_%T@w!Az0^GyL^?AxJ6fB29EicrrrPN|`qgwuy?-MtBQpd@=%uG_M>%A{rce zbv=tnJ|A8En%|}5*>;9Bzp2Z zd$%RYZN|X0)4q72D6H#&-&NPYo+HkoY5Le_)9hl}SQ%Y-Jpe?*k!A1H&s$`>>Y*r| zx`_loF-R~%6M$v1tEJwRY0{{Yaf&4~N^kaKY`Gx9RXuVV8Dd%JM}Y@}J9%R!OePol zQ5))!=vL4+1@%+oqq>vkTl<;9T)jxg3AIj)kWmGe6@zX@5j`%K*CmP?wi))m~fSX+gS1l`UH~W9k7dnZd1Jdeq zqaC7XH_z0qOWeKXk?Hf zC3_Izm#8qTHD`wNk3wFOjpW~xGWoSm0KMz|)pw6TDq!ub4xKdT_#p>ay7!xma1J`T z9&?qkH2b{36t#V0DeUPPqTdY@ z%T)O@<>CKn1^45ESjEr(gHUw%{KDr7>PvCDOfj1N>Hl#CnE#=2?|=UO|NGy+R{x&~ zgm~4aE2{-`(mv!4^?uJtDdne}jgR)90Xf2AexqmhW!DuNV81Oy{n96;*Ry z<<0H*{j%$Ff!e4bH5B=v>P!{dG38^6cFJ+1~o^M;4ge4yNNFU0^q0ZnHScRx%|_w_pwWM7n>m0Vc*U@8qbwy ztm4$dk$WvcR>*glGOFE;_l(eyi9@o3&>Lv{>NqM_ToePyzjSg6+y589jikLd&RAOA zflO3Vki8D~|GWPIF}Ew&9KH)3kO!js17@lqGSi_YObs%xCbSx7>mLR2Wk`Wcz^y>j z)$-Y)4~Tr;_7sN_1+U+*UTFyv)$awiOgUhe{ISj1*ek(Blm{B}F%A$rRa`_-ZU&6F*T2Uf5og z?0x*{QLq5?^K|x1-ru6EDpSPyhkUj^>@U7HzoRNEi^fQ{C5$K$sjofGW!Z9#Oa&8v&bGGtn;q&%Lnze^x99b_T^91URi?8 zPPVlrZyZnXE%VQXi{>|D?cRF8UL`%7@)TRh(5Rfsl)rHs8^^V?GcUPyDk4|3QZ{rIfe}J{JzXk@_-n>e7|43CcX>;B~TYQ9S zOyvaY_owdE-rCZ;U>R7ZR#|9$E6?L{y)hut-fg{kh_R~<<`#X3UXvfT9g?9qj2d`j z-o>1+lQpbZLgM3eHrw%{$cDuc>)nTt5UFZ6DOKGA3vv5kOE(XE5zuMf+&0p+6P32k zsdX8Tg4iSjpFA5=hlk1q#3S=eu-hBlEMA+ue7)D5-7Yc-ks}kteM;m)`Oi&#BCWW( zn(9!7(BD3|AY$HMl=pRWE~wMjj3G%Gt0A35g~_=)){b>~IZ-Gfh9Cvb=R9WySi=JA zBy$H$`#BQ$;;QQpsYvIeDlpmT*)|V%=9FVEF!Lj}NemN$HWADB# zs3Ty8sO`kyRK# z8+_#pgAw{wbJ!_2SkI_%h&azGTKyOzExprF#v0U?12(oW{pg=%1%Lq<+i%LnqU|G)) z_ST$y>ms<7wJq$Lahn1Itb92H_B@;nCFYIPgjcNsjA?6_!jg26#5yo}}u|lv^ zfWu=6I@9ORz76Q;1B_j_nenZ*CrI(=&^D?4F7BV?kYAk)q35K~IZ#!!e*ynvOh##0 zn?8?u6GYGvvk*vWvP`MwV-O8EZ|)|WNBLf_9~Z4ZnfCw{epRwOx0awkMOUghEC)5F z4bcd8*&8tJq;(R&{F7j${r0Ekw;OJlIb&!igR~;^9SCPH4L|j<-RpK!v5Y*!yb2p8 zXD@l71}Xl0lv z6JFfz*9IKS;vMRsDngC#i zQ^KVJz9G~uAxmO)g9g(A zzUX!W$Q0!f8khs-O1db#z)4a;v1ZCOs9suCh+JrTI#VPE)O?Cd#tg`eYORk2rN1N2 z@AiH`6y(n*2JK66^)lGo>FhE#b7WNuqy1Rbji$~!5mE+iR7I(4%s#;vKbiU_x#=V) zj>2!txA$m#lw2wvl_Miq+EI^MO9t@{qTg`5NxL8a1sJ~6{BZr|AHl92*r!`O0D9v< z;NSSpTmPtnM}j-(!|}KO49EW&!v7}^My>aMM)HO;24(-Z>*`0&bGNw6{{={W_}``D zWb{t~Y?oO2K?Dp%z|tb&$OwL%^eafjW$Ev8Dfnc@jBfsWw(~nyvBkUKL;DZ#1%D@6|9T}o)kA$+{03R-b-S8NHyD_vvd2;MP5$WT zPF?=zGm!`Q%2#D&{c~&qp|^V?zrd6URgqzluKxgsU;xaAipR-%6owh|-ffER4G`() z$|$-OwdGA~KG(ftfXG=_oV-$0rL``PH|bdqtskuMy^VPP=>8IM+K&@%tRmQ`bc|?w zq6-vPRQ)7`rFc>1hk+nXK@^~jOS)1)^3dw4WgzF-8&-VUWJ6zgDMhuS%l?v%A9So7 zW#0#s9M3aQd=e*E@A5ZfG0P8P)4^_ToF|;Oj?Q5yfKC)LR7tx&pnQ>K1uc%BP}!L2 z{5Ye2%=cSbKY!~2~E;Mqew$(-jBY$OCI4gC~i&!jAU@&p(=Wv8Z$%HF%5sxxZ` z-ml5ZKjCg{A8owWl09d|9b7fgBX0o|w-bFC2Daqw)*0%IB9g=Phia?O?UV=z;(O|^ zi=_sreJqKw47*NR*XN^zXna1?XjcaDL{J!FwNA;J+aU-H<2gPLS!kk~uyxn1B=sw( zoux%9F#j{K{`-R9Ik`}W|l z37gFs#PUw}qhJd*pyvnEMUQi&(y(8MI^UNKd1^F_6v;XzO_{$5E{J=bi?WJ;o@B#4 z66R8bz5Q-&r^U*<^c0QWTKCOSXwTbt?@|y5*)XeVAopyFqECf;v|+mA+1_ z$jJ`)T78b>N6!olfJKIc!$P4p1K>qM%Xjh2&~vqEPijr*94@sBQ$6)vsU;~uX^>b2 zbSAzH4bucEjt+o1a#^BJY(1gi>X%PuSfBl!%!qb}>PQ-X-#^1{cgurj$+ZnxesE5z zTFclbx{Fgo{vzi%+43pbnLr@kw z8%^1O$n)`l72>&u)ivDk(i&I(BI*?82Qygcyb>{tw-Ro@WNFoAUvS`kOUp^N=6QpEhOFCCUEY+BYF~UC{!&0?hE{0#KxNil5W6)OqVry|Nj4@ml#4FQr-=dS~PNqkcHll6--&fC|r0Y`ji_g6Y5>!-4Z_9cT zZ6wpT4fo5$EWD7u3N&$|4>BNjW*f|_AOgWS8rImw2}&tm=Z-c>i|dc=Z%x7+-u`3Y z$AtOvJUA^eC%C%(3)3(g-KG7}VXCDv=-jwCeQ)}+-87;e^TB)R1w{zLEv3UEolFj} z96yqJ`b`v(W`@YKD}9T<##?#0<6+%}1{F;NZs_KO00aeL!;ovyKSZwq6Bg_?eV(+l zYuB&tN4mB>ky66k39YStV-vTN#GUVsfq4w$17r!KcW~Cm*Hq(+EH7*h1e-r!23Bdf zaR;^WU$mjBVTfV%)PYh&(i01MOgy|79;k)t3tO#bml1`rYoQh%K6mYSu&aL|^)pIj zx4%-1wJhSAJ@eA71MImO$}iZk<1o?^aQ{MnwPZ_sd=Y~s;Bh$c!WXA}%`K3OHj4Ge zBuS4|tKNSC>-7Z9Y9?`peKJL9ZxOD}d)5gU6@$1LrQ#yG5yY>249v+_F*nod{a-`G z;_aC)au{ECD&XWuxgyUX^b8d!bJkCFzD~*ga{yRS^AFa0+fEG^Q*x?^)@PxgW`gVD zi?Vq~I9_&m2Quo;og)kzVtQgUP5dY2$kyyK+D5Lem1CU@TdEFL049yWYIiIVY>_in zJMTwLPq0-;y3>7-@zOJBdQs!(X#ig-Pe!U`vuFT4KHY^kTmtwuGZrYbKm=ron`Y01 zr+*okYK#UlwcWv8d|uXHflR*bveRT9*K$xVX<8m~BUm{&--2FX9A^pE^dE=RGMJ1X z1IXdEkK5$d`uL+{#1IFHv`ychH4N`Ks_$aY5^kkp0EPPgy8~|1G%aJFZFFS^bf=DF z6)aF%O>JZ4k`OH!1&$}t+3o)H`$^~vwSmJOQ0Qm=`iF=A6=p;njr^9oaVwo}%h{Ot zk22?fy)x(j>(3OtrFU8PYU0;9orE9%wus-kasPOv^mLm`_0|6+${21V1;hnQvEtiv zBJBkq;1fT9C&2hr&7;Qe)V&xlO!Zca;}-q_y)5t_MFZzNS`11cU^Q zglR4t@U`x5FL`E$AXLnW-LULHeWa%mO<||mNGRo$6e{6dz=}gTun0N8S%d`cBP)f* zS<@5VK}57JkYrghq(q`XZgz#I4*Kh}pdi-SB0wN33##MKK?wBs3yk~T9CKStLwN%) zTIPdO;#j|!YICg^1kr2VrH@a;F|OQHa_eD84JaPOYc2v5)LgxHM6jaXD4Vyb#_z7% zODrYR@Hw?u!*wp#(rgvVbLy9UHfmRtXVtNrc}BLV{3KLOz; zAZpa57f>4@z$9E&kmIH4H$o?iA5s1m8i0)iut5M1Ucj@*&Jia0K2@KGk0^6lC8%FE z2>Iy^57WEd2vx~}9s11w3ve5(<6@5L2{E@<;Nq8Yrz%{e12L(trl8!Z3*2{`{U2na z7o9j8+rC{WfpnT9^=dS?O@lH0?gefFQSoE#NO4-fm$zhwE{brRYhtHLg3_z2l3r6Y z$+0$&?(YqKp?D1TQgK+9ny3XroT`*%bi}0BDQ68!clJ%I5%9g|;`>YMANa$<=e;$@ zMzV7#`8}Ct-?px%KcBna1bz?ha_0_CTy!dPTCNYw;fIi;X98^e++_i+nm?1mPPSSv zA5ubFB#qwzM}7{w$n>)1bj_^HJ|5JGH!@{OoCW(6hutf<)!mO%y8^2_d09y@_RmU> zFMnuc={2A$Xc!&&Hq+D9+E-Az^g$c&&dT%dp{t_s#T$H1D zO;#!o5vAX|=Lmy=`2$NQj)tTp37s5ROp3c?45ALy9*^9q@9sJ4w^x<)sKc09SD>Hzt=wrED?>1wFuqL<7z>I_$K@uV;Yid4Ql!zJ`j;Wnc`h3q{0fxKf`f_!Uxhb*p~N zE_|Bf#mITr?@jN3363fC>Yx)ZVir~4!3cyK8{Hj(q({AWtJ!&%( zm-{|gXB+0u0q~+u*f{y7;dy!_a|I$vM&h)_%OD{I2GQg_h=Jpet~(W3PChrwwTL|D zd(id@LHdmnqrVF$*e^PEj49wWD;Al|K@+baM$&B2T#BC&DSKWRNj$TQR6+{G}br(Jz4b6WsNMV|QwiQHt&!lU>fggXt6=O8|l(~~Y1 ze7AXR`J#f_^>#6h2lREIln*PH*O+!W-`r}0zcf^Ss3F2HDcY8+n|?|X`EwAIJMD0J zoX^APWKqd(7DSyC0kue;cln98P7XC-0)R%e*3D}(TZEf>LY$&vYBh(d&u6V7>cpq1 z*{eX2tJ>A)21zV&j-}gB(quJzjXOu#B^rI_-ef4x@JXfXRt_qtf(3{43}h5qmaTS3 z!`HJ4;bv4QeA?xVO-W;*!he%FE9_ayauE2bK18%8;;BAZA4sHp!YfAvN!fErH*x7- zN}aijdv&r1yr){K?(}fKt(tX%Q^|?!*)Jv&a7mvNNozYe!(1jcu04P$QcW`-~T*?HL;eA41QCGFGb7Q_o&@>R2h(&QQU- zu-A3!5Y)OVJ9>saT+d&NV49lw6dXx;)|@ZHmgkh>M|{pI3h$JFa|%nJP+|!mu2Q8} zG4Jm5sF9w9-+qzBH60?}V#Mi5Rr*8cDM_Ee9=JQKceA||+sN9UMz@bD8?I^eG z$x?s$7aAe~m=O?WT%YG8eVyvfO!bT1$JfI>Jd256R^) zc_E&Jh+WFmIZI3I{b?W%>iRMDg1S>Hkpyde@_MKAlP|I7=^0~BsQn15y3=TaK~s4_ z2OrDu+uu9z4!QP~Rgy*~T(#J4B{B^PZcsX7cEpJ@ma<7R+`@BQIzRZ;RZHdT78c1D?X0B9XyEmUk=v!yZ&yX+F2^iZga8d zFA86oi4KX{e}5j+yvjGpiegaKfvwn^muZxKf}yW&F(HzE_mYrgxqMq)htS&r57Mgr zGxkqGxtZ!0h@KdwWJx@BBw1g0?7C|LaMn6@DxqtRu)e+S+Tr@hqLINX4xT(-MCc6f zcnbO*UHnADg`@S#m5TPzW?Jqijb|4$b}|_x3LdELOkHyDC>EIQO!M~OQ4e|7ddJsQ?Uih&T)|AVQ^OM=mDyiPFD%;0Z=1UM z6JlwA*%a(_D!rS`*WDeU-BdKx;8q~U*l3il;V;eDI`DGc<1f|Y<~<|f4(ZX6eTpY( zW4cGyb?V(fhzu4A7V2@+`wX@nKCwyzBs(gk&emCWy$|kOq|~BmoqkA=s>k;#uPfr_ z1Q~G7Uh(@=PR^w(&ugX?Hp#K@;zv>#42`+yoEAaQ{RIC|-L91BDZQZM`H~TYd30=S z(Q+@2crIS!*|4a6F1cBmw2%dR7GFd5b}Q}<;h?4&X`Nv^R~E$-tIpA~ZXO1m;t9;~f*DCwND&zm3uaCi$a zS3B4|zK$FyROBn$T;{}G+uqtGo86s^P*@=^_8wU|i~w2tF2e(2w0se91Wzf6mbjxU z@ievj)cb~o=R!uxw8=65+_u__Vhz@LZTCc^7q7M<5?*u<8r2u%;#QDx4hSP={kE4; zduijwwXYm38<_RIM>DOHsBGYX$#}fhKi76HM}d3)WGIzah0B3-8VQGsMq6vAH=VsV z>O$?fr6C&Pei3#OY0ROo(Ovs~a0qcSIJar3{|dAIqD+UTZyRp(LZ3p1(JM?#zTn<@ z>ol^~GL3BgN8y=QwZ`=0HLdD%ez=P{!KRRl60#l2ZXwT2B=XnAo)R}Sr68Wu!VZ9F(fjHyT=Sa&3JN~GFawNSQ{R`S6-NCVALmyNv(i}CW zCtp`Rxz{D?W6pxglY_+HTk;A)c7F>rMwz$7-Ib9)`(e1j_xn6i~7m$msgf|1lbi;8-s91KB9BoY;?IzD}` zr$=r1y2%Bw>xp%F&!`&VB(rDgu+(&xYGs2+xoOv{5Uuay0yLiB+Rn1<%P2rMNz^mrY z>l|4xG7#x+vhMzY{eF%D0x%Mz0@nF^45^eX6GT71o@!xPd$eCk8N^;^Rd){AR_w#I z%3-LjUddC9x)-vJv>JLJV4|aIpD<@6j>ewCQ|t#G%1Ji6ZG7ru+G;`R_d)W`DMVZtfby>0O$e?22L?=@@`;3h zlzVd#iw&z|h@zh{%JyKc_o)L-p#d9x?n@!XQCqrnMWrtN)c0SC%CX?(7vT!w>~H9( z_Qro3fhK}`&VRn?__XCMo%3T+`ai>Y`OnY%|C8ziZQn!b#{LmgX!B$h8WsJ=mh8y$=wiVO#Q2eeqkD1%-*7M7f@f zYteAI9F+lF#rC7e>@x@X)laCWCy#E>X+e>_>I`BnDf0A+_!-Ae?rT7fY zqx>D-!>3U#i}&vd!BRy&HQ`>&I3FinyCOc(bcw92rjTT?Lw3IN9kUQag6FAQry1w- z^kS7}{hHCDl+UtDtd$v8C#oBa1ptfnxh#!%Rz4RFwR~xOYzL32jfu2zcaZ+7IahNF zV-neV9cdWMY;Ccu+@*rSEh0AZe zlx&;jnkuWdcR0Y_tXN2(-W5=xM2vS^Q#fC~Q9E_o8}4!q;OHukxhBWQfKnLSR(G8F zX}u(RsY)vv;i&iCR3XbCgUcPQCIEnkTnr5hj$IEn!LTAd&DzyaN|H9Gxu~Vu!7(Rz zY$<1S{)3>bIqEK3gHQdPdz;Vo0tsXGL^RJO)<0XI%PSD^eU#9h#!0?z8#lDOvbqxw ztwLI&&DER>utCdPm#lLdjhK!2S8&((l(%9{Cu;xd|F= z;zSY19=Ynsn88V0R#&X24LlMd)Q(p&^<}2a${Uk84NVOr;g7KHR5@mdb^cp=oKS6e zjqpy893G^jmc|z99~36+fH~G^o;%uU%NOdX53SXSbm#9_PF7#l z4fOfF*kAj0rE0UdPLB#be z^s!7=M)V1q^zxaj1*nUxey0qMNp86&JP?Wd`;KE{$%Bj^A)d)4gKniqK7QoER10jOw)n`my5jFKWpLdrAG*8~l^oVm)$K8V(kjV8d zj3DAoKY*RsEG1ykd+JV{JaKWhs@VV=JVGVj=~-0y3Xlw3B$&;o{H~!OO zhGoZ*@Y$dl(Jy^;tdYQ5S@H2f@_WF?34w9_@^dP=*)sB29EgP zFo65Vi9OehxS32@%6{3V%iEW2|4S!_^3A?3$tgYX0}%q>RdV)wm1BQRrA1{!@E66V zSuf#BqorBhstxySZB>M?S0wr^A;#2UTA1?0#gU2?@O6=RbArxGDSeOXYHYZqG6}@> z@alX)v-os!#AQ?GLT|Mt0CP2q1&KgKudzT0eChkrzMF`7t|WUwOKo2xOE#cn+tpGd zSW5Xm4a?MDFP>TrmoC;fLu8A7Zx*be%yH6x7rU+BBwp2~%M60pecbp-t9o9*Gjtco z<4)milWj3o>*P$WUnQx(-ipMaKjzvv_xR9hx%C-a_>E?=yOt9fTG^ow%nWUoEj>Df zYT=?8P52qG*T_q)BiE)~#d%P7_SbY4p9$=FZ8(GrxvD!YMZO{ix#@H(xG)~AheD0F z?h?<}Z*+NTtOYWTswA?Q-5-td(G(;ZB-D>aS|i=9ufZzzwFo19S6YVT|Luz;U-2Wu zd-eG{>xWQvv3i~gNf;I3ZbJIW;W_LDH-7CEc_CzgxNz53s0#@?8aw2HQtkMmyAg-1^yuF`KRs68GZ(DjG=!ip@xNvC@- zyih^}m%3N3-~7e1C?=D{P0WqYUtG6b4WkkKQS8qeGk+TUN+y+=yuA0~!Y4fB$!lk( z9S|U%?Oj8Evv|Qftg?s(Cw8mBv*?O;(>unbtU^C03DDr#2UkrZLZVQF;R`(fW(lN9 zv9x!5Ab9Y3E#D}#N*ucW6hs=80`diS+B2tcBJmT^gH10gvFXix+>S;dcL1Ey0MtSB|fy~Mc>1{8eq zkZdia7*90Q`C{kTgBGLlshLZ8Dx@Tw5ab7&(ZA0KrTn?^j~X&id|?1Q4)tCMOP;ah(;!tZ>=#nkb_(j4>9{*eOGgW8y5DiI76h) z8e&7@*c*?S0I4B}E+;11-? z)q~?BEnxM6sCZ-&|CI*>ZQ}iF7l6@moJJfAi~k#Y@7301+r9e&B2twWItrl(p-2lI zM4GhFQh)$TjgZirf~X*Z5)diULQz^E389Cs(tA)!LRX{&5kUn}dDO?1_rK;Bm~&$t z$2#V_k!0uIxvyNwJ+5<{<98mkKNlx9HF&+uh&r#r$3Lu&sq|B@o9Imm2Np>pq2bu) z^NwP3ZO}(2#DDVs{W2#T7SK6Kn{>)0okiBmuGkG%O8#Xj+ig}{JaV<`nMNzia|wu7 z4vO9AA3^8ud={R!4w;B2xNk#51jf-^?VDf8f5d)gxGz`!WvRXkCRa;==9(U>-T2p- znM%j__G1X&ZQ{dt{iVTm>APL<;?=8C#lA>2>I>f$QwE^(Z0IX)RgRbos*G03l0KRB zZ~_vrcP4_At~zARn^Io(J(@eW0(Yu7r(>%L({v`8JkcCFFIk4r?J8 zMZ978;G8u@5L*;Q?TH|bv$gK(p_@Tekz#koV3!IvL%nS%KwDtg6c}x@d$duTrLN@H z6}9Z(MF#yVRo@2^K9wwQ1)d3l=WQ;RdX~-hc@4LiXPN4 z32L6ZHVj?G>uR$JVAds{p^5c|^Z9-pc{rW`7+&ER-%=+XWZCBOOrJO|5J!rRsjD1A z-@N!V2M7Ww%LkaC$Mdj_z=zLC>=IR$YZ%nR^$%PPk>s>sS72JQ`{(-OWG3j+>PA9~ zl6a%q{q2g+NIG(}Dha~A_$~N*(V+_P#vt51_MV{z5B$LjvoQ`4S{X+wRnS|$f7(UJ zbT9Xz?uU5Zc04|BT;V3@yax~K8x`grNjwZoh{_}*vVR}@DYCf^>`j$aStmw$&K?+!Y#=&Aht`NmspKwF`^-L) zI0tKXT2AUR86QG5w;(M%vt9f1K(xLT48- z^1xh~%~S#DQCxTP<6^umLT4Gh<%Dhc$${^Xe5@(bVn_F8L}7xg4iSM{i8!v1_rC+5 z{j|PxZ~ekQ5)1hY^*?Ev?=hSI?);lZ*`h5y{OJG+V`6K<%>F9~{@*PK{-1pRZk@kP z{v#qH%CesI{$-hYd`S0pX&2kA)=x=cI6qukIn47_BV}Oi_&Rs{9`s&P6 zB7XI__;e8jCNgQaDPtR@3f!H~r8n}^pdM_zLiWAqt*oNMwhxtMt;aZ$bV3e{YsbNH zx054!rRZJ-En10fX8WQ4HqAB-xKMUK{lpf_-$zW$dB3`eWwDRH9FN!v!M;ucXyvrGncB`>r)x9-PH2e~T4OJ(#8@QBl^?28`CB zb_X!^><>WLu4TmbL$rmKtho~v`lYR3OEA)zs(6Jhd-D4csnaZ;tV_95JRFU5k4zBn zM)g$bSTCN{#&SeM2l~6AGx>5mWUuaAP25;<2Gg;Qyk5%efp@@79z**g;)wxy2E!WP zs9Nz!I$K6_*zqKJX0ZCKDF|o2@^O$Y1y`4%syCh1MYS`9#6*f5k6-q-_ z6U$Z9VT^Y5E!Qo??cv_9-kREhw>GIZUla3DDp98ms@4v(+73{D<7YRM9n)r7XDM@z zfuTc@ZoaL&`T*lWYjbn+jmq0V$4F0VFr^#*phZxfg;8jLFzDQOjKt1SI4+=H{u9}8 zQWObaCFldxulQ}Hd=|OY5!1u z*6Uj3&FU>9k$`TMX^iQS%bf?x#JqQ;Qz{D^ROlRp%>qRPo>Hw z+X$!DFWI!u%4TlSZUL@^filKMAkXRfWj(TI6A&S7;|KgeYysV5k4cKYaf=21$3VCL zbqxgD<-OuN4DaFr2{`pWjo-EctF3ia4tp`cDlo*Z)dL@Yz&-bNUw8;VL7eGdcop(Y zrn>Vz+TC5%gLq`8aAGB+>JB#ibX*r2EsFGfxK;{nYCw%e9UF7y!QVZkdbUN33V%9% zd+aOKIqTD?hSv}B?RB;VHT`hPeM3~T*FlKL$XLoPA zf+&$b^?;6TvH}<0>MxoDOh|q~-&%=9-0~!wuC<>JYMQxKdoVs6-JLpP%gyKrd|~-0 z^LxWZcsF(2=T_yQ`q1z*aNzbbB`+``SHtVbEo_m9D!b1J+fw=i^Ta3`w)ueU=>ZqzXv*om8eao33A;ciY2Oe2+6gGG zz`V}I-AD+>b>g)%=Q`c91sOFk9{8*epWRWK6cms)vAF9j5;EIXdSpJ)b~@GT2O%%G zE}cEnVHJ_=-sTcHY!>x6$9zh8D@}OV@3!}qP2W6k-4ddTk4OVBWOJLK+E6Ycu6Q45 zViG9WU}a{nvc*j9B0V<~qYtcAS7kyKhxcydr6KmmZ@>PiQ_0AaDRF!@ zK*AfJH$AooZxGvBpXX7Z87Hx-uWr#jMn36t z)QV(zM(!dcY&INpl^;#8GJ&catc8pk^}IXFsqZE>iugKNPGPH0Y5TxlYoH5gr%W^f z@{dQ+>C!nPm}`3h_R}grbMeEr0mhWzDY5ao29G%A9FiX)^NqZE!W+@^N$u!1dj0EC z1f{0yE-hv$u?v{gl^#Ntk0d6y?W z5nE!~?}Xo^J3Pm)9(nw*N!Tb>7|oNF|LOuESjqGm)8y>~%$-z^+yu8m_S_4F4#N24 zZ?Z??Dj_%X6v-YhIjR2jn^LqDTfL@XBhHU9nd9fJoO?SAjNyAcPqD_Dbq5V{QIS|x zd*+dF)CKV6@yY1#BWAIYaZT^qyRT=0zaXlf{IB>M^X?CciqKW2Ot+ZyU(V3~_ne{s zzpr^??9W?^UvzPo|7W)63-QOfhtwyhnTAegHEYc@x4c&`-$4!nE3{6ok_~OyMsZWQ zQTxeW1>mkQYwaVPpYpl(XF*!Ma|8eWX4ae)7}5NPI(sWa%E^v7_g5l!-Jug6k41QL zi!D$@hj^XYEtsGkM1-eMRdGp!OrWsTsgyNO+NmHmq(+pQyl#FkT5}WpgEZVJ)1^4( z0+6cO9{BNTw9SFpXZKfVgOMOT%kypZXDk)YDzoi}(u#v)d&I%@8Z)T;G@A}|6-Py< z6D5jb=Le%ra?epyq9+d>CyE+T~;?NC^ze12Gb?!8B(tToAjPk&SW=P@E z`_|wQYMMfdpvQdz0r0xBpt2Ly-!2hK-Qp5Ymqf1R*93=%UjawiR9Ws@^$lQ$DyZ_0 zx251ea;)R)f8Cd^>l7H*-2&DTQ4Tb^nGo2)^n6D0UGWFNXDaby?r_xu(wo#n8`Gzp zqxN}xi#rt!Ew5cX{CUc0qwlV^yAG5D*V#(0Gsl3IZ~aG^M1(8(gU$J@uU`AM6eeDR zW0`5oHt8c{eqGmH^q*}Ck>W~L>`n&Ra*6{~Z)o7+&4YA3&`X`p`K1f20VBb@F(TJI z_}nDCNttsJvHV*S)3+<9tb=)Rac>i5q>NHwT4k%W5AqYv?BOCa@Qi&@tHz2)bUUAm zoI(^8L4L;rm-9Y0{N0NP;p40pC&-`oA#(mCMT|3RSz&u7@bQvMsZA#6&w|WGQ^Vq+ z3X#3Fv^z81mWcOBYj{^ig!uLpB&>{*c{}8lsF^jrDCi7 z{!A8SdV4ud=W1dP1|jKFRpV;_5IXO9y`;t|9MhBrY+GIw*>%LpN{QDo)JLpqDesq0 z`zUn1Lv{#-M|I%``7i23v-(ZQ5N6waLiCBv??nOfkY5vS;!$7!vc!7Te0w;(_qJPQ z{Bo>UL51uQLDxvSJX!C+4YzCg>>_fbP5jBz-UYZ+mtESZ-#u+WapG1vCHNVjEapU~ zK?l338i~ti28UebpX7cM!zMKo^~t(%xgK<2S99JT{$W~_BRlg?XZHo0S#hZaOZ84LdwmK#vWzd`aGNqmbQI%L@ndce0tV2jI; z!#2v_A}h^n(d_G$YkjokpD5P0J|H<)Mbl_z8MxC?X_XNw<&nj$o=k?xt@>eSCYw)y zRIB@j(%l-!(h#Mn)CH*ZRnBh;UbOlif~|_^oP zsNX3)sb-M8*Q9$TIEqcEOM%7<+V@O%u)!<8g9=uc`eSSi^?!MK37kf zcf9~YW|hf%^+m%*tbmpqv6-C z7czP44<2{o2tYxaaX>Rc!m!0=)5tY1f+`p3asy|sfZBMDIw1_rL+C{!a>lTmm!qm5 zMM65S=Mp%pxN~V0VgpC;{i;bgUeZcKHn-Ax5V{4v9>?dLVR-=X-c%soa{O32n1l0E z(jS0xodH+rk65|-<~bDbM_{Z?5p^>Cq&LwXV|97f&%l_aQURF%lT6y=UCxI1GJahl zUS)~ZJlvh z+fK4O6R19)_4I&E=w-e$;}Hz^OiZ}%3>)q@HaHAzDM?7rpSDtZCIv4@k*h&y@UJQ7 z_ScNn6@NkBQ(}cP9Ab#_t)VgShv2#0j@#|qr2FyGl5*g<*YMnn;)+GZA0ssaq~;X$ z#Tqb<^p16r2JBn+9j{#F+^Jo%!{hFP3v2D5;gzTIm+S}vY8Q3t?RAW2MY1)~as4f% zvEg&V?v}e;ZzUS2b@94CzX4n?DjL%aR;CmnMl9+TJ2qAxrxF+~zjFjuL93!-UJ`L^ zw;B%FH{bf;VAKrbX_+LNeWP(qRTKqRc zXjq)n&l!GBUD(&9%maeI)$XNaJ8Hq_9nvLEwmOK|nya?8QJ?=XtQ#t$kMDaZ1OLk+ zBg{w^FX$VJuP$?F9EL=(-@xAK^<7Xb!tVOnP)%T-N3O3gKdt5-E&tgpr!#cfx%)^T z;Ao)FD1Q#2=TxY?fhukeq8i;ji@o5I@JmXFhTOOE$*0b?ou*29YD&d}&3`!aPqw5} zbm7aC$ELg6Z-W;rM=Byj7RUF!&Jq$9)Lrgi$n#r@W6!_UL(bXRlUjFpd4J$lGT#4@ zt%d9ld-isTxW;>sB!)jF@GqS@H_%1z(%Wa`QH=zb!ge!f4y5-421jVk{HTGPVMC0t zoFW0}&?!8DLA-O&VnGk*-)F_O>I^KMHG%@hB<%){@gvUy48XROK?Z=Th4~6R#;X)= z({xKux!l9trt=LAb#OdCk|=v%ziDFe`b*iT`2T6PW3EBG|FWb@m9c%e@cr^PS+CQv zEsiuKfZw|aUtt-CkY<$B66ccG-xRE43_ms;TgMRg+^Pr-1KC8%KUM65xsWsfdn%R&Gj;l`+ z@*+$exq7}6&SndCKidL#^TOh1i_S^81m99%K@}Vb{sFlU81U9}k?J3gZxEL~o8-Xc z?F~?Y_OpC0GBwAcND-C~Ia>U>rg~>0)Kj#}zop8b!M$mX@qGh5 z&<$I6iO^ow)uhQ{l=CXcHJ~%Vn)J%GF<~#>PO{foE0s|teU^>(vZ_%QxUG7nMG7{E zm$09nSpX;#wo_nq90c0R>emCS?I4uy-0|r`Na0JlPg58l7TTZgll5m0kRescQSPfg7LyH2yRj@@EKM8F*L8tS?vP4@ny~Fxs z%MkZ<1L}OPh4pa7HLiXSp{kHC=}Uco7s6k84oBjsUFWC4<6x?2k{E)++t_~Na4 z%n?Z=1&Nya-+@8)z*TVhAC1 zErJ-tK6|_!f@`X(W#=tk@wua$P9uUYcgEj58a|h2D z3~R+3{3MK1}Eot{F1q-JeEXU<;+Ra zaVHgPuQ5Jl{vhj%?Q?W~jeX@ja(lrSt!G}lT+L^MIzgv?D@ufSX3^>=N2cak+JASF z3eUA0-sc=+C8)pK8fNQqyu!GTIUE1G=XvpG}P zrufu-+}al3+=B~dP1!JWtmZr> zCBNEmdxCy~d@|Lu_SvG;BlJ6d#C2vx^x+<|o5%(8YYy=Y95+2~cekUID2rGccaVEkYLgP?N{ zj1hGAya-l(r)JCMM|!I((I%gsW@UMm8E*&$OP)nrT3Rt@g;ZdczvufX++UREYR4_l zU~@x4h~_FQihPFADYimB^U*IM8Zy~m;a>Ao(a!orfiP+d25zS8UO;W5x4GUg!UG@A z{vIKmoP92>M}ftyU}O6f7vRQYv4t%Hlk5~?%*+tdKk}N>!-93* zXRDx%S+Nt%-j$6H8C}(38CR~<#*6Pm9xxh*lCa|iH(oSS-~}tvr01G!hH$34^sC+b z=(1d?^ZBQ)^iV<|eCF9l4sKdQM#@iygPlZ-;JT0UU`dr%sO}Fo$u@8J4Pk31TNb!r&Tk~oq64{cnYkiRyH#X?;j5)F zYxD=XS00sP;9xH0PG%s@}yy?&dNflO0w1S;Adjw7l!jzqEjFv z&MP4~h%j}L3r}4;#gDq|8$;;TdXe!yg4!N)eSw){D$0S8nqD}LEbQ&NzCBpxhtKrZ z+9S@7!D=~^S#7)@$l3gS_QsPEu{G_P8Hse$T21Hp=Q$nP>5?gKp{L>v>UnNxjeASC zWk@%lnJrcwpI2PAq54VBUYVM)kexo$ zqR>y#r1!pgkj^(tA$}J?(XuM;ei9 zB|}HTUmYrs-ap&*gZZOiv637+Dx|7gAQ|DE2i z{6F(${@?6pRar;`{AB^=Kl#)4Kh6cKYlG`CuOEb*`@_s?n0f4h_-U85V;|7@R0#TU z+Bi!&zPaK zuX&r;`$p?#gd0FGQ=(BSbJmZ%$(CawFV0b?6CyuGK8weD#`xyh`0dUDd~v&>efd_= zUx)aAQz(9)L~#Y$W=dJPo=HN0XR$F8H21Y|vDaYUpaPFWn*6yOSkKmc^GicmW;5tO zr(h=ksYFne6SxZX=)e$_EofJ>$wpqhSnzareMJn2Z*sPl$ey^#Qk($$vd@x0Po=f_ zv2BVEM`@!kNspV|;wYl-ZnL8ER;2{s^bZpp5O*63d{C#j=%YZzRx5%VHN;!i4Orp+ zL&A;cY^wr#?)ONpk~tRpe#sN7E~23PfcSC91R~U`&gYnoj|xuevqetzSTF(*^()Sa z6t8}UtUTP(if=IkP-a}i1&)gNxaG4^)Fl7xZks^vKPk$?_Tf>15fEhOoL`t5jtJy8 z1-VO07fZ5|9;#}(7j($8XnV$~wv-CFjvy&{6JlU_q9TKM9R<{#|9+{LkER-EpW&da zPi)#g-|exEcz_!YGK+Q{fp;p5pi{g zwK*oNGZ)VC!4k2?F&cT6I6spp#S;j!tEj;Wm5X$ElVg4& zNw`OY*O{513o7lN6%c^w&>bK2QDPH4HeH^Vo;s44f{hb@60Ii>xAHzvRu@kxo!h?~ zCszGN`s}mwi>aPR8fYW%5P-MwA()T1gMq5d-kMiddX2oTVWMIHtaMLr(RBOd`{x6= zm~|@2%_^ARiWdVMuZ_rAFu7G8(?t%oejn!xaqYOGG)}bC8ZI2?NA{e#1Y=!Z1XQ5U zUI8Pt1Jgax^Prnja|er|8_(1Gq;PR;@ijy8hya;!gMVo0+q|3{CACAAC%(fX5~^bx zYbu5GunrX|`uCoQoU8qSk#`V=*^WtFP*3Uci-q41me&d2$R>lH7iW8;Bug<@qJP8p z450-Sry2`0v~iC}3%)RBa}T;U2FJ+V2KW8*=8R2|XXsm6A$VlN8vScL<6NSf&RmY> zYqAk1xo%ZBkFCA!k%&8cT2sU3QKoIyGE$?hDP^HgidyK?q`#rrpK|q-_73A z@>P4%YjE2I(it${oZ_sH)oGbj*PY2RYV@0bzkj=sFtnqvaR^0kq<$`^PCTaO;@_y4f^z__I*Jy{J z*nbZ4z;Uo`vYQ$>*QqbED3o#&x|LqsC4 zjTj*?N{}Gy<#I`9T%L*$446BFz*8gE@~(JcQSpSQs3LxmMKrDVLX|Aq!`%O)bUpF))4qo$BdOjz$=T9`6J?CI%U+FmWXme~z_*VCKf_t)qC!%-6L$b@O} zqh)2EH-mvP`P$AzA6jnpYzT8c3}Mp&<9Jt|U)LNayd>MV3*jBlRpq93XMkHlrMP;Z zOUndKMooG$UQQF2$`_N?!MRSjL!tqn+@ zF>IsRR?!L7`#kSK?jqYh)L}zTd$9kqm%VC=K|}kK7I*ytW2%ScYHeJ(%Hm0Onh2z2CToDI6I!U>$=wW_`LttFQ}mYAJ+-{^nS17 zhRG{)Q%D!JdG2R6Gl)G#{yjrlfh^iM|H37^Kq-a!@YuU=jxy;7WtBBaOk!!n>)IfP>H(yG zeNz+Ql3>N1Gk8oC=GL<2EO9{lyoDxJgAFujS$C^?$zeuZ;saApRvZ0&<}ZsmV8ktQ zgN5T^lu2>u~#aQzEIpVa*OIUKXxlknHQpUI#q4I2 z4@ca#8_urDdnDjK#`H~bKh*<^A*f7_Yt8$3&FP!xwY~XT)dmtNyww)QA08y3AzC*uIj_ZuqKH|^vfg7d^|E7- zO8_wYQ%uRJKJ8stP2revq@(>dPY1Zn-1Q05Zj?d?WM`%-xZ|fDued+EPu)6Xb5pzMQ zXB)}=R9_ftuoNJWg(i6qF@x%i5FEw}h6r1Qv5RCK-jACB0Tc64M(0H;o2~g-(8q0( z)r?|=bDG)z9=6E@+-fm|d!yMh4(COp^4md9`edN&C3UsI{IN9;z04T($3=>ZCgJ znvNzf01yNIeEsrYacSDEP_zjD5z<{4o&%;*16$?}1#Six)~50X{um71@m_ysWKhJF~I z7Q5d>e9e)b`tqF_X_ATJP~Qb)oRC7n(#!84{h4G2Wl}cj4yer~T@pJ3v<2!vS^$&B z)^+(}qMj*mf~n{RX=IzZ{R!j_@rc($a=`Y4SgLi0xZZdTQ`y{pSg2pEZL86|QMdZVmJm=F%#^+o@j>Dcylf{pYAGWN zqxMV<87H+-X>(I2RPJ(+*EecRM~K|q;fqKQ-8cSxh_IS9j){?huGc3mlbY{6mz2|`o{#`6@97-YnAF>7RA-C?j5B8WQU`#^t#vj zJBkEZK~I|s$A2bivHZz75~OQrAitS@!(IEdt}dw^MPeW1hWzV0UFj>hPn)-X=j>re z?oS3bkbH=UAQxXrdMb{JE&pQj%#5}wrn>ve6lYQcpnxm$50ZTk)^ ze)OWw1O3R^Mx?d;FPbd}20_(y9#PH+UUy!EQ?CWR^rCxjm9PrevkOf*vCK!-`4`mr z-qUq}9iCHvkcK=mze>S?qNcXy9*~Luj-kpTN6Nr8I1zZfc?zhfrrl$_k zs>(JufOT&BvR(L#5>R!fP|Xze9M_Fc43Vr{rjxAIZ3oLZ7v5CSoeP=|cuG zH{~jduG%$obWiH`ZF+y(y3qSs)F#C}R(1uW(OUH7fEIAQhvU=MC`Br#eL&_`%MDB;)u^k064`m)%+8hRI&k z{f}OP(YSk$P?(%UA+O3cov0HBM_as{o2mc)lg|ixez0Mu9?Sj@d{BZ#YyBtFYm^yu z>oDJQ7z@90x+uBo%7p`r9<nbH37)+biSZ;?c;RghXY?d#*l$KxaJ1a-B%K- zl~XeGbk2I8r6VN<4i&qO>)v9sy5Zd^tz!AW$eu^w2&U+Pm(P=FY z;;4M6p=2d;N|{UBkFuX7La+!gj>gW~e8;j>#q3P&Ld{?Lu_BN|V|soWw=$zo18Xu< z>*Ep^N#8GR69|m}1nV~;N5NAdLmOCFe8{bzkKHFIzD!#OTO3w>6f-3MA z-?ba&M|R=-_!NE~u3#y~!RVe*WaU&?R>B+9z|7%5`|o69vHH`d_D91Qk9WMuxB|}m z_Pzlt#AAFtv506mKU{gD!X}PKjPTj3NyEAecD0z8z-1=RA)RBVAKjZAX*Ud@@ZXT) zp0F`)XLJWuKJH(`_R*GnOE_=glUiC&Tu>z#QZloIUG||rERS?oF}fOMYm6cY-kdgl zn?H>f3@eXu75g!^jO0J`D0XG*hjIGQxODd>ilvJ zouej2S!erIoEX)mw{s#lVYcbOt-!1jKgTfI<6SkPc2GitCs^6%Waf%nD21}j+419{ zqzFD}j)gMV#wzuN0vnBF|F~AMB{c1%omR3>|GJHy6;hm6?>Lp>^XHK_zELyB*a@#I)R3Dk&B(_5Gm}%rzfo!1ZvW{ke=<*j`Mn!H&G_IOm^x}ju zd`<7ib0Lpt&7!Z=kl;H1hw^Y4oyIT``f!QK>G+H3iI#IlLy|Az?kdo9J;sv0H%j2H zcFl`Pt>d1nObU71pB-b4OH5achr~N-&_cIPMPio*Evj)M&(j(9R7&hx|*2*lQ!Jr}?lC-3lk4VA%uF zZcBM&7?w$@l}zW&h4(}ra=Wo-^mM3$ZpW@TuQV`7E4tW%NgI+*U^NqY(3t@MUg^Cd z+&LIZ=&H1ICcHzGCbn&7gzJpEvs#VJh@(S;`x&V6v1{7@i^xXEm%kw>faIqZ#SwU&NY5+b>%pTL{^F;;EWu{ z(-0Y|RVI`_DUlYYJxfZ4j=D}-KW-YUYY_dB?>7|ybCVOKhvF)nrPo1y;Smk)-HnMr8PxBJKm*nEvFTzGy6$s;3e&M?AGi zik2ZnA^bStBwA_aMa=jl|Z zI>Xk-55aDV!|9hCvH{2S8fiu;inr;jwilMT3=; zHA>!rb!Lh$0NZ0u*frLVYa=BD?AmqBH4NBVuAyFLlOaaE)mihujC`JyLd_+mUoH6= zFPv-e6u)#cXAAWcZ|zBvjY}h=(*yIfu(o;%2+jE5PC<@B;(X{uo|||n4ZqP zg&pSe(7vEThIdPzFO2i699o^ARj&nV{Buv!Ydai$@9XZR_rHgIXh3%+Zei$uM{51= zwf~jo|FZ`GyXAuBiF&Ae@0cgK<=*KR;r$WeU&=BPPX6QO2EF3kxL$0V0kGEciGqaN z2En!6J0|D3cU12)pqlG*ZR724fzOb{$E`ARWY%APuU?3pnr#rc++yKRiA)R4dq$2C zLbL29YcF?+2be}OQSstSyU_55Was&(;=7WE1xe2kn0)_Omk}Y;}8c|M|Qx=O=~hs`dmtDn1>csurktVu+ySAKG*A?AoOHef+^@ zbf2|OP2H=rpZL$Be*}_%M6=3sqmk^f9};PjA2f!`S?WJDrYQB3Rf47_d%(JCBJ6g+ zF@WU+!W=Z8_lP-+1wp+X->S&Xu}Tl;#q`%_P;$3B&Z6aee?j*-+Q{FZGgpkmbyUgd zEV_XZG;^i*ET#^lAxa$uu=Ge2+NM4jIUK3A`#D_k zz4x_rg1pFE&+k34Ti8+1q~h>BmF{^~n^b`{k^)P4H=e7z`-`#2O= zlVB4?sSy5{fvt@xS$u^?@DzXbEc?~$*85H&VP12(=lhS!LoFyUgEO*A>pUeuUsGbL zBQuKzaVc>hjtli^>Mb5-$=XlqXDYAmnOjBlP!>R^2` zso>AFiXv7!;2HZ7EKJ=LN)>f#E5~TX--R**9I~onY-A)xQq!3^GZ8$^#QJhKuq|hA zj$CGriw<6I`30zM-aHa&ZG0#Ncek1nI{)#}DcOEPVxQcKi`=J>IUB1*InEcL-dO=f z<#%YYVYjC{3}O@C%gddLEvn2dNq>a{T_l}Mg zC4KxGX(`p_<&=G|e;f6gZrk8%Uh#;s+iq&$1>6>@)gK5OCpDeDPb3T@+YPe0(?2*K1 zPpRylC-mNE=Ra-=BuI0eLr9TcQ8e^P4@~u00*ZV<<}D&dp#QW6AcDaaHzsCT!%-_d zyx$v4R!e5@?BVAGR?Wy1F}q-H24o`{Hxgw*r|oWG znaPl?NO@5aIj5U4Fq!?DixI>oFS6Y46P^8)LI7_sSoP{f;ZODfLJc5{hi-#%m>UmV z5YliOX{l|sxvB>bT?|3gM;=ht_T4`Z4Z$nVGy_<}f@h_;iL;6G{i3k1PMJsW%@DGa zsA6l#j3k05$D!Nl9d|2U-AZl#-YY-}if|SyHRyQ%lG>45==&*PWof(@J@jeFqH^p; zi9tXKz<+Sdfr9#HsJP{Hz?M#$)zQ#t4@$@2j&OFL{}o)>MHo1?7H zvv{k%`r=Bd`?XLPRws_oC8f3FmyY34Kz54ZTJmk<-#A} z{~z|wE2zmm>eqCnsUShB(xfDSw9rKX>AfUC2q>K-fP^a8=p_{CB^2o;O-RiujY5r5JG-TvR4g-9=aYsF z^J)l=#M}^j&&9hP5#O)QfSPO*t0(Fsw@YUlA7jI%@G2EwYGkT!8b%LiE%_&5-HEGR zrqX7vM7H)IW=>4%4>G*l2Ald5STEYk z7X!(PLT=F{XXh>1kx0^_Q?Gd%51d~77qtiA6Kd_YHUmC^<=IeSa z^+aB$US051Os?x=&JGniu!xoAsOcUM8uhN`}89oXa>WpN|Y zJI)oGgP|W)?l!Yg&cT8ULOGg?p@s5q-B<2SeTK?L>^fN|4gxb2VIiD*c)4|Van0KY z%@n-F8f>kj$3psem3NV!%?QBKz14uo-`Mp{a0b^|{0(~V9yb>JvdEbVxAgZkI|e02 z=c0hR`~gfqY4_X79p}d@k+2W`8+2M1|NQ2r2_W@#EX5n~o#W?DV+1ww&YKp}M;-Y* zcdb5?pop?71_71Ha4aoIW-7~729LZzkz&t|DlwEwc-c%r!xniGXFESy``^1}RA0e@ z^bCa?ReU3Wg!~6z40^5jQ(E`PFu8X;_ns7GmlR`K8W|9?+7dFu7)=J0*_+fCgJ>MbKqnK~#o(lLQ$uYGFtoU!o& zuCz_N@Ei*XLJ7H7m~Y`VNln-hN)~n|3Sb1MgmDfcL7f<6T0ZO_^j2m3dRAxG z>w8YajY`x4*AQZSH>X64@WAblg1ia!9-RCPzr|_;rRhmtML^G>lXXG(*>kX0i&2V( zW76_uwX&7(9J(T$$R@#ta_oF>Cxi4LE=sMaH-D$e+kPulWl#FjeanBRM~&HZ|ysG*GdRFe!SgRuTtp5|JgNMVDE_>#qa$=mtIeM zn8nqDb-^HbDw+}06PwHgxqbGWI$_eFWuDz@{)hNX%44=x=ew)^oBHTZs0=v>={%9! zD$;Pf*W9^RAiH`oa!pG-ka&CAVBEQ4dD^l^iw`ncbv%L5$ z;`^iLY5q4bZTD*%ner^^{baBEhK~?l+;yi|qc53*y}+i(ptFhkPt{if$-dU1QiYSQ zHF<0gkd+_%3%8qXTF?rmK5bj4)qO6US<8)qY}?I~XAnu<4h$KVA4bgbYk4LIkbx7vU1HEj`97T!S^ck7^tghSk>NDNs@b|I{Q`&pPXd%UH zgMvjt!bM$Z?u)zTgKm8Te^x23OIK1eo9>Q60cXzdXv#Zk~seF^pK{IOdfhWkIljGewXYsfBzy;Y+H6b=E zNg}ylAKI|a7Oboey74faF)hE6eg5O)s6ek*ng-DAc(Zj1?Y5~1-l9NH_u~6Ju4&d3l91S%kwx?Oa0K2yWEKA88 zw&N|Gdoyd;VodKf;`2&5zQ~^sTDu)FBW_(h0(<%m>DG=a4bDxuf-B|tyOSK@^pIITm(=~45?%(6?rP=; zU9{k?pF9})h8g~1@*8esXRIdzqr|EbFjm?Od!!J`AoY~FXR^AihwO1S=rj9x?3TU- zYw|`%5mw-ri%GCHo)c#l#TC>cu3$@|A7%DWOV(SWxA59K8F2@8u|{<=5}~ypZ)FJyv0`D`uIw+WjiDZgSq)i4w=vIBk8A$ zZ}F1_u=jy9hmgF8U=BWA!<$1eBgzO?KSO7M?2?hP`Yj^H3q3nGMJn$I7iCDj z@KYoSe-e|Q8ZbmbH&5@ha4=JEjs1TS(2)cf8{ZGdVn=QOxfn=I`<*$r+?z?g|NN^PMd?A?-< zfIZ*M*hJv|m(&h>+c_)J!xnb*!*h-Jc7f!90F4cgp%c&kud zvGMwKq9=cv_@cy3D@6L--9&L0l@T>;$mY2Ci369xmLgqA=5l9cPrXp1UjH4Inn`@> zm4}6gAd%dg&>PG1>s!HD%7G6`a}oY8e5a5tXn0T@g5^!zfoVvgJad)h8_bM&413Ox9*_qE(QGOKuq0X2Vpc<9$# zE_@Bu`H5DTEimc6qR)+;X~-B{rEi{%vE#U3eZtw%+9d0w^)sVD-xje9XBR{nln zneot!j1kRS%E)m(#?JJ7I;?m;a2k%lNAW-Qy{9}U555nO zJmX&6fu@^emBAOCoU#0OMcz9RdkYSE8J>oVBDe!k>KYX}ijpRY*y<@?gN3<4L$DNK z*H+bz*fU@LM&)^aPgkl<8+BDz5KEo=!|TclGuq_8#wgZt|81uH7xDaG>sLUF_hruC zIqTkaol%?DkYkJDVv_xN-t?>e{g>%)>hgtEv|i~O&p}}J4>yLGSup`nK(bEX2MOsd z`1xyM-hZq4O2`zM!db<(+8b zcw6b5oNb6VC1AoMMsSs+{k>&q(}pd_m6Zrm^QKCjBIR?r>*WmCk07BTYr_hPm+p%` zM=`cJmStt0WCSGcT&Q667}-BKpFC>^_^1$v6P$>z6HR-aNTh`EY6Fjr>X4U5sWx|3X$lVM@(Tpa!C&xx3}X+EK=HQo_biu(1hG!mj`Y?kW9ySxx;QS{+}7J} z&8h9+r+RtJe+%!e9gdibayvAu-jfeSd6=Z^=5qJPgsRBuNTBYF!o1ehgW3YNo*>v% z10W(V;$AG1rSpaBg0D6N?Q63vzT@bPoo|qXf3}s>IO5OoJ4DuB47qTBoM?ZyKK8-; z;4i7qf{8(S(iu1DOIzW_>9X6WmxV4~V%2;pNo<7-a1OLjsNJOSc2J}+z3IPxT zek1Q*f#==m_Y*YSiiLI>k?-2n=-mCx3nLAN5l)kFEN{V1*15_rNFe1#l|KkgYq=`z zo*uTi74t?|rz2h!w{H8Sv?iU1&k)*LeQf0H&A+vUqjZWkpwx=`Xg9wHAgq$MIyH}S zBb?^VUSNgPLHY=dJ+hwrv->X9d}Yl+{;RS>fO$l|KdOkV{|Yd_rd?REr0LkAS!3$w zRtVHqlfLP@kkdiPA|MbSB?X*jc^?-ABKk_sQMQtQ@LXRv^V# zrx?&sA0)d=s%(Q0(OD;)*A!O+tsBp3T(zQn7B83m^NMwy9J(iz`{|ymc5KAAT1I^= zGa!2)kL?jW*mPk};?n*`v?x_)JGyZJCSOK-g8o%in5Wvkvve^c!-&EJ(e~Kc!1fBV zilq}EZ1AKbk8{y&LxzRSCXigo?e^;Mg`^{B=&J*BOsc}~Ix17-B)a{D$bmbTjJ58i zFO84rzM7lh82Cd?f`*;iyQg5-hZ{!tZeJg?+rl$gWqh5ce?^`J&Y5`2tvkXI{-O<$ zo@|J`TVk+m(F=}K0TQGac)49eYV-KCd?}D*mx@C$TfUFF;&B%+t36P~H-KzEB($P9$ZmWU;v|FdZ-56H^) z34X6ULCOcy%SiUt1-u|4S-%ofqNKbk-szVEflv{6k+=q+j@~DPbsBD5Qnz+r<$25@T+{p zq(Z1VqF$Y2khLTyfJ2}>k}xRK3{951v+I1j5DIEJ-7FizRUPfY+LouwDAwEAcm@e|%ouoC#zxE&?Q2Vq66qmR+&MpAD*QBf5WRzKDs^}f zb!Jld>ub}OPVos!H3*_*=$Jk>-{X64$Z$K5IGdqF?2WL)tgbhXGeDTd(EXs-Cyw?} z21(4>=RGvrKYQ}!#4jdLn0(!A)m7=Xk|lfq%+}8Ih9lh)%FHjHkcx z-nisvtjJsc)0XFf0b*@VCQXCGLmVvv4i#UIKg6f3!z*aN`|f#k(E!maW*Q2?kVebL zm#R+20i58LCdBT{VFmig<2+0`gT6Y16QwnYw{#h>9=L|J?q`%Xd_N|m$WppJl?Za@sUgswg+q?q3D2p8)|LRIzkX#`@ET>IpFlOVybsJRRW;mgIB z8R{r2)RiX%j_aS?t9$uLSZ2(XUANqa==%IboPhXdl6SGA3ys_`Yp!-B_#bK z?q#rC@2^Tj51+7lh-v=2TTG%sXOq|QmOY}hMG~bbf@X`(g^NS*-Qji9`!FeW+Eetm z_o+aS2bw$&#vuA<)>{1lJuCb#f?Z`AU{^{n+DOZ~YTzcsY0p}8>0_e1BtL}ka;>!% zFU$5*VJ5MCxwSHo5ou*|*kD&pCf6(q`HEQ^bf`>UP^LX17Ig=7TY@>Sp1_zTEu z{y^Ex6|MhSe7+euML(toZZ*`138|g7@6bjsOY%Y=R;E;uDbzJRK{`R&bcb5N}{o;qWOJY#{-aCd^||3Qwq zirvs{4hl&5GnXf1M2fz5q7vKk6;ubjEjr3PQ|MN2xCZn1`h^;&4@zo=%5DunE*1po)~<*AR>PGX9|S8iNtS51tb|{d!OUhxzkbo+ zk#CxRt#>yU5YnLEiyt=0qM@^MceuNG`FLEz8CHggTP)SRZ{jFkhCyVZyo zv@<=s_>|z+vneHl<5xOxIcvS#YlqZ)tZZYNdG~V?w6awZqt(E3QUMKD7EDepYqf1S5acj>*@(SQ&`bO!AE$bOO4{+12k2uu7b%F{D9N8w{ z)C%;hC~yzwB_rrxz*T0pFu9#?bnXGMcQ`e)^PQna*3Axw0;Bs2mkPOZCssLXQ{mP( z_n2spD#`EjXEPeA( z`)r4BRRci;S%w!yv+%T)@yT-arYOHqhh*v;kCfefW%{%-pXA=?SNPT>h7-P}y60szXh# zK6}Cn%8i)12w*iek#g67Zuclt)GbOxnqat+Mwo^2%oJO+|HU)<7?QH$*s>zYCLNB;! zc6f}d6)Cp59=U#70Z=2#3g0CEqr`!V+X}AMt#`Ug#7!xVP<;(t&~rJd1f`Vbo=F*f zU_iyedz%~QV1+n-Bi@Sqa8A< zMp$C$qf9|h{0_Fnu2Wz-2^NEv;sn6v)U2`1>Alx5ru;e*pgAzU#b*$RLu0@2w8$2E z#{yh_#DUUh+U2Ppav)*Y84YzIz~-n@w8Nq7Sx~-3iG9x(M;8Ng;>5RK%H{Iw$)t~R z*4u+72P)Cxe?SG@0tT;deunTV{W>ymc;WQ_P*4pDaLf{4C?2qKP zS!j&XKH%E*A$%Z`vwh9QG;44$iP@YBy&OQgYR4sDWz!jU)Ov1xKuvAtd4+Vw6m{jy ze32Kq0o5S3`d!(%X92bqdJh`HClcLiQ6jc+xZ7>|(IQ?P;@e3YrgdEPiPWWOe~ct5r*q;tbYHdWmmK+Rp+X?yjc_}(R{D8C1p3#yXvty^gekU#p<<` z(l(5wc0WUz>w=4jv+K!8h

        ?WXV(SEXQ2e@?*z(U&inlt9jNZjY^8g*PT{O^*C@ z)i!7!)?uq6FP66{P`p1t|N5236LzL7K15~~qRy3*fZ5ZBrQ6M`Hr{Cg$l?9I>gcO( zbOPVW^5h%qH@!^YIjeU+pqGg$>iM? z6wL3F&}_?TC%rzlLwa1hX{?G^<;zS7X>4D;|9km)cSb7kVrGoyM`@3Hj@m|0YaYkW zPe=?BQcMw(!8gLyi|g9!6M}AM^}YqIN(`cu ztxLWU3mP=Sk;LWhH8WeG^Amw?6MUW=C0c$^uOcdg1+kM9L|U|7$b!Dfs2$P1dX={? zrnsrXRogeb_#!(=FK`f^|40#2u+SED7khxddx( zAR~s2_{y#oqO8l3=U``Xr|0?zvj0=(n0~gCAHDlRzqVYLvcqiwT|y6s8Z7rYOLIVB zcnD*p`s5L8&K^tJXc2VtU&?rFSLE$_d$RF`g2gE=|5d6_^#WS)0vyiQ1Eb!nJty}> zQD+tmj_-)g*jzQBYWe#(jvt*~;M+*Gd$VdG+PC927YP!!hrcig@+S zVHloRQFv8h?nYNkG||Infct(8n!S5+f`Hmnv+Q&=2PK=pXR8e+kFO!Zv!bva6QW%^ zQc?i3@oUY%<;g#rmN(I}oN=kQ{d58F3w=bH_=-j2UO1z>J5gXMlkvxKMPNEJdet86 zVV)yL{3sKxN|FuUso18_eOZS9$n#f7004ba>9YpTFW z!8GrjRU60P(HKn{eDO{CAwWP>o8Tkh%hIQ>?^G=gmCG+3N0B<2#z9;{$-$?0>k~Dq zkSlprOJCFOAQ>bOQ^i`|O~v=YLjg_xr?^?;QTK=<-zUq3=@uk2i?xascbttdKm3#x zjnB&PbLhzkL&)|>AQgXphFpQSCtUNfgz0rfn+RpUKctbz#toLY^NHRN^(U&iT zzx_RjonKs3=dR0rCgXCeBHd@V_knG4y99Y*pY{Cb_i+55zd^e^=ZASX zILMCOlCVToEtl4-bM~e1>Pd1cs%gGZ7$rQs>(j>vysQG8byAkP>dbBHZDGb3AM7cZ zMjVgdA3dY2Syvn&&*V`+oIgiI*poI=zfp-)X% zF`4wXH`6io+WKN_w76Hn`;KKc3oJka$Fv=9ELFL@IKN{WkXqBd#3m4qBi8hb6)c+76>PVlMnd+Z?x9Vx@769==;%+))Qx6~DX+?=U3+m6U(qH5tF`g%r zc;kPBKU>zovAWA1fWSvs9YSz%0k|muO{+-=Rb;y_+Ucel3wWYRAHtORZ3qEbD5%t( zCAAjcn@5vP9y(OjHl)G?Ks`(J{#}rRrQCR9@?<@n7&w~@Nr*ltc%)?OVy#hR2@bh{ zJk&HW^l<)uWSdx-f(Fn`I-_vkOSn)XTgbaos=X2LlFYBymAjG{k0a{7HdU^nP+o-e zV)T2Bx9x6krMBq5w(0Ux{affNcr>$h9j(8L%#Ph+L2Zn zK=5OOc$CYTU$sU&l*whJ!DOkK#r}>3PB2wafrE(Q!T?0^>9g?-#HvcjDNc+f!{6U# zx6S9-R?Ko;oSbLp$BS1^V9vDC#nby2Ak?Amon?{Gf%is&lfAXaUD|<|r{5?6>$cZ6 zw4SGUCr<0?9@%p54rJ_}1xX)BxFxtkznBd$kP-jH>N)efjqU6#2zPvsTl$wNh_{Dg z$Xq7M6RUFm|)*^R;qhPKV0B~e}8J0 zi&xPs$B*i9uIxmWJCiblan|dxj(PCZ zriRWH*#wxJC8m1%jLW{Qtzq!H1WF_}AT=h@G?ZEwpTq+R(@Fnw8d1d2>Dw7#n;hY( zq!->i3so^xtEf%(S1ZkAk zZ{!MATLG9cbS6~maJP5%R@Ju{oC|otL!S6Efa*{6t$SH3+U(1XJXjMEN=$RboA z(ZcIvQb3ihZnAo=ex?A-otzsGuXvLy_X6y7Gp56D}{{{8gc(88gX7J;GHP^*?X zaG!sw7ncZOdW`v*)onfbRUNRmO6g}8X-oYakQv=ftuG$vb#pOVvukYh(mFlo%-*fS z00vd|e7+t;N<-yPO2v{e9NK(}Ke6ygaFNgPsusly=$0mG(Y~hE&wG3{EXwp@WFI{2 zvHm#@f}br+r!&fSui7Yns}_2!uAP*0;(~RtTVa*STIk7X8a4g$Wdu{=o=)F|iqp0- z5EpLpFhiogrwUgv3?p)&st9z{J3Hrv_Na`qw_=F(!e39t5Jymg5_OH4;qv2ec|8bL zk-Vy~6SuIBT|}&yo%wrYRTNrc@z_U#JoiEJ)s+7SWUukN;w4ePS5>yR7Sy8TE`KGQ zkq#TS&>`wHYSk%(1-%pLZMIT+nZBQ2?hvbTdTrnRDf`G})e|>m$uD^HwQV$XT(e$FYvx(Z#D`od~-WI&`_0mm5ilJ4InC_;fp);J0 zPX*IHC>~>XRh)T|d4-t%Rv=z{(pi^c^aP6=4~~2lUYk_h;&+-~n-!w@O?Dx(!6T!W z@XwR2r$&>4k_=e-EXYW;wxe4A2ApqKU#1-RfiwC@rm$8{-+T@X~mmk2>H0IWYfA3kG*&y#13Ro zQ`T{_{UU>Ojec5a*_aEz>7Sz{I{Eww)9Zx~7tM67lHA)Z7M5kDtr5JBsh<4)3Ap0i zqTuZ=AER!$(uDJ8{>j|AzZ3ws?5fmfe=MDl^6rH&)t~>Fm25@t&OHA#MwAt{b*+un z5&m2lAEXxY1UXe@Uy7mDrm|A`RDi0>UhcY$xnfGwaM92@P?`i;VPYYB8FTlkC2fvupY z$k->?2GD2Eu#Bwq!gn9Unl|jrPI1Kj2#?joG38#Q#fAXno=IU#8lP$Et*gCg7t-06 z$h+kR5Y%Fd=Gg4RWBaXnk+)#J6%9zK^A5nG*gc`Q<;rdyqm5XoLgw-M$ay`Dy_Gvd-mhp$k{#YtBMIDhqL~=~gT7?UtntYu;9qP>5X!xtWW z_z-o2VG@>2-)rFFmC=C}%a6JUOFG8@G+#tKoFC=K{-Z3+w%vojS6d(9d8sm@hnbOx zW`HkzuDW+a`#I2_?48D@9QY5dY-u{A*t(lD2Wk(IbcoJ&Yv=A*(c=2JtRsBEtfg?R z)KNS{k%F*9YV843o`g1fnaW~%(;|W<;vjf^V;2U1v3D_VXk-Z$+$L|;sUa7Hz68Q9 zeK*PnwdJy`5|jg88U(VmFy}SA()z^p)Mz0T@J0muZi;WVQkDvTV(- z#mZK42Kz3fv%uRlu6EsXInH?p%4n+!B0t&Pch~T|Q`&s(!*bV0Of+D&aqYXgP2?*% zpuUj)4RBbzAu<{;bJrX&bYLEz%ns`R9d>F5G#jNaxEVEe>?EHi=TX_EQ+w5fMlTgP8}~_B_(H!5XXU-dOk}`BR6EN|r+-~H zd^^KJxRG|-1QvZ>iFWEet4Kup7U*o(b3gHAt$?IU4z>J+`!uDiC~(}EKY%sQm_X~} z`CVY*-ByePIbwq@k-b2=Wt5q-!BR>ASRj>L<9BKc3DFYNKL!#hZ_M`Hzr!jTREZF@ z*vma5MB)7zv?5s{BEXgrU0TwxSwA0#{md3|xHGx{!-A@o&K*cgt%8nV;MP zb+c-2xX2e1O;d$Lf?X~csEI2Ij#n8 z>V_Rpg}E%2+iaCjt(YoBJ64@(h)J4|vcTIE=Rj4x3z#B_I_D}>O-*I3PQ+c}V?nNH zx7p`UKAwaC8XBiOS$$fgI8Ic|X?-D=n7{dNcNA!4lznFL*J!oqc=P$E2`yL$=vJ~lc zP7#LZ7ag@Tq>eK5w3H3L00jq$>Tm%jGQ?(I3|cFEJ$KHgQ4O*I>6EnP^a-YDxPkhS1 z%{pkCTIS$UBuufta4Lf>eO$d{G7iCN2VFX2`nW{PyfXjTM zZp>~c4t$L*c2XxEQOQ&Ist8XUlEu~_V!fy3%P4rLFpidK$B@owyma!Hy6VTf2Z*uS z>}qv-OD*B(8@#j;$@U8xTyLWJmbz{aj+7W!$}1>Ns|BlVS3 zwFkY`!G5V|z#;4CJZF{tW))Cb6IX4%?{Y}C(K@GjV0g>$o&8{NtxR>ub+sdwc9oM$ z5uRB!zKg8)T*bm(A&Lr}g60K_DxV}#LSV5EZ_RLgn2`B+gjwivnhh$Xdcr&SemSsOF7Z#$a#9QIeoj!BA3&^`4$FQ0lt>{p-e_g$(D-pxWw*1UJ8y= z)WT$*Svt{r3d`90j!V6t(@8zm{OB0`%naGt!QPa53Nnnok}Xw@b@~k{zg`#tkv1MD zNq=nizc49h#%{`9XQ_p@&rv@F1<;t{)c}0FKJKT;vx?&fw}q*0J(b2jy&LbXuM5iR z7r*OOx6DP(*!FPGb{fA9AlD=}b5vq_fl}9yX8msGNr-F(Be#<8Y?H&aqnBgKc9ayl zl8Z$ex^GWXJyUK3zhwJ7A{g1!nZTiFlVIvx_8rX1UwX88^W@OBx54`tfrfsx#c#vr zV*<%U7jx8j_cy@FnXH;_d`>^QUXX9k$sh3{ZRJ+e35vJ&z*-`zafv1cw&(0wqcoGj zc6@HoO$a?ia6f>)ifhfBZvu$V7eMRa-1p3T9L|g%=lmQDE^X@6(fsj=fhQGc-_6&t z)@k7cN7E$!ryFMK23w)fN7XLl@u(7jkQ{ zUSYbx?c3{6{5<0T?`EE51~FVh!7PvfP4Ylus{<*y!0}P^rKnX zZ>VW%Bby5lacwD$3h6VF=7(uW@?a;c!jey*x$s@GzR%NKAh{x2ECIVT7?q2iaT%+v zOx$z4qpO;O^ef=R)&%CqL&lBkGzk|~t_-nMbjv&lxFa3EN3`atly&tD+Qn~8?_T9- zukFztN5+(ZsItjxm72SXao>rT5BR?cXsowiJ>NzL>3NLce5tn-4Jgy@Gb{ChgHhjF z?^myg)Q#D`yc2g=6PAa8*Mxt$EZGuRapp1D#^wyJTU*$fs^Umb*|{n;*~C@MkDbxJ zn|oq^buWhx#M>T~Tfp2uc&D-PabU$alF;t_SUE-A)}8oft*VgsRh4N^ZqC5+f#H(8 z4#~uN*=9x9ki4;O)2LKn!L#f3_A#djp{2>JaJz$i&3>w|8SDhA-HUR_M!dOjm8GL< ztj-U!yjW@#hV(Roa0-?#g4V2LMrif(c%vTfp{ysWqV!bT&+N_qBBo-ocr}m@@lQd; z;sUP|OV^@ZjuN)Ajsg}2L8L|WjG5lMuV=D3!;HN#W6OS1*m`T-2gXMN{MaVJXQ=Jk zXS|Ep{8<5p0NYqkLT^pyim4Y?xo&5>L#F@u2GKh9;rt~;`c!#rFbmQe6&vGM;A~Nf zC5X@jsj6bK>LIUbA(XWBgO;?_Bsk19Z5$J<qWPOmCCTauxkF5qPy{n{Z0N>9+7MYtCB@WkJ)I2yZ%puLA)SYxIiS*tUq^ zMPoN@B%AB=rGxw56gXkC`9G*57hLLFV|qXZG_ul{>{8jqn`QYP^vM$zB$tY8my<+x z)b^Th`pQ+@Mr^w}W?oJ3AS!4*_yR0qw!{3GcC9LY{J|RWZPTAhhSiYZZONs<@&JWj z6jts5R4659W9Pe#gn6*DX6ck4sY5@+{%@I%-!zo0zQnd<_h_%=Z?@H^zWvePa)4(H z%nT^vKh_N9|2?av_W$$GBz2fRjxJ(|x)^`y#^A}7km}~Yqutk!9{pohz%n`_qUJa( zl5h+hMprm5)$BXpRx;7hhRu^pOL*4Xm*?#P4{4?oU@cZ_r*?NjM-CdU`4f@jgWl@@ zT55S|;V#Y)d$a0yo9`Q;+a5Z*uj2D~bz7=*#yrhXatnKE0`_32^?Tl55fmrm z;BIYRw95|t9)xoH16NbZV}>oQ%V!|pES$N?{NeFsrCoAEszCB?KGlmNm+ z0~Kfof@O|d{ddl`YS}(d!ca0Nuy*5}Lf*mU4zFE0VS!aY7Gmp`-Nw&qtk=Gb${ba% zwMI3#-!K7YEpNhTfN7sM4auQjng@sn6)q(gOhx0*v=H`E>SXhr zr}PVeK%=U!nX$S7z&Mv~ET+ES1nvGk=FI1J_L`^0JGzkI-KXoq8zR!ZF+aV-hyc`dP(PK)iP8w+0G+t_lW{LLCJv3#*OhX9rj&v zHD|L{f6Yq^Xz*9{?G%R$bMG=gyIX&JLb=ryv^#D=4cuCtXe(Zifrg~5Q)YF?QD!L- z(SKE+MB{27er)>y{FkYCwr&O>*pdPSaY@0a)0~P92XfD9cA(x8sX}bkZKrSS432C@ok%<-{86gbXeph2tt)+@w z3aG|hj~l%7uD#Y(K%1|LO8pvknCC0IKsP)@z;;gMu@C;qx&$(`G^4Y+3C!GEeX zD)MZ))BP?I%e~KgsaXyC@VIGNFpEQXG6bX7CT2lei0y1kwtzO$SCJ<`0{Vu~9hFuK zXO|I?XTFY%pYpR?;1+QsXwj4f)g7j&#fYuSP9;GaQeynL&*xEWrz|=QcFrm!eNU3B z4ucUqez%yu@Uc9@gPEy}uE3C$o?KIn-)=dRGb3^i{8Y5|s-sJFmb^4>6W(VTk0eOH z+xW!Dq+Tvv{`78p5_cg=lOPp;;wmKqtFMzIN=Z82=6W%OwrQIyr&?!kB7^F^Z#fc`DWw5cqmlV}%@^f@>!=<7Fs+&PU`8DnHyf)#WfFXgA_B^vUm{*H3Y{Y=I zS}iuyYH^pY^MHbeAS3QlbAZ8l1p@|RQuOB9V%YhBrxWVr0-n;(Fdn_qPxj!Hsa_8H zbAWBEVvfzt0JFxwj{94qW&{eP>zP1LP^UdIJBm zQH82|ep~YYuy@|?Y`^c{H%e=@rD6oFP3(~vMQg>T1VPZ+BeB)2wzMd*H!*6&NFsLZ zwy3Dls9m+HW>K`1R<(WK`Fy|kKk&JKz286Nhu3i=$B|sQlGk~i=kxh^Hh2XVViv^x z3k&4WN3%6^>jJ9W)%p$*?WeV0RRT!$ftHHHa8?hCiY7|xL>f~Q8%8Gtz%w)`JQi(0_F`=fm>K&Ve znCn{r>lat4>MH1>+8Hm=Soc!&tXdN;%jPhk1Vl5<4*OFe!C28AAeB1Z+96)s?%p0N z7gZQN(NV%0YM&toEGxJWf%73?eso9#GPcGanus3XoI$5p^oKz5QfwLqt2Oo8_G2eq zy5Oo(KFX?vpktcc;nOlC5a=}+!PCCCfESxoD#6535{8hu*?H=Wt--R&!3~TCZ1U4o+j09Xj)|_dnk$#w@lzoM&uW?aXiyMK@t`eIi{QOvG>kDyvoK zq<6<~r<)kHP2AC4@ujD*N;}1>;G1V!sN7k(QE$fT&R1?m7ty2Lxki_fxq5}wr>C-s z5afz&_(e!u44H4Uy1DG0LP+qPT=fj?=@~@;QAT8}UQ24zma~}{C0_+fMXT!-HcYZk zxY0@;&vpY19_LpKswWBGc!6thbo*w>JEbxL;rubFHUw20ED#=~b~18yoJcFszmwCD z?hf?TSd^4uT0_ST#QWTI_wuRoOX%4~$OM!gk`fr1%tJGa+FN_HQX*wIR6td=xYhLc*DQxLGnlewt#m*rdUw!Lk483i zt?Oa1U}+~=`tm22*H${B&oHy;AGqV3HnBC!LA}gVEidF1F3j2|QaZ6JOX#dxR9V}I zNudswgaO}HCgi~tU^-Ezpik~_;3$QeZSANjrM zY=Rf7;b6nUU!}G$M}*_xM(@VTklBFIEC;45!bIUe&o>36-s(KC0zF zpU}h;_~La`errugqOwW{SNc5O{$7bT2ULCZe8v(C#UsFlN>`etPt=5WyaXqQmw){&z;!f7i>B*10V4=cM3`{~R+qNB8cB4`tE(?-2w{>;G}9 zT)k#J)Mkg#rzt5Fdz}8U!#Mn)Ka$ zRi-Yw4)DHHNT+D;XR3lcsoH--Wn0vi`A@iO5sF^&inn3b6{!v9D2SY8mouQ5Y|PO6 z#Byn^0=*hQwD*7&Vb_?75iyH2y!h^=^Y=t(pgd)+a^|J0!%;y9v3K6d>v?MXZX+|W zH|1#B>YDrHS1I)d=7{PYn}!UC6LzUPyC8#TEg7>kbNcMZ{cX$3zDMYdEJZ8ZL!Rb6 z3njP8(3jPN*x|}>>$&=|`+qHzkbo5N>hoKl&}Fb3k>)PX0#VGIYHTBd;;8iu{HO5H zG&ACc=vhEp<)_#^+oGsFDF4$gz{!|9uGW4@J@p7lTotwcl0NgF>fX+5U)9=rVPOQs zD^8$>V7w%1NLO5!++;~nDRXzqvXrf{2!@9VSYGJEhrjE;_C*E57 zB`;48$P(>eM345o2n%8K&3~_?am0pjZ*F=XdCigOF)KX5h?i_TU60u`fcj=!2EPd; zA_}L})#bTNTQd+7>D(o3EUq zP$)}o=wH;HJMVU@SY$L464t@DFTFAr^XEoc$#5gu@KAOL#DpK|OEo|zF%Zf^Q( zt4R(ipx|tHasGW&4ENk0EDeBgnt8TibDUf0Xlz+4j2~@o4XSKko%~^lN%WT=qwv4p zR1i)5hlWkqX!5c-64Jzor}%b$)5}yVvbH$*$%yS}m+Q;2yJkM-)h5!$E{EhFth6)Y zQk*Jrmz_#UR!IlRZjInVW%b#r+#y#}O58{hPq|GJSFXv-lkX@xRa?=1uX#1B=s0En z#cZuvKF3uUrwH~O{)a~E&c(OL9wAvc(T)v4&KzSqlJ#6Fo$_kDJJyWSdK?)F_xhlw zhQ^sh5uU}9VL3z^_7j4U8va{4atTGImpdM1In8J{=N}C+W!`HR%YY4cf<14afq@VgegA!;R8F#0L_sr z5EIcZZsjg>Z}TJSj7poEXxWt+AOS&&b^!u6|GY;GaL^4k>{_oWQ$qitLB76@vL8$w z3*?E?c@zBs4dHr~5Q>UAM$b8ZJB{%o86{fxy40f-KL*#{Tiz%`##;wK>e*#TJwA0} z`FS7FIBEEt_WCup4j*;O}>NWHq&Ty**jIAGbzKx-2 zR*e5zkSq?&aCNZ91wdv&XsjP-8dQ{xA)sX#IzIwD zMe)qRFeXK497fpc4hMQnm8Yh-KBBxh|4?j=U?^tbaqMdwOl6gNyoBlqQ8d5_RJM|F zme2yqWbV*Q9nT=SiF7hGqejD%!pT4I-N^cThj5|1=F};2_A*LcyTQG_WA@4qg07Z! z=lSgLAUT+VbTpmDQXVVz1l{JCO=>2BMbE{z{vE#+=%6i3guQN$>tE%_Q; zn|qi=L0K^Wd!>WXlkg$yGIP_Uv-G)vRpHCXje7Yk3O*SzoT_hb? znOg!G`l)^yV(qTBQ^;5S;1>gF)!PG zwuORe0_uW5M*08HkgZzs;PvxiH%RP)ip$kN)OqwpNuJ zA-%|cf$ttscPe|E@wxgHab@%D8**9$=~hmxR#jKS6N0x))QLWmnoXhb%mytveZ4J+(r)mzCs6l~Ml`E)%FDTrsG5C^ zyw23L*8m2}fgLld>>2FOoU-*I<@DJQbv`90yNJ83h;JQtK7&LNX{c)dgLipy3>wUv z!?W{2=J1yeT$&p*?LV|7>%yc<$j~Pn3)BiA$DsUhYXdmcHe)7>o>09N?M1Ek2UX8$ zSdkIk4tATG?{AB+izyzOH;wyZxgne37jw)UV=qfHke-9r>K!RtP3rhf03`|=p0O$3 z+#uLl+Qc*0lbL^pSG*f1#95-dHvhrNpi6XmBWpe9!faMuY^gNY(U1EdaGNZ}EP|d` zRnx!`2s(_N!yBTzZi7mv-j++HKfYEfOKkBdjQV{a-;T{4fn1o%D*%`Xg;pgVfpto( z7k;U$0n&VGRud?Qvgt2`9*9$ASCA8}1-Hmt3*3KsXT|%41pCrc^6|5;q_tmPs1oYA z-j)~F29E;1?oYh?ar?XC-3Z4icygZk09B!H^slDpUv>U}y$ANd!KLCiescefN98oU zg3_M@ul(l%S=Gsv88aI$EEVSm(KIoPCS2+g0`R!nY!q%2nLy%su_Te0wqXSd1*DFM zZ??#_hjytnn#4Qp2e8>j+5f0}Lx<`47fDc5}#~U=M8P~LXi7UWnB?(;de=oe1Ax8wQdTGU5!hf{ylM7swDJN^fYVy zOJuJ8hiGFOr&(Sn#Y$g`anc>iHqj2mE&oQPAN|0>2ros(TUt*sH1^S{SDQ-`Pf& z@hASLE8eQG1eyR{@-^n!s!7fiqq4xIV$Nm-BcPO9u$pMh9r@APQEp|g@M3iSGBb1b z4fzO&N1F?^*(9g4t%UJw<+bC?&WyGR4*4FEFg(LEj?#T+6S*WFiH{JZCF=oi^n#4- z^T+>Qa(UJEIxAc1X=KP86YEoIVEgVASDXu@BoS&Y+C5SlOKf=sqgh%`TfeXr3*Y|#W#u4>eYH9;1W}Vbe!skH^V6Q zhK&OBi}1$KLsKf`gmu3j1BNiB6y4Wy z_}z&~k+&5;9|ua<+{kmflc$i8Fhni3d?IyZy>fXs^}>WrOR+N!_BJc(fm;0^ zoQcG{?%H^AS-8Xfz(*7OP<_83Ntf+ZJ3SP( zM{sneu}Xn=(fF;2g!o*V1Ty7IAnz*hi^#NY8611lt@)fJ2dyiat!^FG&2kn?Ol9OJ zI<-w(iC!SyXW%DkYebaaR`o8*0J?_jJ~m<;?42Ftw_%q> zSdr;Du&Fa>Db}9M9Uq1>O>a?V)_f~V3P0w0hgl)D^zCDNHgG(vg zseZ+LbI#*erotkLpdKeb{|Yf_Em64K+jGo$Y=7`Zyoy`w7bZw>wvKsSHFMU- zDeJnjxGkzZpeE#+pp8ip8g78#l8Qp$_JB+qKdgCdPNh5c6*c z!K_r~TW8_Z1T+WGGO!FbY z(5y1MZsvvO-_4DOGn0UP=d9q-n0;H z64RWu?=<_eHrj*Ny}g^jw-=rog$=+{m*zRRrF;We^Zuw}9Wt?;BzKM71vC7Bj(99< znrxhw0xN8c?r>M@Dv>U0n?zP#djlybk_}KjqJb1A8<+l8u^fn-Nu47S2YuPcSl_dI zHe952*!gj@q|AlAnXH!_q1pgpy0t_{g}YU^lPOIX8>I7^vc)@&MTd50rcMeuK8q*} z`G%m^kaMBhss^=R>*q!CCW>fpAij5cO*abBw&$P+=fLyBx4o@?T5kg0X?1rgm>yw# z=4$Er*Xmb%-pht&KjW>vl{vy?a!hp++u!T`7)=+>U<_YLBWTaC&4X=a1f=_aSo@?jU#XaLTf%LY!<0ZAony|d?{HYCAc)=dpAMMZKk z&3krRaoBhc4O?fzpX!`x$g2n$_7CYrWDVwRY1xM9OiAO^$FdF&hM6?cqnJ;KRR*ok zO-{XMi@kFX$7IS==Xxjqp&5J?^u56uXwWILf|s=8(5L2|cX$Vl4pd^4ZccMRGl>49 z(xoEKj2-U5_nJb~E>-4n$5Uv?8dJDqGs`IrAI6jPwx=P=ycP@Xk|d zSErWqWic6{V(;@u7ef(4Z-p5Toz1#!oBpiTQQVOlZamea8bwbGGZJ?0Ksoq35cidJ zb(T|S{64-oi?$6(ph&xKWJ1L&V#W(0mr7x`3umR1^?!>&u68kX2Tx^?Mh-2~`L7gO zr=oWBk{Ues?BvC>Zk9a@{2MQw)$qm%3e?EOFe9yo5qW^Xe zbT11uSM}ZgAJ1)w?ToL)U$LN!A5_u4E!*H&{3h-bQ(f!RI}XhV=5^83tx{m7?|H{` zA6+beB*Nv6!Z#Dj5J)7)wfxd%hHDKYUH(=KCXO!@#_m#rPO5N+XA43_>eA!b(85yv zLDvAzUfTdQ=P?XToKhpE^;J;aX(&u4Oob(HVg=Edi#LF9D?mG3djqJSgHyqm=IFspy(l zvL+IJIH}D#N7t?5V5uA{L-Av?yr^8Gbfj9!NdAW*iniFCs2U~Bl2CJw z3*dvwLbqk@3)xY5G9ma1Yb&eb&C-}-1n4#_>}4Y=0+X|5SL zU}U2wRILJ2_V6m^uTfd+X#0#=rEjmMh!_o42;kC`p!QGX6=UhT3C`E{Y3N>{VDii| zG`FM#Y*0KrHy>qAWo0=#7d&g_3ZG5U5Td;y8>pj}ridQ5y#}#TB;MEI@7r%klvZ2U z$&|tt;NvlV(S^DD6jyPd%)QRc63M#tDQs}>{aRBwd*Do451lM2g|{p#=`#QQhEOv1 z>DRuHizd5=HUQbX{_C_aRd@+c#9E)t9As}UdU`y>*lAuf)=Xo?hHa6+r^hs+cAi?S zUgD*--9{|TpG_EaELvmvgJJh1la-yDveh?|a@o4@Ef#wFjFqZAIsj+h>Jon2u1XsR zoc}emfewFJ&0Y@an;&A5UIQ1Vx417bZ{w8h`#w&)!5E?U+dpTO`@z|={m20t`y`2l z&TWy%KDonz3Ys~jg=YW>y5(CVeP|~b|ArFU1*|KmfGMDKrSo*2+EiX>G=b}3o(b0@ zi37um7NhCZGYu>MjJK^USwaV|W zKN@Jj`}5*EWI)boB7*Y`(Y6o96gRI+TaKWK!Qz|3&lTb7OyA!rz5~d%moy#0Nu!^V z$$BCs>zp(FmG)w2O%Pqf4JrLqW*nfPuxEEh(cjN{TTHm*E5kN`+5aLBk>wL9vW_Sl z^C`z-^GYI3-&K%2pduCXg5y%5tWLfEIjDc%CjP?!iaScwtHm-f71N5tDFJcU{NkVP zI6`?x3Ja$EGSLM7MGW@kpUn{fgqXg@7Rco)%;a{=AnHE0Rz2hRrI2judU0WG1?4Fs z%_aiE;P)tZOQQ6NHX^~5>zf>6xL?firFl~An^=CV1yW&Yx@5E29E8?!O$`}nZ~Hz< z&7ZmYMf&lbWNo_EETfLim{QQ`H&3#&J#Bg+_b4PatMaCRFm0T(Xt1kv-n6`miyaiO z>apjbG|hMW!h0=(`a%@(!Cq#P5oGM{3U4xC(nM9cSaG`$r#Z`#W`+kW!|uLRcG`a% zKOEJNKrd_@z|y6Tw^L=fh%mnAw$&=psYC@8B{gps2q48-Eqt4{0YVU6E>*$VykdaO zWu$gjZ8s~VRI*T5?k)hNbpm8OIAW2Q%g zjgz0y__)Uz-9osx`(6qxJ$9=XV#Fo=^_$II!>=kuFqx%ttK&T4jVZ$N1!Rplr3MDb zb5340vOyG<{`9cU16EUQxB#L;ooM+(T$|w;<7)Ugg;49N{c0D+_!I1i4myXzSoje_ zCmhvmyAbmOx57Uo8>l^mTS6{TvnUuom~LIqP4w+odP>o&Q^<@VvC@ayOuvaUVs6s^ z1NEr_SRi>U7_>$fr!xDO@1axP8fEBELozGXYX;3JG=7WAL$7IAXIez@FWs8So`&4n zeOqmhmafaomBvm_9F9CkmHa_fbYoO34WyIWZ+qoLLa^Og!KqU|zQuH(Lfgw{T(9_< zoZw9rCBl98A8yGPICK`gbw^;E$D_1R(H~`>AX~Iq0tiL}x0J0$Z-ttR2THp@>>Zb& z5*VJf{A=03`a2Ql#L5E6P|O0HDC0-3zP{2;@Q;=)SHHM-*Qr2BWW%f0@vI}A?Uv=1 zmW{Kcg%aH?s&GBa$Wpps>V#29&rUx_KA#C3l2Z8k)%3^+T~mOJlQ%KqqQ|lkwF_>$ z%^8xu%_Qo7Ez~GXDB$MYR+Y;IzxjF=Ety^JM|LlSO6B4rA#@C3Y<_UfD<&HWIX}bn zB*ojlkapx^Sua%wHH{^)S{L98I^@BQoS4t#7U!DCk$R$@xeDHiIfDI-lc9{6vAbp8 z{x(kH1(5X3Km<@680|n-viPxYvhZm9X9HsEn@qLb07-^_yuxSp_wCa{WdxLYi|4&? z_a~psA`uxWypOEU884)k^@3p=dcL?YtLI_uMJ-fSYo8V7oxhgVkJ^zB=uo@-f6*cCr z;!NPrmqkX1gtPb7{<8*-o@f!Ydskir$Wst#u1b}2Y8ec+k&#P2SG`gaMh--szF9_< zSeFFsSSkn?qN5kLjY=N({HJW)kjBk1{e4^bzgL^M3WZ8ED2ZCe{?~~+|Gwt`?=_RI zoqD{%edT|UlaRM|#?>XhS&weNoiz+rS_JxlQ+>;i;l{3f0P(+4-7_TY~i_Qi|q(mmU`ebdfkU^!EmKaw4Y2H zi-><-R><|nZ0eA#e*=xuQnU{pvn@UH6-d*|!7#Yl@ywVo9BB1(#39aoTG?^}jrbf%(=}OS1+&F5q+=;_{!p_xK&U&0_vo zd(2L%wHzQ@+Wm$XuQFBO=he_#e07$!Dmr=Vnr*11^HPqAvW3AgMM0c%6@*6W=;#;( zn;^ygEa}IN?J8C#IVzi1%%}uoXo6^q*fS)}Mj#NHds1h$gjtwbyF*fnTPgOh0b^ZJ zQmNSFwjTD7u}-_H&(WN^SHs_HRqX__!{9xCWa_PAQBSaGl?my6I-`vy9qh74O=}Go zQ_)Y{I?tp_+x;>Rg*2?O9fM-Ab;Q>>6cpQYFToQscudXoY}3Icbp}3V(pUnV>q%4& zvABqc*5h^oC8zkk6IX?o_^~5<9Yqj0$OOkKP`At5vPYVI8-09-^Bx@^W>gD5kTCU5o3-Q-T_zAcd3T+$FPyMe70P zLQ5YP>U(XCE8R<&{qu%hlu)i*DrVoPgZ5_vUGBrx4}?nANE__6)p}iy)uwt~=A1)? zWCLTiEQGl1SRa0Ea_l}fOU&zzMk3+rkgx@4YQS9p^u!XcLv)KDEeOpOMGtko%=W9$ zv^^hM8Dt(;*!LfVe-moGelVe842X-=S4ZqMp!XpIsBHFIw1EsAfSCJDe$ zqA_y~9w6eGfo|c|vx;~6zE$e^azVD-wudjg@b^HylXf$!Tj_|RoQKRHranHO5UTGy z&TxxQTtbatM#wuo*t3?3%>z&Hyb&fSe=@>%$wAd`aM++1yJsU{Q#P>x&M$IqZkt!A z{MeJ(SPDEYrr#`dbHsR1J$+#G%W%0@Tg;0+r1b3#`d@F8Z;+$6H_lASR0u=nyp*r%4EcP`q^G; z6qePt3bi&$vn{d*nlY_2OZlo$iAe^ZU_%b+Y+Q8m@@&>ZWDB?TY6Hus(AYdjh`6$4 zIm9#Cx`}lN#9GKx`^tnU9;(eIa=(b69g}zW776v!hnGtMIp`l>cLfothM*xR$85EI z_YEJH7I!_i-B-xVwWfbyUmT;`#5q@1d6wRyQ*)uH3jB5w;~IQc1s+w_cFMc#h-pn& zw233jo#NcYgGlsl7zbQDCX;Ru7L^87b1S6DOVGS!2!1XL#%Y1##G!IfXfrOHq#(MM zOKY`{bb`UjTn=eMIenXVi9Lq^3sH5715J8cqVZ)_TEvdmb(yuM^%%2Tq&bJ1q|RS3 z-tiTCkLyw_s6c5=yE|a14{LR%m+n4^Rp0A!xJOrk_WFo8-gzf4gnWMbpz7g5TWqF@ z1Jpit#uT|ua!6zpsB5xsCmz}QZ+cLijd&VOlD~XZC!1=UGW4HNt23BPtQ_)wN0^q> zon~GhF3x&}{hM%93<;689;K2k#lXVQOiwlLoLdto``5ws7o*&39nB2B*y&t+_5~rk zNS$m5&059ZgB_1mJM8(+s*7+jF~$u#KmM;h-% zyz3~PQK`YsviU^jxI{hzukhq&n7zX;JFVi~ywcFh1w@5{%O7c)d(KYo!|p%f2`^?Q z#pU>p>zbh&4|)nuXn8IMpz694)xX5Wcms0SPEyQh7GKuPY(H+@yuw4}A@xO8nLBTf z^8T{Z^7YvL;3?c=9N@a5zwrn>*P6a2JW=;sT0hAHVTD%e5`lDtMO=z!8|W;q?j+@w zX#*_$VZMwLR0WQGiokveyg8fct>YhfcI53RLcOkY2d)7w-L zMsuyQ!(J3F<^eM3oE+PCl+jBqGcGyIgI?9cG1a_4do@dBF(pjI-IpBBzVqaq4Y*<3 zjP2*x_u#{YUddf^Z(ls_?63c0v025F`u-ll7bXL(x+|Ug_g&V823gK!^O{+g64i_D ze?**A)z+XP}Ej~a0Y$t#q zFJGOd!VvLSt)QGV|33a*ga2j^*y+{W_*nq`N%hq`o!q}k^)4jezn5_gv08%3T+==B z5iWU(sbtCuwNCn3qc1fTho)(mguUTnv(T>LV$o&`ZO8CmvoblW(_F5J1?wZBP(dbJ z@)m=^4~@@s_mnY@zi#0h#p|yn?Zm!QEwO&;h1)nJT=EfkmW2seLF$-;8J^Vw0#y6( zCP>@p3lptu6srUr6Gcpsjm%q)aRpz>d3fDZ7lA>fTxt=;`YKvhj8yD^6%FOsoLswQ zY{AqBZx8AuQpSEDVs(<)q>1>(h(TTU|)AImMeZJ?Ri0x;B5g-T{?4r&A5xxtDqF zM^Bxsv+Au{S^Y*Ze$PfW)9fz`WdNQM?kVBOo}o5rh4NRw*Ek{LLb@6rn|4Komdw6y z_w@q!ctogb-K}hYb@?aJF-?-s>>RE|OdNSPSD!d`7)rLYUoejQK}ek7kk*|e2QgJ# z{V@KsSkc$)Az^?7dRt(oyz=ttx3IdK?r}T!ZtZ6B1e82SEL<|1nyg`e^8~s6a_88o z7Z{hc3Y>qkI$TV!!3Y&AE$u%edYFbdfA)R*T$X(y-zC^K#P9x1&08M4r*#eNRh>j< zE+K)JSE_ZQxXz!V3j~d3zy99!w49y0doxOpo)iicNMW*B>lQFjNf&y990-9Vmw8Jq z0blrvnCL!rXdd)H&bP$t>jAfOAO>VoPAt#di`FElM;a8M@ zNEzV=#f77*wDukBQAF~+$mHsDcBoC{&Kak5-n%Wz3|oqaga41h}B-%QJA+vW$NR1bom+PMyPD9rx09%5oaQ z)wH#43+$2dm)%8Yz1xh{2|^{5>a28yOSp};V{QNe%))^gM`fm2Y-F^w9_fg}xk%B? zciP+2{-GJwu4gl#G*lleLPy;FgXMsvRVY2lRfN>ExMMAj9l$B^Nz5|Wtu>V%V@MCf zr{++~E|^)451KAq1P*B4TFcZ%eIfBe=Zds`a_494)pTSjdDwfHz#;cH@8Sm*VB3?8 z{ElKCcsG*jGH7AQqF1Y{PRl0bu_=3*o4!4?#b&;O6!ZGcR=aVf+rZpnBZ6Uxo&v}! zGn;R^7E}Cg)r8BNjcu_@v!82|-efa)M#=0?wYj$o1MRMU0ID<5Fjoh=ZrumCNxmya zcdm~)tra%78&>qNPLYyRHig?>we`L#;5?Av!=blC^H*kVSKlC4ve|7m5=-MfqCBi; zgW(H6?*fbdV1l(I1uSdSYvsxz#SDAO8J7lfN4w~Vy5Tk58jOYr?Z#9Y*U`>5!(*E` z<9*m0-ghQ4^z9zjtm*`0o?w!di}LS!tEmRfFo$W3%mw`|j61~ymjj3M69tunN%-L5 zO8cf0VFMr9IN!xcT)fSpTp{`j7lQaf-#n%bbG zil(i4!&r5~b#1clUfVbAOGY*Mobf<+dNcRbSZmgSDwo-7jNfZ6Yz1p~TG?I-G`Zln zT*vO;U^8QO-fazM{Eo}Mg3y%)xNE23f5uz7(Zzz2hH-!>9B;50^;7G~>KEZeGw6!_ zq6n$Q2|`6>4#&!lToI0(a!4Um-ni6DW@EITDqG*8EjCvXp9W&Gs*lGo`uBrT+=901 z2O>+606U6HCw{+cp-rzDwSVc3Nl=$1@{)g1hDl6*wGRtW;jR$CW4t&N{U-U6tKEX~ zCXD6U8zuF9>!{Bq!4Aw@D!2=2BB?MI;1GS#EBWY(Tj}4jPg=dZH6G66Rijf=O&GxS zTuSY2-n@8jbD;p9*rTjI?OWgH3y#I~MEmsV@w11>FW31m zB;xg&Edbf;bB%`8&AZa@X)_~5%$(zWdsEgRNl*U6Q){KwFl>rJnfQ|ZTG0a{ggB{v z6BzuB1^iH2JTbl4_=UOF;|EVq>M0RqMR0rvmvHIMo zuxs%-VsHAcY25{fZJtIA2L$H_1V-bUigASy7i2c%JU0t|-`~^!)6s_J`E1JhV|4~s zW4FonPw*^R0fD!^brl~{F(sD~ypt2tBV3-iwkl6A`4oB|a(gUXgAy8OrdhimG@-X`Mduj8`?;s|T%zZqRi(%8KlZL_s3W<_t#><_T$R$RkL*OSxCmcgfA3 zp4Bo^`CMak$x})|WT*jGOQ3Mgru|- zmW84(M@59Q@NcK=&)sHOxvI{0`#Ypjw$b1lykh$^ckp(@e}eBz-a{oK3uoX|10NjA zz-guZ@8jP!_^PX%rxU5F8W%ae%%S06!e1| z1)S4@oRd;H+Cs&*ka>~S>%3SAElDuvOHiVPZ+$9KmHMpf(rh|UVOQBW3~anGX=HJ5 zqT(X?l7)|AYX?9fW6TMYuednRTniaXrlH(Ts_$j?>vqaE;gJW(roQmy8wuT>{^E@-=6bG(-sRMd+@;Yd) zw6AXO82$Y=%Mj7~>CWAlgNh+k+tH9|hnIJa? zpWS9185beO&OKv^ES*%$T*vYIKfY@9qGS5aA7Ygl>}EtG{TIEh5R+!bik1}r?d?=* z{-}P)P0#(Aj-GoOq0ZzN9Ys89_^ZCgEQidrM1o^d`JG~bc+$Jh4)!vrcB?_{1vJDT z#zD3@b~@(8=7i*+8N*J0C}ykyis}--0fJ6uB_4Cf>&vUYHb&sGPrCnPZ*} zNxDzqR#hB|->Ngut<`?gT4rz&U3!qeiLSsNnf%%Hy%wAt(ibz7wH^2JoH>1q>nItq z5ym73H()lmO$cpH*8p8vLF84Dl49zmpR{6*AFm+iYA%6`rfrRm|o#egq7Y zt9Q9!`~kv^Y`|Zf8gVtIkcJb8i?*wSxLw}gyC@~21`aOrKt&Jj8ZF(RS(XF=CntZrw2TOft(&BCPo!#twV4}Ck55!s3U7@>n8 zU!5F$S+A1Fnop+r&?9O?|jI^21z9#B<--cDCO29bK!O zo6%4(LfFqXPUn0!SR_NF+N&s56L>$g{ea{&(A?hsdCay!W+pv%&!NM`t1bUS4Zrm^ z(150u=Bp&ijl0_4IB?@eA?y-dCsrzIw0T_i<&MH@^z@T)L1nU2v1yodKQhZ$L`@wR z(g}8S^!dT+)|ODHG8nheLj2K+-jkIbTW}Y^IT?!P|LoL^z%^9x-GsMc9*B~Y*6QvJ zH?snyx9~4>IGsM zLh@v+p3Y&fa(xD^SR2m8UCeDu15R;)hoZG1RS55F>=ZaRMcf>wZJ)kue&^Jag)H0a zFRo}uDM1$}t(C^UJ^Jc<1>j}o4RDw!%eB9>h)E@53S1ehwLxHpuxA4JpjhUn=71eN z?Ex(F6-Gz3TG3mxk=o|h?~b>{HVCa(L#p}-2KoTLtIMD`jyKi}i{>}v5eB8&NLA^-dURnm@9}JeLmU^kQ}ZNz9E-0hHC-!(VH=E2`<4NrahDz;WO~DV-1G&#UkqV zO*d#4i1BNMgR({d2SqClcKA47wZZ8|dM>TeXy0sIo^!}7%j>oy&ZAVjzW}F3M?!mtH<@|9$HL^G5^2cte!|~{; zGnW%AqjWOuDv;enA`^ug$(Vqh1*Z>W@;3K5))5#d!@U9C8v>GYH(|VG&0mmRT)@1w z<6EC=`;mJioQW1Ri|AUJ+bY3ufm0PWwK@+9J?PSpV7M^p6h{=47guy_|K+Z738818 z>zg4s3`6Qq-H2F%%=C+qD))0)X9iBk)Oo53u`Z4~GPj>pylevfhRih7db}We>|#m@ zB_PgF+P6uUs0bRKD_K+df;poqzvd24qf|b;L8>PP=PV98ZZfukd$`?^iW#D1 z5Mr(W{@_bPQ~U{0%ednJG2EUkR2)ic-lQ>n$k@vL%j!m2=t9{uzZ~vV<_5?Oc{&xc zJ@-q$$BHBMU!-$$1OB{2icQ+2jrPtoWqjP~X7Ng@V?#StZK)>_Yo2b}LIOS-J}A=b z2JV&7bfNAK18i*>@^+gi=S2OYdw`Su1<@=W|pAP-=oO_*G+@DPBJU_aU z^6jrziE%!;!58F15H&GDn9!J6JG4*#vdPjF*PLK*g=ktxE!s;ST0!t=8t)P|p_mC* zx?DqTc7S-Mfer;>Q_C|Xc|rT$CRzBUlpc?GNfW(J#Wg1efNewKE(y{BD4V$Y2Sgjq z+mE{&0JTZDgcD^6&V9#u#12aJZs(lQPI^q08BTvVRcU zzvn#T1r-qdSX+NN7dmUsV%-<7(-+oY@_Fwj|qQMX%BuYLANarapB9Lc< z4~0>H#2;6HS(N(*!rpqpkL1Jzff?sgrsJtm4Gs_{4!>i^@JGTv+JsFdFZ54058fpt zU#U>G682dCrJPG=eNnHeXe+F}#xqUYH2=LNN3%vKVnwpkxc-U%2pK-zI2X>BF8$zK zZsG6kqB#=ux|aMiZ13}EI`szhM~m!_cNF=z0kh6K&GyFan!w5SDUP+pVE(md5-ytI zfmhziE-JheZ(NMD*s_{=UujyEgcX&&C8}_ptH#>Bx$nj$3U5DS+xK_OPNJjRYK?j= zZKm~HbnV6F=_~Wyaeeh32jEmbqoL;kJ?G4RzebIZ>gu)%m(1Musc{$JtN+j_)))Ca zY%LP`Xw}}EKXv9!x&sy;7e;H?$%+w{?j2JJ0Z@^8=Wum9G$HiiE9yom9WV3u*K2fw z+gOiI#q@o%cAHkMm=t??%gaIM{>&1-j&z1q40 z7y_07Wtzw`g!T8<=)QI>X(K{Ug-3P9UvgxFr%LzOwkD}_G@4{Qs+Arr0sx0&| z?jRT_UWy&cehbbQ{UTax(E0s;lu|*XzCYtc-_??ZV{3eQt|J$e%HivtDhHd zr;Rh=_DN!qMvp8&Kca(TvTSY*yJ7caC|zdq&@HO@*I(;?U7zO6;YQk#c&*jzBxDayu7fG-_{gW z+cx0x4bimwlO7^_sZ}MFIs>EXmM!xF+CHCP>GJ^wfB$&u>Tt>qdajILccZN*jIgx? zQfyzm^w}1*b&&fz-^26X4F0qkd`sAVoK0p$%mBtd2 z+}tLAV>zuHj`86;2xlKc;irBJFw(0?ioaoE%o9mnoq;X1$)^HOo_+m8>RtulLn7Un z(hsEg0O{Oj?Y^S)fF<8wcdv4QP8ax9rsx#OC^{i+n))TQ(RP@#XirQf!Ewj`>XMe#)d1~&Dj0E&I$(kM2|_W!W=p6_h`@BhCJqeg2*&Dx5fHKJBktM*<&5^9s!QoHSo zDkVnk5u^4_2tusZiXBD7rfToDlvda4m-p}c7ksY2F62t`$Z_Pz@pwL-&-1*W_xsJy zT37@lZx`0?n8leQSYc};L-AALPRTNeMai`*?XIiVvG?r1*sHuG@-NM`s&SF9o`)oz zU`bIFDqP&=lZhSK7ybEj%nvewhuB^v<9-0yF}C{vuo^pAd^Pa!Ptk-FUBM{QcG^W~%0N>R&6dQ=z9fsr1U_}~ zla@SFUO{Ek5iYZ3*vQ?XnAj9ucH&`s9+xO3jtFgXhx+e5Ua;3FgMrw~Cl}xaU6`b(RdLb*D>8JG8)$ZoF zKyBxN+$q&q*P(tlH_0H0u$ikDyo@`)oGh;gUe^-fG%0 z#*p>j|;^QFQ|PHEkH_#_F9Muk|P~mFjsER_Ek2Q0tarWO)cl+S-1i5;z1 zk$qPmrkg-Ir$wSerRWM>*4a$gjAS#e-UcNEM8jgBwzdPpm)pnk;T z8+p-tR(=ZRw4t5S%9~_oe+APlxXjghprL@2>!oB%S4DTNvI;Tnz`?~`XL2c_M+}Uw zdhJUPL&?$0)1gkCqd*PrWx3q!z&h0E?5YMGN9C8HU>FOK8$)5Eu8$&+ zKr<7PL2Ye3W*Nd5CvSbDIui@8+S>7>bB?rU8!Zo3i{$h$!0Vf zOu51y8 z5$Ws0mVQ#GtW(AWJmFEPWAi#4$aR9hanbWbxDC0KFTR>zn>i1ilRZEs9Gwg1f@2fn zll)NU)zaZr>LWJ7Bb_ zkdZ=ZtK2)U^LZR~1+~_tUcRqpaU%li-;F?M3bPi*|0vN95u0rpvrt!ZvX%V@xKuNA zx#g{;0`}YoH8|rh+VLo17IPPuCi4;D!cE51XX^8D4*P5xDCqbuK?Q+ItHs}xoC~?g zW4G}xtI$Ea#sdr3=vRH+uw;eF`!g^Lt z0;T859N=J*GX(1Pud*NL%;3S&t$y7Q+MM0Wt~aCI8 z(F57%3JfHF?u2IE^(od2RPjL!5$<5f%`Tm(9Z9hp$y`$1u1rz+nlW32o-cY?3Y9{p z6_m#QL78-xV?kEnV4jBj7`pfp{IuhO1qr`%{DdZkU8lku&2H~z%H z*aLTj4ISxS&qCdSEhMxL>bQ3ou)bTT5ozVKjALL3QbbWfjWCli3%;7FQN_~4k@i1J zX{ZT`(ys8fkaRa8PwN?4&hJDEiOykd=T!bE5on1VoQmnO*s3%z-xcyzuFgD-(M3>#eN&y(EIl zc`?&9ObMTMl9i|a9rI(M?e?&iy7@OG^jvc*L54F!i%Y1f4flL63IPYvYWUT56n0G# zGtk?sd$Y#uUW7b~Mw$VSDo_1fa^Y8Ql5BGQDUsj_sBvdsG~{*eIjemPE8)rtz4N=- z=ee}h)4{lh+gRYcrmu7v?7qHvSpNYCzGC22kKQNn%T1s!P1Aiv)Rw|^v>Op!WuEis7n=_#7eaUW^p0lUyAKDf_ zJs#|fi-k)kdmz4ci4M8?knOUYl4Eh3Q_lQn$2GxBYa9^X;1$xiy$?EjpXirjL8YL? zEA5`^(*w_id5 zX9t!3dC3lmwgvqKvp(6nR+#|d0j@15RvM-gR9#)Xp%`VS7$#j*a8%wLPVZIWJ38esrx!*V%?(1tDRh=l6ZH!g$Ynx zXzYBghNavJtoHdHtschT9y$_ba=GzkuO&xI2-foNbV!G=^#*9cEK;Ctbh`v)1TSZa zHeJVku-~&R;L?U>|8%19gDmT|+6~f_-{E_{_^c0?y9iSmxm;`D3Fo=MK^y!|bPeZj z*DW|)UNWc}=ei^Qu28riPze*qRR0+Pj1ay5+2L2T`In|?CHhyN*S2`+a&v+@1<8x6 z<`KA|w7bn1UT2(c>rat|iZ|0F;v`-_Sy(NDGkO_gVX zG>Pf>d?)LsBCp)*fe!pPN6r15?JTh-vi8e)S4Ds4{4ilH)3Q6H+*bB%aA59N)tNQFn=DbkDZ zZci_ELPWZcY^Sm?P(e_XPG}v&DL0j7y5p1h%?I6wG+If~slc+Svr8Ja z=^z(}!wi|y8v7Mc_oQkM9WI@zs3hDQAsfT&2^e{z5r|`if3D|QW8lL(uWH1eW*BA4 zc`3dXYdWj9o%JgzsLhCmD1T?#Jo3Xo+Fl90ImCJKXlp(=&x1za`w?H3Xbh}wWP+=~ z-moVKeiBPs5Mx%YC5p&SNZ046SdlEev+wj$BYd};by)XMUXqp7V&f5Lw9@L9ZFfa)st7t1Q)4 zR-*LBf!W{b92_WX6qU_meNUzFQA?ZG7AUY{3MVeC*}ev-wf zZ?8;F6U!%VXzR<^-FuvZx8HI3eG4&3cE6Hl483V^@9uhrK-`V{b0tK{*w?FoHlHtm zYtd-AqkD4Z*0}k>#nQ4cfp!Zkd~@9i5IoI!GWf}oOZBs~`+XJ_uKV64Mck+_(r7qI z@mXo=p{ttVi(RE6hoKVeJF)@8do@Z%rQV+@%Te6U_k+7l8DZ*SAGU#9-Sh*kQa4^7 zmovRo{%6;v>t{>cfPsB1r-ilt|7C{zzqS9({QqwcP#vlN(hMu#8M;7kACyo3OS8%M zKM}<`lru4`oTx66fhxUPnw^27F*H}yk&e;2Ze5cm%|2gjtV08{Lz@0lM&x!N`_5`C z_icXlpy-T!49a<(k*FX?4$D7nPAfWn1OA`B2TvfxzWk~=0K3lSwl22csz^Jv2t-)? zC3)rW)V}dBT5ni+vZDfeq@Ep)vsCyKm)AZ7pe(?(B#yd)4-BpdHDW7yMD4z&S&r(m ztiI>UznyL!Va@WQZ7=JyF1~CiSSm;3tlsPUq9$d6)?-X(PbpkN^(rOg%i+KosJU{b ztxz!K)qwMYyRKWl(jWUo~l>&FAzq5 zXzcZ$;R}MX6)B>TqV@GTYpY#X|w{S zy^4fht_F%q5?x5eRd-hNy`3I+MuL50LOSLWir=*@utx-QiqN+T&7>Jo|540e2B+ z*jKJ^#3&qCg|B<&_fTTC3yVzw9tk-OvZN-CJ?nb^gaRyLZjpHSK>onqR#IjpErM*} zfztV7x<)k{$clOTc+?oZIBmV|ZDf`Jd+BY%>XWtR%7s8Z$lO64u(Uk!*i6aFoDEQX zn``A2H{Kt1kH*{6Y$4$CSCXsaV!iJP`RI-b_C|$Hf_j}Sz(vGrI@xlYyL6ET>-_tP zv`!=MQ2Sg3fy$)`aW$ULH;eq;!T%hn9GpI%YsCB3be1`A14O(0?%6wcPq8uMmq4j2 z7qQ7c5X2LgK@-=JuzkYi8jTF&6&5$Ll;ZoA8O`N>76;$T%+LPs1NR(l%a=|3#(EpH zr_V-`gc($bIw)hDY(Xq)lMEL1x&~@?q8-okJLBp`xW*p>44Bt~kKD_sb-E($>GNuR zGal?4^J%g6AfkyYY_D`xagd>|QGlQKr^Y1(Mbyj4w@RD58|!HO?I9J+rHG#38n5h$ z)-gw@&dEq!&=Z-xu3PxQ0ZU7seDuuAUD|7%s6@uC{DfzHI?5k|?$NzuO^P8I(2P_# zFVD5LOWc1&@y4(lHQL*qTJ^JD>+HULUVu|x=C{P@n+WP>_xTT{P}eXUj0| zeP3r=lp^E0lCo=D{ZpCJ zDcyHWW9@`4;*qgY7Ef`WsGbqtDAEa&L}3YeuqWKqXz zP+!5vt5evTS2t*V1vN{M3iQz*5+US2!qFgYQ<_{&if8Pzp6bq)0!#m%wY!USthd9@ z?hmQf{REeGwma+OViVP{+27fZt;`=fO;(T8<*22yL?L+YP$v<=F5>199u1HlyVhqg4yS&HkBv4(gLmxx$#Pn>^%)CHClzC7d0>1JTsp2>Za&z=5pCRka{h(Z z^IsaT0#A_cXG_;rG$FT5MYVq9(u(Ol<8L4H+KFCC16NbN4IvGGy+*`S*_d9r%(p>e zta(qk>%iwl9tyxnlWFq!RTN%~j`?Tg<9wX`k$eKq@l9yOz!xXiilFZu5@N#;{?tQ{ zC8+|BmZPVAhb%_^PELs`HO_>v%^pBOuH9_QbbunNrsi$avrg}-$s3FM*QI^EAEty1 zUvBb|3D4*|9V0My3Y0J^05K+Xk{PXt+vKdU%T;;2lFfO_<=+57OUB{!+77g>+1PME zl|WK}L|9qg^&#bV0-iAPKcD!Vygi<4kGRxX&aY^kIOm{30V&|)4~L>@_S+W^_Z(Jn zm6(BWV$e-Bt>g~?TQ&=Sn>U7;uqegWN<6t#?g|?;p^W+{>Rx$M@BW|Cn#HO_i{y#B zQa_9GuWB8MG}>KQ26!=*Rnw_j71uUDuKNIZc&Alow$`Y&nOza`ctW>ySIonYA5J;!<9sw2cd&RsP=K_Q_^KL3H=bj*FsiY+?!5LSsf}~f1g|#0 zjWq4CW`KWUfw4(eoXnWhnq>B6`MyL&e>K(#YT2D2>u-`TscuJSckKrdF4s<>wC`Yc zA!)%vKt=ak4nm-~`qJ_@_BMXrH%Gshdo`*}Dpvemw7-cst0LB2Xv=tK#{_jlgY1IK z6)kt5_IzHSZNx3DNF>i()fFqIY~WT%xkA?*am28pJ0`g9jz`%}iEMvx@wIi;sEj>( z1w+VY^;zAKuy*6q5}%Ae{t>WZ13z9NZnQ6PUc-Fa&9}NMYDeipC-7^!?tUaJJZ9{( zeq!5JorS$zD0z!1598yiUSsLfT^B4fu)$sP0LQ^263eQ3DuqO416_7NWpV{+ya_?C zX>5X`nR;9y)hABtcmcp`GWNnb3Pt!l1xUFJSxkX3PQ)oWQPZ=NKtDV?vd1Q zHM7Hw6TngjkyUYek=!ps+&v;G7Rt!M`>#1mmnPP+%L83$)@23dnHTy#DNsdv5QA6& z?^L>(iWAwAVy2{$uABYBg)C?+ihjS%kaM|iB*F}!ECuuJ&bTt$;@Z+PQp@I$W)^ut zv&Ng923ItZfFel{6L+;hSb0v9y3=;{%^;-FqiIh_u-gye{VI7h^ zW;SFet*0CZhl=YtON_PpIo&`wgS@KJ0MwhhKckVYk zu

        V}i| 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%0D_lxzl}3RU!Mh!- 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
        lI*eNKkifywmB&63&XcnnDaj) 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 diff --git a/tests/assets/hlabel_classification/images/val/1.jpg b/tests/assets/hlabel_classification/images/val/1.jpg deleted file mode 100644 index 515127e50bcd338a7884269f53decc466fee0dc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29933 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 diff --git a/tests/assets/hlabel_classification/images/val/10.jpg b/tests/assets/hlabel_classification/images/val/10.jpg deleted file mode 100644 index d0118ec6c79604fc8f7adbec296b12aa61a0fdf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222211 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
        XnU949xtK>(Ji$SG3$>sI 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

        a7u`JHpzKT=ty58E0;LKoqFGdI&y?KM zcJ^eK#H(JC9xrMOrAkeKR8L$onx%|UDs+7BU|j92D5@g*arP_V!^d_8;)w|08P}q8 zSt>{&JEL<*;xEO6K+HsaL-R!Pf^g#QW=q<{8X2a%-62;E+%&U6^qa6`Sw%m4vl+JF zm${TwH*}vttrD5!Cld){tFgD`D>Hq^9|b?>B7W^|hflrWz3(3V&tz)}YYyogR+ z&#|Xtv=Fep!QP***us=MR9M&PD!?>YpT9FBI8<4LGDXc%?2oqrS2q~9;-W8rm`jI^ z=sAWtn{d`!o(Q0C%~hgU>@VG;&9tzd973-L5!$V7kySwY0@~#-hHhGzT5pf8`GE#N zVFJ9^E@k7y(kOD9L_h%}(mHg`kD3Huhf5 z=XtuQ<}NxEH6Z(QIWw?7Lgcj!aOKe&8FkVsrAtF2G8J@1G(V-X@Jg#@n}^+j7<*H`r6l7GRWm|=+29%sLRQq$K~5x&WrZuTL(YV)IiWR5nufNdyK&0M z=av?Q)nW4yzzoVrlcl53`L3srmO&6N_1o8Q^%iqfRgdb}pxiQhfI?TG8V!0*QWW0_ zuhcJff#73H+CE8({-wxIl-snBgF97LAZg_3@d6rroAg+}U?$=A_?+!|Ib~*8bBzL~ z?_}12>wULjC*~hNp&HL1*<+`Su7%+i#tC|H9x|!u7K}Q1233=tY@!`{rFhS_W(yGa zw1(tVmbsx_98sQ+Gl#%QDk4p=VGjCmCFgM&(>J*@lOp6aYXip(wvQ$ycmzjJxA!yA zEL9$&Vd?--5TUy#+Ka%Po|&uBWs)OG*Kf#oDiCz;eWup-;?OhCAy0SoB7Ry^aPn8e zHGL9YWRjiAIt-UR7lc4;mBkMl!uA5`#-Pn`vP`3lAj-(_wFOaz=5lY^Cfs}3Y(6_aT)4QP6`0N>BHbtothw9F9>KEAK zdj_WfJ^P&ITAu0$mZPtYD2AjxZ5s33*ug<0ST(e&jdZsg%yS{YLEulvn5N0b0$0NwS)tgtedwFvYpvN+~8X_55e;-e+@|79$^ylw_ z-Bw*f^D2yD9kGBPT~owqeHVKR?Phw7S0f-Y3f)~6g>PZq>|r}bj81Y*8OaNwVDSO0 zSps8DLu978k@#$=Se>%4j}upEdIOykDH;vIMN8s_g6o+4>Vxb%0!PYE9Of)NZ-GTcnGKnRBeL zkwm(@T$bdhrZSCPH>Lu+q>j(Blw_CK$i@nNt61ijf+jasy70gDOe8c}=gM?`g__uU z^Md%D25TJ)T}<-eDb(fkIq58%Z${E8ccXHG__fo?{$|tH2J2Wc|?Jh^3YXjlq;*MuM6$tVfCWY#m%&slYWvv-pcesp>=e)K2iq(vy*= z%*oC|jTy~~%O^cin$>AxnZyH;>65QkJsPF*%achP&9dT(TmIc(D!F7-kBj8hMU`5Q z{;i@G1*rlWNo*8AYr zBBx`$F+1M^>lJPO zF3|70$#GYjU0dbiP^?1A9E&T+@nuspnrpt*#LC0UN5r&}oJBncT0|XDLj|OYIxXQO<4>wWKmI$U3gy2$ zrd6wa(juDv(>?x+|JX%?`kc?4Nh=EjEsxRwt5XT zwEweM9X+_bTojUMwZOZ>y0oq5Y$M<>I>49u?ckh=T!0ga_+LkP|MUDmmIgzF-q!vu zx!*kap%-;_^Opdbp#tTKIkkqiVx!h?J~IqGudR{x2?}4+^^=+2g5}p86nT1SzmnpJML66dL;H zAOnRJenRqMYPeE&;xrkr^0-d~@5=doO^yg~^L(g|xoC5vPFj$-U z-AfBn0gpKicVulRL`9C#+P=n31@7KL4%iEzNykDZV2&w}zKXtsRF>B8lNU_t$HR*C zkPYhA4(_4|_=@Xb7Fp#3gW0RU2re??M!#$y#!g> z*c|D5O8IEY=yXcG;ut)&0Y9PhY69nHX)mt$&ufAS}jyzVFw`A=Ifnb|O|ii11|$Pzuka z8N3|nMS$CiMcoWL>mEZCdu3o9Il1z*0DPb$;G2LiPh!0+1abm>fHEz9^mc;V5(eSD z5yx+2L1kE|ze0aZ!Ur5SmWGbpfzEe4b#HSslXuFK#Q0`~gprGaZW#E+%~;6KvdrM3 z4d%{O^!_)I@G^#K)}k2atc72S=Z}H}{nEU@z}#v17TXZwt`Bqs^@e{Kw3rR=f6=TPEBj)L{?$4fuyw>@hKWz26w|RRyd^WT{ zW+eX>?M!Iel4rZf9DW$| zAt!Ky4&Rv)Av~bEaH!EAW)mNt1YI$TuCh79YYXpQ=gAp~)%sqp$A6Z1mQ+lm@(?1{ zF$(^y)K*&!U45W?z$9h=uvQIBteNE#q^q1%W-5fBm0h)HBhBGKZ&!3=7@i^3rynQ0 z4=uw?(68KBLw&8P)vbygApvpL*>Ovv1jhvk`O%N}>)&sxe zWv83xO+wOh=f9&B0J|CdrC9$6#qY>;hCMy=l&orY?UFZV9O+e`yL@>9Hqr~yza~q5 zYnZmkyX`E|qkw`MWTwMN3ANVk8rA7Wyeq6rcW^g}ez&bSc(;8LX=E%`VE$y#z0zu>lPX1Qb+Vhe(u?3Mf7J+PXtnsMkkTmn!vLdr0&r?( zwxZb}@JwhW%(A{l6NGkcKJpq)QA2B8BHkBRI@%b69mPm#yIbHA+}FgE20_}af8l9k z7N+J`Z4Bj%5XyzviA6M)q3>1xLY0QZ3*-Zh397^f@h;f&FW+UCH9g5gACG$|1+(YS z`uG;6!=O<>Ip9#?T?M~Td;me+`C*@koH84OfxtE~1%v3o#7Gn)2CcMi2)%C31mm{U zbD%WVU6!C?4ZlsW8ujKM0W+BZJ07Qq!6IUKaE6O(j16*0&UZi-!wxL|hgA2hX=~H1 zD9^NWJ2BLnywBwke3#|HUIr0oYh5oXRFQJmavC7!7qLWBS(YV^?a~%iBtbM1v4MW$ z3gD|&102rg<7RG#Q?K6aDSU=PlwRaTX+e_*Psp^t2N}TdoBBgSc6j=Fi_foaWS9;G zX1%My2{IO;A-qHEao#J|WoNcvv^-}BHtETF1hhK4#3|+%zks=o?$tmm+b;XzI_nR^ zG_wQbuim6r?Yf}q?)~*ysPt%GYL54J{~HsAnu@7Gp~;Fv&agvv`k_Y(ScB%9sXr17 zqlI8NM5o@aH0e#Y+$j@ce?c%KDBXj9;ng*5(SF-O8g0>Rjrbb~iQhqo1$n60eCx&~quRs)8^0uSbAa?k;v+gh$7{`Rj6l@7nah>ah z#=)|D6>&b3unS*R`P19{?p)e;7h8>7L%quoRcwb-#)Y$d#j{xP)87bFpH?M>1ps zpY|;QfUb8^rBU9;5NG{?PGb`d`r@+Exry<$XsG%0)rlxAB?9*K2{j${NT{ujezors zP3yg`m$d`*LtRBu3g9x$Huv>TK2m3~mUR}fE2@xS-i3ec_pdVC#|ox~jVc*wazw>< z5Qj=E2O_koJu|XgAF3qOW=t@(4#$fSL@g`amb(4E%ht!&0UUeWu&RqmSL*sZi(bZI zL2iPG(m2LUkopG?vO$Xz7`j_}zi}a<(li5IbwpR&2+Ou>tkMhFHf4zgKg^7+?I7GjqGL@~VmXH#cvzX^us+0+Ae}`QessGN>^|mWwgWK&Tg|-J( zYAT_ii)*QU*4)8K5eu?*5O`pU5*@x~P9|_2G`@_8gyrL<^+P_}d3}VR(t-iGh4|A$ zt-j&dxd|gJtDLAYGSPoaH$4)TG8lJk7eZjavW*EsS!N{NM*LE{Erm|B&IvPrY{=}j zixKXh*B90H6ts0IW${l$HmQiRj)nDdNvI-*AI}PU;CO+&$ks3CrThH;uoXA~6st9g z%hGQQaOE~aa&im2=i0PidFi7c#Vx$4$We#VK=Ei$%*VUmOGgSp#f@oCCMaP?Su$&v z&syv}9k`viaiF5iHSv8<6xxb6c1#9d15_i}G|d}T{%QGMcI=eNjNP|sil+K=kd!!L zdPP)$0un#)3Dqe%yv-|>>uPRISEVGU&vg!eW!wRi z5qEk5C*q8=DSudWGL2mK2?SEPDPXU;p4|YKtG*m$v#9fh&s8j~pv)&hEQQPBG$ywo zd<6}$E`T5q$N8Uso(C*zN)~C6H);U)@VXWqBY?;kq~N)COJR`To4LA7?~MU{NF}@Z z$tCsBs{GVAy|399gSA4PA6Sujbf_GH{g;wBSwG&d=mlc9LdzR{%X>9E;(3X<6FaXZ zp4%X7^DLX@Ey?ZV1?0o)K&!hi!*{zD=5`ExYpN?@6(xNc??MtcbUWj3tB8to4_%;r znVFaN?c;pPwGk zr1D^dfzuw(m>g?64XQNsslTG01^d-bop5yl%+Bb`;IIXj=lmqX5KPyxg&rohZhK74 zn635>vN^sINW_7y?GnegzLHnDqvOMbe`(`3PWX1~LrQkpvMzwn0fo*ypKot6D#xS_ zd54dFQLL9UZ9B@a4trQ|p5FWoHy_W!Y{famm|Q7}vRGH#w<;L05a+yC>7x8ffxIL* zy6y2Ft-~z4uMc=haJ&+E^JN(I*;2TgQ=$7cIM6LjD6%fz#;el7(LdYsO9lC6-_9~r zq9Dvqe8(|u^WLS^i`R%ZVb^U6FX~ja9}Yeo?wu66Yv|W9)u1=Dwn`Qo^z8n3u0#Hh zI=4$RP)asa(!w;;P_?s5FI<5QOh2JJ}1^ z|E2iT0(&{9GFYX@r1vWhZ}==^Q9Av`N%pVO;EOu^+4n=GOcnirOEOFa5Lx;kefa-H zf2Vw1z6!HjZw=axyQVi%r4`&IkrBok8uON`{~sTYtU)V^WYSF9AJv>sdGe&e&3pT> z!0k(nkD&(yX%Jt2fU;1X+n)G2U^Rt5k8JUQHpt~JI{SXM=DMX7>ZQ;}C9CJd%{yLJ z-a2!eZ44XOx_bLsDMJ>D)u4q6OqtC#TD`F(pKlEb$ILJ|MHZfOZOhSKyo7Xojd__A z-HJZr{w)2R9So>?CFNkiMe$uB@M)+UL;euLxQ@#Eio61jW|%#O#@`m=wI^xj!AtN6 zRHX77ZY32G3*k`&Q->)7Dhr4ENFhJO zqodDvS#XXVKdiuE?WR%#WXSP|<@o08y3%x((Wc+o+GFkOJjB$fG~i!~hq7O*>0IiQ zIOZtAf~nNc$06_1(kjJ!v7?WrWZS~}r$lYOyC!qx4l0UU$XPSY-RJ3lb$7_E*q2jM zB#(X960=IlIC=7yB81dRq9^{Pklz?GYl$D1Nqn#SMSZpd=oacN1l;p76qC|@D;*8} z;Ka~b^}_uYRLH~s)+&Gdcn9m!$M)%|Xz-kB+FQKEGwPNi%PJxBSkTSn;kP*+YJt_O ze<`jkzyorVA>!X#8q^W<$;%64HA}6UHPBt}Uuq0i)Rs-wr!YMRS3hKKPbhl3aT|NL=yo#$g(_Qapb#?-SGt19U zg0_Dw@?gGX4icY@W|DSn^f;MzHN%%7!Z_vnr-z}})g+dO{{oI7Z8(dMJLr5ASg zp5l*1S3igDwoQf%BO;UYXrZwyhd_qqhEP1Y_N4z_-8vG6|9F`&L`Q!ia!2a9XOT(X zva((knD}{po@F(EB%jfQi_wqb(Bwhk4@=<+xRDg}rfreSTn8XN@4>A^lSw6Xhc=BU zoxry;QsayyYv^+yK6A)gC)U8$jGd$GqqF$d>7qhy>;58P05TY6+U3=82!(7giNsa= zHra?nc2q4f6Id?i`{RtkY+<@>9AH^u&09HIRw1EOe%>ov9zd>~Z=n_e;Cb;=u@ZY~ zW1*49n~4cFIf zQZ3W)TFspd8><#E-DgPsabqQ>XCUbxGgax4M%%3pdf^ZHWsFjv?d{aR{gS5KhJx_V zKv|?kaRzE$aA0i-ym^%)6-5Zhg^&~^br93N$;#=}s!osTe3DfR4dJ-HA}Q89sJUSg zxvi4{ykCTtkii&y!ad&=)e|q#9AA7`4N|8;*0gjoT*)vqO%d#ADr$q5$@_HMX|{oU zi$&YdePhuy{;65_{Y^RD%*ViU;E1pWp>#&m;z{e1$BGc*>qz z`w(yNwW|@RP0RWznu=JR9zJ3T4?RHG;N*FtVI)|795i&kHg{Q<6q9raHAJ4~V_Rr@nCV%4q-FM6fPG%x)ObO94ng3&05UkR4Dx`djI&>16w@8 z_MZ?DPF9R^QE_a6Vmc4kQR~E&2p>SXS4D-j0>CRn0LEa}c%n`M!WvZ1r)(FoQ< zi|M05wql$XO2pfGAvYIt2gId&TsJ<*N>*HCmmKIW?mAmMv9j>VUszpg7a8&kd&$7F zxd0TJ%H;KCL1mb3AoTb)7kRmr9-2cmCv~75<#J;uxa)evavK`Ox)VKw2^|O4Ova^I z@;Noox5#`mt^^UpuT)0wLq&!5Qyn<7APDo<8RYIi);xvX4^1nx-I24PZiSPu(TdPUB^96@v0c)XHEA}tq<0zKRHJ0vu)RH70Hm*^ARqi7g;F!ax1^UMi+Kb5z1%<;}0zu^?+N0;NL_qoM+)YNjG+ zJ#jJNC%V_i7xVF2shGo1)p(D?ynJIJ>deZ|i&^V!;?DO^(NSzk(K@PGwWQ+A`JZ-* z#a)nZTx?Ej9)5zF0|}D;i8E0{C%Q|b!EUj5nM;yJ@Nm#eHrC(L<)i<5lo}~ffaq5u}ApvNC^T}-?ok~ z6z8ShZD$p40h{^=bUwUq4jUSMS|IPTaIAb`B#p4~Sk0Zaf+dqGTU$h*qacnw+R03! zYg%^xx^$$K#$z5Hh~+}%oT9Lih|qjbVMH{sb?c2eg_DT%OvH@6c4!dRPRQps1xe7{V{?7v1$+PxY8)!{&=C#Y5nZW$fo0)to4=%EvA4 zSGu&*r8gHsw{BdgP)f9-DzOV&5xIJU?9bRc%GBBgz_@lAzR?-I-ecHO#L8~)-Q@Wq zWr<=dv~8R#hSG!&+1L8?|;w zF}F%80a75UqA3eKnNgIrXusT_UspO;Kd?T_i67CAm9}Xe)7Zt!j+Vt_>z0GloJnfW z{^d9cU(UOY7f>=Z(|yr$?U794>BOkFYp0J`3I4#)#kJ`dsQ4M!A-jo{@?A0ifgWYn zy+b?IVlUoa91jBtV@Kezi*cQ(Nd~YE+p?6tqI*~G_HiNf9w5o>aUKP6Vemy4N0rcs z;3r-~8#{GLMR3$+Ll|a`Ic&yBB4mfv)V_A`|1Glz=&d|p`1wfCJ^2bp@W>0S!(7J7 zJ*H&KICYSIAP?_YQD{z@-7H>kUc6cN);*@T#D>LUxKg=5<)qkyp^uA~X_dik!5Q|e zn3>x*Ou}lvHN3cCW06*{vrs)gL4MtUg8yNh{Acb5h+9LQ_)BHxXyvr*7r|@u;*ctTrI>1cLG=sC(j#Vn zm$3A$X4ce-ye{oX{r`h1O7;E^;{eZ}``tFW*UxDm{L6LFv>xI;{<#Pn{(JM8+P7l_ zJfji1o<}xIoBswa$bM!q6r#-RI1G2!diZj&u>brE4^E*brmH{boV*LOS1fIWz)q3q z069rOHFiJY^gh}RK#Xs@*Q@*BS3qdG;@u`)#^u?2Ko>~h)T`@cE{fzjft|?+?$n76 zTA`DT+KtIg@DWjNvIt~U!Jb6*capc3liS$_dD21EGk+!mY4%6OQ@>G9{&Pfo?slpg zZY3*mg7uyrU!v)*qW6A)|4U(IP`l5D_3C8xt^lR~^ZXLmcf*dgo<7yut@gJaVsTZ5`NCJ7gIAVDV-Q-(uM@hEL1el= zM1ln5`t+BgX+`3SxzNl`$QYejAL(^;;4q{2Ky|%v1()VNsyGgdRP+2=Gm2TF^FlNU z>WBzm0^r+p@9IvKB&XmEnH!(exhvr#4-|G{{PMCmB1MtB-?^(i?}9z*OA2@zn|}7= zl0r%zBy*h^C?7VeIic*t)8bhq*{nv0wNi3V=CSeScw}}rK4a-uRd+sK8I(iOoMk%A zTVSRwng#smJX4nc{ET8g;~%9BcUmey^?v6Ilw(bX|F=JTJZ;>xS_rcoS)@9Qx+NyX|hO`dnGlr~PGo};#v~k^s z)y`y)7hvCS1aTGv^CH-Ge%XD6a__upkO3XmjZFjE*#|vTS`< z_+e=%V^qMzYv_@WmiBSXsQiS|W9-5)h2n^I%qr!oQz%&G@?;S6>9`RkJ8oD-w$nB!B;(S-Kd z-NZf73V*F3=lAm1Z?KZGPOP(Wj`gL7?m3{g^0Ky771=rGDRzSWOA$t?@L8`>_enR@jxdcX+HVl8n*Mtk2D{yKU~&SWF1ra-G$)aXNjIaq7+0qtiUzmeLib@UurPN4Fv-Vo@9ny)!fKgXcT%e zm4+H-nB)!k^Id~S^dzKwh4$_w;*ZPV7bF+Roh9`K^c!94r`5WJhetVQ3eY^MQ_gc= zspKml=m*0ysUf@exgg<7BxFV)?X!jAjqF;VYK#WDD8`7gdVTa^R86iQ*{f_rg{qdN zNK6$=cEbtagn5(q`QBA|9z)IeP(v0W`SSYp3^*^qk9n5;_>n}sl*b}iFgXfxnR4&7 z9*gzLwXI=M4cD&68CHNTby`5MQ>$%^;qZ)_SB6!Q<;8%5T{?$GyIxGv zM`qL(t`e8E+gWNsQiZm*R@l9l$i==9q}GDA`jf^=%Yu;Gv$b zx65iYbzlXA9M14D=?v&U7lIrk36))>-yd77gv|Q}QE8hlrEOFM{ms9{VtV9cw zT0U;{S3eDQ-B-_$6JAGTG7wSo6>jy5a*toc)+RlW=f_G}xA^#ePJDm!vNpEH^P`pk zf5ZB*?Taid^g3f(W1Qk>*{*Qe=1rS)m=P`%3_U$?mXP*n60DAwLFqcUa$c#NTc~)4 zSoILN=@O~;8n$~|k-HR3*Pi#Ys;kDieVUM1K%8V+CeK#3&@W-AQzu2UTs^ub)UV#*^|S}NP}f+!TKVw~`YoYqAP!WlWuNGD~^8)B`- zuJ{muno}HYZS_!$ua*=>g7=LU7{RC-K+%PEY6_Id*QS0lS8etSJ&|5lHAAc)0cOal z$)Y(wv0a$frD_@fE=zi!c-B6@YedBr4$Yn#*d>o8mB;XY_3toa->dIo$t_GJe-neQ zBx=Ia_gj41^9g>KHS}J2kYyEkwLY-yoj*1>C94Kg-x_}bNd8e6^K-$-PF?3S**dsL z&HP2nkeAfJiSs1L1PGyluY$uAwI+j1?4!cwvtVti&|M7LX#g7lskD6YR7ZI3ZfJ4# zHbmv%WD4nh5JI7>iviVICcLXkmvd>=2vp%G=ry)j9wN0JXR-XYGH4!iz;jRfl`P}* z+WrMy@`!STyxX4Oa<$c2q3%@Qnn?JK8x0G>WJk1xRDtN&I5vsNY+&=aE$ z>IecTbr@e6r4bEPGj&!Na)-e@lO5(!Fg<=AuEMnJ`p|Iajul~f&wWd8zTj6t%Z}3i zL6*-4+~Bc#LHk7 zL;tf|wqI?4Gs%e*V~Hm=)02#L+IK~R+Y%RRuGEi-xrA3m@HufkzGa3HNshMIT?>U8 zxE8F+BZXZPAU=#kxjDIN{0LaY6e9^<-Htdt|y_wCv`_$O>@LDkZ(JN}*Z^nL$oGy_qk!pe|c!)Ed3hon>uuTrMeLzqjN#{)|P z_|VUfsYp>0T}3nn@BF&tgu9T-Xr&OFG6;DsooW~{SvkvpGXzs;I|srMN!{GQ_RKY( zVU*1oz!}||QuT_3;YQfHflH*CzQKegMLwk_$gL09DgPnca!^ra4V_X%*8dd5gF2^&V>BbcMGyaT`l_!&>KBM(}HUXCw zb9kgj)T@@re;_=fW5Q=DaE4FEB;+uQB)D2eOzxj)ec3VW>@+Z$)QImjWtD`Ge?5n zH@hW*4%T+n50(RfatBWF_#16CaUWzE(8cf3)U2*zWG;IT!@TZKv`Zgv z0VeDZq{_1yN3r~a1hUkGF5>FKgn`2PI|ZE4_;GO;alQBOF3*CrchD1um}9x^0GzGa zm&sc!A=qnp_M5*?MDNX0-O80sRs*cm>ba?Q*}vr>1>H+C+H z)#ZjvBeuUkT`z1z>XU+*(;5AKM26B4s5$*%t&{#H*9LAQ`l303mLe`9LL!l2_t(QG zAX;A?W5GtWWDyMLSDCBQoVx3>kj9#&j;JwoBBk1S3ei6##mi_@u+wU)6{f@YpA1td z#e0sultz2DYNk!?ywmCn-EZ{=YpyB1DEU6TP^esvlsjUHiv{%3PZcE}n49S|zIzZX ziJ6hxc1%a;lfKzKIZ9PZ z=a|*x->w0`{`?fg?JrxsY}I{7->?EVoHL3S*H%&e@YJ!w`=*NVKUUPW2cEdu(bc`I z^oP@kLd%8gql+>{-M;Y;)6a2EHe5DP?Vpo=$dMsZVUSKBz1%_yunCx{x1FV@1=!_{ z{CC;fe|y<~{}%phj%lW2U&Ha7fUtPI-&z5GDZG{Z880y(;(VW#58irxj2QV#aYz5# z^;cK_Qov6^xebFp#?GPx!@k1TjKS|$k&WxZBVj{#7*<5&-P*rRUyHv+-39TIh*?{n ztBg#w(SlC^TAP?}`X|=OX1A%GS%!LRGA7Tr(mt&JgqxKqCDrZ+bxWu>;(FZ;vR6<9 zevf_b&>o9;!o2JURjDS#b(PyYs{4Fa<_x+ahvs>@+ETg<*2hrbW!L=q7yN`LK`&Hc zNf%T240+VDJxf(#2pZ!YvOc|gvv^#_dSpmMn2H~_YA-nM?pvGA-^4eWfE}*E16yIlpn3J98VQcPHVf2g74^zs`DeFL{AOw)RexAt-Xrn8YnT z7$I7LKDyCQs=FU+fHSZ#`*ejV*g`6o>j|C*t9<0=MKEwiWC$5BiE{T^IyyznHgaAx z@m5&cayz(O#ym;1?8&5&HaI4 zTQ4b|>{ZBPf?;-jBRe}+A0`d*ff zv6ycV+fDMGLGQhpzNL36449XXonRa!TJGd6y^6He)@+4Fjk&alr0Qbse*8<(vQbc= zFkKnI#;z%R6;c%6F6GRFUXd0oFX(Pe{lgbu&RRucB))>Ryi0si5>GqWvmkNlv{)SD;5vT7PwZJ!d^uM%N1U>tLIG zR=3Yvp)O8cO71TOwA0Id=M?TwouXLI0=@MC5a*ekr}N@5am~#8_qyC}pLXF8&p_iy zrb-X}Yw-i^UvtBu05j6)c(7TAb@O7q>{BbOg`H~fv%w+@UvQV&dn8ZIC_cp3F zsf>HNIY~8G;_i@{&`_<*wSqbw6Z&<;p~jw$h0vU_5cXOpQ&nN8tZFs0O1WX_w5Q-kXhx5Zk7Q%j+20wdvFsv`jjeNwN^iXQeb z99xHoZwf!E;nvQM&?87bqS!aSBT8>9p9n7)C3M;~7+#I3-J|>UzxaZY@09oVu%wBF zHAC;?S4B3_xJBunXa)qb+nH;(ELLK-vQc*)pE7>M(AEYbfma9hM!PC$OJlVNq$EEC zbMO^(S5CuYdV-vr(RcNyK?y(RI*tPR$@CKn3+X=K9dRceKPPBAO_hJN(e_enTTIxz zr-1f0z5fAsj8&HOulW-;aoU7f3IO*i%^pBgcLofL`tp_xCWMHuR7l+@)-v>U$mFk~ zLDVz;nztPXD<=+nqFqWtUwo`G_QInQKj`JEwF9UZy0*ZYx;vG4r*%Eb^1Tk05*1GpAbjPp)cKP=>4u} zX(sv;eEw3n#X_;*h}5KOQM}!utfyaV_a84=Rcg<)n6Tep*%;#Xqc%o`0iUE3)oodj z>KltY+cOSYoK@-V=8sJ#Lu>D|PdpdBwxc*K+@#e?sR(|Je8&0=WrH)guOCrv4m`-S zY+s!cy<2PB@`VOjDEdxmo;mkcU=^)S4GfqtWZo99gid{&4G?U71D*ARb6&Ki?6Qb> zIt8S=Rd;qy1k~_%-Tm<|GkWEiGPTXzpFJ?zU7A7)%BFLrviZn5UE)vnn2QCpk)O%` zamk#B){s-gn`>cnM%8D5HEu0@=;>UROjDHgr_3*7cWCPM8}2Fy)vd~01%%z(t0V(L z?;}18nW(&e#|IgFC68zaq(Ai=3?g?5x3`{1cxwf6In(yx5N8)wt~yxczB$0rP>Sg* zg{UfYpxw&5s!PRCtDkt6pJvF!x*O@@A^^tnM)k=vgZNj?tVipc53gtr8Hfu!dNSZG zi89s04!n12+!6ABS7|8wRl>CT;Tn)sSkkej3a5Owa!MX~QGR&L#CYVmn? z-8v;&w(EKIu8^pW7xjQQfx@W*zUx=cy1LG{5xI?1&3O-nUzd#}r?po!gCcH4o2$g8 zf(+i@YkyM2v#93fEOl26{ANbmijTf3Pn|nL3U(peGc@gLDcQSMg2er$cHfZV(b3#6G%cFpN zPsMm|?Ip%WZRc4{u=XM`k{8SAp04$|okGdO{v^SwoglhHXFVUiZkeaO|@t)uMRfXsVMQBNM0^9ur?= z?1V2oU$p(nx1_}`y#BWR$x+bD8-)PUkG3>_>cdh0cJN_B&2P+8uF*vdo2}oYU3@gZ z&h_$PYckucJol<4x3yu#OH%bS=LuhYIp3UTg#zYt@md6Y?gLw!gbIj7+%Y%YxmSbA zAD*1@i@5Nzj@hZ@+tS^H&CXu?9d3?G3xnT3Ka&M<$WqAKmlRpx7ow69t5q%xH8 z()#^>wfEgoO>NuWv7)G;VgOM<0jUCl1f@nmy7biR%V$OqDOZ^kh6 z&7ud9Tu(7`4>P{;U)|@a4QkNU-znwM*hoP%Rxa{1_wvisRTEB0OMyF8CJi!TmLZY+ z!9ugI!43W<&!bde)i$Ji}h2Q=_Z|VQ(yG>1fq4~wNt03XA2wOWnH!2 z?I$0A__qluahVx>cVN0QL{UU2$Aw`+X-&c@RtV5kcH&VJ#E{YK^Z41tU8mjQ?Cm~& zNa&G_Pq1BY$$l;}BqqJd?eT(Peo=i#Vt>?>%XH$@5~P-vV@{{g4E>3kXn1iuT(*g! zF%_{q18poWsiu}*{?cNu2YHp#(E2h)9kPG5EgGdiMXu8#!Msb+lnMpze&Pq9p;knw z;VY(~8_vG2G^Fahm88il%_G_?UyP%+?zwhmKRN(CJ57dA3<1h@ofIhr905N1! zU+Z4Zhp?v((}_o_MFwnPOc@ahp&$ly@Byg&@QGr&;Puy}!P=*XYr(lleyS46toC(X zo6R2_>)@k+1UX~o!{kXSOVs-{Ku7wr1gw`c=sToV05Mwq8eynPU0}j^uCy}Z=%C=%mZ;@^uv(5Cs8y<+^xM$!q%P?vyR++BdmqObM^PaFsQl@e0x{re-j zNnfgNV?{@k5z7kzTUhn4%R9J-{<`lnud7e=^h@Qo`6Q*LI~BJS?lN@rk>n=^#y`Uo z&C`4ktzi2|+HZ8aX?e@;ZKgtSymBKQ3{QLRP5qNSfVXiOjf_1=Tt-!Xu zc@y)-TH2IBe^ptCKnwn+TJHq3F)?7t@bO|v8jbO=)c4u!XmyvFP*+3$;&(DkROoCh zVTNKO*HK(5eaFTcn;pIE610owb}WP`S61{-J}OCk`P>HWyt*Bvb}@NT_sszaLnK3- zyuu$$k=e`4w$GJQt`%U_tPX9#ofiuHPfG1G!Nx#cSI*YTz(ZKGr zWeylO&-;J5#POVKsHQHqvmROE%0UWrBO&?^nS+VFTger@Pzh|};g1R8BhTjxb3|el zAYC1gGr#MuJdu4@u;ZR0zRwp;f(xp6DsY7A0&bq8G$go{vk3zw+4zdAhQh}*q0g9w zE6`~ki(ll;RKii|qUEB8J}~f0U|@%dciG|((KZsuW*_Znc$JhX#E9kq@ucmWIb97D}pE9gRX&p6Wh}L+6?;xP(*1E`8Xhzbq=4qd*uZ& z=d4FMdlXBwCi+WcuP*L-0U{g&ytA{8P!g2HlV@;ivjUKnt?HA3wYfKBwoo*~B_U-+ z%cUci%|VNVV?>vE%Z4r2|mA;b&mcS|r<( z<>^xZ*E1h(o#FdW^R^Uznz(7pSI83g6lPD-%bSQp0my;*U}D+4ROC9sOBNC6tND7e z$~SJ8Lr_e~mFJMHeWkpD!aLYyI-+1W(a07(KY{qalsk8;ii5fG0i|DU8|@bx6|^ne zRhWnTf>Zs}gK~mfroX$Gz@c)_h~1>J$PN?Wp2pEIbFS))ubxDrIQ-A8VQ&h_YfZqu&O>cF?g7O_rK{L3vCq7QK8O-lSe&>43%I|uVB zuFB3g2<*K+kJGgi-Hggk(QQBseL{q)a0I(WL8p0tWviGNY{5>XGwX%dl+sSF24h2m z0PEQOO!5d~l_D@^W<2>g$2MM$uQ3GW0L#y;$UYCsck6eHKumL4{lc59#6h{c_+Ne% z&$DEu@ovj?=V7Gr%xGCp;gLK%xj0+gzF4EgVp%keXe_(2ry=JFlVD1>h+G6T2}WBgs-;&D%4(&1hrW{db%mfu%4R|JQvPhzAzEiYLw9@&V{(N8ruu=q};Vp`~U z!vmSVMbMvI86%S^-3(v8A$kH9H-cijw*X_8WckLj_sipmvI*^I-NG|~cbG7?wSj0% zg}|A%HpjMh5RI8JrYZMP0S0T7J1TfS)KrFDMK+IDl=wEqZqn5f@n-{?-7!|VU=SkbG;>#+|vi_ z2J>e9;hVyjdGC-BzYeY`Q^jhe-b9~k*dSMVG)_WI3S+nC%1`Qt#Q-PiZ0SY}3{+09x~q*^??TK@cPaVn?_gDZkm6 zZ~&6JqP9l{gx20n1^WHx$J)q3eU@cPpC9L~2*U2*-KP4~fy|>rx}Sk1B1i`yBgELu zp%P{@PF9>r?qjbR2HGM3#jeX|YRjRD08a^QoN+-+&GPRp>E;+EPMX_B(YmvOmCo#` zO;$bivjk^@$qC_!yEs2H3@gr)AkV5=Ztmk?D(uLW!4(yj9FnfuMEY8|X z?mWjesy4s+7v+#kJ^YMRA1g4t%$4i|FeRnxWMSCY%@>+UapBK&GV4kNFY>*EZgy*{ zdBfC5PXN8^hg_IHkzVKBg1R+u{bSzw(ZR5C^>AP!(u_*Y?zQF2n|NU3r*1-`PUJA* z??G-a#?c>u!ikQ1yY*8#W12ck9dyU|W?B`?Qm$GYsm2?jl01@l#P9pw z&7G<#yYSS3D+hdfJLqU*|9ofjZL#NLC{(0<&I38!t z7L?OMWVAN=+w!HeBOfG-D7%BS3m6^ILHngmwzh%r3C4k*g$j_ z9_H94xK*Vl)J&rj#OR;6)ZXIxKW+X{qGX9SB4eDXXJD6!mfEktyXT55YX_^*TN26~U9Au@TAkGN(|GWk z4W6T;{ihIUmvYyIna(zpi^O}gRg@GDn0L;nH*odi(VQk{%lftN)cd4MtDe?Z$dV5M zN<9yJ-BB^i$30Uc)uq%Jq+STJUK>UX=>SB-mEhg*P~mrvC5!pDakuz|{PoARIpD$! z@1ym4l@QHJUC<3cTNFA#jIQ5l@pX{TiA6NN*Mnr1glc&;!t(A#3!l@8*Dt;`Ze|pH z05Sm-P%Q@a7#mmf)X$1}NHj|d?(5>9%7???Ieq^+c-rq ztp{N@IqIPge(N>;*%V@YF+nJ{aapz5&jvu^)~i&Z)3N0Kf?dB%;ImK+sp2TAwB_?) zN_vW&b)J+s)%5IVjGi{7W3fsVc%-xNaK=iAP_^0t zPr1yF%?9>LREJ5n>t%5*2>*FoyL?vs&7}RImrz2bh`W)N2+P{h+x;EPQ-*9U_KbmP zSu!3wcDl)Nh58XW@Hf2>;;A=|ID4)Ag&2fkIbxDgRQ`m!nDDM@_HN&dMU^NAQpEn% zRU2?yYcQWemrsGXOxAt`b~7{8xv$G?6$A4##|>ZPf5<9vKZ67_rtg#2p>CB_%JlZ> zX+JWRXdRuukhV6YS!}kIt+5XmGsUgXCe!nyjeXv?BGA_3d$fsSDtyGQ7hDo>sihan zvz)UPel*4f!Pumn!>=^VikVOi5a9^=slB60r>GwWx|%=CY~CkzrX=XxgSf9}W>JVw zQg3s`+*8B=uog{%<=3(Fv>GHwIt)ddt24vBv^XpfXcgm9|cMUaML0!U%WnO1G7`3GLiLw5upT#>Y=4r zfY}JAQgk^*aO=H|Gm$3??XP_5&bH4gDSHc+;0l?*jYXCA8No;Clpj*nW*!R*T!XIW z^?3z-ew$J?`^G>1(5B^4F4*txTmFK41Q*jTrMnobrqK(`=NvG9T^Je^lU1YgRPumBzrE(V$``U+$gvt|B zBgPM5gCgJs;aMJp)ye*%`_Qa(1M#fsfv((^_$x`ziymrwRF%ES?acUDnc$VtYHGHE z{Ajh_L-5fLlz~hH9B~7#zc=5VZhepNl0YXC5_VE;1Ug|-_9}lr8C}|DO|u1@Xrcxp zel?3FCHpeg*WZv*z~PP-z=?i4nXIKpx=~RQt~t%=m7#P7YXbIeW7YFvh@gQzJyj|> zd8I8|oR*$^5@$vAExOSKxc-D!Uu-Sidou8^ z|I0D}pEITQ4hITRZhl**Gu~4apjq zvES_I4)fRxhY>Xh_x0q?<_n(sW;!F9mtG~l@bOC+pcFL!eT9_s?VmF@?%J7Y;aiuj z?WjJuTg-C_M^!WEF}u2g-=7oeD5W4S@gND1xJq^d)n>gFIcOJq0t&Y5)aV;H#IWDM zSQ}L95`Ra{oo{ZOx~lf2w|tkl{J^qP|S zk17@c6yf`^oIRX2Eo{oFJ{Q3G~ zR@Gy3Mqw+>s6+|m>8XkktB}tbZY=LH)QSYAF&d+?`Jr!(h^~{>Vmt$mZ!b!ZedAip z9<6PKOlf$h+AUNRbzIUgN_hFVZ9A^(kc(={#(eqQ>BFC0nHv+MGtg`Qm1b=5yzQz( z%uM)*6t4dQ3f94zuyTd%r^xVWXd`MdF@wU^%2CjBp4yRI>iYe`*2M%i5svjuEGzg` zeO1_^5HE_>H^IToEi~Ef*xQ_(^cuntJ|*O^)%d-Cb~N+Hz2Dz<;_pr?x%cQA;Z|m7+5_4UH9fB2oPR?H;o%4+m5XdPNUyLy}4a8s4 zFtPso!9B=`h4jit9XUmgXU2qFtkBTgk{cHDmMddwQUw?Ef^^HZSFAe6yBa!o(Z_`V`DJieS zXf1}56O}kIH17f30Q&Lwg9d*j5AbNVX^H4#PBq;-08JUV=}0^9&1)i~aO)C0W{;0H zIW;OM>=?UfBQ#7NxUd!GM;2H?YDqPvnJ@Sq}fjf+77~t)LDI6?h6k38{ zsCVHWbt@1p5`nain$>oyKPj|%HoOlzy<%1A*DLJYP4zq-d$Ei~=kmPP`*eU}u8DtN zjP~D^Q}}WJ_qrhe;d8&C37iK|#066!F>?!m2#3i>8f3erH?!hzj zSGJ8vJgr34J{|6(#x9@_&i$*HB}svgUz72Vx$JyKz)cH2>v64l9dkyTA6uAZ_IdKk zssdN8v+-R8i+#tRtC(A1=K_4O7(OswYiXWE~syF_Bw89Q9{ z5(9>3Ktr@Xo>0T@h4i?RkbY%qKLd~NIy$W%JS#hm*C}=hVz$ubazzMlVc^$xiMJhw@xbCQYuljr_JSH zOk*y-Kd-zre>({-uTrut8BtnAc_PH;h zW3>xXsXmJ|b@vy_&}Y66V|e4q7}kiy+WI;AGHqXF){&6M=^$5BAa1Uu4}p~2;6D;G z{j0~~!YnY*c~E9VDSiOUs}p#aju`Jmd&o*iW|z8%W)-vT_$D|7#D*f?ely;1zK{i{ zRF#{}nB=boM{Ej4l3u#p8O)Kl6Noccom7)q@g=wuGH&igN-A{GugKSGVp;0=w0szG z^PDS^2Jch0n!h9w5@f+&D!czvK-rIV{B>L?U?Dm!BVh{{3U`TfGO|L0_+xsaJd6ep zkUhzm0Uu zq|rrkVgjzE|Gy1pL4T=hi7SQgJRjzXuq7YS_aw94Z}))L*3V_QF-wi6yR)9~&^Dhb zEOvH1m+x=6cHGSW0Hicq%6vhQyU56z{^?BDDC@2sGzT_T%PSswb2MRr3=VSjg|u9M zljkAT2gKh^?Vormq`3XGZ;;`cm$#x%wI$NF2>_4#QQ{a=xtmRuSv{u4i%PyJk;t%* zvX`_k{gA=Gjtu@M?iF`2E6{o4^E--@CCjCDpmI>!X43{ZUe?%#bLl~PX61-&)%xaq zl3C}|bzKRH!etYQxLo)XQ0QjMe&hI|3$HmnRsV{cs`gfsjA2<-Q89PBu&|diAPZ>I zmuFoK6=rgS>T)&=c_Io{&oxk{OY^ZTDb6u9R*6Zd61M#pA3)t47-~h`GbF-9v|^NF zG-U1mf_9G8D6y<8L0i%-U|ans$*e3*4Lu9R+ECTlSU;?h;S7tUTOHDiS<84d1Ks8H zzUx*P{|eWK0}!!a#2HoXd~}I<)Ir6G(3mqHmgwcNbq8>dDEj+W)|pL&`uHEM+(WAx zgbjp*N`!NexT66R#`+f%rY7%Ah)PO9COzQp&}aCE_Uhj@UiHJ*@vr0w8Yy>3we_8A zo?6`mbQ$_e>_ux1@=ptC3;Hp-5hy^ryv?kf7nWqHc)^*pwaOF(6_%_erkCKB`R}b| zUO50&3`4vx0iY+jahht;hoLV(@AXuN))j=83((7g^6s83YgE>i!o?CjMvKWaZ?vrl zlYB#AYUA*?#f1EopQ=f@)IZfTM`D%06Sw2EXK3V*oaon|e#eQ_KPSiHALVk$S;^j9 z0?TWvg7SU4E*MG0KE0fVbE;3dpH08gs^Ms1UCa+@Q#Q^kPq{|EB<{~%=_hZL_)w*N zf>q*z7jwLpKF#6|{j*chQHQaY&N@wUqO9rk9$TUk^2VtXrxP_UL5H=T0;e8_Le27q zu%vVKZ}Y$2@J@U3+MrfNu%`7*LPttO27P#xUThm+@8J44fkZm#e|w_+BVzuz{;y@` zKZpLOS^f)w2=CU*R+w!w9Dp5u7dLi z3_iZBRpZ&|kL3}(Wo4c(dfo{}5-n*-gWL~TgN!IMa?W>pPec!FG*2t4i#uSl-)Tjv zV(%0Yz{Xd614Fugihh0AbO($Lu{7qzNi}Un2kf|ErBnGX4o}BkzQ2$I6g`d2oIS3^ z_icrVdPbL+E3_e}0{J%!`AFTpECkT1-*6-rBcCu^L8jl?_t9mX3tq`QH?#a4s?pL! zC`|E^&wX#(k|MsPZirc!(Po;9-qz(E3(;^XE2M>v`BTgo`w_5AlS6~vr>Ez36FZVL zD`FzhGr3E(2+?s=_ySE9y~8Z|UB1w36?}`i=rsu_6+T1~&66yB9KPtyUObPL^a^QT z4CQRlwf4BVWDu^Ys&eC!?#X9=Qq8WvU%38%pN;v0R{w-{S|7b7Dvn%TCNE5DU(!-R zt0&gYHZW8P9JS&^)QEuVt*|C?&(f1sm@`OYK{*(fScAv_R`8?Ys|@qm$mFw`a)J_a zLk$IzCmK{Plh;U}NpQn6IedCC#~R{_9zMXH@!6j+DQJ*3QT5i05`LWX zuJZY~_2oz)1PkG{OWC{zICIVaC5emN=*A3Nc$%==al|PRXhc3LjYh7LwmS|w3MdwX z)_m4#E@?GON4!yrQg0uN0qz$0LJR4o^H$YN`_r8*Q^WKAI$~15DTB{R`cjdXtir|1 zkCDhaEm~LTmmJTE+1R>H`ll19w=boQ^H2f@2jW!uR)Q(gsnQ}aFcaxnEpowI;)^N3 zccTM;o+Yd~un@?|s%dW=abIA0-I}%)>U$C;pY- z`p3I}7f?6SGskDk?0qPrZpKq`XNbsKb6ncGa}7ViMms`7<%;t+ zFI$G}RcwbXSZm+}&h2hsSbmFEIiA&?wBi=9$r{>Dp*qoU0x>9)2AF(x*;cOMd=t0# z3FG9_=^oe}fpPDor%RtjNIv+1jTfw2g|{S8o$#{MN5gY*bcKuSdMSfMf4$vTy-z)O zpY1Sw4QPmWUgehO+}O(|k-=Zj8nZp(z9_FJYaugfBtNMgbsMM^e#-rfSontCh3JK3 z=4Qfb?rFRBVasyaM9%d5i1hzozUqjGw^8?wRpzdP71pBVwZv9<-vOUO`ymT{}B! z;Ujl@cO7$)Xb?J(;9zRaSY)&&!TqA@W9Ri?KQqz^mo8>-VU1JAibpvUN5r(z2Nbz4 znH(GHjZtBraQ1yFnx5?cywaAR?*_fM5#z19T{G%>;0Jcoj#tGs3fT)8lI5@5-QWN# znJCR_gicVTpY;a{TY=lSi{rJdPQ6pSsmK=P&DN_SYpwqWg*pERq5mJDupv)Kny9{bntKdz?!rfnHERm+{nHNQppnNOF#BY7!mt zJa7QK?9obSS~t)>ZduZ_sKllR*B>vRGa_H&UHz*Hd_lNDF&9FQD2^mU`mTQ55b!c> zLdm*2-r3EaLTu?XB-UUgOlcd}-8nEKN3l}q6L$kQ`k0r&%xJeJMvBVl&x%Z;vd&Ik zfm@e>j@I&5pCUB$SXU(gud=~$74iZrPT*nd4U30s8@iR ze~~DnCTLd|S;DMo%vQw^qjMjB({is|1Mwk=xig=wM{$cy?f~Te45g`Nt&+$rdr9Zi z?+^B;`=Y$aq*eaC&J*lQf`;6$hzw8s93l=t)zPH9>TTpl$%RAM36x)OzgBLc9z74& zFeasPN|7=6@M=l!{L~xOj+IvxBwwV>sjpt@*No+2)HyzG*-s4(=4%=>2+{9b1XlXB zN$ZVaXEMa3)Vp&zmm#3p7>HDqas4=Wk274h+mae;yoVM@N znqD~*oCMKMe-#Du6ttl_f8jM}9+tJ)Wu@Iue>O?ha(Z;BBx(R4Y2?cv?)iIv1d{nN z#$ON-EAertgA(sifKEcdm--%`vCyhMPIK+jYa}8ZodzuYRhKe;Od1m<4`Y`vj&wA?-llwq~ z(&xbeNOYPLrJtK_LSJ0*s^Jbgjc?iV80Ki&f-YTyClS{$rh|p1m*{`RZo8N=ain8I zR_c-TYTXz1VN#PJ2qd1mCm1V6N0S3mxpwU7F$PIIaB5!y_$t(O*cG~;9XXX6A+9Ml z$oxr-Tzc0loo}-XsK!D$wZ3IjEa!34dpdaw;!viKj2rIg4Z%^<3~fC(Yu7vgeNq8x zQHWcUg0}m4W%UTKGhgp7)=#h(AAnM|$TFnIK)KSyF0waJ6yynzyrGMfELN9@QALq2 z!qs-BpupLxX!1p0vz@6sz`vC+&fSiCn;N37lU`y*Ejvit)%$*EBk~>)W7LBvDigtC z;a~61cOengHajgqoy?prBs=-a<0gCv=8jIB!XR&y;GD>y^Mb(`3^+5F3X~Qavo4O~ zPWPx(dTl7SxeA%sE0l&FBSUDsg47+tUac#QkJ?(&dou?;(U<*XnKKU6w=NpGps186AV_aPP#{52P^5QANP$qK3!zAFR=R}HdqAXwl8{7tM*-;& zl#l>Y6zR&?YI}gwY9OLBV;^a8S#l^+VeT?UXAn%Fe$4?0J3!D-Z6A?czCL$^- zA)_KMA$37oR8--z;)P49U@%x*UQ0()Of9eaJ!mW@RUz{euvA0U2opByazY=HmKSXkNEIXJnFaq}EM>d<@|z{&pd!NJac)cfAibpSgb2fw814Nd{5E0lD+rZU)ip#?Lt|5OOKV$uN8gM7fx)5Skx}{-gE>7j`+Dx(^2+Mk z`o`wg_TK)%=ff{wzkUDl4_qt&wtse;pg z*;%GGwAtdHW4f>pjcHD;7JPOK8$}w*S9m7%2fXHsSx~v(D9aP}lKU!akmLt^64Of( zlv|6XS`=AjUiWgX3YG&UU+afBxE{c{#m_|)^#l>;E))NrY^f(5=j2g;w>jkw=k6Yq ztA-jhRVGZjTE}dCaPn0o`Rs7?hiYlNMkH+r*m_$5fV%2aqGT^hnx+~X*pm;Q<|FRz z;VWc&*(j*XT#zCL7LOkc8_+x8pU)WOq>lYt6Ae*x*mgTlP+TP{(bu900rd zo_f}sdS-ejK#SrXCdxt!4pz_Y*+qs)Q?$)^LsT*Le?XmLpL8}?)q^ySd*xa})0Qsn zz_@#KyjP`jxC#d!r1bGkk!Ux7qNks-qZ``LJV|s$iK;p0dw<)}tKHJe<;HqzyrsK9M%VRjh@A$n zWoU>b=^M4iU<+p&fkQ+Udl7mEP=!|8iw190&v#)neI8vyXBu7=vCGAH)*vfw4IdW3 zN9eV2u-YKF(kZzDOAccElw1!}xLUwcu7Ip{3W8C=6lg+%*@{s*2A(pGI7GYq?_#^?kttUC@H0ckJP+<_Oxp)Ye!Hxh&(MQ(n18ib5hhUA_#S0GW(;zn=n zB~5mOyNS%4uLx=Bb-Q2%P*RKhC_rlm9sd3~uz#y%oPg}aWx}*zuZIfh z9Yk9%m^w~|=b`Q!FJFuafHelAu>oY1?s`oE3GCSEriUK|-(Q)%w1sYIS(z9FiH1p; z>#4K!R2%bgHsfbqf-Fu0qHWi-=!}@t%S9J%0mIx`Gb`9_*RDv7o4RR{E}QSq+_=Ez zv)mBiwquwYmFD&C@R{GeElw)g-cnyYSeE+;w6{Mnvwv>EW@7CLA}A} zvH*BlpsFc0&kMwpuPHbgW09rHp>H_Ao7q!?i}N8hF1Twmg>UP{(Yz)}dT*^R`95jS zP#4Mi@>OfSVSEi=k;6RWE{tKcTn)0X<3jz$(EHBO5e^ra+HT=hq2UZe%W4%Ly z96Mm%Dh$b&9H|Xycf(K@@Krv1f^hPsoNvoCd-Hq0+%g+7@uk_eLjmKyrr=-UwHb4u zFIDlcpYUd{B5@ixRg`qHp;al*q_9bhizJAT`z`9AL~-Zzj)fGqT%e_b?=-WzS&tcf zHq2b#n@P_HAiw(=+jWF%U>P~fS7?QeXYB&rF$eDr%}spUm_iwS?Naj0>o;8Tb*mh9%?0nelD|e9UD9h z!q!9FwDE*nQrnu<>R3uvIiCJ%oRHM%ReK#Qtav#VBb;sE$z28Zz&2d+*)GK=nx<-V zK_Ibw3bkKPpMZz&z0ZITVAN~rL!(N8diK7@&F``}DR;7#ca(DId-o%*7RlRKhGNbF z(T4(Kkk(6+YuBXXW0`|`@s3X@(c;Nc3pDKVN~_dTaReEpmf2a2GXV8xad4J5pK~L9 z4PFvi&SDUtoq=>@B5p~?%~B5ibSuO^nyJj~y)`n1FlP=E%lud2`FsJ+xws6ni{I7l z1qD;VjW}L9WzH}Q1e47xZFP&S{;AG}qAVkBShi&fbYt|-iGP^EWAF4^HN);OZ32Bo zwm&rnVmwT#Ux$D3P-GrXt4;JaU`AiIg;7dBtLLp|wbkSVKt$7R(W$#ia);pLfH$0S zO=lLdZWQ}D)(Y(NFM$=6{#<2~0*~()ae^w!bcFZdF8N|6kqu=#UdoqZh{{o)m{sp{3KRx*uaEG7x%llwdp}f1IO`#kM_;34fAN>FAf%g&EKvy{X zn?tEF{D#Fy`6pLvUIgEPts)Itl$+d3%nSy#)j~tbULNZv_3LD#wyHspb8Nlb*9%5J zF+rEbd^Ja~(97Nar`OsW1q;!M_c6z#2H!N9U-`joEmO9`Jbme)VNS0z?EAQb{(?F_ z1Q~(~HzEy2rxD{$zP2=^bFQY9taubX0GTEe5*=PLr8Fx!z8X`=pX&s8MxWw9&R+a2nWlA#_qUnuahQF$- zdKH}g=v$>CHrHeTGV9jp9WIi@NgljGy84NEA^5=7rT&4pR7IAmxoBZ^{cx393RN-Y z4ugJ3fbLH<_sxh1Q8b-^AE6@%54m(N^B5vtj zF-8|YRee+cKKZ!05lUch2o=%We5B7!U|>uMuh1konUZljQY0T_Z4DngiQVsUsO zq)i9oXfQE-{L>xgw-3D^9X-1zSMZF)-K7xQ;Tw#hz+*e6+HWtS2NOTOd7?alqCog3 zd4CTwpu%u9i)oo76vhU{MwW8LS@B}i!Sq=$i^jGd^W?S@qp!b^K)VcX z;)v5`c7|`kizs#p_4cO1rXqW-f(LRSEG=d_v-&GpmQC28d&4#_nkI0oThrX`iWh%O z`IHC3v_V@l#OO^pV_>z~Mlm}KTbe9G1qpNUcr{kM>m zYmOv%m5}8>ZeD;`vfeXo^zZyMwL@HQ0n6*s`=C zjz=u8P=t)$SvH#I%BM>;Y@~B`C`ye^JB{l^k_jVz{r7pz`IaAFxI1-rO|U{LDyg0{ z5k)LmFKi4VVOHAohQb17OLDVMXn&^o-pt^UHe2>;J%_1vF~vvbuaO!@(tN3cCiOBC z+`wz^R*^T_78WsH?fsfGzpYIb+furtKcvE6?RPDZwiRKgPeV6#(>|!bsSjin8tXTD zvGY7yCQ$};*9oUG%`?`6>gBdCvq4n}yfpJ<<=NaG1z$Dq25Vqtr;?4D2SfFazS#RC z?qlN8yM@AofT}jL45~RRfWz5F6`omd%N@A`9Y-YFKdc!qh4a%s!q?h|qO~5iRK-~V zb`bLIO9%FJ=ap_-{DIbboJqNF3e_5td?y-FkGKC2Yz;|`lDt(uIBBr&c=n@((R!_P zG-KE&INx1QI0$J_NZuT&4f4uY7_u+{TIpFj6@6eD0eu8Bp)+6^Er_Z!#(*{P+^IyK zZgz=FnlKcSH(h99Q0AH@9l@JFuO8L$JbZ$D#c}Ccd=df zJxTN`mQoRgu7aAbUn2J3NLKwKiQ^ur9Bp5T6>Z-;rp?-k6<%_sy^T(5Clx_DH{a-B6pL`OkGCd$)>!|Npue|Jr~ zkm+yHk6-twdo8I+{SaovS@&_pkR6-&0evZHxPZJfw8Y#=>eVFI=Fx7ALZ3+AWxCOw zaG*vU-tZnyDZq)#y?M$Z($<0Z?<5=Y53e7HG|uufW&Gd_lSE$=uKY{` z;Q^GXc?~Dou&1gdel`Th{H=2>2od{%a#f-32D!LdMc&klwX}NC0>}$DZIMS;L`CFE zLnbXZF@=JyQEuv{!bfACD2!&*zqnNHcTSn|3`ue{ZiRHz^X3*t7UL(b7Ph!ItS%E; zn_najTpxA&t%Q8TvWCM>`|8=2eDp5K^vC;wtue=NaS6hVB`6RC-49>P@mnxNU`1v@i!~q z8GBood1D7opD!)WL=s1KlY`BPde~_vVSU~M{_Y+}k@_>)0cslB@t;Swz9UEi9v$^Y zyuj%jWm-b%)^4Z5^&Z%w%03-_a+GZttbWqPwNG)6E0UbSYQ`tpbHXmWPJsvvqI-y3 zc;7##n~5wD?!12A4z+3eC)ZW`;_si&d*uHDoSV~F{gRT60eGjWEFMx2v*_uX^`GIk zKfi7k{sr)S27jW}{5-IDtH<@3IP+pI`q21x?UOAuxjkdgxHwg4$t&>G-HS)Vl;G9a z4#xYVK`Xy}>1Pe6@+%wu$UNDxRe9{utt|P=Mr6`+Gaq{k|G>lHFW~;o@dpVCbmd;I z(4^VSms`%o%KkGSdQbn5AhLpfGubvl&&v4kH@s9Av25Q87}~=`PP*wR%Uu7;j79(- zK$AWv944O+00VU{c>TiX|Ka-!_!0Qz&-duh(F#9OuWudS;n&{y!?$v@mB0T5e9R~O z1;j@lG5c!|ey;6IZdM@(LMygu_6@HYQI{GIU^!{=Yuehqy?5e1EY zj#h7j)O`cqM!%6hyaE0FgzvX!33^T~@9HgyaE=g5f8p52jjW3?jy#8)Q+^H;TeIM4nCJjDG4fKr#Uw4)x4l@ukaJ7@ekd%ZXO8t%#O zXzCx4uA7}Q%HJ}7D=E6jrPvjeL>Wd1L`HvJ8E>)&Y>nb8t|c02&vQ9O9Me7O;wqPd zSe`&)7qd-Uyq2!GxIBC?SbF8sLXZ|)3+noGL+H_UH;+lw6SH<`X8rG+zkpQTFDQjT z*Wd@Qh5sOm;`25uWqxEp&2GSMu=DkDW)Vuuu?m3yV#XGzO0yP*$Y+%opXTY67BXDL*$D}5t0Z3JB^*6!JbH57)^LD7fd|C>mNm0# znKUNn3%@ORL#CK{HEEf$bcU2)$}-?9U{@Gyo=7asBewruv*v{w;m7tFx0P?SEvGdV zBp;6`yUAH5GnJhUdqwrWA&XudfF@Gd227}G`j*HZLdKNV0GuZUIDzR@vD8ZOdMxrm zdpb4E!o*$XGv$t5n%#fZyIC~a(|x;H}$+;X)8zRt>Jw#gfN zGPW#>5sj>`3GFM3cb6&))uQts#VQqPgp~Q)-g|r5 ztnrc4H9w@9hemLtygasAp0hPfD1&Y7kX*Cmb-MsAXE++AwS|;)TC6p&FLlg>Ho2Yx zHi-=ud@BQ9V@+#5VPO;kuWSnnY1KxBfxR3x2LcN< zZBM;sTBnMCdQ1Wz>-O_E(KI2omb)$qpEVkM`^Q&uN>m}II?69?K6SmpqwR6On#W0B zBk9cP_EYb9sb!V{`r#IUINFK6KuEzU^sVR$u@|k;b-2N!y(5=mPJfI|Rew_%z!5TP zbix&C^NfSv#LzYiNsY3g%M8>-zmoY%GMvS$9cC9!@<$Pt8$NhRLkKl?AvV^2g!2lv za&rZ03!+SWdAX+x^ACrxgjd)}i)n=l83KbN;sE6t&x4cT!7x%87GJOWOlrvyLODp@ zuIRfj>fP8UK(kKe6$!evC$6C#`x}JMVX2G;)uW@YugSKL*h4%5L!IyePWUv)cn`^1 zWi3tL^}~c=j6DAWnqqEr_mJ$X>V>LOk}u3Gh>W#$D3@UD&6XQCTxsYlQjg>^>#Q&- zP8z%L53e;V(-wtRu}zqa;ZPtDF@OUy^J zt(+y3wLB`fLrU6gvj?Ca>mBD(T%vhscWgT=^|jVVK}mG-yaC!q74_aPf-ln{ufx%u z>o!DjJ49!+r6rOLI~~hg8iZ2b?GeT1*)avDTSdhDdV|o(n%s^bixF)Q4@ZrO=3;_( zW~I*XA-QO*lw^f@{CVdV(9&QFa;y2DPVp*=g@jW%{@}8uvBW*NZ9(XKK1J!@em-BF+8NH00GxRizIFA`G z+E?6)v85_$oz~0TU9Uu~=MK-?#2vtO5;KXu2Ye2BBMF@KpVvUg_;N z%5=Up*PI!mV!GaRv7knidf!KA1iMA}tPqvGPPwOHUFa(X;S#1eSSSjS3JMwhH_~br zhT2ol9Eh^mW}L%63eZ>5_dW$tdzj>}Vs8vYo3@B%J85*Vr|^&SSwrF;L>1%cZ?`2_ z*-+XCUG!oRT2JR$n?i9&vq!i~CLFxu-*X1oxJHuoR7e}BS5U&g`y*#2{-S03`o{YQ zpSh@ykH}k6Wzb*1=@$=Nbgh5=zW68h^QQ6FnvMtj+H|bKQri8t%8{sB>Nv;ieu z&G1Ir0>1Hl=CQ%A;nGx1&dzUlue+XoW1R69(DL>j_BpV!OMJUDRflQhTO}O5?S8>_ zGf?A)@Pk8*6Fpx+CckJH{de>00Kqg_F}BOrV~|;>lWL!yoK;uh#L&C@`-}H^o=j7Q z-i-3jAUcgU~p(Q=7CP|asH!EF!&_L_hG zPJ$p5F7fYVaV+VA!6!^b>iz<}y+|X3S@W+HckvIY&qt|~u;r3XpXT~>`NZ8PAHelb zyoP^cil3bg`a@c?Zd6eslsASdK1%>)Iaz5uIQfNHuE1OGBdL%zQ+`fJ#vC)SzX7$cc5cZ$Vi`ky;r&nt0T4ScqI5XAk zexkr0Q6I{(#o9mDW;#4EW2C*9qYSyfcNi!zL#fdhRw`9jl;%1(8doY&__2~4!ua@E z&hjBhErfa%A)mrk>GqmnFCvE-)#X(^%SjnD0!>|Umn>)S(JQiE&NaFB3NDw@Z^Md= z&b_{4Ye;>=Y;)r*_vWhP4EDkpWAmHsFRe6R9wmDjj;=0O+#0a=jwF5LqedtR5)9}f zxGc;~g&($gtap$Zg-ssF-W~XDYhc5t)7?s-59&1EagRBDA{PjcB4Z z6lV?1v}D8Aiv%G8*CmzAD)0rBmjifN#;t*N?-47tY?bE6Gc~J0zjDZm314tFSPNfy zh>R)C@VOkhN6k>9&*FJNLG5N2;!fxmYEqLtueClryi7EoZgL7#x)#ukD|{&_Rjaj3 z8pPb)vu~8sJdg2MCnc^|r`E&FFZfq;)bz)eP!;@!Gh{W5O~@eKzRJ3`sq==$Umjo@ z2m&yGV#rxxLCeeGZ8}X(DDmjT`tpbW|osMB%v*CF_OOU047Cx%Vau948+vS8CN_##fl`iB++Xu zp4kte3(E_+1^W~t2GqxC7=kxJ678x}uo75({B&%C-$*Ck zrjOTtzvTqe0eQjD$NZd7FL@!rT6shY2S%``)Zy{Gy+P1L#p9@Bvuy@1JO2XUmLX?v zs^d4Qv)|>~iE+Zb0ht|-WW-f4cFXJ{nWw*lXGesAJ|Z&40zw;JMZ>-)m|!Gr>G;laI#Qwmg<;TSj;ZMA%!Ng)b&lS z4zG2KtwJ%*GGxGHT$8l_95@CJ>F|ug6B|9IsIt8f)2a9WbV>jC@|*8M3B)~{JAqWr?P{yXMa2ZMSaoj7i9W;;UKWrC}-5hWcyyKwt+ z5GlMb>>|2;w)Gq$0YenCvo&#y$ISwrO@pj6+eolxu#eD*JU^zEC_XnvE zw`Om4#%i-zn>*GmQ=_ihR%+}bhRbx zbXfI-ce#W8Wr(Bj{?4^mXCPDoK8BWvjN8q7Q;whXW;i?UtO-FfMI~s(@P$&7YtFgO zk;V|JJB!j4mk1F-+`L+4%S0sk`--B|u@oiRtv7nAbfN`qxGxx^5`vORDFI!V*{BR9 zdV5!vl~r96^KF}|`pyyfMFBAo)^X=_d3{b?!Sg6No0+RYBam**3sv<2*}{ZE<$|wl zarRk<6yJoUX%8aJJ6V#Ga0=@+WpV?k%&)ULTs0W{dBT)fW?cY>luou+r5T0>)$z^f zxM(alTl}=8gftTk&VD!sNl-ku{eEpCgyz}!5wlg*#L7_4QbIGi2R!HW1`Ml=rA(*1!dq*kJ$>-%MF(B!8w1yDOa|=uiron2_l7d< zOdKox{UfSOsMnWMpXa`-_4*5_9uWVwGE?DTz^96R_e`bcykPUtufG8KKQXd`4<03P ze3Ty8EXN*N{Z|Y+{bxVy|MmR2h{v&{Joi^D*(gc+2bo?MY3FNSZ;p~wgI{wuwJO3$ zjdcjvcr-xGH~g(>{3RBVTPuJczc~WXkL!VmZx7M{V80 zla*CH4-&3(UUsNZTI*#1N5q*w!U=%^az3ZZf0Z{Y?d2jIPoTZa%j)G4E{PmZ4==ha&}s~_v8CT@T7 zfmLpqXi$HryP@G{XpB{PE6!akGu!`zAk@$%7OPOo`hU=lt;H(N_|;3Nf60eDzMiz} zHG=R?|H=EC7c&V}Fmfo*!OyPCe;OalW*=UeOq{Z7?T~q7+TM8W1J@yN6O0t->odxv z$V3_7ukigO+KGQ@r@^wV+SBF77P!K+UcQ^|_FA!MXdlU1RR6uU^d~nr=v};)#=((1 z#dGih)GPaTdW#2%B5<&_Qp8qKkAcLdX@unFfpI$Oxh8OTdsHh6&x6VIe7 zzPlw*4k;hM0$S+sAJ@OimJZkYeT50*$vP&Hb__DHJiSh=uFHWMN*=L$U5@`?J3lH3 z)DBEHOTCGIe7JnLMPjS9s& zRPl~5EYoDdR$6sE257~h0w9A-(t;cHOPInfFEL#Jhy+e#Ipf)E<8_!h~9%HPcJPutZ(}w z19A(mqiGdf{4&JkK})grA@8p~9N4-R%Q+jMD2D_-k!E>nJ%_zbtF zQn5-Z!m6m{?4uy+ORY0^>o8hN3(eZMAktOH`runvvsF{;kTf%bJ76**hqGpDAN(JJ5V*%vA2}dFMcMW^J8z}(ii{ahN}z-1XR0d)+S%hQ^jsoe0to210?N}$3F5d z8Y-|@8YE{a7zRd6$KUScd?KGjm1yQy!(-oY*-hM7uFF!*x&SU!)}^m9phGfKg2L5f z$8~32ZG{#KXFX{9(~zlmwN`7Na+5;j>}LEmMFxahckSS+CI)vbo%io9?7=;(Rqi!U z=OymxlJvcrIA-#1BVGe^F~#)raHK6*)`xdImz1(Lk3?j-tWKBS+iA!#^}KyO4o^-0 zxjRLD)sH!M^Nl%nJKkb!{XSDFgmyz)RusOm8ZT1Kzw7vLZ@T4kltIWX$f$8R%b>5> z#8+QdUU!8;Gx^AX-XVx+zGxUVT4}jI1a;!ObMu&{P}`}td@T~MJovjP{vi&(o^xWs zbR`GdD!Lj$I@avU1&oW4!#eIwXW8#7U8PCZTluJg1j9sx!R>P8y!bRDS>hyax;}Xl za6cD@SYm#$0uja@`g#hKNh=T0U`Co%&)-xJ(JT84rTXt-jNe%rilgs739t9=Gn8LT z)!VTRb2nM;P~0_BtRp~aW_<~45U)x76`-c5tvm3NDC{!M>;s6Lf?C9MQefaU(6R5t z6&#E&&I&nz7BD2>pc<@j^NZaYK5ETs>VDLhi9}#4PZjNe*%{Oc%=XaG)^D%Xn_hq! zo{g>}pGNv6LZ*1J(XyI%Hy|Ko)0`M-Vyx|CWcB; zEYePboQROY@0J{*XkHjTb9}2<*$#HXqNbd!jge_&j(8#w4WBenw+}f-uy0KF2Htzi z$2d*pFj>lk7y1aJaHbF=>0~8mhv1%KWJo{sNQZ8jfh1|`;%!ylD3*9binP{yN)hCg zjUGt#g<>Rvo?o*bp*7E_B0k4QWWR^?7YGqsX5gM>7=!Sw<|E;Te2ksx3uJfp=bCIW z+T+f=hL0-)7=z$uW2m5E+B*B>DuP>Ty?NCUOP-ANEtl{rxJOJq)r^UfrsF@Qn+c8Q zCau>(4P!yKmAEP``A=Y#OUE?Enrt_M1*euBdD8ADw$CxV;IF+1t*sXomMMg*Z!3Kr z>^W>^DW%{m)5D-R>~ykrGwIW%3DBmnvm#tsDs^p8aOPr-#kgf?emy_0i{pIO5O(xx zUU^7}^i>fDC!G>q6H1xK@lcAdvX0ToxowMSKB=kD#nK=rV0X(aPQ@>IN7*!Q@>OHA zLX%d0_Eyd(O|oW=3N$aO?mLk*W`w8z6gKu z=gCnDRySI~?9j~&T>>=}L^b&xF%iIj+yCqXm?3*(%j~QAFYonGv&S|mCZVyq>Za+4 zn|l<>b)zi(KgoW9(d}E`@o$8pj!30jTClRlI`nZ2S8ehA$G&>JQnJ^(lohoBn$(Gz z7obZsu)OOFR$LnnXTBh(<4)^6x)=0vW4fCs>VduJ)IhkFk`C(!=)ME_SkxHr^J?_# zmQT#C$jkiT>Q{Fwe)$Gt-zT2uJJ4D~T40ZRE9d*EN}c**jLyRcIbzi3A`uJQRI()}_`8#0hNjKzp1FIbw+@A^3%6lHVk7 z@CI>(eM;}Zc97$0qk)d=BGTPmXpTe5ja;=5VzwLMZ6Ncdwq>jR;ec)>hjX)1mk4k+ zL@femz@eG2|J5s{<-4IxTQS)fAI>}IWF@#H@-m7IVV+Xaw#t0aw9##D(Byo;O8@?P zxhCp05qMy4Ld)lZfD|D1B2`8Mp7%^Va~{F_BtRXeuWByr#^iZl zZbw>`Gc>mnzJ3%#G$SDW)h>)kqZ`W0^+0`@mU6JRw(`dVqDbPIgl&b_cy%kmVop$% z;vYNk;%rr6kgZGGiZ{i6recJh1!XyLm3eLi@hTf$q^J>s@|KekdC}3xV@mm6a9fkS zQ!}PG5PGB0yTzq5DvWE+=e+khG{^{HpZGpDAF_G$4@ zg!$7``d2yUG$|BDp7FM$}1*tg+gM&59`?38f~W=^?ZA-w->ZZLhT)yvB5_UN_l^j z(t*Q;Em3L6Ns9jAwu_~|oG7cWN3j8~>wvIY>=k71 z?L`*55&mK4VBRZQ$3+spgTEy~#?&=Z_EZe*-~c$^c~BseCasYO1{5dmEm~} z?YV~%^VzV$wwj)G0vC|r;m>J_o4)sYE)euO8;Er6T5pD3k(NqM38Lw^u@a<#=dLOE z)htRWs1CLl&WOP$P=)r8+FjUN zufK1>$-_RUsdnp69exqu<|jepKG+E#q48G<4WL&KHA#VcC}=IG%WRB>C{Mk}dIGK$ zaz##|E(sK5_JWnb##(Vdm;cqMVZ^tCTuR77V z+pt`jm8IM2BrmWSxCv6t(;Vu@EYk7%fx^evH>Hs*EU*^Q!SAdUB&X2v$1KpHPj?U0r zf}A{Yp3NV2 z<&oX4?0B<3*gDDo;4k31?_WTvPWUhzZbocRRqtEy!e7AD++V=vDb$VoU*v-B?`xBe zlmi}{|B(;-|N3*)w*2ro2woYx`6cOkF5GQ(H-63`hcvEnR^Z`i5&SPeAjlyctydVt zei8gQS$OEPIC{(P7x;Z*XSVmB<=_Wbb}Hj?zFsPkG+0w^dK3ewwUpF8@!*CN@JUiY zdd03*RfVd0Z{^g z4dB9~Ky4c^Ho%>`9axS9#s6m!*#ED2r9mCJo+qknj*@VQnWWu~kw-$}ze}Fv`a*60 zN9c6r_TB3dJjMNu1Am5JZaKam%oKi^Td)wWjXgB0ji^SyNq=i_{K4J87kktzZ3<)V z-fkib`-;08t-_&KA9QH}IievOi5n@(e#?kITkoFt%&12YjMstQbKG1msy|RgC~gJ6 zaJ`Pqmy<{;`1||)`H0d#vVWS9?V^rOg$+KtM|b{zJQgbU4IQ~$*aiCtq`8kmMKN5& z<5S$Ut0G3SUcmFbc1rk&+)Vt1n~FTDoK5~!64-KE;00qJ@@xKNhTY34{HDJb`$=D1 zQxSWJL(7aE8$Qu(IaH-L;g(|tOJ&+h@FA&{e*-MMkGZvQjhj^ND3TRy3PEu}ic{g1 z$Ev>8)|qG9^Q|Jr!X;+FlWIn7cNuZ`>T|2i;dZj4g-~UP%@qW#z3K`)IQNWA=yItN zYEiT1MCeq#WsVc3vC;<|#x{T+)Wr>@m3i-2mVc?iU1Lsh`Uogyte1O0$kVUi+jO@! zhZ@W&jLjKpnF4*@4d|hOG2r8}CU74FF9IIb zyQ%S+zh1aMvEH!g5>`!FdWn8lG zVBx!{B&#&$eO=IPNRkk--2VkWNcRqcfKudXiDs0M)Z#@l4ORD3>dyOk4+y@pQ#Dc; z5UOIq{^JuMr?6#?Hv>2^0t5@Ha=|Qe?+}&gA?t)$#!`~#D>)-^mjM&9C|u28sPclz zm<3XzbIQGg{SpxOMzGbVL6=(5Y_M&*#FUdu3*YH$S!ZiHvD{U0^Aj!!=4TYXLGNA* z*2FxGsCHQBe-Pf89*sesRa_#zYl%78FaL=efuzUF7MyxQ@BZMVrX;a!Ao`hl7V)XS z^{l_<&BYRCc?LPkvOR-UMu^mESe!cqT-<(h-i13=HLVLJe?iy6ReD|UgA&kB3NDIo z4TM(oVA344q@2~^cZn7SGOv_)l-V>wkQGoNQAS1|LaXMnxhRhXuaXan4d1%%frOvQ zis{rwG-Zn`lB>)w91`7uqIVfRZtWm-tuuV}uq0Ahz4a-_EH8`Z&b1nGJlme`dg(|i zYYvX5&=yP)2Wn3%UlsK5vt6$w~Lunsgz*=F<;*)*}NCNc_^wd1w?zH|0Eo?M1NtQxtLD06?jB6OJ zvq5B80Z#0PAs|u@vzLxdUal5OQy>*CqQh(HM2_Ctz!s&*igL~_T^6JuQ%GiUz!6;) zP^h>CEomZ59S^aiom}0+wqTEupl!85*C8|+(<_G9%>3ilda9<1EWVI-LoIR6>Xy58 zB!Q7c;)V6^Ly##}Va6O4N&bm2S7b43ehX<(^CpHD+B&YSwaM%`L3)>_Whc}y9$u3# z$9yvBjR6;a*H(IRk}OZ_)Selxj{`o@0sYcgLe%poY*{P_GOA#1lu{ki>X}F#g;tj! zbVkC!?K7By8@C^VUO2=yeU^Y1C4xM>L%!yHldA#kLR!OFN#q>a1$G{dlgK&7!Z~G?VZiSvi%S7fCuwz>F@a+bX-> z$_YXr_^duRd+u5ld{W6EbO<(`?VlhFd^K=p67inWuN#CC?yE#YtX_EJNR{|*4y@@tx+fbl+d<(Zu5w}7{Qp8d0Q zY?pA(GUz7%^Hs;zzksLBM^!iHsY!c$m+t@2Zuy7oboakLq5HQ=LjQOA{R?UQ3+Vh; ztdVJc3Bg{k2?&GSpEt|109TLQwA%QkbjxZo<=MQQi1CMw)|FQ%sOP)kiWiHm1#cIe z1s|k_PxhLj8%wGrmD7B!=;!rY=nM6m6Ay@ga_RQpFyI$XBVB(1L7L)^1EgfU`2;~} zF-CxD6w#XZn6i}?GUw5|ScTIvP}0S696Sf=#CgL|(Dw9E+!OF$+2EJY)H%tGEThh^ zWPXM~1o6*SwB}#(hWVOaUpn=hHw5*=N=7M+%~<|x68j$=-e?_Tu&SexdmbcAr@*V7 z(1q&-|8X1QiErHXz~N`a)~z`-LJ1W zBm6|m>gTOG@-{9TqT8l6T1N&yi}ydDCe55nNol-nsdJ<_AYA%)Dr5h(B;)^GC&kt_ zQaM84f=CVF`-XTEarj6*pWYzr6C7%E;Wcry3^@)R547(ka3NC4vnpY|m}VmC)tFhF ziD5PLF_Z)#;V_<-ti7Ysfz-|@Q*ZJ(`SBByM{4`w&41aO9+2MGbSFN?$i+%h~tOi!8 zjxN(MH^(uf z*urSGGURp$MwWni8ED5-A{=zaSF!p8fH=JxbqB*RTZ5E#oNI?rIOssp8EIkv*EDA= z3VQTMNyS!A#%<|Udc1CP6DF(EIJE#juf`k;LaIU!#MPa%G$C>aS)rv#_37_3u~x9P zxPzGG+uSj)!yq+Re4uJJ(I2D1!YERX>?~l;`k!%=qZ(Plt#g<=VFy=`!Nw3hgdE{L?07JTNlQO&`o!9u zLb~LUpH3(t%7q6(@~u_nzJi`WHy;B7<_jTPIt4v1TmWba7Evhdn}I~nm_`lH5oSz% z-W96oy|au!bH&;7*-tm&cq`Z*)_=;$%9T?;qK9SQZb%!d6#P?c`kAIIrP*^%yCF2BrlVVG^o5so=!eDya=eSecUxQ%+;(qqZDH` z1JO%_U18iu=TG>g2%P-9ni}UpUsK2)6NgscpNJ zrl!cVoOHfv`{|m-FUCL6Zi`h^%P-U}G`0d!12b-3yb2orex zacKzFXlg~n!9;k#DOUOU9ehaH|3%(=M>VyF+oE(80R?=xbOXuM`QlDDuQGjI&HKS(u z4(h?gH~YIJcp<@5?1xRc^0(=Nq26byN~P@nm0@We=cBi$Mb1JPrWXDh)W?O#(DOR# zF6EXkW{S;2QpA_=Hb|p(gL+H{yd{)K#^Hl{BUYvzX~& z#M83DQ=6*QOZFU~8Rw2zz}Tfw-FNU$H0#egL8+X!Gn)b~#_sniFZA7)>X?KP%Ieoi zcpiRZ^DdNbmXw>#LT9Lh^g>mpqf1$W^!P}T+U=bN-55Ge$zuu<832($*j~pcsmF)M8-q~*lA@$tg zeyJ!(xrANSNvM^hXI`_iMQ~e~-aMNmD$(Ml8=li|r?jD0x3~>e8`1V~vUV(tU>G@p zFH{h<34@d44n-${#uJG5s9Tc^gs0l0ZCQkv(>3fJ-wYE>!$n=v<>4ho>$}xm6isX= zNOl(G(?Bj!#?*%pnVd8;#PcZC_D~b6Ts%%_8t)NO)qMyE%2R~ijHVQ#CH7J3`$Cff z5MswnKDQ#(VL_E=MKaTt%uP%Co6A8_lizPbG~K9nPR}7w9xQPkBmGhwA8BylY3Co~ zvB{>|XoB&zKw~l^Xt4yUcvp#PjCp8B5~SO!nSdqOw{#=fkK>44y??!AQ+#<#kf(Z< zG%gR6CzPK!ug30T3_L-z^;YtBA>IOz$_1j1*PZHP(Cp~g19>cCH6xK3q8Fir7~_a% zChik{vYv!LrW#*dI|EG*>M~o^K!lgL!3?2Xh2k#VI=krZfFA1MZN<(FDeu|G`_Mke zfJn2g-S=5#JD%}k0{%V#u%KH}hZZehb6)(?1NvnHiIr>yO5#B>o{>Ltbm;Te}Q?^W;&Nj(d zPFLqxIlUhF)UKbW;T1zby*0v5$(7-((X*6`T=EQbSP1_zegs&jmwLg={{hLF3&E4O z9>@TtrcB`w`OCreYEOW6>~~zbb8yEk&{&Jto%d7j(6BhUw2zzZrt@6YfpFU&&uPo% z(*%d@7`aQKFM!vS7D1!C9MvYIFWR$HVsfRz>^hxs)v6@nCiTf*XV3gf`TXpK<=}z$ z`GPyfvb?{l-T&q?$KF3VY|;3keyWM1*PMF)zi9#e#}?RsC&vj#J!v9J7i{9s^6TB1 z;f+_IpuS)IB5AmI^Xr}OC{lHCYMGr>!`b$tyZ$~tH*ZR{t87G^JYYJX&!*isggM7n zlj7v^uQ;;|U>ys*%2kVh{=D)VE%wYFi~q6mBWK{JbMkWLVd{qHW9d4P0Yupzd}!#X z8$OPt&^}Uh%67)p#;l&F$_mH|;4olX|KX5&ewboUE^&s^S_@PG5PGKKzdXL?%lw;b z|IXb_T=_$m`($vrYUU;Hz`wbk`QCC_T{cgUAKMTyNiL|Rp?ho{!y*J(`P&7np3V^y zH1{79qs^aR6K>vA$&TRoJl;CEd2U24b$4c z^C;qUzkSvQZ+-E5otK2VFZMD~I92;0aif;)hnj0xITJ#RT##{gq%Nx5Y+JS0-m9dj z#7k>+Qv|zVOX9^bkxs$^SaFFGd=*JKuGyVW9!9%8tLF-S?)amco~kjmu!lBa1=})X zpYgq=Wev|;60QunQ{7G|ccbxU=vqN@j&#d*S~m!p5EIhjU;(#qC4`J~!Fvu@@k7ztrPjX8-unx<%6T%Py-6sf z-S!4NXaey;lV6D@eEPNoJx4!BDyn81$_MT>Y-Fmww=$Oq3@>qyp`k@3BFm`$0vzRK z+KxFdka8)Ci0F3+q4@^8bPYenW{|!#TAgQh%i+$s-7SH~=jm_vBZ{RwUU5VFF+^At zuiwjOu!cnMUjFiXF4H@?dWI&B66}PN56+jxC!fx^2=?V5zodq*F=}Qihqio#B#JK7 z@SQA!r*>g(FNb6R)OuaLibmRK^RkHBT+uL8S>9YYpso-;ugH=GVYo;PpK)ETXzY(9S6>_@%#V)r^IRapGq7HvuRG9=;3bFy|>X=H@b) z!hqZ;P%4@|AC!Gd&MM0I#SQrW;l` z06EH9+?}9eeL*@~`q|BDPsxmi#g?Qw{pYx#g%ry6IRht@{^M4 zIXk9i8q($ca=ji*UG7y4RIC&m>xra>@fdzn(z@_9N(HR;i)4y;Az1O%a82NVhu&3d{}cM)nT6yvN{)YXM~p@1RzEn2o>@TG|S}xL8N&)u*>-;QF5UQayDt0;e3LPo+0IG^vTBF^N1G6M_tI7E}S(l-e zOW10fByw^HZvjStsU=GBO`bdNT%Z`9x-8k?19<&DP$t?1rU_|QUy|^>Zag2W>WI`S z#)i4HlRzF*!Hdpzz0$g(_%ul;ew?SJfDS|+*C|45yRaz&9;F97R_vlGxaEU4hMG-W zCfX(LNakhYS~`p3MarIBtFc~1(KFaTh30yNc9io%;1I%mOUfGKvgo9eLy70Jk4HN3 zHx*W2h2E&I6jWUEiuE`V5N$7Cehn;}wk4?;vX2&B4o*fkwe&rl59dcRAQ_XN(?hXA zS%(>36$8z+-RHpTK9ds^pMeh^)Jq-cH_I!@@{sCBHzxw3eMFCzcXa?bR)V}iyA2J^ zdxk*P%4%em>_S?v-0O)smqxw4Y@N?4Jxt4#QWfMw=i;suE+ph5kMv->UTo}Gdpp6_ zHV}E{t~hH@A7mv-C->w^7*o|3Bk9L`Wx$Jf<{9#+ zzm=H2?L+I`G~_^IOI?L}3VyJoCB*xouE!UR9>RPte5qyoMv0@2)a{uA!d1t%d9we| z!c}V9oP#Fzaf~6m)k<@ht=MbQPFU$E>;XzB?Bc9Bk$&a6gcbpbn){_K)&$F#lGRH&V$ceh*$TU`G)o-n_-li10K7LrRKYC#4hCW@#~ppz7Pb_Ye|54A)VFz$WL{LykJp9Q!5?*3Ib^o*ubrYUnY7)N8 z3~{2K_WtWeJ=rx+4I=HeUvrI7P|H`JNd|sA{UvMf6L4mSU@?C6fo;#P(`V0%d(GB@ zI*sOBwf8R<3hd=O2f6*+BhU7o)!QY_c|BeXEcpcNZG3(2PmlP&-n@9Ol==zS=u4NC z4d~yD4aGD|y`-x=&++$!6rK$(N4J+FJr)?Mi>gR*^v-Vq4bySy3 zZ2M0B>i4N5F<_o~`u7Z}&gO(QnX3Vb=lc#hH|={fpO+#|bdT{C-)w#5+wJi^GgWvh zsCyXx{g0!6WJEn11VM&(8nbY*<5^(aQ)z~$SnjT5jJoWTSkZhJt=3=3_<`OI!ea5` znE2?~51(39Nm6F}8CHsPfU5`LU-Pe$&&TVcW~{+!_DrvQUkG8Ff6UVwe73HPxL&Ul zYc=jrhCtjNXp*qI{tGN4Fi1|PDxKB+gJW69t-zWhdp;nPu-!5!$J=(6#UdDfkP%T& zRYlQkk%)2}r!P>(j3RZ>%3d0nW_j znBE&_&}jmU>^jywZbZ?}Md(Ew7CYyu?eZktV^gs0`o32ihxfO=!0yX9sj*I3DEU5r zC+mcN%RN;kMkKkQa4l&jUXnbq^7e3R5|hV93m~#D!$kIM3y0%dL7ywRDi|q24QQ&HX z3!;k_UF~63kYdi486MPDhX<|l;MMA0`EmC0e<$ck{4NOEIABTWf69>+NW(k8XC*mD z4s(u}aekji@GukLGJJ{~(;;R9XbIzKyd4iZ+f7IU@_kp=ISqzfOaZHwihx!fUVV<> z7Sj*5J6N+`Vxc5oy7#PJ=oSS&L9c%<5Dtw^g-;Tb2?KKRv0p`IZ5rIl6 z+^m-22`P4cu~}MY@x|1>0Du zRD9!v!OL8|xD3kj`(eSk_CBE=rpE4(K)6K(y zYPZrB!s3F2&>QkR8mN@cpA=XnS7E$CS zC5+*-%E+$hp&q>wGaTf$-*QYA)g|#jn{ing2k@MIxs(qHlT~Y3%rbZPzqVc%LOzDE zZOG1n8;d(0wu*)26$6C8CsL9TyI+YR0>vTWQ#}QOIFS!goG}74RD0z5nBMaS$J;ZY z0|;qWNyRE?@KS}byK}?JE6zt7KGzYb0MY*>m)ZUNOmWAV`frYK>*mwyy9tEuORAIb zzLVPPb(hs392;zl`)RT z@h~0hWO`-BM>yTAI|@S!iYw)X?pFTcNdAzq_WJFjd`!n$VLw)?l-yh*ey=lF8PV6f zb_UJv^ZU`2^~YY%?+kGITW*~NgB%xH!I8hY8aPRU=Gf-*!&ZY=M%RBEi}PzZ1=7D0 z(&+!x`{)hz`(haTkx}g&`>9c~*F-_TyoCyYJN!_BGk3i733f`f58Jkhf&w;M&*_(` z!0m4u^@8b{MUTjDacym^{Wm8zeOL)a_y6YlOkq8KSxFC7@wV!+K6m%k!c|yud~4X? z=3mZO&eb&x6dl258FOhn*Y$h2pQEgP`#QolW^gj-+5}>WfT%*je~C($Q6Jdu*z~wPeS4rldsE{6~CV8wSl)7r~A^I zUQb`-d3l)q>#N*#`V(xjh4n9=IWMoTLB1P{&q}fedv^1eJ#Kso1zCCA?<}6re4`_u z3+_+a*LyQXoL%IsHx~aIJe3J?jnI{A)(kU%8dNc&L9!1gt4IvXNP*;e&tN7>GZsyX zjv-JCId)ey#rBy|a(!uL?X05hj8SCis^~)b5gl;AoeY0NN!y@Ee(xL&R&$H#{7T#k zeyg}Wx5&7bA(TlHb{?@3Cp@$!ogM_hD0df-v0v+Z_Hq8}dSC)!g zlMJcCVoIj7@pHd?LAly?TTQvHOeegf`x+CSv0L2RJY6`lg0)Y&+FTJehR>#svF~F> zMzV`xF?OGuFDdF&&b6MHz&EO2;Rd$74SBAmoQjy<-$StrgdE+9Vx6{*g}}kNyY|uW zYLHrws6|;M7ACSQtjCfxtMww3EyXe{9HAQCM|9MfHcrGsF)|NHgpMw!Y7>s%XHz?$ zY%L&+$e#Jy1kFwZph0oDMUr%6y&3E?%B1V6oXFU3@-Msdb+z463$3vZl zf1MmEtaVckxZgEH4KSx40%CyfH4Ixc86r_1P`sSAvIWH0t2Oc*X#_GaYR60VYB241 zzBGKQ%MS=YkDc`hBmrEb3-A4!`!r=l=2xgm5Ojsv+YLs~m6mq(T5^Dmi693FxwQ>( zfYAT|KI6bwhqpTv9Y!S8a8w8XEnim!z}4kyr^c;DBI^CtAUxr4QJ~+*=>MLlTobyZ-z^Ge4_~3hBtVjdHo%vL$od?)1}Qv?ES+fiHmMX?9YZws&#* zi?=S=U8ZU0Deq-yF1hlfmBEC*J4vq|dBfVa!|EL1z}fCqF{U!FYh1;4&yMndgoPG& zd~TBF7Aypp7?YhCQ2$D#eK&aSPxX!xd{9%c5d%w%A8A%VF>L|T=10&`>&+W+;($|) zfq+!pz5`5PLbonpn zc9c&ne5CKd2bCS(aYnI=Bx6$MbRy_}NREWS7PY;8_|P~VyUgc+!k41=`=@U4szMIs z^#Fs2csfz;Y_n06jh%OP1x$*+QXbQ)DbZM!JTH}uxNkdF@rA~$MET)S*LBeysvGdZ zB_F%q!4e_BTzAvg+)-sg=(DG?grZs8XtboDs(K|lcM|? zQc7?1zW0_sW9?9c63(dLi}umzGYOQU&Wct=huc*{R01H{>-SAExdJ+KPd&sKWo2tD zE&8G8{hf+wR#uVmf%u*;ShAQv^jvbsdN74?<67xb}I2-Wrco`zXDB+ivpxeIU$x)a6>^})&{(in0)3mUK} z$_SQyH>#0LiwoB6$D(U<^Kr$*H?j-HTD9MRvUmX8Ym`R>YtozY&U1vNGMW0K8pQD@ zc}y9^ktNL0#___T5;NSiCNFYa&SKr@M0_BLg_$0Xx9&MyT>= zJL=ljePUcdoqf&{_OAABMNjn_Ss)_ud7OjlPEe;ZaoEZ6#lnglZs~Ygf2Gu98#q6{l`?`Ya zngH+LT+a3!S3*A$_+uDxvadkD4iA|#{>?ITs;mDRKUK03kQ?UZrrhuqG%W6v;9=jl zHaz#wp0N$}7w)=3e8N;XIr%4$4%TU!+;+8`>SUEg&E~z0zVYVRXWApj_Xn`-OWcV4 zld+#aQ5{We(gqHl@4NG3^YV98*%dvlqO`e}WSAcEd=+*{pa~cR*-bo|0Q8|EsRoq#yEi|Ixd&KZ;sC zTk`BL;NR&CW;J~JCGq{4@KtF-pA#OrnqW%&@9`^_3FT!KLb0dIbnf=&KicsA`5qRz zr3^oNEbvpSJPBmJn=pj6yzFWf~3mTr{-FHu6W+FlA$G#pqVlrg4x9b}t87l=g;{hW_qgLsttj zq9+8$5T_j#^n`R_Fap|)r^*qq%f)X(*K>Q|!uMW8E#zCf<;lx)5x+W8RL$X(Sl8P1 zj30B{e!2MUi@0cb`bT8o1c8hGXHhrbfqPeli{q`93&hd0;Fr;YX^n&nCOD1jy9+yT zqr>NA{Dl6~F)JZa;wHY?xMOd zz$r11nXYT@Q%EVI?mZ#FS;;wxkMDA&Z^y#5$CD_6F<|mKDer4dU;!FLJg^Q^j>p70 zGNoz`%{Vd*-MC1^x+Z=S2l)IyZ)5z#x?)ZwE3t;B%7LjF#y#g2YyIE0T<`2*1T}69 zMy=)${QoOeKw6DB;Mf&V<;a8_=A4lg*(}Na(NL2mw+&SYXN?G8(j`Ced}EyFl_W)W zv9J!yYHOGelSd%T=;c4tj)BWwMDnfI+^2g!<3mMA*_|*=d&|&xsc?%hmazdJaBsp@ z%i44%`1Z7F_s){KD^lEYpPBB&8J)J9Y*iY)xMd3Gk3SFf`eCB%RS}Jxtm|!*k%WCr z#RI+DfJZWvf25I+;`dhP_PEmVjH%%(SESxW>?5mJzoTB~LY2@w#;Br-7Rc&$uBD3r z#24o+QdXrC0iBg|${ODknj8A`EF7W!x$=v#l8GYAo&$HE=wDf|{$34AB3o=j2UkI@ zBCl8#ydl4XV=Qq;q;!_b58}C4d9y{RhbgduGz5Sm^N^Jh<9fTE_j4(cV8zrTcpQ8JDeI zU*ZpJ@bPi~N`A3|yW8(g%pEr-P_`fbYUqM%bWT(0Rx#(WEc=uVYE<&&DUUc>-O@@J z6)+hT=4395@_s_~S@noTk{k%KlLRn$b3TLHNypR*PVN^Fd+z!c|>-$9^xQLA^xhUBRbuqi2sEO5< zZ7B*W9-=Ttdl+#ze5|uGXwcuiDlOn%P$wcM)uS6m&r6~=lJ5kM(xTD{rJ?bG^?R>kD9-l6=_FzK8JJ~Hdn@Sd?hzR;4C_#TNoY(sJ&yV4 zAb``~&kCSpp*?G*8B;3x7DM7{0@ykdCYwvszr z$?Lb?aQUb`HY%++WY5$49^RcV*0cCj2<4!X&@6y1n_N~-(~{_Q&OTaj%cFFCw;g#i#}OryWZkq8^^} z4Jm9|PyD=fqkPhXXD0rVGZoQlIe%FoMX!83=te=sF%_H7z$edlZmN1kY0ou&b!P8dr>u8$CId&pLps#iKr-lXzWsEa6yKiKN zQ2tz^;wF%_^Zg`&JDB41YBc6LqRI&)Jd=}#Yn$ux=aI|t)~ARMGY4Cg=_f#tiznV8 z*CtR`H8&632l%n;50NjNeW?rwy1)q&& zYH(=)DmeRH@F3SBj*6%P&|AY#YG6(CuvqYE?*G9I{Wpfld>wDSV|EyQ8%^+%!{lx( z+_+)buhp-0F9|3WJuOY)HmG3Q%Sbm6Zvn3z=NCvV*mI0#7{2dAm}f&Z_%p*JmdK@? zyapRUPgA=oQG8e>xArwoVDIt!o)9%ieE=Y&evQou?Z;hEIa)t%Y9os(GH!l}f618I zK8ss{UJ`oJq#OCp+nW07aCiccW?b02jcSE9jPDCwmOq7aVv`ti9y? z1oD7DPt*d}eFO)+Wy%h)Mi#Lb8VVu29QUHj{JjzqcK93makp|D)hlF-hbjkPhgl~! zX|6xdq*|9jETwRkaU(XuWUu4Sz*KPIB5fN<-%ItbVmvr<3|MyrU>RIa={gh_{#bCI zosQK-TW&{=7^e1v)&)LMqp@c}L$k}IxA2O>Q=(BIYMu|7Y%bH0Joy944z**4-`gxY~}lN@iV?4buzK(%dSHR<<{W zRKTJ8NQt;u@6RJ`L6U25^z*lJ7(ELcYg!FLcT|u-`m^;G)m&xa%7P9si9>oGs68TEz$ve)hHed#j1BY| zm->WY*%ah#{z+gBOH2z~&zRb+UA#foQ}(OIblAGA-{pOa6$42WLyxlR2xjT{E~!K8M5CileM`JqRVuoAYGiGL9E{KU~(GmpnNd-g4>C zo!kWyO%=T^z5j6bS$`Zsow32b?=O%MYiY*24Q#|5hp?J-*ISqDw0&e3q=Ua|ARgJx z<}aD;AR~g{$qR*k{Fq}&qN?uYP*;wpQ5$U_CWJ!lmsp@=J@3Xu2?srm=ID*AKRk5v z-k<9!p(%~aJT-~S zmE2!$e*&-~mU!c(WSt@VdG-c1v~)+-cn1-~UNwj^zIK-1gLU~<(G7{ca?=NQ*|_f6 zPm7no-fLtk{Fq9<`-`6J;jMK2z;Ms~UKW+~dS*$j*hPNhS;t@JN9bX7{fP`KPg#w7 zZ#fsV`QN?2?_Wrh{4W#>N|j>*difMv8@%gQbLl;_+sg7~L(6z>rO(XPa%M$eJ7USW z8j%~i_s%1{B^Oyr5vj?Q{@m|uo8#iMccW6|+RqU0TiAZ>>Rqmuf-?$5xD^{9N! z*Kg~6&Pp*|;f_aF#sdre3Pyj-#ce|)r2g43KH1a4j?BbB?;Wzs86Dj5Zh6^b)juh=ENT?;+rvvO*xI0fDXa^S$uUUP6vCN&hoDI68Hq-^RmV_SmOZnGbvn zn)fmEmF(e~R4+rI=HaFmM91cQ1s4xhswYoK+peI*rE>j_%+6 zKNIzYEFASVU}ht;H|^|PZTbJ9&%+VEaF;k=a7O17?F5`s5JR=-QfaPehDCH}Z=G4T=0|9VKO_f!GC?7QVg(+;afGJJn-0{i&+= zaW$#6uh_BYe;V0*Z~4^fDesx4&Bfh51sT2UXl<+F;YLc%#Ir`5J)|v+_qA0-d4|{N z!b2kh*o?~Y;~;3lm}f83cFbyMR13V-W`Zq^A9Mx=>DmkcgsxoMb%j-a#Bj!uf(GD% zQ8oMw!^;h`8CnvYZ`nWS0{~rVcrIc6QkFEA!wdn$p>U4W>#@SPR0)oJv#^>&Aa>&h z5Fw9^SaspeZW^m0VDz{$@(iLmJ2gkR*DHm!nSx3mAuxuq;PP;@N+Ye87(Qxs(50K z5cjwjX*&3A{&w)a(oN1_HO-MF2SQBQX?&QP_!jgcb9tnOQc}Scj=0m)oOAFH8$XUO z*UpC-Hge`%H;s`~2AXKjLlDE{MDR z;QjnsU)4ZsZE#mcsHF-|skJ#Oc@+c^X9VR)ba>bxVr%-PcX1zv@}=28 z56h;QxeV%Cyido$1Jc!?R~WztG=Z_|Ns?I*e{uN>QeV5APt?z3JE$ zMi~v*+afnE&Pd@D5mL*zki>Itj8ItCAZ^sfqPAbiexpr#uzDLOkHfh}46OI=_$6HD z5337}@n}71D(+s_Q+Cr08POtJ^pUWT$w=8jj%ye75?eWJ+33*Hi4 zy#LI@m+tfSWAgmb>`|ceQx(b)1&_JnfX7q>GNhW6a)L6A{`qpfcQeD!c9f^#C?g=jGB%S()bDgv|AwiKSctH9eU*Q3#>;&6stVP1TI(g^OTYZ@#TKY!3InA;Rd?+Suc#xc`x7|Aa1 zGfVH>cPBcAb?`ERok;>`!F!Hs-kPz=K4f>eRf6;%<7u4{UDLUS)D_${3hUt|pl7ex zlH72m^w1-jc}g8&wJ@>e!5uiT1N7>hs@0U>AH{F}!ZlBa%4mBSXViM;aJ%beOY87o zam`QfT`cO0cDo>jqIf+Fc&0F1_dN5wB6{kB_x@Jz*m{HdvifkmDZAsH zELSx8+0kz3&BRs09$w#pD0_>VqX!qT3LL3OUTZH%wJgiAYU}aGuXXWRL(W02EgDh{ z-La`2wnU%oJSQ2R=2`~qA9pIG^ zVEpPASo4LD*U9hKe&Z=L*q%EYMNM~3{a>ch^JbDAgE_#f9WT~^jk}d8hwo z;jsVKEFlo}!#VAjTyd_}51KI*T~`95Xg5Z_%&n)Ez_xZK_snQ3P?jTboGPMq*BzE{J z(Wq(jY9(?a4R9QyeTPjIQqE2b7QT>ulMFZS25j*fkwesDIK#569CHPGrE?LriYLLh zckn#}+(=bF2(6;-)OCJTLgU04AZ+y+tWHLE{s_9UteJ9PZmGUH7fjxAGzjLEvBfNM z09Awnper*qkD&qRO0ffl)$rAosC7v{1H zz}#iJeZ;r3i8*Z|RtQgmQrl(n08-`_)i&!nv+U>NX;fa>8;1*OgFwrvcOT_H2Rw7Q z-4j+?9457#7;{qBC9lBYSq~#Klosai9?o!BGQR0N|=1s@@@72;+t|l-N4z5xC-$8ZR}$>f^W^ z@0Y|`-M^*B^^*nvojWi{5~v`!gz=bR2IkCW2Wt+oi53hCLNcApp5Q&_MNV+x>#c{% z?;1?=-C%{>Mwi5K4(PhR<-C-=Ls5bz$N+Bg(uGg)EZqi?k@p~C58wZ2fnau5?Ha^V z_3EpQy)i1(FF`aN+0y5!GmyiUucr5Z@UnMpD(CG_>I-Wcj73wOm+JK2K{l(THvi?Btxn+QlO95`h%|5D)`pMIr`rH zL)i+|dcvo6_Tb~3NQQz5+1aYp^@I(NH))$0}osW#oX>N zl4$NYK&`TCc{SiOQ2~}c(E4LM6mY6reJXruUP?kT7b%k>f*mZT*d-tigeRHhT8`GI zC{%fLwc-W|s&vGpwEQHtERb@}ovxPO88Zhv-t0CscL%eAv$bu=x(^R2``YCB9tUoL zouKwcd;LlN_{=DbBf+NS&pfna=Wjb2W()~Qvp=hX7lAWAung$b^L1<5YpO{0$mnQd zZkfu%YRO!DS9_}4$hvXY6ky~01U}# z$)}sYJw~;x2;7dC_i<;}PpV_a7JAvkGn60!tH=xKksNh$mST6rVkz*(?czDkkWcj_ zC|Gs38~E^wDX0n!_Pvgm(CA|57b>3mBmA~s)K5@di(ihe zKC8b;^7kHpFne{{7KnP`81{k`*JjpXtpoa-%g-g*{c>Z_;ZctYg%;d5IWID8+}?a^ z1DKc8a4E8E>MwR!?$IN%%qd*!>XFCabj2j+pMXyK*d8tN5ou$@dY9517F+GA zN65D#N+qa!>Ov@$x@riTlPc&GHltgAkBjQbOL*E@S_rW+qM=NR)kqv`ur55Ssa+#C zFNyM=WFhN4|GI=^a(v0u9x`pjxurM!FUye25JhNDAT z4F$G@F)@V`Y_Id&1(yQ7Gq&!aAI0bXweTymA8*U@ST+LaXbm3fkC7h1M;wW}s54&> zUG;FJZxtm9Ws1}Cu7phq;`{&bi+EZg?_l{i*9)k1U$zxvscNi&mWEd&|F#(YQ{g-J zyOzWz8LIA-YTi=QebeV~D|C_*_|)9{k?I>Ci%c&Yt^n=RFwZ@L2b+pD)#`bJ_hQzD zYM{k4SyNW(2|^+q3%w0Or2Ocl2wjEo;_U(9Nq3BuBSfr+hLI~$^Lf+OZZbv>$Pm^zUYS?`R-iifDQJPnFji~7&WnDgL3)&9D57i>e z0=#@$3&DrrkUGt+VcL3aF!_#A6BCJ75@nYT>l1JfmuX+r4Vemg(V2S7y47K-g~JRL zK2}Iht{Rc48rDA$_uG-g#o4mhDYBrUTEFeLEQLb(FrMief>k642B7gdV2&Ta{iKT} z2Tf<96#T0(fhGRzma*f!ip>I!UsVg=_@rJP;|+LlNdzXQC6e+BIJt8-GzL(_aj2C9 ze!kPKXDN zjy056Hf|4nY6KeA4MyMi;P4b`w5+K7xrO}pUi)C(V9Zy&-N|3G*>3!qyK()mi|)_( z^)`U!-6s_i;)$@_n_I*a8gP7pp>OgmZ*gKxaFG-6aFn&lT=Rg!Q#98#)!nos2@d8v zsJoFL@LYaaZ`KqraI{%iSCH!wK^L*lR(}C?czWRH)+R0vBUtj2J#Fg-G%*PXz9LxP zjGuZY>EwB!$61)nt0wNC+*UxFB{D}GetuFH5gBv$JuSaAl-s;+K{&C@f`}-TW;xln%W)m}gy#E*IE+Ma8}#+UDiMVCH=j`S&W&Wt7Fj2mvU9lT$W zpI+_409s=WSO^fgAlzC!`O?s-o1eX237?WY{kEw`OnBOv!2 zQqs<0;T!S%fgq_^kK6Y^rjv-^W}W$xin?ZxV}wMh=H_|hvld0(UsOdy?sgPi#9NmD zVueMPK_sm18NIEmEDh~=RzTHqU5ph*?of=>{dprvc0OZe%by#`4oFUnW(Y=xu$$Xj8;8||FOT)O1zi=P zOK%FcMk{e)^9{#0R4em8A6hAKX%J0wUvH?1zC z3h{T4s_QSCibm#sc zFgrFfL9DK~S#mOiq|zJ)i_Jy2ERVhw5RQ`$DWs-Nc*hhmRYPnVyB-^oMasvRacLul zC8>x;^J&Bni_-@4FVAN1Id(lYS`nj`zmE6^0_Xo1q|;iedwkB04( zmG#uVL~R38{hO0+1XF6eOKrP0M0~~_8lxixUkZ{p-*6PhLP#mMbo`5D2ryR12I63& zJlAOrg9?(}ybrtw(1Q@f+Y_heyZ%65QPi1jl+h`Aj4Lc`#BT~YM4o$>{2AAO&S-EI zBDi6Z?a|Q+eLY*1V-2k89yg@mL>4FkGxdh9ZZ_e=AHkb%(Lv?5(P-Dt3=2Mf6XaG7 zFZ4#)jqQNM6VT|ub+Yw}mKYaM#xjKEwkqfcKdg4=0J9W4Vpzj(2gg zM?d||H520=%Z^y*%Ju=&Ct&f7OQ{ zG%TKYtE$Zz$V~Nd;i#wvfNx%DHG@BzaxCTXWIzc1MMf6fc`&-^DpvnrC3vw#R4 zzcSlIJS0O6dlbAnPTId%@a0;;*Jm ze)Qq|*X^&rpLZu`XwCO@u;9W>r3w)Hkx9My!20^$9~Ma56U_eraR20ZQQjrXZ)BAB z0;l1#(85rOArocauSj&TBqO7t3^0W}#*05nII^^6Xjh0tEOd4p)DT`zbnOe@A{Eq-qL z5{r^oVw$<6qlX7ay@QZRlEX3J<$Umwja3K&dMb0X`g6QdHUpk1D^eC?_`#gSgv3vQ zceto7-FyM9Vi)E*L4a*DT~AgZp+Dw{K&SF)!t+AQG$%T*+vaJ#L1wt8uWJ!v&1rX5 z?QoN?0^T9mMXSp#YmlK&%EbU~xaH~1<=Q#8kwG=$fPiv5XDe!NbxM5&$V-4}!5Ap@ z$8TrT4cu&x@XSTpHfEn7IkX>1hVyhql(@S@X}iF%qdNR1&Yr4wf)(nX2y-Ydz;`$6 zhat&xw@@cwQBsI?YKUjx;pADhk$YwPk@LL+(4Axf>Ys}XT1GrQUq5}iE_ro+@hg1X z1eR{A1>){h=^OnOHU+7D|Ep0UiYO0(PnjHT8onxq3z>M_HjvO9N9MI-z3EfJ_sWz* zW%b7-N5G%d%1(Ktr|7yPGr>2V%1piTxSWzXkt}up&_rw$jNmB8*}1d296k8Adc8>w z4vlVL=d#4DhJSYvIZea2r&Lo6WZhsj`k<_!Hu33sx5&|0Lf1h`64dV3FT?dIOji7d zlge|tyX`e2ue_(~sc-V`(W6iNQYp1vOpX`bDc-Yv67s>*{n?I~#B<1aq^X+@?5@_r zwRbtEEA+F4d5o#{jPs%Q4X<;5rsnZ7E__INYWD%hyx?_Hxfn+Mn@bRTGAJO}uqO1r z{W_mlKrfpXCY?6rgzn4V^X3z4WndDo4A&Wq{CfB)^9rDoCkixO)W&Y>S~K&mYd2zI z?v%N3>1D!X?si>4l3{1T_|bH>9PYvr%W{k?#g~L)dhnGHLN-M(_9AoBV;41hnhQ=c zQAM3D;{z~+qL%4HtZn|Evh!YJ7itC@JXiA`tET98D0p|CQ}`pn_$Uz;mc6iF{3&Fi$==T~prn80SiQ1&E{#Hm@HrvpRpUHdGNS?2!U zMU%<`I#zU{fOLHRfMMedr+EDZgDu#LTl@S^U1a@S8TtM}m%xU#fge=?->zbDiTmwC z#mS7i81rd!kRp3GgK#yJ;hsVBvV1l3`31>I#(M3FA&E^-$4hF=?*8VJ*xAqWJ?2tu zUIKNGp|~QO2MQ&`h#17Y3FJc9c>_YEzoL2+#oN7H3iF}B;Db=Bjr9xsto9vLwgTLk z{u<=Q%6x=)Sfoz#Beib2h6DX3c?5&1o?v+WN)sDfh_m0x= z_3VSh1yG4%g|4NvxG+q#*Gv>&vKzv9jZhkbn_Up6Lgg48)MtKM@T@cXHV%;Ll1YEy ztxCK-#)RO!d~wpFTT(&$xHACZH%JR{xTe*p_kO0SM49~h&Q+jyVh{bZt2P-$M_05I zXNAfg_qgUUn<%Ft<2JXV-_uF<>rOFbI`I>Ad4N)SivmsT6n4y8aaHkDmg(53eyest z%<9I+_K!L_AgQp6=z@g>pA^hTAN#zu^6)SK7ckO?4OpHabq!>cZzn7@^F;d*6vt^ll_(K-LsNiGG@&%OO@>|CHGZhg(J3{2RZ+6lRNAct#E0?@n+-LQf%q2vG#H+zT|^pC)C5lzw^29?gF8P0^1#}(O%9G~1f zuSr#f)=RxSo(Id@Srh4bZSbz-0o1gr_SSKl#nEb`8~6ekg@ffgeg+)XiOMFBfbx|| zXoz-q)?Q@J48u<2iWdDkmq87z%fzOS-<$GAo*=F1CW@O9l zDU60N*$x7EHUHs8#VNr7bBp%JkEpYB?SHNQ*mc_mbWuVgN81Erd4l?y2fbB688R(rq z^q-ROzabwN6s-M2Hk^iDvCDkhR%M}nx}d+nb9`;_IiTw8K-aGkJ<6RO@n59B_;2z} z;P!N3|4ilcNBtbjJqp$wK4Jd5_HRJdA;a%$)AFQ|3gY9KHl}Kuz7x3kJLS7)OQYIQ zXhYDohJUu>|3^$cmTizB;gk#O%#a!lX6)eiSpzeXM$pzltcUSVT7_am&x+(M0O4LU z<*lIZY`@{}Qgqj=RiqJ`rNpg6_3aeh&u+=cq+1jlS`_FYic)Ur>XD0AHX}Rizf(^n zk8NFJb9Z@hLI$*!)u2#%)8Ql#>KWcp7axhzt5Q?m73^MXw=~B2g}rDD04A7a(G4N+ z;jH_wvf%CFRM%9AUa=a+)vvf>Gv ziE;Cnu&2R#_ha&R1kinLqMa>i?<-*JG0yXur2;OG>RfWo<*SVs{pnox!&{Z*VI8rA+**)GUU%5zS`<-! zTU?-PZM7&$iL2y)AJz(Ei!tOQFw^3A1htF z|M3aM1Evf1>**975R^3u(d1*!menVn)(tAu4f@{?PHf3I-Fqoc^DMekq4t{7=By#k zUDNj9rH~JbIin{hNr-iOswx@l9ZW(&R*JwOsqmS2e@goH2AQ0da|d<% z(}+R^B4E-kCA!J#a!|(bokKT0rRy3tonp6f2e7fLKJw|Q2(-y_6W8wN=onV`*7;N6 z!!5@IDczXvcTU>-Xo0Ugij0QoL(B9n&hc<0w9gQ0b8nVEsukzE{Yae*AL1? z&?i}+r1U&(dxga1Z?%wj>64d|R32^tr&GlT~Wo)=CNN z5|?#Hy&`D#eAlH>S$EKnrRYo7jceErAD>X;1o8MPZLixU>o}F=ch_@>*030Qv3NO=>-Bh@9q@sCH&6K0(Iw2p z^y)7oPWepkpSJe&X=jR104v1Ho&CP-T+YWT714gb^I13NYD0Q{yYXI(LP@Af%PSA( z38IvWY|4;nBP3g<&B+03M%7E_ip&MOP_0IO!}Pd`e*?5lFH9F$Dkmvx-J1DY5szbO zvF=K7O~Rn%3=F}vPofts?BKDOt>94ZW7W)vLdypOciMIBYGbJsqb-g!$dyM)jjS`*UC#>W)kfyFV`rX26P3A;60UFn5LhvWW8}$sfTv<^s`n=|TWM zL-KWt^$H$7f7Y6i?S8N@_CtYIG{~x}sWlqG09Wd)wfW>_9bP%E7rz+h+8;yVn-ok_ zq%VbZiwv#5A*qGjh<5QUA=SKxf6EoC@0S4(%tBZ#uCA=rhIDVis&`;T$D!K!);W_N z-_K*a8C=-0dr~3=i>T|3(r^?{^=z^a6ud zeMDSVdg)?}?*z$C4POn1G{N&uwz910ig3dX16`<-I*2c*+3;WmzW+{hpOXUEiYAaA0)nZEX ziopcyK&-+uB_?yG+3Fm_xz{lhf?07yx@5LEt$|0l&JQIaC|ltcDssC?aw$sTEN~iH zS*E^!{4H7X8=gdTm3U4ZZ5~>lRYcZ&8Q4DbmFA47YhqcffV@c!}iRBJ2s6i zJKzg%+Tn^Yi(jFZK*1uH6gF3w#YZ|y3)@&vg#KuVA$oG_(a+{;!_!LRXpT}w$5dUK zoDg^T{tZOYsZ^1cmm+aB(0SG6jx?-k6IyiFs}OOv>wDAr1MD@WybR;F=*%CFxQbT^ zOe(k|=F;Ba6Z2$KZ*<0wR<7UjvrHJ+qnUq7jM{7d1|;4)`PMRrNuv5B?fV}pn*=Iq zn!K(t7_k1Gbf~LzTks@>auc7>0JzQgtVW(}Pl+4hyY8;=YwYg03;bKQQe9w2%Gn>4 z<7Qv3tdQCfC7<|g)2@y_a`EmEK5>MLb|X!m#3znDfKNrL+qjYV;&>-XK0jrc|!onCrm{L5{sN*NuxZ@8Ru;mnQA`m~)*tszaz zjoY8*!6(GN>|{B<5^#RrgHEQ$b*fL;=-HoHG0FWK;69h1`Etp2DB zqh((Q0gJ+C%|0lHoR$c~R=q=OGFsklu+0{O0SzSQY2oHhP%X(;u~r&g);V+V8(u3t zUV35DKW+qe>&5J?iT8!bW@v!d%xTTMlR#_nQEKcG&;c!{qTC*62WHV$F~r4Oo4w;? zKvWgGuK--Wx)jNU+58f2-0Gm}B|jW}5wzimeQW@ZSiTq1Jt-RcVgS1kniv^n>^x-T9vuNRchU9Q0G^QiCmUlY*Nt7uYxJ3$mfz4~? z(ntOWKWA4e*G7`k^F~Ha77l^?R{w%pkpYI(fLDRK8=Ai;Pa=h&0B2^Foby)Ec1kQC z*}SN(mQRaW3@FIr!+s*dH=KLwH-Q_e4_7c%Z@+y0usd7UIvm#&-p~?J#1b>-s-bd> zevJ8YBljUNGqCq|?<0Rr|eU+2 z|4?#NwZy*v6xwJ;eb^;QyftO^Am2JD!@b{wZ%i|To^qgq+T}X+o`{1vVCqpM`$GTw zIX97)EE_HBQzWHSw-66Ra{iBq-@Q2XD76W>7E9h$!bnWm`HS9~=Ul2a_ zq>3497q(=q-HC3_k|rWW{tY%?7&<)<*3`oF- zoUGP8d1^2^EDa~FFx&aa$wRA|@ho~eKC?@in`{c){v%5N{@VCxe73r}a^sx)2Ov7a z5?svrB&?y4Fmj_Xt9q?E>!wOICrJBSHD1Kyz@V{gKeW1yS>z>`W34q?@hYm(5h!1Q z3G_5Sd!>Rd*?Cgj3w~T1Bn9UoZicPZSPeuQ^1C=#D(-pxHRH!2>lS{uep%CieAQ{{ zL$3Nd=B3qog~QEpLq;)E|JfC^SQ~FZ6J!4 z@>P?JOl~3XSXlYv(?E|u&aU-EM|FPrO3VFiw4nA<-K`T8gj}1YTuorIGQ>SKz*P|H z32fkR`9_k1Zbq2PziG%gSr_sO<%nuIVvZl!qB^pZm8`pjXoM#4hRp%a zBX=7_Us7xF+}Zx0ycIQHN5jwUI;^L-U%(izOQUNl;&pCT95s+^ugAEuy=j==+P6Ir zL_(=Z^z6w5s zu!cgyWGYT-cA3TBn=-QS#T0b#R{IH3e%NZ+lah9TZGKo2#04Jc`z88|UhNw-YN`97 z1!2UQr)!QMny+6PO5mG0eT}!9oRGT4k*v#|f^b3cN|HP~dff(fRdVy1kCx%rI;kN9 z0&hTX0+w>BQGDbKSS42^={NnXv!yDEm6K}XRp`6}Ru74FnmFc}wACbyxXQQ_OZ_(x zd4(f6I2;if)7Xbr-u_Ua+If1V&DE~T-#OX}Mx~j!iGX}yMn2B5JlamtUi#G7M<+b* z&TZfQc=F2qaxX{f>6O)TCgRueUwazimw#lqZhonK!_-Cpv6_!qOw~;R1&IJRNZl5Z zxdgZpeU@u!X$v0LE1%@>yDc|Ax`OT9rz?Ll>oQetZMl8lz4Rr2%R60|Z)0~1c^F2_ zyb*sd^r%K@bNt-fdeS81W~d@D*Q9s*C39Ly_CKCE`@a_8OWe6?Cz1Bdg*EQi{kg^V z5PXqqap`pEii{_I{xsiqs$GTldLgp_iFo0V|EA4>>&lDg229yl+jU4j;nKE9iz!{a z^K~oblef%yf%m()&@{M`zcxmNER6eq)9**%qF|Es2flcs*|jf@zYvzl{m<7yoT+WO z$i{t;Hgdk`uE<1jDN-Ev|+!W7(J<4xYaJ%mS$=p*kPM1lF6mCX>@ zunV@F!SX!>F>7b9Kly?NL*%gmFgK_`LxO2zf5E07X|fQGcAJXL|CqJY`AGsdHj?O4 z5jxJ^BlnHz?jMeOW^}M2y_$S#kLcAb0vzGTcX!4Tpj-V0yREu9=&84$yc zg>a%_J2r(LI;+G3Gu?H`aOKj`W3Dg!IzWXiL!3NTTwG+?H5KO97?9YI zQIu5(1u2MRy;EJwn;=%-aOIcds`Ik7mLwU>=g;OtL=ya`$PhFmUSzYA`v!bIip$2- zQYKIF75qgY#ZF!r^W1MMkDHt`Vvh;ak(Hfd%e7acL{`BUA&vssO0~C|qSX4XhzRjn zC%dITIsp}YykaGAae$Gy;x`|aNG^`s6eK=>je601Po_}^5xyE6c9-8)LU~%FiwijA zsg=sx5FByJ5?Eid`GRT&nLn4w2OlcZ8Uxk!6(;dteZc$Pg$+8-$AalQq7{P#Wj7IZREl;h_Y)<)eKUD4ZB=DBi(o^C^=a-b>g4B3>|yrhq}|I97-ZZ zfcB!Gf+WF1yy-FM+~LJb*KA$}@ab3y@Lv+ASvc^E3bsDV!*0eC9yh+gZq-LFQ?8F= zBA*uDLFMIK_3T3M#d>jUYzG<7?i_A)x%HDT9*N(28H1Y07AuyH#fD8~a6*D!8NGa@ z)zpc^GFLRA?KfUy=A8>Y^&B<6{dp8AfwuGQ=t{S$xs1H?}oBHtS; z)N{?%g1QW!1k*R~J5JMs25csD2FT&rV@>{phEJ@&-oL*HMu5=8QyP#j4_>JZi$1wF z^=*D~iowk~*7OZ0_7Hpb%T7)7 zR1qRO!GU_hVGYOWS6j{y!7LvdcUf8Ao2UlH+bZi&J0er3SNqw``2PK zU5~R^;`Z!c9M*W-zjAMdZ?gMLLw@VqQbYZ$-d8Z0U~!@s6$)k{ritKj?(fe>5Ps8v z*Wve3>FK#1Q|B+zg?jaopTEt)P1%B;*4p)i$hxi!k>@OC945pGdN89@Bm8SWJksm_ zAUL{Iq9Cv7I^NPI?Ic*v*aaeQSei^#gy~&yv1@8z;9+a>7vLhQmG@DO?PqKxTy-bY zx0o#mZr$AmTVA`Q^X0*;rb*Ua8*YYw@$K8(oSs1)TYfId94-Q8U>?0agQ-G4$5^ zimqDoMg8&=Vl4o(8CR-c{&t>8eJ(J=P&ipJN>s4jTkV$0%>xHbE<0CT)^tjm)NIQG z0--uTK0rj+>#uEg<(~3Ssf+0RnIMiFH3!$R#F?4l;_k))dsU7_^3r0-Ah778*H+eW z%Ya=9X8RlAYO#ab1<5lNQf~JC+!j)9QeSI|XrAt-HXeB_j%oKukZ8?8q;qwU55qP8 z2s74IdVo+a1lv!vkcS|OO4_)jPB091niQ6)7xELKR~xLtCEjo*MId69_##$P!d|>0oge@=G$jHXJd={whMp;ctWp&&G z5(y>!185xGEhNnCY;*ymURy&hqx@~>w|M|HV9~;s_Z{WRW0%4Sg2Zc%Ow6kp#R#;A z;!uCn=^;{;nv9R({rul!hj=06u!T&>2PIRz0#2W|l;Z^%k~LNLHC>6lWrMN^ zd{L$rt(#yDeCIAYW;f`-Q!!Xt`0sVH`vh)0bMS-24pG*D6Zt;4u9VFLX&4+M9jauufi-U1IH-^51}= z&Pe8p3i@ArzXg`GsJ{Us!jb9Wxqn^!r*4ySa?Qdu@Yn0>8qC4HH{1U+tS;v0-@gjp z{!JW1LOZ-;k6X$9(ky=z{s56v%gf8^{tXDjk7{ib-|Biw`OCv=e!ZGo?1}nz#afJx zAI_F$vl&}jnETU>{}Vqs#f+}`SSEOp-?#eB^(@O&4UL%B1}XGEVvzmkXCMIJn@>Gf z9kIr?rHTfDgS>ZN_6+}@!E(bSFibD-rNg-oxmOp;L#^Q#U2ad$$DsfCB-dkpx{Wh|NJHPY9LxBa)E_pd{|AxJ*5)yGW9HL2OJuEt5Nac; z<%z-ChB804O6g7G6f0<1Ark3!nUbk>Ie)6Y;LOTeiFol1dckGo&;xmTPQ>oq**Z8z zI@fcKSJD`9D{mcJCts-hZR___H(T`WG^shxhu*e+9qQ!KQUJZ)dAa{)_YJ!3QWl=k zxMbsO2nLQGa}uAW2Y&93buy2c@v5ZRC#`l97Y*ylq0nw*SaYN_=w<>RXyU$nYmlBH zpCf@JSZKmpnnSKuK%m37CsUs`bb-P@w93aj(DQUuccF$ycSS<>p7!#Dt3MGgDzn3_)pn;0F+G(CGc{W(1YZ&Tm^ghb}u4 z5K&Vp1xlnUcKm>u;z65BhC)G-LqK|HPkd_swV|4)hZf~uahUn`&S&}1H)1fh=eH#m zuGsf-{&aJxorl8VKgegydJ0~hh_e;`bcLGA_`u0TiYCuMe0fz)q+sK)+A42rvFWsc zEY^+L_G^utVy??UrjsRz5k7b!PLZ282W@{P$hX~(oXK44ux{3@?zp>4|0r0!fvB;R zAZvmQ<+*k-nIaeUk5#o7wyZ_MF3?{)Z#lfM58F5k4zPA=l#Eh^TY;@kNVbBM>qJgr zedG@bDv3?2Q7Cwr3(M&Bdd9s_9vr}44#!0AveV?4=#?2jyUR7$2PS$BHi-c^90s51 zX+&t#bt+rlPbgc|roeFqsKhS#`)To8uk!6IT>b_O_MSoQpOF}Ex8kARsH(nIlBXGI zvv*bUZDzjgWg7fJ_Z!O#dGm(EUjwlfyy}k&3#ptD5%i#p zuN3`kU&ciM;piHb8#Mn|;N|%0K(VoSu-8oNE%0>hB{kmh&ON@oB;!}PB-=|_2;Jj~ zM*}35g!A`rnDc*;Pl12Ab*X;-$u7JzjvqkpRKx*%H@PG{N}h-0|#+WcblVU~k{fH>gL zk8>~hMb8czkf$aS>iLy*Em_x4PhZJb3+DZ53SaiC&PwANXWmy5z&l~nrLlH?Dh<<+ zbD>%+KuO+0-6?EJ zc{k~@B2wg=)0#@z`GZ`uMRMn>W?G-H0j(^He=M!iNONU3xM;IaU0hlu>_#82b3W^Q zPWcsHet8*l{C&+awV}PV%jI4ya^T9Fkb@tQ0bEhQTA5Bo+Z5OEJZO!c)HGQg>3}F? z^K_U~EZJa9B+oh0JAf>Nku{f)1rtK7!wg?e7r0ziW~Fu;YWj4}T6*BU+UPgQ&m3>a zYh#}cq-I33e|Rd_-|dBKlvfkZEqJ+f?YhQkOYz?D%)8*S_ByXnR{Q@NU;+c;ym>v0 zA4Q8T{^Y)T6o+R&_6lle8gl;Ub{&mp(YAd(Gv8Il3qX0WY#J}=&1O*cQ=^PEid=^i z7qC1ys+N_qUKq93Zm;E$jRl{eDxM9$;FU~sdxs%gyqK}g;+47eo!D?_;4f~obd%u^O;%TwYBS+U0O+MAkM6}k~$Lc`Gl_A#};*gmaXhzPX%$ni671m00 zdfPjR9CXBN!qRLvN*RebcQsiV{itd_f`+!en5f)joxbHkoyQHynx?m}`sPqdT~l1l z2WRoUC1v(%>@D0Wxsa`T86AOiV=8!@g|I)o2Um5uw0fo#U8(2 z=f=xdN{uK~^~!lGC5Vn#IAVc|@+V1LD{J>F50ps%9`XsUJac=yu;flL!uC4ztJBVl zb?=$`8=#M*a18v8^*VrlT&DjDJz~M;_yy|5X>}lKi?=JTed-he5BWPVv`hc$&Gx_; z!xQVg4?_dl;Zpm47Y@A6Qk`>8EHNF>zX41OboH2!`h%#odNcCpUye*JP1uDylLGhm zPOX1)hTJ^c{2xMCJr6h-Z$<_z4RDiB#Q%Jkm0 zwEkCAHU0nTf2eLFcp8ynIcz!3ef-yV2|lAl-ta2<7_B!)PaKFQckK>p^`oAA5xn*O zn#}XG7{5kPes`Zl1$nS!5{ay<8qnwY?*99g^hr|CT(Ut|ug-K9nasRPt(ZRa|ApJn z`d|JQm{mL`n2WrJTkT;dQnTDDMmkxV?w@l5!HUE zi&zF0Y2Lg5@0yOrSDh)WLk238RlWN_x^hlRdlLMdHuc0Ek}ie7mn(Ru z?QYsXaI)0fJq+Q0*&3i1G+Z%*>H=3Qdeh;Bz(U}lMNG8I1wMG<_&B080MQ&+?^FTc z4_v-RdQhX>7NN0(-A)19y{_QH)(`QJBr~jN&H+X(SMq}JSz2Te&uMAULQ)#dOpa%^ zqdQRl-Im>(FD}NPww!lQPPlf+aT-mv`6kwhe*R09Y~0d5vuqKnEJ3`Ed2x!4mwiW-FV%gahr~sS|y$}{9|2~dv zg9=8yX}x2&|$euMuB&V}@5~)gzH^Slsou20_Yi(6r{-fg|XL!!s?+Au0N%^O?8e_##{d3j_o8 zxB8HCs4j|*x_k2lNoi`N@-KW)r>t)xRhKWV-P2jr^&U#i(=FYB3+T6^D8O^Eay;ge`3kzjJpo{HQ8&mwM1VSX3$ite=f9!!YBzn38`;8lu zH(}3Y^teN=H)X9D5Mse_uA6I_EWB2xhPczR`pEc~;ZlejP@0BjcQ~0Rn&#;?%tvQC zg%HO-9X+Sg?A4W|MRgY>`bRZI3Ee?a7+)kqn!Q@2=GVc@YOflRoY1xR=Q%SuMmP~J z$&S$APjHLj0i0)=jXFoO-htQm2fFU@nQq!DMv(B#q;=NX(jcy@&i3+-4{ur4^IwAJ zyZV}u6Bs|;mq7w*vlrz;?1e_+PUlcIoQBC)qc(0#`?#-sc&+oD5NXkw7R?akk3~!~ zK1GJjeiQwoPH1U?4crhd-&B$0Cf5wOv4@i_&8IgOyTj(9eIqwn@?p;1p^9~uoa7)! zn|p6}4QBY@lwL~{kZH*BO6U^EGH@y1Iil&ZltBY071EPOv1N@`R&5*4pqUlgRfuL` z)Ri+XwewvkX1h5DXC&Lk5L0IZj(s2&>(%4BG1#KrK;`Q6ESN^q{vIFNxr1TsEPnMVxp;S7TjD zyYVoo*#{Clk%Ec{8qt(@?15f0Nd-4g33*d8-$V)?*BnRY?Ea8vc4&oLiGD)xluM>a zc*)Qog~yzcnk5?esDfh?`BYxVHBFgXZz21ks>IXa!teIt(8c-mE#%jM;Xaj4I4#st zqPRg~Ry*Hz^l0=oVx_MI!amz8E93w6 zhRdRyL{Kr#eH`q)duVY5T~&Lk^0oo&Ix#jz|FGuIr|SmbY{gAaQuGKcEbi3snZ6KD zW~Q){J+P)$ll;utWmr@>KPJ}o++I7KaYs#Wrwi;niKSNjwP994x1g|pBTRRW+X|dS z?W9bsXtM}R+O%GekFOota^39opK#W)&eq;F3B4TcoPnuALA;n1hT7f2ebx~%5b%3t zqNlkT&>5N^RHT;1Sz8fAWXYf+3b9i!hO4}hq<&5alyCt56+MN8K={#NImN7teooQI z7kn`79APxyX*0EFeALd5umv_*#ZQao>-FRXKK$?}Otp37|Q4j2uH zaYK}vfRfD{fHPh%m{y0~?0vRZ8UM2+b+xT?5>W(V3_kruq9x<+~L z%@HZiZRgkJi1{0^ul8S}YwuK8*ZE9zE-#Xq8t&iyDCB0(_3L%0<&TU#tV&H^EdAfV zq^N_RTXhXAea{PM^F<$({qg-p+3fzEcJ0}jX^W}bWcaY$G0X1lyS&sX-B;@Qi~23o+#nllg)x5j9uoA__XE&X?NE>a17btEFS`0gZQ zXs~)$?Llv+-gJ9#1Ke<1ZT`FlY@hzVuvXw?^=I^W{1ub&GPW%HQwHCeMJf{e(Oq|1 zjG7N`=wO)+;u|CL|DHx$k;HaNl>(j4sV=p6@k{MIwEOL1Jc?~#NR+6z5OG;c8mfvI zIwhku2b8M$0oWTT#>?ytKHJ$bj_R0wciyHgECr}rdcyv+Ft6v;@;Z<9j#mPpSMMqA zoiF4mb_;^zr%t$cVobr)4lKx5&zx9DF6Rbyq8g%`sP>_Dk6Rk~{0f6&AlmM{aXP>V z`->1N+`oM`>_dJZTF;8n@!pWN`{wBlH|vlEXOA}Jk#i|PXaGq_8`Ms4n$%ZOc|~sr zp?F-_z#MzgM*#@)8 z&MYUG_`f>kc@?UkhsOUBamuQ@?v{Rtn4VWv?$fvH^ZYnhMNtoIB>%1lU{~5oR9~>q z+J2Xxh| z9wt8KQupndUVuZiJTYTWXN)T*jJpMP*4-2>)qo6v>`;0KAry}V9fsL<%m z-gTWkd#A44jpaTRn}Tj}9iO)!0Bw;GY@*sVKFqJKnm`+_a5LRgG%?lfzbRA^21J~y zJW5!42;?!pze(~?^0?1RIQH~ZF2-0SOnZF+q;yau6yqeXgV|$cuat`8+3zhe&Kf=z zo9SGVRW~4Ht8)W}Xs-JARy!I$IW46h z2XA>xXJpqU;{8qZz;MUHI=L{{1YD+XRabr6fs=OlLLD2JSI^$VEZ@ps%h92zE`$xJ zk$nQfY-(jbr0ltMFxiX6V@$wgla+bfjN@6Lq_vZA1zeHUUJ0i2IgC+q9hI8fwN{ym zvXo_Zn+=Mf7KmMe`wy|%hi6KYArHFo7pLZtA)S6K)L_dS5@vt#KLU8!%+-s{JEf(JLCOep)}PA>5_JjkEa-RYgF@$$Bk7K9ggtN-d>A zqUZkY>oaf9xtHtC|B)Z$VX}o{r_%v1J~^7N$aLAC9hi9QMRo_EpWL=lrSlqGvf*^v zOV7gQ4g`6GK!qOdUowfbjFj~d+z<~T`EW`u4rk2r?3kVk@jf89{ z`e5@bQ5j-U5EJ0#XphYSQEB7c^O99`9vAC1dDG zq_YjJJqXWJhRtsNtX9?C8RftcpZjb3i3YZKOm$dU$9Ua^*$YBOZl4y2QJaaE6fxZL z>Zq+x;$iJ-da;2uxPR;j1{1QstvKk2q{7{m7wNAI+^MzXR^N6+GJq6Y@-T4_`~*0* zI@`7hQfc;QpJ^EYDMg_md914Q^;dG{qQ+DMlbXJQZhEGjNMm**21Ly9lb;1O~QmVqcwQON*aB^c4v?j|FZ6N*n|EEia{Qb$i3VB2{HTW z#B66yt)3hUE#xvvlH%*}mPs?{6sSqQfmxC5Zs6#VWz=EgvJJo`i`e}BCO7@GnfEi+ z^M3D8M%MjA!$U-jTfFw(LNcSpE!G~hj+ete^UZ_Xrpm$_1$P>3MOWQP?(U>OIXB=m za=b`tPOn7-HQiAz2AmWNF_H}IgnJeZHBiq%E<)K68XgafbSiqX(wW6bRDw<7HOhs> zX~vc(6gnBu>>24yb^kNQ5sT@L6oZHpZa(yFidYNVj3Ak7tv$x5W2a}tBESxO5ya2I zF#&CSeX8$*78cv(2Kp_U_(Mg|LIvbqm78}_Ekdi?{Q$c5XiEf>gr$&$X?1l1h=`V| z7}S_1xMEa4ciTvwFCECY25a;^bB}hh73H@NGxhbbF4*wCXOd}Au#R~cwcznkXUCrw zYxSme=+(4RC#;+5g89ZaOO{NUWV8lpU(=2VAGiUBbR72}a73{wD!2a>78;;wmKE)` zE(x*8^NFkPTJp5Gj5Jld_oMySQ#B?x=kU@uF7g=cnj6oa-J>5+jpv{CnAK%u;-x*m zf0W=ph^Yi?Xa3zQ!MQ0CZ>~xIa-ykaB(l~Y6%h>hi@N#zW$%-dp*5jtuO16hUuH!) z{tZC(d~NsIjOC?^z_Tk)JN)9W|8VA6Vad@qU>yYbWdH;kI*{z`WnPa)v;R<=LjSd; z`~QQZdzI|;(;s)}LVxzP4p>(1{-k%BiHn(l&T<$m6pP37R#`S7!|s}}EWW$RR?U=v zv*Fh+FiJW)`FeFC&x1@%2$|iqe^G37He<1syL`P#Uh*n{76>qV>i&|Q-U?EPvo>JH2cW| z?xKJI@cr9di*;`~LU5?E;c;PR%HHH#)~wdmJ~8&#;wCy_ZnZni5LgpfJ1RQ%J}U`j zsIhVRfX(~)rPMp4NnwA1Y7?baly|zCx^d2%YJ;5V7|UNZeG-xPvl_rWUmF(ExwjxA zEiXMyKQaped}~gv8_#YIU@q|1EJ_BGPMjVrQvX|5@x{%an0DXS)Tb76$ON6C@Tur*>f=FY~cOwm}cG=w0d( zY)`R*nMEw7h7Fh&-WsBkIYs3$PmGrH)Dg35wWOdViRoQE=Gd_cVOGuyTiMGo^`jTY ziKSk`^oK5soQxRMQ81r%7JEz@>^?Z5OwBPvLQ8A zg4}r&=sO3r;{9IWIhe1?+Fj{jWazrn--N7c{61yFS@uM=t)vV>1<3(;R-S>faRxZ~cEE2p>EC+(ppj7opApv_m+eYpe z3SiflQ~3URv$|gEk}zI~yoW*_KA(%VNxjYM;+TV)k5%*FQS4YE%{9mqD#*vOX4Bh3UF4^K0RpqOn;L zY3u60n%qZH^0DTob=l9HRHb8SqJX#URj3anQ#V6KlX0EW04guDZ3xg5V{oGD8(vJ@ zjpS5i#{f-o-{`Dzd`P>j63F~5kJ4Q(F+q`Li8&_%;BxF6bO+~7eVdWKxe|qbTHyFv ztb7q`BXnn$5PouaRL9i;AJ%YFlgd1WeXmh5t)@AWYCAv}n=jqoLv$SF^roW5|<&j=-A^qvpUV* z+xIDeD>)uGlkIJx%(BGBO|6A}tF@MRjd*Dr0t*H(-B+?Sa#p@88+l|f{~49yG+U|w zppQSOIv%rr%rCrVOMtAfCz`2Z2vfWC`sb}1sp?+An7UQ9=W4H7gklnAzk$ww-WadOz4&Tx3u)_o?w1rn z^O|0jOoyP+KXo8@tH0Xd#8ugEc>fF17ZqQSeuY4x9KN9+&45$L+@ffhb1kO`Z!@nj z%|)DQAPnQY$4$MY=IXCFQ>*);jSA!jgDAEVr#Z{c!L?UFPVb#TnW+nIf`N*?snPk^ zobBL{@rO1)B^e2t z0aG$2yCtPu%W0|kRs`q9mzbFM6J=SOl)=fCmD+M>|^($jgirFX2{Z^K750cOVo6Q0s4{P%n zRK=4nH=xZeVtU|;)dJXbXn`cYPIJ+Ub++GTTZ=_J9awO_ENXw1DZHsSbUVZ$5z;4- z`SIehN%mL5)hQQEfNd3s?}AG=-$;IunDWdR{yorS@*wR7B4EEgAZyOVnvdQUvMdGX zCbgZ5b}jf^1|Z*?WP?fqsd_LmdC&zI9~;d(kOi%Gr@lyEtTo(3HUHTF1DiClRaN|s z<1tOFZfLFHfTv0CZve;#+nlB2MHj8VZOfgu_~f#nGYy{Zq~-E-9mq4ks8ZsYmKERh zE&|3)bLuIoIPl0d%sM34N$8%ddppQklazP8iSx_bqN~i;-3KibRD%<4sBFJ%))#yjww9)<2#CE%60d(rYF$SCamBP!1q=M*WI&7`C&o2Dd|~hOIEO=(wd-G;^HC5&y4gNt+*J$+cGzUnz}$0~-Fd;@uNs zJ`I`j{%M?mQF-F@?Z21u{nxbb-}X`MvQ5W=rsAZ$PtA>+m}&Vfzl$0Z-uj0oPJ6iWFy~TbbxB=hI}Tyf8g6-+K-%9=o3Rs z2H~5-&*!<@QeU1oPuXYfrVmmORd&98b07S+JJ~WHX#FRl*Hi7qR4q&Z&&Lrbbk(gqk_7Q5} zm?jz#Bt|5p<0h}E1#{LTUNnU(^RqIMzxt}$ws?;@kr6}LVPB(;n#eB4bvjoga+h!8R$8D&CD0j%Xfa)=_mqbwmAagVD> z)3cbcS6P#!^newfUd(JU2V8??992x4OM%RsF?oo4ipV@yG>jJz&%_#q12i=Bm^$^J zXc`$>hx>=r&Gb17O?gb0szOz9QWG3mWRTtxHmiUG>dr_~9!_>WR|ltg_(i*a{|pUb zQ$Tdzxz*-Hn-^9Uia`hM-cD}w%08-r#pV=o6Rt(%vn6}258RnzW_S;=5AxOsH9Xh9 zAj;IAXgu3cf;e{`T%7E1?7bpn-tv;!$4Y;Zld$v&seD<(OGozgZBR|k(C^#ZCE`y% z9yd;i1+3(N$nqP4K;N!t$C-NfY8l734TDjN82pG>g5m$h-g`zh*|_VTNS7upNS7u8 z0i=Z@h_uja2mwNoE(uaX2ho>aLJgfzq(gv&-cdmbJxC25l`0|@5Ea{d^6r_vX3orB zGv};zX4c+o{lDc&o)5{Btd;Az@9VmLDg=mBnyZ1t-x)0PtaI!y zfQvBD7mTES5X+PP=BErLOJ*536rVRc$`2yGu2XN%v{>mI8%4Z!@+<8^J6;l5=~%_} zr0JoEL9UD`?~jFgwr~3_di6MXLS!7qfjoS-5f_a)zq&pl9mW%^=xgZ9SdGUIDk#|7 z8v3mIMCxSGCXZKx^|$<;msnNlWpb%i!)cfQ>YS7vjBRYasnx>&%s)Kc^Pu8+d7_c> z!vgI`KqkK>$8Wicj(SHJP}(`hr? z=-O7Q@7A(va6+uG^FpbOkKkmiGu*IPK7w^h*-><3w5U<>IhN`?o3WErWl*y)@LNlZ zqd?22^B1r-{WibC1E#fcKAJj7hC8jkCnzrC=3GB)&%!#7DESH9hu&;iygO1vwoG2e z5o~5=M~iZ@HmHO-LrzZYJ^OcRePY2x0Wj>UmMwg5DegYiE!$2z_s3C0OA8gzH@JwM zYLX<<(fIL9&C9h}B+KHp1Ky@|dS|Dd-xlDIAoGXFp`hI-ob~pAx|u!Op9sUf#lvT4 z;~)!NIZOy;2W#b~Nw(Ff%^>Ni@6)39Kln3i>7R%I98}W3x@1(y` z86+FPNP}H9+U!oCzE4CtO+*5W$ynL^$@;EHTWmLH0Cy)bsTwtK%iqgwwo;>cJ)|0g zZI0lhT?Y3lV4*A0dG`W}I2gypI=U~<9n!a15~dWfX{|Hk_|So=vHS9+DD|ePM9RRh z7bK=n42B{NNBq6?Sk1YgKhVnjtmfA_<}XO_aq-c-%rx!kyj`5Of40U z1J)S4Y&>WohJJ8HaT#?0){`Rya{ardvdN6BNu!hGVx04%Du&reLjBGc;|A>EM+Z&| zZ?zyr&)AE~oq=>c9x7nPnd%dSen=u;IZ@efN)})}SgQE-_uyBDf zG2;tRUZY9ZXITAXL`!pj47q_vjYF*+o_%uwzwzP;815vCA%<2%4cd)7fG9Ch83CYM zEi7@+PFmHUv1Ilq>n)AbTg$-7wsEX~S1FE|_}aNb4{XP_(X^Hwy$J4Anc2K78Uow> ztJtTx$wlcjWM|gSr9>8AD`QPhHEsQAXjFw2e1sAD32mh z!Maud8gY!77Gqi&l=^rY0RLzs?0Ar6MxAE()hz>M=Qbc|*{%2W?7?}1!~Uk7gk4ts z0%X$Glvxh!eQiQ!*9Zfz1@irI{6e_8GXVJ1-Tv=_^vP9Uz;KKU%^wcUyFdO|{iZf0 zW4~cSpQ=PYp=h!3>i^1)NezzLpw!y-w{+qB@}-9)K-=F`umt&sgMY%3YMp)`CtrKt zdVc3{`-y;l`%j^qh-*)Z4g%d*?Q7zzW`ogTlyKn|HO9&~pUJaL-!_%G?*R-{u6e#=A3)%q&T;+1l)h&Od6 ztyCVjRyusMM8KKR{CzdPrDTPcyD}M^TZW*@;)v2wvBlsmAV~Wk8~$_&}J-#r(VLZX}rW4Uf+>QLRB#!gfO%mI!eMYJ9j^XPORRtWnW2$Nd_ zdd?sn!jy)z(P29=ZxGJkoTHvMF~d^9rIpQe=tS!#>gTBW5e@wAm9)640Ti5$c`8$| zFEyVK*#68uOgr>TQ@rXcovZrN@Tq4TB*gf&X_EERG-ItfhO5oG5^JZ;xz9R6Y1 z=wa0}tpmfK^@LLB&y9X=Fi<(_K_s3_G!xCh!t{H+Klt;|C3^uJT$jZQrqwH{!AxU^ z;)&>DG|{r-3sVH%&p5Mq(9?Ip_32%8Lh%f_2BzwG@oRiIPhM%=S}j2c1&}1KA%@&)D*`2X0-}>U5?|yqI{>T zzsu->t}pa$Ptg zFvING&B%3Wp@M3vFuckF8M=v758X#hL8NATf^(UUM2o)ZgYqJL{112m0XKfHa$oxE z1l9B!iHBOFg zZ)kX%qe&kM=sWhVue{I!<$ik^@Ka16r5kay4jnkVB$FdWqkZsh7fQcPf87ds{EPXG z#>w58@>^#lr0-MatSP%keT1*eoV?GIXsBCqx4PnaY4zKhIi}DG%Vk$SD#Kv6e=WE< ztnq5LJMw4f*-xR#mkZXBEua0hLLgI$+OtMTj5r7}B80<{t1@^v!9X3<0)9Nhc6MFI zCRr&3+dhz{xL&Lb>*2UAFMSmxh9>1FC)Hw%ozdm6viT3ZL`LriK@NfornOwQjT|)A zJU1m3iHHzs7DKyZGl{u^aqbJb5s05W)NL=krG7@j=UwoPQlbw@sfmo=stl9c?sw5W z(1S`7J!VboTDx!o-Kw8p{i78&A`_tc&OaZqQ-;R=dzD;{H&c*d?+>Gh_PxO*Qi!>L ztRa0tsGs{}q)Iry~%o$06RzAf|6~_9+ zC?z~FmncKcR6(~X!*GL_=-lDFGaxlNjdCzKQEN3F>st)|n<{+pdLnm|y@J#v;Y$I! zJ;l?5BN-gpW|(%xbc&_D+rephqpPju5XhYh5{RvQy`rI7X^4hF4}O%|Z~D{?-L$#=$q-T0!A{|d ze}l?KTs%{&0DE;kOasrdY5i3f-EwqhH>WGObiYPro{bI#fXA~N?-WPk#PSf$h5__t z3Q|JSy^H`$Y0*AsO3&3yofQZn7R6GEGo=uFBEr|iL9LO)38at+BPC(vJ+FIGtJzTz z_}@y}V0T?Uio`Fna;bzNasoHln`Fj5`Q7#!ZfM!=u6DaVI(Qc!bPZ7Z>W!bxzfVTP zd*zCD`tG0^{z#Vw{`mE81Zk^RV959Ry)GMYL`CfX1V-+Eq?!8vw{vWFGV2RGt%k;_ zOW;2YcrzQs3oq=C{`^(oq8>6eN{Qzj>Q^2$-(EtU{GJg}e(*QdkK>+}-~7|>VNARp zCffHXjElL}>ME;Th60H50%e8VtNb7Bk^l8vb?pD_HQ*?4VQQOd8xgMVWA(F8Gy^q1 z^z{63?dW2q5~h}Vf9R)TP{LtK-iIg0rCe4O^j!_p;!_hcMB@<#S_rJLm3|H8ru{b+ z>-n|AFD>gA7vqJn+YW>OOh>I1s{fBNf0|Wu{e{xrK+`U$M`a%Rvoqg|DMK&uC-#M= z*JJGL^FV?b&NW^+hv;6~1~-zmhGe~=OhK2lQoiYr+8OPm#;*56HDRh>ia_@)Qa{oU z*hwOI%egNn@!~c)yL6sO;T%}3k0zESZKOv?wuM_EWv~)wZ073;lDAJrEjoO6+C2*x zyr4JCLY8*Ph8MCHQg@@3e3pC}NhWQ!mCksxj$UI@!PFuja?&)dur%$u4}cF>292OK+Z)!JzYb*tM`tBu~$TmNi79zv{5guxD_ILLMve58jel zPA-4yO^d?TW{P2osu1>y+bMiwek)x92wLP_yl&lF>lT?|mKA+}8XYP-Jg4EjVRxUX zPC$XO2|ruevaiIAWSlT4AOtQE$PMZ>4`LODH@!(wX)jMfXCW>lC46<2j8^UV)Q@1b zAUmSha4f+}avm zT@skAGt|IyT(6KGMyG^HA(S7kY1}elbQxG@RoHUB4 zb;&GCIP0z+TMUakTFz3 z+3giS*S17v^oD4Rv5FQFZhGoa+W$VVG$cSSSfwxGaJy}~leNkvOQIo;9gee;rw4=m zDyv?C80fw3EZIh)sBlPB-PGPkz82sXkB@DeeciTBA{R0EN5)H@aCt!1|nKm>O z{BKPf1AyBz^sXBB=Ec4JgIEdig9a+;*bjS)L@G^cEci~h${elNB2(c`*|LP2|73lv zhm*oV<2xC1*_4IW>2P*8Sj+-nrlRn}rnie)NmaZW^$4uprmb|5I{jcnFHNpY(ufq> zaudf`q8FfR%E3S zL-qCChS>zk`X}wZm~@%U?z{-U2yfBL0&WkWz$l#B+y{*t7Q7u zYe2Nv!e)dBG;TtB87D`2)6NxT6eXkxjwMzs<8yE=gC71LX0pE_zvQ`YKKm%<{vAT{ zB3+$PH0Bb068Rx{|HKjHH3fw(e2mSv!I-B4{I^JEa*7MI474E--2w4JEgLZ)>)xq! z5AlQ7T9!0RfmfDaP9J`0;YI=XM!a@D_EBOIS>J_FA{f*th_c{Zv)BLhK7~RqeH{IT zRds5cM88Z$mc3fVdkH=7M4Bcyjc&ejTHXB!sjm_!qj9dDJ+chYzI{tdN6i(@{63`G}^>Rw=vC}y3&0JTdM_GI59KO~N0H~HIfxlOAr>iZfR_}XH=R2K=jU423{KwCJa1EjLhMzaXfyZPAG{_>{h8-=d<74=GBD{cEb ziYiT)w3hSyvz9qhaV`DEo!8Ss76?Yn7#p~xF)83hK;5M}AGrAp2M4)CZ3{2G!vwr# z&V`mGLCw=+*P32v9CS;j@>j|DH`&M;=v)-4dGuR`jNLqK`t4RClUsLv^}ArIbd3ny-OUzn=HbC&>A{ zQ)fVziq}c#vt8Q3WAa;8_ePo#>f>+^1YdNbL4rd)*C6&NAZVCu-gv2U3eJq+JjB3? zWhPXYjrZ1(Rw`Eo$#ce8o^U|`Ky(+|8gM~P=!#0k_PU>O6XZud@kW480p}%6nxc)e z(`^jcoPk>^`5=vKLAwV~9~$&L&*42&doJI)gMn|g50W<~8%VnXcrU0$(5 zv`*BY-uep&2^szH?d0z}FU|jJWc9(KVuJ<02#t8q>P6O8MO$ zyGp8N%M!AUu0Y<6J*0GaMg4cQ(g)IcAnp{g;@UjOdkOoDcf7$Rrw7di66C~rkLJXK z+(yA#1RK&QwaKnA2bwpgRqa<_HdgJf<+R@QUheDbo$6$fmrkmm{H;+gsKR8l#L+1p zEb-cQPZQQbkQB|lAr8${7l>+3maX$X`Z&;2RM2w0KVuh{8Co1&Y87KFV>WS$WDd+V z?JJ(T;fU1LwcY7#slU)6`6duU!MrA{V%6@DJac3-pHeeAsSw|BP@}li7iS-+vc`q( z-QU4}K4`;o`9wFogc*pmnpzYi%dY?~bC;BTU3*l3uV2ezlVOd0;*UV9HC3GZZc83y z4@TOS*}HH{c}!Aw%C>lBiBjr*EL2hzZoyLDrg88333YRJMu0pN57bDIA;qTiX2Xdr zRiSqphF08UiED*rY(}>c#&d)tyKzYFCbi>2vcu@_B_GcA%|Q(>+YHLZZgIN) z@uob=vGaQk+`ZlX{zI8O4$IqAYvVmOc?SIVIZfhtNxtyOzoPpql$1}LRQZFGCk2idb&JQ|TS74s5uAiRv#Tq9L5( z&dpfl%i3fLv5hBC3T5d-F_ikq7?Z9L6O}g(u?SAR7oczsSv0Skolh5ARCUyhN3-0| zt3*YfN0a**t$^%@VjrY_p`V2{s+f>GTwU~KI0Xai#h!h=jz9AXn{jK|0y1w8 zfeCj%3ApIB`-$;zg*6vu?Dt7F5s`u)@S2g~y8z=t=On?hgKoV4UU96l_4-Dq zh&uiR#Ch`x(MC-&#av7lLo(y$6c4LY%eua}v409jv5V&Zv&L|{)rsrde}t&ZiZ$LM5;v>6va!G9IIn1k0ZXjuc>^{CV@ zE#qH90~G+WHA?3!K#aAnxNv=|vj+-I5an3tOSkC-+`t+|6Ef%=z_3O)4WB@G(Tqe; zRO6IXeREPkH;Uc~9h|^|Wav}W2)`r8_I7#&J2jXKo5~3%PmprA-&u%AN{U=DJtTzT zTr%9V{B)^)_P<_=fcw{?E(Vj|%^I55aH;IFg<$ay>!^~RlQVmBuZmpFuR zU)q#`mwc`^!XS~7Js^RYT$F>{>21%D@^{Y0y*+*qvRt>}=tp9}$Q2rN8k>UnPwBjF zGq&s}oju+DmArzmx-OCM7QT62?)NI~JZ$T0!X@j@Ur2c?4U z7UGdgd**d1q{9?c7-dWD{vb*#2!qbO!Lep46$&AThL#gk z7lyPT{dr281@)IL-*tuv&qEnr;oZe60>)1A`^5$r+sJ=YC56C@BMI@G2I|dyc?w|; z`f8@_^=k?2>J4|RZqzv~Ix*7G^j9H#8IW_4dgBYF0Xp2h${xx(rHX;Y)P4#%BnXnL ze5`R?jik#4-tT3lRCC%1;c{kF=K`tj`(O=JwT(ZiH<>4PyPOq_!#$Me!}b4$AJ@Bc zc^yMBVov@|RlSFozWI>9J{v$uD$h&(ztw90XMA1S&TOD5qf_vb3H+8hT>~QZ!rJXx z2*pqOC(JrqrA1m3ST3B>koKV}fiFw3fA&_~AIZU{@lPiW7!M!!vl+^nmxO>GPt&jb zGfaj3+c0&2CH`OQ`~Ri(1^NM5Oo#eZn@F~e>w^}=317uhMu%XPcx%G}a31*hvs^t) zvs*~p=ug2ky1s$A)aJ2lwqi8_Y0XPmi@mseaE**xd1(?nC|n(3Pto}~&;R%D=Zbrp z(sG#CMKsi!PR@S#67X=-@#V}tXK;!D^c7jE32=YJg|9Q~U5Q84;7nhjZ zAwkoz5vChyeFUEc4>r2M4MiDNcRAjQ+FoXi5hAr0lYJc|5fauC0455z{z%WSFlpq&b* z-;b@G7(9Z38c2PSlJGnmadKJV>nyOHU(hY+YG1X=9vPp066B{XCD=nnNHJe5nYfP3 zz2%mbXF0INk4MG~hM*h#eK3ZMl#Ue2U+)o`D{_`j-0^M1(au(n<3qUnLoCF0Oi127|q(^<6@&JFHI?8sV^(HG`=F5A}Cw(`640Y@#H> zNXV=cV5xy^xqKN=YvNhbZeg^EGRv6bz6rvyLGIi2*5jsuHKX#u@97tOsqs(b!BbqV1`9 zkacn+CW^MxeWBHwHd31}u=YY}ZW!la86Q{mdI^DBOdaXRru*Cu0~Pfxx=Pe)8OsTR z2Ej-M;}m`Hu=gIOO@N!Jab{3MaQSVu$*l#m`>`lv^XGjQI-n@b z?;xJBUR?5_DTAgRJ@T4p!+tb>+YdaYmZT@ZCe_>QbVlA|q zMd>1|XSYpsqAuaDC-G7tE_uu=1|EPkyv{a|OLKAi7Z008d2oCal?>qfc53_uyrF{7 z63y@>cP1>A>zB_~*2RZ-O=ca|6AP6cu-*xpijic~KX5ubRbJ~3t+mTV4H6KQV(eNC zVdetwkCK~-oV>bUTbl77bG1)=#qcL@-QyhNKYH$wGY~ZGY=+2!C54M(h@v^z@8{oC zT}Iu}D|hJXUc5?{I3b0$uQor<_aWTw?&7(a*p^Oh8P=y786k%BwwIQeW}Qn`JwQD} z9&#Uhd&Es#;8Jn?%ygp3CQbN2&FKSrO)KQ~sd%6BbOsu?uU_PP?K-uOpW$f6f)$7H zDMcrk8E!LWNq>XA_5T#T;8iTGbm6Vja@cWKp^wsyuQyMnG9Tb2Aw|-c6J%@l9Xk%! zT!aNmLp=uMY($d%J94f(fyGa4AH|wekee!v+VlQ>OlzHsCIP;S@3HGtfXHUaH*bzC zE*I@wzB-8xO(kWf03erH2$%5++O9IGLRUCmpEoOr)08c%e~TLWf>-~+0_}mTh)8!) z(<2s~KJFv)Oxo487EeAJzsa;1OJ|HpDBOdXg~{{qC(er=4mrh-#u~y867JPcMUPE zZHTr~GCZ+@-oP6tvA_%PTPgGvO*{p)A~D^yx&=vH=v~Qf$;DAhdVLU}!)G!HxOb8# z<9`A#=@hd7?J!98Td}{CtCc8JCybrG)2r`03Aj24g8>rP@)WyBcJ;IYpuzia{anA? ztSLXISPn~NmoRL{^V%$wGr;ksb=DsRF;8dj&EovHW0!(69zE6&rS;}1lk*l#ZknGF z-Pe`7w0A^y+Pa#(brfCrgA%w)hQzEck5G4uIHRZIOijKz5H&lW7A#6<8z^%#f{>^NnE?!O!@5d|SMUGEPFvtX(Z&C&T;X00WJyc>gqBywsNu=Z^!GC)kzL6y2i)O zxRSMZdja2@EMI3X1_>_V&R|+fs&BCCT_b|a0wWmH1^{hHv6Uo#s|~8&lJjP7-8N_%Q!s(js)kX^KL5DNR-{YASN=5b~Lb|e$ z3>p05*7aGQkv*Y>rwryRbe~iIrXq?XDBbXsId?S$<;xtOp0UF5*(2m;629op>5r7Z zsRW}3lz#81B}5oy&4U3)<^LtKJua5nZnJLPG%SaQ;=LgkSN5HMCyG7jf+*D-1itv2 zsulg^Cd>MM-GKYz-reKhz(G2*3VV&uT>C}_sf}o~LX!eRWnDG;o0S4ZX1o1YHq`!S zeD46j5_6cJ1nd0DHLK`Cw8dTB3eV>C-rRRDIQ7QGk5+#Dw7=t;v}2|y^tvbj;=SjY zk#*$@fobQa*pBhVg%#hIle>4)8zz+(tF(PF6z=Loz`uXW2Xvj~=S?ZnjzLDrcz_$v zkm@XR0pTjhqy|*Er`TW=aIM?aJw!Su&hgZ#)1u5CdS7`xt}biPuc<7#B;AjO1y<{; zw_N4Z{&ndbIs1>)fQID@^h9*3wg(+sDG&-_#Gf-t=u{AvP-@HUTCFcq4Yp zrPVSnN-hA$CJ}rSVBJcxpP)kyN;=y)^cg&9fTy5{;i^21m{7p%5lS!%Sev;<#r-}^ zPa;DBWufwCXo16gOHA1UB+C6TQDe}~(nUf(>fuzA3a44}b#k+`I8L0JkOlB=a1 zI|z1AqItThJ@*$ld#PlGT>*fJsT}ObDIlaZ>MYPc?y;R> zDwWUHG!%GLmfE9f@;a#apfm#;$JAniD+rd#q%(`9 z1IF>C^4nQ=trT&;R6~P#SkTqY)1eHeaeJN%H5{E42&(!w#pgKj%-WB7dd-tK@Td)2 zb-|c1`}13i!W5oM?cY?P2i5t}HYkosDUjn7R)W>~BxbQMN$LtRu<&Y`?J~UVt?9Ce zvixGy4oq@1mPyUvr4B zQ#Bp)y~3u<A<6I@Zlx%T%-aP{C_Tqi#0YS}q@W;vMrk!emfbLo9JV0)P3gyTK3TJYou*s?*I zO33n*kJ_6hD93bH=85Pq9(2m)gL8iIcRd4s2pj~DTCOLeVrqktgMWVQ3A2KOamqB; zK-?W&DEC_n_cigaW32WYDIg~ZF)p=b3o{AmPw@!iQW%Ymc*(Zl1`+^?5&qGT_B zZSH5iI;$E`m;6ayx5+Pv<$=jTv!dHCn14Q|EibCWvs>FsG+Aay-8iA5TgJKO9Hb_F zwQ_cz#AWHJu2PuWtgUS8GMc#HDcwES2!CUR%r&N02iU!Fi{wr>=x4QLR}KZa=u0;% zl6rBq3O~hLqYKs1=M9%%d}0+1mUbw#I*`fAf1!RO&ipO~RKrOJQe_{VUixEIIQUW} zNiVcM&8$47&e>WKACo?|p1NIA265S-QmSs>=N#Llfz7ND=F-LihP!68T-Gn5#DXl~ zKOMuy{IY5@N-**j=vSe(g&Q<+_O5D}k#T@VD(yFd^k7Nf=3$`wg@xPDjTu76xe}|V zK+H2!6Rs(<25>A8WxEeBof4`y0<7-zQOuYR zA<*39=C2l`?~?xxpFLB>5~92(y}Z5V{4j56c_4N zQ|^!Ed%m0y#MojhPD3=#)u`SV+x)=hCm%l2Y&iqAd92Z7n8vwLf69s^G62fG*Ly{2 z{6h_v#d=71dy!J#M#*6i=WSn1i%F}zQgUZy%#?Q5&xQ*N`!4qbmgUuX<<2gbuaysM zU4<$ayVjs%&9D#9`|6FpF=H$Ze%QD&+t$nWAne&S_+)id`qXReVi(0cnh-Pt1PNJ{-y@obIBs*bqe)DBpaE&%F<9iW> z7sTe2{o}zumd^Ss*uAG>W#1oN`SF8t{O;{v9Q=(EQ3aGGW54eIB~g6oKmT!4-Ikr9 zV-{5*BTXH(E0KASi?&GHYQMAFmKl@^x7Sur_z@8M5VvW8fP?0T^2wJj+{Az?+)4F!1=j$6v|$zN3G! z-Y~&R1bqvyoltN7YF%JM47Gh*R~%EI60IqdU6}3TSXe?+2M)EIEqFRWX_p8uMb!-6 z*s7b*S7#olz<*qs;{~ust)WKSkKj{%^L4rfwr2UH>h?jL_y7eOR1ox6;&N&iF69$e z)*`*9mloqh&OH<)3-WN*D*-LJL%AJf14kq^NDgW=fHXl;nNO<$9}&TAmgfDD8tovj zHA#1mB!KR%*LlB-9^^N?fvBw%wFoHgqwnaNFg2_tx0b9YR7)wM^`xU0tP$>sw#GPC zg*h`mv>xJ|tBfDQNsW}OlPn%<_pVn=G6ZX3-CNe|d0`dlIsh>=yEBH)lnw;G186b! zu%@@5FVwd1HU3Q0Qk8B|v{ClJ80^1Oy}u-BYH`s-4Zd^R@wQCuU(xuAe5&WGjnk0! z#|&X*2ee=f^pUSwsfkU`kpsHx^EY+$QHy?eMXOH6-A~3xrh)BP5IA6 zSfpcA^CtoW$+D!Hc^$_%o28dRyS$X6(4+*AJ6&o|Zla#gptJ|EqvO>yhe4I4K{6Pz zvHAvEG;+6F`pEp!V*r;aof2f(ZrUeSF6!6f(EDYUZgq`H=A<+0XP~Pk=27k2v4Es| z&LdALRa+Umf9?TmyBRV?^WN#ksuvU)J)!57=w6`A6s;B%=BH8_w7q> zFYLZ3z6Wx3s5o$WodXQ9{;AO8T0->UY`)}u1RnKxm;11o$qC|>5_M$bQserRK^;_e zwSX?WF!H`_A3FF|lonH#X1ll-`Qh62$rHP$3*thEO~a+*0|lV%gh9`?YY>u962Xlr z-}7li+K*lJNI^%?Ix3}e`;)N4gP$4E`WT{*>+vpWL!9i>A4W^oth}QEkSoOaL04c0 zYtMVwfV0(43tw3ym^qaxqb^|1+J@;LeR->GTt5tyRbOOtTBEWd&2!m|UD07u#PInt z(PNqj%o@RMUYxy&Y{Gf1ZNh5{d48@QG34Hb*!y`r&IkRl7L+|-Pz0A9 zM6lO1>LCeMnGLGe%e_YX0Bk-^{kRjk9OcAq!ftvR$3cjI49Hl4C|hUh{uZ-2FH*31 zbN*jl=t*+7T$po)Pygg=x6z&QfSF83=UL-p{=YPkWrc$q>h+G3eJteMSmXNbWS*t0 z?L}WfXa~5iLF<7c`v- z?^>vk?%3()m-ZGmT=DamE5cx5G-w8YA6HRKhrY^F7!+Z zbIw{s$6sZV8DB3jf_HNnk9xJy0g8f)K{eMW0?us~XFy2FDKW<^u)o17{AF)?fXp{A zzB;{avQ)Ch)Z_}uN|)%*tyDMv97}Ti)!W?rvD?BUW!qBbJ%eK{(h*}1S`Iw(Y*k*k zC-z zrjR?{CkzxjeHk7}h;5iALCY`Ecej;g={Z?sE|g!LCL*`=G+Jk||???GDFm z9K-L2H~+++uXd?}CSnXMo?fGvA{Q6_@ju#`-Wk%j9c(`crPv>R;{Njsz5l*_$No!k z?-rlN^D9fA|A6#*zoh$|vR^uz*+eD0z4P*kpP|;G0W>^q{d>U1R0m?>qAox?fie@s zt~b6;@;}*4iWW=40gpt59crVLA|fa|&X%12Q(T1VUyuLSp~(pmqvTib0w_N`_h-d_ zy65dIEH_Tzq4SptHwu1af$zbz=6w4!O%L;s98vZiVJ~7{Py#)_W0v8o%Wq~(6a;Z2 z!|(rkdF*h07Unsv8a)@0N>S`3|J!h|gu3y_Jn^I% zSx2Sh^_OG~Gn>Ml_wqECq|~E4nX!Go?e~bBC)FEwTp+qs)R><*oO1di*p! zdR4ZBZ0XzYldTl&-Rl#&0xFT$C)`(KJ!(&tNtUW7jjI()=>}e&Xnj)zs~ACL(L#OE zLiDn=`Xuh1D0k8GxmeWNa-O+lZ9xIm5on^L;CT80#jM^U~Y3hQi_c zp!@(&%Z>)xgp?n(=v6t-!pL|75V5I! z$POa_e=CK{Y!^p^PDGcG^n9rp=*yqhG!-RK|fREtA6AD zO4`*DsB|8mklYyf@z?32*~qUgt2=|a=1)_71@?LV+m3}ZJna=Ja1?0}Q2utm8pe=fVo7xy2cyTonE4EsrXH!V?Ue2@Lbv*fa-+Rx_9Vvygs+#ngR z3Ob9SfJVt6-Acx4F_Um?*g}+q|DN5A;Ou@2*Z6lgdj3+`Z>J!OD!|4OG-uIvzO~9M z6~OBj(@1>Z;~OAcNEi#SUER~1F`$-1T4axGyFK_+0SU?DW1jrRsxl<(cBI24;Q^-o zY+IX7wP8mf?8dX}wrrhQ{T$V@?VvIJ@w(#^QbRr*K{`fPE`CnFEi*KbVO}qIig1v1 z{j?PSE=uid6SjN209QP65n|q1jQCk5=+hR4*Pp@E+MZeSsDv>mf_dE?dbu)_66_VD z2m>ZPMj-Wp@b0s;3zSa3kxOLNmy39oQ$It0M?&ZuX$suoiPw_>$k6K*D@Efk3N-{N zHrvu?;e9!6$T)c+!^K?b`SyDn%@|$2Jv5VXjr1A&uWpR&b?34b3oT;`>YnZB@(u15 zIM~3sZ22c7WTDAOfQIDf$T&$g@9~~xFj>($>XIc2yZQ5KiLf~r2`T``El)2$R9Hzqd`D?`y zb=~W?3>gH;xVzUC)!_Dg6}$MHMHKW5($5I+mI?X99(e$th1GU87P(R zeQOlbT<+w8TJSYuSZYaG%W#-v>rfbtArNcbUux(yM(os|vO~e?L5O zoBZ*{%wJn7gv~X~jS`~9q`531A=njFwrHk!Ct%VwDNyMPV?jgxfcC%Gn=$!!MuBVx z%p+w!>Bcvm$l1-tt~2jyFjQk;iIHpHlRU0>e|(}*`D8p=yIJI`_gI|T!#7sO?0-|S zw-m*H+b$4&@~!6G!WEx@7xx?2SYbUGVJ+&MCss3-GOy3F@pmXJ3jxmA1P1J6e~vuZ zrBGuvqGco9WVNE}AzXi7%0$van=kpbG+VjGZC35ust>nVLCF|->a844g5{sRanXZ; zi>|>!{Sktv&0N6Ry#zlX_y zDKMnMA&(+iwEZnc5mw=MSw7NZ(&WxKVOHaxydf^u>Fw2qQ65g%9Fr~^O|g3b-4FCA zSe{Vo$25#Fz>vJwLGhNhxO*SlpPbYPVyvv&%Nr2h0R~UcUm>#FsR+?ZseZ1rZz732 z?o$+Ct3$i<`8uNmc2(X{y~V;*LqMHmCDP9Xc~^0ly1CrFJ;P3s=3+cjy`hWw$Wa}s zpW5Nt)rH}7(x{I@YC_vz!!YSL^2uGr1P#1Ht7?dY!W5meN&aUSMRVCu_E%Ax(3z?v zPp2sy=`MPYD}y}AAT@cCo)G77bxw(k7~b;cX??ssmHWM50~55cx$ygIpQ@p| zag51Y4n}X%xQmNZtion}bjSqZ){~o~ zU%&gx>m26?9^Ao4?SJtBu3ehFyvcUkZ)Dtdzu=|W6<{@`tIPSuKf$s~Us2xY^_)+; zbteAQ7cbnwJ6v^W#p@9tBJ+&@)K?>}EzQyG*pr(ldv7CgnBPbqC9dt*n?R}CetKI) zS$?##73X5pVi78r7L~@Pmtc1L1FAvD;wO1a3w-52u~g}kjL{2;!d;VmagEbhst#_p zFO_lIxDx&vCABA12zuczfTKFeoA1A|cV0nFzFoTyAiYcPy%#BoQl<9}AykzP2}OFd z&=EtCUIHQl0Rn^`Iw-wHB%w$ZsRAO>RmA_!`_1gxM|;mc_`ZX8<~;yH9_AjnpP7g2 zzSdg5>(LxO`%C7j836k1wL$bFak>kGqr|IfnGa7sl4ASMH!u#~SkqQ2K$I!m9v#sE zl%7KL_)QMGWwJ@v1Nav%db!x(NS|J+g~V886j3kSbUvBNfCZxGziQ3SsV!k(Oe>HE z9I^o74c%dPx#uukVJLSYAl^a{q388J4GyhFxT4${3_9m}`H?BH$~B@n+2TYwSgdwV z=3I}-++z;MN-K|vPO!ED)=G%`%xzLrUV0g)#t*zilcV2U^|rHe{b(v#VLL^cSL z>HG|oE_&b!3a3tYx8i)l=}8nw{Q6byM^ZI9N$nkbBZu16mOmJ_f@G|LfbxjpI5=ClvJzzF!R=PGn_i6NO@%rbgFRLs<_Vhi~ z_>3DFg9H@0QTs-~+sf;2f-@&m191}xxVDXS+j|kUxA>Jv5X}@JuZ+oz)K-$^!Nq8c zD8Vw82OcyML|nINl~x%#M@*Yv@JMieF3nB0%J$-)_e`pOs>mRDUc+!^yRx@=zm3R^ zD=0uo8I`^wKDwNID~tS?*8GY=aw87WX2Nk^g_|%Tl=HK3)sEP95q01e*ziwAD|40JKP2hA=AXbXH zZHR;4h!vMUlg#b-MkfiCCHqc4Wh;_q9vmMl%JkS7Q71iYNX!jS{%hm!7qxBe6CgX+ z8UhE*pXO@v^-JA836cGCj^;Xi>zBy%z9IOTme*5>GFq!>(KOvBwo-)){Py=i+P-gE z#R|Q-B!Y8-)sCa#;SILuwhG)$CH;v_25!|b zgYt3oB!0)YT$Zwd$#%iZjnzQdt4E#Ka#^fUU0l4Y{$N*W5LFLtP+PIyo{t(@pB#u9 zs1Y-OGjWtvrui1TN8JsM1U38vkS*Az>#SWU(;e=!=vBP7w0@cTm|92H@Yu=P+_P=z z$imk%D367Anx~CiBG*lPUd?X^ z>ZwgsbZ2_!Zuh06sfPwK$Z?Q)k;|KVVIIC}Xh}FTa4LACpehAR-xf)fSkP=Q|1`&! z*@+>Yt23KB9GgQtrB5YNkviHc98X=Pb%I5GQXdvuS${yn(8;+%n)n8TKBveH2r&!b zi8V2a>hZB1f42)p&`H91GphouE>R|YPZ|pOvOg=BgR5q|gLEXiY~1BTpmQx8v9EjI zo}O5V;7iY`c02Vhkf$TwW2(Ft!ODuJcEYEq6TKJJ6(lyY^yY2f44K($N>$!W!2(m} zI#aNbMn|^GtG^*%V|nMk`1R)%$Ej5-`F{$5o2BFZL*IGmEEnID_615gTA2iIF%`?d zF?J`!k5)Wk+_&{?n$dDv76~r&96vHDxkQWMi`6BCT(&SKR&+Y@xjDEzeN{+Y8~+yb zv!qvA)>{nQ{OK={rb2c+5{K0j`w%z$PEAlsFf*6AgQ`tMlYO*Iz)*G9k+qSVUCmp4 z{&LsUpqpj=*qCq(_U(Dax5UfZotMtZTu$`ganykt$sPUgEnqs;Z;X}QxRdz_vm&MK z2{_D1syC8s;sxwqZ$A1ocUkpX-3jBHs*hsD_KEML1Q#ZlKGL#|@s_07fGjSthgW<{ zTeaTS-j*kADW!tPKTuHCNQ7*+-@XG&^7E9j`8KPT%swl`!we4I*Q6s2qju*%G{2@) z*Ap|MUMf+Ajy+!ut2>Fb60zJvHDsM%(7wjF-K8L&QxShSy&Lm|UR$!apHjbJ&BHOs z6C?{~YgN1Y%aBNpwBSpy^6d}^G0J1PKR_-paVK$JPvXZ%;vT?$2lHC(7+k$DxWu%S z_4@3gXoa_Yb#kNtfvDkYZX=^7QVeBs=mxhHbX!yPZi z)oCprS1_y7hU;psCF#!&s}LEq(%JCxXi#JIf{^4qQa*V+657cXOGz2`{V6;$C0l(A za6Ll-u^#ZW5yo$l4+NdfN!x_JLMwFc=Y3`uR8PA1?ZmsRHeDdNMCEImDknmnry~@! zoErIzRNW<2ozxGNn!A%@$WJHfb6#Gp)KFG$tU?8qcSko?zqq1&B(o*)86eK z1URofUDe5$zd!9<1!!8aM&~(aAMii1EU@x=<#pfYO@C5F4#1B5M8-ZcV3jb?bI12z zH2LV^*)c_L;@-2(a|yo0Bjb$cs|2G_U_v|T^%(n7DS>weNq+lpX0DR0XECzwT>km6 z`Xr8!{E3bBuOwc^i5vV7CqG>8M3dp4?NkcWh;lUJGh$vxgdMxQouV!c5VXtuuTnbc z|3De}KWqM<_5|jz)R;DCh&Z1@`qq!#Uc7zk4Y({CrTpg|I(h&QPcaVKL_^Y z#@+!KlM;}~aK8?b1^hJQ3?6lnDA%7Mi{qnP(vxZ zKwvTDz9u_<-?vnf?;8@-%D)^{X)R77bS+>3(`aVCL~m2U2Frh2k}KZC)5Cmys8{W5 zyFmw%$`6Fu9RWL|vANYcBxMaRE$P~B##P{eoku1TA8nyBJWn{ku@A2$**%H;OvvTl ze%e9)7VWPf>ZTxNxPP$5ieCEk%OK0pw9of($^gnq0$25lNM6enN0iki#^OFj8VrTM zg*tR@W{$JFqN+;Pp#RdS|0PKU(m_n%O0b_Yfy@X^t`scPGa*(q1D;A$f`Rf~?`s|y zDc2D}vlVJk#4Ij-Q1%zKXUbx50kH^>VYw@rd3eu|c*xe_{1kt~*MgTZ{1Y1WSk7oFpC?dj8{#BWgF_Il)$R{TQwT^!K7U-$&bS ze=ddPihq2Hh2(EVRVoZ>i*Y%nffXk|lH7h(uuwmAMxQ%pGoIAja^Kz1)80j2^=0|4 z&Mj|mn8%L^a%`z2`*&8G<}P5emzsopRfjJAABqb4#Mjp2=3MH~UT=c(X?5iR8_$QXMKLV2Fmy zSJz|Q^jP7~67whgq+bG}Yp4FtD5?Gr_9$4JQTWmil8Cz7S;3tF@(#1kwYIPB&G7Y9 zkt)YR#OaxJu?(vEP?47jXBHWuDXt>`uSaCvlkRAHT*ZU?tgC^r!COpGiBB;F=(T{L z?H}qjB#O>+!dXlDgdoQr*4dr#AtJ;((TVZ_K2S(>iOuK;V+vWU;%EoxZ~4LtRiaVf za3z=a7t$M?1FbEr_@Y9Z-wPTkVMRj4ZptIXUj2=YT$Z{9pg2*a$Tz(ew3lAsqRd&z zTvcdN`(+hH?A@9-`9lzjr9R2fW81Z3-x!eeQU1H!7VuJfnc;Hv6KQ`Zuyr~ z@GC);6Dz@_OeP$P$lV)pKR^P4lV2|m35}OyLu+x@4=I;@FzwI(syd(h(jfYS1J?pt z--!cj%`$aR7J(`!3G=$C@V2pVYEs7fWMX@3m9{fN+ zT`xF!S34T3l|_}&*1)PLYn5jfVrM?6^8h_q57%Ug z3#&BzZcisp&3$dUj-hG2D`oC4kFTj!T*+e};cw299*b}cRlFTN3u;vilyK|tR7)_G zw!gNiChmL$zoI3K@Z=HzMhAR$=rdA$U9@zO5~E8rL5!&irVvFWsoVkd_Vn1!fi`(O-bLtN~jv1QQA!9ZG8xD=C~M9XgEN%pO@@K zC!8PMNaj7=&a0d~)LU~Vw0LH{aex0A(Y!6_jxlW;4va*_<3(N>=YmZv$TI1GL&j!69Q%+NaBBepr7?z9 z+n5QYSAp&q&Lso!v}$-ml#4#JZOaTqFh~ zXz2zKufc|NS%M`7<=ValY@#VM3Hcuq62`1*RPJxljI7d8JtM#dsFOVbX;9=pWV~uA#bLSZG|CT z)dnQrS!9x4S}VQ-kq(Pgl3^+m7~$zSO{n2ZO;5lgT`RKFIF4ZIcM{`b@Qf&%nBtx+@M9f{0h@&~dy)LrQz+p%Rwa91 zI~sgxIq0nHiWo{YFP;_?Z9DR-rXk2rukEX37UKJ`%52I4>8Q#qQ;D;Z1@K};lqEDS z2x%V&Sa^Dpnym$l1y-YtUFAhLp)~QTx*gVfkbM0-_`S8N2QWdg6+&025$g^&j#&Bb zz?GSH$8ALys=$t@4A}d0|51C}a`xKKV%QOF96uq}FvM){?%WpeC-7zAvCN21+ETdG zetGKltyxxNP~eE?Eh>9t#!{j=5lDSJtxOzHr`s&FdK$Ofws^y##ZpAOtf{c3_aOe! z5ib)){>slR*b_-^h zM<3Kpl+gum3Q-DE?P83?yeCnh<%f9#^`mgH^p}=CBs1TS8}cZqd<*(AOo}=T>3nA& z-y+uMA`MZ=rTAdnKuYYz(<`X*Hb!)bjuzVEuT+0n3@K`nh?o8O{Hyd(P@_J6Gx>-( zTe(rZqa%BXkT10K^NE#|iWCUws7l!ud9)aufwABJ`mXobDPwSVEih!n*4|`dXP&k2 zMiiny5?aSy5acIuikNE0bFL<*p}7;Fd9N@*Tm{bgeA8s>b^-m3e6O*j`lU{m?3_oP_+w;NOUov@IGS?UP(3 z4x;LQKT72DaeIS&ESr4753s!Ny3irR>;$QMSXtnGi*vDccH2$SDafZ-Q<#b(SQ)K2 zWO(i2YO7}Dl0O?;-bW;}K+z1}>IL`vl)VtbhWUepo4rV6;Ag)C?zU$CaapXR=;+9g zw&U5n5y+dbVE=99&N?3Yh+$Pam66;V4Lv|y;0#j%!hFViM=UO}G*9PeeI~enD#Sj= z+|~w4-;qs|60qr9D6#8MxY+>(FDo&UPs&3UC%%=p`cMT?dK}O`$XL6fHRBnUzhqJB zPt1tVWO?s#WWH`@h_5(j9y|mSrwrX3U}&srMqydc)+AUy-LuiU4a#R$KNz>AhR-Aw zW?=3Vh7i1a`g}Q<#vLKzN81jp=xr}j%;h`9qK18g>~ z2j47QFtO{Mg}>Zk?y__)_tR1rXC=Y4-PRj1Y{(5R>msJsScSH_F$xwt6?2uwP{(kf-4>wR-WcJ%gh}i>j*iq3-sk|PxJ%Hv@m=#G!@=nzNo4k8 zCscbvQN$O(&C}nA7jMf|NP-i7CILT-|CQXE$h`s%wv_H>en>o!)PTp;y1#9Rm@1gV z)f%egX!5XKRy`rKQTllIxd8sQU3@;&?iYIg9!9ga49)Det>rm|= z3kvbhlq*hWM6SNXrhxf|rXnJ@VuC0$lq4Z6Y%7(XT%pQ3pbz3QS!a{2T3pn5O8G`6@LMi%H zQKyyI@&jqgB9L^Pt(-Q9V2g~4a<}GZB_jH=EkOvB@JKh>i#ltgBV~dh$6Ow<)*Q|f!V6}N*6hRGg3i1}=(ZwO{Pg$-sn_zi zBd7{L4D#Ip(&RtTH8u>d|8-aKJ6Y|_V*%j=%NIVKqM1w<5yIr#RWZ;R{$=Pr${uS! ziV(PtlnEn(5PH{kZTOlPV+p?l{2^s5eD{D02q*}QqD>iN;f7_O+#znDytS1D4*23i zX#;Q55$ePwD|QP~*6u$EP49rNm>PJ*6z167$1|GxbD$ZE1-;V8iUfw{jRKG%7zI6r zUGl7}Iv1NGQ9~yhMM58FF*_nkdyhX#&2R2D@QelPF6j^iz^Sf?xCCOeYC6^jGmgj^(G~=pij#=?Y==#d zm7T%|-^0IhSQW6dXm9eCQ31BRTF=A+qzH-6M#0svoCi(#e*aJ9K3f@r99m`cT>VW5 zF)?U;tI+5t*g5*4_?vKMyEm{|zrqbmQmC+OFF1TL$H|#kK@RHwNG8FimncKCR!5y= z@b}aMKsG46S&vt@f}>IRqsQGAj}p5mOdq$D5M z%6v-us9Jy~nH2ka3NT)i(b%?u*rQKjCCnV% zV@v9y)uJ;93=`+%ZiR0>)rkoRdBIX7vzpeU`{JyAB6Q-o_!vR6`a)>bDN?ZJ;KU$| zV=Zl=m-ALlq3FwA4dDc|oLa+oO7|{)l2%Z_=z*cVf5VF9j}5u3xbChYB$plkf*-A~ zx>luT*KUVDiAu0k3Eq!Kgf807h-Z0a!>!dty(~DAEUCRfoz^-2@7+<&y1EcX9#Vcq zoG6c;l)#8{6!@Hl(dCIHvBOAL#zD)%0dFx8VE%QNa;&QrH6IQB9jKQxGbB9PPU{iRq(5@L}$o0mhZex0HWg7%}yk+|*;|=6^j^ z*tkvWvAS>VLeI@!>^EHqd~KkQwfb>kRO@sV>C;LlV}Td+~@;iFB!^ z=Ju9&6}-b3+PnTw*xLq)wWJp&A%gvENXa#mV_PgPVZ0iNi?#}9m7zvG`+85|dd@ev zH21iz5vi^{2paBBuIlIx;{$01*x*Xu^VHEU~o7+ zdL6^j|JX4dJ%g58;glU5KCEHUkVn~pEsm)sdoaNzoaOD={{SLq!&z1G=J3~pm7KXT z>nu~$P=gar+TPFWzz0bgWd~L`_WmP2!^NUFX-*$H5+hmz4tp0mavn=rspci^3V82hZ(9{2; z&Pb+V_Ka$CndA3&VBDTf`e=ayOUAZ|@68wUGntrPsh4&o{{R?KIu$T*G_KO+UjA#jkh125monFK$TR-GKN_q&^Ykf!C&2zHWLuWDcW^vLXQ^jyjQ)#t_Y1fi_CetYadVo>*@;i>803(3`csES8I^2dy=Aj)6FrQS%4MM-rs&I-xqY`Mp==gMPC_q)NsKeIB5t8pm(H=bD{A zvx%$XQb!(!IIldoht^+|Hn6dA>6%ZsGB}~Ks%^Zgd!?v>s=U_8*hTSoEz>#}oY*8c z5dNH@;_3?^K60xI&8)o_b<@;&Jy@YKxOYrw%3S_iRWOrz);TMl^kEw|9+P7I4*>f1 zuYmKNWe>h#=x5)cKl)VzA?+&hgl*<{N4Tw9{RGbSI*_a50%$KA_6j9g%Go=!6}Hj& z;{k%C1V#~a%AJQ3QmY_DUH@!jkAy6W2_2#SRdHvfE(QIYNg)fWeH?!oj!jZ(O#NQ0AY5z@F}b-K+t&DfvG4h#w;|XKiA2{z`2ibYEVRlJv9kag|$1?m=8k}9i*>Nu^A2)_0L{nDR^B`?fJ z-=~RvF(R93poVEW(__8}opF%O&k_WeCbi6t<_t7ar+sABpC@OlUem-;m>G@R8G*7d z;>vxr%HMmpPOX3*8a1#jV2mZhs>T@_we*rg*bmq!qC`u(>-V~lU5Sc8Rztq|lj3~ee1n6&;^Uv)f zpw7~|MAesNI8Hli8g*z@?nPy#JS`CZOhhV6|Z%S>)AcSwh4l+kQN4extb zJ_(LJ%4N)ijX@j#_b=#JCj+E>&IdE!F`%I3Fd-h+v|oml)Ch z^1?WH^#w|w6Vu5!Seez`So6|AsWxpu4@}^7o_`lS+MW~?pa;G76j*;cnYem;^~}2b z*P2q9tW!2caB=LnWz>OMNNp?z&$|5MmhF!c7ucFx}#1O7&@34oqBI8f3d8Qald&b>I6b=>x75Fy` z^RZ$=@U{*jn{V45rHY0TivFqNBhMrPEX&0D9>`H^ylH^dx4blm((O>J6=u#ee`Hgm z>U19iQr>!+g&^K}Vh_1Srxc;ynge%5z2Zc8xvj4;$Y9wdxmRAV|;r1 z-f98|M^1QJ?tIV$X&=4)2axXp@!Y|i2rRE!J?2uHfK?{Bj(_kNgN3F<5Jy|*GnC+d zm4gLe$9kL&H@h$*M!H4ylnx{j{T_7V$SuE z6p=6IGimh-qa%x4dS1lr(Y6xvR}Yc*ppURwA|cuJWI`eV$2z&OH0jHShME$NtkDTh zWtn^>5UJ1Ke;MNwUj%$R0h2rVZyxTsWQ_TTMf;yC)7BC<+KuumGXys&`(=o#~BcG|0HQyz5h{8SOl|uVZ(A`Ni^L z@Ch7SdXm6`mCB89A8rA9?`^j1E;5!J$1HSpejktP513C-$ImERj-lD8OA`wS@C0<0 zZNvH!*@M^y*yshieAx&%wX}W`$PSm4Dz5syCMd!UQD113{U)&4JUGSWw`%5O$i)G4 z&VQI-nVZ&(PNlU(UC+mHes{?e-dF;JEWDm*UUNER$Rz2+-hZaA@UF+zS(wpG;{*F9 z=14G;zAy)D`|?iX&R(e$an$r8^&7{PzSzQaPc4s!rgL)5vyM(e4k)WLx6bW~syFe>h-pVMzJAh6SN6mVFx#-Z&rB}Ppmwi92LbaAG z3%$azPnb;k>~jv1T&r5WE%iG2A3)%UdZpOwx$orK-HAal43#PeB(a-aQbo?^?hZTK zYgg%PjGxo}d7@k$t_qrr>8GXg9E8W#feUB!C!E?Kr*W?jdJ^*P@lwK%d9X?ZCojtg z!LA*NvVyeYbZlP!kgQiN2nN(FE!3(HjM!#x%(|}jYq}${z!2DjtWinN-pfihV1dt} zNxz2UM^BPZ|g;RVMSthjN&>jv!CENmRqgeBp59;bHRkg9uTSQyKa ztA#dRLaR-Me)=M(pU=JNxh&rZF)(-i1N_{iaphaR!iNzZzp3fu9&gFA&@sQ3`pvno z);WU&m8TJCZ@`duo{x=ne2<(OIiJ`nOTlO=*QA0pJ13s_BX!A= z?!YrzIy;t(z`^z0DB69e-4Q*x<(it#U#7OPzrp37)$0rg%Zo93Y;@MiTWrLg7VuNH z9~i=^y?X9nxk5ero2#pKL?;i``&ls>=fB_lCYcRv-XhE*5&t_?pg}-S(%q-Q0KiSm zjqQH4^g~Dv;QRaLKW0(unU#+6{HJ@of2hu{BmdO-RQ%l}{gd=W`Dtj^-ezgLFrl3g zuT1A$uW|FeKY94LPTc9e=1wNlpLfXUQZ!#N(z&rb;7`}z zyxr(=rKF~yak0}zDY;iR9aO(Lz*c%u=(Ae_ z5^igvEtibz-B$O2nbSWRUpV%og_K0p&e~AmgrnBB#kMT_Z z%gCef3=u9ESG-VK)uNVzRrqMhnZaeS)~{N0TDksxPrFWto*2kjD!Ts1cEPOz??IsEuwtyy#kNu)VOxZA%`e)JK? zST^pz!4!(M@?VtMr3bG%u;zaB(Q)E(n z9xp30|BGAB6gRx0rk$B_9YvW-`T6em3(r%{&o){f_py7W7-s*=Wd0L4AqrDy+k;Lr ze=|J}V)j*QdtP~)u}P>8D2Vr-TwBjy{$Vfx;+)#mgHQHn4z6;(<-!pP!2{JRQg*7< zYi+Hhp!pS23d&k-F+5Q4pjR_tx2A6j$|F&u*^90+b{A2Y~}M&sWKfv2kTv zkt5!wXzr5GYJ=z0ArN-T#lTA90VPWX*e0Wld%xv;ooal8WF{w}@j3vOHXZRa8J*|U z8T&ypaa`sVIY`rDciI4C?#KG8R&1UllG>K@2>CqGlJq@`!IbHl2mOq(Jvb}JB9mB7 z<)b`x9XxI@1LlJ+#7)3Hl}QsaWKpBjwlS@8>Ia@l?H{=-oXdb8XYpQf&EWCCPXo1D zzMKLT)yc-#)uDx)8CO&NhB?^%Y4mc9UZ4?L++)0qjM`^l8(inlhcp#f53!NK0Bn4H znsIOMHhUwX7_`2B6w-CIDnBdQ+UT|ct}Po^){Rl#OeLMJmXsgvcdF2#11p;(0Zu_j#U??2mb|<(DWgbzn*kpmy^!EUWCe_@l471Itft1r;-Adz#FyrlYwa6W zYI8o0&Q$9^{|6u!ZT+hP7ngq9YOGe3w+(B6@# zq!%O{3I{XCx!_c~bo|vGZa~$4bxftg0V%KsjLk zX9u(=0U`D)hUzd+wXib1tKP?s4ObS2mKVn+ZM^695kgOj)L~?$j~qL8NY-I=De?bC!icUy zH@2xYW#icO7ZV|UFx^?>#aoSK)~t3)tqKulD4yQprpO#8jxCwd>9i1Mv&_zjhDIbUVboQ0bH-U%i{r-@u;Z6+! znfc1_w~yg2($cY!R+U9?Db6aSe1wq^`AN?wHOTy05+Tc~NG*q?72t*xd;;U1m57h9 zQFByIm<}hmAs9_h2}d#ea-k3l*6&Yj=bgh*x%9@etWpC^f_PzC#9W5llx43@m_oK< zqUqcMAy0iigAvXvN6N;eZ@kihe9+!Tqkl4DT3fb|o=Yt;!lvlc>h4W33P68#rK<3$ zWz-ZLmHZZxSH7+AUcZ{XfP2n2&;s`SW{zQe$&yzEZ&cm(?92+!Ner#ZN0^Z?AWT0( zB>>6Ua0!65m60JaA{vjXMgs(@IG@SkT!47!XtDLAjy;ow)}guHc$NGz%)u~Yi@knO zBg$^a4c1ci2$2luWB&z{;0l1vTB5)rB86~>1WQ$J>BR%~{N1X2MN0tqywWyZHTW*R z+Jy~X&77tRo;JzIb(Z!vA1OrbTM4@fW!WKCnR2YmIBQE`4T8Ok@+KT9e2Cg1|DC%Y ze8zQ6V^QzayxK&HXQ(sgz6&lZCjJBPs_Xeb6MwiNkFZL@pjDqCHd z=Ft=G1)1~~$E<8hv`K3HFms40RsL11!S&YJ+=O_^pH;XOQuuP$?bH1{+BA2pq=hj< z>ZWyFtA>pCy1cn-oz0(W;yOR9CH%ld)MCBTemneE?Q~%JMW}Y?cvYg^(#{ZKV2(21 zk~CDY$@7zBW)sa91ezis&%N!1V8iE!zsP-*P!1z~{cJV$FVDinWk%#V9sOtX9~YiD zwyqV9j(}1ar8nu)b4+MwJ2P3U$dGL^(n*CumJe1_s!p{IZKk3w?TLtMMc%DAJ2#Ii zmubn$Fiop@weG#VL*(bxctCO_pE3qZml?qn1yoW2-WQvs+;xCieEUknrJXjvqP>XA z^L)<|;42@Hn9)Wwqvzrplj{y@h9cW_GkR@~G>iIO(`@$}6XkxnSd#jtnkeXBa9bNa z_WALxon-p5{z=_vly=5$`}q_&j&-Bm0tvhYY0nuMe!p4O)e+~uN=Rb02+paBWUn_0 zxOK|jYUfJHY`7}&5Yra962v_2Kg?Lby@_m_>;08xzpvR~B=zT~y5tkDc>3bBc^XXPBvWLt9duo=4kC zT-@#a;-pl2wJ1G2w%)o)_F1G8OWf7aT`n2rw>sA}>8Z?ut+Y_z{T{yCeWNm=t2_hK zPq7sk#aqrWnCC8Jj)mJ{45ETbsFFv)oyi7#G7ysp%LuA@xUFYxLjN(V z_S4jJv(An)PnC8Kt3N4p;t?l#XaqikpxsIDEviOD?2J-?D!ftU`}HQYpZ3AA_|lV) zZi+Y*k0II38ALs!-m5z-Cut@c@twUoK$!b_o)lYnp$>din1=6Pa@*S;3C@PbMcpQJ zeKL`3h5OtJPgQW=sTvU{#H34cPJUAw0F~dZ`P?0!^-=fZMra+8tLt)CV&5JITFSR6 zC`Zc&=OcK|94~$&si?qsm837q)&0?0FUOxNBhR7j=h9D4j233R@9 zvA22l_}_|Yf5O^DN~BWKBZ~7Tp+!tAT*mQb{F@aXq6FVqW0^Uk>;chw?Jr?`{wgAZ zB{jyE{m_ifw+F3r`8dB0sH?7;n)@l^NF5dwUU+EFFL!|=iiwV3Nq3O7ysB+f`91Gn z{|x#x8@ej3HeQK~N;Q*XG})#huaMKcF|I_(Rvj#dirgvqNa1-X5_!I4I# zXk1k6ye9;O6C_*?|Aqta2bh_+6CT}O=3I|iETj4_`Y#jwwJtTG<&Fx$o4Lxzj$YF? zv?;*vK+7{di9+`bjNkrX&Na|0HP^eSa>nsigN5TqUv3-HQd%}*W-s7b4@-cmwW>|& zXX4cC=`mq2nE^TaLB1^PPU~)wNR0B0&)I53orYKCwG1);wMoYLGxQBCriJSMusD%W zZs!DO=nus2=fjo1@9h316^~gz`-}vBW))ic;!Dtu7XMYwrlIF9hf8YyPpXLj&b1}R zlZ{x&6o!cq{$^gtV*x6Kh=gAuW>P(r-y%vm&6acSUAc>0uq@c!`w}|capx=Z^C-du z^1jr*+~nMhXyY)mm0o}s;S=CLAOG`vz%m?}d^?P~bCDx#>;B<`zTk(#X5EDv>uG$u z8psg0ybXW7WrH_X#*Jyb0pmNqQIm~(9Kz@G7QXvPdtJ_s0yVEHg zD@wtEgqA-t{t#$;q8ocuCTpTTSaMXq-#ezpnGcrYfK~9|Z)-6Rt8bCY{{iPu?cAF& zb#R`+!%dcqTQ+O-EBNg@GV)$Uyew^V+`9opZ?=^CpN42_dtwz9ZzsT8Yot*sN7rpX z?FZv>Lm&nIli#1y!m?6V8-vuW97+Dz*IHWartQ{aVIm^-UtshtDN5#gtVCFqYU93J z>C)t-Qqn7JFXHU@GvD{nO3`G4D5iaxEHiKF;LF~C+h(376kEZ{-5I*= zx2mV5Hu+2U(8OOpTgYM2&;L{RDKw6|`GfGd7|UB%$-UcWW*u2xE#6Pbh+F&{iW-V6 zOz|XM-!F!~r8l)*i?0<2U(_dOrGI)j-`SdKpK!-oLQb?KNt^4~)-Y~Dv}Wd+F#a4Q z-gUaTw*?%g4OjaIAY%bSk|>DHtT%5aD+Y_MN;xYP2jFoChE$TGjyF9%YXUa9&R{c`mu{NRJd(&m;aSGNz8;Jahocu`IWA&If9Kcy;(d}@ujM7sS%CRY$ zq}`3>QH$~ByTeg`*s|^r;wW!)xe>D5db|P{(d76jULdn?{=zy)Bc~^B8Hsr{mELzK z&+l5pHZba=={FPnwU8Wb*?#tLkN*lS+Hoc`^_yxECu@p8C7zQtY^wNdFZtR2-X!dI zHtwYIIhn78H8IP^WrLVQ5C7AuRQT9~u)sUuZf>+z^&>q`dFeo3KAnEi@nn$WP0<_q z?XzXVk3>k9?=l6xP7-|;-EkWj%)Qxio?3B}@BX|pqI`gtIysCyM{n4qX4!x4>FV=i z(3^VwJ1mg7V3o~|T~{>`rv$LfQBqH~GM0_;;pis=7|V-e$qUSo!?s~0@Van;oo1^M z>Al=FS8}{Xz|g0&QU|W&pz5*O%eKmBF)2@8@E%K6NG%W>FKN&6rD*f1{ARte?o<}R z^Un7iC-4cfouY`3lq!j(v5zGb-sHayV(dzsVOt{s$qX}E1kj{0?2wH{JEcZ+)0+PC3#!udBvF4O)MLKr)&Q|P>*WO3ieC(U zE72*HYPs!m2Bw(7mH?FFN;Kb$ZT6XFX^O@#Y(&pF2^H+FN(>6~eC@O^v^dr(Etju3 zwrZ*CJOsmvfz&pM`$If z299`BCzxNGc`cRewe#ccxVmb+4vH?ji!(;sw?{;`>)*9q<8G?gUyr!k1Y0K?PhMN} z-C2INzp89zeF%?Dk^w)*ADc5><4v8b;w7WOZ{5#XN?PaM_*_*71sAaM*SsXID^9zr znO;BtE^A}xFTYzC2CEN%H<0QU=wb-6bR`)uLGV}G`Ja~OFK-{Q09&`Mf`W@s{sPhM zGU2ZZ!A;}$sPdx>n4Oi4KYNjvRabIF!=p{h!rtyV2l{jdXTQ}uwEl%!ZrLu}gS&ko zl!|T4A*7MDf96c?L)Tp{s)}Uj2P(*lcIh3}1`f?P2svD~eD^?nh3Y!It>pw{37MY?JZ9H?MwxKx=jQxiC4{iOw-YfqUj%72~|JV3mtTF39 z{{bW(|JAJ}{1K!I{|NYe?M^rQ69OrrWN_xew?P+T*jd0ma<9x{!_w%trA=ekeW81$ z4TxWcQVVli3qSM7xkNSpI{VDOOym0B*B3g2VCCdE+I#t2k9-$8fP()@JYHQ^DRcf^ z$N1pSG7xnv;2ZJ};AU|2+x2Ngt~ucGWDK2Y$$g#w7{X^D8aGY-fd$D{mxjOQe!M!X zpDxJhn|Q3jzn9+i7?RLP*T8WV84VkaOgIXNB*J>A`c=Z63(0b~r)$B)5`N+%$b^g% z5uNaw{JDawf$7>NmUD~zFyq91ofq4&;vf&TtZUwqXsaW${;zBCHNw#3h_9S{R<~S^ zuogyf(ypNHD!YsNYZ5JTy2Y6{jS+jBItob)lWi^E7x=6jy6#9c7l@Ke6D!zUk~R`= z7$9N5!PCK0TkQ`E?ls&s`K4YFz$N2(?iPKwe5}xOsa_+*o0(}PeR{5%ulsbZ|2@RVE2Cst}-85;Leqz~uhXMlJy(iTspMqeu#GVsud6B=A(r^5aCKFr_U zh+cbe?AjsyFzMr+>GD>c?Xru#ulbi_q2rT0qlk};!*MwHGLDZ?QHy-GFUGV;26U5l z$atQuh-Xr`ztzvDDUnn0(V$qkmQKwIZ+I;3o?*7<(n#IDm*^XpT)km^8X=@Q{&t3f zt(m%XjJJ(-fda!&EH}9T$nM>n3A$|4*7>4<(FRC}ttCf|P`^d!mSxO2FPOGDL}W`c zEfA7>u3-y+XP@5lYAdDE3l5`FP{UexB^L+95mESibAln&Je!=x3-4R})t8^|{zA5c z(!+lbDl!G1gY$2v#jW*T^;mZ+H>H(~ot|NQQ{_BJd>Vcukk}65P0kgt2tv0V5q!&U zNN&y2Rw{SD#@*4;=|{PA@0vs4I`s@Yg;p9y3D%)+06e&4SLPj|f`TlS+F0P+>O+6sQaQOKXlybP&$lQP8b=JN2a* z_f3daOoZq6`XXr?rImUgFDmpE1+1=x<~Lec=i*ggF10;rx#7d?GnAzd2btT;a%TO4g=MmpcC#Y z@VsOzO(ArJtgFkXd??>&zML%kQ7^PnuPx|44oVI9Vm4MK*7^^iV~tj5nue1-B}Y$1 z@q!_e;GF@qP&w{tee%a%<%L-kaiq3nnSvLe7=yO%iJPK}vpsjy%Y=LbnrR8HC`#Ww z6PAs4qNt&g8nrX>ez#VV({rz53C_UigWU6%poK0ZQ=1bxbx{S;T_3UefFzFJNJ>2g6_pq*F0ZmqI>2r5sWiI?MstUQaJ z!9|3N&t$XG>#ntJH{uB%cf4+HH2Pq>TbcCJd0q?S)w|Bz@9c#vRNUFm(Q%#!2`Vx} zQi8Hf3m;>x8a?fvA3FQz&Mx={13k+s3LCWzHXJK0Mnm|!ci54BB=;}mpa!FOP05Nv zHPaDU_}v)7ho%h2vza!mXC1N6rYJn3?fP@iCVy+`0_)RQ8l+a^ z0~Z@BtpSzre~lu%?4j!yn`r)_TRkC9La|bW2!gd9A^^NV#m+hT@PaNI%Iy z)6dS!I86id4YIqvtNzhW6#nAxnPt05h(@N%QPGhe5?%@q2Y%&{t69NF4AkxmQBf8* z!l2f`&rfCV8id#C@35f~$TF*mNAW1+r*vY?iP8AJ&fsACP$;lQiFF)hs|_BeAcLRj zcb#!+ITfoN#jSqi@`{()z+za9D4;>R1L+3Tz}>|}R1ph0k}(&tLTj)y)Qu8|$CC2g zl*Pk%A3S|TnF~&1ydsZBKzyS-2Z{(!N&kG|MY>gS*DerQJzum6UmdokZLL&|tZq{i zUMzkFd$>carLth?&2$;d`-Q(imr@qfa9(A-+FZ!9@mykszE8=g;%_wDKY-0thlm9G zn#S83KJa4RPQiZwlN*>AE_93Yo_lp#sRo)yUTD^;(tU4B%zI|Ag@k3YN&0TfiivAaghCUC( z@vZwRQN@UAe9M;#%YOiHl!MWo{k&^uiiT&yq0rrX7^Stm#kI3Ht#>q)cqmDkjwi%= z@=YJ)&B+K`*(5tobr-S(Fm<)nM|QM|3(t!BnJ9w&FWt5F%{#j2BF8T^B6+mE9=U2k zd*ymmTtPCB)B?mv6r~#%ZD&33)QYz@opH7`SBAcEQ71pBn|;C_(u6GY?wI$X)UBrj z4u>eC9B66mSYDCZ8p2Ac*vpdwPt1f&9*(k+Lyg$5lV(&#U?cFBF+=T4oEEEJm8Em8 zA6z`^M~#pB3ph7VUTfirfoRLCt%X=eJE@jj(!TjBw3_S+{?YA+iCMGz)(^WoXWMb! zTjI^r=Vlz>MY&#br0;oNUzgYr7iUDXr1%?$lcqrJT+YA3dUDU6g5bih6iqm2AMN0| z|Lj%H50?x}<-bb|RbhAldOj=3G}>>0*x-Q;vNjs2lY$N&wK|+G+&Q!QBz6BsympnbGs%?Y4-doO#$2@3kipNb zkJueT=yD#*HMHaHN3_%E(HycXj>|)|@+yR66|ionQ{4@2M*|>rRB#eIX!0Tlz+nWyg_d z<`uvOsOdEHK5*S2gFyk^=WOM>Ey&u z#fxn^<5`Uae~+W^m%kR>pRuGGnHBO`a}=<4@TFw8X&wRvY!4)3CyjXBdIfY`_f)!h z>Fbt?hMQL|!apHr-gfFH*oFY1?)}1?S#{A&erxts8Om}@W^)~*K1#%n!)!lACXOzy zkuO|tXMXD;#(G<#p7uTgy^C;Zf%-cVa%-j?6&uKU!s5Sk4-1?w32O27qt@es)<*An zPDF@3e|XlnF0jBCq~gM{oJo$mK9-zwhVEwc0CHPFy53#e7^%jVXJpHL&ZiUO2W$4A z#*!TsR8TZ5#U7g;ABx-u@eTx5zNBjplzDMy4f9o`*TLQI^sGaBjH}Uak0bs-L*O;& zu~zomk0*bY;)apwsb-ei-!n}=h5m9lunBzAHO8J>Jt2E`>;62bsB%rV&qZrpft|K2 zQmp4j%dY8$275MBPkPcu9cVFIY$Kd~My};Mjdk_X4+B#TGuq<%=yrf@4zYJmJ2$zl zOw=U4g!2b^u>g}vPX}9%{EF|sW)EDg@L}$?i>w8XU&eM_f}0bGktJhP9q^7i*2ghU zs-q}39T8pSd(68wrM*$n{qOH$Y-x2gc6Sq)g?nbM{Y21EdX&=struLtiXVm#-gt_fe6%gN)5oX^6lK32B~+wEyGDf+(% z?svg~X3)~SuXkXT9LkOD68?fqrGD7p%u!)nS~yxHuOQAM|7xR&1%JY9yqT0`h-tg7 zxJIo@u<2)49S`eI;9-pmc4WN`%Fd|sKAC|gyc`{`0u9z~)bNhNlN3V9BFY9|RWS_J zi}qJ6k~Xego77}KNK`m2+9<8Ora~kHByS}`c5t!sekK#kDl$sQnM?IdWOFLU$-*a4 zXTC6`R)uj$b79az2Q(iIu>tDZYvk;~y$H+wro{Kb`gcvU87s$KHFTLEZ{jTDQqaY% zoBbYh&&|2bMLIJ?X4E(7mR@NAG57^;Wyy7KPo5KY!r#XD|^}+E=ur z)5*?^-UF0qqwvSIh_R0Th-+@2ENy;erSm-qKCO}f3e546h%`5!SjzhH7E;&q^5S&A zJ0X%Hn!vM9RXy<#*lYq|Y^*WDa+|Y5@A{H+k{=bDZ0}A3gn(N`P1XrrVP-Ez#w?GM zYH#e&*l}iv@$b2>Xb>&nk5$APP)n_64Xe`9S+!!&6Rxl-JxMuQctW@N0wFlu+Bgmy zG!YsqY5e8*J!-VyIT~IVQgLbdi#VYoUBugUj>0-ed;ceQ$ve8;Y79G_JO#G%=Df5o z=?9~0Sopf?mz%p2r=;$l@vLjDLM>mAR{jBSl^K4Xx&se^pclWGi^&#DK{n8Eqb^JCz&Nbl0tNh0(r}3zi z)BUJ#>PWQ*J6hsb4gKyP*^ZnsTUU!NRG217Y_sDw!opB={*l`ykFSIW8Vd-630S_0 z9`z$t0VSiZ$VCPJ#)r7c)aj~cK{WUFX*h1}J>h-bAokfd-TV4jcp$jobVQp$hPLsu zK_A2D-}xi~TdP;^Vl$5rG8)MA6B3| z!H)2go80a-O2+TEi>f?)W6*g7c--T64G+$zqZV_*kY!om;X2#$!-Rcn;exP8z#{8y zl&-tOs??PaEcXR@0Ppw^QC5|1qlROwG-Tzu7ylP3XwS95GT5v&c>R(9yG?(j#e0n7 zz9Y4<5(8jQKu@%aPr<*}DSd`zOjj8?Z7DRaoAl|JRoXC@>rRx)H;#+|0jydn28lQG2R_I7wZ z9J83y_ZjOs8Nqc=+KhI#M4FiGK~>ZEWbc;R4BK4IMM+4T#Vo({G%6gF9;Iex`U~4j za8EFxUR0_MPjS@UlcBpGSa&6FySDk5`r+Z&cq7LVxY$Bpto5dOSBhB>%$C`XQ(M5v zuU#`TZFzDr@8*rMW}0=YRD_$O%3sU@U5!z7ots~MK{6C8M`go8_ogy*d(QyG4E#!7 z;jh{6$0fDH#5&M-1^OD!w7EXlW)%x}X}@ws@K7!IgM8nA{s#aV&b17Z|u+=!`S#yC%Vk`^2er%{UO-(y(!#Dwc!CV|l$PznH-PwH__Vb^<*n2rfMjM3x2C zKImTAWLK8kG2&>A4b8+UIvDFJngp@zDF8pkZ-Rn;{bdC#4XKh>-xwr*T;6!T{Yugo z20o9j@G}Yg7XJ#VlePn8=|@gd7{{X%p017{+lo+7Ql+o1Zs*E+Pr*&| zv$t0pCaa{n_N9P@G6pi~3rg_>x}#Rf0Es=L(zJLgGoDKjw% z!q#^h%(KXRZ^k!$uMv z)3p8(8~me&0kK7dj&FBH?gHQ7n%8eedNJKGcxfCAEw23g{J}9KGW&A>qFd5JAoE0oaBcou_B0_ zg}g$f;)NG$R};geUj%AhxAS%hkR9K$_GpTfZOnc~=Bq2y_Knq+kp&L1J3YGQg=Td3 zT~+1lZc`&B3F(yuQ<@v-`Edk0(JW=u*UC!dYY9-UE%GvR*UVrIpuXw5|Rm zDN`mlotmC*QUU05)>-M*AHke{CU9)H$R=n8(hb4zce>5ep8rS{^k_g$l#^lGB0Xxv zd;($fDiW}J`XL%4vKE20CNvaGO7yv88ocAkGycT$@vZH4`w1t_8?;CqXFTqArzY~9 z-&XQL5R*GRJsP$3i;z`vJ+^q^J>n;8V1Fb-&Yg$8^lKFvI&Ny31mOKX@>c^`Ki;@9 zEmfRly%cux)+y4XrNq3L<3snd^`YOx!WL3B?H&_{u^2NwIX=Vws{b<5MRhwzzJ`{-p=0dQYa79{4`fbTPWk zH#h1@W2fPwY!m)#vz&@x^xf-e%;x$`CJ>_i9h04vr+uer*>Bx^ zRtZfOe;0QITiA0%t}Aooaqul%`%^Q$E(UM$N5-%y6$9}g;OiQMMY$70OlP*w8#lOdKkyg6tmzLkuB*Lt;19OOhGS4jn z2$qkw94l`G*={0z+unTbRM;pAYMHDamB06dlxZ}VT>T+0VLpf@W|dHVb`%>yFrpx zur+x{uss0$laUt8>RS|@G`C~?ZX{aWsfTGTtqX3NDiM4mah%Q?_|DHCQhnyoX?dx~ zPgS^_a-L#h*c0P;mdNdkA%?OY;i@li{{RlnH0EzwpEpKS!u@uDwraRha&Ah6dgEGr zD`iVn#KRP?=3pdq{mmmQ7)a%L5-Qm^~m#YP@HH4A_*5R^R>X>Gk$32o9kgYl!0A2%xXD zn&dQ*3hp8R7jEe-_YpC*WX0rA_Z!iVr3J`@@B_Lj_@L=lNPYM%$NYvXTMqqR?2{#4 z&TExM8Bs)^jM%k&zJCBJ5SrkKTVwksa!Qt;bA_uYk}VC~vo4bD5zEiO3AUkibNb`f z1}Y!ka96t}WR_L+FH=~Gcc<;JQ3T4p)MgCwPWzc2)D|APs`vX3S*JWI&|ZWJF80nn z9{C4wpda?+@&Osgm({wHLUya8|22Z%|A*Z`Syl9y^5Wm_+u!Rz_K*Gnr1>`e33|LG zg?lFRoGpSmfGpB}`ES|u{M*OLUeFXh#zWq<{e3BI*w(&&K}G(DP>}UZuROt@p1;!G z(Bsx&v&ji9l7@}4I~?dYJOU!U`3c)aBWfK|@P`)d<*s0Z?OOn?jO&t33Uv>FXbi(D z4Zw3;!z#2W;RurKRn@L?AYGhAfvm^vThtp9Ds#Lvtf~(;`KzfEdE+~r+UH@c^jxfV zic7;&*Q8b`N&zTynS;+gwr{A|+h|eky3&Xi7&=5;wQ-OrF{~Yp0(cKZ-ZCXCzQAeT zl%&5J!R`kguns&U)u+^uQP)lEDql_&`1uNgA46YEF(-zNg(KO1Q}9Z zd@f?JA$0{8Y!i0tL*b&_ZS;*Z)w;wd4r)N#t4B35uC#4mL7?6O;!hAA^ISjlSd)t2c^F;tKVJ8$z>l4V~K6sq2djSc5eJ$2rYY3 z`Fp!|a8jlPcY-e>0A6^)zM-z)nc3UVL8arj6}K&83LjiEVLs9vN;M`hdgkt1+&-e6 z$0q`ILn7R>u5UdDl?l|6rv_MZ)(1Z`&tH>SfR^4E+RQQs?+jB!1PEdh0r(y(L zLEEsoX(kR?As8=lF1}v-zz*eKpjti$%pJSt$lCQaJUsyb$AbpqGqdNKCY^3Bs4Ru8p&W>`=Eko~ytSiilonfZ^$)U3 zWp_0*Xyv}YY}*+IpKMh+Nx{^cw@6$%O!S;K(0{^q(c#;GDxV+rd%N1?J9kj?SW{(n z#bz7@>>}q)d}THOzrdjl*p`s(x}0~2khCK@?(#v82Hf0&*h&@o(e$uK!1~3alhY!L+8-G!|tU3GxD1VBsz=w$N z`P4@eR7-GX4fuS;E2-mJoac4Wd;lNvt+KkGlWM){?}71Mi#y9w*>nw5x1W_7iRZM3 z-^=xxh(e6ceNJyYAG-^-51Tvw{-jiovj&@Q*S|-|DwCNgG0gaTBN*$BLCaf$DMkV^ zG9+o{^)G|XMSZ0MEfRxSR&)Tyb7|eUhZ!zVR&!iPdb^3W#pSsfogM`>L$}-wCcqpvh)>8!j%{kRl64flbUnL#gZMA_u; z=Av&<@iFZCgxHo-WWR;Gh#X5j%h^HTI=fB+x*JRkb>6)yO0j1j|1kCKadYNNoV>ET z^$7rXYx1hsxd%EQub-5D5^H-qM6{JH?VDt*3=+|-8En$oFO;G|pr5A`K^Zxj=fx58 zIB-!oGPQHz_Ilo&>);Sc2DsD+)6(<)zT-R`dqYU<9UNo=Mu>SK>Y=%;f8f}^< zv7EYzHknlEwz+SFv9dTcAoUC7K44I0{07f0xMhQk7)tVFYjOg4c1}gmA<^}&6PcMN z-ZjR8p2!!<9%}HBpl+EZJct{!94nc2_-oPMd*fZZAPpH}IWb5i2h;HPPkmDz3514t zb#slKbsbSx3Dq8=mPPk-G@eOM%beu`C<%n!^W({DVNVI0i$fD~q%ieWSEJX^whss@ zm%dEsHy@(Ps|zV|#fT3`^3(H2llEX#4$31eTaiVVqpxNxOw&|@8S^{F3DL`u+3b{r zTq6Pv4z@gFuPNCD?y+Dmy9;jE%sU4%n76>)#Eq!h6)>O3Rl9E!`E?#FrWc=KHWxmW z5}iV;A?y6LCo3aSgs^<($dMl|Qh8liYj5k-LINp0G=s?<7xa0I4)LYRjDC>q$n*x8 zwJ->jJTl|no{9W84it%Sr|Z`Tzy}Pr`1d9pn)$smo_u{jPi{dl2M*Q%>wnK*1zEZ} z$wgEE&(f@69&F{-?ABE7{4ktUtnJ|E*O8Gc9D82PoJFzd+86MyG^8dTW6PwPWtA!| z(%^Cx-@g}r^x>R5Ra^O-7kbpAivjy~H~rnXX)9B~>AedW7>8qt(TXaFF_h`5Cs3~Z z!*;dzw<@bw z5>^DVwV>XgOw>HB>(Qd%yLfOvz`ssR!$$g;x@OjC^!K0Je_s&1_88T9c78iNncp>+ zydzHc4}c#|{X>-;3Y-7ms@wnFRU42VmEWEJ0Ak2@_r(7#pYhh)d}#U8Zugh}?>O1$ z{fvJAQtej2wj#OZzukz=$41Pr1Ri$&2Z_J`zx(<9|7Co3J?Fn;Nxzy-*im-R1y%8) z=k9+)iXO*iGbivpV>5TOTM7}pobP0Y;4W>?Dh^$VeQg;0${nwne$Qw1k@!FLk+U?v zqZTFrj_uh$-bJQ4GWB~=UZly27fUOA9XK*4M~q`AFs)RN7?~r-j9!q%blItq0L<5i zFFHF- z9Q;yagldD__%LVr3O=1_8$K>nP35<3Y{yH!&j#Mn#9q{9k6^y5tkqhqE5+de*4>$| z>Bxq6(UI|c)}CT@3Hqh(G3|*n@mwNeqyQ|V5mW?Q7Yep5jq-0SayU2x3V*r<#Nl@OD7+J0j-x-Bxm@zCc zl*e$8d?&G<`5*Y(Bh8cv%5OY@6f)XjqJ*N)95r?9yQU{f#6&>m_`7zkYJ)m1+O(u6 zg&={MKwCQF%V`}ou_Y-+JbC@=JeIW(B!|TT?@9`o46#wrk3S`q z$P~;|7MN55u5q$+)7^oQ((nN0W{>im$4ZC@ZnlA&KnkA5Yw)e$mr$27`bYFyjDnbg zHmqqQU_`PEw(P~i%3lTC$`V)@-9hN^!gXz7TKrNso=z}}ut{J65~-7<^2z#g9Tx>_ z`Z285??5eRr|i(Wu854ORFrh4*MO2`gDN~d#gi<9GWw$hbaH8_fC-n(Fly0(T#jgH zZ1y=Mt`?_%I^u13E1X%pvKaP^4}y)&V>!8&ebUDY>YK3KM4lcohbJeuKAahmO5;}F z@-!@bxhYmq1=o0UtGn*4q-0R-Wxm^h#5fB`EN*};ri^;s zHIP+664{NgU4(JhCtLrNbg3fM)4c1=fGY7>Hs4`3Y}e^vMy7&$ZJXOOEB6$AZ6~dw zZ;T6mu;~}#?TIkkfX^I*6S2H{hBEHqr=-a4%sNfy~Ut<7!xHpZ@QMvK_}W9arLz<8CMRpirH4A$IfWaRp;KdL8sNsa-6g#(9)o_sI#H)ti#Z z4rwiJ-5VJ^tpFB}^nin4vq>Q^#*8WlDj?++R22&EGb(PEAJFik9M7DFRCFZ-E|o~< zncjibQ<5Y!1*aL!|oBAo`lqZu*{qU1L2P{ujs)#51HDT54 zS`hCRO?$u=F-m&r?Zz;v#4kWc_R|^-m3Zd@L(C&X-__Z!5!(Nx+7u<33F#RIDX(Fc zBq+o36rC8st}b*SepNGy)B<3on+pO0NF3h*NpNvh05KHY&|rf{&$E^QP<)&Ts+7(T z#e{U1D5ci}NeO`%kZs(hS6LoDgCCe%c_ABh7bw(A`4)-;sJN~{w6AfqR=U_m zl^_vaR13TzXX2st`rbW5pLLmh$Lz^BrAP&@Q+<$oce15`_hVDp9cd2LCKdcqUng&J z=#n3b+x#Thkp_N^`GM(12;%Fqvw5LS-p{&0MJ>l)iUDfx8%ay&f=+~?8*uuq$qD2l zv)w?7M)6he2pK%kkKu3=*^HN0#f3Toza_N)*?t*5BhEhj@G1H``tt1SU{8j6&hXwr z)WW#H-!sN^WyN-`{7p?Ho@_V&GT`h^mQh!`gunna@;l zuO*9#8gSZ9X^3Bt6fJbo7~;AuE$jsDu>7ow`e<=3o>l9L0}k0Jo42L~WhHl)ISDr& zpw$Hy=p=8TC5`QCmU#-Uxlry;-s za;O8f91LZh$e09God3I|11z66ByWBnbrvp^EV;=^HRZJP>Pk?4U(1(z+|JHj8*s~b z|NE?BU|27LTP2C$wcyd%v|7qwAqhqdAB+EvtM~`dSaHF#T5@E~nQ(85|K^{Nn`85= zAGW`gBizVkR9gRv1O8?G`}$w1Ki*OpOxSSiXlh^S&h9^e{RXe!|4Rg|p{ItIow-!~ zW8zqUNZ;x8Ez0la&*KK3Uxv|?n)y$S$vG6Q5B^1&Xk=*n|C3Hm)zl3{Z4$Na z6?qeHClv5JaV47nPIkBd(%H&cP6^xW#3PPZy~I7H&*&9*mL4_S zCLd<&x|>isgXlwXVlFwCy4nx5uEqHl{|vV7rMho-SuiU5m{`RXng#$|95Un43n(R3Lf4argbC z_!kOr0Ez8gs0*?r3V2S6WlGwv)#a+*l^NG zrm%9D-g;+WDIu4%MF6@}JuWqg5FxcaM-iIu%kvzwmJDU)o3Jgf6reUg#y z*q7{n9~ZLfyLiv%fgVZlB`tubn;+Jzr&~3Naau%F<gq8}}~Wy4qPs>8`7H=}Q?TJ-3(R*4cBze7IY^OMB8SLe0;; zc}`@=LN`OeNG}bT5`!s0F2tEXy6#4*Yzbtc*AiCS_`cHaIZ?2+YLD!B)xoGibm(HD z7t0&bEr^%QOs>^2U>h$L3Gh%su@@IL29a%*4RobKoxz_t%{~>890U9EyRSK{? zMkdJ`Cu(t2fZ)ot>4?@W3U^554Md~5rQUNG6X(kp#iHy*xmC1e&icW9BN78{Z6P9g zUuzO4fqQt^Q=5Mcwtq{1pklmAG`uRF3k|Hr#7pa?o9K6=FFu`DdS`2oH%Rs>l;iaf zqFmJ$hs=?|#$16AQ-hRYW@2Rm6^ps5C|)|NG4#MF!Q;!7im_@D0k|6;Qyefr$|BcS znyo7oBtR^j)o3x8Lz4>UOuK0n0WWh*AT--c>`lO(t0sgYz*U<^zSS@93#v50J|yPamSD1blUD5K+oSdylMQ{>^proyBik5 zK5?aSOyn(Wu6kp1I()UhRn0A%VROL(YF|Sg=lFUfmHtmPS!?}`PMm#*Gtk6U65+EI z(&U)zW|2=hsBJ~yg1BRj6${VMeHDSDhI=Gqf_|=Q#WU@mv$x|84Rq^x?-UMuGOn&p;@Vvhq(5L5o1k-$e4nWKTBqPrgK$ z4AyY-Aju?e7iZUV$^9#IAXCCjKF+Qf`lR-uqO05h)`a>(1eBBM=fi4E^|Tuj%F-Yr zX-X&(-F{4kdYCND=eBSVqnER<6R(1+z}4zHcOf3Dnmd2!aI0LmdivRjM3$FwsLZ!v zt!^+b^uC2?%RVHCCcmm}X_uM#J}a~<4)QYkPSnpu7EE7wQEsM>ksF=`8++Jq z0lvMDI~|EeIjF$OJ-@ZhYpO3}O7Ha?NJCmQ6+gCQKNFq#a&NF>e^;V+K*0F#f81Sg zEpr_T zFR~a@Rxbnf&dHq$tL#ocZ|%Tt`-{ zJcSn7wBNdqSEHufNDVhEy5D%J)s{5}BHJ{3V{2)6L%k>sSOccv=+y=yIvr8QuHz;& zZ33`&&Z@u2MRxBru2_*h`WuNc*i`BJNmP$)C<4tjZiJI30{sP99r?!)&%CF7R5+_> zB~4@wbuWb`Phh90Wx#lQcik?~&5SnsV+wpY^qm|$XqQ+h=X%AaUr;D1=4VwmJJfSZ zb}4%u_`ZmX)j$$p$ z&ztC5&R@X-s4^dYGzQ1l^bxRthZboKM*Sv2g0s}r7A|NJV1XA!jFU&Od21GB#(B6l|s+1P@JP->B;pi@9EC;StYPR88>_$Bkxy0McF8r0#W# zNrgixaN1HiJ_tZ7Lw4;erCU<|Y$R}s`YiSFaIZSSgKkQsUCN)XiCrd!>*v0{+XIu= zh)ym}5$`}tcHS^hfI*Ay8YbSRSj7MKBtC2Uy9C%eu7bHF(`#29YVFzd|!V*L&0=VyTvvX_Q{ zlRD7lG7T<)$k0Bmo(r`9MO!(mYiaFKYGGk$j-GCoWY&rK;@4=jeS)(|aT+3N67IFv zsfZ)W!UEO;BmyLwdA@gU+mUuAyB(tzCZl+@*!~ouU;c<#>U+ViPXa7puMV@g(k2syZIGj6jUVY4!WbtDB$F&dQd?OfyT77q^cBSD+hET6`1m~?_ga|uxZj}!<{@9o%b%^$jgeft6Jq* z3;;E=dqtLY0bo z>>Pj4xi|)8fp$iO9!Nw?>Da8V5~-71604mm_~JC)wlkrq2i#b{UQJuSn;X#kY814* z`&HCZTM)Ve-xT)%CY_W@e@eJD2FjPdJ0o%yP+-jtLi zqI&{CwD!uq{sAxs{P1;6t(i%eDg`5HE)E*7W*`N}wGd&DN$r?NkUgia6#e0{5$~zu zO%G+{+0$ciZvtD?!dJal?P zsrd{Pn8-et(X5jHzCpv@+DTDrier*{Z4=S?qc?2ECo8GF_nWUSn7d@9&s^cf7s@W*a z9Lp2gr&7Z0J0dLyjeddHTf$%H{YxarNJ>z3@ zI_ib>@IZ19yo{;0hRw`J*1tW>Tiz!5mM3;SLv;|rn2zQvZ4O_WaEI0P@Se;ea9*JclU3|oQ>?~*Vu!aKVMERSpI?* z7V*xjyA|Vg$7JlvKL6Ff1;qNdk2C14*wa0hrgN8@Q;Swm&o?OZTH~qbwhL~5xf^|q zFic)e;h#x?qTf}gxf8g5%^8=!4LD0&rs3rzmYy*83pk$wuP2gUi4oLQ4lnMWl|884U`a2C}lL zdRPTzbigq8 zgMBw*gt4-!2v{i%PHry=>9qiotl ztpJg#IZKnR8B$#djQxpLzyZ{i;`ePV1b2Jd21tzq0qn|10IYEELlt-5_CU4gn|H$H zNqNh(F#^O7YL%}4ujbh6plahr@%2_;s5Fr#!sFb?=zYJdgc|Mb`3mqZ=%@6zBHbRK;X58d%~Lz8q@?oe}*?LEBW0R%82UY4#}5@0;{!g$}Q0JI~M@_?Jk&llsMBA~2ic`go%CHc(n+j79Ew zw%`n1hJ9&-w)Mf0s5Ng_li}NM8S=Nxo?A_>heN#bvbk!qz}79V^qE^0I7MU~;xnUp zvw2@dV36($bTu0dyPf!@X_c0Zu3G1w!bYBA0u2(X-4~=)efnKzH+g=W&sy*GQxoPK zkcub9#U{)OKBm0`IOVm^gfS6>oKU8A*7Ay^EYi6rdBoBQ zd6bm2O&I~z@C03jw!d3N^QHH;gE$Yxy$&J4B6PHn;-G)}*omcwd#B8q+YFWVn zw$BvV$4l+9luL563P==-vJIy5dt--dtW6^(EC1!|>KYfF6U~Vn?{+F*h)~vMn)dfl z_N%e7gC&vfq)#e{249tFdXSf%wv3@~%#V7Q(?_w-5T+-ZrBdC(=Ehe!)C=xoAyMgp zVw<*jt#O>1T&UQ}iN41#!;bQ`BT~DPHufPR#3g*3#7K zmj(aG$VBND!7gAb@ZnsSCeKr|mH{AAQt>4*YG9+aQO0mO3#ReJ+-G%}^ zV;@XywnMv~tm4aTQpo{w&{uiy#H#nAj}P7*rEb9wzB8>@KmWhjd+(^Gy7gUjg(?Ur zk|<3;P#_=(l28;Oq9DD68UYccgkGd70wSQ1-U)~ZArPu`up&tBp{NuEqzDKqMWuQs z?(f_8ch9*0oH6zucRSAhBMBjE%{A9t@0{~3&-=XDTP1_RRm$-|_vi*t^NW8HgxfFc zXj-t7O2;j8ct|kI8#}Au?5-koSXhG?X*c3I&5KAT@rISfqP+^qi&B45WyC+aud6$^ ztQ}r@`FD!$syv6{U?SON(nB+>&f?OuY9ALf->{uA3GI(Gjqe3KOcPn@^Dn=Kx9;>( zV}GiA+?|`ZdidROhhF4e?i!rAH9ffT2>%adP36h`v0D%G0{!;>{n9?R&nPK=RvDZZ zTfb@S`_(COQdx7|z4Fp0WMiZ3oFh3R;HDLu?);|l>#rnr1>Bmg9e2#_LW_oMUX#1p zlRc3v7qSb8!<;>*#eH|A*jT-f+!Ov7QFO6_{750ZoL{h#Z&r<$KnzfWT=JCtQAQH&*>rABhZnI~$xMqF=446v zg5kU+4Es41T~^&8Cf+{Uq=8pt(fm*h49&wGM~DuG_Ic}(CC-E#CIIyS&kE;;{$Cme z)VwuhTW!kAo#+RLJ{k}$sQR?5GS9W5sIJ-cNB11?hvKMIZV0*GCMB&2_~$ou*2WBN zLU^qbgXDmk!~Yb8RXr28Ulr&J5<+~UkDln$%4iW?;Sg|N?Y&bCL5wL6-meRXa#B(0 zdD#qnI&_AiDHIS8Imkf()Go7`_v0}vEy(PLQbwh9aKY{R?EQrcwp@jY2ObjccARf< zQtz2e#4y!`>M(kPVr-WXhbXK>C_R-XL(jgg`7}@&K$rl$4iRvgkO~qxm;^nT|4AMw zfPi`djC{vjI5dDkMV7#!34CK3vN7bS`d(z=uhW=i)E&D8`1a7a*jL$%VVbPJRSJK! zAn^`&ypGgKB*kdR2Ys4ot<9>QkEY2aR>WsjwO_mM#z)%i9UmdrayC0(7OXifis(8# zka-=ek4|dJAud~2UHUAxfR}!}m6I1U_Yme$zBJ;Li?dYsk9w0U8+ZH46S4Id88kjo zw~SBvDLyJbmF$1-rC1(1CcAY9d&_ufiaPMaL~44WN4^4`|?I!%86~tfMa}ET-XZfY})Ydi5H2m&=PqcN?Uuk z%of^K`8t-5g$1VBRIm8tbKRoR74ys&c-8v~^Ck=l&1dxEHKN0e6AU6qnWXGFcoA9A z{^8C8q3h^;f$1pc2TBU(CrlM?VC$;{{c!fhC!!9%D`9-GYE%RZv(mGh z;vjb%OFZXk`5=o_`dkFTvg>EsMUn-g#7az{NoHrkAHBe6j9Rvl#xNVnbQ2OBh0aWvSlqv~&5JXD7dV zC>B0rbMC#AF7YZht9poORXnZZFd8t*si!$ScL}f=I&ZbENo~#!>p53nBsCUPT?3=m z+^@frYx6q$T)o?RDnIdB10z>SO^rCCaQ=YH@I`w8&&wp6cP6T4C!f9g@y!6gf_{5q z3v2H!F<*OB#x&)_XJpv>jEiJe&&Qt;$MrXwJPT=0$vUe_9_HMql+4mEs4#TY*Tjf$ zERiNF^+su|qW!(NPAS1m^V6y%hKrn!G@={O!Fn}qT=34Vpo(k8%&+AR(emROiT37o zfDC?CI*P-X`JV0AnLb+LTxd70=BW_x@cDaTKKZ0wQyks(U?&mYp;5ed6miz(B|}xo z7g#goE;~R-Hdgs&?%8pLcj)c(N3Q&i=<2;KFy4!wxNI*E;~%yR9P2+GplZvn{_0w)h2Uu3BDg&q94ZqMQyeMy9E0T3Scbj7z!} zM01{bKijfF2zO!V5hc~1Vx0-@TVcin&&LxD`kFSV#=cy{1; zouQ{eRDD}EQ$|)Y2QBfIFX3K399oxOG;{~GnMZ5a^6#3h5+QGIYOW%8n(+JT{ZZA> zIraS8Vr=*(6bxC=mdEe_Z1v1|U*fH=qO0`Kr7%_b$Dl;=p@b;=D4P&C_7wubmCdAq z;4rjL2;s)7;hRd72~aFiA33NG(+bteML~C>`iWN;H82O-NNr>T%zqr!SqAEO%6{1cIW*f#KVXQ)Y$j``V4Ew59 zm@Y0Elf6%tDCN{cj}FP+r$B0>aC#cJC;+S_JbOTvTb~u=>PW}{&kSQWaHK~WLDc1J zhQ}L)>aDy3-f9cb^-Mw}s3E5_2p9qrSHKgj95H5nW9=r<1$0{slVaSeQpGrg=gOBOcBG;0Vpaq;OO~_F#fc9iy@8%u& zl+8f!frIuaHgj5SQKs9UtpU@Q4VK9ckLrl%CRJ zWN@%Ctfs8#OpbMlINPlA>!v7U1-jeXq9q9zWBd!=qfOf3#b=X}uSK61Br%gq(>4edGVCkfMqR1Q zJ8a^*qT8|0dz>6DUTdSN2#lF^@jDcwhs(NjV|pczUqZR<3*n;2Wk=qj0(hP+armZ? z)7NMHrPpYM5KZPfMQ;}*c!chTHk9FGplODA?IF}zyz4#nLD@4?e>AWC>E|s-K-KRteLgR%1Et*!PIv&Xe=D6VHkpo~N8j73H~H;_?3U z*JN@!{{zR|0ARmgK4+;I=V#1nxb6-w;vKG9P7ZxLadO1efF&%f*|DU$ChO@vShPe( zF^4;3Z4xwX==3_kMQvn>>@|2;p)~(_gW*+?66ap88b`zdtE$ek1tqSkm7?#@4%?G1 z`ztJ3dMX^(F1&rEDi~oQvp;Wot@sy&jLuddkF?p+ap>IRs9|qI(5=)+8}g^uVvovs zo2ieSP`_#`j492~rDk1YGny*Ed)%7^tjeFB7(Y>UpGle~2sR(LzT|d{vsJM23xOkS z_=&4Y+Xy9|O1#$Ha^F5zwY%J6at2glyO`>S zyGGy~a{NGi94VZ3?^yKt*%EQ~A5N8QX+Z}~3 z(XpEyuu^R6h>LF^{E)yaIkA^N7fp@xd%yeF)VG&N4uAY}gvE*x1)9rZOzkV8U&4_Aq6v20o|wZZKuj>RtSICp-|Yz<)I+-mUt(j3Si zKJz;D;9LQVGMjQinck_C8u@QpS`SWuOq=$GKgkF5C-)Ups7PhM+`P~nJK4Hkc6N}z z>qXpcbw9a`JLN8scf8vcynm4z(n77&lu7R=_dMG31MA%relsYngF2)!;6xD zM+Cplxv1L>NjeHAqVDw3P`S1HiIAqIg#m_1L$~<^bmHi|Cx#ylsTb*v*JPeCk_^aZ z3LRJPB&Zjf@Pke*>h9NRVO8YD$TJ&?G9(e9yM*XSgfyCCEt^3?vzikj*hfo=c&LMd zde%i!qz%^4!n>ldJbF}#MrMChA00iu4((vaX9=0k#yA;fQeCq5ZR8RY?RZ3Kl<@P^ zR69}5Y?_1z1WnK>oG6T50uG`%@Y^XNyrS8LF;$RF+(5Yo0kWgRmaD5mvKe}U83afZ z_d6zogEap(n=WXnLMK4w5Pg^`C-9yCyjX~VmO6k(G$scQs44^w&{!k!^?BV=eDv~I zSU#;bBU?S?P9fA@aK0DbZgboNXc*Nt!^>(@;yGcT9Yb!5hlg4!6Ut8ZKpWAgU3?kL z-cGWLy^_f+Ku6l1m1LT&8L&Lu2Gbraf7^HOz3BH}5JGL`O5)aIPop;mGRnjwgf~^g z1|G}@I`WdQ4l{ghQ=IU$oQ-yMTu>aduQw~By4{OytdhvL6S%FaZnhlrzzHiIqQj0i zDBnmaKUwX~31tqaeqV@hKK6D%N0u!O0sqeaXu?>uI@>BG$c+2su`etJELmct5)Rti zVp)$0Qep{R5ld;SfF&Tu(mLo}$-X;&hnpis5Rsng1JKk%{LSH}4n-=L?_hw8Y@qSI z$<}z*s-^iGwoEv=RQRaha=X=`} z24wfmZa$_Zi{uY?*-2ZCMPvT=gzldWx4S`*5&K!U9CPWYrmYBe{eL&TBo{8Uu1I@*)MbHboU@x zW8XQk=7hVikmirI=6VkPC^G$VLH&Geh7TTLi#!`=*;S-)e0zvf!b>fn`Mh9I;i;;* zGN*ddL+Q&xc`hWIe!Fp7P(~suz;`OWKrd2^)ts7mQ#IODTK2ftAtlE<_YMw;kH*i! z5ze|?QG=%k%AZe)yPq|Db#!qsci>sA_9?_Ge=&h4amW42Y%Y6zjc9dJzme%qur+?X zE#ACdl#p_;-qu!Iv-+2;20ePW`u@RdyEw}gCl|05Sa1-ev@@LIC0Oq1E>uLa zVo%f@kuR|H1-wHJVTKJJQ$nMmvSwV2~6H8_acI6TjvQ2je05e=#Tzcsj33ULIkGyR3(DgXZ|Fu>Cf$} zk*VRv;e^he*CK1A@oKJD&Tj-YPT46OyJxT+?&VWm)LHWm>nAxDtFOHIvlzG0rCm=B ztTN64I+Hi?q2mMN;OE+B0N-igW9>^@g9G~msj_k}tB-6At`8mA8tUB_`-8H`w7Y)n z0q5YGt-%uC()w=eVv*q_P2-wbBKx`Zx2p2!7H95=r6!;2fgxC~jiT_O$ey=o#~wBM z?S@3agP6+M!|L?@$85Rq{Vuoa92q-~^vl8|Y?V#^Wlv7D2{ue4ns$5WNGc#g?pAM; zR%d?OER5`al}i)HE>&P=oYIV$iaSW8XNWU|O-yARlhC!&MSc#!VlKgN&X&X`8F@}{ z-&iBl=?Uv13t_X5;^GEBO#>=11;8M=pW;+L&HA)b+v{+MxUDO!Rg-tQpeA!RMC2`Z zY<=@ZVWj%CQ{9l(lQ?1&2MDGNX5bq$=^?ZlQWwcNmGLA5PPhu+t(vIMW}eYGBXyqg zotc?Fo1tIz1Slb_fuTQ>q2>tlR@Z?iLG`4Z#OIKrf##V#y+KO@!(=G+2*4CS18?P8{_H zF1qK63_Hvut_N53hKh2vmrCl`$TN*KOx5pPX-Tk98@?BpA4Pz>u`GO1)Zu%C)Y*WB zw4NKvyJ|$g2%I=M}y>i%(i9Ef2ZfICyMm|-1gm17IFKZqByc}*uyi4ZsEEsl?=PWuM(ocOJ zQy_Jx^y?+uNN7|cY~Etf&BR(fe_;0-SI(Ogodzq2T9w5g;YLRUe+W*@9Tl@wPgZNb zI@;EItQTI)lxm1q{g9CV!}ukWmj4SYCPcVvZ4d7%4*Ozo5$jcqt#g;17=DnMv)`9* zC@MC6W8Oq0^Aw1!;uHD}X7HzC0_|&EP8c8a9xlI`tsIc$So=(H7u9PBOptTVjeVze zD`3OVy%?EtpGCiO4*MD%nc=_@I>@}RUR3w_I#+zAxXZcu(^LK*qN=N(JxPH!UZLl_ zd&>z9CEQu0rBdB@9beEU*7izv`?v&-^KK3&=hddsvYV#_Joug^Z7ES~BVTTG&-!t6 zU9&^)fiHn3T^AGzS1z(tJEl007>|nax+K(^jJcp=eV-neFTBWcNdTn5On2V)FK(MT zR*qv16+GlXw7yq5VXi<^ek%8vRCOhyWnExUWlZlFL%f<>CNr74imK#oJa^}^I?d2q zwi$Q`KCQLcZgGI?q(1+~rvsmyHLrJinsZZ^D)cr#DaTV%nxqPH2wmBKL8ktRw)#*0 zUgFB9mtYnne=L%+zN=Ez_8R!vh#S(N~duzo5;cz!wQg_1Wpu-8dbvL*owq$(H_~(04@g6xXwoli7-GuY$|g zN^r9`^<)?CS~D(FY(W{+%_8oVDz588(=kG(iPz^jTM*u@77u?x`o#-bD|)iarW4U` z>8=ZSh%s+;#H_fi=M={Q_H=rF^NY>Vx^2GSj0MJ@=3U_$HFR-M=jB7%IvjQCEWLrH zbD*ZH$J;jmXp=pCo#J3Dk8Pg4#o3Fm2Qb(nodMd%nWiE^;@L`9XP>06pL|4TiYT^_ zX?u$RDu%>XRrHF9_p)(sVAXO5lVF9U`{e)&>}k7j->U}dRW+rS0Tf>QG(q~a_^nq6 zjwmD5HWKZOQnG~%EtdAly@N0!M#XGEN8%WXR-McpK<35>9b!;btB_VC#=lrn%OyJQ z-`7Gq9&aF`dCQ+nXUJTv!P;z|Q1lq65FAQl_HpIs@VR*s5Zs0!3X`Q(>q9zb(=`B$ zf<8>mF(>5Us+uS&9Y8SK$+J;?w6)x7wgl}yM_7F>sxB8EqTR1Wkj60Sk^p3~stS1c zEtsVMR+1(qAqflf)T%NBu?5QrXiP}c)*{4;ysO#bN3q-H14 zh(`yqaO{6Gi+FI-S{dQe*{s%?5QVAc2qIAVDVw}NN^8{ns;WS49jp(DvDXt0MiVwgM-d9M;RUW#`+et z&4TC%>sfdj(4BOfg#oA)`8F56&-KJv_#J&(ys9L~bx%l10L~kG;O6j9$RiUh`e^z! zh%%V{M5d`k6kigQmPeBT5GNDJ2CS;QKFxR1`*$t)8(7b7R6}5h9@_0Nl3G||c0qT- zQsd!pao*tlz9EEDvQ=`!V@Ze8*En##Z{ApYw3J^Q)v>63{;C;r-M^K2v&%_+U4(Jg zsf3m(TzeRbnhhnUY(|OjbmMmWL(NlON%9ukXMy_%o&G3H3-xgBd4tXTF*5T=ox zs&9RA^#ImDRY=`S2l5;C`}Ao)pR*xPU4;TynNq~h*V069=UD~JIEF75F^#!7ZBtq8 z&M}H>=x>>jJ?9+u@|y->RBZgYbJVlp*=G-ZKSuR2>IGAJDY~O?D`q2xhdbIDc^DDl z&Ns@voc9~o2e`2@HA~RUJS|aKShk&!7D7A8?5pj{{Q<~vH4i=*w5WHEib-mSC*O?E z5)*b=)zpJG;w}7yJfD_a3=#u1fRe+~qgaf-85Z}P9aPH{eVj$QldiN*gI*)SH*Fp_ z64)G1dJ?eOsAB1OmNDk}CD=aMHwuS#zjA*nwymNF?&y{kV5m7C&@{Wj=Lq^nJ0y&e#bzQ7gUE*JziMucGM}SYuQT&mC#8@ z*w@15`1szT=@_P|M|U@fRcC{rAzKb!E^%?h;j}0!p97`HjQxC!`yKLBPi{;0tk3>} z4&O8t{D3-KarFQv!_%YQ#`o2~CfSSd-1bGUzJFTyqR8JeCV)&YdcLksaxB5i+1%Dx zo11c#VV`hG*V~&|byIGHjY~dmxHc}$%UN1zoba|L@`rpO`?<`{L}CP9#`9JFNhNbM zXB1ZjKEa%2)LCh|R0Car@Lex{`m=?qDvdB|IWJVAR;f;1>h096i0on@t$f$QaDXhX z6b-H;;=@TnBc&_Xrb5SvLUSxHh4Zy3Q$>xQ%@(v#eMdOTa2;uM7qYX@Vew*P4OqHM2h{gA*H1IP+v38?TcSo_+L7q??M9^u%_eq|7Ga zvvb$l4f4Sz(Vm}X2Rh?7ZJr}})d>q;#V~%&O%~pb>;RvSc(xpZ_~V9uXKw9(+a^Hb zx8Ib>Sz7k+@Mst->(cwasoWuU;GgCkQhNRpw<=*}Q?tmg;N9C79B(1NkXc?n5E(sf z^Ba%}wrx1{1=30Jrxlo=4CC$j591T!D=_R^>SUy?Rc^$TiPQHPL zQfK2ViLS5`{8+CbjkLx3$dRP_Ld?7SKFPx6ZbKl7P(o?rh#Ee*T*fs^IruIz@LMUS zSgw{;__HSt7lZhThF{f;BIe3sk6-iLQ4(dKOTV4yeKf7PiKseGo(q# z->r!!0mJvN z{AfcCEuC|~U{}M7f@F!k4Uoo>gwh8ajVdGoPh!Tv4f`=nsuCc9!7vNZGvO=-@WA%= z-uqdUJe$OAEzj*Eg$;)!%r2}L2gYBH(x)?ACm);5^XXRcN5Sv`@aX2#a{0A_A%ga^ zC6X-0`ccF*B@Q^uaAJG#Q$0K(x+O(P6xJo$3jf%Rca)_E$}at{%ZNT@2w za8Gj}Wi!!BNTDScARh@CAMg>x)hy#f*GIdmDubHD>{ml#@?jVj!M_j&(i()sy}i{Y zL?M3)IQTV@NL5;1B%WRZI^|@KoJxhm_>j_uHn&A-6hWE`*nnvi4G00?lnehnmFQ%F zkMmo&p~M;0*YrHvAjFf6@@eF!F%Lfq+k{ zC(}*NPcD=$SgygW+s+t0o>Mf2CA=&0)TjxOD_*9vc0=ck6lQeZgqCj8mB*^a1=wAw15Rd3mWT zogrJ!rA;C4;;N}|h)~Mx<>Y-#=)ewy*$dC^^X_i!EBFBmxC=9s;MKG|sB+?3CmTK| zXdFM<1ISP+XnAgC8W%K3+dUZsHE6wFwqsz5R1Tf)69P1Nk|9wPc1Iuimh=U~2H&*K z@1xZLsXdReGFW8L<`cA=S}QH&}@ByeRKjffF|pS>`xC*LeIVz)n0Iw zJN6-`oUMejHhVw6zbe3Di|%Npt-P&@@3bjsUe&z_^VWeK7Z7AF6FOW6UrY41i9Pir zdw@NcIb2`I>Ac`lv#pj~Drd6>{;bRaO&i&CFZn`_L_Qs-%g}r`c90z-gjSKE|LDL) z5fN6GF={*@Q{p6stZBjDEg_d8Dlfi0#7!Jmp(R8Zq@tjV-3vx^>1l+TF=XNSeW|FB zy66Z*@U4R;Hyt$?@_ifi>7J65uMgYTzx=FG_igsTw9~e?waKko$t@KB8pu4<_>8U( zQ-zz@h5un$Zrr$wqjvuHCL`4JJa>aN*B&r@?4^FB+wM<@+`N@MV^XmpXVB&u640A} z!76MrBRbV^H1YNQx}ZzHpvxPk`Kg1I<*kI6)H8viInNVA{=4xIK+@O0@AdkdL`$dq z1Yjp1UX|)WtP;jF68aa~A_9 zko$3&@cX1bf?vvlce8hD-?g}$Cjp%Y{fl{4XKoLvr}W5SsUk^-5p5riRNF)anz|M^ z3rlszTUb#9$DRT^lB8wN+wtajym8d&M=d&z&3D_IPfw=e(HGNaPmklJE8=HeGL6&# z8&pF@1+{!k?8)@nqX%)`>ihDos?F87^Aapu5&S}l>WthMG@hdDqsCwG0^BbssA@M97qJ<%4ReG96k%# z$KlnSP99E>KWTi`CgVvagRUb@gGGsPn)P6#iac5V!*Iy4=#8Qa@%nVk_(^~(q**WQ z73M)8lL)b$EZ!LAFb|S4k`k{%P{TMvv9tMg4F#2zd8iH@=nNq?Xm6*3j}L;RMKZOY zfRT+G_@>sc&R}Sip(@;uVV{j2oXXu}a*#7z&KuQwtWWD_7^YQ|RUit>9(a|v5BW+J z63u7EfOLR^1a0iR)Sh{|&1{(yBAAo+7Rd#{VF=M&xW4`*0OEB6d|k6z=3@lDPmD&C zhFjBDzFULtYA*^#&qCD zXPuJdMv7-}q+!+s@p^hlRm0GgG>T!#c?jyALmkLbJTQ&V=K&CbY$no|0AyFvph!vmVeWxN~P9v|9>ZjjC3Z#NRB(FsF(Zg$+m4G3KfCMfJ~wVh#nY zO`ah}T9e+=L%0J^^YnN_;md_$qj;RkAS>Mut-7fkwd z3mDR)N+m&Wj4T96Z1`c*3M+spHu7e?|pO>NtfvnH?svhE9Y|^eBpAFex z(9Ub5K5EK9#_wk_x=JgDYOu$9o7_xpmg_K3--pM~(4(UCX?)3k`PXK%4*Fy>FbDB? z>WIK0axN+_S}GA3MdK8KRA*y%nf(MOjwf+PC8{1J84pc8cC{Cycif)>%79Tn#1rza zdus6(>!EaM`{<8Edd*h4F+~kmCAeCc*tMA-K*3Ce1Nfn#%yte)NA)0kjjCBM@jME4 zbu2By>E!OG!NTk)IZev-S;af^82b2e1w^1hb&+JFO69fTJJe-VLyXC(P)oLSa%$BF z!gQ&K<2z5Qj7jRbJ9_P&IN3i<#k6|0Ja~9Q3jw{<#74MzN?bx-Fud6fZ z56s?+6yM%Q#~VgXsjSj^ST*lCJ(Ig}U~*us+3`((i$%LkyHn3ARk-oKe@7qx+a>`L zQ<%McBS6^gZeF2g%eSMw$u^I}RyD_V7&j0csV6Usi7mHs6phu!O+U=CpUdP2JLV!y z*#h;;6ZX|R{FJ$EvGAMYcyFDoYP2KJ>&y@DIkAJv?pQTOx7{$7%?zZTtl}opI%Jcd z^=B^`2|wNU-1*R|$%y*6c0_l3>Bm>|7CqHc($Wk?53s(soLžK&w8_KxM_}nvV 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�oD@2i_O4OuEJ6~kENa}*C@}oSJE=PODoJDmk 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 Tcse})hmijNAE8bCZ~p%Q4WTl+ diff --git a/tests/assets/hlabel_classification/images/val/12.jpg b/tests/assets/hlabel_classification/images/val/12.jpg deleted file mode 100644 index abd463df610ba518c6b1a51e35701bd454ea693e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57832 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 diff --git a/tests/assets/hlabel_classification/images/val/14.jpg b/tests/assets/hlabel_classification/images/val/14.jpg deleted file mode 100644 index d7f2d51fdfa42c69b748a2c41e390b419c03ce77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66226 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

        E-U@*NBLo7_*tW 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 Yl*x38yn1{n(y%6GDnPqf_sgsQ0XTo}k^lez diff --git a/tests/assets/hlabel_classification/images/val/15.jpg b/tests/assets/hlabel_classification/images/val/15.jpg deleted file mode 100644 index 957f8cfdccc1e1b013c6236159bd2a2ace2ca24f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 370121 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 Sa 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

        6LC<2+e@7x}I$j#A*xslxaO(-aznf1f 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 z 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

        ?H=avI|_gVOBoi<;};#9?R$qRjuheWb&^y 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 J27h^f@9*~L=5GK1 diff --git a/tests/assets/hlabel_classification/images/val/16.jpg b/tests/assets/hlabel_classification/images/val/16.jpg deleted file mode 100644 index 9129e9739627f8976bf9a107425098b54d9fef36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188698 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�Y&nW?J^ zPU-pGHu#ZrLBtMoSHb=+e%v5;D z=xfz853Gal2lA2C=?|TE1!ZUkYA)Y6&vyht2I{-_QEyGPiG#A;>br|!+nJMw#peLV zuQiz+c#n-F?juC{5){cg<@t-L_vKN;eKTrAivv|FAKkLg4dDR*OOqRl?-GT}MfsM9 zEsS|u_>%mbwEKz~0ZjL6=8OZv9bKzlNGomlLWr|#K?ye?(m`8_U(cAXBN8L$%^yp- zq#jrMiUBvHxXX^gsM%UkWY;xVHXBehbsuEnuXkV*@bEe^-^V3HQ8wMPNR>KEN@RAB z9HFY0cUU_!*5ZEr<2<{~M_4PLqL<(K9p9xv)}h&vgm5GFW;Ik>;!Sk@H9P--0$xd+ zgC48JH!CP8s?l51*KJ#|<=w|64rqz65fzX!)7&Gj6jrUUv8 z{${AzZ5>~E>*@aD;-|1s`6OjMdfH_7*7x0ldif8vHN1CqJ+TT;OyB)?vG>0&_WtMN z!_QlH$y7QU>Y4zrkYe6}9wZF^I?MpT@-j!zH|8?u9ib0CSubdP(fQ*~+7jwsp*P*6 zCUfPb+r{q=W$=$~Ho>Zyba-~eDTq+0ZmU|(DotBCvY~CO79qEOagQ<|yR`m4<+P94 zTGRDP%&&a70KKE%V!j|)VbuTVBVz8u zOA_tbzP8?Q`W5>=y$hU<4R2>^Z>`gYkV4eCyCVtz-+sC`Exz}w7grVX zIB~bJ6%Fb{v=g;hkS^a*3-TcZ2bdT>(ND7Qg^p;r^Til(2@RJ{-mI0D`r#p=X3G)d zU-r9Sh=Sg8pc3p`7Gyc;9er+3SPHdqlu0B-D!5Oys^7%X`$Cga3*5_8sJeNd~D*2VF^lP!~`A44R znTxj?{uG&1K_%{h?j!RS#Xr5li2u=H zW?RQnf!Q^0S}7P;Qd<0d=p@UQ>f{njva^gc-{B3yH<=0JVp}oHk`19er2Yyjt%rW} z9PM$Uu_RODpjGLU6=xHrjCA2m|DFoWJ*5}gS_DgDCjaqZCo6x{J03|EVnnDx${?#u zW}_Csfk`3oO|tko@R%<*)`sNMO)FjyN2e4r-6NjgxPhq)z;-k+JyPPb6C*L3!NB-O zN+R(M3`?m~9}ucG;FEkt?-^W1DTF-`Zlj{W}p zVs}jVPo#8=*6U`-K8M$vW$+RY%_}~@Sn}OK!Y7;4$(GB#iJG6bx=%=2M)5Zq(qXmG zZ$0)9sC3l&-AkOr2A&Gw2$YZe?FXb!yug$fav@J{^AmK;W#dkIyAYv>>-L^of!nF3 zg9s{rP9lYmA~!6h!MXj(roB^r+AtHyOlSzjLSIyO&oF7&t(l2BWCW`yU7a2~kE(zD zJ5=1gv-cM!E90qY&$aaGaNL`1+->mnTwpLFt6Hg{tAb~*O&$QCLbWFV`!QEi+~)nG zbYouVH{RRP;8hsMiN_hJRVSA8i+X`NOLJn{ngeG(yTg{ z7>3C%2H(I`mcja0g7R~^d|z(p@8D(^Rml$Bf{atm&_V-Xu$}TS$Xz(sBI|>2<_1^P zlIunp9V49)4I7~Ool?|x+^^GoH@T&9+~d~MCvP4@Mas?`r05D{g~zd5XLiOduVPvR z?6ml|hTvTmGLE&(IG38wjUCSQ+CejS>pd=P+L*_T+q^t5Z~yTCeXJ0XzX=SA zPB<-@mvicTEcJx=oOE;UmH&Z5Hy2XH*Q#JA3lPSWVC8u5Dyp_PnW95~VD_l9BVkoI zsA&;amxaB*32L|WWpcaZx{h?d4aVdtU%=it#w})ExQ?(;D??HhI95KUpot)F3->yR>lp&?K z#v+k?_*5aJ#W-$Qs%eTx8Fj?=+A6?09Q#tiH_&YF3~{wJB+-}TmxiH-z5#)_<$TMj z@?R6+@72?v8*|uWXz@x__0xtLzUgt;+51xSWe#RHx^2uc>s%2nyXo~8yrfnZl0}hU zx}4!KK${q0y%EvKYMMH(h+PFApa@;X;MM?P=&vU%> z7GVRG*J`W;t;ibx)L55JOjxQ9Y$R5`e{*1K<*s+&2CA9U5!}wD)dmP`E2eqhjUYPf zxG<7!+CAHpi>2#z*9n;gb=j|D;OhAM$j=Arx$3@pHJ$RYzCH3i%IlqbZNBpWYE!3a zEw6)Im16J8)Y582esjHMj9tVU!9+HiQ9To6J))~X1Z7ART~T&Su=IhEIZCQlhscHz zmwnL6X)$mI%?tL`wb_)ig5{)e%lJs;c+Bxh+0S{lPPZjf>kxEUNyoRcgh30jg>wl` z`Z6~%Z1~mvs%yp^-vtFvN#iz1Wo6+mqz*0V1m~zF08<)kCYD`WsM!j*)9%@Sj5JD0 zLi1asRs~R<A8XEw8PdyXxYSPS1 z+1SuJFhxlf>j9ADU)ERh;eQVg3_+WkCBT&w_XwJhG_YlAk}=fW$~Zt%C#RsHW-*xh|u?+MeP&6r;tzpY2It3bz)jW0dBz zV#zTdugg%^F!eUj{nC&G6)znzz{aeVl!@2U$&Ka|kh( z;sr4^7`Jk=hY=X9bvfL%QyNoInP%q+^J-O`HF{cAlb~A^?S@o;rVUNZz*2PAc(yIG zpzut;i3S%kCm-Roi}>+dfhl7XzkX>+l;a8a`t(3HEeN^$NnC zl5F?(+ZK>Bt!eZ}DBs=+@oPm>V9Q0VmM{9E(YnjM>7^5wwx8P@sh3Fq&EVU7xVx#! z=|EIAig{@Emj6Ujy4NE3F@SHj^SQis%Rt|SQw`We=-#Q7PRe4bn%3!`!tGao#hlnY zhpip=8;pG>^f-*rCf_UcUc`D{D)|{hzt=3gDpz;#3-uh-PTSn(Rz}VSpVwU>_F0IJx;g_I0`q|qg z#PW8ErQv5l30Ly1{BF$pG~C;+_q~pNfmD7<02nfs$>|4B8pc`e=RAqxV#OEZu9r)X z`B^_wjOB4{P1s4I-iy|71H(>kOT2*$)av3qwtCMT!j$ftQtclyhtf3?yAu0P*OyW1 zy&`?B_>qHk-80)&F^@rg0h%r~)b&}+3)P-<6;&HRkN=K~`;yRepD@HIpTpjO!w1Lt zpS|kb0<%6sayh27GoFEEu22$u&juAX4Q1u+Vs2|(*FI-nBG-KWFlI@i>3sGT9A2tC zQX(S^_%!^(5j=;h-1<5#zg+JK5zi)u#w&?;wMZRgYwZ*h5Iy1ru7dSWW=5}M!CW%l z*`A$myNq`^o-04k*T7XPuZ&{tHGD32d+T!K%H(N5YtU)@$~QGr-dtcLVKm zB}&T9H*@^;!9$mJ(5jl~+jk62qtiNEha)9A9vsrTskEXG4gkf-hhKbG_VP0WWM=V^ zd6fO8;gj=FHUH=wrbRr`GR>;D>()bcSC7;q=883Lez~wdkzYXE=w=Zf?{(w8 ztlTMg62(mPHq#k6rl7a*;qY3LoXbx6toI~$_laqfbQi;0K+Qs;TY=L)J6T~~6d+P6~-vrG?q;;r_yd8`IRQROB!E&<}@2t(p zUQdH?@9zZNin0cA6O&I05_3eyMMl0KI0Sk=1X;Xy&n)odbWf<)>q~Vv?DFk$hvmT2 z?YInXB1E3K9gqc_g8DgCWcOH$9&&dJB&}>)fgU;k_pbcQ3_X1P~K< zLxa%meSEztgqtoK%~ zb5GOG$c`72-$%T7RjR5`*jl!!rltol9c!q)An`ZDlls3I7To%mh1mM8q*!DyG1FrT z7WA0HAOi4n%nlbL9zc(oHvaD#p8l%@68_(OPR1{H^RCqEEBA}j8iN!w&=za8SA9Kx z7cfUO^CkFA}xvh;vxPifraIW=L+>a2!|3OGE?>yj9&LfvS0VYx>Dh5vl$?)a0WByhFaK z;YVs_d{O6R$*J;-e$d8JYwu&%fR3C6gf?89dS7){d-7Ox>JbRVsR{j&WW~#xFN#P@ zeZ6D$ zlvN7LYhJfgb<TSAg9SjLwDKkEcaP~KXhTw*y;+}a2%7c z*;(GZgX1gPFTU(Vqx0GxAyTSru7(C<*xU6kHD1^-LCLW3B{vG(k#nWp$G91I%dZ&&| zSK1r1#i;L@e`Wv8ppG2~Y!+buN#g_bq>Ivj^Zo5TQP?zFYxN(JdIPkbto9TNS)Z~k+kDj83tCXAu z-4o;oy=_9^ycYpQ(K}bc$MS-N<3=H=uV1RPt^8wc#8Ov3q%VIU8TEFnH~UFFJ#yrR ztOo}FZmw7RffCN$+(~<^__*niFM2H_Qv=prFFSVAg_469jcy6IlPx?eTd-54+!5fE z*za`RZ4#pLaBZ(}-k589DrC#y4n46Lv>K72)y8PRS=p=|Dq^$|ihr+LfY3*iEv{DY_&h3jI9dG3lv!6ld+vjpYvrGAD+>>}FdFxpM%Y zH;pLK7c#HIHT_IKJYTOvKeD!R^k;R*UB!*ALE20Vc&?uiMVxGRIw(!tNOyz$FjH~- zl)D4AUNgGh$rP3BlvOV;#5WBq_f|fJ`({bw)^4ev0`nJ6jvdYOyxNl>jb1~Ix#Vvl zAn%PfZ8D_1=1N%SS0(FfLIKQp4U{RnRJ$*AH>(DAhcNwW#Q&?h^jbOmc2rHT!4^mo zVrKFY7fq9$*aQzITyQD#_hEwDMMYW zuJ@dL#zl`yINb)!k0(L`QM_u@7=FpDHH>W{ z=4n<=`U^PIgt3ZrP6)m>#|%*7zhQ$~G)oA@t+8S+tJ44V6QYvXTi28e5s)+jdKi&X zT^&}IrXjG?>b&~~;ud*Yw#Yg`ahhVv$goYM=SQ`hYRUlB#eBB5ISu@|Rb(qcwu(h2 zMMthT!Oq#BwC_diS)+x!SwHU6^PZVEVi=`vf*yabd;L@+@YcaUd-;ThD-Qq{Q^if_ zWmPnl4)~(04gdS=|Ku1vD6_Ta;Ajb^sI^iY`P4a5v*FxP7`Xv3qk9!$$@rRYaWF;i zn-z|st4#Pg(+Sy=vvA(Lll!MEL(q*j&{F#c<&-&AN8##mAp0Gzd`5k*$c9JMCs)Dq zT3CjBwuX32Q!jWq49eifh3*LUIHg@7;bI-=2^yByviz_2FLaIG<7T4Yq_0wpm(Vq~ z7^u5HW4$VUjS^*uWLtho+!*iUf27oP)=S^l^<&R40U#GcGeMpJ7WkvmIW@DDdjDh% zL|K6rf3jj7xVB!hPAwUS-p8T2f{f^tfK-CFT5>V{b$}*|Z>bG<*=}65+KX(RfH^bL z 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 Ln@rfMKcD^|N&kwJ diff --git a/tests/assets/hlabel_classification/images/val/17.jpg b/tests/assets/hlabel_classification/images/val/17.jpg deleted file mode 100644 index 4370fdc9b25e130c995e1b97e3146fd974ff65e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 175283 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 Uf26V+cS_)KzyCQ5V28v114qB0R{#J2 diff --git a/tests/assets/hlabel_classification/images/val/19.jpg b/tests/assets/hlabel_classification/images/val/19.jpg deleted file mode 100644 index 26f10d093ae571c5d859382d928fd9d475006989..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44669 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 diff --git a/tests/assets/hlabel_classification/images/val/2.jpg b/tests/assets/hlabel_classification/images/val/2.jpg deleted file mode 100644 index 0dc56601729fa8908564c802c204e714593c92f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 278748 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 ad~0jxLRHFsxf0z5K$QMZuNU|F+y4R8J4a~% diff --git a/tests/assets/hlabel_classification/images/val/3.jpg b/tests/assets/hlabel_classification/images/val/3.jpg deleted file mode 100644 index 86483d70c5cd7e5bde85f68a6def8daf7ab53b48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25473 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

          GgvU0OE=5&!TSEJKni#* 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 H_4@w-$d@%u diff --git a/tests/assets/hlabel_classification/images/val/6.jpg b/tests/assets/hlabel_classification/images/val/6.jpg deleted file mode 100644 index 3a179941df5ad9c0308784e04ff0ecd6fcfdde67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143396 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

          A6MerU%+8s}WN3pc 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�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%0A%6TKYQEyfMq!Pf#ZsGwxKi3{ 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�x?+=PnasoLEJyRv+SUmq^3@r?EA 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

          E|YI&ROki4cO^gQM>Y?!w-#S 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�+4&90GpSv6l1{B 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�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 z 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 B`dR=0 diff --git a/tests/assets/hlabel_classification/images/val/8.jpg b/tests/assets/hlabel_classification/images/val/8.jpg deleted file mode 100644 index ea340fb4cfffaba602f7a48895058b9102b5a083..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178732 zcmeFZ2UJtvwl5rdQAA153wJTRrF8?|Wn1@$R|r-Z9>H?mw_eviD-`HRt-x-<)f%wb#+RqYt2y zx|%wgASx;lhzj@t9ZiDNL5%eD4D@u23=9lROpMIOIa!V$J9eC#or9H=4bCmsjkfxWol%IXO82QAJe+85Jp6IhkLTP%$wv9Y1!Qi-m#C!}W&~Oq&O+`aP zO-n;ZM@tKoz6o3h(X!F8pOwBs&tc-oAn3;_b0?*cQRr&zOR(wChOn%Y|6L~LQ>VGO zA?HLy#l)dgMj@i3tb{3ce8%8Wwf$esoN1 z95yvAJtOmBRyGb_R9sS8R$ftA_oTj|v8lPGwWISDsjIuEw{Li4lrr}A-T1`E*}3_J z#iiwyPn%oYJG*;dzwLkjB^MQl=8ssw-#>!=FLJQ~a#7RL($F&el8cJ^25``@(bAoj zrf0um!r>Yh+iKo2^>BTf}F~1LY9xmhqO4Zu4z6kAy$2scbl;@3k$a>+_m`Z>;L0g z#7B<`Uk5MG3Hc?y6G~X@v%RX%uZ!0?O%P|8#qhWPI{wnZUl{oB!vH6>Hi+HmGOT#G z{wuYDkPZ3l(!1|B>D*S#bCM@`sTr6VzpUEbFIZ`Fi&~SZ`QjoVrq;m-0d5bX<21Sa*YTGQ{=&e269$Ytz273~Kx@3UJ77k-Sb!Wr%qu&O;PF~M zT=W)dvQ&p5;08#~br zMOSx*;eNVAm`2L%!;>DA79KjY+p5|>^TXW-z2TV2UZ+70- zPKFMj|5D6*1gcYoiFST>$a7S#J{!NDaNXz|^D1oWEJI6{MTWi zvSb>iX_W-3CTNd9G?T{8Kbv|Q%wPIM?x|{l%|(e$=H`_zb}>bs?;RVa=DrU5&&*Jk zKWrSl{yhH*RabX@@+DtA({XjiDC%N7b}zNI9T?wB|09Oe@}s?RK}9B^C;^g+HL!8d z!+dwv97gwleVFds{pKyS&t%-vG5rH}m*{TzBdB^_;6-xA*S#7Qio{o%>(68oWaes~ zhcpkU!CI9!c73Y`!o={;=BoZqGJn1QA=|(7@RuI`PcQ(b+^?B%-;Y^;o4K?Fwq+~1 z9$S*NL)}Ui(392Ss|syjc+d9~H*;UqR5M7lFEN>QutV@DQZVjJ zoKu1$rvvlNn=DEgw%Uc)Hm?DfZGP_`RPguVz5n5vk%;VDZ`lvm246F{?H+iQm+G+_ zaUjn0U3dw8q>J&Zz5ejmNMWzdEq_7asjo%;01Ca@kSNh9oLT42kRB2I*()v3Be$r+ zobO6aUX9L3$|q#L3=eQO`a6Hci2ubg;$LI0|CwV?aDZX`4~@xp0+Na<*NsXPNWJ4s zPhoY38Ez)DB|52woHThXVf<-j@y9N6u`2Ed{DhtFobX-z;Z10ty%3&{UV+VY7#k-J z%0D-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%0D(J^xql3SgC~D;vTaj&OaGjmJ95H?*ZL@%e7v2 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

          c-;plccKy_3{(kCzd5_3%exIv~ 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

          }rZ~+9) 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

          (=7UqlgU$ox^o+erVv*7_ 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 s({EkK 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 diff --git a/tests/assets/multilabel_classification/images/train/Slide3.jpg b/tests/assets/multilabel_classification/images/train/Slide3.jpg deleted file mode 100644 index d94b0ba1b96d8e8090fe839a4c7ba2e91718a93d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64358 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з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 De;*s1 diff --git a/tests/assets/multilabel_classification/images/train/Slide4.jpg b/tests/assets/multilabel_classification/images/train/Slide4.jpg deleted file mode 100644 index 2a070ef7f17187a9ce05e4b3486e7f2d8b082aff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66701 zcmeFYWmsFy*FG9tiWY)HaSQJ5!Cit&ae@?g3Z(@~id*sG62VsL>ZfGHIX0MOAeFwikEu&^*OAA3hUeh$DS#d^vl zsEAEw@EnKPhg|4YYB4U0QvGM3;mmhdVSC?bJbVgDDry=wb`DN1ZV^#2aS2H&Wfich zn!1Ljk+BKH)Xdz%!O_Xt#nlb!=N}Ll6dV%z`b|u1+}n6qT6#ui*8A+7+>+9=@`}o; zYIs9qQ*%peTYE=uU;n`1(D2CU?A-jq;?nZU>h{jp-M#&T!=vMi%d6{~+q)k>fBl0C z?GfkyYX1f7|Ave75f?fpCI%+XKe*7)gB}fo6cdX{5c{d30nT$DGG?Jyxa3Ny#r2=@ zScDC~1MPih@F`eDw%IQJf%YF{|1)6G|DTZk@4)^KT&n;A47A6^!ypC71MYrtmPFzI zYyZx{za03N1OIa1Uk?1sfqyyhF9-hRz`q>$mjnND;Qz-QsC_p(p?IRvNycS%uLS$C z$2)I=2=hC@Mjn``mLB$Ci!}ufgF#2ht2` z*!j1A{KJjIUNFJmA4bH0!`;ZSSG9kc8kM{6ETGp9XkQTVtFPxQ_^doLFt9=MtO0xD zj7uV_s}(+l#HcNPxctM;PrkJBThF(oOaGXnCYwmuXkbs0fXZ0LU2pq$9&_>=7kpDSu_kU-#B|cnDn%?8(IH99o?hz}tJ~c1Ov}Jlgtvru*OzM| zlsz|92l{FV%4C`6uh)7hs|)-7?3vp|caa(P%goW9!8z?G)33mqzw z-d}Ep`7U&^yTL(dBnc#GpZ{`LYVj}v1P*^77rR|l($I0*IEl$~Y9A(#a379)+p2tI z0oq&f8XqM_y*LWB@2<@s0>C28O`D^slDy)k{Y|F`34EsR3w?Axe#{#ce6^a4JaH#K zu`IsU0d6zUH@_Rca@^5ut%i=cCcu2h1wfJxp3^p4!?&ZLcRw6h#= zPeC>LKX=O4SNU_SI7?s+(J;;;bQpJ5op6b5@~^=3zW_y%yoNSP*IJT}H_acnLUm*b zUfk5YK>nl&6JF=6WseWkYhA1zqCA7W6y>Mf71bKQhJ(hlh?)(})bkB)J~7=t6JF%3 zWy=%*gL#}?8OUa0@?X=)9wa?WHF+ir>Pg>`J-{g&Wkn zW3+_!_h`}w){4TO3C|zaau>8}t2MLV2sGeFT3mvbLkdyg5S^Hco=bi!6I*t3>jAwb z`OY743%~!lg$~M0ZV@Fdwn1%sHgogo?A`Lu!bWfi=B=~FLez<=V>3IJt)^W2!}P73 zOSOT`bI1D`ao6M9c6z4C02#Gs7w18Bs`_;erEaSFpOAusj8M& z5Hyf5+lMc}7+CoTBd+dBRuy(7(Wk0TPpB~c{P5S-oyuWIVI??(wWVwvWnnX*Pxq&64lI2E#obNOXZA>t6^~#T%L!-v&X)JOyG|>F^&% z!SApiRu24vOeVg7#Kw8j<=*CT-R~T;PyHs)uQv^>lY$(FnUEV;J2QdF_0j$(W=pQ@ z{Vil7qSlsfsC<0lPH)eA%HIj12{QP;7bRLvgzlKa*)q%aWP09|;&P}D9m~ZK_si}3 zRPN1hFT3zL7QdQ9N}^}j?&p7AJNl(+3|I^{Xq1-3nZ)jl6C~`^d3{R@s} zvE3ZqNF1=++l{I74YfTFvZ_e%5B75m54=E5S!zGu9$DRt)^~9LXYfGusctM9HI#N3 zD7nSMvo&J_t@*{H=A$OM;cj8{FAEx%dZW<3SE+>=Sr-Uh!6^}trNM^T?5sluCu{0W z^;xdx7U)ASN}1X$G!TAq0fLaFNx=0CsaB5!?(@)Fd735lIqjht+E45jGE_KF4&zB~ z#*Iu%=WOjQj!M2@AJWxHP>?Jn=mh&>*@nv!p5q31yd;U|jgrlA(a* zd?vZdeN9nwl5YT#9mxY{bA(W#<+igGd5}#Q8M}wqf1PdNVKHxsWlaO6x$!2^WF49{ zG=oj-B!OyXAI=HB0^3;d(4MELS?mS)9N1!x448n{^vfyd)73hJB$ZcN&rUI0e$wR! z);8CL-y!;{lAi@Rq@M3BzEbRB+<{$&s)aHS5Y9!>0htznm(OPrRB5IGM&l7Dvt5P_ zqPx-sln^%yC*Hy1O(7Rdfm*?qjx_W_^DlJG0l8iXx--~NmIQJanPf5ltF*-bX|1Rg zu+>Wt!p_Y3b_$rfUzTo{dj+3&-Hpz?;oaEEOa@KOwvTPtt73)Jq$pBmc&lk1@bjjO zE)O`-pbAK0Rwk@9FEyQ5cTnbDK5dTpT5T;Pvd0sZPkumZT(lBX=?9+}d9DCEjm7z6 zp|wwb-;^v^F|5*)2N}Q<2jMoyNBDmZ{fu|>97B-ra%}9CU{xhY12^pNZQ3$N31Hu3 z=zVv@QzzZuwKjvOHl-F(sCzRNhvaU_ZbJ6V#0gV<<-mia<|O8OW8@9RQcl<4ORPYl2O0M6-W7GLqN-P}FsNGZ zJVH%gpv8;(M)i2u1xJ5M)9W*jcYw&nMcgz=$(~-T5SprJhOg@~)(u)s{t8e0v)F{h=qhsN=;08mOkmvL~=) zPPtHUb1&~NfZpQReYyyx!_x4f_t7Pz-u zvqSzeu7@e^hB4>>%dU-6=q*J}+ z<_}-9V4U5$^sHK+31`}hy~I#Xv;;l3b=>~w#cD1sE6w}y;Q{tnz%>3m4p{as5te8Y zO+HO#)Y~i*wpe0lYREf@{Vbn-v{RLEKCYS%3VFEgvAVKp*_VOk2|)a_8&; zy_&Gwa?@-OMzgk>PSHT#zhU*S)NDLtd=mAyLKYEYH_`j+4zVcZwW20jp)g#E3CT?S2J&f2u&*_8;&7)dKv#H~nVhe=KHq`bEwouq zQWQ-~lrB_69?-H-572NQZ~&ncUc$NMxG4^>Iv2+0hyp}uuFuE8G7p8}{aPh0{Nd&z6&ap{;Zz7uFD(;_VtFORnvfYY8n5il#8DHfQ6S@W_*`sZSKGj zFbMtlq1a}#hqt&cQbg=4yA^vba9sPa!DrLTBvh+)oONnq&+ATiJl#I>QOy8(yfWnVW~D!{|@~ynE-qZ2xrXNlE@!=C%igyh?8(!2U-> z`<#at_6n*rmDnmNbId)G$UjkO6kOk;UgovaRIZ~iVYufq5v>-Z0(%)qg%JCk*a?UX ze(7M7iVElMp7XDj!y+3c0?>UirK~zM?Jyc*t#3G^;>PkS$!qs)WM7-`cR3WdTe#P{ zY3?xjJet3t{h=97oW(RUAv4yoTbGDWBo0Y)J5)U$a_#7|TFaU+QgeRK9mze}lnSmk z@z~w3v7nFbI=+Aj+(iCPE0!Go@VZ7`vI;*Myzk}_hXO(bm z3gBUJ5Vk3c@`JQxAi^hY`uOGLG|ZT{P;vrHz!AA2T25{`X@UHy{w^Q3C~Y=d0P=F0 zKm$$hR2Qj#NVPN^E*H!l%_SVyacvTJV+J8&xQhiL-@cfm5BEu&8V}^2ZEQC4lyW6N z0(jT=s=ism7$;H$Pfb;xV@Xj(p-ma7e8U}d$41up&YuZU5i~y2+-hu10#XmUXNlLo zC@26+tr@koy(dtwirF1M8pSTM4-DW9k2Uy=l^zEM0K{UmR3)YK`N#+x|%fkUW! zG~va3Pj~a?0e9#E9uIjEsN&PBowmM+)v_b1$P}kUy{5yb9b9IX-zH}!WJTJ&ytIaq zib_rxMdoXg2Eh&11nou6=_ z&*5wzme^>~RWEDVBb=9rA0%627Woxnt>bG;jGl-d(JeQ>^$Xq9(kAQK4+C6xa&iN( zhrR)r9AILt_eW7Y=P#g;_%EO+x7j2>{wL;Ul)#)N_oZN(6ikOHJr>XB^=5o4&hR0$oyTEovygT3wJaFK z$bi)0{y313Yrv_=onO<#vAw3ofI1m}E)TQ1Gg^zdMT*AN zw{sI5N>Q0E#()#>o59h3IYN9Yx_tZBSA(=a;B3B@p8B}o!q@)-2-Q1@7ZK|HTCWXN z=goW`gqwr1q0%L;20XHkFl zgQY5pnkS#@a`$^0V$LLBbmXKYPigA^o)`Kw&dEKHI>cT%xIlewJf4qV&-#H|AB3;I zZrWDG5n#-4UH%skzNH!M*Tz_u@{UAk_)V=>Q`PZWqG@X+Z;ovB^YD6$nkdOwU9co= zf{~LnZgFrK2){q6X$WmW{kPjJ1NSpw3~~bZ(g-4n)*)^2q?xlFAM^9&I_u=gQWNDy_W%*$*)E&uxYWqS$}jwyT~Ht77IR>2-05dsbv^ zOkk*dj*%cW>6?eGFuz*P$aP0oBX`cAF(oOEqB>U1+9Z} zeLO7L_GS1Ix`2`tgjXs6wNcu}+2*D@B1zHg*&@1J6gx6&%YV_g(wDpUo?d%6 z`&iJp>=m^yF3V#3(!~=I&vpBf`o&GP)!i6bEQfFO*SNFJ^NTZ8Jj(+Pn`2%uZ&POE zv1xm}?`MTWQ0%A9%!(AIN8d!F!>bGT|7P1h|GPw!D3dNURY>H`6|Da%e^8kLEF@Xi z+c8f%Ta>gqryuKY;`Foo1iHw%OT8+1V#S7u8$(I6}L}0n5!eOMok!h_;#ol^^zL9qUjc zg_D*Nks2r6^svGgWx+;<3uh3CG;Q$<2iWx%(QodSdRuAzhus2&TIIRxF&(iDhn7SI z3~KaR{LKP3vW@~;a*yjgX$gbpEeUo3L!zz@Xa<@Iw?Hw@ns@B}Af`?!x=Svf!XoD^ z7=-9Ke1+AaTrLgf%I__bA4J}ykD*~OxY^_h>P3SEuwm5o23C*m1~D|pR0lgx8A06a zv69!np1}@XD+=DsY)hT@Nq1s(w=-GL?V;bGcilWE2!G4+B4azA9&{qPowOqc1CQ2y zu9dnleT7sEg^x@KE|DVJg@z%X)QnYZ*->0xd!v>D6$~#pFI-x_>oJ9GkPc@>fw z!ykr`_ny>W`gDVl2NwHXJG?7m6GMu|*NNH{CC2%F7QRDI>*8@XkvdBAZmlDNHiS}O zNvq@Y8vySVU2czyFcVK3!;P1FMtdxOXuvG9S-o6l0CgVBS@nD?vCPMEHQ;kje*~e= z;u5^E{$=}4OOs?-y8fozDR&rt?)=)k$X%Kh>4(taFmU~Qzl0^LD;8DXm(YXS(@hsi zI!LJ1s)0(&Y&^?Pw>Wk85k>yZ2K!F2R&=KIC6&e|u|F@uo$HTXL?hz)MDkowZ**D% z*y>_?$=*Ckx_SSiN_6jYh-JC+amP%^IR1*kVh0mhs9q-2e7?5uPb4pq;&o-8YfH=7;Vdmzc9P z`ODvb0gpa4$R?9`yA=MR=N~n#^&d5jsQwEWX21H=CjTcIx?sE@ze^hS7hv`5pOklb zBhC6%+p>RUcB@}*EtMp~l`zCqYPe^J{5ql3G5@tz>wfiu5Edm`Je4y`GsJAP>$U9h zH|4v2L!`sFci3#(sL$)g${fHOhKhVUW+8=hL)G@}4*hIlN!7m2Ys6b|?pE#T+pVri ziE7Q$C$Jz=h#9^WM*e<9j1W<%$!p3SsRynxit!omdgbRy9%Ly{YU<}7I8sgbk_$F7 z--nPr{Bld`+fN>UIQ}lnmHP1gW3&BoT=VA>lQP37i9?1eaO^m=A>i{MH{RN0+e%_3 z^?pSVHph((wEJ8oNA6Zi9~07#OlY4Hg|h9Yo|vFio%A5P3-y;DWe=%o>bK^qPav7m zZHA!>hWFO*>A9ZE`L|DLW?^mSNS>P<*dej~a_KTse%Zr?g+ZYQu(WsOA5~ZbBoM^h z+o~bJ!dYSEXY@zka6S94MbLbbSb3LWwXE6xUpGI1>}T@2O;RtJUfhOM#51rx{P`{+ z3pd+x%u8mxg-087RJaMZlKWMg=VD^c_;pI0^cau(riU?@$T5eToz_Pa8v)*I(8 zAFWF@^B0-U#m$%gCSy#=tiF7eswK$m?{u{JLHaBq9}$%7M!tlOM*P>N?T0aeQy4zXy>%N;b%x1C41CxjUBaCuMrcQer2C0bqWY5e{qw^`mX(B zbY|}W#2VQQ?fGzOX{-0>&Z}*yAAnI);7*6@XNv=44<{() zl^)+k8zB8U#{UMOmO^%31Mg zgJv7;_Z5u>w*(_K5wGVD{U=O{#(h63-ke${4DJ#lGGzyR*m{LN3g>Qpzx}uxFkFx! z@1%AF8`amInA1MI-omkl`jXU*L<`{pmkzgZ_c~fPLf6O{3QpWVe-v!W1t+qY&FZ!v z6E}xyYBLB72aDq43Bom~{yh6W5F6uM*GFL$NlmwSl)Ws??G;$C^ATHh@q7W;4EX0az%43f#G;RE*eQ(g|H4I)@YQDM2+hg z!y3W#u~i!W=w+BJTAqbbb?;_ z=Pc|Y7x%@R@FDDHK^99=_HCol#2yPFJms$+iw+6%EmD6PsS!*L_zpM;zl_Dj(B{V1 z47xrOTD7*5RgBM2d}T3fUhl~W(YYePfAOKoMFz5Qn>MF3F70QPIF&=&Ym+CEQ4BPS&3)G71h)X^&vcBDjmag zma8wjaud`NAn{;?(E3vqp$>~4O@g``WJ_mThw6H{>0;B_TzEFdYZEUgt>E7cQj5NC z?CqU4tl1h$4|up2W-Ds#lN(dG>lBLk3BDnsaOy04R(5v}GN%|q89CLnZ4&nwk!nVK45 zWMR1sZlDBwEqCgB9>A06$Hgu<4Y>%c^t0qFAk{f!BODagZ$dDFk_Q{}3w}YW3y38hiDRJHg&HoW&b8zuEp5F!Z$Z*VjLEjC#aYXnz6n zo{vJ6Ef9EU@P|gK_ib>ksHXxw*hqySuBqoH>%XJRTV!3G%)&KgW`|IqXm-q?a(R$(xe$hNLTHiT zPHG9QFR{pimyZ$W@Bl7^X>xB>?W?ki0Ja{Gg?-a?XiRteIc$$kfL@}y484eD|CvQS zhobUxd)`+WuZ_>h$vM^N6KxDqz>rRz9<()-$dH2=+hxX;VUreH&D{EamO*OjYQFqD zeh?5=0u+{5578G?pUHeFDKSW>`RU?Xmdi0fYg$j)INPigkjfuxx`!wi*U3CuEE9au zWnH?*B1(YZ5oblaoa+FNu(le}3gx$-8W&+dM&YIMiO{}1IG%MBJxHQVN3t)8CbLG= z{?VUqc&ln0i0Yc7PD`HZFcPlgmsdlFIWAdPNqnp}PCsY55cQu{Qr!V3vusJ0)$q@~ z)MSkHgJU;u%(&|gP$Az&`)@d~J9z0iNJ5kZo8x#PKlQB(ETKEwZv?6s2?VPhA@|~d zESz%XN((hi+Q8qZ^RpY!5w9I`>!FVz>7gQ0G%hcag|$fDWflrzbG;gm7g-btQriKc zpD_GkX&PnKhJvc?AJ6a9P_kyv%|m35^jW6|56-{l!bCW6L!%3ZEGfxcKkJ~t*!nAVu&YWNIvdSWX++cOG6oTa`pg0we zTqgNUrv9I1*2v?KDm%eo*3kJ|eC*Q4on~$H6ZY!mjh^G9=l|zz`cIhmkCgKI^+#oY zTIl^x3h94hro;RELJC+BKDGia1$%M|NVr&5EO5`A8dZ9p%$!62VMRYvNgWmEoTxy4 z??^UWpLsvnX05^QH?-*}75F*`-tUA7Ctj>;W(la=q=jH|?sOyc8e;p@25~kfg7>=| z+b=L@^M5Mjn_T$`o2m{^X@{>Iu)r|wYR5yWwq#8!puyWAk`;GMt^-NdsSQbQASP?v zNkl#q&@O4T3CGPh7_jr`2J!g!tWnW|iwH6!byamB9?uSy4Fsv=&4nhCWVAn|o5|J$n{^DY>}2>5qsRi{Ta+L#E@1gdP8W%Hs*LgRyLX5v z)lLiFM1G5&!Ktm9n=eR&}c>@9!u@>V$0YWfkhxd|4dSw;?h9m}H78*P1igX=bn1d`plPFYQogRvr z?j|CVQ-s%AeXdCrx}YCYZBm6E4zq&Uf0>IS2egwrM`LlKN3o=~B;3Mly#d3I$1OZp zdY*9s_x~_m?8;GJXa@%Qkdd)R(v-cqmVLTjKjM%KO{uv(NH8TU`wgBA0A(aL(lX>6 zS-&yNOOT=)48e|9;mdB#%DVFjF~z0VbFeY_`V8NC)k^l-D|+2OQBCxOF>ys!+K3|- z^cJ?RLLY&P?MYoY?75$vI#$>lDn zTsXYy2%^FZ?Hs~xC_yqzr0HU+5lJri;Gj_fQsWy~r2;s>k0ZS!J8`>t?m zsp_Mr|3epKw8Xc+z~bbLA+cut(0oP6_9Vy#?5I^_v98(3iP7y*lV35;_!3Y<=fzYF z_hvVed?%|lwP>^Pey;|hO9BGg0VaH|-2B!~n0H7t(^o3!&0i7S%d?ipQF{2%V|Qc9e;yLd=|3{2`NXB#Wk{AZkjKX(v7XvM{=GvnR$Oe_l_(E$OVF`m1!qR!j(o z1si7l=Oc(ceoXL*@%--vw-BzI;w&v;VgdYI4xR+OquCPI_zdIGLh~V%I%z^|^-R*S zdkwZ5v!qCDs#BEHe)boDpe_f@G42(3=y^>Mr^J4~B}=YH+%AyfmrhLl^!6Y(wNKk0 zk*6S44CV(4871ku&zUS)%-fHx_3ySA}4mwm6AX z{kEW(L_>QKMnjgwI4k1@=HYHhcwi$b0K2sCewdp5-IEND+c74wZ}(^iqLU}2R&MX{ zc#J|)1wUJ~M<9?Z;J*O#HDG4-sLkd=&TXunhF{2z)qso#}n>b1gFfo`Uxr@M=e z>X@U7Ba@N%-4QHW4uthTA#Q;OtL+B{xp38fBKDRL7CL{7#VRcq0^%&%vh?&*)9*Z} z5}2H$K|<()IiKh6^PEPG3u4Voc5&@$(Om8`yo3eTx;71!w?x90wE^T`xOY&T3CvuK zWESvPT`}QPmqJQxr(x0R@z;UtF3rYW&4qR{>9bi+mf}d(BQ78X{T40&zyp>9^O-sB zPd!cd2K5|rFe93S*Xwd3v3VRJK?mmBBL&Ru59x*~ft_S`Zj;4JFpZeEt4gLx(M5)N zeK8}lV3`m6EZt5&=M+Qs9av;wP5m>3J?OvBtk8pRtJ#QqHrj}2H=E9xllGpcQl38? z$DS_EP?GMF!X867>ViBt=>e8*=~Z1xT!a;E_i!WE#lT{dazsje<9J)Fa{EE$?AccVa&v+@-!Nuii3 zdpXoFl8~9sF)|F6h2F22l78o7Xx92$`}Ebx0ml{4f-KYNtfI2s>bM2<7ohWBu|eVZ z`J=YHof3K6j)4D(feu3OJAG_bOveM4oR%>RB-z@>;#=N}Fkproor--9YF`5QBPb4Q z+C?GvdjUfz2Lo`alvw+$9-{GR561^~rQ;g_b09em%{|*rj!25Y3YqMtOAOz|u$o^) zp66LrK&?FU93uLr7bluvh%5W{eO18^2D3=>kS)G__6|vk1EJ{9tQ$P1joyOFZijWk zbuw$Vso!A)HM*d}c6oViQxkE2W9^?TiN<^FrzR6-ZE+ldp(eiEIVDs+xx$L%*~J7x zIHDpR_`5Xmx`TuX_eP%~o}U7e6*Z#TT*A?Evo=_>42UmW7Ry?j+9{uqB<5s0(&Qh# zCphjEj}7qB0VT35gg@C{reJG(o9#@OqPHox@U6qc?;$h1S9qP_X19=h&gD}}3-;Y; zx3*I#foYAIGPi2#7^kzr2>mfFePS@VkRulr0w1?RsedTJ4nJLlk&eqPYUh1`zL`%2 zjp0hwO7*(3)vk}eBnwJ?yja64bK=B?wQ(jjTkO^#pG=3zYBlwDGr%Cjlh4RPE@vG#lWBTCNrpr)wRSc&~&GnvLH!CnM+MOSrp8T!~IDM|$8{;!692v}JZP>P6MI znr-5z!`3~&7PV(&anG1wd+1tEQ-XI*^Ml`Q7rfbW>ufcbxf(`mtJP8okLqKoabu9! zSh1B!Fig(LFP><>3U1BQ&FGWgGJT=pJ*1@K9* zLVzYMGOO3Cp%fH~RCkr}GJo)@!qaA=FtM?m8R5MT=z+fqNo(S%P zb=zC`@0V*?=CMNxCVz8?c6$oG(_;IvgAmJ$M&ekc{djxYmAYABsA)9>rW~t&yTm;! z?eNK>oUdM3V~^!2_jyV&?tq~sBcHf^lga$Yrz|FZ(wThp{qMT#ESsTjr0M>MLLH;7 z7B_Y!9L*CESpiS;;ay08WZ{9DP#CS47?j9q2M(QR+*2H|%uq+AtQ)b_NhO3iyvWUP zp>++o@2UjKt~ajZW)k@&gf2$A2FaOaj%eSa)_B=G_JynlMhnYK^5PTcP7_}m*aTRd z2}wB57bF~(aCtTrx=q%!bV*J)9lyU@6pCVGdzq1XEdD-~Kt*laV*Bvw<)tdmN>dgt z%Fo-F^aR4tyZC?!$M6~Olj?1o%WfozK&bV#dgS{0YREM&jJybTe=LUaq}!8ezYqV; z;H!>k73zP^v*Atwst8fn&E&n({03FuzR{sMxwjgg`2do2x7BJ1-u)`=j=0=u+W}Xi z-e?NF>#5mtEvFTB5_+Qknn2uQrTO(JW9lpO+m^FC7T2$$h%m;MR+b<211{r~FZ?>c zmtxQEvHS^p_JHLc*e)N}DDw;Rl<_mAw*19HO+&Yo#br30m`hX}DejI*e`ub1yW9`6A%UQI2ADVZ-(F$$J_g*htz<9B!s@;e|Sae4&Qv8lT6gGX98V9Wzhn&U+BVZ&6ofzWXVqD3ElF|{}8cFsb zeP%$;l*lq=_Mllk71gN;-&+_?<~pmokOo4|=7c$HkrMV?oPj12vr|g|^R{Gk~UgtVE1%KNZNLyO0Qy8EdHU*x%Oc1i+h0(p7)d_((*eZg+GFwL zT3o@q`r$SE&5p)dd(@VK%-7c z{ROzv%(Qf-m-W*<%p4ip1@Dcvxn5|e{aTfjDa$eM3^NvkBrBIwK2^ruN+AwKiC!y2 zjUtDBbj~kQ3T_^qzhE4Pb&*;j1DEEsgt@lqq6pn})_co50`!L)hLXVA0&h#_7TX_$ zE?sL>e4`}Kt*Oz?@eV=71Iq_`hIYMXEwC)X?D|5mbB^{?d(^5Gw!M&KX|YPfQ1-)s zVM;9Ii*v2Y{n=w|j^HuNsM-xPR8Vu0a|lj0np>?MArb3kN6V`hwb-0yKTZ`R~*%+WGYb0 zR|LSO+7o_`wMNN;40r7v@8FYB?>U+Y)KN5lzCUnl0Ismrpe)b2YMA~ZB`BYP^|t!LLZk9y2pW{vzFFx7|*4yi_> zNflI2{ch3Q=Nj5k${Ig^jErul1pX)S(La4adndu>x(YiVi)*vN-!$_{t=jUA9Ntmo z?X~8-q#a1cx5O^bKsInriB9F!&H5Oh-NZ9xb8-YY<~jS9%p8%PY6-N{gK|F2&FuO1 z)oQkHZ#CJJh$I9q#0P#WuPIOS@Ns;0RoqUuZ?O&rvWK2{Me z4(;51QLYDqmF)gRA00u>!Y5W|l}}zs8v$pJF7Ys`>9OqHY2Um7c>?FahaNc-69li* zf`2XeR?aq+wI-%?I@TRRi)}!@bKKupugh3)uq#1H8Uv~q>7&IjLV4{|rrRYZWsh&V z%fw?$U$WA3wS+pXOJznR9=aE`d~g1ihWJeV_Sav)%kqT7a3POn-pp?-k>5wG%NU-u zxc(k_PI#1bGj>EH0|#Wu^XZ5F1%!4BtG^q5#p2JO`qNlU1TuR~E1rsO72@l{0X(G8 zXH_p^iYPrNTWq}U7F!pz%XZjo8NM`fEff4)@S%co_CtdH2fqN!xjI4)^t_H(8Dqye zbe*q@xn^WhAr%47)we+V6|1kgj$089&v}qN0O_Tu;7UvPlU822eh&0*)1hAp-pkpJ z9$s{QwsIR*eLexqohjtI&Mol-^@IfzGbzjnENX(VMcIYs{?+K-8FIfnZ+8c+kCZ$Y z{RR|FWA6vKyC;i|*q4jR)XlI`=x*SlZu&A;Rr}1!A zGvnAPN!r{9Dz~JJ%2{&ni1yICv1FIp)j*f2AV9OBtNkc`l^v52>$b`e%^-9%2o>Xk zz&9iS;cA|h)hkK-G?-$^ur!O7CmcE9ST>S z?7m6>ujI--{aiwL3#Km*V(RoSa+kkH!n~T?2jZ3MQtQ))EqoPnt_?yIhP?(S39jP!47!KT%ixpqj~N^;vImVNK;~B7M06txFAEcn z`pQ}by$*WC7LjAtHkMrPK+ZMpStT>(uWwGRI^tbkq>xQ;4VJN-m6nj{9t2@GKcLVE z83gAS3u3ySklJIp9Mbp>xV)!gbuRC06JSz5$;#(2w7Hbr%^dDBiY#9}DlfjP8+hEgK#>SC$SMjl$0>m< z7C{Q}bgbx7uzh|{qJ&scU*TD#E818NtKS6s5*IwaIfx{@0-9#gUB8aVt%O@|@E#f; zDc>arp1?7Cnw3C&$lJ3s+$d*!$z-9ZM38f+ zd&_0AjhiQqc~|Ze>aTtfjpfGYPmeC_EsOPNOTO^fobFt(RQZ0tIl;cH{?p-Q`(1_Y=klSzUtLpsA#~C% zdk)vNA{XNEaq+eNe5_(s-mrX(8&vh2VJOU~q2AW$f~Xid$>)^vP~Z!UK)}D>B%n`9 zLYTFJkg_aljj4YDAxxIxK81tb#5(R~NXF)5{D;uMN)xy(9?4yfjH3nI8HE)HhqqaH*Nrl!rD9*%xanoO2Z~ZK z_NsrnS6m2H&t&e>whCAzYsNka&O*<@!2t?+ROP&%Q^blg&4GmAu&lOi$m`_(I&96! z?ql$?{3tWMmv+R@yAnqmh@af|bmU&p|cHQ&>B9kkxMd0xff-9BZsiZ}aJ&T@{Rp zOYLrAl#T;@eM1%L$X#?(&Cko*987PaxR2IzU1P{qD?+a;;gxj4^4lhJ1!vs%a;5Mc zNoG&VD^^EeXusw=PL@~3{&|^c3%Oz&7`|pPem0 znx*?c^er!>SwidR$YMo#@EB$CAOCoyMHlAxYg&mgp>mJ@SpB1Y&v(mQi>T#zmd6z0 zUR4UG(Ujy7gv8Mkyc}%VI8D>KMZkdhWqf{6_wXSa{=}mEh5dw(^6fq$c0i;YqQa+M6o1Q9`%%72H&bvFCMGmV}9g+W9+TM+KSd_ZLC;vcbDSs z?(PsEIKkb$v_PSd;#QzYaS6d)N^y595Tv*Tmr|sMz0ZHH|9!D?vvQS{XU=bpG2Sud zoHyItIp;v+eq`MRBdNt5au5uHYj{Bk%tkX3o} z5OuutDKJmwhTbsW9$@mb+__r0*-rdJiON5OLd-UvU_z;+|G+ORk~9C&wEvf@HqkxD z87<_I;)i^rpVEGNJvCOEVE?JJ3t zR5@2x$S@dXLBpAfZIHo6cd^!ws}=?JDpgVBW=9qvx7i_}_N!v==})@aD8_>hMjamS7q>^TZ~uQOC?D(}k#Gl8c{# zcwT*7MSn$=X0Crdts7z0R9XrH$KAiha)a?WXdB zxzxaI-Tb8ATuM&zD-X6LV%*n)>N`xgH`{zT4th{zzJ=9gW8&;kIi6S^l%ce?8e!H* z`I?Y(Gwf=zDA}PKgY@P<1bCuVZrX?nOv(M83yD zn9W2|I&QBuT_X4`=MQTBEN5v}bKcVFp!GAqtj*i7sY4W^JVf+Yooxi!-)esLyEzTp zYsyG-&3wNFI~E7w$kO)9k8*ZnSbLeJPNxfw1V!HB+=UMPLo{=>jx%|hhq7q3n*Th@ zcUzl?oTJYj%-so-%2%do?}E;?=LtwH27NYeDYV>o&%(Ex1c#bZv^Sd81JTYY#xOV% z6FnPM)oa*T7s#%}m%zzYQz6Hh)z2Ha=KLTphD16 zz-}BYxYJv`4gzZQWNv=HsSwoI zr-Xk*oiWQbia-7z*}d#?q$u< zh~3Q~RudHphOd{QK%J`J*^*mk=te5_iSWazt4Upwa0|| zVpU}o(yG!SwI+_t<@|q#J+_zwzp2lKF6|!bFhRy*UsFxV&dB0(ntZ2Q<$ z{^kPcW~|?!^`IejY1OH3tethgSPkhgMgPvDH1h-DzVG>JRW5g*zL*u6m1IL{Jb$CY zk|s@AgP>WkhrdRs=nTp<>uRMAN7s=4fv-QYqrxKU*Gewfidz8j>N10h;7aNCJ0YI8 z8^JVQkIbXk^4YerGttv>Z5P z8R3Hj_ND5I$2bn>?K7@zEOy!LNx9$;`PCs|oGfsXxNEO>lL0ijz(UVJUXYdM?Js)Qz{* zoJTj4Hped{u3wzLxBB9zhm`=}<1dei!v2s83ngznQ&cn%48U8!UC3Pc`{cU|$?^yL zHhC=f{tqGDZ`F9PJDA4QK~#6pXB8c-?ceFb;uKZMkln2}jOE>83;XQijCo6fyKxB< zSVp+4#KXtSGKXi->wzb`yIQ&K0M%7iS&&Em;{FCB>Uu2M)!JD_4NPKY2U1q5MLMYO zL^_I)JnE*6E(KY<=~iAdN`jMIMXFrv|M<`zufd!AR2TxDpB3s{XB)`8q6V*6>$sfx z8FNh#HA_9`TM5`u;Vk+Htz%dj0KtT<8s`S87b zN6+SaI~p8IMct>xHl7xk*ODNEK|c#dC697x@i5mu_QQ5waM9aVKZiA2D-Zz||mKd74!pgAJNe#6(0X(_&6 zrQGY8D^jWm22;oZjEJf1f2`Hr2!TA$Q9hp~E*vo$h>t`?%|+L1rE{lq785hMg%u73 zm1$*WG(J(gSSxzdo%MrNvBtCO{;Vl-W7aW}6BEyRi&1&&B%we=G>!bobM2HedR7FL ze@fs#8DVt-P3!Y@h#6{bEeiU?3f~M1IC#CJKazlIk6vQ-WnQVPu$R;P2Z%71a&uXv zIBfX8v*TGbYvFN|o>Is`?tfOa|K%dSoB->>sa;Q9{`}6)3A1gl$64a5hJzF|6}hr; z8FNmd+{GGFPqyvuBI=oYKP&}|dFTqXVwDHB+g>?JiL%y0TeUF~c(j$vJB=5^>jXrc7b zsHYUI$&m1)S=>lbvw^RX`j9BEVp8`r*p)&+3nYu~1tZjAbpFXGI2U}BqwHmNHiHsK zD!;)B`$^^HIwlFKR0{sFq0!${N6i7blzLPmj8sSbyPS5cgS|dt;)CPkyHO<(ck0M# z`-U%Gb-1v?E_nbGCah8f_8%ITnCAJwPIKGgH1TEX6|iSUB)iag z<8s^Ou)U4G{pKtXtNqB-lZ{_2Ra;7e)@5dfzWc{=^-iKzW^comKSODqA_zM%k! z)m`0~efKNr+eY@FLA!Nx_s_vzh_^ytwtL8(Evb`6Quj0}V21gwL;joK)J@fCTPjGt z;SEwt99Mqp`x@Xc>~mhuVPxk?`+o?1{%>ltnS9RJ`aNnHq-FH#y35{&H$8;Ol^tJn zaE6=z+Qkf1T**%pNj&2*!4@0mz-kGlD69%33D1G zjY%ooS6I&o2mO`;-8tz;At5o~uLO--Sos<1+mlePSc5h2Rh2=bXe~&6+QigOI4K^7 zonsP*R#Y8f^ro6p)qy__QEXah*|fbqL9gA$bRd@7nQRUj|0+8O4qOd+26J7}BP@i( zTPju!`jBM455a(EuH$(Oz0-@Izl&*f2!MngTNHvOZPqr-CELYVnoC<86Mn@nS>~jv zRggV3#{UHf7VXF|GC8feiY!5!8Cj(lT@H*M6(u}gANwK{p|y$5yy}4ZBySlMsgZYi z(GIr|akc$lG214*)LwDn3r>`Vuo2p4=jSJ;5&|xbbMkg=6 zm>%SJ*x=pK{zXbzjj5OH0NngOh(so@Q+Mr4z1J6~_P~&@q^@*~NHt%wN_F`~YlO)h zJ1j z69%LRDAj9|+9tJ#vg!xToDDjX9~E&p8jneEAaljhrVr7 z)JtgVtqHi-Q|>W=cQ`hCqHmbJPF_axk@N%bFq}?5(-_~EvoD(8~A#lC!K? zlu?c3H&q>O+@-yu$c48hV6HXw4O`Yne-HRX2sq_Fgkmyso4Fh3zzGZS4zf&8>>`Th zvy1oMsqISx-f^|)s!PVMr5&oZPbg;CZZsAp;y1FYsnd~ZN}n#b+p>>#Zli7(GI{{_ zz0G9y*T0wM_%+r0P4&bj_X2^-w7IMrD`eOGRQNJ|x&gScBbpp=exz52(BvYrbzAmn z*sNvOUS@DmuM2B9@S+?AP|Q9|L6fjd{N@Uw&1i&C@CHAFJ-A}t^BLU z1ndchtDc@p$(WmK``Hy|=N?xCt2>Fx;;fQMRRcLS9rS&YYiS1s*?fjs* zkxbgQ7W2oVjuGVneGenFI?Tmn08I7_do4fn(QY)A8#L zmHEs|_eugfQjhtuEAyLsDpfBgsgzo+uT5w;oIL~&p})z-v47oEeQ9zV1Ae=^=i3k! zhVNHWrL5MFfy>82{Hv)6)|+P0Rz~p)dU8%^1j+2g3>HF`C5+ilRfxvD-E7m&5^pox zO~JSwxWwVs_$C!Qa}Dmf&HcqR52$%fEUVay^tj~1#z$~VqwQat)9&B*T$gK#COqV( zW;1GKE}0ufn7rD2r#M=%ImiqXX7{xdhY7G8vH}NVA01QRKp&Q?xADZH!%*Y4DBFkP zecKQ0xAek`iDJrwP#%K8&p8 zPzjrfxa8pRSn%BsHB!z}*?UF*FTpcZbUU1(h8}zl$qZ~IiAwk>iA_+PLo@G#2SbG4 z>EAaKi_&IDN-hf9G(Y$)HlYOGRoyk9pJ68L&}Og&5QzKto*3pZ7dcM&)P&9l2Kxw0 z%$e7x*oyXAEfuY2w+mXliTfQk=8_H)|6(}vgGU%g1uDogeuuInb!_UCV}zuOwq79#PJ`#`sTtKWt>WIo=jp;ODx$xGg1>A*w> zMRb$Led^&qk;w~5KiI3dTrKBZ3uo;2Ao#b zqhc`j&=P}v$Gzzfz|u0vw`k>E!a+fXK&$L;bqB;Q{jSG<&CYgVD#K6cMr?aX`$lLH zL+nGDh0S?_#Jw6>uXYK&G4?F~uiKn=MctrJ|GV{Va*A)iPFGt6*~i%QAt-9A&%<(6 z>O{}^{HuhLshDufyU+WmvwmA^+|r!5v_e7OJhIQHjc1l;SQK~0$X#go`*Ek#4yM9D z*!?35YB7w3zB=5j+pduBs+Ko9OSEksX~+5tbFL|tclYwh5TImEcaT=6hcg+r!Is!aENf&*-$X)wKI@a5i% zqk6dsgTg)Y!27jYq`Z8`r1rNMCBya=sgEdLZq6*fnC3UORD<0XblQz$*X3|hC%loN zC)5`qGkE6{yxt9GFe&q$m`szN?yhi zawna~4!U%Y#&?K$S}x6|`WDNd zU=WdIs7s>~(dS*=5F?Y!poJV_7=FikWMMFhp_&`#hFHoy8r37kt-D6A_gkM$j~qEt z%~#LEComuXtLG|-o$7U8+D4je_M{3^^@Ls8+0~DX`9C2WXMt8MYhh9@5Cv&^Yvi*)e}iBZIC*o~zZ{}7^1;U1}v`sq3{x(TzdNGNt=D!-M> zU5)tIQUIG|^;_C4mJW?zv`jI8&69M7r>%!ccgr>IxA@1LTKs5ZEU`+a%->sd_~7EZ zU=so>;!oK{+?7_p>9};r0HPz+j7%vyyZMHJkW2H1cZmE_F3j{fTqH~E1$>^`i|Q9- z+Y5oh{--$p487Gl@6=b+yhP zm6C-L4#$5yL^F%enT*ExPJ_OYla1wajmiH+7zaQ?huqnRh?j=w^DoL(BN)MtAk zp}h)gUoCJX95|EeRAY2GN}QEE$@FNXEuhFS-(FPmdDI8+Te6_jY|eTAs@Dolw1QND zgJQHbzqz|yb#^V-4V4c(=d1zn*NYEeb8eAG$K1&UXx*fZd)bg93pEOiJMHe!E&0!r zdJ6Ig22-|G9Df12xnmNEjzH$L@cJYW7BfoM$+-hstmp%O@gNd12l$YD4R}h2x zzs(0%-%f1&_XdJ+pDY)GY2-~6aST1i4VUpH#!>qttB9hG08Fj+O~FK!PBUA^$${h) zAXw8f_l}=C>-Y^+Lv?K`$570OHlV@JK%i(uC&Qzi!1ERw`bjwXYb#TnxY(f*f#E=t?KF4QUc4+RduNGE(e? z%w5CfZ-&MB-*=5n_I$mW5>d&#*k`gm)G~jKMdeNK>5WzVk>zzRBF+dq642+({L!~)tEOMsyaEdBY_0L-)^B2KCCX!K) zkU`nYhKEsFCgI@1x|Y%kM^oK_e+UCXHD<#}#4TAMO%jF{SnW5mQC#qLn7!AzSS(d* zA@q==!No1OX_p-Fg-8AKu5004RBVg@xX~xe4TlLkO;LA%Nio@zK$tA&Tt|bd@;GGp z^FM@!W@@sQ0``|fga@IJiCKsl= zLlUXj(dIhd-bNrcN~_-OiIz=9sMMh_FLbbId7C&gF25X8m^{3Pe-)LxlyLimzY+Tp z4LRyuk5)iuK5`{tzI_O{RD~sj$s5D&cPwn>7UDfK#yt%cO$l2W3+0Wu6C}I$p*Z%5?u#y(_PacUsl}rWaCro*XHcL`?iYO^w)kf zg-z5GcmtO(P=ia3)C~*mru9?rqMGtIATGN6Lx}E@UDYgG-L~1IK|O0uFZP)|HG8_^ zM*oQno~ut+jRC+{+t(;7&Y;Apx}(boOL^7D_Ca6ijeJeixhO36Q_?255)*V~`&NA@ zlbu?VQ`if-#0|<5m{y7McA$=J%r+Bc5)pKV!4MN9^MY++dGd)WP%LTBi)uRlJbV?h8ReFa{uX>%+U3!b#jW$ zQPgw&Lh6u=cN0OnJO|)@^}0J#>GQqoBt1(h7=lz6Ppi>Zh;DymO_X7fk2*2ECnfJh zxq)1dK>W#*{>0$8Bo?~G@sjUA(NR;=DsT52Qv$}m=VHynJqS3WD3K$b9b+XwM#a-+ zR@fOI+NCSagQJuab(%CdqmZ~GYqM50VmHi@y1g}o2CF%kVyipb=;ncOS-?h=PG(Wu zYeHi`wysHIZO{Vjzn*+yEwQk!s<_%uhVl!TTgQ*w&k}%bN!mYnQ0r!8PteNi8hDFC z_rDnR|M^aBPGN-fgvr+OT=TWL`pg>K+BJ%*cKn(T!eKInqQWR5D^}UGQk;3TJ3JNw zh^LTQ-Qg6S6g~2G9#gHpSYi;Oo`9+?XvgCsEWN%p)sLc)y%zNF$oRe!qXM^=N;UxB zq_*VODil498<;6~8gWP0M*uHjOzYTQtpb!14ag}uJ)r7fZbWNW)yd+-5`XbKBXk`D z-EogSfD+l?g-j7|SXJs9tBh@qR4$}%*aZC$Y+MyUu9DU(8-PxR3iiRj9eiky_ zBA+yEO+uFbJw^}$- z5|=~IGyaKNycKGD3`j(_Ax^S~8}BSYv*X!%X$Ma8D^4O#C)H;6J2Xku}2J3@^3OzR{I7utz;^ROY9Vsq@yvu=6Mwvfak8MIW-i`?CSTsQQ3mOX8~lp4Y*s3TZ3?_+OVyOW@Rh~Q%W)7JSLG-F2HCuOym=GbpL#!#9(Ss?U+($3O*>e zxvB1)6q)3D`_6tnB?-8ixikLBevDh7K<>zVs(#1Jf{dw6P0kIwJE@yZRn5f~c$Dzm zaM8;k;5lCIZPA_3MKb4xsrh-iIp}t%GL$veagm`-wUv6uW($1SloUJ3U6fF%{Hk;g z{a#E^Po<1I$sFr5ClNb!wPA{imf`rDcspWtl6jxZxHiGN$)9@%RrTB+D+|moNdji8 z5{Q(g>UQBLxm4aIhXXjVg&h*j$@Y2~OO5eI64I{0lQ|`(G^XYxobM|yW01+|-ybts zqyDCuHyEzFvf?_@8?V4g6rCvO!kOJkS#3`u3U0ZJ?+AYXFvU^SacwF3XNk(S?7 zC4-aQ>~R6`l~a?&CuNjtD0A8i+Laj7_$1}@55X;^WC(U?kpM4JaU1h)`wIxqu%&mf z6&2d^4DflB@|f%vlP?7IK>_V;*QJtAk=gG|qV;4flR* z$M|cbEW0QAPV?x^C>vfMm;6W?NBtRF0uI0|U_D_F6TWHtv9FR#;wv{Ogy=I+|Zi-X}rm&8eX!zF#{Sxnn=Anlu3bgT?DIgXdyT&vL7xdi}D>uQla}jH9u?i!m(fHlT zcS3Q@nnXLLmKl+&7=gBpoQu-KsbYgFff8@c9l=NO9)9!gl=PO{ixw9OepO#F9*e5P zpx6XK9)8nYs@#zc9=rKsrt$Ek9b^2=O?q)UBzR#sT4{Avu%vx)-Ew>!8(`T)9w5=xH4wy9>(QYyfez5y*p z9`&BKe0^x%kJ}kj9pn1SFLzlgKV9)CHQMrTj$hpgF{s`L_sAi{q&rN#r zcP*@u_}%#R0^Z9ibM%U|y!L_!9sQ>lWUis^6|br2`)_{JCHrNhd)SKp@>R9TX*IkB zB~bkNhcMq&@E4<$D{JW>P17-$Y-nW|jkw4T@2Jd>vLHUnRFkP;ye1r!F9yxZmx$$S zVzRc2I!T*t2RLs^=MR z3af<~XmaaWLs&DPU;B_R>r|hLq&=;GU`oGl+_0%!!ejQ|hSmt=_LkVVy6wh#^0VKm zD>Kh2?P+U7Ap*$F3O=gUtqDa1AFN!yJ2ZJxpkgoFluJ5$Y`7$h?G(6tSN<~__waMe zs8b`|Ns}hVt0LNw)B zufGzlI3t2iAx^Z0Q*X(LZt30_Q_O(RsH1nSx}t>kJ9hUqTGOA8Z@SfzemSOS zlhp`VT2x~9#>wyrt;8%Fu-6>!v#wsJv@*A|&eg1`AjzhXji$dM%Z6^z<|*^tE{A`; z7llLzeBg|TukXmLAhP_t-+l_ezs2r@vgvJ_;_q&1qIzj$_o1FqBo^k+=6hwPLg;PvQ_1RT|(w6~!X9dIZ$89*w{WGP`=0K1e~hF5gbFT~L2DVkYAh0dX@ z2*6E|zn|I^`|%x0e5Ax&CCBv#Vz0ZL<}2Akdx`Y0=8(;S`-(Nk7I}#l*N8LMcZUJs z6pVhFQs-GwW&c+bDQX}<#DMxtFLDcK1zJg-r6Lm`}SG&TnJBjSf$UO_Fkk za_iFG)tmZ?nFb_khZwes_{ftXr{sdG(y9k>WYhDT5DOU^*~Q2Pbr;kmOoVkxEgUk< z65`i%aq^q9`LH?b^U1@{oMau$E&m~u!VUk@hRmBTkPXeK_5KCOCv<>0e8lI^9ZkxO zSrKvNHY!ggD8IIBI}&`oTBgmjw;abFKpC8pwi;?$3gU+U2MT4?P(Ebqcfjx#Yh4PF~c(YWrk8cITKy~V+uMP#my$O}(!t6yr z@CJ&c_4*atxuI7{J84hqWDtWSbT9lfPBINz!R4eMxpp49P79zO&rLX(Eo|eyq!(XH zPq%d#_ze_Omy{C4L?>O$oVJhQj0EwO2OHI>34dFKW}HI8=s$IvEhQSZ;3&EQSHGFr z#Zw`$!3{<*cm71n_m$?9$v$HPyi`vBFfW%*91gJr4&oO3b|FFYfHFANFDZ7mQje4y z;HU>+ua?i4Gicbw=3DTCHhYuxx+u!l;s`wKM_E%Rp&D+qS-2JxOh^-5Pgm8)Rn7^6 zdn3=eTH4lxYDO21;*Xjz$^6q}e5v%~VKJ*;-p9j`MREd(_2K#5tRbwT`e^rQb~Z& z5<>Bm!x%_xQaAI`u^ow*9PN_J(IcaDbxz{l3tkYh=+|-UlsBHbMKzq+9F#2stjsa< zGdYcS8NPc}sqdBSAPQ7+<;mwSMAIQ8``SRc`cV9h7G?VtR2ED#l)sD49xY=tQP;H_ zwCmMto%ab8YT5>(`s_8u{?tvFc4>rTh)n)Aniw9nG<)%EdF6iw6^JK<1E8K{0M1R3 zW*dv+-iG9sm5&G1Z+yhRez9x#a+->}DG+T;b90L3FLmnT;YmqZNN@Q1N}YEfOWzpG zO`j~P&=#@LD5RymCIl-&rR*F&_ zn7Pgox6rj>ab|+wwOi~*tNtq=gV`kgHr}0R?Hh)@9Qi|{I3}l|we0>bs-YxNdtJNj@C0`=%RD;S@-HBydsf#ajHPY%ToB2l!#$*>} zUs9s%h2gi#9}BYhrj$(M?9g^m@J~!$!V7()Gga?v?Y1ymiCVe&_CZ9M#oVYVuukgQ zd14zzhutk&J;S9vywh}om!cYZfZSCo`@kx}Ucva)pOF{t+iZ?;GWxfK$FA?)`&sx3 zLgH>RzZN3ZL$oGMvqsJL%GQE12sirFaMB6WPMDxH z8#lmzKE5`(|7kDNW9bT^p^Cw;Gb^Wg?OPw1`t8w^p>yY$^`kmIMVzZ_Fhz$NlC@w5 zRtu;bnGC@^R4+!Qo%{NN|1Cfw2NrJl51}z^%+I1aSi9b@CE##2D#=F80T`;s?xPiw zF&x9SkHp5g_}PP<?jM31%qnT1UR>9$~@tc9RxX_)3R%LcQUky^>os!_vpZ*bhWj23l8{8Wu9>RgK! z_vQq&ADpOm4_(Ayc);Q46ZTnMm(UDXIplCOd6a?<=lmp=f)wgtnhnyWu!vGug%;qN zSq|a1Gj_A-dU}gHBC}K>xyLZXG&gmt4OpwIL7gSZ5s#RqdC-PF*}7C2GR5AxsUcbR zU*YJ0%*#rNmP8~>+)+D!HHkvAc)u|PRQPittr^N+tF=1Sl&k%+9NAvLphMv;(vZDO zl%ZI)KpI1@;}#EHaF~M?E2r7*fX9FFx7+z|;?mLB^od*9@56e_?aO=9D~KU7D7;4~Hx@i(J<5S)S?ArdRFn{}X7p z(|!npY->Z9JHeYsMohd1#7bx5TDuS`{A;TDy>tZ!JcQHDP|5ACDTU&!Zb$Ks`b1e} z>7&Z;cIh3~mBT_2<>RcYo8yS#2Fs-y&k!c->S(oTJ&okBp#cI;keV&X1BhRx-d8kT)a~0`s|RGf<1weOwohB;%&5Kf%{C{ULoQBvu(}c9YR9Mx z7yU%+v%bQxW;ut`tCBYj6Fv|PkS|f=I1vohqL{ieQbR+cY$)v4@R$otG(%|Qth#0w z@fK!u%SHc*T($$h!+>j2-W!XIkQV2Eu^(bLol|PV)4kQUtlIc!X9LbK1YSPi z26d0aEC?l^gA_1Wk(V$8c~dKD`=LW(iTU=gC4U2U*9Fv+K0mm`XCOIfAKKU6mJs`p z?!$rc1EgAGp>eS%IqVS?W!@$1qqJ?D7OrqZv$#x#ab*Li-&oA^rl3|2jp-Z4`ze$} zUzK%Lu@Qdu-3V7`D{2aDPk9t_S?w@cGTp7;rIe&)Et8aX%2pAT0!1YCG22wxgc_vE zu6`=mVA}px0t*JnVGP*#lrPoYN`b~Qp6Zj-YxF%UZO<Uu6iHT zi#@2HS?VX?!9u2IWOy)9X(!_}j{mXQQbkmh464fb3B)>A;7%uSF2$?FI552j`WUj{ zGs2jTIP5aVBq075rV@Z5ik34!Ut?#Eg$#g1oSdW z1m@<>6pe5mi_ojbK1@0)o^TBimcxuH= zj=}y)Hr(w1Yn&QryZYr@0LsmU-(Y3m&{P8Gbi6!saD22HBVj>S00H)LHQ4jpSi z{PIS1vU442GAz|r;awGo=$|7_fE0dpYl`N-4{Ux;uUFMe4g#{FemAD1I5@a-l^d!J zD*F-5Kab$Lu8okXw2kS#wE=!39e$DT)uT^qC*EQpEXOm)`MDZC7JVKSNmd(!%xR%^ zSBGKq**&V-)uLDBF#^1E=E3RL!!3e!#XEK3a{l?Yb3BR2neS{uD-?5QEeHyt)+{+o z`d(24zQM8nU)9|jLrMO@Zde1brV#$Dv#UY#LeNmg;dOHh8rV1JGGfWnKRN|uVu?O zi78B0MtV5eH?|BzVl$}TdDrC!SHi88l6ib$v^YQ_gmJ#OD(#xZ~gLUPI__(eu|C33oU zkjq|M=ccxOq&P_1RT=z^Ho(3ofE|YvA1k!E@5)J@_bR0l6JO4w=9<0`WsHtKd|17y z$bx%*i#Soci%x!ktbxmY*8U;*HQ28;n`Ol9v9vAV6WV>?o7OW7U*m{FLgE|b1k9SX zSX^zcmzVWRFH$$SO1q|~5GgC_HejzjG#=?&Xpj)MR{R`cEv#CuWmoD(^$H0ExYZBc zZP=|va3!}Ny{4WhQp|>ZU(zJphY@{ytM1S8m4#d6ejDCa)NsR3;Y;seXEw>d(t1yhUO=o`|gp1sj=t&A;xM{FwbTI{%r;mMFv8ZI|NQ1y5_)>Ot-A(4@TIlwKj= zK;*GzY7bX3g0aPk#NBKrDOZ*;)tY9(*7VF|s>tDPf8c(EFDI8b5#t`~_dzdD@|e>f zOJ<*>o95rN9|D#F0atwaT$if6?~iX@2t_D@+UqFGEK?H8vVUt?2z5)*=bmQL;Dj!J zC>_0HH8jLf4DRh|T)L`QM{`%us$T%_HSA2g2s2X#YRG)u}cSo*-95JN~kyZS~oQNSO z<2W0ZVqXaW3}KK#862VG{YHNA30X*0()ALwnZm=(vL5z)4U|5HfrI>9`ecTTvSaU6 zp@Li_%-uiiDUB`-m?w`uXNcXG30HkW-~;DNY|Y*h91OHa{HjPx{}Z=yLvQjS~0E$4C14U}$h`Cu}DjU&K zj^tB38q`NklDh2KWz3v~m-*yemYR}I&cKSm#7q&Nr zC#SZrh9JHcVk{Qlx#>x*Nn(j%)PQ&pwUXc(HkJk)$H}+({+WY&Q?5tA(PmZ0O8t1M zv%KfhK)c=PdvvnxS6(OAAmHH5GrviRty)&=i+J&0z@EJmsdKRHm9h^?=?A$NA&6|` z|I;cOZ(*-NajD9k+7;&4GmNmE0G_eho7gTTRJK$98Hp)$tR=Mr@fFChkk7_x^?;-b z&&ZRnM>wPND?RaxZqbevN zTXmPBQ}{b$BBd#5z-xfeB$Q%Gb(W%H+i)$fN5~J)Y=lJL1ofSmbjFx7s+7V#LA?T@ zee4VUiP=|?20{Yb48t9ojoY->_Lr~DOjwywWGGZiM!7!n%$$L2w4ryoCund$FFbt!jZ>vm>(@-xRF#)3iWe?$xJQJ>`e>g=9_CQHHc33|s>lRm_W-cY z)t%J>wz|I1r>rIPTu*@K&@~rkJ;)k|b~z`!T!EpAoyvK#1S-AvHT<|urmNLczI16~ zw>8hUY?v|eai z`DDuc_z=MNQQA~IXTHJ4(VoB$1>nBMw7m3Md^C3T4*{5Y=Sy#r_JVvzQ{`69Eble~dKHf6ikj)(AHP36xyP4Xw$QN$T_#%FdiMyzp9ZOt34> zhrcw_mmgsh)NOv8K4zaFyuyQ6~ptu5wH2X*vkw}p2J zja6wTD(z0&VvP~PY&6Z2>sBNY*$-(<2X({+Ql|mJk?6Esbp8Jjx=&0gG8U3ep7X4@ zpIw!ZJS$~Bzt*i;tmED${y|$XR`TZMMVX$!(Xw&EN5A8JOsN5WUeH=x_+j#(_7k!z z@3t~u6K@y@D%eP8`$QrxYeF%t8(rjY8n zMbytBNV|PLw7))^{PN5)GyI0($wA zb7EPet;=&ACYw{=*vAax6NXD|M?KP}8?9B4V=4uW4McwJ)9vmEH91b$b78-=<>8?b z?bYoGyAy=VZ!uPLmu9?}j0Fr@coSF1d3-*1bGt9WQ(wMBm~|6-P|-6TS7tgBUgU!M zdhe6iw)IvpHns6;9tLOB`5wWIb5_o7-$i$+b8YKW<=?a#JEN$K>j(JBELEapt8cGL zNPOVtxz($&)PA_BdC%$LS(?DfLLcZ6cthO_$*F7WxZ<+@YUA6L54)W-GmXi94U*U! zy`{1@{q|=S%E(>DQta&dX5PYuvP`vSk?!!ve%yXIwjRg-tT;O4(CCA+KCy^%n;}2SBSy?W z+b&7o>-M6~U#srs^!zrPlQjB1FedWBc`g1Q!kSV|22Po=>fe0uyP7Dx|9S4FP|8$% zUKtD&+8Z`XD^X6s1d~^4g+z^G$=+7!^$YmrF7|slydK8mOnG{8@2ebbnoQItH&F5- z1Ptwb^vwA`jQv$qTVLF^jY5G^+>5&v0u*lUqhwP3oIzH{Xtx@fSal05mk63t=sRj;43cZL=p8-cB?JNLg|q2I zoSCzXe$&PnKGiM=r4C|U5}IY5*$%}bUgS#4o*g-s5b-9;f`j9vx3xJjl@U+WR9VLQ zd2JY9pJfKWi6ges)Ph2}@DlE10vZtkZr6)bx$WpqOJL|x;=WN(!UKFVXYtR=j@LhK$tn>`|hRh(wZkzvAo;VU3*YStwM-4d(*(*l3bF6&HbO*b30EpBDQ8#<#nQE ziv9E{ihA>kTbk`11y6|KO0t)>Z|Wasl3K?4!E<`9Q<%{Q!Oia|o^-V-8F8Hg9S2)0 z(Mb?)%8Z?j8Nt=NLx&;ok3((=61Z`_F`UoXNnc1^Pvy&Zz!Tmqya~c^L<>|*{4Xo= zYx!7y>7RO9hCj%F3>g=^-(Ob2*+m(mAOFOU(pm;Rf~5X8(xBk{`bV&ktr{XNcGcUE*(9(?k~r8$`j!#~P)6 zAH2fooDJ+oIgjL|h+NNu*YxI%nIv==QsC!Z#jYXI8U8vKZT=h6oD;HTiJ!#FU-B<=U!0T9xA#PBGdk z0;<(4KllSWNd1z{>!cBa{s4t5nxz{#vt&Q$o?D)l7M!)h zcwrau-6o_gQsawXrwU*h>C#lKo8P3d_KxQMvfE8Jd6Yl}^IS?S3r z;gJ+%4g2t^TGEE*Ob%1K&010T$OZiyx)!T4A*?u-dTv>QFo(v@&LbV45&}C)q>TqZ zaxLXqtm+OW-`3DZ8GoMf#dFOxcW~#kbQ{Bv@@M7WZ&i&3<~~6da0Fy!Dyw%0D)b`xE)L>#I6pfB}ndk!6kvP%R3z@Hcfk#Q?;Y#horg2gXzmZNTG>k@Q)NU`* z8z)5=3k3Xym3tE>D}Y>rO(cu^ZSCY0LE9Lq+G<&SL>&(=# z|AH%vxy>?V7nP;P4%E=Fr{C9?G%1=w=Re%8$Atp0WBzfdK+dWH}ty?nU+MWQ-5Mb@TBoGmg$ zJA{V2o~neG3%;7kKUS9<$~h}?2bTeCIMBpxB#a`@8LFbg6Z{SemcV$) zyZ>mK7Am$_1O9)StJM&6H{H&;7m2_>07GU<1YA|o|Gg~#g?+wWsUg@S<_i|NhSSr` z+&2gu1k_%hJ~FgG0~64BS=e_86le9sM1c3)=&KnEthQLROQAjfN}F4Wr}Uz80ohKL zMR&5@-(s$hO67ztg@R={E1W;N@<-Rz_Q+_DoVV1OFG-IG`aNEY7E8eqCqwq%cH;9Y z3HvKfGZbCzrc;o(6Ii`Jr&_nVT~l;Hik_G0)Feo>SSp!|nyi&_TRZx1rncv%#lL;YsC(|A-6kp7|sdg zg2o3)7Y6g@~0Nw*wuvzOlx4=ytQ-T~DL7A30js505*(vX2OSt_Fc==}=QGmvn0q^3+f z-Y)DBVrYKP(6+fqHR(1SI%5RQKsr!39L8`nHj4uR)7uWyUmPzBzq_vb$jS@a9gT=^$_bt|c_SjXwhp`jMwQ2D9T<`it|T%1L(aN3pw z3lO!(YlnGla&*nkXOY<)*c}zs*5dCct zI%;NtHBxjN{On@oig2pW3{}ncpae1c%D#!X6(Vt&xwW>@d|fT$huL z#xzBZc=c*xuQa%&TOk1VlZJ)4(;LxAM9u0;am?3e)1%x*JhhKgDAhaYY}-R_CZANB zn^V#Dz15>7-kIzwyI0bM^@qblmTCOsrep%+(eODkjdj4y&@!A$(;0w0EFpgt-9tSMA!RtjMYEK^peeOxrkU0qI=r8qK>!CQN7!wK3v3A6na zt4IFb6EEJJeo@AymP2-RbXh@o=b>5IWQ?~1Lq-@HVfzCd?nE9V-i2a^GhVf(2!p{& zvbLp?7Kf4+zHxy1PL<4ay#Bv|{d`9OL3Jr*h5c#kL@c`Ll84m~~eAQtDrsn?FyKm(=)j{q*$o)gVgSO#C@#+&+Dr+gIdj5;BWE@%W)?2ZqN~`*ayUJi&w| zAz9neG?P7V`+fT}pN<;rnRK9=sLZK80%i8kiFD0um#r1O4T49L&`;hE#dHdB9TzA& zq?5`uD^^jLJL{z_VOK0p+V<`n$)O7EuM(Krq=)Z6j z1ee_JOZTU3c;aF>0#bH?Rk>zMway3@!A{w^pn+#mAXm3FQd$KSaW z-BiEu=!I?KzAtPYfNi~mpFRNbsABNekt)IHK6~Tf-uwCRWB;E}a5(J$|HuD-7J&b! zyLVG-d+cQp5CRXWZtvX>mDI75-bP@T|Qw54Y5W_SIDvpZG&QG6+*jS2ep5Of6>K>x03*SxjI%zIK zG|ZRK$ImkiaAqtQYJ4ya1*!9{Y$qiFY|oALTdza~XI@92DI%R~7N4E_F(^*x&lM@` zGPDa{#W}-oLU=TuQY@xZ8MZ26&_5m`+|ApQCSjAO>=^;d7b$WJRd6OJ^o<~@R`%4T z79fqOR{wO?xNy4Gr5ejJk_hV*Pq{%p$m~l}BfAJ6X>Uz<&!CC-NrE<|BHAa5z&5F; z!4<4D)y|weoe>TN(CgT<=mVY{ua*vv)g*D_&n1nnU?dnFVz=aajU^pVnt>SqCT_^k zv7=lyQ;5J^D;1)}?_5Z6XNOJ>pYDfoakjrA{|9CxQ`A2mlzgx(!j$!byh{jM=f@ql z7iT60ft9HzW5IGJ!m#RKH_3CH2e*Kxq4+Z>hamOPf0xHE#*{yhNgf9FQ!zOYShd@t zz4f;3LUnfegK<_->3IJ)6V?p@ak=yEMz**A;d}XJ{4kCG<4SbOj6L^rSpr}GI=-a( z+VV-|g6dqF%?Po=f%@>}q1JPhnS_DEEx5 zl*#Y5=$0?O#m!{Wchsl~;?V3<4!yKQY8f5@dRL^T!|GTNz z?8wW0Aw`{DGf%TH(>`^(BwwFZYKICMl>m>(Ad&7XL+YQKW9Ak37!1b}%2^pPk6%Xt zg{?|RlDw(~r7QJD@-)tfiD-m&K|cF6bmjI{4l?A-m|Il>?d6(FNBIY`nrZS08hJ4O zjOL?}TU&LZ&IY4W1hA7Uk}FUHqjm&15Q3@V9 zS9Lva=XQ$FQ*C2~``7PSk;Z0QxE>&#H--8k7p%mUo;&3}>jXqNNNdS2n`q*ow{J}) zInwq#(Z=b>HKt7$mj&_{14kRB?yOBVcBVIXTf0%1#p#3W0F8*!0b!(#gY z@L(D$VxV3Z7i8!pD5uamSuTIJ;AFT_f7r8qdb^f;$E`DMOR?Lk+*MnI&p)%ue$s@GrkG(#CXo-N*i zNHRODO$}}AMTFmUib#Ko#G5?VBM~RAWX_~6LDa}wxUmtKoEgaD|$Co z9tWKC=~^U5rpPd+lo16Zo~uqUEOoh)3ioj58by`2-)(;teg~2H^|d}vs4&rnH|4^` zBwX%bZo-!7(xP?a>AlEm%I_EQmczW#7^V5AP`I`=uRKcZlq8 z)pm%z46-cQKVcl`D1Oae`FNc7W-n#>lB?i9qlmHI`pzqB-N~{(`0%2M7I3R&-YLt& zrb6JW+5LqU=w+cm5H)vTaA9j(r`6Y+hB=8f+yO&Yxl(PQFUGt&1A9w7Hbd^y?vo5U zNBDzy`JU)gZsyME(j)2sS0yx}*TkH`1pxP@08mKk*s4r-=NBOde^*ud2lUogQMFf6 z0JSpi1nnbHC&W4lOHC7S{?)9ga=QQ@4aIakmuW}bNmJ&dF49R~Gf1_<40T0Ba?~F z!Itl`H48JWj>AvJp@{o8#`t@hiQcSlALNhW)=AmLPSAWi(OVD?yns*oL$#mnW&79U zjna!4yl=i(SGroM-cLzl!kQ>BWqBb&0N|(+s7hx||5I2!Vf2NfZa3z7{%`$G%DYQ4 z!Fot{ZUvM|IDK3#k#7;qxTW@c7CFZNtETW2{8$A2wDagcuzCIHtcttx7G!O)*Y~)1 z)2yToVcHDgmWhX?=r|R3Y2#HxeVZj7O0pOF3#{o}j$~jvI znRvDunv&P?PLJFe$c@M>sSb$=Lef+(%Dt+6zuyHQ2DgVg3+Aj&Wc^gH01lXzFAOB1 zJ8v`}E}L^-(OuH;az(rQttZ1_L&*HACvaEoNmNNB38si9ZHVaH_+7o7>?0{eUPgs< zeAKpQJ+n$)V}xBbN`HxC@4yba3qRl;4Z&j$x>5($aizp^b;vtGW@{%l=M#=&vv$@f z)ljCLM9agqAb4m~o-Z1zN)B8I+mqjw%Y{WdU1Vb_*9R^%wM=iB{NHWMmsc!OZBBS` zGj2IBr=EgJ*gj9Sr)2i(qzZ8Fz1!+gA)kI_M>@e1U$3yqFKn>gU0^m(c_e4^avi~0 zz{7LxSon)dNxut$iWnu(cC+)O2?pSL#cC!T>!s1sdqOAtIAi#iX=J4a zFAr29#|eME)p^)j?xB)ZnBIzp!^;d^GzfHrI$31wtLkW3u{%rcHWZBr zLM=0hxgcUyLch{$@NU~)s-o1n_0>3DocW>B4G_`)#cw(8LQ)WPr^HPqm9w=u#%o1^ z{dvo?Qn8vM%YFmtw`d)&dG6m5{rKjl*W$p4l)Y!H*u-xk{3!u{Kx!QJiA#>*(+-B$ z8V5i(u2_T`k*tP~@{fN;52cJ8iTt>CCe#8=yQf;rZZoPyHpn&N+k}~fcs6UfB;k+o z00U-lH^Kd_r?=_@{rszD+v3&ziq2jzRXYr;*i-b=V|@)|P6=#k*0t*-MeOt&T9%Wr zv2#((gKg+&a6u||Kf}eXP~u$yvZ}V#`#YJOQsfu(z6?tI41Ii$Zi&ngndBP|+XD(! z>RWAqE-2-aS1>X{b(a6Q&5tPly%ROXoUJ<%Zqo_0ELCSUUU1S5#aUKN|8Gog|MMBV z+c(cuXuZKt743-S+!JXc$Z zD!j3cFKc06Q3yP)lmyyGUY2x&C4+~yWu?!UChYHJM9jumPg9OBS~;X5UN6T$r7&-V>*-8W3T zHrTX_&qXJsHC_L@HNDq3cPS3<Z)j8alik9|wWu%Wt|1!UeV2Ee(ia1ubC z>nsqGP>wm?eX2{DZfXJ+sCTe(Iz)}|Ja3wIR)S7Ao-EuqV zV@cYXEWN6y9j%S?HBQJj{Nb?OmFW^AV{YK#I2K{XpX2X~e_^339bx8E)(KaBjjh(* z>9wrgZx24^LP+&_X0%BlRrO9?>}%g(43YvMkg*tk=1J1!Y13+f*pb#t>Y)y|Lq~Xuk7k14IGetFrWzr>qJz+>jN#+L zVlAtT`CD3Pl4zi+LE6U5a8OGhw$ic-Adzw1d^?>q9LBSaxozn0b=){HANjBl)S*8( zLn9V5PE5VZm0SXxKv?qi_A?IM(u{|~AD6=Du-F^n!A|FKaH)xZm-psML6aI!yWM3s`&Vo9 z@v*DA5WfY(y4h!rc8B4!Kl9d>;RqB~&>h4%*G<|o*icv$V?!N6FMBKVQEx^eQg)Ic zq)uz*J>A6<2X>Z8z$cg|)|`N)%n^MV)6t;CD_nLBLN=gJ!wYhgr6gF|*jFgi#_b># zOV|h@hO_(2uUsI9ZDL=NP(7!u$k&F}oRbo2n9CDL!wOqQFMOc zhaRjhZ-s5eLKl}C?RJSK3N%6VI}ziIjH)%`5@;T=t<6?=m2>Cm-;}Xc_hn;iwfLX* zK9!y|pwxhIRKq^Lw>B4_MP}zyzAiP{<(^G@qN7l!Eam}e^_X^;)E7I;F8NKMGWHrL z(-i4EVM3t#sgad?@!5)Y1JM1JJK(3dkfce(h*a0%#*ZP)mwelQ5nL+BR=B^eQzZX` z5j;=(nCy+0vJ})%M4R>_MDzScTgv@7{Z~55uI%+S^SUzLXCmOh>fC^@H<>rB0)aj+ z)qfr4%{E`es&>g}?fwhbu#gPyv!4W!TN2S-HH#<#Hu-D{kMt{~a5j?)!@-B90055W zIL0Vf{|_eYY)P0K>k<&=H9yYXIbxUqZw@PWoFMu*=7<ewW{P!^G5~Jzik|6tRt}DOeue9CK;>o%*>> zk8^S?5PEdqnd>ZbQb|`o{AOxU&6Vlsic1}EJO{}z6g6}{zQ8L_&Yv^+=LIuQKQQ;A z9{7jB(B7FGMU;%7bLi?n7zKyGJtV_k)!Z#8^A7*ND^B8mKk-1??42X~X`w7t_(`XPsZ;y&H^5WJ#(& z2dL%C)EV#@P8!{aFK0|mcS*rfo6y|4YP;o=y{aVlO`-6Pv9tEqpD0|H=huv;>P5!{ zqL!v=K$9kTdZ*XQm7Ag~c8J?!dJry+d*-nWdc>NtE`tJf`Aqe3iDU!t@R7N&2~E=h z*Ya4!lt0x3N4oS;yjJD_&rANw=4UCGYrQT$KD=3XKubpa^-;Eq>%3;GjcZ5s<{u#B z{wma^yw1f|yO`Gb$&{;|d;kb9y8m%BL`y4xBylTqgHOcvvVYqjbz3W)Iz(Ab$6_Y@YO``AwdTxu29V2;bh5YfiTDH3hW^0 zuA4hq6gfON=9wZsT>ugEZAHX_j7`}yXGp{84qk_MPvWZ)8Q8(!pY$8bV@jU8xvc&ZPUSV&}S9BlG z`ZFh>l%3l^5$?l3zw5|)9-R1yM6t*U!bT8z;+ z>j(;0>}-swn^p5J-#NKrig>LmY{bVMZScUs^?KW$v9lSzv5Mpi`%A`6YQ{A9say{| zp16C1bPXhE0CX}0FGLW?!yhw!U!Vs9%yVZ0n|xZr9@{K2h}Txc)XF;VC=vHPBJ9;( zS-x|5q^~)QTHayzoR$4+r8iR@1X%U`hbQ|DCZeErzReD8SChA%Nf(kAABfLSy3$S~ zW6neBtISKxq>ii3iogs&HIAD5qPgm*YE19&v0d2_K*}ZW!RuDVXR}O$%yegwJ$z z^1Q9+l%J71W9S*&r2V4}wnK?#u}hEG8qJMGhqn7pxN-}xV)MLkf>0zTo?QzE^Ihw< zC$n>&W)lBtpdufCzYLgTmvInFUUCri2U30Pd!1M>Ooj~zUb|`&Y0nq9f;zfeq{Mli z6V{)loE3Z_7VOd#h!u~dfZR|W-Ev7OKFtBZA{X8T2vWL5oU}5QgnL)$;}F$3(^56V zHPv*xA~s;Ve0?pJ;V;(~;@Dp+;*1aEM_E%|Hux_%t&+Dc})L%nhwr=%SM&-Evv zo{?IOtj@$L=<0FC3iU??{a;)M`hK^R~J@RcfX!n!IYxhiXN=!s(0MiuF76M)dt8+IpczFZ6--Tu-r1rium+UX=2dAQu4-{6oD91 z$+bd0>-qlsd`87H!?T|NcV1PAMs2N##UMj7EhOlZPmUNNL-OeEN-a`yhs^kxPr7_? z+(XH4=R0{SDO6*C2H=|o?X#<*%Te1&Idzaa%2U-m3ysYkp5*f?(&_N!cW)<6;|vAPxzPT#6e7&a5w%N#-Zc!eA20Qq#2w&X0QsYA^+E z2&~x@Sw+s5^}$=8d4o0t#>5>fFkQ&OUPsQLM$V3SeI>>6KQO}$U((fG#N!;ul6TnZ zTr?H#dgLogeqiXE@r&lh`cDjgh!L#gUTscWMFIJW^g0b4#B%40(2SEad+wtZqw$%$c*OLq|Nyb zEBG(tc11aDD}y}%GitLm$sIRaHh-s~N>AKqT>d{ewp5oQ4|d$f9wX*5s?_xG6e4V- zXCt#ET>&MJb9cjQ1NZbG{k{YU?+;d!VB+A1lEF?HTZFnCat3jF1ucrfNrU=>0*yzI zzRrQHWpwUUTud1^;+9VnU!_UfrRG;pQn22e2aW_$)!3w=+Cm0&2ji0$MlC zVmOSPK?mW5;uju=x!;Pj3=%4z3W(C!aPz&(KLyc79kt{Ut+yKXJD38AXlM?#Ky9M9 zuw~p;gOvx;vpyk`Vtpp_&Pw>$4Ym<#Bnx|A1u^7ona~JL$Ed+<_43^0!3rEdDy<1; zB$yG?5|ET4^E4dRr~!BAVd{`uT+?tFP9*NWYgj$nFU+2}rEd1t8*eM@OdO=++elCh zHbn1Vq}*-2#WuIFY@}ubRkFO)G~y25i&jB&{gzG2HzqyGm7Y8$tm;=iACk-5A6>m| zppu`&@}qWSrl~C$H#OpiDLhpwYH6gw^QqF4=uhUO2L^-K5|hcdDf3ljQPi>sRQ0~ zsmpo(MmAAZ-aVJj)g05OV||%rm8~2@pDtvp;$dF5>GKOq%#30ADjDlc6Ej5;#(HTx z7#n6>bK3(l*h zt2u9&X^MN@qoiPb@@I7Z`x2ke;G}ko$tKYT0Mbhr2jiRw*m1-;e?S^`=Npp z_py&yTbO4i`8zybilqV%0>+pSuHj!?y55wD1rMO{PS`-&N15_iH!2Vvuhtn~jpcP{ zyXC$Q5#g@sLA`g8WJTVOztm^ju5xvzh@LlhnkZG&^f~!usQ8|hxJV_UviAKs-bq0y zWa%26-aDU&|YOD$B0PY83yBt!l6U#S2iR$x^+xm-)=-$rWl-%5;iF5tF;CV*p?#TG1Yb-i&{#)6S= zFtIVN>ba3sYg)mHxoJr((2u$;ML$gC$n_F1e7R*CK;6-4wTqFRm|Fl`u}DC&bHJ+? z%Puw!6}mCPun?NYfAR=MA;@bX_gRUvug3A@?_87jVKy;N-g9u2`|J1Zho@a!x0axt z zOxcb%O$3dRtY~FdXT)fpQ%+h}SX zp%?R^M74A-iU5gkEeK0gjqo=!S{a%l+a_)4Dh0)xgyoak>|%Ja`Va6Yji;(|YNOj( z^^bmWJ{lSAPhVT2qIHMdH(cAQjLud{-XT92Wsa1uUzf#MUZThE8ZgBN_ck-8TVp&X{QJy3IVx~ksR$2*Gq1f(QnzF&g%|^+bHddy+vja;2SrfBkuI0+aG@i~T9_}*+ z%RSOxONqsZVO#x{2kLkp8^dGN#{Qc!P*Fom#HYq*T8OxHVc>I8WR^zN-4#~! z$pMf$X%xFz*c#cC?P)ILIFPWjt|PxaE4)Da5qKB0f@6)EO} z#j#zS6_O)|A)%JxtW@g2j>pA4&2U|?=wI3w z{SdZMqaKw8vCAZp9yJDlc^9t)n`fum=iRF^rdv!jV}0l6!=r~rH4{Yn9&7JTS3mQX zNxg5PCNT>5Dum*OqG_7}(^MUPu_7jxOEBZwH&{iuZ^VHJpLL!FdnWm8)1v4{IARBD?V?RWJxi>mf?Uyl409jHwb zzXhVXHk~$a>y*kRM|uoC$G6*?Pm~!lpGz?P7BVDeM3AQHc;KIr*MeL<4SmBhz`A{1 zNl3TC%P1(>I5pqu%=mhk`WF~uU1xuxQ;g4p>?9^+%e~ZKmH3zYSBKc$U&M?zTY5A% zPHD0IUj84Q?LcgFQE2H8Q22<5Eh$Hh0!+A{n{kgv$ zpC0$Ey9~M>^^^IV&4^2F%JO8};^qKTm<8S}oDUSFy{g!{8KDv(SUVr6vus=*}QJ53&oKK>#MdY0b+AtxDrMBfczg1JpW{RKF;{Cz4p(34CI7(F;NAC_;TH*C5a*c! zkf>sVil_gmOc!DCAcFLl@~CmO;fWTy7wubvRN=4@T%WKxE1ft0X|*-c0y+tC84vF? zY17L_Mfb})i4(P}2sE~sq|A+nL*o|Bc8R~f4(;T9Hh@#RMxR{@Hz}|&7B7N%;GCK1 zLA|e@3?ndl_$=Zr`WH1jRUnbe0*~A5q2~5xhKj>9OD8>~ikFWI$#8ern#mJ#9MqWA ze=gsOl_Ty|iP0^J^-1#|VI@9&6>exWgU(4g1@Z0Uxs+2!MUbajjxnlykPt%zhZ0Q# zRgO6)LbyS_>->!jew+hm?!>tq8bxD0&o}tHbEP}>VC0a}q6)VC+^!__aZL*J-cvJL zQbQ#IH!ecHN;t_NFzt}M1GbW`l4n6hnZLMnkl7J+F`mrfHA5xereshbi;=c|LN0&s zPEmGTGoh&n!}*2E=rOlI--)xFK5ULim#+#O!Q}yGjYo>FkJ_F+VrSV1D{+F)ylkau zQVfhWp&G+!nvkn_j1w-5RgO~y85%DUj2a*G93FPkt6Bq2#R`uK45Js_7R30*UMQw( zy|p}E7-Nk_lh;0x^vz4>_UCO?QS2;*+Y?BpN66KQ12vm|B_cFyFnw&gyi@((QjTr! zXsshW!&DMdKspqn`w~&=^jkDu$K7lv@)|#iaA?RC`{IvS-3EiqpB}NjS3)LCojJlD zlS zjCHN}8XY_f$f*DNaXWvKk1$iLJRI=Zo?4kdB8MguSjbQP1}!xG?Lj*P)R%w2Mb+>BVO81^+z^##`_1IYTu4C zETYABwi0a@75T+TTsnsX$Ij(()g%~;$@$|ZchaqAK@M{O6GT{l+h^>p*^9q4tkj6< z;_aDFVpnR_A-CA6*v)WH(sGqnxCytCSOU^~RgPBQ@Kj7yS9e;Q@zxvObAiD}^;ZTe zD_W;XD&I-Zv8S}X3_?6Y&-I}#9)=V+Ed0O`i?I>&FlGn&}Nu{~C;3bB79nxDz0mzHS6d#G$3 zvXR9{TYJ^Y%c$^-S-e0VE~o*x-}^Fu`$2@m0nIz>=Y{t8S5HXe^jM@D)CXLX zATRoJIpctix-UtpgrJ&%s5V)=)~Ag5k?-3oSu@&8_vCaEY>R}YKc9g(apZ%=ntkDn zmLBei!mfUYtACpvzKs!z1eq|_A2C{@gUbCgzD0lLGu{6oBukdQe5%)*U&e30Jt=1e00*7^ zTNUy4MmXB&-JX5#2|*0>(J4te>N!plvwstaSCpTn{qA&M*`1(Y%;dxg>9Rn+|7m8> zSGh0W^eyC!TT_an%cDl?rSx>NdDF-msK-?fN>5PS&sZo$w`7E8EZrY> zMoxBg5);#IT?Hk9ms{RyR}dH<307OW z0$ZyxW7Ap}9t-OXAnv59&O0#QS3L-D$H+(SyIy#y772EU3_cS$;O5Hy<_V7 z@C$APH#f&AT%XM5xR@4qs(yYeQ%fE!d3ec@U}_B4{{wZ?IYAlKQ@K~dPHJuW>xb5_ z*t9c~GTS`Wzim-^*{9j1YOLY{r(|Aoj*dF*vO_0}bB|#=;7+xF_kCTp&R7+8LxnZ2 zB&c#b&L7S3!4I3`>UkVQtH#`j{8x)^O%ow=VBIb=6eP{HILDIzIAuefX)VZ~I&165 z&s#s$lQfYEM(6ufE!V&A!dz}xmmA-meg;!@vRDZtP?>>o?TNQ>PB*A;{oA)fw(H}f z$pCyeM6%YNeQ2_sV7n}zv>d}GiI-RWZ1%oVmRCWhJ<7l5QJyyAOChPy-dKzMXSJ<9 zxyKhGHswCzI8)614P|&O8rkKfb|vk_Vp6~C=zuY>Q*T9>gu^D$`4dt;I`=_2|EA~? zktQM^sgxI9XhErJ=cFt#Qb;!mS-20fs7DInKhpD-j_ZkZ+H@LSIgD&fEe(G0@w#0L z3VM~97+bR=b}SW&3~@cdYn*zU6kZ$ls{TN?m4@|u2#ES?^wNQYP0B=te_k?iYiaT4 zR=s3q!gMeSHub{zKUa|SuGl#~HxTFE>#-lpQXF8(%OQj|OTHCVew zS+xb9YLj<|^X>0si;E&SnwoRh{QtU-fsMeOZB3gZ8I}(RMdj_Q%=>LAT&=I$%09Ch zFPgj|TpL>j{Jt8Tdi-rk1{;Ep+OEw9Ng|OOEe9P~dbw@|Y}77#KYNtH#`?PkdOiDe z0MycBm)my&)Y zXGmwuz4O&B7@m&8k*}{ey-D=aq6o<;w$f$SZ4F=a9>6?2R9|t;ft_jU&gadlYP8zE zdU~QA4N%Z8EgvnKfw84|HNxUTd=_Z2s+!|+dZL?ob*j|!q4ih)l=XGr%!)@3@qz#F z>6gaAQY(uJ9br-|no)vB-f!o{*CU{?0H%d7t-XuBP~bt|&5R|a)1dA}F6J93F693p z`2TPB9*g;+eF>u(1Kf-*hAS~uZ9Y~A&Tyez#LcnwWOpO)g<+ltKf@pIx*0zmy$u=B z3F^=v(zM0<8-G)n4*-)2?xDvQb}-yND}B98Mt1pAWB~Y+@%1;&T6Ux?Oc^e*GANIL z8$M7)YoO5-)nSsV9B>6F3q@ZbYHi3Uxb*FuSj<&geERVUyz2k2??S*X|Pp1 zMAeYH=teB(&4Dp+5QOX@TMN$X1>q&)4vuE>afYdUa^Pa5YWCZ<xO|TT?*BTU@^dAfw1uB0`7IxWw9_zOCCp;%~ z8_)R7?xlF5$FkeZl^_C9zlAtVX@{lvU>`LsQM5DJhe{X*hQT1TkQ}WF)npm1Aj(rY zGyuBwry$HYNN@}qE{r+E9I_v$^2xI@RPjm)7nafdP77-dxvm+3pjt~+D`<1^a$5ZrYTcBRTB_A^eiq$^V`jWFf>U4+%c+LMT^H)Ti4tRe;5|m zFnYo1V$}yX2oq`niXDF#(50O=d00-lN@Se?JHx+?-OE6i$`z^F3qMC#C5rE|MeuJ zEmT}KjoC-gC=k|@Jlh%V{I{(cR?jVkcnJ+DJtS5tQ|pHB=8-fBr_;2ToLTQHKX^Y2 zCHgI^83b#|`4RzKDTWh6kFkk&vTm~TF$+*&gH;HZ8?KcvOMk9_y!=15tBM4hk}&)F zS0I^eaoP-<`Dku4@P>?-lfQ(4QxCC2&Q-PI&W)u18tne1R*O}5X>DxF=w7Or_y;v0 zg$pmT>AdD0hYgfK8#G2QOIl6RhA`9E;K@R2C(P%&;7KKAQ4pXtNIqIsGr1XEgMGMp z4Et?G$zM=o5(>wEp>{zkK<`pvVZ)SW1nwPqP4EVTjEpG(>5n}_S;Oq#_N=%D6orGvYB>V+3 zuJN7sH4>`5?Mc$TS+E7(6CgBcncf(c66}kwLC&#W&+UI)sdT2*2r3!xxg8-pV zkzj?>EoXaDuGOg4s<8HmHE{@LUF@6mN@u>6UaESW{^nq#ROQtP$jUau)5Da$Q<{PP zn|=|V(J_+!Z(ZSg7mUo!ji|+F2c-d&IXF&eS^3kZT5N>X`(Y(n_I^z$^=NViMZ3vP z-9Uw*Aq-_(61f`V-_oXp1{ayP!6P;Cs%<64<@ecK(&hn2!8#Z+D+2iCo4wb=<~xg} zi{b++DyX}jF+II4qj|!{lXR&$xnFgJo*lxGzgC*mOZuW?;#!~OUV)Cm}c%}UC{(l4&fU>YfKYe59SSq zfbimiN6TH7O%KdIDC-i2Tk>}#&noW=%4ZKcy(Q9~r0ZQKj!wz&k^^aEidJVTo6-Wx zrE)ac(o_<`2GKV^*dz`&Mi1||-kntnalh>Apb~oUG5KgU4YvNMHRyG(KjTiMOqtPb z$}65Rlif$!;I;>Kb}}k*0@WvI;*t?0*0kJ$WR9Zkr+ptm2Xdq6+GOsEs=v)+G+}O~ z_;52e!WRG(YO|H7`gt+7c1(Wh0{w?M{?SNmHZ(9eci*c(Liu6%0B3L4Hh}hyG;%3p zKt*W;be+fA#5{jW_a)lEwq>1Z8a?svN{%UcbK?~32E45-IeF~2( zvC6?!rU~*cFc2=gGsL!LGn(iH!_q{~-)O53c~oKKj7pJJRA%EAP17`r2G0z3eB9V3 z@PI@1(mvZgP-ykMcqHSP93uyIpxK;qzZiWIgOk~QOhEWiSrK02`At`EQ|8BY#GE@I zX17J&%>0U`*rV{lU$$nWqS~UoVSS4+<1f?zC~^U`PPOl3H{cuB`v0`|RY7faVY^VQ zP^3`2NRi@BaEC&0_u}pl+$mmy776Zz0zrzqyO-cvoC3wYPk!zJ)Pu@ zv8oNk20MV&qUnZ`S|dxRwH5NF)w>duCwX0=Rv6f-_wZN)_2jgHMBx}8S<9EoETSxA z?y+2TJQFnlvh7ZuR$~W6cgD4*9QjW);gT;}FiKBvD(SPn#{^W5e4^}$WDD|8=Kdu# z%6P1q9VXd#9sqxE&A5xd-s%Q4fFbf%dr zg4ktHh@%NP;hzk=;nMGpiPkU(P*L0ydTqp*5jb@4RYMa1t|9egZcnW8Ojt-hv{_ij zR{l!ml$Pjues~2WuUj8G4qXc-Qia*2y;1z3+${g0f@JE~*FxAdbOneRbTPE+jy$De zZtz1W>D9pMYe6jW8@oSI8*qUZh>fq*T7??j(xPZijYkoWZar;$gtd7Bt(9e1kCtk> zhlgih(CP8)ERp-FNy0CMTP5PLgqA}MhEDg#0Vzq1vA;;fcKha5O6aLGAHslGKv|S3 z&tP)9q6>zk$R5`wY5tu*wMK|;&>ej5l zNn9kauShstf zc%D$f%aM{@?Lm92?=R>mw9aw3u(iWN<2=bqEr~2$?6K6*+?jEdHM4X>@ejb#Dnd~> zT;E@W=h=IxOp+BKUCz^jV@2n{b}wXYrjUnQssfZo>2#2;Zz|I$Y@i0-vn`a~xi!Xv z+ykF^mlmdPZ&sdW!z^zMuYTIBY^{EE=;m)0{r12jsfgjx5j_Zkwz777du-+VYPlWY zUa`p87_(~&zWh=q)?l=GN2Kd4=lF}sBEmx+Pj|5+zz)9>u#Gz1KmJn;7TJAaJipd$ z6Q)H0;-EM4b2tdE#8ThOZM8Ym7}4h8B7JT!z}+i%*|nz8k7N|{lQ5kAlHE#gE67JD zt}w_I2lqcEu{h&l(h5yfRBL&QA#|KY5M7`o<0u_5?lglEwni~m|FhZ9xyN&#sauP zerwB{r{_;+5F)jtgGUxnQV*EE$tUt{7<5^xtsj$3T=;f9cX|4D%qWZ{j)~R=XZ6dhNP6JPq)st%rZszO(LKz=vI~=tl@t2J0o9(@EAp?Wc&oT*} zEFHs}A2H5V4pILijmZA~i?k?sJPTuNbwpThGGil%LP)G4rfFv9`{ld^UI}nrrjXC5 zj6}r-CQKomeTL&HpPG1ceg~hFnfX%up8af_FW(fw|09n?(Iv6M-ajgvZ?3)SZ2Pf~ zL%gN(k==F5Ik_(idj}=TAqz)KRmDQPZq{VOjk|$kFK`i%!XFpI$;Z@P{}POzwVof6 zmmeU0lkT&9e=6PD0Aizi3DgujE_v=r^z>;;cQqF_f>R$ZCSi!1-? z3-fcTtdzqN|MIX*<(jOoi`tpeE~*%bc%$*@fu8l}y!NI8@Xz6Jn=Bc?(SrNIK_Uew zf)Cz+S_ibR_8zLbY&k$AvJ}r`D)7=ej+V-Cz=5yggqdNt{yF82l%++}Y?L%^!B$TW z1Hn`=CxJ_RL|+iTcQ7%KwryR*qoe}oK3HmqT$rjz6_E_eD4n_4Wf|luxQ_49ZvV?s zZtq&mwXe1({T15LWV%Z@(O6TmE*U-Kob>fmi&O0r)9i`mms88M5dzuAZ*sT}ekz1B zh0Z0bB>MDo#p@}xk;dWbiOOG2To@p_!{;x!#2Y6KV6yMxoed7+&N26!vvn418<8u+ z-)ZvD?FIuPTLYBm0pl^d&H@3ZEKgy_`>4OMM(0ZB;>N`WVL<+~Osz1nqq>!&racBn zONuxF1`=!_yiG-1?FQequ74seC(OcOPP58~*kLg+L@@SqaBZ`4Pas`xm z-e!+4ZF9brl&<~d=rZ;`nc4G!Ua6S*!3B(Fexl%7a>T#5YotKb*HXDc>~49R7H{<#2AuVwbktq5ze6M5W*S%*Fhe$I zi}0r`Ev=n&q;Z#6kQ015rV2=e^eJq05-z=Zn~~OMJDw*lpo_hMnwS# zs)ENhPG2YW>}#AEE6)sLf7Iw0VoY@@Euhkz$aW5_6V6Mkbd-;lJ|JU{-{rUjV1E`j zUYZog5l4t@R(iV+3J;CrSBm>lOA`ke+--5DY@5#hmwO=_*|5}TEt~p zH64s6NYMpQXf1wdTkbMQ=3Y!Os3TLG&@}#w^ttMl$}5e|CK4Xy8DJhtUSr^Z0seYp zP8|@Nen9K2`wph5;5674`w_zi1znfIrm2CF5g7VPRdXM-2s$*>+*tk~$&wJ-qTVuS z%~Cldi=h5c&iA5A!z~ekY`R~tEqx&qY~gVdLgh7lxe}JPRxZM*qVxJUG@jsvtqcJ& z7hFdH22++=Lt{pDwvuT=8nw41q_8ZPWSRDLqmdmksm-}`&ftkiYHuKN6!UG8@sQ}G z3_hni^0DUb%T~c}G&Bi=WHFDY!ZT$bSAKrS?3im#8(||vaLT6UIoYk{mz9dkALJ=) zH@=-UILm0a2@s&XbPB%6e>XS8;W91hGE{l@+b`GFmBoj#oQzK(x}o8DAzd?hUROv^ zeFq9^`mM8G-ApQL-`CCl#J912Bp@y=V-p+oV3f!IeGZaN?jum$sBhV1YoouT${!9s z;KL(YeoOmps)p;RMCa8y?#I;0$sNVvw0K{lDM#{;_S_}2fxCHhC|`Fz*w-|SxuzEe zuRLlmaPV5Yd~|UiFvFS*=#+;0eOSU)CG~=VejFv+N1A1i$0b z)3;VQF=p;#>K)F^xdt`F^-4LfMGQ~oSJ|oAdid6!kusr6RVR&FotC@c)8qAO<0@+mrl?TkC#(KajtGjn@>5w`j}2-_Df&K*W!wPfV3A@ zUgAJ71b?s`N7#D0gOxNGmLNV=4yQBqxWT8slZvxO>rZv6FUr?eRe4xwphlHnM5EE7 ze|Kvf>tDG@G_0lurP$vr?n>Qahqkb&NbT z6-tkyCm7Jl-fblkS=*?2Lad{*F7;u%@5z|WMB5sdAhWgr`~V?7Cj)XnU#gy zyozMqpS!8k?C!Y>5VDePsWqSO+sH0lk%w4A_xk|FIfftnw@Dt;FDiZgXzMQ^iKx1Y z+NM24h}7a)+5N1v-o9bDr-rPP$N%j`{-2)Y|Lf{5EyUe0J?YiBPjCz)Am-q`DJaWs zj)P$;FlvHK#C9l^{pt)~&ehyS8!mP)oR;MN04*mPZt=wg&vIrUPH1x$zI;%W6P>7{ znp4;JsnGWlDG@Y1(~04yZ#Q>pJz^0_vo)^Z`LSMMAuDuOyB56pGsvOq>83pM^MEbn zv@12992~_%Ie!10t3S)pnUEEvfT$rhwRr42(qE(#_Nc1Re6`5(S4VO802@jzq<()v zAS>UI;J~ZNsHlwIY@}&{NUbu`L@r5U)`>?2NrTNqDe~-158gF9sHVPAMq~5dsfA({ zaw}8JDj~{Oq7s+fNaUCj(YWsoNS8o=7u{$w6vFk=BrNHy7hwCb@8n4YWf}`m%_3M_ zUg|jxlKvVFihU_90ggN!0BHe5RGoPcw2;b>)ps7%at^abRMZ}vdy#2LyAffkn zC>W`VoWwo;u3ejct0}K_&}eEL9uNu+Rev${#|I?&c~_FWh=s*ORiPAL690s>^b0th zLQB2?-ApSIaBheGiJpLKM^tO*ZI$dw{C&};Xc_8e7V?TaX6flj$-3gE&~(ODMxL-% zlz_{b`sscid2C!r#)i2IWB2&w&$4XD35;}P55O?0q8w^V3P{uJ~uTuUU zm0n8w)t+NX+@>!OHD1AChmEK^cNbgIVa0=Dgk_S{pr@R}*Q@mf&n@?)a$nu%CSUS5RsUHkD+ElFcP;2E90KTJ$?SE zcj+f|?=ui#uzb6qRlnqr%9J#WMv_<5(t)_@U059`&taSh4bx7Iat-S}ipZKT!NaZ* ze))*RC85qy8e0LPKt5}`*?vG!9PaKe`ckz?k;MDCXi*@L&)yelfTsvlIVCVZvC5lrn7G5hMtrF1{_z&Q){WTE-wNqH|sW6IC+u2 z=vb#n!0{W7X9p~ie|+o9F;Nk$jxn}zv!r%w54gDMoUCKQQs0_`+xy^eaF~1_rwDoX z<8HdkekicWO*B?nDd1zt0PmjyDuD#Tg}sF|AJI1^>xmL^>2cmQa>Oe^&3TL7ppo|v z>P!!iItDm>*pximt`)XV;95CVnj?PIXYFb~uBP~XgA*%cAM3P4QJu2W%7X5MEA=-3 zkoYE{o`FQ9O-E)u$kh@5@dqQI_Fkm#`&w*}j5=g%$=aJA%>~8~<5@XI{)t$4u-*O` zJJG$%Dxm8Y&tdj4=o(UUYbX5>I89->Kiz238LKqU>Fm_d?j2zehnWpYSt9=iakV8| zs;tQM>rXi|7z>K0Olml(wU;FiNWq1K*v8aKXo(h05mFZZmid~n62#g)|k_l z(MofE`b>K@7ZQk2p{5&V?#6uL{yg;J7O-@Bc3OSwYPbBGkU|%n%;Rn|IMnLn%2Ht{ zmoGpMuJx&W{}2g3A7%;KnYsQbWZg2D|WAz2_HH8Uuo_GEhSFxm($9$BnRUWLg?3W>pr@+J%v^|*Ja z`-HLf943Co#Z5|Y>^qkp?Kp%deet9%GgX`!g6K9SN2+DCP-Ly;d9Cgbs8?{lOI)Z$ za-jQC&`#aHsiuI@vMpmhaasUX+)__Ss1=!zNu&QImz$WvdamC~H1^?O_)9c+s~I zepIXC76*^i6gsG~Sv#?m9cg6yQ^F;(eZ}1)c&Ka@Ki4_BQE=Y9Ua>$@dZ3#uxc#SB zXg!Zr!lXreK6vcQI33p-sx1 z-!j}nM`xC^j5Cd?o!rJqugdjFZ~4?Qi!AY&Tn^k`6(9sC|IC72fmbC^JsUUOgNo4CK;dz_@ zU4i$sjX7)%BDz!sO{J%5G*fti^E#TJNHAQ47me(Hx{vv13kTOwRyCo1n2^{jthj3{ zxpQvQuj^`5UEAj+l!tQzO3~@8CPq_^waZIR)fy}b!xShH1-PTN-?;Yo1`UFWNH8L% z#Ju_-SF|YsV3O~XHQ$NpuuJS6^~9xzwa%@;pDorn;7IeJrlqy2uY}eoUsdaoyu96J zoD`JvaC~4}91gGFX9mh7K`^+}*>vax85c6@oa1{Ey@9BUI^GbyUUv1D+ol;gUX}LO zhb3+1sPwDIq2R#&S~)yn%2EcEbrqit%JzU`8M{gRl-zP+6jHNQn(s$r><#OI^jHR~OuLC&A(2C+g~&r1slxUC(d*)QAfY7|>K z>WSm5+TuGlh*I5E-bZLc;oshyuZUmloLB3JylpYM%KtSF2ao&4rR@5-_0Q$s!b&@v zWA$c;<*W9oMw!838x;O<8;>?@Uq8H%)dpOqu~k^vL<#-&7fCpCq19w+ zn78=nmCDyYQOjha^&KPqVF7b`RX9}#DesWUyfgJ4H;PwkjE5Mz5-z-4>zB)!E^5fc zxRo5YSU;k-E5T+N{vu^y;>v3sDkyt%RRR<^I1D%~d3+Mwea%k{oBg$m2FXt3U%ItC zdqGQnB3y3j06qjt#3Al=0&B2^lnohs9}q7aIQ%oh8g{I-SVKrjDd0>fW4w!i3j=Cz z=N|;WZ$|Y5@ABJ>h z>Z>8!a}QwR*_SC*A^{4v1Zz5( z5Z?)g3kwJw&m5RqsL}FLGx_eS5I2T3Q!x*pg_`=!Ajaeyf`e3+N?!OcZlr3siQ`zK zyhwS`b@ktf_JV|UfrW_iFaG-#K>KZ1>w{xEX-&s}voO%<7=^0FNFAGc{kJqdkZw=; zj?l3w=)Xa~n8$D1(0zfI$31P#--#jSj4l*3UO)WRr94yTrhjGXsv(aTQ%x(INsqG6 znDx~i8Bymqn!3+OQoj;dpDrk2;SXP1T-R2|=&cy2$L)hiWaKPv0JIQKk;E*lx6y!D z=_}OjEI-5(7vP0S4YCIv%OV95BbC2E5S_u2f#l*Bg!`Ky#I{LNV?gMLIL~dmj*D`8 z?gc5ACuQQgaDvWcNl-SE<%NjYoCbDNOnqnM@uFzGO``JYh>uO(5x2dR7cBimN`aVG z9ixxSXxibB&Amx0O;-rz94g?kwx$FEMD7NcEUq`GWAy9R6{zoGhbw^GIIPAmd!8Ea z_z02rsOxY4BHc@#HQzkXBE&YbvL-c%y6nH&?0?s1!YlV4+AW?NwT6m&5*;tzfM+Vm zx5)rF3dD?QSqtiPvf~0{z9_ALy#9}QZ(Y5Lj<9U%0|8eNxit7Uuay|tJ4;iT@i5uxvT}J0hNwtHxL_JqW2zx;}c zp6~l*wD$)JQa9ApnPOZ1w6Z2ArKJJHJ^EBZVbv_NFQ?K4Y0_*rnn>nys-_0q&C@qz zS|lu~b)1u060S~)=qK{p|E9>cB%`uYY3;_=`Dnd(Y27lznVAtXUB9Q+OvI9t*Tk$q zXYz5KQzNpfnvOpu^okM@(~wIY4#?j1Nk&VS=BciE5^uCnVwXOeVxGz^BMW^f3a}yj zeIYxPTaJoDoF9a2JvCzPE*=erDR>gi+IZDO!>2B~28A&T$>J4ub%|?BkA!m{(Yum2 zcJVr3mqDyhd9U!-ZQ1>cN^bcamZ78O*MkbKj<5-0X3z1zNStEi%hA^*oka_SjZ_CI zYxk}Ot>A5u=qvh9yG}wIEE(q;P}Fjsik^!y>!iHrv|k7g?V^IGb*xfnEQZSrirM3J zWv*#|TkiZ=$UwzSk#bV3%l-78vFDM9lht;6j#pLWP`4d>V{9nlecyx#{G%_`&|of~ zn2dApUnHGjSHt!6KL7p!aF$6?1jU8ZQ*fH=Gk&~z5gw@Y$21Gxm#p9u$k5oq9x;=)pnVtLx5?jWoS-AD|n27j??4JAGKe<=ehX&2!lox0X zoVUibf05Yaq*sNWQ$0qbS$NiKQV$!MLkG6l2upIsX3n|l}rMH}^SGa3bQP7qs z5%<6hq8Aq}xQ)KPwz}G~&I0Nwc02#gtJ((VtF;^Sbe8%|@vg}uAD+D5qB687l&d&J z_UW6YCVF7r|M0n7*M5W?391Wq#Ak^as~PAF3vFn&zA{sw!b| zH$NuX2Uap8`YLfxUs2&57*21Vy%r5kmz_&W*j`XOhp}!^27Rww(JcibbbPWQ=OwwZ zAmHn^_ZLZTZevxy14=Vd}ckXCl=Q+NxOu4f3K@3` z^J0IS1Nu0_Ga>RZkO z-XInyvsVFH#6wftI*#Q-oBmj_H{q)}G7Q*8GI-15Ox|hIL_<~Cq{zoBO1dA4IRfP_kZjyr~5g{yL zoXEbJ*9$QKSS@74z4KD ze^V&_F_AfaT4fyllVv?W?56y@-q%MnanY+J{I~in%L2EnwDf1lo7jT&H&7Mkd1ty; zL~TP>s?fHD{(Awv$kAxYd7aQ9-6?7DPF7jZkk_(2(dVa7H#(!J00klOTxKHx4?qdE zqtWX%Kr^pZt-}$hJC%u6Nu}5>f?SKMNbP^f?sRh-Y5P3T87BKPY<~C}nl3fx?1D2V zOte-ng6_21I(=w#GMZ(VU?O6hKasFc)P!^)B>MbHS99A@Xs4;Q)x!?>gANT=m@YS8 zV@)DJjx(H9b%{swYzof@ZJhu;ro~hI+*f_6Z7mbc2W6P$2WMSZwYyqqC}c>mCf9UF z=FH=hSAJj*B66Q}tsyB@?MfncE0r_}KNx={po{tIXcRiuj=NjT@JYW(UAJeHP!*~? zP&LNVYOOCbG+bLMPgthSX`blJ-l+A8;Q;hovwyd>M_;=CDRvyZ2wdP`r)^15?g9>B zySi+a-wd%_{UlE)w_!j1RXsYTlZTELqBtfxdKBxdf6#GE)=-Q#fZHlGmP1|-ooH&a z+EafmH;?ThbuQP=GeVE(ex&RUs3c3UKtgaFU+BC(ybI%LG2az1HA!7@{>7RTzpC2I z%C_}QRq7Y3On<$Rv2U}22r@#QafVU0!76v$zw!_t19zp@fJ@YOOF%iJuY=s#eH0)n z=)X4roe^EmIV{x|$qE$Mx=FEJ17ixs8;HK-bpjVcJB0M_^KTFSVc;JI{$b!B2L55- a9|rzm;2#G5Vc;JI{$b$%I}D)wUH)H#N|`DLHXD-25=N0FyF&KKz8kWRWh^0tBxNU)U7=(z zN!d~&-n+hipXdAh{&uNGxQMT~C?$vgSprJ-dz$IihiEFvlf5r--$ zDk-a|>gvJt4GfKpktnp4waq14XP0ZPZtfTlzgzwRfkD9`QPDB6aq$V*^o-1`?3~;@ zTxl7;yrQy-Q2nr}x#dx7Tl?egp5DIxfx)5SiOH$ynOCo8=a!aNR@c_wf7tlAv%9zd z_2Aq0!ymu&A_4UL`}3D#|A$^ofL^5J@44f^Q;_z%e(mdE>LpvY{RQ1AXO8wt>Sb=uG%P_&(PuXvk3WJ63n+*B$qt zyOj~qi4?w!iF?IsX|L=1PC?v-kH4H4YoCI8QJXtA2dEyjo`TX-!*^JFXT<@k zRGfmkU;m=~^Ikiz}E5_UWVElZ^BT9iwtWA^k4t^B(HF8`3ToR{^VVn5K+!F02jQyg9*zpGGa`-5C12bV3_sJBjuUDtkaI9!S9+~bm2ZQ&XWOWdI9a{$QxQ0N}Ra6tn{_+HMK^g^|A* zhs}o~Zs*S>15WtRwdC;+Rg5n}z5|MB{!#49>>p`WI3}d~q&d@GHvP?WXGgccXrJQ` zTGwNqsv)DnetfHLh8IP@kDf&F{>8!^fb0Dg=^u3AtIYsf{q7jFs126gr$@TM01edo z9roD)7Vdys^}iRb$ew~^Q~m%sbP%t&?^kl9cuR0OJP}X_c=(q(OFL@);s1K?Jw;cG z`sE||%c$9=GG3OjYbl3+1{3?9x5E3ckqgblVk_^n4!%q|VRK6i{~!&p8~bIQo*kur z-4kct!wZZ63T-HPJsj`i(iwEajxBbIeo|`wnc)^Y*_x}rWcc+GD>3&rf!wBYBzo~> z{s~)PV%Kl+e)D5YSdbe%j@8_?1t8d7FEN)Q0hroCQ1#~bOA7#l;;s`DT>y^o-v-d! zKljG~>rc8o)Vly6`^uMVvOKv4I4tEj`*+yjO*oMQUL5#?W3@F`oZ~_HNGzeLR1>W3 z22d9L2{61^d%*3*qkkw>9V>D*s@8Q6ZhT1T)BGodsroGbaVwE86N)rd24-86o7w*X zEsO8APyc~DYZ*tp%;dVxs+$EsX#EciYA$*F0?*&GqASB7!$6Kf-=Au^{{I$^&O=1$ zG$SIt_I;(}3;!`C!~pcv{0eq|#T&niFdi7!2LOyk*oA*gY(#kX%&%ztC&H9bweUyH z)f5Wt%2#NzKKTdoD~j&%{F;sL{+I{qSivs-D{@T!4$S|I9G7`i{v!P4D(efY@KyLo zRN8$Y5~TbIQv5)W`ZZ^deE|Fa9e$C6mBG-0G@mHd`u~hC$Q-~5L+@?>Oqext=n|W_ zT@sj~_89%|wIlbA#6M=ET{{-%L|B(d3(%NfI*0u_%5vZB$o1QnrrWN1{aXRgx9e`7 zBa0rZ{;;lim3&MM%%tdu@K;?Y)AdeuPM;G+YP+#w6^3va81!Fok*pVSoeRACY2c?G z@)QIcvZv-hs4u?wYnh^&WBd_r@$;N;kbCUI%uk*K4$g@Y5jt4c}oDQOMm2>Al z0UZuvem`0k(5Km^Tv`5G41bfv%^pV^{SlR@hDHQdBhr*Cx&b%tX{HVt&UU2X_VLx)~z1FS5u7J z*L)0ekXzITFio)#WVG%??IEgEHOI2tFhY0BAX7tp5gSEMTl_Q4uvY`3UIdIkzKXl^ zOP*n1?KPOF4;D!g8s8$D1!rGWMFO+Bbq)EXTBA5rASY|fd>3Z7+lJo-1bz&25DG!C zQ$B9)FFwerNLi(g2@3c4C*1Njdb@1gB)FLsnSzXkf%=zv- z(08gG5i7-l{u{2nFNuN|nVU~R^{c0#abIgJNMin?bmDj8S0`N=zo(}-=z;O8b+v}g zHy{djaPwkB;(B9;Qo{%>HG3s1Y3^IGwpt~uImAbQWIW)aC7KdXqvuRoZfn;k7SCK1 zX-=AR^(%h$BD|PZ1~N}^9wyNMKOd=2q2^xZdG4Bx7mOb|IfFBUlkJ(4qdb(jP>mE-=&6_?a$@}$Jlw}!3MPW7$j6bSyJm96LG$B_^$yDvsGTsg#Z*p- zv}RKs)i<8W>O=et%`E8!QatP?4T&I#Kz^FZqtvQCQWk$`1Ag92AcD)rlBZMLG_Qsv0Vmlm@#XF+N&0-p|YD4a+DC%*8mz%$j*9 z_*1cojn1g(IUd6bW(ZBiH0=i;Xg7vlW}+>|3nQyl2JK$L27Gk==;cm6q=YmgDuHh= zxNG$ZlAn^wfdHok6X@4WKS?<53 z9jbn`LNX9t7ruF@xo?-XoL@8d6iAi^%P^-P2cT3aU9CU+M&o*VYj2?A+hUW)1-#g( zi~A+*wYEy_7yS%-upYS7pL15EG7zm|WQ-b}Z?ZTH+`ECeU;-0Sr}gJ6CtRZd!w2d> zU^onp{ju{K&&1SfnVb=;dBleuSFkxOF+{iv%2s>EKc#DZ+$M#f?!?ob3L$oN-3 zf*x#1MqR;#WHJ?q;CCb{;_o!i1qzom|1b`!^r68bAX?>p=QwD<-718^s`%2ChDeOg zWW;g-sC3CO(#H^HXaMdpUT$Opzd=PKeNcIlj^Jn{5sGzc=rL8`2TvNWGz{ySLnl!Q zobJVwG3UrdAco61;I?Aw_=+?{8USAnhs7c@zmvD8po8)dVzAP4i^JqUNO~~mK&t)& z$cY?bV5?Yd7}%2Jzu=-OICfXw_OJXIUU_ma{Rww|Vaw-toG;bS1G%vy`R@o9*<`KY z5Wj)nVjfoNv_ba44QgoLSV_K>n`lv2u%fqk*C)X#;5p@0r8NFDSH$Km(Krd!vxg{~ z%Dh)nB~ol%nO&7;O$m)8Bu-(cA{r_R>8-5@WR?_3J>YH+?&HblL{jh=g)$w^+m@0K z{K?1sjQIc7UShkCtc?b1irg`Tog*pD&ZL!vJu}9D zk1b))XVF+w6dplQOi5yby)zdu2Xa-$Lyr+o5gb?y0kocq1x1gkBI7G~nn8;gm|TJC z7}*D4CAv=Q$jZ-w7q~ya6XFO65EEag+yr}8ekQ}3b>EAL z_3Y!XWTV7kXnV|{lS#q|;gV>ig>fnt!-Z_Cg~3#@x{c@QCVh}GgB5bi1UXxQiuX9q zTzXmND3d~Q8H~|#ZXAnIn(14!w^}?c+bF*Wf8oleUi7>8zax~yqm*YU7rS=Y;V{;c z)c=C)^;ZT>vfgz*`5ndVDd@HKuNbvx*An`rf2oM_(!c)lF^g!utoosUr)>%$-c9Hu z5x4BR;Nt0+-)Jl}3NFWx9#6$pwfBkqxbtS>@YZK+lK>;pG|5olfPhd(J;EBMnHk#R zY7nfos|Y>439={)3HYGmSVWLVP%7HnOv+4+I#WqOyhVq?xU|@?@x04aX-T|ewWD^a zZl9DgzA~V=hF%>>!(J&1tS)KVm6WX9YEashZ>|~8h%qy=whn@a`s6(%>+AAHL;GCY z{6hBzh}_kw?P-t#pMoN+!u&keTY-JU0$hu5-ZK-)&Yg;kw+lf~5oHM^#WazX(n7S+ z3L2Lw@5h>2gS&mS!zPW0hB0rqj2SRsI)rQ=$Owz^f#*S#kgNmQU`kdF4>C&$VBPEJ zgW=@OyE}y5Kp+tlQRY2u+bSh7A|K@qOQKQNXjBM|IK>H4`nGaz51r<>PbPKPu=K*{ z)CWDvc#$YGk$WuOL@7*OrE5f6$FoYw;=(K%=)}A`_Y1W?5t!AU*q&H}o!{*hQ_%(7 zOAkdqSiG-G{=qJ0sRw;!Rzdxi4LEnt?5(+17E?v)XGn<73U~s8pSB@BlKQB`Xzc1v zWYg^sF#z4FXeY{W`m9=>6CZpH9YLknLtjoCn#F#CcLjncO+`tod*5OwC@K<2s)YP- zPMvB3!FiufLFDG)t>IhmPC*{lb+1y6={(=fzxylmdo59&^DJ2$SZ5`Y%beey;oZ%mS@(&o-4(61&9wbr)-{fSq>Q#cy#D%FKd0K;u7e@Q z{bH3%A5hj2K&y>O-PvX2!q?lmr=Uvrls)4W#(u{5l(2?@dT(>t12N6-SZmgl+NyfK z@i2$wgd5{u{JB5(sO$>z4qZowq3b>kb-vf!8`A8~wb`$h7@6OQ;gssgDQmRE4hsb>?@R^%t-NDS z#^s!7{U%x(9`z`FVitCL84*0ywM!m;Y-Yim{6v3*WGejk-8lxU&ZAVY_r`_~l3Js(wqA^T|cc9iW3tYcTVbX=3FR#O1JEA;Eo{ zwP&dQBNl(J{y4U42_>|n{}8{t$NP&>Bu@$~_`<8iW9{={Ip0q~kARe8MvD>TPBbV<6PiyYlOP`38JMabpUKlYs1@RdG%i+Yj?wZ>pxV@Bm z@Uq1Zn($TXUsekFzS-;c%RZ!leR5QPtFjU|a1r2Iobs0iDvG#+GEVOPA!*jNbbB~% z4^{VD*8Bmm_=`=@&OkiS0R!Ip%Y>osnFPZFaf!im{R;x3_jLgo1^^i*^ZTFjlO_Fq zJHA!QGi>l+++GcELDzpUc(+ENHhf3HmzVYWg>UtK zfXy?G#bcZ&-0RhOglEo|zdzBn*e0;_hP1uhtUU$ILjebq4ur#k_kuDmQ3-6O{8%qq z0-9FQIhQn~ez`2;SnLc>94xpasLa;kFbojY=!BJH#~$e8Yjy#0c1JXBWLoF)vTYXa z{RB*!aSYL1l6s~2{y)(F0%%YF^N;2{k~08z-oH6aS?7g!U#dF=jea@NvDhxZC^^&h z{@btl^WBn7Lk_#gvi$buNqG1!b9fB@&Yfn>wcqT2J-mqB?OEaWI|=Us9_~PURy4fB zf#FWHElxxt8b3~1XYg`vzS}zm0m3=|nlfG6`Pbj=p9M6L1@H;|#HQhe-4$H99Tq=$Afx`16UBZj)K%t=q!*Xn!nvFbNUaa4X#Wz2E`*y*8CBMJD8pA{`F%p$*>J|sXL7EH`I4N)v9&fuYKDsEs5@}Sl0&YZf+=}*=+Q^A=!X$Ypi zc0r2b0(rxhA?72c7$2f-a05RT?PFOmX{u!g<)kiV@OU7k&Y8;T5s4II-!N<*I`?_9 zPlm~u-pE?0qJ{rYQyYL#`J3baQQ6_%;v4i#67E0 z3zZuhhLs8?p$sryujh06kX@FSc7by~MjaX-qhIkA#1awL^PD~VUdkKr`Q_*P5#<$7 zSJ==!{*@NW5mKS!!wh<}=*$^DRo3*)fUo zNT=MDWU)3T&oWPi1gbg}#yVL$+DQ)a8=Q(`Y~owb2`iPk{i>GdCNj>Dp+xH}9_0>` zZ0KCGiI_AZ3{HhMi=0X262;y+P)s*Vo*w*u8Fu7aD(aY8(J5%3#%q)=My?*V|-~sf{-nq@>R0U2nclaL9b>6Fet-EJ3-WM zrgQVxSxq%Rw?@A7unClx@cJ?l(LVWFX;|XUeAR$ z^@7FaD$`2wd$a3?U69(tS6CXAqqV~%r#I3fkJZ|q>kcTy+{zWjW+TKRu^~7N zc#R;I$(d!QC5~2*q`_1qPGGGdvJ{5m^3N-=LuGnH$U2Q5C=*0-$W-Dz@S>O?&QgqI zSR4(0Ca#T&7XhD4vqL}aSVcHi9Zk-a3Po+&3My)xU$5CwV#V2ry^k{DsYo;0V`}!V zgHz6#Ji@21k(*w6wA@Y;rXMfG8%P^e8+r<2mQ8@$73{jVjcT1tL_a@iKqYzK<7B<@ znRT^jJN4oxm*q$Moxzp-JUTK#rHg^CW#Obpn%c_;K>Pb)3d$-E!=r7RLYYh5>_CsgDnYxFJW?9EJ^v@_V-` zjEClGo>WultmR&FH(s+Jyp`zpyn6Ra^cDOb+Djx`p6G11z?Qpv-KMwS4=VcPeH8lX z+{Zw&iKda>f>g|qDjTi#P+cbcRfe23kM|`?TdsvXpb?2!RNJClG3ljL?A~oA~4tSQfG*m zq`ZNxGjk@V4mFrIKQn=Z06uP^hHm$OV{u%zI5U1nGf6SZG1tV&Kw4WT@Sd4K0Tp=W zS%quAGw!xbL*-3d3Lkd~92uE8IYTpFD65bir&9rHVh{bzx$YA6L-wo4R@y3|N~+@K zOK)WLVOs1N+Mb;rHZSlp3IXU1D_c)XMrH5i?3K56JnV40=)0|Py?3NF zN2>0<(xOK%W8HI~$K31E%RkU|j3~~Qq^~{mLeFukC4qixH4i37K{LE@tu13|buS&B zN;I`i2zS9yDP9^?ZFY-KbWTC8V+(5?el~^NJ1C`w_*gkL>&mx5Ql7e|q4u&i;nstrfz9A47W=_u2p2`Dh=W@uIyi=X=-jWtAs4nhj&Z3yHd)=fIkbq z6m0BlKWZ!*uDHqIAa^Y@#rSLPt<4}oTBe{c*v$Ff9uk8!<0ZcVm+kf9hcY%Vd^XSn zasDc#pW{mlzt?*l$%QZur!*NPvfAB-*hvK)KV_QAc_Ch2reTILTfAiT+Ja+f!1ya8 zzfU4v2;vj1;tE!`ef3cx!;^M>>lF8|RS(o@ywL(FqU)=#>LaMMD5Nth3+Tq^3x$0a zUv#+U^KB*0*nS8p2;{qc|B({ka_7z#d@qYi!&i0UcH!#?y6XenVoa-S0lUxTbcJrv z9MlbZUb);ENUWGsa~rn%v7B=x_v+{_fnGi%hC_>RCFnuXk>dPOvtmBUCkXaEufX>8 zi%UTpS0*=PQbSNTOP&pl-9hD(5nWC1N-!USh#}Wr#q>1p{+L0XkmpU`Q$iZLs z>vbM~4 z_Kb*D{((dchkxcB&Wag9fp?Gwo<4k{FVsVk6ED|Im*odqti>Ma(5rKHxRu$zmHHx? z(%oFV-8X`#L{K2K{fd0hQh8<350m|_tPMnuz)Rp63lGE4rJWU#l8sdq`bK^3X?eYG zJsNtlQjh1ar{rG_7AYXxJ*ZZV#lK>P#_^FQ&Ol!R>AK+8)#cjkq;p*u!Tu1g7IgYX zCbPNUi3nbtB+o(T{|CYZBg-&Ibmham)?@6AgNQ{9NwpmX|H3NZ>!NQ2l0XN z$cQp9T_QGe)V3L>wGyQTe)F~3w@`dPst%qNR7@+^N?Dy#!<^iL&|glCEW*YQnM%i2 zE!cuW2es<2eb<|RB6=%)1E^c}sW*G@8G@`^U6e1w_yd7`z5u9VOe#=*eIzw>I% zRdb+lZ@3%&pRcq<&KdqXna2QWs*;yt?6<_e?mrc%XuSH zujU!>BVJjkub)*~E1H~Zda#Y)nszO5$I@t~V$0GHLzc#ykF`)N@v2D0&a$^>v)SSD zc&Z5Eeu$chbgoPIdW$^@q(Siz(fRs$`@zRTh`xS$WX6Ghbdh|W!TI>t*qM&Rwo!-jYs4GF zi!-mX3hM>PA2X-K%vxsQ}wSE-}t`>M$2_brsf%F3<|5%JlH z3U0+@v1E7Oe8HqNT<-ol>M|x^dMMX+B;YgVL{leY+X zHIo@mvwKj(vOExPP~ubTUSj%59~k^BWr#)v6V3{*0Rp526jCacBw999tl$*n77-?3 ziFh5Yq`(*AZjMtF2&$3=bouxT!-qJ8lIYKho@al>gCK}w|!6_ zh4dPps>)nNwq;P(oGJ7OjTurfc%h#udJ#olDwq@w*4UNr5>`RkA^UPW^zfJFd zu)z5+Mlo1imySom_WYGBl|yea){&0eY!k1Ssn4w))RD)z@quoOdx)|2lhb@t{#tC5 zv71a~dmC)0XR!wwR{ml^r$$GI7!zxwM~{jWsHQ^Pi^-^FGKy?%#XuaTSTRsVx?i6v z7No@(V)kUe60SwHWHefS0X#;}-zf&-^atCK&xs9!ncA*?!-J%UAu^;S8_)5E_`$i}JL1XxOOZa&->P+`f|_~*emGK3kfcp2tf+RyXaD$!5xgO7YGr-ziG@_- zory0OZ5I9PO5ZB(?ad@yVSf4a%caCwv81P0Rcb4zuavwDuC>l;CF@YZr@IbT4&VK> z;XK8d*`)-xxMwNAc36|gqQnZxdo=lwdv;t3uTAD}9|srmy`Ho;D@gyvy0~2u?Ke~2 zM=`tL?&PU!$rP0-iv3!r**zxwup_d-x-f`#Ob6k1G_D0&lg^Xn6-M z-d|H7Ryhv4yJ`x0`H7P_eWP>=I{RsDLp+PJ-t1e`C8ckkv~2KnHOqS!?RG3;Z#Lie zIOyw|>K(XwbZP$q!bMS==^;m~p$6=>=&}W)?e)33`%`NZdI)o>kUiMJme71opnh12_O-V}tjqgX#0ebFYNjeRzt za09k`{KT~^CLrd1!xLxH!m*cOkLYv`ifI-T^~)1ht-4}IzRe8}unV&_yrW{$nXm9m zw0?9fvn@1+DZ1Wa_Rwf+E)tiGg*7B~(B@ISunojP$L!*-BCI=6IK}8p!8EP$vt{7M zDnAQ}C@iHOOm79-f=zf;CqhAUt`3nERA*q{Vr=*lVn>%}ty1l)YrCr-T%E4(V@8uF z2a7O6r_p;1F;zd*!^97#dCS|s+jc(DJL&>9#lnA{;|`h{=Wf~JrLw*m?KSXIv+Lwu zaZ~eT7U{B4>zm|nd1I7nkiE%thuwRm$|qNNiaKEAZL<95)nT)-WG*w995McYR^TfK@mV zjIu($!x6ZNC7Mq%$;>mXC%5_oW2+B*dDY>iCDfH*JAc;rFr=&bbbL{{!m$DAV(xj$ zoTk)Jn|wN%pc0+JQo`lU89$#U%(t4z>_t}*(xyzIdr5|k*!aGtnNl2CbHQ}n?0H7} zulApB%_lC$S1`K|Karqsck1!?Rhp0=G~vx|X-PPEf88a-TKSsVHrOb16UFV4YTDf_s7VGoc z!JKW^TZRl57aHUd%u#8fHzLkr+2?UV%zYuOhIm2oF9>#*&C17^23!k zWmwPU3W76Z$6;fUedvrpY&q1q#nuH1=OA8=Et6fG8pk$kf8r$%UswBkMe}-+ zv6yl^@UNX6v}O#~^hE)vQ%EABRT)p@z#D#qw3k6az`w<~RJKU7a>`tW(hR6;?irYf zdoe|6dg4SI@GAxr0kmG}fI~m*6kr64q0TCj)q6F=(D2Jdlm8Wcjf<~eZau3|AW*i{ zB`FBGJ+rK=3!7aYWquJzFrR?X3Au+%P4mYxQ%-DW1%{(t99c$>W)em6&x1NRj4a%R?AUAU)brEf>Dg;rT9r zqbs_w<)ueRcovgu!j+~0)&=T7I?j7XQV@0yeUzX1;_T{~?ZuC@yT;YFu5WY$c(S5` zw;x+Q&I@~(D%NXw->AMMdx5-vniZVgkQbfIU|@=DS5y~~j#1?FD)5qC@7WS|@@jwJ z@#?v4Va0AOTZQsNogD6(4?itTV~_9dPzqLHJfWRwTQ0OzrnFLOJPwP%zQTd3ukgz< zPLqp%`}Ni>Eru7!eX7v*AXaEVV{ed(g7lcvy-3E{2_?oRABBW!_o}6}GecWf`0%NM zNwxB@XL~A=g&d5-T2+G_10i3&+$tSh1SF%N29=e{KVm zG1`7(Oe)$Hnf-ewdx>_s&tXMPtjqfeK4ij+zq9&rCnWu0W2J0>5>Jbxjg_sTyw!wu za&w+hM*FBSY!e<=&WMPY4Q1Y>a7V|V`m)c;*!b81cEo;IjZTwuR?x>R<( z-BqTn&L|QcNzgrR_RaFOWV_#reNa!du|Dr{{({f|1uIw7-B5&NqC;}lWmLyy6JPlk z=sr8dL%xpb#fOQN6q|(^@TZ-rTfPL8aM6~MP{u;-fXEwtRnE;A1?Oxucb}}W;m-}{ zca-Hv;v{=4hZCQUROmM?+A~=XZ%d%k@-wboa=Yxa%iz&4y&hJ>Kp}UfcMr}ApReO& zsu8)OqjmTeJKwl-HUczix7~;_Hz38c(MGKkyb)6JJ{oMV&`s^p^2$T3Mb!&)@wv5S z3WGYFg5L&tZH<=q+9fSk;GYl*2X#qp9dx~o!&Ba`M{mn8mAG8YBv10L1vaVQHO_1` zf6%gdcC%cxBHkw?#}gV^p(6zPvQH4ndd9+-^;0~vP=MvQcM;C|YHmIdy=B+pmHgVa zFRwItU0QL~@6u@bVBE`=@XW8Qb*xQ;+IK8bUmB@Ce}_+@bo8I($hfq0cr&bEb1_Zo zS7nJ;3Ri+@?>2j0SaW|4H7;H;??*pP9l!dxsLmm||7{t6W5>j!mAJ4_GyNNaMm?N; zz@A10=LXYb#kC(Os~>h}6gh$)6xtYM0sR2Boie-ZkT$DO8vUNKW`p=rX>A*3?Rx~l zEF-dVe{3C%F*JYkra-6dVgwF<(53kX;hC_$k32kEFt4rJIs3(7mJppWeMq~zZTtXfjizm( zejhrJqnz8wtNSy^A1@R#CA2!>TA!KhYd1lt%G^Ux65KtkMQnq(P<)fq+YoRr9_bC? z*ogTcW~5d9_Twcx)cM02tZW(|frO`p@`u^iGCcuTfm8+ECzf?Y1Bh9=VNf;R1xRna z#G6_bjLy!Umu@)|v)O0N0Mq3o+Pa#4Jq6t*XQ_8Sx&Qmv)>_cFXJr}nDHctqpe@@2 z&QImrWw^>-YYwkWuH@$F+s);zs5g>Q7=1re%KN|(HJjV)B4s_uOL^z~Wg7X}Hx0S{ zX37{wXYMz3k~mpD+b^$*?9;IGGhVv7AdmG{sk;2Yoq625QA1cp5i?uC#+G@j!u3Ax zERI_r11yWZYS279ksq2n!Hx6so5)a~@>OPx)cVAtT6(|#$-8Bq&Q(kbY2={t`s23& z-#*QxKl-fm>g9sW4n^OaJ&1283GVC-pVn8q3r)4PTJ-&9x~XNpT(}4_SvPgJ7MVC? zq*cNV941OGLPmB7FZW=f`JLp7NbV4dzxH^?;?nsR0eu>ka>v$kkl}+f*ZMSaLg_1N zs+;yU5MquCxs3eBZ)@*Z*ft39%iJ4vM)!wKz0f*C*F?4OCha-VzmQ=@O42{ypZRR< z3SXfF&V9l#Q_0RnX3Kn%`?GYNBdGO@7DY%(NH%n|u~uZSZaLMjUAyKea=Bq&vaj4$ z|FVzGv!O4x)u$i@mlut)D|LJ~Cb_?tsIck?4VH6b?iPxXyvENHIm)2a82cdb%J51j z`J|r~>4UR7L_Ug4m#bL2F*6W9aAZYHg^Xi42O8}f!RZ}(2P($r>u27_-bV&T6y}lP zC@zqmJ$`Xl;vBL$c>1o0&t?Dfe6#~IxU1gh&K^plW9iY3mqW&a@REW=Cze8wSe@?D zYs(eRaFu6-tJW?WKIn1~wn?af3AO4ae!hYAnjIO+wO{;G-7c;~cDZCQ8_{+{dd0qF zyWoBID~GSY*AeS9i{V!f!bDC%X;zyjUBKZ0En`T>h(Rz6Y;E{^Q^s?r^+fN5<`K2% zuY-6j0TK&OmBI&e{(TNFqRFY3qv&+ce3C&Yj5gFrMZ^7oyw9@noV7;=-9i)yb8%^(OTm zgaJSCMrp0n8le?DTa!v`Rk#OUMK>3u*}N1P%d32UH2(f*CMXB`)UET{@b#DWawtz> zt0RLO@b&eY%)XbaOS>Lg)0e2<&Qw}5h6FH+(Dy+yNK1vJLmV?i!Lo01jfj`52m9dK z88fy00cn?-ud}&7Z@e0y0rOE^FF&a1VJ&$P{sJW|h?j}U#jo;+#VKy1N#leGS|OfQ9V>8`@Vi|E)#bR_-s5(%W9tOfqA>Em`U(`ar-ELtt3 zpBUOap3&8(G*MFMSlToa%2j*d%-d()Yz;xV&Nt4?>8{Ox$tYo^X<}i!wWKzkfKMUO z-Fc0Od&@&LOl7+(EFnKv$#|=TwUi@>wS}5gqf?xHWfvuv)K{(YnU!&2&cYV+#>}Sm z#*&b@nfrx@BT8g8R@EliZig0r)-xaGM;i6#n{*SDF4*hv#iw|z3-6J6ad5qY-xhem zu5#EoSSv9z&zWT}4{y{bVh4LH}%b5w_DjE*!uUb^jJrbGHoZ8}+^%*gt~$d-lUtnQ zoBp9ah023h!~H6C9|~CK!G>f`cxEf_9BEvpi7k;`JE!+r3MCxikuxQj{ zlk!D!U7po}O$+V`czCz92|_0x*Ht4#A0;G%p^g+fGHPFCNKO2$p=bXFlI z*j(kWRqk_=t?QP=G<0igw@wV$Z&+`5)@~e^-~gfx zQmJvV3sRr#L0v`Oost{p^Ux1NS%|!xW-8Jkybct_h|3G>OyJydZ$qk5a>LS5JU2Bk zO)9LHM^rX!VeH1RaZzw^mYq?b4AItRG`D3Pg`GBYP*C-mJFaLf(m%5at$G}D74b#6XKH}R@bs|u*7QwTg%2TTc|W*Pr*K(MXoXaTe-J% zO}1rExk5SFm!ESHjj@`k?=?IW+x4Q?0_pb57aerZ(-A7O?fh#89L3{WZ?}DfypUQQ zx>*oVy5ux(NW_A z*5U~=koM}0NtaBqN}m)AsQMfWyLMK}i!EdVx;nd3-EzLs@fk-26)tG6SFSsq`t^SO zndt`$*Xw1t);rG(JpS3)H`yIE_he{(B=da>LT}JfvP`tz!*Z7EYsFU~PH)>9W^uL) zqsCT!3J#g zLA2hltj(?T7IICEBa7kgyzbX6f7y2P(bMY!OV9Z`j;zS*JJEj0t5}z7%qJ86FTxg! z%1t;8Wr8B+rw)gu@zNN5&@lN?d^ICjU79ubb-zU+# zp=dX6kKTePbl!Cpe!B;U0)!C;@k6_5Oxbc}rar3LEaXdfWXhQ<;je(REgxv?EmY68 zY--X{s$cLgI{X>_lLRtpmC8L0hF|&p)q&vF{&$BglBb|gd__N`jJMCR0=q$+z(GAT zAkX@1`{!AKwSqK0Gba2BYq<+@dVHpRG7;%u4kV^Z;3Ldag=>uWp`il5!H z;$Ng0;4oKwEF0EwdhLwNoM|o!%your)xEu0E-E!N{M9AFgfSn)_+#A#T~j(g<4tqD zm2wl-1g0cY_T}6JWNq+r>0FK{UEQ6{7mrY*wrl00AnfdppzB8{WfPmmafmfq$PXo9 z@a=h>aG0=Vzp&s$t(S7$h{>6kn6~n`Gz&+pmO%mHGf{z1{I!B?sDgz^K1s740ejv5 z;c&U~OoZC>EwvSrkeO%?X3*t2XT@7ctq%!2d#_kC6lRBL#fiN{p?s3V>nd-Z!t-&K z(5hy^N3qvmG~_CabkcScw{Cq+WO(yRsZ3o*Yw0?{Btb}>F@tMH;p2w$4Q`3XN8?qg z&6O!jH9Ss(Cx~yGXc2$3sOIO1jn51=Kfi?RZQZu8x|ySQT|bGP!dz#D2mg)5zSgo> zIO#^hYx-VC1KS}Z!3@M8&~-#3_5C@lk#VTzS@4G!G#qd47L~p;3iV%W9Ui=B_f=u4 z;_;08qi>3v>Mn~-bc@!S%h1M_>Efwd{o?22wC5hTvnKmrFw<1MoN(N(rK8OL1-XFP zy`6^wS=bU6UvQn=gl7;j2GubBAzv-lQDq!~{w@SgxbuP6^uQ07DO*mHENhOctMCza)aBF{!lepBlFOW=1FW1p6h&DvWmR(*RjtxopeUh9 zBZaF2X0p?=c>U<*!t>x5A9!P*S--`)Q~m+2*0ywrC}9_NAbl^x!X&Y3e1qMRL@wTqPwWuanZYQYZQB_I;NW5%iii;p%0U;Io9V^d4Lo zLLZg;DzJJc>~UNcZ9bkufzCQo#46H)XuOCyyw25;!^cxvb~D@Cd1w@Fid(sLKJHz` z+`HatpmNxx={rZJBSUGlIw|;=dDzKC@TMR7rW7X)IwuRdB5p*IWig^UsRLzrrlsAr z0e>r_E8m;V*GQ*~HbG8vVqlEz3NR9h+&cKCZZ-A2W@5tdG6FJ2{CeK)s6ZW=Mv5ukv<1^V*ulojscChyyLC`otG4*g3Z4cE< z?BQF(XJivuM$dU#I2DJ&x@@)mo^DGgIhZipEDYaqz$rRE=gImMaFPG+i5MQE6_a^w zYK8Kt8{Cbq(0}BDWY8R#>+C|y+het0>6^|Ev*t_!!=gujG=4Ga#gB$s@Kq&4g})0} zz%M`Q4L*NOWx%-ib%Q~R%4-nooe3nAoQGOLm{TpT4iu|AY8#Ofq7!gDyP?H=b1ia( z)%N4p3w3kWT(ly-&J>|DqFCV|XpZAsA0wKkp%clVvM^FHAt9Dn(ARoM(+)#<#b6z{ zR>qeC6O?{APeIfu__euqtt-V~02zjTxjsk7D`X*V=eE;ibYKL&TD9=&)NB}fQi@GD zxZzW<%7%j~dE)5W(4acyK7m`fSvVlu;3!cIvBo{x(9FwZ>k$#1=S#))^+B|}ZA-HRxxKn6x_ZBVQ;>Am8 zd)}P$f5*LJ+^^40^5oOrYp*%yZ$&RrjIrYr_Uheu9Qc!cr+#Ks_j?<3mp>SAHXowz zXk%?yb|xdhryj9jdAJnAHlSk)5TXxWohSg2deXXLm2nlXx>PGy@z++z>fC4qt&omL zpFs)33H7pUbz9L{jea%j{W9+h0eeZETz|a7Xa*sqa=|Cx2*xCYQJ!`SSCmPgg}I}f zt_Y}Y@ZXdhc0q`%>mWg!)cvdfSq||hxV8r+q;hwInf?RNX8x~y_hp_ntvzMIU(Q?;_t>~6JRqS77e&uoK$(E0ckT;$m5|C^@9ug~Pq23? zO@j=Awg`Od40d%qx$+h$$5qXumgsq0eK?|tw@yeK{S3K0t zNZi!~?mkxC%2Z^>OpLr=xu#<)?5V-cDT+l^W2{w1Fp=+CFuQIjguGHj2F0+kB+6R! z7;LABnTIB@f+k<+r4u%1o@n7moT)uTwEyKSd*vm{qn&svVEet86DY+|W7_$A@^V0) zNPw%FjR&Iq^7m<1YEJ#ZV~6`|7UK+z8j?__S2_qhIYR!rX{H-PwS#)zDv8>Ck zCJgn3=evERc9&%-#75@ z^h4T(U?YMQ*37hE>+O{I37BbUwekur@kZUwo8VYOk;jInbQ5K&pj9s9(v<0nXaTFf zGT*PWlzSh-hA2pvG=ycV*a%tM#x2*%lQrL(_)P^^8N--=RuSY-_x! zt=vJeF^&XnMeWo{&ko4j);>^s76z@n52(m8WX9E4hg%kEEx%wt!jIoCbNRhotgQtq z&*h>{w-du_p&~10t3l3*FHY!WfQ1R$msaI2LVT+WJ_dz^ADeS?3Qs+Dyi zX`^p5v#T$LYm+DLt5N?+iQSvRU&mJeb;nESyA?^tBc{AwX{HGM?Z9fDh2i6};IiTl z*qa%seochl+K-^6zl{z^+9(~~2-VNKCC~u1$h!UnB}}>1h<;mR=K|Be~o*PL*1n&iF9&_6=R`IKinbch! z;5_QrW@$5NjHpa)M#`F9Am1qw@FR|E5WhF>+2pMqB>r+3IVgFld$Eb=?v(W=e@bA| zQZ2kcgHSMfwLqw7l%PEXxQryGsWvClMr|02+t3%g?sxSq8`Ho`?MEhG3sw7H`ZJy_ z7&;rOP_O#U)SyaM2;oGmSA+?DUaha_432|207(}?bIhsIxOwK=lcMH_AxMDKWzPKk zSQqNb+z^{7!d0AIbsTjpA80K?pZ&d;Yz9FQk0D5YbL?8Y-gN1NUS>PQa9`K=A_rl< zaT-8tXLN{a!)sHTYltJJczl7iM_$o4uv0Syk6HI_EG9Vbm|jO;eupsJvX1z~L|Z`ZHBX<7cO!q#AW zcrTN>xPLt>-<*c02I&V%H{43^E(9P&R*?_)LXFck*_dcQfN5bl{b@Z-Sg*FWtm}63 zx)jIq@5u01EXu096CjXYkQKyFrTZ-q%< z?Sce*y{jL77*;WmRT|ovl6wV^t^5)QZXkqV#zzeN^DHj!>7IyL{1mRlBze~R%iXtn z=S)49k1X?-1+9G@+EtL{ZJMYk1_6A*XhuHo3A;nPD;}!c(hw5uTX$6WnT;?ToL}as zI`SHpz~-d(hO(Ba&oHkRY|Z}9ODPD@Mr-3Hh^T8k`L5G`BYT@Zj&zbCMn`;jg+2~# zVAL%BBSx>=9<{cKQJ!uuDeA8JY}Oz~r0 z2il3a7;-r6z9p_|FE?Y<e*+sC>ilzDSxhMs`Q|+{R}Ovl zq14NNB<5Sr3I52sSdo;U(Z0KQZ~tre&}x@Ct-}(1TV)my&c&Ibe$g<57C-TA5-*KHWXnR za5TAbmS|Ai)1`Yjd2c)jhEXwzi3?37|*#_1DbnvgnQGGQr|C71qzZ8+fD(f|6eZw z;HG2)<6y{7>VXuMt5Z<35W(g8akYW}PoC@bGcGC&SYFj!0E6<1lO#x2=S?eP7KxlpmY3S8o({SJQD3+`=fl=%QyE$rAd;$RzTG(ik4tXVV!$Hf3F4#%ra`o z?Y}+KFrG(hk?1_db)dFaWmB605p(v&X8yWC8|^6y@cR|PwL;!Fq-|ibDl^Qq@)p$$ zfzh{@U+uI?j8&0<)e=r(u+symQ`EwOoF%G!-uJd3-`!7^Tddk?Pq_~K z0EsLIMQOL`jBPuW4aB+)4>zpw7j5<#1iwVI^SpMfVirm5&n9c39KJHX-C}?8ywy)A z@U+o0_J6UHjSKub$xOq)wR*uKtBz0Kf!L}Js{Vgb~#LV!{Yn)vI2|x^$LR{;-NQ{E$7bI`q`q|UyH}Hb&F6Sn6ZdFT($It0oR(CfEkz~EI!aAu?uz`1+ za7yWQF{U&0Q=6jK*P;4?Rz|!Z6&|T!ZWHx}PIIuLc9IuVnjzMmV0@a-RXMF`BsJXF zo~HL*a91QR3CBgg5v`B{r&^(oY2$jzh7BKE#FYWPiu^?P#66j3h)QMNEwd@{aJ{fO z`!aI*(csuXGW-NGESVU^vn_2GJ%NlYZ)3~B_6B2j8H?}4#NG~iSkTaa)-0huFHEVL$T?f z?ige%Z=1Yszvq+Xnui+<6+yn?qES@1^&FLd4Cf$2mm)L%20{rx;~OW{r}dYWJo;0R zX(^|Q3$P(73^(lcWus2wKY*ZyVosh5gaJN6%JH#=XX45@c%!iORbct2p7P}=?R-If zi48fPcc>iG7Qj>4ExdlTZ5D(ORhTgrjsliAW3SbdN`qDuW}wfCP6~5aoSlxdv}InC zJ+o{ZgoY>?E{oUb_Q$h=G~^Aakt?$Rkz8pVwBnf}3`{|KGfBV9iMJH~aq<92cv5VQnQAB&eWjGYZ7z(_lh1 z?R}_rA!~GZ=HJ>aFta5rHPNR z)`@eqpJg?BSS<=2V&1K0Dpb2zY;Em~AFcoC)0~C+1s_LHhY08JeLdyuNP4(FS!#Ks z$pkKOWiIXjC=;##=7Auok|N_=|9IE`7`u$&d{=psHu?lkgMl6&wm#6(vv# zGG@x1qt`2C^2Lh3w8}gVnER%!YMWd!V?&&(J4=rCAHYZV?MVUPLlvJABQ6tVx_HXH zs^K~ygScm~Lv`Lfoh867J{D-?xLBgiK6?C`kMg0BYt}^pBbnC0&C74G{*;7Mu-9G+ zb{lG9Qq1Pl>D`jb@KBwW7Z7K9yV#k5EyWXfUf)G3*!?+bh^)>x$S$w$C>1&*#ryD& zdzF!LjD?arj6+hnf&4VjhUq&UKZH0=-|g3N3G)Xz9PQVvNSt&VC9@f2jk|Sv((uYO zNm%a`D~(=y%F4BSUnfTd)BEdprZ2?wJ&b7{qgNV;XbFD-&sOC+!J@3;OeDB`jh|hy zv`j3y;7FGCG$ENeCDr)GmQ=nf23(ojl*_1U4kYhCPEV9dw#5hQ&zT`QD#hIH$`AAa2)2kitjKG@iu(vLu9Dv7e6`}eR!{Sh`nx8I@FOJA5hw+{ZQ@WcV zwkhn=K2M*$FS$Paa`joDyO?ee9|dYNT@Foxs|I%XX}H|bMe7N}+@0N@ zI}F&;U^HeyJHO8rZ!s9|=uTZ6U7?@Q zwz9BS%`@zei|G*!!FZFXb6;Q_O#ZZoIN8%bkQm>IuNh z!l+y7&gRa#aRv;5$GZF$Z73pZiZd!^y49L5$NfC`*QCKuDU!DSZ?uVpI)ve zqN?w>_7p40o7Nj;_v+~SW&htu`dAQPw*jt5uQr%IWq!qCNrLJjC9Wq?bc zeFZ_DvU2Kad^M4Tax%<5pY{GdG-Ew+=8fm;xE~U& z7{%Uma~@KMkFQnp#Oxy+KP(z&q!D)T*V@j*99^u=bQOP*5%PMfoZ?1=6{bQJ%_>l) z(zn$8O7tcn-e$JHh^0Mj?Uy;tF$eLNxoI)76H%Y1)!J;2i>H*CXAwaHS45| zR#mSceId_>j*NFb$4#KX_%_;>GoxJL1Xq!}S)XM?k%m6O`UHJ9o9tYM6lWcFAbQ?Z z1ARwg&8goPzkSI)+i|k4DwQ`nx!hFVqny)nrsC>7PFCzaf2S`~X`7KMbpa&P#_fnh zX~ujCrmeNPT$gfvAFEWP#VVK!B1C1|-K$3vK-Hw`<@CeMCzX8hk5ny;x_fzpb)EFj z_(tE?KNL>Ylgx!G-9C576717n-#@J#ZES^ykvkp1rWrJVZz5ZvXU33pmF+@}J{RlG zm{&0`>Y!tF^p7+RvT6~vxk_|m`ex-{4%rx1%!+tn_&PZ?0p(v~4&Zx5Gr965(2Z-% z&$ymcLyhKql)lZ1$>LnaJg~yqT>Pf(3Z!9rE!Fc1adF48bK$9SGPLL;R&xK+u77=J zl%@&X0^T%#tJkQY=$ENu$hW-`y*$SeM`p;nAN5}|vt<#-Ek(5zBi0g0Ez6@+P(Xyf_j4AvD5C2Bj%eRz+`olk$9AY> zi^bmt^9u-W2@cC7cOUM~%N=l=%RjqM5_7Zd4ph5qk`>VrX|ajEc!`ObPQZOy8&avKHS2UL12sq{7yh-Pu;%sW}*K z_M|-H|J3nUER+?y$}Ztc7XXt-qSo5O+O6XtcKIQl@s9gcnK=J@M8^T%Mx%$Z#1 zcrST~cvHpa!2mXQfLD50yaJ*SXUkcCIkhK!HtVo$8uuiBKp!<=WxRu*CGR+` zAbfq?nZYBrI?cBw{iNt*WEafATalwE%AdJZ)zh0$X(4`DTq81S*t?p~WP=ex&#$Fm z`BvMFm0M~ZxRAeSva=3unvlV!3w3^X-k$KGO>yMml5@0v)Q-rA>#fyJ{gwrCWKWM? z?3I{4iQ(u>FgXVm>B24ItBmV|L53dQu5AaO#V2BGcC$V{I?kfW3`0AmC&SF*J3_Lf z-i{m!yyt%vRe@LWqb*A5l)PdQW*S}Kem^8?Yal_wTz-1To_=7f&8O!T#x*%M0sd0lDklHgq5D~u-lt4ACoF7L8{>CZ#d4hm z53CycbL=mSkM+BmJ2m*V`~O+HNclBP;>h*AhcMWgw*!9G0?s0qDxYQ00ZDp9dqbNW z6$}fQC1BYMPiI6a0L3SQTsNw{q=SFZ%t)sk{~PYNHPHz-{Mq!`M~JyT^5p$K0}`sNZW6Ytpn5f0 zm;w8+$oQW5+k-1Y^Vehwq4xYQcY@L+hju_YACwbYIe>iC1yR)81vj{(o;$?XG|o^i zqnTYz?5TIzt&dIwoDp5G0)dWL>npT#X^Iq-)2bQ(B+fQHqk%Na!-zf_5H>pS{$Xi~ z##O+u>(;|RMNof`GDNYbqx0X)%GPAgke{UEhtvRLQ5;2;E1kbHkNY3Ss^1wXK2bV= z3D=sL!xgY~$6ziPU%nuj2lep0#8wn9F03}Z=(>-qIu#?CxVv|DIwJN5>_FR``+pbs zmFE_(406IvdsZ??6RL5PbT?XjU(^0rTM9*pk2p;nV1sW58xE4)_5HT}u9+MT$uvJN zNIu59R`B|hALh)#pw#bj+gJZB;R?ixOpv*{n|%qV*yus>4necOWsV7&OcPD|MWt)O zdFmV_5RpzJ+AS8j*clMzt0Nw-_iSIrvhs>0uTgqqTK?;J2?$+PW&JW!)5|HYsdHb# zFR5>=<%AR=de=hD?g7si*g2FpXtOevY=vj@V3bzoPm*sacevYM_Ht7=p)nW%u&2g zArwXdWQqXpi#g#yHtGu{Qjbn z39f^d#CM`7xRjU`pQq<0s4I}J&IB^0UTFGEM&+V6SKZJpT7FomPQ%HOJj<@QA%bG^ zrBi9|lp+)q_K~f2Fe;>6Gd990MSG#<6~TJbxOAU>+~ho4m6!Xg2vLdEB7xh9H1P~T z;~B3l5GJC0Ctn!yA?4$!_AKkt4>7-b>d+;PRIyGKTKza{CZg5vy?zdD8qER>r2JEv zM%1lXA*)ZkFAGO}8qX#%k`$e*3yGjNJvyT-#TN!tcpqip=R7{&NJb2&71K3U_s@Ch zKWiC(Iz&g@Uto`QK0Y%fNA@;@^{sp$oH?9E#?I$B#fn`MHEMgC1%*ofUdyqPvp&Sm z3tOh`E_?B3`UOVHm#wA1dFZVjy;;h{pjUQHa_ha8X&+xX1(U39%KKY4s4}l=$iY~qtIi3# zj{dDnptO{jQ?A45t)Y)HE<|bQRe5w)zR}QwII4?O)G?|?S$Guyyd3;$=?VZ4Tt;J*l^u-`1xS2T`qKArq0}*|H}HSe zg*oZ7NZ8CQ%6#ws9AfMupo}SHt+8E zRglhyOH&+Hmn$4eX9ZpnBOP0ICGMB1hpSs7Z;bIXSMZin_BU1A53YonrPx42E8T>Y z;;EC%z7CT697Y!<$Md3RdSzmoe$RJ3x+}TJACYt`u+M?j%@n3ioW(qw7YsakH@4 zxqOB9sJoSfIaog^I2C%t%XT|SRWCo894W)LbDTAyD++$Wk#vl>I`*l5Ljp?r=G&r~ zIOBew0@3?+RHzq!t^~z06TtbxRC}RnzXNQZ)_WhuN4gYotUQ|nD8=`lp~T$I)S|MW z-15|F+M+GV(}Dm*dumbK@AG4XBU(jD)=#IbJ!w3x^fXWStHJ{iiFdnyb-yO%wluj4 zUfw*I=y-drjD%le=Zf|G`FjE&TxW8ztgWPykn!| z>dl<#i=?@m$k0&L<-QA6%i$yG_DpFo5=*YI*h*gJlMW;_M%dReq$zNrV1VnoW;9Hp zg%dH}Nq<%uJa0A$*>r?E(?PI)ftqUrY52q3jWyYs9U!LdKd*zmT zorq|mY5K2+uHDVvErU~KBK+y;a2+}DQP#9~d!b?pGB9qeNZ;j;AYHT@xpOW7|KFYc zzJz-g_5L`Q(AkiqgTG#8{-bR~tt#D4?dSM+L zZ3{{fi8VjYW8P+#(SAjFJ2XQ!-+=!Bk|b>0Yfo;Vodl18v*aXP+AWJHkvW1n)Ix+43X_Pt*Mdh5@k~`x0i-v zs%q_Lx=|M|d8#C&r48Nh=W`M0?r0F|Wp~#G;S#squ7!K+S9)tEv}t{v)()oupQA8) zQile;s9^G!|G}8-L{L}^P1HHx@#XLPkN?86eTqx{eY6#P{!foc-M~2*GWdV=2w?~C zUEBqR;4Vs!z(WBNQF=s?O(G+z2;jKv=U9 zc#(|?Mg6*JD>M0Es+3x8rQR$wRh=yPm63%~Jj$3VVlW{-tqvwJIy}G=LVlq^ej#H( zW<+XWqXh$Yx~$fgF=h{i}SZ5#)pLkZu*u*_lJv#eVSs_6e3Q7`vp}sL+`j*}PGy|7VzYT%8=|2T^k@ zOGIV4>b#l6j5^CGYn+bG9d4~eY{tplbV>!%RjSw9b1j6o&-6JQW#jstH?}o#1a5my z@4RMgj#4F7C9*vFyvl333mMd@4+{hHzmZKPgS+!Sm3Bd0+K}xoYF-L}2+=}o3~DTZsNswh6C(=FpPydw1MPMGo<%c_?6jAA_A6Dz6x7f8fOr3S!yu_-VrS~>Lr-A4O z+(2~fjAoR7uG%={0q$!01FGV~S1#>bl}6nobSRz97)HUzKvO@?Npf5;4wXpVVU{p( z;>+H@~g1I(UqMbpapDHbK&)^VA%h93x3Twe+$Ka75v){d*}!r5TO4) zba;%=;J^j#K;GoKe%EJjL>rz5wLawvTV+RZ<%sO<>(7<#w>`;Qn?`jU+VXj zz7507OYNRz&-rJD&G){>A>EbuBtMnBO_N?hTxl4;OMRo&d8@c@=>4;uS;Wf(2 z19b7c6}>RHn||Xnrk39&UvtduT!0!_b)#bdSbb_@CZBXC=7hiV5=S@^Eu21hetuB% zBE!}_GZOYU6jWY&fE>w+I+@=8Hst#vnE4?R@IxucZgg~OG5(U7z2Q+mcoj{ktx={% zdQnz=S@`-TbbR50tuIHdpynyHxk~K;tLN`Q^(RT#3SS_?0lBe73*aC1 z^5W#$(z=bSL{gxu&LFCwr6*TOxOVC-mKp^g|S zcPK#rEIo>v{T{YJl0!E9%=fYE|TzlKK&>a^^KGx>>sq4hLDaJ!-jl+8r@ zH$p0_`E%Let1T%fk;6Dv9G5{_*mBOk{#5oUe186;F0WsiOD6eUv8U1v)u+8~gEqc* z_Z!eCBuFB0mHRkhD?Pi$aPp3{xqk=xUWK+<8?e(6l^(*q zjJ(31k^8j!{p%{83B7*VFRwF$9z*_2G>L4oJM3tXXQh!69-rQrhl2e{5Tq-1Wf%`T zd7g@nOEwg007)mwI{;{@iiOKg7w!H77c|b)xw1Rp<$WLI!a#14c{9;IuV%sh+;^lV z!lG|w3?yETE{Bcm%Q$l{4VUD-@-!wF^WM?Ckw3*fU>wZKlx@#)&zZv&+;luH!H#SH z{2u_Xa`BK)<>#y$y0`MdM#JDGrB{*fDny#4!-!6HLpn|VCCej-3f|zksQv?$H?fh< z>w1Rl+MGf1iq>WOoEIv8j%C%25a8Z{_4~=JxNQfIENoP@+0t(74z)fzVf=+^y-+T`A7I`m3WU1*TUg1y`*of-R}xu zeIai%gr0m|Q8#k?Tn3|~iZrs_Tl2-AS($q#jVOri|H2QZuYQI4G(7hmp_1Y|h|&_;&b- zh4LTUc*q{Uf@Cv_E`2DWFIt_rKRA6pXv*kbXq%6q z|huU@{WaDMyX_%M8~q4X@;DpL0a!q(waV*M01t(*Av!AT6Y;0kJd zsCk#$RoYbK>ba49;U%Vh5qGYk`79Z6qEn5~)HZv$9(bZV?mVt4`5~Cq+eTA#3a ztvuBHO~eo9s9$^C>UcO-xc&p?X*rcY>SH}$oNX=s>W~BYxBKa($V%d{UqTYr8DyP` z;kYZrrY12s?an!lHaa_Gd>54)Jy>o@jI7l#YxCb*zl5b;n8O;HwbvR|BH@4Mp_Vf3 z$HnpDD~VlXv?RNE9^KGLA?91ngf3C6 zHh-KJS;8JRgmZ?LUUbMWI*z$~AXkO+H!s?d{88eG2_jBC)3Vw{FX{y69-%gH`By9L zt@|2z3;{B_Q{>=Y`vaNnzZjF`uVmQDu8}*`Sofn*L z>#cBT>)maoWew$-(XAOd8zS=eNmmkm2d$?>-Xqcqs9@aH0<3Iv+X|?CYzax}C0jFayZZNMDc(FAK(t!uR(7IXZVC2f&dvZ>M) z6#ApnVqYC!;2}~~y4hn#1asNdWlCD97KHQ0d-hO?_rM^h>^=8$`k~Qx!=0oA8Tq@D1($2(4{g3=P+&|V&tyY< zDPC=l!KlhWi(>fXp-0_tJxLGfyMr4ChJ1=s16AkLz*E)6OU4qAJP{^8I_5C&GGRrw zeM@)Fsg%T1GtYpgU00J#G=5oKF?%SUVCkZc45x}iVJHrmEwqseTqdQF7C{Xw(Km%A zAXNR>N73poOWNZ8%zS%5Z2}z)+YO&SI=n%V``-RV75^8|N2s;KP_!u6nkJB@!&#|< zAGY4HD6OS(oObTLU^H8tU7kbIx*I8TVMZWkHRlP{a>^g$zc1zXUfa;+j5Nd(yEY+z z(9W8=!8SGc_En>}RSp#9}RY3+xZZqOe^jRghMC=IU?4>UFZI++@I zWjU^rU5yUK<2J5BD3hyM8kb}Jx<{ML;fN(N!00hLLE-| z%JBo`w{_5>Snyd` z+kDrHGQ5hyg*PGnNy{E9gN}43gPwP#*Z`SX5x&|#ODGE(p)@H9bXsh`!;x<(fC<1l zL;$i<1l2)U-|1?ZKxi}sOFnh_o`p$zZj$V1dWSm8Oh3FeORzR&agL=BkgSmtNLIPTnQ>DH%tc>-PAu$q&_ zTU8U*r$h&$O;_i89bVbou2!`xdzgNAz~1QvR z_3F32THekgi!Y|}JXY6i4T+o4!mS|@0muU9m1Wy^DqDhuhIF)KL~9~N%D#5|8lcQ5ip?m-a;ll zxi)qa(g!05Jb|V;HEEIex$bl~7MMZ#k_dQfw)C^V8~^|U;``ysOC~h&hg-b)F=oom zn}4B-JLk?E5(?HWhdG3h&TGHI+q}agHn?MnF0xR2YqZmXG?4NciADTS>SXHfy zl~1)E9x&!u&=~cXDjk!abFf+V4EpBTq&0lz4f1GbMdFv~C;Yl6i=arn`z(C*-El%9 zmdxIr+!2)}y1srb>9j>ydqrF4iD!9zCEaIuIE1w^j9Q&$w6@zU{0` z1lS@O`6xDe`&t#IiA9-LKzCMO(-IzJ;zDk!G z@aZK;pR_(^c@y$U^nR$BCv&vddVY{5O;OyiG=N4oQGb!On0%|jV?uLqRcbA!M*vC( z25Rd`{|`xDLiKw3XG|H2`TT$UvzU%~N#~7!BAwqbjZ1#V|Bp(KJu~aQ5_9OQom~za z_Yp^AsaJmYXf#{p%j9> zt#^Mf#|+;$4y4z#i$!-sP)30w$%aCc)(Wbc#BV{8X~FXDxVav6{dW&d0p`!kgPT`! zmGp}={{c8!z~nrNSEC-j1<3{W4ZO7-y&&#+h?$2W#Iwd>-($WEh?oU0p2uAfFYtUi zF8qQIAExNnMtxc`lR^x$hKTKXlj$5dk1j09cqbiv~aF>zYTpo7R(jp!o5w$s!D6#x&>ar zYX@Yt7sdjw7p}Du@C(s1k$} z&8K2xFsHm}rq)qed4#>lm}*K^qeK^4U{wT;IBdNWr*GD#XIWDQ1@9uh(sQ%r=|rl% zMZ|5i)wu9-X2~|*(RkDER(czr>l?3zB`(#=`*Xs3YUFkGiGuWL7hDH4QtWu4I>v|+AAppiwp;APbqLZmFu`p3%VVKC`0*Iy3bjY@~jxVI6l$Co|Pc$>N)Y`X!0KbSfX_g z)Oh=enby4Mcl#4L)9+tvP1VLPoxt!65}|Ej=CNp@-~*5Q_hxb9RkX~PW1A6 z)Qf7Cl53WlC|iScb6y1Nc6(*WpI z86Tg8QBFu#loL|T`-m%G-)Bp~yj{8I$vhj%FyZIgkZvtFbhy-`7|lp8LrdK|NuX6b zPG~IHo<7`TSlm&Ck;u}|Xcg($O|j~Fj3>B}X3Pk+7ED6K9_4I&ok(2DST>GcC;Ut> zAXC1pFXo@`L8tYirmpttkGNHs*d!+!A1&XN=~&Nx@KZ-v8#JWhl+b68ZiV_`i%GH9{WFSDG=j&A8F05?=br9l0nw#l1?ndTqIqrps`5{o&`<#l&2^&c6{^@U zyd-F7xwStZ8xqzSH*LwUm1CD=C2h46&Ti`DXo!|_1)W&+`P&;@9>@Ge+O0~8i-G=L z_NX$8Po^6WW1edDTvpk5@z?%`2~J4R`;MAV3sMNb;)*l#z|dC-@mDe|d0%!0$J1Y<23Tfk2a#dDm|2TH5`l^^KbB`G1o7f37$I zGEZOa8^*(!rwp53y0N1}Pssv4v_$Atcj-m(&^s+z;&9q`4-*+>dA_PMU~t&dC~#1j@TpOa~Jv?q#7eK<*5hD303D-h3ij zC7YB_<9ovg15`XvCdINOr5CSq=P(M%0&SKDj7&nNm<`$9#TLCqG(0Z><`1L^jr5eo z-v*`07dvgN1CL_z!YjOr?^PqonmE*Pb08TRXR%my98*q|9 zQW|xIb^uwrY#3~+op|kT?Uc8}xwf=0b2^s%qJ`2JZIvw#tB-a8CMAr=fJ|7|&VuYSNuF zDG)&(9tSpKMjBBKNA0Z&0>cO+XgQ*O2%$BWHZAg40UY|Hb6L%=Tm{@ZPEi0TbG(Gw z|8_6P9pfmyV+9^jiGa~n+9S>&h=Yk#U%-*;%(ma*#2K|;^+wVsbl=)liM8N32|r+D zXcJxvXQsVQhJB%0rvPWnBu_RHr^pz)yFQoSCG;;0+DF2=z`(&py4F!eWRj`pvo0Pp zYT+^ww4Yv23m*7$w=gxj`5r|R%%nR+6r;32#8FU8aGK9v0f(&docdq#D*b0OUQKrJ=M&-SZh2z9oJuk_?R)N8V>6J=8Mut7P^G?9h^ zEuyoaHu^gw7h@IuImd)9ycB6;PXv=YchGVR#u5=H!!OWru0JdyZWaM(`3R3FbgN|E zuh`6)bBf?+%rnwb8UQs9@Dovj;$Fl&DZHU7#k!;_n_xuP%m>MeoXL_>16Zr^fO*Pau@2@b!Z0uElCO6V?-Gc+)?(%FVJzD z;xOWyz!k$corNn`wP=)9;4gsU&P^(p`304=RpUUlZ-)MIoy z5KtxZD~{|WSTJRXA6uBJVUE~Hj6gbLRnc(G*1q_-{h}$b>TA*g-Ct)k^v%H^mvmni z>y5r=82IZ1oGA>QSoSLC65R7=2**@n(E81K*(;_0;u%$6&em=(^q&O{?ZjU4skjjM z&8oYXd8fC(2hVXk3y`xB-q-${(3~?(EvrYbz4Z@WvzW`S;q2y?53;c$&{VBx9%X8j!YWs@b?mn3r_l>Gxme^0sVwa~$dPC-7)kX8F*W9_9`iW*OTNb z81n5ya-TBp?p>`B1GfQB(oS}!#MJ9!-EMkE^t!Gb+`@miuagy9;&qJaLqGdZ;WT;e z9LlS;yn!ix6eez0KWv5pzV-F9TYY0!o_`E%a@KD(NsOHJ7}vz+h6ln@j6Eiw#~6ID z$})D9Jb>OGuYz?feR8bdV0c155Z6BG-GhGY`1dm;p`Do?4NR(Zx-q@>je~Xe7EW{E zpdxpc!lqPgG)J5iDE`H!j3XS$9qv#W(e~R=B)4iSL0LAa8Of|;($;lFx%(6MM47Nh z{S8zxZ?^678Xl435Q7k%N($Zbt#8}|Vx$QamoD25IR3vu4ejJ$q)>^E{8DqG#75Gd;iA02zP~Cf}jb)xk-40prz-|ILd? z@P`EKH4)ABPnMxi+q!hG;^9!%D%yW=gg>D818THJfG#N+P|I*U=)JyT8L`YqHVj@? z0>m;}E;p5j19AyJ}?d} z3CywzaDi9}n=!mVB5zAeN(-#!U~J7G+w4aRI1JZXTG2DMzKXml33YC%S;vkj_wDC5 zkM7r~F#ar)9BD%9Y8(iqpIS@j;$6YF3g;hbU_H}}UYs;rA(mDP1S@~-skeq5V^WnWcW>-k^_>;0t`dt_2+!LhL%TgtI9Iz+s1$Yy0!Re48*lKgwkIU}HU zCQ<(8nZ1sh*rZvS3V7~DhEaFVuda@@zg)SVbLohHj3xlO>s9l^%j3IWOFx-~c5&LV z?kSUmiSNTet=G%4RDrK#&$g<`Ux`h6C~miX=t6{3unnuL&m0~0qMgT6i8ipEsDF^qQTugfbe7_jb-8SHs^b%+hfH??SdPuFoQ?!t# zL|}+JO-e8qhmu(mYNHe{9+>K-l!>3f`*}cn9l@QdqbyzEOxROVI}~dTBIVG<#xI2r zXhI>BP9YM-$*XaY$HGwDS1Z=FY~vPC#?ZHuvtg_RRB9|Rw(?p;EVvyETPTDYK>V%2 zZ8J@LfyYXhOd+^`EhCTygw_j%076cfj6ESfpmaP8vnM+-Z@PV)G`|6O1zH?ej)!{e zp7MY#@_#exys^T+85O~hq>ylcb0Bt+rn%vXT|+$Owwff+5iNCU;#fmcnE=jeT> z?)y%$3kn!$nJG4eAXNVcnN?O!`-iQ5MduR5k*`Y;>{q50kTntVl7YU+ z4oT3^=F;P(9^=aAtq=!`MJ;I-0ff0B8q#%W0Q1mlXGi&B*0)_Ja8C=(JHtch{->mR zQPg5Xo@K5nZSrXM+p4DZ1fD#NZW5}g3>$`wszy~*DWc3XzeMaQ(~d-l!pCv z$qdo=t3EZREEO=z2&ascGBY#XrfSA|*SQ$XD)rVuy>N+-db^O7#)uT3 zopum@029zCvxSb7iU=Z77ZC54tq47}n2YNaGojD8q&{N+skc{>2JRed4h$A1xGaj* zV_}2DlNm=KhKYnT{tk5U3jW%Plozb+SWPnUlUMwvy3ubi!b>d-B5c=Cc}?ZAX=8cG zDxFFcxe5(clwfb`S4MeqA>R%5O`P}BDMZ3|pgx#nbFX>p^X>>u3=ujI4q6<3r_*r+ z4!o*Te9h-JvTt@}!U>LC--+>|oZkymaMMhbs>P14Ox6jV)$vfiN71n^W9^U^n+QUB zI_skQY1H}+^1!bcJBT;C=vPNYGx7F5=t!|VjY%pQyX2~9lk=xr71i@oq$E{>DNg&z zOxmUWJ^ShBuJjev0?sWvXnEZg?L;L3n;t{R7FJnKF5l?90pv2TA!0-Jl+GVb%@o6;on(>jSXj&&ZnRZ@v&6AX(_`Cf`cDpU(Rk4;42 zRWIciWbb}J*!rJ%+-_#OsDA)wRUlVqnLRg+7A40jMi0v2!63GxbLs63yelFXbgk(f zN(+C0(OlUv4JNb0PTaH`hbd5|jtl7sqSX(o;MoYP@Kd83jZOm^r5#Wa+l4Mti9P84 z;!CO7gMgSw3thLGL;!^Y9_90ACVUTbZmh2+nMG!L1Sx7uq9!l>N_}hl1!S3;hU5r^ z#-%KR+4Tvyqn(KbQ>ONdvQC!N<|g(QR=F4xAeGw{YjVhq&qhq_1WeGCThAWC6O59H z%$b&XrXgiqz34IWV)3cp@t=jG+R^5~+X*<=Wd%7D|A2(x%hy$nvMw`ec8KLVQYC0E zLH3BvDMWn)ysollt7i_AdC$8zJ>G2q#OT~F&u-hIl{zxNv_Delm_IX(o>o}>1_0_u zbl%U%0_)9~|E@PHN5VLJe$WGye@6FrsQlIb95eouHNk; zY&FKyN&%K>d!(EN$a1A{tR~W>GOw0PDYhbt=cRTXTX~*6fE00C)6$IE&xe76Sceo~ zz}stA3zpzTQGpnEt^~6^Wy93u(-og@fQ1MP(zk%bCYV!5M+MnOi;BD_Qlt>6v2|K$ zXP8e7-b$ps(U?TB1#r@KpPc69Jzg9(*!qzrh}N7yEZU&Fdd-KE$%?(_x-Ei+cGN9a zipdKuUc9k-XKl7kD4oBS%#uiz(g1wK!UPVNB2tT|vf23}xjz{_(M5+iGy!d?h)=|d ze;6XQT}QhJ?4Va#nY52>7vZovfWTAsN!;>*H^)L{yrXEP&Mh#~5I}sQi?-I=0Jav9 z*v0}KuC6btn}7wiU*IT68-A%Gml1=mH<=hIDwKM1i3(5G&I{%Q?NlD(JBlU}pd&Wd zlyh)TFNX6zU*in97VK~#N@4_0u{X4a7lUO>+QbO*JoQfwp2>3vt;qTIvcS3>Nd5c{ z&73xWdUkSu_lbUc>1V}+nJl)1U7Bu*y|cu}p%BLlq6c4Z;+yab+29#coA??&^pOhWlwNMe9>M9TEtX+Rq*E zDu*HvJow78!nW0ZCDEn>CwUc{bu4c?Vu7;?6KzS5&F67tcNnVK1tn zWQ|>%T$LOdFii!+1zR9oihA`yQfOye(zWR?QC{b{faWwQ*Cj>`zMBN ztb8NBAV+aiq(>c!(x+>=EPVk&nx8szX9N2+5$(YCZz&0-+atW*3*R!K0#JVnFe5S&9Zt!1S&C9Qa+-CBzk2nN zGBlYL6DAfOjLquAcJC-+hPRRWjdwdYqN5<;GH4~BV?DP8mcf;CTB$Z4P!JE5RlK-&SX=VRf;h@tcN> zc7&>*49;T~L&!)wtkxk)ZF1vnMk6E@y`W{HH+9KxG|fp%-Ir&-iC8{N^Q!VsHwHbS zXsfg^Q1p<$&2W#-(h#MxZ=Z8z3 zJIemI4vMx%j1p71n7&raQ#H>yQ&L~t{NY*!@1X&KVtZhPS z2Pw3S=f^N^{&zcunrj33!-b}XhL^VrrIyMMtK~FCAcjKH6QwSM4)?09cY1>02br@< z-s#L&(KTo0cMY)UnK2XVrFcu?d98N*{BQEczgN158}kgg$dg~@=ut1|t)U4rB+@U} z#@-ffPEo|^q~b(?^D-+WTlW-jbYK3`3yc;|toeFdb=s9dQE8$lJpH*hbg#_dd`R-a zR^t=^D2gmwh2%JsP9ax_ikQ3W0~lJ;>dVEiK3RE;!8+TcC@UG#5?s))(U2e91u z|DaZ`BgF|wz)DH^AFAq%{BzXZmHVdSiwE@oCRz8@ly`Is0TS2TdllzDmFf=!naak5 z{WZ~NS_Ise(sE_ytYY+G?#LLCY;x+RibbQ@4M-uZ(+;sm*uwApwQNXi3UL$!wpSq? zb|Zu_`@FkaHe!vvDX&n(5h1jbIVYtA8LxPu0@z9vyOeOc%a>rP2@EGE%{d$Lbo3Fz zhzNOO=EQ~)Emk*Nf!|*bbwkH0b+$u@N;s4o!-@q)Ocyd&Kqd73IqiZ8!4!gbRB=wM zy{L`XCWV&@koFSQw8RtMRwYJ@OIXl?UrKPPjlp_a7jk~LDg7`rEzY%?!8o4}%dF5; zVAVj9Y3ZbWeHxX!No$%5*H45I!3E+qzeDI&#Rl1ZXz?%xlO>+i8l)n86Pz+o?dAWa zFI~35k&vmUOAq7DUO2qEF|Yr;HLK6y6T@BN+um0<+?RG@1M(soHDUg`kF?bAzGk}e zW-PoL~YM9)4LW~AkZRpF=p&uI=ip;BWA+Zo8WRd9aR?V@jVhcHmrUc(ce z)^j~o+iIemE9M$m0_Xb#foDFM2*VEX%LnuYFk##wy-d|lm#W~=Iyu_`_ZNkg zd4^>Enk1Y<%$t7Y#J4s57u9d+%i`9qOHig32I%3H*-57q>G{GEhPoPvhjP9($4+2)r_iqKR{wMjG<~Pid?gTn@ma+4!l- zjjIe*l1-9Hw6Bk{6Da*cYYo#xc}0ipB7hB%-=*J$LPl#>%9uz`1eVi<5iWuePT|x@;8Fx0# zV{`C$aY7%t=X3Edb&WpXmWpFhcBvsQ9GOaI#~P`dYs!)sIe~2R>+g)eN}mk9T}p0{ z|i1Hg^+)u%B2cRIN5Tkm(YG9j)-*}W%_uPXdlU6zQqk=?%dGR$dzTqFFU z?XmL1e&{8GzDD~mjF>G$VCev_RMi*tvqC2BL#<9ST8ZdMRn%6jsq?XbF7dSwUzIRN zia2Q#=!Izdl}_<^l}=%U&c`Y-Eg?is+D*y4%Dt@4y$fScH4Avw$T~F-1c)DScC0<3 zLSaoet;4)J!qmfYC&z#zltohk+?Dzp)weYfYwDgV`@E#<(+&pm6}eH;LyFvM-ZlB6 zsTL`)wZ^mSso#w=MEw@mWR>=T1J;?r{yCSpKehgVo_#KPzx`S=j^_>_aA8W_P7BY0 zi8V^-4ZSp^KqKwDs&Xwe=WwylPEeMAFv1GT9iv{T6HCX#^cJ|?sJu83!~(BZ+}EzX zBOE(jtE8aPfalJdGy@6$B4xijkAxpOs^*Sx!m+T~K1b)Y{+NYfW(2oTWuUIvEftZp zB{kUGOIP(csy>z^)MES4A-OD-7fiAKED!$NpEsWp@5t+t8Fa0sA?z3rDiJW_Z>weX zIR6c3Mmny&CA@RJiB0T@qgy9e?q$aG)Fkmx+>mF-ZQ~G9G=dr@@ZMqK9sP&GEPcuT9sFfV&b0||T zv0YD4P8?^Q7J13jOTJ$Ng*#$MKNn>ra}q5Oai8aANQ-YgJ;_F)*QO(f#Fh&3N>A zV~+Hd`0#hhzXjc9BTbhXKp`pJu7O}c0Hikq%gQUAmdngs9V|VMwG~{M3C5_?g=$(I zjCDCau@-cg{a{?rQpp^jETG1oNfu!M$OCP46jY}rqZ(22K^tcdb=R8|1!z|$J@x2g zZ!5mI9o#`|0&@UO%l?q4r9wj}wVb+y<;Bf0n7)4}4bW#XT>J&q(`8_1gL5js(^BOw z0I^}cRvS*XSp_RZ?82DXSU|H#GH>MQ;%f?ZHA%IV7^U2oD&(BA{mKqdb11uZ3MwB< zFjJ(kV_nK7h1!W2UCCSdRhYjThr&aJL?h`@tIUk=Zzg&5$ev8gVCa2j`;}riM#YI< zJX24Hbt$r4{OvQ4(M4w?wqI`k^H-wKBXs;T^y+oWV|gEpRYshbdKK;8$hVtbJePJU zZy>^sTY?j<9-4SrSAJ9Wmiuk!EmgCAjB0Sk@$N|uw!lG|G+Gxh=|K6>Qa!mr(43dj zF})087|&$>Q>RD3^+d;uBX?E$L~o-k;B`D(ahqE0M%z_BTZ-thiBn^gQ;WUfw@e)r zCtblpp0upvm#>vAEM9RWEoMw~rs5j)6dq}F%6gv3*&SW{0hN&;vHJ;5rj>Y$nlWp6 z5S||DIoIsA$Jl{3$!8o}K4%*D=kthvF?N!ev7p!4poKi(Zi%5f~4?eoekGpDBEruuc!;L9So055?ZKn{go5 zOlMSyyz-d;VRW_mdqiCC-453KKOpS4)GxJRB?_(^HE3B_a-@Ol)$;|X2{g}Z`qtXV zDrO%S+Qm@l`6~BY()W3&=nj>cwB;Bx-7gk+S^2cj6{(DC z+1Ou#x7u)dG(`lhjFCWV15hdLOVYQ5VRql3Nrf4rS{Lxfh!3ZaPtCJ@_`5vhso8b39Z~ve@`XKOcDA_y2tqDuHIvHQilDi#&EK= zcPI2QIh!`x!`n&R^yySxZb1+O=g+1??FG+lGHKEiMY~x|-y?3Zb!SSN`5mL;#`C%bn7yG4Ei~2?pV=t8JZxO)uKXn-7r7yk8 zmBvsLUwiVqjQ&)vWY66ZpPzLx(o0LVcw69{#6ONJaJO%{l;NA`u4<>d=~a-OxBei} z;eAdW5Cnt{O-WF^~8|=hK9dm z!w?A`A8)}~ zY2uO{@u$z%v=)LFJua7U%@B!0&}aM??djuA3f$B=vOA+DlDm|^liJ0#R_4nb_ycW5 za6Vbp{SwM~_Q~0L>tW&|R+P7LYc_8Qlm`e`%6R2aL(q|bY47X-VpmQ`@+TvN7DT(7 z^PlSLGM)BApXn3ATUk&YQczSb&{_9EV8yK`~Jp3n(13F=K2SM!Yg3{&@B z`O-L#uzk=OG$wjo=W$3a$}ofSg}##u^{Ztx@2KZSVd8XFtaYJiMOHixJ8I&sAoXRt zr-AY=vm)7y3CtI~P`>T(9i1@B`-dI0U)y%nKLTkV<6DF*TbnrP^C!aOCR&jy2~WRV zzml9x;>T>eW0(9iJ{1nCZL_7?ij_nB|iZV!26p6vl>o4ov7J`G$$jxC>^ zwIx`q7+fIdQdw0rcCNt4zvlpe=)U)!ckz)t(0^QUPg1s7(zjOM7yC{e^84WNsL8>5 zTsg`K*MBi>7?RSk2I&yNb4QhgNx zsoCTWA2XKH7*~4>Km@fSaxJpLvWg{NSyrAAE_N`hf)!P*#pkg&YjZSpzuHfza=}8< zwNl_|#%`mB>Z3lK!&_15knz`5ys!y+X=Y8Eg`y)>i-b&IN&KSugX_W>#Gc!dz8>aa z7ie8-7!zxKV4^dIG9}9LJ}-?Ebwx_W8al>L=Yk?KUiFF}Y(k4@GCaAtM4r+(AH3K>fsSP=IQ6UzUV#AJ92;+5M7D+YYB2McO0G zv%ZxT%IoskC?`cJ!^w=D&i@8>Pn^$$-0XORxKF7-L9p$f*7BQO`=_UfemdQ*h6?LXw zN{G=>$}Ns=fphypD5}<0#bUY0jq+0loynD!;rd~IJpy)K){ruT92=dB4QZA_!@{w0 zM!A4RuMUQvI@NBZvHnYsourizFMq<1JkLf*%N@VqI`-F{vd4WIVu&jg-^tP45?-N1 zPjMUa6sgWt)|VA$q4b!ZZy$L0qEKNz;)pj!`9&PBX1Q6`C5XLyHvN84h5FNy7kNhu z#WH;U)W_t$c&UrE>>H#;L2xiA%)lzHKrG7t=!AJFjBc+wPw_|i3-saXG{KrdAWv7x zC-jobr0uO%7>^@N>t!)LE|GERb)!kdKCcO9Ogb*~qxE6zs95@2Yqb7M(Eu?+wdnBfH6SI>*p@!J;PTQ=QB7rEMYOO;ob)jLs^-OU) zq>Jm*8+iy;s%SNV(naNU#V$s}UOX$S!RaI?W2NggS)B~>p{AdbU;`_JBdA|3GK4!D zPH*rB6usc1dWiO*t9P^FyR2FqI`y35LsIw1I4fc^?%NBWA!8pmp*H`w341Tb#wzBh z!5w#mgA(W%_z2~t^ED+_MF#zf>ZfdBK}}~Jga}@xc1}HrNePm4+lB3!D$O+Mu=jMU zfYLomSam|`rKS?i&yaQP8b&s^uT`Ll1Djv-cvduvf8cEFB06>DlS!O0*9W+dK$5JCPalHpEatzj?dNT4!xlzmo;g;MsBCjeC8j9Pe$mm)Zie#W}B2_2c z2cyS2N_Jxi!Yv9|RZ26ENR^CU`5giH`xg@n^GvaZ$Zc`~;IE$fkhmgyeP0^hN}Yrk z`$zUtQLB$5gkuUbt@#cE`2JuP%l%z2f}SL&ZRWY%etD%W4bOJ5AFeb!OvCwiUhuy4 ze@~9TnK3=QBI4NQqz;|~m>ZKES0OtMscjNINxekMN|El`>SX%0Z`648j!htXOzGyN z73gFY@8){CDf1vLHUuSo6t(sWfrUJ{g0b=rb3$W${yH7aTAwnqeu63DQ7mJk&T_zm z)1q83_q5)%%i)#KQmn-s?(7agEFw(0uPO!M3&5ZiI!c5*PwZlp+4O^!K#Qn?LHjEo zWOo$Vw~Dw;kr!Uu4Hv0}>v4~k^2J3qvb?8kaZ(;%XOEknqXz94mb|y2NvnDdv80g{ zWIv&*Mzb`D2kY7ewb;7&>jlla3|DQASp^O1y_adK1@u5_N%=QEM0?^YSGv3OVSHq~ zuEot@fI>C8la|tkJTV+;M4sJj`UuAPSkD{`3U%e?J4Gc4TEUd{QLFM7Tb00OPX3EaXr-h0%_MWAt!Wg6^nN7j zM<}O!Lft355lB*^oU6SANAiwpwt7~Yd_PjkSj`NdB%$<;)t2k*8?v5+m5zH_*!Uyo z9BFApsEx}@a!~xfB)y-tSQ{!!-s0>r%qjRR&cQ#eE5j=L&5fYzkoV26w6weqn~E?S zOg@|fXkV0J-m8~tf;vi=t@1!>{d107nSGu$LKBQb&cP4l)mR7{jRM(d1g`Et{s){G z$zEK~!=_0S z#xLSA=6F}dBJS)6x=5($CLq?JVTf!;hV6WXNT7Nu!XFmiO!$-~qQ4NtNN!FR!?`x; zYS>&Hf-sxvmv|WLR}UL^RQBDo!x&Y`(M5(ZcEn+N4B|Q-aeTzuYWkL!c={gfpT0`A$^O`p&(_LSb|8`8 z**UeVh*=mymG_cW8qVv%G;Ug#o`>?k3nC~N=*bK}!F{PJ+KI`e!Vi9`(c$A})Z({i z$;@FlO?$r(DG|d7N%RQ-7cO5?Es|t#YX7j1D$lI%ip6!Q0t2dAFD(cxtGTl zluf^ngw#toNUnC8Y;As5;IrR2lm$f`Xc{-+EBtCbk zd^cJ3f%_%giBJ(`H7Vt`O7SPgoZ(?&#Ameh{Q(K-a`v{)WbBfjAa*%Y0fR_i>D*J-h(0cje@Nnlx8hTAx8 zOucWT+xc8oe0#oyC3qeF!)GJ?ZA4thOy3a5ZhowQNyTPfZI{gC(9R$_*%LIyn! z;YdkSjNYDY7-`OF8TNhZi*|p}n($!^VXuqrbm(n=W{<7B9*U-C|q5^v@+cHY6to zs~wD?;;iYN#I4tU!eDW4C!cKVXbX@}bKg%gywSt0G)-cR@H;f@w1svoelB-qIMt_+ z(+PUFIwhtk=9s@!o3BKHgpd8^>%9hYNAQNQa=xumop)T#fG5ElX|wALKeT+vPtYw$ zU$h*8RVi3V7I?1Td}5?{tysI7I!vn9P;SMpx6j&FIfmzD&!@A)sP(&;@#oV=qS9LW zJ~2P7SV_TbXRP<9?7YU54-N2dL^j$vDY~b&Ic8Y#y!upHnQ#0E54rGCyCg-nlhjd^ zjNJ09UKn}R{s^(Mzf{3Y)h=g}Gccs1bL(IZbGDkkBggtdCNRXyLCYHtVH%hQ7Z6UI z$aCnaf)oq5P4QpHZdmNJfD>&^e@?zjou`G-2;3fm{fuk-SfH4cH6idYsL3U_52*K3=10tj#Rl)l&knuvIcSPpdfS@@zjU+z;Z(Q zK&T|$^0U#q6w{DhIU^yAcEnJRtPA4`C)Dk<;z*Ipjq*1P1<0DUBnpTjdu*$OY4PXt zrK-nxyI$2;OPjH*G|C^{1?Z&+HIX)^vgE(Iu}d=I@avZFe-`3J5`6pGo7*@?lO73l zT|VTJkhpM3@}SdSCLvvbs0e4mV2iLG1#jdndts?byG3LfIxKTWqD5zzWmY;XOhuQ` z<}KRDIUuuZOr9+0e6wA5u?;1}Q(!eDoA=v;_Du3GrGZ68%L{GwE=gU=Y%astE@HX} z!~yP645-(egbC>(A)%z0z5+3hz3AdplYy=1=y(M|z1P<52s4mm!;)#8b5$Lf9?mC# zQ5KDtytIEPjII7>+{J_*_F(o+l6taQb#hS_BF;jNZDj^P7a8b zPo_5dJC0?)Od}rcXo&~uRcW$sf7}2pS(M?;q0~HSTqqm{o^;7&w@v5q2e%a>A!d%Q zJr+775Hn11jyO!c$fV}fL~D9Cy+d=B{2!JXw~zS0^Tw05X8(C7L&1 z9g|f@3|A2yt7D-VrGgX$vBC*cYicy(*rCyDZtjZ96|1D;x?Hb$##O+r83*tJN&gxd z^q*=j9Py?T+US`$&A;=xUhg0gmGg`Ql|Unvd{WfRC}2=KifIl}v5*l`(TcBU?4pdf z6}lH|u(7^VVs4fhEj40@LG;i|gE7L7mKSLZ6pBRectbVWBaYafEXrCi@%cArd-!!M zKU=7=B@zr7y>51peU6!;N4&n~4sWvJUM3T4vx#e}ti{J9v}@E;#hdXT`E<5M!rKhR z*)nxXO5}$+X`!NMkkt}LF#G=Ab#>(LAD2RlUBFUEM=+r=y5`d8Pc&yBp%FeyGReIS zdr>?4Nv&G}GRM0%H2<@F-cG5WO03w2$j;z%!vZ(4qsY7I-N2m&LFE9<(fY>0iMB7@ z`sj3G-udI1Cdp_*x9B{H)f_fN$40+y+P!WfxA%$SZA@dwCGMy=XpL0Wm0^vrz$Orm zeY;7Bn&|spCRff25^>#bNwt8i$VGP0f#ACdS&o*ROJhvZHuH?|6D>9awAKFlK~z!1 zV%I!XozM2{K=Q}A;iXAZY-WYQ=0a`CMve?AAPNhZ?5~NDT(kPQJGv_gMw|-+l}`koezkhv0YTN(NUD)}l_- ze<$L0BQ0E;j?X2(vrq^BTxhwRkm0?^f&HS{`SKXOWU}+b49t~(cMig%=RI;#3@1GO z1D=NoJHLY1rj8KLHaqp=TfKh}*hUR$KT5bibO|C`O9n$TU((*IJD7MA@9!%eOvr@vb-+0h7!3=g10_f{j?V<5Uix%onJNLB_2zc9Fu?z9+Z zt}4+kix&1Ptcn(Gf_q0nUImDrBg#!3xMw?rnF)FJ?L_F8>d!5pS&a7bGi{I)C!i@d_8Yc-uPR_n?rBAvdTiZixJ0~3i1%tMC2{xy zV+AtFJnnMC(d)_u9LZ(LqWR-*URU=UKm~ls2xs$5f?m$Ky`j+2Pt2O;1t$?SXw7Pm z*{B0JAKHX5=@4`QlXRV}s8o`i`TZuL?_^TqHj6bVqHhqQw~L_~-||Hs%b}u~tju8j znC*LnOI$4_>}(af*my^IF}--JbA{Ab=X#tgq-BXBV&~es<2QB^LpENK;#@58sV-w0U|QnFsD*G0D;%Atb|F7=6+Qd$>DzEp>$tGT6?QDww*7^jY?pwOe4m7Rq9)^ zLR!h_|3#kuIRPy~cNLlgGa$Z>a8=km9;;gIcu3oh^>sx|uP&>m`lxG_wHOY&sorz( zBCE7+ZsSG5-&W4BT2T4SwCG&7g~*!@@EXXYld42w&$t`UIDMh%%H{c7R5LbK_WAd(u15_UR#&Mt3#nl~VLzU;;$O z)=`7E%g&2IBK7MJt1Q**7*~I|EuyxmNcrg81UgmM=(v^OtQtyL!$a4?;D*qX-Gimljz$aQF!dnzui;~8D8U#NHB8=y=tBtZ z6|st<$T-c7r0<#9*RX|9VqCBSvK`7_2@5RdrskABfp}_*(M^L@P z9FRG9`?f>zK1>#HuFc&P`I0eoi~e9ROeH3*CNS=W%G^>_$fOUO4wogpJHZKp7#m+F z3yqOcvD?5oP04m$Vb!0LTpC$2=rACsu^;Ko&ey|eQD&6uz6htU7F}vgEgpn%?pq|@ryZ3H z;09~gt?Fbs3fZ(i0TWI;C5|u}w7ljuDu{C=a?h$=+X1aVEbo?fx#uDA>yf zdI|m|w9%ie?N);#xXTvl-dD!^kwGO*+`3c=qevk`9L|Z&U#T9Me!5=hL=L+7`1RoD zU21&P4D0ocMwXzu`-FRs9Q>~a`FE7Q-3c!hXs$0w z{(y+grRKmt==l$Z?;N&w{(yKA9!?4V4<8G^SLqam+>buK zI8Xn1kM!4E$wyNSk^v1;7E_u(AZE)ypzY)D5Vi+K4r&A-N8?~<8Q2B?zgFT7n3E7p zr~Cn>@CKjjUJ(%n2=E`oNRavVCS)ki{QmUU{6&R3(;YpQC&q90g1CP`B<4z5D$9d< z5^sVdBt%{bXgtFHT2f;E{z(Ka7dWKj2@kG-Pl(Ls<^ z&b_YU2n}$vHTwU6DD2`W5`O*q>(_!uuJh`k6AHkuh5!7@&Tc}0v_E(DaX9!2^}p?Q zyVKvH{w4XJaQgx$)YeHx+8+Rl1U3KH^A$7w`Q-WcVL)RSfVzWT$l<_{_Ra5fo`itN zeDS3A*X?j1oGZ@jBzH0Y>tC;{DZ&MOq@htLgST|= zW)kjF{?isk2~ywL;K~2D&%PhAx~bykN^TkY&d^-^?X*lFs;vWg&ny3P`7g*{LerFJ zC2TUb^y3ZN=Oiy&0MBLipg(%%z14Wk4*xsa%PY!9L+)$e5&JH@cnk$zS0Df0dHpS- z6SZ>d8`jziWU0rlJzzKuJgGmPDuttyv_O9Hy@*F?4H3#(4Jv}NA_00>0G#MO{=Cm` zWH10Zm@pR^))PR^`_1vaCn2t_w^ia-j`bf99iZ*kQqmZBw3yl^Tz2)M-Ia?I=05S=2LdUTlj21B6W21l5d2p^4uBIFJ6f{6=f(C zFY0GJyCGNM$Z4hy7biih43C!b9+f+e2W$N*a@_25#VJA6m_&o56Rn2C7ot;mWU|&| zKjq$+DC$g6OIWDUuab@`ymBydxFBUEGV^Iq)>JAQaBAb^AgDBh^pDJ*dFmFY>>^C5 z+Qth^d)U|4i45|5C0v5y#f=bQuB)+8pb_ERCPBxN1LjS~ z+`1!}*BcQiC=Jn| zZVoI7jIrBkj1pS%GgRLjyxB{*lcVfFG)9dVWY2VID^6MMtzn#^A~%)vDgpNWmG>Ij z4(O>bSh7%;FWe7xw_qpHGsi~NgqEhMVc4%(TI5%DU?p`b6rUXoed2SE44vmTk@GXj zYkni0e;y$L)Cz`HSpgBky zWmw%$k(MVWVjOrW5fb=)Y!56iwPmiS&$Q&%F6(@9OpYR~QM=}Diq3Wbt6+WOs2Htk zII0#M`%vEU%5`f^I`&d~4)VHSRHrJEVWnK|D`Z-o6d=<&(r{6HO`%b%df)!RBWP!( zY^ZQwXH!5!7m&67yMP~T;S-AWmyv6TBPwPl2hWs&9n_omuyo* z%|ot0JKs-=s;S(esg~9Sb~UkQbCxf$8Z`5+`dn%sPI`xXeD$m++=X7(cz$PhG^7}_ zaoY4M9T&@BdSG}ycdi!8&w(&fDotP0XCxsspEBiuWQX9;a7j?aUdR`EwzlOWP-Yy3@hOhd8E>TNu0LsNwCL#jWd|^TuvJ~(gkF4DX|`e3 z;G7&7MIWuOl2Gm8^Kdv*;tVab;#a{rG)O0_O-)1_1JX)Ys zBl!H40MjQTKeI>e^gVbt)Z3nF$nNcyU? zofR594paHtCVtHHltSKe6Yq=%UvQ%dvQs{mWt!t#gQMcWd@UaUW z8NFxT^F)J}lbqi9UEtc06|m=`8uTc*TjiO3>gl0WT|mN*8~LC32($mm&}q|;em8?m zbEi1^^X4x#!Z#zXSi5!rVm$UY_2B;xf8ql>^|5#K6O&{#D{KiLGnfLu-%3+J*oJ}_ z&YM&h1kF*u_LUo~ZK!sJi77Z^D)t3-pekzmj!`~*d7qDTR}i8Qw0WZt0k2Q;{7xtTzyG?_=Wcp-T>ZvMm^+8TS4 zg+5@BqUHcH9T%-7K2v~-_a)#sk^&R!--_Pm;hZby;9?&m2N+BMPncDUP;cr0Y2^QF|&>()5i(j^>pNq4B{cbd&0%l<&6jYdq-+x zdmhykKR-Vg@sQl2SmFsj90qC#>G;5@OhI3SnI%;h`be^!z!N<5sygWp$P93-fZApc zfgJtG*=fzw@f$ioFTueu+5wdFJ}6>$$|!TZf;yluHgyqc9q-0B|yiR*y^@Sy?wv}?qcwj z98w$t55RiTgE(z~rZ|*1ytSJC5wkaNNdtj>bHJpUndn;jn1JfrC^YUHpg;55_pd(} zhtJxgH|P$0tqtt1iT_mxvuTahkC9D_X$&iT5SQO2K7g)jVBEn3EIyMidHko{ut1J& zuRowHYeTLhP9~r_NKuS4o- z9WWYs_-6SxqKYORieLINBuRguzNYke|9b@&){@e1#u{((BB~{*=X#Ij4~Rey+_yoC zHU)gr1?a9H-mppbmrc$oQJyUB@ZAHPCe5R#c1f=-KWmv%&4gd?XY%{H>baKw9W)X& zXve*Y;-Xa_m&P^D%2EjI54=xI_!oWRb%W;Gh~~$(aG&FAJTkyKMt+O_9Y1r|(yqVp-eA7T$x73okv(q}1zy3kMj+Zn3C{lYa!Kmh-Xg~U7I z!3-MCJnF@i^_mhIcw{DXY`8ORtO{kpuSGw({(ANus^fy}J_+VF3x?Sief3Sl(cn_f zci87-G{R9WuERjPN>b1TJc|M%6Xk2zG7wMRVADU(Yv~aV7gy}S^YzsK_Vx47KQH3G z57ZaR-km9gMErg?!g8m(JpKLP#CR&h{vG=y=eZft6Gk2l%3aadPYqjB7Rm3<8Zu~p zVH;=k<#FKUc2ZXvKYiK-R;OzGujgJH$_kvGd}$FpY30$Sg8Y!cKX)>wYQ{F%sdyj+ z954BV*F$Www(09?5Eo=LtSNa#p3UP-=1dox2A;3w`NW5VJ7bFJOEV}VN3#se;#&Bf ztZ`Pcf5zNH5eUPnm^$Lzm4xSJuF5lY7iIZG-T!8THz^MxfS^m%80P{kljOuzQ}Mu9 ziIApw|4ECn=I{>Isj}Szlo&*4;4?Dh-MDvZa;sQd-RwCaj z<&iD_40@NF;skWpEUqz`!qbErv(RE}Poo^#xCUxbCw#7$J0aBQ1Q;2eo?E}o8J==V z{*FO|K%(uK6zrm-=vU-$>HlYcH8!i7Bu95wyg8FH-2#K`5&b^ zrMV%%f+|(+TM+->X=_eD=uNY5JIfrRW z(OL5NKZERYU|(JEkuPu{MgJAh!Rvv(={CG8^Cc(6dd{Pzm8aC)mA0jRaPOSm`r+Ri z`?sh3|EbNe5r5R$pZVe0y85@E`eW`)b2r7qy~i7aj?1Xz-R-(C`(T`QPVR?y8`ziM z0M$sDDj&?d&N%3|e=U6duT>vd+Jq*?icGWLvNPakV1Qzmrkt7N(d@$yfI}(=8pDBA zcx~NbkSZTLu8-dD?16O~V}JbyP(gZ`d**q|$ytxNtb!wQUq-7Ryr+4qtHykdkoDg- zpiAZet2V0-K)(F%LQu)KKrhk!l#$hC18%OncDx_GxAC`qZLoh0a`d8nwu+VU{fIhQ z+%0C6S%}2rt~=8Y$!YyweAw#bcQGMg)nzL1A-~(=cv~s3M$7{Vm~d`M-oRC0b?M>e z_LtH7KeYdt>yiJ%_3Nbi*N1>6Pm1TLSQR7x!>x3Jt=&P8KOA-EE&XlF!hWRD{m*aE z*5p?o-#13-tz5!e@hU3y>Gav(>h<3mm{fm!mM-0J&dA>Co7BaeblpQ;8p}L@4LXKV lJQ@b0X<#%BjHZFnG%%V5M$^D(8W>FjqiJBsqyfhNHvuvel->XU diff --git a/tests/assets/multilabel_classification/images/train/Slide7.jpg b/tests/assets/multilabel_classification/images/train/Slide7.jpg deleted file mode 100644 index fd1226dd93469ea0e06008e416a078b58e847b77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75183 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 a83Qr~WDLj{kTD=*K*qrTkpY$MPyYeCdp55C diff --git a/tests/assets/multilabel_classification/images/train/Slide9.jpg b/tests/assets/multilabel_classification/images/train/Slide9.jpg deleted file mode 100644 index 5aceab571d96bb843aed480ab43b6506016e3b3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84714 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 diff --git a/tests/assets/multilabel_classification/images/val/Slide10.jpg b/tests/assets/multilabel_classification/images/val/Slide10.jpg deleted file mode 100644 index c64bd55f666c83e02dfa9adab1ac69a7128ed427..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57545 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

          }rZ~+9) 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

          (=7UqlgU$ox^o+erVv*7_ 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 s({EkK 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 diff --git a/tests/assets/multilabel_classification/images/val/Slide3.jpg b/tests/assets/multilabel_classification/images/val/Slide3.jpg deleted file mode 100644 index d94b0ba1b96d8e8090fe839a4c7ba2e91718a93d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64358 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з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 De;*s1 diff --git a/tests/assets/multilabel_classification/images/val/Slide4.jpg b/tests/assets/multilabel_classification/images/val/Slide4.jpg deleted file mode 100644 index 2a070ef7f17187a9ce05e4b3486e7f2d8b082aff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66701 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 z 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 diff --git a/tests/assets/multilabel_classification/images/val/Slide7.jpg b/tests/assets/multilabel_classification/images/val/Slide7.jpg deleted file mode 100644 index fd1226dd93469ea0e06008e416a078b58e847b77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75183 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 a83Qr~WDLj{kTD=*K*qrTkpY$MPyYeCdp55C diff --git a/tests/assets/multilabel_classification/images/val/Slide9.jpg b/tests/assets/multilabel_classification/images/val/Slide9.jpg deleted file mode 100644 index 5aceab571d96bb843aed480ab43b6506016e3b3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84714 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 A1^@s6 literal 0 HcmV?d00001 diff --git a/tests/assets/small_objects/images/test/sample_1.jpg b/tests/assets/small_objects/images/test/sample_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2de8d1fb92c648ed26c3bdf07021b2f030d90a51 GIT binary patch literal 282541 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

          $0|O2)+B>*9O$s{Bytq4zeI#kC!M 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

            wWok`OVxiaC<11cuIo+UA5*nBM- z9d%i`MItJueXqif_$xYWw{wz{aFrTX;iUVz1BZ$vV#$x83EF)2H%n|^Wd$sHlj5Fs z=J)+r22YhUdHQs7H@|x zS#@sr=KhMJ5&6rHy}>A4hn5-Z(DH*{{|kneP|Q|L!+yjf84-iGs!U}N$`wWV!3;U^ ziv5J0&^fC) zA9lgr?Z!x86;jL@%8jwN@;Y{oTzigOkHggNzJ+N{r0bhbWzu;LyV5Tar=>AbXcP?F zF=#*M;_LkH-@+ScyW=-{#~IPfUQ4%BjS~dR?^nQ2*SN7o6{w3kSYJ1@Hqa<&c%f)^ z9JtrbSH068wAEkbSU)963kCKgK9dR`Da>CZh4M@V0fZXF2yH*K(VYHsXio0|^y=x( z&q@Dvc|FbZ`WX0iV}MmvrTCSBc(Q7qdeDzTbQp4p`v?e$!O#MUbtO{y9uXaBAi+TK z>^tfLi1F^FxzHcB5y$#LK)?IctJxsyJ3zSF^qkp-Jq)3(h6i~lCGStFVZA@4dhb# mxo;ol=L>aS=KaORL7$~m_8njm?gom2_ZJxEFW$$r((_NQ+1Z!? literal 0 HcmV?d00001 diff --git a/tests/assets/small_objects/images/train/sample_2.jpg b/tests/assets/small_objects/images/train/sample_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84577284fde9822cf81a7d8822d604aeaf163c39 GIT binary patch literal 244148 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%0DViW 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 OX<#%BAf*BJ|2F}5fg`E_ literal 0 HcmV?d00001 diff --git a/tests/assets/small_objects/images/train/sample_3.jpg b/tests/assets/small_objects/images/train/sample_3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6104e8fc2bd32ffc6f4d65c4056b006d33240584 GIT binary patch literal 297205 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
            VZRk$F)xJrR&Vo~z%Ztu%B90^Ct7Mfj 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 DWr503 literal 0 HcmV?d00001 diff --git a/tests/assets/small_objects/images/train/sample_6.jpg b/tests/assets/small_objects/images/train/sample_6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34b944b797ad2df6b2ac52856982b5621f614902 GIT binary patch literal 254231 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%0D&srq8>$F}@L$ApU0q+NO40hL!%J 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 W?E6tbOZs>GXq(kP?Ds61{`7ycNfV<0 literal 0 HcmV?d00001 diff --git a/tests/assets/small_objects/images/train/sample_9.jpg b/tests/assets/small_objects/images/train/sample_9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a911168246bf5c52cea7d1b0267440e6ec654ded GIT binary patch literal 283614 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 literal 0 HcmV?d00001 diff --git a/tests/assets/small_objects/images/val/sample_5.jpg b/tests/assets/small_objects/images/val/sample_5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a11ab03c2b9c44182fa71412562c8fedd7458455 GIT binary patch literal 210200 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⁢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 literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/1.jpg b/tests/assets/unlabeled_dataset/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/2.jpg b/tests/assets/unlabeled_dataset/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/3.jpg b/tests/assets/unlabeled_dataset/3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/4.jpg b/tests/assets/unlabeled_dataset/4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/5.jpg b/tests/assets/unlabeled_dataset/5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/6.jpg b/tests/assets/unlabeled_dataset/6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/a/0.jpg b/tests/assets/unlabeled_dataset/a/0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/a/1.jpg b/tests/assets/unlabeled_dataset/a/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/a/2.jpg b/tests/assets/unlabeled_dataset/a/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/a/3.jpg b/tests/assets/unlabeled_dataset/a/3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/a/4.jpg b/tests/assets/unlabeled_dataset/a/4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/a/5.jpg b/tests/assets/unlabeled_dataset/a/5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/a/6.jpg b/tests/assets/unlabeled_dataset/a/6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d28e0c15e09d6a84d9adf911075171c481c09ac GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI z_1g6tH*YuS~;l_iU%Emz-M3agxa*3&!JXHM%@*3D@ k#CfcVET6$WhVa)d1|DWcVB|3iGT1YG;L=#sVE_Ln0Q-o|ng9R* literal 0 HcmV?d00001 diff --git a/tests/assets/unlabeled_dataset/unlabeled_file_list.txt b/tests/assets/unlabeled_dataset/unlabeled_file_list.txt new file mode 100644 index 00000000000..b4c985b7c18 --- /dev/null +++ b/tests/assets/unlabeled_dataset/unlabeled_file_list.txt @@ -0,0 +1,8 @@ +0.jpg +2.jpg +4.jpg +6.jpg +a/0.jpg +a/2.jpg +a/4.jpg +a/6.jpg \ No newline at end of file diff --git a/tests/assets/voc_dataset/voc_dataset1/Annotations/2007_000001.xml b/tests/assets/voc_dataset/voc_dataset1/Annotations/2007_000001.xml new file mode 100644 index 00000000000..04995b5736b --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/Annotations/2007_000001.xml @@ -0,0 +1,54 @@ + + + VOC2007 + 2007_000001.jpg + + 20 + 10 + 3 + + 1 + + cat + Unspecified + 1 + 0 + + 1 + 2 + 3 + 4 + + + + person + + 4 + 5 + 6 + 7 + + + head + + 5.5 + 6 + 7.5 + 8 + + + + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + + + diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Action/test.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Action/test.txt new file mode 100644 index 00000000000..c9fdc2510e1 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Action/test.txt @@ -0,0 +1 @@ +2007_000002 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Action/train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Action/train.txt new file mode 100644 index 00000000000..640b0d53ff2 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Action/train.txt @@ -0,0 +1 @@ +2007_000001 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Layout/test.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Layout/test.txt new file mode 100644 index 00000000000..c9fdc2510e1 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Layout/test.txt @@ -0,0 +1 @@ +2007_000002 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Layout/train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Layout/train.txt new file mode 100644 index 00000000000..640b0d53ff2 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Layout/train.txt @@ -0,0 +1 @@ +2007_000001 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/aeroplane_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/aeroplane_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/aeroplane_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/background_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/background_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/background_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bicycle_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bicycle_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bicycle_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bird_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bird_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bird_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/boat_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/boat_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/boat_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bottle_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bottle_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bottle_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bus_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bus_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/bus_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/car_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/car_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/car_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/cat_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/cat_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/cat_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/chair_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/chair_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/chair_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/cow_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/cow_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/cow_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/diningtable_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/diningtable_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/diningtable_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/dog_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/dog_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/dog_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/horse_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/horse_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/horse_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/ignored_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/ignored_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/ignored_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/motorbike_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/motorbike_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/motorbike_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/person_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/person_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/person_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/pottedplant_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/pottedplant_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/pottedplant_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/sheep_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/sheep_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/sheep_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/sofa_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/sofa_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/sofa_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/test.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/test.txt new file mode 100644 index 00000000000..c9fdc2510e1 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/test.txt @@ -0,0 +1 @@ +2007_000002 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/train.txt new file mode 100644 index 00000000000..640b0d53ff2 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/train.txt @@ -0,0 +1 @@ +2007_000001 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/train_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/train_train.txt new file mode 100644 index 00000000000..a3decd42ad8 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/train_train.txt @@ -0,0 +1 @@ +2007_000001 1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/tvmonitor_train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/tvmonitor_train.txt new file mode 100644 index 00000000000..d4385b69787 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Main/tvmonitor_train.txt @@ -0,0 +1 @@ +2007_000001 -1 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/test.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/test.txt new file mode 100644 index 00000000000..c9fdc2510e1 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/test.txt @@ -0,0 +1 @@ +2007_000002 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/train.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/train.txt new file mode 100644 index 00000000000..640b0d53ff2 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/train.txt @@ -0,0 +1 @@ +2007_000001 diff --git a/tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/val.txt b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/val.txt new file mode 100644 index 00000000000..c9fdc2510e1 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset1/ImageSets/Segmentation/val.txt @@ -0,0 +1 @@ +2007_000002 diff --git a/tests/assets/voc_dataset/voc_dataset1/JPEGImages/2007_000001.jpg b/tests/assets/voc_dataset/voc_dataset1/JPEGImages/2007_000001.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cd08aa30386f92affcf122981f4362858d8fe486 GIT binary patch literal 635 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO`~%g9tOD5(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA3iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO`~%g9tOD5(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA3tlYA(Ugba~44$rjF6*2Ung9%N8utJI literal 0 HcmV?d00001 diff --git a/tests/assets/voc_dataset/voc_dataset2/ImageSets/Main/train.txt b/tests/assets/voc_dataset/voc_dataset2/ImageSets/Main/train.txt new file mode 100644 index 00000000000..04844c25702 --- /dev/null +++ b/tests/assets/voc_dataset/voc_dataset2/ImageSets/Main/train.txt @@ -0,0 +1,3 @@ + +# a comment +2007_000001 diff --git a/tests/assets/voc_dataset/voc_dataset2/JPEGImages/2007_000001.jpg b/tests/assets/voc_dataset/voc_dataset2/JPEGImages/2007_000001.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cd08aa30386f92affcf122981f4362858d8fe486 GIT binary patch literal 635 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO`~%g9tOD5(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA3^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!v`*nMGf} tuple[tuple, SegDataEntity, SegBatchDataEntity]: - img_size = (32, 32) - fake_image = torch.zeros(size=(3, *img_size), dtype=torch.uint8).numpy() - fake_image_info = ImageInfo(img_idx=0, img_shape=img_size, ori_shape=img_size) - fake_masks = Mask(torch.randint(low=0, high=255, size=img_size, dtype=torch.uint8)) - # define data entity - single_data_entity = SegDataEntity(fake_image, fake_image_info, fake_masks) - batch_data_entity = SegBatchDataEntity( - 1, - [Image(data=torch.from_numpy(fake_image))], - [fake_image_info], - [fake_masks], - ) - batch_pred_data_entity = SegBatchPredEntity( - 1, - [Image(data=torch.from_numpy(fake_image))], - [fake_image_info], - [], - [fake_masks], - ) - - return single_data_entity, batch_pred_data_entity, batch_data_entity +def tmp_dir_path(request) -> Generator[Path, None, None]: + prefix = request.config.getoption("--test-workspace") + with TemporaryDirectory(prefix=prefix) as tmp_dir: + yield Path(tmp_dir) @pytest.fixture(autouse=True) -def fxt_clean_up_mem_cache() -> None: - """Clean up the mem-cache instance at the end of the test. +def set_default_tmp_path(tmp_dir_path: Path) -> Generator[None, None, None]: + origin_tmp_dir = os.environ.get("TMPDIR", None) + os.environ["TMPDIR"] = str(tmp_dir_path) + yield + if origin_tmp_dir is None: + del os.environ["TMPDIR"] + else: + os.environ["TMPDIR"] = origin_tmp_dir + + +@pytest.fixture(autouse=True, scope="session") +def manage_tm_config_for_testing(): + # check file existance both 'isip' and 'openvino_telemetry' if not, create it. + # and backup contents if exist + cfg_dir = os.path.join(os.path.expanduser("~"), "intel") + isip_path = os.path.join(cfg_dir, "isip") + otm_path = os.path.join(cfg_dir, "openvino_telemetry") + isip_exist = os.path.exists(isip_path) + otm_exist = os.path.exists(otm_path) + + created_cfg_dir = False + + if not os.path.exists(cfg_dir): + created_cfg_dir = True + os.makedirs(cfg_dir) + + isip_backup = None + + if not isip_exist: + with open(isip_path, "w") as f: + f.write("0") + else: + with open(isip_path, "r") as f: + isip_backup = f.read() + + otm_backup = None + if not otm_exist: + with open(otm_path, "w") as f: + f.write("0") + else: + with open(otm_path, "r") as f: + otm_backup = f.read() + + yield + + # restore or remove + if not isip_exist: + os.remove(isip_path) + else: + if isip_backup is not None: + with open(isip_path, "w") as f: + f.write(isip_backup) + + if not otm_exist: + os.remove(otm_path) + else: + if otm_backup is not None: + with open(otm_path, "w") as f: + f.write(otm_backup) + + if created_cfg_dir: + os.rmdir(cfg_dir) + + +@pytest.fixture(autouse=True, scope="session") +def init_mlflow_tracking(): + uri = os.environ.get("MLFLOW_TRACKING_SERVER_URI") + if uri is not None: + mlflow.set_tracking_uri(uri=uri) - It is required for everyone who tests model training pipeline. - See https://github.com/openvinotoolkit/training_extensions/actions/runs/7326689283/job/19952721142?pr=2749#step:5:3098 - """ yield - MemCacheHandlerSingleton.delete() -# TODO(Jaeguk): Add cpu param when OTX can run integration test parallelly for each task. # noqa: TD003 -@pytest.fixture(params=[pytest.param("gpu", marks=pytest.mark.gpu)]) -def fxt_accelerator(request: pytest.FixtureRequest) -> str: - return request.param +@pytest.fixture(scope="session") +def fxt_mlflow_client(): + uri = os.environ.get("MLFLOW_TRACKING_SERVER_URI") + return mlflow.MlflowClient(uri) if uri is not None else None diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/e2e/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/e2e/cli/__init__.py b/tests/e2e/cli/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/e2e/cli/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/e2e/cli/action/__init__.py b/tests/e2e/cli/action/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/e2e/cli/action/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/e2e/cli/action/test_action_classification.py b/tests/e2e/cli/action/test_action_classification.py new file mode 100644 index 00000000000..eca4096d7b1 --- /dev/null +++ b/tests/e2e/cli/action/test_action_classification.py @@ -0,0 +1,135 @@ +"""Tests for Action Classification Task with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os + +import pytest +import torch + +from otx.algorithms.common.utils.utils import is_xpu_available +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + otx_resume_testing, + get_template_dir, +) + +# Finetuning arguments +# TODO: Need to change sample dataset +args = { + "--train-data-roots": "tests/assets/cvat_dataset/action_classification/train", + "--val-data-roots": "tests/assets/cvat_dataset/action_classification/train", + "--test-data-roots": "tests/assets/cvat_dataset/action_classification/train", + "train_params": ["params", "--learning_parameters.num_iters", "2", "--learning_parameters.batch_size", "4"], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "4", + "--learning_parameters.batch_size", + "4", +] + +if is_xpu_available(): + pytest.skip("Action task is not supported on XPU", allow_module_level=True) + +otx_dir = os.getcwd() + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + os.path.join("src/otx/algorithms/action/configs", "classification", "x3d", "template.yaml") + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] +else: + templates = Registry("src/otx/algorithms/action").filter(task_type="ACTION_CLASSIFICATION").templates + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsOTXActionClassification: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls" + otx_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls" + if template.model_template_id == "Custom_Action_Classification_MoViNet": + pytest.xfail("Issue#2058: MoViNet inference fails in OV 2023.0") + if template.name == "X3D": + pytest.skip(reason="Issue#2435: exported X3D model showed 0.0 acc.") + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.2) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + if template.name == "MoViNet": + pytest.skip(reason="Issue#2058: MoViNet fails with OpenVINO inference occasionally") + tmp_dir_path = tmp_dir_path / "action_cls" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + if template.name == "MoViNet": + pytest.skip(reason="Issue#2058: MoViNet fails with OpenVINO inference occasionally") + tmp_dir_path = tmp_dir_path / "action_cls" + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls/test_multi_gpu" + args1 = copy.deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) diff --git a/tests/e2e/cli/action/test_action_detection.py b/tests/e2e/cli/action/test_action_detection.py new file mode 100644 index 00000000000..442bf60d878 --- /dev/null +++ b/tests/e2e/cli/action/test_action_detection.py @@ -0,0 +1,130 @@ +"""Tests for Action Detection Task with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os + +import pytest +import torch + +from otx.algorithms.common.utils.utils import is_xpu_available +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + otx_resume_testing, + get_template_dir, +) + +# Finetuning arguments +# TODO: Need to change sample dataset +args = { + "--train-data-roots": "tests/assets/cvat_dataset/action_detection/train", + "--val-data-roots": "tests/assets/cvat_dataset/action_detection/train", + "--test-data-roots": "tests/assets/cvat_dataset/action_detection/train", + "train_params": ["params", "--learning_parameters.num_iters", "2", "--learning_parameters.batch_size", "4"], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "4", + "--learning_parameters.batch_size", + "4", +] + +if is_xpu_available(): + pytest.skip("Action detection task is not supported on XPU", allow_module_level=True) + +otx_dir = os.getcwd() + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + os.path.join("src/otx/algorithms/action/configs", "detection", "x3d_fast_rcnn", "template.yaml") + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] +else: + templates = Registry("src/otx/algorithms/action").filter(task_type="ACTION_DETECTION").templates + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsOTXActionDetection: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + otx_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip(reason="Issue#2279: Exported action detection model shows 0.0 on a toy dataset") + def test_otx_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.2) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip(reason="Issue#2279: Exported action detection model shows 0.0 on a toy dataset") + def test_ptq_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip(reason="Issue#2279: Exported action detection model shows 0.0 on a toy dataset") + def test_ptq_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det/test_multi_gpu" + args1 = copy.deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) diff --git a/tests/e2e/cli/anomaly/__init__.py b/tests/e2e/cli/anomaly/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/e2e/cli/anomaly/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_padim/compressed_model.yml b/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_padim/compressed_model.yml new file mode 100644 index 00000000000..bf922ada0f0 --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_padim/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsAnomalyClassification: + nncf: + number_of_fakequantizers: 27 + ptq: + number_of_fakequantizers: 28 diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_padim/nncf/nncf_quantization.dot b/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_padim/nncf/nncf_quantization.dot new file mode 100644 index 00000000000..789824fcf95 --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_padim/nncf/nncf_quantization.dot @@ -0,0 +1,240 @@ +strict digraph { +"0 /nncf_model_input_0" [id=0, type=nncf_model_input]; +"1 AsymmetricQuantizer/asymmetric_quantize_0" [id=1, type=asymmetric_quantize]; +"2 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=2, type=symmetric_quantize]; +"3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" [id=3, type=conv2d]; +"4 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=4, type=batch_norm]; +"5 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" [id=5, type=relu_]; +"6 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=6, type=asymmetric_quantize]; +"7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" [id=7, type=max_pool2d]; +"8 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=8, type=symmetric_quantize]; +"9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=9, type=conv2d]; +"10 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=10, type=batch_norm]; +"11 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" [id=11, type=relu_]; +"12 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=12, type=asymmetric_quantize]; +"13 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=13, type=symmetric_quantize]; +"14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=14, type=conv2d]; +"15 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=15, type=batch_norm]; +"16 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=16, type=asymmetric_quantize]; +"17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" [id=17, type=__iadd__]; +"18 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" [id=18, type=relu_]; +"19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=19, type=asymmetric_quantize]; +"20 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=20, type=symmetric_quantize]; +"21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=21, type=conv2d]; +"22 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=22, type=batch_norm]; +"23 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" [id=23, type=relu_]; +"24 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=24, type=asymmetric_quantize]; +"25 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=25, type=symmetric_quantize]; +"26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=26, type=conv2d]; +"27 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=27, type=batch_norm]; +"28 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=28, type=asymmetric_quantize]; +"29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" [id=29, type=__iadd__]; +"30 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" [id=30, type=relu_]; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=31, type=asymmetric_quantize]; +"32 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=32, type=symmetric_quantize]; +"33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=33, type=conv2d]; +"34 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=34, type=batch_norm]; +"35 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" [id=35, type=relu_]; +"36 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=36, type=asymmetric_quantize]; +"37 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=37, type=symmetric_quantize]; +"38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=38, type=conv2d]; +"39 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=39, type=batch_norm]; +"40 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=40, type=asymmetric_quantize]; +"41 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=41, type=symmetric_quantize]; +"42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=42, type=conv2d]; +"43 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=43, type=batch_norm]; +"44 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=44, type=asymmetric_quantize]; +"45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" [id=45, type=__iadd__]; +"46 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" [id=46, type=relu_]; +"47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=47, type=asymmetric_quantize]; +"48 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=48, type=symmetric_quantize]; +"49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=49, type=conv2d]; +"50 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=50, type=batch_norm]; +"51 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" [id=51, type=relu_]; +"52 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=52, type=asymmetric_quantize]; +"53 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=53, type=symmetric_quantize]; +"54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=54, type=conv2d]; +"55 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=55, type=batch_norm]; +"56 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=56, type=asymmetric_quantize]; +"57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" [id=57, type=__iadd__]; +"58 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" [id=58, type=relu_]; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=59, type=asymmetric_quantize]; +"60 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=60, type=symmetric_quantize]; +"61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=61, type=conv2d]; +"62 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=62, type=batch_norm]; +"63 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" [id=63, type=relu_]; +"64 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=64, type=asymmetric_quantize]; +"65 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=65, type=symmetric_quantize]; +"66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=66, type=conv2d]; +"67 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=67, type=batch_norm]; +"68 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=68, type=asymmetric_quantize]; +"69 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=69, type=symmetric_quantize]; +"70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=70, type=conv2d]; +"71 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=71, type=batch_norm]; +"72 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=72, type=asymmetric_quantize]; +"73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" [id=73, type=__iadd__]; +"74 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" [id=74, type=relu_]; +"75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=75, type=asymmetric_quantize]; +"76 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=76, type=symmetric_quantize]; +"77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=77, type=conv2d]; +"78 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=78, type=batch_norm]; +"79 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" [id=79, type=relu_]; +"80 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=80, type=asymmetric_quantize]; +"81 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=81, type=symmetric_quantize]; +"82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=82, type=conv2d]; +"83 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=83, type=batch_norm]; +"84 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=84, type=asymmetric_quantize]; +"85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" [id=85, type=__iadd__]; +"86 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" [id=86, type=relu_]; +"87 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=87, type=asymmetric_quantize]; +"88 PadimLightning/PadimModel[model]/interpolate_0" [id=88, type=interpolate]; +"89 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_0" [id=89, type=asymmetric_quantize]; +"90 PadimLightning/PadimModel[model]/cat_0" [id=90, type=cat]; +"91 PadimLightning/PadimModel[model]/interpolate_1" [id=91, type=interpolate]; +"92 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_1" [id=92, type=asymmetric_quantize]; +"93 PadimLightning/PadimModel[model]/cat_1" [id=93, type=cat]; +"94 PadimLightning/PadimModel[model]/index_select_0" [id=94, type=index_select]; +"95 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_0" [id=95, type=reshape]; +"96 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" [id=96, type=__sub__]; +"97 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" [id=97, type=asymmetric_quantize]; +"98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0" [id=98, type=permute]; +"99 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/matmul_0" [id=99, type=matmul]; +"100 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" [id=100, type=asymmetric_quantize]; +"101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0" [id=101, type=__mul__]; +"102 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sum_0" [id=102, type=sum]; +"103 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_1" [id=103, type=permute]; +"104 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_1" [id=104, type=reshape]; +"105 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/clamp_0" [id=105, type=clamp]; +"106 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" [id=106, type=asymmetric_quantize]; +"107 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0" [id=107, type=sqrt]; +"108 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" [id=108, type=asymmetric_quantize]; +"109 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0" [id=109, type=interpolate]; +"110 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" [id=110, type=asymmetric_quantize]; +"111 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/pad_0" [id=111, type=pad]; +"112 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0" [id=112, type=conv2d]; +"113 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/view_0" [id=113, type=view]; +"114 /nncf_model_output_0" [id=114, type=nncf_model_output]; +"0 /nncf_model_input_0" -> "1 AsymmetricQuantizer/asymmetric_quantize_0"; +"1 AsymmetricQuantizer/asymmetric_quantize_0" -> "3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"2 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" -> "4 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"4 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "5 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0"; +"5 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" -> "6 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"6 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0"; +"7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"8 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "10 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"10 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "11 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0"; +"11 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" -> "12 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"12 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"13 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "15 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"15 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "16 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"16 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" -> "18 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0"; +"18 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" -> "19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"20 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "22 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"22 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "23 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0"; +"23 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" -> "24 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"24 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"25 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "27 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"27 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "28 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"28 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" -> "30 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0"; +"30 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" -> "31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "90 PadimLightning/PadimModel[model]/cat_0"; +"32 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "34 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"34 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "35 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0"; +"35 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" -> "36 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"36 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"37 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "39 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"39 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "40 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"40 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"41 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "43 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"43 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "44 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"44 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" -> "46 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0"; +"46 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" -> "47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"48 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "50 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"50 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "51 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0"; +"51 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" -> "52 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"52 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"53 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "55 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"55 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "56 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"56 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" -> "58 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0"; +"58 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" -> "59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "88 PadimLightning/PadimModel[model]/interpolate_0"; +"60 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "62 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"62 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "63 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0"; +"63 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" -> "64 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"64 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"65 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "67 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"67 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "68 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"68 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"69 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "71 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"71 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "72 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"72 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" -> "74 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0"; +"74 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" -> "75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"76 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "78 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"78 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "79 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0"; +"79 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" -> "80 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"80 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"81 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "83 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"83 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "84 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"84 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" -> "86 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0"; +"86 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" -> "87 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"87 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "91 PadimLightning/PadimModel[model]/interpolate_1"; +"88 PadimLightning/PadimModel[model]/interpolate_0" -> "89 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_0"; +"89 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_0" -> "90 PadimLightning/PadimModel[model]/cat_0"; +"90 PadimLightning/PadimModel[model]/cat_0" -> "93 PadimLightning/PadimModel[model]/cat_1"; +"91 PadimLightning/PadimModel[model]/interpolate_1" -> "92 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_1"; +"92 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_1" -> "93 PadimLightning/PadimModel[model]/cat_1"; +"93 PadimLightning/PadimModel[model]/cat_1" -> "94 PadimLightning/PadimModel[model]/index_select_0"; +"94 PadimLightning/PadimModel[model]/index_select_0" -> "95 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_0"; +"95 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_0" -> "96 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0"; +"96 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" -> "97 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0"; +"97 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" -> "98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0"; +"98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0" -> "99 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/matmul_0"; +"98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0" -> "101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0"; +"99 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/matmul_0" -> "100 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1"; +"100 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" -> "101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0"; +"101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0" -> "102 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sum_0"; +"102 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sum_0" -> "103 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_1"; +"103 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_1" -> "104 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_1"; +"104 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_1" -> "105 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/clamp_0"; +"105 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/clamp_0" -> "106 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2"; +"106 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" -> "107 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0"; +"107 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0" -> "108 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3"; +"108 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" -> "109 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0"; +"109 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0" -> "110 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4"; +"110 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" -> "111 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/pad_0"; +"111 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/pad_0" -> "112 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0"; +"112 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0" -> "113 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/view_0"; +"113 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/view_0" -> "114 /nncf_model_output_0"; +} diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_stfpm/compressed_model.yml b/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_stfpm/compressed_model.yml new file mode 100644 index 00000000000..6ec6d39b3a0 --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_stfpm/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsAnomalyClassification: + nncf: + number_of_fakequantizers: 55 + ptq: + number_of_fakequantizers: 71 diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_stfpm/nncf/nncf_quantization.dot b/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_stfpm/nncf/nncf_quantization.dot new file mode 100644 index 00000000000..ef13f90986f --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_classification_stfpm/nncf/nncf_quantization.dot @@ -0,0 +1,430 @@ +strict digraph { +"0 /nncf_model_input_0" [id=0, type=nncf_model_input]; +"1 AsymmetricQuantizer/asymmetric_quantize_0" [id=1, type=asymmetric_quantize]; +"2 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=2, type=symmetric_quantize]; +"3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" [id=3, type=conv2d]; +"4 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=4, type=batch_norm]; +"5 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" [id=5, type=relu_]; +"6 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=6, type=asymmetric_quantize]; +"7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" [id=7, type=max_pool2d]; +"8 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=8, type=symmetric_quantize]; +"9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=9, type=conv2d]; +"10 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=10, type=batch_norm]; +"11 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" [id=11, type=relu_]; +"12 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=12, type=asymmetric_quantize]; +"13 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=13, type=symmetric_quantize]; +"14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=14, type=conv2d]; +"15 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=15, type=batch_norm]; +"16 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=16, type=asymmetric_quantize]; +"17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" [id=17, type=__iadd__]; +"18 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" [id=18, type=relu_]; +"19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=19, type=asymmetric_quantize]; +"20 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=20, type=symmetric_quantize]; +"21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=21, type=conv2d]; +"22 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=22, type=batch_norm]; +"23 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" [id=23, type=relu_]; +"24 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=24, type=asymmetric_quantize]; +"25 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=25, type=symmetric_quantize]; +"26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=26, type=conv2d]; +"27 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=27, type=batch_norm]; +"28 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=28, type=asymmetric_quantize]; +"29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" [id=29, type=__iadd__]; +"30 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" [id=30, type=relu_]; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=31, type=asymmetric_quantize]; +"32 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=32, type=symmetric_quantize]; +"33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=33, type=conv2d]; +"34 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=34, type=batch_norm]; +"35 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" [id=35, type=relu_]; +"36 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=36, type=asymmetric_quantize]; +"37 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=37, type=symmetric_quantize]; +"38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=38, type=conv2d]; +"39 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=39, type=batch_norm]; +"40 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=40, type=asymmetric_quantize]; +"41 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=41, type=symmetric_quantize]; +"42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=42, type=conv2d]; +"43 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=43, type=batch_norm]; +"44 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=44, type=asymmetric_quantize]; +"45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" [id=45, type=__iadd__]; +"46 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" [id=46, type=relu_]; +"47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=47, type=asymmetric_quantize]; +"48 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=48, type=symmetric_quantize]; +"49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=49, type=conv2d]; +"50 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=50, type=batch_norm]; +"51 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" [id=51, type=relu_]; +"52 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=52, type=asymmetric_quantize]; +"53 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=53, type=symmetric_quantize]; +"54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=54, type=conv2d]; +"55 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=55, type=batch_norm]; +"56 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=56, type=asymmetric_quantize]; +"57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" [id=57, type=__iadd__]; +"58 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" [id=58, type=relu_]; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=59, type=asymmetric_quantize]; +"60 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=60, type=symmetric_quantize]; +"61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=61, type=conv2d]; +"62 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=62, type=batch_norm]; +"63 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" [id=63, type=relu_]; +"64 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=64, type=asymmetric_quantize]; +"65 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=65, type=symmetric_quantize]; +"66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=66, type=conv2d]; +"67 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=67, type=batch_norm]; +"68 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=68, type=asymmetric_quantize]; +"69 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=69, type=symmetric_quantize]; +"70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=70, type=conv2d]; +"71 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=71, type=batch_norm]; +"72 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=72, type=asymmetric_quantize]; +"73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" [id=73, type=__iadd__]; +"74 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" [id=74, type=relu_]; +"75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=75, type=asymmetric_quantize]; +"76 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=76, type=symmetric_quantize]; +"77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=77, type=conv2d]; +"78 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=78, type=batch_norm]; +"79 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" [id=79, type=relu_]; +"80 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=80, type=asymmetric_quantize]; +"81 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=81, type=symmetric_quantize]; +"82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=82, type=conv2d]; +"83 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=83, type=batch_norm]; +"84 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=84, type=asymmetric_quantize]; +"85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" [id=85, type=__iadd__]; +"86 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" [id=86, type=relu_]; +"87 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=87, type=asymmetric_quantize]; +"88 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=88, type=symmetric_quantize]; +"89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" [id=89, type=conv2d]; +"90 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=90, type=batch_norm]; +"91 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" [id=91, type=relu_]; +"92 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=92, type=asymmetric_quantize]; +"93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" [id=93, type=max_pool2d]; +"94 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=94, type=symmetric_quantize]; +"95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=95, type=conv2d]; +"96 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=96, type=batch_norm]; +"97 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" [id=97, type=relu_]; +"98 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=98, type=asymmetric_quantize]; +"99 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=99, type=symmetric_quantize]; +"100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=100, type=conv2d]; +"101 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=101, type=batch_norm]; +"102 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=102, type=asymmetric_quantize]; +"103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" [id=103, type=__iadd__]; +"104 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" [id=104, type=relu_]; +"105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=105, type=asymmetric_quantize]; +"106 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=106, type=symmetric_quantize]; +"107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=107, type=conv2d]; +"108 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=108, type=batch_norm]; +"109 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" [id=109, type=relu_]; +"110 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=110, type=asymmetric_quantize]; +"111 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=111, type=symmetric_quantize]; +"112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=112, type=conv2d]; +"113 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=113, type=batch_norm]; +"114 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=114, type=asymmetric_quantize]; +"115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" [id=115, type=__iadd__]; +"116 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" [id=116, type=relu_]; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=117, type=asymmetric_quantize]; +"118 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=118, type=symmetric_quantize]; +"119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=119, type=conv2d]; +"120 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=120, type=batch_norm]; +"121 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" [id=121, type=relu_]; +"122 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=122, type=asymmetric_quantize]; +"123 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=123, type=symmetric_quantize]; +"124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=124, type=conv2d]; +"125 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=125, type=batch_norm]; +"126 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=126, type=asymmetric_quantize]; +"127 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=127, type=symmetric_quantize]; +"128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=128, type=conv2d]; +"129 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=129, type=batch_norm]; +"130 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=130, type=asymmetric_quantize]; +"131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" [id=131, type=__iadd__]; +"132 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" [id=132, type=relu_]; +"133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=133, type=asymmetric_quantize]; +"134 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=134, type=symmetric_quantize]; +"135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=135, type=conv2d]; +"136 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=136, type=batch_norm]; +"137 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" [id=137, type=relu_]; +"138 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=138, type=asymmetric_quantize]; +"139 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=139, type=symmetric_quantize]; +"140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=140, type=conv2d]; +"141 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=141, type=batch_norm]; +"142 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=142, type=asymmetric_quantize]; +"143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" [id=143, type=__iadd__]; +"144 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" [id=144, type=relu_]; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=145, type=asymmetric_quantize]; +"146 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=146, type=symmetric_quantize]; +"147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=147, type=conv2d]; +"148 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=148, type=batch_norm]; +"149 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" [id=149, type=relu_]; +"150 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=150, type=asymmetric_quantize]; +"151 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=151, type=symmetric_quantize]; +"152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=152, type=conv2d]; +"153 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=153, type=batch_norm]; +"154 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=154, type=asymmetric_quantize]; +"155 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=155, type=symmetric_quantize]; +"156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=156, type=conv2d]; +"157 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=157, type=batch_norm]; +"158 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=158, type=asymmetric_quantize]; +"159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" [id=159, type=__iadd__]; +"160 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" [id=160, type=relu_]; +"161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=161, type=asymmetric_quantize]; +"162 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=162, type=symmetric_quantize]; +"163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=163, type=conv2d]; +"164 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=164, type=batch_norm]; +"165 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" [id=165, type=relu_]; +"166 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=166, type=asymmetric_quantize]; +"167 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=167, type=symmetric_quantize]; +"168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=168, type=conv2d]; +"169 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=169, type=batch_norm]; +"170 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=170, type=asymmetric_quantize]; +"171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" [id=171, type=__iadd__]; +"172 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" [id=172, type=relu_]; +"173 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=173, type=asymmetric_quantize]; +"174 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_0" [id=174, type=normalize]; +"175 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" [id=175, type=asymmetric_quantize]; +"176 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_1" [id=176, type=normalize]; +"177 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" [id=177, type=asymmetric_quantize]; +"178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" [id=178, type=__sub__]; +"179 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_0" [id=179, type=norm]; +"180 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___0" [id=180, type=__pow__]; +"181 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" [id=181, type=asymmetric_quantize]; +"182 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___0" [id=182, type=__rmul__]; +"183 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" [id=183, type=asymmetric_quantize]; +"184 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0" [id=184, type=interpolate]; +"185 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_2" [id=185, type=normalize]; +"186 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" [id=186, type=asymmetric_quantize]; +"187 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_3" [id=187, type=normalize]; +"188 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_5" [id=188, type=asymmetric_quantize]; +"189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1" [id=189, type=__sub__]; +"190 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_1" [id=190, type=norm]; +"191 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___1" [id=191, type=__pow__]; +"192 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_6" [id=192, type=asymmetric_quantize]; +"193 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___1" [id=193, type=__rmul__]; +"194 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_7" [id=194, type=asymmetric_quantize]; +"195 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_1" [id=195, type=interpolate]; +"196 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_4" [id=196, type=normalize]; +"197 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_8" [id=197, type=asymmetric_quantize]; +"198 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_5" [id=198, type=normalize]; +"199 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_9" [id=199, type=asymmetric_quantize]; +"200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2" [id=200, type=__sub__]; +"201 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_2" [id=201, type=norm]; +"202 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___2" [id=202, type=__pow__]; +"203 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_10" [id=203, type=asymmetric_quantize]; +"204 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___2" [id=204, type=__rmul__]; +"205 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_11" [id=205, type=asymmetric_quantize]; +"206 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_2" [id=206, type=interpolate]; +"0 /nncf_model_input_0" -> "1 AsymmetricQuantizer/asymmetric_quantize_0"; +"1 AsymmetricQuantizer/asymmetric_quantize_0" -> "3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"1 AsymmetricQuantizer/asymmetric_quantize_0" -> "89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"2 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" -> "4 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"4 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "5 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0"; +"5 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" -> "6 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"6 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0"; +"7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"8 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "10 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"10 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "11 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0"; +"11 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" -> "12 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"12 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"13 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "15 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"15 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "16 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"16 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" -> "18 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0"; +"18 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" -> "19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"20 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "22 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"22 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "23 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0"; +"23 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" -> "24 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"24 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"25 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "27 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"27 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "28 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"28 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" -> "30 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0"; +"30 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" -> "31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "174 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_0"; +"32 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "34 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"34 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "35 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0"; +"35 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" -> "36 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"36 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"37 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "39 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"39 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "40 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"40 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"41 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "43 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"43 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "44 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"44 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" -> "46 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0"; +"46 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" -> "47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"48 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "50 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"50 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "51 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0"; +"51 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" -> "52 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"52 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"53 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "55 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"55 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "56 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"56 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" -> "58 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0"; +"58 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" -> "59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "185 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_2"; +"60 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "62 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"62 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "63 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0"; +"63 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" -> "64 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"64 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"65 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "67 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"67 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "68 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"68 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"69 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "71 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"71 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "72 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"72 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" -> "74 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0"; +"74 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" -> "75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"76 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "78 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"78 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "79 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0"; +"79 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" -> "80 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"80 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"81 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "83 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"83 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "84 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"84 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" -> "86 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0"; +"86 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" -> "87 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"87 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "196 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_4"; +"88 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" -> "90 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"90 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "91 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0"; +"91 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" -> "92 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"92 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0"; +"93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"94 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "96 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"96 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "97 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0"; +"97 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" -> "98 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"98 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"99 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "101 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"101 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "102 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"102 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" -> "104 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0"; +"104 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" -> "105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"106 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "108 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"108 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "109 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0"; +"109 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" -> "110 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"110 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"111 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "113 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"113 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "114 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"114 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" -> "116 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0"; +"116 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" -> "117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "176 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_1"; +"118 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "120 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"120 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "121 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0"; +"121 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" -> "122 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"122 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"123 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "125 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"125 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "126 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"126 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"127 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "129 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"129 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "130 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"130 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" -> "132 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0"; +"132 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" -> "133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"134 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "136 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"136 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "137 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0"; +"137 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" -> "138 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"138 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"139 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "141 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"141 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "142 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"142 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" -> "144 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0"; +"144 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" -> "145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "187 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_3"; +"146 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "148 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"148 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "149 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0"; +"149 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" -> "150 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"150 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"151 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "153 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"153 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "154 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"154 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"155 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "157 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"157 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "158 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"158 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" -> "160 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0"; +"160 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" -> "161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"162 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "164 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"164 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "165 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0"; +"165 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" -> "166 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"166 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"167 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "169 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"169 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "170 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"170 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" -> "172 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0"; +"172 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" -> "173 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"173 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "198 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_5"; +"174 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_0" -> "175 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0"; +"175 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" -> "178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0"; +"176 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_1" -> "177 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1"; +"177 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" -> "178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0"; +"178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" -> "179 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_0"; +"179 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_0" -> "180 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___0"; +"180 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___0" -> "181 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2"; +"181 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" -> "182 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___0"; +"182 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___0" -> "183 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3"; +"183 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" -> "184 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0"; +"185 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_2" -> "186 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4"; +"186 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" -> "189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1"; +"187 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_3" -> "188 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_5"; +"188 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_5" -> "189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1"; +"189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1" -> "190 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_1"; +"190 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_1" -> "191 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___1"; +"191 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___1" -> "192 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_6"; +"192 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_6" -> "193 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___1"; +"193 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___1" -> "194 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_7"; +"194 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_7" -> "195 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_1"; +"196 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_4" -> "197 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_8"; +"197 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_8" -> "200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2"; +"198 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_5" -> "199 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_9"; +"199 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_9" -> "200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2"; +"200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2" -> "201 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_2"; +"201 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_2" -> "202 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___2"; +"202 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___2" -> "203 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_10"; +"203 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_10" -> "204 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___2"; +"204 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___2" -> "205 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_11"; +"205 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_11" -> "206 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_2"; +} diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_padim/compressed_model.yml b/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_padim/compressed_model.yml new file mode 100644 index 00000000000..aa1b8c764c0 --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_padim/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsAnomalyDetection: + nncf: + number_of_fakequantizers: 27 + ptq: + number_of_fakequantizers: 28 diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_padim/nncf/nncf_quantization.dot b/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_padim/nncf/nncf_quantization.dot new file mode 100644 index 00000000000..789824fcf95 --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_padim/nncf/nncf_quantization.dot @@ -0,0 +1,240 @@ +strict digraph { +"0 /nncf_model_input_0" [id=0, type=nncf_model_input]; +"1 AsymmetricQuantizer/asymmetric_quantize_0" [id=1, type=asymmetric_quantize]; +"2 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=2, type=symmetric_quantize]; +"3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" [id=3, type=conv2d]; +"4 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=4, type=batch_norm]; +"5 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" [id=5, type=relu_]; +"6 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=6, type=asymmetric_quantize]; +"7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" [id=7, type=max_pool2d]; +"8 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=8, type=symmetric_quantize]; +"9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=9, type=conv2d]; +"10 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=10, type=batch_norm]; +"11 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" [id=11, type=relu_]; +"12 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=12, type=asymmetric_quantize]; +"13 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=13, type=symmetric_quantize]; +"14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=14, type=conv2d]; +"15 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=15, type=batch_norm]; +"16 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=16, type=asymmetric_quantize]; +"17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" [id=17, type=__iadd__]; +"18 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" [id=18, type=relu_]; +"19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=19, type=asymmetric_quantize]; +"20 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=20, type=symmetric_quantize]; +"21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=21, type=conv2d]; +"22 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=22, type=batch_norm]; +"23 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" [id=23, type=relu_]; +"24 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=24, type=asymmetric_quantize]; +"25 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=25, type=symmetric_quantize]; +"26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=26, type=conv2d]; +"27 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=27, type=batch_norm]; +"28 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=28, type=asymmetric_quantize]; +"29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" [id=29, type=__iadd__]; +"30 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" [id=30, type=relu_]; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=31, type=asymmetric_quantize]; +"32 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=32, type=symmetric_quantize]; +"33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=33, type=conv2d]; +"34 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=34, type=batch_norm]; +"35 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" [id=35, type=relu_]; +"36 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=36, type=asymmetric_quantize]; +"37 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=37, type=symmetric_quantize]; +"38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=38, type=conv2d]; +"39 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=39, type=batch_norm]; +"40 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=40, type=asymmetric_quantize]; +"41 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=41, type=symmetric_quantize]; +"42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=42, type=conv2d]; +"43 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=43, type=batch_norm]; +"44 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=44, type=asymmetric_quantize]; +"45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" [id=45, type=__iadd__]; +"46 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" [id=46, type=relu_]; +"47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=47, type=asymmetric_quantize]; +"48 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=48, type=symmetric_quantize]; +"49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=49, type=conv2d]; +"50 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=50, type=batch_norm]; +"51 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" [id=51, type=relu_]; +"52 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=52, type=asymmetric_quantize]; +"53 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=53, type=symmetric_quantize]; +"54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=54, type=conv2d]; +"55 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=55, type=batch_norm]; +"56 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=56, type=asymmetric_quantize]; +"57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" [id=57, type=__iadd__]; +"58 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" [id=58, type=relu_]; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=59, type=asymmetric_quantize]; +"60 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=60, type=symmetric_quantize]; +"61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=61, type=conv2d]; +"62 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=62, type=batch_norm]; +"63 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" [id=63, type=relu_]; +"64 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=64, type=asymmetric_quantize]; +"65 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=65, type=symmetric_quantize]; +"66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=66, type=conv2d]; +"67 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=67, type=batch_norm]; +"68 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=68, type=asymmetric_quantize]; +"69 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=69, type=symmetric_quantize]; +"70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=70, type=conv2d]; +"71 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=71, type=batch_norm]; +"72 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=72, type=asymmetric_quantize]; +"73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" [id=73, type=__iadd__]; +"74 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" [id=74, type=relu_]; +"75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=75, type=asymmetric_quantize]; +"76 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=76, type=symmetric_quantize]; +"77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=77, type=conv2d]; +"78 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=78, type=batch_norm]; +"79 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" [id=79, type=relu_]; +"80 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=80, type=asymmetric_quantize]; +"81 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=81, type=symmetric_quantize]; +"82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=82, type=conv2d]; +"83 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=83, type=batch_norm]; +"84 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=84, type=asymmetric_quantize]; +"85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" [id=85, type=__iadd__]; +"86 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" [id=86, type=relu_]; +"87 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=87, type=asymmetric_quantize]; +"88 PadimLightning/PadimModel[model]/interpolate_0" [id=88, type=interpolate]; +"89 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_0" [id=89, type=asymmetric_quantize]; +"90 PadimLightning/PadimModel[model]/cat_0" [id=90, type=cat]; +"91 PadimLightning/PadimModel[model]/interpolate_1" [id=91, type=interpolate]; +"92 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_1" [id=92, type=asymmetric_quantize]; +"93 PadimLightning/PadimModel[model]/cat_1" [id=93, type=cat]; +"94 PadimLightning/PadimModel[model]/index_select_0" [id=94, type=index_select]; +"95 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_0" [id=95, type=reshape]; +"96 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" [id=96, type=__sub__]; +"97 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" [id=97, type=asymmetric_quantize]; +"98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0" [id=98, type=permute]; +"99 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/matmul_0" [id=99, type=matmul]; +"100 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" [id=100, type=asymmetric_quantize]; +"101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0" [id=101, type=__mul__]; +"102 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sum_0" [id=102, type=sum]; +"103 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_1" [id=103, type=permute]; +"104 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_1" [id=104, type=reshape]; +"105 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/clamp_0" [id=105, type=clamp]; +"106 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" [id=106, type=asymmetric_quantize]; +"107 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0" [id=107, type=sqrt]; +"108 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" [id=108, type=asymmetric_quantize]; +"109 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0" [id=109, type=interpolate]; +"110 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" [id=110, type=asymmetric_quantize]; +"111 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/pad_0" [id=111, type=pad]; +"112 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0" [id=112, type=conv2d]; +"113 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/view_0" [id=113, type=view]; +"114 /nncf_model_output_0" [id=114, type=nncf_model_output]; +"0 /nncf_model_input_0" -> "1 AsymmetricQuantizer/asymmetric_quantize_0"; +"1 AsymmetricQuantizer/asymmetric_quantize_0" -> "3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"2 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" -> "4 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"4 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "5 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0"; +"5 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" -> "6 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"6 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0"; +"7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"8 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "10 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"10 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "11 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0"; +"11 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" -> "12 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"12 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"13 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "15 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"15 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "16 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"16 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" -> "18 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0"; +"18 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" -> "19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"20 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "22 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"22 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "23 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0"; +"23 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" -> "24 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"24 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"25 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "27 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"27 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "28 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"28 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" -> "30 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0"; +"30 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" -> "31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "90 PadimLightning/PadimModel[model]/cat_0"; +"32 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "34 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"34 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "35 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0"; +"35 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" -> "36 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"36 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"37 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "39 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"39 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "40 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"40 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"41 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "43 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"43 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "44 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"44 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" -> "46 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0"; +"46 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" -> "47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"48 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "50 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"50 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "51 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0"; +"51 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" -> "52 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"52 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"53 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "55 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"55 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "56 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"56 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" -> "58 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0"; +"58 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" -> "59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "88 PadimLightning/PadimModel[model]/interpolate_0"; +"60 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "62 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"62 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "63 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0"; +"63 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" -> "64 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"64 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"65 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "67 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"67 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "68 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"68 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"69 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "71 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"71 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "72 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"72 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" -> "74 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0"; +"74 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" -> "75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"76 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "78 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"78 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "79 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0"; +"79 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" -> "80 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"80 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"81 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "83 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"83 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "84 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"84 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" -> "86 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0"; +"86 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" -> "87 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"87 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "91 PadimLightning/PadimModel[model]/interpolate_1"; +"88 PadimLightning/PadimModel[model]/interpolate_0" -> "89 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_0"; +"89 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_0" -> "90 PadimLightning/PadimModel[model]/cat_0"; +"90 PadimLightning/PadimModel[model]/cat_0" -> "93 PadimLightning/PadimModel[model]/cat_1"; +"91 PadimLightning/PadimModel[model]/interpolate_1" -> "92 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_1"; +"92 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_1" -> "93 PadimLightning/PadimModel[model]/cat_1"; +"93 PadimLightning/PadimModel[model]/cat_1" -> "94 PadimLightning/PadimModel[model]/index_select_0"; +"94 PadimLightning/PadimModel[model]/index_select_0" -> "95 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_0"; +"95 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_0" -> "96 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0"; +"96 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" -> "97 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0"; +"97 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" -> "98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0"; +"98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0" -> "99 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/matmul_0"; +"98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0" -> "101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0"; +"99 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/matmul_0" -> "100 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1"; +"100 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" -> "101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0"; +"101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0" -> "102 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sum_0"; +"102 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sum_0" -> "103 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_1"; +"103 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_1" -> "104 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_1"; +"104 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_1" -> "105 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/clamp_0"; +"105 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/clamp_0" -> "106 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2"; +"106 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" -> "107 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0"; +"107 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0" -> "108 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3"; +"108 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" -> "109 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0"; +"109 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0" -> "110 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4"; +"110 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" -> "111 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/pad_0"; +"111 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/pad_0" -> "112 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0"; +"112 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0" -> "113 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/view_0"; +"113 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/view_0" -> "114 /nncf_model_output_0"; +} diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_stfpm/compressed_model.yml b/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_stfpm/compressed_model.yml new file mode 100644 index 00000000000..820c728804c --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_stfpm/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsAnomalyDetection: + nncf: + number_of_fakequantizers: 55 + ptq: + number_of_fakequantizers: 71 diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_stfpm/nncf/nncf_quantization.dot b/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_stfpm/nncf/nncf_quantization.dot new file mode 100644 index 00000000000..ef13f90986f --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_detection_stfpm/nncf/nncf_quantization.dot @@ -0,0 +1,430 @@ +strict digraph { +"0 /nncf_model_input_0" [id=0, type=nncf_model_input]; +"1 AsymmetricQuantizer/asymmetric_quantize_0" [id=1, type=asymmetric_quantize]; +"2 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=2, type=symmetric_quantize]; +"3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" [id=3, type=conv2d]; +"4 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=4, type=batch_norm]; +"5 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" [id=5, type=relu_]; +"6 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=6, type=asymmetric_quantize]; +"7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" [id=7, type=max_pool2d]; +"8 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=8, type=symmetric_quantize]; +"9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=9, type=conv2d]; +"10 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=10, type=batch_norm]; +"11 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" [id=11, type=relu_]; +"12 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=12, type=asymmetric_quantize]; +"13 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=13, type=symmetric_quantize]; +"14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=14, type=conv2d]; +"15 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=15, type=batch_norm]; +"16 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=16, type=asymmetric_quantize]; +"17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" [id=17, type=__iadd__]; +"18 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" [id=18, type=relu_]; +"19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=19, type=asymmetric_quantize]; +"20 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=20, type=symmetric_quantize]; +"21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=21, type=conv2d]; +"22 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=22, type=batch_norm]; +"23 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" [id=23, type=relu_]; +"24 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=24, type=asymmetric_quantize]; +"25 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=25, type=symmetric_quantize]; +"26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=26, type=conv2d]; +"27 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=27, type=batch_norm]; +"28 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=28, type=asymmetric_quantize]; +"29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" [id=29, type=__iadd__]; +"30 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" [id=30, type=relu_]; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=31, type=asymmetric_quantize]; +"32 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=32, type=symmetric_quantize]; +"33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=33, type=conv2d]; +"34 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=34, type=batch_norm]; +"35 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" [id=35, type=relu_]; +"36 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=36, type=asymmetric_quantize]; +"37 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=37, type=symmetric_quantize]; +"38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=38, type=conv2d]; +"39 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=39, type=batch_norm]; +"40 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=40, type=asymmetric_quantize]; +"41 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=41, type=symmetric_quantize]; +"42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=42, type=conv2d]; +"43 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=43, type=batch_norm]; +"44 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=44, type=asymmetric_quantize]; +"45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" [id=45, type=__iadd__]; +"46 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" [id=46, type=relu_]; +"47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=47, type=asymmetric_quantize]; +"48 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=48, type=symmetric_quantize]; +"49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=49, type=conv2d]; +"50 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=50, type=batch_norm]; +"51 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" [id=51, type=relu_]; +"52 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=52, type=asymmetric_quantize]; +"53 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=53, type=symmetric_quantize]; +"54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=54, type=conv2d]; +"55 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=55, type=batch_norm]; +"56 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=56, type=asymmetric_quantize]; +"57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" [id=57, type=__iadd__]; +"58 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" [id=58, type=relu_]; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=59, type=asymmetric_quantize]; +"60 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=60, type=symmetric_quantize]; +"61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=61, type=conv2d]; +"62 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=62, type=batch_norm]; +"63 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" [id=63, type=relu_]; +"64 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=64, type=asymmetric_quantize]; +"65 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=65, type=symmetric_quantize]; +"66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=66, type=conv2d]; +"67 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=67, type=batch_norm]; +"68 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=68, type=asymmetric_quantize]; +"69 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=69, type=symmetric_quantize]; +"70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=70, type=conv2d]; +"71 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=71, type=batch_norm]; +"72 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=72, type=asymmetric_quantize]; +"73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" [id=73, type=__iadd__]; +"74 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" [id=74, type=relu_]; +"75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=75, type=asymmetric_quantize]; +"76 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=76, type=symmetric_quantize]; +"77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=77, type=conv2d]; +"78 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=78, type=batch_norm]; +"79 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" [id=79, type=relu_]; +"80 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=80, type=asymmetric_quantize]; +"81 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=81, type=symmetric_quantize]; +"82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=82, type=conv2d]; +"83 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=83, type=batch_norm]; +"84 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=84, type=asymmetric_quantize]; +"85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" [id=85, type=__iadd__]; +"86 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" [id=86, type=relu_]; +"87 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=87, type=asymmetric_quantize]; +"88 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=88, type=symmetric_quantize]; +"89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" [id=89, type=conv2d]; +"90 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=90, type=batch_norm]; +"91 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" [id=91, type=relu_]; +"92 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=92, type=asymmetric_quantize]; +"93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" [id=93, type=max_pool2d]; +"94 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=94, type=symmetric_quantize]; +"95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=95, type=conv2d]; +"96 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=96, type=batch_norm]; +"97 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" [id=97, type=relu_]; +"98 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=98, type=asymmetric_quantize]; +"99 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=99, type=symmetric_quantize]; +"100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=100, type=conv2d]; +"101 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=101, type=batch_norm]; +"102 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=102, type=asymmetric_quantize]; +"103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" [id=103, type=__iadd__]; +"104 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" [id=104, type=relu_]; +"105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=105, type=asymmetric_quantize]; +"106 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=106, type=symmetric_quantize]; +"107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=107, type=conv2d]; +"108 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=108, type=batch_norm]; +"109 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" [id=109, type=relu_]; +"110 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=110, type=asymmetric_quantize]; +"111 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=111, type=symmetric_quantize]; +"112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=112, type=conv2d]; +"113 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=113, type=batch_norm]; +"114 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=114, type=asymmetric_quantize]; +"115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" [id=115, type=__iadd__]; +"116 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" [id=116, type=relu_]; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=117, type=asymmetric_quantize]; +"118 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=118, type=symmetric_quantize]; +"119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=119, type=conv2d]; +"120 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=120, type=batch_norm]; +"121 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" [id=121, type=relu_]; +"122 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=122, type=asymmetric_quantize]; +"123 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=123, type=symmetric_quantize]; +"124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=124, type=conv2d]; +"125 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=125, type=batch_norm]; +"126 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=126, type=asymmetric_quantize]; +"127 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=127, type=symmetric_quantize]; +"128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=128, type=conv2d]; +"129 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=129, type=batch_norm]; +"130 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=130, type=asymmetric_quantize]; +"131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" [id=131, type=__iadd__]; +"132 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" [id=132, type=relu_]; +"133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=133, type=asymmetric_quantize]; +"134 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=134, type=symmetric_quantize]; +"135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=135, type=conv2d]; +"136 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=136, type=batch_norm]; +"137 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" [id=137, type=relu_]; +"138 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=138, type=asymmetric_quantize]; +"139 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=139, type=symmetric_quantize]; +"140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=140, type=conv2d]; +"141 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=141, type=batch_norm]; +"142 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=142, type=asymmetric_quantize]; +"143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" [id=143, type=__iadd__]; +"144 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" [id=144, type=relu_]; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=145, type=asymmetric_quantize]; +"146 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=146, type=symmetric_quantize]; +"147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=147, type=conv2d]; +"148 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=148, type=batch_norm]; +"149 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" [id=149, type=relu_]; +"150 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=150, type=asymmetric_quantize]; +"151 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=151, type=symmetric_quantize]; +"152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=152, type=conv2d]; +"153 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=153, type=batch_norm]; +"154 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=154, type=asymmetric_quantize]; +"155 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=155, type=symmetric_quantize]; +"156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=156, type=conv2d]; +"157 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=157, type=batch_norm]; +"158 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=158, type=asymmetric_quantize]; +"159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" [id=159, type=__iadd__]; +"160 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" [id=160, type=relu_]; +"161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=161, type=asymmetric_quantize]; +"162 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=162, type=symmetric_quantize]; +"163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=163, type=conv2d]; +"164 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=164, type=batch_norm]; +"165 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" [id=165, type=relu_]; +"166 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=166, type=asymmetric_quantize]; +"167 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=167, type=symmetric_quantize]; +"168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=168, type=conv2d]; +"169 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=169, type=batch_norm]; +"170 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=170, type=asymmetric_quantize]; +"171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" [id=171, type=__iadd__]; +"172 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" [id=172, type=relu_]; +"173 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=173, type=asymmetric_quantize]; +"174 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_0" [id=174, type=normalize]; +"175 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" [id=175, type=asymmetric_quantize]; +"176 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_1" [id=176, type=normalize]; +"177 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" [id=177, type=asymmetric_quantize]; +"178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" [id=178, type=__sub__]; +"179 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_0" [id=179, type=norm]; +"180 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___0" [id=180, type=__pow__]; +"181 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" [id=181, type=asymmetric_quantize]; +"182 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___0" [id=182, type=__rmul__]; +"183 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" [id=183, type=asymmetric_quantize]; +"184 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0" [id=184, type=interpolate]; +"185 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_2" [id=185, type=normalize]; +"186 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" [id=186, type=asymmetric_quantize]; +"187 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_3" [id=187, type=normalize]; +"188 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_5" [id=188, type=asymmetric_quantize]; +"189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1" [id=189, type=__sub__]; +"190 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_1" [id=190, type=norm]; +"191 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___1" [id=191, type=__pow__]; +"192 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_6" [id=192, type=asymmetric_quantize]; +"193 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___1" [id=193, type=__rmul__]; +"194 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_7" [id=194, type=asymmetric_quantize]; +"195 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_1" [id=195, type=interpolate]; +"196 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_4" [id=196, type=normalize]; +"197 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_8" [id=197, type=asymmetric_quantize]; +"198 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_5" [id=198, type=normalize]; +"199 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_9" [id=199, type=asymmetric_quantize]; +"200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2" [id=200, type=__sub__]; +"201 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_2" [id=201, type=norm]; +"202 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___2" [id=202, type=__pow__]; +"203 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_10" [id=203, type=asymmetric_quantize]; +"204 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___2" [id=204, type=__rmul__]; +"205 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_11" [id=205, type=asymmetric_quantize]; +"206 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_2" [id=206, type=interpolate]; +"0 /nncf_model_input_0" -> "1 AsymmetricQuantizer/asymmetric_quantize_0"; +"1 AsymmetricQuantizer/asymmetric_quantize_0" -> "3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"1 AsymmetricQuantizer/asymmetric_quantize_0" -> "89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"2 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" -> "4 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"4 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "5 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0"; +"5 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" -> "6 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"6 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0"; +"7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"8 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "10 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"10 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "11 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0"; +"11 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" -> "12 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"12 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"13 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "15 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"15 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "16 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"16 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" -> "18 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0"; +"18 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" -> "19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"20 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "22 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"22 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "23 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0"; +"23 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" -> "24 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"24 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"25 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "27 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"27 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "28 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"28 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" -> "30 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0"; +"30 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" -> "31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "174 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_0"; +"32 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "34 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"34 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "35 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0"; +"35 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" -> "36 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"36 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"37 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "39 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"39 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "40 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"40 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"41 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "43 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"43 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "44 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"44 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" -> "46 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0"; +"46 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" -> "47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"48 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "50 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"50 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "51 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0"; +"51 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" -> "52 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"52 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"53 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "55 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"55 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "56 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"56 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" -> "58 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0"; +"58 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" -> "59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "185 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_2"; +"60 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "62 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"62 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "63 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0"; +"63 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" -> "64 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"64 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"65 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "67 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"67 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "68 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"68 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"69 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "71 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"71 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "72 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"72 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" -> "74 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0"; +"74 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" -> "75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"76 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "78 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"78 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "79 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0"; +"79 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" -> "80 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"80 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"81 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "83 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"83 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "84 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"84 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" -> "86 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0"; +"86 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" -> "87 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"87 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "196 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_4"; +"88 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" -> "90 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"90 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "91 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0"; +"91 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" -> "92 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"92 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0"; +"93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"94 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "96 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"96 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "97 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0"; +"97 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" -> "98 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"98 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"99 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "101 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"101 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "102 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"102 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" -> "104 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0"; +"104 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" -> "105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"106 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "108 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"108 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "109 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0"; +"109 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" -> "110 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"110 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"111 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "113 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"113 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "114 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"114 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" -> "116 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0"; +"116 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" -> "117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "176 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_1"; +"118 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "120 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"120 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "121 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0"; +"121 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" -> "122 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"122 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"123 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "125 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"125 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "126 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"126 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"127 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "129 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"129 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "130 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"130 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" -> "132 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0"; +"132 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" -> "133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"134 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "136 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"136 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "137 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0"; +"137 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" -> "138 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"138 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"139 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "141 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"141 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "142 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"142 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" -> "144 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0"; +"144 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" -> "145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "187 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_3"; +"146 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "148 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"148 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "149 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0"; +"149 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" -> "150 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"150 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"151 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "153 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"153 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "154 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"154 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"155 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "157 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"157 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "158 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"158 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" -> "160 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0"; +"160 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" -> "161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"162 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "164 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"164 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "165 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0"; +"165 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" -> "166 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"166 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"167 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "169 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"169 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "170 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"170 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" -> "172 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0"; +"172 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" -> "173 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"173 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "198 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_5"; +"174 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_0" -> "175 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0"; +"175 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" -> "178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0"; +"176 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_1" -> "177 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1"; +"177 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" -> "178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0"; +"178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" -> "179 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_0"; +"179 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_0" -> "180 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___0"; +"180 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___0" -> "181 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2"; +"181 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" -> "182 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___0"; +"182 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___0" -> "183 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3"; +"183 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" -> "184 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0"; +"185 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_2" -> "186 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4"; +"186 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" -> "189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1"; +"187 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_3" -> "188 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_5"; +"188 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_5" -> "189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1"; +"189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1" -> "190 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_1"; +"190 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_1" -> "191 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___1"; +"191 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___1" -> "192 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_6"; +"192 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_6" -> "193 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___1"; +"193 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___1" -> "194 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_7"; +"194 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_7" -> "195 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_1"; +"196 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_4" -> "197 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_8"; +"197 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_8" -> "200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2"; +"198 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_5" -> "199 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_9"; +"199 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_9" -> "200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2"; +"200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2" -> "201 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_2"; +"201 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_2" -> "202 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___2"; +"202 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___2" -> "203 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_10"; +"203 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_10" -> "204 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___2"; +"204 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___2" -> "205 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_11"; +"205 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_11" -> "206 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_2"; +} diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_padim/compressed_model.yml b/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_padim/compressed_model.yml new file mode 100644 index 00000000000..f476fb6f822 --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_padim/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsAnomalySegmentation: + nncf: + number_of_fakequantizers: 27 + ptq: + number_of_fakequantizers: 28 diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_padim/nncf/nncf_quantization.dot b/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_padim/nncf/nncf_quantization.dot new file mode 100644 index 00000000000..789824fcf95 --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_padim/nncf/nncf_quantization.dot @@ -0,0 +1,240 @@ +strict digraph { +"0 /nncf_model_input_0" [id=0, type=nncf_model_input]; +"1 AsymmetricQuantizer/asymmetric_quantize_0" [id=1, type=asymmetric_quantize]; +"2 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=2, type=symmetric_quantize]; +"3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" [id=3, type=conv2d]; +"4 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=4, type=batch_norm]; +"5 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" [id=5, type=relu_]; +"6 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=6, type=asymmetric_quantize]; +"7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" [id=7, type=max_pool2d]; +"8 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=8, type=symmetric_quantize]; +"9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=9, type=conv2d]; +"10 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=10, type=batch_norm]; +"11 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" [id=11, type=relu_]; +"12 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=12, type=asymmetric_quantize]; +"13 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=13, type=symmetric_quantize]; +"14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=14, type=conv2d]; +"15 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=15, type=batch_norm]; +"16 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=16, type=asymmetric_quantize]; +"17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" [id=17, type=__iadd__]; +"18 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" [id=18, type=relu_]; +"19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=19, type=asymmetric_quantize]; +"20 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=20, type=symmetric_quantize]; +"21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=21, type=conv2d]; +"22 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=22, type=batch_norm]; +"23 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" [id=23, type=relu_]; +"24 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=24, type=asymmetric_quantize]; +"25 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=25, type=symmetric_quantize]; +"26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=26, type=conv2d]; +"27 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=27, type=batch_norm]; +"28 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=28, type=asymmetric_quantize]; +"29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" [id=29, type=__iadd__]; +"30 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" [id=30, type=relu_]; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=31, type=asymmetric_quantize]; +"32 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=32, type=symmetric_quantize]; +"33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=33, type=conv2d]; +"34 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=34, type=batch_norm]; +"35 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" [id=35, type=relu_]; +"36 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=36, type=asymmetric_quantize]; +"37 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=37, type=symmetric_quantize]; +"38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=38, type=conv2d]; +"39 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=39, type=batch_norm]; +"40 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=40, type=asymmetric_quantize]; +"41 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=41, type=symmetric_quantize]; +"42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=42, type=conv2d]; +"43 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=43, type=batch_norm]; +"44 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=44, type=asymmetric_quantize]; +"45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" [id=45, type=__iadd__]; +"46 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" [id=46, type=relu_]; +"47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=47, type=asymmetric_quantize]; +"48 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=48, type=symmetric_quantize]; +"49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=49, type=conv2d]; +"50 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=50, type=batch_norm]; +"51 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" [id=51, type=relu_]; +"52 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=52, type=asymmetric_quantize]; +"53 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=53, type=symmetric_quantize]; +"54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=54, type=conv2d]; +"55 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=55, type=batch_norm]; +"56 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=56, type=asymmetric_quantize]; +"57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" [id=57, type=__iadd__]; +"58 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" [id=58, type=relu_]; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=59, type=asymmetric_quantize]; +"60 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=60, type=symmetric_quantize]; +"61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=61, type=conv2d]; +"62 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=62, type=batch_norm]; +"63 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" [id=63, type=relu_]; +"64 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=64, type=asymmetric_quantize]; +"65 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=65, type=symmetric_quantize]; +"66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=66, type=conv2d]; +"67 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=67, type=batch_norm]; +"68 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=68, type=asymmetric_quantize]; +"69 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=69, type=symmetric_quantize]; +"70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=70, type=conv2d]; +"71 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=71, type=batch_norm]; +"72 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=72, type=asymmetric_quantize]; +"73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" [id=73, type=__iadd__]; +"74 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" [id=74, type=relu_]; +"75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=75, type=asymmetric_quantize]; +"76 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=76, type=symmetric_quantize]; +"77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=77, type=conv2d]; +"78 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=78, type=batch_norm]; +"79 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" [id=79, type=relu_]; +"80 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=80, type=asymmetric_quantize]; +"81 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=81, type=symmetric_quantize]; +"82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=82, type=conv2d]; +"83 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=83, type=batch_norm]; +"84 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=84, type=asymmetric_quantize]; +"85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" [id=85, type=__iadd__]; +"86 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" [id=86, type=relu_]; +"87 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=87, type=asymmetric_quantize]; +"88 PadimLightning/PadimModel[model]/interpolate_0" [id=88, type=interpolate]; +"89 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_0" [id=89, type=asymmetric_quantize]; +"90 PadimLightning/PadimModel[model]/cat_0" [id=90, type=cat]; +"91 PadimLightning/PadimModel[model]/interpolate_1" [id=91, type=interpolate]; +"92 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_1" [id=92, type=asymmetric_quantize]; +"93 PadimLightning/PadimModel[model]/cat_1" [id=93, type=cat]; +"94 PadimLightning/PadimModel[model]/index_select_0" [id=94, type=index_select]; +"95 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_0" [id=95, type=reshape]; +"96 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" [id=96, type=__sub__]; +"97 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" [id=97, type=asymmetric_quantize]; +"98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0" [id=98, type=permute]; +"99 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/matmul_0" [id=99, type=matmul]; +"100 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" [id=100, type=asymmetric_quantize]; +"101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0" [id=101, type=__mul__]; +"102 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sum_0" [id=102, type=sum]; +"103 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_1" [id=103, type=permute]; +"104 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_1" [id=104, type=reshape]; +"105 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/clamp_0" [id=105, type=clamp]; +"106 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" [id=106, type=asymmetric_quantize]; +"107 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0" [id=107, type=sqrt]; +"108 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" [id=108, type=asymmetric_quantize]; +"109 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0" [id=109, type=interpolate]; +"110 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" [id=110, type=asymmetric_quantize]; +"111 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/pad_0" [id=111, type=pad]; +"112 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0" [id=112, type=conv2d]; +"113 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/view_0" [id=113, type=view]; +"114 /nncf_model_output_0" [id=114, type=nncf_model_output]; +"0 /nncf_model_input_0" -> "1 AsymmetricQuantizer/asymmetric_quantize_0"; +"1 AsymmetricQuantizer/asymmetric_quantize_0" -> "3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"2 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"3 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" -> "4 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"4 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "5 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0"; +"5 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" -> "6 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"6 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0"; +"7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"7 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"8 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"9 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "10 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"10 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "11 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0"; +"11 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" -> "12 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"12 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"13 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"14 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "15 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"15 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "16 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"16 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"17 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" -> "18 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0"; +"18 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" -> "19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"19 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"20 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"21 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "22 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"22 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "23 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0"; +"23 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" -> "24 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"24 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"25 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"26 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "27 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"27 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "28 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"28 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"29 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" -> "30 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0"; +"30 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" -> "31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"31 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "90 PadimLightning/PadimModel[model]/cat_0"; +"32 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"33 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "34 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"34 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "35 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0"; +"35 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" -> "36 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"36 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"37 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"38 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "39 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"39 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "40 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"40 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"41 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"42 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "43 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"43 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "44 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"44 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"45 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" -> "46 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0"; +"46 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" -> "47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"47 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"48 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"49 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "50 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"50 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "51 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0"; +"51 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" -> "52 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"52 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"53 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"54 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "55 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"55 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "56 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"56 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"57 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" -> "58 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0"; +"58 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" -> "59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"59 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "88 PadimLightning/PadimModel[model]/interpolate_0"; +"60 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"61 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "62 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"62 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "63 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0"; +"63 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" -> "64 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"64 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"65 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"66 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "67 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"67 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "68 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"68 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"69 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"70 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "71 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"71 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "72 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"72 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"73 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" -> "74 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0"; +"74 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" -> "75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"75 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"76 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"77 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "78 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"78 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "79 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0"; +"79 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" -> "80 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"80 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"81 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"82 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "83 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"83 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "84 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"84 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"85 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" -> "86 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0"; +"86 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" -> "87 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"87 PadimLightning/PadimModel[model]/FeatureExtractor[feature_extractor]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "91 PadimLightning/PadimModel[model]/interpolate_1"; +"88 PadimLightning/PadimModel[model]/interpolate_0" -> "89 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_0"; +"89 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_0" -> "90 PadimLightning/PadimModel[model]/cat_0"; +"90 PadimLightning/PadimModel[model]/cat_0" -> "93 PadimLightning/PadimModel[model]/cat_1"; +"91 PadimLightning/PadimModel[model]/interpolate_1" -> "92 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_1"; +"92 PadimLightning/PadimModel[model]/AsymmetricQuantizer/asymmetric_quantize_1" -> "93 PadimLightning/PadimModel[model]/cat_1"; +"93 PadimLightning/PadimModel[model]/cat_1" -> "94 PadimLightning/PadimModel[model]/index_select_0"; +"94 PadimLightning/PadimModel[model]/index_select_0" -> "95 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_0"; +"95 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_0" -> "96 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0"; +"96 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" -> "97 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0"; +"97 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" -> "98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0"; +"98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0" -> "99 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/matmul_0"; +"98 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_0" -> "101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0"; +"99 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/matmul_0" -> "100 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1"; +"100 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" -> "101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0"; +"101 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__mul___0" -> "102 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sum_0"; +"102 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sum_0" -> "103 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_1"; +"103 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/permute_1" -> "104 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_1"; +"104 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/reshape_1" -> "105 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/clamp_0"; +"105 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/clamp_0" -> "106 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2"; +"106 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" -> "107 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0"; +"107 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/sqrt_0" -> "108 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3"; +"108 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" -> "109 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0"; +"109 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0" -> "110 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4"; +"110 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" -> "111 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/pad_0"; +"111 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/pad_0" -> "112 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0"; +"112 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/conv2d_0" -> "113 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/view_0"; +"113 PadimLightning/PadimModel[model]/AnomalyMapGenerator[anomaly_map_generator]/GaussianBlur2d[blur]/view_0" -> "114 /nncf_model_output_0"; +} diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_stfpm/compressed_model.yml b/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_stfpm/compressed_model.yml new file mode 100644 index 00000000000..866780cae46 --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_stfpm/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsAnomalySegmentation: + nncf: + number_of_fakequantizers: 55 + ptq: + number_of_fakequantizers: 71 diff --git a/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_stfpm/nncf/nncf_quantization.dot b/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_stfpm/nncf/nncf_quantization.dot new file mode 100644 index 00000000000..ef13f90986f --- /dev/null +++ b/tests/e2e/cli/anomaly/reference/ote_anomaly_segmentation_stfpm/nncf/nncf_quantization.dot @@ -0,0 +1,430 @@ +strict digraph { +"0 /nncf_model_input_0" [id=0, type=nncf_model_input]; +"1 AsymmetricQuantizer/asymmetric_quantize_0" [id=1, type=asymmetric_quantize]; +"2 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=2, type=symmetric_quantize]; +"3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" [id=3, type=conv2d]; +"4 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=4, type=batch_norm]; +"5 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" [id=5, type=relu_]; +"6 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=6, type=asymmetric_quantize]; +"7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" [id=7, type=max_pool2d]; +"8 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=8, type=symmetric_quantize]; +"9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=9, type=conv2d]; +"10 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=10, type=batch_norm]; +"11 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" [id=11, type=relu_]; +"12 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=12, type=asymmetric_quantize]; +"13 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=13, type=symmetric_quantize]; +"14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=14, type=conv2d]; +"15 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=15, type=batch_norm]; +"16 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=16, type=asymmetric_quantize]; +"17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" [id=17, type=__iadd__]; +"18 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" [id=18, type=relu_]; +"19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=19, type=asymmetric_quantize]; +"20 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=20, type=symmetric_quantize]; +"21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=21, type=conv2d]; +"22 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=22, type=batch_norm]; +"23 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" [id=23, type=relu_]; +"24 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=24, type=asymmetric_quantize]; +"25 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=25, type=symmetric_quantize]; +"26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=26, type=conv2d]; +"27 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=27, type=batch_norm]; +"28 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=28, type=asymmetric_quantize]; +"29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" [id=29, type=__iadd__]; +"30 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" [id=30, type=relu_]; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=31, type=asymmetric_quantize]; +"32 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=32, type=symmetric_quantize]; +"33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=33, type=conv2d]; +"34 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=34, type=batch_norm]; +"35 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" [id=35, type=relu_]; +"36 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=36, type=asymmetric_quantize]; +"37 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=37, type=symmetric_quantize]; +"38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=38, type=conv2d]; +"39 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=39, type=batch_norm]; +"40 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=40, type=asymmetric_quantize]; +"41 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=41, type=symmetric_quantize]; +"42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=42, type=conv2d]; +"43 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=43, type=batch_norm]; +"44 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=44, type=asymmetric_quantize]; +"45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" [id=45, type=__iadd__]; +"46 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" [id=46, type=relu_]; +"47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=47, type=asymmetric_quantize]; +"48 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=48, type=symmetric_quantize]; +"49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=49, type=conv2d]; +"50 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=50, type=batch_norm]; +"51 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" [id=51, type=relu_]; +"52 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=52, type=asymmetric_quantize]; +"53 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=53, type=symmetric_quantize]; +"54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=54, type=conv2d]; +"55 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=55, type=batch_norm]; +"56 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=56, type=asymmetric_quantize]; +"57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" [id=57, type=__iadd__]; +"58 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" [id=58, type=relu_]; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=59, type=asymmetric_quantize]; +"60 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=60, type=symmetric_quantize]; +"61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=61, type=conv2d]; +"62 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=62, type=batch_norm]; +"63 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" [id=63, type=relu_]; +"64 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=64, type=asymmetric_quantize]; +"65 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=65, type=symmetric_quantize]; +"66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=66, type=conv2d]; +"67 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=67, type=batch_norm]; +"68 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=68, type=asymmetric_quantize]; +"69 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=69, type=symmetric_quantize]; +"70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=70, type=conv2d]; +"71 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=71, type=batch_norm]; +"72 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=72, type=asymmetric_quantize]; +"73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" [id=73, type=__iadd__]; +"74 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" [id=74, type=relu_]; +"75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=75, type=asymmetric_quantize]; +"76 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=76, type=symmetric_quantize]; +"77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=77, type=conv2d]; +"78 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=78, type=batch_norm]; +"79 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" [id=79, type=relu_]; +"80 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=80, type=asymmetric_quantize]; +"81 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=81, type=symmetric_quantize]; +"82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=82, type=conv2d]; +"83 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=83, type=batch_norm]; +"84 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=84, type=asymmetric_quantize]; +"85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" [id=85, type=__iadd__]; +"86 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" [id=86, type=relu_]; +"87 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=87, type=asymmetric_quantize]; +"88 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=88, type=symmetric_quantize]; +"89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" [id=89, type=conv2d]; +"90 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=90, type=batch_norm]; +"91 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" [id=91, type=relu_]; +"92 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=92, type=asymmetric_quantize]; +"93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" [id=93, type=max_pool2d]; +"94 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=94, type=symmetric_quantize]; +"95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=95, type=conv2d]; +"96 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=96, type=batch_norm]; +"97 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" [id=97, type=relu_]; +"98 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=98, type=asymmetric_quantize]; +"99 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=99, type=symmetric_quantize]; +"100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=100, type=conv2d]; +"101 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=101, type=batch_norm]; +"102 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=102, type=asymmetric_quantize]; +"103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" [id=103, type=__iadd__]; +"104 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" [id=104, type=relu_]; +"105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=105, type=asymmetric_quantize]; +"106 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=106, type=symmetric_quantize]; +"107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=107, type=conv2d]; +"108 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=108, type=batch_norm]; +"109 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" [id=109, type=relu_]; +"110 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=110, type=asymmetric_quantize]; +"111 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=111, type=symmetric_quantize]; +"112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=112, type=conv2d]; +"113 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=113, type=batch_norm]; +"114 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=114, type=asymmetric_quantize]; +"115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" [id=115, type=__iadd__]; +"116 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" [id=116, type=relu_]; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=117, type=asymmetric_quantize]; +"118 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=118, type=symmetric_quantize]; +"119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=119, type=conv2d]; +"120 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=120, type=batch_norm]; +"121 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" [id=121, type=relu_]; +"122 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=122, type=asymmetric_quantize]; +"123 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=123, type=symmetric_quantize]; +"124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=124, type=conv2d]; +"125 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=125, type=batch_norm]; +"126 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=126, type=asymmetric_quantize]; +"127 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=127, type=symmetric_quantize]; +"128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=128, type=conv2d]; +"129 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=129, type=batch_norm]; +"130 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=130, type=asymmetric_quantize]; +"131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" [id=131, type=__iadd__]; +"132 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" [id=132, type=relu_]; +"133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=133, type=asymmetric_quantize]; +"134 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=134, type=symmetric_quantize]; +"135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=135, type=conv2d]; +"136 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=136, type=batch_norm]; +"137 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" [id=137, type=relu_]; +"138 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=138, type=asymmetric_quantize]; +"139 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=139, type=symmetric_quantize]; +"140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=140, type=conv2d]; +"141 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=141, type=batch_norm]; +"142 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=142, type=asymmetric_quantize]; +"143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" [id=143, type=__iadd__]; +"144 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" [id=144, type=relu_]; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=145, type=asymmetric_quantize]; +"146 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=146, type=symmetric_quantize]; +"147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" [id=147, type=conv2d]; +"148 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=148, type=batch_norm]; +"149 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" [id=149, type=relu_]; +"150 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=150, type=asymmetric_quantize]; +"151 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=151, type=symmetric_quantize]; +"152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" [id=152, type=conv2d]; +"153 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=153, type=batch_norm]; +"154 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=154, type=asymmetric_quantize]; +"155 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=155, type=symmetric_quantize]; +"156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" [id=156, type=conv2d]; +"157 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" [id=157, type=batch_norm]; +"158 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=158, type=asymmetric_quantize]; +"159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" [id=159, type=__iadd__]; +"160 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" [id=160, type=relu_]; +"161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=161, type=asymmetric_quantize]; +"162 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=162, type=symmetric_quantize]; +"163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" [id=163, type=conv2d]; +"164 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" [id=164, type=batch_norm]; +"165 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" [id=165, type=relu_]; +"166 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" [id=166, type=asymmetric_quantize]; +"167 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" [id=167, type=symmetric_quantize]; +"168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" [id=168, type=conv2d]; +"169 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" [id=169, type=batch_norm]; +"170 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=170, type=asymmetric_quantize]; +"171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" [id=171, type=__iadd__]; +"172 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" [id=172, type=relu_]; +"173 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" [id=173, type=asymmetric_quantize]; +"174 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_0" [id=174, type=normalize]; +"175 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" [id=175, type=asymmetric_quantize]; +"176 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_1" [id=176, type=normalize]; +"177 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" [id=177, type=asymmetric_quantize]; +"178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" [id=178, type=__sub__]; +"179 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_0" [id=179, type=norm]; +"180 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___0" [id=180, type=__pow__]; +"181 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" [id=181, type=asymmetric_quantize]; +"182 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___0" [id=182, type=__rmul__]; +"183 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" [id=183, type=asymmetric_quantize]; +"184 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0" [id=184, type=interpolate]; +"185 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_2" [id=185, type=normalize]; +"186 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" [id=186, type=asymmetric_quantize]; +"187 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_3" [id=187, type=normalize]; +"188 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_5" [id=188, type=asymmetric_quantize]; +"189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1" [id=189, type=__sub__]; +"190 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_1" [id=190, type=norm]; +"191 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___1" [id=191, type=__pow__]; +"192 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_6" [id=192, type=asymmetric_quantize]; +"193 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___1" [id=193, type=__rmul__]; +"194 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_7" [id=194, type=asymmetric_quantize]; +"195 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_1" [id=195, type=interpolate]; +"196 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_4" [id=196, type=normalize]; +"197 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_8" [id=197, type=asymmetric_quantize]; +"198 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_5" [id=198, type=normalize]; +"199 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_9" [id=199, type=asymmetric_quantize]; +"200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2" [id=200, type=__sub__]; +"201 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_2" [id=201, type=norm]; +"202 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___2" [id=202, type=__pow__]; +"203 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_10" [id=203, type=asymmetric_quantize]; +"204 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___2" [id=204, type=__rmul__]; +"205 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_11" [id=205, type=asymmetric_quantize]; +"206 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_2" [id=206, type=interpolate]; +"0 /nncf_model_input_0" -> "1 AsymmetricQuantizer/asymmetric_quantize_0"; +"1 AsymmetricQuantizer/asymmetric_quantize_0" -> "3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"1 AsymmetricQuantizer/asymmetric_quantize_0" -> "89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"2 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"3 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" -> "4 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"4 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "5 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0"; +"5 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" -> "6 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"6 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0"; +"7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"7 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"8 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"9 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "10 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"10 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "11 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0"; +"11 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" -> "12 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"12 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"13 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"14 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "15 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"15 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "16 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"16 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"17 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" -> "18 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0"; +"18 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" -> "19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"19 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"20 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"21 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "22 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"22 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "23 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0"; +"23 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" -> "24 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"24 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"25 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"26 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "27 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"27 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "28 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"28 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"29 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" -> "30 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0"; +"30 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" -> "31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"31 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "174 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_0"; +"32 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"33 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "34 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"34 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "35 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0"; +"35 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" -> "36 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"36 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"37 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"38 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "39 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"39 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "40 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"40 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"41 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"42 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "43 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"43 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "44 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"44 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"45 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" -> "46 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0"; +"46 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" -> "47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"47 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"48 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"49 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "50 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"50 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "51 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0"; +"51 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" -> "52 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"52 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"53 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"54 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "55 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"55 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "56 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"56 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"57 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" -> "58 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0"; +"58 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" -> "59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"59 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "185 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_2"; +"60 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"61 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "62 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"62 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "63 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0"; +"63 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" -> "64 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"64 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"65 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"66 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "67 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"67 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "68 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"68 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"69 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"70 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "71 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"71 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "72 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"72 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"73 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" -> "74 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0"; +"74 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" -> "75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"75 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"76 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"77 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "78 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"78 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "79 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0"; +"79 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" -> "80 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"80 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"81 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"82 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "83 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"83 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "84 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"84 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"85 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" -> "86 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0"; +"86 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" -> "87 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"87 StfpmLightning/STFPMModel[model]/FeatureExtractor[teacher_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "196 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_4"; +"88 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0"; +"89 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFConv2d[conv1]/conv2d_0" -> "90 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"90 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "91 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0"; +"91 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/relu__0" -> "92 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"92 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0"; +"93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"93 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/MaxPool2d[maxpool]/max_pool2d_0" -> "103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"94 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"95 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "96 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"96 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "97 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0"; +"97 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/relu__0" -> "98 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"98 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"99 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"100 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "101 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"101 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "102 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"102 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0"; +"103 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/__iadd___0" -> "104 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0"; +"104 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/relu__0" -> "105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"105 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"106 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"107 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "108 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"108 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "109 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0"; +"109 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/relu__0" -> "110 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"110 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"111 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"112 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "113 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"113 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "114 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"114 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0"; +"115 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/__iadd___0" -> "116 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0"; +"116 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/relu__0" -> "117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"117 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer1]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "176 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_1"; +"118 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"119 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "120 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"120 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "121 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0"; +"121 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/relu__0" -> "122 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"122 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"123 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"124 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "125 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"125 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "126 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"126 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"127 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"128 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "129 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"129 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "130 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"130 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0"; +"131 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/__iadd___0" -> "132 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0"; +"132 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/relu__0" -> "133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"133 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"134 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"135 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "136 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"136 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "137 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0"; +"137 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/relu__0" -> "138 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"138 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"139 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"140 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "141 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"141 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "142 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"142 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0"; +"143 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/__iadd___0" -> "144 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0"; +"144 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/relu__0" -> "145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"145 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer2]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "187 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_3"; +"146 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0"; +"147 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv1]/conv2d_0" -> "148 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"148 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "149 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0"; +"149 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/relu__0" -> "150 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"150 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"151 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0"; +"152 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFConv2d[conv2]/conv2d_0" -> "153 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"153 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "154 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"154 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"155 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0"; +"156 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFConv2d[0]/conv2d_0" -> "157 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0"; +"157 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/batch_norm_0" -> "158 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"158 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/Sequential[downsample]/NNCFBatchNorm2d[1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0"; +"159 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/__iadd___0" -> "160 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0"; +"160 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/relu__0" -> "161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"161 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[0]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"162 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0"; +"163 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv1]/conv2d_0" -> "164 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0"; +"164 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn1]/batch_norm_0" -> "165 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0"; +"165 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/relu__0" -> "166 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0"; +"166 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act1]/AsymmetricQuantizer/asymmetric_quantize_0" -> "168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"167 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/ModuleDict[pre_ops]/UpdateWeight[0]/SymmetricQuantizer[op]/symmetric_quantize_0" -> "168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0"; +"168 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFConv2d[conv2]/conv2d_0" -> "169 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0"; +"169 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/batch_norm_0" -> "170 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"170 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/NNCFBatchNorm2d[bn2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0"; +"171 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/__iadd___0" -> "172 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0"; +"172 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/relu__0" -> "173 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0"; +"173 StfpmLightning/STFPMModel[model]/FeatureExtractor[student_model]/FeatureListNet[feature_extractor]/Sequential[layer3]/BasicBlock[1]/ReLU[act2]/AsymmetricQuantizer/asymmetric_quantize_0" -> "198 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_5"; +"174 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_0" -> "175 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0"; +"175 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_0" -> "178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0"; +"176 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_1" -> "177 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1"; +"177 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_1" -> "178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0"; +"178 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___0" -> "179 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_0"; +"179 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_0" -> "180 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___0"; +"180 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___0" -> "181 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2"; +"181 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_2" -> "182 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___0"; +"182 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___0" -> "183 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3"; +"183 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_3" -> "184 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_0"; +"185 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_2" -> "186 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4"; +"186 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_4" -> "189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1"; +"187 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_3" -> "188 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_5"; +"188 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_5" -> "189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1"; +"189 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___1" -> "190 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_1"; +"190 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_1" -> "191 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___1"; +"191 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___1" -> "192 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_6"; +"192 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_6" -> "193 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___1"; +"193 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___1" -> "194 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_7"; +"194 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_7" -> "195 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_1"; +"196 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_4" -> "197 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_8"; +"197 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_8" -> "200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2"; +"198 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/normalize_5" -> "199 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_9"; +"199 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_9" -> "200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2"; +"200 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__sub___2" -> "201 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_2"; +"201 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/norm_2" -> "202 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___2"; +"202 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__pow___2" -> "203 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_10"; +"203 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_10" -> "204 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___2"; +"204 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/__rmul___2" -> "205 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_11"; +"205 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/AsymmetricQuantizer/asymmetric_quantize_11" -> "206 StfpmLightning/STFPMModel[model]/AnomalyMapGenerator[anomaly_map_generator]/interpolate_2"; +} diff --git a/tests/e2e/cli/anomaly/test_anomaly_classification.py b/tests/e2e/cli/anomaly/test_anomaly_classification.py new file mode 100644 index 00000000000..cc070e42538 --- /dev/null +++ b/tests/e2e/cli/anomaly/test_anomaly_classification.py @@ -0,0 +1,156 @@ +"""Tests for anomaly classification with OTX CLI""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import os + +import pytest + +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_eval_openvino_testing, + nncf_eval_testing, + nncf_export_testing, + nncf_optimize_testing, + nncf_validate_fq_testing, + otx_demo_deployment_testing, + otx_demo_openvino_testing, + otx_demo_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, +) + +args = { + "--train-data-roots": "tests/assets/anomaly/hazelnut/train", + "--val-data-roots": "tests/assets/anomaly/hazelnut/test", + "--test-data-roots": "tests/assets/anomaly/hazelnut/test", + "--input": "tests/assets/anomaly/hazelnut/test/colour", + "train_params": [], +} + +otx_dir = os.getcwd() + +templates = Registry("src/otx/algorithms").filter(task_type="ANOMALY_CLASSIFICATION").templates +templates_ids = [template.model_template_id for template in templates] + + +class TestToolsAnomalyClassification: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.3) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo(self, template, tmp_dir_path): + otx_demo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_openvino(self, template, tmp_dir_path): + otx_demo_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_deployment(self, template, tmp_dir_path): + otx_demo_deployment_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "anomaly", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_testing(template, tmp_dir_path, otx_dir, args, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "anomaly", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/e2e/cli/anomaly/test_anomaly_detection.py b/tests/e2e/cli/anomaly/test_anomaly_detection.py new file mode 100644 index 00000000000..174b60fe016 --- /dev/null +++ b/tests/e2e/cli/anomaly/test_anomaly_detection.py @@ -0,0 +1,156 @@ +"""Tests for anomaly detection with OTX CLI.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import os + +import pytest + +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_eval_openvino_testing, + nncf_eval_testing, + nncf_export_testing, + nncf_optimize_testing, + nncf_validate_fq_testing, + otx_demo_deployment_testing, + otx_demo_openvino_testing, + otx_demo_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, +) + +args = { + "--train-data-roots": "tests/assets/anomaly/hazelnut/train", + "--val-data-roots": "tests/assets/anomaly/hazelnut/test", + "--test-data-roots": "tests/assets/anomaly/hazelnut/test", + "--input": "tests/assets/anomaly/hazelnut/test/colour", + "train_params": [], +} + +otx_dir = os.getcwd() + +templates = Registry("src/otx/algorithms").filter(task_type="ANOMALY_DETECTION").templates +templates_ids = [template.model_template_id for template in templates] + + +class TestToolsAnomalyDetection: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.2) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo(self, template, tmp_dir_path): + otx_demo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_openvino(self, template, tmp_dir_path): + otx_demo_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_deployment(self, template, tmp_dir_path): + otx_demo_deployment_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "anomaly", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_testing(template, tmp_dir_path, otx_dir, args, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "anomaly", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/e2e/cli/anomaly/test_anomaly_segmentation.py b/tests/e2e/cli/anomaly/test_anomaly_segmentation.py new file mode 100644 index 00000000000..9da1139f1b4 --- /dev/null +++ b/tests/e2e/cli/anomaly/test_anomaly_segmentation.py @@ -0,0 +1,156 @@ +"""Tests for anomaly segmentation with OTX CLI""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import os + +import pytest + +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_eval_openvino_testing, + nncf_eval_testing, + nncf_export_testing, + nncf_optimize_testing, + nncf_validate_fq_testing, + otx_demo_deployment_testing, + otx_demo_openvino_testing, + otx_demo_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, +) + +args = { + "--train-data-roots": "tests/assets/anomaly/hazelnut/train", + "--val-data-roots": "tests/assets/anomaly/hazelnut/test", + "--test-data-roots": "tests/assets/anomaly/hazelnut/test", + "--input": "tests/assets/anomaly/hazelnut/test/colour", + "train_params": [], +} + +otx_dir = os.getcwd() + +templates = Registry("src/otx/algorithms").filter(task_type="ANOMALY_SEGMENTATION").templates +templates_ids = [template.model_template_id for template in templates] + + +class TestToolsAnomalySegmentation: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.2) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo(self, template, tmp_dir_path): + otx_demo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_openvino(self, template, tmp_dir_path): + otx_demo_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_deployment(self, template, tmp_dir_path): + otx_demo_deployment_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "anomaly", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_testing(template, tmp_dir_path, otx_dir, args, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "anomaly", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/e2e/cli/classification/__init__.py b/tests/e2e/cli/classification/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/e2e/cli/classification/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/e2e/cli/classification/reference/Custom_Image_Classification_EfficientNet-V2-S/compressed_model.yml b/tests/e2e/cli/classification/reference/Custom_Image_Classification_EfficientNet-V2-S/compressed_model.yml new file mode 100644 index 00000000000..d234c15bd70 --- /dev/null +++ b/tests/e2e/cli/classification/reference/Custom_Image_Classification_EfficientNet-V2-S/compressed_model.yml @@ -0,0 +1,15 @@ +TestToolsHierarchicalClassification: + nncf: + number_of_fakequantizers: 267 + ptq: + number_of_fakequantizers: 207 +TestToolsMultiClassClassification: + nncf: + number_of_fakequantizers: 267 + ptq: + number_of_fakequantizers: 207 +TestToolsMultilabelClassification: + nncf: + number_of_fakequantizers: 269 + ptq: + number_of_fakequantizers: 209 diff --git a/tests/e2e/cli/classification/reference/Custom_Image_Classification_EfficinetNet-B0/compressed_model.yml b/tests/e2e/cli/classification/reference/Custom_Image_Classification_EfficinetNet-B0/compressed_model.yml new file mode 100644 index 00000000000..ebea5aac8b2 --- /dev/null +++ b/tests/e2e/cli/classification/reference/Custom_Image_Classification_EfficinetNet-B0/compressed_model.yml @@ -0,0 +1,15 @@ +TestToolsHierarchicalClassification: + nncf: + number_of_fakequantizers: 124 + ptq: + number_of_fakequantizers: 92 +TestToolsMultiClassClassification: + nncf: + number_of_fakequantizers: 124 + ptq: + number_of_fakequantizers: 92 +TestToolsMultilabelClassification: + nncf: + number_of_fakequantizers: 126 + ptq: + number_of_fakequantizers: 94 diff --git a/tests/e2e/cli/classification/reference/Custom_Image_Classification_MobileNet-V3-large-1x/compressed_model.yml b/tests/e2e/cli/classification/reference/Custom_Image_Classification_MobileNet-V3-large-1x/compressed_model.yml new file mode 100644 index 00000000000..fdcec70cf55 --- /dev/null +++ b/tests/e2e/cli/classification/reference/Custom_Image_Classification_MobileNet-V3-large-1x/compressed_model.yml @@ -0,0 +1,15 @@ +TestToolsHierarchicalClassification: + nncf: + number_of_fakequantizers: 91 + ptq: + number_of_fakequantizers: 133 +TestToolsMultiClassClassification: + nncf: + number_of_fakequantizers: 91 + ptq: + number_of_fakequantizers: 133 +TestToolsMultilabelClassification: + nncf: + number_of_fakequantizers: 93 + ptq: + number_of_fakequantizers: 135 diff --git a/tests/e2e/cli/classification/test_api_xai_sanity_classification.py b/tests/e2e/cli/classification/test_api_xai_sanity_classification.py new file mode 100644 index 00000000000..39060672ee8 --- /dev/null +++ b/tests/e2e/cli/classification/test_api_xai_sanity_classification.py @@ -0,0 +1,135 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import os.path as osp +import tempfile + +import pytest +import torch +import numpy as np + +from otx.algorithms.classification.adapters.mmcls.task import MMClassificationTask +from otx.algorithms.classification.adapters.openvino.task import ClassificationOpenVINOTask + +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ( + ModelEntity, +) +from otx.api.entities.result_media import ResultMediaEntity +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.cli.utils.io import read_model, save_model_data +from tests.integration.api.classification.test_api_classification import ( + DEFAULT_CLS_TEMPLATE_DIR, + ClassificationTaskAPIBase, +) +from tests.test_suite.e2e_test_system import e2e_pytest_api + +torch.manual_seed(0) + +assert_text_explain_all = "The number of saliency maps should be equal to the number of all classes." +assert_text_explain_predicted = "The number of saliency maps should be equal to the number of predicted classes." + + +def saliency_maps_check( + predicted_dataset, task_labels, raw_sal_map_shape=None, processed_saliency_maps=False, only_predicted=True +): + for data_point in predicted_dataset: + saliency_map_counter = 0 + metadata_list = data_point.get_metadata() + for metadata in metadata_list: + if isinstance(metadata.data, ResultMediaEntity): + if metadata.data.type == "saliency_map": + saliency_map_counter += 1 + if processed_saliency_maps: + assert metadata.data.numpy.ndim == 3, "Number of dims is incorrect." + assert metadata.data.numpy.shape == (data_point.height, data_point.width, 3) + else: + assert metadata.data.numpy.ndim == 2, "Raw saliency map has to be two-dimensional." + if raw_sal_map_shape: + assert ( + metadata.data.numpy.shape == raw_sal_map_shape + ), "Raw saliency map shape is incorrect." + assert metadata.data.numpy.dtype == np.uint8, "Saliency map has to be uint8 dtype." + if only_predicted: + assert saliency_map_counter == len(data_point.annotation_scene.get_labels()), assert_text_explain_predicted + else: + assert saliency_map_counter == len(task_labels), assert_text_explain_all + + +class TestOVClsXAIAPI(ClassificationTaskAPIBase): + ref_raw_saliency_shapes = { + "EfficientNet-B0": (7, 7), + } + + @e2e_pytest_api + @pytest.mark.parametrize( + "multilabel,hierarchical", + [(False, False), (True, False), (False, True)], + ids=["multiclass", "multilabel", "hierarchical"], + ) + def test_inference_xai(self, multilabel, hierarchical): + with tempfile.TemporaryDirectory() as temp_dir: + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_CLS_TEMPLATE_DIR, num_iters=1) + task_environment, dataset = self.init_environment( + hyper_parameters, model_template, multilabel, hierarchical, 20 + ) + + # Train and save a model + task = MMClassificationTask(task_environment=task_environment) + train_parameters = TrainParameters() + output_model = ModelEntity( + dataset, + task_environment.get_model_configuration(), + ) + task.train(dataset, output_model, train_parameters) + save_model_data(output_model, temp_dir) + + for processed_saliency_maps, only_predicted in [[True, False], [False, True]]: + task_environment, dataset = self.init_environment( + hyper_parameters, model_template, multilabel, hierarchical, 20 + ) + + # Infer torch model + task = MMClassificationTask(task_environment=task_environment) + inference_parameters = InferenceParameters( + is_evaluation=False, + process_saliency_maps=processed_saliency_maps, + explain_predicted_classes=only_predicted, + ) + predicted_dataset = task.infer(dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps torch task + task_labels = output_model.configuration.get_label_schema().get_labels(include_empty=False) + saliency_maps_check( + predicted_dataset, + task_labels, + self.ref_raw_saliency_shapes[model_template.name], + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + # Save OV IR model + task._model_ckpt = osp.join(temp_dir, "weights.pth") + exported_model = ModelEntity(None, task_environment.get_model_configuration()) + task.export(ExportType.OPENVINO, exported_model, dump_features=True) + os.makedirs(temp_dir, exist_ok=True) + save_model_data(exported_model, temp_dir) + + # Infer OV IR model + load_weights_ov = osp.join(temp_dir, "openvino.xml") + task_environment.model = read_model(task_environment.get_model_configuration(), load_weights_ov, None) + task = ClassificationOpenVINOTask(task_environment=task_environment) + _, dataset = self.init_environment(hyper_parameters, model_template, multilabel, hierarchical, 20) + predicted_dataset_ov = task.infer(dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps OV task + saliency_maps_check( + predicted_dataset_ov, + task_labels, + self.ref_raw_saliency_shapes[model_template.name], + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) diff --git a/tests/e2e/cli/classification/test_classification.py b/tests/e2e/cli/classification/test_classification.py new file mode 100644 index 00000000000..909858ef524 --- /dev/null +++ b/tests/e2e/cli/classification/test_classification.py @@ -0,0 +1,712 @@ +"""Tests for Classification with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os +from pathlib import Path + +import pytest +import torch + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_eval_openvino_testing, + nncf_eval_testing, + nncf_export_testing, + nncf_optimize_testing, + nncf_validate_fq_testing, + otx_demo_deployment_testing, + otx_demo_openvino_testing, + otx_demo_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_explain_openvino_testing, + otx_explain_testing, + otx_export_testing, + otx_hpo_testing, + otx_resume_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, +) + +# Pre-train w/ 'label_0', 'label_1' classes +args0 = { + "--train-data-roots": "tests/assets/classification_dataset", + "--val-data-roots": "tests/assets/classification_dataset", + "--test-data-roots": "tests/assets/classification_dataset", + "--input": "tests/assets/classification_dataset/0", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", + ], +} + +# Pre-train w/ 'label_0', 'label_1', 'label_2' classes +args = { + "--train-data-roots": "tests/assets/classification_dataset_class_incremental", + "--val-data-roots": "tests/assets/classification_dataset_class_incremental", + "--test-data-roots": "tests/assets/classification_dataset_class_incremental", + "--input": "tests/assets/classification_dataset/0", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "4", + "--learning_parameters.batch_size", + "4", +] + +otx_dir = os.getcwd() + + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + os.path.join( + "src/otx/algorithms/classification", + "configs", + "efficientnet_b0_cls_incr", + "template.yaml", + ) + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] +else: + templates = Registry("src/otx/algorithms/classification").filter(task_type="CLASSIFICATION").templates + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsMultiClassClassification: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_supcon(self, template, tmp_dir_path): + if template.name == "DeiT-Tiny": + pytest.skip(reason="Supcon for ViT template is not supported yet.") + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_supcon" + args1 = copy.deepcopy(args) + args1["train_params"].extend(["--learning_parameters.enable_supcon", "True"]) + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_train_testing(template, tmp_dir_path, otx_dir, args0) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args0) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args0) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_explain_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.2, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_demo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_demo_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_demo_deployment_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "classification", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + nncf_eval_testing(template, tmp_dir_path, otx_dir, args, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + if template.name == "DeiT-Tiny": + pytest.skip(reason="Fake quantization for DeiT template is not supported yet.") + tmp_dir_path = tmp_dir_path / "multi_class_cls" + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "classification", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_multi_gpu" + args1 = copy.deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + +class TestToolsMultiClassSemiSLClassification: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + if not (Path(template.model_template_path).parent / "semisl").is_dir(): + pytest.skip(f"Semi-SL training type isn't available for {template.name}") + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_semisl" + args_semisl = copy.deepcopy(args0) + args_semisl["--unlabeled-data-roots"] = args["--train-data-roots"] + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + if not (Path(template.model_template_path).parent / "semisl").is_dir(): + pytest.skip(f"Semi-SL training type isn't available for {template.name}") + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_semisl" + otx_eval_testing(template, tmp_dir_path, otx_dir, args0) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train_semisl(self, template, tmp_dir_path): + if not (Path(template.model_template_path).parent / "semisl").is_dir(): + pytest.skip(f"Semi-SL training type isn't available for {template.name}") + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_multi_gpu_semisl" + args_semisl_multigpu = copy.deepcopy(args0) + args_semisl_multigpu["--unlabeled-data-roots"] = args["--train-data-roots"] + args_semisl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl_multigpu) + + +# Pre-train w/ 'car', 'tree' classes +args0_m = { + "--train-data-roots": "tests/assets/datumaro_multilabel", + "--val-data-roots": "tests/assets/datumaro_multilabel", + "--test-data-roots": "tests/assets/datumaro_multilabel", + "--input": "tests/assets/datumaro_multilabel/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", + ], +} + +# Class-Incremental learning w/ 'car', 'tree', 'bug' classes +# TODO: Not include incremental case yet +args_m = { + "--train-data-roots": "tests/assets/datumaro_multilabel", + "--val-data-roots": "tests/assets/datumaro_multilabel", + "--test-data-roots": "tests/assets/datumaro_multilabel", + "--input": "tests/assets/datumaro_multilabel/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", + ], +} + + +class TestToolsMultilabelClassification: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_train_testing(template, tmp_dir_path, otx_dir, args0_m) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args_m) + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args0_m) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args0_m) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_eval_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_explain_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args_m, threshold=0.2) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args_m, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "classification", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + nncf_eval_testing(template, tmp_dir_path, otx_dir, args_m, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + if template.name == "DeiT-Tiny": + pytest.skip(reason="Fake quantization for DeiT template is not supported yet.") + tmp_dir_path = tmp_dir_path / "multi_label_cls" + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "classification", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + ptq_eval_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls/test_multi_gpu" + args0 = copy.deepcopy(args_m) + args0["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args0) + + +args_h = { + "--train-data-roots": "tests/assets/datumaro_h-label", + "--val-data-roots": "tests/assets/datumaro_h-label", + "--test-data-roots": "tests/assets/datumaro_h-label", + "--input": "tests/assets/datumaro_h-label/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", + ], +} + + +class TestToolsHierarchicalClassification: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_train_testing(template, tmp_dir_path, otx_dir, args_h) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args_h) + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args_h) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args_h) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_eval_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_explain_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args_h, threshold=0.2) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args_h, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + nncf_eval_testing(template, tmp_dir_path, otx_dir, args_h, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "classification", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + ptq_eval_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + if template.name == "DeiT-Tiny": + pytest.skip(reason="Fake quantization for DeiT template is not supported yet.") + tmp_dir_path = tmp_dir_path / "h_label_cls" + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "classification", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls/test_multi_gpu" + args1 = copy.deepcopy(args_h) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + +# Warmstart using data w/ 'intel', 'openvino', 'opencv' classes +args_selfsl = { + "--train-data-roots": "tests/assets/classification_dataset", + "--train-type": "Selfsupervised", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + "--learning_parameters.learning_rate", + "1e-07", + ], +} + + +class TestToolsSelfSLClassification: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_selfsl_train(self, template, tmp_dir_path): + if template.name == "DeiT-Tiny": + pytest.skip(reason="Self-SL for ViT template is not supported yet.") + tmp_dir_path_1 = tmp_dir_path / "multi_class_cls/test_selfsl" + otx_train_testing(template, tmp_dir_path_1, otx_dir, args_selfsl) + template_work_dir = get_template_dir(template, tmp_dir_path_1) + assert os.path.exists(f"{template_work_dir}/selfsl") + args1 = copy.deepcopy(args) + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + tmp_dir_path_2 = tmp_dir_path / "multi_class_cls/test_selfsl_sl" + otx_train_testing(template, tmp_dir_path_2, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_selfsl_eval(self, template, tmp_dir_path): + if template.name == "DeiT-Tiny": + pytest.skip(reason="Self-SL for ViT template is not supported yet.") + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_selfsl_sl" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train_selfsl(self, template, tmp_dir_path): + if template.name == "DeiT-Tiny": + pytest.skip(reason="Self-SL for ViT template is not supported yet.") + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_multi_gpu_selfsl" + args_selfsl_multigpu = copy.deepcopy(args_selfsl) + args_selfsl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_selfsl_multigpu) + template_dir = get_template_dir(template, tmp_dir_path) + assert os.path.exists(f"{template_dir}/selfsl") diff --git a/tests/e2e/cli/detection/__init__.py b/tests/e2e/cli/detection/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/e2e/cli/detection/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_ATSS/compressed_model.yml b/tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_ATSS/compressed_model.yml new file mode 100644 index 00000000000..cb75f0cc794 --- /dev/null +++ b/tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_ATSS/compressed_model.yml @@ -0,0 +1,10 @@ +TestToolsOTXDetection: + nncf: + number_of_fakequantizers: 157 + ptq: + number_of_fakequantizers: 197 +TestToolsTilingDetection: + nncf: + number_of_fakequantizers: 157 + ptq: + number_of_fakequantizers: 197 diff --git a/tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_SSD/compressed_model.yml b/tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_SSD/compressed_model.yml new file mode 100644 index 00000000000..2750f865dea --- /dev/null +++ b/tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_SSD/compressed_model.yml @@ -0,0 +1,10 @@ +TestToolsOTXDetection: + nncf: + number_of_fakequantizers: 67 + ptq: + number_of_fakequantizers: 67 +TestToolsTilingDetection: + nncf: + number_of_fakequantizers: 67 + ptq: + number_of_fakequantizers: 67 diff --git a/tests/e2e/cli/detection/reference/Custom_Object_Detection_YOLOX/compressed_model.yml b/tests/e2e/cli/detection/reference/Custom_Object_Detection_YOLOX/compressed_model.yml new file mode 100644 index 00000000000..2e06761088a --- /dev/null +++ b/tests/e2e/cli/detection/reference/Custom_Object_Detection_YOLOX/compressed_model.yml @@ -0,0 +1,10 @@ +TestToolsOTXDetection: + nncf: + number_of_fakequantizers: 84 + ptq: + number_of_fakequantizers: 84 +TestToolsTilingDetection: + nncf: + number_of_fakequantizers: 84 + ptq: + number_of_fakequantizers: 84 diff --git a/tests/e2e/cli/detection/reference/Object_Detection_ResNeXt101_ATSS/compressed_model.yml b/tests/e2e/cli/detection/reference/Object_Detection_ResNeXt101_ATSS/compressed_model.yml new file mode 100644 index 00000000000..8d509aaff37 --- /dev/null +++ b/tests/e2e/cli/detection/reference/Object_Detection_ResNeXt101_ATSS/compressed_model.yml @@ -0,0 +1,10 @@ +TestToolsOTXDetection: + nncf: + number_of_fakequantizers: 232 + ptq: + number_of_fakequantizers: 272 +TestToolsTilingDetection: + nncf: + number_of_fakequantizers: 232 + ptq: + number_of_fakequantizers: 272 diff --git a/tests/e2e/cli/detection/reference/Object_Detection_YOLOX_L/compressed_model.yml b/tests/e2e/cli/detection/reference/Object_Detection_YOLOX_L/compressed_model.yml new file mode 100644 index 00000000000..62c472944ef --- /dev/null +++ b/tests/e2e/cli/detection/reference/Object_Detection_YOLOX_L/compressed_model.yml @@ -0,0 +1,10 @@ +TestToolsOTXDetection: + nncf: + number_of_fakequantizers: 146 + ptq: + number_of_fakequantizers: 146 +TestToolsTilingDetection: + nncf: + number_of_fakequantizers: 146 + ptq: + number_of_fakequantizers: 146 diff --git a/tests/e2e/cli/detection/reference/Object_Detection_YOLOX_S/compressed_model.yml b/tests/e2e/cli/detection/reference/Object_Detection_YOLOX_S/compressed_model.yml new file mode 100644 index 00000000000..1d578e9a747 --- /dev/null +++ b/tests/e2e/cli/detection/reference/Object_Detection_YOLOX_S/compressed_model.yml @@ -0,0 +1,10 @@ +TestToolsOTXDetection: + nncf: + number_of_fakequantizers: 108 + ptq: + number_of_fakequantizers: 108 +TestToolsTilingDetection: + nncf: + number_of_fakequantizers: 108 + ptq: + number_of_fakequantizers: 108 diff --git a/tests/e2e/cli/detection/reference/Object_Detection_YOLOX_X/compressed_model.yml b/tests/e2e/cli/detection/reference/Object_Detection_YOLOX_X/compressed_model.yml new file mode 100644 index 00000000000..8e2b9860534 --- /dev/null +++ b/tests/e2e/cli/detection/reference/Object_Detection_YOLOX_X/compressed_model.yml @@ -0,0 +1,10 @@ +TestToolsOTXDetection: + nncf: + number_of_fakequantizers: 177 + ptq: + number_of_fakequantizers: 177 +TestToolsTilingDetection: + nncf: + number_of_fakequantizers: 177 + ptq: + number_of_fakequantizers: 177 diff --git a/tests/e2e/cli/detection/test_api_xai_sanity_detection.py b/tests/e2e/cli/detection/test_api_xai_sanity_detection.py new file mode 100644 index 00000000000..4cd11fa937c --- /dev/null +++ b/tests/e2e/cli/detection/test_api_xai_sanity_detection.py @@ -0,0 +1,171 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import os.path as osp +import tempfile + +import torch + +from otx.algorithms.common.configs.configuration_enums import InputSizePreset +from otx.algorithms.common.utils import set_random_seed +from otx.algorithms.detection.adapters.mmdet.task import MMDetectionTask +from otx.algorithms.detection.adapters.openvino.task import OpenVINODetectionTask +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ( + ModelEntity, +) +from otx.api.entities.subset import Subset +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.cli.utils.io import read_model, save_model_data +from tests.e2e.cli.classification.test_api_xai_sanity_classification import saliency_maps_check +from tests.integration.api.detection.api_detection import DetectionTaskAPIBase, DEFAULT_DET_TEMPLATE_DIR +from tests.test_suite.e2e_test_system import e2e_pytest_api + +set_random_seed(0) + +assert_text_explain_all = "The number of saliency maps should be equal to the number of all classes." +assert_text_explain_predicted = "The number of saliency maps should be equal to the number of predicted classes." + + +class TestOVDetXAIAPI(DetectionTaskAPIBase): + ref_raw_saliency_shapes = { + "MobileNetV2-ATSS": (16, 16), # Need to be adapted to configurable or adaptive input size + } + + @e2e_pytest_api + def test_inference_xai(self): + with tempfile.TemporaryDirectory() as temp_dir: + hyper_parameters, model_template = self.setup_configurable_parameters( + DEFAULT_DET_TEMPLATE_DIR, num_iters=15 + ) + hyper_parameters.learning_parameters.input_size = InputSizePreset._512x512 # To fix saliency map size + task_env, dataset = self.init_environment(hyper_parameters, model_template, 10) + + train_task = MMDetectionTask(task_environment=task_env) + trained_model = ModelEntity( + dataset, + task_env.get_model_configuration(), + ) + train_task.train(dataset, trained_model, TrainParameters()) + save_model_data(trained_model, temp_dir) + + for processed_saliency_maps, only_predicted in [[True, False], [False, True]]: + task_env, dataset = self.init_environment(hyper_parameters, model_template, 10) + inference_parameters = InferenceParameters( + is_evaluation=False, + process_saliency_maps=processed_saliency_maps, + explain_predicted_classes=only_predicted, + ) + + # Infer torch model + task_env.model = trained_model + inference_task = MMDetectionTask(task_environment=task_env) + val_dataset = dataset.get_subset(Subset.VALIDATION) + predicted_dataset = inference_task.infer(val_dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps torch task + task_labels = trained_model.configuration.get_label_schema().get_labels(include_empty=False) + saliency_maps_check( + predicted_dataset, + task_labels, + self.ref_raw_saliency_shapes[model_template.name], + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + # Save OV IR model + inference_task._model_ckpt = osp.join(temp_dir, "weights.pth") + exported_model = ModelEntity(None, task_env.get_model_configuration()) + inference_task.export(ExportType.OPENVINO, exported_model, dump_features=True) + os.makedirs(temp_dir, exist_ok=True) + save_model_data(exported_model, temp_dir) + + # Infer OV IR model + load_weights_ov = osp.join(temp_dir, "openvino.xml") + task_env.model = read_model(task_env.get_model_configuration(), load_weights_ov, None) + task = OpenVINODetectionTask(task_environment=task_env) + _, dataset = self.init_environment(hyper_parameters, model_template, 10) + predicted_dataset_ov = task.infer(dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps OV task + saliency_maps_check( + predicted_dataset_ov, + task_labels, + self.ref_raw_saliency_shapes[model_template.name], + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + +# disable test until fix in PR#2337 is merged +# class TestOVDetTilXAIAPI(DetectionTaskAPIBase): +# ref_raw_saliency_shapes = { +# "ATSS": (6, 8), +# } + +# @e2e_pytest_api +# def test_inference_xai(self): +# with tempfile.TemporaryDirectory() as temp_dir: +# hyper_parameters, model_template = self.setup_configurable_parameters( +# DEFAULT_DET_TEMPLATE_DIR, num_iters=10, tiling=True +# ) +# task_env, dataset = self.init_environment(hyper_parameters, model_template, 10) + +# train_task = MMDetectionTask(task_environment=task_env) +# trained_model = ModelEntity( +# dataset, +# task_env.get_model_configuration(), +# ) +# train_task.train(dataset, trained_model, TrainParameters()) +# save_model_data(trained_model, temp_dir) + +# for processed_saliency_maps, only_predicted in [[True, False], [False, True]]: +# task_env, dataset = self.init_environment(hyper_parameters, model_template, 10) +# inference_parameters = InferenceParameters( +# is_evaluation=False, +# process_saliency_maps=processed_saliency_maps, +# explain_predicted_classes=only_predicted, +# ) + +# # Infer torch model +# task_env.model = trained_model +# inference_task = MMDetectionTask(task_environment=task_env) +# val_dataset = dataset.get_subset(Subset.VALIDATION) +# predicted_dataset = inference_task.infer(val_dataset.with_empty_annotations(), inference_parameters) + +# # Check saliency maps torch task +# task_labels = trained_model.configuration.get_label_schema().get_labels(include_empty=False) +# saliency_maps_check( +# predicted_dataset, +# task_labels, +# self.ref_raw_saliency_shapes[model_template.name], +# processed_saliency_maps=processed_saliency_maps, +# only_predicted=only_predicted, +# ) + +# # Save OV IR model +# inference_task._model_ckpt = osp.join(temp_dir, "weights.pth") +# exported_model = ModelEntity(None, task_env.get_model_configuration()) +# inference_task.export(ExportType.OPENVINO, exported_model, dump_features=True) +# os.makedirs(temp_dir, exist_ok=True) +# save_model_data(exported_model, temp_dir) + +# # Infer OV IR model +# load_weights_ov = osp.join(temp_dir, "openvino.xml") +# task_env.model = read_model(task_env.get_model_configuration(), load_weights_ov, None) +# task = OpenVINODetectionTask(task_environment=task_env) +# _, dataset = self.init_environment(hyper_parameters, model_template, 10) +# inference_parameters.enable_async_inference = False +# predicted_dataset_ov = task.infer(dataset.with_empty_annotations(), inference_parameters) + +# # Check saliency maps OV task +# saliency_maps_check( +# predicted_dataset_ov, +# task_labels, +# self.ref_raw_saliency_shapes[model_template.name], +# processed_saliency_maps=processed_saliency_maps, +# only_predicted=only_predicted, +# ) diff --git a/tests/e2e/cli/detection/test_detection.py b/tests/e2e/cli/detection/test_detection.py new file mode 100644 index 00000000000..cc2efcd0b89 --- /dev/null +++ b/tests/e2e/cli/detection/test_detection.py @@ -0,0 +1,356 @@ +"""Tests for Class-Incremental Learning for object detection with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import os +from pathlib import Path + +import pytest +import torch + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_eval_openvino_testing, + nncf_eval_testing, + nncf_export_testing, + nncf_optimize_testing, + nncf_validate_fq_testing, + otx_demo_deployment_testing, + otx_demo_openvino_testing, + otx_demo_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_explain_all_classes_openvino_testing, + otx_explain_openvino_testing, + otx_explain_process_saliency_maps_openvino_testing, + otx_explain_testing, + otx_explain_testing_all_classes, + otx_explain_testing_process_saliency_maps, + otx_export_testing, + otx_hpo_testing, + otx_resume_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, +) + +# Pre-train w/ 'person' class +args0 = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": ["params", "--learning_parameters.num_iters", "7", "--learning_parameters.batch_size", "4"], +} + +# Class-Incremental learning w/ 'vehicle', 'person', 'non-vehicle' classes +args = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": ["params", "--learning_parameters.num_iters", "5", "--learning_parameters.batch_size", "4"], +} + +args_semisl = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--unlabeled-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": ["params", "--learning_parameters.num_iters", "2", "--learning_parameters.batch_size", "2"], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "8", + "--learning_parameters.batch_size", + "4", +] + +otx_dir = os.getcwd() + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + os.path.join("src/otx/algorithms/detection/configs", "detection", "mobilenetv2_atss", "template.yaml") + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] +else: + _templates = Registry("src/otx/algorithms/detection").filter(task_type="DETECTION").templates + templates = [] + for template in _templates: + if template.name not in ["YOLOX-S", "YOLOX-X"]: + templates.append(template) # YOLOX-S, and YOLOX-X use same model and data pipeline config with YOLOX-L + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsOTXDetection: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_train_testing(template, tmp_dir_path, otx_dir, args0) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "detection" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + if template.name == "YOLOX-L" or template.name == "SSD": + pytest.skip(reason="Issue#2548: Exported model performance is too low") + tmp_dir_path = tmp_dir_path / "detection" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.2, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_testing(template, tmp_dir_path, otx_dir, args, trained=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_all_classes(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_testing_all_classes(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_process_saliency_maps(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_testing_process_saliency_maps(template, tmp_dir_path, otx_dir, args, trained=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args, trained=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_all_classes_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_all_classes_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_process_saliency_maps_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_process_saliency_maps_openvino_testing(template, tmp_dir_path, otx_dir, args, trained=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_demo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_demo_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + if template.name == "YOLOX-L": + pytest.skip(reason="Issue#2518: YOLOX-L, Tiling-ATSS showed 0.0 after export") + if template.name == "SSD": + pytest.skip(reason="Issue#2548: Exported model performance is too low") + tmp_dir_path = tmp_dir_path / "detection" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_demo_deployment_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "detection", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_testing(template, tmp_dir_path, otx_dir, args, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "detection", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection/test_multi_gpu" + args1 = copy.deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + +class TestToolsOTXSemiSLDetection: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + if not (Path(template.model_template_path).parent / "semisl").is_dir(): + pytest.skip(f"Semi-SL training type isn't available for {template.name}") + tmp_dir_path = tmp_dir_path / "detection/test_semisl" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + if not (Path(template.model_template_path).parent / "semisl").is_dir(): + pytest.skip(f"Semi-SL training type isn't available for {template.name}") + tmp_dir_path = tmp_dir_path / "detection/test_semisl" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train_semisl(self, template, tmp_dir_path): + if not (Path(template.model_template_path).parent / "semisl").is_dir(): + pytest.skip(f"Semi-SL training type isn't available for {template.name}") + if template.name == "ResNeXt101-ATSS": + pytest.skip(f"Issue#2705: multi-gpu training e2e test failure for {template.name}") + tmp_dir_path = tmp_dir_path / "detection/test_multi_gpu_semisl" + args_semisl_multigpu = copy.deepcopy(args_semisl) + args_semisl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl_multigpu) diff --git a/tests/e2e/cli/detection/test_tiling_detection.py b/tests/e2e/cli/detection/test_tiling_detection.py new file mode 100644 index 00000000000..b123f5dc502 --- /dev/null +++ b/tests/e2e/cli/detection/test_tiling_detection.py @@ -0,0 +1,262 @@ +"""Tests for OTX Class-Incremental Learning for object detection with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import os + +import pytest + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_eval_openvino_testing, + nncf_eval_testing, + nncf_export_testing, + nncf_optimize_testing, + nncf_validate_fq_testing, + otx_demo_deployment_testing, + otx_demo_openvino_testing, + otx_demo_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_explain_openvino_testing, + otx_explain_testing, + otx_export_testing, + otx_hpo_testing, + otx_resume_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, +) + +args = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "10", + "--learning_parameters.batch_size", + "2", + "--tiling_parameters.enable_tiling", + "1", + "--tiling_parameters.enable_adaptive_params", + "1", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "15", + "--learning_parameters.batch_size", + "4", +] + +otx_dir = os.getcwd() + +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + os.path.join("src/otx/algorithms/detection/configs", "detection", "mobilenetv2_atss", "template.yaml") + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] +else: + _templates = Registry("src/otx/algorithms/detection").filter(task_type="DETECTION").templates + templates = [] + for template in _templates: + if template.name not in ["YOLOX-S", "YOLOX-X"]: + templates.append(template) # YOLOX-S, and YOLOX-X use same model and data pipeline config with YOLOX-L + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsTilingDetection: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.2, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_explain_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_demo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_demo_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + if template.name == "MobileNetV2-ATSS": + pytest.skip(reason="Issue#2518: YOLOX-L, Tiling-ATSS showed 0.0 after export") + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_demo_deployment_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip("Tiling w/ HPO fails in CI") + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "detection", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_testing(template, tmp_dir_path, otx_dir, args, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "detection", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/e2e/cli/instance_segmentation/__init__.py b/tests/e2e/cli/instance_segmentation/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/e2e/cli/instance_segmentation/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_ConvNeXt/compressed_model.yml b/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_ConvNeXt/compressed_model.yml new file mode 100644 index 00000000000..054212713fc --- /dev/null +++ b/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_ConvNeXt/compressed_model.yml @@ -0,0 +1,10 @@ +TestToolsOTXInstanceSegmentation: + ptq: + number_of_fakequantizers: 76 + nncf: + number_of_fakequantizers: 148 +TestToolsTilingInstanceSegmentation: + ptq: + number_of_fakequantizers: 76 + nncf: + number_of_fakequantizers: 148 diff --git a/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B/compressed_model.yml b/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B/compressed_model.yml new file mode 100644 index 00000000000..ed9ad16aa47 --- /dev/null +++ b/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B/compressed_model.yml @@ -0,0 +1,14 @@ +TestToolsOTXInstanceSegmentation: + nncf: + number_of_fakequantizers: 204 + pot: + number_of_fakequantizers: 137 + ptq: + number_of_fakequantizers: 160 +TestToolsTilingInstanceSegmentation: + nncf: + number_of_fakequantizers: 204 + pot: + number_of_fakequantizers: 137 + ptq: + number_of_fakequantizers: 160 diff --git a/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50/compressed_model.yml b/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50/compressed_model.yml new file mode 100644 index 00000000000..5e78888bffc --- /dev/null +++ b/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50/compressed_model.yml @@ -0,0 +1,14 @@ +TestToolsOTXInstanceSegmentation: + nncf: + number_of_fakequantizers: 97 + pot: + number_of_fakequantizers: 99 + ptq: + number_of_fakequantizers: 99 +TestToolsTilingInstanceSegmentation: + nncf: + number_of_fakequantizers: 97 + pot: + number_of_fakequantizers: 99 + ptq: + number_of_fakequantizers: 99 diff --git a/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_SwinT_FP16/compressed_model.yml b/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_SwinT_FP16/compressed_model.yml new file mode 100644 index 00000000000..eb3dc2a8a01 --- /dev/null +++ b/tests/e2e/cli/instance_segmentation/reference/Custom_Counting_Instance_Segmentation_MaskRCNN_SwinT_FP16/compressed_model.yml @@ -0,0 +1,10 @@ +TestToolsOTXInstanceSegmentation: + nncf: + number_of_fakequantizers: 193 + ptq: + number_of_fakequantizers: 180 +TestToolsTilingInstanceSegmentation: + nncf: + number_of_fakequantizers: 193 + ptq: + number_of_fakequantizers: 180 diff --git a/tests/e2e/cli/instance_segmentation/test_api_xai_sanity_instance_segmentation.py b/tests/e2e/cli/instance_segmentation/test_api_xai_sanity_instance_segmentation.py new file mode 100644 index 00000000000..00a019b3e04 --- /dev/null +++ b/tests/e2e/cli/instance_segmentation/test_api_xai_sanity_instance_segmentation.py @@ -0,0 +1,141 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import os.path as osp +import pytest + +import torch + +from otx.algorithms.common.utils.utils import is_xpu_available +from otx.algorithms.detection.adapters.mmdet.task import MMDetectionTask +from otx.algorithms.detection.adapters.openvino.task import OpenVINODetectionTask +from otx.algorithms.detection.configs.base import DetectionConfig +from otx.api.configuration.helper import create +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ( + ModelConfiguration, + ModelEntity, +) +from otx.api.entities.subset import Subset +from otx.api.entities.train_parameters import TrainParameters +from otx.api.entities.model_template import parse_model_template, TaskType +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.cli.utils.io import read_model, save_model_data +from tests.e2e.cli.classification.test_api_xai_sanity_classification import saliency_maps_check +from tests.test_suite.e2e_test_system import e2e_pytest_api +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_ISEG_TEMPLATE_DIR, + init_environment, + generate_det_dataset, +) + +torch.manual_seed(0) + +assert_text_explain_all = "The number of saliency maps should be equal to the number of all classes." +assert_text_explain_predicted = "The number of saliency maps should be equal to the number of predicted classes." + +if is_xpu_available(): + pytest.skip("Instance segmentation task is not supported on XPU", allow_module_level=True) + + +class TestISegmXAIAPI: + def _prepare_task_env(self, temp_dir, train=True, tile=False): + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.num_iters = 5 + if tile: + hyper_parameters.tiling_parameters.enable_tiling = True + task_env = init_environment(hyper_parameters, model_template, task_type=TaskType.INSTANCE_SEGMENTATION) + + iseg_dataset, iseg_labels = generate_det_dataset(TaskType.INSTANCE_SEGMENTATION, 100) + iseg_label_schema = LabelSchemaEntity() + iseg_label_group = LabelGroup( + name="labels", + labels=iseg_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + iseg_label_schema.add_group(iseg_label_group) + + _config = ModelConfiguration(DetectionConfig(), iseg_label_schema) + trained_model = ModelEntity( + iseg_dataset, + _config, + ) + + if train: + train_task = MMDetectionTask(task_env) + train_task.train(iseg_dataset, trained_model, TrainParameters()) + + save_model_data(trained_model, temp_dir) + task_env = init_environment(hyper_parameters, model_template, task_type=TaskType.INSTANCE_SEGMENTATION) + task_env.model = trained_model + + return task_env, iseg_dataset + + def _export_and_save_ov_ir_model(self, temp_dir, task_env, inference_task): + inference_task._model_ckpt = osp.join(temp_dir, "weights.pth") + exported_model = ModelEntity(None, task_env.get_model_configuration()) + inference_task.export(ExportType.OPENVINO, exported_model, dump_features=True) + os.makedirs(temp_dir, exist_ok=True) + save_model_data(exported_model, temp_dir) + + @e2e_pytest_api + @pytest.mark.parametrize("tile", [True, False]) + def test_torch_xai_inference(self, tile, tmp_dir_path): + if tile: + tmp_dir_path = tmp_dir_path / "tile" + else: + tmp_dir_path = tmp_dir_path / "no_tile" + task_env, iseg_dataset = self._prepare_task_env(tmp_dir_path, tile=tile) + inference_task = MMDetectionTask(task_environment=task_env) + val_dataset = iseg_dataset.get_subset(Subset.VALIDATION) + inference_parameters = InferenceParameters( + is_evaluation=False, + ) + predicted_dataset = inference_task.infer(val_dataset.with_empty_annotations(), inference_parameters) + task_labels = task_env.model.configuration.get_label_schema().get_labels(include_empty=False) + + if tile: + ref_shape = None + else: + ref_shape = (28, 28) + saliency_maps_check( + predicted_dataset, + task_labels, + ref_shape, + processed_saliency_maps=False, + only_predicted=True, + ) + self._export_and_save_ov_ir_model(tmp_dir_path, task_env, inference_task) + + @pytest.mark.parametrize( + "tile, enable_async_inference", [[True, True], [True, False], [False, False], [False, True]] + ) + def test_ov_xai_inference(self, tile, enable_async_inference, tmp_dir_path): + if tile: + tmp_dir_path = tmp_dir_path / "tile" + pytest.skip(reason="[Issue#2434] Need to fix merging sailency map") + else: + tmp_dir_path = tmp_dir_path / "no_tile" + task_env, iseg_dataset = self._prepare_task_env(tmp_dir_path, train=False, tile=tile) + load_weights_ov = osp.join(tmp_dir_path, "openvino.xml") + task_env.model = read_model(task_env.get_model_configuration(), load_weights_ov, None) + task = OpenVINODetectionTask(task_environment=task_env) + inference_parameters = InferenceParameters( + is_evaluation=False, + ) + inference_parameters.enable_async_inference = enable_async_inference + val_dataset = iseg_dataset.get_subset(Subset.VALIDATION) + predicted_dataset_ov = task.infer(val_dataset.with_empty_annotations(), inference_parameters) + task_labels = task_env.model.configuration.get_label_schema().get_labels(include_empty=False) + + saliency_maps_check( + predicted_dataset_ov, + task_labels, + (480, 640), + processed_saliency_maps=False, + only_predicted=True, + ) diff --git a/tests/e2e/cli/instance_segmentation/test_instance_segmentation.py b/tests/e2e/cli/instance_segmentation/test_instance_segmentation.py new file mode 100644 index 00000000000..aec8858c4c7 --- /dev/null +++ b/tests/e2e/cli/instance_segmentation/test_instance_segmentation.py @@ -0,0 +1,347 @@ +"""Tests for Class-Incremental Learning for object detection with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import os +from pathlib import Path + +import pytest +import torch + +from otx.algorithms.common.utils.utils import is_xpu_available +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_eval_openvino_testing, + nncf_eval_testing, + nncf_export_testing, + nncf_optimize_testing, + nncf_validate_fq_testing, + otx_demo_deployment_testing, + otx_demo_openvino_testing, + otx_demo_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_explain_openvino_testing, + otx_explain_testing, + otx_export_testing, + otx_hpo_testing, + otx_resume_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, +) + +# Pre-train w/ 'car & tree' class +args0 = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": ["params", "--learning_parameters.num_iters", "5", "--learning_parameters.batch_size", "2"], +} + +# Class-Incremental learning w/ 'car', 'tree', 'bug' classes ## TODO: add class incr sample +args = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": ["params", "--learning_parameters.num_iters", "5", "--learning_parameters.batch_size", "2"], +} + +# Semi-SL +args_semisl = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--unlabeled-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": ["params", "--learning_parameters.num_iters", "5", "--learning_parameters.batch_size", "2"], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "8", + "--learning_parameters.batch_size", + "2", +] + +if is_xpu_available(): + pytest.skip("Instance segmentation task is not supported on XPU", allow_module_level=True) + +otx_dir = os.getcwd() + +iseg_config_root = Path("src/otx/algorithms/detection/configs/instance_segmentation") + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template(iseg_config_root / "resnet50_maskrcnn" / "template.yaml") + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] +else: + templates = Registry("src/otx/algorithms/detection").filter(task_type="INSTANCE_SEGMENTATION").templates + templates_ids = [template.model_template_id for template in templates] + # add experimental templates for new inst-seg models. In the future we will update them as main templates + # but we need to start to test them now. + templates_with_experimental = copy.deepcopy(templates) + templates_ids_with_experimental = copy.deepcopy(templates_ids) + for experimental_template in iseg_config_root.glob("**/*_experimental.yaml"): + template_experimental = parse_model_template(experimental_template) + templates_with_experimental.extend([template_experimental]) + templates_ids_with_experimental.extend([template_experimental.model_template_id]) + + +class TestToolsOTXInstanceSegmentation: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_train_testing(template, tmp_dir_path, otx_dir, args0) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args0) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args0) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + @pytest.mark.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.2, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_explain_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + @pytest.mark.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_demo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_demo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + @pytest.mark.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + def test_otx_demo_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_demo_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + @pytest.mark.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + @pytest.mark.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + @pytest.mark.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + def test_otx_demo_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_demo_deployment_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "instance_segmentation", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip( + reason="Issue#2234 otx eval with nncf optimized model shows different performance with final evaluation when otx optimize" + ) + def test_nncf_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_testing(template, tmp_dir_path, otx_dir, args, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip( + reason="Issue#2234 otx eval with nncf optimized model shows different performance with final evaluation when otx optimize" + ) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + if "MaskRCNN-ConvNeXt" in template.name: + pytest.skip("CVS-118373 ConvNeXt Compilation Error in PTQ") + tmp_dir_path = tmp_dir_path / "ins_seg" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + if "MaskRCNN-ConvNeXt" in template.name: + pytest.skip("CVS-118373 ConvNeXt Compilation Error in PTQ") + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "instance_segmentation", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + if "MaskRCNN-ConvNeXt" in template.name: + pytest.skip("CVS-118373 ConvNeXt Compilation Error in PTQ") + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg/test_multi_gpu" + args1 = copy.deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + +class TestToolsOTXSemiSLInstanceSegmentation: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + if not (Path(template.model_template_path).parent / "semisl").is_dir(): + pytest.skip("Semi-SL training type isn't available for this template") + tmp_dir_path = tmp_dir_path / "ins_seg/test_semisl" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl) + template_dir = get_template_dir(template, tmp_dir_path) + assert (Path(template_dir) / "semisl").is_dir() + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + if not (Path(template.model_template_path).parent / "semisl").is_dir(): + pytest.skip("Semi-SL training type isn't available for this template") + tmp_dir_path = tmp_dir_path / "ins_seg/test_semisl" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train_semisl(self, template, tmp_dir_path): + if not (Path(template.model_template_path).parent / "semisl").is_dir(): + pytest.skip("Semi-SL training type isn't available for this template") + tmp_dir_path = tmp_dir_path / "ins_seg/test_multi_gpu_semisl" + args_semisl_multigpu = copy.deepcopy(args_semisl) + args_semisl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl_multigpu) + template_dir = get_template_dir(template, tmp_dir_path) + assert (Path(template_dir) / "semisl").is_dir() diff --git a/tests/e2e/cli/instance_segmentation/test_tiling_instseg.py b/tests/e2e/cli/instance_segmentation/test_tiling_instseg.py new file mode 100644 index 00000000000..d4c747dd6e9 --- /dev/null +++ b/tests/e2e/cli/instance_segmentation/test_tiling_instseg.py @@ -0,0 +1,281 @@ +"""Tests for OTX Class-Incremental Learning for instance segmentation with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import os + +import pytest + +from otx.algorithms.common.utils.utils import is_xpu_available +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_eval_openvino_testing, + nncf_eval_testing, + nncf_export_testing, + nncf_optimize_testing, + nncf_validate_fq_testing, + otx_demo_deployment_testing, + otx_demo_openvino_testing, + otx_demo_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_explain_openvino_testing, + otx_explain_testing, + otx_export_testing, + otx_hpo_testing, + otx_resume_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, +) + +args = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "10", + "--learning_parameters.batch_size", + "4", + "--tiling_parameters.enable_tiling", + "1", + "--tiling_parameters.enable_adaptive_params", + "1", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "15", + "--learning_parameters.batch_size", + "4", +] + +if is_xpu_available(): + pytest.skip("Instance segmentation task is not supported on XPU", allow_module_level=True) + +otx_dir = os.getcwd() + +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + os.path.join( + "src/otx/algorithms/detection/configs", "instance_segmentation", "resnet50_maskrcnn", "template.yaml" + ) + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] +else: + templates = Registry("src/otx/algorithms/detection").filter(task_type="INSTANCE_SEGMENTATION").templates + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsTilingInstanceSegmentation: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if "ResNet50" in template.name: + pytest.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.6, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_explain_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if "ResNet50" in template.name: + pytest.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_demo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if "ResNet50" in template.name: + pytest.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + otx_demo_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + if "ResNet50" in template.name: + pytest.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if "ResNet50" in template.name: + pytest.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if "ResNet50" in template.name: + pytest.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + otx_demo_deployment_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skip(reason="Issue#2167: E2E test is stuck while testing HPO with i-seg tiling tasks") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "instance_segmentation", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_eval_testing(template, tmp_dir_path, otx_dir, args, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + if "MaskRCNN-ConvNeXt" in template.name: + pytest.skip("CVS-118373 ConvNeXt Compilation Error in PTQ") + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if "MaskRCNN-ConvNeXt" in template.name: + pytest.skip("CVS-118373 ConvNeXt Compilation Error in PTQ") + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if "MaskRCNN-ConvNeXt" in template.name: + pytest.skip("CVS-118373 ConvNeXt Compilation Error in PTQ") + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "instance_segmentation", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if "MaskRCNN-ConvNeXt" in template.name: + pytest.skip("CVS-118373 ConvNeXt Compilation Error in PTQ") + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/e2e/cli/semantic_segmentation/__init__.py b/tests/e2e/cli/semantic_segmentation/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/e2e/cli/semantic_segmentation/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR/compressed_model.yml b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR/compressed_model.yml new file mode 100644 index 00000000000..96a4531d110 --- /dev/null +++ b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsOTXSegmentation: + nncf: + number_of_fakequantizers: 586 + ptq: + number_of_fakequantizers: 15 diff --git a/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-18_OCR/compressed_model.yml b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-18_OCR/compressed_model.yml new file mode 100644 index 00000000000..8cc0f9880bb --- /dev/null +++ b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-18_OCR/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsOTXSegmentation: + nncf: + number_of_fakequantizers: 586 + ptq: + number_of_fakequantizers: 600 diff --git a/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-s-mod2_OCR/compressed_model.yml b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-s-mod2_OCR/compressed_model.yml new file mode 100644 index 00000000000..a11dca7b741 --- /dev/null +++ b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-s-mod2_OCR/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsOTXSegmentation: + nncf: + number_of_fakequantizers: 436 + ptq: + number_of_fakequantizers: 368 diff --git a/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-x-mod3_OCR/compressed_model.yml b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-x-mod3_OCR/compressed_model.yml new file mode 100644 index 00000000000..ccc409483e6 --- /dev/null +++ b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_Lite-HRNet-x-mod3_OCR/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsOTXSegmentation: + nncf: + number_of_fakequantizers: 1138 + ptq: + number_of_fakequantizers: 1026 diff --git a/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_B/compressed_model.yml b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_B/compressed_model.yml new file mode 100644 index 00000000000..155d6d27d89 --- /dev/null +++ b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_B/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsOTXSegmentation: + nncf: + number_of_fakequantizers: 600 + ptq: + number_of_fakequantizers: 587 diff --git a/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_s/compressed_model.yml b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_s/compressed_model.yml new file mode 100644 index 00000000000..b6f6655bfc7 --- /dev/null +++ b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_s/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsOTXSegmentation: + nncf: + number_of_fakequantizers: 336 + ptq: + number_of_fakequantizers: 334 diff --git a/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_t/compressed_model.yml b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_t/compressed_model.yml new file mode 100644 index 00000000000..8056c63985e --- /dev/null +++ b/tests/e2e/cli/semantic_segmentation/reference/Custom_Semantic_Segmentation_SegNext_t/compressed_model.yml @@ -0,0 +1,5 @@ +TestToolsOTXSegmentation: + nncf: + number_of_fakequantizers: 408 + ptq: + number_of_fakequantizers: 182 diff --git a/tests/e2e/cli/semantic_segmentation/test_segmentation.py b/tests/e2e/cli/semantic_segmentation/test_segmentation.py new file mode 100644 index 00000000000..ead9ed6fb42 --- /dev/null +++ b/tests/e2e/cli/semantic_segmentation/test_segmentation.py @@ -0,0 +1,335 @@ +"""Tests for Semantic segmentation with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import os +from pathlib import Path +import pytest +import torch + +from otx.algorithms.common.utils.utils import is_xpu_available +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_eval_openvino_testing, + nncf_eval_testing, + nncf_export_testing, + nncf_optimize_testing, + nncf_validate_fq_testing, + otx_demo_deployment_testing, + otx_demo_openvino_testing, + otx_demo_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_hpo_testing, + otx_resume_testing, + otx_train_testing, + ptq_eval_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, +) + +# TODO: Currently, it is closed to sample test. need to change other sample +args = { + "--train-data-roots": "tests/assets/common_semantic_segmentation_dataset/train", + "--val-data-roots": "tests/assets/common_semantic_segmentation_dataset/val", + "--test-data-roots": "tests/assets/common_semantic_segmentation_dataset/val", + "--input": "tests/assets/common_semantic_segmentation_dataset/train/images", + "train_params": [ + "params", + "--learning_parameters.learning_rate_warmup_iters", + "25", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "4", + "--learning_parameters.batch_size", + "4", +] + +if is_xpu_available(): + pytest.skip("Semantic segmentation task is not supported on XPU", allow_module_level=True) + +otx_dir = Path.cwd() + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + Path("src/otx/algorithms/segmentation/configs") / "ocr_lite_hrnet_18_mod2" / "template.yaml" + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] + +else: + templates = [ + template + for template in Registry("src/otx/algorithms/segmentation").filter(task_type="SEGMENTATION").templates + if "SegNext" not in template.model_template_id + ] + # add one custom model template for new segmentation models. In the future we will update them as main templates + # we need to start to test them now. For time saving - one new model will be validated + custom_model = parse_model_template( + Path("src/otx/algorithms/segmentation/configs") / "ham_segnext_t" / "template.yaml" + ) + templates.append(custom_model) + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsOTXSegmentation: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.2, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_demo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_demo_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=0.0) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_demo_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_demo_deployment_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + nncf_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "semantic_segmentation", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + nncf_eval_testing(template, tmp_dir_path, otx_dir, args, threshold=0.01) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + if template.name not in ["SegNext-t", "Lite-HRNet-x-mod3"]: + pytest.skip(reason="Skip the majority of models to reduce PTQ running time.") + tmp_dir_path = tmp_dir_path / "segmentation" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + if template.name not in ["SegNext-t", "Lite-HRNet-x-mod3"]: + pytest.skip(reason="Skip the majority of models to reduce PTQ running time.") + tmp_dir_path = tmp_dir_path / "segmentation" + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "semantic_segmentation", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + if template.name not in ["SegNext-t", "Lite-HRNet-x-mod3"]: + pytest.skip(reason="Skip the majority of models to reduce PTQ running time.") + tmp_dir_path = tmp_dir_path / "segmentation" + ptq_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_multi_gpu" + args1 = copy.deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + +args_semisl = { + "--train-data-roots": "tests/assets/common_semantic_segmentation_dataset/train", + "--val-data-roots": "tests/assets/common_semantic_segmentation_dataset/val", + "--test-data-roots": "tests/assets/common_semantic_segmentation_dataset/val", + "--unlabeled-data-roots": "tests/assets/common_semantic_segmentation_dataset/train", + "train_params": ["params", "--learning_parameters.num_iters", "2", "--learning_parameters.batch_size", "4"], +} + + +class TestToolsOTXSemiSLSegmentation: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_semisl" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_semisl" + otx_eval_testing(template, tmp_dir_path, otx_dir, args_semisl) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train_semisl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_multi_gpu_semisl" + args_semisl_multigpu = copy.deepcopy(args_semisl) + args_semisl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl_multigpu) + + +args_selfsl = { + "--train-data-roots": "tests/assets/common_semantic_segmentation_dataset/train/images", + "--train-type": "Selfsupervised", + "--input": "tests/assets/segmentation/custom/images/training", + "train_params": ["params", "--learning_parameters.num_iters", "5", "--learning_parameters.batch_size", "4"], +} + + +class TestToolsOTXSelfSLSegmentation: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path_1 = tmp_dir_path / "segmentation/test_selfsl" + otx_train_testing(template, tmp_dir_path_1, otx_dir, args_selfsl) + template_work_dir = get_template_dir(template, tmp_dir_path_1) + assert (Path(template_work_dir) / "selfsl").is_dir() + args1 = copy.deepcopy(args) + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + tmp_dir_path_2 = tmp_dir_path / "segmentation/test_selfsl_sl" + otx_train_testing(template, tmp_dir_path_2, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_selfsl_sl" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train_selfsl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_multi_gpu_selfsl" + args_selfsl_multigpu = copy.deepcopy(args_selfsl) + args_selfsl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_selfsl_multigpu) + template_work_dir = get_template_dir(template, tmp_dir_path) + assert (Path(template_work_dir) / "selfsl").is_dir() diff --git a/tests/e2e/cli/test_cli.py b/tests/e2e/cli/test_cli.py new file mode 100644 index 00000000000..ba469a65304 --- /dev/null +++ b/tests/e2e/cli/test_cli.py @@ -0,0 +1,102 @@ +"""Tests for OTX CLI commands""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +import pytest + +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + otx_build_auto_config, + otx_build_backbone_testing, + otx_find_testing, + otx_train_auto_config, +) + +otx_dir = os.getcwd() + + +build_backbone_args = [ + ("CLASSIFICATION", "torchvision.mobilenet_v3_large"), + ("CLASSIFICATION", "mmcls.MMOVBackbone"), + ("DETECTION", "torchvision.mobilenet_v3_large"), + ("INSTANCE_SEGMENTATION", "torchvision.mobilenet_v3_large"), + ("SEGMENTATION", "torchvision.mobilenet_v3_large"), +] +build_backbone_args_ids = [f"{task}_{backbone}" for task, backbone in build_backbone_args] + + +class TestToolsOTXCLI: + @e2e_pytest_component + def test_otx_find(self): + otx_find_testing() + + @e2e_pytest_component + @pytest.mark.parametrize("build_backbone_args", build_backbone_args, ids=build_backbone_args_ids) + def test_otx_backbone_build(self, tmp_dir_path, build_backbone_args): + tmp_dir_path = tmp_dir_path / build_backbone_args[0] / build_backbone_args[1] + otx_build_backbone_testing(tmp_dir_path, build_backbone_args) + + +auto_config_args_with_autosplit = {"--train-data-roots": "tests/assets/classification_dataset"} + +auto_config_args_with_autosplit_task = { + "--task": "classification", + "--train-data-roots": "tests/assets/classification_dataset", +} + +auto_config_args_without_autosplit = { + "--train-data-roots": "tests/assets/classification_dataset", + "--val-data-roots": "tests/assets/classification_dataset_class_incremental", +} + +build_auto_config_args = { + "classification": {"--train-data-roots": "tests/assets/classification_dataset"}, + "classification_with_task": { + "--task": "classification", + "--train-data-roots": "tests/assets/classification_dataset", + }, + "detection": {"--train-data-roots": "tests/assets/car_tree_bug"}, + "detection_with_task": {"--task": "detection", "--train-data-roots": "tests/assets/car_tree_bug"}, +} + + +class TestToolsOTXBuildAutoConfig: + @e2e_pytest_component + @pytest.mark.parametrize("case", build_auto_config_args.keys()) + def test_otx_build_with_autosplit(self, case, tmp_dir_path): + otx_dir = os.getcwd() + tmp_dir_path = tmp_dir_path / "test_build_auto_config" / case + otx_build_auto_config(root=tmp_dir_path, otx_dir=otx_dir, args=build_auto_config_args[case]) + + +train_auto_config_args = { + "classification": {"--train-data-roots": "tests/assets/classification_dataset"}, + "classification_with_template": { + "template": "src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/template.yaml", + "--train-data-roots": "tests/assets/classification_dataset", + }, + "detection": {"--train-data-roots": "tests/assets/car_tree_bug"}, + "detection_with_template": { + "template": "src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml", + "--train-data-roots": "tests/assets/car_tree_bug", + }, +} + +train_params = [ + "params", + "--learning_parameters.num_iters", + "4", +] + + +class TestToolsOTXTrainAutoConfig: + @e2e_pytest_component + @pytest.mark.parametrize("case", train_auto_config_args.keys()) + def test_otx_train(self, case, tmp_dir_path): + otx_dir = os.getcwd() + tmp_dir_path = tmp_dir_path / case + train_auto_config_args[case]["train_params"] = train_params + otx_train_auto_config(root=tmp_dir_path, otx_dir=otx_dir, args=train_auto_config_args[case]) diff --git a/tests/integration/detection/__init__.py b/tests/e2e/cli/visual_prompting/__init__.py similarity index 100% rename from tests/integration/detection/__init__.py rename to tests/e2e/cli/visual_prompting/__init__.py diff --git a/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_Tiny_ViT/compressed_decoder.yml b/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_Tiny_ViT/compressed_decoder.yml new file mode 100644 index 00000000000..8844f4783bb --- /dev/null +++ b/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_Tiny_ViT/compressed_decoder.yml @@ -0,0 +1,3 @@ +TestToolsVisualPrompting: + ptq: + number_of_fakequantizers: 69 diff --git a/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_Tiny_ViT/compressed_image_encoder.yml b/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_Tiny_ViT/compressed_image_encoder.yml new file mode 100644 index 00000000000..c1bc3a82801 --- /dev/null +++ b/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_Tiny_ViT/compressed_image_encoder.yml @@ -0,0 +1,3 @@ +TestToolsVisualPrompting: + ptq: + number_of_fakequantizers: 89 diff --git a/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_decoder.yml b/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_decoder.yml new file mode 100644 index 00000000000..8844f4783bb --- /dev/null +++ b/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_decoder.yml @@ -0,0 +1,3 @@ +TestToolsVisualPrompting: + ptq: + number_of_fakequantizers: 69 diff --git a/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_image_encoder.yml b/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_image_encoder.yml new file mode 100644 index 00000000000..02e25c67b47 --- /dev/null +++ b/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_image_encoder.yml @@ -0,0 +1,3 @@ +TestToolsVisualPrompting: + ptq: + number_of_fakequantizers: 75 diff --git a/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_Tiny_ViT/compressed_decoder.yml b/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_Tiny_ViT/compressed_decoder.yml new file mode 100644 index 00000000000..bbefedd68ef --- /dev/null +++ b/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_Tiny_ViT/compressed_decoder.yml @@ -0,0 +1,3 @@ +TestToolsZeroShotVisualPrompting: + ptq: + number_of_fakequantizers: 71 diff --git a/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_Tiny_ViT/compressed_image_encoder.yml b/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_Tiny_ViT/compressed_image_encoder.yml new file mode 100644 index 00000000000..2f72e18fd85 --- /dev/null +++ b/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_Tiny_ViT/compressed_image_encoder.yml @@ -0,0 +1,3 @@ +TestToolsZeroShotVisualPrompting: + ptq: + number_of_fakequantizers: 89 diff --git a/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_ViT_B/compressed_decoder.yml b/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_ViT_B/compressed_decoder.yml new file mode 100644 index 00000000000..bbefedd68ef --- /dev/null +++ b/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_ViT_B/compressed_decoder.yml @@ -0,0 +1,3 @@ +TestToolsZeroShotVisualPrompting: + ptq: + number_of_fakequantizers: 71 diff --git a/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_ViT_B/compressed_image_encoder.yml b/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_ViT_B/compressed_image_encoder.yml new file mode 100644 index 00000000000..d538d54e103 --- /dev/null +++ b/tests/e2e/cli/visual_prompting/reference/Zero_Shot_SAM_ViT_B/compressed_image_encoder.yml @@ -0,0 +1,3 @@ +TestToolsZeroShotVisualPrompting: + ptq: + number_of_fakequantizers: 75 diff --git a/tests/e2e/cli/visual_prompting/test_visual_prompting.py b/tests/e2e/cli/visual_prompting/test_visual_prompting.py new file mode 100644 index 00000000000..10887756f57 --- /dev/null +++ b/tests/e2e/cli/visual_prompting/test_visual_prompting.py @@ -0,0 +1,156 @@ +"""Tests for Visual Prompting with OTX CLI""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os + +import pytest + +from otx.algorithms.common.utils.utils import is_xpu_available +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_resume_testing, + otx_train_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, + ptq_eval_testing, +) + +args = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": [ + "params", + "--learning_parameters.trainer.max_epochs", + "1", + "--learning_parameters.dataset.train_batch_size", + "2", + "--learning_parameters.dataset.use_mask", + "False", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.trainer.max_epochs", + "2", + "--learning_parameters.dataset.train_batch_size", + "4", +] + +if is_xpu_available(): + pytest.skip("Visual prompting task is not supported on XPU", allow_module_level=True) + +otx_dir = os.getcwd() + +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + os.path.join("src/otx/algorithms/visual_prompting/configs", "sam_vit_b", "template.yaml") + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] + +else: + templates = [ + template + for template in Registry("src/otx/algorithms/visual_prompting").filter(task_type="VISUAL_PROMPTING").templates + if "Zero_Shot" not in template.name + ] + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsVisualPrompting: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + otx_train_testing(template, tmp_dir_path, otx_dir, args1, deterministic=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_export_testing(template, tmp_dir_path, False) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_eval_openvino_testing( + template, + tmp_dir_path, + otx_dir, + args, + threshold=0.2, + half_precision=half_precision, + is_visual_prompting=True, + ) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args, is_visual_prompting=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "visual_prompting", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + ptq_eval_testing(template, tmp_dir_path, otx_dir, args, is_visual_prompting=True) diff --git a/tests/e2e/cli/visual_prompting/test_zero_shot.py b/tests/e2e/cli/visual_prompting/test_zero_shot.py new file mode 100644 index 00000000000..c6349a281da --- /dev/null +++ b/tests/e2e/cli/visual_prompting/test_zero_shot.py @@ -0,0 +1,129 @@ +"""Tests for Visual Prompting with OTX CLI""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os + +import pytest + +from otx.algorithms.common.utils.utils import is_xpu_available +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, + ptq_validate_fq_testing, + ptq_eval_testing, +) + +args = { + "--train-data-roots": "tests/assets/car_tree_bug_zero_shot", + "--val-data-roots": "tests/assets/car_tree_bug_zero_shot", + "--test-data-roots": "tests/assets/car_tree_bug_zero_shot", + "--input": "tests/assets/car_tree_bug_zero_shot/images/train", + "train_params": [ + "params", + "--learning_parameters.trainer.max_epochs", + "1", + "--learning_parameters.dataset.train_batch_size", + "1", + "--learning_parameters.dataset.use_mask", + "False", + ], +} + +if is_xpu_available(): + pytest.skip("Zero shot visual prompting task is not supported on XPU", allow_module_level=True) + +otx_dir = os.getcwd() + +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + os.path.join("src/otx/algorithms/visual_prompting/configs", "zero_shot_sam_tiny_vit", "template.yaml") + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] + +else: + templates = [ + template + for template in Registry("src/otx/algorithms/visual_prompting").filter(task_type="VISUAL_PROMPTING").templates + if "Zero_Shot" in template.name + ] + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsZeroShotVisualPrompting: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_export_testing(template, tmp_dir_path, False) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.xfail(reason="This test is failing due to unexpected performance gap.") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_eval_openvino_testing( + template, + tmp_dir_path, + otx_dir, + args, + threshold=0.3, + half_precision=half_precision, + is_visual_prompting=True, + ) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + ptq_optimize_testing(template, tmp_dir_path, otx_dir, args, is_visual_prompting=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_validate_fq(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + ptq_validate_fq_testing(template, tmp_dir_path, otx_dir, "visual_prompting", type(self).__name__) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.xfail(reason="This test is failing due to unexpected performance gap.") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + ptq_eval_testing(template, tmp_dir_path, otx_dir, args, is_visual_prompting=True) diff --git a/tests/e2e/test_api_xai_sanity.py b/tests/e2e/test_api_xai_sanity.py new file mode 100644 index 00000000000..8534276839e --- /dev/null +++ b/tests/e2e/test_api_xai_sanity.py @@ -0,0 +1,440 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import os.path as osp +import tempfile +from copy import deepcopy + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.task import MMClassificationTask +from otx.algorithms.classification.adapters.openvino.task import ClassificationOpenVINOTask + +from otx.algorithms.detection.adapters.mmdet.task import MMDetectionTask +from otx.algorithms.detection.adapters.openvino.task import OpenVINODetectionTask +from otx.algorithms.detection.configs.base import DetectionConfig +from otx.api.configuration.helper import create +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ( + ModelConfiguration, + ModelEntity, +) +from otx.api.entities.result_media import ResultMediaEntity +from otx.api.entities.subset import Subset +from otx.api.entities.train_parameters import TrainParameters +from otx.api.entities.model_template import parse_model_template, TaskType +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.cli.utils.io import read_model, save_model_data +from tests.integration.api.classification.test_api_classification import ( + DEFAULT_CLS_TEMPLATE_DIR, + ClassificationTaskAPIBase, +) +from tests.integration.api.detection.api_detection import DetectionTaskAPIBase, DEFAULT_DET_TEMPLATE_DIR +from tests.test_suite.e2e_test_system import e2e_pytest_api +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_ISEG_TEMPLATE_DIR, + init_environment, + generate_det_dataset, +) + +torch.manual_seed(0) + +assert_text_explain_all = "The number of saliency maps should be equal to the number of all classes." +assert_text_explain_predicted = "The number of saliency maps should be equal to the number of predicted classes." + + +def saliency_maps_check( + predicted_dataset, task_labels, raw_sal_map_shape, processed_saliency_maps=False, only_predicted=True +): + for data_point in predicted_dataset: + saliency_map_counter = 0 + metadata_list = data_point.get_metadata() + for metadata in metadata_list: + if isinstance(metadata.data, ResultMediaEntity): + if metadata.data.type == "saliency_map": + saliency_map_counter += 1 + if processed_saliency_maps: + assert metadata.data.numpy.ndim == 3 + assert metadata.data.numpy.shape == (data_point.height, data_point.width, 3) + else: + assert metadata.data.numpy.ndim == 2 + assert metadata.data.numpy.shape == raw_sal_map_shape + if only_predicted: + assert saliency_map_counter == len(data_point.annotation_scene.get_labels()), assert_text_explain_predicted + else: + assert saliency_map_counter == len(task_labels), assert_text_explain_all + + +class TestOVClsXAIAPI(ClassificationTaskAPIBase): + ref_raw_saliency_shapes = { + "EfficientNet-B0": (7, 7), + } + + @e2e_pytest_api + @pytest.mark.parametrize( + "multilabel,hierarchical", + [(False, False), (True, False), (False, True)], + ids=["multiclass", "multilabel", "hierarchical"], + ) + def test_inference_xai(self, multilabel, hierarchical): + with tempfile.TemporaryDirectory() as temp_dir: + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_CLS_TEMPLATE_DIR, num_iters=1) + task_environment, dataset = self.init_environment( + hyper_parameters, model_template, multilabel, hierarchical, 20 + ) + + # Train and save a model + task = MMClassificationTask(task_environment=task_environment) + train_parameters = TrainParameters() + output_model = ModelEntity( + dataset, + task_environment.get_model_configuration(), + ) + task.train(dataset, output_model, train_parameters) + save_model_data(output_model, temp_dir) + + for processed_saliency_maps, only_predicted in [[True, False], [False, True]]: + task_environment, dataset = self.init_environment( + hyper_parameters, model_template, multilabel, hierarchical, 20 + ) + + # Infer torch model + task = MMClassificationTask(task_environment=task_environment) + inference_parameters = InferenceParameters( + is_evaluation=False, + process_saliency_maps=processed_saliency_maps, + explain_predicted_classes=only_predicted, + ) + predicted_dataset = task.infer(dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps torch task + task_labels = output_model.configuration.get_label_schema().get_labels(include_empty=False) + saliency_maps_check( + predicted_dataset, + task_labels, + self.ref_raw_saliency_shapes[model_template.name], + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + # Save OV IR model + task._model_ckpt = osp.join(temp_dir, "weights.pth") + exported_model = ModelEntity(None, task_environment.get_model_configuration()) + task.export(ExportType.OPENVINO, exported_model, dump_features=True) + os.makedirs(temp_dir, exist_ok=True) + save_model_data(exported_model, temp_dir) + + # Infer OV IR model + load_weights_ov = osp.join(temp_dir, "openvino.xml") + task_environment.model = read_model(task_environment.get_model_configuration(), load_weights_ov, None) + task = ClassificationOpenVINOTask(task_environment=task_environment) + _, dataset = self.init_environment(hyper_parameters, model_template, multilabel, hierarchical, 20) + predicted_dataset_ov = task.infer(dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps OV task + saliency_maps_check( + predicted_dataset_ov, + task_labels, + self.ref_raw_saliency_shapes[model_template.name], + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + +class TestOVDetXAIAPI(DetectionTaskAPIBase): + ref_raw_saliency_shapes = { + "MobileNetV2-ATSS": (6, 8), + } + + @e2e_pytest_api + def test_inference_xai(self): + with tempfile.TemporaryDirectory() as temp_dir: + hyper_parameters, model_template = self.setup_configurable_parameters( + DEFAULT_DET_TEMPLATE_DIR, num_iters=15 + ) + task_env, dataset = self.init_environment(hyper_parameters, model_template, 10) + + train_task = MMDetectionTask(task_environment=task_env) + trained_model = ModelEntity( + dataset, + task_env.get_model_configuration(), + ) + train_task.train(dataset, trained_model, TrainParameters()) + save_model_data(trained_model, temp_dir) + + for processed_saliency_maps, only_predicted in [[True, False], [False, True]]: + task_env, dataset = self.init_environment(hyper_parameters, model_template, 10) + inference_parameters = InferenceParameters( + is_evaluation=False, + process_saliency_maps=processed_saliency_maps, + explain_predicted_classes=only_predicted, + ) + + # Infer torch model + task_env.model = trained_model + inference_task = MMDetectionTask(task_environment=task_env) + val_dataset = dataset.get_subset(Subset.VALIDATION) + predicted_dataset = inference_task.infer(val_dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps torch task + task_labels = trained_model.configuration.get_label_schema().get_labels(include_empty=False) + saliency_maps_check( + predicted_dataset, + task_labels, + self.ref_raw_saliency_shapes[model_template.name], + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + # Save OV IR model + inference_task._model_ckpt = osp.join(temp_dir, "weights.pth") + exported_model = ModelEntity(None, task_env.get_model_configuration()) + inference_task.export(ExportType.OPENVINO, exported_model, dump_features=True) + os.makedirs(temp_dir, exist_ok=True) + save_model_data(exported_model, temp_dir) + + # Infer OV IR model + load_weights_ov = osp.join(temp_dir, "openvino.xml") + task_env.model = read_model(task_env.get_model_configuration(), load_weights_ov, None) + task = OpenVINODetectionTask(task_environment=task_env) + _, dataset = self.init_environment(hyper_parameters, model_template, 10) + predicted_dataset_ov = task.infer(dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps OV task + saliency_maps_check( + predicted_dataset_ov, + task_labels, + self.ref_raw_saliency_shapes[model_template.name], + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + +class TestOVDetTilXAIAPI(DetectionTaskAPIBase): + ref_raw_saliency_shapes = { + "MobileNetV2-ATSS": (6, 8), + } + + @e2e_pytest_api + def test_inference_xai(self): + with tempfile.TemporaryDirectory() as temp_dir: + hyper_parameters, model_template = self.setup_configurable_parameters( + DEFAULT_DET_TEMPLATE_DIR, num_iters=10, tiling=True + ) + task_env, dataset = self.init_environment(hyper_parameters, model_template, 10) + + train_task = MMDetectionTask(task_environment=task_env) + trained_model = ModelEntity( + dataset, + task_env.get_model_configuration(), + ) + train_task.train(dataset, trained_model, TrainParameters()) + save_model_data(trained_model, temp_dir) + + for processed_saliency_maps, only_predicted in [[True, False], [False, True]]: + task_env, dataset = self.init_environment(hyper_parameters, model_template, 10) + inference_parameters = InferenceParameters( + is_evaluation=False, + process_saliency_maps=processed_saliency_maps, + explain_predicted_classes=only_predicted, + ) + + # Infer torch model + task_env.model = trained_model + inference_task = MMDetectionTask(task_environment=task_env) + val_dataset = dataset.get_subset(Subset.VALIDATION) + predicted_dataset = inference_task.infer(val_dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps torch task + task_labels = trained_model.configuration.get_label_schema().get_labels(include_empty=False) + saliency_maps_check( + predicted_dataset, + task_labels, + self.ref_raw_saliency_shapes[model_template.name], + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + # Save OV IR model + inference_task._model_ckpt = osp.join(temp_dir, "weights.pth") + exported_model = ModelEntity(None, task_env.get_model_configuration()) + inference_task.export(ExportType.OPENVINO, exported_model, dump_features=True) + os.makedirs(temp_dir, exist_ok=True) + save_model_data(exported_model, temp_dir) + + # Infer OV IR model + load_weights_ov = osp.join(temp_dir, "openvino.xml") + task_env.model = read_model(task_env.get_model_configuration(), load_weights_ov, None) + task = OpenVINODetectionTask(task_environment=task_env) + _, dataset = self.init_environment(hyper_parameters, model_template, 10) + predicted_dataset_ov = task.infer(dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps OV task + saliency_maps_check( + predicted_dataset_ov, + task_labels, + self.ref_raw_saliency_shapes[model_template.name], + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + +class TestOVISegmXAIAPI: + @e2e_pytest_api + def test_inference_xai(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.num_iters = 3 + task_env = init_environment(hyper_parameters, model_template, task_type=TaskType.INSTANCE_SEGMENTATION) + + train_task = MMDetectionTask(task_env) + + iseg_dataset, iseg_labels = generate_det_dataset(TaskType.INSTANCE_SEGMENTATION, 100) + iseg_label_schema = LabelSchemaEntity() + iseg_label_group = LabelGroup( + name="labels", + labels=iseg_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + iseg_label_schema.add_group(iseg_label_group) + + _config = ModelConfiguration(DetectionConfig(), iseg_label_schema) + trained_model = ModelEntity( + iseg_dataset, + _config, + ) + + train_task.train(iseg_dataset, trained_model, TrainParameters()) + + save_model_data(trained_model, temp_dir) + + processed_saliency_maps, only_predicted = False, True + task_env = init_environment(hyper_parameters, model_template, task_type=TaskType.INSTANCE_SEGMENTATION) + inference_parameters = InferenceParameters( + is_evaluation=False, + process_saliency_maps=processed_saliency_maps, + explain_predicted_classes=only_predicted, + ) + + # Infer torch model + task_env.model = trained_model + inference_task = MMDetectionTask(task_environment=task_env) + val_dataset = iseg_dataset.get_subset(Subset.VALIDATION) + val_dataset_copy = deepcopy(val_dataset) + predicted_dataset = inference_task.infer(val_dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps torch task + task_labels = trained_model.configuration.get_label_schema().get_labels(include_empty=False) + saliency_maps_check( + predicted_dataset, + task_labels, + (224, 224), + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + # Save OV IR model + inference_task._model_ckpt = osp.join(temp_dir, "weights.pth") + exported_model = ModelEntity(None, task_env.get_model_configuration()) + inference_task.export(ExportType.OPENVINO, exported_model, dump_features=True) + os.makedirs(temp_dir, exist_ok=True) + save_model_data(exported_model, temp_dir) + + # Infer OV IR model + load_weights_ov = osp.join(temp_dir, "openvino.xml") + task_env.model = read_model(task_env.get_model_configuration(), load_weights_ov, None) + task = OpenVINODetectionTask(task_environment=task_env) + predicted_dataset_ov = task.infer(val_dataset_copy.with_empty_annotations(), inference_parameters) + + # Check saliency maps OV task + saliency_maps_check( + predicted_dataset_ov, + task_labels, + (480, 640), + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + +class TestOVISegmTilXAIAPI: + @e2e_pytest_api + def test_inference_xai(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.num_iters = 5 + hyper_parameters.tiling_parameters.enable_tiling = True + task_env = init_environment(hyper_parameters, model_template, task_type=TaskType.INSTANCE_SEGMENTATION) + + train_task = MMDetectionTask(task_env) + + iseg_dataset, iseg_labels = generate_det_dataset(TaskType.INSTANCE_SEGMENTATION, 100) + iseg_label_schema = LabelSchemaEntity() + iseg_label_group = LabelGroup( + name="labels", + labels=iseg_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + iseg_label_schema.add_group(iseg_label_group) + + _config = ModelConfiguration(DetectionConfig(), iseg_label_schema) + trained_model = ModelEntity( + iseg_dataset, + _config, + ) + + train_task.train(iseg_dataset, trained_model, TrainParameters()) + + save_model_data(trained_model, temp_dir) + + processed_saliency_maps, only_predicted = False, True + task_env = init_environment(hyper_parameters, model_template, task_type=TaskType.INSTANCE_SEGMENTATION) + inference_parameters = InferenceParameters( + is_evaluation=False, + process_saliency_maps=processed_saliency_maps, + explain_predicted_classes=only_predicted, + ) + + # Infer torch model + task_env.model = trained_model + inference_task = MMDetectionTask(task_environment=task_env) + val_dataset = iseg_dataset.get_subset(Subset.VALIDATION) + val_dataset_copy = deepcopy(val_dataset) + predicted_dataset = inference_task.infer(val_dataset.with_empty_annotations(), inference_parameters) + + # Check saliency maps torch task + task_labels = trained_model.configuration.get_label_schema().get_labels(include_empty=False) + saliency_maps_check( + predicted_dataset, + task_labels, + (33, 44), + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) + + # Save OV IR model + inference_task._model_ckpt = osp.join(temp_dir, "weights.pth") + exported_model = ModelEntity(None, task_env.get_model_configuration()) + inference_task.export(ExportType.OPENVINO, exported_model, dump_features=True) + os.makedirs(temp_dir, exist_ok=True) + save_model_data(exported_model, temp_dir) + + # Infer OV IR model + load_weights_ov = osp.join(temp_dir, "openvino.xml") + task_env.model = read_model(task_env.get_model_configuration(), load_weights_ov, None) + task = OpenVINODetectionTask(task_environment=task_env) + predicted_dataset_ov = task.infer(val_dataset_copy.with_empty_annotations(), inference_parameters) + + # Check saliency maps OV task + saliency_maps_check( + predicted_dataset_ov, + task_labels, + (480, 640), + processed_saliency_maps=processed_saliency_maps, + only_predicted=only_predicted, + ) diff --git a/tests/fuzzing/cli_fuzzing.py b/tests/fuzzing/cli_fuzzing.py index 11664e3449e..ed6d17f89ad 100644 --- a/tests/fuzzing/cli_fuzzing.py +++ b/tests/fuzzing/cli_fuzzing.py @@ -2,6 +2,7 @@ import atheris from helper import FuzzingHelper + from otx.cli.tools.cli import main as cli_main from otx.cli.utils.errors import CliException diff --git a/tests/fuzzing/helper.py b/tests/fuzzing/helper.py index 1cd36199922..90cf8daade4 100644 --- a/tests/fuzzing/helper.py +++ b/tests/fuzzing/helper.py @@ -1,7 +1,7 @@ import atheris -class FuzzingHelper: +class FuzzingHelper(object): """Helper to make required data from input_bytes for the fuzzing tests""" def __init__(self, input_bytes): diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 6a16273c024..79931efa777 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1,2 +1,3 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2021-2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/api/__init__.py b/tests/integration/api/__init__.py index 916f3a44b27..79931efa777 100644 --- a/tests/integration/api/__init__.py +++ b/tests/integration/api/__init__.py @@ -1,2 +1,3 @@ -# Copyright (C) 2024 Intel Corporation +# Copyright (C) 2021-2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/api/action/__init__.py b/tests/integration/api/action/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/api/action/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/api/action/test_api_action_classification.py b/tests/integration/api/action/test_api_action_classification.py new file mode 100644 index 00000000000..2764de21620 --- /dev/null +++ b/tests/integration/api/action/test_api_action_classification.py @@ -0,0 +1,228 @@ +"""API Tests for Action Classification training""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import glob +import os.path as osp +import time +from concurrent.futures import ThreadPoolExecutor +from typing import Optional + +import numpy as np +import pytest + +from otx.algorithms.action.tasks import ActionInferenceTask, ActionTrainTask +from otx.algorithms.common.tasks.training_base import BaseTask +from otx.api.configuration.helper import create +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.metrics import Performance +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.core.data.adapter import get_dataset_adapter +from tests.test_suite.e2e_test_system import e2e_pytest_api + +DEFAULT_ACTION_TEMPLATE_DIR = osp.join("src/otx/algorithms/action/configs", "classification", "x3d") + + +def task_eval(task: BaseTask, model: ModelEntity, dataset: DatasetEntity) -> Performance: + start_time = time.time() + result_dataset = task.infer(dataset.with_empty_annotations()) + end_time = time.time() + print(f"{len(dataset)} analysed in {end_time - start_time} seconds") + result_set = ResultSetEntity(model=model, ground_truth_dataset=dataset, prediction_dataset=result_dataset) + task.evaluate(result_set) + assert result_set.performance is not None + return result_set.performance + + +class TestActionTaskAPI: + """ + Collection of tests for OTX API and OTX Model Templates + """ + + train_data_roots = "tests/assets/cvat_dataset/action_classification/train" + val_data_roots = "tests/assets/cvat_dataset/action_classification/train" + + @e2e_pytest_api + def test_reading_action_model_template(self): + model_templates = ["x3d"] + for model_template in model_templates: + parse_model_template( + osp.join("src/otx/algorithms/action/configs", "classification", model_template, "template.yaml") + ) + + def init_environment(self, params, model_template): + algo_backend = model_template.hyper_parameters.parameter_overrides["algo_backend"] + train_type = algo_backend["train_type"]["default_value"] + dataset_adapter = get_dataset_adapter( + model_template.task_type, + train_type, + train_data_roots=self.train_data_roots, + val_data_roots=self.val_data_roots, + ) + dataset = dataset_adapter.get_otx_dataset() + label_schema = dataset_adapter.get_label_schema() + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=label_schema, + model_template=model_template, + ) + return environment, dataset + + @staticmethod + def setup_configurable_parameters(template_dir, num_iters=10): + glb = glob.glob(f"{template_dir}/template*.yaml") + template_path = glb[0] if glb else None + if not template_path: + raise RuntimeError(f"Template YAML not found: {template_dir}") + + model_template = parse_model_template(template_path) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.num_iters = num_iters + hyper_parameters.postprocessing.result_based_confidence_threshold = False + hyper_parameters.postprocessing.confidence_threshold = 0.1 + return hyper_parameters, model_template + + @e2e_pytest_api + @pytest.mark.skip(reason="mmaction does not support EpochRunnerWithCancel") + def test_cancel_training_action(self): + """ + Tests starting and cancelling training. + + Flow of the test: + - Creates a randomly annotated project with a small dataset containing 3 classes: + ['rectangle', 'triangle', 'circle']. + - Start training and give cancel training signal after 10 seconds. Assert that training + stops within 35 seconds after that + - Start training and give cancel signal immediately. Assert that training stops within 25 seconds. + + This test should be finished in under one minute on a workstation. + """ + hyper_parameters, model_template = self.setup_configurable_parameters( + DEFAULT_ACTION_TEMPLATE_DIR, num_iters=500 + ) + action_environment, dataset = self.init_environment(hyper_parameters, model_template) + + action_task = ActionTrainTask(task_environment=action_environment) + + executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="train_thread") + + output_model = ModelEntity( + dataset, + action_environment.get_model_configuration(), + ) + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + + # Test stopping after some time + start_time = time.time() + train_future = executor.submit(action_task.train, dataset, output_model, train_parameters) + # give train_thread some time to initialize the model + while not action_task._is_training: + time.sleep(10) + action_task.cancel_training() + + # stopping process has to happen in less than 35 seconds + train_future.result() + assert training_progress_curve[-1] == 100 + assert time.time() - start_time < 100, "Expected to stop within 100 seconds." + + # Test stopping immediately + start_time = time.time() + train_future = executor.submit(action_task.train, dataset, output_model) + action_task.cancel_training() + + train_future.result() + assert time.time() - start_time < 25 # stopping process has to happen in less than 25 seconds + + @e2e_pytest_api + def test_training_progress_tracking(self): + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_ACTION_TEMPLATE_DIR, num_iters=5) + action_environment, dataset = self.init_environment(hyper_parameters, model_template) + task = ActionTrainTask(task_environment=action_environment) + print("Task initialized, model training starts.") + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + output_model = ModelEntity( + dataset, + action_environment.get_model_configuration(), + ) + task.train(dataset, output_model, train_parameters) + + assert len(training_progress_curve) > 0 + assert np.all(training_progress_curve[1:] >= training_progress_curve[:-1]) + + @e2e_pytest_api + def test_inference_progress_tracking(self): + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_ACTION_TEMPLATE_DIR, num_iters=10) + action_environment, dataset = self.init_environment(hyper_parameters, model_template) + + task = ActionInferenceTask(task_environment=action_environment) + print("Task initialized, model inference starts.") + inference_progress_curve = [] + + def progress_callback(progress: int): + assert isinstance(progress, int) + inference_progress_curve.append(progress) + + inference_parameters = InferenceParameters() + inference_parameters.update_progress = progress_callback + task.infer(dataset, inference_parameters) + + assert len(inference_progress_curve) > 0 + assert np.all(inference_progress_curve[1:] >= inference_progress_curve[:-1]) + + @e2e_pytest_api + def test_inference_task(self): + # FIXME CVS-103071 will handle this + # Prepare pretrained weights + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_ACTION_TEMPLATE_DIR, num_iters=2) + action_environment, dataset = self.init_environment(hyper_parameters, model_template) + # val_dataset = dataset.get_subset(Subset.VALIDATION) + + train_task = ActionTrainTask(task_environment=action_environment) + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + trained_model = ModelEntity( + dataset, + action_environment.get_model_configuration(), + ) + train_task.train(dataset, trained_model, train_parameters) + # performance_after_train = task_eval(train_task, trained_model, val_dataset) + + # Create InferenceTask + action_environment.model = trained_model + inference_task = ActionInferenceTask(task_environment=action_environment) + + # performance_after_load = task_eval(inference_task, trained_model, val_dataset) + + # assert performance_after_train == performance_after_load + + # Export + exported_model = ModelEntity(dataset, action_environment.get_model_configuration()) + inference_task.export(ExportType.OPENVINO, exported_model) diff --git a/tests/integration/api/action/test_api_action_detection.py b/tests/integration/api/action/test_api_action_detection.py new file mode 100644 index 00000000000..78c6cbc2b60 --- /dev/null +++ b/tests/integration/api/action/test_api_action_detection.py @@ -0,0 +1,229 @@ +"""API Tests for Action Detection training""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import glob +import os.path as osp +import time +from concurrent.futures import ThreadPoolExecutor +from typing import Optional + +import numpy as np +import pytest + +from otx.algorithms.action.tasks import ActionInferenceTask, ActionTrainTask +from otx.algorithms.common.tasks.training_base import BaseTask +from otx.api.configuration.helper import create +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.metrics import Performance +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.train_parameters import TrainParameters +from otx.core.data.adapter import get_dataset_adapter +from tests.test_suite.e2e_test_system import e2e_pytest_api + +DEFAULT_ACTION_TEMPLATE_DIR = osp.join("src/otx/algorithms/action/configs", "detection", "x3d_fast_rcnn") + + +def task_eval(task: BaseTask, model: ModelEntity, dataset: DatasetEntity) -> Performance: + start_time = time.time() + result_dataset = task.infer(dataset.with_empty_annotations()) + end_time = time.time() + print(f"{len(dataset)} analysed in {end_time - start_time} seconds") + result_set = ResultSetEntity(model=model, ground_truth_dataset=dataset, prediction_dataset=result_dataset) + task.evaluate(result_set) + assert result_set.performance is not None + return result_set.performance + + +class TestActionTaskAPI: + """ + Collection of tests for OTX API and OTX Model Templates + """ + + train_data_roots = "tests/assets/cvat_dataset/action_detection/train" + val_data_roots = "tests/assets/cvat_dataset/action_detection/train" + + @e2e_pytest_api + def test_reading_action_model_template(self): + model_templates = ["x3d_fast_rcnn"] + for model_template in model_templates: + parse_model_template( + osp.join("src/otx/algorithms/action/configs", "detection", model_template, "template.yaml") + ) + + def init_environment(self, params, model_template): + algo_backend = model_template.hyper_parameters.parameter_overrides["algo_backend"] + train_type = algo_backend["train_type"]["default_value"] + dataset_adapter = get_dataset_adapter( + model_template.task_type, + train_type, + train_data_roots=self.train_data_roots, + val_data_roots=self.val_data_roots, + ) + dataset = dataset_adapter.get_otx_dataset() + label_schema = dataset_adapter.get_label_schema() + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=label_schema, + model_template=model_template, + ) + return environment, dataset + + @staticmethod + def setup_configurable_parameters(template_dir, num_iters=10): + glb = glob.glob(f"{template_dir}/template*.yaml") + template_path = glb[0] if glb else None + if not template_path: + raise RuntimeError(f"Template YAML not found: {template_dir}") + + model_template = parse_model_template(template_path) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.num_iters = num_iters + hyper_parameters.postprocessing.result_based_confidence_threshold = False + hyper_parameters.postprocessing.confidence_threshold = 0.1 + return hyper_parameters, model_template + + @e2e_pytest_api + @pytest.mark.skip(reason="mmaction does not support EpochRunnerWithCancel") + def test_cancel_training_action(self): + """ + Tests starting and cancelling training. + + Flow of the test: + - Creates a randomly annotated project with a small dataset containing 3 classes: + ['rectangle', 'triangle', 'circle']. + - Start training and give cancel training signal after 10 seconds. Assert that training + stops within 35 seconds after that + - Start training and give cancel signal immediately. Assert that training stops within 25 seconds. + + This test should be finished in under one minute on a workstation. + """ + hyper_parameters, model_template = self.setup_configurable_parameters( + DEFAULT_ACTION_TEMPLATE_DIR, num_iters=500 + ) + action_environment, dataset = self.init_environment(hyper_parameters, model_template) + + action_task = ActionTrainTask(task_environment=action_environment) + + executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="train_thread") + + output_model = ModelEntity( + dataset, + action_environment.get_model_configuration(), + ) + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + + # Test stopping after some time + start_time = time.time() + train_future = executor.submit(action_task.train, dataset, output_model, train_parameters) + # give train_thread some time to initialize the model + while not action_task._is_training: + time.sleep(10) + action_task.cancel_training() + + # stopping process has to happen in less than 35 seconds + train_future.result() + assert training_progress_curve[-1] == 100 + assert time.time() - start_time < 100, "Expected to stop within 100 seconds." + + # Test stopping immediately + start_time = time.time() + train_future = executor.submit(action_task.train, dataset, output_model) + action_task.cancel_training() + + train_future.result() + assert time.time() - start_time < 25 # stopping process has to happen in less than 25 seconds + + @e2e_pytest_api + def test_training_progress_tracking(self): + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_ACTION_TEMPLATE_DIR, num_iters=5) + action_environment, dataset = self.init_environment(hyper_parameters, model_template) + task = ActionTrainTask(task_environment=action_environment) + print("Task initialized, model training starts.") + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + output_model = ModelEntity( + dataset, + action_environment.get_model_configuration(), + ) + task.train(dataset, output_model, train_parameters) + + assert len(training_progress_curve) > 0 + assert np.all(training_progress_curve[1:] >= training_progress_curve[:-1]) + + @e2e_pytest_api + def test_inference_progress_tracking(self): + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_ACTION_TEMPLATE_DIR, num_iters=10) + action_environment, dataset = self.init_environment(hyper_parameters, model_template) + + task = ActionInferenceTask(task_environment=action_environment) + print("Task initialized, model inference starts.") + inference_progress_curve = [] + + def progress_callback(progress: int): + assert isinstance(progress, int) + inference_progress_curve.append(progress) + + inference_parameters = InferenceParameters() + inference_parameters.update_progress = progress_callback + task.infer(dataset, inference_parameters) + + assert len(inference_progress_curve) > 0 + assert np.all(inference_progress_curve[1:] >= inference_progress_curve[:-1]) + + @e2e_pytest_api + def test_inference_task(self): + # FIXME CVS-103071 will handle this + # Prepare pretrained weights + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_ACTION_TEMPLATE_DIR, num_iters=2) + action_environment, dataset = self.init_environment(hyper_parameters, model_template) + # val_dataset = dataset.get_subset(Subset.VALIDATION) + + train_task = ActionTrainTask(task_environment=action_environment) + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + trained_model = ModelEntity( + dataset, + action_environment.get_model_configuration(), + ) + train_task.train(dataset, trained_model, train_parameters) + # performance_after_train = task_eval(train_task, trained_model, val_dataset) + + # Create InferenceTask + # action_environment.model = trained_model + # inference_task = ActionInferenceTask(task_environment=action_environment) + + # performance_after_load = task_eval(inference_task, trained_model, val_dataset) + + # FIXME CVS-103071 will handle this + # assert performance_after_train == performance_after_load + + # Export + # CVS-102941 ONNX export of action detection model keeps failed + # exported_model = ModelEntity(dataset, action_environment.get_model_configuration()) + # inference_task.export(ExportType.OPENVINO, exported_model) diff --git a/tests/integration/api/classification/__init__.py b/tests/integration/api/classification/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/api/classification/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/api/classification/test_api_classification.py b/tests/integration/api/classification/test_api_classification.py new file mode 100644 index 00000000000..5983d520a5e --- /dev/null +++ b/tests/integration/api/classification/test_api_classification.py @@ -0,0 +1,294 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os.path as osp +import random +import time +from typing import Optional + +import cv2 as cv +import numpy as np +import pytest +from bson import ObjectId + +from otx.algorithms.classification.adapters.mmcls.task import MMClassificationTask +from otx.algorithms.common.tasks.base_task import OTXTask +from otx.api.configuration.helper import create +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.metrics import Performance +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from tests.test_suite.e2e_test_system import e2e_pytest_api + +DEFAULT_CLS_TEMPLATE_DIR = osp.join("src/otx/algorithms/classification", "configs", "efficientnet_b0_cls_incr") + + +def task_eval(task: OTXTask, model: ModelEntity, dataset: DatasetEntity) -> Performance: + start_time = time.time() + result_dataset = task.infer(dataset.with_empty_annotations()) + end_time = time.time() + print(f"{len(dataset)} analysed in {end_time - start_time} seconds") + result_set = ResultSetEntity(model=model, ground_truth_dataset=dataset, prediction_dataset=result_dataset) + task.evaluate(result_set) + assert result_set.performance is not None + return result_set.performance + + +class ClassificationTaskAPIBase: + @staticmethod + def generate_label_schema(not_empty_labels, multilabel=False, hierarchical=False): + assert len(not_empty_labels) > 1 + + label_schema = LabelSchemaEntity() + if multilabel: + emptylabel = LabelEntity(name="Empty label", is_empty=True, domain=Domain.CLASSIFICATION) + empty_group = LabelGroup(name="empty", labels=[emptylabel], group_type=LabelGroupType.EMPTY_LABEL) + for label in not_empty_labels: + label_schema.add_group( + LabelGroup( + name=label.name, + labels=[label], + group_type=LabelGroupType.EXCLUSIVE, + ) + ) + label_schema.add_group(empty_group) + elif hierarchical: + single_label_classes = ["b", "g", "r"] + multi_label_classes = ["w", "p"] + emptylabel = LabelEntity(name="Empty label", is_empty=True, domain=Domain.CLASSIFICATION) + empty_group = LabelGroup(name="empty", labels=[emptylabel], group_type=LabelGroupType.EMPTY_LABEL) + single_labels = [] + for label in not_empty_labels: + if label.name in multi_label_classes: + label_schema.add_group( + LabelGroup( + name=label.name, + labels=[label], + group_type=LabelGroupType.EXCLUSIVE, + ) + ) + if empty_group not in label_schema.get_groups(include_empty=True): + label_schema.add_group(empty_group) + elif label.name in single_label_classes: + single_labels.append(label) + if single_labels: + single_label_group = LabelGroup( + name="labels", + labels=single_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + label_schema.add_group(single_label_group) + else: + main_group = LabelGroup( + name="labels", + labels=not_empty_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + label_schema.add_group(main_group) + return label_schema + + @staticmethod + def setup_configurable_parameters(template_dir, num_iters=10): + model_template = parse_model_template(osp.join(template_dir, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.num_iters = num_iters + return hyper_parameters, model_template + + @staticmethod + def init_environment(params, model_template, multilabel, hierarchical, number_of_images=10): + resolution = (224, 224) + if hierarchical: + colors = [(0, 255, 0), (0, 0, 255), (255, 0, 0), (0, 0, 0), (230, 230, 250)] + cls_names = ["b", "g", "r", "w", "p"] + texts = ["Blue", "Green", "Red", "White", "Purple"] + else: + colors = [(0, 255, 0), (0, 0, 255)] + cls_names = ["b", "g"] + texts = ["Blue", "Green"] + env_labels = [ + LabelEntity(name=name, domain=Domain.CLASSIFICATION, is_empty=False, id=ID(i)) + for i, name in enumerate(cls_names) + ] + + items = [] + + for _ in range(0, number_of_images): + for j, lbl in enumerate(env_labels): + class_img = np.zeros((*resolution, 3), dtype=np.uint8) + class_img[:] = colors[j] + class_img = cv.putText( + class_img, + texts[j], + (50, 50), + cv.FONT_HERSHEY_SIMPLEX, + 0.8 + j * 0.2, + colors[j - 1], + 2, + cv.LINE_AA, + ) + + image = Image(data=class_img) + labels = [ScoredLabel(label=lbl, probability=1.0)] + shapes = [Annotation(Rectangle.generate_full_box(), labels)] + annotation_scene = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=shapes) + items.append(DatasetItemEntity(media=image, annotation_scene=annotation_scene)) + + rng = random.Random() + rng.seed(100) + rng.shuffle(items) + for i, _ in enumerate(items): + subset_region = i / number_of_images + if subset_region >= 0.9: + subset = Subset.TESTING + elif subset_region >= 0.6: + subset = Subset.VALIDATION + else: + subset = Subset.TRAINING + items[i].subset = subset + + dataset = DatasetEntity(items) + labels_schema = ClassificationTaskAPIBase.generate_label_schema( + dataset.get_labels(), multilabel=multilabel, hierarchical=hierarchical + ) + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + return environment, dataset + + +class TestClassificationTaskAPI(ClassificationTaskAPIBase): + @e2e_pytest_api + def test_reading_classification_cls_incr_model_template(self): + classification_template = [ + "efficientnet_b0_cls_incr", + "efficientnet_v2_s_cls_incr", + "mobilenet_v3_large_1_cls_incr", + ] + for model_template in classification_template: + parse_model_template( + osp.join("src/otx/algorithms/classification", "configs", model_template, "template.yaml") + ) + + @e2e_pytest_api + @pytest.mark.parametrize( + "multilabel,hierarchical", + [(False, False), (True, False), (False, True)], + ids=["multiclass", "multilabel", "hierarchical"], + ) + def test_training_progress_tracking(self, multilabel, hierarchical): + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_CLS_TEMPLATE_DIR, num_iters=10) + task_environment, dataset = self.init_environment( + hyper_parameters, model_template, multilabel, hierarchical, 20 + ) + task = MMClassificationTask(task_environment=task_environment) + print("Task initialized, model training starts.") + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + output_model = ModelEntity( + dataset, + task_environment.get_model_configuration(), + ) + task.train(dataset, output_model, train_parameters) + + assert len(training_progress_curve) > 0 + assert np.all(training_progress_curve[1:] >= training_progress_curve[:-1]) + + @e2e_pytest_api + @pytest.mark.parametrize( + "multilabel,hierarchical", + [(False, False), (True, False), (False, True)], + ids=["multiclass", "multilabel", "hierarchical"], + ) + def test_inference_progress_tracking(self, multilabel, hierarchical): + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_CLS_TEMPLATE_DIR, num_iters=5) + task_environment, dataset = self.init_environment( + hyper_parameters, model_template, multilabel, hierarchical, 20 + ) + task = MMClassificationTask(task_environment=task_environment) + print("Task initialized, model inference starts.") + + inference_progress_curve = [] + + def progress_callback(progress: int): + inference_progress_curve.append(progress) + + inference_parameters = InferenceParameters() + inference_parameters.update_progress = progress_callback + task.infer(dataset.with_empty_annotations(), inference_parameters) + + assert len(inference_progress_curve) > 0 + assert np.all(inference_progress_curve[1:] >= inference_progress_curve[:-1]) + + @e2e_pytest_api + @pytest.mark.parametrize( + "multilabel,hierarchical", + [(False, False), (True, False), (False, True)], + ids=["multiclass", "multilabel", "hierarchical"], + ) + def test_inference_task(self, multilabel, hierarchical): + # Prepare pretrained weights + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_CLS_TEMPLATE_DIR, num_iters=2) + classification_environment, dataset = self.init_environment( + hyper_parameters, model_template, multilabel, hierarchical, 50 + ) + val_dataset = dataset.get_subset(Subset.VALIDATION) + + train_task = MMClassificationTask(task_environment=classification_environment) + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + trained_model = ModelEntity( + dataset, + classification_environment.get_model_configuration(), + ) + train_task.train(dataset, trained_model, train_parameters) + performance_after_train = task_eval(train_task, trained_model, val_dataset) + + # Create InferenceTask + classification_environment.model = trained_model + inference_task = MMClassificationTask(task_environment=classification_environment) + + performance_after_load = task_eval(inference_task, trained_model, val_dataset) + + assert performance_after_train == performance_after_load + + # Export + exported_model = ModelEntity( + dataset, + classification_environment.get_model_configuration(), + _id=ObjectId(), + ) + inference_task.export(ExportType.OPENVINO, exported_model) diff --git a/tests/integration/api/detection/__init__.py b/tests/integration/api/detection/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/api/detection/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/api/detection/api_detection.py b/tests/integration/api/detection/api_detection.py new file mode 100644 index 00000000000..cf911b5cfd0 --- /dev/null +++ b/tests/integration/api/detection/api_detection.py @@ -0,0 +1,100 @@ +"""API Tests for detection training""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import glob +import warnings +import random +import os.path as osp + +from otx.algorithms.detection.utils import generate_label_schema +from otx.api.configuration.helper import create +from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.model_template import ( + TaskType, + parse_model_template, + task_type_to_label_domain, +) +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.utils.shape_factory import ShapeFactory +from tests.test_helpers import generate_random_annotated_image + +DEFAULT_DET_TEMPLATE_DIR = osp.join("src/otx/algorithms/detection/configs", "detection", "mobilenetv2_atss") + + +class DetectionTaskAPIBase: + """ + Collection of tests for OTX API and OTX Model Templates + """ + + def init_environment(self, params, model_template, number_of_images=500, task_type=TaskType.DETECTION): + + labels_names = ("rectangle", "ellipse", "triangle") + labels_schema = generate_label_schema(labels_names, task_type_to_label_domain(task_type)) + labels_list = labels_schema.get_labels(False) + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + warnings.filterwarnings("ignore", message=".* coordinates .* are out of bounds.*") + items = [] + for i in range(0, number_of_images): + image_numpy, annos = generate_random_annotated_image( + image_width=640, + image_height=480, + labels=labels_list, + max_shapes=20, + min_size=50, + max_size=100, + random_seed=None, + ) + # Convert shapes according to task + for anno in annos: + if task_type == TaskType.INSTANCE_SEGMENTATION: + anno.shape = ShapeFactory.shape_as_polygon(anno.shape) + else: + anno.shape = ShapeFactory.shape_as_rectangle(anno.shape) + + image = Image(data=image_numpy) + annotation_scene = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=annos) + items.append(DatasetItemEntity(media=image, annotation_scene=annotation_scene)) + warnings.resetwarnings() + + rng = random.Random() + rng.shuffle(items) + for i, _ in enumerate(items): + subset_region = i / number_of_images + if subset_region >= 0.8: + subset = Subset.TESTING + elif subset_region >= 0.6: + subset = Subset.VALIDATION + else: + subset = Subset.TRAINING + items[i].subset = subset + + dataset = DatasetEntity(items) + return environment, dataset + + @staticmethod + def setup_configurable_parameters(template_dir, num_iters=10, tiling=False): + glb = glob.glob(f"{template_dir}/template*.yaml") + template_path = glb[0] if glb else None + if not template_path: + raise RuntimeError(f"Template YAML not found: {template_dir}") + + model_template = parse_model_template(template_path) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.num_iters = num_iters + hyper_parameters.postprocessing.result_based_confidence_threshold = False + hyper_parameters.postprocessing.confidence_threshold = 0.1 + if tiling: + hyper_parameters.tiling_parameters.enable_tiling = True + return hyper_parameters, model_template diff --git a/tests/integration/api/detection/test_api_detection.py b/tests/integration/api/detection/test_api_detection.py new file mode 100644 index 00000000000..070dc26972f --- /dev/null +++ b/tests/integration/api/detection/test_api_detection.py @@ -0,0 +1,189 @@ +"""API Tests for detection training""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os.path as osp +import time +from concurrent.futures import ThreadPoolExecutor +from typing import Optional + +import numpy as np + +from otx.algorithms.common.tasks.training_base import BaseTask +from otx.algorithms.detection.tasks import DetectionInferenceTask, DetectionTrainTask +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.metrics import Performance +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import ( + TaskType, + parse_model_template, +) +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from tests.integration.api.detection.api_detection import DetectionTaskAPIBase +from tests.test_suite.e2e_test_system import e2e_pytest_api + +DEFAULT_DET_TEMPLATE_DIR = osp.join("src/otx/algorithms/detection/configs", "detection", "mobilenetv2_atss") + + +def task_eval(task: BaseTask, model: ModelEntity, dataset: DatasetEntity) -> Performance: + start_time = time.time() + result_dataset = task.infer(dataset.with_empty_annotations()) + end_time = time.time() + print(f"{len(dataset)} analysed in {end_time - start_time} seconds") + result_set = ResultSetEntity(model=model, ground_truth_dataset=dataset, prediction_dataset=result_dataset) + task.evaluate(result_set) + assert result_set.performance is not None + return result_set.performance + + +class TestDetectionTaskAPI(DetectionTaskAPIBase): + """ + Collection of tests for OTX API and OTX Model Templates + """ + + @e2e_pytest_api + def test_reading_detection_model_template(self): + detection_template = ["mobilenetv2_atss"] + for model_template in detection_template: + parse_model_template( + osp.join("src/otx/algorithms/detection/configs", "detection", model_template, "template.yaml") + ) + + @e2e_pytest_api + def test_cancel_training_detection(self): + """ + Tests starting and cancelling training. + + Flow of the test: + - Creates a randomly annotated project with a small dataset containing 3 classes: + ['rectangle', 'triangle', 'circle']. + - Start training and give cancel training signal after 10 seconds. Assert that training + stops within 35 seconds after that + - Start training and give cancel signal immediately. Assert that training stops within 25 seconds. + + This test should be finished in under one minute on a workstation. + """ + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_DET_TEMPLATE_DIR, num_iters=500) + detection_environment, dataset = self.init_environment(hyper_parameters, model_template, 64) + + detection_task = DetectionTrainTask(task_environment=detection_environment) + + executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="train_thread") + + output_model = ModelEntity( + dataset, + detection_environment.get_model_configuration(), + ) + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + + # Test stopping after some time + start_time = time.time() + train_future = executor.submit(detection_task.train, dataset, output_model, train_parameters) + # give train_thread some time to initialize the model + while not detection_task._is_training: + time.sleep(10) + detection_task.cancel_training() + + # stopping process has to happen in less than 35 seconds + train_future.result() + assert training_progress_curve[-1] == 100 + assert time.time() - start_time < 100, "Expected to stop within 100 seconds." + + # Test stopping immediately + start_time = time.time() + train_future = executor.submit(detection_task.train, dataset, output_model) + detection_task.cancel_training() + + train_future.result() + assert time.time() - start_time < 25 # stopping process has to happen in less than 25 seconds + + @e2e_pytest_api + def test_training_progress_tracking(self): + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_DET_TEMPLATE_DIR, num_iters=5) + detection_environment, dataset = self.init_environment(hyper_parameters, model_template, 50) + + task = DetectionTrainTask(task_environment=detection_environment) + print("Task initialized, model training starts.") + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + output_model = ModelEntity( + dataset, + detection_environment.get_model_configuration(), + ) + task.train(dataset, output_model, train_parameters) + + assert len(training_progress_curve) > 0 + assert np.all(training_progress_curve[1:] >= training_progress_curve[:-1]) + + @e2e_pytest_api + def test_inference_progress_tracking(self): + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_DET_TEMPLATE_DIR, num_iters=10) + detection_environment, dataset = self.init_environment(hyper_parameters, model_template, 50) + + task = DetectionInferenceTask(task_environment=detection_environment) + print("Task initialized, model inference starts.") + inference_progress_curve = [] + + def progress_callback(progress: int): + assert isinstance(progress, int) + inference_progress_curve.append(progress) + + inference_parameters = InferenceParameters() + inference_parameters.update_progress = progress_callback + task.infer(dataset, inference_parameters) + + assert len(inference_progress_curve) > 0 + assert np.all(inference_progress_curve[1:] >= inference_progress_curve[:-1]) + + @e2e_pytest_api + def test_inference_task(self): + # Prepare pretrained weights + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_DET_TEMPLATE_DIR, num_iters=2) + detection_environment, dataset = self.init_environment(hyper_parameters, model_template, 50) + val_dataset = dataset.get_subset(Subset.VALIDATION) + + train_task = DetectionTrainTask(task_environment=detection_environment) + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + trained_model = ModelEntity( + dataset, + detection_environment.get_model_configuration(), + ) + train_task.train(dataset, trained_model, train_parameters) + performance_after_train = task_eval(train_task, trained_model, val_dataset) + + # Create InferenceTask + detection_environment.model = trained_model + inference_task = DetectionInferenceTask(task_environment=detection_environment) + + performance_after_load = task_eval(inference_task, trained_model, val_dataset) + + assert performance_after_train == performance_after_load + + # Export + exported_model = ModelEntity(dataset, detection_environment.get_model_configuration()) + inference_task.export(ExportType.OPENVINO, exported_model) diff --git a/tests/integration/api/segmentation/__init__.py b/tests/integration/api/segmentation/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/api/segmentation/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/api/segmentation/test_api_segmentation.py b/tests/integration/api/segmentation/test_api_segmentation.py new file mode 100644 index 00000000000..0860b4b0457 --- /dev/null +++ b/tests/integration/api/segmentation/test_api_segmentation.py @@ -0,0 +1,308 @@ +"""API Tests for segmentation training""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os.path as osp +import random +import time +import warnings +from concurrent.futures import ThreadPoolExecutor +from typing import Optional + +import numpy as np +from bson import ObjectId + +from otx.algorithms.common.tasks.training_base import BaseTask +from otx.algorithms.segmentation.tasks import ( + SegmentationInferenceTask, + SegmentationTrainTask, +) +from otx.api.configuration.helper import create +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.color import Color +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.metrics import Performance +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from tests.test_helpers import generate_random_annotated_image +from tests.test_suite.e2e_test_system import e2e_pytest_api + +DEFAULT_SEG_TEMPLATE_DIR = osp.join("src/otx/algorithms/segmentation/configs", "ocr_lite_hrnet_18_mod2") + + +def task_eval(task: BaseTask, model: ModelEntity, dataset: DatasetEntity) -> Performance: + start_time = time.time() + result_dataset = task.infer(dataset.with_empty_annotations()) + end_time = time.time() + print(f"{len(dataset)} analysed in {end_time - start_time} seconds") + result_set = ResultSetEntity(model=model, ground_truth_dataset=dataset, prediction_dataset=result_dataset) + task.evaluate(result_set) + assert result_set.performance is not None + return result_set.performance + + +class TestOTXSegAPI: + """ + Collection of tests for OTX API and OTX Model Templates + """ + + @e2e_pytest_api + def test_reading_segmentation_cls_incr_model_template(self): + segmentation_template = [ + "ocr_lite_hrnet_18_mod2", + "ocr_lite_hrnet_s_mod2", + "ocr_lite_hrnet_x_mod3", + ] + for model_template in segmentation_template: + parse_model_template(osp.join("src/otx/algorithms/segmentation/configs", model_template, "template.yaml")) + + @staticmethod + def generate_label_schema(label_names): + label_domain = Domain.SEGMENTATION + rgb = [int(i) for i in np.random.randint(0, 256, 3)] + colors = [Color(*rgb) for _ in range(len(label_names))] + not_empty_labels = [ + LabelEntity(name=name, color=colors[i], domain=label_domain, id=i) for i, name in enumerate(label_names) + ] + empty_label = LabelEntity( + name="Empty label", + color=Color(42, 43, 46), + is_empty=True, + domain=label_domain, + id=len(not_empty_labels), + ) + + label_schema = LabelSchemaEntity() + exclusive_group = LabelGroup(name="labels", labels=not_empty_labels, group_type=LabelGroupType.EXCLUSIVE) + empty_group = LabelGroup(name="empty", labels=[empty_label], group_type=LabelGroupType.EMPTY_LABEL) + label_schema.add_group(exclusive_group) + label_schema.add_group(empty_group) + return label_schema + + def init_environment(self, params, model_template, number_of_images=10): + labels_names = ("rectangle", "ellipse", "triangle") + labels_schema = self.generate_label_schema(labels_names) + labels_list = labels_schema.get_labels(False) + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + warnings.filterwarnings("ignore", message=".* coordinates .* are out of bounds.*") + items = [] + for i in range(0, number_of_images): + image_numpy, shapes = generate_random_annotated_image( + image_width=640, + image_height=480, + labels=labels_list, + max_shapes=20, + min_size=50, + max_size=100, + random_seed=None, + ) + # Convert all shapes to polygons + out_shapes = [] + for shape in shapes: + shape_labels = shape.get_labels(include_empty=True) + + in_shape = shape.shape + if isinstance(in_shape, Rectangle): + points = [ + Point(in_shape.x1, in_shape.y1), + Point(in_shape.x2, in_shape.y1), + Point(in_shape.x2, in_shape.y2), + Point(in_shape.x1, in_shape.y2), + ] + elif isinstance(in_shape, Ellipse): + points = [Point(x, y) for x, y in in_shape.get_evenly_distributed_ellipse_coordinates()] + elif isinstance(in_shape, Polygon): + points = in_shape.points + + out_shapes.append(Annotation(Polygon(points=points), labels=shape_labels)) + + image = Image(data=image_numpy) + annotation = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=out_shapes) + items.append(DatasetItemEntity(media=image, annotation_scene=annotation)) + warnings.resetwarnings() + + rng = random.Random() + rng.shuffle(items) + for i, _ in enumerate(items): + subset_region = i / number_of_images + if subset_region >= 0.8: + subset = Subset.TESTING + elif subset_region >= 0.6: + subset = Subset.VALIDATION + else: + subset = Subset.TRAINING + + items[i].subset = subset + + dataset = DatasetEntity(items) + + return environment, dataset + + @staticmethod + def setup_configurable_parameters(template_dir, num_iters=10): + model_template = parse_model_template(osp.join(template_dir, "template.yaml")) + + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.learning_rate_warmup_iters = 1 + hyper_parameters.learning_parameters.num_iters = num_iters + hyper_parameters.learning_parameters.num_checkpoints = 1 + + return hyper_parameters, model_template + + @e2e_pytest_api + def test_cancel_training_segmentation(self): + """ + Tests starting and cancelling training. + + Flow of the test: + - Creates a randomly annotated project with a small dataset. + - Start training and give cancel training signal after 10 seconds. Assert that training + stops within 35 seconds after that + - Start training and give cancel signal immediately. Assert that training stops within 25 seconds. + + This test should be finished in under one minute on a workstation. + """ + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_SEG_TEMPLATE_DIR, num_iters=200) + segmentation_environment, dataset = self.init_environment(hyper_parameters, model_template, 64) + + segmentation_task = SegmentationTrainTask(task_environment=segmentation_environment) + + executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="train_thread") + + output_model = ModelEntity( + dataset, + segmentation_environment.get_model_configuration(), + ) + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + + # Test stopping after some time + start_time = time.time() + train_future = executor.submit(segmentation_task.train, dataset, output_model, train_parameters) + # give train_thread some time to initialize the model + while not segmentation_task._is_training: + time.sleep(10) + segmentation_task.cancel_training() + + # stopping process has to happen in less than 35 seconds + train_future.result() + assert training_progress_curve[-1] == 100 + assert time.time() - start_time < 100, "Expected to stop within 100 seconds." + + # Test stopping immediately + start_time = time.time() + train_future = executor.submit(segmentation_task.train, dataset, output_model) + segmentation_task.cancel_training() + + train_future.result() + assert time.time() - start_time < 25 # stopping process has to happen in less than 25 seconds + + @e2e_pytest_api + def test_training_progress_tracking(self): + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_SEG_TEMPLATE_DIR, num_iters=5) + segmentation_environment, dataset = self.init_environment(hyper_parameters, model_template, 12) + + task = SegmentationTrainTask(task_environment=segmentation_environment) + print("Task initialized, model training starts.") + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + output_model = ModelEntity( + dataset, + segmentation_environment.get_model_configuration(), + ) + task.train(dataset, output_model, train_parameters) + + assert len(training_progress_curve) > 0 + assert np.all(training_progress_curve[1:] >= training_progress_curve[:-1]) + + @e2e_pytest_api + def test_inference_progress_tracking(self): + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_SEG_TEMPLATE_DIR, num_iters=10) + segmentation_environment, dataset = self.init_environment(hyper_parameters, model_template, 12) + + task = SegmentationInferenceTask(task_environment=segmentation_environment) + print("Task initialized, model inference starts.") + + inference_progress_curve = [] + + def progress_callback(progress: int): + assert isinstance(progress, int) + inference_progress_curve.append(progress) + + inference_parameters = InferenceParameters() + inference_parameters.update_progress = progress_callback + task.infer(dataset.with_empty_annotations(), inference_parameters) + + assert len(inference_progress_curve) > 0 + assert np.all(inference_progress_curve[1:] >= inference_progress_curve[:-1]) + + @e2e_pytest_api + def test_inference_task(self): + # Prepare pretrained weights + hyper_parameters, model_template = self.setup_configurable_parameters(DEFAULT_SEG_TEMPLATE_DIR, num_iters=2) + segmentation_environment, dataset = self.init_environment(hyper_parameters, model_template, 30) + val_dataset = dataset.get_subset(Subset.VALIDATION) + + train_task = SegmentationTrainTask(task_environment=segmentation_environment) + + training_progress_curve = [] + + def progress_callback(progress: float, score: Optional[float] = None): + training_progress_curve.append(progress) + + train_parameters = TrainParameters() + train_parameters.update_progress = progress_callback + trained_model = ModelEntity( + dataset, + segmentation_environment.get_model_configuration(), + ) + train_task.train(dataset, trained_model, train_parameters) + performance_after_train = task_eval(train_task, trained_model, val_dataset) + + # Create InferenceTask + segmentation_environment.model = trained_model + inference_task = SegmentationInferenceTask(task_environment=segmentation_environment) + + performance_after_load = task_eval(inference_task, trained_model, val_dataset) + + assert performance_after_train == performance_after_load + + # Export + exported_model = ModelEntity(dataset, segmentation_environment.get_model_configuration(), _id=ObjectId()) + inference_task.export(ExportType.OPENVINO, exported_model) diff --git a/tests/integration/api/test_auto_configuration.py b/tests/integration/api/test_auto_configuration.py deleted file mode 100644 index 217a1e38688..00000000000 --- a/tests/integration/api/test_auto_configuration.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from pathlib import Path - -import pytest -from otx.core.data.module import OTXDataModule -from otx.core.model.entity.base import OTXModel -from otx.core.types.task import OTXTaskType -from otx.engine import Engine -from otx.engine.utils.auto_configurator import DEFAULT_CONFIG_PER_TASK - - -@pytest.mark.parametrize("task", pytest.TASK_LIST) -def test_auto_configuration( - task: OTXTaskType, - tmp_path: Path, - fxt_accelerator: str, - fxt_target_dataset_per_task: dict, -) -> None: - """Test the auto configuration functionality. - - Args: - task (OTXTaskType): The task for which auto configuration is being tested. - tmp_path (Path): The temporary path for storing training data. - fxt_accelerator (str): The accelerator used for training. - fxt_target_dataset_per_task (dict): A dictionary mapping tasks to target datasets. - """ - if task not in DEFAULT_CONFIG_PER_TASK: - pytest.skip(f"Task {task} is not supported in the auto-configuration.") - if task.lower() in ("h_label_cls"): - pytest.skip( - reason="H-labels require num_multiclass_head, num_multilabel_classes, which skip until we have the ability to automate this.", - ) - if task.lower().startswith("anomaly"): - pytest.skip(reason="This will be added in a future pipeline behavior.") - - tmp_path_train = tmp_path / f"auto_train_{task}" - data_root = fxt_target_dataset_per_task[task.lower()] - engine = Engine( - data_root=data_root, - task=task, - work_dir=tmp_path_train, - device=fxt_accelerator, - ) - - # Check OTXModel & OTXDataModule - assert isinstance(engine.model, OTXModel) - assert isinstance(engine.datamodule, OTXDataModule) - - # Check Auto-Configurator task - assert engine._auto_configurator.task == task - - # Check Default Configuration - from otx.cli.utils.jsonargparse import get_configuration - - default_config = get_configuration(DEFAULT_CONFIG_PER_TASK[task]) - default_config["data"]["config"]["data_root"] = data_root - num_classes = engine.datamodule.label_info.num_classes - - default_config["model"]["init_args"]["num_classes"] = num_classes - - assert engine._auto_configurator.config == default_config - - max_epochs = 2 if task.lower() != "zero_shot_visual_prompting" else 1 - train_metric = engine.train(max_epochs=max_epochs) - if task.lower() != "zero_shot_visual_prompting": - assert len(train_metric) > 0 - - test_metric = engine.test() - assert len(test_metric) > 0 diff --git a/tests/integration/api/test_engine_api.py b/tests/integration/api/test_engine_api.py deleted file mode 100644 index 92b766d7371..00000000000 --- a/tests/integration/api/test_engine_api.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from pathlib import Path - -import pytest -from openvino.model_api.tilers import Tiler -from otx.core.data.module import OTXDataModule -from otx.core.model.entity.base import OTXModel -from otx.core.types.task import OTXTaskType -from otx.engine import Engine -from otx.engine.utils.auto_configurator import DEFAULT_CONFIG_PER_TASK, OVMODEL_PER_TASK - - -@pytest.mark.parametrize("task", pytest.TASK_LIST) -def test_engine_from_config( - task: OTXTaskType, - tmp_path: Path, - fxt_accelerator: str, - fxt_target_dataset_per_task: dict, -) -> None: - """Test the Engine.from_config functionality. - - Args: - task (OTXTaskType): The task type. - tmp_path (Path): The temporary path for storing training data. - fxt_accelerator (str): The accelerator used for training. - fxt_target_dataset_per_task (dict): A dictionary mapping tasks to target datasets. - """ - if task not in DEFAULT_CONFIG_PER_TASK: - pytest.skip("Only the Task has Default config is tested to reduce unnecessary resources.") - if task.lower() in ("action_classification"): - pytest.xfail(reason="xFail until this root cause is resolved on the Datumaro side.") - if task.lower() in ("h_label_cls"): - pytest.skip( - reason="H-labels require num_multiclass_head, num_multilabel_classes, which skip until we have the ability to automate this.", - ) - - tmp_path_train = tmp_path / task - engine = Engine.from_config( - config_path=DEFAULT_CONFIG_PER_TASK[task], - data_root=fxt_target_dataset_per_task[task.value.lower()], - work_dir=tmp_path_train, - device=fxt_accelerator, - ) - - # Check OTXModel & OTXDataModule - assert isinstance(engine.model, OTXModel) - assert isinstance(engine.datamodule, OTXDataModule) - - max_epochs = 2 if task.lower() != "zero_shot_visual_prompting" else 1 - train_metric = engine.train(max_epochs=max_epochs) - if task.lower() != "zero_shot_visual_prompting": - assert len(train_metric) > 0 - - test_metric = engine.test() - assert len(test_metric) > 0 - - predict_result = engine.predict() - assert len(predict_result) > 0 - - # A Task that doesn't have Export implemented yet. - # [TODO]: Enable should progress for all Tasks. - if task in [ - OTXTaskType.ACTION_CLASSIFICATION, - OTXTaskType.ACTION_DETECTION, - OTXTaskType.H_LABEL_CLS, - OTXTaskType.ROTATED_DETECTION, - OTXTaskType.ANOMALY_CLASSIFICATION, - OTXTaskType.ANOMALY_DETECTION, - OTXTaskType.ANOMALY_SEGMENTATION, - ]: - return - - # Export IR Model - exported_model_path: Path | dict[str, Path] = engine.export() - if isinstance(exported_model_path, Path): - assert exported_model_path.exists() - elif isinstance(exported_model_path, dict): - for key, value in exported_model_path.items(): - assert value.exists(), f"{value} for {key} doesn't exist." - else: - AssertionError(f"Exported model path is not a Path or a dictionary of Paths: {exported_model_path}") - - # Test with IR Model - if task in OVMODEL_PER_TASK: - if task.lower() in ["visual_prompting", "zero_shot_visual_prompting"]: - test_metric_from_ov_model = engine.test(checkpoint=exported_model_path["decoder"], accelerator="cpu") - else: - test_metric_from_ov_model = engine.test(checkpoint=exported_model_path, accelerator="cpu") - assert len(test_metric_from_ov_model) > 0 - - # List of models with explain supported. - if task not in [ - OTXTaskType.MULTI_CLASS_CLS, - OTXTaskType.MULTI_LABEL_CLS, - # Restore these models after fixing undetermined CI failures for ATSS and Mask RCNN - # OTXTaskType.DETECTION, - # OTXTaskType.ROTATED_DETECTION, - # OTXTaskType.INSTANCE_SEGMENTATION, - ]: - return - - # Predict Torch model with explain - predictions = engine.predict(explain=True) - assert len(predictions[0].saliency_maps) > 0 - - # Export IR model with explain - exported_model_with_explain = engine.export(explain=True) - assert exported_model_with_explain.exists() - - # Infer IR Model with explain: predict - predictions = engine.predict(explain=True, checkpoint=exported_model_with_explain, accelerator="cpu") - assert len(predictions) > 0 - sal_maps_from_prediction = predictions[0].saliency_maps - assert len(sal_maps_from_prediction) > 0 - - # Infer IR Model with explain: explain - explain_results = engine.explain(checkpoint=exported_model_with_explain, accelerator="cpu") - assert len(explain_results[0].saliency_maps) > 0 - sal_maps_from_explain = explain_results[0].saliency_maps - assert (sal_maps_from_prediction[0][0] == sal_maps_from_explain[0][0]).all() - - -@pytest.mark.parametrize("recipe", pytest.TILE_RECIPE_LIST) -def test_engine_from_tile_recipe( - recipe: str, - tmp_path: Path, - fxt_accelerator: str, - fxt_target_dataset_per_task: dict, -): - task = OTXTaskType.DETECTION if "detection" in recipe else OTXTaskType.INSTANCE_SEGMENTATION - - engine = Engine.from_config( - config_path=recipe, - data_root=fxt_target_dataset_per_task[task.value.lower()], - work_dir=tmp_path / task, - device=fxt_accelerator, - ) - engine.train(max_epochs=1) - exported_model_path = engine.export() - assert exported_model_path.exists() - metric = engine.test(exported_model_path, accelerator="cpu") - assert len(metric) > 0 - - # Check OVModel & OVTiler is set correctly - ov_model = engine._auto_configurator.get_ov_model( - model_name=exported_model_path, - label_info=engine.datamodule.label_info, - ) - assert isinstance(ov_model.model, Tiler), "Model should be an instance of Tiler" - assert engine.datamodule.config.tile_config.tile_size[0] == ov_model.model.tile_size - assert engine.datamodule.config.tile_config.overlap == ov_model.model.tiles_overlap diff --git a/tests/integration/api/test_xai.py b/tests/integration/api/test_xai.py deleted file mode 100644 index 68e961a2230..00000000000 --- a/tests/integration/api/test_xai.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from pathlib import Path - -import numpy as np -import openvino.runtime as ov -import pytest -from otx.core.data.entity.base import OTXBatchPredEntity, OTXBatchPredEntityWithXAI -from otx.engine import Engine - -RECIPE_LIST_ALL = pytest.RECIPE_LIST -MULTI_CLASS_CLS = [recipe for recipe in RECIPE_LIST_ALL if "multi_class_cls" in recipe] -MULTI_LABEL_CLS = [recipe for recipe in RECIPE_LIST_ALL if "multi_label_cls" in recipe] -MC_ML_CLS = MULTI_CLASS_CLS + MULTI_LABEL_CLS - -DETECTION_LIST = [recipe for recipe in RECIPE_LIST_ALL if "/detection" in recipe and "tile" not in recipe] -INST_SEG_LIST = [recipe for recipe in RECIPE_LIST_ALL if "instance_segmentation" in recipe and "tile" not in recipe] -EXPLAIN_MODEL_LIST = MC_ML_CLS + DETECTION_LIST + INST_SEG_LIST - -MEAN_TORCH_OV_DIFF = 150 - - -@pytest.mark.parametrize( - "recipe", - EXPLAIN_MODEL_LIST, -) -def test_forward_explain( - recipe: str, - fxt_target_dataset_per_task: dict, - fxt_accelerator: str, -) -> None: - """ - Test forward == forward_explain. - - Args: - recipe (str): The recipe to use for predicting. (eg. 'classification/otx_mobilenet_v3_large.yaml') - fxt_target_dataset_per_task (dict): A dictionary mapping tasks to target datasets. - fxt_accelerator (str): The accelerator used for predict. - - Returns: - None - """ - task = recipe.split("/")[-2] - model_name = recipe.split("/")[-1].split(".")[0] - - if "dino" in model_name or "rtmdet_inst_tiny" in model_name: - pytest.skip("DINO and Rtmdet_tiny are not supported.") - - engine = Engine.from_config( - config_path=recipe, - data_root=fxt_target_dataset_per_task[task], - device=fxt_accelerator, - ) - - predict_result = engine.predict() - assert isinstance(predict_result[0], OTXBatchPredEntity) - - predict_result_explain = engine.predict(explain=True) - assert isinstance(predict_result_explain[0], OTXBatchPredEntityWithXAI) - - batch_size = len(predict_result[0].scores) - for i in range(batch_size): - assert all(predict_result[0].labels[i] == predict_result_explain[0].labels[i]) - assert all(predict_result[0].scores[i] == predict_result_explain[0].scores[i]) - - -@pytest.mark.parametrize( - "recipe", - EXPLAIN_MODEL_LIST, -) -def test_predict_with_explain( - recipe: str, - tmp_path: Path, - fxt_target_dataset_per_task: dict, - fxt_accelerator: str, -) -> None: - """ - Test XAI. - - Args: - recipe (str): The recipe to use for predicting. (eg. 'classification/otx_mobilenet_v3_large.yaml') - tmp_path (Path): The temporary path for storing the outputs. - fxt_target_dataset_per_task (dict): A dictionary mapping tasks to target datasets. - fxt_accelerator (str): The accelerator used for predict. - - Returns: - None - """ - task = recipe.split("/")[-2] - model_name = recipe.split("/")[-1].split(".")[0] - - if "dino" in model_name or "rtmdet_inst_tiny" in model_name: - pytest.skip("DINO and Rtmdet_tiny are not supported.") - - if "ssd_mobilenetv2" in model_name: - pytest.skip("There's issue with SSD model. Skip for now.") - - tmp_path = tmp_path / f"otx_xai_{model_name}" - engine = Engine.from_config( - config_path=recipe, - data_root=fxt_target_dataset_per_task[task], - device=fxt_accelerator, - work_dir=tmp_path, - ) - - # Predict with explain torch & process maps - predict_result_explain_torch = engine.predict(explain=True) - assert isinstance(predict_result_explain_torch[0], OTXBatchPredEntityWithXAI) - assert predict_result_explain_torch[0].saliency_maps is not None - assert isinstance(predict_result_explain_torch[0].saliency_maps[0], dict) - - # Export with explain - ckpt_path = tmp_path / "checkpoint.ckpt" - engine.trainer.save_checkpoint(ckpt_path) - exported_model_path = engine.export(checkpoint=ckpt_path, explain=True) - - model = ov.Core().read_model(exported_model_path) - feature_vector_output = None - saliency_map_output = None - for output in model.outputs: - if "feature_vector" in output.get_names(): - feature_vector_output = output - if "saliency_map" in output.get_names(): - saliency_map_output = output - assert saliency_map_output is not None - if "instance_segmentation" in recipe: - assert len(saliency_map_output.get_shape()) == 1 - else: - assert len(saliency_map_output.get_shape()) in [3, 4] - - assert feature_vector_output is not None - assert len(feature_vector_output.get_shape()) == 2 - - # Predict OV model with xai & process maps - predict_result_explain_ov = engine.predict(checkpoint=exported_model_path, explain=True) - assert isinstance(predict_result_explain_ov[0], OTXBatchPredEntityWithXAI) - assert predict_result_explain_ov[0].saliency_maps is not None - assert isinstance(predict_result_explain_ov[0].saliency_maps[0], dict) - assert predict_result_explain_ov[0].feature_vectors is not None - assert isinstance(predict_result_explain_ov[0].feature_vectors[0], np.ndarray) - - if task == "instance_segmentation" or "atss_r50_fpn" in recipe: - # For instance segmentation and atss_r50_fpn batch_size for Torch task 1, for OV 2. - # That why the predict_results have different format and we can't compare them. - - # The OV saliency maps are different from Torch and incorrect, possible root cause can be on MAPI side - # TODO(gzalessk): remove this if statement when the issue is resolved # noqa: TD003 - return - - maps_torch = predict_result_explain_torch[0].saliency_maps - maps_ov = predict_result_explain_ov[0].saliency_maps - - assert len(maps_torch) == len(maps_ov) - - for i in range(len(maps_torch)): - for class_id in maps_torch[i]: - assert class_id in maps_ov[i] - assert ( - np.mean(abs(maps_torch[i][class_id].astype(np.float32) - maps_ov[i][class_id].astype(np.float32))) - < MEAN_TORCH_OV_DIFF - ) diff --git a/tests/integration/api/xai/__init__.py b/tests/integration/api/xai/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/api/xai/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/cli/__init__.py b/tests/integration/cli/__init__.py index 6a16273c024..79931efa777 100644 --- a/tests/integration/cli/__init__.py +++ b/tests/integration/cli/__init__.py @@ -1,2 +1,3 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2021-2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/cli/action/__init__.py b/tests/integration/cli/action/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/cli/action/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/cli/action/test_action_classification.py b/tests/integration/cli/action/test_action_classification.py new file mode 100644 index 00000000000..d3cc539777b --- /dev/null +++ b/tests/integration/cli/action/test_action_classification.py @@ -0,0 +1,143 @@ +"""Tests for Action Classification Task with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import copy +from copy import deepcopy + +import pytest +import torch + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + otx_resume_testing, + get_template_dir, +) + +# Finetuning arguments +# TODO: Need to change sample dataset +args = { + "--train-data-roots": "tests/assets/cvat_dataset/action_classification/train", + "--val-data-roots": "tests/assets/cvat_dataset/action_classification/train", + "--test-data-roots": "tests/assets/cvat_dataset/action_classification/train", + "train_params": ["params", "--learning_parameters.num_iters", "1", "--learning_parameters.batch_size", "4"], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", +] + +otx_dir = os.getcwd() + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + os.path.join("src/otx/algorithms/action/configs", "classification", "x3d", "template.yaml") + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] +else: + templates = Registry("src/otx/algorithms/action").filter(task_type="ACTION_CLASSIFICATION").templates + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsOTXActionClassification: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls" + otx_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls" + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + if template.model_template_id == "Custom_Action_Classification_MoViNet": + pytest.xfail("Issue#2058: MoViNet inference fails in OV 2023.0") + tmp_dir_path = tmp_dir_path / "action_cls" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("bs_adapt_type", ["Safe", "Full"]) + def test_otx_train_auto_adapt_batch_size(self, template, tmp_dir_path, bs_adapt_type): + adapting_bs_args = deepcopy(args) + adapting_bs_args["train_params"].extend(["--learning_parameters.auto_adapt_batch_size", bs_adapt_type]) + tmp_dir_path = tmp_dir_path / f"action_cls_auto_adapt_{bs_adapt_type}_batch_size" + otx_train_testing(template, tmp_dir_path, otx_dir, adapting_bs_args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_auto_adapt_num_workers(self, template, tmp_dir_path): + adapting_num_workers_args = deepcopy(args) + adapting_num_workers_args["train_params"].extend(["--learning_parameters.auto_num_workers", "True"]) + tmp_dir_path = tmp_dir_path / f"action_cls_auto_adapt_num_workers" + otx_train_testing(template, tmp_dir_path, otx_dir, adapting_num_workers_args) + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_cls/test_multi_gpu" + args1 = deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) diff --git a/tests/integration/cli/action/test_action_detection.py b/tests/integration/cli/action/test_action_detection.py new file mode 100644 index 00000000000..7e772c0b65b --- /dev/null +++ b/tests/integration/cli/action/test_action_detection.py @@ -0,0 +1,109 @@ +"""Tests for Action Detection Task with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os + +import pytest + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + otx_resume_testing, + get_template_dir, +) + +# Finetuning arguments +# TODO: Need to change sample dataset +args = { + "--train-data-roots": "tests/assets/cvat_dataset/action_detection/train", + "--val-data-roots": "tests/assets/cvat_dataset/action_detection/train", + "--test-data-roots": "tests/assets/cvat_dataset/action_detection/train", + "train_params": ["params", "--learning_parameters.num_iters", "1", "--learning_parameters.batch_size", "4"], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", +] + +otx_dir = os.getcwd() + +TT_STABILITY_TESTS = os.environ.get("TT_STABILITY_TESTS", False) +if TT_STABILITY_TESTS: + default_template = parse_model_template( + os.path.join("src/otx/algorithms/action/configs", "detection", "x3d_fast_rcnn", "template.yaml") + ) + templates = [default_template] * 100 + templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] +else: + templates = Registry("src/otx/algorithms/action").filter(task_type="ACTION_DETECTION").templates + templates_ids = [template.model_template_id for template in templates] + + +class TestToolsOTXActionDetection: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + otx_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True) + + @e2e_pytest_component + @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "action_det" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) diff --git a/tests/integration/cli/anomaly/__init__.py b/tests/integration/cli/anomaly/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/cli/anomaly/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/cli/anomaly/test_anomaly_classification.py b/tests/integration/cli/anomaly/test_anomaly_classification.py new file mode 100644 index 00000000000..833497307a0 --- /dev/null +++ b/tests/integration/cli/anomaly/test_anomaly_classification.py @@ -0,0 +1,88 @@ +"""Tests for anomaly classification with OTX CLI""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +import pytest + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + generate_model_template_testing, +) + +args = { + "--train-data-roots": "tests/assets/anomaly/hazelnut/train", + "--val-data-roots": "tests/assets/anomaly/hazelnut/test", + "--test-data-roots": "tests/assets/anomaly/hazelnut/test", + "--input": "tests/assets/anomaly/hazelnut/test/colour", + "train_params": [], +} + +otx_dir = os.getcwd() + +templates = Registry("src/otx/algorithms").filter(task_type="ANOMALY_CLASSIFICATION").templates +templates_ids = [template.model_template_id for template in templates] + + +TestAnomalyClassificationModelTemplates = generate_model_template_testing(templates) + + +class TestToolsAnomalyClassification: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/integration/cli/anomaly/test_anomaly_detection.py b/tests/integration/cli/anomaly/test_anomaly_detection.py new file mode 100644 index 00000000000..9ba6f10f257 --- /dev/null +++ b/tests/integration/cli/anomaly/test_anomaly_detection.py @@ -0,0 +1,88 @@ +"""Tests for anomaly detection with OTX CLI.""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +import pytest + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + generate_model_template_testing, +) + +args = { + "--train-data-roots": "tests/assets/anomaly/hazelnut/train", + "--val-data-roots": "tests/assets/anomaly/hazelnut/test", + "--test-data-roots": "tests/assets/anomaly/hazelnut/test", + "--input": "tests/assets/anomaly/hazelnut/test/colour", + "train_params": [], +} + +otx_dir = os.getcwd() + +templates = Registry("src/otx/algorithms").filter(task_type="ANOMALY_DETECTION").templates +templates_ids = [template.model_template_id for template in templates] + + +TestAnomalyDetectionModelTemplates = generate_model_template_testing(templates) + + +class TestToolsAnomalyDetection: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/integration/cli/anomaly/test_anomaly_segmentation.py b/tests/integration/cli/anomaly/test_anomaly_segmentation.py new file mode 100644 index 00000000000..6a52f9727fb --- /dev/null +++ b/tests/integration/cli/anomaly/test_anomaly_segmentation.py @@ -0,0 +1,88 @@ +"""Tests for anomaly segmentation with OTX CLI""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +import pytest + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_train_testing, + generate_model_template_testing, +) + +args = { + "--train-data-roots": "tests/assets/anomaly/hazelnut/train", + "--val-data-roots": "tests/assets/anomaly/hazelnut/test", + "--test-data-roots": "tests/assets/anomaly/hazelnut/test", + "--input": "tests/assets/anomaly/hazelnut/test/colour", + "train_params": [], +} + +otx_dir = os.getcwd() + +templates = Registry("src/otx/algorithms").filter(task_type="ANOMALY_SEGMENTATION").templates +templates_ids = [template.model_template_id for template in templates] + + +TestAnomalySegmentationModelTemplates = generate_model_template_testing(templates) + + +class TestToolsAnomalySegmentation: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_openvino(self, template, tmp_dir_path): + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/integration/cli/classification/__init__.py b/tests/integration/cli/classification/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/cli/classification/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/cli/classification/test_classification.py b/tests/integration/cli/classification/test_classification.py new file mode 100644 index 00000000000..186613de79f --- /dev/null +++ b/tests/integration/cli/classification/test_classification.py @@ -0,0 +1,535 @@ +"""Tests for Classification with OTX CLI""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import glob +import os + +import pytest +import torch + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_explain_all_classes_openvino_testing, + otx_explain_openvino_testing, + otx_explain_process_saliency_maps_openvino_testing, + otx_explain_testing, + otx_explain_testing_all_classes, + otx_explain_testing_process_saliency_maps, + otx_export_testing, + otx_hpo_testing, + otx_resume_testing, + otx_train_testing, + generate_model_template_testing, +) + +# Pre-train w/ 'label_0', 'label_1', 'label_2' classes +args = { + "--train-data-roots": "tests/assets/classification_dataset_class_incremental", + "--val-data-roots": "tests/assets/classification_dataset_class_incremental", + "--test-data-roots": "tests/assets/classification_dataset_class_incremental", + "--input": "tests/assets/classification_dataset/0", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + ], +} + +# Warmstart using data w/ 'intel', 'openvino', 'opencv' classes +args_selfsl = { + "--train-data-roots": "tests/assets/classification_dataset", + "--train-type": "Selfsupervised", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", +] + +otx_dir = os.getcwd() + + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +default_template = parse_model_template( + os.path.join( + "src/otx/algorithms/classification", + "configs", + "efficientnet_b0_cls_incr", + "template.yaml", + ) +) +default_templates = [default_template] +default_templates_ids = [default_template.model_template_id] + +templates = Registry("src/otx/algorithms/classification").filter(task_type="CLASSIFICATION").templates +templates_ids = [template.model_template_id for template in templates] + + +TestClassificationModelTemplates = generate_model_template_testing(templates) + + +class TestMultiClassClassificationCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_supcon(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_supcon" + args1 = copy.deepcopy(args) + args1["train_params"].extend(["--learning_parameters.enable_supcon", "True"]) + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_export_testing(template, tmp_dir_path, dump_features, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_export_testing(template, tmp_dir_path, half_precision=False, check_ir_meta=True, is_onnx=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_explain_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_all_classes(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_explain_testing_all_classes(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_process_saliency_maps(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_explain_testing_process_saliency_maps(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_all_classes_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_explain_all_classes_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_process_saliency_maps_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_explain_process_saliency_maps_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_multi_gpu" + args1 = copy.deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_semisl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_semisl" + args_semisl = copy.deepcopy(args) + args_semisl["--unlabeled-data-roots"] = args["--train-data-roots"] + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl) + template_dir = get_template_dir(template, tmp_dir_path) + assert os.path.exists(f"{template_dir}/semisl") + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_multi_gpu_train_semisl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_multi_gpu_semisl" + args_semisl_multigpu = copy.deepcopy(args) + args_semisl_multigpu["--unlabeled-data-roots"] = args["--train-data-roots"] + args_semisl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl_multigpu) + template_dir = get_template_dir(template, tmp_dir_path) + assert os.path.exists(f"{template_dir}/semisl") + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_selfsl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_selfsl" + otx_train_testing(template, tmp_dir_path, otx_dir, args_selfsl) + template_dir = get_template_dir(template, tmp_dir_path) + assert os.path.exists(f"{template_dir}/selfsl") + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_multi_gpu_train_selfsl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_multi_gpu_selfsl" + args_selfsl_multigpu = copy.deepcopy(args_selfsl) + args_selfsl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_selfsl_multigpu) + template_dir = get_template_dir(template, tmp_dir_path) + assert os.path.exists(f"{template_dir}/selfsl") + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_enable_noisy_lable_detection(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_class_cls" + new_args = copy.deepcopy(args) + new_args["train_params"] += ["--algo_backend.enable_noisy_label_detection", "True"] + otx_train_testing(template, tmp_dir_path, otx_dir, new_args) + + has_export_dir = False + for root, _, _ in os.walk(tmp_dir_path): + if "noisy_label_detection" == os.path.basename(root): + has_export_dir = True + assert has_export_dir + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + @pytest.mark.parametrize("bs_adapt_type", ["Safe", "Full"]) + def test_otx_train_auto_adapt_batch_size(self, template, tmp_dir_path, bs_adapt_type): + adapting_bs_args = copy.deepcopy(args) + adapting_bs_args["train_params"].extend(["--learning_parameters.auto_adapt_batch_size", bs_adapt_type]) + tmp_dir_path = tmp_dir_path / f"multi_class_cls_auto_adapt_{bs_adapt_type}_batch_size" + otx_train_testing(template, tmp_dir_path, otx_dir, adapting_bs_args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_auto_adapt_num_workers(self, template, tmp_dir_path): + adapting_num_workers_args = copy.deepcopy(args) + adapting_num_workers_args["train_params"].extend(["--learning_parameters.auto_num_workers", "True"]) + tmp_dir_path = tmp_dir_path / f"multi_class_cls_auto_adapt_num_workers" + otx_train_testing(template, tmp_dir_path, otx_dir, adapting_num_workers_args) + + +# Multi-label training w/ 'car', 'tree', 'bug' classes +args_m = { + "--train-data-roots": "tests/assets/datumaro_multilabel", + "--val-data-roots": "tests/assets/datumaro_multilabel", + "--test-data-roots": "tests/assets/datumaro_multilabel", + "--input": "tests/assets/datumaro_multilabel/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + ], +} + + +class TestMultilabelClassificationCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_train_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_decr(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls/test_cls_decr" + otx_train_testing(template, tmp_dir_path, otx_dir, args_m) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args_m) + args1["--train-data-roots"] = "tests/assets/datumaro_multilabel_class_decremental" + args1["--val-data-roots"] = "tests/assets/datumaro_multilabel_class_decremental" + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_eval_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_explain_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain_all_classes(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_explain_testing_all_classes(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain_process_saliency_maps(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_explain_testing_process_saliency_maps(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain_all_classes_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_explain_all_classes_openvino_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain_process_saliency_maps_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_explain_process_saliency_maps_openvino_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args_m, threshold=1.0, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args_m, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args_m) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_semisl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "multi_label_cls" / "test_semisl" + args_semisl = copy.deepcopy(args_m) + args_semisl["--unlabeled-data-roots"] = args_m["--train-data-roots"] + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl) + template_dir = get_template_dir(template, tmp_dir_path) + assert os.path.exists(f"{template_dir}/semisl") + + +args_h = { + "--train-data-roots": "tests/assets/datumaro_h-label", + "--val-data-roots": "tests/assets/datumaro_h-label", + "--test-data-roots": "tests/assets/datumaro_h-label", + "--input": "tests/assets/datumaro_h-label/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + ], +} + + +class TestHierarchicalClassificationCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_train_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_decr(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls/test_cls_decr" + otx_train_testing(template, tmp_dir_path, otx_dir, args_h) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args_h) + args1["--train-data-roots"] = "tests/assets/datumaro_h-label_class_decremental" + args1["--val-data-roots"] = "tests/assets/datumaro_h-label_class_decremental" + args1["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_eval_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_explain_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain_all_classes(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_explain_testing_all_classes(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain_process_saliency_maps(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_explain_testing_process_saliency_maps(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain_all_classes_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_explain_all_classes_openvino_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_explain_process_saliency_maps_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_explain_process_saliency_maps_openvino_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args_h, threshold=1.0, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args_h) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args_h, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "h_label_cls" + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args_h) diff --git a/tests/integration/cli/detection/__init__.py b/tests/integration/cli/detection/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/cli/detection/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/cli/detection/test_detection.py b/tests/integration/cli/detection/test_detection.py new file mode 100644 index 00000000000..1ae0ab1e825 --- /dev/null +++ b/tests/integration/cli/detection/test_detection.py @@ -0,0 +1,291 @@ +"""Tests for Class-Incremental Learning for object detection with OTX CLI""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import os + +import pytest +import torch + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_explain_all_classes_openvino_testing, + otx_explain_openvino_testing, + otx_explain_process_saliency_maps_openvino_testing, + otx_explain_testing, + otx_explain_testing_all_classes, + otx_explain_testing_process_saliency_maps, + otx_export_testing, + otx_hpo_testing, + otx_resume_testing, + otx_train_testing, + generate_model_template_testing, +) + +args = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + "--postprocessing.max_num_detections", + "200", + ], +} + +args_semisl = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--unlabeled-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", +] + +otx_dir = os.getcwd() + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +default_template = parse_model_template( + os.path.join("src/otx/algorithms/detection/configs", "detection", "mobilenetv2_atss", "template.yaml") +) +default_templates = [default_template] +default_templates_ids = [default_template.model_template_id] + +_templates = Registry("src/otx/algorithms/detection").filter(task_type="DETECTION").templates +templates = [] +for template in _templates: + if template.name not in ["YOLOX-S", "YOLOX-X"]: + templates.append(template) # YOLOX-S, and YOLOX-X use same model and data pipeline config with YOLOX-L +templates_ids = [template.model_template_id for template in templates] + +experimental_templates = [ + parse_model_template( + "src/otx/algorithms/detection/configs/detection/resnet50_deformable_detr/template_experimental.yaml" + ), + parse_model_template("src/otx/algorithms/detection/configs/detection/resnet50_dino/template_experimental.yaml"), + parse_model_template( + "src/otx/algorithms/detection/configs/detection/resnet50_lite_dino/template_experimental.yaml" + ), +] +experimental_template_ids = [template.model_template_id for template in experimental_templates] + +templates_w_experimental = templates + experimental_templates +templates_ids_w_experimental = templates_ids + experimental_template_ids + + +TestDetectionModelTemplates = generate_model_template_testing(templates) + + +class TestDetectionCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + _args = args.copy() + # FIXME: remove this block once Issue#2504 resolved + if "DINO" in template.name: + _args["train_params"] = [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + "--learning_parameters.input_size", + "Default", + ] + otx_train_testing(template, tmp_dir_path, otx_dir, _args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection/test_resume" + _args = args.copy() + _resume_params = resume_params.copy() + # FIXME: remove this block once Issue#2504 resolved + if "DINO" in template.name: + _args["train_params"] = [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + "--learning_parameters.input_size", + "Default", + ] + _resume_params.extend(["--learning_parameters.input_size", "Default"]) + otx_resume_testing(template, tmp_dir_path, otx_dir, _args) + template_work_dir = get_template_dir(template, tmp_dir_path) + _args["train_params"] = _resume_params + _args[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, _args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "detection" + otx_export_testing(template, tmp_dir_path, dump_features, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) + def test_otx_export_onnx(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "detection" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_all_classes(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_testing_all_classes(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_process_saliency_maps(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_testing_process_saliency_maps(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_all_classes_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_all_classes_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_process_saliency_maps_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_explain_process_saliency_maps_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection/test_multi_gpu" + args1 = copy.deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_semisl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection/test_semisl" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl) + template_dir = get_template_dir(template, tmp_dir_path) + assert os.path.exists(f"{template_dir}/semisl") + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_multi_gpu_train_semisl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "detection/test_multi_gpu_semisl" + args_semisl_multigpu = copy.deepcopy(args_semisl) + args_semisl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl_multigpu) + template_dir = get_template_dir(template, tmp_dir_path) + assert os.path.exists(f"{template_dir}/semisl") + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + @pytest.mark.parametrize("bs_adapt_type", ["Safe", "Full"]) + def test_otx_train_auto_adapt_batch_size(self, template, tmp_dir_path, bs_adapt_type): + adapting_bs_args = copy.deepcopy(args) + adapting_bs_args["train_params"].extend(["--learning_parameters.auto_adapt_batch_size", bs_adapt_type]) + tmp_dir_path = tmp_dir_path / f"detection_auto_adapt_{bs_adapt_type}_batch_size" + otx_train_testing(template, tmp_dir_path, otx_dir, adapting_bs_args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_auto_adapt_batch_size(self, template, tmp_dir_path): + adapting_num_workers_args = copy.deepcopy(args) + adapting_num_workers_args["train_params"].extend(["--learning_parameters.auto_num_workers", "True"]) + tmp_dir_path = tmp_dir_path / f"detection_auto_adapt_num_workers" + otx_train_testing(template, tmp_dir_path, otx_dir, adapting_num_workers_args) diff --git a/tests/integration/cli/detection/test_tiling_detection.py b/tests/integration/cli/detection/test_tiling_detection.py new file mode 100644 index 00000000000..d87600ed66c --- /dev/null +++ b/tests/integration/cli/detection/test_tiling_detection.py @@ -0,0 +1,129 @@ +"""Tests for OTX Class-Incremental Learning for object detection with OTX CLI""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os + +import pytest + +from otx.api.entities.model_template import parse_model_template +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_explain_openvino_testing, + otx_explain_testing, + otx_export_testing, + otx_hpo_testing, + otx_train_testing, +) + +args = { + "--train-data-roots": "tests/assets/small_objects", + "--val-data-roots": "tests/assets/small_objects", + "--test-data-roots": "tests/assets/small_objects", + "--input": "tests/assets/small_objects/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + "--tiling_parameters.enable_tiling", + "1", + "--tiling_parameters.enable_adaptive_params", + "1", + "--postprocessing.max_num_detections", + "200", + ], +} + +otx_dir = os.getcwd() + +default_template = parse_model_template( + os.path.join("src/otx/algorithms/detection/configs", "detection", "mobilenetv2_atss", "template.yaml") +) +templates = [default_template] +templates_ids = [default_template.model_template_id] + + +class TestTilingDetectionCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_explain_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_det" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/integration/cli/instance_segmentation/__init__.py b/tests/integration/cli/instance_segmentation/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/cli/instance_segmentation/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/cli/instance_segmentation/test_instance_segmentation.py b/tests/integration/cli/instance_segmentation/test_instance_segmentation.py new file mode 100644 index 00000000000..69ec8b386b7 --- /dev/null +++ b/tests/integration/cli/instance_segmentation/test_instance_segmentation.py @@ -0,0 +1,219 @@ +"""Tests for Class-Incremental Learning for object detection with OTX CLI""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import os +from pathlib import Path + +import pytest +import torch + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_explain_openvino_testing, + otx_explain_testing, + otx_export_testing, + otx_hpo_testing, + otx_resume_testing, + otx_train_testing, + generate_model_template_testing, +) + +args = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "2", + "--postprocessing.max_num_detections", + "200", + ], +} + +args_semisl = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--unlabeled-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "2", + "--postprocessing.max_num_detections", + "200", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "2", + "--postprocessing.max_num_detections", + "200", +] + +otx_dir = os.getcwd() + +iseg_config_root = Path("src/otx/algorithms/detection/configs/instance_segmentation") + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +default_template = parse_model_template(iseg_config_root / "resnet50_maskrcnn" / "template.yaml") +default_templates = [default_template] +default_templates_ids = [default_template.model_template_id] + +templates = Registry("src/otx/algorithms/detection").filter(task_type="INSTANCE_SEGMENTATION").templates +templates_ids = [template.model_template_id for template in templates] + +# Add experimental templates +templates_with_experimental = copy.deepcopy(templates) +templates_ids_with_experimental = copy.deepcopy(templates_ids) +for experimental_template in iseg_config_root.glob("**/*_experimental.yaml"): + template_experimental = parse_model_template(experimental_template) + templates_with_experimental.extend([template_experimental]) + templates_ids_with_experimental.extend([template_experimental.model_template_id]) + + +TestInstanceSegmentationModelTemplates = generate_model_template_testing(templates) + + +class TestInstanceSegmentationCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_export_onnx(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_explain_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + if torch.__version__.startswith("2.") and template.name.startswith("MaskRCNN"): + pytest.skip( + reason="Issue#2451: Torch2.0 CUDA runtime error during NNCF optimization of ROIAlign MMCV kernel for MaskRCNN" + ) + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg/test_multi_gpu" + args1 = copy.deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_semisl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg/test_semisl" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl) + template_dir = get_template_dir(template, tmp_dir_path) + assert (Path(template_dir) / "semisl").is_dir() + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_multi_gpu_train_semisl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg/test_multi_gpu_semisl" + args_semisl_multigpu = copy.deepcopy(args_semisl) + args_semisl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl_multigpu) + template_dir = get_template_dir(template, tmp_dir_path) + assert (Path(template_dir) / "semisl").is_dir() diff --git a/tests/integration/cli/instance_segmentation/test_rotated_detection.py b/tests/integration/cli/instance_segmentation/test_rotated_detection.py new file mode 100644 index 00000000000..e51966eb866 --- /dev/null +++ b/tests/integration/cli/instance_segmentation/test_rotated_detection.py @@ -0,0 +1,45 @@ +"""Tests for rotated object detection with OTX CLI""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import os + +import pytest +import torch + +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + otx_train_testing, + generate_model_template_testing, +) + +args = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": ["params", "--learning_parameters.num_iters", "1", "--learning_parameters.batch_size", "2"], +} + +otx_dir = os.getcwd() + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 + +templates = Registry("src/otx/algorithms/detection").filter(task_type="ROTATED_DETECTION").templates +templates_ids = [template.model_template_id for template in templates] + + +TestRotatedDetectionModelTemplates = generate_model_template_testing(templates) + + +# NOTE: Most of implementation parts are same with the ISeg tasks. +# So, currently just added the `test_otx_train` function to check +# Whether further modifications make Rotated detection fails or not +class TestRotatedDetectionCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "ins_seg" + otx_train_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/integration/cli/instance_segmentation/test_tiling_instseg.py b/tests/integration/cli/instance_segmentation/test_tiling_instseg.py new file mode 100644 index 00000000000..320c6002a14 --- /dev/null +++ b/tests/integration/cli/instance_segmentation/test_tiling_instseg.py @@ -0,0 +1,167 @@ +"""Tests for OTX Class-Incremental Learning for instance segmentation with OTX CLI""" +# Copyright (C) 2022-2023-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import os + +import pytest +import torch + +from otx.api.entities.model_template import parse_model_template +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_explain_openvino_testing, + otx_explain_testing, + otx_export_testing, + otx_hpo_testing, + otx_train_testing, + otx_resume_testing, +) + +args = { + "--train-data-roots": "tests/assets/small_objects", + "--val-data-roots": "tests/assets/small_objects", + "--test-data-roots": "tests/assets/small_objects", + "--input": "tests/assets/small_objects/images/train", + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + "--tiling_parameters.enable_tiling", + "1", + "--tiling_parameters.enable_tile_classifier", + "1", + "--tiling_parameters.enable_adaptive_params", + "1", + "--postprocessing.max_num_detections", + "200", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", + "--tiling_parameters.enable_tiling", + "1", + "--tiling_parameters.enable_tile_classifier", + "1", + "--tiling_parameters.enable_adaptive_params", + "1", +] + +otx_dir = os.getcwd() + +default_template = parse_model_template( + os.path.join("src/otx/algorithms/detection/configs", "instance_segmentation", "resnet50_maskrcnn", "template.yaml") +) +templates = [default_template] +templates_ids = [default_template.model_template_id] + + +class TestTilingInstanceSegmentationCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_export_testing(template, tmp_dir_path, dump_features) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_explain_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_explain_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.skip("Tiling w/ HPO fails in CI") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "tiling_ins_seg" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + if torch.__version__.startswith("2.") and template.name.startswith("MaskRCNN"): + pytest.skip( + reason="Issue#2451: Torch2.0 CUDA runtime error during NNCF optimization of ROIAlign MMCV kernel for MaskRCNN" + ) + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) diff --git a/tests/integration/cli/semantic_segmentation/__init__.py b/tests/integration/cli/semantic_segmentation/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/integration/cli/semantic_segmentation/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/cli/semantic_segmentation/test_segmentation.py b/tests/integration/cli/semantic_segmentation/test_segmentation.py new file mode 100644 index 00000000000..7f1b286e0df --- /dev/null +++ b/tests/integration/cli/semantic_segmentation/test_segmentation.py @@ -0,0 +1,247 @@ +"""Tests for Semantic segmentation with OTX CLI""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +from pathlib import Path + +import pytest +import torch + +from otx.api.entities.model_template import parse_model_template +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_hpo_testing, + otx_resume_testing, + otx_train_testing, + generate_model_template_testing, +) + +args = { + "--train-data-roots": "tests/assets/common_semantic_segmentation_dataset/train", + "--val-data-roots": "tests/assets/common_semantic_segmentation_dataset/val", + "--test-data-roots": "tests/assets/common_semantic_segmentation_dataset/val", + "--input": "tests/assets/common_semantic_segmentation_dataset/train/images", + "train_params": [ + "params", + "--learning_parameters.learning_rate_warmup_iters", + "1", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + ], +} + +args_semisl = { + "--train-data-roots": "tests/assets/common_semantic_segmentation_dataset/train", + "--val-data-roots": "tests/assets/common_semantic_segmentation_dataset/val", + "--test-data-roots": "tests/assets/common_semantic_segmentation_dataset/val", + "--unlabeled-data-roots": "tests/assets/common_semantic_segmentation_dataset/train", + "train_params": [ + "params", + "--learning_parameters.learning_rate_warmup_iters", + "1", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + ], +} + +args_selfsl = { + "--train-data-roots": "tests/assets/common_semantic_segmentation_dataset/train/images", + "--input": "tests/assets/segmentation/custom/images/training", + "train_params": [ + "params", + "--learning_parameters.learning_rate_warmup_iters", + "1", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.num_iters", + "2", + "--learning_parameters.batch_size", + "4", +] + +otx_dir = Path.cwd() + +MULTI_GPU_UNAVAILABLE = torch.cuda.device_count() <= 1 +default_template = parse_model_template( + Path("src/otx/algorithms/segmentation/configs") / "ocr_lite_hrnet_18_mod2" / "template.yaml" +) + +# add integration test for semi-sl with new SegNext model and prototype based approach +segnext_template = parse_model_template( + Path("src/otx/algorithms/segmentation/configs") / "ham_segnext_s" / "template.yaml" +) +default_templates = [default_template, segnext_template] +default_templates_ids = [default_template.model_template_id, segnext_template.model_template_id] + +templates = Registry("src/otx/algorithms/segmentation").filter(task_type="SEGMENTATION").templates +templates_ids = [template.model_template_id for template in templates] +TestSemanticSegmentationModelTemplates = generate_model_template_testing(templates) + + +class TestSegmentationCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_supcon(self, template, tmp_dir_path): + if template.name == "SegNext-s": + pytest.skip(reason="Segnext model doesn't support supcon training.") + args1 = copy.deepcopy(args) + args1["train_params"].extend(["--learning_parameters.enable_supcon", "True"]) + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_train_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + @pytest.mark.parametrize("dump_features", [True, False]) + def test_otx_export(self, template, tmp_dir_path, dump_features): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_export_testing(template, tmp_dir_path, dump_features, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True, check_ir_meta=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0, half_precision=half_precision) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_hpo(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_hpo" + otx_hpo_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_nncf_optimize(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation" + nncf_optimize_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_multi_gpu_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_multi_gpu" + args1 = copy.deepcopy(args) + args1["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_semisl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_semisl" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl) + template_dir = get_template_dir(template, tmp_dir_path) + # Check that semi-sl launched + assert (Path(template_dir) / "semisl").is_dir() + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_multi_gpu_train_semisl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_multi_gpu_semisl" + args_semisl_multigpu = copy.deepcopy(args_semisl) + args_semisl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_semisl_multigpu) + template_dir = get_template_dir(template, tmp_dir_path) + # Check that semi-sl launched + assert (Path(template_dir) / "semisl").is_dir() + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_selfsl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_selfsl" + otx_train_testing(template, tmp_dir_path, otx_dir, args_selfsl) + + @e2e_pytest_component + @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_multi_gpu_train_selfsl(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "segmentation/test_multi_gpu_selfsl" + args_selfsl_multigpu = copy.deepcopy(args_selfsl) + args_selfsl_multigpu["--gpus"] = "0,1" + otx_train_testing(template, tmp_dir_path, otx_dir, args_selfsl_multigpu) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + @pytest.mark.parametrize("bs_adapt_type", ["Safe", "Full"]) + def test_otx_train_auto_adapt_batch_size(self, template, tmp_dir_path, bs_adapt_type): + adapting_bs_args = copy.deepcopy(args) + adapting_bs_args["train_params"].extend(["--learning_parameters.auto_adapt_batch_size", bs_adapt_type]) + tmp_dir_path = tmp_dir_path / f"segmentation_auto_adapt_{bs_adapt_type}_batch_size" + otx_train_testing(template, tmp_dir_path, otx_dir, adapting_bs_args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) + def test_otx_train_auto_adapt_num_workers(self, template, tmp_dir_path): + adapting_num_workers_args = copy.deepcopy(args) + adapting_num_workers_args["train_params"].extend(["--learning_parameters.auto_num_workers", "True"]) + tmp_dir_path = tmp_dir_path / "segmentation_auto_adapt_num_workers" + otx_train_testing(template, tmp_dir_path, otx_dir, adapting_num_workers_args) diff --git a/tests/integration/cli/test_auto_configuration.py b/tests/integration/cli/test_auto_configuration.py deleted file mode 100644 index 069e54ca2c6..00000000000 --- a/tests/integration/cli/test_auto_configuration.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - - -from pathlib import Path - -import pytest -from otx.core.types.task import OTXTaskType -from otx.engine.utils.auto_configurator import DEFAULT_CONFIG_PER_TASK - -from tests.integration.cli.utils import run_main - - -@pytest.mark.parametrize("task", pytest.TASK_LIST) -def test_otx_cli_auto_configuration( - task: OTXTaskType, - tmp_path: Path, - fxt_accelerator: str, - fxt_target_dataset_per_task: dict, - fxt_cli_override_command_per_task: dict, - fxt_open_subprocess: bool, -) -> None: - """Test the OTX auto configuration with CLI. - - Args: - task (OTXTaskType): The task to be performed. - tmp_path (Path): The temporary path for storing outputs. - fxt_accelerator (str): The accelerator to be used. - fxt_target_dataset_per_task (dict): The target dataset per task. - - Returns: - None - """ - if task not in DEFAULT_CONFIG_PER_TASK: - pytest.skip(f"Task {task} is not supported in the auto-configuration.") - if task.lower() in ("action_classification"): - pytest.xfail(reason="xFail until this root cause is resolved on the Datumaro side.") - tmp_path_train = tmp_path / f"otx_auto_train_{task}" - command_cfg = [ - "otx", - "train", - "--data_root", - fxt_target_dataset_per_task[task.lower()], - "--task", - task.upper(), - "--work_dir", - str(tmp_path_train / "outputs"), - "--engine.device", - fxt_accelerator, - "--max_epochs", - "1" if task.lower() in ("zero_shot_visual_prompting") else "2", - *fxt_cli_override_command_per_task[task.lower()], - ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - # Currently, a simple output check - outputs_dir = tmp_path_train / "outputs" - latest_dir = max( - (p for p in outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - assert latest_dir.exists() - assert (latest_dir / "configs.yaml").exists() - assert (latest_dir / "csv").exists() - assert (latest_dir / "checkpoints").exists() - ckpt_files = list((latest_dir / "checkpoints").glob(pattern="epoch_*.ckpt")) - assert len(ckpt_files) > 0 diff --git a/tests/integration/cli/test_cli.py b/tests/integration/cli/test_cli.py index 6c82fb5c6a4..0ab53438085 100644 --- a/tests/integration/cli/test_cli.py +++ b/tests/integration/cli/test_cli.py @@ -1,501 +1,309 @@ -# Copyright (C) 2023 Intel Corporation +"""Tests for OTX CLI commands""" +# Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +# +import os +import sys +from unittest.mock import patch -from pathlib import Path - -import numpy as np import pytest -import yaml -from otx.engine.utils.auto_configurator import DEFAULT_CONFIG_PER_TASK - -from tests.integration.cli.utils import run_main - -@pytest.mark.parametrize( - "recipe", - pytest.RECIPE_LIST, - ids=lambda x: "/".join(Path(x).parts[-2:]), +from otx.cli.tools import cli +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + check_run, + otx_build_auto_config, + otx_build_backbone_testing, + otx_build_testing, + otx_find_testing, + otx_train_auto_config, ) -def test_otx_e2e( - recipe: str, - tmp_path: Path, - fxt_accelerator: str, - fxt_target_dataset_per_task: dict, - fxt_cli_override_command_per_task: dict, - fxt_open_subprocess: bool, -) -> None: - """ - Test OTX CLI e2e commands. - - - 'otx train' with 2 epochs training - - 'otx test' with output checkpoint from 'otx train' - - 'otx export' with output checkpoint from 'otx train' - - 'otx test' with the exported to ONNX/IR model - - Args: - recipe (str): The recipe to use for training. (eg. 'classification/otx_mobilenet_v3_large.yaml') - tmp_path (Path): The temporary path for storing the training outputs. - - Returns: - None - """ - task = recipe.split("/")[-2] - model_name = recipe.split("/")[-1].split(".")[0] - if task in ("action_classification"): - pytest.xfail(reason="xFail until this root cause is resolved on the Datumaro side.") - - # 1) otx train - tmp_path_train = tmp_path / f"otx_train_{model_name}" - command_cfg = [ - "otx", - "train", - "--config", - recipe, - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_train / "outputs"), - "--engine.device", - fxt_accelerator, - "--max_epochs", - "1" if task in ("zero_shot_visual_prompting") else "2", - *fxt_cli_override_command_per_task[task], - ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - outputs_dir = tmp_path_train / "outputs" - latest_dir = max( - (p for p in outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - # Currently, a simple output check - assert latest_dir.exists() - assert (latest_dir / "configs.yaml").exists() - # Check Configs file - with (latest_dir / "configs.yaml").open() as file: - train_output_config = yaml.safe_load(file) - assert "model" in train_output_config - assert "data" in train_output_config - assert "engine" in train_output_config - assert (latest_dir / "csv").exists() - assert (latest_dir / "checkpoints").exists() - ckpt_files = list((latest_dir / "checkpoints").glob(pattern="epoch_*.ckpt")) - assert len(ckpt_files) > 0 - - # 2) otx test - tmp_path_test = tmp_path / f"otx_test_{model_name}" - command_cfg = [ - "otx", - "test", - "--config", - recipe, - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_test / "outputs"), - "--engine.device", - fxt_accelerator, - *fxt_cli_override_command_per_task[task], - "--checkpoint", - str(ckpt_files[-1]), - ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - outputs_dir = tmp_path_test / "outputs" - latest_dir = max( - (p for p in outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - assert latest_dir.exists() - assert (latest_dir / "csv").exists() - - # 3) otx export - if any( - task_name in recipe - for task_name in [ - "h_label_cls", - "detection", - "dino_v2", - "instance_segmentation", - "action", - "anomaly_classification", - "anomaly_detection", - "anomaly_segmentation", - ] - ): - return - - if task in ("visual_prompting", "zero_shot_visual_prompting"): - format_to_file = { - "ONNX": "exported_model_decoder.onnx", - "OPENVINO": "exported_model_decoder.xml", - # TODO (sungchul): EXPORTABLE_CODE will be supported # noqa: TD003 - } - else: - format_to_file = { - "ONNX": "exported_model.onnx", - "OPENVINO": "exported_model.xml", - "EXPORTABLE_CODE": "exportable_code.zip", - } - - tmp_path_test = tmp_path / f"otx_test_{model_name}" - for fmt in format_to_file: - command_cfg = [ - "otx", - "export", - "--config", - recipe, - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_test / "outputs" / fmt), - *fxt_cli_override_command_per_task[task], - "--checkpoint", - str(ckpt_files[-1]), - "--export_format", - f"{fmt}", - ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - outputs_dir = tmp_path_test / "outputs" / fmt - latest_dir = max( - (p for p in outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, +otx_dir = os.getcwd() + + +build_backbone_args = [ + ("CLASSIFICATION", "torchvision.mobilenet_v3_large"), + ("CLASSIFICATION", "mmcls.MMOVBackbone"), + ("DETECTION", "torchvision.mobilenet_v3_large"), + ("INSTANCE_SEGMENTATION", "torchvision.mobilenet_v3_large"), + ("SEGMENTATION", "torchvision.mobilenet_v3_large"), +] +build_backbone_args_ids = [f"{task}_{backbone}" for task, backbone in build_backbone_args] + +rebuild_args = { + "classification": { + "default": "EfficientNet-B0", + "--task": "classification", + "--model": "MobileNet-V3-large-1x", + "--train-type": "Semisupervised", + }, + "detection": { + "default": "MobileNetV2-ATSS", + "--task": "detection", + "--model": "SSD", + "--train-type": "Semisupervised", + }, +} + + +class TestToolsOTXCLI: + @e2e_pytest_component + def test_otx_find(self): + otx_find_testing() + + @e2e_pytest_component + @pytest.mark.parametrize("build_backbone_args", build_backbone_args, ids=build_backbone_args_ids) + def test_otx_backbone_build(self, tmp_dir_path, build_backbone_args): + tmp_dir_path = tmp_dir_path / build_backbone_args[0] / build_backbone_args[1] + otx_build_backbone_testing(tmp_dir_path, build_backbone_args) + + @e2e_pytest_component + @pytest.mark.parametrize("case", rebuild_args.keys()) + def test_otx_build_rebuild(self, tmp_dir_path, case): + tmp_dir_path = tmp_dir_path / "test_rebuild" / case + # 1. Only Task + build_arg = {"--task": rebuild_args[case]["--task"]} + expected = {"model": rebuild_args[case]["default"], "train_type": "Incremental"} + otx_build_testing(tmp_dir_path, build_arg, expected=expected) + # 2. Change Model + build_arg = {"--model": rebuild_args[case]["--model"]} + expected = {"model": rebuild_args[case]["--model"], "train_type": "Incremental"} + otx_build_testing(tmp_dir_path, build_arg, expected=expected) + # 3. Change Train-type + build_arg = {"--train-type": rebuild_args[case]["--train-type"]} + expected = {"model": rebuild_args[case]["--model"], "train_type": rebuild_args[case]["--train-type"]} + otx_build_testing(tmp_dir_path, build_arg, expected=expected) + # 4. Change to Default + build_arg = {"--model": rebuild_args[case]["default"], "--train-type": "Incremental"} + expected = {"model": rebuild_args[case]["default"], "train_type": "Incremental"} + otx_build_testing(tmp_dir_path, build_arg, expected=expected) + + +build_auto_config_args = { + "classification": {"--train-data-roots": "tests/assets/classification_dataset"}, + "classification_with_task": { + "--task": "classification", + "--train-data-roots": "tests/assets/classification_dataset", + }, + "detection": {"--train-data-roots": "tests/assets/car_tree_bug"}, + "detection_with_task": {"--task": "detection", "--train-data-roots": "tests/assets/car_tree_bug"}, +} + + +class TestToolsOTXBuildAutoConfig: + @e2e_pytest_component + @pytest.mark.parametrize("case", build_auto_config_args.keys()) + def test_otx_build_with_autosplit(self, case, tmp_dir_path): + otx_dir = os.getcwd() + tmp_dir_path = tmp_dir_path / "test_build_auto_config" / case + otx_build_auto_config(root=tmp_dir_path, otx_dir=otx_dir, args=build_auto_config_args[case]) + + +train_auto_config_args = { + "classification": {"--train-data-roots": "tests/assets/classification_dataset"}, + "classification_with_template": { + "template": "src/otx/algorithms/classification/configs/mobilenet_v3_large_1_cls_incr/template.yaml", + "--train-data-roots": "tests/assets/classification_dataset", + }, + "detection": {"--train-data-roots": "tests/assets/car_tree_bug"}, + "detection_with_template": { + "template": "src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml", + "--train-data-roots": "tests/assets/car_tree_bug", + }, +} + +train_params = [ + "params", + "--learning_parameters.num_iters", + "1", +] + + +class TestToolsOTXTrainAutoConfig: + @e2e_pytest_component + @pytest.mark.parametrize("case", train_auto_config_args.keys()) + def test_otx_train(self, case, tmp_dir_path): + otx_dir = os.getcwd() + tmp_dir_path = tmp_dir_path / "test_train_auto_config" / case + train_auto_config_args[case]["train_params"] = train_params + otx_train_auto_config(root=tmp_dir_path, otx_dir=otx_dir, args=train_auto_config_args[case]) + # check output (use --workspace & --output) + output_path = os.path.join(tmp_dir_path, "otx-workspace") + assert os.path.exists(os.path.join(output_path, "outputs")) + assert os.path.exists(os.path.join(output_path, "outputs", "latest_trained_model")) + assert os.path.exists(os.path.join(output_path, "outputs", "latest_trained_model", "models")) + assert os.path.exists(os.path.join(output_path, "outputs", "latest_trained_model", "models", "weights.pth")) + assert os.path.exists( + os.path.join(output_path, "outputs", "latest_trained_model", "models", "label_schema.json") ) - assert latest_dir.exists() - assert (latest_dir / f"{format_to_file[fmt]}").exists() - - # 4) infer of the exported models - ov_output_dir = tmp_path_test / "outputs" / "OPENVINO" - ov_latest_dir = max( - (p for p in ov_output_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - exported_model_path = str(ov_latest_dir / "exported_model.xml") - - command_cfg = [ - "otx", - "test", - "--config", - recipe, - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_test / "outputs"), - "--engine.device", - "cpu", - *fxt_cli_override_command_per_task[task], - "--checkpoint", - exported_model_path, - ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - outputs_dir = tmp_path_test / "outputs" - latest_dir = max( - (p for p in outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - assert latest_dir.exists() - - # 5) otx export with XAI - if ("_cls" not in task) and (task not in ["detection", "instance_segmentation"]): - return # Supported only for classification, detection and instance segmentation task. - - if "dino" in model_name or "rtmdet_inst_tiny" in model_name: - return # DINO and Rtmdet_tiny are not supported. - - format_to_file = { - "ONNX": "exported_model.onnx", - "OPENVINO": "exported_model.xml", - "EXPORTABLE_CODE": "exportable_code.zip", - } - - tmp_path_test = tmp_path / f"otx_export_xai_{model_name}" - for fmt in format_to_file: - command_cfg = [ + assert os.path.exists(os.path.join(output_path, "models")) + assert os.path.exists(os.path.join(output_path, "models", "weights.pth")) + + @e2e_pytest_component + def test_otx_train_wo_output_args(self, tmp_dir_path): + otx_dir = os.getcwd() + case = list(train_auto_config_args.keys())[0] + tmp_dir_path = tmp_dir_path / "test_train_auto_config_wo_output" / case + train_auto_config_args[case]["train_params"] = train_params + otx_train_auto_config(root=tmp_dir_path, otx_dir=otx_dir, args=train_auto_config_args[case], use_output=False) + + # check output (without --output -> Default outputs) + output_path = os.path.join(tmp_dir_path, "otx-workspace", "outputs") + assert os.path.exists(output_path) + file_list = sorted(os.listdir(output_path)) + assert len(file_list) == 2 + assert os.path.exists(os.path.join(output_path, "latest_trained_model")) + assert os.path.exists(os.path.join(output_path, "latest_trained_model", "models")) + assert os.path.exists(os.path.join(output_path, "latest_trained_model", "models", "weights.pth")) + assert os.path.exists(os.path.join(output_path, "latest_trained_model", "models", "label_schema.json")) + file_list.remove("latest_trained_model") + assert os.path.exists(os.path.join(output_path, file_list[-1])) + assert os.path.exists(os.path.join(output_path, file_list[-1], "models")) + assert os.path.exists(os.path.join(output_path, file_list[-1], "models", "weights.pth")) + + @e2e_pytest_component + def test_otx_export_wo_output_args(self, tmp_dir_path): + case = list(train_auto_config_args.keys())[0] + tmp_dir_path = tmp_dir_path / "test_train_auto_config_wo_output" / case + workspace_path = os.path.join(tmp_dir_path, "otx-workspace") + command_line = [ "otx", "export", - "--config", - recipe, - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_test / "outputs" / fmt), - *fxt_cli_override_command_per_task[task], - "--checkpoint", - str(ckpt_files[-1]), - "--export_format", - f"{fmt}", - "--explain", - "True", + "--workspace", + os.path.join(tmp_dir_path, "otx-workspace"), ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - fmt_dir = tmp_path_test / "outputs" / fmt - assert fmt_dir.exists() - fmt_latest_dir = max( - (p for p in fmt_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - assert (fmt_latest_dir / f"{format_to_file[fmt]}").exists() - - -@pytest.mark.parametrize( - "recipe", - pytest.RECIPE_LIST, - ids=lambda x: "/".join(Path(x).parts[-2:]), -) -def test_otx_explain_e2e( - recipe: str, - tmp_path: Path, - fxt_accelerator: str, - fxt_target_dataset_per_task: dict, - fxt_cli_override_command_per_task: dict, - fxt_open_subprocess: bool, -) -> None: - """ - Test OTX CLI explain e2e command. - - Args: - recipe (str): The recipe to use for training. (eg. 'classification/otx_mobilenet_v3_large.yaml') - tmp_path (Path): The temporary path for storing the training outputs. - - Returns: - None - """ - if "tile" in recipe: - pytest.skip("Explain is not supported for tiling yet.") - - import cv2 - - task = recipe.split("/")[-2] - model_name = recipe.split("/")[-1].split(".")[0] - - if ("_cls" not in task) and (task not in ["detection", "instance_segmentation"]): - pytest.skip("Supported only for classification, detection and instance segmentation task.") - - if "dino" in model_name or "rtmdet_inst_tiny" in model_name: - pytest.skip("DINO and Rtmdet_tiny are not supported.") - - # otx explain - tmp_path_explain = tmp_path / f"otx_explain_{model_name}" - command_cfg = [ - "otx", - "explain", - "--config", - recipe, - "--model.num_classes", - "1000", - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_explain / "outputs"), - "--engine.device", - fxt_accelerator, - "--seed", - "0", - "--deterministic", - "True", - "--dump", - "True", - *fxt_cli_override_command_per_task[task], - ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - outputs_dir = tmp_path_explain / "outputs" - latest_dir = max( - (p for p in outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - assert (latest_dir / "saliency_maps").exists() - saliency_maps = sorted((latest_dir / "saliency_maps").glob(pattern="*.png")) - sal_map = cv2.imread(str(saliency_maps[0])) - assert sal_map.shape[0] > 0 - assert sal_map.shape[1] > 0 - - sal_diff_thresh = 3 - reference_sal_vals = { - # Classification - "multi_label_cls_efficientnet_v2_light": ( - np.array([66, 97, 84, 33, 42, 79, 0], dtype=np.uint8), - "Slide6_class_0_saliency_map.png", - ), - "h_label_cls_efficientnet_v2_light": ( - np.array([43, 84, 61, 5, 54, 31, 57], dtype=np.uint8), - "5_class_0_saliency_map.png", - ), - # Detection - "detection_yolox_tiny": ( - np.array([111, 163, 141, 141, 146, 147, 158, 169, 184, 193], dtype=np.uint8), - "Slide3_class_0_saliency_map.png", - ), - "detection_ssd_mobilenetv2": ( - np.array([135, 80, 74, 34, 27, 32, 47, 42, 32, 34], dtype=np.uint8), - "Slide3_class_0_saliency_map.png", - ), - "detection_atss_mobilenetv2": ( - np.array([22, 62, 64, 0, 27, 60, 59, 53, 37, 45], dtype=np.uint8), - "Slide3_class_0_saliency_map.png", - ), - # Instance Segmentation - "instance_segmentation_maskrcnn_efficientnetb2b": ( - np.array([54, 54, 54, 54, 0, 0, 0, 54, 0, 0], dtype=np.uint8), - "Slide3_class_0_saliency_map.png", - ), - } - test_case_name = task + "_" + model_name - if test_case_name in reference_sal_vals: - actual_sal_vals = cv2.imread(str(latest_dir / "saliency_maps" / reference_sal_vals[test_case_name][1])) - if test_case_name == "instance_segmentation_maskrcnn_efficientnetb2b": - # Take corner values due to map sparsity of InstSeg - actual_sal_vals = (actual_sal_vals[-10:, -1, -1]).astype(np.uint16) - else: - actual_sal_vals = (actual_sal_vals[:10, 0, 0]).astype(np.uint16) - ref_sal_vals = reference_sal_vals[test_case_name][0] - assert np.max(np.abs(actual_sal_vals - ref_sal_vals) <= sal_diff_thresh) - - -# @pytest.mark.skipif(len(pytest.RECIPE_OV_LIST) < 1, reason="No OV recipe found.") -@pytest.mark.parametrize( - "ov_recipe", - pytest.RECIPE_OV_LIST, -) -def test_otx_ov_test( - ov_recipe: str, - tmp_path: Path, - fxt_target_dataset_per_task: dict, - fxt_open_subprocess: bool, -) -> None: - """ - Test OTX CLI e2e commands. - - - 'otx test' with OV model - - Args: - recipe (str): The OV recipe to use for testing. (eg. 'classification/openvino_model.yaml') - tmp_path (Path): The temporary path for storing the testing outputs. - - Returns: - None - """ - task = ov_recipe.split("/")[-2] - model_name = ov_recipe.split("/")[-1].split(".")[0] - - if task in [ - "multi_label_cls", - "instance_segmentation", - "h_label_cls", - "visual_prompting", - "zero_shot_visual_prompting", - "anomaly_classification", - "anomaly_detection", - "anomaly_segmentation", - ]: - # OMZ doesn't have proper model for Pytorch MaskRCNN interface - # TODO(Kirill): Need to change this test when export enabled #noqa: TD003 - pytest.skip("OMZ doesn't have proper model for these types of tasks.") - - if task in ["action_classification"]: - pytest.skip("Action classification test will be enabled after solving Datumaro issue.") - - # otx test - tmp_path_test = tmp_path / f"otx_test_{task}_{model_name}" - command_cfg = [ - "otx", - "test", - "--config", - ov_recipe, - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_test / "outputs"), - "--engine.device", - "cpu", - "--disable-infer-num-classes", - ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - outputs_dir = tmp_path_test / "outputs" - latest_dir = max( - (p for p in outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - assert latest_dir.exists() - assert (latest_dir / "csv").exists() - metric_result = list((latest_dir / "csv").glob(pattern="**/metrics.csv")) - assert len(metric_result) > 0 - - -@pytest.mark.parametrize("task", pytest.TASK_LIST) -def test_otx_hpo_e2e( - task: str, - tmp_path: Path, - fxt_accelerator: str, - fxt_target_dataset_per_task: dict, - fxt_cli_override_command_per_task: dict, - fxt_open_subprocess: bool, -) -> None: - """ - Test HPO e2e commands with default template of each task. - - Args: - task (OTXTaskType): The task to run HPO with. - tmp_path (Path): The temporary path for storing the training outputs. - - Returns: - None - """ - if task in ("action_classification"): - pytest.xfail(reason="xFail until this root cause is resolved on the Datumaro side.") - if task not in DEFAULT_CONFIG_PER_TASK: - pytest.skip(f"Task {task} is not supported in the auto-configuration.") - - task = task.lower() - tmp_path_hpo = tmp_path / f"otx_hpo_{task}" - tmp_path_hpo.mkdir(parents=True) - - command_cfg = [ - "otx", - "train", - "--task", - task.upper(), - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_hpo), - "--engine.device", - fxt_accelerator, - "--max_epochs", - "1" if task in ("zero_shot_visual_prompting") else "2", - "--run_hpo", - "true", - "--hpo_config.expected_time_ratio", - "2", - *fxt_cli_override_command_per_task[task], - ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - # zero_shot_visual_prompting doesn't support HPO. Check just there is no error. - if task in ("zero_shot_visual_prompting"): - return - - latest_dir = max( - (p for p in tmp_path_hpo.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - hpo_work_dor = latest_dir / "hpo" - assert hpo_work_dor.exists() - # Anomaly doesn't do validation. Check just there is no error. - if task.startswith("anomaly"): - return - - assert len([val for val in hpo_work_dor.rglob("*.json") if str(val.stem).isdigit()]) == 2 + check_run(command_line) + + # check output (without --output -> Default outputs) + output_path = os.path.join(workspace_path, "outputs") + assert os.path.exists(output_path) + file_list = sorted(os.listdir(output_path)) + assert len(file_list) == 3 + file_list.remove("latest_trained_model") + assert os.path.exists(os.path.join(output_path, file_list[-1])) + assert os.path.exists(os.path.join(output_path, file_list[-1], "openvino")) + assert os.path.exists(os.path.join(output_path, file_list[-1], "openvino", "openvino.xml")) + assert os.path.exists(os.path.join(output_path, file_list[-1], "openvino", "openvino.bin")) + assert os.path.exists(os.path.join(output_path, file_list[-1], "openvino", "label_schema.json")) + + @e2e_pytest_component + def test_otx_optimize_wo_output_args(self, tmp_dir_path): + case = list(train_auto_config_args.keys())[0] + tmp_dir_path = tmp_dir_path / "test_train_auto_config_wo_output" / case + workspace_path = os.path.join(tmp_dir_path, "otx-workspace") + command_line = [ + "otx", + "optimize", + "--workspace", + os.path.join(tmp_dir_path, "otx-workspace"), + ] + check_run(command_line) + + # check output (without --output -> Default outputs) + output_path = os.path.join(workspace_path, "outputs") + assert os.path.exists(output_path) + file_list = sorted(os.listdir(output_path)) + assert len(file_list) == 4 + file_list.remove("latest_trained_model") + assert os.path.exists(os.path.join(output_path, file_list[-1])) + assert os.path.exists(os.path.join(output_path, file_list[-1], "nncf")) + assert os.path.exists(os.path.join(output_path, file_list[-1], "nncf", "weights.pth")) + assert os.path.exists(os.path.join(output_path, file_list[-1], "nncf", "label_schema.json")) + + @e2e_pytest_component + def test_otx_train_wo_workspace_and_output_args(self, tmp_dir_path): + otx_dir = os.getcwd() + case = list(train_auto_config_args.keys())[0] + tmp_dir_path = tmp_dir_path / "test_otx_train_wo_workspace_and_output_args" + tmp_dir_path.mkdir(exist_ok=True) + expected_workspace_path = os.path.join(tmp_dir_path, f"otx-workspace-{case.upper()}") + command_line = [ + "otx", + "train", + ] + args = train_auto_config_args[case] + for option, value in args.items(): + if option in ["--train-data-roots", "--val-data-roots"]: + command_line.extend([option, f"{os.path.join(otx_dir, value)}"]) + command_line.extend(train_params) + check_run(command_line, cwd=tmp_dir_path) + + # check output (without --output -> Default outputs) + assert os.path.exists(expected_workspace_path) + expected_output_path = os.path.join(expected_workspace_path, "outputs") + assert os.path.exists(expected_output_path) + file_list = sorted(os.listdir(expected_output_path)) + assert len(file_list) == 2 + assert os.path.exists(os.path.join(expected_output_path, "latest_trained_model")) + assert os.path.exists(os.path.join(expected_output_path, "latest_trained_model", "models")) + assert os.path.exists(os.path.join(expected_output_path, "latest_trained_model", "models", "weights.pth")) + assert os.path.exists(os.path.join(expected_output_path, "latest_trained_model", "models", "label_schema.json")) + file_list.remove("latest_trained_model") + assert os.path.exists(os.path.join(expected_output_path, file_list[-1])) + assert os.path.exists(os.path.join(expected_output_path, file_list[-1], "models")) + assert os.path.exists(os.path.join(expected_output_path, file_list[-1], "models", "weights.pth")) + assert os.path.exists(os.path.join(expected_output_path, file_list[-1], "models", "label_schema.json")) + + +class TestTelemetryIntegration: + _CMDS = ["demo", "build", "deploy", "eval", "explain", "export", "find", "optimize", "train"] + + @e2e_pytest_component + @patch("otx.cli.utils.telemetry.init_telemetry_session", return_value=None) + @patch("otx.cli.utils.telemetry.close_telemetry_session", return_value=None) + @patch("otx.cli.utils.telemetry.send_version", return_value=None) + @patch("otx.cli.utils.telemetry.send_cmd_results", return_value=None) + def test_tm_integration_exit_0( + self, + mock_send_cmd, + mock_send_version, + mock_close_tm, + mock_init_tm, + ): + backup_argv = sys.argv + for cmd in self._CMDS: + sys.argv = ["otx", cmd] + with patch(f"otx.cli.tools.cli.otx_{cmd}", return_value=None) as mock_cmd: + ret = cli.main() + + assert ret == 0 + mock_cmd.assert_called_once() + mock_init_tm.assert_called_once() + mock_close_tm.assert_called_once() + mock_send_cmd.assert_called_once_with(None, cmd, {"retcode": 0}) + # reset mock state + mock_init_tm.reset_mock() + mock_close_tm.reset_mock() + mock_send_cmd.reset_mock() + sys.argv = backup_argv + + @e2e_pytest_component + @patch("otx.cli.utils.telemetry.init_telemetry_session", return_value=None) + @patch("otx.cli.utils.telemetry.close_telemetry_session", return_value=None) + @patch("otx.cli.utils.telemetry.send_version", return_value=None) + @patch("otx.cli.utils.telemetry.send_cmd_results", return_value=None) + def test_tm_integration_exit_exception( + self, + mock_send_cmd, + mock_send_version, + mock_close_tm, + mock_init_tm, + ): + backup_argv = sys.argv + for cmd in self._CMDS: + with patch(f"otx.cli.tools.cli.otx_{cmd}", side_effect=Exception()): + sys.argv = ["otx", cmd] + with pytest.raises(Exception) as e: + cli.main() + + assert e.type == Exception, f"{e}" + mock_init_tm.assert_called_once() + mock_close_tm.assert_called_once() + mock_send_cmd.assert_called_once_with(None, cmd, {"retcode": -1, "exception": repr(Exception())}) + # reset mock state + mock_init_tm.reset_mock() + mock_close_tm.reset_mock() + mock_send_cmd.reset_mock() + sys.argv = backup_argv diff --git a/tests/integration/cli/test_export_inference.py b/tests/integration/cli/test_export_inference.py deleted file mode 100644 index 757feeeb720..00000000000 --- a/tests/integration/cli/test_export_inference.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - - -import logging -from pathlib import Path - -import pandas as pd -import pytest - -from tests.integration.cli.utils import run_main - -log = logging.getLogger(__name__) - - -def _check_relative_metric_diff(ref: float, value: float, eps: float) -> None: - assert ref >= 0 - assert value >= 0 - assert eps >= 0 - - if value < ref: - avg = max(0.5 * (ref + value), 1e-9) - diff = abs(value - ref) - - assert diff / avg <= eps, f"Relative difference exceeded {eps} threshold. Absolute difference: {diff}" - - -@pytest.fixture(scope="module", autouse=True) -def fxt_local_seed() -> int: - """The number of repetition for each test case. - - The random seed will be set for [0, fxt_num_repeat - 1]. Default is one. - """ - selected_seed = 7 - msg = f"seed : {selected_seed}" - log.info(msg) - return selected_seed - - -TASK_NAME_TO_MAIN_METRIC_NAME = { - "semantic_segmentation": "test/Dice", - "multi_label_cls": "test/accuracy", - "multi_class_cls": "test/accuracy", - "h_label_cls": "test/accuracy", - "detection": "test/map_50", - "instance_segmentation": "test/map_50", - "visual_prompting": "test/Dice", - "zero_shot_visual_prompting": "test/F1", -} - - -@pytest.mark.parametrize( - "recipe", - pytest.RECIPE_LIST, - ids=lambda x: "/".join(Path(x).parts[-2:]), -) -def test_otx_export_infer( - recipe: str, - tmp_path: Path, - fxt_local_seed: int, - fxt_target_dataset_per_task: dict, - fxt_cli_override_command_per_task: dict, - fxt_accelerator: str, - fxt_open_subprocess: bool, - request: pytest.FixtureRequest, -) -> None: - """ - Test OTX CLI e2e commands. - - - 'otx train' with 2 epochs training - - 'otx test' with output checkpoint from 'otx train' - - 'otx export' with output checkpoint from 'otx train' - - 'otx test' with the exported to ONNX/IR model model - - compare accuracy of the exported model vs the original accuracy - - Args: - recipe (str): The recipe to use for training. (eg. 'classification/otx_mobilenet_v3_large.yaml') - tmp_path (Path): The temporary path for storing the training outputs. - - Returns: - None - """ - task = recipe.split("/")[-2] - - if task not in TASK_NAME_TO_MAIN_METRIC_NAME: - pytest.skip(f"Inference pipeline for {recipe} is not implemented") - elif (task == "detection" and "atss_mobilenetv2" not in recipe) or ( - task == "instance_segmentation" and "maskrcnn_efficientnetb2b" not in recipe - ): - pytest.skip("To prevent memory bug from aborting integration test, test single model per task.") - elif "tile" in recipe: - pytest.skip("Exporting models with tiling isn't supported yet.") - - model_name = recipe.split("/")[-1].split(".")[0] - # 1) otx train - tmp_path_train = tmp_path / f"otx_train_{model_name}" - command_cfg = [ - "otx", - "train", - "--config", - recipe, - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_train / "outputs"), - "--engine.device", - fxt_accelerator, - "--max_epochs", - "1" if task in ("zero_shot_visual_prompting") else "2", - "--seed", - f"{fxt_local_seed}", - "--deterministic", - "warn", - *fxt_cli_override_command_per_task[task], - ] - # H-Label-CLS need to add --metric - if task in ("h_label_cls"): - command_cfg.extend(["--metric.num_multiclass_heads", "2"]) - command_cfg.extend(["--metric.num_multilabel_classes", "3"]) - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - outputs_dir = tmp_path_train / "outputs" - latest_dir = max( - (p for p in outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - ckpt_files = list((latest_dir / "checkpoints").glob(pattern="epoch_*.ckpt")) - assert len(ckpt_files) > 0 - - # 2) otx test - def run_cli_test(test_recipe: str, checkpoint_path: str, work_dir: Path, device: str = fxt_accelerator) -> Path: - tmp_path_test = tmp_path / f"otx_test_{model_name}" - command_cfg = [ - "otx", - "test", - "--config", - test_recipe, - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_test / work_dir), - "--engine.device", - device, - *fxt_cli_override_command_per_task[task], - "--checkpoint", - checkpoint_path, - ] - # H-Label-CLS need to add --metric - if task in ("h_label_cls") and not test_recipe.endswith("openvino_model.yaml"): - command_cfg.extend(["--metric.num_multiclass_heads", "2"]) - command_cfg.extend(["--metric.num_multilabel_classes", "3"]) - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - return tmp_path_test - - tmp_path_test = run_cli_test(recipe, str(ckpt_files[-1]), Path("outputs") / "torch") - - assert (tmp_path_test / "outputs").exists() - - # 3) otx export - format_to_ext = {"OPENVINO": "xml"} # [TODO](@Vlad): extend to "ONNX": "onnx" - - tmp_path_test = tmp_path / f"otx_test_{model_name}" - for fmt in format_to_ext: - command_cfg = [ - "otx", - "export", - "--config", - recipe, - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_test / "outputs"), - *fxt_cli_override_command_per_task[task], - "--checkpoint", - str(ckpt_files[-1]), - "--export_format", - f"{fmt}", - ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - outputs_dir = tmp_path_test / "outputs" - latest_dir = max( - (p for p in outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - assert latest_dir.exists() - if task in ("visual_prompting", "zero_shot_visual_prompting"): - assert (latest_dir / f"exported_model_image_encoder.{format_to_ext[fmt]}").exists() - assert (latest_dir / f"exported_model_decoder.{format_to_ext[fmt]}").exists() - else: - assert (latest_dir / f"exported_model.{format_to_ext[fmt]}").exists() - - # 4) infer of the exported models - task = recipe.split("/")[-2] - tmp_path_test = tmp_path / f"otx_test_{model_name}" - if "_cls" in recipe: - export_test_recipe = f"src/otx/recipe/classification/{task}/openvino_model.yaml" - else: - export_test_recipe = f"src/otx/recipe/{task}/openvino_model.yaml" - - if task in ("visual_prompting", "zero_shot_visual_prompting"): - exported_model_path = str(latest_dir / "exported_model_decoder.xml") - else: - exported_model_path = str(latest_dir / "exported_model.xml") - - tmp_path_test = run_cli_test(export_test_recipe, exported_model_path, Path("outputs") / "openvino", "cpu") - assert (tmp_path_test / "outputs").exists() - - # 5) test optimize - if task in ("visual_prompting", "zero_shot_visual_prompting"): - pytest.xfail( - "Optimize for visual prompting and zero shot visual prompting yields segmentation fault after optimize.", - ) - - command_cfg = [ - "otx", - "optimize", - "--config", - export_test_recipe, - "--data_root", - fxt_target_dataset_per_task[task], - "--work_dir", - str(tmp_path_test / "outputs"), - "--engine.device", - "cpu", - *fxt_cli_override_command_per_task[task], - "--checkpoint", - exported_model_path, - ] - - run_main(command_cfg=command_cfg, open_subprocess=fxt_open_subprocess) - - outputs_dir = tmp_path_test / "outputs" - latest_dir = max( - (p for p in outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - assert latest_dir.exists() - if task in ("visual_prompting", "zero_shot_visual_prompting"): - exported_model_path = str(latest_dir / "optimized_model_decoder.xml") - else: - exported_model_path = str(latest_dir / "optimized_model.xml") - - # 6) test optimized model - tmp_path_test = run_cli_test(export_test_recipe, exported_model_path, Path("outputs") / "nncf_ptq", "cpu") - torch_outputs_dir = tmp_path_test / "outputs" / "torch" - torch_latest_dir = max( - (p for p in torch_outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - openvino_outputs_dir = tmp_path_test / "outputs" / "openvino" - openvino_latest_dir = max( - (p for p in openvino_outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - nncf_ptq_outputs_dir = tmp_path_test / "outputs" / "nncf_ptq" - nncf_ptq_latest_dir = max( - (p for p in nncf_ptq_outputs_dir.iterdir() if p.is_dir() and p.name != ".latest"), - key=lambda p: p.stat().st_mtime, - ) - assert nncf_ptq_latest_dir.exists() - - df_torch = pd.read_csv(next(torch_latest_dir.glob("**/metrics.csv"))) - df_openvino = pd.read_csv(next(openvino_latest_dir.glob("**/metrics.csv"))) - df_nncf_ptq = pd.read_csv(next(nncf_ptq_latest_dir.glob("**/metrics.csv"))) - - metric_name = TASK_NAME_TO_MAIN_METRIC_NAME[task] - - assert metric_name in df_torch.columns - assert metric_name in df_openvino.columns - assert metric_name in df_nncf_ptq.columns - - torch_acc = df_torch[metric_name].item() - ov_acc = df_openvino[metric_name].item() - ptq_acc = df_nncf_ptq[metric_name].item() # noqa: F841 - - msg = f"Recipe: {recipe}, (torch_accuracy, ov_accuracy): {torch_acc} , {ov_acc}" - log.info(msg) - - # Not compare w/ instance segmentation and visual prompting tasks because training isn't able to be deterministic, which can lead to unstable test result. - if "maskrcnn_efficientnetb2b" in recipe or task in ("visual_prompting", "zero_shot_visual_prompting"): - return - threshold = 0.2 - if "multi_label_cls/efficientnet_b0_light" in request.node.name: - msg = f"multi_label_cls/efficientnet_b0_light exceeds the following threshold = {threshold}" - pytest.xfail(msg) - if "multi_label_cls/mobilenet_v3_large_light" in request.node.name: - msg = f"multi_label_cls/mobilenet_v3_large_light exceeds the following threshold = {threshold}" - pytest.xfail(msg) - if "h_label_cls/efficientnet_v2_light" in request.node.name: - msg = f"h_label_cls/efficientnet_v2_light exceeds the following threshold = {threshold}" - pytest.xfail(msg) - if "multi_class_cls/tv_" in request.node.name: - msg = "torchvision model for multi_class_cls exceeds the following threshold = 0.1" - pytest.xfail(msg) - - _check_relative_metric_diff(torch_acc, ov_acc, threshold) diff --git a/tests/integration/cli/utils.py b/tests/integration/cli/utils.py deleted file mode 100644 index 749de187250..00000000000 --- a/tests/integration/cli/utils.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import subprocess -import sys -from unittest.mock import patch - -from otx.cli import main - - -def run_main(command_cfg: list[str], open_subprocess: bool) -> None: - if open_subprocess: - _run_main_with_open_subprocess(command_cfg) - else: - _run_main(command_cfg) - - -def _run_main_with_open_subprocess(command_cfg) -> None: - completed = subprocess.run( - [sys.executable, __file__, *command_cfg], # noqa: S603 - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - check=True, - ) - - completed.check_returncode() - - -def _run_main(command_cfg) -> None: - with patch("sys.argv", command_cfg): - main() - - -if __name__ == "__main__": - _run_main(sys.argv[1:]) diff --git a/tests/integration/cli/visual_prompting/__init__.py b/tests/integration/cli/visual_prompting/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/integration/cli/visual_prompting/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/integration/cli/visual_prompting/test_visual_prompting.py b/tests/integration/cli/visual_prompting/test_visual_prompting.py new file mode 100644 index 00000000000..59472623fed --- /dev/null +++ b/tests/integration/cli/visual_prompting/test_visual_prompting.py @@ -0,0 +1,132 @@ +"""Tests for Visual Prompting with OTX CLI""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os + +import pytest + +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + otx_deploy_openvino_testing, + otx_eval_deployment_testing, + otx_eval_openvino_testing, + otx_eval_testing, + otx_export_testing, + otx_resume_testing, + otx_train_testing, +) + +args = { + "--train-data-roots": "tests/assets/car_tree_bug", + "--val-data-roots": "tests/assets/car_tree_bug", + "--test-data-roots": "tests/assets/car_tree_bug", + "--input": "tests/assets/car_tree_bug/images/train", + "train_params": [ + "params", + "--learning_parameters.trainer.max_epochs", + "1", + "--learning_parameters.dataset.train_batch_size", + "2", + "--learning_parameters.dataset.use_mask", + "False", + ], +} + +# Training params for resume, num_iters*2 +resume_params = [ + "params", + "--learning_parameters.trainer.max_epochs", + "2", + "--learning_parameters.dataset.train_batch_size", + "4", +] + +otx_dir = os.getcwd() + + +templates = [ + template + for template in Registry("src/otx/algorithms/visual_prompting").filter(task_type="VISUAL_PROMPTING").templates + if "Zero_Shot" not in template.name +] +templates_ids = [template.model_template_id for template in templates] + + +class TestVisualPromptingCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_resume(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting/test_resume" + otx_resume_testing(template, tmp_dir_path, otx_dir, args) + template_work_dir = get_template_dir(template, tmp_dir_path) + args1 = copy.deepcopy(args) + args1["train_params"] = resume_params + args1[ + "--resume-from" + ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" + otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_export_testing(template, tmp_dir_path, False, check_ir_meta=False) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_eval_openvino_testing( + template, + tmp_dir_path, + otx_dir, + args, + threshold=1.0, + half_precision=half_precision, + is_visual_prompting=True, + ) + + @e2e_pytest_component + @pytest.mark.skip("demo.py is not supported.") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_openvino(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_deploy_openvino_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.skip("openvino.zip is not created because `otx_deploy_openvino_testing` is not executed.") + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval_deployment(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "visual_prompting" + otx_eval_deployment_testing(template, tmp_dir_path, otx_dir, args, threshold=1.0) diff --git a/tests/integration/cli/visual_prompting/test_zero_shot.py b/tests/integration/cli/visual_prompting/test_zero_shot.py new file mode 100644 index 00000000000..83963359d37 --- /dev/null +++ b/tests/integration/cli/visual_prompting/test_zero_shot.py @@ -0,0 +1,86 @@ +"""Tests for Zero-shot visual prompting with OTX CLI""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import os + +import pytest + +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + otx_eval_testing, + otx_train_testing, + otx_export_testing, + otx_eval_openvino_testing, +) + +args = { + "--train-data-roots": "tests/assets/car_tree_bug_zero_shot", + "--val-data-roots": "tests/assets/car_tree_bug_zero_shot", + "--test-data-roots": "tests/assets/car_tree_bug_zero_shot", + "--input": "tests/assets/car_tree_bug_zero_shot/images/train", + "train_params": [ + "params", + "--learning_parameters.trainer.max_epochs", + "1", + ], +} + +otx_dir = os.getcwd() + + +templates = [ + template + for template in Registry("src/otx/algorithms/visual_prompting").filter(task_type="VISUAL_PROMPTING").templates + if "Zero_Shot" in template.name +] +templates_ids = [template.model_template_id for template in templates] + + +class TestZeroShotVisualPromptingCLI: + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_eval(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_eval_testing(template, tmp_dir_path, otx_dir, args) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_export_testing(template, tmp_dir_path, False, check_ir_meta=False) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_fp16(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_export_testing(template, tmp_dir_path, half_precision=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_onnx(self, template, tmp_dir_path): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True) + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("half_precision", [True, False]) + def test_otx_eval_openvino(self, template, tmp_dir_path, half_precision): + tmp_dir_path = tmp_dir_path / "zero_shot_visual_prompting" + otx_eval_openvino_testing( + template, + tmp_dir_path, + otx_dir, + args, + threshold=1.0, + half_precision=half_precision, + is_visual_prompting=True, + ) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py deleted file mode 100644 index b4dcbd6f1b8..00000000000 --- a/tests/integration/conftest.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -from __future__ import annotations - -import importlib -import inspect -from pathlib import Path - -import pytest -from mmengine.config import Config as MMConfig -from otx.core.types.task import OTXTaskType - - -def pytest_addoption(parser: pytest.Parser) -> None: - parser.addoption( - "--open-subprocess", - action="store_true", - help="Open subprocess for each CLI integration test case. " - "This option can be used for easy memory management " - "while running consecutive multiple tests (default: false).", - ) - parser.addoption( - "--task", - action="store", - default="all", - type=str, - help="Task type of OTX to use integration test.", - ) - - -@pytest.fixture(scope="module", autouse=True) -def fxt_open_subprocess(request: pytest.FixtureRequest) -> bool: - """Open subprocess for each CLI integration test case. - - This option can be used for easy memory management - while running consecutive multiple tests (default: false). - """ - return request.config.getoption("--open-subprocess", False) - - -def find_recipe_folder(base_path: Path, folder_name: str) -> Path: - """ - Find the folder with the given name within the specified base path. - - Args: - base_path (Path): The base path to search within. - folder_name (str): The name of the folder to find. - - Returns: - Path: The path to the folder. - """ - for folder_path in base_path.rglob(folder_name): - if folder_path.is_dir(): - return folder_path - msg = f"Folder {folder_name} not found in {base_path}." - raise FileNotFoundError(msg) - - -def get_task_list(task: str) -> list[OTXTaskType]: - if task == "all": - return [task_type for task_type in OTXTaskType if task_type != OTXTaskType.DETECTION_SEMI_SL] - if task == "classification": - return [OTXTaskType.MULTI_CLASS_CLS, OTXTaskType.MULTI_LABEL_CLS, OTXTaskType.H_LABEL_CLS] - if task == "action": - return [OTXTaskType.ACTION_CLASSIFICATION, OTXTaskType.ACTION_DETECTION] - if task == "visual_prompting": - return [OTXTaskType.VISUAL_PROMPTING, OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING] - if task == "anomaly": - return [OTXTaskType.ANOMALY_CLASSIFICATION, OTXTaskType.ANOMALY_DETECTION, OTXTaskType.ANOMALY_SEGMENTATION] - return [OTXTaskType(task.upper())] - - -def pytest_configure(config): - """Configure pytest options and set task, recipe, and recipe_ov lists. - - Args: - config (pytest.Config): The pytest configuration object. - - Returns: - None - """ - task = config.getoption("--task") - - # This assumes have OTX installed in environment. - otx_module = importlib.import_module("otx") - # Modify RECIPE_PATH based on the task - recipe_path = Path(inspect.getfile(otx_module)).parent / "recipe" - task_list = get_task_list(task.lower()) - recipe_dir = [find_recipe_folder(recipe_path, task_type.value.lower()) for task_type in task_list] - - # Update RECIPE_LIST - target_recipe_list = [] - target_ov_recipe_list = [] - for task_recipe_dir in recipe_dir: - recipe_list = [str(p) for p in task_recipe_dir.glob("**/*.yaml") if "_base_" not in p.parts] - recipe_ov_list = [str(p) for p in task_recipe_dir.glob("**/openvino_model.yaml") if "_base_" not in p.parts] - recipe_list = set(recipe_list) - set(recipe_ov_list) - - target_recipe_list.extend(recipe_list) - target_ov_recipe_list.extend(recipe_ov_list) - tile_recipe_list = [recipe for recipe in target_recipe_list if "tile" in recipe] - - pytest.TASK_LIST = task_list - pytest.RECIPE_LIST = target_recipe_list - pytest.RECIPE_OV_LIST = target_ov_recipe_list - pytest.TILE_RECIPE_LIST = tile_recipe_list - - -@pytest.fixture(scope="session") -def fxt_asset_dir() -> Path: - return Path(__file__).parent.parent / "assets" - - -@pytest.fixture(scope="session") -def fxt_rtmdet_tiny_config(fxt_asset_dir: Path) -> MMConfig: - config_path = fxt_asset_dir / "mmdet_configs" / "rtmdet_tiny_8xb32-300e_coco.py" - - return MMConfig.fromfile(config_path) - - -# [TODO]: This is a temporary approach. -@pytest.fixture() -def fxt_target_dataset_per_task() -> dict: - return { - "multi_class_cls": "tests/assets/classification_dataset", - "multi_label_cls": "tests/assets/multilabel_classification", - "h_label_cls": "tests/assets/hlabel_classification", - "detection": "tests/assets/car_tree_bug", - "rotated_detection": "tests/assets/car_tree_bug", - "instance_segmentation": "tests/assets/car_tree_bug", - "semantic_segmentation": "tests/assets/common_semantic_segmentation_dataset/supervised", - "action_classification": "tests/assets/action_classification_dataset/", - "action_detection": "tests/assets/action_detection_dataset/", - "visual_prompting": "tests/assets/car_tree_bug", - "zero_shot_visual_prompting": "tests/assets/car_tree_bug_zero_shot", - "anomaly_classification": "tests/assets/anomaly_hazelnut", - "anomaly_detection": "tests/assets/anomaly_hazelnut", - "anomaly_segmentation": "tests/assets/anomaly_hazelnut", - } - - -@pytest.fixture() -def fxt_cli_override_command_per_task() -> dict: - return { - "multi_class_cls": [], - "multi_label_cls": [], - "h_label_cls": [], - "detection": [], - "rotated_detection": [], - "instance_segmentation": [], - "semantic_segmentation": [], - "action_classification": [], - "action_detection": [ - "--model.topk", - "3", - ], - "visual_prompting": [], - "zero_shot_visual_prompting": [], - "anomaly_classification": ["--limit_val_batches", "0"], - "anomaly_detection": ["--limit_val_batches", "0"], - "anomaly_segmentation": ["--limit_val_batches", "0"], - } diff --git a/tests/integration/detection/conftest.py b/tests/integration/detection/conftest.py deleted file mode 100644 index 1464fc7d5ac..00000000000 --- a/tests/integration/detection/conftest.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest -from omegaconf import DictConfig -from otx.core.config.data import ( - DataModuleConfig, - SubsetConfig, - TileConfig, - VisualPromptingConfig, -) -from otx.core.data.module import OTXDataModule -from otx.core.types.task import OTXTaskType -from otx.core.utils.config import mmconfig_dict_to_dict - -if TYPE_CHECKING: - from mmengine.config import Config as MMConfig - - -@pytest.fixture() -def fxt_mmcv_det_transform_config(fxt_rtmdet_tiny_config: MMConfig) -> list[DictConfig]: - return [DictConfig(cfg) for cfg in mmconfig_dict_to_dict(fxt_rtmdet_tiny_config.train_pipeline)] - - -@pytest.fixture() -def fxt_datamodule(fxt_asset_dir, fxt_mmcv_det_transform_config) -> OTXDataModule: - data_root = fxt_asset_dir / "car_tree_bug" - - batch_size = 8 - num_workers = 0 - config = DataModuleConfig( - data_format="coco_instances", - data_root=data_root, - train_subset=SubsetConfig( - subset_name="train", - batch_size=batch_size, - num_workers=num_workers, - transform_lib_type="MMDET", - transforms=fxt_mmcv_det_transform_config, - ), - val_subset=SubsetConfig( - subset_name="val", - batch_size=batch_size, - num_workers=num_workers, - transform_lib_type="MMDET", - transforms=fxt_mmcv_det_transform_config, - ), - test_subset=SubsetConfig( - subset_name="test", - batch_size=batch_size, - num_workers=num_workers, - transform_lib_type="MMDET", - transforms=fxt_mmcv_det_transform_config, - ), - tile_config=TileConfig(), - vpm_config=VisualPromptingConfig(), - ) - datamodule = OTXDataModule( - task=OTXTaskType.DETECTION, - config=config, - ) - datamodule.prepare_data() - return datamodule diff --git a/tests/integration/detection/test_data_module.py b/tests/integration/detection/test_data_module.py deleted file mode 100644 index 80be1372d1a..00000000000 --- a/tests/integration/detection/test_data_module.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -from otx.core.data.entity.detection import DetBatchDataEntity -from otx.core.data.module import OTXDataModule - - -class TestOTXDataModule: - def test_train_dataloader(self, fxt_datamodule: OTXDataModule) -> None: - for batch in fxt_datamodule.train_dataloader(): - assert isinstance(batch, DetBatchDataEntity) diff --git a/tests/integration/detection/test_model.py b/tests/integration/detection/test_model.py deleted file mode 100644 index b88a02bf725..00000000000 --- a/tests/integration/detection/test_model.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -import pytest -from omegaconf import DictConfig -from otx.core.data.module import OTXDataModule -from otx.core.model.entity.detection import MMDetCompatibleModel -from otx.core.utils.config import mmconfig_dict_to_dict - - -class TestOTXModel: - @pytest.fixture() - def fxt_rtmdet_tiny_model_config(self, fxt_rtmdet_tiny_config) -> DictConfig: - return DictConfig(mmconfig_dict_to_dict(fxt_rtmdet_tiny_config.model)) - - @pytest.fixture() - def fxt_model(self, fxt_rtmdet_tiny_model_config) -> MMDetCompatibleModel: - return MMDetCompatibleModel(num_classes=3, config=fxt_rtmdet_tiny_model_config) - - def test_forward_train( - self, - fxt_model: MMDetCompatibleModel, - fxt_datamodule: OTXDataModule, - ) -> None: - dataloader = fxt_datamodule.train_dataloader() - for inputs in dataloader: - outputs = fxt_model.forward(inputs) - assert isinstance(outputs, dict) - break diff --git a/tests/integration/test_tiling.py b/tests/integration/test_tiling.py deleted file mode 100644 index 574d8db14eb..00000000000 --- a/tests/integration/test_tiling.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from __future__ import annotations - -import numpy as np -import pytest -from datumaro import Dataset as DmDataset -from omegaconf import DictConfig, OmegaConf -from otx.core.config.data import ( - DataModuleConfig, - SubsetConfig, - TileConfig, - VisualPromptingConfig, -) -from otx.core.data.dataset.tile import OTXTileTransform -from otx.core.data.entity.detection import DetBatchDataEntity -from otx.core.data.entity.tile import TileBatchDetDataEntity -from otx.core.data.module import OTXDataModule -from otx.core.types.task import OTXTaskType - - -class TestOTXTiling: - @pytest.fixture() - def fxt_mmcv_det_transform_config(self) -> list[DictConfig]: - mmdet_base = OmegaConf.load("src/otx/recipe/_base_/data/mmdet_base.yaml") - return mmdet_base.config.train_subset.transforms - - @pytest.fixture() - def fxt_det_data_config(self, fxt_asset_dir, fxt_mmcv_det_transform_config) -> OTXDataModule: - data_root = fxt_asset_dir / "car_tree_bug" - - batch_size = 8 - num_workers = 0 - return DataModuleConfig( - data_format="coco_instances", - data_root=data_root, - train_subset=SubsetConfig( - subset_name="train", - batch_size=batch_size, - num_workers=num_workers, - transform_lib_type="MMDET", - transforms=fxt_mmcv_det_transform_config, - ), - val_subset=SubsetConfig( - subset_name="val", - batch_size=batch_size, - num_workers=num_workers, - transform_lib_type="MMDET", - transforms=fxt_mmcv_det_transform_config, - ), - test_subset=SubsetConfig( - subset_name="test", - batch_size=batch_size, - num_workers=num_workers, - transform_lib_type="MMDET", - transforms=fxt_mmcv_det_transform_config, - ), - tile_config=TileConfig(), - vpm_config=VisualPromptingConfig(), - ) - - def test_tile_transform(self): - dataset = DmDataset.import_from("tests/assets/car_tree_bug", format="coco_instances") - first_item = next(iter(dataset), None) - height, width = first_item.media.data.shape[:2] - - rng = np.random.default_rng() - tile_size = rng.integers(low=100, high=500, size=(2,)) - overlap = rng.random(2) - threshold_drop_ann = rng.random() - tiled_dataset = DmDataset.import_from("tests/assets/car_tree_bug", format="coco_instances") - tiled_dataset.transform( - OTXTileTransform, - tile_size=tile_size, - overlap=overlap, - threshold_drop_ann=threshold_drop_ann, - ) - - h_stride = max(int((1 - overlap[0]) * tile_size[0]), 1) - w_stride = max(int((1 - overlap[1]) * tile_size[1]), 1) - num_tile_rows = (height + h_stride - 1) // h_stride - num_tile_cols = (width + w_stride - 1) // w_stride - assert len(tiled_dataset) == (num_tile_rows * num_tile_cols * len(dataset)), "Incorrect number of tiles" - - def test_adaptive_tiling(self, fxt_det_data_config): - # Enable tile adapter - fxt_det_data_config.tile_config.enable_tiler = True - fxt_det_data_config.tile_config.enable_adaptive_tiling = True - tile_datamodule = OTXDataModule( - task=OTXTaskType.DETECTION, - config=fxt_det_data_config, - ) - tile_datamodule.prepare_data() - - assert tile_datamodule.config.tile_config.tile_size == (6750, 6750), "Tile size should be [6750, 6750]" - assert ( - pytest.approx(tile_datamodule.config.tile_config.overlap, rel=1e-3) == 0.03608 - ), "Overlap should be 0.03608" - assert tile_datamodule.config.tile_config.max_num_instances == 3, "Max num instances should be 3" - - def test_tile_sampler(self, fxt_det_data_config): - rng = np.random.default_rng() - - fxt_det_data_config.tile_config.enable_tiler = True - fxt_det_data_config.tile_config.enable_adaptive_tiling = False - fxt_det_data_config.tile_config.sampling_ratio = rng.random() - tile_datamodule = OTXDataModule( - task=OTXTaskType.DETECTION, - config=fxt_det_data_config, - ) - tile_datamodule.prepare_data() - sampled_count = max( - 1, - int(len(tile_datamodule._get_dataset("train")) * fxt_det_data_config.tile_config.sampling_ratio), - ) - - count = 0 - for batch in tile_datamodule.train_dataloader(): - count += batch.batch_size - assert isinstance(batch, DetBatchDataEntity) - - assert sampled_count == count, "Sampled count should be equal to the count of the dataloader batch size" - - def test_train_dataloader(self, fxt_det_data_config) -> None: - # Enable tile adapter - fxt_det_data_config.tile_config.enable_tiler = True - tile_datamodule = OTXDataModule( - task=OTXTaskType.DETECTION, - config=fxt_det_data_config, - ) - tile_datamodule.prepare_data() - for batch in tile_datamodule.train_dataloader(): - assert isinstance(batch, DetBatchDataEntity) - - def test_val_dataloader(self, fxt_det_data_config) -> None: - # Enable tile adapter - fxt_det_data_config.tile_config.enable_tiler = True - tile_datamodule = OTXDataModule( - task=OTXTaskType.DETECTION, - config=fxt_det_data_config, - ) - tile_datamodule.prepare_data() - for batch in tile_datamodule.val_dataloader(): - assert isinstance(batch, TileBatchDetDataEntity) - - def test_tile_merge(self): - pytest.skip("Not implemented yet") diff --git a/tests/perf/__init__.py b/tests/perf/__init__.py index d832bb41bf2..9984d0cb25b 100644 --- a/tests/perf/__init__.py +++ b/tests/perf/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 +"""OTX Perfomance tests.""" -"""OTX perfomance benchamrk tests.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/perf/benchmark-reference.csv b/tests/perf/benchmark-reference.csv new file mode 100644 index 00000000000..54ba6c212a9 --- /dev/null +++ b/tests/perf/benchmark-reference.csv @@ -0,0 +1,100 @@ +task,data_size,model,Precision(export),Precision(optimize),Precision(train),Recall(export),Recall(optimize),Recall(train),f-measure(export),f-measure(optimize),f-measure(train),Dice Average(export),Dice Average(optimize),Dice Average(train),Accuracy(export),Accuracy(optimize),Accuracy(train),val_score,epoch,train_e2e_time,data,avg_data_time,avg_iter_time,avg_time_per_image(export),avg_time_per_image(optimize),avg_cpu_util(%),avg_gpu_util(%),max_cpu_mem(GiB),max_gpu_mem(GiB) +anomaly_classification,large,ote_anomaly_classification_padim,1.0,1.0,1.0,0.9982,1.0,1.0,0.9991,1.0,1.0,,,,,,,0.0,,17.0,anomaly/mvtec/hazelnut_large,,,,,35.68,10.72,10.77,2.41 +anomaly_classification,large,ote_anomaly_classification_stfpm,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,,,,,,,0.0,,86.0,anomaly/mvtec/hazelnut_large,,,,,36.99,21.01,9.27,3.07 +anomaly_classification,medium,ote_anomaly_classification_padim,0.9991,0.9853,1.0,0.9991,0.9853,1.0,0.9991,0.9853,1.0,,,,,,,0.0,,13.0,anomaly/mvtec/wood_medium,,,,,33.9,8.23,10.47,2.42 +anomaly_classification,medium,ote_anomaly_classification_stfpm,0.9974,0.9983,1.0,0.9974,0.9983,1.0,0.9974,0.9983,1.0,,,,,,,0.0,,71.0,anomaly/mvtec/wood_medium,,,,,32.0,15.05,10.04,3.09 +anomaly_classification,small,ote_anomaly_classification_padim,0.9961,1.0,1.0,0.9954333333333332,1.0,1.0,0.9957666666666666,1.0,1.0,,,,,,,0.0,,9.333333333333334,anomaly/mvtec/bottle_small/,,,,,32.156666666666666,5.4433333333333325,9.773333333333332,2.41 +anomaly_classification,small,ote_anomaly_classification_stfpm,0.9973666666666666,0.9980333333333332,1.0,0.9973666666666666,0.9980333333333332,1.0,0.9973666666666666,0.9980333333333332,1.0,,,,,,,0.0,,42.66666666666666,anomaly/mvtec/bottle_small/,,,,,26.03333333333333,12.043333333333337,9.633333333333333,3.053333333333333 +anomaly_detection,large,ote_anomaly_detection_padim,,,,,,,0.9947,0.993,0.9991,,,,,,,0.0,,17.0,anomaly/mvtec/hazelnut_large,,,,,35.65,10.57,9.74,2.41 +anomaly_detection,large,ote_anomaly_detection_stfpm,,,,,,,0.9982,1.0,0.9991,,,,,,,0.0,,85.0,anomaly/mvtec/hazelnut_large,,,,,34.75,20.73,8.28,3.07 +anomaly_detection,medium,ote_anomaly_detection_padim,,,,,,,0.9948,0.9867,0.9987,,,,,,,0.0,,13.0,anomaly/mvtec/wood_medium,,,,,32.71,7.44,9.18,2.42 +anomaly_detection,medium,ote_anomaly_detection_stfpm,,,,,,,0.9889,0.9674,0.9983,,,,,,,0.0,,71.0,anomaly/mvtec/wood_medium,,,,,31.47,14.76,8.76,3.09 +anomaly_detection,small,ote_anomaly_detection_padim,,,,,,,0.9310333333333332,0.9343666666666668,0.9971,,,,,,,0.0,,9.333333333333334,anomaly/mvtec/bottle_small/,,,,,31.776666666666667,5.436666666666667,8.85,2.41 +anomaly_detection,small,ote_anomaly_detection_stfpm,,,,,,,0.9967333333333332,0.9980333333333332,0.9964333333333334,,,,,,,0.0,,41.0,anomaly/mvtec/bottle_small/,,,,,26.226666666666663,12.090000000000002,9.48,3.08 +anomaly_segmentation,large,ote_anomaly_segmentation_padim,,,,,,,0.9982,0.993,0.9991,,,,,,,0.0,,19.0,anomaly/mvtec/hazelnut_large,,,,,37.08,9.85,9.06,2.41 +anomaly_segmentation,large,ote_anomaly_segmentation_stfpm,,,,,,,1.0,1.0,0.9991,,,,,,,0.0,,104.0,anomaly/mvtec/hazelnut_large,,,,,38.71,16.42,7.93,3.08 +anomaly_segmentation,medium,ote_anomaly_segmentation_padim,,,,,,,1.0,0.9922,0.9987,,,,,,,0.0,,15.0,anomaly/mvtec/wood_medium,,,,,32.37,7.39,9.44,2.42 +anomaly_segmentation,medium,ote_anomaly_segmentation_stfpm,,,,,,,0.9983,0.9991,0.9987,,,,,,,0.0,,95.0,anomaly/mvtec/wood_medium,,,,,30.6,11.56,9.1,3.1 +anomaly_segmentation,small,ote_anomaly_segmentation_padim,,,,,,,0.9957666666666666,1.0,0.9971,,,,,,,0.0,,10.333333333333334,anomaly/mvtec/bottle_small/,,,,,30.086666666666662,5.523333333333333,8.886666666666665,2.41 +anomaly_segmentation,small,ote_anomaly_segmentation_stfpm,,,,,,,0.9973666666666666,0.9986666666666668,0.9957666666666666,,,,,,,0.0,,52.66666666666666,anomaly/mvtec/bottle_small/,,,,,23.38,10.1,8.573333333333332,3.09 +detection,large,Custom_Object_Detection_Gen3_ATSS,0.866,0.8576,0.8295,0.7345,0.7441,0.7612,0.7948,0.7968,0.7939,,,,,,,0.775,28.0,338.0,detection/vitens_large,0.0135,0.2005,0.0336,0.0199,9.87,36.12,8.33,9.63 +detection,large,Custom_Object_Detection_Gen3_SSD,0.7056,0.746,0.7338,0.6792,0.6234,0.6595,0.6921,0.6792,0.6946,,,,,,,0.6815,48.0,436.0,detection/vitens_large,0.0165,0.157,0.0244,0.0124,13.01,35.88,8.28,6.77 +detection,large,Custom_Object_Detection_YOLOX,0.8594,0.8794,0.857,0.7046,0.6551,0.7122,0.7744,0.7509,0.7779,,,,,,,0.7438,34.0,303.0,detection/vitens_large,0.0113,0.1427,0.0101,0.0086,9.4,18.03,7.98,3.77 +detection,large,Object_Detection_ResNeXt101_ATSS,0.8745,0.8818,0.8748,0.7924,0.7864,0.7926,0.8314,0.8314,0.8317,,,,,,,0.8059,32.0,1365.0,detection/vitens_large,0.0053,0.321,0.6785,0.3538,7.11,72.35,9.68,12.49 +detection,large,Object_Detection_YOLOX_L,0.855,0.8668,0.7781,0.7607,0.7532,0.789,0.8051,0.806,0.7835,,,,,,,0.8191,48.0,691.0,detection/vitens_large,0.0135,0.2609,0.1644,0.0943,10.92,42.19,8.98,10.44 +detection,large,Object_Detection_YOLOX_S,0.8273,0.8632,0.8273,0.7184,0.6699,0.7272,0.769,0.7543,0.774,,,,,,,0.7594,18.0,226.0,detection/vitens_large,0.0134,0.1914,0.0399,0.025,13.48,23.63,8.07,5.05 +detection,large,Object_Detection_YOLOX_X,0.8166,0.8091,0.7647,0.7547,0.7643,0.7776,0.7845,0.7861,0.7711,,,,,,,0.8049,40.0,1208.0,detection/vitens_large,0.0043,0.2309,0.2895,0.1634,8.93,50.39,10.21,9.48 +detection,medium,Custom_Object_Detection_Gen3_ATSS,0.7784,0.7812,0.7836,0.6841,0.6994,0.6754,0.7264,0.7374,0.7247,,,,,,,0.7473,38.6667,322.0,detection/pothole_medium,0.0147,0.2107,0.031,0.0191,8.71,36.32,11.44,8.79 +detection,medium,Custom_Object_Detection_Gen3_SSD,0.7465,0.7072,0.7465,0.5316,0.549,0.5316,0.6204,0.6175,0.6204,,,,,,,0.6701,58.0,337.0,detection/pothole_medium,0.0143,0.1444,0.0222,0.0118,10.81,35.75,11.15,6.77 +detection,medium,Custom_Object_Detection_YOLOX,0.794,0.8134,0.8089,0.6514,0.5664,0.6536,0.7155,0.667,0.7229,,,,,,,0.6809,40.6667,243.0,detection/pothole_medium,0.0129,0.1448,0.0104,0.0097,8.88,17.82,9.51,3.96 +detection,medium,Object_Detection_ResNeXt101_ATSS,0.7942,0.7974,0.8222,0.6928,0.6776,0.6667,0.7382,0.732,0.7362,,,,,,,0.7639,24.6667,719.0,detection/pothole_medium,0.0058,0.3288,0.6173,0.3144,6.91,65.14,22.22,12.55 +detection,medium,Object_Detection_YOLOX_L,0.7183,0.7479,0.8111,0.634,0.6079,0.5926,0.671,0.6692,0.6844,,,,,,,0.6878,39.3333,404.0,detection/pothole_medium,0.0152,0.2377,0.1493,0.083,9.94,36.56,17.11,10.97 +detection,medium,Object_Detection_YOLOX_S,0.7866,0.8289,0.8309,0.6231,0.5904,0.5904,0.6948,0.6889,0.689,,,,,,,0.6813,46.0,339.0,detection/pothole_medium,0.0156,0.19,0.0365,0.0234,11.91,22.25,11.21,5.29 +detection,medium,Object_Detection_YOLOX_X,0.8159,0.822,0.7949,0.6732,0.6732,0.6667,0.7373,0.7402,0.7249,,,,,,,0.6903,41.3333,866.0,detection/pothole_medium,0.0047,0.2395,0.2638,0.1446,8.3,43.91,21.4,9.92 +detection,small,Custom_Object_Detection_Gen3_ATSS,0.6698,0.6764666666666667,0.6608333333333333,0.4437333333333333,0.4335333333333333,0.4524333333333333,0.5290666666666667,0.5231333333333333,0.5305333333333334,,,,,,,0.6503333333333333,39.444433333333336,46.0,detection/pothole_small/,0.0585333333333333,0.2513,0.0313,0.0200333333333333,7.133333333333333,12.313333333333333,14.886666666666668,10.74 +detection,small,Custom_Object_Detection_Gen3_SSD,0.5493666666666667,0.6093666666666667,0.5592666666666667,0.3602,0.3391666666666666,0.3544,0.4266333333333333,0.431,0.4274333333333333,,,,,,,0.5454666666666667,46.1111,43.0,detection/pothole_small/,0.0636666666666666,0.1903,0.022,0.0125333333333333,8.5,10.343333333333334,14.173333333333334,7.849999999999999 +detection,small,Custom_Object_Detection_YOLOX,0.5974666666666666,0.5909666666666666,0.5782333333333334,0.4364666666666667,0.4001333333333334,0.4379,0.4978333333333333,0.4564666666666667,0.4891666666666667,,,,,,,0.5445666666666666,38.33336666666667,37.66666666666666,detection/pothole_small/,0.0853666666666666,0.2108333333333333,0.0101999999999999,0.0082333333333333,7.653333333333333,6.63,11.236666666666666,3.233333333333333 +detection,small,Object_Detection_ResNeXt101_ATSS,0.6268333333333334,0.6405333333333333,0.6311666666666667,0.5345333333333334,0.5177999999999999,0.5330666666666667,0.5697666666666666,0.5681666666666666,0.5708333333333333,,,,,,,0.7014999999999999,29.33333333333333,113.33333333333331,detection/pothole_small/,0.0194333333333333,0.3340333333333333,0.6159666666666667,0.3149666666666667,7.1866666666666665,20.84,36.34666666666666,11.766666666666666 +detection,small,Object_Detection_YOLOX_L,0.4252333333333333,0.4520333333333333,0.4489666666666667,0.3979999999999999,0.3550999999999999,0.3958,0.4079,0.3945,0.4135333333333333,,,,,,,0.4736666666666667,30.0,62.66666666666666,detection/pothole_small/,0.1217333333333333,0.3413333333333333,0.1493333333333333,0.0835,8.07,9.943333333333332,24.61,9.05 +detection,small,Object_Detection_YOLOX_S,0.4303333333333333,0.4604333333333333,0.4392666666666667,0.2803333333333333,0.2585333333333333,0.2890333333333333,0.3258666666666667,0.3051,0.3326,,,,,,,0.3561666666666667,67.77776666666666,88.66666666666667,detection/pothole_small/,0.1149999999999999,0.2748,0.0361666666666666,0.0226,7.663333333333334,7.003333333333334,14.47,4.2 +detection,small,Object_Detection_YOLOX_X,0.5061333333333333,0.5411,0.5042333333333333,0.3827333333333333,0.3384333333333333,0.3935666666666666,0.4305,0.4130666666666666,0.4313333333333333,,,,,,,0.4696333333333333,36.8889,138.0,detection/pothole_small/,0.0353333333333333,0.2470333333333333,0.2628333333333333,0.1478,8.066666666666666,12.003333333333332,32.873333333333335,7.87 +hierarchical_label_classification,medium,Custom_Image_Classification_DeiT-Tiny,,,,,,,,,,,,,0.3128,0.3114,0.3128,73.4618,26.0,79.0,classification/h_label/h_label_CUB_medium,0.0038,0.0831,0.0068,0.0073,10.55,45.81,8.38,4.22 +hierarchical_label_classification,medium,Custom_Image_Classification_EfficientNet-V2-S,,,,,,,,,,,,,0.3408,0.3311,0.3402,74.2912,26.0,211.0,classification/h_label/h_label_CUB_medium,0.0043,0.2407,0.0106,0.0108,7.97,65.09,10.3,11.94 +hierarchical_label_classification,medium,Custom_Image_Classification_EfficinetNet-B0,,,,,,,,,,,,,0.3064,0.2871,0.3071,73.0667,36.6667,255.0,classification/h_label/h_label_CUB_medium,0.0037,0.2224,0.0053,0.0058,8.33,76.59,8.49,9.18 +hierarchical_label_classification,medium,Custom_Image_Classification_MobileNet-V3-large-1x,,,,,,,,,,,,,0.2441,0.2205,0.2447,72.4591,28.6667,104.0,classification/h_label/h_label_CUB_medium,0.0036,0.1071,0.005,0.0058,9.88,58.49,8.25,5.85 +hierarchical_label_classification,small,Custom_Image_Classification_DeiT-Tiny,,,,,,,,,,,,,0.7608333333333334,0.7608333333333333,0.7617666666666666,71.83643333333333,29.444433333333336,13.333333333333334,classification/h_label/h_label_CUB_small/,0.0497333333333333,0.0919666666666666,0.0065,0.0075,6.510000000000001,4.739999999999999,9.453333333333331,3.333333333333333 +hierarchical_label_classification,small,Custom_Image_Classification_EfficientNet-V2-S,,,,,,,,,,,,,0.8531333333333334,0.8474666666666666,0.8540666666666666,74.1512,30.0,23.666666666666668,classification/h_label/h_label_CUB_small/,0.0505333333333333,0.1831666666666666,0.0105,0.0109,7.043333333333333,8.676666666666668,14.206666666666663,6.883333333333333 +hierarchical_label_classification,small,Custom_Image_Classification_EfficinetNet-B0,,,,,,,,,,,,,0.8475,0.8088333333333333,0.8493666666666666,70.60183333333333,40.55556666666666,15.666666666666666,classification/h_label/h_label_CUB_small/,0.0493333333333333,0.1206999999999999,0.0046333333333333,0.0055,5.7700000000000005,10.52,9.786666666666669,6.19 +hierarchical_label_classification,small,Custom_Image_Classification_MobileNet-V3-large-1x,,,,,,,,,,,,,0.7363,0.6873999999999999,0.742,72.68516666666666,34.44446666666666,12.333333333333334,classification/h_label/h_label_CUB_small/,0.0496,0.0924666666666666,0.0045666666666666,0.0065,5.023333333333333,6.536666666666666,9.333333333333334,3.9033333333333338 +instance_segmentation,medium,Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B,0.6167666666666667,0.6246,0.5984333333333334,0.3954666666666666,0.3279,0.3753999999999999,0.4800666666666666,0.4273333333333333,0.4595333333333333,,,,,,,0.5273,18.555533333333333,485.0,instance_seg/coco_car_person_medium,0.006,0.2325,0.3387,0.2247,,,, +instance_segmentation,medium,Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50,0.7623000000000001,0.7542,0.7540333333333334,0.5637,0.5621999999999999,0.5555666666666667,0.6478666666666667,0.6438333333333334,0.6395,,,,,,,0.7068666666666666,12.666666666666666,438.3333333333333,instance_seg/coco_car_person_medium,0.0126,0.3511,0.5008,0.2897,,,, +multi_label_classification,large,Custom_Image_Classification_DeiT-Tiny,,,,,,,,,,,,,0.9787,0.9785,0.9787,0.9773,17.0,294.0,classification/multi_label/multilabel_food101_large,0.0076,0.0679,0.0053,0.0061,12.43,35.89,6.86,3.38 +multi_label_classification,large,Custom_Image_Classification_EfficientNet-V2-S,,,,,,,,,,,,,0.9894,0.9891,0.9895,0.9895,17.0,958.0,classification/multi_label/multilabel_food101_large,0.0064,0.2833,0.0098,0.0096,7.39,51.61,7.29,6.77 +multi_label_classification,large,Custom_Image_Classification_EfficinetNet-B0,,,,,,,,,,,,,0.9824,0.9805,0.9823,0.9828,25.0,774.0,classification/multi_label/multilabel_food101_large,0.0063,0.1545,0.0038,0.0045,10.23,62.26,7.08,5.62 +multi_label_classification,large,Custom_Image_Classification_MobileNet-V3-large-1x,,,,,,,,,,,,,0.9803,0.9786,0.9802,0.9803,16.0,392.0,classification/multi_label/multilabel_food101_large,0.0074,0.1153,0.0036,0.0047,11.32,48.44,6.96,3.55 +multi_label_classification,medium,Custom_Image_Classification_DeiT-Tiny,,,,,,,,,,,,,0.9886,0.9885,0.9886,0.9893,28.0,89.0,classification/multi_label/multilabel_CUB_medium,0.0129,0.0726,0.0058,0.0064,10.33,29.06,8.24,3.73 +multi_label_classification,medium,Custom_Image_Classification_EfficientNet-V2-S,,,,,,,,,,,,,0.9957,0.9957,0.9957,0.9962,24.6667,279.0,classification/multi_label/multilabel_CUB_medium,0.0127,0.303,0.0096,0.0101,7.52,49.52,10.06,6.97 +multi_label_classification,medium,Custom_Image_Classification_EfficinetNet-B0,,,,,,,,,,,,,0.9931,0.9924,0.9931,0.9935,27.3333,162.0,classification/multi_label/multilabel_CUB_medium,0.0119,0.1609,0.0043,0.0049,9.26,55.08,8.42,6.19 +multi_label_classification,medium,Custom_Image_Classification_MobileNet-V3-large-1x,,,,,,,,,,,,,0.9934,0.9924,0.9933,0.994,26.6667,122.0,classification/multi_label/multilabel_CUB_medium,0.0124,0.1207,0.004,0.0051,9.82,42.59,8.2,3.8 +multi_label_classification,small,Custom_Image_Classification_DeiT-Tiny,,,,,,,,,,,,,0.9887,0.9899666666666668,0.9899666666666668,0.9814666666666666,16.0,16.666666666666668,classification/multi_label/multilabel_CUB_small/,0.0408666666666666,0.0878666666666666,0.0068333333333333,0.0077,6.596666666666667,8.299999999999999,9.373333333333331,2.8633333333333333 +multi_label_classification,small,Custom_Image_Classification_EfficientNet-V2-S,,,,,,,,,,,,,0.9899666666666668,0.9918333333333336,0.9887333333333332,0.9907333333333334,16.0,39.66666666666666,classification/multi_label/multilabel_CUB_small/,0.0579,0.3075,0.0096333333333333,0.0118666666666666,6.763333333333333,18.406666666666663,14.15,4.163333333333333 +multi_label_classification,small,Custom_Image_Classification_EfficinetNet-B0,,,,,,,,,,,,,0.9887333333333332,0.9868333333333332,0.9887333333333332,1.0,16.444433333333333,21.33333333333333,classification/multi_label/multilabel_CUB_small/,0.0373666666666666,0.1513333333333333,0.0047333333333333,0.0058666666666666,6.09,17.483333333333334,9.763333333333334,3.736666666666667 +multi_label_classification,small,Custom_Image_Classification_MobileNet-V3-large-1x,,,,,,,,,,,,,0.9918666666666668,0.99,0.9925,0.9907333333333334,16.0,18.33333333333333,classification/multi_label/multilabel_CUB_small/,0.0378666666666666,0.1293,0.0044333333333333,0.0060333333333333,5.546666666666667,12.69,9.286666666666669,2.6966666666666668 +semantic_segmentation,large,Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR,,,,,,,,,,0.8611,0.8591500000000001,0.86175,,,,0.91855,19.0,683.0,semantic_seg/kvasir_large,0.0058499999999999,0.2657,0.086,0.08755,8.16,29.31,8.14,3.54 +semantic_segmentation,large,Custom_Semantic_Segmentation_Lite-HRNet-18_OCR,,,,,,,,,,0.87695,0.3024,0.8774,,,,0.9185,17.0,607.0,semantic_seg/kvasir_large,0.00595,0.2741,0.0857,0.15855,8.0,28.71,8.13,3.54 +semantic_segmentation,large,Custom_Semantic_Segmentation_Lite-HRNet-s-mod2_OCR,,,,,,,,,,0.8465,0.8330500000000001,0.84695,,,,0.91315,18.5,530.0,semantic_seg/kvasir_large,0.00605,0.21355,0.0723,0.07365,8.95,21.33,8.06,2.39 +semantic_segmentation,large,Custom_Semantic_Segmentation_Lite-HRNet-x-mod3_OCR,,,,,,,,,,0.86605,0.8060499999999999,0.8669,,,,0.9141,9.0,604.5,semantic_seg/kvasir_large,0.0062,0.5105500000000001,0.24245,0.1956999999999999,6.98,51.15,8.4,8.88 +semantic_segmentation,large,Custom_Semantic_Segmentation_SegNext_B,,,,,,,,,,0.90045,0.8891,0.90115,,,,0.9418,34.0,1436.5,semantic_seg/kvasir_large,0.00665,0.34235,0.1377999999999999,0.0996,7.76,74.71,8.82,12.75 +semantic_segmentation,large,Custom_Semantic_Segmentation_SegNext_s,,,,,,,,,,0.88795,0.8791500000000001,0.8895,,,,0.92885,28.5,692.0,semantic_seg/kvasir_large,0.0063999999999999,0.1941,0.09495,0.0841,9.33,73.28,8.38,8.06 +semantic_segmentation,large,Custom_Semantic_Segmentation_SegNext_t,,,,,,,,,,0.90395,0.5005499999999999,0.9039,,,,0.9414,55.0,1119.5,semantic_seg/kvasir_large,0.00655,0.16275,0.07675,0.04375,10.22,65.39,8.25,5.95 +semantic_segmentation,medium,Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR,,,,,,,,,,0.8221,0.8216,0.8222,,,,0.8896,17.33335,281.5,semantic_seg/kvasir_medium,0.00825,0.2835499999999999,0.10135,0.1036499999999999,7.54,19.1,11.63,3.54 +semantic_segmentation,medium,Custom_Semantic_Segmentation_Lite-HRNet-18_OCR,,,,,,,,,,0.8176000000000001,0.28995,0.8177000000000001,,,,0.88855,17.83335,291.5,semantic_seg/kvasir_medium,0.0084,0.2851,0.1002,0.26565,7.47,18.96,12.17,3.54 +semantic_segmentation,medium,Custom_Semantic_Segmentation_Lite-HRNet-s-mod2_OCR,,,,,,,,,,0.81725,0.7763500000000001,0.8176,,,,0.88825,15.33335,197.5,semantic_seg/kvasir_medium,0.0081999999999999,0.22465,0.08045,0.0849999999999999,8.05,14.97,11.12,2.47 +semantic_segmentation,medium,Custom_Semantic_Segmentation_Lite-HRNet-x-mod3_OCR,,,,,,,,,,0.8139000000000001,0.7655,0.8141499999999999,,,,0.8937999999999999,16.16665,485.0,semantic_seg/kvasir_medium,0.0093,0.53175,0.34815,0.37795,6.73,33.57,18.29,8.88 +semantic_segmentation,medium,Custom_Semantic_Segmentation_SegNext_B,,,,,,,,,,0.87405,0.70145,0.87485,,,,0.9128,36.6667,573.0,semantic_seg/kvasir_medium,0.0097,0.3444,0.1251,0.0763,7.67,53.27,15.64,12.76 +semantic_segmentation,medium,Custom_Semantic_Segmentation_SegNext_s,,,,,,,,,,0.85365,0.84995,0.8545499999999999,,,,0.9167,49.5,442.5,semantic_seg/kvasir_medium,0.0091499999999999,0.19495,0.07465,0.0523499999999999,8.99,51.21,12.77,8.06 +semantic_segmentation,medium,Custom_Semantic_Segmentation_SegNext_t,,,,,,,,,,0.82795,0.61695,0.82925,,,,0.91675,42.66665,342.0,semantic_seg/kvasir_medium,0.01,0.16745,0.0436,0.0292,9.4,42.37,11.35,5.95 +semantic_segmentation,small,Custom_Semantic_Segmentation_Lite-HRNet-18-mod2_OCR,,,,,,,,,,0.6704666666666667,0.6694,0.6708500000000001,,,,0.8038333333333334,43.61111666666667,93.0,semantic_seg/kvasir_small/,0.0598833333333333,0.3396166666666667,0.2023,0.1444166666666666,6.453333333333333,10.933333333333332,16.203333333333333,3.5400000000000005 +semantic_segmentation,small,Custom_Semantic_Segmentation_Lite-HRNet-18_OCR,,,,,,,,,,0.6844333333333333,0.2878833333333333,0.6845333333333333,,,,0.8043999999999999,42.94446666666666,93.0,semantic_seg/kvasir_small/,0.06095,0.3437833333333333,0.19685,0.4613,6.083333333333333,10.496666666666668,16.743333333333336,3.5400000000000005 +semantic_segmentation,small,Custom_Semantic_Segmentation_Lite-HRNet-s-mod2_OCR,,,,,,,,,,0.6797166666666666,0.6080333333333333,0.6796333333333333,,,,0.7821666666666666,42.44445,72.16666666666667,semantic_seg/kvasir_small/,0.0603333333333333,0.2786,0.1879166666666666,0.1863,5.986666666666667,7.553333333333334,14.886666666666663,2.4966666666666666 +semantic_segmentation,small,Custom_Semantic_Segmentation_Lite-HRNet-x-mod3_OCR,,,,,,,,,,0.6940666666666666,0.5269166666666667,0.6944,,,,0.8036333333333333,44.05555,163.0,semantic_seg/kvasir_small/,0.0741333333333333,0.6359666666666667,0.6796166666666666,0.1729,6.45,20.426666666666662,28.44666666666667,8.876666666666667 +semantic_segmentation,small,Custom_Semantic_Segmentation_SegNext_B,,,,,,,,,,0.7581666666666665,0.4395333333333333,0.75955,,,,0.8298333333333333,47.94445,128.66666666666666,semantic_seg/kvasir_small/,0.0648166666666666,0.4011166666666666,0.20945,0.2101833333333333,5.903333333333333,23.820000000000004,23.236666666666668,12.743333333333334 +semantic_segmentation,small,Custom_Semantic_Segmentation_SegNext_s,,,,,,,,,,0.7175166666666666,0.6184666666666666,0.7174999999999999,,,,0.8137833333333333,40.16666666666666,62.66666666666666,semantic_seg/kvasir_small/,0.0633333333333333,0.2501333333333333,0.23045,0.2357833333333333,6.153333333333333,21.01,17.736666666666668,8.06 +semantic_segmentation,small,Custom_Semantic_Segmentation_SegNext_t,,,,,,,,,,0.6835666666666667,0.4820333333333333,0.6836666666666668,,,,0.7762833333333333,40.44445,52.833333333333336,semantic_seg/kvasir_small/,0.0652,0.22215,0.1798,0.1545333333333333,6.416666666666667,18.026666666666667,15.31,5.953333333333333 +single_label_classification,large,Custom_Image_Classification_DeiT-Tiny,,,,,,,,,,,,,0.822,0.8202,0.8217,0.821,18.0,279.0,classification/single_label/multiclass_food101_large,0.0015,0.057,0.006,0.007,12.96,38.5,8.01,3.38 +single_label_classification,large,Custom_Image_Classification_EfficientNet-V2-S,,,,,,,,,,,,,0.8833,0.8755,0.8835,0.8872,19.0,639.0,classification/single_label/multiclass_food101_large,0.0008,0.145,0.0102,0.0113,6.9,24.18,8.54,6.88 +single_label_classification,large,Custom_Image_Classification_EfficinetNet-B0,,,,,,,,,,,,,0.837,0.8278,0.8375,0.8475,23.0,434.0,classification/single_label/multiclass_food101_large,0.0008,0.084,0.0042,0.0052,12.86,51.12,8.15,5.57 +single_label_classification,large,Custom_Image_Classification_MobileNet-V3-large-1x,,,,,,,,,,,,,0.8188,0.767,0.818,0.822,30.0,437.0,classification/single_label/multiclass_food101_large,0.0012,0.0566,0.0041,0.0056,12.96,34.14,8.01,3.37 +single_label_classification,medium,Custom_Image_Classification_DeiT-Tiny,,,,,,,,,,,,,0.7748,0.7719,0.774,0.8107,42.0,102.0,classification/single_label/multiclass_CUB_medium,0.0031,0.0575,0.0064,0.0073,10.86,31.84,9.45,3.73 +single_label_classification,medium,Custom_Image_Classification_EfficientNet-V2-S,,,,,,,,,,,,,0.8382,0.8205,0.8387,0.8715,24.0,139.0,classification/single_label/multiclass_CUB_medium,0.0032,0.1421,0.0106,0.0119,8.83,36.15,11.4,7.05 +single_label_classification,medium,Custom_Image_Classification_EfficinetNet-B0,,,,,,,,,,,,,0.7735,0.7225,0.7733,0.7883,26.0,88.0,classification/single_label/multiclass_CUB_medium,0.0032,0.087,0.0047,0.0056,10.73,42.48,9.62,6.14 +single_label_classification,medium,Custom_Image_Classification_MobileNet-V3-large-1x,,,,,,,,,,,,,0.7594,0.6956,0.76,0.7912,20.0,53.0,classification/single_label/multiclass_CUB_medium,0.0031,0.0608,0.0044,0.006,10.6,29.84,9.33,3.62 +single_label_classification,small,Custom_Image_Classification_DeiT-Tiny,,,,,,,,,,,,,0.9981333333333332,1.0,0.9981333333333332,1.0,20.0,13.666666666666666,classification/single_label/multiclass_CUB_small/,0.0383333333333333,0.0847,0.0074333333333333,0.0090666666666666,6.63,3.17,10.74,2.8200000000000003 +single_label_classification,small,Custom_Image_Classification_EfficientNet-V2-S,,,,,,,,,,,,,1.0,1.0,1.0,1.0,20.0,17.333333333333332,classification/single_label/multiclass_CUB_small/,0.0393,0.1711666666666666,0.0117333333333333,0.0134333333333333,7.056666666666668,4.866666666666667,15.45,3.956666666666667 +single_label_classification,small,Custom_Image_Classification_EfficinetNet-B0,,,,,,,,,,,,,0.9981333333333332,0.9962333333333334,0.9981333333333332,1.0,20.0,9.333333333333334,classification/single_label/multiclass_CUB_small/,0.0374666666666666,0.1051333333333333,0.0051333333333333,0.0067,5.876666666666668,3.926666666666667,11.023333333333332,3.7566666666666655 +single_label_classification,small,Custom_Image_Classification_MobileNet-V3-large-1x,,,,,,,,,,,,,0.9812,0.9529333333333332,0.9831,1.0,25.0,9.666666666666666,classification/single_label/multiclass_CUB_small/,0.0376333333333333,0.0898333333333333,0.0049,0.0081,5.06,2.786666666666666,10.573333333333332,2.6966666666666668 +tiling_instance_segmentation,medium,Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B,0.9706,0.8699,0.969,0.8997,0.2112,0.8977,0.9338,0.3389,0.932,,,,,,,0.8792,23.3333,807.0,tiling_instance_seg/vitens_aeromonas_medium,0.0017,0.1738,0.7004,0.1075,,,, +tiling_instance_segmentation,medium,Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50,0.9674,0.7812,0.967,0.894,0.2521,0.8912,0.9293,0.38,0.9275,,,,,,,0.8703,23.0,758.0,tiling_instance_seg/vitens_aeromonas_medium,0.0016,0.1689,1.6392,0.1318,,,, +tiling_instance_segmentation,medium,Custom_Counting_Instance_Segmentation_MaskRCNN_SwinT_FP16,0.6741,0.5214,0.9596,0.8599,0.1626,0.8971,0.6751,0.2475,0.9273,,,,,,,0.8687,21.6667,796.0,tiling_instance_seg/vitens_aeromonas_medium,0.0018,0.1779,2.4882,0.2203,,,, +tiling_instance_segmentation,small,Custom_Counting_Instance_Segmentation_MaskRCNN_EfficientNetB2B,0.9665333333333334,0.8414,0.9650333333333334,0.8255333333333333,0.1620666666666666,0.8228333333333332,0.8891333333333332,0.2647,0.8866666666666667,,,,,,,0.8278,30.666666666666668,180.66666666666663,tiling_instance_seg/vitens_aeromonas_small/,0.0033666666666666,0.1826333333333333,0.6970000000000001,0.1159999999999999,,,, +tiling_instance_segmentation,small,Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50,0.9610666666666666,0.8028,0.9570666666666666,0.7963333333333332,0.1649333333333333,0.8095333333333333,0.8683333333333333,0.2709,0.8758666666666667,,,,,,,0.8178000000000001,33.0,195.66666666666663,tiling_instance_seg/vitens_aeromonas_small/,,,,,,,, diff --git a/tests/perf/benchmark.py b/tests/perf/benchmark.py index 1d85332b15a..c554296cae3 100644 --- a/tests/perf/benchmark.py +++ b/tests/perf/benchmark.py @@ -1,309 +1,145 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 +"""OTX Benchmark based on tools/experiment.py.""" -"""OTX benchmark runner.""" +# Copyright (C) 2023-2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations -import gc -import logging import os -import subprocess -from dataclasses import dataclass +import glob +import pandas as pd +import yaml from pathlib import Path -from time import time -from typing import Any +from typing import List, Optional -import pandas as pd +from tests.test_suite.run_test_command import check_run -log = logging.getLogger(__name__) +class OTXBenchmark: + """Benchmark runner based on tools/experiment.py in OTX1.x. -class Benchmark: - """Benchmark runner for OTX2.x. + Example: + >>> bm = OTXBenchmark(['random_sample1', 'random_sample2'], data_root='./data/coco') + >>> atss_result = bm.run('MobileNetV2-ATSS') + >>> yolox_result = bm.run('YOLOX-TINY') Args: + datasets (List[str]): Paths to datasets relative to the data_root. + Intended for, but not restricted to different sampling based on same dataset. data_root (str): Path to the root of dataset directories. Defaults to './data'. - output_root (str): Output root dirctory for logs and results. Defaults to './otx-benchmark'. num_epoch (int): Overrides the per-model default number of epoch settings. Defaults to 0, which means no overriding. num_repeat (int): Number for trials with different random seed, which would be set as range(0, num_repeat). Defaults to 1. + train_params (dict, optional): Additional training parameters. + e.x) {'learning_parameters.num_iters': 2}. Defaults to {}. + track_resources (bool): Whether to track CPU & GPU usage metrics. Defaults to False. eval_upto (str): The last serial operation to evaluate. Choose one of ('train', 'export', 'optimize'). Operations include the preceeding ones. e.x) Eval up to 'optimize': train -> eval -> export -> eval -> optimize -> eval Default to 'train'. - tags (dict, optional): Key-values pair metadata for the experiment. + output_root (str): Output root dirctory for logs and results. Defaults to './otx-benchmark'. dry_run (bool): Whether to just print the OTX command without execution. Defaults to False. - deterministic (bool): Whether to turn on deterministic training mode. Defaults to False. - accelerator (str): Accelerator device on which to run benchmark. Defaults to gpu. + tags (dict, optional): Key-values pair metadata for the experiment. + subset_dir_names (dict, optional): Specify dataset subset directory names, if any. + e.x) {"train": "train_10percent", "val": "val_all", "test": "test"} """ - @dataclass - class Model: - """Benchmark model.""" - - task: str - name: str - category: str - - @dataclass - class Dataset: - """Benchmark dataset.""" - - name: str - path: Path - size: str - data_format: str - num_classes: int - num_repeat: int = 1 - extra_overrides: dict | None = None - - @dataclass - class Criterion: - """Benchmark criterion.""" - - name: str - summary: str - compare: str - margin: float - def __init__( self, - data_root: Path = Path("data"), - output_root: Path = Path("otx-benchmark"), + datasets: List[str], + data_root: str = "data", num_epoch: int = 0, num_repeat: int = 1, + train_params: dict | None = None, + track_resources: bool = False, eval_upto: str = "train", - tags: dict[str, str] | None = None, + output_root: str = "otx-benchmark", dry_run: bool = False, - deterministic: bool = False, - accelerator: str = "gpu", + tags: dict | None = None, + subset_dir_names: dict | None = None, ): + self.datasets = datasets self.data_root = data_root - self.output_root = output_root self.num_epoch = num_epoch self.num_repeat = num_repeat + self.train_params = train_params or {} + self.track_resources = track_resources self.eval_upto = eval_upto - self.tags = tags or {} + self.output_root = output_root self.dry_run = dry_run - self.deterministic = deterministic - self.accelerator = accelerator + self.tags = tags or {} + self.subset_dir_names = subset_dir_names or {"train": "", "val": "", "test": ""} def run( self, - model: Model, - dataset: Dataset, - criteria: list[Criterion], + model_id: str, + train_params: dict = {}, + tags: dict = {}, ) -> pd.DataFrame | None: - """Run configured benchmark with given dataset and model and return the result. + """Run configured benchmark with given model and return the result. Args: - model (Model): Target model settings - dataset (Dataset): Target dataset settings - criteria (list[Criterion]): Target criteria settings + model_id (str): Target model identifier + train_params (dict): Overrides global benchmark train params + tags (dict): Overrides global benchmark tags Retruns: pd.DataFrame | None: Table with benchmark metrics """ - run_name = f"{model.task}/{model.name}/{dataset.name}" - log.info(f"{run_name = }") - work_dir = self.output_root / run_name - data_root = self.data_root / dataset.path - - tags = { - "task": model.task, - "data_size": dataset.size, - "model": model.name, - "dataset": dataset.name, - **self.tags, - } - - num_repeat = dataset.num_repeat - if self.num_repeat > 0: - num_repeat = self.num_repeat # Override by global setting - - for seed in range(num_repeat): - sub_work_dir = work_dir / str(seed) - tags["seed"] = str(seed) - extra_metrics = {} - - # Train & test - command = [ - "otx", - "train", - "--config", - f"src/otx/recipe/{model.task}/{model.name}.yaml", - "--data_root", - str(data_root), - "--work_dir", - str(sub_work_dir), - "--model.num_classes", - str(dataset.num_classes), - "--data.config.data_format", - dataset.data_format, - "--engine.device", - self.accelerator, - ] - for key, value in dataset.extra_overrides.items(): - command.append(f"--{key}") - command.append(str(value)) - command.extend(["--seed", str(seed)]) - command.extend(["--deterministic", str(self.deterministic)]) - if self.num_epoch > 0: - command.extend(["--max_epochs", str(self.num_epoch)]) - start_time = time() - self._run_command(command) - extra_metrics["train/e2e_time"] = time() - start_time - self._rename_raw_data(work_dir=sub_work_dir / ".latest" / "train", replaces={"epoch": "train/epoch"}) - - command = [ - "otx", - "test", - "--work_dir", - str(sub_work_dir), - ] - self._run_command(command) - - # Export & test - if self.eval_upto in ["export", "optimize"]: - command = [ - "otx", - "export", - "--work_dir", - str(sub_work_dir), - ] - self._run_command(command) - - command = [ # NOTE: not working for h_label_cls. to be fixed - "otx", - "test", - "--config", - str(sub_work_dir / ".latest" / "export" / "configs.yaml"), - "--checkpoint", - str(sub_work_dir / ".latest" / "export" / "exported_model.xml"), - "--work_dir", - str(sub_work_dir), - ] - self._run_command(command) - - self._rename_raw_data(work_dir=sub_work_dir / ".latest" / "test", replaces={"test": "export"}) - - # Optimize & test - if self.eval_upto == "optimize": - command = [ - "otx", - "optimize", - # NOTE: auto config should be implemented - "--config", - f"src/otx/recipe/{model.task}/openvino_model.yaml", - "--checkpoint", - str(sub_work_dir / ".latest" / "export" / "exported_model.xml"), - "--work_dir", - str(sub_work_dir), - ] - self._run_command(command) - - command = [ - "otx", - "test", - # NOTE: auto config should be implemented - "--config", - f"src/otx/recipe/{model.task}/openvino_model.yaml", - "--checkpoint", - str(sub_work_dir / ".latest" / "optimize" / "optimized_model.xml"), - "--work_dir", - str(sub_work_dir), - ] - self._run_command(command) - - self._rename_raw_data(work_dir=sub_work_dir / ".latest" / "test", replaces={"test": "optimize"}) - - # Parse raw data into raw metrics - self._log_metrics(work_dir=sub_work_dir, tags=tags, criteria=criteria, extra_metrics=extra_metrics) - - # Force memory clean up - gc.collect() - - return self.load_result(work_dir) - - def _run_command(self, command: list[str]) -> None: + # Build config file + cfg = self._build_config(model_id, train_params, tags) + cfg_dir = Path(cfg["output_path"]) + cfg_dir.mkdir(parents=True, exist_ok=True) + cfg_path = cfg_dir / "cfg.yaml" + with open(cfg_path, "w") as cfg_file: + yaml.dump(cfg, cfg_file, indent=2) + cmd = [ + "python", + "tools/experiment.py", + "-f", + cfg_path, + ] if self.dry_run: - print(" ".join(command)) - else: - subprocess.run(command, check=True) # noqa: S603 - - def _log_metrics( - self, - work_dir: Path, - tags: dict[str, str], - criteria: list[Benchmark.Criterion], - extra_metrics: dict[str, Any], - ) -> None: - if not work_dir.exists(): - return - - # Load raw metrics - csv_files = work_dir.glob("**/metrics.csv") - raw_data = [pd.read_csv(csv_file) for csv_file in csv_files] - raw_data = pd.concat(raw_data, ignore_index=True) - for k, v in extra_metrics.items(): - raw_data[k] = v - - # Summarize - metrics = [] - for criterion in criteria: - if criterion.name not in raw_data: - continue - column = raw_data[criterion.name].dropna() - if len(column) == 0: - continue - if criterion.summary == "mean": - value = column[min(1, len(column) - 1) :].mean() # Drop 1st epoch if possible - elif criterion.summary == "max": - value = column.max() - elif criterion.summary == "min": - value = column.min() - else: - value = 0.0 - metrics.append(pd.Series([value], name=criterion.name)) - if len(metrics) == 0: - return - metrics = pd.concat(metrics, axis=1) - - # Write csv w/ tags - for k, v in tags.items(): - metrics[k] = v - metrics.to_csv(work_dir / "benchmark.raw.csv", index=False) - - def _rename_raw_data(self, work_dir: Path, replaces: dict[str, str]) -> None: - csv_files = work_dir.glob("**/metrics.csv") - for csv_file in csv_files: - data = pd.read_csv(csv_file) - for src_str, dst_str in replaces.items(): - data.columns = data.columns.str.replace(src_str, dst_str) - data.to_csv(csv_file, index=False) + cmd.append("-d") + # Run benchmark + check_run(cmd) + # Load result + result = self.load_result(cfg_dir) + return result @staticmethod - def load_result(result_path: Path) -> pd.DataFrame | None: + def load_result(result_path: str) -> pd.DataFrame | None: """Load benchmark results recursively and merge as pd.DataFrame. Args: - result_path (Path): Result directory or speicific file. + result_path (str): Result directory or speicific file. Retruns: pd.DataFrame: Table with benchmark metrics & options """ - if not result_path.exists(): - return None + # Search csv files + if os.path.isdir(result_path): + csv_file_paths = glob.glob(f"{result_path}/**/exp_summary.csv", recursive=True) + else: + csv_file_paths = [result_path] + results = [] # Load csv data - csv_files = result_path.glob("**/benchmark.raw.csv") if result_path.is_dir() else [result_path] - results = [pd.read_csv(csv_file) for csv_file in csv_files] + for csv_file_path in csv_file_paths: + result = pd.read_csv(csv_file_path) + # Append metadata if any + cfg_file_path = Path(csv_file_path).parent / "cfg.yaml" + if cfg_file_path.exists(): + with cfg_file_path.open("r") as cfg_file: + tags = yaml.safe_load(cfg_file).get("tags", {}) + for k, v in tags.items(): + result[k] = v + results.append(result) if len(results) == 0: return None - # Merge data + # Merge experiments data = pd.concat(results, ignore_index=True) + data["train_e2e_time"] = pd.to_timedelta(data["train_e2e_time"]).dt.total_seconds() # H:M:S str -> seconds # Average by unique group grouped = data.groupby(["task", "data_size", "model"]) aggregated = grouped.mean(numeric_only=True) @@ -317,5 +153,66 @@ def load_result(result_path: Path) -> pd.DataFrame | None: task_aggregated = task_grouped.mean(numeric_only=True) task_aggregated["data_size"] = "all" task_aggregated["model"] = "all" - task_aggregated = task_aggregated.set_index(["task", "data_size", "model"]) + task_aggregated.set_index(["task", "data_size", "model"], inplace=True) return pd.concat([aggregated, task_aggregated]) + + def _build_config( + self, + model_id: str, + train_params: dict = {}, + tags: dict = {}, + ) -> dict: + """Build config for tools/expeirment.py.""" + all_train_params = self.train_params.copy() + all_train_params.update(train_params) + all_tags = self.tags.copy() + all_tags.update(tags) + + cfg = {} + cfg["tags"] = all_tags # metadata + cfg["output_path"] = os.path.abspath(self.output_root) + cfg["constants"] = { + "dataroot": os.path.abspath(self.data_root), + } + cfg["variables"] = { + "model": [model_id], + "data": self.datasets, + } + cfg["repeat"] = self.num_repeat + cfg["command"] = [] + resource_param = "" + if self.track_resources: + resource_param = "--track-resource-usage all" + if self.num_epoch > 0: + self._set_num_epoch(model_id, all_train_params, self.num_epoch) + params_str = " ".join([f"--{k} {v}" for k, v in all_train_params.items()]) + cfg["command"].append( + "otx train ${model}" + " --train-data-roots ${dataroot}/${data}" + f"/{self.subset_dir_names['train']}" + " --val-data-roots ${dataroot}/${data}" + f"/{self.subset_dir_names['val']}" + " --deterministic" + f" {resource_param}" + f" params {params_str}" + ) + cfg["command"].append("otx eval --test-data-roots ${dataroot}/${data}" + f"/{self.subset_dir_names['test']}") + if self.eval_upto == "train": + return cfg + + cfg["command"].append("otx export") + cfg["command"].append("otx eval --test-data-roots ${dataroot}/${data}" + f"/{self.subset_dir_names['test']}") + if self.eval_upto == "export": + return cfg + + cfg["command"].append("otx optimize") + cfg["command"].append("otx eval --test-data-roots ${dataroot}/${data}" + f"/{self.subset_dir_names['test']}") + return cfg + + @staticmethod + def _set_num_epoch(model_id: str, train_params: dict, num_epoch: int): + """Set model specific num_epoch parameter.""" + if "padim" in model_id: + return # No configurable parameter for num_epoch + elif "stfpm" in model_id: + train_params["learning_parameters.max_epochs"] = num_epoch + else: + train_params["learning_parameters.num_iters"] = num_epoch diff --git a/tests/perf/conftest.py b/tests/perf/conftest.py index 5d6132bfca8..057ab25c37e 100644 --- a/tests/perf/conftest.py +++ b/tests/perf/conftest.py @@ -1,27 +1,27 @@ -# Copyright (C) 2024 Intel Corporation +# Copyright (C) 2023-2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations -import logging import os import platform import subprocess +import re +import shutil from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import TYPE_CHECKING -from urllib.parse import urlparse +from typing import Dict, List, Tuple, Callable, TYPE_CHECKING -import pytest from cpuinfo import get_cpu_info from mlflow.client import MlflowClient +import numpy as np +import pandas as pd +import pytest +import yaml -from .benchmark import Benchmark - -if TYPE_CHECKING: - import pandas as pd +from otx import __version__ as VERSION +from otx.api.entities.model_template import ModelCategory, ModelTemplate -log = logging.getLogger(__name__) +from .benchmark import OTXBenchmark def pytest_addoption(parser): @@ -84,12 +84,6 @@ def pytest_addoption(parser): default=False, help="Print OTX commands without execution.", ) - parser.addoption( - "--deterministic", - action="store_true", - default=False, - help="Turn on deterministic training.", - ) parser.addoption( "--user-name", type=str, @@ -97,88 +91,18 @@ def pytest_addoption(parser): help='Sign-off the user name who launched the regression tests this time, e.g., `--user-name "John Doe"`.', ) parser.addoption( - "--mlflow-tracking-uri", + "--mlflow-tracking-uri", # Currently set by MLFLOW_TRACKING_SERVER_URI env variable. To be fixed. type=str, help="URI for MLFlow Tracking server to store the regression test results.", ) -@pytest.fixture(scope="session") -def fxt_model_category(request: pytest.FixtureRequest) -> str: - """Model category to run the benchmark.""" - model_category = request.config.getoption("--model-category") - msg = f"{model_category = }" - log.info(msg) - return model_category - - -@pytest.fixture(scope="session") -def fxt_data_size(request: pytest.FixtureRequest) -> str: - """Data size to run the benchmark.""" - data_size = request.config.getoption("--data-size") - msg = f"{data_size = }" - log.info(msg) - return data_size - - -@pytest.fixture(scope="session") -def fxt_num_repeat(request: pytest.FixtureRequest) -> int: - """Number of repeated run with different random seed.""" - num_repeat = int(request.config.getoption("--num-repeat")) - msg = f"{num_repeat = }" - log.info(msg) - return num_repeat - - -@pytest.fixture(scope="session") -def fxt_num_epoch(request: pytest.FixtureRequest) -> int: - """Number of epochs to train models.""" - num_epoch = int(request.config.getoption("--num-epoch")) - msg = f"{num_epoch = }" - log.info(msg) - return num_epoch - - -@pytest.fixture(scope="session") -def fxt_eval_upto(request: pytest.FixtureRequest) -> str: - """Last operation to evaluate ~ train|export|optimize.""" - eval_upto = request.config.getoption("--eval-upto") - msg = f"{eval_upto = }" - log.info(msg) - return eval_upto - - -@pytest.fixture(scope="session") -def fxt_data_root(request: pytest.FixtureRequest) -> Path: - """Dataset root directory path.""" - data_root = Path(request.config.getoption("--data-root")) - msg = f"{data_root = }" - log.info(msg) - return data_root - - @pytest.fixture(scope="session") def fxt_current_date() -> str: tz = timezone(offset=timedelta(hours=9), name="Seoul") return datetime.now(tz=tz).strftime("%Y%m%d-%H%M%S") -@pytest.fixture(scope="session") -def fxt_output_root( - request: pytest.FixtureRequest, - tmp_path_factory: pytest.TempPathFactory, - fxt_current_date: str, -) -> Path: - """Output root + date + short commit hash.""" - output_root = request.config.getoption("--output-root") - if output_root is None: - output_root = tmp_path_factory.mktemp("otx-benchmark") - output_root = Path(output_root) / fxt_current_date - msg = f"{output_root = }" - log.info(msg) - return output_root - - @pytest.fixture(scope="session") def fxt_version_tags(fxt_current_date: str) -> dict[str, str]: """Version / branch / commit info.""" @@ -186,11 +110,15 @@ def fxt_version_tags(fxt_current_date: str) -> dict[str, str]: version_str = otx.__version__ try: - branch_str = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode("ascii").strip() # noqa: S603, S607 + branch_str = ( + subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode("ascii").strip() + ) # noqa: S603, S607 except Exception: branch_str = os.environ.get("GH_CTX_REF_NAME", "unknown") try: - commit_str = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode("ascii").strip() # noqa: S603, S607 + commit_str = ( + subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode("ascii").strip() + ) # noqa: S603, S607 except Exception: commit_str = os.environ.get("GH_CTX_SHA", "unknown") version_tags = { @@ -199,85 +127,15 @@ def fxt_version_tags(fxt_current_date: str) -> dict[str, str]: "commit": commit_str, "date": fxt_current_date, } - msg = f"{version_tags = }" - log.info(msg) return version_tags @pytest.fixture(scope="session") -def fxt_summary_csv(request: pytest.FixtureRequest, fxt_output_root: Path) -> Path: - """Path to benchmark result summary csv file.""" - summary_csv = request.config.getoption("--summary-csv") - summary_csv = fxt_output_root / "benchmark-summary.csv" if summary_csv is None else Path(summary_csv) - msg = f"{summary_csv = }" - log.info(msg) - return summary_csv - - -@pytest.fixture(scope="session") -def fxt_dry_run(request: pytest.FixtureRequest) -> str: - """Option to print OTX commands without execution.""" - dry_run = request.config.getoption("--dry-run") - msg = f"{dry_run = }" - log.info(msg) - return dry_run - - -@pytest.fixture(scope="session") -def fxt_deterministic(request: pytest.FixtureRequest) -> str: - """Option to turn on deterministic training.""" - deterministic = request.config.getoption("--deterministic") - msg = f"{deterministic = }" - log.info(msg) - return deterministic - - -@pytest.fixture(scope="session") -def fxt_user_name(request: pytest.FixtureRequest) -> str: - """User name to sign off the regression test execution.""" - user_name = request.config.getoption("--user-name") - msg = f"{user_name = }" - log.info(msg) - return user_name - - -@pytest.fixture(scope="session") -def fxt_mlflow_client(request: pytest.FixtureRequest) -> MlflowClient: - """MLFLow tracking client.""" - mlflow_tracking_uri = urlparse( - request.config.getoption("--mlflow-tracking-uri"), - ).geturl() - msg = f"{mlflow_tracking_uri = }" - log.info(msg) - if mlflow_tracking_uri: - return MlflowClient(mlflow_tracking_uri) - return None - - -@pytest.fixture() -def fxt_model(request: pytest.FixtureRequest, fxt_model_category) -> Benchmark.Model: - """Skip models according to user options.""" - model: Benchmark.Model = request.param - if fxt_model_category == "default" and model.category == "other": - pytest.skip(f"{model.category} category model") - return model - - -@pytest.fixture() -def fxt_dataset(request: pytest.FixtureRequest, fxt_data_size) -> Benchmark.Data: - """Skip datasets according to user options.""" - dataset: Benchmark.Dataset = request.param - if fxt_data_size not in {"all", dataset.size}: - pytest.skip(f"{dataset.size} size dataset") - return dataset - - -@pytest.fixture(scope="session") -def fxt_tags(fxt_user_name: str, fxt_version_tags: dict[str, str]) -> dict[str, str]: +def fxt_tags(request: pytest.FixtureRequest, fxt_version_tags: dict[str, str]) -> dict[str, str]: """Tag fields to record the machine and user executing this perf test.""" tags = { **fxt_version_tags, - "user_name": fxt_user_name, + "user_name": request.config.getoption("--user-name"), "machine_name": platform.node(), "cpu_info": get_cpu_info()["brand_raw"], "accelerator_info": subprocess.check_output( @@ -286,59 +144,72 @@ def fxt_tags(fxt_user_name: str, fxt_version_tags: dict[str, str]) -> dict[str, .decode() .strip(), } - msg = f"{tags = }" - log.info(msg) + print(f"{tags = }") return tags -@pytest.fixture() -def fxt_benchmark( - fxt_data_root: Path, - fxt_output_root: Path, - fxt_num_epoch: int, - fxt_num_repeat: int, - fxt_eval_upto: str, - fxt_tags: dict[str, str], - fxt_dry_run: bool, - fxt_deterministic: bool, - fxt_accelerator: str, -) -> Benchmark: - """Configure benchmark.""" - return Benchmark( - data_root=fxt_data_root, - output_root=fxt_output_root, - num_epoch=fxt_num_epoch, - num_repeat=fxt_num_repeat, - eval_upto=fxt_eval_upto, - tags=fxt_tags, - dry_run=fxt_dry_run, - deterministic=fxt_deterministic, - accelerator=fxt_accelerator, - ) +@pytest.fixture(scope="session") +def fxt_output_root( + request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory, fxt_current_date: str +) -> Path: + """Output root + date + short commit hash.""" + output_root = request.config.getoption("--output-root") + if output_root is None: + output_root = tmp_path_factory.mktemp("otx-benchmark") + return Path(output_root) / fxt_current_date -@pytest.fixture(scope="session", autouse=True) -def fxt_benchmark_summary( - fxt_output_root: Path, - fxt_summary_csv: Path, - fxt_mlflow_client: MlflowClient, - fxt_tags: dict[str, str], -): - """Summarize all results at the end of test session.""" - yield - all_results = Benchmark.load_result(fxt_output_root) - if all_results is not None: - print("=" * 20, "[Benchmark summary]") - print(all_results) - fxt_summary_csv.parent.mkdir(parents=True, exist_ok=True) - all_results.to_csv(fxt_summary_csv) - print(f" -> Saved to {fxt_summary_csv}.") +@pytest.fixture +def fxt_model_id(request: pytest.FixtureRequest) -> str: + """Skip by model category.""" + model_category: str = request.config.getoption("--model-category") + model_template: ModelTemplate = request.param + if model_category == "default": + if model_template.model_category == ModelCategory.OTHER: + pytest.skip(f"{model_template.model_category} category model") + return model_template.model_template_id + + +@pytest.fixture +def fxt_benchmark(request: pytest.FixtureRequest, fxt_output_root: Path, fxt_tags: dict[str, str]) -> OTXBenchmark: + """Configure benchmark.""" + # Skip by dataset size + data_size_option: str = request.config.getoption("--data-size") + data_size: str = request.param[0] + if data_size_option != "all": + if data_size_option != data_size: + pytest.skip(f"{data_size} datasets") + + # Options + cfg: dict = request.param[1].copy() + + tags = cfg.get("tags", {}) + tags["data_size"] = data_size + tags.update(fxt_tags) + cfg["tags"] = tags + + num_epoch_override: int = int(request.config.getoption("--num-epoch")) + if num_epoch_override > 0: # 0: use default + cfg["num_epoch"] = num_epoch_override + + num_repeat_override: int = int(request.config.getoption("--num-repeat")) + if num_repeat_override > 0: # 0: use default + cfg["num_repeat"] = num_repeat_override + + cfg["eval_upto"] = request.config.getoption("--eval-upto") + cfg["data_root"] = request.config.getoption("--data-root") + cfg["output_root"] = str(fxt_output_root) + cfg["dry_run"] = request.config.getoption("--dry-run") + + # Create benchmark + benchmark = OTXBenchmark( + **cfg, + ) - if fxt_mlflow_client: - _log_benchmark_results_to_mlflow(all_results, fxt_mlflow_client, fxt_tags) + return benchmark -def _log_benchmark_results_to_mlflow(results: pd.DataFrame, client: MlflowClient, tags: dict[str, str]) -> None: +def log_perf_results_to_mlflow(results: pd.DataFrame, tags: dict[str, str], client: MlflowClient): for index, data in results.iterrows(): task, data_size, model = index exp_name = f"[Benchmark] {task} | {model} | {data_size}" @@ -357,23 +228,94 @@ def _log_benchmark_results_to_mlflow(results: pd.DataFrame, client: MlflowClient run = client.create_run(exp_id, run_name=run_name, tags=run_tags) run_metrics = {k: v for k, v in data.items() if not isinstance(v, str)} for k, v in run_metrics.items(): + k = k.replace("(", "_") + k = k.replace(")", "") + k = k.replace("%", "percentage") client.log_metric(run.info.run_id, k, v) -class PerfTestBase: - """Base perf test structure.""" - - def _test_perf( - self, - model: Benchmark.Model, - dataset: Benchmark.Dataset, - benchmark: Benchmark, - criteria: list[Benchmark.Criterion], - ) -> None: - result = benchmark.run( - model=model, - dataset=dataset, - criteria=criteria, - ) - print(result) - # Check results +@pytest.fixture(scope="session", autouse=True) +def fxt_benchmark_summary( + request: pytest.FixtureRequest, fxt_output_root: Path, fxt_tags: dict[str, str], fxt_mlflow_client: MlflowClient +): + """Summarize all results at the end of test session.""" + yield + all_results = OTXBenchmark.load_result(fxt_output_root) + if all_results is not None: + print("=" * 20, "[Benchmark summary]") + print(all_results) + output_path = request.config.getoption("--summary-csv") + if not output_path: + output_path = fxt_output_root / "benchmark-summary.csv" + all_results.to_csv(output_path) + print(f" -> Saved to {output_path}.") + + if fxt_mlflow_client is None: + print( + "Tracking server is not configured. for logging results, " + "set 'MLFLOW_TRACKING_SERVER_URI' environment variable to server URI ." + ) + return + + # logging to the mlflow for 'develop' or 'releases/x.x.x' branch + working_branch = fxt_tags["branch"] + if working_branch == "develop" or bool(re.match("^releases/[0-9]+\.[0-9]+\.[0-9]+$", working_branch)): + try: + log_perf_results_to_mlflow(all_results, fxt_tags, fxt_mlflow_client) + except Exception as e: + print("MLFlow loging failed: ", e) + + if os.environ.get("BENCHMARK_RESULTS_CLEAR", False): + shutil.rmtree(fxt_output_root) + + +@pytest.fixture(scope="session") +def fxt_benchmark_reference() -> pd.DataFrame | None: + """Load reference benchmark results with index.""" + ref = pd.read_csv(Path(__file__).parent.resolve() / "benchmark-reference.csv") + if ref is not None: + ref.set_index(["task", "data_size", "model"], inplace=True) + return ref + + +@pytest.fixture(scope="session") +def fxt_check_benchmark_result(fxt_benchmark_reference: pd.DataFrame | None) -> Callable: + """Return result checking function with reference data.""" + + def check_benchmark_result(result: pd.DataFrame, key: Tuple, checks: List[Dict]): + if fxt_benchmark_reference is None: + print("No benchmark references loaded. Skipping result checking.") + return + + if result is None: + return + + def get_entry(data: pd.DataFrame, key: Tuple) -> pd.Series: + if key in data.index: + return data.loc[key] + return None + + target_entry = get_entry(fxt_benchmark_reference, key) + if target_entry is None: + print(f"No benchmark reference for {key} loaded. Skipping result checking.") + return + + result_entry = get_entry(result, key) + assert result_entry is not None + + def compare(name: str, op: str, margin: float): + if name not in result_entry or result_entry[name] is None or np.isnan(result_entry[name]): + return + if name not in target_entry or target_entry[name] is None or np.isnan(target_entry[name]): + return + if op == "==": + assert abs(result_entry[name] - target_entry[name]) < target_entry[name] * margin + elif op == "<": + assert result_entry[name] < target_entry[name] * (1.0 + margin) + elif op == ">": + assert result_entry[name] > target_entry[name] * (1.0 - margin) + + for check in checks: + compare(**check) + + return check_benchmark_result diff --git a/tests/perf/test_anomaly.py b/tests/perf/test_anomaly.py index fcb06875d61..2031201aa95 100644 --- a/tests/perf/test_anomaly.py +++ b/tests/perf/test_anomaly.py @@ -1,15 +1,224 @@ -# Copyright (C) 2024 Intel Corporation +"""OTX Anomaly perfomance tests.""" + +# Copyright (C) 2023-2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -"""OTX anomaly perfomance benchmark tests.""" -from __future__ import annotations +import pytest -from .conftest import PerfTestBase +from otx.cli.registry import Registry +from typing import Callable +from .benchmark import OTXBenchmark -class TestPerfAnomalyClassification(PerfTestBase): +class TestPerfAnomalyClassification: """Benchmark anomaly classification.""" - def test_dummay(self): - pass + MODEL_TEMPLATES = Registry(f"src/otx/algorithms").filter(task_type="ANOMALY_CLASSIFICATION").templates + MODEL_IDS = [template.model_template_id for template in MODEL_TEMPLATES] + + BENCHMARK_CONFIGS = { + "small": { + "tags": { + "task": "anomaly_classification", + }, + "datasets": [ + "anomaly/mvtec/bottle_small/1", + "anomaly/mvtec/bottle_small/2", + "anomaly/mvtec/bottle_small/3", + ], + "num_repeat": 5, + }, + "medium": { + "tags": { + "task": "anomaly_classification", + }, + "datasets": [ + "anomaly/mvtec/wood_medium", + ], + "num_repeat": 5, + }, + "large": { + "tags": { + "task": "anomaly_classification", + }, + "datasets": [ + "anomaly/mvtec/hazelnut_large", + ], + "num_repeat": 5, + }, + } + + @pytest.mark.parametrize("fxt_model_id", MODEL_TEMPLATES, ids=MODEL_IDS, indirect=True) + @pytest.mark.parametrize("fxt_benchmark", BENCHMARK_CONFIGS.items(), ids=BENCHMARK_CONFIGS.keys(), indirect=True) + def test_perf(self, fxt_model_id: str, fxt_benchmark: OTXBenchmark, fxt_check_benchmark_result: Callable): + """Benchmark performance metrics.""" + result = fxt_benchmark.run(model_id=fxt_model_id) + fxt_check_benchmark_result( + result, + key=(fxt_benchmark.tags["task"], fxt_benchmark.tags["data_size"], fxt_model_id), + checks=[ + { + "name": "f-measure(train)", + "op": ">", + "margin": 0.1, + }, + { + "name": "f-measure(export)", + "op": ">", + "margin": 0.1, + }, + { + "name": "f-measure(optimize)", + "op": ">", + "margin": 0.1, + }, + { + "name": "train_e2e_time", + "op": "<", + "margin": 0.1, + }, + ], + ) + + +class TestPerfAnomalyDetection: + """Benchmark anomaly detection.""" + + MODEL_TEMPLATES = Registry(f"src/otx/algorithms").filter(task_type="ANOMALY_DETECTION").templates + MODEL_IDS = [template.model_template_id for template in MODEL_TEMPLATES] + + BENCHMARK_CONFIGS = { + "small": { + "tags": { + "task": "anomaly_detection", + }, + "datasets": [ + "anomaly/mvtec/bottle_small/1", + "anomaly/mvtec/bottle_small/2", + "anomaly/mvtec/bottle_small/3", + ], + "num_repeat": 5, + }, + "medium": { + "tags": { + "task": "anomaly_detection", + }, + "datasets": [ + "anomaly/mvtec/wood_medium", + ], + "num_repeat": 5, + }, + "large": { + "tags": { + "task": "anomaly_detection", + }, + "datasets": [ + "anomaly/mvtec/hazelnut_large", + ], + "num_repeat": 5, + }, + } + + @pytest.mark.parametrize("fxt_model_id", MODEL_TEMPLATES, ids=MODEL_IDS, indirect=True) + @pytest.mark.parametrize("fxt_benchmark", BENCHMARK_CONFIGS.items(), ids=BENCHMARK_CONFIGS.keys(), indirect=True) + def test_perf(self, fxt_model_id: str, fxt_benchmark: OTXBenchmark, fxt_check_benchmark_result: Callable): + """Benchmark performance metrics.""" + result = fxt_benchmark.run(model_id=fxt_model_id) + fxt_check_benchmark_result( + result, + key=(fxt_benchmark.tags["task"], fxt_benchmark.tags["data_size"], fxt_model_id), + checks=[ + { + "name": "f-measure(train)", + "op": ">", + "margin": 0.1, + }, + { + "name": "f-measure(export)", + "op": ">", + "margin": 0.1, + }, + { + "name": "f-measure(optimize)", + "op": ">", + "margin": 0.1, + }, + { + "name": "train_e2e_time", + "op": "<", + "margin": 0.1, + }, + ], + ) + + +class TestPerfAnomalySegmentation: + """Benchmark anomaly segmentation.""" + + MODEL_TEMPLATES = Registry(f"src/otx/algorithms").filter(task_type="ANOMALY_SEGMENTATION").templates + MODEL_IDS = [template.model_template_id for template in MODEL_TEMPLATES] + + BENCHMARK_CONFIGS = { + "small": { + "tags": { + "task": "anomaly_segmentation", + }, + "datasets": [ + "anomaly/mvtec/bottle_small/1", + "anomaly/mvtec/bottle_small/2", + "anomaly/mvtec/bottle_small/3", + ], + "num_repeat": 5, + }, + "medium": { + "tags": { + "task": "anomaly_segmentation", + }, + "datasets": [ + "anomaly/mvtec/wood_medium", + ], + "num_repeat": 5, + }, + "large": { + "tags": { + "task": "anomaly_segmentation", + }, + "datasets": [ + "anomaly/mvtec/hazelnut_large", + ], + "num_repeat": 5, + }, + } + + @pytest.mark.parametrize("fxt_model_id", MODEL_TEMPLATES, ids=MODEL_IDS, indirect=True) + @pytest.mark.parametrize("fxt_benchmark", BENCHMARK_CONFIGS.items(), ids=BENCHMARK_CONFIGS.keys(), indirect=True) + def test_perf(self, fxt_model_id: str, fxt_benchmark: OTXBenchmark, fxt_check_benchmark_result: Callable): + """Benchmark performance metrics.""" + result = fxt_benchmark.run(model_id=fxt_model_id) + fxt_check_benchmark_result( + result, + key=(fxt_benchmark.tags["task"], fxt_benchmark.tags["data_size"], fxt_model_id), + checks=[ + { + "name": "f-measure(train)", + "op": ">", + "margin": 0.1, + }, + { + "name": "f-measure(export)", + "op": ">", + "margin": 0.1, + }, + { + "name": "f-measure(optimize)", + "op": ">", + "margin": 0.1, + }, + { + "name": "train_e2e_time", + "op": "<", + "margin": 0.1, + }, + ], + ) diff --git a/tests/perf/test_classification.py b/tests/perf/test_classification.py index 236fa07614e..fa44e5e9578 100644 --- a/tests/perf/test_classification.py +++ b/tests/perf/test_classification.py @@ -1,255 +1,294 @@ -# Copyright (C) 2024 Intel Corporation +"""OTX Classification perfomance tests.""" + +# Copyright (C) 2023-2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -"""OTX classification perfomance benchmark tests.""" -from __future__ import annotations +import pytest -from pathlib import Path +from otx.cli.registry import Registry +from typing import Callable +from .benchmark import OTXBenchmark -import pytest -from .benchmark import Benchmark -from .conftest import PerfTestBase +MODEL_TEMPLATES = Registry(f"src/otx/algorithms").filter(task_type="CLASSIFICATION").templates +MODEL_IDS = [template.model_template_id for template in MODEL_TEMPLATES] -class TestPerfSingleLabelClassification(PerfTestBase): +class TestPerfSingleLabelClassification: """Benchmark single-label classification.""" - MODEL_TEST_CASES = [ # noqa: RUF012 - Benchmark.Model(task="classification/multi_class_cls", name="efficientnet_b0_light", category="speed"), - Benchmark.Model(task="classification/multi_class_cls", name="efficientnet_v2_light", category="balance"), - Benchmark.Model(task="classification/multi_class_cls", name="mobilenet_v3_large_light", category="accuracy"), - Benchmark.Model(task="classification/multi_class_cls", name="otx_deit_tiny", category="other"), - Benchmark.Model(task="classification/multi_class_cls", name="otx_dino_v2", category="other"), - ] - - DATASET_TEST_CASES = [ - Benchmark.Dataset( - name=f"multiclass_CUB_small_{idx}", - path=Path("multiclass_classification/multiclass_CUB_small") / f"{idx}", - size="small", - data_format="imagenet_with_subset_dirs", - num_classes=2, - num_repeat=5, - extra_overrides={}, - ) - for idx in (1, 2, 3) - ] + [ - Benchmark.Dataset( - name="multiclass_CUB_medium", - path=Path("multiclass_classification/multiclass_CUB_medium"), - size="medium", - data_format="imagenet_with_subset_dirs", - num_classes=67, - num_repeat=5, - extra_overrides={}, - ), - Benchmark.Dataset( - name="multiclass_food101_large", - path=Path("multiclass_classification/multiclass_food101_large"), - size="large", - data_format="imagenet_with_subset_dirs", - num_classes=20, - num_repeat=5, - extra_overrides={}, - ), - ] - - BENCHMARK_CRITERIA = [ # noqa: RUF012 - Benchmark.Criterion(name="train/epoch", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="train/e2e_time", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="val/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="test/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="export/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="optimize/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="train/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="test/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="export/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="optimize/iter_time", summary="mean", compare="<", margin=0.1), - ] - - @pytest.mark.parametrize( - "fxt_model", - MODEL_TEST_CASES, - ids=lambda model: model.name, - indirect=True, - ) - @pytest.mark.parametrize( - "fxt_dataset", - DATASET_TEST_CASES, - ids=lambda dataset: dataset.name, - indirect=True, - ) - def test_perf( - self, - fxt_model: Benchmark.Model, - fxt_dataset: Benchmark.Dataset, - fxt_benchmark: Benchmark, - ): - self._test_perf( - model=fxt_model, - dataset=fxt_dataset, - benchmark=fxt_benchmark, - criteria=self.BENCHMARK_CRITERIA, + BENCHMARK_CONFIGS = { + "small": { + "tags": { + "task": "single_label_classification", + }, + "datasets": [ + "classification/single_label/multiclass_CUB_small/1", + "classification/single_label/multiclass_CUB_small/2", + "classification/single_label/multiclass_CUB_small/3", + ], + "num_repeat": 5, + }, + "medium": { + "tags": { + "task": "single_label_classification", + }, + "datasets": [ + "classification/single_label/multiclass_CUB_medium", + ], + "num_repeat": 5, + }, + "large": { + "tags": { + "task": "single_label_classification", + }, + "datasets": [ + "classification/single_label/multiclass_food101_large", + ], + "num_repeat": 5, + }, + } + + @pytest.mark.parametrize("fxt_model_id", MODEL_TEMPLATES, ids=MODEL_IDS, indirect=True) + @pytest.mark.parametrize("fxt_benchmark", BENCHMARK_CONFIGS.items(), ids=BENCHMARK_CONFIGS.keys(), indirect=True) + def test_perf(self, fxt_model_id: str, fxt_benchmark: OTXBenchmark, fxt_check_benchmark_result: Callable): + """Benchmark performance metrics.""" + result = fxt_benchmark.run(model_id=fxt_model_id) + fxt_check_benchmark_result( + result, + key=(fxt_benchmark.tags["task"], fxt_benchmark.tags["data_size"], fxt_model_id), + checks=[ + { + "name": "Accuracy(train)", + "op": ">", + "margin": 0.1, + }, + { + "name": "Accuracy(export)", + "op": ">", + "margin": 0.1, + }, + { + "name": "Accuracy(optimize)", + "op": ">", + "margin": 0.1, + }, + { + "name": "epoch", + "op": "<", + "margin": 0.1, + }, + { + "name": "train_e2e_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_data_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_iter_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(export)", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(optimize)", + "op": "<", + "margin": 0.1, + }, + ], ) -class TestPerfMultiLabelClassification(PerfTestBase): +class TestPerfMultiLabelClassification: """Benchmark multi-label classification.""" - MODEL_TEST_CASES = [ # noqa: RUF012 - Benchmark.Model(task="classification/multi_label_cls", name="efficientnet_b0_light", category="speed"), - Benchmark.Model(task="classification/multi_label_cls", name="efficientnet_v2_light", category="balance"), - Benchmark.Model(task="classification/multi_label_cls", name="mobilenet_v3_large_light", category="accuracy"), - Benchmark.Model(task="classification/multi_label_cls", name="otx_deit_tiny", category="other"), - ] - - DATASET_TEST_CASES = [ - Benchmark.Dataset( - name=f"multilabel_CUB_small_{idx}", - path=Path("multilabel_classification/multilabel_CUB_small") / f"{idx}", - size="small", - data_format="datumaro", - num_classes=3, - num_repeat=5, - extra_overrides={}, - ) - for idx in (1, 2, 3) - ] + [ - Benchmark.Dataset( - name="multilabel_CUB_medium", - path=Path("multilabel_classification/multilabel_CUB_medium"), - size="medium", - data_format="datumaro", - num_classes=68, - num_repeat=5, - extra_overrides={}, - ), - Benchmark.Dataset( - name="multilabel_food101_large", - path=Path("multilabel_classification/multilabel_food101_large"), - size="large", - data_format="datumaro", - num_classes=21, - num_repeat=5, - extra_overrides={}, - ), - ] - - BENCHMARK_CRITERIA = [ # noqa: RUF012 - Benchmark.Criterion(name="train/epoch", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="train/e2e_time", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="val/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="test/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="export/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="optimize/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="train/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="test/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="export/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="optimize/iter_time", summary="mean", compare="<", margin=0.1), - ] - - @pytest.mark.parametrize( - "fxt_model", - MODEL_TEST_CASES, - ids=lambda model: model.name, - indirect=True, - ) - @pytest.mark.parametrize( - "fxt_dataset", - DATASET_TEST_CASES, - ids=lambda dataset: dataset.name, - indirect=True, - ) - def test_perf( - self, - fxt_model: Benchmark.Model, - fxt_dataset: Benchmark.Dataset, - fxt_benchmark: Benchmark, - ): - self._test_perf( - model=fxt_model, - dataset=fxt_dataset, - benchmark=fxt_benchmark, - criteria=self.BENCHMARK_CRITERIA, + BENCHMARK_CONFIGS = { + "small": { + "tags": { + "task": "multi_label_classification", + }, + "datasets": [ + "classification/multi_label/multilabel_CUB_small/1", + "classification/multi_label/multilabel_CUB_small/2", + "classification/multi_label/multilabel_CUB_small/3", + ], + "num_repeat": 5, + }, + "medium": { + "tags": { + "task": "multi_label_classification", + }, + "datasets": [ + "classification/multi_label/multilabel_CUB_medium", + ], + "num_repeat": 5, + }, + "large": { + "tags": { + "task": "multi_label_classification", + }, + "datasets": [ + "classification/multi_label/multilabel_food101_large", + ], + "num_repeat": 5, + }, + } + + @pytest.mark.parametrize("fxt_model_id", MODEL_TEMPLATES, ids=MODEL_IDS, indirect=True) + @pytest.mark.parametrize("fxt_benchmark", BENCHMARK_CONFIGS.items(), ids=BENCHMARK_CONFIGS.keys(), indirect=True) + def test_perf(self, fxt_model_id: str, fxt_benchmark: OTXBenchmark, fxt_check_benchmark_result: Callable): + """Benchmark performance metrics.""" + result = fxt_benchmark.run(model_id=fxt_model_id) + fxt_check_benchmark_result( + result, + key=(fxt_benchmark.tags["task"], fxt_benchmark.tags["data_size"], fxt_model_id), + checks=[ + { + "name": "Accuracy(train)", + "op": ">", + "margin": 0.1, + }, + { + "name": "Accuracy(export)", + "op": ">", + "margin": 0.1, + }, + { + "name": "Accuracy(optimize)", + "op": ">", + "margin": 0.1, + }, + { + "name": "epoch", + "op": "<", + "margin": 0.1, + }, + { + "name": "train_e2e_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_data_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_iter_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(export)", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(optimize)", + "op": "<", + "margin": 0.1, + }, + ], ) -class TestPerfHierarchicalLabelClassification(PerfTestBase): - """Benchmark hierarchical-label classification.""" - - MODEL_TEST_CASES = [ # noqa: RUF012 - Benchmark.Model(task="classification/h_label_cls", name="efficientnet_b0_light", category="speed"), - Benchmark.Model(task="classification/h_label_cls", name="efficientnet_v2_light", category="balance"), - Benchmark.Model(task="classification/h_label_cls", name="mobilenet_v3_large_light", category="accuracy"), - Benchmark.Model(task="classification/h_label_cls", name="otx_deit_tiny", category="other"), - ] - - DATASET_TEST_CASES = [ - Benchmark.Dataset( - name=f"hlabel_CUB_small_{idx}", - path=Path("hlabel_classification/hlabel_CUB_small") / f"{idx}", - size="small", - data_format="datumaro", - num_classes=6, - num_repeat=5, - extra_overrides={ - "model.num_multiclass_heads": "3", - "model.num_multilabel_classes": "0", +class TestPerfHierarchicalLabelClassification: + """Benchmark hierarchcial-label classification.""" + + BENCHMARK_CONFIGS = { + "small": { + "tags": { + "task": "hierarchical_label_classification", }, - ) - for idx in (1, 2, 3) - ] + [ - Benchmark.Dataset( - name="hlabel_CUB_medium", - path=Path("hlabel_classification/hlabel_CUB_medium"), - size="medium", - data_format="datumaro", - num_classes=102, - num_repeat=5, - extra_overrides={ - "model.num_multiclass_heads": "23", - "model.num_multilabel_classes": "0", + "datasets": [ + "classification/h_label/h_label_CUB_small/1", + "classification/h_label/h_label_CUB_small/2", + "classification/h_label/h_label_CUB_small/3", + ], + "num_repeat": 5, + }, + "medium": { + "tags": { + "task": "hierarchical_label_classification", }, - ), - # Add large dataset - ] - - BENCHMARK_CRITERIA = [ # noqa: RUF012 - Benchmark.Criterion(name="train/epoch", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="train/e2e_time", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="val/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="test/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="export/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="optimize/accuracy", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="train/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="test/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="export/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="optimize/iter_time", summary="mean", compare="<", margin=0.1), - ] - - @pytest.mark.parametrize( - "fxt_model", - MODEL_TEST_CASES, - ids=lambda model: model.name, - indirect=True, - ) - @pytest.mark.parametrize( - "fxt_dataset", - DATASET_TEST_CASES, - ids=lambda dataset: dataset.name, - indirect=True, - ) - def test_perf( - self, - fxt_model: Benchmark.Model, - fxt_dataset: Benchmark.Dataset, - fxt_benchmark: Benchmark, - ): - self._test_perf( - model=fxt_model, - dataset=fxt_dataset, - benchmark=fxt_benchmark, - criteria=self.BENCHMARK_CRITERIA, + "datasets": [ + "classification/h_label/h_label_CUB_medium", + ], + "num_repeat": 5, + }, + # TODO: Add large dataset + # "large": { + # "tags": { + # "task": "hierarchical_label_classification", + # }, + # "datasets": [ + # ], + # "num_repeat": 5, + # }, + } + + @pytest.mark.parametrize("fxt_model_id", MODEL_TEMPLATES, ids=MODEL_IDS, indirect=True) + @pytest.mark.parametrize("fxt_benchmark", BENCHMARK_CONFIGS.items(), ids=BENCHMARK_CONFIGS.keys(), indirect=True) + def test_perf(self, fxt_model_id: str, fxt_benchmark: OTXBenchmark, fxt_check_benchmark_result: Callable): + """Benchmark performance metrics.""" + result = fxt_benchmark.run(model_id=fxt_model_id) + fxt_check_benchmark_result( + result, + key=(fxt_benchmark.tags["task"], fxt_benchmark.tags["data_size"], fxt_model_id), + checks=[ + { + "name": "Accuracy(train)", + "op": ">", + "margin": 0.1, + }, + { + "name": "Accuracy(export)", + "op": ">", + "margin": 0.1, + }, + { + "name": "Accuracy(optimize)", + "op": ">", + "margin": 0.1, + }, + { + "name": "epoch", + "op": "<", + "margin": 0.1, + }, + { + "name": "train_e2e_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_data_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_iter_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(export)", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(optimize)", + "op": "<", + "margin": 0.1, + }, + ], ) diff --git a/tests/perf/test_detection.py b/tests/perf/test_detection.py index b86372862f2..65479399d72 100644 --- a/tests/perf/test_detection.py +++ b/tests/perf/test_detection.py @@ -1,112 +1,108 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""OTX object detection perfomance benchmark tests.""" +"""OTX Detection perfomance tests.""" -from __future__ import annotations +# Copyright (C) 2023-2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 -from pathlib import Path import pytest -from .benchmark import Benchmark -from .conftest import PerfTestBase +from otx.cli.registry import Registry +from typing import Callable +from .benchmark import OTXBenchmark -class TestPerfObjectDetection(PerfTestBase): - """Benchmark object detection.""" +MODEL_TEMPLATES = Registry(f"src/otx/algorithms").filter(task_type="DETECTION").templates +MODEL_IDS = [template.model_template_id for template in MODEL_TEMPLATES] - MODEL_TEST_CASES = [ # noqa: RUF012 - Benchmark.Model(task="detection", name="atss_mobilenetv2", category="accuracy"), - Benchmark.Model(task="detection", name="atss_resnext101", category="other"), - Benchmark.Model(task="detection", name="ssd_mobilenetv2", category="balance"), - Benchmark.Model(task="detection", name="yolox_tiny", category="speed"), - Benchmark.Model(task="detection", name="yolox_s", category="other"), - Benchmark.Model(task="detection", name="yolox_l", category="other"), - Benchmark.Model(task="detection", name="yolox_x", category="other"), - ] - DATASET_TEST_CASES = [ - Benchmark.Dataset( - name=f"pothole_small_{idx}", - path=Path("detection/pothole_small") / f"{idx}", - size="small", - data_format="coco", - num_classes=1, - num_repeat=5, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", +class TestPerfDetection: + """Benchmark basic object detection.""" + + BENCHMARK_CONFIGS = { + "small": { + "tags": { + "task": "detection", }, - ) - for idx in (1, 2, 3) - ] + [ - Benchmark.Dataset( - name="pothole_medium", - path=Path("detection/pothole_medium"), - size="medium", - data_format="coco", - num_classes=1, - num_repeat=5, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", + "datasets": [ + "detection/pothole_small/1", + "detection/pothole_small/2", + "detection/pothole_small/3", + ], + "num_repeat": 5, + }, + "medium": { + "tags": { + "task": "detection", }, - ), - Benchmark.Dataset( - name="vitens_large", - path=Path("detection/vitens_large"), - size="large", - data_format="coco", - num_classes=1, - num_repeat=5, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", + "datasets": [ + "detection/pothole_medium", + ], + "num_repeat": 5, + }, + "large": { + "tags": { + "task": "detection", }, - ), - ] - - BENCHMARK_CRITERIA = [ # noqa: RUF012 - Benchmark.Criterion(name="train/epoch", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="train/e2e_time", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="val/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="test/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="export/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="optimize/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="train/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="test/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="export/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="optimize/iter_time", summary="mean", compare="<", margin=0.1), - ] + "datasets": [ + "detection/vitens_large", + ], + "num_repeat": 5, + }, + } - @pytest.mark.parametrize( - "fxt_model", - MODEL_TEST_CASES, - ids=lambda model: model.name, - indirect=True, - ) - @pytest.mark.parametrize( - "fxt_dataset", - DATASET_TEST_CASES, - ids=lambda dataset: dataset.name, - indirect=True, - ) - def test_perf( - self, - fxt_model: Benchmark.Model, - fxt_dataset: Benchmark.Dataset, - fxt_benchmark: Benchmark, - ): - self._test_perf( - model=fxt_model, - dataset=fxt_dataset, - benchmark=fxt_benchmark, - criteria=self.BENCHMARK_CRITERIA, + @pytest.mark.parametrize("fxt_model_id", MODEL_TEMPLATES, ids=MODEL_IDS, indirect=True) + @pytest.mark.parametrize("fxt_benchmark", BENCHMARK_CONFIGS.items(), ids=BENCHMARK_CONFIGS.keys(), indirect=True) + def test_perf(self, fxt_model_id: str, fxt_benchmark: OTXBenchmark, fxt_check_benchmark_result: Callable): + """Benchmark performance metrics.""" + result = fxt_benchmark.run(model_id=fxt_model_id) + fxt_check_benchmark_result( + result, + key=(fxt_benchmark.tags["task"], fxt_benchmark.tags["data_size"], fxt_model_id), + checks=[ + { + "name": "f-measure(train)", + "op": ">", + "margin": 0.1, + }, + { + "name": "epoch", + "op": "<", + "margin": 0.1, + }, + { + "name": "f-measure(export)", + "op": ">", + "margin": 0.1, + }, + { + "name": "f-measure(optimize)", + "op": ">", + "margin": 0.1, + }, + { + "name": "train_e2e_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_data_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_iter_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(export)", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(optimize)", + "op": "<", + "margin": 0.1, + }, + ], ) diff --git a/tests/perf/test_instance_segmentation.py b/tests/perf/test_instance_segmentation.py index 20faa367460..dc07d9c5a47 100644 --- a/tests/perf/test_instance_segmentation.py +++ b/tests/perf/test_instance_segmentation.py @@ -1,190 +1,209 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""OTX instance segmentation perfomance benchmark tests.""" +"""OTX Instance Segmentation perfomance tests.""" -from __future__ import annotations +# Copyright (C) 2023-2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 -from pathlib import Path import pytest -from .benchmark import Benchmark -from .conftest import PerfTestBase +from otx.cli.registry import Registry +from typing import Callable +from .benchmark import OTXBenchmark -class TestPerfInstanceSegmentation(PerfTestBase): - """Benchmark instance segmentation.""" +MODEL_TEMPLATES = Registry(f"src/otx/algorithms").filter(task_type="INSTANCE_SEGMENTATION").templates +MODEL_IDS = [template.model_template_id for template in MODEL_TEMPLATES] - MODEL_TEST_CASES = [ # noqa: RUF012 - Benchmark.Model(task="instance_segmentation", name="maskrcnn_efficientnetb2b", category="speed"), - Benchmark.Model(task="instance_segmentation", name="maskrcnn_r50", category="accuracy"), - Benchmark.Model(task="instance_segmentation", name="maskrcnn_swint", category="other"), - ] - DATASET_TEST_CASES = [ - Benchmark.Dataset( - name=f"wgisd_small_{idx}", - path=Path("instance_seg/wgisd_small") / f"{idx}", - size="small", - data_format="coco", - num_classes=5, - num_repeat=5, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ) - for idx in (1, 2, 3) - ] + [ - Benchmark.Dataset( - name="coco_car_person_medium", - path=Path("instance_seg/coco_car_person_medium"), - size="medium", - data_format="coco", - num_classes=2, - num_repeat=5, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", +class TestPerfInstanceSegmentation: + """Benchmark basic instance segmentation.""" + + BENCHMARK_CONFIGS = { + "small": { + "tags": { + "task": "instance_segmentation", }, - ), - Benchmark.Dataset( - name="vitens_coliform", - path=Path("instance_seg/Vitens-Coliform-coco"), - size="large", - data_format="coco", - num_classes=1, - num_repeat=5, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", + "datasets": [ + "instance_seg/wgisd_small/1", + "instance_seg/wgisd_small/2", + "instance_seg/wgisd_small/3", + ], + "num_repeat": 5, + }, + "medium": { + "tags": { + "task": "instance_segmentation", }, - ), - ] - - BENCHMARK_CRITERIA = [ # noqa: RUF012 - Benchmark.Criterion(name="train/epoch", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="train/e2e_time", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="val/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="test/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="export/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="optimize/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="train/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="test/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="export/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="optimize/iter_time", summary="mean", compare="<", margin=0.1), - ] - - @pytest.mark.parametrize( - "fxt_model", - MODEL_TEST_CASES, - ids=lambda model: model.name, - indirect=True, - ) - @pytest.mark.parametrize( - "fxt_dataset", - DATASET_TEST_CASES, - ids=lambda dataset: dataset.name, - indirect=True, - ) - def test_perf( - self, - fxt_model: Benchmark.Model, - fxt_dataset: Benchmark.Dataset, - fxt_benchmark: Benchmark, - ): - self._test_perf( - model=fxt_model, - dataset=fxt_dataset, - benchmark=fxt_benchmark, - criteria=self.BENCHMARK_CRITERIA, + "datasets": [ + "instance_seg/coco_car_person_medium", + ], + "num_repeat": 5, + }, + # TODO: Refine large dataset + # "large": { + # "tags": { + # "task": "instance_segmentation", + # }, + # "datasets": [ + # "instance_seg/bdd_large", + # ], + # "num_repeat": 5, + # }, + } + + @pytest.mark.parametrize("fxt_model_id", MODEL_TEMPLATES, ids=MODEL_IDS, indirect=True) + @pytest.mark.parametrize("fxt_benchmark", BENCHMARK_CONFIGS.items(), ids=BENCHMARK_CONFIGS.keys(), indirect=True) + def test_perf(self, fxt_model_id: str, fxt_benchmark: OTXBenchmark, fxt_check_benchmark_result: Callable): + """Benchmark performance metrics.""" + result = fxt_benchmark.run(model_id=fxt_model_id) + fxt_check_benchmark_result( + result, + key=(fxt_benchmark.tags["task"], fxt_benchmark.tags["data_size"], fxt_model_id), + checks=[ + { + "name": "f-measure(train)", + "op": ">", + "margin": 0.1, + }, + { + "name": "epoch", + "op": "<", + "margin": 0.1, + }, + { + "name": "f-measure(export)", + "op": ">", + "margin": 0.1, + }, + { + "name": "f-measure(optimize)", + "op": ">", + "margin": 0.1, + }, + { + "name": "train_e2e_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_data_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_iter_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(export)", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(optimize)", + "op": "<", + "margin": 0.1, + }, + ], ) -class TestPerfTilingInstanceSegmentation(PerfTestBase): +class TestPerfTilingInstanceSegmentation: """Benchmark tiling instance segmentation.""" - MODEL_TEST_CASES = [ # noqa: RUF012 - Benchmark.Model(task="instance_segmentation", name="maskrcnn_efficientnetb2b_tile", category="speed"), - Benchmark.Model(task="instance_segmentation", name="maskrcnn_r50_tile", category="accuracy"), - Benchmark.Model(task="instance_segmentation", name="maskrcnn_swint_tile", category="other"), - ] - - DATASET_TEST_CASES = [ - Benchmark.Dataset( - name=f"vitens_aeromonas_small_{idx}", - path=Path("tiling_instance_seg/vitens_aeromonas_small") / f"{idx}", - size="small", - data_format="coco", - num_classes=1, - num_repeat=5, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", + TILING_PARAMS = { + "tiling_parameters.enable_tiling": 1, + } + BENCHMARK_CONFIGS = { + "small": { + "tags": { + "task": "tiling_instance_segmentation", }, - ) - for idx in (1, 2, 3) - ] + [ - Benchmark.Dataset( - name="vitens_aeromonas_medium", - path=Path("tiling_instance_seg/vitens_aeromonas_medium"), - size="medium", - data_format="coco", - num_classes=1, - num_repeat=5, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", + "datasets": [ + "tiling_instance_seg/vitens_aeromonas_small/1", + "tiling_instance_seg/vitens_aeromonas_small/2", + "tiling_instance_seg/vitens_aeromonas_small/3", + ], + "num_repeat": 5, + "train_params": TILING_PARAMS, + }, + "medium": { + "tags": { + "task": "tiling_instance_segmentation", }, - ), - # Add large dataset - ] - - BENCHMARK_CRITERIA = [ # noqa: RUF012 - Benchmark.Criterion(name="train/epoch", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="train/e2e_time", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="val/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="test/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="export/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="optimize/f1-score", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="train/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="test/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="export/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="optimize/iter_time", summary="mean", compare="<", margin=0.1), - ] - - @pytest.mark.parametrize( - "fxt_model", - MODEL_TEST_CASES, - ids=lambda model: model.name, - indirect=True, - ) - @pytest.mark.parametrize( - "fxt_dataset", - DATASET_TEST_CASES, - ids=lambda dataset: dataset.name, - indirect=True, - ) - def test_perf( - self, - fxt_model: Benchmark.Model, - fxt_dataset: Benchmark.Dataset, - fxt_benchmark: Benchmark, - ): - self._test_perf( - model=fxt_model, - dataset=fxt_dataset, - benchmark=fxt_benchmark, - criteria=self.BENCHMARK_CRITERIA, + "datasets": [ + "tiling_instance_seg/vitens_aeromonas_medium", + ], + "num_repeat": 5, + "train_params": TILING_PARAMS, + }, + # TODO: Refine large dataset + # "large": { + # "tags": { + # "task": "tiling_instance_segmentation", + # }, + # "datasets": [ + # "tiling_instance_seg/dota_large", + # ], + # "num_repeat": 5, + # "train_params": TILING_PARAMS, + # }, + } + + @pytest.mark.parametrize("fxt_model_id", MODEL_TEMPLATES, ids=MODEL_IDS, indirect=True) + @pytest.mark.parametrize("fxt_benchmark", BENCHMARK_CONFIGS.items(), ids=BENCHMARK_CONFIGS.keys(), indirect=True) + def test_perf(self, fxt_model_id: str, fxt_benchmark: OTXBenchmark, fxt_check_benchmark_result: Callable): + """Benchmark performance metrics.""" + result = fxt_benchmark.run(model_id=fxt_model_id) + fxt_check_benchmark_result( + result, + key=(fxt_benchmark.tags["task"], fxt_benchmark.tags["data_size"], fxt_model_id), + checks=[ + { + "name": "f-measure(train)", + "op": ">", + "margin": 0.1, + }, + { + "name": "epoch", + "op": "<", + "margin": 0.1, + }, + { + "name": "f-measure(export)", + "op": ">", + "margin": 0.1, + }, + { + "name": "f-measure(optimize)", + "op": ">", + "margin": 0.1, + }, + { + "name": "train_e2e_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_data_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_iter_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(export)", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(optimize)", + "op": "<", + "margin": 0.1, + }, + ], ) diff --git a/tests/perf/test_semantic_segmentation.py b/tests/perf/test_semantic_segmentation.py index 4dd64298002..4de9257ae29 100644 --- a/tests/perf/test_semantic_segmentation.py +++ b/tests/perf/test_semantic_segmentation.py @@ -1,97 +1,111 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""OTX semantic segmentation perfomance benchmark tests.""" +"""OTX Semantic Segmentation perfomance tests.""" -from __future__ import annotations +# Copyright (C) 2023-2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 -from pathlib import Path import pytest -from .benchmark import Benchmark -from .conftest import PerfTestBase +from otx.cli.registry import Registry +from typing import Callable +from .benchmark import OTXBenchmark -class TestPerfSemanticSegmentation(PerfTestBase): - """Benchmark semantic segmentation.""" +MODEL_TEMPLATES = Registry(f"src/otx/algorithms").filter(task_type="SEGMENTATION").templates +MODEL_IDS = [template.model_template_id for template in MODEL_TEMPLATES] - MODEL_TEST_CASES = [ # noqa: RUF012 - Benchmark.Model(task="semantic_segmentation", name="litehrnet_18", category="balance"), - Benchmark.Model(task="semantic_segmentation", name="litehrnet_s", category="speed"), - Benchmark.Model(task="semantic_segmentation", name="litehrnet_x", category="accuracy"), - Benchmark.Model(task="semantic_segmentation", name="segnext_b", category="other"), - Benchmark.Model(task="semantic_segmentation", name="segnext_s", category="other"), - Benchmark.Model(task="semantic_segmentation", name="segnext_t", category="other"), - Benchmark.Model(task="semantic_segmentation", name="dino_v2", category="other"), - ] - DATASET_TEST_CASES = [ - Benchmark.Dataset( - name=f"kvasir_small_{idx}", - path=Path("semantic_seg/kvasir_small") / f"{idx}", - size="small", - data_format="common_semantic_segmentation_with_subset_dirs", - num_classes=2, - num_repeat=5, - extra_overrides={}, - ) - for idx in (1, 2, 3) - ] + [ - Benchmark.Dataset( - name="kvasir_medium", - path=Path("semantic_seg/kvasir_medium"), - size="medium", - data_format="common_semantic_segmentation_with_subset_dirs", - num_classes=2, - num_repeat=5, - extra_overrides={}, - ), - Benchmark.Dataset( - name="kvasir_large", - path=Path("semantic_seg/kvasir_large"), - size="large", - data_format="common_semantic_segmentation_with_subset_dirs", - num_classes=2, - num_repeat=5, - extra_overrides={}, - ), - ] +class TestPerfSemanticSegmentation: + """Benchmark basic semantic segmentation.""" - BENCHMARK_CRITERIA = [ # noqa: RUF012 - Benchmark.Criterion(name="train/epoch", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="train/e2e_time", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="val/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="test/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="export/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="optimize/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="train/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="test/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="export/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="optimize/iter_time", summary="mean", compare="<", margin=0.1), - ] + BENCHMARK_CONFIGS = { + "small": { + "tags": { + "task": "semantic_segmentation", + }, + "datasets": [ + "semantic_seg/kvasir_small/1", + "semantic_seg/kvasir_small/2", + "semantic_seg/kvasir_small/3", + ], + "subset_dir_names": {"train": "train", "val": "val", "test": "test"}, + "num_repeat": 5, + }, + "medium": { + "tags": { + "task": "semantic_segmentation", + }, + "datasets": [ + "semantic_seg/kvasir_medium", + ], + "subset_dir_names": {"train": "train", "val": "val", "test": "test"}, + "num_repeat": 5, + }, + "large": { + "tags": { + "task": "semantic_segmentation", + }, + "datasets": [ + "semantic_seg/kvasir_large", + ], + "subset_dir_names": {"train": "train", "val": "val", "test": "test"}, + "num_repeat": 5, + }, + } - @pytest.mark.parametrize( - "fxt_model", - MODEL_TEST_CASES, - ids=lambda model: model.name, - indirect=True, - ) - @pytest.mark.parametrize( - "fxt_dataset", - DATASET_TEST_CASES, - ids=lambda dataset: dataset.name, - indirect=True, - ) - def test_perf( - self, - fxt_model: Benchmark.Model, - fxt_dataset: Benchmark.Dataset, - fxt_benchmark: Benchmark, - ): - self._test_perf( - model=fxt_model, - dataset=fxt_dataset, - benchmark=fxt_benchmark, - criteria=self.BENCHMARK_CRITERIA, + @pytest.mark.parametrize("fxt_model_id", MODEL_TEMPLATES, ids=MODEL_IDS, indirect=True) + @pytest.mark.parametrize("fxt_benchmark", BENCHMARK_CONFIGS.items(), ids=BENCHMARK_CONFIGS.keys(), indirect=True) + def test_perf(self, fxt_model_id: str, fxt_benchmark: OTXBenchmark, fxt_check_benchmark_result: Callable): + """Benchmark performance metrics.""" + result = fxt_benchmark.run(model_id=fxt_model_id) + fxt_check_benchmark_result( + result, + key=(fxt_benchmark.tags["task"], fxt_benchmark.tags["data_size"], fxt_model_id), + checks=[ + { + "name": "Dice Average(train)", + "op": ">", + "margin": 0.1, + }, + { + "name": "epoch", + "op": "<", + "margin": 0.1, + }, + { + "name": "Dice Average(export)", + "op": ">", + "margin": 0.1, + }, + { + "name": "Dice Average(optimize)", + "op": ">", + "margin": 0.1, + }, + { + "name": "train_e2e_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_data_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_iter_time", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(export)", + "op": "<", + "margin": 0.1, + }, + { + "name": "avg_time_per_image(optimize)", + "op": "<", + "margin": 0.1, + }, + ], ) diff --git a/tests/perf/test_visual_prompting.py b/tests/perf/test_visual_prompting.py index d33ba95e597..5d59f7ba09c 100644 --- a/tests/perf/test_visual_prompting.py +++ b/tests/perf/test_visual_prompting.py @@ -1,151 +1,4 @@ +"""OTX Visual Prompting perfomance tests.""" + # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 - -"""OTX visual prompting perfomance benchmark tests.""" - -from __future__ import annotations - -from pathlib import Path - -import pytest - -from .benchmark import Benchmark -from .conftest import PerfTestBase - - -class TestPerfVisualPrompting(PerfTestBase): - """Benchmark visual prompting.""" - - MODEL_TEST_CASES = [ # noqa: RUF012 - Benchmark.Model(task="visual_prompting", name="sam_tiny_vit", category="speed"), - Benchmark.Model(task="visual_prompting", name="sam_vit_b", category="accuracy"), - ] - - DATASET_TEST_CASES = [ - Benchmark.Dataset( - name=f"wgisd_small_{idx}", - path=Path("visual_prompting/wgisd_small") / f"{idx}", - size="small", - data_format="coco", - num_classes=5, - num_repeat=5, - extra_overrides={}, - ) - for idx in (1, 2, 3) - ] + [ - Benchmark.Dataset( - name="coco_car_person_medium", - path=Path("visual_prompting/coco_car_person_medium"), - size="medium", - data_format="coco", - num_classes=2, - num_repeat=5, - extra_overrides={}, - ), - Benchmark.Dataset( - name="vitens_coliform", - path=Path("visual_prompting/Vitens-Coliform-coco"), - size="large", - data_format="coco", - num_classes=1, - num_repeat=5, - extra_overrides={}, - ), - ] - - BENCHMARK_CRITERIA = [ # noqa: RUF012 - Benchmark.Criterion(name="train/epoch", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="train/e2e_time", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="val/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="test/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="export/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="optimize/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="train/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="test/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="export/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="optimize/iter_time", summary="mean", compare="<", margin=0.1), - ] - - @pytest.mark.parametrize( - "fxt_model", - MODEL_TEST_CASES, - ids=lambda model: model.name, - indirect=True, - ) - @pytest.mark.parametrize( - "fxt_dataset", - DATASET_TEST_CASES, - ids=lambda dataset: dataset.name, - indirect=True, - ) - def test_perf( - self, - fxt_model: Benchmark.Model, - fxt_dataset: Benchmark.Dataset, - fxt_benchmark: Benchmark, - ): - self._test_perf( - model=fxt_model, - dataset=fxt_dataset, - benchmark=fxt_benchmark, - criteria=self.BENCHMARK_CRITERIA, - ) - - -class TestPerfZeroShotVisualPrompting(PerfTestBase): - """Benchmark zero-shot visual prompting.""" - - MODEL_TEST_CASES = [ # noqa: RUF012 - Benchmark.Model(task="zero_shot_visual_prompting", name="sam_tiny_vit", category="speed"), - Benchmark.Model(task="zero_shot_visual_prompting", name="sam_vit_b", category="accuracy"), - ] - - DATASET_TEST_CASES = [ # noqa: RUF012 - Benchmark.Dataset( - name="coco_car_person_medium_datumaro", - path=Path("zero_shot_visual_prompting/coco_car_person_medium_datumaro"), - size="medium", - data_format="datumaro", - num_classes=2, - num_repeat=5, - extra_overrides={"max_epochs": "1"}, - ), - ] - - BENCHMARK_CRITERIA = [ # noqa: RUF012 - Benchmark.Criterion(name="train/epoch", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="train/e2e_time", summary="max", compare="<", margin=0.1), - Benchmark.Criterion(name="val/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="test/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="export/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="optimize/Dice", summary="max", compare=">", margin=0.1), - Benchmark.Criterion(name="train/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="test/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="export/iter_time", summary="mean", compare="<", margin=0.1), - Benchmark.Criterion(name="optimize/iter_time", summary="mean", compare="<", margin=0.1), - ] - - @pytest.mark.parametrize( - "fxt_model", - MODEL_TEST_CASES, - ids=lambda model: model.name, - indirect=True, - ) - @pytest.mark.parametrize( - "fxt_dataset", - DATASET_TEST_CASES, - ids=lambda dataset: dataset.name, - indirect=True, - ) - def test_perf( - self, - fxt_model: Benchmark.Model, - fxt_dataset: Benchmark.Dataset, - fxt_benchmark: Benchmark, - ): - self._test_perf( - model=fxt_model, - dataset=fxt_dataset, - benchmark=fxt_benchmark, - criteria=self.BENCHMARK_CRITERIA, - ) diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 00000000000..1ce68d2ef23 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +addopts = -s -v +markers = + priority_medium + components + reqids + unit diff --git a/tests/regression/__init__.py b/tests/regression/__init__.py index 6a16273c024..79931efa777 100644 --- a/tests/regression/__init__.py +++ b/tests/regression/__init__.py @@ -1,2 +1,3 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2021-2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/regression/action/test_action_classification.py b/tests/regression/action/test_action_classification.py new file mode 100644 index 00000000000..6f2595700ba --- /dev/null +++ b/tests/regression/action/test_action_classification.py @@ -0,0 +1,175 @@ +"""Tests for Action Classification with OTX CLI.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import json +import os +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_test_helpers import REGRESSION_TEST_EPOCHS, TIME_LOG, RegressionTestConfig +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, +) + +from tests.regression.regression_command import ( + regression_eval_testing, + regression_openvino_testing, + regression_deployment_testing, + regression_nncf_eval_testing, + regression_ptq_eval_testing, + regression_train_time_testing, + regression_eval_time_testing, +) + + +class TestRegressionActionClassification: + REG_CATEGORY = "action" + TASK_TYPE = "action_classification" + TRAIN_TYPE = "supervised" + LABEL_TYPE = "multi_class" + + TRAIN_PARAMS = ["--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): + if template.name == "MoViNet": + pytest.skip(reason="Issue#2058: MoViNet fails with OpenVINO inference occasionally") + test_type = "export" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): + if template.name == "MoViNet": + pytest.skip(reason="Issue#2058: MoViNet fails with OpenVINO inference occasionally") + test_type = "ptq" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] diff --git a/tests/regression/action/test_action_detection.py b/tests/regression/action/test_action_detection.py new file mode 100644 index 00000000000..f7dd494dcf8 --- /dev/null +++ b/tests/regression/action/test_action_detection.py @@ -0,0 +1,115 @@ +"""Tests for Action Detection with OTX CLI.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import json +import os +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_test_helpers import ( + REGRESSION_TEST_EPOCHS, + TIME_LOG, + RegressionTestConfig, +) +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + otx_train_testing, +) + +from tests.regression.regression_command import ( + regression_eval_testing, + regression_openvino_testing, + regression_deployment_testing, + regression_nncf_eval_testing, + regression_ptq_eval_testing, + regression_train_time_testing, + regression_eval_time_testing, +) + + +class TestRegressionActionDetection: + REG_CATEGORY = "action" + TASK_TYPE = "action_detection" + TRAIN_TYPE = "supervised" + LABEL_TYPE = "multi_class" + + TRAIN_PARAMS = ["--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] diff --git a/tests/regression/anomaly/test_anomaly_classificaiton.py b/tests/regression/anomaly/test_anomaly_classificaiton.py new file mode 100644 index 00000000000..ae928e7997b --- /dev/null +++ b/tests/regression/anomaly/test_anomaly_classificaiton.py @@ -0,0 +1,274 @@ +"""Tests for Anomaly Classification with OTX CLI.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import json +import os +import random +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_command import ( + regression_deployment_testing, + regression_eval_testing, + regression_eval_time_testing, + regression_nncf_eval_testing, + regression_openvino_testing, + regression_ptq_eval_testing, + regression_train_time_testing, +) +from tests.regression.regression_test_helpers import ( + ANOMALY_DATASET_CATEGORIES, + TIME_LOG, + RegressionTestConfig, +) +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, +) + + +class TestRegressionAnomalyClassification: + # Configurations for regression test. + REG_CATEGORY = "anomaly" + TASK_TYPE = "anomaly_classification" + TRAIN_TYPE = None + LABEL_TYPE = None + TRAIN_PARAMS = None + + SAMPLED_ANOMALY_DATASET_CATEGORIES = ANOMALY_DATASET_CATEGORIES + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + enable_auto_num_worker=False, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict(dump_path=os.path.join(cls.reg_cfg.result_dir, f"result_{cls.TASK_TYPE}.json")) + + def setup_method(self): + self.performance = {} + + def _apply_category(self, data_dict, category): + return_dict = {} + for k, v in data_dict.items(): + if "train" in k: + return_dict[k] = f"{v}/{category}/train" + if "val" in k or "test" in k: + return_dict[k] = f"{v}/{category}/test" + else: + return_dict[k] = v + return return_dict + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_train(self, reg_cfg, template, tmp_dir_path, category): + test_type = "train" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args, deterministic=False) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + reg_cfg.config_dict["regression_criteria"][test_type][category], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_train_kpi_test(self, reg_cfg, template, category): + """KPI tests: measure the train+val time and evaluation time and compare with criteria.""" + performance = reg_cfg.get_template_performance(template, category=category) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + # Compare train+val time with the KPI criteria. + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"][category], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + # Compare evaluation time with the KPI criteria. + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"][category], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, category): + if category in ["transistor", "cable"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "export" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, category): + if category in ["transistor", "cable"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "deploy" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): + if category in ["transistor", "cable", "bottle"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "nncf" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): + test_type = "ptq" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] diff --git a/tests/regression/anomaly/test_anomaly_detection.py b/tests/regression/anomaly/test_anomaly_detection.py new file mode 100644 index 00000000000..e638a88ad28 --- /dev/null +++ b/tests/regression/anomaly/test_anomaly_detection.py @@ -0,0 +1,276 @@ +"""Tests for Anomaly Detection with OTX CLI.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import json +import os +import random +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_command import ( + regression_deployment_testing, + regression_eval_testing, + regression_eval_time_testing, + regression_nncf_eval_testing, + regression_openvino_testing, + regression_ptq_eval_testing, + regression_train_time_testing, +) +from tests.regression.regression_test_helpers import ( + ANOMALY_DATASET_CATEGORIES, + TIME_LOG, + RegressionTestConfig, +) +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, +) + + +class TestRegressionAnomalyDetection: + # Configurations for regression test. + REG_CATEGORY = "anomaly" + TASK_TYPE = "anomaly_detection" + TRAIN_TYPE = None + LABEL_TYPE = None + TRAIN_PARAMS = None + + SAMPLED_ANOMALY_DATASET_CATEGORIES = ANOMALY_DATASET_CATEGORIES + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + enable_auto_num_worker=False, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict(dump_path=os.path.join(cls.reg_cfg.result_dir, f"result_{cls.TASK_TYPE}.json")) + + def setup_method(self): + self.performance = {} + + def _apply_category(self, data_dict, category): + return_dict = {} + for k, v in data_dict.items(): + if "train" in k: + return_dict[k] = f"{v}/{category}/train" + if "val" in k or "test" in k: + return_dict[k] = f"{v}/{category}/test" + else: + return_dict[k] = v + return return_dict + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_train(self, reg_cfg, template, tmp_dir_path, category): + test_type = "train" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args, deterministic=False) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + reg_cfg.config_dict["regression_criteria"][test_type][category], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_train_kpi_test(self, reg_cfg, template, category): + """KPI tests: measure the train+val time and evaluation time and compare with criteria.""" + performance = reg_cfg.get_template_performance(template, category=category) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + # Compare train+val time with the KPI criteria. + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"][category], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + # Compare evaluation time with the KPI criteria. + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"][category], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, category): + if category in ["tile", "grid"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "export" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, category): + if category in ["tile", "cable"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "deploy" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): + if category in ["tile", "cable", "grid"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "nncf" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): + if category in ["tile", "grid"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "ptq" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] diff --git a/tests/regression/anomaly/test_anomaly_segmentation.py b/tests/regression/anomaly/test_anomaly_segmentation.py new file mode 100644 index 00000000000..13f90320aab --- /dev/null +++ b/tests/regression/anomaly/test_anomaly_segmentation.py @@ -0,0 +1,276 @@ +"""Tests for Anomaly Segmentation with OTX CLI.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import json +import os +import random +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_command import ( + regression_deployment_testing, + regression_eval_testing, + regression_eval_time_testing, + regression_nncf_eval_testing, + regression_openvino_testing, + regression_ptq_eval_testing, + regression_train_time_testing, +) +from tests.regression.regression_test_helpers import ( + ANOMALY_DATASET_CATEGORIES, + TIME_LOG, + RegressionTestConfig, +) +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, +) + + +class TestRegressionAnomalySegmentation: + # Configurations for regression test. + REG_CATEGORY = "anomaly" + TASK_TYPE = "anomaly_segmentation" + TRAIN_TYPE = None + LABEL_TYPE = None + TRAIN_PARAMS = None + + SAMPLED_ANOMALY_DATASET_CATEGORIES = ANOMALY_DATASET_CATEGORIES + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + enable_auto_num_worker=False, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict(dump_path=os.path.join(cls.reg_cfg.result_dir, f"result_{cls.TASK_TYPE}.json")) + + def setup_method(self): + self.performance = {} + + def _apply_category(self, data_dict, category): + return_dict = {} + for k, v in data_dict.items(): + if "train" in k: + return_dict[k] = f"{v}/{category}/train" + if "val" in k or "test" in k: + return_dict[k] = f"{v}/{category}/test" + else: + return_dict[k] = v + return return_dict + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_train(self, reg_cfg, template, tmp_dir_path, category): + test_type = "train" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args, deterministic=False) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + reg_cfg.config_dict["regression_criteria"][test_type][category], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_train_kpi_test(self, reg_cfg, template, category): + """KPI tests: measure the train+val time and evaluation time and compare with criteria.""" + performance = reg_cfg.get_template_performance(template, category=category) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + # Compare train+val time with the KPI criteria. + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"][category], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + # Compare evaluation time with the KPI criteria. + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"][category], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, category): + if category in ["metal_nut", "screw"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "export" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, category): + if category in ["metal_nut", "screw"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "deploy" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): + if category in ["screw"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "nncf" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("category", SAMPLED_ANOMALY_DATASET_CATEGORIES) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): + if category in ["metal_nut", "screw"]: + pytest.skip("Issue#2189: Anomaly task sometimes shows performance drop") + test_type = "ptq" + self.performance[template.name] = {} + category_data_args = self._apply_category(reg_cfg.args, category) + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, category_data_args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + category_data_args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) + + assert test_result["passed"] is True, test_result["log"] diff --git a/tests/regression/classification/test_classification.py b/tests/regression/classification/test_classification.py new file mode 100644 index 00000000000..83a7c26627f --- /dev/null +++ b/tests/regression/classification/test_classification.py @@ -0,0 +1,1032 @@ +"""Tests for Classification with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import json +import os +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_test_helpers import ( + REGRESSION_TEST_EPOCHS, + TIME_LOG, + RegressionTestConfig, +) +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, +) + +from tests.regression.regression_command import ( + regression_eval_testing, + regression_openvino_testing, + regression_deployment_testing, + regression_nncf_eval_testing, + regression_ptq_eval_testing, + regression_train_time_testing, + regression_eval_time_testing, +) + + +class TestRegressionMultiClassClassification: + REG_CATEGORY = "classification" + TASK_TYPE = "classification" + TRAIN_TYPE = "supervised" + LABEL_TYPE = "multi_class" + + TRAIN_PARAMS = ["--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_class_cls" + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_incr(self, reg_cfg, template, tmp_dir_path): + if template.name == "DeiT-Tiny": + pytest.skip(reason="Issue#2567: error while calc IB loss for DeiT-Tiny") + train_type = "class_incr" + test_type = "train" + self.performance[template.name] = {} + + sl_template_work_dir = get_template_dir(template, tmp_dir_path / "multi_class_cls") + + tmp_dir_path = tmp_dir_path / "multi_class_cls_incr" + config_cls_incr = reg_cfg.load_config(train_type=train_type) + args_cls_incr = config_cls_incr["data_path"] + args_cls_incr[ + "--load-weights" + ] = f"{sl_template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + args_cls_incr["train_params"] = ["params", "--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + + reg_cfg.update_gpu_args(args_cls_incr) + + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, args_cls_incr) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + args_cls_incr, + config_cls_incr["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): + train_type = "class_incr" + config_cls_incr = reg_cfg.load_config(train_type=train_type) + + performance = reg_cfg.get_template_performance(template, train_type=train_type) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=config_cls_incr["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=config_cls_incr["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_semisl(self, reg_cfg, template, tmp_dir_path): + train_type = "semi_supervised" + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_semisl" + # config_semisl = load_regression_configuration(reg_cfg.otx_dir, TASK_TYPE, "semi_supervised", reg_cfg.label_type) + config_semisl = reg_cfg.load_config(train_type=train_type) + args_semisl = config_semisl["data_path"] + + args_semisl["train_params"] = [ + "params", + "--learning_parameters.num_iters", + REGRESSION_TEST_EPOCHS, + "--algo_backend.train_type", + "Semisupervised", + ] + + reg_cfg.update_gpu_args(args_semisl) + + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, args_semisl) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + args_semisl, + config_semisl["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_semisl_kpi_test(self, reg_cfg, template): + train_type = "semi_supervised" + config_semisl = reg_cfg.load_config(train_type=train_type) + + performance = reg_cfg.get_template_performance(template, train_type=train_type) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=config_semisl["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=config_semisl["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_selfsl(self, reg_cfg, template, tmp_dir_path): + if template.name == "DeiT-Tiny": + pytest.skip(reason="Self-SL for ViT template is not supported yet.") + train_type = "self_supervised" + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_class_cls/test_selfsl" + config_selfsl = reg_cfg.load_config(train_type=train_type) + args_selfsl = config_selfsl["data_path"] + + selfsl_train_args = copy.deepcopy(args_selfsl) + selfsl_train_args["--train-type"] = "Selfsupervised" + selfsl_train_args["train_params"] = [ + "params", + "--learning_parameters.batch_size", + "64", + "--learning_parameters.num_iters", + "10", + ] + + reg_cfg.update_gpu_args(selfsl_train_args) + + # Self-supervised Training + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, selfsl_train_args) + train_elapsed_time = timer() - train_start_time + + # Supervised Training + template_work_dir = get_template_dir(template, tmp_dir_path) + assert os.path.exists(f"{template_work_dir}/selfsl") + new_tmp_dir_path = tmp_dir_path / "test_supervised" + args_selfsl["train_params"] = ["params", "--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + args_selfsl["--val-data-roots"] = reg_cfg.args["--val-data-roots"] + args_selfsl["--test-data-roots"] = reg_cfg.args["--test-data-roots"] + args_selfsl["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + + reg_cfg.update_gpu_args(args_selfsl) + + otx_train_testing(template, new_tmp_dir_path, reg_cfg.otx_dir, args_selfsl) + + # Evaluation with self + supervised training model + args_selfsl.pop("--load-weights") + infer_start_time = timer() + test_result = regression_eval_testing( + template, + new_tmp_dir_path, + reg_cfg.otx_dir, + args_selfsl, + config_selfsl["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_selfsl_kpi_test(self, reg_cfg, template): + train_type = "self_supervised" + config_selfsl = reg_cfg.load_config(train_type=train_type) + + performance = reg_cfg.get_template_performance(template, train_type=train_type) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=config_selfsl["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=config_selfsl["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): + test_type = "export" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_class_cls" + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): + test_type = "deploy" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_class_cls" + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "nncf" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_class_cls" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "ptq" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_class_cls" + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + +class TestRegressionMultiLabelClassification: + REG_CATEGORY = "classification" + TASK_TYPE = "classification" + TRAIN_TYPE = "supervised" + LABEL_TYPE = "multi_label" + + TRAIN_PARAMS = ["--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_label_cls" + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_incr(self, reg_cfg, template, tmp_dir_path): + train_type = "class_incr" + test_type = "train" + self.performance[template.name] = {} + + sl_template_work_dir = get_template_dir(template, tmp_dir_path / "multi_label_cls") + + tmp_dir_path = tmp_dir_path / "multi_label_cls_incr" + config_cls_incr = reg_cfg.load_config(train_type=train_type) + args_cls_incr = config_cls_incr["data_path"] + args_cls_incr[ + "--load-weights" + ] = f"{sl_template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + args_cls_incr["train_params"] = ["params", "--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + + reg_cfg.update_gpu_args(args_cls_incr) + + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, args_cls_incr) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + args_cls_incr, + config_cls_incr["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): + train_type = "class_incr" + config_cls_incr = reg_cfg.load_config(train_type=train_type) + + performance = reg_cfg.get_template_performance(template, train_type=train_type) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=config_cls_incr["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=config_cls_incr["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): + test_type = "export" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_label_cls" + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): + test_type = "deploy" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_label_cls" + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "nncf" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_label_cls" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "ptq" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "multi_label_cls" + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + +class TestRegressionHierarchicalLabelClassification: + REG_CATEGORY = "classification" + TASK_TYPE = "classification" + TRAIN_TYPE = "supervised" + LABEL_TYPE = "h_label" + + TRAIN_PARAMS = ["--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "h_label_cls" + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): + test_type = "export" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "h_label_cls" + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): + test_type = "deploy" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "h_label_cls" + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "nncf" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "h_label_cls" + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "ptq" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "h_label_cls" + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + +class TestRegressionSupconClassification: + REG_CATEGORY = "classification" + TASK_TYPE = "classification" + TRAIN_TYPE = "supervised" + LABEL_TYPE = "supcon" + + TRAIN_PARAMS = [ + "--learning_parameters.num_iters", + REGRESSION_TEST_EPOCHS, + "--learning_parameters.enable_supcon", + "True", + ] + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + if template.name == "DeiT-Tiny": + pytest.skip(reason="Supcon for ViT template is not supported yet.") + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "supcon_cls" + + # Supcon + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + # Evaluation with supcon + supervised training + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] diff --git a/tests/regression/conftest.py b/tests/regression/conftest.py index af5f741ed84..37f1bc0d379 100644 --- a/tests/regression/conftest.py +++ b/tests/regression/conftest.py @@ -1,155 +1,20 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2021-2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import logging -import platform -import subprocess -from datetime import datetime, timedelta, timezone -from pathlib import Path -from urllib.parse import urlparse - +import os import pytest -from cpuinfo import get_cpu_info -from otx import __version__ - -import mlflow - -log = logging.getLogger(__name__) - - -def pytest_addoption(parser: pytest.Parser) -> None: - parser.addoption( - "--user-name", - type=str, - required=True, - help="Sign-off the user name who launched the regression tests this time, " 'e.g., `--user-name "John Doe"`.', - ) - parser.addoption( - "--dataset-root-dir", - type=Path, - required=True, - help="Dataset root directory path for the regression tests", - ) - parser.addoption( - "--mlflow-tracking-uri", - type=str, - required=True, - help="URI for MLFlow Tracking server to store the regression test results.", - ) - parser.addoption( - "--num-repeat", - type=int, - default=1, - help="The number of repetitions for each test case with different seed (default=1).", - ) - - -@pytest.fixture(scope="module", autouse=True) -def fxt_user_name(request: pytest.FixtureRequest) -> str: - """User name to sign off the regression test execution. - - This should be given by the PyTest CLI option. - """ - user_name = request.config.getoption("--user-name") - msg = f"user_name: {user_name}" - log.info(msg) - return user_name - - -@pytest.fixture(scope="module", autouse=True) -def fxt_dataset_root_dir(request: pytest.FixtureRequest) -> Path: - """Dataset root directory path. - - This should be given by the PyTest CLI option. - """ - dataset_root_dir = request.config.getoption("--dataset-root-dir") - msg = f"dataset_root_dir: {dataset_root_dir}" - log.info(msg) - return dataset_root_dir - - -@pytest.fixture(scope="module", autouse=True) -def fxt_mlflow_tracking_uri(request: pytest.FixtureRequest) -> str: - """MLFLow tracking server URI. - - This should be given by the PyTest CLI option. - """ - mlflow_tracking_uri = urlparse( - request.config.getoption("--mlflow-tracking-uri"), - ).geturl() - msg = f"fxt_mlflow_tracking_uri: {mlflow_tracking_uri}" - log.info(msg) - return mlflow_tracking_uri - - -@pytest.fixture(scope="module", autouse=True) -def fxt_num_repeat(request: pytest.FixtureRequest) -> int: - """The number of repetition for each test case. - - The random seed will be set for [0, fxt_num_repeat - 1]. Default is one. - """ - num_repeat = request.config.getoption("--num-repeat") - msg = f"fxt_num_repeat: {fxt_num_repeat}" - log.info(msg) - return num_repeat - - -@pytest.fixture(scope="module", autouse=True) -def fxt_mlflow_experiment_name(fxt_user_name) -> str: - """MLFlow Experiment name (unique key). - - MLFlow Experiment name is an unique key as same as experiment id. - Every MLFlow Run belongs to MLFlow Experiment. - """ - tz = timezone(offset=timedelta(hours=9), name="Seoul") - date = datetime.now(tz=tz).date() - return f"OTX: {__version__}, Signed-off-by: {fxt_user_name}, Date: {date}" - - -@pytest.fixture(scope="module", autouse=True) -def fxt_tags(fxt_user_name) -> dict[str, str]: - """Tag fields to record the machine and user executing this regression test.""" - return { - "user_name": fxt_user_name, - "machine_name": platform.node(), - "cpu_info": get_cpu_info()["brand_raw"], - "accelerator_info": subprocess.check_output( - ["nvidia-smi", "-L"], # noqa: S603, S607 - ) - .decode() - .strip(), - } +from tests.regression.summarize_test_results import summarize_results_data -@pytest.fixture(scope="module", autouse=True) -def fxt_mlflow_experiment( - fxt_mlflow_experiment_name: str, - fxt_mlflow_tracking_uri: str, - fxt_tags: dict[str, str], -) -> None: - """Set MLFlow Experiment - If there is a MLFlow Experiment which has the same name with the given name, - it will use that MLFlow Experiment. Otherwise, it will create a new one and use it. - """ - mlflow.set_tracking_uri(fxt_mlflow_tracking_uri) - exp = mlflow.get_experiment_by_name(name=fxt_mlflow_experiment_name) - exp_id = ( - mlflow.create_experiment( - name=fxt_mlflow_experiment_name, - tags=fxt_tags, - ) - if exp is None - else exp.experiment_id - ) - mlflow.set_experiment(experiment_id=exp_id) +@pytest.fixture(autouse=True, scope="session") +def run_regression_tests(tmp_dir_path): + result_path = os.path.join(os.environ.get("REG_RESULTS_ROOT", tmp_dir_path), "reg_test_results") + print(f"reg results path = {result_path}") + if not os.path.exists(result_path): + os.makedirs(result_path) + yield -@pytest.fixture(scope="module", autouse=True) -def fxt_recipe_dir() -> Path: - """OTX recipe directory.""" - import otx.recipe as otx_recipe + output_path = os.environ.get("TOX_WORK_DIR", os.getcwd()) - return Path(otx_recipe.__file__).parent + summarize_results_data(result_path, output_path) diff --git a/tests/regression/detection/test_detection.py b/tests/regression/detection/test_detection.py new file mode 100644 index 00000000000..c6b5a508d26 --- /dev/null +++ b/tests/regression/detection/test_detection.py @@ -0,0 +1,374 @@ +"""Tests for Detection with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import json +import os +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_test_helpers import ( + REGRESSION_TEST_EPOCHS, + TIME_LOG, + RegressionTestConfig, +) +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, +) + +from tests.regression.regression_command import ( + regression_eval_testing, + regression_openvino_testing, + regression_deployment_testing, + regression_nncf_eval_testing, + regression_ptq_eval_testing, + regression_train_time_testing, + regression_eval_time_testing, +) + + +class TestRegressionDetection: + REG_CATEGORY = "detection" + TASK_TYPE = "detection" + TRAIN_TYPE = "supervised" + LABEL_TYPE = "multi_class" + + TRAIN_PARAMS = ["--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_incr(self, reg_cfg, template, tmp_dir_path): + train_type = "class_incr" + test_type = "train" + self.performance[template.name] = {} + + sl_template_work_dir = get_template_dir(template, tmp_dir_path / reg_cfg.task_type) + + tmp_dir_path = tmp_dir_path / "det_incr" + config_cls_incr = reg_cfg.load_config(train_type=train_type) + args_cls_incr = config_cls_incr["data_path"] + args_cls_incr[ + "--load-weights" + ] = f"{sl_template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + args_cls_incr["train_params"] = ["params", "--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + + reg_cfg.update_gpu_args(args_cls_incr) + + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, args_cls_incr) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + args_cls_incr, + config_cls_incr["regression_criteria"]["train"], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): + train_type = "class_incr" + config_cls_incr = reg_cfg.load_config(train_type=train_type) + performance = reg_cfg.get_template_performance(template, train_type=train_type) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=config_cls_incr["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=config_cls_incr["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_semisl(self, reg_cfg, template, tmp_dir_path): + train_type = "semi_supervised" + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / f"{reg_cfg.task_type}/test_semisl" + config_semisl = reg_cfg.load_config(train_type=train_type) + args_semisl = config_semisl["data_path"] + + args_semisl["train_params"] = [ + "params", + "--learning_parameters.num_iters", + REGRESSION_TEST_EPOCHS, + "--algo_backend.train_type", + "Semisupervised", + ] + reg_cfg.update_gpu_args(args_semisl) + + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, args_semisl) + train_elapsed_time = timer() - train_start_time + + args_semisl.pop("train_params") + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + args_semisl, + config_semisl["regression_criteria"]["train"], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_semisl_kpi_test(self, reg_cfg, template): + train_type = "semi_supervised" + config_semisl = reg_cfg.load_config(train_type=train_type) + performance = reg_cfg.get_template_performance(template, train_type=train_type) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=config_semisl["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=config_semisl["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): + test_type = "export" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): + test_type = "deploy" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): + if template.name == "YOLOX-S": + pytest.skip("Issue#2596: IndexError") + test_type = "nncf" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "ptq" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] diff --git a/tests/regression/detection/test_tiling_detection.py b/tests/regression/detection/test_tiling_detection.py new file mode 100644 index 00000000000..b25f8990675 --- /dev/null +++ b/tests/regression/detection/test_tiling_detection.py @@ -0,0 +1,248 @@ +"""Tests for Tiling Detection with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import json +import os +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_test_helpers import ( + REGRESSION_TEST_EPOCHS, + TIME_LOG, + RegressionTestConfig, +) +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, +) + +from tests.regression.regression_command import ( + regression_eval_testing, + regression_openvino_testing, + regression_deployment_testing, + regression_nncf_eval_testing, + regression_ptq_eval_testing, + regression_train_time_testing, + regression_eval_time_testing, +) + + +class TestRegressionTilingDetection: + REG_CATEGORY = "detection" + TASK_TYPE = "detection" + TRAIN_TYPE = "tiling" + LABEL_TYPE = "multi_class" + + TRAIN_PARAMS = [ + "--learning_parameters.num_iters", + REGRESSION_TEST_EPOCHS, + "--tiling_parameters.enable_tiling", + "1", + "--tiling_parameters.enable_adaptive_params", + "1", + ] + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): + test_type = "export" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): + test_type = "deploy" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "nncf" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "ptq" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] diff --git a/tests/regression/instance_segmentation/test_instance_segmentation.py b/tests/regression/instance_segmentation/test_instance_segmentation.py new file mode 100644 index 00000000000..14029c56b7c --- /dev/null +++ b/tests/regression/instance_segmentation/test_instance_segmentation.py @@ -0,0 +1,308 @@ +"""Tests for Instance Segmentation with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import json +import os +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_test_helpers import ( + REGRESSION_TEST_EPOCHS, + TIME_LOG, + RegressionTestConfig, +) +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, +) + +from tests.regression.regression_command import ( + regression_eval_testing, + regression_openvino_testing, + regression_deployment_testing, + regression_nncf_eval_testing, + regression_ptq_eval_testing, + regression_train_time_testing, + regression_eval_time_testing, +) + + +class TestRegressionInstanceSegmentation: + REG_CATEGORY = "detection" + TASK_TYPE = "instance_segmentation" + TRAIN_TYPE = "supervised" + LABEL_TYPE = "multi_class" + + TRAIN_PARAMS = ["--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_incr(self, reg_cfg, template, tmp_dir_path): + train_type = "class_incr" + test_type = "train" + self.performance[template.name] = {} + + sl_template_work_dir = get_template_dir(template, tmp_dir_path / reg_cfg.task_type) + + tmp_dir_path = tmp_dir_path / "inst_seg_incr" + config_cls_incr = reg_cfg.load_config(train_type=train_type) + args_cls_incr = config_cls_incr["data_path"] + args_cls_incr[ + "--load-weights" + ] = f"{sl_template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + args_cls_incr["train_params"] = ["params", "--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + + reg_cfg.update_gpu_args(args_cls_incr) + + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, args_cls_incr) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + args_cls_incr, + config_cls_incr["regression_criteria"]["train"], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): + train_type = "class_incr" + config_cls_incr = reg_cfg.load_config(train_type=train_type) + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=config_cls_incr["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=config_cls_incr["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + # @pytest.mark.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): + test_type = "export" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + # @pytest.mark.skip(reason="Issue#2290: MaskRCNN shows degraded performance when inferencing in OpenVINO") + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): + test_type = "deploy" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "nncf" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "ptq" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] diff --git a/tests/regression/instance_segmentation/test_tiling_instance_segmentation.py b/tests/regression/instance_segmentation/test_tiling_instance_segmentation.py new file mode 100644 index 00000000000..5c45a0983cd --- /dev/null +++ b/tests/regression/instance_segmentation/test_tiling_instance_segmentation.py @@ -0,0 +1,254 @@ +"""Tests for Tiling Instance Segmentation with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import json +import os +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_test_helpers import ( + REGRESSION_TEST_EPOCHS, + TIME_LOG, + RegressionTestConfig, +) +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, +) + +from tests.regression.regression_command import ( + regression_eval_testing, + regression_openvino_testing, + regression_deployment_testing, + regression_nncf_eval_testing, + regression_ptq_eval_testing, + regression_train_time_testing, + regression_eval_time_testing, +) + + +class TestRegressionTilingInstanceSegmentation: + REG_CATEGORY = "detection" + TASK_TYPE = "instance_segmentation" + TRAIN_TYPE = "tiling" + LABEL_TYPE = "multi_class" + + TRAIN_PARAMS = [ + "--learning_parameters.num_iters", + REGRESSION_TEST_EPOCHS, + "--tiling_parameters.enable_tiling", + "1", + "--tiling_parameters.enable_adaptive_params", + "1", + ] + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip(reason="Issue#2381: Tiling isn't available at class incremental/deremental learning scenario") + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip(reason="Issue#2381: Tiling isn't available at class incremental/deremental learning scenario") + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip(reason="Issue#2381: Tiling isn't available at class incremental/deremental learning scenario") + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): + test_type = "export" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip(reason="Issue#2381: Tiling isn't available at class incremental/deremental learning scenario") + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): + test_type = "deploy" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip(reason="Issue#2381: Tiling isn't available at class incremental/deremental learning scenario") + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "nncf" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.skip(reason="Issue#2381: Tiling isn't available at class incremental/deremental learning scenario") + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "ptq" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] diff --git a/tests/regression/regression_command.py b/tests/regression/regression_command.py new file mode 100644 index 00000000000..8fa0b1e919a --- /dev/null +++ b/tests/regression/regression_command.py @@ -0,0 +1,327 @@ +import json +import os + +from tests.test_suite.run_test_command import ( + get_template_dir, + check_run, +) + + +def regression_eval_testing( + template, + root, + otx_dir, + args, + criteria, + result_dict, + threshold=0.10, +): + regression_result = { + "passed": True, + "log": "", + } + + template_work_dir = get_template_dir(template, root) + + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--load-weights", + f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth", + "--output", + f"{template_work_dir}/trained_{template.model_template_id}", + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + command_line.extend(args.get("eval_params", [])) + check_run(command_line) + + performance_json_path = f"{template_work_dir}/trained_{template.model_template_id}/performance.json" + assert os.path.exists(performance_json_path) + + with open(performance_json_path) as read_file: + trained_performance = json.load(read_file) + + for k in trained_performance.keys(): + result_dict[k] = round(trained_performance[k], 3) + model_criteria = 0.0 + if template.name not in criteria.keys(): + regression_result["passed"] = False + regression_result["log"] = ( + f"Cannot find regression criteria for the template '{template.name}'. " + + f"train_performance = {trained_performance}" + ) + else: + model_criteria = criteria[template.name] * (1.0 - threshold) + if trained_performance[k] < model_criteria: + regression_result["passed"] = False + regression_result[ + "log" + ] = f"[{template.name}] Performance: ({trained_performance[k]}) < Criteria: ({model_criteria}), " + f"threshold: {threshold}." + + result_dict["Model size (MB)"] = round( + os.path.getsize(f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth") / 1e6, 2 + ) + + return regression_result + + +def regression_openvino_testing( + template, + root, + otx_dir, + args, + threshold=0.0, + criteria=None, + reg_threshold=0.10, + result_dict=None, + half_precision=False, +): + regression_result = { + "passed": True, + "log": "", + } + + template_work_dir = get_template_dir(template, root) + weights_path = f"{template_work_dir}/exported_{template.model_template_id}/openvino.xml" + output_path = f"{template_work_dir}/exported_{template.model_template_id}" + perf_path = f"{template_work_dir}/exported_{template.model_template_id}/performance.json" + + if half_precision: + weights_path = f"{template_work_dir}/exported_{template.model_template_id}_fp16/openvino.xml" + output_path = f"{template_work_dir}/exported_{template.model_template_id}_fp16" + perf_path = f"{template_work_dir}/exported_{template.model_template_id}_fp16/performance.json" + + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--load-weights", + weights_path, + "--output", + output_path, + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + check_run(command_line) + + trained_perf_path = f"{template_work_dir}/trained_{template.model_template_id}/performance.json" + assert os.path.exists(trained_perf_path) + with open(trained_perf_path) as read_file: + trained_performance = json.load(read_file) + + assert os.path.exists(perf_path) + with open(perf_path) as read_file: + exported_performance = json.load(read_file) + + for k in trained_performance.keys(): + if k == "avg_time_per_image": + continue + result_dict[k] = round(exported_performance[k], 3) + if ( + exported_performance[k] < trained_performance[k] + and abs(trained_performance[k] - exported_performance[k]) / (trained_performance[k] + 1e-10) > threshold + ): + regression_result["passed"] = False + regression_result[ + "log" + ] = f"[{template.name}] {trained_performance[k]=}, {exported_performance[k]=}, {threshold=}" + + return regression_result + + +def regression_deployment_testing( + template, root, otx_dir, args, threshold=0.0, criteria=None, reg_threshold=0.10, result_dict=None +): + regression_result = { + "passed": True, + "log": "", + } + + template_work_dir = get_template_dir(template, root) + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--load-weights", + f"{template_work_dir}/deployed_{template.model_template_id}/openvino.zip", + "--output", + f"{template_work_dir}/deployed_{template.model_template_id}", + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/deployed_{template.model_template_id}/performance.json") + with open(f"{template_work_dir}/exported_{template.model_template_id}/performance.json") as read_file: + exported_performance = json.load(read_file) + with open(f"{template_work_dir}/deployed_{template.model_template_id}/performance.json") as read_file: + deployed_performance = json.load(read_file) + + for k in exported_performance.keys(): + if k == "avg_time_per_image": + continue + result_dict[k] = round(deployed_performance[k], 3) + if ( + deployed_performance[k] < exported_performance[k] + and abs(exported_performance[k] - deployed_performance[k]) / (exported_performance[k] + 1e-10) > threshold + ): + regression_result["passed"] = False + regression_result[ + "log" + ] = f"[{template.name}] {exported_performance[k]=}, {deployed_performance[k]=}, {threshold=}" + + return regression_result + + +def regression_nncf_eval_testing( + template, root, otx_dir, args, threshold=0.01, criteria=None, reg_threshold=0.10, result_dict=None +): + regression_result = { + "passed": True, + "log": "", + } + + template_work_dir = get_template_dir(template, root) + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--load-weights", + f"{template_work_dir}/nncf_{template.model_template_id}/weights.pth", + "--output", + f"{template_work_dir}/nncf_{template.model_template_id}", + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/nncf_{template.model_template_id}/performance.json") + with open(f"{template_work_dir}/nncf_{template.model_template_id}/nncf_performance.json") as read_file: + trained_performance = json.load(read_file) + with open(f"{template_work_dir}/nncf_{template.model_template_id}/performance.json") as read_file: + evaluated_performance = json.load(read_file) + + for k in trained_performance.keys(): + result_dict[k] = round(evaluated_performance[k], 3) + model_criteria = 0.0 + if template.name not in criteria.keys(): + regression_result["passed"] = False + regression_result["log"] = ( + f"Cannot find regression criteria for the template '{template.name}'. " + + f"{trained_performance=}, {evaluated_performance=}" + ) + else: + model_criteria = criteria[template.name] * (1.0 - threshold) + if evaluated_performance[k] < model_criteria: + regression_result["passed"] = False + regression_result[ + "log" + ] = f"[{template.name}] NNCF performance is lower than criteria: {evaluated_performance[k]=}, " + f"{model_criteria=}, {threshold=}" + elif evaluated_performance[k] < trained_performance[k]: + regression_result["passed"] = False + regression_result[ + "log" + ] = f"[{template.name}] NNCF eval performance is lower than train: {evaluated_performance[k]=}, " + f"{trained_performance=}" + elif abs(trained_performance[k] - evaluated_performance[k]) / (trained_performance[k] + 1e-10) > threshold: + regression_result["passed"] = False + regression_result[ + "log" + ] = f"[{template.name}] NNCF train & eval delta is too big: {evaluated_performance[k]=}, " + f"{trained_performance[k]=}, {threshold=}" + + return regression_result + + +def regression_ptq_eval_testing(template, root, otx_dir, args, criteria=None, reg_threshold=0.10, result_dict=None): + regression_result = { + "passed": True, + "log": "", + } + + template_work_dir = get_template_dir(template, root) + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--load-weights", + f"{template_work_dir}/ptq_{template.model_template_id}/openvino.xml", + "--output", + f"{template_work_dir}/ptq_{template.model_template_id}", + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/ptq_{template.model_template_id}/performance.json") + + with open(f"{template_work_dir}/ptq_{template.model_template_id}/performance.json") as read_file: + ptq_performance = json.load(read_file) + + for k in ptq_performance.keys(): + result_dict[k] = round(ptq_performance[k], 3) + model_criteria = 0.0 + if template.name not in criteria.keys(): + regression_result["passed"] = False + regression_result["log"] = ( + f"Cannot find regression criteria for the template '{template.name}'. " + f"{ptq_performance=}" + ) + else: + model_criteria = criteria[template.name] * (1.0 * reg_threshold) + if ptq_performance[k] < model_criteria: + regression_result["passed"] = False + regression_result[ + "log" + ] = f"[{template.name}] ptq performance: {ptq_performance[k]=}, {model_criteria=}, {reg_threshold=}" + + return regression_result + + +def regression_train_time_testing(train_time_criteria, e2e_train_time, template, threshold=0.30): + """Measure train+val time and comapre with test criteria. + + Test criteria was set by previous measurement. + """ + regression_result = { + "passed": True, + "log": "", + } + + e2e_train_time_criteria = train_time_criteria[template.name] if template.name in train_time_criteria.keys() else 0.0 + modified_train_criteria = e2e_train_time_criteria + (e2e_train_time_criteria * threshold) + + if e2e_train_time > modified_train_criteria: + regression_result["passed"] = False + regression_result[ + "log" + ] = f"[{template.name}] Train time: ({e2e_train_time}) < Criteria: ({modified_train_criteria})." + + return regression_result + + +def regression_eval_time_testing(eval_time_criteria, e2e_eval_time, template, threshold=0.30): + """Measure evaluation time and comapre with test criteria. + + Test criteria was set by previous measurement. + """ + regression_result = { + "passed": True, + "log": "", + } + + e2e_eval_time_criteria = eval_time_criteria[template.name] if template.name in eval_time_criteria.keys() else 0.0 + modified_eval_criteria = e2e_eval_time_criteria + (e2e_eval_time_criteria * threshold) + + if e2e_eval_time > modified_eval_criteria: + regression_result["passed"] = False + regression_result[ + "log" + ] = f"[{template.name}] Eval time: ({e2e_eval_time}) < criteria: ({modified_eval_criteria})." + + return regression_result diff --git a/tests/regression/regression_config.json b/tests/regression/regression_config.json new file mode 100644 index 00000000000..6429a5fd1e2 --- /dev/null +++ b/tests/regression/regression_config.json @@ -0,0 +1,1724 @@ +{ + "data_path": { + "classification": { + "supervised": { + "multi_class": { + "--train-data-roots": "classification/multiclass_CUB_cls_decr/train", + "--val-data-roots": "classification/multiclass_CUB_cls_decr/test", + "--test-data-roots": "classification/multiclass_CUB_cls_decr/test", + "--input": "classification/multiclass_CUB/test/Acadian_Flycatcher" + }, + "multi_label": { + "--train-data-roots": "classification/multi_label_coco_subset_cls_decr", + "--val-data-roots": "classification/multi_label_coco_subset_cls_decr", + "--test-data-roots": "classification/multi_label_coco_subset_cls_decr", + "--input": "classification/multi_label_coco_subset/images/test" + }, + "h_label": { + "--train-data-roots": "classification/h_label_cifar10_subset", + "--val-data-roots": "classification/h_label_cifar10_subset", + "--test-data-roots": "classification/h_label_cifar10_subset", + "--input": "classification/h_label_cifar10_subset/images/test/airplane" + }, + "supcon": { + "--train-data-roots": "classification/multiclass_CUB_cls_decr/train", + "--val-data-roots": "classification/multiclass_CUB_cls_decr/test", + "--test-data-roots": "classification/multiclass_CUB_cls_decr/test" + } + }, + "class_incr": { + "multi_class": { + "--train-data-roots": "classification/multiclass_CUB/train", + "--val-data-roots": "classification/multiclass_CUB/test", + "--test-data-roots": "classification/multiclass_CUB/test" + }, + "multi_label": { + "--train-data-roots": "classification/multi_label_coco_subset", + "--val-data-roots": "classification/multi_label_coco_subset", + "--test-data-roots": "classification/multi_label_coco_subset" + } + }, + "semi_supervised": { + "multi_class": { + "--train-data-roots": "classification/multiclass_CUB_cls_decr/train", + "--val-data-roots": "classification/multiclass_CUB_cls_decr/test", + "--test-data-roots": "classification/multiclass_CUB_cls_decr/test", + "--unlabeled-data-roots": "classification/CUB_unlabeled" + } + }, + "self_supervised": { + "multi_class": { + "--train-data-roots": "classification/multiclass_CUB_cls_decr/train" + } + } + }, + "detection": { + "supervised": { + "multi_class": { + "--train-data-roots": "detection/coco_subset_cls_decr", + "--val-data-roots": "detection/coco_subset_cls_decr", + "--test-data-roots": "detection/coco_subset_cls_decr", + "--input": "detection/coco_subset_cls_decr/images/test" + } + }, + "class_incr": { + "multi_class": { + "--train-data-roots": "detection/coco_subset", + "--val-data-roots": "detection/coco_subset", + "--test-data-roots": "detection/coco_subset" + } + }, + "semi_supervised": { + "multi_class": { + "--train-data-roots": "detection/coco_subset_cls_decr", + "--val-data-roots": "detection/coco_subset_cls_decr", + "--test-data-roots": "detection/coco_subset_cls_decr", + "--unlabeled-data-roots": "detection/coco_subset/images/unlabeled" + } + }, + "tiling": { + "multi_class": { + "--train-data-roots": "detection/coco_subset", + "--val-data-roots": "detection/coco_subset", + "--test-data-roots": "detection/coco_subset", + "--input": "detection/coco_subset/images/test" + } + } + }, + "segmentation": { + "supervised": { + "multi_class": { + "--train-data-roots": "segmentation/regression_voc_cls_decr/train", + "--val-data-roots": "segmentation/regression_voc_cls_decr/val", + "--test-data-roots": "segmentation/regression_voc_cls_decr/test", + "--input": "segmentation/regression_voc_cls_decr/test/images" + }, + "supcon": { + "--train-data-roots": "segmentation/regression_voc_cls_decr/train", + "--val-data-roots": "segmentation/regression_voc_cls_decr/val", + "--test-data-roots": "segmentation/regression_voc_cls_decr/test" + } + }, + "class_incr": { + "multi_class": { + "--train-data-roots": "segmentation/regression_voc_sl/train", + "--val-data-roots": "segmentation/regression_voc_sl/val", + "--test-data-roots": "segmentation/regression_voc_sl/test" + } + }, + "semi_supervised": { + "multi_class": { + "--train-data-roots": "segmentation/regression_voc_cls_decr/train", + "--val-data-roots": "segmentation/regression_voc_cls_decr/val", + "--test-data-roots": "segmentation/regression_voc_cls_decr/test", + "--unlabeled-data-roots": "segmentation/regression_voc_semisl/unlabeled" + } + }, + "self_supervised": { + "multi_class": { + "--train-data-roots": "segmentation/regression_voc_cls_decr/train" + } + } + }, + "instance_segmentation": { + "supervised": { + "multi_class": { + "--train-data-roots": "detection/coco_subset_cls_decr", + "--val-data-roots": "detection/coco_subset_cls_decr", + "--test-data-roots": "detection/coco_subset_cls_decr", + "--input": "detection/coco_subset_cls_decr/images/test" + } + }, + "class_incr": { + "multi_class": { + "--train-data-roots": "detection/coco_subset", + "--val-data-roots": "detection/coco_subset", + "--test-data-roots": "detection/coco_subset" + } + }, + "tiling": { + "multi_class": { + "--train-data-roots": "detection/coco_subset_cls_decr", + "--val-data-roots": "detection/coco_subset_cls_decr", + "--test-data-roots": "detection/coco_subset_cls_decr", + "--input": "detection/coco_subset/images/test" + } + } + }, + "action_classification": { + "supervised": { + "multi_class": { + "--train-data-roots": "action/classification/HMDB51_30percent/train", + "--val-data-roots": "action/classification/HMDB51_30percent/val", + "--test-data-roots": "action/classification/HMDB51_30percent/val" + } + } + }, + "action_detection": { + "supervised": { + "multi_class": { + "--train-data-roots": "action/detection/JHMDB_5percent/train", + "--val-data-roots": "action/detection/JHMDB_5percent/test", + "--test-data-roots": "action/detection/JHMDB_5percent/test" + } + } + }, + "anomaly_classification": { + "--train-data-roots": "anomaly/mvtec", + "--val-data-roots": "anomaly/mvtec", + "--test-data-roots": "anomaly/mvtec", + "--input": "anomaly/mvtec/bottle/test/contamination", + "train_params": [] + }, + "anomaly_detection": { + "--train-data-roots": "anomaly/mvtec", + "--val-data-roots": "anomaly/mvtec", + "--test-data-roots": "anomaly/mvtec", + "--input": "anomaly/mvtec/bottle/test/contamination", + "train_params": [] + }, + "anomaly_segmentation": { + "--train-data-roots": "anomaly/mvtec", + "--val-data-roots": "anomaly/mvtec", + "--test-data-roots": "anomaly/mvtec", + "--input": "anomaly/mvtec/bottle/test/contamination", + "train_params": [] + } + }, + + "regression_criteria": { + "action_classification": { + "supervised": { + "multi_class": { + "train": { + "MoViNet": 0.519, + "X3D": 0.613 + }, + "export": { + "MoViNet": 0.0, + "X3D": 0.0 + }, + "deploy": { + "MoViNet": 0.0, + "X3D": 0.0 + }, + "nncf": { + "MoViNet": 0.0, + "X3D": 0.0 + }, + "ptq": { + "MoViNet": 0.0, + "X3D": 0.0 + } + } + } + }, + "action_detection": { + "supervised": { + "multi_class": { + "train": { + "X3D_FAST_RCNN": 0.613 + }, + "export": { + "X3D_FAST_RCNN": 0.0 + }, + "deploy": { + "X3D_FAST_RCNN": 0.0 + }, + "nncf": { + "X3D_FAST_RCNN": 0.0 + }, + "ptq": { + "X3D_FAST_RCNN": 0.0 + } + } + } + }, + "anomaly_segmentation": { + "train": { + "carpet": { + "STFPM": 0.322, + "PADIM": 0.313 + }, + "wood": { + "STFPM": 0.31, + "PADIM": 0.355 + }, + "zipper": { + "STFPM": 0.357, + "PADIM": 0.232 + } + }, + "export": { + "carpet": { + "STFPM": 0.034, + "PADIM": 0.205 + }, + "wood": { + "STFPM": 0.129, + "PADIM": 0.208 + }, + "zipper": { + "STFPM": 0.357, + "PADIM": 0.232 + } + }, + "deploy": { + "carpet": { + "STFPM": 0.034, + "PADIM": 0.205 + }, + "wood": { + "STFPM": 0.129, + "PADIM": 0.208 + }, + "zipper": { + "STFPM": 0.357, + "PADIM": 0.232 + } + }, + "nncf": { + "carpet": { + "STFPM": 0.43, + "PADIM": 0.374 + }, + "wood": { + "STFPM": 0.448, + "PADIM": 0.312 + }, + "zipper": { + "STFPM": 0.483, + "PADIM": 0.305 + } + }, + "ptq": { + "carpet": { + "STFPM": 0.034, + "PADIM": 0.225 + }, + "wood": { + "STFPM": 0.127, + "PADIM": 0.227 + }, + "zipper": { + "STFPM": 0.354, + "PADIM": 0.195 + } + } + }, + "anomaly_detection": { + "train": { + "carpet": { + "STFPM": 0.167, + "PADIM": 0.267 + }, + "wood": { + "STFPM": 0.103, + "PADIM": 0.159 + }, + "zipper": { + "STFPM": 0.108, + "PADIM": 0.063 + } + }, + "export": { + "carpet": { + "STFPM": 0.045, + "PADIM": 0.226 + }, + "wood": { + "STFPM": 0.063, + "PADIM": 0.153 + }, + "zipper": { + "STFPM": 0.101, + "PADIM": 0.062 + } + }, + "deploy": { + "carpet": { + "STFPM": 0.045, + "PADIM": 0.226 + }, + "wood": { + "STFPM": 0.063, + "PADIM": 0.153 + }, + "zipper": { + "STFPM": 0.101, + "PADIM": 0.062 + } + }, + "nncf": { + "carpet": { + "STFPM": 0.145, + "PADIM": 0.164 + }, + "wood": { + "STFPM": 0.09, + "PADIM": 0.223 + }, + "zipper": { + "STFPM": 0.166, + "PADIM": 0.067 + } + }, + "ptq": { + "carpet": { + "STFPM": 0.037, + "PADIM": 0.208 + }, + "wood": { + "STFPM": 0.053, + "PADIM": 0.159 + }, + "zipper": { + "STFPM": 0.125, + "PADIM": 0.043 + } + } + }, + "anomaly_classification": { + "train": { + "carpet": { + "STFPM": 0.829, + "PADIM": 0.761 + }, + "wood": { + "STFPM": 0.886, + "PADIM": 0.924 + }, + "zipper": { + "STFPM": 0.788, + "PADIM": 0.821 + } + }, + "export": { + "carpet": { + "STFPM": 0.784, + "PADIM": 0.761 + }, + "wood": { + "STFPM": 0.8, + "PADIM": 0.82 + }, + "zipper": { + "STFPM": 0.781, + "PADIM": 0.815 + } + }, + "deploy": { + "carpet": { + "STFPM": 0.784, + "PADIM": 0.761 + }, + "wood": { + "STFPM": 0.8, + "PADIM": 0.82 + }, + "zipper": { + "STFPM": 0.781, + "PADIM": 0.815 + } + }, + "nncf": { + "carpet": { + "STFPM": 0.88, + "PADIM": 0.761 + }, + "wood": { + "STFPM": 0.886, + "PADIM": 0.937 + }, + "zipper": { + "STFPM": 0.821, + "PADIM": 0.808 + } + }, + "ptq": { + "carpet": { + "STFPM": 0.796, + "PADIM": 0.739 + }, + "wood": { + "STFPM": 0.8, + "PADIM": 0.826 + }, + "zipper": { + "STFPM": 0.788, + "PADIM": 0.768 + } + } + }, + "classification": { + "supervised": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 0.778, + "EfficientNet-B0": 0.699, + "MobileNet-V3-large-1x": 0.687, + "DeiT-Tiny": 0.596 + }, + "export": { + "EfficientNet-V2-S": 0.778, + "EfficientNet-B0": 0.698, + "MobileNet-V3-large-1x": 0.686, + "DeiT-Tiny": 0.597 + }, + "deploy": { + "EfficientNet-V2-S": 0.778, + "EfficientNet-B0": 0.698, + "MobileNet-V3-large-1x": 0.686, + "DeiT-Tiny": 0.597 + }, + "nncf": { + "EfficientNet-V2-S": 0.776, + "EfficientNet-B0": 0.691, + "MobileNet-V3-large-1x": 0.677, + "DeiT-Tiny": 0.0 + }, + "ptq": { + "EfficientNet-V2-S": 0.768, + "EfficientNet-B0": 0.681, + "MobileNet-V3-large-1x": 0.624, + "DeiT-Tiny": 0.594 + } + }, + "multi_label": { + "train": { + "EfficientNet-V2-S": 0.968, + "EfficientNet-B0": 0.958, + "MobileNet-V3-large-1x": 0.965, + "DeiT-Tiny": 0.952 + }, + "export": { + "EfficientNet-V2-S": 0.968, + "EfficientNet-B0": 0.958, + "MobileNet-V3-large-1x": 0.965, + "DeiT-Tiny": 0.952 + }, + "deploy": { + "EfficientNet-V2-S": 0.968, + "EfficientNet-B0": 0.958, + "MobileNet-V3-large-1x": 0.965, + "DeiT-Tiny": 0.952 + }, + "nncf": { + "EfficientNet-V2-S": 0.971, + "EfficientNet-B0": 0.961, + "MobileNet-V3-large-1x": 0.965, + "DeiT-Tiny": 0.0 + }, + "ptq": { + "EfficientNet-V2-S": 0.971, + "EfficientNet-B0": 0.96, + "MobileNet-V3-large-1x": 0.965, + "DeiT-Tiny": 0.952 + } + }, + "h_label": { + "train": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 + }, + "export": { + "EfficientNet-V2-S": 0.761, + "EfficientNet-B0": 0.737, + "MobileNet-V3-large-1x": 0.736, + "DeiT-Tiny": 0.768 + }, + "deploy": { + "EfficientNet-V2-S": 0.761, + "EfficientNet-B0": 0.737, + "MobileNet-V3-large-1x": 0.736, + "DeiT-Tiny": 0.768 + }, + "nncf": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 + }, + "ptq": { + "EfficientNet-V2-S": 0.757, + "EfficientNet-B0": 0.727, + "MobileNet-V3-large-1x": 0.691, + "DeiT-Tiny": 0.768 + } + }, + "supcon": { + "train": { + "EfficientNet-V2-S": 0.773, + "EfficientNet-B0": 0.675, + "MobileNet-V3-large-1x": 0.677 + }, + "export": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + }, + "deploy": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + }, + "nncf": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + }, + "ptq": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + } + } + }, + "semi_supervised": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 0.758, + "EfficientNet-B0": 0.674, + "MobileNet-V3-large-1x": 0.658, + "DeiT-Tiny": 0.656 + }, + "export": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 + }, + "deploy": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 + }, + "nncf": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 + }, + "ptq": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 + } + } + }, + "self_supervised": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 0.776, + "EfficientNet-B0": 0.687, + "MobileNet-V3-large-1x": 0.662 + }, + "export": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + }, + "deploy": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + }, + "nncf": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + }, + "ptq": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 0.783, + "EfficientNet-B0": 0.702, + "MobileNet-V3-large-1x": 0.687 + }, + "export": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + }, + "deploy": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + }, + "nncf": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + }, + "ptq": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + } + }, + "multi_label": { + "train": { + "EfficientNet-V2-S": 0.97, + "EfficientNet-B0": 0.954, + "MobileNet-V3-large-1x": 0.96, + "DeiT-Tiny": 0.954 + }, + "export": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 + }, + "deploy": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 + }, + "nncf": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 + }, + "ptq": { + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 + } + } + } + }, + "detection": { + "tiling": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 0.662, + "YOLOX-S": 0.389, + "MobileNetV2-ATSS": 0.474, + "SSD": 0.228, + "YOLOX-TINY": 0.549, + "YOLOX-L": 0.655, + "YOLOX-X": 0.675 + }, + "export": { + "ResNeXt101-ATSS": 0.663, + "YOLOX-S": 0.386, + "MobileNetV2-ATSS": 0.475, + "SSD": 0.228, + "YOLOX-TINY": 0.549, + "YOLOX-L": 0.654, + "YOLOX-X": 0.674 + }, + "deploy": { + "ResNeXt101-ATSS": 0.663, + "YOLOX-S": 0.386, + "MobileNetV2-ATSS": 0.475, + "SSD": 0.228, + "YOLOX-TINY": 0.549, + "YOLOX-L": 0.654, + "YOLOX-X": 0.674 + }, + "nncf": { + "ResNeXt101-ATSS": 0.655, + "YOLOX-S": 0.0, + "MobileNetV2-ATSS": 0.0, + "SSD": 0.0, + "YOLOX-TINY": 0.0, + "YOLOX-L": 0.0, + "YOLOX-X": 0.0 + }, + "ptq": { + "ResNeXt101-ATSS": 0.651, + "YOLOX-S": 0.423, + "MobileNetV2-ATSS": 0.465, + "SSD": 0.229, + "YOLOX-TINY": 0.54, + "YOLOX-L": 0.677, + "YOLOX-X": 0.691 + } + } + }, + "supervised": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 0.635, + "YOLOX-S": 0.429, + "MobileNetV2-ATSS": 0.467, + "SSD": 0.221, + "YOLOX-TINY": 0.577, + "YOLOX-L": 0.684, + "YOLOX-X": 0.696 + }, + "export": { + "ResNeXt101-ATSS": 0.635, + "YOLOX-S": 0.429, + "MobileNetV2-ATSS": 0.466, + "SSD": 0.222, + "YOLOX-TINY": 0.575, + "YOLOX-L": 0.681, + "YOLOX-X": 0.69 + }, + "deploy": { + "ResNeXt101-ATSS": 0.635, + "YOLOX-S": 0.429, + "MobileNetV2-ATSS": 0.466, + "SSD": 0.222, + "YOLOX-TINY": 0.575, + "YOLOX-L": 0.681, + "YOLOX-X": 0.69 + }, + "nncf": { + "ResNeXt101-ATSS": 0.63, + "YOLOX-S": 0.429, + "MobileNetV2-ATSS": 0.462, + "SSD": 0.217, + "YOLOX-TINY": 0.571, + "YOLOX-L": 0.68, + "YOLOX-X": 0.691 + }, + "ptq": { + "ResNeXt101-ATSS": 0.632, + "YOLOX-S": 0.424, + "MobileNetV2-ATSS": 0.461, + "SSD": 0.219, + "YOLOX-TINY": 0.568, + "YOLOX-L": 0.675, + "YOLOX-X": 0.689 + } + } + }, + "semi_supervised": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 0.635, + "YOLOX-S": 0.074, + "MobileNetV2-ATSS": 0.374, + "SSD": 0.158, + "YOLOX-TINY": 0.453, + "YOLOX-L": 0.652, + "YOLOX-X": 0.673 + }, + "export": { + "ResNeXt101-ATSS": 0.0, + "YOLOX-S": 0.0, + "MobileNetV2-ATSS": 0.0, + "SSD": 0.0, + "YOLOX-TINY": 0.0, + "YOLOX-L": 0.0, + "YOLOX-X": 0.0 + }, + "deploy": { + "ResNeXt101-ATSS": 0.0, + "YOLOX-S": 0.0, + "MobileNetV2-ATSS": 0.0, + "SSD": 0.0, + "YOLOX-TINY": 0.0, + "YOLOX-L": 0.0, + "YOLOX-X": 0.0 + }, + "nncf": { + "ResNeXt101-ATSS": 0.0, + "YOLOX-S": 0.0, + "MobileNetV2-ATSS": 0.0, + "SSD": 0.0, + "YOLOX-TINY": 0.0, + "YOLOX-L": 0.0, + "YOLOX-X": 0.0 + }, + "ptq": { + "ResNeXt101-ATSS": 0.0, + "YOLOX-S": 0.0, + "MobileNetV2-ATSS": 0.0, + "SSD": 0.0, + "YOLOX-TINY": 0.0, + "YOLOX-L": 0.0, + "YOLOX-X": 0.0 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 0.631, + "YOLOX-S": 0.425, + "MobileNetV2-ATSS": 0.463, + "SSD": 0.23, + "YOLOX-TINY": 0.572, + "YOLOX-L": 0.679, + "YOLOX-X": 0.691 + }, + "export": { + "ResNeXt101-ATSS": 0.0, + "YOLOX-S": 0.0, + "MobileNetV2-ATSS": 0.0, + "SSD": 0.0, + "YOLOX-TINY": 0.0, + "YOLOX-L": 0.0, + "YOLOX-X": 0.0 + }, + "deploy": { + "ResNeXt101-ATSS": 0.0, + "YOLOX-S": 0.0, + "MobileNetV2-ATSS": 0.0, + "SSD": 0.0, + "YOLOX-TINY": 0.0, + "YOLOX-L": 0.0, + "YOLOX-X": 0.0 + }, + "nncf": { + "ResNeXt101-ATSS": 0.0, + "YOLOX-S": 0.0, + "MobileNetV2-ATSS": 0.0, + "SSD": 0.0, + "YOLOX-TINY": 0.0, + "YOLOX-L": 0.0, + "YOLOX-X": 0.0 + }, + "ptq": { + "ResNeXt101-ATSS": 0.0, + "YOLOX-S": 0.0, + "MobileNetV2-ATSS": 0.0, + "SSD": 0.0, + "YOLOX-TINY": 0.0, + "YOLOX-L": 0.0, + "YOLOX-X": 0.0 + } + } + } + }, + "instance_segmentation": { + "supervised": { + "multi_class": { + "train": { + "MaskRCNN-SwinT-FP16": 0.477, + "MaskRCNN-EfficientNetB2B": 0.333, + "MaskRCNN-ResNet50": 0.448 + }, + "export": { + "MaskRCNN-SwinT-FP16": 0.482, + "MaskRCNN-EfficientNetB2B": 0.347, + "MaskRCNN-ResNet50": 0.47 + }, + "deploy": { + "MaskRCNN-SwinT-FP16": 0.482, + "MaskRCNN-EfficientNetB2B": 0.347, + "MaskRCNN-ResNet50": 0.47 + }, + "nncf": { + "MaskRCNN-SwinT-FP16": 0.457, + "MaskRCNN-EfficientNetB2B": 0.0, + "MaskRCNN-ResNet50": 0.0 + }, + "ptq": { + "MaskRCNN-SwinT-FP16": 0.453, + "MaskRCNN-EfficientNetB2B": 0.337, + "MaskRCNN-ResNet50": 0.467 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "MaskRCNN-SwinT-FP16": 0.476, + "MaskRCNN-EfficientNetB2B": 0.306, + "MaskRCNN-ResNet50": 0.487 + }, + "export": { + "MaskRCNN-SwinT-FP16": 0.0, + "MaskRCNN-EfficientNetB2B": 0.0, + "MaskRCNN-ResNet50": 0.0 + }, + "deploy": { + "MaskRCNN-SwinT-FP16": 0.0, + "MaskRCNN-EfficientNetB2B": 0.0, + "MaskRCNN-ResNet50": 0.0 + }, + "nncf": { + "MaskRCNN-SwinT-FP16": 0.0, + "MaskRCNN-EfficientNetB2B": 0.0, + "MaskRCNN-ResNet50": 0.0 + }, + "ptq": { + "MaskRCNN-SwinT-FP16": 0.0, + "MaskRCNN-EfficientNetB2B": 0.0, + "MaskRCNN-ResNet50": 0.0 + } + } + } + }, + "segmentation": { + "supervised": { + "multi_class": { + "train": { + "SegNext-B": 0.823, + "Lite-HRNet-18": 0.706, + "SegNext-t": 0.769, + "SegNext-s": 0.796, + "Lite-HRNet-18-mod2": 0.709, + "Lite-HRNet-s-mod2": 0.681, + "Lite-HRNet-x-mod3": 0.678 + }, + "export": { + "SegNext-B": 0.823, + "Lite-HRNet-18": 0.705, + "SegNext-t": 0.769, + "SegNext-s": 0.795, + "Lite-HRNet-18-mod2": 0.708, + "Lite-HRNet-s-mod2": 0.681, + "Lite-HRNet-x-mod3": 0.678 + }, + "deploy": { + "SegNext-B": 0.823, + "Lite-HRNet-18": 0.705, + "SegNext-t": 0.769, + "SegNext-s": 0.795, + "Lite-HRNet-18-mod2": 0.708, + "Lite-HRNet-s-mod2": 0.681, + "Lite-HRNet-x-mod3": 0.678 + }, + "nncf": { + "SegNext-B": 0.0, + "Lite-HRNet-18": 0.0, + "SegNext-t": 0.0, + "SegNext-s": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "ptq": { + "SegNext-B": 0.821, + "Lite-HRNet-18": 0.121, + "SegNext-t": 0.756, + "SegNext-s": 0.794, + "Lite-HRNet-18-mod2": 0.706, + "Lite-HRNet-s-mod2": 0.619, + "Lite-HRNet-x-mod3": 0.623 + } + }, + "supcon": { + "train": { + "Lite-HRNet-18": 0.551, + "Lite-HRNet-18-mod2": 0.539, + "Lite-HRNet-s-mod2": 0.466, + "Lite-HRNet-x-mod3": 0.551 + }, + "export": { + "Lite-HRNet-18": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "deploy": { + "Lite-HRNet-18": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "nncf": { + "Lite-HRNet-18": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "ptq": { + "Lite-HRNet-18": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + } + } + }, + "semi_supervised": { + "multi_class": { + "train": { + "SegNext-B": 0.825, + "Lite-HRNet-18": 0.665, + "SegNext-t": 0.73, + "SegNext-s": 0.746, + "Lite-HRNet-18-mod2": 0.677, + "Lite-HRNet-s-mod2": 0.681, + "Lite-HRNet-x-mod3": 0.658 + }, + "export": { + "SegNext-B": 0.0, + "Lite-HRNet-18": 0.0, + "SegNext-t": 0.0, + "SegNext-s": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "deploy": { + "SegNext-B": 0.0, + "Lite-HRNet-18": 0.0, + "SegNext-t": 0.0, + "SegNext-s": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "nncf": { + "SegNext-B": 0.0, + "Lite-HRNet-18": 0.0, + "SegNext-t": 0.0, + "SegNext-s": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "ptq": { + "SegNext-B": 0.0, + "Lite-HRNet-18": 0.0, + "SegNext-t": 0.0, + "SegNext-s": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + } + } + }, + "self_supervised": { + "multi_class": { + "train": { + "SegNext-B": 0.797, + "Lite-HRNet-18": 0.657, + "SegNext-t": 0.653, + "SegNext-s": 0.759, + "Lite-HRNet-18-mod2": 0.647, + "Lite-HRNet-s-mod2": 0.633, + "Lite-HRNet-x-mod3": 0.658 + }, + "export": { + "SegNext-B": 0.0, + "Lite-HRNet-18": 0.0, + "SegNext-t": 0.0, + "SegNext-s": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "deploy": { + "SegNext-B": 0.0, + "Lite-HRNet-18": 0.0, + "SegNext-t": 0.0, + "SegNext-s": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "nncf": { + "SegNext-B": 0.0, + "Lite-HRNet-18": 0.0, + "SegNext-t": 0.0, + "SegNext-s": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "ptq": { + "SegNext-B": 0.0, + "Lite-HRNet-18": 0.0, + "SegNext-t": 0.0, + "SegNext-s": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "Lite-HRNet-18": 0.635, + "Lite-HRNet-18-mod2": 0.656, + "Lite-HRNet-s-mod2": 0.639, + "Lite-HRNet-x-mod3": 0.565 + }, + "export": { + "Lite-HRNet-18": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "deploy": { + "Lite-HRNet-18": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "nncf": { + "Lite-HRNet-18": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + }, + "ptq": { + "Lite-HRNet-18": 0.0, + "Lite-HRNet-18-mod2": 0.0, + "Lite-HRNet-s-mod2": 0.0, + "Lite-HRNet-x-mod3": 0.0 + } + } + } + } + }, + "kpi_e2e_train_time_criteria": { + "action_classification": { + "supervised": { + "multi_class": { + "train": { + "MoViNet": 261.901, + "X3D": 293.573 + } + } + } + }, + "action_detection": { + "supervised": { + "multi_class": { + "train": { + "X3D_FAST_RCNN": 1324.578 + } + } + } + }, + "anomaly_segmentation": { + "train": { + "carpet": { + "STFPM": 606.461, + "PADIM": 51.744 + }, + "wood": { + "STFPM": 843.012, + "PADIM": 54.485 + }, + "zipper": { + "STFPM": 570.453, + "PADIM": 52.987 + } + } + }, + "anomaly_detection": { + "train": { + "carpet": { + "STFPM": 380.418, + "PADIM": 35.742 + }, + "wood": { + "STFPM": 233.758, + "PADIM": 35.502 + }, + "zipper": { + "STFPM": 389.025, + "PADIM": 36.59 + } + } + }, + "anomaly_classification": { + "train": { + "carpet": { + "STFPM": 483.17, + "PADIM": 33.738 + }, + "wood": { + "STFPM": 391.582, + "PADIM": 33.527 + }, + "zipper": { + "STFPM": 284.238, + "PADIM": 33.347 + } + } + }, + "classification": { + "supervised": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 218.197, + "EfficientNet-B0": 178.446, + "MobileNet-V3-large-1x": 166.941, + "DeiT-Tiny": 164.726 + } + }, + "multi_label": { + "train": { + "EfficientNet-V2-S": 184.384, + "EfficientNet-B0": 126.295, + "MobileNet-V3-large-1x": 102.924, + "DeiT-Tiny": 123.242 + } + }, + "h_label": { + "train": { + "EfficientNet-V2-S": 110.326, + "EfficientNet-B0": 130.742, + "MobileNet-V3-large-1x": 115.362, + "DeiT-Tiny": 117.818 + } + }, + "supcon": { + "train": { + "EfficientNet-V2-S": 262.529, + "EfficientNet-B0": 218.38, + "MobileNet-V3-large-1x": 201.337 + } + } + }, + "semi_supervised": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 1114.203, + "EfficientNet-B0": 659.838, + "MobileNet-V3-large-1x": 545.86, + "DeiT-Tiny": 403.769 + } + } + }, + "self_supervised": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 19.834, + "EfficientNet-B0": 16.462, + "MobileNet-V3-large-1x": 15.144 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 202.416, + "EfficientNet-B0": 229.533, + "MobileNet-V3-large-1x": 144.591 + } + }, + "multi_label": { + "train": { + "EfficientNet-V2-S": 175.599, + "EfficientNet-B0": 180.583, + "MobileNet-V3-large-1x": 118.757, + "DeiT-Tiny": 139.84 + } + } + } + }, + "detection": { + "tiling": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 1252.666, + "YOLOX-S": 280.936, + "MobileNetV2-ATSS": 354.523, + "SSD": 298.68, + "YOLOX-TINY": 212.431, + "YOLOX-L": 395.392, + "YOLOX-X": 800.653 + } + } + }, + "supervised": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 2121.852, + "YOLOX-S": 620.867, + "MobileNetV2-ATSS": 316.917, + "SSD": 271.298, + "YOLOX-TINY": 464.486, + "YOLOX-L": 816.651, + "YOLOX-X": 1672.582 + } + } + }, + "semi_supervised": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 2666.141, + "YOLOX-S": 443.316, + "MobileNetV2-ATSS": 536.618, + "SSD": 429.656, + "YOLOX-TINY": 406.971, + "YOLOX-L": 728.398, + "YOLOX-X": 1814.394 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 2098.555, + "YOLOX-S": 626.248, + "MobileNetV2-ATSS": 518.843, + "SSD": 401.059, + "YOLOX-TINY": 471.214, + "YOLOX-L": 848.263, + "YOLOX-X": 1671.449 + } + } + } + }, + "instance_segmentation": { + "supervised": { + "multi_class": { + "train": { + "MaskRCNN-SwinT-FP16": 1364.765, + "MaskRCNN-EfficientNetB2B": 605.386, + "MaskRCNN-ResNet50": 1284.017 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "MaskRCNN-SwinT-FP16": 1369.209, + "MaskRCNN-EfficientNetB2B": 984.011, + "MaskRCNN-ResNet50": 1285.941 + } + } + } + }, + "segmentation": { + "supervised": { + "multi_class": { + "train": { + "SegNext-B": 125.655, + "Lite-HRNet-18": 100.721, + "SegNext-t": 75.879, + "SegNext-s": 84.361, + "Lite-HRNet-18-mod2": 99.325, + "Lite-HRNet-s-mod2": 87.493, + "Lite-HRNet-x-mod3": 152.836 + } + }, + "supcon": { + "train": { + "Lite-HRNet-18": 130.635, + "Lite-HRNet-18-mod2": 129.879, + "Lite-HRNet-s-mod2": 117.903, + "Lite-HRNet-x-mod3": 181.323 + } + } + }, + "semi_supervised": { + "multi_class": { + "train": { + "SegNext-B": 159.307, + "Lite-HRNet-18": 147.132, + "SegNext-t": 106.195, + "SegNext-s": 114.734, + "Lite-HRNet-18-mod2": 143.903, + "Lite-HRNet-s-mod2": 122.088, + "Lite-HRNet-x-mod3": 236.362 + } + } + }, + "self_supervised": { + "multi_class": { + "train": { + "SegNext-B": 185.828, + "Lite-HRNet-18": 158.973, + "SegNext-t": 143.487, + "SegNext-s": 149.674, + "Lite-HRNet-18-mod2": 161.458, + "Lite-HRNet-s-mod2": 150.652, + "Lite-HRNet-x-mod3": 205.917 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "Lite-HRNet-18": 148.859, + "Lite-HRNet-18-mod2": 148.226, + "Lite-HRNet-s-mod2": 125.109, + "Lite-HRNet-x-mod3": 255.65 + } + } + } + } + }, + "kpi_e2e_eval_time_criteria": { + "action_classification": { + "supervised": { + "multi_class": { + "train": { + "MoViNet": 39.944, + "X3D": 42.252 + } + } + } + }, + "action_detection": { + "supervised": { + "multi_class": { + "train": { + "X3D_FAST_RCNN": 94.44 + } + } + } + }, + "anomaly_segmentation": { + "train": { + "carpet": { + "STFPM": 11.986, + "PADIM": 10.973 + }, + "wood": { + "STFPM": 14.704, + "PADIM": 13.279 + }, + "zipper": { + "STFPM": 14.002, + "PADIM": 13.41 + } + } + }, + "anomaly_detection": { + "train": { + "carpet": { + "STFPM": 8.138, + "PADIM": 8.243 + }, + "wood": { + "STFPM": 7.5, + "PADIM": 7.6 + }, + "zipper": { + "STFPM": 8.728, + "PADIM": 8.825 + } + } + }, + "anomaly_classification": { + "train": { + "carpet": { + "STFPM": 5.954, + "PADIM": 6.075 + }, + "wood": { + "STFPM": 5.839, + "PADIM": 5.853 + }, + "zipper": { + "STFPM": 5.778, + "PADIM": 5.972 + } + } + }, + "classification": { + "supervised": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 15.683, + "EfficientNet-B0": 14.97, + "MobileNet-V3-large-1x": 15.073, + "DeiT-Tiny": 55.993 + } + }, + "multi_label": { + "train": { + "EfficientNet-V2-S": 6.952, + "EfficientNet-B0": 6.299, + "MobileNet-V3-large-1x": 6.105, + "DeiT-Tiny": 8.112 + } + }, + "h_label": { + "train": { + "EfficientNet-V2-S": 10.636, + "EfficientNet-B0": 9.836, + "MobileNet-V3-large-1x": 9.835, + "DeiT-Tiny": 26.898 + } + }, + "supcon": { + "train": { + "EfficientNet-V2-S": 15.597, + "EfficientNet-B0": 14.843, + "MobileNet-V3-large-1x": 14.996 + } + } + }, + "semi_supervised": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 16.646, + "EfficientNet-B0": 16.055, + "MobileNet-V3-large-1x": 15.865, + "DeiT-Tiny": 66.312 + } + } + }, + "self_supervised": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 15.71, + "EfficientNet-B0": 15.043, + "MobileNet-V3-large-1x": 15.007 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "EfficientNet-V2-S": 16.179, + "EfficientNet-B0": 15.279, + "MobileNet-V3-large-1x": 15.256 + } + }, + "multi_label": { + "train": { + "EfficientNet-V2-S": 6.893, + "EfficientNet-B0": 6.312, + "MobileNet-V3-large-1x": 6.107, + "DeiT-Tiny": 8.018 + } + } + } + }, + "detection": { + "tiling": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 104.089, + "YOLOX-S": 105.713, + "MobileNetV2-ATSS": 59.865, + "SSD": 135.033, + "YOLOX-TINY": 58.541, + "YOLOX-L": 110.035, + "YOLOX-X": 124.0 + } + } + }, + "supervised": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 44.526, + "YOLOX-S": 14.622, + "MobileNetV2-ATSS": 14.839, + "SSD": 13.216, + "YOLOX-TINY": 13.123, + "YOLOX-L": 27.983, + "YOLOX-X": 30.28 + } + } + }, + "semi_supervised": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 41.62, + "YOLOX-S": 12.791, + "MobileNetV2-ATSS": 14.857, + "SSD": 13.156, + "YOLOX-TINY": 16.424, + "YOLOX-L": 22.869, + "YOLOX-X": 37.766 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "ResNeXt101-ATSS": 44.94, + "YOLOX-S": 26.482, + "MobileNetV2-ATSS": 24.495, + "SSD": 16.785, + "YOLOX-TINY": 22.293, + "YOLOX-L": 30.249, + "YOLOX-X": 35.993 + } + } + } + }, + "instance_segmentation": { + "supervised": { + "multi_class": { + "train": { + "MaskRCNN-SwinT-FP16": 47.091, + "MaskRCNN-EfficientNetB2B": 30.544, + "MaskRCNN-ResNet50": 40.727 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "MaskRCNN-SwinT-FP16": 47.186, + "MaskRCNN-EfficientNetB2B": 30.885, + "MaskRCNN-ResNet50": 39.559 + } + } + } + }, + "segmentation": { + "supervised": { + "multi_class": { + "train": { + "SegNext-B": 12.568, + "Lite-HRNet-18": 14.135, + "SegNext-t": 10.923, + "SegNext-s": 10.89, + "Lite-HRNet-18-mod2": 14.046, + "Lite-HRNet-s-mod2": 12.14, + "Lite-HRNet-x-mod3": 21.656 + } + }, + "supcon": { + "train": { + "Lite-HRNet-18": 15.703, + "Lite-HRNet-18-mod2": 16.187, + "Lite-HRNet-s-mod2": 12.206, + "Lite-HRNet-x-mod3": 27.72 + } + } + }, + "semi_supervised": { + "multi_class": { + "train": { + "SegNext-B": 13.182, + "Lite-HRNet-18": 14.826, + "SegNext-t": 11.236, + "SegNext-s": 11.108, + "Lite-HRNet-18-mod2": 14.542, + "Lite-HRNet-s-mod2": 12.768, + "Lite-HRNet-x-mod3": 21.772 + } + } + }, + "self_supervised": { + "multi_class": { + "train": { + "SegNext-B": 12.84, + "Lite-HRNet-18": 12.651, + "SegNext-t": 10.582, + "SegNext-s": 12.587, + "Lite-HRNet-18-mod2": 18.138, + "Lite-HRNet-s-mod2": 13.276, + "Lite-HRNet-x-mod3": 20.736 + } + } + }, + "class_incr": { + "multi_class": { + "train": { + "Lite-HRNet-18": 17.608, + "Lite-HRNet-18-mod2": 14.745, + "Lite-HRNet-s-mod2": 13.395, + "Lite-HRNet-x-mod3": 23.336 + } + } + } + } + } +} diff --git a/tests/regression/regression_test_helpers.py b/tests/regression/regression_test_helpers.py new file mode 100644 index 00000000000..5cc308720c0 --- /dev/null +++ b/tests/regression/regression_test_helpers.py @@ -0,0 +1,240 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import json +import os +from copy import copy +from pathlib import Path +from typing import Any, Dict, List, Union + +import torch + +from otx.api.entities.model_template import ModelTemplate + +TEST_TYPES = ["train", "export", "deploy", "nncf", "ptq"] +TASK_TYPES = [ + "classification", + "detection", + "semantic_segmentation", + "instance_segmentation", + "action_classification", + "action_detection", + "anomaly", +] +TASKS_TO_RUN_SIGNLE_GPU = [ + "detection", + "semantic_segmentation", + "instance_segmentation", +] +TRAIN_TYPES = ["supervised", "semi_supervised", "self_supervised", "class_incr", "tiling"] +LABEL_TYPES = ["multi_class", "multi_label", "h_label", "supcon"] + +REGRESSION_TEST_EPOCHS = "10" + +ANOMALY_DATASET_CATEGORIES = [ + # "bottle", + # "cable", + # "capsule", + "carpet", + # "grid", + # "hazelnut", + # "leather", + # "metal_nut", + # "pill", + # "screw", + # "tile", + # "toothbrush", + # "transistor", + "wood", + "zipper", +] + + +TIME_LOG = { + "train_time": "Train + val time (sec.)", + "infer_time": "Infer time (sec.)", + "export_time": "Export time (sec.)", + "export_eval_time": "Export eval time (sec.)", + "deploy_time": "Deploy time (sec.)", + "deploy_eval_time": "Deploy eval time (sec.)", + "nncf_time": "NNCF time (sec.)", + "nncf_eval_time": "NNCF eval time (sec.)", + "ptq_time": "PTQ time (sec.)", + "ptq_eval_time": "PTQ eval time (sec.)", +} + + +class RegressionTestConfig(object): + """Configurations for regression test.""" + + def __init__(self, task_type, train_type, label_type, otx_dir, **kwargs): + self.task_type = task_type + self.train_type = train_type + self.label_type = label_type + self.otx_dir = otx_dir + + self._result_dict = {} + results_root = kwargs.get("results_root", "/tmp/reg_test_results") + result_suffix = copy(self.task_type) + if result_suffix.startswith("action_"): + result_suffix = "action" + elif result_suffix.startswith("anomaly_"): + result_suffix = "anomaly" + self.result_dir = os.path.join(results_root, "reg_test_results", f"{result_suffix}") + Path(self.result_dir).mkdir(parents=True, exist_ok=True) + self.config_dict = self.load_config() + self.args = self.config_dict["data_path"] + train_params = kwargs.get("train_params") + if train_params is not None: + self.args["train_params"] = ["params"] + self.args["train_params"].extend(train_params) + + self.num_cuda_devices = torch.cuda.device_count() + if self.task_type in TASKS_TO_RUN_SIGNLE_GPU and self.num_cuda_devices > 0: + self.num_cuda_devices = 1 + self.update_gpu_args(self.args, enable_auto_num_worker=kwargs.get("enable_auto_num_worker", True)) + + @property + def result_dict(self): + return self._result_dict + + def dump_result_dict(self, dump_path=None): + dump_path_ = ( + dump_path + if dump_path is not None + else os.path.join(self.result_dir, f"result_{self.task_type}_{self.train_type}_{self.label_type}.json") + ) + print(f"writing regression result to {dump_path_}") + with open(dump_path_, "w") as result_file: + json.dump(self.result_dict, result_file, indent=4) + + def update_gpu_args(self, args, enable_auto_num_worker=True): + if self.num_cuda_devices > 1: + if enable_auto_num_worker: + if args.get("train_params") is None: + args["train_params"] = ["params"] + train_params = args.get("train_params") + train_params.append("--learning_parameters.auto_num_workers") + train_params.append("True") + args["--gpus"] = "0,1" + + def _load_config_from_json(self) -> Dict[str, Any]: + """Load regression config from path. + + Returns: + Dict[str, Any]: The dictionary that includes data roots + """ + root_path = Path(self.otx_dir) + with open(root_path / ("tests/regression/regression_config.json"), "r") as f: + reg_config = json.load(f) + return reg_config + + def load_config(self, **kwargs) -> Dict[str, Union[int, float]]: + """load dataset path according to task, train, label types. + + Returns: + Dict[str, Union[int, float]]: The dictionary that includes model criteria + """ + task_type = kwargs.get("task_type", self.task_type) + train_type = kwargs.get("train_type", self.train_type) + label_type = kwargs.get("label_type", self.label_type) + + reg_config = self._load_config_from_json() + result: Dict[str, Union[str, int, float]] = { + "data_path": "", + "model_criteria": 0, + } + + data_root = os.environ.get("CI_DATA_ROOT", "/storageserver/pvd_data/otx_data_archive/regression_datasets") + + if "anomaly" not in task_type: + if train_type == "" or label_type == "": + raise ValueError() + result["regression_criteria"] = reg_config["regression_criteria"][task_type][train_type][label_type] + result["kpi_e2e_train_time_criteria"] = reg_config["kpi_e2e_train_time_criteria"][task_type][train_type][ + label_type + ] + result["kpi_e2e_eval_time_criteria"] = reg_config["kpi_e2e_eval_time_criteria"][task_type][train_type][ + label_type + ] + + # update data_path using data_root setting + data_paths = reg_config["data_path"][task_type][train_type][label_type] + for key, value in data_paths.items(): + data_paths[key] = os.path.join(data_root, value) + + result["data_path"] = data_paths + else: + result["regression_criteria"] = reg_config["regression_criteria"][task_type] + result["kpi_e2e_train_time_criteria"] = reg_config["kpi_e2e_train_time_criteria"][task_type] + result["kpi_e2e_eval_time_criteria"] = reg_config["kpi_e2e_eval_time_criteria"][task_type] + + # update data_path using data_root setting + data_paths = reg_config["data_path"][task_type] + for key, value in data_paths.items(): + if key != "train_params": + data_paths[key] = os.path.join(data_root, value) + + result["data_path"] = data_paths + + return result + + def update_result(self, test_type, result, is_anomaly=False, **kwargs): + task_type = self.task_type + if task_type not in self._result_dict: + self._result_dict[task_type] = {} + + if not is_anomaly: + label_type = kwargs.get("label_type", self.label_type) + train_type = kwargs.get("train_type", self.train_type) + + if label_type not in self._result_dict[task_type]: + self._result_dict[task_type][label_type] = {} + if train_type not in self._result_dict[task_type][label_type]: + self._result_dict[task_type][label_type][train_type] = {} + if test_type not in self._result_dict[task_type][label_type][train_type]: + self._result_dict[task_type][label_type][train_type][test_type] = [] + self._result_dict[task_type][label_type][train_type][test_type].append(result) + print(f"update_result({task_type=}, {label_type=}, {train_type=}, {test_type=}, {result=}, {is_anomaly=}") + else: + category = kwargs.get("category", "unknown") + if test_type not in self._result_dict[task_type]: + self._result_dict[task_type][test_type] = {} + if category not in self._result_dict[task_type][test_type]: + self._result_dict[task_type][test_type][category] = [] + self._result_dict[task_type][test_type][category].append(result) + print(f"update_result({task_type=}, {test_type=}, {category=}, {result=}, {is_anomaly=}") + + def get_template_performance(self, template: ModelTemplate, **kwargs): + """Get proper template performance inside of performance list.""" + performance = None + results = None + task_type = kwargs.get("task_type", self.task_type) + train_type = kwargs.get("train_type", self.train_type) + label_type = kwargs.get("label_type", self.label_type) + + if "anomaly" in task_type: + category = kwargs.get("category") + if category is None: + raise RuntimeError("missing required keyword arg 'category'") + results = self._result_dict[task_type]["train"][category] + else: + results = self._result_dict[task_type][label_type][train_type]["train"] + + for result in results: + template_name = list(result.keys())[0] + if template_name == template.name: + performance = result + break + return performance diff --git a/tests/regression/semantic_segmentation/test_segmentation.py b/tests/regression/semantic_segmentation/test_segmentation.py new file mode 100644 index 00000000000..e8870fa54c6 --- /dev/null +++ b/tests/regression/semantic_segmentation/test_segmentation.py @@ -0,0 +1,549 @@ +"""Tests for Segmentation with OTX CLI""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import json +import os +from pathlib import Path +from timeit import default_timer as timer + +import pytest + +from otx.cli.registry import Registry +from tests.regression.regression_test_helpers import ( + REGRESSION_TEST_EPOCHS, + TIME_LOG, + RegressionTestConfig, +) +from tests.test_suite.e2e_test_system import e2e_pytest_component +from tests.test_suite.run_test_command import ( + get_template_dir, + nncf_optimize_testing, + otx_deploy_openvino_testing, + otx_export_testing, + otx_train_testing, + ptq_optimize_testing, +) + +from tests.regression.regression_command import ( + regression_eval_testing, + regression_openvino_testing, + regression_deployment_testing, + regression_nncf_eval_testing, + regression_ptq_eval_testing, + regression_train_time_testing, + regression_eval_time_testing, +) + + +class TestRegressionSegmentation: + REG_CATEGORY = "segmentation" + TASK_TYPE = "segmentation" + TRAIN_TYPE = "supervised" + LABEL_TYPE = "multi_class" + + TRAIN_PARAMS = ["--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_incr(self, reg_cfg, template, tmp_dir_path): + if "SegNext" in template.name: + pytest.skip("Issue#2600: RuntimeError - can't cast ComplexFloat to Float") + train_type = "class_incr" + test_type = "train" + self.performance[template.name] = {} + + sl_template_work_dir = get_template_dir(template, tmp_dir_path / reg_cfg.task_type) + + tmp_dir_path = tmp_dir_path / "seg_incr" + config_cls_incr = reg_cfg.load_config(train_type=train_type) + args_cls_incr = config_cls_incr["data_path"] + args_cls_incr[ + "--load-weights" + ] = f"{sl_template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + args_cls_incr["train_params"] = ["params", "--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + + reg_cfg.update_gpu_args(args_cls_incr) + + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, args_cls_incr) + train_elapsed_time = timer() - train_start_time + + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + args_cls_incr, + config_cls_incr["regression_criteria"]["train"], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): + train_type = "class_incr" + config_cls_incr = reg_cfg.load_config(train_type=train_type) + performance = reg_cfg.get_template_performance(template, train_type=train_type) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=config_cls_incr["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=config_cls_incr["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_semisl(self, reg_cfg, template, tmp_dir_path): + train_type = "semi_supervised" + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / f"{reg_cfg.task_type}/test_semisl" + config_semisl = reg_cfg.load_config(train_type=train_type) + args_semisl = config_semisl["data_path"] + + args_semisl["train_params"] = [ + "params", + "--learning_parameters.num_iters", + REGRESSION_TEST_EPOCHS, + "--algo_backend.train_type", + "Semisupervised", + ] + + reg_cfg.update_gpu_args(args_semisl) + + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, args_semisl) + train_elapsed_time = timer() - train_start_time + + args_semisl.pop("train_params") + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + args_semisl, + config_semisl["regression_criteria"]["train"], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_semisl_kpi_test(self, reg_cfg, template): + train_type = "semi_supervised" + config_semisl = reg_cfg.load_config(train_type=train_type) + performance = reg_cfg.get_template_performance(template, train_type=train_type) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=config_semisl["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=config_semisl["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_selfsl(self, reg_cfg, template, tmp_dir_path): + train_type = "self_supervised" + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / f"{reg_cfg.task_type}/test_selfsl" + config_selfsl = reg_cfg.load_config(train_type=train_type) + args_selfsl = config_selfsl["data_path"] + + selfsl_train_args = copy.deepcopy(args_selfsl) + selfsl_train_args["--train-type"] = "Selfsupervised" + + reg_cfg.update_gpu_args(selfsl_train_args) + + # Self-supervised Training + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, selfsl_train_args) + train_elapsed_time = timer() - train_start_time + + # Supervised Training + template_work_dir = get_template_dir(template, tmp_dir_path) + new_tmp_dir_path = tmp_dir_path / "test_supervised" + args_selfsl["train_params"] = ["params", "--learning_parameters.num_iters", REGRESSION_TEST_EPOCHS] + args_selfsl["--val-data-roots"] = reg_cfg.args["--val-data-roots"] + args_selfsl["--test-data-roots"] = reg_cfg.args["--test-data-roots"] + args_selfsl["--load-weights"] = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + + reg_cfg.update_gpu_args(args_selfsl) + + otx_train_testing(template, new_tmp_dir_path, reg_cfg.otx_dir, args_selfsl) + + # Evaluation with self + supervised training model + args_selfsl.pop("--load-weights") + infer_start_time = timer() + test_result = regression_eval_testing( + template, + new_tmp_dir_path, + reg_cfg.otx_dir, + args_selfsl, + config_selfsl["regression_criteria"]["train"], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_selfsl_kpi_test(self, reg_cfg, template): + train_type = "self_supervised" + config_selfsl = reg_cfg.load_config(train_type=train_type) + performance = reg_cfg.get_template_performance(template, train_type=train_type) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=config_selfsl["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=config_selfsl["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): + test_type = "export" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + export_start_time = timer() + otx_export_testing(template, tmp_dir_path) + export_elapsed_time = timer() - export_start_time + + export_eval_start_time = timer() + test_result = regression_openvino_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.05, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + export_eval_elapsed_time = timer() - export_eval_start_time + + self.performance[template.name][TIME_LOG["export_time"]] = round(export_elapsed_time, 3) + self.performance[template.name][TIME_LOG["export_eval_time"]] = round(export_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): + test_type = "deploy" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + deploy_start_time = timer() + otx_deploy_openvino_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + deploy_elapsed_time = timer() - deploy_start_time + + deploy_eval_start_time = timer() + test_result = regression_deployment_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.0, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + deploy_eval_elapsed_time = timer() - deploy_eval_start_time + + self.performance[template.name][TIME_LOG["deploy_time"]] = round(deploy_elapsed_time, 3) + self.performance[template.name][TIME_LOG["deploy_eval_time"]] = round(deploy_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "nncf" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + if template.entrypoints.nncf is None: + pytest.skip("nncf entrypoint is none") + + nncf_start_time = timer() + nncf_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + nncf_elapsed_time = timer() - nncf_start_time + + nncf_eval_start_time = timer() + test_result = regression_nncf_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + threshold=0.01, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + nncf_eval_elapsed_time = timer() - nncf_eval_start_time + + self.performance[template.name][TIME_LOG["nncf_time"]] = round(nncf_elapsed_time, 3) + self.performance[template.name][TIME_LOG["nncf_eval_time"]] = round(nncf_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): + test_type = "ptq" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / reg_cfg.task_type + ptq_start_time = timer() + ptq_optimize_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + ptq_elapsed_time = timer() - ptq_start_time + + ptq_eval_start_time = timer() + test_result = regression_ptq_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + criteria=reg_cfg.config_dict["regression_criteria"][test_type], + reg_threshold=0.10, + result_dict=self.performance[template.name], + ) + ptq_eval_elapsed_time = timer() - ptq_eval_start_time + + self.performance[template.name][TIME_LOG["ptq_time"]] = round(ptq_elapsed_time, 3) + self.performance[template.name][TIME_LOG["ptq_eval_time"]] = round(ptq_eval_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + +class TestRegressionSupconSegmentation: + REG_CATEGORY = "segmentation" + TASK_TYPE = "segmentation" + TRAIN_TYPE = "supervised" + LABEL_TYPE = "supcon" + + TRAIN_PARAMS = [ + "--learning_parameters.num_iters", + REGRESSION_TEST_EPOCHS, + "--learning_parameters.enable_supcon", + "True", + ] + + templates = Registry(f"src/otx/algorithms/{REG_CATEGORY}").filter(task_type=TASK_TYPE.upper()).templates + templates_ids = [template.model_template_id for template in templates] + + reg_cfg: RegressionTestConfig + + @classmethod + @pytest.fixture(scope="class") + def reg_cfg(cls, tmp_dir_path): + results_root = os.environ.get("REG_RESULTS_ROOT", tmp_dir_path) + cls.reg_cfg = RegressionTestConfig( + cls.TASK_TYPE, + cls.TRAIN_TYPE, + cls.LABEL_TYPE, + os.getcwd(), + train_params=cls.TRAIN_PARAMS, + results_root=results_root, + ) + + yield cls.reg_cfg + + cls.reg_cfg.dump_result_dict() + + def setup_method(self): + self.performance = {} + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train(self, reg_cfg, template, tmp_dir_path): + if not (Path(template.model_template_path).parent / "supcon").is_dir(): + pytest.skip("Supcon training type isn't available for this template") + test_type = "train" + self.performance[template.name] = {} + + tmp_dir_path = tmp_dir_path / "supcon_seg" + + # Supcon + train_start_time = timer() + otx_train_testing(template, tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args) + train_elapsed_time = timer() - train_start_time + + # Evaluation with supcon + supervised training + infer_start_time = timer() + test_result = regression_eval_testing( + template, + tmp_dir_path, + reg_cfg.otx_dir, + reg_cfg.args, + reg_cfg.config_dict["regression_criteria"][test_type], + self.performance[template.name], + ) + infer_elapsed_time = timer() - infer_start_time + + self.performance[template.name][TIME_LOG["train_time"]] = round(train_elapsed_time, 3) + self.performance[template.name][TIME_LOG["infer_time"]] = round(infer_elapsed_time, 3) + reg_cfg.update_result(test_type, self.performance) + + assert test_result["passed"] is True, test_result["log"] + + @e2e_pytest_component + @pytest.mark.parametrize("template", templates, ids=templates_ids) + def test_otx_train_kpi_test(self, reg_cfg, template): + if not (Path(template.model_template_path).parent / "supcon").is_dir(): + pytest.skip("Supcon training type isn't available for this template") + + performance = reg_cfg.get_template_performance(template) + if performance is None: + pytest.skip(reason="Cannot find performance data from results.") + + kpi_train_result = regression_train_time_testing( + train_time_criteria=reg_cfg.config_dict["kpi_e2e_train_time_criteria"]["train"], + e2e_train_time=performance[template.name][TIME_LOG["train_time"]], + template=template, + ) + + kpi_eval_result = regression_eval_time_testing( + eval_time_criteria=reg_cfg.config_dict["kpi_e2e_eval_time_criteria"]["train"], + e2e_eval_time=performance[template.name][TIME_LOG["infer_time"]], + template=template, + ) + + assert kpi_train_result["passed"] is True, kpi_train_result["log"] + assert kpi_eval_result["passed"] is True, kpi_eval_result["log"] diff --git a/tests/regression/summarize_test_results.py b/tests/regression/summarize_test_results.py new file mode 100644 index 00000000000..6dec6f6d088 --- /dev/null +++ b/tests/regression/summarize_test_results.py @@ -0,0 +1,268 @@ +import argparse +import json +import os +from typing import Dict, Union, List, Any + +import pandas as pd + +from tests.regression.regression_test_helpers import ( + ANOMALY_DATASET_CATEGORIES, + LABEL_TYPES, + TRAIN_TYPES, +) + + +ANOMALY_DATA = { + "Task type": [], + "MVTec Category": [], + "Model": [], + "train": [], + "export": [], + "deploy": [], + "nncf": [], + "ptq": [], + "train E2E Time (Sec.)": [], + "export E2E Time (Sec.)": [], + "deploy E2E Time (Sec.)": [], + "nncf E2E Time (Sec.)": [], + "ptq E2E Time (Sec.)": [], + "train Eval Time (Sec.)": [], + "export Eval Time (Sec.)": [], + "deploy Eval Time (Sec.)": [], + "nncf Eval Time (Sec.)": [], + "ptq Eval Time (Sec.)": [], +} + +NON_ANOMALY_DATA = { + "Task type": [], + "Train type": [], + "Label type": [], + "Model": [], + "train": [], + "export": [], + "deploy": [], + "nncf": [], + "ptq": [], + "train E2E Time (Sec.)": [], + "export E2E Time (Sec.)": [], + "deploy E2E Time (Sec.)": [], + "nncf E2E Time (Sec.)": [], + "ptq E2E Time (Sec.)": [], + "train Eval Time (Sec.)": [], + "export Eval Time (Sec.)": [], + "deploy Eval Time (Sec.)": [], + "nncf Eval Time (Sec.)": [], + "ptq Eval Time (Sec.)": [], +} + + +def get_metric_dict(dict_data: Union[List[Dict[str, Any]], None], idx: int, model: str): + """Get the proper dict item by referencing the index and model information. + + Since all models could be optimized by PTQ or NNCF, we need to check that there are proper values in the data. + For example, if model A could be optimized by both PTQ and NNCF and model B couldn't be supported by PTQ and NNCF. + In this case, we have PTQ, NNCF results about A, however, we don't have PTQ, NNCF results about B. + + So, if we don't have results, we need to mark the empty result as "-". + + """ + if dict_data and len(dict_data) > idx: + if dict_data[idx].get(model) is None: + return "-" + return dict_data[idx][model] + else: + return "-" + + +def get_metric_items(input_data: Union[str, List[Dict[str, Any]]]): + """Divide the data by using the model name. + + i.e. + input_data : { + 'A': { + 'Accuracy': 0.5, + 'Model size (MB)': 12.65, + ... + }, + 'B':{ + ... + } + } + + --> return_list: [(A, {'Accuracy': 0.5, 'Model size(MB)': 12.65, ...}), (B, {...})] + + """ + if isinstance(input_data, dict): + return_list = [] + for k, v in input_data.items(): + return_list.append((k, v)) + return return_list + else: + return "-" + + +def filter_task(root: str) -> Dict[str, str]: + """Find prpoer task and task_key.""" + task = root.split("/")[-1] + if "tiling" in task: + task_key = "_".join(task.split("_")[1:]) + else: + task_key = task + return task_key, task + + +def is_anomaly_task(task: str) -> bool: + """Returns True if task is anomaly.""" + return "anomaly" in task + + +def fill_model_performance(items: Union[list, str], test_type: str, result_data: dict): + """Fill the result_data by checking the index of data.""" + if isinstance(items, list): + result_data[test_type].append(f"{items[0][0]}: {items[0][1]}") + if test_type == "train": + result_data[f"{test_type} E2E Time (Sec.)"].append(f"{items[2][1]}") + result_data[f"{test_type} Eval Time (Sec.)"].append(f"{items[3][1]}") + else: + result_data[f"{test_type} E2E Time (Sec.)"].append(f"{items[1][1]}") + result_data[f"{test_type} Eval Time (Sec.)"].append(f"{items[2][1]}") + else: + result_data[test_type].append(items) + result_data[f"{test_type} E2E Time (Sec.)"].append(items) + result_data[f"{test_type} Eval Time (Sec.)"].append(items) + + +def summarize_non_anomaly_data(json_data: dict, result_data: dict) -> dict: + """Make DataFrame by gathering all results.""" + for task_key in json_data.keys(): + for label_type in LABEL_TYPES: + if label_type not in json_data[task_key].keys(): + continue + for train_type in TRAIN_TYPES: + if train_type not in json_data[task_key][label_type].keys(): + continue + task_data = json_data[task_key][label_type][train_type] + + train_data = task_data.get("train") + if train_data is None: + raise ValueError("Train data can't be empty.") + export_data = task_data.get("export", None) + deploy_data = task_data.get("deploy", None) + nncf_data = task_data.get("nncf", None) + ptq_data = task_data.get("ptq", None) + + for i, per_model_data in enumerate(train_data): + for model in per_model_data: + train_items = get_metric_items(get_metric_dict(train_data, i, model)) + export_items = get_metric_items(get_metric_dict(export_data, i, model)) + deploy_items = get_metric_items(get_metric_dict(deploy_data, i, model)) + nncf_items = get_metric_items(get_metric_dict(nncf_data, i, model)) + ptq_items = get_metric_items(get_metric_dict(ptq_data, i, model)) + + result_data["Task type"].append(task_key) + result_data["Train type"].append(train_type) + result_data["Label type"].append(label_type) + result_data["Model"].append(model) + + fill_model_performance(train_items, "train", result_data) + fill_model_performance(export_items, "export", result_data) + fill_model_performance(deploy_items, "deploy", result_data) + fill_model_performance(nncf_items, "nncf", result_data) + fill_model_performance(ptq_items, "ptq", result_data) + + +def summarize_anomaly_data(json_data: dict, result_data: dict) -> dict: + """Make DataFrame by gathering all results.""" + for task_key in json_data.keys(): + task_data = json_data[task_key] + + train_data = task_data.get("train") + if train_data is None: + raise ValueError("Train data can't be empty.") + export_data = task_data.get("export") + deploy_data = task_data.get("deploy") + nncf_data = task_data.get("nncf") + ptq_data = task_data.get("ptq") + + for anomaly_category in ANOMALY_DATASET_CATEGORIES: + train_cat_data = train_data.get(anomaly_category) + if train_cat_data is None: + continue + export_cat_data = export_data.get(anomaly_category) + deploy_cat_data = deploy_data.get(anomaly_category) + nncf_cat_data = nncf_data.get(anomaly_category) + ptq_cat_data = ptq_data.get(anomaly_category) + + for i, per_model_data in enumerate(train_cat_data): + for model in per_model_data: + train_items = get_metric_items(get_metric_dict(train_cat_data, i, model)) + export_items = get_metric_items(get_metric_dict(export_cat_data, i, model)) + deploy_items = get_metric_items(get_metric_dict(deploy_cat_data, i, model)) + nncf_items = get_metric_items(get_metric_dict(nncf_cat_data, i, model)) + ptq_items = get_metric_items(get_metric_dict(ptq_cat_data, i, model)) + + result_data["Task type"].append(task_key) + result_data["MVTec Category"].append(anomaly_category) + result_data["Model"].append(model) + + fill_model_performance(train_items, "train", result_data) + fill_model_performance(export_items, "export", result_data) + fill_model_performance(deploy_items, "deploy", result_data) + fill_model_performance(nncf_items, "nncf", result_data) + fill_model_performance(ptq_items, "ptq", result_data) + + +def save_file(result_data: dict, output_path: str, file_name: str): + df = pd.DataFrame(result_data) + if not os.path.exists(output_path): + os.makedirs(output_path, exist_ok=True) + df.to_csv(os.path.join(output_path, file_name)) + + +def merge_reg_results_dict(target, source, overwrite=False): + target = target.copy() + for k, v in source.items(): + if isinstance(v, Dict): + if k in target: + target[k] = merge_reg_results_dict(target[k], v) + else: + target[k] = v + elif isinstance(v, List): + if len(target[k]) == 0 or overwrite: + target[k] = v + return target + + +def merge_results_list(results_list: List[Dict]): + if len(results_list) == 1: + return results_list[0] + results_dict = {} + for results in results_list: + results_dict = merge_reg_results_dict(results_dict, results) + return results_dict + + +def summarize_results_data(input_path: str, output_path: str): + """summarize regression test result data.""" + input_path = input_path + + for entity in os.listdir(input_path): + entity_path = os.path.join(input_path, entity) + if os.path.isdir(entity_path): + _, task = filter_task(entity_path) + results_list = [] + for result_json in os.listdir(entity_path): + result_json_path = os.path.join(entity_path, result_json) + if os.path.isfile(result_json_path) and result_json_path.split(".")[-1] == "json": + with open(result_json_path, "r") as f: + results_list.append(json.load(f)) + json_data = merge_results_list(results_list) + + assert len(json_data) != 0, "no json results to summary" + + if is_anomaly_task(task) is True: + summarize_anomaly_data(json_data, ANOMALY_DATA) + save_file(ANOMALY_DATA, output_path, f"tests-reg_{task}.csv") + else: + summarize_non_anomaly_data(json_data, NON_ANOMALY_DATA) + save_file(NON_ANOMALY_DATA, output_path, f"tests-reg_{task}.csv") diff --git a/tests/regression/test_regression.py b/tests/regression/test_regression.py deleted file mode 100644 index c8b2e9aef42..00000000000 --- a/tests/regression/test_regression.py +++ /dev/null @@ -1,847 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - - -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path - -import pytest -from otx.cli.cli import OTXCLI -from unittest.mock import patch - -import mlflow - - -@dataclass -class ModelTestCase: - task: str - name: str - - -@dataclass -class DatasetTestCase: - name: str - data_root: Path - data_format: str - num_classes: int - extra_overrides: dict - - -@dataclass -class RegressionTestCase: - model: ModelTestCase - dataset: DatasetTestCase - output_dir: Path - - -class BaseTest: - def _test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - for seed in range(fxt_num_repeat): - test_case = RegressionTestCase( - model=model_test_case, - dataset=dataset_test_case, - output_dir=Path(tmpdir) / str(seed), - ) - - run_name = f"{test_case.model.task}/{test_case.model.name}/{test_case.dataset.name}/{seed}" - tags = { - "task": test_case.model.task, - "model": test_case.model.name, - "dataset": test_case.dataset.name, - "seed": str(seed), - **fxt_tags, - } - data_root = ( - fxt_dataset_root_dir - # / test_case.model.task - / test_case.dataset.data_root - ) - with mlflow.start_run(tags=tags, run_name=run_name): - command_cfg = [ - "otx", "train", - "--config", f"src/otx/recipe/{test_case.model.task}/{test_case.model.name}.yaml", - "--model.num_classes", str(test_case.dataset.num_classes), - "--data_root", str(data_root), - "--data.config.data_format", test_case.dataset.data_format, - "--work_dir", str(test_case.output_dir), - "--engine.device", fxt_accelerator, - ] - deterministic = test_case.dataset.extra_overrides.pop("deterministic", "False") - for key, value in test_case.dataset.extra_overrides.items(): - command_cfg.append(f"--{key}") - command_cfg.append(str(value)) - train_cfg = command_cfg.copy() - train_cfg.extend(["--seed", str(seed)]) - train_cfg.extend(["--deterministic", deterministic]) - with patch("sys.argv", train_cfg): - cli = OTXCLI() - train_metrics = cli.engine.trainer.callback_metrics - checkpoint = cli.engine.checkpoint - command_cfg[1] = "test" - command_cfg += ["--checkpoint", checkpoint] - with patch("sys.argv", command_cfg): - cli = OTXCLI() - test_metrics = cli.engine.trainer.callback_metrics - metrics = {**train_metrics, **test_metrics} - - # Submit metrics to MLFlow Tracker server - mlflow.log_metrics(metrics) - - -class TestMultiClassCls(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="classification/multi_class_cls", name="otx_deit_tiny"), - ModelTestCase(task="classification/multi_class_cls", name="otx_dino_v2"), - ModelTestCase(task="classification/multi_class_cls", name="otx_efficientnet_b0"), - ModelTestCase(task="classification/multi_class_cls", name="otx_efficientnet_v2"), - ModelTestCase(task="classification/multi_class_cls", name="otx_mobilenet_v3_large"), - ] - # Test case parametrization for dataset - DATASET_TEST_CASES = [ # noqa: RUF012 - DatasetTestCase( - name=f"multiclass_CUB_small_{idx}", - data_root=Path("multiclass_classification/multiclass_CUB_small") / f"{idx}", - data_format="imagenet_with_subset_dirs", - num_classes=2, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.accuracy.MulticlassAccuracywithLabelGroup", - } - ) - for idx in range(1, 4) - ] + [ - DatasetTestCase( - name=f"multiclass_CUB_medium", - data_root=Path("multiclass_classification/multiclass_CUB_medium"), - data_format="imagenet_with_subset_dirs", - num_classes=67, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.accuracy.MulticlassAccuracywithLabelGroup", - } - ), - DatasetTestCase( - name=f"multiclass_food101_large", - data_root=Path("multiclass_classification/multiclass_food101_large"), - data_format="imagenet_with_subset_dirs", - num_classes=20, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.accuracy.MulticlassAccuracywithLabelGroup", - } - ) - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) - - -class TestMultilabelCls(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="classification/multi_label_cls", name="efficientnet_b0_light"), - ModelTestCase(task="classification/multi_label_cls", name="efficientnet_v2_light"), - ModelTestCase(task="classification/multi_label_cls", name="mobilenet_v3_large_light"), - ModelTestCase(task="classification/multi_label_cls", name="otx_deit_tiny"), - ] - # Test case parametrization for dataset - DATASET_TEST_CASES = [ # noqa: RUF012 - DatasetTestCase( - name=f"multilabel_CUB_small_{idx}", - data_root=Path("multilabel_classification/multilabel_CUB_small") / f"{idx}", - data_format="datumaro", - num_classes=3, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.accuracy.MultilabelAccuracywithLabelGroup", - } - ) - for idx in range(1, 4) - ] + [ - DatasetTestCase( - name=f"multilabel_CUB_medium", - data_root=Path("multilabel_classification/multilabel_CUB_medium"), - data_format="datumaro", - num_classes=68, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.accuracy.MultilabelAccuracywithLabelGroup", - } - ), - DatasetTestCase( - name=f"multilabel_food101_large", - data_root=Path("multilabel_classification/multilabel_food101_large"), - data_format="datumaro", - num_classes=21, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.accuracy.MultilabelAccuracywithLabelGroup", - } - ) - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) - - -class TestHlabelCls(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="classification/h_label_cls", name="efficientnet_b0_light"), - ModelTestCase(task="classification/h_label_cls", name="efficientnet_v2_light"), - ModelTestCase(task="classification/h_label_cls", name="mobilenet_v3_large_light"), - ModelTestCase(task="classification/h_label_cls", name="otx_deit_tiny"), - ] - # Test case parametrization for dataset - DATASET_TEST_CASES = [ # noqa: RUF012 - DatasetTestCase( - name=f"hlabel_CUB_small_{idx}", - data_root=Path("hlabel_classification/hlabel_CUB_small") / f"{idx}", - data_format="datumaro", - num_classes=6, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.accuracy.HlabelAccuracy", - }, - ) - for idx in range(1, 4) - ] + [ - DatasetTestCase( - name=f"hlabel_CUB_medium", - data_root=Path("hlabel_classification/hlabel_CUB_medium"), - data_format="datumaro", - num_classes=102, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.accuracy.HlabelAccuracy", - }, - ) - - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) - - -class TestObjectDetection(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="detection", name="atss_mobilenetv2"), - ModelTestCase(task="detection", name="atss_resnext101"), - ModelTestCase(task="detection", name="ssd_mobilenetv2"), - ModelTestCase(task="detection", name="yolox_tiny"), - ModelTestCase(task="detection", name="yolox_s"), - ModelTestCase(task="detection", name="yolox_l"), - ModelTestCase(task="detection", name="yolox_x"), - ] - # Test case parametrization for dataset - DATASET_TEST_CASES = [ # noqa: RUF012 - DatasetTestCase( - name=f"pothole_small_{idx}", - data_root=Path("detection/pothole_small") / f"{idx}", - data_format="coco", - num_classes=1, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ) - for idx in range(1, 4) - ] + [ - DatasetTestCase( - name="pothole_medium", - data_root=Path("detection/pothole_medium"), - data_format="coco", - num_classes=1, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ), - DatasetTestCase( - name="vitens_large", - data_root=Path("detection/vitens_large"), - data_format="coco", - num_classes=1, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ) - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) - -class TestSemanticSegmentation(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="semantic_segmentation", name="litehrnet_18"), - ModelTestCase(task="semantic_segmentation", name="litehrnet_s"), - ModelTestCase(task="semantic_segmentation", name="litehrnet_x"), - ModelTestCase(task="semantic_segmentation", name="segnext_b"), - ModelTestCase(task="semantic_segmentation", name="segnext_s"), - ModelTestCase(task="semantic_segmentation", name="segnext_t"), - ModelTestCase(task="semantic_segmentation", name="dino_v2"), - ] - # Test case parametrization for dataset - DATASET_TEST_CASES = [ # noqa: RUF012 - DatasetTestCase( - name=f"kvasir_small_{idx}", - data_root=Path("semantic_seg/kvasir_small") / f"{idx}", - data_format="common_semantic_segmentation_with_subset_dirs", - num_classes=2, - extra_overrides={}, - ) - for idx in range(1, 4) - ] + [ - DatasetTestCase( - name="kvasir_medium", - data_root=Path("semantic_seg/kvasir_medium"), - data_format="common_semantic_segmentation_with_subset_dirs", - num_classes=2, - extra_overrides={}, - ), - DatasetTestCase( - name="kvasir_large", - data_root=Path("semantic_seg/kvasir_large"), - data_format="common_semantic_segmentation_with_subset_dirs", - num_classes=2, - extra_overrides={}, - ) - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) - -class TestInstanceSegmentation(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="instance_segmentation", name="maskrcnn_efficientnetb2b"), - ModelTestCase(task="instance_segmentation", name="maskrcnn_r50"), - ModelTestCase(task="instance_segmentation", name="maskrcnn_swint"), - ] - # Test case parametrization for dataset - DATASET_TEST_CASES = [ # noqa: RUF012 - DatasetTestCase( - name=f"wgisd_small_{idx}", - data_root=Path("instance_seg/wgisd_small") / f"{idx}", - data_format="coco", - num_classes=5, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ) - for idx in range(1, 4) - ] + [ - DatasetTestCase( - name="coco_car_person_medium", - data_root=Path("instance_seg/coco_car_person_medium"), - data_format="coco", - num_classes=2, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ), - DatasetTestCase( - name="vitens_coliform", - data_root=Path("instance_seg/Vitens-Coliform-coco"), - data_format="coco", - num_classes=1, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ) - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) - - -class TestVisualPrompting(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="visual_prompting", name="sam_tiny_vit"), - ] - # Test case parametrization for dataset - DATASET_TEST_CASES = [ # noqa: RUF012 - DatasetTestCase( - name=f"wgisd_small_{idx}", - data_root=Path("visual_prompting/wgisd_small") / f"{idx}", - data_format="coco", - num_classes=5, - extra_overrides={}, - ) - for idx in range(1, 4) - ] + [ - DatasetTestCase( - name="coco_car_person_medium", - data_root=Path("visual_prompting/coco_car_person_medium"), - data_format="coco", - num_classes=2, - extra_overrides={}, - ), - DatasetTestCase( - name="vitens_coliform", - data_root=Path("visual_prompting/Vitens-Coliform-coco"), - data_format="coco", - num_classes=1, - extra_overrides={}, - ) - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) - - -class TestZeroShotVisualPrompting(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="zero_shot_visual_prompting", name="sam_tiny_vit"), - ModelTestCase(task="zero_shot_visual_prompting", name="sam_vit_b"), - ] - # Test case parametrization for dataset - DATASET_TEST_CASES = [ - DatasetTestCase( - name="coco_car_person_medium_datumaro", - data_root=Path("zero_shot_visual_prompting/coco_car_person_medium_datumaro"), - data_format="datumaro", - num_classes=2, - extra_overrides={"max_epochs": "1"} - ), - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) - - -class TestTileObjectDetection(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="detection", name="atss_mobilenetv2_tile"), - ModelTestCase(task="detection", name="ssd_mobilenetv2_tile"), - ModelTestCase(task="detection", name="yolox_tiny_tile"), - ModelTestCase(task="detection", name="yolox_s_tile"), - ModelTestCase(task="detection", name="yolox_l_tile"), - ModelTestCase(task="detection", name="yolox_x_tile"), - ] - # Test case parametrization for dataset - DATASET_TEST_CASES = [ - DatasetTestCase( - name="vitens_coliform", - data_root=Path("instance_seg/Vitens-Coliform-coco"), - data_format="coco", - num_classes=1, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ), - DatasetTestCase( - name="vitens_aeromonas", - data_root=Path("instance_seg/Vitens-Aeromonas-coco"), - data_format="coco", - num_classes=1, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ) - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) - - -class TestTileInstanceSegmentation(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="instance_segmentation", name="maskrcnn_efficientnetb2b_tile"), - ModelTestCase(task="instance_segmentation", name="maskrcnn_r50_tile"), - ModelTestCase(task="instance_segmentation", name="maskrcnn_swint_tile"), - ] - # Test case parametrization for dataset - DATASET_TEST_CASES = [ - DatasetTestCase( - name="vitens_coliform", - data_root=Path("instance_seg/Vitens-Coliform-coco"), - data_format="coco", - num_classes=1, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ), - DatasetTestCase( - name="vitens_aeromonas", - data_root=Path("instance_seg/Vitens-Aeromonas-coco"), - data_format="coco", - num_classes=1, - extra_overrides={ - "deterministic": "True", - "metric": "otx.core.metrics.fmeasure.FMeasure", - "callback_monitor": "val/f1-score", - "scheduler.monitor": "val/f1-score", - }, - ) - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) - - -class TestActionClassification(BaseTest): - # Test case parametrization for model - MODEL_TEST_CASES = [ # noqa: RUF012 - ModelTestCase(task="action/action_classification", name="x3d"), - ModelTestCase(task="action/action_classification", name="movinet"), - ] - DATASET_TEST_CASES = [ - DatasetTestCase( - name="ucf-5percent", - data_root=Path("action_classification/ucf-kinetics-5percent"), - data_format="kinetics", - num_classes=101, - extra_overrides={"max_epochs": "10", "deterministic": "True"} - ), - DatasetTestCase( - name="ucf-30percent", - data_root=Path("action_classification/ucf-kinetics-30percent"), - data_format="kinetics", - num_classes=101, - extra_overrides={"max_epochs": "10", "deterministic": "True"} - ), - ] - - @pytest.mark.parametrize( - "model_test_case", - MODEL_TEST_CASES, - ids=[tc.name for tc in MODEL_TEST_CASES], - ) - @pytest.mark.parametrize( - "dataset_test_case", - DATASET_TEST_CASES, - ids=[tc.name for tc in DATASET_TEST_CASES], - ) - def test_regression( - self, - model_test_case: ModelTestCase, - dataset_test_case: DatasetTestCase, - fxt_dataset_root_dir: Path, - fxt_tags: dict, - fxt_num_repeat: int, - fxt_accelerator: str, - tmpdir: pytest.TempdirFactory, - ) -> None: - self._test_regression( - model_test_case=model_test_case, - dataset_test_case=dataset_test_case, - fxt_dataset_root_dir=fxt_dataset_root_dir, - fxt_tags=fxt_tags, - fxt_num_repeat=fxt_num_repeat, - fxt_accelerator=fxt_accelerator, - tmpdir=tmpdir, - ) diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 00000000000..f01ca2c981b --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,436 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import contextlib +import logging +import os +import random +import tempfile +from typing import Iterator, List, Optional, Sequence, Tuple + +import cv2 +import numpy as np +from bson import ObjectId + +from otx.api.configuration import ConfigurableParameters +from otx.api.configuration.elements import ( + ParameterGroup, + add_parameter_group, + configurable_boolean, + configurable_float, + configurable_integer, + string_attribute, +) +from otx.api.configuration.model_lifecycle import ModelLifecycle +from otx.api.entities.annotation import Annotation +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.image import Image + +logger = logging.getLogger(__name__) + + +def generate_unique_id() -> ID: + """ + Generates unique ID for testing + :return: + """ + return ID(ObjectId()) + + +class LabelSchemaExample: + def __init__(self) -> None: + self.label_domain = Domain.CLASSIFICATION + + self.flowering = self.new_label_by_name("flowering") + self.no_plant = self.new_label_by_name("no_plant") + self.vegetative = self.new_label_by_name("vegetative") + + def new_label_by_name(self, name: str, is_empty: bool = False) -> LabelEntity: + label = LabelEntity(name=name, color=Color.random(), domain=self.label_domain, is_empty=is_empty) + label.id_ = generate_unique_id() + return label + + def add_hierarchy(self, label_schema: LabelSchemaEntity) -> Tuple[LabelEntity, LabelEntity, LabelEntity]: + """Adds children to flowering, no_plant and vegetative""" + label_schema.add_group( + LabelGroup( + "plant_state", + [self.flowering, self.no_plant, self.vegetative], + LabelGroupType.EXCLUSIVE, + ) + ) + flower_partial_visible = self.new_label_by_name("flower_partial_visible") + flower_fully_visible = self.new_label_by_name("flower_fully_visible") + label_schema.add_group( + LabelGroup( + "flowering_state", + [flower_fully_visible, flower_partial_visible], + LabelGroupType.EXCLUSIVE, + ) + ) + label_schema.add_child(self.flowering, flower_partial_visible) + label_schema.add_child(self.flowering, flower_fully_visible) + + assert self.flowering == label_schema.get_parent(flower_partial_visible) + assert label_schema.get_parent(self.no_plant) is None + + few_leaves = self.new_label_by_name("few_leaves") + label_schema.add_group(LabelGroup("leaf_state", [few_leaves], LabelGroupType.EXCLUSIVE)) + label_schema.add_child(self.vegetative, few_leaves) + return few_leaves, flower_fully_visible, flower_partial_visible + + +def generate_random_annotated_image( + image_width: int, + image_height: int, + labels: Sequence[LabelEntity], + min_size=50, + max_size=250, + shape: Optional[str] = None, + max_shapes: int = 10, + intensity_range: List[Tuple[int, int]] = None, + random_seed: Optional[int] = None, + use_mask_as_annotation: bool = False, +) -> Tuple[np.ndarray, List[Annotation]]: + """ + Generate a random image with the corresponding annotation entities. + + :param intensity_range: Intensity range for RGB channels ((r_min, r_max), (g_min, g_max), (b_min, b_max)) + :param max_shapes: Maximum amount of shapes in the image + :param shape: {"rectangle", "ellipse", "triangle"} + :param image_height: Height of the image + :param image_width: Width of the image + :param labels: Task Labels that should be applied to the respective shape + :param min_size: Minimum size of the shape(s) + :param max_size: Maximum size of the shape(s) + :param random_seed: Seed to initialize the random number generator + :param use_mask_as_annotation: If True, masks will be added in annotation + :return: uint8 array, list of shapes + """ + from skimage.draw import random_shapes, rectangle + + if intensity_range is None: + intensity_range = [(100, 200)] + + image1: Optional[np.ndarray] = None + sc_labels = [] + # Sporadically, it might happen there is no shape in the image, especially on low-res images. + # It'll retry max 5 times until we see a shape, and otherwise raise a runtime error + if shape == "ellipse": # ellipse shape is not available in random_shapes function. use circle instead + shape = "circle" + for _ in range(5): + rand_image, sc_labels = random_shapes( + (image_height, image_width), + min_shapes=1, + max_shapes=max_shapes, + intensity_range=intensity_range, + min_size=min_size, + max_size=max_size, + shape=shape, + random_seed=random_seed, + ) + num_shapes = len(sc_labels) + if num_shapes > 0: + image1 = rand_image + break + + if image1 is None: + raise RuntimeError("Was not able to generate a random image that contains any shapes") + + annotations: List[Annotation] = [] + for sc_label in sc_labels: + sc_label_name = sc_label[0] + sc_label_shape_r = sc_label[1][0] + sc_label_shape_c = sc_label[1][1] + y_min, y_max = max(0.0, float(sc_label_shape_r[0] / image_height)), min( + 1.0, float(sc_label_shape_r[1] / image_height) + ) + x_min, x_max = max(0.0, float(sc_label_shape_c[0] / image_width)), min( + 1.0, float(sc_label_shape_c[1] / image_width) + ) + + if sc_label_name == "ellipse": + # Fix issue with newer scikit-image libraries that generate ellipses. + # For now we render a rectangle on top of it + sc_label_name = "rectangle" + rr, cc = rectangle( + start=(sc_label_shape_r[0], sc_label_shape_c[0]), + end=(sc_label_shape_r[1] - 1, sc_label_shape_c[1] - 1), + shape=image1.shape, + ) + image1[rr, cc] = ( + # disable B311 random - used for the random sampling not for security/crypto + random.randint(0, 200), # nosec B311 + random.randint(0, 200), # nosec B311 + random.randint(0, 200), # nosec B311 + ) + if sc_label_name == "circle": + sc_label_name = "ellipse" + + label_matches = [label for label in labels if sc_label_name == label.name] + if len(label_matches) > 0: + label = label_matches[0] + box_annotation = Annotation( + Rectangle(x1=x_min, y1=y_min, x2=x_max, y2=y_max), + labels=[ScoredLabel(label, probability=1.0)], + ) + + annotation: Annotation + + if label.name == "ellipse": + annotation = Annotation( + Ellipse( + x1=box_annotation.shape.x1, + y1=box_annotation.shape.y1, + x2=box_annotation.shape.x2, + y2=box_annotation.shape.y2, + ), + labels=box_annotation.get_labels(include_empty=True), + ) + elif label.name == "triangle": + points = [ + Point( + x=(box_annotation.shape.x1 + box_annotation.shape.x2) / 2, + y=box_annotation.shape.y1, + ), + Point(x=box_annotation.shape.x1, y=box_annotation.shape.y2), + Point(x=box_annotation.shape.x2, y=box_annotation.shape.y2), + ] + + annotation = Annotation( + Polygon(points=points), + labels=box_annotation.get_labels(include_empty=True), + ) + else: + annotation = box_annotation + + annotations.append(annotation) + + if use_mask_as_annotation: + mask = np.zeros_like(image1, dtype=np.uint8) + y_min, y_max = int(y_min * image_height), int(y_max * image_height) + x_min, x_max = int(x_min * image_width), int(x_max * image_width) + + coords_object = np.where(image1[y_min:y_max, x_min:x_max] < 255) + mask[y_min:y_max, x_min:x_max][coords_object] = 1 + mask = mask.sum(axis=-1) + mask[mask > 0] = 1 + mask_annotation = Annotation( + Image(data=mask, size=mask.shape), + labels=box_annotation.get_labels(include_empty=True), + ) + annotations.append(mask_annotation) + else: + logger.warning( + "Generated a random image, but was not able to associate a label with a shape. " + f"The name of the shape was `{sc_label_name}`. " + ) + + return image1, annotations + + +@contextlib.contextmanager +def generate_random_image_folder(width: int = 480, height: int = 360, number_of_images: int = 10) -> Iterator[str]: + """ + Generates a folder with random images, cleans up automatically if used in a `with` statement + + :param width: height of the images. Defaults to 480. + :param height: width of the images. Defaults to 360. + :param number_of_images: number of generated images. Defaults to 10. + + :return: The temporary directory + """ + temp_dir = tempfile.TemporaryDirectory() + + for n in range(number_of_images): + temp_file = os.path.join(temp_dir.name, f"{n}.jpg") + _write_random_image(width, height, temp_file) + + try: + yield temp_dir.name + finally: + temp_dir.cleanup() + + +@contextlib.contextmanager +def generate_random_video_folder( + width: int = 480, + height: int = 360, + number_of_videos: int = 10, + number_of_frames: int = 150, +) -> Iterator[str]: + """ + Generates a folder with random videos, cleans up automatically if used in a `with` statement + :param width: Width of the video. Defaults to 480. + :param height: Height of the video. Defaults to 360. + :param number_of_videos: Number of videos to generate. Defaults to 10. + :param number_of_frames: Number of frames in each video. Defaults to 150. + + :return: A temporary directory with videos + """ + temp_dir = tempfile.TemporaryDirectory() + + for n in range(number_of_videos): + temp_file = os.path.join(temp_dir.name, f"{n}.mp4") + _write_random_video(width, height, number_of_frames, temp_file) + + try: + yield temp_dir.name + finally: + temp_dir.cleanup() + + +@contextlib.contextmanager +def generate_random_single_image(width: int = 480, height: int = 360) -> Iterator[str]: + """ + Generates a random image, cleans up automatically if used in a `with` statement + :param width: Width of the image. Defaults to 480. + :param height: Height of the image. Defaults to 360. + + :return: Path to an image file + """ + + temp_dir = tempfile.TemporaryDirectory() + temp_file = os.path.join(temp_dir.name, "temp_image.jpg") + _write_random_image(width, height, temp_file) + + try: + yield temp_file + finally: + temp_dir.cleanup() + + +@contextlib.contextmanager +def generate_random_single_video(width: int = 480, height: int = 360, number_of_frames: int = 150) -> Iterator[str]: + """ + Generates a random video, cleans up automatically if used in a `with` statement + :param width: Width of the video. Defaults to 480. + :param height: Height of the video. Defaults to 360. + :param number_of_frames: Number of frames in the video. Defaults to 150. + + :return: Path to a video file + """ + temp_dir = tempfile.TemporaryDirectory() + temp_file = os.path.join(temp_dir.name, "temp_video.mp4") + _write_random_video(width, height, number_of_frames, temp_file) + + try: + yield temp_file + finally: + temp_dir.cleanup() + + +def _write_random_image(width: int, height: int, filename: str): + img = np.uint8(np.random.random((height, width, 3)) * 255) + cv2.imwrite(filename, img) + + +def _write_random_video(width: int, height: int, number_of_frames: int, filename: str): + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + f = filename + videowriter = cv2.VideoWriter(f, fourcc, 30, (width, height)) + + for _ in range(number_of_frames): + img = np.uint8(np.random.random((height, width, 3)) * 255) + videowriter.write(img) + + videowriter.release() + + +class ConfigExample(ConfigurableParameters): + header = string_attribute("Test configuration for an object detection task") + description = header + + class __LearningParameters(ParameterGroup): + header = string_attribute("Test Learning Parameters") + description = header + + batch_size = configurable_integer( + default_value=5, + min_value=1, + max_value=512, + header="Test batch size", + description="The number of training samples seen in each iteration of training. Increasing this value " + "improves training time and may make the training more stable. A larger batch size has higher " + "memory requirements.", + warning="Increasing this value may cause the system to use more memory than available, " + "potentially causing out of memory errors, please update with caution.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + num_iters = configurable_integer( + default_value=1, + min_value=1, + max_value=100000, + header="Number of training iterations", + description="Increasing this value causes the results to be more robust but training time will be longer.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + learning_rate = configurable_float( + default_value=0.01, + min_value=1e-07, + max_value=1e-01, + header="Learning rate", + description="Increasing this value will speed up training convergence but might make it unstable.", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + learning_rate_warmup_iters = configurable_integer( + default_value=100, + min_value=1, + max_value=10000, + header="Number of iterations for learning rate warmup", + description="Test learning rate warmup", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + num_workers = configurable_integer( + default_value=4, + min_value=2, + max_value=10, + header="num_workers test header", + description="num_workers test description", + affects_outcome_of=ModelLifecycle.NONE, + ) + + class __Postprocessing(ParameterGroup): + header = string_attribute("Test Postprocessing") + description = header + + result_based_confidence_threshold = configurable_boolean( + default_value=True, + header="Test Result based confidence threshold", + description="Test confidence threshold is derived from the results", + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + confidence_threshold = configurable_float( + default_value=0.25, + min_value=0, + max_value=1, + header="Test Confidence threshold", + description="This threshold only takes effect if the threshold is not set based on the result.--Only test", + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + learning_parameters = add_parameter_group(__LearningParameters) + postprocessing = add_parameter_group(__Postprocessing) diff --git a/tests/test_suite/ARCHITECTURE.md b/tests/test_suite/ARCHITECTURE.md new file mode 100644 index 00000000000..4c0c654443f --- /dev/null +++ b/tests/test_suite/ARCHITECTURE.md @@ -0,0 +1,1451 @@ +# OpenVINO™ Training Extensions API test suite architecture + +## I. General description + +The folder `otx_sdk/otx_sdk/test_suite/` contains `otx_sdk.test_suite` library that +simplifies creation of training tests for OpenVINO™ Training Extensions algo backend. + +The training tests are tests that may run in some unified manner such stages as + +- training of a model, +- evaluation of the trained model, +- export or optimization of the trained model, +- and evaluation of exported/optimized model. + +Typically each OpenVINO™ Training Extensions algo backend contains test file `test_otx_training.py` that allows to run the +training tests. + +Note that there are a lot of dependencies between different stages of training tests: most of them +require trained model, so they depends on training stage; also for example POT optimization stage +and evaluation of exported model stage require the exported model, so export stage should be run +before, etc. + +The `test_suite` library allows to create training tests such that + +1. the tests do not repeat the common steps that can be re-used +2. if we point for pytest that only some test stage is required, all dependency stages are run + automatically +3. if a stage is failed all the stage that depend on this stage are also failed. + +Note that the second item above is absent in such pytest library as `pytest-dependency` that just +skip a test if any of the dependencies did fail or has been skipped. + +To avoid repeating of the common steps between stages the results of stages should be kept in a +special cache to be re-used by the next stages. + +We suppose that each test executes one test stage (also called test action). + +## II. General architecture overview + +Here and below we will write paths to test suite library files relatively with the folder +`otx_sdk/otx_sdk` of OpenVINO™ Training Extensions git repository, so path to this file is referred as +`test_suite/ARCHITECTURE.md`. + +When we run some test that uses `test_suite` library (typically `test_otx_training.py` in some of +the algo backends) the callstack of the test looks as follows: + +- Pytest framework + +- Instance of a test class. + Typically this class is defined in `test_otx_training.py` in the algo backend. + This class contains some fixtures implementation and uses test helper (see the next item). + The name of the class is started from `Test`, so pytest uses it as a usual test class. + The instance is responsible on the connection between test suite and pytest parameters and + fixtures. + +- Instance of training test helper class `OTXTestHelper` from `test_suite/training_tests_helper.py`. + The instance of the class should be a static field of the test class stated above. + The instance controls all execution of tests. + Also the instance keeps in its cache an instance of a test case class between runs of different + tests (see the next item). + +- Instance of a test case class. + This instance connects all the test stages between each other and keeps in its fields results of + all test stages between tests. + (Since the instance of this class is kept in the cache of training test helper's instance between + runs of tests, results of one test may be re-used by other tests.) + Note that each test executes only one test stage. + And note that the class of the test case is generated "on the fly" by the function + `generate_otx_integration_test_case_class` from the file `test_suite/training_test_case.py`; + the function + + - receives as the input the list of action classes that should be used in tests for the + algo backend + - and returns the class type that will be used by the instance of the test helper. + +- Instance of the test stage class `OTXTestStage` from `test_suite/training_tests_stage.py`. + The class wraps a test action class (see the next item) to run it only once. + Also it makes validation of the results of the wrapped test action if this is required. + +- Instance of a test action class. + The class makes the real actions that should be done for a test using calls of OpenVINO™ Training Extensions interfaces. + +The next sections will describe the corresponding classes from the bottom to the top. + +## III. Test actions + +### III.1 General description of test actions classes + +The test action classes in test suite make the real work. + +Each test action makes operations for one test stage. At the moment the file +`test_suite/training_tests_actions.py` contains the reference code of the following test actions +for mmdetection algo backend: + +- class `OTXTestTrainingAction` -- training of a model +- class `OTXTestTrainingEvaluationAction` -- evaluation after the training +- class `OTXTestExportAction` -- export after the training +- class `OTXTestExportEvaluationAction` -- evaluation of exported model +- class `OTXTestPotAction` -- POT compression of exported model +- class `OTXTestPotEvaluationAction` -- evaluation of POT-compressed model +- class `OTXTestNNCFAction` -- NNCF-compression of the trained model +- class `OTXTestNNCFGraphAction` -- check of NNCF compression graph (work on not trained model) +- class `OTXTestNNCFEvaluationAction` -- evaluation of NNCF-compressed model +- class `OTXTestNNCFExportAction` -- export of NNCF-compressed model +- class `OTXTestNNCFExportEvaluationAction` -- evaluation after export of NNCF-compressed model + +Note that these test actions are implementation for mmdetection algo backend due to historical +reasons. +But since the actions make operations using OpenVINO™ Training Extensions interface, most of test actions code may be +re-used for all algo backends. + +One of obvious exceptions is the training action -- it uses real datasets for a concrete algo +backend, and since different algo backends have their own classes for datasets (and may could have a +bit different ways of loading of the datasets) the training action should be re-implemented for each +algo backends. + +Note that each test action class MUST have the following properties: + +- it MUST be derived from the base class `BaseOTXTestAction`; +- it MUST override the static field `_name` -- the name of the action, it will be used as a unique + identifier of the test action and it should be unique for the algo backend; +- if validation of the results of the action is required, it MUST override the static field + `_with_validation` and set `_with_validation = True`; +- if it depends on the results of other test actions, it MUST override the field + `_depends_stages_names`, the field should be a list of `str` values and should contain + all the names of actions that's results are used in this action + (the desired order of the names could be the order how the actions should be executed, but note + that even in the case of another order in this list the dependent actions will be executed in the + correct order); +- (NB: the most important) it MUST override the method `__call__` -- the method should execute the + main action of the class and return a dict that will be stored as the action results. + +Please, note that the method `__call__` of an action class MUST also have the following declaration: + +```python + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): +``` + +It receives as the first parameter the `DataCollector` class that allows to store some results of +execution of the action into the test system's database +(if the test is executed on our CI system, these results will be stored to the centralized database +of our CI that could be accessed through several dashboards). + +Also it receives as the second parameter `results_prev_stages` -- it is an `OrderedDict` that +contains all the results of the previous stages: + +- each key is a name of test action +- each value is a dict, that was returned as the result of the action. + +The `__call__` method MUST return as the result a dict that will be stored as the result of the +action (an empty dict is acceptable). + +**Example:** +The class `OTXTestTrainingAction` in the file `test_suite/training_tests_actions.py` +implements the training action for mmdetection, it has `_name = "training"` and its method +`__call__` returns as the result a dict + +```python + results = { + "model_template": self.model_template, + "task": self.task, + "dataset": self.dataset, + "environment": self.environment, + "output_model": self.output_model, + } +``` + +It means that the action class `OTXTestTrainingEvaluationAction` that makes evaluation after +training in its method `__call__` can use + +```python +kwargs = { + "dataset": results_prev_stages["training"]["dataset"], + "task": results_prev_stages["training"]["task"], + "trained_model": results_prev_stages["training"]["output_model"], +} +``` + +### III.2 When implementation of own test action class is required + +Please, note that `test_suite/training_tests_actions.py` contains reference code of actions for +mmdetection algo backend. This is done due to historical reasons and due to fact that mmdetection is +the first algo backend used in OpenVINO™ Training Extensions. + +As we stated above, fortunately, most of test actions may be re-used for other algo backends, since +to make some test action the same OpenVINO™ Training Extensions calls should be done. + +But if for an algo backend some specific test action should be done, an additional test action class +could be also implemented for the algo backend (typically, in the file `test_otx_training.py` in the +folder `tests/` of the algo backend). + +Also if an algo backend should make some test action in a bit different way than in mmdetection, the +test action for the algo backend should be re-implemented. + +_Example:_ For MobileNet models in image classification algo backend the NNCF compression requires +loading of the secondary (auxiliary) model. (It is required since NNCF compression requires +training, and for training MobileNet models deep-object-reid algo backend uses a specific auxiliary +model as a regularizer.) + +Please, note that if you re-implementing a test action class for an algo backend it is HIGHLY +RECOMMENDED that it returns as the result dict with THE SAME keys as for the original test action +class in `test_suite/training_tests_actions.py`, and, obviously, the values for the keys have the +same meaning as for the original class. It is required since other test actions could use the result +of this test action, and if you replace a test action you should keep its interface for other +actions classes -- otherwise you will have to re-implement also all the test actions classes that +depends on this one. + +Also there is a case when a new test action class should be additionally implemented in +`test_suite/training_tests_actions.py` -- when we found out that addition test action should be used +for all algo backends. + +### III.3 How to implement own test action class + +Please, note that this section covers the topic how to implement a new test action class, but does +not cover the topic how to make the test action class to be used by tests -- it is covered below in +the section TODO[should be written]. + +To implement your own test action you should do as follows: + +1. Create a class derived from `OTXTestTrainingAction` +2. Set in the class the field `_name` to the name of the action +3. Set in the class the field `_with_validation = True` if validation of the action results is + required +4. Set in the class the field `_depends_stages_names` to the list of `str` values of the names of + test actions which results will be used in this test +5. Implement a protected method of the class which makes the real work by calling OpenVINO™ Training Extensions operations + NB: the method should receive the parameter `data_collector: DataCollector` and use it to + store some results of the action to the CI database + (see how the class `DataCollector` is used in several actions in + `test_suite/training_tests_actions.py`) +6. Implement the method `__call__` of the class with the declaration + `def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict):` + See as the reference the method `__call__` of the class `OTXTestTrainingEvaluationAction` + from the file `test_suite/training_tests_actions.py`. + The method should work as follows: + +- call `self._check_result_prev_stages(results_prev_stages, self.depends_stages_names)` + (NB: this is a required step, it will allow to catch important errors if you connect several + test actions with each other in a wrong way) +- get from the field `results_prev_stages` results of previous stages that should be used + and convert them to the arguments of the protected method in the item 5 above +- call the protected function from the item 5 above +- the results of the method convert to a dict and return the dict from the method `__call__` + to store them as the result of the action + +## IV. Test stage class + +### IV.1 General description of test stage class + +The class `OTXTestStage` from `test_suite/training_tests_stage.py` works as a wrapper for a test +action. For each instance of a test action an instance of the class `OTXTestStage` is created. + +It's constructor has declaration + +```python +def __init__(self, action: BaseOTXTestAction, stages_storage: OTXTestStagesStorageInterface): +``` + +- The `action` parameter here is the instance of action that is wrapped. + It is kept inside the `OTXTestStage` instance. +- The `stages_storage` here is an instance of a class that allows to get a stage by name, this will + be a test case class that connects all the test stages between each other and keeps in its fields + results of all test stages between tests + (all the test case classes are derived from OTXTestStagesStorageInterface) + +The `stages_storage` instance is also kept inside `OTXTestStage`, it will be used to get for each +stage its dependencies. +Note that the abstract interface class `OTXTestStagesStorageInterface` has the only abstract method +`get_stage` with declaration + +```python +def get_stage(self, name: str) -> "OTXTestStage": +``` + +-- it returns test stage class by its name. + +Note that test stage has the property `name` that returns the name of its action +(i.e. the name of a stage equals to the name of the wrapped action). + +The class `OTXTestStage` has method `get_depends_stages` that works as follows: + +1. get for the wrapped action the list of names from its field `_depends_stages_names` using the + property `depends_stages_names` +2. for each of the name get the stage using the method `self.stages_storage.get_stage(name)` + -- this will be a stage (instance of `OTXTestStage`) that wraps the action with the corresponding + name. +3. Return the list of `OTXTestStage` instances received in the previous item. + +As stated above, the main purposes of the class `OTXTestStage` are: + +- wrap a test action class (see the next item) to run it only once, together with all its + dependencies +- make validation of the results of the wrapped test action if this is required. + +See the next sections about that. + +### IV.2 Running a test action through its test stage + +The class `OTXTestStage` has a method `run_once` that has the following declaration + +```python + def run_once( + self, + data_collector: DataCollector, + test_results_storage: OrderedDict, + validator: Optional[Validator], + ): +``` + +The parameters are as follows: + +- `data_collector` -- interface to connect to CI database, see description of the methods `__call__` + of the actions in the section "III.1 General description of test actions classes." +- `test_results_storage` -- it is an OrderedDict where the results of the tests are kept between + tests, see description of the parameter `results_prev_stages` in the section + "III.1 General description of test actions classes." +- `validator` -- optional parameter, if `Validator` instance is passed, then validation may be done + (see the next section "IV.3 Validation of action results"), otherwise validation is skipped. + +The method works as follows: + +1. runs the dependency chain of this stage using recursive call of `run_once` as follows: + - Get all the dependencies using the method `OTXTestStage.get_depends_stages` described in the + previous section -- it will be the list of other `OTXTestStage` instances. + - For each of the received `OTXTestStage` call the method `run_once` -- it is the recursion step + Attention: in the recursion step the method `run_once` is called with parameter + `validator=None` to avoid validation during recursion step -- see details in the next section + "IV.3 Validation of action results" +2. runs the action of the stage only once: + - If it was not run earlier -- run the action + - if the action executed successfully + - store result of the action into `test_result_storage` parameter + - run validation if required + - return + - if the action executed with exception + - store the exception in a special field + - re-raise the exception + - If it was already run earlier, check if there is stored exception + - if there is no stored exception -- it means that the actions was successful + and its result is already stored in the `test_result_storage` parameter + - run validation if required + (see details in the next section) + - return + - if there is a stored exception -- it means that the actions was NOT successful + - re-raise the exception + +As you can see if an exception is raised during some action, all the actions that depends on this +one will re-raise the same exception. + +Also as you can see if we run a test for only one action, the `run_once` call of the stage will run +actions in all the dependent stages and use their results, but when we run many tests each of the +test also will call `run_once` for all the stages in the dependency chains, but the `run_once` calls +will NOT re-run actions for the tests. + +### IV.3 Validation of action results -- how it works + +As stated above, one of the purposes of `OTXTestStage` is validation of results of the wrapped +action. + +As you can see from the previous section the validation is done inside `run_once` method, +and the necessary (but not sufficient) condition of running validation is that `validator` parameter +of this method is not None. + +The class `Validator` is also implemented in `test_suite/training_tests_stage.py` file. +It has only one public method `validate` that has the declaration + +```python + def validate(self, current_result: Dict, test_results_storage: Dict): +``` + +The parameters are: + +- `current_result` -- the result of the current action +- `test_results_storage` -- an OrderedDict that stores results from the other actions that were run. + +The method returns nothing, but may raise exceptions to fail the test. + +The `Validator` compares the results of the current action with expected metrics and with results of +the previous actions. Note that results of previous actions are important, since possible validation +criteria also may be + +- "the quality metric of the current action is not worse than the result of _that_ action with + possible quality drop 1%" +- "the quality metric of the current action is the same as the result of _that_ action with + possible quality difference 1%" + +-- these criteria are highly useful for "evaluation after export" action (quality should be almost +the same as for "evaluation after training" action) and for "evaluation after NNCF compression" +action (quality should be not worse than for "evaluation after training" action with small possible +quality drop). + +As we stated above in the previous section, when the method `run_once` runs the recursion to run +actions for the dependency chain of the current action, the method `run_once` in recursion step is +called with the parameter `validator=None`. + +It is required since + +- `Validator` does not return values but just raises exception to fail the test if the required + validation conditions are not met +- so, if we ran dependency actions with non-empty `Validator`, then the action test would be failed + if some validation conditions for the dependent stages are failed -- this is not what we want to + receive, since we run the dependency actions just to receive results of these actions +- so, we do NOT do it, so we run dependency chain with `validator=None` + +Also note that there is possible (but rare) case when a stage is called from dependency chain, and +only after that it is run from a test for which this action is the main action. +For this case (as we stated above in the previous section when we described how the method +`run_once` works) we may call validation (if it is required) even if the stage was already run +earlier and was successful. +Why this case is rare? because we ask users to mention dependencies in the field +`_depends_stages_names` in the order of their execution (see description of the field), so typically +the stages are run in the right order. + +As we stated above the `validator is not None` is the necessary condition to run validation, but it +is not sufficient. +The list of sufficient conditions to run real validation in `run_once` is as follows: + +- The parameter `validator` of `run_once` method satisfies `validator is not None` + (i.e. the validation is run not from the dependency chain). +- For the action the field `_with_validation == True`. + If `_with_validation == False` it means that validation for this action is impossible -- e.g. + "export" action cannot be validated since it does not return quality metrics, but the action + "evaluation after export" is validated. +- The current test has the parameter `usecase == "reallife"`. + If a test is not a "reallife" test it means that a real training is not made for the test, + so we cannot expect real quality, so validation is not done. + See description of test parameters below in the section TODO. + +To investigate in details the conditions see the declaration of constructor of the `Validator` +class: + +```python + def __init__(self, cur_test_expected_metrics_callback: Optional[Callable[[], Dict]]): +``` + +As you can see it receives only one parameter, and this parameter is NOT a structure that +describes the requirements for the expected metrics for the action, but the parameter is +a FACTORY that returns the structure. + +It is required since + +1. constructing the structure requires complicated operations and reading of YAML files, +2. if validation should be done for the current test, and the expected metrics for the tests are + absent, the test MUST fail + (it is important to avoid situations when developers forget to add info on expected metrics and + due to it tests are not failed) +3. but if validation for the current test is not required the test should not try to get the + expected metrics + +So to avoid checking of expected metrics structures for the tests without validation, an algo +backend a factory is used -- the factory for an action's validator is called if and only if +the action should be validated. + +The factory is implemented in the test suite as a pytest fixture -- see the fixture +`cur_test_expected_metrics_callback_fx` in the file `test_suite/fixtures.py`. + +The fixture works as follows: + +- receives from other fixtures contents of the YAML file that is pointed to pytest as the pytest + parameter `--expected-metrics-file` +- checks if the current test is "reallife" training or not (if the "usecase" parameter of the test + is set to the value "reallife"), +- if it is not reallife then validation is not required -- in this case + - the fixture returns None, + - the Validator class receives None as the constructor's parameter instead of a factory, + - Validator understands it as "skip validation" +- if this is reallife training test, the fixture returns a factory function + +The returned factory function extracts from all expected metrics the expected metrics for the +current test (and if the metrics are absent -- fail the current test). + +### IV.4 Validation of action results -- how expected metrics are set + +As stated in the previous section, a file with expected metrics for validation is passed to pytest +as an additional parameter `--expected-metrics-file`. +It should be a YAML file. +Such YAML files are stored in each algo backend in the following path +`tests/expected_metrics/metrics_test_otx_training.yml` +(the path relative w.r.t. the algo backend root) +Examples: + +- `external/mmdetection/tests/expected_metrics/metrics_test_otx_training.yml` +- `external/deep-object-reid/tests/expected_metrics/metrics_test_otx_training.yml` +- `external/mmsegmentation/tests/expected_metrics/metrics_test_otx_training.yml` + +The expected metric YAML file should store a dict that maps tests to the expected metric +requirements. + +The keys of the dict are strings -- the parameters' part of the test id-s. This string uniquely +identifies the test, since it contains the required action, and also the description of a model, a +dataset used for training, and training parameters. + +See the detailed description how the method `OTXTestHelper._generate_test_id` works in the +subsection "VI.5.5 `short_test_parameters_names_for_generating_id`" of the section +"VI.5 Methods of the test parameters interface class `OTXTestCreationParametersInterface`" + +Although the id-s are unique, they have a drawback -- they are quite long, since they contain all +the info to identify the test. + +Examples of such keys are: + +- `ACTION-training_evaluation,model-Custom_Object_Detection_Gen3_ATSS,dataset-bbcd,num_iters-CONFIG,batch-CONFIG,usecase-reallife` +- `ACTION-nncf_export_evaluation,model-Custom_Image_Classification_EfficinetNet-B0,dataset-lg_chem,num_epochs-CONFIG,batch-CONFIG,usecase-reallife` + +Example of the whole part of expected metrics configuration for one of mmdetection test cases + +```yaml +"ACTION-training_evaluation,model-Custom_Object_Detection_Gen3_ATSS,dataset-bbcd,num_iters-CONFIG,batch-CONFIG,usecase-reallife": + "metrics.accuracy.f-measure": + "target_value": 0.81 + "max_diff_if_less_threshold": 0.005 + "max_diff_if_greater_threshold": 0.06 +"ACTION-export_evaluation,model-Custom_Object_Detection_Gen3_ATSS,dataset-bbcd,num_iters-CONFIG,batch-CONFIG,usecase-reallife": + "metrics.accuracy.f-measure": + "base": "training_evaluation.metrics.accuracy.f-measure" + "max_diff": 0.01 +"ACTION-pot_evaluation,model-Custom_Object_Detection_Gen3_ATSS,dataset-bbcd,num_iters-CONFIG,batch-CONFIG,usecase-reallife": + "metrics.accuracy.f-measure": + "base": "export_evaluation.metrics.accuracy.f-measure" + "max_diff": 0.01 +"ACTION-nncf_evaluation,model-Custom_Object_Detection_Gen3_ATSS,dataset-bbcd,num_iters-CONFIG,batch-CONFIG,usecase-reallife": + "metrics.accuracy.f-measure": + "base": "training_evaluation.metrics.accuracy.f-measure" + "max_diff_if_less_threshold": 0.01 +"ACTION-nncf_export_evaluation,model-Custom_Object_Detection_Gen3_ATSS,dataset-bbcd,num_iters-CONFIG,batch-CONFIG,usecase-reallife": + "metrics.accuracy.f-measure": + "base": "nncf_evaluation.metrics.accuracy.f-measure" + "max_diff": 0.01 +``` + +As you can see in this example + +- the target metric "metrics.accuracy.f-measure" for the action "evaluation after training" for this + test case is `0.81` with permissible variation `[-0.005, +0.06]` +- the target metric "metrics.accuracy.f-measure" for the action "evaluation after export" should be + the same as for the action "evaluation after training" with permissible variation `[-0.01, +0.01]` +- the target metric "metrics.accuracy.f-measure" for the action "evaluation after pot" should be + the same as for the action "evaluation after export" with permissible variation `[-0.01, +0.01]` +- the target metric "metrics.accuracy.f-measure" for the action "evaluation after nncf" should be + the same as for the action "evaluation after training" with permissible variation + `[-0.01, +infinity]` + +## V. Test Case class + +### V.1 General description of test case class + +As stated above, test case class instance connects the test stages between each other and keeps +in its fields results of the kept test stages between tests. + +Since the instance of this class is kept in the cache of training test helper's instance between +runs of tests, results of one test may be re-used by other tests. + +One of the most important question is when a test may re-use results of another test. +We can consider this from the following point of view. +We suppose that the test suite indeed do not make several independent tests, but make a set of +actions with several "test cases". +Since the test suite works with OpenVINO™ Training Extensions, each "test case" is considered as a situation that could be +happened during some process of work with OpenVINO™ Training Extensions, and the process may include different actions. + +Since OpenVINO™ Training Extensions is focused on training a neural network and making some operations on the trained model, +we defined the test case by the parameters that define training process +(at least they defines it as much as it is possible for such stochastic process). + +Usually the parameters defining the training process are: + +1. a model - typically it is a name of OpenVINO™ Training Extensions template to be used +2. a dataset - typically it is a dataset name that should be used + (we use known pre-defined names for the datasets on our CI) +3. other training parameters: + - `batch_size` + - `num_training_epochs` + +We suppose that for each algo backend there is a known set of parameters that define training +process, and we suppose that if two tests have the same these parameters, then they are belong to +the same test case. +We call these parameters "the parameters defining the test case". + +But from pytest point of view there are just a lot of tests with some parameters. + +The general approach that is used to allow re-using results of test stages between test is the +following: + +- The tests are grouped such that the tests from one group have the same parameters from the list + of "parameters that define the test case" -- it means that the tests are grouped by the + "test cases" +- After that the tests are reordered such that + - the test from one group are executed sequentially one-by-one, without tests from other group + between tests in one group + - the test from one group are executed sequentially in the order defined for the test actions + beforehand; +- An instance of the test case class is created once for each of the group of tests stated above + -- so, the instance of test case class is created for each "test case" described above. + +As stated above, the instance of test case class is kept inside cache in OpenVINO™ Training Extensions Test Helper class, it +allows to use the results of the previous tests of the same test case in the current test. + +### V.2 Base interface of a test case class, creation of a test case class + +The class of the test case is generated "on the fly" by the function +`generate_otx_integration_test_case_class` from the file `test_suite/training_test_case.py`; +the function has the declaration + +```python +def generate_otx_integration_test_case_class( + test_actions_classes: List[Type[BaseOTXTestAction]], +) -> Type: +``` + +The function `generate_otx_integration_test_case_class` works as follows: + +- receives as the input the list of action classes that should be used in the test case + -- the test case will be a storage for the test stages wrapping the actions and will connect the + test stages with each other +- and returns the class type that will be used by the instance of the test helper. + +The variable with the type of test case received from the function is stored in the test helper +instance -- it is stored in a special class "test creation parameters", see about it below in the +section TODO. + +Note that the result of this function is a `class`, not an `instance` of a class. +Also note that the function receives list of action `classes`, not `instances` -- the instances of +test action classes are created when the instance of the test case class is created. + +The class of the test case for a test is always inherited from the abstract interface class +`OTXTestCaseInterface`. +It is derived from the abstract interface class `OTXTestStagesStorageInterface`, so it has the +abstract method `get_stage` that for a string `name` returns test stage instance with this name. + +The interface class `OTXTestCaseInterface` has two own methods: + +- abstract classmethod `get_list_of_test_stages` without parameters that returns the list of names + of test stages for this test case +- abstract method `run_stage` that runs a stage with pointed name, the method has declaration + +```python + @abstractmethod + def run_stage(self, stage_name: str, data_collector: DataCollector, + cur_test_expected_metrics_callback: Optional[Callable[[], Dict]]): +``` + +When the test case method `run_stage` is called, it receives as the parameters + +- `stage_name` -- the name of the test stage to be called +- `data_collector` -- the `DataCollector` instance that is used when the method `run_once` of the + test stage is called +- `cur_test_expected_metrics_callback` -- a factory function that returns the expected metrics for + the current test, the factory function is used to create the `Validator` instance that will make + validation for the current test. + +The method `run_stage` of a created test case class always does the following: + +1. checks that `stage_name` is a known name of a test stage for this test case +2. creates a `Validator` instance for the given `cur_test_expected_metrics_callback` +3. finds the test stage instance for the given `stage_name` and run for it `run_once` method as + described above in the section "IV.2 Running a test action through its test stage" with the + parameters `data_collector` and validator + +If we return back to the `OTXTestCaseInterface`, we can see that the test case class derived from it +should implement the classmethod `get_list_of_test_stages` without parameters that returns the list +of names of test stages for this test case. + +Note that this method `get_list_of_test_stages` is a classmethod, since it is used when pytest +collects information on tests, before the first instance of the test case class is created. + +> NB: We decided to make the test case class as a class that is generated by a function instead of a +> "normal" class, since we would like to encapsulate everything related to the test case in one +> entity -- due to it the method`get_list_of_test_stages` is not a method of a separate entity, but +> a classmethod of the test case class. +> This could be changed in the future. + +Also note that the function `generate_otx_integration_test_case_class` does not makes anything +really complicated for creation of a test case class: all test case classes are the same except the +parameter `test_actions_classes` with the list of action classes that is used to create test stage +wrapper for each of the test action from the list. + +### V.3 The constructor of a test case class + +As stated above, the function `generate_otx_integration_test_case_class` receives as a parameter +list of action `classes`, not `instances` -- the instances of test action classes are created when +the instance of the test case class is created. +That is during construction of test case class its constructor creates instances of all the actions. + +Each test case class created by the function `generate_otx_integration_test_case_class` has +the following constructor: + +```python +def __init__(self, params_factories_for_test_actions: Dict[str, Callable[[], Dict]]): +``` + +The only parameter of this constructor is `params_factories_for_test_actions` that is +a dict: + +- each key of the dict is a name of a test action +- each value of the dict is a factory function without parameters that returns the + structure with kwargs for the constructor of the corresponding action + +Note that most of the test actions do not receive parameters at all -- they receive the result of +previous actions, makes its own action, may make validation, etc. + +For this case if the dict `params_factories_for_test_actions` does not contain as a key the name of +an action, then the constructor of the corresponding action will be called without parameters. + +The constructor works as follows: + +- For each action that was passed to the function `generate_otx_integration_test_case_class` during + creation of this test case class + - take name of the action + - take `cur_params_factory = params_factories_for_test_actions.get(name)` + - if the result is None, `cur_params = {}` + - otherwise, `cur_params = cur_params_factory()` + - call constructor of the current action as + `cur_action = action_cls(**cur_params)` + - wraps the current action with the class `OTXTestStage` as follows: + `cur_stage = OTXTestStage(action=cur_action, stages_storage=self)` + - store the current stage instance as + `self._stages[cur_name] = cur_stage` + +As you can see for each factory in the dict `params_factories_for_test_actions` the factory is +called lazily -- it means, it is called when and only when the corresponding action should be +created. + +Also as you can see the dict `params_factories_for_test_actions` with the factories is passed to the +constructor as the parameter -- so, the factories may be different for each test to pass to the test +case the values corresponding to the current test. + +## VI. Test Helper class + +### VI.1 General description + +Training test helper class `OTXTestHelper` is implemented in `test_suite/training_tests_helper.py`. +An instance of the class controls all execution of tests and keeps in its cache an instance of a +test case class between runs of different tests. + +The most important method of the class are + +- `get_list_of_tests` -- allows pytest trick generating test parameters for the test class. + When pytest collects the info on all tests, the method returns structures that allows to make + "pytest magic" to group and reorder the tests (see details below). +- `get_test_case` -- gets an instance of the test case class for the current test parameters, allows + re-using the instance between several tests. + +Note that the both of the methods work with test parameters that are used by pytest. + +### VI.2 How pytest works with test parameters + +#### VI.2.1 Short description how pytest works + +Since `OTXTestHelper` makes all the operations related to pytest parametrization mechanisms, we need +to describe here how pytest works with test parameters. + +Generally pytest works as follows: +(NB: it is a short and may be approximate description! do not use it as a pytest documentation) + +1. Pytest collects test info, for each test function or test method it gets information on + parameters of the test and possible combination of parameters that may be executed. +2. Then pytest makes filtering -- it selects/deselects tests based on the pytest parameters + (e.g. `-k`) and the names of the tests + -- each test with some combination of parameters has a full name of "test with parameters" that + uniquely identifies the test with the parameters +3. Then pytest executes the selected tests one by one. + When pytest executes a test function or a test method it gets a concrete combinations of + parameter values for the parameters of the test and executes the test function/method with this + combination. + During the execution pytest may print the full name of the "test with parameters" + +#### VI.2.2 How pytest gets information on parameters + +In pytest the information on test parameters for each test function/method consists of the following +3 elements: +(NB: it is a short and may be approximate description! do not use it as a pytest documentation) + +1. `argnames` -- a tuple of names of parameters of the test, typically this is a short tuple of + strings + - its length is the number of parameters of the test, + - it contains string names of the parameters +2. `argvalues` -- a list of parameters of the test, this is a long list, + - its length is the number of different combination of parameter values for the test, + - each element of the list should be a tuple, + - the length of each of the tuples is the same as the length of `argnames` above, + - the tuple stores a concrete combination of values of the parameters +3. `ids` -- a list of string identifiers, + - the list has the same length as the list `argvalues` + - each value is a string + - the string is used as an ID of the concrete combination of parameters + particularly, this parameters ID is used when pytest generates the full name of the + "test with parameters" + (as stated above it is required for printing the full name or when some filtering is made in + pytest on full test names) + -- note that usually this full name in pytest looks as + `test_name + "[" + parameters_ID + "]"` + +Usually pytest collects this information inside itself, but our test suite uses special interface +that allows to change it: if pytest finds the function `pytest_generate_tests` with declaration + +```python +def pytest_generate_tests(metafunc): +``` + +then special "pytest magic" is allowed. This 'pytest magic" allows sets for a concrete test +function/method the three elements stated above. + +See a bit more details how this pytest magic works in the description of the function +`otx_pytest_generate_tests_insertion` below in the section TODO. + +#### VI.2.3 How pytest runs a test with a combination of parameters + +When pytest runs a test function/method that has some parameters, pytest works as follows: +(NB: it is a short and may be approximate description! do not use it as a pytest documentation) + +1. gets the triplet `argnames, argvalues, ids` for this test function/method +2. check that the test function/method has all the parameters with names from the tuple `argnames` +3. makes filtering (selecting/deselecting) of concrete parameter values combinations as on pairs of + `zip(argvalues, ids)` based on `ids` string identifiers and different pytest command line + arguments (see pytest option `-k`) +4. for each selected combination of parameter values -- a pair `(arvalue_el, id)` from + `zip(argvalues, ids)` -- do the following: + - check that `argvalue_el` is a tuple with the length equal to `argnames` + - create kwargs dict for the test function/method + - sets in the kwargs dict for each key from `argnames` the corresponding value from + `argvalue_el` probably in the following manner: + `for i in range(len(argnames)): kwargs[argnames[i]] = argvalue_el[i]` + +### VI.3 How pytest parametrization mechanisms relates to the test suite and `OTXTestHelper` + +**(IMPORTANT)** The description how pytest works with test functions/methods parametrization in the +previous section relates to all pytest-based code. +But we would like to describe some important points related to `OTXTestHelper` and the test suite as +a whole: + +- typically for one OpenVINO™ Training Extensions task type for all training tests there is only one test class with only only + one test method that has a lot of combination of test parameters values +- the method `get_list_of_tests` of `OTXTestHelper` returns this triplet + `argnames, argvalues, ids` that is used later in `pytest_generate_tests`-related pytest magic to + parametrize this test method + Note that the triplet `argnames, argvalues, ids` received from `get_list_of_tests` is used as is + without any changes. +- `OTXTestHelper` always defines `argnames = ("test_parameters",)`, so formally the only test method + uses **only one** test parameter to parametrise tests, but values of the parameter are dict-s that + contain info on real test parameters + +### VI.4 Constructor of the class `OTXTestHelper` + +The constructor of the class `OTXTestHelper` has the following declaration + +```python +def __init__(self, test_creation_parameters: OTXTestCreationParametersInterface): +``` + +As you can see it receives as the only parameter the class that is derived from +`OTXTestCreationParametersInterface`. +We will refer to it as a _test parameters class_ and we will refer to the base class +`OTXTestCreationParametersInterface` as to _test parameters interface_. + +We suppose that such test parameter class derived from `OTXTestCreationParametersInterface` contains +most of information required to connect the test suite with a concrete algo backend. +All the methods of the interface class are abstract methods without parameters that return +structures making this connection. + +Example of such implementation is the class `DefaultOTXTestCreationParametersInterface` that +contains implementation of almost all the test parameter class methods for mmdetection algo backend +(mmdetection is chosen due to historical reasons). +Nevertheless, although these methods are implemented for mmdetection, most of them may +be used without modification (or with only slight modification) for other algo backends. + +The constructor of the class `OTXTestHelper` indeed makes the following: + +- calls the methods of the received parameter class instance and stores the info received as + the result of the calls in the `OTXTestHelper` instance fields +- check that the info stored in `OTXTestHelper` instance fields has a proper structure +- initialize a cache to store a test case class + +### VI.5 Methods of the test parameters interface class `OTXTestCreationParametersInterface` + +Let's consider all the methods of the abstract test parameters interface class one by one: + +- `test_case_class` +- `test_bunches` +- `default_test_parameters` +- `test_parameters_defining_test_case_behavior` +- `short_test_parameters_names_for_generating_id` + +#### VI.5.1 `test_case_class` + +```python +@abstractmethod +def test_case_class(self) -> Type[OTXTestCaseInterface]: +``` + +The method returns a class that will be used as a Test Case class for training tests. +Note that it should return a class itself (not an instance of the class). + +Typically OpenVINO™ Training Extensions Test Case class should be generated by the function +`generate_otx_integration_test_case_class` and the only parameter of the function is the list of all +test action classes that should be used in the training tests for the algo backend. + +See details above in the section "V. Test Case class" + +#### VI.5.2 `test_bunches` + +This is the most important method since it defines the scope of the tests. + +```python +@abstractmethod +def test_bunches(self) -> List[Dict[str, Any]]: +``` + +The method returns a test bunches list, it defines the combinations of test parameters for +which the test suite training test should be run. + +The method should return a list of dicts, each of the dicts defines one test case -- see description +how test cases are defined in the section "V.1 General description of test case class". +We will call such a dict _"a test bunch dict"_ or just a _"test bunch"_. + +All keys of the test bunch dicts are strings. + +**(IMPORTANT)** +As stated above in "VI.3 How pytest parametrization mechanisms relates to the test suite and +`OTXTestHelper`" typically an algo backend for training tests has only one test class with only one +test method. +Note that in a typical situation a test bunch dict is passed to the only test method of the training +test class as the value `test_parameters` -- see again the section +"VI.3 How pytest parametrization mechanisms relates to the test suite and `OTXTestHelper`" + +Mandatory keys of the test bunch dicts are: + +- `"model_name"` -- the value is a string that is the name of a model to work with as it is defined + in the template.yaml file of the model +- `"dataset_name"` -- the value is a string that is the name of the dataset, note that we use known + pre-defined names for the datasets on our CI +- `"usecase"` -- the value is a string, if it is equal to `REALLIFE_USECASE_CONSTANT="reallife"` + then validation will be run for the tests + +Also typical non-mandatory keys of the test bunch dicts are + +- `"num_training_iters"` or `"num_training_epochs"` or `"patience"` -- integer parameter + restricting the training time +- `"batch_size"` -- integer parameter, affects training speed and quality + +Note that the following additional tricks are used: + +1. For the mandatory fields `"model_name"` and `"dataset_name"` the value may be not only a string, + but a list of strings -- in this case a Cartesian product of all possible pairs + `(model, dataset)` is used. + This is the reason why this method is called `test_bunches` -- since each element of the returned + list may define a "bunch" of tests +2. If a non-mandatory key in a test bunch dict is absent or equals to a string + `DEFAULT_FIELD_VALUE_FOR_USING_IN_TEST`, then it may be replaced by the corresponding default + value pointed by the method `default_test_parameters` + (see about it below in the section "VI.5.3 `default_test_parameters`") + +Note that also most of actions that make real training (e.g. `OTXTestTrainingAction`) use one more +additional trick: if values either for `batch_size` key or for `num_training_iters` key in a test +bunch dict contain a string constant `KEEP_CONFIG_FIELD_VALUE="CONFIG"` instead of an integer value, +the action reads the values of such parameters from the template file of the model or internal +config of the model and do not change them. +It is important when we want to keep some training parameters "as is" for reallife tests and do not +want to point our own values for them. + +Example of a test bunch that could be in `external/mmdetection/tests/test_otx_training.py` + +``` +[ + dict( + model_name=[ + 'Custom_Object_Detection_Gen3_ATSS', + 'Custom_Object_Detection_Gen3_SSD', + ], + dataset_name='dataset1_tiled_shortened_500_A', + usecase='precommit', + ), + ... + dict( + model_name=[ + 'Custom_Object_Detection_Gen3_ATSS', + 'Custom_Object_Detection_Gen3_SSD', + ], + dataset_name=[ + 'bbcd', + 'weed-coco', + 'pcd', + 'aerial', + 'dice', + 'fish', + 'vitens', + 'diopsis', + ], + num_training_iters=KEEP_CONFIG_FIELD_VALUE, + batch_size=KEEP_CONFIG_FIELD_VALUE, + usecase=REALLIFE_USECASE_CONSTANT, + ) +] +``` + +-- in this example + +- the first test bunch will make test suite to run tests for two models (ATDD and SSD) on the + dataset `dataset1_tiled_shortened_500_A` with non-reallife training with the default `batch_size` + and `num_training_iters` +- the second test bunch will will make test suite to run tests for two models (ATDD and SSD) on 8 of + datasets (all pairs `model,dataset` will be run) with reallife training with the `batch_size` and + `num_training_iters` from the original template config. + +#### VI.5.3 `default_test_parameters` + +```python +@abstractmethod +def default_test_parameters(self) -> Dict[str, Any]: +``` + +The method returns a dict that points for test parameters the default values. +The dict should have the following structure: + +- each key is a string, it is a possible key in a test bunch dict +- each value is the default value for this test bunch dict key (typically, for `"batch_size"` + and `"num_training_iters"` it is integer). + +During construction of a test helper class its call the method `default_test_parameters` and stores +it to an inner field -- _default value dict_. + +When a test helper instance prepares the triplet `argnames, argvalues, id` for the training test +parametrization, it does it using as the base the value received from the method `test_bunches` +-- see above in the section "VI.5.2 `test_bunches`". +As stated above in those section, during this preparation sometimes it fills some fields in the test +bunch dict by the default values. + +In details, test helper in this case works as follows: + +- get the default values dict received from the call of the method `default_test_parameters` of the + test parameter class +- for each key in the dict + - if the key is absent in the test bunch dict, or the test bunch dict contains for the key value + `"DEFAULT_FIELD_VALUE_FOR_USING_IN_TEST"`, then + - set in the test bunch dict for the key the value from the default value dict + +After that test helper continue work with test bunch dict as if the values always were here. + +#### VI.5.4 `test_parameters_defining_test_case_behavior` + +```python +@abstractmethod +def test_parameters_defining_test_case_behavior(self) -> List[str]: +``` + +The method returns a list of strings -- names of the test parameters +(i.e. keys of test bunches dicts) that define test case behavior. + +See the detailed explanation on test cases and parameters defining test case in the section +"V.1 General description of test case class". + +When several test cases are handled, if the next test has these parameters +the same as for the previous test, the test case class is re-used for the next test. +This is what allows re-using the result of previous test stages in the next test stages. + +#### VI.5.5 `short_test_parameters_names_for_generating_id` + +```python +@abstractmethod +def short_test_parameters_names_for_generating_id(self) -> OrderedDict: +``` + +This method returns an `OrderedDict` that is used to generate the `ids` part of the triplet +`argnames, argvalues, ids` that is returned by the OpenVINO™ Training Extensions test helper method `get_list_of_tests` for +the training test parametrization. + +The returned OrderedDict has the following structure + +- each key is a string that is a key of test bunch dicts that should be used for generating id-s +- each value is a short name of this key that will be used as an alias for string id-s generating + +In details, for each combination of test parameters the string identifier `id` for the parameters' +combination is generated by the method `OTXTestHelper._generate_test_id` that is equivalent to the +following one: + +```python + def _generate_test_id(self, test_parameters): + id_parts = [] + for par_name, short_par_name in self.short_test_parameters_names_for_generating_id.items(): + id_parts.append(f"{short_par_name}-{test_parameters[par_name]}") + return ",".join(id_parts) +``` + +(here `self.short_test_parameters_names_for_generating_id` is the OrderedDict stored in the +constructor) + +Note that + +- If a key of test bunch dicts is not present in this OrderedDict, then it will not be present in + the string identifier. + So it is important to have as keys all elements of the list returned by + `test_parameters_defining_test_case_behavior` in this OrderedDict. +- Since the length of test identifiers may be an issue, it is important to have as the values of the + OrderedDict descriptive, but short aliases. + +Example of such OrderedDict for mmdetection is as follows: + +```python +OrderedDict( + [ + ("test_stage", "ACTION"), + ("model_name", "model"), + ("dataset_name", "dataset"), + ("num_training_iters", "num_iters"), + ("batch_size", "batch"), + ("usecase", "usecase"), + ] +) +``` + +### VI.6 How the method `OTXTestHelper.get_list_of_tests` works + +As stated above, the method `get_list_of_tests` returns the triplet +`argnames, argvalues, ids` that is used later in `pytest_generate_tests`-related pytest magic to +parametrize this test method, and the triplet `argnames, argvalues, ids` received from +`get_list_of_tests` is used as is without any changes. + +The method `get_list_of_tests` of the class `OTXTestHelper` works as follows: + +- set `argnames = ("test_parameters",)` -- as we stated above test suite training tests always use + one parameter for pytest test, but the values of the parameter will be a dict +- get `test_bunches` list stored earlier to a field from test parameters class in constructor + See the detailed description in the section "VI.5.2 `test_bunches`" +- get the class `test_case_class` stored earlier to a field from test parameters class in + constructor + See the detailed description in the section "VI.5.1 `test_case_class`" +- initialize + `argvalues = []` + `ids = []` +- for each test bunch in the list: + - take the mandatory fields `model_name` and `dataset_name` from the test bunch dict + - create the list of pairs `(model_name, dataset_name)` to be handled: + - if either the field `model_name` or `dataset_name` is a list, generate cartesian product of + all possible pairs using `itertools.product` + - otherwise just take one pair `(model_name, dataset_name)` + - for each pair `(model_name, dataset_name)` + - for each test action name received from `test_case_class.get_list_of_test_stages()` + - make deepcopy of the test bunch dict + - set the key `"test_stage"` in the copied dict to the current test action name + - set the keys `model_name` and `dataset_name` in the copied dict to the current model name + and dataset name + - make filling of the default values in the copied test bunch dict + -- see the detailed description how it is done above in the subsection + "VI.5.3 `default_test_parameters`" of the section + "VI.5 Methods of the test parameters interface class `OTXTestCreationParametersInterface`" + - generate the string id that corresponds to this combination of parameters using the method + `OTXTestHelper._generate_test_id` + -- see the detailed description how this method works in the subsection + "VI.5.5 `short_test_parameters_names_for_generating_id`" of the section + "VI.5 Methods of the test parameters interface class `OTXTestCreationParametersInterface`" + - append to `argvalues` the current copied-and-modified dict + - append to `ids` the generated string id +- when exit from all the cycles, return the triplet `argnames, argvalues, ids` + +What is the result of this function? + +As we stated above in the section "V.1 General description of test case class" to work properly the +test suite tests should be organized as follows: + +> - The tests are grouped such that the tests from one group have the same parameters from the list +> of "parameters that define the test case" -- it means that the tests are grouped by the +> "test cases" +> - After that the tests are reordered such that +> - the test from one group are executed sequentially one-by-one, without tests from other group +> between tests in one group +> - the test from one group are executed sequentially in the order defined for the test actions +> beforehand; + +Since for an algo backend we typically have only one test class for the training tests, only one +test method in the class, and the method is parametrized by the triplet `argnames, argvalues, ids` +received from the function `get_list_of_tests`, described above, we can say that these conditions +are fulfilled. + +### VI.7 How the method `OTXTestHelper.get_test_case` works + +As stated above `get_test_case` -- gets an instance of the test case class for the current test +parameters, allows re-using the instance between several tests. + +It has the following declaration: + +```python +def get_test_case(self, test_parameters, params_factories_for_test_actions): +``` + +It has the following parameters: + +- `test_parameters` -- the parameters of the current test, indeed it is one of elements of the list + `argvalues` from the triplet `argnames, argvalues, ids` received from the method + `get_list_of_tests` -- see the previous section how it is generated +- `params_factories_for_test_actions` -- this is a dict mapping action names to factories, + generating parameters to construct the actions, it is the same as the input parameter for the test + case class, see detailed description in the section + "V.3 The constructor of a test case class" + Note that this parameter is passed to the constructor of a test case class without any changes. + +Also as stated above in the section "V.1 General description of test case class" to make test suite +tests work properly the following should be fulfilled: + +> - An instance of the test case class is created once for each of the group of tests stated above +> -- so, the instance of test case class is created for each "test case" described above. + +Also as we stated at the bottom of the previous section, the parameters of the only test method of +the training tests are reordered in such a way that the tests from one test case are executed +sequentially, without tests from another test case between them. + +And, as also was stated in the section "V.1 General description of test case class" + +> We suppose that for each algo backend there is a known set of parameters that define training +> process, and we suppose that if two tests have the same these parameters, then they are belong to +> the same test case. +> We call these parameters "the parameters defining the test case". + +These parameters defining test case are received by test helper instance from the method +`test_parameters_defining_test_case_behavior` of the test parameters class. + +So to keep one test case class instance the method `get_test_case` of the test helper class +`OTXTestHelper` works as follows: + +- get the class `test_case_class` stored earlier to a field from test parameters class in + constructor + See the detailed description in the section "VI.5.1 `test_case_class`" +- get the list of string `important_params = self.test_parameters_defining_test_case_behavior` + -- get the list of names of parameters defining test case, it was stored earlier to a field from + the test parameters class + See the detailed description in the section "VI.5.4 `test_parameters_defining_test_case_behavior`" +- if we already created and stored in the cache some instance of the test case class, + - check the parameters that were used during its creation: + if for all parameters from the list `important_params` the values of + the parameters were the same + - if it is True -- it is the same test case, so the function just returns the stored instance of + the test case class +- Otherwise -- that is, if either the cache does not contain created instance of test case class, or + some parameters from the list `important_params` were changed -- tests for another test case are + started. + In this case the function creates a new instance of the class `test_case_class` passing to its + constructor the parameter `params_factories_for_test_actions` + +## VII. Connecting algo backend with test suite. Test class in algo backend + +The direct connection between the training test in an algo backend and the test suite is made by + +- Algo backend implementation of some fixtures required for test suite + -- see about that in the next section TODO +- Insertions that is made in the special algo backend file `tests/conftest.py` that is loaded by + pytest before starting its work -- all the pytest magic is inserted into it. +- Test parameter class that will provide parameters to connect the algo backend with the test suite +- A test case class in the file `tests/test_otx_training.py` in the algo backend + +Note again that before the test class there should be implemented a test parameters class that will +provide parameters to connect the algo backend with with test suite. +It should be a class derived from the test parameters interface class +`OTXTestCreationParametersInterface`. +See details above in the sections "VI.4 Constructor of the class `OTXTestHelper`" and +"VI.5 Methods of the test parameters interface class `OTXTestCreationParametersInterface`" +As an example of the test parameters class see + +- the class `ObjectDetectionTrainingTestParameters` in the file + `external/mmdetection/tests/test_otx_training.py` +- the class `ClassificationTrainingTestParameters` in the file + `external/deep-object-reid/tests/test_otx_training.py` + -- the latter is more interesting, since deep-object-reid algo backend is different w.r.t. the + mmdetection algo backend, and we implemented the default test case parameter class + `DefaultOTXTestCreationParametersInterface` mostly for mmdetection. + +Note that test class class itself contains mostly a boilerplate code that connects test suite with +pytest. +(We made out the best to decrease the number of the boilerplate code, but nevertheless it is +required.) + +Also note that the test class uses a lot of fixtures implemented in test suite. + +The test case class should be implemented as follows: + +- The test class should be derived from the interface class `OTXTrainingTestInterface`. + This is required to distinguish the test classes implemented for the test suite: when pytest magic + related to the function `pytest_generate_tests` works, it checks if the current test class is a + subclass of this interface `OTXTrainingTestInterface` and makes parametrization only in this case. + (See details on this pytest magic above in the section + "VI.2.2 How pytest gets information on parameters" and below in the section TODO) + + The interface class has only one abstract classmethod `get_list_of_tests` -- see on its + implementation below. + +- The test class should have a static field `helper` defined as follows: + ```python + helper = OTXTestHelper(()) + ``` +- The test class should have the following implementation of the method `get_list_of_tests` + ```python + @classmethod + def get_list_of_tests(cls, usecase: Optional[str] = None): + return cls.helper.get_list_of_tests(usecase) + ``` +- The test class should implement as its method the fixture `params_factories_for_test_actions_fx` + that will give the parameters for actions for the current test. + It should work as follows: + + - use other fixtures to extract info on the current test parameters and some parameters of the + environment (e.g. the root path where datasets is placed, etc) + - create factories generating parameters for the test actions as function closures using + the info extracted from the fixtures + - and the result of the fixture is the dict `params_factories_for_test_actions` + that maps the name of each action that requires parameters to one of the factories + + **Example**: if the algo backend has two actions that require parameters in the constructors, and + the first of the action has the name "training" and its constructor has parameters + `def __init__(self, dataset, labels_schema, template_path, num_training_iters, batch_size):` + then the fixture `params_factories_for_test_actions_fx` should return a dict + `params_factories_for_test_actions` such that + `params_factories_for_test_actions["training"]` is a function closure that returns a dict + + ```python + return { + 'dataset': dataset, + 'labels_schema': labels_schema, + 'template_path': template_path, + 'num_training_iters': num_training_iters, + 'batch_size': batch_size, + } + ``` + +- The test class should implement as its method the fixture `test_case_fx` that will return the test + case from the current implementation using the test helper cache: if it is required the + instance of the test case class is created, otherwise the cached version of the instance is used + (See detailed description above in the section + "VI.7 How the method `OTXTestHelper.get_test_case` works") + This fixture should have the following implementation + ```python + @pytest.fixture + def test_case_fx(self, current_test_parameters_fx, params_factories_for_test_actions_fx): + test_case = type(self).helper.get_test_case(current_test_parameters_fx, + params_factories_for_test_actions_fx) + return test_case + ``` +- The test class should implement as its method the fixture `data_collector_fx` that will return the + test the `DataCollector` instance + NB: probably this fixture should be moved to the common fixtures + See examples in `external/mmdetection/tests/test_otx_training.py` + +- The test class should implement as its method the only test method with the name `test` and the + following implementation: + ```python + @e2e_pytest_performance + def test(self, + test_parameters, + test_case_fx, data_collector_fx, + cur_test_expected_metrics_callback_fx): + test_case_fx.run_stage(test_parameters['test_stage'], data_collector_fx, + cur_test_expected_metrics_callback_fx) + ``` + +## VIII. Connecting algo backend with test suite. Pytest magic and fixtures + +## VIII.1. Connecting algo backend with test suite. Pytest magic + +As stated above in the previous section the direct connection between the training test in an algo +backend and the test suite is made, particularly, by + +> - Algo backend implementation of some fixtures required for test suite +> -- see about that in the next section TODO +> - Insertions that is made in the special algo backend file `tests/conftest.py` that is loaded by +> pytest before starting its work -- all the pytest magic is inserted into it. + +The algo backend file `tests/conftest.py` is very important, since it is loaded by pytest before +many other operations, particularly, before collecting the tests. + +The file `tests/conftest.py` for algo backend should implement the following two functions + +- `pytest_generate_tests` -- as we stated above in the section + "VI.2.2 How pytest gets information on parameters" it allows to override parametrization of a test + function/method + This function is called for each pytest function/method and gives the possibility to parametrize the test + through its parameter `metafunc` +- `pytest_addoption` -- the function allows to add more command line arguments to pytest, + the values passed to the command line arguments may be read later using the pytest fixture + `request`. + The function is called once before parsing of pytest command line parameters. + +In test suite the file `otx_sdk/otx_sdk/test_suite/pytest_insertions.py` contains implementations of +the special functions `otx_pytest_generate_tests_insertion` and `otx_pytest_addoption_insertion` +that makes all what is required for the test suite. + +As the result the minimal implementation of the functions `pytest_generate_tests` and +`pytest_addoption` contain the following boilerplate code only + +```python +# pytest magic +def pytest_generate_tests(metafunc): + otx_pytest_generate_tests_insertion(metafunc) + +def pytest_addoption(parser): + otx_pytest_addoption_insertion(parser) +``` + +(Why we say that it is "a minimal implementation"? because the algo backend could make its own +operations in these two functions pytest, the test suite implementation of the insertions allow to +use them together with other code.) + +As we can see from the implementation `otx_pytest_generate_tests_insertion`, its main operations are +as follows: +(note that this function is called for each test function/method) + +- the function get the current test class using `metafunc.cls` +- if the class is None (for test functions) or is not a subclass of `OTXTrainingTestInterface`, then + return +- otherwise make + ```python + argnames, argvalues, ids = metafunc.cls.get_list_of_tests(usecase) + ``` +- parametrize the current test method by the call + ```python + metafunc.parametrize(argnames, argvalues, ids=ids, scope="class") + ``` + Note that the scope "class" is used, it is required. + +## VIII.2. Connecting algo backend with test suite. Pytest fixtures and others + +To connect an algo backend with the test suite the following fixtures should be implemented +in the file `tests/conftest.py` of the algo backend. + +- the fixture `otx_test_domain_fx` -- it should return the string name of the + current algo backend domain +- the fixture `otx_test_scenario_fx` -- it should return the string on the + current test scenario, usually we use the following implementation + ```python + @pytest.fixture + def otx_test_scenario_fx(current_test_parameters_fx): + assert isinstance(current_test_parameters_fx, dict) + if current_test_parameters_fx.get('usecase') == REALLIFE_USECASE_CONSTANT: + return 'performance' + else: + return 'integration' + ``` +- the fixture `otx_templates_root_dir_fx` -- it should return the absolute + path of the folder where OpenVINO™ Training Extensions model templates are stored for this algo backend, usually it uses + something like `osp.dirname(osp.dirname(osp.realpath(__file__)))` to get the absolute path to the + root of the algo backend and then using knowledge of algo backend structures point to the template + path +- the fixture `otx_reference_root_dir_fx` -- it should return the absolute + path of the folder where the reference values for some test operations are stored (at the moment + such folder store the reference files for NNCF compressed graphs for the model templates). + +Also the following operations should be done + +```python +pytest_plugins = get_pytest_plugins_from_ote() +otx_conftest_insertion(default_repository_name='src/otx/training_extensions/external/mmdetection') +``` + +The first line points to pytest additional modules from which the fixtures should be loaded -- these +may be e2e package modules and test suite fixture module. + +The second line makes some operations on variables in e2e test library that is used in our CI. diff --git a/tests/test_suite/QUICK_HOWTO.md b/tests/test_suite/QUICK_HOWTO.md new file mode 100644 index 00000000000..bb318d5ae40 --- /dev/null +++ b/tests/test_suite/QUICK_HOWTO.md @@ -0,0 +1,185 @@ +# Quick HOW TO add training tests using OpenVINO™ Training Extensions test suite + +## I. Introduction to OpenVINO™ Training Extensions test suite + +### I.1 General description + +OpenVINO™ Training Extensions test suite allows to create training tests + +The training tests are tests that may run in some unified manner such stages (or, as we also +call it, "actions") as + +- training of a model, +- evaluation of the trained model, +- export or optimization of the trained model, +- and evaluation of exported/optimized model. + +Typically each OpenVINO™ Training Extensions algo backend contains test file `test_otx_training.py` that allows to run the +training tests. + +Note that there are a lot of dependencies between different stages of training tests: most of them +require trained model, so they depends on training stage; also for example POT optimization stage +and evaluation of exported model stage require the exported model, so export stage should be run +before, etc. + +The `test_suite` library allows to create training tests such that + +1. the tests do not repeat the common steps that can be re-used +2. if we point for pytest that only some test stage is required, all dependency stages are run + automatically +3. if a stage is failed all the stage that depend on this stage are also failed. + +To avoid repeating of the common steps between stages the results of stages should be kept in a +special cache to be re-used by the next stages. + +We suppose that each test executes one test stage (also called test action). + +At the moment we have the following test actions: + +- class `"training"` -- training of a model +- class `"training_evaluation"` -- evaluation after the training +- class `"export"` -- export after the training +- class `"export_evaluation"` -- evaluation of exported model +- class `"pot"` -- POT compression of exported model +- class `"pot_evaluation"` -- evaluation of POT-compressed model +- class `"nncf"` -- NNCF-compression of the trained model +- class `"nncf_graph"` -- check of NNCF compression graph (work on not trained model) +- class `"nncf_evaluation"` -- evaluation of NNCF-compressed model +- class `"nncf_export"` -- export of NNCF-compressed model +- class `"nncf_export_evaluation"` -- evaluation after export of NNCF-compressed model + +### I.2. General description of test cases + +One of the most important question is when a test may re-use results of another test. +We can consider this from the following point of view. +We suppose that the test suite indeed do not make several independent tests, but make a set of +actions with several "test cases". +Since the test suite works with OpenVINO™ Training Extensions, each "test case" is considered as a situation that could be +happened during some process of work with OpenVINO™ Training Extensions, and the process may include different actions. + +Since OpenVINO™ Training Extensions is focused on training a neural network and making some operations on the trained model, +we defined the test case by the parameters that define training process +(at least they defines it as much as it is possible for such stochastic process). + +Usually the parameters defining the training process are: + +1. a model - typically it is a name of OpenVINO™ Training Extensions template to be used + -- this is the field `model_template_id` of the model template YAML file +2. a dataset - typically it is a dataset name that should be used + (we use known pre-defined names for the datasets on our CI) +3. other training parameters: + - `batch_size` + - `num_training_epochs` or `num_training_iters` + +We suppose that for each algo backend there is a known set of parameters that define training +process, and we suppose that if two tests have the same these parameters, then they are belong to +the same test case. +We call these parameters "the parameters defining the test case". + +But from pytest point of view there are just a lot of tests with some parameters. + +The general approach that is used to allow re-using results of test stages between test is the +following: + +- The tests are grouped such that the tests from one group have the same parameters from the list + of "parameters that define the test case" -- it means that the tests are grouped by the + "test cases" +- After that the tests are reordered such that + - the test from one group are executed sequentially one-by-one, without tests from other group + between tests in one group + - the test from one group are executed sequentially in the order defined for the test actions + beforehand; +- An instance of a special test case class is created once for each of the group of tests stated above + -- so, the instance of test case class is created for each "test case" described above. + +The instance of the special test case class (described in the last item of the list above) +is kept inside cache in test suite, it allows to use the results of the +previous tests of the same test case in the current test. + +### I.3. String names of tests + +Pytest allows running parametrized test methods in test classes. + +The test suite is made such that for each OpenVINO™ Training Extensions task (e.g. "object detection", "image classification", +etc) there is one test class with one test method with the name `test`, the method is parametrized +using special pytest tricks in the function `pytest_generate_tests` in the file `conftest.py` in the +folder `tests/`. + +(Note that "classical" way of parametrization of a class method is using pytest decorator +`@pytest.mark.parametrize`, but we do NOT use this way, since we need to regroup tests by test cases +-- see details in the previous section.) + +For each parametrized test method the pytest framework generates its name as follows: +`.[]` + +For the test suite the test names are generated in the same way (this is the inner part of pytest +that was not changed by us), but test suite generates the `parameters_string` part. + +Test suite generates the parameters string using + +1. the name of the test action (aka test stage) +2. the values of the test's parameters defining test behavior + (see the previous section "II. General description of test cases") +3. the usecase -- at the moment it is either "precommit" or "reallife" + +Note that in test suite the test parameters may have "short names" that are used during generation +of the test parameters strings. +Examples of test parameters short names + +- for parameter `model_name` -- `"model"` +- for parameter `dataset_name` -- `"dataset"` +- for parameter `num_training_iters` -- `"num_iters"` +- for parameter `batch_size` -- `"batch"` + +So, examples of test parameters strings are + +- `ACTION-training_evaluation,model-Custom_Object_Detection_Gen3_ATSS,dataset-bbcd,num_iters-CONFIG,batch-CONFIG,usecase-reallife` +- `ACTION-nncf_export_evaluation,model-Custom_Image_Classification_EfficinetNet-B0,dataset-lg_chem,num_epochs-CONFIG,batch-CONFIG,usecase-reallife` + +The test parameters strings are used in the test suite as test id-s. +Although the id-s are unique, they have a drawback -- they are quite long, since they contain all +the info to identify the test. + +## II. How To-s + +### II.1 How to add a new model+dataset pair to the training tests + +Let's there are implemented training tests for some OpenVINO™ Training Extensions algo backend, and we want to add +new model+dataset pair to the training test. + +In this case you should do as follows: + +1. Open the file with the training tests for the task type. + Typically it has name `test_otx_training.py` and it is placed in the folder + `external//tests/`. + +2. Find the class derived either from the class `OTXTestCreationParametersInterface` + or from the class `DefaultOTXTestCreationParametersInterface`. + There should be only one such class in the file, it should have name like + `ObjectDetectionTrainingTestParameters`. + +3. Find the method `test_bunches` in the class. + Most probably the method creates a variable `test_bunches` with a list of dicts, + and returns the deepcopy of the variable. + +4. Make change: add to the list a new element -- dict with the following keys + - `model_name` -- either a string with the model name or a list of strings with the model names, + the model names should be taken from the field `model_template_id` of the model template YAML + file + - `dataset_name` -- either a string with the dataset name or a list of strings with the dataset names, + we use known pre-defined names for the datasets on our CI. + The dataset names may be taken from the YAML file `dataset_definitions.yml` in the dataset server + of the CI. + (If you should add a new dataset -- please, upload your dataset in the proper folder to the + server and point the relative paths to the dataset parts to the file `dataset_definitions.yml` + in the folder) + Note that if `model_name` and/or `dataset_name` are lists, the test will be executed for + all possible pairs `(model, dataset)` from Cartesian product of the lists. + - `num_training_iters` or `max_num_epochs` or `patience` -- either integer, or a constant + `KEEP_CONFIG_FIELD_VALUE` to keep the value from the template, or just do not add (skip) the + key to use the default small value for the precommit tests (1 or 2) + - `batch_size` -- either integer, or a constant `KEEP_CONFIG_FIELD_VALUE` to keep the value from + the template, or just do not add (skip) the key to use the default small value for the + precommit tests (1 or 2) + - `usecase` -- either `REALLIFE_USECASE_CONSTANT` for reallife training tests or "precommit" for + precommit tests diff --git a/tests/test_suite/__init__.py b/tests/test_suite/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_suite/e2e_test_system.py b/tests/test_suite/e2e_test_system.py new file mode 100644 index 00000000000..a4976eba473 --- /dev/null +++ b/tests/test_suite/e2e_test_system.py @@ -0,0 +1,151 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +""" +The functions in the file generate pytest decorators +for integrating with e2e test system and the class DataCollector +that allows pushing information to the dashboard of e2e test system. + +If e2e test system is not installed, the generated pytest decorators do nothing, +and the DataCollector class is replaced with a stub that does nothing too. +""" + +import functools +import traceback + +import pytest + + +def _generate_e2e_pytest_decorators(): + try: + from e2e.markers.mark_meta import MarkMeta + except ImportError: + + def _e2e_pytest_api(func): + return func + + def _e2e_pytest_performance(func): + return func + + def _e2e_pytest_component(func): + return func + + def _e2e_pytest_unit(func): + return func + + return ( + _e2e_pytest_api, + _e2e_pytest_performance, + _e2e_pytest_component, + _e2e_pytest_unit, + ) + + class Requirements: + # Dummy requirement + REQ_DUMMY = "Dummy requirement" + + class OTXComponent(MarkMeta): + OTX = "otx" + + def _e2e_pytest_api(func): + @pytest.mark.components(OTXComponent.OTX) + @pytest.mark.priority_medium + @pytest.mark.reqids(Requirements.REQ_DUMMY) + @pytest.mark.api_other + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + def _e2e_pytest_performance(func): + @pytest.mark.components(OTXComponent.OTX) + @pytest.mark.priority_medium + @pytest.mark.reqids(Requirements.REQ_DUMMY) + @pytest.mark.api_performance + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + def _e2e_pytest_component(func): + @pytest.mark.components(OTXComponent.OTX) + @pytest.mark.priority_medium + @pytest.mark.reqids(Requirements.REQ_DUMMY) + @pytest.mark.component + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + def _e2e_pytest_unit(func): + @pytest.mark.components(OTXComponent.OTX) + @pytest.mark.priority_medium + @pytest.mark.reqids(Requirements.REQ_DUMMY) + @pytest.mark.unit + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + return ( + _e2e_pytest_api, + _e2e_pytest_performance, + _e2e_pytest_component, + _e2e_pytest_unit, + ) + + +def _create_class_DataCollector(): + try: + from e2e.collection_system.systems import TinySystem + + return TinySystem + except ImportError: + + class _dummy_DataCollector: # should have the same interface as TinySystem + def __init__(self, *args, **kwargs): + pass + + def flush(self): + pass + + def register_collector(self, *args, **kwargs): + pass + + def register_exporter(self, *args, **kwargs): + pass + + def log_final_metric(self, *args, **kwargs): + pass + + def log_internal_metric(self, *args, **kwargs): + pass + + def update_metadata(self, *args, **kwargs): + pass + + def __enter__(self): + return self + + def __exit__(self, typerr, value, tback): + if typerr is not None: + traceback.format_tb(tback) + raise typerr(value) + return True + + return _dummy_DataCollector + + +( + e2e_pytest_api, + e2e_pytest_performance, + e2e_pytest_component, + e2e_pytest_unit, +) = _generate_e2e_pytest_decorators() +DataCollector = _create_class_DataCollector() diff --git a/tests/test_suite/fixtures.py b/tests/test_suite/fixtures.py new file mode 100644 index 00000000000..60d8fc3a2af --- /dev/null +++ b/tests/test_suite/fixtures.py @@ -0,0 +1,395 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +""" +The file contains fixtures that may be used in algo backend's +reallife training tests. + +Note that the fixtures otx_templates_root_dir_fx and otx_test_domain_fx +MUST be overriden in algo backend's conftest.py file. +""" + +# pylint: disable=redefined-outer-name + +import glob +import os +import os.path as osp +from copy import deepcopy +from pprint import pformat +from typing import Callable, Dict, Optional + +import pytest +import yaml + +from otx.api.entities.model_template import parse_model_template + +from .e2e_test_system import DataCollector +from .logging import get_logger, set_log_level +from .training_tests_common import REALLIFE_USECASE_CONSTANT, ROOT_PATH_KEY + +logger = get_logger() + +######################################################################################### +# Fixtures that should be overriden in algo backends + + +@pytest.fixture(scope="session") +def otx_templates_root_dir_fx(): + """ + The fixture returns an absolute path to the folder where (in the subfolders) + the reallife training tests will look OTX template files (the files 'template.yaml'). + + The fixture MUST be overriden in algo backend's conftest.py file. + """ + raise NotImplementedError("The fixture otx_templates_root_dir_fx should be overriden in algo backend") + + +@pytest.fixture +def otx_reference_root_dir_fx(): + """ + The fixture returns an absolute path to the folder where reference files + for OTX models are stored. + """ + raise NotImplementedError("The fixture otx_reference_root_dir_fx should be overriden in algo backend") + + +@pytest.fixture +def otx_current_reference_dir_fx(otx_reference_root_dir_fx, current_test_parameters_fx): + """ + The fixture returns an absolute path to the folder where reference files + for the current model are stored. + """ + if otx_reference_root_dir_fx is None: + return None + path = os.path.join(otx_reference_root_dir_fx, current_test_parameters_fx["model_name"]) + if not os.path.isdir(path): + return None + return path + + +@pytest.fixture +def otx_test_domain_fx(): + """ + The fixture returns a string that will be used as the 'subject' field in the + e2e test system dashboard. + At the moment it is supposed that the fixture should return something like + 'custom-object-detection'. + + The fixture MUST be overriden in algo backend's conftest.py file. + """ + raise NotImplementedError("The fixture otx_test_domain_fx should be overriden in algo backend") + + +@pytest.fixture +def otx_test_scenario_fx(): + """ + The fixture returns a string that will be used as the 'scenario' field in the + e2e test system dashboard. + At the moment it is supposed that the fixture should return something like + 'api' or 'integration' or 'reallife'. + + The fixture may be overriden in algo backend's conftest.py file. + """ + return "api" + + +# +######################################################################################### + + +@pytest.fixture +def dataset_definitions_fx(request): + """ + Return dataset definitions read from a YAML file passed as the parameter --dataset-definitions. + + Note that the dataset definitions should store the following structure: + { + : { ...... }, + : { ...... }, + ... + } + The elements describing datasets could have arbitrary structure, the + structure is defined by the functions parsing dataset in the algo backends. + + An example for mmdetection algo backend: + { + : { + 'annotations_train': + 'images_train_dir': + 'annotations_val': + 'images_val_dir': + 'annotations_test': + 'images_test_dir': + } + } + + Also one more key with value ROOT_PATH_KEY is added -- it is the path to + the folder where the dataset definitions file is placed, this path will be + used to resolve relative paths in the dataset structures. + """ + path = request.config.getoption("--dataset-definitions") + if path is None: + logger.warning( + f"The command line parameter '--dataset-definitions' is not set" + f"whereas it is required for the test {request.node.originalname or request.node.name}" + f" -- ALL THE TESTS THAT REQUIRE THIS PARAMETER ARE SKIPPED" + ) + return None + with open(path, encoding="utf-8") as f: + data = yaml.safe_load(f) + data[ROOT_PATH_KEY] = osp.dirname(path) + return data + + +@pytest.fixture(scope="session") +def template_paths_fx(otx_templates_root_dir_fx): + """ + Return mapping model names to template paths, received from globbing the + folder pointed by the fixture otx_templates_root_dir_fx. + Note that the function searches files with name `template.yaml`, and for each such file + the model name is the name of the parent folder of the file. + """ + root = otx_templates_root_dir_fx + assert osp.isabs(root), f"Error: otx_templates_root_dir_fx is not an absolute path: {root}" + template_glob = glob.glob(f"{root}/**/template*.yaml", recursive=True) + data = {} + for cur_path in template_glob: + assert osp.isabs(cur_path), f"Error: not absolute path {cur_path}" + name = parse_model_template(cur_path).model_template_id + if name in data: + raise RuntimeError(f"Duplication of names in {root} folder: {data[name]} and {cur_path}") + assert name != ROOT_PATH_KEY, f"Wrong model name {name}" + data[name] = cur_path + data[ROOT_PATH_KEY] = "" + return data + + +@pytest.fixture +def expected_metrics_all_tests_fx(request): + # pylint: disable=line-too-long + """ + Return expected metrics for reallife tests read from a YAML file passed as the parameter --expected-metrics-file. + Note that the structure of expected metrics should be a dict that maps tests to the expected metric numbers. + The keys of the dict are the parameters' part of the test id-s -- see the function + OTXTestHelper._generate_test_id, also see the fixture current_test_parameters_string_fx below. + + The value for each key is a structure that stores a requirement on some metric. + The requirement can be either a target value (probably, with max size of quality drop) + or the reference to another stage of the same model (also probably with max size of quality drop). + See details in the description of the fixture cur_test_expected_metrics_callback_fx below. + E.g. + ``` + 'ACTION-training_evaluation,model-gen3_mobilenetV2_ATSS,dataset-bbcd,num_iters-KEEP_CONFIG_FIELD_VALUE,batch-KEEP_CONFIG_FIELD_VALUE,usecase-reallife': + 'metrics.accuracy.f-measure': + 'target_value': 0.81 + 'max_diff': 0.005 + 'ACTION-export_evaluation,model-gen3_mobilenetV2_ATSS,dataset-bbcd,num_iters-KEEP_CONFIG_FIELD_VALUE,batch-KEEP_CONFIG_FIELD_VALUE,usecase-reallife': + 'metrics.accuracy.f-measure': + 'base': 'training_evaluation.metrics.accuracy.f-measure' + 'max_diff': 0.01 + ``` + """ + path = request.config.getoption("--expected-metrics-file") + if path is None: + logger.warning( + "The command line parameter '--expected-metrics-file' is not set" + "whereas it is required to compare with target metrics" + " -- ALL THE COOTXRISON WITH TARGET METRICS IN TESTS WILL BE FAILED" + ) + return None + with open(path, encoding="utf-8") as f: + expected_metrics_all_tests = yaml.safe_load(f) + assert isinstance(expected_metrics_all_tests, dict), f"Wrong metrics file {path}: {expected_metrics_all_tests}" + return expected_metrics_all_tests + + +@pytest.fixture(scope="session", autouse=True) +def force_logging_session_fx(request): + """ + This fixture force setting log level for test suite. + It may be required in the case when one of the packages + sets global log level to logging.ERROR. + This fixture has session scope. + """ + level = request.config.getoption("--force-log-level") + recursive_level = request.config.getoption("--force-log-level-recursive") + if recursive_level is not None: + set_log_level(recursive_level, recursive=True) + if level is not None: + set_log_level(level) + + +@pytest.fixture +def force_logging_fx(request): + """ + This fixture force setting log level for test suite. + It may be required in the case when one of the packages + sets global log level to logging.ERROR. + Note that using --force-log-level-recursive option + it is possible to set log level for all parents of the test + suite logger. + This fixture has function scope -- it may be required if some + of packages changes log level of some loggers during work of test. + """ + level = request.config.getoption("--force-log-level") + recursive_level = request.config.getoption("--force-log-level-recursive") + if recursive_level is not None: + set_log_level(recursive_level, recursive=True) + if level is not None: + set_log_level(level) + + +@pytest.fixture +def current_test_parameters_fx(request, force_logging_fx): + # pylint: disable=unused-argument + """ + This fixture returns the test parameter `test_parameters` of the current test. + """ + cur_test_params = deepcopy(request.node.callspec.params) + assert "test_parameters" in cur_test_params, ( + f"The test {request.node.name} should be parametrized " f"by parameter 'test_parameters'" + ) + return cur_test_params["test_parameters"] + + +@pytest.fixture +def current_test_parameters_string_fx(request, force_logging_fx): + # pylint: disable=unused-argument + """ + This fixture returns the part of the test id between square brackets + (i.e. the part of id that corresponds to the test parameters) + """ + node_name = request.node.name + assert "[" in node_name, f"Wrong format of node name {node_name}" + assert node_name.endswith("]"), f"Wrong format of node name {node_name}" + index = node_name.find("[") + return node_name[index + 1 : -1] + + +# TODO(lbeynens): replace 'callback' with 'factory' +@pytest.fixture +def cur_test_expected_metrics_callback_fx( + expected_metrics_all_tests_fx, + current_test_parameters_string_fx, + current_test_parameters_fx, +) -> Optional[Callable[[], Dict]]: + """ + This fixture returns + * either a callback -- a function without parameters that returns + expected metrics for the current test, + * or None if the test validation should be skipped. + + The expected metrics for a test is a dict with the structure that stores the + requirements on metrics on the current test. In this dict + * each key is a dot-separated metric "address" in the structure received as the result of the test + * each value is a structure describing a requirement for this metric + e.g. + ``` + { + 'metrics.accuracy.f-measure': { + 'target_value': 0.81, + 'max_diff': 0.005 + } + } + ``` + + Note that the fixture returns a callback instead of returning the expected metrics structure + themselves, to avoid attempts to read expected metrics for the stages that do not make validation + at all -- now the callback is called if and only if validation is made for the stage. + (E.g. the stage 'export' does not make validation, but the stage 'export_evaluation' does.) + + Also note that if the callback is called, but the expected metrics for the current test + are not found in the structure with expected metrics for all tests, then the callback + raises exception ValueError to fail the test. + + And also note that each requirement for each metric is a dict with the following structure: + * The dict points a target value of the metric. + The target_value may be pointed + ** either by key 'target_value' (in this case the value is float), + ** or by the key 'base', in this case the value is a dot-separated address to another value in the + storage of previous stages' results, e.g. + 'base': 'training_evaluation.metrics.accuracy.f-measure' + + * The dict points a range of acceptable values for the metric. + The range for the metric values may be pointed + ** either by key 'max_diff' (with float value), + in this case the acceptable range will be + [target_value - max_diff, target_value + max_diff] + (inclusively). + + ** or the range may be pointed by keys 'max_diff_if_less_threshold' and/or 'max_diff_if_greater_threshold' + (with float values), in this case the acceptable range is + `[target_value - max_diff_if_less_threshold, target_value + max_diff_if_greater_threshold]` + (also inclusively). + This allows to point non-symmetric ranges w.r.t. the target_value. + One of 'max_diff_if_less_threshold' or 'max_diff_if_greater_threshold' may be absent, in this case + it is set to `+infinity`, so the range will be half-bounded. + E.g. if `max_diff_if_greater_threshold` is absent, the range will be + [target_value - max_diff_if_less_threshold, +infinity] + """ + if REALLIFE_USECASE_CONSTANT != current_test_parameters_fx["usecase"]: + return None + + # make a copy to avoid later changes in the structs + expected_metrics_all_tests = deepcopy(expected_metrics_all_tests_fx) + current_test_parameters_string = deepcopy(current_test_parameters_string_fx) + + def _get_expected_metrics_callback(): + if expected_metrics_all_tests is None: + raise ValueError( + f"The dict with expected metrics cannot be read, although it is required " + f"for validation in the test '{current_test_parameters_string}'" + ) + if current_test_parameters_string not in expected_metrics_all_tests: + raise ValueError( + f"The parameters id string {current_test_parameters_string} is not inside " + f"the dict with expected metrics -- cannot make validation, so test is failed" + ) + expected_metrics = expected_metrics_all_tests[current_test_parameters_string] + if not isinstance(expected_metrics, dict): + raise ValueError( + f"The expected metric for parameters id string {current_test_parameters_string} " + f"should be a dict, whereas it is: {pformat(expected_metrics)}" + ) + return expected_metrics + + return _get_expected_metrics_callback + + +@pytest.fixture +def data_collector_fx(request, otx_test_scenario_fx, otx_test_domain_fx) -> DataCollector: + """ + The fixture returns the DataCollector instance that may be used to pass + the values (metrics, intermediate results, etc) to the e2e test system dashboard. + Please, see the interface of DataCollector class in the function + e2e_test_system._create_class_DataCollector + (the function creates a stub class with the proper interface if e2e test system is not installed). + + Note that the fixture contains both setup and teardown parts using yield from fixture. + Each test uses its own instance of DataCollector class, so each test will create its own row in the + dashboard of e2e test system. + """ + setup = deepcopy(request.node.callspec.params) + setup["environment_name"] = os.environ.get("TT_ENVIRONMENT_NAME", "no-env") + setup["test_type"] = os.environ.get("TT_TEST_TYPE", "no-test-type") # TODO: get from e2e test type + setup["scenario"] = otx_test_scenario_fx + setup["test"] = request.node.name + setup["subject"] = otx_test_domain_fx + setup["project"] = "otx" + if "test_parameters" in setup: + assert isinstance(setup["test_parameters"], dict) + if "dataset_name" not in setup: + setup["dataset_name"] = setup["test_parameters"].get("dataset_name") + if "model_name" not in setup: + setup["model_name"] = setup["test_parameters"].get("model_name") + if "test_stage" not in setup: + setup["test_stage"] = setup["test_parameters"].get("test_stage") + if "usecase" not in setup: + setup["usecase"] = setup["test_parameters"].get("usecase") + logger.info(f"creating DataCollector: setup=\n{pformat(setup, width=140)}") + data_collector = DataCollector(name="TestOTXIntegration", setup=setup) + with data_collector: + logger.info("data_collector is created") + yield data_collector + logger.info("data_collector is released") diff --git a/tests/test_suite/logging.py b/tests/test_suite/logging.py new file mode 100644 index 00000000000..6ad38b402d8 --- /dev/null +++ b/tests/test_suite/logging.py @@ -0,0 +1,35 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +""" +The module with common logger for all OTX training tests. +""" + +import logging + + +def get_logger(): + """ + The function returns the common logger for all OTX + training tests. + """ + logger_name = ".".join(__name__.split(".")[:-1]) + return logging.getLogger(logger_name) + + +def set_log_level(level, recursive=False): + """ + The function sets log level for the common logger for all + OTX training tests. + The parameter `level` may be either int code or string + (e.g. 'DEBUG') + """ + + logger = get_logger() + logger.setLevel(level) + if not recursive: + return + while logger: + logger.setLevel(level) + logger = logger.parent diff --git a/tests/test_suite/pytest_insertions.py b/tests/test_suite/pytest_insertions.py new file mode 100644 index 00000000000..b88060c8763 --- /dev/null +++ b/tests/test_suite/pytest_insertions.py @@ -0,0 +1,140 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +""" +This file contains functions that may be used in the conftest file of algo +backend and in the standard pytest hooks: +* pytest_addoption +* pytest_generate_tests +to add to pytest functionality required for algo backends reallife training tests. +""" + +try: + import e2e.fixtures + from e2e import config # noqa + from e2e.conftest_utils import * # noqa + from e2e.conftest_utils import pytest_addoption as _e2e_pytest_addoption # noqa + from e2e.utils import get_plugins_from_packages + + _pytest_plugins_from_e2e = get_plugins_from_packages([e2e]) +except ImportError: + _e2e_pytest_addoption = None + _pytest_plugins_from_e2e = [] + + +def get_pytest_plugins_from_otx(): + """ + The function generates pytest_plugins variable that should be used + in an algo backend' conftest.py file. + """ + import tests.test_suite.fixtures # noqa + + pytest_plugins_from_otx_api = ["tests.test_suite.fixtures"] + pytest_plugins = list(_pytest_plugins_from_e2e) + pytest_plugins_from_otx_api + return pytest_plugins + + +def otx_pytest_addoption_insertion(parser): + """ + The function should be called in the standard pytest hook pytest_addoption + to add the options required for reallife training tests. + """ + if _e2e_pytest_addoption: + _e2e_pytest_addoption(parser) + + parser.addoption( + "--dataset-definitions", + action="store", + default=None, + help="Path to the dataset_definitions.yml file for tests that require datasets.", + ) + parser.addoption( + "--test-usecase", + action="store", + default=None, + help="Optional. If the parameter is set, it filters test_otx_training tests by usecase field.", + ) + parser.addoption( + "--expected-metrics-file", + action="store", + default=None, + help="Optional. If the parameter is set, it points the YAML file with expected test metrics.", + ) + parser.addoption( + "--force-log-level", + action="store", + default=None, + help="Optional. If the parameter is set, the logger in each test is forced to this level.", + ) + parser.addoption( + "--force-log-level-recursive", + action="store", + default=None, + help="Optional. If the parameter is set, the logger in each test and its parents " "are forced to this level.", + ) + + # TODO(lbeynens): remove it after update CI + parser.addoption( + "--template-paths", + action="store", + default=None, + help="Obsolete parameter. Should be removed when CI is changed.", + ) + + parser.addoption( + "--test-workspace", + type=str, + default=None, + help="OTX test requires a certain amount of storage in the test work directory. " + "If you don't have enough space on the drive where the default path is located (e.g. /tmp on linux), " + "you can use this option to change the test work directory path to a different drive.", + ) + + +def otx_pytest_generate_tests_insertion(metafunc): + """ + The function should be called in the standard pytest hook pytest_generate_tests + in algo backend's conftest.py file to generate parameters of reallife training tests. + """ + from .logging import get_logger + from .training_tests_helper import OTXTrainingTestInterface + + logger = get_logger() + if metafunc.cls is None: + return False + if not issubclass(metafunc.cls, OTXTrainingTestInterface): + return False + + logger.debug(f"otx_pytest_generate_tests_insertion: begin handling {metafunc.cls}") + + # It allows to filter by usecase + usecase = metafunc.config.getoption("--test-usecase") + + argnames, argvalues, ids = metafunc.cls.get_list_of_tests(usecase) + + assert isinstance(argnames, (list, tuple)) + assert "test_parameters" in argnames + assert isinstance(argvalues, list) + assert isinstance(ids, list) + assert len(argvalues) == len(ids) + assert all(isinstance(v, str) for v in ids) + + metafunc.parametrize(argnames, argvalues, ids=ids, scope="class") + logger.debug(f"otx_pytest_generate_tests_insertion: end handling {metafunc.cls}") + return True + + +def otx_conftest_insertion(*, default_repository_name=""): + """ + The function should be called in an algo backend's conftest.py file + to set default repository name in e2e- test system. + """ + try: + import os + + from e2e import config as config_e2e + + config_e2e.repository_name = os.environ.get("TT_REPOSITORY_NAME", default_repository_name) + except ImportError: + pass diff --git a/tests/test_suite/run_test_command.py b/tests/test_suite/run_test_command.py new file mode 100644 index 00000000000..cef1a5cca56 --- /dev/null +++ b/tests/test_suite/run_test_command.py @@ -0,0 +1,1272 @@ +"""Common test case and helpers for OTX""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import asyncio +import json +import os +import shutil +import sys +from pathlib import Path +from typing import Dict, Union + +import onnx +import onnxruntime +import pytest +import torch +import yaml + +from otx.api.entities.model_template import ModelCategory, ModelStatus +from otx.cli.tools.find import SUPPORTED_BACKBONE_BACKENDS as find_supported_backends +from otx.cli.tools.find import SUPPORTED_TASKS as find_supported_tasks +from otx.cli.utils.nncf import get_number_of_fakequantizers_in_xml +from tests.test_suite.e2e_test_system import e2e_pytest_component + +try: + import intel_extension_for_pytorch +except ImportError: + pass + + +def get_template_rel_dir(template): + return os.path.dirname(os.path.relpath(template.model_template_path)) + + +def get_template_dir(template, root) -> str: + + # Get the template directory of the algorithm. + # The location of the template files are as follows: + # ~/training_extensions/src/otx/algorithms//**/template.yaml + # To get the ``algorithm``, index of the "algorithms" can be + # searched, where ``algorithm`` comes next. + template_path_parts = template.model_template_path.split(os.sep) + idx = template_path_parts.index("algorithms") + algorithm = template_path_parts[idx + 1] + + algo_backend_dir = f"src/otx/algorithms/{algorithm}" + work_dir = os.path.join(root, f"src/otx/algorithms/{algorithm}") + template_dir = os.path.dirname(os.path.relpath(template.model_template_path, start=algo_backend_dir)) + template_work_dir = os.path.join(work_dir, template_dir) + + os.makedirs(template_work_dir, exist_ok=True) + return template_work_dir + + +def runner( + cmd, + stdout_stream=sys.stdout.buffer, + stderr_stream=sys.stderr.buffer, + **kwargs, +): + async def stream_handler(in_stream, out_stream): + output = bytearray() + # buffer line + line = bytearray() + while True: + c = await in_stream.read(1) + if not c: + break + line.extend(c) + if c == b"\n": + out_stream.write(line) + output.extend(line) + line = bytearray() + return output + + async def run_and_capture(cmd): + environ = os.environ.copy() + environ["PYTHONUNBUFFERED"] = "1" + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=environ, + **kwargs, + ) + + try: + stdout, stderr = await asyncio.gather( + stream_handler(process.stdout, stdout_stream), + stream_handler(process.stderr, stderr_stream), + ) + except Exception: + process.kill() + raise + finally: + rc = await process.wait() + return rc, stdout, stderr + + rc, stdout, stderr = asyncio.run(run_and_capture(cmd)) + + return rc, stdout, stderr + + +def check_run(cmd, **kwargs): + rc, _, stderr = runner(cmd, **kwargs) + sys.stdout.flush() + sys.stderr.flush() + + if rc != 0: + stderr = stderr.decode("utf-8").splitlines() + i = 0 + for i, line in enumerate(stderr): + if line.startswith("Traceback"): + break + stderr = "\n".join(stderr[i:]) + assert rc == 0, stderr + + +def otx_train_testing(template, root, otx_dir, args, deterministic=True): + template_work_dir = get_template_dir(template, root) + command_line = ["otx", "train", template.model_template_path] + for arg in [ + "--train-ann_file", + "--train-data-roots", + "--val-ann-file", + "--val-data-roots", + "--unlabeled-data-roots", + "--unlabeled-file-list", + ]: + arg_value = args.get(arg, None) + if arg_value: + command_line.extend([arg, os.path.join(otx_dir, arg_value)]) + command_line.extend(["--output", f"{template_work_dir}/trained_{template.model_template_id}"]) + command_line.extend(["--workspace", f"{template_work_dir}"]) + if "--load-weights" in args: + if not os.path.exists(args["--load-weights"]): + pytest.skip(reason=f"required file is not exist - {args['--load-weights']}") + command_line.extend(["--load-weights", args["--load-weights"]]) + if "--gpus" in args: + command_line.extend(["--gpus", args["--gpus"]]) + if "--multi-gpu-port" in args: + command_line.extend(["--multi-gpu-port", args["--multi-gpu-port"]]) + if "--train-type" in args: + command_line.extend(["--train-type", args["--train-type"]]) + if deterministic: + command_line.extend(["--deterministic"]) + if "train_params" in args: + command_line.extend(args["train_params"]) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth") + assert os.path.exists(f"{template_work_dir}/trained_{template.model_template_id}/models/label_schema.json") + + +def otx_resume_testing(template, root, otx_dir, args): + template_work_dir = get_template_dir(template, root) + command_line = [ + "otx", + "train", + template.model_template_path, + ] + for option in [ + "--train-ann-file", + "--train-data-roots", + "--val-ann-file", + "--val-data-roots", + "--unlabeled-data-roots", + "--unlabeled-file-list", + "--resume-from", + ]: + if option in args: + command_line.extend([option, f"{os.path.join(otx_dir, args[option])}"]) + + if "--resume-from" in args: + if not os.path.exists(args["--resume-from"]): + pytest.skip(reason=f"required file is not exist - {args['--resume-from']}") + + command_line.extend(["--output", f"{template_work_dir}/trained_for_resume_{template.model_template_id}"]) + command_line.extend(["--workspace", f"{template_work_dir}"]) + command_line.extend(args["train_params"]) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth") + assert os.path.exists( + f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/label_schema.json" + ) + + +def otx_hpo_testing(template, root, otx_dir, args): + template_work_dir = get_template_dir(template, root) + if os.path.exists(f"{template_work_dir}/hpo"): + shutil.rmtree(f"{template_work_dir}/hpo") + + command_line = ["otx", "train", template.model_template_path] + + for arg in ["--train-data-roots", "--val-data-roots"]: + arg_value = args.get(arg, None) + if arg_value: + command_line.extend([arg, os.path.join(otx_dir, arg_value)]) + command_line.extend(["--output", f"{template_work_dir}/hpo_trained_{template.model_template_id}"]) + command_line.extend(["--workspace", f"{template_work_dir}"]) + command_line.extend(["--enable-hpo", "--hpo-time-ratio", "1"]) + + command_line.extend(args["train_params"]) + check_run(command_line) + trials_json = list( + filter( + lambda x: x.name.split(".")[0].isnumeric(), + Path(f"{template_work_dir}/hpo_trained_{template.model_template_id}/hpo/").rglob("*.json"), + ) + ) + assert trials_json + for trial_json in trials_json: + with trial_json.open("r") as f: + trial_result = json.load(f) + assert trial_result.get("score") + + assert os.path.exists(f"{template_work_dir}/hpo_trained_{template.model_template_id}/models/weights.pth") + assert os.path.exists(f"{template_work_dir}/hpo_trained_{template.model_template_id}/models/label_schema.json") + + +def otx_export_testing(template, root, dump_features=False, half_precision=False, check_ir_meta=False, is_onnx=False): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + save_path = f"{template_work_dir}/exported_{template.model_template_id}" + command_line = [ + "otx", + "export", + template.model_template_path, + "--load-weights", + weights_path, + "--output", + save_path, + ] + + if dump_features: + command_line[-1] += "_w_features" + save_path = command_line[-1] + command_line.append("--dump-features") + if half_precision: + command_line[-1] += "_fp16" + save_path = command_line[-1] + command_line.append("--half-precision") + if is_onnx: + command_line.extend(["--export-type", "onnx"]) + + check_run(command_line) + + path_to_xml = os.path.join(save_path, "openvino.xml") + assert os.path.exists(os.path.join(save_path, "label_schema.json")) + if not is_onnx: + if any(map(lambda x: x in template.model_template_id, ("Visual_Prompting", "Zero_Shot"))): + path_to_xml = os.path.join(save_path, "visual_prompting_decoder.xml") + assert os.path.exists(os.path.join(save_path, "visual_prompting_image_encoder.xml")) + assert os.path.exists(os.path.join(save_path, "visual_prompting_image_encoder.bin")) + assert os.path.exists(os.path.join(save_path, "visual_prompting_decoder.xml")) + assert os.path.exists(os.path.join(save_path, "visual_prompting_decoder.bin")) + else: + assert os.path.exists(path_to_xml) + assert os.path.exists(os.path.join(save_path, "openvino.bin")) + ckpt = torch.load(f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth") + input_size = ckpt.get("input_size", None) + if input_size: + with open(path_to_xml, encoding="utf-8") as xml_stream: + xml_model = xml_stream.read() + assert f"{input_size[1]},{input_size[0]}" in xml_model + else: + if any(map(lambda x: x in template.model_template_id, ("Visual_Prompting", "Zero_Shot"))): + assert os.path.exists(os.path.join(save_path, "visual_prompting_image_encoder.onnx")) + assert os.path.exists(os.path.join(save_path, "visual_prompting_decoder.onnx")) + else: + path_to_onnx = os.path.join(save_path, "model.onnx") + assert os.path.exists(path_to_onnx) + + if check_ir_meta: + onnx_model = onnx.load(path_to_onnx) + is_model_type_presented = False + for prop in onnx_model.metadata_props: + assert "model_info" in prop.key + if "model_type" in prop.key: + is_model_type_presented = True + assert is_model_type_presented + + # In case of tile classifier mmdeploy inserts mark nodes in onnx, making it non-standard + if not os.path.exists(os.path.join(save_path, "tile_classifier.onnx")): + onnx.checker.check_model(path_to_onnx) + onnxruntime.InferenceSession(path_to_onnx) + return + + if dump_features: + with open(path_to_xml, encoding="utf-8") as stream: + xml_model = stream.read() + assert "feature_vector" in xml_model + + if half_precision: + with open(path_to_xml, encoding="utf-8") as stream: + xml_model = stream.read() + assert "FP16" in xml_model + + if check_ir_meta: + with open(path_to_xml, encoding="utf-8") as stream: + xml_model = stream.read() + assert "model_info" in xml_model + assert "model_type" in xml_model + assert "labels" in xml_model + + +def otx_eval_testing(template, root, otx_dir, args): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--load-weights", + weights_path, + "--output", + f"{template_work_dir}/trained_{template.model_template_id}", + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + command_line.extend(args.get("eval_params", [])) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/trained_{template.model_template_id}/performance.json") + + +def otx_eval_openvino_testing( + template, + root, + otx_dir, + args, + threshold=0.0, + half_precision=False, + is_visual_prompting=False, +): + template_work_dir = get_template_dir(template, root) + weights_file = "visual_prompting_decoder" if is_visual_prompting else "openvino" + weights_path = f"{template_work_dir}/exported_{template.model_template_id}/{weights_file}.xml" + output_path = f"{template_work_dir}/exported_{template.model_template_id}" + perf_path = f"{template_work_dir}/exported_{template.model_template_id}/performance.json" + + if half_precision: + weights_path = f"{template_work_dir}/exported_{template.model_template_id}_fp16/{weights_file}.xml" + output_path = f"{template_work_dir}/exported_{template.model_template_id}_fp16" + perf_path = f"{template_work_dir}/exported_{template.model_template_id}_fp16/performance.json" + + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--load-weights", + weights_path, + "--output", + output_path, + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + check_run(command_line) + assert os.path.exists(perf_path) + with open(f"{template_work_dir}/trained_{template.model_template_id}/performance.json") as read_file: + trained_performance = json.load(read_file) + with open(perf_path) as read_file: + exported_performance = json.load(read_file) + + compare_model_accuracy(exported_performance, trained_performance, threshold) + + +def otx_demo_testing(template, root, otx_dir, args): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "demo", + template.model_template_path, + "--load-weights", + weights_path, + "--input", + os.path.join(otx_dir, args["--input"]), + "--delay", + "-1", + "--output", + os.path.join(template_work_dir, "output"), + ] + check_run(command_line) + assert os.path.exists(os.path.join(template_work_dir, "output")) + + +def otx_demo_openvino_testing(template, root, otx_dir, args): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/exported_{template.model_template_id}/openvino.xml" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "demo", + template.model_template_path, + "--load-weights", + weights_path, + "--input", + os.path.join(otx_dir, args["--input"]), + "--delay", + "-1", + "--output", + os.path.join(template_work_dir, "output"), + ] + check_run(command_line) + assert os.path.exists(os.path.join(template_work_dir, "output")) + + +def otx_deploy_openvino_testing(template, root, otx_dir, args): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/exported_{template.model_template_id}/openvino.xml" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + deployment_dir = f"{template_work_dir}/deployed_{template.model_template_id}" + command_line = [ + "otx", + "deploy", + template.model_template_path, + "--load-weights", + weights_path, + "--output", + deployment_dir, + ] + check_run(command_line) + check_run(["unzip", "-o", "openvino.zip"], cwd=deployment_dir) + # TODO: Need to check Requirements.txt & new environment is working + # check_run( + # ["python3", "-m", "venv", "venv"], + # cwd=os.path.join(deployment_dir, "python"), + # ) + # check_run( + # ["python3", "-m", "pip", "install", "wheel"], + # cwd=os.path.join(deployment_dir, "python"), + # ) + # check_run( + # ["python3", "-m", "pip", "install", "pip", "--upgrade"], + # cwd=os.path.join(deployment_dir, "python"), + # ) + # check_run( + # ["python3", "-m", "pip", "install", "torch>=1.8.1, <=1.9.1"], + # cwd=os.path.join(deployment_dir, "python"), + # ) + # check_run( + # [ + # "python3", + # "-m", + # "pip", + # "install", + # "-r", + # os.path.join(deployment_dir, "python", "requirements.txt"), + # ], + # cwd=os.path.join(deployment_dir, "python"), + # ) + check_run( + [ + "python3", + "demo.py", + "-m", + "../model", + "-i", + os.path.join(otx_dir, args["--input"]), + "--inference_type", + "sync", + "--no_show", + "--output", + os.path.join(deployment_dir, "output"), + ], + cwd=os.path.join(deployment_dir, "python"), + ) + assert os.path.exists(os.path.join(deployment_dir, "output")) + + check_run( + [ + "python3", + "demo.py", + "-m", + "../model", + "-i", + os.path.join(otx_dir, args["--input"]), + "--inference_type", + "async", + "--no_show", + "--output", + os.path.join(deployment_dir, "output"), + ], + cwd=os.path.join(deployment_dir, "python"), + ) + assert os.path.exists(os.path.join(deployment_dir, "output")) + + +def otx_eval_deployment_testing(template, root, otx_dir, args, threshold=0.0): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/deployed_{template.model_template_id}/openvino.zip" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--load-weights", + weights_path, + "--output", + f"{template_work_dir}/deployed_{template.model_template_id}", + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/deployed_{template.model_template_id}/performance.json") + with open(f"{template_work_dir}/exported_{template.model_template_id}/performance.json") as read_file: + exported_performance = json.load(read_file) + with open(f"{template_work_dir}/deployed_{template.model_template_id}/performance.json") as read_file: + deployed_performance = json.load(read_file) + + compare_model_accuracy(deployed_performance, deployed_performance, threshold) + + +def otx_demo_deployment_testing(template, root, otx_dir, args): + template_work_dir = get_template_dir(template, root) + deployment_dir = f"{template_work_dir}/deployed_{template.model_template_id}" + + weights_path = f"{deployment_dir}/openvino.zip" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "demo", + template.model_template_path, + "--load-weights", + weights_path, + "--input", + os.path.join(otx_dir, args["--input"]), + "--delay", + "-1", + "--output", + os.path.join(deployment_dir, "output"), + ] + check_run(command_line) + assert os.path.exists(os.path.join(deployment_dir, "output")) + + +def ptq_optimize_testing(template, root, otx_dir, args, is_visual_prompting=False): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/exported_{template.model_template_id}/openvino.xml" + if is_visual_prompting: + weights_path = f"{template_work_dir}/exported_{template.model_template_id}/visual_prompting_decoder.xml" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "optimize", + template.model_template_path, + "--train-data-roots", + f'{os.path.join(otx_dir, args["--train-data-roots"])}', + "--val-data-roots", + f'{os.path.join(otx_dir, args["--val-data-roots"])}', + "--output", + f"{template_work_dir}/ptq_{template.model_template_id}", + "--load-weights", + weights_path, + ] + + command_line.extend(["--workspace", f"{template_work_dir}"]) + check_run(command_line) + if is_visual_prompting: + assert os.path.exists( + f"{template_work_dir}/ptq_{template.model_template_id}/visual_prompting_image_encoder.xml" + ) + assert os.path.exists( + f"{template_work_dir}/ptq_{template.model_template_id}/visual_prompting_image_encoder.bin" + ) + assert os.path.exists(f"{template_work_dir}/ptq_{template.model_template_id}/visual_prompting_decoder.xml") + assert os.path.exists(f"{template_work_dir}/ptq_{template.model_template_id}/visual_prompting_decoder.bin") + else: + assert os.path.exists(f"{template_work_dir}/ptq_{template.model_template_id}/openvino.xml") + assert os.path.exists(f"{template_work_dir}/ptq_{template.model_template_id}/openvino.bin") + assert os.path.exists(f"{template_work_dir}/ptq_{template.model_template_id}/label_schema.json") + + +def _validate_fq_in_xml(xml_path, path_to_ref_data, compression_type, test_name, update=False): + num_fq = get_number_of_fakequantizers_in_xml(xml_path) + assert os.path.exists(path_to_ref_data), f"Reference file does not exist: {path_to_ref_data} [num_fq = {num_fq}]" + + with open(path_to_ref_data, encoding="utf-8") as stream: + ref_data = yaml.safe_load(stream) + ref_num_fq = ref_data.get(test_name, {}).get(compression_type, {}).get("number_of_fakequantizers", -1) + if update: + print(f"Updating FQ refs: {ref_num_fq}->{num_fq} for {compression_type}") + ref_data[test_name][compression_type]["number_of_fakequantizers"] = num_fq + with open(path_to_ref_data, encoding="utf-8", mode="w") as stream: + stream.write(yaml.safe_dump(ref_data)) + assert num_fq == ref_num_fq, f"Incorrect number of FQs in optimized model: {num_fq} != {ref_num_fq}" + + +def ptq_validate_fq_testing(template, root, otx_dir, task_type, test_name): + template_work_dir = get_template_dir(template, root) + if "visual_prompting" == task_type: + xml_paths = [ + f"{template_work_dir}/ptq_{template.model_template_id}/visual_prompting_image_encoder.xml", + f"{template_work_dir}/ptq_{template.model_template_id}/visual_prompting_decoder.xml", + ] + else: + xml_paths = [f"{template_work_dir}/ptq_{template.model_template_id}/openvino.xml"] + + for xml_path in xml_paths: + if not os.path.exists(xml_path): + pytest.skip(reason=f"required file is not exist - {xml_path}") + + if "visual_prompting" == task_type: + paths_to_ref_data = [ + os.path.join( + otx_dir, + "tests", + "e2e/cli", + task_type, + "reference", + template.model_template_id, + "compressed_image_encoder.yml", + ), + os.path.join( + otx_dir, + "tests", + "e2e/cli", + task_type, + "reference", + template.model_template_id, + "compressed_decoder.yml", + ), + ] + else: + paths_to_ref_data = [ + os.path.join( + otx_dir, "tests", "e2e/cli", task_type, "reference", template.model_template_id, "compressed_model.yml" + ) + ] + + for xml_path, path_to_ref_data in zip(xml_paths, paths_to_ref_data): + _validate_fq_in_xml(xml_path, path_to_ref_data, "ptq", test_name) + + +def ptq_eval_testing(template, root, otx_dir, args, is_visual_prompting=False): + template_work_dir = get_template_dir(template, root) + if is_visual_prompting: + weights_path = f"{template_work_dir}/ptq_{template.model_template_id}/visual_prompting_decoder.xml" + else: + weights_path = f"{template_work_dir}/ptq_{template.model_template_id}/openvino.xml" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--output", + f"{template_work_dir}/ptq_{template.model_template_id}", + "--load-weights", + weights_path, + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/ptq_{template.model_template_id}/performance.json") + + +def nncf_optimize_testing(template, root, otx_dir, args): + if template.entrypoints.nncf is None: + pytest.skip("NNCF QAT is disabled: entrypoints.nncf in template is not specified") + + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "optimize", + template.model_template_path, + "--train-data-roots", + f'{os.path.join(otx_dir, args["--train-data-roots"])}', + "--val-data-roots", + f'{os.path.join(otx_dir, args["--val-data-roots"])}', + "--load-weights", + weights_path, + "--output", + f"{template_work_dir}/nncf_{template.model_template_id}", + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + command_line.extend(args["train_params"]) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/nncf_{template.model_template_id}/weights.pth") + assert os.path.exists(f"{template_work_dir}/nncf_{template.model_template_id}/label_schema.json") + + +def nncf_export_testing(template, root): + if template.entrypoints.nncf is None: + pytest.skip("NNCF QAT is disabled: entrypoints.nncf in template is not specified") + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/nncf_{template.model_template_id}/weights.pth" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "export", + template.model_template_path, + "--load-weights", + weights_path, + "--output", + f"{template_work_dir}/exported_nncf_{template.model_template_id}", + ] + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/exported_nncf_{template.model_template_id}/openvino.xml") + assert os.path.exists(f"{template_work_dir}/exported_nncf_{template.model_template_id}/openvino.bin") + assert os.path.exists(f"{template_work_dir}/exported_nncf_{template.model_template_id}/label_schema.json") + original_bin_size = os.path.getsize(f"{template_work_dir}/exported_{template.model_template_id}/openvino.bin") + compressed_bin_size = os.path.getsize( + f"{template_work_dir}/exported_nncf_{template.model_template_id}/openvino.bin" + ) + assert compressed_bin_size < original_bin_size, f"{compressed_bin_size=}, {original_bin_size=}" + ckpt = torch.load(f"{template_work_dir}/nncf_{template.model_template_id}/weights.pth") + input_size = ckpt.get("input_size", None) + if input_size: + with open( + f"{template_work_dir}/exported_nncf_{template.model_template_id}/openvino.xml", encoding="utf-8" + ) as xml_stream: + xml_model = xml_stream.read() + assert f"{input_size[1]},{input_size[0]}" in xml_model + + +def nncf_validate_fq_testing(template, root, otx_dir, task_type, test_name): + if template.entrypoints.nncf is None: + pytest.skip("NNCF QAT is disabled: entrypoints.nncf in template is not specified") + template_work_dir = get_template_dir(template, root) + + xml_path = f"{template_work_dir}/exported_nncf_{template.model_template_id}/openvino.xml" + if not os.path.exists(xml_path): + pytest.skip(reason=f"required file is not exist - {xml_path}") + + path_to_ref_data = os.path.join( + otx_dir, "tests", "e2e/cli", task_type, "reference", template.model_template_id, "compressed_model.yml" + ) + + _validate_fq_in_xml(xml_path, path_to_ref_data, "nncf", test_name) + + +def nncf_eval_testing(template, root, otx_dir, args, threshold=0.01): + if template.entrypoints.nncf is None: + pytest.skip("NNCF QAT is disabled: entrypoints.nncf in template is not specified") + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/nncf_{template.model_template_id}/weights.pth" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--load-weights", + weights_path, + "--output", + f"{template_work_dir}/nncf_{template.model_template_id}", + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/nncf_{template.model_template_id}/performance.json") + with open(f"{template_work_dir}/nncf_{template.model_template_id}/nncf_performance.json") as read_file: + trained_performance = json.load(read_file) + with open(f"{template_work_dir}/nncf_{template.model_template_id}/performance.json") as read_file: + evaluated_performance = json.load(read_file) + + compare_model_accuracy(evaluated_performance, trained_performance, threshold) + + +def nncf_eval_openvino_testing(template, root, otx_dir, args): + if template.entrypoints.nncf is None: + pytest.skip("NNCF QAT is disabled: entrypoints.nncf in template is not specified") + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/exported_nncf_{template.model_template_id}/openvino.xml" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + command_line = [ + "otx", + "eval", + template.model_template_path, + "--test-data-roots", + f'{os.path.join(otx_dir, args["--test-data-roots"])}', + "--load-weights", + weights_path, + "--output", + f"{template_work_dir}/exported_nncf_{template.model_template_id}", + ] + command_line.extend(["--workspace", f"{template_work_dir}"]) + check_run(command_line) + assert os.path.exists(f"{template_work_dir}/exported_nncf_{template.model_template_id}/performance.json") + + +def xfail_templates(templates, xfail_template_ids_reasons): + xfailed_templates = [] + for template in templates: + reasons = [ + reason for template_id, reason in xfail_template_ids_reasons if template_id == template.model_template_id + ] + if len(reasons) == 0: + xfailed_templates.append(template) + elif len(reasons) == 1: + xfailed_templates.append(pytest.param(template, marks=pytest.mark.xfail(reason=reasons[0]))) + else: + raise RuntimeError( + "More than one reason for template. If you have more than one Jira tickets, list them in one reason." + ) + return xfailed_templates + + +def otx_explain_testing(template, root, otx_dir, args, trained=False): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + test_algorithm = "ClassWiseSaliencyMap" + + train_ann_file = args.get("--train-ann-file", "") + if "hierarchical" in train_ann_file: + train_type = "hierarchical" + elif "multilabel" in train_ann_file: + train_type = "multilabel" + else: + train_type = "default" + + save_dir = f"explain_{template.model_template_id}/{test_algorithm}/{train_type}/" + output_dir = os.path.join(template_work_dir, save_dir) + data_input = os.path.join(otx_dir, args["--input"]) + command_line = [ + "otx", + "explain", + template.model_template_path, + "--load-weights", + weights_path, + "--input", + data_input, + "--output", + output_dir, + "--explain-algorithm", + test_algorithm, + ] + check_run(command_line) + assert os.path.exists(output_dir) + if trained: + assert len(os.listdir(output_dir)) > 0 + assert all([os.path.splitext(fname)[1] in [".tiff", ".log"] for fname in os.listdir(output_dir)]) + + +def otx_explain_testing_all_classes(template, root, otx_dir, args): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + test_algorithm = "ClassWiseSaliencyMap" + + train_ann_file = args.get("--train-ann-file", "") + if "hierarchical" in train_ann_file: + train_type = "hierarchical" + elif "multilabel" in train_ann_file: + train_type = "multilabel" + else: + train_type = "default" + + save_dir = f"explain_all_classes_{template.model_template_id}/{test_algorithm}/{train_type}/" + output_dir = os.path.join(template_work_dir, save_dir) + data_input = os.path.join(otx_dir, args["--input"]) + command_line = [ + "otx", + "explain", + template.model_template_path, + "--load-weights", + weights_path, + "--input", + data_input, + "--output", + output_dir, + "--explain-algorithm", + test_algorithm, + "--explain-all-classes", + ] + check_run(command_line) + assert os.path.exists(output_dir) + + save_dir_explain_only_predicted_classes = f"explain_{template.model_template_id}/{test_algorithm}/{train_type}/" + output_dir_explain_only_predicted_classes = os.path.join(template_work_dir, save_dir_explain_only_predicted_classes) + if test_algorithm == "ActivationMap": + assert len(os.listdir(output_dir)) == len(os.listdir(output_dir_explain_only_predicted_classes)) + else: + assert len(os.listdir(output_dir)) >= len(os.listdir(output_dir_explain_only_predicted_classes)) + assert all([os.path.splitext(fname)[1] in [".tiff", ".log"] for fname in os.listdir(output_dir)]) + + +def otx_explain_testing_process_saliency_maps(template, root, otx_dir, args, trained=False): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + test_algorithm = "ClassWiseSaliencyMap" + + train_ann_file = args.get("--train-ann-file", "") + if "hierarchical" in train_ann_file: + train_type = "hierarchical" + elif "multilabel" in train_ann_file: + train_type = "multilabel" + else: + train_type = "default" + + save_dir = f"explain_process_saliency_maps_{template.model_template_id}/{test_algorithm}/{train_type}/" + output_dir = os.path.join(template_work_dir, save_dir) + data_input = os.path.join(otx_dir, args["--input"]) + command_line = [ + "otx", + "explain", + template.model_template_path, + "--load-weights", + weights_path, + "--input", + data_input, + "--output", + output_dir, + "--explain-algorithm", + test_algorithm, + "--process-saliency-maps", + ] + check_run(command_line) + assert os.path.exists(output_dir) + if trained: + assert len(os.listdir(output_dir)) > 0 + assert all([os.path.splitext(fname)[1] in [".png", ".log"] for fname in os.listdir(output_dir)]) + + +def otx_explain_openvino_testing(template, root, otx_dir, args, trained=False): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/exported_{template.model_template_id}_w_features/openvino.xml" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + test_algorithm = "ClassWiseSaliencyMap" + + train_ann_file = args.get("--train-ann-file", "") + if "hierarchical" in train_ann_file: + train_type = "hierarchical" + elif "multilabel" in train_ann_file: + train_type = "multilabel" + else: + train_type = "default" + + save_dir = f"explain_ov_{template.model_template_id}/{test_algorithm}/{train_type}/" + output_dir = os.path.join(template_work_dir, save_dir) + data_input = os.path.join(otx_dir, args["--input"]) + command_line = [ + "otx", + "explain", + template.model_template_path, + "--load-weights", + weights_path, + "--input", + data_input, + "--output", + output_dir, + "--explain-algorithm", + test_algorithm, + ] + assert os.path.exists(f"{template_work_dir}/exported_{template.model_template_id}_w_features/openvino.xml") + check_run(command_line) + assert os.path.exists(output_dir) + if trained: + assert len(os.listdir(output_dir)) > 0 + assert all([os.path.splitext(fname)[1] in [".tiff", ".log"] for fname in os.listdir(output_dir)]) + + +def otx_explain_all_classes_openvino_testing(template, root, otx_dir, args): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/exported_{template.model_template_id}_w_features/openvino.xml" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + test_algorithm = "ClassWiseSaliencyMap" + + train_ann_file = args.get("--train-ann-file", "") + if "hierarchical" in train_ann_file: + train_type = "hierarchical" + elif "multilabel" in train_ann_file: + train_type = "multilabel" + else: + train_type = "default" + + save_dir = f"explain_ov_all_classes_{template.model_template_id}/{test_algorithm}/{train_type}/" + output_dir = os.path.join(template_work_dir, save_dir) + data_input = os.path.join(otx_dir, args["--input"]) + command_line = [ + "otx", + "explain", + template.model_template_path, + "--load-weights", + weights_path, + "--input", + data_input, + "--output", + output_dir, + "--explain-algorithm", + test_algorithm, + "--explain-all-classes", + ] + assert os.path.exists(f"{template_work_dir}/exported_{template.model_template_id}_w_features/openvino.xml") + check_run(command_line) + assert os.path.exists(output_dir) + + save_dir_explain_only_predicted_classes = f"explain_ov_{template.model_template_id}/{test_algorithm}/{train_type}/" + output_dir_explain_only_predicted_classes = os.path.join(template_work_dir, save_dir_explain_only_predicted_classes) + if test_algorithm == "ActivationMap": + assert len(os.listdir(output_dir)) == len(os.listdir(output_dir_explain_only_predicted_classes)) + else: + assert len(os.listdir(output_dir)) >= len(os.listdir(output_dir_explain_only_predicted_classes)) + assert all([os.path.splitext(fname)[1] in [".tiff", ".log"] for fname in os.listdir(output_dir)]) + + +def otx_explain_process_saliency_maps_openvino_testing(template, root, otx_dir, args, trained=False): + template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/exported_{template.model_template_id}_w_features/openvino.xml" + if not os.path.exists(weights_path): + pytest.skip(reason=f"required file is not exist - {weights_path}") + + test_algorithm = "ClassWiseSaliencyMap" + + train_ann_file = args.get("--train-ann-file", "") + if "hierarchical" in train_ann_file: + train_type = "hierarchical" + elif "multilabel" in train_ann_file: + train_type = "multilabel" + else: + train_type = "default" + + save_dir = f"explain_ov_process_saliency_maps_{template.model_template_id}/{test_algorithm}/{train_type}/" + output_dir = os.path.join(template_work_dir, save_dir) + data_input = os.path.join(otx_dir, args["--input"]) + command_line = [ + "otx", + "explain", + template.model_template_path, + "--load-weights", + weights_path, + "--input", + data_input, + "--output", + output_dir, + "--explain-algorithm", + test_algorithm, + "--process-saliency-maps", + ] + assert os.path.exists(f"{template_work_dir}/exported_{template.model_template_id}_w_features/openvino.xml") + check_run(command_line) + assert os.path.exists(output_dir) + if trained: + assert len(os.listdir(output_dir)) > 0 + assert all([os.path.splitext(fname)[1] in [".png", ".log"] for fname in os.listdir(output_dir)]) + + +def otx_find_testing(): + """Performs several options of available otx find.""" + # Find all model template + command_line = ["otx", "find", "--template"] + check_run(command_line) + + # Find command per tasks + for task in find_supported_tasks: + command_line = ["otx", "find", "--template", "--task", task] + check_run(command_line) + + # Find Backbones per backends + for backbone_backends in find_supported_backends: + command_line = [ + "otx", + "find", + "--backbone", + backbone_backends, + ] + check_run(command_line) + + +def otx_build_task_testing(root, task): + """Build OTX-workspace per tasks. + + Build and verify the otx-workspace corresponding to each task. + """ + # Build otx-workspace per tasks check - Default Model Template only + command_line = [ + "otx", + "build", + "--task", + task, + "--workspace", + os.path.join(root, f"otx-workspace-{task}"), + ] + check_run(command_line) + + +def otx_build_backbone_testing(root, backbone_args): + """Build backbone & Update model testing. + + Build each backbone to create backbone.yaml into workspace, + build for the default model for the task, + and even test updating the model config. + This is done on the premise that the otx_workspace + has been created well through otx_build_task_testing. + """ + task, backbone = backbone_args + task_workspace = os.path.join(root, f"otx-workspace-{task}") + command_line = [ + "otx", + "build", + "--task", + f"{task}", + "--workspace", + task_workspace, + ] + check_run(command_line) + assert os.path.exists(task_workspace) + + # Build model.py from backbone type + command_line = [ + "otx", + "build", + "--backbone", + backbone, + "--workspace", + task_workspace, + ] + check_run(command_line) + from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig + + model_config = OTXConfig.fromfile(os.path.join(task_workspace, "model.py")) + assert os.path.exists(os.path.join(task_workspace, "model.py")) + assert "backbone" in model_config["model"], "'backbone' is not in model configs" + assert ( + model_config["model"]["backbone"]["type"] == backbone + ), f"{model_config['model']['backbone']['type']} != {backbone}" + + +def otx_build_testing(root, args: Dict[str, str], expected: Dict[str, str]): + workspace_root = os.path.join(root, "otx-workspace") + command_line = ["otx", "build", "--workspace", workspace_root] + for option, value in args.items(): + command_line.extend([option, value]) + check_run(command_line) + from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig + + template_config = OTXConfig.fromfile(os.path.join(workspace_root, "template.yaml")) + assert template_config.name == expected["model"] + assert ( + template_config.hyper_parameters.parameter_overrides.algo_backend.train_type.default_value + == expected["train_type"] + ) + + +def otx_build_auto_config(root, otx_dir: str, args: Dict[str, str]): + workspace_root = os.path.join(root, "otx-workspace") + command_line = ["otx", "build", "--workspace", workspace_root] + + for option, value in args.items(): + if option in ["--train-data-roots", "--val-data-roots"]: + command_line.extend([option, f"{os.path.join(otx_dir, value)}"]) + elif option in ["--task"]: + command_line.extend([option, args[option]]) + check_run(command_line) + + +def otx_train_auto_config(root, otx_dir: str, args: Dict[str, str], use_output: bool = True): + work_dir = os.path.join(root, "otx-workspace") + command_line = ["otx", "train"] + + for option, value in args.items(): + if option == "template": + command_line.extend([args[option]]) + elif option in ["--train-data-roots", "--val-data-roots"]: + command_line.extend([option, f"{os.path.join(otx_dir, value)}"]) + if use_output: + command_line.extend(["--output", f"{work_dir}"]) + command_line.extend(["--workspace", f"{work_dir}"]) + command_line.extend(args["train_params"]) + check_run(command_line) + + +def generate_model_template_testing(templates): + class _TestModelTemplates: + @e2e_pytest_component + def test_model_category(self): + stat = { + ModelCategory.SPEED: 0, + ModelCategory.BALANCE: 0, + ModelCategory.ACCURACY: 0, + ModelCategory.OTHER: 0, + } + for template in templates: + stat[template.model_category] += 1 + assert stat[ModelCategory.SPEED] == 1 + assert stat[ModelCategory.BALANCE] <= 1 + assert stat[ModelCategory.ACCURACY] == 1 + + @e2e_pytest_component + def test_model_status(self): + for template in templates: + if template.model_status == ModelStatus.DEPRECATED: + assert template.model_category == ModelCategory.OTHER + + @e2e_pytest_component + def test_default_for_task(self): + num_default_model = 0 + for template in templates: + if template.is_default_for_task: + num_default_model += 1 + assert template.model_category != ModelCategory.OTHER + assert template.model_status == ModelStatus.ACTIVE + assert num_default_model == 1 + + return _TestModelTemplates + + +def compare_model_accuracy(performance_to_test: Dict, target_performance: Dict, threshold: Union[float, int]): + for k in target_performance.keys(): + if k == "avg_time_per_image": + continue + assert ( + performance_to_test[k] >= target_performance[k] + or abs(target_performance[k] - performance_to_test[k]) / (target_performance[k] + 1e-10) <= threshold + ), f"{target_performance[k]=}, {performance_to_test[k]=}" diff --git a/tests/test_suite/training_test_case.py b/tests/test_suite/training_test_case.py new file mode 100644 index 00000000000..01c0e8f8d03 --- /dev/null +++ b/tests/test_suite/training_test_case.py @@ -0,0 +1,120 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from abc import abstractmethod +from collections import Counter, OrderedDict +from copy import deepcopy +from typing import Callable, Dict, List, Optional, Type + +from .e2e_test_system import DataCollector +from .logging import get_logger +from .training_tests_actions import BaseOTXTestAction +from .training_tests_stage import OTXTestStage, OTXTestStagesStorageInterface, Validator + +logger = get_logger() + + +def _get_duplications(arr): + c = Counter(arr) + dups = [k for k, v in c.items() if v > 1] + return dups + + +def _str_dict_with_shortened_vals(d, max_len=200): + assert isinstance(d, dict) + if not d: + return "{}" + + def _shorten(v): + sv = str(v) + if len(sv) <= max_len: + return sv + return sv[:max_len] + "..." + + s = "\n".join(f"{k}: {_shorten(v)}," for k, v in d.items()) + s = "\n ".join(s.split("\n")) + s = "{\n " + s + "\n}" + return s + + +class OTXTestCaseInterface(OTXTestStagesStorageInterface): + @classmethod + @abstractmethod + def get_list_of_test_stages(cls): + raise NotImplementedError("The method get_list_of_test_stages is not implemented") + + @abstractmethod + def run_stage( + self, + stage_name: str, + data_collector: DataCollector, + cur_test_expected_metrics_callback: Optional[Callable[[], Dict]], + ): + raise NotImplementedError("The method run_stage is not implemented") + + +def generate_otx_integration_test_case_class( + test_actions_classes: List[Type[BaseOTXTestAction]], +) -> Type: + test_actions_classes = deepcopy(test_actions_classes) + + # check names' duplication + classes_names = [action_cls._name for action_cls in test_actions_classes] + name_dups = _get_duplications(classes_names) + if name_dups: + raise ValueError(f"Wrong input: there are duplications in names of actions; duplications = {name_dups}") + + class _OTXIntegrationTestCase(OTXTestCaseInterface): + _TEST_STAGES = [action_cls._name for action_cls in test_actions_classes] + + @classmethod + def get_list_of_test_stages(cls): + return deepcopy(cls._TEST_STAGES) + + def __init__(self, params_factories_for_test_actions: Dict[str, Callable[[], Dict]]): + logger.debug("initialization of test case: begin") + self._stages = OrderedDict() + for action_cls in test_actions_classes: + logger.debug(f"initialization of test case: action_cls={action_cls}") + + cur_name = action_cls._name + assert cur_name is not None + cur_params_factory = params_factories_for_test_actions.get(cur_name) + if cur_params_factory is not None: + logger.debug("initialization of test case: calling params factory") + cur_params = cur_params_factory() + else: + cur_params = {} + + assert isinstance(cur_params, dict), f"Wrong params received from factory: {cur_params}" + short_params_str = _str_dict_with_shortened_vals(cur_params) + logger.info(f"initialization of test case: add action '{cur_name}' " f"with params={short_params_str}") + + cur_action = action_cls(**cur_params) + + # Note that `self` is used as stages_storage for OTXTestStage below + cur_stage = OTXTestStage(action=cur_action, stages_storage=self) + self._stages[cur_name] = cur_stage + + assert list(self._stages.keys()) == list(self._TEST_STAGES) + + # test results should be kept between stages + self.test_results_storage: OrderedDict = OrderedDict() + logger.debug("initialization of test case: end") + + # implementation of method from OTXTestStagesStorageInterface + def get_stage(self, name: str) -> "OTXTestStage": + return self._stages[name] + + def run_stage( + self, + stage_name: str, + data_collector: DataCollector, + cur_test_expected_metrics_callback: Optional[Callable[[], Dict]], + ): + assert stage_name in self._TEST_STAGES, f"Wrong stage_name {stage_name}" + validator = Validator(cur_test_expected_metrics_callback) + self._stages[stage_name].run_once(data_collector, self.test_results_storage, validator) + + return _OTXIntegrationTestCase diff --git a/tests/test_suite/training_tests_actions.py b/tests/test_suite/training_tests_actions.py new file mode 100644 index 00000000000..5e8c25ee5cf --- /dev/null +++ b/tests/test_suite/training_tests_actions.py @@ -0,0 +1,799 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import importlib +import json +import os +import os.path as osp +import re +from abc import ABC, abstractmethod +from collections import OrderedDict +from copy import deepcopy +from typing import List, Optional, Type + +import pytest +import yaml + +from otx.api.configuration.helper import create as otx_api_configuration_helper_create +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ModelEntity, ModelFormat, ModelOptimizationType +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.serialization.label_mapper import LabelSchemaMapper, label_schema_to_bytes +from otx.api.usecases.adapters.model_adapter import ModelAdapter +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.api.utils.importing import get_impl_class + +from .e2e_test_system import DataCollector +from .logging import get_logger +from .training_tests_common import ( + KEEP_CONFIG_FIELD_VALUE, + performance_to_score_name_value, +) + +logger = get_logger() + + +class BaseOTXTestAction(ABC): + _name: Optional[str] = None + _with_validation = False + _depends_stages_names: List[str] = [] + + def __init__(*args, **kwargs): + pass + + @property + def name(self): + return type(self)._name + + @property + def with_validation(self): + return type(self)._with_validation + + @property + def depends_stages_names(self): + return type(self)._depends_stages_names + + def __str__(self): + return ( + f"{type(self).__name__}(" + f"name={self.name}, " + f"with_validation={self.with_validation}, " + f"depends_stages_names={self.depends_stages_names})" + ) + + def _check_result_prev_stages(self, results_prev_stages, list_required_stages): + for stage_name in list_required_stages: + if not results_prev_stages or stage_name not in results_prev_stages: + raise RuntimeError( + f"The action {self.name} requires results of the stage {stage_name}, " f"but they are absent" + ) + + @abstractmethod + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + raise NotImplementedError("The main action method is not implemented") + + +def create_environment_and_task(params, labels_schema, model_template, dataset=None, model_adapters=None): + + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + if model_adapters is not None: + environment.model = ModelEntity( + train_dataset=dataset, + configuration=environment.get_model_configuration(), + model_adapters=model_adapters, + ) + + logger.info("Create base Task") + task_impl_path = model_template.entrypoints.base + task_cls = get_impl_class(task_impl_path) + task = task_cls(task_environment=environment) + return environment, task + + +class OTXTestTrainingAction(BaseOTXTestAction): + _name = "training" + + def __init__( + self, + dataset, + labels_schema, + template_path, + num_training_iters, + batch_size, + checkpoint=None, + ): + self.dataset = dataset + self.labels_schema = labels_schema + self.template_path = template_path + self.num_training_iters = num_training_iters + self.batch_size = batch_size + self.checkpoint = checkpoint + + def _get_training_performance_as_score_name_value(self): + training_performance = getattr(self.output_model, "performance", None) + if training_performance is None: + raise RuntimeError("Cannot get training performance") + return performance_to_score_name_value(training_performance) + + def _run_otx_training(self, data_collector): + logger.debug(f"self.template_path = {self.template_path}") + + print(f"train dataset: {len(self.dataset.get_subset(Subset.TRAINING))} items") + print(f"validation dataset: " f"{len(self.dataset.get_subset(Subset.VALIDATION))} items") + + logger.debug("Load model template") + self.model_template = parse_model_template(self.template_path) + + logger.debug("Set hyperparameters") + params = otx_api_configuration_helper_create(self.model_template.hyper_parameters.data) + if self.num_training_iters != KEEP_CONFIG_FIELD_VALUE: + params.learning_parameters.num_iters = int(self.num_training_iters) + logger.debug(f"Set params.learning_parameters.num_iters=" f"{params.learning_parameters.num_iters}") + else: + logger.debug(f"Keep params.learning_parameters.num_iters=" f"{params.learning_parameters.num_iters}") + + if self.batch_size != KEEP_CONFIG_FIELD_VALUE: + params.learning_parameters.batch_size = int(self.batch_size) + logger.debug(f"Set params.learning_parameters.batch_size=" f"{params.learning_parameters.batch_size}") + else: + logger.debug(f"Keep params.learning_parameters.batch_size=" f"{params.learning_parameters.batch_size}") + + model_adapters = None + if self.checkpoint is not None: + logger.debug("Load pretrained model") + model_adapters = { + "weights.pth": ModelAdapter(open(self.checkpoint, "rb").read()), + } + label_schema_path = osp.join(osp.dirname(self.checkpoint), "label_schema.json") + if osp.exists(label_schema_path): + with open(label_schema_path, encoding="UTF-8") as read_file: + serialized_label_schema = LabelSchemaMapper.backward(json.load(read_file)) + model_adapters.update( + {"label_schema.json": ModelAdapter(label_schema_to_bytes(serialized_label_schema))} + ) + + self.environment, self.task = create_environment_and_task( + params, + self.labels_schema, + self.model_template, + self.dataset, + model_adapters, + ) + + self.output_model = ModelEntity( + self.dataset, + self.environment.get_model_configuration(), + ) + + self.copy_hyperparams = deepcopy(self.task._hyperparams) + + logger.debug("Train model") + + try: + self.task.train(self.dataset, self.output_model) + except Exception as ex: + raise RuntimeError("Training failed") from ex + + score_name, score_value = self._get_training_performance_as_score_name_value() + logger.info(f"performance={self.output_model.performance}") + data_collector.log_final_metric("metric_name", self.name + "/" + score_name) + data_collector.log_final_metric("metric_value", score_value) + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._run_otx_training(data_collector) + results = { + "model_template": self.model_template, + "task": self.task, + "dataset": self.dataset, + "environment": self.environment, + "output_model": self.output_model, + } + return results + + +def is_nncf_enabled(): + return importlib.util.find_spec("nncf") is not None + + +def run_evaluation(dataset, task, model): + logger.debug("Evaluation: Get predictions on the dataset") + predicted_dataset = task.infer(dataset.with_empty_annotations(), InferenceParameters(is_evaluation=True)) + resultset = ResultSetEntity( + model=model, + ground_truth_dataset=dataset, + prediction_dataset=predicted_dataset, + ) + logger.debug("Evaluation: Estimate quality on dataset") + task.evaluate(resultset) + evaluation_performance = resultset.performance + logger.info(f"Evaluation: performance={evaluation_performance}") + score_name, score_value = performance_to_score_name_value(evaluation_performance) + return score_name, score_value + + +class OTXTestTrainingEvaluationAction(BaseOTXTestAction): + _name = "training_evaluation" + _with_validation = True + _depends_stages_names = ["training"] + + def __init__(self, subset=Subset.TESTING): + self.subset = subset + + def _run_otx_evaluation(self, data_collector, dataset, task, trained_model): + logger.info("Begin evaluation of trained model") + validation_dataset = dataset.get_subset(self.subset) + score_name, score_value = run_evaluation(validation_dataset, task, trained_model) + data_collector.log_final_metric("metric_name", self.name + "/" + score_name) + data_collector.log_final_metric("metric_value", score_value) + logger.info(f"End evaluation of trained model, results: {score_name}: {score_value}") + return score_name, score_value + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "dataset": results_prev_stages["training"]["dataset"], + "task": results_prev_stages["training"]["task"], + "trained_model": results_prev_stages["training"]["output_model"], + } + + score_name, score_value = self._run_otx_evaluation(data_collector, **kwargs) + results = {"metrics": {"accuracy": {score_name: score_value}}} + return results + + +def run_export(environment, dataset, task, action_name, expected_optimization_type): + logger.debug(f'For action "{action_name}": Copy environment for evaluation exported model') + + environment_for_export = deepcopy(environment) + + logger.debug(f'For action "{action_name}": Create exported model') + exported_model = ModelEntity( + dataset, + environment_for_export.get_model_configuration(), + ) + logger.debug("Run export") + + try: + task.export(ExportType.OPENVINO, exported_model) + except Exception as ex: + raise RuntimeError("Export to OpenVINO failed") from ex + + assert ( + exported_model.model_format == ModelFormat.OPENVINO + ), f"In action '{action_name}': Wrong model format after export" + assert ( + exported_model.optimization_type == expected_optimization_type + ), f"In action '{action_name}': Wrong optimization type" + + logger.debug(f'For action "{action_name}": Set exported model into environment for export') + environment_for_export.model = exported_model + return environment_for_export, exported_model + + +class OTXTestExportAction(BaseOTXTestAction): + _name = "export" + _depends_stages_names = ["training"] + + def _run_otx_export(self, data_collector, environment, dataset, task): + self.environment_for_export, self.exported_model = run_export( + environment, + dataset, + task, + action_name=self.name, + expected_optimization_type=ModelOptimizationType.MO, + ) + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "environment": results_prev_stages["training"]["environment"], + "dataset": results_prev_stages["training"]["dataset"], + "task": results_prev_stages["training"]["task"], + } + + self._run_otx_export(data_collector, **kwargs) + results = { + "environment": self.environment_for_export, + "exported_model": self.exported_model, + } + return results + + +def create_openvino_task(model_template, environment): + logger.debug("Create OpenVINO Task") + openvino_task_impl_path = model_template.entrypoints.openvino + openvino_task_cls = get_impl_class(openvino_task_impl_path) + openvino_task = openvino_task_cls(environment) + return openvino_task + + +class OTXTestExportEvaluationAction(BaseOTXTestAction): + _name = "export_evaluation" + _with_validation = True + _depends_stages_names = ["training", "export", "training_evaluation"] + + def __init__(self, subset=Subset.TESTING): + self.subset = subset + + def _run_otx_export_evaluation( + self, + data_collector, + model_template, + dataset, + environment_for_export, + exported_model, + ): + logger.info("Begin evaluation of exported model") + self.openvino_task = create_openvino_task(model_template, environment_for_export) + validation_dataset = dataset.get_subset(self.subset) + score_name, score_value = run_evaluation(validation_dataset, self.openvino_task, exported_model) + data_collector.log_final_metric("metric_name", self.name + "/" + score_name) + data_collector.log_final_metric("metric_value", score_value) + logger.info("End evaluation of exported model") + return score_name, score_value + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "model_template": results_prev_stages["training"]["model_template"], + "dataset": results_prev_stages["training"]["dataset"], + "environment_for_export": results_prev_stages["export"]["environment"], + "exported_model": results_prev_stages["export"]["exported_model"], + } + + score_name, score_value = self._run_otx_export_evaluation(data_collector, **kwargs) + results = {"metrics": {"accuracy": {score_name: score_value}}} + return results + + +class OTXTestPotAction(BaseOTXTestAction): + _name = "pot" + _depends_stages_names = ["export"] + + def _run_otx_pot(self, data_collector, model_template, dataset, environment_for_export): + logger.debug("Creating environment and task for POT optimization") + self.environment_for_pot = deepcopy(environment_for_export) + self.openvino_task_pot = create_openvino_task(model_template, environment_for_export) + + self.optimized_model_pot = ModelEntity( + dataset, + self.environment_for_pot.get_model_configuration(), + ) + logger.info("Run POT optimization") + + try: + self.openvino_task_pot.optimize( + OptimizationType.POT, + dataset, + self.optimized_model_pot, + OptimizationParameters(), + ) + except Exception as ex: + raise RuntimeError("POT optimization failed") from ex + + assert self.optimized_model_pot.model_format == ModelFormat.OPENVINO, "Wrong model format after pot" + assert self.optimized_model_pot.optimization_type == ModelOptimizationType.POT, "Wrong optimization type" + logger.info("POT optimization is finished") + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "model_template": results_prev_stages["training"]["model_template"], + "dataset": results_prev_stages["training"]["dataset"], + "environment_for_export": results_prev_stages["export"]["environment"], + } + + self._run_otx_pot(data_collector, **kwargs) + results = { + "openvino_task_pot": self.openvino_task_pot, + "optimized_model_pot": self.optimized_model_pot, + } + return results + + +def check_fq_in_compressed_model(path_to_ref, compressed_type, model): + """ + Check number of FakeQuantize nodes in the compressed model. + """ + + num_fq = len(re.findall(r'type="FakeQuantize"', model)) + + assert os.path.exists( + path_to_ref + ), f"Reference file does not exist: {path_to_ref}. Current: {num_fq} for {compressed_type}." + + with open(path_to_ref, encoding="utf-8") as stream: + ref_data = yaml.safe_load(stream) + ref_num_fq = ref_data[compressed_type]["number_of_fakequantizers"] + assert num_fq == ref_num_fq, f"Incorrect number of FQs in compressed model: {num_fq} != {ref_num_fq}" + + +class OTXTestPotValidationFQAction(BaseOTXTestAction): + """ + Test to check number of FakeQuantize nodes in the compressed model by POT. + """ + + _name = "pot_validate_fq" + _depends_stages_names = ["training", "pot"] + + def __init__(self, reference_dir): + super().__init__() + self.reference_dir = reference_dir + + def _run_otx_pot_validate_fq(self, optimized_model_pot): + logger.info("Begin validation FQs of pot model") + + path_to_ref = os.path.join(self.reference_dir, "compressed_model.yml") + + check_fq_in_compressed_model(path_to_ref, "pot", optimized_model_pot.get_data("openvino.xml")) + + logger.info("End validation FQs of pot model") + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "optimized_model_pot": results_prev_stages["pot"]["optimized_model_pot"], + } + + self._run_otx_pot_validate_fq(**kwargs) + + return {} + + +class OTXTestPotEvaluationAction(BaseOTXTestAction): + _name = "pot_evaluation" + _with_validation = True + _depends_stages_names = ["training", "pot", "export_evaluation"] + + def __init__(self, subset=Subset.TESTING): + self.subset = subset + + def _run_otx_pot_evaluation(self, data_collector, dataset, openvino_task_pot, optimized_model_pot): + logger.info("Begin evaluation of pot model") + validation_dataset_pot = dataset.get_subset(self.subset) + score_name, score_value = run_evaluation(validation_dataset_pot, openvino_task_pot, optimized_model_pot) + data_collector.log_final_metric("metric_name", self.name + "/" + score_name) + data_collector.log_final_metric("metric_value", score_value) + logger.info("End evaluation of pot model") + return score_name, score_value + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "dataset": results_prev_stages["training"]["dataset"], + "openvino_task_pot": results_prev_stages["pot"]["openvino_task_pot"], + "optimized_model_pot": results_prev_stages["pot"]["optimized_model_pot"], + } + + score_name, score_value = self._run_otx_pot_evaluation(data_collector, **kwargs) + results = {"metrics": {"accuracy": {score_name: score_value}}} + return results + + +class OTXTestNNCFAction(BaseOTXTestAction): + _name = "nncf" + _depends_stages_names = ["training"] + + def _run_otx_nncf(self, data_collector, model_template, dataset, trained_model, environment): + logger.debug("Get predictions on the validation set for exported model") + self.environment_for_nncf = deepcopy(environment) + + logger.info("Create NNCF Task") + nncf_task_class_impl_path = model_template.entrypoints.nncf + if not nncf_task_class_impl_path: + pytest.skip("NNCF is not enabled for this template") + + if not is_nncf_enabled(): + pytest.skip("NNCF is not installed") + + logger.info("Creating NNCF task and structures") + self.nncf_model = ModelEntity( + dataset, + self.environment_for_nncf.get_model_configuration(), + ) + self.nncf_model.set_data("weights.pth", trained_model.get_data("weights.pth")) + + self.environment_for_nncf.model = self.nncf_model + + nncf_task_cls = get_impl_class(nncf_task_class_impl_path) + self.nncf_task = nncf_task_cls(task_environment=self.environment_for_nncf) + + logger.info("Run NNCF optimization") + try: + self.nncf_task.optimize(OptimizationType.NNCF, dataset, self.nncf_model, None) + except Exception as ex: + raise RuntimeError("NNCF optimization failed") from ex + + assert self.nncf_model.optimization_type == ModelOptimizationType.NNCF, "Wrong optimization type" + assert self.nncf_model.model_format == ModelFormat.BASE_FRAMEWORK, "Wrong model format" + + logger.info("NNCF optimization is finished") + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "model_template": results_prev_stages["training"]["model_template"], + "dataset": results_prev_stages["training"]["dataset"], + "trained_model": results_prev_stages["training"]["output_model"], + "environment": results_prev_stages["training"]["environment"], + } + + self._run_otx_nncf(data_collector, **kwargs) + results = { + "nncf_task": self.nncf_task, + "nncf_model": self.nncf_model, + "nncf_environment": self.environment_for_nncf, + } + return results + + +# TODO: think about move to special file +def check_nncf_model_graph(model, path_to_dot): + import networkx as nx + + logger.info(f"Reference graph: {path_to_dot}") + load_graph = nx.drawing.nx_pydot.read_dot(path_to_dot) + + graph = model.get_graph() + nx_graph = graph.get_graph_for_structure_analysis() + + for _, node in nx_graph.nodes(data=True): + if "scope" in node: + node.pop("scope") + + for k, attrs in nx_graph.nodes.items(): + attrs = {k: str(v) for k, v in attrs.items()} + load_attrs = {k: str(v).strip('"') for k, v in load_graph.nodes[k].items()} + if "scope" in load_attrs: + load_attrs.pop("scope") + if attrs != load_attrs: + logger.info("ATTR: {} : {} != {}".format(k, attrs, load_attrs)) + return False + + return load_graph.nodes.keys() == nx_graph.nodes.keys() and nx.DiGraph(load_graph).edges == nx_graph.edges + + +class OTXTestNNCFGraphAction(BaseOTXTestAction): + _name = "nncf_graph" + + def __init__( + self, + dataset, + labels_schema, + template_path, + reference_dir, + fn_get_compressed_model, + ): + self.dataset = dataset + self.labels_schema = labels_schema + self.template_path = template_path + self.reference_dir = reference_dir + self.fn_get_compressed_model = fn_get_compressed_model + + def _run_otx_nncf_graph(self, data_collector): + # pylint:disable=protected-access + logger.debug("Load model template") + model_template = parse_model_template(self.template_path) + nncf_task_class_impl_path = model_template.entrypoints.nncf + + if not nncf_task_class_impl_path: + pytest.skip("NNCF is not enabled for this template") + + if not is_nncf_enabled(): + pytest.skip("NNCF is not installed") + + if not os.path.exists(self.reference_dir): + pytest.skip("Reference directory does not exist") + + params = otx_api_configuration_helper_create(model_template.hyper_parameters.data) + environment, task = create_environment_and_task(params, self.labels_schema, model_template) + output_model = ModelEntity( + self.dataset, + environment.get_model_configuration(), + ) + # Save model without training to create nncf_task + task.save_model(output_model) + + logger.info("Create NNCF Task") + environment_for_nncf = deepcopy(environment) + + logger.info("Creating NNCF task and structures") + nncf_model = ModelEntity( + self.dataset, + environment_for_nncf.get_model_configuration(), + ) + nncf_model.set_data("weights.pth", output_model.get_data("weights.pth")) + + environment_for_nncf.model = nncf_model + + nncf_task_cls = get_impl_class(nncf_task_class_impl_path) + nncf_task = nncf_task_cls(task_environment=environment_for_nncf) + + path_to_ref_dot = os.path.join(self.reference_dir, "nncf", f"{nncf_task._nncf_preset}.dot") + if not os.path.exists(path_to_ref_dot): + pytest.skip("Reference file does not exist: {}".format(path_to_ref_dot)) + + compressed_model = self.fn_get_compressed_model(nncf_task) + + assert check_nncf_model_graph(compressed_model, path_to_ref_dot), "Compressed model differs from the reference" + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + self._run_otx_nncf_graph(data_collector) + return {} + + +class OTXTestNNCFEvaluationAction(BaseOTXTestAction): + _name = "nncf_evaluation" + _with_validation = True + _depends_stages_names = ["training", "nncf", "training_evaluation"] + + def __init__(self, subset=Subset.TESTING): + self.subset = subset + + def _run_otx_nncf_evaluation(self, data_collector, dataset, nncf_task, nncf_model): + logger.info("Begin evaluation of nncf model") + validation_dataset = dataset.get_subset(self.subset) + score_name, score_value = run_evaluation(validation_dataset, nncf_task, nncf_model) + data_collector.log_final_metric("metric_name", self.name + "/" + score_name) + data_collector.log_final_metric("metric_value", score_value) + logger.info("End evaluation of nncf model") + return score_name, score_value + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "dataset": results_prev_stages["training"]["dataset"], + "nncf_task": results_prev_stages["nncf"]["nncf_task"], + "nncf_model": results_prev_stages["nncf"]["nncf_model"], + } + + score_name, score_value = self._run_otx_nncf_evaluation(data_collector, **kwargs) + results = {"metrics": {"accuracy": {score_name: score_value}}} + return results + + +class OTXTestNNCFExportAction(BaseOTXTestAction): + _name = "nncf_export" + _depends_stages_names = ["training", "nncf"] + + def __init__(self, subset=Subset.VALIDATION): + self.subset = subset + + def _run_otx_nncf_export(self, data_collector, nncf_environment, dataset, nncf_task): + logger.info("Begin export of nncf model") + self.environment_nncf_export, self.nncf_exported_model = run_export( + nncf_environment, + dataset, + nncf_task, + action_name=self.name, + expected_optimization_type=ModelOptimizationType.NNCF, + ) + logger.info("End export of nncf model") + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "nncf_environment": results_prev_stages["nncf"]["nncf_environment"], + "dataset": results_prev_stages["training"]["dataset"], + "nncf_task": results_prev_stages["nncf"]["nncf_task"], + } + + self._run_otx_nncf_export(data_collector, **kwargs) + results = { + "environment": self.environment_nncf_export, + "exported_model": self.nncf_exported_model, + } + return results + + +class OTXTestNNCFExportEvaluationAction(BaseOTXTestAction): + _name = "nncf_export_evaluation" + _with_validation = True + _depends_stages_names = ["training", "nncf_export", "nncf_evaluation"] + + def __init__(self, subset=Subset.TESTING): + self.subset = subset + + def _run_otx_nncf_export_evaluation( + self, + data_collector, + model_template, + dataset, + nncf_environment_for_export, + nncf_exported_model, + ): + logger.info("Begin evaluation of NNCF exported model") + self.openvino_task = create_openvino_task(model_template, nncf_environment_for_export) + validation_dataset = dataset.get_subset(self.subset) + score_name, score_value = run_evaluation(validation_dataset, self.openvino_task, nncf_exported_model) + data_collector.log_final_metric("metric_name", self.name + "/" + score_name) + data_collector.log_final_metric("metric_value", score_value) + logger.info("End evaluation of NNCF exported model") + return score_name, score_value + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "model_template": results_prev_stages["training"]["model_template"], + "dataset": results_prev_stages["training"]["dataset"], + "nncf_environment_for_export": results_prev_stages["nncf_export"]["environment"], + "nncf_exported_model": results_prev_stages["nncf_export"]["exported_model"], + } + + score_name, score_value = self._run_otx_nncf_export_evaluation(data_collector, **kwargs) + results = {"metrics": {"accuracy": {score_name: score_value}}} + return results + + +class OTXTestNNCFValidationFQAction(BaseOTXTestAction): + """ + Test to check number of FakeQuantize nodes in the compressed model by POT. + """ + + _name = "nncf_validate_fq" + _depends_stages_names = ["training", "nncf_export"] + + def __init__(self, reference_dir): + super().__init__() + self.reference_dir = reference_dir + + def _run_otx_nncf_validate_fq(self, nncf_exported_model): + logger.info("Begin validation FQs of nncf model") + + path_to_ref = os.path.join(self.reference_dir, "compressed_model.yml") + + check_fq_in_compressed_model(path_to_ref, "nncf", nncf_exported_model.get_data("openvino.xml")) + + logger.info("End validation FQs of nncf model") + + def __call__(self, data_collector: DataCollector, results_prev_stages: OrderedDict): + self._check_result_prev_stages(results_prev_stages, self.depends_stages_names) + + kwargs = { + "nncf_exported_model": results_prev_stages["nncf_export"]["exported_model"], + } + + self._run_otx_nncf_validate_fq(**kwargs) + + return {} + + +def get_default_test_action_classes() -> List[Type[BaseOTXTestAction]]: + return [ + OTXTestTrainingAction, + OTXTestTrainingEvaluationAction, + OTXTestExportAction, + OTXTestExportEvaluationAction, + OTXTestPotAction, + OTXTestPotEvaluationAction, + OTXTestPotValidationFQAction, + OTXTestNNCFAction, + OTXTestNNCFEvaluationAction, + OTXTestNNCFExportAction, + OTXTestNNCFExportEvaluationAction, + OTXTestNNCFGraphAction, + OTXTestNNCFValidationFQAction, + ] diff --git a/tests/test_suite/training_tests_common.py b/tests/test_suite/training_tests_common.py new file mode 100644 index 00000000000..1842014c9e4 --- /dev/null +++ b/tests/test_suite/training_tests_common.py @@ -0,0 +1,63 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os.path as osp +from typing import Union + +from otx.api.entities.metrics import Performance, ScoreMetric + +# This string constant will be used as a special constant for a config field +# value to point that the field should be filled in tests' code by some default +# value specific for this field. +DEFAULT_FIELD_VALUE_FOR_USING_IN_TEST = "DEFAULT_FIELD_VALUE_FOR_USING_IN_TEST" + + +# This string constant will be used as a special constant for a config field value to point +# that the field should NOT be changed in tests -- its value should be taken +# from the template file or the config file of the model. +KEEP_CONFIG_FIELD_VALUE = "CONFIG" + + +# This is a constant for pointing usecase for reallife training tests +REALLIFE_USECASE_CONSTANT = "reallife" + + +# Constant for storing in dict-s with paths the root path +# that will be used for resolving relative paths. +ROOT_PATH_KEY = "_root_path" + + +def make_path_be_abs(some_val, root_path): + assert isinstance(some_val, str), f"Wrong type of value: {some_val}, type={type(some_val)}" + + assert isinstance(root_path, str), f"Wrong type of root_path: {root_path}, type={type(root_path)}" + + # Note that os.path.join(a, b) == b if b is an absolute path + return osp.join(root_path, some_val) + + +def make_paths_be_abs(some_dict, root_path): + assert isinstance(some_dict, dict), f"Wrong type of value: {some_dict}, type={type(some_dict)}" + + assert isinstance(root_path, str), f"Wrong type of root_path: {root_path}, type={type(root_path)}" + + assert all(isinstance(v, str) for v in some_dict.values()), f"Wrong input dict {some_dict}" + + for k in list(some_dict.keys()): + # Note that os.path.join(a, b) == b if b is an absolute path + some_dict[k] = osp.join(root_path, some_dict[k]) + return some_dict + + +def performance_to_score_name_value(perf: Union[Performance, None]): + """ + The method is intended to get main score info from Performance class + """ + if perf is None: + return None, None + assert isinstance(perf, Performance) + score = perf.score + assert isinstance(score, ScoreMetric) + assert isinstance(score.name, str) and score.name, f'Wrong score name "{score.name}"' + return score.name, score.value diff --git a/tests/test_suite/training_tests_helper.py b/tests/test_suite/training_tests_helper.py new file mode 100644 index 00000000000..7acf0906882 --- /dev/null +++ b/tests/test_suite/training_tests_helper.py @@ -0,0 +1,330 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import itertools +from abc import ABC, abstractmethod +from collections import OrderedDict +from copy import deepcopy +from pprint import pformat +from typing import Any, Dict, List, Optional, Type + +from .logging import get_logger +from .training_test_case import ( + OTXTestCaseInterface, + generate_otx_integration_test_case_class, +) +from .training_tests_actions import get_default_test_action_classes +from .training_tests_common import DEFAULT_FIELD_VALUE_FOR_USING_IN_TEST + +logger = get_logger() + + +class OTXTrainingTestInterface(ABC): + """ + The interface for all OTX training tests + (both reallife training tests and integration training tests) + """ + + @classmethod + @abstractmethod + def get_list_of_tests(cls, usecase: Optional[str] = None): + raise NotImplementedError("The method is not implemented") + + +class OTXTestCreationParametersInterface(ABC): + """ + The interface for classes that gives parameters for creating + OTX training tests. + It is used as the input value for OTXTestHelper class that + makes most part of functionality for the training tests. + """ + + @abstractmethod + def test_bunches(self) -> List[Dict[str, Any]]: + """ + The method should return test bunches struct. + It should be a list of dicts, each dict contains info on one "test bunch", + e.g. + ``` + [ + dict( + model_name=[ + 'gen3_mobilenetV2_SSD', + 'gen3_mobilenetV2_ATSS', + 'gen3_resnet50_VFNet', + ], + dataset_name='dataset1_tiled_shortened_500_A', + usecase='precommit', + ), + ... + ] + ``` + Note that the dict-s are passed to the tests as is through the parameter 'test_parameters' + -- see the method OTXTestHelper.get_list_of_tests below and the fixture + fixtures.current_test_parameters_fx. + """ + raise NotImplementedError("The method is not implemented") + + @abstractmethod + def test_case_class(self) -> Type[OTXTestCaseInterface]: + """ + The method returns a class that will be used as a Test Case class + for training tests. + Note that it should return a class itself (not an instance of the class). + + Typically OTX Test Case class should be generated by the function + training_test_case.generate_otx_integration_test_case_class. + + Note that the function receives as the parameter the list of action + classes -- see the function + training_tests_actions.get_default_test_action_classes, + it returns the default test action classes. + """ + raise NotImplementedError("The method is not implemented") + + @abstractmethod + def short_test_parameters_names_for_generating_id(self) -> OrderedDict: + """ + The method returns an OrderedDict that is used for generating string id-s of tests + by test parameters, received from test bunches dicts: + * keys of the OrderedDict should be the string keys of test bunches dict-s that should be + used for generating id-s + * values of the OrderedDict should be the strings that will be used as names of test parameters + + See the function OTXTestHelper._generate_test_id below. + """ + raise NotImplementedError("The method is not implemented") + + @abstractmethod + def test_parameters_defining_test_case_behavior(self) -> List[str]: + """ + The method returns a list of strings -- names of the test parameters + (i.e. keys of test bunches dicts) that define test case behavior. + + When several test cases are handled, if the next test has these parameters + the same as for the previous test, the test case class is re-used for the next test. + This allows re-using the result of previous test stages in the next test stages. + """ + raise NotImplementedError("The method is not implemented") + + @abstractmethod + def default_test_parameters(self) -> Dict[str, Any]: + """ + The method returns a dict that points for test parameters + the default values. + + If some dict in test bunches does not have a field that is pointed + in the dict returned by default_test_parameters, the value for the field is + set by the default value. + """ + raise NotImplementedError("The method is not implemented") + + +class DefaultOTXTestCreationParametersInterface(OTXTestCreationParametersInterface): + """ + The default implementation of some of the parameters for creation training tests. + """ + + def test_case_class(self) -> Type[OTXTestCaseInterface]: + return generate_otx_integration_test_case_class(get_default_test_action_classes()) + + def short_test_parameters_names_for_generating_id(self) -> OrderedDict: + DEFAULT_SHORT_TEST_PARAMETERS_NAMES_FOR_GENERATING_ID = OrderedDict( + [ + ("test_stage", "ACTION"), + ("model_name", "model"), + ("dataset_name", "dataset"), + ("num_training_iters", "num_iters"), + ("batch_size", "batch"), + ("usecase", "usecase"), + ] + ) + return deepcopy(DEFAULT_SHORT_TEST_PARAMETERS_NAMES_FOR_GENERATING_ID) + + def test_parameters_defining_test_case_behavior(self) -> List[str]: + DEFAULT_TEST_PARAMETERS_DEFINING_IMPL_BEHAVIOR = [ + "model_name", + "dataset_name", + "num_training_iters", + "batch_size", + ] + return deepcopy(DEFAULT_TEST_PARAMETERS_DEFINING_IMPL_BEHAVIOR) + + def default_test_parameters(self) -> Dict[str, Any]: + DEFAULT_TEST_PARAMETERS = { + "num_training_iters": 1, + "batch_size": 2, + } + return deepcopy(DEFAULT_TEST_PARAMETERS) + + +class OTXTestHelper: + """ + The main class helping creating OTX training tests. + + The instance of this class (with proper test_creation_parameters) should be + created as a static field for the test class making training tests + -- the class should be derived from the interface OTXTrainingTestInterface + and forward the call of the class method get_list_of_tests to the helper + (see method get_list_of_tests below). + + The most important method of the class are + * get_list_of_tests -- allows pytest trick generating test parameters for + the test class + * get_test_case -- gets an instance of the test case class for the current test parameters, + allows re-using the instance between several tests. + """ + + class _Cache: + def __init__(self): + self._cache_parameters = {} + self._cached_value = None + + def get(self): + return self._cached_value + + def set(self, params, value): + logger.debug(f"cache.set new value for parameters {params}") + self._cache_parameters = deepcopy(params) + self._cached_value = value + + def has_same_params(self, params): + res = self._cache_parameters == params + res_str = "==" if res else "!=" + logger.debug( + f"cache.has_same_params: " f"cache_parameters={self._cache_parameters} {res_str} {params}, res={res}" + ) + return res + + def __init__(self, test_creation_parameters: OTXTestCreationParametersInterface): + assert isinstance(test_creation_parameters, OTXTestCreationParametersInterface) + + self.test_case_class = test_creation_parameters.test_case_class() + self.test_bunches = test_creation_parameters.test_bunches() + + self.short_test_parameters_names_for_generating_id = ( + test_creation_parameters.short_test_parameters_names_for_generating_id() + ) + self.test_parameters_defining_test_case_behavior = ( + test_creation_parameters.test_parameters_defining_test_case_behavior() + ) + self.default_test_parameters = test_creation_parameters.default_test_parameters() + + self._cache = OTXTestHelper._Cache() + + assert issubclass(self.test_case_class, OTXTestCaseInterface) + assert isinstance(self.short_test_parameters_names_for_generating_id, OrderedDict) + assert all( + isinstance(k, str) and isinstance(v, str) + for k, v in self.short_test_parameters_names_for_generating_id.items() + ) + assert "test_stage" in self.short_test_parameters_names_for_generating_id + assert "model_name" in self.short_test_parameters_names_for_generating_id + assert "dataset_name" in self.short_test_parameters_names_for_generating_id + assert "usecase" in self.short_test_parameters_names_for_generating_id + + assert isinstance(self.test_parameters_defining_test_case_behavior, list) + assert all(isinstance(s, str) for s in self.test_parameters_defining_test_case_behavior) + + assert isinstance(self.default_test_parameters, dict) + assert all(isinstance(k, str) for k in self.default_test_parameters.keys()) + + def _get_list_of_test_stages(self): + return self.test_case_class.get_list_of_test_stages() + + def _fill_test_parameters_default_values(self, test_parameters): + for key, default_val in self.default_test_parameters.items(): + val = test_parameters.get(key) + if val is None or val == DEFAULT_FIELD_VALUE_FOR_USING_IN_TEST: + test_parameters[key] = default_val + + def _generate_test_id(self, test_parameters): + param_name_to_short_name = self.short_test_parameters_names_for_generating_id + id_parts = [] + for par_name, short_par_name in param_name_to_short_name.items(): + if par_name not in test_parameters: + raise ValueError( + f"Test parameters do not contain key '{par_name}', " f"test_parameters={pformat(test_parameters)}" + ) + id_parts.append(f"{short_par_name}-{test_parameters[par_name]}") + + return ",".join(id_parts) + + def get_list_of_tests(self, usecase: Optional[str] = None): + """ + The functions generates the lists of values for the tests from the field test_bunches of the class. + + The function returns two lists + * argnames -- a tuple with names of the test parameters, at the moment it is + a one-element tuple with the parameter name "test_parameters" + * argvalues -- list of tuples, each tuple has the same len as argname tuple, + at the moment it is a one-element tuple with the dict `test_parameters` + that stores the parameters of the test + * ids -- list of strings with ids corresponding the parameters of the tests + each id is a string generated from the corresponding test_parameters + value -- see the functions _generate_test_id + + The lists argvalues and ids will have the same length. + + If the parameter `usecase` is set, it makes filtering by usecase field of test bunches. + """ + test_bunches = self.test_bunches + assert all(isinstance(el, dict) for el in test_bunches) + + argnames = ("test_parameters",) + argvalues = [] + ids = [] + for el in test_bunches: + el_model_name = el.get("model_name") + el_dataset_name = el.get("dataset_name") + el_usecase = el.get("usecase") + if usecase is not None and el_usecase != usecase: + continue + if isinstance(el_model_name, (list, tuple)): + model_names = el_model_name + else: + model_names = [el_model_name] + if isinstance(el_dataset_name, (list, tuple)): + dataset_names = el_dataset_name + else: + dataset_names = [el_dataset_name] + + model_dataset_pairs = list(itertools.product(model_names, dataset_names)) + + for cur_model_name, cur_dataset_name in model_dataset_pairs: + for test_stage in self._get_list_of_test_stages(): + test_parameters = deepcopy(el) + test_parameters["test_stage"] = test_stage + test_parameters["model_name"] = cur_model_name + test_parameters["dataset_name"] = cur_dataset_name + self._fill_test_parameters_default_values(test_parameters) + argvalues.append((test_parameters,)) + ids.append(self._generate_test_id(test_parameters)) + + return argnames, argvalues, ids + + def get_test_case(self, test_parameters, params_factories_for_test_actions): + """ + The method returns an instance of test case class for the current test parameters. + If the main test parameters (pointed by the field test_parameters_defining_test_case_behavior) + are the same as in the previous test, the instance of the test case is re-used from the previous test. + It allows re-using results of previous tests stages in the next test stages. + + If a new instance of test case should be created, the method creates a class pointed + by the field test_case_class. The parameter params_factories_for_test_actions is passed + to the test case constructor + (note that params_factories_for_test_actions are factories, not structs, to + create the parameters only when this is required) + """ + params_defining_cache = {k: test_parameters[k] for k in self.test_parameters_defining_test_case_behavior} + if not self._cache.has_same_params(params_defining_cache): + logger.info("OTXTestHelper: parameters were changed -- updating cache") + logger.info(f"OTXTestHelper: before creating test case (class {self.test_case_class})") + test_case = self.test_case_class(params_factories_for_test_actions) + logger.info(f"OTXTestHelper: after creating test case (class {self.test_case_class})") + self._cache.set(params_defining_cache, test_case) + else: + logger.info("OTXTestHelper: parameters were not changed -- cache is kept") + + return self._cache.get() diff --git a/tests/test_suite/training_tests_stage.py b/tests/test_suite/training_tests_stage.py new file mode 100644 index 00000000000..cf46e855ce9 --- /dev/null +++ b/tests/test_suite/training_tests_stage.py @@ -0,0 +1,431 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from abc import ABC, abstractmethod +from collections import OrderedDict +from pprint import pformat +from typing import Any, Callable, Dict, Optional + +import pytest + +from .e2e_test_system import DataCollector +from .logging import get_logger +from .training_tests_actions import BaseOTXTestAction +from .training_tests_common import REALLIFE_USECASE_CONSTANT + +logger = get_logger() + + +def get_value_from_dict_by_dot_separated_address(struct, address): + def _get(cur_struct, addr): + assert isinstance(addr, list) + if not addr: + return cur_struct + assert isinstance(cur_struct, dict) + if addr[0] not in cur_struct: + raise ValueError(f"Cannot find address {address} in struct {struct}: {addr[0]} is absent in {cur_struct}") + return _get(cur_struct[addr[0]], addr[1:]) + + assert isinstance(address, str), f"The parameter address should be string, address={address}" + return _get(struct, address.split(".")) + + +class Validator: + """ + The class receives info on results metric of the current test stage and + compares it with the expected metrics. + """ + + def __init__(self, cur_test_expected_metrics_callback: Optional[Callable[[], Dict]]): + self.cur_test_expected_metrics_callback = cur_test_expected_metrics_callback + + # TODO(lbeynens): add a method to extract dependency info from expected metrics + # to add the stages we depend on to the dependency list. + + @staticmethod + def _get_min_max_value_from_expected_metrics(cur_metric_requirements: Dict, test_results_storage: Dict): + """ + The method gets requirement for some metric and convert it to the triplet + (target_value, min_value, max_value). + Note that the target_value may be pointed either by key 'target_value' (in this case it is float), + or by the key 'base', in this case it is a dot-separated address to another value in the + storage of previous stages' results `test_results_storage`. + + Note that the range for the metric values may be pointed by key 'max_diff', + in this case the range will be [target_value - max_diff, target_value + max_diff] + (inclusively). + + But also the range may be pointed by keys 'max_diff_if_less_threshold' and + 'max_diff_if_greater_threshold', in this case the range is + [target_value - max_diff_if_less_threshold, target_value + max_diff_if_greater_threshold] + (also inclusively). This allows to point non-symmetric ranges w.r.t. the target_value. + + Also note that if one of 'max_diff_if_less_threshold' and 'max_diff_if_greater_threshold' + is absent, it is set to `+infinity`, so the range will be bounded from one side + (but not both of them, this will be an error) + """ + keys = set(cur_metric_requirements.keys()) + if "target_value" not in keys and "base" not in keys: + raise ValueError( + f'Wrong cur_metric_requirements: either "target_value" or "base" ' + f" should be pointed in the structure, whereas " + f"cur_metric_requirements={pformat(cur_metric_requirements)}" + ) + if "target_value" in keys and "base" in keys: + raise ValueError( + f'Wrong cur_metric_requirements: either "target_value" or "base" ' + f" should be pointed in the structure, but not both, whereas " + f"cur_metric_requirements={pformat(cur_metric_requirements)}" + ) + if ( + ("max_diff" not in keys) + and ("max_diff_if_less_threshold" not in keys) + and ("max_diff_if_greater_threshold" not in keys) + ): + raise ValueError( + f'Wrong cur_metric_requirements: either "max_diff" or one/two of ' + f'"max_diff_if_less_threshold" and "max_diff_if_greater_threshold" should be ' + f"pointed in the structure, whereas " + f"cur_metric_requirements={pformat(cur_metric_requirements)}" + ) + + if ("max_diff" in keys) and ("max_diff_if_less_threshold" in keys or "max_diff_if_greater_threshold" in keys): + raise ValueError( + f'Wrong cur_metric_requirements: either "max_diff" or one/two of ' + f'"max_diff_if_less_threshold" and "max_diff_if_greater_threshold" should be ' + f"pointed in the structure, but not both, whereas " + f"cur_metric_requirements={pformat(cur_metric_requirements)}" + ) + + if "target_value" in cur_metric_requirements: + target_value = float(cur_metric_requirements["target_value"]) + elif "base" in cur_metric_requirements: + base_metric_address = cur_metric_requirements["base"] + target_value = get_value_from_dict_by_dot_separated_address(test_results_storage, base_metric_address) + target_value = float(target_value) + else: + raise RuntimeError(f"ERROR: Wrong parsing of metric requirements {cur_metric_requirements}") + + if "max_diff" in cur_metric_requirements: + max_diff = cur_metric_requirements["max_diff"] + max_diff = float(max_diff) + if not max_diff >= 0: + raise ValueError(f"Wrong max_diff {max_diff} -- it should be a non-negative number") + return (target_value, target_value - max_diff, target_value + max_diff) + + max_diff_if_less_threshold = cur_metric_requirements.get("max_diff_if_less_threshold") + max_diff_if_greater_threshold = cur_metric_requirements.get("max_diff_if_greater_threshold") + if max_diff_if_less_threshold is None and max_diff_if_greater_threshold is None: + raise ValueError( + f"Wrong cur_metric_requirements: all of max_diff, max_diff_if_less_threshold, and " + f"max_diff_if_greater_threshold are None, " + f"cur_metric_requirements={pformat(cur_metric_requirements)}" + ) + + if max_diff_if_greater_threshold is not None: + max_diff_if_greater_threshold = float(max_diff_if_greater_threshold) + if not max_diff_if_greater_threshold >= 0: + raise ValueError( + f"Wrong max_diff_if_greater_threshold {max_diff_if_greater_threshold} " + f"-- it should be a non-negative number" + ) + + max_value = target_value + max_diff_if_greater_threshold + else: + max_value = None + + if max_diff_if_less_threshold is not None: + max_diff_if_less_threshold = float(max_diff_if_less_threshold) + if not max_diff_if_less_threshold >= 0: + raise ValueError( + f"Wrong max_diff_if_less_threshold {max_diff_if_less_threshold} " + f"-- it should be a non-negative number" + ) + + min_value = target_value - max_diff_if_less_threshold + else: + min_value = None + + return (target_value, min_value, max_value) + + @staticmethod + def _compare( + current_metric: float, + cur_res_addr: str, + target_value: float, + min_value: Optional[float], + max_value: Optional[float], + ): + assert all(isinstance(v, float) for v in [current_metric, target_value]) + assert all((v is None) or isinstance(v, float) for v in [min_value, max_value]) + + if min_value is not None and max_value is not None: + assert min_value <= target_value <= max_value + + if min_value <= current_metric <= max_value: + logger.info( + f"Validation: passed: The metric {cur_res_addr} is in the acceptable range " + f"near the target value {target_value}: " + f"{current_metric} is in [{min_value}, {max_value}]" + ) + is_passed = True + cur_fail_reason = None + else: + cur_fail_reason = ( + f"Validation: failed: The metric {cur_res_addr} is NOT in the acceptable range " + f"near the target value {target_value}: " + f"{current_metric} is NOT in [{min_value}, {max_value}]" + ) + logger.error(cur_fail_reason) + is_passed = False + return is_passed, cur_fail_reason + + assert (min_value is not None) or (max_value is not None) + if min_value is not None: + cmp_op = lambda x: x >= min_value # noqa: E731 + cmp_str_true = "greater or equal" + cmp_op_str_true = ">=" + cmp_op_str_false = "<" + threshold = min_value + else: + assert max_value is not None + cmp_op = lambda x: x <= max_value # noqa: E731 + cmp_str_true = "less or equal" + cmp_op_str_true = "<=" + cmp_op_str_false = ">" + threshold = max_value + acceptable_error = abs(threshold - target_value) + if cmp_op(current_metric): + logger.info( + f"Validation: passed: The metric {cur_res_addr} is {cmp_str_true} " + f"the target value {target_value} with acceptable error {acceptable_error}: " + f"{current_metric} {cmp_op_str_true} {threshold}" + ) + is_passed = True + cur_fail_reason = None + else: + cur_fail_reason = ( + f"Validation: failed: The metric {cur_res_addr} is NOT {cmp_str_true} " + f"the target value {target_value} with acceptable error {acceptable_error}: " + f"{current_metric} {cmp_op_str_false} {threshold}" + ) + logger.error(cur_fail_reason) + is_passed = False + return is_passed, cur_fail_reason + + def validate(self, current_result: Dict, test_results_storage: Dict): + """ + The method validates results of the current test. + :param current_result -- dict with result of the current test + :param test_results_storage -- dict with results of previous tests + of this test case + (e.g. the same training parameters) + + The function returns nothing, but may raise exceptions to fail the test. + If the structure stored expected metrics is wrong, the function raises ValueError. + """ + if self.cur_test_expected_metrics_callback is None: + # most probably, it is not a reallife test + logger.info( + f"Validation: skipped, since there should not be expected metrics for this test, " + f'most probably the test is not run in "{REALLIFE_USECASE_CONSTANT}" usecase' + ) + return + + logger.info("Validation: begin") + + # calling the callback to receive expected metrics for the current test + cur_test_expected_metrics = self.cur_test_expected_metrics_callback() + + assert isinstance( + cur_test_expected_metrics, dict + ), f"Wrong current test expected metric: {cur_test_expected_metrics}" + logger.debug(f"Validation: received cur_test_expected_metrics={pformat(cur_test_expected_metrics)}") + is_passed = True + fail_reasons = [] + for k, v in cur_test_expected_metrics.items(): + # TODO(lbeynens): add possibility to point a list of requirements for a metric + cur_res_addr = k + cur_metric_requirements = v + logger.info(f"Validation: begin check {cur_res_addr}") + try: + current_metric = get_value_from_dict_by_dot_separated_address(current_result, cur_res_addr) + current_metric = float(current_metric) + except (ValueError, TypeError) as e: + raise ValueError(f"Cannot get metric {cur_res_addr} from the current result {current_result}") from e + + logger.debug(f"current_metric = {current_metric}") + try: + ( + target_value, + min_value, + max_value, + ) = self._get_min_max_value_from_expected_metrics(cur_metric_requirements, test_results_storage) + except (ValueError, TypeError) as e: + raise ValueError(f"Error when parsing expected metrics for the metric {cur_res_addr}") from e + + cur_is_passed, cur_fail_reason = self._compare( + current_metric, cur_res_addr, target_value, min_value, max_value + ) + if not cur_is_passed: + is_passed = False + fail_reasons.append(cur_fail_reason) + + logger.info(f"Validation: end check {cur_res_addr}") + + logger.info(f"Validation: end, result={is_passed}") + if not is_passed: + fail_reasons_str = "\n".join(fail_reasons) + pytest.fail(f"Validation failed:\n{fail_reasons_str}") + + +class OTXTestStagesStorageInterface(ABC): + @abstractmethod + def get_stage(self, name: str) -> "OTXTestStage": + raise NotImplementedError("The method get_stage is not implemented") + + +class OTXTestStage: + """ + OTXTestStage -- auxiliary class that + 1. Allows to set up dependency between test stages: before the main action of a test stage is run, all the actions + for the stages that are pointed in 'depends' list are called beforehand; + 2. Runs for each test stage its main action only once: the main action is run inside try-except clause, and + 2.1. if the action was executed without exceptions, a flag `was_processed` is set, the results of the action + are kept, and the next time the stage is called no action is executed; + 2.2. if the action raised an exception, the exception is stored, the flag `was_processed` is set, and the next + time the stage is called the exception is re-raised. + """ + + def __init__(self, action: BaseOTXTestAction, stages_storage: OTXTestStagesStorageInterface): + self.was_processed = False + self.stored_exception: Optional[Exception] = None + self.action = action + self.stages_storage = stages_storage + self.stage_results: Dict[str, Any] = {} + assert isinstance(self.stages_storage, OTXTestStagesStorageInterface) + assert isinstance(self.action, BaseOTXTestAction) + + def __str__(self): + return ( + f"{type(self).__name__}(" + f"action={self.action}, " + f"was_processed={self.was_processed}, " + f"stored_exception={self.stored_exception}, " + f"stage_results.keys={list(self.stage_results.keys())}, " + f"id(stages_storage)={id(self.stages_storage)}" + f")" + ) + + @property + def name(self): + return self.action.name + + def get_depends_stages(self): + logger.debug(f"get_depends_stages for stage {self.name}: begin") + depends_stages_names = self.action.depends_stages_names + assert isinstance(depends_stages_names, list) + assert all(isinstance(v, str) for v in depends_stages_names) + + stages = [] + for stage_name in depends_stages_names: + logger.debug(f'get_depends_stages: get stage with name "{stage_name}"') + cur_stage = self.stages_storage.get_stage(stage_name) + assert isinstance(cur_stage, OTXTestStage), f'Wrong stage for stage_name="{stage_name}"' + assert ( + cur_stage.name == stage_name + ), f'For stage_name="{stage_name}" got the stage with name="{cur_stage.name}"' + logger.debug(f'get_depends_stages: cur_stage="{cur_stage}"') + stages.append(cur_stage) + logger.debug(f"get_depends_stages for stage {self.name}: end") + return stages + + def _reraise_stage_exception_if_was_failed(self): + assert ( + self.was_processed + ), "The method _reraise_stage_exception_if_was_failed should be used only for stages that were processed" + if self.stored_exception is None: + # nothing to do here + return + + logger.warning( + f"In stage {self.name}: found that previous call of the stage " "caused exception -- re-raising it" + ) + raise self.stored_exception + + def _run_validation(self, test_results_storage: Dict, validator: Optional[Validator]): + if not self.action.with_validation: + return + if validator is None: + logger.debug( + "The validator is None -- the validation should be skipped, " + "most probably this test stage was run from a dependency chain" + ) + return + + validator.validate(self.stage_results, test_results_storage) + + def run_once( + self, + data_collector: DataCollector, + test_results_storage: OrderedDict, + validator: Optional[Validator], + ): + logger.info(f'Begin stage "{self.name}"') + assert isinstance(test_results_storage, OrderedDict) + logger.debug(f'For test stage "{self.name}": test_results_storage.keys = {list(test_results_storage.keys())}') + + for dep_stage in self.get_depends_stages(): + # Processing all dependency stages of the current test. + # Note that + # * the stages may run their own dependency stages -- they will compose so called "dependency chain" + # * the dependency stages are run with `validator = None` + # to avoid validation of stages that are run from the dependency chain. + logger.debug(f'For test stage "{self.name}": Before running dep. stage "{dep_stage.name}"') + dep_stage.run_once(data_collector, test_results_storage, validator=None) + logger.debug(f'For test stage "{self.name}": After running dep. stage "{dep_stage.name}"') + + if self.was_processed: + self._reraise_stage_exception_if_was_failed() + # if we are here, then the stage was processed without exceptions + logger.info(f"The stage {self.name} was already processed SUCCESSFULLY") + + # Run validation here for the rare case if this test now is being run *not* from a dependency chain + # (i.e. the test is run with `validator != None`), + # but the test already has been run from some dependency chain earlier. + self._run_validation(test_results_storage, validator) + + logger.info(f'End stage "{self.name}"') + return + + if self.name in test_results_storage: + raise RuntimeError( + f'Error: For test stage "{self.name}": ' + f"another OTXTestStage with name {self.name} has been run already" + ) + + try: + logger.info(f'For test stage "{self.name}": Before running main action') + self.stage_results = self.action(data_collector=data_collector, results_prev_stages=test_results_storage) + logger.info(f'For test stage "{self.name}": After running main action') + self.was_processed = True + test_results_storage[self.name] = self.stage_results + logger.debug( + f'For test stage "{self.name}": after addition test_results_storage.keys = ' + f"{list(test_results_storage.keys())}" + ) + except Exception as e: + logger.info( + f'For test stage "{self.name}": After running action for stage {self.name} -- CAUGHT EXCEPTION:\n{e}' + ) + logger.info(f'End stage "{self.name}"') + self.stored_exception = e + self.was_processed = True + raise e + + # The validation step is made outside the central try...except clause, since if the test was successful, but + # the quality numbers were lower than expected, the result of the stage still may be re-used + # in other stages. + self._run_validation(test_results_storage, validator) + logger.info(f'End stage "{self.name}"') diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 6a16273c024..79931efa777 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,2 +1,3 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2021-2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algo/__init__.py b/tests/unit/algo/__init__.py deleted file mode 100644 index 0f5595bbeb3..00000000000 --- a/tests/unit/algo/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit test of custom algo modules of OTX tasks.""" diff --git a/tests/unit/algo/action_classification/__init__.py b/tests/unit/algo/action_classification/__init__.py deleted file mode 100644 index 49980011de5..00000000000 --- a/tests/unit/algo/action_classification/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom algo modules of OTX Action Classification task.""" diff --git a/tests/unit/algo/action_classification/backbones/__init__.py b/tests/unit/algo/action_classification/backbones/__init__.py deleted file mode 100644 index eab964fd97f..00000000000 --- a/tests/unit/algo/action_classification/backbones/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom backbones of OTX Action Classification task.""" diff --git a/tests/unit/algo/action_classification/backbones/test_movinet.py b/tests/unit/algo/action_classification/backbones/test_movinet.py deleted file mode 100644 index 792387d52e2..00000000000 --- a/tests/unit/algo/action_classification/backbones/test_movinet.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of MoViNet backbone.""" - -import pytest -import torch -from otx.algo.action_classification.backbones.movinet import OTXMoViNet - - -class TestMoViNet: - @pytest.fixture() - def fxt_movinet(self) -> OTXMoViNet: - return OTXMoViNet() - - def test_forward(self, fxt_movinet: OTXMoViNet) -> None: - x = torch.randn(1, 3, 8, 224, 224) - assert fxt_movinet(x).shape == torch.Size([1, 480, 1, 1, 1]) diff --git a/tests/unit/algo/action_classification/heads/__init__.py b/tests/unit/algo/action_classification/heads/__init__.py deleted file mode 100644 index 909fd357209..00000000000 --- a/tests/unit/algo/action_classification/heads/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom heads of OTX Action Classification task.""" diff --git a/tests/unit/algo/action_classification/heads/test_movinet_head.py b/tests/unit/algo/action_classification/heads/test_movinet_head.py deleted file mode 100644 index 5d1f9568a1e..00000000000 --- a/tests/unit/algo/action_classification/heads/test_movinet_head.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of MoViNet Head.""" - - -import pytest -import torch -from otx.algo.action_classification.heads.movinet_head import MoViNetHead - - -class TestMoViNetHead: - @pytest.fixture() - def fxt_movinet_head(self) -> MoViNetHead: - return MoViNetHead( - 5, - 24, - 48, - {"type": "CrossEntropyLoss", "loss_weight": 1.0}, - average_clips="prob", - ) - - def test_forward(self, fxt_movinet_head: MoViNetHead) -> None: - fxt_movinet_head.init_weights() - x = torch.randn(5, 24, 1, 1, 1) - assert fxt_movinet_head(x).shape == torch.Size([5, 5]) diff --git a/tests/unit/algo/action_classification/recognizers/__init__.py b/tests/unit/algo/action_classification/recognizers/__init__.py deleted file mode 100644 index c951ad67227..00000000000 --- a/tests/unit/algo/action_classification/recognizers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom recognizers of OTX Action Classification task.""" diff --git a/tests/unit/algo/action_classification/recognizers/test_movinet_recognizer.py b/tests/unit/algo/action_classification/recognizers/test_movinet_recognizer.py deleted file mode 100644 index 861e4ab97ec..00000000000 --- a/tests/unit/algo/action_classification/recognizers/test_movinet_recognizer.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of movinet recognizer.""" - - -import pytest -import torch -from otx.algo.action_classification.recognizers.movinet_recognizer import MoViNetRecognizer - - -class TestMoViNetRecognizer: - @pytest.fixture() - def fxt_movinet_recognizer(self, mocker) -> MoViNetRecognizer: - mocker.patch.object(MoViNetRecognizer, "__init__", return_value=None) - return MoViNetRecognizer() - - def test_state_dict_hook(self, fxt_movinet_recognizer: MoViNetRecognizer) -> None: - state_dict = {"cls_head.conv1": torch.Tensor([0]), "backbone.conv2": torch.Tensor([1])} - fxt_movinet_recognizer.state_dict_hook(torch.nn.Module(), state_dict) - assert state_dict["conv1"] == torch.Tensor([0]) - assert state_dict["conv2"] == torch.Tensor([1]) - - def test_load_stete_dict_pre_hook(self, fxt_movinet_recognizer: MoViNetRecognizer) -> None: - state_dict = {"classifier.conv1": torch.Tensor([0]), "model.conv2": torch.Tensor([1])} - fxt_movinet_recognizer.load_state_dict_pre_hook(torch.nn.Module(), state_dict, "model.") - assert state_dict["cls_head.classifier.conv1"] == torch.Tensor([0]) - assert state_dict["model.backbone.conv2"] == torch.Tensor([1]) diff --git a/tests/unit/algo/callbacks/test_adaptive_train_scheduling.py b/tests/unit/algo/callbacks/test_adaptive_train_scheduling.py deleted file mode 100644 index 85eb23f8e85..00000000000 --- a/tests/unit/algo/callbacks/test_adaptive_train_scheduling.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import logging as log -from unittest.mock import MagicMock - -from lightning import LightningModule, Trainer -from lightning.pytorch.callbacks.early_stopping import EarlyStopping -from lightning.pytorch.cli import ReduceLROnPlateau -from lightning.pytorch.utilities.types import LRSchedulerConfig -from otx.algo.callbacks.adaptive_train_scheduling import AdaptiveTrainScheduling -from torch.utils.data import DataLoader - - -class TestAdaptiveTrainScheduling: - def test_callback(self, caplog) -> None: - callback = AdaptiveTrainScheduling(max_interval=5, decay=-0.025) - - mock_trainer = MagicMock(spec=Trainer) - mock_pl_module = MagicMock(spec=LightningModule) - - mock_dataloader = MagicMock(spec=DataLoader) - mock_dataloader.__len__.return_value = 32 - mock_trainer.train_dataloader = mock_dataloader - - mock_trainer.max_epochs = 10 - mock_trainer.check_val_every_n_epoch = 1 - mock_trainer.log_every_n_steps = 50 - - mock_callback = MagicMock(spec=EarlyStopping) - mock_callback.patience = 5 - mock_trainer.callbacks = [mock_callback] - - mock_lr_scheduler_config = MagicMock(spec=LRSchedulerConfig) - mock_lr_scheduler_config.scheduler = MagicMock(spec=ReduceLROnPlateau) - mock_lr_scheduler_config.scheduler.patience = 4 - mock_lr_scheduler_config.frequency = 1 - mock_lr_scheduler_config.interval = "epoch" - mock_trainer.lr_scheduler_configs = [mock_lr_scheduler_config] - - with caplog.at_level(log.WARNING): - callback.on_train_start(trainer=mock_trainer, pl_module=mock_pl_module) - assert mock_trainer.check_val_every_n_epoch != 1 # Adaptively updated, in this case, 2 - assert mock_trainer.callbacks[0].patience != 5 - assert mock_trainer.lr_scheduler_configs[0].frequency != 1 - assert mock_trainer.lr_scheduler_configs[0].scheduler.patience == 1 # int((4+1) / 2) - 1 = 1 - assert mock_trainer.log_every_n_steps == 32 # Equal to len(train_dataloader) - assert len(caplog.records) == 5 # Warning two times - - callback.on_train_end(trainer=mock_trainer, pl_module=mock_pl_module) - - # Restore temporarily updated values - assert mock_trainer.check_val_every_n_epoch == 1 - assert mock_trainer.log_every_n_steps == 50 - assert mock_trainer.callbacks[0].patience == 5 - assert mock_trainer.lr_scheduler_configs[0].frequency == 1 - assert mock_trainer.lr_scheduler_configs[0].scheduler.patience == 4 diff --git a/tests/unit/algo/callbacks/test_iteration_timer.py b/tests/unit/algo/callbacks/test_iteration_timer.py deleted file mode 100644 index c17de5dab66..00000000000 --- a/tests/unit/algo/callbacks/test_iteration_timer.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from unittest.mock import MagicMock, patch - -import pytest -from otx.algo.callbacks.iteration_timer import IterationTimer - - -class TestIterationTimer: - @pytest.mark.parametrize("phase", ["train", "validation", "test"]) - @patch("otx.algo.callbacks.iteration_timer.time") - def test_all_phases(self, mock_time, phase) -> None: - mock_trainer = MagicMock() - mock_batch = MagicMock() - batch_size = 64 - mock_batch.batch_size = batch_size - mock_outputs = MagicMock() - - timer = IterationTimer() - - epoch_len = 3 - time_span = 10 - batch_len = 2 - - for epoch_idx in range(epoch_len): - # Timestamp - # 0 + time_span * epoch_idx, batch_start - # 1 + time_span * epoch_idx, batch_end - # 2 + time_span * epoch_idx, ... - # 3 + time_span * epoch_idx, - mock_time.side_effect = [time + time_span * epoch_idx for time in range(2 * batch_len)] - - mock_pl_module = MagicMock() - - # Timer member variables should be reset. - # Without this, test should be failed. - getattr(timer, f"on_{phase}_epoch_start")( - trainer=mock_trainer, - pl_module=mock_pl_module, - ) - - for batch_idx in range(batch_len): - getattr(timer, f"on_{phase}_batch_start")( - trainer=mock_trainer, - pl_module=mock_pl_module, - batch=mock_batch, - batch_idx=batch_idx, - ) - - if batch_idx == 0: - assert not mock_pl_module.log.called, "Cannot log data and iter time at the first batch step" - else: - mock_pl_module.log.assert_called_with( - name=f"{phase}/data_time", - value=1, - prog_bar=timer.prog_bar, - on_step=timer.on_step, - on_epoch=timer.on_epoch, - batch_size=batch_size, - ) - - getattr(timer, f"on_{phase}_batch_end")( - trainer=mock_trainer, - pl_module=mock_pl_module, - outputs=mock_outputs, - batch=mock_batch, - batch_idx=batch_idx, - ) - - assert timer.start_time[phase] == time_span * epoch_idx + 2 * batch_idx - assert timer.end_time[phase] == time_span * epoch_idx + 2 * batch_idx + 1 - - if batch_idx == 0: - assert not mock_pl_module.log.called, "Cannot log data and iter time at the first batch step" - else: - mock_pl_module.log.assert_called_with( - name=f"{phase}/iter_time", - value=2, - prog_bar=timer.prog_bar, - on_step=timer.on_step, - on_epoch=timer.on_epoch, - batch_size=batch_size, - ) diff --git a/tests/unit/algo/classification/__init__.py b/tests/unit/algo/classification/__init__.py deleted file mode 100644 index 6a16273c024..00000000000 --- a/tests/unit/algo/classification/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algo/classification/backbones/__init__.py b/tests/unit/algo/classification/backbones/__init__.py deleted file mode 100644 index 916f3a44b27..00000000000 --- a/tests/unit/algo/classification/backbones/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algo/classification/backbones/test_otx_efficientnet.py b/tests/unit/algo/classification/backbones/test_otx_efficientnet.py deleted file mode 100644 index 3d4adefa7b5..00000000000 --- a/tests/unit/algo/classification/backbones/test_otx_efficientnet.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - - -import pytest -import torch -from otx.algo.classification.backbones.otx_efficientnet import OTXEfficientNet - - -class TestOTXEfficientNet: - @pytest.mark.parametrize("version", ["b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8"]) - def test_forward(self, version): - model = OTXEfficientNet(version, pretrained=None) - assert model(torch.randn(1, 3, 244, 244))[0].shape[-1] == 8 - assert model(torch.randn(1, 3, 244, 244))[0].shape[-2] == 8 diff --git a/tests/unit/algo/classification/backbones/test_otx_efficientnet_v2.py b/tests/unit/algo/classification/backbones/test_otx_efficientnet_v2.py deleted file mode 100644 index 8dcdeb3698a..00000000000 --- a/tests/unit/algo/classification/backbones/test_otx_efficientnet_v2.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import torch -from otx.algo.classification.backbones.otx_efficientnet_v2 import OTXEfficientNetV2 - - -class TestOTXEfficientNetV2: - def test_forward(self): - model = OTXEfficientNetV2() - model.init_weights() - assert model(torch.randn(1, 3, 244, 244))[0].shape == torch.Size([1, 1280, 8, 8]) - - def test_get_config_optim(self): - model = OTXEfficientNetV2() - assert model.get_config_optim([0.01])[0]["lr"] == 0.01 - assert model.get_config_optim(0.01)[0]["lr"] == 0.01 diff --git a/tests/unit/algo/classification/backbones/test_otx_mobilenet_v3.py b/tests/unit/algo/classification/backbones/test_otx_mobilenet_v3.py deleted file mode 100644 index 51e3a235022..00000000000 --- a/tests/unit/algo/classification/backbones/test_otx_mobilenet_v3.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import torch -from otx.algo.classification.backbones.otx_mobilenet_v3 import OTXMobileNetV3 - - -class TestOTXMobileNetV3: - def test_forward(self): - model = OTXMobileNetV3() - assert model(torch.randn(1, 3, 244, 244))[0].shape == torch.Size([1, 960, 8, 8]) - - def test_glob_feature_vector(self): - model = OTXMobileNetV3() - assert model._glob_feature_vector(torch.randn([1, 960, 8, 8]), "avg").shape == torch.Size([1, 960]) - assert model._glob_feature_vector(torch.randn([1, 960, 8, 8]), "max").shape == torch.Size([1, 960]) - assert model._glob_feature_vector(torch.randn([1, 960, 8, 8]), "avg+max").shape == torch.Size([1, 960]) - assert model._glob_feature_vector(torch.randn([1, 960, 8, 8]), "avg", reduce_dims=False).shape == torch.Size( - [1, 960, 1, 1], - ) diff --git a/tests/unit/algo/classification/conftest.py b/tests/unit/algo/classification/conftest.py deleted file mode 100644 index bf536b62815..00000000000 --- a/tests/unit/algo/classification/conftest.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -from __future__ import annotations - -import pytest -import torch -from mmpretrain.structures import DataSample -from omegaconf import DictConfig -from otx.core.data.dataset.classification import HLabelInfo, MulticlassClsBatchDataEntity -from otx.core.data.entity.base import ImageInfo -from torchvision import tv_tensors - - -@pytest.fixture() -def fxt_data_sample() -> DataSample: - data_sample = DataSample( - img_shape=(24, 24, 3), - gt_label=torch.zeros(6, dtype=torch.long), - ) - return [data_sample, data_sample] - - -@pytest.fixture() -def fxt_hlabel_data() -> HLabelInfo: - return HLabelInfo( - label_names=[ - "Heart", - "Spade", - "Heart_Queen", - "Heart_King", - "Spade_A", - "Spade_King", - ], - label_groups=[ - ["Heart", "Spade"], - ["Heart_Queen", "Heart_King"], - ["Spade_A", "Spade_King"], - ], - num_multiclass_heads=3, - num_multilabel_classes=0, - head_idx_to_logits_range={"0": (0, 2), "1": (2, 4), "2": (4, 6)}, - num_single_label_classes=0, - empty_multiclass_head_indices=[], - class_to_group_idx={ - "Heart": (0, 0), - "Spade": (0, 1), - "Heart_Queen": (1, 0), - "Heart_King": (1, 1), - "Spade_A": (2, 0), - "Spade_King": (2, 1), - }, - all_groups=[ - ["Heart", "Spade"], - ["Heart_Queen", "Heart_King"], - ["Spade_A", "Spade_King"], - ], - label_to_idx={ - "Heart": 0, - "Spade": 1, - "Heart_Queen": 2, - "Heart_King": 3, - "Spade_A": 4, - "Spade_King": 5, - }, - label_tree_edges=[ - ["Heart_Queen", "Heart"], - ["Heart_King", "Heart"], - ["Spade_A", "Spade"], - ["Spade_King", "Spade"], - ], - ) - - -@pytest.fixture() -def fxt_hlabel_multilabel_info() -> HLabelInfo: - return HLabelInfo( - label_names=[ - "Heart", - "Spade", - "Heart_Queen", - "Heart_King", - "Spade_A", - "Spade_King", - "Black_Joker", - "Red_Joker", - "Extra_Joker", - ], - label_groups=[ - ["Heart", "Spade"], - ["Heart_Queen", "Heart_King"], - ["Spade_A", "Spade_King"], - ["Black_Joker"], - ["Red_Joker"], - ["Extra_Joker"], - ], - num_multiclass_heads=3, - num_multilabel_classes=3, - head_idx_to_logits_range={"0": (0, 2), "1": (2, 4), "2": (4, 6)}, - num_single_label_classes=3, - empty_multiclass_head_indices=[], - class_to_group_idx={ - "Heart": (0, 0), - "Spade": (0, 1), - "Heart_Queen": (1, 0), - "Heart_King": (1, 1), - "Spade_A": (2, 0), - "Spade_King": (2, 1), - "Black_Joker": (3, 0), - "Red_Joker": (3, 1), - "Extra_Joker": (3, 2), - }, - all_groups=[ - ["Heart", "Spade"], - ["Heart_Queen", "Heart_King"], - ["Spade_A", "Spade_King"], - ["Black_Joker"], - ["Red_Joker"], - ["Extra_Joker"], - ], - label_to_idx={ - "Heart": 0, - "Spade": 1, - "Heart_Queen": 2, - "Heart_King": 3, - "Spade_A": 4, - "Spade_King": 5, - "Black_Joker": 6, - "Red_Joker": 7, - "Extra_Joker": 8, - }, - label_tree_edges=[ - ["Heart_Queen", "Heart"], - ["Heart_King", "Heart"], - ["Spade_A", "Spade"], - ["Spade_King", "Spade"], - ], - ) - - -@pytest.fixture() -def fxt_multiclass_cls_batch_data_entity() -> MulticlassClsBatchDataEntity: - batch_size = 2 - random_tensor = torch.randn((batch_size, 3, 224, 224)) - tv_tensor = tv_tensors.Image(data=random_tensor) - img_infos = [ImageInfo(img_idx=i, img_shape=(224, 224), ori_shape=(224, 224)) for i in range(batch_size)] - return MulticlassClsBatchDataEntity( - batch_size=2, - images=tv_tensor, - imgs_info=img_infos, - labels=[torch.tensor([0]), torch.tensor([1])], - ) - - -@pytest.fixture() -def fxt_config_mock() -> DictConfig: - pseudo_model_config = { - "backbone": { - "name": "dinov2_vits14_reg", - "frozen": False, - }, - "head": { - "in_channels": 384, - "num_classes": 2, - }, - "data_preprocess": { - "mean": [1, 1, 1], - "std": [1, 1, 1], - "to_rgb": True, - }, - } - return DictConfig(pseudo_model_config) diff --git a/tests/unit/algo/classification/heads/__init__.py b/tests/unit/algo/classification/heads/__init__.py deleted file mode 100644 index 6a16273c024..00000000000 --- a/tests/unit/algo/classification/heads/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algo/classification/heads/test_custom_hlabel_cls_head.py b/tests/unit/algo/classification/heads/test_custom_hlabel_cls_head.py deleted file mode 100644 index fe7533c1cda..00000000000 --- a/tests/unit/algo/classification/heads/test_custom_hlabel_cls_head.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from __future__ import annotations - -from typing import Any - -import pytest -import torch -from mmpretrain.structures import DataSample -from otx.algo.classification.heads.custom_hlabel_linear_cls_head import CustomHierarchicalLinearClsHead -from otx.algo.classification.heads.custom_hlabel_non_linear_cls_head import CustomHierarchicalNonLinearClsHead -from torch import nn - - -@pytest.fixture() -def fxt_data_sample() -> DataSample: - data_sample = DataSample( - gt_label=torch.ones(6, dtype=torch.long), - metainfo={ - "img_shape": (24, 24, 3), - "ignored_labels": None, - }, - ) - return [data_sample] * 18 - - -@pytest.fixture() -def fxt_data_sample_with_ignored_labels() -> DataSample: - data_sample = DataSample( - gt_label=torch.ones(6, dtype=torch.long), - metainfo={ - "img_shape": (24, 24, 3), - "ignored_labels": [5], - }, - ) - return [data_sample] * 18 - - -class TestCustomHierarchicalLinearClsHead: - @pytest.fixture() - def fxt_head_attrs(self) -> dict[str, Any]: - return { - "num_multiclass_heads": 3, - "num_multilabel_classes": 3, - "in_channels": 24, - "num_classes": 6, - "multiclass_loss_cfg": { - "type": "CrossEntropyLoss", - "use_sigmoid": False, - "reduction": "mean", - "loss_weight": 1.0, - }, - "multilabel_loss_cfg": { - "reduction": "sum", - "gamma_neg": 1.0, - "gamma_pos": 0.0, - "type": "AsymmetricAngularLossWithIgnore", - }, - } - - @pytest.fixture() - def fxt_hlabel_linear_head(self, fxt_head_attrs) -> nn.Module: - return CustomHierarchicalLinearClsHead(**fxt_head_attrs) - - @pytest.fixture() - def fxt_hlabel_non_linear_head(self, fxt_head_attrs) -> nn.Module: - return CustomHierarchicalNonLinearClsHead(**fxt_head_attrs) - - def test_linear_loss( - self, - fxt_hlabel_linear_head, - fxt_data_sample, - fxt_data_sample_with_ignored_labels, - fxt_hlabel_multilabel_info, - ) -> None: - fxt_hlabel_linear_head.set_hlabel_info(fxt_hlabel_multilabel_info) - - dummy_input = (torch.ones((18, 24)), torch.ones((18, 24))) - result_without_ignored_labels = fxt_hlabel_linear_head.loss(dummy_input, fxt_data_sample) - - result_with_ignored_labels = fxt_hlabel_linear_head.loss( - dummy_input, - fxt_data_sample_with_ignored_labels, - ) - assert result_with_ignored_labels["loss"] < result_without_ignored_labels["loss"] - - def test_non_linear_loss( - self, - fxt_hlabel_non_linear_head, - fxt_data_sample, - fxt_data_sample_with_ignored_labels, - fxt_hlabel_multilabel_info, - ) -> None: - fxt_hlabel_non_linear_head.set_hlabel_info(fxt_hlabel_multilabel_info) - - dummy_input = (torch.ones((18, 24)), torch.ones((18, 24))) - result_without_ignored_labels = fxt_hlabel_non_linear_head.loss(dummy_input, fxt_data_sample) - - result_with_ignored_labels = fxt_hlabel_non_linear_head.loss( - dummy_input, - fxt_data_sample_with_ignored_labels, - ) - assert result_with_ignored_labels["loss"] < result_without_ignored_labels["loss"] - - def test_predict( - self, - fxt_hlabel_linear_head, - fxt_hlabel_non_linear_head, - fxt_data_sample, - fxt_hlabel_multilabel_info, - ) -> None: - fxt_hlabel_linear_head.set_hlabel_info(fxt_hlabel_multilabel_info) - dummy_input = (torch.ones((2, 24)), torch.ones((2, 24))) - result = fxt_hlabel_linear_head.predict(dummy_input, fxt_data_sample) - assert isinstance(result[0], DataSample) - - fxt_hlabel_non_linear_head.set_hlabel_info(fxt_hlabel_multilabel_info) - dummy_input = (torch.ones((2, 24)), torch.ones((2, 24))) - result = fxt_hlabel_non_linear_head.predict(dummy_input, fxt_data_sample) - assert isinstance(result[0], DataSample) diff --git a/tests/unit/algo/classification/heads/test_custom_multilabel_cls_head.py b/tests/unit/algo/classification/heads/test_custom_multilabel_cls_head.py deleted file mode 100644 index 0499a013cd0..00000000000 --- a/tests/unit/algo/classification/heads/test_custom_multilabel_cls_head.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - - -from __future__ import annotations - -import pytest -import torch -from mmpretrain.structures import DataSample -from otx.algo.classification.heads import CustomMultiLabelLinearClsHead, CustomMultiLabelNonLinearClsHead -from otx.algo.classification.losses import AsymmetricAngularLossWithIgnore - - -@pytest.fixture() -def fxt_linear_head() -> None: - return CustomMultiLabelLinearClsHead( - num_classes=3, - in_channels=5, - loss={ - "type": AsymmetricAngularLossWithIgnore.__name__, - "reduction": "sum", - }, - ) - - -@pytest.fixture() -def fxt_non_linear_head() -> None: - return CustomMultiLabelNonLinearClsHead( - num_classes=3, - in_channels=5, - hid_channels=10, - act_cfg={"type": "PReLU"}, - loss={ - "type": AsymmetricAngularLossWithIgnore.__name__, - "reduction": "sum", - }, - ) - - -@pytest.fixture() -def fxt_data_sample() -> None: - return DataSample( - gt_score=torch.Tensor([1, 1, 1]), - metainfo={ - "scale_factor": (0.448, 0.797153024911032), - "img_shape": (224, 224), - "pad_shape": (224, 224), - "ignored_labels": None, - "ori_shape": (281, 500), - }, - ) - - -@pytest.fixture() -def fxt_data_sample_with_ignore_labels() -> None: - return DataSample( - gt_score=torch.Tensor([1, 1, -1]), - metainfo={ - "scale_factor": (0.448, 0.797153024911032), - "img_shape": (224, 224), - "pad_shape": (224, 224), - "ignored_labels": [2], - "ori_shape": (281, 500), - }, - ) - - -class TestCustomMultiLabelClsHead: - def test_linear_loss(self, fxt_linear_head, fxt_data_sample, fxt_data_sample_with_ignore_labels) -> None: - inputs = (torch.ones((2, 5)),) - - result_with_ignore_label = fxt_linear_head.loss( - inputs, - [fxt_data_sample_with_ignore_labels, fxt_data_sample_with_ignore_labels], - ) - result_without_ignore_label = fxt_linear_head.loss(inputs, [fxt_data_sample, fxt_data_sample]) - assert result_with_ignore_label["loss"] < result_without_ignore_label["loss"] - - def test_nonlinear_loss(self, fxt_non_linear_head, fxt_data_sample, fxt_data_sample_with_ignore_labels) -> None: - inputs = (torch.ones((2, 5)),) - - result_with_ignore_label = fxt_non_linear_head.loss( - inputs, - [fxt_data_sample_with_ignore_labels, fxt_data_sample_with_ignore_labels], - ) - result_without_ignore_label = fxt_non_linear_head.loss(inputs, [fxt_data_sample, fxt_data_sample]) - assert result_with_ignore_label["loss"] < result_without_ignore_label["loss"] diff --git a/tests/unit/algo/classification/losses/__init__.py b/tests/unit/algo/classification/losses/__init__.py deleted file mode 100644 index 6a16273c024..00000000000 --- a/tests/unit/algo/classification/losses/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algo/classification/losses/test_asymmetric_multilabel.py b/tests/unit/algo/classification/losses/test_asymmetric_multilabel.py deleted file mode 100644 index 70159dbf0ee..00000000000 --- a/tests/unit/algo/classification/losses/test_asymmetric_multilabel.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -import pytest -import torch -from otx.algo.classification.losses import AsymmetricAngularLossWithIgnore - - -class TestAsymmetricAngularLoss: - @pytest.fixture() - def fxt_num_classes(self) -> None: - return 2 - - @pytest.fixture() - def fxt_gt(self, fxt_num_classes) -> None: - gt = torch.zeros((2, fxt_num_classes)) - gt[0, 0] = 1 - return gt - - @pytest.fixture() - def fxt_input(self, fxt_num_classes) -> None: - inputs = torch.zeros((2, fxt_num_classes)) - inputs[0, 0] = 1 - return inputs - - @pytest.fixture() - def loss(self) -> None: - return AsymmetricAngularLossWithIgnore(reduction="mean") - - def test_forward(self, loss, fxt_input, fxt_gt) -> None: - result_c = loss(fxt_input, fxt_gt) - fxt_input[0, 1] = 1 - result_w = loss(fxt_input, fxt_gt) - assert result_c < result_w diff --git a/tests/unit/algo/classification/test_otx_dino_v2.py b/tests/unit/algo/classification/test_otx_dino_v2.py deleted file mode 100644 index 2ec0100bb55..00000000000 --- a/tests/unit/algo/classification/test_otx_dino_v2.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from unittest.mock import MagicMock, patch - -import pytest -import torch -from otx.algo.classification import DINOv2 - - -class TestDINOv2: - @pytest.fixture() - def model(self) -> None: - mock_backbone = MagicMock() - mock_backbone.return_value = torch.randn(1, 12) - - with patch("torch.hub.load", autospec=True) as mock_load: - mock_load.return_value = mock_backbone - - return DINOv2( - backbone_name="dinov2_vits14_reg", - freeze_backbone=True, - head_in_channels=12, - num_classes=2, - ) - - def test_freeze_backbone(self, model) -> None: - for _, v in model.backbone.named_parameters(): - assert v.requires_grad is False - - def test_forward(self, model) -> None: - rand_img = torch.randn((1, 3, 224, 224), dtype=torch.float32) - rand_label = torch.ones((1), dtype=torch.int64) - outputs = model(rand_img, rand_label) - assert isinstance(outputs, torch.Tensor) diff --git a/tests/unit/algo/classification/test_torchvision_model.py b/tests/unit/algo/classification/test_torchvision_model.py deleted file mode 100644 index ee6dee37b76..00000000000 --- a/tests/unit/algo/classification/test_torchvision_model.py +++ /dev/null @@ -1,70 +0,0 @@ -import pytest -import torch -from otx.algo.classification.torchvision_model import OTXTVModel, TVModelWithLossComputation -from otx.core.data.entity.base import ImageInfo, OTXBatchLossEntity -from otx.core.data.entity.classification import ( - MulticlassClsBatchDataEntity, - MulticlassClsBatchPredEntity, -) - - -@pytest.fixture() -def fxt_tv_model(): - return OTXTVModel(backbone="resnet50", num_classes=10) - - -@pytest.fixture() -def fxt_inputs(): - return MulticlassClsBatchDataEntity( - batch_size=16, - images=torch.randn(16, 3, 224, 224), - imgs_info=[ImageInfo(img_idx=i, img_shape=(224, 224), ori_shape=(224, 224)) for i in range(16)], - labels=[torch.randint(0, 10, (16,))], - ) - - -class TestOTXTVModel: - def test_create_model(self, fxt_tv_model): - assert isinstance(fxt_tv_model._create_model(), TVModelWithLossComputation) - - def test_customize_inputs(self, fxt_tv_model, fxt_inputs): - outputs = fxt_tv_model._customize_inputs(fxt_inputs) - assert "images" in outputs - assert "labels" in outputs - assert "mode" in outputs - - def test_customize_outputs(self, fxt_tv_model, fxt_inputs): - outputs = torch.randn(16, 10) - fxt_tv_model.training = True - preds = fxt_tv_model._customize_outputs(outputs, fxt_inputs) - assert isinstance(preds, OTXBatchLossEntity) - - fxt_tv_model.training = False - preds = fxt_tv_model._customize_outputs(outputs, fxt_inputs) - assert isinstance(preds, MulticlassClsBatchPredEntity) - - def test_export_parameters(self, fxt_tv_model): - params = fxt_tv_model._export_parameters - assert isinstance(params, dict) - assert "input_size" in params - assert "resize_mode" in params - assert "pad_value" in params - assert "swap_rgb" in params - assert "via_onnx" in params - assert "onnx_export_configuration" in params - assert "mean" in params - assert "std" in params - - def test_forward_explain_image_classifier(self, fxt_tv_model): - images = torch.randn(16, 3, 224, 224) - fxt_tv_model._explain_mode = True - fxt_tv_model._reset_model_forward() - outputs = fxt_tv_model._forward_explain_image_classifier(fxt_tv_model.model, images) - assert "logits" in outputs - assert "feature_vector" in outputs - assert "saliency_map" in outputs - - def test_head_forward_fn(self, fxt_tv_model): - x = torch.randn(16, 2048) - output = fxt_tv_model.head_forward_fn(x) - assert output.shape == (16, 10) diff --git a/tests/unit/algo/detection/__init__.py b/tests/unit/algo/detection/__init__.py deleted file mode 100644 index ccab1344fbc..00000000000 --- a/tests/unit/algo/detection/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom algo modules of OTX Detection task.""" diff --git a/tests/unit/algo/detection/backbones/__init__.py b/tests/unit/algo/detection/backbones/__init__.py deleted file mode 100644 index e7288e7e7f0..00000000000 --- a/tests/unit/algo/detection/backbones/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom backbones of OTX Detection task.""" diff --git a/tests/unit/algo/detection/backbones/test_pytorchcv_backbones.py b/tests/unit/algo/detection/backbones/test_pytorchcv_backbones.py deleted file mode 100644 index 7fb49351a58..00000000000 --- a/tests/unit/algo/detection/backbones/test_pytorchcv_backbones.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of pytorchcv backbones.""" - -from __future__ import annotations - -import torch -from otx.algo.detection.backbones.pytorchcv_backbones import ( - _build_model_including_pytorchcv, - multioutput_forward, - replace_activation, - replace_norm, - train, -) -from torch import nn - - -class MockModule(nn.Module): - def __init__(self): - super().__init__() - self._modules = { - "linear1": nn.Linear(256, 256), - "bn1": nn.BatchNorm2d(256), - "activ1": nn.ReLU(), - "linear2": nn.Linear(256, 32), - "bn2": nn.BatchNorm2d(32), - "activ2": nn.ReLU(), - "linear3": nn.Linear(32, 10), - } - - -def test_replace_activation() -> None: - activation_cfg = {"type": "GELU"} - model = MockModule() - model = replace_activation(model, activation_cfg) - assert isinstance(model._modules["activ1"], nn.GELU) - assert isinstance(model._modules["activ2"], nn.GELU) - - activation_cfg = {"type": "torch_swish"} - model = replace_activation(model, activation_cfg) - assert isinstance(model._modules["activ1"], nn.SiLU) - assert isinstance(model._modules["activ2"], nn.SiLU) - - -def test_replace_norm(mocker) -> None: - mocker.patch( - "otx.algo.detection.backbones.pytorchcv_backbones.build_norm_layer", - return_value=[None, nn.BatchNorm1d(100)], - ) - cfg = {"type": "BatchNorm1d"} - model = MockModule() - model = replace_norm(model, cfg) - assert isinstance(model._modules["bn1"], nn.BatchNorm1d) - assert isinstance(model._modules["bn2"], nn.BatchNorm1d) - - -def test_multioutput_forward() -> None: - class MockModel(nn.Module): - def __init__(self): - super().__init__() - self.out_indices = [1, 2] - self.features = [ - nn.Linear(256, 256), - nn.Linear(256, 128), - nn.Linear(128, 64), - nn.Linear(64, 32), - ] - self.verbose = True - - model = MockModel() - out = multioutput_forward(model, torch.randn(3, 256)) - assert len(out) == 2 - assert out[0].shape == torch.Size([3, 128]) - assert out[1].shape == torch.Size([3, 64]) - - -def test_train() -> None: - class MockModel(nn.Module): - def __init__(self): - super().__init__() - self.frozen_stages = 2 - self.features = [ - nn.Linear(256, 256), - nn.Linear(256, 128), - nn.Linear(128, 64), - nn.Linear(64, 32), - ] - self.norm_eval = True - - model = MockModel() - train(model) - assert model.features[0].training is False - assert model.features[1].training is False - assert model.features[2].training is False - assert model.features[3].training is True - - -def test_generate_backbones() -> None: - cfg = {"type": "alexnet", "out_indices": [-1]} - model = _build_model_including_pytorchcv(cfg) - - assert "alexnet" in model.__class__.__name__.lower() - assert model.out_indices == [-1] diff --git a/tests/unit/algo/detection/heads/__init__.py b/tests/unit/algo/detection/heads/__init__.py deleted file mode 100644 index 89e2132007b..00000000000 --- a/tests/unit/algo/detection/heads/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom heads of OTX Detection task.""" diff --git a/tests/unit/algo/detection/heads/test_class_incremental_mixin.py b/tests/unit/algo/detection/heads/test_class_incremental_mixin.py deleted file mode 100644 index 370a070f403..00000000000 --- a/tests/unit/algo/detection/heads/test_class_incremental_mixin.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of ClassIncrementalMixin.""" - -import torch -from otx.algo.detection.atss import ATSS - - -class MockGTInstance: - bboxes = torch.Tensor([[0.0, 0.0, 240, 240], [240, 240, 480, 480]]) - labels = torch.LongTensor([0, 1]) - - -class TestClassIncrementalMixin: - def test_ignore_label(self) -> None: - atss = ATSS(3, "mobilenetv2") - atss_head = atss.model.bbox_head - - cls_scores = [ - torch.randn(1, 3, 92, 124), - torch.randn(1, 3, 46, 62), - torch.randn(1, 3, 23, 31), - torch.randn(1, 3, 12, 16), - torch.randn(1, 3, 6, 8), - ] - bbox_preds = [ - torch.randn(1, 4, 92, 124), - torch.randn(1, 4, 46, 62), - torch.randn(1, 4, 23, 31), - torch.randn(1, 4, 12, 16), - torch.randn(1, 4, 6, 8), - ] - centernesses = [ - torch.randn(1, 1, 92, 124), - torch.randn(1, 1, 46, 62), - torch.randn(1, 1, 23, 31), - torch.randn(1, 1, 12, 16), - torch.randn(1, 1, 6, 8), - ] - - batch_gt_instances = [MockGTInstance()] - batch_img_metas = [ - { - "ignored_labels": [2], - "img_shape": (480, 480), - "ori_shape": (480, 480), - "scale_factor": (1.0, 1.0), - "pad_shape": (480, 480), - }, - ] - - loss_with_ignored_labels = atss_head.loss_by_feat( - cls_scores, - bbox_preds, - centernesses, - batch_gt_instances, - batch_img_metas, - ) - loss_cls_with_ignored_labels = torch.sum(torch.Tensor(loss_with_ignored_labels["loss_cls"])) - - batch_img_metas = [ - { - "ignored_labels": None, - "img_shape": (480, 480), - "ori_shape": (480, 480), - "scale_factor": (1.0, 1.0), - "pad_shape": (480, 480), - }, - ] - loss_without_ignored_labels = atss_head.loss_by_feat( - cls_scores, - bbox_preds, - centernesses, - batch_gt_instances, - batch_img_metas, - ) - loss_cls_without_ignored_labels = torch.sum(torch.Tensor(loss_without_ignored_labels["loss_cls"])) - - assert loss_cls_with_ignored_labels < loss_cls_without_ignored_labels diff --git a/tests/unit/algo/detection/heads/test_custom_anchor_generator.py b/tests/unit/algo/detection/heads/test_custom_anchor_generator.py deleted file mode 100644 index 484d81c1dc8..00000000000 --- a/tests/unit/algo/detection/heads/test_custom_anchor_generator.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of CustomAnchorGenerator.""" - -import pytest -import torch -from otx.algo.detection.heads.custom_anchor_generator import SSDAnchorGeneratorClustered - - -class TestSSDAnchorGeneratorClustered: - @pytest.fixture() - def anchor_generator(self) -> SSDAnchorGeneratorClustered: - return SSDAnchorGeneratorClustered( - strides=(16, 32), - widths=[ - [38.641007923271076, 92.49516032784699, 271.4234764938237, 141.53469410876247], - [206.04136086566515, 386.6542727907841, 716.9892752215089, 453.75609561761405, 788.4629155558277], - ], - heights=[ - [48.9243877087132, 147.73088476194903, 158.23569788707474, 324.14510379107367], - [587.6216059488938, 381.60024152086544, 323.5988913027747, 702.7486097568518, 741.4865860938451], - ], - ) - - def test_gen_base_anchors(self, anchor_generator) -> None: - assert anchor_generator.base_anchors[0].shape == torch.Size([4, 4]) - assert anchor_generator.base_anchors[1].shape == torch.Size([5, 4]) diff --git a/tests/unit/algo/detection/heads/test_custom_ssd_head.py b/tests/unit/algo/detection/heads/test_custom_ssd_head.py deleted file mode 100644 index 1d4e1f19d33..00000000000 --- a/tests/unit/algo/detection/heads/test_custom_ssd_head.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of CustomSSDHead.""" - -from mmdet.models.losses.cross_entropy_loss import CrossEntropyLoss -from otx.algo.detection.heads.custom_ssd_head import CustomSSDHead - - -class TestCustomSSDHead: - def test_init(self, mocker) -> None: - self.head = CustomSSDHead( - num_classes=80, - in_channels=(96, 320), - use_depthwise=True, - anchor_generator={ - "type": "SSDAnchorGeneratorClustered", - "strides": (16, 32), - "widths": [[38, 92, 271, 141], [206, 386, 716, 453, 788]], - "heights": [[48, 147, 158, 324], [587, 381, 323, 702, 741]], - }, - act_cfg={"type": "ReLU"}, - ) - - assert isinstance(self.head.loss_cls, CrossEntropyLoss) diff --git a/tests/unit/algo/detection/test_ssd.py b/tests/unit/algo/detection/test_ssd.py deleted file mode 100644 index 9a21a1a570d..00000000000 --- a/tests/unit/algo/detection/test_ssd.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of OTX SSD architecture.""" - -import pytest -from otx.algo.detection.ssd import SSD - - -class TestSSD: - @pytest.fixture() - def fxt_model(self) -> SSD: - return SSD(num_classes=3, variant="mobilenetv2") - - def test_save_and_load_anchors(self, fxt_model) -> None: - anchor_widths = fxt_model.model.bbox_head.anchor_generator.widths - anchor_heights = fxt_model.model.bbox_head.anchor_generator.heights - state_dict = fxt_model.state_dict() - assert anchor_widths == state_dict["model.model.anchors"]["widths"] - assert anchor_heights == state_dict["model.model.anchors"]["heights"] - - state_dict["model.model.anchors"]["widths"][0][0] = 40 - state_dict["model.model.anchors"]["heights"][0][0] = 50 - - fxt_model.load_state_dict(state_dict) - assert fxt_model.model.bbox_head.anchor_generator.widths[0][0] == 40 - assert fxt_model.model.bbox_head.anchor_generator.heights[0][0] == 50 diff --git a/tests/unit/algo/hooks/__init__.py b/tests/unit/algo/hooks/__init__.py deleted file mode 100644 index 916f3a44b27..00000000000 --- a/tests/unit/algo/hooks/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algo/hooks/test_saliency_map_dumping.py b/tests/unit/algo/hooks/test_saliency_map_dumping.py deleted file mode 100644 index 28c9d3254cf..00000000000 --- a/tests/unit/algo/hooks/test_saliency_map_dumping.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from pathlib import Path - -import cv2 -import numpy as np -from otx.algo.utils.xai_utils import dump_saliency_maps -from otx.core.config.explain import ExplainConfig -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.classification import MulticlassClsBatchPredEntityWithXAI -from otx.core.types.task import OTXTaskType -from otx.engine.utils.auto_configurator import AutoConfigurator - -NUM_CLASSES = 5 -BATCH_SIZE = 25 -RAW_SIZE = 7 -SALIENCY_MAPS = [{i: np.ones((RAW_SIZE, RAW_SIZE), dtype=np.uint8) for i in range(NUM_CLASSES)}] * BATCH_SIZE -IMGS_INFO = [ImageInfo(img_idx=i, img_shape=None, ori_shape=None) for i in range(BATCH_SIZE)] - - -def test_sal_map_dump( - tmp_path: Path, -) -> None: - explain_config = ExplainConfig() - - data_root = "tests/assets/classification_dataset" - task = OTXTaskType.MULTI_CLASS_CLS - auto_configurator = AutoConfigurator(data_root=data_root, task=task) - datamodule = auto_configurator.get_datamodule() - - predict_result = [ - MulticlassClsBatchPredEntityWithXAI( - batch_size=BATCH_SIZE, - images=None, - imgs_info=IMGS_INFO, - scores=None, - labels=None, - saliency_maps=SALIENCY_MAPS, - feature_vectors=None, - ), - ] - - dump_saliency_maps( - predict_result, - explain_config, - datamodule, - output_dir=tmp_path, - ) - - saliency_maps_paths = sorted((tmp_path / "saliency_maps").glob(pattern="*.png")) - - assert len(saliency_maps_paths) == NUM_CLASSES * BATCH_SIZE - - file_name = saliency_maps_paths[0].name - first_class_id = "0" - assert file_name[0] == first_class_id - assert "class" in file_name - assert "saliency_map.png" in file_name - - sal_map = cv2.imread(str(saliency_maps_paths[0])) - assert sal_map is not None - assert sal_map.shape[0] > 0 - assert sal_map.shape[1] > 0 diff --git a/tests/unit/algo/hooks/test_saliency_map_processing.py b/tests/unit/algo/hooks/test_saliency_map_processing.py deleted file mode 100644 index 628649925b0..00000000000 --- a/tests/unit/algo/hooks/test_saliency_map_processing.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -import numpy as np -import pytest -import torch -from otx.algo.utils.xai_utils import process_saliency_maps, process_saliency_maps_in_pred_entity -from otx.core.config.explain import ExplainConfig -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.classification import MulticlassClsBatchPredEntityWithXAI, MultilabelClsBatchPredEntityWithXAI -from otx.core.types.explain import TargetExplainGroup - -NUM_CLASSES = 5 -BATCH_SIZE = 3 -RAW_SIZE = 7 -OUT_SIZE = 224 - -PRED_LABELS = [[0, 2, 3], [1], []] -PRED_LABELS_TOP_ONE = [[1], [0], [4]] -SALIENCY_MAPS = [np.ones((NUM_CLASSES, RAW_SIZE, RAW_SIZE), dtype=np.uint8) for _ in range(BATCH_SIZE)] -SALIENCY_MAPS_IMAGE = [np.ones((RAW_SIZE, RAW_SIZE), dtype=np.uint8) for _ in range(BATCH_SIZE)] -ORI_IMG_SHAPES = [(OUT_SIZE, OUT_SIZE)] * BATCH_SIZE -IMGS_INFO = [ImageInfo(img_idx=i, img_shape=None, ori_shape=(OUT_SIZE, OUT_SIZE)) for i in range(BATCH_SIZE)] - - -@pytest.mark.parametrize("postprocess", [False, True]) -def test_process_all(postprocess) -> None: - explain_config = ExplainConfig(target_explain_group=TargetExplainGroup.ALL, postprocess=postprocess) - - with pytest.raises(ValueError, match="Shape mismatch."): - processed_saliency_maps = process_saliency_maps( - SALIENCY_MAPS_IMAGE, - explain_config, - PRED_LABELS, - ORI_IMG_SHAPES, - ) - - processed_saliency_maps = process_saliency_maps(SALIENCY_MAPS, explain_config, PRED_LABELS, ORI_IMG_SHAPES) - - assert len(processed_saliency_maps) == BATCH_SIZE - assert all(len(s_map_dict) == NUM_CLASSES for s_map_dict in processed_saliency_maps) - - if postprocess: - assert all( - next(iter(s_map_dict.values())).shape == (OUT_SIZE, OUT_SIZE, 3) for s_map_dict in processed_saliency_maps - ) - else: - assert all( - next(iter(s_map_dict.values())).shape == (RAW_SIZE, RAW_SIZE) for s_map_dict in processed_saliency_maps - ) - - -@pytest.mark.parametrize("postprocess", [False, True]) -def test_process_predictions(postprocess) -> None: - explain_config = ExplainConfig(target_explain_group=TargetExplainGroup.PREDICTIONS, postprocess=postprocess) - - with pytest.raises(ValueError, match="Shape mismatch."): - processed_saliency_maps = process_saliency_maps( - SALIENCY_MAPS_IMAGE, - explain_config, - PRED_LABELS, - ORI_IMG_SHAPES, - ) - - processed_saliency_maps = process_saliency_maps(SALIENCY_MAPS, explain_config, PRED_LABELS, ORI_IMG_SHAPES) - - assert len(processed_saliency_maps) == BATCH_SIZE - assert all(len(s_map_dict) == len(PRED_LABELS[i]) for (i, s_map_dict) in enumerate(processed_saliency_maps)) - - if postprocess: - assert all( - next(iter(s_map_dict.values())).shape == (OUT_SIZE, OUT_SIZE, 3) - for s_map_dict in processed_saliency_maps - if s_map_dict - ) - else: - assert all( - next(iter(s_map_dict.values())).shape == (RAW_SIZE, RAW_SIZE) - for s_map_dict in processed_saliency_maps - if s_map_dict - ) - - -@pytest.mark.parametrize("postprocess", [False, True]) -def test_process_image(postprocess) -> None: - explain_config = ExplainConfig(target_explain_group=TargetExplainGroup.IMAGE, postprocess=postprocess) - - with pytest.raises(ValueError, match="Shape mismatch."): - processed_saliency_maps = process_saliency_maps(SALIENCY_MAPS, explain_config, PRED_LABELS, ORI_IMG_SHAPES) - - processed_saliency_maps = process_saliency_maps(SALIENCY_MAPS_IMAGE, explain_config, PRED_LABELS, ORI_IMG_SHAPES) - - assert len(processed_saliency_maps) == BATCH_SIZE - assert all(len(s_map_dict) == 1 for s_map_dict in processed_saliency_maps) - - if postprocess: - assert all( - s_map_dict["map_per_image"].shape == (OUT_SIZE, OUT_SIZE, 3) for s_map_dict in processed_saliency_maps - ) - else: - assert all(s_map_dict["map_per_image"].shape == (RAW_SIZE, RAW_SIZE) for s_map_dict in processed_saliency_maps) - - -def _get_pred_result_multiclass(pred_labels) -> MulticlassClsBatchPredEntityWithXAI: - return MulticlassClsBatchPredEntityWithXAI( - batch_size=BATCH_SIZE, - images=None, - imgs_info=IMGS_INFO, - scores=None, - labels=pred_labels, - saliency_maps=SALIENCY_MAPS, - feature_vectors=None, - ) - - -def _get_pred_result_multilabel(pred_labels) -> MultilabelClsBatchPredEntityWithXAI: - return MultilabelClsBatchPredEntityWithXAI( - batch_size=BATCH_SIZE, - images=None, - imgs_info=IMGS_INFO, - scores=None, - labels=pred_labels, - saliency_maps=SALIENCY_MAPS, - feature_vectors=None, - ) - - -def test_process_saliency_maps_in_pred_entity_multiclass() -> None: - explain_config = ExplainConfig(target_explain_group=TargetExplainGroup.PREDICTIONS) - - pred_labels = [torch.tensor(labels) for labels in PRED_LABELS_TOP_ONE] - predict_result_batch1 = _get_pred_result_multiclass(pred_labels) - predict_result_batch2 = _get_pred_result_multiclass(pred_labels) - - predict_result = process_saliency_maps_in_pred_entity( - [predict_result_batch1, predict_result_batch2], - explain_config, - ) - - for i in range(len(predict_result)): - assert isinstance(predict_result[i].saliency_maps, list) - assert isinstance(predict_result[i].saliency_maps[0], dict) - processed_saliency_maps = predict_result[i].saliency_maps - assert all(len(s_map_dict) == 1 for s_map_dict in processed_saliency_maps) - - -def test_process_saliency_maps_in_pred_entity_multilabel() -> None: - explain_config = ExplainConfig(target_explain_group=TargetExplainGroup.PREDICTIONS) - - pred_labels = [torch.tensor(labels) for labels in PRED_LABELS] - predict_result_batch1 = _get_pred_result_multilabel(pred_labels) - predict_result_batch2 = _get_pred_result_multilabel(pred_labels) - - predict_result = process_saliency_maps_in_pred_entity( - [predict_result_batch1, predict_result_batch2], - explain_config, - ) - - for i in range(len(predict_result)): - assert isinstance(predict_result[i].saliency_maps, list) - assert isinstance(predict_result[i].saliency_maps[0], dict) - processed_saliency_maps = predict_result[i].saliency_maps - assert all(len(s_map_dict) == len(PRED_LABELS[i]) for (i, s_map_dict) in enumerate(processed_saliency_maps)) diff --git a/tests/unit/algo/hooks/test_xai_hooks.py b/tests/unit/algo/hooks/test_xai_hooks.py deleted file mode 100644 index 65869496356..00000000000 --- a/tests/unit/algo/hooks/test_xai_hooks.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -import torch -from datumaro import Polygon -from otx.algo.hooks.recording_forward_hook import ( - ActivationMapHook, - DetClassProbabilityMapHook, - MaskRCNNRecordingForwardHook, - ReciproCAMHook, - ViTReciproCAMHook, -) -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.instance_segmentation import InstanceSegBatchPredEntity -from torch import LongTensor -from torchvision import tv_tensors - - -def test_activationmap() -> None: - hook = ActivationMapHook() - - assert hook.handle is None - assert hook.records == [] - assert hook._norm_saliency_maps - - feature_map = torch.zeros((1, 10, 5, 5)) - - saliency_maps = hook.func(feature_map) - assert saliency_maps.size() == torch.Size([1, 5, 5]) - - hook.recording_forward(None, None, feature_map) - assert len(hook.records) == 1 - - hook.reset() - assert hook.records == [] - - -def test_reciprocam() -> None: - def cls_head_forward_fn(_) -> None: - return torch.zeros((25, 2)) - - num_classes = 2 - optimize_gap = False - hook = ReciproCAMHook( - cls_head_forward_fn, - num_classes=num_classes, - optimize_gap=optimize_gap, - ) - - assert hook.handle is None - assert hook.records == [] - assert hook._norm_saliency_maps - - feature_map = torch.zeros((1, 10, 5, 5)) - - saliency_maps = hook.func(feature_map) - assert saliency_maps.size() == torch.Size([1, 2, 5, 5]) - - hook.recording_forward(None, None, feature_map) - assert len(hook.records) == 1 - - hook.reset() - assert hook.records == [] - - -def test_vitreciprocam() -> None: - def cls_head_forward_fn(_) -> None: - return torch.zeros((196, 2)) - - num_classes = 2 - hook = ViTReciproCAMHook( - cls_head_forward_fn, - num_classes=num_classes, - ) - - assert hook.handle is None - assert hook.records == [] - assert hook._norm_saliency_maps - - feature_map = torch.zeros((1, 197, 192)) - - saliency_maps = hook.func(feature_map) - assert saliency_maps.size() == torch.Size([1, 2, 14, 14]) - - hook.recording_forward(None, None, feature_map) - assert len(hook.records) == 1 - - hook.reset() - assert hook.records == [] - - -def test_detclassprob() -> None: - num_classes = 2 - num_anchors = [1] * 10 - hook = DetClassProbabilityMapHook( - num_classes=num_classes, - num_anchors=num_anchors, - ) - - assert hook.handle is None - assert hook.records == [] - assert hook._norm_saliency_maps - - backbone_out = torch.zeros((1, 5, 2, 2, 2)) - - saliency_maps = hook.func(backbone_out) - assert saliency_maps.size() == torch.Size([5, 2, 2, 2]) - - -def test_maskrcnn() -> None: - num_classes = 2 - hook = MaskRCNNRecordingForwardHook( - num_classes=num_classes, - ) - - assert hook.handle is None - assert hook.records == [] - assert hook._norm_saliency_maps - - # One image, 3 masks to aggregate - pred = InstanceSegBatchPredEntity( - batch_size=1, - masks=tv_tensors.Mask(torch.ones(3, 10, 10)), - scores=LongTensor([0.1, 0.2, 0.3]), - labels=LongTensor([0, 0, 1]), - # not used during saliency map calculation - images=[tv_tensors.Image(torch.randn(3, 10, 10))], - imgs_info=[ImageInfo(img_idx=0, img_shape=(10, 10), ori_shape=(10, 10))], - bboxes=[ - 3 - * tv_tensors.BoundingBoxes( - data=torch.Tensor([0, 0, 5, 5]), - format="xywh", - canvas_size=(10, 10), - ), - ], - polygons=[Polygon(points=[1, 1, 2, 2, 3, 3, 4, 4])], - ) - - # 2 images - saliency_maps = hook.func([pred, pred]) - assert len(saliency_maps) == 2 - assert saliency_maps[0].shape == (2, 10, 10) diff --git a/tests/unit/algo/instance_segmentation/__init__.py b/tests/unit/algo/instance_segmentation/__init__.py deleted file mode 100644 index a0856ee1891..00000000000 --- a/tests/unit/algo/instance_segmentation/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit test of custom algo modules of OTX Instance Segmentation tasks.""" diff --git a/tests/unit/algo/instance_segmentation/heads/__init__.py b/tests/unit/algo/instance_segmentation/heads/__init__.py deleted file mode 100644 index cec0b024d16..00000000000 --- a/tests/unit/algo/instance_segmentation/heads/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit test of custom heads of OTX Instance Segmentation tasks.""" diff --git a/tests/unit/algo/instance_segmentation/heads/test_custom_roi_head.py b/tests/unit/algo/instance_segmentation/heads/test_custom_roi_head.py deleted file mode 100644 index 9ed6683250a..00000000000 --- a/tests/unit/algo/instance_segmentation/heads/test_custom_roi_head.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit test of custom heads of OTX Instance Segmentation tasks.""" - -from __future__ import annotations - -from copy import deepcopy - -import pytest -import torch -from mmdet.structures import DetDataSample -from mmengine.structures import InstanceData -from otx.algo.instance_segmentation.heads.custom_roi_head import CustomRoIHead -from otx.algo.instance_segmentation.maskrcnn import MaskRCNN - - -@pytest.fixture() -def fxt_data_sample() -> list[DetDataSample]: - data_sample = DetDataSample( - metainfo={ - "img_shape": (480, 480), - "ori_shape": (480, 480), - "scale_factor": (1.0, 1.0), - "pad_shape": (480, 480), - "ignored_labels": [], - }, - gt_instances=InstanceData( - bboxes=torch.Tensor([[0.0, 0.0, 240, 240], [240, 240, 480, 480]]), - labels=torch.LongTensor([0, 1]), - ), - ) - return [data_sample] - - -@pytest.fixture() -def fxt_data_sample_with_ignored_label() -> list[DetDataSample]: - data_sample = DetDataSample( - metainfo={ - "img_shape": (480, 480), - "ori_shape": (480, 480), - "scale_factor": (1.0, 1.0), - "pad_shape": (480, 480), - "ignored_labels": [2], - }, - gt_instances=InstanceData( - bboxes=torch.Tensor([[0.0, 0.0, 240, 240], [240, 240, 480, 480]]), - labels=torch.LongTensor([0, 1]), - ), - ) - return [data_sample] - - -@pytest.fixture() -def fxt_instance_list() -> list[InstanceData]: - data = InstanceData( - bboxes=torch.Tensor([[0.0, 0.0, 240, 240], [240, 240, 480, 480]]), - labels=torch.LongTensor([0, 0]), - scores=torch.Tensor([1.0, 1.0]), - ) - return [data] - - -class TestClassIncrementalMixin: - def test_ignore_label( - self, - mocker, - fxt_data_sample, - fxt_data_sample_with_ignored_label, - fxt_instance_list, - ) -> None: - maskrcnn = MaskRCNN(3, "r50") - input_tensors = [ - torch.randn([4, 256, 144, 256]), - torch.randn([4, 256, 72, 128]), - torch.randn([4, 256, 36, 64]), - torch.randn([4, 256, 18, 32]), - torch.randn([4, 256, 9, 16]), - ] - - mocker.patch.object( - CustomRoIHead, - "mask_loss", - return_value={"loss_mask": {"loss_mask": torch.Tensor([0.0])}}, - ) - loss_without_ignore = maskrcnn.model.roi_head.loss( - input_tensors, - deepcopy(fxt_instance_list), - fxt_data_sample, - ) - loss_with_ignore = maskrcnn.model.roi_head.loss( - input_tensors, - deepcopy(fxt_instance_list), - fxt_data_sample_with_ignored_label, - ) - assert loss_with_ignore["loss_cls"] < loss_without_ignore["loss_cls"] diff --git a/tests/unit/algo/instance_segmentation/test_evaluation.py b/tests/unit/algo/instance_segmentation/test_evaluation.py deleted file mode 100644 index 06ae5a4772c..00000000000 --- a/tests/unit/algo/instance_segmentation/test_evaluation.py +++ /dev/null @@ -1,32 +0,0 @@ -import torch -from otx.algo.instance_segmentation.otx_instseg_evaluation import OTXMaskRLEMeanAveragePrecision -from otx.core.utils.mask_util import encode_rle -from torchmetrics.detection.mean_ap import MeanAveragePrecision - - -def test_custom_rle_map_metric(num_masks=50, h=10, w=10): - """Test custom RLE MAP metric.""" - custom_map_metric = OTXMaskRLEMeanAveragePrecision(iou_type="segm") - torch_map_metric = MeanAveragePrecision(iou_type="segm") - - # Create random masks - pred_masks = torch.randint(low=0, high=2, size=(num_masks, h, w)).bool() - target_masks = torch.randint(low=0, high=2, size=(num_masks, h, w)).bool() - labels = torch.zeros(num_masks, dtype=torch.long) - scores = torch.rand(num_masks) - - torch_map_metric.update( - preds=[{"masks": pred_masks, "labels": labels, "scores": scores}], - target=[{"masks": target_masks, "labels": labels}], - ) - - custom_map_metric.update( - preds=[{"masks": [encode_rle(pred) for pred in pred_masks], "labels": labels, "scores": scores}], - target=[{"masks": [encode_rle(target) for target in target_masks], "labels": labels}], - ) - - # Compare the results - torch_results = torch_map_metric.compute() - custom_results = custom_map_metric.compute() - - assert custom_results == torch_results, f"Expected {torch_results} but got {custom_results}" diff --git a/tests/unit/algo/samplers/__init__.py b/tests/unit/algo/samplers/__init__.py deleted file mode 100644 index 916f3a44b27..00000000000 --- a/tests/unit/algo/samplers/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algo/samplers/test_balanced_sampler.py b/tests/unit/algo/samplers/test_balanced_sampler.py deleted file mode 100644 index 587f9d61990..00000000000 --- a/tests/unit/algo/samplers/test_balanced_sampler.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import math - -import pytest -from datumaro.components.annotation import Label -from datumaro.components.dataset import Dataset as DmDataset -from datumaro.components.dataset_base import DatasetItem -from otx.algo.samplers.balanced_sampler import BalancedSampler -from otx.core.data.dataset.base import OTXDataset -from otx.core.utils.utils import get_idx_list_per_classes - - -@pytest.fixture() -def fxt_imbalanced_dataset() -> OTXDataset: - dataset_items = [ - DatasetItem( - id=f"item00{i}_0", - subset="train", - media=None, - annotations=[ - Label(label=0), - ], - ) - for i in range(1, 101) - ] + [ - DatasetItem( - id=f"item00{i}_1", - subset="train", - media=None, - annotations=[ - Label(label=1), - ], - ) - for i in range(1, 9) - ] - - dm_dataset = DmDataset.from_iterable(dataset_items, categories=["0", "1"]) - return OTXDataset( - dm_subset=dm_dataset, - transforms=[], - ) - - -class TestBalancedSampler: - def test_sampler_iter(self, fxt_imbalanced_dataset): - sampler = BalancedSampler(fxt_imbalanced_dataset) - sampler_iter = iter(sampler) - count = 0 - - for _ in sampler_iter: - count += 1 - - assert count == len(sampler) - assert sampler.num_trials == math.ceil(len(fxt_imbalanced_dataset) / sampler.num_cls) - - def test_sampler_efficient_mode(self, fxt_imbalanced_dataset): - sampler = BalancedSampler(fxt_imbalanced_dataset, efficient_mode=True) - sampler_iter = iter(sampler) - count = 0 - - for _ in sampler_iter: - count += 1 - - assert count == len(sampler) - assert sampler.num_trials == 51 - - def test_sampler_iter_with_multiple_replicas(self, fxt_imbalanced_dataset): - sampler = BalancedSampler(fxt_imbalanced_dataset, num_replicas=2) - sampler_iter = iter(sampler) - count = 0 - - for _ in sampler_iter: - count += 1 - - assert count == len(sampler) - - def test_compute_class_statistics(self, fxt_imbalanced_dataset): - # Compute class statistics - stats = get_idx_list_per_classes(fxt_imbalanced_dataset.dm_subset) - - # Check the expected results - assert stats == {0: list(range(100)), 1: list(range(100, 108))} - - def test_sampler_iter_per_class(self, fxt_imbalanced_dataset): - batch_size = 4 - sampler = BalancedSampler(fxt_imbalanced_dataset) - - stats = get_idx_list_per_classes(fxt_imbalanced_dataset.dm_subset) - class_0_idx = stats[0] - class_1_idx = stats[1] - list_iter = list(iter(sampler)) - for i in range(0, len(sampler), batch_size): - batch = sorted(list_iter[i : i + batch_size]) - assert all(idx in class_0_idx for idx in batch[:2]) - assert all(idx in class_1_idx for idx in batch[2:]) diff --git a/tests/unit/algo/samplers/test_class_incremental_sampler.py b/tests/unit/algo/samplers/test_class_incremental_sampler.py deleted file mode 100644 index 2482c9c1f1d..00000000000 --- a/tests/unit/algo/samplers/test_class_incremental_sampler.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import pytest -from datumaro.components.annotation import Label -from datumaro.components.dataset import Dataset as DmDataset -from datumaro.components.dataset_base import DatasetItem -from otx.algo.samplers.class_incremental_sampler import ClassIncrementalSampler -from otx.core.data.dataset.base import OTXDataset -from otx.core.utils.utils import get_idx_list_per_classes - - -@pytest.fixture() -def fxt_old_new_dataset() -> OTXDataset: - dataset_items = ( - [ - DatasetItem( - id=f"item00{i}_0", - subset="train", - media=None, - annotations=[ - Label(label=0), - ], - ) - for i in range(1, 101) - ] - + [ - DatasetItem( - id=f"item00{i}_1", - subset="train", - media=None, - annotations=[ - Label(label=1), - ], - ) - for i in range(1, 9) - ] - + [ - DatasetItem( - id=f"item00{i}_2", - subset="train", - media=None, - annotations=[ - Label(label=2), - ], - ) - for i in range(1, 9) - ] - ) - - dm_dataset = DmDataset.from_iterable(dataset_items, categories=["0", "1", "2"]) - return OTXDataset( - dm_subset=dm_dataset, - transforms=[], - ) - - -class TestBalancedSampler: - def test_sampler_iter(self, fxt_old_new_dataset): - sampler = ClassIncrementalSampler( - fxt_old_new_dataset, - batch_size=4, - old_classes=["0", "1"], - new_classes=["2"], - ) - sampler_iter = iter(sampler) - count = 0 - - for _ in sampler_iter: - count += 1 - - assert count == len(sampler) - assert len(sampler.old_indices) == 108 # "0" + "1" - assert len(sampler.new_indices) == 8 # "2" - assert sampler.old_new_ratio == 3 # np.sqrt(108 / 8) - assert sampler.num_samples == len(fxt_old_new_dataset) - - def test_sampler_efficient_mode(self, fxt_old_new_dataset): - sampler = ClassIncrementalSampler( - fxt_old_new_dataset, - batch_size=4, - old_classes=["0", "1"], - new_classes=["2"], - efficient_mode=True, - ) - sampler_iter = iter(sampler) - count = 0 - - for _ in sampler_iter: - count += 1 - - assert count == len(sampler) - assert len(sampler.old_indices) == 108 # "0" + "1" - assert len(sampler.new_indices) == 8 # "2" - assert sampler.old_new_ratio == 1 # efficient_mode - assert sampler.data_length == 37 # 37 - - def test_sampler_iter_per_class(self, fxt_old_new_dataset): - batch_size = 4 - sampler = ClassIncrementalSampler( - fxt_old_new_dataset, - batch_size=batch_size, - old_classes=["0", "1"], - new_classes=["2"], - ) - - stats = get_idx_list_per_classes(fxt_old_new_dataset.dm_subset, True) - old_idx = stats["0"] + stats["1"] - new_idx = stats["2"] - list_iter = list(iter(sampler)) - for i in range(0, len(sampler), batch_size): - batch = sorted(list_iter[i : i + batch_size]) - assert all(idx in old_idx for idx in batch[: sampler.old_new_ratio]) - assert all(idx in new_idx for idx in batch[sampler.old_new_ratio :]) diff --git a/tests/unit/algo/segmentation/__init__.py b/tests/unit/algo/segmentation/__init__.py deleted file mode 100644 index 96032408907..00000000000 --- a/tests/unit/algo/segmentation/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom algo modules of OTX segmentation task.""" diff --git a/tests/unit/algo/segmentation/backbones/__init__.py b/tests/unit/algo/segmentation/backbones/__init__.py deleted file mode 100644 index 5f4b5b61156..00000000000 --- a/tests/unit/algo/segmentation/backbones/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom backbones of OTX segmentation task.""" diff --git a/tests/unit/algo/segmentation/backbones/litehrnet.py b/tests/unit/algo/segmentation/backbones/litehrnet.py deleted file mode 100644 index 97b075b25fa..00000000000 --- a/tests/unit/algo/segmentation/backbones/litehrnet.py +++ /dev/null @@ -1,113 +0,0 @@ -from copy import deepcopy - -import pytest -import torch -from otx.algo.segmentation.model.backbones.litehrnet import LiteHRNet, NeighbourSupport, SpatialWeightingV2, StemV2 - - -class TestSpatialWeightingV2: - def test_forward(self) -> None: - swv2 = SpatialWeightingV2(channels=32) - assert swv2 is not None - - inputs = torch.randn(1, 32, 32, 32) - outputs = swv2(inputs) - assert outputs is not None - - -class TestStemV2: - @pytest.fixture() - def stemv2(self) -> StemV2: - return StemV2(in_channels=32, stem_channels=32, out_channels=32, expand_ratio=1) - - def test_init(self) -> None: - stemv2_extra_stride = StemV2( - in_channels=32, - stem_channels=32, - out_channels=32, - expand_ratio=1, - extra_stride=True, - ) - assert stemv2_extra_stride is not None - - stemv2_input_norm = StemV2(in_channels=32, stem_channels=32, out_channels=32, expand_ratio=1, input_norm=True) - assert stemv2_input_norm is not None - - def test_forward(self, stemv2) -> None: - inputs = torch.randn(1, 32, 32, 32) - outputs = stemv2(inputs) - assert outputs is not None - - -class TestNeighbourSupport: - def test_forward(self) -> None: - neighbour_support = NeighbourSupport(channels=32) - assert neighbour_support is not None - - inputs = torch.randn(1, 32, 32, 32) - outputs = neighbour_support(inputs) - assert outputs is not None - - -class TestLiteHRNet: - @pytest.fixture() - def extra_cfg(self) -> dict: - return { - "stem": { - "stem_channels": 32, - "out_channels": 32, - "expand_ratio": 1, - "strides": (2, 2), - "extra_stride": False, - "input_norm": False, - }, - "num_stages": 3, - "stages_spec": { - "num_modules": (2, 4, 2), - "num_branches": (2, 3, 4), - "num_blocks": (2, 2, 2), - "module_type": ("LITE", "LITE", "LITE"), - "with_fuse": (True, True, True), - "reduce_ratios": (8, 8, 8), - "num_channels": [ - (40, 80), - (40, 80, 160), - (40, 80, 160, 320), - ], - }, - } - - @pytest.fixture() - def backbone(self, extra_cfg) -> LiteHRNet: - return LiteHRNet(extra=extra_cfg) - - def test_init(self, extra_cfg) -> None: - extra = deepcopy(extra_cfg) - - extra["add_stem_features"] = True - model = LiteHRNet(extra=extra) - assert model is not None - - extra["stages_spec"]["module_type"] = ("NAIVE", "NAIVE", "NAIVE") - extra["stages_spec"]["weighting_module_version"] = "v2" - model = LiteHRNet(extra=extra) - assert model is not None - - def test_init_weights(self, backbone) -> None: - backbone.init_weights() - - with pytest.raises(TypeError): - backbone.init_weights(0) - - def test_forward(self, extra_cfg, backbone) -> None: - backbone.train() - inputs = torch.randn((1, 3, 224, 224)) - outputs = backbone(inputs) - assert outputs is not None - - extra = deepcopy(extra_cfg) - extra["stages_spec"]["module_type"] = ("NAIVE", "NAIVE", "NAIVE") - extra["stages_spec"]["weighting_module_version"] = "v2" - model = LiteHRNet(extra=extra) - outputs = model(inputs) - assert outputs is not None diff --git a/tests/unit/algo/segmentation/heads/__init__.py b/tests/unit/algo/segmentation/heads/__init__.py deleted file mode 100644 index 787943a79f6..00000000000 --- a/tests/unit/algo/segmentation/heads/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom heads of OTX segmentation task.""" diff --git a/tests/unit/algo/segmentation/heads/test_class_incremental_mixin.py b/tests/unit/algo/segmentation/heads/test_class_incremental_mixin.py deleted file mode 100644 index 7b16a0b94f6..00000000000 --- a/tests/unit/algo/segmentation/heads/test_class_incremental_mixin.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of class incremental mixin of OTX segmentation task.""" - -from typing import ClassVar - -import torch -from otx.algo.segmentation.litehrnet import LiteHRNet - - -class MockGT: - data = torch.randint(0, 2, (1, 512, 512)) - - -class MockDataSample: - metainfo: ClassVar = { - "ignored_labels": [2], - "img_shape": [512, 512], - "scale_factor": (7.03125, 7.03125), - "padding_size": (0, 0, 0, 0), - "ori_shape": (128, 128), - "pad_shape": (512, 512), - } - gt_sem_seg = MockGT() - - -class TestClassIncrementalMixin: - def test_ignore_label(self) -> None: - hrnet = LiteHRNet(3, "s") - hrnet_head = hrnet.model.decode_head - - seg_logits = torch.randn(1, 3, 128, 128) - batch_data_samples = [MockDataSample()] - - loss_with_ignored_labels = hrnet_head.loss_by_feat(seg_logits, batch_data_samples)["loss_ce_ignore"] - - batch_data_samples[0].metainfo["ignored_labels"] = None - loss_without_ignored_labels = hrnet_head.loss_by_feat(seg_logits, batch_data_samples)["loss_ce_ignore"] - - assert loss_with_ignored_labels < loss_without_ignored_labels diff --git a/tests/unit/algo/utils/__init__.py b/tests/unit/algo/utils/__init__.py deleted file mode 100644 index dd755ccccbd..00000000000 --- a/tests/unit/algo/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Unit test of utils of OTX algo.""" diff --git a/tests/unit/algo/utils/test_support_otx_v1.py b/tests/unit/algo/utils/test_support_otx_v1.py deleted file mode 100644 index 57eeca84ea2..00000000000 --- a/tests/unit/algo/utils/test_support_otx_v1.py +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from __future__ import annotations - -import pytest -import torch -from otx.algo.utils.support_otx_v1 import OTXv1Helper - - -class TestOTXv1Helper: - @pytest.fixture() - def fxt_random_tensor(self) -> torch.Tensor: - return torch.randn(3, 10) - - def _check_ckpt_pairs(self, src_state_dict: dict, dst_state_dict: dict) -> None: - for (src_key, src_value), (dst_key, dst_value) in zip(src_state_dict.items(), dst_state_dict.items()): - assert src_key == dst_key - assert src_value.shape == dst_value.shape - - @pytest.mark.parametrize("label_type", ["multiclass", "multilabel", "hlabel"]) - def test_load_cls_effnet_b0_ckpt(self, label_type: str, fxt_random_tensor: torch.Tensor) -> None: - src_state_dict = { - "model": { - "state_dict": { - "features.weights": fxt_random_tensor, - "features.activ.weights": fxt_random_tensor, - "output.asl.weights": fxt_random_tensor, - }, - }, - } - - if label_type != "hlabel": - dst_state_dict = { - "model.model.backbone.features.weights": fxt_random_tensor, - "model.model.features.activ.weights": fxt_random_tensor, - "model.model.head.fc.weights": fxt_random_tensor.t(), - } - else: - dst_state_dict = { - "model.model.backbone.features.weights": fxt_random_tensor, - "model.model.features.activ.weights": fxt_random_tensor, - "model.model.head.asl.weights": fxt_random_tensor, - } - - converted_state_dict = OTXv1Helper.load_cls_effnet_b0_ckpt( - src_state_dict, - label_type, - add_prefix="model.model.", - ) - self._check_ckpt_pairs(converted_state_dict, dst_state_dict) - - @pytest.mark.parametrize("label_type", ["multiclass", "multilabel", "hlabel"]) - def test_load_cls_effnet_v2_ckpt(self, label_type: str, fxt_random_tensor: torch.Tensor) -> None: - src_state_dict = { - "model": { - "state_dict": { - "model.weights": fxt_random_tensor, - "model.classifier.weights": fxt_random_tensor, - }, - }, - } - - if label_type != "hlabel": - dst_state_dict = { - "model.model.backbone.model.weights": fxt_random_tensor, - "model.model.head.fc.weights": fxt_random_tensor.t(), - } - else: - dst_state_dict = { - "model.model.backbone.model.weights": fxt_random_tensor, - "model.model.head.fc.weights": fxt_random_tensor, - } - - converted_state_dict = OTXv1Helper.load_cls_effnet_v2_ckpt( - src_state_dict, - label_type, - add_prefix="model.model.", - ) - self._check_ckpt_pairs(converted_state_dict, dst_state_dict) - - @pytest.mark.parametrize("label_type", ["multiclass", "multilabel", "hlabel"]) - def test_load_cls_mobilenet_v3_ckpt(self, label_type: str, fxt_random_tensor: torch.Tensor) -> None: - src_state_dict = { - "model": { - "state_dict": { - "model.weights": fxt_random_tensor, - "classifier.2.weights": fxt_random_tensor, - "classifier.4.weights": fxt_random_tensor, - "act.weights": fxt_random_tensor, - }, - }, - } - - if label_type == "multilabel": - dst_state_dict = { - "model.model.backbone.model.weights": fxt_random_tensor, - "model.model.head.classifier.2.weights": fxt_random_tensor, - "model.model.head.classifier.3.weights": fxt_random_tensor.t(), - "model.model.head.act.weights": fxt_random_tensor, - } - else: - dst_state_dict = { - "model.model.backbone.model.weights": fxt_random_tensor, - "model.model.head.classifier.2.weights": fxt_random_tensor, - "model.model.head.classifier.3.weights": fxt_random_tensor, - "model.model.head.act.weights": fxt_random_tensor, - } - - converted_state_dict = OTXv1Helper.load_cls_mobilenet_v3_ckpt( - src_state_dict, - label_type, - add_prefix="model.model.", - ) - self._check_ckpt_pairs(converted_state_dict, dst_state_dict) - - def test_load_det_ckpt(self, fxt_random_tensor: torch.Tensor) -> None: - src_state_dict = { - "model": { - "state_dict": { - "model.weights": fxt_random_tensor, - "head.weights": fxt_random_tensor, - "ema_model.weights": fxt_random_tensor, - }, - }, - } - - dst_state_dict = { - "model.model.model.weights": fxt_random_tensor, - "model.model.head.weights": fxt_random_tensor, - } - - converted_state_dict = OTXv1Helper.load_det_ckpt(src_state_dict, add_prefix="model.model.") - self._check_ckpt_pairs(converted_state_dict, dst_state_dict) - - def test_load_ssd_ckpt(self, fxt_random_tensor: torch.Tensor) -> None: - src_state_dict = { - "model": { - "state_dict": { - "model.weights": fxt_random_tensor, - "head.weights": fxt_random_tensor, - "ema_model.weights": fxt_random_tensor, - }, - }, - "anchors": fxt_random_tensor, - } - dst_state_dict = { - "model.model.model.weights": fxt_random_tensor, - "model.model.head.weights": fxt_random_tensor, - "model.model.anchors": fxt_random_tensor, - } - converted_state_dict = OTXv1Helper.load_det_ckpt(src_state_dict, add_prefix="model.model.") - self._check_ckpt_pairs(converted_state_dict, dst_state_dict) - - def test_load_iseg_ckpt(self, fxt_random_tensor: torch.Tensor) -> None: - src_state_dict = { - "model": { - "state_dict": { - "model.weights": fxt_random_tensor, - "head.weights": fxt_random_tensor, - "ema_model.weights": fxt_random_tensor, - }, - }, - } - - dst_state_dict = { - "model.model.model.weights": fxt_random_tensor, - "model.model.head.weights": fxt_random_tensor, - } - - converted_state_dict = OTXv1Helper.load_iseg_ckpt(src_state_dict, add_prefix="model.model.") - self._check_ckpt_pairs(converted_state_dict, dst_state_dict) - - def test_load_seg_segnext_ckpt(self, fxt_random_tensor: torch.Tensor) -> None: - src_state_dict = { - "model": { - "state_dict": { - "model.weights": fxt_random_tensor, - "head.weights": fxt_random_tensor, - "ham.bases.weights": fxt_random_tensor, - }, - }, - } - - dst_state_dict = { - "model.model.model.weights": fxt_random_tensor, - "model.model.head.weights": fxt_random_tensor, - } - - converted_state_dict = OTXv1Helper.load_seg_segnext_ckpt(src_state_dict, add_prefix="model.model.") - self._check_ckpt_pairs(converted_state_dict, dst_state_dict) - - def test_load_seg_lite_hrnet_ckpt(self, fxt_random_tensor: torch.Tensor) -> None: - src_state_dict = { - "model": { - "state_dict": { - "model.weights": fxt_random_tensor, - "head.weights": fxt_random_tensor, - "decode_head.aggregator.projects.weights": fxt_random_tensor, - }, - }, - } - - dst_state_dict = { - "model.model.model.weights": fxt_random_tensor, - "model.model.head.weights": fxt_random_tensor, - } - - converted_state_dict = OTXv1Helper.load_seg_lite_hrnet_ckpt(src_state_dict, add_prefix="model.model.") - self._check_ckpt_pairs(converted_state_dict, dst_state_dict) - - def test_load_action_ckpt(self, fxt_random_tensor: torch.Tensor) -> None: - src_state_dict = { - "model": { - "state_dict": { - "model.weights": fxt_random_tensor, - "head.weights": fxt_random_tensor, - }, - }, - } - - dst_state_dict = { - "model.model.model.weights": fxt_random_tensor, - "model.model.head.weights": fxt_random_tensor, - } - - converted_state_dict = OTXv1Helper.load_iseg_ckpt(src_state_dict, add_prefix="model.model.") - self._check_ckpt_pairs(converted_state_dict, dst_state_dict) diff --git a/tests/unit/algo/visual_prompting/__init__.py b/tests/unit/algo/visual_prompting/__init__.py deleted file mode 100644 index 06fe10f2165..00000000000 --- a/tests/unit/algo/visual_prompting/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom algo modules of OTX visual prompting task.""" diff --git a/tests/unit/algo/visual_prompting/backbones/__init__.py b/tests/unit/algo/visual_prompting/backbones/__init__.py deleted file mode 100644 index 12988224643..00000000000 --- a/tests/unit/algo/visual_prompting/backbones/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom backbones of OTX visual prompting task.""" diff --git a/tests/unit/algo/visual_prompting/backbones/test_tiny_vit.py b/tests/unit/algo/visual_prompting/backbones/test_tiny_vit.py deleted file mode 100644 index 334ec8d3ec4..00000000000 --- a/tests/unit/algo/visual_prompting/backbones/test_tiny_vit.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import pytest -import torch -from otx.algo.visual_prompting.backbones.tiny_vit import ( - Attention, - BasicLayer, - Conv2d_BN, - ConvLayer, - MBConv, - Mlp, - PatchEmbed, - PatchMerging, - TinyViT, - TinyViTBlock, -) -from torch import nn - - -class TestConv2d_BN: # noqa: N801 - @pytest.fixture() - def conv2d_bn(self) -> Conv2d_BN: - return Conv2d_BN(a=1, b=1) - - def test_init(self, conv2d_bn) -> None: - """Test __init__.""" - assert isinstance(conv2d_bn.c, nn.Conv2d) - assert isinstance(conv2d_bn.bn, nn.BatchNorm2d) - - def test_fuse(self, conv2d_bn) -> None: - """Test fuse.""" - fulsed_module = conv2d_bn.fuse() - - tmp_w = conv2d_bn.bn.weight / (conv2d_bn.bn.running_var + conv2d_bn.bn.eps) ** 0.5 - new_w = conv2d_bn.c.weight * tmp_w[:, None, None, None] - new_b = conv2d_bn.bn.bias - conv2d_bn.bn.running_mean * tmp_w - - assert torch.isclose(fulsed_module.weight, new_w) - assert torch.isclose(fulsed_module.bias, new_b) - - -class TestPatchEmbed: - def test_forward(self) -> None: - """Test forward.""" - patch_embed = PatchEmbed(in_chans=3, embed_dim=4, resolution=6, activation=nn.Identity) - input_tensor = torch.rand(1, 3, 6, 6) - - results = patch_embed(input_tensor) - - assert results.shape == torch.Size((1, 4, 2, 2)) - - -class TestMBConv: - def test_forward(self) -> None: - """Test forward.""" - mbconv = MBConv(in_chans=3, out_chans=3, expand_ratio=1.0, activation=nn.Identity, drop_path=1.0) - input_tensor = torch.rand(1, 3, 24, 24) - - results = mbconv(input_tensor) - - assert results.shape == torch.Size((1, 3, 24, 24)) - - -class TestPatchMerging: - def test_forward(self) -> None: - """Test forward.""" - patch_merging = PatchMerging(input_resolution=(6, 6), dim=3, out_dim=4, activation=nn.Identity) - input_tensor = torch.rand(1, 3, 6, 6) - - results = patch_merging(input_tensor) - - assert results.shape == torch.Size((1, 9, 4)) - - -class TestConvLayer: - def test_forward(self) -> None: - """Test forward.""" - conv_layer = ConvLayer(dim=3, input_resolution=(6, 6), depth=1, activation=nn.Identity) - input_tensor = torch.rand(1, 3, 6, 6) - - results = conv_layer(input_tensor) - - assert results.shape == torch.Size((1, 3, 6, 6)) - - -class TestMlp: - def test_forward(self) -> None: - """Test forward.""" - mlp = Mlp(in_features=4, hidden_features=5, out_features=6) - input_tensor = torch.rand(1, 4) - - results = mlp(input_tensor) - - assert results.shape == torch.Size((1, 6)) - - -class TestAttention: - def test_forward(self) -> None: - """Test forward.""" - attention = Attention(dim=4, key_dim=4, num_heads=1, attn_ratio=1, resolution=(2, 2)) - input_tensor = torch.rand(9, 4, 4) - - results = attention(input_tensor) - - assert results.shape == torch.Size((9, 4, 4)) - - -class TestTinyViTBlock: - @pytest.fixture() - def tiny_vit_block(self) -> TinyViTBlock: - return TinyViTBlock( - dim=4, - input_resolution=(6, 6), - num_heads=1, - window_size=2, - mlp_ratio=1.0, - ) - - def test_forward(self, tiny_vit_block) -> None: - """Test forward.""" - input_tensor = torch.rand(1, 36, 4) - - results = tiny_vit_block(input_tensor) - - assert results.shape == torch.Size((1, 36, 4)) - - -class TestBasicLayer: - @pytest.fixture() - def basic_layer(self) -> BasicLayer: - return BasicLayer( - dim=4, - input_resolution=(6, 6), - depth=1, - num_heads=1, - window_size=2, - ) - - def test_forward(self, basic_layer) -> None: - """Test forward.""" - input_tensor = torch.rand(1, 36, 4) - - results = basic_layer(input_tensor) - - assert results.shape == torch.Size((1, 36, 4)) - - def test_extra_repr(self, basic_layer) -> None: - """Test extra_repr.""" - assert basic_layer.extra_repr() == "dim=4, input_resolution=(6, 6), depth=1" - - -class TestTinyViT: - @pytest.fixture() - def tiny_vit(self) -> TinyViT: - return TinyViT( - img_size=1024, - embed_dims=[64, 128, 160, 320], - depths=[2, 2, 6, 2], - num_heads=[2, 4, 5, 10], - window_sizes=[7, 7, 14, 7], - drop_path_rate=0.0, - layer_lr_decay=2.0, - ) - - def test_forward(self, tiny_vit) -> None: - """Test forward.""" - input_tensor = torch.rand(1, 3, 1024, 1024) - - results = tiny_vit(input_tensor) - - assert results.shape == torch.Size((1, 256, 64, 64)) diff --git a/tests/unit/algo/visual_prompting/backbones/test_vit.py b/tests/unit/algo/visual_prompting/backbones/test_vit.py deleted file mode 100644 index 92dcb8c4a3e..00000000000 --- a/tests/unit/algo/visual_prompting/backbones/test_vit.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import pytest -import torch -from otx.algo.visual_prompting.backbones.vit import ( - Attention, - Block, - PatchEmbed, - ViT, - add_decomposed_rel_pos, - get_rel_pos, - window_partition, - window_unpartition, -) -from otx.algo.visual_prompting.utils import MLPBlock -from torch import Tensor, nn - - -class TestViT: - @pytest.fixture() - def vit(self) -> ViT: - return ViT(img_size=4, patch_size=2, embed_dim=8, depth=2, num_heads=2, out_chans=4) - - def test_init(self, vit) -> None: - """Test init.""" - assert isinstance(vit.patch_embed, PatchEmbed) - assert isinstance(vit.blocks, nn.ModuleList) - assert len(vit.blocks) == 2 - assert isinstance(vit.neck, nn.Sequential) - - @pytest.mark.parametrize(("inputs", "expected"), [(torch.empty((1, 3, 4, 4)), (1, 4, 2, 2))]) - def test_forward(self, vit, inputs: Tensor, expected: tuple[int]) -> None: - """Test forward.""" - results = vit.forward(inputs) - - assert results.shape == expected - - -class TestBlock: - @pytest.fixture() - def block(self) -> Block: - return Block(dim=8, num_heads=2) - - def test_init(self, block) -> None: - """Test init.""" - assert isinstance(block.attn, Attention) - assert isinstance(block.mlp, MLPBlock) - - @pytest.mark.parametrize(("inputs", "expected"), [(torch.empty((1, 4, 4, 8)), (1, 4, 4, 8))]) - def test_forward(self, block, inputs: Tensor, expected: tuple[int]) -> None: - """Test forward.""" - results = block.forward(inputs) - - assert results.shape == expected - - -class TestAttention: - @pytest.fixture() - def attention(self) -> Attention: - return Attention(dim=8, num_heads=2) - - def test_init(self, attention) -> None: - """Test init.""" - assert isinstance(attention.qkv, nn.Linear) - assert isinstance(attention.proj, nn.Linear) - - @pytest.mark.parametrize(("inputs", "expected"), [(torch.empty((1, 4, 4, 8)), (1, 4, 4, 8))]) - def test_forward(self, attention, inputs: Tensor, expected: tuple[int]) -> None: - """Test forward.""" - results = attention.forward(inputs) - - assert results.shape == expected - - -@pytest.mark.parametrize( - ("inputs", "window_size", "expected"), - [(torch.empty((1, 4, 4, 4)), 2, ((4, 2, 2, 4), (4, 4)))], -) -def test_window_partition(inputs: Tensor, window_size: int, expected: tuple[int]) -> None: - """Test window_partition.""" - results = window_partition(inputs, window_size) - windows, (hp, wp) = results - - assert windows.shape == expected[0] - assert (hp, wp) == expected[1] - - -@pytest.mark.parametrize( - ("windows", "window_size", "pad_hw", "hw", "expected"), - [(torch.empty((2, 2, 2, 2)), 2, (2, 2), (4, 4), (2, 2, 2, 2))], -) -def test_window_unpartition( - windows: Tensor, - window_size: int, - pad_hw: tuple[int, int], - hw: tuple[int, int], - expected: tuple[int], -) -> None: - """Test window_unpartition.""" - results = window_unpartition(windows, window_size, pad_hw, hw) - - assert results.shape == expected - - -@pytest.mark.parametrize( - ("q_size", "k_size", "rel_pos", "expected"), - [(2, 2, torch.empty((1, 2, 2)), (2, 2, 4)), (2, 2, torch.empty((3, 2, 2)), (2, 2, 2, 2))], -) -def test_get_rel_pos(q_size: int, k_size: int, rel_pos: Tensor, expected: tuple[int]) -> None: - """Test get_rel_pos.""" - results = get_rel_pos(q_size, k_size, rel_pos) - - assert results.shape == expected - - -@pytest.mark.parametrize( - ("attn", "q", "rel_pos_h", "rel_pos_w", "q_size", "k_size", "expected"), - [ - ( - torch.empty((1, 4, 4)), - torch.empty((1, 4, 1)), - torch.empty((1, 2, 2, 2)), - torch.empty((1, 2, 2, 2)), - (2, 2), - (2, 2), - (1, 4, 4), - ), - ], -) -def test_add_decomposed_rel_pos( - attn: Tensor, - q: Tensor, - rel_pos_h: Tensor, - rel_pos_w: Tensor, - q_size: tuple[int, int], - k_size: tuple[int, int], - expected: tuple[int], -) -> None: - """Test add_decomposed_rel_pos.""" - results = add_decomposed_rel_pos(attn, q, rel_pos_h, rel_pos_w, q_size, k_size) - - assert results.shape == expected - - -class TestPatchEmbed: - @pytest.fixture() - def patch_embed(self) -> PatchEmbed: - return PatchEmbed(kernel_size=(2, 2), stride=(2, 2), embed_dim=8) - - def test_init(self, patch_embed) -> None: - """Test init.""" - assert isinstance(patch_embed.proj, nn.Conv2d) - - @pytest.mark.parametrize(("inputs", "expected"), [(torch.empty((8, 3, 2, 2)), (8, 1, 1, 8))]) - def test_forward(self, patch_embed, inputs: Tensor, expected: tuple[int]) -> None: - """Test forward.""" - results = patch_embed.forward(torch.empty((8, 3, 2, 2))) - - assert results.shape == expected diff --git a/tests/unit/algo/visual_prompting/conftest.py b/tests/unit/algo/visual_prompting/conftest.py deleted file mode 100644 index f9567775649..00000000000 --- a/tests/unit/algo/visual_prompting/conftest.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (C) 2023-2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import pytest -import torch -from otx.core.data.entity.base import ImageInfo, Points -from otx.core.data.entity.visual_prompting import ( - VisualPromptingBatchDataEntity, - VisualPromptingBatchPredEntity, - VisualPromptingDataEntity, - ZeroShotVisualPromptingBatchDataEntity, - ZeroShotVisualPromptingBatchPredEntity, - ZeroShotVisualPromptingDataEntity, -) -from torchvision import tv_tensors - - -@pytest.fixture(scope="session") -def fxt_vpm_data_entity() -> ( - tuple[VisualPromptingDataEntity, VisualPromptingBatchDataEntity, VisualPromptingBatchPredEntity] -): - img_size = (1024, 1024) - fake_image = tv_tensors.Image(torch.rand(img_size)) - fake_image_info = ImageInfo(img_idx=0, img_shape=img_size, ori_shape=img_size) - fake_bboxes = tv_tensors.BoundingBoxes( - [[0, 0, 1, 1]], - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_size, - dtype=torch.float32, - ) - fake_points = Points([[2, 2]], canvas_size=img_size, dtype=torch.float32) - fake_masks = tv_tensors.Mask(torch.rand(img_size)) - fake_labels = {"bboxes": torch.as_tensor([1], dtype=torch.int64)} - fake_polygons = [None] - # define data entity - single_data_entity = VisualPromptingDataEntity( - image=fake_image, - img_info=fake_image_info, - masks=fake_masks, - labels=fake_labels, - polygons=fake_polygons, - bboxes=fake_bboxes, - points=fake_points, - ) - batch_data_entity = VisualPromptingBatchDataEntity( - batch_size=1, - images=[fake_image], - imgs_info=[fake_image_info], - masks=[fake_masks], - labels=[fake_labels], - polygons=[fake_polygons], - bboxes=[fake_bboxes], - points=[fake_points], - ) - batch_pred_data_entity = VisualPromptingBatchPredEntity( - batch_size=1, - images=[fake_image], - imgs_info=[fake_image_info], - masks=[fake_masks], - labels=[fake_labels], - polygons=[fake_polygons], - bboxes=[fake_bboxes], - points=[fake_points], - scores=[], - ) - - return single_data_entity, batch_data_entity, batch_pred_data_entity - - -@pytest.fixture(scope="session") -def fxt_zero_shot_vpm_data_entity() -> ( - tuple[ - ZeroShotVisualPromptingDataEntity, - ZeroShotVisualPromptingBatchDataEntity, - ZeroShotVisualPromptingBatchPredEntity, - ] -): - img_size = (1024, 1024) - fake_image = tv_tensors.Image(torch.rand(img_size)) - fake_image_info = ImageInfo(img_idx=0, img_shape=img_size, ori_shape=img_size) - fake_bboxes = tv_tensors.BoundingBoxes( - [[0, 0, 1, 1]], - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_size, - dtype=torch.float32, - ) - fake_points = Points([[2, 2]], canvas_size=img_size, dtype=torch.float32) - fake_masks = tv_tensors.Mask(torch.rand(img_size)) - fake_labels = torch.as_tensor([1], dtype=torch.int64) - fake_polygons = [None] - # define data entity - single_data_entity = ZeroShotVisualPromptingDataEntity( - image=fake_image, - img_info=fake_image_info, - masks=fake_masks, - labels=fake_labels, - polygons=fake_polygons, - prompts=[fake_bboxes, fake_points], - ) - batch_data_entity = ZeroShotVisualPromptingBatchDataEntity( - batch_size=1, - images=[fake_image], - imgs_info=[fake_image_info], - masks=[fake_masks], - labels=[fake_labels], - polygons=[fake_polygons], - prompts=[[fake_bboxes, fake_points]], - ) - batch_pred_data_entity = ZeroShotVisualPromptingBatchPredEntity( - batch_size=1, - images=[fake_image], - imgs_info=[fake_image_info], - masks=[fake_masks], - labels=[fake_labels], - polygons=[fake_polygons], - prompts=[[fake_bboxes, fake_points]], - scores=[], - ) - - return single_data_entity, batch_data_entity, batch_pred_data_entity diff --git a/tests/unit/algo/visual_prompting/decoders/__init__.py b/tests/unit/algo/visual_prompting/decoders/__init__.py deleted file mode 100644 index 0b6599494ee..00000000000 --- a/tests/unit/algo/visual_prompting/decoders/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom decoders of OTX visual prompting task.""" diff --git a/tests/unit/algo/visual_prompting/decoders/test_sam_mask_decoder.py b/tests/unit/algo/visual_prompting/decoders/test_sam_mask_decoder.py deleted file mode 100644 index 2aa5a998690..00000000000 --- a/tests/unit/algo/visual_prompting/decoders/test_sam_mask_decoder.py +++ /dev/null @@ -1,276 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import pytest -import torch -from otx.algo.visual_prompting.decoders.sam_mask_decoder import ( - MLP, - Attention, - SAMMaskDecoder, - TwoWayAttentionBlock, - TwoWayTransformer, -) -from otx.algo.visual_prompting.utils.mlp_block import MLPBlock -from torch import nn - - -class TestSAMMaskDecoder: - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.num_multimask_outputs = 3 - self.sam_mask_decoder = SAMMaskDecoder( - transformer_dim=8, - transformer_cfg={"depth": 2, "embedding_dim": 8, "mlp_dim": 8, "num_heads": 2}, - num_multimask_outputs=self.num_multimask_outputs, - iou_head_depth=1, - iou_head_hidden_dim=8, - ) - - def test_init(self) -> None: - """Test init.""" - assert isinstance(self.sam_mask_decoder.transformer, TwoWayTransformer) - assert isinstance(self.sam_mask_decoder.iou_token, nn.Embedding) - assert isinstance(self.sam_mask_decoder.mask_tokens, nn.Embedding) - assert isinstance(self.sam_mask_decoder.output_upscaling, nn.Sequential) - assert isinstance(self.sam_mask_decoder.output_hypernetworks_mlps, nn.ModuleList) - assert len(self.sam_mask_decoder.output_hypernetworks_mlps) == self.num_multimask_outputs + 1 - assert isinstance(self.sam_mask_decoder.iou_prediction_head, MLP) - - @pytest.mark.parametrize( - ( - "image_embeddings", - "image_pe", - "sparse_prompt_embeddings", - "dense_prompt_embeddings", - "multimask_output", - "expected", - ), - [ - ( - torch.empty((1, 8, 2, 2)), - torch.empty((1, 8, 2, 2)), - torch.empty((1, 4, 8)), - torch.empty((1, 8, 2, 2)), - False, - (torch.Size((1, 1, 8, 8)), torch.Size((1, 1))), - ), - ( - torch.empty((1, 8, 2, 2)), - torch.empty((1, 8, 2, 2)), - torch.empty((1, 4, 8)), - torch.empty((1, 8, 2, 2)), - True, - (torch.Size((1, 3, 8, 8)), torch.Size((1, 3))), - ), - ], - ) - def test_forward( - self, - image_embeddings: torch.Tensor, - image_pe: torch.Tensor, - sparse_prompt_embeddings: torch.Tensor, - dense_prompt_embeddings: torch.Tensor, - multimask_output: bool, - expected: tuple[torch.Size, ...], - ) -> None: - """Test forward.""" - results = self.sam_mask_decoder.forward( - image_embeddings, - image_pe, - sparse_prompt_embeddings, - dense_prompt_embeddings, - multimask_output, - ) - masks, iou_pred = results - - assert masks.shape == expected[0] - assert iou_pred.shape == expected[1] - - @pytest.mark.parametrize( - ("image_embeddings", "image_pe", "sparse_prompt_embeddings", "dense_prompt_embeddings", "expected"), - [ - ( - torch.empty((1, 8, 2, 2)), - torch.empty((1, 8, 2, 2)), - torch.empty((1, 4, 8)), - torch.empty((1, 8, 2, 2)), - (torch.Size((1, 4, 8, 8)), torch.Size((1, 4))), - ), - ], - ) - def test_predict_masks( - self, - image_embeddings: torch.Tensor, - image_pe: torch.Tensor, - sparse_prompt_embeddings: torch.Tensor, - dense_prompt_embeddings: torch.Tensor, - expected: tuple[torch.Size, ...], - ) -> None: - """Test predict_masks.""" - results = self.sam_mask_decoder.predict_masks( - image_embeddings, - image_pe, - sparse_prompt_embeddings, - dense_prompt_embeddings, - ) - masks, iou_pred = results - - assert masks.shape == expected[0] - assert iou_pred.shape == expected[1] - - -class TestMLP: - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.mlp = MLP(input_dim=4, hidden_dim=4, output_dim=4, num_layers=2) - - def test_init(self) -> None: - """Test init.""" - assert len(self.mlp.layers) == 2 - - @pytest.mark.parametrize(("inputs", "expected"), [(torch.empty((1, 1, 4)), torch.Size((1, 1, 4)))]) - def test_forward(self, inputs: torch.Tensor, expected: torch.Size) -> None: - """Test forward.""" - results = self.mlp.forward(inputs) - - assert results.shape == expected - - -class TestTwoWayTransformer: - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.depth = 2 - self.embedding_dim = 8 - self.num_heads = 2 - self.two_way_transformer = TwoWayTransformer( - depth=self.depth, - embedding_dim=self.embedding_dim, - num_heads=self.num_heads, - mlp_dim=self.embedding_dim, - ) - - def test_init(self) -> None: - """Test init.""" - assert len(self.two_way_transformer.layers) == self.depth - assert isinstance(self.two_way_transformer.final_attn_token_to_image, Attention) - assert isinstance(self.two_way_transformer.norm_final_attn, nn.LayerNorm) - - @pytest.mark.parametrize( - ("image_embedding", "image_pe", "point_embedding", "expected"), - [ - ( - torch.empty((1, 8, 2, 2)), - torch.empty((1, 8, 2, 2)), - torch.empty((1, 1, 8)), - (torch.Size((1, 1, 8)), torch.Size((1, 4, 8))), - ), - ], - ) - def test_forward( - self, - image_embedding: torch.Tensor, - image_pe: torch.Tensor, - point_embedding: torch.Tensor, - expected: tuple[torch.Size, ...], - ) -> None: - """Test forward.""" - results = self.two_way_transformer.forward(image_embedding, image_pe, point_embedding) - queries, keys = results - - assert queries.shape == expected[0] - assert keys.shape == expected[1] - - -class TestTwoWayAttentionBlock: - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.embedding_dim = 8 - self.num_heads = 2 - self.two_way_attention_block = TwoWayAttentionBlock( - embedding_dim=self.embedding_dim, - num_heads=self.num_heads, - mlp_dim=self.embedding_dim, - ) - - def test_init(self) -> None: - """Test init.""" - assert isinstance(self.two_way_attention_block.self_attn, Attention) - assert isinstance(self.two_way_attention_block.norm1, nn.LayerNorm) - assert isinstance(self.two_way_attention_block.cross_attn_token_to_image, Attention) - assert isinstance(self.two_way_attention_block.norm2, nn.LayerNorm) - assert isinstance(self.two_way_attention_block.mlp, MLPBlock) - assert isinstance(self.two_way_attention_block.norm3, nn.LayerNorm) - assert isinstance(self.two_way_attention_block.norm4, nn.LayerNorm) - assert isinstance(self.two_way_attention_block.cross_attn_image_to_token, Attention) - - @pytest.mark.parametrize(("queries", "keys", "query_pe", "key_pe"), [[torch.empty((1, 8, 8)) for _ in range(4)]]) - def test_forward( - self, - queries: torch.Tensor, - keys: torch.Tensor, - query_pe: torch.Tensor, - key_pe: torch.Tensor, - ) -> None: - """Test forward.""" - results = self.two_way_attention_block.forward(queries, keys, query_pe, key_pe) - queries, keys = results - - assert queries.shape == torch.Size((1, 8, 8)) - assert keys.shape == torch.Size((1, 8, 8)) - - -class TestAttention: - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.embedding_dim = 8 - self.num_heads = 2 - self.attention = Attention(embedding_dim=self.embedding_dim, num_heads=self.num_heads) - - def test_init(self) -> None: - """Test init.""" - assert isinstance(self.attention.q_proj, nn.Linear) - assert self.attention.q_proj.in_features == self.embedding_dim - assert isinstance(self.attention.k_proj, nn.Linear) - assert self.attention.k_proj.in_features == self.embedding_dim - assert isinstance(self.attention.v_proj, nn.Linear) - assert self.attention.v_proj.in_features == self.embedding_dim - assert isinstance(self.attention.out_proj, nn.Linear) - assert self.attention.out_proj.out_features == self.embedding_dim - - @pytest.mark.parametrize( - ("x", "expected"), - [ - (torch.empty((1, 4, 4)), torch.Size((1, 2, 4, 2))), - (torch.empty((1, 2, 2)), torch.Size((1, 2, 2, 1))), - ], - ) - def test_separate_heads(self, x: torch.Tensor, expected: torch.Size) -> None: - """Test _separate_heads.""" - results = self.attention._separate_heads(x, self.num_heads) - - assert results.shape == expected - - @pytest.mark.parametrize( - ("x", "expected"), - [ - (torch.empty((1, 2, 4, 2)), torch.Size((1, 4, 4))), - (torch.empty((1, 2, 2, 1)), torch.Size((1, 2, 2))), - ], - ) - def test_recombine_heads(self, x: torch.Tensor, expected: torch.Size) -> None: - """Test _recombine_heads.""" - results = self.attention._recombine_heads(x) - - assert results.shape == expected - - @pytest.mark.parametrize( - ("q", "k", "v", "expected"), - [(torch.empty((1, 1, 8)), torch.empty((1, 1, 8)), torch.empty((1, 1, 8)), torch.Size((1, 1, 8)))], - ) - def test_forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, expected: torch.Size) -> None: - """Test forward.""" - results = self.attention.forward(q, k, v) - - assert results.shape == expected diff --git a/tests/unit/algo/visual_prompting/encoders/__init__.py b/tests/unit/algo/visual_prompting/encoders/__init__.py deleted file mode 100644 index e50ab12e0dc..00000000000 --- a/tests/unit/algo/visual_prompting/encoders/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom encoders of OTX visual prompting task.""" diff --git a/tests/unit/algo/visual_prompting/encoders/test_sam_image_encoder.py b/tests/unit/algo/visual_prompting/encoders/test_sam_image_encoder.py deleted file mode 100644 index f1f87b81ffa..00000000000 --- a/tests/unit/algo/visual_prompting/encoders/test_sam_image_encoder.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import pytest -from otx.algo.visual_prompting.encoders.sam_image_encoder import SAMImageEncoder - - -class TestSAMImageEncoder: - @pytest.mark.parametrize( - ("backbone", "expected"), - [ - ("tiny_vit", "TinyViT"), - ], - ) - def test_new(self, backbone: str, expected: str) -> None: - """Test __new__.""" - sam_image_encoder = SAMImageEncoder(backbone=backbone) - - assert sam_image_encoder.__class__.__name__ == expected diff --git a/tests/unit/algo/visual_prompting/encoders/test_sam_prompt_encoder.py b/tests/unit/algo/visual_prompting/encoders/test_sam_prompt_encoder.py deleted file mode 100644 index dbb57cb74ea..00000000000 --- a/tests/unit/algo/visual_prompting/encoders/test_sam_prompt_encoder.py +++ /dev/null @@ -1,147 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import pytest -import torch -from otx.algo.visual_prompting.encoders.sam_prompt_encoder import PositionEmbeddingRandom, SAMPromptEncoder -from torch import nn - - -class TestSAMPromptEncoder: - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.image_embedding_size = 4 - self.input_image_size = (4, 4) - self.prompt_encoder = SAMPromptEncoder( - embed_dim=4, - image_embedding_size=(self.image_embedding_size, self.image_embedding_size), - input_image_size=self.input_image_size, - mask_in_chans=4, - ) - - def test_init(self) -> None: - """Test init.""" - assert isinstance(self.prompt_encoder.pe_layer, PositionEmbeddingRandom) - assert isinstance(self.prompt_encoder.point_embeddings, nn.ModuleList) - assert isinstance(self.prompt_encoder.not_a_point_embed, nn.Embedding) - assert isinstance(self.prompt_encoder.mask_input_size, tuple) - assert isinstance(self.prompt_encoder.mask_downscaling, nn.Sequential) - assert isinstance(self.prompt_encoder.no_mask_embed, nn.Embedding) - - def test_get_dense_pe(self) -> None: - """Test get_dense_pe.""" - results = self.prompt_encoder.get_dense_pe() - - assert results.shape == torch.Size( - (1, self.image_embedding_size, self.image_embedding_size, self.image_embedding_size), - ) - - @pytest.mark.parametrize( - ("points", "labels", "pad", "expected"), - [ - (torch.ones((1, 2, 2), dtype=torch.float32), torch.Tensor([[0, 1]]), True, torch.Size((1, 3, 4))), - (torch.ones((1, 2, 2), dtype=torch.float32), torch.Tensor([[0, 1]]), False, torch.Size((1, 2, 4))), - ], - ) - def test_embed_points(self, points: torch.Tensor, labels: torch.Tensor, pad: bool, expected: torch.Size) -> None: - """Test _embed_points.""" - results = self.prompt_encoder._embed_points(points, labels, pad) - - assert results.shape == expected - - @pytest.mark.parametrize(("boxes", "expected"), [(torch.Tensor([[0, 0, 1, 1]]), torch.Size((1, 2, 4)))]) - def test_embed_boxes(self, boxes: torch.Tensor, expected: torch.Size) -> None: - """Test _embed_boxes.""" - results = self.prompt_encoder._embed_boxes(boxes) - - assert results.shape == expected - - @pytest.mark.parametrize(("mask", "expected"), [(torch.zeros((1, 4, 4)), torch.Size((4, 1, 1)))]) - def test_embed_masks(self, mask: torch.Tensor, expected: torch.Size) -> None: - """Test _embed_masks.""" - results = self.prompt_encoder._embed_masks(mask) - - assert results.shape == expected - - @pytest.mark.parametrize( - ("points", "boxes", "masks", "expected"), - [ - ((torch.tensor([1]), torch.tensor([1])), None, None, 1), - (None, torch.Tensor([[0, 0, 1, 1]]), None, 1), - (None, None, torch.zeros((1, 2, 2)), 1), - (None, None, None, 1), - ], - ) - def test_get_batch_size( - self, - points: tuple[torch.Tensor, torch.Tensor] | None, - boxes: torch.Tensor | None, - masks: torch.Tensor | None, - expected: int, - ) -> None: - """Test _get_batch_size.""" - results = self.prompt_encoder._get_batch_size(points, boxes, masks) - - assert results == expected - - @pytest.mark.parametrize( - ("points", "boxes", "masks", "expected"), - [ - ( - (torch.ones((1, 2, 2), dtype=torch.float32), torch.Tensor([[0, 1]])), - None, - None, - (torch.Size((1, 3, 4)), torch.Size((1, 4, 4, 4))), - ), - (None, torch.Tensor([[0, 0, 1, 1]]), None, (torch.Size((1, 2, 4)), torch.Size((1, 4, 4, 4)))), - (None, None, torch.zeros((1, 4, 4)), (torch.Size((1, 0, 4)), torch.Size((4, 1, 1)))), - (None, None, None, (torch.Size((1, 0, 4)), torch.Size((1, 4, 4, 4)))), - ], - ) - def test_forward( - self, - points: tuple[torch.Tensor, torch.Tensor] | None, - boxes: torch.Tensor | None, - masks: torch.Tensor | None, - expected: tuple[torch.Size, ...], - ) -> None: - """Test forward.""" - results = self.prompt_encoder.forward(points, boxes, masks) - sparse_embeddings, dense_embeddings = results - - assert sparse_embeddings.shape == expected[0] - assert dense_embeddings.shape == expected[1] - - -class TestPositionEmbeddingRandom: - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.position_embedding_random = PositionEmbeddingRandom(num_pos_feats=4) - - def test_init(self) -> None: - """Test init.""" - assert hasattr(self.position_embedding_random, "positional_encoding_gaussian_matrix") - assert self.position_embedding_random.positional_encoding_gaussian_matrix.shape == torch.Size((2, 4)) - - def test_pe_encoding(self) -> None: - """Test _pe_encoding.""" - results = self.position_embedding_random._pe_encoding(torch.ones((2, 2, 2), dtype=torch.float32)) - - assert results.shape == torch.Size((2, 2, 8)) - - def test_forward(self) -> None: - """Test forward.""" - results = self.position_embedding_random.forward(size=(2, 2)) - - assert results.shape == torch.Size((8, 2, 2)) - - def test_forward_with_coords(self) -> None: - """Test forward_with_coords.""" - results = self.position_embedding_random.forward_with_coords( - coords_input=torch.ones((2, 2, 2), dtype=torch.float32), - image_size=(2, 2), - ) - - assert results.shape == torch.Size((2, 2, 8)) diff --git a/tests/unit/algo/visual_prompting/test_openvino_models.py b/tests/unit/algo/visual_prompting/test_openvino_models.py deleted file mode 100644 index bd821b1f60c..00000000000 --- a/tests/unit/algo/visual_prompting/test_openvino_models.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from typing import Any - -import numpy as np -import pytest -from openvino.model_api.adapters.openvino_adapter import OpenvinoAdapter -from openvino.model_api.models import ImageModel, SegmentationModel -from openvino.model_api.models.types import NumericalValue -from otx.algo.visual_prompting.openvino_models import VisualPromptingDecoder, VisualPromptingImageEncoder - - -class TestVisualPromptingImageEncoder: - def test_parameters(self): - """Test parameters.""" - params = VisualPromptingImageEncoder.parameters() - - assert params.get("resize_type").default_value == "fit_to_window" - assert params.get("image_size").default_value == 1024 - - def test_preproces(self, mocker): - """Test preprocess.""" - mocker.patch.object(ImageModel, "__init__") - image_encoder = VisualPromptingImageEncoder("adapter") - fake_inputs = np.ones((4, 4, 3)) - image_encoder.h, image_encoder.w, image_encoder.c = fake_inputs.shape - image_encoder.image_blob_name = "images" - image_encoder.resize_type = "fit_to_window" - - dict_inputs, meta = image_encoder.preprocess(fake_inputs) - - assert dict_inputs["images"].shape == (1, 4, 4, 3) - assert meta["original_shape"] == (4, 4, 3) - assert meta["resized_shape"] == (4, 4, 3) - assert "resize_type" in meta - assert meta["resize_type"] == "fit_to_window" - - -class TestVisualPromptingDecoder: - @pytest.fixture(autouse=True) - def setup(self, mocker): - mocker.patch.object(SegmentationModel, "__init__") - mocker_model_adapter = mocker.Mock(spec=OpenvinoAdapter) - self.decoder = VisualPromptingDecoder(mocker_model_adapter) - self.decoder.image_size = 6 - - def test_parameters(self): - """Test parameters.""" - params = VisualPromptingDecoder.parameters() - - assert isinstance(params.get("image_size"), NumericalValue) - assert params.get("image_size").default_value == 1024 - - def test_get_outputs(self): - """Test _get_outputs.""" - results = self.decoder._get_outputs() - - assert results == "upscaled_masks" - - @pytest.mark.parametrize( - ("prompts", "expected"), - [ - ( - { - "bboxes": [np.array([[1, 1], [2, 2]])], - "points": [], - "labels": {"bboxes": [1]}, - "orig_size": (4, 4), - }, - { - "point_coords": (1, 2, 2), - "point_labels": (1, 2), - }, - ), - ( - {"bboxes": [], "points": [np.array([[1, 1]])], "labels": {"points": [1]}, "orig_size": (4, 4)}, - { - "point_coords": (1, 1, 2), - "point_labels": (1, 1), - }, - ), - ], - ) - def test_preprocess(self, prompts: dict[str, Any], expected: dict[str, Any]): - """Test preprocess""" - results = self.decoder.preprocess(prompts) - - assert isinstance(results, list) - assert "point_coords" in results[0] - assert results[0]["point_coords"].shape == expected["point_coords"] - assert "point_labels" in results[0] - assert results[0]["point_labels"].shape == expected["point_labels"] - assert "mask_input" in results[0] - assert "has_mask_input" in results[0] - assert "orig_size" in results[0] - - def test_apply_coords(self): - """Test apply_coords.""" - coords = np.array([[[1, 1], [2, 2]]]) - original_size = (12, 12) - - results = self.decoder.apply_coords(coords, original_size) - - assert results.shape == (1, 2, 2) - assert np.all(results == np.array([[[0.5, 0.5], [1.0, 1.0]]])) - - @pytest.mark.parametrize( - ("old_h", "old_w", "image_size", "expected"), - [ - (4, 3, 6, (6, 5)), - (3, 4, 6, (5, 6)), - ], - ) - def test_get_preprocess_shape(self, old_h: int, old_w: int, image_size: int, expected: tuple[int]): - """Test _get_preprocess_shape.""" - result = self.decoder._get_preprocess_shape(old_h, old_w, image_size) - - assert result == expected - - def test_get_inputs(self): - """Test _get_inputs.""" - self.decoder.inputs = {"images": np.ones((1, 4, 4, 3))} - - returned_value = self.decoder._get_inputs() - - assert returned_value[0] == ["images"] - - def test_postprocess(self, mocker): - """Test postprocess.""" - self.decoder.output_blob_name = "upscaled_masks" - self.decoder.mask_threshold = 0.0 - self.decoder.blur_strength = 2 - fake_output = {"upscaled_masks": np.ones((1, 1, 4, 4)), "scores": 0.1} - fake_metadata = {"orig_size": np.array([[6, 6]]), "label": [1]} - - returned_value = self.decoder.postprocess(outputs=fake_output, meta=fake_metadata) - - assert isinstance(returned_value, dict) - assert "upscaled_masks" in returned_value - assert returned_value["upscaled_masks"].shape == (1, 1, 4, 4) - assert "scores" in returned_value - assert returned_value["scores"] == 0.1 - assert "hard_prediction" in returned_value - assert returned_value["hard_prediction"].shape == (1, 4, 4) - assert "soft_prediction" in returned_value - assert returned_value["soft_prediction"].shape == (1, 4, 4) - assert np.all(returned_value["soft_prediction"] == 0.1 * np.ones((1, 4, 4))) diff --git a/tests/unit/algo/visual_prompting/test_segment_anything.py b/tests/unit/algo/visual_prompting/test_segment_anything.py deleted file mode 100644 index 329289e976a..00000000000 --- a/tests/unit/algo/visual_prompting/test_segment_anything.py +++ /dev/null @@ -1,352 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import pytest -import torch -from otx.algo.visual_prompting.segment_anything import OTXSegmentAnything, SegmentAnything -from otx.core.data.entity.base import Points -from otx.core.data.entity.visual_prompting import VisualPromptingBatchPredEntity -from torch import Tensor -from torchvision import tv_tensors - - -class TestSegmentAnything: - @pytest.mark.parametrize(("backbone", "expected_backbone"), [("tiny_vit", "TinyViT")]) - @pytest.mark.parametrize("freeze_image_encoder", [True, False]) - @pytest.mark.parametrize("freeze_prompt_encoder", [True, False]) - @pytest.mark.parametrize("freeze_mask_decoder", [True, False]) - def test_init( - self, - mocker, - backbone: str, - expected_backbone: str, - freeze_image_encoder: bool, - freeze_prompt_encoder: bool, - freeze_mask_decoder: bool, - ) -> None: - """Test __init__.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_state_dict") - mocker_load_state_dict_from_url = mocker.patch( - "otx.algo.visual_prompting.segment_anything.torch.hub.load_state_dict_from_url", - ) - segment_anything = SegmentAnything( - backbone=backbone, - freeze_image_encoder=freeze_image_encoder, - freeze_prompt_encoder=freeze_prompt_encoder, - freeze_mask_decoder=freeze_mask_decoder, - ) - - # check import modules - assert hasattr(segment_anything, "image_encoder") - assert segment_anything.image_encoder.__class__.__name__ == expected_backbone - assert hasattr(segment_anything, "prompt_encoder") - assert hasattr(segment_anything, "mask_decoder") - - # check load_checkpoint - mocker_load_state_dict_from_url.assert_called_once() - - # check freeze_networks - for param in segment_anything.image_encoder.parameters(): - assert param.requires_grad == (freeze_image_encoder is False) - - for param in segment_anything.prompt_encoder.parameters(): - assert param.requires_grad == (freeze_prompt_encoder is False) - - for param in segment_anything.mask_decoder.parameters(): - assert param.requires_grad == (freeze_mask_decoder is False) - - @pytest.mark.parametrize( - "ori_shape", - [ - torch.tensor([512, 256]), - torch.tensor([256, 512]), - torch.tensor([1536, 1280]), - torch.tensor([1280, 1536]), - ], - ) - def test_forward_inference(self, mocker, ori_shape: Tensor) -> None: - """Test forward_inference.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - segment_anything.training = False - - image_embeddings = torch.zeros(1, 256, 64, 64, dtype=torch.float32) - point_coords = torch.tensor([[[0, 0], [10, 10]]], dtype=torch.float32) - point_labels = torch.tensor([[2, 3]], dtype=torch.float32) - mask_input = torch.zeros(1, 1, 256, 256, dtype=torch.float32) - has_mask_inputs = torch.tensor([[0.0]]) - - results = segment_anything.forward_inference( - image_embeddings=image_embeddings, - point_coords=point_coords, - point_labels=point_labels, - mask_input=mask_input, - has_mask_input=has_mask_inputs[0], - ori_shape=ori_shape, - ) - - assert results[0].shape[2:] == torch.Size(ori_shape) - - @pytest.mark.parametrize("training", [True, False]) - @pytest.mark.parametrize( - "ori_shapes", - [ - [torch.tensor([512, 256])], - [torch.tensor([256, 512])], - [torch.tensor([1536, 1280])], - [torch.tensor([1280, 1536])], - ], - ) - def test_forward_train(self, mocker, training: bool, ori_shapes: list[Tensor]) -> None: - """Test forward_train.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - segment_anything.training = training - - images = tv_tensors.Image(torch.zeros((1, 3, 1024, 1024), dtype=torch.float32)) - bboxes = [ - tv_tensors.BoundingBoxes( - torch.tensor([[0, 0, 10, 10]]), - format="xyxy", - canvas_size=(1024, 1024), - dtype=torch.float32, - ), - ] - points = [Points(torch.tensor([[5, 5]]), canvas_size=(1024, 1024), dtype=torch.float32)] - gt_masks = [torch.zeros((2, *os)) for os in ori_shapes] if training else None - - results = segment_anything.forward_train( - images=images, - ori_shapes=ori_shapes, - bboxes=bboxes, - points=points, - gt_masks=gt_masks, - ) - - if training: - # check loss - assert isinstance(results, dict) - for metric in ["loss", "loss_focal", "loss_dice", "loss_iou"]: - assert isinstance(results[metric], Tensor) - assert results["loss"].ndim == 0 - else: - assert isinstance(results, tuple) - assert all([isinstance(r, list) for r in results]) # noqa: C419 - assert all([isinstance(r[0], Tensor) for r in results]) # noqa: C419 - - # check post_processed_pred_masks - assert results[0][0][0].shape == torch.Size(ori_shapes[0]) - - # check ious - assert results[1][0].ndim == 2 - - @pytest.mark.parametrize( - ("point_coords", "point_labels", "expected"), - [ - (Tensor([[[1, 1]]]), Tensor([[1]]), (1, 1, 256)), - (Tensor([[[1, 1], [2, 2]]]), Tensor([[2, 3]]), (1, 2, 256)), - ], - ) - def test_embed_points(self, mocker, point_coords: Tensor, point_labels: Tensor, expected: tuple[int]) -> None: - """Test _embed_points.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - - results = segment_anything._embed_points(point_coords, point_labels) - - assert results.shape == expected - - @pytest.mark.parametrize( - ("masks_input", "has_mask_input", "expected"), - [ - (torch.randn(1, 1, 4, 4, dtype=torch.float), torch.tensor([1], dtype=torch.float), (1, 256, 1, 1)), - ], - ) - def test_embed_masks(self, mocker, masks_input: Tensor, has_mask_input: Tensor, expected: tuple[int]) -> None: - """Test _embed_masks.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - - results = segment_anything._embed_masks(masks_input, has_mask_input) - - assert results.shape == expected - - @pytest.mark.parametrize( - ("inputs", "targets", "expected"), - [ - (Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.0])), - (Tensor([[0, 0, 0.5, 0.5, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.25])), - (Tensor([[0, 0, 0.3, 0.3, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.3888888359])), - ], - ) - def test_calculate_dice_loss(self, mocker, inputs: Tensor, targets: Tensor, expected: Tensor) -> None: - """Test calculate_dice_loss.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - - results = segment_anything.calculate_dice_loss(inputs, targets, num_masks=1) - - assert torch.isclose(results, expected) - - @pytest.mark.parametrize( - ("inputs", "targets", "expected"), - [ - (Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.0])), - (Tensor([[0, 0, 0.5, 0.5, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.0098766042])), - (Tensor([[0, 0, 0.3, 0.3, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.0226361733])), - ], - ) - def test_calculate_sigmoid_ce_focal_loss(self, mocker, inputs: Tensor, targets: Tensor, expected: Tensor) -> None: - """Test calculate_sigmoid_ce_focal_loss.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - - results = segment_anything.calculate_sigmoid_ce_focal_loss(inputs, targets, num_masks=1) - - assert torch.isclose(results, expected) - - @pytest.mark.parametrize( - ("inputs", "targets", "expected"), - [ - (Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([1.0])), - (Tensor([[0, 0, 0.5, 0.5, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([1.0])), - (Tensor([[0, 0, 0.3, 0.3, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.0])), - ], - ) - def test_calculate_iou(self, mocker, inputs: Tensor, targets: Tensor, expected: Tensor) -> None: - """Test calculate_iou.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - - results = segment_anything.calculate_iou(inputs, targets) - - assert results == expected - - @pytest.mark.parametrize( - ("input_size", "orig_size", "expected"), - [ - (6, torch.tensor((8, 8)), torch.Size((8, 8))), - (6, torch.tensor((10, 8)), torch.Size((10, 8))), - (6, torch.tensor((8, 10)), torch.Size((8, 10))), - ], - ) - def test_postprocess_masks(self, mocker, input_size: int, orig_size: Tensor, expected: torch.Size) -> None: - """Test postprocess_masks.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - masks = torch.zeros((1, 1, 4, 4)) - - results = segment_anything.postprocess_masks(masks, input_size, orig_size).squeeze() - - assert results.shape == expected - - @pytest.mark.parametrize( - ("input_image_size", "expected"), - [ - (torch.tensor((2, 4)), torch.tensor((3, 6))), - (torch.tensor((4, 2)), torch.tensor((6, 3))), - ], - ) - def test_get_prepadded_size(self, mocker, input_image_size: Tensor, expected: Tensor) -> None: - """Test get_prepadded_size.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - - longest_side = 6 - - results = segment_anything.get_prepadded_size(input_image_size, longest_side) - - assert torch.all(results == expected) - - @pytest.mark.parametrize( - ("masks", "expected"), - [ - (Tensor([[[-2, -2], [2, 2]]]), 1), - (Tensor([[[-2, -2], [1, 1]]]), 0), - ], - ) - def test_calculate_stability_score(self, mocker, masks: Tensor, expected: int) -> None: - """Test calculate_stability_score.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - - results = segment_anything.calculate_stability_score(masks, mask_threshold=0.0, threshold_offset=1.0) - - assert results == expected - - def test_select_masks(self, mocker) -> None: - """Test select_masks.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - segment_anything = SegmentAnything(backbone="tiny_vit") - - masks = Tensor([[[[1]], [[2]], [[3]], [[4]]]]) - iou_preds = Tensor([[0.1, 0.2, 0.3, 0.4]]) - num_points = 1 - - selected_mask, selected_iou_pred = segment_anything.select_masks(masks, iou_preds, num_points) - - assert masks[:, -1, :, :] == selected_mask - assert iou_preds[:, -1] == selected_iou_pred - - -class TestOTXSegmentAnything: - @pytest.fixture() - def model(self) -> OTXSegmentAnything: - return OTXSegmentAnything(backbone="tiny_vit", num_classes=0) - - def test_create_model(self, model) -> None: - """Test _create_model.""" - segment_anything = model._create_model() - assert segment_anything is not None - assert isinstance(segment_anything, torch.nn.Module) - assert segment_anything.__class__.__name__ == "SegmentAnything" - - def test_customize_inputs(self, model, fxt_vpm_data_entity) -> None: - """Test _customize_inputs.""" - output_data = model._customize_inputs(fxt_vpm_data_entity[1]) - assert output_data is not None - assert output_data["mode"] == "finetuning" - assert isinstance(output_data["ori_shapes"][0], Tensor) - assert output_data["images"].shape[-2:] == torch.Size(output_data["ori_shapes"][0]) - assert isinstance(output_data["images"], tv_tensors.Image) - assert output_data["gt_masks"][0].shape[-2:] == torch.Size(output_data["ori_shapes"][0]) - assert isinstance(output_data["bboxes"][0], tv_tensors.BoundingBoxes) - assert isinstance(output_data["points"][0], tuple) - assert isinstance(output_data["points"][0][0], Points) - assert isinstance(output_data["points"][0][1], Tensor) - - def test_customize_outputs(self, model, fxt_vpm_data_entity) -> None: - """Test _customize_outputs.""" - # training - outputs = {"loss": torch.tensor(1.0)} - result = model._customize_outputs(outputs, fxt_vpm_data_entity[1]) - assert isinstance(result, dict) - assert "loss" in result - - # inference - model.training = False - outputs = (torch.tensor([1]), torch.tensor([1])) - result = model._customize_outputs(outputs, fxt_vpm_data_entity[1]) - assert isinstance(result, VisualPromptingBatchPredEntity) - assert result.masks[0].data == outputs[0] - assert result.scores[0] == outputs[1] - - def test_inspect_prompts(self, model) -> None: - """Test _inspect_prompts.""" - # TODO(sungchul): Add point prompts # noqa: TD003 - prompts: list[tv_tensors.BoundingBoxes] = [ - tv_tensors.BoundingBoxes( - [[0, 0, 1, 1]], - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=(2, 2), - dtype=torch.float32, - ), - tv_tensors.BoundingBoxes(torch.zeros((0, 4)), format=tv_tensors.BoundingBoxFormat.XYXY, canvas_size=(2, 2)), - ] - - result = model._inspect_prompts(prompts) - - assert torch.all(result[0] == prompts[0]) - assert result[1] is None diff --git a/tests/unit/algo/visual_prompting/test_zero_shot_segment_anything.py b/tests/unit/algo/visual_prompting/test_zero_shot_segment_anything.py deleted file mode 100644 index 95578b3de69..00000000000 --- a/tests/unit/algo/visual_prompting/test_zero_shot_segment_anything.py +++ /dev/null @@ -1,750 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from pathlib import Path -from typing import Any, Callable - -import pytest -import torch -from otx.algo.visual_prompting.zero_shot_segment_anything import ( - OTXZeroShotSegmentAnything, - PromptGetter, - ZeroShotSegmentAnything, -) -from otx.core.data.entity.base import Points -from otx.core.data.entity.visual_prompting import ZeroShotVisualPromptingBatchPredEntity -from torch import Tensor -from torchvision import tv_tensors - - -class TestPromptGetter: - @pytest.fixture() - def prompt_getter(self) -> PromptGetter: - return PromptGetter(image_size=3, downsizing=1) - - def test_set_default_thresholds(self, prompt_getter) -> None: - """Test set_default_thresholds.""" - assert prompt_getter.default_threshold_reference == 0.3 - assert prompt_getter.default_threshold_target == 0.65 - - prompt_getter.set_default_thresholds(default_threshold_reference=0.5, default_threshold_target=0.7) - - assert prompt_getter.default_threshold_reference == 0.5 - assert prompt_getter.default_threshold_target == 0.7 - - @pytest.mark.parametrize( - "result_point_selection", - [torch.tensor([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]]), torch.tensor([[-1, -1, -1]])], - ) - def test_forward(self, mocker, prompt_getter, result_point_selection: Tensor) -> None: - """Test forward.""" - mocker.patch("otx.algo.visual_prompting.zero_shot_segment_anything.ZeroShotSegmentAnything") - mocker.patch.object(prompt_getter, "_point_selection", return_value=(result_point_selection, torch.zeros(1, 2))) - - image_embeddings = torch.ones(1, 4, 4, 4) - reference_feat = torch.rand(1, 4) - ori_shape = torch.tensor((prompt_getter.image_size, prompt_getter.image_size), dtype=torch.int64) - - points_scores, bg_coords = prompt_getter( - image_embeddings=image_embeddings, - reference_feat=reference_feat, - ori_shape=ori_shape, - ) - - assert torch.all(points_scores == result_point_selection) - assert torch.all(bg_coords == torch.zeros(1, 2)) - - @pytest.mark.parametrize( - "result_point_selection", - [torch.tensor([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]]), torch.tensor([[-1, -1, -1]])], - ) - def test_get_prompt_candidates(self, mocker, prompt_getter, result_point_selection: Tensor) -> None: - """Test get_prompt_candidates.""" - mocker.patch.object(prompt_getter, "_point_selection", return_value=(result_point_selection, torch.zeros(1, 2))) - image_embeddings = torch.ones(1, 4, 4, 4) - reference_feats = torch.rand(1, 1, 4) - used_indices = torch.as_tensor([0]) - ori_shape = torch.tensor([prompt_getter.image_size, prompt_getter.image_size], dtype=torch.int64) - - total_points_scores, total_bg_coords = prompt_getter.get_prompt_candidates( - image_embeddings=image_embeddings, - reference_feats=reference_feats, - used_indices=used_indices, - ori_shape=ori_shape, - ) - - assert total_points_scores[0].shape[0] == len(result_point_selection) - assert total_bg_coords[0].shape[0] == 1 - - @pytest.mark.parametrize( - ("mask_sim", "expected"), - [ - ( - torch.arange(0.1, 1.0, 0.1).reshape(3, 3), - torch.tensor([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]]), - ), - (torch.zeros(3, 3), torch.tensor([[-1, -1, -1]])), - ], - ) - def test_point_selection(self, prompt_getter, mask_sim: torch.Tensor, expected: torch.Tensor) -> None: - """Test _point_selection.""" - points_scores, bg_coords = prompt_getter._point_selection( - mask_sim=mask_sim, - ori_shape=torch.tensor([prompt_getter.image_size, prompt_getter.image_size]), - threshold=torch.tensor([[0.5]]), - num_bg_points=torch.tensor([[1]], dtype=torch.int64), - ) - - assert torch.equal(points_scores, expected) - - -class TestZeroShotSegmentAnything: - @pytest.fixture() - def build_zero_shot_segment_anything(self) -> Callable: - def _build_zero_shot_segment_anything( - backbone: str = "tiny_vit", - freeze_image_encoder: bool = True, - freeze_prompt_encoder: bool = True, - freeze_mask_decoder: bool = True, - default_threshold_reference: float = 0.3, - default_threshold_target: float = 0.65, - ) -> ZeroShotSegmentAnything: - return ZeroShotSegmentAnything( - backbone=backbone, - freeze_image_encoder=freeze_image_encoder, - freeze_prompt_encoder=freeze_prompt_encoder, - freeze_mask_decoder=freeze_mask_decoder, - default_threshold_reference=default_threshold_reference, - default_threshold_target=default_threshold_target, - ) - - return _build_zero_shot_segment_anything - - @pytest.mark.parametrize(("backbone", "expected_backbone"), [("tiny_vit", "TinyViT")]) - @pytest.mark.parametrize("freeze_image_encoder", [True, False]) - @pytest.mark.parametrize("freeze_prompt_encoder", [True, False]) - @pytest.mark.parametrize("freeze_mask_decoder", [True, False]) - def test_init( - self, - mocker, - build_zero_shot_segment_anything, - backbone: str, - expected_backbone: str, - freeze_image_encoder: bool, - freeze_prompt_encoder: bool, - freeze_mask_decoder: bool, - ) -> None: - """Test __init__.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_state_dict") - mocker_load_state_dict_from_url = mocker.patch( - "otx.algo.visual_prompting.segment_anything.torch.hub.load_state_dict_from_url", - ) - zero_shot_segment_anything = build_zero_shot_segment_anything( - backbone=backbone, - freeze_image_encoder=freeze_image_encoder, - freeze_prompt_encoder=freeze_prompt_encoder, - freeze_mask_decoder=freeze_mask_decoder, - default_threshold_reference=0.3, - default_threshold_target=0.65, - ) - - # check import modules - assert hasattr(zero_shot_segment_anything, "image_encoder") - assert zero_shot_segment_anything.image_encoder.__class__.__name__ == expected_backbone - assert hasattr(zero_shot_segment_anything, "prompt_encoder") - assert hasattr(zero_shot_segment_anything, "mask_decoder") - - # check load_checkpoint - mocker_load_state_dict_from_url.assert_called_once() - - # check freeze_networks - for param in zero_shot_segment_anything.image_encoder.parameters(): - assert not param.requires_grad - - for param in zero_shot_segment_anything.prompt_encoder.parameters(): - assert not param.requires_grad - - for param in zero_shot_segment_anything.mask_decoder.parameters(): - assert not param.requires_grad - - @pytest.mark.parametrize( - "kwargs", - [ - {}, - { - "backbone": "tiny_vit", - }, - { - "mask_threshold": 0.0, - "use_stability_score": True, - "return_single_mask": True, - "return_extra_metrics": True, - "stability_score_offset": 2.0, - }, - ], - ) - def test_set_default_config(self, mocker, kwargs: dict[str, Any]) -> None: - """Test set_default_config.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - zero_shot_segment_anything = ZeroShotSegmentAnything(**kwargs) - - assert zero_shot_segment_anything.image_encoder.__class__.__name__ == "TinyViT" - for param in zero_shot_segment_anything.image_encoder.parameters(): - assert not param.requires_grad - - for param in zero_shot_segment_anything.prompt_encoder.parameters(): - assert not param.requires_grad - - for param in zero_shot_segment_anything.mask_decoder.parameters(): - assert not param.requires_grad - - for key, value in kwargs.items(): - if key in ["backbone"]: - continue - assert getattr(zero_shot_segment_anything, key) == value - - @pytest.mark.parametrize("new_largest_label", [0, 3]) - def test_expand_reference_info(self, build_zero_shot_segment_anything, new_largest_label: int) -> None: - """Test expand_reference_info.""" - zero_shot_segment_anything = build_zero_shot_segment_anything() - reference_feats = torch.zeros(0, 1, 256) - - results = zero_shot_segment_anything.expand_reference_info( - reference_feats=reference_feats, - new_largest_label=new_largest_label, - ) - - assert len(results) == new_largest_label + 1 - - def test_learn(self, mocker, build_zero_shot_segment_anything) -> None: - """Test learn.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - zero_shot_segment_anything = build_zero_shot_segment_anything() - images = [tv_tensors.Image(torch.zeros((1, 3, 1024, 1024), dtype=torch.float32))] - processed_prompts = [ - { - torch.tensor(0): [ - tv_tensors.BoundingBoxes( - torch.tensor([[0, 0, 10, 10]]), - format="xyxy", - canvas_size=(1024, 1024), - dtype=torch.float32, - ), - Points(torch.tensor([[5, 5]]), canvas_size=(1024, 1024), dtype=torch.float32), - ], - }, - ] - reference_feats = torch.zeros(0, 1, 256) - used_indices = torch.tensor([], dtype=torch.int64) - ori_shapes = [torch.tensor((1024, 1024))] - - reference_info, ref_masks = zero_shot_segment_anything.learn( - images=images, - processed_prompts=processed_prompts, - reference_feats=reference_feats, - used_indices=used_indices, - ori_shapes=ori_shapes, - ) - - assert reference_info["reference_feats"].shape == torch.Size((1, 1, 256)) - assert ref_masks[0].shape == torch.Size((1, *ori_shapes[0])) - assert 0 in reference_info["used_indices"] - - new_processed_prompts = [ - { - torch.tensor(1): [ - tv_tensors.BoundingBoxes( - torch.tensor([[0, 0, 10, 10]]), - format="xyxy", - canvas_size=(1024, 1024), - dtype=torch.float32, - ), - Points(torch.tensor([[5, 5]]), canvas_size=(1024, 1024), dtype=torch.float32), - ], - }, - ] - - reference_info, ref_masks = zero_shot_segment_anything.learn( - images=images, - processed_prompts=new_processed_prompts, - reference_feats=reference_info["reference_feats"], - used_indices=reference_info["used_indices"], - ori_shapes=ori_shapes, - ) - - assert reference_info["reference_feats"].shape == torch.Size((2, 1, 256)) - assert ref_masks[0].shape == torch.Size((2, *ori_shapes[0])) - assert 0 in reference_info["used_indices"] - assert 1 in reference_info["used_indices"] - - def test_infer(self, mocker, build_zero_shot_segment_anything) -> None: - """Test infer.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - zero_shot_segment_anything = build_zero_shot_segment_anything() - mocker.patch.object( - zero_shot_segment_anything.prompt_getter, - "get_prompt_candidates", - return_value=({0: torch.tensor([[0, 0, 0.5], [1000, 1000, 0.7]])}, {0: torch.tensor([[500, 500]])}), - ) - - def _patch_predict_masks(**kwargs) -> Tensor: - point_coords = kwargs.get("point_coords") - mask = torch.zeros(*kwargs["ori_shape"], dtype=torch.bool) - mask[int(point_coords[0, 0, 1]), int(point_coords[0, 0, 0])] = True - return mask - - zero_shot_segment_anything._predict_masks = _patch_predict_masks - - images = [tv_tensors.Image(torch.zeros((1, 3, 1024, 1024), dtype=torch.float32))] - reference_feats = torch.rand(1, 1, 1, 256) - used_indices = {0: [0]} - ori_shapes = [torch.tensor((1024, 1024))] - - results = zero_shot_segment_anything.infer( - images=images, - reference_feats=reference_feats, - used_indices=used_indices, - ori_shapes=ori_shapes, - ) - - for predicted_masks, used_points in results: - for label, predicted_mask in predicted_masks.items(): - for pm, up in zip(predicted_mask, used_points[label]): - assert pm[int(up[1]), int(up[0])] == up[2] - - def test_inspect_overlapping_areas(self, mocker, build_zero_shot_segment_anything) -> None: - """Test _inspect_overlapping_areas.""" - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - zero_shot_segment_anything = build_zero_shot_segment_anything() - predicted_masks = { - 0: [ - torch.tensor( - [ - [1, 1, 0, 0, 0, 0], - [1, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - ], - ), - torch.tensor( - [ - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 0], - [0, 0, 1, 1, 1, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - ], - ), - torch.tensor( - [ - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 1, 1, 1, 0, 0], - [0, 1, 1, 1, 0, 0], - ], - ), - ], - 1: [ - torch.tensor( - [ - [0, 0, 0, 1, 1, 0], - [0, 0, 0, 1, 1, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - ], - ), - torch.tensor( - [ - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1], - [0, 0, 0, 0, 1, 1], - ], - ), - torch.tensor( - [ - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 1, 1, 0, 0, 0], - [0, 1, 1, 0, 0, 0], - ], - ), - torch.tensor( - [ - [1, 1, 0, 0, 0, 0], - [1, 1, 0, 0, 0, 0], - [1, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - ], - ), - ], - } - used_points = { - 0: [ - torch.tensor([0, 0, 0.5]), # to be removed - torch.tensor([2, 2, 0.5]), - torch.tensor([1, 4, 0.5]), - ], - 1: [ - torch.tensor([3, 0, 0.5]), - torch.tensor([4, 4, 0.5]), - torch.tensor([1, 4, 0.3]), # to be removed - torch.tensor([0, 0, 0.7]), - ], - } - - zero_shot_segment_anything._inspect_overlapping_areas(predicted_masks, used_points, threshold_iou=0.5) - - assert len(predicted_masks[0]) == 2 - assert len(predicted_masks[1]) == 3 - assert all(torch.tensor([2, 2, 0.5]) == used_points[0][0]) - assert all(torch.tensor([0, 0, 0.7]) == used_points[1][2]) - - def test_predict_masks(self, mocker, build_zero_shot_segment_anything) -> None: - """Test _predict_masks.""" - mocker.patch( - "otx.algo.visual_prompting.segment_anything.SegmentAnything.forward", - return_value=(torch.ones(1, 4, 8, 8), torch.tensor([[0.1, 0.2, 0.5, 0.7]]), torch.ones(1, 4, 4, 4)), - ) - - zero_shot_segment_anything = build_zero_shot_segment_anything() - zero_shot_segment_anything.image_size = 6 - - mask = zero_shot_segment_anything._predict_masks( - mode="infer", - image_embeddings=torch.rand(1), - point_coords=torch.rand(1, 2, 2), - point_labels=torch.randint(low=0, high=2, size=(1, 2)), - ori_shape=torch.tensor([8, 8], dtype=torch.int64), - ) - assert mask.shape == (8, 8) - - @pytest.mark.parametrize( - ("masks", "logits", "expected"), - [ - (torch.ones(1, 4, 8, 8), torch.ones(1, 4, 4, 4), torch.ones(8, 8)), - (torch.zeros(1, 4, 8, 8), torch.zeros(1, 4, 4, 4), torch.zeros(8, 8)), - ], - ) - def test_decide_cascade_results( - self, - mocker, - build_zero_shot_segment_anything, - masks: Tensor, - logits: Tensor, - expected: Tensor, - ) -> None: - mocker.patch("otx.algo.visual_prompting.segment_anything.SegmentAnything.load_checkpoint") - zero_shot_segment_anything = build_zero_shot_segment_anything() - scores = torch.tensor([[0.0, 0.1, 0.2, 0.3]]) - - _, result = zero_shot_segment_anything._decide_cascade_results(masks, logits, scores) - - assert torch.equal(result, expected) - - -class TestOTXZeroShotSegmentAnything: - @pytest.fixture() - def model(self) -> OTXZeroShotSegmentAnything: - return OTXZeroShotSegmentAnything(backbone="tiny_vit", num_classes=0) - - def test_create_model(self, model) -> None: - """Test _create_model.""" - zero_shot_segment_anything = model._create_model() - assert zero_shot_segment_anything is not None - assert isinstance(zero_shot_segment_anything, torch.nn.Module) - assert zero_shot_segment_anything.__class__.__name__ == "ZeroShotSegmentAnything" - - @pytest.mark.parametrize("training", [True, False]) - def test_forward(self, mocker, model, training: bool) -> None: - """Test forward.""" - mocker_learn = mocker.patch.object(model, "learn") - mocker_infer = mocker.patch.object(model, "infer") - model.training = training - - model.forward(None) - - if training: - mocker_learn.assert_called_once() - else: - mocker_infer.assert_called_once() - - @pytest.mark.parametrize("reset_feat", [True, False]) - def test_learn(self, mocker, model, reset_feat: bool) -> None: - """Test learn.""" - mocker_initialize_reference_info = mocker.patch.object(model, "initialize_reference_info") - mocker_learn = mocker.patch.object(model.model, "learn") - mocker_customize_inputs = mocker.patch.object(model, "_customize_inputs") - mocker_customize_outputs = mocker.patch.object(model, "_customize_outputs") - - model.learn(None, reset_feat=reset_feat) - - if reset_feat: - mocker_initialize_reference_info.assert_called_once() - else: - mocker_initialize_reference_info.assert_not_called() - mocker_learn.assert_called_once() - mocker_customize_inputs.assert_called_once() - mocker_customize_outputs.assert_called_once() - - def test_infer(self, mocker, model) -> None: - """Test infer.""" - mocker_infer = mocker.patch.object(model.model, "infer") - mocker_customize_inputs = mocker.patch.object(model, "_customize_inputs") - mocker_customize_outputs = mocker.patch.object(model, "_customize_outputs") - - model.infer(None) - - mocker_infer.assert_called_once() - mocker_customize_inputs.assert_called_once() - mocker_customize_outputs.assert_called_once() - - @pytest.mark.parametrize("is_training", [True, False]) - def test_customize_inputs_learn( - self, - model: OTXZeroShotSegmentAnything, - fxt_zero_shot_vpm_data_entity, - is_training: bool, - ) -> None: - """Test _customize_inputs with training=True.""" - model.training = is_training - model.initialize_reference_info() - output_data = model._customize_inputs(fxt_zero_shot_vpm_data_entity[1]) - - assert output_data is not None - assert isinstance(output_data["images"][0], tv_tensors.Image) - assert output_data["images"][0].shape[-2:] == torch.Size(output_data["ori_shapes"][0]) - assert isinstance(output_data["ori_shapes"][0], Tensor) - assert isinstance(output_data["reference_feats"], Tensor) - assert torch.all(output_data["reference_feats"] == model.reference_feats) - assert isinstance(output_data["used_indices"], Tensor) - assert torch.all(output_data["used_indices"] == model.used_indices) - - if is_training: - assert "processed_prompts" in output_data - else: - assert "is_cascade" in output_data - - def test_customize_inputs_infer(self, model: OTXZeroShotSegmentAnything, fxt_zero_shot_vpm_data_entity) -> None: - """Test _customize_inputs with training=False.""" - model.training = False - model.reference_feats = torch.rand(1, 1, 256) - model.used_indices = torch.tensor([0.0]) - output_data = model._customize_inputs(fxt_zero_shot_vpm_data_entity[1]) - - assert output_data is not None - assert isinstance(output_data["images"][0], tv_tensors.Image) - assert output_data["images"][0].shape[-2:] == torch.Size(output_data["ori_shapes"][0]) - assert isinstance(output_data["ori_shapes"][0], Tensor) - assert "reference_feats" in output_data - assert torch.all(output_data["reference_feats"] == model.reference_feats) - assert torch.all(output_data["used_indices"] == model.used_indices) - - def test_customize_outputs(self, model, fxt_zero_shot_vpm_data_entity) -> None: - """Test _customize_outputs.""" - - # training - outputs = [ - {"reference_feats": torch.zeros(0, 1, 256)}, - {"used_indices": torch.tensor([1, 1, 1])}, - "reference_masks", - ] - model.training = True - result = model._customize_outputs(outputs, fxt_zero_shot_vpm_data_entity[1]) - assert result == outputs - - # inference - label = 0 - outputs = [[{label: [torch.tensor(0)]}, {label: [torch.tensor([1, 1, 1])]}]] - model.training = False - result = model._customize_outputs(outputs, fxt_zero_shot_vpm_data_entity[1]) - assert isinstance(result, ZeroShotVisualPromptingBatchPredEntity) - assert result.masks[0].data == outputs[0][0][label][0] - assert result.scores[0] == outputs[0][1][label][0][2] - assert result.labels[0] == label - assert torch.all(result.prompts[0].data == outputs[0][1][label][0][:2].unsqueeze(0)) - - def test_gather_prompts_with_labels(self, model) -> None: - """Test _gather_prompts_with_labels.""" - prompts = [[torch.tensor(0), torch.tensor(1), torch.tensor(2), torch.tensor(2), torch.tensor(4)]] - labels = [torch.tensor([0, 1, 2, 2, 4])] - - results = model._gather_prompts_with_labels(prompts, labels) - - assert results[0][0][0] == prompts[0][0] - assert results[0][1][0] == prompts[0][1] - assert results[0][2] == prompts[0][2:4] - assert results[0][4][0] == prompts[0][4] - - @pytest.mark.parametrize( - ("image", "expected"), - [ - (tv_tensors.Image(torch.zeros(3, 2, 4)), (3, 4, 8)), - (tv_tensors.Image(torch.zeros(3, 12, 16)), (3, 6, 8)), - ], - ) - def test_apply_image(self, model, image: tv_tensors.Image, expected: tuple[int, ...]) -> None: - """Test apply_image.""" - results = model.apply_image(image, target_length=8) - - assert results.shape == expected - - @pytest.mark.parametrize( - ("coords", "ori_shape", "expected"), - [ - (torch.tensor([[1, 1], [2, 2]]), (4, 4), torch.tensor([[2, 2], [4, 4]])), - (torch.tensor([[4, 4], [8, 8]]), (16, 16), torch.tensor([[2, 2], [4, 4]])), - ], - ) - def test_apply_points(self, model, coords: Tensor, ori_shape: tuple[int, int], expected: Tensor) -> None: - """Test apply_points.""" - result = model.apply_points(Points(coords, canvas_size=ori_shape), ori_shape, target_length=8) - - assert isinstance(result, torch.Tensor) - assert torch.equal(result, expected) - - @pytest.mark.parametrize( - ("boxes", "ori_shape", "expected"), - [ - (torch.tensor([[1, 1, 2, 2], [2, 2, 3, 3]]), (4, 4), torch.tensor([[2, 2, 4, 4], [4, 4, 6, 6]])), - (torch.tensor([[4, 4, 8, 8], [8, 8, 12, 12]]), (16, 16), torch.tensor([[2, 2, 4, 4], [4, 4, 6, 6]])), - ], - ) - def test_apply_boxes(self, model, boxes: Tensor, ori_shape: tuple[int, int], expected: Tensor) -> None: - """Test apply_boxes.""" - result = model.apply_boxes( - tv_tensors.BoundingBoxes(boxes, format="xyxy", canvas_size=ori_shape), - ori_shape, - target_length=8, - ) - - assert isinstance(result, torch.Tensor) - assert torch.equal(result, expected) - - @pytest.mark.parametrize( - ("prompts", "ori_shape", "expected"), - [ - ( - [ - Points([[4, 4], [8, 8]], canvas_size=(16, 16)), - tv_tensors.BoundingBoxes([[4, 4, 8, 8], [8, 8, 12, 12]], format="xyxy", canvas_size=(16, 16)), - ], - (16, 16), - [ - Points([[2, 2], [4, 4]], canvas_size=(8, 8)), - tv_tensors.BoundingBoxes([[2, 2, 4, 4], [4, 4, 6, 6]], format="xyxy", canvas_size=(8, 8)), - ], - ), - ], - ) - def test_apply_prompts( - self, - model, - prompts: list[Points | tv_tensors.BoundingBoxes], - ori_shape: tuple[int, int], - expected: list[Points | tv_tensors.BoundingBoxes], - ) -> None: - """Test apply_prompts.""" - results = model.apply_prompts(prompts, ori_shape, target_length=8) - - for r, e in zip(results, expected): - assert torch.all(r == e) - - @pytest.mark.parametrize( - ("oldh", "oldw", "expected"), - [ - (3, 4, (6, 8)), - (12, 16, (6, 8)), - ], - ) - def test_get_preprocess_shape(self, model, oldh: int, oldw: int, expected: tuple[int, int]): - """Test get_preprocess_shape.""" - results = model.get_preprocess_shape(oldh, oldw, target_length=8) - - assert results == expected - - @pytest.mark.parametrize("image", (tv_tensors.Image(torch.zeros(1, 3, 2, 4, dtype=torch.uint8)))) - def test_preprocess(self, model, image: tv_tensors.Image) -> None: - """Test preprocess.""" - model.pixel_mean = torch.ones_like(model.pixel_mean) - model.pixel_std = torch.ones_like(model.pixel_std) * 2 - model.model.image_size = 8 - - results = model.preprocess(image) - - assert results.shape == (3, 8, 8) - assert torch.all(torch.unique(results) == torch.tensor((-0.5, 0.0))) - - def test_initialize_reference_info(self, model) -> None: - """Test initialize_reference_info.""" - model.initialize_reference_info() - - assert model.reference_feats.shape == (0, 1, 256) - assert model.used_indices.shape == (0,) - - def test_find_latest_reference_info(self, mocker, model) -> None: - """Test _find_latest_reference_info.""" - mocker.patch( - "otx.algo.visual_prompting.zero_shot_segment_anything.os.path.isdir", - return_value=True, - ) - - # there are some saved reference info - mocker.patch( - "otx.algo.visual_prompting.zero_shot_segment_anything.os.listdir", - return_value=["1", "2"], - ) - results = model._find_latest_reference_info(Path()) - assert results == "2" - - # there are no saved reference info - mocker.patch( - "otx.algo.visual_prompting.zero_shot_segment_anything.os.listdir", - return_value=[], - ) - results = model._find_latest_reference_info(Path()) - assert results is None - - def test_load_latest_reference_info(self, mocker, model) -> None: - """Test load_latest_reference_info.""" - # get previously saved reference info - mocker.patch( - "otx.algo.visual_prompting.zero_shot_segment_anything.OTXZeroShotSegmentAnything._find_latest_reference_info", - return_value="1", - ) - mocker.patch( - "otx.algo.visual_prompting.zero_shot_segment_anything.torch.load", - return_value={"reference_feats": torch.zeros((1, 1, 256)), "used_indices": torch.tensor([0.0])}, - ) - mocker.patch("builtins.open", return_value="Mocked data") - - model.load_latest_reference_info() - assert model.reference_feats.shape == (1, 1, 256) - assert model.used_indices.shape == (1,) - - # no saved reference info - mocker.patch( - "otx.algo.visual_prompting.zero_shot_segment_anything.OTXZeroShotSegmentAnything._find_latest_reference_info", - return_value=None, - ) - - model.initialize_reference_info() - model.load_latest_reference_info() - - assert model.reference_feats.shape == (0, 1, 256) - assert model.used_indices.shape == (0,) diff --git a/tests/unit/algo/visual_prompting/utils/__init__.py b/tests/unit/algo/visual_prompting/utils/__init__.py deleted file mode 100644 index 0f653870200..00000000000 --- a/tests/unit/algo/visual_prompting/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Test of custom utils of OTX visual prompting task.""" diff --git a/tests/unit/algo/visual_prompting/utils/test_layer_norm_2d.py b/tests/unit/algo/visual_prompting/utils/test_layer_norm_2d.py deleted file mode 100644 index 93718efc71b..00000000000 --- a/tests/unit/algo/visual_prompting/utils/test_layer_norm_2d.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - - -import torch -from otx.algo.visual_prompting.utils.layer_norm_2d import LayerNorm2d - - -class TestLayerNorm2d: - def test_forward(self) -> None: - """Test forward.""" - layer_norm_2d = LayerNorm2d(num_channels=2) - - assert torch.all(layer_norm_2d.weight == torch.ones(2)) - assert torch.all(layer_norm_2d.bias == torch.zeros(2)) - - inputs = torch.arange(32, dtype=torch.float32).view(1, 2, 4, 4) - - result = layer_norm_2d(inputs) - - assert result.shape == inputs.shape - assert torch.all(result == torch.cat((torch.ones(1, 1, 4, 4) * (-1), torch.ones(1, 1, 4, 4)), dim=1)) diff --git a/tests/unit/algorithms/__init__.py b/tests/unit/algorithms/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/__init__.py b/tests/unit/algorithms/action/__init__.py new file mode 100644 index 00000000000..2faffbe2b1f --- /dev/null +++ b/tests/unit/algorithms/action/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/tests/unit/algorithms/action/adapters/__init__.py b/tests/unit/algorithms/action/adapters/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/mmaction/__init__.py b/tests/unit/algorithms/action/adapters/mmaction/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/mmaction/data/__init__.py b/tests/unit/algorithms/action/adapters/mmaction/data/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/data/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/mmaction/data/pipelines/__init__.py b/tests/unit/algorithms/action/adapters/mmaction/data/pipelines/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/data/pipelines/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/mmaction/data/pipelines/test_action_loading.py b/tests/unit/algorithms/action/adapters/mmaction/data/pipelines/test_action_loading.py new file mode 100755 index 00000000000..61f7615c535 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/data/pipelines/test_action_loading.py @@ -0,0 +1,64 @@ +"""Unit Test for otx.algorithms.action.adapters.mmaction.data.pipeline.loading..""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest + +from otx.algorithms.action.adapters.mmaction.data.cls_dataset import OTXActionClsDataset +from otx.algorithms.action.adapters.mmaction.data.pipelines.loading import ( + RawFrameDecode, +) +from otx.algorithms.action.configs.classification.x3d.data_pipeline import ( + train_pipeline, +) +from otx.api.entities.label import Domain +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.action.test_helpers import ( + MockPipeline, + generate_action_cls_otx_dataset, + generate_labels, +) + + +class TestRawFrameDecode: + """Test RawFrameDecode class. + + + 1. Create sample OTXActionClsDataset + 2. Get sample inputs from sample OTXActionClsDataset + 3. Add "frame_inds", "gt_bboxes", "proposals" attributes to sample inputs + 4. Check RawFrameDecode transform's results + 1. Whether transform creates imgs + 2. Whether transform creates proper img size + 3. Whether transform modify gt_bboxes and proposals w.r.t img_size + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.video_len = 3 + self.frame_len = 3 + self.labels = generate_labels(3, Domain.ACTION_CLASSIFICATION) + self.otx_dataset = generate_action_cls_otx_dataset(self.video_len, self.frame_len, self.labels) + self.pipeline = train_pipeline + self.dataset = OTXActionClsDataset(self.otx_dataset, self.labels, self.pipeline) + self.dataset.pipeline = MockPipeline() + + @e2e_pytest_unit + def test_call(self): + """Test __call__ function.""" + + inputs = self.dataset[0] + inputs["frame_inds"] = list(range(2)) + inputs["gt_bboxes"] = np.array([[0, 0, 1, 1]]) + inputs["proposals"] = np.array([[0, 0, 1, 1]]) + decode = RawFrameDecode() + decode.otx_dataset = self.otx_dataset + outputs = decode(inputs) + assert len(outputs["imgs"]) == 2 + assert outputs["original_shape"] == (256, 256) + assert outputs["img_shape"] == (256, 256) + assert np.all(outputs["gt_bboxes"] == np.array([[0, 0, 256, 256]])) + assert np.all(outputs["proposals"] == np.array([[0, 0, 256, 256]])) diff --git a/tests/unit/algorithms/action/adapters/mmaction/data/test_action_cls_dataset.py b/tests/unit/algorithms/action/adapters/mmaction/data/test_action_cls_dataset.py new file mode 100644 index 00000000000..61dadab8764 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/data/test_action_cls_dataset.py @@ -0,0 +1,91 @@ +"""Unit Test for otx.algorithms.action.adapters.mmaction.data.cls_dataset.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.algorithms.action.adapters.mmaction.data.cls_dataset import OTXActionClsDataset +from otx.algorithms.action.adapters.mmaction.data.pipelines import RawFrameDecode +from otx.algorithms.action.configs.classification.x3d.data_pipeline import ( + train_pipeline, +) +from otx.api.entities.label import Domain +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.action.test_helpers import ( + MockPipeline, + generate_action_cls_otx_dataset, + generate_labels, +) + + +class TestOTXActionClsDataset: + """Test OTXActionClsDataset class. + + 1. Check _DataInfoProxy + + 1. Create otx_dataset, labels + 2. Check len(_DataInfoProxy) + 2. Check data pipelines + 3. Check "__len__" function + 4. Check "prepare_train_frames" function + 5. Check "prepare_test_frames" function + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.video_len = 3 + self.frame_len = 3 + self.labels = generate_labels(3, Domain.ACTION_CLASSIFICATION) + self.otx_dataset = generate_action_cls_otx_dataset(self.video_len, self.frame_len, self.labels) + self.pipeline = train_pipeline + + @e2e_pytest_unit + def test_DataInfoProxy(self) -> None: + """Test _DataInfoProxy Class.""" + + proxy = OTXActionClsDataset._DataInfoProxy(self.otx_dataset, self.labels, modality="RGB") + sample = proxy[0] + assert len(proxy) == self.video_len + assert "total_frames" in sample + assert "start_index" in sample + assert "label" in sample + + @e2e_pytest_unit + def test_pipeline(self) -> None: + """Test RawFrameDecode transform contains otx_dataset.""" + + dataset = OTXActionClsDataset(self.otx_dataset, self.labels, self.pipeline) + for transform in dataset.pipeline.transforms: + if isinstance(transform, RawFrameDecode): + assert transform.otx_dataset == self.otx_dataset + + @e2e_pytest_unit + def test_len(self) -> None: + """Test dataset length is same with video_len.""" + + dataset = OTXActionClsDataset(self.otx_dataset, self.labels, self.pipeline) + assert len(dataset) == self.video_len + + @e2e_pytest_unit + def test_prepare_train_frames(self) -> None: + """Test prepare_train_frames function.""" + + dataset = OTXActionClsDataset(self.otx_dataset, self.labels, self.pipeline) + dataset.pipeline = MockPipeline() + sample = dataset.prepare_train_frames(0) + assert "total_frames" in sample + assert "start_index" in sample + assert "label" in sample + + @e2e_pytest_unit + def test_prepare_test_frames(self) -> None: + """Test prepare_test_frames function.""" + + dataset = OTXActionClsDataset(self.otx_dataset, self.labels, self.pipeline) + dataset.pipeline = MockPipeline() + sample = dataset.prepare_test_frames(0) + assert "total_frames" in sample + assert "start_index" in sample + assert "label" in sample diff --git a/tests/unit/algorithms/action/adapters/mmaction/data/test_action_det_dataset.py b/tests/unit/algorithms/action/adapters/mmaction/data/test_action_det_dataset.py new file mode 100644 index 00000000000..ee7c5c549fb --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/data/test_action_det_dataset.py @@ -0,0 +1,142 @@ +"""Unit Test for otx.algorithms.action.adapters.mmaction.data.cls_dataset..""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest + +from otx.algorithms.action.adapters.mmaction.data.det_dataset import OTXActionDetDataset +from otx.algorithms.action.adapters.mmaction.data.pipelines import RawFrameDecode +from otx.algorithms.action.configs.detection.base.data_pipeline import train_pipeline +from otx.api.entities.label import Domain +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.action.test_helpers import ( + MockPipeline, + generate_action_det_otx_dataset, + generate_labels, +) + + +class MockDataInfoProxy(OTXActionDetDataset._DataInfoProxy): + """Mock class for _DataInfoProxy of OTXAcitonDetDataset.""" + + def __init__(self, proposals): + self.proposals = proposals + + +class TestOTXActionDetDataset: + """Test OTXActionDetDataset class. + + 1. Check _DataInfoProxy + + 1. Create otx_dataset, labels + 2. Create _DataInfoProxy + 3. Check metadata and annotations + 4. Create proposals and check _patch_proposals + 2. Check pipelines + 3. Check loading functions + 4. Check evaluation function + + 1. Create sample detection inference results + 2. Check det2csv function's results + 3. Check _get_predictions function's results + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.video_len = 3 + self.frame_len = 5 + self.labels = generate_labels(3, Domain.ACTION_DETECTION) + self.otx_dataset, self.proposals = generate_action_det_otx_dataset(self.video_len, self.frame_len, self.labels) + self.pipeline = train_pipeline + + @e2e_pytest_unit + def test_DataInfoProxy(self) -> None: + """Test _DataInfoProxy in OTXActionDeteDataset.""" + + proxy = OTXActionDetDataset._DataInfoProxy(self.otx_dataset, self.labels, fps=1) + sample = proxy[0] + assert len(proxy) == 9 + assert sample["shot_info"] == (0, 4) + assert sample["is_empty_frame"] is False + assert "start_index" in sample + assert "video_id" in sample + assert "timestamp" in sample + assert "gt_bboxes" in sample + assert "gt_labels" in sample + + @e2e_pytest_unit + def test_DataInfoProxy_patch_proposals(self) -> None: + """Test _patch_proposals function in _DataInfoProxy class.""" + + proxy = MockDataInfoProxy(self.proposals) + pre_len = len(proxy.proposals) + proxy._patch_proposals() + assert len(proxy.proposals) == pre_len + for key in proxy.proposals: + assert str(int(key.split(",")[-1])) == key.split(",")[-1] + + @e2e_pytest_unit + def test_pipeline(self) -> None: + """Test RawFrameDecode transform contains otx_dataset.""" + + dataset = OTXActionDetDataset(self.otx_dataset, self.labels, self.pipeline, fps=1) + for transform in dataset.pipeline.transforms: + if isinstance(transform, RawFrameDecode): + assert transform.otx_dataset == self.otx_dataset + + @e2e_pytest_unit + def test_prepare_train_frames(self) -> None: + """Test prepare_train_frames function. + + prepare_train_frames function's output should contain essential attributes for training + """ + + dataset = OTXActionDetDataset(self.otx_dataset, self.labels, self.pipeline, fps=1) + dataset.pipeline = MockPipeline() + sample = dataset.prepare_train_frames(0) + assert sample["shot_info"] == (0, 4) + assert sample["is_empty_frame"] is False + assert "start_index" in sample + assert "video_id" in sample + assert "timestamp" in sample + assert "gt_bboxes" in sample + assert "gt_labels" in sample + + @e2e_pytest_unit + def test_prepare_test_frames(self) -> None: + """Test prepare_test_frames function. + + Same with test_prepare_train_frames + """ + + dataset = OTXActionDetDataset(self.otx_dataset, self.labels, self.pipeline, fps=1) + dataset.pipeline = MockPipeline() + sample = dataset.prepare_test_frames(0) + assert sample["shot_info"] == (0, 4) + assert sample["is_empty_frame"] is False + assert "start_index" in sample + assert "video_id" in sample + assert "timestamp" in sample + assert "gt_bboxes" in sample + assert "gt_labels" in sample + + @e2e_pytest_unit + def test_evaluate(self, mocker) -> None: + """Test evaluate function""" + + mocker.patch( + "otx.algorithms.action.adapters.mmaction.data.det_dataset.det_eval", return_value={"mAP@0.5IOU": 0.5} + ) + dataset = OTXActionDetDataset(self.otx_dataset, self.labels, self.pipeline, fps=1) + results = [ + [ + np.array([[0.0, 0.0, 1.0, 1.0, 1.0]]), + np.array([[0.0, 0.0, 1.0, 1.0, 0.0]]), + np.array([[0.0, 0.0, 1.0, 1.0, 0.0]]), + ] + ] * 9 + output = dataset.evaluate(results) + assert output == {"mAP@0.5IOU": 0.5} diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/__init__.py b/tests/unit/algorithms/action/adapters/mmaction/models/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/backbones/__init__.py b/tests/unit/algorithms/action/adapters/mmaction/models/backbones/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/backbones/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/backbones/test_action_movinet.py b/tests/unit/algorithms/action/adapters/mmaction/models/backbones/test_action_movinet.py new file mode 100644 index 00000000000..0138b65d198 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/backbones/test_action_movinet.py @@ -0,0 +1,203 @@ +"""Unit test for otx.algorithms.action.adapters.mmaction.models.backbones.movinet""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import pytest +import torch +from torch import nn + +from otx.algorithms.action.adapters.mmaction.models.backbones.movinet import ( + BasicBneck, + Conv2dBNActivation, + ConvBlock3D, + MoViNet, + OTXMoViNet, + SqueezeExcitation, + TFAvgPool3D, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestConv2dBNActivation: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.layer = Conv2dBNActivation(3, 16, kernel_size=3, padding=1) + + @e2e_pytest_unit + def test_conv2d_bn_activation_output_shape(self): + x = torch.Tensor(1, 3, 32, 32) + output = self.layer(x) + assert output.shape == (1, 16, 32, 32) + + @e2e_pytest_unit + def test_conv2d_bn_activation_attributes(self): + assert self.layer.kernel_size == (3, 3) + assert self.layer.stride == (1, 1) + assert self.layer.out_channels == 16 + + +class TestConvBlock3D: + @e2e_pytest_unit + def test_conv_block_3d_output_shape(self): + x = torch.Tensor(1, 3, 32, 32, 32) + layer = ConvBlock3D(3, 16, kernel_size=(3, 3, 3), tf_like=True, conv_type="3d") + output = layer(x) + assert output.shape == (1, 16, 32, 32, 32) + + @e2e_pytest_unit + @pytest.mark.parametrize("conv_type", ["3d", "2plus1d"]) + def test_conv_block_3d_attributes(self, conv_type): + layer = ConvBlock3D(3, 16, kernel_size=(3, 3, 3), tf_like=True, conv_type=conv_type) + assert layer.kernel_size == (3, 3, 3) + assert layer.stride == (1, 1, 1) + assert layer.dim_pad == 2 + assert layer.conv_type == conv_type + assert layer.tf_like + + +class TestSqueezeExcitation: + @pytest.fixture + def se_block(self): + return SqueezeExcitation(16, nn.ReLU, nn.Sigmoid, conv_type="2plus1d", squeeze_factor=4, bias=True) + + @e2e_pytest_unit + def test_scale_output_shape(self, se_block): + x = torch.Tensor(1, 16, 32, 32, 32) + scale = se_block._scale(x) + assert scale.shape == (1, 16, 1, 1, 1) + + @e2e_pytest_unit + def test_forward_output_shape(self, se_block): + x = torch.Tensor(1, 16, 32, 32, 32) + output = se_block(x) + assert output.shape == (1, 16, 32, 32, 32) + + @e2e_pytest_unit + def test_se_block_attributes(self, se_block): + assert se_block.fc1.kernel_size == (1, 1, 1) + assert se_block.fc2.kernel_size == (1, 1, 1) + assert se_block.fc1.conv_type == "2plus1d" + assert se_block.fc2.conv_type == "2plus1d" + + +class TestTFAvgPool3D: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.pool = TFAvgPool3D() + + @e2e_pytest_unit + def test_tf_avg_pool_output_shape(self): + x = torch.Tensor(1, 3, 32, 32, 32) + output = self.pool(x) + assert output.shape == (1, 3, 32, 16, 16) + + @e2e_pytest_unit + def test_tf_avg_pool_output_shape_odd(self): + x = torch.Tensor(1, 3, 31, 31, 31) + output = self.pool(x) + assert output.shape == (1, 3, 31, 16, 16) + + @e2e_pytest_unit + def test_tf_avg_pool_output_shape_odd_padding(self): + x = torch.Tensor(1, 3, 30, 30, 30) + output = self.pool(x) + assert output.shape == (1, 3, 30, 15, 15) + + +class AttrDict(dict): + def __init__(self, *args, **kwargs): + super(AttrDict, self).__init__(*args, **kwargs) + self.__dict__ = self + + +class TestBasicBneck: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.config = AttrDict( + input_channels=64, + expanded_channels=64, + out_channels=64, + kernel_size=(3, 3, 3), + padding=(1, 1, 1), + stride=(1, 1, 1), + padding_avg=(1, 1, 1), + ) + + @e2e_pytest_unit + def test_basic_bneck_output_shape(self): + module = BasicBneck(self.config, tf_like=False, conv_type="3d", activation_layer=nn.ReLU) + x = torch.randn(1, self.config.input_channels, 32, 32, 32) + output = module(x) + assert output.shape == (1, self.config.out_channels, 32, 32, 32) + + +class TestMoViNet: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.cfg = AttrDict() + self.cfg.conv1 = AttrDict( + { + "input_channels": 3, + "out_channels": 16, + "kernel_size": (3, 5, 5), + "stride": (1, 1, 1), + "padding": (1, 2, 2), + } + ) + self.cfg.blocks = [ + [ + AttrDict( + { + "input_channels": 16, + "expanded_channels": 24, + "out_channels": 24, + "kernel_size": (3, 3, 3), + "stride": (1, 1, 1), + "padding": (1, 1, 1), + } + ), + ] + ] + self.cfg.conv7 = AttrDict( + { + "input_channels": 40, + "out_channels": 256, + "kernel_size": (1, 1, 1), + "stride": (1, 1, 1), + "padding": (0, 0, 0), + } + ) + + @e2e_pytest_unit + def test_movinet_output_shape(self): + module = MoViNet(self.cfg) + x = torch.randn(1, 3, 32, 32, 32) + module.conv1 = nn.Identity() + module.blocks = nn.Identity() + module.conv7 = nn.Identity() + output = module(x) + assert output.shape == (1, 3, 1, 1, 1) + + @e2e_pytest_unit + def test_init_weights(self): + module = MoViNet(self.cfg) + module.apply(module._init_weights) + for m in module.modules(): + if isinstance(m, nn.Conv3d): + if m.bias is not None: + assert m.bias.mean().item() == pytest.approx(0, abs=1e-2) + assert m.bias.std().item() == pytest.approx(0, abs=1e-2) + elif isinstance(m, (nn.BatchNorm3d, nn.BatchNorm2d, nn.GroupNorm)): + assert m.bias.mean().item() == pytest.approx(0, abs=1e-2) + assert m.bias.std().item() == pytest.approx(0, abs=1e-2) + elif isinstance(m, nn.Linear): + assert m.bias.mean().item() == pytest.approx(0, abs=1e-2) + assert m.bias.std().item() == pytest.approx(0, abs=1e-2) + + @e2e_pytest_unit + def test_OTXMoViNet(self): + model = OTXMoViNet() + input_tensor = torch.randn(1, 3, 32, 224, 224) + output_tensor = model(input_tensor) + assert output_tensor.shape == torch.Size([1, 480, 1, 1, 1]) diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/backbones/test_action_register_backbone.py b/tests/unit/algorithms/action/adapters/mmaction/models/backbones/test_action_register_backbone.py new file mode 100644 index 00000000000..0bdb5aeacec --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/backbones/test_action_register_backbone.py @@ -0,0 +1,21 @@ +"""Unit test for otx.algorithms.action.adapters.mmaction.models.backbones.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from mmaction.models import BACKBONES as MMACTION_BACKBONES +from mmdet.models import BACKBONES as MMDET_BACKBONES + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_register_action_backbones() -> None: + """Test register_action_backbones function. + + Since this function is called while initialization, X3D should be in mmdet backbone registry + """ + + assert "X3D" in MMDET_BACKBONES + assert "X3D" in MMACTION_BACKBONES + assert "OTXMoViNet" in MMACTION_BACKBONES diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/detectors/__init__.py b/tests/unit/algorithms/action/adapters/mmaction/models/detectors/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/detectors/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/detectors/test_action_fast_rcnn.py b/tests/unit/algorithms/action/adapters/mmaction/models/detectors/test_action_fast_rcnn.py new file mode 100644 index 00000000000..9c1c90ac197 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/detectors/test_action_fast_rcnn.py @@ -0,0 +1,195 @@ +"""Unit Test for otx.algorithms.action.adapters.mmaction.data.cls_dataset.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest +import torch +from mmcv.utils import Config, ConfigDict +from mmdet.models.detectors.faster_rcnn import FasterRCNN +from torch import nn + +from otx.algorithms.action.adapters.mmaction.models.detectors.fast_rcnn import ( + AVAFastRCNN, + ONNXPool3D, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockDetector(nn.Module): + """Mock class for person detector.""" + + def simple_test(self, *args, **kwargs): + """Return dummy person detection results.""" + + sample_det_bboxes = torch.Tensor([[0.0, 0.0, 1.0, 1.0, 1.0]] * 100).unsqueeze(0) + sample_det_labels = torch.ones(1, 100) + sample_det_labels[0][0] = 0 + return sample_det_bboxes, sample_det_labels + + +class TestONNXPool3d: + """Test ONNXPool3D class. + + 1. Check every possible ONNXPool3D generation + 2. Check every possible ONNXPool3D actuall pooling input tensor as expected + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.temporal_avg_pool = ONNXPool3D("temporal", "avg") + self.temporal_max_pool = ONNXPool3D("temporal", "max") + self.spatial_avg_pool = ONNXPool3D("spatial", "avg") + self.spatial_max_pool = ONNXPool3D("spatial", "max") + + @e2e_pytest_unit + def test_init(self) -> None: + """Test __init__ function.""" + + self.temporal_avg_pool.pool == torch.mean + self.temporal_max_pool.pool == torch.max + self.spatial_avg_pool.pool == torch.mean + self.spatial_max_pool.pool == torch.max + + @e2e_pytest_unit + def test_forward(self) -> None: + """Test forward function.""" + + sample_input = torch.randn(1, 100, 8, 8, 8) + output = self.temporal_avg_pool(sample_input) + assert list(output.shape) == [1, 100, 1, 8, 8] + output = self.temporal_max_pool(sample_input) + assert list(output.shape) == [1, 100, 1, 8, 8] + output = self.spatial_avg_pool(sample_input) + assert list(output.shape) == [1, 100, 8, 1, 1] + output = self.spatial_max_pool(sample_input) + assert list(output.shape) == [1, 100, 8, 1, 1] + + +class TestAVAFastRCNN: + """Test AVAFastRCNN class. + + 1. Check _add_detector function + 2. Check _patch_pools function + 3. Check forward_infer function + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.model = AVAFastRCNN( + backbone=ConfigDict(type="X3D", gamma_w=1, gamma_b=2.25, gamma_d=2.2), + roi_head=ConfigDict( + type="AVARoIHead", + bbox_roi_extractor=ConfigDict( + type="SingleRoIExtractor3D", roi_layer_type="RoIAlign", output_size=8, with_temporal_pool=True + ), + bbox_head=ConfigDict( + type="BBoxHeadAVA", in_channels=432, num_classes=81, multilabel=False, dropout_ratio=0.5 + ), + ), + train_cfg=ConfigDict( + rcnn=ConfigDict( + assigner=ConfigDict(type="MaxIoUAssignerAVA", pos_iou_thr=0.9, neg_iou_thr=0.9, min_pos_iou=0.9), + sampler=ConfigDict( + type="RandomSampler", num=32, pos_fraction=1, neg_pos_ub=-1, add_gt_as_proposals=True + ), + pos_weight=1.0, + debug=False, + ) + ), + test_cfg=ConfigDict(rcnn=ConfigDict(action_thr=0.002)), + ) + + @e2e_pytest_unit + def test_patch_for_export(self, mocker) -> None: + """Test patch_for_export function.""" + + mocker.patch.object(AVAFastRCNN, "_add_detector", return_value=True) + mocker.patch.object(AVAFastRCNN, "_patch_pools", return_value=True) + self.model.patch_for_export() + + @e2e_pytest_unit + def test_add_detector(self, mocker) -> None: + """Test add_deector function. + + + 1. Check added detector is FasterRCNN + 2. Check added detector has COCO classes + 3. Check added detector's CLASSES is properly initialized + 4. Check added detector raise exception if detector's first class is not person + """ + + mock_deploy_cfg = Config( + dict(codebase_config=dict(type="mmdet", task="ObjectDetection"), backend_config=dict(type="openvino")) + ) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.models.detectors.fast_rcnn.load_checkpoint", + return_value={"meta": {"CLASSES": ["person", "motorcycle", "car"]}}, + ) + self.model.deploy_cfg = mock_deploy_cfg + self.model._add_detector() + assert isinstance(self.model.detector, FasterRCNN) + assert self.model.detector.roi_head.bbox_head.num_classes == 80 + assert self.model.detector.CLASSES == ["person", "motorcycle", "car"] + + mocker.patch( + "otx.algorithms.action.adapters.mmaction.models.detectors.fast_rcnn.load_checkpoint", + return_value={"meta": {"CLASSES": ["motorcycle", "car", "person"]}}, + ) + with pytest.raises(Exception): + self.model._add_detector() + + @e2e_pytest_unit + def test_patch_pools(self) -> None: + """Test _patch_pools function. + + + 1. Check bbox_head's temporal pool is avg_pool and it pools through temporal axis + 2. Check bbox_head's spatial pool is max_pool and it pools through spatial axis + """ + + self.model._patch_pools() + assert isinstance(self.model.roi_head.bbox_head.temporal_pool, ONNXPool3D) + assert self.model.roi_head.bbox_head.temporal_pool.pool == torch.mean + assert self.model.roi_head.bbox_head.temporal_pool.dim == "temporal" + assert isinstance(self.model.roi_head.bbox_head.spatial_pool, ONNXPool3D) + assert self.model.roi_head.bbox_head.spatial_pool.pool == torch.max + assert self.model.roi_head.bbox_head.spatial_pool.dim == "spatial" + + @e2e_pytest_unit + def test_forward_infer(self, mocker) -> None: + """Test forward_infer function. + + + 1. Prepare sample imgs and img_metas + 2. Patch model's detector to MockDetector + 3. Check amount of output bboxes and output labels are same + 4. Check output bboxes have 4 cooridnates + 5. Check output labels have all num_classes of bbox_head + """ + + mocker.patch( + "otx.algorithms.action.adapters.mmaction.models.heads.roi_head.is_in_onnx_export", return_value=True + ) + height = width = 256 + sample_imgs = torch.randn(1, 3, 32, height, width) + sample_img_metas = { + "img_metas": [ + [ + { + "ori_shape": (height, width), + "img_shape": (height, width), + "pad_shape": (height, width), + "scale_factor": np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float32), + } + ] + ] + } + self.model.detector = MockDetector() + with torch.no_grad(): + bboxes, labels = self.model.forward_infer(self=self.model, imgs=sample_imgs, **sample_img_metas) + assert bboxes.shape[-1] == 4 + assert labels.shape[-1] == self.model.roi_head.bbox_head.num_classes + assert bboxes.shape[0] == labels.shape[0] diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/heads/__init__.py b/tests/unit/algorithms/action/adapters/mmaction/models/heads/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/heads/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/heads/test_action_movinet_head.py b/tests/unit/algorithms/action/adapters/mmaction/models/heads/test_action_movinet_head.py new file mode 100644 index 00000000000..1f2601b4999 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/heads/test_action_movinet_head.py @@ -0,0 +1,32 @@ +"""Unit Test for otx.algorithms.action.adapters.mmaction.heads.movinet_head.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.action.adapters.mmaction.models.heads.movinet_head import ( + MoViNetHead, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMoViNetHead: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.movinet_head = MoViNetHead( + num_classes=400, + in_channels=480, + hidden_dim=2048, + loss_cls=dict(type="CrossEntropyLoss", loss_weight=1.0), + ) + + @e2e_pytest_unit + def test_forward(self) -> None: + """Test forward function.""" + sample_input = torch.randn(1, 480, 1, 1, 1) + with torch.no_grad(): + out = self.movinet_head(sample_input) + assert out.shape == (1, self.movinet_head.num_classes) diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/heads/test_action_roi_head.py b/tests/unit/algorithms/action/adapters/mmaction/models/heads/test_action_roi_head.py new file mode 100644 index 00000000000..39c68f07826 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/heads/test_action_roi_head.py @@ -0,0 +1,72 @@ +"""Unit Test for otx.algorithms.action.adapters.mmaction.heads.roi_head.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest +import torch +from mmcv.utils import Config + +from otx.algorithms.action.adapters.mmaction.models.heads.roi_head import AVARoIHead +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockAVARoIHead(AVARoIHead): + """Mock class for AVARoIHead.""" + + def __init__(self): + self.bbox_head = Config() + self.test_cfg = Config() + self.bbox_head.num_classes = 3 + self.test_cfg.action_thr = 0.5 + + +class TestAVARoIHead: + """Check AVARoIHead class. + + 1. Check simple_test function + + 1. Generate sample tensor(1, 432, 32, 8, 11) + 2. Generate proposal_list: List[Tensor] + 3. Generate img_metas: List[Dict[str, Any]] + 4. Check bbox_results + 4-1. Check output lenth is fit with num_classes + 4-2. Check output has appropriate information(x1, y1, x2, y2, conf) + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.roi_head = AVARoIHead( + bbox_roi_extractor=dict( + type="SingleRoIExtractor3D", roi_layer_type="RoIAlign", output_size=8, with_temporal_pool=True + ), + bbox_head=dict(type="BBoxHeadAVA", in_channels=432, num_classes=81, multilabel=False, dropout_ratio=0.5), + ) + self.roi_head.test_cfg = Config() + self.roi_head.test_cfg.action_thr = 0.5 + + @e2e_pytest_unit + def test_simple_test(self, mocker) -> None: + """Test simple test function.""" + + sample_input = torch.randn(1, 432, 32, 8, 1) + proposal_list = [torch.Tensor([[0, 0, 10, 10]])] + img_metas = [{"scores": np.array([1.0]), "img_shape": (256, 256)}] + + with torch.no_grad(): + out = self.roi_head.simple_test(sample_input, proposal_list, img_metas) + + assert len(out[0]) == self.roi_head.bbox_head.num_classes - 1 + assert out[0][0].shape[1] == 5 + + mocker.patch( + "otx.algorithms.action.adapters.mmaction.models.heads.roi_head.is_in_onnx_export", return_value=True + ) + with torch.no_grad(): + bboxes, labels = self.roi_head.simple_test(sample_input, proposal_list, img_metas) + assert isinstance(bboxes, torch.Tensor) + assert isinstance(labels, torch.Tensor) + assert bboxes.shape[-1] == 4 + assert labels.shape[-1] == self.roi_head.bbox_head.num_classes diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/recognizers/__init__.py b/tests/unit/algorithms/action/adapters/mmaction/models/recognizers/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/recognizers/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/mmaction/models/recognizers/test_action_movinet_recognizer.py b/tests/unit/algorithms/action/adapters/mmaction/models/recognizers/test_action_movinet_recognizer.py new file mode 100644 index 00000000000..0a487333203 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/models/recognizers/test_action_movinet_recognizer.py @@ -0,0 +1,67 @@ +"""Unit Test for otx.algorithms.action.adapters.mmaction.models.recognizers.movinet_recognizer.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from copy import deepcopy + +import pytest +import torch +from mmaction.models.recognizers.recognizer3d import Recognizer3D + +from otx.algorithms.action.adapters.mmaction.models.recognizers.movinet_recognizer import ( + MoViNetRecognizer, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockOTXMoViNet: + pass + + +class MockModule: + def __init__(self): + self.backbone = MockOTXMoViNet() + self._state_dict = { + "classifier.0.conv_1.conv3d.weight": torch.rand(1, 1), + "conv1.conv_1.conv3d.weight": torch.rand(1, 1), + } + self.is_export = False + + def state_dict(self): + return self._state_dict + + +class TestMoViNetRecognizer: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + mocker.patch.object(Recognizer3D, "__init__", return_value=None) + MoViNetRecognizer._register_state_dict_hook = mocker.MagicMock() + MoViNetRecognizer._register_load_state_dict_pre_hook = mocker.MagicMock() + self.recognizer = MoViNetRecognizer() + self.prefix = "" + + @e2e_pytest_unit + def test_load_state_dict_pre_hook(self) -> None: + """Test load_state_dict_pre_hook function.""" + module = MockModule() + state_dict = module.state_dict() + self.recognizer.load_state_dict_pre_hook(module, state_dict, prefix=self.prefix) + + for key in state_dict: + if "classifier" in key: + assert "cls_head.classifier.0.conv_1.conv3d.weight" in state_dict + else: + assert "backbone.conv1.conv_1.conv3d.weight" in state_dict + + @e2e_pytest_unit + def test_state_dict_hook(self): + """Test state_dict_hook function.""" + module = MockModule() + state_dict = module.state_dict() + state_dict_copy = deepcopy(state_dict) + self.recognizer.load_state_dict_pre_hook(module, state_dict, prefix=self.prefix) + # backward state dict + self.recognizer.state_dict_hook(module, state_dict, prefix=self.prefix) + + assert state_dict.keys() == state_dict_copy.keys() diff --git a/tests/unit/algorithms/action/adapters/mmaction/test_task.py b/tests/unit/algorithms/action/adapters/mmaction/test_task.py new file mode 100644 index 00000000000..8245d27d7cc --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/test_task.py @@ -0,0 +1,450 @@ +"""Unit Test for otx.algorithms.action.adapters.mmaction.task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +from copy import deepcopy +from typing import Any, Dict + +import numpy as np +from otx.algorithms.common.utils.utils import is_xpu_available +import pytest +import torch +from mmaction.models.backbones.x3d import X3D +from mmaction.models.recognizers.recognizer3d import Recognizer3D +from mmcv.utils import Config +from torch import nn + +from otx.algorithms.action.configs.base.configuration import ActionConfig +from otx.algorithms.action.adapters.mmaction import task as target_file +from otx.algorithms.action.adapters.mmaction.task import MMActionTask +from otx.algorithms.common.adapters.mmcv.utils import config_utils +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.api.configuration import ConfigurableParameters +from otx.api.configuration.helper import create +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import Domain +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.model import ( + ModelConfiguration, + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, +) +from otx.api.entities.model_template import InstantiationType, parse_model_template, TaskFamily, TaskType +from otx.api.entities.resultset import ResultSetEntity +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.action.test_helpers import ( + init_environment, + MockModelTemplate, + generate_action_cls_otx_dataset, + generate_action_det_otx_dataset, + generate_labels, + return_inputs, +) + +DEFAULT_ACTION_CLS_DIR = os.path.join("src/otx/algorithms/action/configs/classification", "x3d") +DEFAULT_ACTION_DET_DIR = os.path.join("src/otx/algorithms/action/configs/detection", "x3d_fast_rcnn") + +if is_xpu_available(): + pytest.skip("Action task is not supported on XPU", allow_module_level=True) + + +class MockModule(nn.Module): + """Mock class for nn.Module.""" + + def forward(self, inputs: Any): + return inputs + + +class MockModel(nn.Module): + """Mock class for pytorch model.""" + + def __init__(self, task_type): + super().__init__() + self.backbone = MockModule() + self.task_type = task_type + + def forward(self, return_loss: bool, imgs: DatasetItemEntity): + feat = self.backbone(torch.randn(1, 400, 8, 7, 7)) # bs, channel, video_len, h, w + if self.task_type == "cls": + return np.array([[0, 0, 1]]) + return [[np.array([[0, 0, 1, 1, 0.1]]), np.array([[0, 0, 1, 1, 0.2]]), np.array([[0, 0, 1, 1, 0.7]])]] + + @staticmethod + def named_parameters(): + return {"name": torch.Tensor([0.5])}.items() + + +class MockDataset(DatasetEntity): + """Mock class for mm_dataset.""" + + def __init__(self, dataset: DatasetEntity, task_type: str): + self.dataset = dataset + self.task_type = task_type + self.CLASSES = ["1", "2", "3"] + + def evaluate(self, prediction, *args, **kwargs): + if self.task_type == "cls": + return {"mean_class_accuracy": 1.0} + else: + return {"mAP@0.5IOU": 1.0} + + def __len__(self): + return len(self.dataset) + + +class MockDataLoader: + """Mock class for data loader.""" + + def __init__(self, dataset: DatasetEntity): + self.dataset = dataset + self.iter = iter(self.dataset) + + def __len__(self) -> int: + return len(self.dataset) + + def __next__(self) -> Dict[str, DatasetItemEntity]: + return {"imgs": next(self.iter)} + + def __iter__(self): + return self + + +class MockExporter: + """Mock class for Exporter.""" + + def __init__(self, task): + self.work_dir = task._output_path + + def export(self): + dummy_data = np.ndarray((1, 1, 1)) + with open(os.path.join(self.work_dir, "openvino.bin"), "wb") as f: + f.write(dummy_data) + with open(os.path.join(self.work_dir, "openvino.xml"), "wb") as f: + f.write(dummy_data) + with open(os.path.join(self.work_dir, "model.onnx"), "wb") as f: + f.write(dummy_data) + + +def return_model(model, *args, **kwargs): + return model + + +class TestMMActionTask: + """Test class for MMActionTask. + + Details are explained in each test function. + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.video_len = 3 + self.frame_len = 3 + + cls_labels = generate_labels(3, Domain.ACTION_CLASSIFICATION) + self.cls_label_schema = LabelSchemaEntity() + cls_label_group = LabelGroup( + name="labels", + labels=cls_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + self.cls_label_schema.add_group(cls_label_group) + self.cls_dataset = generate_action_cls_otx_dataset(self.video_len, self.frame_len, cls_labels) + + cls_model_template = parse_model_template(os.path.join(DEFAULT_ACTION_CLS_DIR, "template.yaml")) + cls_hyper_parameters = create(cls_model_template.hyper_parameters.data) + cls_hyper_parameters.learning_parameters.auto_num_workers = True + cls_task_env = init_environment(cls_hyper_parameters, cls_model_template, self.cls_label_schema) + self.cls_task = MMActionTask(task_environment=cls_task_env) + + det_labels = generate_labels(3, Domain.ACTION_DETECTION) + self.det_label_schema = LabelSchemaEntity() + det_label_group = LabelGroup( + name="labels", + labels=det_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + self.det_label_schema.add_group(det_label_group) + self.det_dataset = generate_action_det_otx_dataset(self.video_len, self.frame_len, det_labels)[0] + + det_model_template = parse_model_template(os.path.join(DEFAULT_ACTION_DET_DIR, "template.yaml")) + det_hyper_parameters = create(det_model_template.hyper_parameters.data) + det_task_env = init_environment(det_hyper_parameters, det_model_template, self.det_label_schema) + self.det_task = MMActionTask(task_environment=det_task_env) + + @e2e_pytest_unit + def test_build_model(self, mocker) -> None: + """Test build_model function.""" + _mock_recipe_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_ACTION_CLS_DIR, "model.py")) + mock_load_checkpoint = mocker.patch.object(target_file, "load_checkpoint") + model = self.cls_task.build_model(_mock_recipe_cfg, True) + assert isinstance(model, Recognizer3D) + assert isinstance(model.backbone, X3D) + mock_load_checkpoint.assert_called_once() + assert mock_load_checkpoint.call_args.args[1] == _mock_recipe_cfg.load_from + + @e2e_pytest_unit + def test_train(self, mocker) -> None: + """Test train function.""" + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataset", + return_value=MockDataset(self.cls_dataset, "cls"), + ) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataloader", + return_value=MockDataLoader(self.cls_dataset), + ) + mocker.patch.object(MMActionTask, "build_model", return_value=MockModel("cls")) + mocker.patch.object(MMActionTask, "get_model_ckpt", return_value="fake_weight") + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_data_parallel", + side_effect=return_model, + ) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.train_model", + return_value=True, + ) + mocker.patch("torch.load", return_value={"state_dict": np.ndarray([1, 1, 1])}) + + # mock for testing num_workers + num_cpu = 20 + mock_multiprocessing = mocker.patch.object(config_utils, "multiprocessing") + mock_multiprocessing.cpu_count.return_value = num_cpu + num_gpu = 5 + mock_torch = mocker.patch.object(config_utils, "torch") + mock_torch.cuda.device_count.return_value = num_gpu + + _config = ModelConfiguration(ActionConfig(), self.cls_label_schema) + output_model = ModelEntity(self.cls_dataset, _config) + self.cls_task.train(self.cls_dataset, output_model) + assert output_model.performance.score.value == 1.0 + assert self.cls_task._recipe_cfg.data.workers_per_gpu == num_cpu // num_gpu # test adaptive num_workers + + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataset", + return_value=MockDataset(self.det_dataset, "det"), + ) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataloader", + return_value=MockDataLoader(self.det_dataset), + ) + mocker.patch.object(MMActionTask, "build_model", return_value=MockModel("det")) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_data_parallel", + side_effect=return_model, + ) + _config = ModelConfiguration(ActionConfig(), self.det_label_schema) + output_model = ModelEntity(self.det_dataset, _config) + self.det_task.train(self.det_dataset, output_model) + assert output_model.performance.score.value == 1.0 + assert self.cls_task._recipe_cfg.data.workers_per_gpu == num_cpu // num_gpu # test adaptive num_workers + + @e2e_pytest_unit + def test_infer(self, mocker) -> None: + """Test infer function. + + + 1. Create mock model for action classification + 2. Create mock recipe for action classification + 3. Run infer funciton + 4. Check whether inference results are added to output + 5. Do 1 - 4 for action detection + """ + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataset", + return_value=MockDataset(self.cls_dataset, "cls"), + ) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataloader", + return_value=MockDataLoader(self.cls_dataset), + ) + mocker.patch.object(MMActionTask, "build_model", return_value=MockModel("cls")) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_data_parallel", + side_effect=return_model, + ) + + inference_parameters = InferenceParameters(is_evaluation=True) + outputs = self.cls_task.infer(self.cls_dataset, inference_parameters) + for output in outputs: + assert len(output.get_annotations()[0].get_labels()) == 2 + + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataset", + return_value=MockDataset(self.det_dataset, "det"), + ) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataloader", + return_value=MockDataLoader(self.det_dataset), + ) + mocker.patch.object(MMActionTask, "build_model", return_value=MockModel("det")) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_data_parallel", + side_effect=return_model, + ) + inference_parameters = InferenceParameters(is_evaluation=True) + outputs = self.det_task.infer(self.det_dataset, inference_parameters) + for output in outputs: + assert len(output.get_annotations()) == 2 + + @e2e_pytest_unit + def test_evaluate(self) -> None: + """Test evaluate function.""" + _config = ModelConfiguration(ActionConfig(), self.cls_label_schema) + _model = ModelEntity(self.cls_dataset, _config) + resultset = ResultSetEntity(_model, self.cls_dataset, self.cls_dataset) + self.cls_task.evaluate(resultset) + assert resultset.performance.score.value == 1.0 + + @e2e_pytest_unit + def test_evaluate_with_empty_annot(self) -> None: + """Test evaluate function with empty_annot.""" + _config = ModelConfiguration(ActionConfig(), self.cls_label_schema) + _model = ModelEntity(self.cls_dataset, _config) + resultset = ResultSetEntity(_model, self.cls_dataset, self.cls_dataset.with_empty_annotations()) + self.cls_task.evaluate(resultset) + assert resultset.performance.score.value == 0.0 + + @e2e_pytest_unit + def test_evaluate_det(self) -> None: + """Test evaluate function for action detection.""" + _config = ModelConfiguration(ActionConfig(), self.det_label_schema) + _model = ModelEntity(self.det_dataset, _config) + resultset = ResultSetEntity(_model, self.det_dataset, self.det_dataset) + self.det_task.evaluate(resultset) + assert resultset.performance.score.value == 0.0 + + @pytest.mark.parametrize("precision", [ModelPrecision.FP16, ModelPrecision.FP32]) + @e2e_pytest_unit + def test_export(self, mocker, precision: ModelPrecision, export_type: ExportType = ExportType.OPENVINO) -> None: + """Test export function. + + + 1. Create model entity + 2. Run export function + 3. Check output model attributes + """ + _config = ModelConfiguration(ActionConfig(), self.cls_label_schema) + _model = ModelEntity(self.cls_dataset, _config) + mocker.patch("otx.algorithms.action.adapters.mmaction.task.Exporter", return_value=MockExporter(self.cls_task)) + mocker.patch("torch.load", return_value={}) + mocker.patch("torch.nn.Module.load_state_dict", return_value=True) + + self.cls_task.export(export_type, _model, precision, False) + + if export_type == ExportType.OPENVINO: + assert _model.model_format == ModelFormat.OPENVINO + assert _model.optimization_type == ModelOptimizationType.MO + assert _model.get_data("openvino.bin") is not None + assert _model.get_data("openvino.xml") is not None + else: + assert _model.model_format == ModelFormat.ONNX + assert _model.optimization_type == ModelOptimizationType.ONNX + assert _model.get_data("model.onnx") is not None + + assert _model.precision[0] == precision + + assert _model.get_data("confidence_threshold") is not None + assert _model.precision == self.cls_task._precision + assert _model.optimization_methods == self.cls_task._optimization_methods + assert _model.get_data("label_schema.json") is not None + + @e2e_pytest_unit + def test_export_onnx(self, mocker) -> None: + """Test export function. + + + 1. Create model entity + 2. Run export to ONNX function + 3. Check output model attributes + """ + self.test_export(mocker, ModelPrecision.FP32, ExportType.ONNX) + + @e2e_pytest_unit + def test_configure_distributed(self, mocker) -> None: + """Test configure_distributed function. + + + 1. Create config for test + 2. Run MMActionTask.configure_distributed + 3. Check updated learning rate + """ + mock_dist = mocker.patch.object(target_file, "dist") + world_size = 2 + mock_dist.get_world_size.return_value = world_size + origin_lr = 0.01 + config = Config({"optimizer": {"lr": origin_lr}, "dist_params": {"linear_scale_lr": True}}) + + MMActionTask.configure_distributed(config) + + assert config.optimizer.lr == pytest.approx(origin_lr * world_size) + + @e2e_pytest_unit + def test_geti_scenario(self, mocker): + """Test Geti scenario. + + Train -> Eval -> Export + """ + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataset", + return_value=MockDataset(self.cls_dataset, "cls"), + ) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataloader", + return_value=MockDataLoader(self.cls_dataset), + ) + mocker.patch.object(MMActionTask, "build_model", return_value=MockModel("cls")) + mocker.patch.object(MMActionTask, "get_model_ckpt", return_value="fake_weight") + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_data_parallel", + side_effect=return_model, + ) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.train_model", + return_value=True, + ) + mocker.patch("torch.load", return_value={"state_dict": np.ndarray([1, 1, 1])}) + + # mock for testing num_workers + num_cpu = 20 + mock_multiprocessing = mocker.patch.object(config_utils, "multiprocessing") + mock_multiprocessing.cpu_count.return_value = num_cpu + num_gpu = 5 + mock_torch = mocker.patch.object(config_utils, "torch") + mock_torch.cuda.device_count.return_value = num_gpu + + _config = ModelConfiguration(ActionConfig(), self.cls_label_schema) + output_model = ModelEntity(self.cls_dataset, _config) + self.cls_task.train(self.cls_dataset, output_model) + + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataset", + return_value=MockDataset(self.cls_dataset, "cls"), + ) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_dataloader", + return_value=MockDataLoader(self.cls_dataset), + ) + mocker.patch.object(MMActionTask, "build_model", return_value=MockModel("cls")) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.task.build_data_parallel", + side_effect=return_model, + ) + + inference_parameters = InferenceParameters(is_evaluation=True) + outputs = self.cls_task.infer(self.cls_dataset, inference_parameters) + + mocker.patch("otx.algorithms.action.adapters.mmaction.task.Exporter", return_value=MockExporter(self.cls_task)) + mocker.patch("torch.load", return_value={}) + mocker.patch("torch.nn.Module.load_state_dict", return_value=True) + + export_type = ExportType.OPENVINO + precision = ModelPrecision.FP32 + self.cls_task.export(export_type, output_model, precision, False) diff --git a/tests/unit/algorithms/action/adapters/mmaction/utils/__init__.py b/tests/unit/algorithms/action/adapters/mmaction/utils/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/utils/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/mmaction/utils/test_action_det_eval_utils.py b/tests/unit/algorithms/action/adapters/mmaction/utils/test_action_det_eval_utils.py new file mode 100644 index 00000000000..164a048d45d --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/utils/test_action_det_eval_utils.py @@ -0,0 +1,74 @@ +"""Unit Test for otx.algorithms.action.adapters.mmaction.utils.config_utils.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from collections import defaultdict +from typing import Any, Dict + +import numpy as np + +from otx.algorithms.action.adapters.mmaction.data import OTXActionDetDataset +from otx.algorithms.action.adapters.mmaction.utils import det_eval +from otx.algorithms.common.utils.utils import is_xpu_available +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +import pytest +from tests.test_suite.e2e_test_system import e2e_pytest_unit + +FULL_BOX = np.array([[0, 0, 1, 1]]) + +if is_xpu_available(): + pytest.skip("Action task is not supported on XPU", allow_module_level=True) + + +class MockDataInfoProxy(OTXActionDetDataset._DataInfoProxy): + """Mock clsass for data proxy in OTXActionDetDataset.""" + + def __init__(self): + self.video_infos = [ + {"img_key": "video_0,0", "gt_bboxes": FULL_BOX, "gt_labels": np.array([[0, 1, 0, 0]])}, + {"img_key": "video_0,1", "gt_bboxes": FULL_BOX, "gt_labels": np.array([[0, 0, 1, 0]])}, + {"img_key": "video_0,2", "gt_bboxes": FULL_BOX, "gt_labels": np.array([[0, 0, 0, 1]])}, + ] + + def __getitem__(self, index: int) -> Dict[str, Any]: + return self.video_infos[index] + + +@e2e_pytest_unit +def test_det_eval() -> None: + """Test for det_eval function. + + + 1. Generate sample predictions: Tuple[defaultdict] + 2. Generate sample labels: List[LabelEntity] + 3. Generate mock video_infos + """ + + pred_bboxes = defaultdict() + pred_bboxes["video_0,0"] = [FULL_BOX[0], FULL_BOX[0], FULL_BOX[0]] + pred_bboxes["video_0,1"] = [FULL_BOX[0], FULL_BOX[0], FULL_BOX[0]] + pred_bboxes["video_0,2"] = [FULL_BOX[0], FULL_BOX[0], FULL_BOX[0]] + pred_labels = defaultdict() + pred_labels["video_0,0"] = [1, 2, 3] + pred_labels["video_0,1"] = [1, 2, 3] + pred_labels["video_0,2"] = [1, 2, 3] + pred_confs = defaultdict() + pred_confs["video_0,0"] = [1, 0, 0] + pred_confs["video_0,1"] = [0, 1, 0] + pred_confs["video_0,2"] = [0, 0, 1] + predictions = (pred_bboxes, pred_labels, pred_confs) + + labels = [ + LabelEntity(name="0", domain=Domain.ACTION_DETECTION, id=ID(1)), + LabelEntity(name="1", domain=Domain.ACTION_DETECTION, id=ID(2)), + LabelEntity(name="2", domain=Domain.ACTION_DETECTION, id=ID(3)), + ] + + video_infos = MockDataInfoProxy() + custom_classes = [0, 1, 2, 3] + + out = det_eval(predictions, "mAP", labels, video_infos, None, True, custom_classes) + assert out["mAP@0.5IOU"] == 1.0 diff --git a/tests/unit/algorithms/action/adapters/mmaction/utils/test_action_export_utils.py b/tests/unit/algorithms/action/adapters/mmaction/utils/test_action_export_utils.py new file mode 100755 index 00000000000..90ea29b69f0 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/mmaction/utils/test_action_export_utils.py @@ -0,0 +1,178 @@ +"""Unit Test for otx.algorithms.action.adapters.mmaction.utils.config_utils.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Any + +import torch +from mmaction.models import Recognizer3D +from mmcv.runner import BaseModule +from mmcv.utils import Config +from torch import nn + +from otx.algorithms.action.adapters.mmaction.models.detectors.fast_rcnn import ( + AVAFastRCNN, +) +from otx.algorithms.action.adapters.mmaction.utils.export_utils import ( + Exporter, + _convert_sync_batch_to_normal_batch, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockRecognizer3D(Recognizer3D, BaseModule): + """Mock class for Recognizer3D.""" + + def __init__(self) -> None: + super(BaseModule, self).__init__() + + def forward(self, inputs: Any) -> str: + return "Forward function is replaced!" + + def load_state_dict(self, weights) -> Recognizer3D: + pass + + +class MockAVAFastRCNN(AVAFastRCNN): + """Mock class for AVAFastRCNN.""" + + def __init__(self) -> None: + super(BaseModule, self).__init__() + self.deploy_cfg = None + + def patch_for_export(self) -> None: + pass + + def forward_infer(self, inputs: Any, img_metas: Any) -> str: + return "Forward function is replaced!" + + def load_state_dict(self, weights) -> AVAFastRCNN: + pass + + +def _mock_sync_batchnorm(inputs): + """Mock function for _sync_batch_to_normal_batch function. + + It returns its inputs + """ + + return inputs + + +@e2e_pytest_unit +def test_convert_sync_batch_to_normal_batch() -> None: + """Test _convert_sync_batch_to_normal_batch function. + + + 1. Create sample module, which has some Conv3D, SyncBatchNorm, BatchNorm3d ops + 2. Run _convert_sync_batch_to_normal_batch function to sample module + 3. Check SyncBatchNorm is changed into BatchNorm3d + 4. Check the other ops don't affect by this function + """ + + sample_module = nn.Sequential( + nn.Conv3d(100, 100, 3), nn.SyncBatchNorm(100), nn.Conv3d(100, 100, 3), nn.BatchNorm3d(100) + ) + output_module = _convert_sync_batch_to_normal_batch(sample_module) + assert isinstance(output_module[0], nn.Conv3d) + assert isinstance(output_module[1], nn.BatchNorm3d) + assert isinstance(output_module[2], nn.Conv3d) + assert isinstance(output_module[3], nn.BatchNorm3d) + + +class MockTaskProcessor: + """Mock class of task_processor.""" + + def __init__(self, model_cfg, deploy_cfg, device): + self.model_cfg = model_cfg + + def init_pytorch_model(self, weights): + if self.model_cfg.model == "cls": + return MockRecognizer3D() + return MockAVAFastRCNN() + + +def mock_build_task_processor(model_cfg, deploy_cfg, device): + return MockTaskProcessor(model_cfg, deploy_cfg, device) + + +class TestExporter: + """Test class for Exporter.""" + + @e2e_pytest_unit + def test_init(self, mocker) -> None: + """Test __init__ function. + + + 1. Create mock task_processor + 2. Create mock Recognizer3D using task_processor + 3. Get inputs + 4. Create mock AVAFastRCNN using task_processor + 5. Get inputs + 6. Check mo options when half precision + """ + + mocker.patch( + "otx.algorithms.action.adapters.mmaction.utils.export_utils.build_task_processor", + side_effect=mock_build_task_processor, + ) + + recipe_cfg = Config(dict(model="cls")) + deploy_cfg = Config( + dict( + backend_config=dict( + type="openvino", + mo_options={}, + model_inputs=[dict(opt_shapes=dict(input=[1, 1, 3, 32, 224, 224]))], + ) + ) + ) + exporter = Exporter(recipe_cfg, None, deploy_cfg, "./tmp_dir/openvino", False, False) + assert isinstance(exporter.model, Recognizer3D) + assert exporter.input_tensor.shape == torch.Size([1, 1, 3, 32, 224, 224]) + assert exporter.input_metas is None + + recipe_cfg = Config(dict(model="det")) + deploy_cfg = Config( + dict( + backend_config=dict( + type="openvino", + mo_options={}, + model_inputs=[dict(opt_shapes=dict(input=[1, 3, 32, 224, 224]))], + ) + ) + ) + exporter = Exporter(recipe_cfg, None, deploy_cfg, "./tmp_dir/openvino", False, False) + assert isinstance(exporter.model, AVAFastRCNN) + assert exporter.input_tensor.shape == torch.Size([1, 3, 32, 224, 224]) + assert exporter.input_metas is not None + + exporter = Exporter(recipe_cfg, None, deploy_cfg, "./tmp_dir/openvino", True, False) + assert exporter.deploy_cfg.backend_config.mo_options["flags"] == ["--compress_to_fp16"] + + @e2e_pytest_unit + def test_export(self, mocker) -> None: + """Test export function.""" + + mocker.patch("otx.algorithms.action.adapters.mmaction.utils.export_utils.export", return_value=True) + mocker.patch("otx.algorithms.action.adapters.mmaction.utils.export_utils.from_onnx", return_value=True) + mocker.patch( + "otx.algorithms.action.adapters.mmaction.utils.export_utils.build_task_processor", + side_effect=mock_build_task_processor, + ) + + recipe_cfg = Config(dict(model="cls")) + deploy_cfg = Config( + dict( + backend_config=dict( + type="openvino", + mo_options={}, + model_inputs=[dict(opt_shapes=dict(input=[1, 1, 3, 32, 224, 224]))], + ), + ir_config=dict(input_names=["input"], output_names=["output"]), + ) + ) + exporter = Exporter(recipe_cfg, None, deploy_cfg, "./tmp_dir/openvino", False, False) + exporter.export() diff --git a/tests/unit/algorithms/action/adapters/openvino/__init__.py b/tests/unit/algorithms/action/adapters/openvino/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/openvino/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/adapters/openvino/test_action_dataloader.py b/tests/unit/algorithms/action/adapters/openvino/test_action_dataloader.py new file mode 100644 index 00000000000..67645c0e4fc --- /dev/null +++ b/tests/unit/algorithms/action/adapters/openvino/test_action_dataloader.py @@ -0,0 +1,335 @@ +"""Unit Test for otx.algorithms.action.adapters.openvino.dataloader.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import List, Optional +from otx.algorithms.common.utils.utils import is_xpu_available + +import pytest + +from otx.algorithms.action.adapters.openvino.dataloader import ( + ActionOVClsDataLoader, + ActionOVDemoDataLoader, + ActionOVDetDataLoader, + _is_multi_video, + get_ovdataloader, +) +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.metadata import MetadataItemEntity, VideoMetadata +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.action.test_helpers import ( + generate_action_cls_otx_dataset, + generate_action_det_otx_dataset, + generate_labels, +) + + +if is_xpu_available(): + pytest.skip("Action task is not supported on XPU", allow_module_level=True) + + +class MockDatasetEntity(DatasetEntity): + """Mock class for DatasetEntity.""" + + def __init__(self): + pass + + +class MockDatasetItemEntity(DatasetItemEntity): + """Mock class for DatasetItemEntity""" + + def __init__( + self, + metadata: List[MetadataItemEntity], + annotation_scene: Optional[AnnotationSceneEntity] = None, + ): + self.__metadata = metadata + self.__annotation_scene: AnnotationSceneEntity = annotation_scene + + def get_metadata(self) -> List[MetadataItemEntity]: + return self.__metadata + + +@e2e_pytest_unit +def test_get_ovdataloader(mocker) -> None: + """Test get_ovdataloader function. + + 1. Check ovdataloader type when function get ACTION_CLASSIFIACTION as task_type + 2. Check ovdataloader type when function get ACTION_DETECTION as task_type + 3. Check ovdataloader type when function get single video dataset + """ + + class MockActionOVClsDataLoader(ActionOVClsDataLoader): + """Mock class for ActionOVClsDataLoader.""" + + def __init__(self, dataset: DatasetEntity, clip_len: int, width: int, height: int): + pass + + class MockActionOVDetDataLoader(ActionOVDetDataLoader): + """Mock class for ActionOVDetDataLoader.""" + + def __init__(self, dataset: DatasetEntity, clip_len: int, width: int, height: int): + pass + + mocker.patch("otx.algorithms.action.adapters.openvino.dataloader._is_multi_video", return_value=True) + mocker.patch( + "otx.algorithms.action.adapters.openvino.dataloader.ActionOVClsDataLoader", + side_effect=MockActionOVClsDataLoader, + ) + mocker.patch( + "otx.algorithms.action.adapters.openvino.dataloader.ActionOVDetDataLoader", + side_effect=MockActionOVDetDataLoader, + ) + + _task_type = "ACTION_CLASSIFICATION" + out = get_ovdataloader(MockDatasetEntity(), _task_type, 8, 256, 256) + assert isinstance(out, ActionOVClsDataLoader) + + _task_type = "ACTION_DETECTION" + out = get_ovdataloader(MockDatasetEntity(), _task_type, 8, 256, 256) + assert isinstance(out, ActionOVDetDataLoader) + + _task_type = "ACTION_SEGMENTATION" + with pytest.raises(NotImplementedError): + out = get_ovdataloader(MockDatasetEntity, _task_type, 8, 256, 256) + + mocker.patch("otx.algorithms.action.adapters.openvino.dataloader._is_multi_video", return_value=False) + + _task_type = "ACTION_DETECTION" + out = get_ovdataloader(MockDatasetEntity(), _task_type, 8, 256, 256) + assert isinstance(out, ActionOVDemoDataLoader) + + +@e2e_pytest_unit +def test_is_multi_video() -> None: + """Test _is_multi_video function. + + + 1. Check return value: bool when function get single video DatasetEntity + 2. Check return value: bool when function get multi video DatasetEntity + """ + + items: List[DatasetItemEntity] = [] + items.append(MockDatasetItemEntity(metadata=[MetadataItemEntity(data=VideoMetadata("2", 0, False))])) + items.append(MockDatasetItemEntity(metadata=[MetadataItemEntity(data=VideoMetadata("2", 1, False))])) + items.append(MockDatasetItemEntity(metadata=[MetadataItemEntity(data=VideoMetadata("2", 2, False))])) + + dataset = DatasetEntity(items) + assert _is_multi_video(dataset) is False + + items.append(MockDatasetItemEntity(metadata=[MetadataItemEntity(data=VideoMetadata("1", 0, False))])) + items.append(MockDatasetItemEntity(metadata=[MetadataItemEntity(data=VideoMetadata("1", 1, False))])) + items.append(MockDatasetItemEntity(metadata=[MetadataItemEntity(data=VideoMetadata("1", 2, False))])) + + dataset = DatasetEntity(items) + assert _is_multi_video(dataset) is True + + +class TestActionOVDemoDataLoader: + """Test ActionOVDemoDataLoader class. + + 1. Initialize ActionOVDemoDataLoader and check its length + 2. Test __getitem__ function + + 1. Create ActionOVDemoDataLoader + 2. Sample first item from ActionOVDemoDataLoader + 3. The item's frame indices should be [0, 0, 0, 0, 0, 2, 4, 6] + 3. Test add_prediction function + + 1. Create sample prediction + 2. Check whether empty annotation changed to sample prediction + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.data_len = 40 + self.labels = generate_labels(1, Domain.ACTION_CLASSIFICATION) + self.dataset = generate_action_cls_otx_dataset(1, self.data_len, self.labels) + self.dataloader = ActionOVClsDataLoader(self.dataset, 8, 256, 256) + + @e2e_pytest_unit + def test_len(self) -> None: + """Test initialization and __len__ function.""" + + dataloader = ActionOVDemoDataLoader(self.dataset, "ACTION_CLASSIFICATION", 8, 256, 256) + assert len(dataloader) == self.data_len + + @e2e_pytest_unit + def test_getitem(self) -> None: + """Test __getitem__ function.""" + + dataloader = ActionOVDemoDataLoader(self.dataset, "ACTION_CLASSIFICATION", 8, 256, 256) + outs = dataloader[0] + frame_indices: List[int] = [] + for out in outs: + frame_idx = out.get_metadata()[0].data.frame_idx + frame_indices.append(frame_idx) + assert frame_indices == [0, 0, 0, 0, 0, 2, 4, 6] + + @e2e_pytest_unit + def test_add_prediction(self) -> None: + """Test add_prediction function.""" + prediction = AnnotationSceneEntity( + annotations=[ + Annotation( + Rectangle.generate_full_box(), + [ScoredLabel(LabelEntity("2", Domain.ACTION_CLASSIFICATION, id=ID(1)))], + ) + ], + kind=AnnotationSceneKind.ANNOTATION, + ) + + dataloader = ActionOVDemoDataLoader(self.dataset, "ACTION_CLASSIFICATION", 8, 256, 256) + items = self.dataset.with_empty_annotations()._items + dataloader.add_prediction(items, prediction) + assert len(items[0].get_annotations()) == 0 + assert len(items[len(items) // 2].get_annotations()) >= 1 + + dataloader = ActionOVDemoDataLoader(self.dataset, "ACTION_DETECTION", 8, 256, 256) + items = self.dataset.with_empty_annotations()._items + dataloader.add_prediction(items, prediction) + assert len(items[0].get_annotations()) == 0 + assert len(items[len(items) // 2].get_annotations()) >= 1 + + +class TestActionOVClsDataLoader: + """Test class for ActionOVClsDataLoader. + + 1. Test initialization + + 1. Check self.dataloader's length. It should be 1 because all dataset item have same video_id + 2. Check self.dataloader.dataset's length. It should be self.data_len(40) + 2. Test __getitem__ + + 1. Check len(output) == clip_len(8) + 2. Check frame_indices == [4, 8, 12, 16, 20, 24, 28, 32]. It comes from setting indices rule. + 3. Test add_prediciton + + 1. Check self.dataset.get_labels(). It should be 1, because only one label is added to empty dataset. + 2. Check self.dataset's label's id is 2. + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.data_len = 40 + self.labels = generate_labels(1, Domain.ACTION_CLASSIFICATION) + self.dataset = generate_action_cls_otx_dataset(1, self.data_len, self.labels) + self.dataloader = ActionOVClsDataLoader(self.dataset, 8, 256, 256) + + @e2e_pytest_unit + def test_init(self) -> None: + """Test __init__ function.""" + + assert len(self.dataloader) == 1 + assert len(self.dataloader.dataset[0]) == self.data_len + + @e2e_pytest_unit + def test_getitem(self) -> None: + """Test __getitem__ function.""" + + outs = self.dataloader[0] + frame_indices = [] + for out in outs: + frame_idx = out.get_metadata()[0].data.frame_idx + frame_indices.append(frame_idx) + assert len(outs) == 8 + assert frame_indices == [4, 8, 12, 16, 20, 24, 28, 32] + + @e2e_pytest_unit + def test_add_prediction(self) -> None: + """Test add_prediciton function.""" + + prediction = AnnotationSceneEntity( + annotations=[ + Annotation( + Rectangle.generate_full_box(), + [ScoredLabel(LabelEntity("2", Domain.ACTION_CLASSIFICATION, id=ID(2)))], + ) + ], + kind=AnnotationSceneKind.ANNOTATION, + ) + items = deepcopy(self.dataset._items) + self.dataset = self.dataset.with_empty_annotations() + self.dataloader.add_prediction(self.dataset, items, prediction) + assert len(self.dataset.get_labels()) == 1 + assert int(self.dataset.get_labels()[0].id) == 2 + + +class TestActionOVDetDataLoader: + """Test class for ActionOVDetDataLoader. + + 1. Test initialization + + 1. Check self.dataloader's length. It should be 1 because all dataset item have same video_id + 2. Check self.dataloader.dataset's length. It should be 20 (self.data_len - # fo empty frame) + 3. Check self.dataloader.original_dataset's length. It should be 40 (self.data_len) + 2. Test __getitem__ + + 1. Check len(output) == clip_len(8) + 2. Check frame_indices == [0, 0, 0, 0, 0, 2, 4, 6]. It comes from setting indices rule. + 3. Test add_prediciton + + 1. Check only center frame's annotations are updated + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.data_len = 40 + self.labels = generate_labels(1, Domain.ACTION_DETECTION) + self.dataset = generate_action_det_otx_dataset(1, self.data_len, self.labels)[0] + self.dataloader = ActionOVDetDataLoader(self.dataset, 8, 256, 256) + + @e2e_pytest_unit + def test_init(self) -> None: + """Test __init__ function.""" + + assert len(self.dataloader) == 20 + assert len(self.dataloader.original_dataset) == 40 + + sample = self.dataloader.dataset[0] + metadata = sample.get_metadata()[0].data + assert "start_index" in metadata.metadata + assert "timestamp_start" in metadata.metadata + assert "timestamp_end" in metadata.metadata + + @e2e_pytest_unit + def test_getitem(self) -> None: + """Test __getitem__ function.""" + + outs = self.dataloader[0] + frame_indices = [] + for out in outs: + frame_idx = out.get_metadata()[0].data.frame_idx + frame_indices.append(frame_idx) + assert len(outs) == 8 + assert frame_indices == [0, 0, 0, 0, 0, 2, 4, 6] + + @e2e_pytest_unit + def test_add_prediction(self) -> None: + """Test add_prediciton function.""" + + prediction = AnnotationSceneEntity( + annotations=[ + Annotation( + Rectangle.generate_full_box(), [ScoredLabel(LabelEntity("2", Domain.ACTION_DETECTION, id=ID(2)))] + ) + ], + kind=AnnotationSceneKind.ANNOTATION, + ) + items = self.dataset.with_empty_annotations()._items + self.dataloader.add_prediction(items, prediction) + assert len(items[0].get_annotations()) == 0 + assert len(items[len(items) // 2].get_annotations()) >= 1 diff --git a/tests/unit/algorithms/action/adapters/openvino/test_action_openvino_models.py b/tests/unit/algorithms/action/adapters/openvino/test_action_openvino_models.py new file mode 100644 index 00000000000..381bbf025d7 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/openvino/test_action_openvino_models.py @@ -0,0 +1,224 @@ +"""Unit Test for otx.algorithms.action.adapters.openvino.model_wrappers.openvino_models.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict + +import numpy as np +import pytest +from mmcv.utils import Config +from openvino.model_api.adapters import OpenvinoAdapter + +from otx.algorithms.action.adapters.openvino.model_wrappers.openvino_models import ( + OTXOVActionCls, + OTXOVActionDet, + get_multiclass_predictions, + softmax_numpy, +) +from otx.api.entities.label import Domain +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.action.test_helpers import ( + generate_action_cls_otx_dataset, + generate_labels, +) + + +class MockOpenvinoAdapter(OpenvinoAdapter): + """Mock class for OpenvinoAdapter.""" + + def __init__(self): + pass + + +class MockOTXOVActionCls(OTXOVActionCls): + """Mock class for OTXOVActionCls.""" + + def __init__(self, *args): + self.inputs: Dict[str, np.ndarray] = { + "cls_data": np.ndarray([1, 1, 3, 8, 256, 256]), + "det_data": np.ndarray([1, 3, 8, 256, 256]), + "cls_info": np.ndarray([1, 1, 1, 1]), + "dummy": np.ndarray([1, 1, 1]), + } + + self.outputs: Dict[str, Config] = { + "logits": Config({"names": "cls_layer"}), + "gt_bboxes": Config({"names": "reg_layer"}), + "gt_labels": Config({"names": "cls_layer"}), + } + super().__init__(MockOpenvinoAdapter) + + +class MockOTXOVActionDet(OTXOVActionDet): + """Mock class for OTXOVActionDet.""" + + def __init__(self, *args): + self.inputs: Dict[str, np.ndarray] = { + "cls_data": np.ndarray([1, 1, 3, 8, 256, 256]), + "det_data": np.ndarray([1, 3, 8, 256, 256]), + "cls_info": np.ndarray([1, 1, 1, 1]), + "dummy": np.ndarray([1, 1, 1]), + } + + self.outputs: Dict[str, Config] = { + "logits": Config({"names": "cls_layer"}), + "gt_bboxes": Config({"names": "reg_layer"}), + "gt_labels": Config({"names": "cls_layer"}), + } + super().__init__(MockOpenvinoAdapter) + + +@e2e_pytest_unit +def test_softmax_numpy() -> None: + """Test softmax_numpy function. + + It checks argmax of inputs and outputs + """ + + x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + out = softmax_numpy(x) + assert np.argmax(x) == np.argmax(out) + + +@e2e_pytest_unit +def test_get_multiclass_predictions(mocker) -> None: + """Test get_multiclass_predictions function. + + It checks argmax and max of inputs and outputs + """ + + inputs = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + outputs = get_multiclass_predictions(inputs, False) + assert 4 == outputs.top_labels[0][0] + assert 5.0 == outputs.top_labels[0][1] + + mocker.patch( + "otx.algorithms.action.adapters.openvino.model_wrappers.openvino_models.softmax_numpy", return_value=inputs + ) + outputs = get_multiclass_predictions(inputs, False) + assert 4 == outputs.top_labels[0][0] + assert 5.0 == outputs.top_labels[0][1] + + +class TestOTXOVActionCls: + """Test OTXOVActionCls class. + + 1. Test __init__ function + + 1. Check model's input, output name + 2. Check model's input's dimension + 2. Test preprocess function + + 1. Generate sample items: List[DatasetItemEntity] + 2. Check pre-processed inputs + 1. Check inputs' dimension + 2. Check meta information + 3. Test postprocess function + + 1. Generate sample output + 2. Check postprocess function's output return's argmax + """ + + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + mocker.patch( + "otx.algorithms.action.adapters.openvino.model_wrappers.openvino_models.Model.__init__", + return_value=True, + ) + self.model = MockOTXOVActionCls() + + @e2e_pytest_unit + def test_init(self, mocker) -> None: + """Test __init__ function.""" + assert self.model.image_blob_names == ["cls_data"] + assert self.model.image_info_blob_names == ["cls_info"] + assert self.model.out_layer_name == "logits" + assert self.model.n == 1 + assert self.model.c == 3 + assert self.model.t == 8 + assert self.model.h == 256 + assert self.model.w == 256 + + @e2e_pytest_unit + def test_preprocess(self) -> None: + """Test preprocess function.""" + + labels = generate_labels(1, Domain.ACTION_CLASSIFICATION) + items = generate_action_cls_otx_dataset(1, 10, labels)._items + + dict_inputs, meta = self.model.preprocess(items) + assert dict_inputs["cls_data"].shape == (1, 1, 3, 10, 256, 256) + assert meta["original_shape"] == (256, 256, 3) + assert meta["resized_shape"] == (1, 3, 10, 256, 256) + + @e2e_pytest_unit + def test_postprocess(self) -> None: + """Test postprocess function.""" + + sample_output = {"logits": np.array([1.0, 2.0, 3.0, 4.0, 5.0])} + out = self.model.postprocess(sample_output, meta={"Any": "Any"}) + assert out.top_labels[0][0] == 4 + + +class TestOTXOVActionDet: + """Test OTXOVActionDet class. + + 1. Test __init__ function + + 1. Check model's input, output name + 2. Check model's input's dimension + 2. Test preprocess function + + 1. Generate sample items: List[DatasetItemEntity] + 2. Check pre-processed inputs + 1. Check inputs' dimension + 2. Check meta information + 3. Test postprocess function + + 1. Generate sample output + 2. Check postprocess function output's id, score, bbox + """ + + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + mocker.patch( + "otx.algorithms.action.adapters.openvino.model_wrappers.openvino_models.Model.__init__", + return_value=True, + ) + self.model = MockOTXOVActionDet() + + @e2e_pytest_unit + def test_init(self, mocker) -> None: + """Test __init__ function.""" + assert self.model.image_blob_names == ["det_data"] + assert self.model.out_layer_names == {"bboxes": "gt_bboxes", "labels": "gt_labels"} + assert self.model.n == 1 + assert self.model.c == 3 + assert self.model.t == 8 + assert self.model.h == 256 + assert self.model.w == 256 + + @e2e_pytest_unit + def test_preprocess(self) -> None: + """Test preprocess function.""" + + labels = generate_labels(1, Domain.ACTION_CLASSIFICATION) + items = generate_action_cls_otx_dataset(1, 10, labels)._items + + dict_inputs, meta = self.model.preprocess(items) + assert dict_inputs["det_data"].shape == (1, 3, 10, 256, 256) + assert meta["original_shape"] == (256, 256, 3) + assert meta["resized_shape"] == (1, 3, 10, 256, 256) + + @e2e_pytest_unit + def test_postprocess(self) -> None: + """Test postprocess function.""" + + sample_output = {"gt_bboxes": np.array([[0, 0, 1, 1]]), "gt_labels": np.array([[0.3, 0.2, 0.1, 0.7]])} + out = self.model.postprocess(sample_output, meta={"original_shape": (256, 256, 3)}) + # argmax index is 2 because first index is for background + assert out[0].id == 2 + assert out[0].score == 0.7 + assert (out[0].xmin, out[0].ymin, out[0].xmax, out[0].ymax) == (0, 0, 256, 256) diff --git a/tests/unit/algorithms/action/adapters/openvino/test_task.py b/tests/unit/algorithms/action/adapters/openvino/test_task.py new file mode 100644 index 00000000000..f62b7464a95 --- /dev/null +++ b/tests/unit/algorithms/action/adapters/openvino/test_task.py @@ -0,0 +1,395 @@ +"""Unit Test for otx.algorithms.action.adapters.openvino.task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import pathlib +from typing import Any, Dict + +import numpy as np +import pytest +from openvino.model_api.adapters import OpenvinoAdapter + +from otx.algorithms.action.adapters.openvino import ActionOVClsDataLoader +from otx.algorithms.action.configs.base.configuration import ActionConfig +from otx.algorithms.action.adapters.openvino.task import ( + ActionOpenVINOInferencer, + ActionOpenVINOTask, + DataLoaderWrapper, +) +from otx.api.configuration import ConfigurableParameters +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.label import Domain +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.model import ( + ModelConfiguration, + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.model_template import InstantiationType, TaskFamily, TaskType +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.exportable_code.inference import IInferencer +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( + ClassificationToAnnotationConverter, + DetectionBoxToAnnotationConverter, +) +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.action.test_helpers import ( + MockModelTemplate, + generate_action_cls_otx_dataset, + generate_labels, + return_args, +) + + +class MockOVInferencer(IInferencer): + """Mock class for OV inferencer.""" + + def __init__(self, *args, **kwargs): + self.model = MockModel() + self.model.t = 8 + self.model.w = 256 + self.model.h = 256 + self.labels = generate_labels(1, Domain.ACTION_CLASSIFICATION) + self.configuration: Dict[Any, Any] = {} + + def predict(self, data): + return AnnotationSceneEntity( + annotations=[Annotation(shape=Rectangle(0, 0, 1, 1), labels=[ScoredLabel(self.labels[0], 1.0)])], + kind=AnnotationSceneKind.PREDICTION, + ) + + def pre_process(self, item): + return item, {"dummy_meta": "dummy_info"} + + def forward(self, item): + pass + + def post_process(self, item): + pass + + +class MockModel: + """Mock class for OV model.""" + + def preprocess(self, image): + return "Preprocess function is called", None + + def postprocess(self, prediction, metadata): + return "Postprocess function is called" + + def infer_sync(self, image): + return "Funtion infer_sync is called" + + +class MockOpenvinoAdapter(OpenvinoAdapter): + """Mock class for OpenvinoAdapter.""" + + def __init__(self, *args, **kwargs): + pass + + +class MockDataloader(ActionOVClsDataLoader): + """Mock class for dataloader for OpenVINO inference.""" + + def __init__(self, dataset, *args, **kwargs): + self.dataset = dataset + + def __len__(self): + return 1 + + def __getitem__(self, index): + if index >= len(self): + raise StopIteration + return self.dataset._items + + def add_prediction(self, dataset, data, prediction): + for dataset_item in dataset: + dataset_item.append_labels(prediction.annotations[0].get_labels()) + + +class TestActionOVInferencer: + """Test class for ActionOpenVINOInferencer.""" + + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + self.labels = generate_labels(3, Domain.ACTION_CLASSIFICATION) + self.label_schema = LabelSchemaEntity() + label_group = LabelGroup( + name="labels", + labels=self.labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + self.label_schema.add_group(label_group) + mocker.patch("otx.algorithms.action.adapters.openvino.task.OpenvinoAdapter.__init__", return_value=None) + mocker.patch("otx.algorithms.action.adapters.openvino.task.Model.create_model", return_value=MockModel()) + self.inferencer = ActionOpenVINOInferencer( + "ACTION_CLASSIFICATION", + ActionConfig(), + self.label_schema, + "openvino.xml", + "openvino.bin", + ) + + @e2e_pytest_unit + def test_init(self) -> None: + """Test __init__ function.""" + inferencer = ActionOpenVINOInferencer( + "ACTION_CLASSIFICATION", + ActionConfig(), + self.label_schema, + "openvino.xml", + "openvino.bin", + ) + assert inferencer.task_type == "ACTION_CLASSIFICATION" + assert inferencer.label_schema == self.label_schema + assert isinstance(inferencer.model, MockModel) + assert isinstance(inferencer.converter, ClassificationToAnnotationConverter) + + inferencer = ActionOpenVINOInferencer( + "ACTION_DETECTION", + ActionConfig(), + self.label_schema, + "openvino.xml", + "openvino.bin", + ) + assert inferencer.task_type == "ACTION_DETECTION" + assert isinstance(inferencer.converter, DetectionBoxToAnnotationConverter) + + @e2e_pytest_unit + def test_pre_process(self) -> None: + """Test pre_process funciton.""" + dataset = generate_action_cls_otx_dataset(1, 10, self.labels) + inputs = dataset._items + assert self.inferencer.pre_process(inputs) == ("Preprocess function is called", None) + + @e2e_pytest_unit + def test_post_process(self, mocker) -> None: + """Test post_process function.""" + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.ClassificationToAnnotationConverter.convert_to_annotation", + side_effect=return_args, + ) + assert ( + self.inferencer.post_process({"dummy": np.ndarray(1)}, {"dummy": "meta"})[0][0] + == "Postprocess function is called" + ) + + @e2e_pytest_unit + def test_forward(self) -> None: + """Test forward function.""" + + assert self.inferencer.forward({"dummy": np.ndarray(1)}) == "Funtion infer_sync is called" + + @e2e_pytest_unit + def test_predict(self, mocker) -> None: + """Test predict function.""" + + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.ActionOpenVINOInferencer.pre_process", + return_value=("data", "metadata"), + ) + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.ActionOpenVINOInferencer.forward", + return_value="raw_predictions", + ) + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.ActionOpenVINOInferencer.post_process", + return_value="predictions", + ) + + dataset = generate_action_cls_otx_dataset(1, 10, self.labels) + inputs = dataset._items + assert self.inferencer.predict(inputs) == "predictions" + + +class TestActionOVTask: + """Test class for ActionOpenVINOTask.""" + + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + self.video_len = 1 + self.frame_len = 10 + + labels = generate_labels(3, Domain.ACTION_CLASSIFICATION) + self.label_schema = LabelSchemaEntity() + label_group = LabelGroup( + name="labels", + labels=labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + self.label_schema.add_group(label_group) + template = MockModelTemplate( + model_template_id="template_id", + model_template_path="template_path", + name="template", + task_family=TaskFamily.VISION, + task_type=TaskType.ACTION_CLASSIFICATION, + instantiation=InstantiationType.CLASS, + ) + + config = ModelConfiguration(ActionConfig(), self.label_schema) + self.dataset = generate_action_cls_otx_dataset(1, 10, labels) + self.model = ModelEntity(self.dataset, config) + self.model.set_data("openvino.xml", np.ndarray([1]).tobytes()) + self.model.set_data("openvino.bin", np.ndarray([1]).tobytes()) + + self.task_environment = TaskEnvironment( + model=self.model, + hyper_parameters=ConfigurableParameters(header="h-params"), + label_schema=self.label_schema, + model_template=template, + ) + + @e2e_pytest_unit + def test_load_inferencer(self, mocker) -> None: + """Test load_inferencer function.""" + + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.ActionOpenVINOInferencer", return_value=MockOVInferencer() + ) + task = ActionOpenVINOTask(self.task_environment) + assert isinstance(task.inferencer, MockOVInferencer) + + self.task_environment.model = None + with pytest.raises(RuntimeError): + task = ActionOpenVINOTask(self.task_environment) + + @e2e_pytest_unit + def test_infer(self, mocker) -> None: + """Test infer function.""" + + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.ActionOpenVINOTask.load_inferencer", + return_value=MockOVInferencer(), + ) + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.get_ovdataloader", return_value=MockDataloader(self.dataset) + ) + task = ActionOpenVINOTask(self.task_environment) + output = task.infer(self.dataset.with_empty_annotations()) + assert output[0].annotation_scene.kind == AnnotationSceneKind.PREDICTION + + @e2e_pytest_unit + def test_evaluate(self, mocker) -> None: + """Test evaluate function.""" + + class MockPerformance: + def get_performance(self): + return 1.0 + + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.ActionOpenVINOTask.load_inferencer", + return_value=MockOVInferencer(), + ) + task = ActionOpenVINOTask(self.task_environment) + + resultset = ResultSetEntity( + self.model, + self.dataset, + self.dataset, + ) + + mocker.patch.object(MetricsHelper, "compute_accuracy", return_value=MockPerformance()) + task.evaluate(resultset, "Accuracy") + assert resultset.performance == 1.0 + + mocker.patch.object(MetricsHelper, "compute_f_measure", return_value=MockPerformance()) + self.task_environment.model_template.task_type = TaskType.ACTION_DETECTION + task = ActionOpenVINOTask(self.task_environment) + task.evaluate(resultset, "Accuracy") + assert resultset.performance == 1.0 + + @e2e_pytest_unit + def test_deploy(self, mocker) -> None: + """Test function for deploy function.""" + + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.ActionOpenVINOTask.load_inferencer", + return_value=MockOVInferencer(), + ) + task = ActionOpenVINOTask(self.task_environment) + assert self.model.exportable_code is None + task.deploy(self.model) + assert self.model.exportable_code is not None + + @e2e_pytest_unit + def test_optimize(self, mocker) -> None: + """Test optimization function.""" + + class MockPipeline: + """Mock class for POT pipeline""" + + def run(self, model): + return model + + def mock_save_model(model, output_xml): + """Mock function for save_model function.""" + with open(output_xml, "wb") as f: + f.write(np.ndarray(1).tobytes()) + bin_path = pathlib.Path(output_xml).parent / pathlib.Path(str(pathlib.Path(output_xml).stem) + ".bin") + with open(bin_path, "wb") as f: + f.write(np.ndarray(1).tobytes()) + + mocker.patch("otx.algorithms.action.adapters.openvino.task.ov.Core.read_model", autospec=True) + mocker.patch("otx.algorithms.action.adapters.openvino.task.ov.save_model", new=mock_save_model) + fake_quantize = mocker.patch("otx.algorithms.action.adapters.openvino.task.nncf.quantize", autospec=True) + + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.get_ovdataloader", return_value=MockDataloader(self.dataset) + ) + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.DataLoaderWrapper", return_value=MockDataloader(self.dataset) + ) + mocker.patch( + "otx.algorithms.action.adapters.openvino.task.ActionOpenVINOTask.load_inferencer", + return_value=MockOVInferencer(), + ) + task = ActionOpenVINOTask(self.task_environment) + task.optimize(OptimizationType.POT, self.dataset, self.model, OptimizationParameters()) + fake_quantize.assert_called_once() + assert self.model.get_data("openvino.xml") is not None + assert self.model.get_data("openvino.bin") is not None + assert self.model.model_format == ModelFormat.OPENVINO + assert self.model.optimization_type == ModelOptimizationType.POT + assert self.model.optimization_methods == [OptimizationMethod.QUANTIZATION] + assert self.model.precision == [ModelPrecision.INT8] + + +class TestDataLoaderWrapper: + """Test class for DataLoaderWrapper""" + + def setup(self, mocker) -> None: + labels = generate_labels(3, Domain.ACTION_CLASSIFICATION) + self.dataset = generate_action_cls_otx_dataset(1, 10, labels) + ovdataloader = MockDataloader(self.dataset) + inferencer = MockOVInferencer() + self.dataloader = DataLoaderWrapper(ovdataloader, inferencer) + + @e2e_pytest_unit + def test_len(self) -> None: + """Test __len__ function.""" + + assert len(self.dataloader) == 1 + + def test_getitem(self) -> None: + """Test __getitem__ function.""" + + out = self.dataloader[0] + assert isinstance(out[1], AnnotationSceneEntity) + assert len(out[0]) == 29 diff --git a/tests/unit/algorithms/action/test_helpers.py b/tests/unit/algorithms/action/test_helpers.py new file mode 100644 index 00000000000..834da3d82b2 --- /dev/null +++ b/tests/unit/algorithms/action/test_helpers.py @@ -0,0 +1,143 @@ +"""Collection of helper functions for unit tests of otx.algorithms.action.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Any, Dict, List + +import numpy as np + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.metadata import MetadataItemEntity, VideoMetadata +from otx.api.entities.model_template import ModelTemplate +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment + + +class MockImage(Image): + """Mock class for Image entity.""" + + def __init__(self, file_path): + self.__file_path = file_path + self.__data = np.ndarray((256, 256, 3)) + super().__init__(self.__data) + + +class MockPipeline: + """Mock class for data pipeline. + + It returns its inputs. + """ + + def __call__(self, results: Dict[str, Any]) -> Dict[str, Any]: + return results + + +def generate_labels(length: int, domain: Domain) -> List[LabelEntity]: + """Generate list of LabelEntity given length and domain.""" + + output: List[LabelEntity] = [] + for i in range(length): + output.append(LabelEntity(name=f"{i + 1}", domain=domain, id=ID(i + 1))) + return output + + +def generate_action_cls_otx_dataset(video_len: int, frame_len: int, labels: List[LabelEntity]) -> DatasetEntity: + """Generate otx_dataset for action classification task.""" + + items: List[DatasetItemEntity] = [] + for video_id in range(video_len): + if video_id > 1: + subset = Subset.VALIDATION + else: + subset = Subset.TRAINING + for frame_idx in range(frame_len): + item = DatasetItemEntity( + media=MockImage(f"{video_id}_{frame_idx}.png"), + annotation_scene=AnnotationSceneEntity( + annotations=[Annotation(Rectangle.generate_full_box(), [ScoredLabel(labels[video_id])])], + kind=AnnotationSceneKind.ANNOTATION, + ), + metadata=[MetadataItemEntity(data=VideoMetadata(video_id, frame_idx, is_empty_frame=False))], + subset=subset, + ) + items.append(item) + dataset = DatasetEntity(items=items) + return dataset + + +def generate_action_det_otx_dataset(video_len: int, frame_len: int, labels: List[LabelEntity]) -> DatasetEntity: + """Generate otx_dataset for action detection task.""" + + items: List[DatasetItemEntity] = [] + proposals: Dict[str, List[float]] = {} + for video_id in range(video_len): + if video_id > 1: + subset = Subset.VALIDATION + else: + subset = Subset.TRAINING + for frame_idx in range(frame_len): + if frame_idx % 2 == 0: + item = DatasetItemEntity( + media=MockImage(f"{video_id}_{frame_idx}.png"), + annotation_scene=AnnotationSceneEntity( + annotations=[Annotation(Rectangle.generate_full_box(), [ScoredLabel(labels[video_id])])], + kind=AnnotationSceneKind.ANNOTATION, + ), + metadata=[MetadataItemEntity(data=VideoMetadata(str(video_id), frame_idx, is_empty_frame=False))], + subset=subset, + ) + proposals[f"{video_id},{frame_idx:04d}"] = [0.0, 0.0, 1.0, 1.0] + else: + item = DatasetItemEntity( + media=MockImage(f"{video_id}_{frame_idx}.png"), + annotation_scene=AnnotationSceneEntity( + annotations=[Annotation(Rectangle.generate_full_box(), [ScoredLabel(labels[video_id])])], + kind=AnnotationSceneKind.ANNOTATION, + ), + metadata=[MetadataItemEntity(data=VideoMetadata(str(video_id), frame_idx, is_empty_frame=True))], + subset=subset, + ) + items.append(item) + dataset = DatasetEntity(items=items) + return dataset, proposals + + +class MockModelTemplate(ModelTemplate): + """Mock class for ModelTemplate.""" + + def __post_init__(self): + pass + + +def return_args(*args, **kwargs): + """This function returns its args.""" + return args, kwargs + + +def return_inputs(inputs): + """This function returns its input.""" + return inputs + + +def init_environment(params, model_template, label_schema): + """Initialize environment.""" + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=label_schema, + model_template=model_template, + ) + return environment diff --git a/tests/unit/algorithms/action/tools/__init__.py b/tests/unit/algorithms/action/tools/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/tools/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/tools/test_action_sample_classification.py b/tests/unit/algorithms/action/tools/test_action_sample_classification.py new file mode 100644 index 00000000000..8775fce8fd2 --- /dev/null +++ b/tests/unit/algorithms/action/tools/test_action_sample_classification.py @@ -0,0 +1,120 @@ +"""Unit Test for otx.algorithms.action.tools.sample_classification.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.utils import Config + +from otx.algorithms.action.tools.sample_classification import ( + load_test_dataset, + main, + parse_args, +) +from otx.algorithms.common.configs.training_base import TrainType +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import Domain +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model_template import TaskType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.action.test_helpers import ( + generate_action_cls_otx_dataset, + generate_labels, +) + + +@e2e_pytest_unit +def test_parse_args(mocker) -> None: + """Test parse_args function.""" + + class MockArgParser: + def __init__(self, description): + self.description = description + + def add_argument(self, name, *args, **kwargs): + setattr(self, name.split("--")[-1], True) + + def parse_args(self): + return self + + mocker.patch("otx.algorithms.action.tools.sample_classification.argparse.ArgumentParser", side_effect=MockArgParser) + parser = parse_args() + assert parser.template_file_path is not None + assert parser.export is not None + + +@e2e_pytest_unit +def test_load_test_dataset() -> None: + """Test laod_test_dataset function.""" + + class MockTemplate: + task_type = TaskType.ACTION_CLASSIFICATION + hyper_parameters = Config( + {"parameter_overrides": {"algo_backend": {"train_type": {"default_value": TrainType.Incremental.value}}}} + ) + + dataset, label_schema = load_test_dataset(MockTemplate()) + isinstance(dataset, DatasetEntity) + isinstance(label_schema, LabelSchemaEntity) + + +@e2e_pytest_unit +def test_main(mocker) -> None: + """Test main function.""" + + class MockArgs: + template_file_path = "dummy_path" + export = False + + class MockTaskEnvironment: + def __init__(self, *args, **kwargs): + self.model = None + + def get_model_configuration(self): + return True + + class MockTestCls: + def __init__(self, task_environment): + pass + + def train(self, dataset, output_model): + pass + + def infer(self, dataset, params): + return dataset + + def evaluate(self, resultset): + resultset.performance = 1.0 + + def export(self, export_type, model, dump_features): + return model + + def optimize(self, optimization_type, dataset, modle, params): + pass + + mocker.patch( + "otx.algorithms.action.tools.sample_classification.parse_model_template", + return_value=Config( + { + "hyper_parameters": {"data": "dummy_data"}, + "entrypoints": {"base": "dummy_base", "openvino": "dummy_base"}, + } + ), + ) + mocker.patch( + "otx.algorithms.action.tools.sample_classification.load_test_dataset", + return_value=( + generate_action_cls_otx_dataset(3, 3, generate_labels(3, Domain.ACTION_CLASSIFICATION)), + "dummy_label_schema", + ), + ) + mocker.patch( + "otx.algorithms.action.tools.sample_classification.create", + return_value=Config({"learning_parameters": {"num_iters": 4}}), + ) + mocker.patch("otx.algorithms.action.tools.sample_classification.TaskEnvironment", side_effect=MockTaskEnvironment) + mocker.patch("otx.algorithms.action.tools.sample_classification.get_task_class", return_value=MockTestCls) + main(MockArgs()) + + MockArgs.export = True + main(MockArgs()) diff --git a/tests/unit/algorithms/action/tools/test_action_sample_detection.py b/tests/unit/algorithms/action/tools/test_action_sample_detection.py new file mode 100644 index 00000000000..aa7074f16f3 --- /dev/null +++ b/tests/unit/algorithms/action/tools/test_action_sample_detection.py @@ -0,0 +1,121 @@ +"""Unit Test for otx.algorithms.action.tools.sample_detection.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from mmcv.utils import Config + +from otx.algorithms.action.tools.sample_detection import ( + load_test_dataset, + main, + parse_args, +) +from otx.algorithms.common.configs.training_base import TrainType +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import Domain +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model_template import TaskType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.action.test_helpers import ( + generate_action_cls_otx_dataset, + generate_labels, +) + + +@e2e_pytest_unit +def test_parse_args(mocker) -> None: + """Test parse_args function.""" + + class MockArgParser: + def __init__(self, description): + self.description = description + + def add_argument(self, name, *args, **kwargs): + setattr(self, name.split("--")[-1], True) + + def parse_args(self): + return self + + mocker.patch("otx.algorithms.action.tools.sample_detection.argparse.ArgumentParser", side_effect=MockArgParser) + parser = parse_args() + assert parser.template_file_path is not None + assert parser.export is not None + + +@e2e_pytest_unit +def test_load_test_dataset() -> None: + """Test laod_test_dataset function.""" + + class MockTemplate: + task_type = TaskType.ACTION_DETECTION + hyper_parameters = Config( + {"parameter_overrides": {"algo_backend": {"train_type": {"default_value": TrainType.Incremental.value}}}} + ) + + dataset, label_schema = load_test_dataset(MockTemplate()) + isinstance(dataset, DatasetEntity) + isinstance(label_schema, LabelSchemaEntity) + + +@e2e_pytest_unit +def test_main(mocker) -> None: + """Test main function.""" + + class MockArgs: + template_file_path = "dummy_path" + export = False + + class MockTaskEnvironment: + def __init__(self, *args, **kwargs): + self.model = None + + def get_model_configuration(self): + return True + + class MockTestCls: + def __init__(self, task_environment): + pass + + def train(self, dataset, output_model): + pass + + def infer(self, dataset, params): + return dataset + + def evaluate(self, resultset): + resultset.performance = 1.0 + + def export(self, export_type, model, dump_features): + return model + + def optimize(self, optimization_type, dataset, modle, params): + pass + + mocker.patch( + "otx.algorithms.action.tools.sample_detection.parse_model_template", + return_value=Config( + { + "hyper_parameters": {"data": "dummy_data"}, + "entrypoints": {"base": "dummy_base", "openvino": "dummy_base"}, + } + ), + ) + mocker.patch( + "otx.algorithms.action.tools.sample_detection.load_test_dataset", + return_value=( + generate_action_cls_otx_dataset(3, 3, generate_labels(3, Domain.ACTION_DETECTION)), + "dummy_label_schema", + ), + ) + mocker.patch( + "otx.algorithms.action.tools.sample_detection.create", + return_value=Config({"learning_parameters": {"num_iters": 4}}), + ) + mocker.patch("otx.algorithms.action.tools.sample_detection.TaskEnvironment", side_effect=MockTaskEnvironment) + mocker.patch("otx.algorithms.action.tools.sample_detection.get_task_class", return_value=MockTestCls) + main(MockArgs()) + + MockArgs.export = True + main(MockArgs()) diff --git a/tests/unit/algorithms/action/utils/__init__.py b/tests/unit/algorithms/action/utils/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/action/utils/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/action/utils/test_action_convert_public_data_to_cvat.py b/tests/unit/algorithms/action/utils/test_action_convert_public_data_to_cvat.py new file mode 100644 index 00000000000..26aa6099275 --- /dev/null +++ b/tests/unit/algorithms/action/utils/test_action_convert_public_data_to_cvat.py @@ -0,0 +1,176 @@ +"""Unit Test for otx.algorithms.action.utils.convert_public_data_to_cvat.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import tempfile + +import numpy as np +import pytest + +from otx.algorithms.action.utils.convert_public_data_to_cvat import ( + convert_action_cls_dataset_to_datumaro, + convert_ava_dataset_to_datumaro, + generate_default_cvat_xml_fields, + main, + read_ava_csv, + rename_and_copy, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockPath: + """Mock class for pathlib.Path""" + + def __init__(self, path): + self.path = path + + def mkdir(self, *args, **kwargs): + return self.path + + +class MockFileObject: + """Mock class for python default File object.""" + + def readlines(self): + lines = [ + "# Some comment\n", + "video_dir frame_len class_idx\n", + "video_dir frame_len class_idx\n", + "video_dir frame_len class_idx\n", + "video_dir frame_len class_idx\n", + "video_dir frame_len class_idx\n", + ] + return lines + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + return None + + +class MockCsvFile: + """Mock class for python default File object.""" + + def __init__(self, *args, **kwargs): + self.lines = [ + "0,0,0,0,1,1,0", + "0,1,0,0,1,1,0", + "0,2,0,0,1,1,0", + "0,3,0,0,1,1,0", + "0,4,0,0,1,1,0", + ] + + def __enter__(self): + return self.lines + + def __exit__(self, *args, **kwargs): + return None + + +class MockElementTree: + """Mock class for lxml.etree.ElementTree.""" + + def write(self, *args, **kwargs): + pass + + +@e2e_pytest_unit +def test_generate_default_cvat_xml_fields(mocker) -> None: + """Test generate_default_cvat_xml_fields function.""" + video_path = "dummy_path" + frame_list = ["dummy_frame0", "dummy_frame1", "dummy_frame2", "dummy_frame3"] + + mocker.patch( + "otx.algorithms.action.utils.convert_public_data_to_cvat.cv2.imread", return_value=np.ndarray((256, 256, 3)) + ) + + output = generate_default_cvat_xml_fields(1, video_path, frame_list) + assert len(output[0].getchildren()) == 2 + assert output[1] == (256, 256, 3) + assert len(output[2].getchildren()) == 0 + + +@e2e_pytest_unit +def test_convert_action_cls_dataset_to_datumaro(mocker) -> None: + """Test convert_jester_dataset_to_datumaro function.""" + + src_path = "dummy_src_path" + ann_file = "dummy_ann_file" + + with tempfile.TemporaryDirectory() as dst_path: + mocker.patch("otx.algorithms.action.utils.convert_public_data_to_cvat.open", return_value=MockFileObject()) + mocker.patch("otx.algorithms.action.utils.convert_public_data_to_cvat.pathlib.Path.mkdir", return_value=True) + # mocker.patch("otx.algorithms.action.utils.convert_public_data_to_cvat.os.makedirs", return_value=True) + mocker.patch("otx.algorithms.action.utils.convert_public_data_to_cvat.shutil.copy", return_value=True) + mocker.patch( + "otx.algorithms.action.utils.convert_public_data_to_cvat.generate_default_cvat_xml_fields", + return_value=([], (256, 256, 3), []), + ) + mocker.patch( + "otx.algorithms.action.utils.convert_public_data_to_cvat.os.listdir", return_value=(["frame0", "frame1"]) + ) + mocker.patch( + "otx.algorithms.action.utils.convert_public_data_to_cvat.etree.ElementTree", return_value=MockElementTree() + ) + convert_action_cls_dataset_to_datumaro(src_path, dst_path, ann_file) + + +@e2e_pytest_unit +def test_convert_ava_dataset_to_datumaro(mocker) -> None: + """Test convert_ava_dataset_to_datumaro function.""" + + src_path = "dummy_src_path" + ann_file = "dummy_ann_file" + + with tempfile.TemporaryDirectory() as dst_path: + mocker.patch( + "otx.algorithms.action.utils.convert_public_data_to_cvat.read_ava_csv", + return_value={"video_0": {"frame_idx": [[0, 0, 1, 1, "action"]]}}, + ) + mocker.patch("otx.algorithms.action.utils.convert_public_data_to_cvat.os.listdir", return_value=["video_0"]) + mocker.patch("otx.algorithms.action.utils.convert_public_data_to_cvat.shutil.copytree", return_value=True) + mocker.patch( + "otx.algorithms.action.utils.convert_public_data_to_cvat.generate_default_cvat_xml_fields", + return_value=([], (256, 256, 3), []), + ) + mocker.patch( + "otx.algorithms.action.utils.convert_public_data_to_cvat.etree.ElementTree", return_value=MockElementTree() + ) + convert_ava_dataset_to_datumaro(src_path, dst_path, ann_file) + + +@e2e_pytest_unit +def test_rename_and_copy(mocker) -> None: + mocker.patch("otx.algorithms.action.utils.convert_public_data_to_cvat.shutil.copy2", return_value=True) + frame_name = "root/vid/frame_1.png" + rename_and_copy(frame_name, frame_name) + frame_name = "root/vid/1.png" + rename_and_copy(frame_name, frame_name) + + +@e2e_pytest_unit +def test_read_ava_csv(mocker) -> None: + mocker.patch("otx.algorithms.action.utils.convert_public_data_to_cvat.open", return_value=MockCsvFile()) + annot_info = read_ava_csv("dummy_path") + assert len(annot_info) == 1 + assert len(annot_info["0"]) == 5 + assert annot_info["0"][0] == [["0", "0", "1", "1", "0"]] + + +@e2e_pytest_unit +@pytest.mark.parametrize("task", ["action_classification", "action_detection", "pose_estimation"]) +def test_main(task, mocker) -> None: + """Test main function.""" + mocker.patch( + "otx.algorithms.action.utils.convert_public_data_to_cvat.parse_args", return_value=mocker.MagicMock(task=task) + ) + mocker.patch( + "otx.algorithms.action.utils.convert_public_data_to_cvat.convert_action_cls_dataset_to_datumaro", + return_value=True, + ) + mocker.patch( + "otx.algorithms.action.utils.convert_public_data_to_cvat.convert_ava_dataset_to_datumaro", return_value=True + ) + main() diff --git a/tests/unit/algorithms/action/utils/test_action_data.py b/tests/unit/algorithms/action/utils/test_action_data.py new file mode 100644 index 00000000000..89f44084efd --- /dev/null +++ b/tests/unit/algorithms/action/utils/test_action_data.py @@ -0,0 +1,142 @@ +"""Unit Test for otx.algorithms.action.utils.data.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np + +from otx.algorithms.action.utils.data import ( + find_label_by_name, + load_cls_annotations, + load_cls_dataset, + load_det_annotations, + load_det_dataset, +) +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.action.test_helpers import generate_labels + + +@e2e_pytest_unit +def test_find_label_by_name() -> None: + labels = generate_labels(3, Domain.ACTION_CLASSIFICATION) + assert find_label_by_name(labels, "1", Domain.ACTION_CLASSIFICATION).name == "1" + assert find_label_by_name(labels, "5", Domain.ACTION_CLASSIFICATION).name == "5" + + +@e2e_pytest_unit +def test_load_cls_annotations(mocker) -> None: + class MockFile: + def __init__(self, *args, **kwargs): + self.lines = [ + "# Comment\n", + "vid_0 5 1\n", + "vid_1 5 1\n", + "vid_2 5 1\n", + "vid_3 5 2\n", + "vid_4 5 2\n", + ] + + def __enter__(self): + return self.lines + + def __exit__(self, *args, **kwargs): + return None + + mocker.patch("otx.algorithms.action.utils.data.open", return_value=MockFile()) + + video_infos = load_cls_annotations("ann_file", "data_root") + assert len(video_infos) == 5 + assert video_infos[0]["frame_dir"] == "data_root/vid_0" + assert video_infos[0]["total_frames"] == 5 + assert video_infos[0]["label"] == 1 + + +@e2e_pytest_unit +def test_load_det_annotations(mocker) -> None: + class MockFile: + def __init__(self, *args, **kwargs): + self.lines = [ + "vid_0,0,0,0,1,1,1,0\n", + "vid_0,1,0,0,1,1,1,1\n", + "vid_0,1,0,0,1,1,2,1\n", + "vid_0,2,0,0,1,1,1,2\n", + "vid_0,3,0,0,1,1,2,3\n", + "vid_0,4,0,0,1,1,2,4\n", + ] + + def __enter__(self): + return self.lines + + def __exit__(self, *args, **kwargs): + return None + + mocker.patch("otx.algorithms.action.utils.data.open", return_value=MockFile()) + video_infos = load_det_annotations("ann_file", "data_root") + assert len(video_infos) == 5 + assert video_infos[1]["frame_dir"] == "data_root/vid_0" + assert video_infos[1]["video_id"] == "vid_0" + assert video_infos[1]["timestamp"] == 1 + assert video_infos[1]["img_key"] == "vid_0,1" + assert np.all(video_infos[1]["ann"]["gt_bboxes"] == np.array([[0.0, 0.0, 1.0, 1.0]])) + assert np.all(video_infos[1]["ann"]["gt_labels"] == np.array([1, 2])) + assert np.all(video_infos[1]["ann"]["entity_ids"] == np.array([1])) + + +@e2e_pytest_unit +def test_load_cls_dataset(mocker) -> None: + """Test load_cls_dataset function.""" + + mocker.patch( + "otx.algorithms.action.utils.data.load_cls_annotations", + return_value=[{"frame_dir": "data_root/vid_0", "total_frames": 5, "label": 1}], + ) + mocker.patch( + "otx.algorithms.action.utils.data.find_label_by_name", + return_value=LabelEntity(name="1", domain=Domain.ACTION_CLASSIFICATION, id=ID(1)), + ) + items = load_cls_dataset("ann_file", "data_root", Domain.ACTION_CLASSIFICATION) + assert len(items) == 1 + assert items[0].media == {"frame_dir": "data_root/vid_0", "total_frames": 5} + assert items[0].annotation_scene.get_labels()[0].name == "1" + + +@e2e_pytest_unit +def test_load_det_dataset(mocker) -> None: + """Test load_det_dataset function.""" + + mocker.patch( + "otx.algorithms.action.utils.data.load_det_annotations", + return_value=[ + { + "frame_dir": "data_root/vid_0", + "video_id": "vid_0", + "timestamp": 0, + "img_key": "vid_0,0", + "ann": { + "gt_bboxes": np.array([[0.0, 0.0, 1.0, 1.0]]), + "gt_labels": [np.array([1])], + "entity_ids": np.array([0]), + }, + "width": 320, + "height": 240, + } + ], + ) + mocker.patch( + "otx.algorithms.action.utils.data.find_label_by_name", + return_value=LabelEntity(name="1", domain=Domain.ACTION_DETECTION, id=ID(1)), + ) + items = load_det_dataset("ann_file", "data_root", Domain.ACTION_DETECTION) + assert len(items) == 1 + assert items[0].media == { + "frame_dir": "data_root/vid_0", + "video_id": "vid_0", + "timestamp": 0, + "img_key": "vid_0,0", + "width": 320, + "height": 240, + } + assert items[0].annotation_scene.get_labels()[0].name == "1" diff --git a/tests/unit/algorithms/anomaly/__init__.py b/tests/unit/algorithms/anomaly/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/anomaly/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/anomaly/adapters/__init__.py b/tests/unit/algorithms/anomaly/adapters/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/anomaly/adapters/anomalib/accelerators/__init__.py b/tests/unit/algorithms/anomaly/adapters/anomalib/accelerators/__init__.py new file mode 100644 index 00000000000..3b508890f94 --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/anomalib/accelerators/__init__.py @@ -0,0 +1,5 @@ +"""Test for otx.algorithms.anomaly.adapters.anomalib.accelerators""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/anomaly/adapters/anomalib/accelerators/xpu.py b/tests/unit/algorithms/anomaly/adapters/anomalib/accelerators/xpu.py new file mode 100644 index 00000000000..d277fc2c6dc --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/anomalib/accelerators/xpu.py @@ -0,0 +1,59 @@ +"""Test for otx.algorithms.anomaly.adapters.anomalib.accelerators.xpu""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch +from otx.algorithms.anomaly.adapters.anomalib.accelerators import XPUAccelerator +from otx.algorithms.common.utils import is_xpu_available + + +class TestXPUAccelerator: + @pytest.fixture + def accelerator(self, mocker): + mock_torch = mocker.patch("otx.algorithms.anomaly.adapters.anomalib.accelerators.xpu.torch") + return XPUAccelerator(), mock_torch + + def test_setup_device(self, accelerator): + accelerator, mock_torch = accelerator + device = torch.device("xpu") + accelerator.setup_device(device) + assert mock_torch.xpu.set_device.called + + def test_parse_devices(self, accelerator): + accelerator, _ = accelerator + devices = [1, 2, 3] + parsed_devices = accelerator.parse_devices(devices) + assert isinstance(parsed_devices, list) + assert parsed_devices == devices + + def test_get_parallel_devices(self, accelerator, mocker): + accelerator, _ = accelerator + devices = [1, 2, 3] + parallel_devices = accelerator.get_parallel_devices(devices) + assert isinstance(parallel_devices, list) + assert all([isinstance(device, mocker.MagicMock) for device in parallel_devices]) + + def test_auto_device_count(self, accelerator, mocker): + accelerator, mock_torch = accelerator + count = accelerator.auto_device_count() + assert isinstance(count, mocker.MagicMock) + assert mock_torch.xpu.device_count.called + + def test_is_available(self, accelerator): + accelerator, _ = accelerator + available = accelerator.is_available() + assert isinstance(available, bool) + assert available == is_xpu_available() + + def test_get_device_stats(self, accelerator): + accelerator, _ = accelerator + device = torch.device("xpu") + stats = accelerator.get_device_stats(device) + assert isinstance(stats, dict) + + def test_teardown(self, accelerator): + accelerator, _ = accelerator + accelerator.teardown() diff --git a/tests/unit/algorithms/anomaly/adapters/anomalib/plugins/__init__.py b/tests/unit/algorithms/anomaly/adapters/anomalib/plugins/__init__.py new file mode 100644 index 00000000000..e715eaf7d23 --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/anomalib/plugins/__init__.py @@ -0,0 +1,5 @@ +"""Test for otx.algorithms.anomaly.adapters.anomalib.plugins""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/anomaly/adapters/anomalib/plugins/xpu_precision.py b/tests/unit/algorithms/anomaly/adapters/anomalib/plugins/xpu_precision.py new file mode 100644 index 00000000000..9671b4fec94 --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/anomalib/plugins/xpu_precision.py @@ -0,0 +1,58 @@ +"""Test for otx.algorithms.anomaly.adapters.anomalib.plugins.xpu_precision""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch +from torch.optim import Optimizer +from otx.algorithms.anomaly.adapters.anomalib.plugins.xpu_precision import MixedPrecisionXPUPlugin + + +class TestMixedPrecisionXPUPlugin: + @pytest.fixture + def plugin(self): + return MixedPrecisionXPUPlugin() + + def test_init(self, plugin): + assert plugin.scaler is None + + def test_pre_backward(self, plugin, mocker): + tensor = torch.zeros(1) + module = mocker.MagicMock() + output = plugin.pre_backward(tensor, module) + assert output == tensor + + def test_optimizer_step_no_scaler(self, plugin, mocker): + optimizer = mocker.MagicMock(Optimizer) + model = mocker.MagicMock() + optimizer_idx = 0 + closure = mocker.MagicMock() + kwargs = {} + mock_optimizer_step = mocker.patch( + "otx.algorithms.anomaly.adapters.anomalib.plugins.xpu_precision.PrecisionPlugin.optimizer_step" + ) + out = plugin.optimizer_step(optimizer, model, optimizer_idx, closure, **kwargs) + assert isinstance(out, mocker.MagicMock) + mock_optimizer_step.assert_called_once() + + def test_optimizer_step_with_scaler(self, plugin, mocker): + optimizer = mocker.MagicMock(Optimizer) + model = mocker.MagicMock() + optimizer_idx = 0 + closure = mocker.MagicMock() + plugin.scaler = mocker.MagicMock() + kwargs = {} + out = plugin.optimizer_step(optimizer, model, optimizer_idx, closure, **kwargs) + assert isinstance(out, mocker.MagicMock) + + def test_clip_gradients(self, plugin, mocker): + optimizer = mocker.MagicMock(Optimizer) + clip_val = 0.1 + gradient_clip_algorithm = "norm" + mock_clip_gradients = mocker.patch( + "otx.algorithms.anomaly.adapters.anomalib.plugins.xpu_precision.PrecisionPlugin.clip_gradients" + ) + plugin.clip_gradients(optimizer, clip_val, gradient_clip_algorithm) + mock_clip_gradients.assert_called_once() diff --git a/tests/unit/algorithms/anomaly/adapters/anomalib/strategies/__init__.py b/tests/unit/algorithms/anomaly/adapters/anomalib/strategies/__init__.py new file mode 100644 index 00000000000..c718569f812 --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/anomalib/strategies/__init__.py @@ -0,0 +1,5 @@ +"""Test for otx.algorithms.anomaly.adapters.anomalib.strategies""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/anomaly/adapters/anomalib/strategies/test_xpu_single.py b/tests/unit/algorithms/anomaly/adapters/anomalib/strategies/test_xpu_single.py new file mode 100644 index 00000000000..f1e0de3204b --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/anomalib/strategies/test_xpu_single.py @@ -0,0 +1,48 @@ +"""Tests the XPU strategy.""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import torch +import pytorch_lightning as pl +from otx.algorithms.anomaly.adapters.anomalib.strategies.xpu_single import SingleXPUStrategy +from pytorch_lightning.utilities.exceptions import MisconfigurationException + + +class TestSingleXPUStrategy: + def test_init(self, mocker): + with pytest.raises(MisconfigurationException): + strategy = SingleXPUStrategy(device="xpu:0") + mocked_is_xpu_available = mocker.patch( + "otx.algorithms.anomaly.adapters.anomalib.strategies.xpu_single.is_xpu_available", return_value=True + ) + strategy = SingleXPUStrategy(device="xpu:0") + assert mocked_is_xpu_available.call_count == 1 + assert strategy._root_device.type == "xpu" + assert strategy.accelerator is None + + @pytest.fixture + def strategy(self, mocker): + mocker.patch( + "otx.algorithms.anomaly.adapters.anomalib.strategies.xpu_single.is_xpu_available", return_value=True + ) + return SingleXPUStrategy(device="xpu:0") + + def test_is_distributed(self, strategy): + assert not strategy.is_distributed + + def test_setup_optimizers(self, strategy, mocker): + mocker.patch("otx.algorithms.anomaly.adapters.anomalib.strategies.xpu_single.torch") + mocker.patch( + "otx.algorithms.anomaly.adapters.anomalib.strategies.xpu_single.torch.xpu.optimize", + return_value=(mocker.MagicMock(), mocker.MagicMock()), + ) + trainer = pl.Trainer() + # Create mock optimizers and models for testing + model = torch.nn.Linear(10, 2) + strategy._optimizers = [torch.optim.Adam(model.parameters(), lr=0.001)] + strategy._model = model + trainer.model = model + strategy.setup_optimizers(trainer) + assert len(strategy.optimizers) == 1 diff --git a/tests/unit/algorithms/anomaly/adapters/callbacks/__init__.py b/tests/unit/algorithms/anomaly/adapters/callbacks/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/callbacks/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/anomaly/adapters/callbacks/test_inference_callback.py b/tests/unit/algorithms/anomaly/adapters/callbacks/test_inference_callback.py new file mode 100644 index 00000000000..6d1cbd95689 --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/callbacks/test_inference_callback.py @@ -0,0 +1,35 @@ +"""Tests the inference callback on a dummy lightning module.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import pytorch_lightning as pl + +from otx.algorithms.anomaly.adapters.anomalib.callbacks import AnomalyInferenceCallback +from otx.api.entities.model_template import TaskType +from tests.unit.algorithms.anomaly.helpers.dummy_dataset import DummyDataModule +from tests.unit.algorithms.anomaly.helpers.dummy_model import DummyModel + + +class TestInferenceCallback: + @pytest.mark.parametrize( + "task_type", [TaskType.ANOMALY_CLASSIFICATION, TaskType.ANOMALY_DETECTION, TaskType.ANOMALY_SEGMENTATION] + ) + def test_inference_callback(self, task_type): + """For each task type test the inference callback. + + The inference callback is responsible for processing the predictions and generating the annotations. + + Args: + task_type (TaskType): Task type. + """ + datamodule = DummyDataModule(task_type) + model = DummyModel() + labels = datamodule.labels + callback = AnomalyInferenceCallback(datamodule.dataset.dataset, labels, task_type) + trainer = pl.Trainer(logger=False, callbacks=[callback]) + result = trainer.predict(model, datamodule=datamodule) + # TODO: Currently it only checks that the result has predicted labels. This should be expanded to check the + # box labels and masks based on the task type. + assert result[0]["pred_labels"].item() == 1.0 diff --git a/tests/unit/algorithms/anomaly/adapters/callbacks/test_progress_callback.py b/tests/unit/algorithms/anomaly/adapters/callbacks/test_progress_callback.py new file mode 100644 index 00000000000..90e2b523405 --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/callbacks/test_progress_callback.py @@ -0,0 +1,51 @@ +"""Tests the progress callback on a dummy lightning module.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +import pytorch_lightning as pl + +from otx.algorithms.anomaly.adapters.anomalib.callbacks import ProgressCallback +from otx.api.entities.model_template import TaskType +from tests.unit.algorithms.anomaly.helpers.dummy_dataset import DummyDataModule +from tests.unit.algorithms.anomaly.helpers.dummy_model import DummyModel + + +class ProgressStageCheckerCallback(pl.Callback): + """This callback is injected into the model to check the stage of progress callback. + + Args: + progress_callback (ProgressCallback): Reference to progress callback. + """ + + def __init__(self, progress_callback: ProgressCallback): + self.progress_callback = progress_callback + + def on_validation_start(self, trainer: pl.Trainer, pl_module: pl.LightningModule) -> None: + assert self.progress_callback._get_progress("train") != 0.0 + + def on_validation_batch_end(self, *args, **kwds) -> None: + """Check that training progress is not 0.0 after validation batch end.""" + assert self.progress_callback._get_progress("train") != 0.0 + + +class TestProgressCallback: + def test_progress_callback(self): + """Tests if progress callback runs and that the progress is not reset after validation step.""" + datamodule = DummyDataModule(TaskType.ANOMALY_CLASSIFICATION) + model = DummyModel() + progress_callback = ProgressCallback() + # inject callback after progress callback + stage_checker = ProgressStageCheckerCallback(progress_callback) + # turn off sanity check on validation step as it will fail due to missing training data length + trainer = pl.Trainer( + logger=False, + enable_checkpointing=False, + max_epochs=5, + callbacks=[progress_callback, stage_checker], + num_sanity_val_steps=0, + ) + trainer.fit(model, datamodule) + trainer.test(model, datamodule) + trainer.predict(model, datamodule) diff --git a/tests/unit/algorithms/anomaly/adapters/data/__init__.py b/tests/unit/algorithms/anomaly/adapters/data/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/data/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/anomaly/adapters/data/test_dataset.py b/tests/unit/algorithms/anomaly/adapters/data/test_dataset.py new file mode 100644 index 00000000000..48077cf03d8 --- /dev/null +++ b/tests/unit/algorithms/anomaly/adapters/data/test_dataset.py @@ -0,0 +1,34 @@ +"""Tests whether the dataloaders can load the data correctly.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from otx.api.entities.model_template import TaskType +from tests.unit.algorithms.anomaly.helpers.dummy_dataset import HazelnutDataModule + + +@pytest.mark.parametrize("stage", ["predict", "fit", "validate", "test"]) +@pytest.mark.parametrize("task_type", [TaskType.ANOMALY_CLASSIFICATION, TaskType.ANOMALY_DETECTION]) +def test_dataloaders(task_type, stage): + """Tests whether the datamodule can load the data correctly. + + For all the test stages and the task types, the datamodule should return the correct keys. + """ + datamodule = HazelnutDataModule(task_type) + datamodule.setup(stage) + if stage == "fit": + batch = next(iter(datamodule.train_dataloader())) + elif stage == "validate": + batch = next(iter(datamodule.val_dataloader())) + elif stage == "test": + batch = next(iter(datamodule.test_dataloader())) + else: + batch = next(iter(datamodule.predict_dataloader())) + if task_type == TaskType.ANOMALY_CLASSIFICATION: + assert batch.keys() == {"image", "label", "index"} + elif task_type == TaskType.ANOMALY_DETECTION: + assert batch.keys() == {"image", "label", "index", "boxes"} + else: + assert batch.keys() == {"image", "label", "index", "mask"} diff --git a/tests/unit/algorithms/anomaly/config/__init__.py b/tests/unit/algorithms/anomaly/config/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/anomaly/config/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/anomaly/config/test_model_config_load.py b/tests/unit/algorithms/anomaly/config/test_model_config_load.py new file mode 100644 index 00000000000..32afaa696da --- /dev/null +++ b/tests/unit/algorithms/anomaly/config/test_model_config_load.py @@ -0,0 +1,65 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +import pytest + +from otx.algorithms.anomaly.adapters.anomalib.config import get_anomalib_config +from otx.algorithms.anomaly.configs.classification.draem import ( + DraemAnomalyClassificationConfig, +) +from otx.algorithms.anomaly.configs.classification.padim import ( + PadimAnomalyClassificationConfig, +) +from otx.algorithms.anomaly.configs.classification.stfpm import ( + STFPMAnomalyClassificationConfig, +) +from otx.algorithms.anomaly.configs.detection.draem import DraemAnomalyDetectionConfig +from otx.algorithms.anomaly.configs.detection.padim import PadimAnomalyDetectionConfig +from otx.algorithms.anomaly.configs.detection.stfpm import STFPMAnomalyDetectionConfig +from otx.algorithms.anomaly.configs.segmentation.draem import ( + DraemAnomalySegmentationConfig, +) +from otx.algorithms.anomaly.configs.segmentation.padim import ( + PadimAnomalySegmentationConfig, +) +from otx.algorithms.anomaly.configs.segmentation.stfpm import ( + STFPMAnomalySegmentationConfig, +) +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.helper import convert, create +from tests.unit.algorithms.anomaly.helpers.utils import get_model_template + + +@pytest.mark.parametrize( + ["model_name", "configurable_parameters"], + [ + ("padim", PadimAnomalyClassificationConfig), + ("padim", PadimAnomalyDetectionConfig), + ("padim", PadimAnomalySegmentationConfig), + ("stfpm", STFPMAnomalyClassificationConfig), + ("stfpm", STFPMAnomalyDetectionConfig), + ("stfpm", STFPMAnomalySegmentationConfig), + ("draem", DraemAnomalyClassificationConfig), + ("draem", DraemAnomalyDetectionConfig), + ("draem", DraemAnomalySegmentationConfig), + ], +) +def test_model_template_loading(model_name, configurable_parameters): + """Test that we can load the model template and create a config from it.""" + # Create from class + configuration = configurable_parameters() + configurable_parameters_yaml_str = convert(configuration, str) + assert isinstance(configurable_parameters_yaml_str, str) + + # Check if it can be created from yaml + configurable_parameters_yaml: ConfigurableParameters = create(configurable_parameters_yaml_str) + + model_template = get_model_template(model_name) + hyper_parameters: dict = model_template.hyper_parameters.data + configurable_parameters_loaded = create(hyper_parameters) + + assert configurable_parameters_yaml == configurable_parameters_loaded + + # Confirm that we can create an anomalib config from the loaded yaml + get_anomalib_config(model_name, configurable_parameters_loaded) diff --git a/tests/unit/algorithms/anomaly/conftest.py b/tests/unit/algorithms/anomaly/conftest.py new file mode 100644 index 00000000000..4a8215820d1 --- /dev/null +++ b/tests/unit/algorithms/anomaly/conftest.py @@ -0,0 +1,40 @@ +"""Fixtures for anomaly tests.""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass + +import pytest + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.task_environment import TaskEnvironment +from tests.unit.algorithms.anomaly.helpers.dummy_dataset import get_hazelnut_dataset +from tests.unit.algorithms.anomaly.helpers.utils import create_task_environment + + +@dataclass(frozen=True) # this ensures that the objects are immutable across tests +class TestEnvironment: + """Test environment for anomaly tests.""" + + task_environment: TaskEnvironment + output_model: ModelEntity + dataset: DatasetEntity + task_type: TaskType + + +@pytest.fixture( + scope="session", params=[TaskType.ANOMALY_CLASSIFICATION, TaskType.ANOMALY_DETECTION, TaskType.ANOMALY_SEGMENTATION] +) +def setup_task_environment(request): + """Returns a task environment, a model and datset.""" + task_type = request.param + dataset: DatasetEntity = get_hazelnut_dataset(task_type, one_each=True) + task_environment = create_task_environment(dataset, task_type) + output_model = ModelEntity( + dataset, + task_environment.get_model_configuration(), + ) + environment = TestEnvironment(task_environment, output_model, dataset, task_type) + return environment diff --git a/tests/unit/algorithms/anomaly/helpers/__init__.py b/tests/unit/algorithms/anomaly/helpers/__init__.py new file mode 100644 index 00000000000..500bc1fef2e --- /dev/null +++ b/tests/unit/algorithms/anomaly/helpers/__init__.py @@ -0,0 +1,4 @@ +"""Test helpers.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algorithms/anomaly/helpers/dummy_dataset.py b/tests/unit/algorithms/anomaly/helpers/dummy_dataset.py new file mode 100644 index 00000000000..5bd7f39d3c7 --- /dev/null +++ b/tests/unit/algorithms/anomaly/helpers/dummy_dataset.py @@ -0,0 +1,179 @@ +"""Datasets for testing anomaly tasks""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path +from typing import Dict, Tuple + +import albumentations as A +import numpy as np +from albumentations.pytorch import ToTensorV2 +from bson import ObjectId +from omegaconf import OmegaConf +from pytorch_lightning.core.datamodule import LightningDataModule +from torch.utils.data import DataLoader + +from otx.algorithms.anomaly.adapters.anomalib.data.data import ( + OTXAnomalyDataModule, + OTXAnomalyDataset, +) +from otx.algorithms.anomaly.adapters.anomalib.data.dataset import ( + AnomalyClassificationDataset, + AnomalyDetectionDataset, + AnomalySegmentationDataset, +) +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity, DatasetPurpose +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset + + +def get_hazelnut_dataset(task_type: TaskType, one_each: bool = False) -> DatasetEntity: + """Get hazelnut dataset. + + Args: + task_type (TaskType): Task type. + one_each (bool): If this flag is true then it will sample one normal and one abnormal image for each split. + The training split will have only one normal image. Defaults to False. + """ + dataset: DatasetEntity + if task_type == TaskType.ANOMALY_CLASSIFICATION: + train_subset, test_subset, val_subset = _get_annotations("classification") + dataset = AnomalyClassificationDataset(train_subset, val_subset, test_subset) + elif task_type == TaskType.ANOMALY_SEGMENTATION: + train_subset, test_subset, val_subset = _get_annotations("segmentation") + dataset = AnomalySegmentationDataset(train_subset, val_subset, test_subset) + elif task_type == TaskType.ANOMALY_DETECTION: + train_subset, test_subset, val_subset = _get_annotations("detection") + dataset = AnomalyDetectionDataset(train_subset, val_subset, test_subset) + else: + raise ValueError(f"{task_type} not supported.") + + if one_each: + train_subset = [] + test_subset = [] + val_subset = [] + for item in dataset._items: + if item.subset == Subset.TRAINING and len(train_subset) < 1: + train_subset.append(item) + # Check if the label is already present in the subset. + elif item.subset == Subset.TESTING and item.annotation_scene.annotations[0].get_labels()[0].name not in [ + a.annotation_scene.annotations[0].get_labels()[0].name for a in test_subset + ]: + test_subset.append(item) + # Check if the label is already present in the subset. + elif item.subset == Subset.VALIDATION and item.annotation_scene.annotations[0].get_labels()[0].name not in [ + a.annotation_scene.annotations[0].get_labels()[0].name for a in val_subset + ]: + val_subset.append(item) + dataset._items = train_subset + test_subset + val_subset + return dataset + + +class DummyDataset(OTXAnomalyDataset): + def __init__(self, task_type: TaskType): + self.normal_label = LabelEntity(id=ID(0), name="Normal", domain=Domain.ANOMALY_CLASSIFICATION) + self.abnormal_label = LabelEntity( + id=ID(1), + name="Anomalous", + domain=Domain.ANOMALY_CLASSIFICATION, + is_anomalous=True, + ) + self.dataset = self.get_mock_dataitems() + self.task_type = task_type + self.transform = A.Compose( + [ + A.Resize(32, 32, always_apply=True), + ToTensorV2(), + ] + ) + self.config = OmegaConf.create({"dataset": {"image_size": [32, 32]}}) + + def get_mock_dataitems(self) -> DatasetEntity: + dataset_items = [] + + image_anomalous = Image(np.ones((32, 32, 1))) + annotations = [ + Annotation(Rectangle.generate_full_box(), labels=[ScoredLabel(label=self.abnormal_label, probability=1.0)]) + ] + polygon = Polygon(points=[Point(0.0, 0.0), Point(1.0, 1.0)]) + annotations.append( + Annotation(shape=polygon, labels=[ScoredLabel(self.abnormal_label, probability=1.0)], id=ID(ObjectId())) + ) + annotation_scene = AnnotationSceneEntity(annotations=annotations, kind=AnnotationSceneKind.ANNOTATION) + dataset_items.append(DatasetItemEntity(media=image_anomalous, annotation_scene=annotation_scene)) + + image_normal = Image(np.zeros((32, 32, 1))) + annotations = [ + Annotation(Rectangle.generate_full_box(), labels=[ScoredLabel(label=self.normal_label, probability=1.0)]) + ] + annotation_scene = AnnotationSceneEntity(annotations=annotations, kind=AnnotationSceneKind.ANNOTATION) + dataset_items.append(DatasetItemEntity(media=image_normal, annotation_scene=annotation_scene)) + + return DatasetEntity(items=dataset_items, purpose=DatasetPurpose.INFERENCE) + + +class DummyDataModule(LightningDataModule): + def __init__(self, task_type: TaskType): + super().__init__() + self.dataset = DummyDataset(task_type) + self.labels = [self.dataset.abnormal_label, self.dataset.normal_label] + + def train_dataloader(self) -> DataLoader: + return DataLoader(self.dataset, pin_memory=True) + + def test_dataloader(self) -> DataLoader: + return DataLoader(self.dataset, pin_memory=True) + + def val_dataloader(self) -> DataLoader: + return DataLoader(self.dataset, pin_memory=True) + + def predict_dataloader(self) -> DataLoader: + return DataLoader(self.dataset, shuffle=False, pin_memory=True) + + +class HazelnutDataModule(OTXAnomalyDataModule): + """Creates datamodule with hazelnut dataset. + + Args: + task_type (TaskType): Task type (classification, detection, segmentation) + """ + + def __init__(self, task_type: TaskType): + self.config = OmegaConf.create( + { + "dataset": { + "eval_batch_size": 32, + "train_batch_size": 32, + "test_batch_size": 32, + "num_workers": 2, + "image_size": [32, 32], + "transform_config": {"train": None}, + } + } + ) + self.dataset = get_hazelnut_dataset(task_type) + super().__init__(config=self.config, dataset=self.dataset, task_type=task_type) + + +def _get_annotations(task: str) -> Tuple[Dict, Dict, Dict]: + ann_file_root = Path("tests", "assets", "anomaly", task) + data_root = Path("tests", "assets", "anomaly", "hazelnut") + + train_subset = {"ann_file": str(ann_file_root / "train.json"), "data_root": str(data_root)} + test_subset = {"ann_file": str(ann_file_root / "test.json"), "data_root": str(data_root)} + + val_subset = {"ann_file": str(ann_file_root / "val.json"), "data_root": str(data_root)} + return train_subset, test_subset, val_subset diff --git a/tests/unit/algorithms/anomaly/helpers/dummy_model.py b/tests/unit/algorithms/anomaly/helpers/dummy_model.py new file mode 100644 index 00000000000..e09a8bfc731 --- /dev/null +++ b/tests/unit/algorithms/anomaly/helpers/dummy_model.py @@ -0,0 +1,41 @@ +"""Dummy lightning modules for testing.""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any + +import pytorch_lightning as pl +import torch +from anomalib.data.utils import masks_to_boxes +from anomalib.utils.metrics import AnomalyScoreThreshold, MinMax + + +class DummyModel(pl.LightningModule): + """Returns mock outputs.""" + + def __init__(self): + super().__init__() + self.image_threshold = AnomalyScoreThreshold() + self.normalization_metrics = MinMax() + + def configure_optimizers(self): + return None + + def training_step(self, *args, **kwargs): + pass + + def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> Any: + # Just return everything as anomalous + batch["anomaly_maps"] = batch["pred_masks"] = torch.ones(batch["image"].shape[0], 1, *batch["image"].shape[2:]) + batch["pred_labels"] = batch["pred_scores"] = torch.ones((batch["image"].shape[0])) + batch["pred_boxes"], batch["box_scores"] = masks_to_boxes(batch["pred_masks"], batch["anomaly_maps"]) + is_anomalous = [scores > 0.5 for scores in batch["box_scores"]] + batch["box_labels"] = [labels.int() for labels in is_anomalous] + return batch + + def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0): + return self.predict_step(batch, batch_idx, dataloader_idx) + + def test_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0): + return self.predict_step(batch, batch_idx, dataloader_idx) diff --git a/tests/unit/algorithms/anomaly/helpers/utils.py b/tests/unit/algorithms/anomaly/helpers/utils.py new file mode 100644 index 00000000000..e1427e937bd --- /dev/null +++ b/tests/unit/algorithms/anomaly/helpers/utils.py @@ -0,0 +1,40 @@ +"""Helper utils for tests.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path + +from otx.api.configuration.helper import create as create_hyper_parameters +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model_template import ( + ModelTemplate, + TaskType, + parse_model_template, +) +from otx.api.entities.task_environment import TaskEnvironment + + +def get_model_template(model_name: str, task_type: str = "classification") -> ModelTemplate: + template_file_root = Path("src", "otx", "algorithms", "anomaly", "configs", task_type, model_name) + template_file_path = ( + template_file_root / "template.yaml" + if (template_file_root / "template.yaml").exists() + else template_file_root / "template_experimental.yaml" + ) + model_template: ModelTemplate = parse_model_template(str(template_file_path)) + return model_template + + +def create_task_environment(dataset: DatasetEntity, task_type: TaskType) -> TaskEnvironment: + # get padim model template + padim_template = get_model_template("padim") + padim_template.task_type = task_type + hyper_parameters = create_hyper_parameters(padim_template.hyper_parameters.data) + labels = dataset.get_labels() + label_schema = LabelSchemaEntity.from_labels(labels) + + return TaskEnvironment( + model_template=padim_template, model=None, hyper_parameters=hyper_parameters, label_schema=label_schema + ) diff --git a/tests/unit/algorithms/anomaly/tasks/__init__.py b/tests/unit/algorithms/anomaly/tasks/__init__.py new file mode 100644 index 00000000000..db8c395f215 --- /dev/null +++ b/tests/unit/algorithms/anomaly/tasks/__init__.py @@ -0,0 +1,4 @@ +"""Test tasks.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algorithms/anomaly/tasks/test_inference.py b/tests/unit/algorithms/anomaly/tasks/test_inference.py new file mode 100644 index 00000000000..7c050866674 --- /dev/null +++ b/tests/unit/algorithms/anomaly/tasks/test_inference.py @@ -0,0 +1,86 @@ +"""Tests the methods in the Inference task.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from copy import deepcopy + +from otx.algorithms.anomaly.tasks.inference import InferenceTask +from otx.algorithms.anomaly.tasks.train import TrainingTask +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from tests.unit.algorithms.anomaly.helpers.dummy_dataset import get_hazelnut_dataset +from tests.unit.algorithms.anomaly.helpers.utils import create_task_environment + + +class TestInferenceTask: + """Tests the methods in the inference task.""" + + @pytest.mark.skip(reason="CVS-107918 FAIL code -11 in anomaly unit test on python3.10") + def test_inference(self, tmpdir, setup_task_environment): + """Tests the inference method.""" + root = str(tmpdir.mkdir("anomaly_inference_test")) + + # Get task environment + setup_task_environment = deepcopy(setup_task_environment) # since fixture is mutable + task_environment = setup_task_environment.task_environment + task_type = setup_task_environment.task_type + output_model = setup_task_environment.output_model + dataset = setup_task_environment.dataset + + # 1. Create the training task and get inference results on an untrained model. + dataset = dataset.get_subset(Subset.VALIDATION) + train_task = TrainingTask(task_environment, output_path=root) + dataset = train_task.infer(dataset.with_empty_annotations(), InferenceParameters(is_evaluation=True)) + train_task.save_model(output_model) + # 2. check if the model is saved correctly + assert output_model.get_data("weights.pth") is not None # Should not raise an error + + # 3. Create new task environment and inference task and test inference + new_dataset: DatasetEntity = get_hazelnut_dataset(task_type, one_each=True) + gt_val_dataset = new_dataset.get_subset(Subset.VALIDATION) + new_task_environment = create_task_environment(gt_val_dataset, task_type) + # this loads the output model from the previous training task when creating the new InferenceTask + new_task_environment.model = output_model + inference_task = InferenceTask(new_task_environment, output_path=root) + pred_val_dataset = inference_task.infer( + gt_val_dataset.with_empty_annotations(), InferenceParameters(is_evaluation=True) + ) + + # compare labels with the original validation dataset + for item1, item2 in zip(dataset, pred_val_dataset): + assert set([label.name for label in item1.annotation_scene.get_labels()]) == set( + [label.name for label in item2.annotation_scene.get_labels()] + ) + + # 4. Check whether performance metrics are produced correctly + # This tests whether the performance metrics are calculated correctly and assigned to the result set + output_model = ModelEntity( + gt_val_dataset, + new_task_environment.get_model_configuration(), + ) + result_set = ResultSetEntity( + model=output_model, ground_truth_dataset=gt_val_dataset, prediction_dataset=pred_val_dataset + ) + inference_task.evaluate(result_set) + if task_type in (TaskType.ANOMALY_CLASSIFICATION, TaskType.ANOMALY_DETECTION): + assert result_set.performance.score.name == "f-measure" + elif task_type == TaskType.ANOMALY_SEGMENTATION: + assert result_set.performance.score.name == "Dice Average" + + # 5. Check if OpenVINO model can be generated + inference_task.export(ExportType.OPENVINO, output_model, dump_features=False) + assert output_model.get_data("openvino.bin") is not None # Should not raise an error + assert not output_model.has_xai + + # 6. Check if ONNX model can be generated + inference_task.export(ExportType.OPENVINO, output_model, dump_features=False) + assert output_model.get_data("model.onnx") is not None # Should not raise an error + assert not output_model.has_xai diff --git a/tests/unit/algorithms/anomaly/tasks/test_nncf.py b/tests/unit/algorithms/anomaly/tasks/test_nncf.py new file mode 100644 index 00000000000..cd229ad0d2a --- /dev/null +++ b/tests/unit/algorithms/anomaly/tasks/test_nncf.py @@ -0,0 +1,52 @@ +"""Tests the methods in the NNCF task.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from copy import deepcopy + +from otx.algorithms.anomaly.tasks.nncf import NNCFTask +from otx.algorithms.anomaly.tasks.train import TrainingTask +from otx.api.entities.model import ModelEntity, ModelOptimizationType +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType + + +class TestNNCFTask: + """Tests methods in the NNCF task.""" + + @pytest.mark.skip(reason="CVS-107918 FAIL code -11 in anomaly unit test on python3.10") + def test_nncf(self, tmpdir, setup_task_environment): + """Tests the NNCF optimize method.""" + root = str(tmpdir.mkdir("anomaly_nncf_test")) + + # Get task environment + setup_task_environment = deepcopy(setup_task_environment) # since fixture is mutable + task_environment = setup_task_environment.task_environment + output_model = setup_task_environment.output_model + dataset = setup_task_environment.dataset + + train_task = TrainingTask(task_environment, output_path=root) + train_task.train(dataset, output_model, TrainParameters()) + task_environment.model = output_model + + # create normal nncf task + nncf_task = NNCFTask(task_environment, output_path=root) + + # optimize the model + assert output_model.optimization_type == ModelOptimizationType.NONE + optimized_model = ModelEntity(dataset, configuration=task_environment.get_model_configuration()) + nncf_task.optimize(OptimizationType.NNCF, dataset, optimized_model) + assert optimized_model.optimization_type == ModelOptimizationType.NNCF + + # load the optimized model + new_nncf_task = NNCFTask(task_environment, output_path=root) + assert new_nncf_task.compression_ctrl is None + new_nncf_task.model = new_nncf_task.load_model(optimized_model) + assert new_nncf_task.compression_ctrl is not None + + # Export model + new_nncf_task.export(ExportType.OPENVINO, optimized_model) + optimized_model.get_data("openvino.bin") # Should not raise an exception diff --git a/tests/unit/algorithms/anomaly/tasks/test_openvino.py b/tests/unit/algorithms/anomaly/tasks/test_openvino.py new file mode 100644 index 00000000000..8fb222189aa --- /dev/null +++ b/tests/unit/algorithms/anomaly/tasks/test_openvino.py @@ -0,0 +1,143 @@ +"""Tests the methods in the OpenVINO task.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import json +from copy import deepcopy +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest + +from otx.algorithms.anomaly.tasks.openvino import OpenVINOTask +from otx.algorithms.anomaly.tasks.train import TrainingTask +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ModelEntity, ModelOptimizationType +from otx.api.entities.model_template import TaskType +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.cli.utils.io import read_model + + +class TestOpenVINOTask: + """Tests methods in the OpenVINO task.""" + + @pytest.fixture + def tmp_dir(self): + with TemporaryDirectory() as tmp_dir: + yield tmp_dir + + def set_normalization_params(self, output_model: ModelEntity): + """Sets normalization parameters for an untrained output model. + + This is needed as untrained model might have nan values for normalization parameters which will raise an error. + """ + output_model.set_data("image_threshold", np.float32(0.5).tobytes()) + output_model.set_data("pixel_threshold", np.float32(0.5).tobytes()) + output_model.set_data("min", np.float32(0).tobytes()) + output_model.set_data("max", np.float32(1).tobytes()) + + @pytest.mark.skip(reason="CVS-107918 FAIL code -11 in anomaly unit test on python3.10") + def test_openvino(self, tmpdir, setup_task_environment): + """Tests the OpenVINO optimize method.""" + root = str(tmpdir.mkdir("anomaly_openvino_test")) + + setup_task_environment = deepcopy(setup_task_environment) # since fixture is mutable + task_type = setup_task_environment.task_type + dataset: DatasetEntity = setup_task_environment.dataset + task_environment = setup_task_environment.task_environment + output_model = setup_task_environment.output_model + + # set normalization params for the output model + train_task = TrainingTask(task_environment, output_path=root) + self.set_normalization_params(output_model) + train_task.save_model(output_model) + task_environment.model = output_model + train_task.export(ExportType.OPENVINO, output_model) + + # Create OpenVINO task + openvino_task = OpenVINOTask(task_environment) + + # call inference + dataset = dataset.get_subset(Subset.VALIDATION) + predicted_dataset = openvino_task.infer( + dataset.with_empty_annotations(), InferenceParameters(is_evaluation=True) + ) + + # call evaluate + result_set = ResultSetEntity(output_model, dataset, predicted_dataset) + openvino_task.evaluate(result_set) + if task_type in (TaskType.ANOMALY_CLASSIFICATION, TaskType.ANOMALY_DETECTION): + assert result_set.performance.score.name == "f-measure" + elif task_type == TaskType.ANOMALY_SEGMENTATION: + assert result_set.performance.score.name == "Dice Average" + + # optimize to POT + openvino_task.optimize(OptimizationType.POT, dataset, output_model, OptimizationParameters()) + assert output_model.optimization_type == ModelOptimizationType.POT + assert output_model.get_data("label_schema.json") is not None + + # deploy + openvino_task.deploy(output_model) + assert output_model.exportable_code is not None + + @patch.multiple(OpenVINOTask, get_config=MagicMock(), get_openvino_model=MagicMock()) + @patch("otx.algorithms.anomaly.tasks.openvino.get_transforms", MagicMock()) + def test_anomaly_legacy_keys(self, mocker, tmp_dir): + """Checks whether the model is loaded correctly with legacy and current keys.""" + + tmp_dir = Path(tmp_dir) + xml_model_path = tmp_dir / "model.xml" + xml_model_path.write_text("xml_model") + bin_model_path = tmp_dir / "model.bin" + bin_model_path.write_text("bin_model") + + # Test loading legacy keys + legacy_keys = ("image_threshold", "pixel_threshold", "min", "max") + for key in legacy_keys: + (tmp_dir / key).write_bytes(np.zeros(1, dtype=np.float32).tobytes()) + + model = read_model(mocker.MagicMock(), str(xml_model_path), mocker.MagicMock()) + task_environment = TaskEnvironment( + model_template=mocker.MagicMock(), + model=model, + hyper_parameters=mocker.MagicMock(), + label_schema=LabelSchemaEntity.from_labels( + [ + LabelEntity("Anomalous", is_anomalous=True, domain=Domain.ANOMALY_SEGMENTATION), + LabelEntity("Normal", domain=Domain.ANOMALY_SEGMENTATION), + ] + ), + ) + openvino_task = OpenVINOTask(task_environment) + metadata = openvino_task.get_metadata() + for key in legacy_keys: + assert metadata[key] == np.zeros(1, dtype=np.float32) + + # cleanup legacy keys + for key in legacy_keys: + (tmp_dir / key).unlink() + + # Test loading new keys + new_metadata = { + "image_threshold": np.zeros(1, dtype=np.float32).tolist(), + "pixel_threshold": np.zeros(1, dtype=np.float32).tolist(), + "min": np.zeros(1, dtype=np.float32).tolist(), + "max": np.zeros(1, dtype=np.float32).tolist(), + } + (tmp_dir / "metadata").write_bytes(json.dumps(new_metadata).encode()) + task_environment.model = read_model(mocker.MagicMock(), str(xml_model_path), mocker.MagicMock()) + openvino_task = OpenVINOTask(task_environment) + metadata = openvino_task.get_metadata() + for key in new_metadata.keys(): + assert metadata[key] == np.zeros(1, dtype=np.float32) diff --git a/tests/unit/algorithms/anomaly/tasks/test_train.py b/tests/unit/algorithms/anomaly/tasks/test_train.py new file mode 100644 index 00000000000..144b8fe57f3 --- /dev/null +++ b/tests/unit/algorithms/anomaly/tasks/test_train.py @@ -0,0 +1,50 @@ +"""Tests the methods in the train task.""" + +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from copy import deepcopy + +from torch import nn + +from otx.algorithms.anomaly.tasks.train import TrainingTask +from otx.api.entities.train_parameters import TrainParameters +from tests.unit.algorithms.anomaly.helpers.utils import create_task_environment + + +class TestTrainTask: + """Tests the methods in the train task.""" + + def _compare_state_dict(self, model1: nn.Module, model2: nn.Module): + """Compares the state dict of two models.""" + state_dict1 = model1.state_dict() + state_dict2 = model2.state_dict() + for (key1, param1), (key2, param2) in zip(state_dict1.items(), state_dict2.items()): + assert key1 == key2 + if not param1.data.isnan().any() and "bn" not in key1: + assert param1.data.allclose(param2.data) + + @pytest.mark.skip(reason="CVS-107918 FAIL code -11 in anomaly unit test on python3.10") + def test_train_and_load(self, tmpdir, setup_task_environment): + """Tests the train method and check if it can be loaded correctly.""" + root = str(tmpdir.mkdir("anomaly_training_test")) + + # Get task environment + setup_task_environment = deepcopy(setup_task_environment) # since fixture is mutable + task_environment = setup_task_environment.task_environment + output_model = setup_task_environment.output_model + dataset = setup_task_environment.dataset + task_type = setup_task_environment.task_type + + train_task = TrainingTask(task_environment, output_path=root) + train_task.train(dataset, output_model, TrainParameters()) + train_task.model = train_task.load_model(output_model) + + # create a new output_model and load from task environment + # check if the loaded model is the same as the trained model + new_task_environment = create_task_environment(dataset, task_type) + new_task_environment.model = output_model + new_task = TrainingTask(new_task_environment, output_path=root) # should load the model from the output_model + self._compare_state_dict(train_task.model, new_task.model) diff --git a/tests/unit/algorithms/classification/__init__.py b/tests/unit/algorithms/classification/__init__.py new file mode 100644 index 00000000000..2faffbe2b1f --- /dev/null +++ b/tests/unit/algorithms/classification/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/tests/unit/algorithms/classification/adapters/__init__.py b/tests/unit/algorithms/classification/adapters/__init__.py new file mode 100644 index 00000000000..2faffbe2b1f --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/tests/unit/algorithms/classification/adapters/mmcls/__init__.py b/tests/unit/algorithms/classification/adapters/mmcls/__init__.py new file mode 100644 index 00000000000..86d8e783a9f --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.classification.adapters.mmcls""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/mmcls/api/__init__.py b/tests/unit/algorithms/classification/adapters/mmcls/api/__init__.py new file mode 100644 index 00000000000..8d0d86fe233 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/api/__init__.py @@ -0,0 +1,5 @@ +"""Test for otx.algorithms.classification.adapters.mmcls.api""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/mmcls/api/test_train.py b/tests/unit/algorithms/classification/adapters/mmcls/api/test_train.py new file mode 100644 index 00000000000..26143b66807 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/api/test_train.py @@ -0,0 +1,120 @@ +"""Test for otx.algorithms.classification.adapters.mmcls.apis.train""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from unittest import mock +from otx.algorithms.classification.adapters.mmcls.apis.train import train_model +import mmcv +import torch +from otx.algorithms.common.utils.utils import is_xpu_available + + +class TestTrainModel: + @pytest.fixture + def mock_modules(self, mocker): + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.get_root_logger", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.wrap_distributed_model", + return_value=mock.MagicMock(), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.wrap_non_distributed_model", + return_value=mock.MagicMock(), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.build_optimizer", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.build_runner", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.build_dataset", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.DistEvalHook", return_value=mock.MagicMock() + ) + mocker.patch("otx.algorithms.classification.adapters.mmcls.apis.train.EvalHook", return_value=mock.MagicMock()) + + @pytest.fixture + def mmcv_cfg(self): + return mmcv.Config( + { + "gpu_ids": [0], + "seed": 42, + "data": mock.MagicMock(), + "device": "cpu", + "optimizer": "SGD", + "optimizer_config": {}, + "total_epochs": 1, + "work_dir": "test", + "lr_config": {}, + "checkpoint_config": {}, + "log_config": {}, + "resume_from": False, + "load_from": "", + "workflow": "", + } + ) + + @pytest.fixture + def model(self): + return mock.MagicMock() + + @pytest.fixture + def dataset(self): + return mock.MagicMock() + + def test_train_model_single_dataset_no_validation(self, mock_modules, mmcv_cfg, model, dataset): + # Create mock inputs + _ = mock_modules + # Call the function + train_model(model, dataset, mmcv_cfg, validate=False) + + def test_train_model_multiple_datasets_distributed_training(self, mock_modules, mmcv_cfg, model, dataset): + # Create mock inputs + _ = mock_modules + # Call the function + train_model(model, [dataset, dataset], mmcv_cfg, distributed=True, validate=True) + + def test_train_model_specific_timestamp_and_cuda_device(self, mock_modules, mmcv_cfg, model, dataset): + # Create mock inputs + _ = mock_modules + timestamp = "2024-01-01" + device = "cuda" + mmcv_cfg.device = "cuda" + meta = {"info": "some_info"} + # Call the function + train_model(model, dataset, mmcv_cfg, timestamp=timestamp, device=device, meta=meta) + + def test_train_model_xpu_device(self, mock_modules, mmcv_cfg, model, dataset, mocker): + # Create mock inputs + _ = mock_modules + device = "xpu" + mmcv_cfg.device = "xpu" + mocker.patch("otx.algorithms.classification.adapters.mmcls.apis.train.torch") + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.apis.train.torch.xpu.optimize", + return_value=(mocker.MagicMock(), mocker.MagicMock()), + ) + # Call the function + train_model(model, dataset, mmcv_cfg, device=device) diff --git a/tests/unit/algorithms/classification/adapters/mmcls/data/__init__.py b/tests/unit/algorithms/classification/adapters/mmcls/data/__init__.py new file mode 100644 index 00000000000..76e2b007675 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/data/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.classification.adapters.mmcls.data""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/mmcls/data/test_datasets.py b/tests/unit/algorithms/classification/adapters/mmcls/data/test_datasets.py new file mode 100644 index 00000000000..3b5a66d2e57 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/data/test_datasets.py @@ -0,0 +1,173 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest + +from otx.algorithms.classification.adapters.mmcls.datasets import ( + OTXClsDataset, + OTXHierarchicalClsDataset, + OTXMultilabelClsDataset, + SelfSLDataset, +) +from otx.algorithms.classification.utils import get_multihead_class_info +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.classification.test_helper import ( + DEFAULT_CLS_TEMPLATE, + init_environment, + setup_configurable_parameters, +) +from tests.unit.api.parameters_validation.validation_helper import ( + check_value_error_exception_raised, +) + + +def create_cls_dataset(): + image = Image(data=np.random.randint(low=0, high=255, size=(8, 8, 3)).astype(np.uint8)) + annotation = Annotation( + shape=Rectangle.generate_full_box(), + labels=[ScoredLabel(LabelEntity(name="test_selfsl_dataset", domain=Domain.CLASSIFICATION))], + ) + annotation_scene = AnnotationSceneEntity(annotations=[annotation], kind=AnnotationSceneKind.ANNOTATION) + dataset_item = DatasetItemEntity(media=image, annotation_scene=annotation_scene) + + dataset = DatasetEntity(items=[dataset_item]) + return dataset, dataset.get_labels() + + +class TestSelfSLDataset: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.otx_dataset, _ = create_cls_dataset() + self.pipeline = { + "view0": [dict(type="ImageToTensor", keys=["img"])], + "view1": [dict(type="ImageToTensor", keys=["img"])], + } + + @e2e_pytest_unit + def test_getitem(self): + """Test __getitem__ method.""" + dataset = SelfSLDataset(otx_dataset=self.otx_dataset, pipeline=self.pipeline) + + data_item = dataset[0] + for i in range(1, 3): + assert f"dataset_item{i}" in data_item + assert f"width{i}" in data_item + assert f"height{i}" in data_item + assert f"index{i}" in data_item + assert f"filename{i}" in data_item + assert f"ori_filename{i}" in data_item + assert f"img{i}" in data_item + assert f"img_shape{i}" in data_item + assert f"ori_shape{i}" in data_item + assert f"pad_shape{i}" in data_item + assert f"img_norm_cfg{i}" in data_item + assert f"img_fields{i}" in data_item + + +class TestOTXClsDataset: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.hyper_parameters, self.model_template = setup_configurable_parameters(DEFAULT_CLS_TEMPLATE) + self.dataset_len = 20 + + @pytest.mark.parametrize( + "adapter_type", + [OTXClsDataset, OTXMultilabelClsDataset], + ) + @e2e_pytest_unit + def test_create_dataset_adapter(self, adapter_type): + multilabel = adapter_type == OTXClsDataset + self.task_environment, self.dataset = init_environment( + self.hyper_parameters, self.model_template, multilabel, False, self.dataset_len + ) + dataset = adapter_type(self.dataset, labels=self.dataset.get_labels()) + assert dataset.num_classes == len(self.dataset.get_labels()) + assert len(dataset) == self.dataset_len * dataset.num_classes + for d in dataset: + assert d is not None + + @pytest.mark.parametrize( + "adapter_type", + [OTXClsDataset, OTXMultilabelClsDataset], + ) + @e2e_pytest_unit + def test_metric_dataset_adapter(self, adapter_type): + multilabel = adapter_type == OTXMultilabelClsDataset + self.task_environment, self.dataset = init_environment( + self.hyper_parameters, self.model_template, multilabel, False, self.dataset_len + ) + dataset = adapter_type(self.dataset, labels=self.dataset.get_labels()) + results = np.ones((len(dataset), dataset.num_classes)) + metrics = dataset.evaluate(results) + + assert len(metrics) > 0 + if multilabel: + metrics["mAP"] > 0 + else: + metrics["accuracy"] > 0 + + @e2e_pytest_unit + def test_create_hierarchical_adapter(self): + self.task_environment, self.dataset = init_environment( + self.hyper_parameters, self.model_template, False, True, self.dataset_len + ) + class_info = get_multihead_class_info(self.task_environment.label_schema) + dataset = OTXHierarchicalClsDataset( + otx_dataset=self.dataset, labels=self.dataset.get_labels(), hierarchical_info=class_info + ) + assert dataset.num_classes == len(self.dataset.get_labels()) + assert len(dataset) == self.dataset_len * dataset.num_classes + for d in dataset: + assert d is not None + + @e2e_pytest_unit + def test_metric_hierarchical_adapter(self): + self.task_environment, self.dataset = init_environment( + self.hyper_parameters, self.model_template, False, True, self.dataset_len + ) + class_info = get_multihead_class_info(self.task_environment.label_schema) + dataset = OTXHierarchicalClsDataset( + otx_dataset=self.dataset, labels=self.dataset.get_labels(), hierarchical_info=class_info + ) + results = np.zeros((len(dataset), dataset.num_classes)) + metrics = dataset.evaluate(results) + + assert len(metrics) > 0 + assert metrics["accuracy"] > 0 + + @e2e_pytest_unit + def test_hierarchical_with_empty_heads(self): + self.task_environment, self.dataset = init_environment( + self.hyper_parameters, self.model_template, False, True, self.dataset_len + ) + class_info = get_multihead_class_info(self.task_environment.label_schema) + dataset = OTXHierarchicalClsDataset( + otx_dataset=self.dataset, labels=self.dataset.get_labels(), hierarchical_info=class_info + ) + pseudo_gt_labels = [] + pseudo_head_idx = 0 + for label in dataset.gt_labels: + pseudo_gt_label = label + pseudo_gt_label[pseudo_head_idx] = -1 + pseudo_gt_labels.append(pseudo_gt_label) + pseudo_gt_labels = np.array(pseudo_gt_labels) + + from copy import deepcopy + + pseudo_dataset = deepcopy(dataset) + pseudo_dataset.gt_labels = pseudo_gt_labels + pseudo_dataset._update_heads_information() + assert pseudo_dataset.hierarchical_info["empty_multiclass_head_indices"][pseudo_head_idx] == 0 diff --git a/tests/unit/algorithms/classification/adapters/mmcls/data/test_pipelines.py b/tests/unit/algorithms/classification/adapters/mmcls/data/test_pipelines.py new file mode 100644 index 00000000000..cc42cf4bdd5 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/data/test_pipelines.py @@ -0,0 +1,209 @@ +import numpy as np +import pytest +from PIL import Image + +from otx.algorithms.classification.adapters.mmcls.datasets.pipelines.otx_pipelines import ( + GaussianBlur, + LoadImageFromOTXDataset, + LoadResizeDataFromOTXDataset, + ResizeTo, + OTXColorJitter, + PILImageToNDArray, + PostAug, + RandomAppliedTrans, +) +from otx.core.data.caching import MemCacheHandlerSingleton +from tests.test_suite.e2e_test_system import e2e_pytest_unit + +from .test_datasets import create_cls_dataset + + +@pytest.fixture(scope="module") +def inputs_np(): + return {"img": np.random.randint(0, 10, (16, 16, 3), dtype=np.uint8), "img_fields": ["img"]} + + +@pytest.fixture(scope="module") +def inputs_PIL(): + return { + "img": Image.fromarray(np.random.randint(0, 10, (16, 16, 3), dtype=np.uint8)), + } + + +@e2e_pytest_unit +@pytest.mark.parametrize("to_float32", [False, True]) +def test_load_image_from_otx_dataset_call(to_float32): + """Test LoadImageFromOTXDataset.""" + otx_dataset, labels = create_cls_dataset() + load_image_from_otx_dataset = LoadImageFromOTXDataset(to_float32) + results = dict( + dataset_item=otx_dataset[0], + width=otx_dataset[0].width, + height=otx_dataset[0].height, + index=0, + ann_info=dict(label_list=labels), + ) + + results = load_image_from_otx_dataset(results) + + assert "filename" in results + assert "ori_filename" in results + assert "img" in results + assert "img_shape" in results + assert "ori_shape" in results + assert "pad_shape" in results + assert "img_norm_cfg" in results + assert "img_fields" in results + assert isinstance(results["img"], np.ndarray) + + +@e2e_pytest_unit +def test_load_resize_data_from_otx_dataset_call(mocker): + """Test LoadResizeDataFromOTXDataset.""" + otx_dataset, labels = create_cls_dataset() + MemCacheHandlerSingleton.create("singleprocessing", otx_dataset[0].numpy.size) + op = LoadResizeDataFromOTXDataset( + resize_cfg=dict(type="Resize", size=(4, 4)), # 8x8 -> 4x4 + ) + src_dict = dict( + dataset_item=otx_dataset[0], + width=otx_dataset[0].width, + height=otx_dataset[0].height, + index=0, + ann_info=dict(label_list=labels), + ) + dst_dict = op(src_dict) + assert dst_dict["ori_shape"][0] == 8 + assert dst_dict["img_shape"][0] == 4 + assert dst_dict["img"].shape == dst_dict["img_shape"] + op._load_img_op = mocker.MagicMock() + dst_dict_from_cache = op(src_dict) + assert op._load_img_op.call_count == 0 # _load_img() should not be called + assert np.array_equal(dst_dict["img"], dst_dict_from_cache["img"]) + assert dst_dict["ann_info"] == dst_dict_from_cache["ann_info"] + + +@e2e_pytest_unit +def test_load_resize_data_from_otx_dataset_downscale_only(mocker): + """Test LoadResizeDataFromOTXDataset.""" + otx_dataset, labels = create_cls_dataset() + MemCacheHandlerSingleton.create("singleprocessing", otx_dataset[0].numpy.size) + op = LoadResizeDataFromOTXDataset( + resize_cfg=dict(type="Resize", size=(12, 12), downscale_only=True), # 8x8 -> 12x12 + ) + src_dict = dict( + dataset_item=otx_dataset[0], + width=otx_dataset[0].width, + height=otx_dataset[0].height, + index=0, + ann_info=dict(label_list=labels), + ) + dst_dict = op(src_dict) + assert dst_dict["ori_shape"][0] == 8 + assert dst_dict["img_shape"][0] == 8 # Skipped upscale + assert dst_dict["img"].shape == dst_dict["img_shape"] + op._load_img_op = mocker.MagicMock() + dst_dict_from_cache = op(src_dict) + assert op._load_img_op.call_count == 0 # _load_img() should not be called + assert np.array_equal(dst_dict["img"], dst_dict_from_cache["img"]) + assert dst_dict["ann_info"] == dst_dict_from_cache["ann_info"] + + +@e2e_pytest_unit +def test_resize_to(mocker, inputs_np): + """Test LoadResizeDataFromOTXDataset.""" + otx_dataset, labels = create_cls_dataset() + src_dict = dict( + **inputs_np, + ori_shape=(16, 16), + img_shape=(16, 16), + ) + # Test downscale + op = ResizeTo(size=(4, 4)) + dst_dict = op(src_dict) + assert dst_dict["ori_shape"][0] == 16 + assert dst_dict["img_shape"][0] == 4 + assert dst_dict["img"].shape == dst_dict["img_shape"] + # Test upscale from output + op = ResizeTo(size=(8, 8)) + dst_dict = op(dst_dict) + assert dst_dict["ori_shape"][0] == 16 + assert dst_dict["img_shape"][0] == 8 + assert dst_dict["img"].shape == dst_dict["img_shape"] + # Test same size from output + op = ResizeTo(size=(8, 8)) + op._resize_img = mocker.MagicMock() + dst_dict = op(dst_dict) + assert dst_dict["ori_shape"][0] == 16 + assert dst_dict["img_shape"][0] == 8 + assert op._resize_img.call_count == 0 # _resize_img() should not be called + + +@e2e_pytest_unit +def test_random_applied_transforms(mocker, inputs_np): + """Test RandomAppliedTrans.""" + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.datasets.pipelines.otx_pipelines.build_from_cfg", + return_value=lambda x: x, + ) + + random_applied_transforms = RandomAppliedTrans(transforms=[dict()]) + + results = random_applied_transforms(inputs_np) + + assert isinstance(results, dict) + assert "img" in results + assert repr(random_applied_transforms) == "RandomAppliedTrans" + + +@e2e_pytest_unit +def test_otx_color_jitter(inputs_np): + """Test OTXColorJitter.""" + otx_color_jitter = OTXColorJitter() + + results = otx_color_jitter(inputs_np) + + assert isinstance(results, dict) + assert "img" in results + + +@e2e_pytest_unit +def test_gaussian_blur(inputs_np): + """Test GaussianBlur.""" + gaussian_blur = GaussianBlur(sigma_min=0.1, sigma_max=0.2) + + results = gaussian_blur(inputs_np) + + assert isinstance(results, dict) + assert "img" in results + assert repr(gaussian_blur) == "GaussianBlur" + + +@e2e_pytest_unit +def test_pil_image_to_nd_array(inputs_PIL) -> None: + """Test PILImageToNDArray.""" + pil_image_to_nd_array = PILImageToNDArray(keys=["img"]) + + results = pil_image_to_nd_array(inputs_PIL) + + assert "img" in results + assert isinstance(results["img"], np.ndarray) + assert repr(pil_image_to_nd_array) == "PILImageToNDArray" + + +@e2e_pytest_unit +def test_post_aug(mocker, inputs_np): + """Test PostAug.""" + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.datasets.pipelines.otx_pipelines.Compose", + return_value=lambda x: x, + ) + + post_aug = PostAug(keys=dict(orig=lambda x: x)) + + results = post_aug(inputs_np) + + assert isinstance(results, dict) + assert "img" in results and "img" in results["img_fields"] + assert "orig" in results and "orig" in results["img_fields"] + assert repr(post_aug) == "PostAug" diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/__init__.py b/tests/unit/algorithms/classification/adapters/mmcls/models/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/__init__.py b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/__init__.py new file mode 100644 index 00000000000..0c606196d7a --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.classification.adapters.mmcls.models.classifiers""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_byol.py b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_byol.py new file mode 100644 index 00000000000..419e9193c94 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_byol.py @@ -0,0 +1,137 @@ +from copy import deepcopy +from typing import Any, Dict + +import pytest +import torch +import torch.nn as nn + +from otx.algorithms.classification.adapters.mmcls import BYOL +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +class TestBYOL: + """Test BYOL.""" + + @pytest.fixture(autouse=True) + def setup(self, monkeypatch, mocker) -> None: + class MockBackbone(nn.Module): + def __init__(self): + super().__init__() + self.pipeline = nn.Sequential(nn.Conv2d(3, 1, (1, 1), bias=False), nn.Conv2d(1, 1, (1, 1), bias=False)) + + def init_weights(self, pretrained=None): + pass + + def forward(self, x): + return self.pipeline(x) + + class MockNeck(nn.Sequential): + def __init__(self): + super().__init__(nn.Linear(2, 2, bias=False), nn.Linear(2, 2, bias=False)) + + def init_weights(self, init_linear=None): + pass + + class MockHead(nn.Sequential): + def __init__(self): + super().__init__(nn.Linear(2, 2, bias=False)) + + def init_weights(self, init_linear=None): + pass + + def forward(self, *args, **kwargs): + return {"loss": torch.Tensor(1)} + + def build_mock_backbone(*args, **kwargs): + return MockBackbone() + + def build_mock_neck(*args, **kwargs): + return MockNeck() + + def build_mock_head(*args, **kwargs): + return MockHead() + + monkeypatch.setattr( + "otx.algorithms.classification.adapters.mmcls.models.classifiers.byol.build_backbone", build_mock_backbone + ) + monkeypatch.setattr( + "otx.algorithms.classification.adapters.mmcls.models.classifiers.byol.build_neck", build_mock_neck + ) + monkeypatch.setattr( + "otx.algorithms.classification.adapters.mmcls.models.classifiers.byol.build_head", build_mock_head + ) + + self.byol = BYOL(backbone={}, neck={}, head={}) + + @e2e_pytest_unit + def test_init_weights(self) -> None: + """Test init_weights function.""" + for param_ol, param_tgt in zip(self.byol.online_backbone.parameters(), self.byol.target_backbone.parameters()): + assert torch.all(param_ol == param_tgt) + assert param_ol.requires_grad + assert not param_tgt.requires_grad + + for param_ol, param_tgt in zip( + self.byol.online_projector.parameters(), self.byol.target_projector.parameters() + ): + assert torch.all(param_ol == param_tgt) + assert param_ol.requires_grad + assert not param_tgt.requires_grad + + @e2e_pytest_unit + def test_momentum_update(self) -> None: + """Test _momentum_update function.""" + original_params = {"backbone": [], "projector": []} + for param_tgt in self.byol.target_backbone.parameters(): + param_tgt.data *= 2.0 + original_params["backbone"].append(deepcopy(param_tgt)) + + for param_tgt in self.byol.target_projector.parameters(): + param_tgt.data *= 2.0 + original_params["projector"].append(deepcopy(param_tgt)) + + self.byol.momentum_update() + + for param_ol, param_tgt, orig_tgt in zip( + self.byol.online_backbone.parameters(), self.byol.target_backbone.parameters(), original_params["backbone"] + ): + assert torch.all( + param_tgt.data == orig_tgt * self.byol.momentum + param_ol.data * (1.0 - self.byol.momentum) + ) + + for param_ol, param_tgt, orig_tgt in zip( + self.byol.online_projector.parameters(), + self.byol.target_projector.parameters(), + original_params["projector"], + ): + assert torch.all( + param_tgt.data == orig_tgt * self.byol.momentum + param_ol.data * (1.0 - self.byol.momentum) + ) + + @e2e_pytest_unit + def test_train_step(self) -> None: + """Test train_step function wraps forward and _parse_losses.""" + img1 = torch.randn((1, 3, 2, 2)) + img2 = torch.randn((1, 3, 2, 2)) + + outputs = self.byol.train_step(data=dict(img1=img1, img2=img2), optimizer=None) + + assert "loss" in outputs + assert "log_vars" in outputs + assert "num_samples" in outputs + + @e2e_pytest_unit + @pytest.mark.parametrize( + "orig_state_dict,prefix,expected", + [ + ({"online_backbone.layer": 1}, "", {"layer": 1}), + ({"backbone.layer": 1}, "", {"backbone.layer": 1}), + ({"backbone.layer": 1}, "backbone.", {"backbone.layer": 1}), + ], + ) + def test_state_dict_hook(self, orig_state_dict: Dict[str, Any], prefix: str, expected: Dict[str, Any]) -> None: + """Test state_dict_hook function.""" + new_state_dict = BYOL.state_dict_hook(module=self.byol, state_dict=orig_state_dict, prefix=prefix) + + assert new_state_dict == expected diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_custom_image_classifier.py b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_custom_image_classifier.py new file mode 100644 index 00000000000..54b69219a56 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_custom_image_classifier.py @@ -0,0 +1,224 @@ +""" Tests for CustomImageClassifier.""" +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os.path as osp +from copy import deepcopy +from typing import Any, Dict + +import pytest +import torch +import datumaro as dm + +from otx.algorithms.classification.adapters.mmcls.models.classifiers.custom_image_classifier import ( + ImageClassifier, + CustomImageClassifier, +) +from otx.api.entities.datasets import DatasetEntity +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class OTXMobileNetV3: + pass + + +class OTXEfficientNet: + pass + + +class OTXEfficientNetV2: + pass + + +class MockModule: + def __init__(self, name): + if name == "mobilenet": + self.backbone = OTXMobileNetV3() + self._state_dict = { + "classifier.4.weight": torch.rand(1, 1), + "classifier.4.bias": torch.rand(1), + "act.weight": torch.rand(1), + "someweights": torch.rand(2), + } + elif name == "effnetv2": + self.backbone = OTXEfficientNetV2() + self._state_dict = { + "model.classifier.weight": torch.rand(1, 1), + "model.weight": torch.rand(1), + } + elif name == "effnet": + self.backbone = OTXEfficientNet() + self._state_dict = { + "features.weight": torch.rand(1, 1), + "features.active.weight": torch.rand(1, 1), + "output.weight": torch.rand(1), + "output.asl.weight": torch.rand(1), + } + self.multilabel = False + self.hierarchical = False + self.is_export = False + + def state_dict(self): + return self._state_dict + + +class TestCustomImageClassifier: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + mocker.patch.object(ImageClassifier, "__init__", return_value=None) + CustomImageClassifier._register_state_dict_hook = mocker.MagicMock() + CustomImageClassifier._register_load_state_dict_pre_hook = mocker.MagicMock() + self.classifier = CustomImageClassifier() + + @e2e_pytest_unit + def test_forward_train(self, mocker): + img = torch.rand(1, 3, 224, 224) + gt_labels = torch.rand(1, 1) + self.classifier.extract_feat = mocker.MagicMock() + self.classifier.head = mocker.MagicMock() + self.classifier.augments = None + self.classifier.multilabel = False + self.classifier.hierarchical = False + losses = self.classifier.forward_train(img, gt_labels) + + assert losses is not None + + @pytest.mark.parametrize("name", ["mobilenet", "effnetv2", "effnet"]) + @e2e_pytest_unit + def test_load_state_dict_pre_hook(self, name): + self.module = MockModule(name) + state_dict = self.module.state_dict() + self.classifier.load_state_dict_pre_hook(self.module, state_dict, prefix="") + + for key in state_dict: + if name == "mobilenet": + if "classifier" in key: + assert ".3." in key + if "someweights" in key: + assert "backbone." in key + if "act" in key: + assert "head." in key + elif name == "effnetv2": + assert "classifier" not in key + if "model" in key: + assert "backbone" in key + else: + if "features" in key and "active" not in key: + assert "backbone" in key + elif "active" in key: + assert key == "features.active.weight" + else: + assert "head" in key or "fc" in key + + @pytest.mark.parametrize("name", ["mobilenet", "effnetv2", "effnet"]) + @e2e_pytest_unit + def test_state_dict_hook(self, name): + self.module = MockModule(name) + state_dict = self.module.state_dict() + state_dict_copy = deepcopy(state_dict) + self.classifier.load_state_dict_pre_hook(self.module, state_dict, prefix="") + # backward state dict + self.classifier.state_dict_hook(self.module, state_dict, prefix="") + + assert state_dict.keys() == state_dict_copy.keys() + + @pytest.mark.parametrize("name", ["mobilenet", "effnetv2", "effnet"]) + def test_load_state_dict_mixing_hook(self, name): + self.module = MockModule(name) + state_dict = self.module.state_dict() + chkpt_dict = deepcopy(state_dict) + model_classes = [0, 1, 2] + chkpt_classes = [0, 1] + self.classifier.load_state_dict_mixing_hook(self.module, model_classes, chkpt_classes, chkpt_dict, prefix="") + + assert chkpt_dict.keys() == state_dict.keys() + + +class TestLossDynamicsTrackingMixin: + TESTCASE = [ + ("CustomLinearClsHead", "CrossEntropyLoss"), + ("CustomNonLinearClsHead", "CrossEntropyLoss"), + ("CustomLinearClsHead", "IBLoss"), + ("CustomNonLinearClsHead", "IBLoss"), + ] + + @pytest.fixture() + def classifier(self, request, mocker, fxt_multi_class_cls_dataset_entity: DatasetEntity) -> CustomImageClassifier: + head_type, loss_type = request.param + n_data = len(fxt_multi_class_cls_dataset_entity) + labels = fxt_multi_class_cls_dataset_entity.get_labels() + num_classes = len(labels) + cfg = { + "backbone": None, + "head": { + "type": head_type, + "num_classes": num_classes, + "in_channels": 10, + "loss": { + "type": loss_type, + }, + }, + "multilabel": False, + "hierarchical": False, + } + if loss_type == "IBLoss": + cfg["head"]["loss"]["num_classes"] = num_classes + cfg["head"]["loss"]["start"] = 0 # Should be zero + + class MockBackbone(torch.nn.Module): + def forward(self, *args, **kwargs): + return torch.randn([n_data, 10]) + + mocker.patch("mmcls.models.classifiers.image.build_backbone", return_value=MockBackbone()) + classifier = CustomImageClassifier(track_loss_dynamics=True, **cfg) + classifier.loss_dyns_tracker.init_with_otx_dataset(fxt_multi_class_cls_dataset_entity) + return classifier + + @pytest.fixture() + def data(self, fxt_multi_class_cls_dataset_entity: DatasetEntity): + n_data = len(fxt_multi_class_cls_dataset_entity) + labels = fxt_multi_class_cls_dataset_entity.get_labels() + img = torch.rand(n_data, 3, 8, 8) + gt_label = torch.arange(0, len(labels), dtype=torch.long).reshape(-1, 1) + entity_ids = [item.id_ for item in fxt_multi_class_cls_dataset_entity] + label_ids = [ + label.id_ + for item in fxt_multi_class_cls_dataset_entity + for ann in item.get_annotations() + for label in ann.get_labels() + ] + + assert len(entity_ids) == len(label_ids) + + return { + "img": img, + "gt_label": gt_label, + "img_metas": [ + {"entity_id": entity_id, "label_id": label_id} for entity_id, label_id in zip(entity_ids, label_ids) + ], + } + + @torch.no_grad() + @pytest.mark.parametrize("classifier", TESTCASE, indirect=True, ids=lambda x: "-".join(x)) + def test_train_step(self, classifier: CustomImageClassifier, data: Dict[str, Any], tmp_dir_path: str): + outputs = classifier.train_step(data) + + assert "loss_dyns" in outputs + assert "entity_ids" in outputs + assert "label_ids" in outputs + + n_steps = 3 + for iter in range(n_steps): + classifier.loss_dyns_tracker.accumulate(outputs, iter) + + export_dir = osp.join(tmp_dir_path, "noisy_label_detection") + classifier.loss_dyns_tracker.export(export_dir) + + dataset = dm.Dataset.import_from(export_dir, format="datumaro") + + for item in dataset: + for ann in item.annotations: + for k, v in ann.attributes.items(): + assert k in {"iters", "loss_dynamics"} + assert len(v) == n_steps diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_semisl_classifier.py b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_semisl_classifier.py new file mode 100644 index 00000000000..ed0ba1c29a8 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_semisl_classifier.py @@ -0,0 +1,31 @@ +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.classifiers.semisl_classifier import ( + CustomImageClassifier, + SemiSLClassifier, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSemiSLClassifier: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + mocker.patch.object(CustomImageClassifier, "__init__", return_value=None) + self.semisl_classifier = SemiSLClassifier() + self.kwargs = dict() + + @e2e_pytest_unit + def test_forward_train(self, mocker): + img = torch.rand(1, 3, 224, 224) + self.kwargs["gt_label"] = torch.rand(1, 1) + self.kwargs["extra_0"] = {"img": torch.rand(1, 3, 224, 224), "img_strong": torch.rand(1, 3, 224, 224)} + self.semisl_classifier.extract_feat = mocker.MagicMock() + self.semisl_classifier.head = mocker.MagicMock() + losses = self.semisl_classifier.forward_train(img, **self.kwargs) + + assert losses is not None diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_semisl_mlc_classifier.py b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_semisl_mlc_classifier.py new file mode 100644 index 00000000000..7ed1e00c6f3 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_semisl_mlc_classifier.py @@ -0,0 +1,32 @@ +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.classifiers.semisl_multilabel_classifier import ( + CustomImageClassifier, + SemiSLMultilabelClassifier, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSemiSLMultilabelClassifier: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + mocker.patch.object(CustomImageClassifier, "__init__", return_value=None) + self.semisl_classifier = SemiSLMultilabelClassifier() + self.kwargs = dict() + + @e2e_pytest_unit + def test_forward_train(self, mocker): + img = torch.rand(1, 3, 224, 224) + self.kwargs["gt_label"] = torch.rand(1, 1) + self.kwargs["extra_0"] = {"img": torch.rand(1, 3, 224, 224), "img_strong": torch.rand(1, 3, 224, 224)} + self.kwargs["img_strong"] = torch.rand(1, 3, 224, 224) + self.semisl_classifier.extract_feat = mocker.MagicMock() + self.semisl_classifier.head = mocker.MagicMock() + losses = self.semisl_classifier.forward_train(img, **self.kwargs) + + assert losses is not None diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_supcon_classifier.py b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_supcon_classifier.py new file mode 100644 index 00000000000..cff88b03e24 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/classifiers/test_supcon_classifier.py @@ -0,0 +1,26 @@ +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.classifiers.supcon_classifier import ( + ImageClassifier, + SupConClassifier, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSupConClassifier: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + mocker.patch.object(ImageClassifier, "__init__", return_value=None) + self.kwargs = dict(multilabel=False, hierarchical=False) + self.supcon_classifier = SupConClassifier(backbone=None, **self.kwargs) + + @e2e_pytest_unit + def test_forward_train(self, mocker): + img = torch.rand(1, 3, 224, 224) + gt_label = torch.rand(1, 1) + self.supcon_classifier.extract_feat = mocker.MagicMock() + self.supcon_classifier.head = mocker.MagicMock() + losses = self.supcon_classifier.forward_train(img, gt_label, **self.kwargs) + + assert losses is not None diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/heads/__init__.py b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/__init__.py new file mode 100644 index 00000000000..a6fd382ff11 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.classification.adapters.mmcls.models.heads""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_contrastive_head.py b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_contrastive_head.py new file mode 100644 index 00000000000..5ef1f3faa71 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_contrastive_head.py @@ -0,0 +1,51 @@ +import pytest +import torch +import torch.nn as nn + +from otx.algorithms.classification.adapters.mmcls import ConstrastiveHead +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestConstrastiveHead: + @pytest.fixture(autouse=True) + def setup(self, monkeypatch) -> None: + class MockNeck(nn.Sequential): + def __init__(self): + super().__init__(nn.Linear(2, 2, bias=False)) + + def init_weights(self, init_linear=None): + for module in self.modules(): + if hasattr(module, "weight") and module.weight is not None: + nn.init.constant_(module.weight, 1) + + def build_mock_neck(*args, **kwargs): + return MockNeck() + + monkeypatch.setattr( + "otx.algorithms.classification.adapters.mmcls.models.heads.contrastive_head.build_neck", build_mock_neck + ) + + self.inputs = torch.Tensor([[1.0, 2.0], [3.0, 4.0]]) + self.targets = torch.Tensor([[2.0, 3.0], [4.0, 5.0]]) + + @e2e_pytest_unit + def test_forward(self) -> None: + """Test forward function.""" + contrastive_head = ConstrastiveHead(predictor={}) + contrastive_head.init_weights() + + result = contrastive_head(self.inputs, self.targets) + expected_result = {"loss": torch.tensor(0.0255)} + + assert torch.allclose(result["loss"], expected_result["loss"], rtol=1e-4, atol=1e-4) + + @e2e_pytest_unit + def test_forward_no_size_average(self) -> None: + """Test forward function without size averaging.""" + contrastive_head = ConstrastiveHead(predictor={}, size_average=False) + contrastive_head.init_weights() + + result = contrastive_head(self.inputs, self.targets) + expected_result = {"loss": torch.tensor(0.0511)} + + assert torch.allclose(result["loss"], expected_result["loss"], rtol=1e-4, atol=1e-4) diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_cls_head.py b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_cls_head.py new file mode 100644 index 00000000000..377966ad505 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_cls_head.py @@ -0,0 +1,58 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.heads.custom_cls_head import ( + CustomLinearClsHead, + CustomNonLinearClsHead, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestLinearClsHead: + @pytest.fixture(autouse=True) + def head_type(self) -> None: + return CustomLinearClsHead + + @pytest.fixture(autouse=True) + def setup(self, head_type) -> None: + self.num_classes = 2 + self.head_dim = 5 + self.default_head = head_type(self.num_classes, self.head_dim) + self.default_input = torch.ones((2, self.head_dim)) + self.default_gt = torch.zeros(2).long() + + @e2e_pytest_unit + def test_forward(self) -> None: + result = self.default_head.forward_train(self.default_input, self.default_gt) + assert "loss" in result + assert result["loss"] >= 0 + + @e2e_pytest_unit + def test_forward_accuracy(self, head_type) -> None: + head = head_type(self.num_classes, self.head_dim, cal_acc=True) + result = head.forward_train(self.default_input, self.default_gt) + assert "loss" in result + assert "accuracy" in result + assert "top-1" in result["accuracy"] + assert result["accuracy"]["top-1"] >= 0 + + @e2e_pytest_unit + def test_simple_test(self) -> None: + result = self.default_head.simple_test(self.default_input) + assert result[0].shape[0] == self.num_classes + + +class TestNonLinearClsHead(TestLinearClsHead): + @pytest.fixture(autouse=True) + def head_type(self) -> None: + return CustomNonLinearClsHead + + @e2e_pytest_unit + def test_dropout(self, head_type) -> None: + head = head_type(self.num_classes, self.head_dim, dropout=True) + head.init_weights() + assert len(head.classifier) > len(self.default_head.classifier) diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_hierarchical_cls_head.py b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_hierarchical_cls_head.py new file mode 100644 index 00000000000..8f8ec9b6550 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_hierarchical_cls_head.py @@ -0,0 +1,104 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.heads.custom_hierarchical_linear_cls_head import ( + CustomHierarchicalLinearClsHead, +) +from otx.algorithms.classification.adapters.mmcls.models.heads.custom_hierarchical_non_linear_cls_head import ( + CustomHierarchicalNonLinearClsHead, +) +from otx.algorithms.classification.adapters.mmcls.models.losses.asymmetric_loss_with_ignore import ( + AsymmetricLossWithIgnore, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomHierarchicalLinearClsHead: + @pytest.fixture(autouse=True) + def head_type(self) -> None: + return CustomHierarchicalLinearClsHead + + @pytest.fixture(autouse=True) + def setup(self, head_type) -> None: + self.num_classes = 6 + self.head_dim = 10 + self.cls_heads_info = { + "num_multiclass_heads": 3, + "num_multilabel_classes": 0, + "head_idx_to_logits_range": {"0": (0, 2), "1": (2, 4), "2": (4, 6)}, + "num_single_label_classes": 6, + "empty_multiclass_head_indices": [], + } + self.loss = dict(type="CrossEntropyLoss", use_sigmoid=False, reduction="mean", loss_weight=1.0) + self.multilabel_loss = dict(type=AsymmetricLossWithIgnore.__name__, reduction="sum") + self.default_head = head_type( + self.num_classes, + self.head_dim, + hierarchical_info=self.cls_heads_info, + loss=self.loss, + multilabel_loss=self.multilabel_loss, + ) + self.default_head.init_weights() + self.default_input = torch.ones((2, self.head_dim)) + self.default_gt = torch.zeros((2, 3)) + + @e2e_pytest_unit + def test_forward(self) -> None: + result = self.default_head.forward_train(self.default_input, self.default_gt) + assert "loss" in result + assert result["loss"] >= 0 and not torch.isnan(result["loss"]) + + empty_head_gt_full = torch.tensor([[-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]]) + result_include_empty_full = self.default_head.forward_train(self.default_input, empty_head_gt_full) + assert "loss" in result_include_empty_full + assert result_include_empty_full["loss"] >= 0 and not torch.isnan(result_include_empty_full["loss"]) + + empty_head_gt_partial = torch.tensor([[0.0, 0.0, 0.0], [-1.0, 0.0, 0.0]]) + result_include_empty_partial = self.default_head.forward_train(self.default_input, empty_head_gt_partial) + assert "loss" in result_include_empty_partial + assert result_include_empty_partial["loss"] >= 0 and not torch.isnan(result_include_empty_partial["loss"]) + + @e2e_pytest_unit + def test_simple_test(self) -> None: + result = self.default_head.simple_test(self.default_input) + assert result[0].shape[0] == self.num_classes + + @e2e_pytest_unit + def test_zero_classes(self, head_type) -> None: + self.cls_heads_info["num_multiclass_heads"] = 0 + self.cls_heads_info["num_multilabel_classes"] = 0 + with pytest.raises(ValueError): + head_type( + self.num_classes, + self.head_dim, + hierarchical_info=self.cls_heads_info, + loss=self.loss, + multilabel_loss=self.multilabel_loss, + ) + + @e2e_pytest_unit + def test_neg_classes(self, head_type) -> None: + with pytest.raises(ValueError): + head_type( + -1, + self.head_dim, + hierarchical_info=self.cls_heads_info, + loss=self.loss, + multilabel_loss=self.multilabel_loss, + ) + + +class TestCustomHierarchicalNonLinearClsHead(TestCustomHierarchicalLinearClsHead): + @pytest.fixture(autouse=True) + def head_type(self) -> None: + return CustomHierarchicalNonLinearClsHead + + @e2e_pytest_unit + def test_dropout(self, head_type) -> None: + head = head_type(self.num_classes, self.head_dim, dropout=True, hierarchical_info=self.cls_heads_info) + head.init_weights() + assert len(head.classifier) > len(self.default_head.classifier) diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_multilabel_cls_head.py b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_multilabel_cls_head.py new file mode 100644 index 00000000000..c67afc16840 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_multilabel_cls_head.py @@ -0,0 +1,67 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.heads.custom_multi_label_linear_cls_head import ( + CustomMultiLabelLinearClsHead, +) +from otx.algorithms.classification.adapters.mmcls.models.heads.custom_multi_label_non_linear_cls_head import ( + CustomMultiLabelNonLinearClsHead, +) +from otx.algorithms.classification.adapters.mmcls.models.losses.asymmetric_loss_with_ignore import ( + AsymmetricLossWithIgnore, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomMultiLabelLinearClsHead: + @pytest.fixture(autouse=True) + def head_type(self): + return CustomMultiLabelLinearClsHead + + @pytest.fixture(autouse=True) + def setup(self, head_type) -> None: + self.num_classes = 2 + self.head_dim = 5 + self.loss = dict(type=AsymmetricLossWithIgnore.__name__, reduction="sum") + self.default_head = head_type(self.num_classes, self.head_dim, loss=self.loss) + self.default_head.init_weights() + self.default_input = torch.ones((2, self.head_dim)) + self.default_gt = torch.zeros((2, self.num_classes)) + + @e2e_pytest_unit + def test_forward(self) -> None: + result = self.default_head.forward_train(self.default_input, self.default_gt) + assert "loss" in result + assert result["loss"] >= 0 + + @e2e_pytest_unit + def test_simple_test(self) -> None: + result = self.default_head.simple_test(self.default_input) + assert result[0].shape[0] == self.num_classes + + @e2e_pytest_unit + def test_angular(self, head_type) -> None: + head = head_type(self.num_classes, self.head_dim, loss=self.loss, normalized=True) + result = head.simple_test(self.default_input) + assert result[0].shape[0] == self.num_classes + + @e2e_pytest_unit + def test_neg_classes(self, head_type) -> None: + with pytest.raises(ValueError): + head_type(-1, self.head_dim, loss=self.loss, normalized=True) + + +class TestCustomMultiLabelNonLinearClsHead(TestCustomMultiLabelLinearClsHead): + @pytest.fixture(autouse=True) + def head_type(self): + return CustomMultiLabelNonLinearClsHead + + @e2e_pytest_unit + def test_dropout(self, head_type) -> None: + head = head_type(self.num_classes, self.head_dim, dropout=True) + head.init_weights() + assert len(head.classifier) > len(self.default_head.classifier) diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_multilabel_semisl.py b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_multilabel_semisl.py new file mode 100644 index 00000000000..d02d7d06ab9 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_multilabel_semisl.py @@ -0,0 +1,76 @@ +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.heads.semisl_multilabel_cls_head import ( + SemiLinearMultilabelClsHead, + SemiNonLinearMultilabelClsHead, +) +from otx.algorithms.classification.adapters.mmcls.models.losses.asymmetric_loss_with_ignore import ( + AsymmetricLossWithIgnore, +) +from otx.algorithms.classification.adapters.mmcls.models.losses.barlowtwins_loss import ( + BarlowTwinsLoss, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSemiLinearMultilabelClsHead: + @pytest.fixture(autouse=True) + def head_type(self): + return SemiLinearMultilabelClsHead + + @pytest.fixture(autouse=True) + def setup(self, head_type) -> None: + self.num_classes = 2 + self.head_dim = 5 + self.loss = dict(type=AsymmetricLossWithIgnore.__name__, reduction="sum") + self.aux_loss = dict(type=BarlowTwinsLoss.__name__, off_diag_penality=1.0 / 128.0, loss_weight=1.0) + self.default_head = head_type(self.num_classes, self.head_dim, loss=self.loss) + self.default_head.init_weights() + self.default_input = torch.ones((2, self.head_dim)) + self.default_ssl_input = { + "labeled_weak": self.default_input, + "labeled_strong": self.default_input, + "unlabeled_weak": self.default_input, + "unlabeled_strong": self.default_input, + } + self.default_gt = torch.zeros((2, self.num_classes)) + + @e2e_pytest_unit + def test_forward(self) -> None: + result = self.default_head.forward_train(self.default_ssl_input, self.default_gt) + assert "loss" in result + assert "unlabeled_loss" in result + assert result["loss"] >= 0 + + @e2e_pytest_unit + def test_simple_test(self) -> None: + result = self.default_head.simple_test(self.default_input) + assert result[0].shape[0] == self.num_classes + + @e2e_pytest_unit + def test_angular(self, head_type) -> None: + head = head_type(self.num_classes, self.head_dim, loss=self.loss, normalized=True) + result = head.simple_test(self.default_input) + assert result[0].shape[0] == self.num_classes + + @e2e_pytest_unit + def test_neg_classes(self, head_type) -> None: + with pytest.raises(ValueError): + head_type(-1, self.head_dim, loss=self.loss, normalized=True) + + +class TestSemiNonLinearMultilabelClsHead(TestSemiLinearMultilabelClsHead): + @pytest.fixture(autouse=True) + def head_type(self): + return SemiNonLinearMultilabelClsHead + + @e2e_pytest_unit + def test_dropout(self, head_type) -> None: + head = head_type(self.num_classes, self.head_dim, dropout=True) + head.init_weights() + assert len(head.classifier) > len(self.default_head.classifier) diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_semisl_cls_head.py b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_semisl_cls_head.py new file mode 100644 index 00000000000..4f0ddbd0a04 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_semisl_cls_head.py @@ -0,0 +1,119 @@ +import pytest +import torch +from mmcls.models.builder import build_head + +from otx.algorithms.classification.adapters.mmcls.models.heads.semisl_cls_head import ( + SemiLinearClsHead, + SemiNonLinearClsHead, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSemiSLClsHead: + @pytest.fixture(autouse=True) + def setUp(self): + """Semi-SL for Classification Head Settings.""" + self.in_channels = 1280 + self.num_classes = 10 + self.head_cfg = dict( + type="SemiLinearClsHead", + in_channels=self.in_channels, + num_classes=self.num_classes, + ) + + @e2e_pytest_unit + def test_build_semisl_cls_head(self): + """Verifies that SemiSLClsHead builds.""" + head = build_head(self.head_cfg) + assert isinstance(head, SemiLinearClsHead) + + head_cfg = dict( + type="SemiNonLinearClsHead", + in_channels=self.in_channels, + num_classes=self.num_classes, + ) + head = build_head(head_cfg) + assert isinstance(head, SemiNonLinearClsHead) + + @e2e_pytest_unit + def test_build_semisl_cls_head_type_error(self): + """Verifies that SemiSLClsHead parameters check with TypeError.""" + with pytest.raises(TypeError): + self.head_cfg["num_classes"] = [1] + build_head(self.head_cfg) + with pytest.raises(TypeError): + self.head_cfg["in_channels"] = [1] + build_head(self.head_cfg) + with pytest.raises(TypeError): + self.head_cfg["loss"] = [1] + build_head(self.head_cfg) + with pytest.raises(TypeError): + self.head_cfg["topk"] = [1] + build_head(self.head_cfg) + with pytest.raises(TypeError): + self.head_cfg["unlabeled_coef"] = [1] + build_head(self.head_cfg) + with pytest.raises(TypeError): + self.head_cfg["min_threshold"] = [1] + build_head(self.head_cfg) + + @e2e_pytest_unit + def test_build_semisl_cls_head_value_error(self): + """Verifies that SemiSLClsHead parameters check with ValueError.""" + with pytest.raises(ValueError): + self.head_cfg["num_classes"] = 0 + build_head(self.head_cfg) + with pytest.raises(ValueError): + self.head_cfg["num_classes"] = -1 + build_head(self.head_cfg) + with pytest.raises(ValueError): + self.head_cfg["in_channels"] = 0 + build_head(self.head_cfg) + with pytest.raises(ValueError): + self.head_cfg["in_channels"] = -1 + build_head(self.head_cfg) + + @e2e_pytest_unit + def test_forward(self, mocker): + """Verifies that SemiSLClsHead forward function works.""" + head = build_head(self.head_cfg) + labeled_batch_size = 16 + unlabeled_batch_size = 64 + + dummy_gt = torch.randint(self.num_classes, (labeled_batch_size,)) + labeled = torch.rand(labeled_batch_size, self.in_channels) + unlabeled_weak = torch.rand(unlabeled_batch_size, self.in_channels) + unlabeled_strong = torch.rand(unlabeled_batch_size, self.in_channels) + + mocker.patch("torch.cuda.is_available", return_value=False) + + dummy_data = { + "labeled": labeled, + "unlabeled_weak": unlabeled_weak, + "unlabeled_strong": unlabeled_strong, + } + head.classwise_acc = head.classwise_acc.cpu() + loss = head.forward_train(dummy_data, dummy_gt) + + assert isinstance(loss, dict) + assert len(loss) == 3 + assert isinstance(loss["accuracy"], dict) + assert len(loss["accuracy"]) == 2 + + # No Unlabeled Data + dummy_feature = torch.rand(labeled_batch_size, self.in_channels) + loss = head.forward_train(dummy_feature, dummy_gt) + + assert isinstance(loss, dict) + assert len(loss) == 3 + assert isinstance(loss["accuracy"], dict) + assert len(loss["accuracy"]) == 2 + + @e2e_pytest_unit + def test_simple_test(self): + """Verifies that SemiSLClsHead simple_test function works.""" + head = build_head(self.head_cfg) + dummy_feature = torch.rand(3, self.in_channels) + features = head.simple_test(dummy_feature) + assert len(features) == 3 + assert len(features[0]) == self.num_classes diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/heads/text_mixin.py b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/text_mixin.py new file mode 100644 index 00000000000..54d96b3d572 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/text_mixin.py @@ -0,0 +1,21 @@ +import pytest + +from otx.algorithms.classification.adapters.mmcls.models.heads.mixin import OTXHeadMixin +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestOTXHeadMixin: + @e2e_pytest_unit + @pytest.mark.parametrize( + "input,expected_output", + [ + ([1, 2, 3], 3), + ({"a": 1, "b": 2}, {"a": 1, "b": 2}), + (42, 42), + ("test_string", "test_string"), + ], + ) + def test_pre_logits(self, input, expected_output): + """Verifies pre-logits logic for list output from ViT backbones.""" + output = OTXHeadMixin.pre_logits(input) + assert output == expected_output, f"Expected {expected_output}, but got {output}" diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/losses/__init__.py b/tests/unit/algorithms/classification/adapters/mmcls/models/losses/__init__.py new file mode 100644 index 00000000000..d369804d764 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/losses/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.classification.adapters.mmcls.models.losses.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/losses/test_asymmetric_multilabel.py b/tests/unit/algorithms/classification/adapters/mmcls/models/losses/test_asymmetric_multilabel.py new file mode 100644 index 00000000000..5c9d6311a55 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/losses/test_asymmetric_multilabel.py @@ -0,0 +1,70 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.losses.asymmetric_angular_loss_with_ignore import ( + AsymmetricAngularLossWithIgnore, +) +from otx.algorithms.classification.adapters.mmcls.models.losses.asymmetric_loss_with_ignore import ( + AsymmetricLossWithIgnore, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestAsymmetricLoss: + @pytest.fixture(autouse=True) + def loss_type(self) -> None: + return AsymmetricLossWithIgnore + + @pytest.fixture(autouse=True) + def setup(self, loss_type) -> None: + self.num_classes = 2 + self.default_gt = torch.zeros((2, self.num_classes)) + self.default_input = torch.zeros((2, self.num_classes)) + self.default_input[0, 0] = 1 + self.default_gt[0, 0] = 1 + self.default_loss = loss_type(reduction="mean") + + @e2e_pytest_unit + def test_forward(self) -> None: + result_c = self.default_loss(self.default_input, self.default_gt) + self.default_input[0, 1] = 1 + result_w = self.default_loss(self.default_input, self.default_gt) + assert result_c < result_w + + @e2e_pytest_unit + def test_weight(self, loss_type) -> None: + result = self.default_loss(self.default_input, self.default_gt) + loss_w = loss_type(loss_weight=2, reduction="mean") + result_w = loss_w(self.default_input, self.default_gt) + assert result_w > result + + @e2e_pytest_unit + def test_reduction(self, loss_type) -> None: + result = self.default_loss(self.default_input, self.default_gt) + loss_s = loss_type(reduction="sum") + result_s = loss_s(self.default_input, self.default_gt) + assert result_s > result + + @e2e_pytest_unit + def test_gamma_neg(self, loss_type) -> None: + result = self.default_loss(self.default_input, self.default_gt) + loss_s = loss_type(gamma_neg=0.0, reduction="mean") + result_s = loss_s(self.default_input, self.default_gt) + assert result_s > result + + @e2e_pytest_unit + def test_gamma_pos(self, loss_type) -> None: + result = self.default_loss(self.default_input, self.default_gt) + loss_s = loss_type(gamma_pos=1.0, reduction="mean") + result_s = loss_s(self.default_input, self.default_gt) + assert result_s < result + + +class TestAsymmetricAngularLoss(TestAsymmetricLoss): + @pytest.fixture(autouse=True) + def loss_type(self) -> None: + return AsymmetricAngularLossWithIgnore diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/losses/test_cross_entropy.py b/tests/unit/algorithms/classification/adapters/mmcls/models/losses/test_cross_entropy.py new file mode 100644 index 00000000000..cadafc007f8 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/losses/test_cross_entropy.py @@ -0,0 +1,45 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.losses.cross_entropy_loss import ( + CrossEntropyLossWithIgnore, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCrossEntropyLossWithIgnore: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.num_classes = 2 + self.default_gt = torch.zeros(2).long() + self.default_input = torch.zeros((2, self.num_classes)) + self.default_input[0, 0] = 1 + self.default_loss = CrossEntropyLossWithIgnore() + + @e2e_pytest_unit + def test_forward(self): + result_c = self.default_loss(self.default_input, self.default_gt) + self.default_input[0, 1] = 1 + result_w = self.default_loss(self.default_input, self.default_gt) + assert result_c < result_w + + def test_weight(self): + result = self.default_loss(self.default_input, self.default_gt) + loss_w = CrossEntropyLossWithIgnore(loss_weight=2) + result_w = loss_w(self.default_input, self.default_gt) + assert result_w > result + + def test_ignore(self): + loss_i = CrossEntropyLossWithIgnore(ignore_index=0) + result_i = loss_i(self.default_input, self.default_gt) + assert result_i == 0 + + def test_reduction(self): + result = self.default_loss(self.default_input, self.default_gt) + loss_s = CrossEntropyLossWithIgnore(reduction="sum") + result_s = loss_s(self.default_input, self.default_gt) + assert result_s > result diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/necks/__init__.py b/tests/unit/algorithms/classification/adapters/mmcls/models/necks/__init__.py new file mode 100644 index 00000000000..7edb312512a --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/necks/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.classification.adapters.mmcls.models.necks""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/mmcls/models/necks/test_selfsl_mlp.py b/tests/unit/algorithms/classification/adapters/mmcls/models/necks/test_selfsl_mlp.py new file mode 100644 index 00000000000..f11abd37d0c --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/models/necks/test_selfsl_mlp.py @@ -0,0 +1,153 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict + +import pytest +import torch +import torch.nn as nn + +from otx.algorithms.classification.adapters.mmcls import SelfSLMLP +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSelfSLMLP: + @e2e_pytest_unit + @pytest.mark.parametrize("use_conv", [True, False]) + @pytest.mark.parametrize("with_avg_pool", [True, False]) + def test_init(self, use_conv: bool, with_avg_pool: bool) -> None: + """Test __init__ function.""" + selfslmlp = SelfSLMLP( + in_channels=2, hid_channels=2, out_channels=2, use_conv=use_conv, with_avg_pool=with_avg_pool + ) + + if with_avg_pool: + assert isinstance(selfslmlp.avgpool, nn.AdaptiveAvgPool2d) + + if use_conv: + assert isinstance(selfslmlp.mlp[0], nn.Conv2d) + assert isinstance(selfslmlp.mlp[3], nn.Conv2d) + else: + assert isinstance(selfslmlp.mlp[0], nn.Linear) + assert isinstance(selfslmlp.mlp[3], nn.Linear) + + @e2e_pytest_unit + @pytest.mark.parametrize("init_linear", ["normal", "kaiming"]) + @pytest.mark.parametrize("std", [0.01, 1.0, 10.0]) + @pytest.mark.parametrize("bias", [0.1, 0.0, 1.0]) + def test_init_weights(self, init_linear: str, std: float, bias: float) -> None: + """Test init_weights function. + + Check if weights of nn.Linear was changed except for biases. + BatchNorm weights are already set to 1, so it isn't required to be checked. + """ + + def gather_weight_mean_std(modules): + mean = [] + std = [] + for module in modules: + if isinstance(module, nn.Linear): + mean.append(module.weight.mean()) + std.append(module.weight.std()) + return mean, std + + selfslmlp = SelfSLMLP(in_channels=2, hid_channels=2, out_channels=2, use_conv=False, with_avg_pool=True) + + orig_mean, orig_std = gather_weight_mean_std(selfslmlp.modules()) + + selfslmlp.init_weights(init_linear, std, bias) + + updated_mean, updated_std = gather_weight_mean_std(selfslmlp.modules()) + + for origs, updateds in zip([orig_mean, orig_std], [updated_mean, updated_std]): + for orig, updated in zip(origs, updateds): + assert orig != updated + + @e2e_pytest_unit + @pytest.mark.parametrize("init_linear", ["undefined"]) + def test_init_weights_undefined_initialization(self, init_linear: str) -> None: + """Test init_weights function when undefined initialization is given.""" + selfslmlp = SelfSLMLP(in_channels=2, hid_channels=2, out_channels=2, use_conv=False, with_avg_pool=True) + + with pytest.raises(ValueError): + selfslmlp.init_weights(init_linear) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "inputs,norm_cfg,use_conv,with_avg_pool,expected", + [ + (torch.rand((2, 2)), dict(type="BN1d"), False, False, torch.Size([2, 2])), + (torch.rand((2, 2, 2, 2)), dict(type="BN1d"), False, True, torch.Size([2, 2])), + (torch.rand((2, 2, 2, 2)), dict(type="BN2d"), True, False, torch.Size([2, 2, 2, 2])), + (torch.rand((2, 2, 2, 2)), dict(type="BN2d"), True, True, torch.Size([2, 2, 1, 1])), + ], + ) + def test_forward_tensor( + self, inputs: torch.Tensor, norm_cfg: Dict, use_conv: bool, with_avg_pool: bool, expected: torch.Size + ) -> None: + """Test forward function for tensor.""" + selfslmlp = SelfSLMLP( + in_channels=2, + hid_channels=2, + out_channels=2, + norm_cfg=norm_cfg, + use_conv=use_conv, + with_avg_pool=with_avg_pool, + ) + + results = selfslmlp(inputs) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "inputs,norm_cfg,use_conv,with_avg_pool,expected", + [ + ([torch.rand((2, 2)), torch.rand((2, 2))], dict(type="BN1d"), False, False, torch.Size([2, 2])), + ([torch.rand((2, 2, 2, 2)), torch.rand((2, 2, 2, 2))], dict(type="BN1d"), False, True, torch.Size([2, 2])), + ( + [torch.rand((2, 2, 2, 2)), torch.rand((2, 2, 2, 2))], + dict(type="BN2d"), + True, + False, + torch.Size([2, 2, 2, 2]), + ), + ( + [torch.rand((2, 2, 2, 2)), torch.rand((2, 2, 2, 2))], + dict(type="BN2d"), + True, + True, + torch.Size([2, 2, 1, 1]), + ), + ], + ) + def test_forward_list_tuple( + self, inputs: torch.Tensor, norm_cfg: Dict, use_conv: bool, with_avg_pool: bool, expected: torch.Size + ) -> None: + """Test forward function for list or tuple.""" + selfslmlp = SelfSLMLP( + in_channels=2, + hid_channels=2, + out_channels=2, + norm_cfg=norm_cfg, + use_conv=use_conv, + with_avg_pool=with_avg_pool, + ) + + results = selfslmlp(inputs) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize("inputs", ["unsupported", 1]) + def test_forward_unsupported_format(self, inputs: str) -> None: + """Test forward function for unsupported format.""" + selfslmlp = SelfSLMLP( + in_channels=2, + hid_channels=2, + out_channels=2, + ) + + with pytest.raises(TypeError): + selfslmlp(inputs) diff --git a/tests/unit/algorithms/classification/adapters/mmcls/nncf/__init__.py b/tests/unit/algorithms/classification/adapters/mmcls/nncf/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/nncf/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_builder.py b/tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_builder.py new file mode 100644 index 00000000000..926d5de77a0 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_builder.py @@ -0,0 +1,58 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import tempfile + +import numpy as np +import torch +from nncf.torch.nncf_network import NNCFNetwork + +from otx.algorithms.classification.adapters.mmcls.nncf.builder import ( + build_nncf_classifier, +) +from otx.algorithms.common.adapters.nncf.compression import NNCFMetaState +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmcv.nncf.test_helpers import ( + create_config, + create_dataset, + create_model, +) + + +@e2e_pytest_unit +def test_build_nncf_classifier(): + mock_config = create_config(lib="mmcls") + model = create_model(lib="mmcls") + create_dataset(lib="mmcls") + + with tempfile.TemporaryDirectory() as tempdir: + model_path = os.path.join(tempdir, "model.bin") + state_to_build = model.state_dict() + torch.save(state_to_build, model_path) + mock_config.load_from = model_path + mock_config.nncf_config.log_dir = tempdir + ctrl, model = build_nncf_classifier(mock_config) + assert isinstance(model, NNCFNetwork) + assert len([hook for hook in mock_config.custom_hooks if hook.type == "CompressionHook"]) == 1 + mock_config.pop("custom_hooks") + + torch.save( + { + "meta": { + "nncf_enable_compression": True, + "nncf_meta": NNCFMetaState( + data_to_build=np.zeros((50, 50, 3)), + compression_ctrl=ctrl.get_compression_state(), + state_to_build=state_to_build, + ), + }, + "state_dict": model.state_dict(), + }, + model_path, + ) + mock_config.nncf_config.log_dir = tempdir + ctrl, model = build_nncf_classifier(mock_config, model_path) + assert isinstance(model, NNCFNetwork) + assert len([hook for hook in mock_config.custom_hooks if hook.type == "CompressionHook"]) == 1 diff --git a/tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_patches.py b/tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_patches.py new file mode 100644 index 00000000000..44a24a146b2 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_patches.py @@ -0,0 +1,14 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcls.models.classifiers.base import BaseClassifier + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_patches(): + import otx.algorithms.classification.adapters.mmcls.nncf.patches # noqa: F401 + + assert getattr(BaseClassifier, "nncf_trace_context", None) is not None diff --git a/tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_registers.py b/tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_registers.py new file mode 100644 index 00000000000..8fef30e3494 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/nncf/test_mmcls_nncf_registers.py @@ -0,0 +1,16 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import otx.algorithms.classification.adapters.mmcls.nncf.registers # noqa: F401 +from otx.algorithms.common.adapters.nncf.utils import is_nncf_enabled +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_registers(): + if is_nncf_enabled(): + from nncf.torch.layers import UNWRAPPED_USER_MODULES + from timm.models.layers.conv2d_same import Conv2dSame + + assert Conv2dSame in UNWRAPPED_USER_MODULES.registry_dict.values() diff --git a/tests/unit/algorithms/classification/adapters/mmcls/optimizer/__init__.py b/tests/unit/algorithms/classification/adapters/mmcls/optimizer/__init__.py new file mode 100644 index 00000000000..5df735fd824 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/optimizer/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.classification.adapters.mmcls.optimizer""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/mmcls/optimizer/test_lars.py b/tests/unit/algorithms/classification/adapters/mmcls/optimizer/test_lars.py new file mode 100644 index 00000000000..f82abf3b1ee --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/optimizer/test_lars.py @@ -0,0 +1,70 @@ +"""Test for otx.mpa.modules.optimizer.lars.""" +import pytest +import torch +import torch.nn as nn + +from otx.algorithms.classification.adapters.mmcls.optimizer.lars import LARS +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmcv.tasks.test_helpers import ( + generate_random_torch_image, + generate_toy_cnn_model, +) + + +class TestLARS: + def setup(self): + self.model = generate_toy_cnn_model() + self.model.train() + + self.loss = nn.MSELoss() + + @e2e_pytest_unit + def test_init(self): + negative_lr = -0.1 + with pytest.raises(ValueError, match="Invalid learning rate: {}".format(negative_lr)): + LARS(self.model.parameters(), lr=negative_lr) + + lr = 1e-1 + negative_momentum = -0.1 + with pytest.raises(ValueError, match="Invalid momentum value: {}".format(negative_momentum)): + LARS(self.model.parameters(), lr=lr, momentum=negative_momentum) + + negative_wd = -0.1 + with pytest.raises(ValueError, match="Invalid weight_decay value: {}".format(negative_wd)): + LARS(self.model.parameters(), lr=lr, weight_decay=negative_wd) + + negative_eta = -0.1 + with pytest.raises(ValueError, match="Invalid LARS coefficient value: {}".format(negative_eta)): + LARS(self.model.parameters(), lr=lr, eta=negative_eta) + + nesterov = True + with pytest.raises(ValueError, match="Nesterov momentum requires a momentum and zero dampening"): + LARS(self.model.parameters(), lr=lr, nesterov=nesterov) + + LARS(self.model.parameters(), lr=lr, exclude_bn_from_weight_decay=True) + + @e2e_pytest_unit + def test_step(self): + selfsl_optimizer = LARS(self.model.parameters(), lr=0.1, mode="selfsl", exclude_bn_from_weight_decay=True) + selfsl_optimizer.zero_grad() + input_img = generate_random_torch_image(1, 3, 3, 3) + input_img.requires_grad = True + target = torch.randn(1, 3) + + logit = self.model(input_img) + + loss = self.loss(logit.view(1, -1), target) + loss.backward() + selfsl_optimizer.step() + + optimizer = LARS(self.model.parameters(), lr=0.1, momentum=0.9, exclude_bn_from_weight_decay=True) + optimizer.zero_grad() + input_img = generate_random_torch_image(1, 3, 3, 3) + input_img.requires_grad = True + target = torch.randn(1, 3) + + logit = self.model(input_img) + + loss = self.loss(logit.view(1, -1), target) + loss.backward() + optimizer.step() diff --git a/tests/unit/algorithms/classification/adapters/mmcls/test_cls_config_builder.py b/tests/unit/algorithms/classification/adapters/mmcls/test_cls_config_builder.py new file mode 100644 index 00000000000..2c671f49176 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/test_cls_config_builder.py @@ -0,0 +1,39 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.algorithms.classification.adapters.mmcls.utils import patch_evaluation +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@pytest.fixture +def otx_default_cls_config(): + config_name = "src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/model.py" + conf = OTXConfig.fromfile(config_name) + conf.checkpoint_config = {} + conf.evaluation = {"metric": "None"} + conf.data = {} + return conf + + +@e2e_pytest_unit +def test_patch_evaluation(otx_default_cls_config) -> None: + """Test patch_evaluation function. + + + 1. Check eval metrics not empty + 2. Check the early stop metric is in eval metrics + 3. Check an exception is thrown for a frong task type + """ + tasks = ["multilabel", "hierarchical", "normal"] + for task in tasks: + patch_evaluation(otx_default_cls_config, task) + eval_metrics = otx_default_cls_config.evaluation.metric + assert len(eval_metrics) > 0 + assert otx_default_cls_config.early_stop_metric in eval_metrics + + with pytest.raises(NotImplementedError): + patch_evaluation(otx_default_cls_config, "None") diff --git a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py new file mode 100644 index 00000000000..ab22b3249b5 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py @@ -0,0 +1,337 @@ +import copy +import os +from otx.algorithms.common.utils.utils import is_xpu_available + +import pytest +import tempfile +from mmcv.runner import CheckpointLoader +from mmcv.utils import ConfigDict + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.algorithms.classification.adapters.mmcls import configurer +from otx.algorithms.classification.adapters.mmcls.configurer import ( + ClassificationConfigurer, + IncrClassificationConfigurer, + SemiSLClassificationConfigurer, +) +from otx.algorithms.common.configs.configuration_enums import InputSizePreset +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.classification.test_helper import DEFAULT_CLS_TEMPLATE_DIR + + +class TestClassificationConfigurer: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.configurer = ClassificationConfigurer( + "classification", + True, + False, + {}, + None, + None, + None, + ) + self.model_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "model.py")) + self.data_pipeline_path = os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "data_pipeline.py") + self.data_cfg = OTXConfig( + { + "data": { + "train": {"otx_dataset": [], "labels": []}, + "val": {"otx_dataset": [], "labels": []}, + "test": {"otx_dataset": [], "labels": []}, + } + } + ) + + self.multilabel_model_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "model_multilabel.py")) + self.hierarchical_model_cfg = OTXConfig.fromfile( + os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "model_hierarchical.py") + ) + + @e2e_pytest_unit + def test_configure(self, mocker): + mock_cfg_merge = mocker.patch.object(ClassificationConfigurer, "merge_configs") + mock_cfg_ckpt = mocker.patch.object(ClassificationConfigurer, "configure_ckpt") + mock_cfg_env = mocker.patch.object(ClassificationConfigurer, "configure_env") + mock_cfg_data_pipeline = mocker.patch.object(ClassificationConfigurer, "configure_data_pipeline") + mock_cfg_recipe = mocker.patch.object(ClassificationConfigurer, "configure_recipe") + mock_cfg_model = mocker.patch.object(ClassificationConfigurer, "configure_model") + mock_cfg_hook = mocker.patch.object(ClassificationConfigurer, "configure_hooks") + mock_cfg_compat_cfg = mocker.patch.object(ClassificationConfigurer, "configure_compat_cfg") + + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.model_task = "classification" + data_cfg = copy.deepcopy(self.data_cfg) + returned_value = self.configurer.configure(model_cfg, self.data_pipeline_path, None, "", data_cfg) + + mock_cfg_merge.assert_called_once_with(model_cfg, data_cfg, self.data_pipeline_path, None) + mock_cfg_ckpt.assert_called_once_with(model_cfg, "") + mock_cfg_env.assert_called_once_with(model_cfg) + mock_cfg_data_pipeline.assert_called_once_with(model_cfg, None, "") + mock_cfg_recipe.assert_called_once_with(model_cfg) + mock_cfg_model.assert_called_once_with(model_cfg, None, None, None) + mock_cfg_hook.assert_called_once_with(model_cfg) + mock_cfg_compat_cfg.assert_called_once_with(model_cfg) + assert returned_value == model_cfg + + @e2e_pytest_unit + def test_merge_configs(self, mocker): + mocker.patch("otx.algorithms.common.adapters.mmcv.configurer.patch_from_hyperparams", return_value=True) + self.configurer.merge_configs(self.model_cfg, self.data_cfg, self.data_pipeline_path, None) + assert self.model_cfg.data + assert self.model_cfg.data.train + assert self.model_cfg.data.val + + @e2e_pytest_unit + def test_configure_ckpt(self, mocker): + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.resume = True + + mocker.patch.object( + CheckpointLoader, + "load_checkpoint", + return_value={"model": None}, + ) + with tempfile.TemporaryDirectory() as tempdir: + self.configurer.configure_ckpt(model_cfg, os.path.join(tempdir, "dummy.pth")) + + @e2e_pytest_unit + def test_configure_env(self): + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + self.model_cfg.merge_from_dict(data_pipeline_cfg) + self.configurer.configure_env(self.model_cfg) + + @e2e_pytest_unit + def test_configure_device(self, mocker): + mocker.patch( + "torch.distributed.is_initialized", + return_value=True, + ) + mocker.patch( + "torch.distributed.get_world_size", + return_value=2, + ) + world_size = 2 + mocker.patch("os.environ", return_value={"LOCAL_RANK": 2}) + config = copy.deepcopy(self.model_cfg) + origin_lr = config.optimizer.lr + self.configurer.configure_device(config) + assert config.distributed is True + assert config.optimizer.lr == pytest.approx(origin_lr * world_size) + + mocker.patch( + "torch.distributed.is_initialized", + return_value=False, + ) + mocker.patch( + "torch.cuda.is_available", + return_value=False, + ) + config = copy.deepcopy(self.model_cfg) + self.configurer.configure_device(config) + assert config.distributed is False + assert config.device in ["cpu", "xpu"] + + mocker.patch( + "torch.distributed.is_initialized", + return_value=False, + ) + mocker.patch( + "torch.cuda.is_available", + return_value=True, + ) + config = copy.deepcopy(self.model_cfg) + self.configurer.configure_device(config) + assert config.distributed is False + assert config.device == "cuda" + + @e2e_pytest_unit + def test_configure_samples_per_gpu(self): + model_cfg = copy.deepcopy(self.model_cfg) + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + model_cfg.merge_from_dict(data_pipeline_cfg) + model_cfg.data.train_dataloader = ConfigDict({"samples_per_gpu": 2}) + model_cfg.data.train.otx_dataset = range(1) + self.configurer.configure_samples_per_gpu(model_cfg) + assert model_cfg.data.train_dataloader == {"samples_per_gpu": 1, "drop_last": True} + + @e2e_pytest_unit + @pytest.mark.parametrize("input_size", [None, (0, 0), (128, 128)]) + @pytest.mark.parametrize("training", [True, False]) + def test_configure_input_size(self, mocker, input_size, training): + # prepare + mock_cfg = mocker.MagicMock() + mock_input_manager_cls = mocker.patch.object(configurer, "InputSizeManager") + mock_input_manager = mock_input_manager_cls.return_value + mock_input_manager.get_trained_input_size.return_value = (32, 32) + mock_input_manager_cls.return_value = mock_input_manager + mock_base_configurer_cls = mocker.patch.object(configurer, "BaseConfigurer") + mock_base_configurer_cls.adapt_input_size_to_dataset.return_value = (64, 64) + + # execute + self.configurer.configure_input_size(mock_cfg, input_size, "ckpt/path", training=training) + + # check + if input_size is None: + mock_input_manager.set_input_size.assert_not_called() + elif input_size == (0, 0): + if training: + mock_input_manager.set_input_size.assert_called_once_with((64, 64)) + else: + mock_input_manager.set_input_size.assert_called_once_with((32, 32)) + else: + mock_input_manager.set_input_size.assert_called_once_with(input_size) + + @e2e_pytest_unit + def test_configure_fp16(self): + if is_xpu_available(): + pytest.skip("FP16 is not supported on XPU") + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.fp16 = {} + model_cfg.optimizer_config.type = "OptimizerHook" + self.configurer.configure_fp16(model_cfg) + assert model_cfg.optimizer_config.type == "Fp16OptimizerHook" + + model_cfg.fp16 = {} + model_cfg.optimizer_config.type = "SAMOptimizerHook" + self.configurer.configure_fp16(model_cfg) + assert model_cfg.optimizer_config.type == "Fp16SAMOptimizerHook" + + model_cfg.fp16 = {} + model_cfg.optimizer_config.type = "DummyOptimizerHook" + self.configurer.configure_fp16(model_cfg) + assert model_cfg.optimizer_config.type == "DummyOptimizerHook" + + @e2e_pytest_unit + def test_configure_model(self): + ir_options = {"ir_model_path": {"ir_weight_path": "", "ir_weight_init": ""}} + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + self.model_cfg.merge_from_dict(data_pipeline_cfg) + self.model_cfg.model.head.in_channels = -1 + self.model_cfg.merge_from_dict(self.data_cfg) + self.configurer.configure_model(self.model_cfg, [], [], ir_options) + assert self.model_cfg.model_task + assert self.model_cfg.model.head.in_channels == 960 + + multilabel_model_cfg = self.multilabel_model_cfg + multilabel_model_cfg.merge_from_dict(data_pipeline_cfg) + multilabel_model_cfg.merge_from_dict(self.data_cfg) + self.configurer.configure_model(multilabel_model_cfg, [], [], ir_options) + + h_label_model_cfg = self.hierarchical_model_cfg + h_label_model_cfg.merge_from_dict(data_pipeline_cfg) + h_label_model_cfg.merge_from_dict(self.data_cfg) + self.configurer.configure_model(h_label_model_cfg, [], [], ir_options) + + @e2e_pytest_unit + def test_configure_model_not_classification_task(self): + ir_options = {"ir_model_path": {"ir_weight_path": "", "ir_weight_init": ""}} + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + self.model_cfg.merge_from_dict(data_pipeline_cfg) + self.model_cfg.merge_from_dict(self.data_cfg) + configure_cfg = copy.deepcopy(self.model_cfg) + configure_cfg.model.task = "detection" + with pytest.raises(ValueError): + self.configurer.configure_model(configure_cfg, [], [], ir_options) + + @e2e_pytest_unit + def test_configure_task(self, mocker): + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.update(self.data_cfg) + model_cfg.task_adapt = {"type": "default_task_adapt", "op": "REPLACE", "use_adaptive_anchor": True} + self.configurer.configure_task(model_cfg) + + self.configurer.model_classes = [] + self.configurer.data_classes = ["red", "green"] + self.configurer.configure_task(model_cfg) + + @e2e_pytest_unit + def test_configure_hooks(self): + self.configurer.override_configs = {"custom_hooks": [{"type": "LazyEarlyStoppingHook", "patience": 6}]} + self.configurer.time_monitor = [] + self.configurer.configure_hooks(self.model_cfg) + assert self.model_cfg.custom_hooks[0]["patience"] == 6 + assert self.model_cfg.custom_hooks[-2]["type"] == "CancelInterfaceHook" + assert self.model_cfg.custom_hooks[-1]["type"] == "OTXProgressHook" + assert self.model_cfg.log_config.hooks[-1]["type"] == "OTXLoggerHook" + + @e2e_pytest_unit + def test_configure_compat_cfg(self): + model_cfg = copy.deepcopy(self.model_cfg) + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + model_cfg.merge_from_dict(data_pipeline_cfg) + model_cfg.update(self.data_cfg) + model_cfg.data.train_dataloader = {} + model_cfg.data.val_dataloader = {} + model_cfg.data.test_dataloader = {} + self.configurer.configure_compat_cfg(model_cfg) + + @e2e_pytest_unit + def test_get_subset_data_cfg(self): + config = copy.deepcopy(self.model_cfg) + config.update(self.data_cfg) + config.data.train.dataset = ConfigDict({"dataset": [1, 2, 3]}) + assert [1, 2, 3] == self.configurer.get_subset_data_cfg(config, "train") + + +class TestIncrClassificationConfigurer: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.configurer = IncrClassificationConfigurer( + "classification", + True, + False, + {}, + None, + None, + None, + ) + self.model_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "model.py")) + self.data_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "data_pipeline.py")) + + def test_configure_task(self, mocker): + mocker.patch.object(ClassificationConfigurer, "configure_task") + self.model_cfg.update(self.data_cfg) + self.model_cfg.task_adapt = {} + self.configurer.task_adapt_type = "default_task_adapt" + self.configurer.configure_task(self.model_cfg) + assert "TaskAdaptHook" in [i.type for i in self.model_cfg.custom_hooks] + + +class TestSemiSLClassificationConfigurer: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.configurer = SemiSLClassificationConfigurer( + "classification", + True, + False, + {}, + None, + None, + None, + ) + self.cfg = OTXConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "semisl", "model.py")) + data_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "semisl", "data_pipeline.py")) + self.cfg.merge_from_dict(data_cfg) + + @e2e_pytest_unit + def test_configure_data_pipeline(self, mocker): + mocker.patch("otx.algorithms.common.adapters.mmcv.semisl_mixin.build_dataset", return_value=True) + mocker.patch("otx.algorithms.common.adapters.mmcv.semisl_mixin.build_dataloader", return_value=True) + mocker.patch.object(ClassificationConfigurer, "configure_input_size", return_value=True) + + data_cfg = OTXConfig( + { + "data": { + "train": {"otx_dataset": [], "labels": []}, + "val": {"otx_dataset": [], "labels": []}, + "test": {"otx_dataset": [], "labels": []}, + "unlabeled": {"otx_dataset": [0, 1, 2, 3], "labels": []}, + } + } + ) + self.cfg.merge_from_dict(data_cfg) + self.cfg.model_task = "classification" + self.cfg.distributed = False + self.configurer.configure_data_pipeline(self.cfg, InputSizePreset.DEFAULT, "") + assert self.cfg.custom_hooks[-1]["type"] == "ComposedDataLoadersHook" diff --git a/tests/unit/algorithms/classification/adapters/mmcls/test_task.py b/tests/unit/algorithms/classification/adapters/mmcls/test_task.py new file mode 100644 index 00000000000..dda75f16d97 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/mmcls/test_task.py @@ -0,0 +1,530 @@ +"""Unit Test for otx.algorithms.detection.adapters.mmdet.task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +from contextlib import nullcontext +from typing import Any, Dict + +import numpy as np +from otx.algorithms.common.utils.utils import is_xpu_available +import pytest +import torch +from torch import nn + +from otx.algorithms.common.adapters.mmcv.utils import config_utils +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.algorithms.classification.adapters.mmcls.task import MMClassificationTask +from otx.algorithms.classification.adapters.mmcls.models.classifiers.custom_image_classifier import ( + CustomImageClassifier, +) +from otx.algorithms.classification.configs.base import ClassificationConfig +from otx.api.configuration.helper import create +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model import ( + ModelConfiguration, + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, +) +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.resultset import ResultSetEntity +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.classification.test_helper import ( + DEFAULT_CLS_TEMPLATE_DIR, + init_environment, + generate_label_schema, +) + + +class MockModule(nn.Module): + """Mock class for nn.Module.""" + + def forward(self, inputs: Any): + return inputs + + +class MockModel(nn.Module): + """Mock class for pytorch model.""" + + def __init__(self): + super().__init__() + self.module = MockModule() + self.module.backbone = MockModule() + self.backbone = MockModule() + + def forward(self, *args, **kwargs): + forward_hooks = list(self.module.backbone._forward_hooks.values()) + for hook in forward_hooks: + hook(1, 2, 3) + return np.array([[0.3, 0.7]]) + + @staticmethod + def named_parameters(*args, **kwargs): + return {"name": torch.Tensor([0.5])}.items() + + +class MockDataset(DatasetEntity): + """Mock class for mm_dataset.""" + + def __init__(self, dataset: DatasetEntity): + self.dataset = dataset + self.CLASSES = ["1", "2", "3"] + + def __len__(self): + return len(self.dataset) + + def evaluate(self, prediction, *args, **kwargs): + return {"mAP": 1.0} + + +class MockDataLoader: + """Mock class for data loader.""" + + def __init__(self, dataset: DatasetEntity): + self.dataset = dataset + self.iter = iter(self.dataset) + + def __len__(self) -> int: + return len(self.dataset) + + def __next__(self) -> Dict[str, DatasetItemEntity]: + return {"imgs": next(self.iter)} + + def __iter__(self): + return self + + +class MockExporter: + """Mock class for Exporter.""" + + def __init__(self, task): + self._output_path = task._output_path + + def run(self, cfg, *args, **kwargs): + assert cfg.model.head.num_classes == 2 + with open(os.path.join(self._output_path, "openvino.bin"), "wb") as f: + f.write(np.ndarray([0])) + with open(os.path.join(self._output_path, "openvino.xml"), "wb") as f: + f.write(np.ndarray([0])) + with open(os.path.join(self._output_path, "model.onnx"), "wb") as f: + f.write(np.ndarray([0])) + + return { + "outputs": { + "bin": os.path.join(self._output_path, "openvino.bin"), + "xml": os.path.join(self._output_path, "openvino.xml"), + "onnx": os.path.join(self._output_path, "model.onnx"), + } + } + + +class TestMMClassificationTask: + """Test class for MMClassificationTask. + + Details are explained in each test function. + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + model_template = parse_model_template(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.auto_num_workers = True + + mc_task_env, self.mc_cls_dataset = init_environment(hyper_parameters, model_template, False, False, 100) + self.mc_cls_task = MMClassificationTask(mc_task_env) + self.mc_cls_label_schema = generate_label_schema(self.mc_cls_dataset.get_labels(), False, False) + + ml_task_env, self.ml_cls_dataset = init_environment(hyper_parameters, model_template, True, False, 100) + self.ml_cls_task = MMClassificationTask(ml_task_env) + self.ml_cls_label_schema = generate_label_schema(self.ml_cls_dataset.get_labels(), True, False) + + hl_task_env, self.hl_cls_dataset = init_environment(hyper_parameters, model_template, False, True, 100) + self.hl_cls_task = MMClassificationTask(hl_task_env) + self.hl_cls_label_schema = generate_label_schema(self.hl_cls_dataset.get_labels(), False, False) + + @e2e_pytest_unit + def test_build_model(self, mocker) -> None: + """Test build_model function.""" + _mock_recipe_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "model.py")) + _mock_recipe_cfg.model.pop("task") + _mock_recipe_cfg["channel_last"] = False + model = self.mc_cls_task.build_model(_mock_recipe_cfg, True) + assert isinstance(model, CustomImageClassifier) + + _mock_recipe_cfg["channel_last"] = True + new_model = self.mc_cls_task.build_model(_mock_recipe_cfg, True) + dummy_model = model.backbone.features[0] + dummy_model_channel_last = new_model.backbone.features[0] + + dummy_tensor = torch.randn((1, 3, 224, 224)) + dummy_output = dummy_model(dummy_tensor) + dummy_output_channel_last = dummy_model_channel_last(dummy_tensor) + assert dummy_output.stride() != dummy_output_channel_last.stride() + + @e2e_pytest_unit + def test_train_multiclass(self, mocker) -> None: + """Test train function.""" + + def _mock_train_model(*args, **kwargs): + with open(os.path.join(self.mc_cls_task._output_path, "latest.pth"), "wb") as f: + torch.save({"dummy": torch.randn(1, 3, 3, 3)}, f) + + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataset", + return_value=MockDataset(self.mc_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataloader", + return_value=MockDataLoader(self.mc_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.train_model", + side_effect=_mock_train_model, + ) + mocker.patch.object(MMClassificationTask, "build_model", return_value=MockModel()) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_data_parallel", + return_value=MockModel(), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + # mock for testing num_workers + num_cpu = 20 + mock_multiprocessing = mocker.patch.object(config_utils, "multiprocessing") + mock_multiprocessing.cpu_count.return_value = num_cpu + num_gpu = 5 + mock_torch = mocker.patch.object(config_utils, "torch") + mock_torch.cuda.device_count.return_value = num_gpu + if is_xpu_available(): + mock_devcnt = mocker.patch.object(config_utils, "get_adaptive_num_workers") + mock_devcnt.return_value = num_cpu // num_gpu + + _config = ModelConfiguration(ClassificationConfig("header"), self.mc_cls_label_schema) + output_model = ModelEntity(self.mc_cls_dataset, _config) + self.mc_cls_task.train(self.mc_cls_dataset, output_model) + output_model.performance == 1.0 + assert ( + self.mc_cls_task._config.data.train_dataloader.workers_per_gpu == num_cpu // num_gpu + ) # test adaptive num_workers + + @e2e_pytest_unit + def test_train_multilabel(self, mocker) -> None: + """Test train function.""" + + def _mock_train_model(*args, **kwargs): + with open(os.path.join(self.ml_cls_task._output_path, "latest.pth"), "wb") as f: + torch.save({"dummy": torch.randn(1, 3, 3, 3)}, f) + + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataset", + return_value=MockDataset(self.ml_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataloader", + return_value=MockDataLoader(self.ml_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.train_model", + side_effect=_mock_train_model, + ) + mocker.patch.object(MMClassificationTask, "build_model", return_value=MockModel()) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_data_parallel", + return_value=MockModel(), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + _config = ModelConfiguration(ClassificationConfig("header"), self.ml_cls_label_schema) + output_model = ModelEntity(self.ml_cls_dataset, _config) + self.ml_cls_task.train(self.ml_cls_dataset, output_model) + output_model.performance == 1.0 + + @e2e_pytest_unit + def test_train_hierarchicallabel(self, mocker) -> None: + """Test train function.""" + + def _mock_train_model(*args, **kwargs): + with open(os.path.join(self.hl_cls_task._output_path, "latest.pth"), "wb") as f: + torch.save({"dummy": torch.randn(1, 3, 3, 3)}, f) + + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataset", + return_value=MockDataset(self.hl_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataloader", + return_value=MockDataLoader(self.hl_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.train_model", + side_effect=_mock_train_model, + ) + mocker.patch.object(MMClassificationTask, "build_model", return_value=MockModel()) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_data_parallel", + return_value=MockModel(), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + _config = ModelConfiguration(ClassificationConfig("header"), self.hl_cls_label_schema) + output_model = ModelEntity(self.hl_cls_dataset, _config) + self.hl_cls_task.train(self.hl_cls_dataset, output_model) + output_model.performance == 1.0 + + @e2e_pytest_unit + def test_infer_multiclass(self, mocker) -> None: + """Test infer function.""" + + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataset", + return_value=MockDataset(self.mc_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataloader", + return_value=MockDataLoader(self.mc_cls_dataset), + ) + mocker.patch.object(MMClassificationTask, "build_model", return_value=MockModel()) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_data_parallel", + return_value=MockModel(), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + inference_parameters = InferenceParameters(is_evaluation=True) + outputs = self.mc_cls_task.infer(self.mc_cls_dataset.with_empty_annotations(), inference_parameters) + for output in outputs: + assert output.get_annotations()[-1].get_labels()[0].probability == 0.7 + + @e2e_pytest_unit + def test_infer_multilabel(self, mocker) -> None: + """Test infer function.""" + + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataset", + return_value=MockDataset(self.ml_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataloader", + return_value=MockDataLoader(self.ml_cls_dataset), + ) + mocker.patch.object(MMClassificationTask, "build_model", return_value=MockModel()) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_data_parallel", + return_value=MockModel(), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + inference_parameters = InferenceParameters(is_evaluation=True) + outputs = self.ml_cls_task.infer(self.ml_cls_dataset.with_empty_annotations(), inference_parameters) + for output in outputs: + assert output.get_annotations()[-1].get_labels()[0].probability == 0.7 + + @e2e_pytest_unit + def test_infer_hierarchicallabel(self, mocker) -> None: + """Test infer function.""" + + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataset", + return_value=MockDataset(self.hl_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataloader", + return_value=MockDataLoader(self.hl_cls_dataset), + ) + mocker.patch.object(MMClassificationTask, "build_model", return_value=MockModel()) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_data_parallel", + return_value=MockModel(), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + inference_parameters = InferenceParameters(is_evaluation=True) + outputs = self.hl_cls_task.infer(self.hl_cls_dataset.with_empty_annotations(), inference_parameters) + for output in outputs: + assert output.get_annotations()[-1].get_labels()[0].probability == 0.0 + + @e2e_pytest_unit + def test_cls_evaluate(self) -> None: + """Test evaluate function for classification.""" + + _config = ModelConfiguration(ClassificationConfig("header"), self.mc_cls_label_schema) + _model = ModelEntity(self.mc_cls_dataset, _config) + resultset = ResultSetEntity(_model, self.mc_cls_dataset, self.mc_cls_dataset) + self.mc_cls_task.evaluate(resultset) + assert resultset.performance.score.value == 1.0 + + @e2e_pytest_unit + def test_cls_evaluate_with_empty_annotations(self) -> None: + """Test evaluate function for classification with empty predictions.""" + + _config = ModelConfiguration(ClassificationConfig("header"), self.mc_cls_label_schema) + _model = ModelEntity(self.mc_cls_dataset, _config) + resultset = ResultSetEntity(_model, self.mc_cls_dataset, self.mc_cls_dataset.with_empty_annotations()) + self.mc_cls_task.evaluate(resultset) + assert resultset.performance.score.value == 0.0 + + @pytest.mark.parametrize("precision", [ModelPrecision.FP16, ModelPrecision.FP32]) + @e2e_pytest_unit + def test_export(self, mocker, precision: ModelPrecision, export_type: ExportType = ExportType.OPENVINO) -> None: + """Test export function. + + + 1. Create model entity + 2. Run export function + 3. Check output model attributes + """ + _config = ModelConfiguration(ClassificationConfig("header"), self.mc_cls_label_schema) + _model = ModelEntity(self.mc_cls_dataset, _config) + + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.ClassificationExporter", + return_value=MockExporter(self.mc_cls_task), + ) + mocker.patch( + "otx.algorithms.classification.task.embed_ir_model_data", + return_value=True, + ) + + self.mc_cls_task.export(export_type, _model, precision, False) + + assert _model.model_format == ModelFormat.ONNX if export_type == ExportType.ONNX else ModelFormat.OPENVINO + assert _model.precision[0] == precision + assert _model.precision == self.mc_cls_task._precision + + if export_type == ExportType.OPENVINO: + assert _model.get_data("openvino.bin") is not None + assert _model.get_data("openvino.xml") is not None + assert _model.optimization_type == ModelOptimizationType.MO + else: + assert _model.get_data("model.onnx") is not None + assert _model.optimization_type == ModelOptimizationType.ONNX + + assert _model.optimization_methods == self.mc_cls_task._optimization_methods + assert _model.get_data("label_schema.json") is not None + + @e2e_pytest_unit + def test_export_onnx(self, mocker) -> None: + self.test_export(mocker, ModelPrecision.FP32, ExportType.ONNX) + + @e2e_pytest_unit + def test_explain(self, mocker): + """Test explain function.""" + explain_parameters = ExplainParameters( + explainer="ClassWiseSaliencyMap", + process_saliency_maps=False, + explain_predicted_classes=True, + ) + outputs = self.hl_cls_task.explain(self.hl_cls_dataset, explain_parameters) + assert isinstance(outputs, DatasetEntity) + assert len(outputs) == 500 + + @e2e_pytest_unit + def test_geti_scenario(self, mocker): + """Test Geti scenario. + + Train -> Eval -> Export + """ + + def _mock_train_model(*args, **kwargs): + with open(os.path.join(self.mc_cls_task._output_path, "latest.pth"), "wb") as f: + torch.save({"dummy": torch.randn(1, 3, 3, 3)}, f) + + # TRAIN + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataset", + return_value=MockDataset(self.mc_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataloader", + return_value=MockDataLoader(self.mc_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.train_model", + side_effect=_mock_train_model, + ) + mocker.patch.object(MMClassificationTask, "build_model", return_value=MockModel()) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_data_parallel", + return_value=MockModel(), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + # mock for testing num_workers + num_cpu = 20 + mock_multiprocessing = mocker.patch.object(config_utils, "multiprocessing") + mock_multiprocessing.cpu_count.return_value = num_cpu + if is_xpu_available(): + mock_devcnt = mocker.patch.object(config_utils, "get_adaptive_num_workers") + mock_devcnt.return_value = 1 + num_gpu = 5 + mock_torch = mocker.patch.object(config_utils, "torch") + mock_torch.cuda.device_count.return_value = num_gpu + + _config = ModelConfiguration(ClassificationConfig("header"), self.mc_cls_label_schema) + output_model = ModelEntity(self.mc_cls_dataset, _config) + self.mc_cls_task.train(self.mc_cls_dataset, output_model) + + # INFERENCE + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataset", + return_value=MockDataset(self.mc_cls_dataset), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_dataloader", + return_value=MockDataLoader(self.mc_cls_dataset), + ) + mocker.patch.object(MMClassificationTask, "build_model", return_value=MockModel()) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.build_data_parallel", + return_value=MockModel(), + ) + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + inference_parameters = InferenceParameters(is_evaluation=True) + outputs = self.mc_cls_task.infer(self.mc_cls_dataset.with_empty_annotations(), inference_parameters) + + # EXPORT + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.task.ClassificationExporter", + return_value=MockExporter(self.mc_cls_task), + ) + mocker.patch( + "otx.algorithms.classification.task.embed_ir_model_data", + return_value=True, + ) + + export_type = ExportType.OPENVINO + precision = ModelPrecision.FP32 + self.mc_cls_task.export(export_type, output_model, precision, False) diff --git a/tests/unit/algorithms/classification/adapters/openvino/__init__.py b/tests/unit/algorithms/classification/adapters/openvino/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/openvino/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/adapters/openvino/test_openvino_models.py b/tests/unit/algorithms/classification/adapters/openvino/test_openvino_models.py new file mode 100644 index 00000000000..13269e9ba0f --- /dev/null +++ b/tests/unit/algorithms/classification/adapters/openvino/test_openvino_models.py @@ -0,0 +1,6 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest diff --git a/tests/unit/algorithms/classification/conftest.py b/tests/unit/algorithms/classification/conftest.py new file mode 100644 index 00000000000..b50f8a1e18e --- /dev/null +++ b/tests/unit/algorithms/classification/conftest.py @@ -0,0 +1,36 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import numpy as np + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntityWithID +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle + + +@pytest.fixture() +def fxt_multi_class_cls_dataset_entity() -> DatasetEntity: + labels = ["car", "dog", "cat"] + items = [] + for label in labels: + image = Image(data=np.random.randint(low=0, high=255, size=(8, 8, 3))) + annotation = Annotation( + shape=Rectangle.generate_full_box(), + labels=[ScoredLabel(LabelEntity(name=label, domain=Domain.CLASSIFICATION, id=ID(label)))], + ) + annotation_scene = AnnotationSceneEntity(annotations=[annotation], kind=AnnotationSceneKind.ANNOTATION) + items += [DatasetItemEntityWithID(media=image, annotation_scene=annotation_scene, id_=ID(label))] + + dataset = DatasetEntity(items=items) + return dataset diff --git a/tests/unit/algorithms/classification/tasks/__init__.py b/tests/unit/algorithms/classification/tasks/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/classification/tasks/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/tasks/test_classification_nncf.py b/tests/unit/algorithms/classification/tasks/test_classification_nncf.py new file mode 100644 index 00000000000..2542c8ccbe2 --- /dev/null +++ b/tests/unit/algorithms/classification/tasks/test_classification_nncf.py @@ -0,0 +1,76 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from mmcv.utils import Config + +from otx.algorithms.classification.adapters.mmcls.nncf.task import ( + ClassificationNNCFTask, +) +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.helper import create +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.metrics import NullPerformance +from otx.api.entities.model import ModelConfiguration, ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.classification.test_helper import ( + DEFAULT_CLS_TEMPLATE, + init_environment, +) + + +@pytest.fixture +def otx_model(): + model_configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="header", description="description"), + label_schema=LabelSchemaEntity(), + ) + return ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + + +class TestOTXClsTaskNNCF: + @pytest.fixture(autouse=True) + def setup(self, otx_model, tmp_dir_path) -> None: + model_template = parse_model_template(DEFAULT_CLS_TEMPLATE) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.num_iters = 10 + self.task_env, self.dataset = init_environment(params=hyper_parameters, model_template=model_template) + self.model = otx_model + self.cls_nncf_task = ClassificationNNCFTask(self.task_env, output_path=str(tmp_dir_path)) + + @e2e_pytest_unit + def test_save_model(self, mocker): + mocker.patch("torch.load", return_value="") + self.cls_nncf_task._recipe_cfg = Config({"model": {}}) + self.cls_nncf_task.save_model(self.model) + + assert self.model.get_data("weights.pth") + assert self.model.get_data("label_schema.json") + + @e2e_pytest_unit + def test_optimize(self, mocker): + from otx.algorithms.common.adapters.mmcv.hooks import OTXLoggerHook + + # generate some dummy learning curves + mock_lcurve_val = OTXLoggerHook.Curve() + mock_lcurve_val.x = [0, 1] + mock_lcurve_val.y = [0.1, 0.2] + # patch training process + self.cls_nncf_task._learning_curves = {"val/accuracy_top-1": mock_lcurve_val} + mocker.patch.object(ClassificationNNCFTask, "save_model") + mocker.patch.object(ClassificationNNCFTask, "_train_model") + mocker.patch( + "otx.algorithms.classification.adapters.mmcls.nncf.task.build_nncf_classifier", + return_value=( + mocker.MagicMock(), + mocker.MagicMock(), + ), + ) + self.cls_nncf_task.optimize(OptimizationType.NNCF, self.dataset, self.model) + + assert self.model.performance != NullPerformance() + assert self.model.performance.score.value == 0.2 diff --git a/tests/unit/algorithms/classification/tasks/test_classification_openvino_task.py b/tests/unit/algorithms/classification/tasks/test_classification_openvino_task.py new file mode 100644 index 00000000000..ae1dc20f370 --- /dev/null +++ b/tests/unit/algorithms/classification/tasks/test_classification_openvino_task.py @@ -0,0 +1,242 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import pathlib + +import numpy as np +import pytest +from openvino.model_api.models import Model + +import otx.algorithms.classification.adapters.openvino.task +from openvino.model_api.models.utils import ClassificationResult +from otx.algorithms.classification.adapters.openvino.task import ( + ClassificationOpenVINOInferencer, + ClassificationOpenVINOTask, +) +from otx.algorithms.classification.configs.base import ClassificationConfig +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.metrics import Performance, ScoreMetric +from otx.api.entities.model import ModelConfiguration, ModelEntity +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.api.utils.shape_factory import ShapeFactory +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.classification.test_helper import ( + DEFAULT_CLS_TEMPLATE, + init_environment, + setup_configurable_parameters, +) + + +@pytest.fixture +def otx_model(): + model_configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="header", description="description"), + label_schema=LabelSchemaEntity(), + ) + return ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + + +class TestOpenVINOClassificationInferencer: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + hyper_parameters, model_template = setup_configurable_parameters(DEFAULT_CLS_TEMPLATE) + cls_params = ClassificationConfig(header=hyper_parameters.header) + environment, dataset = init_environment(hyper_parameters, model_template) + self.label_schema = environment.label_schema + mocker.patch("otx.algorithms.classification.adapters.openvino.task.OpenvinoAdapter") + mocker.patch.object(Model, "create_model") + self.cls_ov_inferencer = ClassificationOpenVINOInferencer(cls_params, self.label_schema, "") + model_path = "openvino.model_api.models.classification.ClassificationModel" + self.cls_ov_inferencer.model = mocker.patch( + model_path, autospec=True, return_value=ClassificationResult([], np.array(0), np.array(0), np.array(0)) + ) + self.fake_input = np.random.rand(3, 224, 224) + + @e2e_pytest_unit + def test_predict(self, mocker): + fake_output = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=[]) + returned_value = self.cls_ov_inferencer.predict(self.fake_input) + + assert returned_value[0] == ClassificationResult([], np.array(0), np.array(0), np.array(0)) + + +class TestOpenVINOClassificationTask: + @pytest.fixture(autouse=True) + def setup(self, mocker, otx_model) -> None: + hyper_parameters, model_template = setup_configurable_parameters(DEFAULT_CLS_TEMPLATE) + cls_params = ClassificationConfig(header=hyper_parameters.header) + self.task_env, self.dataset = init_environment(params=hyper_parameters, model_template=model_template) + self.label_schema = self.task_env.label_schema + mocker.patch("otx.algorithms.classification.adapters.openvino.task.OpenvinoAdapter") + mocker.patch.object(Model, "create_model") + cls_ov_inferencer = ClassificationOpenVINOInferencer(cls_params, self.label_schema, "") + self.task_env.model = otx_model + mocker.patch.object(ClassificationOpenVINOTask, "load_inferencer", return_value=cls_ov_inferencer) + self.cls_ov_task = ClassificationOpenVINOTask(self.task_env) + self.labels = self.label_schema.get_labels(include_empty=True) + fake_annotation = [ + Annotation( + Rectangle.generate_full_box(), + id=0, + labels=[ScoredLabel(label, probability=1.0) for label in self.labels], + ) + ] + self.fake_ann_scene = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=fake_annotation) + self.fake_input = mocker.MagicMock() + self.fake_hierarchical_info = { + "cls_heads_info": { + "num_multiclass_heads": 2, + "head_idx_to_logits_range": {"0": (0, 1), "1": (1, 2)}, + "all_groups": [["a"], ["b"]], + "label_to_idx": {"a": 0, "b": 1}, + "num_multilabel_classes": 0, + } + } + + @e2e_pytest_unit + def test_infer(self, mocker): + mock_predict = mocker.patch.object( + ClassificationOpenVINOInferencer, + "predict", + return_value=(ClassificationResult([], np.array(0), np.array(0), np.array(0)), self.fake_ann_scene), + ) + mocker.patch.object(ShapeFactory, "shape_produces_valid_crop", return_value=True) + updated_dataset = self.cls_ov_task.infer( + self.dataset, InferenceParameters(enable_async_inference=False, is_evaluation=True) + ) + + mock_predict.assert_called() + for updated in updated_dataset: + assert updated.annotation_scene.contains_any(self.labels) + + @e2e_pytest_unit + def test_infer_w_features_hierarhicallabel(self, mocker): + mock_predict = mocker.patch.object( + ClassificationOpenVINOInferencer, + "predict", + return_value=( + ClassificationResult([], np.empty((2, 2, 2)), np.array([0, 1]), np.array([0, 1])), + self.fake_ann_scene, + ), + ) + mocker.patch.object(ShapeFactory, "shape_produces_valid_crop", return_value=True) + self.cls_ov_task.inferencer.model.hierarchical_info = self.fake_hierarchical_info + updated_dataset = self.cls_ov_task.infer( + self.dataset, InferenceParameters(enable_async_inference=False, is_evaluation=False) + ) + mock_predict.assert_called() + for updated in updated_dataset: + assert updated.annotation_scene.contains_any(self.labels) + + @e2e_pytest_unit + def test_infer_async(self, mocker): + mocker.patch.object(ShapeFactory, "shape_produces_valid_crop", return_value=True) + + def fake_enqueue_prediciton(obj, x, idx, result_handler): + result_handler(idx, self.fake_ann_scene, (x, x, x)) + + mock_enqueue = mocker.patch.object( + ClassificationOpenVINOInferencer, "enqueue_prediction", fake_enqueue_prediciton + ) + + updated_dataset = self.cls_ov_task.infer( + self.dataset, InferenceParameters(enable_async_inference=True, is_evaluation=True) + ) + + for updated in updated_dataset: + assert updated.annotation_scene.contains_any(self.labels) + + @e2e_pytest_unit + def test_explain(self, mocker): + self.fake_silency_map = np.random.randint(255, size=(2, 224, 224), dtype=np.uint8) + mocker.patch.object( + ClassificationOpenVINOInferencer, + "predict", + return_value=( + ClassificationResult([], self.fake_silency_map, np.array([0, 1]), np.array([0, 1])), + self.fake_ann_scene, + ), + ) + self.cls_ov_task.inferencer.model.hierarchical = False + updpated_dataset = self.cls_ov_task.explain(self.dataset) + + assert updpated_dataset is not None + assert updpated_dataset.get_labels() == self.dataset.get_labels() + + @e2e_pytest_unit + def test_explain_hierarhicallabel(self, mocker): + mocker.patch.object( + ClassificationOpenVINOInferencer, + "predict", + return_value=( + ClassificationResult([], np.empty((2, 2, 2)), np.array([0, 1]), np.array([0, 1])), + self.fake_ann_scene, + ), + ) + self.cls_ov_task.inferencer.model.hierarchical_info = self.fake_hierarchical_info + updpated_dataset = self.cls_ov_task.explain(self.dataset) + + assert updpated_dataset is not None + assert updpated_dataset.get_labels() == self.dataset.get_labels() + + @e2e_pytest_unit + def test_evaluate(self, mocker): + result_set = ResultSetEntity( + model=None, + ground_truth_dataset=DatasetEntity(), + prediction_dataset=DatasetEntity(), + ) + fake_metrics = mocker.patch("otx.api.usecases.evaluation.accuracy.Accuracy", autospec=True) + fake_metrics.get_performance.return_value = Performance( + score=ScoreMetric(name="fake", value=0.1), dashboard_metrics="Accuracy" + ) + mocker.patch.object(MetricsHelper, "compute_accuracy", return_value=fake_metrics) + self.cls_ov_task.evaluate(result_set) + + assert result_set.performance.score.value == 0.1 + + @e2e_pytest_unit + def test_deploy(self, otx_model): + output_model = copy.deepcopy(otx_model) + self.cls_ov_task.model.set_data("openvino.bin", b"foo") + self.cls_ov_task.model.set_data("openvino.xml", b"bar") + self.cls_ov_task.deploy(output_model) + + assert output_model.exportable_code is not None + + @e2e_pytest_unit + def test_optimize(self, mocker, otx_model): + def patch_save_model(model, output_xml): + with open(output_xml, "wb") as f: + f.write(b"foo") + bin_path = pathlib.Path(output_xml).parent / pathlib.Path(str(pathlib.Path(output_xml).stem) + ".bin") + with open(bin_path, "wb") as f: + f.write(b"bar") + + output_model = copy.deepcopy(otx_model) + self.cls_ov_task.model.set_data("openvino.bin", b"foo") + self.cls_ov_task.model.set_data("openvino.xml", b"bar") + mocker.patch("otx.algorithms.classification.adapters.openvino.task.ov.Core.read_model", autospec=True) + mocker.patch("otx.algorithms.classification.adapters.openvino.task.ov.save_model", new=patch_save_model) + fake_quantize = mocker.patch( + "otx.algorithms.classification.adapters.openvino.task.nncf.quantize", autospec=True + ) + self.cls_ov_task.optimize(OptimizationType.POT, dataset=self.dataset, output_model=output_model) + + fake_quantize.assert_called_once() + assert self.cls_ov_task.model.get_data("openvino.bin") + assert self.cls_ov_task.model.get_data("openvino.xml") diff --git a/tests/unit/algorithms/classification/test_helper.py b/tests/unit/algorithms/classification/test_helper.py new file mode 100644 index 00000000000..27740753183 --- /dev/null +++ b/tests/unit/algorithms/classification/test_helper.py @@ -0,0 +1,188 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import random +from pathlib import Path + +import cv2 as cv +import numpy as np + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.api.configuration.helper import create +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment + +DEFAULT_CLS_TEMPLATE_DIR = ( + Path("src") / "otx" / "algorithms" / "classification" / "configs" / "mobilenet_v3_large_1_cls_incr" +) +DEFAULT_CLS_TEMPLATE = DEFAULT_CLS_TEMPLATE_DIR / "template.yaml" + + +def generate_label_schema(not_empty_labels, multilabel=False, hierarchical=False): + assert len(not_empty_labels) > 1 + + label_schema = LabelSchemaEntity() + if multilabel: + emptylabel = LabelEntity(name="Empty label", is_empty=True, domain=Domain.CLASSIFICATION) + empty_group = LabelGroup(name="empty", labels=[emptylabel], group_type=LabelGroupType.EMPTY_LABEL) + for label in not_empty_labels: + label_schema.add_group( + LabelGroup( + name="___" + label.name, + labels=[label], + group_type=LabelGroupType.EXCLUSIVE, + ) + ) + label_schema.add_group(empty_group) + elif hierarchical: + single_label_classes = ["b", "g", "r"] + multi_label_classes = ["w", "p"] + emptylabel = LabelEntity(name="Empty label", is_empty=True, domain=Domain.CLASSIFICATION) + empty_group = LabelGroup(name="empty", labels=[emptylabel], group_type=LabelGroupType.EMPTY_LABEL) + single_labels = [] + for label in not_empty_labels: + if label.name in multi_label_classes: + label_schema.add_group( + LabelGroup( + name=label.name, + labels=[label], + group_type=LabelGroupType.EXCLUSIVE, + ) + ) + if empty_group not in label_schema.get_groups(include_empty=True): + label_schema.add_group(empty_group) + elif label.name in single_label_classes: + single_labels.append(label) + if single_labels: + single_label_group = LabelGroup( + name="labels", + labels=single_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + label_schema.add_group(single_label_group) + else: + main_group = LabelGroup( + name="labels", + labels=not_empty_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + label_schema.add_group(main_group) + return label_schema + + +def generate_cls_dataset(hierarchical=False, number_of_images=10): + resolution = (224, 224) + if hierarchical: + colors = [(0, 255, 0), (0, 0, 255), (255, 0, 0), (0, 0, 0), (230, 230, 250)] + cls_names = ["b", "g", "r", "w", "p"] + texts = ["Blue", "Green", "Red", "White", "Purple"] + else: + colors = [(0, 255, 0), (0, 0, 255)] + cls_names = ["b", "g"] + texts = ["Blue", "Green"] + env_labels = [ + LabelEntity(name=name, domain=Domain.CLASSIFICATION, is_empty=False, id=ID(i)) + for i, name in enumerate(cls_names) + ] + + items = [] + + for _ in range(0, number_of_images): + for j, lbl in enumerate(env_labels): + class_img = np.zeros((*resolution, 3), dtype=np.uint8) + class_img[:] = colors[j] + class_img = cv.putText( + class_img, + texts[j], + (50, 50), + cv.FONT_HERSHEY_SIMPLEX, + 0.8 + j * 0.2, + colors[j - 1], + 2, + cv.LINE_AA, + ) + + image = Image(data=class_img) + labels = [ScoredLabel(label=lbl, probability=1.0)] + shapes = [Annotation(Rectangle.generate_full_box(), labels)] + annotation_scene = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=shapes) + items.append(DatasetItemEntity(media=image, annotation_scene=annotation_scene)) + + rng = random.Random() + rng.seed(100) + rng.shuffle(items) + for i, _ in enumerate(items): + subset_region = i / number_of_images + if subset_region >= 0.9: + subset = Subset.TESTING + elif subset_region >= 0.6: + subset = Subset.VALIDATION + else: + subset = Subset.TRAINING + items[i].subset = subset + + dataset = DatasetEntity(items) + return dataset + + +def init_environment(params, model_template, multilabel=False, hierarchical=False, number_of_images=10): + dataset = generate_cls_dataset(hierarchical, number_of_images) + labels_schema = generate_label_schema(dataset.get_labels(), multilabel=multilabel, hierarchical=hierarchical) + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + return environment, dataset + + +def setup_configurable_parameters(template_dir, num_iters=10): + model_template = parse_model_template(str(template_dir)) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.num_iters = num_iters + + return hyper_parameters, model_template + + +def setup_mpa_task_parameters(task_type, create_val=False, create_test=False): + if task_type == "semisl": + recipie_path = "src/otx/recipes/stages/classification/semisl.yaml" + elif task_type == "incremental": + recipie_path = "src/otx/recipes/stages/classification/incremental.yaml" + recipie_cfg = OTXConfig.fromfile(recipie_path) + model_cfg = OTXConfig.fromfile(DEFAULT_CLS_TEMPLATE_DIR / "model.py") + model_cfg.model.multilabel = False + model_cfg.model.hierarchical = False + data_cfg = OTXConfig.fromfile(DEFAULT_CLS_TEMPLATE_DIR / "data_pipeline.py") + data_cfg.data.train.data_dir = "tests/assets/classification_dataset" + if create_val: + data_cfg.data.val.data_dir = "tests/assets/classification_dataset" + else: + data_cfg.data.val = None + if create_test: + data_cfg.data.test.data_dir = "tests/assets/classification_dataset" + else: + data_cfg.data.test = None + dummy_dataset = generate_cls_dataset(number_of_images=1) + data_cfg.data.train.otx_dataset = dummy_dataset + data_cfg.data.train.labels = dummy_dataset.get_labels() + data_cfg.data.train.data_classes = ["label_0", "label_1"] + data_cfg.data.train.new_classes = ["label_0", "label_1", "label_3"] + + return model_cfg, data_cfg, recipie_cfg diff --git a/tests/unit/algorithms/classification/test_xai_classification_validity.py b/tests/unit/algorithms/classification/test_xai_classification_validity.py new file mode 100644 index 00000000000..053d90d8e7e --- /dev/null +++ b/tests/unit/algorithms/classification/test_xai_classification_validity.py @@ -0,0 +1,130 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +import numpy as np +import pytest +import torch +from torch.nn import LayerNorm +from mmcls.models import build_classifier + +from otx.algorithms.classification.adapters.mmcls.configurer import ClassificationConfigurer +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + ReciproCAMHook, + ViTReciproCAMHook, +) +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.cli.registry import Registry +from otx.algorithms.classification.adapters.mmcls.models.classifiers.custom_image_classifier import _extract_vit_feat +from tests.test_suite.e2e_test_system import e2e_pytest_unit + +templates_cls = Registry("src/otx/algorithms").filter(task_type="CLASSIFICATION").templates +templates_cls_ids = [template.model_template_id for template in templates_cls] + + +class TestExplainMethods: + ref_saliency_vals_cls = { + "EfficientNet-B0": np.array([57, 0, 161, 127, 102, 96, 92], dtype=np.uint8), + "MobileNet-V3-large-1x": np.array([140, 82, 87, 81, 79, 117, 254], dtype=np.uint8), + "EfficientNet-V2-S": np.array([125, 42, 24, 21, 27, 55, 145], dtype=np.uint8), + "DeiT-Tiny": np.array([0, 108, 108, 108, 108, 108, 108, 108, 108, 109, 109, 109, 109, 0], dtype=np.uint8), + } + + @e2e_pytest_unit + @pytest.mark.parametrize("template", templates_cls, ids=templates_cls_ids) + def test_saliency_map_cls(self, template): + torch.manual_seed(0) + base_dir = os.path.abspath(os.path.dirname(template.model_template_path)) + cfg_path = os.path.join(base_dir, "model.py") + cfg = OTXConfig.fromfile(cfg_path) + + cfg.model.pop("task") + ClassificationConfigurer.configure_in_channel(cfg) + model = build_classifier(cfg.model) + model = model.eval() + + img = torch.ones(2, 3, 224, 224) - 0.5 + data = {"img_metas": {}, "img": img} + + if template.name == "DeiT-Tiny": + explainer_hook = ViTReciproCAMHook + saliency_map_ref_shape = (1000, 14, 14) + else: + explainer_hook = ReciproCAMHook + saliency_map_ref_shape = (1000, 7, 7) + + with explainer_hook(model) as rcam_hook: + with torch.no_grad(): + _ = model(return_loss=False, **data) + saliency_maps = rcam_hook.records + + assert len(saliency_maps) == 2 + assert saliency_maps[0].ndim == 3 + assert saliency_maps[0].shape == saliency_map_ref_shape + # convert to int16 in case of negative value difference + actual_sal_vals = saliency_maps[0][0][0].astype(np.int16) + ref_sal_vals = self.ref_saliency_vals_cls[template.name].astype(np.uint8) + assert np.all(np.abs(actual_sal_vals - ref_sal_vals) <= 1) + + +class TestViTExplain: + DEIT_TEMPLATE_DIR = os.path.join("src/otx/algorithms/classification/configs", "deit_tiny") + + def _create_model(self): + torch.manual_seed(0) + base_dir = os.path.abspath(self.DEIT_TEMPLATE_DIR) + cfg_path = os.path.join(base_dir, "model.py") + cfg = OTXConfig.fromfile(cfg_path) + + cfg.model.pop("task") + ClassificationConfigurer.configure_in_channel(cfg) + model = build_classifier(cfg.model) + model = model.eval() + return model + + @e2e_pytest_unit + def test_extract_vit_feat(self): + model = self._create_model() + + img = torch.ones(2, 3, 224, 224) - 0.5 + feat, layernorm_feat = _extract_vit_feat(model, img) + + assert len(feat) == 2 + assert feat[0].shape == torch.Size([2, 192, 14, 14]) + assert feat[1].shape == torch.Size([2, 192]) + assert abs(feat[0][0][0][0][0].detach().cpu().item() - 0.4621) < 0.05 + assert layernorm_feat.shape == torch.Size([2, 197, 192]) + assert abs(layernorm_feat[0][0][0].detach().cpu().item() - 0.7244) < 0.05 + + @e2e_pytest_unit + @pytest.mark.parametrize("layer_index", [-1, -2]) + @pytest.mark.parametrize("use_gaussian", [True, False]) + @pytest.mark.parametrize("cls_token", [True, False]) + def test_vit_reciprocam_hook_initiate(self, layer_index, use_gaussian, cls_token): + model = self._create_model() + + explainer_hook = ViTReciproCAMHook(model, layer_index, use_gaussian, cls_token) + assert explainer_hook.records == [] + assert isinstance(explainer_hook._target_layernorm, LayerNorm) + + mosaic_feature_map = explainer_hook._get_mosaic_feature_map(torch.ones(197, 192)) + logit = explainer_hook._predict_from_feature_map(torch.ones(2, 197, 192)) + assert mosaic_feature_map is not None + assert logit is not None + assert mosaic_feature_map.shape == torch.Size([196, 197, 192]) + assert logit.shape == torch.Size([2, 1000]) + + @e2e_pytest_unit + @pytest.mark.parametrize("layer_index", [-1, -2]) + @pytest.mark.parametrize("use_gaussian", [True, False]) + @pytest.mark.parametrize("cls_token", [True, False]) + def test_vit_reciprocam_hook_func(self, layer_index, use_gaussian, cls_token): + model = self._create_model() + + explainer_hook = ViTReciproCAMHook(model, layer_index, use_gaussian, cls_token) + img = torch.ones(2, 3, 224, 224) - 0.5 + _, layernorm_feat = _extract_vit_feat(model, img) + saliency_map = explainer_hook.func(layernorm_feat) + assert saliency_map is not None diff --git a/tests/unit/algorithms/classification/utils/__init__.py b/tests/unit/algorithms/classification/utils/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/classification/utils/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/classification/utils/test_utils.py b/tests/unit/algorithms/classification/utils/test_utils.py new file mode 100644 index 00000000000..f0f3819f009 --- /dev/null +++ b/tests/unit/algorithms/classification/utils/test_utils.py @@ -0,0 +1,99 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import json +from pathlib import Path + +import pytest + +from otx.algorithms.classification.utils.cls_utils import ( + get_cls_deploy_config, + get_cls_inferencer_configuration, + get_cls_model_api_configuration, + get_multihead_class_info, +) +from otx.algorithms.classification.utils.convert_coco_to_multilabel import ( + coco_to_datumaro_multilabel, + multilabel_ann_format, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.classification.test_helper import ( + generate_cls_dataset, + generate_label_schema, +) + + +@pytest.fixture +def default_hierarchical_data(): + hierarchical_dataset = generate_cls_dataset(hierarchical=True) + label_schema = generate_label_schema(hierarchical_dataset.get_labels(), multilabel=False, hierarchical=True) + return hierarchical_dataset, label_schema + + +@e2e_pytest_unit +def test_coco_conversion(tmp_dir_path): + path_to_coco_example_ann_file = ( + Path("tests/assets/car_tree_bug/annotations/instances_train.json").absolute().as_posix() + ) + path_to_coco_example_image_dir = Path("tests/assets/car_tree_bug/images/train").absolute().as_posix() + output_path = Path(tmp_dir_path) / "annotations.json" + coco_to_datumaro_multilabel( + path_to_coco_example_ann_file, path_to_coco_example_image_dir, output_path, test_mode=True + ) + assert Path.exists(output_path) + with open(output_path, "r") as f: + coco_ann = json.load(f) + for key in multilabel_ann_format: + assert key in coco_ann.keys() + assert len(coco_ann["items"]) > 0 + + +@e2e_pytest_unit +def test_get_multihead_class_info(default_hierarchical_data): + hierarchical_dataset, label_schema = default_hierarchical_data + class_info = get_multihead_class_info(label_schema) + assert ( + len(class_info["label_to_idx"]) + == len(class_info["class_to_group_idx"]) + == len(hierarchical_dataset.get_labels()) + ) + + +@e2e_pytest_unit +def test_get_cls_inferencer_configuration(default_hierarchical_data) -> None: + _, label_schema = default_hierarchical_data + config = get_cls_inferencer_configuration(label_schema) + + assert config["hierarchical"] + assert not config["multilabel"] + assert "multihead_class_info" in config + + +@e2e_pytest_unit +def test_get_cls_deploy_config(default_hierarchical_data) -> None: + _, label_schema = default_hierarchical_data + inf_conf = {"test": "test"} + config = get_cls_deploy_config(label_schema, inf_conf) + + assert config["type_of_model"] == "Classification" + assert config["converter_type"] == "CLASSIFICATION" + assert "labels" in config["model_parameters"] + for k in inf_conf: + assert k in config["model_parameters"] + + +@e2e_pytest_unit +def test_get_cls_model_api_configuration(default_hierarchical_data): + _, label_schema = default_hierarchical_data + config = get_cls_inferencer_configuration(label_schema) + + model_api_cfg = get_cls_model_api_configuration(label_schema, config) + + assert len(model_api_cfg) > 0 + assert model_api_cfg[("model_info", "confidence_threshold")] == str(config["confidence_threshold"]) + assert ("model_info", "hierarchical_config") in model_api_cfg + assert ("model_info", "labels") in model_api_cfg + assert ("model_info", "label_ids") in model_api_cfg + assert len(label_schema.get_labels(include_empty=False)) == len(model_api_cfg[("model_info", "labels")].split()) + assert len(label_schema.get_labels(include_empty=False)) == len(model_api_cfg[("model_info", "label_ids")].split()) diff --git a/tests/unit/algorithms/common/__init__.py b/tests/unit/algorithms/common/__init__.py new file mode 100644 index 00000000000..8be278f32ee --- /dev/null +++ b/tests/unit/algorithms/common/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/__init__.py b/tests/unit/algorithms/common/adapters/__init__.py new file mode 100644 index 00000000000..ef29afc91ad --- /dev/null +++ b/tests/unit/algorithms/common/adapters/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common.adapters""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/mmcv/__init__.py b/tests/unit/algorithms/common/adapters/mmcv/__init__.py new file mode 100644 index 00000000000..7c350a44bea --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common.adapters.mmcv""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_adaptive_repeat_data_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_adaptive_repeat_data_hook.py new file mode 100644 index 00000000000..550aea5f611 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_adaptive_repeat_data_hook.py @@ -0,0 +1,37 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.adaptive_repeat_data_hooks.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import pytest +from mmcv.utils import Config +from torch.utils.data import Dataset +from torch.utils.data import DataLoader +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from otx.algorithms.common.adapters.mmcv.hooks import AdaptiveRepeatDataHook + + +class TestAdaptiveRepeatDataHook: + """Test class for AdaptiveRepeatDataHook.""" + + @pytest.fixture(autouse=True) + def setup(self): + class MockDataset(Dataset): + def __init__(self): + self.img_indices = {"foo": list(range(0, 6)), "bar": list(range(6, 10))} + + def __len__(self): + return 10 + + self.mock_dataset = MockDataset() + self.mock_data_loader = DataLoader( + dataset=MockDataset(), + batch_size=len(MockDataset()), + ) + self.mock_runner = Config({"data_loader": self.mock_data_loader}) + + @e2e_pytest_unit + def test_before_epoch(self) -> None: + hook = AdaptiveRepeatDataHook(64, len(self.mock_dataset)) + hook.before_epoch(self.mock_runner) + + assert self.mock_runner.data_loader.sampler.repeat == 5 diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_adaptive_training_hooks.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_adaptive_training_hooks.py new file mode 100644 index 00000000000..51a30756b97 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_adaptive_training_hooks.py @@ -0,0 +1,92 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.adaptive_training_hooks.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.runner import LrUpdaterHook +from mmcv.runner.hooks.checkpoint import CheckpointHook +from mmcv.runner.hooks.evaluation import EvalHook +from mmcv.utils import Config + +from otx.algorithms.common.adapters.mmcv.hooks.adaptive_training_hook import ( + AdaptiveTrainSchedulingHook, +) +from otx.algorithms.common.adapters.mmcv.hooks.early_stopping_hook import ( + EarlyStoppingHook, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockEvalHook(EvalHook): + def __init__(self): + self.interval = 5 + self.start = 5 + self.by_epoch = False + + +class MockLrUpdaterHook(LrUpdaterHook): + def __init__(self): + self.interval = 1 + self.patience = 1 + + +class MockEarlyStoppingHook(EarlyStoppingHook): + def __init__(self): + self.start = 1 + self.interval = 1 + self.patience = 1 + + +class MockCheckpointHook(CheckpointHook): + def __init__(self): + self.interval = 1 + self.by_epoch = False + + +class TestAdaptiveTrainSchedulingHook: + """Test class for AdaptiveTrainSchedulingHook.""" + + @e2e_pytest_unit + def test_before_run(self) -> None: + """Test before_run function.""" + + eval_hook = MockEvalHook() + mock_runner = Config({"hooks": [None, eval_hook]}) + hook = AdaptiveTrainSchedulingHook(enable_eval_before_run=True) + hook.before_run(mock_runner) + assert eval_hook.interval == 1 + assert eval_hook.start == 0 + assert hook._original_interval == 5 + + @e2e_pytest_unit + def test_before_train_iter(self) -> None: + """Test before_train_iter function.""" + + hook = AdaptiveTrainSchedulingHook(enable_eval_before_run=True, enable_adaptive_interval_hook=True) + hook._original_interval = 5 + + eval_hook = MockEvalHook() + lr_hook = MockLrUpdaterHook() + early_hook = MockEarlyStoppingHook() + ckpt_hook = MockCheckpointHook() + + mock_runner = Config( + { + "hooks": [eval_hook, lr_hook, early_hook, ckpt_hook], + "max_epochs": 200, + "max_iters": 200, + "data_loader": range(10), + "epoch": 5, + } + ) + + hook.before_train_iter(mock_runner) + assert hook._initialized is True + assert hook.max_interval == 5 + assert hook._original_interval is None + assert eval_hook.interval == 4 + assert lr_hook.interval == 4 + assert lr_hook.patience == 2 + assert early_hook.interval == 4 + assert early_hook.patience == 3 + assert ckpt_hook.interval == 4 diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_cancel_interface_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_cancel_interface_hook.py new file mode 100644 index 00000000000..1147fa5a73b --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_cancel_interface_hook.py @@ -0,0 +1,56 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.cancel_interface_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.runner import EpochBasedRunner + +from otx.algorithms.common.adapters.mmcv.hooks.cancel_hook import CancelInterfaceHook +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def mock_callback(*args): + pass + + +class TestCancelInterfaceHook: + """Test class for CancelInterfaceHook""" + + @e2e_pytest_unit + def test_cancel(self): + """Test cancel function.""" + + class MockRunner(EpochBasedRunner): + def __init__(self): + self._max_epoch = 10 + self.should_stop = False + self._epoch = 5 + + hook = CancelInterfaceHook(mock_callback) + assert hook.cancel() is None + + mock_runner = MockRunner() + mock_runner.should_stop = True + hook.runner = mock_runner + assert hook.cancel() is None + + mock_runner.should_stop = False + hook.runner = mock_runner + hook.cancel() + assert hook.runner.should_stop is True + assert hook.runner._max_epochs == hook.runner.epoch + + @e2e_pytest_unit + def test_before_run(self) -> None: + hook = CancelInterfaceHook(mock_callback) + runner = "RUNNER" + + hook.before_run(runner) + assert hook.runner == "RUNNER" + + @e2e_pytest_unit + def test_after_run(self) -> None: + hook = CancelInterfaceHook(mock_callback) + hook.runner = "RUNNER" + hook.after_run("RUNNER") + assert hook.runner is None diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_checkpoint_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_checkpoint_hook.py new file mode 100644 index 00000000000..4b05a5f7089 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_checkpoint_hook.py @@ -0,0 +1,63 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.checkpoint_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmcv.utils import Config + +from otx.algorithms.common.adapters.mmcv.hooks.checkpoint_hook import ( + CheckpointHookWithValResults, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockRunner: + class _MockModel: + def __init__(self, string): + self.name = string + + def buffers(self): + return None + + class _MockLogger: + def info(self, string): + print(string) + + def __init__(self): + self.model = self._MockModel("model") + self.ema_model = self._MockModel("ema_model") + self.meta = {} + self.epoch = 1 + self.iter = 1 + self.save_ckpt = True + self.logger = self._MockLogger() + self.save_ema_model = True + + def save_checkpoint(self, *args, **kwrags): + pass + + +class TestCheckpointHookWithValResults: + """Test class for CheckpointHookWithValResults.""" + + @e2e_pytest_unit + def test_before_run(self) -> None: + """Test before_run function.""" + + hook = CheckpointHookWithValResults() + mock_runner = Config({"work_dir": "./temp_dir/"}) + hook.before_run(mock_runner) + assert hook.out_dir == "./temp_dir/" + + @e2e_pytest_unit + def test_after_train_epoch(self, mocker) -> None: + """Test after_train_epoch function.""" + + mocker.patch.object(CheckpointHookWithValResults, "every_n_epochs", return_value=True) + mocker.patch("otx.algorithms.common.adapters.mmcv.hooks.checkpoint_hook.allreduce_params", return_value=True) + hook = CheckpointHookWithValResults(sync_buffer=True, out_dir="./tmp_dir/") + runner = MockRunner() + hook.after_train_epoch(runner) + + assert runner.model.name == "model" + assert runner.save_ema_model is False diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_composed_dataloader_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_composed_dataloader_hook.py new file mode 100644 index 00000000000..c97204b9aed --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_composed_dataloader_hook.py @@ -0,0 +1,20 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.composed_dataloaders_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.mmcv.hooks.composed_dataloaders_hook import ( + ComposedDataLoadersHook, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestComposedDataLoadersHook: + @e2e_pytest_unit + def test_temp(self) -> None: + try: + hook = ComposedDataLoadersHook() + assert hook is None + except Exception as e: + print(e) + pass diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_early_stopping_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_early_stopping_hook.py new file mode 100644 index 00000000000..fd952042d27 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_early_stopping_hook.py @@ -0,0 +1,275 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.early_stopping_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from logging import Logger +from math import inf + +import numpy as np +import pytest +from mmcv.runner import BaseRunner, LrUpdaterHook +from mmcv.utils import Config + +from otx.algorithms.common.adapters.mmcv.hooks.early_stopping_hook import ( + EarlyStoppingHook, + LazyEarlyStoppingHook, + ReduceLROnPlateauLrUpdaterHook, + StopLossNanTrainingHook, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockRunner(BaseRunner): + """Mock class for BaseRunner.""" + + def __init__(self) -> None: + self._max_epochs = None + self._iter = 9 + self._hooks = [LrUpdaterHook(warmup_iters=3)] + self._rank = 0 + self.log_buffer = Config({"output": {"acc": 1.0}}) + self.logger = Logger("otx") + self.should_stop = False + self.optimizer = Config({"param_groups": [{"lr": 1e-4}]}) + self.bbox_mAP = 50.0 + + def run(self): + pass + + def save_checkpoint(self): + pass + + def train(self): + pass + + def val(self): + pass + + +class TestEarlyStoppingHook: + """Test class for EarlyStoppingHook.""" + + @e2e_pytest_unit + def test_init_rule(self) -> None: + """Test funciton for init_rule function.""" + + hook = EarlyStoppingHook(interval=5) + with pytest.raises(KeyError): + hook._init_rule("Invalid Key", "Invalid Indicator") + with pytest.raises(ValueError): + hook._init_rule(None, "Invalid Indicator") + hook._init_rule("greater", "acc") + assert hook.rule == "greater" + assert hook.key_indicator == "acc" + assert hook.compare_func(5, 9) is False + hook._init_rule("less", "loss") + assert hook.rule == "less" + assert hook.key_indicator == "loss" + assert hook.compare_func(5, 9) is True + + @e2e_pytest_unit + def test_before_run(self) -> None: + """Test function for before_run.""" + + hook = EarlyStoppingHook(interval=5) + runner = MockRunner() + hook.before_run(runner) + assert hook.by_epoch is False + assert hook.warmup_iters == 3 + + runner._hooks = [] + hook = EarlyStoppingHook(interval=5) + with pytest.raises(ValueError): + hook.before_run(runner) + + @e2e_pytest_unit + def test_after_train_iter(self, mocker) -> None: + """Test after_train_iter function.""" + + mocker.patch.object(EarlyStoppingHook, "_do_check_stopping", return_value=True) + hook = EarlyStoppingHook(interval=5) + runner = MockRunner() + hook.by_epoch = False + hook.after_train_iter(runner) + + @e2e_pytest_unit + def test_after_train_epoch(self, mocker) -> None: + """Test after_train_epoch function.""" + + mocker.patch.object(EarlyStoppingHook, "_do_check_stopping", return_value=True) + runner = MockRunner() + hook = EarlyStoppingHook(interval=5) + hook.by_epoch = True + hook.after_train_epoch(runner) + + @e2e_pytest_unit + def test_do_check_stopping(self, mocker): + """Test _do_check_stopping function.""" + + runner = MockRunner() + mocker.patch.object(EarlyStoppingHook, "_should_check_stopping", return_value=False) + hook = EarlyStoppingHook(interval=5) + hook.warmup_iters = 3 + hook._do_check_stopping(runner) + mocker.patch.object(EarlyStoppingHook, "_should_check_stopping", return_value=True) + runner.log_buffer = Config({"output": []}) + with pytest.raises(KeyError): + hook._do_check_stopping(runner) + runner = MockRunner() + hook.key_indicator = "acc" + hook._do_check_stopping(runner) + assert hook.best_score == 1.0 + assert hook.wait_count == 0 + assert hook.last_iter == 9 + + hook = EarlyStoppingHook(interval=5) + hook.by_epoch = False + hook.key_indicator = "acc" + hook.warmup_iters = 3 + hook.best_score = 2.0 + hook.patience = 1.0 + hook.iteration_patience = 0.0 + hook._do_check_stopping(runner) + assert hook.wait_count == 1 + assert runner.should_stop is True + + runner.should_stop = False + hook = EarlyStoppingHook(interval=5) + hook.by_epoch = False + hook.key_indicator = "acc" + hook.warmup_iters = 3 + hook.best_score = 2.0 + hook.patience = 1.0 + hook.iteration_patience = 20.0 + hook._do_check_stopping(runner) + assert hook.wait_count == 1 + assert runner.should_stop is False + + @e2e_pytest_unit + def test_should_check_stopping(self) -> None: + """Test _should_check_stopping function.""" + + hook = EarlyStoppingHook(interval=5) + hook.by_epoch = False + runner = MockRunner() + assert hook._should_check_stopping(runner) is True + + runner._iter = 8 + assert hook._should_check_stopping(runner) is False + + +class TestLazyEarlyStoppingHook: + """Test LazyEarlyStoppingHook function.""" + + def test_should_check_stopping(self) -> None: + hook = LazyEarlyStoppingHook(interval=5) + hook.by_epoch = False + + runner = MockRunner() + runner._iter = 8 + assert hook._should_check_stopping(runner) is False + + hook.start = 20 + assert hook._should_check_stopping(runner) is False + + runner._iter = 17 + hook.start = 4 + assert hook._should_check_stopping(runner) is False + + runner._iter = 18 + hook.start = 4 + assert hook._should_check_stopping(runner) is True + + +class TestReduceLROnPlateauLrUpdaterHook: + """Test class for ReduceLROnPlateauLrUpdaterHook.""" + + @e2e_pytest_unit + def test_init_rule(self) -> None: + """Test funciton for init_rule function.""" + + hook = ReduceLROnPlateauLrUpdaterHook(interval=5, min_lr=1e-5) + with pytest.raises(KeyError): + hook._init_rule("Invalid Key", "Invalid Indicator") + with pytest.raises(ValueError): + hook._init_rule(None, "Invalid Indicator") + hook._init_rule("greater", "acc") + assert hook.rule == "greater" + assert hook.key_indicator == "acc" + assert hook.compare_func(5, 9) is False + hook._init_rule("less", "loss") + assert hook.rule == "less" + assert hook.key_indicator == "loss" + assert hook.compare_func(5, 9) is True + + @e2e_pytest_unit + def test_is_check_timing(self) -> None: + """Test _should_check_stopping function.""" + + hook = ReduceLROnPlateauLrUpdaterHook(interval=5, min_lr=1e-5) + hook.by_epoch = False + runner = MockRunner() + assert hook._is_check_timing(runner) is False + + @e2e_pytest_unit + def test_get_lr(self, mocker) -> None: + """Test function for get_lr.""" + + mocker.patch.object(ReduceLROnPlateauLrUpdaterHook, "_is_check_timing", return_value=False) + hook = ReduceLROnPlateauLrUpdaterHook(interval=5, min_lr=1e-5) + hook.warmup_iters = 3 + runner = MockRunner() + assert hook.get_lr(runner, 1e-2) == 1e-2 + + mocker.patch.object(ReduceLROnPlateauLrUpdaterHook, "_is_check_timing", return_value=True) + hook = ReduceLROnPlateauLrUpdaterHook(interval=5, min_lr=1e-5) + hook.warmup_iters = 3 + runner = MockRunner() + assert hook.get_lr(runner, 1e-2) == 1e-2 + assert hook.bad_count == 0 + + mocker.patch.object(ReduceLROnPlateauLrUpdaterHook, "_is_check_timing", return_value=True) + hook = ReduceLROnPlateauLrUpdaterHook(interval=5, min_lr=1e-5) + hook.best_score = 90 + hook.warmup_iters = 3 + hook.bad_count = 2 + hook.iteration_patience = 5 + hook.last_iter = 8 + runner = MockRunner() + assert hook.get_lr(runner, 1e-2) == 1e-2 + + hook = ReduceLROnPlateauLrUpdaterHook(interval=5, min_lr=1e-5) + hook.best_score = 90 + hook.warmup_iters = 3 + hook.bad_count = 2 + hook.iteration_patience = 5 + hook.last_iter = 2 + runner = MockRunner() + assert hook.get_lr(runner, 1e-3) == 1e-3 + assert hook.last_iter == 2 + assert hook.bad_count == 2 + + @e2e_pytest_unit + def test_before_run(self) -> None: + """Test function for before_run.""" + + hook = ReduceLROnPlateauLrUpdaterHook(interval=5, min_lr=1e-5) + runner = MockRunner() + hook.before_run(runner) + assert hook.base_lr == [1e-4] + assert hook.bad_count == 0 + assert hook.last_iter == 0 + assert hook.current_lr == -1.0 + assert hook.best_score == -inf + + +class TestStopLossNanTrainingHook: + """Test class for StopLossNanTrainingHook.""" + + def test_after_train_iter(self) -> None: + hook = StopLossNanTrainingHook() + runner = MockRunner() + runner.outputs = {"loss": np.array([np.nan])} + hook.after_train_iter(runner) + assert runner.should_stop is True diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_ema_v2_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_ema_v2_hook.py new file mode 100644 index 00000000000..eeb1c8d3b1a --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_ema_v2_hook.py @@ -0,0 +1,32 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.model_ema_v2_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.mmcv.hooks.model_ema_v2_hook import ( + ModelEmaV2, + ModelEmaV2Hook, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestModelEmaV2Hook: + @e2e_pytest_unit + def test_temp(self) -> None: + try: + hook = ModelEmaV2Hook() + assert hook is None + except Exception as e: + print(e) + pass + + +class TestModelEmaV2: + @e2e_pytest_unit + def test_temp(self) -> None: + try: + model = ModelEmaV2() + assert model is None + except Exception as e: + print(e) + pass diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_eval_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_eval_hook.py new file mode 100644 index 00000000000..960705fd980 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_eval_hook.py @@ -0,0 +1,215 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.eval_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch +from mmcv.runner import BaseRunner +from torch.utils.data import DataLoader + +from otx.algorithms.common.adapters.mmcv.hooks.eval_hook import ( + CustomEvalHook, + DistCustomEvalHook, + single_gpu_test, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockDataloader(DataLoader): + """Mock class for pytorch dataloader.""" + + class _MockDataset: + def __init__(self, data) -> None: + self.data = data + + def __len__(self) -> None: + return len(self.data) + + def evaluate(self, results, *args, **kwargs): + if len(results) == 0: + return {"top-1": 0.5} + return {"top-1": 0.7} + + def __init__(self) -> None: + self.data = [{"img": torch.randn(1, 3, 224, 224)}] + self.dataset = self._MockDataset(self.data) + + def __len__(self) -> int: + return len(self.data) + + def __getitem__(self, idx: int) -> torch.Tensor: + return self.data[idx] + + def __iter__(self): + return iter(self.data) + + +class MockRunner(BaseRunner): + """Mock class for BaseRunner.""" + + class _ModuleWrapper: + module = torch.nn.Module() + + class _MockBuffer: + output = {} + ready = False + + def clear(self): + self.output = {} + + def __init__(self): + self.model = torch.nn.Module() + self.ema_model = self._ModuleWrapper() + self._epoch = 9 + self._iter = 9 + self.log_buffer = self._MockBuffer() + self.logger = None + self.work_dir = "./tmp_dir/" + + def run(self): + pass + + def save_checkpoint(self): + pass + + def train(self): + pass + + def val(self): + pass + + +class TestCustomEvalHook: + """Test class for CustomEvalHook.""" + + @e2e_pytest_unit + def test_init(self) -> None: + """Test __init__ function.""" + + hook = CustomEvalHook(metric="accuracy", dataloader=MockDataloader()) + assert hook.metric == "top-1" + hook = CustomEvalHook(metric=["class_accuracy"], dataloader=MockDataloader()) + assert hook.metric == "accuracy" + hook = CustomEvalHook(metric=["accuracy"], dataloader=MockDataloader()) + assert hook.metric == "top-1" + + @e2e_pytest_unit + def test_do_evaluate(self, mocker) -> None: + """Test do_evaluate function.""" + + hook = CustomEvalHook(metric="accuracy", dataloader=MockDataloader()) + runner = MockRunner() + mocker.patch("otx.algorithms.common.adapters.mmcv.hooks.eval_hook.single_gpu_test", return_value=[]) + mocker.patch.object(CustomEvalHook, "evaluate", return_value=True) + hook._do_evaluate(runner, ema=False) + hook.ema_eval_start_epoch = 3 + hook._do_evaluate(runner, ema=True) + + @e2e_pytest_unit + def test_after_train_epoch(self, mocker) -> None: + """Test after_train_epoch function.""" + + hook = CustomEvalHook(metric="accuracy", dataloader=MockDataloader()) + runner = MockRunner() + hook.by_epoch = True + hook.interval = 4 + mocker.patch.object(CustomEvalHook, "_do_evaluate", return_value=True) + hook.after_train_epoch(runner) + hook.interval = 5 + hook.after_train_epoch(runner) + + @e2e_pytest_unit + def test_after_train_iter(self, mocker) -> None: + """Test after_train_iter function.""" + + hook = CustomEvalHook(metric="accuracy", dataloader=MockDataloader()) + runner = MockRunner() + hook.by_epoch = False + hook.interval = 4 + mocker.patch.object(CustomEvalHook, "_do_evaluate", return_value=True) + hook.after_train_iter(runner) + hook.interval = 5 + hook.after_train_iter(runner) + + @e2e_pytest_unit + def test_evaluate(self) -> None: + """Test evaluate function.""" + + hook = CustomEvalHook(metric="accuracy", dataloader=MockDataloader()) + runner = MockRunner() + hook.evaluate(runner, results=[], results_ema=None) + assert runner.log_buffer.output["top-1"] == 0.5 + assert runner.log_buffer.ready is True + assert hook.best_score == 0.5 + assert runner.save_ckpt is True + + hook.evaluate(runner, results=[], results_ema=[0]) + assert runner.log_buffer.output["top-1_EMA"] == 0.7 + assert runner.save_ema_model is True + + +@e2e_pytest_unit +def test_single_gpu_test() -> None: + """Test function for single_gpu_test.""" + + class _MockModel(torch.nn.Module): + def __init__(self): + super().__init__() + + def forward(self, *args, **kwargs): + return torch.Tensor([0]) + + model = _MockModel() + single_gpu_test(model, MockDataloader()) + + +class TestDistCustomEvalHook: + """Test class for DistCustomEvalHook.""" + + @e2e_pytest_unit + def test_do_evaluate(self, mocker) -> None: + """Test _do_evaluate function.""" + + mocker.patch("mmcls.apis.multi_gpu_test", return_value=True) + mocker.patch.object(DistCustomEvalHook, "evaluate", return_value=True) + runner = MockRunner() + dataloader = MockDataloader() + runner._rank = 0 + hook = DistCustomEvalHook(dataloader, metric="top-1") + hook._do_evaluate(runner) + + with pytest.raises(TypeError): + hook = DistCustomEvalHook(None) + + @e2e_pytest_unit + def test_after_train_epoch(self, mocker) -> None: + """Test after_train_epoch function.""" + + dataloader = MockDataloader() + hook = DistCustomEvalHook(dataloader, metric="top-1") + mocker.patch.object(DistCustomEvalHook, "_do_evaluate", side_effect=RuntimeError("VALID ERROR")) + hook.by_epoch = True + hook.interval = 5 + runner = MockRunner() + with pytest.raises(RuntimeError): + hook.after_train_epoch(runner) + + hook.interval = 3 + hook.after_train_epoch(runner) + + @e2e_pytest_unit + def test_after_train_iter(self, mocker) -> None: + """Test after_train_iter function.""" + + dataloader = MockDataloader() + hook = DistCustomEvalHook(dataloader, metric="top-1") + mocker.patch.object(DistCustomEvalHook, "_do_evaluate", side_effect=RuntimeError("VALID ERROR")) + hook.by_epoch = False + hook.interval = 5 + runner = MockRunner() + with pytest.raises(RuntimeError): + hook.after_train_iter(runner) + + hook.interval = 3 + hook.after_train_iter(runner) diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_fp16_sam_optimizer_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_fp16_sam_optimizer_hook.py new file mode 100644 index 00000000000..bb1c24217e1 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_fp16_sam_optimizer_hook.py @@ -0,0 +1,20 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.fp16_sam_optimizer_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.mmcv.hooks.fp16_sam_optimizer_hook import ( + Fp16SAMOptimizerHook, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestFp16SAMOptimizerHook: + @e2e_pytest_unit + def test_temp(self) -> None: + try: + hook = Fp16SAMOptimizerHook() + assert hook is None + except Exception as e: + print(e) + pass diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_hooks.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_hooks.py new file mode 100644 index 00000000000..03f414b297d --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_hooks.py @@ -0,0 +1,196 @@ +from typing import List +from unittest.mock import MagicMock + +import numpy as np +import pytest +from mmcv.runner import BaseRunner + +from otx.algorithms.common.adapters.mmcv.hooks import ( + EMAMomentumUpdateHook, + TwoCropTransformHook, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@pytest.fixture +def mock_iter_runner(mocker): + _mock_iter_runner = mocker.patch("mmcv.runner.IterBasedRunner", autospec=True) + _mock_iter_runner.data_loader = MagicMock() + _mock_iter_runner.model = MagicMock() + + return _mock_iter_runner + + +@pytest.fixture +def mock_epoch_runner(mocker): + _mock_epoch_runner = mocker.patch("mmcv.runner.EpochBasedRunner", autospec=True) + _mock_epoch_runner.model = MagicMock() + + return _mock_epoch_runner + + +@pytest.mark.usefixtures("mock_epoch_runner") +@pytest.mark.usefixtures("mock_iter_runner") +class TestEMAMomentumUpdateHook: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.ema_momentum_update_hook = EMAMomentumUpdateHook() + + @e2e_pytest_unit + @pytest.mark.parametrize( + "by_epoch,cur_epoch,expected", + [ + (True, 1, 0.9960002467350366), + (True, 50, 0.9965857864376269), + (True, 150, 0.9994142135623731), + (False, 200, 0.996), + ], + ) + def test_before_train_epoch( + self, mock_epoch_runner: BaseRunner, by_epoch: bool, cur_epoch: int, expected: float + ) -> None: + """Test before_train_epoch.""" + mock_epoch_runner.model.momentum = 0.996 # default value of BYOL + mock_epoch_runner.model.base_momentum = 0.996 + mock_epoch_runner.epoch = cur_epoch + mock_epoch_runner.max_epochs = 200 + self.ema_momentum_update_hook.by_epoch = by_epoch + + self.ema_momentum_update_hook.before_train_epoch(mock_epoch_runner) + + assert np.allclose(mock_epoch_runner.model.momentum, expected) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "by_epoch,cur_iter,expected", + [ + (False, 1, 0.9960002467350366), + (False, 50, 0.9965857864376269), + (False, 150, 0.9994142135623731), + (True, 200, 0.996), + ], + ) + def test_before_train_iter( + self, mock_iter_runner: BaseRunner, by_epoch: bool, cur_iter: int, expected: float + ) -> None: + """Test before_train_iter.""" + mock_iter_runner.model.momentum = 0.996 # default value of BYOL + mock_iter_runner.model.base_momentum = 0.996 + mock_iter_runner.iter = cur_iter + mock_iter_runner.max_iters = 200 + self.ema_momentum_update_hook.by_epoch = by_epoch + + self.ema_momentum_update_hook.before_train_iter(mock_iter_runner) + + assert np.allclose(mock_iter_runner.model.momentum, expected) + + @e2e_pytest_unit + def test_after_train_iter(self, mock_iter_runner: BaseRunner) -> None: + """Test after_train_iter.""" + mock_iter_runner.iter = 1 + mock_iter_runner.model.momentum_update = MagicMock() + + self.ema_momentum_update_hook.after_train_iter(mock_iter_runner) + + mock_iter_runner.model.momentum_update.assert_called_once() + + +@pytest.mark.usefixtures("mock_iter_runner") +class TestTwoCropTransformHook: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.two_crop_transform_hook = TwoCropTransformHook(interval=1) + + def set_mock_name(self, name: str, is_both: bool = True) -> MagicMock: + mock_class = MagicMock() + mock_class.__class__.__name__ = name + if name == "TwoCropTransform": + mock_class.is_both = is_both + return mock_class + + @e2e_pytest_unit + def test_use_not_implemented_by_epoch(self) -> None: + with pytest.raises(NotImplementedError): + TwoCropTransformHook(interval=1, by_epoch=True) + + @e2e_pytest_unit + def test_get_dataset(self, mock_iter_runner: BaseRunner) -> None: + """Test _get_dataset.""" + mock_iter_runner.data_loader.dataset = True + assert not hasattr(mock_iter_runner.data_loader.dataset, "dataset") + + results = self.two_crop_transform_hook._get_dataset(mock_iter_runner) + assert results + + @e2e_pytest_unit + def test_get_dataset_repeat_dataset(self, mock_iter_runner: BaseRunner) -> None: + """Test _get_dataset when dataset includes child dataset (e.g. RepeatDataset).""" + mock_iter_runner.data_loader.dataset.dataset = True + assert hasattr(mock_iter_runner.data_loader.dataset, "dataset") + + results = self.two_crop_transform_hook._get_dataset(mock_iter_runner) + assert results + + @e2e_pytest_unit + @pytest.mark.parametrize( + "transforms_order", + [["TwoCropTransform", "A", "B"], ["A", "TwoCropTransform", "B"], ["A", "B", "TwoCropTransform"]], + ) + def test_find_two_crop_transform(self, transforms_order: List[object]) -> None: + """Test _find_two_crop_transform.""" + transforms = [self.set_mock_name(name) for name in transforms_order] + + results = self.two_crop_transform_hook._find_two_crop_transform(transforms) + assert results.__class__.__name__ == "TwoCropTransform" + + @e2e_pytest_unit + @pytest.mark.parametrize("interval,cnt,expected", [(1, 0, True), (2, 0, False), (2, 1, True)]) + def test_before_train_epoch(self, mock_iter_runner: BaseRunner, interval: int, cnt: int, expected: bool) -> None: + """Test before_train_epoch.""" + if hasattr(mock_iter_runner.data_loader.dataset, "dataset"): + del mock_iter_runner.data_loader.dataset.dataset + + setattr(self.two_crop_transform_hook, "interval", interval) + setattr(self.two_crop_transform_hook, "cnt", cnt) + + transforms_order = ["TwoCropTransform", "A", "B"] + transforms = [self.set_mock_name(name=name) for name in transforms_order] + mock_iter_runner.data_loader.dataset.pipeline.transforms = transforms + + self.two_crop_transform_hook.before_train_epoch(mock_iter_runner) + + assert transforms[0].is_both == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "is_both,interval,cnt,expected", + [ + (False, 1, 0, False), + (True, 1, 0, True), + (False, 3, 0, False), + (True, 3, 0, True), + (False, 3, 1, True), + (True, 3, 1, False), + (True, 2, 0, False), + ], + ) + def test_after_train_iter( + self, mock_iter_runner: BaseRunner, is_both: bool, interval: int, cnt: int, expected: bool + ) -> None: + """Test after_train_iter.""" + if hasattr(mock_iter_runner.data_loader.dataset, "dataset"): + del mock_iter_runner.data_loader.dataset.dataset + + setattr(self.two_crop_transform_hook, "interval", interval) + setattr(self.two_crop_transform_hook, "cnt", cnt) + + transforms_order = ["TwoCropTransform", "A", "B"] + transforms = [self.set_mock_name(name=name, is_both=is_both) for name in transforms_order] + mock_iter_runner.data_loader.dataset.pipeline.transforms = transforms + + self.two_crop_transform_hook.after_train_iter(mock_iter_runner) + + assert transforms[0].is_both == expected + if is_both and interval - cnt == 2: + # test cnt initialization + assert self.two_crop_transform_hook.cnt == 0 diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_ib_loss_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_ib_loss_hook.py new file mode 100644 index 00000000000..e6086c0b1d2 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_ib_loss_hook.py @@ -0,0 +1,18 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.ib_loss_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.mmcv.hooks.ib_loss_hook import IBLossHook +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestIBLossHook: + @e2e_pytest_unit + def test_temp(self) -> None: + try: + hook = IBLossHook() + assert hook is None + except Exception as e: + print(e) + pass diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_logger_replace_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_logger_replace_hook.py new file mode 100644 index 00000000000..f0c5b523737 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_logger_replace_hook.py @@ -0,0 +1,18 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.logger_replace_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.mmcv.hooks import LoggerReplaceHook +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestLoggerReplaceHook: + @e2e_pytest_unit + def test_temp(self) -> None: + try: + hook = LoggerReplaceHook() + assert hook is None + except Exception as e: + print(e) + pass diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_mean_teacher_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_mean_teacher_hook.py new file mode 100644 index 00000000000..d45b2ecce90 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_mean_teacher_hook.py @@ -0,0 +1,20 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.unbiased_teacher_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.mmcv.hooks.mean_teacher_hook import ( + MeanTeacherHook, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestUnbiasedTeacherHook: + @e2e_pytest_unit + def test_temp(self) -> None: + try: + hook = MeanTeacherHook() + assert hook is None + except Exception as e: + print(e) + pass diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_model_ema_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_model_ema_hook.py new file mode 100644 index 00000000000..3ca60cf62e7 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_model_ema_hook.py @@ -0,0 +1,126 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.model_ema_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import math + +import torch +from mmcv.runner import BaseRunner +from mmcv.runner.hooks.ema import EMAHook + +from otx.algorithms.common.adapters.mmcv.hooks import ( + CustomModelEMAHook, + DualModelEMAHook, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockModel(torch.nn.Module): + def __init__(self): + super().__init__() + self.model = torch.nn.Sequential( + torch.nn.Conv2d(3, 8, 3), + torch.nn.Conv2d(8, 16, 3), + torch.nn.Conv2d(16, 64, 3), + ) + + def forward(self, x): + return self.model(x) + + +class MockRunner(BaseRunner): + class _DualModel: + model_s = MockModel() + model_t = MockModel() + + def __init__(self): + self.model = self._DualModel() + self._epoch = 0 + self._iter = 0 + self.data_loader = range(1000) + + def run(self): + pass + + def save_checkpoint(self): + pass + + def train(self): + pass + + def val(self): + pass + + +class TestDualModelEMAHook: + """Test class for DualModelEMAHook.""" + + @e2e_pytest_unit + def test_before_run(self) -> None: + """Test before_run function.""" + + hook = DualModelEMAHook() + runner = MockRunner() + hook.before_run(runner) + + @e2e_pytest_unit + def test_before_train_epoch(self) -> None: + """Test before_train_epoch function.""" + + hook = DualModelEMAHook(epoch_momentum=0.99) + hook.enabled = True + runner = MockRunner() + hook.before_train_epoch(runner) + assert hook.momentum == 1 - math.pow((1 - 0.99), (1 / 1000)) + assert hook.epoch_momentum == 0.0 + + @e2e_pytest_unit + def test_after_train_iter(self) -> None: + """Test after_train_iter function.""" + + hook = DualModelEMAHook() + runner = MockRunner() + # Not enable + hook.after_train_iter(runner) + + hook.enabled = True + hook.interval = 5 + + runner._iter = 9 + # Skip + hook.after_train_iter(runner) + + runner._iter = 0 + hook.before_run(runner) + runner._iter = 10 + # Just copy + hook.after_train_iter(runner) + + runner._iter = 0 + hook.before_run(runner) + runner._epoch = 9 + # EMA + hook.after_train_iter(runner) + + @e2e_pytest_unit + def test_after_train_epoch(self): + """Test after_train_epoch function.""" + + hook = DualModelEMAHook() + runner = MockRunner() + hook.before_run(runner) + hook.after_train_epoch(runner) + + +class TestCustomModelEMAHook: + @e2e_pytest_unit + def test_before_train_epoch(self, mocker): + """Test before_train_epoch function.""" + + mocker.patch.object(EMAHook, "before_train_epoch", return_value=True) + hook = CustomModelEMAHook(epoch_momentum=0.99) + runner = MockRunner() + hook.before_train_epoch(runner) + assert hook.momentum == 1 - math.pow((1 - 0.99), (1 / 1000)) + assert hook.epoch_momentum == 0.0 diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_no_bias_decay_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_no_bias_decay_hook.py new file mode 100644 index 00000000000..11e95fdfba2 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_no_bias_decay_hook.py @@ -0,0 +1,53 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.no_bias_decay_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcv.utils import Config + +from otx.algorithms.common.adapters.mmcv.hooks.no_bias_decay_hook import NoBiasDecayHook +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockRunner: + def __init__(self): + self.model = torch.nn.Sequential( + torch.nn.Conv2d(3, 8, 3), + torch.nn.BatchNorm2d(8), + ) + self.optimizer = Config( + { + "param_groups": [ + { + "params": None, + "weight_decay": None, + "lr": 0.01, + } + ] + } + ) + + +class TestNoBiasDecayHook: + @e2e_pytest_unit + def test_before_train_epoch(self): + """Test before_train_epoch function.""" + + hook = NoBiasDecayHook() + runner = MockRunner() + hook.before_train_epoch(runner) + assert len(runner.optimizer.param_groups) == 3 + assert runner.optimizer.param_groups[0]["params"] is not None + assert runner.optimizer.param_groups[1]["params"] is not None + assert runner.optimizer.param_groups[1]["weight_decay"] == 0.0 + assert runner.optimizer.param_groups[1]["lr"] == 0.02 + assert runner.optimizer.param_groups[2]["params"] is not None + assert runner.optimizer.param_groups[2]["weight_decay"] == 0.0 + + @e2e_pytest_unit + def test_after_train_epoch(self): + hook = NoBiasDecayHook() + runner = MockRunner() + hook.after_train_epoch(runner) + assert runner.optimizer.param_groups[0]["params"] is not None diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_recording_forward_hooks.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_recording_forward_hooks.py new file mode 100644 index 00000000000..b8a8fd229e7 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_recording_forward_hooks.py @@ -0,0 +1,144 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest +import torch + +from otx.algorithms.common.adapters.mmcv.hooks.recording_forward_hook import ( + ActivationMapHook, + BaseRecordingForwardHook, + EigenCamHook, + FeatureVectorHook, + ReciproCAMHook, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockBaseRecordingForwardHook(BaseRecordingForwardHook): + @staticmethod + def func(*args): + return torch.Tensor([[0]]) + + +class TestBaseRecordingForwardHook: + """Test class for BaseRecordingForwardHook""" + + @e2e_pytest_unit + def test_records(self, mocker) -> None: + """Test records function.""" + + hook = MockBaseRecordingForwardHook(torch.nn.Module()) + assert hook.records == [] + + @e2e_pytest_unit + def test_func(self) -> None: + """Test func function.""" + hook = MockBaseRecordingForwardHook(torch.nn.Module()) + assert hook.func() == torch.Tensor([[0]]) + + @e2e_pytest_unit + def test_recording_forward(self) -> None: + """Test _recording_forward.""" + + hook = MockBaseRecordingForwardHook(torch.nn.Module()) + hook._recording_forward(torch.nn.Module(), torch.Tensor([0]), torch.Tensor([0])) + assert hook._records == [np.array([0.0])] + + @e2e_pytest_unit + def test_enter(self) -> None: + """Test __enter__ function.""" + + class _MockModule(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.backbone = torch.nn.Module() + + hook = MockBaseRecordingForwardHook(_MockModule()) + hook.__enter__() + + @e2e_pytest_unit + def test_exit(self) -> None: + """Test __exit__ function.""" + + class MockHandle: + def remove(self): + pass + + hook = MockBaseRecordingForwardHook(torch.nn.Module()) + hook._handle = MockHandle() + hook.__exit__(None, None, None) + + @e2e_pytest_unit + def test_normalize_map(self) -> None: + hook = MockBaseRecordingForwardHook(torch.nn.Module()) + maps = torch.full((2, 2), 0.5, dtype=torch.float32) + norm_map = hook._normalize_map(maps) + assert (norm_map == torch.zeros((2, 2), dtype=torch.uint8)).all() + + maps = torch.full((2, 2, 2), 0.5, dtype=torch.float32) + norm_map = hook._normalize_map(maps) + assert (norm_map == torch.zeros((2, 2, 2), dtype=torch.uint8)).all() + + +class TestEigenCamHook: + """Test class for EigenCamHook.""" + + def test_func(self) -> None: + """Test func function.""" + + hook = EigenCamHook(torch.nn.Module()) + feature_map = torch.randn(8, 3, 14, 14) + assert hook.func(feature_map) is not None + + +class TestActivationMapHook: + """Test class for ActivationMapHook.""" + + def test_func(self) -> None: + """Test func function.""" + + hook = ActivationMapHook(torch.nn.Module()) + feature_map = torch.randn(8, 3, 14, 14) + assert hook.func(feature_map) is not None + + +class TestFeatureVectorHook: + """Test class for FeatureVectorHook.""" + + def test_func(self) -> None: + """Test func function.""" + + hook = FeatureVectorHook(torch.nn.Module()) + feature_map = torch.randn(8, 3, 14, 14) + assert hook.func(feature_map) is not None + + +class TestReciproCAMHook: + """Test class for ReciproCAMHook.""" + + @pytest.fixture(autouse=True) + def setup(self) -> None: + class _MockModule(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.with_neck = True + self.neck = torch.nn.Module() + self.neck.forward = self.forward + self.head = torch.nn.Module() + self.head.num_classes = 3 + self.head.simple_test = self.forward + + def forward(self, x): + return x + + self.module = _MockModule() + self.hook = ReciproCAMHook(self.module) + + @e2e_pytest_unit + def test_func(self) -> None: + """Test func function.""" + + assert self.hook.func([torch.randn(1, 3, 1, 1)]) is not None diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_save_initial_weight_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_save_initial_weight_hook.py new file mode 100644 index 00000000000..e666e75cd2b --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_save_initial_weight_hook.py @@ -0,0 +1,18 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.save_initial_weight_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.mmcv.hooks import SaveInitialWeightHook +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSaveInitialWeightHook: + @e2e_pytest_unit + def test_temp(self) -> None: + try: + hook = SaveInitialWeightHook() + assert hook is None + except Exception as e: + print(e) + pass diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_semisl_cls_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_semisl_cls_hook.py new file mode 100644 index 00000000000..47086c34391 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_semisl_cls_hook.py @@ -0,0 +1,18 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.semisl_cls_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.mmcv.hooks.semisl_cls_hook import SemiSLClsHook +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSemiSLClsHook: + @e2e_pytest_unit + def test_temp(self) -> None: + try: + hook = SemiSLClsHook() + assert hook is None + except Exception as e: + print(e) + pass diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_task_adapt_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_task_adapt_hook.py new file mode 100644 index 00000000000..15fdb8c8cee --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_task_adapt_hook.py @@ -0,0 +1,18 @@ +"""Unit test for otx.algorithms.common.adapters.mmcv.hooks.task_adapt_hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.mmcv.hooks.task_adapt_hook import TaskAdaptHook +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestTaskAdaptHook: + @e2e_pytest_unit + def test_temp(self) -> None: + try: + hook = TaskAdaptHook() + assert hook is None + except Exception as e: + print(e) + pass diff --git a/tests/unit/algorithms/common/adapters/mmcv/hooks/test_xpu_optimizer_hook.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_xpu_optimizer_hook.py new file mode 100644 index 00000000000..0151ccff104 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_xpu_optimizer_hook.py @@ -0,0 +1,18 @@ +"""Test for XPU optimizer hook""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +def test_init(mocker): + from otx.algorithms.common.adapters.mmcv.hooks.xpu_optimizer_hook import BFp16XPUOptimizerHook + + mocker.patch( + "otx.algorithms.common.adapters.mmcv.hooks.xpu_optimizer_hook.XPUGradScaler", return_value=mocker.MagicMock() + ) + hook = BFp16XPUOptimizerHook(grad_clip=None, coalesce=True, bucket_size_mb=-1, loss_scale=512.0, distributed=True) + assert hook.coalesce is True # Check coalesce is True + assert hook.bucket_size_mb == -1 # Check bucket size is -1 + assert hook._scale_update_param is 512.0 # Check scale update param is 512.0 + assert hook.distributed is True # Check distributed is True + assert isinstance(hook.loss_scaler, mocker.MagicMock) diff --git a/tests/unit/algorithms/common/adapters/mmcv/nncf/__init__.py b/tests/unit/algorithms/common/adapters/mmcv/nncf/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/nncf/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/mmcv/nncf/test_helpers.py b/tests/unit/algorithms/common/adapters/mmcv/nncf/test_helpers.py new file mode 100644 index 00000000000..55b9c7e3195 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/nncf/test_helpers.py @@ -0,0 +1,310 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from functools import partial + +import numpy as np +import torch +from mmcls.core import EvalHook +from mmcls.datasets import build_dataloader +from mmcls.datasets.pipelines import Compose +from mmcv.utils import Config, ConfigDict, get_logger +from torch.optim import SGD + +import otx.algorithms.common.adapters.mmcv.nncf.patches # noqa: F401 +from otx.algorithms.common.adapters.mmcv.nncf.hooks import CompressionHook +from otx.algorithms.common.adapters.mmcv.nncf.runners import AccuracyAwareRunner +from otx.algorithms.common.adapters.mmcv.nncf.utils import ( + get_fake_input, + model_eval, + wrap_nncf_model, +) +from otx.algorithms.common.adapters.mmcv.utils import build_data_parallel +from otx.algorithms.common.adapters.nncf.patches import nncf_trace_context + + +def create_model(lib="mmcls"): + class MockModel(torch.nn.Module): + def __init__(self, *args, **kwargs): + super().__init__() + self.conv1 = torch.nn.Conv2d(3, 3, 3) + self.conv2 = torch.nn.Conv2d(3, 3, 3) + self.linear = torch.nn.Linear(3, 1) + + def forward(self, img, img_metas=None, **kwargs): + if isinstance(img, list): + img = img[0] + if img.shape[-1] == 3: + img = img.permute(0, 3, 1, 2) + + x = self.conv1(img) + x = self.conv2(img) + x = torch.mean(x, dim=(2, 3)) + x = self.linear(x) + return x + + def forward_dummy(self, img, img_metas=None, **kwargs): + if isinstance(img, list): + img = img[0] + if img.shape[-1] == 3: + img = img.permute(0, 3, 1, 2) + + x = self.conv1(img) + x = self.conv2(img) + x = torch.mean(x, dim=(2, 3)) + x = self.linear(x) + return x + + def train_step(self, *args, **kwargs): + return dict() + + def init_weights(self, *args, **kwargs): + pass + + def set_step_params(self, *args, **kwargs): + pass + + MockModel.nncf_trace_context = nncf_trace_context + + if lib == "mmcls": + from mmcls.models import CLASSIFIERS + + CLASSIFIERS.register_module(MockModel, force=True) + elif lib == "mmdet": + from mmdet.models import DETECTORS + + DETECTORS.register_module(MockModel, force=True) + elif lib == "mmseg": + from mmseg.models import SEGMENTORS + + SEGMENTORS.register_module(MockModel, force=True) + else: + raise ValueError() + + return MockModel() + + +def create_dataset(pipeline=None, lib="mmcls"): + class MockDataset(torch.utils.data.Dataset): + def __init__(self, pipeline, *args, **kwargs): + super().__init__() + self.dataset = np.zeros((10, 128, 128, 3), dtype=np.float32) + + if isinstance(pipeline, list): + if lib == "mmcls": + from mmcls.datasets.pipelines import Compose + elif lib == "mmdet": + from mmdet.datasets.pipelines import Compose + elif lib == "mmseg": + from mmseg.datasets.pipelines import Compose + else: + raise ValueError() + pipeline = Compose(pipeline) + self.pipeline = pipeline + + def __getitem__(self, idx): + if self.pipeline: + return self.pipeline( + dict( + img=self.dataset[idx], + filename=None, + ori_filename=None, + img_fields=["img"], + img_shape=self.dataset[idx].shape, + ori_shape=self.dataset[idx].shape, + ) + ) + return self.dataset[idx] + + def __len__(self): + return len(self.dataset) + + def evaluate(self, model_output, **kwargs): + # if isinstance(model_output, list): + # model_output = torch.cat(model_output) + # return {"accuracy": model_output.mean()} + return {"accuracy": torch.tensor(0.9)} + + if lib == "mmcls": + from mmcls.datasets import DATASETS + elif lib == "mmdet": + from mmdet.datasets import DATASETS + elif lib == "mmseg": + from mmseg.datasets import DATASETS + else: + raise ValueError() + + DATASETS.register_module(MockDataset, force=True) + return MockDataset(pipeline) + + +def create_dataloader(config=None): + if config is None: + config = create_config() + pipeline = Compose(config.data.val.pipeline) + mock_dataset = create_dataset(pipeline) + dataloader = build_dataloader(mock_dataset, samples_per_gpu=2, workers_per_gpu=1) + return dataloader + + +def create_config(lib="mmcls"): + config = Config( + { + "gpu_ids": [0], + "evaluation": {"metric": "accuracy"}, + "data": { + "samples_per_gpu": 2, + "workers_per_gpu": 0, + "persistent_workers": False, + "shuffle": False, + }, + "model": { + "type": "MockModel", + }, + "nncf_config": { + "input_info": {"sample_size": (1, 3, 128, 128)}, + "target_metric_name": "accuracy", + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": {"num_init_samples": 10}, + "batchnorm_adaptation": {"num_bn_adaptation_samples": 10}, + }, + } + ], + "accuracy_aware_training": {"params": {"maximal_total_epochs": 5, "mode": "early_exit"}}, + }, + "runner": { + "type": "AccuracyAwareRunner", + "nncf_config": { + "input_info": {"sample_size": (1, 3, 128, 128)}, + "target_metric_name": "accuracy", + "compression": [ + { + "algorithm": "quantization", + "preset": "mixed", + "initializer": { + "range": {"num_init_samples": 10}, + "batchnorm_adaptation": {"num_bn_adaptation_samples": 10}, + }, + } + ], + "accuracy_aware_training": {"params": {"maximal_total_epochs": 5, "mode": "early_exit"}}, + }, + }, + } + ) + + for subset in ("train", "test", "val"): + if lib == "mmcls": + config.data[subset] = ConfigDict( + { + "type": "MockDataset", + "pipeline": [ + {"type": "Resize", "size": (50, 50)}, + {"type": "Normalize", "mean": [0, 0, 0], "std": [1, 1, 1]}, + {"type": "ImageToTensor", "keys": ["img"]}, + { + "type": "Collect", + "keys": ["img"], + "meta_keys": [ + "filename", + "ori_filename", + "ori_shape", + "img_shape", + "img_norm_cfg", + ], + }, + ], + } + ) + else: + config.data[subset] = ConfigDict( + { + "type": "MockDataset", + "pipeline": [ + {"type": "Resize", "img_scale": (50, 50)}, + {"type": "Normalize", "mean": [0, 0, 0], "std": [1, 1, 1]}, + {"type": "ImageToTensor", "keys": ["img"]}, + { + "type": "Collect", + "keys": ["img"], + "meta_keys": [ + "filename", + "ori_filename", + "ori_shape", + "img_shape", + "img_norm_cfg", + ], + }, + ], + } + ) + return config + + +def create_eval_fn(): + def evaluate_fn(model, loader, *args, **kwargs): + out = [] + for data in loader: + out.append(torch.sigmoid(model(**data))) + return torch.cat(out) + + return evaluate_fn + + +def create_nncf_model(workdir): + mock_model = create_model() + mock_config = create_config() + mock_eval_fn = create_eval_fn() + dataloader = create_dataloader() + + mock_config = create_config() + mock_config.nncf_config.log_dir = workdir + pipeline = Compose(mock_config.data.val.pipeline) + get_fake_input_fn = partial(get_fake_input, pipeline) + + model_eval_fn = partial( + model_eval, + config=mock_config, + val_dataloader=dataloader, + evaluate_fn=mock_eval_fn, + distributed=False, + ) + + ctrl, model = wrap_nncf_model( + mock_config, + mock_model, + model_eval_fn=model_eval_fn, + get_fake_input_fn=get_fake_input_fn, + dataloader_for_init=dataloader, + is_accuracy_aware=True, + ) + return ctrl, model + + +def create_nncf_runner(work_dir): + mock_config = create_config() + dataloader = create_dataloader() + ctrl, model = create_nncf_model(work_dir) + + runner = AccuracyAwareRunner( + build_data_parallel(model, mock_config), + logger=get_logger("mmcv"), + work_dir=work_dir, + nncf_config=mock_config["nncf_config"], + optimizer=SGD(model.parameters(), lr=0.01), + ) + runner.register_hook( + EvalHook( + dataloader, + save_best="accuracy", + file_client_args={"backend": "disk"}, + priority="LOW", + ) + ) + runner.register_hook(CompressionHook(ctrl)) + return runner diff --git a/tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_hooks.py b/tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_hooks.py new file mode 100644 index 00000000000..83b3255c9c5 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_hooks.py @@ -0,0 +1,53 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import tempfile +from mmcv.utils import get_logger + +from otx.algorithms.common.adapters.mmcv.nncf.hooks import CompressionHook +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmcv.nncf.test_helpers import ( + create_nncf_model, +) + + +class TestCompressionHook: + @e2e_pytest_unit + def test_after_train_iter(self): + class SimpleRunner: + def __init__(self): + self.logger = get_logger("mmcv") + self.rank = 0 + + runner = SimpleRunner() + with tempfile.TemporaryDirectory() as tempdir: + ctrl, _ = create_nncf_model(tempdir) + compression_hook = CompressionHook(ctrl) + compression_hook.after_train_iter(runner) + + @e2e_pytest_unit + def test_after_train_epoch(self): + class SimpleRunner: + def __init__(self): + self.logger = get_logger("mmcv") + self.rank = 0 + + runner = SimpleRunner() + with tempfile.TemporaryDirectory() as tempdir: + ctrl, _ = create_nncf_model(tempdir) + compression_hook = CompressionHook(ctrl) + compression_hook.after_train_epoch(runner) + + @e2e_pytest_unit + def test_before_run(self): + class SimpleRunner: + def __init__(self): + self.logger = get_logger("mmcv") + self.rank = 0 + + runner = SimpleRunner() + with tempfile.TemporaryDirectory() as tempdir: + ctrl, _ = create_nncf_model(tempdir) + compression_hook = CompressionHook(ctrl) + compression_hook.before_run(runner) diff --git a/tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_runners.py b/tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_runners.py new file mode 100644 index 00000000000..87520346559 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_runners.py @@ -0,0 +1,23 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import tempfile + +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmcv.nncf.test_helpers import ( + create_dataloader, + create_nncf_runner, +) + + +class TestAccuracyAwareRunner: + @e2e_pytest_unit + def test_run(self): + dataloader = create_dataloader() + + with tempfile.TemporaryDirectory() as tempdir: + runner = create_nncf_runner(tempdir) + runner.run([dataloader]) + assert [f for f in os.listdir(tempdir) if f.startswith("best_accuracy")] diff --git a/tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_utils.py b/tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_utils.py new file mode 100644 index 00000000000..759b214fc8f --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/nncf/test_mmcv_nncf_utils.py @@ -0,0 +1,173 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import tempfile +from functools import partial + +import numpy as np +import pytest +import torch +from mmcls.datasets.pipelines import Compose +from nncf.torch.nncf_network import NNCFNetwork + +from otx.algorithms.common.adapters.mmcv.nncf.utils import ( + get_fake_input, + model_eval, + wrap_nncf_model, +) +from otx.algorithms.common.adapters.nncf.compression import NNCFMetaState +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmcv.nncf.test_helpers import ( + create_config, + create_dataloader, + create_eval_fn, + create_model, +) + + +@e2e_pytest_unit +def test_get_fake_input(): + pipeline = Compose([{"type": "Resize", "size": (50, 50)}, {"type": "Collect", "keys": ["img"]}]) + + output = get_fake_input(pipeline) + assert torch.equal(output["img"][0], torch.zeros((1, 50, 50, 3), dtype=torch.uint8)) + + if torch.cuda.is_available(): + output = get_fake_input(pipeline, device="cuda") + assert torch.equal( + output["img"][0], + torch.zeros((1, 50, 50, 3), dtype=torch.uint8, device="cuda"), + ) + + output = get_fake_input(pipeline, np.zeros((128, 128, 3), dtype=np.uint8)) + assert torch.equal(output["img"][0], torch.zeros((1, 50, 50, 3), dtype=torch.uint8)) + + +@e2e_pytest_unit +def test_model_eval(): + mock_model = create_model() + mock_config = create_config() + mock_eval_fn = create_eval_fn() + dataloader = create_dataloader() + + if torch.cuda.is_available(): + model_eval( + mock_model, + config=mock_config, + val_dataloader=dataloader, + evaluate_fn=mock_eval_fn, + distributed=False, + ) + + with pytest.raises(RuntimeError): + model_eval( + mock_model, + config=mock_config, + val_dataloader=None, + evaluate_fn=mock_eval_fn, + distributed=False, + ) + + with pytest.raises(RuntimeError): + mock_config["nncf_config"]["target_metric_name"] = "failed" + model_eval( + mock_model, + config=mock_config, + val_dataloader=dataloader, + evaluate_fn=mock_eval_fn, + distributed=False, + ) + + +@e2e_pytest_unit +def test_wrap_nncf_model(): + mock_model = create_model() + mock_config = create_config() + mock_eval_fn = create_eval_fn() + dataloader = create_dataloader() + + pipeline = Compose( + [ + {"type": "Resize", "size": (50, 50)}, + {"type": "Normalize", "mean": [0, 0, 0], "std": [1, 1, 1]}, + {"type": "Collect", "keys": ["img"]}, + ] + ) + get_fake_input_fn = partial(get_fake_input, pipeline) + + model_eval_fn = partial( + model_eval, + config=mock_config, + val_dataloader=dataloader, + evaluate_fn=mock_eval_fn, + distributed=False, + ) + + with tempfile.TemporaryDirectory() as tempdir: + mock_config.nncf_config.log_dir = tempdir + ctrl, model = wrap_nncf_model( + mock_config, + mock_model, + model_eval_fn=model_eval_fn, + get_fake_input_fn=get_fake_input_fn, + dataloader_for_init=dataloader, + is_accuracy_aware=True, + ) + assert isinstance(model, NNCFNetwork) + + mock_model = create_model() + mock_config.nncf_config["input_info"] = {"sample_size": (1, 3, 128, 128)} + + with tempfile.TemporaryDirectory() as tempdir: + mock_config.nncf_config.log_dir = tempdir + ctrl, model = wrap_nncf_model( + mock_config, + mock_model, + model_eval_fn=model_eval_fn, + get_fake_input_fn=get_fake_input_fn, + dataloader_for_init=dataloader, + is_accuracy_aware=True, + ) + assert isinstance(model, NNCFNetwork) + mock_config.nncf_config.pop("input_info") + + with tempfile.TemporaryDirectory() as tempdir: + mock_model = create_model() + model_path = os.path.join(tempdir, "model.bin") + torch.save(mock_model.state_dict(), model_path) + mock_config.load_from = model_path + mock_config.nncf_config.log_dir = tempdir + ctrl, model = wrap_nncf_model( + mock_config, + mock_model, + model_eval_fn=model_eval_fn, + get_fake_input_fn=get_fake_input_fn, + dataloader_for_init=dataloader, + is_accuracy_aware=True, + ) + assert isinstance(model, NNCFNetwork) + mock_config.pop("load_from") + mock_config.nncf_config.pop("log_dir") + + init_state_dict = { + "meta": { + "nncf_enable_compression": True, + "nncf_meta": NNCFMetaState(compression_ctrl=ctrl.get_compression_state()), + }, + "state_dict": model.state_dict(), + } + mock_model = create_model() + + with tempfile.TemporaryDirectory() as tempdir: + mock_config.nncf_config.log_dir = tempdir + ctrl, model = wrap_nncf_model( + mock_config, + mock_model, + model_eval_fn=model_eval_fn, + get_fake_input_fn=get_fake_input_fn, + dataloader_for_init=dataloader, + init_state_dict=init_state_dict, + is_accuracy_aware=True, + ) diff --git a/tests/unit/algorithms/common/adapters/mmcv/ops/__init__.py b/tests/unit/algorithms/common/adapters/mmcv/ops/__init__.py new file mode 100644 index 00000000000..1344d3cacb2 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/ops/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common.adapters.mmcv.ops""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/mmcv/ops/test_multi_scale_deformable_attn_pytorch.py b/tests/unit/algorithms/common/adapters/mmcv/ops/test_multi_scale_deformable_attn_pytorch.py new file mode 100644 index 00000000000..9743b20a502 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/ops/test_multi_scale_deformable_attn_pytorch.py @@ -0,0 +1,20 @@ +"""Test for otx.algorithms.common.adapters.mmcv.ops.multi_scale_deformable_attn_pytorch.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch + +from otx.algorithms.common.adapters.mmcv.ops import multi_scale_deformable_attn_pytorch +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_multi_scale_deformable_attn_pytorch(): + value = torch.randn([1, 22223, 8, 32]) + value_spatial_shapes = torch.tensor([[100, 167], [50, 84], [25, 42], [13, 21]]) + sampling_locations = torch.randn([1, 2223, 8, 4, 4, 2]) + attention_weights = torch.randn([1, 2223, 8, 4, 4]) + + out = multi_scale_deformable_attn_pytorch(value, value_spatial_shapes, sampling_locations, attention_weights) + assert out.shape == torch.Size([1, 2223, 256]) diff --git a/tests/unit/algorithms/common/adapters/mmcv/pipelines/__init__.py b/tests/unit/algorithms/common/adapters/mmcv/pipelines/__init__.py new file mode 100644 index 00000000000..e946fcc52f5 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/pipelines/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common.adapters.mmcv.pipelines""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/mmcv/pipelines/test_load_image_from_otx_dataset.py b/tests/unit/algorithms/common/adapters/mmcv/pipelines/test_load_image_from_otx_dataset.py new file mode 100644 index 00000000000..67a706c9685 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/pipelines/test_load_image_from_otx_dataset.py @@ -0,0 +1,203 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import string +from unittest.mock import patch + +import numpy as np +import pytest +from torch.utils.data import DataLoader, Dataset + +from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.image import Image +from otx.core.data.caching import MemCacheHandlerSingleton +from otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset import ( + LoadImageFromOTXDataset, + LoadResizeDataFromOTXDataset, +) + + +@pytest.fixture +def fxt_data_list(): + np.random.seed(3003) + + num_data = 10 + h = w = key_len = 16 + + data_list = [] + for _ in range(num_data): + data = np.random.randint(0, 256, size=[h, w, 3], dtype=np.uint8) + key = "".join( + [string.ascii_lowercase[i] for i in np.random.randint(0, len(string.ascii_lowercase), size=[key_len])] + ) + meta = { + "key": key, + } + data_list += [(key, data, meta)] + + return data_list + + +@pytest.fixture +def fxt_caching_dataset_cls(fxt_data_list: list): + class CachingDataset(Dataset): + def __init__(self, enable_memcache: bool = True, load_resize: bool = False) -> None: + super().__init__() + self.d_items = [ + DatasetItemEntity( + media=Image(data=data), + annotation_scene=AnnotationSceneEntity(annotations=[], kind=AnnotationSceneKind.ANNOTATION), + ) + for _, data, _ in fxt_data_list + ] + if load_resize == False: + self.load = LoadImageFromOTXDataset(enable_memcache=enable_memcache) + else: + self.load = LoadResizeDataFromOTXDataset({}, enable_memcache=enable_memcache) + + def __len__(self): + return len(self.d_items) + + def __getitem__(self, index): + d_item = self.d_items[index] + + results = { + "dataset_item": d_item, + "height": d_item.media.numpy.shape[0], + "width": d_item.media.numpy.shape[1], + "index": index, + } + + results = self.load(results) + return results["img"] + + yield CachingDataset + + +def get_data_list_size(data_list): + size = 0 + for _, data, _ in data_list: + size += data.size + return size + + +class TestLoadImageFromFileWithCache: + @pytest.mark.parametrize("mode", ["singleprocessing", "multiprocessing"]) + def test_combine_with_dataloader(self, mode, fxt_caching_dataset_cls, fxt_data_list): + mem_size = get_data_list_size(fxt_data_list) + MemCacheHandlerSingleton.create(mode, mem_size) + + dataset = fxt_caching_dataset_cls() + + with patch( + "otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset.get_image", + side_effect=[data for _, data, _ in fxt_data_list], + ) as mock: + for _ in DataLoader(dataset): + continue + + # This initial round requires all data samples to be read from disk. + assert mock.call_count == len(dataset) + + with patch( + "otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset.get_image", + side_effect=[data for _, data, _ in fxt_data_list], + ) as mock: + for _ in DataLoader(dataset): + continue + + # The second round requires no read. + assert mock.call_count == 0 + + @pytest.mark.parametrize("mode", ["singleprocessing", "multiprocessing"]) + def test_disable_mem_cache(self, mode, fxt_caching_dataset_cls, fxt_data_list): + mem_size = get_data_list_size(fxt_data_list) + MemCacheHandlerSingleton.create(mode, mem_size) + + dataset = fxt_caching_dataset_cls(enable_memcache=False) + + with patch( + "otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset.get_image", + side_effect=[data for _, data, _ in fxt_data_list], + ) as mock: + for _ in DataLoader(dataset): + continue + + # This initial round requires all data samples to be read from disk. + assert mock.call_count == len(dataset) + + with patch( + "otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset.get_image", + side_effect=[data for _, data, _ in fxt_data_list], + ) as mock: + for _ in DataLoader(dataset): + continue + + # The second round goes the same due to no cache support + assert mock.call_count == len(dataset) + + +class TestLoadResizeDataFromOTXDataset: + def test_init_assertion_error(self): + with patch.object(LoadResizeDataFromOTXDataset, "_create_resize_op", return_value={}): + with pytest.raises(AssertionError): + op = LoadResizeDataFromOTXDataset( + load_ann_cfg=None, + resize_cfg={"size": [(1, 1), (2, 2)]}, + ) + + def test_disable_memcache(self, fxt_caching_dataset_cls, fxt_data_list): + dataset = fxt_caching_dataset_cls(enable_memcache=False, load_resize=True) + + with patch( + "otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset.get_image", + side_effect=[data for _, data, _ in fxt_data_list], + ): + with patch.object(dataset.load, "_get_unique_key") as mock: + for _ in DataLoader(dataset): + continue + assert mock.call_count == 0 + + def test_enable_memcache(self, fxt_caching_dataset_cls, fxt_data_list): + mem_size = get_data_list_size(fxt_data_list) + MemCacheHandlerSingleton.create("singleprocessing", mem_size) + + dataset = fxt_caching_dataset_cls(load_resize=True) + + with patch( + "otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset.get_image", + side_effect=[data for _, data, _ in fxt_data_list], + ) as mock: + for _ in DataLoader(dataset): + continue + + # This initial round requires all data samples to be read from disk. + assert mock.call_count == len(dataset) + + with patch( + "otx.algorithms.common.adapters.mmcv.pipelines.load_image_from_otx_dataset.get_image", + side_effect=[data for _, data, _ in fxt_data_list], + ) as mock: + for _ in DataLoader(dataset): + continue + + # The second round requires no read. + assert mock.call_count == 0 + + +@pytest.mark.parametrize("mode", ["singleprocessing", "multiprocessing"]) +def test_memcache_image_itemtype(mode): + img = (np.random.rand(10, 10, 3) * 255).astype(np.uint8) + MemCacheHandlerSingleton.create(mode, img.size * img.itemsize) + cache = MemCacheHandlerSingleton.get() + cache.put("img_u8", img) + img_cached, _ = cache.get("img_u8") + assert np.array_equal(img, img_cached) + img = np.random.rand(10, 10, 3).astype(np.float) + MemCacheHandlerSingleton.create(mode, img.size * img.itemsize) + cache = MemCacheHandlerSingleton.get() + cache.put("img_f32", img) + img_cached, _ = cache.get("img_f32") + assert np.array_equal(img, img_cached) diff --git a/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/__init__.py b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/__init__.py new file mode 100644 index 00000000000..e046173f05a --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common.adapters.mmcv.pipelines.transforms""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_augments.py b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_augments.py new file mode 100644 index 00000000000..c2ab35eda97 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_augments.py @@ -0,0 +1,102 @@ +"""Unit Tests for the OTX Dataset Pipelines Transforms Augments.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Any + +import pytest +from PIL import Image + +from otx.algorithms.common.adapters.mmcv.pipelines.transforms.augments import ( + Augments, + CythonAugments, +) + + +@pytest.fixture +def image() -> Image.Image: + return Image.new("RGB", (100, 100), color="red") + + +class TestAugment: + @pytest.mark.parametrize( + "augmentation_str, args", + [ + ("autocontrast", {}), + ("equalize", {}), + ("solarize", {"threshold": 128}), + ("posterize", {"bits_to_keep": 4}), + ("posterize", {"bits_to_keep": 8}), + ("color", {"factor": 0.5}), + ("contrast", {"factor": 0.5}), + ("brightness", {"factor": 0.5}), + ("sharpness", {"factor": 0.5}), + ("rotate", {"degree": 45}), + ("shear_x", {"factor": 0.5}), + ("shear_y", {"factor": 0.5}), + ("translate_x_rel", {"pct": 0.5}), + ("translate_y_rel", {"pct": 0.5}), + ], + ) + def test_augmentation_function(self, image: Image.Image, augmentation_str: str, args: dict[str, Any]) -> None: + """Test that the augmentation functions returns an Image object.""" + augmentation_func = getattr(Augments, augmentation_str) + result = augmentation_func(image, **args) + assert isinstance(result, Image.Image) + + def test_rotate_with_list_interpolation_instance(self, image: Image.Image) -> None: + """Test whether list of interpolation instances are accepted.""" + result = Augments.rotate(image, 45, resample=[Image.BICUBIC, Image.BILINEAR]) + assert isinstance(result, Image.Image) + + +class TestCythonAugments: + @pytest.mark.parametrize( + "augmentation_str, args", + [ + ("autocontrast", {}), + ("equalize", {}), + ("solarize", {"threshold": 128}), + ("posterize", {"bits_to_keep": 4}), + ("posterize", {"bits_to_keep": 8}), + ("color", {"factor": 0.5}), + ("contrast", {"factor": 0.5}), + ("brightness", {"factor": 0.5}), + ("sharpness", {"factor": 0.5}), + ], + ) + def test_augmentation_output_equals_to_input_image( + self, image: Image.Image, augmentation_str: str, args: dict[str, Any] + ) -> None: + """Test that the augmentation functions returns an Image object.""" + augmentation_func = getattr(CythonAugments, augmentation_str) + result = augmentation_func(image, **args) + assert isinstance(result, Image.Image) + assert result == image + + @pytest.mark.parametrize( + "augmentation_str, args", + [ + ("rotate", {"degree": 45}), + ("shear_x", {"factor": 0.5}), + ("shear_y", {"factor": 0.5}), + ("translate_x_rel", {"pct": 0.5}), + ("translate_y_rel", {"pct": 0.5}), + ], + ) + def test_augmentation_output_not_equals_to_input_image( + self, image: Image.Image, augmentation_str: str, args: dict[str, Any] + ) -> None: + """Test that the augmentation functions returns an Image object.""" + augmentation_func = getattr(CythonAugments, augmentation_str) + result = augmentation_func(image, **args) + assert isinstance(result, Image.Image) + assert result != image + + def test_blend(self, image: Image.Image) -> None: + """Test that it raises an assertion error if dst is not a numpy array.""" + with pytest.raises(AssertionError): + CythonAugments.blend(image, image, 0.5) diff --git a/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_augmix.py b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_augmix.py new file mode 100644 index 00000000000..7daf3182f80 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_augmix.py @@ -0,0 +1,118 @@ +"""Unit Tests for the OTX Dataset Pipelines Transforms Augments.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +import random + +import numpy as np +import pytest +from PIL import Image + +from otx.algorithms.classification.adapters.mmcls.datasets.pipelines.transforms.augmix import ( + AugMixAugment, + OpsFabric, +) +from otx.algorithms.common.adapters.mmcv.pipelines.transforms.augments import ( + CythonAugments, +) + + +@pytest.fixture +def ops_fabric() -> OpsFabric: + return OpsFabric("Rotate", 5, {"img_mean": 128}) + + +@pytest.mark.xfail(reason="random may not return the same value on different machines.") +class TestOpsFabric: + def test_init(self, ops_fabric: OpsFabric) -> None: + """Test OpsFabric initialization.""" + assert ops_fabric.prob == 1.0 + assert ops_fabric.hparams == {"img_mean": 128} + assert ops_fabric.aug_kwargs == { + "fillcolor": 128, + "resample": (Image.BILINEAR, Image.BICUBIC), + } + assert ops_fabric.aug_factory.magnitude == 5 + assert ops_fabric.aug_factory.magnitude_std == float("inf") + assert ops_fabric.aug_factory.level_fn == ops_fabric._rotate_level_to_arg + assert ops_fabric.aug_factory.aug_fn == CythonAugments.rotate + + def test_randomly_negate(self) -> None: + """Test randomly_negate function.""" + random.seed(1234) + assert OpsFabric.randomly_negate(5) == -5 + assert OpsFabric.randomly_negate(5) == 5 + assert OpsFabric.randomly_negate(5) == -5 + + def test_rotate_level_to_arg(self, ops_fabric: OpsFabric) -> None: + """Test rotate_level_to_arg function.""" + assert ops_fabric._rotate_level_to_arg(0, ops_fabric.hparams) == (0,) + assert ops_fabric._rotate_level_to_arg(5, ops_fabric.hparams) == (5 / 10 * 30,) + + def test_enhance_increasing_level_to_arg(self, ops_fabric: OpsFabric) -> None: + """Test enhance_increasing_level_to_arg function.""" + assert ops_fabric._enhance_increasing_level_to_arg(0, ops_fabric.hparams) == (1.0,) + assert ops_fabric._enhance_increasing_level_to_arg(5, ops_fabric.hparams) == (1.0 + 5 / 10 * 0.9,) + + def test_shear_level_to_arg(self, ops_fabric: OpsFabric) -> None: + """Test shear_level_to_arg function.""" + assert ops_fabric._shear_level_to_arg(0, ops_fabric.hparams) == (0,) + assert ops_fabric._shear_level_to_arg(5, ops_fabric.hparams) == (5 / 10 * 0.3,) + + def test_translate_rel_level_to_arg(self, ops_fabric: OpsFabric) -> None: + """Test translate_rel_level_to_arg function.""" + assert ops_fabric._translate_rel_level_to_arg(0, ops_fabric.hparams) == (0,) + assert ops_fabric._translate_rel_level_to_arg(5, {"translate_pct": 0.5}) == (5 / 10 * 0.5,) + + def test_posterize_increasing_level_to_arg(self, ops_fabric: OpsFabric) -> None: + """Test posterize_increasing_level_to_arg function.""" + assert ops_fabric._posterize_increasing_level_to_arg(0, ops_fabric.hparams) == (4,) + assert ops_fabric._posterize_increasing_level_to_arg(5, ops_fabric.hparams) == (4 - int(5 / 10 * 4),) + + def test_solarize_increasing_level_to_arg(self, ops_fabric: OpsFabric) -> None: + """Test solarize_increasing_level_to_arg function.""" + assert ops_fabric._solarize_increasing_level_to_arg(0, ops_fabric.hparams) == (0,) + assert ops_fabric._solarize_increasing_level_to_arg(5, ops_fabric.hparams) == (256 - int(5 / 10 * 256),) + + def test_call(self, ops_fabric: OpsFabric) -> None: + """Test __call__ function.""" + img = Image.new("RGB", (256, 256)) + transformed_img = ops_fabric(img) + assert transformed_img != img # make sure the image was actually transformed + + +class TestAugMixAugment: + def test_init(self) -> None: + """Test AugMixAugment initialization.""" + aug_mix_augment = AugMixAugment(config_str="augmix-m5-w3") + assert isinstance(aug_mix_augment, AugMixAugment) + assert len(aug_mix_augment.ops) > 0 + + def test_apply_basic(self) -> None: + """Test _apply_basic function.""" + aug_mix_augment = AugMixAugment(config_str="augmix-m5-w3") + + img = Image.new("RGB", (224, 224), color=(255, 0, 0)) + mixing_weights = np.float32(np.random.dirichlet([aug_mix_augment.alpha] * aug_mix_augment.width)) + m = np.float32(np.random.beta(aug_mix_augment.alpha, aug_mix_augment.alpha)) + + mixed_img = aug_mix_augment._apply_basic(img, mixing_weights, m) + assert isinstance(mixed_img, Image.Image) + + def test_augmix_ops(self) -> None: + """Test augmix_ops function.""" + aug_mix_augment = AugMixAugment(config_str="augmix-m5-w3") + assert len(aug_mix_augment.ops) > 0 + assert isinstance(aug_mix_augment.alpha, float) + assert isinstance(aug_mix_augment.width, int) + assert isinstance(aug_mix_augment.depth, int) + + def test_call(self) -> None: + """Test __call__ method.""" + aug_mix_augment = AugMixAugment(config_str="augmix-m5-w3") + data = {"img": np.random.randint(0, 255, size=(224, 224, 3)).astype(np.uint8)} + results = aug_mix_augment(data) + assert "augmix" in results + assert isinstance(results["img"], Image.Image) diff --git a/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_otx_transforms.py b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_otx_transforms.py new file mode 100644 index 00000000000..2acff8f8e61 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_otx_transforms.py @@ -0,0 +1,56 @@ +"""Unit Tests for the OTX Dataset Pipelines OTX Transforms.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from __future__ import annotations + +import pytest +import torch +from PIL import Image +from torchvision.transforms import functional as F + +from otx.algorithms.classification.adapters.mmcls.datasets.pipelines.transforms.otx_transforms import ( + PILToTensor, + RandomRotate, + TensorNormalize, +) + + +@pytest.fixture +def data() -> dict[str, list[str] | Image]: + # a sample result dictionary to use in tests + return { + "img_fields": ["img"], + "img": Image.new(mode="RGB", size=(224, 224), color="red"), + } + + +def test_PILToTensor(data: dict[str, list[str] | Image]) -> None: + """Test PILToTensor transform.""" + transform = PILToTensor() + result = transform(data.copy()) # copy to avoid modifying the original data + + assert result["PILToTensor"] is True + assert result["img_fields"] == ["img"] + assert torch.equal(result["img"], F.to_tensor(data["img"])) + + +def test_TensorNormalize(data: dict[str, list[str] | Image]) -> None: + """Test TensorNormalize transform.""" + mean, std = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] + data = PILToTensor()(data) # convert to tensor + transform = TensorNormalize(mean=mean, std=std) + result = transform(data.copy()) # copy to avoid modifying the original data + + assert result["TensorNormalize"] is True + assert result["img_fields"] == ["img"] + assert torch.equal(result["img"], F.normalize(data["img"], mean, std)) + + +def test_RandomRotate(data: dict[str, list[str] | Image]) -> None: + """Test RandomRotate transform.""" + transform = RandomRotate(p=1.0, angle=(-10, 10)) + result = transform(data.copy()) # copy to avoid modifying the original data. + assert result["img"] != data["img"] # image should be rotated diff --git a/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_random_augment.py b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_random_augment.py new file mode 100644 index 00000000000..37f78bfa9ba --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_random_augment.py @@ -0,0 +1,79 @@ +"""Unit Tests for the OTX Dataset Pipelines - Random Augment.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +import numpy as np +import pytest +from PIL import Image + +from otx.algorithms.classification.adapters.mmcls.datasets.pipelines.transforms.random_augment import ( + OTXRandAugment, + cutout_abs, + rand_augment_pool, +) + + +@pytest.fixture +def sample_np_image() -> np.ndarray: + return np.ones((256, 256, 3), dtype=np.uint8) + + +@pytest.fixture +def sample_pil_image() -> Image: + return Image.new("RGB", (256, 256), (255, 255, 255)) + + +def test_all_transforms_return_valid_image(sample_pil_image: Image.Image) -> None: + """Test all transforms return valid image.""" + for transform, value, max_value in rand_augment_pool: + img, *extra = transform(sample_pil_image, value=value, max_value=max_value) + assert isinstance(img, Image.Image) + assert img.size == sample_pil_image.size + + +def test_cutoutabs_transform(sample_pil_image: Image.Image) -> None: + """Test cutout_abs transform.""" + img, (x0, y0, x1, y1), color = cutout_abs(sample_pil_image, 2) + assert isinstance(img, Image.Image) + assert img.size == sample_pil_image.size + assert x0 >= 0 and y0 >= 0 + assert x1 <= sample_pil_image.width and y1 <= sample_pil_image.height + assert color == (127, 127, 127) + + +class TestOTXRandAugment: + def test_with_default_arguments(self, mocker, sample_np_image: np.ndarray) -> None: + """Test case with default arguments.""" + mocker.patch("random.random", return_value=0.1) # RandAugment is applied only when random.random() < 0.5 + transform = OTXRandAugment(num_aug=2, magnitude=5, cutout_value=16) + data = {"img": sample_np_image} + results = transform(data) + + assert isinstance(results["img"], np.ndarray) + assert any(item.startswith("rand_mc_") for item in results.keys()) + assert "CutoutAbs" in results + + def test_with_img_fields_argument(self, mocker, sample_np_image: np.ndarray) -> None: + """Test case with img_fields argument.""" + mocker.patch("random.random", return_value=0.1) # RandAugment is applied only when random.random() < 0.5 + transform = OTXRandAugment(num_aug=2, magnitude=5, cutout_value=16) + data = { + "img1": sample_np_image, + "img2": sample_np_image, + "img_fields": ["img1"], + } + results = transform(data) + assert isinstance(results["img1"], np.ndarray) + assert any(item.startswith("rand_mc_") for item in results.keys()) + assert "CutoutAbs" in results + + def test_with_pil_image_input(self, sample_pil_image: Image.Image) -> None: + """Test case with PIL.Image input.""" + transform = OTXRandAugment(num_aug=2, magnitude=5, cutout_value=16) + data = {"img": sample_pil_image} + results = transform(data) + + assert isinstance(results["img"], np.ndarray) + assert "CutoutAbs" in results diff --git a/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_twocrop_transform.py b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_twocrop_transform.py new file mode 100644 index 00000000000..b6d65916d8b --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/pipelines/transforms/test_twocrop_transform.py @@ -0,0 +1,45 @@ +"""Unit Tests for the OTX Dataset Pipelines Transforms - Two Crop.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +import numpy as np +from mmcls.datasets.builder import PIPELINES +from mmcls.datasets.pipelines import Compose +from mmcv.utils import build_from_cfg + +from otx.algorithms.classification.adapters.mmcls.datasets.pipelines.transforms.twocrop_transform import ( + TwoCropTransform, +) + + +def test_TwoCropTransform() -> None: + """Test the TwoCropTransform instance.""" + # Data to be transformed + data = {} + data["img"] = np.ones((224, 224, 3), dtype=np.uint8) + data["gt_label"] = 0 + + # Pipeline to be used for transformation + pipeline = [ + dict(type="Resize", size=(256, 256)), + dict(type="RandomCrop", size=(224, 224)), + dict( + type="Normalize", + mean=[123.675, 116.28, 103.53], + std=[58.395, 57.12, 57.375], + ), + dict(type="ToTensor", keys=["img"]), + ] + + # Create TwoCropTransform instance + transform = TwoCropTransform(pipeline) + transform_pipeline = Compose([build_from_cfg(p, PIPELINES) for p in pipeline]) + transform.pipeline1 = transform_pipeline + transform.pipeline2 = transform_pipeline + + # Test the TwoCropTransform instance + transformed_data = transform(data) + assert isinstance(transformed_data, dict) + assert transformed_data["img"].shape == (2, 224, 224, 3) diff --git a/tests/unit/algorithms/common/adapters/mmcv/tasks/test_exporter.py b/tests/unit/algorithms/common/adapters/mmcv/tasks/test_exporter.py new file mode 100644 index 00000000000..6544f25be50 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/tasks/test_exporter.py @@ -0,0 +1,76 @@ +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +import mmcv +import pytest + +from otx.algorithms.common.adapters.mmcv.tasks.exporter import Exporter +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestExporter: + @pytest.fixture(autouse=True) + def setup(self, mocker): + def mock_init_logger(): + pass + + def mock_configure(model_cfg, model_ckpt, data_cfg, training=False, **kwargs): + return mmcv.ConfigDict() + + self.exporter = Exporter() + self.exporter._init_logger = mock_init_logger + self.fake_config = mmcv.ConfigDict(work_dir="/path/work_dir", data=dict(test=dict(dataset=mocker.MagicMock()))) + mocker.patch("os.listdir") + + @e2e_pytest_unit + def test_run_with_error_raise(self): + return_value = self.exporter.run(self.fake_config) + + assert "outputs" in return_value + assert return_value["outputs"] is None + assert "msg" in return_value + + @e2e_pytest_unit + def test_run_without_deploy_cfg(self, mocker): + def mock_naive_export(output_dir, model_builder, precision, export_type, cfg, model_name="model"): + pass + + self.exporter.naive_export = mock_naive_export + return_value = self.exporter.run(self.fake_config) + + assert "outputs" in return_value + assert return_value["outputs"]["bin"] == "/path/work_dir/model.bin" + assert return_value["outputs"]["xml"] == "/path/work_dir/model.xml" + assert "msg" in return_value + assert return_value["msg"] == "" + + @e2e_pytest_unit + def test_run_with_deploy_cfg(self, mocker): + def mock_mmdeploy_export( + output_dir, model_builder, precision, export_type, cfg, deploy_cfg, model_name="model" + ): + pass + + self.exporter.mmdeploy_export = mock_mmdeploy_export + return_value = self.exporter.run(self.fake_config, deploy_cfg=mmcv.ConfigDict()) + + assert "outputs" in return_value + assert return_value["outputs"]["bin"] == "/path/work_dir/model.bin" + assert return_value["outputs"]["xml"] == "/path/work_dir/model.xml" + assert "msg" in return_value + assert return_value["msg"] == "" + + @e2e_pytest_unit + def test_mmdeploy_export(self, mocker): + from otx.algorithms.common.adapters.mmdeploy.apis import MMdeployExporter + + mock_export_openvino = mocker.patch.object(MMdeployExporter, "export2backend") + + Exporter.mmdeploy_export( + "", + None, + "FP16", + ExportType.OPENVINO, + dict(), + mmcv.ConfigDict(backend_config=dict(mo_options=dict(flags=[]))), + ) + + mock_export_openvino.assert_called_once() diff --git a/tests/unit/algorithms/common/adapters/mmcv/tasks/test_helpers.py b/tests/unit/algorithms/common/adapters/mmcv/tasks/test_helpers.py new file mode 100644 index 00000000000..b89ec740b1e --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/tasks/test_helpers.py @@ -0,0 +1,33 @@ +"""Test helpers for OTX.""" +import torch +import torch.nn as nn + + +def generate_random_torch_image(batch=1, width=3, height=3, channels=3, channel_last=False): + """Generate random torch tensor image. + + Args: + batch (int, optional): A size of batch. Defaults to 1. + width (int, optional): the image width. Defaults to 224. + height (int, optional): the image height. Defaults to 224. + channels (int, optional): the image channel. Defaults to 3. + channel_last (bool, optional): if this is True, image shape will follow BHWC. Defaults to True. + + Returns: + torch.tensor: random image tensor. + """ + if channel_last is False: + img = torch.rand(batch, channels, height, width) + else: + img = torch.rand(batch, height, width, channels) + return img + + +def generate_toy_cnn_model(in_channels=3, mid_channels=3, out_channels=3): + return nn.Sequential( + nn.Conv2d(in_channels, mid_channels, (1, 1)), nn.BatchNorm2d(mid_channels), nn.AdaptiveAvgPool2d((1, 1)) + ) + + +def generate_toy_head(in_features, out_features): + return nn.Linear(in_features, out_features) diff --git a/tests/unit/algorithms/common/adapters/mmcv/tasks/test_version.py b/tests/unit/algorithms/common/adapters/mmcv/tasks/test_version.py new file mode 100644 index 00000000000..301a4d48971 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/tasks/test_version.py @@ -0,0 +1,9 @@ +from otx.algorithms.common.adapters.mmcv.tasks.version import __version__, get_version +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_version(): + return_value = get_version() + + assert return_value == __version__ diff --git a/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py b/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py new file mode 100644 index 00000000000..954dc37482c --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py @@ -0,0 +1,70 @@ +"""Test for otx.algorithms.common.adapters.mmcv.configurer""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from mmcv.utils import Config +from otx.algorithms.common.adapters.mmcv import configurer +from otx.algorithms.common.adapters.mmcv.utils.config_utils import InputSizeManager +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +class TestBaseConfigurer: + def test_get_input_size_to_fit_dataset(self, mocker): + cfg = Config({"data": {"train": {"otx_dataset": None}}}) + input_size_manager = InputSizeManager(cfg) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset(cfg, input_size_manager) + assert input_size is None + + cfg = Config({"data": {"train": {"otx_dataset": True}}}) + input_size_manager = InputSizeManager(cfg, base_input_size=512) + mock_stat = mocker.patch.object(configurer, "compute_robust_dataset_statistics") + + mock_stat.return_value = {} + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset(cfg, input_size_manager) + assert input_size is None + + mock_stat.return_value = dict( + image=dict(robust_max=150), + ) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset(cfg, input_size_manager) + assert input_size == (128, 128) + + mock_stat.return_value = dict( + image=dict(robust_max=150), + annotation=dict(), + ) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset( + cfg, input_size_manager, use_annotations=True + ) + assert input_size == (128, 128) + + mock_stat.return_value = dict( + image=dict(robust_max=256), + annotation=dict(size_of_shape=dict(robust_min=64)), + ) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset( + cfg, input_size_manager, use_annotations=True + ) + assert input_size == (256, 256) + + mock_stat.return_value = dict( + image=dict(robust_max=1024), + annotation=dict(size_of_shape=dict(robust_min=64)), + ) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset( + cfg, input_size_manager, use_annotations=True + ) + assert input_size == (512, 512) + + mock_stat.return_value = dict( + image=dict(robust_max=2045), + annotation=dict(size_of_shape=dict(robust_min=64)), + ) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset( + cfg, input_size_manager, use_annotations=True + ) + assert input_size == (512, 512) diff --git a/tests/unit/algorithms/common/adapters/mmcv/utils/__init__.py b/tests/unit/algorithms/common/adapters/mmcv/utils/__init__.py new file mode 100644 index 00000000000..68495dde18a --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/utils/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common.adapters.mmcv.utils""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/mmcv/utils/test_automatic_bs.py b/tests/unit/algorithms/common/adapters/mmcv/utils/test_automatic_bs.py new file mode 100644 index 00000000000..8fd3122d5bc --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/utils/test_automatic_bs.py @@ -0,0 +1,160 @@ +from otx.algorithms.common.utils.utils import is_xpu_available +import pytest +from math import sqrt + +from otx.algorithms.common.adapters.mmcv.utils import automatic_bs +from otx.algorithms.common.adapters.mmcv.utils import adapt_batch_size +from otx.algorithms.common.adapters.mmcv.utils.automatic_bs import SubDataset + +DEFAULT_BS = 8 +DEFAULT_LR = 0.001 +TRAINSET_SIZE = 100 + + +class MockBsSearchAlgo: + def __init__(self, train_func, default_bs: int, max_bs: int): + self.train_func = train_func + self.default_bs = default_bs + self.max_bs = max_bs + + def auto_decrease_batch_size(self): + self.train_func(self.default_bs) + self.train_func(self.default_bs // 2) + return self.default_bs // 2 + + def find_big_enough_batch_size(self, drop_last: bool): + self.train_func(self.default_bs) + self.train_func(self.default_bs + 2) + return self.default_bs + 2 + + +@pytest.fixture +def mock_adapt_algo_cls(mocker): + return mocker.patch.object(automatic_bs, "BsSearchAlgo", side_effect=MockBsSearchAlgo) + + +@pytest.fixture +def common_cfg(mocker): + mock_cfg = mocker.MagicMock() + mock_cfg.runner = {"type": "EpochRunnerWithCancel", "max_epochs": 100} + mock_cfg.custom_hooks = [ + {"type": "AdaptiveTrainSchedulingHook", "enable_eval_before_run": True}, + {"type": "OTXProgressHook"}, + ] + mock_cfg.optimizer.lr = DEFAULT_LR + return mock_cfg + + +def set_mock_cfg_not_action(common_cfg): + common_cfg.data.train_dataloader = {"samples_per_gpu": DEFAULT_BS} + return common_cfg + + +def set_mock_cfg_action(common_cfg): + common_cfg.data.videos_per_gpu = DEFAULT_BS + common_cfg.domain = "ACTION_CLASSIFICATION" + return common_cfg + + +@pytest.fixture +def mock_dataset(mocker): + mock_ds = [mocker.MagicMock()] + mock_ds[0].__len__.return_value = TRAINSET_SIZE + return mock_ds + + +@pytest.mark.parametrize("not_increase", [True, False]) +@pytest.mark.parametrize("is_action_task", [True, False]) +@pytest.mark.parametrize("is_iter_based_runner", [True, False]) +def test_adapt_batch_size( + mocker, mock_adapt_algo_cls, common_cfg, mock_dataset, not_increase, is_action_task, is_iter_based_runner +): + if is_xpu_available(): + pytest.skip("Adaptive batch size is not supported on XPU") + # prepare + mock_train_func = mocker.MagicMock() + new_bs = DEFAULT_BS // 2 if not_increase else DEFAULT_BS + 2 + + max_eph_name = "max_epochs" + if is_iter_based_runner: + common_cfg.runner = {"type": "IterBasedRunnerWithCancel", "max_iters": 100} + max_eph_name = "max_iters" + + if is_action_task: + mock_config = set_mock_cfg_action(common_cfg) + else: + mock_config = set_mock_cfg_not_action(common_cfg) + + # execute + adapt_batch_size(mock_train_func, mock_config, mock_dataset, False, not_increase) + + # check adapted batch size is applied + if is_action_task: + assert mock_config.data.videos_per_gpu == new_bs + else: + assert mock_config.data.train_dataloader["samples_per_gpu"] == new_bs + # check leanring rate is updated depending on adapted batch size + bs_change_ratio = new_bs / DEFAULT_BS + assert mock_config.optimizer.lr == pytest.approx(DEFAULT_LR * sqrt(bs_change_ratio)) + # check adapt function gets proper arguments + assert mock_adapt_algo_cls.call_args.kwargs["default_bs"] == DEFAULT_BS + assert mock_adapt_algo_cls.call_args.kwargs["max_bs"] == TRAINSET_SIZE + # check length of dataset is decreased to reduce time + assert len(mock_train_func.call_args_list[0].kwargs["dataset"][0]) == DEFAULT_BS + assert len(mock_train_func.call_args_list[1].kwargs["dataset"][0]) == new_bs + # check max epoch is set as 1 to reduce time + assert mock_train_func.call_args_list[0].kwargs["cfg"].runner[max_eph_name] == 1 + assert mock_train_func.call_args_list[1].kwargs["cfg"].runner[max_eph_name] == 1 + # check eval before run is disabled to reduce time + assert not mock_train_func.call_args_list[0].kwargs["cfg"].custom_hooks[0]["enable_eval_before_run"] + assert not mock_train_func.call_args_list[1].kwargs["cfg"].custom_hooks[0]["enable_eval_before_run"] + # check OTXProgressHook is removed + assert len(mock_train_func.call_args_list[0].kwargs["cfg"].custom_hooks) == 1 + + +def test_adapt_batch_size_no_gpu(mocker, common_cfg, mock_dataset): + # prepare + mock_train_func = mocker.MagicMock() + mock_config = set_mock_cfg_not_action(common_cfg) + mocker.patch.object(automatic_bs, "cuda_available", return_value=False) + + # execute + adapt_batch_size(mock_train_func, mock_config, mock_dataset, False, True) + + # check train function ins't called. + mock_train_func.assert_not_called() + + +class TestSubDataset: + @pytest.fixture(autouse=True) + def set_up(self, mocker): + self.num_samples = 3 + self.fullset = mocker.MagicMock() + self.sub_dataset = SubDataset(self.fullset, self.num_samples) + + def test_init(self, mocker): + fullset = mocker.MagicMock() + subset = SubDataset(fullset, 3) + + # test for class incremental case. If below assert can't be passed, ClsIncrSampler can't work well. + assert len(subset.img_indices["new"]) / len(subset.img_indices["old"]) + 1 <= self.num_samples + + @pytest.mark.parametrize("num_samples", [-1, 0]) + def test_init_w_wrong_num_samples(self, mocker, num_samples): + fullset = mocker.MagicMock() + with pytest.raises(ValueError): + SubDataset(fullset, num_samples) + + def test_len(self): + assert len(self.sub_dataset) == self.num_samples + + def test_getitem(self): + self.sub_dataset[0] + self.fullset.__getitem__.assert_called_once_with(0) + + def test_getattr(self): + self.fullset.data = "data" + assert self.sub_dataset.data == "data" + + def test_flag(self): + assert len(self.sub_dataset.flag) == self.num_samples diff --git a/tests/unit/algorithms/common/adapters/mmcv/utils/test_config_utils.py b/tests/unit/algorithms/common/adapters/mmcv/utils/test_config_utils.py new file mode 100644 index 00000000000..9e1a43528dc --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/utils/test_config_utils.py @@ -0,0 +1,506 @@ +import re + +import pytest + +from otx.algorithms.common.adapters.mmcv.utils import config_utils +from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + patch_persistent_workers, + get_adaptive_num_workers, + InputSizeManager, + get_proper_repeat_times, +) +from otx.algorithms.common.configs.configuration_enums import InputSizePreset + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def get_subset_data_cfg(workers_per_gpu: int = 2) -> dict: + data_cfg = {} + for subset in ["train", "val", "test", "unlabeled"]: + data_cfg[subset] = "fake" + data_cfg[f"{subset}_dataloader"] = {"persistent_workers": True, "workers_per_gpu": workers_per_gpu} + + return data_cfg + + +@e2e_pytest_unit +@pytest.mark.parametrize("workers_per_gpu", [0, 2]) +def test_patch_persistent_workers(mocker, workers_per_gpu): + data_cfg = get_subset_data_cfg(workers_per_gpu) + config = mocker.MagicMock() + config.data = data_cfg + + mock_torch = mocker.patch.object(config_utils, "torch") + mock_torch.distributed.is_initialized.return_value = False + + patch_persistent_workers(config) + + for subset in ["train", "val", "test", "unlabeled"]: + # check persistent_workers is turned off if workers_per_gpu is 0 + assert data_cfg[f"{subset}_dataloader"]["persistent_workers"] is (workers_per_gpu != 0) + # check pin_memory is automatically set if it doesn't exist + assert data_cfg[f"{subset}_dataloader"]["pin_memory"] is True + + +@e2e_pytest_unit +def test_patch_persistent_workers_dist_semisl(mocker): + data_cfg = get_subset_data_cfg() + config = mocker.MagicMock() + config.data = data_cfg + + mock_torch = mocker.patch.object(config_utils, "torch") + mock_torch.distributed.is_initialized.return_value = True + + patch_persistent_workers(config) + + for subset in ["train", "val", "test", "unlabeled"]: + # check persistent_workers is turned off in distributed training semi-SL case + assert data_cfg[f"{subset}_dataloader"]["persistent_workers"] is False + # check pin_memory is automatically set if it doesn't exist + assert data_cfg[f"{subset}_dataloader"]["pin_memory"] is True + + +@e2e_pytest_unit +@pytest.mark.parametrize("num_dataloader", [1, 2, 4]) +def test_get_adaptive_num_workers(mocker, num_dataloader): + num_gpu = 5 + mock_torch = mocker.patch.object(config_utils, "torch") + mocker.patch.object(config_utils, "is_xpu_available", return_value=False) + mock_torch.cuda.device_count.return_value = num_gpu + + num_cpu = 20 + mock_multiprocessing = mocker.patch.object(config_utils, "multiprocessing") + mock_multiprocessing.cpu_count.return_value = num_cpu + + assert get_adaptive_num_workers(num_dataloader) == num_cpu // (num_gpu * num_dataloader) + + +@e2e_pytest_unit +def test_get_adaptive_num_workers_no_gpu(mocker): + num_gpu = 0 + mock_torch = mocker.patch.object(config_utils, "torch") + mocker.patch.object(config_utils, "is_xpu_available", return_value=False) + mock_torch.cuda.device_count.return_value = num_gpu + + num_cpu = 20 + mock_multiprocessing = mocker.patch.object(config_utils, "multiprocessing") + mock_multiprocessing.cpu_count.return_value = num_cpu + + assert get_adaptive_num_workers() is None + + +@e2e_pytest_unit +@pytest.mark.parametrize("num_dataloader", [1, 2, 4]) +def test_get_adaptive_num_workers_xpu(mocker, num_dataloader): + num_gpu = 5 + mock_torch = mocker.patch.object(config_utils, "torch") + mocker.patch.object(config_utils, "is_xpu_available", return_value=True) + mock_torch.xpu.device_count.return_value = num_gpu + + num_cpu = 20 + mock_multiprocessing = mocker.patch.object(config_utils, "multiprocessing") + mock_multiprocessing.cpu_count.return_value = num_cpu + + assert get_adaptive_num_workers(num_dataloader) == num_cpu // (num_gpu * num_dataloader) + + +@pytest.fixture +def mock_data_pipeline(): + image_size = (400, 400) + + return [ + dict(type="Mosaic", img_scale=image_size), + dict( + type="RandomAffine", + border=image_size, + ), + dict(type="Resize", img_scale=image_size, keep_ratio=True), + dict(type="Pad", pad_to_square=True), + dict( + type="MultiScaleFlipAug", + img_scale=image_size, + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Pad", size=image_size), + ], + ), + dict(type="RandomResizedCrop", size=image_size[0], efficientnet_style=True), + dict( + type="AutoAugment", + policies=[ + [ + dict( + type="Resize", + img_scale=[ + image_size, + ], + multiscale_mode="value", + keep_ratio=True, + ) + ], + [ + dict( + type="Resize", + img_scale=[ + image_size, + ], + multiscale_mode="value", + keep_ratio=True, + ), + dict(type="RandomCrop", crop_type="absolute_range", crop_size=image_size), + dict( + type="Resize", + img_scale=[ + image_size, + ], + multiscale_mode="value", + override=True, + keep_ratio=True, + ), + ], + ], + ), + dict( + type="TwoCropTransform", + view0=[ + dict(type="RandomResizedCrop", size=image_size), + ], + view1=[ + dict(type="RandomResizedCrop", size=image_size), + ], + pipeline=[ + dict(type="Resize", size=image_size), + ], + ), + ] + + +mock_data_pipeline_to_estimate = { + "pad_smaller_than_resize": { + "pipeline": [ + dict(type="Resize", img_scale=(300, 300), keep_ratio=True), + dict(type="Pad", size=(200, 200)), + ], + "input_size": (300, 300), + }, + "crop_bigger_than_resize": { + "pipeline": [ + dict(type="Resize", img_scale=(300, 300), keep_ratio=True), + dict(type="crop", size=(400, 400)), + ], + "input_size": (300, 300), + }, + "multi_scale_flip_aug": { + "pipeline": [ + dict( + type="MultiScaleFlipAug", + img_scale=(232, 232), + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Pad", size=(500, 500)), + ], + ), + ], + "input_size": (500, 500), + }, + "multi_scale_flip_aug_pad_bigger_than_resize": { + "pipeline": [ + dict( + type="MultiScaleFlipAug", + img_scale=(232, 232), + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=True), + dict(type="RandomFlip"), + dict(type="Pad", size=(200, 200)), + ], + ), + ], + "input_size": (232, 232), + }, + "resize_crop_pad": { + "pipeline": [ + dict(type="Resize", img_scale=(300, 300), keep_ratio=True), + dict(type="crop", size=(200, 200)), + dict(type="Pad", size=(400, 400)), + ], + "input_size": (400, 400), + }, + "auto_augment": { + "pipeline": [ + dict( + type="AutoAugment", + policies=[ + [ + dict( + type="Resize", + img_scale=[ + (500, 500), + (900, 900), + ], + multiscale_mode="value", + keep_ratio=True, + ) + ], + [ + dict( + type="Resize", + img_scale=[(600, 600), (400, 400)], + multiscale_mode="value", + keep_ratio=True, + ), + dict(type="RandomCrop", crop_type="absolute_range", crop_size=(200, 300)), + dict( + type="Resize", + img_scale=[ + (100, 200), + ], + multiscale_mode="value", + override=True, + keep_ratio=True, + ), + ], + ], + ), + ], + "input_size": (500, 500), + }, + "two_crop_transform": { + "pipeline": [ + dict(type="Resize", img_scale=(300, 300), keep_ratio=True), + dict(type="crop", size=(200, 200)), + dict(type="Pad", size=(400, 400)), + dict( + type="TwoCropTransform", + view0=[ + dict(type="RandomResizedCrop", size=(600, 600)), + ], + view1=[ + dict(type="RandomResizedCrop", size=(500, 500)), + ], + ), + ], + "input_size": (600, 600), + }, + "load_resize_data_from_otxdataset": { + "pipeline": [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", size=(100, 100)), + ), + ], + "input_size": (100, 100), + }, + "resize_to": { + "pipeline": [ + dict( + type="LoadResizeDataFromOTXDataset", + resize_cfg=dict(type="Resize", size=(100, 100), downscale_only=True), + ), + dict(type="ResizeTo", size=(100, 100)), + ], + "input_size": (100, 100), + }, +} + + +def get_mock_model_ckpt(case): + if case == "none": + return None + if case == "no_input_size": + return {} + if case == "input_size_default": + return {"input_size": None} + if case == "input_size_exist": + return {"input_size": (512, 512)} + + +@e2e_pytest_unit +class TestInputSizeManager: + @pytest.mark.parametrize("base_input_size", [None, 100, [100, 200], {"train": 100}]) + def test_init(self, base_input_size): + # prepare + mock_config = {"data": {"train": {"pipeline": []}}} + + # check + InputSizeManager(mock_config, base_input_size) + + def test_init_insufficient_base_input_size(self): + # prepare + mock_config = {"data": {"train": {"pipeline": []}}} + base_input_size = {"val": 100} + + # check if data pipeline has train but base_input_size doesn't have it, error is raised + with pytest.raises(ValueError): + InputSizeManager(mock_config, base_input_size) + + @pytest.mark.parametrize("input_size", [200, (200, 100)]) + def test_set_input_size(self, mock_data_pipeline, input_size): + # prepare + base_input_size = 400 + if isinstance(input_size, tuple): + expected_input_size_tuple = input_size + expected_input_size_int = input_size[0] + elif isinstance(input_size, int): + expected_input_size_tuple = (input_size, input_size) + expected_input_size_int = input_size + + mock_config = {"data": {"train": {"pipeline": mock_data_pipeline}}} + + # execute + InputSizeManager(mock_config, base_input_size).set_input_size(input_size) + + # check all input sizes are updated as expected + def check_val_changed(pipelines): + if isinstance(pipelines, list): + for pipeline in pipelines: + check_val_changed(pipeline) + elif isinstance(pipelines, dict): + for value in pipelines.values(): + check_val_changed(value) + elif isinstance(pipelines, tuple): + assert pipelines == expected_input_size_tuple + elif not isinstance(pipelines, bool) and isinstance(pipelines, int): + assert pipelines == expected_input_size_int + + check_val_changed(mock_data_pipeline) + + @pytest.mark.parametrize("input_size", [(256, 256), (300, 300)]) + def test_set_input_size_yolox(self, mock_data_pipeline, input_size): + mock_config = { + "model": {"type": "CustomYOLOX"}, + "data": {"train": {"pipeline": mock_data_pipeline}}, + } + + manager = InputSizeManager(mock_config) + + if input_size[0] % 32 != 0: + with pytest.raises(ValueError): + manager.set_input_size(input_size) + else: + manager.set_input_size(input_size) + assert mock_config["model"]["input_size"] == input_size + + @pytest.mark.parametrize("base_input_size", [100, [100, 200], {"train": 100}]) + def test_base_input_size_with_given_args(self, base_input_size): + # prepare + mock_config = {"data": {"train": {"pipeline": []}}} + if isinstance(base_input_size, int): + base_input_size = [base_input_size, base_input_size] + elif isinstance(base_input_size, dict): + for task in base_input_size.keys(): + if isinstance(base_input_size[task], int): + base_input_size[task] = [base_input_size[task], base_input_size[task]] + + # execute + input_size_manager = InputSizeManager(mock_config, base_input_size) + + # check base_input_size attribute is same as argument given when class initialization + assert input_size_manager.base_input_size == base_input_size + + def test_base_input_size_without_given_args(self, mocker): + # prepare + input_size_manager = InputSizeManager(mocker.MagicMock()) + estimated_input_size = [100, 100] + + # execute + input_size_manager.get_input_size_from_cfg = mocker.MagicMock(return_value=estimated_input_size) + + # check if base_input_size argument isn't given, input size is estimated + assert input_size_manager.base_input_size == estimated_input_size + + @pytest.mark.parametrize("test_case", list(mock_data_pipeline_to_estimate.keys())) + def test_get_input_size_from_cfg(self, test_case): + # prepare + pipeline = mock_data_pipeline_to_estimate[test_case]["pipeline"] + input_size = mock_data_pipeline_to_estimate[test_case]["input_size"] + mock_config = {"data": {"train": {"pipeline": pipeline}}} + input_size_manager = InputSizeManager(mock_config) + + # check input size is estimated as expected + assert input_size_manager.get_input_size_from_cfg("train") == input_size + + @e2e_pytest_unit + @pytest.mark.parametrize("model_ckpt_case", ["none", "no_input_size", "input_size_default", "input_size_exist"]) + def test_get_trained_input_size(self, mocker, model_ckpt_case): + # prepare + mock_torch = mocker.patch.object(config_utils, "torch") + mock_torch.load.return_value = get_mock_model_ckpt(model_ckpt_case) + + if model_ckpt_case == "none" or model_ckpt_case == "no_input_size" or model_ckpt_case == "input_size_default": + expected_value = None + else: + expected_value = (512, 512) + + # check expected value is returned + assert ( + InputSizeManager.get_trained_input_size(None if model_ckpt_case == "none" else mocker.MagicMock()) + == expected_value + ) + + def test_select_closest_size(self): + manager = InputSizeManager({}) + input_size = (100, 100) + preset_sizes = [] + assert manager.select_closest_size(input_size, preset_sizes) == input_size + preset_sizes = [(99, 99), (102, 102)] + assert manager.select_closest_size(input_size, preset_sizes) == (99, 99) + preset_sizes = InputSizePreset.input_sizes() + assert manager.select_closest_size(input_size, preset_sizes) == (128, 128) + + def test_adapt_input_size_to_dataset(self): + base_input_size = (512, 512) + manager = InputSizeManager({}, base_input_size) + input_size = manager.adapt_input_size_to_dataset( + max_image_size=-1, + ) + assert input_size == base_input_size + + input_size = manager.adapt_input_size_to_dataset( + max_image_size=1024, + ) # 1024 -> 512 + assert input_size == base_input_size + + input_size = manager.adapt_input_size_to_dataset( + max_image_size=1024, + downscale_only=False, + ) # 512 -> 1024 + assert input_size == (1024, 1024) + + input_size = manager.adapt_input_size_to_dataset( + max_image_size=1024, + min_object_size=128, + ) # 1024 -> 256 + assert input_size == (256, 256) + + input_size = manager.adapt_input_size_to_dataset( + max_image_size=1024, + min_object_size=16, + ) # 1024 -> 2048 -> 512 + assert input_size == base_input_size + + input_size = manager.adapt_input_size_to_dataset( + max_image_size=1024, + min_object_size=16, + downscale_only=False, + ) # 1024 -> 2048 -> 1024 + assert input_size == (1024, 1024) + + +@e2e_pytest_unit +def test_get_proper_repeat_times(): + batch_size = 2 + coef = 1.0 + min_repeat = 1.0 + + data_size = 0 + repeats = get_proper_repeat_times(data_size=data_size, batch_size=batch_size, coef=coef, min_repeat=min_repeat) + assert repeats == 1 + + batch_size = 0 + repeats = get_proper_repeat_times(data_size=data_size, batch_size=batch_size, coef=coef, min_repeat=min_repeat) + assert repeats == 1 diff --git a/tests/unit/algorithms/common/adapters/mmcv/utils/test_fp16_utils.py b/tests/unit/algorithms/common/adapters/mmcv/utils/test_fp16_utils.py new file mode 100644 index 00000000000..da50590124b --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/utils/test_fp16_utils.py @@ -0,0 +1,98 @@ +"""Test for otx.algorithms.common.adapters.mmcv.utils.fp16_utils""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from unittest.mock import MagicMock, patch +from torch import nn, torch +from otx.algorithms.common.adapters.mmcv.utils.fp16_utils import custom_auto_fp16 +from otx.algorithms.common.adapters.mmcv.utils.fp16_utils import custom_force_fp32 +from otx.algorithms.common.utils import is_xpu_available + + +@pytest.fixture +def test_module(): + class TestModule(torch.nn.Module): + def __init__(self): + super(TestModule, self).__init__() + self.fp16_enabled = False + + @custom_auto_fp16() + def test_method_fp16(self, arg1, arg2): + return torch.tensor(arg1) + torch.tensor(arg2) + + @custom_auto_fp16(out_fp32=True) + def test_method_force_out_fp32(self, arg1, arg2): + return torch.tensor(arg1) + torch.tensor(arg2) + + @custom_force_fp32(out_fp16=False) + def test_func_force_fp16_to_fp32(self, arg1, arg2): + return torch.tensor(arg1) + torch.tensor(arg2) + + @custom_force_fp32(out_fp16=True) + def test_func_force_fp32_out_fp16(self, arg1, arg2): + return torch.tensor(arg1) + torch.tensor(arg2) + + def set_fp16(self, enabled): + self.fp16_enabled = enabled + + return TestModule() + + +class TestCustomAutoFP16: + def test_simple_apply(self, test_module): + test_func = test_module.test_method_fp16 + # assertion simple ints + assert test_func(5, 6) == 11 + # no fp16 enabled + assert test_func(torch.tensor(5.3), torch.tensor(8.3)).dtype == torch.float32 + + def test_fp16_enabled_true(self, test_module): + test_module.set_fp16(enabled=True) + test_func = test_module.test_method_fp16 + # check fp16 casting + if not is_xpu_available(): + assert test_func(torch.tensor(5.3), torch.tensor(8.3)).dtype == torch.float16 + else: + assert test_func(torch.tensor(5.3), torch.tensor(8.3)).dtype == torch.bfloat16 + + def test_out_fp32_true(self, test_module): + test_module.set_fp16(enabled=True) + test_func = test_module.test_method_force_out_fp32 + # cast back to fp32 + assert test_func(torch.tensor(5.3), torch.tensor(8.3)).dtype == torch.float32 + + +class TestCustomForceFP32: + def test_simple_apply(self, test_module): + test_func = test_module.test_func_force_fp16_to_fp32 + # assertion simple ints + assert test_func(5, 6) == 11 + # no fp16 enabled + assert test_func(torch.tensor(5.3), torch.tensor(8.3)).dtype == torch.float32 + + @pytest.mark.skipif(is_xpu_available(), reason="cuda is not available") + def test_fp16_enabled_true(self, test_module): + test_module.set_fp16(enabled=True) + test_func = test_module.test_func_force_fp16_to_fp32 + output_type = test_func(torch.tensor(5.3, dtype=torch.float16), torch.tensor(8.3, dtype=torch.float16)).dtype + # check fp16 casting + assert output_type == torch.float32 + + def test_out_fp32_true(self, test_module): + test_module.set_fp16(enabled=True) + test_func = test_module.test_func_force_fp32_out_fp16 + output_type = test_func(torch.tensor(5.3, dtype=torch.float16), torch.tensor(8.3, dtype=torch.float16)).dtype + # cast back to fp32 + assert output_type == torch.float16 + + @pytest.mark.skipif(not is_xpu_available(), reason="XPU is not available") + def test_fp16_enabled_xpu(self, test_module): + # setup + test_module.set_fp16(enabled=True) + test_func = test_module.test_func_force_fp16_to_fp32 + output_type = test_func(torch.tensor(5.3, dtype=torch.bfloat16), torch.tensor(8.3, dtype=torch.bfloat16)).dtype + # assertion + assert output_type == torch.float32 diff --git a/tests/unit/algorithms/common/adapters/mmdeploy/__init__.py b/tests/unit/algorithms/common/adapters/mmdeploy/__init__.py new file mode 100644 index 00000000000..be388f4bebe --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmdeploy/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algorithms/common/adapters/mmdeploy/ops/__init__.py b/tests/unit/algorithms/common/adapters/mmdeploy/ops/__init__.py new file mode 100644 index 00000000000..ce553f5423b --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmdeploy/ops/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common.adapters.mmdeploy.ops""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/mmdeploy/ops/test_custom_ops.py b/tests/unit/algorithms/common/adapters/mmdeploy/ops/test_custom_ops.py new file mode 100644 index 00000000000..46b7ef11a3e --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmdeploy/ops/test_custom_ops.py @@ -0,0 +1,54 @@ +"""Test for otx.algorithms.common.adapters.mmdeploy.ops.custom_ops.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcv.utils import Config +from mmdeploy.core import SYMBOLIC_REWRITER + +from otx.algorithms.common.adapters.mmdeploy.ops.custom_ops import squeeze__default +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_symbolic_registery(): + assert len(SYMBOLIC_REWRITER._registry._rewrite_records["squeeze"]) == 1 + + +class MockOps: + def op(self, *args, **kwargs): + return (args, kwargs) + + +@e2e_pytest_unit +def test_squeeze(mocker): + """Test squeeze__default function.""" + + class MockClass: + class _size: + def sizes(self): + return [1, 1, 1] + + size = _size() + + def type(self): + return self.size + + # Patching for squeeze op + mock_ctx = Config({"cfg": Config({"opset_version": 11})}) + mock_g = MockOps() + mock_self = MockClass() + mocker.patch("otx.algorithms.common.adapters.mmdeploy.ops.custom_ops.get_ir_config", return_value=mock_ctx.cfg) + op = squeeze__default(mock_ctx, mock_g, mock_self) + assert op[0][0] == "Squeeze" + assert op[1]["axes_i"] == [0, 1, 2] + + mock_ctx = Config({"cfg": Config({"opset_version": 13})}) + mock_g = MockOps() + mock_self = MockClass() + mocker.patch("otx.algorithms.common.adapters.mmdeploy.ops.custom_ops.get_ir_config", return_value=mock_ctx.cfg) + op = squeeze__default(mock_ctx, mock_g, mock_self) + assert op[0][0] == "Squeeze" + assert op[0][2][0][0] == "Constant" + assert torch.all(op[0][2][1]["value_t"] == torch.Tensor([0, 1, 2])) diff --git a/tests/unit/algorithms/common/adapters/mmdeploy/test_deploy_apis.py b/tests/unit/algorithms/common/adapters/mmdeploy/test_deploy_apis.py new file mode 100644 index 00000000000..b27be3d2790 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmdeploy/test_deploy_apis.py @@ -0,0 +1,220 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import tempfile + +import numpy as np +import torch +from mmcv.utils import Config + +from otx.algorithms.common.adapters.mmdeploy.apis import NaiveExporter +from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmdeploy.test_helpers import ( + create_config, + create_model, +) + + +class TestNaiveExporter: + @e2e_pytest_unit + def test_sub_component(self): + model = create_model("mmcls") + + with tempfile.TemporaryDirectory() as tempdir: + onnx_path = NaiveExporter.torch2onnx( + tempdir, + model, + {"img": [torch.zeros((1, 50, 50, 3))], "img_metas": []}, + ) + assert os.path.exists(onnx_path) + + openvino_paths = NaiveExporter.onnx2openvino( + tempdir, + onnx_path, + ) + for openvino_path in openvino_paths: + assert os.path.exists(openvino_path) + + @e2e_pytest_unit + def test_export2backend(self): + from otx.algorithms.classification.adapters.mmcls.utils.builder import ( + build_classifier, + ) + + config = create_config() + create_model("mmcls") + + with tempfile.TemporaryDirectory() as tempdir: + NaiveExporter.export2backend( + tempdir, + build_classifier, + config, + {"img": [torch.zeros((50, 50, 3))], "img_metas": []}, + ) + assert [f for f in os.listdir(tempdir) if f.endswith(".xml")] + assert [f for f in os.listdir(tempdir) if f.endswith(".bin")] + + +if is_mmdeploy_enabled(): + from mmdeploy.core import FUNCTION_REWRITER, mark + + from otx.algorithms.common.adapters.mmdeploy.apis import MMdeployExporter + + class TestMMdeployExporter: + @e2e_pytest_unit + def test_sub_component(self): + config = create_config() + deploy_config = Config( + { + "ir_config": { + "type": "onnx", + "input_names": ["input"], + "output_names": ["output"], + }, + "codebase_config": { + "type": "mmcls", + "task": "Classification", + }, + "backend_config": { + "type": "openvino", + "model_inputs": [ + { + "opt_shapes": { + "input": [1, 3, 50, 50], + } + } + ], + }, + } + ) + create_model("mmcls") + + with tempfile.TemporaryDirectory() as tempdir: + onnx_path = MMdeployExporter.torch2onnx( + tempdir, + np.zeros((50, 50, 3), dtype=np.float32), + config, + deploy_config, + ) + assert isinstance(onnx_path, str) + assert os.path.exists(onnx_path) + + openvino_paths = MMdeployExporter.onnx2openvino( + tempdir, + onnx_path, + deploy_config, + ) + for openvino_path in openvino_paths: + assert os.path.exists(openvino_path) + + @e2e_pytest_unit + def test_export2backend(self): + from otx.algorithms.classification.adapters.mmcls.utils.builder import ( + build_classifier, + ) + + config = create_config() + deploy_config = Config( + { + "ir_config": { + "type": "onnx", + "input_names": ["input"], + "output_names": ["output"], + }, + "codebase_config": { + "type": "mmcls", + "task": "Classification", + }, + "backend_config": { + "type": "openvino", + "model_inputs": [ + { + "opt_shapes": { + "input": [1, 3, 50, 50], + } + } + ], + }, + } + ) + create_model("mmcls") + + with tempfile.TemporaryDirectory() as tempdir: + MMdeployExporter.export2backend( + tempdir, + build_classifier, + config, + deploy_config, + str(ExportType.OPENVINO), + ) + assert [f for f in os.listdir(tempdir) if f.endswith(".xml")] + assert [f for f in os.listdir(tempdir) if f.endswith(".bin")] + + @e2e_pytest_unit + def test_partition(self): + from otx.algorithms.classification.adapters.mmcls.utils.builder import ( + build_classifier, + ) + + config = create_config() + deploy_config = Config( + { + "ir_config": { + "type": "onnx", + "input_names": ["input"], + "output_names": ["output"], + }, + "codebase_config": { + "type": "mmcls", + "task": "Classification", + }, + "backend_config": { + "type": "openvino", + "model_inputs": [ + { + "opt_shapes": { + "input": [1, 3, 50, 50], + } + } + ], + }, + "partition_config": { + "apply_marks": True, + "partition_cfg": [ + { + "save_file": "partition.onnx", + "start": ["test:input"], + "end": ["test:output"], + "output_names": ["output"], + "mo_options": { + "_delete_": True, + "args": {}, + "flags": [], + }, + } + ], + }, + } + ) + create_model("mmcls") + + @FUNCTION_REWRITER.register_rewriter( + "tests.unit.algorithms.common.adapters.mmdeploy.test_helpers.MockModel.forward" + ) + @mark("test", inputs=["input"], outputs=["output"]) + def forward(ctx, self, *args, **kwargs): + return ctx.origin_func(self, *args, **kwargs) + + with tempfile.TemporaryDirectory() as tempdir: + MMdeployExporter.export2backend(tempdir, build_classifier, config, deploy_config, "OPENVINO") + files = os.listdir(tempdir) + assert "model.onnx" in files + assert "model.xml" in files + assert "model.bin" in files + assert "partition.onnx" in files + assert "partition.xml" in files + assert "partition.bin" in files diff --git a/tests/unit/algorithms/common/adapters/mmdeploy/test_helpers.py b/tests/unit/algorithms/common/adapters/mmdeploy/test_helpers.py new file mode 100644 index 00000000000..7f2f9bbe2c6 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmdeploy/test_helpers.py @@ -0,0 +1,87 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmcv.utils import Config + + +class MockModel(torch.nn.Module): + def __init__(self, *args, **kwargs): + super().__init__() + self.conv1 = torch.nn.Conv2d(3, 3, 3) + self.conv2 = torch.nn.Conv2d(3, 3, 3) + self.linear = torch.nn.Linear(3, 1) + + def forward(self, img, img_metas=None, **kwargs): + if isinstance(img, list): + img = img[0] + if img.shape[-1] == 3: + img = img.permute(0, 3, 1, 2) + + x = self.conv1(img) + x = self.conv2(img) + x = torch.mean(x, dim=(2, 3)) + x = self.linear(x) + return x + + def forward_dummy(self, img, img_metas=None, **kwargs): + if isinstance(img, list): + img = img[0] + if img.shape[-1] == 3: + img = img.permute(0, 3, 1, 2) + + x = self.conv1(img) + x = self.conv2(img) + x = torch.mean(x, dim=(2, 3)) + x = self.linear(x) + return x + + def train_step(self, *args, **kwargs): + return dict() + + def init_weights(self, *args, **kwargs): + pass + + def set_step_params(self, *args, **kwargs): + pass + + +def create_model(lib="mmcls"): + if lib == "mmcls": + from mmcls.models import CLASSIFIERS + + CLASSIFIERS.register_module(MockModel, force=True) + elif lib == "mmdet": + from mmdet.models import DETECTORS + + DETECTORS.register_module(MockModel, force=True) + elif lib == "mmseg": + from mmseg.models import SEGMENTORS + + SEGMENTORS.register_module(MockModel, force=True) + else: + raise ValueError() + + return MockModel() + + +def create_config(): + config = Config( + { + "model": { + "type": "MockModel", + }, + "data": { + "test": { + "pipeline": [ + {"type": "LoadImageFromFile"}, + {"type": "Normalize", "mean": [0, 0, 0], "std": [1, 1, 1]}, + {"type": "ImageToTensor", "keys": ["img"]}, + ] + } + }, + } + ) + + return config diff --git a/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_mmdeploy.py b/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_mmdeploy.py new file mode 100644 index 00000000000..b22c8178350 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_mmdeploy.py @@ -0,0 +1,42 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import importlib + +from mmcv.utils import Config + +from otx.algorithms.common.adapters.mmdeploy.utils.mmdeploy import ( + is_mmdeploy_enabled, + mmdeploy_init_model_helper, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmdeploy.test_helpers import ( + create_config, + create_model, +) + + +@e2e_pytest_unit +def test_is_mmdeploy_enabled(): + assert (importlib.util.find_spec("mmdeploy") is not None) == is_mmdeploy_enabled() + + +@e2e_pytest_unit +def test_mmdeploy_init_model_helper(): + from otx.algorithms.classification.adapters.mmcls.utils.builder import ( + build_classifier, + ) + + config = Config( + { + "model_cfg": create_config(), + "device": "cpu", + } + ) + + if importlib.util.find_spec("mmcls"): + create_model("mmcls") + model = mmdeploy_init_model_helper(config, model_builder=build_classifier) + for i in model.parameters(): + assert not i.requires_grad diff --git a/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_onnx.py b/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_onnx.py new file mode 100644 index 00000000000..ab726d9f10f --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_onnx.py @@ -0,0 +1,67 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import tempfile + +import onnx +import pytest +import torch + +from otx.algorithms.common.adapters.mmdeploy.apis import NaiveExporter +from otx.algorithms.common.adapters.mmdeploy.utils.onnx import ( + prepare_onnx_for_openvino, + remove_nodes_by_op_type, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmdeploy.test_helpers import create_model + + +@e2e_pytest_unit +def test_remove_nodes_by_op_type(): + model = create_model("mmcls") + + with tempfile.TemporaryDirectory() as tempdir: + onnx_path = NaiveExporter.torch2onnx( + tempdir, + model, + {"img": [torch.zeros((1, 50, 50, 3))], "img_metas": []}, + ) + assert os.path.exists(onnx_path) + + onnx_model = onnx.load(onnx_path) + onnx_model = remove_nodes_by_op_type(onnx_model, "Gemm") + nodes = [] + for node in onnx_model.graph.node: + if node.op_type == "Gemm": + nodes.append(node) + assert not nodes + + # NOTE: Currently does not work for multiple op_types + with pytest.raises(AssertionError) as e: + onnx_model = onnx.load(onnx_path) + onnx_model = remove_nodes_by_op_type(onnx_model, "Conv") + assert e.type == Exception, f"{e}" + + +@e2e_pytest_unit +def test_prepare_onnx_for_openvino(): + + model = create_model("mmcls") + + with tempfile.TemporaryDirectory() as tempdir: + onnx_path = NaiveExporter.torch2onnx( + tempdir, + model, + {"img": [torch.zeros((1, 50, 50, 3))], "img_metas": []}, + ) + assert os.path.exists(onnx_path) + + prepare_onnx_for_openvino(onnx_path, onnx_path) + onnx_model = onnx.load(onnx_path) + nodes = [] + for node in onnx_model.graph.node: + if node.op_type == "Mark": + nodes.append(node) + assert not nodes diff --git a/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_operations_domain.py b/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_operations_domain.py new file mode 100644 index 00000000000..6a74f2aed8c --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_operations_domain.py @@ -0,0 +1,14 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.common.adapters.mmdeploy.utils.operations_domain import ( + DOMAIN_CUSTOM_OPS_NAME, + add_domain, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_add_domain(): + assert add_domain("abc") == DOMAIN_CUSTOM_OPS_NAME + "::" + "abc" diff --git a/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_utils.py b/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_utils.py new file mode 100644 index 00000000000..b4e1dd41ee6 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmdeploy/utils/test_deploy_utils_utils.py @@ -0,0 +1,76 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import torch + +from otx.algorithms.common.adapters.mmdeploy.utils.utils import ( + numpy_2_list, + sync_batchnorm_2_batchnorm, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def create_model(): + class MockModel(torch.nn.Module): + def __init__(self, *args, **kwargs): + super().__init__() + self.conv1 = torch.nn.Conv2d(3, 3, 3) + self.norm = torch.nn.BatchNorm2d(3) + self.dict_norm = torch.nn.ModuleDict( + { + "0": torch.nn.BatchNorm2d(2), + } + ) + self.list_norm = torch.nn.ModuleList( + [ + torch.nn.BatchNorm2d(2), + ] + ) + + return MockModel() + + +@e2e_pytest_unit +def test_convert_batchnorm(): + mock_model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(create_model()) + assert isinstance(mock_model.norm, torch.nn.SyncBatchNorm) + assert isinstance(mock_model.dict_norm["0"], torch.nn.SyncBatchNorm) + assert isinstance(mock_model.list_norm[0], torch.nn.SyncBatchNorm) + + mock_model = sync_batchnorm_2_batchnorm(mock_model, 1) + assert isinstance(mock_model.norm, torch.nn.BatchNorm1d) + assert isinstance(mock_model.dict_norm["0"], torch.nn.BatchNorm1d) + assert isinstance(mock_model.list_norm[0], torch.nn.BatchNorm1d) + + mock_model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(create_model()) + assert isinstance(mock_model.norm, torch.nn.SyncBatchNorm) + assert isinstance(mock_model.dict_norm["0"], torch.nn.SyncBatchNorm) + assert isinstance(mock_model.list_norm[0], torch.nn.SyncBatchNorm) + + mock_model = sync_batchnorm_2_batchnorm(mock_model, 2) + assert isinstance(mock_model.norm, torch.nn.BatchNorm2d) + assert isinstance(mock_model.dict_norm["0"], torch.nn.BatchNorm2d) + assert isinstance(mock_model.list_norm[0], torch.nn.BatchNorm2d) + + mock_model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(create_model()) + assert isinstance(mock_model.norm, torch.nn.SyncBatchNorm) + assert isinstance(mock_model.dict_norm["0"], torch.nn.SyncBatchNorm) + assert isinstance(mock_model.list_norm[0], torch.nn.SyncBatchNorm) + + mock_model = sync_batchnorm_2_batchnorm(mock_model, 3) + assert isinstance(mock_model.norm, torch.nn.BatchNorm3d) + assert isinstance(mock_model.dict_norm["0"], torch.nn.BatchNorm3d) + assert isinstance(mock_model.list_norm[0], torch.nn.BatchNorm3d) + + +@e2e_pytest_unit +def test_numpy2list(): + assert (0,) == numpy_2_list((0,)) + assert [0] == numpy_2_list([0]) + assert 0 == numpy_2_list(0) + assert {0: 0} == numpy_2_list({0: 0}) + assert [0] == numpy_2_list(np.array([0])) + assert 0 == numpy_2_list(np.array(0)) + assert {0: 0} == numpy_2_list({0: np.array(0)}) diff --git a/tests/unit/algorithms/common/adapters/nncf/__init__.py b/tests/unit/algorithms/common/adapters/nncf/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/nncf/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/nncf/test_nncf_compression.py b/tests/unit/algorithms/common/adapters/nncf/test_nncf_compression.py new file mode 100644 index 00000000000..edc09394746 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/nncf/test_nncf_compression.py @@ -0,0 +1,96 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest +import torch +from torch import nn + +from otx.algorithms.common.adapters.nncf.compression import ( + AccuracyAwareLrUpdater, + NNCFMetaState, + is_checkpoint_nncf, + is_state_nncf, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def helper(fn, returns_with_params): + for item in returns_with_params: + ret = item[0] + args = item[1] + kwargs = item[2] + + assert ret == fn(*args, **kwargs) + + +class TestNNCFMetaState: + @e2e_pytest_unit + def test_repr(self): + state = NNCFMetaState(None, None, None) + assert repr(state) == "NNCFMetaState()" + assert state.state_to_build is None + assert state.data_to_build is None + assert state.compression_ctrl is None + + state = NNCFMetaState({"dummy": torch.tensor(1)}) + assert repr(state) == "NNCFMetaState(state_to_build='')" + assert state.state_to_build == {"dummy": torch.tensor(1)} + assert state.data_to_build is None + assert state.compression_ctrl is None + + state = NNCFMetaState(None, np.array(1)) + assert repr(state) == "NNCFMetaState(data_to_build='')" + assert state.state_to_build is None + assert state.data_to_build == np.array(1) + assert state.compression_ctrl is None + + state = NNCFMetaState(None, None, {"dummy": "dummy"}) + assert repr(state) == "NNCFMetaState(compression_ctrl='')" + assert state.state_to_build is None + assert state.data_to_build is None + assert state.compression_ctrl == {"dummy": "dummy"} + + +@e2e_pytest_unit +def test_is_state_nncf(): + returns_with_params = [ + (True, [{"meta": {"nncf_enable_compression": True}}], {}), + (False, [{"meta": {"nncf_enable_compression": False}}], {}), + (False, [{"meta": {}}], {}), + ] + + helper(is_state_nncf, returns_with_params) + + +@e2e_pytest_unit +def test_is_checkpoint_nncf(): + returns_with_params = [ + (False, ["dummy_file_path"], {}), + ] + + helper(is_checkpoint_nncf, returns_with_params) + + +@pytest.fixture() +def mock_model(): + class MockModule(nn.Module): + def __init__(self): + super().__init__() + + return MockModule() + + +class TestAccuracyAwareLrUpdater: + @e2e_pytest_unit + def test_step(self, mock_model): + updater = AccuracyAwareLrUpdater(mock_model) + updater.step() + + @e2e_pytest_unit + def test_base_lrs(self, mock_model): + updater = AccuracyAwareLrUpdater(mock_model) + for value in np.arange(0, 0.1, 0.005): + updater.base_lrs = value + assert value == updater._lr_hook.base_lr diff --git a/tests/unit/algorithms/common/adapters/nncf/test_nncf_config.py b/tests/unit/algorithms/common/adapters/nncf/test_nncf_config.py new file mode 100644 index 00000000000..2bb32c9765b --- /dev/null +++ b/tests/unit/algorithms/common/adapters/nncf/test_nncf_config.py @@ -0,0 +1,116 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import json +import os +import tempfile + +import pytest + +from otx.algorithms.common.adapters.nncf.config import ( + compose_nncf_config, + load_nncf_config, + merge_dicts_and_lists_b_into_a, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_load_nncf_config(): + with pytest.raises(AssertionError): + load_nncf_config("invalid_path") + with tempfile.TemporaryDirectory() as directory: + tmp = os.path.join(directory, "temp.json") + with open(tmp, "w") as f: + json.dump({"dummy": "dummy"}, f) + assert {"dummy": "dummy"} == load_nncf_config(tmp) + + +@e2e_pytest_unit +def test_compose_nncf_config(): + nncf_config = { + "base": { + "find_unused_parameters": True, + "nncf_config": { + "target_metric_name": "mAP", + "input_info": {"sample_size": [1, 3, 864, 864]}, + "compression": [], + "log_dir": "/tmp", + "accuracy_aware_training": { + "mode": "early_exit", + }, + }, + }, + "nncf_quantization": { + "optimizer": {"lr": 0.0005}, + "nncf_config": { + "compression": [ + { + "algorithm": "quantization", + "initializer": { + "range": {"num_init_samples": 300}, + "batchnorm_adaptation": {"num_bn_adaptation_samples": 300}, + }, + } + ], + "accuracy_aware_training": { + "mode": "early_exit", + "params": { + "maximal_absolute_accuracy_degradation": 0.01, + "maximal_total_epochs": 20, + }, + }, + }, + }, + "nncf_quantization_pruning": { + "nncf_config": { + "accuracy_aware_training": { + "mode": "adaptive_compression_level", + "params": { + "initial_training_phase_epochs": 5, + "maximal_total_epochs": 100, + "patience_epochs": 5, + }, + }, + "compression": [ + { + "algorithm": "filter_pruning", + "ignored_scopes": ["{re}SingleStageDetector/SSDHead\\[bbox_head\\].*"], + "params": { + "schedule": "baseline", + "pruning_flops_target": 0.1, + "filter_importance": "geometric_median", + }, + }, + { + "algorithm": "quantization", + "initializer": { + "range": {"num_init_samples": 300}, + "batchnorm_adaptation": {"num_bn_adaptation_samples": 300}, + }, + }, + ], + } + }, + "error": {"nncf_config": {"accuracy_aware_training": ["error"]}}, + "order_of_parts": ["nncf_quantization", "nncf_quantization_pruning", "error"], + } + + assert nncf_config["base"] == compose_nncf_config(nncf_config, []) + assert merge_dicts_and_lists_b_into_a(nncf_config["base"], nncf_config["nncf_quantization"]) == compose_nncf_config( + nncf_config, ["nncf_quantization"] + ) + assert merge_dicts_and_lists_b_into_a( + nncf_config["base"], nncf_config["nncf_quantization_pruning"] + ) == compose_nncf_config(nncf_config, ["nncf_quantization_pruning"]) + + with pytest.raises(RuntimeError): + compose_nncf_config(nncf_config, ["error"]) + + +@e2e_pytest_unit +def test_merge_dicts_and_lists_b_into_a(): + assert {"a": 1, "b": 2} == merge_dicts_and_lists_b_into_a({"a": 1}, {"b": 2}) + assert [1, 2] == merge_dicts_and_lists_b_into_a([1], [2]) + assert {"a": [1, 2]} == merge_dicts_and_lists_b_into_a({"a": [1]}, {"a": [2]}) diff --git a/tests/unit/algorithms/common/adapters/nncf/test_nncf_patches.py b/tests/unit/algorithms/common/adapters/nncf/test_nncf_patches.py new file mode 100644 index 00000000000..fa890473e03 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/nncf/test_nncf_patches.py @@ -0,0 +1,40 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from functools import partial + +from otx.algorithms.common.adapters.nncf.patches import ( + nncf_trace_wrapper, + no_nncf_trace_wrapper, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmcv.nncf.test_helpers import create_model + + +@e2e_pytest_unit +def test_nncf_trace_context(): + model = create_model() + bak = model.forward + + with model.nncf_trace_context({}, True): + assert isinstance(model.forward, partial) + assert model.forward.func == bak + assert not isinstance(model.forward, partial) + assert model.forward == bak + + with model.nncf_trace_context({}, False): + assert isinstance(model.forward, partial) + assert model.forward.func == model.forward_dummy + assert not isinstance(model.forward, partial) + assert model.forward == bak + + +@e2e_pytest_unit +def test_nncf_trace_wrapper(): + nncf_trace_wrapper("", lambda: None) + + +@e2e_pytest_unit +def test_no_nncf_trace_wrapper(): + no_nncf_trace_wrapper("", lambda: None) diff --git a/tests/unit/algorithms/common/adapters/nncf/test_nncf_utils.py b/tests/unit/algorithms/common/adapters/nncf/test_nncf_utils.py new file mode 100644 index 00000000000..02853783367 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/nncf/test_nncf_utils.py @@ -0,0 +1,100 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from io import BytesIO + +import pytest +import torch + +from otx.algorithms.common.adapters.nncf.compression import NNCFMetaState +from otx.algorithms.common.adapters.nncf.utils.utils import ( + _is_nncf_enabled, + check_nncf_is_enabled, + is_accuracy_aware_training_set, + is_in_nncf_tracing, + is_nncf_enabled, + load_checkpoint, + nncf_trace, + no_nncf_trace, + nullcontext, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmcv.nncf.test_helpers import create_model + + +@e2e_pytest_unit +def test_is_nncf_enabled(): + assert _is_nncf_enabled == is_nncf_enabled() + + +@e2e_pytest_unit +def test_nncf_is_enabled(): + if is_nncf_enabled(): + check_nncf_is_enabled() + else: + with pytest.raises(RuntimeError): + check_nncf_is_enabled() + + +@e2e_pytest_unit +def test_load_checkpoint(): + mock_model = create_model() + state_dict = mock_model.state_dict() + buffer = BytesIO() + torch.save(state_dict, buffer) + buffer.seek(0) + + load_checkpoint(mock_model, buffer, strict=False) + assert state_dict.keys() == mock_model.state_dict().keys() + for k in state_dict.keys(): + assert torch.equal(state_dict[k], mock_model.state_dict()[k]) + + buffer = BytesIO() + torch.save( + { + "state_dict": {"dummy": "dummy"}, + "meta": {"nncf_meta": NNCFMetaState(state_to_build=state_dict, compression_ctrl={"dummy": "dummy"})}, + }, + buffer, + ) + buffer.seek(0) + load_checkpoint(mock_model, buffer, strict=False) + assert state_dict.keys() == mock_model.state_dict().keys() + + state_dict["dummy"] = torch.tensor(1) + buffer = BytesIO() + torch.save(state_dict, buffer) + buffer.seek(0) + with pytest.raises(RuntimeError): + load_checkpoint(mock_model, buffer, strict=True) + + +@e2e_pytest_unit +def test_nullcontext(): + with nullcontext(): + pass + + +@e2e_pytest_unit +def test_no_nncf_trace(): + with no_nncf_trace(): + pass + + +@e2e_pytest_unit +def test_nncf_trace(): + with nncf_trace(): + pass + + +@e2e_pytest_unit +def test_is_in_nncf_tracing(): + assert is_in_nncf_tracing() is False + + +@e2e_pytest_unit +def test_is_accuracy_aware_training_set(): + assert is_accuracy_aware_training_set({"accuracy_aware_training": True}) is True + assert is_accuracy_aware_training_set({}) is False + assert is_accuracy_aware_training_set({"accuracy_aware_training": {}}) is True diff --git a/tests/unit/algorithms/common/adapters/torch/__init__.py b/tests/unit/algorithms/common/adapters/torch/__init__.py new file mode 100644 index 00000000000..638e450c552 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/torch/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common.adapters.torch""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/torch/amp/test_xpu_grad_scaler.py b/tests/unit/algorithms/common/adapters/torch/amp/test_xpu_grad_scaler.py new file mode 100644 index 00000000000..465834b9a37 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/torch/amp/test_xpu_grad_scaler.py @@ -0,0 +1,39 @@ +"""Test for otx.algorithms.common.adapters.torch.amp.xpu_grad_scaler """ + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.common.adapters.torch.amp.xpu_grad_scaler import XPUGradScaler + + +class TestXPUGradScaler: + @pytest.fixture + def grad_scaler(self, mocker): + mocker.patch("otx.algorithms.common.adapters.torch.amp.xpu_grad_scaler.is_xpu_available", return_value=True) + return XPUGradScaler() + + @pytest.fixture + def optimizer(self): + model = torch.nn.Linear(3, 3) + optimizer = torch.optim.SGD(model.parameters(), lr=0.1) + return optimizer + + def test_init(self, grad_scaler): + assert grad_scaler._enabled + assert grad_scaler._init_scale == 2.0**16 + assert grad_scaler._growth_factor == 2.0 + assert grad_scaler._backoff_factor == 0.5 + assert grad_scaler._growth_interval == 2000 + + def test_scale(self, grad_scaler, mocker): + outputs = mocker.MagicMock(torch.Tensor) + outputs.device.type = "xpu" + outputs.device.index = 0 + grad_scaler._lazy_init_scale_growth_tracker = mocker.MagicMock() + grad_scaler._scale = mocker.MagicMock() + scaled_outputs = grad_scaler.scale(outputs) + assert isinstance(scaled_outputs.device.type, mocker.MagicMock) diff --git a/tests/unit/algorithms/common/adapters/torch/dataloaders/__init__.py b/tests/unit/algorithms/common/adapters/torch/dataloaders/__init__.py new file mode 100644 index 00000000000..482cdca4b24 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/torch/dataloaders/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common.adapters.torch.dataloaders""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_balanced_sampler.py b/tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_balanced_sampler.py new file mode 100644 index 00000000000..3ec8ec660d6 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_balanced_sampler.py @@ -0,0 +1,51 @@ +import pytest +from torch.utils.data import Dataset + +from otx.algorithms.common.adapters.torch.dataloaders.samplers import BalancedSampler +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestBalancedSampler: + @pytest.fixture(autouse=True) + def setup(self): + class MockDataset(Dataset): + def __init__(self): + self.img_indices = {"foo": list(range(0, 6)), "bar": list(range(6, 10))} + + def __len__(self): + return 10 + + self.mock_dataset = MockDataset() + + @e2e_pytest_unit + def test_sampler_iter(self): + sampler = BalancedSampler(self.mock_dataset, 4) + sampler_iter = iter(sampler) + count = 0 + + for _ in sampler_iter: + count += 1 + + assert count == len(sampler) + + @e2e_pytest_unit + @pytest.mark.parametrize("batch", [1, 2, 4, 8, 16]) + def test_sampler_iter_with_adptive_repeat(self, batch): + sampler = BalancedSampler(self.mock_dataset, batch) + sampler_iter = iter(sampler) + count = 0 + + for _ in sampler_iter: + count += 1 + assert count == len(self.mock_dataset) * sampler.repeat + + @e2e_pytest_unit + def test_sampler_iter_with_multiple_replicas(self): + sampler = BalancedSampler(self.mock_dataset, 4, num_replicas=2) + sampler_iter = iter(sampler) + count = 0 + + for _ in sampler_iter: + count += 1 + + assert count == len(sampler) diff --git a/tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_cls_incr_sampler.py b/tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_cls_incr_sampler.py new file mode 100644 index 00000000000..2e89b647d71 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_cls_incr_sampler.py @@ -0,0 +1,57 @@ +import pytest +import math +from torch.utils.data import Dataset + +from otx.algorithms.common.adapters.torch.dataloaders.samplers import ClsIncrSampler +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestClsIncrSampler: + @pytest.fixture(autouse=True) + def setup(self, mocker): + class MockDataset(Dataset): + def __init__(self): + self.img_indices = {"old": list(range(0, 6)), "new": list(range(6, 10))} + + def __len__(self): + return 10 + + self.mock_dataset = MockDataset() + + @e2e_pytest_unit + def test_sampler_iter(self): + sampler = ClsIncrSampler(self.mock_dataset, 4) + sampler_iter = iter(sampler) + count = 0 + + for _ in sampler_iter: + count += 1 + + assert count == len(sampler) + + @e2e_pytest_unit + @pytest.mark.parametrize("batch", [1, 2, 4, 8, 16]) + def test_sampler_iter_with_adptive_repeat(self, batch): + sampler = ClsIncrSampler(self.mock_dataset, batch) + sampler_iter = iter(sampler) + count = 0 + + for _ in sampler_iter: + count += 1 + + repeated_len = len(self.mock_dataset) * sampler.repeat + if not sampler.drop_last: + assert count == math.ceil(repeated_len / batch) * batch + else: + assert count == len(self.mock_dataset) * sampler.repeat + + @e2e_pytest_unit + def test_sampler_iter_with_multiple_replicas(self): + sampler = ClsIncrSampler(self.mock_dataset, 4, num_replicas=2) + sampler_iter = iter(sampler) + count = 0 + + for _ in sampler_iter: + count += 1 + + assert count == len(sampler) diff --git a/tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_otx_sampler.py b/tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_otx_sampler.py new file mode 100644 index 00000000000..6d10da154ab --- /dev/null +++ b/tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_otx_sampler.py @@ -0,0 +1,31 @@ +import pytest +from torch.utils.data import Dataset + +from otx.algorithms.common.adapters.torch.dataloaders.samplers import OTXSampler +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestOTXSampler: + @pytest.fixture(autouse=True) + def setup(self): + class MockDataset(Dataset): + def __init__(self): + self.img_indices = {"old": list(range(0, 6)), "new": list(range(6, 10))} + + def __len__(self): + return 10 + + self.mock_dataset = MockDataset() + + @e2e_pytest_unit + @pytest.mark.parametrize("batch", [1, 2, 4, 8, 16]) + def test_sampler_iter(self, batch): + sampler = OTXSampler(self.mock_dataset, batch) + sampler_iter = iter(sampler) + count = 0 + + for _ in sampler_iter: + count += 1 + + repeated_len = len(self.mock_dataset) * sampler.repeat + assert count == repeated_len diff --git a/tests/unit/algorithms/common/adapters/torch/utils/__init__.py b/tests/unit/algorithms/common/adapters/torch/utils/__init__.py new file mode 100644 index 00000000000..b4d255c3c3b --- /dev/null +++ b/tests/unit/algorithms/common/adapters/torch/utils/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.common.adapters.torch.utils""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/adapters/torch/utils/test_bs_search_algo.py b/tests/unit/algorithms/common/adapters/torch/utils/test_bs_search_algo.py new file mode 100644 index 00000000000..a347968dc5e --- /dev/null +++ b/tests/unit/algorithms/common/adapters/torch/utils/test_bs_search_algo.py @@ -0,0 +1,241 @@ +from typing import Optional, List + +import pytest +import torch + +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from otx.algorithms.common.adapters.torch.utils import BsSearchAlgo +from otx.algorithms.common.adapters.torch.utils import bs_search_algo + + +@e2e_pytest_unit +class TestBsSearchAlgo: + @pytest.fixture(autouse=True) + def setup_test(self, mocker): + self.mock_torch = mocker.patch.object(bs_search_algo, "torch") + self.mock_torch.cuda.mem_get_info.return_value = (1, 10000) + self.mock_dist = mocker.patch.object(bs_search_algo, "dist") + self.mock_dist.is_initialized.return_value = False + + def test_init(self, mocker): + BsSearchAlgo(mocker.MagicMock(), 4, 10) + + @pytest.mark.parametrize("default_bs", [-2, 0]) + def test_init_w_wrong_default_bs(self, mocker, default_bs): + with pytest.raises(ValueError): + BsSearchAlgo(mocker.MagicMock(), default_bs=default_bs, max_bs=10) + + @pytest.mark.parametrize("max_bs", [-2, 0]) + def test_init_w_wrong_default_bs(self, mocker, max_bs): + with pytest.raises(ValueError): + BsSearchAlgo(mocker.MagicMock(), default_bs=4, max_bs=max_bs) + + def get_mock_train_func(self, cuda_oom_bound: int, max_runnable_bs: int): + def mock_train_func(batch_size): + if batch_size > cuda_oom_bound: + mem_usage = 10000 + raise RuntimeError("CUDA out of memory.") + elif batch_size > max_runnable_bs: + mem_usage = 8500 + 1500 * batch_size / (cuda_oom_bound - max_runnable_bs) + else: + mem_usage = 8500 * batch_size / max_runnable_bs + + self.mock_torch.cuda.max_memory_reserved.return_value = mem_usage + return mem_usage + + return mock_train_func + + def test_try_batch_size(self): + mock_train_func = self.get_mock_train_func(cuda_oom_bound=10000, max_runnable_bs=80) + bs_search_algo = BsSearchAlgo(mock_train_func, 128, 1000) + batch_size = 40 + + cuda_oom, max_memory_reserved = bs_search_algo._try_batch_size(batch_size) + + assert cuda_oom is False + assert max_memory_reserved == mock_train_func(batch_size) + self.mock_torch.cuda.reset_max_memory_cached.assert_called() + self.mock_torch.cuda.empty_cache.assert_called() + + def test_try_batch_size_cuda_oom(self): + mock_train_func = self.get_mock_train_func(cuda_oom_bound=100, max_runnable_bs=80) + bs_search_algo = BsSearchAlgo(mock_train_func, 128, 1000) + batch_size = 200 + + cuda_oom, _ = bs_search_algo._try_batch_size(batch_size) + + assert cuda_oom is True + self.mock_torch.cuda.reset_max_memory_cached.assert_called() + self.mock_torch.cuda.empty_cache.assert_called() + + def _prepare_dist_test(self, broadcast_val: torch.Tensor, gather_val: Optional[List[torch.Tensor]] = None): + self.mock_dist.is_initialized.return_value = True + + # mocking torch.distributed.broadcast + def mock_broadcast(tensor: torch.Tensor, src: int): + tensor.copy_(broadcast_val) + + self.mock_dist.broadcast.side_effect = mock_broadcast + + # mocking torch.distributed.gather if gather_val is given + def mock_gather(tensor: torch.Tensor, gather_list: Optional[List[torch.Tensor]] = None, dst: int = 0): + for i in range(len(gather_list)): + gather_list[i].copy_(gather_val[i]) + + if gather_val is not None: + self.mock_dist.gather.side_effect = mock_gather + + # revert some of torch function + def mock_tensor_cuda(self, *args, **kwargs): + return self + + torch.Tensor.cuda = mock_tensor_cuda + self.mock_torch.tensor = torch.tensor + self.mock_torch.int64 = torch.int64 + self.mock_torch.max = torch.max + self.mock_torch.any = torch.any + self.mock_torch.stack = torch.stack + self.mock_torch.empty = torch.empty + + def test_try_batch_size_distributed_not_rank_0(self): + self.mock_dist.get_rank.return_value = 1 + broadcasted_cuda_oom = False + broadcasted_max_memory_reserved = 4000 + self._prepare_dist_test( + broadcast_val=torch.tensor([broadcasted_cuda_oom, broadcasted_max_memory_reserved], dtype=torch.int64) + ) + mock_train_func = self.get_mock_train_func(cuda_oom_bound=10000, max_runnable_bs=80) + batch_size = 40 + bs_search_algo = BsSearchAlgo(mock_train_func, 128, 1000) + w1_max_memory_reserved = mock_train_func(batch_size) + + cuda_oom, max_memory_reserved = bs_search_algo._try_batch_size(batch_size) + + # check dist.gather is called and get [cuda_oom, maxmemory_reserved] as arguments. + self.mock_dist.gather.assert_called_once() + assert self.mock_dist.gather.call_args.args[0][0].item() == False + assert self.mock_dist.gather.call_args.args[0][1].item() == w1_max_memory_reserved + assert self.mock_dist.gather.call_args.kwargs["dst"] == 0 + # check dist.broadcast is called + self.mock_dist.broadcast.assert_called_once() + assert self.mock_dist.broadcast.call_args.kwargs["src"] == 0 + # check broadcased values are returned + assert cuda_oom is broadcasted_cuda_oom + assert max_memory_reserved == broadcasted_max_memory_reserved + + def test_try_batch_size_distributed_rank_0(self): + self.mock_dist.get_rank.return_value = 0 + self.mock_dist.get_world_size.return_value = 2 + self._prepare_dist_test( + broadcast_val=torch.tensor([True, 4000], dtype=torch.int64), + gather_val=[ + torch.tensor([False, 3000], dtype=torch.int64), + torch.tensor([True, 4000], dtype=torch.int64), + ], + ) + mock_train_func = self.get_mock_train_func(cuda_oom_bound=10000, max_runnable_bs=80) + batch_size = 40 + bs_search_algo = BsSearchAlgo(mock_train_func, 128, 1000) + w0_max_memory_reserved = mock_train_func(batch_size) + + cuda_oom, max_memory_reserved = bs_search_algo._try_batch_size(batch_size) + + # check dist.gather is called and get [cuda_oom, max_memory_reserved] as arguments. + self.mock_dist.gather.assert_called_once() + assert self.mock_dist.gather.call_args.args[0][0].item() == False + assert self.mock_dist.gather.call_args.args[0][1].item() == w0_max_memory_reserved + assert self.mock_dist.gather.call_args.kwargs["dst"] == 0 + # check if any process get cuda oom then set cuda_oom to True and + # set max_memory_reserved to maximum value of processes' + self.mock_dist.broadcast.assert_called_once() + self.mock_dist.broadcast.assert_called_once() + assert self.mock_dist.broadcast.call_args.kwargs["src"] == 0 + assert self.mock_dist.broadcast.call_args.args[0][0].item() == True + assert self.mock_dist.broadcast.call_args.args[0][1].item() == 4000 + # check proper values are returned + assert cuda_oom is True + assert max_memory_reserved == 4000 + + def test_auto_decrease_batch_size(self): + mock_train_func = self.get_mock_train_func(cuda_oom_bound=10000, max_runnable_bs=80) + + bs_search_algo = BsSearchAlgo(mock_train_func, 128, 1000) + adapted_bs = bs_search_algo.auto_decrease_batch_size() + + assert adapted_bs == 80 + + def test_find_max_usable_bs_gpu_memory_too_small(self): + mock_train_func = self.get_mock_train_func(cuda_oom_bound=4, max_runnable_bs=1) + + bs_search_algo = BsSearchAlgo(mock_train_func, 128, 1000) + with pytest.raises(RuntimeError): + bs_search_algo.auto_decrease_batch_size() + + @pytest.mark.parametrize( + "max_runnable_bs,max_bs,expected_bs", + [ + (100, 1000, None), + (32, 1000, None), + (100, 64, 64), + (66, 1000, None), + ], + ) + def test_find_big_enough_batch_size(self, max_runnable_bs, max_bs, expected_bs): + mock_train_func = self.get_mock_train_func(cuda_oom_bound=10000, max_runnable_bs=max_runnable_bs) + + bs_search_algo = BsSearchAlgo(mock_train_func, 64, max_bs) + adapted_bs = bs_search_algo.find_big_enough_batch_size() + + if expected_bs is None: + assert 7500 <= mock_train_func(adapted_bs) <= 8500 + else: + assert adapted_bs == expected_bs + + def test_find_big_enough_batch_size_gpu_memory_too_small(self): + mock_train_func = self.get_mock_train_func(cuda_oom_bound=4, max_runnable_bs=1) + + bs_search_algo = BsSearchAlgo(mock_train_func, 128, 1000) + with pytest.raises(RuntimeError): + bs_search_algo.find_big_enough_batch_size() + + def test_find_big_enough_batch_size_gradient_zero(self): + def mock_train_func(batch_size): + if batch_size > 1000: + mem_usage = 10000 + raise RuntimeError("CUDA out of memory.") + elif batch_size > 100: + mem_usage = 9000 + else: + mem_usage = 1000 + self.mock_torch.cuda.max_memory_reserved.return_value = mem_usage + return mem_usage + + bs_search_algo = BsSearchAlgo(mock_train_func, 64, 1000) + adapted_bs = bs_search_algo.find_big_enough_batch_size() + + assert adapted_bs == 100 + + def test_find_big_enough_batch_size_not_exceed_upper_bound(self): + def mock_train_func(batch_size): + if batch_size > 1000: + mem_usage = 10000 + raise RuntimeError("CUDA out of memory.") + elif batch_size > 100: + mem_usage = 9000 + else: + mem_usage = 1000 + batch_size / 1000 + self.mock_torch.cuda.max_memory_reserved.return_value = mem_usage + return mem_usage + + bs_search_algo = BsSearchAlgo(mock_train_func, 64, 1000) + adapted_bs = bs_search_algo.find_big_enough_batch_size() + + assert mock_train_func(adapted_bs) <= 8500 + + def test_find_big_enough_batch_size_drop_last(self): + mock_train_func = self.get_mock_train_func(cuda_oom_bound=10000, max_runnable_bs=180) + + bs_search_algo = BsSearchAlgo(mock_train_func, 64, 200) + adapted_bs = bs_search_algo.find_big_enough_batch_size(True) + + assert adapted_bs == 100 diff --git a/tests/unit/algorithms/common/adapters/torch/utils/test_utils.py b/tests/unit/algorithms/common/adapters/torch/utils/test_utils.py new file mode 100644 index 00000000000..f93f428e972 --- /dev/null +++ b/tests/unit/algorithms/common/adapters/torch/utils/test_utils.py @@ -0,0 +1,48 @@ +"""Tests for util functions related to torch.""" + +from unittest.mock import MagicMock + +import pytest + +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from otx.algorithms.common.adapters.torch.utils import utils as target_module +from otx.algorithms.common.adapters.torch.utils import model_from_timm, convert_sync_batchnorm + + +class OrdinaryModule: + def __init__(self): + self.module_arr = [MagicMock() for _ in range(3)] + + def modules(self): + return self.module_arr + + +def get_module(is_timm: bool = False): + module = OrdinaryModule() + if is_timm: + timm_sub_module = MagicMock() + timm_sub_module.__module__ = "timm.fake" + module.module_arr.append(timm_sub_module) + + return module + + +@e2e_pytest_unit +@pytest.mark.parametrize("is_timm", [True, False]) +def test_model_from_timm(is_timm): + assert model_from_timm(get_module(is_timm)) is is_timm + + +@e2e_pytest_unit +@pytest.mark.parametrize("is_timm", [True, False]) +def test_convert_sync_batchnorm(mocker, is_timm): + mock_timm_cvt_sycnbn = mocker.patch.object(target_module, "timm_cvt_sycnbn") + mock_torch = mocker.patch.object(target_module, "torch") + model = get_module(is_timm) + + convert_sync_batchnorm(model) + + if is_timm: + mock_timm_cvt_sycnbn.assert_called_once() + else: + mock_torch.nn.SyncBatchNorm.convert_sync_batchnorm.assert_called_once() diff --git a/tests/unit/algo/callbacks/__init__.py b/tests/unit/algorithms/common/configs/__init__.py similarity index 100% rename from tests/unit/algo/callbacks/__init__.py rename to tests/unit/algorithms/common/configs/__init__.py diff --git a/tests/unit/algorithms/common/configs/test_configuration_enums.py b/tests/unit/algorithms/common/configs/test_configuration_enums.py new file mode 100644 index 00000000000..f434b1cf71e --- /dev/null +++ b/tests/unit/algorithms/common/configs/test_configuration_enums.py @@ -0,0 +1,38 @@ +"""Tests for common configuration enums in OTX algorithms.""" +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from otx.algorithms.common.configs.configuration_enums import ( + POTQuantizationPreset, + StorageCacheScheme, + BatchSizeAdaptType, + InputSizePreset, +) + + +@e2e_pytest_unit +def test_pot_quansization_preset(): + assert len(POTQuantizationPreset) == 2 + + +@e2e_pytest_unit +def test_storage_cache_scheme(): + assert len(StorageCacheScheme) == 6 + + +@e2e_pytest_unit +def test_batsh_size_adapt_type(): + assert len(BatchSizeAdaptType) == 3 + + +@e2e_pytest_unit +def test_input_size_preset(): + assert len(InputSizePreset) == 10 + assert InputSizePreset.parse("xxx") == None + assert InputSizePreset.parse("Default") == None + assert InputSizePreset.parse("Auto") == (0, 0) + assert InputSizePreset.parse("1x1") == (1, 1) + assert InputSizePreset.DEFAULT.tuple == None + assert InputSizePreset.AUTO.tuple == (0, 0) + assert InputSizePreset._64x64.tuple == (64, 64) + input_sizes = InputSizePreset.input_sizes() + assert len(input_sizes) == 8 + assert input_sizes[-1] == (1024, 1024) diff --git a/tests/unit/algorithms/common/utils/__init__.py b/tests/unit/algorithms/common/utils/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/common/utils/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/common/utils/test_data.py b/tests/unit/algorithms/common/utils/test_data.py new file mode 100644 index 00000000000..1b7f2e2df1d --- /dev/null +++ b/tests/unit/algorithms/common/utils/test_data.py @@ -0,0 +1,107 @@ +"""Tests for data utils for common OTX algorithms.""" +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from otx.algorithms.common.utils.data import ( + compute_robust_statistics, + compute_robust_scale_statistics, + compute_robust_dataset_statistics, +) +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.image import Image +from otx.api.entities.annotation import Annotation, AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.scored_label import ScoredLabel, LabelEntity, Domain + +import numpy as np + + +@e2e_pytest_unit +def test_compute_robust_statistics(): + values = np.array([]) + stat = compute_robust_statistics(values) + assert len(stat) == 0 + + values = np.array([0.5, 1, 1.5]) + stat = compute_robust_statistics(values) + assert np.isclose(stat["avg"], 1.0) + assert np.isclose(stat["min"], 0.5) + assert np.isclose(stat["max"], 1.5) + + values = np.random.rand(10) + stat = compute_robust_statistics(values) + assert np.isclose(stat["min"], np.min(values)) + assert np.isclose(stat["max"], np.max(values)) + assert stat["min"] <= stat["robust_min"] + assert stat["max"] <= stat["robust_max"] + + +@e2e_pytest_unit +def test_compute_robust_scale_statistics(): + scales = np.array([]) + stat = compute_robust_scale_statistics(scales) + assert len(stat) == 0 + + scales = np.array([0.5, 1, 2]) + stat = compute_robust_scale_statistics(scales) + assert np.isclose(stat["avg"], 1.0) + assert np.isclose(stat["min"], 0.5) + assert np.isclose(stat["max"], 2.0) + + scales = np.random.rand(10) + stat = compute_robust_scale_statistics(scales) + assert np.isclose(stat["min"], np.min(scales)) + assert np.isclose(stat["max"], np.max(scales)) + assert stat["min"] <= stat["robust_min"] + assert stat["max"] <= stat["robust_max"] + + +@e2e_pytest_unit +def test_compute_robuste_dataset_statistics(): + dataset = DatasetEntity() + stat = compute_robust_dataset_statistics(dataset) + assert len(stat) == 0 + + label = ScoredLabel(label=LabelEntity(name="test", domain=Domain.DETECTION)) + dataset = DatasetEntity( + items=[ + DatasetItemEntity( + Image(data=np.random.rand(50, 50)), + AnnotationSceneEntity( + annotations=[ + Annotation(shape=Rectangle(x1=0.0, y1=0.0, x2=0.1, y2=0.1), labels=[label]), + ], + kind=AnnotationSceneKind.ANNOTATION, + ), + ), + DatasetItemEntity( + Image(data=np.random.rand(100, 100)), + AnnotationSceneEntity( + annotations=[ + Annotation(shape=Rectangle(x1=0.0, y1=0.0, x2=0.1, y2=0.1), labels=[label]), + Annotation(shape=Rectangle(x1=0.1, y1=0.1, x2=0.3, y2=0.3), labels=[label]), + ], + kind=AnnotationSceneKind.ANNOTATION, + ), + ), + DatasetItemEntity( + Image(data=np.random.rand(200, 200)), + AnnotationSceneEntity( + annotations=[], + kind=AnnotationSceneKind.ANNOTATION, + ), + ), + ] + ) + + stat = compute_robust_dataset_statistics(dataset, max_samples=0) + assert len(stat) == 0 + stat = compute_robust_dataset_statistics(dataset, max_samples=-1) + assert len(stat) == 0 + + stat = compute_robust_dataset_statistics(dataset, ann_stat=False) + assert np.isclose(stat["image"]["avg"], 100) + assert "annotation" not in stat + + stat = compute_robust_dataset_statistics(dataset, ann_stat=True) + assert np.isclose(stat["annotation"]["num_per_image"]["avg"], 1.0) + assert np.isclose(stat["annotation"]["size_of_shape"]["avg"], 10.0) diff --git a/tests/unit/algorithms/common/utils/test_dist_utils.py b/tests/unit/algorithms/common/utils/test_dist_utils.py new file mode 100644 index 00000000000..82423dc7f8e --- /dev/null +++ b/tests/unit/algorithms/common/utils/test_dist_utils.py @@ -0,0 +1,23 @@ +"""Tests for test_distance.py""" +from otx.algorithms.common.utils import dist_utils +from otx.algorithms.common.utils.dist_utils import append_dist_rank_suffix +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_append_dist_rank_suffix_not_distributed_training(mocker): + mock_os = mocker.patch.object(dist_utils, "os") + mock_os.environ = {} + file_name = "temporary.pth" + new_file_name = append_dist_rank_suffix(file_name) + + assert file_name == new_file_name + + +@e2e_pytest_unit +def test_append_dist_rank_suffix_distributed_training(mocker): + mock_os = mocker.patch.object(dist_utils, "os") + mock_os.environ = {"LOCAL_RANK": "2"} + new_file_name = append_dist_rank_suffix("temporary.pth") + + assert new_file_name == "temporary_proc2.pth" diff --git a/tests/unit/algorithms/common/utils/test_utils.py b/tests/unit/algorithms/common/utils/test_utils.py new file mode 100644 index 00000000000..324279016ed --- /dev/null +++ b/tests/unit/algorithms/common/utils/test_utils.py @@ -0,0 +1,28 @@ +"""Tests for Utils for common OTX algorithms.""" +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from otx.algorithms.common.utils.utils import embed_onnx_model_data + +import onnx + + +class Meta: + def __init__(self): + self.key = None + self.value = None + + +class MockModel: + def __init__(self): + self.metadata_props = self + + def add(self): + return Meta() + + +@e2e_pytest_unit +def test_embed_onnx_model_data(mocker): + mocker.patch.object(onnx, "load", return_value=MockModel()) + mocker.patch.object(onnx, "save") + data = {(str("model_info"),): "info"} + + embed_onnx_model_data("", data) diff --git a/tests/unit/algorithms/detection/__init__.py b/tests/unit/algorithms/detection/__init__.py new file mode 100644 index 00000000000..2faffbe2b1f --- /dev/null +++ b/tests/unit/algorithms/detection/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/tests/unit/algorithms/detection/adapters/__init__.py b/tests/unit/algorithms/detection/adapters/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/api/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/api/__init__.py new file mode 100644 index 00000000000..b68d52460fd --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/api/__init__.py @@ -0,0 +1,5 @@ +"""Test for otx.algorithms.detection.adapters.mmdet.api""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/api/test_train.py b/tests/unit/algorithms/detection/adapters/mmdet/api/test_train.py new file mode 100644 index 00000000000..1048cabbe2c --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/api/test_train.py @@ -0,0 +1,111 @@ +"""Test for otx.algorithms.detection.adapters.mmdet.apis.train""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from unittest import mock +from otx.algorithms.detection.adapters.mmdet.apis.train import train_detector +import mmcv +import os +import torch +from otx.algorithms.common.utils.utils import is_xpu_available + + +class TestTrainDetector: + @pytest.fixture + def mock_modules(self, mocker): + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.apis.train.get_root_logger", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch("otx.algorithms.detection.adapters.mmdet.apis.train.build_dp", return_value=mock.MagicMock()) + mocker.patch("otx.algorithms.detection.adapters.mmdet.apis.train.build_ddp", return_value=mock.MagicMock()) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.apis.train.build_optimizer", return_value=mock.MagicMock() + ) + mocker.patch("otx.algorithms.detection.adapters.mmdet.apis.train.build_runner", return_value=mock.MagicMock()) + mocker.patch("otx.algorithms.detection.adapters.mmdet.apis.train.build_dataset", return_value=mock.MagicMock()) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch("otx.algorithms.detection.adapters.mmdet.apis.train.DistEvalHook", return_value=mock.MagicMock()) + mocker.patch("otx.algorithms.detection.adapters.mmdet.apis.train.EvalHook", return_value=mock.MagicMock()) + + @pytest.fixture + def mmcv_cfg(self): + return mmcv.Config( + { + "gpu_ids": [0], + "seed": 42, + "data": mock.MagicMock(), + "device": "cpu", + "optimizer": "Adam", + "optimizer_config": {}, + "total_epochs": 1, + "work_dir": "test", + "lr_config": {}, + "checkpoint_config": {}, + "log_config": {}, + "resume_from": False, + "load_from": "", + "workflow": "", + "log_level": 1, + "total_iters": 1000, + } + ) + + @pytest.fixture + def model(self): + return mock.MagicMock() + + @pytest.fixture + def dataset(self): + return mock.MagicMock() + + def test_train_model_single_dataset_no_validation(self, mock_modules, mmcv_cfg, model, dataset): + # Create mock inputs + _ = mock_modules + # Call the function + train_detector(model, dataset, mmcv_cfg, validate=False) + + def test_train_model_multiple_datasets_distributed_training(self, mock_modules, mmcv_cfg, model, dataset): + # Create mock inputs + _ = mock_modules + os.environ["LOCAL_RANK"] = "0" + # Call the function + train_detector(model, [dataset, dataset], mmcv_cfg, distributed=True, validate=True) + + def test_train_model_specific_timestamp_and_cuda_device(self, mock_modules, mmcv_cfg, model, dataset, mocker): + # Create mock inputs + _ = mock_modules + timestamp = "2024-01-01" + mmcv_cfg.device = "cuda" + meta = {"info": "some_info"} + + # Call the function + train_detector(model, dataset, mmcv_cfg, timestamp=timestamp, meta=meta) + + def test_train_model_xpu_device(self, mock_modules, mmcv_cfg, model, dataset, mocker): + # Create mock inputs + _ = mock_modules + mmcv_cfg.device = "xpu" + mocker.patch("otx.algorithms.detection.adapters.mmdet.apis.train.torch") + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.apis.train.torch.xpu.optimize", + return_value=(mocker.MagicMock(), mocker.MagicMock()), + ) + # Call the function + train_detector(model, dataset, mmcv_cfg) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/datasets/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/datasets/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/datasets/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/test_load_pipelines.py b/tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/test_load_pipelines.py new file mode 100644 index 00000000000..f03c54085fd --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/test_load_pipelines.py @@ -0,0 +1,127 @@ +import numpy as np +import pytest +from PIL import Image +from typing import Iterator, List, Optional, Sequence, Tuple + +from otx.algorithms.detection.adapters.mmdet.datasets.pipelines import ( + LoadResizeDataFromOTXDataset, + ResizeTo, +) +from otx.api.entities.model_template import TaskType +from otx.core.data.caching import MemCacheHandlerSingleton +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import generate_det_dataset + + +@e2e_pytest_unit +def test_load_resize_data_from_otx_dataset_call(mocker): + """Test LoadResizeDataFromOTXDataset.""" + otx_dataset, labels = generate_det_dataset( + TaskType.INSTANCE_SEGMENTATION, # covers det & iseg format both + image_width=320, + image_height=320, + ) + MemCacheHandlerSingleton.create("singleprocessing", otx_dataset[0].numpy.size) + op = LoadResizeDataFromOTXDataset( + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict(type="ResizeTo", img_scale=(32, 16), keep_ratio=False), # 320x320 -> 16x32 + ) + src_dict = dict( + dataset_item=otx_dataset[0], + width=otx_dataset[0].width, + height=otx_dataset[0].height, + index=0, + ann_info=dict(label_list=labels), + bbox_fields=[], + mask_fields=[], + ) + dst_dict = op(src_dict) + assert dst_dict["ori_shape"][0] == 320 + assert dst_dict["img_shape"][0] == 16 # height + assert dst_dict["img"].shape == dst_dict["img_shape"] + assert dst_dict["gt_masks"].width == 32 + assert dst_dict["gt_masks"].height == 16 + op._load_img = mocker.MagicMock() + dst_dict_from_cache = op(src_dict) + assert op._load_img.call_count == 0 # _load_img() should not be called + assert np.array_equal(dst_dict["img"], dst_dict_from_cache["img"]) + assert (dst_dict["gt_labels"] == dst_dict_from_cache["gt_labels"]).all() + assert (dst_dict["gt_bboxes"] == dst_dict_from_cache["gt_bboxes"]).all() + assert dst_dict["gt_masks"] == dst_dict_from_cache["gt_masks"] + + +@e2e_pytest_unit +def test_load_resize_data_from_otx_dataset_downscale_only(mocker): + """Test LoadResizeDataFromOTXDataset.""" + otx_dataset, labels = generate_det_dataset( + TaskType.INSTANCE_SEGMENTATION, # covers det & iseg format both + image_width=320, + image_height=320, + ) + MemCacheHandlerSingleton.create("singleprocessing", otx_dataset[0].numpy.size) + op = LoadResizeDataFromOTXDataset( + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + domain="instance_segmentation", + with_bbox=True, + with_mask=True, + poly2mask=False, + ), + resize_cfg=dict(type="ResizeTo", img_scale=(640, 640), downscale_only=True), # 320x320 -> 16x32 + ) + src_dict = dict( + dataset_item=otx_dataset[0], + width=otx_dataset[0].width, + height=otx_dataset[0].height, + index=0, + ann_info=dict(label_list=labels), + bbox_fields=[], + mask_fields=[], + ) + dst_dict = op(src_dict) + assert dst_dict["ori_shape"][0] == 320 + assert dst_dict["img_shape"][0] == 320 # Skipped upscale + assert dst_dict["img"].shape == dst_dict["img_shape"] + op._load_img_op = mocker.MagicMock() + dst_dict_from_cache = op(src_dict) + assert op._load_img_op.call_count == 0 # _load_img() should not be called + assert np.array_equal(dst_dict["img"], dst_dict_from_cache["img"]) + assert (dst_dict["gt_labels"] == dst_dict_from_cache["gt_labels"]).all() + assert (dst_dict["gt_bboxes"] == dst_dict_from_cache["gt_bboxes"]).all() + assert dst_dict["gt_masks"] == dst_dict_from_cache["gt_masks"] + + +@e2e_pytest_unit +def test_resize_to(mocker): + """Test ResizeTo.""" + src_dict = dict( + img=np.random.randint(0, 10, (16, 16, 3), dtype=np.uint8), + img_fields=["img"], + ori_shape=(16, 16), + img_shape=(16, 16), + ) + # Test downscale + op = ResizeTo(img_scale=(4, 4)) + dst_dict = op(src_dict) + assert dst_dict["ori_shape"][0] == 16 + assert dst_dict["img_shape"][0] == 4 + assert dst_dict["img"].shape == dst_dict["img_shape"] + # Test upscale from output + op = ResizeTo(img_scale=(8, 8)) + dst_dict = op(dst_dict) + assert dst_dict["ori_shape"][0] == 16 + assert dst_dict["img_shape"][0] == 8 + assert dst_dict["img"].shape == dst_dict["img_shape"] + # Test same size from output + op = ResizeTo(img_scale=(8, 8)) + op._resize_img = mocker.MagicMock() + dst_dict = op(dst_dict) + assert dst_dict["ori_shape"][0] == 16 + assert dst_dict["img_shape"][0] == 8 + assert op._resize_img.call_count == 0 # _resize_img() should not be called diff --git a/tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/test_torchvision2mmdet.py b/tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/test_torchvision2mmdet.py new file mode 100644 index 00000000000..f96dd5fe455 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/datasets/pipelines/test_torchvision2mmdet.py @@ -0,0 +1,188 @@ +"""Unit Tests for the OTX Dataset Pipeline Torchvision to MMDet.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import numpy as np +import pytest +import torch +from PIL import Image, ImageFilter +from torch import Tensor + +from otx.algorithms.detection.adapters.mmdet.datasets.pipelines.torchvision2mmdet import ( + BranchImage, + ColorJitter, + NDArrayToPILImage, + NDArrayToTensor, + PILImageToNDArray, + RandomApply, + RandomErasing, + RandomGaussianBlur, + RandomGrayscale, +) + + +@pytest.fixture +def data() -> dict[str, np.ndarray]: + return {"img": np.ones((256, 256, 3), dtype=np.uint8)} + + +@pytest.fixture() +def image_tensor() -> Tensor: + return torch.rand(3, 256, 256) + + +class TestColorJitter: + def test_call(self, data: dict[str, np.ndarray]) -> None: + """Test __call__ method of ColorJitter.""" + transform = ColorJitter() + outputs = transform(data) + assert outputs.keys() == data.keys() + assert np.array_equal(outputs["img"], data["img"]) + + def test_repr(self) -> None: + """Test __repr__ method of ColorJitter.""" + transform = ColorJitter(brightness=0.2) + assert str(transform) in [ + "ColorJitter(brightness=[0.8, 1.2], contrast=None, saturation=None, hue=None)", + "ColorJitter(brightness=(0.8, 1.2), contrast=None, saturation=None, hue=None)", + ] + + +class TestRandomGrayscale: + def test_random_grayscale(self, image_tensor: Tensor) -> None: + """Test random grayscale.""" + inputs = {"img": image_tensor} + pipeline = RandomGrayscale(p=0.5) + outputs = pipeline.forward(inputs) + + assert isinstance(outputs, dict) + assert set(outputs.keys()) == set(inputs.keys()) + assert outputs["img"].shape == inputs["img"].shape + + +class TestRandomErasing: + def test_random_erasing(self, image_tensor: Tensor) -> None: + """Test random erasing.""" + transform = RandomErasing(p=1.0, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0, inplace=False) + + data = {"img": image_tensor} + transformed_data = transform(data) + + assert "img" in transformed_data + assert transformed_data["img"].shape == data["img"].shape + + +class TestRandomGaussianBlur: + def test_random_gaussian_blur(self) -> None: + """Test initialization.""" + sigma_min = 0.1 + sigma_max = 2.0 + pipeline = RandomGaussianBlur(sigma_min, sigma_max) + assert pipeline.sigma_min == sigma_min + assert pipeline.sigma_max == sigma_max + + # Test forward pass + inputs = {"img": Image.fromarray(np.zeros((256, 256, 3), dtype=np.uint8))} + outputs = pipeline(inputs) + assert outputs.keys() == inputs.keys() + assert isinstance(outputs["img"], Image.Image) + + # Test that the output image is blurred + blur_radius = (pipeline.sigma_min + pipeline.sigma_max) / 2 + blurred_image = inputs["img"].filter(ImageFilter.GaussianBlur(radius=blur_radius)) + assert np.array_equal(np.array(outputs["img"]), np.array(blurred_image)) + + def test_repr(self) -> None: + """Test __repr__ method of RandomGaussianBlur.""" + pipeline = RandomGaussianBlur(0.1, 2.0) + assert repr(pipeline) == "RandomGaussianBlur" + + +class TestRandomApply: + def test_random_apply_with(self) -> None: + """Test RandomApply with a single transform.""" + # Define the transforms to be applied randomly + transform_cfgs = [ + dict( + type="ColorJitter", + brightness=0.4, + contrast=0.4, + saturation=0.4, + hue=0.1, + ) + ] + + # Create the RandomApply pipeline + random_apply = RandomApply(transform_cfgs, p=0.0) + + # Define the inputs and expected outputs + inputs = {"img": Image.fromarray(np.ones((256, 256, 3), dtype=np.uint8))} + results = random_apply(inputs) + assert np.allclose(np.array(results["img"]), np.array(inputs["img"])) + + +class TestNDArrayToTensor: + def test_ndarray_to_tensor_with_single_channel_image(self, data: dict[str, np.ndarray]) -> None: + """Test NDArrayToTensor with a single channel image.""" + pipeline = NDArrayToTensor(keys=["img"]) + output = pipeline(data) + + assert output["img"].shape == (3, 256, 256) + assert isinstance(output["img"], torch.Tensor) + + +class TestNDArrayToPILImage: + def test_ndarray_to_pil_conversion(self, data: dict[str, np.ndarray]) -> None: + """Test NDArrayToPILImage with a three channel image.""" + pipeline = NDArrayToPILImage(keys=["img"]) + output = pipeline(data) + + assert isinstance(output["img"], Image.Image) + assert output["img"].size == (256, 256) + + def test_rept(self) -> None: + """Test __repr__ method of NDArrayToPILImage.""" + pipeline = NDArrayToPILImage(keys=["image"]) + assert repr(pipeline) == "NDArrayToPILImage" + + +class TestPILImageToNDArray: + def test_call(self, data: dict[str, np.ndarray]) -> None: + """Test __call__ method of PILImageToNDArray.""" + pipeline = PILImageToNDArray(keys=["image"]) + data = {"image": Image.fromarray(data["img"])} + output = pipeline(data) + + assert isinstance(output["image"], np.ndarray) + assert output["image"].shape == (256, 256, 3) + + def test_repr(self) -> None: + """Test __repr__ method of PILImageToNDArray.""" + pipeline = PILImageToNDArray(keys=["image"]) + assert repr(pipeline) == "PILImageToNDArray" + + +class TestBranchImage: + def test_branch_image(self) -> None: + """Test BranchImage pipeline.""" + # Test data + data = {"img": "test.jpg", "label": 0, "img_fields": ["img"]} + + # Call the pipeline + key_map = {"img": "img2", "label": "label2"} + pipeline = BranchImage(key_map) + results = pipeline(data) + + # Check that the results have been updated correctly + assert results["img2"] == "test.jpg" + assert results["label2"] == 0 + assert "img2" in data["img_fields"] + assert "label2" not in data["img_fields"] + + def test_repr(self) -> None: + """Test __repr__ method of BranchImage.""" + pipeline = BranchImage() + assert repr(pipeline) == "BranchImage" diff --git a/tests/unit/algorithms/detection/adapters/mmdet/datasets/test_detection_dataset.py b/tests/unit/algorithms/detection/adapters/mmdet/datasets/test_detection_dataset.py new file mode 100644 index 00000000000..24b12886406 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/datasets/test_detection_dataset.py @@ -0,0 +1,236 @@ +"""Unit Test for otx.algorithms.detection.adapters.mmdet.data.dataset.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import numpy as np +from otx.algorithms.common.utils.utils import is_xpu_available +import pytest + +from otx.algorithms.detection.adapters.mmdet.datasets.dataset import OTXDetDataset, get_annotation_mmdet_format +from otx.api.entities.label import Domain +from otx.api.entities.model_template import TaskType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import ( + MockPipeline, + generate_det_dataset, +) +from mmdet.core.mask.structures import BitmapMasks +import pycocotools.mask as mask_util + +from otx.algorithms.detection.utils import create_detection_shapes, create_mask_shapes + + +class TestOTXDetDataset: + """ + Test OTXDetDataset class. + 1. Test _DataInfoProxy + 2. Test prepare_train_img + 3. Test prepare_test_img + 4. Test get_ann_info + 5. Test evaluate + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.dataset = dict() + for task_type in [TaskType.DETECTION, TaskType.INSTANCE_SEGMENTATION]: + self.dataset[task_type] = generate_det_dataset(task_type=task_type) + self.pipeline = [] + + @e2e_pytest_unit + @pytest.mark.parametrize("task_type", [TaskType.DETECTION, TaskType.INSTANCE_SEGMENTATION]) + def test_DataInfoProxy(self, task_type): + """Test _DataInfoProxy Class.""" + otx_dataset, labels = self.dataset[task_type] + proxy = OTXDetDataset._DataInfoProxy(otx_dataset, labels) + sample = proxy[0] + assert "dataset_item" in sample + assert "width" in sample + assert "height" in sample + assert "index" in sample + assert "ann_info" in sample + assert "ignored_labels" in sample + + @e2e_pytest_unit + @pytest.mark.parametrize( + "task_type, domain", + [(TaskType.DETECTION, Domain.DETECTION), (TaskType.INSTANCE_SEGMENTATION, Domain.INSTANCE_SEGMENTATION)], + ) + def test_prepare_train_img(self, task_type, domain) -> None: + """Test prepare_train_img method""" + otx_dataset, labels = self.dataset[task_type] + dataset = OTXDetDataset(otx_dataset, labels, self.pipeline, test_mode=False) + img = dataset.prepare_train_img(0) + assert isinstance(img, dict) + assert "dataset_item" in img + assert "bbox_fields" in img + assert "mask_fields" in img + assert "seg_fields" in img + + @e2e_pytest_unit + @pytest.mark.parametrize( + "task_type, domain", + [(TaskType.DETECTION, Domain.DETECTION), (TaskType.INSTANCE_SEGMENTATION, Domain.INSTANCE_SEGMENTATION)], + ) + def test_prepare_test_img(self, task_type, domain) -> None: + """Test prepare_test_img method""" + otx_dataset, labels = self.dataset[task_type] + dataset = OTXDetDataset(otx_dataset, labels, self.pipeline, test_mode=True) + img = dataset.prepare_test_img(0) + assert isinstance(img, dict) + assert "dataset_item" in img + assert "bbox_fields" in img + assert "mask_fields" in img + assert "seg_fields" in img + + @e2e_pytest_unit + @pytest.mark.parametrize( + "task_type, domain", + [(TaskType.DETECTION, Domain.DETECTION), (TaskType.INSTANCE_SEGMENTATION, Domain.INSTANCE_SEGMENTATION)], + ) + def test_get_ann_info(self, task_type, domain) -> None: + """Test get_ann_info method""" + otx_dataset, labels = self.dataset[task_type] + dataset = OTXDetDataset(otx_dataset, labels, self.pipeline) + dataset.pipeline = MockPipeline() + ann_info = dataset.get_ann_info(0) + assert isinstance(ann_info, dict) + assert "bboxes" in ann_info + assert "masks" in ann_info + assert "labels" in ann_info + + @e2e_pytest_unit + @pytest.mark.parametrize( + "task_type, domain", + [(TaskType.DETECTION, Domain.DETECTION), (TaskType.INSTANCE_SEGMENTATION, Domain.INSTANCE_SEGMENTATION)], + ) + @pytest.mark.parametrize("metric", ["mAP"]) + @pytest.mark.parametrize("logger", ["silent", None]) + def test_evaluate(self, task_type, domain, metric, logger) -> None: + """Test evaluate method for detection and instance segmentation""" + otx_dataset, labels = self.dataset[task_type] + dataset = OTXDetDataset(otx_dataset, labels, self.pipeline) + dataset.pipeline = MockPipeline() + sample = dataset[0] + if task_type == TaskType.DETECTION: + results = [[np.random.rand(1, 5)]] + elif task_type == TaskType.INSTANCE_SEGMENTATION: + if is_xpu_available(): + pytest.skip("Subprocess failure in XPU environment") + results = [ + ( + [np.random.rand(1, 5)] * len(otx_dataset.get_labels()), + [[{"size": [sample["width"], sample["height"]], "counts": b"1"}]] * len(otx_dataset.get_labels()), + ) + ] + eval_results = dataset.evaluate(results, metric, logger) + assert isinstance(eval_results, dict) + assert metric in eval_results + + @e2e_pytest_unit + def test_mask_evaluate(self) -> None: + """Test evaluate method for instance segmentation""" + if is_xpu_available(): + pytest.skip("Subprocess failure in XPU environment") + otx_dataset, labels = self.dataset[TaskType.INSTANCE_SEGMENTATION] + dataset = OTXDetDataset(otx_dataset, labels, self.pipeline) + dataset.pipeline = MockPipeline() + sample = dataset[0] + + num_classes = len(dataset.labels) + anno = get_annotation_mmdet_format(sample["dataset_item"], dataset.labels, Domain.INSTANCE_SEGMENTATION) + bboxes = anno["bboxes"] + scores = np.random.random((len(bboxes), 1)) + bboxes = np.hstack((bboxes, scores)) + labels = anno["labels"] + masks = mask_util.encode(np.full((28, 28, len(bboxes)), 1, dtype=np.uint8, order="F")) + + bbox_results = [bboxes[labels == i, :] for i in range(num_classes)] + mask_results = [list(np.array(masks)[labels == i]) for i in range(num_classes)] + results = [(bbox_results, mask_results)] + eval_results = dataset.evaluate(results, "mAP", None) + assert isinstance(eval_results, dict) + assert eval_results["mAP"] >= 0.0 + + @e2e_pytest_unit + @pytest.mark.parametrize("use_ellipse_shapes", [True, False]) + def test_create_detection_shape(self, use_ellipse_shapes) -> None: + """Test create_detection_shapes method""" + otx_dataset, labels = self.dataset[TaskType.DETECTION] + dataset = OTXDetDataset(otx_dataset, labels, self.pipeline) + dataset.pipeline = MockPipeline() + sample = dataset[0] + h, w = sample["dataset_item"].height, sample["dataset_item"].width + + num_classes = len(dataset.labels) + anno = get_annotation_mmdet_format(sample["dataset_item"], dataset.labels, Domain.DETECTION) + bboxes = anno["bboxes"] + scores = np.full((len(bboxes), 1), 0.5, dtype=np.float32) + bboxes = np.hstack((bboxes, scores)) + labels = anno["labels"] + pred_results = [] + for i in range(num_classes): + bboxes_i = bboxes[labels == i, :] + pred_results.append(bboxes_i) + + shapes = create_detection_shapes( + pred_results, + width=w, + height=h, + confidence_threshold=0.0, + use_ellipse_shapes=use_ellipse_shapes, + labels=dataset.labels, + ) + assert len(shapes) > 0, "Shapes should be created for confidence_threshold=1.0" + + shapes = create_detection_shapes( + pred_results, + width=w, + height=h, + confidence_threshold=0.6, + use_ellipse_shapes=use_ellipse_shapes, + labels=dataset.labels, + ) + assert len(shapes) == 0, "No shapes should be created for confidence_threshold=0.0" + + @e2e_pytest_unit + @pytest.mark.parametrize("use_ellipse_shapes", [True, False]) + def test_create_mask_shape(self, use_ellipse_shapes) -> None: + """Test create_mask_shapes method""" + otx_dataset, labels = self.dataset[TaskType.INSTANCE_SEGMENTATION] + dataset = OTXDetDataset(otx_dataset, labels, self.pipeline) + dataset.pipeline = MockPipeline() + sample = dataset[0] + h, w = sample["dataset_item"].height, sample["dataset_item"].width + + num_classes = len(dataset.labels) + anno = get_annotation_mmdet_format(sample["dataset_item"], dataset.labels, Domain.INSTANCE_SEGMENTATION) + bboxes = anno["bboxes"] + scores = np.full((len(bboxes), 1), 0.5, dtype=np.float32) + bboxes = np.hstack((bboxes, scores)) + labels = anno["labels"] + masks = mask_util.encode(np.full((28, 28, len(bboxes)), 1, dtype=np.uint8, order="F")) + + bbox_results = [bboxes[labels == i, :] for i in range(num_classes)] + mask_results = [list(np.array(masks)[labels == i]) for i in range(num_classes)] + pred_results = (bbox_results, mask_results) + shapes = create_mask_shapes( + pred_results, + width=w, + height=h, + confidence_threshold=0.0, + use_ellipse_shapes=use_ellipse_shapes, + labels=dataset.labels, + ) + assert len(shapes) > 0, "Shapes should be created for confidence_threshold=1.0" + + shapes = create_mask_shapes( + pred_results, + width=w, + height=h, + confidence_threshold=0.6, + use_ellipse_shapes=use_ellipse_shapes, + labels=dataset.labels, + ) + assert len(shapes) == 0, "No shapes should be created for confidence_threshold=0.0" diff --git a/tests/unit/algorithms/detection/adapters/mmdet/hooks/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/hooks/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/hooks/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/hooks/test_det_class_probability_map_hook.py b/tests/unit/algorithms/detection/adapters/mmdet/hooks/test_det_class_probability_map_hook.py new file mode 100644 index 00000000000..a63bc766138 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/hooks/test_det_class_probability_map_hook.py @@ -0,0 +1,100 @@ +import pytest +import torch + +from otx.algorithms.detection.adapters.mmdet.hooks.det_class_probability_map_hook import ( + DetClassProbabilityMapHook, +) +from otx.algorithms.detection.adapters.mmdet.models.heads.custom_atss_head import ( + CustomATSSHead, +) +from otx.algorithms.detection.adapters.mmdet.models.heads.custom_ssd_head import ( + CustomSSDHead, +) +from otx.algorithms.detection.adapters.mmdet.models.heads.custom_vfnet_head import ( + CustomVFNetHead, +) +from otx.algorithms.detection.adapters.mmdet.models.heads.custom_yolox_head import ( + CustomYOLOXHead, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestDetClassProbabilityMapHook: + """Test class for DetClassProbabilityMapHook.""" + + @pytest.fixture(autouse=True) + def setup(self) -> None: + class _MockModule(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.with_neck = True + self.neck = torch.nn.Module() + self.neck.forward = self.forward + self.bbox_head = torch.nn.Module() + self.bbox_head.cls_out_channels = 3 + + def forward(self, x): + return x + + self.module = _MockModule() + self.hook = DetClassProbabilityMapHook(self.module) + + @e2e_pytest_unit + def test_func(self, mocker) -> None: + """Test func function.""" + + mocker.patch.object( + DetClassProbabilityMapHook, "_get_cls_scores_from_feature_map", return_value=[torch.randn(1, 3, 14, 14)] + ) + assert self.hook.func(torch.randn(1, 3, 14, 14)) is not None + + @e2e_pytest_unit + def test_get_cls_scores_from_feature_map_atss(self) -> None: + """Test _get_cls_scores_from_feature_map function.""" + + self.module.bbox_head = CustomATSSHead(num_classes=3, in_channels=64) + self.hook = DetClassProbabilityMapHook(self.module) + assert self.hook._get_cls_scores_from_feature_map(torch.Tensor(1, 3, 64, 32, 32)) is not None + + @e2e_pytest_unit + def test_get_cls_scores_from_feature_map_yolox(self) -> None: + """Test _get_cls_scores_from_feature_map function.""" + + self.module.bbox_head = CustomYOLOXHead(num_classes=3, in_channels=64) + self.hook = DetClassProbabilityMapHook(self.module) + assert self.hook._get_cls_scores_from_feature_map(torch.Tensor(1, 3, 64, 32, 32)) is not None + + @e2e_pytest_unit + def test_get_cls_scores_from_feature_map_vfnet(self) -> None: + """Test _get_cls_scores_from_feature_map function.""" + + self.module.bbox_head = CustomVFNetHead(num_classes=3, in_channels=64) + self.module.bbox_head.anchor_generator.num_base_anchors = 1 + self.hook = DetClassProbabilityMapHook(self.module) + assert self.hook._get_cls_scores_from_feature_map(torch.Tensor(1, 3, 64, 32, 32)) is not None + + @e2e_pytest_unit + def test_get_cls_scores_from_feature_map_ssd(self) -> None: + """Test _get_cls_scores_from_feature_map function.""" + + self.module.bbox_head = CustomSSDHead( + anchor_generator=dict( + type="SSDAnchorGenerator", + basesize_ratio_range=(0.15, 0.9), + strides=(16, 32, 48), + ratios=[[0.5], [0.1], [0.3]], + ), + act_cfg={}, + ) + self.hook = DetClassProbabilityMapHook(self.module) + assert self.hook._get_cls_scores_from_feature_map(torch.Tensor(1, 3, 512, 32, 32)) is not None + + @e2e_pytest_unit + def test_get_cls_scores_from_feature_map_not_implemented_head(self) -> None: + """Test _get_cls_scores_from_feature_map function.""" + + self.module.bbox_head = torch.nn.Module() + self.module.bbox_head.cls_out_channels = 3 + self.hook = DetClassProbabilityMapHook(self.module) + with pytest.raises(NotImplementedError): + self.hook._get_cls_scores_from_feature_map(torch.Tensor(1, 3, 512, 32, 32)) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/hooks/test_tile_sampling_hook.py b/tests/unit/algorithms/detection/adapters/mmdet/hooks/test_tile_sampling_hook.py new file mode 100644 index 00000000000..bb8b76353dc --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/hooks/test_tile_sampling_hook.py @@ -0,0 +1,47 @@ +"""Test tiling sampling hook.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import random + +from otx.algorithms.detection.adapters.mmdet.hooks.tile_sampling_hook import TileSamplingHook +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestTilingSamplingHook: + """Test class for TileSamplingHook.""" + + @e2e_pytest_unit + def test_before_epoch(self, mocker): + "Test function for before_poch function." + + class MockTileDataset: + def __init__(self): + self.tiles_all = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + self.sample_num = 4 + self.tiles = [1, 2, 3, 4] + + class MockDataset: + def __init__(self, tile_dataset): + self.tile_dataset = tile_dataset + + class MockDataLoader: + def __init__(self, dataset): + self.dataset = dataset + + class MockRunner: + def __init__(self, data_loader): + self.data_loader = data_loader + + hook = TileSamplingHook() + tile_dataset = MockTileDataset() + runner = MockRunner(MockDataLoader(MockDataset(tile_dataset))) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.hooks.tile_sampling_hook.sample", return_value=[5, 6, 7, 8] + ) + hook.before_epoch(runner) + assert tile_dataset.tiles[0] == 5 + assert tile_dataset.tiles[1] == 6 + assert tile_dataset.tiles[2] == 7 + assert tile_dataset.tiles[3] == 8 diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/models/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/assigners/test_custom_max_iou_assigner.py b/tests/unit/algorithms/detection/adapters/mmdet/models/assigners/test_custom_max_iou_assigner.py new file mode 100644 index 00000000000..d873190b04f --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/assigners/test_custom_max_iou_assigner.py @@ -0,0 +1,57 @@ +"""Unit test for cusom max iou assigner.""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.detection.adapters.mmdet.models.assigners import CustomMaxIoUAssigner +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomMaxIoUAssigner: + @pytest.fixture(autouse=True) + def setup(self): + """Initial setup for unit tests.""" + self.assigner = CustomMaxIoUAssigner( + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=True, + ignore_iof_thr=-1, + gpu_assign_thr=300, + ) + self.assigner.cpu_assign_thr = 400 + + @e2e_pytest_unit + def test_assign_gpu(self): + """Test custom assign function on gpu.""" + gt_bboxes = torch.randn(200, 4) + bboxes = torch.randn(20000, 4) + assign_result = self.assigner.assign(bboxes, gt_bboxes) + assert assign_result.gt_inds.shape == torch.Size([20000]) + assert assign_result.max_overlaps.shape == torch.Size([20000]) + + @e2e_pytest_unit + def test_assign_cpu(self): + """Test custom assign function on cpu.""" + gt_bboxes = torch.randn(350, 4) + bboxes = torch.randn(20000, 4) + assign_result = self.assigner.assign(bboxes, gt_bboxes) + assert assign_result.gt_inds.shape == torch.Size([20000]) + assert assign_result.max_overlaps.shape == torch.Size([20000]) + + @e2e_pytest_unit + def test_assign_cpu_oom(self): + """Test custom assign function on cpu in case of cpu oom.""" + gt_bboxes = torch.randn(450, 4) + bboxes = torch.randn(20000, 4) + assign_result = self.assigner.assign(bboxes, gt_bboxes) + assert assign_result.gt_inds.shape == torch.Size([20000]) + assert assign_result.max_overlaps.shape == torch.Size([20000]) + + self.assigner_cpu_assign_thr = 500 + new_assign_result = self.assigner.assign(bboxes, gt_bboxes) + assert torch.all(new_assign_result.gt_inds == assign_result.gt_inds) + assert torch.all(new_assign_result.max_overlaps == assign_result.max_overlaps) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/backones/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/models/backones/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/backones/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/backones/test_ov_mmdet_mmov_backbone.py b/tests/unit/algorithms/detection/adapters/mmdet/models/backones/test_ov_mmdet_mmov_backbone.py new file mode 100644 index 00000000000..5a1f1ead7e4 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/backones/test_ov_mmdet_mmov_backbone.py @@ -0,0 +1,44 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import openvino.runtime as ov +import pytest +import torch + +from otx.algorithms.detection.adapters.mmdet.models.backbones.mmov_backbone import ( + MMOVBackbone, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMMOVBackbone: + @pytest.fixture(autouse=True) + def setup(self): + + param = ov.opset10.parameter([1, 3, 64, 64], ov.Type.f32, name="in") + filter = ov.opset10.constant(np.random.normal(size=(1, 3, 64, 64)), ov.Type.f32) + mul = ov.opset10.matmul(param, filter, False, False) + result = ov.opset10.result(mul, name="out") + ov_model = ov.Model([result], [param], "det_backbone") + + self.model = MMOVBackbone( + model_path_or_model=ov_model, + remove_normalize=True, + merge_bn=True, + paired_bn=True, + ) + + @e2e_pytest_unit + def test_init_weights(self): + self.model.init_weights() + + @e2e_pytest_unit + def test_forward(self): + data = {} + for key, shape in self.model.input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data[key] = torch.randn(shape) + self.model.train() + self.model(list(data.values()), torch.tensor([0])) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/conftest.py b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/conftest.py new file mode 100644 index 00000000000..0db005f1b67 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/conftest.py @@ -0,0 +1,186 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict + +import mmcv +import pytest +import torch +from mmdet.models.builder import build_head + +from otx.algorithms.detection.adapters.mmdet.models.heads import * + + +@pytest.fixture +def fxt_head_input( + img_size=256, + n_bboxes=3, + n_classes=4, + batch_size=8, + n_channels=64, +): + img_metas = [ + {"img_shape": (img_size, img_size, 3), "scale_factor": 1, "pad_shape": (img_size, img_size, 3)} + for _ in range(batch_size) + ] + + def _gen_gt_bboxes(): + gt_bboxes = torch.rand(size=[n_bboxes, 4]) + gt_bboxes[:, :2] = img_size * 0.5 * gt_bboxes[:, :2] + gt_bboxes[:, 2:] = img_size * (0.5 * gt_bboxes[:, 2:] + 0.5) + return gt_bboxes.clamp(0, img_size) + + feat = [ + torch.rand(batch_size, n_channels, img_size // feat_size, img_size // feat_size) + for feat_size in [4, 8, 16, 32, 64] + ] + gt_bboxes = [_gen_gt_bboxes() for _ in range(batch_size)] + gt_labels = [torch.randint(0, n_classes, size=(n_bboxes,)) for _ in range(batch_size)] + gt_bboxes_ignore = None + return feat, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore + + +@pytest.fixture +def fxt_cfg_atss_head(n_classes=4, n_channels=64) -> Dict: + train_cfg = mmcv.Config( + dict( + assigner=dict(type="ATSSAssigner", topk=9), + allowed_border=-1, + pos_weight=-1, + debug=False, + ) + ) + + head_cfg = dict( + type="CustomATSSHead", + num_classes=n_classes, + in_channels=n_channels, + feat_channels=n_channels, + anchor_generator=dict( + type="AnchorGenerator", + ratios=[1.0], + octave_base_scale=8, + scales_per_octave=1, + strides=[8, 16, 32, 64, 128], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[0.1, 0.1, 0.2, 0.2], + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="GIoULoss", loss_weight=2.0), + loss_centerness=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + use_qfl=False, + qfl_cfg=dict( + type="QualityFocalLoss", + use_sigmoid=True, + beta=2.0, + loss_weight=1.0, + ), + train_cfg=train_cfg, + ) + + return head_cfg + + +@pytest.fixture +def fxt_cfg_ssd_head(n_classes=4, n_channels=64) -> Dict: + head_cfg = { + "type": "CustomSSDHead", + "num_classes": n_classes, + "in_channels": (n_channels, n_channels), + "use_depthwise": True, + "norm_cfg": {"type": "BN"}, + "act_cfg": {"type": "ReLU"}, + "init_cfg": {"type": "Xavier", "layer": "Conv2d", "distribution": "uniform"}, + "loss_balancing": False, + "anchor_generator": { + "type": "SSDAnchorGeneratorClustered", + "strides": (16, 32), + "reclustering_anchors": True, + "widths": [ + [38.641007923271076, 92.49516032784699, 271.4234764938237, 141.53469410876247], + [206.04136086566515, 386.6542727907841, 716.9892752215089, 453.75609561761405, 788.4629155558277], + ], + "heights": [ + [48.9243877087132, 147.73088476194903, 158.23569788707474, 324.14510379107367], + [587.6216059488938, 381.60024152086544, 323.5988913027747, 702.7486097568518, 741.4865860938451], + ], + }, + "bbox_coder": { + "type": "DeltaXYWHBBoxCoder", + "target_means": (0.0, 0.0, 0.0, 0.0), + "target_stds": (0.1, 0.1, 0.2, 0.2), + }, + "loss_cls": {"type": "FocalLoss", "loss_weight": 1.0, "gamma": 2, "reduction": "none"}, + "train_cfg": mmcv.ConfigDict( + { + "assigner": { + "type": "MaxIoUAssigner", + "min_pos_iou": 0.0, + "ignore_iof_thr": -1, + "gt_max_assign_all": False, + "pos_iou_thr": 0.4, + "neg_iou_thr": 0.4, + }, + "smoothl1_beta": 1.0, + "allowed_border": -1, + "pos_weight": -1, + "neg_pos_ratio": 3, + "debug": False, + "use_giou": False, + "use_focal": False, + } + ), + } + + return head_cfg + + +@pytest.fixture +def fxt_cfg_vfnet_head(n_classes=4, n_channels=64) -> Dict: + head_cfg = { + "type": "CustomVFNetHead", + "num_classes": n_classes, + "in_channels": n_channels, + "stacked_convs": 3, + "feat_channels": 256, + "strides": [8, 16, 32, 64, 128], + "center_sampling": False, + "dcn_on_last_conv": False, + "use_atss": True, + "use_vfl": True, + "loss_cls": { + "type": "VarifocalLoss", + "use_sigmoid": True, + "alpha": 0.75, + "gamma": 2.0, + "iou_weighted": True, + "loss_weight": 1.0, + }, + "loss_bbox": {"type": "GIoULoss", "loss_weight": 1.5}, + "loss_bbox_refine": {"type": "GIoULoss", "loss_weight": 2.0}, + "train_cfg": mmcv.Config( + { + "assigner": {"type": "ATSSAssigner", "topk": 9}, + "allowed_border": -1, + "pos_weight": -1, + "debug": False, + } + ), + } + + return head_cfg + + +@pytest.fixture +def fxt_cfg_yolox_head(n_classes=4, n_channels=64): + return { + "type": "CustomYOLOXHead", + "num_classes": n_classes, + "in_channels": n_channels, + "feat_channels": n_channels, + "train_cfg": mmcv.Config({"assigner": {"type": "SimOTAAssigner", "center_radius": 2.5}}), + } diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_loss_dynamics_tracking_heads.py b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_loss_dynamics_tracking_heads.py new file mode 100644 index 00000000000..92f0e448167 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_loss_dynamics_tracking_heads.py @@ -0,0 +1,71 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from copy import deepcopy +from typing import Dict, Tuple + +import pytest +import torch +from mmdet.models.builder import build_head + +from otx.algorithms.detection.adapters.mmdet.models.heads import ( + CustomATSSHead, + CustomATSSHeadTrackingLossDynamics, +) + + +class TestLossDynamicsTrackingHeads: + @pytest.fixture(scope="class", autouse=True) + def set_seed(self): + torch.random.manual_seed(3003) + + @pytest.fixture + def fxt_head_pair( + self, request: pytest.FixtureRequest + ) -> Tuple[CustomATSSHead, CustomATSSHeadTrackingLossDynamics]: + fxt_cfg_name = request.param + fxt_cfg_head = request.getfixturevalue(fxt_cfg_name) + fxt_cfg_head_tracking_loss_dyns = deepcopy(fxt_cfg_head) + fxt_cfg_head_tracking_loss_dyns["type"] = fxt_cfg_head["type"] + "TrackingLossDynamics" + + head = build_head(fxt_cfg_head) + head_tracking_loss_dyns = build_head(fxt_cfg_head_tracking_loss_dyns) + # Copy-paste the original head's weights + head_tracking_loss_dyns.load_state_dict(head.state_dict()) + return head, head_tracking_loss_dyns + + @torch.no_grad() + @pytest.mark.parametrize( + "fxt_head_pair", + [ + "fxt_cfg_atss_head", + "fxt_cfg_ssd_head", + "fxt_cfg_vfnet_head", + "fxt_cfg_yolox_head", + ], + indirect=True, + ) + def test_output_equivalance(self, fxt_head_pair, fxt_head_input): + head, head_with_tracking_loss = fxt_head_pair + feat, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore = fxt_head_input + + scores = head_with_tracking_loss.forward(feat) + expected_scores = head.forward(feat) + + for actual, expected in zip(scores, expected_scores): + # actual, expected are list (# of feature pyramid level) + for a, e in zip(actual, expected): + assert torch.allclose(a, e) + + losses = head_with_tracking_loss.loss(*scores, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore) + expected_losses = head.loss(*expected_scores, gt_bboxes, gt_labels, img_metas, gt_bboxes_ignore) + + for actual, expected in zip(losses.values(), expected_losses.values()): + # actual, expected are list (# of feature pyramid level) + if isinstance(actual, list) and isinstance(expected, list): + for a, e in zip(actual, expected): + assert torch.allclose(a, e) + else: + assert torch.allclose(actual, expected) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_rpn_head.py b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_rpn_head.py new file mode 100644 index 00000000000..88117a010fb --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_rpn_head.py @@ -0,0 +1,68 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import openvino.runtime as ov +import pytest +import torch + +from otx.algorithms.detection.adapters.mmdet.models.dense_heads.mmov_rpn_head import ( + MMOVRPNHead, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMMOVSSDHead: + @pytest.fixture(autouse=True) + def setup(self): + + params = [] + results = [] + + param = ov.opset10.parameter([1, 24, 64, 64], ov.Type.f32, name="in") + filter = ov.opset10.constant(np.random.normal(size=(1, 24, 64, 64)), ov.Type.f32) + mul = ov.opset10.matmul(param, filter, False, False) + result_1 = ov.opset10.result(mul, name="cls_score_out") + result_2 = ov.opset10.result(mul, name="bbox_pred_out") + params.append(param) + results.append(result_1) + results.append(result_2) + + ov_model = ov.Model(results, params, "rpn_head") + + self.model = MMOVRPNHead( + model_path_or_model=ov_model, + transpose_reg=True, + transpose_cls=True, + inputs="in", + outputs=[ + "cls_score_out", + "bbox_pred_out", + ], + anchor_generator=dict( + type="AnchorGenerator", + base_sizes=[256], + scales=[0.25, 0.5, 1, 2], + ratios=[0.5, 1.0, 2.0], + strides=[8], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[0.1, 0.1, 0.2, 0.2], + ), + ) + + @e2e_pytest_unit + def test_init_weights(self): + self.model.init_weights() + + @e2e_pytest_unit + def test_forward(self): + data = {} + for key, shape in self.model.model.input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data[key] = torch.randn(shape) + self.model.train() + self.model(list(data.values())) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_ssd_head.py b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_ssd_head.py new file mode 100644 index 00000000000..4aef5514776 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_ssd_head.py @@ -0,0 +1,85 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import openvino.runtime as ov +import pytest +import torch + +from otx.algorithms.detection.adapters.mmdet.models.dense_heads.mmov_ssd_head import ( + MMOVSSDHead, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMMOVSSDHead: + @pytest.fixture(autouse=True) + def setup(self): + + params = [] + results = [] + + param = ov.opset10.parameter([1, 24, 64, 64], ov.Type.f32, name="in") + filter = ov.opset10.constant(np.random.normal(size=(1, 24, 64, 64)), ov.Type.f32) + mul = ov.opset10.matmul(param, filter, False, False) + result = ov.opset10.result(mul, name="reg_out") + params.append(param) + results.append(result) + + filter = ov.opset10.constant(np.random.normal(size=(1, 24, 64, 64)), ov.Type.f32) + mul = ov.opset10.matmul(param, filter, False, False) + result = ov.opset10.result(mul, name="cls_out") + results.append(result) + + ov_model = ov.Model(results, params, "ssd_head") + + self.model = MMOVSSDHead( + model_path_or_model=ov_model, + transpose_reg=True, + transpose_cls=True, + background_index=0, + inputs=dict( + reg_convs=[ + "in", + ], + cls_convs=[ + "in", + ], + ), + outputs=dict( + reg_convs=[ + "reg_out", + ], + cls_convs=[ + "cls_out", + ], + ), + num_classes=3, + anchor_generator=dict( + type="SSDAnchorGenerator", + scale_major=False, + input_size=300, + basesize_ratio_range=(0.15, 0.9), + strides=[24, 48, 92, 171, 400, 400], + ratios=[[2], [2, 3], [2, 3], [2, 3], [2], [2]], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=[0.0, 0.0, 0.0, 0.0], + target_stds=[0.1, 0.1, 0.2, 0.2], + ), + ) + + @e2e_pytest_unit + def test_init_weights(self): + self.model.init_weights() + + @e2e_pytest_unit + def test_forward(self): + data = {} + for key, shape in self.model.cls_convs[0].input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data[key] = torch.randn(shape) + self.model.train() + self.model(list(data.values())) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_yolov3_head.py b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_yolov3_head.py new file mode 100644 index 00000000000..dd44b9b92ca --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/dense_heads/test_ov_mmdet_mmov_yolov3_head.py @@ -0,0 +1,94 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import tempfile + +import numpy as np +import openvino.runtime as ov +import pytest +import torch + +from otx.algorithms.detection.adapters.mmdet.models.dense_heads.mmov_yolov3_head import ( + MMOVYOLOV3Head, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMMOVYOLOV3Head: + @pytest.fixture(autouse=True) + def setup(self): + + params = [] + results = [] + + param = ov.opset10.parameter([1, 24, 64, 64], ov.Type.f32, name="in") + params.append(param) + constant = ov.opset10.constant(np.random.normal(size=(32, 24, 3, 3)), ov.Type.f32) + node = ov.opset10.convolution(param, constant, [2, 2], [1, 1], [1, 1], [1, 1], "explicit", name="pred_in_1_tmp") + result = ov.opset10.result(node, name="bridge_out_1") + results.append(result) + constant = ov.opset10.constant(np.random.normal(size=(32, 32, 3, 3)), ov.Type.f32) + node = ov.opset10.convolution(node, constant, [2, 2], [1, 1], [1, 1], [1, 1], "explicit", name="pred_in_1") + result = ov.opset10.result(node, name="pred_out_1") + results.append(result) + + constant = ov.opset10.constant(np.random.normal(size=(16, 24, 3, 3)), ov.Type.f32) + node = ov.opset10.convolution(param, constant, [2, 2], [1, 1], [1, 1], [1, 1], "explicit", name="pred_in_2_tmp") + result = ov.opset10.result(node, name="bridge_out_2") + results.append(result) + constant = ov.opset10.constant(np.random.normal(size=(16, 16, 3, 3)), ov.Type.f32) + node = ov.opset10.convolution(node, constant, [2, 2], [1, 1], [1, 1], [1, 1], "explicit", name="pred_in_2") + result = ov.opset10.result(node, name="pred_out_2") + results.append(result) + + ov_model = ov.Model(results, params, "yolov3_head") + with tempfile.TemporaryDirectory() as tempdir: + ov.serialize(ov_model, os.path.join(tempdir, "model.xml"), os.path.join(tempdir, "model.bin")) + + self.model = MMOVYOLOV3Head( + model_path_or_model=os.path.join(tempdir, "model.xml"), + inputs=dict( + convs_bridge=[ + "in", + "in", + ], + convs_pred=[ + "pred_in_1_tmp||pred_in_1", + "pred_in_2_tmp||pred_in_2", + ], + ), + outputs=dict( + convs_bridge=[ + "bridge_out_1", + "bridge_out_2", + ], + convs_pred=[ + "pred_out_1", + "pred_out_2", + ], + ), + anchor_generator=dict( + type="YOLOAnchorGenerator", + base_sizes=[[(81, 82), (135, 169), (344, 319)], [(23, 27), (37, 58), (81, 82)]], + strides=[32, 16], + ), + bbox_coder=dict(type="YOLOBBoxCoder"), + featmap_strides=[32, 16], + num_classes=80, + ) + + @e2e_pytest_unit + def test_init_weights(self): + self.model.init_weights() + + @e2e_pytest_unit + def test_forward(self): + data = [] + for conv_bridge in self.model.convs_bridge: + for key, shape in conv_bridge.input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data.append(torch.randn(shape)) + self.model.train() + self.model(data) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/conftest.py b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/conftest.py new file mode 100644 index 00000000000..4ac44156cba --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/conftest.py @@ -0,0 +1,487 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict + +import mmcv +import pytest +from mmcv.utils import ConfigDict + + +@pytest.fixture +def fxt_cfg_custom_atss(num_classes: int = 3) -> Dict: + train_cfg = mmcv.Config( + dict( + assigner=dict(type="ATSSAssigner", topk=9), + allowed_border=-1, + pos_weight=-1, + debug=False, + ) + ) + cfg = dict( + type="CustomATSS", + backbone=dict( + avg_down=False, + base_channels=64, + conv_cfg=None, + dcn=None, + deep_stem=False, + depth=18, + dilations=(1, 1, 1, 1), + frozen_stages=-1, + in_channels=3, + init_cfg=None, + norm_cfg=dict(requires_grad=True, type="BN"), + norm_eval=True, + num_stages=4, + out_indices=(0, 1, 2, 3), + plugins=None, + pretrained=None, + stage_with_dcn=(False, False, False, False), + stem_channels=None, + strides=(1, 2, 2, 2), + style="pytorch", + type="mmdet.ResNet", + with_cp=False, + zero_init_residual=True, + ), + neck=dict( + type="FPN", + in_channels=[64, 128, 256, 512], + out_channels=64, + start_level=1, + add_extra_convs="on_output", + num_outs=5, + relu_before_extra_convs=True, + ), + bbox_head=dict( + type="CustomATSSHead", + num_classes=num_classes, + in_channels=64, + stacked_convs=4, + feat_channels=64, + anchor_generator=dict( + type="AnchorGenerator", + ratios=[1.0], + octave_base_scale=8, + scales_per_octave=1, + strides=[8, 16, 32, 64, 128], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[0.1, 0.1, 0.2, 0.2] + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="GIoULoss", loss_weight=2.0), + loss_centerness=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + use_qfl=False, + qfl_cfg=dict(type="QualityFocalLoss", use_sigmoid=True, beta=2.0, loss_weight=1.0), + ), + train_cfg=train_cfg, + ) + return cfg + + +@pytest.fixture +def fxt_cfg_custom_ssd(num_classes: int = 3) -> Dict: + train_cfg = mmcv.Config( + { + "assigner": { + "type": "MaxIoUAssigner", + "min_pos_iou": 0.0, + "ignore_iof_thr": -1, + "gt_max_assign_all": False, + "pos_iou_thr": 0.4, + "neg_iou_thr": 0.4, + }, + "smoothl1_beta": 1.0, + "allowed_border": -1, + "pos_weight": -1, + "neg_pos_ratio": 3, + "debug": False, + "use_giou": False, + "use_focal": False, + } + ) + + cfg = dict( + type="CustomSingleStageDetector", + backbone=dict( + type="mmdet.mobilenetv2_w1", out_indices=(4, 5), frozen_stages=-1, norm_eval=False, pretrained=True + ), + neck=None, + bbox_head=dict( + type="CustomSSDHead", + num_classes=num_classes, + in_channels=(int(96.0), int(320.0)), + use_depthwise=True, + norm_cfg=dict(type="BN"), + act_cfg=dict(type="ReLU"), + init_cfg=dict(type="Xavier", layer="Conv2d", distribution="uniform"), + loss_balancing=False, + anchor_generator=dict( + type="SSDAnchorGeneratorClustered", + strides=(16, 32), + reclustering_anchors=True, + widths=[ + [ + 38.641007923271076, + 92.49516032784699, + 271.4234764938237, + 141.53469410876247, + ], + [ + 206.04136086566515, + 386.6542727907841, + 716.9892752215089, + 453.75609561761405, + 788.4629155558277, + ], + ], + heights=[ + [ + 48.9243877087132, + 147.73088476194903, + 158.23569788707474, + 324.14510379107367, + ], + [ + 587.6216059488938, + 381.60024152086544, + 323.5988913027747, + 702.7486097568518, + 741.4865860938451, + ], + ], + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", + target_means=(0.0, 0.0, 0.0, 0.0), + target_stds=(0.1, 0.1, 0.2, 0.2), + ), + ), + train_cfg=train_cfg, + ) + + return cfg + + +@pytest.fixture +def fxt_cfg_custom_vfnet(num_classes: int = 3): + return ConfigDict( + type="CustomVFNet", + backbone=dict( + type="ResNet", + depth=50, + num_stages=4, + out_indices=(0, 1, 2, 3), + frozen_stages=1, + norm_cfg=dict(type="BN", requires_grad=True), + norm_eval=True, + style="pytorch", + ), + neck=dict( + type="FPN", + in_channels=[256, 512, 1024, 2048], + out_channels=256, + start_level=1, + add_extra_convs="on_output", + num_outs=5, + relu_before_extra_convs=True, + ), + bbox_head=dict( + type="CustomVFNetHead", + num_classes=num_classes, + in_channels=256, + stacked_convs=3, + feat_channels=256, + strides=[8, 16, 32, 64, 128], + center_sampling=False, + dcn_on_last_conv=False, + use_atss=True, + use_vfl=True, + loss_cls=dict( + type="VarifocalLoss", use_sigmoid=True, alpha=0.75, gamma=2.0, iou_weighted=True, loss_weight=1.0 + ), + loss_bbox=dict(type="GIoULoss", loss_weight=1.5), + loss_bbox_refine=dict(type="GIoULoss", loss_weight=2.0), + ), + train_cfg=dict(assigner=dict(type="ATSSAssigner", topk=9), allowed_border=-1, pos_weight=-1, debug=False), + test_cfg=dict( + nms_pre=1000, min_bbox_size=0, score_thr=0.01, nms=dict(type="nms", iou_threshold=0.5), max_per_img=100 + ), + task_adapt=dict( + src_classes=["person", "car"], + dst_classes=["tree", "car", "person"], + ), + ) + + +@pytest.fixture +def fxt_cfg_custom_yolox(num_classes: int = 3): + cfg = { + "train_cfg": mmcv.Config({"assigner": {"type": "SimOTAAssigner", "center_radius": 2.5}}), + "type": "CustomYOLOX", + "backbone": {"type": "CSPDarknet", "deepen_factor": 0.33, "widen_factor": 0.375, "out_indices": (2, 3, 4)}, + "neck": {"type": "YOLOXPAFPN", "in_channels": [96, 192, 384], "out_channels": 96, "num_csp_blocks": 1}, + "bbox_head": {"type": "CustomYOLOXHead", "num_classes": num_classes, "in_channels": 96, "feat_channels": 96}, + "task_adapt": { + "src_classes": ( + "person", + "bicycle", + "car", + "motorcycle", + "airplane", + "bus", + "train", + "truck", + "boat", + "traffic light", + "fire hydrant", + "stop sign", + "parking meter", + "bench", + "bird", + "cat", + "dog", + "horse", + "sheep", + "cow", + "elephant", + "bear", + "zebra", + "giraffe", + "backpack", + "umbrella", + "handbag", + "tie", + "suitcase", + "frisbee", + "skis", + "snowboard", + "sports ball", + "kite", + "baseball bat", + "baseball glove", + "skateboard", + "surfboard", + "tennis racket", + "bottle", + "wine glass", + "cup", + "fork", + "knife", + "spoon", + "bowl", + "banana", + "apple", + "sandwich", + "orange", + "broccoli", + "carrot", + "hot dog", + "pizza", + "donut", + "cake", + "chair", + "couch", + "potted plant", + "bed", + "dining table", + "toilet", + "tv", + "laptop", + "mouse", + "remote", + "keyboard", + "cell phone", + "microwave", + "oven", + "toaster", + "sink", + "refrigerator", + "book", + "clock", + "vase", + "scissors", + "teddy bear", + "hair drier", + "toothbrush", + ), + "dst_classes": ["car", "tree", "bug"], + }, + } + return cfg + + +@pytest.fixture +def fxt_cfg_custom_deformable_detr(num_classes: int = 3): + return ConfigDict( + type="CustomDeformableDETR", + backbone=dict( + type="ResNet", + depth=50, + num_stages=4, + out_indices=(1, 2, 3), + frozen_stages=1, + norm_cfg=dict(type="BN", requires_grad=False), + norm_eval=True, + style="pytorch", + init_cfg=dict(type="Pretrained", checkpoint="torchvision://resnet50"), + ), + neck=dict( + type="ChannelMapper", + in_channels=[512, 1024, 2048], + kernel_size=1, + out_channels=256, + act_cfg=None, + norm_cfg=dict(type="GN", num_groups=32), + num_outs=4, + ), + bbox_head=dict( + type="DeformableDETRHead", + num_query=300, + num_classes=num_classes, + in_channels=2048, + sync_cls_avg_factor=True, + with_box_refine=True, + as_two_stage=True, + transformer=dict( + type="DeformableDetrTransformer", + encoder=dict( + type="DetrTransformerEncoder", + num_layers=6, + transformerlayers=dict( + type="BaseTransformerLayer", + attn_cfgs=dict(type="MultiScaleDeformableAttention", embed_dims=256), + feedforward_channels=1024, + ffn_dropout=0.1, + operation_order=("self_attn", "norm", "ffn", "norm"), + ), + ), + decoder=dict( + type="DeformableDetrTransformerDecoder", + num_layers=6, + return_intermediate=True, + transformerlayers=dict( + type="DetrTransformerDecoderLayer", + attn_cfgs=[ + dict(type="MultiheadAttention", embed_dims=256, num_heads=8, dropout=0.1), + dict(type="MultiScaleDeformableAttention", embed_dims=256), + ], + feedforward_channels=1024, + ffn_dropout=0.1, + operation_order=("self_attn", "norm", "cross_attn", "norm", "ffn", "norm"), + ), + ), + ), + positional_encoding=dict(type="SinePositionalEncoding", num_feats=128, normalize=True, offset=-0.5), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=2.0), + loss_bbox=dict(type="L1Loss", loss_weight=5.0), + loss_iou=dict(type="GIoULoss", loss_weight=2.0), + ), + # training and testing settings + train_cfg=dict( + assigner=dict( + type="HungarianAssigner", + cls_cost=dict(type="FocalLossCost", weight=2.0), + reg_cost=dict(type="BBoxL1Cost", weight=5.0, box_format="xywh"), + iou_cost=dict(type="IoUCost", iou_mode="giou", weight=2.0), + ) + ), + test_cfg=dict(max_per_img=100), + task_adapt=dict( + src_classes=["person", "car"], + dst_classes=["tree", "car", "person"], + ), + ) + + +@pytest.fixture +def fxt_cfg_custom_dino(num_classes: int = 3): + return ConfigDict( + type="CustomDINO", + backbone=dict( + type="ResNet", + depth=50, + num_stages=4, + out_indices=(1, 2, 3), + frozen_stages=1, + norm_cfg=dict(type="BN", requires_grad=False), + norm_eval=True, + style="pytorch", + init_cfg=dict(type="Pretrained", checkpoint="torchvision://resnet50"), + ), + neck=dict( + type="ChannelMapper", + in_channels=[512, 1024, 2048], + kernel_size=1, + out_channels=256, + act_cfg=None, + norm_cfg=dict(type="GN", num_groups=32), + num_outs=4, + ), + bbox_head=dict( + type="CustomDINOHead", + num_query=900, + num_classes=num_classes, + in_channels=2048, + sync_cls_avg_factor=True, + with_box_refine=True, + as_two_stage=True, + transformer=dict( + type="CustomDINOTransformer", + encoder=dict( + type="DetrTransformerEncoder", + num_layers=6, + transformerlayers=dict( + type="BaseTransformerLayer", + attn_cfgs=dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "ffn", "norm"), + ), + ), + decoder=dict( + type="DINOTransformerDecoder", + num_layers=6, + return_intermediate=True, + transformerlayers=dict( + type="DetrTransformerDecoderLayer", + attn_cfgs=[ + dict(type="MultiheadAttention", embed_dims=256, num_heads=8, dropout=0.0), + dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + ], + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "cross_attn", "norm", "ffn", "norm"), + ), + ), + ), + positional_encoding=dict( + type="SinePositionalEncoding", num_feats=128, normalize=True, offset=0.0, temperature=20 + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=5.0), + loss_iou=dict(type="GIoULoss", loss_weight=2.0), + dn_cfg=dict( + label_noise_scale=0.5, + box_noise_scale=1.0, # 0.4 for DN-DETR + group_cfg=dict(dynamic=True, num_groups=None, num_dn_queries=100), + ), + ), + # training and testing settings + train_cfg=dict( + assigner=dict( + type="HungarianAssigner", + cls_cost=dict(type="FocalLossCost", weight=1.0), + reg_cost=dict(type="BBoxL1Cost", weight=5.0, box_format="xywh"), + iou_cost=dict(type="IoUCost", iou_mode="giou", weight=2.0), + ) + ), + test_cfg=dict(max_per_img=300), + task_adapt=dict( + src_classes=["person", "car"], + dst_classes=["tree", "car", "person"], + ), + ) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_atss_detector.py b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_atss_detector.py new file mode 100644 index 00000000000..55bd05f200a --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_atss_detector.py @@ -0,0 +1,74 @@ +from typing import Dict +import torch +from mmdet.models.builder import build_detector + +from otx.algorithms.detection.adapters.mmdet.models.detectors.custom_atss_detector import ( + CustomATSS, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomATSS: + @e2e_pytest_unit + def test_custom_atss_build(self, fxt_cfg_custom_atss: Dict): + model = build_detector(fxt_cfg_custom_atss) + assert isinstance(model, CustomATSS) + + @e2e_pytest_unit + def test_custom_atss_load_state_dict_pre_hook(self): + chkpt_classes = ["person", "car"] + model_classes = ["tree", "car", "person"] + chkpt_dict = { + "bbox_head.atss_cls.weight": torch.tensor( + [ + [1, 1, 1, 1], + [2, 2, 2, 2], + ] + ), + "bbox_head.atss_cls.bias": torch.tensor( + [ + [1], + [2], + ] + ), + } + model_dict = { + "bbox_head.atss_cls.weight": torch.tensor( + [ + [3, 3, 3, 3], + [4, 4, 4, 4], + [5, 5, 5, 5], + ] + ), + "bbox_head.atss_cls.bias": torch.tensor( + [ + [3], + [4], + [5], + ] + ), + } + gt_dict = { + "bbox_head.atss_cls.weight": torch.tensor( + [ + [3, 3, 3, 3], + [2, 2, 2, 2], + [1, 1, 1, 1], + ] + ), + "bbox_head.atss_cls.bias": torch.tensor( + [ + [3], + [2], + [1], + ] + ), + } + + class Model: + def state_dict(self): + return model_dict + + CustomATSS.load_state_dict_pre_hook(Model(), model_classes, chkpt_classes, chkpt_dict, "") + for k, gt in gt_dict.items(): + assert (chkpt_dict[k] != gt).sum() == 0 diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_deformable_detr_detector.py b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_deformable_detr_detector.py new file mode 100644 index 00000000000..fc12212dcc9 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_deformable_detr_detector.py @@ -0,0 +1,77 @@ +"""Test for CustomDeformableDETR Detector.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +from mmdet.models.builder import build_detector + +from otx.algorithms.detection.adapters.mmdet.models.detectors.custom_deformable_detr_detector import ( + CustomDeformableDETR, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomDeformableDETR: + @e2e_pytest_unit + def test_custom_deformable_detr_build(self, fxt_cfg_custom_deformable_detr): + model = build_detector(fxt_cfg_custom_deformable_detr) + assert isinstance(model, CustomDeformableDETR) + assert model.cls_layers is not None + + @e2e_pytest_unit + def test_custom_deformable_detr_load_state_dict_pre_hook(self, fxt_cfg_custom_deformable_detr, mocker): + model = build_detector(fxt_cfg_custom_deformable_detr) + chkpt_classes = ["person", "car"] + model_classes = ["tree", "car", "person"] + chkpt_dict = { + "bbox_head.cls_branches.weight": torch.tensor( + [ + [1, 1, 1, 1], + [2, 2, 2, 2], + ] + ), + "bbox_head.cls_branches.bias": torch.tensor( + [ + [1], + [2], + ] + ), + } + model_dict = { + "bbox_head.cls_branches.weight": torch.tensor( + [ + [3, 3, 3, 3], + [4, 4, 4, 4], + [5, 5, 5, 5], + ] + ), + "bbox_head.cls_branches.bias": torch.tensor( + [ + [3], + [4], + [5], + ] + ), + } + gt_dict = { + "bbox_head.cls_branches.weight": torch.tensor( + [ + [3, 3, 3, 3], + [2, 2, 2, 2], + [1, 1, 1, 1], + ] + ), + "bbox_head.cls_branches.bias": torch.tensor( + [ + [3], + [2], + [1], + ] + ), + } + + mocker.patch.object(model, "state_dict", return_value=model_dict) + model.load_state_dict_pre_hook(model_classes, chkpt_classes, chkpt_dict) + for k, gt in gt_dict.items(): + assert (chkpt_dict[k] != gt).sum() == 0 diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_dino_detector.py b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_dino_detector.py new file mode 100644 index 00000000000..477805e751c --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_dino_detector.py @@ -0,0 +1,57 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict +import torch +from mmdet.models.builder import build_detector + +from otx.algorithms.detection.adapters.mmdet.models.detectors.custom_deformable_detr_detector import ( + CustomDeformableDETR, +) +from otx.algorithms.detection.adapters.mmdet.models.detectors.custom_dino_detector import ( + CustomDINO, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomDINO: + @e2e_pytest_unit + def test_custom_dino_build(self, fxt_cfg_custom_dino: Dict): + model = build_detector(fxt_cfg_custom_dino) + assert isinstance(model, CustomDINO) + + @e2e_pytest_unit + def test_custom_dino_load_state_pre_hook(self, fxt_cfg_custom_dino: Dict, mocker): + mocker.patch.object(CustomDeformableDETR, "load_state_dict_pre_hook", return_value=True) + model = build_detector(fxt_cfg_custom_dino) + ckpt_dict = { + "level_embed": "level_embed", + "encoder.self_attn": "encoder.self_attn", + "encoder.cross_attn": "encoder.cross_attn", + "encoder.ffn": "encoder.ffn", + "level_embed": "level_embed", + "decoder.self_attn": "decoder.self_attn", + "decoder.cross_attn": "decoder.cross_attn", + "decoder.ffn": "decoder.ffn", + "query_embedding.weight": "query_embedding.weight", + "dn_query_generator.label_embedding.weight": "dn_query_generator.label_embedding.weight", + "memory_trans_fc": "memory_trans_fc", + "memory_trans_norm": "memory_trans_norm", + } + model.load_state_dict_pre_hook([], [], ckpt_dict) + + assert ckpt_dict["bbox_head.transformer.level_embeds"] == "level_embed" + assert ckpt_dict["bbox_head.transformer.encoder.attentions.0"] == "encoder.self_attn" + assert ckpt_dict["bbox_head.transformer.encoder.attentions.1"] == "encoder.cross_attn" + assert ckpt_dict["bbox_head.transformer.encoder.ffns.0"] == "encoder.ffn" + assert ckpt_dict["bbox_head.transformer.decoder.attentions.0"] == "decoder.self_attn" + assert ckpt_dict["bbox_head.transformer.decoder.attentions.1"] == "decoder.cross_attn" + assert ckpt_dict["bbox_head.transformer.decoder.ffns.0"] == "decoder.ffn" + assert ckpt_dict["bbox_head.query_embedding.weight"] == "query_embedding.weight" + assert ( + ckpt_dict["bbox_head.transformer.dn_query_generator.label_embedding.weight"] + == "dn_query_generator.label_embedding.weight" + ) + assert ckpt_dict["bbox_head.transformer.enc_output"] == "memory_trans_fc" + assert ckpt_dict["bbox_head.transformer.enc_output_norm"] == "memory_trans_norm" diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_single_stage_detector.py b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_single_stage_detector.py new file mode 100644 index 00000000000..780238e4c5d --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_single_stage_detector.py @@ -0,0 +1,129 @@ +import torch + +from otx.algorithms.detection.adapters.mmdet.models.backbones import imgclsmob +from otx.algorithms.detection.adapters.mmdet.models.detectors.custom_single_stage_detector import ( + CustomSingleStageDetector, +) +from otx.algorithms.detection.adapters.mmdet.models.heads import custom_anchor_generator + +__all__ = ["imgclsmob", "custom_anchor_generator"] + +from mmdet.models.builder import build_detector + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomSingleStageDetector: + @e2e_pytest_unit + def test_custom_single_stage_detector_build(self, fxt_cfg_custom_ssd): + model = build_detector(fxt_cfg_custom_ssd) + assert isinstance(model, CustomSingleStageDetector) + + @e2e_pytest_unit + def test_custom_single_stage_detector_load_state_dict_pre_hook(self): + chkpt_classes = ["person", "car"] + model_classes = ["tree", "car", "person"] + chkpt_dict = { + "bbox_head.cls_convs.0.weight": torch.tensor( + [ + [1, 1, 1, 1], + [2, 2, 2, 2], + [0, 0, 0, 0], # BG + [1, 1, 1, 1], + [2, 2, 2, 2], + [0, 0, 0, 0], # BG + [1, 1, 1, 1], + [2, 2, 2, 2], + [0, 0, 0, 0], # BG + ] + ), + "bbox_head.cls_convs.0.bias": torch.tensor( + [ + [1], + [2], + [0], # BG + [1], + [2], + [0], # BG + [1], + [2], + [0], # BG + ] + ), + } + model_dict = { + "bbox_head.cls_convs.0.weight": torch.tensor( + [ + [3, 3, 3, 3], + [4, 4, 4, 4], + [5, 5, 5, 5], + [0, 0, 0, 0], # BG + [3, 3, 3, 3], + [4, 4, 4, 4], + [5, 5, 5, 5], + [0, 0, 0, 0], # BG + [3, 3, 3, 3], + [4, 4, 4, 4], + [5, 5, 5, 5], + [0, 0, 0, 0], # BG + ] + ), + "bbox_head.cls_convs.0.bias": torch.tensor( + [ + [3], + [4], + [5], + [0], # BG + [3], + [4], + [5], + [0], # BG + [3], + [4], + [5], + [0], # BG + ] + ), + } + gt_dict = { + "bbox_head.cls_convs.0.weight": torch.tensor( + [ + [3, 3, 3, 3], + [2, 2, 2, 2], + [1, 1, 1, 1], + [0, 0, 0, 0], # BG + [3, 3, 3, 3], + [2, 2, 2, 2], + [1, 1, 1, 1], + [0, 0, 0, 0], # BG + [3, 3, 3, 3], + [2, 2, 2, 2], + [1, 1, 1, 1], + [0, 0, 0, 0], # BG + ] + ), + "bbox_head.cls_convs.0.bias": torch.tensor( + [ + [3], + [2], + [1], + [0], # BG + [3], + [2], + [1], + [0], # BG + [3], + [2], + [1], + [0], # BG + ] + ), + } + + class Model: + def state_dict(self): + return model_dict + + CustomSingleStageDetector.load_state_dict_pre_hook(Model(), model_classes, chkpt_classes, chkpt_dict, "") + for k, gt in gt_dict.items(): + assert (chkpt_dict[k] != gt).sum() == 0 diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_two_stage_detector.py b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_two_stage_detector.py new file mode 100644 index 00000000000..64508e787c3 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_two_stage_detector.py @@ -0,0 +1,261 @@ +import torch +from mmcv.utils import ConfigDict +from mmdet.models.builder import build_detector + +from otx.algorithms.detection.adapters.mmdet.models.detectors.custom_two_stage_detector import ( + CustomTwoStageDetector, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomTwoStageDetector: + @e2e_pytest_unit + def test_custom_two_stage_detector_build(self): + model_cfg = ConfigDict( + type="CustomTwoStageDetector", + backbone=dict( + type="ResNet", + depth=50, + num_stages=4, + out_indices=(0, 1, 2, 3), + frozen_stages=1, + norm_cfg=dict(type="BN", requires_grad=True), + norm_eval=True, + style="pytorch", + ), + neck=dict(type="FPN", in_channels=[256, 512, 1024, 2048], out_channels=256, num_outs=5), + rpn_head=dict( + type="RPNHead", + in_channels=256, + feat_channels=256, + anchor_generator=dict( + type="AnchorGenerator", scales=[8], ratios=[0.5, 1.0, 2.0], strides=[4, 8, 16, 32, 64] + ), + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[1.0, 1.0, 1.0, 1.0] + ), + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=True, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + roi_head=dict( + type="StandardRoIHead", + bbox_roi_extractor=dict( + type="SingleRoIExtractor", + roi_layer=dict(type="RoIAlign", output_size=7, sampling_ratio=0.0), + out_channels=256, + featmap_strides=[4, 8, 16, 32], + ), + bbox_head=dict( + type="Shared2FCBBoxHead", + in_channels=256, + fc_out_channels=1024, + roi_feat_size=7, + num_classes=80, + bbox_coder=dict( + type="DeltaXYWHBBoxCoder", target_means=[0.0, 0.0, 0.0, 0.0], target_stds=[0.1, 0.1, 0.2, 0.2] + ), + reg_class_agnostic=False, + loss_cls=dict(type="CrossEntropyLoss", use_sigmoid=False, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=1.0), + ), + ), + # model training and testing settings + train_cfg=dict( + rpn=dict( + assigner=dict( + type="MaxIoUAssigner", + pos_iou_thr=0.7, + neg_iou_thr=0.3, + min_pos_iou=0.3, + match_low_quality=True, + ignore_iof_thr=-1, + ), + sampler=dict( + type="RandomSampler", num=256, pos_fraction=0.5, neg_pos_ub=-1, add_gt_as_proposals=False + ), + allowed_border=-1, + pos_weight=-1, + debug=False, + ), + rpn_proposal=dict( + nms_across_levels=False, nms_pre=2000, nms_post=1000, max_num=1000, nms_thr=0.7, min_bbox_size=0 + ), + rcnn=dict( + assigner=dict( + type="MaxIoUAssigner", + pos_iou_thr=0.5, + neg_iou_thr=0.5, + min_pos_iou=0.5, + match_low_quality=False, + ignore_iof_thr=-1, + ), + sampler=dict( + type="RandomSampler", num=512, pos_fraction=0.25, neg_pos_ub=-1, add_gt_as_proposals=True + ), + pos_weight=-1, + debug=False, + ), + ), + test_cfg=dict( + rpn=dict( + nms_across_levels=False, nms_pre=1000, nms_post=1000, max_num=1000, nms_thr=0.7, min_bbox_size=0 + ), + rcnn=dict(score_thr=0.05, nms=dict(type="nms", iou_threshold=0.5), max_per_img=100) + # soft-nms is also supported for rcnn testing + # e.g., nms=dict(type='soft_nms', iou_threshold=0.5, min_score=0.05) + ), + task_adapt=dict( + src_classes=["person", "car"], + dst_classes=["tree", "car", "person"], + ), + ) + + model = build_detector(model_cfg) + assert isinstance(model, CustomTwoStageDetector) + + @e2e_pytest_unit + def test_custom_two_stage_detector_load_state_dict_pre_hook(self): + chkpt_classes = ["person", "car"] + model_classes = ["tree", "car", "person"] + chkpt_dict = { + "roi_head.bbox_head.fc_cls.weight": torch.tensor( + [ + [1, 1, 1, 1], + [2, 2, 2, 2], + ] + ), + "roi_head.bbox_head.fc_cls.bias": torch.tensor( + [ + [1], + [2], + ] + ), + "roi_head.bbox_head.fc_reg.weight": torch.tensor( + [ + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [2, 2, 2, 2], + [2, 2, 2, 2], + [2, 2, 2, 2], + [2, 2, 2, 2], + ] + ), + "roi_head.bbox_head.fc_reg.bias": torch.tensor( + [ + [1], + [1], + [1], + [1], + [2], + [2], + [2], + [2], + ] + ), + } + model_dict = { + "roi_head.bbox_head.fc_cls.weight": torch.tensor( + [ + [3, 3, 3, 3], + [4, 4, 4, 4], + [5, 5, 5, 5], + ] + ), + "roi_head.bbox_head.fc_cls.bias": torch.tensor( + [ + [3], + [4], + [5], + ] + ), + "roi_head.bbox_head.fc_reg.weight": torch.tensor( + [ + [3, 3, 3, 3], + [3, 3, 3, 3], + [3, 3, 3, 3], + [3, 3, 3, 3], + [4, 4, 4, 4], + [4, 4, 4, 4], + [4, 4, 4, 4], + [4, 4, 4, 4], + [5, 5, 5, 5], + [5, 5, 5, 5], + [5, 5, 5, 5], + [5, 5, 5, 5], + ] + ), + "roi_head.bbox_head.fc_reg.bias": torch.tensor( + [ + [3], + [3], + [3], + [3], + [4], + [4], + [4], + [4], + [5], + [5], + [5], + [5], + ] + ), + } + gt_dict = { + "roi_head.bbox_head.fc_cls.weight": torch.tensor( + [ + [3, 3, 3, 3], + [2, 2, 2, 2], + [1, 1, 1, 1], + ] + ), + "roi_head.bbox_head.fc_cls.bias": torch.tensor( + [ + [3], + [2], + [1], + ] + ), + "roi_head.bbox_head.fc_reg.weight": torch.tensor( + [ + [3, 3, 3, 3], + [3, 3, 3, 3], + [3, 3, 3, 3], + [3, 3, 3, 3], + [2, 2, 2, 2], + [2, 2, 2, 2], + [2, 2, 2, 2], + [2, 2, 2, 2], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 1, 1], + ] + ), + "roi_head.bbox_head.fc_reg.bias": torch.tensor( + [ + [3], + [3], + [3], + [3], + [2], + [2], + [2], + [2], + [1], + [1], + [1], + [1], + ] + ), + } + + class Model: + def state_dict(self): + return model_dict + + CustomTwoStageDetector.load_state_dict_pre_hook(Model(), model_classes, chkpt_classes, chkpt_dict, "") + for k, gt in gt_dict.items(): + assert (chkpt_dict[k] != gt).sum() == 0 diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_vfnet_detector.py b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_vfnet_detector.py new file mode 100644 index 00000000000..065de1d0d94 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_custom_vfnet_detector.py @@ -0,0 +1,74 @@ +from typing import Dict +import torch +from mmdet.models.builder import build_detector + +from otx.algorithms.detection.adapters.mmdet.models.detectors.custom_vfnet_detector import ( + CustomVFNet, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomVFNet: + @e2e_pytest_unit + def test_custom_vfnet_build(self, fxt_cfg_custom_vfnet: Dict): + model = build_detector(fxt_cfg_custom_vfnet) + assert isinstance(model, CustomVFNet) + + @e2e_pytest_unit + def test_custom_vfnet_load_state_dict_pre_hook(self): + chkpt_classes = ["person", "car"] + model_classes = ["tree", "car", "person"] + chkpt_dict = { + "bbox_head.vfnet_cls.weight": torch.tensor( + [ + [1, 1, 1, 1], + [2, 2, 2, 2], + ] + ), + "bbox_head.vfnet_cls.bias": torch.tensor( + [ + [1], + [2], + ] + ), + } + model_dict = { + "bbox_head.vfnet_cls.weight": torch.tensor( + [ + [3, 3, 3, 3], + [4, 4, 4, 4], + [5, 5, 5, 5], + ] + ), + "bbox_head.vfnet_cls.bias": torch.tensor( + [ + [3], + [4], + [5], + ] + ), + } + gt_dict = { + "bbox_head.vfnet_cls.weight": torch.tensor( + [ + [3, 3, 3, 3], + [2, 2, 2, 2], + [1, 1, 1, 1], + ] + ), + "bbox_head.vfnet_cls.bias": torch.tensor( + [ + [3], + [2], + [1], + ] + ), + } + + class Model: + def state_dict(self): + return model_dict + + CustomVFNet.load_state_dict_pre_hook(Model(), model_classes, chkpt_classes, chkpt_dict, "") + for k, gt in gt_dict.items(): + assert (chkpt_dict[k] != gt).sum() == 0 diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_loss_dynamics_tracking.py b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_loss_dynamics_tracking.py new file mode 100644 index 00000000000..c2ba747bb61 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_loss_dynamics_tracking.py @@ -0,0 +1,116 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os.path as osp +from typing import Any, Dict, Type + +import datumaro as dm +import pytest +import torch +from mmcv import ConfigDict +from mmdet.datasets import build_dataloader, build_dataset +from mmdet.models.builder import build_detector +from mmdet.models.detectors import BaseDetector + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label import Domain + + +class TestLossDynamicsTrackingMixin: + @pytest.fixture() + def dataloader(self, fxt_det_dataset_entity: DatasetEntity): + dataloader_cfg = dict(samples_per_gpu=len(fxt_det_dataset_entity), workers_per_gpu=1) + dataset_cfg = ConfigDict( + dict( + type="OTXDetDataset", + pipeline=[ + dict(type="LoadImageFromOTXDataset"), + dict( + type="LoadAnnotationFromOTXDataset", + with_bbox=True, + with_mask=False, + domain="detection", + min_size=-1, + ), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="DefaultFormatBundle"), + dict( + type="Collect", + keys=["img", "gt_bboxes", "gt_labels"], + meta_keys=( + "filename", + "ori_shape", + "img_shape", + "pad_shape", + "scale_factor", + "flip", + "img_norm_cfg", + "gt_ann_ids", + ), + ), + ], + otx_dataset=fxt_det_dataset_entity, + labels=fxt_det_dataset_entity.get_labels(), + # domain=Domain.DETECTION, + ) + ) + + dataset = build_dataset(dataset_cfg) + dataloader = build_dataloader(dataset, **dataloader_cfg) + + return dataloader + + @pytest.fixture() + def detector(self, request: Type[pytest.FixtureRequest], fxt_det_dataset_entity: DatasetEntity) -> BaseDetector: + fxt_cfg_detector = request.getfixturevalue(request.param) + fxt_cfg_detector["track_loss_dynamics"] = True + + detector = build_detector(fxt_cfg_detector) + detector.loss_dyns_tracker.init_with_otx_dataset(fxt_det_dataset_entity) + return detector + + TESTCASE = [ + "fxt_cfg_custom_atss", + "fxt_cfg_custom_ssd", + "fxt_cfg_custom_vfnet", + "fxt_cfg_custom_yolox", + ] + + @torch.no_grad() + @pytest.mark.parametrize("detector", TESTCASE, indirect=True) + def test_train_step(self, detector, dataloader: Dict[str, Any], tmp_dir_path: str): + for data in dataloader: + outputs = detector.train_step({k: v.data[0] for k, v in data.items()}, None) + + output_keys = {key for key in outputs.keys()} + for loss_type in detector.TRACKING_LOSS_TYPE: + assert loss_type in output_keys + + n_steps = 3 + for iter in range(n_steps): + detector.loss_dyns_tracker.accumulate(outputs, iter) + + export_dir = osp.join(tmp_dir_path, "noisy_label_detection") + detector.loss_dyns_tracker.export(export_dir) + + dataset = dm.Dataset.import_from(export_dir, format="datumaro") + + cnt = 0 + for item in dataset: + for ann in item.annotations: + has_attrs = False + for v in ann.attributes.values(): + assert set(list(ann.attributes.keys())) == { + "iters", + *[f"loss_dynamics_{loss_type.name}" for loss_type in detector.TRACKING_LOSS_TYPE], + } + assert len(v) == n_steps + has_attrs = True + if has_attrs: + cnt += 1 + + for loss_type, values in outputs.items(): + if loss_type in detector.TRACKING_LOSS_TYPE: + assert cnt == len( + values + ), "The number of accumulated statistics is equal to the number of Datumaro items which have attirbutes." diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_mean_teacher.py b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_mean_teacher.py new file mode 100644 index 00000000000..f52a0130358 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/detectors/test_mean_teacher.py @@ -0,0 +1,114 @@ +from typing import Dict +import pytest +import torch +import numpy as np +from mmdet.core.mask.structures import BitmapMasks + +from otx.algorithms.detection.adapters.mmdet.models.detectors.mean_teacher import ( + MeanTeacher, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMeanTeacher: + @pytest.fixture(autouse=True) + def setup(self, mocker): + mocker.patch("otx.algorithms.detection.adapters.mmdet.models.detectors.mean_teacher.build_detector") + mocker.patch.object(MeanTeacher, "_register_state_dict_hook") + mocker.patch.object(MeanTeacher, "_register_load_state_dict_pre_hook") + self.mt_is = MeanTeacher("CustomMaskRCNN") + self.mt_det = MeanTeacher("CustomATSS", unlabeled_loss_weights={"cls": 1, "bbox": 1, "obj": 1}) + self.img = torch.rand(4, 3, 300, 300) + self.img_metas = [dict(ori_shape=(300, 300), scale_factor=1.0)] * 4 + self.gt_bboxes = torch.rand(4, 4) + self.gt_labels = torch.randint(20, (4, 1)) + self.gt_masks = torch.rand(4, 3, 300, 300) + + @e2e_pytest_unit + def test_forward_train_segmentation(self, mocker, monkeypatch): + def mock_forward_train(*args, **kwargs): + return {"loss_bbox": 1.0, "loss_cls": 1.0, "loss_mask": 1.0} + + def mock_generate_pseudo_labels(*args, **kwargs): + return (self.gt_bboxes, self.gt_labels, self.gt_masks, 0.0) + + monkeypatch.setattr(self.mt_is.model_s, "forward_train", mock_forward_train) + loss = self.mt_is.forward_train( + self.img, self.img_metas, self.img, self.gt_bboxes, self.gt_labels, self.gt_masks + ) + gt_loss = mock_forward_train() + assert loss == gt_loss + self.mt_is.enable_unlabeled_loss(True) + monkeypatch.setattr(MeanTeacher, "generate_pseudo_labels", mock_generate_pseudo_labels) + mocker.patch.object(MeanTeacher, "forward_teacher") + kwargs = {"extra_0": {"img0": self.img, "img": self.img, "img_metas": self.img_metas}} + loss_mask = self.mt_is.forward_train( + self.img, self.img_metas, None, self.gt_bboxes, self.gt_labels, self.gt_masks, **kwargs + ) + gt_loss.update( + { + "ps_ratio": torch.tensor([0.0]), + "loss_bbox_ul": 1.0, + "loss_cls_ul": 1.0, + "loss_mask_ul": 1.0, + } + ) + assert loss_mask == gt_loss + + @e2e_pytest_unit + def test_forward_train_detection(self, mocker, monkeypatch): + def mock_forward_train(*args, **kwargs): + return {"loss_bbox": 1.0, "loss_cls": 1.0, "loss_obj": 1.0} + + def mock_generate_pseudo_labels(*args, **kwargs): + return (self.gt_bboxes, self.gt_labels, [], 0.0) + + monkeypatch.setattr(self.mt_det.model_s, "forward_train", mock_forward_train) + monkeypatch.setattr(self.mt_is.model_s, "with_mask", False) + loss = self.mt_det.forward_train(self.img, self.img_metas, self.img, self.gt_bboxes, self.gt_labels) + gt_loss = mock_forward_train() + assert loss == gt_loss + self.mt_det.enable_unlabeled_loss(True) + monkeypatch.setattr(MeanTeacher, "generate_pseudo_labels", mock_generate_pseudo_labels) + mocker.patch.object(MeanTeacher, "forward_teacher") + kwargs = {"extra_0": {"img0": self.img, "img": self.img, "img_metas": self.img_metas}} + loss_det = self.mt_det.forward_train( + self.img, self.img_metas, self.img, self.gt_bboxes, self.gt_labels, **kwargs + ) + gt_loss.update( + { + "ps_ratio": torch.tensor([0.0]), + "loss_bbox_ul": 1.0, + "loss_cls_ul": 1.0, + "loss_obj_ul": 1.0, + } + ) + assert loss_det == gt_loss + + @e2e_pytest_unit + def test_generate_pseudo_labels(self, mocker, monkeypatch): + gt_bboxes = np.random.rand(1, 1, 5) + gt_masks = np.random.rand(1, 1, 300, 300) > 0.5 + teacher_output = [([gt_bboxes, gt_masks])] + img_metas = [{"img_shape": (300, 300, 3)}] + monkeypatch.setattr(self.mt_is.model_t, "with_mask", True) + out = self.mt_is.generate_pseudo_labels(teacher_output, img_metas, **{"device": "cpu"}) + assert len(out) == 4 + assert isinstance(out[2][-1], BitmapMasks) + teacher_output = [gt_bboxes] + monkeypatch.setattr(self.mt_is.model_t, "with_mask", False) + out = self.mt_is.generate_pseudo_labels(teacher_output, img_metas, **{"device": "cpu"}) + assert len(out) == 4 + assert len(out[2]) == 0 + + @e2e_pytest_unit + def test_forward_teacher(self, mocker, monkeypatch): + def mock_simple_test_bboxes(*args, **kwargs): + return [self.gt_bboxes], [self.gt_labels] + + monkeypatch.setattr(self.mt_is.model_t.roi_head, "simple_test_bboxes", mock_simple_test_bboxes) + mocker.patch("otx.algorithms.detection.adapters.mmdet.models.detectors.mean_teacher.bbox2result") + mocker.patch("otx.algorithms.detection.adapters.mmdet.models.detectors.mean_teacher.bbox2roi") + teacher_output = self.mt_is.forward_teacher(self.img, self.img_metas) + assert teacher_output is not None + assert isinstance(teacher_output, list) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/heads/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/models/heads/__init__.py new file mode 100644 index 00000000000..3aac4fccae5 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/heads/__init__.py @@ -0,0 +1,4 @@ +"""Unit tests for src/otx/algorithms/detection/adapters/mmdet/models/heads.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/heads/test_custom_dino_head.py b/tests/unit/algorithms/detection/adapters/mmdet/models/heads/test_custom_dino_head.py new file mode 100644 index 00000000000..d614a8eb141 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/heads/test_custom_dino_head.py @@ -0,0 +1,222 @@ +"""Unit tests for CustomDINOHead.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest +import torch +from mmcv.utils import ConfigDict +from mmdet.core import build_assigner +from mmdet.core.bbox.assigners import AssignResult, HungarianAssigner +from mmdet.models.builder import build_detector + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomDINOHead: + @pytest.fixture(autouse=True) + def setup(self): + torch.manual_seed(5) + cfg = ConfigDict( + dict( + type="CustomDINOHead", + num_query=900, + num_classes=80, + in_channels=2048, + sync_cls_avg_factor=True, + with_box_refine=True, + as_two_stage=True, + transformer=dict( + type="CustomDINOTransformer", + encoder=dict( + type="DetrTransformerEncoder", + num_layers=6, + transformerlayers=dict( + type="BaseTransformerLayer", + attn_cfgs=dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "ffn", "norm"), + ), + ), + decoder=dict( + type="DINOTransformerDecoder", + num_layers=6, + return_intermediate=True, + transformerlayers=dict( + type="DetrTransformerDecoderLayer", + attn_cfgs=[ + dict(type="MultiheadAttention", embed_dims=256, num_heads=8, dropout=0.0), + dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + ], + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "cross_attn", "norm", "ffn", "norm"), + ), + ), + ), + positional_encoding=dict( + type="SinePositionalEncoding", num_feats=128, normalize=True, offset=0.0, temperature=20 + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=5.0), + loss_iou=dict(type="GIoULoss", loss_weight=2.0), + dn_cfg=dict( + label_noise_scale=0.5, + box_noise_scale=1.0, # 0.4 for DN-DETR + group_cfg=dict(dynamic=True, num_groups=None, num_dn_queries=100), + ), + ), + ) + self.bbox_head = build_detector(cfg) + + assigner_cfg = ConfigDict( + type="HungarianAssigner", + cls_cost=dict(type="FocalLossCost", weight=1.0), + reg_cost=dict(type="BBoxL1Cost", weight=5.0, box_format="xywh"), + iou_cost=dict(type="IoUCost", iou_mode="giou", weight=2.0), + ) + self.bbox_head.assigner = build_assigner(assigner_cfg) + + test_cfg = dict(max_per_img=300) + self.bbox_head.test_cfg = test_cfg + + @e2e_pytest_unit + def test_forward_train(self, mocker): + inputs = [ + torch.zeros([2, 256, 92, 95]), + torch.zeros([2, 256, 46, 48]), + torch.zeros([2, 256, 23, 24]), + torch.zeros([2, 256, 12, 12]), + ] + gt_bboxes = [ + torch.Tensor( + [ + [432.2500, 514.2661, 632.6323, 638.8889], + [361.2484, 294.9931, 558.4751, 466.9410], + [616.8542, 201.9204, 752.5462, 328.1207], + [591.6091, 386.4883, 733.6124, 571.0562], + [728.8790, 255.5556, 760.0000, 408.5734], + [713.1008, 397.5309, 760.0000, 541.0837], + [246.0680, 354.9383, 427.5165, 498.4911], + [113.5316, 361.2483, 309.1805, 517.4211], + [457.4950, 654.6639, 646.8326, 736.0000], + [132.4654, 631.0014, 187.6889, 684.6365], + [217.6673, 694.1015, 298.1358, 736.0000], + [0.0000, 583.6763, 56.7303, 672.0164], + [86.7088, 675.1714, 168.7551, 736.0000], + [173.4885, 93.0727, 253.9570, 151.4403], + [738.3458, 119.8903, 760.0000, 164.0603], + [683.1224, 522.1536, 760.0000, 736.0000], + ] + ), + torch.Tensor( + [ + [442.0, 279.0, 544.0, 377.0], + [386.0, 1.0, 497.0, 108.0], + [288.0, 1.0, 399.0, 84.0], + [154.0, 1.0, 268.0, 77.0], + [530.0, 163.0, 625.0, 248.0], + [179.0, 298.0, 278.0, 398.0], + [275.0, 320.0, 374.0, 420.0], + [525.0, 394.0, 613.0, 480.0], + [332.0, 160.0, 463.0, 286.0], + [210.0, 395.0, 308.0, 480.0], + [141.0, 395.0, 239.0, 480.0], + [106.0, 225.0, 204.0, 310.0], + [12.0, 1.0, 148.0, 70.0], + [165.0, 79.0, 396.0, 247.0], + [483.0, 13.0, 518.0, 52.0], + ], + ), + ] + gt_labels = [ + torch.Tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2]).long(), + torch.Tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0]).long(), + ] + img_metas = [ + { + "flip_direction": "horizontal", + "img_shape": (736, 760, 3), + "ori_shape": (480, 640, 3), + "img_norm_cfg": { + "mean": np.array([123.675, 116.28, 103.53], dtype=np.float32), + "std": np.array([58.395, 57.12, 57.375], dtype=np.float32), + "to_rgb": False, + }, + "scale_factor": np.array([1.5139443, 1.5144033, 1.5139443, 1.5144033], dtype=np.float32), + "flip": True, + "pad_shape": (736, 760, 3), + "batch_input_shape": (736, 760), + }, + { + "flip_direction": "horizontal", + "img_shape": (480, 640, 3), + "ori_shape": (480, 640, 3), + "img_norm_cfg": { + "mean": np.array([123.675, 116.28, 103.53], dtype=np.float32), + "std": np.array([58.395, 57.12, 57.375], dtype=np.float32), + "to_rgb": False, + }, + "scale_factor": np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float32), + "flip": True, + "pad_shape": (480, 640, 3), + "batch_input_shape": (736, 760), + }, + ] + + mock_assign_result = AssignResult( + num_gts=16, + gt_inds=torch.randint(0, 2, (900,)), + max_overlaps=None, + labels=torch.zeros(900), + ) + mocker.patch.object(HungarianAssigner, "assign", return_value=mock_assign_result) + + losses = self.bbox_head.forward_train(inputs, img_metas, gt_bboxes, gt_labels) + assert len(losses) == 39 + + @e2e_pytest_unit + def test_simple_test_bboxes(self): + feats = [ + torch.zeros([2, 256, 100, 134]), + torch.zeros([2, 256, 50, 67]), + torch.zeros([2, 256, 25, 34]), + torch.zeros([2, 256, 13, 17]), + ] + img_metas = [ + { + "ori_shape": (480, 640, 3), + "img_shape": (800, 1067, 3), + "pad_shape": (800, 1067, 3), + "scale_factor": np.array([1.6671875, 1.6666666, 1.6671875, 1.6666666], dtype=np.float32), + "flip": False, + "flip_direction": None, + "img_norm_cfg": { + "mean": np.array([123.675, 116.28, 103.53], dtype=np.float32), + "std": np.array([58.395, 57.12, 57.375], dtype=np.float32), + "to_rgb": False, + }, + "batch_input_shape": (800, 1067), + }, + { + "ori_shape": (480, 640, 3), + "img_shape": (800, 1067, 3), + "pad_shape": (800, 1067, 3), + "scale_factor": np.array([1.6671875, 1.6666666, 1.6671875, 1.6666666], dtype=np.float32), + "flip": False, + "flip_direction": None, + "img_norm_cfg": { + "mean": np.array([123.675, 116.28, 103.53], dtype=np.float32), + "std": np.array([58.395, 57.12, 57.375], dtype=np.float32), + "to_rgb": False, + }, + "batch_input_shape": (800, 1067), + }, + ] + self.bbox_head.eval() + results = self.bbox_head.simple_test_bboxes(feats, img_metas) + assert len(results) == 2 + assert results[0][0].shape == torch.Size([300, 5]) + assert results[0][1].shape == torch.Size([300]) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/heads/test_custom_lite_dino_head.py b/tests/unit/algorithms/detection/adapters/mmdet/models/heads/test_custom_lite_dino_head.py new file mode 100644 index 00000000000..a8695c1acac --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/heads/test_custom_lite_dino_head.py @@ -0,0 +1,244 @@ +"""Unit tests for CustomDINOHead.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest +import torch +from mmcv.utils import ConfigDict +from mmdet.core import build_assigner +from mmdet.core.bbox.assigners import AssignResult, HungarianAssigner +from mmdet.models.builder import build_detector + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCustomDINOHead: + @pytest.fixture(autouse=True) + def setup(self): + torch.manual_seed(5) + cfg = ConfigDict( + dict( + type="CustomDINOHead", + num_query=900, + num_classes=80, + in_channels=2048, + sync_cls_avg_factor=True, + with_box_refine=True, + as_two_stage=True, + transformer=dict( + type="CustomDINOTransformer", + encoder=dict( + type="EfficientTransformerEncoder", + num_expansion=3, + enc_scale=1, + num_layers=6, + transformerlayers=[ + dict( + type="EfficientTransformerLayer", + enc_scale=1, + attn_cfgs=dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "ffn", "norm"), + ), + dict( + type="EfficientTransformerLayer", + enc_scale=1, + small_expand=True, + attn_cfgs=dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + ffn_cfgs=dict( + type="SmallExpandFFN", + embed_dims=256, + feedforward_channels=1024, + num_fcs=2, + ffn_drop=0.0, + act_cfg=dict(type="ReLU", inplace=True), + ), + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "ffn"), + ), + ], + ), + decoder=dict( + type="DINOTransformerDecoder", + num_layers=6, + return_intermediate=True, + transformerlayers=dict( + type="DetrTransformerDecoderLayer", + attn_cfgs=[ + dict(type="MultiheadAttention", embed_dims=256, num_heads=8, dropout=0.0), + dict(type="MultiScaleDeformableAttention", embed_dims=256, dropout=0.0), + ], + feedforward_channels=2048, + ffn_dropout=0.0, + operation_order=("self_attn", "norm", "cross_attn", "norm", "ffn", "norm"), + ), + ), + ), + positional_encoding=dict( + type="SinePositionalEncoding", num_feats=128, normalize=True, offset=0.0, temperature=20 + ), + loss_cls=dict(type="FocalLoss", use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), + loss_bbox=dict(type="L1Loss", loss_weight=5.0), + loss_iou=dict(type="GIoULoss", loss_weight=2.0), + dn_cfg=dict( + label_noise_scale=0.5, + box_noise_scale=1.0, # 0.4 for DN-DETR + group_cfg=dict(dynamic=True, num_groups=None, num_dn_queries=100), + ), + ), + ) + self.bbox_head = build_detector(cfg) + + assigner_cfg = ConfigDict( + type="HungarianAssigner", + cls_cost=dict(type="FocalLossCost", weight=1.0), + reg_cost=dict(type="BBoxL1Cost", weight=5.0, box_format="xywh"), + iou_cost=dict(type="IoUCost", iou_mode="giou", weight=2.0), + ) + self.bbox_head.assigner = build_assigner(assigner_cfg) + + test_cfg = dict(max_per_img=300) + self.bbox_head.test_cfg = test_cfg + + @e2e_pytest_unit + def test_forward_train(self, mocker): + inputs = [ + torch.zeros([2, 256, 92, 95]), + torch.zeros([2, 256, 46, 48]), + torch.zeros([2, 256, 23, 24]), + torch.zeros([2, 256, 12, 12]), + ] + gt_bboxes = [ + torch.Tensor( + [ + [432.2500, 514.2661, 632.6323, 638.8889], + [361.2484, 294.9931, 558.4751, 466.9410], + [616.8542, 201.9204, 752.5462, 328.1207], + [591.6091, 386.4883, 733.6124, 571.0562], + [728.8790, 255.5556, 760.0000, 408.5734], + [713.1008, 397.5309, 760.0000, 541.0837], + [246.0680, 354.9383, 427.5165, 498.4911], + [113.5316, 361.2483, 309.1805, 517.4211], + [457.4950, 654.6639, 646.8326, 736.0000], + [132.4654, 631.0014, 187.6889, 684.6365], + [217.6673, 694.1015, 298.1358, 736.0000], + [0.0000, 583.6763, 56.7303, 672.0164], + [86.7088, 675.1714, 168.7551, 736.0000], + [173.4885, 93.0727, 253.9570, 151.4403], + [738.3458, 119.8903, 760.0000, 164.0603], + [683.1224, 522.1536, 760.0000, 736.0000], + ] + ), + torch.Tensor( + [ + [442.0, 279.0, 544.0, 377.0], + [386.0, 1.0, 497.0, 108.0], + [288.0, 1.0, 399.0, 84.0], + [154.0, 1.0, 268.0, 77.0], + [530.0, 163.0, 625.0, 248.0], + [179.0, 298.0, 278.0, 398.0], + [275.0, 320.0, 374.0, 420.0], + [525.0, 394.0, 613.0, 480.0], + [332.0, 160.0, 463.0, 286.0], + [210.0, 395.0, 308.0, 480.0], + [141.0, 395.0, 239.0, 480.0], + [106.0, 225.0, 204.0, 310.0], + [12.0, 1.0, 148.0, 70.0], + [165.0, 79.0, 396.0, 247.0], + [483.0, 13.0, 518.0, 52.0], + ], + ), + ] + gt_labels = [ + torch.Tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2]).long(), + torch.Tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0]).long(), + ] + img_metas = [ + { + "flip_direction": "horizontal", + "img_shape": (736, 760, 3), + "ori_shape": (480, 640, 3), + "img_norm_cfg": { + "mean": np.array([123.675, 116.28, 103.53], dtype=np.float32), + "std": np.array([58.395, 57.12, 57.375], dtype=np.float32), + "to_rgb": False, + }, + "scale_factor": np.array([1.5139443, 1.5144033, 1.5139443, 1.5144033], dtype=np.float32), + "flip": True, + "pad_shape": (736, 760, 3), + "batch_input_shape": (736, 760), + }, + { + "flip_direction": "horizontal", + "img_shape": (480, 640, 3), + "ori_shape": (480, 640, 3), + "img_norm_cfg": { + "mean": np.array([123.675, 116.28, 103.53], dtype=np.float32), + "std": np.array([58.395, 57.12, 57.375], dtype=np.float32), + "to_rgb": False, + }, + "scale_factor": np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float32), + "flip": True, + "pad_shape": (480, 640, 3), + "batch_input_shape": (736, 760), + }, + ] + + mock_assign_result = AssignResult( + num_gts=16, + gt_inds=torch.randint(0, 2, (900,)), + max_overlaps=None, + labels=torch.zeros(900), + ) + mocker.patch.object(HungarianAssigner, "assign", return_value=mock_assign_result) + + losses = self.bbox_head.forward_train(inputs, img_metas, gt_bboxes, gt_labels) + assert len(losses) == 39 + + @e2e_pytest_unit + def test_simple_test_bboxes(self): + feats = [ + torch.zeros([2, 256, 100, 134]), + torch.zeros([2, 256, 50, 67]), + torch.zeros([2, 256, 25, 34]), + torch.zeros([2, 256, 13, 17]), + ] + img_metas = [ + { + "ori_shape": (480, 640, 3), + "img_shape": (800, 1067, 3), + "pad_shape": (800, 1067, 3), + "scale_factor": np.array([1.6671875, 1.6666666, 1.6671875, 1.6666666], dtype=np.float32), + "flip": False, + "flip_direction": None, + "img_norm_cfg": { + "mean": np.array([123.675, 116.28, 103.53], dtype=np.float32), + "std": np.array([58.395, 57.12, 57.375], dtype=np.float32), + "to_rgb": False, + }, + "batch_input_shape": (800, 1067), + }, + { + "ori_shape": (480, 640, 3), + "img_shape": (800, 1067, 3), + "pad_shape": (800, 1067, 3), + "scale_factor": np.array([1.6671875, 1.6666666, 1.6671875, 1.6666666], dtype=np.float32), + "flip": False, + "flip_direction": None, + "img_norm_cfg": { + "mean": np.array([123.675, 116.28, 103.53], dtype=np.float32), + "std": np.array([58.395, 57.12, 57.375], dtype=np.float32), + "to_rgb": False, + }, + "batch_input_shape": (800, 1067), + }, + ] + self.bbox_head.eval() + results = self.bbox_head.simple_test_bboxes(feats, img_metas) + assert len(results) == 2 + assert results[0][0].shape == torch.Size([300, 5]) + assert results[0][1].shape == torch.Size([300]) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/losses/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/models/losses/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/losses/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/losses/test_cross_focal_loss.py b/tests/unit/algorithms/detection/adapters/mmdet/models/losses/test_cross_focal_loss.py new file mode 100644 index 00000000000..7e2256ffdd2 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/losses/test_cross_focal_loss.py @@ -0,0 +1,63 @@ +"""Test cross focal loss.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import numpy as np +import pytest +import torch + +from otx.algorithms.detection.adapters.mmdet.models.losses.cross_focal_loss import ( + CrossSigmoidFocalLoss, + OrdinaryFocalLoss, +) + + +class TestCrossFocalLoss: + """Test cross focal loss.""" + + @pytest.fixture(autouse=True) + def setup(self): + """Create the loss object""" + self.predictions = torch.tensor([[0, 1, 0], [0, 1, 0], [0, 0, 1]], dtype=torch.float32) + self.labels = torch.tensor([1, 1, 2]) + self.loss = CrossSigmoidFocalLoss(num_classes=3) + + def test_loss_computation(self): + """Test loss output.""" + assert np.round(self.loss(self.predictions, self.labels).item(), decimals=4) == 0.0885 + + @pytest.mark.xfail(reason="This should fail as the masks are different") + def test_loss_computation_with_mask(self): + """Tests loss output with ignored label is provided.""" + valid_label_mask1 = torch.ones((3, 3)) + loss1 = self.loss(self.predictions, self.labels, valid_label_mask=valid_label_mask1) + valid_label_mask2 = torch.zeros((3, 3)) + loss2 = self.loss(self.predictions, self.labels, valid_label_mask=valid_label_mask2) + assert loss1 != loss2 + + def test_reduction(self): + """Test reduction.""" + + loss1 = self.loss(self.predictions, self.labels, reduction_override="none") + loss2 = self.loss(self.predictions, self.labels, reduction_override="mean") + loss3 = self.loss(self.predictions, self.labels, reduction_override="sum") + assert loss1.shape == (3, 3) + assert loss2 != loss3 + + +class TestFocalLoss: + @pytest.fixture(autouse=True) + def setup(self): + """Create the loss object""" + self.predictions = torch.tensor([[0, 1, 0], [0, 1, 0], [0, 0, 1]], dtype=torch.float32) + self.labels = torch.tensor([1, 1, 2]) + self.loss = OrdinaryFocalLoss(gamma=1.5) + + def test_forward(self): + loss = self.loss(self.predictions, self.labels, reduction="none") + assert loss is not None + assert loss.shape == (3,) + loss = self.loss(self.predictions, self.labels, avg_factor=1, reduction="mean") + assert isinstance(loss.item(), float) + assert loss > 0 diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/losses/test_l2sp_loss.py b/tests/unit/algorithms/detection/adapters/mmdet/models/losses/test_l2sp_loss.py new file mode 100644 index 00000000000..4dd9e8d3e37 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/losses/test_l2sp_loss.py @@ -0,0 +1,93 @@ +"""Test L2SP loss with ignore.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from tempfile import TemporaryDirectory + +import numpy as np +import pytest +import torch +from torch import nn +from torchvision.models import resnet18 + +from otx.algorithms.detection.adapters.mmdet.models.losses.l2sp_loss import L2SPLoss + + +class DummyModel(nn.Module): + """Creates a dummy model for testing purposes.""" + + def __init__(self, set_init_weights: float = 0.0): + super().__init__() + self.fc = nn.Linear(4, 1) + self.cnn = nn.Conv2d(1, 1, 1) + self._set_weights(set_init_weights) + + def _set_weights(self, set_init_weights: float = 0.0): + """Sets weights of all the parameters with a given value.""" + for param in self.parameters(): + param.data.fill_(set_init_weights) + + +class TestL2SPLoss: + """Test L2SP loss.""" + + @pytest.fixture(scope="class") + def model_checkpoint(self): + """Create a model checkpoint.""" + with TemporaryDirectory() as tmp_path: + model = DummyModel(set_init_weights=1.0) + model_ckpt = tmp_path + "/model.ckpt" + torch.save(model.state_dict(), model_ckpt) + yield model_ckpt + + def test_loss(self, model_checkpoint): + """Test loss output.""" + new_model = DummyModel(set_init_weights=0.0) + loss = L2SPLoss(new_model, model_checkpoint) + + assert np.round(loss().item(), 4).item() == 0.0007 + + new_model = DummyModel(set_init_weights=1.0) + loss = L2SPLoss(new_model, model_checkpoint) + assert loss().item() == 0.0 + + def test_value_error(self, model_checkpoint): + """Test ValueError.""" + model = DummyModel() + + with pytest.raises(ValueError): + L2SPLoss(model, model_ckpt=None) + + with pytest.raises(ValueError): + L2SPLoss(model, model_checkpoint, loss_weight=-1.0) + + def test_state_dict(self): + """Test model load with weights stored under state_dict key.""" + with TemporaryDirectory() as tmp_path: + model = DummyModel(set_init_weights=1.0) + model_ckpt = tmp_path + "/model.ckpt" + torch.save({"state_dict": model.state_dict()}, model_ckpt) + new_model = DummyModel(set_init_weights=1.0) + loss = L2SPLoss(new_model, model_ckpt) + assert loss().item() == 0.0 + + def test_backbone(self): + """Test model load with backbone weights. + + This tests the case when the new model has backbone param whereas the source model as the backbone layers + directly. + """ + with TemporaryDirectory() as tmp_path: + model = DummyModel() + for param, value in resnet18(pretrained=True).named_children(): + setattr(model, param, value) + model_ckpt = tmp_path + "/model.ckpt" + torch.save(model.state_dict(), model_ckpt) + + new_model = DummyModel() + setattr(new_model, "backbone", resnet18(pretrained=True)) + # new_model.load_state_dict(torch.load(model_ckpt)) + loss = L2SPLoss(new_model, model_ckpt) + assert loss().item() == 0.0 diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/necks/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/models/necks/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/necks/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/necks/test_ov_mmdet_mmov_ssd_neck.py b/tests/unit/algorithms/detection/adapters/mmdet/models/necks/test_ov_mmdet_mmov_ssd_neck.py new file mode 100644 index 00000000000..3139c6da47f --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/necks/test_ov_mmdet_mmov_ssd_neck.py @@ -0,0 +1,52 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import openvino.runtime as ov +import pytest +import torch + +from otx.algorithms.detection.adapters.mmdet.models.necks.mmov_ssd_neck import ( + MMOVSSDNeck, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMMOVSSDNeck: + @pytest.fixture(autouse=True) + def setup(self): + + param = ov.opset10.parameter([1, 24, 64, 64], ov.Type.f32, name="in") + filter = ov.opset10.constant(np.random.normal(size=(1, 24, 64, 64)), ov.Type.f32) + mul = ov.opset10.matmul(param, filter, False, False) + result = ov.opset10.result(mul, name="out") + + ov_model = ov.Model([result], [param], "ssd_neck") + + self.model = MMOVSSDNeck( + model_path_or_model=ov_model, + inputs=dict( + extra_layers=[ + "in", + ], + ), + outputs=dict( + extra_layers=[ + "out", + ], + ), + ) + + @e2e_pytest_unit + def test_init_weights(self): + self.model.init_weights() + + @e2e_pytest_unit + def test_forward(self): + data = {} + for key, shape in self.model.extra_layers[0].input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data[key] = torch.randn(shape) + self.model.train() + self.model(list(data.values())) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/test_ov_mmdet_single_level_roi_extractor.py b/tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/test_ov_mmdet_single_level_roi_extractor.py new file mode 100644 index 00000000000..c75c0a40125 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/roi_heads/roi_extractors/test_ov_mmdet_single_level_roi_extractor.py @@ -0,0 +1,20 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.algorithms.detection.adapters.mmdet.models.roi_heads.roi_extractors.single_level_roi_extractor import ( + RoIInterpolationPool, + SingleRoIExtractor, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSingleRoIExtractor: + @e2e_pytest_unit + def test_build_roi_layers(self): + extractor = SingleRoIExtractor( + roi_layer=dict(type="RoIInterpolationPool", output_size=14, mode="bilinear"), + out_channels=1024, + featmap_strides=[8], + ) + assert all(isinstance(layer, RoIInterpolationPool) for layer in extractor.roi_layers) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/nncf/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/nncf/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/nncf/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/nncf/test_mmdet_nncf_builder.py b/tests/unit/algorithms/detection/adapters/mmdet/nncf/test_mmdet_nncf_builder.py new file mode 100644 index 00000000000..e72dd4b9cfc --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/nncf/test_mmdet_nncf_builder.py @@ -0,0 +1,97 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import tempfile + +import pytest +import numpy as np +import torch +from nncf.torch.nncf_network import NNCFNetwork + +from otx.algorithms.common.adapters.nncf.compression import NNCFMetaState +from otx.algorithms.detection.adapters.mmdet.nncf.builder import build_nncf_detector +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmcv.nncf.test_helpers import ( + create_config, + create_dataset, + create_model, +) + + +@pytest.fixture(autouse=True) +def prepare_dataset(): + create_dataset(lib="mmdet") + + +@pytest.fixture(scope="module") +def temp_dir(): + with tempfile.TemporaryDirectory() as tempdir: + yield tempdir + + +@pytest.fixture +def nncf_model_path(temp_dir): + return os.path.join(temp_dir, "nncf_model.bin") + + +@pytest.fixture(scope="module") +def state_to_build(): + model = create_model(lib="mmdet") + return model.state_dict() + + +@pytest.fixture +def mock_config(temp_dir, state_to_build): + model_path = os.path.join(temp_dir, "model.bin") + mock_config = create_config(lib="mmdet") + torch.save(state_to_build, model_path) + mock_config.load_from = model_path + return mock_config + + +@e2e_pytest_unit +def test_build_nncf_detector(mock_config): + with tempfile.TemporaryDirectory() as tempdir: + mock_config.nncf_config.log_dir = tempdir + _, model = build_nncf_detector(mock_config) + + assert isinstance(model, NNCFNetwork) + assert len([hook for hook in mock_config.custom_hooks if hook.type == "CompressionHook"]) == 1 + + +@e2e_pytest_unit +def test_build_nncf_detector_not_compress_postprocessing(mock_config, state_to_build, nncf_model_path): + with tempfile.TemporaryDirectory() as tempdir: + mock_config.nncf_config.log_dir = tempdir + mock_config.nncf_compress_postprocessing = False + ctrl, model = build_nncf_detector(mock_config) + + assert isinstance(model, NNCFNetwork) + assert len([hook for hook in mock_config.custom_hooks if hook.type == "CompressionHook"]) == 1 + + # save a model for next test + torch.save( + { + "meta": { + "nncf_enable_compression": True, + "nncf_meta": NNCFMetaState( + data_to_build=np.zeros((50, 50, 3)), + compression_ctrl=ctrl.get_compression_state(), + state_to_build=state_to_build, + ), + }, + "state_dict": model.state_dict(), + }, + nncf_model_path, + ) + + +@e2e_pytest_unit +def test_build_nncf_detector_with_nncf_ckpt(mock_config, nncf_model_path): + with tempfile.TemporaryDirectory() as tempdir: + mock_config.nncf_config.log_dir = tempdir + _, model = build_nncf_detector(mock_config, nncf_model_path) + assert isinstance(model, NNCFNetwork) + assert len([hook for hook in mock_config.custom_hooks if hook.type == "CompressionHook"]) == 1 diff --git a/tests/unit/algorithms/detection/adapters/mmdet/nncf/test_mmdet_nncf_patches.py b/tests/unit/algorithms/detection/adapters/mmdet/nncf/test_mmdet_nncf_patches.py new file mode 100644 index 00000000000..63a02750f8c --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/nncf/test_mmdet_nncf_patches.py @@ -0,0 +1,14 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmdet.models.detectors.base import BaseDetector + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_patches(): + import otx.algorithms.detection.adapters.mmdet.nncf.patches # noqa: F401 + + assert getattr(BaseDetector, "nncf_trace_context", None) is not None diff --git a/tests/unit/algorithms/detection/adapters/mmdet/nncf/test_task.py b/tests/unit/algorithms/detection/adapters/mmdet/nncf/test_task.py new file mode 100644 index 00000000000..b000a0f7eca --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/nncf/test_task.py @@ -0,0 +1,130 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from contextlib import nullcontext +import os +from unittest.mock import MagicMock + +import numpy as np +import pytest +from mmcv.utils import Config + +from otx.algorithms.common.adapters.mmcv.hooks import OTXLoggerHook +from otx.algorithms.detection.adapters.mmdet.nncf.task import DetectionNNCFTask +from otx.api.configuration.helper import create +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.metrics import NullPerformance, Performance, ScoreMetric +from otx.api.entities.model_template import TaskType, parse_model_template +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.adapters.mmdet.test_task import MockDataLoader, MockDataset +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_DET_TEMPLATE_DIR, + generate_det_dataset, + init_environment, +) + + +class TestOTXDetTaskNNCF: + @pytest.fixture(autouse=True) + def setup(self, otx_model, tmp_dir_path) -> None: + model_template = parse_model_template(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + task_env = init_environment(hyper_parameters, model_template) + self.model = otx_model + self.det_nncf_task = DetectionNNCFTask(task_env, output_path=str(tmp_dir_path)) + self.dataset, _ = generate_det_dataset(task_type=TaskType.DETECTION) + + @e2e_pytest_unit + def test_save_model(self, mocker): + """Test save_model method in OTXDetTaskNNCF.""" + mocker.patch("torch.load", return_value="") + self.det_nncf_task._recipe_cfg = Config( + { + "model": { + "bbox_head": { + "anchor_generator": { + "reclustering_anchors": True, + "heights": [10], + "widths": [10], + } + } + } + } + ) + self.det_nncf_task.config = self.det_nncf_task._recipe_cfg + self.det_nncf_task.save_model(self.model) + + assert self.model.get_data("weights.pth") + assert self.model.get_data("label_schema.json") + + @e2e_pytest_unit + def test_optimize(self, mocker): + """Test optimize method in OTXDetTaskNNCF.""" + mock_lcurve_val = OTXLoggerHook.Curve() + mock_lcurve_val.x = [0, 1] + mock_lcurve_val.y = [0.1, 0.2] + mock_run_task = mocker.patch.object(DetectionNNCFTask, "_train_model", return_value={"final_ckpt": ""}) + self.det_nncf_task._learning_curves = {"val/mAP": mock_lcurve_val} + mocker.patch.object(DetectionNNCFTask, "save_model") + + fake_prediction = [[np.array([[0, 0, 32, 24, 0.55]], dtype=np.float32)]] + fake_feature_vectors = [np.zeros((1, 1, 1))] + fake_saliency_maps = [None] + mocker.patch.object( + DetectionNNCFTask, + "_infer_model", + return_value=(zip(fake_prediction, fake_feature_vectors, fake_saliency_maps), 1.0), + ) + fake_metrics = mocker.patch("otx.api.usecases.evaluation.f_measure.FMeasure", autospec=True) + fake_metrics.get_performance.return_value = Performance( + score=ScoreMetric(name="fake", value=0.1), dashboard_metrics=["mAP"] + ) + mocker.patch.object(MetricsHelper, "compute_f_measure", return_value=fake_metrics) + + mocker.patch.object(DetectionNNCFTask, "_init_task") + + self.det_nncf_task.optimize(OptimizationType.NNCF, self.dataset, self.model) + + mock_run_task.assert_called_once() + assert self.model.performance != NullPerformance() + assert self.model.performance.score.value == 0.1 + + @e2e_pytest_unit + def test_infer(self, mocker) -> None: + """Test infer function.""" + + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.nncf.task.build_nncf_detector", + return_value=(None, MagicMock()), + ) + mocker.patch( + "otx.algorithms.common.adapters.mmcv.utils.builder.build_dataset", + return_value=MockDataset(self.dataset, "det"), + ) + mocker.patch( + "otx.algorithms.common.adapters.mmcv.utils.builder.build_dataloader", + return_value=MockDataLoader(self.dataset), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.single_gpu_test", + return_value=[ + np.array([np.array([[0, 0, 1, 1, 0.1]]), np.array([[0, 0, 1, 1, 0.2]]), np.array([[0, 0, 1, 1, 0.7]])]) + ], + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + inference_parameters = InferenceParameters(is_evaluation=True) + outputs = self.det_nncf_task.infer(self.dataset, inference_parameters) + for output in outputs: + assert output.get_annotations()[-1].get_labels()[0].probability == 0.7 + + @e2e_pytest_unit + def test_initialize(self, mocker): + """Test initialize method in OTXDetTaskNNCF.""" + self.det_nncf_task._init_task() diff --git a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py new file mode 100644 index 00000000000..1d6d2db1237 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py @@ -0,0 +1,426 @@ +"""Test otx mmdet configurer.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os + +import pytest +import tempfile +from mmcv.runner import CheckpointLoader +from mmcv.utils import ConfigDict + +from otx.api.entities.model_template import TaskType +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.algorithms.detection.adapters.mmdet import configurer +from otx.algorithms.detection.adapters.mmdet.configurer import ( + DetectionConfigurer, + IncrDetectionConfigurer, + SemiSLDetectionConfigurer, +) +from otx.algorithms.common.configs.configuration_enums import InputSizePreset +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_DET_TEMPLATE_DIR, + generate_det_dataset, +) + + +@pytest.fixture +def device_availability_func(mocker): + return { + "cuda": mocker.patch("torch.cuda.is_available"), + "xpu": mocker.patch("otx.algorithms.common.adapters.mmcv.configurer.is_xpu_available"), + "hpu": mocker.patch("otx.algorithms.common.adapters.mmcv.configurer.is_hpu_available"), + } + + +class TestDetectionConfigurer: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.configurer = DetectionConfigurer( + "segmentation", + True, + False, + {}, + None, + None, + None, + ) + self.model_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "model.py")) + self.data_pipeline_path = os.path.join(DEFAULT_DET_TEMPLATE_DIR, "data_pipeline.py") + + self.det_dataset, self.det_labels = generate_det_dataset(TaskType.DETECTION, 100) + self.data_cfg = ConfigDict( + { + "data": { + "train": {"otx_dataset": self.det_dataset, "labels": self.det_labels}, + "val": {"otx_dataset": self.det_dataset, "labels": self.det_labels}, + "test": {"otx_dataset": self.det_dataset, "labels": self.det_labels}, + } + } + ) + + @e2e_pytest_unit + def test_configure(self, mocker): + mock_cfg_merge = mocker.patch.object(DetectionConfigurer, "merge_configs") + mock_cfg_ckpt = mocker.patch.object(DetectionConfigurer, "configure_ckpt") + mock_cfg_env = mocker.patch.object(DetectionConfigurer, "configure_env") + mock_cfg_data_pipeline = mocker.patch.object(DetectionConfigurer, "configure_data_pipeline") + mock_cfg_recipe = mocker.patch.object(DetectionConfigurer, "configure_recipe") + mock_cfg_model = mocker.patch.object(DetectionConfigurer, "configure_model") + mock_cfg_hook = mocker.patch.object(DetectionConfigurer, "configure_hooks") + mock_cfg_compat_cfg = mocker.patch.object(DetectionConfigurer, "configure_compat_cfg") + + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.model_task = "detection" + data_cfg = copy.deepcopy(self.data_cfg) + returned_value = self.configurer.configure( + model_cfg, + self.data_pipeline_path, + None, + "", + data_cfg, + train_dataset=self.det_dataset, + max_num_detections=100, + ) + + mock_cfg_merge.assert_called_once_with( + model_cfg, + data_cfg, + self.data_pipeline_path, + None, + train_dataset=self.det_dataset, + max_num_detections=100, + ) + mock_cfg_ckpt.assert_called_once_with(model_cfg, "") + mock_cfg_env.assert_called_once_with(model_cfg) + mock_cfg_data_pipeline.assert_called_once_with( + model_cfg, None, "", train_dataset=self.det_dataset, max_num_detections=100 + ) + mock_cfg_recipe.assert_called_once_with(model_cfg, train_dataset=self.det_dataset, max_num_detections=100) + mock_cfg_hook.assert_called_once_with(model_cfg) + mock_cfg_model.assert_called_once_with( + model_cfg, None, None, None, train_dataset=self.det_dataset, max_num_detections=100 + ) + mock_cfg_compat_cfg.assert_called_once_with(model_cfg) + assert returned_value == model_cfg + + @e2e_pytest_unit + def test_merge_configs(self, mocker): + mocker.patch("otx.algorithms.common.adapters.mmcv.configurer.patch_from_hyperparams", return_value=True) + mocker.patch("otx.algorithms.detection.adapters.mmdet.configurer.patch_tiling", return_value=True) + self.configurer.merge_configs(self.model_cfg, self.data_cfg, self.data_pipeline_path, None) + assert self.model_cfg.data + assert self.model_cfg.data.train + assert self.model_cfg.data.val + + @e2e_pytest_unit + def test_configure_ckpt(self, mocker): + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.resume = True + + mocker.patch.object( + CheckpointLoader, + "load_checkpoint", + return_value={"model": None}, + ) + with tempfile.TemporaryDirectory() as tempdir: + self.configurer.configure_ckpt(model_cfg, os.path.join(tempdir, "dummy.pth")) + for hook in model_cfg.custom_hooks: + if hook.type in self.configurer.ema_hooks: + assert hook.resume_from == model_cfg.resume_from + + @e2e_pytest_unit + def test_configure_env(self): + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + self.model_cfg.merge_from_dict(data_pipeline_cfg) + self.configurer.configure_env(self.model_cfg) + + @e2e_pytest_unit + @pytest.mark.parametrize("current_device", ["cpu", "cuda", "xpu", "hpu"]) + def test_configure_device(self, mocker, device_availability_func, current_device): + for key, mock_func in device_availability_func.items(): + if current_device == key: + mock_func.return_value = True + else: + mock_func.return_value = False + + mocker.patch( + "torch.distributed.is_initialized", + return_value=False, + ) + + config = copy.deepcopy(self.model_cfg) + self.configurer.configure_device(config) + assert config.distributed is False + assert config.device == current_device + + @e2e_pytest_unit + def test_configure_dist_device(self, mocker): + mocker.patch( + "torch.distributed.is_initialized", + return_value=True, + ) + + config = copy.deepcopy(self.model_cfg) + mocker.patch("torch.distributed.get_world_size", return_value=2) + world_size = 2 + mocker.patch("os.environ", return_value={"LOCAL_RANK": 2}) + origin_lr = config.optimizer.lr + + self.configurer.configure_device(config) + assert config.distributed is True + assert config.optimizer.lr == pytest.approx(origin_lr * world_size) + + @e2e_pytest_unit + def test_configure_samples_per_gpu(self): + model_cfg = copy.deepcopy(self.model_cfg) + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + model_cfg.merge_from_dict(data_pipeline_cfg) + model_cfg.data.train_dataloader = ConfigDict({"samples_per_gpu": 2}) + model_cfg.data.train.otx_dataset = range(1) + self.configurer.configure_samples_per_gpu(model_cfg) + assert model_cfg.data.train_dataloader == {"samples_per_gpu": 1, "drop_last": True} + + @e2e_pytest_unit + @pytest.mark.parametrize("input_size", [None, (0, 0), (256, 256)]) + @pytest.mark.parametrize("training", [True, False]) + def test_configure_input_size_not_yolox(self, mocker, input_size, training): + # prepare + mock_cfg = mocker.MagicMock() + mock_input_manager_cls = mocker.patch.object(configurer, "InputSizeManager") + mock_input_manager = mock_input_manager_cls.return_value + mock_input_manager.get_trained_input_size.return_value = (32, 32) + mock_input_manager_cls.return_value = mock_input_manager + mock_base_configurer_cls = mocker.patch.object(configurer, "BaseConfigurer") + mock_base_configurer_cls.adapt_input_size_to_dataset.return_value = (64, 64) + + # execute + self.configurer.configure_input_size(mock_cfg, input_size, "ckpt/path", training=training) + + # check + if input_size is None: + mock_input_manager.set_input_size.assert_not_called() + elif input_size == (0, 0): + if training: + mock_input_manager.set_input_size.assert_called_once_with((64, 64)) + else: + mock_input_manager.set_input_size.assert_called_once_with((32, 32)) + else: + mock_input_manager.set_input_size.assert_called_once_with(input_size) + + @e2e_pytest_unit + @pytest.mark.parametrize("is_yolox_tiny", [True, False]) + def test_configure_input_size_yolox(self, mocker, is_yolox_tiny): + # prepare + mock_cfg = mocker.MagicMock() + mock_cfg.model.type = "CustomYOLOX" + if is_yolox_tiny: + mock_cfg.model.backbone.widen_factor = 0.375 + base_input_size = { + "train": (640, 640), + "val": (416, 416), + "test": (416, 416), + "unlabeled": (992, 736), + } + else: + base_input_size = None + + mock_input_manager_cls = mocker.patch.object(configurer, "InputSizeManager") + mock_input_manager = mock_input_manager_cls.return_value + mock_input_manager.get_configured_input_size.return_value = None + + # excute + self.configurer.configure_input_size(mock_cfg, InputSizePreset.DEFAULT) + + # check + mock_input_manager_cls.assert_called_once_with(mock_cfg, base_input_size) + + @e2e_pytest_unit + @pytest.mark.parametrize("optimizer_hook", ["OptimizerHook", "SAMOptimizerHook", "DummyOptimizerHook"]) + def test_configure_fp16_cpu(self, device_availability_func, optimizer_hook): + for func in device_availability_func.values(): + func.return_value = False + + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.fp16 = {} + model_cfg.optimizer_config.type = optimizer_hook + self.configurer.configure_fp16(model_cfg) + assert model_cfg.optimizer_config.type == optimizer_hook + + @e2e_pytest_unit + @pytest.mark.parametrize("optimizer_hook", ["OptimizerHook", "SAMOptimizerHook", "DummyOptimizerHook"]) + def test_configure_fp16_cuda(self, device_availability_func, optimizer_hook): + for key, func in device_availability_func.items(): + if key == "cuda": + func.return_value = True + else: + func.return_value = False + + if "Dummy" in optimizer_hook: + expected_optimizer_hook = optimizer_hook + else: + expected_optimizer_hook = f"Fp16{optimizer_hook}" + + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.fp16 = {} + model_cfg.optimizer_config.type = optimizer_hook + self.configurer.configure_fp16(model_cfg) + assert model_cfg.optimizer_config.type == expected_optimizer_hook + + @e2e_pytest_unit + def test_configure_model(self): + ir_options = {"ir_model_path": {"ir_weight_path": "", "ir_weight_init": ""}} + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + self.model_cfg.merge_from_dict(data_pipeline_cfg) + self.model_cfg.merge_from_dict(self.data_cfg) + self.configurer.configure_model(self.model_cfg, [], self.det_labels, ir_options, train_dataset=self.det_dataset) + assert len(self.configurer.model_classes) == 3 + + @e2e_pytest_unit + def test_configure_model_without_model(self): + ir_options = {"ir_model_path": {"ir_weight_path": "", "ir_weight_init": ""}} + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + self.model_cfg.merge_from_dict(data_pipeline_cfg) + self.model_cfg.merge_from_dict(self.data_cfg) + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.pop("model") + with pytest.raises(AttributeError): + self.configurer.configure_model(model_cfg, [], self.det_labels, ir_options, train_dataset=self.det_dataset) + + @e2e_pytest_unit + def test_configure_task(self, mocker): + ssd_dir = os.path.join("src/otx/algorithms/detection/configs/detection", "mobilenetv2_ssd") + ssd_cfg = OTXConfig.fromfile(os.path.join(ssd_dir, "model.py")) + data_pipeline_cfg = OTXConfig.fromfile(os.path.join(ssd_dir, "data_pipeline.py")) + ssd_cfg.task_adapt = {"type": "default_task_adapt", "op": "REPLACE", "use_adaptive_anchor": True} + model_cfg = copy.deepcopy(ssd_cfg) + model_cfg.merge_from_dict(data_pipeline_cfg) + self.configurer.configure_task(model_cfg, train_dataset=self.det_dataset) + assert model_cfg.model.bbox_head.anchor_generator != ssd_cfg.model.bbox_head.anchor_generator + + model_cfg = copy.deepcopy(self.model_cfg) + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + model_cfg.merge_from_dict(data_pipeline_cfg) + model_cfg.task_adapt = {"type": "default_task_adapt", "op": "REPLACE", "use_adaptive_anchor": True} + model_cfg.model.bbox_head.type = "ATSSHead" + self.configurer.configure_task(model_cfg, train_dataset=self.det_dataset) + + model_cfg.model.bbox_head.type = "VFNetHead" + self.configurer.configure_task(model_cfg, train_dataset=self.det_dataset) + + model_cfg.model.bbox_head.type = "YOLOXHead" + model_cfg.data.train.type = "MultiImageMixDataset" + self.configurer.configure_task(model_cfg, train_dataset=self.det_dataset) + + def mock_configure_classes(*args, **kwargs): + return True + + mocker.patch.object(DetectionConfigurer, "configure_classes") + self.configurer.model_classes = [] + self.configurer.data_classes = ["red", "green"] + self.configurer.configure_classes = mock_configure_classes + self.configurer.configure_task(model_cfg, train_dataset=self.det_dataset) + + @e2e_pytest_unit + def test_configure_regularization(self): + configure_cfg = copy.deepcopy(self.model_cfg) + configure_cfg.model.l2sp_weight = 1.0 + self.configurer.configure_regularization(configure_cfg) + assert "l2sp_ckpt" in configure_cfg.model + assert configure_cfg.optimizer.weight_decay == 0.0 + + @e2e_pytest_unit + def test_configure_hooks(self): + self.configurer.override_configs = {"custom_hooks": [{"type": "LazyEarlyStoppingHook", "patience": 6}]} + self.configurer.time_monitor = [] + self.configurer.configure_hooks(self.model_cfg) + assert self.model_cfg.custom_hooks[0]["patience"] == 6 + assert self.model_cfg.custom_hooks[-2]["type"] == "CancelInterfaceHook" + assert self.model_cfg.custom_hooks[-1]["type"] == "OTXProgressHook" + assert self.model_cfg.log_config.hooks[-1]["type"] == "OTXLoggerHook" + + @e2e_pytest_unit + def test_configure_compat_cfg(self): + model_cfg = copy.deepcopy(self.model_cfg) + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + model_cfg.merge_from_dict(data_pipeline_cfg) + model_cfg.data.train_dataloader = {} + model_cfg.data.val_dataloader = {} + model_cfg.data.test_dataloader = {} + self.configurer.configure_compat_cfg(model_cfg) + + @e2e_pytest_unit + def test_get_subset_data_cfg(self): + config = copy.deepcopy(self.model_cfg) + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + config.merge_from_dict(data_pipeline_cfg) + config.data.train.dataset = ConfigDict({"dataset": [1, 2, 3]}) + assert [1, 2, 3] == self.configurer.get_subset_data_cfg(config, "train") + + +class TestIncrDetectionConfigurer: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.configurer = IncrDetectionConfigurer( + "segmentation", + True, + False, + {}, + None, + None, + None, + ) + self.model_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "model.py")) + self.data_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "data_pipeline.py")) + self.det_dataset, self.det_labels = generate_det_dataset(TaskType.DETECTION, 100) + + def test_configure_task(self, mocker): + mocker.patch.object(DetectionConfigurer, "configure_task") + self.model_cfg.task_adapt = {} + self.configurer.task_adapt_type = "default_task_adapt" + self.configurer.configure_task(self.model_cfg, train_dataset=self.det_dataset) + assert self.model_cfg.custom_hooks[2].type == "TaskAdaptHook" + assert self.model_cfg.custom_hooks[2].sampler_flag is False + + +class TestSemiSLDetectionConfigurer: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.configurer = SemiSLDetectionConfigurer( + "segmentation", + True, + False, + {}, + None, + None, + None, + ) + self.model_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "semisl", "model.py")) + self.data_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "semisl", "data_pipeline.py")) + self.model_cfg.merge_from_dict(self.data_cfg) + self.det_dataset, self.det_labels = generate_det_dataset(TaskType.DETECTION, 100) + + @e2e_pytest_unit + def test_configure_data_pipeline(self, mocker): + mocker.patch("otx.algorithms.common.adapters.mmcv.semisl_mixin.build_dataset", return_value=True) + mocker.patch("otx.algorithms.common.adapters.mmcv.semisl_mixin.build_dataloader", return_value=True) + mocker.patch.object(DetectionConfigurer, "configure_input_size", return_value=True) + + data_cfg = OTXConfig( + { + "data": { + "train": {"otx_dataset": [], "labels": []}, + "val": {"otx_dataset": [], "labels": []}, + "test": {"otx_dataset": [], "labels": []}, + "unlabeled": {"otx_dataset": self.det_dataset, "labels": []}, + } + } + ) + self.model_cfg.merge_from_dict(data_cfg) + self.model_cfg.model_task = "detection" + self.model_cfg.distributed = False + self.configurer.configure_data_pipeline(self.model_cfg, InputSizePreset.DEFAULT, "") + assert self.model_cfg.custom_hooks[-1]["type"] == "ComposedDataLoadersHook" diff --git a/tests/unit/algorithms/detection/adapters/mmdet/test_task.py b/tests/unit/algorithms/detection/adapters/mmdet/test_task.py new file mode 100644 index 00000000000..b4dffa9127a --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/test_task.py @@ -0,0 +1,557 @@ +"""Unit Test for otx.algorithms.detection.adapters.mmdet.task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +from contextlib import nullcontext +from typing import Any, Dict + +import numpy as np +from otx.algorithms.common.utils.utils import is_xpu_available +import pytest +import torch +from torch import nn + +from otx.algorithms.common.adapters.mmcv.utils import config_utils +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.algorithms.detection.adapters.mmdet.task import MMDetectionTask +from otx.algorithms.detection.adapters.mmdet.models.detectors.custom_atss_detector import CustomATSS +from otx.algorithms.detection.configs.base import DetectionConfig +from otx.api.configuration.helper import create +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.explain_parameters import ExplainParameters +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.model import ( + ModelConfiguration, + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, +) +from otx.api.entities.model_template import parse_model_template, TaskType +from otx.api.entities.resultset import ResultSetEntity +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_DET_TEMPLATE_DIR, + DEFAULT_ISEG_TEMPLATE_DIR, + init_environment, + generate_det_dataset, +) + +import pycocotools.mask as mask_util + + +class MockModule(nn.Module): + """Mock class for nn.Module.""" + + def forward(self, inputs: Any): + return inputs + + +class MockModel(nn.Module): + """Mock class for pytorch model.""" + + def __init__(self, task_type): + super().__init__() + self.module = MockModule() + self.module.backbone = MockModule() + self.backbone = MockModule() + self.task_type = task_type + + def forward(self, *args, **kwargs): + forward_hooks = list(self.module.backbone._forward_hooks.values()) + for hook in forward_hooks: + hook(1, 2, 3) + return [[np.array([[0, 0, 1, 1, 0.1]]), np.array([[0, 0, 1, 1, 0.2]]), np.array([[0, 0, 1, 1, 0.7]])]] + + @staticmethod + def named_parameters(): + return {"name": torch.Tensor([0.5])}.items() + + +class MockDataset(DatasetEntity): + """Mock class for mm_dataset.""" + + def __init__(self, dataset: DatasetEntity, task_type: str): + self.otx_dataset = dataset + self.task_type = task_type + self.CLASSES = ["1", "2", "3"] + + def __len__(self): + return len(self.otx_dataset) + + def evaluate(self, prediction, *args, **kwargs): + if self.task_type == "det": + return {"mAP": 1.0} + else: + return {"mAP": 1.0} + + +class MockDataLoader: + """Mock class for data loader.""" + + def __init__(self, dataset: DatasetEntity): + self.otx_dataset = dataset + self.iter = iter(self.otx_dataset) + + def __len__(self) -> int: + return len(self.otx_dataset) + + def __next__(self) -> Dict[str, DatasetItemEntity]: + return {"imgs": next(self.iter)} + + def __iter__(self): + return self + + +class MockExporter: + """Mock class for Exporter.""" + + def __init__(self, task): + self._output_path = task._output_path + + def run(self, cfg, *args, **kwargs): + cfg.model.bbox_head.num_classes == 3 + with open(os.path.join(self._output_path, "openvino.bin"), "wb") as f: + f.write(np.ndarray([0])) + with open(os.path.join(self._output_path, "openvino.xml"), "wb") as f: + f.write(np.ndarray([0])) + with open(os.path.join(self._output_path, "model.onnx"), "wb") as f: + f.write(np.ndarray([0])) + + return { + "outputs": { + "bin": os.path.join(self._output_path, "openvino.bin"), + "xml": os.path.join(self._output_path, "openvino.xml"), + "onnx": os.path.join(self._output_path, "model.onnx"), + } + } + + +class TestMMDetectionTask: + """Test class for MMDetectionTask. + + Details are explained in each test function. + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + model_template = parse_model_template(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.auto_num_workers = True + task_env = init_environment(hyper_parameters, model_template, task_type=TaskType.DETECTION) + + self.det_task = MMDetectionTask(task_env) + + self.det_dataset, self.det_labels = generate_det_dataset(TaskType.DETECTION, 100) + self.det_label_schema = LabelSchemaEntity() + det_label_group = LabelGroup( + name="labels", + labels=self.det_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + self.det_label_schema.add_group(det_label_group) + + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + task_env = init_environment(hyper_parameters, model_template, task_type=TaskType.INSTANCE_SEGMENTATION) + + self.iseg_task = MMDetectionTask(task_env) + + self.iseg_dataset, self.iseg_labels = generate_det_dataset(TaskType.INSTANCE_SEGMENTATION, 100) + self.iseg_label_schema = LabelSchemaEntity() + iseg_label_group = LabelGroup( + name="labels", + labels=self.iseg_labels, + group_type=LabelGroupType.EXCLUSIVE, + ) + self.iseg_label_schema.add_group(iseg_label_group) + + @e2e_pytest_unit + def test_build_model(self, mocker) -> None: + """Test build_model function.""" + _mock_recipe_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "model.py")) + model = self.det_task.build_model(_mock_recipe_cfg, True) + assert isinstance(model, CustomATSS) + + @e2e_pytest_unit + def test_load_postprocessing(self): + """Test _load_postprocessing function.""" + mock_model_data = { + "config": {"postprocessing": {"use_ellipse_shapes": {"value": True}}}, + "confidence_threshold": 0.75, + } + self.det_task._load_postprocessing(mock_model_data) + assert self.det_task._hyperparams.postprocessing.use_ellipse_shapes == True + assert self.det_task.confidence_threshold == 0.75 + + mock_model_data = { + "config": {"postprocessing": {"use_ellipse_shapes": {"value": False}}}, + "confidence_threshold": 0.75, + } + self.det_task._hyperparams.postprocessing.result_based_confidence_threshold = False + self.det_task._hyperparams.postprocessing.confidence_threshold = 0.45 + self.det_task._load_postprocessing(mock_model_data) + assert self.det_task._hyperparams.postprocessing.use_ellipse_shapes == False + assert self.det_task.confidence_threshold == 0.45 + + @e2e_pytest_unit + def test_train(self, mocker) -> None: + """Test train function.""" + + def _mock_train_detector_det(*args, **kwargs): + with open(os.path.join(self.det_task._output_path, "latest.pth"), "wb") as f: + torch.save({"dummy": torch.randn(1, 3, 3, 3)}, f) + + def _mock_train_detector_iseg(*args, **kwargs): + with open(os.path.join(self.iseg_task._output_path, "latest.pth"), "wb") as f: + torch.save({"dummy": torch.randn(1, 3, 3, 3)}, f) + + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataset", + return_value=MockDataset(self.det_dataset, "det"), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataloader", + return_value=MockDataLoader(self.det_dataset), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.train_detector", + side_effect=_mock_train_detector_det, + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.single_gpu_test", + return_value=[ + np.array([np.array([[0, 0, 1, 1, 0.1]]), np.array([[0, 0, 1, 1, 0.2]]), np.array([[0, 0, 1, 1, 0.7]])]) + ] + * 100, + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + # mock for testing num_workers + num_cpu = 20 + mock_multiprocessing = mocker.patch.object(config_utils, "multiprocessing") + mock_multiprocessing.cpu_count.return_value = num_cpu + num_gpu = 5 + mock_torch = mocker.patch.object(config_utils, "torch") + mock_torch.cuda.device_count.return_value = num_gpu + if is_xpu_available(): + mock_devcnt = mocker.patch.object(config_utils, "get_adaptive_num_workers") + mock_devcnt.return_value = num_cpu // num_gpu + + _config = ModelConfiguration(DetectionConfig(), self.det_label_schema) + output_model = ModelEntity(self.det_dataset, _config) + self.det_task.train(self.det_dataset, output_model) + output_model.performance == 1.0 + assert ( + self.det_task._config.data.train_dataloader.workers_per_gpu == num_cpu // num_gpu + ) # test adaptive num_workers + + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.train_detector", + side_effect=_mock_train_detector_iseg, + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.single_gpu_test", + return_value=[(np.array([[[0, 0, 1, 1, 1]]]), np.ones((1, 1, 28, 28)))] * 100, + ) + _config = ModelConfiguration(DetectionConfig(), self.iseg_label_schema) + output_model = ModelEntity(self.iseg_dataset, _config) + self.iseg_task.train(self.iseg_dataset, output_model) + output_model.performance == 1.0 + assert ( + self.det_task._config.data.train_dataloader.workers_per_gpu == num_cpu // num_gpu + ) # test adaptive num_workers + + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.single_gpu_test", + return_value=[ + ( + np.array([[[0, 0, 1, 1, 1]]]), + [[mask_util.encode(np.ones((28, 28, 1), dtype=np.uint8, order="F"))[0]]], + ) + ] + * 100, + ) + _config = ModelConfiguration(DetectionConfig(), self.iseg_label_schema) + output_model = ModelEntity(self.iseg_dataset, _config) + self.iseg_task.train(self.iseg_dataset, output_model) + output_model.performance == 1.0 + assert ( + self.det_task._config.data.train_dataloader.workers_per_gpu == num_cpu // num_gpu + ) # test adaptive num_workers + + @e2e_pytest_unit + def test_infer(self, mocker) -> None: + """Test infer function.""" + + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataset", + return_value=MockDataset(self.det_dataset, "det"), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataloader", + return_value=MockDataLoader(self.det_dataset), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.single_gpu_test", + return_value=[ + np.array([np.array([[0, 0, 1, 1, 0.1]]), np.array([[0, 0, 1, 1, 0.2]]), np.array([[0, 0, 1, 1, 0.7]])]) + ] + * 100, + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + inference_parameters = InferenceParameters(is_evaluation=True) + outputs = self.det_task.infer(self.det_dataset, inference_parameters) + for output in outputs: + assert output.get_annotations()[-1].get_labels()[0].probability == 0.7 + + @e2e_pytest_unit + def test_det_evaluate(self) -> None: + """Test evaluate function for detection.""" + + _config = ModelConfiguration(DetectionConfig(), self.det_label_schema) + _model = ModelEntity(self.det_dataset, _config) + resultset = ResultSetEntity(_model, self.det_dataset, self.det_dataset) + self.det_task.evaluate(resultset) + assert resultset.performance.score.value == 1.0 + + @e2e_pytest_unit + def test_det_evaluate_with_empty_annotations(self) -> None: + """Test evaluate function for detection with empty predictions.""" + + _config = ModelConfiguration(DetectionConfig(), self.det_label_schema) + _model = ModelEntity(self.det_dataset, _config) + resultset = ResultSetEntity(_model, self.det_dataset, self.det_dataset.with_empty_annotations()) + self.det_task.evaluate(resultset) + assert resultset.performance.score.value == 0.0 + + @e2e_pytest_unit + def test_iseg_evaluate(self) -> None: + """Test evaluate function for instance segmentation.""" + + _config = ModelConfiguration(DetectionConfig(), self.iseg_label_schema) + _model = ModelEntity(self.iseg_dataset, _config) + resultset = ResultSetEntity(_model, self.iseg_dataset, self.iseg_dataset) + self.iseg_task.evaluate(resultset) + assert resultset.performance.score.value == 1.0 + + @pytest.mark.parametrize("precision", [ModelPrecision.FP16, ModelPrecision.FP32]) + @e2e_pytest_unit + def test_export(self, mocker, precision: ModelPrecision, export_type: ExportType = ExportType.OPENVINO) -> None: + """Test export function. + + + 1. Create model entity + 2. Run export function + 3. Check output model attributes + """ + _config = ModelConfiguration(DetectionConfig(), self.det_label_schema) + _model = ModelEntity(self.det_dataset, _config) + + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.DetectionExporter", + return_value=MockExporter(self.det_task), + ) + mocker.patch( + "otx.algorithms.detection.task.embed_ir_model_data", + return_value=True, + ) + + self.det_task.export(export_type, _model, precision, False) + + assert _model.model_format == ModelFormat.ONNX if export_type == ExportType.ONNX else ModelFormat.OPENVINO + assert _model.precision[0] == precision + assert _model.precision == self.det_task._precision + + if export_type == ExportType.OPENVINO: + assert _model.get_data("openvino.bin") is not None + assert _model.get_data("openvino.xml") is not None + assert _model.optimization_type == ModelOptimizationType.MO + else: + assert _model.get_data("model.onnx") is not None + assert _model.optimization_type == ModelOptimizationType.ONNX + + assert _model.get_data("confidence_threshold") is not None + assert _model.optimization_methods == self.det_task._optimization_methods + assert _model.get_data("label_schema.json") is not None + + @e2e_pytest_unit + def test_export_onnx(self, mocker) -> None: + self.test_export(mocker, ModelPrecision.FP32, ExportType.ONNX) + + @e2e_pytest_unit + def test_explain(self, mocker): + """Test explain function.""" + + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataset", + return_value=MockDataset(self.det_dataset, "det"), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataloader", + return_value=MockDataLoader(self.det_dataset), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_data_parallel", + return_value=MockModel(TaskType.DETECTION), + ) + + explain_parameters = ExplainParameters( + explainer="ClassWiseSaliencyMap", + process_saliency_maps=False, + explain_predicted_classes=True, + ) + outputs = self.det_task.explain(self.det_dataset, explain_parameters) + + @e2e_pytest_unit + def test_anchor_clustering(self, mocker): + + ssd_dir = os.path.join("src/otx/algorithms/detection/configs/detection", "mobilenetv2_ssd") + ssd_cfg = OTXConfig.fromfile(os.path.join(ssd_dir, "model.py")) + model_template = parse_model_template(os.path.join(ssd_dir, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.learning_parameters.auto_num_workers = True + task_env = init_environment(hyper_parameters, model_template, task_type=TaskType.DETECTION) + + det_task = MMDetectionTask(task_env) + + def _mock_train_detector_det(*args, **kwargs): + with open(os.path.join(self.det_task._output_path, "latest.pth"), "wb") as f: + torch.save({"dummy": torch.randn(1, 3, 3, 3)}, f) + + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataset", + return_value=MockDataset(self.det_dataset, "det"), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataloader", + return_value=MockDataLoader(self.det_dataset), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.train_detector", + side_effect=_mock_train_detector_det, + ) + + det_task._train_model(self.det_dataset) + assert ssd_cfg.model.bbox_head.anchor_generator != det_task.config.model.bbox_head.anchor_generator + + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.single_gpu_test", + return_value=[ + np.array([np.array([[0, 0, 1, 1, 0.1]]), np.array([[0, 0, 1, 1, 0.2]]), np.array([[0, 0, 1, 1, 0.7]])]) + ] + * 100, + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.FeatureVectorHook", + return_value=nullcontext(), + ) + inference_parameters = InferenceParameters(is_evaluation=True) + det_task._infer_model(self.det_dataset, inference_parameters) + assert ssd_cfg.model.bbox_head.anchor_generator != det_task.config.model.bbox_head.anchor_generator + + @e2e_pytest_unit + def test_geti_scenario(self, mocker): + """Test Geti scenario. + + Train -> Eval -> Export + """ + + # TRAIN + def _mock_train_detector_det(*args, **kwargs): + with open(os.path.join(self.det_task._output_path, "latest.pth"), "wb") as f: + torch.save({"dummy": torch.randn(1, 3, 3, 3)}, f) + + def _mock_train_detector_iseg(*args, **kwargs): + with open(os.path.join(self.iseg_task._output_path, "latest.pth"), "wb") as f: + torch.save({"dummy": torch.randn(1, 3, 3, 3)}, f) + + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataset", + return_value=MockDataset(self.det_dataset, "det"), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataloader", + return_value=MockDataLoader(self.det_dataset), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.train_detector", + side_effect=_mock_train_detector_det, + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.single_gpu_test", + return_value=[ + np.array([np.array([[0, 0, 1, 1, 0.1]]), np.array([[0, 0, 1, 1, 0.2]]), np.array([[0, 0, 1, 1, 0.7]])]) + ] + * 100, + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + # mock for testing num_workers + num_cpu = 20 + mock_multiprocessing = mocker.patch.object(config_utils, "multiprocessing") + mock_multiprocessing.cpu_count.return_value = num_cpu + num_gpu = 5 + mock_torch = mocker.patch.object(config_utils, "torch") + mock_torch.cuda.device_count.return_value = num_gpu + if is_xpu_available(): + mock_devcnt = mocker.patch.object(config_utils, "get_adaptive_num_workers") + mock_devcnt.return_value = 1 + + _config = ModelConfiguration(DetectionConfig(), self.det_label_schema) + output_model = ModelEntity(self.det_dataset, _config) + self.det_task.train(self.det_dataset, output_model) + + # INFER + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataset", + return_value=MockDataset(self.det_dataset, "det"), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.build_dataloader", + return_value=MockDataLoader(self.det_dataset), + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.single_gpu_test", + return_value=[ + np.array([np.array([[0, 0, 1, 1, 0.1]]), np.array([[0, 0, 1, 1, 0.2]]), np.array([[0, 0, 1, 1, 0.7]])]) + ] + * 100, + ) + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.FeatureVectorHook", + return_value=nullcontext(), + ) + + inference_parameters = InferenceParameters(is_evaluation=True) + outputs = self.det_task.infer(self.det_dataset, inference_parameters) + + # EXPORT + mocker.patch( + "otx.algorithms.detection.adapters.mmdet.task.DetectionExporter", + return_value=MockExporter(self.det_task), + ) + mocker.patch( + "otx.algorithms.detection.task.embed_ir_model_data", + return_value=True, + ) + + export_type = ExportType.OPENVINO + precision = ModelPrecision.FP32 + self.det_task.export(export_type, output_model, precision, False) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/utils/__init__.py b/tests/unit/algorithms/detection/adapters/mmdet/utils/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/utils/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/mmdet/utils/test_detection_builder.py b/tests/unit/algorithms/detection/adapters/mmdet/utils/test_detection_builder.py new file mode 100644 index 00000000000..c3d88c4fd01 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/utils/test_detection_builder.py @@ -0,0 +1,24 @@ +"""Unit Test for otx.algorithms.detection.adapters.mmdet.utils.builder.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import pytest + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.algorithms.detection.adapters.mmdet.utils import build_detector +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_DET_MODEL_CONFIG_PATH, + DEFAULT_ISEG_MODEL_CONFIG_PATH, +) + + +@e2e_pytest_unit +@pytest.mark.parametrize("model_cfg", [DEFAULT_DET_MODEL_CONFIG_PATH, DEFAULT_ISEG_MODEL_CONFIG_PATH]) +@pytest.mark.parametrize("cfg_options", [None, OTXConfig()]) +def test_build_detector(model_cfg, cfg_options): + """Test build_detector function.""" + cfg = OTXConfig.fromfile(model_cfg) + model = build_detector(cfg, checkpoint=cfg.load_from, cfg_options=cfg_options) + assert model is not None diff --git a/tests/unit/algorithms/detection/adapters/mmdet/utils/test_detection_config_utils.py b/tests/unit/algorithms/detection/adapters/mmdet/utils/test_detection_config_utils.py new file mode 100644 index 00000000000..873d8a8d9a4 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/utils/test_detection_config_utils.py @@ -0,0 +1,83 @@ +"""Unit Test for otx.algorithms.detection.adapters.mmdet.utils.config_utils.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import pytest +from mmcv.utils import Config + +from pathlib import Path + +from otx.algorithms.detection.adapters.mmdet.utils.config_utils import ( + cluster_anchors, + should_cluster_anchors, +) +from otx.api.entities.model_template import TaskType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import ( + generate_det_dataset, +) + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig, patch_from_hyperparams + +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_DET_MODEL_CONFIG_PATH, + DEFAULT_ISEG_MODEL_CONFIG_PATH, +) +from otx.api.configuration.helper import create +from otx.api.entities.model_template import parse_model_template + + +@e2e_pytest_unit +@pytest.mark.parametrize("reclustering_anchors", [True, False]) +def test_should_cluster_anchors(reclustering_anchors): + """Test should_cluster_anchors function.""" + config = Config( + dict( + model=dict( + bbox_head=dict( + anchor_generator=dict(reclustering_anchors=reclustering_anchors), + ) + ) + ) + ) + out = should_cluster_anchors(config) + assert out == reclustering_anchors + + +@e2e_pytest_unit +@pytest.mark.parametrize("widths, heights", [([1, 10], [1, 10]), ([1, 3, 5, 7, 9], [1, 3, 5, 7, 9])]) +def test_cluster_anchors(widths, heights): + """Test cluster_anchors function.""" + recipe_config = Config( + dict( + model=dict(bbox_head=dict(anchor_generator=dict(widths=[widths], heights=[heights]))), + data=dict( + test=dict( + pipeline=[ + dict(type="LoadImageFromFile"), + dict( + type="MultiScaleFlipAug", + img_scale=(10, 10), + ), + ] + ) + ), + ) + ) + dataset, _ = generate_det_dataset(task_type=TaskType.DETECTION) + cluster_anchors(recipe_config, dataset) + + +@e2e_pytest_unit +@pytest.mark.parametrize("model_cfg", [DEFAULT_DET_MODEL_CONFIG_PATH, DEFAULT_ISEG_MODEL_CONFIG_PATH]) +def test_patch_samples_per_gpu(model_cfg): + """Test samples per gpu function works correctly.""" + cfg = OTXConfig.fromfile(model_cfg) + model_template = parse_model_template(Path(model_cfg).parent / "template.yaml") + hyper_parameters = create(model_template.hyper_parameters.data) + + patch_from_hyperparams(cfg, hyper_parameters) + params = hyper_parameters.learning_parameters + assert cfg.data.val_dataloader.samples_per_gpu == params.inference_batch_size + assert cfg.data.test_dataloader.samples_per_gpu == params.inference_batch_size diff --git a/tests/unit/algorithms/detection/adapters/mmdet/utils/test_exporter.py b/tests/unit/algorithms/detection/adapters/mmdet/utils/test_exporter.py new file mode 100644 index 00000000000..340fb116042 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/utils/test_exporter.py @@ -0,0 +1,51 @@ +import os + +import pytest + +from otx.algorithms.common.adapters.mmcv.tasks.exporter import Exporter +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.algorithms.common.adapters.mmdeploy.apis import NaiveExporter +from otx.algorithms.detection.adapters.mmdet.utils.builder import build_detector +from otx.algorithms.detection.adapters.mmdet.utils.exporter import DetectionExporter +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_DET_RECIPE_CONFIG_PATH, + DEFAULT_DET_TEMPLATE_DIR, + DEFAULT_ISEG_RECIPE_CONFIG_PATH, + DEFAULT_ISEG_TEMPLATE_DIR, +) + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "recipe_cfg, template_dir", + [ + (DEFAULT_DET_RECIPE_CONFIG_PATH, DEFAULT_DET_TEMPLATE_DIR), + (DEFAULT_ISEG_RECIPE_CONFIG_PATH, DEFAULT_ISEG_TEMPLATE_DIR), + ], +) +def test_run(recipe_cfg, template_dir, mocker): + exporter = DetectionExporter() + model_cfg = OTXConfig.fromfile(os.path.join(template_dir, "model.py")) + model_cfg.work_dir = "/tmp/" + args = {"precision": "FP32", "model_builder": build_detector} + mocker.patch.object(Exporter, "run", return_value=True) + returned_value = exporter.run(model_cfg) + assert "model_builder" in args + assert returned_value is True + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "recipe_cfg, template_dir", + [ + (DEFAULT_DET_RECIPE_CONFIG_PATH, DEFAULT_DET_TEMPLATE_DIR), + (DEFAULT_ISEG_RECIPE_CONFIG_PATH, DEFAULT_ISEG_TEMPLATE_DIR), + ], +) +def test_naive_export(recipe_cfg, template_dir, mocker): + exporter = DetectionExporter() + data_cfg = OTXConfig.fromfile(os.path.join(template_dir, "data_pipeline.py")) + mock_export_ov = mocker.patch.object(NaiveExporter, "export2backend") + exporter.naive_export("", build_detector, "FP32", "OPENVINO", data_cfg) + mock_export_ov.assert_called_once() diff --git a/tests/unit/algorithms/detection/adapters/openvino/__init__.py b/tests/unit/algorithms/detection/adapters/openvino/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/openvino/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/openvino/model_wrappers/__init__.py b/tests/unit/algorithms/detection/adapters/openvino/model_wrappers/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/openvino/model_wrappers/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/adapters/openvino/test_task.py b/tests/unit/algorithms/detection/adapters/openvino/test_task.py new file mode 100644 index 00000000000..7ad1ab5d788 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/openvino/test_task.py @@ -0,0 +1,244 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os +import pathlib + +import numpy as np +import pytest +from openvino.model_api.models import Model + +from otx.algorithms.detection.adapters.openvino.task import ( + OpenVINODetectionInferencer, + OpenVINODetectionTask, + OpenVINOMaskInferencer, + OpenVINORotatedRectInferencer, +) +from otx.algorithms.detection.configs.base import DetectionConfig +from otx.algorithms.detection.utils import generate_label_schema +from otx.api.configuration.helper import create +from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import LabelEntity +from otx.api.entities.metrics import Performance, ScoreMetric +from otx.api.entities.model_template import ( + TaskType, + parse_model_template, + task_type_to_label_domain, +) +from otx.api.entities.resultset import ResultSetEntity +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import DetectionBoxToAnnotationConverter +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_DET_TEMPLATE_DIR, + DEFAULT_ISEG_TEMPLATE_DIR, + generate_det_dataset, + init_environment, +) + + +class TestOpenVINODetectionInferencer: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + classes = ("rectangle", "ellipse", "triangle") + self.ov_inferencer = dict() + task_type = TaskType.DETECTION + model_template = parse_model_template(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + params = DetectionConfig(header=hyper_parameters.header) + label_schema = generate_label_schema(classes, task_type_to_label_domain(task_type)) + mocker.patch("otx.algorithms.detection.adapters.openvino.task.OpenvinoAdapter") + mocked_model = mocker.patch.object(Model, "create_model") + adapter_mock = mocker.Mock(set_callback=mocker.Mock(return_value=None)) + mocked_model.return_value = mocker.MagicMock(spec=Model, inference_adapter=adapter_mock) + self.ov_inferencer = OpenVINODetectionInferencer(params, label_schema, "") + self.fake_input = np.full((5, 1), 0.1) + + @e2e_pytest_unit + def test_pre_process(self): + """Test pre_process method in OpenVINODetectionInferencer.""" + self.ov_inferencer.model.preprocess.return_value = {"foo": "bar"} + returned_value = self.ov_inferencer.pre_process(self.fake_input) + assert returned_value == {"foo": "bar"} + + @e2e_pytest_unit + def test_predict(self, mocker): + """Test predict method in OpenVINODetectionInferencer.""" + fake_output = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=[]) + mock_pre_process = mocker.patch.object(OpenVINODetectionInferencer, "pre_process", return_value=("", "")) + mock_forward = mocker.patch.object(OpenVINODetectionInferencer, "forward") + mock_converter = mocker.patch.object( + self.ov_inferencer.converter, "convert_to_annotation", return_value=fake_output + ) + returned_value, _ = self.ov_inferencer.predict(self.fake_input) + + mock_pre_process.assert_called_once() + mock_forward.assert_called_once() + mock_converter.assert_called_once() + assert returned_value == fake_output + + @e2e_pytest_unit + def test_forward(self): + """Test forward method in OpenVINODetectionInferencer.""" + fake_output = {"pred": np.full((5, 1), 0.9)} + self.ov_inferencer.model.infer_sync.return_value = fake_output + returned_value = self.ov_inferencer.forward({"image": self.fake_input}) + assert returned_value == fake_output + + +class TestOpenVINOMaskInferencer: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + classes = ("rectangle", "ellipse", "triangle") + self.ov_inferencer = dict() + task_type = TaskType.INSTANCE_SEGMENTATION + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + params = DetectionConfig(header=hyper_parameters.header) + label_schema = generate_label_schema(classes, task_type_to_label_domain(task_type)) + mocker.patch("otx.algorithms.detection.adapters.openvino.task.OpenvinoAdapter") + mocked_model = mocker.patch.object(Model, "create_model") + adapter_mock = mocker.Mock(set_callback=mocker.Mock(return_value=None)) + mocked_model.return_value = mocker.MagicMock(spec=Model, inference_adapter=adapter_mock) + self.ov_inferencer = OpenVINOMaskInferencer(params, label_schema, "") + self.fake_input = np.full((5, 1), 0.1) + + @e2e_pytest_unit + def test_pre_process(self): + """Test pre_process method in OpenVINOMaskInferencer.""" + self.ov_inferencer.model.preprocess.return_value = {"foo": "bar"} + returned_value = self.ov_inferencer.pre_process(self.fake_input) + assert returned_value == {"foo": "bar"} + + +class TestOpenVINORotatedRectInferencer: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + classes = ("rectangle", "ellipse", "triangle") + self.ov_inferencer = dict() + task_type = TaskType.DETECTION + model_template = parse_model_template(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + params = DetectionConfig(header=hyper_parameters.header) + label_schema = generate_label_schema(classes, task_type_to_label_domain(task_type)) + mocker.patch("otx.algorithms.detection.adapters.openvino.task.OpenvinoAdapter") + mocked_model = mocker.patch.object(Model, "create_model") + adapter_mock = mocker.Mock(set_callback=mocker.Mock(return_value=None)) + mocked_model.return_value = mocker.MagicMock(spec=Model, inference_adapter=adapter_mock) + self.ov_inferencer = OpenVINORotatedRectInferencer(params, label_schema, "") + self.fake_input = np.full((5, 1), 0.1) + + @e2e_pytest_unit + def test_pre_process(self): + """Test pre_process method in RotatedRectInferencer.""" + self.ov_inferencer.model.preprocess.return_value = None, {"foo": "bar"} + returned_value = self.ov_inferencer.pre_process(self.fake_input) + assert returned_value == (None, {"foo": "bar"}) + + +class TestOpenVINODetectionTask: + @pytest.fixture(autouse=True) + def setup(self, mocker, otx_model) -> None: + + classes = ("rectangle", "ellipse", "triangle") + self.ov_inferencer = dict() + task_type = TaskType.DETECTION + + model_template = parse_model_template(os.path.join(DEFAULT_DET_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + label_schema = generate_label_schema(classes, task_type_to_label_domain(task_type)) + task_env = init_environment(hyper_parameters, model_template, task_type=task_type) + params = DetectionConfig(header=hyper_parameters.header) + mocker.patch("otx.algorithms.detection.adapters.openvino.task.OpenvinoAdapter") + mocked_model = mocker.patch.object(Model, "create_model") + adapter_mock = mocker.Mock(set_callback=mocker.Mock(return_value=None)) + mocked_model.return_value = mocker.MagicMock(spec=Model, inference_adapter=adapter_mock) + ov_inferencer = OpenVINODetectionInferencer(params, label_schema, "") + ov_inferencer.model.__model__ = "OTX_SSD" + task_env.model = otx_model + mocker.patch.object(OpenVINODetectionTask, "load_inferencer", return_value=ov_inferencer) + + self.ov_task = OpenVINODetectionTask(task_env) + + @e2e_pytest_unit + def test_infer(self, mocker): + """Test infer method in OpenVINODetectionTask.""" + self.dataset, labels = generate_det_dataset(task_type=TaskType.DETECTION) + fake_ann_scene = self.dataset[0].annotation_scene + mock_predict = mocker.patch.object( + OpenVINODetectionInferencer, "predict", return_value=(fake_ann_scene, (None, None)) + ) + updated_dataset = self.ov_task.infer(self.dataset, InferenceParameters(enable_async_inference=False)) + + mock_predict.assert_called() + for updated in updated_dataset: + assert updated.annotation_scene.contains_any([LabelEntity(name=labels[0].name, domain="DETECTION")]) + + @e2e_pytest_unit + def test_infer_async(self, mocker): + """Test async infer method in OpenVINODetectionTask.""" + self.dataset, labels = generate_det_dataset(task_type=TaskType.DETECTION) + mock_pre_process = mocker.patch.object( + OpenVINODetectionInferencer, "pre_process", return_value=(None, {"foo", "bar"}) + ) + updated_dataset = self.ov_task.infer(self.dataset, InferenceParameters(enable_async_inference=True)) + + mock_pre_process.assert_called() + for updated in updated_dataset: + assert updated.annotation_scene.contains_any([LabelEntity(name=labels[0].name, domain="DETECTION")]) + + @e2e_pytest_unit + def test_evaluate(self, mocker): + """Test evaluate method in OpenVINODetectionTask.""" + result_set = ResultSetEntity( + model=None, + ground_truth_dataset=DatasetEntity(), + prediction_dataset=DatasetEntity(), + ) + fake_metrics = mocker.patch("otx.api.usecases.evaluation.f_measure.FMeasure", autospec=True) + fake_metrics.get_performance.return_value = Performance( + score=ScoreMetric(name="fake", value=0.1), dashboard_metrics="mAP" + ) + mocker.patch.object(MetricsHelper, "compute_f_measure", return_value=fake_metrics) + self.ov_task.evaluate(result_set) + + assert result_set.performance.score.value == 0.1 + + @e2e_pytest_unit + def test_deploy(self, otx_model): + """Test deploy method in OpenVINODetectionTask.""" + output_model = copy.deepcopy(otx_model) + self.ov_task.model.set_data("openvino.bin", b"foo") + self.ov_task.model.set_data("openvino.xml", b"bar") + self.ov_task.deploy(output_model) + + assert output_model.exportable_code is not None + + @e2e_pytest_unit + def test_optimize(self, mocker, otx_model): + """Test optimize method in OpenVINODetectionTask.""" + + def patch_save_model(model, output_xml): + with open(output_xml, "wb") as f: + f.write(b"foo") + bin_path = pathlib.Path(output_xml).parent / pathlib.Path(str(pathlib.Path(output_xml).stem) + ".bin") + with open(bin_path, "wb") as f: + f.write(b"bar") + + dataset, _ = generate_det_dataset(task_type=TaskType.DETECTION) + output_model = copy.deepcopy(otx_model) + self.ov_task.model.set_data("openvino.bin", b"foo") + self.ov_task.model.set_data("openvino.xml", b"bar") + mocker.patch("otx.algorithms.detection.adapters.openvino.task.ov.Core.read_model", autospec=True) + mocker.patch("otx.algorithms.detection.adapters.openvino.task.ov.save_model", new=patch_save_model) + fake_quantize = mocker.patch("otx.algorithms.detection.adapters.openvino.task.nncf.quantize", autospec=True) + self.ov_task.optimize(OptimizationType.POT, dataset=dataset, output_model=output_model) + + fake_quantize.assert_called_once() + assert self.ov_task.model.get_data("openvino.bin") + assert self.ov_task.model.get_data("openvino.xml") diff --git a/tests/unit/algorithms/detection/conftest.py b/tests/unit/algorithms/detection/conftest.py new file mode 100644 index 00000000000..a036f17d07b --- /dev/null +++ b/tests/unit/algorithms/detection/conftest.py @@ -0,0 +1,23 @@ +import pytest + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ModelConfiguration, ModelEntity +from otx.api.entities.model_template import TaskType +from .test_helpers import generate_det_dataset + + +@pytest.fixture +def otx_model(): + model_configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="header", description="description"), + label_schema=LabelSchemaEntity(), + ) + return ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + + +@pytest.fixture(scope="session") +def fxt_det_dataset_entity(number_of_images: int = 8) -> DatasetEntity: + dataset, _ = generate_det_dataset(TaskType.DETECTION, number_of_images) + return dataset diff --git a/tests/unit/algorithms/detection/test_helpers.py b/tests/unit/algorithms/detection/test_helpers.py new file mode 100644 index 00000000000..d93d5531d68 --- /dev/null +++ b/tests/unit/algorithms/detection/test_helpers.py @@ -0,0 +1,150 @@ +"""Collection of helper functions for unit tests of otx.algorithms.detection.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import json +import os +from typing import Any, Dict, List + +import numpy as np + +from otx.algorithms.detection.utils import generate_label_schema +from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.dataset_item import DatasetItemEntityWithID +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.model_template import TaskType, task_type_to_label_domain +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.utils.shape_factory import ShapeFactory +from tests.test_helpers import generate_random_annotated_image + +DEFAULT_DET_MODEL_CONFIG_PATH = "src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/model.py" +DEFAULT_ISEG_MODEL_CONFIG_PATH = ( + "src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/model.py" +) + +DEFAULT_DET_TEMPLATE_DIR = os.path.join("src/otx/algorithms/detection/configs/detection", "mobilenetv2_atss") +DEFAULT_ISEG_TEMPLATE_DIR = os.path.join( + "src/otx/algorithms/detection/configs/instance_segmentation", "efficientnetb2b_maskrcnn" +) +DEFAULT_DET_RECIPE_CONFIG_PATH = "src/otx/recipes/stages/detection/incremental.py" +DEFAULT_ISEG_RECIPE_CONFIG_PATH = "src/otx/recipes/stages/instance_segmentation/incremental.py" + + +class MockImage(Image): + """Mock class for Image entity.""" + + @property + def numpy(self) -> np.ndarray: + """Returns empty numpy array""" + + return np.ndarray((256, 256)) + + +class MockPipeline: + """Mock class for data pipeline. + + It returns its inputs. + """ + + def __call__(self, results: Dict[str, Any]) -> Dict[str, Any]: + return results + + +def init_environment(params, model_template, task_type=TaskType.DETECTION): + classes = ("rectangle", "ellipse", "triangle") + label_schema = generate_label_schema(classes, task_type_to_label_domain(task_type)) + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=label_schema, + model_template=model_template, + ) + return environment + + +def generate_det_dataset(task_type, number_of_images=1, image_width=640, image_height=480): + classes = ("rectangle", "ellipse", "triangle") + label_schema = generate_label_schema(classes, task_type_to_label_domain(task_type)) + + items = [] + for idx in range(number_of_images): + if idx < 30: + subset = Subset.VALIDATION + else: + subset = Subset.TRAINING + image_numpy, annos = generate_random_annotated_image( + image_width=image_width, + image_height=image_height, + labels=label_schema.get_labels(False), + ) + # Convert shapes according to task + for anno in annos: + if task_type == TaskType.DETECTION: + anno.shape = ShapeFactory.shape_as_rectangle(anno.shape) + elif task_type == TaskType.INSTANCE_SEGMENTATION: + anno.shape = ShapeFactory.shape_as_polygon(anno.shape) + image = Image(data=image_numpy) + annotation_scene = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=annos) + items.append(DatasetItemEntityWithID(media=image, annotation_scene=annotation_scene, subset=subset)) + dataset = DatasetEntity(items) + return dataset, dataset.get_labels() + + +def generate_labels(length: int, domain: Domain) -> List[LabelEntity]: + """Generate list of LabelEntity given length and domain.""" + + output: List[LabelEntity] = [] + for i in range(length): + output.append(LabelEntity(name=f"{i + 1}", domain=domain, id=ID(i + 1))) + return output + + +def create_dummy_coco_json(json_name): + image = { + "id": 0, + "width": 640, + "height": 640, + "file_name": "fake_name.jpg", + } + + annotation_1 = { + "id": 1, + "image_id": 0, + "category_id": 0, + "area": 400, + "bbox": [50, 60, 20, 20], + "segmentation": [[165.16, 2.58, 344.95, 41.29, 27.5, 363.0, 9.46, 147.1]], + "iscrowd": 0, + } + + annotation_2 = { + "id": 2, + "image_id": 0, + "category_id": 0, + "area": 900, + "bbox": [100, 120, 30, 30], + "segmentation": [[165.16, 2.58, 344.95, 41.29, 27.5, 363.0, 9.46, 147.1]], + "iscrowd": 0, + } + + categories = [ + { + "id": 0, + "name": "car", + "supercategory": "car", + } + ] + + fake_json = { + "images": [image], + "annotations": [annotation_1, annotation_2], + "categories": categories, + } + with open(json_name, "w") as f: + json.dump(fake_json, f) diff --git a/tests/unit/algorithms/detection/test_xai_detection_validity.py b/tests/unit/algorithms/detection/test_xai_detection_validity.py new file mode 100644 index 00000000000..0b38853397e --- /dev/null +++ b/tests/unit/algorithms/detection/test_xai_detection_validity.py @@ -0,0 +1,129 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +import numpy as np +import pytest +import torch +from mmdet.models import build_detector + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.algorithms.detection.adapters.mmdet.hooks import DetClassProbabilityMapHook +from otx.algorithms.detection.adapters.mmdet.hooks.det_class_probability_map_hook import MaskRCNNRecordingForwardHook +from otx.cli.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_unit + +templates_det = Registry("src/otx/algorithms").filter(task_type="DETECTION").templates +templates_det_ids = [template.model_template_id for template in templates_det] + +templates_two_stage_det = Registry("src/otx/algorithms/detection").filter(task_type="INSTANCE_SEGMENTATION").templates +templates_two_stage_det_ids = [template.model_template_id for template in templates_two_stage_det] + + +class TestExplainMethods: + ref_saliency_shapes = { + "MobileNetV2-ATSS": (2, 13, 13), + "ResNeXt101-ATSS": (2, 13, 13), + "SSD": (81, 13, 13), + "YOLOX-TINY": (80, 26, 26), + "YOLOX-S": (80, 26, 26), + "YOLOX-L": (80, 26, 26), + "YOLOX-X": (80, 26, 26), + } + + ref_saliency_vals_det = { + "MobileNetV2-ATSS": np.array([34, 67, 148, 132, 172, 147, 146, 155, 167, 159], dtype=np.uint8), + "ResNeXt101-ATSS": np.array([52, 75, 68, 76, 89, 94, 101, 111, 125, 123], dtype=np.uint8), + "YOLOX-TINY": np.array([177, 94, 147, 147, 161, 162, 164, 164, 163, 166], dtype=np.uint8), + "YOLOX-S": np.array([158, 170, 180, 158, 152, 148, 153, 153, 148, 145], dtype=np.uint8), + "YOLOX-L": np.array([255, 80, 97, 88, 73, 71, 72, 76, 75, 76], dtype=np.uint8), + "YOLOX-X": np.array([185, 218, 189, 103, 83, 70, 62, 66, 66, 67], dtype=np.uint8), + "SSD": np.array([255, 178, 212, 90, 93, 79, 79, 80, 87, 83], dtype=np.uint8), + } + + ref_saliency_vals_det_wo_postprocess = { + "MobileNetV2-ATSS": -0.014513552, + "ResNeXt101-ATSS": -0.055565584, + "YOLOX-TINY": 0.04948914, + "YOLOX-S": 0.011557617, + "YOLOX-L": 0.020231, + "YOLOX-X": 0.0043506604, + "SSD": 0.6629989, + } + + @staticmethod + def _get_model(template): + torch.manual_seed(0) + + base_dir = os.path.abspath(os.path.dirname(template.model_template_path)) + cfg_path = os.path.join(base_dir, "model.py") + cfg = OTXConfig.fromfile(cfg_path) + + model = build_detector(cfg.model) + model = model.eval() + return model + + @staticmethod + def _get_data(): + img = torch.ones(2, 3, 416, 416) - 0.5 + img_metas = [ + { + "img_shape": (416, 416, 3), + "ori_shape": (416, 416, 3), + "scale_factor": np.array([1.1784703, 0.832, 1.1784703, 0.832], dtype=np.float32), + }, + ] * 2 + data = {"img_metas": [img_metas], "img": [img]} + return data + + @e2e_pytest_unit + @pytest.mark.parametrize("template", templates_det, ids=templates_det_ids) + def test_saliency_map_det(self, template): + model = self._get_model(template) + data = self._get_data() + + with DetClassProbabilityMapHook(model) as det_hook: + with torch.no_grad(): + _ = model(return_loss=False, rescale=True, **data) + saliency_maps = det_hook.records + + assert len(saliency_maps) == 2 + assert saliency_maps[0].ndim == 3 + assert saliency_maps[0].shape == self.ref_saliency_shapes[template.name] + # convert to int16 in case of negative value difference + actual_sal_vals = saliency_maps[0][0][0][:10].astype(np.int16) + ref_sal_vals = self.ref_saliency_vals_det[template.name].astype(np.uint8) + assert np.all(np.abs(actual_sal_vals - ref_sal_vals) <= 1) + + @e2e_pytest_unit + @pytest.mark.parametrize("template", templates_det, ids=templates_det_ids) + def test_saliency_map_det_wo_postprocessing(self, template): + model = self._get_model(template) + data = self._get_data() + + with DetClassProbabilityMapHook(model, normalize=False, use_cls_softmax=False) as det_hook: + with torch.no_grad(): + _ = model(return_loss=False, rescale=True, **data) + saliency_maps = det_hook.records + + assert len(saliency_maps) == 2 + assert saliency_maps[0].ndim == 3 + assert saliency_maps[0].shape == self.ref_saliency_shapes[template.name] + assert np.abs(saliency_maps[0][0][0][0] - self.ref_saliency_vals_det_wo_postprocess[template.name]) < 1e-4 + + @e2e_pytest_unit + @pytest.mark.parametrize("template", templates_two_stage_det, ids=templates_two_stage_det_ids) + def test_saliency_map_two_stage_det(self, template): + model = self._get_model(template) + data = self._get_data() + + with MaskRCNNRecordingForwardHook(model, input_img_shape=(800, 1344)) as det_hook: + with torch.no_grad(): + _ = model(return_loss=False, rescale=True, **data) + saliency_maps = det_hook.records + + # MaskRCNNRecordingForwardHook generates saliency maps based on predictions. + # Current test does not intend to test a trained model - so no prediction and no saliency maps are available. + assert saliency_maps == [[None] * model.roi_head.mask_head.num_classes] * 2 diff --git a/tests/unit/algorithms/detection/tiling/__init__.py b/tests/unit/algorithms/detection/tiling/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/detection/tiling/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/tiling/test_tiling_detection.py b/tests/unit/algorithms/detection/tiling/test_tiling_detection.py new file mode 100644 index 00000000000..d9c251d0c1b --- /dev/null +++ b/tests/unit/algorithms/detection/tiling/test_tiling_detection.py @@ -0,0 +1,452 @@ +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import os +from typing import List + +import numpy as np +import pytest +import torch +from mmcv import Config, ConfigDict +from mmdet.datasets import build_dataloader, build_dataset +from mmdet.models import DETECTORS +from openvino.model_api.adapters import OpenvinoAdapter, create_core +from torch import nn + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.algorithms.common.adapters.mmdeploy.apis import MMdeployExporter +from otx.algorithms.common.utils.data import get_dataset +from otx.algorithms.detection.adapters.mmdet.task import MMDetectionTask +from otx.algorithms.detection.adapters.mmdet.utils import build_detector, patch_tiling +from otx.api.configuration.helper import create +from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity, DatasetPurpose +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.subset import Subset +from otx.api.usecases.adapters.model_adapter import ModelAdapter +from otx.api.utils.shape_factory import ShapeFactory +from tests.test_helpers import generate_random_annotated_image +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_ISEG_TEMPLATE_DIR, + init_environment, +) +from otx.algorithms.detection.utils.data import adaptive_tile_params + + +@DETECTORS.register_module(force=True) +class MockDetModel(nn.Module): + def __init__(self, backbone, train_cfg=None, test_cfg=None, init_cfg=None): + super().__init__() + self.conv = torch.nn.Conv2d(3, 3, 3) + self.box_dummy = torch.nn.AdaptiveAvgPool2d((1, 5)) + self.label_dummy = torch.nn.AdaptiveAvgPool2d((1)) + self.mask_dummy = torch.nn.AdaptiveAvgPool2d((28, 28)) + + def forward(self, *args, **kwargs): + img = args[0] + x = self.conv(img) + boxes = self.box_dummy(x).mean(1) + labels = self.label_dummy(x).mean(1) + masks = self.mask_dummy(x).mean(1) + return boxes, labels, masks + + +def create_otx_dataset(height: int, width: int, labels: List[str], domain: Domain = Domain.DETECTION): + """Create a random OTX dataset. + + Args: + height (int): The height of the image + width (int): The width of the image + + Returns: + DatasetEntity: OTX dataset entity + List[LabelEntity]: The list of labels + """ + labels = [] + for label in ["rectangle", "ellipse", "triangle"]: + labels.append(LabelEntity(name=label, domain=domain)) + image, anno_list = generate_random_annotated_image(width, height, labels) + image = Image(data=image) + annotation_scene = AnnotationSceneEntity(annotations=anno_list, kind=AnnotationSceneKind.ANNOTATION) + dataset_item = DatasetItemEntity(media=image, annotation_scene=annotation_scene, subset=Subset.TRAINING) + return DatasetEntity([dataset_item]), labels + + +class TestTilingDetection: + """Test the tiling detection algorithm.""" + + @pytest.fixture(autouse=True) + def setUp(self) -> None: + """Setup the test case.""" + self.height = 1024 + self.width = 1024 + self.label_names = ["rectangle", "ellipse", "triangle"] + self.tile_cfg = dict( + tile_size=np.random.randint(low=100, high=500), + overlap_ratio=np.random.uniform(low=0.0, high=0.5), + max_per_img=np.random.randint(low=1, high=10000), + max_annotation=1000, + ) + self.dataloader_cfg = dict(samples_per_gpu=1, workers_per_gpu=1) + self.otx_dataset, self.labels = create_otx_dataset(self.height, self.width, self.label_names) + + img_norm_cfg = dict(mean=[0, 0, 0], std=[255, 255, 255], to_rgb=True) + + self.train_data_cfg = ConfigDict( + dict( + type="ImageTilingDataset", + filter_empty_gt=False, + pipeline=[ + dict(type="Resize", img_scale=(self.height, self.width), keep_ratio=False), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img", "gt_bboxes", "gt_labels"]), + ], + dataset=dict( + type="OTXDetDataset", + pipeline=[ + dict(type="LoadImageFromOTXDataset"), + dict( + type="LoadAnnotationFromOTXDataset", + with_bbox=True, + with_mask=False, + domain="detection", + min_size=-1, + ), + ], + otx_dataset=self.otx_dataset, + labels=self.labels, + ), + **self.tile_cfg + ) + ) + + self.test_data_cfg = ConfigDict( + dict( + type="ImageTilingDataset", + filter_empty_gt=False, + pipeline=[ + dict( + type="MultiScaleFlipAug", + img_scale=(self.height, self.width), + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="Normalize", **img_norm_cfg), + dict(type="ImageToTensor", keys=["img"]), + dict(type="Collect", keys=["img"]), + ], + ) + ], + dataset=dict( + type="OTXDetDataset", + pipeline=[dict(type="LoadImageFromOTXDataset")], + otx_dataset=self.otx_dataset.with_empty_annotations(), + labels=list(self.labels), + ), + test_mode=True, + **self.tile_cfg + ) + ) + + @e2e_pytest_unit + def test_tiling_train_dataloader(self): + """Test that the training dataloader is built correctly for tiling.""" + + dataset = build_dataset(self.train_data_cfg) + train_dataloader = build_dataloader(dataset, **self.dataloader_cfg) + for data in train_dataloader: + assert isinstance(data["img"].data[0], torch.Tensor) + assert isinstance(data["gt_bboxes"].data[0][0], torch.Tensor) + assert isinstance(data["gt_labels"].data[0][0], torch.Tensor) + + @e2e_pytest_unit + def test_tiling_test_dataloader(self): + """Test that the testing dataloader is built correctly for tiling.""" + + dataset = build_dataset(self.test_data_cfg) + stride = int((1 - self.tile_cfg["overlap_ratio"]) * self.tile_cfg["tile_size"]) + num_tile_rows = (self.height + stride - 1) // stride + num_tile_cols = (self.width + stride - 1) // stride + assert len(dataset) == (num_tile_rows * num_tile_cols), "Incorrect number of tiles" + + test_dataloader = build_dataloader(dataset, **self.dataloader_cfg) + for data in test_dataloader: + assert isinstance(data["img"][0], torch.Tensor) + assert "gt_bboxes" not in data + assert "gt_labels" not in data + + @e2e_pytest_unit + def test_tiling_sampling(self): + self.train_data_cfg.sampling_ratio = 0.5 + dataset = build_dataset(self.train_data_cfg) + assert len(dataset) == max(int(len(dataset.tile_dataset.tiles_all) * 0.5), 1) + + self.train_data_cfg.sampling_ratio = 0.01 + dataset = build_dataset(self.train_data_cfg) + assert len(dataset) == max(int(len(dataset.tile_dataset.tiles_all) * 0.01), 1) + + self.test_data_cfg.sampling_ratio = 0.1 + self.test_data_cfg.dataset.otx_dataset[0].subset = Subset.TESTING + dataset = build_dataset(self.test_data_cfg) + assert len(dataset) == len(dataset.tile_dataset.tiles_all) + + @e2e_pytest_unit + def test_inference_merge(self): + """Test that the inference merge works correctly.""" + dataset = build_dataset(self.test_data_cfg) + + # create simulated inference results + results: List[List[np.ndarray]] = [] + for i in range(len(dataset)): + results.append([]) + for _ in range(len(self.labels)): + results[i].append(np.zeros((0, 5), dtype=np.float32)) + + # generate tile predictions + for i in range(len(dataset)): + img_width, img_height = self.tile_cfg["tile_size"], self.tile_cfg["tile_size"] + if i == 0: + # first index belongs is the full image + img_width, img_height = self.width, self.height + + _, anno_list = generate_random_annotated_image(img_width, img_height, self.labels) + for anno in anno_list: + shape = ShapeFactory.shape_as_rectangle(anno.shape) + bbox = np.array([shape.x1, shape.y1, shape.x2, shape.y2], np.float32) + bbox *= np.tile([img_width, img_height], 2) + score_bbox = np.array([*bbox, np.random.rand()], np.float32) + label_idx = self.label_names.index(anno.get_labels()[0].name) + results[i][label_idx] = np.append(results[i][label_idx], [score_bbox], axis=0) + + merged_bbox_results = dataset.merge(results) + assert len(merged_bbox_results) == dataset.num_samples + + @e2e_pytest_unit + def test_merge_feature_vectors(self): + """Test that the merge feature vectors works correctly.""" + dataset = build_dataset(self.test_data_cfg) + + # create simulated vectors results + feature_vectors: List[np.ndarray] = [] + vectors_per_image = 5 + vector_length = 10 + feature_vectors = [np.zeros((vectors_per_image, vector_length), dtype=np.float32) for _ in range(len(dataset))] + + # Test merge_vectors if vectors are to be returned + merged_vectors = dataset.merge_vectors(feature_vectors, dump_vectors=True) + assert len(merged_vectors) == dataset.num_samples + + # Test merge_vectors if merged vectors should be a list of None + merged_vectors = dataset.merge_vectors(feature_vectors, dump_vectors=False) + assert len(merged_vectors) == dataset.num_samples + + @e2e_pytest_unit + def test_merge_saliency_maps(self): + """Test that the inference merge works correctly.""" + dataset = build_dataset(self.test_data_cfg) + + # create simulated maps results + saliency_maps: List[np.ndarray] = [] + num_classes = len(dataset.CLASSES) + feature_map_size = (num_classes, 2, 2) + features_per_image = 5 + saliency_maps = [np.zeros(feature_map_size, dtype=np.float32) for _ in range(len(dataset) * features_per_image)] + + # Test merge_maps if maps are to be processed + merged_maps = dataset.merge_maps(saliency_maps, dump_maps=True) + assert len(merged_maps) == dataset.num_samples + + # Test merge_maps if maps should be a list of None + merged_maps = dataset.merge_maps(saliency_maps, dump_maps=False) + assert len(merged_maps) == dataset.num_samples + + @e2e_pytest_unit + def test_load_tiling_parameters(self, tmp_dir_path): + maskrcnn_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "model.py")) + detector = build_detector(maskrcnn_cfg) + + # Enable tiling and save weights + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.tiling_parameters.enable_tiling = True + task_env = init_environment(hyper_parameters, model_template) + output_model = ModelEntity(self.otx_dataset, task_env.get_model_configuration()) + task = MMDetectionTask(task_env, output_path=str(tmp_dir_path)) + model_ckpt = os.path.join(tmp_dir_path, "maskrcnn.pth") + task._init_task() + torch.save(detector.state_dict(), model_ckpt) + task._model_ckpt = model_ckpt + task.save_model(output_model) + for filename, model_adapter in output_model.model_adapters.items(): + with open(os.path.join(tmp_dir_path, filename), "wb") as write_file: + write_file.write(model_adapter.data) + + # Read tiling parameters from weights + with open(os.path.join(tmp_dir_path, "weights.pth"), "rb") as f: + bin_data = f.read() + model = ModelEntity( + self.otx_dataset, + configuration=task_env.get_model_configuration(), + model_adapters={"weights.pth": ModelAdapter(bin_data)}, + ) + task_env.model = model + task = MMDetectionTask(task_env, output_path=str(tmp_dir_path)) + + @e2e_pytest_unit + def test_patch_tiling_func(self): + """Test that patch_tiling function works correctly.""" + cfg = OTXConfig.fromfile(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "model.py")) + data_pipeline_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "data_pipeline.py")) + cfg.merge_from_dict(data_pipeline_cfg) + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.tiling_parameters.enable_tiling = True + + self.otx_dataset.purpose = DatasetPurpose.TRAINING + patch_tiling(cfg, hyper_parameters, self.otx_dataset) + + self.otx_dataset.purpose = DatasetPurpose.INFERENCE + patch_tiling(cfg, hyper_parameters, self.otx_dataset) + + @e2e_pytest_unit + @pytest.mark.parametrize("scale_factor", [1, 1.5, 2, 3, 4]) + def test_tile_ir_scale_deploy(self, tmp_dir_path, scale_factor): + """Test that the IR scale factor is correctly applied during inference.""" + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.tiling_parameters.enable_tiling = True + hyper_parameters.tiling_parameters.tile_ir_scale_factor = scale_factor + task_env = init_environment(hyper_parameters, model_template) + img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) + task = MMDetectionTask(task_env) + pipeline = [ + dict(type="LoadImageFromFile"), + dict( + type="MultiScaleFlipAug", + img_scale=(512, 512), + flip=False, + transforms=[ + dict(type="Resize", keep_ratio=False), + dict(type="RandomFlip"), + dict(type="Normalize", **img_norm_cfg), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img"]), + ], + ), + ] + config = Config( + dict(model=dict(type="MockDetModel", backbone=dict(init_cfg=None)), data=dict(test=dict(pipeline=pipeline))) + ) + + deploy_cfg = task._init_deploy_cfg(config) + onnx_path = MMdeployExporter.torch2onnx( + tmp_dir_path, + np.zeros((50, 50, 3), dtype=np.float32), + config, + deploy_cfg, + ) + assert isinstance(onnx_path, str) + assert os.path.exists(onnx_path) + + openvino_paths = MMdeployExporter.onnx2openvino( + tmp_dir_path, + onnx_path, + deploy_cfg, + ) + for openvino_path in openvino_paths: + assert os.path.exists(openvino_path) + + task._init_task() + task.configure(True, None, get_dataset(self.otx_dataset, Subset.TRAINING)) + original_width, original_height = task._config.data.test.pipeline[0].img_scale # w, h + + model_adapter = OpenvinoAdapter(create_core(), openvino_paths[0], openvino_paths[1]) + + ir_input_shape = model_adapter.get_input_layers()["image"].shape + _, _, ir_height, ir_width = ir_input_shape + assert ir_height == original_height * scale_factor + assert ir_width == original_width * scale_factor + + @e2e_pytest_unit + def test_max_annotation(self, max_annotation=200): + otx_dataset, labels = create_otx_dataset( + self.height, self.width, self.label_names, Domain.INSTANCE_SEGMENTATION + ) + factor = int(max_annotation / len(otx_dataset[0].annotation_scene.annotations)) + 2 + otx_dataset[0].annotation_scene.annotations = otx_dataset[0].annotation_scene.annotations * factor + dataloader_cfg = dict(samples_per_gpu=1, workers_per_gpu=1) + + tile_cfg = dict( + tile_size=np.random.randint(low=100, high=500), + overlap_ratio=np.random.uniform(low=0.0, high=0.5), + max_per_img=np.random.randint(low=1, high=10000), + max_annotation=max_annotation, + include_full_img=True, + ) + train_data_cfg = ConfigDict( + dict( + type="ImageTilingDataset", + pipeline=[ + dict(type="Resize", img_scale=(self.height, self.width), keep_ratio=False), + dict(type="RandomFlip", flip_ratio=0.5), + dict(type="Pad", size_divisor=32), + dict(type="DefaultFormatBundle"), + dict(type="Collect", keys=["img", "gt_bboxes", "gt_labels", "gt_masks"]), + ], + dataset=dict( + type="OTXDetDataset", + pipeline=[ + dict(type="LoadImageFromOTXDataset"), + dict( + type="LoadAnnotationFromOTXDataset", + with_bbox=True, + with_mask=True, + domain="instance_segmentation", + min_size=-1, + ), + ], + otx_dataset=otx_dataset, + labels=labels, + ), + **tile_cfg + ) + ) + + # original annotation over the limitation + assert len(otx_dataset[0].annotation_scene.annotations) > max_annotation + dataset = build_dataset(train_data_cfg) + train_dataloader = build_dataloader(dataset, **dataloader_cfg) + # check gt annotation is under the limitation + for data in train_dataloader: + assert len(data["gt_bboxes"].data[0][0]) <= max_annotation + assert len(data["gt_labels"].data[0][0]) <= max_annotation + assert len(data["gt_masks"].data[0][0]) <= max_annotation + + @e2e_pytest_unit + def test_adaptive_tile_parameters(self): + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hp = create(model_template.hyper_parameters.data) + + default_tile_size = hp.tiling_parameters.tile_size + default_tile_overlap = hp.tiling_parameters.tile_overlap + default_tile_max_number = hp.tiling_parameters.tile_max_number + + adaptive_tile_params(hp.tiling_parameters, self.otx_dataset) + + # check tile size is changed + assert hp.tiling_parameters.tile_size != default_tile_size + + # check tile overlap is changed + assert hp.tiling_parameters.tile_overlap != default_tile_overlap + + # check max output prediction size is changed + assert hp.tiling_parameters.tile_max_number != default_tile_max_number diff --git a/tests/unit/algorithms/detection/tiling/test_tiling_tile_classifier.py b/tests/unit/algorithms/detection/tiling/test_tiling_tile_classifier.py new file mode 100644 index 00000000000..105603e0b09 --- /dev/null +++ b/tests/unit/algorithms/detection/tiling/test_tiling_tile_classifier.py @@ -0,0 +1,213 @@ +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +import copy +import os +from functools import partial + +import numpy as np +import pytest +import torch +from openvino.model_api.models import ImageModel, Model + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.algorithms.detection.adapters.mmdet.task import MMDetectionTask +from otx.algorithms.detection.adapters.mmdet.utils import build_detector, patch_tiling +from openvino.model_api.models import MaskRCNNModel +from otx.algorithms.detection.adapters.openvino.task import ( + OpenVINODetectionTask, + OpenVINOMaskInferencer, + OpenVINOTileClassifierWrapper, +) +from otx.algorithms.detection.configs.base import DetectionConfig +from otx.algorithms.detection.utils import generate_label_schema +from otx.api.configuration.helper import create +from otx.api.entities.label import LabelEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import ( + TaskType, + parse_model_template, + task_type_to_label_domain, +) +from otx.api.usecases.adapters.model_adapter import ModelAdapter +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import ( + DEFAULT_ISEG_TEMPLATE_DIR, + generate_det_dataset, + init_environment, +) +from openvino.model_api.models.utils import InstanceSegmentationResult + + +class TestTilingTileClassifier: + """Test the tile classifier""" + + @pytest.fixture(autouse=True) + def setUp(self, otx_model) -> None: + """Set up the test + + Args: + otx_model (mocker): Mocked model + """ + classes = ("rectangle", "ellipse", "triangle") + self.ov_inferencer = dict() + task_type = TaskType.INSTANCE_SEGMENTATION + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + self.hyper_parameters = create(model_template.hyper_parameters.data) + self.hyper_parameters.tiling_parameters.enable_tiling = True + self.hyper_parameters.tiling_parameters.enable_tile_classifier = True + self.label_schema = generate_label_schema(classes, task_type_to_label_domain(task_type)) + self.task_env = init_environment(self.hyper_parameters, model_template, task_type=task_type) + self.task_env.model = otx_model + dataset, labels = generate_det_dataset(task_type=TaskType.INSTANCE_SEGMENTATION) + self.dataset = dataset + self.labels = labels + + @e2e_pytest_unit + def test_openvino_sync(self, mocker): + """Test OpenVINO tile classifier + + Args: + mocker (_type_): pytest mocker from fixture + """ + mocker.patch("otx.algorithms.detection.adapters.openvino.task.OpenvinoAdapter") + mocked_model = mocker.patch.object(Model, "create_model") + adapter_mock = mocker.Mock( + set_callback=mocker.Mock(return_value=None), get_rt_info=mocker.Mock(return_value=np.array([1])) + ) + mocker.patch.object(ImageModel, "__init__", return_value=None) + mocker.patch.object(Model, "__init__", return_value=None) + mocked_model.return_value = mocker.MagicMock( + spec=MaskRCNNModel, inference_adapter=adapter_mock, postprocess_semantic_masks=False + ) + params = DetectionConfig(header=self.hyper_parameters.header) + ov_mask_inferencer = OpenVINOMaskInferencer(params, self.label_schema, "") + original_shape = (self.dataset[0].media.width, self.dataset[0].media.height, 3) + ov_mask_inferencer.model.resize_mask = False + ov_mask_inferencer.model.preprocess.return_value = ( + {"foo": "bar"}, + {"baz": "qux", "original_shape": original_shape}, + ) + ov_mask_inferencer.model.postprocess.return_value = ( + np.array([], dtype=np.float32), + np.array([], dtype=np.uint32), + np.zeros((0, 4), dtype=np.float32), + [], + ) + ov_inferencer = OpenVINOTileClassifierWrapper( + ov_mask_inferencer, tile_classifier_model_file="", tile_classifier_weight_file="", mode="sync" + ) + ov_inferencer.model.__model__ = "MaskRCNN" + mock_predict = mocker.patch.object( + ov_inferencer.tiler.tile_classifier_model, "infer_sync", return_value={"tile_prob": 0.5} + ) + mocker.patch.object(ov_inferencer.tiler, "_postprocess_tile", return_value={}) + mocker.patch.object( + ov_inferencer.tiler, + "_merge_results", + return_value=InstanceSegmentationResult( + [], [np.zeros((0, 4), dtype=np.float32)], np.zeros((0, 4), dtype=np.float32) + ), + ) + ov_inferencer.tiler.model.infer_sync.return_value = { + "feature_vector": np.zeros((1, 5), dtype=np.float32), + "saliency_map": np.zeros((1, 1, 2, 2), dtype=np.float32), + } + mocker.patch.object(OpenVINODetectionTask, "load_inferencer", return_value=ov_inferencer) + ov_task = OpenVINODetectionTask(self.task_env) + updated_dataset = ov_task.infer(self.dataset) + + mock_predict.assert_called() + for updated in updated_dataset: + assert updated.annotation_scene.contains_any([LabelEntity(name=self.labels[0].name, domain="DETECTION")]) + + output_model = copy.deepcopy(self.task_env.model) + ov_task.model.set_data("openvino.bin", b"foo") + ov_task.model.set_data("openvino.xml", b"bar") + ov_task.model.set_data("tile_classifier.bin", b"foo") + ov_task.model.set_data("tile_classifier.xml", b"bar") + ov_task.deploy(output_model) + assert output_model.exportable_code is not None + + @e2e_pytest_unit + def test_load_tile_classifier_parameters(self, tmp_dir_path): + """Test loading tile classifier parameters + + Args: + tmp_dir_path (str): Path to temporary directory + """ + maskrcnn_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "model.py")) + detector = build_detector(maskrcnn_cfg) + model_ckpt = os.path.join(tmp_dir_path, "maskrcnn_without_tile_classifier.pth") + torch.save({"state_dict": detector.state_dict()}, model_ckpt) + + # Enable tiling and save weights + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.tiling_parameters.enable_tiling = True + hyper_parameters.tiling_parameters.enable_tile_classifier = True + task_env = init_environment(hyper_parameters, model_template) + output_model = ModelEntity(self.dataset, task_env.get_model_configuration()) + task = MMDetectionTask(task_env, output_path=str(tmp_dir_path)) + task._model_ckpt = model_ckpt + task._init_task() + task.save_model(output_model) + for filename, model_adapter in output_model.model_adapters.items(): + with open(os.path.join(tmp_dir_path, filename), "wb") as write_file: + write_file.write(model_adapter.data) + + # Read tiling parameters from weights + with open(os.path.join(tmp_dir_path, "weights.pth"), "rb") as f: + bin_data = f.read() + model = ModelEntity( + self.dataset, + configuration=task_env.get_model_configuration(), + model_adapters={"weights.pth": ModelAdapter(bin_data)}, + ) + task_env.model = model + with pytest.raises(RuntimeError) as e: + task = MMDetectionTask(task_env, output_path=str(tmp_dir_path)) + assert ( + str(e.value) + == "Tile classifier is enabled but not found in the trained model. Please retrain your model." + ) + + maskrcnn_classifier_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "model.py")) + maskrcnn_classifier_cfg.model.type = "CustomMaskRCNNTileOptimized" + tile_classifier_detector = build_detector(maskrcnn_classifier_cfg) + tile_classifier_ckpt = os.path.join(tmp_dir_path, "maskrcnn_with_tile_classifier.pth") + torch.save({"state_dict": tile_classifier_detector.state_dict()}, tile_classifier_ckpt) + + task_env = init_environment(hyper_parameters, model_template) + output_model = ModelEntity(self.dataset, task_env.get_model_configuration()) + task = MMDetectionTask(task_env, output_path=str(tmp_dir_path)) + task._model_ckpt = tile_classifier_ckpt + task._init_task() + task.save_model(output_model) + for filename, model_adapter in output_model.model_adapters.items(): + with open(os.path.join(tmp_dir_path, filename), "wb") as write_file: + write_file.write(model_adapter.data) + + # Read tiling parameters from weights + with open(os.path.join(tmp_dir_path, "weights.pth"), "rb") as f: + bin_data = f.read() + model = ModelEntity( + self.dataset, + configuration=task_env.get_model_configuration(), + model_adapters={"weights.pth": ModelAdapter(bin_data)}, + ) + task_env.model = model + task = MMDetectionTask(task_env, output_path=str(tmp_dir_path)) + + @e2e_pytest_unit + def test_patch_tiling_func(self): + """Test that patch_tiling function works correctly""" + cfg = OTXConfig.fromfile(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "model.py")) + data_pipeline_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "tile_pipeline.py")) + cfg.merge_from_dict(data_pipeline_cfg) + model_template = parse_model_template(os.path.join(DEFAULT_ISEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + hyper_parameters.tiling_parameters.enable_tiling = True + hyper_parameters.tiling_parameters.enable_tile_classifier = True + patch_tiling(cfg, hyper_parameters, self.dataset) diff --git a/tests/unit/algorithms/detection/utils/__init__.py b/tests/unit/algorithms/detection/utils/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/detection/utils/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/detection/utils/test_detection_data.py b/tests/unit/algorithms/detection/utils/test_detection_data.py new file mode 100644 index 00000000000..2d3bbc56eef --- /dev/null +++ b/tests/unit/algorithms/detection/utils/test_detection_data.py @@ -0,0 +1,86 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os +import tempfile + +import pytest + +from otx.algorithms.detection.utils import generate_label_schema +from otx.algorithms.detection.utils.data import ( + find_label_by_name, + format_list_to_str, + get_anchor_boxes, + get_sizes_from_dataset_entity, + load_dataset_items_coco_format, +) +from otx.api.entities.label import Domain +from otx.api.entities.model_template import TaskType, task_type_to_label_domain +from otx.api.entities.subset import Subset +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import ( + create_dummy_coco_json, + generate_det_dataset, +) + + +@e2e_pytest_unit +@pytest.mark.parametrize("name", ["rectangle", "something"]) +def test_find_label_by_name(name): + classes = ("rectangle", "ellipse", "triangle") + label_schema = generate_label_schema(classes, task_type_to_label_domain(TaskType.DETECTION)) + out = find_label_by_name(label_schema.get_labels(include_empty=False), name, Domain.DETECTION) + assert out.name == name + + +@e2e_pytest_unit +def test_find_label_by_name_error(): + classes = ("rectangle", "rectangle", "triangle") + label_schema = generate_label_schema(classes, task_type_to_label_domain(TaskType.DETECTION)) + with pytest.raises(ValueError): + find_label_by_name(label_schema.get_labels(include_empty=False), "rectangle", Domain.DETECTION) + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "task_type, domain", + [(TaskType.DETECTION, Domain.DETECTION), (TaskType.INSTANCE_SEGMENTATION, Domain.INSTANCE_SEGMENTATION)], +) +def test_load_dataset_items_coco_format(task_type, domain): + _, labels = generate_det_dataset(task_type=task_type) + tmp_dir = tempfile.TemporaryDirectory() + with tempfile.TemporaryDirectory() as tmp_dir: + fake_json_file = os.path.join(tmp_dir, "fake_data.json") + create_dummy_coco_json(fake_json_file) + data_root_dir = "./some_data_root_dir" + with_mask = True if domain == Domain.INSTANCE_SEGMENTATION else False + out = load_dataset_items_coco_format( + fake_json_file, + data_root_dir, + subset=Subset.TRAINING, + domain=domain, + with_mask=with_mask, + labels_list=labels, + ) + assert out is not None + + +@e2e_pytest_unit +def test_get_sizes_from_dataset_entity(): + dataset, _ = generate_det_dataset(task_type=TaskType.DETECTION) + out = get_sizes_from_dataset_entity(dataset, [480, 640]) + assert out is not None + + +@e2e_pytest_unit +def test_get_anchor_boxes(): + out = get_anchor_boxes([(100, 120), (100, 120)], [1, 1]) + expected_out = ([[100.0], [100.0]], [[120.0], [120.0]]) + assert out == expected_out + + +@e2e_pytest_unit +def test_format_list_to_str(): + out = format_list_to_str([[0.1839128319, 0.47398123]]) + expected_out = "[[0.18, 0.47]]" + assert out == expected_out diff --git a/tests/unit/algorithms/detection/utils/test_detection_mask_to_bbox.py b/tests/unit/algorithms/detection/utils/test_detection_mask_to_bbox.py new file mode 100644 index 00000000000..ccdfcd699ab --- /dev/null +++ b/tests/unit/algorithms/detection/utils/test_detection_mask_to_bbox.py @@ -0,0 +1,27 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest + +from otx.algorithms.common.utils.mask_to_bbox import mask2bbox, mask_to_border +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMask2Bbox: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.mask = np.zeros((3, 3)) + self.mask[0, 0] = 1 + + @e2e_pytest_unit + def test_mask_to_border(self): + out = mask_to_border(self.mask) + assert (out == self.mask).all() + + @e2e_pytest_unit + def test_mask2bbox(self): + out = mask2bbox(self.mask) + expected_out = [[0, 0, 1, 1]] + assert out == expected_out diff --git a/tests/unit/algorithms/detection/utils/test_detection_utils.py b/tests/unit/algorithms/detection/utils/test_detection_utils.py new file mode 100644 index 00000000000..0a3a645e29e --- /dev/null +++ b/tests/unit/algorithms/detection/utils/test_detection_utils.py @@ -0,0 +1,40 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import addict + +from otx.algorithms.detection.utils.utils import ( + generate_label_schema, + get_det_model_api_configuration, +) +from otx.api.entities.model_template import TaskType, task_type_to_label_domain +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_get_det_model_api_configuration(): + classes = ("rectangle", "ellipse", "triangle") + label_schema = generate_label_schema(classes, task_type_to_label_domain(TaskType.DETECTION)) + det_thr = 0.5 + tiling_parameters = addict.Addict( + { + "enable_tiling": True, + "tile_size": 10, + "tile_overlap": 0.1, + "tile_ir_scale_factor": 1.0, + "tile_max_number": 100, + } + ) + model_api_cfg = get_det_model_api_configuration(label_schema, TaskType.DETECTION, det_thr, tiling_parameters) + + assert len(model_api_cfg) > 0 + assert model_api_cfg[("model_info", "confidence_threshold")] == str(det_thr) + assert model_api_cfg[("model_info", "tiles_overlap")] == str( + tiling_parameters.tile_overlap / tiling_parameters.tile_ir_scale_factor + ) + assert model_api_cfg[("model_info", "max_pred_number")] == str(tiling_parameters.tile_max_number) + assert ("model_info", "labels") in model_api_cfg + assert ("model_info", "label_ids") in model_api_cfg + assert len(label_schema.get_labels(include_empty=False)) == len(model_api_cfg[("model_info", "labels")].split()) + assert len(label_schema.get_labels(include_empty=False)) == len(model_api_cfg[("model_info", "label_ids")].split()) diff --git a/tests/unit/algorithms/segmentation/__init__.py b/tests/unit/algorithms/segmentation/__init__.py new file mode 100644 index 00000000000..2faffbe2b1f --- /dev/null +++ b/tests/unit/algorithms/segmentation/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/tests/unit/algorithms/segmentation/adapters/__init__.py b/tests/unit/algorithms/segmentation/adapters/__init__.py new file mode 100644 index 00000000000..2faffbe2b1f --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/__init__.py new file mode 100644 index 00000000000..28badfe4621 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/api/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/api/__init__.py new file mode 100644 index 00000000000..3189b1b1499 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/api/__init__.py @@ -0,0 +1,5 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.api""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/api/test_train.py b/tests/unit/algorithms/segmentation/adapters/mmseg/api/test_train.py new file mode 100644 index 00000000000..8f70756764b --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/api/test_train.py @@ -0,0 +1,116 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.api.train""" + +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from unittest import mock +from otx.algorithms.segmentation.adapters.mmseg.apis.train import train_segmentor +import mmcv +import os +import torch +from otx.algorithms.common.utils.utils import is_xpu_available + + +class TestTrainSegmentor: + @pytest.fixture + def mock_modules(self, mocker): + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.get_root_logger", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch("otx.algorithms.segmentation.adapters.mmseg.apis.train.build_dp", return_value=mock.MagicMock()) + mocker.patch("otx.algorithms.segmentation.adapters.mmseg.apis.train.build_ddp", return_value=mock.MagicMock()) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.build_optimizer", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.build_runner", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.build_dataset", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.build_dataloader", return_value=mock.MagicMock() + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.DistEvalHook", return_value=mock.MagicMock() + ) + mocker.patch("otx.algorithms.segmentation.adapters.mmseg.apis.train.EvalHook", return_value=mock.MagicMock()) + + @pytest.fixture + def mmcv_cfg(self): + return mmcv.Config( + { + "gpu_ids": [0], + "seed": 42, + "data": mock.MagicMock(), + "device": "cpu", + "optimizer": "Adam", + "optimizer_config": {}, + "total_epochs": 1, + "work_dir": "test", + "lr_config": {}, + "checkpoint_config": {}, + "log_config": {}, + "resume_from": False, + "load_from": "", + "workflow": "", + "log_level": 1, + "total_iters": 1000, + } + ) + + @pytest.fixture + def model(self): + return mock.MagicMock() + + @pytest.fixture + def dataset(self): + return mock.MagicMock() + + def test_train_model_single_dataset_no_validation(self, mock_modules, mmcv_cfg, model, dataset): + # Create mock inputs + _ = mock_modules + # Call the function + train_segmentor(model, dataset, mmcv_cfg, validate=False) + + def test_train_model_multiple_datasets_distributed_training(self, mock_modules, mmcv_cfg, model, dataset): + # Create mock inputs + _ = mock_modules + os.environ["LOCAL_RANK"] = "0" + # Call the function + train_segmentor(model, [dataset, dataset], mmcv_cfg, distributed=True, validate=True) + + def test_train_model_specific_timestamp_and_cuda_device(self, mock_modules, mmcv_cfg, model, dataset): + # Create mock inputs + _ = mock_modules + timestamp = "2024-01-01" + mmcv_cfg.device = "cuda" + meta = {"info": "some_info"} + # Call the function + train_segmentor(model, dataset, mmcv_cfg, timestamp=timestamp, meta=meta) + + def test_train_model_xpu_device(self, mock_modules, mmcv_cfg, model, dataset, mocker): + # Create mock inputs + _ = mock_modules + mmcv_cfg.device = "xpu" + mocker.patch("otx.algorithms.segmentation.adapters.mmseg.apis.train.torch") + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.apis.train.torch.xpu.optimize", + return_value=(mocker.MagicMock(), mocker.MagicMock()), + ) + # Call the function + train_segmentor(model, dataset, mmcv_cfg) diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/__init__.py new file mode 100644 index 00000000000..d671e6bb59c --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.datasets""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py new file mode 100644 index 00000000000..e2b1bd6ce7b --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.datasets.pipelines""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_compose.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_compose.py new file mode 100644 index 00000000000..92703625cb8 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_compose.py @@ -0,0 +1,151 @@ +"""Unit Tests for the OTX Dataset Pipeline - Compose.""" + +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from unittest.mock import MagicMock + +import numpy as np +import pytest +from mmseg.datasets.builder import PIPELINES +from mmseg.datasets.pipelines import RandomCrop + +from otx.algorithms.segmentation.adapters.mmseg.datasets import MaskCompose, ProbCompose + + +class TestProbCompose: + """ProbCompose Unit Tests.""" + + def test_inputs(self) -> None: + """Test when all inputs are correct.""" + transforms = [MagicMock(), MagicMock(), MagicMock()] + probs = [0.3, 0.4, 0.3] + prob_compose = ProbCompose(transforms, probs) + assert prob_compose.transforms == transforms + assert np.array_equal(prob_compose.limits, np.array([0.0, 0.3, 0.7, 1.0])) + + # Test when transforms and probs have different lengths + transforms = [MagicMock(), MagicMock()] + probs = [0.5, 0.5, 0.5] + with pytest.raises(AssertionError): + ProbCompose(transforms, probs) + + # Test when probs have negative values + transforms = [MagicMock(), MagicMock(), MagicMock()] + probs = [0.5, -0.2, 0.7] + with pytest.raises(AssertionError): + ProbCompose(transforms, probs) + + # Test when sum of probs is 0 + transforms = [MagicMock(), MagicMock(), MagicMock()] + probs = [0.0, 0.0, 0.0] + with pytest.raises(AssertionError): + ProbCompose(transforms, probs) + + def test_dict_transforms(self) -> None: + """Test whether dict transforms are correctly appended.""" + prob_compose = ProbCompose(transforms=[dict(type="Resize")], probs=[0.7]) + assert ( + repr(prob_compose.transforms[0]) + == "Resize(img_scale=None, multiscale_mode=range, ratio_range=None, keep_ratio=True)" + ) + + def test_invalid_transform_type(self) -> None: + """Test invalid transform type raises error.""" + with pytest.raises(TypeError): + transforms = ["Dummy Transform"] + probs = [0.5] + pipeline = ProbCompose(transforms, probs) + del pipeline # This is to silence the pylint's unused variable warning. + + def test_repr(self) -> None: + """Test the repr method.""" + transforms = [MagicMock()] + probs = [0.3] + prob_compose = ProbCompose(transforms, probs) + expected_repr = f"ProbCompose(\n {transforms[0]}\n)" + assert repr(prob_compose) == expected_repr + + +@PIPELINES.register_module() +class TestTransform(object): + def __init__(self) -> None: + pass + + def __call__(self, data: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + return data + + +@pytest.fixture() +def data() -> dict[str, np.ndarray]: + return {"img": np.ones((100, 100, 3), dtype=np.uint8), "label": np.ones((100, 100))} + + +class TestMaskCompose: + """MaskCompose Unit Tests.""" + + def test_keep_original_false(self, data: dict[str, np.ndarray]) -> None: + """Test that the main image is replaced with a mixed image when keep_original is False.""" + transforms = [dict(type="TestTransform")] + pipeline = MaskCompose(transforms=transforms, prob=1.0, keep_original=False) + mixed_data = pipeline(data) + + assert np.array_equal(data["img"], mixed_data["img"]) + assert np.array_equal(data["label"], mixed_data["label"]) + + def test_keep_original_true(self, data: dict[str, np.ndarray]) -> None: + """Test that the mixed image is added as aux_img when keep_original is True.""" + transforms = [dict(type="TestTransform")] + pipeline = MaskCompose(transforms=transforms, prob=1.0, keep_original=True) + mixed_data = pipeline(data) + + assert np.array_equal(mixed_data["img"], mixed_data["aux_img"]) + assert np.array_equal(data["img"], mixed_data["img"]) + assert np.array_equal(data["label"], mixed_data["label"]) + + def test_callable_transform(self, data: dict[str, np.ndarray]) -> None: + """Test callable transform is appended to the list of transforms.""" + crop_size = (10, 10) + transform = RandomCrop(crop_size=crop_size) + pipeline = MaskCompose(transforms=[transform], prob=1.0, keep_original=False) + mixed_data = pipeline(data) + assert mixed_data["img_shape"][:2] == crop_size + + def test_invalid_transform_type(self) -> None: + """Test invalid transform type raises error.""" + with pytest.raises(TypeError): + transform = [{"Dummy Transform": "So dummy"}] + pipeline = MaskCompose(transforms=[transform], prob=1.0, keep_original=False) + del pipeline # This is to silence the pylint's unused variable warning. + + def test_prob_zero(self, data: dict[str, np.ndarray]) -> None: + """Test that the main image is not modified when prob is 0.""" + transforms = [ + dict(type="RandomFlip", prob=0.0, direction="horizontal"), + dict(type="RandomRotate", prob=0.0, degree=30, pad_val=0, seg_pad_val=255), + ] + pipeline = MaskCompose(transforms=transforms, prob=0, keep_original=False) + mixed_data = pipeline(data) + + assert np.array_equal(data["img"], mixed_data["img"]) + assert np.array_equal(data["label"], mixed_data["label"]) + + def test_apply_transforms_returns_none(self, data: dict[str, np.ndarray]) -> None: + """Test that None is returned when apply_transforms returns None.""" + transforms = [ + dict(type="RandomFlip", prob=0.5, direction="horizontal"), + lambda x: None, + ] + pipeline = MaskCompose(transforms=transforms, prob=1.0, keep_original=False) + + with pytest.raises(AssertionError): + mixed_data = pipeline(data) + del mixed_data # This is to silence the pylint's unused variable warning. + + def test_repr(self) -> None: + """Test __repr__ method.""" + pipeline = MaskCompose(transforms=[dict(type="RandomFlip")], prob=1.0) + expected_repr = "MaskCompose(\n RandomFlip(prob=None)\n)" + assert repr(pipeline) == expected_repr diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_loads.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_loads.py new file mode 100644 index 00000000000..eef126f9c5f --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_loads.py @@ -0,0 +1,98 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest + +from otx.algorithms.segmentation.adapters.mmseg.datasets.pipelines import ( + LoadAnnotationFromOTXDataset, + LoadResizeDataFromOTXDataset, +) +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.core.data.caching import MemCacheHandlerSingleton + +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.detection.test_helpers import generate_det_dataset + +from typing import Iterator, List, Optional, Sequence, Tuple + + +def label_entity(name="test label") -> LabelEntity: + return LabelEntity(name=name, domain=Domain.SEGMENTATION) + + +def dataset_item() -> DatasetItemEntity: + image: Image = Image(data=np.random.randint(low=0, high=255, size=(10, 16, 3)).astype(np.uint8)) + annotation: Annotation = Annotation(shape=Rectangle.generate_full_box(), labels=[ScoredLabel(label_entity())]) + annotation_scene: AnnotationSceneEntity = AnnotationSceneEntity( + annotations=[annotation], kind=AnnotationSceneKind.ANNOTATION + ) + return DatasetItemEntity(media=image, annotation_scene=annotation_scene) + + +class TestLoadAnnotationFromOTXDataset: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + + self.dataset_item: DatasetItemEntity = dataset_item() + self.results: dict = { + "dataset_item": self.dataset_item, + "ann_info": {"labels": [label_entity("class_1")]}, + "seg_fields": [], + } + self.pipeline: LoadAnnotationFromOTXDataset = LoadAnnotationFromOTXDataset() + + @e2e_pytest_unit + def test_call(self) -> None: + loaded_annotations: dict = self.pipeline(self.results) + assert "gt_semantic_seg" in loaded_annotations + + +@e2e_pytest_unit +@pytest.mark.parametrize("mode", ["singleprocessing", "multiprocessing"]) +def test_load_resize_data_from_otx_dataset_call(mocker, mode): + """Test LoadResizeDataFromOTXDataset.""" + item: DatasetItemEntity = dataset_item() + MemCacheHandlerSingleton.create(mode, item.numpy.size * 2) + op = LoadResizeDataFromOTXDataset( + use_otx_adapter=True, + load_ann_cfg=dict( + type="LoadAnnotationFromOTXDataset", + use_otx_adapter=True, + ), + resize_cfg=dict( + type="Resize", + img_scale=(8, 5), # (w, h) + ), # 10x16 -> 5x8 + ) + src_dict = dict( + dataset_item=item, + width=item.width, + height=item.height, + index=0, + ann_info=dict(labels=[label_entity()]), + seg_fields=[], + ) + dst_dict = op(src_dict) + assert dst_dict["ori_shape"][0] == 10 + assert dst_dict["img_shape"][0] == 5 # height + assert dst_dict["img_shape"][1] == 8 # width + assert dst_dict["img"].shape == dst_dict["img_shape"] + assert dst_dict["img"].shape[:2] == dst_dict["gt_semantic_seg"].shape[:2] + assert "gt_semantic_seg" not in src_dict # src_dict not affected + op._load_img = mocker.MagicMock() + dst_dict_from_cache = op(src_dict) + assert op._load_img.call_count == 0 # _load_img() should not be called + assert np.array_equal(dst_dict["img"], dst_dict_from_cache["img"]) + assert np.array_equal(dst_dict["gt_semantic_seg"], dst_dict_from_cache["gt_semantic_seg"]) diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_transforms.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_transforms.py new file mode 100644 index 00000000000..5187a0f507d --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_transforms.py @@ -0,0 +1,313 @@ +from typing import Any, Dict, List + +import numpy as np +import pytest +import torch +from mmcv.parallel import DataContainer +from PIL import Image + +from otx.algorithms.segmentation.adapters.mmseg.datasets.pipelines.transforms import ( + BranchImage, + DefaultFormatBundle, + NDArrayToPILImage, + Normalize, + PILImageToNDArray, + RandomColorJitter, + RandomGaussianBlur, + RandomGrayscale, + RandomResizedCrop, + RandomSolarization, + TwoCropTransform, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@pytest.fixture(scope="module") +def inputs_np(): + return { + "img": np.random.randint(0, 10, (16, 16, 3), dtype=np.uint8), + "gt_semantic_seg": np.random.rand(16, 16), + "flip": True, + } + + +@pytest.fixture(scope="module") +def inputs_PIL(): + return { + "img": Image.fromarray(np.random.randint(0, 10, (16, 16, 3), dtype=np.uint8)), + "gt_semantic_seg": np.random.randint(0, 5, (16, 16), dtype=np.uint8), + "seg_fields": ["gt_semantic_seg"], + "ori_shape": (16, 16, 3), + } + + +class TestNDArrayToPILImage: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + self.results: dict = {"img": np.random.randint(0, 255, (3, 3, 3), dtype=np.uint8)} + self.nd_array_to_pil_image: NDArrayToPILImage = NDArrayToPILImage(keys=["img"]) + + @e2e_pytest_unit + def test_call(self) -> None: + converted_img: dict = self.nd_array_to_pil_image(self.results) + assert "img" in converted_img + assert isinstance(converted_img["img"], Image.Image) + + @e2e_pytest_unit + def test_repr(self) -> None: + assert str(self.nd_array_to_pil_image) == "NDArrayToPILImage" + + +class TestPILImageToNDArray: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + self.results: dict = {"img": Image.new("RGB", (3, 3))} + self.pil_image_to_nd_array: PILImageToNDArray = PILImageToNDArray(keys=["img"]) + + @e2e_pytest_unit + def test_call(self) -> None: + converted_array: dict = self.pil_image_to_nd_array(self.results) + assert "img" in converted_array + assert isinstance(converted_array["img"], np.ndarray) + + @e2e_pytest_unit + def test_repr(self) -> None: + assert str(self.pil_image_to_nd_array) == "PILImageToNDArray" + + +class TestRandomResizedCrop: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + self.results: dict = {"img": Image.new("RGB", (10, 16)), "img_shape": (10, 16), "ori_shape": (10, 16)} + self.random_resized_crop: RandomResizedCrop = RandomResizedCrop((5, 5), (0.5, 1.0)) + + @e2e_pytest_unit + def test_call(self) -> None: + cropped_img: dict = self.random_resized_crop(self.results) + assert cropped_img["img_shape"] == (5, 5) + assert cropped_img["ori_shape"] == (10, 16) + + +class TestRandomSolarization: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + self.results: dict = {"img": np.random.randint(0, 255, (3, 3, 3), dtype=np.uint8)} + self.random_solarization: RandomSolarization = RandomSolarization(p=1.0) + + @e2e_pytest_unit + def test_call(self) -> None: + solarized: dict = self.random_solarization(self.results) + assert "img" in solarized + assert isinstance(solarized["img"], np.ndarray) + + @e2e_pytest_unit + def test_repr(self) -> None: + assert str(self.random_solarization) == "RandomSolarization" + + +class TestNormalize: + @e2e_pytest_unit + @pytest.mark.parametrize( + "mean,std,to_rgb,expected", + [ + ([[[1.0, 1.0, 1.0]]], [[[1.0, 1.0, 1.0]]], True, np.array([[[1.0, 0.0, -1.0]]], dtype=np.float32)), + ([[[1.0, 1.0, 1.0]]], [[[1.0, 1.0, 1.0]]], False, np.array([[[-1.0, 0.0, 1.0]]], dtype=np.float32)), + ], + ) + def test_call(self, mean: List[float], std: List[float], to_rgb: bool, expected: np.array) -> None: + """Test __call__.""" + normalize = Normalize(mean=mean, std=std, to_rgb=to_rgb) + inputs = dict(img=np.arange(3).reshape(1, 1, 3)) + + results = normalize(inputs.copy()) + + assert "img" in results + assert "img_norm_cfg" in results + assert np.all(results["img"] == expected) + + @e2e_pytest_unit + @pytest.mark.parametrize("mean,std,to_rgb", [(1.0, 1.0, True)]) + def test_repr(self, mean: float, std: float, to_rgb: bool) -> None: + """Test __repr__.""" + normalize = Normalize(mean=mean, std=std, to_rgb=to_rgb) + + assert repr(normalize) == normalize.__class__.__name__ + f"(mean={mean}, std={std}, to_rgb=" f"{to_rgb})" + + +class TestDefaultFormatBundle: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.default_format_bundle = DefaultFormatBundle() + + @e2e_pytest_unit + @pytest.mark.parametrize("img", [np.ones((1, 1)), np.ones((1, 1, 1)), np.ones((1, 1, 1, 1))]) + @pytest.mark.parametrize("gt_semantic_seg,pixel_weights", [(np.ones((1, 1)), np.ones((1, 1)))]) + def test_call(self, img: np.array, gt_semantic_seg: np.array, pixel_weights: np.array) -> None: + """Test __call__.""" + inputs = dict(img=img, gt_semantic_seg=gt_semantic_seg, pixel_weights=pixel_weights) + + results = self.default_format_bundle(inputs.copy()) + + assert isinstance(results, dict) + assert "img" in results + assert isinstance(results["img"], DataContainer) + assert len(results["img"].data.shape) >= 3 + assert results["img"].data.dtype == torch.float32 + assert "gt_semantic_seg" in results + assert len(results["gt_semantic_seg"].data.shape) == len(inputs["gt_semantic_seg"].shape) + 1 + assert results["gt_semantic_seg"].data.dtype == torch.int64 + assert "pixel_weights" in results + assert len(results["pixel_weights"].data.shape) == len(inputs["pixel_weights"].shape) + 1 + assert results["pixel_weights"].data.dtype == torch.float32 + + @e2e_pytest_unit + @pytest.mark.parametrize("img", [np.ones((1,))]) + def test_call_invalid_shape(self, img: np.array): + inputs = dict(img=img) + + with pytest.raises(ValueError): + self.default_format_bundle(inputs.copy()) + + @e2e_pytest_unit + def test_repr(self) -> None: + """Test __repr__.""" + assert repr(self.default_format_bundle) == self.default_format_bundle.__class__.__name__ + + +class TestBranchImage: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.branch_image = BranchImage(key_map={"key1": "key2"}) + + @e2e_pytest_unit + def test_call(self) -> None: + """Test __call__.""" + inputs = dict(key1="key1", img_fields=["key1"]) + + results = self.branch_image(inputs.copy()) + + assert isinstance(results, dict) + assert "key2" in results + assert results["key1"] == results["key2"] + assert "key2" in results["img_fields"] + + @e2e_pytest_unit + def test_repr(self) -> None: + """Test __repr__.""" + assert repr(self.branch_image) == self.branch_image.__class__.__name__ + + +class TestTwoCropTransform: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.datasets.pipelines.transforms.build_from_cfg", + return_value=lambda x: x, + ) + self.two_crop_transform = TwoCropTransform(view0=[], view1=[]) + + @e2e_pytest_unit + def test_call(self, mocker, inputs_np: Dict[str, Any]) -> None: + """Test __call__.""" + results = self.two_crop_transform(inputs_np) + + assert isinstance(results, dict) + assert "img" in results and results["img"].ndim == 4 + assert "gt_semantic_seg" in results and results["gt_semantic_seg"].ndim == 3 + assert "flip" in results and isinstance(results["flip"], list) + + @e2e_pytest_unit + def test_call_with_single_pipeline(self, mocker, inputs_np: Dict[str, Any]) -> None: + """Test __call__ with single pipeline.""" + self.two_crop_transform.is_both = False + + results = self.two_crop_transform(inputs_np) + + assert isinstance(results, dict) + assert "img" in results and results["img"].ndim == 3 + assert "gt_semantic_seg" in results and results["gt_semantic_seg"].ndim == 2 + assert "flip" in results and isinstance(results["flip"], bool) + + +@e2e_pytest_unit +def test_random_resized_crop(inputs_PIL: Dict[str, Any]) -> None: + """Test RandomResizedCrop.""" + random_resized_crop = RandomResizedCrop(size=(8, 8)) + + results = random_resized_crop(inputs_PIL) + + assert isinstance(results, dict) + assert "img" in results and results["img"].size == (8, 8) + assert "gt_semantic_seg" in results and results["gt_semantic_seg"].shape == (8, 8) + assert "img_shape" in results + assert "ori_shape" in results + assert "scale_factor" in results + + +@e2e_pytest_unit +def test_random_color_jitter(inputs_PIL: Dict[str, Any]) -> None: + """Test RandomColorJitter.""" + random_color_jitter = RandomColorJitter(p=1.0) + + results = random_color_jitter(inputs_PIL) + + assert isinstance(results, dict) + assert "img" in results + + +@e2e_pytest_unit +def test_random_grayscale(inputs_PIL: Dict[str, Any]) -> None: + """Test RandomGrayscale.""" + random_grayscale = RandomGrayscale() + + results = random_grayscale(inputs_PIL) + + assert isinstance(results, dict) + assert "img" in results + + +@e2e_pytest_unit +def test_random_gaussian_blur(inputs_PIL: Dict[str, Any]) -> None: + """Test RandomGaussianBlur.""" + random_gaussian_blur = RandomGaussianBlur(p=1.0, kernel_size=3) + + results = random_gaussian_blur(inputs_PIL) + + assert isinstance(results, dict) + assert "img" in results + + +@e2e_pytest_unit +def test_random_solarization(inputs_np: Dict[str, Any]) -> None: + """Test RandomSolarization.""" + random_solarization = RandomSolarization(p=1.0) + + results = random_solarization(inputs_np) + + assert isinstance(results, dict) + assert "img" in results + assert repr(random_solarization) == "RandomSolarization" + + +@e2e_pytest_unit +def test_nd_array_to_pil_image(inputs_np: Dict[str, Any]) -> None: + """Test NDArrayToPILImage.""" + nd_array_to_pil_image = NDArrayToPILImage(keys=["img"]) + + results = nd_array_to_pil_image(inputs_np) + + assert "img" in results + assert isinstance(results["img"], Image.Image) + assert repr(nd_array_to_pil_image) == "NDArrayToPILImage" + + +@e2e_pytest_unit +def test_pil_image_to_nd_array(inputs_PIL: Dict[str, Any]) -> None: + """Test PILImageToNDArray.""" + pil_image_to_nd_array = PILImageToNDArray(keys=["img"]) + + results = pil_image_to_nd_array(inputs_PIL) + + assert "img" in results + assert isinstance(results["img"], np.ndarray) + assert repr(pil_image_to_nd_array) == "PILImageToNDArray" diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/test_dataset.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/test_dataset.py new file mode 100644 index 00000000000..adfffd5672e --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/test_dataset.py @@ -0,0 +1,102 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest + +from otx.algorithms.segmentation.adapters.mmseg.datasets import OTXSegDataset +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def label_entity(name="test label", id="0") -> LabelEntity: + return LabelEntity(name=name, id=ID(id), domain=Domain.SEGMENTATION) + + +def dataset_item() -> DatasetItemEntity: + image: Image = Image(data=np.random.randint(low=0, high=255, size=(10, 16, 3))) + annotation: Annotation = Annotation(shape=Rectangle.generate_full_box(), labels=[ScoredLabel(label_entity())]) + annotation_scene: AnnotationSceneEntity = AnnotationSceneEntity( + annotations=[annotation], kind=AnnotationSceneKind.ANNOTATION + ) + return DatasetItemEntity(media=image, annotation_scene=annotation_scene) + + +class TestOTXSegDataset: + @pytest.fixture(autouse=True) + def setUp(self, mocker) -> None: + self.otx_dataset: DatasetEntity = DatasetEntity(items=[dataset_item()]) + self.pipeline: list[dict] = [{"type": "LoadImageFromOTXDataset", "to_float32": True}] + self.classes: list[str] = ["class_1", "class_2"] + labels_entities = [label_entity(name, i) for i, name in enumerate(self.classes)] + + mocker.patch.object(OTXSegDataset, "filter_labels", return_value=labels_entities) + + self.dataset: OTXSegDataset = OTXSegDataset( + otx_dataset=self.otx_dataset, + pipeline=self.pipeline, + labels=labels_entities, + new_classes=self.classes, + ) + + @e2e_pytest_unit + def test_mpasegdataset_initialization(self) -> None: + assert self.dataset.otx_dataset == self.otx_dataset + assert self.dataset.CLASSES == ["background"] + self.classes + + # Check if img_indices are generated properly + assert hasattr(self.dataset, "img_indices") + + # Check if label_map is created as expected + assert self.dataset.label_map == {0: 0, 1: 1, 2: 2} + + @e2e_pytest_unit + def test_classes_sorted(self, mocker) -> None: + self.otx_dataset: DatasetEntity = DatasetEntity(items=[dataset_item()]) + self.pipeline: list[dict] = [{"type": "LoadImageFromOTXDataset", "to_float32": True}] + self.classes: list[str] = [f"class_{i+1}" for i in range(11)] + labels_entities = [label_entity(name, str(i)) for i, name in enumerate(self.classes)] + + mocker.patch.object(OTXSegDataset, "filter_labels", return_value=labels_entities) + + self.dataset: OTXSegDataset = OTXSegDataset( + otx_dataset=self.otx_dataset, + pipeline=self.pipeline, + labels=labels_entities, + new_classes=self.classes, + ) + + assert self.dataset.CLASSES == ["background"] + self.classes + assert self.dataset.CLASSES == ["background"] + [ + label.name for label in sorted(labels_entities, key=lambda x: int(x.id)) + ] + + assert self.dataset.label_map == {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11} + + @e2e_pytest_unit + def test_getitem_method(self) -> None: + data_item: dict = self.dataset[0] + assert "dataset_item" in data_item + assert "ann_info" in data_item + + @e2e_pytest_unit + def test_getting_annotation_info(self) -> None: + annotation_info: dict = self.dataset.get_ann_info(0) + assert "gt_semantic_seg" in annotation_info + + @e2e_pytest_unit + def test_get_gt_seg_maps(self) -> None: + gt_seg_map: np.ndarray = self.dataset.get_gt_seg_maps()[0] + assert np.equal(gt_seg_map, np.zeros((10, 16))).all() diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/__init__.py new file mode 100644 index 00000000000..e200c90f7bc --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.models""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py new file mode 100644 index 00000000000..dc3be0d0648 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.models.backbones.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_litehrnet.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_litehrnet.py new file mode 100644 index 00000000000..361e1527f1d --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_litehrnet.py @@ -0,0 +1,101 @@ +from copy import deepcopy + +import pytest +import torch + +from otx.algorithms.common.adapters.mmcv.configs.backbones.lite_hrnet_18 import ( + model as model_cfg, +) +from otx.algorithms.segmentation.adapters.mmseg.models.backbones.litehrnet import ( + LiteHRNet, + NeighbourSupport, + SpatialWeightingV2, + StemV2, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSpatialWeightingV2: + @e2e_pytest_unit + def test_forward(self): + swv2 = SpatialWeightingV2(channels=32) + assert swv2 is not None + + inputs = torch.randn(1, 32, 32, 32) + outputs = swv2(inputs) + assert outputs is not None + + +class TestStemV2: + def setup_method(self): + self.stemv2 = StemV2(in_channels=32, stem_channels=32, out_channels=32, expand_ratio=1) + + @e2e_pytest_unit + def test_init(self): + stemv2_extra_stride = StemV2( + in_channels=32, stem_channels=32, out_channels=32, expand_ratio=1, extra_stride=True + ) + assert stemv2_extra_stride is not None + + stemv2_input_norm = StemV2(in_channels=32, stem_channels=32, out_channels=32, expand_ratio=1, input_norm=True) + assert stemv2_input_norm is not None + + @e2e_pytest_unit + def test_forward(self): + inputs = torch.randn(1, 32, 32, 32) + outputs = self.stemv2(inputs) + assert outputs is not None + + +class TestNeighbourSupport: + @e2e_pytest_unit + def test_forward(self): + neighbour_support = NeighbourSupport(channels=32) + assert neighbour_support is not None + + inputs = torch.randn(1, 32, 32, 32) + outputs = neighbour_support(inputs) + assert outputs is not None + + +class TestLiteHRNet: + def setup_method(self): + self.extra = model_cfg["backbone"]["extra"] + self.model = LiteHRNet(extra=self.extra) + + @e2e_pytest_unit + def test_init(self): + extra = deepcopy(self.extra) + extra["out_modules"]["conv"]["enable"] = True + model = LiteHRNet(extra=extra) + assert model is not None + + extra["add_stem_features"] = True + model = LiteHRNet(extra=extra) + assert model is not None + + extra["stages_spec"]["module_type"] = ("NAIVE", "NAIVE", "NAIVE") + extra["stages_spec"]["weighting_module_version"] = "v2" + model = LiteHRNet(extra=extra) + assert model is not None + + @e2e_pytest_unit + def test_init_weights(self): + self.model.init_weights() + + with pytest.raises(TypeError, match="pretrained must be a str or None"): + self.model.init_weights(0) + + @e2e_pytest_unit + def test_forward(self): + self.model.train() + inputs = torch.randn((1, 3, 224, 224)) + outputs = self.model(inputs) + assert outputs is not None + + extra = deepcopy(self.extra) + extra["stages_spec"]["module_type"] = ("NAIVE", "NAIVE", "NAIVE") + extra["stages_spec"]["weighting_module_version"] = "v2" + model = LiteHRNet(extra=extra) + outputs = model(inputs) + assert outputs is not None diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_mmseg_mmov_backbone.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_mmseg_mmov_backbone.py new file mode 100644 index 00000000000..24aebfcbe81 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_mmseg_mmov_backbone.py @@ -0,0 +1,48 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import openvino.runtime as ov +import pytest +import torch + +from otx.algorithms.segmentation.adapters.mmseg.models.backbones import MMOVBackbone +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMMOVBackbone: + @pytest.fixture(autouse=True) + def setup(self): + param = ov.opset10.parameter([1, 3, 64, 64], ov.Type.f32, name="in") + filter = ov.opset10.constant(np.random.normal(size=(1, 3, 64, 64)), ov.Type.f32) + mul = ov.opset10.matmul(param, filter, False, False) + result = ov.opset10.result(mul, name="out") + ov_model = ov.Model([result], [param], "seg_backbone") + + self.model = MMOVBackbone( + model_path_or_model=ov_model, + outputs=["out"], + remove_normalize=True, + merge_bn=True, + paired_bn=True, + ) + + @e2e_pytest_unit + def test_init_weights(self): + self.model.init_weights() + + @e2e_pytest_unit + def test_forward(self): + assert self.model.inputs == self.model._inputs + assert self.model.outputs == self.model._outputs + assert self.model.features == self.model._feature_dict + assert self.model.input_shapes == self.model._input_shapes + assert self.model.output_shapes == self.model._output_shapes + + data = {} + for key, shape in self.model.input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data[key] = torch.randn(shape) + self.model.train() + self.model(list(data.values()), torch.tensor([0])) diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py new file mode 100644 index 00000000000..60b6f78ebef --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.models.heads.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_detcon_head.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_detcon_head.py new file mode 100644 index 00000000000..fcb6e2e7e09 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_detcon_head.py @@ -0,0 +1,64 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy + +import pytest +import torch + +from otx.algorithms.segmentation.adapters.mmseg.models import DetConHead +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestDetConHead: + @pytest.fixture(autouse=True) + def setup(self): + self.detcon_head = DetConHead( + predictor=dict( + type="SelfSLMLP", + in_channels=4, + hid_channels=8, + out_channels=4, + norm_cfg=dict(type="BN1d", requires_grad=True), + with_avg_pool=False, + ), + loss_cfg=dict(type="DetConLoss", temperature=0.1), + ) + + @e2e_pytest_unit + def test_init(self): + assert self.detcon_head.predictor.__class__.__name__ == "SelfSLMLP" + assert self.detcon_head.detcon_loss.__class__.__name__ == "DetConLoss" + + @e2e_pytest_unit + def test_init_weights(self): + self.detcon_head.predictor.mlp[0].weight = torch.nn.Parameter( + torch.ones_like(self.detcon_head.predictor.mlp[0].weight) + ) + old_weights = deepcopy(self.detcon_head.predictor.mlp[0].weight) + + self.detcon_head.init_weights() + + new_weights = self.detcon_head.predictor.mlp[0].weight + + assert torch.all(old_weights != new_weights) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "projs,projs_tgt,ids,ids_tgt,batch_size,num_samples", + [(torch.ones((4, 4)), torch.ones((4, 4)), torch.Tensor([0, 0, 0, 0]), torch.Tensor([1, 1, 1, 1]), 2, 1)], + ) + def test_forward( + self, + projs: torch.Tensor, + projs_tgt: torch.Tensor, + ids: torch.Tensor, + ids_tgt: torch.Tensor, + batch_size: int, + num_samples: int, + ): + loss = self.detcon_head(projs, projs_tgt, ids, ids_tgt, batch_size, num_samples) + + assert isinstance(loss, dict) + assert "loss" in loss diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_mmseg_mmov_decode_head.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_mmseg_mmov_decode_head.py new file mode 100644 index 00000000000..07f46d0e8ae --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_mmseg_mmov_decode_head.py @@ -0,0 +1,66 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import openvino.runtime as ov +import pytest +import torch + +from otx.algorithms.segmentation.adapters.mmseg.models import MMOVDecodeHead +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMMOVBackbone: + @pytest.fixture(autouse=True) + def setup(self): + + params = [] + results = [] + + param = ov.opset10.parameter([1, 24, 64, 64], ov.Type.f32, name="in") + filter = ov.opset10.constant(np.random.normal(size=(1, 24, 64, 64)), ov.Type.f32) + mul = ov.opset10.matmul(param, filter, False, False) + result = ov.opset10.result(mul, name="extractor_out") + params.append(param) + results.append(result) + + filter = ov.opset10.constant(np.random.normal(size=(1, 24, 64, 64)), ov.Type.f32) + mul = ov.opset10.matmul(param, filter, False, False) + result = ov.opset10.result(mul, name="cls_seg_out") + results.append(result) + + ov_model = ov.Model(results, params, "seg_head") + + self.model = MMOVDecodeHead( + model_path_or_model=ov_model, + inputs=dict( + extractor="in", + cls_seg="in", + ), + outputs=dict( + extractor="extractor_out", + cls_seg="cls_seg_out", + ), + in_channels=320, + num_classes=24, + ) + + @e2e_pytest_unit + def test_init_weights(self): + self.model.init_weights() + + @e2e_pytest_unit + def test_forward(self): + + data = {} + input_shapes = self.model.conv_seg.input_shapes + if getattr(self.model, "extractor", None): + input_shapes = self.model.extractor.input_shapes + + for key, shape in input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data[key] = torch.randn(shape) + + output = self.model(list(data.values())) + assert output.shape[1] == 24 diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_prototype_head.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_prototype_head.py new file mode 100644 index 00000000000..695bbe76d6b --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_prototype_head.py @@ -0,0 +1,31 @@ +import pytest +import torch + +from otx.algorithms.segmentation.adapters.mmseg.models.heads.proto_head import ProtoNet +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestProtoNet: + @pytest.fixture(autouse=True) + def setup(self): + self.proto_net = ProtoNet( + gamma=0.99, num_prototype=4, in_proto_channels=512, in_channels=512, channels=512, num_classes=4 + ) + + def test_prototype_learning(self): + dummy_input = torch.rand(32768, 512) + dummy_out_seg = torch.rand(8, 4, 64, 64) + dummy_masks = torch.rand(32768, 4, 4) + dummy_gt_seg = torch.randint(low=0, high=5, size=(32768,)) + proto_logits, proto_target = self.proto_net.prototype_learning( + dummy_input, dummy_out_seg, dummy_gt_seg, dummy_masks + ) + assert proto_logits is not None + assert proto_target is not None + + def test_forward(self): + dummy_input = torch.rand(8, 512, 64, 64) + dummy_gt_seg = torch.randint(low=0, high=5, size=(8, 1, 512, 512)) + proto_out = self.proto_net(inputs=dummy_input, gt_semantic_seg=dummy_gt_seg) + assert isinstance(proto_out, dict) + assert proto_out["out_seg"] is not None diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py new file mode 100644 index 00000000000..f3d10286d13 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.models.losses""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_cross_entropy_loss_with_ignore.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_cross_entropy_loss_with_ignore.py new file mode 100644 index 00000000000..6350be9055f --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_cross_entropy_loss_with_ignore.py @@ -0,0 +1,46 @@ +import pytest +import torch + +from otx.algorithms.segmentation.adapters.mmseg.models.losses.cross_entropy_loss_with_ignore import ( + CrossEntropyLossWithIgnore, +) +from mmseg.models.losses import CrossEntropyLoss + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCrossEntropyLosWithIgnore: + @pytest.fixture(autouse=True) + def setup(self): + self.mock_score = torch.rand([1, 2, 5, 5]) + self.mock_gt = torch.zeros((1, 5, 5), dtype=torch.long) + self.mock_gt[::2, 1::2, :] = 1 + self.mock_gt[1::2, ::2, :] = 1 + + self.loss_f = CrossEntropyLossWithIgnore() + + @e2e_pytest_unit + def test_is_label_ignored(self): + loss = self.loss_f(self.mock_score, self.mock_gt, reduction_override="none") + assert type(loss) == torch.Tensor + + mock_valid_label_mask = torch.Tensor([[1, 0]]) + loss_ignore = self.loss_f( + self.mock_score, self.mock_gt, reduction_override="none", valid_label_mask=mock_valid_label_mask + ) + + assert torch.all(loss_ignore[::2, 1::2, :] == 0) + assert torch.all(loss_ignore[1::2, ::2, :] == 0) + + assert torch.equal(loss, loss_ignore) is False + + @e2e_pytest_unit + def test_is_equal_to_ce_loss(self): + loss_f_mmseg = CrossEntropyLoss() + + loss_1 = loss_f_mmseg(self.mock_score, self.mock_gt) + loss_2 = self.loss_f(self.mock_score, self.mock_gt) + loss_3 = self.loss_f(self.mock_score, self.mock_gt, valid_label_mask=torch.Tensor([1, 1])) + + assert loss_1 == loss_2 + assert loss_2 == loss_3 diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_detcon_loss.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_detcon_loss.py new file mode 100644 index 00000000000..1c44d0eb845 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_detcon_loss.py @@ -0,0 +1,63 @@ +import pytest +import torch + +from otx.algorithms.segmentation.adapters.mmseg.models.losses import detcon_loss as detcon_loss_file +from otx.algorithms.segmentation.adapters.mmseg.models.losses.detcon_loss import ( + DetConLoss, + manual_cross_entropy, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "logits,labels,weight,expected", + [ + ( + torch.Tensor([[[-1.2786, 2.1673, -1.0000e09, 6.2263], [-6.7169, -7.1416, 7.3364, -1.0000e09]]]), + torch.Tensor([[[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]]]), + torch.Tensor([[1.0, 1.0]]), + torch.tensor(11.0003), + ), + ( + torch.Tensor( + [[[4.3260e-01, 1.6511e00, -1.0000e09, -7.9047e-01], [9.4888e-01, 2.6476e00, -3.7093e-01, -1.0000e09]]] + ), + torch.Tensor([[[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]]]), + torch.Tensor([[1.0, 1.0]]), + torch.tensor(0.8755), + ), + ], +) +def test_manual_cross_entropy(logits, labels, weight, expected): + results = manual_cross_entropy(logits, labels, weight) + + assert torch.allclose(results, expected) + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "inputs,expected", + [ + ( + { + "pred1": torch.Tensor([[[-0.0461, 0.1686, -0.1898, -0.1727], [-0.0609, -0.0948, -0.0463, -0.1461]]]), + "pred2": torch.Tensor([[[-0.0811, -0.0115, -0.1901, -0.0227], [-0.0230, 0.0898, -0.1495, -0.0360]]]), + "target1": torch.Tensor([[[-0.7734, 0.1523, 0.4729, -2.2324], [-0.3711, 0.2083, 0.0198, -1.9111]]]), + "target2": torch.Tensor([[[-1.0625, 1.4160, 0.7988, 1.3291], [-0.2479, 0.9683, 0.0859, 0.4832]]]), + "pind1": torch.Tensor([[1, 0]]), + "pind2": torch.Tensor([[1, 0]]), + "tind1": torch.Tensor([[1, 0]]), + "tind2": torch.Tensor([[1, 0]]), + }, + torch.tensor(11.8758), + ) + ], +) +def test_detcon_loss(mocker, inputs, expected): + mocker.patch.object(detcon_loss_file, "dist").is_initialized.return_value = False + detcon_loss = DetConLoss() + + loss = detcon_loss(**inputs) + + assert torch.allclose(loss, expected) diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_pixel_prototype_ce_loss.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_pixel_prototype_ce_loss.py new file mode 100644 index 00000000000..7a86638ea36 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/losses/test_pixel_prototype_ce_loss.py @@ -0,0 +1,24 @@ +import pytest +import torch + +from otx.algorithms.segmentation.adapters.mmseg.models.losses import ( + PixelPrototypeCELoss, +) + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestPixelPrototypeCELoss: + @pytest.fixture(autouse=True) + def setup(self): + self.loss_proto_ce = PixelPrototypeCELoss() + + @e2e_pytest_unit + def test_forward(self): + dummy_out = torch.rand(4, 5, 512, 512) + proto_logits = torch.rand(1280, 16) + proto_target = torch.rand(1280) + target = torch.randint(low=0, high=5, size=(4, 1, 512, 512)) + loss = self.loss_proto_ce(dummy_out, proto_logits, proto_target, target) + assert loss is not None + assert loss >= 0 diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py new file mode 100644 index 00000000000..d403115a3e3 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.models.necks""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/necks/test_selfsl_mlp.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/necks/test_selfsl_mlp.py new file mode 100644 index 00000000000..a4216943e6c --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/necks/test_selfsl_mlp.py @@ -0,0 +1,153 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Dict + +import pytest +import torch +import torch.nn as nn + +from otx.algorithms.segmentation.adapters.mmseg import SelfSLMLP +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSelfSLMLP: + @e2e_pytest_unit + @pytest.mark.parametrize("use_conv", [True, False]) + @pytest.mark.parametrize("with_avg_pool", [True, False]) + def test_init(self, use_conv: bool, with_avg_pool: bool): + """Test __init__ function.""" + selfslmlp = SelfSLMLP( + in_channels=2, hid_channels=2, out_channels=2, use_conv=use_conv, with_avg_pool=with_avg_pool + ) + + if with_avg_pool: + assert isinstance(selfslmlp.avgpool, nn.AdaptiveAvgPool2d) + + if use_conv: + assert isinstance(selfslmlp.mlp[0], nn.Conv2d) + assert isinstance(selfslmlp.mlp[3], nn.Conv2d) + else: + assert isinstance(selfslmlp.mlp[0], nn.Linear) + assert isinstance(selfslmlp.mlp[3], nn.Linear) + + @e2e_pytest_unit + @pytest.mark.parametrize("init_linear", ["normal", "kaiming"]) + @pytest.mark.parametrize("std", [0.01, 1.0, 10.0]) + @pytest.mark.parametrize("bias", [0.1, 0.0, 1.0]) + def test_init_weights(self, init_linear: str, std: float, bias: float): + """Test init_weights function. + + Check if weights of nn.Linear was changed except for biases. + BatchNorm weights are already set to 1, so it isn't required to be checked. + """ + + def gather_weight_mean_std(modules): + mean = [] + std = [] + for module in modules: + if isinstance(module, nn.Linear): + mean.append(module.weight.mean()) + std.append(module.weight.std()) + return mean, std + + selfslmlp = SelfSLMLP(in_channels=2, hid_channels=2, out_channels=2, use_conv=False, with_avg_pool=True) + + orig_mean, orig_std = gather_weight_mean_std(selfslmlp.modules()) + + selfslmlp.init_weights(init_linear, std, bias) + + updated_mean, updated_std = gather_weight_mean_std(selfslmlp.modules()) + + for origs, updateds in zip([orig_mean, orig_std], [updated_mean, updated_std]): + for orig, updated in zip(origs, updateds): + assert orig != updated + + @e2e_pytest_unit + @pytest.mark.parametrize("init_linear", ["undefined"]) + def test_init_weights_undefined_initialization(self, init_linear: str): + """Test init_weights function when undefined initialization is given.""" + selfslmlp = SelfSLMLP(in_channels=2, hid_channels=2, out_channels=2, use_conv=False, with_avg_pool=True) + + with pytest.raises(ValueError): + selfslmlp.init_weights(init_linear) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "inputs,norm_cfg,use_conv,with_avg_pool,expected", + [ + (torch.rand((2, 2)), dict(type="BN1d"), False, False, torch.Size([2, 2])), + (torch.rand((2, 2, 2, 2)), dict(type="BN1d"), False, True, torch.Size([2, 2])), + (torch.rand((2, 2, 2, 2)), dict(type="BN2d"), True, False, torch.Size([2, 2, 2, 2])), + (torch.rand((2, 2, 2, 2)), dict(type="BN2d"), True, True, torch.Size([2, 2, 1, 1])), + ], + ) + def test_forward_tensor( + self, inputs: torch.Tensor, norm_cfg: Dict, use_conv: bool, with_avg_pool: bool, expected: torch.Size + ): + """Test forward function for tensor.""" + selfslmlp = SelfSLMLP( + in_channels=2, + hid_channels=2, + out_channels=2, + norm_cfg=norm_cfg, + use_conv=use_conv, + with_avg_pool=with_avg_pool, + ) + + results = selfslmlp(inputs) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "inputs,norm_cfg,use_conv,with_avg_pool,expected", + [ + ([torch.rand((2, 2)), torch.rand((2, 2))], dict(type="BN1d"), False, False, torch.Size([2, 2])), + ([torch.rand((2, 2, 2, 2)), torch.rand((2, 2, 2, 2))], dict(type="BN1d"), False, True, torch.Size([2, 2])), + ( + [torch.rand((2, 2, 2, 2)), torch.rand((2, 2, 2, 2))], + dict(type="BN2d"), + True, + False, + torch.Size([2, 2, 2, 2]), + ), + ( + [torch.rand((2, 2, 2, 2)), torch.rand((2, 2, 2, 2))], + dict(type="BN2d"), + True, + True, + torch.Size([2, 2, 1, 1]), + ), + ], + ) + def test_forward_list_tuple( + self, inputs: torch.Tensor, norm_cfg: Dict, use_conv: bool, with_avg_pool: bool, expected: torch.Size + ): + """Test forward function for list or tuple.""" + selfslmlp = SelfSLMLP( + in_channels=2, + hid_channels=2, + out_channels=2, + norm_cfg=norm_cfg, + use_conv=use_conv, + with_avg_pool=with_avg_pool, + ) + + results = selfslmlp(inputs) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize("inputs", ["unsupported", 1]) + def test_forward_unsupported_format(self, inputs: str): + """Test forward function for unsupported format.""" + selfslmlp = SelfSLMLP( + in_channels=2, + hid_channels=2, + out_channels=2, + ) + + with pytest.raises(TypeError): + selfslmlp(inputs) diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/__init__.py new file mode 100644 index 00000000000..c1b25fc5a37 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.models.scheculers""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/test_schedulers.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/test_schedulers.py new file mode 100644 index 00000000000..1295b8d37eb --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/test_schedulers.py @@ -0,0 +1,128 @@ +"""Test schedulers.""" + + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from otx.algorithms.segmentation.adapters.mmseg.models.schedulers import ( + ConstantScalarScheduler, + PolyScalarScheduler, + StepScalarScheduler, +) + + +class TestSchedulers: + """Test schedulers.""" + + def test_constant_scalar_scheduler(self): + """Test constant scalar scheduler. + + Learning rate should not change over time. + """ + scheduler = ConstantScalarScheduler(scale=30.0) + assert scheduler(0, 1) == 30.0 + assert scheduler(1, 1) == 30.0 + assert scheduler(2, 10) == 30.0 + + def test_constant_scalar_scheduler_invalid_scale(self): + """Test constant scalar scheduler with invalid scale.""" + with pytest.raises(AssertionError): + ConstantScalarScheduler(scale=-1.0) + + @pytest.mark.xfail + def test_constant_scalar_scheduler_invalid_step(self): + """Test constant scalar scheduler with invalid step. + + TODO: ConstantScalarScheculer should be modified to raise this error + """ + scheduler = ConstantScalarScheduler(scale=30.0) + with pytest.raises(AssertionError): + scheduler(-1, 1) + + def test_poly_scalar_scheduler_by_epoch_false(self): + """Test poly scalar scheduler.""" + # By epoch is False + scheduler = PolyScalarScheduler( + start_scale=30.0, + end_scale=0.0, + num_iters=100, + power=0.9, + by_epoch=False, + ) + + # learning rate should decrease over time + assert scheduler(0, 1) == 30.0 + assert scheduler(1, 1) < 30.0 + assert scheduler(2, 1) < scheduler(1, 1) + assert scheduler(3, 1) < scheduler(2, 1) + + assert scheduler(50, 10) == scheduler(50, 1) # as this is not by epoch + + # learning rate should not change after num_iters + assert scheduler(100, 1) == 0.0 + assert scheduler(101, 1) == 0.0 + assert scheduler(102, 1) == 0.0 + + def test_poly_scalar_scheduler_by_epoch_true(self): + scheduler = PolyScalarScheduler( + start_scale=30.0, + end_scale=0.0, + num_iters=100, + power=0.9, + by_epoch=True, + ) + + # learning rate should decrease over time + assert scheduler(0, 1) == 30.0 + assert scheduler(1, 1) < 30.0 + assert scheduler(2, 1) < scheduler(1, 1) + assert scheduler(3, 1) < scheduler(2, 1) + + assert scheduler(50, 10) != scheduler(50, 1) # as this is by epoch + + # learning rate should not change after num_iters + assert scheduler(100, 1) == 0.0 + assert scheduler(101, 1) == 0.0 + assert scheduler(102, 1) == 0.0 + + def test_step_scalar_scheduler_by_epoch_false(self): + """Test step scalar scheduler.""" + # By epoch is False + scheduler = StepScalarScheduler( + scales=[30.0, 20.0, 10.0, 5.0], + num_iters=[2, 3, 4], + by_epoch=False, + ) + + # learning rate should decrease over time as a step function + assert scheduler(0, 1) == 30.0 + assert scheduler(1, 1) == 30.0 + assert scheduler(2, 1) < scheduler(1, 1) + assert scheduler(3, 1) < scheduler(2, 1) + + assert scheduler(50, 10) == scheduler(50, 1) + + assert scheduler(5, 2) == 5.0 + assert scheduler(5, 0) == scheduler(10, 1) + + assert scheduler(10, 1) == 5.0 # steps greater than total num_iters + + def test_step_scalar_scheduler_by_epoch_true(self): + # By epoch is True + scheduler = StepScalarScheduler( + scales=[30.0, 20.0, 10.0, 5.0], + num_iters=[2, 3, 4], + by_epoch=True, + ) + + # learning rate should decrease over time as a step function + assert scheduler(0, 1) == 30.0 + assert scheduler(1, 1) == 30.0 + assert scheduler(2, 1) < scheduler(1, 1) + assert scheduler(3, 1) < scheduler(2, 1) + + assert scheduler(9, 5) == 30.0 + assert scheduler(5, 2) == 20.0 + assert scheduler(5, 2) < scheduler(10, 11) diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py new file mode 100644 index 00000000000..9bda6deaf07 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.models.segmentors""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/test_detcon.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/test_detcon.py new file mode 100644 index 00000000000..fca2da73d4a --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/test_detcon.py @@ -0,0 +1,272 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from copy import deepcopy +from functools import partial, reduce +from typing import List, Union + +import pytest +import torch +import torch.nn as nn + +from otx.algorithms.segmentation.adapters.mmseg import DetConB, SupConDetConB +from otx.algorithms.segmentation.adapters.mmseg.models.segmentors.detcon import ( + MaskPooling, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@pytest.fixture(autouse=True) +def setup_module(monkeypatch, mocker): + class MockBackbone(nn.Module): + def __init__(self): + super().__init__() + self.pipeline1 = nn.Sequential(nn.Conv2d(3, 1, (1, 1), bias=False), nn.Conv2d(1, 1, (1, 1), bias=False)) + self.pipeline2 = nn.Sequential( + nn.Conv2d(3, 1, (1, 1), stride=2, bias=False), nn.Conv2d(1, 1, (1, 1), bias=False) + ) + + def init_weights(self, init_linear=None): + pass + + def forward(self, x): + return [self.pipeline1(x), self.pipeline2(x)] + + class MockNeck(nn.Sequential): + def __init__(self): + super().__init__(nn.Linear(2, 2, bias=False), nn.Linear(2, 2, bias=False)) + + def init_weights(self, init_linear=None): + pass + + class MockHead(nn.Sequential): + def __init__(self): + super().__init__(nn.Linear(2, 2, bias=False), nn.Linear(2, 2, bias=False)) + + def init_weights(self, init_linear=None): + pass + + def forward(self, *args, **kwargs): + return {"loss": torch.tensor(1.0)} + + class MockDecodeHead(nn.Module): + def __init__(self): + super().__init__() + self.align_corners = None + self.num_classes = 1 + self.out_channels = 1 + + def forward(self, x): + return x + + def build_mock(mock_class, *args, **kwargs): + return mock_class() + + # DetCon + monkeypatch.setattr( + "otx.algorithms.segmentation.adapters.mmseg.models.segmentors.detcon.build_backbone", + partial(build_mock, MockBackbone), + ) + monkeypatch.setattr( + "otx.algorithms.segmentation.adapters.mmseg.models.segmentors.detcon.build_neck", partial(build_mock, MockNeck) + ) + monkeypatch.setattr( + "otx.algorithms.segmentation.adapters.mmseg.models.segmentors.detcon.build_head", partial(build_mock, MockHead) + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.models.segmentors.detcon.DetConB._register_state_dict_hook" + ) + mocker.patch("otx.algorithms.segmentation.adapters.mmseg.models.segmentors.detcon.DetConB.state_dict_hook") + mocker.patch("otx.algorithms.segmentation.adapters.mmseg.models.segmentors.detcon.load_checkpoint") + + # SupCon + monkeypatch.setattr( + "mmseg.models.segmentors.encoder_decoder.builder.build_backbone", partial(build_mock, MockBackbone) + ) + monkeypatch.setattr( + "mmseg.models.segmentors.encoder_decoder.builder.build_head", partial(build_mock, MockDecodeHead) + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.models.segmentors.detcon.SupConDetConB._decode_head_forward_train", + return_value=dict(loss=1.0), + ) + + +class TestMaskPooling: + @e2e_pytest_unit + @pytest.mark.parametrize( + "masks", [torch.Tensor([[[[0, 1], [1, 2]]]]).to(torch.int64), torch.Tensor([[[0, 1], [1, 2]]]).to(torch.int64)] + ) + @pytest.mark.parametrize("num_classes", [256, 3]) + def test_pool_masks(self, masks, num_classes): + """Test pool_masks function.""" + mask_pooling = MaskPooling(num_classes=num_classes, downsample=1) + + binary_masks = mask_pooling.pool_masks(masks) + + assert binary_masks.shape[1] == num_classes + + @e2e_pytest_unit + def test_sample_masks(self): + """Test sample_masks function.""" + num_classes = 3 + masks = torch.Tensor([[[[0, 1], [1, 2]]]]).to(torch.int64) + mask_pooling = MaskPooling(num_classes=num_classes, downsample=1) + + binary_masks = mask_pooling.pool_masks(masks) + _, sampled_mask_ids = mask_pooling.sample_masks(binary_masks) + + assert len(torch.unique(sampled_mask_ids)) > 1 + assert len(torch.unique(sampled_mask_ids)) <= num_classes + + @e2e_pytest_unit + def test_forward(self): + """Test forward function.""" + num_classes = 3 + masks = torch.Tensor([[[[0, 1], [1, 2]]]]).to(torch.int64) + mask_pooling = MaskPooling(num_classes=num_classes, downsample=1) + + sampled_masks, _ = mask_pooling(masks) + + assert torch.all(sampled_masks.sum(dim=-1) == 1) + + +class TestDetConB: + """Test DetConB.""" + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.detconb = DetConB(backbone={}, neck={}, head={}, downsample=1) + + @e2e_pytest_unit + def test_init_weights(self) -> None: + """Test init_weights function.""" + for param_ol, param_tgt in zip( + self.detconb.online_backbone.parameters(), self.detconb.target_backbone.parameters() + ): + assert torch.all(param_ol == param_tgt) + assert param_ol.requires_grad + assert not param_tgt.requires_grad + + for param_ol, param_tgt in zip( + self.detconb.online_projector.parameters(), self.detconb.target_projector.parameters() + ): + assert torch.all(param_ol == param_tgt) + assert param_ol.requires_grad + assert not param_tgt.requires_grad + + @e2e_pytest_unit + def test_momentum_update(self) -> None: + """Test _momentum_update function.""" + original_params = {"backbone": [], "projector": []} + for param_tgt in self.detconb.target_backbone.parameters(): + param_tgt.data *= 2.0 + original_params["backbone"].append(deepcopy(param_tgt)) + + for param_tgt in self.detconb.target_projector.parameters(): + param_tgt.data *= 2.0 + original_params["projector"].append(deepcopy(param_tgt)) + + self.detconb._momentum_update() + + for param_ol, param_tgt, orig_tgt in zip( + self.detconb.online_backbone.parameters(), + self.detconb.target_backbone.parameters(), + original_params["backbone"], + ): + assert torch.all( + param_tgt.data == orig_tgt * self.detconb.momentum + param_ol.data * (1.0 - self.detconb.momentum) + ) + + for param_ol, param_tgt, orig_tgt in zip( + self.detconb.online_projector.parameters(), + self.detconb.target_projector.parameters(), + original_params["projector"], + ): + assert torch.all( + param_tgt.data == orig_tgt * self.detconb.momentum + param_ol.data * (1.0 - self.detconb.momentum) + ) + + @e2e_pytest_unit + @pytest.mark.parametrize("input_transform", ["resize_concat", "multiple_select", "etc"]) + @pytest.mark.parametrize( + "inputs,in_index", + [ + ([torch.ones(1, 1, 4, 4), torch.ones(1, 1, 2, 2)], [0, 1]), + ([torch.ones(1, 1, 4, 4), torch.ones(1, 1, 2, 2)], [0]), + ([torch.ones(1, 1, 4, 4)], [0]), + ], + ) + def test_transform_inputs(self, inputs: List, in_index: Union[List, int], input_transform: str) -> None: + """Test transform_inputs function.""" + setattr(self.detconb, "in_index", in_index) + setattr(self.detconb, "input_transform", input_transform) + + transformed_inputs = self.detconb.transform_inputs(inputs) + + if input_transform == "resize_concat": + assert transformed_inputs.shape[1] == len(in_index) + elif input_transform == "multiple_select": + assert len(transformed_inputs) == len(in_index) + else: + assert len(transformed_inputs) == 1 + + @e2e_pytest_unit + def test_sample_masked_feats(self) -> None: + """Test sample_masked_feats function.""" + setattr(self.detconb, "in_index", [0, 1]) + setattr(self.detconb, "input_transform", "resize_concat") + feats = [torch.randn((1, 1, 4, 4)), torch.randn((1, 1, 2, 2))] + masks = torch.randint(0, 3, (1, 1, 4, 4), dtype=torch.int64) + + proj, sampled_mask_ids = self.detconb.sample_masked_feats(feats, masks, self.detconb.online_projector) + + assert proj.ndim == 2 + assert proj.shape[0] == reduce(lambda x, y: x * y, feats[0].shape[2:]) + assert proj.shape[1] == sum([feat.shape[1] for feat in feats]) + assert proj.shape[0] == sampled_mask_ids.shape[1] + + @e2e_pytest_unit + def test_detconb_train_step(self) -> None: + """Test train_step function wraps forward and _parse_losses.""" + setattr(self.detconb, "in_index", [0, 1]) + setattr(self.detconb, "input_transform", "resize_concat") + img = torch.randn((1, 2, 3, 4, 4)) + gt_semantic_seg = torch.randint(0, 3, (1, 1, 2, 4, 4), dtype=torch.int64) + + outputs = self.detconb.train_step( + data_batch=dict(img=img, img_metas={}, gt_semantic_seg=gt_semantic_seg), optimizer=None + ) + + assert "loss" in outputs + assert "log_vars" in outputs + assert "num_samples" in outputs + + +class TestSupConDetConB: + """Test SupConDetConB.""" + + @e2e_pytest_unit + @pytest.mark.parametrize( + "img,gt_semantic_seg,expected", + [ + (torch.ones((1, 2, 3, 4, 4), dtype=torch.float32), torch.ones((1, 1, 2, 4, 4), dtype=torch.int64), True), + (torch.ones((1, 3, 4, 4), dtype=torch.float32), torch.ones((1, 1, 4, 4), dtype=torch.int64), False), + ], + ) + def test_forward_train(self, img: torch.Tensor, gt_semantic_seg: torch.Tensor, expected: bool): + """Test forward_train function.""" + supcon_detconb = SupConDetConB( + backbone={}, + neck={}, + head={}, + decode_head={}, + downsample=1, + input_transform="resize_concat", + in_index=[0, 1], + task_adapt=dict(dst_classes=1, src_classes=1), + ) + + results = supcon_detconb(img=img, img_metas=[], gt_semantic_seg=gt_semantic_seg) + + assert ("loss_detcon" in results) == expected diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/test_mean_teacher_segmentor.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/test_mean_teacher_segmentor.py new file mode 100644 index 00000000000..bd3a7c677c5 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/segmentors/test_mean_teacher_segmentor.py @@ -0,0 +1,51 @@ +import pytest +import torch + +from otx.algorithms.segmentation.adapters.mmseg import MeanTeacherSegmentor +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMeanTeacherSegmentor: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.models.segmentors.mean_teacher_segmentor.build_segmentor" + ) + self.mean_teacher = MeanTeacherSegmentor(None, 100, test_cfg=dict(), decode_head={"align_corners": False}) + self.mean_teacher.proto_net = mocker.MagicMock() + self.mean_teacher.use_prototype_head = True + self.input = torch.rand(4, 3, 512, 512) + self.gt_seg = torch.randint(low=0, high=5, size=(4, 1, 512, 512)) + + @e2e_pytest_unit + def test_decode_proto_network(self, mocker): + mocker_update_loss = mocker.patch.object(self.mean_teacher, "_update_summary_loss") + self.mean_teacher.decode_proto_network(self.input, self.gt_seg) + mocker_update_loss.assert_called_once() + # dummy input + self.mean_teacher.decode_proto_network(self.input, self.gt_seg, self.input, self.gt_seg) + + @e2e_pytest_unit + def test_generate_pseudo_labels(self, mocker): + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.models.segmentors.mean_teacher_segmentor.resize", + return_value=self.input, + ) + pl_from_teacher, reweight_unsup = self.mean_teacher.generate_pseudo_labels( + ul_w_img=self.input, ul_img_metas=dict() + ) + assert isinstance(pl_from_teacher, torch.Tensor) + assert pl_from_teacher.shape == (4, 1, 512, 512) + assert round(reweight_unsup.item(), 2) == 1.25 + + @e2e_pytest_unit + def test_forward_train(self, mocker): + loss = self.mean_teacher(self.input, img_metas=dict(), gt_semantic_seg=self.gt_seg) + assert loss is not None + self.mean_teacher.semisl_start_iter = -1 + mocker.patch.object(self.mean_teacher, "decode_proto_network") + mocker.patch.object(self.mean_teacher, "generate_pseudo_labels", return_value=(self.gt_seg, 1.0)) + ul_kwargs = dict(extra_0=dict(img=self.input, ul_w_img=self.input, img_metas=dict())) + loss = self.mean_teacher(self.input, img_metas=dict(), gt_semantic_seg=self.gt_seg, **ul_kwargs) + assert loss is not None + assert loss["sum_loss"] == 0.0 diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/utils/test_utils.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/utils/test_utils.py new file mode 100644 index 00000000000..7ff9fc63803 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/utils/test_utils.py @@ -0,0 +1,29 @@ +"""Test utils.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import torch + +from otx.algorithms.segmentation.adapters.mmseg.models.utils import LossEqualizer + + +class TestLossEqualizer: + """Test loss equalizer.""" + + @pytest.fixture(autouse=True) + def setup(self): + """Create the loss object""" + weights = {"loss_a": 1.0, "loss_b": 2.0, "loss_c": 3.0} + self.loss_equalizer = LossEqualizer(weights, momentum=0) + + def test_loss_equalizer(self): + """Test value""" + losses = { + "loss_a": torch.tensor(10.0), + "loss_b": torch.tensor(4.0), + "loss_c": torch.tensor(1.0), + } + result = {val.item() for val in self.loss_equalizer.reweight(losses).values()} + assert result == {2.5, 5.0, 7.5} diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_builder.py b/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_builder.py new file mode 100644 index 00000000000..20979e2e920 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_builder.py @@ -0,0 +1,55 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import tempfile + +import numpy as np +import torch +from nncf.torch.nncf_network import NNCFNetwork + +from otx.algorithms.common.adapters.nncf.compression import NNCFMetaState +from otx.algorithms.segmentation.adapters.mmseg.nncf.builder import build_nncf_segmentor +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.common.adapters.mmcv.nncf.test_helpers import ( + create_config, + create_dataset, + create_model, +) + + +@e2e_pytest_unit +def test_build_nncf_segmentor(): + mock_config = create_config(lib="mmseg") + model = create_model(lib="mmseg") + create_dataset(lib="mmseg") + + with tempfile.TemporaryDirectory() as tempdir: + model_path = os.path.join(tempdir, "model.bin") + state_to_build = model.state_dict() + torch.save(state_to_build, model_path) + mock_config.load_from = model_path + mock_config.nncf_config.log_dir = tempdir + ctrl, model = build_nncf_segmentor(mock_config) + assert isinstance(model, NNCFNetwork) + assert len([hook for hook in mock_config.custom_hooks if hook.type == "CompressionHook"]) == 1 + mock_config.pop("custom_hooks") + + torch.save( + { + "meta": { + "nncf_enable_compression": True, + "nncf_meta": NNCFMetaState( + data_to_build=np.zeros((50, 50, 3)), + compression_ctrl=ctrl.get_compression_state(), + state_to_build=state_to_build, + ), + }, + "state_dict": model.state_dict(), + }, + model_path, + ) + ctrl, model = build_nncf_segmentor(mock_config, model_path) + assert isinstance(model, NNCFNetwork) + assert len([hook for hook in mock_config.custom_hooks if hook.type == "CompressionHook"]) == 1 diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_patches.py b/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_patches.py new file mode 100644 index 00000000000..102bfea1be6 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_patches.py @@ -0,0 +1,14 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from mmseg.models.segmentors.base import BaseSegmentor + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_patches(): + import otx.algorithms.segmentation.adapters.mmseg.nncf.patches # noqa: F401 + + assert getattr(BaseSegmentor, "nncf_trace_context", None) is not None diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_task.py b/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_task.py new file mode 100644 index 00000000000..5e9dfe9bfb7 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/nncf/test_mmseg_nncf_task.py @@ -0,0 +1,64 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +import pytest +from mmcv.utils import Config + +from otx.algorithms.segmentation.adapters.mmseg.nncf.task import SegmentationNNCFTask +from otx.api.configuration.helper import create +from otx.api.entities.metrics import NullPerformance +from otx.api.entities.model_template import parse_model_template +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.segmentation.test_helpers import ( + DEFAULT_SEG_TEMPLATE_DIR, + generate_otx_dataset, + init_environment, +) + + +class TestOTXSegTaskNNCF: + @pytest.fixture(autouse=True) + def setup(self, otx_model, tmp_dir_path) -> None: + model_template = parse_model_template(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + task_env = init_environment(hyper_parameters, model_template) + self.model = otx_model + self.seg_nncf_task = SegmentationNNCFTask(task_env, output_path=str(tmp_dir_path)) + + @e2e_pytest_unit + def test_save_model(self, mocker): + mocker.patch("torch.load", return_value="") + self.seg_nncf_task._recipe_cfg = Config({"model": {}}) + self.seg_nncf_task.save_model(self.model) + + assert self.model.get_data("weights.pth") + assert self.model.get_data("label_schema.json") + + @e2e_pytest_unit + def test_optimize(self, mocker): + from otx.algorithms.common.adapters.mmcv.hooks import OTXLoggerHook + + self.dataset = generate_otx_dataset() + + mock_lcurve_val = OTXLoggerHook.Curve() + mock_lcurve_val.x = [0, 1] + mock_lcurve_val.y = [0.1, 0.2] + + self.seg_nncf_task._learning_curves = {f"val/{self.seg_nncf_task.metric}": mock_lcurve_val} + mocker.patch.object(SegmentationNNCFTask, "save_model") + mocker.patch.object(SegmentationNNCFTask, "_train_model") + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.nncf.task.build_nncf_segmentor", + return_value=( + mocker.MagicMock(), + mocker.MagicMock(), + ), + ) + self.seg_nncf_task.optimize(OptimizationType.NNCF, self.dataset, self.model) + + assert self.model.performance != NullPerformance() + assert self.model.performance.score.value == 0.2 diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py b/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py new file mode 100644 index 00000000000..7cb1dbdc749 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py @@ -0,0 +1,339 @@ +"""Test otx mmseg configurer.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os +from otx.algorithms.common.utils.utils import is_xpu_available + +import pytest +import tempfile +from mmcv.utils import ConfigDict + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.algorithms.segmentation.adapters.mmseg import configurer +from otx.algorithms.segmentation.adapters.mmseg.configurer import ( + SegmentationConfigurer, + IncrSegmentationConfigurer, + SemiSLSegmentationConfigurer, +) +from otx.algorithms.common.configs.configuration_enums import InputSizePreset +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.segmentation.test_helpers import ( + DEFAULT_SEG_TEMPLATE_DIR, +) + + +class TestSegmentationConfigurer: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.configurer = SegmentationConfigurer( + "segmentation", + True, + False, + {}, + None, + None, + None, + ) + self.model_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "model.py")) + self.data_pipeline_path = os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "data_pipeline.py") + self.data_cfg = OTXConfig( + { + "data": { + "train": {"otx_dataset": [], "labels": []}, + "val": {"otx_dataset": [], "labels": []}, + "test": {"otx_dataset": [], "labels": []}, + } + } + ) + + @e2e_pytest_unit + def test_configure(self, mocker): + mock_cfg_merge = mocker.patch.object(SegmentationConfigurer, "merge_configs") + mock_cfg_ckpt = mocker.patch.object(SegmentationConfigurer, "configure_ckpt") + mock_cfg_env = mocker.patch.object(SegmentationConfigurer, "configure_env") + mock_cfg_data_pipeline = mocker.patch.object(SegmentationConfigurer, "configure_data_pipeline") + mock_cfg_recipe = mocker.patch.object(SegmentationConfigurer, "configure_recipe") + mock_cfg_model = mocker.patch.object(SegmentationConfigurer, "configure_model") + mock_cfg_hook = mocker.patch.object(SegmentationConfigurer, "configure_hooks") + mock_cfg_compat_cfg = mocker.patch.object(SegmentationConfigurer, "configure_compat_cfg") + + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.model_task = "segmentation" + data_cfg = copy.deepcopy(self.data_cfg) + returned_value = self.configurer.configure(model_cfg, self.data_pipeline_path, None, "", data_cfg) + + mock_cfg_merge.assert_called_once_with(model_cfg, data_cfg, self.data_pipeline_path, None) + mock_cfg_ckpt.assert_called_once_with(model_cfg, "") + mock_cfg_env.assert_called_once_with(model_cfg) + mock_cfg_data_pipeline.assert_called_once_with(model_cfg, None, "") + mock_cfg_recipe.assert_called_once_with(model_cfg) + mock_cfg_model.assert_called_once_with(model_cfg, None, None, None) + mock_cfg_hook.assert_called_once_with(model_cfg) + mock_cfg_compat_cfg.assert_called_once_with(model_cfg) + assert returned_value == model_cfg + + @e2e_pytest_unit + def test_merge_configs(self, mocker): + mocker.patch("otx.algorithms.common.adapters.mmcv.configurer.patch_from_hyperparams", return_value=True) + self.configurer.merge_configs(self.model_cfg, self.data_cfg, self.data_pipeline_path, None) + assert self.model_cfg.data + assert self.model_cfg.data.train + assert self.model_cfg.data.val + + @e2e_pytest_unit + def test_configure_ckpt(self, mocker): + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.resume = True + + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.configurer.CheckpointLoader.load_checkpoint", + return_value={"model": None}, + ) + with tempfile.TemporaryDirectory() as tempdir: + self.configurer.configure_ckpt(model_cfg, os.path.join(tempdir, "dummy.pth")) + + @e2e_pytest_unit + def test_configure_env(self): + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + self.model_cfg.merge_from_dict(data_pipeline_cfg) + self.configurer.configure_env(self.model_cfg) + + @e2e_pytest_unit + def test_configure_device(self, mocker): + mocker.patch( + "torch.distributed.is_initialized", + return_value=True, + ) + mocker.patch( + "torch.distributed.get_world_size", + return_value=2, + ) + world_size = 2 + mocker.patch("os.environ", return_value={"LOCAL_RANK": 2}) + config = copy.deepcopy(self.model_cfg) + origin_lr = config.optimizer.lr + self.configurer.configure_device(config) + assert config.distributed is True + assert config.optimizer.lr == pytest.approx(origin_lr * world_size) + + mocker.patch( + "torch.distributed.is_initialized", + return_value=False, + ) + mocker.patch( + "torch.cuda.is_available", + return_value=False, + ) + config = copy.deepcopy(self.model_cfg) + self.configurer.configure_device(config) + assert config.distributed is False + assert config.device in ["cpu", "xpu"] + + mocker.patch( + "torch.distributed.is_initialized", + return_value=False, + ) + mocker.patch( + "torch.cuda.is_available", + return_value=True, + ) + config = copy.deepcopy(self.model_cfg) + self.configurer.configure_device(config) + assert config.distributed is False + assert config.device == "cuda" + + @e2e_pytest_unit + def test_configure_samples_per_gpu(self): + model_cfg = copy.deepcopy(self.model_cfg) + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + model_cfg.merge_from_dict(data_pipeline_cfg) + model_cfg.data.train_dataloader = ConfigDict({"samples_per_gpu": 2}) + model_cfg.data.train.otx_dataset = range(1) + self.configurer.configure_samples_per_gpu(model_cfg) + assert model_cfg.data.train_dataloader == {"samples_per_gpu": 1, "drop_last": True} + + @e2e_pytest_unit + @pytest.mark.parametrize("input_size", [None, (0, 0), (256, 256)]) + @pytest.mark.parametrize("training", [True, False]) + def test_configure_input_size(self, mocker, input_size, training): + # prepare + mock_cfg = mocker.MagicMock() + mock_input_manager_cls = mocker.patch.object(configurer, "InputSizeManager") + mock_input_manager = mock_input_manager_cls.return_value + mock_input_manager.get_trained_input_size.return_value = (32, 32) + mock_input_manager_cls.return_value = mock_input_manager + mock_base_configurer_cls = mocker.patch.object(configurer, "BaseConfigurer") + mock_base_configurer_cls.adapt_input_size_to_dataset.return_value = (64, 64) + + # execute + self.configurer.configure_input_size(mock_cfg, input_size, "ckpt/path", training=training) + + # check + if input_size is None: + mock_input_manager.set_input_size.assert_not_called() + elif input_size == (0, 0): + if training: + mock_input_manager.set_input_size.assert_called_once_with((64, 64)) + else: + mock_input_manager.set_input_size.assert_called_once_with((32, 32)) + else: + mock_input_manager.set_input_size.assert_called_once_with(input_size) + + @e2e_pytest_unit + def test_configure_fp16(self): + if is_xpu_available(): + pytest.skip("FP16 is not supported on XPU") + model_cfg = copy.deepcopy(self.model_cfg) + model_cfg.fp16 = {} + self.configurer.configure_fp16(model_cfg) + assert model_cfg.optimizer_config.type == "Fp16OptimizerHook" + + model_cfg.fp16 = {} + model_cfg.optimizer_config.type = "SAMOptimizerHook" + self.configurer.configure_fp16(model_cfg) + assert model_cfg.optimizer_config.type == "Fp16SAMOptimizerHook" + model_cfg.fp16 = {} + model_cfg.optimizer_config.type = "DummyOptimizerHook" + self.configurer.configure_fp16(model_cfg) + assert model_cfg.optimizer_config.type == "DummyOptimizerHook" + + @e2e_pytest_unit + def test_configure_model(self): + ir_options = {"ir_model_path": {"ir_weight_path": "", "ir_weight_init": ""}} + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + self.model_cfg.merge_from_dict(data_pipeline_cfg) + self.configurer.configure_model(self.model_cfg, [], [], ir_options) + assert len(self.configurer.model_classes) == 1001 + assert len(self.configurer.data_classes) == 1 + + @e2e_pytest_unit + def test_configure_task(self, mocker): + model_cfg = copy.deepcopy(self.model_cfg) + mock_cfg_classes = mocker.patch.object(SegmentationConfigurer, "configure_classes") + mock_cfg_ignore = mocker.patch.object(SegmentationConfigurer, "configure_decode_head") + self.configurer.configure_task(model_cfg) + + mock_cfg_classes.assert_called_once() + mock_cfg_ignore.assert_called_once() + + @e2e_pytest_unit + def test_configure_decode_head(self): + model_cfg = copy.deepcopy(self.model_cfg) + self.configurer.configure_decode_head(model_cfg) + if "decode_head" in model_cfg.model: + assert model_cfg.model.decode_head.loss_decode.type == "CrossEntropyLossWithIgnore" + + @e2e_pytest_unit + def test_configure_classes_replace(self, mocker): + model_cfg = copy.deepcopy(self.model_cfg) + self.configurer.task_adapt_op = "REPLACE" + mocker.patch.object(SegmentationConfigurer, "get_data_classes", return_value=["foo", "bar"]) + self.configurer.configure_classes(model_cfg) + assert "background" in self.configurer.model_classes + assert self.configurer.model_classes == ["background", "foo", "bar"] + + @e2e_pytest_unit + def test_configure_classes_merge(self, mocker): + model_cfg = copy.deepcopy(self.model_cfg) + self.configurer.task_adapt_op = "MERGE" + mocker.patch.object(SegmentationConfigurer, "get_model_classes", return_value=["foo", "bar"]) + mocker.patch.object(SegmentationConfigurer, "get_data_classes", return_value=["foo", "baz"]) + self.configurer.configure_classes(model_cfg) + assert "background" in self.configurer.model_classes + assert self.configurer.model_classes == ["background", "foo", "bar", "baz"] + + @e2e_pytest_unit + def test_configure_hooks(self): + self.configurer.override_configs = {"custom_hooks": [{"type": "LazyEarlyStoppingHook", "patience": 6}]} + self.configurer.time_monitor = [] + self.configurer.configure_hooks(self.model_cfg) + assert self.model_cfg.custom_hooks[0]["patience"] == 6 + assert self.model_cfg.custom_hooks[-2]["type"] == "CancelInterfaceHook" + assert self.model_cfg.custom_hooks[-1]["type"] == "OTXProgressHook" + assert self.model_cfg.log_config.hooks[-1]["type"] == "OTXLoggerHook" + + @e2e_pytest_unit + def test_configure_compat_cfg(self): + model_cfg = copy.deepcopy(self.model_cfg) + data_pipeline_cfg = OTXConfig.fromfile(self.data_pipeline_path) + model_cfg.merge_from_dict(data_pipeline_cfg) + model_cfg.data.train_dataloader = {} + model_cfg.data.val_dataloader = {} + model_cfg.data.test_dataloader = {} + self.configurer.configure_compat_cfg(model_cfg) + + +class TestIncrSegmentationConfigurer: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.configurer = IncrSegmentationConfigurer( + "segmentation", + True, + False, + {}, + None, + None, + None, + ) + self.model_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "model.py")) + data_pipeline_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "data_pipeline.py")) + self.model_cfg.merge_from_dict(data_pipeline_cfg) + self.data_cfg = OTXConfig( + { + "data": { + "train": {"otx_dataset": [], "labels": []}, + "val": {"otx_dataset": [], "labels": []}, + "test": {"otx_dataset": [], "labels": []}, + } + } + ) + + @e2e_pytest_unit + def test_configure_task(self, mocker): + mocker.patch.object(SegmentationConfigurer, "configure_task") + self.configurer.task_adapt_type = "default_task_adapt" + self.configurer.configure_task(self.model_cfg) + assert self.model_cfg.custom_hooks[3].type == "TaskAdaptHook" + assert self.model_cfg.custom_hooks[3].sampler_flag is False + + +class TestSemiSLSegmentationConfigurer: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.configurer = SemiSLSegmentationConfigurer( + "segmentation", + True, + False, + {}, + None, + None, + None, + ) + self.cfg = OTXConfig.fromfile(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "semisl", "model.py")) + data_pipeline_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "semisl", "data_pipeline.py")) + self.cfg.merge_from_dict(data_pipeline_cfg) + + @e2e_pytest_unit + def test_configure_data_pipeline(self, mocker): + mocker.patch("otx.algorithms.common.adapters.mmcv.semisl_mixin.build_dataset", return_value=True) + mocker.patch("otx.algorithms.common.adapters.mmcv.semisl_mixin.build_dataloader", return_value=True) + mocker.patch.object(SegmentationConfigurer, "configure_input_size", return_value=True) + + data_cfg = OTXConfig( + { + "data": { + "train": {"otx_dataset": [], "labels": []}, + "val": {"otx_dataset": [], "labels": []}, + "test": {"otx_dataset": [], "labels": []}, + "unlabeled": {"otx_dataset": [0, 1, 2, 3], "labels": []}, + } + } + ) + self.cfg.merge_from_dict(data_cfg) + self.cfg.model_task = "classification" + self.cfg.distributed = False + self.configurer.configure_data_pipeline(self.cfg, InputSizePreset.DEFAULT, "") + assert self.cfg.custom_hooks[-1]["type"] == "ComposedDataLoadersHook" diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/utils/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/__init__.py new file mode 100644 index 00000000000..2e7d4985d06 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.utils.""" +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_builder.py b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_builder.py new file mode 100644 index 00000000000..29db15ff389 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_builder.py @@ -0,0 +1,26 @@ +import pytest +from otx.algorithms.segmentation.adapters.mmseg.utils import build_scalar_scheduler, build_segmentor +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_build_scalar_scheduler(mocker): + cfg = mocker.MagicMock() + builder = mocker.patch("mmseg.models.builder.MODELS.build", return_value=True) + build_scalar_scheduler(cfg) + builder.assert_called_once_with(cfg) + + +@e2e_pytest_unit +def test_build_segmentor(mocker): + from mmcv.utils import Config + + cfg = Config({"model": {}, "load_from": "foo.pth"}) + mocker.patch("mmseg.models.build_segmentor") + load_ckpt = mocker.patch("otx.algorithms.segmentation.adapters.mmseg.utils.builder.load_checkpoint") + build_segmentor(cfg) + load_ckpt.assert_called() + + build_segmentor(cfg, is_training=True) + load_ckpt.assert_called() + assert cfg.load_from is None diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_data_utils.py b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_data_utils.py new file mode 100644 index 00000000000..791332f79a8 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_data_utils.py @@ -0,0 +1,89 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os +import os.path as osp +from tempfile import TemporaryDirectory + +import cv2 +import mmcv +import numpy as np +import pytest + +from otx.algorithms.segmentation.adapters.mmseg.utils.data_utils import ( + add_labels, + check_labels, + create_annotation_from_hard_seg_map, + create_pseudo_masks, + load_dataset_items, +) +from otx.api.entities.label import Domain, LabelEntity +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def label_entity(name="test label", id=None) -> LabelEntity: + return LabelEntity(name=name, id=id, domain=Domain.SEGMENTATION) + + +def generate_random_single_image(filename: str, width: int = 10, height: int = 10) -> None: + img: np.ndarray = np.uint8(np.random.random((height, width, 3)) * 255) + cv2.imwrite(filename, img) + + +class TestMMSegDataUtilsValidation: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + + self.hard_seg_map: np.ndarray = np.array([[1, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]]) + self.labels: list = [label_entity("class_0", id="00000000"), label_entity("class_1", id="00000001")] + + @e2e_pytest_unit + def test_create_annotation_from_hard_seg_map(self) -> None: + annotations: list = create_annotation_from_hard_seg_map(self.hard_seg_map, self.labels) + + assert len(annotations) == 1 + assert annotations[0].get_labels()[0].label.name == "class_0" + + @e2e_pytest_unit + def test_check_labels(self) -> None: + check_labels(cur_labels=self.labels, new_labels=[("class_1", None), ("class_0", None)]) + # function doesn't return anything, but throws exeption in case of failure + assert True + + @e2e_pytest_unit + def test_add_labels(self) -> None: + add_labels(cur_labels=self.labels, new_labels=[("class_2", None)]) + + assert len(self.labels) == 3 + + @e2e_pytest_unit + def test_create_pseudo_masks_fh_mode(self, mocker) -> None: + mocker.patch("cv2.imread", return_value=np.array([[1, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]])) + mocker.patch("cv2.imwrite") + mocker.patch("os.listdir", return_value=["image.jpg"]) + + ann_file_path: str = "ann_file_path" + data_root_dir: str = "data_root_dir" + + # ann_file_path dir should not exist + create_pseudo_masks(ann_file_path, data_root_dir) + + assert osp.exists(osp.join(ann_file_path, "meta.json")) + + os.remove(f"{ann_file_path}/meta.json") + os.rmdir(ann_file_path) + + @e2e_pytest_unit + def test_load_dataset_items(self) -> None: + + tmp_dir: TemporaryDirectory = TemporaryDirectory() + + generate_random_single_image(osp.join(tmp_dir.name, "image.jpg")) + generate_random_single_image(osp.join(tmp_dir.name, "image.png")) + fake_json_file: str = osp.join(tmp_dir.name, "meta.json") + mmcv.dump({"labels_map": []}, fake_json_file) + + # ann_file_path dir should exist + dataset_items: list = load_dataset_items(tmp_dir.name, tmp_dir.name) + + assert len(dataset_items) == 1 diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_exporter.py b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_exporter.py new file mode 100644 index 00000000000..1a5e342bdf6 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_exporter.py @@ -0,0 +1,36 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import build_segmentor +from otx.algorithms.common.adapters.mmdeploy.apis import NaiveExporter +from otx.algorithms.common.adapters.mmcv.tasks.exporter import Exporter +from otx.algorithms.segmentation.adapters.mmseg.utils.exporter import SegmentationExporter +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.segmentation.test_helpers import ( + DEFAULT_SEG_TEMPLATE_DIR, +) + + +@e2e_pytest_unit +def test_run(mocker): + exporter = SegmentationExporter() + model_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "model.py")) + model_cfg.work_dir = "/tmp/" + args = {"precision": "FP32", "model_builder": build_segmentor} + mocker.patch.object(Exporter, "run", return_value=True) + returned_value = exporter.run(model_cfg) + assert "model_builder" in args + assert returned_value is True + + +@e2e_pytest_unit +def test_naive_export(mocker): + exporter = SegmentationExporter() + data_cfg = OTXConfig.fromfile(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "data_pipeline.py")) + mock_export_ov = mocker.patch.object(NaiveExporter, "export2backend") + exporter.naive_export("", build_segmentor, "FP32", "OPENVINO", data_cfg) + mock_export_ov.assert_called_once() diff --git a/tests/unit/algorithms/segmentation/adapters/openvino/__init__.py b/tests/unit/algorithms/segmentation/adapters/openvino/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/openvino/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/openvino/model_wrappers/__init__.py b/tests/unit/algorithms/segmentation/adapters/openvino/model_wrappers/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/openvino/model_wrappers/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/segmentation/adapters/openvino/test_openvino_task.py b/tests/unit/algorithms/segmentation/adapters/openvino/test_openvino_task.py new file mode 100644 index 00000000000..e621bf2eaa7 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/openvino/test_openvino_task.py @@ -0,0 +1,193 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os +import pathlib + +import numpy as np +import pytest +from openvino.model_api.models import Model + +import otx.algorithms.segmentation.adapters.openvino + +from otx.algorithms.segmentation.configs.base import SegmentationConfig +from otx.algorithms.segmentation.adapters.openvino import ( + OpenVINOSegmentationInferencer, + OpenVINOSegmentationTask, +) +from otx.api.configuration.helper import create +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import LabelEntity +from otx.api.entities.metrics import Performance, ScoreMetric +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +from otx.api.utils.shape_factory import ShapeFactory +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.segmentation.test_helpers import ( + DEFAULT_SEG_TEMPLATE_DIR, + generate_otx_dataset, + generate_otx_label_schema, + init_environment, +) +from openvino.model_api.models.utils import ImageResultWithSoftPrediction + + +class TestOpenVINOSegmentationInferencer: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + model_template = parse_model_template(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + seg_params = SegmentationConfig(header=hyper_parameters.header) + label_schema = generate_otx_label_schema() + mocker.patch("otx.algorithms.segmentation.adapters.openvino.task.OpenvinoAdapter") + mocker.patch.object(Model, "create_model") + self.seg_ov_inferencer = OpenVINOSegmentationInferencer(seg_params, label_schema, "") + self.seg_ov_inferencer.model = mocker.patch("openvino.model_api.models.Model", autospec=True) + + self.fake_input = np.full((5, 1), 0.1) + + @e2e_pytest_unit + def test_predict(self, mocker): + fake_output = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=[]) + mock_converter = mocker.patch.object( + self.seg_ov_inferencer.converter, "convert_to_annotation", return_value=fake_output + ) + _, returned_value = self.seg_ov_inferencer.predict(self.fake_input) + + mock_converter.assert_called_once() + assert returned_value == fake_output + + +class TestOpenVINOSegmentationTask: + @pytest.fixture(autouse=True) + def setup(self, mocker, otx_model) -> None: + model_template = parse_model_template(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + label_schema = generate_otx_label_schema() + task_env = init_environment(hyper_parameters, model_template) + seg_params = SegmentationConfig(header=hyper_parameters.header) + mocker.patch("otx.algorithms.segmentation.adapters.openvino.task.OpenvinoAdapter") + mocker.patch.object(Model, "create_model") + seg_ov_inferencer = OpenVINOSegmentationInferencer(seg_params, label_schema, "") + + task_env.model = otx_model + mocker.patch.object(OpenVINOSegmentationTask, "load_inferencer", return_value=seg_ov_inferencer) + self.seg_ov_task = OpenVINOSegmentationTask(task_env) + + @e2e_pytest_unit + def test_infer(self, mocker): + self.dataset = generate_otx_dataset() + fake_annotation = [ + Annotation( + Polygon(points=[Point(0, 0)]), + id=0, + labels=[ScoredLabel(LabelEntity(name="fake", domain="SEGMENTATION"), probability=1.0)], + ) + ] + fake_ann_scene = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=fake_annotation) + mock_predict = mocker.patch.object( + OpenVINOSegmentationInferencer, + "predict", + return_value=( + ImageResultWithSoftPrediction(np.array(0), np.array(0), np.array(0), np.array(0)), + fake_ann_scene, + ), + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.openvino.task.get_activation_map", return_value=np.zeros((5, 1)) + ) + mocker.patch.object(ShapeFactory, "shape_produces_valid_crop", return_value=True) + updated_dataset = self.seg_ov_task.infer(self.dataset, InferenceParameters(enable_async_inference=False)) + mock_predict.assert_called() + + for updated in updated_dataset: + assert updated.annotation_scene.contains_any([LabelEntity(name="fake", domain="SEGMENTATION")]) + + @e2e_pytest_unit + def test_infer_async(self, mocker): + self.dataset = generate_otx_dataset() + fake_annotation = [ + Annotation( + Polygon(points=[Point(0, 0)]), + id=0, + labels=[ScoredLabel(LabelEntity(name="fake", domain="SEGMENTATION"), probability=1.0)], + ) + ] + fake_ann_scene = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=fake_annotation) + + def fake_enqueue_prediciton(obj, x, idx, result_handler): + result_handler(idx, fake_ann_scene, None, None) + + mock_enqueue = mocker.patch.object( + OpenVINOSegmentationInferencer, "enqueue_prediction", fake_enqueue_prediciton + ) + mocker.patch( + "otx.algorithms.segmentation.adapters.openvino.task.get_activation_map", return_value=np.zeros((5, 1)) + ) + mocker.patch.object(ShapeFactory, "shape_produces_valid_crop", return_value=True) + updated_dataset = self.seg_ov_task.infer( + self.dataset, InferenceParameters(is_evaluation=True, enable_async_inference=True) + ) + + for updated in updated_dataset: + assert updated.annotation_scene.contains_any([LabelEntity(name="fake", domain="SEGMENTATION")]) + + @e2e_pytest_unit + def test_evaluate(self, mocker): + result_set = ResultSetEntity( + model=None, + ground_truth_dataset=DatasetEntity(), + prediction_dataset=DatasetEntity(), + ) + fake_metrics = mocker.patch("otx.api.usecases.evaluation.dice.DiceAverage", autospec=True) + fake_metrics.get_performance.return_value = Performance( + score=ScoreMetric(name="fake", value=0.1), dashboard_metrics="mDice" + ) + mocker.patch.object(MetricsHelper, "compute_dice_averaged_over_pixels", return_value=fake_metrics) + self.seg_ov_task.evaluate(result_set) + + assert result_set.performance.score.value == 0.1 + + @e2e_pytest_unit + def test_deploy(self, otx_model): + output_model = copy.deepcopy(otx_model) + self.seg_ov_task.model.set_data("openvino.bin", b"foo") + self.seg_ov_task.model.set_data("openvino.xml", b"bar") + self.seg_ov_task.deploy(output_model) + + assert output_model.exportable_code is not None + + @e2e_pytest_unit + def test_optimize(self, mocker, otx_model): + def patch_save_model(model, output_xml): + with open(output_xml, "wb") as f: + f.write(b"foo") + bin_path = pathlib.Path(output_xml).parent / pathlib.Path(str(pathlib.Path(output_xml).stem) + ".bin") + with open(bin_path, "wb") as f: + f.write(b"bar") + + dataset = generate_otx_dataset() + output_model = copy.deepcopy(otx_model) + self.seg_ov_task.model.set_data("openvino.bin", b"foo") + self.seg_ov_task.model.set_data("openvino.xml", b"bar") + + mocker.patch("otx.algorithms.segmentation.adapters.openvino.task.ov.Core.read_model", autospec=True) + mocker.patch("otx.algorithms.segmentation.adapters.openvino.task.ov.save_model", new=patch_save_model) + fake_quantize = mocker.patch("otx.algorithms.segmentation.adapters.openvino.task.nncf.quantize", autospec=True) + self.seg_ov_task.optimize(OptimizationType.POT, dataset=dataset, output_model=output_model) + + fake_quantize.assert_called_once() + assert self.seg_ov_task.model.get_data("openvino.bin") + assert self.seg_ov_task.model.get_data("openvino.xml") diff --git a/tests/unit/algorithms/segmentation/adapters/test_otx_segmentation_task.py b/tests/unit/algorithms/segmentation/adapters/test_otx_segmentation_task.py new file mode 100644 index 00000000000..00e0cecb391 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/test_otx_segmentation_task.py @@ -0,0 +1,87 @@ +"""Test MMSegmentationTask.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import torch +import pytest +from mmcv import ConfigDict + +from otx.api.entities.label import LabelEntity, Domain +from otx.api.entities.id import ID +from otx.algorithms.segmentation.adapters.mmseg.task import MMSegmentationTask +from otx.algorithms.segmentation.adapters.mmseg.models.heads import otx_head_factory +from otx.api.configuration.helper import create +from otx.api.entities.model_template import ( + parse_model_template, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.segmentation.test_helpers import ( + DEFAULT_SEG_TEMPLATE_DIR, + init_environment, +) + + +class TestMMSegmentationTask: + @pytest.fixture(autouse=True) + def setup(self, mocker): + model_template = parse_model_template(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + task_env = init_environment(hyper_parameters, model_template) + self.mmseg_task = MMSegmentationTask(task_env) + self.mmseg_task._init_task() + + @e2e_pytest_unit + def test_configure(self): + cfg = self.mmseg_task.configure() + assert "work_dir" in cfg + assert "resume" in cfg + + @e2e_pytest_unit + def test_build_model(self, mocker): + mocker.patch("otx.algorithms.segmentation.adapters.mmseg.utils.builder.build_segmentor") + self.mmseg_task._recipe_cfg.model.decode_head.base_type = "FCNHead" + self.mmseg_task._recipe_cfg.model.decode_head.type = otx_head_factory + self.mmseg_task._recipe_cfg.model.task_adapt = ConfigDict( + op="REPLACE", + type="mpa", + final=["background", "target"], + src_classes=[], + dst_classes=["background", "target"], + ) + model = self.mmseg_task.build_model(self.mmseg_task._recipe_cfg, fp16=True) + assert isinstance(model, torch.nn.Module) + assert model.fp16_enabled + + @e2e_pytest_unit + def test_update_override_configurations(self): + cfg = ConfigDict(fake_key="fake_value") + self.mmseg_task.update_override_configurations(cfg) + assert "fake_key" in self.mmseg_task.override_configs + assert self.mmseg_task.override_configs == dict(fake_key="fake_value") + + @e2e_pytest_unit + def test_save_model(self, otx_model, mocker): + mocker_load = mocker.patch("torch.load", return_value="foo") + mocker_save = mocker.patch("torch.save") + self.mmseg_task.save_model(otx_model) + + mocker_load.assert_called_once() + mocker_save.assert_called_once() + + @e2e_pytest_unit + def test_label_order(self, mocker): + mocker.patch("otx.algorithms.segmentation.task.os") + mocker.patch("otx.algorithms.segmentation.task.TRAIN_TYPE_DIR_PATH") + mock_environemnt = mocker.MagicMock() + + fake_label = [] + for i in range(20): + fake_label.append(LabelEntity(name=f"class_{i}", domain=Domain.SEGMENTATION, id=ID(str(i)))) + mock_environemnt.get_labels.return_value = fake_label + del mock_environemnt.get_hyper_parameters.return_value.learning_parameters.input_size # To avoid mocking error + task = MMSegmentationTask(mock_environemnt) + + for i, label_entity in task._label_dictionary.items(): + assert label_entity.name == f"class_{i-1}" diff --git a/tests/unit/algorithms/segmentation/conftest.py b/tests/unit/algorithms/segmentation/conftest.py new file mode 100644 index 00000000000..2ea32f0969c --- /dev/null +++ b/tests/unit/algorithms/segmentation/conftest.py @@ -0,0 +1,15 @@ +import pytest + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ModelConfiguration, ModelEntity + + +@pytest.fixture +def otx_model(): + model_configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="header", description="description"), + label_schema=LabelSchemaEntity(), + ) + return ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) diff --git a/tests/unit/algorithms/segmentation/test_helpers.py b/tests/unit/algorithms/segmentation/test_helpers.py new file mode 100644 index 00000000000..bf0138ea2fe --- /dev/null +++ b/tests/unit/algorithms/segmentation/test_helpers.py @@ -0,0 +1,105 @@ +import os +import warnings + +import numpy as np + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.color import Color +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.task_environment import TaskEnvironment +from tests.test_helpers import generate_random_annotated_image + +DEFAULT_SEG_TEMPLATE_DIR = os.path.join("src/otx/algorithms/segmentation/configs", "ocr_lite_hrnet_18_mod2") +DEFAULT_RECIPE_CONFIG_PATH = "src/otx/recipes/stages/segmentation/incremental.py" + +labels_names = ("rectangle", "ellipse", "triangle") + + +def generate_otx_label_schema(labels_names=labels_names): + label_domain = Domain.SEGMENTATION + rgb = [int(i) for i in np.random.randint(0, 256, 3)] + colors = [Color(*rgb) for _ in range(len(labels_names))] + not_empty_labels = [ + LabelEntity(name=name, color=colors[i], domain=label_domain, id=i) for i, name in enumerate(labels_names) + ] + empty_label = LabelEntity( + name="Empty label", + color=Color(42, 43, 46), + is_empty=True, + domain=label_domain, + id=len(not_empty_labels), + ) + + label_schema = LabelSchemaEntity() + exclusive_group = LabelGroup(name="labels", labels=not_empty_labels, group_type=LabelGroupType.EXCLUSIVE) + empty_group = LabelGroup(name="empty", labels=[empty_label], group_type=LabelGroupType.EMPTY_LABEL) + label_schema.add_group(exclusive_group) + label_schema.add_group(empty_group) + return label_schema + + +def init_environment(params, model_template): + labels_schema = generate_otx_label_schema() + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + return environment + + +def generate_otx_dataset(number_of_images=5): + items = [] + labels_schema = generate_otx_label_schema() + labels_list = labels_schema.get_labels(False) + for i in range(0, number_of_images): + image_numpy, shapes = generate_random_annotated_image( + image_width=640, + image_height=480, + labels=labels_list, + max_shapes=20, + min_size=50, + max_size=100, + random_seed=None, + ) + # Convert all shapes to polygons + out_shapes = [] + for shape in shapes: + shape_labels = shape.get_labels(include_empty=True) + + in_shape = shape.shape + if isinstance(in_shape, Rectangle): + points = [ + Point(in_shape.x1, in_shape.y1), + Point(in_shape.x2, in_shape.y1), + Point(in_shape.x2, in_shape.y2), + Point(in_shape.x1, in_shape.y2), + ] + elif isinstance(in_shape, Ellipse): + points = [Point(x, y) for x, y in in_shape.get_evenly_distributed_ellipse_coordinates()] + elif isinstance(in_shape, Polygon): + points = in_shape.points + + out_shapes.append(Annotation(Polygon(points=points), labels=shape_labels)) + + image = Image(data=image_numpy) + annotation = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=out_shapes) + items.append(DatasetItemEntity(media=image, annotation_scene=annotation)) + warnings.resetwarnings() + + dataset = DatasetEntity(items) + + return dataset diff --git a/tests/unit/algorithms/segmentation/test_task.py b/tests/unit/algorithms/segmentation/test_task.py new file mode 100644 index 00000000000..87a4aa3ab91 --- /dev/null +++ b/tests/unit/algorithms/segmentation/test_task.py @@ -0,0 +1,134 @@ +"""Test otx segmentation task.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os + +import numpy as np +import pytest + +from otx.algorithms.segmentation.task import OTXSegmentationTask +from otx.api.configuration.helper import create +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.model_template import ( + parse_model_template, +) +from tests.unit.algorithms.segmentation.test_helpers import ( + DEFAULT_SEG_TEMPLATE_DIR, + generate_otx_dataset, + generate_otx_label_schema, + init_environment, +) +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockOTXSegmentationTask(OTXSegmentationTask): + def _infer_model(*args, **kwargs): + return dict( + classes=["background", "rectangle", "ellipse", "triangle"], + eval_predictions=[[np.random.rand(4, 128, 128)]], + feature_vectors=[np.random.rand(600, 1, 1)], + ) + + def _train_model(*args, **kwargs): + return {"final_ckpt": "dummy.pth"} + + def _explain_model(*args, **kwargs): + pass + + def _export_model(*args, **kwargs): + return { + "outputs": {"bin": f"/tmp/model.xml", "xml": f"/tmp/model.bin", "onnx": f"/tmp/model.onnx"}, + "inference_parameters": {"mean_values": "", "scale_values": ""}, + } + + +class MockModel: + class _Configuration: + def __init__(self, label_schema): + self.label_schema = label_schema + + def get_label_schema(self): + return self.label_schema + + def __init__(self): + self.model_adapters = ["weights.pth"] + self.data = np.ndarray(1) + + label_schema = generate_otx_label_schema() + + self.configuration = self._Configuration(label_schema) + + def get_data(self, name): + return self.data + + def set_data(self, *args, **kwargs): + return + + +class TestOTXSegmentationTask: + @pytest.fixture(autouse=True) + def setup(self): + model_template = parse_model_template(os.path.join(DEFAULT_SEG_TEMPLATE_DIR, "template.yaml")) + hyper_parameters = create(model_template.hyper_parameters.data) + task_env = init_environment(hyper_parameters, model_template) + + self.seg_task = MockOTXSegmentationTask(task_env) + + @e2e_pytest_unit + def test_load_model_ckpt(self, mocker): + mocker_torch_load = mocker.patch("torch.load") + self.seg_task._load_model_ckpt(MockModel()) + mocker_torch_load.assert_called_once() + + @e2e_pytest_unit + def test_train(self, mocker): + dataset = generate_otx_dataset(5) + mocker.patch("torch.load", return_value=np.ndarray([1])) + self.seg_task.train(dataset, MockModel()) + assert self.seg_task._model_ckpt == "dummy.pth" + + @e2e_pytest_unit + def test_infer(self): + dataset = generate_otx_dataset(5) + predicted_dataset = self.seg_task.infer( + dataset.with_empty_annotations(), inference_parameters=InferenceParameters(is_evaluation=False) + ) + assert predicted_dataset[0].annotation_scene.annotations[0] + + @e2e_pytest_unit + def test_evaluate(self, mocker): + class _MockScoreMetric: + def __init__(self, value): + self.value = value + + class _MockMetric: + def __init__(self): + self.overall_dice = _MockScoreMetric(1.0) + + def get_performance(self): + return 1.0 + + class _MockResultEntity: + performance = 0.0 + + mocker.patch( + "otx.algorithms.segmentation.task.MetricsHelper.compute_dice_averaged_over_pixels", + return_value=_MockMetric(), + ) + + _result_entity = _MockResultEntity() + self.seg_task.evaluate(_result_entity) + assert _result_entity.performance == 1.0 + + @e2e_pytest_unit + @pytest.mark.parametrize("export_type", [ExportType.ONNX, ExportType.OPENVINO]) + def test_export(self, otx_model, mocker, export_type): + mocker_open = mocker.patch("builtins.open") + mocker_open.__enter__.return_value = True + mocker.patch("otx.algorithms.segmentation.task.embed_ir_model_data", return_value=None) + mocker.patch("otx.algorithms.segmentation.task.embed_onnx_model_data", return_value=None) + self.seg_task.export(export_type, otx_model) + mocker_open.assert_called() diff --git a/tests/unit/algorithms/visual_prompting/__init__.py b/tests/unit/algorithms/visual_prompting/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py b/tests/unit/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/openvino/model_wrappers/test_openvino_models.py b/tests/unit/algorithms/visual_prompting/adapters/openvino/model_wrappers/test_openvino_models.py new file mode 100644 index 00000000000..e14d5b757e7 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/openvino/model_wrappers/test_openvino_models.py @@ -0,0 +1,155 @@ +"""Tests model wrappers for openvino task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Tuple, Dict, Any + +import numpy as np +import pytest +from openvino.model_api.adapters.openvino_adapter import OpenvinoAdapter +from openvino.model_api.models import ImageModel, SegmentationModel +from openvino.model_api.models.types import NumericalValue + +from otx.algorithms.visual_prompting.adapters.openvino.model_wrappers import ( + Decoder, + ImageEncoder, +) +from otx.api.entities.label import LabelEntity +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestImageEncoder: + @e2e_pytest_unit + def test_parameters(self): + """Test parameters.""" + params = ImageEncoder.parameters() + + assert params.get("resize_type").default_value == "fit_to_window" + + @e2e_pytest_unit + def test_preproces(self, mocker): + """Test preprocess.""" + mocker.patch.object(ImageModel, "__init__") + image_encoder = ImageEncoder("adapter") + fake_inputs = np.ones((4, 4, 3)) + image_encoder.h, image_encoder.w, image_encoder.c = fake_inputs.shape + image_encoder.image_blob_name = "images" + image_encoder.resize_type = "fit_to_window" + + dict_inputs, meta = image_encoder.preprocess(fake_inputs) + + assert dict_inputs["images"].shape == (1, 4, 4, 3) + assert meta["original_shape"] == (4, 4, 3) + assert meta["resized_shape"] == (4, 4, 3) + assert "resize_type" in meta + assert meta["resize_type"] == "fit_to_window" + + +class TestDecoder: + @pytest.fixture(autouse=True) + def setup(self, mocker): + mocker.patch.object(SegmentationModel, "__init__") + mocker_model_adapter = mocker.Mock(spec=OpenvinoAdapter) + self.decoder = Decoder(mocker_model_adapter) + self.decoder.image_size = 6 + + @e2e_pytest_unit + def test_parameters(self): + """Test parameters.""" + params = Decoder.parameters() + + assert isinstance(params.get("image_size"), NumericalValue) + assert params.get("image_size").default_value == 1024 + + @e2e_pytest_unit + def test_get_outputs(self): + """Test _get_outputs.""" + results = self.decoder._get_outputs() + + assert "upscaled_masks" == results + + @e2e_pytest_unit + @pytest.mark.parametrize( + "prompts,expected", + [ + ( + { + "bboxes": [np.array([[1, 1], [2, 2]])], + "points": [], + "labels": {"bboxes": [1]}, + "original_size": (4, 4), + }, + { + "point_coords": (1, 2, 2), + "point_labels": (1, 2), + }, + ), + ( + {"bboxes": [], "points": [np.array([[1, 1]])], "labels": {"points": [1]}, "original_size": (4, 4)}, + { + "point_coords": (1, 1, 2), + "point_labels": (1, 1), + }, + ), + ], + ) + def test_preprocess(self, prompts: Dict[str, Any], expected: Dict[str, Any]): + """Test preprocess""" + results = self.decoder.preprocess(prompts, {}) + + assert isinstance(results, list) + assert "point_coords" in results[0] + assert results[0]["point_coords"].shape == expected["point_coords"] + assert "point_labels" in results[0] + assert results[0]["point_labels"].shape == expected["point_labels"] + assert "mask_input" in results[0] + assert "has_mask_input" in results[0] + assert "orig_size" in results[0] + + @e2e_pytest_unit + def test_apply_coords(self): + """Test _apply_coords.""" + coords = np.array([[[1, 1], [2, 2]]]) + original_size = (12, 12) + + results = self.decoder._apply_coords(coords, original_size) + + assert results.shape == (1, 2, 2) + assert np.all(results == np.array([[[0.5, 0.5], [1.0, 1.0]]])) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "old_h,old_w,image_size,expected", + [ + (4, 3, 6, (6, 5)), + (3, 4, 6, (5, 6)), + ], + ) + def test_get_preprocess_shape(self, old_h: int, old_w: int, image_size: int, expected: Tuple[int]): + """Test _get_preprocess_shape.""" + result = self.decoder._get_preprocess_shape(old_h, old_w, image_size) + + assert result == expected + + @e2e_pytest_unit + def test_get_inputs(self): + """Test _get_inputs.""" + self.decoder.inputs = {"images": np.ones((1, 4, 4, 3))} + + returned_value = self.decoder._get_inputs() + + assert returned_value[0] == ["images"] + + @e2e_pytest_unit + def test_postprocess(self, mocker): + """Test postprocess.""" + self.decoder.output_blob_name = "upscaled_masks" + self.decoder.mask_threshold = 0.0 + self.decoder.blur_strength = 2 + fake_output = {"upscaled_masks": np.ones((4, 4)), "scores": 0.1} + fake_metadata = {"original_size": np.array([[6, 6]]), "label": mocker.Mock(spec=LabelEntity)} + returned_value = self.decoder.postprocess(outputs=fake_output, meta=fake_metadata) + + assert isinstance(returned_value, tuple) diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/__init__.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/test_inference_callback.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/test_inference_callback.py new file mode 100644 index 00000000000..8053f3dcc6c --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/test_inference_callback.py @@ -0,0 +1,179 @@ +"""Tests inference callback for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +from typing import Any + +import numpy as np +import pytest +import torch +from bson import ObjectId + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.callbacks import ( + InferenceCallback, + ZeroShotInferenceCallback, +) +from otx.api.entities.annotation import Annotation +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.visual_prompting.test_helpers import ( + generate_visual_prompting_dataset, + generate_otx_label_schema, +) + + +class TestInferenceCallback: + @pytest.fixture(autouse=True) + def setup(self, mocker, monkeypatch): + monkeypatch.setattr( + "otx.api.utils.segmentation_utils.create_annotation_from_segmentation_map", + lambda *args, **kwargs: Annotation( + shape=Image(data=np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]), size=(3, 3)), + labels=[ScoredLabel(label=LabelEntity("foreground", domain=Domain.VISUAL_PROMPTING), probability=0.9)], + id=ID(ObjectId()), + ), + ) + monkeypatch.setattr( + "otx.api.utils.segmentation_utils.create_hard_prediction_from_soft_prediction", + lambda *args, **kwargs: np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]), + ) + + self.mocker_trainer = mocker.patch("pytorch_lightning.Trainer") + self.mocker_lightning_module = mocker.patch("pytorch_lightning.LightningModule") + + @e2e_pytest_unit + @pytest.mark.parametrize( + "use_mask,expected", + [ + (True, np.ones((3, 3), dtype=np.uint8)), + ( + False, + [ + Point(0.0, 0.0), + Point(0.0, 0.5), + Point(0.0, 1.0), + Point(0.5, 1.0), + Point(1.0, 1.0), + Point(1.0, 0.5), + Point(1.0, 0.0), + Point(0.5, 0.0), + ], + ), + ], + ) + def test_on_predict_epoch_end(self, use_mask: bool, expected: Any): + """Test on_predict_epoch_end.""" + otx_dataset = generate_visual_prompting_dataset(use_mask=use_mask) + inference_callback = InferenceCallback(otx_dataset) + + outputs = [ + [ + { + "masks": [torch.Tensor([[[0, 1, 0], [1, 1, 1], [0, 1, 0]]])], + "iou_predictions": [torch.Tensor([[0.9]])], + "labels": [ + { + "bboxes": [ + ScoredLabel( + label=LabelEntity("foreground", domain=Domain.VISUAL_PROMPTING), probability=0.0 + ) + ], + } + ], + } + ] + ] + + inference_callback.on_predict_epoch_end(self.mocker_trainer, self.mocker_lightning_module, outputs) + predicted_otx_dataset = inference_callback.otx_dataset + + assert len(predicted_otx_dataset) == 4 + dataset_item = predicted_otx_dataset[0] + assert len(dataset_item.annotation_scene.annotations) == 1 + + annotation = dataset_item.annotation_scene.annotations[0] + assert isinstance(annotation, Annotation) + if use_mask: + assert isinstance(annotation.shape, Image) + assert (annotation.shape.numpy == expected).all() + else: + assert isinstance(annotation.shape, Polygon) + assert annotation.shape.points == expected + assert annotation.get_labels()[0].name == "foreground" + assert annotation.get_labels()[0].probability == 0.5 + + +class TestZeroShotInferenceCallback: + @pytest.fixture(autouse=True) + def setup(self, mocker, monkeypatch): + monkeypatch.setattr( + "otx.api.utils.segmentation_utils.create_annotation_from_segmentation_map", + lambda *args, **kwargs: Annotation( + shape=Image(data=np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]), size=(3, 3)), + labels=[ScoredLabel(label=LabelEntity("foreground", domain=Domain.VISUAL_PROMPTING), probability=0.9)], + id=ID(ObjectId()), + ), + ) + monkeypatch.setattr( + "otx.api.utils.segmentation_utils.create_hard_prediction_from_soft_prediction", + lambda *args, **kwargs: np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]), + ) + + self.mocker_trainer = mocker.patch("pytorch_lightning.Trainer") + self.mocker_lightning_module = mocker.patch("pytorch_lightning.LightningModule") + + @e2e_pytest_unit + @pytest.mark.parametrize( + "expected", + [ + [ + Point(0.0, 0.0), + Point(0.0, 0.5), + Point(0.0, 1.0), + Point(0.5, 1.0), + Point(1.0, 1.0), + Point(1.0, 0.5), + Point(1.0, 0.0), + Point(0.5, 0.0), + ] + ], + ) + def test_on_predict_epoch_end(self, expected: Any): + """Test on_predict_epoch_end.""" + otx_dataset = generate_visual_prompting_dataset(use_mask=False) + labels_schema = generate_otx_label_schema() + inference_callback = ZeroShotInferenceCallback(otx_dataset, labels_schema) + + outputs = [ + [ + [ + { + 0: [ + torch.Tensor([[0, 1, 0], [1, 1, 1], [0, 1, 0]]).to(torch.uint8), + ] + } + ] + ] + ] + + inference_callback.on_predict_epoch_end(self.mocker_trainer, self.mocker_lightning_module, outputs) + predicted_otx_dataset = inference_callback.otx_dataset + + assert len(predicted_otx_dataset) == 4 + dataset_item = predicted_otx_dataset[0] + assert len(dataset_item.annotation_scene.annotations) == 1 + + annotation = dataset_item.annotation_scene.annotations[0] + assert isinstance(annotation, Annotation) + + # TODO (sungchul): consider use_mask + assert isinstance(annotation.shape, Polygon) + assert annotation.shape.points == expected + assert annotation.get_labels()[0].name == "rectangle" + assert annotation.get_labels()[0].probability == 0.5 diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/config/__init__.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/config/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/config/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/config/test_visual_prompting_config.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/config/test_visual_prompting_config.py new file mode 100644 index 00000000000..32f3e9c6fe7 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/config/test_visual_prompting_config.py @@ -0,0 +1,82 @@ +"""Tests the methods in config.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Optional + +import pytest +from omegaconf import DictConfig, OmegaConf + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.config.visual_prompting_config import ( + get_visual_promtping_config, + update_visual_prompting_config, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "model_checkpoint,resume_from_checkpoint", + [ + (None, None), + ("model_checkpoint.ckpt", None), + (None, "resume_from_checkpoint.ckpt"), + ], +) +@pytest.mark.parametrize("mode", ["train", "inference"]) +def test_get_visual_promtping_config( + tmpdir, mocker, mode: str, model_checkpoint: Optional[str], resume_from_checkpoint: Optional[str] +): + """Test get_visual_promtping_config.""" + task_name = "sam_vit_b" + mocker_otx_config = mocker.patch("otx.api.configuration.configurable_parameters.ConfigurableParameters") + config_dir = str(tmpdir.mkdir("visual_prompting_training_test")) + config = get_visual_promtping_config( + task_name=task_name, + otx_config=mocker_otx_config, + config_dir=config_dir, + mode=mode, + model_checkpoint=model_checkpoint, + resume_from_checkpoint=resume_from_checkpoint, + ) + + assert isinstance(config, DictConfig) + assert config.get("dataset", False) + assert config.get("model", False) + assert config.get("optimizer", False) + assert config.get("callback", False) + assert config.get("trainer", False) + if mode == "train": + if model_checkpoint: + assert config.get("model").get("checkpoint", None) == model_checkpoint + else: + assert config.get("model").get("checkpoint", None) != model_checkpoint + assert config.get("trainer").get("resume_from_checkpoint", None) == resume_from_checkpoint + + +@e2e_pytest_unit +def test_update_visual_prompting_config(): + """Test update_visual_prompting_config.""" + otx_config = OmegaConf.create( + { + "groups": ["learning_parameters", "pot_parameters", "postprocessing", "algo_backend"], + "learning_parameters": {"parameters": ["param1"], "param1": "updated_value1"}, + "pot_parameters": {"parameters": ["param2"], "param2": "updated_value2"}, + "postprocessing": {"parameters": ["param3"], "param3": "updated_value3"}, + "algo_backend": {"parameters": ["param4"], "param4": "updated_value4"}, + "parameters": [], + } + ) + visual_prompting_config = OmegaConf.create( + {"param1": "value1", "param2": "value2", "param3": "value3", "param4": "value4", "param5": "value5"} + ) + + update_visual_prompting_config(visual_prompting_config, otx_config) + + assert visual_prompting_config["param1"] == "updated_value1" + assert visual_prompting_config["param2"] == "updated_value2" + assert visual_prompting_config["param3"] == "updated_value3" + assert visual_prompting_config["param4"] == "updated_value4" + assert visual_prompting_config["param5"] == "value5" diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/__init__.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/__init__.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/test_sam_transforms.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/test_sam_transforms.py new file mode 100644 index 00000000000..dca279d5175 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/test_sam_transforms.py @@ -0,0 +1,97 @@ +"""Tests sam transforms used for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import torch +import numpy as np +from typing import Tuple +import pytest +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.pipelines.sam_transforms import ( + ResizeLongestSide, +) + +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestResizeLongestSide: + @pytest.fixture(autouse=True) + def setup(self): + self.resize_longest_side = ResizeLongestSide(8) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "image,expected", + [ + (np.zeros((2, 4, 3), dtype=np.uint8), (4, 8, 3)), + (np.zeros((12, 16, 3), dtype=np.uint8), (6, 8, 3)), + ], + ) + def test_apply_image(self, image: np.ndarray, expected: Tuple[int, int, int]): + """Test apply_image.""" + results = self.resize_longest_side.apply_image(image, self.resize_longest_side.target_length) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "coords,original_size,expected", + [ + (np.array([[1, 1], [2, 2]]), (4, 4), np.array([[2, 2], [4, 4]])), + (np.array([[4, 4], [8, 8]]), (16, 16), np.array([[2, 2], [4, 4]])), + ], + ) + @pytest.mark.parametrize("type", ["numpy", "torch"]) + def test_apply_coords(self, coords: np.ndarray, original_size: Tuple[int, int], expected: np.ndarray, type: str): + """Test apply_coords.""" + if type == "torch": + coords = torch.tensor(coords) + original_size = torch.tensor(original_size) + expected = torch.tensor(expected) + result = self.resize_longest_side.apply_coords(coords, original_size, self.resize_longest_side.target_length) + + if type == "torch": + assert isinstance(result, torch.Tensor) + assert torch.equal(result, expected) + else: + assert isinstance(result, np.ndarray) + assert np.array_equal(result, expected) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "boxes,original_size,expected", + [ + (np.array([[1, 1, 2, 2], [2, 2, 3, 3]]), (4, 4), np.array([[2, 2, 4, 4], [4, 4, 6, 6]])), + (np.array([[4, 4, 8, 8], [8, 8, 12, 12]]), (16, 16), np.array([[2, 2, 4, 4], [4, 4, 6, 6]])), + ], + ) + @pytest.mark.parametrize("type", ["numpy", "torch"]) + def test_apply_boxes(self, boxes: np.ndarray, original_size: Tuple[int, int], expected: np.ndarray, type: str): + """Test apply_boxes.""" + if type == "torch": + boxes = torch.tensor(boxes) + original_size = torch.tensor(original_size) + expected = torch.tensor(expected) + result = self.resize_longest_side.apply_boxes(boxes, original_size, self.resize_longest_side.target_length) + + if type == "torch": + assert isinstance(result, torch.Tensor) + assert torch.equal(result, expected) + else: + assert isinstance(result, np.ndarray) + assert np.array_equal(result, expected) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "oldh,oldw,expected", + [ + (3, 4, (6, 8)), + (12, 16, (6, 8)), + ], + ) + def test_get_preprocess_shape(self, oldh: int, oldw: int, expected: Tuple[int, int]): + """Test get_preprocess_shape.""" + result = self.resize_longest_side.get_preprocess_shape(oldh, oldw, self.resize_longest_side.target_length) + + assert result == expected diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/test_transforms.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/test_transforms.py new file mode 100644 index 00000000000..80890c5a155 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/test_transforms.py @@ -0,0 +1,182 @@ +"""Tests transforms used for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Any, Dict, Tuple, List + +import pytest +import torch +from torch import Tensor +import numpy as np +from torchvision.transforms import Normalize + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.pipelines.transforms import ( + MultipleInputsCompose, + Pad, + collate_fn, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "batch,expected", + [ + ( + [ + { + "index": 0, + "images": Tensor([1, 2, 3]), + "bboxes": Tensor([[1, 2, 3, 4], [5, 6, 7, 8]]), + "points": torch.zeros((0, 2)), + "gt_masks": [Tensor([1, 2, 3])], + "original_size": Tensor([1, 3]), + "path": [], + "labels": [], + }, + { + "index": 1, + "images": Tensor([4, 5, 6]), + "bboxes": Tensor([[9, 10, 11, 12]]), + "points": torch.zeros((0, 2)), + "gt_masks": [Tensor([4, 5, 6])], + "original_size": Tensor([1, 3]), + "path": [], + "labels": [], + }, + ], + { + "index": [0, 1], + "images": Tensor([[1, 2, 3], [4, 5, 6]]), + "bboxes": [Tensor([[1, 2, 3, 4], [5, 6, 7, 8]]), Tensor([[9, 10, 11, 12]])], + "points": [None, None], + "gt_masks": [Tensor([[1, 2, 3]]), Tensor([[4, 5, 6]])], + "original_size": [Tensor([1, 3]), Tensor([1, 3])], + "path": [[], []], + "labels": [[], []], + }, + ), + ( + [ + { + "index": 0, + "images": Tensor([1, 2, 3]), + "bboxes": torch.zeros((0, 4)), + "points": Tensor([[1, 1]]), + "gt_masks": [Tensor([1, 2, 3])], + "original_size": Tensor([1, 3]), + "path": [], + "labels": [], + }, + { + "index": 1, + "images": Tensor([4, 5, 6]), + "bboxes": torch.zeros((0, 4)), + "points": Tensor([[2, 2]]), + "gt_masks": [Tensor([4, 5, 6])], + "original_size": Tensor([1, 3]), + "path": [], + "labels": [], + }, + ], + { + "index": [0, 1], + "images": Tensor([[1, 2, 3], [4, 5, 6]]), + "bboxes": [None, None], + "points": [Tensor([[1, 1]]), Tensor([[2, 2]])], + "gt_masks": [Tensor([[1, 2, 3]]), Tensor([[4, 5, 6]])], + "original_size": [Tensor([1, 3]), Tensor([1, 3])], + "path": [[], []], + "labels": [[], []], + }, + ), + ], +) +def test_collate_fn(batch: List[Dict[str, Any]], expected: Dict[str, Any]): + """Test collate_fn.""" + results = collate_fn(batch) + + assert results["index"] == expected["index"] + assert torch.all(results["images"] == expected["images"]) + for r, e in zip(results["bboxes"], expected["bboxes"]): + if r is not None and e is not None: + assert torch.all(r == e) + + for r, e in zip(results["points"], expected["points"]): + if r is not None and e is not None: + assert torch.all(r == e) + + assert len(results["gt_masks"]) == len(expected["gt_masks"]) + for r, e in zip(results["gt_masks"], expected["gt_masks"]): + assert torch.all(r == e) + + for r, e in zip(results["original_size"], expected["original_size"]): + assert torch.all(r == e) + + assert results["path"] == expected["path"] + assert results["labels"] == expected["labels"] + + +class TestPad: + @e2e_pytest_unit + @pytest.mark.parametrize( + "item,expected", + [ + ( + dict( + images=torch.zeros((3, 4, 6)), + gt_masks=[torch.zeros((4, 6))], + bboxes=[[1, 1, 3, 3]], + points=[[1, 1, 2, 2]], + ), + ((3, 6, 6), [(4, 6)], [[1, 1, 3, 3]], [[1, 1, 2, 2]]), + ), + ( + dict(images=torch.zeros((3, 4, 6)), gt_masks=[torch.zeros((4, 6))], bboxes=[[1, 1, 3, 3]], points=None), + ((3, 6, 6), [(4, 6)], [[1, 1, 3, 3]], None), + ), + ], + ) + def test_call(self, item: Dict[str, Any], expected: Tuple[Any]): + """Test __call__.""" + pad_transform = Pad() + expected_images_shape, expected_gt_masks_shape, expected_bboxes, expected_points = expected + + result = pad_transform(item) + + assert result["images"].shape == expected_images_shape + assert len(result["gt_masks"]) == len(expected_gt_masks_shape) + assert all(gt_mask.shape == shape for gt_mask, shape in zip(result["gt_masks"], expected_gt_masks_shape)) + assert result["bboxes"] == expected_bboxes + assert result["points"] == expected_points + + +class TestMultipleInputsCompose: + @e2e_pytest_unit + def test_call(self, mocker): + """Test __call__.""" + transform1_mock = mocker.Mock() + transform2_mock = mocker.Mock(spec=Normalize) + + # Create a sample item + item = {"images": Tensor([1, 2, 3])} + + # Set the return values of the mocked transforms + transform1_mock.return_value = item + transform2_mock.return_value = item["images"] + + # Instantiate the MultipleInputsCompose object with mocked transforms + transforms = [transform1_mock, transform2_mock] + multiple_inputs_compose = MultipleInputsCompose(transforms) + + # Call the __call__ method + results = multiple_inputs_compose(item) + + # Assert that each transform is called appropriately + transform1_mock.assert_called_once() + transform2_mock.assert_called_once() + + # Assert the output of the __call__ method + assert results == {"images": transform2_mock.return_value} diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/test_dataset.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/test_dataset.py new file mode 100644 index 00000000000..6e5211c1899 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/test_dataset.py @@ -0,0 +1,316 @@ +"""Tests dataset and datamodule used for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +from typing import Callable +import pytest +from torch.utils.data import DataLoader +from torchvision import transforms +from otx.algorithms.common.configs.training_base import TrainType + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset import ( + OTXVisualPromptingDataModule, + OTXZeroShotVisualPromptingDataset, + OTXVisualPromptingDataset, + convert_polygon_to_mask, + generate_bbox, + generate_bbox_from_mask, + get_transform, +) +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.pipelines import ( + MultipleInputsCompose, + Pad, + ResizeLongestSide, + collate_fn, +) +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.shapes.polygon import Point, Polygon +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.visual_prompting.test_helpers import ( + MockDatasetConfig, + generate_visual_prompting_dataset, +) + + +@pytest.fixture +def transform(): + return lambda items: items + + +@pytest.fixture +def image_size(): + return 32 + + +@pytest.fixture +def mean(): + return [1.0, 1.0, 1.0] + + +@pytest.fixture +def std(): + return [0.0, 0.0, 0.0] + + +@pytest.fixture +def dataset_polygon() -> DatasetEntity: + """Set dataset with polygon.""" + return generate_visual_prompting_dataset(use_mask=False) + + +@pytest.fixture +def dataset_mask() -> DatasetEntity: + """Set dataset with mask.""" + return generate_visual_prompting_dataset(use_mask=True) + + +@e2e_pytest_unit +def test_get_transform(image_size, mean, std): + """Test get_transform.""" + transform = get_transform(image_size=image_size, mean=mean, std=std) + + assert isinstance(transform, MultipleInputsCompose) + assert isinstance(transform.transforms[0], ResizeLongestSide) + assert transform.transforms[0].target_length == 32 + assert isinstance(transform.transforms[1], Pad) + assert isinstance(transform.transforms[2], transforms.Normalize) + assert transform.transforms[2].mean == mean + assert transform.transforms[2].std == std + + +@e2e_pytest_unit +def test_convert_polygon_to_mask(mocker) -> None: + """Test convert_polygon_to_mask.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset.get_transform", + return_value=transform, + ) + + polygon = Polygon(points=[Point(x=0.1, y=0.1), Point(x=0.2, y=0.2), Point(x=0.3, y=0.3)]) + width = 100 + height = 100 + + mask = convert_polygon_to_mask(polygon, width, height) + + assert isinstance(mask, np.ndarray) + assert mask.shape == (height, width) + assert mask.sum() == 21 + + +@e2e_pytest_unit +def test_generate_bbox(mocker) -> None: + """Test generate_bbox.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset.get_transform", + return_value=transform, + ) + + x1, y1, x2, y2 = 10, 20, 30, 40 + width = 100 + height = 100 + + bbox = generate_bbox(x1, y1, x2, y2, width, height) + + assert isinstance(bbox, list) + assert len(bbox) == 4 + assert bbox[0] >= 0 and bbox[0] <= width + assert bbox[1] >= 0 and bbox[1] <= height + assert bbox[2] >= 0 and bbox[2] <= width + assert bbox[3] >= 0 and bbox[3] <= height + + +@e2e_pytest_unit +def test_generate_bbox_from_mask(mocker) -> None: + """Test generate_bbox_from_mask.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset.get_transform", + return_value=transform, + ) + + gt_mask = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) + width = 3 + height = 3 + + bbox = generate_bbox_from_mask(gt_mask, width, height) + + assert isinstance(bbox, list) + assert len(bbox) == 4 + assert bbox[0] >= 0 and bbox[0] <= width + assert bbox[1] >= 0 and bbox[1] <= height + assert bbox[2] >= 0 and bbox[2] <= width + assert bbox[3] >= 0 and bbox[3] <= height + + +class TestOTXVIsualPromptingDataset: + @e2e_pytest_unit + def test_len(self, mocker, dataset_polygon, transform, image_size, mean, std) -> None: + """Test __len__.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset.get_transform", + return_value=transform, + ) + otx_dataset = OTXVisualPromptingDataset("testing", dataset_polygon, image_size, mean, std) + assert len(otx_dataset) == 4 + + @e2e_pytest_unit + @pytest.mark.parametrize("use_mask", [False, True]) + def test_getitem( + self, mocker, dataset_polygon, dataset_mask, transform, image_size, mean, std, use_mask: bool + ) -> None: + """Test __getitem__.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset.get_transform", + return_value=transform, + ) + dataset = dataset_mask if use_mask else dataset_polygon + otx_dataset = OTXVisualPromptingDataset("testing", dataset, image_size, mean, std) + + item = otx_dataset[0] + + # Check the returned item's keys + expected_keys = {"index", "original_size", "images", "path", "gt_masks", "bboxes", "points", "labels"} + assert set(item.keys()) == expected_keys + + # Check specific values in the item + assert item["index"] == 0 + assert (item["images"] == dataset[0].media.numpy).all() + assert np.all(item["original_size"] == dataset[0].media.numpy.shape[:2]) + assert item["path"] == dataset[0].media.path + assert isinstance(item["gt_masks"], list) + assert isinstance(item["gt_masks"][0], np.ndarray) + assert isinstance(item["bboxes"], np.ndarray) + assert len(item["points"]) == 0 + + +class TestOTXZeroShotVisualPromptingDataset: + """Test OTXZeroShotVisualPromptingDataset. + + To be updated. + """ + + @e2e_pytest_unit + @pytest.mark.parametrize("use_mask", [False, True]) + def test_getitem( + self, mocker, dataset_polygon, dataset_mask, transform, image_size, mean, std, use_mask: bool + ) -> None: + """Test __getitem__.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset.get_transform", + return_value=transform, + ) + dataset = dataset_mask if use_mask else dataset_polygon + otx_dataset = OTXZeroShotVisualPromptingDataset("testing", dataset, image_size, mean, std) + + item = otx_dataset[0] + + # Check the returned item's keys + expected_keys = {"index", "original_size", "images", "path", "gt_masks", "bboxes", "points", "labels"} + assert set(item.keys()) == expected_keys + + # Check specific values in the item + assert item["index"] == 0 + assert (item["images"] == dataset[0].media.numpy).all() + assert np.all(item["original_size"] == dataset[0].media.numpy.shape[:2]) + assert item["path"] == dataset[0].media.path + assert isinstance(item["gt_masks"], list) + assert isinstance(item["gt_masks"][0], np.ndarray) + assert isinstance(item["bboxes"], np.ndarray) + assert len(item["points"]) == 0 + + +class TestOTXVisualPromptingDataModule: + @pytest.fixture + def set_datamodule(self) -> Callable: + def datamodule(train_type: TrainType = TrainType.Incremental) -> OTXVisualPromptingDataModule: + dataset = generate_visual_prompting_dataset() + + # Create a mock config + config = MockDatasetConfig() + + # Create an instance of OTXVisualPromptingDataModule + return OTXVisualPromptingDataModule(config, dataset, train_type) + + return datamodule + + @e2e_pytest_unit + def test_init_zeroshot(self, set_datamodule): + """Test __init__ when train_type is TrainType.Zeroshot.""" + datamodule = set_datamodule(train_type=TrainType.Zeroshot) + + assert datamodule.config.get("train_batch_size") == 1 + assert "use_point" in datamodule.kwargs + assert "use_bbox" in datamodule.kwargs + + @e2e_pytest_unit + def test_setup(self, mocker, set_datamodule) -> None: + """Test setup.""" + datamodule = set_datamodule() + mocker.patch.object(datamodule, "summary", return_value=None) + + datamodule.setup() + + assert isinstance(datamodule.train_dataset, OTXVisualPromptingDataset) + assert isinstance(datamodule.val_dataset, OTXVisualPromptingDataset) + + @e2e_pytest_unit + def test_train_dataloader(self, mocker, set_datamodule) -> None: + """Test train_dataloader.""" + datamodule = set_datamodule() + mocker.patch.object(datamodule, "summary", return_value=None) + datamodule.setup(stage="fit") + + # Call the train_dataloader method + dataloader = datamodule.train_dataloader() + + assert isinstance(dataloader, DataLoader) + assert dataloader.batch_size == datamodule.config.train_batch_size + assert dataloader.num_workers == datamodule.config.num_workers + assert dataloader.collate_fn == collate_fn + + @e2e_pytest_unit + def test_val_dataloader(self, mocker, set_datamodule) -> None: + """Test val_dataloader.""" + datamodule = set_datamodule() + mocker.patch.object(datamodule, "summary", return_value=None) + datamodule.setup(stage="fit") + + # Call the val_dataloader method + dataloader = datamodule.val_dataloader() + + assert isinstance(dataloader, DataLoader) + assert dataloader.batch_size == datamodule.config.val_batch_size + assert dataloader.num_workers == datamodule.config.num_workers + assert dataloader.collate_fn == collate_fn + + @e2e_pytest_unit + def test_test_dataloader(self, mocker, set_datamodule) -> None: + """Test test_dataloader.""" + datamodule = set_datamodule() + mocker.patch.object(datamodule, "summary", return_value=None) + datamodule.setup(stage="test") + + # Call the test_dataloader method + dataloader = datamodule.test_dataloader() + + assert isinstance(dataloader, DataLoader) + assert dataloader.batch_size == datamodule.config.test_batch_size + assert dataloader.num_workers == datamodule.config.num_workers + assert dataloader.collate_fn == collate_fn + + @e2e_pytest_unit + def test_predict_dataloader(self, set_datamodule) -> None: + """Test predict_dataloader.""" + datamodule = set_datamodule() + datamodule.setup(stage="predict") + + # Call the predict_dataloader method + dataloader = datamodule.predict_dataloader() + + assert isinstance(dataloader, DataLoader) + assert dataloader.batch_size == 1 + assert dataloader.num_workers == datamodule.config.num_workers + assert dataloader.collate_fn == collate_fn diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/__init__.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/__init__.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/test_tiny_vit.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/test_tiny_vit.py new file mode 100644 index 00000000000..b31ea3db87d --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/test_tiny_vit.py @@ -0,0 +1,237 @@ +"""Tests TinyViT.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import torch +import torch.nn as nn + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.backbones.tiny_vit import ( + Conv2d_BN, + ConvLayer, + DropPath, + LayerNorm2d, + TinyViT, + MBConv, + BasicLayer, + TinyViTBlock, + Mlp, + Attention, + PatchEmbed, + PatchMerging, + build_tiny_vit, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestConv2d_BN: + def setup(self): + self.conv2d_bn = Conv2d_BN(a=1, b=1) + + @e2e_pytest_unit + def test_init(self): + """Test __init__.""" + assert isinstance(self.conv2d_bn.c, nn.Conv2d) + assert isinstance(self.conv2d_bn.bn, nn.BatchNorm2d) + + @e2e_pytest_unit + def test_fuse(self): + """Test fuse.""" + m = self.conv2d_bn.fuse() + + tmp_w = self.conv2d_bn.bn.weight / (self.conv2d_bn.bn.running_var + self.conv2d_bn.bn.eps) ** 0.5 + new_w = self.conv2d_bn.c.weight * tmp_w[:, None, None, None] + new_b = self.conv2d_bn.bn.bias - self.conv2d_bn.bn.running_mean * tmp_w + + assert torch.isclose(m.weight, new_w) + assert torch.isclose(m.bias, new_b) + + +class TestDropPath: + @e2e_pytest_unit + def test_repr(self, drop_prob: float = 1.0): + """Test __repr__.""" + drop_path = DropPath(drop_prob=drop_prob) + + assert f"(drop_prob={drop_prob})" in repr(drop_path) + + +class TestPatchEmbed: + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + patch_embed = PatchEmbed(in_chans=3, embed_dim=4, resolution=6, activation=nn.Identity) + input_tensor = torch.rand(1, 3, 6, 6) + + results = patch_embed(input_tensor) + + assert results.shape == (1, 4, 2, 2) + + +class TestMBConv: + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + mbconv = MBConv(in_chans=3, out_chans=3, expand_ratio=1.0, activation=nn.Identity, drop_path=1.0) + input_tensor = torch.rand(1, 3, 24, 24) + + results = mbconv(input_tensor) + + assert results.shape == (1, 3, 24, 24) + + +class TestPatchMerging: + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + patch_merging = PatchMerging(input_resolution=(6, 6), dim=3, out_dim=4, activation=nn.Identity) + input_tensor = torch.rand(1, 3, 6, 6) + + results = patch_merging(input_tensor) + + assert results.shape == (1, 9, 4) + + +class TestConvLayer: + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + conv_layer = ConvLayer(dim=3, input_resolution=(6, 6), depth=1, activation=nn.Identity) + input_tensor = torch.rand(1, 3, 6, 6) + + results = conv_layer(input_tensor) + + assert results.shape == (1, 3, 6, 6) + + +class TestMlp: + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + mlp = Mlp(in_features=4, hidden_features=5, out_features=6) + input_tensor = torch.rand(1, 4) + + results = mlp(input_tensor) + + assert results.shape == (1, 6) + + +class TestAttention: + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + attention = Attention(dim=4, key_dim=4, num_heads=1, attn_ratio=1, resolution=(2, 2)) + input_tensor = torch.rand(9, 4, 4) + + results = attention(input_tensor) + + assert results.shape == (9, 4, 4) + + +class TestTinyViTBlock: + def setup(self): + self.dim = 4 + self.input_resolution = (6, 6) + self.num_heads = 1 + self.window_size = 2 + self.mlp_ratio = 1.0 + self.tiny_vit_block = TinyViTBlock( + dim=self.dim, + input_resolution=self.input_resolution, + num_heads=self.num_heads, + window_size=self.window_size, + mlp_ratio=self.mlp_ratio, + ) + + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + input_tensor = torch.rand(1, 36, 4) + + results = self.tiny_vit_block(input_tensor) + + assert results.shape == (1, 36, 4) + + @e2e_pytest_unit + def test_extra_repr(self): + """Test extra_repr.""" + ( + f"dim={self.dim}, input_resolution={self.input_resolution}, num_heads={self.num_heads}, " + f"window_size={self.window_size}, mlp_ratio={self.mlp_ratio}" + ) == self.tiny_vit_block.extra_repr() + + +class TestBasicLayer: + def setup(self): + self.dim = 4 + self.input_resolution = (6, 6) + self.depth = 1 + self.basic_layer = BasicLayer( + dim=self.dim, input_resolution=self.input_resolution, depth=self.depth, num_heads=1, window_size=2 + ) + + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + input_tensor = torch.rand(1, 36, 4) + + results = self.basic_layer(input_tensor) + + assert results.shape == (1, 36, 4) + + @e2e_pytest_unit + def test_extra_repr(self): + """Test extra_repr.""" + assert ( + f"dim={self.dim}, input_resolution={self.input_resolution}, depth={self.depth}" + == self.basic_layer.extra_repr() + ) + + +class TestLayerNorm2d: + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + layer_norm_2d = LayerNorm2d(num_channels=4) + input_tensor = torch.rand(1, 4, 4, 4) + + results = layer_norm_2d(input_tensor) + + assert results.shape == (1, 4, 4, 4) + + +class TestTinyViT: + def setup(self): + self.tiny_vit = TinyViT( + img_size=1024, + num_classes=1, + embed_dims=[64, 128, 160, 320], + depths=[2, 2, 6, 2], + num_heads=[2, 4, 5, 10], + window_sizes=[7, 7, 14, 7], + drop_path_rate=0.0, + layer_lr_decay=2.0, + ) + + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + input_tensor = torch.rand(1, 3, 1024, 1024) + + results = self.tiny_vit(input_tensor) + + assert results.shape == (1, 256, 64, 64) + + +@e2e_pytest_unit +def test_build_tiny_vit(mocker): + """Test build_tiny_vit.""" + mocker_vit = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.backbones.tiny_vit.TinyViT" + ) + + _ = build_tiny_vit(img_size=1024) + + mocker_vit.assert_called_once() diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/test_vit.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/test_vit.py new file mode 100644 index 00000000000..397b7f34818 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/backbones/test_vit.py @@ -0,0 +1,198 @@ +"""Tests Vision Transformers.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from typing import Tuple + +import pytest +import torch +import torch.nn as nn + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.backbones.vit import ( + Attention, + Block, + PatchEmbed, + ViT, + add_decomposed_rel_pos, + build_vit, + get_rel_pos, + window_partition, + window_unpartition, +) +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.utils import ( + MLPBlock, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestViT: + @pytest.fixture(autouse=True) + def setup(self): + self.embed_dim = 8 + self.num_heads = 2 + self.depth = 2 + self.vit = ViT( + img_size=4, patch_size=2, embed_dim=self.embed_dim, depth=self.depth, num_heads=self.num_heads, out_chans=4 + ) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert isinstance(self.vit.patch_embed, PatchEmbed) + assert isinstance(self.vit.blocks, nn.ModuleList) + assert len(self.vit.blocks) == self.depth + assert isinstance(self.vit.neck, nn.Sequential) + + @e2e_pytest_unit + @pytest.mark.parametrize("inputs,expected", [(torch.empty((1, 3, 4, 4)), (1, 4, 2, 2))]) + def test_forward(self, inputs: torch.Tensor, expected: Tuple[int]): + """Test forward.""" + results = self.vit.forward(inputs) + + assert results.shape == expected + + +class TestBlock: + @pytest.fixture(autouse=True) + def setup(self): + self.dim = 8 + self.num_heads = 2 + self.block = Block(dim=self.dim, num_heads=self.num_heads) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert isinstance(self.block.attn, Attention) + assert isinstance(self.block.mlp, MLPBlock) + + @e2e_pytest_unit + @pytest.mark.parametrize("inputs,expected", [(torch.empty((1, 4, 4, 8)), (1, 4, 4, 8))]) + def test_forward(self, inputs: torch.Tensor, expected: Tuple[int]): + """Test forward.""" + results = self.block.forward(inputs) + + assert results.shape == expected + + +class TestAttention: + @pytest.fixture(autouse=True) + def setup(self): + self.dim = 8 + self.num_heads = 2 + self.attention = Attention(dim=self.dim, num_heads=self.num_heads) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert isinstance(self.attention.qkv, nn.Linear) + assert isinstance(self.attention.proj, nn.Linear) + + @e2e_pytest_unit + @pytest.mark.parametrize("inputs,expected", [(torch.empty((1, 4, 4, 8)), (1, 4, 4, 8))]) + def test_forward(self, inputs: torch.Tensor, expected: Tuple[int]): + """Test forward.""" + results = self.attention.forward(inputs) + + assert results.shape == expected + + +@e2e_pytest_unit +@pytest.mark.parametrize("inputs,window_size,expected", [(torch.empty((1, 4, 4, 4)), 2, ((4, 2, 2, 4), (4, 4)))]) +def test_window_partition(inputs: torch.Tensor, window_size: int, expected: Tuple[int]): + """Test window_partition.""" + results = window_partition(inputs, window_size) + windows, (Hp, Wp) = results + + assert windows.shape == expected[0] + assert (Hp, Wp) == expected[1] + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "windows,window_size,pad_hw,hw,expected", [(torch.empty((2, 2, 2, 2)), 2, (2, 2), (4, 4), (2, 2, 2, 2))] +) +def test_window_unpartition( + windows: torch.Tensor, window_size: int, pad_hw: Tuple[int, int], hw: Tuple[int, int], expected: Tuple[int] +): + """Test window_unpartition.""" + results = window_unpartition(windows, window_size, pad_hw, hw) + + assert results.shape == expected + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "q_size,k_size,rel_pos,expected", + [(2, 2, torch.empty((1, 2, 2)), (2, 2, 4)), (2, 2, torch.empty((3, 2, 2)), (2, 2, 2, 2))], +) +def test_get_rel_pos(q_size: int, k_size: int, rel_pos: torch.Tensor, expected: Tuple[int]): + """Test get_rel_pos.""" + results = get_rel_pos(q_size, k_size, rel_pos) + + assert results.shape == expected + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "attn,q,rel_pos_h,rel_pos_w,q_size,k_size,expected", + [ + ( + torch.empty((1, 4, 4)), + torch.empty((1, 4, 1)), + torch.empty((1, 2, 2, 2)), + torch.empty((1, 2, 2, 2)), + (2, 2), + (2, 2), + (1, 4, 4), + ) + ], +) +def test_add_decomposed_rel_pos( + attn: torch.Tensor, + q: torch.Tensor, + rel_pos_h: torch.Tensor, + rel_pos_w: torch.Tensor, + q_size: Tuple[int, int], + k_size: Tuple[int, int], + expected: Tuple[int], +): + """Test add_decomposed_rel_pos.""" + results = add_decomposed_rel_pos(attn, q, rel_pos_h, rel_pos_w, q_size, k_size) + + assert results.shape == expected + + +class TestPatchEmbed: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.kernel_size = (2, 2) + self.stride = (2, 2) + self.embed_dim = 8 + self.patch_embed = PatchEmbed(kernel_size=self.kernel_size, stride=self.stride, embed_dim=self.embed_dim) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert isinstance(self.patch_embed.proj, nn.Conv2d) + + @e2e_pytest_unit + @pytest.mark.parametrize("inputs,expected", [(torch.empty((8, 3, 2, 2)), (8, 1, 1, 8))]) + def test_forward(self, inputs: torch.Tensor, expected: Tuple[int]): + """Test forward.""" + results = self.patch_embed.forward(torch.empty((8, 3, 2, 2))) + + assert results.shape == expected + + +@e2e_pytest_unit +@pytest.mark.parametrize("backbone", ["vit_b", "vit_l", "vit_h"]) +def test_build_vit(mocker, backbone: str): + """Test build_vit.""" + mocker_vit = mocker.patch("otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.backbones.vit.ViT") + + _ = build_vit(backbone=backbone, image_size=1024) + + mocker_vit.assert_called_once() diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/__init__.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/test_sam_mask_decoder.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/test_sam_mask_decoder.py new file mode 100644 index 00000000000..24d7e2f58c4 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/decoders/test_sam_mask_decoder.py @@ -0,0 +1,263 @@ +"""Tests sam mask decoder used for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Tuple + +import pytest +import torch +import torch.nn as nn + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.decoders.sam_mask_decoder import ( + MLP, + Attention, + SAMMaskDecoder, + TwoWayAttentionBlock, + TwoWayTransformer, +) +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.utils import ( + MLPBlock, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSAMMaskDecoder: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.num_multimask_outputs = 3 + self.sam_mask_decoder = SAMMaskDecoder( + transformer_dim=8, + transformer_cfg=dict(depth=2, embedding_dim=8, mlp_dim=8, num_heads=2), + num_multimask_outputs=self.num_multimask_outputs, + iou_head_depth=1, + iou_head_hidden_dim=8, + ) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert isinstance(self.sam_mask_decoder.transformer, TwoWayTransformer) + assert isinstance(self.sam_mask_decoder.iou_token, nn.Embedding) + assert isinstance(self.sam_mask_decoder.mask_tokens, nn.Embedding) + assert isinstance(self.sam_mask_decoder.output_upscaling, nn.Sequential) + assert isinstance(self.sam_mask_decoder.output_hypernetworks_mlps, nn.ModuleList) + assert len(self.sam_mask_decoder.output_hypernetworks_mlps) == self.num_multimask_outputs + 1 + assert isinstance(self.sam_mask_decoder.iou_prediction_head, MLP) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "image_embeddings,image_pe,sparse_prompt_embeddings,dense_prompt_embeddings,multimask_output,expected", + [ + ( + torch.empty((1, 8, 2, 2)), + torch.empty((1, 8, 2, 2)), + torch.empty((1, 4, 8)), + torch.empty((1, 8, 2, 2)), + False, + ((1, 1, 8, 8), (1, 1)), + ), + ( + torch.empty((1, 8, 2, 2)), + torch.empty((1, 8, 2, 2)), + torch.empty((1, 4, 8)), + torch.empty((1, 8, 2, 2)), + True, + ((1, 3, 8, 8), (1, 3)), + ), + ], + ) + def test_forward( + self, + image_embeddings: torch.Tensor, + image_pe: torch.Tensor, + sparse_prompt_embeddings: torch.Tensor, + dense_prompt_embeddings: torch.Tensor, + multimask_output: bool, + expected: Tuple[Tuple[int]], + ): + """Test forward.""" + results = self.sam_mask_decoder.forward( + image_embeddings, image_pe, sparse_prompt_embeddings, dense_prompt_embeddings, multimask_output + ) + masks, iou_pred = results + + assert masks.shape == expected[0] + assert iou_pred.shape == expected[1] + + @e2e_pytest_unit + @pytest.mark.parametrize( + "image_embeddings,image_pe,sparse_prompt_embeddings,dense_prompt_embeddings,expected", + [ + ( + torch.empty((1, 8, 2, 2)), + torch.empty((1, 8, 2, 2)), + torch.empty((1, 4, 8)), + torch.empty((1, 8, 2, 2)), + ((1, 4, 8, 8), (1, 4)), + ) + ], + ) + def test_predict_masks( + self, + image_embeddings: torch.Tensor, + image_pe: torch.Tensor, + sparse_prompt_embeddings: torch.Tensor, + dense_prompt_embeddings: torch.Tensor, + expected: Tuple[Tuple[int]], + ): + """Test predict_masks.""" + results = self.sam_mask_decoder.predict_masks( + image_embeddings, image_pe, sparse_prompt_embeddings, dense_prompt_embeddings + ) + masks, iou_pred = results + + assert masks.shape == expected[0] + assert iou_pred.shape == expected[1] + + +class TestMLP: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.mlp = MLP(input_dim=4, hidden_dim=4, output_dim=4, num_layers=2) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert len(self.mlp.layers) == 2 + + @e2e_pytest_unit + @pytest.mark.parametrize("inputs,expected", [(torch.empty((1, 1, 4)), (1, 1, 4))]) + def test_forward(self, inputs: torch.Tensor, expected: Tuple[int]): + """Test forward.""" + results = self.mlp.forward(inputs) + + assert results.shape == expected + + +class TestTwoWayTransformer: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.depth = 2 + self.embedding_dim = 8 + self.num_heads = 2 + self.two_way_transformer = TwoWayTransformer( + depth=self.depth, embedding_dim=self.embedding_dim, num_heads=self.num_heads, mlp_dim=self.embedding_dim + ) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert len(self.two_way_transformer.layers) == self.depth + assert isinstance(self.two_way_transformer.final_attn_token_to_image, Attention) + assert isinstance(self.two_way_transformer.norm_final_attn, nn.LayerNorm) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "image_embedding,image_pe,point_embedding,expected", + [(torch.empty((1, 8, 2, 2)), torch.empty((1, 8, 2, 2)), torch.empty((1, 1, 8)), ((1, 1, 8), (1, 4, 8)))], + ) + def test_forward( + self, + image_embedding: torch.Tensor, + image_pe: torch.Tensor, + point_embedding: torch.Tensor, + expected: Tuple[Tuple[int]], + ): + """Test forward.""" + results = self.two_way_transformer.forward(image_embedding, image_pe, point_embedding) + queries, keys = results + + assert queries.shape == expected[0] + assert keys.shape == expected[1] + + +class TestTwoWayAttentionBlock: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.embedding_dim = 8 + self.num_heads = 2 + self.two_way_attention_block = TwoWayAttentionBlock( + embedding_dim=self.embedding_dim, num_heads=self.num_heads, mlp_dim=self.embedding_dim + ) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert isinstance(self.two_way_attention_block.self_attn, Attention) + assert isinstance(self.two_way_attention_block.norm1, nn.LayerNorm) + assert isinstance(self.two_way_attention_block.cross_attn_token_to_image, Attention) + assert isinstance(self.two_way_attention_block.norm2, nn.LayerNorm) + assert isinstance(self.two_way_attention_block.mlp, MLPBlock) + assert isinstance(self.two_way_attention_block.norm3, nn.LayerNorm) + assert isinstance(self.two_way_attention_block.norm4, nn.LayerNorm) + assert isinstance(self.two_way_attention_block.cross_attn_image_to_token, Attention) + + @e2e_pytest_unit + @pytest.mark.parametrize("queries,keys,query_pe,key_pe", [[torch.empty((1, 8, 8)) for _ in range(4)]]) + def test_forward(self, queries: torch.Tensor, keys: torch.Tensor, query_pe: torch.Tensor, key_pe: torch.Tensor): + """Test forward.""" + results = self.two_way_attention_block.forward(queries, keys, query_pe, key_pe) + queries, keys = results + + assert queries.shape == (1, 8, 8) + assert keys.shape == (1, 8, 8) + + +class TestAttention: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.embedding_dim = 8 + self.num_heads = 2 + self.attention = Attention(embedding_dim=self.embedding_dim, num_heads=self.num_heads) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert isinstance(self.attention.q_proj, nn.Linear) + assert self.attention.q_proj.in_features == self.embedding_dim + assert isinstance(self.attention.k_proj, nn.Linear) + assert self.attention.k_proj.in_features == self.embedding_dim + assert isinstance(self.attention.v_proj, nn.Linear) + assert self.attention.v_proj.in_features == self.embedding_dim + assert isinstance(self.attention.out_proj, nn.Linear) + assert self.attention.out_proj.out_features == self.embedding_dim + + @e2e_pytest_unit + @pytest.mark.parametrize( + "x,expected", + [ + (torch.empty((1, 4, 4)), (1, 2, 4, 2)), + (torch.empty((1, 2, 2)), (1, 2, 2, 1)), + ], + ) + def test_separate_heads(self, x: torch.Tensor, expected: Tuple[int]): + """Test _separate_heads.""" + results = self.attention._separate_heads(x, self.num_heads) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "x,expected", + [ + (torch.empty((1, 2, 4, 2)), (1, 4, 4)), + (torch.empty((1, 2, 2, 1)), (1, 2, 2)), + ], + ) + def test_recombine_heads(self, x: torch.Tensor, expected: Tuple[int]): + """Test _recombine_heads.""" + results = self.attention._recombine_heads(x) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "q,k,v,expected", [(torch.empty((1, 1, 8)), torch.empty((1, 1, 8)), torch.empty((1, 1, 8)), (1, 1, 8))] + ) + def test_forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, expected: Tuple[int]): + """Test forward.""" + results = self.attention.forward(q, k, v) + + assert results.shape == expected diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/__init__.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/test_sam_image_encoder.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/test_sam_image_encoder.py new file mode 100644 index 00000000000..66ae6958f0b --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/test_sam_image_encoder.py @@ -0,0 +1,43 @@ +"""Tests sam image encoder used for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.encoders.sam_image_encoder import SAMImageEncoder +import pytest +from omegaconf import DictConfig +import torch.nn as nn +import torch + + +class MockBackbone(nn.Module): + def __init__(self, *args, **kwargs): + super().__init__() + self.backbone = nn.Linear(1, 1) + + def forward(self, *args, **kwargs): + return torch.Tensor([[1]]) + + +class TestSAMImageEncoder: + @pytest.fixture() + def config(self, mocker) -> DictConfig: + return DictConfig(dict(image_size=1024)) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "backbone,expected", + [ + ("tiny_vit", "TinyViT"), + ("vit_b", "ViT"), + ], + ) + def test_new(self, config: DictConfig, backbone: str, expected: str) -> None: + """Test __new__.""" + config.update({"backbone": backbone}) + + sam_image_encoder = SAMImageEncoder(config) + + assert sam_image_encoder.__class__.__name__ == expected diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/test_sam_prompt_encoder.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/test_sam_prompt_encoder.py new file mode 100644 index 00000000000..6430eca9e4a --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/encoders/test_sam_prompt_encoder.py @@ -0,0 +1,176 @@ +"""Tests sam prompt encoder used for visual prompting task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Optional, Tuple + +import pytest +import torch +import torch.nn as nn + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.encoders.sam_prompt_encoder import ( + PositionEmbeddingRandom, + SAMPromptEncoder, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSAMPromptEncoder: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.image_embedding_size = 4 + self.input_image_size = (4, 4) + self.prompt_encoder = SAMPromptEncoder( + embed_dim=4, + image_embedding_size=(self.image_embedding_size, self.image_embedding_size), + input_image_size=self.input_image_size, + mask_in_chans=4, + ) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert isinstance(self.prompt_encoder.pe_layer, PositionEmbeddingRandom) + assert isinstance(self.prompt_encoder.point_embeddings, nn.ModuleList) + assert isinstance(self.prompt_encoder.not_a_point_embed, nn.Embedding) + assert isinstance(self.prompt_encoder.mask_input_size, tuple) + assert isinstance(self.prompt_encoder.mask_downscaling, nn.Sequential) + assert isinstance(self.prompt_encoder.no_mask_embed, nn.Embedding) + + @e2e_pytest_unit + def test_get_dense_pe(self): + """Test get_dense_pe.""" + results = self.prompt_encoder.get_dense_pe() + + assert results.shape == (1, self.image_embedding_size, self.image_embedding_size, self.image_embedding_size) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "points,labels,pad,expected", + [ + (torch.ones((1, 2, 2), dtype=torch.float32), torch.Tensor([[0, 1]]), True, (1, 3, 4)), + (torch.ones((1, 2, 2), dtype=torch.float32), torch.Tensor([[0, 1]]), False, (1, 2, 4)), + ], + ) + def test_embed_points(self, points: torch.Tensor, labels: torch.Tensor, pad: bool, expected: Tuple[int]): + """Test _embed_points.""" + results = self.prompt_encoder._embed_points(points, labels, pad) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize("boxes,expected", [(torch.Tensor([[0, 0, 1, 1]]), (1, 2, 4))]) + def test_embed_boxes(self, boxes: torch.Tensor, expected: Tuple[int]): + """Test _embed_boxes.""" + results = self.prompt_encoder._embed_boxes(boxes) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize("mask,expected", [(torch.zeros((1, 4, 4)), (4, 1, 1))]) + def test_embed_masks(self, mask: torch.Tensor, expected: Tuple[int]): + """Test _embed_masks.""" + results = self.prompt_encoder._embed_masks(mask) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "points,boxes,masks,expected", + [ + ((torch.tensor([1]), torch.tensor([1])), None, None, 1), + (None, torch.Tensor([[0, 0, 1, 1]]), None, 1), + (None, None, torch.zeros((1, 2, 2)), 1), + (None, None, None, 1), + ], + ) + def test_get_batch_size( + self, + points: Optional[Tuple[torch.Tensor, torch.Tensor]], + boxes: Optional[torch.Tensor], + masks: Optional[torch.Tensor], + expected: int, + ): + """Test _get_batch_size.""" + results = self.prompt_encoder._get_batch_size(points, boxes, masks) + + assert results == expected + + @e2e_pytest_unit + @pytest.mark.parametrize("device", ["cpu", "cuda"]) + def test_get_device(self, device: str): + """Test _get_device.""" + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA is not available") + + self.prompt_encoder.point_embeddings.to(device) + + results = self.prompt_encoder._get_device() + + assert device == str(results).split(":")[0] + + @e2e_pytest_unit + @pytest.mark.parametrize( + "points,boxes,masks,expected", + [ + ( + (torch.ones((1, 2, 2), dtype=torch.float32), torch.Tensor([[0, 1]])), + None, + None, + ((1, 3, 4), (1, 4, 4, 4)), + ), + (None, torch.Tensor([[0, 0, 1, 1]]), None, ((1, 2, 4), (1, 4, 4, 4))), + (None, None, torch.zeros((1, 4, 4)), ((1, 0, 4), (4, 1, 1))), + (None, None, None, ((1, 0, 4), (1, 4, 4, 4))), + ], + ) + def test_forward( + self, + points: Optional[Tuple[torch.Tensor, torch.Tensor]], + boxes: Optional[torch.Tensor], + masks: Optional[torch.Tensor], + expected: Tuple[int], + ): + """Test forward.""" + results = self.prompt_encoder.forward(points, boxes, masks) + sparse_embeddings, dense_embeddings = results + + assert sparse_embeddings.shape == expected[0] + assert dense_embeddings.shape == expected[1] + + +class TestPositionEmbeddingRandom: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.position_embedding_random = PositionEmbeddingRandom(num_pos_feats=4) + + @e2e_pytest_unit + def test_init(self): + """Test init.""" + assert hasattr(self.position_embedding_random, "positional_encoding_gaussian_matrix") + assert self.position_embedding_random.positional_encoding_gaussian_matrix.shape == (2, 4) + + @e2e_pytest_unit + def test_pe_encoding(self): + """Test _pe_encoding.""" + results = self.position_embedding_random._pe_encoding(torch.ones((2, 2, 2), dtype=torch.float32)) + + assert results.shape == (2, 2, 8) + + @e2e_pytest_unit + def test_forward(self): + """Test forward.""" + results = self.position_embedding_random.forward(size=(2, 2)) + + assert results.shape == (8, 2, 2) + + @e2e_pytest_unit + def test_forward_with_coords(self): + """Test forward_with_coords.""" + results = self.position_embedding_random.forward_with_coords( + coords_input=torch.ones((2, 2, 2), dtype=torch.float32), image_size=(2, 2) + ) + + assert results.shape == (2, 2, 8) diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/__init__.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/test_segment_anything.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/test_segment_anything.py new file mode 100644 index 00000000000..eecddf412a1 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/test_segment_anything.py @@ -0,0 +1,567 @@ +"""Tests Segment Anything.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from collections import OrderedDict +from typing import Tuple + +import pytest +import torch +import torch.nn as nn +from omegaconf import DictConfig +from torch import Tensor + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything import ( + SegmentAnything, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.visual_prompting.test_helpers import MockImageEncoder, MockPromptEncoder, MockMaskDecoder + + +class TestSegmentAnything: + @pytest.fixture(autouse=True) + def setup(self, monkeypatch) -> None: + monkeypatch.setattr( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SAMImageEncoder", + MockImageEncoder, + ) + monkeypatch.setattr( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SAMPromptEncoder", + MockPromptEncoder, + ) + monkeypatch.setattr( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SAMMaskDecoder", + MockMaskDecoder, + ) + + self.base_config = DictConfig( + dict( + model=dict( + backbone="vit_b", + image_size=1024, + freeze_image_encoder=True, + freeze_prompt_encoder=True, + freeze_mask_decoder=False, + loss_type="sam", + checkpoint=None, + mask_threshold=0.0, + return_logits=False, + ) + ) + ) + + @e2e_pytest_unit + @pytest.mark.parametrize("backbone", ["vit_b", "resnet"]) + def test_set_models(self, mocker, backbone: str) -> None: + """Test set_models.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.freeze_networks" + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.set_metrics" + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + + config = self.base_config.copy() + config.model.update(dict(backbone=backbone)) + if backbone == "resnet": + with pytest.raises(NotImplementedError): + sam = SegmentAnything(config) + else: + # backbone == vit_b + sam = SegmentAnything(config) + + assert isinstance(sam.image_encoder, nn.Linear) + assert isinstance(sam.prompt_encoder, MockPromptEncoder) + assert isinstance(sam.mask_decoder, MockMaskDecoder) + + @e2e_pytest_unit + @pytest.mark.parametrize("freeze_image_encoder", [True, False]) + @pytest.mark.parametrize("freeze_prompt_encoder", [True, False]) + @pytest.mark.parametrize("freeze_mask_decoder", [True, False]) + def test_freeze_networks( + self, mocker, freeze_image_encoder: bool, freeze_prompt_encoder: bool, freeze_mask_decoder: bool + ): + """Test freeze_networks.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.set_metrics" + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + + config = self.base_config.copy() + config.model.update( + dict( + freeze_image_encoder=freeze_image_encoder, + freeze_prompt_encoder=freeze_prompt_encoder, + freeze_mask_decoder=freeze_mask_decoder, + ) + ) + sam = SegmentAnything(config) + + for param in sam.image_encoder.parameters(): + if freeze_image_encoder: + assert param.requires_grad == False + else: + assert param.requires_grad == True + + for param in sam.prompt_encoder.parameters(): + if freeze_prompt_encoder: + assert param.requires_grad == False + else: + assert param.requires_grad == True + + for param in sam.mask_decoder.parameters(): + if freeze_mask_decoder: + assert param.requires_grad == False + else: + assert param.requires_grad == True + + @e2e_pytest_unit + @pytest.mark.parametrize("loss_type", ["sam", "medsam"]) + def test_set_metrics(self, mocker, loss_type: str): + """Test set_metrics.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.set_models" + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.freeze_networks" + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + + config = self.base_config.copy() + config.model.update(dict(loss_type=loss_type)) + sam = SegmentAnything(config) + + if loss_type == "sam": + assert "train_loss_focal" in sam.train_metrics + assert "train_loss_iou" in sam.train_metrics + + elif loss_type == "medsam": + assert "train_loss_ce" in sam.train_metrics + + else: + assert 0 + + @e2e_pytest_unit + @pytest.mark.parametrize( + "state_dict", + [ + OrderedDict( + [ + ("image_encoder.weight", torch.ones(4, 4)), + ("image_encoder.bias", torch.ones(4)), + ("prompt_encoder.layer.weight", Tensor([[1.0]])), + ("prompt_encoder.layer.bias", Tensor([1.0])), + ("mask_decoder.layer.weight", Tensor([[1.0]])), + ("mask_decoder.layer.bias", Tensor([1.0])), + ] + ), + ], + ) + def test_load_checkpoint_with_state_dict(self, mocker, state_dict: OrderedDict): + """Test load_checkpoint with state_dict.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.freeze_networks" + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.set_metrics" + ) + + sam = SegmentAnything(self.base_config, state_dict=state_dict) + sam_state_dict = sam.state_dict() + + for k, v in state_dict.items(): + assert k in sam_state_dict + assert torch.all(v == sam_state_dict[k]) + + @e2e_pytest_unit + def test_load_checkpoint_with_url(self, mocker): + """Test load_checkpoint with url.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.freeze_networks" + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.set_metrics" + ) + mocker_load_state_dict_from_url = mocker.patch("torch.hub.load_state_dict_from_url", return_value=OrderedDict()) + mocker_load_state_dict = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_state_dict" + ) + mocker_load_from_checkpoint = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_from_checkpoint" + ) + + config = self.base_config.copy() + config.model.update(dict(checkpoint="http://checkpoint")) + + sam = SegmentAnything(config, state_dict=None) + + mocker_load_from_checkpoint.assert_not_called() + mocker_load_state_dict_from_url.assert_called_once() + mocker_load_state_dict.assert_called_once() + + @e2e_pytest_unit + @pytest.mark.parametrize("checkpoint", ["checkpoint.pth", "checkpoint.ckpt"]) + def test_load_checkpoint_from_local_checkpoint(self, mocker, monkeypatch, checkpoint: str): + """Test load_checkpoint from local checkpoint.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.freeze_networks" + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.set_metrics" + ) + mocker.patch("builtins.open").__enter__.return_value = True + mocker.patch("torch.load", return_value=OrderedDict()) + mocker_load_from_checkpoint = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_from_checkpoint" + ) + mocker_load_state_dict = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_state_dict" + ) + + config = self.base_config.copy() + config.model.update(dict(checkpoint=checkpoint)) + + sam = SegmentAnything(config, state_dict=None) + + if checkpoint.endswith(".ckpt"): + mocker_load_from_checkpoint.assert_called_once() + mocker_load_state_dict.assert_not_called() + else: + mocker_load_from_checkpoint.assert_not_called() + mocker_load_state_dict.assert_called_once() + + @e2e_pytest_unit + def test_load_checkpoint_without_checkpoint(self, mocker): + """Test load_checkpoint without checkpoint.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.freeze_networks" + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.set_metrics" + ) + mocker_load_from_checkpoint = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_from_checkpoint" + ) + mocker_load_state_dict = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_state_dict" + ) + mocker_load_state_dict_from_url = mocker.patch("torch.hub.load_state_dict_from_url", return_value=OrderedDict()) + + config = self.base_config.copy() + sam = SegmentAnything(config, state_dict=None) + + mocker_load_from_checkpoint.assert_not_called() + mocker_load_state_dict_from_url.assert_called_once() + mocker_load_state_dict.assert_called_once() + + @e2e_pytest_unit + @pytest.mark.parametrize( + "point_coords,point_labels,expected", + [ + (Tensor([[[1, 1]]]), Tensor([[1]]), (1, 1, 4)), + (Tensor([[[1, 1], [2, 2]]]), Tensor([[2, 3]]), (1, 2, 4)), + ], + ) + def test_embed_points(self, mocker, point_coords: Tensor, point_labels: Tensor, expected: Tuple[int]) -> None: + """Test _embed_points.""" + sam = SegmentAnything(config=self.base_config) + sam.prompt_encoder.not_a_point_embed = nn.Embedding(1, sam.prompt_encoder.embed_dim) + sam.prompt_encoder.num_point_embeddings = 4 + point_embeddings = [ + nn.Embedding(1, sam.prompt_encoder.embed_dim) for i in range(sam.prompt_encoder.num_point_embeddings) + ] + sam.prompt_encoder.point_embeddings = nn.ModuleList(point_embeddings) + + num_points = point_coords.shape[1] + + mocker_pe_layer = mocker.patch.object(sam.prompt_encoder, "pe_layer") + mocker_pe_layer._pe_encoding.return_value = torch.empty((1, num_points, 4)) + + results = sam._embed_points(point_coords, point_labels) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "masks_input,has_mask_input,expected", + [ + (torch.randn(1, 1, 4, 4, dtype=torch.float), torch.tensor([1], dtype=torch.float), (1, 4, 2, 2)), + ], + ) + def test_embed_masks(self, mocker, masks_input: Tensor, has_mask_input: Tensor, expected: Tuple[int]) -> None: + """Test _embed_masks.""" + sam = SegmentAnything(config=self.base_config) + sam.prompt_encoder.no_mask_embed = nn.Embedding(1, sam.prompt_encoder.embed_dim) + + mocker.patch.object(sam.prompt_encoder, "mask_downscaling", return_value=torch.empty((1, 1, 2, 2))) + + masks_input = torch.randn(1, 1, 4, 4, dtype=torch.float) + has_mask_input = torch.tensor([1], dtype=torch.float) + + results = sam._embed_masks(masks_input, has_mask_input) + + assert results.shape == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "masks,expected", + [ + (Tensor([[[-2, -2], [2, 2]]]), 1), + (Tensor([[[-2, -2], [1, 1]]]), 0), + ], + ) + def test_calculate_stability_score(self, masks: Tensor, expected: int) -> None: + """Test calculate_stability_score.""" + sam = SegmentAnything(config=self.base_config) + + results = sam.calculate_stability_score(masks, mask_threshold=0.0, threshold_offset=1.0) + + assert results == expected + + @e2e_pytest_unit + def test_select_masks(self) -> None: + """Test select_masks.""" + sam = SegmentAnything(config=self.base_config) + + masks = Tensor([[[[1]], [[2]], [[3]], [[4]]]]) + iou_preds = Tensor([[0.1, 0.2, 0.3, 0.4]]) + num_points = 1 + + selected_mask, selected_iou_pred = sam.select_masks(masks, iou_preds, num_points) + + assert masks[:, -1, :, :] == selected_mask + assert iou_preds[:, -1] == selected_iou_pred + + @e2e_pytest_unit + def test_forward_train(self) -> None: + """Test forward.""" + sam = SegmentAnything(config=self.base_config) + images = torch.zeros((1, 3, 4, 4)) + bboxes = torch.zeros((1)) + + results = sam.forward_train(images=images, bboxes=bboxes, points=[None]) + pred_masks, ious = results + + assert len(bboxes) == len(pred_masks) == len(ious) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "loss_type,expected", [("sam", torch.tensor(9.7160396576)), ("medsam", torch.tensor(3.8603453636))] + ) + def test_training_step(self, mocker, loss_type: str, expected: Tensor) -> None: + """Test training_step.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.forward", + return_value=([torch.Tensor([[0, 1, 1, 0] for _ in range(4)])], [torch.tensor(1.0)]), + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.postprocess_masks", + return_value=torch.Tensor([[0, 1, 1, 0] for _ in range(4)]), + ) + + config = self.base_config.copy() + config.model.update(dict(loss_type=loss_type)) + sam = SegmentAnything(config=config) + batch = dict( + images=torch.ones((1, 3, 4, 4)), + gt_masks=[torch.Tensor([[0, 1, 1, 0] for _ in range(4)]).to(torch.int32)], + bboxes=torch.Tensor([[0, 0, 1, 1]]), + points=[None], + padding=[[0, 0, 0, 0]], + original_size=[[4, 4]], + ) + + results = sam.training_step(batch, None) + + assert torch.equal(results, expected) + + @e2e_pytest_unit + def test_training_epoch_end(self) -> None: + """Test training_epoch_end.""" + sam = SegmentAnything(config=self.base_config) + for k, v in sam.train_metrics.items(): + v.update(torch.zeros((2, 2)), torch.zeros((2, 2), dtype=torch.int32)) + + sam.training_epoch_end(None) + + assert sam.train_metrics["train_Dice"].compute() == 0.0 + assert sam.train_metrics["train_F1"].compute() == 0.0 + assert sam.train_metrics["train_IoU"].compute().isnan() + assert sam.train_metrics["train_loss"].compute().isnan() + assert sam.train_metrics["train_loss_dice"].compute().isnan() + assert sam.train_metrics["train_loss_focal"].compute().isnan() + assert sam.train_metrics["train_loss_iou"].compute().isnan() + + @e2e_pytest_unit + def test_validation_step(self, mocker) -> None: + """Test validation_step.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.forward", + return_value=([torch.Tensor([[0, 1, 1, 0] for _ in range(4)])], [torch.tensor(1.0)]), + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.postprocess_masks", + return_value=torch.Tensor([[0, 1, 1, 0] for _ in range(4)]), + ) + sam = SegmentAnything(config=self.base_config) + batch = dict( + images=torch.ones((1, 3, 4, 4)), + gt_masks=[torch.Tensor([[0, 1, 1, 0] for _ in range(4)]).to(torch.int32)], + bboxes=torch.Tensor([[0, 0, 1, 1]]), + points=[None], + path=None, + labels=None, + padding=[0], + original_size=[0], + ) + + results = sam.validation_step(batch, None) + + assert torch.equal(results["val_Dice"].compute(), torch.tensor(0.6666666865)) + assert torch.equal(results["val_F1"].compute(), torch.tensor(1.0)) + assert torch.equal(results["val_IoU"].compute(), torch.tensor(1.0)) + + @e2e_pytest_unit + def test_validation_epoch_end(self) -> None: + """Test validation_epoch_end.""" + sam = SegmentAnything(config=self.base_config) + for k, v in sam.val_metrics.items(): + v.update(torch.zeros((2, 2)), torch.zeros((2, 2), dtype=torch.int32)) + + sam.validation_epoch_end(None) + + assert sam.val_metrics["val_Dice"].compute() == 0.0 + assert sam.val_metrics["val_F1"].compute() == 0.0 + assert sam.val_metrics["val_IoU"].compute().isnan() + + @e2e_pytest_unit + @pytest.mark.parametrize( + "return_logits,expected", + [ + (True, torch.Tensor([[0.5 for _ in range(4)] for _ in range(4)])), + (False, torch.Tensor([[False for _ in range(4)] for _ in range(4)])), + ], + ) + def test_predict_step(self, mocker, return_logits: bool, expected: Tensor) -> None: + """Test predict_step.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.forward", + return_value=([torch.zeros((4, 4))], [torch.tensor(1.0)]), + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.postprocess_masks", + return_value=torch.zeros((4, 4)), + ) + + config = self.base_config.copy() + config.model.update(dict(return_logits=return_logits)) + sam = SegmentAnything(config=config) + batch = dict( + images=torch.zeros((1, 3, 4, 4)), + bboxes=torch.Tensor([[0, 0, 1, 1]]), + points=[None], + path=None, + labels=None, + padding=[0], + original_size=[0], + ) + + results = sam.predict_step(batch, None) + + assert torch.equal(results["masks"][0], expected) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "input_size,original_size,expected", + [ + (6, torch.tensor((8, 8)), (1, 8, 8)), + (6, torch.tensor((8, 8)), (1, 8, 8)), + ], + ) + def test_postprocess_masks(self, input_size: int, original_size: Tuple[int], expected: Tuple[int]) -> None: + """Test postprocess_masks.""" + sam = SegmentAnything(config=self.base_config) + masks = torch.zeros((1, 1, 4, 4)) + + results = sam.postprocess_masks(masks, input_size, original_size) + + assert results.shape[1:] == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "input_image_size,expected", + [ + (torch.tensor((2, 4)), torch.tensor((3, 6))), + (torch.tensor((4, 2)), torch.tensor((6, 3))), + ], + ) + def test_get_prepadded_size(self, input_image_size: Tensor, expected: Tensor) -> None: + """Test get_prepadded_size.""" + sam = SegmentAnything(config=self.base_config) + + longest_side = 6 + + results = sam.get_prepadded_size(input_image_size, longest_side) + + assert torch.all(results == expected) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "inputs,targets,expected", + [ + (Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.0])), + (Tensor([[0, 0, 0.5, 0.5, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.25])), + (Tensor([[0, 0, 0.3, 0.3, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.3888888359])), + ], + ) + def test_calculate_dice_loss(self, inputs: Tensor, targets: Tensor, expected: Tensor) -> None: + """Test calculate_dice_loss.""" + sam = SegmentAnything(config=self.base_config) + + results = sam.calculate_dice_loss(inputs, targets, num_masks=1) + + assert torch.isclose(results, expected) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "inputs,targets,expected", + [ + (Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.0])), + (Tensor([[0, 0, 0.5, 0.5, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.0098766042])), + (Tensor([[0, 0, 0.3, 0.3, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.0226361733])), + ], + ) + def test_calculate_sigmoid_ce_focal_loss(self, inputs: Tensor, targets: Tensor, expected: Tensor) -> None: + """Test calculate_sigmoid_ce_focal_loss.""" + sam = SegmentAnything(config=self.base_config) + + results = sam.calculate_sigmoid_ce_focal_loss(inputs, targets, num_masks=1) + + assert torch.isclose(results, expected) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "inputs,targets,expected", + [ + (Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([1.0])), + (Tensor([[0, 0, 0.5, 0.5, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([1.0])), + (Tensor([[0, 0, 0.3, 0.3, 0, 0]]), Tensor([[0, 0, 1, 1, 0, 0]]), Tensor([0.0])), + ], + ) + def test_calculate_iou(self, inputs: Tensor, targets: Tensor, expected: Tensor) -> None: + """Test calculate_iou.""" + sam = SegmentAnything(config=self.base_config) + + results = sam.calculate_iou(inputs, targets) + + assert results == expected diff --git a/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/test_zero_shot_segment_anything.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/test_zero_shot_segment_anything.py new file mode 100644 index 00000000000..9e57326577c --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/test_zero_shot_segment_anything.py @@ -0,0 +1,536 @@ +"""Tests Segment Anything for zero-shot learning.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +import pytest +from typing import Dict, Any, Optional +from collections import OrderedDict +from tests.test_suite.e2e_test_system import e2e_pytest_unit +import torch +import numpy as np +from torch import nn +from omegaconf import DictConfig + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything import ( + SegmentAnything, +) +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything import ( + PromptGetter, + ZeroShotSegmentAnything, +) +from tests.unit.algorithms.visual_prompting.test_helpers import ( + MockScoredLabel, + MockImageEncoder, + MockPromptGetter, + MockMaskDecoder, +) +from pytorch_lightning import Trainer + + +class TestPromptGetter: + @pytest.fixture + def prompt_getter(self) -> PromptGetter: + return PromptGetter(image_size=4, downsizing=1) + + @e2e_pytest_unit + def test_set_default_thresholds(self, prompt_getter) -> None: + """Test set_default_thresholds.""" + assert prompt_getter.default_threshold_reference == 0.3 + assert prompt_getter.default_threshold_target == 0.65 + + prompt_getter.set_default_thresholds(default_threshold_reference=0.5, default_threshold_target=0.7) + + assert prompt_getter.default_threshold_reference == 0.5 + assert prompt_getter.default_threshold_target == 0.7 + + @e2e_pytest_unit + @pytest.mark.parametrize( + "result_point_selection", + [torch.tensor([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]]), torch.tensor([[-1, -1, -1]])], + ) + def test_forward(self, mocker, prompt_getter, result_point_selection: torch.Tensor) -> None: + """Test forward.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.ZeroShotSegmentAnything" + ) + mocker.patch.object(prompt_getter, "_point_selection", return_value=(result_point_selection, torch.zeros(1, 2))) + image_embeddings = torch.ones(1, 4, 4, 4) + reference_feat = torch.rand(1, 4) + original_size = torch.tensor([[prompt_getter.image_size, prompt_getter.image_size]], dtype=torch.int64) + + points_scores, bg_coords = prompt_getter( + image_embeddings=image_embeddings, reference_feat=reference_feat, original_size=original_size + ) + + assert torch.all(points_scores == result_point_selection) + assert torch.all(bg_coords == torch.zeros(1, 2)) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "result_point_selection", + [torch.tensor([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]]), torch.tensor([[-1, -1, -1]])], + ) + def test_get_prompt_candidates(self, mocker, prompt_getter, result_point_selection: torch.Tensor) -> None: + """Test get_prompt_candidates.""" + mocker.patch.object(prompt_getter, "_point_selection", return_value=(result_point_selection, torch.zeros(1, 2))) + image_embeddings = torch.ones(1, 4, 4, 4) + reference_feats = torch.rand(1, 1, 4) + used_indices = torch.as_tensor([[0]]) + original_size = torch.tensor((prompt_getter.image_size, prompt_getter.image_size), dtype=torch.int64) + + total_points_scores, total_bg_coords = prompt_getter.get_prompt_candidates( + image_embeddings=image_embeddings, + reference_feats=reference_feats, + used_indices=used_indices, + original_size=original_size, + ) + + assert total_points_scores[0].shape[0] == len(result_point_selection) + assert total_bg_coords[0].shape[0] == 1 + + @e2e_pytest_unit + @pytest.mark.parametrize( + "mask_sim,expected", + [ + ( + torch.arange(0.1, 1.0, 0.1).reshape(3, 3), + torch.tensor([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]]), + ), + (torch.zeros(3, 3), torch.tensor([[-1, -1, -1]])), + ], + ) + def test_point_selection(self, prompt_getter, mask_sim: torch.Tensor, expected: torch.Tensor) -> None: + """Test _point_selection.""" + points_scores, bg_coords = prompt_getter._point_selection( + mask_sim=mask_sim, + original_size=torch.tensor([prompt_getter.image_size, prompt_getter.image_size]), + threshold=torch.tensor([[0.5]]), + ) + + assert torch.equal(points_scores, expected) + + +class TestZeroShotSegmentAnything: + @pytest.fixture + def set_zero_shot_segment_anything(self, monkeypatch): + def zero_shot_segment_anything( + manual_config_update: Optional[Dict] = None, state_dict: Optional[OrderedDict] = None + ): + monkeypatch.setattr( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SAMImageEncoder", + MockImageEncoder, + ) + monkeypatch.setattr( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SAMMaskDecoder", + MockMaskDecoder, + ) + return ZeroShotSegmentAnything(manual_config_update=manual_config_update, state_dict=state_dict) + + return zero_shot_segment_anything + + @e2e_pytest_unit + @pytest.mark.parametrize( + "state_dict", + [ + None, + {}, + ], + ) + def test_init(self, set_zero_shot_segment_anything, state_dict: Optional[Dict[str, Any]]) -> None: + """Test __init__.""" + if state_dict is not None: + state_dict = set_zero_shot_segment_anything().state_dict() + state_dict.pop("reference_info.reference_feats") + state_dict.pop("reference_info.used_indices") + + zero_shot_segment_anything = set_zero_shot_segment_anything(state_dict=state_dict) + + assert zero_shot_segment_anything.config.model.freeze_image_encoder + assert zero_shot_segment_anything.config.model.freeze_prompt_encoder + assert zero_shot_segment_anything.config.model.freeze_mask_decoder + + if state_dict: + assert zero_shot_segment_anything.reference_info.reference_feats is not None + assert zero_shot_segment_anything.reference_info.used_indices is not None + + assert zero_shot_segment_anything.reference_info.reference_feats.dtype == torch.float32 + assert zero_shot_segment_anything.reference_info.used_indices.dtype == torch.int64 + + @e2e_pytest_unit + def test_set_default_config(self, set_zero_shot_segment_anything) -> None: + """Test set_default_config.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + + default_config = zero_shot_segment_anything.set_default_config() + + assert isinstance(default_config, DictConfig) + assert "model" in default_config + assert "backbone" in default_config.model + assert "checkpoint" in default_config.model + assert "default_threshold_reference" in default_config.model + assert "default_threshold_target" in default_config.model + assert "freeze_image_encoder" in default_config.model + assert "freeze_mask_decoder" in default_config.model + assert "freeze_prompt_encoder" in default_config.model + assert "image_size" in default_config.model + assert "mask_threshold" in default_config.model + + @e2e_pytest_unit + def test_expand_reference_info(self, set_zero_shot_segment_anything): + """Test expand_reference_info.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + zero_shot_segment_anything.reference_info["reference_feats"] = torch.ones((3, 2, 2)) + new_largest_label = 5 + + zero_shot_segment_anything.expand_reference_info(new_largest_label) + + assert zero_shot_segment_anything.reference_info["reference_feats"].shape == (6, 2, 2) + assert torch.all(zero_shot_segment_anything.reference_info["reference_feats"][:3] == 1.0) + assert torch.all(zero_shot_segment_anything.reference_info["reference_feats"][3:] == 0.0) + + @e2e_pytest_unit + def test_learn(self, mocker, set_zero_shot_segment_anything) -> None: + """Test learn.""" + zero_shot_segment_anything = set_zero_shot_segment_anything(manual_config_update={"model.image_size": 4}) + mocker.patch.object( + zero_shot_segment_anything, + "_predict_masks", + return_value=torch.tensor([[[[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]]]]), + ) + mocker.patch.object(zero_shot_segment_anything, "_generate_masked_features", return_value=torch.ones(1, 256)) + + batch = [ + { + "images": np.ones((4, 4, 3), dtype=np.uint8), + "gt_masks": np.ones((4, 4), dtype=np.uint8), + "bboxes": np.array([[0, 0, 1, 1]], dtype=np.float32), + "points": np.zeros((0, 2), dtype=np.float32), + "labels": {"bboxes": [MockScoredLabel(label=0, name="label")]}, + "original_size": np.array([4, 4], dtype=np.int64), + } + ] + zero_shot_segment_anything.learn(batch=batch, reset_feat=True) + + assert zero_shot_segment_anything.reference_info.reference_feats.shape == (1, 1, 256) + assert zero_shot_segment_anything.reference_info.used_indices == torch.as_tensor([0]) + + @e2e_pytest_unit + @pytest.mark.parametrize("expected", [[torch.ones((4, 4)) / 2, torch.tensor([0.0, 0.0, 0.5])]]) + def test_infer(self, monkeypatch, mocker, set_zero_shot_segment_anything, expected: torch.Tensor) -> None: + """Test infer.""" + monkeypatch.setattr( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.PromptGetter", + MockPromptGetter, + ) + + zero_shot_segment_anything = set_zero_shot_segment_anything(manual_config_update={"model.image_size": 4}) + reference_feats = nn.Parameter(torch.rand(1, 1, 256), requires_grad=False) + used_indices = nn.Parameter(torch.as_tensor([[0]], dtype=torch.int64), requires_grad=False) + mocker.patch.object( + SegmentAnything, + "forward", + return_value=(torch.ones(1, 4, 4, 4), torch.tensor([[0.1, 0.2, 0.5, 0.7]]), torch.ones(1, 4, 4, 4)), + ) + + batch = [ + { + "images": np.ones((4, 4, 3), dtype=np.uint8), + "gt_masks": np.ones((4, 4), dtype=np.uint8), + "original_size": np.array([4, 4], dtype=np.int64), + } + ] + total_results = zero_shot_segment_anything.infer( + batch=batch, + reference_feats=reference_feats, + used_indices=used_indices, + ) + + for i, results in enumerate(total_results[0]): + for _, result in results.items(): + assert torch.equal(result[0], expected[i]) + + @e2e_pytest_unit + def test_inspect_overlapping_areas(self, mocker, set_zero_shot_segment_anything) -> None: + """Test _inspect_overlapping_areas.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + zero_shot_segment_anything = set_zero_shot_segment_anything() + predicted_masks = { + 0: [ + torch.tensor( + [ + [1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ], + ), + torch.tensor( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ], + ), + torch.tensor( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 0, 0], + ], + ), + ], + 1: [ + torch.tensor( + [ + [0, 0, 0, 1, 1, 0], + [0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ], + ), + torch.tensor( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1], + [0, 0, 0, 0, 1, 1], + ], + ), + torch.tensor( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0], + [0, 1, 1, 0, 0, 0], + ], + ), + torch.tensor( + [ + [1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ], + ), + ], + } + used_points = { + 0: [ + torch.tensor([0, 0, 0.5]), # to be removed + torch.tensor([2, 2, 0.5]), + torch.tensor([1, 4, 0.5]), + ], + 1: [ + torch.tensor([3, 0, 0.5]), + torch.tensor([4, 4, 0.5]), + torch.tensor([1, 4, 0.3]), # to be removed + torch.tensor([0, 0, 0.7]), + ], + } + + zero_shot_segment_anything._inspect_overlapping_areas(predicted_masks, used_points, threshold_iou=0.5) + + assert len(predicted_masks[0]) == 2 + assert len(predicted_masks[1]) == 3 + assert all(torch.tensor([2, 2, 0.5]) == used_points[0][0]) + assert all(torch.tensor([0, 0, 0.7]) == used_points[1][2]) + + @e2e_pytest_unit + def test_predict_masks(self, mocker, set_zero_shot_segment_anything) -> None: + """Test _predict_masks.""" + mocker.patch.object( + SegmentAnything, + "forward", + return_value=(torch.ones(1, 4, 8, 8), torch.tensor([[0.1, 0.2, 0.5, 0.7]]), torch.ones(1, 4, 4, 4)), + ) + + zero_shot_segment_anything = set_zero_shot_segment_anything() + zero_shot_segment_anything.config.model.image_size = 6 + + mask = zero_shot_segment_anything._predict_masks( + image_embeddings=torch.rand(1), + point_coords=torch.rand(1, 2, 2), + point_labels=torch.randint(low=0, high=2, size=(1, 2)), + original_size=torch.tensor([8, 8], dtype=torch.int64), + ) + assert mask.shape == (8, 8) + + @e2e_pytest_unit + def test_preprocess_prompts(self, set_zero_shot_segment_anything) -> None: + """Test _preprocess_prompts.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + transformed_batch = { + "bboxes": torch.tensor([[0, 0, 1, 1]]), + "points": torch.tensor([[2, 2]]), + "labels": {"bboxes": [MockScoredLabel(label=1)], "points": [MockScoredLabel(label=1)]}, + } + processed_prompts = zero_shot_segment_anything._preprocess_prompts(transformed_batch) + + for prompts in processed_prompts.values(): + for prompt in prompts: + if "bboxes" in prompt: + prompt["bboxes"]["point_coords"].shape == (1, 2, 2) + elif "points" in prompt: + prompt["points"]["point_coords"].shape == (1, 1, 2) + + @e2e_pytest_unit + def test_generate_masked_features(self, set_zero_shot_segment_anything) -> None: + """Test _generate_masked_features.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + zero_shot_segment_anything.config.model.image_size = 16 + feats = torch.rand((8, 8, 1)) + masks = torch.zeros((16, 16), dtype=torch.float32) + masks[4:12, 4:12] = 1.0 + + masked_feat = zero_shot_segment_anything._generate_masked_features(feats=feats, masks=masks, threshold_mask=0.3) + + assert masked_feat.shape == (1, 1) + + @e2e_pytest_unit + def test_pad_to_square(self, set_zero_shot_segment_anything) -> None: + """Test _pad_to_square.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + zero_shot_segment_anything.config.model.image_size = 16 + + result = zero_shot_segment_anything._pad_to_square(x=torch.ones(1, 1, 8, 8)) + + assert result[:8, :8].sum() == 8**2 + assert result[:8, 8:].sum() == 0 + assert result[8:, :8].sum() == 0 + assert result[8:, 8:].sum() == 0 + + @e2e_pytest_unit + @pytest.mark.parametrize( + "masks,logits,expected", + [ + (torch.ones(1, 4, 8, 8), torch.ones(1, 4, 4, 4), torch.ones(8, 8)), + (torch.zeros(1, 4, 8, 8), torch.zeros(1, 4, 4, 4), torch.zeros(8, 8)), + ], + ) + def test_postprocess_masks( + self, set_zero_shot_segment_anything, masks: torch.Tensor, logits: torch.Tensor, expected: torch.Tensor + ) -> None: + """Test _postprocess_masks.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + zero_shot_segment_anything.config.model.image_size = 4 + scores = torch.tensor([[0.0, 0.1, 0.2, 0.3]]) + + _, result = zero_shot_segment_anything._postprocess_masks(masks, logits, scores) + + assert torch.equal(result, expected) + + @e2e_pytest_unit + def test_find_latest_reference_info(self, mocker, set_zero_shot_segment_anything): + """Test _find_latest_reference_info.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.os.path.isdir", + return_value=True, + ) + + # there are some saved reference info + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.os.listdir", + return_value=["1", "2"], + ) + results = zero_shot_segment_anything._find_latest_reference_info() + assert results == "2" + + # there are no saved reference info + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.os.listdir", + return_value=[], + ) + results = zero_shot_segment_anything._find_latest_reference_info() + assert results is None + + @e2e_pytest_unit + def test_on_predict_start(self, mocker, set_zero_shot_segment_anything): + """Test on_predict_start.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.os.path.isdir", + return_value=True, + ) + + # get previously saved reference info + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.os.listdir", + return_value=["1", "2"], + ) + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.torch.load", + return_value=torch.nn.ParameterDict( + {"reference_feats": torch.zeros((1, 1, 256)), "used_indices": torch.tensor([0.0])} + ), + ) + mocker.patch("builtins.open", return_value="Mocked data") + + zero_shot_segment_anything.on_predict_start() + assert isinstance(zero_shot_segment_anything.reference_info, torch.nn.ParameterDict) + assert zero_shot_segment_anything.reference_info["reference_feats"].shape == (1, 1, 256) + assert zero_shot_segment_anything.reference_info["used_indices"].shape == (1,) + + # no saved reference info + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.os.listdir", + return_value=[], + ) + + zero_shot_segment_anything.set_empty_reference_info() + zero_shot_segment_anything.on_predict_start() + + assert zero_shot_segment_anything.reference_info["reference_feats"].shape == (0,) + assert zero_shot_segment_anything.reference_info["used_indices"].shape == (0,) + + @e2e_pytest_unit + def test_training_epoch_end(self, mocker, set_zero_shot_segment_anything): + """Test training_epoch_end.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + zero_shot_segment_anything.config.model.save_outputs = True + + mocker_makedirs = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.os.makedirs" + ) + mocker_save = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.torch.save" + ) + mocker_open = mocker.patch("builtins.open", return_value="Mocked data") + mocker_pickle_dump = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.pickle.dump" + ) + mocker_json_dump = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.json.dump" + ) + + from unittest.mock import Mock + + zero_shot_segment_anything._trainer = Mock(autospec=Trainer) + zero_shot_segment_anything.training_epoch_end(None) + + mocker_makedirs.assert_called_once() + mocker_save.assert_called_once() + mocker_open.assert_called() + assert mocker_open.call_count == 2 + mocker_pickle_dump.assert_called_once() + mocker_json_dump.assert_called_once() diff --git a/tests/unit/algorithms/visual_prompting/tasks/__init__.py b/tests/unit/algorithms/visual_prompting/tasks/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/tasks/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/algorithms/visual_prompting/tasks/test_inference.py b/tests/unit/algorithms/visual_prompting/tasks/test_inference.py new file mode 100644 index 00000000000..a0c777934d7 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/tasks/test_inference.py @@ -0,0 +1,307 @@ +"""Tests the methods in the inference task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import torch +import numpy as np +from typing import Optional, Dict, Any + +import pytest +from functools import wraps +from omegaconf import DictConfig + +from otx.algorithms.visual_prompting.tasks.inference import InferenceTask, ZeroShotTask +from otx.api.usecases.tasks.interfaces.export_interface import ExportType +from otx.api.entities.metrics import NullPerformance +from otx.api.entities.model import ModelEntity, ModelFormat, ModelOptimizationType +from otx.api.entities.resultset import ResultSetEntity +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from otx.algorithms.common.configs.training_base import TrainType +from otx.api.entities.train_parameters import TrainParameters +from otx.utils.logger import get_logger +from tests.unit.algorithms.visual_prompting.test_helpers import ( + generate_visual_prompting_dataset, + init_environment, + MockImageEncoder, +) +import onnxruntime + +logger = get_logger() + + +class TestInferenceTask: + @pytest.fixture + def load_inference_task(self, tmpdir, mocker): + def _load_inference_task( + output_path: Optional[str] = str(tmpdir.mkdir("visual_prompting_inference_test")), + path: Optional[str] = None, + resume: bool = False, + mode: str = "visual_prompt", + ): + if path is None: + mocker_model = None + else: + mocker_model = mocker.patch("otx.api.entities.model.ModelEntity") + mocker_model.model_adapters = {} + mocker.patch.dict(mocker_model.model_adapters, {"path": path, "resume": resume}) + + mocker.patch("pathlib.Path.write_text") + self.task_environment = init_environment(mocker_model, mode=mode) + + return InferenceTask(self.task_environment, output_path) + + return _load_inference_task + + @e2e_pytest_unit + @pytest.mark.parametrize("resume", [False, True]) + @pytest.mark.parametrize("path", [None, "checkpoint.ckpt", "checkpoint.pth"]) + def test_get_config_train(self, mocker, load_inference_task, path: Optional[str], resume: bool): + """Test get_config when train.""" + mocker.patch("otx.algorithms.visual_prompting.tasks.inference.InferenceTask.load_model") + inference_task = load_inference_task(path=path, resume=resume) + + assert inference_task.mode == "train" + assert isinstance(inference_task.config, DictConfig) + assert inference_task.config.dataset.task == "visual_prompting" + if path: + if resume: + assert inference_task.config.trainer.resume_from_checkpoint == path + else: + assert inference_task.config.model.checkpoint == path + assert inference_task.config.trainer.resume_from_checkpoint is None + + @e2e_pytest_unit + def test_get_config_eval(self, mocker, load_inference_task): + """Test get_config when eval.""" + mocker.patch("otx.algorithms.visual_prompting.tasks.inference.InferenceTask.load_model") + inference_task = load_inference_task(output_path=None) + + assert inference_task.mode == "inference" + assert isinstance(inference_task.config, DictConfig) + assert inference_task.config.dataset.task == "visual_prompting" + + @e2e_pytest_unit + @pytest.mark.parametrize("path", [None, "checkpoint.ckpt", "checkpoint.pth"]) + @pytest.mark.parametrize("resume", [True, False]) + @pytest.mark.parametrize( + "load_return_value", + [ + {"state_dict": {"layer": "weights"}, "pytorch-lightning_version": "version"}, + {"model": {"layer": "weights"}, "config": {"model": {"backbone": "sam_vit_b"}}}, + {}, + ], + ) + def test_load_model(self, mocker, load_inference_task, path: str, resume: bool, load_return_value: Dict[str, Any]): + """Test load_model.""" + mocker_segment_anything = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.SegmentAnything" + ) + mocker_io_bytes_io = mocker.patch("io.BytesIO") + mocker_torch_load = mocker.patch( + "torch.load", + return_value=load_return_value, + ) + + inference_task = load_inference_task(path=path, resume=resume) + inference_task.load_model(otx_model=inference_task.task_environment.model) + + mocker_segment_anything.assert_called_once() + if resume or path is None: + mocker_io_bytes_io.assert_not_called() + mocker_torch_load.assert_not_called() + else: + mocker_io_bytes_io.assert_called_once() + mocker_torch_load.assert_called_once() + + @e2e_pytest_unit + def test_load_model_zeroshot(self, mocker, load_inference_task): + """Test load_model when zero-shot.""" + mocker_segment_anything = mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.ZeroShotSegmentAnything" + ) + + inference_task = load_inference_task(mode="zero_shot") + + assert inference_task.hyper_parameters.algo_backend.train_type == TrainType.Zeroshot + + model = inference_task.load_model(otx_model=inference_task.task_environment.model) + + mocker_segment_anything.assert_called_once() + assert "ZeroShotSegmentAnything" in str(model) + + @e2e_pytest_unit + def test_infer(self, mocker, load_inference_task): + """Test infer.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + mocker_trainer = mocker.patch("otx.algorithms.visual_prompting.tasks.inference.Trainer") + + inference_task = load_inference_task(output_path=None) + dataset = generate_visual_prompting_dataset() + model = ModelEntity(dataset, inference_task.task_environment.get_model_configuration()) + + inference_task.infer(dataset, model) + + mocker_trainer.assert_called_once() + + @e2e_pytest_unit + def test_evaluate(self, mocker, load_inference_task): + """Test evaluate.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + mocker_dice_average = mocker.patch("otx.api.usecases.evaluation.metrics_helper.DiceAverage") + + inference_task = load_inference_task(output_path=None) + validation_dataset = generate_visual_prompting_dataset() + resultset = ResultSetEntity( + model=inference_task.task_environment.model, + ground_truth_dataset=validation_dataset, + prediction_dataset=validation_dataset, + ) + + inference_task.evaluate(resultset) + + mocker_dice_average.assert_called_once() + assert not isinstance(resultset.performance, NullPerformance) + + @e2e_pytest_unit + def test_model_info(self, mocker, load_inference_task): + """Test model_info.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + mocker.patch("torch.load", return_value={"state_dict": {"layer": "weights"}, "epoch": 0}) + + inference_task = load_inference_task(output_path=None) + inference_task._model_ckpt = "checkpoint" + + model_info = inference_task.model_info() + + assert isinstance(model_info.get("state_dict", None), dict) + assert model_info.get("state_dict", None)["layer"] == "weights" + assert isinstance(model_info.get("epoch", None), int) + assert model_info.get("epoch", None) == 0 + + @e2e_pytest_unit + def test_save_model(self, mocker, load_inference_task): + """Test save_model.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + + inference_task = load_inference_task(output_path=None) + mocker.patch.object(inference_task, "model_info") + mocker_otx_model = mocker.patch("otx.api.entities.model.ModelEntity") + mocker_io_bytes_io = mocker.patch("io.BytesIO") + mocker_torch_save = mocker.patch("torch.save") + + inference_task.save_model(mocker_otx_model) + + mocker_io_bytes_io.assert_called_once() + mocker_torch_save.assert_called_once() + + @e2e_pytest_unit + @pytest.mark.parametrize("export_type", [ExportType.ONNX, ExportType.OPENVINO]) + def test_export(self, mocker, load_inference_task, export_type: ExportType): + """Test export.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + + inference_task = load_inference_task(output_path=None) + dataset = generate_visual_prompting_dataset() + output_model = ModelEntity(dataset, inference_task.task_environment.get_model_configuration()) + + inference_task.export(export_type, output_model, dump_features=False) + + if export_type == ExportType.ONNX: + assert output_model.model_format == ModelFormat.ONNX + output_model.optimization_type = ModelOptimizationType.ONNX + assert "visual_prompting_image_encoder.onnx" in output_model.model_adapters + assert "visual_prompting_decoder.onnx" in output_model.model_adapters + + elif export_type == ExportType.OPENVINO: + assert output_model.model_format == ModelFormat.OPENVINO + output_model.optimization_type = ModelOptimizationType.ONNX + assert "visual_prompting_image_encoder.bin" in output_model.model_adapters + assert "visual_prompting_image_encoder.xml" in output_model.model_adapters + assert "visual_prompting_decoder.bin" in output_model.model_adapters + assert "visual_prompting_decoder.xml" in output_model.model_adapters + + assert not output_model.has_xai + + +class TestZeroShotTask: + @pytest.fixture(autouse=True) + def setup(self, tmpdir, mocker): + mocker.patch("pathlib.Path.write_text") + self.task_environment = init_environment(mode="zero_shot") + + self.output_path = str(tmpdir.mkdir("visual_prompting_zeroshot_test")) + + self.zero_shot_task = ZeroShotTask(self.task_environment, self.output_path) + + @e2e_pytest_unit + def test_train(self, mocker): + """Test train.""" + mocker_trainer = mocker.patch("otx.algorithms.visual_prompting.tasks.inference.Trainer") + mocker_save = mocker.patch("torch.save") + mocker.patch.object(self.zero_shot_task, "model_info") + + dataset = generate_visual_prompting_dataset() + output_model = ModelEntity( + dataset, + self.task_environment.get_model_configuration(), + ) + + self.zero_shot_task.train(dataset, output_model, TrainParameters()) + + mocker_trainer.assert_called_once() + mocker_save.assert_called_once() + assert isinstance(output_model.performance, NullPerformance) + assert output_model.model_adapters.get("weights.pth", None) + assert output_model.model_adapters.get("label_schema.json", None) + + @e2e_pytest_unit + def test_infer(self, mocker): + """Test infer.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + mocker_trainer = mocker.patch("otx.algorithms.visual_prompting.tasks.inference.Trainer") + + dataset = generate_visual_prompting_dataset() + model = ModelEntity(dataset, self.zero_shot_task.task_environment.get_model_configuration()) + + self.zero_shot_task.infer(dataset, model) + + mocker_trainer.assert_called_once() + + @e2e_pytest_unit + def test_save_model(self, mocker): + """Test save_model.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_checkpoint" + ) + + self.zero_shot_task.model = MockImageEncoder() + mocker_otx_model = mocker.patch("otx.api.entities.model.ModelEntity") + mocker_io_bytes_io = mocker.patch("io.BytesIO") + mocker_torch_save = mocker.patch("torch.save") + mocker.patch.object( + self.zero_shot_task.model, + "state_dict", + return_value={"reference_info.reference_feats": None, "reference_info.used_indices": None}, + ) + + self.zero_shot_task.model.reference_info = "reference_info" + + self.zero_shot_task.save_model(mocker_otx_model) + + mocker_io_bytes_io.assert_called_once() + mocker_torch_save.assert_called_once() diff --git a/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py b/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py new file mode 100644 index 00000000000..10e335bbbbe --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py @@ -0,0 +1,916 @@ +"""Tests the methods in the OpenVINO task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from typing import Optional, Dict, Tuple + +import os +import numpy as np +import pytest +from otx.api.usecases.tasks.interfaces.optimization_interface import OptimizationType +import torch +from openvino.model_api.models import Model +from otx.api.entities.subset import Subset + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset import ( + OTXVisualPromptingDataset, +) +from otx.algorithms.visual_prompting.configs.base import VisualPromptingBaseConfig +from otx.algorithms.visual_prompting.tasks.openvino import ( + OpenVINOVisualPromptingInferencer, + OpenVINOZeroShotVisualPromptingInferencer, + OpenVINOVisualPromptingTask, + OpenVINOZeroShotVisualPromptingTask, + OTXOpenVinoDataLoader, +) +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.entities.annotation import Annotation +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label import LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.metrics import Performance, ScoreMetric +from otx.api.entities.model import ModelConfiguration, ModelEntity +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.usecases.evaluation.metrics_helper import MetricsHelper +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( + VisualPromptingToAnnotationConverter, +) +from otx.api.utils.shape_factory import ShapeFactory +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.visual_prompting.test_helpers import ( + generate_visual_prompting_dataset, + init_environment, +) +from tests.unit.algorithms.visual_prompting.test_helpers import MockScoredLabel + + +class TestOpenVINOVisualPromptingInferencer: + @pytest.fixture(autouse=True) + def setup(self, mocker): + self.fake_annotation = [ + Annotation( + Polygon(points=[Point(0, 0)]), + id=0, + labels=[ScoredLabel(LabelEntity(name="fake", domain="VISUALPROMPTING"), probability=1.0)], + ) + ] + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.OpenvinoAdapter") + mocker.patch.object(Model, "create_model") + mocker.patch.object( + VisualPromptingToAnnotationConverter, "convert_to_annotation", return_value=self.fake_annotation + ) + self.task_environment = init_environment() + visual_prompting_hparams = self.task_environment.get_hyper_parameters(VisualPromptingBaseConfig) + label_schema = self.task_environment.label_schema + + self.visual_prompting_ov_inferencer = OpenVINOVisualPromptingInferencer( + visual_prompting_hparams, + label_schema, + {"image_encoder": "", "decoder": ""}, + {"image_encoder": "", "decoder": ""}, + ) + self.visual_prompting_ov_inferencer.model["decoder"] = mocker.patch( + "openvino.model_api.models.Model", autospec=True + ) + + @e2e_pytest_unit + def test_pre_process(self, mocker): + """Test pre_process.""" + mocker_get_prompts = mocker.patch.object(OTXVisualPromptingDataset, "get_prompts", return_value={}) + mocker.patch.object(self.visual_prompting_ov_inferencer, "transform", lambda items: items) + mocker.patch.object( + self.visual_prompting_ov_inferencer.model["image_encoder"], "preprocess", return_value=({}, {}) + ) + mocker.patch.object(self.visual_prompting_ov_inferencer.model["decoder"], "preprocess", return_value=[{}]) + fake_input = mocker.Mock(spec=DatasetItemEntity) + + returned_value = self.visual_prompting_ov_inferencer.pre_process(fake_input) + + assert isinstance(returned_value, tuple) + mocker_get_prompts.assert_called_once() + + @e2e_pytest_unit + def test_post_process(self, mocker): + """Test post_process.""" + fake_prediction = {"masks": np.empty((1, 1, 2, 2))} + fake_metadata = {"label": mocker.Mock(spec=LabelEntity), "original_size": np.array((2, 2))} + self.visual_prompting_ov_inferencer.model["decoder"].postprocess.return_value = ( + np.ones((2, 2)), + np.ones((2, 2)), + ) + + returned_value = self.visual_prompting_ov_inferencer.post_process(fake_prediction, fake_metadata) + + assert len(returned_value) == 3 + assert np.array_equal(returned_value[0], self.fake_annotation) + assert np.array_equal(returned_value[1], np.ones((2, 2))) + assert np.array_equal(returned_value[2], np.ones((2, 2))) + + @e2e_pytest_unit + def test_predict(self, mocker): + """Teset predict.""" + mocker_pre_process = mocker.patch.object( + OpenVINOVisualPromptingInferencer, + "pre_process", + return_value=( + torch.zeros((1, 3, 2, 2)), + {}, + [ + { + "point_coords": [np.array([[[1, 1], [2, 2]]])], + "point_labels": [1, 2], + "label": LabelEntity(name="fake", domain="VISUALPROMPTING"), + "orig_size": (4, 4), + } + ], + ), + ) + mocker_forward = mocker.patch.object( + OpenVINOVisualPromptingInferencer, + "forward_image_encoder", + return_value={"image_embeddings": np.empty((4, 2, 2))}, + ) + mocker_forward_decoder = mocker.patch.object( + OpenVINOVisualPromptingInferencer, "forward_decoder", return_value={"iou_predictions": 0.1} + ) + mocker_post_process = mocker.patch.object( + OpenVINOVisualPromptingInferencer, "post_process", return_value=(self.fake_annotation, None, None) + ) + fake_input = mocker.Mock(spec=DatasetItemEntity) + + returned_value = self.visual_prompting_ov_inferencer.predict(fake_input) + + mocker_pre_process.assert_called_once() + mocker_forward.assert_called_once() + mocker_forward_decoder.assert_called_once() + mocker_post_process.assert_called_once() + assert returned_value == self.fake_annotation + + @e2e_pytest_unit + def test_forward_image_encoder(self): + """Test forward_image_encoder.""" + fake_input = {"images": np.ones((1, 3, 2, 2))} + fake_output = {"image_embeddings": np.ones((1, 1, 2, 2))} + self.visual_prompting_ov_inferencer.model["image_encoder"].infer_sync.return_value = fake_output + returned_value = self.visual_prompting_ov_inferencer.forward_image_encoder(fake_input) + + assert returned_value == fake_output + + @e2e_pytest_unit + def test_forward_decoder(self): + """Test forward_decoder.""" + fake_input = {} + fake_output = {"masks": np.ones((1, 1, 2, 2))} + self.visual_prompting_ov_inferencer.model["decoder"].infer_sync.return_value = fake_output + returned_value = self.visual_prompting_ov_inferencer.forward_decoder(fake_input) + + assert returned_value == fake_output + + +class TestOpenVINOZeroShotVisualPromptingInferencer: + @pytest.fixture(autouse=True) + def setup(self, mocker): + self.fake_annotation = [ + Annotation( + Polygon(points=[Point(0, 0)]), + id=0, + labels=[ScoredLabel(LabelEntity(name="fake", domain="VISUALPROMPTING"), probability=1.0)], + ) + ] + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.OpenvinoAdapter") + mocker.patch.object(Model, "create_model") + mocker.patch.object( + VisualPromptingToAnnotationConverter, "convert_to_annotation", return_value=self.fake_annotation + ) + self.task_environment = init_environment() + visual_prompting_hparams = self.task_environment.get_hyper_parameters(VisualPromptingBaseConfig) + label_schema = self.task_environment.label_schema + + self.zero_shot_visual_prompting_ov_inferencer = OpenVINOZeroShotVisualPromptingInferencer( + visual_prompting_hparams, + label_schema, + {"image_encoder": "", "decoder": ""}, + {"image_encoder": "", "decoder": ""}, + ) + self.zero_shot_visual_prompting_ov_inferencer.model["decoder"] = mocker.patch( + "otx.algorithms.visual_prompting.tasks.openvino.model_wrappers.Decoder", + autospec=True, + ) + self.zero_shot_visual_prompting_ov_inferencer.model["decoder"].mask_threshold = 0.3 + self.zero_shot_visual_prompting_ov_inferencer.model["decoder"]._apply_coords.return_value = np.array([[1, 1]]) + self.zero_shot_visual_prompting_ov_inferencer.model["decoder"].output_blob_name = "upscaled_masks" + + @e2e_pytest_unit + def test_learn(self, mocker): + """Test learn.""" + mocker_pre_process = mocker.patch.object( + OpenVINOVisualPromptingInferencer, + "pre_process", + return_value=( + torch.zeros((1, 3, 2, 2)), + {"original_shape": np.array((4, 4))}, + [ + { + "point_coords": [np.array([[[1, 1], [2, 2]]])], + "point_labels": [1, 2], + "label": MockScoredLabel(label=0, name="fake"), + "orig_size": (4, 4), + } + ], + ), + ) + mocker_forward_image_encoder = mocker.patch.object( + OpenVINOZeroShotVisualPromptingInferencer, + "forward_image_encoder", + return_value={"image_embeddings": np.empty((4, 2, 2))}, + ) + mocker_generate_masked_features = mocker.patch.object( + OpenVINOZeroShotVisualPromptingInferencer, "_generate_masked_features", return_value=torch.ones(1, 256) + ) + + self.zero_shot_visual_prompting_ov_inferencer.model["decoder"].infer_sync.return_value = { + "upscaled_masks": np.ones((1, 4, 4, 4), dtype=np.bool), + "iou_predictions": np.array([[0.9, 0.7, 0.9, 0.8]]), + "low_res_masks": np.ones((1, 4, 2, 2)), + } + mocker_pickle_dump = mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.pickle.dump") + mocker.patch("builtins.open", return_value="Mocked data") + + fake_input = mocker.Mock(spec=DatasetItemEntity) + results = self.zero_shot_visual_prompting_ov_inferencer.learn(fake_input, reset_feat=True) + + assert results[0]["reference_feats"].shape == (1, 1, 256) + assert results[0]["used_indices"] == np.array([[0]]) + assert np.all(results[1] == np.ones((1, 4, 4))) + mocker_pre_process.assert_called_once() + mocker_forward_image_encoder.assert_called_once() + mocker_generate_masked_features.assert_called_once() + mocker_pickle_dump.assert_called_once() + + @e2e_pytest_unit + def test_predict(self, mocker): + """Test predict.""" + mocker_pre_process = mocker.patch.object( + OpenVINOZeroShotVisualPromptingInferencer, + "pre_process_image_encoder", + return_value=(torch.zeros((1, 3, 2, 2)), {"original_shape": (4, 4, 1)}), + ) + mocker_forward = mocker.patch.object( + OpenVINOZeroShotVisualPromptingInferencer, + "forward_image_encoder", + return_value={"image_embeddings": np.empty((4, 2, 2))}, + ) + mocker_get_prompt_candidates = mocker.patch.object( + OpenVINOZeroShotVisualPromptingInferencer, + "_get_prompt_candidates", + return_value=({0: np.array([[1, 1, 1]])}, {0: np.array([[2, 2]])}), + ) + mocker_forward_decoder = mocker.patch.object( + OpenVINOZeroShotVisualPromptingInferencer, "forward_decoder", return_value={"upscaled_masks": None} + ) + mocker_post_process = mocker.patch.object( + OpenVINOZeroShotVisualPromptingInferencer, "post_process", return_value=(self.fake_annotation, None, None) + ) + self.zero_shot_visual_prompting_ov_inferencer.reference_feats = np.random.rand(1, 1, 1) + self.zero_shot_visual_prompting_ov_inferencer.used_indices = np.array([[0]]) + fake_input = mocker.Mock(spec=DatasetItemEntity) + + results = self.zero_shot_visual_prompting_ov_inferencer.predict(fake_input) + + mocker_pre_process.assert_called_once() + mocker_forward.assert_called_once() + mocker_get_prompt_candidates.assert_called_once() + mocker_forward_decoder.assert_called_once() + mocker_post_process.assert_called_once() + assert results == self.fake_annotation + + @e2e_pytest_unit + @pytest.mark.parametrize( + "postprocess_output,infer_sync_output,expected", + [ + ( + (np.ones((1, 1)), np.ones((3, 3))), + { + "upscaled_masks": np.ones((3, 3)), + "iou_predictions": np.array([[0.9]]), + "low_res_masks": np.ones((1, 1, 2, 2)), + }, + {"upscaled_masks": np.ones((3, 3))}, + ), + ( + (np.zeros((2, 2)), np.zeros((3, 3))), + { + "upscaled_masks": np.zeros((3, 3)), + "iou_predictions": np.array([[0.9]]), + "low_res_masks": np.ones((1, 1, 2, 2)), + }, + {"upscaled_masks": np.zeros((3, 3))}, + ), + ], + ) + def test_forward_decoder( + self, + mocker, + postprocess_output: Tuple[torch.Tensor, torch.Tensor], + infer_sync_output: Dict[str, np.ndarray], + expected: Dict[str, torch.Tensor], + ): + """Test forward_decoder.""" + mocker.patch.object( + self.zero_shot_visual_prompting_ov_inferencer.model["decoder"], "infer_sync", return_value=infer_sync_output + ) + mocker.patch.object( + self.zero_shot_visual_prompting_ov_inferencer.model["decoder"], + "_apply_coords", + return_value=np.array([[[1, 1]]], dtype=np.float32), + ) + mocker.patch.object( + self.zero_shot_visual_prompting_ov_inferencer, "_postprocess_masks", return_value=postprocess_output + ) + + result = self.zero_shot_visual_prompting_ov_inferencer.forward_decoder( + inputs={ + "image_embeddings": np.empty((1, 4, 2, 2)), + "point_coords": np.array([[[1, 1]]], dtype=np.float32), + "point_labels": np.array([[1]], dtype=np.float32), + }, + original_size=np.array([3, 3]), + ) + + assert np.all(result["upscaled_masks"] == expected["upscaled_masks"]) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "result_point_selection", + [np.array([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]]), np.array([[-1, -1, -1]])], + ) + def test_get_prompt_candidates(self, mocker, result_point_selection: np.ndarray) -> None: + """Test _get_prompt_candidates.""" + mocker.patch.object( + OpenVINOZeroShotVisualPromptingInferencer, + "_point_selection", + return_value=(result_point_selection, np.zeros((1, 2))), + ) + image_embeddings = np.ones((1, 4, 4, 4)) + reference_feats = np.random.rand(1, 1, 4) + used_indices = np.array([0]) + original_shape = np.array([4, 4], dtype=np.int64) + + total_points_scores, total_bg_coords = self.zero_shot_visual_prompting_ov_inferencer._get_prompt_candidates( + image_embeddings=image_embeddings, + reference_feats=reference_feats, + used_indices=used_indices, + original_shape=original_shape, + image_size=4, + downsizing=1, + ) + + assert total_points_scores[0].shape[0] == len(result_point_selection) + assert total_bg_coords[0].shape[0] == 1 + + @e2e_pytest_unit + @pytest.mark.parametrize( + "mask_sim,expected", + [ + ( + np.arange(0.1, 1.0, 0.1).reshape(3, 3), + np.array([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]]), + ), + (np.zeros((3, 3)), None), + ], + ) + def test_point_selection(self, mask_sim: np.ndarray, expected: np.ndarray) -> None: + """Test _point_selection.""" + points_scores, bg_coords = self.zero_shot_visual_prompting_ov_inferencer._point_selection( + mask_sim=mask_sim, + original_shape=np.array([4, 4]), + threshold=0.5, + image_size=4, + downsizing=1, + ) + + if points_scores is not None: + assert np.allclose(points_scores, expected) + else: + assert points_scores == expected + + @e2e_pytest_unit + @pytest.mark.parametrize( + "masks,expected_masks", + [ + ( + np.repeat(np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]])[None], 4, axis=0)[None], + np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.bool_), + ), + ( + np.concatenate( + ( + np.repeat(np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]])[None], 3, axis=0)[None], + np.zeros((1, 1, 3, 3)), + ), + axis=1, + ), + np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.bool_), + ), + (np.zeros((1, 4, 3, 3)), np.zeros((3, 3))), + ], + ) + def test_postprocess_masks(self, masks: np.ndarray, expected_masks: np.ndarray): + """Test _postprocess_masks.""" + self.zero_shot_visual_prompting_ov_inferencer.model["decoder"].mask_threshold = 0.0 + self.zero_shot_visual_prompting_ov_inferencer.model["decoder"].image_size = 3 + + _, result_masks = self.zero_shot_visual_prompting_ov_inferencer._postprocess_masks( + masks=masks, logits=np.empty((1, 4, 2, 2)), scores=np.array([[0.5, 0.7, 0.8, 0.9]]) + ) + + assert result_masks.shape == (3, 3) + assert np.all(result_masks == expected_masks) + + @e2e_pytest_unit + def test_inspect_overlapping_areas(self) -> None: + """Test _inspect_overlapping_areas.""" + predicted_masks = { + 0: [ + np.array( + [ + [1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ], + ), + np.array( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ], + ), + np.array( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 0, 0], + ], + ), + ], + 1: [ + np.array( + [ + [0, 0, 0, 1, 1, 0], + [0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ], + ), + np.array( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1], + [0, 0, 0, 0, 1, 1], + ], + ), + np.array( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0], + [0, 1, 1, 0, 0, 0], + ], + ), + np.array( + [ + [1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ], + ), + ], + } + used_points = { + 0: [ + np.array([0, 0, 0.5]), # to be removed + np.array([2, 2, 0.5]), + np.array([1, 4, 0.5]), + ], + 1: [ + np.array([3, 0, 0.5]), + np.array([4, 4, 0.5]), + np.array([1, 4, 0.3]), # to be removed + np.array([0, 0, 0.7]), + ], + } + + self.zero_shot_visual_prompting_ov_inferencer._inspect_overlapping_areas( + predicted_masks, used_points, threshold_iou=0.5 + ) + + assert len(predicted_masks[0]) == 2 + assert len(predicted_masks[1]) == 3 + assert all(np.array([2, 2, 0.5]) == used_points[0][0]) + assert all(np.array([0, 0, 0.7]) == used_points[1][2]) + + @e2e_pytest_unit + def test_find_latest_reference_info(self, mocker): + """Test _find_latest_reference_info.""" + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.os.path.isdir", return_value=True) + + # there are some saved reference info + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.os.listdir", return_value=["1", "2"]) + results = self.zero_shot_visual_prompting_ov_inferencer._find_latest_reference_info() + assert results == "2" + + # there are no saved reference info + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.os.listdir", return_value=[]) + results = self.zero_shot_visual_prompting_ov_inferencer._find_latest_reference_info() + assert results is None + + @e2e_pytest_unit + def test_get_reference_info(self, mocker): + """Test _get_reference_info.""" + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.os.path.isdir", return_value=True) + + # get previously saved reference info + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.os.listdir", return_value=["1", "2"]) + mocker.patch( + "otx.algorithms.visual_prompting.tasks.openvino.pickle.load", + return_value={"reference_feats": 1, "used_indices": 2}, + ) + mocker.patch("builtins.open", return_value="Mocked data") + + results = self.zero_shot_visual_prompting_ov_inferencer._get_reference_info() + assert results == (1, 2) + + # no saved reference info + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.os.listdir", return_value=[]) + + results = self.zero_shot_visual_prompting_ov_inferencer._get_reference_info() + assert results == (None, None) + + @e2e_pytest_unit + def test_expand_reference_info(self): + """Test expand_reference_info.""" + self.zero_shot_visual_prompting_ov_inferencer.reference_feats = np.ones((3, 2, 2)) + new_largest_label = 5 + + self.zero_shot_visual_prompting_ov_inferencer.expand_reference_info(new_largest_label) + + assert self.zero_shot_visual_prompting_ov_inferencer.reference_feats.shape == (6, 2, 2) + assert np.all(self.zero_shot_visual_prompting_ov_inferencer.reference_feats[:3] == 1.0) + assert np.all(self.zero_shot_visual_prompting_ov_inferencer.reference_feats[3:] == 0.0) + + @e2e_pytest_unit + def test_generate_masked_features(self) -> None: + """Test _generate_masked_features.""" + self.zero_shot_visual_prompting_ov_inferencer.model["image_encoder"].image_size = 16 + feats = np.random.rand(8, 8, 1) + masks = np.zeros((16, 16), dtype=np.float32) + masks[4:12, 4:12] = 1.0 + + masked_feat = self.zero_shot_visual_prompting_ov_inferencer._generate_masked_features( + feats=feats, masks=masks, threshold_mask=0.3 + ) + + assert masked_feat.shape == (1, 1) + + @e2e_pytest_unit + def test_pad_to_square(self) -> None: + """Test _pad_to_square.""" + self.zero_shot_visual_prompting_ov_inferencer.model["image_encoder"].image_size = 16 + + result = self.zero_shot_visual_prompting_ov_inferencer._pad_to_square(x=np.ones((8, 8))) + + assert result[:8, :8].sum() == 8**2 + assert result[:8, 8:].sum() == 0 + assert result[8:, :8].sum() == 0 + assert result[8:, 8:].sum() == 0 + + +class TestOTXOpenVinoDataLoader: + @pytest.fixture + def load_dataloader(self, mocker): + def _load_dataloader(module_name: str, output_model: Optional[ModelEntity] = None): + dataset = generate_visual_prompting_dataset() + dataset = dataset.get_subset(Subset.TRAINING) + return OTXOpenVinoDataLoader(dataset, self.mocker_inferencer, module_name, output_model=output_model) + + return _load_dataloader + + @pytest.fixture(autouse=True) + def setup(self, mocker): + self.mocker_read_model = mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.ov.Core.read_model") + self.mocker_compile_model = mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.ov.Core.compile_model") + self.mocker_inferencer = mocker.patch.object(OpenVINOVisualPromptingInferencer, "__init__") + + @e2e_pytest_unit + @pytest.mark.parametrize("module_name", ["image_encoder", "decoder"]) + def test_getitem(self, mocker, load_dataloader, module_name: str): + """Test __getitem__.""" + mocker_output_model = mocker.patch("otx.api.entities.model.ModelEntity") + if module_name == "decoder": + mocker.patch.object(mocker_output_model, "get_data") + self.mocker_read_model.reset_mock() + self.mocker_compile_model.reset_mock() + + dataloader = load_dataloader(module_name, mocker_output_model) + + setattr(dataloader, "target_length", 8) + mocker.patch.object( + dataloader.inferencer, + "pre_process", + return_value=({"images": np.zeros((1, 3, 4, 4), dtype=np.uint8)}, None, [{"label": 1, "orig_size": 1}]), + ) + + results = dataloader.__getitem__(0) + + if module_name == "image_encoder": + assert results["images"].shape == (1, 3, 8, 8) + else: + self.mocker_read_model.assert_called_once() + self.mocker_compile_model.assert_called_once() + assert "label" not in results + assert "orig_size" in results + assert "image_embeddings" in results + + +class TestOpenVINOVisualPromptingTask: + @pytest.fixture + def otx_model(self): + model_configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="header", description="description"), + label_schema=LabelSchemaEntity(), + ) + return ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + + @pytest.fixture(autouse=True) + def setup(self, mocker, otx_model): + """Load the OpenVINOVisualPromptingTask.""" + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.OpenvinoAdapter") + mocker.patch.object(Model, "create_model") + self.task_environment = init_environment() + visual_prompting_hparams = self.task_environment.get_hyper_parameters(VisualPromptingBaseConfig) + + visual_prompting_ov_inferencer = OpenVINOVisualPromptingInferencer( + visual_prompting_hparams, + self.task_environment.label_schema, + {"image_encoder": "", "decoder": ""}, + {"image_encoder": "", "decoder": ""}, + ) + + # self.task_environment.model = mocker.patch("otx.api.entities.model.ModelEntity") + self.task_environment.model = otx_model + mocker.patch.object(OpenVINOVisualPromptingTask, "load_inferencer", return_value=visual_prompting_ov_inferencer) + self.visual_prompting_ov_task = OpenVINOVisualPromptingTask(task_environment=self.task_environment) + + @e2e_pytest_unit + def test_infer(self, mocker): + """Test infer.""" + fake_annotation = [ + Annotation( + Polygon(points=[Point(0, 0)]), + id=0, + labels=[ScoredLabel(LabelEntity(name="fake", domain="VISUALPROMPTING"), probability=1.0)], + ) + ] + + mocker_predict = mocker.patch.object(OpenVINOVisualPromptingInferencer, "predict", return_value=fake_annotation) + mocker.patch.object(ShapeFactory, "shape_produces_valid_crop", return_value=True) + + dataset = generate_visual_prompting_dataset() + + updated_dataset = self.visual_prompting_ov_task.infer( + dataset, InferenceParameters(enable_async_inference=False) + ) + + for updated in updated_dataset: + assert updated.annotation_scene.contains_any([LabelEntity(name="fake", domain="VISUALPROMPTING")]) + + mocker_predict.assert_called() + assert mocker_predict.call_count == len(updated_dataset) + + @e2e_pytest_unit + def test_evaluate(self, mocker): + """Test evaluate.""" + result_set = ResultSetEntity( + model=None, + ground_truth_dataset=DatasetEntity(), + prediction_dataset=DatasetEntity(), + ) + fake_metrics = mocker.patch("otx.api.usecases.evaluation.dice.DiceAverage", autospec=True) + fake_metrics.get_performance.return_value = Performance( + score=ScoreMetric(name="fake", value=0.1), dashboard_metrics="mDice" + ) + mocker.patch.object(MetricsHelper, "compute_dice_averaged_over_pixels", return_value=fake_metrics) + self.visual_prompting_ov_task.evaluate(result_set) + + assert result_set.performance.score.value == 0.1 + + @e2e_pytest_unit + def test_deploy(self): + """Test deploy.""" + output_model = deepcopy(self.task_environment.model) + self.visual_prompting_ov_task.model.set_data("visual_prompting_image_encoder.xml", b"image_encoder_xml") + self.visual_prompting_ov_task.model.set_data("visual_prompting_image_encoder.bin", b"image_encoder_bin") + self.visual_prompting_ov_task.model.set_data("visual_prompting_decoder.xml", b"decoder_xml") + self.visual_prompting_ov_task.model.set_data("visual_prompting_decoder.bin", b"decoder_bin") + + self.visual_prompting_ov_task.deploy(output_model) + + assert output_model.exportable_code is not None + + @e2e_pytest_unit + def test_optimize(self, mocker): + """Test optimize.""" + + def patch_save_model(model, output_xml): + output_bin = output_xml.replace(".xml", ".bin") + with open(output_xml, "wb") as f: + f.write(f"compressed_{os.path.basename(output_xml)}".encode("utf-8")) + with open(output_bin, "wb") as f: + f.write(f"compressed_{os.path.basename(output_bin)}".encode("utf-8")) + + dataset = generate_visual_prompting_dataset() + output_model = deepcopy(self.task_environment.model) + self.visual_prompting_ov_task.model.set_data("visual_prompting_image_encoder.xml", b"image_encoder_xml") + self.visual_prompting_ov_task.model.set_data("visual_prompting_image_encoder.bin", b"image_encoder_bin") + self.visual_prompting_ov_task.model.set_data("visual_prompting_decoder.xml", b"decoder_xml") + self.visual_prompting_ov_task.model.set_data("visual_prompting_decoder.bin", b"decoder_bin") + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.ov.Core.read_model", autospec=True) + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.ov.save_model", new=patch_save_model) + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.ov.Core.compile_model") + fake_quantize = mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.nncf.quantize", autospec=True) + + self.visual_prompting_ov_task.optimize(OptimizationType.POT, dataset=dataset, output_model=output_model) + + fake_quantize.assert_called() + assert fake_quantize.call_count == 2 + + assert ( + self.visual_prompting_ov_task.model.get_data("visual_prompting_image_encoder.xml") + == b"compressed_visual_prompting_image_encoder.xml" + ) + assert ( + self.visual_prompting_ov_task.model.get_data("visual_prompting_image_encoder.bin") + == b"compressed_visual_prompting_image_encoder.bin" + ) + assert ( + self.visual_prompting_ov_task.model.get_data("visual_prompting_decoder.xml") + == b"compressed_visual_prompting_decoder.xml" + ) + assert ( + self.visual_prompting_ov_task.model.get_data("visual_prompting_decoder.bin") + == b"compressed_visual_prompting_decoder.bin" + ) + + +class TestOpenVINOZeroShotVisualPromptingTask: + @pytest.fixture + def otx_model(self): + model_configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="header", description="description"), + label_schema=LabelSchemaEntity(), + ) + return ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + + @pytest.fixture(autouse=True) + def setup(self, mocker, otx_model): + """Load the OpenVINOZeroShotVisualPromptingTask.""" + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.OpenvinoAdapter") + mocker.patch.object(Model, "create_model") + self.task_environment = init_environment() + visual_prompting_hparams = self.task_environment.get_hyper_parameters(VisualPromptingBaseConfig) + + visual_prompting_ov_inferencer = OpenVINOZeroShotVisualPromptingInferencer( + visual_prompting_hparams, + self.task_environment.label_schema, + {"image_encoder": "", "decoder": ""}, + {"image_encoder": "", "decoder": ""}, + ) + + # self.task_environment.model = mocker.patch("otx.api.entities.model.ModelEntity") + self.task_environment.model = otx_model + mocker.patch.object( + OpenVINOZeroShotVisualPromptingTask, "load_inferencer", return_value=visual_prompting_ov_inferencer + ) + self.zero_shot_visual_prompting_ov_task = OpenVINOZeroShotVisualPromptingTask( + task_environment=self.task_environment + ) + + @e2e_pytest_unit + def test_infer_without_reference_info(self): + """Test infer without reference_info.""" + dataset = generate_visual_prompting_dataset() + + updated_dataset = self.zero_shot_visual_prompting_ov_task.infer( + dataset, InferenceParameters(enable_async_inference=False), "empty_dir" + ) + + for updated in updated_dataset: + assert len(updated.annotation_scene.annotations) == 0 + + @e2e_pytest_unit + def test_infer_with_reference_info(self, mocker): + """Test infer with reference_info.""" + fake_annotation = [ + Annotation( + Polygon(points=[Point(0, 0)]), + id=0, + labels=[ScoredLabel(LabelEntity(name="fake", domain="VISUALPROMPTING"), probability=1.0)], + ) + ] + + mocker_predict = mocker.patch.object( + OpenVINOZeroShotVisualPromptingInferencer, "predict", return_value=fake_annotation + ) + mocker.patch.object(ShapeFactory, "shape_produces_valid_crop", return_value=True) + mocker.patch.object( + self.zero_shot_visual_prompting_ov_task.inferencer, "_get_reference_info", return_value=({}, {}) + ) + + dataset = generate_visual_prompting_dataset() + + updated_dataset = self.zero_shot_visual_prompting_ov_task.infer( + dataset, InferenceParameters(enable_async_inference=False) + ) + + for updated in updated_dataset: + assert updated.annotation_scene.contains_any([LabelEntity(name="fake", domain="VISUALPROMPTING")]) + + mocker_predict.assert_called() + assert mocker_predict.call_count == len(updated_dataset) + + @e2e_pytest_unit + def test_optimize(self, mocker): + """Test optimize.""" + + def patch_save_model(model, output_xml): + output_bin = output_xml.replace(".xml", ".bin") + with open(output_xml, "wb") as f: + f.write(f"compressed_{os.path.basename(output_xml)}".encode("utf-8")) + with open(output_bin, "wb") as f: + f.write(f"compressed_{os.path.basename(output_bin)}".encode("utf-8")) + + dataset = generate_visual_prompting_dataset() + output_model = deepcopy(self.task_environment.model) + self.zero_shot_visual_prompting_ov_task.model.set_data( + "visual_prompting_image_encoder.xml", b"image_encoder_xml" + ) + self.zero_shot_visual_prompting_ov_task.model.set_data( + "visual_prompting_image_encoder.bin", b"image_encoder_bin" + ) + self.zero_shot_visual_prompting_ov_task.model.set_data("visual_prompting_decoder.xml", b"decoder_xml") + self.zero_shot_visual_prompting_ov_task.model.set_data("visual_prompting_decoder.bin", b"decoder_bin") + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.ov.Core.read_model", autospec=True) + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.ov.save_model", new=patch_save_model) + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.ov.Core.compile_model") + fake_quantize = mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.nncf.quantize", autospec=True) + + self.zero_shot_visual_prompting_ov_task.optimize( + OptimizationType.POT, dataset=dataset, output_model=output_model + ) + + fake_quantize.assert_called() + assert fake_quantize.call_count == 2 + + assert ( + self.zero_shot_visual_prompting_ov_task.model.get_data("visual_prompting_image_encoder.xml") + == b"compressed_visual_prompting_image_encoder.xml" + ) + assert ( + self.zero_shot_visual_prompting_ov_task.model.get_data("visual_prompting_image_encoder.bin") + == b"compressed_visual_prompting_image_encoder.bin" + ) + assert ( + self.zero_shot_visual_prompting_ov_task.model.get_data("visual_prompting_decoder.xml") + == b"compressed_visual_prompting_decoder.xml" + ) + assert ( + self.zero_shot_visual_prompting_ov_task.model.get_data("visual_prompting_decoder.bin") + == b"compressed_visual_prompting_decoder.bin" + ) diff --git a/tests/unit/algorithms/visual_prompting/tasks/test_train.py b/tests/unit/algorithms/visual_prompting/tasks/test_train.py new file mode 100644 index 00000000000..5d1ea57c6b2 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/tasks/test_train.py @@ -0,0 +1,47 @@ +"""Tests the methods in the train task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.algorithms.visual_prompting.tasks.train import TrainingTask +from otx.api.entities.metrics import NullPerformance +from otx.api.entities.model import ModelEntity +from otx.api.entities.train_parameters import TrainParameters +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.algorithms.visual_prompting.test_helpers import ( + generate_visual_prompting_dataset, + init_environment, +) + + +class TestTrainingTask: + @pytest.fixture(autouse=True) + def setup(self, tmpdir, mocker): + mocker.patch("pathlib.Path.write_text") + self.task_environment = init_environment() + self.output_path = str(tmpdir.mkdir("visual_prompting_training_test")) + self.training_task = TrainingTask(self.task_environment, self.output_path) + + @e2e_pytest_unit + def test_train(self, mocker): + """Test train.""" + mocker_trainer = mocker.patch("otx.algorithms.visual_prompting.tasks.train.Trainer") + mocker_save = mocker.patch("torch.save") + mocker.patch.object(self.training_task, "model_info") + + dataset = generate_visual_prompting_dataset() + output_model = ModelEntity( + dataset, + self.task_environment.get_model_configuration(), + ) + + self.training_task.train(dataset, output_model, TrainParameters()) + + mocker_trainer.assert_called_once() + mocker_save.assert_called_once() + assert not isinstance(output_model.performance, NullPerformance) + assert output_model.model_adapters.get("weights.pth", None) + assert output_model.model_adapters.get("label_schema.json", None) diff --git a/tests/unit/algorithms/visual_prompting/test_helpers.py b/tests/unit/algorithms/visual_prompting/test_helpers.py new file mode 100644 index 00000000000..5a5343f806a --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/test_helpers.py @@ -0,0 +1,217 @@ +""""Collection of helper functions for unit tests of otx.algorithms.visual_prompting.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import torch +import torch.nn as nn +from typing import List, Optional, Tuple, Any + +import numpy as np + +from otx.api.configuration.helper import create +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from unittest.mock import Mock +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.color import Color +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from tests.test_helpers import generate_random_annotated_image + +DEFAULT_VISUAL_PROMPTING_TEMPLATE_DIR = { + "visual_prompt": os.path.join("src/otx/algorithms/visual_prompting/configs", "sam_vit_b"), + "zero_shot": os.path.join("src/otx/algorithms/visual_prompting/configs", "zero_shot_sam_tiny_vit"), +} + +labels_names = ("rectangle", "ellipse", "triangle") + + +def generate_otx_label_schema(labels_names: List[str] = labels_names): + label_domain = Domain.VISUAL_PROMPTING + rgb = [int(i) for i in np.random.randint(0, 256, 3)] + colors = [Color(*rgb) for _ in range(len(labels_names))] + not_empty_labels = [ + LabelEntity(name=name, color=colors[i], domain=label_domain, id=i) for i, name in enumerate(labels_names) + ] + empty_label = LabelEntity( + name="Empty label", + color=Color(42, 43, 46), + is_empty=True, + domain=label_domain, + id=len(not_empty_labels), + ) + + label_schema = LabelSchemaEntity() + exclusive_group = LabelGroup(name="labels", labels=not_empty_labels, group_type=LabelGroupType.EXCLUSIVE) + empty_group = LabelGroup(name="empty", labels=[empty_label], group_type=LabelGroupType.EMPTY_LABEL) + label_schema.add_group(exclusive_group) + label_schema.add_group(empty_group) + return label_schema + + +def generate_visual_prompting_dataset(use_mask: bool = False) -> DatasetEntity: + items = [] + labels_schema = generate_otx_label_schema() + labels_list = labels_schema.get_labels(False) + for subset in [Subset.TRAINING, Subset.VALIDATION, Subset.TESTING, Subset.NONE]: + image_numpy, shapes = generate_random_annotated_image( + image_width=640, + image_height=480, + labels=labels_list, + max_shapes=20, + min_size=50, + max_size=100, + random_seed=None, + use_mask_as_annotation=use_mask, + ) + + out_shapes = [] + for shape in shapes: + shape_labels = shape.get_labels(include_empty=True) + + in_shape = shape.shape + if use_mask: + if isinstance(in_shape, Image): + out_shapes.append(shape) + else: + if isinstance(in_shape, Rectangle): + points = [ + Point(in_shape.x1, in_shape.y1), + Point(in_shape.x2, in_shape.y1), + Point(in_shape.x2, in_shape.y2), + Point(in_shape.x1, in_shape.y2), + ] + elif isinstance(in_shape, Ellipse): + points = [Point(x, y) for x, y in in_shape.get_evenly_distributed_ellipse_coordinates()] + elif isinstance(in_shape, Polygon): + points = in_shape.points + + out_shapes.append(Annotation(Polygon(points=points), labels=shape_labels)) + image = Image(data=image_numpy) + annotation = AnnotationSceneEntity(kind=AnnotationSceneKind.ANNOTATION, annotations=out_shapes) + items.append(DatasetItemEntity(media=image, annotation_scene=annotation, subset=subset)) + + return DatasetEntity(items) + + +def init_environment(model: Optional[ModelEntity] = None, mode: str = "visual_prompt"): + model_template = parse_model_template( + os.path.join(DEFAULT_VISUAL_PROMPTING_TEMPLATE_DIR.get(mode), "template.yaml") + ) + hyper_parameters = create(model_template.hyper_parameters.data) + labels_schema = generate_otx_label_schema() + environment = TaskEnvironment( + model=model, + hyper_parameters=hyper_parameters, + label_schema=labels_schema, + model_template=model_template, + ) + return environment + + +class MockDatasetConfig: + class _normalize: + mean = [1.0, 1.0, 1.0] + std = [0.0, 0.0, 0.0] + + def __init__(self, use_mask: bool = False): + self.image_size: Tuple[int] = (4, 4) + self.use_mask: bool = use_mask + self.num_workers: int = 1 + self.train_batch_size: int = 1 + self.val_batch_size: int = 1 + self.test_batch_size: int = 1 + self.offset_bbox: int = 0 + self.normalize = self._normalize + + def get(self, value: str, default: Optional[Any] = None) -> Any: + return getattr(self, value, default) + + +class MockConfig: + def __init__(self, use_mask: bool = False): + self.dataset = MockDatasetConfig(use_mask=use_mask) + + +class MockImageEncoder(nn.Module): + def __new__(cls, *args, **kwargs): + return nn.Linear(4, 4) + + +class MockPromptEncoder(nn.Module): + def __init__(self, *args, **kwargs): + super().__init__() + self.layer = nn.Linear(1, 1) + self.embed_dim = 4 + self.pe_layer = None + self.mask_downscaling = None + + def forward(self, *args, **kwargs): + return torch.Tensor([[1]]), torch.Tensor([[1]]) + + def get_dense_pe(self): + return torch.Tensor([[1]]) + + +class MockMaskDecoder(nn.Module): + def __init__(self, *args, **kwargs): + super().__init__() + self.layer = nn.Linear(1, 1) + self.num_mask_tokens = 4 + self.predict_masks = None + + def forward(self, *args, **kwargs): + return torch.Tensor([[1]]), torch.Tensor([[1]]) + + def predict_mask(self, *args, **kwargs): + return self(*args, **kwargs) + + +class MockScoredLabel: + def __init__( + self, + label: int, + name: str = "background", + probability: float = 0.0, + label_source=None, + ): + self.name = name + self.label = Mock() + self.label.id_ = label + self.label.id = label + self.probability = probability + self.label_source = label_source + self.__class__ = ScoredLabel + + +class MockPromptGetter(nn.Module): + def __init__(self, *args, **kwargs): + super().__init__() + + def initialize(self): + pass + + def set_default_thresholds(self, *args, **kwargs): + pass + + def get_prompt_candidates(self, *args, **kwargs): + return {1: torch.Tensor([[0, 0, 0.5]])}, {1: torch.Tensor([[1, 1]])} + + def forward(self, *args, **kwargs): + return torch.tensor([[[0, 0, 0.5], [1, 1, 0.7]]]), torch.tensor([[[2, 2]]]) diff --git a/tests/unit/api/__init__.py b/tests/unit/api/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/api/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/api/configuration/__init__.py b/tests/unit/api/configuration/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/api/configuration/dummy_broken_config.yaml b/tests/unit/api/configuration/dummy_broken_config.yaml new file mode 100644 index 00000000000..e3eaa5b4d55 --- /dev/null +++ b/tests/unit/api/configuration/dummy_broken_config.yaml @@ -0,0 +1,28 @@ +type: CONFIGURABLE_PARAMETERS +description: Configurable parameters for Task X +header: Task X configuration -- TEST ONLY +learning_parameters: + type: PARAMETER_GROUP + header: Learning parameters + batch_size: + type: INTEGER + min_value: 1 + max_value: 255 + default_value: 42 + header: Batch_size + affects_outcome_of: TRAINING + learning_rate: + type: FLOAT + min_value: 1.0e-6 + max_value: 1.0e+6 + default_value: 1.0e-3 + header: Learning rate + affects_outcome_of: TRAINING + epochs: + type: INTEGER + min_value: 1 + max_value: 1000 + default_value: 25 + header: Number of epochs + value: -5 + affects_outcome_of: TRAINING diff --git a/tests/unit/api/configuration/dummy_config.py b/tests/unit/api/configuration/dummy_config.py new file mode 100644 index 00000000000..5cbcb2b6c3a --- /dev/null +++ b/tests/unit/api/configuration/dummy_config.py @@ -0,0 +1,185 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +""" +Dummy configurable parameter class to test configuration functionality +""" + +import attr + +from otx.api.configuration import ( + ConfigurableParameters, + ModelLifecycle, + Operator, + Rule, + UIRules, +) +from otx.api.configuration.elements import ( + ConfigurableEnum, + ParameterGroup, + add_parameter_group, + configurable_boolean, + configurable_float, + configurable_integer, + float_selectable, + selectable, + string_attribute, +) +from otx.api.configuration.enums import AutoHPOState +from otx.api.configuration.ui_rules import Action + + +class SomeEnumSelectable(ConfigurableEnum): + """Test Enum selectable""" + + TEST_NAME1 = "test_name_1" + TEST_2 = "test_2_test" + BOGUS_NAME = "bogus" + OPTION_C = "option_c" + + +@attr.s +class DatasetManagerConfig(ConfigurableParameters): + """Dummy configurable parameters class""" + + # type: ignore + # This class is used for testing purposes only, so mypy should ignore it + + # Component and header are required, description is optional. + header = string_attribute("Dataset Manager configuration -- TEST ONLY") + description = string_attribute("Configurable parameters for the DatasetManager -- TEST ONLY") + + # Add some parameters + number_of_samples_for_auto_train = configurable_integer( + default_value=5, + min_value=1, + max_value=1000, + header="Samples required for new training round", + ) + label_constraints = configurable_boolean(default_value=True, header="Apply label constraints") + + @attr.s + class _NestedParameterGroup(ParameterGroup): + # A nested group of parameters + # header is a required attribute, all parameter groups should define one. + header = string_attribute("Test group of parameter groups") + + @attr.s + class _SubgroupOne(ParameterGroup): + # Subgroup one of the nested group, with a couple of parameters + header = string_attribute("Parameter group one") + + __ui_rules = UIRules( + rules=[ + Rule( + parameter=["nested_parameter_group", "show_subgroup_one"], + operator=Operator.EQUAL_TO, + value=False, + ) + ], + action=Action.HIDE, + ) + + bogus_parameter_one = configurable_float( + default_value=42, + ui_rules=__ui_rules, + header="Bogus parameter to test nested parameter groups", + ) + bogus_parameter_two = configurable_float( + default_value=42, + ui_rules=__ui_rules, + header="Bogus parameter to test nested parameter groups", + ) + + subgroup_one = add_parameter_group(_SubgroupOne) + + show_subgroup_one = configurable_boolean(default_value=True, header="Show the parameters in subgroup one?") + + @attr.s + class _SubgroupTwo(ParameterGroup): + # Subgroup two of the nested group, with a couple of parameters + header = string_attribute("Parameter group two") + bogus_parameter_three = configurable_float( + default_value=42, + header="Bogus parameter to test nested parameter groups", + ) + + bogus_parameter_four = configurable_float( + default_value=42, + header="Bogus parameter to test nested parameter groups", + ) + + subgroup_two = add_parameter_group(_SubgroupTwo) + + @attr.s + class _SubsetParameters(ParameterGroup): + # Parameters governing sample distribution over subsets + header = string_attribute("Subset parameters") + description = string_attribute("Parameters for the different subsets") + + # Add a parameter group 'Subset parameters' + auto_subset_fractions = configurable_boolean( + default_value=True, + description="Test", + header="Automatically determine subset proportions", + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + __ui_rules = UIRules( + rules=[ + Rule( + parameter="auto_subset_fractions", + value=False, + operator=Operator.EQUAL_TO, + ) + ], + action=Action.SHOW, + ) + + train_proportion = configurable_float( + default_value=0.75, + min_value=0.0, + max_value=1.0, + header="Training set proportion", + ui_rules=__ui_rules, + affects_outcome_of=ModelLifecycle.TRAINING, + auto_hpo_state=AutoHPOState.POSSIBLE, + ) + + validation_proportion = configurable_float( + default_value=0.1, + min_value=0.0, + max_value=1.0, + header="Validation set proportion", + ui_rules=__ui_rules, + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + test_proportion = configurable_float( + default_value=0.15, + min_value=0.0, + max_value=1.0, + header="Test set proportion", + ui_rules=__ui_rules, + affects_outcome_of=ModelLifecycle.TRAINING, + ) + + # Add a selectable and float selectable parameter + dummy_float_selectable = float_selectable( + options=[1.0, 2.0, 3.0, 4.0], + default_value=2.0, + header="Test float selectable", + auto_hpo_state=AutoHPOState.POSSIBLE, + ) + + dummy_selectable = selectable( + default_value=SomeEnumSelectable.BOGUS_NAME, + header="Test", + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + # Finally, add the nested parameter group and subset parameter groups to the config + # NOTE! group initialization should use a factory to avoid passing mutable default arguments. This is why the + # add_parameter_group function is needed. + nested_parameter_group = add_parameter_group(_NestedParameterGroup) + subset_parameters = add_parameter_group(_SubsetParameters) diff --git a/tests/unit/api/configuration/dummy_config.yaml b/tests/unit/api/configuration/dummy_config.yaml new file mode 100644 index 00000000000..ab718297c83 --- /dev/null +++ b/tests/unit/api/configuration/dummy_config.yaml @@ -0,0 +1,134 @@ +type: CONFIGURABLE_PARAMETERS +description: Configurable parameters for the DatasetManager -- TEST ONLY +header: Dataset Manager configuration -- TEST ONLY +number_of_samples_for_auto_train: + type: INTEGER + default_value: 5 + min_value: 1 + max_value: 1000 + header: Samples required for new training round +label_constraints: + type: BOOLEAN + default_value: true + header: Apply label constraints +nested_parameter_group: + type: PARAMETER_GROUP + header: Test group of parameter groups + subgroup_one: + type: PARAMETER_GROUP + header: Parameter group one + bogus_parameter_one: + type: FLOAT + default_value: 42.0 + header: Bogus parameter to test nested parameter groups + ui_rules: + rules: + - parameter: + - nested_parameter_group + - show_subgroup_one + type: RULE + value: false + action: HIDE + type: UI_RULES + bogus_parameter_two: + type: FLOAT + default_value: 42.0 + header: Bogus parameter to test nested parameter groups + ui_rules: + rules: + - parameter: + - nested_parameter_group + - show_subgroup_one + type: RULE + value: false + action: HIDE + type: UI_RULES + show_subgroup_one: + type: BOOLEAN + header: Show the parameters in subgroup one? + default_value: true + subgroup_two: + type: PARAMETER_GROUP + header: Parameter group two + bogus_parameter_four: + type: FLOAT + default_value: 42.0 + header: Bogus parameter to test nested parameter groups + bogus_parameter_three: + type: FLOAT + default_value: 42.0 + header: Bogus parameter to test nested parameter groups +subset_parameters: + type: PARAMETER_GROUP + header: Subset parameters + description: Parameters for the different subsets + auto_subset_fractions: + default_value: true + description: Test + affects_outcome_of: TRAINING + header: Automatically determine subset proportions + type: BOOLEAN + test_proportion: + type: FLOAT + min_value: 0.0 + max_value: 1.0 + default_value: 0.15 + header: Test set proportion + affects_outcome_of: TRAINING + ui_rules: + rules: + - parameter: auto_subset_fractions + type: RULE + value: false + action: SHOW + type: UI_RULES + train_proportion: + type: FLOAT + min_value: 0.0 + max_value: 1.0 + default_value: 0.75 + header: Training set proportion + affects_outcome_of: TRAINING + ui_rules: + rules: + - parameter: auto_subset_fractions + type: RULE + value: false + action: SHOW + type: UI_RULES + auto_hpo_state: possible + validation_proportion: + type: FLOAT + min_value: 0.0 + max_value: 1.0 + default_value: 0.1 + header: Validation set proportion + affects_outcome_of: TRAINING + ui_rules: + rules: + - parameter: auto_subset_fractions + type: RULE + value: false + action: SHOW + type: UI_RULES +dummy_float_selectable: + default_value: 2.0 + header: Test float selectable + options: + - 1.0 + - 2.0 + - 3.0 + - 4.0 + type: FLOAT_SELECTABLE + auto_hpo_state: possible +dummy_selectable: + default_value: bogus + header: Test + enum_name: SomeEnumSelectable + affects_outcome_of: INFERENCE + options: + BOGUS_NAME: bogus + TEST_2: test_2_test + TEST_NAME1: test_name_1 + OPTION_C: option_c + type: SELECTABLE diff --git a/tests/unit/api/configuration/elements/test_elements_utils.py b/tests/unit/api/configuration/elements/test_elements_utils.py new file mode 100644 index 00000000000..f523ed0fe83 --- /dev/null +++ b/tests/unit/api/configuration/elements/test_elements_utils.py @@ -0,0 +1,399 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from types import FunctionType + +import pytest +from attr import fields + +from otx.api.configuration import ConfigurableParameters +from otx.api.configuration.elements.parameter_group import ParameterGroup +from otx.api.configuration.elements.utils import ( + _convert_enum_selectable_value, + _validate_and_convert_float, + attr_enum_to_str_serializer, + attr_strict_float_converter, + attr_strict_float_on_setattr, + attr_strict_int_validator, + construct_attr_enum_selectable_converter, + construct_attr_enum_selectable_onsetattr, + construct_attr_selectable_validator, + construct_attr_value_validator, + convert_string_to_id, +) +from otx.api.configuration.enums.config_element_type import ElementCategory +from otx.api.entities.id import ID +from tests.unit.api.configuration.dummy_config import SomeEnumSelectable +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestUtilsFunctions: + parameter_group = ParameterGroup(header="test header") + attribute = fields(ConfigurableParameters) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_utils_attr_enum_to_str_serializer(self): + """ + Description: + Check "attr_enum_to_str_serializer" function + + Input data: + "instance" Enum object, "attribute" Attribute object, "value" parameter + + Expected results: + Test passes if value returned by "attr_enum_to_str_serializer" function is equal to expected + + Steps + 1. Check value returned by "attr_enum_to_str_serializer" function when "Enum" object is specified as "value" + parameter + 2. Check value returned by "attr_enum_to_str_serializer" function when "str" object is specified as "value" + parameter + """ + # Checking value returned by "attr_enum_to_str_serializer" when "Enum" object is specified as "value" + assert ( + attr_enum_to_str_serializer( + instance=ElementCategory, + attribute=ElementCategory.PRIMITIVES.name, + value=ElementCategory.PRIMITIVES, + ) + == "PRIMITIVES" + ) + assert ( + attr_enum_to_str_serializer( + instance=ElementCategory, + attribute=ElementCategory.RULES.name, + value=ElementCategory.RULES, + ) + == "RULES" + ) + # Checking value returned by "attr_enum_to_str_serializer" when "str" object is specified as "value" + assert ( + attr_enum_to_str_serializer( + instance=ElementCategory, + attribute=self.attribute.id, + value="non enum string", + ) + == "non enum string" + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_convert_enum_selectable_value(self): + """ + Description: + Check "_convert_enum_selectable_value" function + + Input data: + "value" parameter, "enum_class" ConfigurableEnum object + + Expected results: + Test passes if ConfigurableEnum class element returned by "_convert_enum_selectable_value" function is + equal to expected + + Steps + 1. Check ConfigurableEnum class element returned by "_convert_enum_selectable_value" function when string + is specified as "value" parameter + 2. Check ConfigurableEnum class element returned by "_convert_enum_selectable_value" function when + ConfigurableEnum class element is specified as "value" parameter + 3. Check that ValueError exception is raised by "_convert_enum_selectable_value" function when unexpected string + is specified as "value" parameter + """ + # Checking ConfigurableEnum element returned by "_convert_enum_selectable_value" when string is specified as + # "value" + assert ( + _convert_enum_selectable_value(value="test_2_test", enum_class=SomeEnumSelectable) + == SomeEnumSelectable.TEST_2 + ) + # Checking ConfigurableEnum element returned by "_convert_enum_selectable_value" when ConfigurableEnum element + # is specified as "value" + assert ( + _convert_enum_selectable_value(value=SomeEnumSelectable.OPTION_C, enum_class=SomeEnumSelectable) + == SomeEnumSelectable.OPTION_C + ) + # Checking that ValueError exception is raised by "_convert_enum_selectable_value" when unexpected string is + # specified as "value" + with pytest.raises(ValueError): + _convert_enum_selectable_value(value="some string", enum_class=SomeEnumSelectable) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_construct_attr_enum_selectable_converter(self): + """ + Description: + Check "construct_attr_enum_selectable_converter" function + + Input data: + "default_value" ConfigurableEnum element + + Expected results: + Test passes if function returned by "construct_attr_enum_selectable_converter" function is equal to expected + """ + converter = construct_attr_enum_selectable_converter(default_value=SomeEnumSelectable.TEST_NAME1) + assert isinstance(converter, FunctionType) + assert converter(SomeEnumSelectable.BOGUS_NAME) == SomeEnumSelectable.BOGUS_NAME + assert converter("test_2_test") == SomeEnumSelectable.TEST_2 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_construct_attr_enum_selectable_onsetattr(self): + """ + Description: + Check "construct_attr_enum_selectable_onsetattr" function + + Input data: + "default_value" ConfigurableEnum element, "parameter_group" ParameterGroup object, "attribute" Attribute object, + "value" parameter + + Expected results: + Test passes if function returned by "construct_attr_enum_selectable_onsetattr" function is equal to expected + """ + on_set_attr = construct_attr_enum_selectable_onsetattr(default_value=SomeEnumSelectable.TEST_2) + assert isinstance(on_set_attr, FunctionType) + assert ( + on_set_attr( + self.parameter_group, + SomeEnumSelectable.TEST_2.value, + SomeEnumSelectable.TEST_2, + ) + == SomeEnumSelectable.TEST_2 + ) + assert ( + on_set_attr(self.parameter_group, SomeEnumSelectable.OPTION_C.value, "option_c") + == SomeEnumSelectable.OPTION_C + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_construct_attr_value_validator(self): + """ + Description: + Check "construct_attr_value_validator" function + + Input data: + "min_value" and "max_value" parameters, "parameter_group" ParameterGroup object, "attribute" Attribute object, + "value" parameter + + Expected results: + Test passes if validator returned by "construct_attr_value_validator" function is equal to expected + + Steps + 1. Check that ValueError exception is not raised by validator returned by "construct_attr_value_validator" + function for values within specified bounds + 2. Check that ValueError exception is raised by validator returned by "construct_attr_value_validator" function + for values out of specified bounds + """ + attr_value_validator = construct_attr_value_validator(min_value=1, max_value=4) + # Checking that ValueError exception is not raised by validator returned by "construct_attr_value_validator" + # for values within specified bounds + for value_within in range(1, 5): + attr_value_validator(self.parameter_group, self.attribute.id, value_within) + # Checking that ValueError exception is raised by validator returned by "construct_attr_value_validator" for + # values out of specified bounds + for out_of_bounds_value in [0, 5]: + with pytest.raises(ValueError): + attr_value_validator(self.parameter_group, self.attribute.id, out_of_bounds_value) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_construct_attr_selectable_validator(self): + """ + Description: + Check "construct_attr_selectable_validator" function + + Input data: + "options" list + + Expected results: + Test passes if validator returned by "construct_attr_selectable_validator" function is equal to expected + + Steps + 1. Check that ValueError exception is not raised by validator returned by "construct_attr_selectable_validator" + function for values included in "options" list + 2. Check that ValueError exception is raised by validator returned by "construct_attr_selectable_validator" + function for values not included in "options" list + """ + attr_selectable_validator = construct_attr_selectable_validator(options=["str_option", 2]) + # Checking that ValueError exception is not raised by validator returned by + # "construct_attr_selectable_validator" for values included in "options" + for value_within in ["str_option", 2]: + attr_selectable_validator(self.parameter_group, self.attribute.id, value_within) + # Checking that ValueError exception is raised by validator returned by "construct_attr_selectable_validator" + # for values not included in "options" + for out_of_bounds_value in ["other_str_option", 3]: + with pytest.raises(ValueError): + attr_selectable_validator(self.parameter_group, self.attribute.id, out_of_bounds_value) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_convert_string_to_id(self): + """ + Description: + Check "convert_string_to_id" function + + Input data: + "id_string" string or ID object + + Expected results: + Test passes if ID object returned by "convert_string_to_id" function is equal to expected + + Steps + 1. Check ID object returned by "convert_string_to_id" function for string "id_string" parameter + 2. Check ID object returned by "convert_string_to_id" function for ID "id_string" parameter + 3. Check ID object returned by "convert_string_to_id" function for "id_string" parameter equal to None + 4. Check ID object returned by "convert_string_to_id" function for int "id_string" parameter + """ + # Checking ID returned by "convert_string_to_id" for string "id_string" + assert convert_string_to_id("some_id") == ID("some_id") + # Checking ID returned by "convert_string_to_id" for ID "id_string" + assert convert_string_to_id(ID("id_string")) == ID("id_string") + # Checking ID returned by "convert_string_to_id" for "id_string" equal to None + assert convert_string_to_id(None) == ID() + # Checking ID returned by "convert_string_to_id" for int "id_string" + assert convert_string_to_id(4) == 4 # type: ignore + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_attr_strict_int_validator(self): + """ + Description: + Check "attr_strict_int_validator" function + + Input data: + "parameter_group" ParameterGroup object, "attribute" Attribute object, "value" parameter + + Expected results: + Test passes if "attr_strict_int_validator" function raises TypeError exception for non-int "value" + parameter + + Steps + 1. Check that "attr_strict_int_validator" function not raises TypeError exception for int "value" parameter + 2. Check that "attr_strict_int_validator" function raises TypeError exception for bool "value" parameter + 3. Check that "attr_strict_int_validator" function raises TypeError exception for string "value" parameter + """ + # Checking that "attr_strict_int_validator" not raises TypeError exception for int "value" + attr_strict_int_validator(instance=self.parameter_group, attribute=self.attribute.id, value=1) + # Checking that "attr_strict_int_validator" raises TypeError exception for bool "value" + with pytest.raises(TypeError): + attr_strict_int_validator(instance=self.parameter_group, attribute=self.attribute.id, value=True) + # Checking that "attr_strict_int_validator" raises TypeError exception for string "value" + with pytest.raises(TypeError): + attr_strict_int_validator( + instance=self.parameter_group, + attribute=self.attribute.id, + value="some string", # type: ignore + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_validate_and_convert_float(self): + """ + Description: + Check "_validate_and_convert_float" function + + Input data: + "value" parameter + + Expected results: + Test passes if value returned by "_validate_and_convert_float" function is equal to expected + + Steps + 1. Check value returned by "_validate_and_convert_float" function for float "value" parameter + 2. Check value returned by "_validate_and_convert_float" function for int "value" parameter + 3. Check value returned by "_validate_and_convert_float" function for bool "value" parameter + 4. Check value returned by "_validate_and_convert_float" function for str "value" parameter + """ + # Checking value returned by "_validate_and_convert_float" for float "value" + assert _validate_and_convert_float(value=1.3) == 1.3 + # Checking value returned by "_validate_and_convert_float" for int "value" + converted_value = _validate_and_convert_float(value=2) + assert isinstance(converted_value, float) + assert converted_value == float(2) + # Checking value returned by "_validate_and_convert_float" for bool "value" + assert not _validate_and_convert_float(value=True) + assert not _validate_and_convert_float(value=False) + # Checking value returned by "_validate_and_convert_float" for str "value" + assert not _validate_and_convert_float(value="some string") # type: ignore + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_attr_strict_float_on_setattr(self): + """ + Description: + Check "attr_strict_float_on_setattr" function + + Input data: + "parameter_group" ParameterGroup object, "attribute" Attribute object, "value" parameter + + Expected results: + Test passes if value returned by "attr_strict_float_on_setattr" function is equal to expected + + Steps + 1. Check value returned by "attr_strict_float_on_setattr" function for float "value" parameter + 2. Check value returned by "attr_strict_float_on_setattr" function for int "value" parameter + 3. Check that "attr_strict_float_on_setattr" function raises TypeError exception for bool "value" parameter + 4. Check that "attr_strict_float_on_setattr" function raises TypeError exception for str "value" parameter + """ + # Checking value returned by "attr_strict_float_on_setattr" for float "value" + assert ( + attr_strict_float_on_setattr(instance=self.parameter_group, attribute=self.attribute.id, value=10.7) == 10.7 + ) + # Checking value returned by "attr_strict_float_on_setattr" for int "value" + converted_value = attr_strict_float_on_setattr( + instance=self.parameter_group, attribute=self.attribute.id, value=2 + ) + assert isinstance(converted_value, float) + assert converted_value == float(2) + # Checking that "attr_strict_float_on_setattr" raises TypeError exception for bool "value" + with pytest.raises(TypeError): + attr_strict_float_on_setattr(instance=self.parameter_group, attribute=self.attribute.id, value=True) + # Checking that "attr_strict_float_on_setattr" raises TypeError exception for str "value" + with pytest.raises(TypeError): + attr_strict_float_on_setattr( + instance=self.parameter_group, + attribute=self.attribute.id, + value="some string", # type: ignore + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_attr_strict_float_converter(self): + """ + Description: + Check "attr_strict_float_converter" function + + Input data: + "value" parameter + + Expected results: + Test passes if value returned by "attr_strict_float_converter" function is equal to expected + + Steps + 1. Check value returned by "attr_strict_float_converter" function for float "value" parameter + 2. Check value returned by "attr_strict_float_converter" function for int "value" parameter + 3. Check value returned by "attr_strict_float_converter" function for bool or str "value" parameter + """ + # Checking value returned by "attr_strict_float_converter" for float "value" + assert attr_strict_float_converter(value=20.1) == 20.1 + # Checking value returned by "attr_strict_float_converter" for int "value" + converted_value = attr_strict_float_converter(value=0) + assert isinstance(converted_value, float) + assert converted_value == float(0) + # Checking that "attr_strict_float_converter" raises TypeError for bool or str "value" + for non_bool_value in [True, False, "some string"]: + with pytest.raises(TypeError): + attr_strict_float_converter(non_bool_value) # type: ignore diff --git a/tests/unit/api/configuration/elements/test_metadata_keys.py b/tests/unit/api/configuration/elements/test_metadata_keys.py new file mode 100644 index 00000000000..80cd99d7706 --- /dev/null +++ b/tests/unit/api/configuration/elements/test_metadata_keys.py @@ -0,0 +1,136 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.api.configuration.elements.metadata_keys import ( + AFFECTS_OUTCOME_OF, + DEFAULT_VALUE, + DESCRIPTION, + EDITABLE, + ENUM_NAME, + HEADER, + MAX_VALUE, + MIN_VALUE, + OPTIONS, + TYPE, + UI_RULES, + VISIBLE_IN_UI, + WARNING, + allows_dictionary_values, + allows_model_template_override, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestMetadataKeys: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_metadata_keys_constants(self): + """ + Description: + Check metadata_keys constants + + Input data: + metadata_keys constants + + Expected results: + Test passes if values of metadata_keys constants are equal to expected + """ + assert DEFAULT_VALUE == "default_value" + assert MIN_VALUE == "min_value" + assert MAX_VALUE == "max_value" + assert DESCRIPTION == "description" + assert HEADER == "header" + assert WARNING == "warning" + assert EDITABLE == "editable" + assert VISIBLE_IN_UI == "visible_in_ui" + assert AFFECTS_OUTCOME_OF == "affects_outcome_of" + assert UI_RULES == "ui_rules" + assert TYPE == "type" + assert OPTIONS == "options" + assert ENUM_NAME == "enum_name" + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_allows_model_template_override(self): + """ + Description: + Check "allows_model_template_override" function + + Input data: + "keyword" constant + + Expected results: + Test passes if value returned by "allows_model_template_override" function is equal to expected + + Steps + 1. Check value returned by "allows_model_template_override" function for "keyword" that can be overridden + 2. Check value returned by "allows_model_template_override" function for "keyword" that can not be overridden + """ + # Checking value returned by "allows_model_template_override" for "keyword" that can be overridden + for keyword in [ + DEFAULT_VALUE, + MIN_VALUE, + MAX_VALUE, + DESCRIPTION, + HEADER, + EDITABLE, + WARNING, + VISIBLE_IN_UI, + OPTIONS, + ENUM_NAME, + UI_RULES, + AFFECTS_OUTCOME_OF, + ]: + assert allows_model_template_override(keyword) + # Checking value returned by "allows_model_template_override" for "keyword" that can not be overridden + for keyword in [TYPE, "non-constant keyword"]: + assert not allows_model_template_override(keyword) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_allows_dictionary_values(self): + """ + Description: + Check "allows_dictionary_values" function + + Input data: + "keyword" constant + + Expected results: + Test passes if value returned by "allows_dictionary_values" function is equal to expected + + Steps + 1. Check value returned by "allows_dictionary_values" function for "keyword" that allowed to have a dictionary + as its value + 2. Check value returned by "allows_dictionary_values" function for "keyword" that not allowed to have a + dictionary as its value + """ + # Checking value returned by "allows_dictionary_values" for "keyword" that allowed to have a dictionary as its + # value + for keyword in [UI_RULES, OPTIONS]: + assert allows_dictionary_values(keyword) + # Checking value returned by "allows_dictionary_values" for "keyword" that not allowed to have a dictionary as + # its value + for keyword in [ + DEFAULT_VALUE, + MIN_VALUE, + MAX_VALUE, + DESCRIPTION, + HEADER, + WARNING, + EDITABLE, + VISIBLE_IN_UI, + AFFECTS_OUTCOME_OF, + TYPE, + ENUM_NAME, + "non-constant keyword", + ]: + assert not allows_dictionary_values(keyword) diff --git a/tests/unit/api/configuration/elements/test_primitive_parameters.py b/tests/unit/api/configuration/elements/test_primitive_parameters.py new file mode 100644 index 00000000000..1402f410a42 --- /dev/null +++ b/tests/unit/api/configuration/elements/test_primitive_parameters.py @@ -0,0 +1,619 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from typing import Optional + +import pytest +from attr import _make, validators + +from otx.api.configuration.elements.configurable_enum import ConfigurableEnum +from otx.api.configuration.elements.primitive_parameters import ( + boolean_attribute, + configurable_boolean, + configurable_float, + configurable_integer, + float_selectable, + selectable, + set_common_metadata, + string_attribute, +) +from otx.api.configuration.enums import AutoHPOState, ConfigElementType, ModelLifecycle +from otx.api.configuration.ui_rules import NullUIRules, Rule, UIRules +from tests.unit.api.configuration.dummy_config import SomeEnumSelectable +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestPrimitiveParameters: + ui_rules = UIRules(rules=[Rule(parameter="rule parameter", value=1)]) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_set_common_metadata(self): + """ + Description: + Check "set_common_metadata" function + + Input data: + "default_value" int, float, str, bool or ConfigurableEnum element, "header" string, "description" string, + "warning" string, "editable" bool value, "affects_outcome_of" ModelLifecycle element, "ui_rules" UIRules object, + "visible_in_ui" bool value, "parameter_type" ConfigElementType element + + Expected results: + Test passes if dictionary returned by "set_common_metadata" function is equal to expected + """ + header = "test header" + description = "test description" + warning = "test warning" + editable = False + affects_outcome_of = ModelLifecycle.TESTING + ui_rules = self.ui_rules + visible_in_ui = True + parameter_type = ConfigElementType.CONFIGURABLE_PARAMETERS + auto_hpo_state = AutoHPOState.POSSIBLE + + for default_value in [ + 5, # int "default_value" + 1.3, # float "default_value" + "default value string", # str "default_value" + False, # bool "default_value" + SomeEnumSelectable.TEST_2, # ConfigurableEnum "default_value" + ]: + assert set_common_metadata( + default_value=default_value, + header=header, + description=description, + warning=warning, + editable=editable, + affects_outcome_of=affects_outcome_of, + ui_rules=ui_rules, + visible_in_ui=visible_in_ui, + parameter_type=parameter_type, + auto_hpo_state=auto_hpo_state, + auto_hpo_value=default_value, + ) == { + "default_value": default_value, + "description": description, + "header": header, + "warning": warning, + "editable": editable, + "visible_in_ui": visible_in_ui, + "affects_outcome_of": affects_outcome_of, + "ui_rules": ui_rules, + "type": parameter_type, + "auto_hpo_state": auto_hpo_state, + "auto_hpo_value": default_value, + } + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_configurable_integer(self): + """ + Description: + Check "configurable_integer" function + + Input data: + "default_value" int, "header" string, "min_value" int, "max_value": int, "description" string, "warning" string, + "editable" bool, "visible_in_ui" bool, "affects_outcome_of" ModelLifecycle element, "ui_rules" UIRules object + + Expected results: + Test passes if _CountingAttr object returned by "configurable_integer" function is equal to expected + + Steps + 1. Check _CountingAttr object returned by "configurable_integer" function for default values of optional + parameters + 2. Check _CountingAttr object returned by "configurable_integer" function for specified values of optional + parameters + """ + + def check_configurable_integer( + integer_instance, + expected_min_value: int = 0, + expected_max_value: int = 255, + expected_description: str = "Default integer description", + expected_warning: str = None, + expected_editable: bool = True, + expected_visible_in_ui: bool = True, + expected_affects_outcome_of: ModelLifecycle = ModelLifecycle.NONE, + expected_ui_rules: UIRules = NullUIRules(), + expected_auto_hpo_state: AutoHPOState = AutoHPOState.NOT_POSSIBLE, + expected_auto_hpo_value: int = None, + ): + expected_metadata = { + "default_value": 100, + "description": expected_description, + "header": "configurable_integer header", + "warning": expected_warning, + "editable": expected_editable, + "visible_in_ui": expected_visible_in_ui, + "affects_outcome_of": expected_affects_outcome_of, + "ui_rules": expected_ui_rules, + "type": ConfigElementType.INTEGER, + "min_value": expected_min_value, + "max_value": expected_max_value, + "auto_hpo_state": expected_auto_hpo_state, + "auto_hpo_value": expected_auto_hpo_value, + } + assert isinstance(integer_instance, _make._CountingAttr) + assert integer_instance._default == 100 + assert integer_instance.type == int + assert len(integer_instance._validator._validators) == 2 + assert integer_instance.metadata == expected_metadata + + # Checking _CountingAttr object returned by "configurable_integer" for default values of optional parameters + default_value = 100 + header = "configurable_integer header" + actual_integer = configurable_integer(default_value=default_value, header=header) + check_configurable_integer(integer_instance=actual_integer) # type: ignore + # Checking _CountingAttr object returned by "configurable_integer" for specified values of optional parameters + min_value = 10 + max_value = 200 + description = "configurable_integer description" + warning = "configurable_integer warning" + editable = False + visible_in_ui = False + affects_outcome_of = ModelLifecycle.TESTING + ui_rules = self.ui_rules + auto_hpo_state = AutoHPOState.POSSIBLE + auto_hpo_value = min_value + + actual_integer = configurable_integer( + default_value=default_value, + header=header, + min_value=min_value, + max_value=max_value, + description=description, + warning=warning, + editable=editable, + visible_in_ui=visible_in_ui, + affects_outcome_of=affects_outcome_of, + ui_rules=ui_rules, + auto_hpo_value=auto_hpo_value, + auto_hpo_state=auto_hpo_state, + ) + check_configurable_integer( + integer_instance=actual_integer, # type: ignore + expected_min_value=min_value, + expected_max_value=max_value, + expected_description=description, + expected_warning=warning, + expected_editable=editable, + expected_visible_in_ui=visible_in_ui, + expected_affects_outcome_of=affects_outcome_of, + expected_ui_rules=ui_rules, + expected_auto_hpo_state=auto_hpo_state, + expected_auto_hpo_value=auto_hpo_value, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_configurable_float(self): + """ + Description: + Check "configurable_float" function + + Input data: + "default_value" float, "header" string, "min_value" float, "max_value": float, "description" string, + "warning" string, "editable" bool, "visible_in_ui" bool, "affects_outcome_of" ModelLifecycle element, + "ui_rules" UIRules object + + Expected results: + Test passes if _CountingAttr object returned by "configurable_float" function is equal to expected + + Steps + 1. Check _CountingAttr object returned by "configurable_float" function for default values of optional + parameters + 2. Check _CountingAttr object returned by "configurable_float" function for specified values of optional + parameters + """ + + def check_configurable_float( + float_instance, + expected_min_value: float = 0.0, + expected_max_value: float = 255.0, + expected_step_size: Optional[float] = None, + expected_description: str = "Default float description", + expected_warning: str = None, + expected_editable: bool = True, + expected_visible_in_ui: bool = True, + expected_affects_outcome_of: ModelLifecycle = ModelLifecycle.NONE, + expected_ui_rules: UIRules = NullUIRules(), + expected_auto_hpo_state: AutoHPOState = AutoHPOState.NOT_POSSIBLE, + expected_auto_hpo_value: float = None, + ): + expected_metadata = { + "default_value": 100.1, + "description": expected_description, + "header": "configurable_float header", + "warning": expected_warning, + "editable": expected_editable, + "visible_in_ui": expected_visible_in_ui, + "affects_outcome_of": expected_affects_outcome_of, + "ui_rules": expected_ui_rules, + "type": ConfigElementType.FLOAT, + "min_value": expected_min_value, + "max_value": expected_max_value, + "step_size": expected_step_size, + "auto_hpo_state": expected_auto_hpo_state, + "auto_hpo_value": expected_auto_hpo_value, + } + assert isinstance(float_instance, _make._CountingAttr) + assert float_instance._default == 100.1 + assert float_instance.type == float + assert float_instance.metadata == expected_metadata + + # Checking _CountingAttr object returned by "configurable_float" for default values of optional parameters + default_value = 100.1 + header = "configurable_float header" + actual_float = configurable_float(default_value=default_value, header=header) + check_configurable_float(float_instance=actual_float) # type: ignore + # Checking _CountingAttr object returned by "configurable_float" for specified values of optional parameters + min_value = 0.1 + max_value = 160.2 + step_size = 0.3 + description = "configurable_float description" + warning = "configurable_float warning" + editable = False + visible_in_ui = False + affects_outcome_of = ModelLifecycle.TESTING + ui_rules = self.ui_rules + auto_hpo_state = AutoHPOState.POSSIBLE + auto_hpo_value = min_value + + actual_float = configurable_float( + default_value=default_value, + header=header, + min_value=min_value, + max_value=max_value, + step_size=step_size, + description=description, + warning=warning, + editable=editable, + visible_in_ui=visible_in_ui, + affects_outcome_of=affects_outcome_of, + ui_rules=ui_rules, + auto_hpo_value=auto_hpo_value, + auto_hpo_state=auto_hpo_state, + ) + check_configurable_float( + float_instance=actual_float, # type: ignore + expected_min_value=min_value, + expected_max_value=max_value, + expected_step_size=step_size, + expected_description=description, + expected_warning=warning, + expected_editable=editable, + expected_visible_in_ui=visible_in_ui, + expected_affects_outcome_of=affects_outcome_of, + expected_ui_rules=ui_rules, + expected_auto_hpo_state=auto_hpo_state, + expected_auto_hpo_value=auto_hpo_value, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_configurable_boolean(self): + """ + Description: + Check "configurable_boolean" function + + Input data: + "default_value" bool, "header" string, "description" string, "warning" string, "editable" bool, + "visible_in_ui" bool, "affects_outcome_of" ModelLifecycle element, "ui_rules" UIRules object + + Expected results: + Test passes if _CountingAttr object returned by "configurable_boolean" function is equal to expected + + Steps + 1. Check _CountingAttr object returned by "configurable_boolean" function for default values of optional + parameters + 2. Check _CountingAttr object returned by "configurable_boolean" function for specified values of optional + parameters + """ + + def check_configurable_boolean( + boolean_instance, + expected_description: str = "Default configurable boolean description", + expected_warning: str = None, + expected_editable: bool = True, + expected_visible_in_ui: bool = True, + expected_affects_outcome_of: ModelLifecycle = ModelLifecycle.NONE, + expected_ui_rules: UIRules = NullUIRules(), + expected_auto_hpo_state: AutoHPOState = AutoHPOState.NOT_POSSIBLE, + expected_auto_hpo_value: bool = None, + ): + expected_metadata = { + "default_value": True, + "description": expected_description, + "header": "configurable_boolean header", + "warning": expected_warning, + "editable": expected_editable, + "visible_in_ui": expected_visible_in_ui, + "affects_outcome_of": expected_affects_outcome_of, + "ui_rules": expected_ui_rules, + "type": ConfigElementType.BOOLEAN, + "auto_hpo_state": expected_auto_hpo_state, + "auto_hpo_value": expected_auto_hpo_value, + } + assert isinstance(boolean_instance, _make._CountingAttr) + assert boolean_instance._default + assert boolean_instance.type == bool + assert isinstance(boolean_instance._validator, validators._InstanceOfValidator) # type: ignore + assert boolean_instance._validator.type == bool + assert boolean_instance.metadata == expected_metadata + + # Checking _CountingAttr object returned by "configurable_boolean" for default values of optional parameters + default_value = True + header = "configurable_boolean header" + actual_boolean = configurable_boolean(default_value=default_value, header=header) + check_configurable_boolean(boolean_instance=actual_boolean) # type: ignore + # Checking _CountingAttr object returned by "configurable_boolean" for specified values of optional parameters + description = "configurable_boolean description" + warning = "configurable_boolean warning" + editable = False + visible_in_ui = False + affects_outcome_of = ModelLifecycle.TESTING + ui_rules = self.ui_rules + auto_hpo_state = AutoHPOState.POSSIBLE + auto_hpo_value = False + + actual_boolean = configurable_boolean( + default_value=default_value, + header=header, + description=description, + warning=warning, + editable=editable, + visible_in_ui=visible_in_ui, + affects_outcome_of=affects_outcome_of, + ui_rules=ui_rules, + auto_hpo_value=auto_hpo_value, + auto_hpo_state=auto_hpo_state, + ) + check_configurable_boolean( + boolean_instance=actual_boolean, # type: ignore + expected_description=description, + expected_warning=warning, + expected_editable=editable, + expected_visible_in_ui=visible_in_ui, + expected_affects_outcome_of=affects_outcome_of, + expected_ui_rules=ui_rules, + expected_auto_hpo_value=auto_hpo_value, + expected_auto_hpo_state=auto_hpo_state, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_float_selectable(self): + """ + Description: + Check "float_selectable" function + + Input data: + "default_value" float, "header" string, "options" list, "description" string, "warning" string, "editable" bool, + "visible_in_ui" bool, "affects_outcome_of" ModelLifecycle element, "ui_rules" UIRules object + + Expected results: + Test passes if _CountingAttr object returned by "float_selectable" function is equal to expected + + Steps + 1. Check _CountingAttr object returned by "float_selectable" function for default values of optional parameters + 2. Check _CountingAttr object returned by "float_selectable" function for specified values of optional + parameters + """ + + def check_float_selectable( + float_selectable_instance, + expected_description: str = "Default selectable description", + expected_warning: str = None, + expected_editable: bool = True, + expected_visible_in_ui: bool = True, + expected_affects_outcome_of: ModelLifecycle = ModelLifecycle.NONE, + expected_ui_rules: UIRules = NullUIRules(), + expected_auto_hpo_state: AutoHPOState = AutoHPOState.NOT_POSSIBLE, + expected_auto_hpo_value: float = None, + ): + expected_metadata = { + "default_value": 0.1, + "description": expected_description, + "header": "float_selectable header", + "warning": expected_warning, + "editable": expected_editable, + "visible_in_ui": expected_visible_in_ui, + "affects_outcome_of": expected_affects_outcome_of, + "ui_rules": expected_ui_rules, + "type": ConfigElementType.FLOAT_SELECTABLE, + "options": [0.2, 1.4, 2.8], + "auto_hpo_state": expected_auto_hpo_state, + "auto_hpo_value": expected_auto_hpo_value, + } + assert isinstance(float_selectable_instance, _make._CountingAttr) + assert float_selectable_instance._default + assert float_selectable_instance.type == float + assert float_selectable_instance.metadata == expected_metadata + + # Checking _CountingAttr object returned by "float_selectable" for default values of optional parameters + default_value = 0.1 + header = "float_selectable header" + options = [0.2, 1.4, 2.8] + actual_float_selectable = float_selectable(default_value=default_value, options=options, header=header) + check_float_selectable(float_selectable_instance=actual_float_selectable) # type: ignore + # Checking _CountingAttr object returned by "float_selectable" for specified values of optional parameters + description = "float_selectable description" + warning = "float_selectable warning" + editable = False + visible_in_ui = False + affects_outcome_of = ModelLifecycle.TESTING + ui_rules = self.ui_rules + auto_hpo_state = AutoHPOState.POSSIBLE + auto_hpo_value = options[-1] + + actual_float_selectable = float_selectable( + default_value=default_value, + options=options, + header=header, + description=description, + warning=warning, + editable=editable, + visible_in_ui=visible_in_ui, + affects_outcome_of=affects_outcome_of, + ui_rules=ui_rules, + auto_hpo_value=auto_hpo_value, + auto_hpo_state=auto_hpo_state, + ) + check_float_selectable( + float_selectable_instance=actual_float_selectable, # type: ignore + expected_description=description, + expected_warning=warning, + expected_editable=editable, + expected_visible_in_ui=visible_in_ui, + expected_affects_outcome_of=affects_outcome_of, + expected_ui_rules=ui_rules, + expected_auto_hpo_value=auto_hpo_value, + expected_auto_hpo_state=auto_hpo_state, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_selectable(self): + """ + Description: + Check "selectable" function + + Input data: + "default_value" ConfigurableEnum element, "header" string, "description" string, "warning" string, "editable" + bool, "visible_in_ui" bool, "affects_outcome_of" ModelLifecycle element, "ui_rules" UIRules object + + Expected results: + Test passes if _CountingAttr object returned by "selectable" function is equal to expected + + Steps + 1. Check _CountingAttr object returned by "selectable" function for default values of optional parameters + 2. Check _CountingAttr object returned by "selectable" function for specified values of optional parameters + """ + + def check_selectable( + selectable_instance, + expected_description: str = "Default selectable description", + expected_warning: str = None, + expected_editable: bool = True, + expected_visible_in_ui: bool = True, + expected_affects_outcome_of: ModelLifecycle = ModelLifecycle.NONE, + expected_ui_rules: UIRules = NullUIRules(), + expected_auto_hpo_state: AutoHPOState = AutoHPOState.NOT_POSSIBLE, + expected_auto_hpo_value: SomeEnumSelectable = None, + ): + expected_metadata = { + "default_value": SomeEnumSelectable.OPTION_C, + "description": expected_description, + "header": "selectable header", + "warning": expected_warning, + "editable": expected_editable, + "visible_in_ui": expected_visible_in_ui, + "affects_outcome_of": expected_affects_outcome_of, + "ui_rules": expected_ui_rules, + "type": ConfigElementType.SELECTABLE, + "enum_name": "SomeEnumSelectable", + "options": { + "TEST_NAME1": "test_name_1", + "TEST_2": "test_2_test", + "BOGUS_NAME": "bogus", + "OPTION_C": "option_c", + }, + "auto_hpo_state": expected_auto_hpo_state, + "auto_hpo_value": expected_auto_hpo_value, + } + assert isinstance(selectable_instance, _make._CountingAttr) + assert selectable_instance._default == SomeEnumSelectable.OPTION_C + assert selectable_instance.type == ConfigurableEnum + assert selectable_instance.metadata == expected_metadata + + # Checking _CountingAttr object returned by "selectable" for default values of optional parameters + default_value = SomeEnumSelectable.OPTION_C + header = "selectable header" + actual_selectable = selectable(default_value=default_value, header=header) + check_selectable(selectable_instance=actual_selectable) # type: ignore + + # Checking _CountingAttr object returned by "selectable" for specified values of optional parameters + description = "selectable description" + warning = "selectable warning" + editable = False + visible_in_ui = False + affects_outcome_of = ModelLifecycle.TESTING + ui_rules = self.ui_rules + auto_hpo_state = AutoHPOState.POSSIBLE + auto_hpo_value = SomeEnumSelectable.BOGUS_NAME + + actual_selectable = selectable( + default_value=default_value, + header=header, + description=description, + warning=warning, + editable=editable, + visible_in_ui=visible_in_ui, + affects_outcome_of=affects_outcome_of, + ui_rules=ui_rules, + auto_hpo_value=auto_hpo_value, + auto_hpo_state=auto_hpo_state, + ) + check_selectable( + selectable_instance=actual_selectable, # type: ignore + expected_description=description, + expected_warning=warning, + expected_editable=editable, + expected_visible_in_ui=visible_in_ui, + expected_affects_outcome_of=affects_outcome_of, + expected_ui_rules=ui_rules, + expected_auto_hpo_state=auto_hpo_state, + expected_auto_hpo_value=auto_hpo_value, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_string_attribute(self): + """ + Description: + Check "string_attribute" function + + Input data: + "value" string + + Expected results: + Test passes if _CountingAttr object returned by "string_attribute" function is equal to expected + """ + value = "some string" + actual_string = string_attribute(value=value) + assert isinstance(actual_string, _make._CountingAttr) + assert actual_string._default == value # type: ignore + assert actual_string.kw_only # type: ignore + assert actual_string.type == str # type: ignore + assert not actual_string._validator # type: ignore + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_boolean_attribute(self): + """ + Description: + Check "boolean_attribute" function + + Input data: + "value" bool + + Expected results: + Test passes if _CountingAttr object returned by "boolean_attribute" function is equal to expected + """ + value = True + actual_boolean = boolean_attribute(value=value) + assert isinstance(actual_boolean, _make._CountingAttr) + assert actual_boolean._default == value # type: ignore + assert actual_boolean.kw_only # type: ignore + assert actual_boolean.type == bool # type: ignore + assert not actual_boolean._validator # type: ignore diff --git a/tests/unit/api/configuration/enums/test_config_element_type.py b/tests/unit/api/configuration/enums/test_config_element_type.py new file mode 100644 index 00000000000..662638f60bd --- /dev/null +++ b/tests/unit/api/configuration/enums/test_config_element_type.py @@ -0,0 +1,100 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.api.configuration.enums.config_element_type import ( + ConfigElementType, + ElementCategory, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestElementCategory: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_element_category(self): + """ + Description: + Check "ElementCategory" Enum class elements + + Expected results: + Test passes if "ElementCategory" Enum class length is equal to expected value and its elements have expected + sequence numbers, "name" attributes and values returned by __str__ method + """ + assert len(ElementCategory) == 3 + assert ElementCategory.PRIMITIVES.value == 1 + assert ElementCategory.PRIMITIVES.name == "PRIMITIVES" + assert str(ElementCategory.PRIMITIVES) == "PRIMITIVES" + assert ElementCategory.GROUPS.value == 2 + assert ElementCategory.GROUPS.name == "GROUPS" + assert str(ElementCategory.GROUPS) == "GROUPS" + assert ElementCategory.RULES.value == 3 + assert ElementCategory.RULES.name == "RULES" + assert str(ElementCategory.RULES) == "RULES" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestConfigElementType: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_element_category(self): + """ + Description: + Check "ConfigElementType" Enum class elements + + Expected results: + Test passes if "ConfigElementType" Enum class length is equal to expected value and its elements have expected + sequence numbers, "names" attributes, "category" properties and values returned by and __str__ method + """ + assert len(ConfigElementType) == 9 + # Checking "INTEGER" element + assert ConfigElementType.INTEGER.category == ElementCategory.PRIMITIVES + assert ConfigElementType.INTEGER.name == "INTEGER" + assert ConfigElementType.INTEGER.value == 0 + assert str(ConfigElementType.INTEGER) == "INTEGER" + # Checking "FLOAT" element + assert ConfigElementType.FLOAT.category == ElementCategory.PRIMITIVES + assert ConfigElementType.FLOAT.name == "FLOAT" + assert ConfigElementType.FLOAT.value == 1 + assert str(ConfigElementType.FLOAT) == "FLOAT" + # Checking "BOOLEAN" element + assert ConfigElementType.BOOLEAN.category == ElementCategory.PRIMITIVES + assert ConfigElementType.BOOLEAN.name == "BOOLEAN" + assert ConfigElementType.BOOLEAN.value == 2 + assert str(ConfigElementType.BOOLEAN) == "BOOLEAN" + # Checking "FLOAT_SELECTABLE" element + assert ConfigElementType.FLOAT_SELECTABLE.category == ElementCategory.PRIMITIVES + assert ConfigElementType.FLOAT_SELECTABLE.name == "FLOAT_SELECTABLE" + assert ConfigElementType.FLOAT_SELECTABLE.value == 3 + assert str(ConfigElementType.FLOAT_SELECTABLE) == "FLOAT_SELECTABLE" + # Checking "SELECTABLE" element + assert ConfigElementType.SELECTABLE.category == ElementCategory.PRIMITIVES + assert ConfigElementType.SELECTABLE.name == "SELECTABLE" + assert ConfigElementType.SELECTABLE.value == 4 + assert str(ConfigElementType.SELECTABLE) == "SELECTABLE" + # Checking "PARAMETER_GROUP" element + assert ConfigElementType.PARAMETER_GROUP.category == ElementCategory.GROUPS + assert ConfigElementType.PARAMETER_GROUP.name == "PARAMETER_GROUP" + assert ConfigElementType.PARAMETER_GROUP.value == 5 + assert str(ConfigElementType.PARAMETER_GROUP) == "PARAMETER_GROUP" + # Checking "CONFIGURABLE_PARAMETERS" element + assert ConfigElementType.CONFIGURABLE_PARAMETERS.category == ElementCategory.GROUPS + assert ConfigElementType.CONFIGURABLE_PARAMETERS.name == "CONFIGURABLE_PARAMETERS" + assert ConfigElementType.CONFIGURABLE_PARAMETERS.value == 6 + assert str(ConfigElementType.CONFIGURABLE_PARAMETERS) == "CONFIGURABLE_PARAMETERS" + # Checking "RULE" element + assert ConfigElementType.RULE.category == ElementCategory.RULES + assert ConfigElementType.RULE.name == "RULE" + assert ConfigElementType.RULE.value == 7 + assert str(ConfigElementType.RULE) == "RULE" + # Checking "RULE" element + assert ConfigElementType.UI_RULES.category == ElementCategory.RULES + assert ConfigElementType.UI_RULES.name == "UI_RULES" + assert ConfigElementType.UI_RULES.value == 8 + assert str(ConfigElementType.UI_RULES) == "UI_RULES" diff --git a/tests/unit/api/configuration/enums/test_enum_utils.py b/tests/unit/api/configuration/enums/test_enum_utils.py new file mode 100644 index 00000000000..aaadca5056a --- /dev/null +++ b/tests/unit/api/configuration/enums/test_enum_utils.py @@ -0,0 +1,39 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.api.configuration.enums.config_element_type import ConfigElementType +from otx.api.configuration.enums.utils import get_enum_names +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestMetadataKeys: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_get_enum_names(self): + """ + Description: + Check "get_enum_names" function + + Input data: + Enum object + + Expected results: + Test passes if list returned by "get_enum_names" function is equal to expected + """ + assert get_enum_names(ConfigElementType) == [ + "INTEGER", + "FLOAT", + "BOOLEAN", + "FLOAT_SELECTABLE", + "SELECTABLE", + "PARAMETER_GROUP", + "CONFIGURABLE_PARAMETERS", + "RULE", + "UI_RULES", + ] diff --git a/tests/unit/api/configuration/enums/test_model_lifecycle.py b/tests/unit/api/configuration/enums/test_model_lifecycle.py new file mode 100644 index 00000000000..5e0e28af812 --- /dev/null +++ b/tests/unit/api/configuration/enums/test_model_lifecycle.py @@ -0,0 +1,36 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.api.configuration.enums.model_lifecycle import ModelLifecycle +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelLifecycle: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_lifecycle(self): + """ + Description: + Check ModelLifecycle Enum class elements + + Expected results: + Test passes if ModelLifecycle Enum class length is equal to expected value and its elements have expected + sequence numbers and values returned by __str__ method + """ + assert len(ModelLifecycle) == 5 + assert ModelLifecycle.NONE.value == 1 + assert str(ModelLifecycle.NONE) == "NONE" + assert ModelLifecycle.ARCHITECTURE.value == 2 + assert str(ModelLifecycle.ARCHITECTURE) == "ARCHITECTURE" + assert ModelLifecycle.TRAINING.value == 3 + assert str(ModelLifecycle.TRAINING) == "TRAINING" + assert ModelLifecycle.INFERENCE.value == 4 + assert str(ModelLifecycle.INFERENCE) == "INFERENCE" + assert ModelLifecycle.TESTING.value == 5 + assert str(ModelLifecycle.TESTING) == "TESTING" diff --git a/tests/unit/api/configuration/helper/test_config_element_mapping.py b/tests/unit/api/configuration/helper/test_config_element_mapping.py new file mode 100644 index 00000000000..87fa3168231 --- /dev/null +++ b/tests/unit/api/configuration/helper/test_config_element_mapping.py @@ -0,0 +1,140 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from functools import partial + +import pytest + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.elements import ( + ParameterGroup, + configurable_boolean, + configurable_float, + configurable_integer, + float_selectable, + selectable, +) +from otx.api.configuration.helper.config_element_mapping import ( + GroupElementMapping, + PrimitiveElementMapping, + RuleElementMapping, +) +from otx.api.configuration.ui_rules.rules import Rule, UIRules +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestPrimitiveElementMapping: + @staticmethod + def check_primitive_element( + primitive_element: PrimitiveElementMapping, + expected_name: str, + expected_partial_element: partial, + ): + assert primitive_element.name == expected_name + assert str(primitive_element) == expected_name + assert isinstance(primitive_element.value, partial) + assert primitive_element.value.args == expected_partial_element.args + assert primitive_element.value.keywords == expected_partial_element.keywords + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_primitive_element_mapping(self): + """ + Description: + Check "PrimitiveElementMapping" Enum class elements + + Expected results: + Test passes if "PrimitiveElementMapping" Enum class length is equal to expected value and its elements have + expected "value" and "name" attributes and value returned by "__str__" method + """ + + assert len(PrimitiveElementMapping) == 5 + self.check_primitive_element( + primitive_element=PrimitiveElementMapping.INTEGER, + expected_name="INTEGER", + expected_partial_element=partial(configurable_integer), + ) + self.check_primitive_element( + primitive_element=PrimitiveElementMapping.FLOAT, + expected_name="FLOAT", + expected_partial_element=partial(configurable_float), + ) + self.check_primitive_element( + primitive_element=PrimitiveElementMapping.BOOLEAN, + expected_name="BOOLEAN", + expected_partial_element=partial(configurable_boolean), + ) + self.check_primitive_element( + primitive_element=PrimitiveElementMapping.FLOAT_SELECTABLE, + expected_name="FLOAT_SELECTABLE", + expected_partial_element=partial(float_selectable), + ) + self.check_primitive_element( + primitive_element=PrimitiveElementMapping.SELECTABLE, + expected_name="SELECTABLE", + expected_partial_element=partial(selectable), + ) + + +def check_mapping_element(mapping_element, expected_name, expected_value): + assert mapping_element.name == expected_name + assert mapping_element.value == expected_value + assert str(mapping_element) == expected_name + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestGroupElementMapping: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_group_element_mapping(self): + """ + Description: + Check "GroupElementMapping" Enum class elements + + Expected results: + Test passes if "GroupElementMapping" Enum class length is equal to expected value and its elements have + expected "name" attribute and value returned by "__str__" method + """ + assert len(GroupElementMapping) == 2 + check_mapping_element( + mapping_element=GroupElementMapping.PARAMETER_GROUP, + expected_name="PARAMETER_GROUP", + expected_value=ParameterGroup, + ) + check_mapping_element( + mapping_element=GroupElementMapping.CONFIGURABLE_PARAMETERS, + expected_name="CONFIGURABLE_PARAMETERS", + expected_value=ConfigurableParameters, + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestRuleElementMapping: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_group_element_mapping(self): + """ + Description: + Check "RuleElementMapping" Enum class elements + + Expected results: + Test passes if "RuleElementMapping" Enum class length is equal to expected value and its elements have + expected "name" attribute and value returned by "__str__" method + """ + assert len(RuleElementMapping) == 2 + check_mapping_element( + mapping_element=RuleElementMapping.RULE, + expected_name="RULE", + expected_value=Rule, + ) + check_mapping_element( + mapping_element=RuleElementMapping.UI_RULES, + expected_name="UI_RULES", + expected_value=UIRules, + ) diff --git a/tests/unit/api/configuration/helper/test_create.py b/tests/unit/api/configuration/helper/test_create.py new file mode 100644 index 00000000000..4e3fa743b2f --- /dev/null +++ b/tests/unit/api/configuration/helper/test_create.py @@ -0,0 +1,640 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from attr import _make +from omegaconf import DictConfig + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.elements import ConfigurableEnum +from otx.api.configuration.elements.parameter_group import ParameterGroup +from otx.api.configuration.enums.config_element_type import ConfigElementType +from otx.api.configuration.enums.model_lifecycle import ModelLifecycle +from otx.api.configuration.helper.config_element_mapping import GroupElementMapping +from otx.api.configuration.helper.create import ( + construct_attrib_from_dict, + construct_ui_rules_from_dict, + contains_parameter_groups, + create, + create_default_configurable_enum_from_dict, + create_nested_parameter_group, + from_dict_attr, + gather_parameter_arguments_and_values_from_dict, +) +from otx.api.configuration.ui_rules.rules import NullUIRules, Rule, UIRules +from otx.api.configuration.ui_rules.types import Action, Operator +from tests.unit.api.configuration.dummy_config import SomeEnumSelectable +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestCreateFunctions: + @staticmethod + def int_rule_dict(): + return { + "parameter": "int parameter", + "value": 100, + "operator": Operator.LESS_THAN, + "type": "RULE", + } + + def int_ui_rules_params(self): + return {"rules": [self.int_rule_dict()], "type": "UI_RULES"} + + def non_selectable_dict(self): + return { + "default_value": 70, + "header": "non selectable parameter header", + "ui_rules": self.int_ui_rules_params(), + "type": "INTEGER", + } + + @staticmethod + def selectable_dict(): + return DictConfig( + content={ + "header": "selectable parameter header", + "enum_name": "test enum", + "options": ["test_1", "test_2"], + "default_value": 2, + "type": "SELECTABLE", + "value": 1, + "affects_outcome_of": ModelLifecycle.TESTING, + } + ) + + @staticmethod + def check_ui_rules( + ui_rules, + expected_rules, + expected_action=Action.DISABLE_EDITING, + expected_operator=Operator.AND, + ): + assert isinstance(ui_rules, UIRules) + assert ui_rules.rules == expected_rules + assert ui_rules.action == expected_action + assert ui_rules.operator == expected_operator + assert ui_rules.type == ConfigElementType.UI_RULES + + def nested_config_dict_section(self): + non_nested_parameters = { + "header": "non-nested parameter header", + "non_selectable": self.non_selectable_dict(), + "type": GroupElementMapping.CONFIGURABLE_PARAMETERS, + } + selectable_parameters = { + "header": "nested parameter header", + "selectable": self.selectable_dict(), + "type": GroupElementMapping.CONFIGURABLE_PARAMETERS, + } + + nested_parameters = { + "header": "nested parameters group header", + "selectable": selectable_parameters, + "type": GroupElementMapping.PARAMETER_GROUP, + } + + return { + "header": "test header", + "non_nested": non_nested_parameters, + "nested": nested_parameters, + "type": GroupElementMapping.CONFIGURABLE_PARAMETERS, + } + + @staticmethod + def check_parameter_group(parameter_group, expected_type): + # Checking parameter group attributes + assert isinstance(parameter_group, expected_type) + assert parameter_group.description == "Default parameter group description" + assert parameter_group.groups == ["nested", "non_nested"] + assert parameter_group.header == "test header" + assert parameter_group.parameters == [] + assert parameter_group.type == ConfigElementType.CONFIGURABLE_PARAMETERS + # Checking non-nested configurable parameter + non_nested = parameter_group.non_nested + assert isinstance(non_nested, ConfigurableParameters) + assert non_nested.description == "Default parameter group description" + assert non_nested.groups == [] + assert non_nested.header == "non-nested parameter header" + assert non_nested.non_selectable == 70 # type: ignore + assert non_nested.parameters == ["non_selectable"] + assert non_nested.type == ConfigElementType.CONFIGURABLE_PARAMETERS + # Checking nested parameter group + nested = parameter_group.nested + assert isinstance(nested, ParameterGroup) + assert nested.groups == ["selectable"] + assert nested.header == "nested parameters group header" + assert nested.parameters == [] + assert nested.type == ConfigElementType.PARAMETER_GROUP + # Checking nested parameter + parameter = nested.selectable # type: ignore + assert isinstance(parameter, ConfigurableParameters) + assert parameter.description == "Default parameter group description" + assert parameter.groups == [] + assert parameter.header == "nested parameter header" + assert parameter.selectable == 1 # type: ignore + assert parameter.parameters == ["selectable"] + assert parameter.type == ConfigElementType.CONFIGURABLE_PARAMETERS + + @staticmethod + def check_non_nested_configurable_parameters(non_nested): + assert isinstance(non_nested, ConfigurableParameters) + assert non_nested.description == "Default parameter group description" + assert non_nested.groups == [] + assert non_nested.header == "non-nested parameter header" + assert non_nested.non_selectable == 70 # type: ignore + assert non_nested.parameters == ["non_selectable"] + assert non_nested.type == ConfigElementType.CONFIGURABLE_PARAMETERS + + @staticmethod + def check_nested_parameter_group(nested): + assert isinstance(nested, ParameterGroup) + assert nested.groups == ["selectable"] + assert nested.header == "nested parameters group header" + assert nested.parameters == [] + assert nested.type == ConfigElementType.PARAMETER_GROUP + # Checking nested parameter + parameter = nested.selectable # type: ignore + assert isinstance(parameter, ConfigurableParameters) + assert parameter.description == "Default parameter group description" + assert parameter.groups == [] + assert parameter.header == "nested parameter header" + assert parameter.selectable == 1 # type: ignore + assert parameter.parameters == ["selectable"] + assert parameter.type == ConfigElementType.CONFIGURABLE_PARAMETERS + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_construct_attrib_from_dict(self): + """ + Description: + Check "construct_attrib_from_dict" function + + Input data: + "dict_object" dictionary or DictConfig parameter + + Expected results: + Test passes if object returned by "construct_attrib_from_dict" function is equal to expected + + Steps + 1. Check Rule object returned by "construct_attrib_from_dict" function + 2. Check UIRules object returned by "construct_attrib_from_dict" function + 3. Check that ValueError exception is raised when unexpected "type" is specified in "dict_object" parameter + """ + + def check_rule( + rule, + expected_parameter, + expected_value, + expected_operator=Operator.EQUAL_TO, + ): + assert isinstance(rule, Rule) + assert rule.parameter == expected_parameter + assert rule.value == expected_value + assert rule.type == ConfigElementType.RULE + assert rule.operator == expected_operator + + # Checking Rule object returned by "construct_attrib_from_dict" + # Constructing Rule with default optional parameters + params = { + "parameter": "string parameter", + "value": "some string", + "type": "RULE", + } + for dict_object in [params, DictConfig(content=params)]: + check_rule( + rule=construct_attrib_from_dict(dict_object=dict_object), + expected_parameter="string parameter", + expected_value="some string", + ) + # Constructing Rule with specified optional parameters + params = { + "parameter": "int parameter", + "value": 5, + "operator": Operator.LESS_THAN, + "type": "RULE", + } + for dict_object in [params, DictConfig(content=params)]: + check_rule( + rule=construct_attrib_from_dict(dict_object=dict_object), + expected_parameter="int parameter", + expected_value=5, + expected_operator=Operator.LESS_THAN, + ) + # Checking UIRules object returned by "construct_attrib_from_dict" + rules = [ + {"parameter": "string parameter", "value": "some string"}, + {"parameter": "int parameter", "value": 2}, + ] + # Constructing UIRules with default optional parameters + params = {"rules": rules, "type": "UI_RULES"} + for dict_object in [params, DictConfig(content=params)]: + self.check_ui_rules( + ui_rules=construct_attrib_from_dict(dict_object=dict_object), + expected_rules=rules, + ) + # Constructing UIRules with specified optional parameters + params = { + "rules": rules, + "action": Action.ENABLE_EDITING, + "operator": Operator.EQUAL_TO, + "type": "UI_RULES", + } + for dict_object in [params, DictConfig(content=params)]: + self.check_ui_rules( + ui_rules=construct_attrib_from_dict(dict_object=dict_object), + expected_rules=rules, + expected_action=Action.ENABLE_EDITING, + expected_operator=Operator.EQUAL_TO, + ) + # Checking that ValueError exception is raised when unexpected "type" is specified in "dict_object" + params = { + "parameter": "string parameter", + "value": "some string", + "type": "unexpected type", + } + with pytest.raises(ValueError): + construct_attrib_from_dict(dict_object=params) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_construct_ui_rules_from_dict(self): + """ + Description: + Check "construct_ui_rules_from_dict" function + + Input data: + "ui_exposure_settings" dictionary or DictConfig parameter + + Expected results: + Test passes if UIRules object returned by "construct_ui_rules_from_dict" function is equal to expected + + Steps + 1. Check UIRules object returned by "construct_ui_rules_from_dict" function when several "RULE" objects are + specified in "rules" key of "ui_exposure_settings" parameter + 2. Check UIRules object returned by "construct_ui_rules_from_dict" function when several "RULE" objects and + nested "UI_RULE" are specified in "rules" key of "ui_exposure_settings" parameter + 3. Check that NullUIRules object is returned by "construct_ui_rules_from_dict" function when None is specified + as "ui_exposure_settings" parameter + 4. Check that NullUIRules object is returned by "construct_ui_rules_from_dict" function when empty list is + specified in "rules" key of "ui_exposure_settings" parameter + 5. Check that ValueError exception is raised when one of objects specified in "rules" key of + "ui_exposure_settings" parameter has unexpected "TYPE" + """ + # Checking UIRules returned by "construct_ui_rules_from_dict" when several "RULE" are specified in "rules" of + # "ui_exposure_settings" + rule = self.int_rule_dict() + other_rule = {"parameter": "bool parameter", "value": False, "type": "RULE"} + params = {"rules": [rule, other_rule], "type": "UI_RULES"} + expected_rules = [ + Rule(parameter="int parameter", value=100, operator=Operator.LESS_THAN), + Rule(parameter="bool parameter", value=False), + ] + for ui_exposure_settings in [params, DictConfig(content=dict(params))]: + self.check_ui_rules( + ui_rules=construct_ui_rules_from_dict(ui_exposure_settings=ui_exposure_settings), + expected_rules=expected_rules, + ) + # Checking UIRules returned by "construct_ui_rules_from_dict" when several "RULE" and "UI_RULE" are specified in + # "rules" of "ui_exposure_settings" + nested_rule = { + "parameter": "nested float parameter", + "value": 10.6, + "operator": Operator.GREATER_THAN, + "type": "RULE", + } + + other_nested_rule = { + "parameter": "nested bool parameter", + "value": True, + "type": "RULE", + } + nested_ui_rules = { + "rules": [nested_rule, other_nested_rule], + "action": Action.ENABLE_EDITING, + "operator": Operator.EQUAL_TO, + "type": "UI_RULES", + } + params = {"rules": [rule, other_rule, nested_ui_rules], "type": "UI_RULES"} + expected_nested_rules = [ + Rule( + parameter="nested float parameter", + value=10.6, + operator=Operator.GREATER_THAN, + ), + Rule(parameter="nested bool parameter", value=True), + ] + expected_rules = [ + Rule(parameter="int parameter", value=100, operator=Operator.LESS_THAN), + Rule(parameter="bool parameter", value=False), + UIRules( + rules=expected_nested_rules, + action=Action.ENABLE_EDITING, + operator=Operator.EQUAL_TO, + ), + ] + for ui_exposure_settings in [params, DictConfig(content=dict(params))]: + self.check_ui_rules( + ui_rules=construct_ui_rules_from_dict(ui_exposure_settings=ui_exposure_settings), + expected_rules=expected_rules, + ) + # Checking that NullUIRules returned by "construct_ui_rules_from_dict" when None is specified as + # "ui_exposure_settings" + assert construct_ui_rules_from_dict(ui_exposure_settings=None) == NullUIRules() # type: ignore + # Checking that NullUIRules returned by "construct_ui_rules_from_dict" when empty list is specified in "rules" + # key of "ui_exposure_settings" + params = {"rules": [], "type": "UI_RULES"} + assert construct_ui_rules_from_dict(ui_exposure_settings=params) == NullUIRules() + # Checking that ValueError exception is raised when one of objects specified in "rules" key of + # "ui_exposure_settings" has unexpected "TYPE" + invalid_rule = { + "parameter": "invalid rule", + "value": False, + "type": "unexpected type", + } + params = {"rules": [rule, invalid_rule], "type": "UI_RULES"} + with pytest.raises(ValueError): + construct_ui_rules_from_dict(params) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_create_default_configurable_enum_from_dict(self): + """ + Description: + Check "create_default_configurable_enum_from_dict" function + + Input data: + "parameter_dict" dictionary or DictConfig parameter + + Expected results: + Test passes if dictionary returned by "create_default_configurable_enum_from_dict" function is equal to expected + + Steps + 1. Check dictionary returned by "create_default_configurable_enum_from_dict" function when ConfigurableEnum + element is specified as "default_value" key value of "parameter_dict" parameter + 2. Check dictionary returned by "create_default_configurable_enum_from_dict" function when int object is + specified as "default_value" key value of "parameter_dict" parameter + 3. Check that TypeError exception is raised when DictConfig object with "content" attribute equal to None is + specified as "default_value" key value of "parameter_dict" parameter + """ + # Checking dictionary returned by "create_default_configurable_enum_from_dict" when ConfigurableEnum element is + # specified as "default_value" key value of "parameter_dict" + params = { + "enum_name": "test enum", + "options": ["test_1", "test_2"], + "default_value": SomeEnumSelectable.TEST_NAME1, + } + for parameter_dict in [params, DictConfig(content=dict(params))]: + configurable_enum = create_default_configurable_enum_from_dict(parameter_dict=parameter_dict) + assert isinstance(configurable_enum, dict) + assert len(configurable_enum) == 1 + default_value = configurable_enum.get("default_value") + assert default_value.name == "TEST_NAME1" + assert default_value.value == "test_name_1" + assert type(default_value) == SomeEnumSelectable + # Checking dictionary returned by "create_default_configurable_enum_from_dict" when int object is specified as + # "default_value" key value of "parameter_dict" + params = { + "enum_name": "test enum", + "options": ["test_1", "test_2"], + "default_value": 2, + } + for parameter_dict in [params, DictConfig(content=dict(params))]: + configurable_enum = create_default_configurable_enum_from_dict(parameter_dict=parameter_dict) + assert isinstance(configurable_enum, dict) + assert len(configurable_enum) == 1 + default_value = configurable_enum.get("default_value") + assert default_value.name == "test_2" + assert default_value.value == 2 + default_value_type = type(default_value) + assert default_value_type.__name__ == "test enum" + assert issubclass(default_value_type, ConfigurableEnum) + assert len(default_value_type) == 2 + assert default_value_type.test_1.name == "test_1" + assert default_value_type.test_1.value == 1 + assert default_value_type.test_2.name == "test_2" + assert default_value_type.test_2.value == 2 + # Checking that TypeError exception is raised when DictConfig with "content" equal to None is specified as + # "default_value" key value of "parameter_dict" + with pytest.raises(TypeError): + create_default_configurable_enum_from_dict(parameter_dict=DictConfig(content=None)) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_gather_parameter_arguments_and_values_from_dict(self): + """ + Description: + Check "gather_parameter_arguments_and_values_from_dict" function + + Input data: + "config_dict_section" dictionary or DictConfig parameter + + Expected results: + Test passes if dictionary returned by "gather_parameter_arguments_and_values_from_dict" function is equal to + expected + + Steps + 1. Check dictionary returned by "gather_parameter_arguments_and_values_from_dict" function when INTEGER and + SELECTABLE parameter dictionaries and non-dictionary value are specified in "config_dict_section" parameter + 2. Check that ValueError exception is raised when one of parameters contains "type" key equal to None + """ + # Checking dictionary returned by "gather_parameter_arguments_and_values_from_dict" when INTEGER and + # SELECTABLE argument dictionaries and non-dictionary value are specified in "config_dict_section" + non_selectable_dict = self.non_selectable_dict() + selectable_dict = self.selectable_dict() + params = { + "non_selectable": non_selectable_dict, + "selectable": selectable_dict, + "non_dict_key": 6, + "type": "non required type", + } + for config_dict_section in [params, DictConfig(content=dict(params))]: + arguments_and_values_dict = gather_parameter_arguments_and_values_from_dict( + config_dict_section=config_dict_section + ) + assert len(arguments_and_values_dict) == 3 + # Checking "make_arguments" key + assert len(arguments_and_values_dict.get("make_arguments")) == 2 + # Checking non-Selectable argument + argument = arguments_and_values_dict.get("make_arguments").get("non_selectable") + assert isinstance(argument, _make._CountingAttr) + assert argument.type == int + metadata = argument.metadata + assert metadata.get("default_value") == 70 + assert metadata.get("description") == "Default integer description" + assert metadata.get("header") == "non selectable parameter header" + assert metadata.get("affects_outcome_of") == ModelLifecycle.NONE + assert metadata.get("type") == ConfigElementType.INTEGER + assert metadata.get("ui_rules") == construct_ui_rules_from_dict(dict(self.int_ui_rules_params())) + # Checking Selectable argument + argument = arguments_and_values_dict.get("make_arguments").get("selectable") + assert isinstance(argument, _make._CountingAttr) + assert argument.type == ConfigurableEnum + metadata = argument.metadata + assert type(metadata.get("default_value")).__name__ == "test enum" + assert metadata.get("default_value").name == "test_2" + assert metadata.get("default_value").value == 2 + assert metadata.get("description") == "Default selectable description" + assert metadata.get("header") == "selectable parameter header" + assert metadata.get("affects_outcome_of") == ModelLifecycle.TESTING + assert metadata.get("type") == ConfigElementType.SELECTABLE + assert metadata.get("ui_rules") == NullUIRules() + # Checking "call_arguments" key + assert arguments_and_values_dict.get("call_arguments") == {"non_dict_key": 6} + # Checking "values" key + assert len(arguments_and_values_dict.get("values")) == 2 + assert arguments_and_values_dict.get("values").get("selectable") == 1 + assert not arguments_and_values_dict.get("values")["non_selectable"] + # Checking that ValueError exception is raised when one of parameters contains "type" key equal to None + non_selectable_dict.pop("type") + config_dict_section = { + "no_type_parameter": non_selectable_dict, + "selectable": selectable_dict, + } + with pytest.raises(ValueError): + gather_parameter_arguments_and_values_from_dict(config_dict_section=config_dict_section) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_contains_parameter_groups(self): + """ + Description: + Check "contains_parameter_groups" function + + Input data: + "config_dict" dictionary or DictConfig parameter + + Expected results: + Test passes if list returned by "contains_parameter_groups" function is equal to expected + """ + configurable_parameters = { + "header": "int group header", + "int_value": 10, + "type": GroupElementMapping.CONFIGURABLE_PARAMETERS, + } + parameter_group = { + "header": "bool group header", + "bool_value": False, + "type": GroupElementMapping.PARAMETER_GROUP, + } + + unexpected_type_group = { + "header": "unexpected type header", + "value": "some value", + "type": "unexpected type", + } + + parameters = { + "configurable_parameters_test_group": configurable_parameters, + "parameter_test_group": parameter_group, + "unexpected_type_test_group": unexpected_type_group, + } + + for config_dict in [parameters, DictConfig(content=dict(parameters))]: + assert contains_parameter_groups(config_dict=config_dict) == [ + "configurable_parameters_test_group", + "parameter_test_group", + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_create_nested_parameter_group(self): + """ + Description: + Check "create_nested_parameter_group" function + + Input data: + "config_dict_section" dictionary or DictConfig parameter + + Expected results: + Test passes if ParameterGroup object returned by "create_nested_parameter_group" function is equal to expected + """ + parameters = self.nested_config_dict_section() + for config_dict_section in parameters, DictConfig(content=dict(parameters)): + nested_parameter_group = create_nested_parameter_group(config_dict_section=config_dict_section) + self.check_parameter_group( + parameter_group=nested_parameter_group, # type: ignore + expected_type=ParameterGroup, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_from_dict_attr(self): + """ + Description: + Check "from_dict_attr" function + + Input data: + "config_dict" dictionary or DictConfig parameter + + Expected results: + Test passes if ConfigurableParameters returned by "from_dict_attr" function is equal to expected + """ + parameters = self.nested_config_dict_section() + for config_dict in [parameters, DictConfig(content=dict(parameters))]: + parameter_group = from_dict_attr(config_dict=config_dict) + self.check_parameter_group( + parameter_group=parameter_group, # type: ignore + expected_type=ConfigurableParameters, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_create(self): + """ + Description: + Check "create" function + + Input data: + "input_config" dictionary or DictConfig parameter + + Expected results: + Test passes if ConfigurableParameters object returned by "from_dict_attr" function is equal to expected + + Steps + 1. Check ConfigurableParameters object returned by "from_dict_attr" function when dictionary is specified as + "input_config" parameter + 2. Check ConfigurableParameters object returned by "from_dict_attr" function when string is specified as + "input_config" parameter + """ + # Checking ConfigurableParameters returned by "from_dict_attr" when dictionary is specified as "input_config" + parameters = self.nested_config_dict_section() + for input_config in [parameters, DictConfig(content=dict(parameters))]: + parameter_group = create(input_config=input_config) + self.check_parameter_group( + parameter_group=parameter_group, # type: ignore + expected_type=ConfigurableParameters, + ) + + # Checking ConfigurableParameters returned by "from_dict_attr" when string is specified as "input_config" + str_parameter = { + "header": "str parameter header", + "enum_name": "test enum", + "options": ["test_1", "test_2"], + "default_value": 2, + "type": "SELECTABLE", + "value": 1, + "affects_outcome_of": "TESTING", + } + str_parameter_group = { + "header": "str parameter group header", + "str_parameter": str_parameter, + "type": "PARAMETER_GROUP", + } + parameter_group = create(str(str_parameter_group)) + assert parameter_group.description == "Default parameter group description" + assert parameter_group.groups == [] + assert parameter_group.header == "str parameter group header" + assert parameter_group.parameters == ["str_parameter"] + assert parameter_group.str_parameter == 1 # type: ignore + assert parameter_group.type == ConfigElementType.PARAMETER_GROUP diff --git a/tests/unit/api/configuration/helper/test_helper_utils.py b/tests/unit/api/configuration/helper/test_helper_utils.py new file mode 100644 index 00000000000..5bf949b0ee1 --- /dev/null +++ b/tests/unit/api/configuration/helper/test_helper_utils.py @@ -0,0 +1,354 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from enum import Enum +from pathlib import Path + +import pytest +import yaml +from omegaconf import DictConfig + +from otx.api.configuration.helper.utils import ( + _search_in_config_dict_inner, + deserialize_enum_value, + ids_to_strings, + input_to_config_dict, + search_in_config_dict, +) +from otx.api.entities.id import ID +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestUtilsFunctions: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_utils_search_in_config_dict_inner(self): + """ + Description: + Check "_search_in_config_dict_inner" function + + Input data: + "config_dict" dictionary, "key_to_search" string, "prior_keys" list, "results" list + + Expected results: + Test passes if list returned by "_search_in_config_dict_inner" function is equal to expected + + Steps + 1. Check list returned by "_search_in_config_dict_inner" function with default values of optional parameters + 2. Check list returned by "_search_in_config_dict_inner" function with specified "prior_keys" parameter + 3. Check list returned by "_search_in_config_dict_inner" function with specified "results" parameter + 4. Check list returned by "_search_in_config_dict_inner" function with specified both optional parameters + 5. Check list returned by "_search_in_config_dict_inner" function with list object specified as "config_dict" + parameter + """ + # Checking list returned by "_search_in_config_dict_inner" with default values of optional parameters + config_dict = {"key_1": 2, "key_2": 4, "key_3": 8} + # Checking search of existing key + assert _search_in_config_dict_inner(config_dict=config_dict, key_to_search="key_2") == [(4, [])] + # Checking search of non-existing key + assert _search_in_config_dict_inner(config_dict=config_dict, key_to_search="key_4") == [] + + # Checking list returned by "_search_in_config_dict_inner" with specified "prior_keys" + prior_keys = ["prior_key_1", "prior_key_2"] + # Checking search of existing key + assert _search_in_config_dict_inner(config_dict=config_dict, key_to_search="key_2", prior_keys=prior_keys) == [ + (4, prior_keys) + ] + # Checking search of non-existing key + assert _search_in_config_dict_inner(config_dict=config_dict, key_to_search="key_4", prior_keys=prior_keys) == [] + # Checking list returned by "_search_in_config_dict_inner" with specified "results" + # Checking search of existing key + results = [(4, ["result_key", "other_result_key"])] + expected_results = [(4, ["result_key", "other_result_key"]), (2, [])] + assert ( + _search_in_config_dict_inner(config_dict=config_dict, key_to_search="key_1", results=results) + == expected_results + ) + assert results == expected_results + # Checking search of non-existing key + results = [(4, ["result_key", "other_result_key"])] + expected_results = list(results) + assert ( + _search_in_config_dict_inner(config_dict=config_dict, key_to_search="key_4", results=results) + == expected_results + ) + assert results == expected_results + # Checking list returned by "_search_in_config_dict_inner" with specified "prior_keys" and "results" + # Checking search of existing key + results = [(4, ["result_key", "other_result_key"])] + expected_results = [ + (4, ["result_key", "other_result_key"]), + (8, ["prior_key_1", "prior_key_2"]), + ] + assert ( + _search_in_config_dict_inner( + config_dict=config_dict, + key_to_search="key_3", + prior_keys=prior_keys, + results=results, + ) + == expected_results + ) + assert results == expected_results + # Checking search of non-existing key + results = [(4, ["result_key", "other_result_key"])] + expected_results = list(results) + assert ( + _search_in_config_dict_inner( + config_dict=config_dict, + key_to_search="key_4", + prior_keys=prior_keys, + results=results, + ) + == expected_results + ) + assert results == expected_results + # Checking list returned by "_search_in_config_dict_inner" with list specified as "config_dict" + # Checking search of existing key + config_list = [1, 3, 9] + results = [(4, ["result_key", "other_result_key"])] + expected_results = [ + (4, ["result_key", "other_result_key"]), + (3, ["prior_key_1", "prior_key_2"]), + ] + assert ( + _search_in_config_dict_inner( + config_dict=config_list, # type: ignore + key_to_search=1, # type: ignore + prior_keys=prior_keys, + results=results, + ) + == expected_results + ) + assert results == expected_results + # Checking search of non-existing key + results = [(4, ["result_key", "other_result_key"])] + expected_results = list(results) + assert ( + _search_in_config_dict_inner( + config_dict=config_list, # type: ignore + key_to_search=5, # type: ignore + prior_keys=prior_keys, + results=results, + ) + == expected_results + ) + assert results == expected_results + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_utils_search_in_config_dict(self): + """ + Description: + Check "search_in_config_dict" function + + Input data: + "config_dict" dictionary, "key_to_search" string + + Expected results: + Test passes if list returned by "search_in_config_dict" function is equal to expected + + Steps + 1. Check list returned by "search_in_config_dict" function when searching key in "config_dict" dictionary + 2. Check list returned by "search_in_config_dict" function when searching key in "config_dict" list + """ + # Checking list returned by "search_in_config_dict" when searching key in dictionary "config_dict" + config_dict = {"key_1": 2, "key_2": 4, "key_3": 8} + assert search_in_config_dict(config_dict, "key_1") == [(2, [])] + # Checking search of non-existing key + assert search_in_config_dict(config_dict, "key_4") == [] + # Checking list returned by "search_in_config_dict" when searching key in list "config_dict" + config_dict = [1, 3, 9] + assert search_in_config_dict(config_dict, 1) == [(3, [])] # type: ignore + # Checking search of non-existing key + assert search_in_config_dict(config_dict, "key_1") == [] # type: ignore + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_utils_input_to_config_dict(self): + """ + Description: + Check "input_to_config_dict" function + + Input data: + "input_config" string, DictConfig or dictionary object, "check_config_type" bool value + + Expected results: + Test passes if dictionary returned by "input_to_config_dict" function is equal to expected + + Steps + 1. Check dictionary returned by "input_to_config_dict" function when "check_config_type" parameter is "True" + 2. Check dictionary returned by "input_to_config_dict" function when "check_config_type" parameter is "False" + and "type" key is not specified in "input_config" parameter + 3. Check that ValueError exception is raised when type of "input_config" parameter is not equal to string, + DictConfig or dictionary + 4. Check that ValueError exception is raised by "input_to_config_dict" function when "check_config_type" + parameter is "True" and "type" key is not specified in "input_config" parameter + 5. Check that ValueError exception is raised by "input_to_config_dict" function when "check_config_type" + parameter is "True" and "type" key in "input_config" parameter is equal to unexpected value + """ + # Checking dictionary returned by "input_to_config_dict" when "check_config_type" is "True" + path_to_config = str(Path(__file__).parent / Path(r"../dummy_config.yaml")) + with open(path_to_config, "r", encoding="UTF-8") as file: + expected_path_to_config_dict = yaml.safe_load(file) + string_config = "{'str_key_1': 2, 'str_key_2': 4, 'str_key_3': 8, 'type': PARAMETER_GROUP}" + expected_string_config_dict = { + "str_key_1": 2, + "str_key_2": 4, + "str_key_3": 8, + "type": "PARAMETER_GROUP", + } + dict_config = { + "dict_key_1": 2, + "dict_key_2": 4, + "dict_key_3": 8, + "type": "PARAMETER_GROUP", + } + expected_dict_config = { + "dict_key_1": 2, + "dict_key_2": 4, + "dict_key_3": 8, + "type": "PARAMETER_GROUP", + } + dict_config_instance = DictConfig( + content={ + "DictConfig_key_1": 2, + "DictConfig_key_2": 4, + "DictConfig_key_3": 8, + "type": "PARAMETER_GROUP", + } + ) + expected_dict_config_instance_dict = { + "DictConfig_key_1": 2, + "DictConfig_key_2": 4, + "DictConfig_key_3": 8, + "type": "PARAMETER_GROUP", + } + + for input_config, expected_dict in [ + (path_to_config, expected_path_to_config_dict), + (string_config, expected_string_config_dict), + (dict_config, expected_dict_config), + (dict_config_instance, expected_dict_config_instance_dict), + ]: + assert input_to_config_dict(input_config=input_config, check_config_type=True) == expected_dict + # Checking dictionary returned by "input_to_config_dict" when "check_config_type" is "False" and "type" key + # is not specified in "input_config" + string_config = "{'str_key_1': 2, 'str_key_2': 4, 'str_key_3': 8}" + expected_string_config_dict = {"str_key_1": 2, "str_key_2": 4, "str_key_3": 8} + dict_config = {"dict_key_1": 2, "dict_key_2": 4, "dict_key_3": 8} + expected_dict_config = {"dict_key_1": 2, "dict_key_2": 4, "dict_key_3": 8} + dict_config_instance = DictConfig( + content={ + "DictConfig_key_1": 2, + "DictConfig_key_2": 4, + "DictConfig_key_3": 8, + } + ) + expected_dict_config_instance_dict = { + "DictConfig_key_1": 2, + "DictConfig_key_2": 4, + "DictConfig_key_3": 8, + } + + for input_config, expected_dict in [ + (path_to_config, expected_path_to_config_dict), + (string_config, expected_string_config_dict), + (dict_config, expected_dict_config), + (dict_config_instance, expected_dict_config_instance_dict), + ]: + assert input_to_config_dict(input_config=input_config, check_config_type=False) == expected_dict + # Checking that ValueError exception is raised when type of "input_config" is not equal to string, DictConfig or + # dictionary + with pytest.raises(ValueError): + input_to_config_dict(input_config=1) # type: ignore + # Checking that ValueError exception is raised by "input_to_config_dict" when "check_config_type" is "True" and + # "type" key is not specified in "input_config" + for none_type_input_config in [ + "{'key_1': 2, 'key_2': 4, 'key_3': 8}", + {"key_1": 2, "key_2": 4, "key_3": 8}, + DictConfig(content={"key_1": 2, "key_2": 4, "key_3": 8}), + ]: + with pytest.raises(ValueError): + input_to_config_dict(input_config=none_type_input_config) + # Check that ValueError exception is raised by "input_to_config_dict" when "check_config_type" is "True" and + # "type" key in "input_config" is equal to unexpected value + for none_type_input_config in [ + "{'key_1': 2, 'key_2': 4, 'key_3': 8, 'type': unexpected_type}", + {"key_1": 2, "key_2": 4, "key_3": 8, "type": "unexpected_type"}, + DictConfig(content={"key_1": 2, "key_2": 4, "key_3": 8, "type": "unexpected_type"}), + ]: + with pytest.raises(ValueError): + input_to_config_dict(input_config=none_type_input_config) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_utils_deserialize_enum_value(self): + """ + Description: + Check "deserialize_enum_value" function + + Input data: + "enum_type" Enum object, "value" Enum element or string object + + Expected results: + Test passes if value returned by "deserialize_enum_value" function is equal to expected + + Steps + 1. Check value returned by "deserialize_enum_value" function when Enum class element is specified as "value" + parameter + 2. Check value returned by "deserialize_enum_value" function when string is specified as "value" parameter + 3. Check that ValueError exception is raised when type of "value" parameter for "deserialize_enum_value" + function is not equal to Enum or string + """ + + class ValidationEnum(Enum): + FIRST = "first element" + SECOND = "second element" + + # Checking value returned by "deserialize_enum_value" when Enum class element is specified as "value" + assert deserialize_enum_value(value=ValidationEnum.FIRST, enum_type=ValidationEnum) == ValidationEnum.FIRST + # Checking value returned by "deserialize_enum_value" when string is specified as "value" + assert deserialize_enum_value(value="SECOND", enum_type=ValidationEnum) == ValidationEnum.SECOND + with pytest.raises(KeyError): + deserialize_enum_value(value="THIRD", enum_type=ValidationEnum) + # Checking that ValueError exception is raised when type of "value" for "deserialize_enum_value" is not equal to + # Enum or string + with pytest.raises(ValueError): + deserialize_enum_value(value=1, enum_type=ValidationEnum) # type: ignore + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_utils_ids_to_strings(self): + """ + Description: + Check "ids_to_strings" function + + Input data: + "config_dict" dictionary + + Expected results: + Test passes if dictionary returned by "ids_to_strings" function is equal to expected + """ + config_dict = { + "id_key_1": ID("1"), + "not_id_key_1": 2, + "not_id_key_2": 3, + "id_key_2": ID("4"), + } + expected_dict = { + "id_key_1": "1", + "not_id_key_1": 2, + "not_id_key_2": 3, + "id_key_2": "4", + } + assert ids_to_strings(config_dict) == expected_dict + assert config_dict == expected_dict diff --git a/tests/unit/api/configuration/test_configurable_parameters.py b/tests/unit/api/configuration/test_configurable_parameters.py new file mode 100644 index 00000000000..d6e062f3403 --- /dev/null +++ b/tests/unit/api/configuration/test_configurable_parameters.py @@ -0,0 +1,215 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy + +import pytest + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.configuration.elements import metadata_keys +from otx.api.configuration.enums import AutoHPOState +from otx.api.configuration.enums.config_element_type import ConfigElementType +from otx.api.entities.id import ID +from tests.unit.api.configuration.dummy_config import DatasetManagerConfig +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestConfigurableParameters: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_configurable_parameters(self): + """ + Description: + Check "ConfigurableParameters" class object initialization + + Input data: + "ConfigurableParameters" class object with specified initialization parameters + + Expected results: + Test passes if attributes of initialized "ConfigurableParameters" class object are equal to expected + """ + + def check_configurable_parameters_attributes( + configurable_parameters: ConfigurableParameters, + expected_header: str, + expected_description: str, + expected_id: ID, + expected_visible_in_ui: bool, + ): + assert configurable_parameters.header == expected_header + assert configurable_parameters.description == expected_description + assert configurable_parameters.type == ConfigElementType.CONFIGURABLE_PARAMETERS + assert configurable_parameters.groups == [] + assert configurable_parameters.id == expected_id + assert configurable_parameters.visible_in_ui == expected_visible_in_ui + + header = "Test Header" + # Checking "ConfigurableParameters" initialized with default optional parameters + check_configurable_parameters_attributes( + configurable_parameters=ConfigurableParameters(header=header), + expected_header=header, + expected_description="Default parameter group description", + expected_id=ID(""), + expected_visible_in_ui=True, + ) + # Checking "ConfigurableParameters" initialized with specified optional parameters + description = "Test Description" + config_id = ID("Test ID") + visible_in_ui = False + check_configurable_parameters_attributes( + configurable_parameters=ConfigurableParameters( + header=header, + description=description, + id=config_id, + visible_in_ui=visible_in_ui, + ), + expected_header=header, + expected_description=description, + expected_id=config_id, + expected_visible_in_ui=visible_in_ui, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_set_metadata(self): + """ + Description: + Check "ConfigurableParameters" class parameter metadata setting + + Input data: + Dummy configuration -- DatasetManagerConfig + + Expected results: + Test passes if: + 1. Metadata for a given parameter inside the ConfigurableParameters can be + set successfully, + 2. Attempting to set metadata for a non-existing parameter results in + failure + 3. Attempting to set metadata for a non-existing metadata key results in + failure + 4. Attempting to set metadata to a value of a type that does not match the + original metadata item type results in failure + 5. Resetting the metadata back to its original value can be done + successfully + """ + # Arrange + config = DatasetManagerConfig( + description="Configurable parameters for the DatasetManager -- TEST ONLY", + header="Dataset Manager configuration -- TEST ONLY", + ) + test_parameter_name = "dummy_float_selectable" + metadata_key = metadata_keys.AUTO_HPO_STATE + old_value = config.get_metadata(test_parameter_name)[metadata_key] + new_value = AutoHPOState.OPTIMIZED + + # Act + success = config.set_metadata_value( + parameter_name=test_parameter_name, + metadata_key=metadata_key, + value=new_value, + ) + no_success_invalid_param = config.set_metadata_value( + parameter_name=test_parameter_name + "_invalid", + metadata_key=metadata_key, + value=new_value, + ) + no_success_invalid_key = config.set_metadata_value( + parameter_name=test_parameter_name, + metadata_key=metadata_key + "_invalid", + value=new_value, + ) + no_success_invalid_value_type = config.set_metadata_value( + parameter_name=test_parameter_name, + metadata_key=metadata_key, + value=str(new_value), + ) + config_copy = copy.deepcopy(config) + success_revert = config_copy.set_metadata_value( + parameter_name=test_parameter_name, + metadata_key=metadata_key, + value=old_value, + ) + + # Assert + assert old_value != new_value + assert all([success, success_revert]) + assert not any( + [ + no_success_invalid_key, + no_success_invalid_param, + no_success_invalid_value_type, + ] + ) + assert config.get_metadata(test_parameter_name)[metadata_key] == new_value + assert config_copy.get_metadata(test_parameter_name)[metadata_key] == old_value + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_update_auto_hpo_state(self): + """ + Description: + Check that updating the auto_hpo_state for all parameters within a + "ConfigurableParameters" class parameter works as expected + + Input data: + Dummy configuration -- DatasetManagerConfig + + Expected results: + Test passes if: + 1. The `auto_hpo_state` metadata field is updated to `optimized` upon + calling config.update_auto_hpo_states() if the value for a + configurable parameter matches that of its `auto_hpo_value` + metadata field + 2. The `auto_hpo_state` is updated to `overridden` upon calling + config.update_hpo_state() if the value for a configurable parameter + does not match that of its `auto_hpo_value` metadata field + """ + # Arrange + config = DatasetManagerConfig( + description="Configurable parameters for the DatasetManager -- TEST ONLY", + header="Dataset Manager configuration -- TEST ONLY", + ) + test_parameter_1 = "dummy_float_selectable" + test_parameter_2 = "train_proportion" + auto_hpo_result_float = 4.0 + auto_hpo_result_train_prop = 0.9 + + config.dummy_float_selectable = auto_hpo_result_float + success_1 = config.set_metadata_value( + parameter_name=test_parameter_1, + metadata_key=metadata_keys.AUTO_HPO_VALUE, + value=auto_hpo_result_float, + ) + config.subset_parameters.train_proportion = auto_hpo_result_train_prop + success_2 = config.subset_parameters.set_metadata_value( + parameter_name=test_parameter_2, + metadata_key=metadata_keys.AUTO_HPO_VALUE, + value=auto_hpo_result_train_prop, + ) + + # Act + config.update_auto_hpo_states() + auto_hpo_state_1 = config.get_metadata(test_parameter_1)[metadata_keys.AUTO_HPO_STATE] + auto_hpo_state_2 = config.subset_parameters.get_metadata(test_parameter_2)[metadata_keys.AUTO_HPO_STATE] + + # Simulate override + config.dummy_float_selectable = auto_hpo_result_float - 1 + config.subset_parameters.train_proportion = auto_hpo_result_train_prop - 0.001 + + config.update_auto_hpo_states() + auto_hpo_state_override_1 = config.get_metadata(test_parameter_1)[metadata_keys.AUTO_HPO_STATE] + auto_hpo_state_override_2 = config.subset_parameters.get_metadata(test_parameter_2)[ + metadata_keys.AUTO_HPO_STATE + ] + + # Assert + assert all([success_1, success_2]) + assert auto_hpo_state_1 == AutoHPOState.OPTIMIZED + assert auto_hpo_state_2 == AutoHPOState.OPTIMIZED + assert auto_hpo_state_override_1 == AutoHPOState.OVERRIDDEN + assert auto_hpo_state_override_2 == AutoHPOState.OVERRIDDEN diff --git a/tests/unit/api/configuration/test_configuration_helper.py b/tests/unit/api/configuration/test_configuration_helper.py new file mode 100644 index 00000000000..4ed7a7c2a6d --- /dev/null +++ b/tests/unit/api/configuration/test_configuration_helper.py @@ -0,0 +1,464 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from pathlib import Path + +import pytest +from omegaconf import OmegaConf + +from otx.api.configuration import cfg_helper +from otx.api.configuration.elements import metadata_keys +from otx.api.configuration.enums import AutoHPOState, ModelLifecycle +from tests.unit.api.configuration.dummy_config import ( + DatasetManagerConfig, + SomeEnumSelectable, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestConfigurationHelper: + @staticmethod + def __get_path_to_file(filename: str): + """ + Return the path to the file named 'filename', which lives in the tests/configuration directory + """ + return str(Path(__file__).parent / Path(filename)) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_config_reconstruction(self): + """ + Description: + This test verifies that a configuration can be converted to a dictionary and yaml string, and back again. The + test passes if the reconstructed configuration is equal to the initial one. + + Input data: + Dummy configuration -- DatasetManagerConfig + + Expected results: + Test passes if reconstructed configuration is equal to the original one + + Steps + 1. Create configuration + 2. Convert to dictionary and yaml string representation + 3. Reconstruct configuration from dict + 4. Reconstruct configuration from yaml string + 5. Verify that contents of reconstructed configs are equal to original config + """ + # Initialize the config object + config = DatasetManagerConfig( + description="Configurable parameters for the DatasetManager -- TEST ONLY", + header="Dataset Manager configuration -- TEST ONLY", + ) + + # Convert config to dictionary and to yaml string + cfg = cfg_helper.convert(config, dict) + cfg_yaml = cfg_helper.convert(config, str) + + # Reconstruct the config from dictionary and from yaml string + reconstructed_config = cfg_helper.create(cfg) + reconstructed_config_from_yaml = cfg_helper.create(cfg_yaml) + + # Compare the config dictionaries. Order of some parameters may change in the conversion, so dictionary + # comparison will work while comparing objects or yaml strings directly likely does not result in equality. + assert cfg == cfg_helper.convert(reconstructed_config, dict) + assert cfg == cfg_helper.convert(reconstructed_config_from_yaml, dict) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_config_reconstruction_with_metadata_change(self): + """ + Description: + This test verifies that metadata changes are capture upon converting a + configuration to a dictionary and yaml string, and back again. The test passes + if the reconstructed configuration is equal to the initial one, with + non-default metadata values set. + + Input data: + Dummy configuration -- DatasetManagerConfig + + Expected results: + Test passes if metadata changes are reflected in the reconstructed + configuration, such that it is equal to the original one + + Steps + 1. Create configuration + 2. Set metadata for a parameter to a non-default value + 3. Convert to dictionary and yaml string representation + 4. Reconstruct configuration from dict + 5. Reconstruct configuration from yaml string + 6. Verify that contents of reconstructed configs are equal to original config + """ + # Arrange + # Initialize the config object + config = DatasetManagerConfig( + description="Configurable parameters for the DatasetManager -- TEST ONLY", + header="Dataset Manager configuration -- TEST ONLY", + ) + test_parameter_name = "dummy_float_selectable" + metadata_key = metadata_keys.AUTO_HPO_STATE + old_state = config.get_metadata(test_parameter_name)[metadata_key] + new_state = AutoHPOState.OPTIMIZED + set_success = config.set_metadata_value( + parameter_name=test_parameter_name, + metadata_key=metadata_key, + value=new_state, + ) + + # Act + # Convert config to dictionary and to yaml string + cfg = cfg_helper.convert(config, dict) + cfg_yaml = cfg_helper.convert(config, str) + + # Reconstruct the config from dictionary and from yaml string + reconstructed_config = cfg_helper.create(cfg) + reconstructed_config_from_yaml = cfg_helper.create(cfg_yaml) + + # Assert + assert old_state != new_state + assert set_success + # Check that metadata changes are properly converted + assert reconstructed_config.get_metadata(parameter_name=test_parameter_name)[metadata_key] == new_state + assert ( + reconstructed_config_from_yaml.get_metadata(parameter_name=test_parameter_name)[metadata_key] == new_state + ) + # Compare the config dictionaries + assert cfg == cfg_helper.convert(reconstructed_config, dict) + assert cfg == cfg_helper.convert(reconstructed_config_from_yaml, dict) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_creation_from_yaml(self): + """ + Description: + This test verifies that a configuration can be created from a yaml file + + Input data: + dummy_config.py -- DatasetManagerConfig class definition + dummy_config.yaml -- yaml file specifying a configuration equivalent to DatasetManagerConfig, but in yaml + + Expected results: + Test passes if the contents of the config created from dummy_config.yaml is equal to the configuration in + DatasetManagerConfig + + Steps + 1. Create configuration from class + 2. Create configuration from yaml + 3. Convert both configs to dictionary + 4. Compare resulting dictionaries + """ + # Initialize the config + config = DatasetManagerConfig( + description="Configurable parameters for the DatasetManager -- TEST ONLY", + header="Dataset Manager configuration -- TEST ONLY", + ) + + cfg_from_yaml = cfg_helper.create(self.__get_path_to_file("./dummy_config.yaml")) + + # Compare the config dictionaries. Order of some parameters may change in the conversion, so dictionary + # comparison will work while comparing objects or yaml strings directly likely does not result in equality. + cfg_dict = cfg_helper.convert(config, dict) + cfg_from_yaml_dict = cfg_helper.convert(cfg_from_yaml, dict) + + # Check the parameter groups individually, to narrow down any errors more easily + cfg_subset_parameters = cfg_dict.pop("subset_parameters") + yamlcfg_subset_parameters = cfg_from_yaml_dict.pop("subset_parameters") + assert cfg_subset_parameters == yamlcfg_subset_parameters + + cfg_nested_group = cfg_dict.pop("nested_parameter_group") + yamlcfg_nested_group = cfg_from_yaml_dict.pop("nested_parameter_group") + assert cfg_nested_group == yamlcfg_nested_group + + assert config.dummy_selectable == cfg_dict["dummy_selectable"]["value"] + + assert cfg_dict == cfg_from_yaml_dict + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_broken_config(self): + """ + Description: + This test verifies that a configuration created from a yaml file that contains invalid parameter values will + raise a ValueError. It also verifies that no ValueError is raised if the config is created with valid values. + + Input data: + dummy_broken_config.yaml -- yaml file specifying a configuration, holding an out-of-bounds value for the number + of epochs + + Expected results: + Test passes if a ValueError is raised upon creating the config from yaml, and no ValueError is raised upon + config creation after correcting the out-of-bounds value. + + Steps + 1. Create configuration from dummy_broken_config.yaml + 2. Assert that ValueError is raised + 3. Load dummy_broken_config.yaml as dictionary and correct invalid epochs value + 4. Create configuration from the corrected dictionary + 5. Assert that epochs value is set in the configuration correctly + """ + # Loading a config that has an invalid value (-5 for epochs) should raise a ValueError due to runtime + # input validation upon config creation. + broken_config_path = self.__get_path_to_file("dummy_broken_config.yaml") + with pytest.raises(ValueError) as error: + config = cfg_helper.create(broken_config_path) + + assert "Invalid value set for epochs: -5 is out of bounds." == str(error.value) + + # Test correcting the broken config by first loading it from the yaml file, and then setting epochs to a valid + # value. Config should now be created correctly. Finally, assert that the value for epochs has been corrected + dict_config = OmegaConf.load(broken_config_path) + dict_config.learning_parameters.epochs.value = 10 + config = cfg_helper.create(dict_config) + assert config.learning_parameters.epochs == 10 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_validation(self): + """ + Description: + This test verifies that validating the parameter values in the configuration works. Validation should raise a + ValueError if any of the config parameters is set to an invalid value. + + Input data: + dummy_config.py -- DatasetManagerConfig class definition + + + Expected results: + Test passes if a ValueError is raised upon validation of the configuration, if it contains invalid values for + any of its parameters. + + Steps + 1. Create DatasetManagerConfig configuration + 2. Set test_proportion to an out of bounds value + 3. Validate config and assert that ValueError is raised + 4. Set test_proportion to a valid value + 5. Validate config and assert that no ValueError is raised + 6. Set dummy_selectable parameter to invalid value + 7. Validate config and assert that ValueError is raised + 8. Correct dummy_selectable and assert that ValueError is not raised upon validation + """ + # Initialize the config + config = DatasetManagerConfig( + description="TEST ONLY", + header="TEST ONLY", + ) + + # Assert that config passes validation initially + assert cfg_helper.validate(config) + + # Set invalid test_proportion, and assert that this raises an error upon validation + with pytest.raises(ValueError): + config.subset_parameters.test_proportion = 1.1 + + # Assert that validation passes again after restoring a value that is within the bounds + config.subset_parameters.test_proportion = 0.25 + assert cfg_helper.validate(config) + + # Set value that is not one of the options for dummy_selectable, assert that this raises a ValueError + with pytest.raises(ValueError): + config.dummy_selectable = "invalid_value" + + # Assert that validation passes again after restoring to a value that is in the options list + config.dummy_selectable = "option_c" + assert cfg_helper.validate(config) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_substitute_values(self): + """ + Description: + This test verifies that parameter values can be substituted into a configuration, using an input dictionary or + input configuration. + + Input data: + dummy_config.py -- DatasetManagerConfig class definition + + Expected results: + Test passes if the configuration is successfully converted into a dictionary, + with keys and values matching the structure of the original configuration + + Steps + 1. Create configuration from dummy_config.yaml + 2. Check that values are set according to their initial definition in dummy_config.yaml + 3. Load dummy_config.yaml as an input dictionary, and change some of its parameter values. + 4. Substitute the values from the dictionary + 5. Check that the parameter values have been changed correctly in the config + 6. Change the parameter values in the input dictionary again + 7. Convert the input dictionary to an input configuration + 8. Substitute the values from the input configuration + 9. Check that the parameter values have been changed correctly in the config + """ + # Initialize the config from yaml + config = cfg_helper.create(self.__get_path_to_file("dummy_config.yaml")) + + # Assert that the values are set according to what is specified in the yaml + assert config.subset_parameters.test_proportion == 0.15 + assert config.number_of_samples_for_auto_train == 5 + + # Load the config as a dict from the yaml and change some of the values + config_dict = OmegaConf.load(self.__get_path_to_file("dummy_config.yaml")) + config_dict.subset_parameters.test_proportion.value = 0.05 + config_dict.number_of_samples_for_auto_train.value = 50 + + # Substitute values from this dict + cfg_helper.substitute_values(config, value_input=config_dict) + + # Assert that the values are changed in the config, according to what was substituted above + assert config.subset_parameters.test_proportion == 0.05 + assert config.number_of_samples_for_auto_train == 50 + + # Convert config_dict to actual config object, and then use that as input for the value substitution + config_dict.subset_parameters.test_proportion.value = 0.80 + config_dict.number_of_samples_for_auto_train.value = 500 + reconstructed_config = cfg_helper.create(config_dict) + cfg_helper.substitute_values(config, value_input=reconstructed_config) + + # Assert that the values are changed in the config, according to what was substituted above + assert config.subset_parameters.test_proportion == 0.80 + assert config.number_of_samples_for_auto_train == 500 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_substitute_values_for_lifecycle(self): + """ + Description: + This test verifies that parameter values can be substituted into a + configuration, conditional on the phase in the model lifecycle that they affect + + Input data: + dummy_config.py -- DatasetManagerConfig class definition + + Expected results: + Test passes if the configuration values that affect the model life cycle + specified are updated, whereas others are not + + Steps + 1. Create configuration from dummy_config.yaml + 2. Check that values are set according to their initial definition in + dummy_config.yaml + 3. Create second config from dummy_config.yaml, and change some of its values + 4. Substitute the values from the modified config + 5. Check that the parameter values have been changed correctly in the config + 6. Check that parameter values for parameters that do not belong to the target + model lifecycle did not change + """ + # Initialize the config from yaml + config = cfg_helper.create(self.__get_path_to_file("dummy_config.yaml")) + + # Assert that the values are set according to what is specified in the yaml + assert config.subset_parameters.test_proportion == 0.15 + assert config.subset_parameters.get_metadata("test_proportion")["affects_outcome_of"] == ModelLifecycle.TRAINING + assert config.dummy_float_selectable == 2 + assert config.get_metadata("dummy_float_selectable")["affects_outcome_of"] == ModelLifecycle.NONE + assert config.dummy_selectable == SomeEnumSelectable.BOGUS_NAME + assert config.get_metadata("dummy_selectable")["affects_outcome_of"] == ModelLifecycle.INFERENCE + + # Load the config again from the yaml and change some of the values + config_2 = cfg_helper.create(self.__get_path_to_file("dummy_config.yaml")) + config_2.subset_parameters.test_proportion = 0.05 + config_2.dummy_float_selectable = 4.0 + config_2.dummy_selectable = SomeEnumSelectable.TEST_NAME1 + + cfg_helper.substitute_values_for_lifecycle(config, config_2, model_lifecycle=ModelLifecycle.INFERENCE) + + assert config.subset_parameters.test_proportion == 0.15 + assert config.dummy_selectable == SomeEnumSelectable.TEST_NAME1 + assert config.dummy_float_selectable == 2 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_substitute_values_for_lifecycle_list(self): + """ + Description: + This test verifies that parameter values can be substituted into a + configuration, conditional on the phase in the model lifecycle that they affect + + Input data: + dummy_config.py -- DatasetManagerConfig class definition + + Expected results: + Test passes if the configuration values that affect the model life cycle + specified are updated, whereas others are not + + Steps + 1. Create configuration from dummy_config.yaml + 2. Check that values are set according to their initial definition in + dummy_config.yaml + 3. Create second config from dummy_config.yaml, and change some of its values + 4. Substitute the values from the modified config + 5. Check that the parameter values have been changed correctly in the config + 6. Check that parameter values for parameters that do not belong to the target + model lifecycle did not change + """ + # Initialize the config from yaml + config = cfg_helper.create(self.__get_path_to_file("dummy_config.yaml")) + + # Assert that the values are set according to what is specified in the yaml + assert config.subset_parameters.test_proportion == 0.15 + assert config.subset_parameters.get_metadata("test_proportion")["affects_outcome_of"] == ModelLifecycle.TRAINING + assert config.dummy_float_selectable == 2 + assert config.get_metadata("dummy_float_selectable")["affects_outcome_of"] == ModelLifecycle.NONE + assert config.dummy_selectable == SomeEnumSelectable.BOGUS_NAME + assert config.get_metadata("dummy_selectable")["affects_outcome_of"] == ModelLifecycle.INFERENCE + + # Load the config again from the yaml and change some of the values + config_2 = cfg_helper.create(self.__get_path_to_file("dummy_config.yaml")) + config_2.subset_parameters.test_proportion = 0.05 + config_2.dummy_float_selectable = 4.0 + config_2.dummy_selectable = SomeEnumSelectable.TEST_NAME1 + + cfg_helper.substitute_values_for_lifecycle( + config, + config_2, + model_lifecycle=[ModelLifecycle.INFERENCE, ModelLifecycle.NONE], + ) + + assert config.subset_parameters.test_proportion == 0.15 + assert config.dummy_selectable == SomeEnumSelectable.TEST_NAME1 + assert config.dummy_float_selectable == 4 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_values_only_conversion(self): + """ + Description: + This test verifies that converting a configuration into a dictionary while + retaining only parameter values (and discarding metadata) works + + Input data: + dummy_config.yaml -- yaml file specifying a configuration equivalent to + DatasetManagerConfig, but in yaml + + Expected results: + Test passes if the parameter values of the configuration are updated according to the input dictionary and + input configuration from which the values are substituted. + + Steps + 1. Create DatasetManagerConfig configuration + 2. Change a couple of parameters away from their defaults + 3. Convert the configuration to a dictionary, with `values_only=True` to + discard meta-data + 4. Assert that the resulting dictionary contains the expected values + """ + config = DatasetManagerConfig() + config.subset_parameters.test_proportion = 0.3 + config.dummy_selectable = SomeEnumSelectable.OPTION_C + + config_dict = cfg_helper.convert(config, target=dict, values_only=True) + + assert config_dict["subset_parameters"]["test_proportion"] == 0.3 + assert config_dict["dummy_selectable"] == SomeEnumSelectable.OPTION_C + assert config_dict["number_of_samples_for_auto_train"] == 5 + assert config_dict["nested_parameter_group"]["subgroup_one"]["bogus_parameter_one"] == 42 diff --git a/tests/unit/api/configuration/test_model_configuration.py b/tests/unit/api/configuration/test_model_configuration.py new file mode 100644 index 00000000000..3ae96270773 --- /dev/null +++ b/tests/unit/api/configuration/test_model_configuration.py @@ -0,0 +1,31 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.api.configuration.default_model_parameters import DefaultModelParameters +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelConfiguration: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_configuration(self): + mc = DefaultModelParameters() + assert hasattr(mc, "learning_parameters") + + epoch_default = mc.learning_parameters.get_metadata("epochs")["default_value"] + batch_size_default = mc.learning_parameters.get_metadata("batch_size")["default_value"] + + assert mc.learning_parameters.epochs == epoch_default + assert mc.learning_parameters.batch_size == batch_size_default + + mc.learning_parameters.epochs = epoch_default + 5 + mc.learning_parameters.batch_size = batch_size_default + 4 + + assert mc.learning_parameters.batch_size == batch_size_default + 4 + assert mc.learning_parameters.epochs == epoch_default + 5 diff --git a/tests/unit/api/constants/__init__.py b/tests/unit/api/constants/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/api/constants/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/api/constants/components.py b/tests/unit/api/constants/components.py new file mode 100644 index 00000000000..dcd56f4339d --- /dev/null +++ b/tests/unit/api/constants/components.py @@ -0,0 +1,25 @@ +""" +OTX components markers. +""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +try: + from e2e.markers.mark_meta import MarkMeta +except ImportError: + + class MarkMeta: # type: ignore[no-redef] + """ + Empty marker. + """ + + +class OtxSdkComponent(MarkMeta): + """ + OTX Component marker. + """ + + OTX_API = "otx.api" diff --git a/tests/unit/api/constants/requirements.py b/tests/unit/api/constants/requirements.py new file mode 100644 index 00000000000..559ada8a39d --- /dev/null +++ b/tests/unit/api/constants/requirements.py @@ -0,0 +1,15 @@ +""" +Requirements module. +""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +class Requirements: + """ + Dummy requirement. + """ + + REQ_1 = "Dummy requirement" diff --git a/tests/unit/api/entities/dummy_config.yaml b/tests/unit/api/entities/dummy_config.yaml new file mode 100644 index 00000000000..84789f4f12b --- /dev/null +++ b/tests/unit/api/entities/dummy_config.yaml @@ -0,0 +1,148 @@ +description: Configuration for an object detection task -- TEST ONLY +header: Configuration for an object detection task -- TEST ONLY +learning_parameters: + batch_size: + affects_outcome_of: TRAINING + default_value: 5 + description: Test description + editable: true + header: Batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 5 + visible_in_ui: true + warning: Test warning + description: Learning Parameters + header: Learning Parameters + learning_rate: + affects_outcome_of: TRAINING + default_value: 0.01 + description: Test learning_rate description + editable: true + header: Learning rate --TEST ONLY + max_value: 0.1 + min_value: 1.0e-07 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.01 + visible_in_ui: true + warning: null + learning_rate_warmup_iters: + affects_outcome_of: TRAINING + default_value: 100 + description: "" + editable: true + header: Test learning_rate_warmup_iters header + max_value: 10000 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 100 + visible_in_ui: true + warning: null + num_checkpoints: + affects_outcome_of: NONE + default_value: 5 + description: "" + editable: true + header: Test num_checkpoints header + max_value: 100 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 5 + visible_in_ui: true + warning: null + num_iters: + affects_outcome_of: TRAINING + default_value: 1 + description: Test num_iters description + editable: true + header: Test num_iters header + max_value: 100000 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: null + num_workers: + affects_outcome_of: NONE + default_value: 0 + description: Test num_workers description + editable: true + header: Test num_workers header + max_value: 8 + min_value: 0 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0 + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +postprocessing: + confidence_threshold: + affects_outcome_of: INFERENCE + default_value: 0.35 + description: Test postprocessing confidence_threshold description + editable: true + header: Test postprocessing confidence_threshold header + max_value: 1 + min_value: 0 + type: FLOAT + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.35 + visible_in_ui: true + warning: null + description: Postprocessing + header: Postprocessing + result_based_confidence_threshold: + affects_outcome_of: INFERENCE + default_value: true + description: Test result_based_confidence_threshold description + editable: true + header: Test result_based_confidence_threshold header + type: BOOLEAN + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: true + visible_in_ui: true + warning: null + type: PARAMETER_GROUP + visible_in_ui: true +type: CONFIGURABLE_PARAMETERS +visible_in_ui: true diff --git a/tests/unit/api/entities/dummy_template.yaml b/tests/unit/api/entities/dummy_template.yaml new file mode 100644 index 00000000000..2cb77704b23 --- /dev/null +++ b/tests/unit/api/entities/dummy_template.yaml @@ -0,0 +1,34 @@ +name: Custom Object Detection -- TEST ONLY +task_type: DETECTION +task_family: VISION +instantiation: "CLASS" +summary: Fast and lightweight object detector. +application: ~ + +framework: Test framework + +entrypoints: + base: base entrypoints + +hyper_parameters: + base_path: ./dummy_config.yaml + parameter_overrides: + learning_parameters: + batch_size: + default_value: 64 + learning_rate: + default_value: 0.05 + learning_rate_warmup_iters: + default_value: 100 + num_iters: + default_value: 13000 + +max_nodes: 1 +training_targets: + - GPU + - CPU + +inference_targets: + - CPU + - GPU + - VPU diff --git a/tests/unit/api/entities/interfaces/test_graph_interface.py b/tests/unit/api/entities/interfaces/test_graph_interface.py new file mode 100644 index 00000000000..43d199b486e --- /dev/null +++ b/tests/unit/api/entities/interfaces/test_graph_interface.py @@ -0,0 +1,52 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from unittest.mock import patch + +import pytest + +from otx.api.entities.interfaces.graph_interface import IGraph +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIGraph: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @patch("otx.api.entities.interfaces.graph_interface.IGraph.__abstractmethods__", set()) + def test_i_graph(self): + """ + Description: + Check IGraph class object initialization + + Input data: + IGraph object + + Expected results: + Test passes if IGraph object methods raise NotImplementedError exception + """ + i_graph = IGraph() + with pytest.raises(NotImplementedError): + i_graph.add_node(1) + with pytest.raises(NotImplementedError): + i_graph.add_edge(1, 2) + with pytest.raises(NotImplementedError): + i_graph.has_edge_between(1, 2) + with pytest.raises(NotImplementedError): + i_graph.neighbors(1) + with pytest.raises(NotImplementedError): + i_graph.find_cliques() + with pytest.raises(NotImplementedError): + i_graph.num_nodes() + with pytest.raises(NotImplementedError): + i_graph.remove_edges(1, 2) + with pytest.raises(NotImplementedError): + i_graph.find_out_edges(1) + with pytest.raises(NotImplementedError): + i_graph.find_in_edges(1) + with pytest.raises(NotImplementedError): + i_graph.edges + with pytest.raises(NotImplementedError): + i_graph.nodes diff --git a/tests/unit/api/entities/shapes/test_ellipse.py b/tests/unit/api/entities/shapes/test_ellipse.py new file mode 100644 index 00000000000..c50c6e5c745 --- /dev/null +++ b/tests/unit/api/entities/shapes/test_ellipse.py @@ -0,0 +1,315 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import pytest +from shapely.geometry.polygon import Polygon + +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.utils.time_utils import now +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestEllipse: + modification_date = now() + + def ellipse_params(self): + ellipse_params = { + "x1": 0.5, + "x2": 1.0, + "y1": 0.0, + "y2": 0.5, + "modification_date": self.modification_date, + } + return ellipse_params + + def ellipse(self): + return Ellipse(**self.ellipse_params()) + + @staticmethod + def width_gt_height_ellipse_params(): + width_gt_height_ellipse_params = { + "x1": 0.5, + "x2": 0.8, + "y1": 0.1, + "y2": 0.3, + } + return width_gt_height_ellipse_params + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_ellipse(self): + """ + Description: + Check ellipse parameters + + Input data: + Coordinates + + Expected results: + Test passes if Ellipse correctly calculates parameters and returns default values + + Steps + 1. Check ellipse params + 2. Check ellipse default values + 3. Check ellipse with incorrect coordinates + """ + + ellipse = self.ellipse() + modification_date = self.modification_date + + assert ellipse.width == 0.5 + assert ellipse.height == 0.5 + assert ellipse.x_center == 0.75 + assert ellipse.y_center == 0.25 + assert ellipse.minor_axis == 0.25 + assert ellipse.major_axis == 0.25 + assert ellipse.modification_date == modification_date + + incorrect_ellipse_params = { + "x1": 0, + "x2": 0, + "y1": 0, + "y2": 0, + } + + with pytest.raises(ValueError): + Ellipse(**incorrect_ellipse_params) + + width_lt_height_ellipse_params = { + "x1": 0.4, + "x2": 0.5, + "y1": 0.3, + "y2": 0.4, + } + + width_lt_height_ellipse = Ellipse(**width_lt_height_ellipse_params) + assert width_lt_height_ellipse.height > width_lt_height_ellipse.width + assert width_lt_height_ellipse.major_axis == width_lt_height_ellipse.height / 2 + assert width_lt_height_ellipse.width == pytest.approx(0.09999999999999998) + assert width_lt_height_ellipse.height == pytest.approx(0.10000000000000003) + assert width_lt_height_ellipse.x_center == pytest.approx(0.45) + assert width_lt_height_ellipse.y_center == pytest.approx(0.35) + assert width_lt_height_ellipse.minor_axis == pytest.approx(0.04999999999999999) + assert width_lt_height_ellipse.major_axis == pytest.approx(0.05000000000000002) + + width_gt_height_ellipse = Ellipse(**self.width_gt_height_ellipse_params()) + assert width_gt_height_ellipse.height < width_gt_height_ellipse.width + assert width_gt_height_ellipse.minor_axis == width_gt_height_ellipse.height / 2 + assert width_gt_height_ellipse.width == pytest.approx(0.30000000000000004) + assert width_gt_height_ellipse.height == pytest.approx(0.19999999999999998) + assert width_gt_height_ellipse.x_center == pytest.approx(0.65) + assert width_gt_height_ellipse.y_center == pytest.approx(0.2) + assert width_gt_height_ellipse.minor_axis == pytest.approx(0.09999999999999999) + assert width_gt_height_ellipse.major_axis == pytest.approx(0.15000000000000002) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_ellipse_magic_methods(self): + """ + Description: + Check Ellipse __repr__, __eq__, __hash__ methods + + Input data: + Initialized instance of Ellipse + + Expected results: + Test passes if Ellipse magic methods returns correct values + + Steps + 1. Initialize Ellipse instance + 2. Check returning value of magic methods + """ + + x1 = self.ellipse_params()["x1"] + x2 = self.ellipse_params()["x2"] + y1 = self.ellipse_params()["y1"] + y2 = self.ellipse_params()["y2"] + + ellipse = self.ellipse() + assert repr(ellipse) == f"Ellipse(x1={x1}, y1={y1}, x2={x2}, y2={y2})" + + other_ellipse_params = { + "x1": 0.5, + "x2": 1.0, + "y1": 0.0, + "y2": 0.5, + "modification_date": self.modification_date, + } + + third_ellipse_params = { + "x1": 0.3, + "y1": 0.5, + "x2": 0.4, + "y2": 0.6, + "modification_date": self.modification_date, + } + + other_ellipse = Ellipse(**other_ellipse_params) + third_ellipse = Ellipse(**third_ellipse_params) + + assert ellipse == other_ellipse + assert ellipse != third_ellipse + assert ellipse != str + + assert hash(ellipse) == hash(str(ellipse)) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_ellipse_normalize_wrt_roi_shape(self): + """ + Description: + Check Ellipse normalize_wrt_roi_shape methods + + Input data: + Initialized instance of Ellipse + Initialized instance of Rectangle + + Expected results: + Test passes if Ellipse normalize_wrt_roi_shape returns correct values + + Steps + 1. Initialize Ellipse instance + 2. Check returning value + """ + + ellipse = self.ellipse() + roi = Rectangle(x1=0.0, x2=0.5, y1=0.0, y2=0.5) + normalized = ellipse.normalize_wrt_roi_shape(roi) + assert normalized.x1 == 0.25 + assert normalized.y1 == 0.0 + assert normalized.x2 == 0.5 + assert normalized.y2 == 0.25 + + with pytest.raises(ValueError): + ellipse.normalize_wrt_roi_shape("123") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_ellipse_denormalize_wrt_roi_shape(self): + """ + Description: + Check Ellipse denormalize_wrt_roi_shape methods + + Input data: + Initialized instance of Ellipse + Initialized instance of Rectangle + + Expected results: + Test passes if Ellipse denormalize_wrt_roi_shape returns correct values + + Steps + 1. Initialize Ellipse instance + 2. Check returning value + """ + + ellipse = self.ellipse() + roi = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0) + denormalized = ellipse.denormalize_wrt_roi_shape(roi) + assert denormalized.x1 == 0.0 + assert denormalized.y1 == 0.0 + assert denormalized.x2 == 1.0 + assert denormalized.y2 == 0.5 + + with pytest.raises(ValueError): + ellipse.denormalize_wrt_roi_shape("123") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_ellipse_get_evenly_distributed_ellipse_coordinates(self): + """ + Description: + Check Ellipse get_evenly_distributed_ellipse_coordinates methods + + Input data: + Initialized instance of Ellipse + + Expected results: + Test passes if Ellipse get_evenly_distributed_ellipse_coordinates returns correct values + + Steps + 1. Initialize Ellipse instance + 2. Check returning value + """ + ellipse = self.ellipse() + number_of_coordinates = 3 + coordinates_ellipse_line = ellipse.get_evenly_distributed_ellipse_coordinates(number_of_coordinates) + assert len(coordinates_ellipse_line) == 3 + assert coordinates_ellipse_line[0] == pytest.approx((1.0, 0.25)) + assert coordinates_ellipse_line[1] == pytest.approx((0.625, 0.4665063509461097)) + assert coordinates_ellipse_line[2] == pytest.approx((0.6249999999999999, 0.033493649053890406)) + + width_gt_height_ellipse = Ellipse(**self.width_gt_height_ellipse_params()) + coordinates_ellipse_line = width_gt_height_ellipse.get_evenly_distributed_ellipse_coordinates( + number_of_coordinates + ) + assert width_gt_height_ellipse.height < width_gt_height_ellipse.width + assert len(coordinates_ellipse_line) == 3 + assert coordinates_ellipse_line[0] == pytest.approx((0.65, 0.3)) + assert coordinates_ellipse_line[1] == pytest.approx((0.7666223198362645, 0.1371094972158116)) + assert coordinates_ellipse_line[2] == pytest.approx((0.5333776801637811, 0.13710949721577403)) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_ellipse__as_shapely_polygon(self): + """ + Description: + Check Ellipse _as_shapely_polygon methods + + Input data: + Initialized instance of Ellipse + + Expected results: + Test passes if Ellipse _as_shapely_polygon returns correct values + + Steps + 1. Initialize Ellipse instance + 2. Check returning value + """ + + ellipse = self.ellipse() + shapely_polygon = ellipse._as_shapely_polygon() + assert shapely_polygon.__class__ == Polygon + assert shapely_polygon.area == pytest.approx(0.1958331774442254) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_ellipse_get_area(self): + """ + Description: + Check Ellipse get_area methods + + Input data: + Initialized instance of Ellipse + + Expected results: + Test passes if Ellipse get_area returns correct values + + Steps + 1. Initialize Ellipse instance + 2. Check returning value + """ + + ellipse = self.ellipse() + area = ellipse.get_area() + assert area == pytest.approx(0.19634954084936207) diff --git a/tests/unit/api/entities/shapes/test_polygon.py b/tests/unit/api/entities/shapes/test_polygon.py new file mode 100644 index 00000000000..9a66de85fa5 --- /dev/null +++ b/tests/unit/api/entities/shapes/test_polygon.py @@ -0,0 +1,325 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from operator import attrgetter + +import pytest + +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.utils.time_utils import now +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestPoint: + def coordinates(self): + return [0.5, 0.4] + + def other_coordinates(self): + return [0.3, 0.2] + + def point(self): + return Point(*self.coordinates()) + + def other_point(self): + return Point(*self.other_coordinates()) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_point_magic_methods(self): + """ + Description: + Check Point __repr__, __eq__ methods + + Input data: + Initialized instance of Point + + Expected results: + Test passes if Point magic methods returns correct values + + Steps + 1. Initialize Point instance + 2. Check returning value of magic methods + """ + + point1 = self.point() + assert repr(point1) == "Point(0.5, 0.4)" + + point2 = self.point() + point3 = self.other_point() + assert point1 == point2 + assert point1 != point3 + assert point1 != str + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_point_normalize_wrt_roi(self): + """ + Description: + Check Point normalize_wrt_roi methods + + Input data: + Initialized instance of Point + Initialized instance of Rectangle + + Expected results: + Test passes if Point normalize_wrt_roi returns correct values + + Steps + 1. Initialize Point instance + 2. Check returning value + """ + + point = self.point() + roi = Rectangle(x1=0.3, x2=0.5, y1=0.3, y2=0.5) + normalized = point.normalize_wrt_roi(roi) + assert normalized.x == 0.4 + assert normalized.y == 0.38 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_point_denormalize_wrt_roi_shape(self): + """ + Description: + Check Point denormalize_wrt_roi_shape methods + + Input data: + Initialized instance of Point + Initialized instance of Rectangle + + Expected results: + Test passes if Point denormalize_wrt_roi_shape returns correct values + + Steps + 1. Initialize Point instance + 2. Check returning value + """ + + point = self.point() + roi = Rectangle(x1=0.4, x2=0.5, y1=0.3, y2=0.5) + normalized = point.denormalize_wrt_roi_shape(roi) + assert normalized.x == 1.0 + assert normalized.y == 0.5000000000000001 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestPolygon: + modification_date = now() + + def points(self): + point1 = Point(0.5, 0.0) + point2 = Point(0.75, 0.2) + point3 = Point(0.6, 0.1) + return [point1, point2, point3] + + def other_points(self): + point1 = Point(0.3, 0.1) + point2 = Point(0.8, 0.3) + point3 = Point(0.6, 0.2) + return [point1, point2, point3] + + def polygon(self): + return Polygon(self.points(), modification_date=self.modification_date) + + def other_polygon(self): + return Polygon(self.other_points(), modification_date=self.modification_date) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_polygon(self): + """ + Description: + Check Polygon parameters + + Input data: + Points + + Expected results: + Test passes if Polygon correctly calculates parameters and returns default values + + Steps + 1. Check Polygon params + 2. Check Polygon default values + 3. Check Polygon with empty points + """ + + polygon = self.polygon() + modification_date = self.modification_date + assert len(polygon.points) == 3 + assert polygon.modification_date == modification_date + assert polygon.points == self.points() + + empty_points_list = [] + with pytest.raises(ValueError): + Polygon(empty_points_list) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_polygon_magic_methods(self): + """ + Description: + Check Polygon __repr__, __eq__, __hash__ methods + + Input data: + Initialized instance of Polygon + + Expected results: + Test passes if Polygon magic methods returns correct values + + Steps + 1. Initialize Polygon instance + 2. Check returning value of magic methods + """ + + polygon = self.polygon() + points_len = len(self.points()) + min_x = min(self.points(), key=attrgetter("x")).x + max_x = max(self.points(), key=attrgetter("x")).x + min_y = min(self.points(), key=attrgetter("y")).y + max_y = max(self.points(), key=attrgetter("y")).y + + assert f"Polygon(len(points)={points_len}, min_x={min_x}" in repr(polygon) + assert f", max_x={max_x}, min_y={min_y}, max_y={max_y})" in repr(polygon) + + other_polygon = self.polygon() + thirs_polygon = self.other_polygon() + assert polygon == other_polygon + assert polygon != thirs_polygon + assert polygon != str + + assert hash(polygon) == hash(str(polygon)) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_polygon_normalize_wrt_roi_shape(self): + """ + Description: + Check Polygon normalize_wrt_roi_shape methods + + Input data: + Initialized instance of Polygon + Initialized instance of Rectangle + + Expected results: + Test passes if Polygon normalize_wrt_roi_shape returns correct values + + Steps + 1. Initialize Polygon instance + 2. Check returning value + """ + + polygon = self.polygon() + roi = Rectangle(x1=0.0, x2=0.5, y1=0.0, y2=0.5) + normalized = polygon.normalize_wrt_roi_shape(roi) + assert len(normalized.points) == 3 + assert normalized.min_x == 0.25 + assert normalized.max_x == 0.375 + assert normalized.min_y == 0.0 + assert normalized.max_y == 0.1 + + with pytest.raises(ValueError): + polygon.normalize_wrt_roi_shape("123") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_polygon_denormalize_wrt_roi_shape(self): + """ + Description: + Check Polygon denormalize_wrt_roi_shape methods + + Input data: + Initialized instance of Polygon + Initialized instance of Rectangle + + Expected results: + Test passes if Polygon denormalize_wrt_roi_shape returns correct values + + Steps + 1. Initialize Polygon instance + 2. Check returning value + """ + + polygon = self.polygon() + roi = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0) + denormalized = polygon.denormalize_wrt_roi_shape(roi) + assert len(denormalized.points) == 3 + assert denormalized.min_x == 0.0 + assert denormalized.max_x == 0.5 + assert denormalized.min_y == 0.0 + assert denormalized.max_y == 0.2 + + with pytest.raises(ValueError): + polygon.denormalize_wrt_roi_shape("123") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_polygon__as_shapely_polygon(self): + """ + Description: + Check Polygon _as_shapely_polygon methods + + Input data: + Initialized instance of Polygon + + Expected results: + Test passes if Polygon _as_shapely_polygon returns correct values + + Steps + 1. Initialize Polygon instance + 2. Check returning value + """ + + polygon = self.polygon() + polygon2 = self.other_polygon() + shapely_polygon = polygon._as_shapely_polygon() + shapely_polygon2 = polygon2._as_shapely_polygon() + assert shapely_polygon.area == 0.0025000000000000022 + assert str(shapely_polygon) == "POLYGON ((0.5 0, 0.75 0.2, 0.6 0.1, 0.5 0))" + assert shapely_polygon != shapely_polygon2 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_polygon_get_area(self): + """ + Description: + Check Polygon get_area method + + Input data: + Initialized instance of Polygon + + Expected results: + Test passes if Polygon get_area returns correct values + + Steps + 1. Initialize Polygon instance + 2. Check returning value + """ + + polygon = self.polygon() + polygon2 = self.other_polygon() + area = polygon.get_area() + area2 = polygon2.get_area() + assert area == 0.0025000000000000022 + assert area != area2 diff --git a/tests/unit/api/entities/shapes/test_rectangle.py b/tests/unit/api/entities/shapes/test_rectangle.py new file mode 100644 index 00000000000..8fb35bca124 --- /dev/null +++ b/tests/unit/api/entities/shapes/test_rectangle.py @@ -0,0 +1,656 @@ +# Copyright (C) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import itertools +import warnings +from datetime import datetime + +import numpy as np +import pytest +from shapely.geometry.polygon import Polygon + +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import LabelEntity +from otx.api.entities.scored_label import Domain, ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.shapes.shape import ShapeType +from otx.api.utils.time_utils import now +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestRectangle: + modification_date = now() + + @staticmethod + def rectangle_labels() -> list: + rectangle_label = LabelEntity( + name="Rectangle label", + domain=Domain.DETECTION, + color=Color(red=100, green=50, blue=200), + id=ID("rectangle_label_1"), + ) + other_rectangle_label = LabelEntity( + name="Other rectangle label", + domain=Domain.SEGMENTATION, + color=Color(red=200, green=80, blue=100), + id=ID("rectangle_label_2"), + ) + return [ + ScoredLabel(label=rectangle_label), + ScoredLabel(label=other_rectangle_label), + ] + + @staticmethod + def horizontal_rectangle_params() -> dict: + return {"x1": 0.1, "y1": 0.0, "x2": 0.4, "y2": 0.2} + + def horizontal_rectangle(self) -> Rectangle: + return Rectangle(**self.horizontal_rectangle_params()) + + def vertical_rectangle_params(self) -> dict: + return { + "x1": 0.1, + "y1": 0.1, + "x2": 0.3, + "y2": 0.4, + "modification_date": datetime(year=2020, month=1, day=1, hour=9, minute=30, second=15, microsecond=2), + } + + def vertical_rectangle(self) -> Rectangle: + return Rectangle(**self.vertical_rectangle_params()) + + @staticmethod + def square_params() -> dict: + return {"x1": 0.1, "y1": 0.1, "x2": 0.3, "y2": 0.3} + + def square(self) -> Rectangle: + return Rectangle(**self.square_params()) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_required_parameters(self): + """ + Description: + Check Rectangle class instance required parameters + + Input data: + Rectangle class initiation parameters + + Expected results: + Test passes if Rectangle instance has expected attributes specified during required parameters initiation + + Steps + 1. Compare x1, y1, x2, y2 and type Rectangle instance parameters with expected values + """ + rectangle = self.horizontal_rectangle() + assert rectangle.x1 == 0.1 + assert rectangle.y1 == 0.0 + assert rectangle.x2 == 0.4 + assert rectangle.y2 == 0.2 + assert rectangle.type == ShapeType.RECTANGLE + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_optional_parameters(self): + """ + Description: + Check Rectangle optional parameters + + Input data: + Instance of Rectangle class + + Expected results: + Test passes if Rectangle instance has expected modification attributes specified during Rectangle + class object initiation with optional parameters + + Steps + 1. Compare default Rectangle instance attribute with expected value + 2. Check type of default modification_date Rectangle instance attribute + 3. Compare specified Rectangle instance attribute with expected value + 4. Compare specified modification_date Rectangle instance attribute with expected value + """ + # Checking default values of optional parameters + default_params_rectangle = self.horizontal_rectangle() + assert isinstance(default_params_rectangle.modification_date, datetime) + # check for specified values of optional parameters + specified_params_rectangle = self.vertical_rectangle() + assert specified_params_rectangle.modification_date == datetime( + year=2020, month=1, day=1, hour=9, minute=30, second=15, microsecond=2 + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_coordinates_validation(self): + """ + Description: + Check Rectangle coordinates + + Input data: + Rectangle class initiation parameters + + Expected results: + Test passes if Rectangle correctly checks coordinates during initiation of Rectangle object + + Steps + 1. Check Rectangle coordinates with correct width and height + 2. Check Rectangle coordinates with incorrect width: (x2 - x1) <= 0 + 3. Check Rectangle coordinates with incorrect height: (y2 - y1) <= 0 + 4. Check Rectangle coordinates with all coordinates equal 0 + """ + # checks for correct width and height + self.horizontal_rectangle() + self.vertical_rectangle() + self.square() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Rectangle coordinates") + Rectangle(x1=0.2, y1=0.1, x2=1.4, y2=1.5) + Rectangle(x1=0.2, y1=0.1, x2=0.4, y2=0.5) + # checks for incorrect coordinates + width_less_than_zero_params = {"x1": 0.4, "y1": 0.0, "x2": 0.1, "y2": 0.2} + width_equal_zero_params = {"x1": 0.1, "y1": 0.0, "x2": 0.1, "y2": 0.2} + height_less_than_zero_params = {"x1": 0.1, "y1": 0.4, "x2": 0.3, "y2": 0.1} + height_params_equal_zero_params = {"x1": 0.1, "y1": 0.4, "x2": 0.3, "y2": 0.4} + zero_rectangle_params = {"x1": 0.0, "x2": 0.0, "y1": 0.0, "y2": 0.0} + for incorrect_coordinates in [ + width_less_than_zero_params, + width_equal_zero_params, + height_less_than_zero_params, + height_params_equal_zero_params, + zero_rectangle_params, + ]: + with pytest.raises(ValueError): + Rectangle(**incorrect_coordinates) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_repr(self): + """ + Description: + Check Rectangle __repr__ method + + Input data: + Instance of Rectangle class + + Expected results: + Test passes if __repr__ method prints expected message for Rectangle class instance + + Steps + 1. Check message printed by __repr__ method + """ + rectangle = self.horizontal_rectangle() + assert repr(rectangle) == "Rectangle(x=0.1, y=0.0, width=0.30000000000000004, height=0.2)" + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_eq(self): + """ + Description: + Check Rectangle __eq__ method + + Input data: + Instances of Rectangle class + + Expected results: + Test passes if __eq__ method returns expected bool value + + Steps + 1. Check __eq__ method for instances of Rectangle class with equal parameters + 2. Check __eq__ method for different instances of Rectangle class + 3. Check __eq__ method for instances of different classes + 4. Check __eq__ method for instances of Rectangle class with unequal x1, y1, x2, y2 and + modification_date attributes + """ + rectangle = self.vertical_rectangle() + # Check for instances with equal parameters + equal_rectangle = self.vertical_rectangle() + assert rectangle == equal_rectangle + # Check for different instances of Rectangle class + assert rectangle != self.horizontal_rectangle() + # Check for different types branch + assert rectangle != str + + assert rectangle == equal_rectangle + # Check for instances with unequal parameters combinations + # Generating all possible scenarios of parameter values submission + keys_list = ["x1", "y1", "x2", "y2", "modification_date"] + parameter_combinations = [] + for i in range(1, len(keys_list) + 1): + parameter_combinations.append(list(itertools.combinations(keys_list, i))) + # In each of scenario creating a copy of equal Rectangle parameters and replacing to values from prepared + # dictionary + unequal_values_dict = { + "x1": 0.09, + "y1": 0.09, + "x2": 0.29, + "y2": 0.39, + "modification_date": datetime(year=2019, month=2, day=3, hour=7, minute=15, second=10, microsecond=1), + } + for scenario in parameter_combinations: + for rectangle_parameters in scenario: + unequal_rectangle_params_dict = dict(self.vertical_rectangle_params()) + for key in rectangle_parameters: + unequal_rectangle_params_dict[key] = unequal_values_dict.get(key) + unequal_rectangle = Rectangle(**unequal_rectangle_params_dict) + assert rectangle != unequal_rectangle, ( + "Failed to check that Rectangle instances with different " f"{rectangle_parameters} are unequal" + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_hash(self): + """ + Description: + Check Rectangle __hash__ method + + Input data: + Instance of Rectangle class + + Expected results: + Test passes if __hash__ method returns expected value + + Steps + 1. Check value returned by __hash__ method + """ + rectangle = self.horizontal_rectangle() + assert hash(rectangle) == hash(str(rectangle)) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_clip_to_visible_region(self): + """ + Description: + Check Rectangle clip_to_visible_region method + + Input data: + Rectangle class initiation parameters + + Expected results: + Test passes if clip_to_visible_region method return correct value + + Steps + 1. Check values returned by clip_to_visible_region method for 01, y2>1 + 3. Check values returned by clip_to_visible_region method for x1=0, y1=0, x2=1, y2=1 + 4. Check ValueError exception raised if x1<0 and x11: clipped Rectangle width will be equal 0 + 6. Check ValueError exception raised if y1<0 and y11: clipped Rectangle height will be equal 0 + """ + positive_scenarios = [ + { + "input_params": { + "x1": 0.3, + "y1": 0.2, + "x2": 0.6, + "y2": 0.4, + }, + "params_expected": {"x1": 0.3, "y1": 0.2, "x2": 0.6, "y2": 0.4}, + }, + { + "input_params": { + "x1": -0.2, + "y1": -0.3, + "x2": 1.6, + "y2": 1.4, + }, + "params_expected": {"x1": 0.0, "y1": 0.0, "x2": 1.0, "y2": 1.0}, + }, + { + "input_params": { + "x1": 0.0, + "y1": 0.0, + "x2": 1.0, + "y2": 1.0, + }, + "params_expected": {"x1": 0.0, "y1": 0.0, "x2": 1.0, "y2": 1.0}, + }, + ] + for scenario in positive_scenarios: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Rectangle coordinates") + rectangle_actual = Rectangle(**scenario.get("input_params")) + rectangle_expected = Rectangle(**scenario.get("params_expected")) + rectangle_actual.modification_date = self.modification_date + rectangle_expected.modification_date = self.modification_date + assert rectangle_actual.clip_to_visible_region() == rectangle_expected + negative_scenarios = [ + {"x1": -0.4, "y1": 0.2, "x2": -0.2, "y2": 0.4}, + {"x1": 1.2, "y1": 0.2, "x2": 1.6, "y2": 0.4}, + {"x1": 0.4, "y1": -0.4, "x2": 0.6, "y2": -0.2}, + {"x1": 1.2, "y1": 1.2, "x2": 1.6, "y2": 1.4}, + ] + for scenario in negative_scenarios: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Rectangle coordinates") + rectangle_actual = Rectangle(**scenario) + with pytest.raises(ValueError): + rectangle_actual.clip_to_visible_region() + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_normalize_wrt_roi_shape(self): + """ + Description: + Check Rectangle normalize_wrt_roi_shape method + + Input data: + Instance of Rectangle class + + Expected results: + Test passes if normalize_wrt_roi_shape method returns expected instance of Rectangle class + + Steps + 1. Check values returned by normalized Rectangle instance + 2. Check raise ValueError exception when roi parameter has unexpected type + """ + # Positive scenario + rectangle = self.horizontal_rectangle() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Rectangle coordinates") + roi_shape = Rectangle(x1=0.0, y1=0.0, x2=2.1, y2=2.2) + normalized = rectangle.normalize_wrt_roi_shape(roi_shape) + assert normalized.x1 == 0.1 + assert normalized.y1 == 0.0 + assert normalized.x2 == 0.4 + assert normalized.y2 == 0.2 + assert normalized.modification_date == rectangle.modification_date + # Negative scenario + with pytest.raises(ValueError): + rectangle.normalize_wrt_roi_shape(str) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_denormalize_wrt_roi_shape(self): + """ + Description: + Check Rectangle denormalize_wrt_roi_shape method + + Input data: + Instance of Rectangle class + + Expected results: + Test passes if denormalize_wrt_roi_shape method return expected instance of Rectangle class + + Steps + 1. Check values returned by denormalized Rectangle instance + 2. Check raise ValueError exception when roi parameter has unexpected type + """ + # Positive scenario + rectangle = self.horizontal_rectangle() + roi_shape = Rectangle(x1=0.2, y1=0.2, x2=0.4, y2=0.4) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Rectangle coordinates") + denormalized = rectangle.denormalize_wrt_roi_shape(roi_shape) + assert denormalized.x1 == -0.5 + assert denormalized.y1 == -1.0 + assert denormalized.x2 == 1.0 + assert denormalized.y2 == 0.0 + assert denormalized.modification_date == rectangle.modification_date + # Negative scenario + with pytest.raises(ValueError): + rectangle.denormalize_wrt_roi_shape(str) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_as_shapely_polygon(self): + """ + Description: + Check Rectangle _as_shapely_polygon method + + Input data: + Instance of Rectangle class + + Expected results: + Test passes if _as_shapely_polygon method returns expected instance of Polygon class + + Steps + 1. Check Polygon instance returned by _as_shapely_polygon method + """ + rectangle = self.horizontal_rectangle() + shapely_polygon = rectangle._as_shapely_polygon() + assert shapely_polygon.__class__ == Polygon + assert shapely_polygon.bounds == (0.1, 0.0, 0.4, 0.2) + assert shapely_polygon.area == 0.06000000000000001 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_generate_full_box(self): + """ + Description: + Check Rectangle generate_full_box method + + Expected results: + Test passes if generate_full_box method returns instance of Rectangle class with coordinates + (x1=0.0, y1=0.0, x2=1.0, y2=1.0) + + Steps + 1. Check generate_full_box method for Rectangle instance + """ + full_box = Rectangle.generate_full_box() + assert full_box.type == ShapeType.RECTANGLE + assert full_box.x1 == full_box.y1 == 0.0 + assert full_box.x2 == full_box.y2 == 1.0 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_is_full_box(self): + """ + Description: + Check Rectangle is_full_box method + + Input data: + Rectangle class initiation parameters + + Expected results: + Test passes if is_full_box method return expected bool value + + Steps + 1. Check positive scenario for is_full_box method: x1=y1=0, x2=y2=1 + 2. Check negative scenarios for is_full_box method + """ + full_box_rectangle_params = {"x1": 0.0, "y1": 0.0, "x2": 1.0, "y2": 1.0} + # Positive scenario for Rectangle instance + full_box_rectangle = Rectangle(**full_box_rectangle_params) + assert Rectangle.is_full_box(full_box_rectangle) + # Negative scenarios for Rectangle instance + # Generating all scenarios when Rectangle object not a full_box + keys_list = ["x1", "y1", "x2", "y2"] + parameter_combinations = [] + for i in range(1, len(keys_list) + 1): + parameter_combinations.append(list(itertools.combinations(keys_list, i))) + # In each of scenario creating a copy of full_Box rectangle parameters and replacing to values from prepared + # dictionary + not_full_box_values_dict = {"x1": 0.01, "y1": 0.01, "x2": 0.9, "y2": 0.9} + for combination in parameter_combinations: + for scenario in combination: + not_full_box_params = dict(full_box_rectangle_params) + for key in scenario: + not_full_box_params[key] = not_full_box_values_dict.get(key) + not_full_box_rectangle = Rectangle(**not_full_box_params) + assert not Rectangle.is_full_box(not_full_box_rectangle), ( + f"Expected False returned by is_full_box method for rectangle with parameters " + f"{not_full_box_params}" + ) + # Negative scenario for not Rectangle class instance + assert not Rectangle.is_full_box(str) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_crop_numpy_array(self): + """ + Description: + Check Rectangle crop_numpy_array method + + Input data: + Rectangle class initiation parameters + + Expected results: + Test passes if crop_numpy_array method return expected cropped array + Steps + 1. Check crop_numpy_array method for Rectangle with parameters less than 0 + 2. Check crop_numpy_array method for Rectangle with parameters range from 0 to 1 + 3. Check crop_numpy_array method for Rectangle with parameters more than 1 + """ + image_height = image_width = 128 + numpy_image_array = np.random.uniform(low=0.0, high=255.0, size=(image_height, image_width, 3)) + scenarios = [ + { + "input_params": {"x1": -0.2, "x2": -0.1, "y1": -0.3, "y2": -0.2}, + "cropped_expected": {"x1": 0, "y1": 0, "x2": 0, "y2": 0}, + }, + { + "input_params": {"x1": 0.2, "x2": 0.3, "y1": 0.4, "y2": 0.8}, + "cropped_expected": {"x1": 26, "y1": 51, "x2": 38, "y2": 102}, + }, + { + "input_params": {"x1": 1.1, "x2": 1.3, "y1": 1.1, "y2": 1.5}, + "cropped_expected": {"x1": 141, "y1": 141, "x2": 166, "y2": 192}, + }, + ] + for rectangle_parameters in scenarios: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Rectangle coordinates") + rectangle = Rectangle(**rectangle_parameters.get("input_params")) + expected_output = rectangle_parameters.get("cropped_expected") + actual_cropped_image_array = rectangle.crop_numpy_array(numpy_image_array) + expected_image_array = numpy_image_array[ + expected_output.get("y1") : expected_output.get("y2"), + expected_output.get("x1") : expected_output.get("x2"), + ::, + ] + assert actual_cropped_image_array.shape[2] == 3 + try: + assert (expected_image_array == actual_cropped_image_array).all() + except AttributeError: + raise AssertionError("Unequal expected and cropped image arrays") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_width(self): + """ + Description: + Check Rectangle width method + + Input data: + Instances of Rectangle class + + Expected results: + Test passes if width method returns expected value of Rectangle width + + Steps + 1. Check width method for Rectangle instances + """ + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Rectangle coordinates") + negative_x1_rectangle = Rectangle(x1=-0.3, y1=0.2, x2=0.7, y2=0.5) + for rectangle, expected_width in [ + (self.horizontal_rectangle(), 0.30000000000000004), + (self.vertical_rectangle(), 0.19999999999999998), + (self.square(), 0.19999999999999998), + (negative_x1_rectangle, 1.0), + ]: + assert rectangle.width == expected_width + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_height(self): + """ + Description: + Check Rectangle height method + + Input data: + Instances of Rectangle class + + Expected results: + Test passes if height method returns expected value of Rectangle height + + Steps + 1. Check height method for Rectangle instances + """ + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Rectangle coordinates") + negative_y1_rectangle = Rectangle(x1=0.3, y1=-0.4, x2=0.7, y2=0.5) + for rectangle, expected_height in [ + (self.horizontal_rectangle(), 0.2), + (self.vertical_rectangle(), 0.30000000000000004), + (self.square(), 0.19999999999999998), + (negative_y1_rectangle, 0.9), + ]: + assert rectangle.height == expected_height + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_diagonal(self): + """ + Description: + Check Rectangle diagonal method + + Input data: + Instances of Rectangle class + + Expected results: + Test passes if diagonal method returns expected value of Rectangle diagonal + + Steps + 1. Check diagonal method for Rectangle instance + """ + for rectangle, expected_diagonal in [ + (self.horizontal_rectangle(), 0.36055512754639896), + (self.vertical_rectangle(), 0.36055512754639896), + (self.square(), 0.282842712474619), + ]: + np.testing.assert_approx_equal(rectangle.diagonal, expected_diagonal) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_get_area(self): + """ + Description: + Check Rectangle get_area method + + Input data: + Instances of Rectangle class + + Expected results: + Test passes if get_area method returns expected value of Rectangle diagonal + + Steps + 1. Check get_area method for Rectangle instance + """ + for rectangle, expected_area in [ + (self.horizontal_rectangle(), 0.06000000000000001), + (self.vertical_rectangle(), 0.060000000000000005), + (self.square(), 0.039999999999999994), + ]: + assert rectangle.get_area() == expected_area diff --git a/tests/unit/api/entities/shapes/test_shape.py b/tests/unit/api/entities/shapes/test_shape.py new file mode 100644 index 00000000000..6a975f51d22 --- /dev/null +++ b/tests/unit/api/entities/shapes/test_shape.py @@ -0,0 +1,362 @@ +# Copyright (C) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import itertools +import warnings +from datetime import datetime + +import pytest + +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.shapes.shape import ( + GeometryException, + Shape, + ShapeEntity, + ShapeType, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestShapeType: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_shapetype(self): + """ + Description: + Check ShapeType class length returns expected value + + Expected results: + Test passes if ShapeType enum class length equal expected value + """ + assert len(ShapeType) == 3 + assert ShapeType.ELLIPSE.value == 1 + assert ShapeType.RECTANGLE.value == 2 + assert ShapeType.POLYGON.value == 3 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestShapeEntity: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_shape_entity_not_implemented_methods(self): + """ + Description: + Check not implemented methods of ShapeEntity class + + Expected results: + Test passes if NotImplementedError exception raises when using not implemented methods on ShapeEntity instance + """ + rectangle_entity = Rectangle(x1=0.2, y1=0.2, x2=0.6, y2=0.7) + ellipse_entity = Ellipse(x1=0.4, y1=0.1, x2=0.9, y2=0.8) + polygon_entity = Polygon( + [ + Point(0.3, 0.4), + Point(0.3, 0.7), + Point(0.5, 0.75), + Point(0.8, 0.7), + Point(0.8, 0.4), + ] + ) + for shape in [rectangle_entity, ellipse_entity, polygon_entity]: + with pytest.raises(NotImplementedError): + ShapeEntity.get_area(shape) + with pytest.raises(NotImplementedError): + ShapeEntity.intersects(shape, shape) + with pytest.raises(NotImplementedError): + ShapeEntity.contains_center(shape, shape) + with pytest.raises(NotImplementedError): + ShapeEntity.normalize_wrt_roi_shape(shape, rectangle_entity) + with pytest.raises(NotImplementedError): + ShapeEntity.denormalize_wrt_roi_shape(shape, rectangle_entity) + with pytest.raises(NotImplementedError): + ShapeEntity._as_shapely_polygon(shape) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestShape: + @staticmethod + def fully_covering_rectangle() -> Rectangle: + return Rectangle.generate_full_box() + + @staticmethod + def fully_covering_ellipse() -> Ellipse: + return Ellipse(x1=0.0, y1=0.0, x2=1.0, y2=1.0) + + @staticmethod + def fully_covering_polygon() -> Polygon: + return Polygon( + [ + Point(0.0, 0.1), + Point(0.0, 0.9), + Point(0.5, 1.0), + Point(1.0, 0.9), + Point(1.0, 0.0), + Point(0.0, 0.1), + ] + ) + + def rectangle(self) -> Rectangle: + return Rectangle(x1=0.2, y1=0.2, x2=0.6, y2=0.7) + + def ellipse(self) -> Ellipse: + return Ellipse(x1=0.4, y1=0.1, x2=0.9, y2=0.8) + + def polygon(self) -> Polygon: + return Polygon( + [ + Point(0.3, 0.4), + Point(0.3, 0.7), + Point(0.5, 0.75), + Point(0.8, 0.7), + Point(0.8, 0.4), + Point(0.3, 0.4), + ], + ) + + @staticmethod + def not_inscribed_rectangle() -> Rectangle: + return Rectangle(x1=0.0, y1=0.0, x2=0.01, y2=0.01) + + @staticmethod + def not_inscribed_ellipse() -> Ellipse: + return Ellipse(x1=0.0, y1=0.0, x2=0.01, y2=0.01) + + @staticmethod + def not_inscribed_polygon() -> Polygon: + return Polygon( + [ + Point(0.0, 0.0), + Point(0.0, 0.01), + Point(0.01, 0.02), + Point(0.02, 0.01), + Point(0.02, 0.0), + Point(0.0, 0.0), + ] + ) + + @staticmethod + def base_self_intersect_polygon() -> Polygon: + return Polygon( + [ + Point(0.3, 0.3), + Point(0.4, 0.3), + Point(0.3, 0.3), + Point(0.3, 0.2), + Point(0.3, 1), + Point(0.2, 0.2), + ] + ) + + @staticmethod + def other_self_intersect_polygon() -> Polygon: + return Polygon( + [ + Point(0.3, 0.2), + Point(0.2, 0.3), + Point(0.3, 0.1), + Point(0.3, 0.2), + Point(0, 0.2), + Point(0, 4), + ] + ) + + @staticmethod + def lower_side_intersect_shapes() -> list: + return [ + Rectangle(x1=0.2, y1=0.1, x2=0.5, y2=0.4), + Polygon( + [ + Point(0.35, 0.1), + Point(0.2, 0.2), + Point(0.2, 0.4), + Point(0.5, 0.4), + Point(0.5, 0.2), + Point(0.35, 0.1), + ] + ), + ] + + @staticmethod + def upper_side_intersect_shapes() -> list: + return [ + Rectangle(x1=0.2, y1=0.4, x2=0.5, y2=0.7), + Polygon( + [ + Point(0.35, 0.7), + Point(0.2, 0.6), + Point(0.2, 0.4), + Point(0.5, 0.4), + Point(0.5, 0.6), + Point(0.35, 0.7), + ] + ), + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_shape_get_area_method(self): + """ + Description: + Check get_area not implemented method of Shape class + + Expected results: + Test passes if NotImplementedError exception raised when using get_area method on Shape instance + """ + for shape in [self.rectangle(), self.ellipse(), self.polygon()]: + with pytest.raises(NotImplementedError): + Shape.get_area(shape) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_shape_magic_methods(self): + """ + Description: + Check __repr__ and __hash__ methods for Shape class instance + + Expected results: + Test passes if __repr__ and __hash__ method return expected values + + Steps + 1. Check that __repr__ method returns expected value + 2. Check that __hash__ method returns expected value + """ + test_rectangle = Rectangle(0.0, 0.0, 1.0, 1.0, modification_date=datetime(year=2021, month=11, day=23)) + assert Shape.__repr__(test_rectangle) == "Shape with modification date:('2021-11-23 00:00:00')" + assert Shape.__hash__(test_rectangle) == hash(str(test_rectangle)) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_shape_intersects(self): + """ + Description: + Check Shape intersects method for Rectangle, Ellipse and Polygon objects + + Expected results: + Test passes if intersects method returns expected values + + Steps + 1. Check intersects method when Shapes intersect completely + 2. Check intersects method when Shapes intersect in several points + 3. Check intersects method when Shapes intersect by one side + 4. Check intersects method when Shapes not intersect + 5. Check GeometryException exception raised with incorrect parameters for intersect method + """ + inscribed_shapes_list = [self.rectangle(), self.ellipse(), self.polygon()] + # Check when Shapes intersect fully + for full_element in [ + self.fully_covering_rectangle(), + self.fully_covering_ellipse(), + self.fully_covering_polygon(), + ]: + for inscribed in inscribed_shapes_list: + assert full_element.intersects(inscribed) + assert inscribed.intersects(full_element) + # Check when Shapes intersect in several points + for shape, other_shape in list(itertools.combinations(inscribed_shapes_list, 2)): + assert shape.intersects(other_shape) + assert other_shape.intersects(shape) + # Check when Shapes intersect by one side + for upper_shape in self.upper_side_intersect_shapes(): + for lower_shape in self.lower_side_intersect_shapes(): + assert lower_shape.intersects(upper_shape) + # Check when Shapes not intersect + for shape in inscribed_shapes_list: + for not_inscribed_shape in ( + self.not_inscribed_rectangle(), + self.not_inscribed_ellipse(), + self.not_inscribed_polygon(), + ): + assert not shape.intersects(not_inscribed_shape) + assert not not_inscribed_shape.intersects(shape) + # Checking GeometryException exception raised + with pytest.raises(GeometryException): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Polygon coordinates") + self.base_self_intersect_polygon().intersects(self.other_self_intersect_polygon()) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_shape_contains_center(self): + """ + Description: + Check Shape contains_center method for Rectangle, Ellipse and Polygon objects + + Expected results: + Test passes if contains_center method returns expected values + + Steps + 1. Check contains_center method when a Polygon, Rectangle and Ellipse fall within a Rectangle + 2. Check contains_center method when a Polygon, Rectangle and Ellipse fall outside a Rectangle + """ + rectangle_full = self.fully_covering_rectangle() + shapes_inside = [self.polygon(), self.ellipse(), self.rectangle()] + + rectangle_part = self.rectangle() + shapes_outside = [ + self.not_inscribed_polygon(), + self.not_inscribed_ellipse(), + self.not_inscribed_rectangle(), + ] + + for shape_inside in shapes_inside: + assert rectangle_full.contains_center(shape_inside) + for shape_outside in shapes_outside: + assert not rectangle_part.contains_center(shape_outside) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_validate_coordinates(self): + """ + Description: + Check Shape validate_coordinates method for Rectangle, Ellipse and Polygon objects + + Expected results: + Test passes if validate_coordinates method returns expected values + + Steps + 1. Check validate_coordinates method for Shapes with 0.0<=x,y<=1.0 + 2. Check validate_coordinates method for Shapes with x<0.0 + 3. Check validate_coordinates method for Shapes with x>1.0 + 4. Check validate_coordinates method for Shapes with y<0.0 + 5. Check validate_coordinates method for Shapes with y>1.0 + 6. Check validate_coordinates method for Shapes with x,y<0.0 + 7. Check validate_coordinates method for Shapes with x,y>1.0 + 8. Check validate_coordinates method for Shapes with x>1.0, y<0.0 + 9. Check validate_coordinates method for Shapes with x<1.0, y>1.0 + """ + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", r".* coordinates") + for shape in [self.rectangle(), self.ellipse(), self.polygon()]: + assert shape._validate_coordinates(x=0.0, y=0.0) + assert shape._validate_coordinates(x=1.0, y=1.0) + assert shape._validate_coordinates(x=0.2, y=0.3) + assert not shape._validate_coordinates(x=-0.1, y=0.0) + assert not shape._validate_coordinates(x=1.1, y=1.0) + assert not shape._validate_coordinates(x=0.2, y=-0.3) + assert not shape._validate_coordinates(x=0.2, y=1.3) + assert not shape._validate_coordinates(x=-0.1, y=-0.2) + assert not shape._validate_coordinates(x=1.1, y=1.2) + assert not shape._validate_coordinates(x=1.2, y=-0.3) + assert not shape._validate_coordinates(x=-1.2, y=1.3) diff --git a/tests/unit/api/entities/test_annotation.py b/tests/unit/api/entities/test_annotation.py new file mode 100644 index 00000000000..290b31b1b97 --- /dev/null +++ b/tests/unit/api/entities/test_annotation.py @@ -0,0 +1,598 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import datetime +from typing import List + +import pytest + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, + NullAnnotationSceneEntity, +) +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.utils.time_utils import now +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestAnnotation: + + rectangle = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5) + labels: List[ScoredLabel] = [] + annotation = Annotation(shape=rectangle, labels=labels) + + car = LabelEntity( + id=ID(123456789), + name="car", + domain=Domain.DETECTION, + color=Color(red=16, green=15, blue=56, alpha=255), + is_empty=True, + ) + person = LabelEntity( + id=ID(987654321), + name="person", + domain=Domain.DETECTION, + color=Color(red=11, green=18, blue=38, alpha=200), + is_empty=False, + ) + car_label = ScoredLabel(car) + person_label = ScoredLabel(person) + labels2 = [car_label, person_label] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_default_property(self): + """ + Description: + Check that Annotation can correctly return default property value + + Input data: + Annotation class + + Expected results: + Test passes if the Annotation return correct values + + Steps + 1. Create Annotation instances + 2. Check default values + """ + + annotation = self.annotation + + assert type(annotation.id_) == ID + assert annotation.id_ is not None + assert str(annotation.shape) == "Rectangle(x=0.5, y=0.0, width=0.5, height=0.5)" + assert annotation.get_labels() == [] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_setters(self): + """ + Description: + Check that Annotation can correctly return modified property value + + Input data: + Annotation class + + Expected results: + Test passes if the Annotation return correct values + + Steps + 1. Create Annotation instances + 2. Set another values + 3. Check changed values + """ + + annotation = self.annotation + ellipse = Ellipse(x1=0.5, y1=0.1, x2=0.8, y2=0.3) + annotation.shape = ellipse + annotation.id_ = ID(123456789) + + assert annotation.id_ == ID(123456789) + assert annotation.shape == ellipse + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_magic_methods(self): + """ + Description: + Check Annotation __repr__, __eq__ methods + + Input data: + Initialized instance of Annotation + + Expected results: + Test passes if Annotation magic methods returns correct values + + Steps + 1. Create Annotation instances + 2. Check returning value of magic methods + """ + + annotation = self.annotation + other_annotation = self.annotation + + point1 = Point(0.3, 0.1) + point2 = Point(0.8, 0.3) + point3 = Point(0.6, 0.2) + points = [point1, point2, point3] + third_annotation = Annotation(shape=Polygon(points=points), labels=self.labels) + + assert repr(annotation) == "Annotation(shape=Ellipse(x1=0.5, y1=0.1, x2=0.8, y2=0.3), labels=[], id=123456789)" + assert annotation == other_annotation + assert annotation != third_annotation + assert annotation != str + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_get_labels(self): + """ + Description: + Check Annotation get_labels method + + Input data: + Initialized instance of Annotation + + Expected results: + Test passes if Annotation get_labels method returns correct values + + Steps + 1. Create Annotation instances + 2. Check returning value of get_labels method + 3. Check returning value of get_labels method with include_empty=True + """ + annotation = Annotation(shape=self.rectangle, labels=self.labels2) + + assert "[ScoredLabel(987654321, name=person, probability=0.0, domain=DETECTION," in str(annotation.get_labels()) + assert "color=Color(red=11, green=18, blue=38, alpha=200), hotkey=" in str(annotation.get_labels()) + assert ", label_source=LabelSource(user_id='', model_id=ID(), model_storage_id=ID()))]" in str( + annotation.get_labels() + ) + + assert "[ScoredLabel(123456789, name=car" in str(annotation.get_labels(include_empty=True)) + assert ", probability=0.0, domain=DETECTION," in str(annotation.get_labels(include_empty=True)) + assert "color=Color(red=16, green=15," in str(annotation.get_labels(include_empty=True)) + assert "blue=56, alpha=255), hotkey=," in str(annotation.get_labels(include_empty=True)) + assert "label_source=LabelSource(user_id='', model_id=ID(), model_storage_id=ID()))," in str( + annotation.get_labels(include_empty=True) + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_get_label_ids(self): + """ + Description: + Check Annotation get_label_ids method + + Input data: + Initialized instance of Annotation + + Expected results: + Test passes if Annotation get_label_ids method returns correct values + + Steps + 1. Create Annotation instances + 2. Check returning value of get_label_ids method + 3. Check returning value of get_label_ids method with include_empty=True + """ + + annotation = Annotation(shape=self.rectangle, labels=self.labels2) + + assert annotation.get_label_ids() == {ID(987654321)} + assert annotation.get_label_ids(include_empty=True) == { + ID(987654321), + ID(123456789), + } + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_append_label(self): + """ + Description: + Check Annotation append_label method + + Input data: + Initialized instance of Annotation + + Expected results: + Test passes if Annotation append_label method correct appending label + + Steps + 1. Create Annotation instances + 2. Append label + 3. Check labels + """ + + annotation = self.annotation + + annotation.append_label(label=self.car_label) + assert annotation.get_labels() == [] # car_label is empty + + annotation.append_label(label=self.person_label) + assert "name=person" in str(annotation.get_labels()) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_set_labels(self): + """ + Description: + Check Annotation set_labels method + + Input data: + Initialized instance of Annotation + + Expected results: + Test passes if Annotation set_labels method correct setting label + + Steps + 1. Create Annotation instances + 2. Set labels + 3. Check labels + """ + + annotation = self.annotation + assert annotation.get_labels() != [] + + annotation.set_labels(labels=[]) + assert annotation.get_labels() == [] + + annotation.set_labels(labels=self.labels2) + assert "name=person" in str(annotation.get_labels()) + assert "name=car" not in str(annotation.get_labels()) # car_label is empty + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestAnnotationSceneKind: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_scene_kind(self): + """ + Description: + Check that AnnotationSceneKind Enum lenght + + Input data: + AnnotationSceneKind class + + Expected results: + Test passes if the AnnotationSceneKind return correct values + + Steps + 1. Create AnnotationSceneKind instances + 2. Check Enum lenght + """ + + annotation_scene_kind = AnnotationSceneKind + assert len(annotation_scene_kind) == 6 + + assert str(annotation_scene_kind(0)) == "NONE" + assert str(annotation_scene_kind(1)) == "ANNOTATION" + assert str(annotation_scene_kind(2)) == "PREDICTION" + assert str(annotation_scene_kind(3)) == "EVALUATION" + assert str(annotation_scene_kind(4)) == "INTERMEDIATE" + assert str(annotation_scene_kind(5)) == "TASK_PREDICTION" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestAnnotationSceneEntity: + + creation_date = now() + labels: List[ScoredLabel] = [] + rectangle = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=0.5) + annotation = Annotation(shape=rectangle, labels=labels) + + point1 = Point(0.3, 0.1) + point2 = Point(0.8, 0.3) + point3 = Point(0.6, 0.2) + points = [point1, point2, point3] + polygon = Polygon(points=points) + annotation2 = Annotation(shape=polygon, labels=labels) + + annotations = [annotation, annotation2] + + annotation_scene_entity = AnnotationSceneEntity(annotations=annotations, kind=AnnotationSceneKind.ANNOTATION) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_scene_entity_default_value(self): + """ + Description: + Check that AnnotationSceneEntity default values + + Input data: + AnnotationSceneEntity class + + Expected results: + Test passes if the AnnotationSceneEntity return correct values + + Steps + 1. Create AnnotationSceneEntity instances + 2. Check default values + """ + + annotation_scene_entity = self.annotation_scene_entity + + assert annotation_scene_entity.id_ == ID() + assert annotation_scene_entity.kind == AnnotationSceneKind.ANNOTATION + assert annotation_scene_entity.editor_name == "" + assert type(annotation_scene_entity.creation_date) == datetime.datetime + assert "Annotation(shape=Rectangle" in str(annotation_scene_entity.annotations) + assert "Annotation(shape=Polygon" in str(annotation_scene_entity.annotations) + assert annotation_scene_entity.shapes == [self.rectangle, self.polygon] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_scene_entity_setters(self): + """ + Description: + Check that AnnotationSceneEntity can correctly return modified property value + + Input data: + Annotation class + + Expected results: + Test passes if the AnnotationSceneEntity return correct values + + Steps + 1. Create AnnotationSceneEntity instances + 2. Set another values + 3. Check changed values + """ + + annotation_scene_entity = self.annotation_scene_entity + + creation_date = self.creation_date + annotation_scene_entity.id_ = ID(123456789) + annotation_scene_entity.kind = AnnotationSceneKind.PREDICTION + annotation_scene_entity.editor_name = "editor" + annotation_scene_entity.creation_date = creation_date + annotation_scene_entity.annotations = self.annotation + + assert annotation_scene_entity.id_ == ID(123456789) + assert annotation_scene_entity.kind == AnnotationSceneKind.PREDICTION + assert annotation_scene_entity.editor_name == "editor" + assert annotation_scene_entity.creation_date == creation_date + assert annotation_scene_entity.annotations == self.annotation + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_scene_entity_magic_methods(self): + """ + Description: + Check Annotation __repr__ method + + Input data: + Initialized instance of AnnotationSceneEntity + + Expected results: + Test passes if AnnotationSceneEntity magic method returns correct values + + Steps + 1. Create AnnotationSceneEntity instances + 2. Check returning value of magic method + """ + + annotation_scene_entity = self.annotation_scene_entity + + annotation_scene_entity_repr = [ + f"{annotation_scene_entity.__class__.__name__}(" + f"annotations={annotation_scene_entity.annotations}, " + f"kind={annotation_scene_entity.kind}, " + f"editor={annotation_scene_entity.editor_name}, " + f"creation_date={annotation_scene_entity.creation_date}, " + f"id={annotation_scene_entity.id_})" + ] + + for i in annotation_scene_entity_repr: + assert i in repr(annotation_scene_entity) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_scene_entity_contains_any(self): + """ + Description: + Check Annotation contains_any method + + Input data: + Initialized instance of AnnotationSceneEntity + + Expected results: + Test passes if AnnotationSceneEntity contains_any method returns correct values + + Steps + 1. Create AnnotationSceneEntity instances + 2. Check returning value of contains_any method + """ + + annotation_scene_entity = self.annotation_scene_entity + annotation_scene_entity.annotations = self.annotations + + car = LabelEntity(name="car", domain=Domain.DETECTION, is_empty=True) + person = LabelEntity(name="person", domain=Domain.DETECTION) + tree = LabelEntity(name="tree", domain=Domain.DETECTION) + car_label = ScoredLabel(car) + person_label = ScoredLabel(person) + tree_label = ScoredLabel(tree) + labels = [car_label] + labels2 = [car_label, person_label] + + annotation = Annotation(shape=self.rectangle, labels=labels2) + annotations = [annotation] + annotation_scene_entity2 = AnnotationSceneEntity(annotations=annotations, kind=AnnotationSceneKind.ANNOTATION) + + assert annotation_scene_entity.contains_any(labels=labels) is False + assert annotation_scene_entity2.contains_any(labels=labels2) is True + assert annotation_scene_entity2.contains_any(labels=[tree_label]) is False + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_scene_entity_append_annotation(self): + """ + Description: + Check Annotation append_annotation method + + Input data: + Initialized instance of AnnotationSceneEntity + + Expected results: + Test passes if AnnotationSceneEntity append_annotation method returns correct values + + Steps + 1. Create AnnotationSceneEntity instances + 2. Check returning value of append_annotation method + """ + + annotation_scene_entity = self.annotation_scene_entity + + tree = LabelEntity(name="tree", domain=Domain.DETECTION) + tree_label = ScoredLabel(tree) + labels = [tree_label] + annotation = Annotation(shape=self.rectangle, labels=labels) + + assert len(annotation_scene_entity.annotations) == 2 + + annotation_scene_entity.append_annotation(annotation) + assert len(annotation_scene_entity.annotations) == 3 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_scene_entity_append_annotations(self): + """ + Description: + Check Annotation append_annotations method + + Input data: + Initialized instance of AnnotationSceneEntity + + Expected results: + Test passes if AnnotationSceneEntity append_annotations method returns correct values + + Steps + 1. Create AnnotationSceneEntity instances + 2. Check returning value of append_annotations method + """ + + annotation_scene_entity = self.annotation_scene_entity + + annotation_scene_entity.append_annotations(self.annotations) + assert len(annotation_scene_entity.annotations) == 6 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_scene_entity_get_labels(self): + """ + Description: + Check Annotation get_labels method + + Input data: + Initialized instance of AnnotationSceneEntity + + Expected results: + Test passes if AnnotationSceneEntity get_labels method returns correct values + + Steps + 1. Create AnnotationSceneEntity instances + 2. Check returning value of get_labels method + """ + + annotation_scene_entity = self.annotation_scene_entity + + assert len(annotation_scene_entity.get_labels()) == 1 + assert "name=tree" in str(annotation_scene_entity.get_labels()) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_annotation_scene_entity_get_label_ids(self): + """ + Description: + Check Annotation get_label_ids method + + Input data: + Initialized instance of AnnotationSceneEntity + + Expected results: + Test passes if AnnotationSceneEntity get_label_ids method returns correct values + + Steps + 1. Create AnnotationSceneEntity instances + 2. Check returning value of get_label_ids method + """ + + annotation_scene_entity = self.annotation_scene_entity + + assert annotation_scene_entity.get_label_ids() == {ID()} + + bus = LabelEntity(id=ID(123456789), name="bus", domain=Domain.DETECTION) + bus_label = ScoredLabel(bus) + labels = [bus_label] + annotation = Annotation(shape=self.rectangle, labels=labels) + annotation_scene_entity.append_annotation(annotation) + + assert annotation_scene_entity.get_label_ids() == {ID(), ID(123456789)} + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestNullAnnotationSceneEntity: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_null_annotation_scene_entity(self): + """ + Description: + Check that NullAnnotationSceneEntity + + Input data: + NullAnnotationSceneEntity class + + Expected results: + Test passes if the NullAnnotationSceneEntity return correct values + + Steps + 1. Create NullAnnotationSceneEntity instances + 2. Check default values + """ + + null_annotation = NullAnnotationSceneEntity() + + assert null_annotation.id_ == ID() + assert null_annotation.kind == AnnotationSceneKind.NONE + assert null_annotation.editor_name == "" + assert type(null_annotation.creation_date) == datetime.datetime + assert null_annotation.annotations == [] + assert repr(null_annotation) == "NullAnnotationSceneEntity()" diff --git a/tests/unit/api/entities/test_color.py b/tests/unit/api/entities/test_color.py new file mode 100644 index 00000000000..7f9db499dac --- /dev/null +++ b/tests/unit/api/entities/test_color.py @@ -0,0 +1,89 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import pytest + +from otx.api.entities.color import Color, ColorEntity +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + +red = 40 +red_hex = "28" +green = 210 +green_hex = "d2" +blue = 43 +blue_hex = "2b" +alpha = 255 +alpha_hex = "ff" +color_hex = f"{red_hex}{green_hex}{blue_hex}" + +color = Color.from_hex_str(color_hex) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestColor: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_color(self): + """ + Description: + Check that Color can correctly return the value + + Expected results: + Test passes if the results match + """ + + assert color == Color(red=red, green=green, blue=blue, alpha=alpha) + assert color.hex_str == f"#{color_hex}{alpha_hex}" + assert type(color.random()) == Color + assert color.rgb_tuple == (red, green, blue) + assert color.bgr_tuple == (blue, green, red) + assert color != ColorEntity + assert repr(color) == f"Color(red={red}, green={green}, blue={blue}, alpha={alpha})" + assert color.red == red + assert color.green == green + assert color.blue == blue + + color.red = 68 + color.green = 54 + color.blue = 32 + color.alpha = 0 + assert color.hex_str == "#44362000" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestColorEntity: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_color_entity(self): + """ + Description: + Check that ColorEntity raises some exceptions + + Expected results: + Test passes if the NotImplementedError is raised + """ + + color_entity = ColorEntity + + with pytest.raises(NotImplementedError): + color_entity.hex_str.__get__(property) + + with pytest.raises(NotImplementedError): + color_entity.random() + + with pytest.raises(NotImplementedError): + color_entity.from_hex_str(color_hex) diff --git a/tests/unit/api/entities/test_coordinate.py b/tests/unit/api/entities/test_coordinate.py new file mode 100644 index 00000000000..d05d5173649 --- /dev/null +++ b/tests/unit/api/entities/test_coordinate.py @@ -0,0 +1,89 @@ +# Copyright (C) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +import pytest + +from otx.api.entities.coordinate import Coordinate +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestCoordinate: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_coordinate(self): + """ + Description: + To test Coordinate class + + Input data: + Three coordinate instances + + Expected results: + 1. It raises TypeError in case of attempt of instantiation with wrong number of parameters + 2. Fields of instances initialized with correct values + 3. repr method returns correct strings then used against each instance + 4. hash method works as expected + 5. as_tuple method works as expected + 6. as_int_tuple method works as expected + 7. '==' method works as expected + + Steps + 1. Attempt to create Coordinate with wrong parameters numbers + 2. Create three Coordinate: + two of them with similar set of init values, third one with different one. + 3. Check repr method + 4. Check hash method + 5. Check as_tuple() method + 6. Check as_int_tuple() method + 7. Check __eq__ method + """ + with pytest.raises(TypeError): + Coordinate() + + with pytest.raises(TypeError): + Coordinate(1) + + coord_a = Coordinate(x=0, y=0) + coord_b = Coordinate(x=0.0, y=0.0) + coord_c = Coordinate(x=1, y=1) + + assert isinstance(coord_a, Coordinate) + assert coord_a.x == 0 + assert coord_a.y == 0 + assert repr(coord_a) == "Coordinate(x=0, y=0)" + assert hash(coord_a) == coord_a.__hash__() + assert coord_a.as_tuple() == (0, 0) + assert coord_a.as_int_tuple() == (0, 0) + + assert isinstance(coord_b, Coordinate) + assert coord_b.x == 0.0 + assert coord_b.y == 0.0 + assert repr(coord_b) == "Coordinate(x=0.0, y=0.0)" + assert hash(coord_b) == coord_b.__hash__() + assert coord_b.as_tuple() == (0.0, 0.0) + assert coord_b.as_int_tuple() == (0, 0) + + assert isinstance(coord_c, Coordinate) + assert coord_c.x == 1 + assert coord_c.y == 1 + assert repr(coord_c) == "Coordinate(x=1, y=1)" + assert hash(coord_c) == coord_c.__hash__() + assert coord_c.as_tuple() == (1, 1) + assert coord_c.as_int_tuple() == (1, 1) + + assert coord_a == coord_b != coord_c diff --git a/tests/unit/api/entities/test_dataset_item.py b/tests/unit/api/entities/test_dataset_item.py new file mode 100644 index 00000000000..3195d345dcc --- /dev/null +++ b/tests/unit/api/entities/test_dataset_item.py @@ -0,0 +1,994 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import datetime +from copy import deepcopy +from typing import List, Union + +import numpy as np +import pytest + +from otx.api.configuration import ConfigurableParameters +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.color import Color +from otx.api.entities.dataset_item import DatasetItemEntity, DatasetItemEntityWithID +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.metadata import MetadataItemEntity +from otx.api.entities.model import ModelConfiguration, ModelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset +from otx.api.entities.tensor import TensorEntity +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +class DatasetItemParameters: + @staticmethod + def generate_random_image() -> Image: + image = Image(data=np.random.randint(low=0, high=255, size=(10, 16, 3))) + return image + + @staticmethod + def labels() -> List[LabelEntity]: + creation_date = datetime.datetime(year=2021, month=12, day=9) + detection_label = LabelEntity( + name="Label for Detection", + domain=Domain.DETECTION, + color=Color(red=100, green=200, blue=150), + creation_date=creation_date, + id=ID("detection_label"), + ) + segmentation_label = LabelEntity( + name="Label for Segmentation", + domain=Domain.DETECTION, + color=Color(red=50, green=80, blue=200), + creation_date=creation_date, + is_empty=True, + id=ID("segmentation_label"), + ) + return [detection_label, segmentation_label] + + def annotations(self) -> List[Annotation]: + labels = self.labels() + rectangle = Rectangle(x1=0.2, y1=0.2, x2=0.6, y2=0.7) + other_rectangle = Rectangle(x1=0.3, y1=0.2, x2=0.9, y2=0.9) + detection_annotation = Annotation( + shape=rectangle, + labels=[ScoredLabel(label=labels[0])], + id=ID("detection_annotation_1"), + ) + segmentation_annotation = Annotation( + shape=other_rectangle, + labels=[ScoredLabel(label=labels[1])], + id=ID("segmentation_annotation_1"), + ) + return [detection_annotation, segmentation_annotation] + + @staticmethod + def roi_labels() -> List[LabelEntity]: + creation_date = datetime.datetime(year=2021, month=12, day=9) + roi_label = LabelEntity( + name="ROI label", + domain=Domain.DETECTION, + color=Color(red=40, green=180, blue=80), + creation_date=creation_date, + id=ID("roi_label_1"), + ) + other_roi_label = LabelEntity( + name="Second ROI label", + domain=Domain.SEGMENTATION, + color=Color(red=80, green=90, blue=70), + creation_date=creation_date, + is_empty=True, + id=ID("roi_label_2"), + ) + return [roi_label, other_roi_label] + + def roi_scored_labels(self) -> List[ScoredLabel]: + roi_labels = self.roi_labels() + return [ScoredLabel(roi_labels[0]), ScoredLabel(roi_labels[1])] + + def roi(self): + roi = Annotation( + shape=Rectangle( + x1=0.1, + y1=0.1, + x2=0.9, + y2=0.9, + modification_date=datetime.datetime(year=2021, month=12, day=9), + ), + labels=self.roi_scored_labels(), + id=ID("roi_annotation"), + ) + return roi + + @staticmethod + def metadata() -> List[MetadataItemEntity]: + data = TensorEntity( + name="test_metadata", + numpy=np.random.uniform(low=0.0, high=255.0, size=(10, 15, 3)), + ) + other_data = TensorEntity( + name="other_metadata", + numpy=np.random.uniform(low=0.0, high=255.0, size=(10, 15, 3)), + ) + return [MetadataItemEntity(data=data), MetadataItemEntity(data=other_data)] + + def annotations_entity(self) -> AnnotationSceneEntity: + return AnnotationSceneEntity( + annotations=self.annotations(), + kind=AnnotationSceneKind.ANNOTATION, + creation_date=datetime.datetime(year=2021, month=12, day=19), + id=ID("annotation_entity_1"), + ) + + def default_values_dataset_item(self) -> DatasetItemEntity: + return DatasetItemEntity(self.generate_random_image(), self.annotations_entity()) + + def dataset_item(self) -> DatasetItemEntity: + return DatasetItemEntity( + media=self.generate_random_image(), + annotation_scene=self.annotations_entity(), + roi=self.roi(), + metadata=self.metadata(), + subset=Subset.TESTING, + ignored_labels={self.labels()[1]}, + ) + + def dataset_item_with_id(self) -> DatasetItemEntityWithID: + return DatasetItemEntityWithID( + id_=ID("test"), + media=self.generate_random_image(), + annotation_scene=self.annotations_entity(), + roi=self.roi(), + metadata=self.metadata(), + subset=Subset.TESTING, + ignored_labels={self.labels()[1]}, + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDatasetItemEntity: + @staticmethod + def compare_denormalized_annotations(actual_annotations, expected_annotations) -> None: + assert len(actual_annotations) == len(expected_annotations) + for index in range(len(expected_annotations)): + actual_annotation = actual_annotations[index] + expected_annotation = expected_annotations[index] + # Redefining id and modification_date required because of new Annotation objects created after shape + # denormalize + actual_annotation.id_ = expected_annotation.id_ + actual_annotation.shape.modification_date = expected_annotation.shape.modification_date + assert actual_annotation == expected_annotation + + @staticmethod + def labels_to_add() -> List[LabelEntity]: + label_to_add = LabelEntity( + name="Label which will be added", + domain=Domain.DETECTION, + color=Color(red=60, green=120, blue=70), + creation_date=datetime.datetime(year=2021, month=12, day=12), + id=ID("label_to_add_1"), + ) + other_label_to_add = LabelEntity( + name="Other label to add", + domain=Domain.SEGMENTATION, + color=Color(red=80, green=70, blue=100), + creation_date=datetime.datetime(year=2021, month=12, day=11), + is_empty=True, + id=ID("label_to_add_2"), + ) + return [label_to_add, other_label_to_add] + + def annotations_to_add(self) -> List[Annotation]: + labels_to_add = self.labels_to_add() + annotation_to_add = Annotation( + shape=Rectangle(x1=0.1, y1=0.1, x2=0.7, y2=0.8), + labels=[ScoredLabel(label=labels_to_add[0])], + id=ID("added_annotation_1"), + ) + other_annotation_to_add = Annotation( + shape=Rectangle(x1=0.2, y1=0.3, x2=0.8, y2=0.9), + labels=[ScoredLabel(label=labels_to_add[1])], + id=ID("added_annotation_2"), + ) + return [annotation_to_add, other_annotation_to_add] + + @staticmethod + def metadata_item_with_model() -> MetadataItemEntity: + data = TensorEntity( + name="appended_metadata_with_model", + numpy=np.random.randint(low=0, high=255, size=(10, 15, 3)), + ) + configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="Test Header"), + label_schema=LabelSchemaEntity(), + ) + model = ModelEntity(configuration=configuration, train_dataset=DatasetEntity()) + metadata_item_with_model = MetadataItemEntity(data=data, model=model) + return metadata_item_with_model + + @staticmethod + def check_roi_equal_annotation(dataset_item: DatasetItemEntity, expected_labels: list, include_empty=False) -> None: + roi_annotation_in_scene = None + for annotation in dataset_item.annotation_scene.annotations: + if annotation == dataset_item.roi: + assert annotation.get_labels(include_empty=include_empty) == expected_labels + roi_annotation_in_scene = annotation + break + assert roi_annotation_in_scene + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_initialization(self): + """ + Description: + Check DatasetItemEntity class object initialization + + Input data: + DatasetItemEntity class objects with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if attributes of DatasetItemEntity class object are equal to expected + + Steps + 1. Check attributes of DatasetItemEntity object initialized with default optional parameters + 2. Check attributes of DatasetItemEntity object initialized with specified optional parameters + """ + media = DatasetItemParameters.generate_random_image() + annotations_scene = DatasetItemParameters().annotations_entity() + # Checking attributes of DatasetItemEntity object initialized with default optional parameters + default_values_dataset_item = DatasetItemEntity(media, annotations_scene) + assert default_values_dataset_item.media == media + assert default_values_dataset_item.annotation_scene == annotations_scene + assert not default_values_dataset_item.get_metadata() + assert default_values_dataset_item.subset == Subset.NONE + assert default_values_dataset_item.ignored_labels == set() + # Checking attributes of DatasetItemEntity object initialized with specified optional parameters + roi = DatasetItemParameters().roi() + metadata = DatasetItemParameters.metadata + subset = Subset.TESTING + ignored_labels = set(DatasetItemParameters().labels()) + specified_values_dataset_item = DatasetItemEntity( + media, annotations_scene, roi, metadata, subset, ignored_labels + ) + assert specified_values_dataset_item.media == media + assert specified_values_dataset_item.annotation_scene == annotations_scene + assert specified_values_dataset_item.roi == roi + assert specified_values_dataset_item.get_metadata() == metadata + assert specified_values_dataset_item.subset == subset + assert specified_values_dataset_item.ignored_labels == ignored_labels + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_repr(self): + """ + Description: + Check DatasetItemEntity class __repr__ method + + Input data: + DatasetItemEntity class objects with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if value returned by __repr__ method is equal to expected + + Steps + 1. Check value returned by __repr__ method for DatasetItemEntity object with default optional parameters + 2. Check value returned by __repr__ method for DatasetItemEntity object with specified optional parameters + """ + media = DatasetItemParameters.generate_random_image() + annotation_scene = DatasetItemParameters().annotations_entity() + # Checking __repr__ method for DatasetItemEntity object initialized with default optional parameters + default_values_dataset_item = DatasetItemEntity(media, annotation_scene) + generated_roi = default_values_dataset_item.roi + + assert repr(default_values_dataset_item) == ( + f"DatasetItemEntity(media=Image(with data, width=16, height=10), " + f"annotation_scene={annotation_scene}, roi={generated_roi}, " + f"subset=NONE), meta=[]" + ) + # Checking __repr__ method for DatasetItemEntity object initialized with specified optional parameters + roi = DatasetItemParameters().roi() + metadata = DatasetItemParameters.metadata() + subset = Subset.TESTING + specified_values_dataset_item = DatasetItemEntity(media, annotation_scene, roi, metadata, subset) + assert repr(specified_values_dataset_item) == ( + f"DatasetItemEntity(media=Image(with data, width=16, height=10), annotation_scene={annotation_scene}, " + f"roi={roi}, subset=TESTING), meta={metadata}" + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_roi(self): + """ + Description: + Check DatasetItemEntity class "roi" property + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if value returned by "roi" property is equal to expected + + Steps + 1. Check value returned by "roi" property for DatasetItemEntity with specified "roi" parameter + 2. Check value returned by "roi" property for DatasetItemEntity with not specified "roi" parameter + 3. Check value returned by "roi" property for DatasetItemEntity with not specified "roi" parameter but one + of annotation objects in annotation_scene is equal to full Rectangle + """ + media = DatasetItemParameters.generate_random_image() + annotations = DatasetItemParameters().annotations() + annotation_scene = DatasetItemParameters().annotations_entity() + roi = DatasetItemParameters().roi() + metadata = DatasetItemParameters.metadata() + # Checking "roi" property for DatasetItemEntity with specified "roi" parameter + specified_roi_dataset_item = DatasetItemParameters().dataset_item() + assert specified_roi_dataset_item.roi == roi + # Checking that "roi" property is equal to full_box for DatasetItemEntity with not specified "roi" parameter + non_specified_roi_dataset_item = DatasetItemEntity(media, annotation_scene, metadata=metadata) + default_roi = non_specified_roi_dataset_item.roi.shape + assert isinstance(default_roi, Rectangle) + assert Rectangle.is_full_box(default_roi) + # Checking that "roi" property will be equal to full_box for DatasetItemEntity with not specified "roi" but one + # of Annotation objects in annotation_scene is equal to full Rectangle + full_box_label = LabelEntity("Full-box label", Domain.DETECTION, id=ID("full_box_label")) + full_box_annotation = Annotation(Rectangle.generate_full_box(), [ScoredLabel(full_box_label)]) + annotations.append(full_box_annotation) + annotation_scene.annotations.append(full_box_annotation) + full_box_label_dataset_item = DatasetItemEntity(media, annotation_scene, metadata=metadata) + assert full_box_label_dataset_item.roi is full_box_annotation + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_roi_numpy(self): + """ + Description: + Check DatasetItemEntity class "roi_numpy" method + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if array returned by "roi_numpy" method is equal to expected + + Steps + 1. Check array returned by roi_numpy method with not specified "roi" parameter for DatasetItemEntity with + "roi" attribute is "None" + 2. Check array returned by roi_numpy method with Rectangle-shape "roi" parameter + 3. Check array returned by roi_numpy method with Ellipse-shape "roi" parameter + 4. Check array returned by roi_numpy method with Polygon-shape "roi" parameter + 5. Check array returned by roi_numpy method with non-specified "roi" parameter for DatasetItemEntity with "roi" + attribute + """ + media = DatasetItemParameters.generate_random_image() + annotation_scene = DatasetItemParameters().annotations_entity() + roi_label = LabelEntity("ROI label", Domain.DETECTION, id=ID("roi_label")) + dataset_item = DatasetItemEntity(media, annotation_scene) + # Checking array returned by "roi_numpy" method with non-specified "roi" parameter for DatasetItemEntity + # "roi" attribute is "None" + assert np.array_equal(dataset_item.roi_numpy(), media.numpy) + # Checking array returned by "roi_numpy" method with specified Rectangle-shape "roi" parameter + rectangle_roi = Annotation( + Rectangle(x1=0.2, y1=0.1, x2=0.8, y2=0.9), + [ScoredLabel(roi_label)], + ID("rectangle_roi"), + ) + assert np.array_equal(dataset_item.roi_numpy(rectangle_roi), media.numpy[1:9, 3:13]) + # Checking array returned by "roi_numpy" method with specified Ellipse-shape "roi" parameter + ellipse_roi = Annotation( + Ellipse(x1=0.1, y1=0.0, x2=0.9, y2=0.8), + [ScoredLabel(roi_label)], + ID("ellipse_roi"), + ) + assert np.array_equal(dataset_item.roi_numpy(ellipse_roi), media.numpy[0:8, 2:14]) + # Checking array returned by "roi_numpy" method with specified Polygon-shape "roi" parameter + polygon_roi = Annotation( + shape=Polygon( + [ + Point(0.3, 0.4), + Point(0.3, 0.7), + Point(0.5, 0.75), + Point(0.8, 0.7), + Point(0.8, 0.4), + ] + ), + labels=[], + id=ID("polygon_roi"), + ) + assert np.array_equal(dataset_item.roi_numpy(polygon_roi), media.numpy[4:8, 5:13]) + # Checking array returned by "roi_numpy" method with not specified "roi" parameter for DatasetItemEntity with + # "roi" attribute + roi_specified_dataset_item = DatasetItemEntity(media, annotation_scene, DatasetItemParameters().roi()) + roi_specified_dataset_item.roi_numpy() + assert np.array_equal(roi_specified_dataset_item.roi_numpy(), media.numpy[1:9, 2:14]) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_numpy(self): + """ + Description: + Check DatasetItemEntity class "numpy" property + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if array returned by "numpy" property is equal to array returned by "roi_numpy" method + + Steps + 1. Check array returned by "numpy" property for DatasetItemEntity with "roi" attribute is "None" + 2. Check array returned by "numpy" property for DatasetItemEntity with specified "roi" attribute + """ + # Checking array returned by numpy property for DatasetItemEntity with "roi" attribute is "None" + none_roi_dataset_item = DatasetItemParameters().default_values_dataset_item() + assert np.array_equal(none_roi_dataset_item.numpy, none_roi_dataset_item.roi_numpy()) + # Checking array returned by numpy property for DatasetItemEntity with specified "roi" attribute + roi_specified_dataset_item = DatasetItemParameters().dataset_item() + assert np.array_equal(roi_specified_dataset_item.numpy, roi_specified_dataset_item.roi_numpy()) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_width(self): + """ + Description: + Check DatasetItemEntity class "width" property + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if value returned by "width" property is equal to expected + + Steps + 1. Check value returned by "width" property for DatasetItemEntity with "roi" attribute is "None" + 2. Check value returned by "width" property for DatasetItemEntity with specified "roi" attribute + """ + # Checking value returned by "width" property for DatasetItemEntity with "roi" attribute is "None" + none_roi_dataset_item = DatasetItemParameters().default_values_dataset_item() + assert none_roi_dataset_item.width == 16 + # Checking value returned by "width" property for DatasetItemEntity with specified "roi" attribute + roi_specified_dataset_item = DatasetItemParameters().dataset_item() + assert roi_specified_dataset_item.width == 12 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_height(self): + """ + Description: + Check DatasetItemEntity class "height" property + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if value returned by "height" property is equal to expected + + Steps + 1. Check value returned by "height" property for DatasetItemEntity with "roi" attribute is "None" + 2. Check value returned by "height" property for DatasetItemEntity with specified "roi" attribute + """ + # Checking value returned by "width" property for DatasetItemEntity with None "roi" attribute + none_roi_dataset_item = DatasetItemParameters().default_values_dataset_item() + assert none_roi_dataset_item.height == 10 + # Checking value returned by "width" property for DatasetItemEntity with specified "roi" attribute + roi_specified_dataset_item = DatasetItemParameters().dataset_item() + assert roi_specified_dataset_item.height == 8 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @pytest.mark.xfail + # TODO: Fix this - https://jira.devtools.intel.com/browse/CVS-91526 + def test_dataset_item_get_annotations(self): + """ + Description: + Check DatasetItemEntity class "get_annotations" method + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if list returned by "get_annotations" method is equal to expected + + Steps + 1. Check that get_annotations returns all annotations in the dataset item if the ROI is a full box + 2. Check that after adding the parameter "labels", only the annotations with that label are returned + 3. Check that for a ROI that includes only one of the annotations, only that annotation is returned + """ + # Check that get_annotations returns all items if the ROI is a full box. + full_box_roi_dataset_item = DatasetItemParameters().default_values_dataset_item() + full_box_annotations = list(full_box_roi_dataset_item.annotation_scene.annotations) + result_annotations = full_box_roi_dataset_item.get_annotations(include_empty=True) + expected_annotations = full_box_annotations + self.compare_denormalized_annotations(result_annotations, expected_annotations) + + # Check that get_annotations returns only the items with the right label if the "labels" param is used + first_annotation = full_box_roi_dataset_item.annotation_scene.annotations[0] + first_annotation_label = first_annotation.get_labels()[0].label + result_annotations = full_box_roi_dataset_item.get_annotations( + labels=[first_annotation_label], include_empty=True + ) + expected_annotations = [first_annotation] + self.compare_denormalized_annotations(result_annotations, expected_annotations) + + # Check that get_annotations only returns the annotations whose center falls within the ROI + partial_box_dataset_item = deepcopy(full_box_roi_dataset_item) + partial_box_dataset_item.roi = Annotation(shape=Rectangle(x1=0.0, y1=0.0, x2=0.4, y2=0.5), labels=[]) + expected_annotation = deepcopy(first_annotation) + expected_annotation.shape = expected_annotation.shape.denormalize_wrt_roi_shape( + roi_shape=partial_box_dataset_item.roi.shape + ) + result_annotations = partial_box_dataset_item.get_annotations(include_empty=True) + self.compare_denormalized_annotations(result_annotations, [expected_annotation]) + + # Check if ignored labels are properly removed + ignore_labels_dataset_item = DatasetItemParameters().default_values_dataset_item() + ignore_labels_dataset_item.ignored_labels = ignore_labels_dataset_item.get_shapes_labels( + include_ignored=True, include_empty=True + ) + assert ignore_labels_dataset_item.get_annotations(include_empty=True) == [] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_append_annotations(self): + """ + Description: + Check DatasetItemEntity class "append_annotations" method + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if annotations list returned after "append_annotations" method is equal to expected + + Steps + 1. Check annotations list returned after "append_annotations" method with specified non-included annotations + 2. Check annotations list returned after "append_annotations" method with incorrect shape annotation + """ + # Checking annotations list returned after "append_annotations" method with specified non-included annotations + dataset_item = DatasetItemParameters().default_values_dataset_item() + full_box_annotations = list(dataset_item.annotation_scene.annotations) + annotations_to_add = self.annotations_to_add() + normalized_annotations = [] + for annotation in annotations_to_add: + normalized_annotations.append( + Annotation( + shape=annotation.shape.normalize_wrt_roi_shape(dataset_item.roi.shape), + labels=annotation.get_labels(), + ) + ) + dataset_item.append_annotations(annotations_to_add) + # Random id is generated for normalized annotations + normalized_annotations[0].id_ = dataset_item.annotation_scene.annotations[2].id_ + normalized_annotations[1].id_ = dataset_item.annotation_scene.annotations[3].id_ + assert dataset_item.annotation_scene.annotations == full_box_annotations + normalized_annotations + # Checking annotations list returned after "append_annotations" method with incorrect shape annotation + incorrect_shape_label = LabelEntity( + name="Label for incorrect shape", + domain=Domain.CLASSIFICATION, + color=Color(red=80, green=70, blue=155), + id=ID("incorrect_shape_label"), + ) + incorrect_polygon = Polygon([Point(x=0.01, y=0.1), Point(x=0.35, y=0.1), Point(x=0.35, y=0.1)]) + incorrect_shape_annotation = Annotation( + shape=incorrect_polygon, + labels=[ScoredLabel(incorrect_shape_label)], + id=ID("incorrect_shape_annotation"), + ) + dataset_item.append_annotations([incorrect_shape_annotation]) + assert dataset_item.annotation_scene.annotations == full_box_annotations + normalized_annotations + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_get_roi_labels(self): + """ + Description: + Check DatasetItemEntity class "get_roi_labels" method + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if annotations list returned by "get_roi_labels" method is equal to expected + + Steps + 1. Check annotations list returned by "get_roi_labels" for non-specified "labels" parameter + 2. Check annotations list returned by "get_roi_labels" for specified "labels" parameter + 3. Check annotations list returned by "get_roi_labels" if dataset item ignores a label + """ + dataset_item = DatasetItemParameters().dataset_item() + roi_labels = DatasetItemParameters.roi_labels() + # Checking annotations list returned by "get_roi_labels" method with non-specified labels parameter + # Scenario for "include_empty" is "False" + assert dataset_item.get_roi_labels() == [roi_labels[0]] + # Scenario for "include_empty" is "True" + assert dataset_item.get_roi_labels(include_empty=True) == roi_labels + # Checking annotations list returned by "get_roi_labels" method with specified labels parameter + empty_roi_label = roi_labels[1] + # Scenario for "include_empty" is "False" + assert dataset_item.get_roi_labels(labels=[empty_roi_label]) == [] + # Scenario for "include_empty" is "True" + assert dataset_item.get_roi_labels([empty_roi_label], True) == [empty_roi_label] + # Scenario for ignored labels + dataset_item.ignored_labels = [empty_roi_label] + assert dataset_item.get_roi_labels([empty_roi_label], True) == [] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_get_shapes_labels(self): + """ + Description: + Check DatasetItemEntity class "get_shapes_labels" method + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if labels list returned by "get_shapes_labels" method is equal to expected + + Steps + 1. Check labels list returned by "get_shapes_labels" for non-specified "labels" parameter + 2. Check labels list returned by "get_shapes_labels" for specified "labels" parameter + 3. Check labels list returned by "get_shapes_labels" if dataset_item ignores labels + """ + dataset_item = DatasetItemParameters().default_values_dataset_item() + labels = DatasetItemParameters.labels() + detection_label = labels[0] + segmentation_label = labels[1] + # Checking labels list returned by "get_shapes_labels" method with non-specified "labels" parameter + # Scenario for "include_empty" is "False" + assert dataset_item.get_shapes_labels() == [detection_label] + # Scenario for "include_empty" is "True" + shapes_labels_actual = dataset_item.get_shapes_labels(include_empty=True) + assert len(shapes_labels_actual) == 2 + assert isinstance(shapes_labels_actual, list) + assert detection_label in shapes_labels_actual + assert segmentation_label in shapes_labels_actual + # Checking labels list returned by "get_shapes_labels" method with specified "labels" parameter + # Scenario for "include_empty" is "False" + non_included_label = LabelEntity("Non-included label", Domain.CLASSIFICATION) + list_labels = [segmentation_label, non_included_label] + assert dataset_item.get_shapes_labels(labels=list_labels) == [] + # Scenario for "include_empty" is "True", expected that non_included label will not be shown + assert dataset_item.get_shapes_labels(list_labels, include_empty=True) == [segmentation_label] + # Check ignore labels functionality + dataset_item.ignored_labels = [detection_label] + assert dataset_item.get_shapes_labels(include_empty=True, include_ignored=False) == [segmentation_label] + assert dataset_item.get_shapes_labels(include_empty=False, include_ignored=True) == [detection_label] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_append_labels(self): + """ + Description: + Check DatasetItemEntity class "append_labels" method + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if annotations list returned after using "append_labels" method is equal to expected + + Steps + 1. Check annotations list after "append_labels" method for DatasetItemEntity object with ROI-annotation + specified in annotation_scene.annotations + 2. Check annotations list after "append_labels" method for DatasetItemEntity object with non-specified + ROI-annotation in annotation_scene.annotations + """ + annotation_labels = DatasetItemParameters.labels() + labels_to_add = self.labels_to_add() + scored_labels_to_add = [ + ScoredLabel(labels_to_add[0]), + ScoredLabel(labels_to_add[1]), + ] + media = DatasetItemParameters.generate_random_image() + roi_labels = DatasetItemParameters.roi_labels() + roi_scored_labels = DatasetItemParameters().roi_scored_labels() + roi = DatasetItemParameters().roi() + equal_roi = DatasetItemParameters().roi() + annotations = DatasetItemParameters().annotations() + annotations_with_roi = annotations + [equal_roi] + annotations_scene = AnnotationSceneEntity(annotations_with_roi, AnnotationSceneKind.ANNOTATION) + # Scenario for checking "append_labels" method for DatasetItemEntity object with ROI-annotation specified in + # annotation_scene.annotations object + roi_label_dataset_item = DatasetItemEntity(media, annotations_scene, roi) + roi_label_dataset_item.append_labels(scored_labels_to_add) + # Check for include_empty is "False" + expected_labels = [annotation_labels[0], roi_labels[0], labels_to_add[0]] + assert roi_label_dataset_item.annotation_scene.get_labels() == expected_labels + expected_labels = [roi_scored_labels[0], scored_labels_to_add[0]] + self.check_roi_equal_annotation(roi_label_dataset_item, expected_labels) + # Check for include_empty is "True" + expected_labels = annotation_labels + roi_labels + labels_to_add + assert roi_label_dataset_item.annotation_scene.get_labels(True) == expected_labels + expected_labels = roi_scored_labels + scored_labels_to_add + self.check_roi_equal_annotation(roi_label_dataset_item, expected_labels, True) + # Scenario for checking "append_labels" method for DatasetItemEntity object with non-specified ROI-annotation in + # annotation_scene.annotations object + non_roi_dataset_item = DatasetItemParameters().dataset_item() + non_roi_dataset_item.append_labels(scored_labels_to_add) + # Check for "include_empty" is "False" + expected_labels = [annotation_labels[0], roi_labels[0], labels_to_add[0]] + assert non_roi_dataset_item.annotation_scene.get_labels() == expected_labels + expected_labels = [roi_scored_labels[0], scored_labels_to_add[0]] + self.check_roi_equal_annotation(non_roi_dataset_item, expected_labels) + # Check for "include_empty" is "True" + expected_labels = annotation_labels + roi_labels + labels_to_add + assert non_roi_dataset_item.annotation_scene.get_labels(True) == expected_labels + expected_labels = roi_scored_labels + scored_labels_to_add + self.check_roi_equal_annotation(non_roi_dataset_item, expected_labels, True) + # Scenario for "labels" parameter is equal to [] + dataset_item = DatasetItemParameters().dataset_item() + dataset_item.append_labels([]) + assert dataset_item.annotation_scene.get_labels() == [annotation_labels[0]] + assert dataset_item.annotation_scene.get_labels(include_empty=True) == annotation_labels + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_eq(self): + """ + Description: + Check DatasetItemEntity class __eq__ method + + Input data: + DatasetItemEntity class objects with specified "media", "annotation_scene", "roi", "metadata", "subset" + and "ignored_labels" parameters + + Expected results: + Test passes if value returned by __eq__ method is equal to expected + + Steps + 1. Check value returned by __eq__ method for equal DatasetItemEntity objects + 2. Check value returned by __eq__ method for DatasetItemEntity objects with unequal "media", "annotation_scene", + "roi", "subset" or "ignored_labels" parameters + 3. Check value returned by __eq__ method for DatasetItemEntity objects with unequal "metadata" parameters + 4. Check value returned by __eq__ method for DatasetItemEntity object compared to different type object + """ + media = DatasetItemParameters.generate_random_image() + annotation_scene = DatasetItemParameters().annotations_entity() + roi = DatasetItemParameters().roi() + metadata = DatasetItemParameters.metadata() + ignored_labels = DatasetItemParameters.labels()[:1] + dataset_parameters = { + "media": media, + "annotation_scene": annotation_scene, + "roi": roi, + "metadata": metadata, + "subset": Subset.TESTING, + "ignored_labels": ignored_labels, + } + dataset_item = DatasetItemEntity(**dataset_parameters) + # Checking value returned by __eq__ method for equal DatasetItemEntity objects + equal_dataset_item = DatasetItemEntity(**dataset_parameters) + assert dataset_item == equal_dataset_item + # Checking inequality of DatasetItemEntity objects with unequal initialization parameters + unequal_annotation_scene = DatasetItemParameters().annotations_entity() + unequal_annotation_scene.annotations.pop(0) + unequal_ignored_labels = DatasetItemParameters.labels()[1:] + unequal_values = [ + ("media", DatasetItemParameters.generate_random_image()), + ("annotation_scene", unequal_annotation_scene), + ("roi", None), + ("subset", Subset.VALIDATION), + ("ignored_labels", unequal_ignored_labels), + ] + for key, value in unequal_values: + unequal_parameters = dict(dataset_parameters) + unequal_parameters[key] = value + unequal_dataset_item = DatasetItemEntity(**unequal_parameters) + assert dataset_item != unequal_dataset_item, ( + f"Expected False returned for DatasetItemEntity objects with " f"unequal {key} parameters" + ) + # Checking value returned by __eq__ method for DatasetItemEntity objects with unequal "metadata" parameters + # expected equality + unequal_metadata_parameters = dict(dataset_parameters) + unequal_metadata_parameters["metadata"] = None + unequal_metadata_dataset_item = DatasetItemEntity(**unequal_metadata_parameters) + assert dataset_item == unequal_metadata_dataset_item + # Checking value returned by __eq__ method for DatasetItemEntity object compared to different type object + assert not dataset_item == str + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_deepcopy(self): + """ + Description: + Check DatasetItemEntity class __deepcopy__ method + + Input data: + DatasetItemEntity class objects with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if DatasetItemEntity object created by __deepcopy__ method is equal to expected + """ + dataset_item = DatasetItemParameters().dataset_item() + copy_dataset = deepcopy(dataset_item) + assert dataset_item._DatasetItemEntity__roi_lock != copy_dataset._DatasetItemEntity__roi_lock + assert np.array_equal(dataset_item.media.numpy, copy_dataset.media.numpy) + assert dataset_item.annotation_scene.annotations == copy_dataset.annotation_scene.annotations + assert dataset_item.annotation_scene.creation_date == copy_dataset.annotation_scene.creation_date + assert dataset_item.annotation_scene.editor_name == copy_dataset.annotation_scene.editor_name + assert dataset_item.annotation_scene.id_ == copy_dataset.annotation_scene.id_ + assert dataset_item.annotation_scene.kind == copy_dataset.annotation_scene.kind + assert dataset_item.annotation_scene.shapes == copy_dataset.annotation_scene.shapes + assert dataset_item.roi == copy_dataset.roi + assert dataset_item.get_metadata() == copy_dataset.get_metadata() + assert dataset_item.subset == copy_dataset.subset + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_append_metadata_item(self): + """ + Description: + Check DatasetItemEntity class "append_metadata_item" method + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if "metadata" attribute after "append_metadata_item" method is equal to expected + + Steps + 1. Check "metadata" attribute after "append_metadata_item" method with non-specified "model" parameter + 2. Check "metadata" attribute after "append_metadata_item" method with specified "model" parameter + """ + dataset_item = DatasetItemParameters().dataset_item() + expected_metadata = dataset_item.get_metadata() + # Checking metadata attribute returned after "append_metadata_item" method with non-specified "model" parameter + data_to_append = TensorEntity( + name="appended_metadata", + numpy=np.random.uniform(low=0.0, high=255.0, size=(10, 15, 3)), + ) + expected_metadata.append(MetadataItemEntity(data=data_to_append)) + dataset_item.append_metadata_item(data=data_to_append) + assert dataset_item.get_metadata() == expected_metadata + # Checking metadata attribute returned after "append_metadata_item" method with specified "model" parameter + metadata_item_with_model = self.metadata_item_with_model() + data_to_append = metadata_item_with_model.data + model_to_append = metadata_item_with_model.model + new_metadata_item_with_model = MetadataItemEntity(data_to_append, model_to_append) + expected_metadata.append(new_metadata_item_with_model) + dataset_item.append_metadata_item(data_to_append, model_to_append) + assert dataset_item.get_metadata() == expected_metadata + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_get_metadata_by_name_and_model(self): + """ + Description: + Check DatasetItemEntity class "get_metadata_by_name_and_model" method + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if MetadataItemEntity object returned by "get_metadata_by_name_and_model" is equal to expected + + Steps + 1. Check value returned by "get_metadata_by_name_and_model" method for searching metadata object with "model" + is "None" + 2. Check value returned by "get_metadata_by_name_and_model" method for searching metadata object with specified + "model" attribute + 3. Check value returned by "get_metadata_by_name_and_model" method for searching non-existing metadata object + """ + dataset_item = DatasetItemParameters().dataset_item() + metadata_item_with_model = self.metadata_item_with_model() + dataset_model = metadata_item_with_model.model + dataset_item.append_metadata_item(metadata_item_with_model.data, dataset_model) + dataset_metadata = dataset_item.get_metadata() + # Checking "get_metadata_by_name_and_model" method for "model" parameter is "None" + assert dataset_item.get_metadata_by_name_and_model("test_metadata", None) == [dataset_metadata[0]] + # Checking "get_metadata_by_name_and_model" method for specified "model" parameter + assert dataset_item.get_metadata_by_name_and_model("appended_metadata_with_model", dataset_model) == [ + dataset_metadata[2] + ] + # Checking "get_metadata_by_name_and_model" method for searching non-existing metadata + assert dataset_item.get_metadata_by_name_and_model("test_metadata", dataset_model) == [] + assert dataset_item.get_metadata_by_name_and_model("appended_metadata_with_model", None) == [] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_item_setters(self): + """ + Description: + Check DatasetItemEntity class "roi", "subset" and "annotation_scene" setters + + Input data: + DatasetItemEntity class object with specified "media", "annotation_scene", "roi", "metadata" and "subset" + parameters + + Expected results: + Test passes if assigned values of "roi", "subset" and "annotation_scene" properties are equal to expected + + Steps + 1. Check value returned by "roi" property after using @roi.setter + 2. Check value returned by "subset" property after using @subset.setter + 3. Check value returned by "annotation_scene" property after using @subset.annotation_scene + """ + dataset_item = DatasetItemParameters().dataset_item() + # Checking value returned by "roi" property after using @roi.setter + new_roi_label = ScoredLabel(LabelEntity("new ROI label", Domain.DETECTION)) + new_dataset_roi = Annotation(Rectangle(x1=0.2, y1=0.2, x2=1.0, y2=1.0), [new_roi_label]) + dataset_item.roi = new_dataset_roi + assert dataset_item.roi == new_dataset_roi + # Checking value returned by subset property after using @subset.setter + new_subset = Subset.TRAINING + dataset_item.subset = new_subset + assert dataset_item.subset == new_subset + # Checking value returned by annotation_scene property after using @annotation_scene.setter + new_annotation_label = ScoredLabel(LabelEntity("new annotation label", Domain.CLASSIFICATION)) + new_annotation = Annotation(Rectangle(x1=0.1, y1=0, x2=0.9, y2=1.0), [new_annotation_label]) + new_annotation_scene = AnnotationSceneEntity([new_annotation], AnnotationSceneKind.PREDICTION) + dataset_item.annotation_scene = new_annotation_scene + assert dataset_item.annotation_scene == new_annotation_scene + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @pytest.mark.parametrize("func_name", ["dataset_item", "dataset_item_with_id"]) + def test_wrap(self, func_name): + constructor = DatasetItemParameters() + func = getattr(constructor, func_name) + item: DatasetItemEntity = func() + + new_media = DatasetItemParameters().generate_random_image() + assert item.media != new_media + + new_subset = Subset.PSEUDOLABELED + assert item.subset != new_subset + + new_metadata = DatasetItemParameters().metadata() + assert item.get_metadata() != new_metadata + + new_item = item.wrap(media=new_media, subset=new_subset, metadata=new_metadata) + assert new_item.media == new_media + assert new_item.subset == new_subset + assert new_item.get_metadata() == new_metadata + + if hasattr(item, "id_"): + new_id = ID("new_id") + assert item.id_ != new_id + item = item.wrap(id_=new_id) + assert item.id_ == new_id diff --git a/tests/unit/api/entities/test_datasets.py b/tests/unit/api/entities/test_datasets.py new file mode 100644 index 00000000000..bc29f8ece65 --- /dev/null +++ b/tests/unit/api/entities/test_datasets.py @@ -0,0 +1,672 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import List + +import pytest + +from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity, DatasetPurpose +from otx.api.entities.label import LabelEntity +from otx.api.entities.subset import Subset +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements +from tests.unit.api.entities.test_dataset_item import DatasetItemParameters + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDatasetPurpose: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_purpose(self): + """ + Description: + Check DatasetPurpose Enum class elements + + Expected results: + Test passes if DatasetPurpose Enum class length is equal to expected value, its elements have expected + sequence numbers and value returned by __str__ method is equal to expected + """ + assert len(DatasetPurpose) == 6 + assert DatasetPurpose.INFERENCE.value == 0 + assert str(DatasetPurpose.INFERENCE) == "INFERENCE" + assert DatasetPurpose.TRAINING.value == 1 + assert str(DatasetPurpose.TRAINING) == "TRAINING" + assert DatasetPurpose.EVALUATION.value == 2 + assert str(DatasetPurpose.EVALUATION) == "EVALUATION" + assert DatasetPurpose.GENERATING_OUTPUT.value == 3 + assert str(DatasetPurpose.GENERATING_OUTPUT) == "GENERATING_OUTPUT" + assert DatasetPurpose.TEMPORARY_DATASET.value == 4 + assert str(DatasetPurpose.TEMPORARY_DATASET) == "TEMPORARY_DATASET" + assert DatasetPurpose.TASK_INFERENCE.value == 5 + assert str(DatasetPurpose.TASK_INFERENCE) == "TASK_INFERENCE" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDatasetEntity: + @staticmethod + def generate_random_image(): + return DatasetItemParameters.generate_random_image() + + @staticmethod + def labels() -> List[LabelEntity]: + return DatasetItemParameters.labels() + + @staticmethod + def annotations_entity() -> AnnotationSceneEntity: + return DatasetItemParameters().annotations_entity() + + @staticmethod + def metadata(): + return DatasetItemParameters.metadata() + + @staticmethod + def default_values_dataset_item() -> DatasetItemEntity: + return DatasetItemParameters().default_values_dataset_item() + + @staticmethod + def dataset_item() -> DatasetItemEntity: + return DatasetItemParameters().dataset_item() + + def dataset(self) -> DatasetEntity: + other_dataset_item = DatasetItemEntity( + media=self.generate_random_image(), + annotation_scene=self.annotations_entity(), + metadata=self.metadata(), + subset=Subset.VALIDATION, + ) + items = [ + self.default_values_dataset_item(), + self.dataset_item(), + other_dataset_item, + ] + return DatasetEntity(items, DatasetPurpose.TEMPORARY_DATASET) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_initialization(self): + """ + Description: + Check DatasetEntity class object initialization + + Input data: + DatasetEntity class objects with specified "items" and "purpose" parameters + + Expected results: + Test passes if "items" and "purpose" attributes of DatasetEntity object are equal to expected values + + Steps + 1. Check attributes of DatasetItemEntity object initialized with default optional parameters + 2. Check attributes of DatasetItemEntity object initialized with specified optional parameters + """ + # Checking attributes of DatasetItemEntity object initialized with default optional parameters + default_parameters_dataset = DatasetEntity() + assert default_parameters_dataset._items == [] + assert default_parameters_dataset.purpose == DatasetPurpose.INFERENCE + # Checking attributes of DatasetItemEntity object initialized with specified optional parameters + items = [ + self.default_values_dataset_item(), + self.dataset_item(), + self.dataset_item(), + ] + purpose = DatasetPurpose.TEMPORARY_DATASET + optional_parameters_dataset = DatasetEntity(items, purpose) + assert optional_parameters_dataset._items == items + assert optional_parameters_dataset.purpose == purpose + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_purpose_setter(self): + """ + Description: + Check DatasetEntity class "purpose" setter + + Input data: + DatasetEntity class objects with specified "items" and "purpose" parameters + + Expected results: + Test passes if assigned value of "purpose" property is equal to expected + + Steps + 1. Check value returned by "purpose" property after using @purpose.setter for DatasetEntity with + default optional parameters + 2. Check value returned by "purpose" property after using @purpose.setter for DatasetEntity initialized with + specified optional parameters + """ + # Checking "purpose" property after using @purpose.setter for DatasetEntity with default optional parameters + default_parameters_dataset = DatasetEntity() + expected_purpose = DatasetPurpose.TRAINING + default_parameters_dataset.purpose = expected_purpose + assert default_parameters_dataset.purpose == expected_purpose + # Checking "purpose" property after using @purpose.setter for DatasetEntity with specified optional parameters + optional_parameters_dataset = self.dataset() + expected_purpose = DatasetPurpose.TASK_INFERENCE + optional_parameters_dataset.purpose = expected_purpose + assert optional_parameters_dataset.purpose == expected_purpose + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_fetch(self): + """ + Description: + Check DatasetEntity class "_fetch" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if value returned by "_fetch" method is equal to expected + + Steps + 1. Check value returned by "_fetch" method when list of DatasetItems indexes is specified as "key" parameter + 2. Check value returned by "_fetch" method when slice of DatasetItems indexes is specified as "key" parameter + 3. Check value returned by "_fetch" method when DatasetItem index is specified as "key" parameter + 4. Check TypeError exception is raised when unexpected type object is specified as "key" parameter + """ + dataset = self.dataset() + dataset_items = dataset._items + # Checking "_fetch" method when list of DatasetItems indexes is specified as "key" parameter + assert dataset._fetch([0, 2]) == [dataset_items[0], dataset_items[2]] + # Checking "_fetch" method when slice of DatasetItems indexes is specified as "key" parameter + assert dataset._fetch(slice(0, 3, 2)) == [dataset_items[0], dataset_items[2]] + assert dataset._fetch(slice(-1, -4, -1)) == [ + dataset_items[2], + dataset_items[1], + dataset_items[0], + ] + # Checking "_fetch" method when DatasetItem index is specified as "key" parameter + assert dataset._fetch(1) == dataset_items[1] + # Checking that TypeError exception is raised when unexpected type object is specified as "key" parameter + with pytest.raises(TypeError): + dataset._fetch(str) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_repr(self): + """ + Description: + Check DatasetEntity class "__repr__" method + + Input data: + DatasetEntity class objects with specified "items" and "purpose" parameters + + Expected results: + Test passes if value returned by "__repr__" method is equal to expected + + Steps + 1. Check value returned by "__repr__" method for DatasetEntity with default optional parameters + 2. Check value returned by "__repr__" method for DatasetEntity with specified optional parameters + """ + # Checking value returned by "__repr__" method for DatasetEntity with default optional parameters + default_parameters_dataset = DatasetEntity() + assert repr(default_parameters_dataset) == "DatasetEntity(items=[], purpose=INFERENCE)" + # Checking value returned by "__repr__" method for DatasetEntity with specified optional parameters + optional_parameters_dataset = self.dataset() + assert ( + repr(optional_parameters_dataset) == f"DatasetEntity(items={optional_parameters_dataset._items}, " + f"purpose=TEMPORARY_DATASET)" + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_str(self): + """ + Description: + Check DatasetEntity class "__str__" method + + Input data: + DatasetEntity class objects with specified "items" and "purpose" parameters + + Expected results: + Test passes if value returned by "__str__" method is equal to expected + + Steps + 1. Check value returned by "__str__" method for DatasetEntity with default optional parameters + 2. Check value returned by "__str__" method for DatasetEntity with specified optional parameters + """ + # Checking value returned by "__str__" method for DatasetEntity with default optional parameters + default_parameters_dataset = DatasetEntity() + assert str(default_parameters_dataset) == "DatasetEntity(size=0, purpose=INFERENCE)" + # Checking value returned by "__str__" method for DatasetEntity with specified optional parameters + optional_parameters_dataset = self.dataset() + assert str(optional_parameters_dataset) == "DatasetEntity(size=3, purpose=TEMPORARY_DATASET)" + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_len(self): + """ + Description: + Check DatasetEntity class "__len__" method + + Input data: + DatasetEntity class objects with specified "items" and "purpose" parameters + + Expected results: + Test passes if value returned by "__len__" method is equal to expected + + Steps + 1. Check value returned by "__len__" method for DatasetEntity with default optional parameters + 2. Check value returned by "__len__" method for DatasetEntity with specified optional parameters + """ + # Checking value returned by "__str__" method for DatasetEntity with default optional parameters + default_parameters_dataset = DatasetEntity() + assert len(default_parameters_dataset) == 0 + # Checking value returned by "__str__" method for DatasetEntity with specified optional parameters + optional_parameters_dataset = self.dataset() + assert len(optional_parameters_dataset) == 3 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_eq(self): + """ + Description: + Check DatasetEntity class "__eq__" method + + Input data: + DatasetEntity class objects with specified "items" and "purpose" parameters + + Expected results: + Test passes if value returned by "__eq__" method is equal to expected + + Steps + 1. Check value returned by "__eq__" method for equal DatasetEntity objects + 2. Check value returned by "__eq__" method for DatasetEntity objects with unequal length + 3. Check value returned by "__eq__" method for DatasetEntity objects with equal length, but unequal + DatasetItem objects + 4. Check value returned by "__eq__" method for DatasetEntity objects with unequal "purpose" attributes + 5. Check value returned by "__eq__" method for comparing DatasetEntity object with object of different type + """ + # Checking value returned by "__eq__" method for equal DatasetEntity objects + items = [ + self.default_values_dataset_item(), + self.dataset_item(), + self.dataset_item(), + ] + purpose = DatasetPurpose.TEMPORARY_DATASET + dataset = DatasetEntity(items, purpose) + equal_dataset = DatasetEntity(items, purpose) + assert dataset == equal_dataset + # Checking value returned by "__eq__" method for DatasetEntity objects with unequal length + unequal_items = list(items) + unequal_items.pop(-1) + unequal_dataset = DatasetEntity(unequal_items, purpose) + assert dataset != unequal_dataset + # Checking value returned by "__eq__" method for DatasetEntity objects with equal length, but unequal + # DatasetItem objects + unequal_items.append(self.dataset_item()) + unequal_dataset = DatasetEntity(unequal_items, purpose) + assert dataset != unequal_dataset + # Checking value returned by "__eq__" method for DatasetEntity objects with unequal "purpose" attributes + unequal_dataset = DatasetEntity(items, DatasetPurpose.EVALUATION) + assert dataset != unequal_dataset + # Checking value returned by "__eq__" method for comparing DatasetEntity object with object of different type + assert dataset != str + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_add(self): + """ + Description: + Check DatasetEntity class "__add__" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if DatasetEntity object returned by "__add__"" method is equal to expected + + Steps + 1. Check DatasetEntity object returned by "__add__"" method with DatasetEntity specified as "other" parameter + 2. Check DatasetEntity object returned by "__add__"" method with list of DatasetItemEntity objects specified + as "other" parameter + 3. Check ValueError exception is raised when unexpected type object is specified in "other" parameter of + "__add__" method + """ + dataset = self.dataset() + dataset_items = list(dataset._items) + # Checking DatasetEntity object returned by "__add__"" method with DatasetEntity specified as "other" parameter + other_dataset_items = [self.dataset_item(), self.dataset_item()] + other_dataset = DatasetEntity(other_dataset_items, DatasetPurpose.TRAINING) + new_dataset = dataset.__add__(other_dataset) + assert new_dataset._items == dataset_items + other_dataset_items + assert new_dataset.purpose == DatasetPurpose.TEMPORARY_DATASET + # Checking DatasetEntity object returned by "__add__"" method with list of DatasetItemEntity objects specified + # as "other" parameter + items_to_add = [ + self.dataset_item(), + self.dataset_item(), + "unexpected type object", + ] + new_dataset = dataset.__add__(items_to_add) + # Expected that str object will not be added to new_dataset._items + assert new_dataset._items == dataset_items + items_to_add[0:2] + assert new_dataset.purpose == DatasetPurpose.TEMPORARY_DATASET + # Checking ValueError exception is raised when unexpected type object is specified in "other" parameter of + # "__add__" method + with pytest.raises(ValueError): + dataset.__add__(str) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_getitem(self): + """ + Description: + Check DatasetEntity class "__getitem__" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if value returned by "__getitem__" method is equal to expected + + Steps + 1. Check value returned by "__getitem__" method when index is specified as "key" parameter + 2. Check value returned by "__getitem__" method when slice is specified as "key" parameter + """ + dataset = self.dataset() + dataset_items = dataset._items + # Checking value returned by "__getitem__" method when index is specified as "key" parameter + assert dataset[1] == dataset_items[1] + # Checking value returned by "__getitem__" method when slice is specified as "key" parameter + assert dataset[slice(0, 3, 2)] == [dataset_items[0], dataset_items[2]] + assert dataset[slice(-1, -4, -1)] == [ + dataset_items[2], + dataset_items[1], + dataset_items[0], + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_iter(self): + """ + Description: + Check DatasetEntity class "__iter__" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if DatasetItemEntity returned by "__iter__" method is equal to expected + """ + dataset = self.dataset() + dataset_items = list(dataset._items) + dataset_iterator = dataset.__iter__() + expected_index = 0 + for expected_dataset_item in dataset_items: + assert dataset_iterator.index == expected_index + assert next(dataset_iterator) == expected_dataset_item + expected_index += 1 + with pytest.raises(StopIteration): + next(dataset_iterator) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_with_empty_annotations(self): + """ + Description: + Check DatasetEntity class "with_empty_annotations" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if DatasetEntity object returned by "with_empty_annotations" method is equal to expected + + Steps + 1. Check DatasetEntity object returned by "with_empty_annotations" with non-specified "annotation_kind" + parameter + 2. Check DatasetEntity object returned by "with_empty_annotations" with specified "annotation_kind" parameter + """ + + def check_empty_annotations_dataset(actual_dataset, expected_dataset, expected_kind): + expected_items = expected_dataset._items + actual_items = actual_dataset._items + assert actual_dataset.purpose is expected_dataset.purpose + for i in range(len(expected_items) - 1): + actual_item = actual_items[i] + expected_item = expected_items[i] + assert actual_item.media is expected_item.media + assert actual_item.annotation_scene.annotations == [] + assert actual_item.annotation_scene.kind == expected_kind + assert actual_item.roi.id_ != expected_item.roi.id_ + assert actual_item.roi.shape is expected_item.roi.shape + assert actual_item.roi.get_labels() == [] + assert actual_item.subset is expected_item.subset + + dataset = self.dataset() + # Checking DatasetEntity object returned by "with_empty_annotations" with non-specified "annotation_kind" + # parameter + empty_annotations_dataset = dataset.with_empty_annotations() + check_empty_annotations_dataset(empty_annotations_dataset, dataset, AnnotationSceneKind.PREDICTION) + # Checking DatasetEntity object returned by "with_empty_annotations" with specified "annotation_kind" parameter + kind = AnnotationSceneKind.ANNOTATION + empty_annotations_dataset = dataset.with_empty_annotations(kind) + check_empty_annotations_dataset(empty_annotations_dataset, dataset, kind) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_get_subset(self): + """ + Description: + Check DatasetEntity class "get_subset" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if DatasetEntity returned by "get_subset" method is equal to expected + + Steps + 1. Check DatasetEntity object returned by "get_subset" method with "subset" parameter that in items of base + dataset + 2. Check DatasetEntity object returned by "get_subset" method with "subset" parameter that not in items of base + dataset + """ + validation_item = self.dataset_item() + validation_item.subset = Subset.VALIDATION + dataset = self.dataset() + dataset._items.append(validation_item) + # Checking DatasetEntity object returned by "get_subset" method with "subset" parameter that in items of base + # dataset + validation_dataset = dataset.get_subset(Subset.VALIDATION) + assert validation_dataset.purpose is dataset.purpose + assert validation_dataset._items == [dataset._items[2]] + [validation_item] + # Checking DatasetEntity object returned by "get_subset" method with "subset" parameter that not in items of + # base dataset + empty_items_dataset = dataset.get_subset(Subset.UNLABELED) + assert empty_items_dataset.purpose is dataset.purpose + assert empty_items_dataset._items == [] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_get_combined_subset(self): + """ + Description: + Check DatasetEntity class "get_combined_subset" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if DatasetEntity returned by "get_combined_subset" method is equal to expected + + Steps + 1. Check DatasetEntity object returned by "get_combined_subset" method with list of subsets that in items of + base dataset + 2. Check DatasetEntity object returned by "get_combined_subset" method with list of subsets that partialy in + items of base dataset + 3. Check DatasetEntity object returned by "get_combined_subset" method with list of only one subset that in + items of base dataset + """ + validation_item = self.dataset_item() + validation_item.subset = Subset.VALIDATION + training_item = self.dataset_item() + training_item.subset = Subset.TRAINING + dataset = self.dataset() + dataset._items.append(training_item) + dataset._items.append(validation_item) + # Checking DatasetEntity object returned by "get_combined_subset" method with list of subsets + # TRAINING + VALIDATION + mixed_items_dataset = dataset.get_combined_subset([Subset.TRAINING, Subset.VALIDATION]) + assert mixed_items_dataset.purpose is dataset.purpose + assert mixed_items_dataset._items == [dataset._items[2]] + [training_item] + [validation_item] + # TRAINING + UNLABELED (which is not in items of the base dataset) + mixed_items_dataset = dataset.get_combined_subset([Subset.TRAINING, Subset.UNLABELED]) + assert mixed_items_dataset._items == [training_item] + # VALIDATION only + mixed_items_dataset = dataset.get_combined_subset([Subset.VALIDATION]) + assert mixed_items_dataset._items == [dataset._items[2]] + [validation_item] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_remove(self): + """ + Description: + Check DatasetEntity class "remove" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if "items" attribute of DatasetEntity object is equal to expected after using "remove" method + """ + dataset = self.dataset() + dataset_items = list(dataset._items) + # Removing DatasetItemEntity included in DatasetEntity + dataset.remove(dataset_items[1]) + dataset_items.pop(1) + assert dataset._items == dataset_items + non_included_dataset_item = self.dataset_item() + # Check that ValueError exception is raised when removing non-included DatasetItemEntity + with pytest.raises(ValueError): + dataset.remove(non_included_dataset_item) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_append(self): + """ + Description: + Check DatasetEntity class "append" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if "items" attribute of DatasetEntity object is equal to expected after using "append" method + + Steps + 1. Check "items" attribute of DatasetEntity object after adding new DatasetEntity object + 2. Check "items" attribute of DatasetEntity object after adding existing DatasetEntity object + 3. Check that ValueError exception is raised when appending DatasetEntity with "media" attribute is equal to + "None" + """ + dataset = self.dataset() + expected_items = list(dataset._items) + # Checking "items" attribute of DatasetEntity object after adding new DatasetEntity object + item_to_add = self.dataset_item() + dataset.append(item_to_add) + expected_items.append(item_to_add) + assert dataset._items == expected_items + # Checking "items" attribute of DatasetEntity object after adding existing DatasetEntity object + dataset.append(item_to_add) + expected_items.append(item_to_add) + assert dataset._items == expected_items + # Checking that ValueError exception is raised when appending DatasetEntity with "media" is "None" attribute + no_media_item = DatasetItemEntity(None, self.annotations_entity()) + with pytest.raises(ValueError): + dataset.append(no_media_item) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_sort_items(self): + """ + Description: + Check DatasetEntity class "sort_items" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if "items" attribute of DatasetEntity object is remained the same after using "sort_items" method + """ + dataset = self.dataset() + expected_items = list(dataset._items) + dataset.sort_items() + assert dataset._items == expected_items + assert dataset.purpose == DatasetPurpose.TEMPORARY_DATASET + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_remove_at_indices(self): + """ + Description: + Check DatasetEntity class "remove_at_indices" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if "items" attribute of DatasetEntity object is equal to expected after using "remove_at_indices" + method + """ + dataset = self.dataset() + expected_items = list(dataset._items) + # Removing DatasetItemEntity included in DatasetEntity + dataset.remove_at_indices([0, 2]) + expected_items.pop(2) + expected_items.pop(0) + assert dataset._items == expected_items + # Check that IndexError exception is raised when removing DatasetItemEntity with non-included index + with pytest.raises(IndexError): + dataset.remove_at_indices([20]) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dataset_entity_get_labels(self): + """ + Description: + Check DatasetEntity class "get_labels" method + + Input data: + DatasetEntity class object with specified "items" and "purpose" parameters + + Expected results: + Test passes if list returned by "get_labels" method is equal to expected + + Steps + 1. Check list returned by "get_labels" method with "include_empty" parameter is "False" + 2. Check list returned by "get_labels" method with "include_empty" parameter is "True" + """ + labels = self.labels() + detection_label = labels[0] + segmentation_empty_label = labels[1] + dataset = self.dataset() + # Checking list returned by "get_labels" method with "include_empty" parameter is "False" + assert dataset.get_labels() == [detection_label] + # Checking list returned by "get_labels" method with "include_empty" parameter is "True" + actual_empty_labels = dataset.get_labels(include_empty=True) + assert len(actual_empty_labels) == 2 + assert isinstance(actual_empty_labels, list) + assert segmentation_empty_label in actual_empty_labels + assert detection_label in actual_empty_labels diff --git a/tests/unit/api/entities/test_graph.py b/tests/unit/api/entities/test_graph.py new file mode 100644 index 00000000000..4642e52958e --- /dev/null +++ b/tests/unit/api/entities/test_graph.py @@ -0,0 +1,992 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from networkx.classes.reportviews import ( + DegreeView, + DiMultiDegreeView, + EdgeDataView, + EdgeView, + InMultiDegreeView, + NodeView, + OutMultiDegreeView, + OutMultiEdgeDataView, + OutMultiEdgeView, +) +from networkx.exception import NetworkXError, NetworkXNotImplemented + +from otx.api.entities.graph import Graph, MultiDiGraph +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestGraph: + @staticmethod + def add_edges_to_graph(graph_to_add_edges: Graph, edges_parameters: list) -> None: + for edge_dict in edges_parameters: + graph_to_add_edges.add_edge(**edge_dict) + + def non_directed_graph(self) -> Graph: + non_directed_graph = Graph() + self.add_edges_to_graph( + non_directed_graph, + [ + {"node1": 1, "node2": 2, "edge_value": 2}, + {"node1": 2, "node2": 3, "edge_value": 3}, + {"node1": 3, "node2": 4, "edge_value": 4}, + ], + ) + return non_directed_graph + + def directed_graph(self) -> Graph: + directed_graph = Graph(directed=True) + self.add_edges_to_graph( + directed_graph, + [ + {"node1": "A", "node2": "B", "edge_value": 1}, + {"node1": "B", "node2": "C", "edge_value": 2}, + {"node1": "C", "node2": "D", "edge_value": 3}, + {"node1": "D", "node2": "A", "edge_value": 5}, + {"node1": "A", "node2": "D", "edge_value": 4}, + ], + ) + return directed_graph + + @staticmethod + def check_graph_non_list_attributes(expected_attributes_dicts: list) -> None: + for expected_attribute_dict in expected_attributes_dicts: + assert expected_attribute_dict.get("attribute") == expected_attribute_dict.get("expected_value") + + @staticmethod + def check_graph_list_attributes(actual_expected_attributes_dict: list) -> None: + for expected_attribute_dict in actual_expected_attributes_dict: + attribute = expected_attribute_dict.get("attribute") + assert isinstance(attribute, expected_attribute_dict.get("expected_type")) + assert list(attribute) == expected_attribute_dict.get("expected_value") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @pytest.mark.filterwarnings("ignore::PendingDeprecationWarning") + def test_graph_initialization(self): + """ + Description: + Check Graph class object initialization + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if Graph object "directed" attribute, "edges", "nodes" and "num_labels" properties and "num_nodes" + method return expected values + + Steps + 1. Check initialization of non-directed Graph object + 2. Check initialization of directed Graph object + """ + # Checking not directed Graph values + non_directed_graph = self.non_directed_graph() + self.check_graph_non_list_attributes( + [ + {"attribute": non_directed_graph.directed, "expected_value": False}, + {"attribute": non_directed_graph.num_labels, "expected_value": 4}, + {"attribute": non_directed_graph.num_nodes(), "expected_value": 4}, + ] + ) + self.check_graph_list_attributes( + [ + { + "attribute": non_directed_graph.edges, + "expected_type": EdgeDataView, + "expected_value": [ + (1, 2, {"value": 2}), + (2, 3, {"value": 3}), + (3, 4, {"value": 4}), + ], + }, + { + "attribute": non_directed_graph.nodes, + "expected_type": NodeView, + "expected_value": [1, 2, 3, 4], + }, + ] + ) + # Checking directed Graph values + directed_graph = self.directed_graph() + self.check_graph_non_list_attributes( + [ + {"attribute": directed_graph.directed, "expected_value": True}, + {"attribute": directed_graph.num_labels, "expected_value": 4}, + {"attribute": directed_graph.num_nodes(), "expected_value": 4}, + ] + ) + self.check_graph_list_attributes( + [ + { + "attribute": directed_graph.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": [ + ("A", "B", 0, {"value": 1}), + ("A", "D", 0, {"value": 4}), + ("B", "C", 0, {"value": 2}), + ("C", "D", 0, {"value": 3}), + ("D", "A", 0, {"value": 5}), + ], + }, + { + "attribute": directed_graph.nodes, + "expected_type": NodeView, + "expected_value": ["A", "B", "C", "D"], + }, + ] + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_get_graph(self): + """ + Description: + Check get_graph method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if Graph object returned by get_graph method returns expected attributes + + Steps + 1. Check attributes of Graph returned by get_graph method for non-directed Graph object + 2. Check attributes of Graph returned by get_graph method for directed Graph object + """ + # Checking non-directed Graph attributes + non_directed_graph = self.non_directed_graph().get_graph() + self.check_graph_list_attributes( + [ + { + "attribute": non_directed_graph.degree, + "expected_type": DegreeView, + "expected_value": [(1, 1), (2, 2), (3, 2), (4, 1)], + }, + { + "attribute": non_directed_graph.edges, + "expected_type": EdgeView, + "expected_value": [(1, 2), (2, 3), (3, 4)], + }, + { + "attribute": non_directed_graph.nodes, + "expected_type": NodeView, + "expected_value": [1, 2, 3, 4], + }, + ] + ) + # Checking directed Graph attributes + directed_graph = self.directed_graph().get_graph() + self.check_graph_list_attributes( + [ + { + "attribute": directed_graph.nodes, + "expected_type": NodeView, + "expected_value": ["A", "B", "C", "D"], + }, + { + "attribute": directed_graph.edges, + "expected_type": OutMultiEdgeView, + "expected_value": [ + ("A", "B", 0), + ("A", "D", 0), + ("B", "C", 0), + ("C", "D", 0), + ("D", "A", 0), + ], + }, + { + "attribute": directed_graph.degree, + "expected_type": DiMultiDegreeView, + "expected_value": [("A", 3), ("B", 2), ("C", 2), ("D", 3)], + }, + { + "attribute": directed_graph.in_degree, + "expected_type": InMultiDegreeView, + "expected_value": [("A", 1), ("B", 1), ("C", 1), ("D", 2)], + }, + { + "attribute": directed_graph.out_degree, + "expected_type": OutMultiDegreeView, + "expected_value": [("A", 2), ("B", 1), ("C", 1), ("D", 1)], + }, + ] + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_set_graph(self): + """ + Description: + Check set_graph method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if graph attribute returned after set_graph method has expected values + + Steps + 1. Check graph attribute after setting unequal non-directed graph to non-directed Graph object + 2. Check graph attribute after setting directed graph to non-directed Graph object + 3. Check graph attribute after setting unequal directed graph to directed Graph object + 4. Check graph attribute after setting non-directed graph to directed Graph object + """ + # Setting unequal non-directed graph to non-directed Graph + non_directed_graph = self.non_directed_graph() + unequal_non_directed_graph = Graph() + self.add_edges_to_graph( + unequal_non_directed_graph, + [ + {"node1": 1, "node2": 3, "edge_value": 2}, + {"node1": 2, "node2": 4, "edge_value": 2}, + ], + ) + non_directed_graph.set_graph(unequal_non_directed_graph) + assert non_directed_graph.get_graph() == unequal_non_directed_graph + # Setting directed graph to non-directed Graph + non_directed_graph = self.non_directed_graph() + directed_graph = self.directed_graph() + non_directed_graph.set_graph(directed_graph) + assert non_directed_graph.get_graph() == directed_graph + # Setting unequal directed graph to directed Graph + unequal_directed_graph = Graph(directed=True) + self.add_edges_to_graph( + unequal_directed_graph, + [ + {"node1": "A", "node2": "B", "edge_value": 1}, + {"node1": "B", "node2": "C", "edge_value": 2}, + {"node1": "C", "node2": "B", "edge_value": 1}, + ], + ) + directed_graph.set_graph(unequal_directed_graph) + assert directed_graph.get_graph() == unequal_directed_graph + # Setting non-directed graph to directed Graph + directed_graph = self.directed_graph() + non_directed_graph = self.non_directed_graph() + directed_graph.set_graph(non_directed_graph) + assert directed_graph.get_graph() == non_directed_graph + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_add_edge(self): + """ + Description: + Check add_edge method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if edges attribute returned after add_edge method has expected values + + Steps + 1. Check add_edge method by adding equal edge to non-directed graph (expected, that edges not changed) + 2. Check add_edge method by adding reversed edge to non-directed graph (expected, that edges not changed) + 3. Check add_edge method by adding unequal edge to non-directed graph + 4. Check add_edge method by adding equal edge to directed graph + 5. Check add_edge method by adding reversed edge to directed graph + 6. Check add_edge method by adding unequal edge to directed graph + """ + # Scenario for non-directed graph + non_directed_graph = self.non_directed_graph() + non_directed_graph_edges = [ + (1, 2, {"value": 2}), + (2, 3, {"value": 3}), + (3, 4, {"value": 4}), + ] + # Adding equal edge to non-directed graph + non_directed_graph.add_edge(node1=2, node2=3, edge_value=3) + assert list(non_directed_graph.edges) == non_directed_graph_edges + # Adding equal reversed edge to non-directed graph + non_directed_graph.add_edge(node1=3, node2=2, edge_value=3) + assert list(non_directed_graph.edges) == non_directed_graph_edges + # Adding unequal edge with existing nodes to non-directed graph + non_directed_graph.add_edge(node1=1, node2=3, edge_value=1) + # Adding unequal edge with new node to non-directed graph + non_directed_graph.add_edge(node1=4, node2=5, edge_value=2) + assert list(non_directed_graph.edges) == [ + (1, 2, {"value": 2}), + (1, 3, {"value": 1}), + (2, 3, {"value": 3}), + (3, 4, {"value": 4}), + (4, 5, {"value": 2}), + ] + # Scenario for directed graph + directed_graph = self.directed_graph() + # Adding equal edge to directed graph + directed_graph.add_edge(node1="C", node2="D", edge_value=3) + assert list(directed_graph.edges) == [ + ("A", "B", 0, {"value": 1}), + ("A", "D", 0, {"value": 4}), + ("B", "C", 0, {"value": 2}), + ("C", "D", 0, {"value": 3}), + ("C", "D", 1, {"value": 3}), + ("D", "A", 0, {"value": 5}), + ] + # Adding equal reversed edge to directed graph + directed_graph.add_edge(node1="B", node2="A", edge_value=2) + assert list(directed_graph.edges) == [ + ("A", "B", 0, {"value": 1}), + ("A", "D", 0, {"value": 4}), + ("B", "C", 0, {"value": 2}), + ("B", "A", 0, {"value": 2}), + ("C", "D", 0, {"value": 3}), + ("C", "D", 1, {"value": 3}), + ("D", "A", 0, {"value": 5}), + ] + # Adding unequal edge with new nodes to non-directed graph + directed_graph.add_edge(node1="E", node2="F") + assert list(directed_graph.edges) == [ + ("A", "B", 0, {"value": 1}), + ("A", "D", 0, {"value": 4}), + ("B", "C", 0, {"value": 2}), + ("B", "A", 0, {"value": 2}), + ("C", "D", 0, {"value": 3}), + ("C", "D", 1, {"value": 3}), + ("D", "A", 0, {"value": 5}), + ("E", "F", 0, {"value": None}), + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_has_edge_between(self): + """ + Description: + Check has_edge_between method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if has_edge_between method returns expected value + + Steps + 1. Check value returned by has_edge_between method for non-directed graph + 2. Check value returned by has_edge_between method for directed graph + """ + # Scenario for non-directed graph + non_directed_graph = self.non_directed_graph() + for node_1, node_2 in ([1, 2], [2, 1], [2, 3], [3, 2], [3, 4], [4, 3]): + assert non_directed_graph.has_edge_between(node_1, node_2) + for node_1, node_2 in ([1, 3], [3, 1], [1, 4], [4, 1], [2, 4], [4, 2]): + assert not non_directed_graph.has_edge_between(node_1, node_2) + # Scenario for directed graph + directed_graph = self.directed_graph() + for node_1, node_2 in ( + ["B", "A"], + ["C", "B"], + ["D", "C"], + ["A", "D"], + ["D", "A"], + ): + assert directed_graph.has_edge_between(node_1, node_2) + for node_1, node_2 in ( + ["A", "B"], + ["B", "C"], + ["C", "D"], + ["A", "C"], + ["C", "A"], + ["B", "D"], + ["D", "B"], + ): + assert not directed_graph.has_edge_between(node_1, node_2) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_neighbors(self): + """ + Description: + Check neighbors method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if neighbors method returns expected value + + Steps + 1. Check value returned by neighbors method for non-directed graph + 2. Check value returned by neighbors method for directed graph + """ + # Scenario for non-directed graph + non_directed_graph = self.non_directed_graph() + for node, expected_neighbors in ([1, [2]], [2, [1, 3]], [3, [2, 4]], [4, [3]]): + assert non_directed_graph.neighbors(node) == expected_neighbors + assert non_directed_graph.neighbors(5) == [] + # Scenario for directed graph + directed_graph = self.directed_graph() + for node, expected_neighbors in ( + ["A", ["B", "D"]], + ["B", ["C"]], + ["C", ["D"]], + ["D", ["A"]], + ): + assert directed_graph.neighbors(node) == expected_neighbors + assert directed_graph.neighbors(1) == [] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_find_out_edges(self): + """ + Description: + Check find_out_edges method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if find_out_edges method returns expected value + + Steps + 1. Check empty list returned by find_out_edges method for non-directed graph + 2. Check value returned by find_out_edges method for directed graph + """ + # Scenario for non-directed graph + non_directed_graph = self.non_directed_graph() + for node in [1, 2, 3, 4]: + assert non_directed_graph.find_out_edges(node) == [] + # Scenario for directed graph + directed_graph = self.directed_graph() + for node, expected_edges in ( + ["A", [("A", "B"), ("A", "D")]], + ["B", [("B", "C")]], + ["C", [("C", "D")]], + ["D", [("D", "A")]], + ): + assert list(directed_graph.find_out_edges(node)) == expected_edges + with pytest.raises(KeyError): + directed_graph.find_out_edges(1) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_find_in_edges(self): + """ + Description: + Check find_in_edges method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if find_in_edges method returns expected value + + Steps + 1. Check empty list returned by find_in_edges method for non-directed graph + 2. Check value returned by find_in_edges method for directed graph + """ + # Scenario for non-directed graph + non_directed_graph = self.non_directed_graph() + for node in [1, 2, 3, 4]: + assert non_directed_graph.find_in_edges(node) == [] + # Scenario for directed graph + directed_graph = self.directed_graph() + for node, expected_edges in ( + ["A", [("D", "A")]], + ["B", [("A", "B")]], + ["C", [("B", "C")]], + ["D", [("C", "D"), ("A", "D")]], + ): + assert list(directed_graph.find_in_edges(node)) == expected_edges + with pytest.raises(KeyError): + directed_graph.find_in_edges(1) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_find_cliques(self): + """ + Description: + Check find_cliques method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if find_cliques method returns generator object with expected values + + Steps + 1. Check value returned by find_cliques generator for non-directed graph + 2. Check NetworkXNotImplemented exception raised when find_cliques method used for directed graph + """ + # Scenario for non-directed graph + non_directed_graph_cliques = self.non_directed_graph().find_cliques() + for expected_clique in ([2, 1], [2, 3], [4, 3]): + assert next(non_directed_graph_cliques) == expected_clique + # Scenario for directed graph + with pytest.raises(NetworkXNotImplemented): + # this is for networkx<2.6.0 + cliques = self.directed_graph().find_cliques() + if next(cliques, None): + # this is for networkx>=2.6.0 + raise NetworkXNotImplemented + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @pytest.mark.filterwarnings("ignore::PendingDeprecationWarning") + def test_graph_num_labels(self): + """ + Description: + Check num_labels property of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if num_labels property returns expected value + + Steps + 1. Check value returned by num_labels property for non-directed graph + 2. Check value returned by num_labels property for directed graph + """ + for empty_graph in (Graph(), Graph(directed=True)): + assert empty_graph.num_labels == 0 + # Scenario for non-directed graph + non_directed_graph = self.non_directed_graph() + assert non_directed_graph.num_labels == 4 + # Checking num_labels property after adding edge with non existing nodes to non-directed graph + non_directed_graph.add_edge(5, 6) + assert non_directed_graph.num_labels == 6 + # Checking num_labels property after removing edge (nodes not removed) from non-directed graph + non_directed_graph.remove_edges(5, 6) + assert non_directed_graph.num_labels == 6 + # Checking num_labels property after removing node from non-directed graph + non_directed_graph.remove_node(1) + assert non_directed_graph.num_labels == 5 + # Scenario for directed graph + directed_graph = self.directed_graph() + assert directed_graph.num_labels == 4 + # Checking num_labels property after adding edge with one existing node to directed graph + directed_graph.add_edge(node1="A", node2="E", edge_value=7) + assert directed_graph.num_labels == 5 + # Checking num_labels property after removing edge (nodes not removed) from non-directed graph + directed_graph.remove_edges(node1="A", node2="B") + assert directed_graph.num_labels == 5 + # Checking num_labels property after removing node from non-directed graph + directed_graph.remove_node("E") + assert directed_graph.num_labels == 4 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_remove_edges(self): + """ + Description: + Check remove_edges method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if returned expected value of edges property after using remove_edges method on Graph class object + + Steps + 1. Check value returned by edges property after using remove_edges method by non-directed Graph class object + 2. Check value returned by edges property after using remove_edges method by directed Graph class object + """ + # Scenario for non-directed graph + non_directed_graph = self.non_directed_graph() + # Removing two existing edges from non-directed graph + non_directed_graph.remove_edges(1, 2) + non_directed_graph.remove_edges(3, 2) + assert list(non_directed_graph.edges) == [(3, 4, {"value": 4})] + # Checking that NetworkXError exception raised when trying to remove non existing edges from non-directed graph + for node_1, node_2 in ([1, 2], [1, 3], [5, 6]): + with pytest.raises(NetworkXError): + non_directed_graph.remove_edges(node_1, node_2) + # Checking "edges" property of non-directed graph after adding edge which were removed in previous scenario + non_directed_graph.add_edge(node1=1, node2=2, edge_value=2) + assert list(non_directed_graph.edges) == [ + (1, 2, {"value": 2}), + (3, 4, {"value": 4}), + ] + # Scenario for directed graph + directed_graph = self.directed_graph() + # Removing two existing edges from directed graph, edge "D"-"A" - multi directional + directed_graph.remove_edges("A", "B") + directed_graph.remove_edges("D", "A") + assert list(directed_graph.edges) == [ + ("A", "D", 0, {"value": 4}), + ("B", "C", 0, {"value": 2}), + ("C", "D", 0, {"value": 3}), + ] + # Checking that NetworkXError exception raised when trying to remove non existing edges from directed graph + for node_1, node_2 in (["A", "B"], ["A", "C"], ["E", "F"]): + with pytest.raises(NetworkXError): + non_directed_graph.remove_edges(node_1, node_2) + # Checking "edges" property of directed graph after adding edge which were removed in previous scenario + directed_graph.add_edge(node1="D", node2="A", edge_value=5) + assert list(directed_graph.edges) == [ + ("A", "D", 0, {"value": 4}), + ("B", "C", 0, {"value": 2}), + ("C", "D", 0, {"value": 3}), + ("D", "A", 0, {"value": 5}), + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_remove_node(self): + """ + Description: + Check remove_node method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if returned expected values of nodes property and num_nodes method after using remove_node method on + Graph class object + + Steps + 1. Check values returned by nodes and edges properties and num_nodes method after using remove_node method by + non-directed Graph class object + 2. Check values returned by nodes and edges properties and num_nodes method after using remove_node method by + directed Graph class object + """ + # Scenario for non-directed graph + non_directed_graph = self.non_directed_graph() + # Removing node that connected to one node of non-directed graph + non_directed_graph.remove_node(1) + assert non_directed_graph.num_nodes() == 3 + self.check_graph_list_attributes( + [ + { + "attribute": non_directed_graph.edges, + "expected_type": EdgeDataView, + "expected_value": [(2, 3, {"value": 3}), (3, 4, {"value": 4})], + }, + { + "attribute": non_directed_graph.nodes, + "expected_type": NodeView, + "expected_value": [2, 3, 4], + }, + ] + ) + # Checking that NetworkXError exception raised when removing node non existing nodes from non-directed graph + for node in [1, 5]: + with pytest.raises(NetworkXError): + non_directed_graph.remove_node(node) + # Removing node that connected to two nodes of non-directed graph + non_directed_graph.remove_node(3) + assert non_directed_graph.num_nodes() == 2 + self.check_graph_list_attributes( + [ + { + "attribute": non_directed_graph.edges, + "expected_type": EdgeDataView, + "expected_value": [], + }, + { + "attribute": non_directed_graph.nodes, + "expected_type": NodeView, + "expected_value": [2, 4], + }, + ] + ) + # Scenario for directed graph + directed_graph = self.directed_graph() + # Removing node that connected to two nodes of non-directed graph and has multi-direction edge + directed_graph.remove_node("A") + assert directed_graph.num_nodes() == 3 + self.check_graph_list_attributes( + [ + { + "attribute": directed_graph.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": [ + ("B", "C", 0, {"value": 2}), + ("C", "D", 0, {"value": 3}), + ], + }, + { + "attribute": directed_graph.nodes, + "expected_type": NodeView, + "expected_value": ["B", "C", "D"], + }, + ] + ) + # Checking that NetworkXError exception raised when removing node non existing nodes from directed graph + for node in ["A", "E"]: + with pytest.raises(NetworkXError): + directed_graph.remove_node(node) + # Removing node that connected to two nodes of directed graph, it causes removal of all edges + directed_graph.remove_node("C") + assert non_directed_graph.num_nodes() == 2 + self.check_graph_list_attributes( + [ + { + "attribute": directed_graph.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": [], + }, + { + "attribute": directed_graph.nodes, + "expected_type": NodeView, + "expected_value": ["B", "D"], + }, + ] + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_num_nodes(self): + """ + Description: + Check num_nodes method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if num_nodes method returns expected value + + Steps + 1. Check values returned by num_nodes method for non-directed Graph class object + 2. Check values returned by num_nodes method for directed Graph class object + """ + for empty_graph in [Graph(), Graph(directed=True)]: + assert empty_graph.num_nodes() == 0 + # Scenario for non-directed graph + non_directed_graph = self.non_directed_graph() + assert non_directed_graph.num_nodes() == 4 + # Checking num_nodes method after adding one node to non-directed graph + non_directed_graph.add_node(5) + assert non_directed_graph.num_nodes() == 5 + # Checking num_nodes method after adding edge with one existing node to non-directed graph + non_directed_graph.add_edge(1, 6) + assert non_directed_graph.num_nodes() == 6 + # Checking num_nodes method after adding edge with non-existing nodes to non-directed graph + non_directed_graph.add_edge(7, 8) + assert non_directed_graph.num_nodes() == 8 + # Checking num_nodes method after removing node from non-directed graph + non_directed_graph.remove_node(1) + assert non_directed_graph.num_nodes() == 7 + # Checking num_nodes method after removing edge from non-directed graph + non_directed_graph.remove_edges(2, 3) + assert non_directed_graph.num_nodes() == 7 + # Scenario for directed graph + directed_graph = self.directed_graph() + assert directed_graph.num_nodes() == 4 + # Checking num_nodes method after adding one node to directed graph + directed_graph.add_node("E") + assert directed_graph.num_nodes() == 5 + # Checking num_nodes method after adding edge with one existing node to directed graph + directed_graph.add_edge("A", "F") + assert directed_graph.num_nodes() == 6 + # Checking num_nodes method after adding edge with non-existing nodes to directed graph + directed_graph.add_edge("G", "H") + assert directed_graph.num_nodes() == 8 + # Checking num_nodes method after removing node from directed graph + directed_graph.remove_node("A") + assert directed_graph.num_nodes() == 7 + # Checking num_nodes method after removing edge from non-directed graph + directed_graph.remove_edges("B", "C") + assert directed_graph.num_nodes() == 7 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_eq(self): + """ + Description: + Check __eq__ method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if __eq__ method returns expected value + + Steps + 1. Check values returned by __eq__ method for non-directed Graph class object + 2. Check values returned by __eq__ method for directed Graph class object + """ + non_directed_graph = Graph() + directed_graph = Graph(directed=True) + unequal_nodes_graph = Graph() + unequal_edges_graph = Graph() + equal_non_directed_graph = Graph() + graph_edges_list = [ + {"node1": 1, "node2": 2, "edge_value": 1}, + {"node1": 2, "node2": 3, "edge_value": 2}, + ] + for no_edges_graph in [ + non_directed_graph, + directed_graph, + equal_non_directed_graph, + unequal_nodes_graph, + unequal_edges_graph, + ]: + self.add_edges_to_graph(no_edges_graph, graph_edges_list) + + # Checking __eq__ method for equal non-directed graphs + assert non_directed_graph == equal_non_directed_graph + # Checking __eq__ method for non-directed graphs with unequal directed attributes + assert non_directed_graph != directed_graph + # Checking __eq__ method for non-directed graphs with unequal nodes + unequal_nodes_graph.add_node(4) + assert non_directed_graph != unequal_nodes_graph + # Checking __eq__ method for non-directed graphs with unequal edges + unequal_edges_graph.add_edge(node1=1, node2=3, edge_value=4) + assert non_directed_graph != unequal_edges_graph + # Checking __eq__ method by comparing non-directed Graph with object of other type + assert non_directed_graph != str + # Check for non-directed graph + equal_directed_graph = Graph(directed=True) + unequal_nodes_graph = Graph(directed=True) + unequal_edges_graph = Graph(directed=True) + for no_edges_graph in [ + equal_directed_graph, + unequal_nodes_graph, + unequal_edges_graph, + ]: + self.add_edges_to_graph(no_edges_graph, graph_edges_list) + # Checking __eq__ method for equal directed graphs + assert directed_graph == equal_directed_graph + # Checking __eq__ method for directed graphs with unequal directed attributes + assert directed_graph != non_directed_graph + # Checking __eq__ method for directed graphs with unequal nodes + unequal_nodes_graph.add_node(4) + assert directed_graph != unequal_nodes_graph + # Checking __eq__ method for directed graphs with unequal edges + unequal_edges_graph.add_edge(2, 1, 4) + assert directed_graph != unequal_edges_graph + # Checking __eq__ method by comparing directed Graph with object of other type + assert directed_graph != str + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_graph_descendants(self): + """ + Description: + Check descendants method of Graph class object + + Input data: + Graph objects with specified "directed" and "edges" parameters + + Expected results: + Test passes if descendants method returns expected value + + Steps + 1. Check values returned by descendants method for non-directed Graph class object + 2. Check values returned by descendants method for directed Graph class object + """ + # Scenario for non-directed graph + non_directed_graph = self.non_directed_graph() + assert non_directed_graph.descendants(1) == [1, 2, 3] + assert non_directed_graph.descendants(2) == [2, 2, 3] + assert non_directed_graph.descendants(3) == [3, 2, 3] + assert non_directed_graph.descendants(4) == [4, 3, 2] + # Checking descendants after removing node from non-directed graph + non_directed_graph.remove_node(2) + assert non_directed_graph.descendants(1) == [] + assert non_directed_graph.descendants(2) == [] + assert non_directed_graph.descendants(3) == [3] + assert non_directed_graph.descendants(4) == [4] + # Scenario for directed graph + directed_graph = self.directed_graph() + assert directed_graph.descendants("A") == ["D", "C", "B", "A", "A"] + assert directed_graph.descendants("B") == ["A", "D", "C", "B", "A"] + assert directed_graph.descendants("C") == ["B", "A", "D", "C", "A"] + assert directed_graph.descendants("D") == ["C", "B", "A", "D", "A"] + # Checking descendants after removing node from directed graph + directed_graph.remove_node("B") + assert directed_graph.descendants("A") == ["D", "C", "A"] + assert directed_graph.descendants("B") == [] + assert directed_graph.descendants("C") == [] + assert directed_graph.descendants("D") == ["C", "A", "D"] + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestMultiDiGraph: + @staticmethod + def multi_di_graph(): + multi_di_graph = MultiDiGraph() + TestGraph.add_edges_to_graph( + multi_di_graph, + [ + {"node1": (1, 1), "node2": (1, 2)}, + {"node1": (1, 2), "node2": (3, 1)}, + {"node1": (3, 1), "node2": (2, 1)}, + ], + ) + return multi_di_graph + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @pytest.mark.filterwarnings("ignore::PendingDeprecationWarning") + def test_multi_di_graph(self): + """ + Description: + Check MultiDiGraph class object initialization + + Input data: + MultiDiGraph object with specified "edges" parameter + + Expected results: + Test passes if MultiDiGraph object "directed" attribute, "edges", "nodes" and "num_labels" properties and + "num_nodes" method return expected values + """ + multi_di_graph = self.multi_di_graph() + TestGraph.check_graph_non_list_attributes( + [ + {"attribute": multi_di_graph.directed, "expected_value": True}, + {"attribute": multi_di_graph.num_labels, "expected_value": 4}, + {"attribute": multi_di_graph.num_nodes(), "expected_value": 4}, + ] + ) + TestGraph.check_graph_list_attributes( + [ + { + "attribute": multi_di_graph.nodes, + "expected_type": NodeView, + "expected_value": [(1, 1), (1, 2), (3, 1), (2, 1)], + }, + { + "attribute": multi_di_graph.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": [ + ((1, 1), (1, 2), 0, {"value": None}), + ((1, 2), (3, 1), 0, {"value": None}), + ((3, 1), (2, 1), 0, {"value": None}), + ], + }, + ] + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_multi_di_graph_topological_sort(self): + """ + Description: + Check topological_sort method of MultiDiGraph class object + + Input data: + MultiDiGraph objects with specified "edges" parameter + + Expected results: + Test passes if topological_sort method returns generator object with expected values + """ + multi_di_graph = self.multi_di_graph() + topological_sort = multi_di_graph.topological_sort() + for expected_value in [(1, 1), (1, 2), (3, 1), (2, 1)]: + assert next(topological_sort) == expected_value diff --git a/tests/unit/api/entities/test_id.py b/tests/unit/api/entities/test_id.py new file mode 100644 index 00000000000..8e2a2195afa --- /dev/null +++ b/tests/unit/api/entities/test_id.py @@ -0,0 +1,48 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import pytest +from bson import ObjectId + +from otx.api.entities.id import ID +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestID: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_id(self): + """ + Description: + Check ID class object initialization + + Input data: + ID object with specified representation parameter + + Expected results: + Test passes if ID object representation property and __repr__ method return expected values + + Steps + 1. Check representation property and __repr__ method for ID object with not specified representation parameter + 2. Check representation property and __repr__ method for ID object with ObjectId class representation parameter + 3. Check representation property and __repr__ method for ID object with str type representation parameter + """ + # Scenario for ID object with not specified representation parameter + no_representation_id = ID() + assert no_representation_id.representation == "" + assert repr(no_representation_id.representation) == "ID()" + # Scenario for ID object with ObjectId class representation parameter + expected_oid = "61a8b869fb7665916a39eb95" + oid_representation = ObjectId(expected_oid) + oid_representation_id = ID(oid_representation) + assert oid_representation_id.representation == "61a8b869fb7665916a39eb95" + assert repr(oid_representation_id.representation) == "ID(61a8b869fb7665916a39eb95)" + # Scenario for ID object with str-type representation parameter + str_representation = " String-type representation ID_1 " + str_representation_id = ID(str_representation) + # Leading and trailing whitespaces should be removed, only uppercase letters should be replaced by lowercase + assert str_representation_id.representation == "string-type representation id_1" + assert repr(str_representation_id) == "ID(string-type representation id_1)" diff --git a/tests/unit/api/entities/test_image.py b/tests/unit/api/entities/test_image.py new file mode 100644 index 00000000000..90f22c69640 --- /dev/null +++ b/tests/unit/api/entities/test_image.py @@ -0,0 +1,143 @@ +"""This module tests classes related to image""" + +# Copyright (C) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +import os +import tempfile + +import cv2 +import numpy as np +import pytest + +from otx.api.entities.annotation import Annotation +from otx.api.entities.image import Image +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.rectangle import Rectangle +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestImage: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_image(self): + """ + Description: + To test Image class + + Input data: + Instances of Image sourced with data or file path to forged image or both + Rectangle and Ellipse classes instances + Instances of Annotation made against rectangle and Ellipsis + + Expected results: + 1. Value error is raised + 2. Value error is raised + 3. Created successfully + 4. Created successfully + 5. Calls of the method against both instances return expected strings + 6. Numpy property contains expected data + 7. Numpy property is changed and contains expected data + 8. Called method returns expected value + 9. Value error raises + 10. Value error raises + 11. Called method returns expected value + 12. Height property contains expected data + 13. Width property contains expected data + + Steps + 1. Create Image without parameters + 2. Create Image with both parameters + 3. Create Image with data parameter + 4. Create Image with path to forged image + 5. Check __str__ method against data instance and fp instance + 6. Check numpy property against data instance and fp instance + 7. Change numpy property and check it against data instance and fp instance + 8. Check roi_numpy method against data instance and fp instance without Annotation + 9. Check roi_numpy method against data instance and fp instance with Annotation of Ellipsis shape + 10. Check roi_numpy method against 1 dimensional data instance with Annotation of Rectangle shape + 11. Check roi_numpy property against data instance and fp instance with Annotation of Rectangle shape + 12. Check height property against data instance and fp instance + 13. Check width property against data instance and fp instance + """ + test_height = test_width = 128 + test_height1 = test_width1 = 64 + test_depth = 4 + data0 = np.random.random_sample((test_height, test_width, test_depth)) + data1 = np.random.random_sample((test_height1, test_width1, test_depth)) + d1_data = np.random.random_sample((test_height1,)) + image = np.random.random_sample((test_height, test_width, test_depth)) + image_path = os.path.join(tempfile.gettempdir(), "test_image.png") + cv2.imwrite(image_path, image) + + with pytest.raises(ValueError): + Image() + + with pytest.raises(ValueError): + Image(data=data0, file_path=image_path) + + data_instance = Image(data=data0) + assert isinstance(data_instance, Image) + + fp_instance = Image(file_path=image_path) + assert isinstance(fp_instance, Image) + + assert str(data_instance) == f"Image(with data, width={test_width}, height={test_height})" + assert str(fp_instance) == f"Image({image_path}, width={test_width}, height={test_height})" + + assert np.array_equal(data_instance.numpy, data0) + height, width, depth = fp_instance.numpy.shape + assert height == test_height + assert width == test_width + assert depth != test_depth + + data_instance.numpy = data1 + fp_instance.numpy = data1 + + assert np.array_equal(data_instance.numpy, data1) + assert np.array_equal(fp_instance.numpy, data1) + + assert np.array_equal(data_instance.roi_numpy(), data1) + assert np.array_equal(fp_instance.roi_numpy(), data1) + + rec_shape = Rectangle(x1=0.0, x2=0.5, y1=0.0, y2=0.5) + ellipsis_shape = Ellipse(x1=0.0, x2=0.5, y1=0.0, y2=0.5) + label = [] + rect_annotation = Annotation(rec_shape, label) + ellipsis_annotation = Annotation(ellipsis_shape, label) + + with pytest.raises(ValueError): + data_instance.roi_numpy(ellipsis_annotation) + + with pytest.raises(ValueError): + fp_instance.roi_numpy(ellipsis_annotation) + + d1_data_instance = Image(data=d1_data) + + with pytest.raises(ValueError): + d1_data_instance.roi_numpy(rect_annotation) + + for arr in data_instance.roi_numpy(rect_annotation): + for it in arr: + assert it in data1 + + assert data_instance.height == fp_instance.height == test_height1 + assert data_instance.width == fp_instance.width == test_width1 + + if os.path.exists(image_path): + os.remove(image_path) diff --git a/tests/unit/api/entities/test_inference_parameters.py b/tests/unit/api/entities/test_inference_parameters.py new file mode 100644 index 00000000000..2d2e257f261 --- /dev/null +++ b/tests/unit/api/entities/test_inference_parameters.py @@ -0,0 +1,92 @@ +"""This module tests classes related to InferenceParameters""" + +# Copyright (C) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import dataclasses + +import pytest + +from otx.api.entities.inference_parameters import InferenceParameters +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestInferenceParameters: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_inference_parameters_members(self): + """ + Description: + To test InferenceParameters dataclass members + + Input data: + Initialized instance of InferenceParameters class + + Expected results: + + Steps + 1. Create InferenceParameters + 2. Check members + """ + infer_params = InferenceParameters() + + assert dataclasses.is_dataclass(infer_params) + assert len(dataclasses.fields(infer_params)) == 6 + assert dataclasses.fields(infer_params)[0].name == "is_evaluation" + assert dataclasses.fields(infer_params)[1].name == "update_progress" + assert dataclasses.fields(infer_params)[2].name == "explainer" + assert dataclasses.fields(infer_params)[3].name == "process_saliency_maps" + assert dataclasses.fields(infer_params)[4].name == "explain_predicted_classes" + assert dataclasses.fields(infer_params)[5].name == "enable_async_inference" + assert type(infer_params.is_evaluation) is bool + assert type(infer_params.process_saliency_maps) is bool + assert type(infer_params.explain_predicted_classes) is bool + assert callable(infer_params.update_progress) + assert type(infer_params.explainer) is str + with pytest.raises(AttributeError): + str(infer_params.WRONG) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_inference_parameters_update_member(self): + """ + Description: + To test InferenceParameters dataclass members update + + Input data: + Initialized instance of InferenceParameters class + + Expected results: + + Steps + 1. Create InferenceParameters + 2. Check members update + """ + infer_params = InferenceParameters(False) + assert infer_params.is_evaluation is False + assert ( + infer_params.update_progress(-2147483648) is infer_params.update_progress(0) + and infer_params.update_progress(2147483648) is None + ) + + infer_params = InferenceParameters(True) + assert infer_params.is_evaluation is True + assert ( + infer_params.update_progress(-2147483648) is infer_params.update_progress(0) + and infer_params.update_progress(2147483648) is None + ) diff --git a/tests/unit/api/entities/test_label.py b/tests/unit/api/entities/test_label.py new file mode 100644 index 00000000000..a277aecf3be --- /dev/null +++ b/tests/unit/api/entities/test_label.py @@ -0,0 +1,182 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import pytest + +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.utils.time_utils import now +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDomain: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_domain(self): + """ + Description: + Check the Domain can correctly return the value + + Expected results: + Test passes if the results match + """ + domain = Domain + assert len(domain) == 12 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLabelEntity: + + creation_date = now() + + label_car_params = { + "name": "car", + "domain": Domain.DETECTION, + "color": Color(255, 0, 0), + "hotkey": "ctrl+1", + "creation_date": creation_date, + "is_empty": False, + "id": ID(123456789), + } + + label_person_params = { + "name": "person", + "domain": Domain.DETECTION, + "color": Color(255, 17, 17), + "hotkey": "ctrl+2", + "creation_date": creation_date, + "is_empty": False, + "id": ID(987654321), + } + car = LabelEntity(**label_car_params) # type: ignore + empty = LabelEntity(name="empty", domain=Domain.SEGMENTATION, is_empty=True) + person = LabelEntity(**label_person_params) # type: ignore + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_entity(self): + """ + Description: + Check the LabelEntity can correctly return the value + + Input data: + Dummy data + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Use already created dummy data + 2. Check the processing of default values + 3. Check the processing of changed values + """ + + assert self.car == LabelEntity(**self.label_car_params) + assert self.car != Domain + assert self.car != self.person + + for attr in [ + "name", + "domain", + "color", + "hotkey", + "creation_date", + "is_empty", + "id", + ]: + assert getattr(self.car, attr) == self.label_car_params[attr] + + label_car_new_name = "electric car" + label_car_new_domain = Domain.CLASSIFICATION + label_car_new_color = Color(0, 255, 0) + label_car_new_hotkey = "ctrl+2" + label_car_new_id = ID(987654321) + + setattr(self.car, "name", label_car_new_name) + setattr(self.car, "domain", label_car_new_domain) + setattr(self.car, "color", label_car_new_color) + setattr(self.car, "hotkey", label_car_new_hotkey) + setattr(self.car, "id", label_car_new_id) + + assert self.car.name == label_car_new_name + assert self.car.domain == label_car_new_domain + assert self.car.color == label_car_new_color + assert self.car.hotkey == label_car_new_hotkey + assert self.car.id_ == label_car_new_id + + test_label_entity_repr = [ + f"{self.car.id_}", + f"name={self.car.name}", + f"hotkey={self.car.hotkey}", + f"domain={self.car.domain}", + f"color={self.car.color}", + ] + + for i in test_label_entity_repr: + assert i in self.car.__repr__() + + assert hash(self.car) == hash(str(self.car)) + assert self.car.__lt__(Domain) is False + assert self.car.__gt__(Domain) is False + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_empty_label_entity(self): + """ + Description: + Check the LabelEntity can correctly return the value for empty label + + Input data: + Dummy data + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Use already created dummy data + 2. Check the processing of default values + """ + + assert self.empty.hotkey == "" + assert self.empty.id_ == ID() + assert type(self.empty.color) == Color + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_comparison(self): + """ + Description: + Check the LabelEntity __lt__, __gt__ methods with changed id + + Input data: + Dummy data + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Use already created dummy data + 2. Check the processing of changed id + """ + + self.empty.id_ = ID(999999999) + assert self.empty > self.car + assert self.car < self.empty diff --git a/tests/unit/api/entities/test_label_schema.py b/tests/unit/api/entities/test_label_schema.py new file mode 100644 index 00000000000..bcfc30ccf50 --- /dev/null +++ b/tests/unit/api/entities/test_label_schema.py @@ -0,0 +1,1939 @@ +"""This module tests classes related to LabelSchema""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from networkx.classes.reportviews import NodeView, OutMultiEdgeDataView + +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import Domain +from otx.api.entities.label_schema import ( + LabelEntity, + LabelGroup, + LabelGroupDoesNotExistException, + LabelGroupExistsException, + LabelGroupType, + LabelSchemaEntity, + LabelTree, + ScoredLabel, + natural_sort_label_id, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +def get_label_entity(id_val: str): + return LabelEntity(name=id_val, domain=Domain.DETECTION, id=ID(id_val)) + + +def get_scored_label(id_val: str): + return ScoredLabel(label=get_label_entity(id_val)) + + +@pytest.mark.priority_medium +@pytest.mark.unit +@pytest.mark.reqids(Requirements.REQ_1) +@pytest.mark.parametrize("id_val", ["3", "fake1name2"]) +@pytest.mark.parametrize("target_class", [ID, get_label_entity, get_scored_label]) +def test_natural_sort_label_id(id_val: str, target_class): + target = target_class(id_val) + + if id_val.isdecimal(): + assert natural_sort_label_id(target) == ["", int(id_val)] + else: + assert natural_sort_label_id(target) == [id_val] + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLabelSchema: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_tree(self, label_schema_example): + """ + Description: + Check that childs and parents of Labels can be found correctly + + Input data: + A simple LabelTree + + Expected results: + Test passes if the correct parent and child labels can be found + + Steps + 1. Create LabelTree + 2. Find parents of Labels + 3. Find children of Label + """ + threat = label_schema_example.new_label_by_name("threat") + benign = label_schema_example.new_label_by_name("benign") + gun = label_schema_example.new_label_by_name("gun") + rifle = label_schema_example.new_label_by_name("rifle") + bottle = label_schema_example.new_label_by_name("bottle") + + label_tree = LabelTree() + label_tree.add_child(threat, gun) + label_tree.add_child(threat, rifle) + label_tree.add_child(benign, bottle) + + assert label_tree.get_parent(gun) == threat + assert label_tree.get_parent(rifle) == threat + assert label_tree.get_parent(bottle) == benign + assert label_tree.get_parent(threat) is None + + threat_children = label_tree.get_children(threat) + assert rifle in threat_children + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_exclusive(self, label_schema_example): + """ + Description: + Tests that the are_exclusive method properly computes the labels that are exclusive with each other + + Input data: + Exclusive group: flowering,vegetative,no_plant + Exclusive group: bee + + Expected results: + Test passes if the exclusive groups are correctly saved and retrieved + + Steps + 1. Create LabelRelation with exclusive group + 2. Check that relations are exclusive + 3. Check that number of relations is correct + 4. Add new exclusive group + 5. Check that exclusive relations still resolve correctly + """ + flowering = label_schema_example.flowering + no_plant = label_schema_example.no_plant + vegetative = label_schema_example.vegetative + + label_schema = LabelSchemaEntity() + label_schema.add_group( + LabelGroup( + "plant_state", + [flowering, no_plant, vegetative], + ) + ) + + assert label_schema.are_exclusive(flowering, no_plant) + assert label_schema.are_exclusive(vegetative, no_plant) + assert label_schema.are_exclusive(vegetative, flowering) + + bee = label_schema_example.new_label_by_name("bee") + label_schema.add_group(LabelGroup("bee_state", [bee], LabelGroupType.EXCLUSIVE)) + + assert not label_schema.are_exclusive(flowering, bee) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_complex(self, label_schema_example): + """ + Description: + Tests that exclusivity is computed correctly in a schema with complex hierarchy + + Input data: + Exclusive groups: plant, animal + Exclusive groups: tree, bush (children of plant), mammal, insect (children of animal) + Empty label + + Expected results: + Test passes if the are_exclusive method properly computes the exclusivity between labels with various relations + + Steps + 1. Create LabelSchemaEntity with hierarchy + 2. Check that labels are not exclusive with their parent + 3. Check that labels are exclusive with: + - labels in the same group + - children of labels in the same group + - labels in the same group as their parent + - children of labels in the same group as their parent + 4. Add an empty label and check that it's exclusive with all other labels + 5. Make the empty label the child of a different label, and check that the empty label is not exclusive with + its parent. + 6. Check that LabelSchema.are_exclusive is symmetric for all cases + + """ + label_schema = LabelSchemaEntity() + + plant = label_schema_example.new_label_by_name("plant") + animal = label_schema_example.new_label_by_name("animal") + label_schema.add_group(LabelGroup("organism_type", [plant, animal], LabelGroupType.EXCLUSIVE)) + + tree = label_schema_example.new_label_by_name("tree") + bush = label_schema_example.new_label_by_name("bush") + label_schema.add_group(LabelGroup("plant_type", [tree, bush], LabelGroupType.EXCLUSIVE)) + + label_schema.add_child(parent=plant, child=tree) + label_schema.add_child(parent=plant, child=bush) + + insect = label_schema_example.new_label_by_name("insect") + mammal = label_schema_example.new_label_by_name("mammal") + label_schema.add_group(LabelGroup("animal_type", [insect, mammal], LabelGroupType.EXCLUSIVE)) + label_schema.add_child(parent=animal, child=insect) + label_schema.add_child(parent=animal, child=mammal) + + assert not label_schema.are_exclusive(plant, tree) + assert label_schema.are_exclusive(tree, bush) + assert label_schema.are_exclusive(plant, insect) + assert label_schema.are_exclusive(tree, animal) + assert label_schema.are_exclusive(tree, insect) + + # Check that the empty label is exclusive with all labels + empty_label = label_schema_example.new_label_by_name("empty_label", is_empty=True) + label_schema.add_group(LabelGroup("empty_label", [empty_label], LabelGroupType.EMPTY_LABEL)) + for label_iter in label_schema.get_labels(include_empty=False): + assert label_schema.are_exclusive(empty_label, label_iter) + + # Check that the empty label is not exclusive with its parent + label_schema.add_child(parent=tree, child=empty_label) + assert not label_schema.are_exclusive(tree, empty_label) + + # Check that label_schema.are_exclusive is symmetric for all cases + for label_1 in label_schema.get_labels(include_empty=True): + for label_2 in label_schema.get_labels(include_empty=True): + assert label_schema.are_exclusive(label_1, label_2) == label_schema.are_exclusive(label_2, label_1) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_add_label_to_group(self, label_schema_example): + """ + Description: + Check that new labels can be added to groups + + Input data: + Empty Label Tree + + Expected results: + Test passes if an exclusive group can be added to the LabelTree + as long as a group with the same name does not exist and new labels can be added to a group. + + Steps + 1. Create LabelRelation + 2. Create new label and add to exclusive group + 3. Create new label and attemp to create new group with the same name + 4. Add label to group by group name + """ + label_schema = LabelSchemaEntity() + bee = label_schema_example.new_label_by_name("bee") # indicates presence/absence of bee + bee_state = LabelGroup("bee_state", [bee], LabelGroupType.EXCLUSIVE) + label_schema.add_group(bee_state) + + # Try to add an extra bee property as a new exclusive label group, but with the same name + # as an already existing label group + flying = label_schema_example.new_label_by_name("flying") + with pytest.raises(ValueError): + label_schema.add_group(LabelGroup("bee_state", [flying], LabelGroupType.EXCLUSIVE)) + + label_schema.add_labels_to_group_by_group_name("bee_state", [flying]) + + assert "bee_state" == bee_state.name + assert 2 == len(bee_state.labels) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_exclusivity_with_group_addition(self, label_schema_example): + """ + Description: + Check that exclusive groups work correctly + + Input data: + Exclusive Group "Vegetation" + LabelSchemaExample instance + + Expected results: + Test passes if a single label and multiple labels can be added to the exclusive group + and the correct labels can be retrieved + + Steps + 1. Create LabelSchemaEntity with "Vegetation" exclusive group + 2. Add "no_plant" to exclusive group and check that "flowering" is exclusive to "no_plant" + 3. Add "vegetative" to exclusive group and check that "vegetative" is exclusive to "no_plant" + 4. Add "flowering" to exclusive group and check that "no_plant" and "vegetative" are exclusive to "flowering" + 5. Create new LabelRelation instace + 6. Add "no_plant" and "vegetative" to exclusive group "flowering" + 7. Check that "flowering" and "vegetative" are exclusive to "no_plants" + 8. Check that no_plant" and "vegetative" are exclusive to "flowering + """ + label_schema = LabelSchemaEntity() + label_group_name = "Vegetation" + label_schema.add_group( + LabelGroup( + label_group_name, + [label_schema_example.flowering], + LabelGroupType.EXCLUSIVE, + ) + ) + + label_schema.add_labels_to_group_by_group_name(label_group_name, [label_schema_example.no_plant]) + exclusive_to_no_plants = label_schema.get_labels_exclusive_to(label_schema_example.no_plant) + assert label_schema_example.flowering in exclusive_to_no_plants + + label_schema.add_labels_to_group_by_group_name(label_group_name, [label_schema_example.vegetative]) + exclusive_to_no_plants = label_schema.get_labels_exclusive_to(label_schema_example.no_plant) + assert label_schema_example.vegetative in exclusive_to_no_plants + + exclusive_to_flowering = label_schema.get_labels_exclusive_to(label_schema_example.flowering) + assert label_schema_example.no_plant in exclusive_to_flowering + assert label_schema_example.vegetative in exclusive_to_flowering + + # new label schema (test adding multiple labels at once) + label_schema = LabelSchemaEntity() + label_group_name = "Vegetation" + label_schema.add_group( + LabelGroup( + label_group_name, + [label_schema_example.flowering], + LabelGroupType.EXCLUSIVE, + ) + ) + label_schema.add_labels_to_group_by_group_name( + label_group_name, + [label_schema_example.no_plant, label_schema_example.vegetative], + ) + + exclusive_to_no_plants = label_schema.get_labels_exclusive_to(label_schema_example.no_plant) + assert label_schema_example.flowering in exclusive_to_no_plants + assert label_schema_example.vegetative in exclusive_to_no_plants + + exclusive_to_flowering = label_schema.get_labels_exclusive_to(label_schema_example.flowering) + assert label_schema_example.no_plant in exclusive_to_flowering + assert label_schema_example.vegetative in exclusive_to_flowering + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_labelschema_equality(self, label_schema_example): + """ + Description: + Check that LabelSchemaEntity equality works correctly + + Input data: + LabelSchemaEntity instances + + Expected results: + == and != operations work correctly for various inputs + + Steps + 1. Test LabelSchemaEntity equality + """ + label_schema = LabelSchemaEntity() + + label_group_name = "Vegetation" + label_schema.add_group( + LabelGroup( + label_group_name, + [label_schema_example.flowering], + LabelGroupType.EXCLUSIVE, + ) + ) + + label_schema.add_labels_to_group_by_group_name(label_group_name, [label_schema_example.no_plant]) + + copy_schema = label_schema + assert label_schema == copy_schema + + new_schema = LabelSchemaEntity() + + label_group_name = "Vegetation" + new_schema.add_group( + LabelGroup( + label_group_name, + [label_schema_example.flowering], + LabelGroupType.EXCLUSIVE, + ) + ) + + new_schema.add_labels_to_group_by_group_name(label_group_name, [label_schema_example.vegetative]) + + assert new_schema != label_schema + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLabelGroupType: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_group_type(self): + """ + Description: + Check LabelGroupType Enum class elements + + Expected results: + Test passes if LabelGroupType Enum class length is equal to expected value and its elements have expected + sequence numbers + """ + assert len(LabelGroupType) == 2 + assert LabelGroupType.EXCLUSIVE.value == 1 + assert LabelGroupType.EMPTY_LABEL.value == 2 + + +class Labels: + def __init__(self): + self.label_0 = LabelEntity( + name="Label 0", + domain=Domain.CLASSIFICATION, + id=ID("0"), + color=Color(25, 200, 166), + ) + self.label_0_1 = LabelEntity( + name="Label 0_1", + domain=Domain.DETECTION, + id=ID("0_1"), + color=Color(40, 100, 17), + ) + self.label_0_2 = LabelEntity( + name="Label 0_2", + domain=Domain.SEGMENTATION, + id=ID("0_2"), + color=Color(30, 80, 40), + ) + self.label_0_1_3 = LabelEntity( + name="Label_0_1_3", + domain=Domain.SEGMENTATION, + id=ID("0_1_3"), + color=Color(40, 100, 17), + ) + self.label_0_2_4 = LabelEntity( + name="Label_0_2_4", + domain=Domain.SEGMENTATION, + id=ID("0_2_4"), + color=Color(30, 80, 40), + ) + self.label_0_2_5 = LabelEntity( + name="Label_0_2_5", + domain=Domain.SEGMENTATION, + id=ID("0_2_5"), + color=Color(30, 80, 40), + ) + self.no_id_label = LabelEntity(name="No ID Label", domain=Domain.SEGMENTATION) + self.non_included_label = LabelEntity(name="Label non included to group", domain=Domain.SEGMENTATION) + + +labels = Labels() + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLabelGroup: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_group_initialization(self): + """ + Description: + Check LabelGroup class object initialization + + Input data: + LabelGroup object with specified name, labels, group type and ID parameters + + Expected results: + Test passes if LabelGroup object id, labels, name and group_type attributes return expected values + + Steps + 1. Check id, labels, name and group_type attributes of LabelGroup object with not specified group_type + parameter + 2. Check id, labels, name and group_type attributes of LabelGroup object with not specified id parameter + """ + # Checking attributes of LabelGroup object with specified id and not specified group_type parameters + no_group_type_label_group = LabelGroup( + name="Type non-specified specified label group", + labels=[labels.label_0_1, labels.no_id_label, labels.label_0], + id=ID("1"), + ) + assert no_group_type_label_group.id_ == "1" + # Expected ascending sorting of labels + assert no_group_type_label_group.labels == [ + labels.no_id_label, + labels.label_0, + labels.label_0_1, + ] + assert no_group_type_label_group.name == "Type non-specified specified label group" + assert no_group_type_label_group.group_type == LabelGroupType.EXCLUSIVE + assert isinstance(no_group_type_label_group.minimum_label_id, ID) + assert no_group_type_label_group.minimum_label_id == "" + # Checking attributes of LabelGroup object with specified group_type and not specified id parameters + no_id_label_group = LabelGroup( + name="ID non-specified Label Group", + labels=[labels.label_0_1, labels.label_0], + group_type=LabelGroupType.EMPTY_LABEL, + ) + # Expected randomly generated ID object with 24 characters as "id" attribute + assert isinstance(no_id_label_group.id_, ID) + assert len(no_id_label_group.id_) == 24 + # Expected ascending sorting of labels + assert no_id_label_group.labels == [labels.label_0, labels.label_0_1] + assert no_id_label_group.name == "ID non-specified Label Group" + assert no_id_label_group.group_type == LabelGroupType.EMPTY_LABEL + assert isinstance(no_id_label_group.minimum_label_id, ID) + assert no_id_label_group.minimum_label_id == "0" + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_group_remove_label(self): + """ + Description: + Check remove_label method of LabelGroup class object + + Input data: + LabelGroup objects with specified name, labels, group type and id_parameters parameters + + Expected results: + Test passes if after using remove_label method values of "labels" property, "minimum_label_id" and + "is_single_label" methods are equal to expected + """ + label_group = LabelGroup(name="Test Label Group", labels=[labels.label_0, labels.label_0_1]) + assert not label_group.is_single_label() + # Removing first label in "labels" property and checking values of "labels", "minimum_label_id" and + # "is_single_label" + label_group.remove_label(labels.label_0) + assert label_group.labels == [labels.label_0_1] + assert label_group.minimum_label_id == "0_1" + assert label_group.is_single_label() + # Removing label that not included to LabelGroup object and repeat checks + label_group.remove_label(labels.non_included_label) + assert label_group.labels == [labels.label_0_1] + assert label_group.minimum_label_id == "0_1" + assert label_group.is_single_label() + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_group_magic_methods(self): + """ + Description: + Check __eq__ and __repr__ methods of LabelGroup class object + + Input data: + LabelGroup class object with specified name, labels, group type and ID parameters + + Expected results: + Test passes if __eq__ and __repr__ methods return expected values + + Steps + 1. Check __eq__ method for LabelGroup class objects + 2. Check __repr__ method for LabelGroup class objects + """ + group_labels = [labels.label_0, labels.label_0_1] + name = "Test Label Group" + group_id = ID("1") + label_group = LabelGroup(name=name, labels=group_labels, id=group_id) + equal_label_group = LabelGroup(name=name, labels=group_labels, id=group_id) + no_id_specified_label_group = LabelGroup(name=name, labels=group_labels) + # Checking __eq__ method for equal LabelGroup objects + assert label_group == equal_label_group + # Checking equality of LabelGroups with different "name" attributes + assert label_group == LabelGroup(name="Different name LabelGroup", labels=group_labels, id=group_id) + # Checking inequality of LabelGroups with different "id" attributes + assert not label_group == no_id_specified_label_group + # Checking inequality of LabelGroups with different "labels" attributes + assert not label_group == LabelGroup(name=name, labels=[labels.label_0], id=group_id) + # Checking inequality of LabelGroups with different "group_type" attributes + assert not label_group == LabelGroup( + name=name, + labels=group_labels, + id=group_id, + group_type=LabelGroupType.EMPTY_LABEL, + ) + # Checking inequality of LabelGroups with different object of different type + assert not label_group == str + # Checking __repr__ method for LabelGroup object with specified id + assert repr(label_group) == ( + "LabelGroup(id=1, name=Test Label Group, group_type=LabelGroupType.EXCLUSIVE, " f"labels={group_labels})" + ) + # Checking __repr__ method for LabelGroup object with [] labels and not specified id + no_labels_no_id_label_group = LabelGroup( + name="Null labels, random id group", + labels=[], + group_type=LabelGroupType.EMPTY_LABEL, + ) + generated_id = no_labels_no_id_label_group.id_ + assert repr(no_labels_no_id_label_group) == ( + f"LabelGroup(id={generated_id}, name=Null labels, random id group," + f" group_type=LabelGroupType.EMPTY_LABEL, labels=[])" + ) + + +class CommonGraphMethods: + @staticmethod + def check_graph_non_list_attributes(expected_attributes_dicts: list) -> None: + for expected_attribute_dict in expected_attributes_dicts: + assert expected_attribute_dict.get("attribute") == expected_attribute_dict.get("expected_value") + + @staticmethod + def check_graph_list_attributes(actual_expected_attributes_dicts: list) -> None: + for expected_attribute_dict in actual_expected_attributes_dicts: + attribute = expected_attribute_dict.get("attribute") + assert isinstance(attribute, expected_attribute_dict.get("expected_type")) + assert list(attribute) == expected_attribute_dict.get("expected_value") + + +class Edges: + def __init__(self): + self.edge_0_to_0_1 = (labels.label_0, labels.label_0_1) + self.edge_0_to_0_2 = (labels.label_0, labels.label_0_2) + self.edge_0_1_to_0_2 = (labels.label_0_1, labels.label_0_2) + self.edge_0_2_to_0 = (labels.label_0_2, labels.label_0) + self.edge_0_1_to_0_1_3 = (labels.label_0_1, labels.label_0_1_3) + self.edge_0_2_to_0_2_4 = (labels.label_0_2, labels.label_0_2_4) + self.edge_0_2_to_0_2_5 = (labels.label_0_2, labels.label_0_2_5) + self.edge_0_2_4_to_0_2_5 = (labels.label_0_2_4, labels.label_0_2_5) + + +edges = Edges() + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLabelTree: + @staticmethod + def label_tree_no_children() -> LabelTree: + label_tree = LabelTree() + label_tree.add_edges([edges.edge_0_to_0_1, edges.edge_0_to_0_2]) + return label_tree + + @staticmethod + def label_tree() -> LabelTree: + label_tree = LabelTree() + label_tree.get_labels_in_topological_order() + # Forming Label Tree with children + for parent, child in [ + edges.edge_0_to_0_1, + edges.edge_0_to_0_2, + edges.edge_0_1_to_0_1_3, + edges.edge_0_2_to_0_2_4, + edges.edge_0_2_to_0_2_5, + ]: + label_tree.add_child(parent, child) + return label_tree + + @staticmethod + def check_get_children_method(label_tree) -> None: + for label, expected in [ + (labels.label_0, [labels.label_0_1, labels.label_0_2]), + (labels.label_0_1, [labels.label_0_1_3]), + (labels.label_0_2, [labels.label_0_2_4, labels.label_0_2_5]), + (labels.label_0_1_3, []), + (labels.label_0_2_4, []), + (labels.label_0_2_5, []), + (LabelEntity("not included", domain=Domain.CLASSIFICATION), []), + ]: + assert label_tree.get_children(label) == expected + + @staticmethod + def check_get_descendants_method(label_tree) -> None: + for label, expected in [ + ( + labels.label_0, + [ + labels.label_0_1, + labels.label_0_1_3, + labels.label_0_2, + labels.label_0_2_4, + labels.label_0_2_5, + ], + ), + (labels.label_0_1, [labels.label_0_1_3]), + (labels.label_0_2, [labels.label_0_2_4, labels.label_0_2_5]), + (labels.label_0_1_3, []), + (labels.label_0_2_4, []), + (labels.label_0_2_5, []), + ]: + assert label_tree.get_descendants(label) == expected + + @staticmethod + def check_get_ancestors_method(label_tree) -> None: + for label, expected in [ + (labels.label_0, [labels.label_0]), + (labels.label_0_1, [labels.label_0_1, labels.label_0]), + (labels.label_0_2, [labels.label_0_2, labels.label_0]), + ( + labels.label_0_1_3, + [labels.label_0_1_3, labels.label_0_1, labels.label_0], + ), + ( + labels.label_0_2_4, + [labels.label_0_2_4, labels.label_0_2, labels.label_0], + ), + ( + labels.label_0_2_5, + [labels.label_0_2_5, labels.label_0_2, labels.label_0], + ), + ]: + assert label_tree.get_ancestors(label) == expected + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_tree_initialization(self): + """ + Description: + Check LabelTree class object initialization + + Input data: + LabelTree object, edges and nodes to add + + Expected results: + Test passes if LabelTree object "directed", "edges" and "nodes" attributes and "num_labels" and "type" + properties and value returned by "num_nodes" method are equal expected + """ + label_tree = LabelTree() + # Check for initiated non-directed LabelGraph + CommonGraphMethods().check_graph_non_list_attributes( + [ + {"attribute": label_tree.directed, "expected_value": True}, + {"attribute": label_tree.num_labels, "expected_value": 0}, + {"attribute": label_tree.num_nodes(), "expected_value": 0}, + {"attribute": label_tree.type, "expected_value": "tree"}, + ] + ) + # Check for LabelTree with added edges and nodes + label_tree.add_edges([edges.edge_0_to_0_1, edges.edge_0_to_0_2]) + CommonGraphMethods().check_graph_non_list_attributes( + [ + {"attribute": label_tree.directed, "expected_value": True}, + {"attribute": label_tree.num_labels, "expected_value": 3}, + {"attribute": label_tree.num_nodes(), "expected_value": 3}, + {"attribute": label_tree.type, "expected_value": "tree"}, + ] + ) + expected_nodes = [labels.label_0, labels.label_0_1, labels.label_0_2] + CommonGraphMethods().check_graph_list_attributes( + [ + { + "attribute": label_tree.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": [ + (labels.label_0, labels.label_0_1, 0, {}), + (labels.label_0, labels.label_0_2, 0, {}), + ], + }, + { + "attribute": label_tree.nodes, + "expected_type": NodeView, + "expected_value": expected_nodes, + }, + ] + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_tree_add_edge(self): + """ + Description: + Check LabelTree class add_edge and add_edges methods + + Input data: + LabelTree object with specified directed parameters and added edges + + Expected results: + Test passes if "edges" attribute of LabelTree is equal expected value after using add_edge and add_edges methods + """ + label_tree = self.label_tree_no_children() + # Adding edges, one of which already in LabelTree + label_tree.topological_sort() + label_tree.add_edges([edges.edge_0_to_0_1, edges.edge_0_1_to_0_1_3]) + CommonGraphMethods().check_graph_non_list_attributes( + [ + {"attribute": label_tree.num_labels, "expected_value": 4}, + {"attribute": label_tree.num_nodes(), "expected_value": 4}, + ] + ) + CommonGraphMethods().check_graph_list_attributes( + [ + { + "attribute": label_tree.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": [ + (labels.label_0, labels.label_0_1, 0, {}), + (labels.label_0, labels.label_0_1, 1, {}), + (labels.label_0, labels.label_0_2, 0, {}), + (labels.label_0_1, labels.label_0_1_3, 0, {}), + ], + }, + { + "attribute": label_tree.nodes, + "expected_type": NodeView, + "expected_value": [ + labels.label_0, + labels.label_0_1, + labels.label_0_2, + labels.label_0_1_3, + ], + }, + ] + ) + assert not label_tree._LabelTree__topological_order_cache + # Adding one existing and one non-existing edge + label_tree.topological_sort() + label_tree.add_edges([edges.edge_0_to_0_2, edges.edge_0_2_to_0_2_4]) + CommonGraphMethods().check_graph_non_list_attributes( + [ + {"attribute": label_tree.num_labels, "expected_value": 5}, + {"attribute": label_tree.num_nodes(), "expected_value": 5}, + ] + ) + CommonGraphMethods().check_graph_list_attributes( + [ + { + "attribute": label_tree.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": [ + (labels.label_0, labels.label_0_1, 0, {}), + (labels.label_0, labels.label_0_1, 1, {}), + (labels.label_0, labels.label_0_2, 0, {}), + (labels.label_0, labels.label_0_2, 1, {}), + (labels.label_0_1, labels.label_0_1_3, 0, {}), + (labels.label_0_2, labels.label_0_2_4, 0, {}), + ], + }, + { + "attribute": label_tree.nodes, + "expected_type": NodeView, + "expected_value": [ + labels.label_0, + labels.label_0_1, + labels.label_0_2, + labels.label_0_1_3, + labels.label_0_2_4, + ], + }, + ] + ) + assert not label_tree._LabelTree__topological_order_cache + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_tree_add_node(self): + """ + Description: + Check LabelTree class add_node method + + Input data: + LabelTree object with specified directed parameter and added edges + + Expected results: + Test passes if "nodes" attribute of LabelTree is equal expected value after using add_node method + """ + label_tree = self.label_tree_no_children() + # Adding new node + label_tree.get_labels_in_topological_order() + label_tree.add_node(labels.label_0_1_3) + CommonGraphMethods().check_graph_non_list_attributes( + [ + {"attribute": label_tree.num_labels, "expected_value": 4}, + {"attribute": label_tree.num_nodes(), "expected_value": 4}, + ] + ) + expected_edges = [ + (labels.label_0, labels.label_0_1, 0, {}), + (labels.label_0, labels.label_0_2, 0, {}), + ] + expected_nodes = [ + labels.label_0, + labels.label_0_1, + labels.label_0_2, + labels.label_0_1_3, + ] + CommonGraphMethods().check_graph_list_attributes( + [ + { + "attribute": label_tree.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": expected_edges, + }, + { + "attribute": label_tree.nodes, + "expected_type": NodeView, + "expected_value": expected_nodes, + }, + ] + ) + assert not label_tree._LabelTree__topological_order_cache + # Adding existing node, only topological_order_cache should be empty + label_tree.get_labels_in_topological_order() + label_tree.add_node(labels.label_0) + CommonGraphMethods().check_graph_non_list_attributes( + [ + {"attribute": label_tree.num_labels, "expected_value": 4}, + {"attribute": label_tree.num_nodes(), "expected_value": 4}, + ] + ) + CommonGraphMethods().check_graph_list_attributes( + [ + { + "attribute": label_tree.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": expected_edges, + }, + { + "attribute": label_tree.nodes, + "expected_type": NodeView, + "expected_value": expected_nodes, + }, + ] + ) + assert not label_tree._LabelTree__topological_order_cache + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_tree_clear_topological_cache(self): + """ + Description: + Check LabelTree class clear_topological_cache method + + Input data: + LabelTree object with specified directed parameter and added edges + + Expected results: + Test passes if "__topological_order_cache" attribute of LabelTree is equal "None" after clear_topological_cache + """ + # Check for empty LabelTree + label_tree = LabelTree() + label_tree.get_labels_in_topological_order() + label_tree.clear_topological_cache() + assert not label_tree._LabelTree__topological_order_cache + # Check for LabelTree with specified nodes and edges + label_tree = self.label_tree_no_children() + label_tree.get_labels_in_topological_order() + label_tree.clear_topological_cache() + assert not label_tree._LabelTree__topological_order_cache + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_tree_relations(self): + """ + Description: + Check LabelTree class relations methods + + Input data: + LabelTree object with specified directed parameter, added edges and children + + Expected results: + Test passes if "get_parent", "get_children", "get_descendants", "get_siblings" and "get_ancestors" methods + of LabelTree return expected values + + Steps + 1. Check add_children method + 2. Check "get_parent" method + 3. Check "get_descendants" method + 4. Check "get_siblings" method + 5. Check "get_ancestors" method + """ + label_tree = self.label_tree() + assert not label_tree._LabelTree__topological_order_cache + # Checking new nodes and edges added after add_children method + CommonGraphMethods().check_graph_non_list_attributes( + [ + {"attribute": label_tree.num_labels, "expected_value": 6}, + {"attribute": label_tree.num_nodes(), "expected_value": 6}, + ] + ) + CommonGraphMethods().check_graph_list_attributes( + [ + { + "attribute": label_tree.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": [ + (labels.label_0_1, labels.label_0, 0, {"value": None}), + (labels.label_0_2, labels.label_0, 0, {"value": None}), + (labels.label_0_1_3, labels.label_0_1, 0, {"value": None}), + (labels.label_0_2_4, labels.label_0_2, 0, {"value": None}), + (labels.label_0_2_5, labels.label_0_2, 0, {"value": None}), + ], + }, + { + "attribute": label_tree.nodes, + "expected_type": NodeView, + "expected_value": [ + labels.label_0_1, + labels.label_0, + labels.label_0_2, + labels.label_0_1_3, + labels.label_0_2_4, + labels.label_0_2_5, + ], + }, + ] + ) + # Checking "get_parent" method + for label, expected in [ + (labels.label_0, None), + (labels.label_0_1, labels.label_0), + (labels.label_0_2, labels.label_0), + (labels.label_0_1_3, labels.label_0_1), + (labels.label_0_2_4, labels.label_0_2), + (labels.label_0_2_5, labels.label_0_2), + ]: + assert label_tree.get_parent(label) == expected + # Checking "get_children" method + self.check_get_children_method(label_tree) + # Checking "get_descendants" method + self.check_get_descendants_method(label_tree) + # Checking "get_siblings" method + for label, expected in [ + (labels.label_0, []), + (labels.label_0_1, [labels.label_0_2]), + (labels.label_0_2, [labels.label_0_1]), + (labels.label_0_1_3, []), + (labels.label_0_2_4, [labels.label_0_2_5]), + (labels.label_0_2_5, [labels.label_0_2_4]), + ]: + assert label_tree.get_siblings(label) == expected + # Checking "get_ancestors" method + self.check_get_ancestors_method(label_tree) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_tree_get_labels_in_topological_order(self): + """ + Description: + Check LabelTree class get_labels_in_topological_order method + + Input data: + LabelTree object with specified directed parameter, added edges and children + + Expected results: + Test passes if get_labels_in_topological_order method of LabelTree returns expected value + + Steps + 1. Check value returned by get_labels_in_topological_order method for Tree with multiple children branches + 2. Remove node with children from tree and check value returned by get_labels_in_topological_order method + """ + label_tree = self.label_tree() + # Checking value returned by get_labels_in_topological_order method for tree with multiple branches + labels_topological_order = label_tree.get_labels_in_topological_order() + + def previous_vertexes(vert_id): + vertexes = vert_id.split("_") + return vertexes[:-1], vertexes[-1] + + returned_vertexes = set() + for label in labels_topological_order: + previous, curent = previous_vertexes(label.id_) + for vertex in previous: + assert vertex in returned_vertexes + returned_vertexes.add(curent) + assert {"0", "1", "2", "3", "4", "5"} == returned_vertexes + + assert label_tree._LabelTree__topological_order_cache == labels_topological_order + # Removing node with children and checking value returned by get_labels_in_topological_order method + label_tree.remove_node(labels.label_0_1) + labels_topological_order = label_tree.get_labels_in_topological_order() + returned_vertexes = set() + for label in labels_topological_order: + previous, curent = previous_vertexes(label.id_) + for vertex in previous: + if curent != "3": # the '1' has been removed, so that '3' is separated from the rest graph. + assert vertex in returned_vertexes + returned_vertexes.add(curent) + assert {"0", "2", "3", "4", "5"} == returned_vertexes + assert label_tree._LabelTree__topological_order_cache == labels_topological_order + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_tree_remove_node(self): + """ + Description: + Check LabelTree class remove_node method + + Input data: + LabelTree object with specified directed parameter, added edges and children + + Expected results: + Test passes if after using remove_node method on LabelTree object "edges", "nodes" and "num_labels" properties + and "num_nodes" method return expected values + + Steps + 1. Check values returned by "edges", "nodes" and "num_labels" properties and "num_nodes" method after removing + children node + 2. Check values returned by "edges", "nodes" and "num_labels" properties and "num_nodes" method after removing + parent node + """ + label_tree = self.label_tree() + # Removing children node and checking "edges", "nodes" and "num_labels" properties and "num_nodes" method values + label_tree.remove_node(labels.label_0_1_3) + assert label_tree.num_nodes() == 5 + assert label_tree.num_labels == 5 + assert not label_tree._LabelTree__topological_order_cache + CommonGraphMethods().check_graph_list_attributes( + [ + { + "attribute": label_tree.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": [ + (labels.label_0_1, labels.label_0, 0, {"value": None}), + (labels.label_0_2, labels.label_0, 0, {"value": None}), + (labels.label_0_2_4, labels.label_0_2, 0, {"value": None}), + (labels.label_0_2_5, labels.label_0_2, 0, {"value": None}), + ], + }, + { + "attribute": label_tree.nodes, + "expected_type": NodeView, + "expected_value": [ + labels.label_0_1, + labels.label_0, + labels.label_0_2, + labels.label_0_2_4, + labels.label_0_2_5, + ], + }, + ] + ) + # Removing node with children and checking value returned by get_labels_in_topological_order method + label_tree.get_labels_in_topological_order() + label_tree.remove_node(labels.label_0_2) + assert label_tree.num_nodes() == 4 + assert label_tree.num_labels == 4 + assert not label_tree._LabelTree__topological_order_cache + expected_edges = [(labels.label_0_1, labels.label_0, 0, {"value": None})] + CommonGraphMethods().check_graph_list_attributes( + [ + { + "attribute": label_tree.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": expected_edges, + }, + { + "attribute": label_tree.nodes, + "expected_type": NodeView, + "expected_value": [ + labels.label_0_1, + labels.label_0, + labels.label_0_2_4, + labels.label_0_2_5, + ], + }, + ] + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_tree_subgraph(self): + """ + Description: + Check LabelTree class subgraph method + + Input data: + LabelTree object with specified directed parameter, added edges and children + + Expected results: + Test passes if LabelTree object returned by subgraph method is equal expected + """ + label_tree = self.label_tree() + non_included_label = LabelEntity("not included", domain=Domain.CLASSIFICATION) + subgraph = label_tree.subgraph( + [ + labels.label_0, + labels.label_0_1, + labels.label_0_2, + labels.label_0_2_5, + non_included_label, + ] + ) + assert subgraph.num_nodes() == 4 + assert subgraph.num_labels == 4 + CommonGraphMethods().check_graph_list_attributes( + [ + { + "attribute": subgraph.edges, + "expected_type": OutMultiEdgeDataView, + "expected_value": [ + (labels.label_0_1, labels.label_0, 0, {"value": None}), + (labels.label_0_2, labels.label_0, 0, {"value": None}), + (labels.label_0_2_5, labels.label_0_2, 0, {"value": None}), + ], + }, + { + "attribute": subgraph.nodes, + "expected_type": NodeView, + "expected_value": [ + labels.label_0_1, + labels.label_0, + labels.label_0_2, + labels.label_0_2_5, + ], + }, + ] + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_tree_eq(self): + """ + Description: + Check LabelTree class __eq__ method + + Input data: + LabelTree objects with specified directed parameter, added edges and children + + Expected results: + Test passes if value returned by __eq__ method is equal expected + + Steps + 1. Check value returned by __eq__ method for equal LabelTree objects + 2. Check value returned by __eq__ method for LabelTree objects with different edges + 3. Check value returned by __eq__ method for LabelTree objects with different nodes + 4. Check value returned by __eq__ method for comparing LabelTree objects with different type object + """ + label_tree = self.label_tree() + # Checking __eq__ method for equal LabelTree objects + equal_label_tree = self.label_tree() + assert label_tree == equal_label_tree + # Checking __eq__ method for LabelTree objects with different edges + different_edges_tree = self.label_tree() + different_edges_tree.add_edge(labels.label_0, labels.label_0_1) + assert not label_tree == different_edges_tree + # Checking __eq__ method for LabelTree objects with different nodes + different_nodes_tree = self.label_tree() + different_nodes_tree.add_node(LabelEntity("not included", domain=Domain.CLASSIFICATION)) + assert label_tree != different_nodes_tree + # Checking __eq__ method for comparing LabelTree object with different type object + assert label_tree != str + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLabelSchemaEntity: + @staticmethod + def label_groups() -> list: + group_1 = LabelGroup( + name="Exclusive group 1", + labels=[labels.label_0_1, labels.label_0_2], + id=ID("Exclusive group 1"), + ) + group_2 = LabelGroup( + name="Exclusive group 2", + labels=[labels.label_0_2_4, labels.label_0_2_5], + id=ID("Exclusive group 2"), + ) + return [group_1, group_2] + + @staticmethod + def empty_labels() -> list: + empty_label = LabelEntity( + name="Empty label", + domain=Domain.SEGMENTATION, + color=Color(255, 255, 255), + is_empty=True, + id=ID("empty_label_1"), + ) + empty_non_exclusive_label = LabelEntity( + name="Empty non-exclusive label", + domain=Domain.DETECTION, + color=Color(255, 255, 255), + is_empty=True, + id=ID("empty_non_excl_label_1"), + ) + return [empty_label, empty_non_exclusive_label] + + def empty_labels_groups(self) -> list: + empty_labels = self.empty_labels() + empty_group_1 = LabelGroup( + name="Exclusive group with empty label", + labels=[empty_labels[0]], + id=ID("Exclusive group with empty label"), + ) + empty_group_2 = LabelGroup( + name="Empty label group with empty label", + labels=[empty_labels[1]], + group_type=LabelGroupType.EMPTY_LABEL, + id=ID("Empty label group with empty label"), + ) + return [empty_group_1, empty_group_2] + + def label_schema_entity(self) -> LabelSchemaEntity: + return LabelSchemaEntity( + label_tree=TestLabelTree.label_tree(), + label_groups=self.label_groups() + self.empty_labels_groups(), + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_entity_default_parameters_initialization(self): + """ + Description: + Check LabelSchemaEntity object initialization + + Input data: + LabelSchemaEntity object with not specified label_tree and label_groups parameters + + Expected results: + Test passes if attributes for LabelSchemaEntity with not specified "label_tree" and + "label_groups" parameters are equal expected + """ + empty_label_schema_entity = LabelSchemaEntity() + assert empty_label_schema_entity.label_tree == LabelTree() + assert empty_label_schema_entity._groups == [] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_entity_get_labels(self): + """ + Description: + Check LabelSchemaEntity class get_labels method + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if list returned by get_labels method is equal expected + + Steps + 1. Check list returned by get_labels method with include_empty parameter set to True + 2. Check list returned by get_labels method with include_empty parameter set to False + """ + empty_labels = self.empty_labels() + label_schema_entity = self.label_schema_entity() + # Checking list returned by get_labels method with include_empty parameter set to True + assert label_schema_entity.get_labels(include_empty=True) == [ + labels.label_0_1, + labels.label_0_2, + labels.label_0_2_4, + labels.label_0_2_5, + empty_labels[0], + empty_labels[1], + ] + # Checking list returned by get_labels method with include_empty parameter set to False + assert label_schema_entity.get_labels(include_empty=False) == [ + labels.label_0_1, + labels.label_0_2, + labels.label_0_2_4, + labels.label_0_2_5, + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_entity_get_groups(self): + """ + Description: + Check LabelSchemaEntity class get_groups method + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if list returned by get_groups method is equal expected + + Steps + 1. Check list returned by get_groups method with include_empty parameter set to True + 2. Check list returned by get_groups method with include_empty parameter set to False + """ + label_groups = self.label_groups() + empty_label_groups = self.empty_labels_groups() + label_schema_entity = self.label_schema_entity() + # Checking list returned by get_groups method with include_empty parameter set to True + assert label_schema_entity.get_groups(include_empty=True) == [ + label_groups[0], + label_groups[1], + empty_label_groups[0], + empty_label_groups[1], + ] + # Checking list returned by get_groups method with include_empty parameter set to False + assert label_schema_entity.get_groups(include_empty=False) == [ + label_groups[0], + label_groups[1], + empty_label_groups[0], + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_entity_add_group(self): + """ + Description: + Check LabelSchemaEntity class add_group method + + Input data: + LabelSchemaEntity object with label_tree and label_groups parameters + + Expected results: + Test passes if value returned by "get_exclusive_groups" method is equal expected + + Steps + 1. Check value returned by "get_exclusive_groups" method after adding group with new labels + 2. Check value returned by "get_exclusive_groups" method after adding group with single label + 3. Check value returned by "get_exclusive_groups" method after adding group with already added label + 4. Check value returned by "get_exclusive_groups" method and exclusivity of labels after adding group exclusive + to other + 5. Check value returned by "get_exclusive_groups" method after adding non-exclusive group + 6. Check LabelGroupExistsException raised when adding LabelGroup with already existing name + """ + empty_label_groups = self.empty_labels_groups() + exclusive_groups = self.label_groups() + [empty_label_groups[0]] + label_schema_entity = self.label_schema_entity() + # Scenario for adding exclusive group with new labels + new_exclusive_label = LabelEntity(name="New label", domain=Domain.DETECTION, id=ID("new_ex_1")) + other_new_exclusive_label = LabelEntity(name="Other new label", domain=Domain.DETECTION, id=ID("new_ex_2")) + new_exclusive_group = LabelGroup( + name="New exclusive labels group", + labels=[new_exclusive_label, other_new_exclusive_label], + id=ID("new_ex_group"), + ) + label_schema_entity.add_group(new_exclusive_group) + assert label_schema_entity.get_exclusive_groups() == (exclusive_groups + [new_exclusive_group]) + # Scenario for adding exclusive group with single label + label_schema_entity = self.label_schema_entity() + new_exclusive_group = LabelGroup( + name="Exclusive group with one label", + labels=[new_exclusive_label], + id=ID("single_excl_group"), + ) + label_schema_entity.add_group(new_exclusive_group) + assert label_schema_entity.get_exclusive_groups() == (exclusive_groups + [new_exclusive_group]) + # Scenario for adding exclusive group with one already existing label + label_schema_entity = self.label_schema_entity() + new_exclusive_group = LabelGroup( + name="Exclusive group to link with existing", + labels=[labels.label_0_1, new_exclusive_label], + id=ID("new_ex_group"), + ) + label_schema_entity.add_group(new_exclusive_group) + assert label_schema_entity.get_exclusive_groups() == (exclusive_groups + [new_exclusive_group]) + # Scenario for adding non-exclusive group + label_schema_entity = self.label_schema_entity() + new_exclusive_group = LabelGroup( + name="Non exclusive label group", + labels=[new_exclusive_label], + group_type=LabelGroupType.EMPTY_LABEL, + id=ID("non_exclusive_group"), + ) + label_schema_entity.add_group(new_exclusive_group) + assert label_schema_entity.get_exclusive_groups() == exclusive_groups + # Raise LabelGroupExistsException when adding LabelGroup with same name + for group_type in [LabelGroupType.EXCLUSIVE, LabelGroupType.EMPTY_LABEL]: + with pytest.raises(LabelGroupExistsException): + label_schema_entity.add_group( + LabelGroup( + name="Exclusive group 1", + labels=[], + group_type=group_type, + ) + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_add_child(self): + """ + Description: + Check LabelSchemaEntity class add_child method + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if list returned by get_children method after using add_child method are equal to expected + + Steps + 1. Check get_children list after using add_child method + 2. Check get_children list after using add_child method for previous pair for a second time + """ + label_schema_entity = self.label_schema_entity() + label_schema_entity.add_child(parent=labels.label_0_2_4, child=labels.label_0_2_5) + assert label_schema_entity.get_children(labels.label_0_2_4) == [labels.label_0_2_5] + label_schema_entity.add_child(parent=labels.label_0_2_4, child=labels.label_0_2_5) + assert label_schema_entity.get_children(labels.label_0_2_4) == [labels.label_0_2_5] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_entity_get_label_ids(self): + """ + Description: + Check LabelSchemaEntity class get_label_ids method + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if values returned by get_label_ids method is equal expected + + Steps + Check value returned by get_label_ids method for LabelSchemaEntity object + """ + expected_non_empty_labels = [ID("0_1"), ID("0_2"), ID("0_2_4"), ID("0_2_5")] + expected_include_empty_labels = expected_non_empty_labels + [ + ID("empty_label_1"), + ID("empty_non_excl_label_1"), + ] + label_schema_entity = self.label_schema_entity() + assert label_schema_entity.get_label_ids(include_empty=True) == expected_include_empty_labels + assert label_schema_entity.get_label_ids(include_empty=False) == expected_non_empty_labels + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_get_label_group_by_name(self): + """ + Description: + Check LabelSchemaEntity class get_label_group_by_name method + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if value returned by get_label_group_by_name method is equal expected + + Steps + 1. Check get_label_group_by_name method for searching exclusive group + 2. Check get_label_group_by_name method for searching empty label group + 2. Check get_label_group_by_name method for searching non_existing group + """ + label_schema_entity = self.label_schema_entity() + label_groups = self.label_groups() + empty_non_excl_group = self.empty_labels_groups()[1] + # Checking get_label_group_by_name method for searching exclusive group for not specified empty labels + assert label_schema_entity.get_label_group_by_name("Exclusive group 1") == label_groups[0] + # Checking get_label_group_by_name method for searching empty label group with empty label + assert label_schema_entity.get_label_group_by_name("Empty label group with empty label") == empty_non_excl_group + # Checking get_label_group_by_name method for searching non-existing group + assert not label_schema_entity.get_label_group_by_name("Non-existing group") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_get_exclusive_groups(self): + """ + Description: + Check LabelSchemaEntity class get_exclusive_groups method + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if value returned by get_exclusive_groups method is equal expected + + Steps + 1. Check get_exclusive_groups method + 2. Check get_exclusive_groups method after adding exclusive group + 3. Check get_exclusive_groups method after adding empty label group + """ + label_schema_entity = self.label_schema_entity() + exclusive_groups = self.label_groups() + [self.empty_labels_groups()[0]] + # Checking get_exclusive_groups method for searching exclusive groups + assert label_schema_entity.get_exclusive_groups() == exclusive_groups + # Checking get_exclusive_groups method after adding new exclusive group + new_label = LabelEntity( + name="New label", + domain=Domain.DETECTION, + color=Color(100, 16, 25), + id=ID("new_ex_1"), + ) + new_labels_group = LabelGroup( + name="New exclusive labels group", + labels=[new_label], + id=ID("new_ex_group"), + ) + label_schema_entity.add_group(new_labels_group) + exclusive_groups.append(new_labels_group) + assert label_schema_entity.get_exclusive_groups() == exclusive_groups + # Checking get_exclusive_groups method after adding empty label group + empty_label_group = LabelGroup( + name="New non-exclusive labels group", + labels=[new_label], + group_type=LabelGroupType.EMPTY_LABEL, + id=ID("new_ex_group"), + ) + label_schema_entity.add_group(empty_label_group) + assert label_schema_entity.get_exclusive_groups() == exclusive_groups + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_add_labels_to_group_by_group_name(self): + """ + Description: + Check LabelSchemaEntity class add_labels_to_group_by_group_name method + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if labels attribute returned by group which specified in add_labels_to_group_by_group_name method + is equal expected + + Steps + 1. Check add_labels_to_group_by_group_name method to add labels to exclusive group + 2. Check add_labels_to_group_by_group_name method to add labels to empty label group + 3. Check LabelGroupDoesNotExistException raised when adding labels to non-existing group + """ + label_schema_entity = self.label_schema_entity() + non_exclusive_label = self.empty_labels()[1] + # Checking add_labels_to_group_by_group_name method to add labels to exclusive group + new_label = LabelEntity( + name="New label", + domain=Domain.DETECTION, + color=Color(100, 16, 25), + id=ID("new_ex_1"), + ) + new_empty_label = LabelEntity( + name="New empty label", + domain=Domain.DETECTION, + color=Color(81, 100, 10), + id=ID("new_ex_2"), + ) + exclusive_group_name = "Exclusive group 1" + label_schema_entity.add_labels_to_group_by_group_name( + group_name=exclusive_group_name, labels=[new_label, new_empty_label] + ) + assert label_schema_entity.get_label_group_by_name(exclusive_group_name).labels == [ + labels.label_0_1, + labels.label_0_2, + new_label, + new_empty_label, + ] + # Checking add_labels_to_group_by_group_name method to add labels to non-exclusive group + new_empty_label = LabelEntity( + name="New non-exclusive empty_label", + domain=Domain.SEGMENTATION, + is_empty=True, + id=ID("empty_label_1"), + ) + empty_label_group_name = "Empty label group with empty label" + label_schema_entity.add_labels_to_group_by_group_name( + group_name=empty_label_group_name, labels=[new_empty_label] + ) + assert label_schema_entity.get_label_group_by_name(empty_label_group_name).labels == [ + non_exclusive_label, + new_empty_label, + ] + # Checking that LabelGroupDoesNotExistException raised when adding labels to non-existing group + with pytest.raises(LabelGroupDoesNotExistException): + label_schema_entity.add_labels_to_group_by_group_name( + group_name="Non-existing group", labels=[new_empty_label] + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_relations(self): + """ + Description: + Check LabelSchemaEntity relations methods + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if "get_children", "get_descendants" and "get_ancestors" methods + of LabelTree return expected values + + Steps + 1. Check "get_children" method + 2. Check "get_descendants" method + 3. Check "get_ancestors" method + """ + label_schema_entity = self.label_schema_entity() + # Checking get_children method + TestLabelTree.check_get_children_method(label_schema_entity) + # Checking get_descendants method + TestLabelTree.check_get_descendants_method(label_schema_entity) + # Checking get_ancestors method + TestLabelTree.check_get_ancestors_method(label_schema_entity) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_get_group_containing_label(self): + """ + Description: + Check LabelSchemaEntity class get_group_containing_label method + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if value returned by get_group_containing_label method is equal expected + + Steps + 1. Check get_group_containing_label method for label included in exclusive group + 2. Check get_group_containing_label method for label included in empty label + 3. Check get_group_containing_label method for label not included in any group + """ + label_schema_entity = self.label_schema_entity() + label_groups = self.label_groups() + # Checking get_group_containing_label method for label included in exclusive group + assert label_schema_entity.get_group_containing_label(labels.label_0_1) == label_groups[0] + assert label_schema_entity.get_group_containing_label(labels.label_0_2) == label_groups[0] + assert label_schema_entity.get_group_containing_label(labels.label_0_2_4) == label_groups[1] + assert label_schema_entity.get_group_containing_label(labels.label_0_2_5) == label_groups[1] + # Checking get_group_containing_label method for label included in non-exclusive group + assert label_schema_entity.get_group_containing_label(self.empty_labels()[1]) == self.empty_labels_groups()[1] + # Checking get_group_containing_label method for label not included in any group + assert not label_schema_entity.get_group_containing_label(labels.label_0) + assert not label_schema_entity.get_group_containing_label(labels.label_0_1_3) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_get_label(self): + """ + Description: + Check LabelSchemaEntity class __get_label method + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if LabelEntity object returned by __get_label method is equal expected + + Steps + 1. Check __get_label method for searching ScoredLabel object + 2. Check __get_label method for searching LabelEntity object + 3. Check __get_label method for searching non-label object + """ + label_schema_entity = self.label_schema_entity() + # Checking __get_label for searching ScoredLabel object + label_to_set_scored = LabelEntity( + name="Scored label", + domain=Domain.DETECTION, + color=Color(red=100, green=50, blue=200), + id=ID("scored_label_1"), + ) + scored_label = ScoredLabel(label=label_to_set_scored) + scored_labels_group = LabelGroup(name="Group with scored label", labels=[scored_label]) + label_schema_entity.add_group(scored_labels_group) + assert label_schema_entity._LabelSchemaEntity__get_label(scored_label) == scored_label.get_label() + # Checking __get_label for searching LabelEntity object + assert label_schema_entity._LabelSchemaEntity__get_label(labels.label_0) == labels.label_0 + # Checking __get_label method for searching non-label object + with pytest.raises(ValueError): + label_schema_entity._LabelSchemaEntity__get_label(str) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_repr(self): + """ + Description: + Check LabelSchemaEntity class __repr__ method + + Input data: + LabelSchemaEntity object with specified label_tree and label_groups parameters + + Expected results: + Test passes if value returned by __repr__ method is equal expected + """ + label_schema_entity = self.label_schema_entity() + label_groups = self.label_groups() + self.empty_labels_groups() + assert repr(label_schema_entity) == f"LabelSchemaEntity(label_groups={label_groups})" + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_eq(self): + """ + Description: + Check LabelSchemaEntity class __eq__ method + + Input data: + LabelSchemaEntity objects with specified label_tree and label_groups parameters + + Expected results: + Test passes if value returned by __eq__ method is equal expected + + Steps + 1. Check __eq__ method for equal LabelSchemaEntity objects + 2. Check __eq__ method for LabelSchemaEntity objects with unequal label_tree + 3. Check __eq__ method for LabelSchemaEntity objects with unequal LabelGroups + 4. Check __eq__ method for comparing LabelSchemaEntity object object of other type + """ + # Checking __eq__ method for equal LabelSchemaEntity objects + label_schema_entity = self.label_schema_entity() + equal_label_schema_entity = self.label_schema_entity() + assert label_schema_entity == equal_label_schema_entity + # Checking __eq__ method for equal LabelSchemaEntity objects with unequal label_tree + unequal_tree_label_schema_entity = self.label_schema_entity() + unequal_tree_label_schema_entity.label_tree.add_edge(labels.label_0_1_3, labels.label_0_2_4) + assert label_schema_entity != unequal_tree_label_schema_entity + # Checking __eq__ method for equal LabelSchemaEntity objects with unequal LabelGroups + unequal_groups_label_schema_entity = self.label_schema_entity() + unequal_groups_label_schema_entity.add_labels_to_group_by_group_name("Exclusive group 1", [labels.label_0]) + assert label_schema_entity != unequal_groups_label_schema_entity + # Checking __eq__ method for comparing LabelSchemaEntity object object of other type + assert label_schema_entity != str + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_from_labels(self): + """ + Description: + Check LabelSchemaEntity class from_labels method + + Input data: + LabelSchemaEntity objects with specified label_tree and label_groups parameters + + Expected results: + Test passes if LabelSchemaEntity object returned by from_labels method is equal expected + """ + expected_labels = [ + labels.label_0, + labels.label_0_1, + labels.label_0_2, + labels.label_0_2_4, + ] + label_schema_entity = LabelSchemaEntity.from_labels(expected_labels) + labels_schema_entity_groups = label_schema_entity._groups + assert isinstance(label_schema_entity, LabelSchemaEntity) + assert label_schema_entity.label_tree == LabelTree() + assert len(labels_schema_entity_groups) == 1 + assert labels_schema_entity_groups[0].name == "from_label_list" + assert labels_schema_entity_groups[0].labels == expected_labels + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_resolve_labels(self): + """ + Description: + Check LabelSchemaEntity label resolving algorithms + + Input data: + LabelSchemaEntity objects with specified label_tree and label_groups parameters + + Expected results: + Test passes if labels list returned by resolving methods + is equal expected + """ + label_schema = LabelSchemaEntity() + labels_1 = [ + LabelEntity( + name=f"Label {i}", + domain=Domain.CLASSIFICATION, + id=ID(f"{i}"), + color=Color(i, 200, 166), + ) + for i in range(2) + ] + labels_2 = [ + LabelEntity( + name=f"Label {i}", + domain=Domain.CLASSIFICATION, + id=ID(f"{i}"), + color=Color(i, 200, 166), + ) + for i in range(2, 4) + ] + labels_3 = [ + LabelEntity( + name=f"Label {i}", + domain=Domain.CLASSIFICATION, + id=ID(f"{i}"), + color=Color(i, 200, 166), + ) + for i in range(4, 5) + ] + group_1 = LabelGroup(name="labels1", labels=labels_1, group_type=LabelGroupType.EXCLUSIVE) + label_schema.add_group(group_1) + group_2 = LabelGroup(name="labels2", labels=labels_2, group_type=LabelGroupType.EXCLUSIVE) + label_schema.add_group(group_2) + + label_schema.add_group(LabelGroup("child_labels_2", labels_3, LabelGroupType.EXCLUSIVE)) + label_schema.add_child(labels_2[0], labels_3[0]) + + assert 1 == len(label_schema.get_descendants(labels_2[0])) + + # supress non-maximum labels + predicted_labels = [ + ScoredLabel(labels_1[0], 0.1), + ScoredLabel(labels_1[1], 0.5), + ScoredLabel(labels_2[0], 0.2), + ScoredLabel(labels_2[1], 0.5), + ] + resloved_labels = label_schema.resolve_labels_probabilistic(predicted_labels) + + ref_labels = [ + ScoredLabel(labels_1[1], 0.5), + ScoredLabel(labels_2[1], 0.5), + ] + assert ref_labels == resloved_labels + + resloved_labels_greedy = label_schema.resolve_labels_greedily(predicted_labels) + assert ref_labels == resloved_labels_greedy + + # supress children of non-maximum labels + predicted_labels = [ + ScoredLabel(labels_2[0], 0.1), + ScoredLabel(labels_2[1], 0.5), + ScoredLabel(labels_3[0], 0.4), + ] + resloved_labels = label_schema.resolve_labels_probabilistic(predicted_labels) + ref_labels = [ScoredLabel(labels_2[1], 0.5)] + assert ref_labels == resloved_labels + + resloved_labels_greedy = label_schema.resolve_labels_greedily(predicted_labels) + assert ref_labels == resloved_labels_greedy + + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_schema_resolve_labels_greedy(self): + """ + Description: + Check LabelSchemaEntity label gredy resolving algorithm + + Input data: + LabelSchemaEntity objects with specified label_tree and label_groups parameters + + Expected results: + Test passes if labels list returned by resolving method + is equal expected + """ + + label_schema = LabelSchemaEntity() + g1_labels = [ + LabelEntity("A", Domain.CLASSIFICATION), + LabelEntity("B", Domain.CLASSIFICATION), + LabelEntity("C", Domain.CLASSIFICATION), + ] + g2_labels = [LabelEntity("D", Domain.CLASSIFICATION), LabelEntity("E", Domain.CLASSIFICATION)] + g3_labels = [LabelEntity("F", Domain.CLASSIFICATION), LabelEntity("G", Domain.CLASSIFICATION)] + g4_labels = [LabelEntity("H", Domain.CLASSIFICATION)] + + label_schema.add_group(LabelGroup(name="labels1", labels=g1_labels, group_type=LabelGroupType.EXCLUSIVE)) + label_schema.add_group(LabelGroup(name="labels2", labels=g2_labels, group_type=LabelGroupType.EXCLUSIVE)) + label_schema.add_group(LabelGroup(name="labels3", labels=g3_labels, group_type=LabelGroupType.EXCLUSIVE)) + label_schema.add_group(LabelGroup(name="labels4", labels=g4_labels, group_type=LabelGroupType.EXCLUSIVE)) + + label_schema.add_child(g1_labels[0], g2_labels[0]) + label_schema.add_child(g1_labels[0], g2_labels[1]) + + label_schema.add_child(g1_labels[1], g3_labels[0]) + label_schema.add_child(g1_labels[1], g3_labels[1]) + + predicted_labels = [ + ScoredLabel(g1_labels[0], 0.6), + ScoredLabel(g1_labels[1], 0.3), + ScoredLabel(g1_labels[2], 0.1), + ScoredLabel(g2_labels[0], 0.2), + ScoredLabel(g2_labels[1], 0.8), + ScoredLabel(g3_labels[0], 0.7), + ScoredLabel(g3_labels[1], 0.3), + ScoredLabel(g4_labels[0], 0.9), + ] + + ref_labels = [ + ScoredLabel(g1_labels[0], 0.6), + ScoredLabel(g2_labels[1], 0.8), + ScoredLabel(g4_labels[0], 0.9), + ] + + assert ref_labels == label_schema.resolve_labels_greedily(predicted_labels) diff --git a/tests/unit/api/entities/test_media.py b/tests/unit/api/entities/test_media.py new file mode 100644 index 00000000000..f60c8ed6c2a --- /dev/null +++ b/tests/unit/api/entities/test_media.py @@ -0,0 +1,81 @@ +# Copyright (C) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import abc + +import pytest + +from otx.api.entities.media import IMedia2DEntity, IMediaEntity +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIMediaEntity: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_i_media_entity(self): + """ + Description: + To test IMediaEntity class + + Input data: + Instance of IMediaEntity class + + Expected results: + 1. Test instance is instance of class IMediaEntity + + + Steps + 1. Create IMediaEntity + + """ + test_inst = IMediaEntity() + assert isinstance(test_inst, IMediaEntity) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIMedia2DEntity: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_i_media_2d_entity(self): + """ + Description: + To test IMedia2DEntity abstract class + + Input data: + Instance of IMedia2DEntity abstract class + + Expected results: + 1. TypeError is raised + 2. Expected method numbers and names + + + Steps + 1. Create IMedia2DEntity + 2. Check abstract methods + + """ + with pytest.raises(TypeError): + IMedia2DEntity() + + assert type(IMedia2DEntity) is abc.ABCMeta + abc_methods = IMedia2DEntity.__abstractmethods__ + assert len(abc_methods) == 4 + assert "width" in abc_methods + assert "roi_numpy" in abc_methods + assert "numpy" in abc_methods + assert "height" in abc_methods diff --git a/tests/unit/api/entities/test_metadata.py b/tests/unit/api/entities/test_metadata.py new file mode 100644 index 00000000000..f81fb657169 --- /dev/null +++ b/tests/unit/api/entities/test_metadata.py @@ -0,0 +1,237 @@ +"""This module tests classes related to metadata""" + +# Copyright (C) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +import re + +import pytest + +from otx.api.entities.metadata import ( + FloatMetadata, + FloatType, + IMetadata, + MetadataItemEntity, +) +from otx.api.entities.model import ModelEntity +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIMetadata: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_imetadata(self): + """ + Description: + To test IMetadata class + + Input data: + Initialized instance of IMetadata class + + Expected results: + 1. Initialized instance is instance of the class + 2. Default value of field has expected value: "typing.Union[str, NoneType]" + 3. Changed fields value has expected value: "String" + + Steps + 1. Create IMetadata + 2. Check default value of class field + 3. Change value of the field + """ + + test_instance = IMetadata() + assert isinstance(test_instance, IMetadata) + assert str(test_instance.name) in ["typing.Union[str, NoneType]", "typing.Optional[str]"] + + test_instance.name = "String" + assert test_instance.name == "String" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestFloatType: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_float_type_members(self): + """ + Description: + To test FloatType enumeration members + + Input data: + Initialized instance of FloatType enum class + + Expected results: + 1. Enum members return correct values: + FLOAT = 1 + EMBEDDING_VALUE = 2 + ACTIVE_SCORE = 3 + 2. In case incorrect member it raises AttributeError exception + 3. In case incorrect member value it raises ValueError exception + + Steps + 0. Create FloatType + 1. Check members + 2. Check incorrect member + 3. Check incorrect member value + """ + + test_instance = FloatType + + for i in range(1, 4): + assert test_instance(i) in list(FloatType) + + with pytest.raises(AttributeError): + test_instance.WRONG + + with pytest.raises(ValueError): + test_instance(6) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_float_type_magic_str(self): + """ + Description: + To test FloatType __str__ method + + Input data: + Initialized instance of FloatType enum + + Expected results: + 1. __str__ returns correct string for every enum member + 2. In case incorrect member it raises AttributeError exception + 3. In case incorrect member value it raises ValueError exception + + Steps + 0. Create FloatType + 1. Check returning value of __str__ method + 2. Try incorrect field name + 3. Try incorrect value name + """ + test_instance = FloatType + magic_str_list = [str(i) for i in list(FloatType)] + + for i in range(1, 4): + assert str(test_instance(i)) in magic_str_list + + with pytest.raises(AttributeError): + str(test_instance.WRONG) + + with pytest.raises(ValueError): + str(test_instance(6)) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestMetadataItemEntity: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_metadata_item_entity(self): + """ + Description: + To test MetadataItemEntity class + + Input data: + Initialized instances of IMetadata, ModelEntity and MetadataItemEntity classes + + Expected results: + 1. Initialized instances + 2. Default values of fields are None + 3. repr method returns expected value "NullMetadata(None, None)" after initiation for both instances + 4. Instance test_instance1 fields values changed and have expected values: + name == String1 + value == 0 + 5. repr method of test_instance1 returns expected value "NullMetadata(String1, 0)" + 6. == method behavior is expected + + Steps + 1. Create class instances test_instance0 and test_instance1 + 2. Perform checks of its field default values after initiations + 3. Perform checks of repr method returns expected values after initiations + 4. Change fields value of test_instance1 + 5. Perform checks of repr method returns expected values after changes + 6. Check that test_instance0 == test_instance1 + """ + i_metadata = IMetadata() + i_metadata.name = "default_i_metadata" + test_data0 = test_data1 = i_metadata.name + i_metadata.name = "i_metadata" + test_data2 = i_metadata.name + test_model0 = test_model1 = ModelEntity(train_dataset="default_dataset", configuration="default_config") + test_instance0 = MetadataItemEntity(test_data0, test_model0) + test_instance1 = MetadataItemEntity(test_data1, test_model1) + test_instance2 = MetadataItemEntity(test_data2, test_model1) + assert test_instance0 == test_instance1 != test_instance2 + __repr = repr(test_instance0) + repr_pattern = ( + r"MetadataItemEntity\(model=\\)" + ) + assert re.match(repr_pattern, __repr) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestFloatMetadata: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_float_metadata(self): + """ + Description: + To test FloatMetadata class + + Input data: + Initialized instance of FloatMetadata + + Expected results: + 1. It raises TypeError in case attempt of initiation with wrong parameters numbers + 2. Fields of instances are with correct values + 3. repr method returns correct strings then used against each instance + 4. '==' method works as expected + + Steps + 1. Attempt to initiate class instance with wrong parameters numbers + 2. Initiate three class instances: + two of them with similar set of init values, third one with different one. + 3. Check repr method + 4. Check __eq__ method + """ + with pytest.raises(TypeError): + FloatMetadata() + + with pytest.raises(TypeError): + FloatMetadata("only name") + + test_inst0 = FloatMetadata(name="Instance0", value=42) + assert test_inst0.name == "Instance0" + assert test_inst0.value == 42 + assert repr(test_inst0.float_type) == "" + assert repr(test_inst0) == "FloatMetadata(Instance0, 42, FLOAT)" + + test_inst1 = FloatMetadata(name="Instance1", value=42.0) + assert test_inst1.name == "Instance1" + assert test_inst1.value == 42.0 + assert repr(test_inst1.float_type) == "" + assert repr(test_inst1) == "FloatMetadata(Instance1, 42.0, FLOAT)" + + test_inst2 = FloatMetadata(name="Instance0", value=42) + assert test_inst2.name == "Instance0" + assert test_inst2.value == 42 + assert repr(test_inst2.float_type) == "" + assert repr(test_inst2) == "FloatMetadata(Instance0, 42, FLOAT)" + + assert test_inst0 == test_inst2 != test_inst1 diff --git a/tests/unit/api/entities/test_metrics.py b/tests/unit/api/entities/test_metrics.py new file mode 100644 index 00000000000..11debccf454 --- /dev/null +++ b/tests/unit/api/entities/test_metrics.py @@ -0,0 +1,1061 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import datetime +import warnings + +import numpy as np +import pytest + +from otx.api.entities.metrics import ( + BarChartInfo, + BarMetricsGroup, + ColorPalette, + CountMetric, + CurveMetric, + DateMetric, + DurationMetric, + InfoMetric, + LineChartInfo, + LineMetricsGroup, + MatrixChartInfo, + MatrixMetric, + MatrixMetricsGroup, + MultiScorePerformance, + NullMetric, + NullPerformance, + Performance, + ScoreMetric, + TextChartInfo, + TextMetricsGroup, + VisualizationInfo, + VisualizationType, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestMetrics: + @staticmethod + def mixed_conditions_duration_metric() -> DurationMetric: + return DurationMetric(name="Mixed conditions metric", hour=0, minute=1, second=2.1) + + @staticmethod + def matrix_data(): + return np.array([[0, 1, 1], [0, 1, 1], [1, 0, 0]]) + + def normalized_matrix_metric(self) -> MatrixMetric: + return MatrixMetric(name="test matrix", matrix_values=self.matrix_data(), normalize=True) + + @staticmethod + def normalized_matrix_zero_sum() -> MatrixMetric: + matrix_data_with_zero_sum = np.array([[0, 0, 0], [0, 1, 1], [1, 0, 0]]) + return MatrixMetric(name="test matrix", matrix_values=matrix_data_with_zero_sum, normalize=True) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_duration_metrics(self): + """ + Description: + Check that duration is correctly calculated + + Input data: + 1 hour, 1 minute and 15.4 seconds + + Expected results: + Test passes if DurationMetrics correctly converts seconds to hours, minutes and seconds + + Steps + 1. Create DurationMetrics + 2. Check hour, minute and second calculated by DurationMetric + 3. Check value returned by get_duration_string method + """ + hour = 1 + minute = 1 + second = 15.5 + seconds = (hour * 3600) + (minute * 60) + second + duration_metric = DurationMetric.from_seconds(name="Training duration", seconds=seconds) + assert duration_metric.hour == hour + assert duration_metric.minute == minute + assert duration_metric.second == second + assert duration_metric.type() == "duration" + print(duration_metric.get_duration_string()) + # Checking get_duration_string method for 0 specified as DurationMetric parameters + zero_duration_metric = DurationMetric(name="Zero Duration Metric", hour=0, minute=0, second=0.0) + assert zero_duration_metric.get_duration_string() == "" + # Checking get_duration_string method for 1 specified as DurationMetric parameters + one_duration_metric = DurationMetric(name="One Duration Metric", hour=1, minute=1, second=1.0) + assert one_duration_metric.get_duration_string() == "1 hour 1 minute 1.00 second" + # Checking get_duration_string method for value>1 specified as DurationMetric parameters + more_than_one_duration_metric = DurationMetric( + name="More than one duration metric", hour=2, minute=3, second=1.1 + ) + assert more_than_one_duration_metric.get_duration_string() == "2 hours 3 minutes 1.10 seconds" + # Checking get_duration_string method for 0, 1, ">1" values specified as DurationMetric parameters + mixed_conditions_metric = self.mixed_conditions_duration_metric() + assert mixed_conditions_metric.get_duration_string() == "1 minute 2.10 seconds" + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_matrix_metric(self): + """ + Description: + Check that MatrixMetric correctly normalizes the values in a given matrix + + Input data: + Three square matrices + + Expected results: + Test passes if the values of the normalized matrices match the pre-computed matrices + + Steps + 1. Create Matrices + 2. Check normalized matrices against pre-computed matrices + 3. Check positive scenario when row_labels and column_labels parameters specified during MatrixMetric object + initialization + 4. Check ValueError exception raised when row_labels parameter length is not equal to number of rows of + MatrixMetric object + 5. Check ValueError exception raised when column_labels parameter length is not equal to number of columns of + MatrixMetric object + """ + matrix_metric = self.normalized_matrix_metric() + + required_normalised_matrix_data = np.array([[0, 0.5, 0.5], [0, 0.5, 0.5], [1, 0, 0]]) + assert np.array_equal(required_normalised_matrix_data, matrix_metric.matrix_values) + assert repr(matrix_metric) == ( + "MatrixMetric(name=`test matrix`, matrix_values=(3x3) matrix, row labels=None, column labels=None)" + ) + + with warnings.catch_warnings(): + # there is a matrix with zero sum in row, so we expect 0/0 division. + warnings.filterwarnings("ignore", "invalid value encountered in true_divide") + matrix_data_with_zero_sum = np.array([[0, 0, 0], [0, 1, 1], [1, 0, 0]]) + matrix_metric_with_zero_sum = MatrixMetric( + name="test matrix", + matrix_values=matrix_data_with_zero_sum, + normalize=True, + ) + + required_normalised_matrix_data_with_zero_sum = np.array([[0, 0, 0], [0, 0.5, 0.5], [1, 0, 0]]) + assert np.array_equal( + required_normalised_matrix_data_with_zero_sum, + matrix_metric_with_zero_sum.matrix_values, + ) + # Checking scenario with specified row_labels and column_labels parameters + matrix_metric_with_labels_name = "MatrixMetric with row and column labels" + row_labels = ["row_1", "row_2", "row_3"] + column_labels = ["column_1", "column_2", "column_3"] + matrix_metric_with_labels = MatrixMetric( + name=matrix_metric_with_labels_name, + matrix_values=self.matrix_data(), + row_labels=row_labels, + column_labels=column_labels, + ) + assert matrix_metric_with_labels.name == matrix_metric_with_labels_name + assert np.array_equal(self.matrix_data(), matrix_metric_with_labels.matrix_values) + assert matrix_metric_with_labels.row_labels == row_labels + assert matrix_metric_with_labels.column_labels == column_labels + assert matrix_metric_with_labels.type() == "matrix" + assert repr(matrix_metric_with_labels) == ( + "MatrixMetric(name=`MatrixMetric with row and column labels`, matrix_values=(3x3) matrix, " + "row labels=['row_1', 'row_2', 'row_3'], column labels=['column_1', 'column_2', 'column_3'])" + ) + # Checking ValueError exception raised when row_labels parameter length not equal to number of rows + for incorrect_row_labels in ( + ["row_1", "row_2"], + ["row_1", "row_2", "row_3", "row_4"], + ): + with pytest.raises(ValueError): + MatrixMetric( + name="MatrixMetric with incorrect number of row labels", + matrix_values=self.matrix_data(), + row_labels=incorrect_row_labels, + ) + # Checking ValueError exception raised when column_labels parameter length not equal to number of columns + for incorrect_column_labels in ( + ["column_1", "column_2"], + ["column_1", "column_2", "column_3", "column_4"], + ): + with pytest.raises(ValueError): + MatrixMetric( + name="MatrixMetric with incorrect number of column labels", + matrix_values=self.matrix_data(), + column_labels=incorrect_column_labels, + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestCountMetric: + @staticmethod + def count_metric() -> CountMetric: + return CountMetric(name="Test CountMetric", value=10) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_count_metric(self): + """ + Description: + Check CountMetric class + + Input data: + CountMetric object with specified name and value parameters + + Expected results: + Test passes if CountMetric object name, value and type attributes return expected values + """ + for name, value in ["null metric", "positive metric"], [0, 10]: + count_metric = CountMetric(name=name, value=value) + assert count_metric.name == name + assert count_metric.value == value + assert count_metric.type() == "count" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestInfoMetric: + @staticmethod + def info_metric() -> InfoMetric: + return InfoMetric(name="Test InfoMetric", value="This Metric is prepared for test purposes") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_info_metric(self): + """ + Description: + Check InfoMetric class + + Input data: + InfoMetric object with specified name and value parameters + + Expected results: + Test passes if InfoMetric object name, value and type attributes return expected values + """ + info_metric = self.info_metric() + assert info_metric.name == "Test InfoMetric" + assert info_metric.value == "This Metric is prepared for test purposes" + assert info_metric.type() == "string" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDateMetric: + @staticmethod + def date_metric_no_date_specified() -> DateMetric: + return DateMetric(name="DateMetric with not specified date") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_date_metric(self): + """ + Description: + Check DateMetric class + + Input data: + DateMetric object with specified name and date parameters + + Expected results: + Test passes if DateMetric object name, date and type attributes return expected values + + Steps + 1. Check name, date and type attributes of DateMetric object with not specified date parameter + 2. Check name, date and type attributes of DateMetric object with specified date parameter + """ + # Check for DateMetric with not specified date parameter + date_not_specified_metric = self.date_metric_no_date_specified() + assert date_not_specified_metric.name == "DateMetric with not specified date" + assert isinstance(date_not_specified_metric.date, datetime.datetime) + assert date_not_specified_metric.type() == "date" + # Check for DateMetric with specified date parameter + date_specified_metric_name = "DateMetric with specified date" + date_expected = datetime.datetime(year=2020, month=11, day=29, hour=13, minute=25, second=10, microsecond=3) + date_specified_metric = DateMetric(name=date_specified_metric_name, date=date_expected) + assert date_specified_metric.name == date_specified_metric_name + assert date_specified_metric.date == date_expected + assert date_specified_metric.type() == "date" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestScoreMetric: + @staticmethod + def score_metric() -> ScoreMetric: + return ScoreMetric(name="Test ScoreMetric", value=2.0) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_score_metric(self): + """ + Description: + Check ScoreMetric class + + Input data: + ScoreMetric object with specified name and value parameters + + Expected results: + Test passes if ScoreMetric object name, value and type attributes and __eq__ and __repr__ methods return + expected values + + Steps + 1. Check name, value and type attributes for ScoreMetric object + 2. Check ValueError exception raised when ScoreMetric name parameter is float NaN + 3. Check __eq__ method for ScoreMetric object + 4. Check __repr__ method for ScoreMetric object + """ + # Checking ScoreMetric object attributes + score_metric = self.score_metric() + assert score_metric.name == "Test ScoreMetric" + assert score_metric.value == 2.0 + assert score_metric.type() == "score" + # Checking exception raised when value is NaN + with pytest.raises(ValueError): + ScoreMetric(name="Test ScoreMetric", value=float("nan")) + # Checking __eq__ method + equal_score_metric = ScoreMetric(name="Test ScoreMetric", value=2.0) + # Checking __eq__ method for equal ScoreMetric objects + assert score_metric == equal_score_metric + # Checking __eq__ method for ScoreMetric objects with unequal names + different_name_score_metric = ScoreMetric(name="Other name ScoreMetric", value=2.0) + assert score_metric != different_name_score_metric + # Checking __eq__ method for ScoreMetric objects with unequal values + different_value_score_metric = ScoreMetric(name="Test ScoreMetric", value=3.4) + assert score_metric != different_value_score_metric + # Checking __eq__ method by comparing ScoreMetric object with different type object + assert score_metric != str + # Checking __repr__ method + assert repr(score_metric) == "ScoreMetric(name=`Test ScoreMetric`, score=`2.0`)" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestCurveMetric: + @staticmethod + def ys() -> list: + return [2.0, 4.1, 3.3, 8.2, 7.1] + + def curve_metric(self) -> CurveMetric: + xs = [0.0, 0.1, 0.2, 0.3, 0.4] + return CurveMetric(name="Test CurveMetric", ys=self.ys(), xs=xs) + + def x_not_specified_curve_metric(self) -> CurveMetric: + return CurveMetric(name="x not specified CurveMetric", ys=self.ys()) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_curve_metric(self): + """ + Description: + Check CurveMetric class + + Input data: + CurveMetric object with specified name, xs and ys parameters + + Expected results: + Test passes if CurveMetric object has expected attributes and methods + + Steps + 1. Check name, ys, xs and type attributes of CurveMetric object + 2. Check name, ys, xs and type attributes of CurveMetric object with not specified xs initialization parameter + 3. Check ValueError exception raised when length of ys CurveMetric object initialization parameter is not equal + to length + of xs parameter + 4. Check __repr__ method for CurveMetric object + """ + # Checking positive scenario + curve_metric = self.curve_metric() + assert curve_metric.name == "Test CurveMetric" + assert curve_metric.xs == [0.0, 0.1, 0.2, 0.3, 0.4] + assert curve_metric.ys == self.ys() + assert curve_metric.type() == "curve" + # Checking positive scenario with not specified xs parameter + x_not_specified_curve_metric = self.x_not_specified_curve_metric() + assert x_not_specified_curve_metric.name == "x not specified CurveMetric" + assert x_not_specified_curve_metric.xs == [1, 2, 3, 4, 5] + assert x_not_specified_curve_metric.ys == self.ys() + assert x_not_specified_curve_metric.type() == "curve" + # Checking ValueError exception raised when len(ys) != len(xs) + with pytest.raises(ValueError): + CurveMetric(name="Negative CurveMetric Scenario", ys=[0.0, 0.1], xs=[1, 2, 3]) + # Checking __repr__ method + assert repr(curve_metric) == "CurveMetric(name=`Test CurveMetric`, ys=(5 values), xs=(5 values))" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestNullMetric: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_null_metric(self): + """ + Description: + Check NullMetric class + + Input data: + NullMetric object + + Expected results: + Test passes if NullMetric object name and type attributes and __repr__ and __eq__ methods return expected values + """ + # Checking NullMetric attributes + null_metric = NullMetric() + assert null_metric.name == "NullMetric" + assert null_metric.type() == "null" + # Checking NullMetric __repr__ method + assert repr(null_metric) == "NullMetric()" + # Checking NullMetric __eq__ method + equal_null_metric = NullMetric() + assert null_metric == equal_null_metric + # Checking __eq__ method by comparing NullMetric object with different type object + assert null_metric != TestInfoMetric().info_metric() + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestVisualizationType: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_visualization_type(self): + """ + Description: + Check VisualizationType Enum class elements + + Expected results: + Test passes if VisualizationType Enum class length equal expected value and its elements have expected + sequence numbers + """ + assert len(VisualizationType) == 5 + assert VisualizationType.TEXT.value == 0 + assert VisualizationType.RADIAL_BAR.value == 1 + assert VisualizationType.BAR.value == 2 + assert VisualizationType.LINE.value == 3 + assert VisualizationType.MATRIX.value == 4 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestColorPalette: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_color_palette(self): + """ + Description: + Check ColorPalette Enum class elements + + Expected results: + Test passes if ColorPalette Enum class length equal expected value and its elements have expected + sequence numbers + """ + assert len(ColorPalette) == 2 + assert ColorPalette.DEFAULT.value == 0 + assert ColorPalette.LABEL.value == 1 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestVisualizationInfo: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_visualization_info(self): + """ + Description: + Check VisualizationInfo class + + Input data: + VisualizationInfo object with specified name, visualisation_type and palette parameters + + Expected results: + Test passes if VisualizationInfo object name and palette parameters and type and __repr__ methods return + expected values + + Steps + 1. Check name, type and palette attributes and __repr__ method for VisualizationInfo object with not specified + palette parameter + 2. Check name, type and palette attributes and __repr__ method for VisualizationInfo object with specified + palette parameter + """ + # Checks for not specified palette parameter + no_palette_specified_name = "No palette specified VisualizationInfo" + for visualisation_type in VisualizationType: + visualisation_info = VisualizationInfo( + name=no_palette_specified_name, visualisation_type=visualisation_type + ) + assert visualisation_info.name == no_palette_specified_name + assert visualisation_info.type == visualisation_type + assert visualisation_info.palette == ColorPalette.DEFAULT + assert repr(visualisation_info) == ( + f"VisualizationInfo(name='No palette specified VisualizationInfo', " + f"type='{visualisation_type.name}', palette='DEFAULT')" + ) + # Check for specified palette parameter + palette_specified_name = "VisualizationInfo with palette parameter set to LABEL" + for visualisation_type in VisualizationType: + visualisation_info = VisualizationInfo( + name=palette_specified_name, + visualisation_type=visualisation_type, + palette=ColorPalette.LABEL, + ) + assert visualisation_info.name == palette_specified_name + assert visualisation_info.type == visualisation_type + assert visualisation_info.palette == ColorPalette.LABEL + assert repr(visualisation_info) == ( + f"VisualizationInfo(name='VisualizationInfo with palette parameter set " + f"to LABEL', type='{visualisation_type.name}', palette='LABEL')" + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTextChartInfo: + @staticmethod + def text_chart_info(): + return TextChartInfo("Test TextChartInfo") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_text_chart_info(self): + """ + Description: + Check TextChartInfo class + + Input data: + TextChartInfo object with specified name parameter + + Expected results: + Test passes if TextChartInfo object name and type attributes and __repr__ method return expected values + """ + text_chart_info = self.text_chart_info() + assert text_chart_info.name == "Test TextChartInfo" + assert text_chart_info.type == VisualizationType.TEXT + assert repr(text_chart_info) == ("TextChartInfo(name='Test TextChartInfo, " "'type='VisualizationType.TEXT')") + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLineChartInfo: + @staticmethod + def default_parameters_line_chart_info(): + return LineChartInfo("Test default parameters LineChartInfo") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_line_chart_info(self): + """ + Description: + Check LineChartInfo class + + Input data: + LineChartInfo object with specified name, x_axis_label, y_axis_label and palette parameters + + Expected results: + Test passes if LineChartInfo object name, x_axis_label, y_axis_label, palette and type attributes and + __repr__ method return expected values + + Steps + 1. Check name, x_axis_label, y_axis_label, palette and type attributes and __repr__ method values for + LineChartInfo with specified x_axis_label, y_axis_label and palette parameters + 2. Check name, x_axis_label, y_axis_label, palette and type attributes and __repr__ method values for + LineChartInfo with not specified x_axis_label, y_axis_label and palette parameters + """ + # Scenario for specified parameters + line_chart_info_name = "Test LineChartInfo" + x_axis_label = "Test x-axis label for LineChartInfo" + y_axis_label = "Test y-axis label for LineChartInfo" + palette = ColorPalette.LABEL + parameters_specified_line_chart_info = LineChartInfo( + name=line_chart_info_name, + x_axis_label=x_axis_label, + y_axis_label=y_axis_label, + palette=palette, + ) + assert parameters_specified_line_chart_info.name == line_chart_info_name + assert parameters_specified_line_chart_info.x_axis_label == x_axis_label + assert parameters_specified_line_chart_info.y_axis_label == y_axis_label + assert parameters_specified_line_chart_info.palette == palette + assert parameters_specified_line_chart_info.type == VisualizationType.LINE + assert repr(parameters_specified_line_chart_info) == ( + "LineChartInfo(name='Test LineChartInfo, 'type='VisualizationType.LINE', x_axis_label='Test x-axis " + "label for LineChartInfo', y_axis_label='Test y-axis label for LineChartInfo')" + ) + # Scenario for default parameters + default_values_line_chart_info = self.default_parameters_line_chart_info() + assert default_values_line_chart_info.name == "Test default parameters LineChartInfo" + assert default_values_line_chart_info.x_axis_label == "" + assert default_values_line_chart_info.y_axis_label == "" + assert default_values_line_chart_info.palette == ColorPalette.DEFAULT + assert default_values_line_chart_info.type == VisualizationType.LINE + assert repr(default_values_line_chart_info) == ( + "LineChartInfo(name='Test default parameters LineChartInfo, 'type='VisualizationType.LINE', " + "x_axis_label='', y_axis_label='')" + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestBarChartInfo: + @staticmethod + def default_parameters_bar_chart_info(): + return BarChartInfo(name="BarChartInfo with default parameters") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_bar_chart_info(self): + """ + Description: + Check BarChartInfo class + + Input data: + BarChartInfo object with specified name, palette and visualization_type parameters + + Expected results: + Test passes if BarChartInfo object name, type and palette attributes and __repr__ method return expected values + + Steps + 1. Check name, palette and type attributes and __repr__ method values for BarChartInfo object with specified + palette and visualisation_type parameters + 2. Check name, palette and type attributes and __repr__ method values for BarChartInfo object with not + specified palette and visualisation_type parameters + 3. Check ValueError exception raised when visualization_type BarChartInfo object initialization parameter is + not equal to BAR or RADIAL_BAR + """ + # Scenario for specified parameters + bar_chart_info_name = "Test BarChartInfo" + for visualisation_type in [VisualizationType.BAR, VisualizationType.RADIAL_BAR]: + bar_chart_info = BarChartInfo( + name=bar_chart_info_name, + palette=ColorPalette.LABEL, + visualization_type=visualisation_type, + ) + assert bar_chart_info.name == bar_chart_info_name + assert bar_chart_info.palette == ColorPalette.LABEL + assert bar_chart_info.type == visualisation_type + assert repr(bar_chart_info) == (f"BarChartInfo(name='Test BarChartInfo', " f"type='{visualisation_type}')") + # Scenario for default parameters + default_values_bar_chart_info = self.default_parameters_bar_chart_info() + assert default_values_bar_chart_info.name == "BarChartInfo with default parameters" + assert default_values_bar_chart_info.palette == ColorPalette.DEFAULT + assert default_values_bar_chart_info.type == VisualizationType.BAR + assert repr(default_values_bar_chart_info) == ( + "BarChartInfo(name='BarChartInfo with default parameters', " "type='VisualizationType.BAR')" + ) + # Check ValueError exception raised when visualization_type not equal to BAR or RADIAL_BAR + for visualisation_type in [ + VisualizationType.TEXT, + VisualizationType.LINE, + VisualizationType.MATRIX, + ]: + with pytest.raises(ValueError): + BarChartInfo(name=bar_chart_info_name, visualization_type=visualisation_type) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestMatrixChartInfo: + @staticmethod + def default_values_matrix_chart_info(): + return MatrixChartInfo("Test MatrixCharInfo with default parameters") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_matrix_chart_info(self): + """ + Description: + Check MatrixChartInfo class + + Input data: + MatrixChartInfo object with specified name, header, row_header, column_header and palette parameters + + Expected results: + Test passes if MatrixChartInfo object name, header, row_header, column_header, palette and type attributes and + __repr__ method return expected values + + Steps + 1. Check name, header, row_header, column_header, palette and type attributes and __repr__ method values for + MatrixChartInfo object with specified parameters + 2. Check name, header, row_header, column_header, palette and type attributes and __repr__ method values for + MatrixChartInfo object with default parameters + """ + # Check for specified parameters + matrix_chart_info_name = "Test MatrixChartInfo" + matrix_chart_info_header = "Header of Test MatrixChartInfo" + matrix_chart_info_row_header = "Specified row header" + matrix_chart_info_column_header = "Specified column header" + matrix_chart_info = MatrixChartInfo( + name=matrix_chart_info_name, + header=matrix_chart_info_header, + row_header=matrix_chart_info_row_header, + column_header=matrix_chart_info_column_header, + palette=ColorPalette.LABEL, + ) + assert matrix_chart_info.name == matrix_chart_info_name + assert matrix_chart_info.header == matrix_chart_info_header + assert matrix_chart_info.row_header == matrix_chart_info_row_header + assert matrix_chart_info.column_header == matrix_chart_info_column_header + assert matrix_chart_info.palette == ColorPalette.LABEL + assert matrix_chart_info.type == VisualizationType.MATRIX + assert repr(matrix_chart_info) == ( + "MatrixChartInfo(name='Test MatrixChartInfo', type='VisualizationType.MATRIX', header='Header of Test " + "MatrixChartInfo', row_header='Specified row header', column_header='Specified column header')" + ) + # Check for default parameters + default_parameters_matrix_chart_info = self.default_values_matrix_chart_info() + assert default_parameters_matrix_chart_info.name == "Test MatrixCharInfo with default parameters" + assert default_parameters_matrix_chart_info.palette == ColorPalette.DEFAULT + assert default_parameters_matrix_chart_info.type == VisualizationType.MATRIX + with pytest.raises(AttributeError): + default_parameters_matrix_chart_info.header + with pytest.raises(AttributeError): + default_parameters_matrix_chart_info.row_header + with pytest.raises(AttributeError): + default_parameters_matrix_chart_info.column_header + with pytest.raises(AttributeError): + repr(default_parameters_matrix_chart_info) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestMatrixMetricsGroup: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_matrix_metrics_group(self): + """ + Description: + Check MatrixMetricsGroup class + + Input data: + MatrixMetricsGroup object with specified metrics and visualization_info parameters + + Expected results: + Test passes if MatrixMetricsGroup object metrics and visualization_info attributes return expected values + + Steps + 1. Check metrics and visualization_info attributes for MatrixMetricsGroup object with specified parameters + 2. Check ValueError raised when MatrixMetricsGroup object has metrics parameter equal to None or empty list + 3. Check ValueError raised when MatrixMetricsGroup object has visualization_info parameter equal to None + """ + # Positive scenario for MatrixMetricsGroup object with specified parameters + with warnings.catch_warnings(): + # there is a matrix with zero sum in row, so we expect 0/0 division. + warnings.filterwarnings("ignore", "invalid value encountered in true_divide") + matrix_metrics = [ + TestMetrics().normalized_matrix_metric(), + TestMetrics().normalized_matrix_zero_sum(), + ] + matrix_chart_info = TestMatrixChartInfo.default_values_matrix_chart_info() + matrix_metrics_group = MatrixMetricsGroup(metrics=matrix_metrics, visualization_info=matrix_chart_info) + assert matrix_metrics_group.metrics == matrix_metrics + assert matrix_metrics_group.visualization_info == matrix_chart_info + # Negative scenarios for MatrixMetricsGroup object with metrics parameter equal to None or [] + for incorrect_metrics in [None, []]: + with pytest.raises(ValueError): + MatrixMetricsGroup(metrics=incorrect_metrics, visualization_info=matrix_chart_info) + # Negative scenario for MatrixMetricsGroup object with visualization_info parameter equal to None + with pytest.raises(ValueError): + MatrixMetricsGroup(metrics=matrix_metrics, visualization_info=None) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLineMetricsGroup: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_line_metrics_group(self): + """ + Description: + Check LineMetricsGroup class + + Input data: + LineMetricsGroup object with specified metrics and visualization_info parameters + + Expected results: + Test passes if LineMetricsGroup object metrics and visualization_info attributes return expected values + + Steps + 1. Check metrics and visualization_info attributes for LineMetricsGroup object with specified parameters + 2. Check ValueError raised when LineMetricsGroup object has metrics parameter equal to None or empty tuple + 3. Check ValueError raised when LineMetricsGroup object has visualization_info parameter equal to None + """ + # Positive scenario for TestLineMetricsGroup object with specified parameters + curve_metrics = ( + TestCurveMetric().curve_metric(), + TestCurveMetric().x_not_specified_curve_metric(), + ) + line_chart_info = TestLineChartInfo().default_parameters_line_chart_info() + line_metrics_group = LineMetricsGroup(metrics=curve_metrics, visualization_info=line_chart_info) + assert line_metrics_group.metrics == curve_metrics + assert line_metrics_group.visualization_info == line_chart_info + # Negative scenarios for LineMetricsGroup object with metrics parameter equal to None or [] + for incorrect_metrics in [None, ()]: + with pytest.raises(ValueError): + LineMetricsGroup(metrics=incorrect_metrics, visualization_info=line_chart_info) + # Negative scenario for LineMetricsGroup object with visualization_info parameter equal to None + with pytest.raises(ValueError): + LineMetricsGroup(metrics=curve_metrics, visualization_info=None) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestBarMetricsGroup: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_bar_metrics_group(self): + """ + Description: + Check BarMetricsGroup class + + Input data: + BarMetricsGroup object with specified metrics and visualization_info parameters + + Expected results: + Test passes if BarMetricsGroup object metrics and visualization_info attributes return expected values + + Steps + 1. Check metrics and visualization_info attributes for BarMetricsGroup object with specified parameters + 2. Check ValueError raised when BarMetricsGroup object has metrics parameter equal to None or empty list + 3. Check ValueError raised when BarMetricsGroup object has visualization_info parameter equal to None + """ + # Positive scenario for BarMetricsGroup object with specified parameters + bar_metrics = [ + TestScoreMetric().score_metric(), + TestCountMetric().count_metric(), + ] + bar_chart_info = TestBarChartInfo().default_parameters_bar_chart_info() + bar_metrics_group = BarMetricsGroup(metrics=bar_metrics, visualization_info=bar_chart_info) + assert bar_metrics_group.metrics == bar_metrics + assert bar_metrics_group.visualization_info == bar_chart_info + # Negative scenarios for BarMetricsGroup object with metrics parameter equal to None or [] + for incorrect_metrics in [None, []]: + with pytest.raises(ValueError): + BarMetricsGroup(metrics=incorrect_metrics, visualization_info=bar_chart_info) + # Negative scenario for BarMetricsGroup object with visualization_info parameter equal to None + with pytest.raises(ValueError): + BarMetricsGroup(metrics=bar_metrics, visualization_info=None) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTextMetricsGroup: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_text_metrics_group(self): + """ + Description: + Check TextMetricsGroup class + + Input data: + TextMetricsGroup object with specified metrics and visualization_info parameters + + Expected results: + Test passes if TextMetricsGroup object metrics and visualization_info attributes return expected values + + Steps + 1. Check metrics and visualization_info attributes for TextMetricsGroup object with specified parameters + 2. Check ValueError raised when TextMetricsGroup object has metrics parameter length more than 1 + 3. Check ValueError raised when TextMetricsGroup object has metrics parameter equal empty tuple + 4. Check ValueError raised when TextMetricsGroup object has visualization_info parameter equal to None + """ + # Positive scenario for TextMetricsGroup object with specified parameters + score_metric = TestScoreMetric().score_metric() + count_metric = TestCountMetric().count_metric() + text_chart_info = TestTextChartInfo().text_chart_info() + for metric in [ + score_metric, + count_metric, + TestInfoMetric().info_metric(), + TestDateMetric().date_metric_no_date_specified(), + TestMetrics().mixed_conditions_duration_metric(), + ]: + text_metric_group = TextMetricsGroup(metrics=[metric], visualization_info=text_chart_info) + assert text_metric_group.metrics == [metric] + assert text_metric_group.visualization_info == text_chart_info + # Negative scenarios for TextMetricsGroup object with metrics parameter length equal to 2 + with pytest.raises(ValueError): + TextMetricsGroup(metrics=(score_metric, count_metric), visualization_info=text_chart_info) + # Negative scenarios for TextMetricsGroup object with metrics parameter equal () + with pytest.raises(ValueError): + TextMetricsGroup(metrics=(), visualization_info=text_chart_info) + # Negative scenario for TextMetricsGroup object with visualization_info parameter equal to None + with pytest.raises(ValueError): + TextMetricsGroup(metrics=[score_metric], visualization_info=None) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestPerformance: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_performance(self): + """ + Description: + Check Performance class + + Input data: + Performance object with specified score and dashboard_metrics parameters + + Expected results: + Test passes if Performance object score and dashboard_metrics attributes and __eq__ and __repr__ method return + expected values + + Steps + 1. Check score and dashboard_metrics attributes for Performance object with not specified dashboard_metrics + parameter + 2. Check score and dashboard_metrics attributes for Performance object with specified dashboard_metrics + parameter + 3. Check __eq__ method for equal Performance objects, Performance objects with different dashboard_metrics + attributes, Performance objects with different score attributes + 4. Check __repr__ method + 5. Check ValueError exception raised when score attributes type not equal to ScoreMetric + """ + # Positive scenario for Performance object with default parameters + score_metric = TestScoreMetric().score_metric() + default_parameters_performance = Performance(score_metric) + assert default_parameters_performance.score == score_metric + assert default_parameters_performance.dashboard_metrics == [] + # Positive scenario for Performance object with specified dashboard_metrics parameter + # Preparing dashboard metrics list + with warnings.catch_warnings(): + # there is a matrix with zero sum in row, so we expect 0/0 division. + warnings.filterwarnings("ignore", "invalid value encountered in true_divide") + matrix_metrics = [ + TestMetrics().normalized_matrix_metric(), + TestMetrics().normalized_matrix_zero_sum(), + ] + matrix_chart_info = TestMatrixChartInfo.default_values_matrix_chart_info() + matrix_metrics_group = MatrixMetricsGroup(metrics=matrix_metrics, visualization_info=matrix_chart_info) + curve_metrics = ( + TestCurveMetric().curve_metric(), + TestCurveMetric().x_not_specified_curve_metric(), + ) + line_chart_info = TestLineChartInfo().default_parameters_line_chart_info() + line_metrics_group = LineMetricsGroup(metrics=curve_metrics, visualization_info=line_chart_info) + bar_metrics = [ + TestScoreMetric().score_metric(), + TestCountMetric().count_metric(), + ] + bar_chart_info = TestBarChartInfo().default_parameters_bar_chart_info() + bar_metrics_group = BarMetricsGroup(metrics=bar_metrics, visualization_info=bar_chart_info) + text_score_metric = TestScoreMetric().score_metric() + text_chart_info = TestTextChartInfo().text_chart_info() + text_metric_group = TextMetricsGroup(metrics=[text_score_metric], visualization_info=text_chart_info) + dashboard_metrics = [ + matrix_metrics_group, + line_metrics_group, + bar_metrics_group, + text_metric_group, + ] + # Checking Performance attributes + specified_parameters_performance = Performance(score=score_metric, dashboard_metrics=dashboard_metrics) + assert specified_parameters_performance.score == score_metric + assert specified_parameters_performance.dashboard_metrics == dashboard_metrics + # Checking __eq__ method + equal_default_parameters_performance = Performance(score_metric) + assert default_parameters_performance == equal_default_parameters_performance + different_metrics_performance = Performance(score_metric, [matrix_metrics_group]) + assert default_parameters_performance == different_metrics_performance + unequal_score_metric = ScoreMetric(name="Unequal ScoreMetric", value=1.0) + assert default_parameters_performance != Performance(unequal_score_metric) + assert default_parameters_performance != str + # Checking __repr__ method + assert repr(default_parameters_performance) == "Performance(score: 2.0, dashboard: (0 metric groups))" + assert repr(specified_parameters_performance) == "Performance(score: 2.0, dashboard: (4 metric groups))" + # Checking ValueError exception raised when score parameter not ScoreMetric class + count_metric = TestCountMetric().count_metric() + with pytest.raises(ValueError): + Performance(count_metric) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestNullPerformance: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_null_performance(self): + """ + Description: + Check NullPerformance class + + Input data: + NullPerformance object + + Expected results: + Test passes if NullPerformance object score and dashboard_metrics attributes and __repr__ and __eq__ methods + return expected values + + Steps + 1. Check NullPerformance object score and dashboard_metrics attributes + 2. Check NullPerformance object __repr__ method + 3. Check NullPerformance object __eq__ method + """ + # Checking NullPerformance score and dashboard_metrics attributes + null_performance = NullPerformance() + assert null_performance.score == ScoreMetric(name="Null score", value=0.0) + assert null_performance.dashboard_metrics == [] + # Checking NullPerformance __repr__ method + assert repr(null_performance) == "NullPerformance()" + # Checking __eq__ method for equal NullPerformance objects + equal_null_performance = NullPerformance() + assert null_performance == equal_null_performance + # Checking NullPerformance __eq__ method by comparing with Performance object + score_metric = TestScoreMetric().score_metric() + performance = Performance(score_metric) + assert null_performance != performance + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestMultiScorePerformance: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_multi_score_performance(self): + """ + Description: + Check MultiScorePerformance class + + Input data: + MultiScorePerformance object with specified score + + Expected results: + Test passes if MultiScorePerformance object score attribute and __eq__ and __repr__ method return + expected values + + Steps + 1. Check primary and additional score attributes for MultiScorePerformance object + 2. Check primary and additional score attributes for MultiScorePerformance object when only primary score is + passed + 3. Check primary and additional score attributes for MultiScorePerformance object when only additional score is + passed + 4. Check __eq__ method for equal and unequal Performance objects + 5. Check __repr__ method + """ + # Positive scenario for Performance object with default parameters + primary_score = TestScoreMetric().score_metric() + additional_score = TestScoreMetric().score_metric() + default_parameters_performance = MultiScorePerformance(primary_score, [additional_score]) + assert default_parameters_performance.score == primary_score + assert default_parameters_performance.primary_score == primary_score + assert default_parameters_performance.additional_scores == [additional_score] + assert default_parameters_performance.dashboard_metrics == [] + # Positive scenario for Performance object with only primary metric + only_primary_performance = MultiScorePerformance(primary_score) + assert only_primary_performance.score == primary_score + assert only_primary_performance.primary_score == primary_score + assert only_primary_performance.additional_scores == [] + assert only_primary_performance.dashboard_metrics == [] + # Positive scenario for Performance object with only additional metric + only_additional_performance = MultiScorePerformance(additional_scores=[additional_score]) + assert only_additional_performance.score == additional_score + assert only_additional_performance.primary_score is None + assert only_additional_performance.additional_scores == [additional_score] + assert only_additional_performance.dashboard_metrics == [] + # Checking __eq__ method + equal_default_parameters_performance = MultiScorePerformance(primary_score, [additional_score]) + assert default_parameters_performance == equal_default_parameters_performance + assert default_parameters_performance != only_primary_performance + # Checking __repr__ method + assert ( + repr(default_parameters_performance) + == "MultiScorePerformance(score: 2.0, primary_metric: ScoreMetric(name=`Test ScoreMetric`, score=`2.0`), " + "additional_metrics: (1 metrics), dashboard: (0 metric groups))" + ) diff --git a/tests/unit/api/entities/test_model.py b/tests/unit/api/entities/test_model.py new file mode 100644 index 00000000000..d9fdf681fa7 --- /dev/null +++ b/tests/unit/api/entities/test_model.py @@ -0,0 +1,393 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import os +import tempfile +from datetime import datetime +from pathlib import Path + +import pytest + +from otx.api.configuration import ConfigurableParameters, cfg_helper +from otx.api.entities.annotation import NullAnnotationSceneEntity +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.metrics import NullPerformance, Performance, ScoreMetric +from otx.api.entities.model import ( + ModelConfiguration, + ModelEntity, + ModelFormat, + ModelOptimizationType, + ModelPrecision, + OptimizationMethod, +) +from otx.api.entities.model_template import TargetDevice, parse_model_template +from otx.api.entities.task_environment import TaskEnvironment +from otx.api.usecases.adapters.model_adapter import ModelAdapter +from otx.api.utils.time_utils import now +from tests.test_helpers import generate_random_single_image +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelPrecision: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_precision(self): + """ + Description: + Check that ModelPrecision correctly returns the precision name + + Expected results: + Test passes if ModelPrecision correctly returns the precision name + + Steps + 1. Check precisions in the ModelPrecision + """ + + model_precision = ModelPrecision + assert len(model_precision) == 4 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelFormat: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_format(self): + """ + Description: + Check that ModelFormat correctly returns the format name + + Expected results: + Test passes if ModelFormat correctly returns the format name + + Steps + 1. Check formats in the ModelFormat + """ + + model_format = ModelFormat + assert len(model_format) == 3 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelOptimizationType: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_optimization_type(self): + """ + Description: + Check that ModelOptimizationType correctly returns the optimization type name + + Expected results: + Test passes if ModelOptimizationType correctly returns the optimization type name + + Steps + 1. Check optimization types in the ModelOptimizationType + """ + + model_optimization_type = ModelOptimizationType + assert len(model_optimization_type) == 5 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestOptimizationMethod: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_optimization_method(self): + """ + Description: + Check that OptimizationMethod correctly returns the optimization method name + + Expected results: + Test passes if OptimizationMethod correctly returns the optimization method name + + Steps + 1. Check optimization methods in the OptimizationMethod + """ + + optimization_method = OptimizationMethod + assert len(optimization_method) == 2 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelConfiguration: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_configuration(self): + """ + Description: + Check that ModelConfiguration correctly returns the configuration + + Input data: + ConfigurableParameters, LabelSchemaEntity + + Expected results: + Test passes if ModelConfiguration correctly returns the configuration + + Steps + 1. Check configuration params in the ModelConfiguration + """ + parameters = ConfigurableParameters(header="Test header") + label_schema = LabelSchemaEntity() + model_configuration = ModelConfiguration(configurable_parameters=parameters, label_schema=label_schema) + assert model_configuration.configurable_parameters == parameters + assert model_configuration.get_label_schema() == label_schema + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelEntity: + creation_date = now() + + def generate_random_image(self): + with generate_random_single_image() as path: + image = Image(file_path=path) + return DatasetItemEntity(media=image, annotation_scene=NullAnnotationSceneEntity()) + + def dataset(self): + return DatasetEntity(items=[self.generate_random_image()]) + + def configuration(self): + parameters = ConfigurableParameters(header="Test header") + label_schema = LabelSchemaEntity() + return ModelConfiguration(configurable_parameters=parameters, label_schema=label_schema) + + def other_configuration(self): + parameters = ConfigurableParameters(header="Other test header") + label_schema = LabelSchemaEntity() + return ModelConfiguration(configurable_parameters=parameters, label_schema=label_schema) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_entity_default_values(self): + """ + Description: + Check that ModelEntity correctly returns the default values + + Expected results: + Test passes if ModelEntity correctly returns the default values + + Steps + 1. Check default values in the ModelEntity + """ + + model_entity = ModelEntity(train_dataset=self.dataset(), configuration=self.configuration()) + + assert model_entity.id_ == ID() + assert type(model_entity.configuration) == ModelConfiguration + assert type(model_entity.creation_date) == datetime + assert type(model_entity.train_dataset) == DatasetEntity + assert model_entity.version == 1 + assert model_entity.model_format == ModelFormat.OPENVINO + assert model_entity.precision == [ModelPrecision.FP32] + assert model_entity.target_device == TargetDevice.CPU + assert model_entity.optimization_type == ModelOptimizationType.NONE + assert model_entity.performance == NullPerformance() + + for default_val_none in [ + "previous_trained_revision", + "previous_revision", + "target_device_type", + ]: + assert getattr(model_entity, default_val_none) is None + + for default_val_0_0 in ["training_duration", "model_size_reduction"]: + assert getattr(model_entity, default_val_0_0) == 0.0 + + for default_val_empty_list in ["tags", "optimization_methods"]: + assert getattr(model_entity, default_val_empty_list) == [] + + for default_val_empty_dict in [ + "model_adapters", + "optimization_objectives", + "performance_improvement", + ]: + assert getattr(model_entity, default_val_empty_dict) == {} + + for default_val_zero in ["latency", "fps_throughput"]: + assert getattr(model_entity, default_val_zero) == 0 + + assert model_entity.is_optimized() is False + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_entity_sets_values(self): + """ + Description: + Check that ModelEntity correctly returns the set values + + Expected results: + Test passes if ModelEntity correctly returns the set values + + Steps + 1. Check set values in the ModelEntity + """ + + def __get_path_to_file(filename: str): + """ + Return the path to the file named 'filename', which lives in the tests/entities directory + """ + return str(Path(__file__).parent / Path(filename)) + + car = LabelEntity(name="car", domain=Domain.DETECTION) + labels_list = [car] + dummy_template = __get_path_to_file("./dummy_template.yaml") + model_template = parse_model_template(dummy_template) + hyper_parameters = model_template.hyper_parameters.data + params = cfg_helper.create(hyper_parameters) + labels_schema = LabelSchemaEntity.from_labels(labels_list) + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + + item = self.generate_random_image() + dataset = DatasetEntity(items=[item]) + score_metric = ScoreMetric(name="Model accuracy", value=0.5) + + model_entity = ModelEntity(train_dataset=self.dataset(), configuration=self.configuration()) + + set_params = { + "configuration": environment.get_model_configuration(), + "train_dataset": dataset, + "id": ID(1234567890), + "creation_date": self.creation_date, + "previous_trained_revision": 5, + "previous_revision": 2, + "version": 2, + "tags": ["tree", "person"], + "model_format": ModelFormat.BASE_FRAMEWORK, + "performance": Performance(score_metric), + "training_duration": 5.8, + "precision": [ModelPrecision.INT8], + "latency": 328, + "fps_throughput": 20, + "target_device": TargetDevice.GPU, + "target_device_type": "notebook", + "optimization_methods": [OptimizationMethod.QUANTIZATION], + "optimization_type": ModelOptimizationType.MO, + "optimization_objectives": {"param": "Test param"}, + "performance_improvement": {"speed", 0.5}, + "model_size_reduction": 1.0, + } + + for key, value in set_params.items(): + setattr(model_entity, key, value) + assert getattr(model_entity, key) == value + + assert model_entity.is_optimized() is True + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_entity_model_adapters(self): + """ + Description: + Check that ModelEntity correctly returns the adapters + + Expected results: + Test passes if ModelEntity correctly returns the adapters + + Steps + 1. Create a ModelEntity with adapters + 2. Change data source for an adapter + 3. Remove an adapter + """ + + data_source_0 = b"{0: binaryrepo://localhost/repo/data_source/0}" + data_source_1 = b"binaryrepo://localhost/repo/data_source/1" + data_source_2 = b"binaryrepo://localhost/repo/data_source/2" + data_source_3 = b"binaryrepo://localhost/repo/data_source/3" + + temp_dir = tempfile.TemporaryDirectory() + temp_file = os.path.join(temp_dir.name, "data_source_0") + + with open(temp_file, "wb") as tmp: + tmp.write(data_source_0) + + model_adapters = { + "0": ModelAdapter(data_source=data_source_0), + "1": ModelAdapter(data_source=data_source_1), + "2": ModelAdapter(data_source=data_source_2), + } + + model_entity = ModelEntity( + train_dataset=self.dataset(), + configuration=self.configuration(), + model_adapters=model_adapters, + ) + + # Adapter with key 0 not from file + assert model_entity.model_adapters["0"].from_file_storage is False + + model_entity.set_data("0", temp_file) + + for adapter in model_entity.model_adapters: + if adapter == "0": + # Adapter with key 0 from file + assert model_entity.model_adapters[adapter].from_file_storage is True + else: + assert model_entity.model_adapters[adapter].from_file_storage is False + + assert model_entity.get_data("1") == data_source_1 + + model_entity.set_data("2", data_source_1) + assert model_entity.get_data("2") == data_source_1 + assert len(model_entity.model_adapters) == 3 + + model_entity.set_data("3", data_source_3) + assert model_entity.get_data("3") == data_source_3 + assert len(model_entity.model_adapters) == 4 + + model_entity.delete_data("3") + assert len(model_entity.model_adapters) == 3 + + # Attempt to retrieve a missing and deleted key + with pytest.raises(KeyError): + model_entity.get_data("5") + + with pytest.raises(KeyError): + model_entity.get_data("3") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_entity__eq__(self): + """ + Description: + Check that ModelEntity __eq__ method + + Expected results: + Test passes if ModelEntity equal ModelEntity and not equal another type + """ + dataset = self.dataset() + other_model_entity = ModelEntity(train_dataset=dataset, configuration=self.configuration()) + model_entity = ModelEntity(train_dataset=dataset, configuration=self.configuration()) + third_model_entity = ModelEntity(train_dataset=self.dataset(), configuration=self.other_configuration()) + assert model_entity.__eq__("") is False + assert model_entity == other_model_entity + assert model_entity != third_model_entity diff --git a/tests/unit/api/entities/test_model_template.py b/tests/unit/api/entities/test_model_template.py new file mode 100644 index 00000000000..d00b4f5dc19 --- /dev/null +++ b/tests/unit/api/entities/test_model_template.py @@ -0,0 +1,1187 @@ +"""Tests for model template entity""" +# Copyright (C) 2020-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import copy +import itertools +from os import remove +from pathlib import Path + +import pytest +import yaml + +from otx.api.entities.label import Domain +from otx.api.entities.model_template import ( + ANOMALY_TASK_TYPES, + TRAINABLE_TASK_TYPES, + DatasetRequirements, + Dependency, + EntryPoints, + ExportableCodePaths, + HyperParameterData, + InstantiationType, + ModelOptimizationMethod, + ModelCategory, + ModelStatus, + ModelTemplate, + NullModelTemplate, + TargetDevice, + TaskFamily, + TaskType, + _parse_model_template_from_omegaconf, + parse_model_template, + parse_model_template_from_dict, + task_type_to_label_domain, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +class CommonMethods: + @staticmethod + def cut_parameter_overrides_from_model_template(save_file: bool = True) -> dict: + """Function saves config file with removed override_parameters node. Returns dictionary with path + to new config file and dictionary with override_parameters""" + with open(TestHyperParameterData().model_template_path()) as model_to_copy: + model_data = yaml.safe_load(model_to_copy) + override_parameters = model_data["hyper_parameters"]["parameter_overrides"] + model_data["hyper_parameters"].pop("parameter_overrides") + new_config_path = TestHyperParameterData.get_path_to_file(r"./no_overrides_template.yaml") + if save_file: + with open(new_config_path, "w+") as new_config: + new_config.write(yaml.dump(model_data)) + return { + "override_parameters": override_parameters, + "new_config_path": new_config_path, + } + + @staticmethod + def check_model_attributes(model: ModelTemplate, expected_values: dict): + assert model.model_template_id == expected_values.get("model_template_id") + assert model.model_template_path == expected_values.get("model_template_path") + assert model.name == expected_values.get("name") + assert model.task_family == expected_values.get("task_family") + assert model.task_type == expected_values.get("task_type") + assert model.instantiation == expected_values.get("instantiation") + assert model.summary == expected_values.get("summary", "") + assert model.framework == expected_values.get("framework") + assert model.max_nodes == expected_values.get("max_nodes", 1) + assert model.application == expected_values.get("application") + assert model.dependencies == expected_values.get("dependencies", []) + assert model.initial_weights == expected_values.get("initial_weights") + assert model.training_targets == expected_values.get("training_targets", []) + assert model.inference_targets == expected_values.get("inference_targets", []) + assert model.dataset_requirements == expected_values.get( + "dataset_requirements", DatasetRequirements(classes=None) + ) + assert model.model_optimization_methods == expected_values.get("model_optimization_methods", []) + assert model.hyper_parameters == expected_values.get( + "hyper_parameters", + HyperParameterData(base_path=None, parameter_overrides={}), + ) + assert model.is_trainable == expected_values.get("is_trainable", True) + assert model.capabilities == expected_values.get("capabilities", []) + assert model.grpc_address == expected_values.get("grpc_address") + assert model.entrypoints == expected_values.get("entrypoints") + assert model.exportable_code_paths == expected_values.get( + "exportable_code_paths", ExportableCodePaths(default=None, openvino=None) + ) + assert model.task_type_sort_priority == expected_values.get("task_type_sort_priority", -1) + assert model.gigaflops == expected_values.get("gigaflops", 0) + assert model.size == expected_values.get("size", 0) + assert model.model_category == expected_values.get("model_category", ModelCategory.OTHER) + assert model.model_status == expected_values.get("model_status", ModelStatus.ACTIVE) + assert model.is_default_for_task == expected_values.get("is_default_for_task", False) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTargetDevice: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_targetdevice(self): + """ + Description: + Check TargetDevice IntEnum class elements + Expected results: + Test passes if TargetDevice IntEnum class length is equal to expected value and its elements have expected + sequence number + """ + assert len(TargetDevice) == 4 + assert TargetDevice.UNSPECIFIED.value == 1 + assert TargetDevice.CPU.value == 2 + assert TargetDevice.GPU.value == 3 + assert TargetDevice.VPU.value == 4 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelOptimizationMethod: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_modeloptymizationmethod(self): + """ + Description: + Check ModelOptimizationMethod Enum class elements + Expected results: + Test passes if ModelOptimizationMethod Enum class length, methods and attributes return expected values + Steps + 1. Check ModelOptimizationMethod length + 2. Check ModelOptimizationMethod elements value attribute + 3. Check ModelOptimizationMethod str method + """ + assert len(ModelOptimizationMethod) == 2 + assert ModelOptimizationMethod.TENSORRT.value == 1 + assert ModelOptimizationMethod.OPENVINO.value == 2 + assert str(ModelOptimizationMethod.TENSORRT) == "TENSORRT" + assert str(ModelOptimizationMethod.OPENVINO) == "OPENVINO" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDatasetRequirements: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_datasetrequirements(self): + """ + Description: + Check DatasetRequirements dataclass + Expected results: + Test passes if classes attribute of DatasetRequirements dataclass returns expected values + """ + classes_list = ["class_1", "class_2"] + test_dataset_requirements = DatasetRequirements(classes_list) + equal_dataset_requirements = DatasetRequirements(classes_list) + other_test_dataset_requirements = DatasetRequirements(["class_1", "class_3"]) + assert test_dataset_requirements.classes == ["class_1", "class_2"] + assert other_test_dataset_requirements.classes == ["class_1", "class_3"] + assert test_dataset_requirements == equal_dataset_requirements + assert test_dataset_requirements != other_test_dataset_requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestExportableCodePaths: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_exportablecodepaths(self): + """ + Description: + Check ExportableCodePaths dataclass + Expected results: + Test passes if default, openvino attributes of ExportableCodePaths dataclass return expected values + """ + exportable_code_paths = ExportableCodePaths("default code path", "openvino code path") + equal_exportable_code_paths = ExportableCodePaths("default code path", "openvino code path") + unequal_exportable_code_paths = ExportableCodePaths("other default code path", "openvino code path") + assert exportable_code_paths.default == "default code path" + assert exportable_code_paths.openvino == "openvino code path" + assert exportable_code_paths == equal_exportable_code_paths + assert exportable_code_paths != unequal_exportable_code_paths + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTaskFamily: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_taskfamily(self): + """ + Description: + Check TaskFamily Enum class elements + Expected results: + Test passes if TaskFamily Enum class length, attributes and methods return expected values + Steps + 1. Check TaskFamily length + 2. Check TaskFamily elements value attribute + 3. Check TaskFamily str method + """ + assert len(TaskFamily) == 3 + assert TaskFamily.VISION.value == 1 + assert TaskFamily.FLOW_CONTROL.value == 2 + assert TaskFamily.DATASET.value == 3 + assert str(TaskFamily.VISION) == "VISION" + assert str(TaskFamily.FLOW_CONTROL) == "FLOW_CONTROL" + assert str(TaskFamily.DATASET) == "DATASET" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTaskType: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_tasktype(self): + """ + Description: + Check TaskType Enum class elements + Expected results: + Test passes if TaskType Enum class length, attributes and methods return expected values + Steps + 1. Check TaskType length + 2. Check TaskType elements value attribute + 3. Check TaskType str method + """ + assert len(TaskType) == 16 + assert TaskType.NULL.value == 1 + assert TaskType.DATASET.value == 2 + assert TaskType.CLASSIFICATION.value == 3 + assert TaskType.SEGMENTATION.value == 4 + assert TaskType.DETECTION.value == 5 + assert TaskType.ANOMALY_DETECTION.value == 6 + assert TaskType.CROP.value == 7 + assert TaskType.TILE.value == 8 + assert TaskType.INSTANCE_SEGMENTATION.value == 9 + assert TaskType.ACTIVELEARNING.value == 10 + assert TaskType.ANOMALY_SEGMENTATION.value == 11 + assert TaskType.ANOMALY_CLASSIFICATION.value == 12 + assert TaskType.ROTATED_DETECTION.value == 13 + assert TaskType.ACTION_CLASSIFICATION.value == 14 + assert TaskType.ACTION_DETECTION.value == 15 + assert TaskType.VISUAL_PROMPTING.value == 16 + assert str(TaskType.NULL) == "NULL" + assert str(TaskType.DATASET) == "DATASET" + assert str(TaskType.CLASSIFICATION) == "CLASSIFICATION" + assert str(TaskType.SEGMENTATION) == "SEGMENTATION" + assert str(TaskType.DETECTION) == "DETECTION" + assert str(TaskType.ANOMALY_DETECTION) == "ANOMALY_DETECTION" + assert str(TaskType.CROP) == "CROP" + assert str(TaskType.TILE) == "TILE" + assert str(TaskType.INSTANCE_SEGMENTATION) == "INSTANCE_SEGMENTATION" + assert str(TaskType.ACTIVELEARNING) == "ACTIVELEARNING" + assert str(TaskType.ANOMALY_SEGMENTATION) == "ANOMALY_SEGMENTATION" + assert str(TaskType.ANOMALY_CLASSIFICATION) == "ANOMALY_CLASSIFICATION" + assert str(TaskType.ROTATED_DETECTION) == "ROTATED_DETECTION" + assert str(TaskType.ACTION_CLASSIFICATION) == "ACTION_CLASSIFICATION" + assert str(TaskType.ACTION_DETECTION) == "ACTION_DETECTION" + assert str(TaskType.VISUAL_PROMPTING) == "VISUAL_PROMPTING" + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_tasktype_to_label_domain(self): + """ + Description: + Check task_type_to_label_domain function + Expected results: + Test passes if task_type_to_label_domain function returns expected mapping of TaskType class element + to Domain class element + expected values + Steps + 1. Check positive scenario with existing TaskType element + 2. Check ValueError exception raised when requests non-existing TaskType element + """ + assert task_type_to_label_domain(TaskType.CLASSIFICATION) == Domain.CLASSIFICATION + assert task_type_to_label_domain(TaskType.DETECTION) == Domain.DETECTION + assert task_type_to_label_domain(TaskType.SEGMENTATION) == Domain.SEGMENTATION + assert task_type_to_label_domain(TaskType.INSTANCE_SEGMENTATION) == Domain.INSTANCE_SEGMENTATION + assert task_type_to_label_domain(TaskType.ANOMALY_CLASSIFICATION) == Domain.ANOMALY_CLASSIFICATION + assert task_type_to_label_domain(TaskType.ANOMALY_DETECTION) == Domain.ANOMALY_DETECTION + assert task_type_to_label_domain(TaskType.ANOMALY_SEGMENTATION) == Domain.ANOMALY_SEGMENTATION + for not_mapped_task in [ + TaskType.NULL, + TaskType.DATASET, + TaskType.CROP, + TaskType.TILE, + TaskType.ACTIVELEARNING, + "key", + ]: + with pytest.raises(ValueError): + task_type_to_label_domain(not_mapped_task) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestHyperParameterData: + @staticmethod + def get_path_to_file(filename: str) -> str: + """Return the path to the file named 'filename', which saved in tests/entities directory""" + return str(Path(__file__).parent / Path(filename)) + + def config_path(self) -> str: + return self.get_path_to_file(r"./dummy_config.yaml") + + def model_template_path(self) -> str: + return self.get_path_to_file(r"./dummy_template.yaml") + + @staticmethod + def parameter_overrides() -> dict: + return { + "learning_parameters": { + "batch_size": {"default_value": 10}, + "learning_rate": {"default_value": 0.5}, + "learning_rate_warmup_iters": {"min_value": 10}, + "num_checkpoints": {"max_value": 95}, + "num_iters": {"min_value": 2}, + "num_workers": {"description": "New workers description"}, + }, + "postprocessing": { + "confidence_threshold": {"default_value": 0.4}, + "result_based_confidence_threshold": {"header": "New header"}, + }, + } + + def remove_value_key_from_config(self, config_content: dict) -> None: + """Function removes "value" key from config dictionary""" + config_content_copy = copy.deepcopy(config_content) + for key, value in config_content_copy.items(): + if isinstance(value, dict): + if key != "ui_rules": + self.remove_value_key_from_config(config_content[key]) + elif key == "value": + config_content.pop(key) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_hyperparameterdata_data(self): + """ + Description: + Check data property of HyperParameterData class instance + Expected results: + Test passes if data property of HyperParameterData class instance has expected values + Steps + 1. Check that data attribute of HyperParameterData object has empty dictionary before using load_parameters + method + 2. Check scenario when path to config.yaml file specified in base_path parameter during HyperParameterData + object initiation + """ + + def open_config_file_and_remove_value_key() -> dict: + """Function returns config file dictionary with removed value key""" + with open(TestHyperParameterData().config_path()) as config_file: + config_content = yaml.safe_load(config_file) + self.remove_value_key_from_config(config_content) + return config_content + + # Forming expected data dictionary + expected_config_data = open_config_file_and_remove_value_key() + # Creating model template with no override_params_node + model_template = CommonMethods.cut_parameter_overrides_from_model_template() + model_template_path = model_template.get("new_config_path") + # Checking data attribute of HyperParameterData class instance + hyper_parameter_data = HyperParameterData(base_path=self.config_path()) + assert hyper_parameter_data.data == {} + hyper_parameter_data.load_parameters(model_template_path) + assert hyper_parameter_data.data == expected_config_data + remove(model_template_path) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_remove_parameter_values_from_data(self): + """ + Description: + Check remove_parameter_values_from_data method of HyperParameterData class instance + Expected results: + Test passes if data property of HyperParameterData class instance has no "value" key after using + load_parameters method + Steps + 1. Check that yaml config file has "value" keys + 2. Check that data property of HyperParameterData class instance has no value key after using + load_parameters method + """ + + def search_value_key_in_config(config_content: dict) -> bool: + """ + Function returns "True" if "value" key presents in config dictionary and "False" if absents + """ + is_value_key_exists = False + config_content_copy = copy.deepcopy(config_content) + for key, value in config_content_copy.items(): + if isinstance(value, dict): + if key != "ui_rules": + if search_value_key_in_config(config_content[key]): + is_value_key_exists = True + break + elif key == "value": + is_value_key_exists = True + return is_value_key_exists + + with open(self.config_path()) as config_file: + config_data = yaml.safe_load(config_file) + # Checking test dataset + assert search_value_key_in_config(config_data) + model_template = CommonMethods.cut_parameter_overrides_from_model_template() + model_template_path = model_template.get("new_config_path") + hyper_parameter_data = HyperParameterData(base_path=self.config_path()) + hyper_parameter_data.load_parameters(model_template_path) + assert not search_value_key_in_config(hyper_parameter_data.data) + remove(model_template_path) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_hyperparameterdata_has_overrides(self): + """ + Description: + Check has_overrides property of HyperParameterData class instance + Expected results: + Test passes if has_overrides property of HyperParameterData class instance has expected values + Steps + 1. Check scenario when HyperParameterData object has specified parameter_overrides dictionary + 2. Check scenario when parameter_overrides dictionary not specified for HyperParameterData object + """ + model_template = CommonMethods.cut_parameter_overrides_from_model_template() + model_template_path = model_template.get("new_config_path") + hyper_parameter_data = HyperParameterData( + base_path=self.config_path(), + parameter_overrides=model_template.get("override_parameters"), + ) + hyper_parameter_data.load_parameters(model_template_path) + has_overrides_hyper_parameter_data = HyperParameterData( + base_path=self.config_path(), parameter_overrides=self.parameter_overrides() + ) + assert has_overrides_hyper_parameter_data.has_overrides + has_overrides_hyper_parameter_data.load_parameters(self.model_template_path()) + assert has_overrides_hyper_parameter_data.has_overrides + no_overrides_hyper_parameter_data = HyperParameterData(base_path=self.config_path()) + assert not no_overrides_hyper_parameter_data.has_overrides + no_overrides_hyper_parameter_data.load_parameters(model_template_path) + assert not no_overrides_hyper_parameter_data.has_overrides + remove(model_template_path) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_hyperparameterdata_has_valid_configurable_parameters(self): + """ + Description: + Check has_valid_configurable_parameters property of HyperParameterData class instance + Expected results: + Test passes if has_valid_configurable_parameters property of HyperParameterData class instance has + expected values + Steps + 1. Check scenario when base_path parameter specified during HyperParameterData object initiation and + model_template_path file specified in load_parameters method exists + 2. Check scenario when __has_valid_configurable_parameters returns False when base_path parameter + not specified during HyperParameterData object initiation + 3. Check scenario when __has_valid_configurable_parameters returns False when model_template_path file + specified in load_parameters method not exists + 4. Check that ValueError exception raised when base_path file has unexpected structure + """ + # positive scenario when expected has_valid_configurable_parameters = True + model_template = CommonMethods.cut_parameter_overrides_from_model_template() + model_template_path = model_template.get("new_config_path") + hyper_parameter_data = HyperParameterData(base_path=self.config_path()) + hyper_parameter_data.load_parameters(model_template_path) + assert hyper_parameter_data.has_valid_configurable_parameters + # scenario when base_path parameter not specified + no_config_specified_data = HyperParameterData() + no_config_specified_data.load_parameters(model_template_path) + assert not no_config_specified_data.base_path + assert not no_config_specified_data.has_valid_configurable_parameters + # scenario when model_template_path parameter not exists + config_path = self.config_path() + no_model_specified_data = HyperParameterData(config_path) + no_model_specified_data.load_parameters(r"./file_not_exists.yaml") + assert no_model_specified_data.base_path == config_path + assert not no_config_specified_data.has_valid_configurable_parameters + # check for incorrect config file + incorrect_config_yaml_path = self.get_path_to_file(r"./incorrect_config.yaml") + with open(incorrect_config_yaml_path, "w+") as incorrect_yaml_file: + incorrect_yaml_file.write("[]") + incorrect_config_data = HyperParameterData(incorrect_config_yaml_path) + with pytest.raises(ValueError): + incorrect_config_data.load_parameters(model_template_path) + remove(model_template_path) + remove(incorrect_config_yaml_path) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_hyperparameterdata_substitute_parameter_overrides(self): + """ + Description: + Check substitute_parameter_overrides method of HyperParameterData class instance + Expected results: + Test passes if substitute_parameter_overrides method returns data attribute of HyperParameterData class + instance has expected value + Steps + 1. Check positive scenario with valid parameter_overrides dictionary + 2. Check negative scenario with unexpected key to override + 3. Check negative scenario with "value" key to override in parameter_overrides dictionary + """ + # positive scenario with valid overrides parameters dictionary + model_template = CommonMethods.cut_parameter_overrides_from_model_template() + model_template_path = model_template.get("new_config_path") + hyper_parameter_data = HyperParameterData( + base_path=self.config_path(), parameter_overrides=self.parameter_overrides() + ) + hyper_parameter_data.load_parameters(model_template_path) + learning_parameters_data = hyper_parameter_data.data.get("learning_parameters") + postprocessing_data = hyper_parameter_data.data.get("postprocessing") + assert learning_parameters_data.get("batch_size").get("default_value") == 10 + assert learning_parameters_data.get("learning_rate").get("default_value") == 0.5 + assert learning_parameters_data.get("learning_rate_warmup_iters").get("min_value") == 10 + assert learning_parameters_data.get("num_checkpoints").get("max_value") == 95 + assert learning_parameters_data.get("num_iters").get("min_value") == 2 + assert learning_parameters_data.get("num_workers").get("description") == "New workers description" + assert postprocessing_data.get("confidence_threshold").get("default_value") == 0.4 + assert postprocessing_data.get("result_based_confidence_threshold").get("header") == "New header" + # negative scenario with key not specified in config.yaml file + unexpected_key_dict = { + "learning_parameters": {"batch_size": {"default_value": 10}}, + "unexpected_key": {"parameter1": 1}, + } + hyper_parameter_data = HyperParameterData(base_path=self.config_path(), parameter_overrides=unexpected_key_dict) + with pytest.raises(ValueError): + hyper_parameter_data.load_parameters(self.model_template_path()) + # negative scenario with "value" key not allowed to override + restricted_key_dict = {"learning_parameters": {"batch_size": {"default_value": 10, "value": 1}}} + hyper_parameter_data = HyperParameterData(base_path=self.config_path(), parameter_overrides=restricted_key_dict) + with pytest.raises(KeyError): + hyper_parameter_data.load_parameters(self.model_template_path()) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_manually_set_data_and_validate(self): + """ + Description: + Check manually_set_data_and_validate method of HyperParameterData class instance + Expected results: + Test passes if manually_set_data_and_validate method of HyperParameterData class instance has expected values + Steps + 1. Check scenario when manually_set_data_and_validate method overrides HyperParameterData class data + 2. Check scenario when manually_set_data_and_validate method sets HyperParameterData class data + 3. Check scenario when manually_set_data_and_validate method removes HyperParameterData class data + """ + # Check for override class data + model_template = CommonMethods.cut_parameter_overrides_from_model_template() + model_template_path = model_template.get("new_config_path") + hyper_parameter_data = HyperParameterData(base_path=self.config_path()) + hyper_parameter_data.load_parameters(model_template_path) + parameter_overrides = self.parameter_overrides() + hyper_parameter_data.manually_set_data_and_validate(parameter_overrides) + assert hyper_parameter_data.data == parameter_overrides + assert hyper_parameter_data.has_valid_configurable_parameters + # Check for set class data + with open(self.config_path()) as config_file: + data_to_set = yaml.safe_load(config_file) + set_hyper_parameter_data = HyperParameterData(self.config_path()) + set_hyper_parameter_data.manually_set_data_and_validate(data_to_set) + assert set_hyper_parameter_data.data == data_to_set + assert set_hyper_parameter_data.has_valid_configurable_parameters + # Check for set empty class data + empty_hyper_parameter_data = HyperParameterData(self.config_path()) + empty_hyper_parameter_data.manually_set_data_and_validate({}) + assert empty_hyper_parameter_data.data == {} + assert empty_hyper_parameter_data.has_valid_configurable_parameters + remove(model_template_path) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestInstantiationType: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_instantiationtype(self): + """ + Description: + Check InstantiationType Enum class elements + Expected results: + Test passes if InstantiationType Enum class length, methods and attributes return expected values + Steps + 1. Check InstantiationType length + 2. Check InstantiationType elements value attribute + 3. Check InstantiationType str method + """ + assert len(InstantiationType) == 3 + assert InstantiationType.NONE.value == 1 + assert InstantiationType.CLASS.value == 2 + assert InstantiationType.GRPC.value == 3 + assert str(InstantiationType.NONE) == "NONE" + assert str(InstantiationType.CLASS) == "CLASS" + assert str(InstantiationType.GRPC) == "GRPC" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDependency: + @staticmethod + def dependency_parameters() -> dict: + return { + "source": "dependency source", + "destination": "dependency destination", + "size": 1024, + "sha256": "wgyjp0xks3obuwiu0jqea3q94ninfo0dgphv2t57wv1tq6qlwtr3jitvn0uo8a14", + } + + def dependency(self) -> Dependency: + return Dependency(**self.dependency_parameters()) + + @staticmethod + def unequal_dependency_parameters() -> dict: + return { + "source": "other source", + "destination": "other destination", + "size": 512, + "sha256": "q0y3vrh9wcff5lc77epfx08pb6ioredv341u68rp1qvxyl41wzt9tlih94s5273i", + } + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_depencdency(self): + """ + Description: + Check Dependency dataclass elements + Expected results: + Test passes if attributes of Dependency dataclass return expected values + Steps + 1. Check Dependency source, destination, size and sha256 attributes of initiated Dependency object + 2. Check comparing Dependency object with equal object + 3. Check comparing Dependency object with unequal object + """ + # Checking Dependency object attributes + expected_source = self.dependency_parameters().get("source") + expected_destination = self.dependency_parameters().get("destination") + dependency = self.dependency() + assert dependency.source == expected_source + assert dependency.destination == expected_destination + assert dependency.size == self.dependency_parameters().get("size") + assert dependency.sha256 == self.dependency_parameters().get("sha256") + default_attributes_dependency = Dependency(source=expected_source, destination=expected_destination) + assert default_attributes_dependency.source == expected_source + assert default_attributes_dependency.destination == expected_destination + assert not default_attributes_dependency.size + assert not default_attributes_dependency.sha256 + # Comparing Dependency object with equal + equal_dependency = self.dependency() + assert dependency == equal_dependency + # Comparing Dependency object with unequal + keys_list = ["source", "destination", "size", "sha256"] + parameter_combinations = [] + for i in range(1, len(keys_list) + 1): + parameter_combinations.append(list(itertools.combinations(keys_list, i))) + # In each of scenario creating a copy of equal parameters and replacing to values from prepared + # dictionary + unequal_dependency_parameters = self.unequal_dependency_parameters() + for scenario in parameter_combinations: + for parameters in scenario: + unequal_params_dict = dict(self.dependency_parameters()) + for key in parameters: + unequal_params_dict[key] = unequal_dependency_parameters.get(key) + unequal_dependency = Dependency(**unequal_params_dict) + assert dependency != unequal_dependency, ( + "Failed to check that Dependency instances with different " f"{parameters} are unequal" + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestEntryPoints: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_entrypoints(self): + """ + Description: + Check EntryPoints dataclass elements + Expected results: + Test passes if classes attributes of EntryPoints dataclass return expected values + Steps + 1. Check EntryPoints base, openvino and nncf attributes of initiated Dependency object + 2. Check comparing EntryPoints object with equal object + 3. Check comparing EntryPoints object with unequal object + """ + base = "base interface" + openvino = "OpenVINO interface" + nncf = "NNCF interface" + entrypoints_parameters = {"base": base, "openvino": openvino, "nncf": nncf} + # Checking EntryPoints object attributes + entry_points = EntryPoints(**entrypoints_parameters) + assert entry_points.base == base + assert entry_points.openvino == openvino + assert entry_points.nncf == nncf + default_attributes_entrypoints = EntryPoints(base=base) + assert default_attributes_entrypoints.base == base + assert not default_attributes_entrypoints.openvino + assert not default_attributes_entrypoints.nncf + # Comparing EntryPoints object with equal + equal_entry_points = EntryPoints(**entrypoints_parameters) + assert entry_points == equal_entry_points + # Comparing Dependency object with unequal + keys_list = ["base", "openvino", "nncf"] + parameter_combinations = [] + for i in range(1, len(keys_list) + 1): + parameter_combinations.append(list(itertools.combinations(keys_list, i))) + # In each of scenario creating a copy of equal parameters and replacing to values from prepared + # dictionary + unequal_entrypoints_parameters = { + "base": "other base interface", + "openvino": "other OpenVINO interface", + "nncf": "other NNCF interface", + } + for scenario in parameter_combinations: + for parameters in scenario: + unequal_params_dict = dict(entrypoints_parameters) + for key in parameters: + unequal_params_dict[key] = unequal_entrypoints_parameters.get(key) + unequal_entry_points = EntryPoints(**unequal_params_dict) + assert entry_points != unequal_entry_points, ( + "Failed to check that EntryPoints instances with " f"different {parameters} are unequal" + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelCategory: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_category(self): + """ + Description: + Check ModelCategory Enum class elements + Expected results: + Test passes if ModelCategory Enum class length, attributes and methods return expected values + Steps + 1. Check ModelCategory length + 2. Check ModelCategory elements value attribute + 3. Check ModelCategory str method + """ + assert len(ModelCategory) == 4 + assert ModelCategory.SPEED.value == 1 + assert ModelCategory.BALANCE.value == 2 + assert ModelCategory.ACCURACY.value == 3 + assert ModelCategory.OTHER.value == 4 + assert str(ModelCategory.SPEED) == "SPEED" + assert str(ModelCategory.BALANCE) == "BALANCE" + assert str(ModelCategory.ACCURACY) == "ACCURACY" + assert str(ModelCategory.OTHER) == "OTHER" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelStatus: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_category(self): + """ + Description: + Check ModelStatus Enum class elements + Expected results: + Test passes if ModelStatus Enum class length, attributes and methods return expected values + Steps + 1. Check ModelStatus length + 2. Check ModelStatus elements value attribute + 3. Check ModelStatus str method + """ + assert len(ModelStatus) == 2 + assert ModelStatus.ACTIVE.value == 1 + assert ModelStatus.DEPRECATED.value == 2 + assert str(ModelStatus.ACTIVE) == "ACTIVE" + assert str(ModelStatus.DEPRECATED) == "DEPRECATED" + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelTemplate: + @staticmethod + def default_model_parameters() -> dict: + return { + "model_template_id": "A16", + "model_template_path": TestHyperParameterData().model_template_path(), + "name": "test_model", + "task_family": TaskFamily.DATASET, + "task_type": TaskType.DETECTION, + "instantiation": InstantiationType.NONE, + } + + def optional_model_parameters(self): + model_template = CommonMethods.cut_parameter_overrides_from_model_template() + model_template_path = model_template.get("new_config_path") + hyper_parameter_data = HyperParameterData(base_path=TestHyperParameterData().config_path()) + hyper_parameter_data.load_parameters(model_template_path) + optional_parameters = dict(self.default_model_parameters()) + optional_parameters["model_template_path"] = model_template_path + optional_parameters["model_template_id"] = "B18" + optional_parameters["name"] = "test_model2" + optional_parameters["task_family"] = TaskFamily.VISION + optional_parameters["summary"] = "algorithm related information" + optional_parameters["framework"] = "test framework" + optional_parameters["max_nodes"] = 2 + optional_parameters["application"] = "test application" + optional_parameters["dependencies"] = [ + TestDependency().dependency(), + Dependency(**TestDependency().unequal_dependency_parameters()), + ] + optional_parameters["initial_weights"] = "https://some_url.com" + optional_parameters["training_targets"] = [ + TargetDevice.CPU, + TargetDevice.GPU, + TargetDevice.VPU, + ] + optional_parameters["inference_targets"] = [ + TargetDevice.CPU, + TargetDevice.VPU, + TargetDevice.GPU, + ] + optional_parameters["dataset_requirements"] = [ + DatasetRequirements(["class1", "class_2"]), + DatasetRequirements(["class_3", "class_4"]), + ] + optional_parameters["model_optimization_methods"] = [ + ModelOptimizationMethod.OPENVINO, + ModelOptimizationMethod.TENSORRT, + ] + optional_parameters["hyper_parameters"] = hyper_parameter_data + optional_parameters["is_trainable"] = False + optional_parameters["capabilities"] = [ + "compute_uncertainty_score", + "compute_representations", + ] + optional_parameters["grpc_address"] = "192.168.1.1" + optional_parameters["entrypoints"] = EntryPoints.openvino + optional_parameters["exportable_code_paths"] = ExportableCodePaths.openvino + optional_parameters["task_type_sort_priority"] = 0 + optional_parameters["gigaflops"] = 1 + optional_parameters["size"] = 1024 + optional_parameters["model_category"] = ModelCategory.SPEED + optional_parameters["model_status"] = ModelStatus.ACTIVE + optional_parameters["is_default_for_task"] = False + return optional_parameters + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_template_initiation(self): + """ + Description: + Check TestModelTemplate dataclass attributes + Expected results: + Test passes if classes attributes of ModelTemplate object return expected values + Steps + 1. Check attributes of ModelTemplate object initiated with default values + 2. Check attributes of ModelTemplate object initiated with fully specified values + """ + # Checks for object with default values + default_model_template = ModelTemplate(**self.default_model_parameters()) + CommonMethods.check_model_attributes(default_model_template, self.default_model_parameters()) + # Checks for object with specified values + optional_parameters_model = ModelTemplate(**self.optional_model_parameters()) + CommonMethods.check_model_attributes(optional_parameters_model, self.optional_model_parameters()) + assert default_model_template != optional_parameters_model + remove(optional_parameters_model.model_template_path) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_template_post_initialization(self): + """ + Description: + Check __post_init__ method of ModelTemplate dataclass + Expected results: + Test passes if __post_init__ method of ModelTemplate object raises ValueError exception for parameters violation + Steps + 1. Check ValueError exception when initialized ModelTemplate has gRPC instantiation and "" gRPC address + 2. Check ValueError exception when initialized ModelTemplate has CLASS instantiation and no entry points + 3. Check ValueError exception when initialized ModelTemplate has VISION task family and has no specified path to + config file + 4. Check ValueError exception when initialized ModelTemplate with task family in not equal to VISION and but has + specified path to config file + """ + model_template_parameters = self.default_model_parameters() + # gRPC instantiation and "" GRPC address + grpc_address_empty = dict(model_template_parameters) + grpc_address_empty["instantiation"] = InstantiationType.GRPC + grpc_address_empty["grpc_address"] = "" + with pytest.raises(ValueError): + ModelTemplate(**grpc_address_empty) + # CLASS instantiation and no entrypoints + class_no_entrypoints = dict(model_template_parameters) + class_no_entrypoints["instantiation"] = InstantiationType.CLASS + with pytest.raises(ValueError): + ModelTemplate(**class_no_entrypoints) + # VISION task family no config file path specified + vision_no_config = dict(model_template_parameters) + vision_no_config["task_family"] = TaskFamily.VISION + with pytest.raises(ValueError): + ModelTemplate(**vision_no_config) + # Not VISION task family and specified path to config + class_not_vision_with_config = dict(model_template_parameters) + class_not_vision_with_config["hyper_parameters"] = HyperParameterData( + base_path=TestHyperParameterData().config_path() + ) + with pytest.raises(ValueError): + ModelTemplate(**class_not_vision_with_config) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_template_capabilities(self): + """ + Description: + Check computes_uncertainty_score and computes_representations methods of ModelTemplate dataclass + Expected results: + Test passes if computes_uncertainty_score and computes_representations methods of ModelTemplate object return + expected bool values related to capabilities attribute + Steps + 1. Check computes_uncertainty_score and computes_representations methods return True value for ModelTemplate + 2. Check computes_uncertainty_score method returns True and computes_representations returns False value + 3. Check computes_uncertainty_score method returns False and computes_representations returns True value + 4. Check computes_uncertainty_score and computes_representations methods return False value + """ + # Check for computes_uncertainty_score and computes_representations methods return True + score_representations_model = ModelTemplate(**self.optional_model_parameters()) + assert score_representations_model.computes_uncertainty_score() + assert score_representations_model.computes_representations() + model_template_parameters = self.default_model_parameters() + # Check for computes_uncertainty_score is True and computes_representations is False + score_true_presentations_false_parameters = dict(model_template_parameters) + score_true_presentations_false_parameters["capabilities"] = [ + "compute_uncertainty_score", + "not test parameter", + ] + score_true_presentations_false_model = ModelTemplate(**score_true_presentations_false_parameters) + assert score_true_presentations_false_model.computes_uncertainty_score() + assert not score_true_presentations_false_model.computes_representations() + # Check for computes_uncertainty_score is False and computes_representations is True + score_true_presentations_false_parameters = dict(model_template_parameters) + score_true_presentations_false_parameters["capabilities"] = [ + "compute_representations", + "not test parameter", + ] + score_true_presentations_false_model = ModelTemplate(**score_true_presentations_false_parameters) + assert not score_true_presentations_false_model.computes_uncertainty_score() + assert score_true_presentations_false_model.computes_representations() + # Check for computes_uncertainty_score and computes_representations methods return False + no_score_representations_model = ModelTemplate(**self.default_model_parameters()) + assert not no_score_representations_model.computes_uncertainty_score() + assert not no_score_representations_model.computes_representations() + remove(score_representations_model.model_template_path) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_template_is_task_global(self): + """ + Description: + Check is_task_global method of ModelTemplate dataclass + Expected results: + Test passes if is_task_global method of ModelTemplate object returns expected bool values related to + task_type attribute + Steps + 1. Check is_task_global method returns True if task_type equal to CLASSIFICATION or ANOMALY_CLASSIFICATION + 2. Check is_task_global method returns False if task_type not equal to CLASSIFICATION or ANOMALY_CLASSIFICATION + """ + # Check is_task_global method returns True for CLASSIFICATION and ANOMALY_CLASSIFICATION + for global_task_type in ( + TaskType.CLASSIFICATION, + TaskType.ANOMALY_CLASSIFICATION, + ): + default_parameters = self.default_model_parameters() + task_global_parameters = dict(default_parameters) + task_global_parameters["task_type"] = global_task_type + task_global_model_template = ModelTemplate(**task_global_parameters) + assert ( + task_global_model_template.is_task_global() + ), f"Expected True value returned by is_task_global for {global_task_type}" + # Check is_task_global method returns False for the other tasks + non_global_task_parameters = dict(default_parameters) + non_global_tasks_list = [] + for task_type in TaskType: + if not task_type.is_global: + non_global_tasks_list.append(task_type) + for non_global_task in non_global_tasks_list: + non_global_task_parameters["task_type"] = non_global_task + non_global_task_template = ModelTemplate(**non_global_task_parameters) + assert not non_global_task_template.is_task_global(), ( + f"Expected False value returned by is_task_global method for {non_global_task}, " + f"only CLASSIFICATION and ANOMALY_CLASSIFICATION task types are global" + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestNullModelTemplate: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_null_mode_attributes(self): + """ + Description: + Check attributes of NullModelTemplate class object + Expected results: + Test passes if NullModelTemplate class object attributes have expected values + """ + null_model_template = NullModelTemplate() + expected_null_model_parameters = { + "model_template_id": "", + "model_template_path": "", + "name": "Null algorithm", + "task_family": TaskFamily.FLOW_CONTROL, + "task_type": TaskType.NULL, + "instantiation": InstantiationType.NONE, + } + CommonMethods.check_model_attributes(null_model_template, expected_null_model_parameters) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTaskTypesConstants: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_task_type_constants(self): + """ + Description: + Check values of ANOMALY_TASK_TYPES and TRAINABLE_TASK_TYPES constants + Expected results: + Test passes if ANOMALY_TASK_TYPES and TRAINABLE_TASK_TYPES constants return expected values + """ + assert ANOMALY_TASK_TYPES == ( + TaskType.ANOMALY_DETECTION, + TaskType.ANOMALY_CLASSIFICATION, + TaskType.ANOMALY_SEGMENTATION, + ) + assert TRAINABLE_TASK_TYPES == ( + TaskType.CLASSIFICATION, + TaskType.DETECTION, + TaskType.SEGMENTATION, + TaskType.INSTANCE_SEGMENTATION, + TaskType.ANOMALY_DETECTION, + TaskType.ANOMALY_CLASSIFICATION, + TaskType.ANOMALY_SEGMENTATION, + TaskType.ROTATED_DETECTION, + TaskType.ACTION_CLASSIFICATION, + TaskType.ACTION_DETECTION, + TaskType.VISUAL_PROMPTING, + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestParseModelTemplate: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_parse_model_template_from_omegaconf(self): + """ + Description: + Check _parse_model_template_from_omegaconf function returns expected instance of ModelTemplate class + Expected results: + Test passes if _parse_model_template_from_omegaconf function returns expected instance of ModelTemplate + class + """ + model_template_path = TestHyperParameterData().model_template_path() + with open(model_template_path) as model_template_file: + model_template_content = yaml.safe_load(model_template_file) + model_template_content["model_template_id"] = model_template_content["name"].replace(" ", "_") + model_template_content["model_template_path"] = model_template_path + parsed_model_template = _parse_model_template_from_omegaconf(model_template_content) + assert isinstance(parsed_model_template, ModelTemplate) + # Forming expected hyper parameter data dictionary from config.yaml and overridden parameters from + # model_template,yaml + parameter_overrides = { + "learning_parameters": { + "batch_size": {"default_value": 64}, + "learning_rate": {"default_value": 0.05}, + "learning_rate_warmup_iters": {"default_value": 100}, + "num_iters": {"default_value": 13000}, + } + } + expected_hyper_parameters = HyperParameterData( + base_path="./dummy_config.yaml", parameter_overrides=parameter_overrides + ) + expected_hyper_parameters.load_parameters(model_template_path) + expected_parsed_model_parameters = { + "entrypoints": EntryPoints(base="base entrypoints", openvino=None, nncf=None), + "framework": "Test framework", + "hyper_parameters": expected_hyper_parameters, + "inference_targets": [ + TargetDevice.CPU, + TargetDevice.GPU, + TargetDevice.VPU, + ], + "instantiation": InstantiationType.CLASS, + "model_template_id": "Custom_Object_Detection_--_TEST_ONLY", + "model_template_path": model_template_path, + "name": "Custom Object Detection -- TEST ONLY", + "summary": "Fast and lightweight object detector.", + "task_family": TaskFamily.VISION, + "task_type": TaskType.DETECTION, + "training_targets": [TargetDevice.GPU, TargetDevice.CPU], + } + CommonMethods.check_model_attributes(parsed_model_template, expected_parsed_model_parameters) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_parse_model_template(self): + """ + Description: + Check parse_model_template function returns expected instance of ModelTemplate class + Expected results: + Test passes if parse_model_template function returns instance of ModelTemplate class + Steps + 1. Check model_template_id and model_template_path attributes of ModelTemplate instance returned by + parse_model_template function for template file with not specified model_template_id parameter + 2. Check model_template_id and model_template_path attributes of ModelTemplate instance returned by + parse_model_template function for template file with specified model_template_id parameter + 3. Check ValueError exception raised if path to list-type template file is specified as input parameter in + parse_model_template function + """ + # Check for template file with not specified model_template_id + model_template_path = TestHyperParameterData().model_template_path() + not_specified_id_template = parse_model_template(model_template_path) + assert not_specified_id_template.model_template_id == "Custom_Object_Detection_--_TEST_ONLY" + assert not_specified_id_template.model_template_path == model_template_path + # Check for template file with specified model_template_id + id_specified_model_path = TestHyperParameterData.get_path_to_file(r"./id_specified_template.yaml") + model_id = "Parsed_Model_ID_1" + with open(model_template_path) as model_template_file: + id_specified_template_content = yaml.safe_load(model_template_file) + id_specified_template_content["model_template_id"] = model_id + with open(id_specified_model_path, "w") as new_template: + new_template.write(yaml.dump(id_specified_template_content)) + id_specified_template = parse_model_template(id_specified_model_path) + assert id_specified_template.model_template_id == model_id + assert id_specified_template.model_template_path == id_specified_model_path + remove(id_specified_model_path) + # Check ValueError exception raised if model template is list-type + incorrect_model_template_path = TestHyperParameterData.get_path_to_file(r"./incorrect_model_template.yaml") + with open(incorrect_model_template_path, "w+") as incorrect_yaml_file: + incorrect_yaml_file.write("[]") + with pytest.raises(ValueError): + parse_model_template(incorrect_model_template_path) + remove(incorrect_model_template_path) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_parse_model_template_from_dict(self): + """ + Description: + Check parse_model_template_from_dict function returns expected instance of ModelTemplate class + Expected results: + Test passes if parse_model_template_from_dict function returns instance of ModelTemplate class + Steps + 1. Check ModelTemplate instance returned by test_parse_model_template_from_dict function for dictionary + with specified model_template_id and model_template_path + parameters + """ + model_template = CommonMethods.cut_parameter_overrides_from_model_template() + model_template_path = model_template.get("new_config_path") + override_parameters = model_template.get("override_parameters") + hyper_parameters = HyperParameterData( + base_path=TestHyperParameterData().config_path(), + parameter_overrides=override_parameters, + ) + hyper_parameters.load_parameters(model_template_path) + template_dictionary = { + "model_template_id": "Dictionary_Model_1", + "model_template_path": model_template_path, + "name": "Custom Object Detection -- TEST ONLY", + "task_type": TaskType.DETECTION, + "task_family": TaskFamily.VISION, + "instantiation": InstantiationType.CLASS, + "summary": "Fast and lightweight object detector.", + "application": None, + "framework": "Test framework", + "entrypoints": EntryPoints(base="base interface", openvino="OpenVINO interface"), + "hyper_parameters": hyper_parameters, + "max_nodes": 1, + "training_targets": [TargetDevice.GPU, TargetDevice.CPU], + "inference_targets": [ + TargetDevice.CPU, + TargetDevice.GPU, + TargetDevice.VPU, + ], + } + model_template_from_dictionary = parse_model_template_from_dict(template_dictionary) + CommonMethods.check_model_attributes(model_template_from_dictionary, template_dictionary) + remove(model_template_path) diff --git a/tests/unit/api/entities/test_optimization_parameters.py b/tests/unit/api/entities/test_optimization_parameters.py new file mode 100644 index 00000000000..66ec63ac44c --- /dev/null +++ b/tests/unit/api/entities/test_optimization_parameters.py @@ -0,0 +1,99 @@ +"""This module tests classes related to OptimizationParameters""" + +# Copyright (C) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import dataclasses + +import pytest + +from otx.api.entities.optimization_parameters import OptimizationParameters +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestOptimizationParameters: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_optimization_parameters_members(self): + """ + Description: + To test OptimizationParameters dataclass members + + Input data: + Initialized instance of OptimizationParameters class + + Expected results: + + Steps + 1. Create OptimizationParameters + 2. Check members + """ + opt_params = OptimizationParameters() + + assert dataclasses.is_dataclass(opt_params) + assert len(dataclasses.fields(opt_params)) == 3 + assert dataclasses.fields(opt_params)[0].name == "resume" + assert dataclasses.fields(opt_params)[1].name == "update_progress" + assert dataclasses.fields(opt_params)[2].name == "save_model" + assert type(opt_params.resume) is bool + assert callable(opt_params.update_progress) + assert callable(opt_params.save_model) + with pytest.raises(AttributeError): + str(opt_params.WRONG) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_optimization_parameters_update_member(self): + """ + Description: + To test OptimizationParameters dataclass members update + + Input data: + Initialized instance of OptimizationParameters class + + Expected results: + + Steps + 1. Initiate OptimizationParameters instance + 2. Check members update + """ + opt_params = OptimizationParameters(False) + assert opt_params.resume is False + assert ( + opt_params.update_progress(0) + is opt_params.update_progress(50.5) + is opt_params.update_progress(100) + is opt_params.update_progress(0, 0.3) + is opt_params.update_progress(50.5, 1.4) + is opt_params.update_progress(100, -6.1) + is None + ) + assert opt_params.save_model() is None + + opt_params = OptimizationParameters(True) + assert opt_params.resume is True + assert ( + opt_params.update_progress(0) + is opt_params.update_progress(50.5) + is opt_params.update_progress(100) + is opt_params.update_progress(0, 0.3) + is opt_params.update_progress(50.5, 1.4) + is opt_params.update_progress(100, -6.1) + is None + ) + assert opt_params.save_model() is None diff --git a/tests/unit/api/entities/test_pickle.py b/tests/unit/api/entities/test_pickle.py new file mode 100644 index 00000000000..213770ecf22 --- /dev/null +++ b/tests/unit/api/entities/test_pickle.py @@ -0,0 +1,41 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import pickle # nosec B403 + +import pytest +from bson import ObjectId + +from otx.api.entities.id import ID +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestPickle: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_pickle_id(self): + """ + Description: + Check ID can be correctly pickled and unpickled + + Input data: + ID + + Expected results: + Test passes if the ID is correctly unpickled + + Steps + 1. Create ID + 2. Pickle and unpickle ID + 3. Check ID against unpickled ID + """ + original_id = ID(ObjectId()) + pickled_id = pickle.dumps(original_id) + unpickled_id = pickle.loads(pickled_id) # nosec B301 + assert id(original_id) != id(pickled_id), "Expected two different memory instanced" + assert original_id == unpickled_id, "Expected content of entities to be equal" diff --git a/tests/unit/api/entities/test_result_media.py b/tests/unit/api/entities/test_result_media.py new file mode 100644 index 00000000000..62dd9e6d0b6 --- /dev/null +++ b/tests/unit/api/entities/test_result_media.py @@ -0,0 +1,247 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import datetime + +import numpy as np +import pytest + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.result_media import ResultMediaEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + +RANDOM_IMAGE = np.random.randint(low=0, high=255, size=(32, 64, 3)) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestResultMediaEntity: + @staticmethod + def default_result_media_parameters() -> dict: + rectangle_label = LabelEntity( + name="Rectangle Annotation Label", + domain=Domain.DETECTION, + color=Color(100, 200, 60), + creation_date=datetime.datetime(year=2021, month=12, day=16), + id=ID("rectangle_label_1"), + ) + rectangle_annotation = Annotation( + shape=Rectangle(x1=0.1, y1=0.4, x2=0.4, y2=0.9), + labels=[ScoredLabel(rectangle_label)], + id=ID("rectangle_annotation"), + ) + annotation_scene = AnnotationSceneEntity( + annotations=[rectangle_annotation], + kind=AnnotationSceneKind.ANNOTATION, + creation_date=datetime.datetime(year=2021, month=12, day=16), + id=ID("annotation_scene"), + ) + return { + "name": "ResultMedia name", + "type": "Test ResultMedia", + "annotation_scene": annotation_scene, + "numpy": RANDOM_IMAGE, + } + + def optional_result_media_parameters(self) -> dict: + optional_result_media_parameters = self.default_result_media_parameters() + roi_label = LabelEntity( + "ROI label", + Domain.DETECTION, + Color(10, 200, 40), + creation_date=datetime.datetime(year=2021, month=12, day=18), + id=ID("roi_label_1"), + ) + roi = Annotation( + shape=Rectangle(x1=0.3, y1=0.2, x2=0.7, y2=0.6), + labels=[ScoredLabel(roi_label)], + id=ID("roi_annotation"), + ) + result_media_label = LabelEntity( + "ResultMedia label", + Domain.CLASSIFICATION, + Color(200, 60, 100), + creation_date=datetime.datetime(year=2021, month=12, day=20), + id=ID("result_media_1"), + ) + optional_result_media_parameters["roi"] = roi + optional_result_media_parameters["label"] = result_media_label + return optional_result_media_parameters + + def result_media(self): + return ResultMediaEntity(**self.optional_result_media_parameters()) + + @staticmethod + def check_result_media_attributes(result_media: ResultMediaEntity, expected_values: dict): + assert result_media.name == expected_values.get("name") + assert result_media.type == expected_values.get("type") + assert result_media.annotation_scene == expected_values.get("annotation_scene") + assert np.array_equal(result_media.numpy, expected_values.get("numpy")) + if not expected_values.get("roi"): + assert isinstance(result_media.roi.shape, Rectangle) + assert Rectangle.is_full_box(result_media.roi.shape) + else: + assert result_media.roi == expected_values.get("roi") + assert result_media.label == expected_values.get("label") + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_result_media_initialization(self): + """ + Description: + Check ResultMediaEntity class object initialization + + Input data: + ResultMediaEntity class object with specified "name", "type", "annotation_scene", "numpy", "roi" + and "label" parameters + + Expected results: + Test passes if attributes of initialized ResultMediaEntity class object are equal to expected + + Steps + 1. Check attributes of ResultMediaEntity class object initialized with default optional parameters + 2. Check attributes of ResultMediaEntity class object initialized with specified optional parameters + """ + # Checking attributes of ResultMediaEntity class object initialized with default optional parameters + initialization_params = self.default_result_media_parameters() + result_media = ResultMediaEntity(**initialization_params) + self.check_result_media_attributes(result_media, initialization_params) + # Checking attributes of ResultMediaEntity class object initialized with specified optional parameters + initialization_params = self.optional_result_media_parameters() + result_media = ResultMediaEntity(**initialization_params) + self.check_result_media_attributes(result_media, initialization_params) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_result_media_repr(self): + """ + Description: + Check ResultMediaEntity class object __repr__ method + + Input data: + ResultMediaEntity class object with specified "name", "type", "annotation_scene", "numpy", "roi" + and "label" parameters + + Expected results: + Test passes if value returned by __repr__ method is equal to expected + + Steps + 1. Check value returned by __repr__ method for ResultMediaEntity class object initialized with default optional + parameters + 2. Check value returned by __repr__ method for ResultMediaEntity class object initialized with specified + optional parameters + """ + # Checking __repr__ method for ResultMediaEntity class object initialized with default optional parameters + initialization_params = self.default_result_media_parameters() + annotation_scene = initialization_params.get("annotation_scene") + result_media = ResultMediaEntity(**initialization_params) + assert repr(result_media) == ( + f"ResultMediaEntity(name=ResultMedia name, type=Test ResultMedia, annotation_scene={annotation_scene}, " + f"roi={result_media.roi}, label=None)" + ) + # Checking __repr__ method for ResultMediaEntity class object initialized with specified optional parameters + initialization_params = self.optional_result_media_parameters() + annotation_scene = initialization_params.get("annotation_scene") + roi = initialization_params.get("roi") + label = initialization_params.get("label") + result_media = ResultMediaEntity(**initialization_params) + assert repr(result_media) == ( + f"ResultMediaEntity(name=ResultMedia name, type=Test ResultMedia, annotation_scene={annotation_scene}, " + f"roi={roi}, label={label})" + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_result_media_shape(self): + """ + Description: + Check ResultMediaEntity class object "width" and "height" properties + + Input data: + ResultMediaEntity class object with specified "name", "type", "annotation_scene", "numpy", "roi" + and "label" parameters + + Expected results: + Test passes if values returned by "width" and "height" properties are equal to expected + + Steps + 1. Check values returned by "width" and "height" properties for initialized ResultMediaEntity object + 2. Manually set new value of "numpy" property and check re-check "numpy", "width" and "height" properties + """ + # Checking values returned by "width" and "height" properties for initialized ResultMediaEntity object + result_media = self.result_media() + assert result_media.width == 64 + assert result_media.height == 32 + # Manually setting new value of "numpy" property and re-checking "numpy, "width" and "height" properties + new_numpy = np.random.uniform(low=0.0, high=255.0, size=(16, 32, 3)) + result_media.numpy = new_numpy + np.array_equal(result_media.numpy, new_numpy) + assert result_media.width == 32 + assert result_media.height == 16 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_result_media_eq(self): + """ + Description: + Check ResultMediaEntity class object __eq__ method + + Input data: + ResultMediaEntity class objects with specified "name", "type", "annotation_scene", "numpy", "roi" + and "label" parameters + + Expected results: + Test passes if value returned by __eq__ method is equal to expected + + Steps + 1. Check value returned by __eq__ method for comparing equal ResultMediaEntity objects + 2. Check value returned by __eq__ method for comparing ResultMediaEntity objects with unequal + "name", "type", "label" and "numpy" parameters - expected equality + 3. Check value returned by __eq__ method for comparing ResultMediaEntity objects with unequal + "annotation_scene" and "roi" parameters - expected inequality + 4. Check value returned by __eq__ method for comparing ResultMediaEntity with different type object + """ + initialization_params = self.optional_result_media_parameters() + result_media = ResultMediaEntity(**initialization_params) + # Comparing equal ResultMediaEntity objects + equal_result_media = ResultMediaEntity(**initialization_params) + assert result_media == equal_result_media + # Comparing ResultMediaEntity objects with unequal "name", "type", "label" and "numpy" parameters, + # expected equality + unequal_values = { + "name": "Unequal name", + "type": "Unequal type", + "label": LabelEntity("Unequal label", Domain.CLASSIFICATION), + "numpy": np.random.uniform(low=0.0, high=255.0, size=(1, 2, 3)), + } + for key in unequal_values: + unequal_params = dict(initialization_params) + unequal_params[key] = unequal_values.get(key) + equal_result_media = ResultMediaEntity(**unequal_params) + assert result_media == equal_result_media + # Comparing ResultMediaEntity objects with unequal "annotation_scene" and "roi" parameters, expected inequality + unequal_values = { + "annotation_scene": AnnotationSceneEntity(annotations=[], kind=AnnotationSceneKind.NONE), + "roi": Rectangle.generate_full_box(), + } + for key in unequal_values: + unequal_params = dict(initialization_params) + unequal_params[key] = unequal_values.get(key) + unequal_result_media = ResultMediaEntity(**unequal_params) + assert result_media != unequal_result_media + # Comparing ResultMediaEntity with different type object + assert result_media != str diff --git a/tests/unit/api/entities/test_resultset.py b/tests/unit/api/entities/test_resultset.py new file mode 100644 index 00000000000..62991312653 --- /dev/null +++ b/tests/unit/api/entities/test_resultset.py @@ -0,0 +1,129 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import datetime + +import pytest + +from otx.api.entities.id import ID +from otx.api.entities.metrics import NullPerformance +from otx.api.entities.resultset import ResultSetEntity, ResultsetPurpose +from otx.api.utils.time_utils import now +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestResultset: + creation_date = now() + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_resultset_purpose(self): + """ + Description: + Check the ResultsetPurpose can correctly return the value + + Input data: + Denotes, stages + + Expected results: + Test passes if the results match + """ + + denotes = ["EVALUATION", "TEST", "PREEVALUATION"] + stages = ["Validation", "Test", "Pre-validation"] + + resultset_purpose = ResultsetPurpose + assert len(resultset_purpose) == 3 + + for i in ResultsetPurpose: + resultset_purpose = ResultsetPurpose(i) + assert repr(resultset_purpose) == denotes[i.value] + assert str(resultset_purpose) == stages[i.value] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_resultset_entity(self): + """ + Description: + Check the ResultSetEntity can correctly return the value + + Input data: + Mock data + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Create dummy data + 2. Check the processing of default values + 3. Check the processing of changed values + """ + + test_data = { + "model": None, + "ground_truth_dataset": None, + "prediction_dataset": None, + "purpose": None, + "performance": None, + "creation_date": None, + "id": None, + } + + result_set = ResultSetEntity(**test_data) + + for name, value in test_data.items(): + set_attr_name = f"test_{name}" + if name in [ + "model", + "ground_truth_dataset", + "prediction_dataset", + "purpose", + ]: + assert getattr(result_set, name) == value + setattr(result_set, name, set_attr_name) + assert getattr(result_set, name) == set_attr_name + + assert result_set.performance == NullPerformance() + assert type(result_set.creation_date) == datetime.datetime + assert result_set.id_ == ID() + + assert result_set.has_score_metric() is False + result_set.performance = "test_performance" + assert result_set.performance != NullPerformance() + assert result_set.has_score_metric() is True + + creation_date = self.creation_date + result_set.creation_date = creation_date + assert result_set.creation_date == creation_date + + set_attr_id = ID(123456789) + result_set.id_ = set_attr_id + assert result_set.id_ == set_attr_id + + test_result_set_repr = [ + f"model={result_set.model}", + f"ground_truth_dataset={result_set.ground_truth_dataset}", + f"prediction_dataset={result_set.prediction_dataset}", + f"purpose={result_set.purpose}", + f"performance={result_set.performance}", + f"creation_date={result_set.creation_date}", + f"id={result_set.id_}", + ] + + for i in test_result_set_repr: + assert i in repr(result_set) diff --git a/tests/unit/api/entities/test_scored_label.py b/tests/unit/api/entities/test_scored_label.py new file mode 100644 index 00000000000..84c14b5929d --- /dev/null +++ b/tests/unit/api/entities/test_scored_label.py @@ -0,0 +1,75 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import pytest + +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import LabelSource, ScoredLabel +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestScoredLabel: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_scored_label(self): + """ + Description: + Check the ScoredLabel can correctly return the value + + Input data: + LabelEntity + + Expected results: + Test passes if the results match + """ + car = LabelEntity(id=ID(123456789), name="car", domain=Domain.DETECTION, is_empty=False) + person = LabelEntity(id=ID(987654321), name="person", domain=Domain.DETECTION, is_empty=False) + car_label = ScoredLabel(car) + person_label = ScoredLabel(person) + + for attr in ["id", "name", "color", "hotkey", "creation_date", "is_empty"]: + assert getattr(car_label, attr) == getattr(car, attr) + + assert car_label.get_label() == car + assert car_label == ScoredLabel(car) + assert car_label != car + assert car_label != person_label + assert hash(car_label) == hash(str(car_label)) + + probability = 0.0 + assert car_label.probability == probability + delta_probability = 0.4 + probability += delta_probability + car_label.probability += delta_probability + assert car_label.probability == probability + + label_source = LabelSource() + assert car_label.label_source == label_source + user_name = "User Name" + car_label.label_source.user_id = user_name + label_source_with_user = LabelSource(user_id=user_name) + assert car_label.label_source == label_source_with_user + + car.color = Color(red=16, green=15, blue=56, alpha=255) + assert repr(car_label) == ( + "ScoredLabel(123456789, name=car, probability=0.4, domain=DETECTION, color=" + "Color(red=16, green=15, blue=56, alpha=255), hotkey=, " + "label_source=LabelSource(user_id='User Name', model_id=ID(), " + "model_storage_id=ID()))" + ) diff --git a/tests/unit/api/entities/test_subset.py b/tests/unit/api/entities/test_subset.py new file mode 100644 index 00000000000..72f76dccc94 --- /dev/null +++ b/tests/unit/api/entities/test_subset.py @@ -0,0 +1,126 @@ +"""This module tests classes related to Subset""" + +# INTEL CONFIDENTIAL +# +# Copyright (C) 2021 Intel Corporation +# +# This software and the related documents are Intel copyrighted materials, and +# your use of them is governed by the express license under which they were provided to +# you ("License"). Unless the License provides otherwise, you may not use, modify, copy, +# publish, distribute, disclose or transmit this software or the related documents +# without Intel's prior written permission. +# +# This software and the related documents are provided as is, +# with no express or implied warranties, other than those that are expressly stated +# in the License. + +import pytest + +from otx.api.entities.subset import Subset +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestSubset: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_subset_members(self): + """ + Description: + To test Subset enumeration members + + Input data: + Initialized instance of Subset enum + + Expected results: + Enum members return correct values: + + NONE = 0 + TRAINING = 1 + VALIDATION = 2 + TESTING = 3 + UNLABELED = 4 + PSEUDOLABELED = 5 + UNASSIGNED = 6 + + Steps + 1. Create enum instance + 2. Check members + """ + test_instance = Subset + + for i in range(0, 7): + assert test_instance(i) in list(Subset) + + with pytest.raises(AttributeError): + test_instance.WRONG + + with pytest.raises(ValueError): + test_instance(7) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_subset_magic_str(self): + """ + Description: + To test Subset __str__ method + + Input data: + Initialized instance of Subset enum + + Expected results: + __str__ return correct string for every enum member + In case incorrect member it raises attribute exception + + Steps + 1. Create enum instance + 2. Check returning value of __str__ method + """ + test_instance = Subset + magic_str_list = [str(i) for i in list(Subset)] + + for i in range(0, 7): + assert str(test_instance(i)) in magic_str_list + + with pytest.raises(AttributeError): + str(test_instance.WRONG) + + with pytest.raises(ValueError): + str(test_instance(7)) + + assert len(set(magic_str_list)) == len(magic_str_list) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_subset_magic_repr(self): + """ + Description: + To test Subset __repr__ method + + Input data: + Initialized instance of Subset enum + + Expected results: + __repr__ method returns correct string + + Steps + 1. Create enum instance + 2. Check returning value of magic methods + """ + test_instance = Subset + magic_repr_list = [repr(i) for i in list(Subset)] + + for i in range(0, 7): + assert repr(test_instance(i)) in magic_repr_list + + with pytest.raises(AttributeError): + repr(test_instance.WRONG) + + with pytest.raises(ValueError): + repr(test_instance(7)) + + assert len(set(magic_repr_list)) == len(magic_repr_list) diff --git a/tests/unit/api/entities/test_task_environment.py b/tests/unit/api/entities/test_task_environment.py new file mode 100644 index 00000000000..6983059e748 --- /dev/null +++ b/tests/unit/api/entities/test_task_environment.py @@ -0,0 +1,409 @@ +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from pathlib import Path + +import pytest +import yaml + +from otx.api.configuration import ConfigurableParameters, cfg_helper +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model_template import parse_model_template +from otx.api.entities.task_environment import TaskEnvironment +from tests.test_helpers import ConfigExample +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +def __get_path_to_file(filename: str): + """ + Return the path to the file named 'filename', which lives in the tests/entities directory + """ + return str(Path(__file__).parent / Path(filename)) + + +def dummy_config(): + """ + Return dict from yaml file + """ + cur_config = __get_path_to_file("./dummy_config.yaml") + with open(cur_config, "r") as stream: + return yaml.safe_load(stream) + + +def environment(): + """ + Return TaskEnvironment + """ + car = LabelEntity(id=ID(123456789), name="car", domain=Domain.DETECTION, is_empty=False) + person = LabelEntity(id=ID(987654321), name="person", domain=Domain.DETECTION, is_empty=False) + labels_list = [car, person] + dummy_template = __get_path_to_file("./dummy_template.yaml") + model_template = parse_model_template(dummy_template) + hyper_parameters = model_template.hyper_parameters.data + params = cfg_helper.create(hyper_parameters) + labels_schema = LabelSchemaEntity.from_labels(labels_list) + environment = TaskEnvironment( + model=None, + hyper_parameters=params, + label_schema=labels_schema, + model_template=model_template, + ) + return environment + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTaskEnvironment: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_task_environment(self): + """ + Description: + Check the TaskEnvironment can correctly return the value + + Input data: + Dummy data + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Using an already created dummy environment. + 2. Checking class fields + """ + + env = environment() + __dummy_config = dummy_config() + + assert env == TaskEnvironment( + model=None, + model_template=env.model_template, + hyper_parameters=env.get_hyper_parameters(), + label_schema=env.label_schema, + ) + assert isinstance(env, TaskEnvironment) + assert env != "Fail params" + assert len(env.get_labels()) == 2 + + for i in ["header", "description", "visible_in_ui"]: + assert getattr(env.get_model_configuration().configurable_parameters, i) == __dummy_config[i] + + assert env.get_model_configuration().configurable_parameters.id == ID() + + for param in __dummy_config: + getattr(env.get_hyper_parameters(), param) == __dummy_config[param] + + assert env.get_hyper_parameters().id == ID() + + assert "model=None" in repr(env) + assert "label_schema=LabelSchemaEntity(label_groups=[LabelGroup(id=" in repr(env) + assert "name=from_label_list" in repr(env) + assert "group_type=LabelGroupType.EXCLUSIVE" in repr(env) + assert "labels=[LabelEntity(123456789, name=car, hotkey=, domain=DETECTION" in repr(env) + assert "LabelEntity(987654321, name=person, hotkey=, domain=DETECTION" in repr(env) + assert "CONFIGURABLE_PARAMETERS(header='Configuration for an object detection task -- TEST ONLY'" in repr(env) + assert "description='Configuration for an object detection task -- TEST ONLY'" in repr(env) + assert "visible_in_ui=True" in repr(env) + assert "id=ID()" in repr(env) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_compare_learning_parameters_num_workers(self): + """ + Description: + Check matches ConfigExample and dummy_config.yaml num_workers default_value + + Input data: + ConfigExample, dummy_config.yaml + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Checking num_workers default_value from ConfigExample and dummy_config.yaml + """ + + env = environment() + config_example = env.get_hyper_parameters(ConfigExample) + + env_learning_parameters_num_workers = ( + env.get_hyper_parameters().learning_parameters.num_workers + ) # "default_value" + config_example_learning_parameters_num_workers = ( + config_example.learning_parameters._default.factory.num_workers.metadata["default_value"] + ) + + # From dummy_config.yaml because it is missing in dummy_template.yaml "parameter_overrides" + assert env_learning_parameters_num_workers == 0 + assert config_example_learning_parameters_num_workers == 4 # From ConfigExample + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_compare_learning_parameters_batch_size(self): + """ + Description: + Check matches ConfigExample and dummy_config.yaml batch_size default_value + + Input data: + ConfigExample, dummy_config.yaml + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Checking batch_size from ConfigExample and dummy_config.yaml + """ + + env = environment() + config_example = env.get_hyper_parameters(ConfigExample) + + env_learning_parameters_batch_size = ( + env.get_hyper_parameters().learning_parameters.batch_size + ) # "default_value" + config_example_learning_parameters_batch_size = ( + config_example.learning_parameters._default.factory.batch_size.metadata["default_value"] + ) + + assert env_learning_parameters_batch_size == 64 # From dummy_template.yaml "parameter_overrides" + assert config_example_learning_parameters_batch_size == 5 # From ConfigExample + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_compare_learning_parameters_num_iters(self): + """ + Description: + Check matches ConfigExample and dummy_config.yaml num_iters default_value + + Input data: + ConfigExample, dummy_config.yaml + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Checking num_iters from ConfigExample and dummy_config.yaml + """ + + env = environment() + config_example = env.get_hyper_parameters(ConfigExample) + + env_learning_parameters_num_iters = env.get_hyper_parameters().learning_parameters.num_iters # "default_value" + config_example_learning_parameters_num_iters = ( + config_example.learning_parameters._default.factory.num_iters.metadata["default_value"] + ) + + assert env_learning_parameters_num_iters == 13000 # From dummy_template.yaml "parameter_overrides" + assert config_example_learning_parameters_num_iters == 1 # From ConfigExample + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_compare_learning_parameters_learning_rate(self): + """ + Description: + Check matches ConfigExample and dummy_config.yaml learning_rate default_value + + Input data: + ConfigExample, dummy_config.yaml + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Checking learning_rate from ConfigExample and dummy_config.yaml + """ + + env = environment() + config_example = env.get_hyper_parameters(ConfigExample) + + env_learning_parameters_learning_rate = ( + env.get_hyper_parameters().learning_parameters.learning_rate + ) # "default_value" + config_example_learning_parameters_learning_rate = ( + config_example.learning_parameters._default.factory.learning_rate.metadata["default_value"] + ) + + assert env_learning_parameters_learning_rate == 0.05 # From dummy_template.yaml "parameter_overrides" + assert config_example_learning_parameters_learning_rate == 0.01 # From ConfigExample + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_compare_learning_parameters_num_checkpoints(self): + """ + Description: + Check matches ConfigExample and dummy_config.yaml num_checkpoints default_value + "num_checkpoints" is missing in ConfigExample + + Input data: + ConfigExample, dummy_config.yaml + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Checking num_checkpoints from ConfigExample and dummy_config.yaml + """ + + env = environment() + config_example = env.get_hyper_parameters(ConfigExample) + + env_learning_parameters_num_checkpoints = ( + env.get_hyper_parameters().learning_parameters.num_checkpoints + ) # "default_value" + + # From dummy_config.yaml because it is missing in dummy_template.yaml "parameter_overrides" + assert env_learning_parameters_num_checkpoints == 5 + + # Attempt to access the missing parameter in ConfigExample + with pytest.raises(AttributeError): + # AttributeError: type object '__LearningParameters' has no attribute 'num_checkpoints' + config_example.learning_parameters._default.factory.num_checkpoints.metadata["default_value"] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dummy_config_missing_param(self): + """ + Description: + Check dummy_config missing_param + + Input data: + dummy_config + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Checking missing_param dummy_config + """ + + env = environment() + + # Attempt to access the missing parameter in dummy_config.yaml + with pytest.raises(AttributeError): + # AttributeError: 'PARAMETER_GROUP' object has no attribute 'missing_param' + env.get_hyper_parameters().learning_parameters.missing_param + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_compare_postprocessing_confidence_threshold(self): + """ + Description: + Check matches ConfigExample and dummy_config.yaml confidence_threshold default_value + + Input data: + ConfigExample, dummy_config.yaml + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Checking confidence_threshold from ConfigExample and dummy_config.yaml + """ + + env = environment() + config_example = env.get_hyper_parameters(ConfigExample) + + env_postprocessing_confidence_threshold = env.get_hyper_parameters().postprocessing.confidence_threshold + cep = config_example.postprocessing + config_example_postprocessing_confidence_threshold = cep._default.factory.confidence_threshold.metadata[ + "default_value" + ] + + # From dummy_config.yaml because it is missing in dummy_template.yaml "parameter_overrides" + assert env_postprocessing_confidence_threshold == 0.35 + assert config_example_postprocessing_confidence_threshold == 0.25 # From ConfigExample + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_compare_postprocessing_result_based_confidence_threshold(self): + """ + Description: + Check matches ConfigExample and dummy_config.yaml result_based_confidence_threshold default_value + + Input data: + ConfigExample, dummy_config.yaml + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Checking result_based_confidence_threshold from ConfigExample and dummy_config.yaml + """ + + env = environment() + config_example = env.get_hyper_parameters(ConfigExample) + + env_postprocessing_result_based_confidence_threshold = ( + env.get_hyper_parameters().postprocessing.result_based_confidence_threshold + ) # "default_value" + cep = config_example.postprocessing + def_factory = cep._default.factory + rbct = def_factory.result_based_confidence_threshold + config_example_postprocessing_result_based_confidence_threshold = rbct.metadata["default_value"] + + # From dummy_config.yaml because it is missing in dummy_template.yaml "parameter_overrides" + assert env_postprocessing_result_based_confidence_threshold is True + assert config_example_postprocessing_result_based_confidence_threshold is True # From ConfigExample + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_set_hyper_parameters(self): + """ + Description: + Check set_hyper_parameters() method + + Input data: + Dummmy data + + Expected results: + Test passes if incoming data is processed correctly + + Steps + 1. Checking parameters after setting + """ + env = environment() + + header = "Test header" + description = "Test description" + visible_in_ui = False + id = ID(123456789) + + hyper_parameters = ConfigurableParameters( + header=header, description=description, visible_in_ui=visible_in_ui, id=id + ) + env.set_hyper_parameters(hyper_parameters=hyper_parameters) + assert env.get_hyper_parameters().header == header + assert env.get_hyper_parameters().description == description + assert env.get_hyper_parameters().visible_in_ui == visible_in_ui + assert env.get_hyper_parameters().id == id + + assert env.get_model_configuration().configurable_parameters.header == header + assert env.get_model_configuration().configurable_parameters.description == description + assert env.get_model_configuration().configurable_parameters.visible_in_ui == visible_in_ui + assert env.get_model_configuration().configurable_parameters.id == id + + with pytest.raises(ValueError): + # ValueError: Unable to set hyper parameters, invalid input: 123 + env.set_hyper_parameters(hyper_parameters="123") diff --git a/tests/unit/api/entities/test_tensor.py b/tests/unit/api/entities/test_tensor.py new file mode 100644 index 00000000000..b2a5c646dbe --- /dev/null +++ b/tests/unit/api/entities/test_tensor.py @@ -0,0 +1,142 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest + +from otx.api.entities.tensor import TensorEntity +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + +RANDOM_NUMPY = np.random.randint(low=0, high=255, size=(16, 32, 3)) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTensorEntity: + @staticmethod + def tensor_params(): + return {"name": "Test Tensor", "numpy": RANDOM_NUMPY} + + def tensor(self): + return TensorEntity(**self.tensor_params()) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_tensor_initialization(self): + """ + Description: + Check TensorEntity class object initialization + + Input data: + TensorEntity class object with specified "name" and "numpy" parameters + + Expected results: + Test passes if attributes of initialized TensorEntity class object are equal to expected + """ + tensor_params = self.tensor_params() + tensor = TensorEntity(**tensor_params) + assert tensor.name == tensor_params.get("name") + assert np.array_equal(tensor.numpy, tensor_params.get("numpy")) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_tensor_shape(self): + """ + Description: + Check TensorEntity class object "shape" property + + Input data: + TensorEntity class object with specified "name" and "numpy" parameters + + Expected results: + Test passes if value returned by "shape" property is equal to expected + + Steps + 1. Check value returned by "shape" property for initialized TensorEntity object + 2. Manually set new value of "numpy" property and check re-check "numpy" and "shape" properties + """ + # Checking values returned by "shape" property for initialized TensorEntity object + tensor = self.tensor() + assert tensor.shape == (16, 32, 3) + # Manually setting new value of "numpy" property and re-checking "numpy and "shape" properties + new_numpy = np.random.uniform(low=0.0, high=255.0, size=(8, 16, 3)) + tensor.numpy = new_numpy + assert np.array_equal(tensor.numpy, new_numpy) + assert tensor.shape == (8, 16, 3) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_tensor_eq(self): + """ + Description: + Check TensorEntity class object __eq__ method + + Input data: + TensorEntity class objects with specified "name" and "numpy" parameters + + Expected results: + Test passes if value returned by __eq__ method is equal to expected + + Steps + 1. Check value returned by __eq__ method for comparing equal TensorEntity objects + 2. Check value returned by __eq__ method for comparing TensorEntity objects with unequal "name" parameters: + expected equality + 3. Check value returned by __eq__ method for comparing TensorEntity objects with unequal "numpy" parameters - + expected inequality + 4. Check value returned by __eq__ method for comparing TensorEntity with different type object + """ + initialization_params = self.tensor_params() + tensor = TensorEntity(**initialization_params) + # Comparing equal TensorEntity objects + equal_tensor = TensorEntity(**initialization_params) + assert tensor == equal_tensor + # Comparing TensorEntity objects with unequal "name" parameter, expected equality + unequal_params = dict(initialization_params) + unequal_params["name"] = "Unequal_name" + equal_tensor = TensorEntity(**unequal_params) + assert tensor == equal_tensor + # Comparing TensorEntity objects with unequal "numpy" parameter, expected inequality + unequal_params = dict(initialization_params) + unequal_params["numpy"] = np.random.uniform(low=0.0, high=255.0, size=(1, 2, 3)) + unequal_tensor = TensorEntity(**unequal_params) + assert tensor != unequal_tensor + # Comparing TensorEntity with different type object + assert tensor != str + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_tensor_str(self): + """ + Description: + Check TensorEntity class object __str__ method + + Input data: + TensorEntity class object with specified "name" and "numpy" parameters + + Expected results: + Test passes if value returned by __str__ method is equal to expected + """ + tensor = self.tensor() + assert str(tensor) == "TensorEntity(name=Test Tensor, shape=(16, 32, 3))" + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_tensor_repr(self): + """ + Description: + Check TensorEntity class object __repr__ method + + Input data: + TensorEntity class object with specified "name" and "numpy" parameters + + Expected results: + Test passes if value returned by __repr__ method is equal to expected + """ + tensor = self.tensor() + assert repr(tensor) == "TensorEntity(name=Test Tensor)" diff --git a/tests/unit/api/entities/test_train_parameters.py b/tests/unit/api/entities/test_train_parameters.py new file mode 100644 index 00000000000..5eef6361f41 --- /dev/null +++ b/tests/unit/api/entities/test_train_parameters.py @@ -0,0 +1,51 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.api.entities.train_parameters import ( + TrainParameters, + default_progress_callback, + default_save_model_callback, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTrainParameters: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_train_parameters(self): + """ + Description: + Check TrainParameters class object attributes + + Expected results: + Test passes if attributes of TrainParameters class object are equal to expected + + Steps + 1. Check attributes of TrainParameters object initialized with default parameters + 2. Check attributes of TrainParameters object initialized with "resume", "update_progress" and "save_model" + parameters + """ + # Checking attributes of TrainParameters object initiated with default parameters + default_values_train_parameters = TrainParameters() + assert not default_values_train_parameters.resume + # Expected that update_progress equal to function for default parameters TrainParameters object + assert default_values_train_parameters.update_progress == default_progress_callback + # Expected that save_model is equal to function for default parameters TrainParameters object + assert default_values_train_parameters.save_model == default_save_model_callback + # Checking attributes of TrainParameters object initiated with specified + progress_callback = default_progress_callback(99.9, 99.9) + save_model_callback = default_save_model_callback() + specified_values_train_parameters = TrainParameters( + resume=True, + update_progress=progress_callback, + save_model=save_model_callback, + ) + assert specified_values_train_parameters.resume + assert specified_values_train_parameters.update_progress == progress_callback + assert specified_values_train_parameters.save_model == save_model_callback diff --git a/tests/unit/api/fixtures/__init__.py b/tests/unit/api/fixtures/__init__.py new file mode 100644 index 00000000000..79931efa777 --- /dev/null +++ b/tests/unit/api/fixtures/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/api/fixtures/general.py b/tests/unit/api/fixtures/general.py new file mode 100644 index 00000000000..d14ca9776b3 --- /dev/null +++ b/tests/unit/api/fixtures/general.py @@ -0,0 +1,20 @@ +""" +General fixtures. +""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from tests.test_helpers import LabelSchemaExample + + +@pytest.fixture(scope="session") +def label_schema_example(): + """ + Returns a label schema example. + """ + + return LabelSchemaExample() diff --git a/tests/unit/api/parameters_validation/validation_helper.py b/tests/unit/api/parameters_validation/validation_helper.py new file mode 100644 index 00000000000..39e2d13222c --- /dev/null +++ b/tests/unit/api/parameters_validation/validation_helper.py @@ -0,0 +1,85 @@ +""" +Common functions for input parameters validation tests +""" + +import numpy as np +import pytest + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.subset import Subset + + +def check_value_error_exception_raised(correct_parameters: dict, unexpected_values: list, class_or_function) -> None: + """ + Function checks that ValueError exception is raised when unexpected type values are specified as parameters for + methods or functions + """ + for key, value in unexpected_values: + incorrect_parameters_dict = dict(correct_parameters) + incorrect_parameters_dict[key] = value + with pytest.raises(ValueError): + class_or_function(**incorrect_parameters_dict) + + +def load_test_dataset(): + """Function prepares DatasetEntity object and labels list""" + + def gen_image(resolution, x1, y1, x2, y2): + width, height = resolution + image = np.full([height, width, 3], fill_value=255, dtype=np.uint8) + image[int(y1 * height) : int(y2 * height), int(x1 * width) : int(x2 * width), :] = np.array( + [0, 128, 128], dtype=np.uint8 + )[None, None, :] + return image, Rectangle(x1=x1, y1=y1, x2=x2, y2=y2) + + images = [ + gen_image((640, 480), 0.0, 0.0, 0.5, 0.5), + gen_image((640, 480), 0.5, 0.0, 1.0, 0.5), + gen_image((640, 480), 0.0, 0.5, 0.5, 1.0), + gen_image((640, 480), 0.5, 0.5, 1.0, 1.0), + ] + labels = [LabelEntity(name="rect", domain=Domain.DETECTION, id=ID("0"))] + + def get_image(i, subset): + image, bbox = images[i] + return DatasetItemEntity( + media=Image(data=image), + annotation_scene=AnnotationSceneEntity( + annotations=[Annotation(bbox, labels=[ScoredLabel(label=labels[0])])], + kind=AnnotationSceneKind.ANNOTATION, + ), + subset=subset, + ) + + items = [ + get_image(0, Subset.TRAINING), + get_image(1, Subset.TRAINING), + get_image(2, Subset.TRAINING), + get_image(3, Subset.TRAINING), + get_image(0, Subset.TRAINING), + get_image(1, Subset.TRAINING), + get_image(2, Subset.TRAINING), + get_image(3, Subset.TRAINING), + get_image(0, Subset.TRAINING), + get_image(1, Subset.TRAINING), + get_image(0, Subset.VALIDATION), + get_image(1, Subset.VALIDATION), + get_image(2, Subset.VALIDATION), + get_image(3, Subset.VALIDATION), + get_image(0, Subset.TESTING), + get_image(1, Subset.TESTING), + get_image(2, Subset.TESTING), + get_image(3, Subset.TESTING), + ] + return DatasetEntity(items), labels diff --git a/tests/unit/api/serialization/test_datetime_mapper.py b/tests/unit/api/serialization/test_datetime_mapper.py new file mode 100644 index 00000000000..49da01d76ec --- /dev/null +++ b/tests/unit/api/serialization/test_datetime_mapper.py @@ -0,0 +1,34 @@ +# +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from datetime import datetime + +import pytest + +from otx.api.serialization.datetime_mapper import DatetimeMapper +from otx.api.utils.time_utils import now +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDatetimeMapper: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_serialization_deserialization(self): + """ + This test serializes datetime, deserializes serialized datetime and compares with original one. + """ + + original_time = now() + serialized_time = DatetimeMapper.forward(original_time) + assert serialized_time == original_time.strftime("%Y-%m-%dT%H:%M:%S.%f") + + deserialized_time = DatetimeMapper.backward(serialized_time) + assert original_time == deserialized_time + + deserialized_time = DatetimeMapper.backward(None) + assert isinstance(deserialized_time, datetime) diff --git a/tests/unit/api/serialization/test_id_mapper.py b/tests/unit/api/serialization/test_id_mapper.py new file mode 100644 index 00000000000..e8224b42f33 --- /dev/null +++ b/tests/unit/api/serialization/test_id_mapper.py @@ -0,0 +1,41 @@ +# +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +""" This module contains tests for the mapper for ID entities """ + +import pytest + +from otx.api.entities.id import ID +from otx.api.serialization.id_mapper import IDMapper +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIDMapper: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_serialized_representiaton(self): + """ + This test serializes ID and checks serialized representation. + """ + + id_ = ID("21434231456") + serialized_id = IDMapper.forward(id_) + assert serialized_id == "21434231456" + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_serialization_deserialization(self): + """ + This test serializes ID, deserializes serialized ID and compare with original. + """ + + id_ = ID("21434231456") + serialized_id = IDMapper.forward(id_) + deserialized_id = IDMapper.backward(serialized_id) + assert id_ == deserialized_id diff --git a/tests/unit/api/serialization/test_label_mapper.py b/tests/unit/api/serialization/test_label_mapper.py new file mode 100644 index 00000000000..f126bf92a55 --- /dev/null +++ b/tests/unit/api/serialization/test_label_mapper.py @@ -0,0 +1,354 @@ +# +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import json +from random import randint + +import pytest + +from otx.api.entities.id import ID +from otx.api.entities.label import Color, Domain, LabelEntity +from otx.api.entities.label_schema import ( + LabelGroup, + LabelGroupType, + LabelSchemaEntity, + LabelTree, +) +from otx.api.serialization.datetime_mapper import DatetimeMapper +from otx.api.serialization.label_mapper import ( + ColorMapper, + LabelTreeMapper, + LabelGroupMapper, + LabelMapper, + LabelSchemaMapper, + label_schema_to_bytes, +) +from otx.api.utils.time_utils import now +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestColorMapper: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_color_serialization(self): + """ + This test serializes Color and checks serialized representation. + Then it compares deserialized Color with original one. + """ + # disable B311 random - used for the random sampling not for security/crypto + red = randint(0, 255) # nosec B311 + green = randint(0, 255) # nosec B311 + blue = randint(0, 255) # nosec B311 + alpha = randint(0, 255) # nosec B311 + color = Color(red, green, blue, alpha) + serialized = ColorMapper.forward(color) + assert serialized == {"red": red, "green": green, "blue": blue, "alpha": alpha} + + deserialized = ColorMapper.backward(serialized) + assert color == deserialized + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLabelEntityMapper: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_entity_serialization(self): + """ + This test serializes LabelEntity and checks serialized representation. + Then it compares deserialized LabelEntity with original one. + """ + + cur_date = now() + # disable B311 random - used for the random sampling not for security/crypto + red = randint(0, 255) # nosec B311 + green = randint(0, 255) # nosec B311 + blue = randint(0, 255) # nosec B311 + alpha = randint(0, 255) # nosec B311 + + label = LabelEntity( + name="my_label", + domain=Domain.DETECTION, + color=Color(red, green, blue, alpha), + hotkey="ctrl+1", + creation_date=cur_date, + is_empty=False, + id=ID("0000213"), + ) + serialized = LabelMapper.forward(label) + + assert serialized == { + "_id": "0000213", + "name": "my_label", + "color": {"red": red, "green": green, "blue": blue, "alpha": alpha}, + "hotkey": "ctrl+1", + "domain": "DETECTION", + "creation_date": DatetimeMapper.forward(cur_date), + "is_empty": False, + "is_anomalous": False, + } + + deserialized = LabelMapper.backward(serialized) + assert label == deserialized + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLabelSchemaEntityMapper: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_flat_label_schema_serialization(self): + """ + This test serializes flat LabelSchema and checks serialized representation. + Then it compares deserialized LabelSchema with original one. + """ + + cur_date = now() + names = ["cat", "dog", "mouse"] + colors = [ + Color( + # disable B311 random - used for the random sampling not for security/crypto + randint(0, 255), # nosec B311 + randint(0, 255), # nosec B311 + randint(0, 255), # nosec B311 + randint(0, 255), # nosec B311 + ) # nosec # noqa + for _ in range(3) + ] + labels = [ + LabelEntity( + name=name, + domain=Domain.CLASSIFICATION, + creation_date=cur_date, + id=ID(i), + color=colors[i], + ) + for i, name in enumerate(names) + ] + label_schema = LabelSchemaEntity.from_labels(labels) + serialized = LabelSchemaMapper.forward(label_schema) + + assert serialized == { + "label_tree": {"type": "tree", "directed": True, "nodes": [], "edges": []}, + "label_groups": [ + { + "_id": label_schema.get_groups()[0].id_, + "name": "from_label_list", + "label_ids": ["0", "1", "2"], + "relation_type": "EXCLUSIVE", + } + ], + "all_labels": { + "0": { + "_id": "0", + "name": "cat", + "color": ColorMapper.forward(colors[0]), + "hotkey": "", + "domain": "CLASSIFICATION", + "creation_date": DatetimeMapper.forward(cur_date), + "is_empty": False, + "is_anomalous": False, + }, + "1": { + "_id": "1", + "name": "dog", + "color": ColorMapper.forward(colors[1]), + "hotkey": "", + "domain": "CLASSIFICATION", + "creation_date": DatetimeMapper.forward(cur_date), + "is_empty": False, + "is_anomalous": False, + }, + "2": { + "_id": "2", + "name": "mouse", + "color": ColorMapper.forward(colors[2]), + "hotkey": "", + "domain": "CLASSIFICATION", + "creation_date": DatetimeMapper.forward(cur_date), + "is_empty": False, + "is_anomalous": False, + }, + }, + } + + deserialized = LabelSchemaMapper.backward(serialized) + assert label_schema == deserialized + + # Checking value returned by "label_schema_to_bytes" function + expected_label_schema_to_bytes = json.dumps(serialized, indent=4).encode() + actual_label_schema_to_bytes = label_schema_to_bytes(label_schema) + assert actual_label_schema_to_bytes == expected_label_schema_to_bytes + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLabelGroupMapper: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_group_serialization(self): + """ + This test serializes flat LabelGroup and checks serialized representation. + Then it compares deserialized LabelGroup with original one. + """ + + names = ["cat", "dog", "mouse"] + labels = [ + LabelEntity( + name=name, + domain=Domain.CLASSIFICATION, + id=ID(str(i)), + ) + for i, name in enumerate(names) + ] + label_group = LabelGroup(name="Test LabelGroup", labels=labels, group_type=LabelGroupType.EMPTY_LABEL) + serialized = LabelGroupMapper.forward(label_group) + assert serialized == { + "_id": label_group.id_, + "name": "Test LabelGroup", + "label_ids": ["0", "1", "2"], + "relation_type": "EMPTY_LABEL", + } + all_labels = {ID(str(i)): labels[i] for i in range(3)} + + deserialized = LabelGroupMapper.backward(instance=serialized, all_labels=all_labels) + assert deserialized == label_group + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestLabelTreeMapper: + label_0 = LabelEntity(name="label_0", domain=Domain.SEGMENTATION, id=ID("0")) + label_0_1 = LabelEntity(name="label_0_1", domain=Domain.SEGMENTATION, id=ID("0_1")) + label_0_2 = LabelEntity(name="label_0_2", domain=Domain.SEGMENTATION, id=ID("0_2")) + label_0_1_1 = LabelEntity(name="label_0_1_1", domain=Domain.SEGMENTATION, id=ID("0_1_1")) + label_0_1_2 = LabelEntity(name="label_0_1_2", domain=Domain.SEGMENTATION, id=ID("0_1_2")) + label_0_2_1 = LabelEntity(name="label_0_2_1", domain=Domain.SEGMENTATION, id=ID("0_2_1")) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_graph_forward(self): + """ + Description: + Check "LabelTreeMapper" class "forward" method + + Input data: + "LabelTree" and "LabelTree" objects + + Expected results: + Test passes if dictionary returned by "forward" method is equal to expected + + Steps + 1. Check dictionary returned by "forward" method for "LabelTree" object + """ + # Checking dictionary returned by "forward" for "LabelTree" + label_tree = LabelTree() + for parent, child in [ + (self.label_0, self.label_0_1), + (self.label_0, self.label_0_2), + (self.label_0_1, self.label_0_1_1), + (self.label_0_1, self.label_0_1_2), + (self.label_0_2, self.label_0_2_1), + ]: + label_tree.add_child(parent, child) + forward = LabelTreeMapper.forward(label_tree) + assert forward == { + "type": "tree", + "directed": True, + "nodes": ["0_1", "0", "0_2", "0_1_1", "0_1_2", "0_2_1"], + "edges": [ + ("0_1", "0"), + ("0_2", "0"), + ("0_1_1", "0_1"), + ("0_1_2", "0_1"), + ("0_2_1", "0_2"), + ], + } + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_label_graph_backward(self): + """ + Description: + Check "LabelTreeMapper" class "backward" method + + Input data: + Dictionary object to deserialize, labels list + + Expected results: + Test passes if "LabelTree" or "LabelTree" object returned by "backward" method is equal to expected + + Steps + 1. Check dictionary returned by "backward" method for "LabelTree" object + 2. Check that "ValueError" exception is raised when unsupported type is specified as "type" key in dictionary + object of "instance" parameter for "backward" method + """ + # Checking dictionary returned by "backward" for "LabelTree" + forward = { + "type": "tree", + "directed": True, + "nodes": ["0_1", "0", "0_2", "0_1_1", "0_2_1"], + "edges": [("0_1", "0"), ("0_2", "0"), ("0_1_1", "0_1"), ("0_2_1", "0_2")], + } + labels = { + ID("0"): self.label_0, + ID("0_1"): self.label_0_1, + ID("0_2"): self.label_0_2, + ID("0_1_1"): self.label_0_1_1, + ID("0_1_2"): self.label_0_1_2, + ID("0_2_1"): self.label_0_2_1, + } + expected_backward = LabelTree() + for parent, child in [ + (self.label_0, self.label_0_1), + (self.label_0, self.label_0_2), + (self.label_0_1, self.label_0_1_1), + (self.label_0_2, self.label_0_2_1), + ]: + expected_backward.add_child(parent, child) + actual_backward = LabelTreeMapper.backward(instance=forward, all_labels=labels) + assert actual_backward == expected_backward + # Checking "ValueError" exception raised when unsupported type specified as "type" in dictionary "instance" for + # "backward" + forward = { + "type": "rectangle", + "directed": True, + "nodes": ["0_1", "0", "0_2", "0_1_1", "0_2_1"], + "edges": [("0_1", "0"), ("0_2", "0"), ("0_1_1", "0_1"), ("0_2_1", "0_2")], + } + with pytest.raises(ValueError): + LabelTreeMapper.backward(instance=forward, all_labels=labels) + # Checking label deletion case + forward = { + "type": "tree", + "directed": True, + "nodes": ["0", "0_1", "0_2", "0_1_1", "0_2_1"], + "edges": [("0_1", "0"), ("0_2", "0"), ("0_1_1", "0_1"), ("0_2_1", "0_2")], + } + labels = { + ID("0"): self.label_0, + ID("0_1"): self.label_0_1, + # ID("0_2"): self.label_0_2, + ID("0_1_1"): self.label_0_1_1, + # ID("0_1_2"): self.label_0_1_2, + ID("0_2_1"): self.label_0_2_1, + } + expected_backward = LabelTree() + for node in labels.values(): + expected_backward.add_node(node) + for parent, child in [ + (self.label_0, self.label_0_1), + # (self.label_0, self.label_0_2), + (self.label_0_1, self.label_0_1_1), + # (self.label_0_2, self.label_0_2_1), + ]: + expected_backward.add_child(parent, child) + actual_backward = LabelTreeMapper.backward(instance=forward, all_labels=labels) + assert LabelTreeMapper.forward(actual_backward) == LabelTreeMapper.forward(expected_backward) diff --git a/tests/unit/api/usecases/adapters/test_model_adapter.py b/tests/unit/api/usecases/adapters/test_model_adapter.py new file mode 100644 index 00000000000..8c1ef9db2ad --- /dev/null +++ b/tests/unit/api/usecases/adapters/test_model_adapter.py @@ -0,0 +1,183 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.api.usecases.adapters.model_adapter import ( + ExportableCodeAdapter, + IDataSource, + ModelAdapter, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIDataSource: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_i_data_source_data(self): + """ + Description: + Check IDataSource class "data" property + + Input data: + IDataSource object + + Expected results: + Test passes if IDataSource object "data" property raises NotImplementedError exception + """ + with pytest.raises(NotImplementedError): + IDataSource().data() + + +class DummyDataSource(IDataSource): + def __init__(self, data: str): + self._data = data + + @property + def data(self): + return self._data + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestModelAdapter: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_adapter_initialization(self): + """ + Description: + Check ModelAdapter object initialization + + Input data: + ModelAdapter object initialized with "data_source" IDataSource or bytes parameter + + Expected results: + Test passes if properties of initialized ModelAdapter object are equal to expected + + Steps + 1. Check attributes of ModelAdapter object initialized with IDataSource "data_source" parameter + 2. Check attributes of ModelAdapter object initialized with bytes "data_source" parameter + 3. Check that ValueError exception is raised when initializing ModelAdapter object with "data_source" parameter + type not equal to bytes or IDataSource + """ + # Checking properties of "ModelAdapter" initialized with IDataSource "data_source" + data = "some data" + data_source = DummyDataSource(data=data) + model_adapter = ModelAdapter(data_source=data_source) + assert model_adapter.data_source == data_source + assert model_adapter.from_file_storage + assert model_adapter.data == data + # Checking properties of "ModelAdapter" initialized with bytes "data_source" + data_source = b"binaryrepo://localhost/repo/data_source/1" + model_adapter = ModelAdapter(data_source=data_source) + assert model_adapter.data_source == data_source + assert not model_adapter.from_file_storage + assert model_adapter.data == data_source + # Checking that ValueError exception is raised when initializing ModelAdapter with "data_source" type not equal + # to bytes or IDataSource + model_adapter = ModelAdapter(data_source=1) # type: ignore + with pytest.raises(ValueError): + model_adapter.data() + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_model_adapter_data_source_setter(self): + """ + Description: + Check ModelAdapter "data_source" property setter + + Input data: + ModelAdapter object initialized with "data_source" parameter + + Expected results: + Test passes if properties of ModelAdapter are equal to expected after manual setting "data_source" property + + Steps + 1. Check properties of ModelAdapter object after manual setting "data_source" property to other IDataSource + object + 2. Check properties of ModelAdapter object after manual setting "data_source" property to bytes object + 3. Check properties of ModelAdapter object after manual setting "data_source" property to other bytes object + 4. Check properties of ModelAdapter object after manual setting "data_source" property to IDataSource object + """ + model_adapter = ModelAdapter(data_source=DummyDataSource(data="some data")) + # Checking properties of ModelAdapter after manual setting "data_source" to other IDataSource + other_data = "other data" + other_data_source = DummyDataSource(data=other_data) + model_adapter.data_source = other_data_source + assert model_adapter.data_source == other_data_source + assert model_adapter.data == other_data + assert model_adapter.from_file_storage + # Checking properties of ModelAdapter after manual setting "data_source" to bytes + bytes_data_source = b"binaryrepo://localhost/repo/data_source/1" + model_adapter.data_source = bytes_data_source + assert model_adapter.data_source == bytes_data_source + assert model_adapter.data == bytes_data_source + assert not model_adapter.from_file_storage + # Checking properties of ModelAdapter after manual setting "data_source" to other bytes + other_bytes_data_source = b"binaryrepo://localhost/repo/data_source/2" + model_adapter.data_source = b"binaryrepo://localhost/repo/data_source/2" + assert model_adapter.data_source == other_bytes_data_source + assert model_adapter.data == other_bytes_data_source + assert not model_adapter.from_file_storage + # Checking properties of ModelAdapter after manual setting "data_source" to IDataSource + model_adapter.data_source = other_data_source + assert model_adapter.data_source == other_data_source + assert model_adapter.data == other_data + assert model_adapter.from_file_storage + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestExportableCodeAdapter: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_exportable_code_adapter_initialization(self): + """ + Description: + Check ExportableCodeAdapter object initialization + + Input data: + ExportableCodeAdapter object initialized with "data_source" IDataSource or bytes parameter + + Expected results: + Test passes if properties of initialized ExportableCodeAdapter object are equal to expected + + Steps + 1. Check attributes of ExportableCodeAdapter object initialized with IDataSource "data_source" parameter + 2. Check attributes of ExportableCodeAdapter object initialized with bytes "data_source" parameter + 3. Check that ValueError exception is raised when initializing ExportableCodeAdapter object with "data_source" + parameter type not equal to bytes or IDataSource + """ + # Checking properties of "ExportableCodeAdapter" initialized with IDataSource "data_source" + data = "some_data" + data_source = DummyDataSource(data=data) + exportable_code_adapter = ExportableCodeAdapter(data_source=data_source) + assert exportable_code_adapter.data_source == data_source + assert exportable_code_adapter.from_file_storage + assert exportable_code_adapter.data == data + # Checking properties after setting "data_source" to bytes + bytes_data_source = b"binaryrepo://localhost/repo/data_source/1" + exportable_code_adapter.data_source = bytes_data_source + assert exportable_code_adapter.data_source == bytes_data_source + assert not exportable_code_adapter.from_file_storage + assert exportable_code_adapter.data == bytes_data_source + # Checking properties of "ExportableCodeAdapter" initialized with bytes "data_source" + exportable_code_adapter = ExportableCodeAdapter(data_source=bytes_data_source) + assert exportable_code_adapter.data_source == bytes_data_source + assert not exportable_code_adapter.from_file_storage + assert exportable_code_adapter.data == bytes_data_source + # Checking properties after setting "data_source" to IDataSource + exportable_code_adapter.data_source = data_source + assert exportable_code_adapter.data_source == data_source + assert exportable_code_adapter.from_file_storage + assert exportable_code_adapter.data == data + # Checking that ValueError exception is raised when initializing ExportableCodeAdapter with "data_source" type + # not equal to bytes or IDataSource + exportable_code_adapter = ExportableCodeAdapter(data_source=1) # type: ignore + with pytest.raises(ValueError): + exportable_code_adapter.data() diff --git a/tests/unit/api/usecases/evaluation/test_accuracy.py b/tests/unit/api/usecases/evaluation/test_accuracy.py new file mode 100644 index 00000000000..4681bc3eaf1 --- /dev/null +++ b/tests/unit/api/usecases/evaluation/test_accuracy.py @@ -0,0 +1,638 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from datetime import datetime + +import numpy as np +import pytest + +from otx.api.configuration import ConfigurableParameters +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.color import Color +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelSchemaEntity +from otx.api.entities.metrics import ( + BarChartInfo, + BarMetricsGroup, + ColorPalette, + MatrixMetric, + Performance, + ScoreMetric, + VisualizationType, +) +from otx.api.entities.model import ModelConfiguration, ModelEntity +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.usecases.evaluation.accuracy import ( + Accuracy, + compute_unnormalized_confusion_matrices_from_resultset, + precision_metrics_group, + recall_metrics_group, +) +from otx.api.usecases.evaluation.averaging import MetricAverageMethod +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +class CommonActions: + color = Color(0, 255, 0) + creation_date = datetime(year=2021, month=12, day=27) + image = Image(data=np.random.randint(low=0, high=255, size=(10, 16, 3))) + + def car(self) -> LabelEntity: + return LabelEntity( + name="car", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("car_label"), + ) + + def human(self) -> LabelEntity: + return LabelEntity( + name="human", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("human_label"), + ) + + def dog(self) -> LabelEntity: + return LabelEntity( + name="dog", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("dog_label"), + ) + + def human_1_dataset_item(self) -> DatasetItemEntity: + human_1_roi = Annotation(shape=Rectangle.generate_full_box(), labels=[ScoredLabel(self.human())]) + human_1_annotation = Annotation( + shape=Rectangle(x1=0.3, y1=0, x2=0.4, y2=0.2), + labels=[ScoredLabel(self.human())], + ) + human_1_annotation_scene = AnnotationSceneEntity( + annotations=[human_1_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + human_1_dataset_item = DatasetItemEntity( + media=self.image, annotation_scene=human_1_annotation_scene, roi=human_1_roi + ) + return human_1_dataset_item + + def human_2_dataset_item(self) -> DatasetItemEntity: + human_2_roi = Annotation(shape=Rectangle.generate_full_box(), labels=[ScoredLabel(self.human())]) + human_2_annotation = Annotation( + shape=Rectangle(x1=0.6, y1=0, x2=0.7, y2=0.3), + labels=[ScoredLabel(self.human())], + ) + human_2_annotation_scene = AnnotationSceneEntity( + annotations=[human_2_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + human_2_dataset_item = DatasetItemEntity( + media=self.image, annotation_scene=human_2_annotation_scene, roi=human_2_roi + ) + return human_2_dataset_item + + def human_3_dataset_item(self) -> DatasetItemEntity: + human_3_roi = Annotation(shape=Rectangle.generate_full_box(), labels=[ScoredLabel(self.human())]) + human_3_annotation = Annotation( + shape=Rectangle(x1=0.7, y1=0, x2=0.8, y2=0.3), + labels=[ScoredLabel(self.human())], + ) + human_3_annotation_scene = AnnotationSceneEntity( + annotations=[human_3_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + human_3_dataset_item = DatasetItemEntity( + media=self.image, annotation_scene=human_3_annotation_scene, roi=human_3_roi + ) + return human_3_dataset_item + + def result_set(self) -> ResultSetEntity: + configurable_params = ConfigurableParameters(header="Test model configurable params") + labels_group = LabelGroup( + name="model_labels_group", + labels=[self.car(), self.human(), self.dog()], + ) + other_label_group = LabelGroup( + name="other_model_labels_group", + labels=[self.human(), self.dog()], + ) + model_configuration = ModelConfiguration( + configurable_params, + LabelSchemaEntity(label_groups=[labels_group, other_label_group]), + ) + model = ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + + car_1_roi = Annotation( + shape=Rectangle.generate_full_box(), + labels=[ScoredLabel(self.car())], + ) + car_1_annotation = Annotation( + shape=Rectangle(x1=0, y1=0, x2=0.3, y2=0.2), + labels=[ScoredLabel(self.car())], + ) + car_1_annotation_scene = AnnotationSceneEntity( + annotations=[car_1_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + car_1_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=car_1_annotation_scene, roi=car_1_roi) + + car_2_roi = Annotation(shape=Rectangle.generate_full_box(), labels=[ScoredLabel(self.car())]) + car_2_annotation = Annotation( + shape=Rectangle(x1=0.8, y1=0, x2=1.0, y2=0.2), + labels=[ScoredLabel(self.car())], + ) + car_2_annotation_scene = AnnotationSceneEntity( + annotations=[car_2_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + car_2_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=car_2_annotation_scene, roi=car_2_roi) + + dog_1_roi = Annotation(shape=Rectangle.generate_full_box(), labels=[ScoredLabel(self.dog())]) + dog_1_annotation = Annotation( + shape=Rectangle(x1=0.5, y1=0, x2=0.6, y2=0.1), + labels=[ScoredLabel(self.dog())], + ) + dog_1_annotation_scene = AnnotationSceneEntity( + annotations=[dog_1_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + dog_1_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=dog_1_annotation_scene, roi=dog_1_roi) + + dog_2_roi = Annotation(shape=Rectangle.generate_full_box(), labels=[ScoredLabel(self.dog())]) + dog_2_annotation = Annotation( + shape=Rectangle(x1=0.7, y1=0, x2=0.8, y2=0.3), + labels=[ScoredLabel(self.dog())], + ) + dog_2_annotation_scene = AnnotationSceneEntity( + annotations=[dog_2_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + dog_2_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=dog_2_annotation_scene, roi=dog_2_roi) + + ground_truth_dataset = DatasetEntity( + [ + car_1_dataset_item, + car_2_dataset_item, + self.human_1_dataset_item(), + self.human_2_dataset_item(), + self.human_3_dataset_item(), + dog_1_dataset_item, + ] + ) + prediction_dataset = DatasetEntity( + [ + car_1_dataset_item, + car_2_dataset_item, + self.human_1_dataset_item(), + self.human_2_dataset_item(), + dog_1_dataset_item, + dog_2_dataset_item, + ] + ) + result_set = ResultSetEntity( + model=model, + ground_truth_dataset=ground_truth_dataset, + prediction_dataset=prediction_dataset, + ) + return result_set + + def single_label_result_set(self) -> ResultSetEntity: + configurable_params = ConfigurableParameters(header="Test model configurable params") + labels_group = LabelGroup( + name="single_class_model_labels_group", + labels=[self.human()], + ) + model_configuration = ModelConfiguration(configurable_params, LabelSchemaEntity(label_groups=[labels_group])) + model = ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + + ground_truth_dataset = DatasetEntity( + [ + self.human_1_dataset_item(), + self.human_2_dataset_item(), + self.human_3_dataset_item(), + ] + ) + prediction_dataset = DatasetEntity([self.human_1_dataset_item(), self.human_2_dataset_item()]) + result_set = ResultSetEntity( + model=model, + ground_truth_dataset=ground_truth_dataset, + prediction_dataset=prediction_dataset, + ) + return result_set + + @staticmethod + def check_confusion_matrix(matrix, expected_name, expected_labels, expected_matrix) -> None: + assert matrix.name == expected_name + assert matrix.row_labels == matrix.column_labels + assert matrix.row_labels == expected_labels + assert np.array_equal(matrix.matrix_values, expected_matrix) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestAccuracyFunctions: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_precision_metrics_group(self): + """ + Description: + Check "precision_metrics_group" function + + Input data: + "confusion_matrix" MatrixMetric-class object with specified "name", "matrix_values", "row_labels", + "column_labels" and "normalize" parameters + + Expected results: + Test passes if "BarMetricsGroup" object returned by "precision_metrics_group" function is equal to expected + + Steps + 1. Check "BarMetricsGroup" object returned by "precision_metrics_group" function for "confusion_matrix" with + default value of "row_labels" parameter + 2. Check "BarMetricsGroup" object returned by "precision_metrics_group" function for "confusion_matrix" with + specified value of "row_labels" parameter + """ + + def check_precision_metrics_group(matrix_for_precision, expected_metrics): + precision_metrics = precision_metrics_group(matrix_for_precision) + assert isinstance(precision_metrics, BarMetricsGroup) + assert precision_metrics.metrics == expected_metrics + assert isinstance(precision_metrics.visualization_info, BarChartInfo) + assert precision_metrics.visualization_info.name == "Precision per class" + assert precision_metrics.visualization_info.palette == ColorPalette.LABEL + assert precision_metrics.visualization_info.type == VisualizationType.BAR + + matrix_values = np.array([[0, 0.5, 0.5], [0, 0.5, 0.5], [1, 0, 0]]) + # Checking "BarMetricsGroup" object returned by "precision_metrics_group" for "confusion_matrix" with default + # "row_labels" + confusion_matrix = MatrixMetric(name="no_row_labels MatrixMetric", matrix_values=matrix_values) + check_precision_metrics_group( + matrix_for_precision=confusion_matrix, + expected_metrics=[ + ScoreMetric(name=np.int32(0), value=0.0), + ScoreMetric(name=np.int32(1), value=0.5), + ScoreMetric(name=np.int32(2), value=0.0), + ], + ) + # Checking "BarMetricsGroup" object returned by "precision_metrics_group" for "confusion_matrix" with specified + # "row_labels" + confusion_matrix = MatrixMetric( + name="row_labels MatrixMetric", + matrix_values=matrix_values, + row_labels=["label for row_1", "label for row_2", "label for row_3"], + column_labels=[ + "label for column_1", + "label for column_2", + "label for column_3", + ], + ) + check_precision_metrics_group( + matrix_for_precision=confusion_matrix, + expected_metrics=[ + ScoreMetric(name="label for row_1", value=0.0), + ScoreMetric(name="label for row_2", value=0.5), + ScoreMetric(name="label for row_3", value=0.0), + ], + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_recall_metrics_group(self): + """ + Description: + Check "recall_metrics_group" function + + Input data: + MatrixMetric-class object with specified "name", "matrix_values", "row_labels", "column_labels" and "normalize" + parameters + + Expected results: + Test passes if "BarMetricsGroup" object returned by "recall_metrics_group" function is equal to expected + + Steps + 1. Check "BarMetricsGroup" object returned by "recall_metrics_group" function when "confusion_matrix" + initialized with default "row_labels" parameter + 2. Check "BarMetricsGroup" object returned by "recall_metrics_group" function when "confusion_matrix" + initialized with specified "row_labels" parameter + """ + + def check_recall_metrics_group(matrix_for_recall, expected_metrics): + recall_metrics = recall_metrics_group(matrix_for_recall) + assert isinstance(recall_metrics, BarMetricsGroup) + assert recall_metrics.metrics == expected_metrics + assert isinstance(recall_metrics.visualization_info, BarChartInfo) + assert recall_metrics.visualization_info.name == "Recall per class" + assert recall_metrics.visualization_info.palette == ColorPalette.LABEL + assert recall_metrics.visualization_info.type == VisualizationType.BAR + + matrix_values = np.array([[0, 0.8, 0.4], [0, 0.8, 0.2], [0.4, 0.2, 0]]) + # Checking "BarMetricsGroup" returned by "recall_metrics_group" when "confusion_matrix" initialized with + # default "row_labels" + confusion_matrix = MatrixMetric(name="no_row_labels MatrixMetric", matrix_values=matrix_values) + + check_recall_metrics_group( + matrix_for_recall=confusion_matrix, + expected_metrics=[ + ScoreMetric(name=np.int32(0), value=0.0), + ScoreMetric(name=np.int32(1), value=0.8), + ScoreMetric(name=np.int32(2), value=0.0), + ], + ) + # Checking "BarMetricsGroup" returned by "recall_metrics_group" when "confusion_matrix" initialized with + # specified "row_labels" + confusion_matrix = MatrixMetric( + name="row_labels MatrixMetric", + matrix_values=matrix_values, + row_labels=["label for row_1", "label for row_2", "label for row_3"], + column_labels=[ + "label for column_1", + "label for column_2", + "label for column_3", + ], + ) + check_recall_metrics_group( + matrix_for_recall=confusion_matrix, + expected_metrics=[ + ScoreMetric(name="label for row_1", value=0.0), + ScoreMetric(name="label for row_2", value=0.8), + ScoreMetric(name="label for row_3", value=0.0), + ], + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_compute_unnormalized_confusion_matrices_from_resultset(self): + """ + Description: + Check "compute_unnormalized_confusion_matrices_from_resultset" function + + Input data: + ResultSetEntity-class object with specified "ground_truth_dataset" and "prediction_dataset" parameters + + Expected results: + Test passes if list returned by "compute_unnormalized_confusion_matrices_from_resultset" function is equal to + expected + + Steps + 1. Check list returned by "compute_unnormalized_confusion_matrices_from_resultset" function for model with + multiple labels specified in LabelGroup + 2. Check list returned by "compute_unnormalized_confusion_matrices_from_resultset" function for model with + single label specified in LabelGroup + 3. Check "ValueError" exception is raised when trying to compute confusion matrix for "ResultSetEntity" object + with "ground_truth_dataset" attribute equal to empty list + 4. Check "ValueError" exception is raised when trying to compute confusion matrix for "ResultSetEntity" object + with "prediction_dataset" attribute equal to empty list + """ + # Checking list returned by "compute_unnormalized_confusion_matrices_from_resultset" for model with multiple + # labels in LabelGroup + result_set = CommonActions().result_set() + confusion_matrices = compute_unnormalized_confusion_matrices_from_resultset(result_set) + assert len(confusion_matrices) == 2 + # Checking first confusion matrix + confusion_matrix = confusion_matrices[0] + CommonActions.check_confusion_matrix( + matrix=confusion_matrix, + expected_name="model_labels_group", + expected_labels=["car", "dog", "human"], + expected_matrix=np.array([[2, 0, 0], [0, 1, 0], [0, 1, 2]]), + ) + # Checking second confusion matrix + confusion_matrix = confusion_matrices[1] + CommonActions.check_confusion_matrix( + matrix=confusion_matrix, + expected_name="other_model_labels_group", + expected_labels=["dog", "human"], + expected_matrix=np.array([[1, 0], [1, 2]]), + ) + # Checking list returned by "compute_unnormalized_confusion_matrices_from_resultset" function for model with + # single label specified in LabelGroup + result_set = CommonActions().single_label_result_set() + confusion_matrices = compute_unnormalized_confusion_matrices_from_resultset(result_set) + assert len(confusion_matrices) == 1 + confusion_matrix = confusion_matrices[0] + CommonActions.check_confusion_matrix( + matrix=confusion_matrix, + expected_name="single_class_model_labels_group", + expected_labels=["human", "~ human"], + expected_matrix=np.array([[2, 0], [0, 0]]), + ) + # Checking "ValueError" exception is raised when trying to compute confusion matrix for "ResultSetEntity" with + # "ground_truth_dataset" equal to empty list + result_set.ground_truth_dataset = [] + with pytest.raises(ValueError): + compute_unnormalized_confusion_matrices_from_resultset(result_set) + # Checking "ValueError" exception is raised when trying to compute confusion matrix for "ResultSetEntity" with + # "prediction_dataset" equal to empty list + result_set = CommonActions().single_label_result_set() + result_set.prediction_dataset = [] + with pytest.raises(ValueError): + compute_unnormalized_confusion_matrices_from_resultset(result_set) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestAccuracy: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_accuracy_initialization(self): + """ + Description: + Check "Accuracy" class object initialization + + Input data: + "Accuracy" class object with specified "resultset" and "average" parameters + + Expected results: + Test passes if attributes of initialized "Accuracy" class object are equal to expected + + Steps + 1. Check attributes of "Accuracy" class object initialized with default optional parameters + 2. Check attributes of "Accuracy" class object initialized with specified optional parameters + """ + + def check_confusion_matrices(unnormalized_matrices): + assert len(unnormalized_matrices) == 2 + # Checking first confusion matrix + confusion_matrix = unnormalized_matrices[0] + assert isinstance(confusion_matrix, MatrixMetric) + CommonActions.check_confusion_matrix( + matrix=confusion_matrix, + expected_name="model_labels_group", + expected_labels=["car", "dog", "human"], + expected_matrix=np.array([[2, 0, 0], [0, 1, 0], [0, 1, 2]]), + ) + # Checking second confusion matrix + confusion_matrix = unnormalized_matrices[1] + assert isinstance(confusion_matrix, MatrixMetric) + CommonActions.check_confusion_matrix( + matrix=confusion_matrix, + expected_name="other_model_labels_group", + expected_labels=["dog", "human"], + expected_matrix=np.array([[1, 0], [1, 2]]), + ) + + result_set = CommonActions().result_set() + # Checking attributes of "Accuracy" object initialized with default optional parameters + accuracy = Accuracy(result_set) + # Checking "accuracy" attribute + assert accuracy.accuracy == ScoreMetric(name="Accuracy", value=0.8) + # Checking "unnormalized_matrices" attribute + actual_unnormalized_matrices = accuracy._unnormalized_matrices + check_confusion_matrices(actual_unnormalized_matrices) + # Checking attributes of "Accuracy" object initialized with specified optional parameters + accuracy = Accuracy(resultset=result_set, average=MetricAverageMethod.MACRO) + # Checking "accuracy" attribute + assert accuracy.accuracy == ScoreMetric(name="Accuracy", value=0.7916666666666667) + # Checking "unnormalized_matrices" attribute + actual_unnormalized_matrices = accuracy._unnormalized_matrices + check_confusion_matrices(actual_unnormalized_matrices) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_accuracy_get_performance(self): + """ + Description: + Check "Accuracy" class object "get_performance" method + + Input data: + "Accuracy" class object with specified "resultset" and "average" parameters + + Expected results: + Test passes if "Performance" object returned by "get_performance" method is equal to expected + """ + + def check_precision_recall_metrics(precision_metric, recall_metric, expected_precision, expected_recall): + # Checking Precision per class metric + assert isinstance(precision_metric, BarMetricsGroup) + assert precision_metric.metrics == expected_precision + assert precision_metric.visualization_info.name == "Precision per class" + assert precision_metric.visualization_info.palette == ColorPalette.LABEL + assert precision_metric.visualization_info.type == VisualizationType.BAR + # Checking Recall per class metric + assert isinstance(recall_metric, BarMetricsGroup) + assert recall_metric.metrics == expected_recall + assert recall_metric.visualization_info.name == "Recall per class" + assert recall_metric.visualization_info.palette == ColorPalette.LABEL + + result_set = CommonActions().result_set() + accuracy = Accuracy(result_set) + actual_performance = accuracy.get_performance() + assert isinstance(actual_performance, Performance) + assert actual_performance.score == ScoreMetric(name="Accuracy", value=0.8) + assert len(actual_performance.dashboard_metrics) == 5 + # Checking dashboard_metrics + # Checking first MatrixMetric object + actual_matrix_metric = actual_performance.dashboard_metrics[0].metrics[0] + CommonActions.check_confusion_matrix( + matrix=actual_matrix_metric, + expected_name="model_labels_group", + expected_labels=["car", "dog", "human"], + expected_matrix=np.array([[1, 0, 0], [0, 1, 0], [0, 0.33333334, 0.6666667]], dtype=np.float32), + ) + # Checking second MatrixMetric object + actual_matrix_metric = actual_performance.dashboard_metrics[0].metrics[1] + CommonActions.check_confusion_matrix( + matrix=actual_matrix_metric, + expected_name="other_model_labels_group", + expected_labels=["dog", "human"], + expected_matrix=np.array([[1, 0], [0.33333334, 0.6666667]], dtype=np.float32), + ) + # Checking Precision and Recall BarMetricsGroup for first label_group + precision_per_class_metric = actual_performance.dashboard_metrics[1] + recall_per_class_metric = actual_performance.dashboard_metrics[2] + check_precision_recall_metrics( + precision_metric=precision_per_class_metric, + recall_metric=recall_per_class_metric, + expected_precision=[ + ScoreMetric("car", 1.0), + ScoreMetric("dog", 0.5), + ScoreMetric("human", 1.0), + ], + expected_recall=[ + ScoreMetric("car", 1.0), + ScoreMetric("dog", 1.0), + ScoreMetric("human", 0.6666666666666666), + ], + ) + # Checking Precision and Recall BarMetricsGroup for second label_group + precision_per_class_metric = actual_performance.dashboard_metrics[3] + recall_per_class_metric = actual_performance.dashboard_metrics[4] + check_precision_recall_metrics( + precision_metric=precision_per_class_metric, + recall_metric=recall_per_class_metric, + expected_precision=[ScoreMetric("dog", 0.5), ScoreMetric("human", 1.0)], + expected_recall=[ + ScoreMetric("dog", 1.0), + ScoreMetric("human", 0.6666666666666666), + ], + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_accuracy_compute_accuracy(self): + """ + Description: + Check "Accuracy" class object "_compute_accuracy" method + + Input data: + "confusion_matrices" list and "average" MetricAverageMethod parameters + + Expected results: + Test passes if value returned by "_compute_accuracy" method is equal to expected + + Steps + 1. Check value returned by "_compute_accuracy" method for single confusion matrix specified as + "confusion_matrices" parameter + 2. Check value returned by "_compute_accuracy" method for several confusion matrices specified as + "confusion_matrices" parameter + 3. Check "ValueError" exception is raised when empty list is specified as "confusion_matrices" parameter + 4. Check "RuntimeError" exception is raised when unexpected method is specified as "average" parameter + """ + result_set = CommonActions().single_label_result_set() + accuracy = Accuracy(result_set) + # Checking value returned by "_compute_accuracy" for single confusion matrix specified as "confusion_matrices" + confusion_matrix = MatrixMetric( + name="confusion_matrix", + matrix_values=np.array([[6, 1, 0], [2, 6, 1], [0, 0, 4]]), + ) + expected_accuracy = np.float64(0.8) + assert ( + accuracy._compute_accuracy(average=MetricAverageMethod.MICRO, confusion_matrices=[confusion_matrix]) + == expected_accuracy + ) + assert ( + accuracy._compute_accuracy(average=MetricAverageMethod.MACRO, confusion_matrices=[confusion_matrix]) + == expected_accuracy + ) + # Checking value returned by "_compute_accuracy" for several confusion matrices specified as + # "confusion_matrices" + other_confusion_matrix = MatrixMetric( + name="other_confusion_matrix", + matrix_values=np.array([[4, 0, 0], [2, 4, 0], [0, 0, 6]]), + ) + assert accuracy._compute_accuracy( + average=MetricAverageMethod.MICRO, + confusion_matrices=[confusion_matrix, other_confusion_matrix], + ) == np.float64(0.8333333333333334) + assert accuracy._compute_accuracy( + average=MetricAverageMethod.MACRO, + confusion_matrices=[confusion_matrix, other_confusion_matrix], + ) == np.float64(0.8375) + # Checking "ValueError" exception is raised when empty list is specified as "confusion_matrices" + with pytest.raises(ValueError): + accuracy._compute_accuracy(average=MetricAverageMethod.MACRO, confusion_matrices=[]) + # Checking "RuntimeError" exception is raised when unexpected method is specified as "average" + with pytest.raises(RuntimeError): + accuracy._compute_accuracy(average="unknown average", confusion_matrices=[confusion_matrix]) diff --git a/tests/unit/api/usecases/evaluation/test_basic_operations.py b/tests/unit/api/usecases/evaluation/test_basic_operations.py new file mode 100644 index 00000000000..4101948f2f9 --- /dev/null +++ b/tests/unit/api/usecases/evaluation/test_basic_operations.py @@ -0,0 +1,221 @@ +# Copyright (C) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import numpy as np +import pytest + +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.usecases.evaluation.basic_operations import ( + divide_arrays_with_possible_zeros, + get_intersections_and_cardinalities, + intersection_box, + intersection_over_union, + precision_per_class, + recall_per_class, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestBasicOperationsFunctions: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_get_intersections_and_cardinalities(self): + """ + Description: + Check "get_intersections_and_cardinalities" function + + Input data: + "references" masks array, "predictions" masks array, "labels" list of "LabelEntity" class objects + + Expected results: + Test passes if tuple returned by "get_intersections_and_cardinalities" function is equal to expected + """ + equal_array = np.array([(1, 3, 1, 0), (2, 0, 2, 1), (3, 1, 0, 2), (0, 1, 1, 0)]) + other_equal_array = np.array([(2, 1, 1, 0), (1, 0, 2, 0), (2, 1, 0, 1)]) + unequal_reference_array = np.array([(1, 2, 3), (3, 0, 1), (1, 1, 0)]) + unequal_predictions_array = np.array([(0, 2, 3), (3, 0, 2), (1, 0, 1)]) + label_for_first_intersection = LabelEntity(name="label_for_intersection_1", domain=Domain.DETECTION) + label_for_second_intersection = LabelEntity(name="label_for_intersection_2", domain=Domain.DETECTION) + label_for_third_intersection = LabelEntity(name="label_for_intersection_3", domain=Domain.DETECTION) + non_assigned_label = LabelEntity(name="non_assigned", domain=Domain.DETECTION) + labels = [ + label_for_first_intersection, + label_for_second_intersection, + label_for_third_intersection, + non_assigned_label, + ] + intersections_and_cardinalities = get_intersections_and_cardinalities( + references=[equal_array, unequal_reference_array, other_equal_array], + predictions=[equal_array, unequal_predictions_array, other_equal_array], + labels=labels, + ) + # Checking intersections + intersections = intersections_and_cardinalities[0] + assert len(intersections) == 5 + assert intersections.get(label_for_first_intersection) == 12 + assert intersections.get(label_for_second_intersection) == 7 + assert intersections.get(label_for_third_intersection) == 4 + assert intersections.get(non_assigned_label) == 0 + assert intersections.get(None) == 23 + # Checking cardinalities + cardinalities = intersections_and_cardinalities[1] + assert len(cardinalities) == 5 + assert cardinalities.get(label_for_first_intersection) == 28 + assert cardinalities.get(label_for_second_intersection) == 15 + assert cardinalities.get(label_for_third_intersection) == 8 + assert cardinalities.get(non_assigned_label) == 0 + assert cardinalities.get(None) == 51 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_intersection_box(self): + """ + Description: + Check "intersection_box" function + + Input data: + "box_1" and "box_2" Rectangle-class objects + + Expected results: + Test passes if list returned by "intersection_box" function is equal to expected + + Steps + 1. Check list returned by "intersection_box" function for boxes that intersect in two points + 2. Check list returned by "intersection_box" function for boxes that intersect in one point + 3. Check list returned by "intersection_box" function for boxes that intersect by one side + 4. Check list returned by "intersection_box" function when one of boxes completely overlaps other + 5. Check list returned by "intersection_box" function for boxes that not intersect + """ + box_1 = Rectangle(x1=0.1, y1=0.1, x2=0.3, y2=0.4) + # Checking list returned by "intersection_box" for boxes that intersect in two points + box_2 = Rectangle(x1=0.2, y1=0.2, x2=0.5, y2=0.5) + assert intersection_box(box_1, box_2) == [0.2, 0.2, 0.3, 0.4] + # Checking list returned by "intersection_box" for boxes that intersect in one point + box_2 = Rectangle(x1=0.3, y1=0.4, x2=0.5, y2=0.5) + assert not intersection_box(box_1, box_2) + # Checking list returned by "intersection_box" for boxes that intersect by one side + box_2 = Rectangle(x1=0.3, y1=0.3, x2=0.5, y2=0.4) + assert not intersection_box(box_1, box_2) + # Checking list returned by "intersection_box" when one of boxes completely overlaps other + box_2 = Rectangle(x1=0.1, y1=0.1, x2=0.2, y2=0.3) + assert intersection_box(box_1, box_2) == [0.1, 0.1, 0.2, 0.3] + # Checking list returned by "intersection_box" for boxes that not intersect + box_2 = Rectangle(x1=0.4, y1=0.4, x2=0.6, y2=0.7) + assert not intersection_box(box_1, box_2) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_intersection_over_union(self): + """ + Description: + Check "intersection_over_union" function + + Input data: + "box_1" and "box_2" Rectangle-class objects + + Expected results: + Test passes if value returned by "intersection_over_union" function is equal to expected + + Steps + 1. Check value returned by "intersection_over_union" function for boxes that intersect in two points + 2. Check value returned by "intersection_over_union" function for boxes that intersect in one point + 3. Check value returned by "intersection_over_union" function for boxes that intersect by one side + 4. Check value returned by "intersection_over_union" function when one of boxes completely overlaps other + 5. Check value returned by "intersection_over_union" function for boxes that not intersect + """ + box_1 = Rectangle(x1=0.1, y1=0.1, x2=0.3, y2=0.4) + # Checking value returned by "intersection_over_union" for boxes that intersect in two points + box_2 = Rectangle(x1=0.2, y1=0.2, x2=0.5, y2=0.5) + assert round(intersection_over_union(box_1, box_2), 6) == 0.153846 + # Checking value returned by "intersection_over_union" for boxes that intersect in one point + box_2 = Rectangle(x1=0.3, y1=0.4, x2=0.5, y2=0.5) + assert intersection_over_union(box_1, box_2) == 0.0 + # Checking value returned by "intersection_over_union" for boxes that intersect by one side + box_2 = Rectangle(x1=0.3, y1=0.3, x2=0.5, y2=0.4) + assert intersection_over_union(box_1, box_2) == 0.0 + # Checking value returned by "intersection_over_union" when one of boxes completely overlaps other + box_2 = Rectangle(x1=0.1, y1=0.1, x2=0.2, y2=0.3) + assert round(intersection_over_union(box_1, box_2), 6) == 0.333333 + # Checking value returned by "intersection_over_union" for boxes that not intersect + box_2 = Rectangle(x1=0.4, y1=0.4, x2=0.6, y2=0.7) + assert intersection_over_union(box_1, box_2) == 0.0 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_precision_per_class(self): + """ + Description: + Check "precision_per_class" function + + Input data: + Confusion matrix + + Expected results: + Test passes if array returned by "precision_per_class" function is equal to expected + + Steps + 1. Check array returned by "precision_per_class" function for square matrix + 2. Check array returned by "precision_per_class" function for non-square matrix + """ + # Checking array returned by "precision_per_class" for square matrix + matrix = np.array([(0.5, 1.0, 1.0), (1.0, 0.5, 0.5), (0.5, 1.0, 1.0)]) + assert np.array_equal(precision_per_class(matrix), np.array([0.25, 0.2, 0.4])) + # Checking array returned by "precision_per_class" for non-square matrix + matrix = np.array([(0.6, 0.3, 0.6), (0.3, 0.6, 0.3), (0.6, 0.8, 0.3), (0.9, 0.3, 0.6)]) + assert np.array_equal(precision_per_class(matrix), np.array([0.25, 0.3])) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_recall_per_class(self): + """ + Description: + Check "recall_per_class" function + + Input data: + Confusion matrix + + Expected results: + Test passes if array returned by "recall_per_class" function is equal to expected + """ + matrix = np.array([(6, 2, 0), (0, 10, 6), (0, 8, 12)]) + assert np.array_equal(recall_per_class(matrix), np.array([0.75, 0.625, 0.6])) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_divide_arrays_with_possible_zeros(self): + """ + Description: + Check "divide_arrays_with_possible_zeros" function + + Input data: + "numerator" and "denominator" matrices + + Expected results: + Test passes if array returned by "divide_arrays_with_possible_zeros" function is equal to expected + """ + array = np.array([(6, 2, 0), (0, 10, 6), (0, 8, 12)]) + other_array = np.array([(2, 4, 0), (0, 2, 4), (0, 0, 1)]) + assert np.array_equal( + a1=divide_arrays_with_possible_zeros(array, other_array), + a2=np.array([(3, 0.5, 0), (0, 5, 1.5), (0, 0, 12)]), + ) diff --git a/tests/unit/api/usecases/evaluation/test_dice.py b/tests/unit/api/usecases/evaluation/test_dice.py new file mode 100644 index 00000000000..637ea433cd3 --- /dev/null +++ b/tests/unit/api/usecases/evaluation/test_dice.py @@ -0,0 +1,547 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import datetime + +import numpy as np +import pytest + +from otx.api.configuration import ConfigurableParameters +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.color import Color +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelSchemaEntity +from otx.api.entities.metrics import ( + BarChartInfo, + BarMetricsGroup, + ColorPalette, + Performance, + ScoreMetric, + VisualizationType, +) +from otx.api.entities.model import ModelConfiguration, ModelEntity +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.usecases.evaluation.averaging import MetricAverageMethod +from otx.api.usecases.evaluation.dice import DiceAverage +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDice: + color = Color(0, 255, 0) + creation_date = datetime.datetime(year=2022, month=1, day=10) + image = Image(data=np.random.randint(low=0, high=255, size=(64, 32, 3))) + full_box_roi = Rectangle.generate_full_box() + car_label = LabelEntity( + name="car", + domain=Domain.DETECTION, + color=color, + creation_date=creation_date, + id=ID("car_label"), + ) + human_label = LabelEntity( + name="human", + domain=Domain.DETECTION, + color=color, + creation_date=creation_date, + id=ID("human_label"), + ) + dog_label = LabelEntity( + name="dog", + domain=Domain.DETECTION, + color=color, + creation_date=creation_date, + id=ID("dog_label"), + ) + cat_label = LabelEntity( + name="cat", + domain=Domain.DETECTION, + color=color, + creation_date=creation_date, + id=ID("cat_label"), + ) + configurable_params = ConfigurableParameters(header="Test model configurable params") + + def human_1_ground_truth(self) -> DatasetItemEntity: + human_roi = Annotation(shape=self.full_box_roi, labels=[ScoredLabel(self.human_label)]) + human_annotation = Annotation( + shape=Rectangle(x1=0.4, y1=0, x2=0.5, y2=0.2), + labels=[ScoredLabel(self.human_label)], + ) + human_annotation_scene = AnnotationSceneEntity( + annotations=[human_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + human_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=human_annotation_scene, roi=human_roi) + return human_dataset_item + + def human_2_ground_truth(self) -> DatasetItemEntity: + human_roi = Annotation(shape=self.full_box_roi, labels=[ScoredLabel(self.human_label)]) + human_annotation = Annotation( + shape=Rectangle(x1=0.6, y1=0, x2=0.7, y2=0.2), + labels=[ScoredLabel(self.human_label)], + ) + human_annotation_scene = AnnotationSceneEntity( + annotations=[human_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + human_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=human_annotation_scene, roi=human_roi) + return human_dataset_item + + def dog_ground_truth(self) -> DatasetItemEntity: + dog_roi = Annotation(shape=self.full_box_roi, labels=[ScoredLabel(self.dog_label)]) + dog_annotation = Annotation( + shape=Rectangle(x1=0.8, y1=0, x2=0.9, y2=0.1), + labels=[ScoredLabel(self.dog_label)], + ) + dog_annotation_scene = AnnotationSceneEntity(annotations=[dog_annotation], kind=AnnotationSceneKind.ANNOTATION) + dog_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=dog_annotation_scene, roi=dog_roi) + return dog_dataset_item + + def human_1_predicted(self) -> DatasetItemEntity: + human_roi = Annotation(shape=self.full_box_roi, labels=[ScoredLabel(self.human_label)]) + human_annotation = Annotation( + shape=Rectangle(x1=0.4, y1=0, x2=0.5, y2=0.4), + labels=[ScoredLabel(self.human_label)], + ) + human_annotation_scene = AnnotationSceneEntity( + annotations=[human_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + human_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=human_annotation_scene, roi=human_roi) + return human_dataset_item + + def human_2_predicted(self) -> DatasetItemEntity: + human_roi = Annotation(shape=self.full_box_roi, labels=[ScoredLabel(self.human_label)]) + human_annotation = Annotation( + shape=Rectangle(x1=0.6, y1=0, x2=0.7, y2=0.2), + labels=[ScoredLabel(self.human_label)], + ) + human_annotation_scene = AnnotationSceneEntity( + annotations=[human_annotation], kind=AnnotationSceneKind.ANNOTATION + ) + human_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=human_annotation_scene, roi=human_roi) + return human_dataset_item + + def cat_predicted(self) -> DatasetItemEntity: + cat_roi = Annotation(shape=self.full_box_roi, labels=[ScoredLabel(self.cat_label)]) + cat_annotation = Annotation( + shape=Rectangle(x1=0.9, y1=0, x2=1.0, y2=0.1), + labels=[ScoredLabel(self.cat_label)], + ) + cat_annotation_scene = AnnotationSceneEntity(annotations=[cat_annotation], kind=AnnotationSceneKind.ANNOTATION) + cat_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=cat_annotation_scene, roi=cat_roi) + return cat_dataset_item + + def car_dataset_item(self) -> DatasetItemEntity: + car_roi = Annotation( + shape=self.full_box_roi, + labels=[ScoredLabel(self.car_label)], + ) + car_annotation = Annotation( + shape=Rectangle(x1=0.1, y1=0, x2=0.3, y2=0.2), + labels=[ScoredLabel(self.car_label)], + ) + car_annotation_scene = AnnotationSceneEntity(annotations=[car_annotation], kind=AnnotationSceneKind.ANNOTATION) + car_dataset_item = DatasetItemEntity(media=self.image, annotation_scene=car_annotation_scene, roi=car_roi) + return car_dataset_item + + def model(self) -> ModelEntity: + labels_group = LabelGroup( + name="model_labels_group", + labels=[self.car_label, self.human_label, self.dog_label, self.cat_label], + ) + model_configuration = ModelConfiguration( + configurable_parameters=self.configurable_params, + label_schema=LabelSchemaEntity(label_groups=[labels_group]), + ) + model = ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + return model + + @staticmethod + def check_score_metric(score_metric, expected_name, expected_value): + assert isinstance(score_metric, ScoreMetric) + assert score_metric.name == expected_name + assert score_metric.value == pytest.approx(expected_value) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dice_initialization(self): + """ + Description: + Check "DiceAverage" class object initialization + + Input data: + "DiceAverage" object with specified "resultset" and "average" parameters + + Expected results: + Test passes if attributes of initialized "DiceAverage" object are equal to expected + + Steps + 1. Check attributes of "DiceAverage" object initialized with default value of "average" parameter + 2. Check attributes of "DiceAverage" object initialized with specified value of "average" parameter + 3. Check that "ValueError" exception is raised when initializing "DiceAverage" object with empty list prediction + "resultset" attribute + 4. Check "ValueError" exception is raised when initializing "DiceAverage" object with "resultset" attribute with + unequal length of "ground_truth_dataset" and "prediction_dataset" + """ + + def check_dice_attributes( + dice_actual: DiceAverage, + expected_average_type: MetricAverageMethod, + expected_overall_dice: float, + ): + assert dice_actual.average == expected_average_type + # Checking "overall_dice" attribute + self.check_score_metric( + score_metric=dice_actual.overall_dice, + expected_name="Dice Average", + expected_value=expected_overall_dice, + ) + # Checking "dice_per_label" attribute + assert len(dice_actual.dice_per_label) == 4 + self.check_score_metric( + score_metric=dice_actual.dice_per_label.get(self.car_label), + expected_name="car", + expected_value=1.0, + ) + self.check_score_metric( + score_metric=dice_actual.dice_per_label.get(self.human_label), + expected_name="human", + expected_value=0.782608695652174, + ) + self.check_score_metric( + score_metric=dice_actual.dice_per_label.get(self.dog_label), + expected_name="dog", + expected_value=0.0, + ) + self.check_score_metric( + score_metric=dice_actual.dice_per_label.get(self.cat_label), + expected_name="cat", + expected_value=0.0, + ) + + model = self.model() + human_1_ground_truth = self.human_1_ground_truth() + car_dataset_item = self.car_dataset_item() + ground_truth_dataset = DatasetEntity( + [ + car_dataset_item, + human_1_ground_truth, + self.human_2_ground_truth(), + self.dog_ground_truth(), + ] + ) + predicted_dataset = DatasetEntity( + [ + car_dataset_item, + self.human_1_predicted(), + self.human_2_predicted(), + self.cat_predicted(), + ] + ) + result_set = ResultSetEntity( + model=model, + ground_truth_dataset=ground_truth_dataset, + prediction_dataset=predicted_dataset, + ) + # Checking attributes of "DiceAverage" initialized with default "average" + dice = DiceAverage(resultset=result_set) + check_dice_attributes( + dice_actual=dice, + expected_average_type=MetricAverageMethod.MACRO, + expected_overall_dice=0.44565217391304346, + ) + # Checking attributes of "DiceAverage" initialized with specified "average" + dice = DiceAverage(resultset=result_set, average=MetricAverageMethod.MICRO) + check_dice_attributes( + dice_actual=dice, + expected_average_type=MetricAverageMethod.MICRO, + expected_overall_dice=0.7746741154562383, + ) + # Checking "ValueError" exception raised when initializing "DiceAverage" with empty list prediction result_set + result_set = ResultSetEntity( + model=model, + ground_truth_dataset=ground_truth_dataset, + prediction_dataset=DatasetEntity(items=[]), + ) + with pytest.raises(ValueError): + DiceAverage(resultset=result_set) + + # Checking "ValueError" exception raised when initializing "DiceAverage" with "resultset" with unequal length of + # "ground_truth_dataset" and "prediction_dataset" + result_set = ResultSetEntity( + model=model, + ground_truth_dataset=DatasetEntity([car_dataset_item, human_1_ground_truth]), + prediction_dataset=DatasetEntity([car_dataset_item]), + ) + with pytest.raises(ValueError): + DiceAverage(resultset=result_set) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dice_get_performance(self): + """ + Description: + Check "DiceAverage" class "get_performance" method + + Input data: + "DiceAverage" object with specified "resultset" and "average" parameters + + Expected results: + Test passes if "Performance" object returned by "get_performance" method is equal to expected + + Steps + 1. Check "Performance" object returned by "get_performance" method for "DiceAverage" class object with length of + "dice_per_label" attribute more than 0 + 2. Check "Performance" object returned by "get_performance" method for "DiceAverage" class object with length of + "dice_per_label" attribute equal to 0 + """ + human_1_ground_truth = self.human_1_ground_truth() + human_1_predicted = self.human_1_predicted() + car_dataset_item = self.car_dataset_item() + # Checking "Performance" returned by "get_performance" for "DiceAverage" with length of "dice_per_label" more + # than 0 + configurable_params = self.configurable_params + labels_group = LabelGroup( + name="model_labels_group", + labels=[self.car_label, self.human_label, self.dog_label, self.cat_label], + ) + model_configuration = ModelConfiguration( + configurable_parameters=configurable_params, + label_schema=LabelSchemaEntity(label_groups=[labels_group]), + ) + model = ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + ground_truth_dataset = DatasetEntity( + [ + car_dataset_item, + human_1_ground_truth, + self.human_2_ground_truth(), + self.dog_ground_truth(), + ] + ) + predicted_dataset = DatasetEntity( + [ + car_dataset_item, + human_1_predicted, + self.human_2_predicted(), + self.cat_predicted(), + ] + ) + result_set = ResultSetEntity( + model=model, + ground_truth_dataset=ground_truth_dataset, + prediction_dataset=predicted_dataset, + ) + dice = DiceAverage(resultset=result_set) + performance = dice.get_performance() + assert isinstance(performance, Performance) + # Checking "score" attribute + self.check_score_metric( + score_metric=performance.score, + expected_name="Dice Average", + expected_value=0.44565217391304346, + ) + # Checking "dashboard_metrics" attribute + assert len(performance.dashboard_metrics) == 1 + dashboard_metric = performance.dashboard_metrics[0] + assert isinstance(dashboard_metric, BarMetricsGroup) + # Checking "metrics" attribute + assert len(dashboard_metric.metrics) == 4 + self.check_score_metric( + score_metric=dashboard_metric.metrics[0], + expected_name="car", + expected_value=1.0, + ) + self.check_score_metric( + score_metric=dashboard_metric.metrics[1], + expected_name="cat", + expected_value=0.0, + ) + self.check_score_metric( + score_metric=dashboard_metric.metrics[2], + expected_name="dog", + expected_value=0.0, + ) + self.check_score_metric( + score_metric=dashboard_metric.metrics[3], + expected_name="human", + expected_value=0.782608695652174, + ) + # Checking "visualization_info" attribute + assert isinstance(dashboard_metric.visualization_info, BarChartInfo) + assert dashboard_metric.visualization_info.name == "Dice Average Per Label" + assert dashboard_metric.visualization_info.palette == ColorPalette.LABEL + assert dashboard_metric.visualization_info.type == VisualizationType.BAR + # Checking "Performance" returned by "get_performance" for "DiceAverage" with length of "dice_per_label" equal + # to 0 + labels_group = LabelGroup(name="model_labels_group", labels=[self.car_label]) + model_configuration = ModelConfiguration(configurable_params, LabelSchemaEntity(label_groups=[labels_group])) + model = ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + ground_truth_dataset = DatasetEntity([human_1_ground_truth]) + predicted_dataset = DatasetEntity([human_1_predicted]) + result_set = ResultSetEntity( + model=model, + ground_truth_dataset=ground_truth_dataset, + prediction_dataset=predicted_dataset, + ) + dice = DiceAverage(resultset=result_set) + performance = dice.get_performance() + assert isinstance(performance, Performance) + # Checking "score" attribute + self.check_score_metric( + score_metric=performance.score, + expected_name="Dice Average", + expected_value=0.0, + ) + # Checking "dashboard_metrics" attribute + assert performance.dashboard_metrics == [] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dice_compute_dice_using_intersection_and_cardinality(self): + """ + Description: + Check "DiceAverage" class "compute_dice_using_intersection_and_cardinality" method + + Input data: + "DiceAverage" object, "all_intersection" dictionary, "all_cardinality" dictionary, "average" MetricAverageMethod + object + + Expected results: + Test passes if tuple returned by "compute_dice_using_intersection_and_cardinality" method is equal to expected + + Steps + 1. Check tuple returned by "compute_dice_using_intersection_and_cardinality" method for MICRO "average" + parameter + 2. Check tuple returned by "compute_dice_using_intersection_and_cardinality" method for MACRO "average" + parameter + 3. Check tuple returned by "compute_dice_using_intersection_and_cardinality" method when "None" key is not + specified in "all_intersection" and "all_cardinality" dictionaries + 4. Check "KeyError" exception is raised when keys of "all_intersection" and "all_cardinality" dictionaries are + not match + 5. Check "ValueError" exception is raised when intersection of certain key is larger than its cardinality + """ + + def check_dice(dice_actual: tuple, expected_overall_dice: float): + assert len(dice_actual) == 2 + self.check_score_metric( + score_metric=dice_actual[0], + expected_name="Dice Average", + expected_value=expected_overall_dice, + ) + dice_per_label = dice_actual[1] + assert len(dice_per_label) == 4 + self.check_score_metric( + score_metric=dice_per_label.get(self.car_label), + expected_name="car", + expected_value=2.0, + ) + self.check_score_metric( + score_metric=dice_per_label.get(self.human_label), + expected_name="human", + expected_value=1.6, + ) + self.check_score_metric( + score_metric=dice_per_label.get(self.dog_label), + expected_name="dog", + expected_value=0.5, + ) + self.check_score_metric( + score_metric=dice_per_label.get(self.cat_label), + expected_name="cat", + expected_value=0.0, + ) + + # Checking tuple returned by "compute_dice_using_intersection_and_cardinality" for MICRO "average" + all_intersection = { + self.car_label: 10, + self.human_label: 8, + self.dog_label: 2, + self.cat_label: 0, + None: 9, + } + all_cardinality = { + self.car_label: 10, + self.human_label: 10, + self.dog_label: 8, + self.cat_label: 2, + None: 12, + } + dice = DiceAverage.compute_dice_using_intersection_and_cardinality( + all_intersection=all_intersection, + all_cardinality=all_cardinality, + average=MetricAverageMethod.MICRO, + ) + check_dice(dice_actual=dice, expected_overall_dice=1.5) + # Checking tuple returned by "compute_dice_using_intersection_and_cardinality" for MACRO "average" + dice = DiceAverage.compute_dice_using_intersection_and_cardinality( + all_intersection=all_intersection, + all_cardinality=all_cardinality, + average=MetricAverageMethod.MACRO, + ) + check_dice(dice_actual=dice, expected_overall_dice=1.025) + # Checking tuple returned by "compute_dice_using_intersection_and_cardinality" when "None" key is not + # specified in "all_intersection" and "all_cardinality" dictionaries + all_intersection = { + self.car_label: 10, + self.human_label: 8, + self.dog_label: 2, + self.cat_label: 0, + } + all_cardinality = { + self.car_label: 10, + self.human_label: 10, + self.dog_label: 8, + self.cat_label: 2, + } + # Check for MACRO "average" parameter + dice = DiceAverage.compute_dice_using_intersection_and_cardinality( + all_intersection=all_intersection, + all_cardinality=all_cardinality, + average=MetricAverageMethod.MACRO, + ) + check_dice(dice_actual=dice, expected_overall_dice=1.025) + # Expected KeyError exception for MICRO "average" parameter + with pytest.raises(KeyError): + DiceAverage.compute_dice_using_intersection_and_cardinality( + all_intersection=all_intersection, + all_cardinality=all_cardinality, + average=MetricAverageMethod.MICRO, + ) + # Checking "KeyError" exception is raised when keys of "all_intersection" and "all_cardinality" dictionaries are + # not match + all_intersection = {self.car_label: 10, self.human_label: 9, None: 9} + all_cardinality = { + self.car_label: 10, + self.dog_label: 8, + self.cat_label: 2, + None: 12, + } + with pytest.raises(KeyError): + DiceAverage.compute_dice_using_intersection_and_cardinality( + all_intersection=all_intersection, + all_cardinality=all_cardinality, + average=MetricAverageMethod.MACRO, + ) + # Checking "ValueError" exception is raised when intersection of certain key is larger than its cardinality + all_intersection = {self.car_label: 10, self.human_label: 9, None: 12} + all_cardinality = {self.car_label: 10, self.human_label: 8, None: 12} + with pytest.raises(ValueError): + DiceAverage.compute_dice_using_intersection_and_cardinality( + all_intersection=all_intersection, + all_cardinality=all_cardinality, + average=MetricAverageMethod.MACRO, + ) diff --git a/tests/unit/api/usecases/evaluation/test_f_measure.py b/tests/unit/api/usecases/evaluation/test_f_measure.py new file mode 100644 index 00000000000..20f3e5bc775 --- /dev/null +++ b/tests/unit/api/usecases/evaluation/test_f_measure.py @@ -0,0 +1,1739 @@ +# Copyright (C) 2020-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import datetime +from typing import cast + +import numpy as np +import pytest + +from otx.api.configuration import ConfigurableParameters +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.color import Color +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelSchemaEntity +from otx.api.entities.metrics import ( + BarChartInfo, + BarMetricsGroup, + ColorPalette, + CurveMetric, + LineChartInfo, + LineMetricsGroup, + Performance, + ScoreMetric, + TextChartInfo, + TextMetricsGroup, + VisualizationType, +) +from otx.api.entities.model import ModelConfiguration, ModelEntity +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.usecases.evaluation.f_measure import ( + FMeasure, + _AggregatedResults, + _FMeasureCalculator, + _Metrics, + _OverallResults, + _ResultCounters, + bounding_box_intersection_over_union, + get_iou_matrix, + get_n_false_negatives, + intersection_box, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestFMeasureFunctions: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_intersection_box(self): + """ + Description: + Check "intersection_box" function + + Input data: + Bounding boxes coordinates + + Expected results: + Test passes if box returned by "intersection_box" function has expected coordinates + + Steps + 1. Check box returned by "intersection_box" function for boxes that intersect in two points + 2. Check box returned by "intersection_box" function for boxes that intersect in one point + 3. Check box returned by "intersection_box" function for boxes that intersect by one side + 4. Check box returned by "intersection_box" function when one of boxes completely covers other + """ + base_box = [2, 2, 5, 6] + # Checking box returned by "intersection_box" for boxes that intersect in two points + assert intersection_box(box1=base_box, box2=[4, 4, 7, 8]) == (4, 5, 6, 4) + # Checking box returned by "intersection_box" for boxes that intersect in one point + assert intersection_box(box1=base_box, box2=[1, 1, 2, 2]) == (2, 2, 2, 2) + # Checking box returned by "intersection_box" for boxes that intersect by one side + assert intersection_box(box1=base_box, box2=[2, 1, 5, 2]) == (2, 5, 2, 2) + # Checking box returned by "intersection_box" when one of boxes completely covers other + assert intersection_box(box1=base_box, box2=[0, 0, 10, 10]) == (2, 5, 6, 2) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_bounding_box_intersection_over_union(self): + """ + Description: + Check "bounding_box_intersection_over_union" function + + Input data: + Bounding boxes coordinates + + Expected results: + Test passes if value returned by "bounding_box_intersection_over_union" function is equal to expected + + Steps + 1. Check value returned by "bounding_box_intersection_over_union" function when "x_right" coordinate of + intersection box more than "x_left" + 2. Check value returned by "bounding_box_intersection_over_union" function when "y_bottom" coordinate of + intersection box more than "y_top" + 3. Check value returned by "bounding_box_intersection_over_union" function when boxes intersect in two points + """ + # Checking value returned by "bounding_box_intersection_over_union" when "x_right" coordinate of + # intersection box more than "x_left" + assert bounding_box_intersection_over_union(box1=[2, 2, 5, 6], box2=[7, 4, 4, 8]) == 0.0 + # Checking value returned by "bounding_box_intersection_over_union" when "y_bottom" coordinate of + # intersection box more than "y_top" + assert bounding_box_intersection_over_union(box1=[2, 8, 6, 1], box2=[1, 7, 5, 2]) == 0.0 + # Checking value returned by "bounding_box_intersection_over_union" when boxes intersect in two points + assert bounding_box_intersection_over_union(box1=[1, 3, 3, 7], box2=[2, 4, 5, 5]) == 0.1 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_get_iou_matrix(self): + """ + Description: + Check "get_iou_matrix" function + + Input data: + Bounding boxes coordinates + + Expected results: + Test passes if array returned by "get_iou_matrix" function is equal to expected + """ + boxes_1 = [[2, 2, 5, 6], [2, 8, 6, 1], [1, 3, 3, 7]] + boxes_2 = [[7, 4, 4, 8], [2, 4, 5, 5], [1, 1, 2, 2], [0, 0, 10, 10]] + expected_matrix = [ + [0.0, 0.25, 0.0, 0.12], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.1, 0.0, 0.08], + ] + assert np.array_equal(get_iou_matrix(boxes_1, boxes_2), expected_matrix) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_get_n_false_negatives(self): + """ + Description: + Check "get_n_false_negatives" function + + Input data: + IoU-matrix np.array + + Expected results: + Test passes if value returned by "get_n_false_negatives" function is equal to expected + + Steps + 1. Check value returned by "get_n_false_negatives" function when max element in a row is less than + "iou_threshold" parameter + 2. Check value returned by "get_n_false_negatives" function when several elements in a column are more than + "iou_threshold" parameter + """ + iou_matrix = np.array([[0.0, 0.25, 0.0, 0.09], [0.0, 0.0, 0.0, 0.0], [0.0, 0.1, 0.0, 0.08]]) + # Checking value returned by "get_n_false_negatives" when max element in a row is less than "iou_threshold" + assert get_n_false_negatives(iou_matrix, 0.11) == 2 + # Checking value returned by "get_n_false_negatives" when several elements in a column are more than + # "iou_threshold" + assert get_n_false_negatives(iou_matrix, 0.09) == 2 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestMetrics: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_metrics_initialization(self): + """ + Description: + Check "_Metrics" class object initialization + + Input data: + "_Metrics" class object with specified "f_measure", "precision" and "recall" parameters + + Expected results: + Test passes if "f_measure", "precision" and "recall" attributes of initialized "_Metrics" class object + are equal to expected + """ + f_measure = 0.4 + precision = 0.9 + recall = 0.3 + metrics = _Metrics(f_measure=f_measure, precision=precision, recall=recall) + assert metrics.f_measure == f_measure + assert metrics.precision == precision + assert metrics.recall == recall + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestResultCounters: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_result_counters_initialization(self): + """ + Description: + Check "_ResultCounters" class object initialization + + Input data: + "_ResultCounters" class object with specified "n_false_negatives", "n_true" and "n_predicted" parameters + + Expected results: + Test passes if "n_false_negatives", "n_true" and "n_predicted" attributes of initialized "_ResultCounters" + class object are equal to expected + """ + n_false_negatives = 2 + n_true = 9 + n_predicted = 9 + result_counters = _ResultCounters(n_false_negatives=n_false_negatives, n_true=n_true, n_predicted=n_predicted) + assert result_counters.n_false_negatives == n_false_negatives + assert result_counters.n_true == n_true + assert result_counters.n_predicted == n_predicted + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_result_counters_calculate_f_measure(self): + """ + Description: + Check "_ResultCounters" class "calculate_f_measure" method + + Input data: + "_ResultCounters" class object with specified "n_false_negatives", "n_true" and "n_predicted" parameters + + Expected results: + Test passes if value returned by "calculate_f_measure" method is equal to expected + + Steps + 1. Check value returned by "calculate_f_measure" method when "n_true" attribute of "_ResultCounters" object is + more than "n_false_negatives" + 2. Check value returned by "calculate_f_measure" method when "n_true" attribute of "_ResultCounters" object is + less than "n_false_negatives" + 3. Check value returned by "calculate_f_measure" method when "n_true" attribute of "_ResultCounters" object is + equal to "n_false_negatives" + 4. Check value returned by "calculate_f_measure" method when "n_predicted" attribute of "_ResultCounters" object + is equal to 0 + 5. Check value returned by "calculate_f_measure" method when "n_true" attribute of "_ResultCounters" object is + equal to 0 + """ + + def check_calculated_f_measure( + f_measure_actual: _Metrics, + expected_precision: float, + expected_recall: float, + ): + expected_f_measure = (2 * expected_precision * expected_recall) / ( + expected_precision + expected_recall + np.finfo(float).eps + ) + assert f_measure_actual.f_measure == expected_f_measure + assert f_measure_actual.precision == expected_precision + assert f_measure_actual.recall == expected_recall + + # Checking value returned by "calculate_f_measure" when "n_true" is more than "n_false_negatives" + result_counters = _ResultCounters(n_false_negatives=4, n_true=8, n_predicted=16) + precision = (8 - 4) / 16 # (n_true-n_false_negatives)/n_predicted + recall = (8 - 4) / 8 # (n_true-n_false_negatives)/n_true + f_measure = result_counters.calculate_f_measure() + check_calculated_f_measure(f_measure, precision, recall) + # Checking value returned by "calculate_f_measure" when "n_true" is less than "n_false_negatives" + result_counters = _ResultCounters(n_false_negatives=16, n_true=8, n_predicted=16) + f_measure = result_counters.calculate_f_measure() + precision = (8 - 16) / 16 # (n_true-n_false_negatives)/n_predicted + recall = (8 - 16) / 8 # (n_true-n_false_negatives)/n_true + check_calculated_f_measure(f_measure, precision, recall) + # Checking value returned by "calculate_f_measure" when "n_true" is equal to "n_false_negatives" + result_counters = _ResultCounters(n_false_negatives=8, n_true=8, n_predicted=14) + f_measure = result_counters.calculate_f_measure() + check_calculated_f_measure(f_measure, 0.0, 0.0) + # Checking value returned by "calculate_f_measure" when "n_predicted" is equal to 0 + result_counters = _ResultCounters(n_false_negatives=2, n_true=8, n_predicted=0) + f_measure = result_counters.calculate_f_measure() + check_calculated_f_measure(f_measure, 1.0, 0.0) + # Checking value returned by "calculate_f_measure" method when "n_true" is equal to 0 + result_counters = _ResultCounters(n_false_negatives=2, n_true=0, n_predicted=8) + f_measure = result_counters.calculate_f_measure() + check_calculated_f_measure(f_measure, 0.0, 1.0) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestAggregatedResults: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_aggregate_results_initialization(self): + """ + Description: + Check "_AggregatedResults" class object initialization + + Input data: + "_AggregatedResults" class object with specified "classes" parameter + + Expected results: + Test passes if attributes of initialized "_AggregatedResults" class object are equal to expected + + Steps + 1. Check attributes of "_AggregatedResults" object initialized with "classes" attribute equal to list with + several classes + 2. Check attributes of "_AggregatedResults" object initialized with "classes" attribute equal to list with one + class + 3. Check attributes of "_AggregatedResults" object initialized with "classes" attribute equal to empty list + """ + + def check_aggregate_results_attributes(aggregate_results, expected_classes_curve): + assert aggregate_results.f_measure_curve == expected_classes_curve + assert aggregate_results.precision_curve == expected_classes_curve + assert aggregate_results.recall_curve == expected_classes_curve + assert aggregate_results.all_classes_f_measure_curve == [] + assert aggregate_results.best_f_measure == 0.0 + assert aggregate_results.best_threshold == 0.0 + + # Checking attributes of "_AggregatedResults" object initialized with "classes" equal to list with + # several classes + check_aggregate_results_attributes( + aggregate_results=_AggregatedResults(["class_1", "class_2", "class_3"]), + expected_classes_curve={"class_1": [], "class_2": [], "class_3": []}, + ) + # Checking attributes of "_AggregatedResults" object initialized with "classes" equal to list with one class + check_aggregate_results_attributes( + aggregate_results=_AggregatedResults(["class_1"]), + expected_classes_curve={"class_1": []}, + ) + # Checking attributes of "_AggregatedResults" object initialized with "classes" equal to empty list + check_aggregate_results_attributes(aggregate_results=_AggregatedResults([]), expected_classes_curve={}) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestOverallResults: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_result_counters_initialization(self): + """ + Description: + Check "_OverallResults" class object initialization + + Input data: + "_OverallResults" class object with specified "per_confidence", "per_nms", "best_f_measure_per_class" and + "best_f_measure" parameters + + Expected results: + Test passes if attributes of initialized "_OverallResults" class object are equal to expected + """ + per_confidence = _AggregatedResults(["confidence_class_1", "confidence_class_2"]) + per_nms = _AggregatedResults(["nms_class_1", "nms_class_2"]) + best_f_measure_per_class = {"class_1": 0.6, "class_2": 0.8} + best_f_measure = 0.8 + overall_results = _OverallResults( + per_confidence=per_confidence, + per_nms=per_nms, + best_f_measure_per_class=best_f_measure_per_class, + best_f_measure=best_f_measure, + ) + assert overall_results.per_confidence == per_confidence + assert overall_results.per_nms == per_nms + assert overall_results.best_f_measure_per_class == best_f_measure_per_class + assert overall_results.best_f_measure == best_f_measure + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestFMeasureCalculator: + @staticmethod + def ground_truth_boxes_per_image(): + return [ # images + [ # image_1 boxes + (0.5, 0.1, 0.8, 0.9, "class_1", 0.95), + (0.1, 0.1, 0.3, 0.7, "class_2", 0.93), + (0.05, 0.05, 0.3, 0.75, "class_2", 0.91), + ], + [ # image_2 boxes + (0.15, 0.0, 0.4, 0.75, "class_1", 0.9), + (0.1, 0.0, 0.45, 0.7, "class_1", 0.88), + (0.45, 0.0, 0.95, 0.45, "class_3", 0.94), + (0.45, 0.0, 1.0, 0.5, "class_3", 0.92), + ], + ] + + @staticmethod + def prediction_boxes_per_image(): + return [ # images + [ # image_1 boxes + (0.45, 0.2, 0.75, 0.85, "class_1", 0.92), + (0.5, 0.05, 0.8, 0.85, "class_1", 0.93), + (0.1, 0.15, 0.35, 0.75, "class_2", 0.92), + (0.05, 0.05, 0.35, 0.7, "class_2", 0.91), + ], + [ # image_2 boxes + (0.15, 0.05, 0.45, 0.8, "class_1", 0.89), + (0.15, 0.0, 0.5, 0.7, "class_1", 0.85), + (0.5, 0.0, 0.9, 0.5, "class_3", 0.95), + (0.45, 0.05, 0.95, 0.5, "class_3", 0.94), + ], + ] + + def f_measure_calculator(self): + return _FMeasureCalculator( + ground_truth_boxes_per_image=self.ground_truth_boxes_per_image(), + prediction_boxes_per_image=self.prediction_boxes_per_image(), + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_initialization(self): + """ + Description: + Check "_FMeasureCalculator" class object initialization + + Input data: + "_FMeasureCalculator" class object with specified "ground_truth_boxes_per_image" and + "prediction_boxes_per_image" parameters + + Expected results: + Test passes if attributes of initialized "_FMeasureCalculator" class object are equal to expected + """ + ground_truth_boxes_per_image = self.ground_truth_boxes_per_image() + prediction_boxes_per_image = self.prediction_boxes_per_image() + f_measure_calculator = _FMeasureCalculator( + ground_truth_boxes_per_image=ground_truth_boxes_per_image, + prediction_boxes_per_image=prediction_boxes_per_image, + ) + assert f_measure_calculator.ground_truth_boxes_per_image == ground_truth_boxes_per_image + assert f_measure_calculator.prediction_boxes_per_image == prediction_boxes_per_image + assert f_measure_calculator.confidence_range == [0.025, 1.0, 0.025] + assert f_measure_calculator.nms_range == [0.1, 1, 0.05] + assert f_measure_calculator.default_confidence_threshold == 0.35 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_get_counters(self): + """ + Description: + Check "_FMeasureCalculator" class "get_counters" method + + Input data: + "_FMeasureCalculator" class object with specified "ground_truth_boxes_per_image" and + "prediction_boxes_per_image" parameters + + Expected results: + Test passes if "_ResultCounters" object returned by "get_counters" method is equal to expected + + Steps + 1. Check value returned by "get_counters" method for "ground_truth_boxes" and "predicted_boxes" attributes with + length is more than 0 + 2. Check value returned by "get_counters" method for "predicted_boxes" attribute with length is more than 0 and + "ground_truth_boxes" attribute with length is equal to 0 + 3. Check value returned by "get_counters" method for "ground_truth_boxes" attribute with length is more than 0 + and "predicted_boxes" attribute with length is equal to 0 + """ + + def check_get_counters( + calculator: _FMeasureCalculator, + expected_n_false_negatives: int, + expected_n_predicted: int, + expected_n_true: int, + ): + get_counters = calculator.get_counters(0.75) + assert isinstance(get_counters, _ResultCounters) + assert get_counters.n_false_negatives == expected_n_false_negatives + assert get_counters.n_predicted == expected_n_predicted + assert get_counters.n_true == expected_n_true + + ground_boxes = self.ground_truth_boxes_per_image() + predicted_boxes = self.prediction_boxes_per_image() + # Checking value returned by "get_counters" for "ground_truth_boxes" and "predicted_boxes" with length is more + # than 0 + f_measure_calculator = _FMeasureCalculator( + ground_truth_boxes_per_image=ground_boxes, + prediction_boxes_per_image=predicted_boxes, + ) + check_get_counters( + f_measure_calculator, + expected_n_false_negatives=3, + expected_n_predicted=8, + expected_n_true=7, + ) + # Checking value returned by "get_counters" for "predicted_boxes" with length is more than 0 and + # "ground_truth_boxes" with length is equal to 0 + f_measure_calculator = _FMeasureCalculator( + ground_truth_boxes_per_image=[[]], + prediction_boxes_per_image=predicted_boxes, + ) + check_get_counters( + f_measure_calculator, + expected_n_false_negatives=0, + expected_n_predicted=4, + expected_n_true=0, + ) + # Checking value returned by "get_counters" for "ground_truth_boxes" with length is more than 0 and + # "predicted_boxes" with length is equal to 0 + f_measure_calculator = _FMeasureCalculator( + ground_truth_boxes_per_image=ground_boxes, prediction_boxes_per_image=[[]] + ) + check_get_counters( + f_measure_calculator, + expected_n_false_negatives=3, + expected_n_predicted=0, + expected_n_true=3, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_filter_confidence(self): + """ + Description: + Check "_FMeasureCalculator" class "__filter_confidence" method + + Input data: + "_FMeasureCalculator" class object, "boxes_per_image" list and "confidence_threshold" parameter + + Expected results: + Test passes if list returned by "__filter_confidence" method is equal to expected + + Steps + 1. Check value returned by "__filter_confidence" method for "confidence_threshold" equal to 0.0 + 2. Check value returned by "__filter_confidence" method for "confidence_threshold" equal to filter some boxes + 3. Check value returned by "__filter_confidence" method for "confidence_threshold" equal to 1.0 + """ + f_measure_calculator = self.f_measure_calculator() + boxes_per_image = f_measure_calculator.prediction_boxes_per_image + # Checking value returned by "__filter_confidence" for "confidence_threshold" equal to 0.0 + assert ( + f_measure_calculator._FMeasureCalculator__filter_confidence( # type: ignore[attr-defined] + boxes_per_image, 0.0 + ) + == boxes_per_image + ) + # Checking value returned by "__filter_confidence" for "confidence_threshold" equal to filter some boxes + assert f_measure_calculator._FMeasureCalculator__filter_confidence( # type: ignore[attr-defined] + boxes_per_image, 0.92 + ) == [ + [(0.5, 0.05, 0.8, 0.85, "class_1", 0.93)], + [ + (0.5, 0.0, 0.9, 0.5, "class_3", 0.95), + (0.45, 0.05, 0.95, 0.5, "class_3", 0.94), + ], + ] + assert f_measure_calculator._FMeasureCalculator__filter_confidence( # type: ignore[attr-defined] + boxes_per_image, 0.93 + ) == [ + [], + [ + (0.5, 0.0, 0.9, 0.5, "class_3", 0.95), + (0.45, 0.05, 0.95, 0.5, "class_3", 0.94), + ], + ] + # Checking value returned by "__filter_confidence" for "confidence_threshold" equal to 1.0 + assert f_measure_calculator._FMeasureCalculator__filter_confidence( # type: ignore[attr-defined] + boxes_per_image, 1.0 + ) == [ + [], + [], + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_filter_class(self): + """ + Description: + Check "_FMeasureCalculator" class "__filter_class" method + + Input data: + "_FMeasureCalculator" class object, "boxes_per_image" list and classes to filter + + Expected results: + Test passes if list returned by "__filter_class" method is equal to expected + + Steps + 1. Check list returned by "__filter_class" method to get class represented in one image + 2. Check list returned by "__filter_class" method to get class represented in several images + 3. Check list returned by "__filter_class" method to get class that is not represented in any of images + """ + f_measure_calculator = self.f_measure_calculator() + boxes_per_image = f_measure_calculator.prediction_boxes_per_image + # Checking list returned by "__filter_class" to get class represented in one image + assert f_measure_calculator._FMeasureCalculator__filter_class( # type: ignore[attr-defined] + boxes_per_image, "class_2" + ) == [ + [ + (0.1, 0.15, 0.35, 0.75, "class_2", 0.92), + (0.05, 0.05, 0.35, 0.7, "class_2", 0.91), + ], + [], + ] + # Checking list returned by "__filter_class" to get class represented in several images + assert f_measure_calculator._FMeasureCalculator__filter_class( # type: ignore[attr-defined] + boxes_per_image, "class_1" + ) == [ + [ + (0.45, 0.2, 0.75, 0.85, "class_1", 0.92), + (0.5, 0.05, 0.8, 0.85, "class_1", 0.93), + ], + [ + (0.15, 0.05, 0.45, 0.8, "class_1", 0.89), + (0.15, 0.0, 0.5, 0.7, "class_1", 0.85), + ], + ] + # Checking list returned by "__filter_class" to get class that is not represented in any of images + assert f_measure_calculator._FMeasureCalculator__filter_class( # type: ignore[attr-defined] + boxes_per_image, "class_6" + ) == [ + [], + [], + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_filter_nms(self): + """ + Description: + Check "_FMeasureCalculator" class "__filter_nms" method + + Input data: + "_FMeasureCalculator" class object, "boxes_per_image" list, "critical_nms" list and "nms_threshold" values + + Expected results: + Test passes if list returned by "__filter_nms" method is equal to expected + + Steps + 1. Check list returned by "__filter_nms" method for "nms_threshold" parameter that not filters any boxes + 2. Check list returned by "__filter_nms" method for "nms_threshold" that filters some boxes + 3. Check list returned by "__filter_nms" method for "nms_threshold" parameter that filters all boxes + """ + f_measure_calculator = self.f_measure_calculator() + boxes_per_image = f_measure_calculator.prediction_boxes_per_image + critical_nms = [[0.5, 0.55, 0.65, 0.6], [0.6, 0.55, 0.5, 0.65]] + # Checking list returned by "__filter_nms" for "nms_threshold" that not filters any boxes + assert ( + f_measure_calculator._FMeasureCalculator__filter_nms( # type: ignore[attr-defined] + boxes_per_image, critical_nms, 1.0 + ) + == boxes_per_image + ) + # Checking list returned by "__filter_nms" for "nms_threshold" that filters some boxes + assert f_measure_calculator._FMeasureCalculator__filter_nms( # type: ignore[attr-defined] + boxes_per_image, critical_nms, 0.6 + ) == [ + [ + (0.45, 0.2, 0.75, 0.85, "class_1", 0.92), + (0.5, 0.05, 0.8, 0.85, "class_1", 0.93), + ], + [ + (0.15, 0.0, 0.5, 0.7, "class_1", 0.85), + (0.5, 0.0, 0.9, 0.5, "class_3", 0.95), + ], + ] + # Checking list returned by "__filter_nms" for "nms_threshold" that filters all boxes + assert f_measure_calculator._FMeasureCalculator__filter_nms( # type: ignore[attr-defined] + boxes_per_image, critical_nms, 0.1 + ) == [ + [], + [], + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_critical_nms(self): + """ + Description: + Check "_FMeasureCalculator" class "__get_critical_nms" method + + Input data: + "_FMeasureCalculator" class object, "boxes_per_image" list and "cross_class_nms" bool value + + Expected results: + Test passes if list returned by "__get_critical_nms" method is equal to expected + + Steps + 1. Check list returned by "__get_critical_nms" method when "cross_class_nms" parameter is "False" + 2. Check list returned by "__get_critical_nms" method when "cross_class_nms" parameter is "True" + """ + f_measure_calculator = self.f_measure_calculator() + boxes_per_image = [ # images + [ # image_1 boxes + (0.1, 0.1, 0.4, 0.4, "class_1", 0.98), + (0.2, 0.2, 0.5, 0.5, "class_1", 0.97), + (0.6, 0.2, 0.9, 0.5, "class_2", 0.97), + (0.6, 0.3, 1.0, 0.6, "class_3", 0.98), + ], + [ # image_2 boxes + (0.1, 0.1, 0.4, 0.5, "class_2", 0.94), + (0.2, 0.2, 0.5, 0.6, "class_2", 0.95), + (0.6, 0.1, 0.9, 0.6, "class_3", 0.92), + (0.7, 0.1, 1.0, 0.6, "class_4", 0.94), + ], + ] + # Checking list returned by "__get_critical_nms" when "cross_class_nms" is "False" + assert f_measure_calculator._FMeasureCalculator__get_critical_nms( # type: ignore[attr-defined] + boxes_per_image, False + ) == [ + [0.0, 0.28571428571428575, 0.0, 0.0], + [0.3333333333333333, 0.0, 0.0, 0.0], + ] + # Checking list returned by "__get_critical_nms" when "cross_class_nms" is "True" + assert f_measure_calculator._FMeasureCalculator__get_critical_nms( # type: ignore[attr-defined] + boxes_per_image, True + ) == [ + [0.0, 0.28571428571428575, 0.4, 0.0], + [0.3333333333333333, 0.0, 0.5000000000000001, 0.0], + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_get_f_measure_for_class(self): + """ + Description: + Check "_FMeasureCalculator" class "get_f_measure_for_class" method + + Input data: + "_FMeasureCalculator" class object with specified "ground_truth_boxes_per_image" and + "prediction_boxes_per_image" parameters + + Expected results: + Test passes if tuple returned by "get_f_measure_for_class" method is equal to expected + + Steps + 1. Check tuple returned by "get_f_measure_for_class" method for class included in _FMeasureCalculator object + 2. Check tuple returned by "get_f_measure_for_class" method for class non-included in _FMeasureCalculator object + 3. Check tuple returned by "get_f_measure_for_class" method for _FMeasureCalculator object with + "ground_truth_boxes_per_image" parameter equal to empty list + """ + + def check_get_f_measure(actual_f_measure_for_class, expected_values_dict: dict): + # Checking _Metrics object + metrics = actual_f_measure_for_class[0] + assert isinstance(metrics, _Metrics) + assert metrics.f_measure == expected_values_dict.get("f_measure") + assert metrics.precision == expected_values_dict.get("precision") + assert metrics.recall == expected_values_dict.get("recall") + # Checking _ResultCounters object + result_counters = actual_f_measure_for_class[1] + assert isinstance(result_counters, _ResultCounters) + assert result_counters.n_false_negatives == expected_values_dict.get("n_false_negatives") + assert result_counters.n_predicted == expected_values_dict.get("n_predicted") + assert result_counters.n_true == expected_values_dict.get("n_true") + + f_measure_calculator = self.f_measure_calculator() + # Checking tuple returned by "get_f_measure_for_class" method for class included in _FMeasureCalculator object + check_get_f_measure( + actual_f_measure_for_class=f_measure_calculator.get_f_measure_for_class("class_1", 0.75, 0.9), + expected_values_dict={ + "f_measure": 0.3999999999999999, + "precision": 0.5, + "recall": 0.3333333333333333, + "n_false_negatives": 2, + "n_predicted": 2, + "n_true": 3, + }, + ) + # Checking tuple returned by "get_f_measure_for_class" method for non-included class in + # _FMeasureCalculator object + check_get_f_measure( + actual_f_measure_for_class=f_measure_calculator.get_f_measure_for_class("class_6", 0.75, 0.9), + expected_values_dict={ + "f_measure": 0.0, + "precision": 1.0, + "recall": 0.0, + "n_false_negatives": 0, + "n_predicted": 0, + "n_true": 0, + }, + ) + # Checking tuple returned by "get_f_measure_for_class" method for _FMeasureCalculator object with empty list + # of ground_truth_boxes_per_image + f_measure_calculator.ground_truth_boxes_per_image = [] + check_get_f_measure( + actual_f_measure_for_class=f_measure_calculator.get_f_measure_for_class("class_1", 0.75, 0.9), + expected_values_dict={ + "f_measure": 0.0, + "precision": 0.0, + "recall": 0.0, + "n_false_negatives": 0, + "n_predicted": 0, + "n_true": 0, + }, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_evaluate_classes(self): + """ + Description: + Check "_FMeasureCalculator" class "evaluate_classes" method + + Input data: + "_FMeasureCalculator" class object with specified "ground_truth_boxes_per_image" and + "prediction_boxes_per_image" parameters + + Expected results: + Test passes if dictionary returned by "evaluate_classes" method is equal to expected + + Steps + 1. Check dictionary returned by "evaluate_classes" method when one class is specified as "classes" parameter + 2. Check dictionary returned by "evaluate_classes" method when several classes specified as "classes" parameter + 3. Check dictionary returned by "evaluate_classes" method when "All classes" is specified in "classes" parameter + """ + + def compare_metrics(actual_metric: _Metrics, expected_metric: _Metrics): + assert actual_metric.f_measure == expected_metric.f_measure + assert actual_metric.recall == expected_metric.recall + assert actual_metric.precision == expected_metric.precision + + f_measure_calculator = self.f_measure_calculator() + # Checking dictionary returned by "evaluate_classes" when one class is specified as "classes" parameter + evaluate_classes = f_measure_calculator.evaluate_classes(["class_1"], 0.87, 0.8) + expected_class_1 = f_measure_calculator.get_f_measure_for_class("class_1", 0.87, 0.8)[0] + compare_metrics(evaluate_classes.get("class_1"), expected_class_1) + compare_metrics(evaluate_classes.get("All Classes"), expected_class_1) + # Checking dictionary returned by "evaluate_classes" when several classes specified as "classes" parameter + evaluate_classes = f_measure_calculator.evaluate_classes(["class_1", "class_2"], 0.87, 0.8) + expected_class_1 = f_measure_calculator.get_f_measure_for_class("class_1", 0.87, 0.8)[0] + expected_class_2 = f_measure_calculator.get_f_measure_for_class("class_2", 0.87, 0.8)[0] + expected_all_classes = _ResultCounters(n_false_negatives=4, n_predicted=6, n_true=5).calculate_f_measure() + compare_metrics(evaluate_classes.get("class_1"), expected_class_1) + compare_metrics(evaluate_classes.get("class_2"), expected_class_2) + compare_metrics(evaluate_classes.get("All Classes"), expected_all_classes) + # Checking dictionary returned by "evaluate_classes" when "All classes" is specified in "classes" parameter + evaluate_classes = f_measure_calculator.evaluate_classes(["class_1", "All Classes"], 0.87, 0.8) + compare_metrics(evaluate_classes.get("class_1"), expected_class_1) + compare_metrics(evaluate_classes.get("All Classes"), expected_class_1) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_get_results_per_nms(self): + """ + Description: + Check "_FMeasureCalculator" class "get_results_per_nms" method + + Input data: + "_FMeasureCalculator" class object with specified "ground_truth_boxes_per_image" and + "prediction_boxes_per_image" parameters + + Expected results: + Test passes if "_AggregatedResults" object returned by "get_results_per_nms" method is equal to expected + + Steps + 1. Check "_AggregatedResults" object returned by "get_results_per_nms" method when "cross_class_nms" parameter + is "False" and All Classes "f-measure" is less than "min_f_measure" parameter + 2. Check "_AggregatedResults" object returned by "get_results_per_nms" method when "cross_class_nms" parameter + is "True" and All Classes "f-measure" is more than "min_f_measure" parameter + """ + + def check_critical_nms_per_image( + calculator: _FMeasureCalculator, + actual_results: _AggregatedResults, + iou_threshold: float, + cross_class_nms: bool, + expected_best_f_measure: float, + expected_best_threshold: float, + ): + exp_results_per_nms = _AggregatedResults(["class_1", "class_2"]) + exp_critical_nms_per_image = calculator._FMeasureCalculator__get_critical_nms( # type: ignore[attr-defined] + calculator.prediction_boxes_per_image, cross_class_nms + ) + for nms_threshold in np.arange(*calculator.nms_range): + predict_boxes_per_image_nms = calculator._FMeasureCalculator__filter_nms( # type: ignore[attr-defined] + calculator.prediction_boxes_per_image, + exp_critical_nms_per_image, + nms_threshold, + ) + boxes_pair_for_nms = _FMeasureCalculator( + calculator.ground_truth_boxes_per_image, + predict_boxes_per_image_nms, + ) + result_point = boxes_pair_for_nms.evaluate_classes( + classes=["class_1", "class_2"], + iou_threshold=iou_threshold, + confidence_threshold=calculator.default_confidence_threshold, + ) + all_classes_f_measure = result_point["All Classes"].f_measure + exp_results_per_nms.all_classes_f_measure_curve.append(all_classes_f_measure) + + for class_name in ["class_1", "class_2"]: + exp_results_per_nms.f_measure_curve[class_name].append(result_point[class_name].f_measure) + exp_results_per_nms.precision_curve[class_name].append(result_point[class_name].precision) + exp_results_per_nms.recall_curve[class_name].append(result_point[class_name].recall) + assert actual_results.all_classes_f_measure_curve == exp_results_per_nms.all_classes_f_measure_curve + assert actual_results.f_measure_curve == exp_results_per_nms.f_measure_curve + assert actual_results.precision_curve == exp_results_per_nms.precision_curve + assert actual_results.recall_curve == exp_results_per_nms.recall_curve + assert actual_results.best_f_measure == expected_best_f_measure + assert actual_results.best_threshold == expected_best_threshold + + f_measure_calculator = self.f_measure_calculator() + # Checking "_AggregatedResults" object returned by "get_results_per_nms" when "cross_class_nms" is "False" and + # All Classes "f-measure" is more than "min_f_measure" + actual_results_per_nms = f_measure_calculator.get_results_per_nms( + classes=["class_1", "class_2"], iou_threshold=0.6, min_f_measure=0.5 + ) + check_critical_nms_per_image( + f_measure_calculator, + actual_results_per_nms, + iou_threshold=0.6, + cross_class_nms=False, + expected_best_f_measure=0.7499999999999998, + expected_best_threshold=0.5500000000000002, + ) + # Checking "_AggregatedResults" object returned by "get_results_per_nms" when "cross_class_nms" is "True" and + # All Classes "f-measure" is less than "min_f_measure" + actual_results_per_nms = f_measure_calculator.get_results_per_nms( + classes=["class_1", "class_2"], + iou_threshold=0.7, + min_f_measure=0.8, + cross_class_nms=True, + ) + check_critical_nms_per_image( + f_measure_calculator, + actual_results_per_nms, + iou_threshold=0.7, + cross_class_nms=True, + expected_best_f_measure=0.8, + expected_best_threshold=0.5, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_get_results_per_confidence(self): + """ + Description: + Check "_FMeasureCalculator" class "get_results_per_confidence" method + + Input data: + "_FMeasureCalculator" class object with specified "ground_truth_boxes_per_image" and + "prediction_boxes_per_image" parameters + + Expected results: + Test passes if "_AggregatedResults" object returned by "get_results_per_confidence" method is equal to expected + + Steps + 1. Check "_AggregatedResults" object returned by "get_results_per_confidence" method when All Classes f-measure + is more than best f-measure in results_per_confidence + 2. Check "_AggregatedResults" object returned by "get_results_per_confidence" method when All Classes f-measure + is less than best f-measure in results_per_confidence + """ + f_measure_calculator = self.f_measure_calculator() + # Check "_AggregatedResults" object returned by "get_results_per_confidence" when All Classes f-measure is more + # than best f-measure in results_per_confidence + expected_results_per_confidence = _AggregatedResults(["class_1", "class_2"]) + for confidence_threshold in np.arange(*[0.6, 0.9, 0.1]): + result_point = f_measure_calculator.evaluate_classes( + classes=["class_1", "class_2"], + iou_threshold=0.7, + confidence_threshold=confidence_threshold, + ) + all_classes_f_measure = result_point["All Classes"].f_measure + expected_results_per_confidence.all_classes_f_measure_curve.append(all_classes_f_measure) + + for class_name in ["class_1", "class_2"]: + expected_results_per_confidence.f_measure_curve[class_name].append(result_point[class_name].f_measure) + expected_results_per_confidence.precision_curve[class_name].append(result_point[class_name].precision) + expected_results_per_confidence.recall_curve[class_name].append(result_point[class_name].recall) + + actual_results_per_confidence = f_measure_calculator.get_results_per_confidence( + classes=["class_1", "class_2"], + confidence_range=[0.6, 0.9, 0.1], # arrange(0.6, 0.9, 0.1) + iou_threshold=0.7, + ) + assert actual_results_per_confidence.all_classes_f_measure_curve == ( + expected_results_per_confidence.all_classes_f_measure_curve + ) + assert actual_results_per_confidence.f_measure_curve == expected_results_per_confidence.f_measure_curve + assert actual_results_per_confidence.recall_curve == expected_results_per_confidence.recall_curve + assert actual_results_per_confidence.best_f_measure == 0.5454545454545453 + # 0.6 -> 0.54, 0.7 -> 0.54, 0.8 -> 0.54, 0.9 -> 0.44 + # Best ""LARGEST" trehshold should be 0.8 (considering numerical error) + assert abs(actual_results_per_confidence.best_threshold - 0.8) < 0.001 + # Check "_AggregatedResults" object returned by "get_results_per_confidence" when All Classes f-measure is less + # than best f-measure in results_per_confidence + actual_results_per_confidence = f_measure_calculator.get_results_per_confidence( + classes=["class_1", "class_2"], + confidence_range=[0.6, 0.9], + iou_threshold=1.0, + ) + assert actual_results_per_confidence.all_classes_f_measure_curve == [0.0] + assert actual_results_per_confidence.f_measure_curve == { + "class_1": [0.0], + "class_2": [0.0], + } + assert actual_results_per_confidence.recall_curve == { + "class_1": [0.0], + "class_2": [0.0], + } + assert actual_results_per_confidence.best_f_measure == 0.0 + assert actual_results_per_confidence.best_threshold == 0.1 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_calculator_evaluate_detections(self): + """ + Description: + Check "_FMeasureCalculator" class "evaluate_detections" method + + Input data: + "_FMeasureCalculator" class object, "boxes_per_image" list and "cross_class_nms" bool value + + Expected results: + Test passes if "_OverallResults" object returned by "evaluate_detections" method is equal to expected + + Steps + 1. Check "_OverallResults" object returned by "evaluate_detections" method with default optional parameters + 2. Check "_OverallResults" object returned by "evaluate_detections" method with specified optional parameters + """ + + def check_evaluate_detections( + calculator: _FMeasureCalculator, + evaluate_detection: _OverallResults, + iou_threshold: float = 0.5, + result_based_nms_threshold: bool = False, + cross_class_nms: bool = True, + ): + best_f_measure_per_class = {} + results_per_confidence = calculator.get_results_per_confidence( + classes=["class_1", "class_3"], + confidence_range=calculator.confidence_range, + iou_threshold=iou_threshold, + ) + best_f_measure = results_per_confidence.best_f_measure + for class_name in ["class_1", "class_3"]: + best_f_measure_per_class[class_name] = max(results_per_confidence.f_measure_curve[class_name]) + results_per_nms = None + if result_based_nms_threshold: + results_per_nms = calculator.get_results_per_nms( + classes=["class_1", "class_3"], + iou_threshold=iou_threshold, + min_f_measure=results_per_confidence.best_f_measure, + cross_class_nms=cross_class_nms, + ) + + for class_name in ["class_1", "class_3"]: + best_f_measure_per_class[class_name] = max(results_per_nms.f_measure_curve[class_name]) + expected_evaluate_detection = _OverallResults( + results_per_confidence, + results_per_nms, + best_f_measure_per_class, + best_f_measure, + ) + assert isinstance(evaluate_detection, _OverallResults) + assert evaluate_detection.best_f_measure == expected_evaluate_detection.best_f_measure + assert evaluate_detection.best_f_measure_per_class == expected_evaluate_detection.best_f_measure_per_class + assert evaluate_detection.per_confidence.all_classes_f_measure_curve == ( + expected_evaluate_detection.per_confidence.all_classes_f_measure_curve + ) + assert evaluate_detection.per_confidence.best_f_measure == ( + expected_evaluate_detection.per_confidence.best_f_measure + ) + assert evaluate_detection.per_confidence.best_threshold == ( + expected_evaluate_detection.per_confidence.best_threshold + ) + assert evaluate_detection.per_confidence.f_measure_curve == ( + expected_evaluate_detection.per_confidence.f_measure_curve + ) + assert evaluate_detection.per_confidence.precision_curve == ( + expected_evaluate_detection.per_confidence.precision_curve + ) + assert evaluate_detection.per_confidence.recall_curve == ( + expected_evaluate_detection.per_confidence.recall_curve + ) + + f_measure_calculator = self.f_measure_calculator() + # Checking "_OverallResults" object returned by "evaluate_detections" with default optional parameters + actual_evaluate_detection = f_measure_calculator.evaluate_detections(["class_1", "class_3"]) + check_evaluate_detections( + calculator=f_measure_calculator, + evaluate_detection=actual_evaluate_detection, + ) + # Checking "_OverallResults" object returned by "evaluate_detections" with specified optional parameters + actual_evaluate_detection = f_measure_calculator.evaluate_detections(["class_1", "class_3"], 0.79, True, True) + check_evaluate_detections( + calculator=f_measure_calculator, + evaluate_detection=actual_evaluate_detection, + iou_threshold=0.79, + result_based_nms_threshold=True, + cross_class_nms=True, + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestFMeasure: + color = Color(0, 255, 0) + creation_date = datetime.datetime(year=2021, month=12, day=23) + + @staticmethod + def generate_random_image() -> Image: + image = Image(data=np.random.randint(low=0, high=255, size=(10, 16, 3))) + return image + + def model_labels(self): + class_1_label = LabelEntity( + name="class_1", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + ) + class_2_label = LabelEntity( + name="class_2", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + ) + class_3_label = LabelEntity( + name="class_3", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + ) + return [class_1_label, class_2_label, class_3_label] + + def model(self): + configurable_params = ConfigurableParameters(header="Test model configurable params") + + model_label_group = LabelGroup(name="model_labels", labels=self.model_labels(), id=ID("model_label")) + + model_configuration = ModelConfiguration( + configurable_params, LabelSchemaEntity(label_groups=[model_label_group]) + ) + + model = ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + return model + + def roi(self): + return Annotation( + shape=Rectangle(x1=0.0, y1=0.0, x2=1.0, y2=1.0), + labels=[ + ScoredLabel( + LabelEntity( + name="image_roi", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("image_roi"), + ) + ) + ], + ) + + def image_1_ground_boxes(self): + box_1_annotation = Annotation( + shape=Rectangle(x1=0.5, y1=0.1, x2=0.8, y2=0.9), + labels=[ + ScoredLabel( + LabelEntity( + name="class_1", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_1_image_1"), + ), + probability=0.95, + ) + ], + ) + + box_2_annotation = Annotation( + shape=Rectangle(x1=0.1, y1=0.1, x2=0.3, y2=0.7), + labels=[ + ScoredLabel( + LabelEntity( + name="class_2", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_2_image_1"), + ), + probability=0.93, + ) + ], + ) + + box_3_annotation = Annotation( + shape=Rectangle(x1=0.05, y1=0.05, x2=0.3, y2=0.75), + labels=[ + ScoredLabel( + LabelEntity( + name="class_2", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_2_image_1"), + ), + probability=0.91, + ) + ], + ) + + annotation_scene = AnnotationSceneEntity( + annotations=[box_1_annotation, box_2_annotation, box_3_annotation], + kind=AnnotationSceneKind.ANNOTATION, + ) + + image_1 = DatasetItemEntity( + media=self.generate_random_image(), + annotation_scene=annotation_scene, + roi=self.roi(), + ) + return image_1 + + def image_2_ground_boxes(self): + box_1_annotation = Annotation( + shape=Rectangle(x1=0.15, y1=0.0, x2=0.4, y2=0.75), + labels=[ + ScoredLabel( + LabelEntity( + name="class_1", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_1_image_2"), + ), + probability=0.9, + ) + ], + ) + + box_2_annotation = Annotation( + shape=Rectangle(x1=0.1, y1=0.0, x2=0.45, y2=0.7), + labels=[ + ScoredLabel( + LabelEntity( + name="class_1", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_1_image_2"), + ), + probability=0.88, + ) + ], + ) + + box_3_annotation = Annotation( + shape=Ellipse(x1=0.45, y1=0.0, x2=0.95, y2=0.45), + labels=[ + ScoredLabel( + LabelEntity( + name="class_3", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_3_image_2"), + ), + probability=0.94, + ) + ], + ) + + box_4_annotation = Annotation( + shape=Ellipse(x1=0.45, y1=0.0, x2=1.0, y2=0.5), + labels=[ + ScoredLabel( + LabelEntity( + name="class_3", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_3_image_2"), + ), + probability=0.92, + ) + ], + ) + + annotation_scene = AnnotationSceneEntity( + annotations=[ + box_1_annotation, + box_2_annotation, + box_3_annotation, + box_4_annotation, + ], + kind=AnnotationSceneKind.ANNOTATION, + ) + + image_2 = DatasetItemEntity( + media=self.generate_random_image(), + annotation_scene=annotation_scene, + roi=self.roi(), + ) + return image_2 + + def image_1_prediction_boxes(self): + box_1_annotation = Annotation( + shape=Rectangle(x1=0.45, y1=0.2, x2=0.75, y2=0.85), + labels=[ + ScoredLabel( + LabelEntity( + name="class_1", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_1_image_1"), + ), + probability=0.92, + ) + ], + ) + + box_2_annotation = Annotation( + shape=Rectangle(x1=0.5, y1=0.05, x2=0.8, y2=0.85), + labels=[ + ScoredLabel( + LabelEntity( + name="class_1", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_1_image_1"), + ), + probability=0.93, + ) + ], + ) + + box_3_annotation = Annotation( + shape=Rectangle(x1=0.1, y1=0.15, x2=0.35, y2=0.75), + labels=[ + ScoredLabel( + LabelEntity( + name="class_2", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_2_image_1"), + ), + probability=0.92, + ) + ], + ) + + box_4_annotation = Annotation( + shape=Rectangle(x1=0.05, y1=0.05, x2=0.35, y2=0.7), + labels=[ + ScoredLabel( + LabelEntity( + name="class_2", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_2_image_1"), + ), + probability=0.91, + ) + ], + ) + + annotation_scene = AnnotationSceneEntity( + annotations=[ + box_1_annotation, + box_2_annotation, + box_3_annotation, + box_4_annotation, + ], + kind=AnnotationSceneKind.ANNOTATION, + ) + + image_1 = DatasetItemEntity( + media=self.generate_random_image(), + annotation_scene=annotation_scene, + roi=self.roi(), + ) + return image_1 + + def image_2_prediction_boxes(self): + box_1_annotation = Annotation( + shape=Rectangle(x1=0.15, y1=0.05, x2=0.45, y2=0.8), + labels=[ + ScoredLabel( + LabelEntity( + name="class_1", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_1_image_2"), + ), + probability=0.89, + ) + ], + ) + + box_2_annotation = Annotation( + shape=Rectangle(x1=0.15, y1=0.0, x2=0.5, y2=0.7), + labels=[ + ScoredLabel( + LabelEntity( + name="class_1", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_1_image_2"), + ), + probability=0.85, + ) + ], + ) + + box_3_annotation = Annotation( + shape=Ellipse(x1=0.5, y1=0.0, x2=0.9, y2=0.5), + labels=[ + ScoredLabel( + LabelEntity( + name="class_3", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_3_image_2"), + ), + probability=0.95, + ) + ], + ) + + box_4_annotation = Annotation( + shape=Ellipse(x1=0.45, y1=0.05, x2=0.95, y2=0.5), + labels=[ + ScoredLabel( + LabelEntity( + name="class_3", + domain=Domain.DETECTION, + color=self.color, + creation_date=self.creation_date, + id=ID("class_3_image_2"), + ), + probability=0.94, + ) + ], + ) + + annotation_scene = AnnotationSceneEntity( + annotations=[ + box_1_annotation, + box_2_annotation, + box_3_annotation, + box_4_annotation, + ], + kind=AnnotationSceneKind.ANNOTATION, + ) + + image_2 = DatasetItemEntity( + media=self.generate_random_image(), + annotation_scene=annotation_scene, + roi=self.roi(), + ) + return image_2 + + def ground_truth_dataset(self): + return DatasetEntity([self.image_1_ground_boxes(), self.image_2_ground_boxes()]) + + def prediction_dataset(self): + return DatasetEntity([self.image_1_prediction_boxes(), self.image_2_prediction_boxes()]) + + def incorrect_prediction_dataset(self): + return DatasetEntity([self.image_2_prediction_boxes(), self.image_2_prediction_boxes()]) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_initialization(self): + """ + Description: + Check "FMeasure" class object initialization + + Input data: + "FMeasure" class object with specified "ground_truth_boxes_per_image" and "prediction_boxes_per_image" + parameters + + Expected results: + Test passes if attributes of initialized "FMeasure" object are equal to expected + + Steps + 1. Check attributes of "FMeasure" class object initialized with default optional parameters + 2. Check attributes of "FMeasure" class object initialized with specified optional parameters + 3. Check ValueError exception is raised when empty list "ground_truth_dataset" is specified in "result_set" + parameter + 4. Check ValueError exception is raised when empty list "prediction_dataset" is specified in "result_set" + parameter + """ + + def check_f_measure_common_attributes(f_measure_actual): + assert f_measure_actual.box_class_index == 4 + assert f_measure_actual.box_score_index == 5 + assert isinstance(f_measure_actual.f_measure, ScoreMetric) + assert f_measure_actual.f_measure.name == "f-measure" + assert f_measure_actual.f_measure.value == pytest.approx(0.2857142857142856) + + ground_dataset = self.ground_truth_dataset() + prediction_dataset = self.prediction_dataset() + result_set = ResultSetEntity( + model=self.model(), + ground_truth_dataset=ground_dataset, + prediction_dataset=prediction_dataset, + ) + labels = self.model_labels() + # Checking attributes of "FMeasure" class object initialized with default optional parameters + f_measure = FMeasure(result_set) + check_f_measure_common_attributes(f_measure_actual=f_measure) + assert f_measure.f_measure_per_label == { + labels[0]: ScoreMetric(name="class_1", value=0.6666666666666665), + labels[2]: ScoreMetric(name="class_3", value=0.0), + labels[1]: ScoreMetric(name="class_2", value=0.0), + } + assert not f_measure.best_confidence_threshold + assert not f_measure.best_nms_threshold + assert not f_measure.f_measure_per_confidence + assert not f_measure.f_measure_per_nms + # Checking attributes of "FMeasure" class object initialized with specified optional parameters + f_measure = FMeasure( + resultset=result_set, + vary_confidence_threshold=True, + vary_nms_threshold=True, + cross_class_nms=True, + ) + check_f_measure_common_attributes(f_measure_actual=f_measure) + assert f_measure.f_measure_per_label == { + labels[0]: ScoreMetric(name="class_1", value=0.7999999999999999), + labels[1]: ScoreMetric(name="class_2", value=0.6666666666666665), + labels[2]: ScoreMetric(name="class_3", value=0.6666666666666665), + } + label_schema_labels = result_set.model.configuration.get_label_schema().get_labels(include_empty=False) + classes = [label.name for label in label_schema_labels] + boxes_pair = _FMeasureCalculator( + f_measure._FMeasure__get_boxes_from_dataset_as_list(ground_dataset, labels), # type: ignore[attr-defined] + f_measure._FMeasure__get_boxes_from_dataset_as_list( # type: ignore[attr-defined] + prediction_dataset, labels + ), + ) + result = boxes_pair.evaluate_detections( + result_based_nms_threshold=True, + classes=classes, + cross_class_nms=True, + ) + expected_f_measure_per_confidence = CurveMetric( + name="f-measure per confidence", + xs=list(np.arange(*boxes_pair.confidence_range)), + ys=result.per_confidence.all_classes_f_measure_curve, + ) + assert f_measure.f_measure_per_confidence.name == expected_f_measure_per_confidence.name + assert f_measure.f_measure_per_confidence.xs == expected_f_measure_per_confidence.xs + assert f_measure.f_measure_per_confidence.ys == expected_f_measure_per_confidence.ys + expected_f_measure_per_nms = CurveMetric( + name="f-measure per nms", + xs=list(np.arange(*boxes_pair.nms_range)), + ys=result.per_nms.all_classes_f_measure_curve, + ) + assert f_measure.f_measure_per_nms.name == expected_f_measure_per_nms.name + assert f_measure.f_measure_per_nms.xs == expected_f_measure_per_nms.xs + assert f_measure.f_measure_per_nms.ys == expected_f_measure_per_nms.ys + # Checking ValueError exception is raised when empty list "ground_truth_dataset" is specified in "result_set" + empty_dataset = DatasetEntity([]) + result_set = ResultSetEntity( + model=self.model(), + ground_truth_dataset=empty_dataset, + prediction_dataset=prediction_dataset, + ) + with pytest.raises(ValueError): + FMeasure(result_set) + # Checking ValueError exception is raised when empty list "prediction_dataset" is specified in "result_set" + result_set = ResultSetEntity( + model=self.model(), + ground_truth_dataset=ground_dataset, + prediction_dataset=empty_dataset, + ) + with pytest.raises(ValueError): + FMeasure(result_set) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_f_measure_get_performance(self): + """ + Description: + Check "FMeasure" class "get_performance" method + + Input data: + "FMeasure" class object with specified "ground_truth_boxes_per_image" and "prediction_boxes_per_image" + parameters + + Expected results: + Test passes if "Performance" object returned by "get_performance" method is equal to expected + + Steps + 1. Check "Performance" object returned by "get_performance" method for "FMeasure" object initialized with + default optional parameters + 2. Check "Performance" object returned by "get_performance" method for "FMeasure" object initialized with + specified optional parameters + """ + + def check_performance(performance, expected_score, expected_metric_groups): + assert isinstance(performance, Performance) + assert isinstance(performance.score, ScoreMetric) + assert performance.score.name == "f-measure" + assert performance.score.value == pytest.approx(expected_score) + # Checking dashboard metrics + for expected_metric_group in expected_metric_groups: + metric_group_index = expected_metric_groups.index(expected_metric_group) + actual_metric_group = performance.dashboard_metrics[metric_group_index] + if isinstance(expected_metric_group, BarMetricsGroup): + assert actual_metric_group.metrics == expected_metric_group.metrics + assert isinstance(actual_metric_group.visualization_info, BarChartInfo) + assert actual_metric_group.visualization_info.name == "F-measure per label" + assert actual_metric_group.visualization_info.palette == ColorPalette.LABEL + assert actual_metric_group.visualization_info.type == VisualizationType.RADIAL_BAR + if isinstance(expected_metric_group, LineMetricsGroup): + assert actual_metric_group.metrics == expected_metric_group.metrics + assert actual_metric_group.visualization_info.name == expected_metric_group.visualization_info.name + assert actual_metric_group.visualization_info.palette == ColorPalette.DEFAULT + assert ( + actual_metric_group.visualization_info.x_axis_label + == expected_metric_group.visualization_info.x_axis_label + ) + assert ( + actual_metric_group.visualization_info.y_axis_label + == expected_metric_group.visualization_info.y_axis_label + ) + if isinstance(expected_metric_group, TextMetricsGroup): + assert actual_metric_group.metrics == expected_metric_group.metrics + assert actual_metric_group.visualization_info.name == expected_metric_group.visualization_info.name + assert actual_metric_group.visualization_info.palette == ColorPalette.DEFAULT + assert actual_metric_group.visualization_info.type == VisualizationType.TEXT + + def generate_expected_default_dashboard_metric_groups( + actual_f_measure: FMeasure, + ): + return [ + BarMetricsGroup( + metrics=list(actual_f_measure.f_measure_per_label.values()), + visualization_info=BarChartInfo( + name="F-measure per label", + palette=ColorPalette.LABEL, + visualization_type=VisualizationType.RADIAL_BAR, + ), + ) + ] + + def generate_expected_optional_dashboard_metric_groups( + actual_f_measure: FMeasure, + ): + return [ + generate_expected_default_dashboard_metric_groups(actual_f_measure)[0], + LineMetricsGroup( + metrics=[cast(CurveMetric, actual_f_measure.f_measure_per_confidence)], + visualization_info=LineChartInfo( + name="F-measure per confidence", + x_axis_label="Confidence threshold", + y_axis_label="F-measure", + ), + ), + TextMetricsGroup( + metrics=[cast(ScoreMetric, actual_f_measure.best_confidence_threshold)], + visualization_info=TextChartInfo(name="Optimal confidence threshold"), + ), + LineMetricsGroup( + metrics=[cast(CurveMetric, actual_f_measure.f_measure_per_nms)], + visualization_info=LineChartInfo( + name="F-measure per nms", + x_axis_label="NMS threshold", + y_axis_label="F-measure", + ), + ), + TextMetricsGroup( + metrics=[cast(ScoreMetric, actual_f_measure.best_nms_threshold)], + visualization_info=TextChartInfo(name="Optimal nms threshold"), + ), + ] + + ground_dataset = self.ground_truth_dataset() + prediction_dataset = self.prediction_dataset() + result_set = ResultSetEntity( + model=self.model(), + ground_truth_dataset=ground_dataset, + prediction_dataset=prediction_dataset, + ) + # Checking "Performance" object returned by "get_performance" for "FMeasure" object initialized with default + # optional parameters + f_measure = FMeasure(result_set) + expected_dashboard_metric_groups = generate_expected_default_dashboard_metric_groups(f_measure) + actual_performance = f_measure.get_performance() + check_performance( + performance=actual_performance, + expected_score=0.2857142857142856, + expected_metric_groups=expected_dashboard_metric_groups, + ) + # Check for incorrect prediction dataset + incorrect_prediction_dataset = self.incorrect_prediction_dataset() + incorrect_result_set = ResultSetEntity( + model=self.model(), + ground_truth_dataset=ground_dataset, + prediction_dataset=incorrect_prediction_dataset, + ) + f_measure = FMeasure(incorrect_result_set) + expected_dashboard_metric_groups = generate_expected_default_dashboard_metric_groups(f_measure) + actual_performance = f_measure.get_performance() + check_performance( + performance=actual_performance, + expected_score=0.15384615384615372, + expected_metric_groups=expected_dashboard_metric_groups, + ) + # Checking attributes of "FMeasure" class object initialized with specified values of optional parameters + f_measure = FMeasure( + resultset=result_set, + vary_confidence_threshold=True, + vary_nms_threshold=True, + cross_class_nms=True, + ) + expected_dashboard_metric_groups = generate_expected_optional_dashboard_metric_groups(f_measure) + actual_performance = f_measure.get_performance() + check_performance( + performance=actual_performance, + expected_score=0.2857142857142856, + expected_metric_groups=expected_dashboard_metric_groups, + ) + # Check for incorrect prediction dataset + f_measure = FMeasure( + resultset=incorrect_result_set, + vary_confidence_threshold=True, + vary_nms_threshold=True, + cross_class_nms=True, + ) + expected_dashboard_metric_groups = generate_expected_optional_dashboard_metric_groups(f_measure) + actual_performance = f_measure.get_performance() + check_performance( + performance=actual_performance, + expected_score=0.15384615384615372, + expected_metric_groups=expected_dashboard_metric_groups, + ) diff --git a/tests/unit/api/usecases/exportable_code/test_prediction_to_annotation_converter.py b/tests/unit/api/usecases/exportable_code/test_prediction_to_annotation_converter.py new file mode 100644 index 00000000000..f48729b1775 --- /dev/null +++ b/tests/unit/api/usecases/exportable_code/test_prediction_to_annotation_converter.py @@ -0,0 +1,947 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from unittest.mock import patch + +import numpy as np +import pytest +from openvino.model_api.models.utils import ( + ClassificationResult, + Detection, + ImageResultWithSoftPrediction, +) + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.id import ID +from otx.api.entities.label import Color, Domain, LabelEntity +from otx.api.entities.label_schema import LabelGroup, LabelSchemaEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( + AnomalyClassificationToAnnotationConverter, + AnomalyDetectionToAnnotationConverter, + AnomalySegmentationToAnnotationConverter, + ClassificationToAnnotationConverter, + DetectionBoxToAnnotationConverter, + DetectionToAnnotationConverter, + IPredictionToAnnotationConverter, + SegmentationToAnnotationConverter, + create_converter, +) +from otx.api.utils.time_utils import now +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDetectionToAnnotationConverter: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_detection_to_annotation_convert(self): + """ + Description: + Check that DetectionToAnnotationConverter correctly converts Network output to list of Annotation + + Input data: + Array of network output with shape [4,6] + + Expected results: + Test passes if each Converted annotation has the same values as the network output + + Steps + 1. Create mock network output + 2. Convert network output to Annotation + 3. Check Annotations + """ + test_boxes = np.array( + ( + (0, 0.6, 0.1, 0.1, 0.2, 0.3), + (1, 0.2, 0.2, 0.1, 0.3, 0.4), + (1, 0.7, 0.3, 0.2, 0.5, 0.6), + (0, 0.1, 0.1, 0.1, 0.2, 0.3), + ) + ) + + labels = [ + LabelEntity("Zero", domain=Domain.DETECTION), + LabelEntity("One", domain=Domain.DETECTION), + ] + configuration = {"use_ellipse_shapes": False, "confidence_threshold": 0.01} + + converter = DetectionToAnnotationConverter(labels, configuration) + + annotation_scene = converter.convert_to_annotation(test_boxes) + + for i, annotation in enumerate(annotation_scene.annotations): + label: ScoredLabel = next(iter(annotation.get_labels())) + test_label = labels[int(test_boxes[i][0])] + assert test_label.name == label.name + + assert test_boxes[i][1], label.probability + + assert test_boxes[i][2] == annotation.shape.x1 + assert test_boxes[i][3] == annotation.shape.y1 + assert test_boxes[i][4] == annotation.shape.x2 + assert test_boxes[i][5] == annotation.shape.y2 + + annotation_scene = converter.convert_to_annotation(np.ndarray((0, 6))) + assert 0 == len(annotation_scene.shapes) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_detection_to_annotation_convert_openvino_shape(self): + """ + Description: + Check that DetectionToAnnotationConverter correctly converts OpenVINO Network output to annotations + + Input data: + Array of network output with shape [4,7] + + Expected results: + Test passes if each Converted annotation has the same values as the network output + + Steps + 1. Create mock network output + 2. Convert network output to Annotation + 3. Check Annotations + """ + test_boxes = np.array( + ( + (-12, 0, 0.6, 0.1, 0.1, 0.2, 0.3), + (12, 1, 0.2, 0.0, 0.1, 0.1, 0.2), + (1234, 1, 0.7, 0.2, 0.4, 0.7, 0.5), + (1251, 0, 0.1, 0.1, 0.1, 0.2, 0.3), + ) + ) + + labels = [ + LabelEntity("Zero", domain=Domain.DETECTION), + LabelEntity("One", domain=Domain.DETECTION), + ] + configuration = {"use_ellipse_shapes": False, "confidence_threshold": 0.01} + + converter = DetectionToAnnotationConverter(labels, configuration) + + annotation_scene = converter.convert_to_annotation(test_boxes) + + for i, annotation in enumerate(annotation_scene.annotations): + label: ScoredLabel = next(iter(annotation.get_labels())) + test_label = labels[int(test_boxes[i][1])] + assert test_label.name == label.name + + assert test_boxes[i][2] == label.probability + + assert test_boxes[i][3] == annotation.shape.x1 + assert test_boxes[i][4] == annotation.shape.y1 + assert test_boxes[i][5] == annotation.shape.x2 + assert test_boxes[i][6] == annotation.shape.y2 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_detection_to_annotation_convert_invalid_input(self): + """ + Description: + Check that DetectionToAnnotationConverter raises an error if invalid inputs are provided + + Input data: + Array of size [1203, 5] + Array of size [3, 8] + + Expected results: + Test passes a ValueError is raised for both inputs + + Steps + 1. Create DetectionToAnnotationConverter + 2. Attempt to convert array of [1203,5] to annotations + 3. Attempt to convert array of [3, 8] to annotations + """ + labels = [ + LabelEntity("Zero", domain=Domain.DETECTION), + LabelEntity("One", domain=Domain.DETECTION), + ] + configuration = {"use_ellipse_shapes": False, "confidence_threshold": 0.01} + converter = DetectionToAnnotationConverter(labels, configuration) + + with pytest.raises(ValueError): + converter.convert_to_annotation(np.ndarray((1203, 5))) + + with pytest.raises(ValueError): + converter.convert_to_annotation(np.ndarray((3, 8))) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIPredictionToAnnotation: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @patch( + ( + "otx.api.usecases.exportable_code.prediction_to_annotation_converter." + "IPredictionToAnnotationConverter.__abstractmethods__" + ), + set(), + ) + def test_i_prediction_to_annotation(self): + """ + Description: + Check "IPredictionToAnnotationConverter" class "convert_to_annotation" method + + Input data: + "IPredictionToAnnotationConverter" class object, "predictions" array, "metadata" dictionary + + Expected results: + Test passes if "NotImplementedError" exception is raised by "convert_to_annotation" method + """ + i_prediction_to_annotation_converter = IPredictionToAnnotationConverter() + with pytest.raises(NotImplementedError): + i_prediction_to_annotation_converter.convert_to_annotation( + predictions=np.random.randint(low=0, high=255, size=(5, 5, 3)), + metadata={}, + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestCreateConverter: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_create_converter(self): + """ + Description: + Check "create_converter" function + + Input data: + "converter_type" Domain-class object, "labels" LabelSchemaEntity-class object + + Expected results: + Test passes if "IPredictionToAnnotationConverter" object returned by "create_converter" function is equal + to expected + + Steps + 1. Check "DetectionBoxToAnnotationConverter" object returned by "create_converter" function when + "DETECTION" domain is specified as "converter_type" parameter + 2. Check "SegmentationToAnnotationConverter" object returned by "create_converter" function when + "SEGMENTATION" domain is specified as "converter_type" parameter + 3. Check "ClassificationToAnnotationConverter" object returned by "create_converter" function when + "CLASSIFICATION" domain is specified as "converter_type" parameter + 4. Check "AnomalyClassificationToAnnotationConverter" object returned by "create_converter" function when + "ANOMALY_CLASSIFICATION" domain is specified as "converter_type" parameter + 5. Check that "ValueError" exception is raised when "ANOMALY_DETECTION" or "ANOMALY_SEGMENTATION" domain is + specified as "converter_type" parameter + """ + # Checking "DetectionBoxToAnnotationConverter" returned by "create_converter" function when "DETECTION" is + # specified as "converter_type" + labels = [ + LabelEntity(name="Detection label", domain=Domain.DETECTION, id=ID("1")), + LabelEntity(name="Other Detection label", domain=Domain.DETECTION, id=ID("2")), + ] + label_group = LabelGroup(name="Detection labels group", labels=labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + configuration = {"use_ellipse_shapes": False, "confidence_threshold": 0.01} + converter = create_converter(converter_type=Domain.DETECTION, labels=label_schema, configuration=configuration) + assert isinstance(converter, DetectionToAnnotationConverter) + assert converter.labels == labels + # Checking "SegmentationToAnnotationConverter" returned by "create_converter" function when "SEGMENTATION"is + # specified as "converter_type" + labels = [ + LabelEntity(name="Segmentation label", domain=Domain.SEGMENTATION, id=ID("1")), + LabelEntity( + name="Other Segmentation label", + domain=Domain.SEGMENTATION, + id=ID("2"), + ), + ] + label_group = LabelGroup(name="Segmentation labels group", labels=labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = create_converter(converter_type=Domain.SEGMENTATION, labels=label_schema) + assert isinstance(converter, SegmentationToAnnotationConverter) + assert converter.label_map == {1: labels[0], 2: labels[1]} + # Checking "ClassificationToAnnotationConverter" returned by "create_converter" function when + # "CLASSIFICATION" is specified as "converter_type" + labels = [ + LabelEntity( + name="Classification label", + domain=Domain.CLASSIFICATION, + id=ID("1"), + ), + LabelEntity( + name="Other Classification label", + domain=Domain.CLASSIFICATION, + id=ID("2"), + ), + ] + label_group = LabelGroup(name="Classification labels group", labels=labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = create_converter(converter_type=Domain.CLASSIFICATION, labels=label_schema) + assert isinstance(converter, ClassificationToAnnotationConverter) + assert converter.labels == labels + # Checking that "AnomalyClassificationToAnnotationConverter" returned by "create_converter" function when + # "ANOMALY_CLASSIFICATION" is specified as "converter_type" + labels = [ + LabelEntity(name="Normal", domain=Domain.ANOMALY_CLASSIFICATION, id=ID("1")), + LabelEntity( + name="Anomalous", + domain=Domain.ANOMALY_CLASSIFICATION, + id=ID("2"), + is_anomalous=True, + ), + ] + label_group = LabelGroup(name="Anomaly classification labels group", labels=labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = create_converter(converter_type=Domain.ANOMALY_CLASSIFICATION, labels=label_schema) + assert isinstance(converter, AnomalyClassificationToAnnotationConverter) + assert converter.normal_label == labels[0] + assert converter.anomalous_label == labels[1] + # Checking that "AnomalyDetectionToAnnotationConverter" returned by "create_converter" function when + # "ANOMALY_DETECTION" is specified as "converter_type" + labels = [ + LabelEntity(name="Normal", domain=Domain.ANOMALY_DETECTION, id=ID("1")), + LabelEntity( + name="Anomalous", + domain=Domain.ANOMALY_DETECTION, + id=ID("2"), + is_anomalous=True, + ), + ] + label_group = LabelGroup(name="Anomaly detection labels group", labels=labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = create_converter(converter_type=Domain.ANOMALY_DETECTION, labels=label_schema) + assert isinstance(converter, AnomalyDetectionToAnnotationConverter) + assert converter.normal_label == labels[0] + assert converter.anomalous_label == labels[1] + # Checking that "AnomalySegmentationToAnnotationConverter" returned by "create_converter" function when + # "ANOMALY_SEGMENTATION" is specified as "converter_type" + labels = [ + LabelEntity(name="Normal", domain=Domain.ANOMALY_SEGMENTATION, id=ID("1")), + LabelEntity( + name="Anomalous", + domain=Domain.ANOMALY_SEGMENTATION, + id=ID("2"), + is_anomalous=True, + ), + ] + label_group = LabelGroup(name="Anomaly detection labels group", labels=labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = create_converter(converter_type=Domain.ANOMALY_SEGMENTATION, labels=label_schema) + assert isinstance(converter, AnomalySegmentationToAnnotationConverter) + assert converter.normal_label == labels[0] + assert converter.anomalous_label == labels[1] + + +def check_annotation_scene(annotation_scene: AnnotationSceneEntity, expected_length: int): + assert isinstance(annotation_scene, AnnotationSceneEntity) + assert annotation_scene.kind == AnnotationSceneKind.PREDICTION + assert len(annotation_scene.annotations) == expected_length + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDetectionBoxToAnnotation: + color = Color(red=180, green=230, blue=30) + creation_date = now() + labels = [ + LabelEntity( + name="Detection label", + domain=Domain.DETECTION, + color=color, + creation_date=creation_date, + id=ID("1"), + ), + LabelEntity( + name="Other Detection label", + domain=Domain.DETECTION, + color=color, + creation_date=creation_date, + id=ID("2"), + ), + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_detection_box_to_annotation_init(self): + """ + Description: + Check "DetectionBoxToAnnotationConverter" class object initialization + + Input data: + "DetectionBoxToAnnotationConverter" class object with specified "labels" parameter + + Expected results: + Test passes if attributes of initialized "DetectionBoxToAnnotationConverter" object are equal to expected + + Steps + 1. Check "labels" attribute of "DetectionBoxToAnnotationConverter" object initialized with non-empty labels + list + 2. Check "labels" attribute of "DetectionBoxToAnnotationConverter" object initialized with empty labels list + 3. Check "labels" attributes of "DetectionBoxToAnnotationConverter" object initialized with non-empty and + empty labels list + """ + # Checking "labels" of "DetectionBoxToAnnotationConverter" initialized with non-empty labels list + non_empty_labels = self.labels + label_group = LabelGroup(name="Detection labels group", labels=non_empty_labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = DetectionBoxToAnnotationConverter(labels=label_schema) + assert converter.labels == non_empty_labels + # Checking "labels" of "DetectionBoxToAnnotationConverter" initialized with empty labels list + empty_labels = [ + LabelEntity( + name="empty label", + domain=Domain.DETECTION, + is_empty=True, + id=ID("3"), + ), + LabelEntity( + name="other empty label", + domain=Domain.DETECTION, + is_empty=True, + id=ID("4"), + ), + ] + label_group = LabelGroup(name="Detection labels group", labels=empty_labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = DetectionBoxToAnnotationConverter(labels=label_schema) + assert converter.labels == [] + # Checking "labels" of "DetectionBoxToAnnotationConverter" initialized with non-empty and empty labels list + label_group = LabelGroup(name="Detection labels group", labels=non_empty_labels + empty_labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = DetectionBoxToAnnotationConverter(labels=label_schema) + assert converter.labels == non_empty_labels + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_detection_box_to_annotation_convert(self): + """ + Description: + Check "DetectionBoxToAnnotationConverter" class "convert_to_annotation" method + + Input data: + "DetectionBoxToAnnotationConverter" class object, "predictions" list with Detection-class objects, + "metadata" dictionary + + Expected results: + Test passes if "AnnotationSceneEntity" object returned by "convert_to_annotation" method is equal to + expected + """ + + def check_annotation( + actual_annotation: Annotation, + expected_label: LabelEntity, + expected_probability: float, + expected_x1: float, + expected_y1: float, + expected_x2: float, + expected_y2: float, + ): + assert isinstance(actual_annotation, Annotation) + assert actual_annotation.get_labels() == [ + ScoredLabel(label=expected_label, probability=expected_probability) + ] + assert isinstance(actual_annotation.shape, Rectangle) + assert actual_annotation.shape.x1 == pytest.approx(expected_x1) + assert actual_annotation.shape.y1 == pytest.approx(expected_y1) + assert actual_annotation.shape.x2 == pytest.approx(expected_x2) + assert actual_annotation.shape.y2 == pytest.approx(expected_y2) + + labels = self.labels + label_group = LabelGroup(name="Detection labels group", labels=labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = DetectionBoxToAnnotationConverter(labels=label_schema) + metadata = { + "non-required key": 1, + "other non-required key": 2, + "original_shape": [10, 20], + } + box_1 = Detection(xmin=2, ymin=2, xmax=4, ymax=6, score=0.8, id=0) + box_2 = Detection(xmin=6, ymin=4, xmax=10, ymax=9, score=0.9, id=1) + predictions = [box_1, box_2] + predictions_to_annotations = converter.convert_to_annotation(predictions=predictions, metadata=metadata) + check_annotation_scene(annotation_scene=predictions_to_annotations, expected_length=2) + check_annotation( + actual_annotation=predictions_to_annotations.annotations[0], + expected_label=labels[0], + expected_probability=0.8, + expected_x1=0.1, + expected_y1=0.2, + expected_x2=0.2, + expected_y2=0.6, + ) + check_annotation( + actual_annotation=predictions_to_annotations.annotations[1], + expected_label=labels[1], + expected_probability=0.9, + expected_x1=0.3, + expected_y1=0.4, + expected_x2=0.5, + expected_y2=0.9, + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestSegmentationToAnnotation: + color = Color(red=180, green=230, blue=30) + creation_date = now() + labels = [ + LabelEntity( + name="Segmentation label", + domain=Domain.SEGMENTATION, + color=color, + creation_date=creation_date, + id=ID("0"), + ), + LabelEntity( + name="Other Segmentation label", + domain=Domain.SEGMENTATION, + color=color, + creation_date=creation_date, + id=ID("1"), + ), + ] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_segmentation_to_annotation_init(self): + """ + Description: + Check "SegmentationToAnnotationConverter" class object initialization + + Input data: + "SegmentationToAnnotationConverter" class object with specified "label_schema" parameter + + Expected results: + Test passes if attributes of initialized "SegmentationToAnnotationConverter" object are equal to expected + + Steps + 1. Check "label_map" attribute of "SegmentationToAnnotationConverter" object initialized with non-empty + labels list + 2. Check "label_map" attribute of "SegmentationToAnnotationConverter" object initialized with empty labels + list + 3. Check "label_map" attributes of "SegmentationToAnnotationConverter" object initialized with non-empty and + empty labels list + """ + # Checking "label_map" of "SegmentationToAnnotationConverter" initialized with non-empty labels list + non_empty_labels = self.labels + label_group = LabelGroup(name="Segmentation labels group", labels=non_empty_labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = SegmentationToAnnotationConverter(label_schema=label_schema) + expected_non_empty_labels_map = { + 1: non_empty_labels[0], + 2: non_empty_labels[1], + } + assert converter.label_map == expected_non_empty_labels_map + # Checking "label_map" of "SegmentationToAnnotationConverter" initialized with empty labels list + empty_labels = [ + LabelEntity( + name="empty label", + domain=Domain.SEGMENTATION, + is_empty=True, + id=ID("3"), + ), + LabelEntity( + name="other empty label", + domain=Domain.SEGMENTATION, + is_empty=True, + id=ID("4"), + ), + ] + label_group = LabelGroup(name="Segmentation labels group", labels=empty_labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = SegmentationToAnnotationConverter(label_schema=label_schema) + assert converter.label_map == {} + # Checking "label_map" of "SegmentationToAnnotationConverter" initialized with non-empty and empty labels list + label_group = LabelGroup(name="Segmentation labels group", labels=non_empty_labels + empty_labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = SegmentationToAnnotationConverter(label_schema=label_schema) + assert converter.label_map == expected_non_empty_labels_map + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_segmentation_to_annotation_convert(self): + """ + Description: + Check "SegmentationToAnnotationConverter" class "convert_to_annotation" method + + Input data: + "SegmentationToAnnotationConverter" class object, "predictions" array with hard predictions, + "metadata" dictionary + + Expected results: + Test passes if "AnnotationSceneEntity" object returned by "convert_to_annotation" method is equal to + expected + """ + + def check_annotation( + actual_annotation: Annotation, + expected_label: LabelEntity, + expected_probability: float, + expected_points: list, + ): + assert isinstance(actual_annotation, Annotation) + annotation_labels = actual_annotation.get_labels() + # Checking Annotation ScoredLabel + assert len(annotation_labels) == 1 + assert isinstance(annotation_labels[0], ScoredLabel) + assert annotation_labels[0].label == expected_label + assert annotation_labels[0].probability == pytest.approx(expected_probability) + # Checking Annotation Shape + assert isinstance(actual_annotation.shape, Polygon) + assert actual_annotation.shape.points == expected_points + + labels = self.labels + label_group = LabelGroup(name="Segmentation labels group", labels=labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = SegmentationToAnnotationConverter(label_schema=label_schema) + soft_prediction = np.array( + [ + ( + [0.8, 0.1, 0.2], + [0.9, 0.1, 0.2], + [0.3, 0.2, 0.8], + [0.1, 0.2, 0.8], + ), + ( + [0.1, 0.8, 0.3], + [0.0, 0.8, 0.2], + [0.2, 0.1, 0.9], + [0.0, 0.2, 0.8], + ), + ( + [0.1, 0.7, 0.3], + [0.3, 0.8, 0.2], + [0.1, 0.2, 0.8], + [0.4, 0.3, 0.7], + ), + ( + [0.0, 1.0, 0.0], + [0.1, 0.9, 0.1], + [0.1, 0.1, 0.9], + [0.2, 0.2, 0.8], + ), + ] + ) + result = ImageResultWithSoftPrediction( + np.array([(0, 0, 2, 2), (1, 1, 2, 2), (1, 1, 2, 2), (1, 1, 2, 2)]), + soft_prediction, + np.array(0), + np.array(0), + ) + + metadata = { + "non-required key": 1, + "other non-required key": 2, + "soft_prediction": soft_prediction, + } + + predictions_to_annotations = converter.convert_to_annotation(predictions=result, metadata=metadata) + check_annotation_scene(annotation_scene=predictions_to_annotations, expected_length=2) + check_annotation( + actual_annotation=predictions_to_annotations.annotations[0], + expected_label=labels[0], + expected_probability=0.8333333333333333, + expected_points=[ + Point(0.0, 0.3333333333333333), + Point(0.0, 0.6666666666666666), + Point(0.0, 1.0), + Point(0.3333333333333333, 1.0), + Point(0.3333333333333333, 0.6666666666666666), + Point(0.3333333333333333, 0.3333333333333333), + ], + ) + check_annotation( + actual_annotation=predictions_to_annotations.annotations[1], + expected_label=labels[1], + expected_probability=0.8125, + expected_points=[ + Point(0.6666666666666666, 0.0), + Point(0.6666666666666666, 0.3333333333333333), + Point(0.6666666666666666, 0.6666666666666666), + Point(0.6666666666666666, 1.0), + Point(1.0, 1.0), + Point(1.0, 0.6666666666666666), + Point(1.0, 0.3333333333333333), + Point(1.0, 0.0), + ], + ) + + @pytest.mark.components(OtxSdkComponent.OTX_API) + class TestClassificationToAnnotation: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_classification_to_annotation_init(self): + """ + Description: + Check "ClassificationToAnnotationConverter" class object initialization + + Input data: + "ClassificationToAnnotationConverter" class object with specified "label_schema" parameter + + Expected results: + Test passes if attributes of initialized "ClassificationToAnnotationConverter" object are equal to + expected + + Steps + 1. Check attributes of "ClassificationToAnnotationConverter" object initialized with one label group + with non-empty labels list length more than 1 + 2. Check attributes of "ClassificationToAnnotationConverter" object initialized with one label group + with non-empty labels list length equal to 1 + 3. Check attributes of "ClassificationToAnnotationConverter" object initialized with two label groups + with one label in each + 4. Check attributes of "ClassificationToAnnotationConverter" object initialized with two label groups + with several labels in each + """ + label_0 = LabelEntity(name="label_0", domain=Domain.CLASSIFICATION, id=ID("0")) + label_0_1 = LabelEntity(name="label_0_1", domain=Domain.CLASSIFICATION, id=ID("0_1")) + label_0_2 = LabelEntity(name="label_0_2", domain=Domain.CLASSIFICATION, id=ID("0_2")) + label_0_1_1 = LabelEntity(name="label_0_1_1", domain=Domain.CLASSIFICATION, id=ID("0_1_1")) + + non_empty_labels = [label_0, label_0_1, label_0_1_1, label_0_2] + empty_labels = [ + LabelEntity( + name="empty label", + domain=Domain.CLASSIFICATION, + is_empty=True, + id=ID("3"), + ) + ] + # Checking attributes of "ClassificationToAnnotationConverter" initialized with one label group with + # non-empty labels list length more than 1 + label_group = LabelGroup( + name="Classification labels group", + labels=non_empty_labels + empty_labels, + ) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = ClassificationToAnnotationConverter(label_schema=label_schema) + assert converter.labels == non_empty_labels + assert converter.empty_label == empty_labels[0] + assert converter.label_schema == label_schema + assert not converter.hierarchical + # Checking attributes of "ClassificationToAnnotationConverter" initialized with one label group with + # non-empty labels list length equal to 1 + label_group = LabelGroup(name="Classification labels group", labels=[label_0] + empty_labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = ClassificationToAnnotationConverter(label_schema=label_schema) + assert converter.labels == [label_0] + empty_labels + assert converter.empty_label == empty_labels[0] + assert converter.label_schema == label_schema + assert not converter.hierarchical + # Checking attributes of "ClassificationToAnnotationConverter" initialized with two label groups with + # one label in each + label_group = LabelGroup(name="Classification labels group", labels=[label_0_1]) + other_label_group = LabelGroup(name="Other Classification labels group", labels=[label_0_2]) + label_schema = LabelSchemaEntity(label_groups=[label_group, other_label_group]) + converter = ClassificationToAnnotationConverter(label_schema=label_schema) + assert converter.labels == [label_0_1, label_0_2] + assert not converter.empty_label + assert converter.label_schema == label_schema + assert not converter.hierarchical + # Checking attributes of "ClassificationToAnnotationConverter" initialized with two label groups with + # several labels in each + other_non_empty_labels = [ + LabelEntity(name="label", domain=Domain.CLASSIFICATION, id=ID("3")), + LabelEntity(name="other label", domain=Domain.CLASSIFICATION, id=ID("4")), + ] + label_group = LabelGroup(name="Classification labels group", labels=non_empty_labels) + other_label_group = LabelGroup( + name="Other Classification labels group", + labels=other_non_empty_labels, + ) + label_schema = LabelSchemaEntity(label_groups=[label_group, other_label_group]) + converter = ClassificationToAnnotationConverter(label_schema=label_schema) + assert not converter.empty_label + assert converter.label_schema == label_schema + assert converter.hierarchical + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_classification_to_annotation_convert(self): + """ + Description: + Check "ClassificationToAnnotationConverter" class "convert_to_annotation" method + + Input data: + "ClassificationToAnnotationConverter" class object, "predictions" list, "metadata" dictionary + + Expected results: + Test passes if "AnnotationSceneEntity" object returned by "convert_to_annotation" method is equal to + expected + + Steps + 1. Check attributes of "AnnotationSceneEntity" object returned by "convert_to_annotation" method for + "ClassificationToAnnotationConverter" object initialized with label group with several non-empty labels + 2. Check attributes of "AnnotationSceneEntity" object returned by "convert_to_annotation" method with + "predictions" parameter equal to empty list + 3. Check attributes of "AnnotationSceneEntity" object returned by "convert_to_annotation" method for + "ClassificationToAnnotationConverter" object initialized with several LabelGroups + """ + + def check_annotation(actual_annotation: Annotation, expected_labels: list): + assert isinstance(actual_annotation, Annotation) + assert actual_annotation.get_labels(include_empty=True) == expected_labels + assert isinstance(actual_annotation.shape, Rectangle) + assert Rectangle.is_full_box(rectangle=actual_annotation.shape) + + label_0 = LabelEntity(name="label_0", domain=Domain.CLASSIFICATION, id=ID("0")) + label_0_1 = LabelEntity(name="label_0_1", domain=Domain.CLASSIFICATION, id=ID("0_1")) + label_0_2 = LabelEntity(name="label_0_2", domain=Domain.CLASSIFICATION, id=ID("0_2")) + label_0_1_1 = LabelEntity(name="label_0_1_1", domain=Domain.CLASSIFICATION, id=ID("0_1_1")) + non_empty_labels = [label_0, label_0_1, label_0_1_1, label_0_2] + empty_labels = [ + LabelEntity( + name="empty label", + domain=Domain.CLASSIFICATION, + is_empty=True, + id=ID("3"), + ) + ] + # Checking "AnnotationSceneEntity" returned by "convert_to_annotation" for + # "ClassificationToAnnotationConverter" initialized with label group with several non-empty labels + label_group = LabelGroup( + name="Classification labels group", + labels=non_empty_labels + empty_labels, + ) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + label_schema.add_child(parent=label_0, child=label_0_1) + label_schema.add_child(parent=label_0, child=label_0_2) + label_schema.add_child(parent=label_0_1, child=label_0_1_1) + converter = ClassificationToAnnotationConverter(label_schema=label_schema) + predictions = ClassificationResult([(0, 0.9), (1, 0.8), (2, 0.94), (3, 0.86)], None, None, None) + predictions_to_annotations = converter.convert_to_annotation(predictions) + check_annotation_scene(annotation_scene=predictions_to_annotations, expected_length=1) + check_annotation( + actual_annotation=predictions_to_annotations.annotations[0], + expected_labels=[ + ScoredLabel(label=label_0, probability=0.9), + ScoredLabel(label=label_0_1, probability=0.8), + ScoredLabel(label=label_0_1_1, probability=0.94), + ScoredLabel(label=label_0_2, probability=0.86), + ], + ) + # Checking attributes of "AnnotationSceneEntity" returned by "convert_to_annotation" method with + # "predictions" equal to empty list + converter = ClassificationToAnnotationConverter(label_schema=label_schema) + predictions = ClassificationResult([], None, None, None) + predictions_to_annotations = converter.convert_to_annotation(predictions) + check_annotation_scene(annotation_scene=predictions_to_annotations, expected_length=1) + check_annotation( + actual_annotation=predictions_to_annotations.annotations[0], + expected_labels=[ScoredLabel(label=empty_labels[0], probability=1.0)], + ) + # Checking attributes of "AnnotationSceneEntity" returned by "convert_to_annotation" for + # "ClassificationToAnnotationConverter" initialized with several LabelGroups + label_group = LabelGroup(name="Classification labels group", labels=[label_0_1_1]) + other_label_group = LabelGroup( + name="Other Classification labels group", + labels=[label_0_1, label_0_2], + ) + label_schema = LabelSchemaEntity(label_groups=[label_group, other_label_group]) + + label_schema.add_child(parent=label_0_1, child=label_0_1_1) + converter = ClassificationToAnnotationConverter(label_schema=label_schema) + predictions = ClassificationResult([(2, 0.9), (1, 0.8)], None, None, None) + predictions_to_annotations = converter.convert_to_annotation(predictions) + check_annotation_scene(annotation_scene=predictions_to_annotations, expected_length=1) + check_annotation( + predictions_to_annotations.annotations[0], + expected_labels=[ + ScoredLabel(label=label_0_2, probability=0.9), + ScoredLabel(label=label_0_1_1, probability=0.8), + ], + ) + + @pytest.mark.components(OtxSdkComponent.OTX_API) + class TestAnomalyClassificationToAnnotation: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_anomaly_classification_to_annotation_init( + self, + ): + """ + Description: + Check "AnomalyClassificationToAnnotationConverter" class initialization + + Input data: + "AnomalyClassificationToAnnotationConverter" class object with specified "label_schema" parameter + + Expected results: + Test passes if attributes of initialized "AnomalyClassificationToAnnotationConverter" object are equal + to expected + + Steps + 1. Check attributes of "AnomalyClassificationToAnnotationConverter" object initialized with non-empty + labels list + 2. Check attributes of "AnomalyClassificationToAnnotationConverter" object initialized with non-empty + and empty labels list + """ + # Checking attributes of "AnomalyClassificationToAnnotationConverter" initialized with non-empty labels + # list + non_empty_labels = [ + LabelEntity(name="Normal", domain=Domain.CLASSIFICATION, id=ID("1")), + LabelEntity(name="Normal", domain=Domain.CLASSIFICATION, id=ID("2")), + LabelEntity( + name="Anomalous", + domain=Domain.CLASSIFICATION, + id=ID("1"), + is_anomalous=True, + ), + LabelEntity( + name="Anomalous", + domain=Domain.CLASSIFICATION, + id=ID("2"), + is_anomalous=True, + ), + ] + label_group = LabelGroup(name="Classification labels group", labels=non_empty_labels) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = AnomalyClassificationToAnnotationConverter(label_schema=label_schema) + assert converter.normal_label == non_empty_labels[0] + assert converter.anomalous_label == non_empty_labels[2] + # Checking attributes of "AnomalyClassificationToAnnotationConverter" initialized with non-empty and + # empty labels list + empty_labels = [ + LabelEntity( + name="Normal", + domain=Domain.CLASSIFICATION, + is_empty=True, + id=ID("3"), + ), + LabelEntity( + name="Normal", + domain=Domain.CLASSIFICATION, + is_empty=True, + id=ID("4"), + ), + LabelEntity( + name="Anomalous", + domain=Domain.CLASSIFICATION, + is_empty=True, + id=ID("3"), + ), + LabelEntity( + name="Anomalous", + domain=Domain.CLASSIFICATION, + is_empty=True, + id=ID("4"), + ), + ] + label_group = LabelGroup( + name="Anomaly classification labels group", + labels=non_empty_labels + empty_labels, + ) + label_schema = LabelSchemaEntity(label_groups=[label_group]) + converter = AnomalyClassificationToAnnotationConverter(label_schema=label_schema) + assert converter.normal_label == non_empty_labels[0] + assert converter.anomalous_label == non_empty_labels[2] diff --git a/tests/unit/api/usecases/exportable_code/test_streamer.py b/tests/unit/api/usecases/exportable_code/test_streamer.py new file mode 100644 index 00000000000..b2d79888955 --- /dev/null +++ b/tests/unit/api/usecases/exportable_code/test_streamer.py @@ -0,0 +1,353 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import tempfile +from pathlib import Path +from time import sleep + +import pytest + +from otx.api.usecases.exportable_code.streamer import ( + CameraStreamer, + DirStreamer, + ImageStreamer, + OpenError, + ThreadedStreamer, + VideoStreamer, + get_streamer, +) +from tests.test_helpers import ( + generate_random_image_folder, + generate_random_single_image, + generate_random_single_video, + generate_random_video_folder, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestStreamer: + @staticmethod + def assert_streamer_element(streamer): + for element in streamer: + assert element.shape == (360, 480, 3) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_image_streamer_with_single_image(self): + """ + Description: + Test that ImageStreamer works correctly with a single image as input + + Input data: + Random image file + + Expected results: + Test passes if ImageStreamer returns a single image with the correct size + + Steps + 1. Create ImageStreamer + 2. Request image from streamer + """ + with generate_random_single_image(height=360, width=480) as path: + streamer = ImageStreamer(path) + self.assert_streamer_element(streamer) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_dir_streamer_with_folder(self): + """ + Description: + Test that DirStreamer works correctly with a folder of images as input + + Input data: + Folder with 10 random images + + Expected results: + Test passes if DirStreamer returns ten images with the correct size + + Steps + 1. Create DirStreamer + 2. Request images from streamer + """ + with generate_random_image_folder(height=360, width=480) as path: + streamer = DirStreamer(path) + self.assert_streamer_element(streamer) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_video_streamer_with_single_video(self): + """ + Description: + Test that VideoStreamer works correctly with a single video as input + + Input data: + Random Video file + + Expected results: + Test passes if VideoStreamer can read the Video file frame by frame + + Steps + 1. Create VideoStreamer + 2. Request frame from VideoStreamer + """ + with generate_random_single_video(height=360, width=480) as path: + streamer = VideoStreamer(path) + self.assert_streamer_element(streamer) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_video_streamer_with_loop_flag(self): + """ + Description: + Test that VideoStreamer works correctly with a loop flag + + Input data: + Random Video file + + Expected results: + Test passes if VideoStreamer returns frames with the correct amount of dimensions + after the end of the video + + Steps + 1. Create VideoStreamer + 2. Request frames from streamer + """ + with generate_random_single_video(height=360, width=480, number_of_frames=100) as path: + streamer = VideoStreamer(path, loop=True) + + for index, frame in enumerate(streamer): + assert frame.shape[-1] == 3 + if index > 200: + break + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_video_streamer_with_single_image(self): + """ + Description: + Test that VideoStreamer works correctly with a single image as input + + Input data: + Random image file + + Expected results: + Test passes if VideoStreamer can read the single frame + + Steps + 1. Create VideoStreamer + 2. Request frame from VideoStreamer + """ + with generate_random_single_video(height=360, width=480) as path: + streamer = VideoStreamer(path) + self.assert_streamer_element(streamer) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_invalid_inputs_to_get_streamer(self): + """ + Description: + Test that get_streamer does not allow invalid inputs + + Input data: + Invalid file + Empty directory + Folder with random videos + Name of file that does not exist + + Expected results: + Test passes if get_streamer raises a ValueError + + Steps + 1. Create invalid input file with .bin extension + 2. Attempt to call get_streamer with .bin file + 3. Attempt to call get_streamer with empty directory + 4. Attempt to call get_streamer with video folder, this raises because it is not supported + 5. Attempt to call get_streamer with a string that does not point to a file + """ + with tempfile.TemporaryDirectory() as temp_dir: + invalid_file = Path(temp_dir) / "not_valid.bin" + invalid_file.touch() + + with pytest.raises(Exception) as context: + get_streamer(str(invalid_file)) + + the_exception = context # .exception + assert "Can't open" in str(the_exception), str(the_exception) + + with tempfile.TemporaryDirectory() as empty_dir: + with pytest.raises(Exception): + get_streamer(empty_dir) + + with generate_random_video_folder() as path: + with pytest.raises(Exception): + get_streamer(path) + + with pytest.raises(Exception) as context: + get_streamer("not_a_file") + + the_exception = context # .exception + assert "Can't find" in str(the_exception), str(the_exception) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_valid_inputs_to_get_streamer(self): + """ + Description: + Test that get_streamer return the correct Streamer class for each input + + Input data: + A Video + An Image + Folder with images + + Expected results: + Test passes if each call to get_streamer return the correct Streamer instance + + Steps + 1. Call get_streamer with a video file + 2. Call get_streamer with an image file + 3. Call get_streamer with a folder of images + 4. Call get_streamer with a camera index + 5. Call get_streamer with the threaded argument + """ + with generate_random_single_video() as path: + streamer = get_streamer(path) + assert isinstance(streamer, VideoStreamer) + + with generate_random_single_image() as path: + streamer = get_streamer(path) + assert isinstance(streamer, ImageStreamer) + + with generate_random_image_folder() as path: + streamer = get_streamer(path) + assert isinstance(streamer, DirStreamer) + + streamer = get_streamer(0) + assert isinstance(streamer, CameraStreamer) + + streamer = get_streamer(input_stream=0, threaded=True) + assert isinstance(streamer, ThreadedStreamer) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_video_file_fails_on_image_streamer(self): + """ + Description: + Test that ImageStreamer raises an exception if a video is passed + + Input data: + Random Video file + + Expected results: + Test passes if a OpenError is raised + + Steps + 1. Attempt to create ImageStreamer + """ + with generate_random_single_video() as path: + with pytest.raises(OpenError): + ImageStreamer(path) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_camera_streamer(self): + """ + Description: + Check that CameraStreamer works correctly + + Input data: + CameraStreamer + + Expected results: + Test passes if CameraStreamer can read 10 frames from the camera + + Steps + 1. Create camera streamer + 2. Retrieve frames from camera streamer + """ + streamer = get_streamer() + n = 10 + + for frame in streamer: + assert frame.shape[-1] == 3 + n -= 1 + if n == 0: + break + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @pytest.mark.timeout(10) + @pytest.mark.xfail(reason="CVS-102619") + def test_threaded_streamer(self): + """ + Description: + Check that ThreadedStreamer works correctly + + Input data: + Folder with images + + Expected results: + Test passes if ThreadedStreamer reads 3 frames from the folder + + Steps + 1. Create ThreadedStreamer + 2. Retrieve frames from ThreadedStreamer + """ + with generate_random_image_folder() as path: + streamer = get_streamer(path, threaded=True) + frame_count = 0 + + for frame in streamer: + assert frame.shape[-1] == 3 + frame_count += 1 + + if frame_count == 3: + break + + assert frame_count == 3 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_threaded_streamer_timeout(self): + """ + Description: + Check that ThreadedStreamer works correctly even if the main thread is very slow + + Input data: + Folder with images + + Expected results: + Test passes if ThreadedStreamer returns 5 images + + Steps + 1. Create ThreadedStreamer + 2. Retrieve frames from camera streamer and suspend each thread in between frames + """ + with generate_random_image_folder() as path: + streamer = get_streamer(path, threaded=True) + + streamer.buffer_size = 2 + frame_count = 0 + + for frame in streamer: + assert frame.shape[-1] == 3 + sleep(1) + frame_count += 1 + if frame_count == 5: + break + + assert frame_count == 5 diff --git a/tests/unit/api/usecases/exportable_code/test_visualization.py b/tests/unit/api/usecases/exportable_code/test_visualization.py new file mode 100644 index 00000000000..677aca5d256 --- /dev/null +++ b/tests/unit/api/usecases/exportable_code/test_visualization.py @@ -0,0 +1,188 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import cv2 +import numpy as np +import pytest + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.color import Color +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.usecases.exportable_code.visualizers import Visualizer +from otx.api.utils.shape_drawer import ShapeDrawer +from otx.api.utils.time_utils import now +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestVisualizer: + image = np.random.randint(low=0, high=255, size=(480, 640, 3)).astype(np.float32) + + @staticmethod + def annotation_scene() -> AnnotationSceneEntity: + creation_date = now() + annotation_color = Color(red=30, green=180, blue=70) + other_annotation_color = Color(red=240, green=30, blue=40) + detection_label = LabelEntity( + name="detection label", + domain=Domain.DETECTION, + color=annotation_color, + creation_date=creation_date, + id=ID("detection_1"), + ) + segmentation_label = LabelEntity( + name="segmentation label", + domain=Domain.SEGMENTATION, + color=annotation_color, + creation_date=creation_date, + id=ID("segmentation_1"), + ) + annotation = Annotation( + shape=Rectangle(x1=0.1, y1=0.1, x2=0.4, y2=0.5), + labels=[ + ScoredLabel(detection_label, 0.9), + ScoredLabel(segmentation_label, 0.8), + ], + ) + + classification_label = LabelEntity( + name="classification label", + domain=Domain.CLASSIFICATION, + color=other_annotation_color, + creation_date=creation_date, + id=ID("classification_1"), + ) + anomaly_segmentation_label = LabelEntity( + name="anomaly_segmentation label", + domain=Domain.ANOMALY_SEGMENTATION, + color=other_annotation_color, + creation_date=creation_date, + id=ID("anomaly_segmentation_1"), + ) + other_annotation = Annotation( + shape=Ellipse(x1=0.6, y1=0.4, x2=0.7, y2=0.9), + labels=[ + ScoredLabel(classification_label, 0.75), + ScoredLabel(anomaly_segmentation_label, 0.9), + ], + ) + annotation_scene = AnnotationSceneEntity( + annotations=[annotation, other_annotation], + kind=AnnotationSceneKind.ANNOTATION, + ) + return annotation_scene + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_visualizer_initialization(self): + """ + Description: + Check "Visualizer" class object initialization + + Input data: + "Visualizer" object with specified attributes + + Expected results: + Test passes if attributes of initialized "Visualizer" object are equal to expected + + Steps + 1. Check attributes of "Visualizer" object initialized with default optional parameters + 2. Check attributes of "Visualizer" object initialized with specified optional parameters + """ + + def check_visualizer_attributes( + actual_visualizer: Visualizer, + expected_name: str, + expected_delay: int, + expected_show_count: bool, + expected_is_one_label: bool, + expected_no_show: bool, + ): + assert actual_visualizer.window_name == expected_name + assert actual_visualizer.delay == expected_delay + assert actual_visualizer.no_show == expected_no_show + assert isinstance(actual_visualizer.shape_drawer, ShapeDrawer) + assert actual_visualizer.shape_drawer.show_count == expected_show_count + assert actual_visualizer.shape_drawer.is_one_label == expected_is_one_label + + # Checking attributes of "Visualizer" initialized with default optional parameters + visualizer = Visualizer() + check_visualizer_attributes( + actual_visualizer=visualizer, + expected_name="Window", + expected_delay=1, + expected_show_count=False, + expected_is_one_label=False, + expected_no_show=False, + ) + # Checking attributes of "Visualizer" initialized with specified optional parameters + visualizer = Visualizer( + window_name="Test Visualizer", + show_count=True, + is_one_label=True, + no_show=True, + delay=5, + ) + check_visualizer_attributes( + actual_visualizer=visualizer, + expected_name="Test Visualizer", + expected_delay=5, + expected_show_count=True, + expected_is_one_label=True, + expected_no_show=True, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_visualizer_draw(self): + """ + Description: + Check "Visualizer" class "draw" method + + Input data: + "Visualizer" object with specified attributes, "image" array, "annotation" AnnotationSceneEntity-type object + + Expected results: + Test passes if array returned by "draw" method is equal to expected + """ + annotation_scene = self.annotation_scene() + image = self.image + expected_image = image.copy() + expected_image = cv2.cvtColor(expected_image, cv2.COLOR_RGB2BGR) + shape_drawer = ShapeDrawer(show_count=False, is_one_label=False) + expected_image = shape_drawer.draw(image=expected_image, entity=annotation_scene, labels=[]) + + actual_image = Visualizer().draw(image=image, annotation=annotation_scene) + assert np.array_equal(actual_image, expected_image) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_visualizer_no_show_mode(self): + """ + Description: + Check "Visualizer" class "no_show" parameter + + Input data: + "Visualizer" object with specified attributes + + Expected results: + Test passes if no exception is occured + """ + self.annotation_scene() + image = self.image + visualizer = Visualizer(no_show=True) + visualizer.show(image) + visualizer.is_quit() diff --git a/tests/unit/api/usecases/reporting/test_callback.py b/tests/unit/api/usecases/reporting/test_callback.py new file mode 100644 index 00000000000..2d6d0caade0 --- /dev/null +++ b/tests/unit/api/usecases/reporting/test_callback.py @@ -0,0 +1,76 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import pytest + +from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label_schema import LabelGroup, LabelSchemaEntity +from otx.api.entities.model import ModelConfiguration, ModelEntity +from otx.api.usecases.reporting.callback import Callback +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestCallback: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_callback_attributes(self): + """ + Description: + Check "Callback" class object initialization + + Input data: + "Callback" class object + + Expected results: + Test passes if "params" and "model" attributes of initialized "Callback" class object are equal to expected + + Steps + 1. Check "model" attribute of "Callback" object after "set_model" method + 2. Check "params" attribute of "Callback" object after "set_params" method + """ + callback = Callback() + # Checking "params" of "Callback" object after "set_params" + params = {"parameter_1": 1, "parameter_2": 4, "parameter_3": 9} + callback.set_params(params) + assert callback.params == params + # Checking "model" of "Callback" after "set_model" + configurable_params = ConfigurableParameters(header="Test model configurable params") + labels_group = LabelGroup(name="model_group", labels=[]) + model_configuration = ModelConfiguration(configurable_params, LabelSchemaEntity(label_groups=[labels_group])) + model = ModelEntity(train_dataset=DatasetEntity(), configuration=model_configuration) + callback.set_model(model) + assert callback.model == model + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_callback_abstract_methods(self): + """ + Description: + Check "Callback" class object abstract methods + + Input data: + "Callback" class object + + Expected results: + Test passes if none of "Callback" class abstract methods raised an exception + """ + callback = Callback() + callback.on_epoch_begin(epoch=1, logs="on_epoch_begin logs") + callback.on_epoch_end(epoch=1, logs="on_epoch_end logs") + callback.on_batch_begin(batch=1, logs="on_batch_begin logs") + callback.on_batch_end(batch=1, logs="on_batch_end logs") + callback.on_train_begin(logs="on_train_begin logs") + callback.on_train_end(logs="on_train_end logs") + callback.on_train_batch_begin(batch=1, logs="on_train_batch_begin logs") + callback.on_train_batch_end(batch=1, logs="on_train_batch_end logs") + callback.on_test_begin("on_test_begin logs") + callback.on_test_end("on_epoch_begin logs") + callback.on_test_batch_begin(batch=1, logs="on_test_batch_begin logs") + callback.on_test_batch_end(batch=1, logs="on_epoch_begin logs") diff --git a/tests/unit/api/usecases/reporting/test_time_monitor_callback.py b/tests/unit/api/usecases/reporting/test_time_monitor_callback.py new file mode 100644 index 00000000000..7c067246c3a --- /dev/null +++ b/tests/unit/api/usecases/reporting/test_time_monitor_callback.py @@ -0,0 +1,449 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from time import time + +import pytest + +from otx.api.entities.train_parameters import default_progress_callback +from otx.api.usecases.reporting.time_monitor_callback import TimeMonitorCallback +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTimeMonitorCallback: + @staticmethod + def time_monitor_callback(): + return TimeMonitorCallback( + num_epoch=3, + num_train_steps=4, + num_val_steps=5, + num_test_steps=6, + update_progress_callback=default_progress_callback, + ) + + @staticmethod + def check_current_step_start_step_time_attributes( + callback: TimeMonitorCallback, + expected_step: int, + expected_step_time_before: float, + ): + assert callback.current_step == expected_step + assert callback.start_step_time > expected_step_time_before + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_initialization(self): + """ + Description: + Check "TimeMonitorCallback" class object initialization + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if attributes of initialized "TimeMonitorCallback" class object are equal to expected + + Steps + 1. Check attributes of "TimeMonitorCallback" class object initialized with default optional parameters + 2. Check attributes of "TimeMonitorCallback" class object initialized with specified optional parameters + """ + + def check_time_monitor_callback_attributes( + actual_time_monitor_callback: TimeMonitorCallback, + expected_step_history: int, + expected_epoch_history: int, + expected_update_progress_callback, + ): + assert actual_time_monitor_callback.total_epochs == 3 + assert actual_time_monitor_callback.train_steps == 4 + assert actual_time_monitor_callback.val_steps == 5 + assert actual_time_monitor_callback.test_steps == 6 + assert actual_time_monitor_callback.steps_per_epoch == 9 # train_steps + val_steps + assert actual_time_monitor_callback.total_steps == 33 # steps_per_epoch * total_epochs + num_test_steps + assert actual_time_monitor_callback.current_step == 0 + assert actual_time_monitor_callback.current_epoch == 0 + assert actual_time_monitor_callback.start_step_time # calculated using time.time() method + assert actual_time_monitor_callback.past_step_duration == [] + assert actual_time_monitor_callback.average_step == 0 + assert actual_time_monitor_callback.step_history == expected_step_history + assert actual_time_monitor_callback.start_epoch_time # calculated using time.time() method + assert actual_time_monitor_callback.past_epoch_duration == [] + assert actual_time_monitor_callback.average_epoch == 0 + assert actual_time_monitor_callback.epoch_history == expected_epoch_history + assert not actual_time_monitor_callback.is_training + assert actual_time_monitor_callback.update_progress_callback == expected_update_progress_callback + + # Checking attributes of "TimeMonitorCallback" initialized with default optional parameters + num_epoch = 3 + num_train_steps = 4 + num_val_steps = 5 + num_test_steps = 6 + + time_monitor_callback = TimeMonitorCallback( + num_epoch=num_epoch, + num_train_steps=num_train_steps, + num_val_steps=num_val_steps, + num_test_steps=num_test_steps, + ) + check_time_monitor_callback_attributes( + actual_time_monitor_callback=time_monitor_callback, + expected_epoch_history=5, + expected_step_history=50, + expected_update_progress_callback=default_progress_callback, + ) + # Checking attributes of "TimeMonitorCallback" initialized with specified optional parameters + step_history = 10 + epoch_history = 100 + callback = default_progress_callback + time_monitor_callback = TimeMonitorCallback( + num_epoch=num_epoch, + num_train_steps=num_train_steps, + num_val_steps=num_val_steps, + num_test_steps=num_test_steps, + step_history=step_history, + epoch_history=epoch_history, + update_progress_callback=callback, + ) + check_time_monitor_callback_attributes( + actual_time_monitor_callback=time_monitor_callback, + expected_step_history=step_history, + expected_epoch_history=epoch_history, + expected_update_progress_callback=callback, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_on_train_batch_begin(self): + """ + Description: + Check "TimeMonitorCallback" class object "on_train_batch_begin" method + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if "current_step" and "start_step_time" attributes of "TimeMonitorCallback" class object after + "on_train_batch_begin" method are equal to expected + """ + + time_monitor_callback = self.time_monitor_callback() + start_step_time_before = time() - 1 + time_monitor_callback.start_step_time = start_step_time_before + time_monitor_callback.on_train_batch_begin(batch=1, logs="on_train_batch_begin logs") + self.check_current_step_start_step_time_attributes( + callback=time_monitor_callback, + expected_step=1, + expected_step_time_before=start_step_time_before, + ) + time_monitor_callback.start_step_time = start_step_time_before + time_monitor_callback.on_train_batch_begin(batch=2, logs="on_train_batch_begin logs") + self.check_current_step_start_step_time_attributes( + callback=time_monitor_callback, + expected_step=2, + expected_step_time_before=start_step_time_before, + ) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_on_train_batch_end(self): + """ + Description: + Check "TimeMonitorCallback" class object "on_train_batch_end" method + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if "past_step_duration" and "average_step" attributes of "TimeMonitorCallback" class object after + "on_train_batch_end" method are equal to expected + + Steps + 1. Check "past_step_duration" and "average_step" attributes after "on_train_batch_end" method for + "TimeMonitorCallback" class object with length of "past_step_duration" attribute more than value of + "step_history" attribute + 2. Check "past_step_duration" and "average_step" attributes after "on_train_batch_end" method for + "TimeMonitorCallback" class object with length of "past_step_duration" attribute less than value of + "step_history" attribute + """ + # Checking "past_step_duration" and "average_step" after "on_train_batch_end" for "TimeMonitorCallback" with + # length of "past_step_duration" more than "step_history" value + time_monitor_callback = self.time_monitor_callback() + time_monitor_callback.past_step_duration = [10.0, 12.0] + time_monitor_callback.step_history = 2 + time_monitor_callback.on_train_batch_end(batch=1, logs="on_train_batch_end logs") + assert len(time_monitor_callback.past_step_duration) == 2 + assert round(time_monitor_callback.average_step, 4) == 6.0 + # Checking "past_step_duration" and "average_step" after "on_train_batch_end" for "TimeMonitorCallback" with + # length of "past_step_duration" less than "step_history" value + time_monitor_callback = self.time_monitor_callback() + time_monitor_callback.past_step_duration = [10, 14] + time_monitor_callback.step_history = 4 + time_monitor_callback.on_train_batch_end(batch=2, logs="on_train_batch_end logs") + assert len(time_monitor_callback.past_step_duration) == 3 + assert round(time_monitor_callback.average_step, 4) == 8.0 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_is_stalling(self): + """ + Description: + Check "TimeMonitorCallback" class object "is_stalling" method + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if bool value returned by "is_stalling" method is equal to expected + + Steps + 1. Check value returned by "is_stalling" method for "TimeMonitorCallback" object with "is_training" attribute + is "True" and "current_step" more than 2 + 2. Check value returned by "is_stalling" method for "TimeMonitorCallback" object with "is_training" attribute + is "True" and "current_step" equal to 2 + 3. Check value returned by "is_stalling" method for "TimeMonitorCallback" object with "is_training" attribute + is "False" and "current_step" more than 2 + """ + time_monitor_callback = self.time_monitor_callback() + # Checking value returned by "is_stalling" for "TimeMonitorCallback" with "is_training" is "True" and + # "current_step" more than 2 + time_monitor_callback.is_training = True + time_monitor_callback.current_step = 3 + time_monitor_callback.start_step_time = 40 + assert time_monitor_callback.is_stalling() + # Checking value returned by "is_stalling" for "TimeMonitorCallback" with "is_training" is "True" and + # "current_step" equal to 2 + time_monitor_callback.current_step = 2 + assert not time_monitor_callback.is_stalling() + # Checking value returned by "is_stalling" for "TimeMonitorCallback" with "is_training" is "False" and + # "current_step" more than 2 + time_monitor_callback.current_step = 3 + time_monitor_callback.is_training = False + assert not time_monitor_callback.is_stalling() + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_on_test_batch_begin(self): + """ + Description: + Check "TimeMonitorCallback" class object "on_test_batch_begin" method + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if "current_step" and "start_step_time" attributes of "TimeMonitorCallback" class object after + "on_test_batch_begin" method are equal to expected + """ + time_monitor_callback = self.time_monitor_callback() + start_step_time_before = time() - 1 + time_monitor_callback.start_step_time = start_step_time_before + time_monitor_callback.on_test_batch_begin(batch=1, logs="on_test_batch_begin logs") + self.check_current_step_start_step_time_attributes(time_monitor_callback, 1, start_step_time_before) + time_monitor_callback.start_step_time = start_step_time_before + time_monitor_callback.on_test_batch_begin(batch=2, logs="on_test_batch_begin logs") + self.check_current_step_start_step_time_attributes(time_monitor_callback, 2, start_step_time_before) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_on_test_batch_end(self): + """ + Description: + Check "TimeMonitorCallback" class object "on_test_batch_end" method + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if "current_step" and "start_step_time" attributes of "TimeMonitorCallback" class object after + "on_test_batch_end" method are equal to expected + + Steps + 1. Check "past_step_duration" and "average_step" attributes after "on_test_batch_end" method for + "TimeMonitorCallback" class object with length of "past_step_duration" attribute more than value of + "step_history" attribute + 2. Check "past_step_duration" and "average_step" attributes after "on_test_batch_end" method for + "TimeMonitorCallback" class object with length of "past_step_duration" attribute less than value of + "step_history" attribute + """ + # Checking "past_step_duration" and "average_step" after "on_train_batch_end" for "TimeMonitorCallback" with + # length of "past_step_duration" more than "step_history" value + time_monitor_callback = self.time_monitor_callback() + time_monitor_callback.past_step_duration = [5.0, 6.0] + time_monitor_callback.step_history = 2 + time_monitor_callback.on_test_batch_end(batch=1, logs="on_test_batch_end logs") + assert len(time_monitor_callback.past_step_duration) == 2 + assert round(time_monitor_callback.average_step, 4) == 3.0 + # Checking "past_step_duration" and "average_step" after "on_train_batch_end" for "TimeMonitorCallback" with + # length of "past_step_duration" less than "step_history" value + time_monitor_callback = self.time_monitor_callback() + time_monitor_callback.past_step_duration = [10.0, 14.0, 16.0] + time_monitor_callback.step_history = 5 + time_monitor_callback.on_test_batch_end(batch=2, logs="on_test_batch_end logs") + assert len(time_monitor_callback.past_step_duration) == 4 + assert round(time_monitor_callback.average_step, 4) == 10.0 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_on_train_begin(self): + """ + Description: + Check "TimeMonitorCallback" class object "on_train_begin" method + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if "is_training" attribute of "TimeMonitorCallback" class object after "on_train_begin" method is + "True" + + Steps + 1. Check "is_training" attribute after "on_train_begin" method for "TimeMonitorCallback" class object with + "is_training" attribute is "False" + 2. Check "is_training" attribute after "on_train_begin" method for "TimeMonitorCallback" class object with + "is_training" attribute is "True" + """ + # Checking "is_training" after "on_train_begin" for "TimeMonitorCallback" with "is_training" is "False" + time_monitor_callback = self.time_monitor_callback() + time_monitor_callback.on_train_begin("on_train_begin logs") + assert time_monitor_callback.is_training + # Checking "is_training" after "on_train_begin" for "TimeMonitorCallback" with "is_training" is "True" + time_monitor_callback.on_train_begin("on_train_begin logs") + assert time_monitor_callback.is_training + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_on_train_end(self): + """ + Description: + Check "TimeMonitorCallback" class object "on_train_end" method + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if "current_step", "current_epoch" and "is_training" attributes of "TimeMonitorCallback" class + object after "on_train_end" method are equal to expected + + Steps + 1. Check "current_step", "current_epoch" and "is_training" attributes after "on_train_end" method for + "TimeMonitorCallback" class object with "is_training" attribute is "True" + 2. Check "is_training" attribute after "on_train_begin" method for "TimeMonitorCallback" class object with + "is_training" attribute is "False" + """ + + def check_attributes_after_on_train_end(callback: TimeMonitorCallback): + assert callback.current_step == 27 # total_steps - test_steps + + assert callback.current_epoch == 3 # total_epochs + assert not callback.is_training + + # Checking "current_step", "current_epoch" and "is_training" after "on_train_end" for "TimeMonitorCallback" with + # "is_training": "True" + time_monitor_callback = self.time_monitor_callback() + time_monitor_callback.on_train_begin("on_train_begin logs") # setting "is_training" to "True" + time_monitor_callback.on_train_end("on_train_end logs") + check_attributes_after_on_train_end(time_monitor_callback) + # Checking "current_step", "current_epoch" and "is_training" after "on_train_end" for "TimeMonitorCallback" with + # "is_training": "False" + time_monitor_callback = self.time_monitor_callback() + time_monitor_callback.on_train_end("on_train_end logs") + check_attributes_after_on_train_end(time_monitor_callback) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_on_epoch_begin(self): + """ + Description: + Check "TimeMonitorCallback" class object "on_epoch_begin" method + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if "current_epoch" and "start_epoch_time" attributes of "TimeMonitorCallback" class object after + "on_epoch_begin" method are equal to expected + """ + start_epoch_time = time() - 1 + time_monitor_callback = self.time_monitor_callback() + time_monitor_callback.start_epoch_time = start_epoch_time + time_monitor_callback.on_epoch_begin(epoch=0, logs="on_epoch_begin logs") + assert time_monitor_callback.current_epoch == 1 + assert time_monitor_callback.start_epoch_time > start_epoch_time + time_monitor_callback.start_epoch_time = start_epoch_time + time_monitor_callback.on_epoch_begin(epoch=2, logs="on_epoch_begin logs") + assert time_monitor_callback.current_epoch == 3 + assert time_monitor_callback.start_epoch_time > start_epoch_time + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_on_epoch_end(self): + """ + Description: + Check "TimeMonitorCallback" class object "on_epoch_end" method + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if "past_epoch_duration" and "average_epoch" attributes of "TimeMonitorCallback" class object after + "on_epoch_end" method are equal to expected + + Steps + 1. Check "past_epoch_duration" and "average_epoch" attributes after "on_test_batch_end" method for + "TimeMonitorCallback" class object with length of "past_epoch_duration" attribute more than value of + "epoch_history" attribute + 2. Check "past_epoch_duration" and "average_epoch" attributes after "on_test_batch_end" method for + "TimeMonitorCallback" class object with length of "past_epoch_duration" attribute less than value of + "epoch_history" attribute + """ + # Checking "past_epoch_duration" and "average_epoch" after "on_test_batch_end" for "TimeMonitorCallback" with + # length of "past_epoch_duration" more than "epoch_history" value + time_monitor_callback = self.time_monitor_callback() + time_monitor_callback.past_epoch_duration = [4.0, 5.0, 10.0] + time_monitor_callback.epoch_history = 3 + time_monitor_callback.on_epoch_end(epoch=1, logs="on_epoch_end logs") + assert len(time_monitor_callback.past_epoch_duration) == 3 + assert round(time_monitor_callback.average_epoch, 4) == 5.0 + # Checking "past_epoch_duration" and "average_epoch" after "on_test_batch_end" for "TimeMonitorCallback" with + # length of "past_epoch_duration" less than "epoch_history" value + time_monitor_callback = self.time_monitor_callback() + time_monitor_callback.past_epoch_duration = [4.0, 5.0] + time_monitor_callback.epoch_history = 4 + time_monitor_callback.on_epoch_end(epoch=2, logs="on_epoch_end logs") + assert len(time_monitor_callback.past_epoch_duration) == 3 + assert round(time_monitor_callback.average_epoch, 4) == 3.0 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_time_monitor_callback_get_progress(self): + """ + Description: + Check "TimeMonitorCallback" class object "get_progress" method + + Input data: + "TimeMonitorCallback" class object with specified initialization parameters + + Expected results: + Test passes if value returned by "get_progress" method is equal to expected + """ + time_monitor_callback = self.time_monitor_callback() + # Checking value returned by "get_progress" for "TimeMonitorCallback" with "current_step" equal to 0 + assert time_monitor_callback.get_progress() == 0.0 + # Checking value returned by "get_progress" for "TimeMonitorCallback" with "current_step" not equal to 0 + time_monitor_callback.current_step = 16 + time_monitor_callback.total_steps = 64 + assert time_monitor_callback.get_progress() == 25.0 # (current_step / total_steps)*100 diff --git a/tests/unit/api/usecases/tasks/interfaces/test_interfaces.py b/tests/unit/api/usecases/tasks/interfaces/test_interfaces.py new file mode 100644 index 00000000000..dec8e3dd0da --- /dev/null +++ b/tests/unit/api/usecases/tasks/interfaces/test_interfaces.py @@ -0,0 +1,289 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from unittest.mock import patch + +import pytest + +from otx.api.configuration import ConfigurableParameters +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.inference_parameters import InferenceParameters +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model import ModelConfiguration, ModelEntity, ModelPrecision +from otx.api.entities.optimization_parameters import OptimizationParameters +from otx.api.entities.resultset import ResultSetEntity +from otx.api.entities.train_parameters import TrainParameters +from otx.api.usecases.tasks.interfaces.evaluate_interface import IEvaluationTask +from otx.api.usecases.tasks.interfaces.export_interface import ExportType, IExportTask +from otx.api.usecases.tasks.interfaces.inference_interface import ( + IInferenceTask, + IRawInference, +) +from otx.api.usecases.tasks.interfaces.optimization_interface import ( + IOptimizationTask, + OptimizationType, +) +from otx.api.usecases.tasks.interfaces.training_interface import ITrainingTask +from otx.api.usecases.tasks.interfaces.unload_interface import IUnload +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIEvaluationTask: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @patch( + "otx.api.usecases.tasks.interfaces.evaluate_interface.IEvaluationTask.__abstractmethods__", + set(), + ) + def test_evaluate_interface(self): + """ + Description: + Check IEvaluationTask class object initialization + + Input data: + IEvaluationTask object + + Expected results: + Test passes if IEvaluationTask object evaluate method raises NotImplementedError exception + """ + dataset = DatasetEntity() + configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="Test Header"), + label_schema=LabelSchemaEntity(), + ) + model_entity = ModelEntity(configuration=configuration, train_dataset=dataset) + with pytest.raises(NotImplementedError): + IEvaluationTask().evaluate( + ResultSetEntity( + model=model_entity, + ground_truth_dataset=dataset, + prediction_dataset=dataset, + ) + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIInferenceTask: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @patch( + "otx.api.usecases.tasks.interfaces.inference_interface.IInferenceTask.__abstractmethods__", + set(), + ) + def test_i_inference_task(self): + """ + Description: + Check IInferenceTask class object initialization + + Input data: + IInferenceTask object + + Expected results: + Test passes if IInferenceTask object infer method raises NotImplementedError exception + """ + dataset = DatasetEntity() + inference_parameters = InferenceParameters() + with pytest.raises(NotImplementedError): + IInferenceTask().infer(dataset=dataset, inference_parameters=inference_parameters) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIRawInference: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @patch( + "otx.api.usecases.tasks.interfaces.inference_interface.IRawInference.__abstractmethods__", + set(), + ) + def test_i_raw_inference(self): + """ + Description: + Check TestIRawInference class object initialization + + Input data: + TestIRawInference object + + Expected results: + Test passes if TestIRawInference object raw_infer method raises NotImplementedError exception + """ + with pytest.raises(NotImplementedError): + IRawInference().raw_infer(input_tensors={}, output_tensors={}) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestOptimizationType: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_optimization_type(self): + """ + Description: + Check OptimizationType Enum class elements + + Expected results: + Test passes if OptimizationType Enum class length is equal to expected value and its elements have expected + sequence numbers + """ + assert len(OptimizationType) == 2 + assert OptimizationType.POT.value == 1 + assert OptimizationType.NNCF.value == 2 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIOptimizationTask: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @patch( + "otx.api.usecases.tasks.interfaces.optimization_interface.IOptimizationTask.__abstractmethods__", + set(), + ) + def test_optimization_interface(self): + """ + Description: + Check IOptimizationTask class object initialization + + Input data: + IOptimizationTask object + + Expected results: + Test passes if IOptimizationTask object optimize method raises NotImplementedError exception + """ + dataset = DatasetEntity() + configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="Test Header"), + label_schema=LabelSchemaEntity(), + ) + model_entity = ModelEntity(configuration=configuration, train_dataset=dataset) + optimization_parameters = OptimizationParameters() + with pytest.raises(NotImplementedError): + IOptimizationTask().optimize( + optimization_type=OptimizationType.POT, + dataset=dataset, + output_model=model_entity, + optimization_parameters=optimization_parameters, + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestITrainingTask: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @patch( + "otx.api.usecases.tasks.interfaces.training_interface.ITrainingTask.__abstractmethods__", + set(), + ) + def test_training_interface(self): + """ + Description: + Check ITrainingTask class object initialization + + Input data: + ITrainingTask object + + Expected results: + Test passes if ITrainingTask object methods raise NotImplementedError exception + """ + i_training_task = ITrainingTask() + dataset = DatasetEntity() + configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="Test Header"), + label_schema=LabelSchemaEntity(), + ) + model_entity = ModelEntity(configuration=configuration, train_dataset=dataset) + train_parameters = TrainParameters() + + with pytest.raises(NotImplementedError): + i_training_task.save_model(model_entity) + with pytest.raises(NotImplementedError): + i_training_task.train( + dataset=dataset, + output_model=model_entity, + train_parameters=train_parameters, + ) + with pytest.raises(NotImplementedError): + i_training_task.cancel_training() + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIUnload: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @patch( + "otx.api.usecases.tasks.interfaces.unload_interface.IUnload.__abstractmethods__", + set(), + ) + def test_unload_interface(self): + """ + Description: + Check IUnload class object initialization + + Input data: + IUnload object + + Expected results: + Test passes if IUnload object unload method raises NotImplementedError exception + """ + with pytest.raises(NotImplementedError): + IUnload().unload() + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestExportType: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_export_type(self): + """ + Description: + Check ExportType Enum class elements + + Expected results: + Test passes if ExportType Enum class length is equal to expected value and its elements have expected + sequence numbers + """ + assert len(ExportType) == 2 + assert ExportType.OPENVINO.value == 1 + assert ExportType.ONNX.value == 2 + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestIExportTask: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + @patch( + "otx.api.usecases.tasks.interfaces.export_interface.IExportTask.__abstractmethods__", + set(), + ) + def test_export_interface(self): + """ + Description: + Check IExportTask class object initialization + + Input data: + IExportTask object + + Expected results: + Test passes if IExportTask object export method raises NotImplementedError exception + """ + dataset = DatasetEntity() + configuration = ModelConfiguration( + configurable_parameters=ConfigurableParameters(header="Test Header"), + label_schema=LabelSchemaEntity(), + ) + model_entity = ModelEntity(configuration=configuration, train_dataset=dataset) + with pytest.raises(NotImplementedError): + IExportTask().export( + export_type=ExportType.OPENVINO, + output_model=model_entity, + precision=ModelPrecision.FP32, + dump_features=False, + ) diff --git a/tests/unit/api/utils/test_segmentation_utils.py b/tests/unit/api/utils/test_segmentation_utils.py new file mode 100644 index 00000000000..a4dbba88667 --- /dev/null +++ b/tests/unit/api/utils/test_segmentation_utils.py @@ -0,0 +1,566 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import warnings + +import cv2 +import numpy as np +import pytest + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.id import ID +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.utils.segmentation_utils import ( + create_annotation_from_segmentation_map, + create_hard_prediction_from_soft_prediction, + get_subcontours, + mask_from_annotation, + mask_from_dataset_item, +) +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestSegmentationUtils: + @staticmethod + def rectangle_label(): + return LabelEntity( + name="Rectangle label", + domain=Domain.SEGMENTATION, + id=ID("1_rectangle_label"), + ) + + @staticmethod + def ellipse_label(): + return LabelEntity(name="Ellipse label", domain=Domain.SEGMENTATION, id=ID("3_ellipse_label")) + + @staticmethod + def polygon_label(): + return LabelEntity(name="Polygon label", domain=Domain.SEGMENTATION, id=ID("6_polygon_label")) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_mask_from_annotation(self): + """ + Description: + Check "mask_from_annotation" function + + Input data: + List with "Annotation" class objects, list with "LabelEntity" class objects, "width", "height" + + Expected results: + Test passes if array returned by "mask_from_annotation" function is equal to expected + """ + rectangle_label = self.rectangle_label() + ellipse_label = self.ellipse_label() + polygon_label = self.polygon_label() + empty_rectangle_label = LabelEntity( + name="Empty Rectangle label", + domain=Domain.SEGMENTATION, + is_empty=True, + id=ID("2_empty_rectangle_label"), + ) + empty_ellipse_label = LabelEntity( + name="Empty Ellipse label", + domain=Domain.SEGMENTATION, + is_empty=True, + id=ID("5_empty_ellipse_label"), + ) + empty_polygon_label = LabelEntity( + name="Empty Polygon label", + domain=Domain.SEGMENTATION, + is_empty=True, + id=ID("7_empty_polygon_label"), + ) + non_annotation_label = LabelEntity( + name="Non-annotation label", + domain=Domain.SEGMENTATION, + id=ID("4_empty_annotation_label"), + ) + rectangle_annotation = Annotation( + shape=Rectangle(x1=0.5, y1=0.7, x2=0.9, y2=0.9), + labels=[ScoredLabel(rectangle_label), ScoredLabel(empty_rectangle_label)], + ) + ellipse_annotation = Annotation( + shape=Ellipse(x1=0.5, y1=0.2, x2=0.9, y2=0.4), + labels=[ScoredLabel(ellipse_label), ScoredLabel(empty_ellipse_label)], + ) + polygon_shape = Polygon( + points=[ + Point(x=0.1, y=0.1), + Point(x=0.1, y=0.3), + Point(x=0.3, y=0.4), + Point(x=0.4, y=0.4), + Point(x=0.4, y=0.1), + ] + ) + polygon_annotation = Annotation( + shape=polygon_shape, + labels=[ScoredLabel(polygon_label), ScoredLabel(empty_polygon_label)], + ) + no_labels_annotation = Annotation(shape=Rectangle(x1=0.1, y1=0.8, x2=0.2, y2=0.9), labels=[]) + annotations = [ + rectangle_annotation, + ellipse_annotation, + polygon_annotation, + no_labels_annotation, + ] + labels = [ + rectangle_label, + empty_rectangle_label, + ellipse_label, + non_annotation_label, + empty_ellipse_label, + polygon_label, + empty_polygon_label, + ] + expected_array = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 6, 6, 6, 6, 0, 0, 0, 0, 0], + [0, 6, 6, 6, 6, 3, 3, 3, 3, 0], + [0, 6, 6, 6, 6, 3, 3, 3, 3, 0], + [0, 0, 0, 6, 6, 0, 0, 3, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + ] + expected_mask = np.expand_dims(expected_array, axis=2) + mask = mask_from_annotation(annotations=annotations, labels=labels, width=10, height=10) + assert np.array_equal(mask, expected_mask) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_mask_from_dataset_item(self): + """ + Description: + Check "mask_from_dataset_item" function + + Input data: + "DatasetItemEntity" class object, list with "LabelEntity" class objects + + Expected results: + Test passes if array returned by "mask_from_dataset_item" function is equal to expected + """ + rectangle_label = self.rectangle_label() + non_included_label = self.rectangle_label() + ellipse_label = self.ellipse_label() + polygon_label = self.polygon_label() + rectangle_annotation = Annotation( + shape=Rectangle(x1=0.5, y1=0.7, x2=0.9, y2=0.9), + labels=[ScoredLabel(rectangle_label), ScoredLabel(non_included_label)], + ) + ellipse_annotation = Annotation( + shape=Ellipse(x1=0.5, y1=0.2, x2=0.9, y2=0.4), + labels=[ScoredLabel(ellipse_label)], + ) + polygon_shape = Polygon( + points=[ + Point(x=0.1, y=0.1), + Point(x=0.1, y=0.3), + Point(x=0.3, y=0.4), + Point(x=0.4, y=0.4), + Point(x=0.4, y=0.1), + ] + ) + polygon_annotation = Annotation(shape=polygon_shape, labels=[ScoredLabel(polygon_label)]) + image = Image(np.random.randint(low=0, high=255, size=(480, 640, 3))) + annotation_scene = AnnotationSceneEntity( + annotations=[rectangle_annotation, ellipse_annotation, polygon_annotation], + kind=AnnotationSceneKind.ANNOTATION, + ) + dataset_item = DatasetItemEntity(media=image, annotation_scene=annotation_scene) + labels = [rectangle_label, ellipse_label, polygon_label] + expected_mask = mask_from_annotation( + annotations=dataset_item.get_annotations(), + labels=labels, + width=640, + height=480, + ) + mask = mask_from_dataset_item(dataset_item=dataset_item, labels=labels) + assert np.array_equal(mask, expected_mask) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_create_hard_prediction_from_soft_prediction(self): + """ + Description: + Check "create_hard_prediction_from_soft_prediction" function + + Input data: + "soft_prediction" nd.array, "soft_threshold" value, "blur_strength" value parameters + + Expected results: + Test passes if array returned by "create_hard_prediction_from_soft_prediction" function is equal to expected + + Steps + 1. Check array returned by "create_hard_prediction_from_soft_prediction" function for 2-dimensional array + specified as "soft_prediction" parameter + 2. Check array returned by "create_hard_prediction_from_soft_prediction" function for 3-dimensional array + specified as "soft_prediction" parameter + 3. Check that "ValueError" exception is raised by "create_hard_prediction_from_soft_prediction" function when + 1-dimensional array specified as "soft_prediction" parameter + """ + + def generate_two_dimensional_hard_prediction(prediction, threshold, strength): + prediction_copy = prediction.copy() + soft_prediction_blurred = cv2.blur(prediction_copy, (strength, strength)) + hard_prediction = soft_prediction_blurred > threshold + return hard_prediction + + def generate_three_dimensional_hard_prediction(prediction, threshold, strength): + prediction_copy = prediction.copy() + soft_prediction_blurred = cv2.blur(prediction_copy, (strength, strength)) + soft_prediction_blurred[soft_prediction_blurred < threshold] = 0 + hard_prediction = np.argmax(soft_prediction_blurred, axis=2) + return hard_prediction + + # Checking array returned by "create_hard_prediction_from_soft_prediction" for 2-dimensional array + # Default value of "blur_strength" + soft_prediction = np.random.uniform(0, 1.0, size=(10, 15)) + soft_threshold = 0.5 + expected_hard_prediction = generate_two_dimensional_hard_prediction( + prediction=soft_prediction, threshold=soft_threshold, strength=5 + ) + actual_hard_prediction = create_hard_prediction_from_soft_prediction( + soft_prediction=soft_prediction, soft_threshold=soft_threshold + ) + assert np.array_equal(actual_hard_prediction, expected_hard_prediction) + # Specified value of "blur_strength" + soft_prediction = np.random.uniform(0, 1.0, size=(10, 15)) + soft_threshold = 0.6 + blur_strength = 4 + expected_hard_prediction = generate_two_dimensional_hard_prediction( + prediction=soft_prediction, threshold=soft_threshold, strength=blur_strength + ) + actual_hard_prediction = create_hard_prediction_from_soft_prediction( + soft_prediction=soft_prediction, + soft_threshold=soft_threshold, + blur_strength=blur_strength, + ) + assert np.array_equal(actual_hard_prediction, expected_hard_prediction) + # Checking array returned by "create_hard_prediction_from_soft_prediction" for 3-dimensional array + # Default value of "blur_strength" + soft_prediction = np.random.uniform(0, 1.0, size=(8, 10, 5)) + soft_threshold = 0.4 + expected_hard_prediction = generate_three_dimensional_hard_prediction( + prediction=soft_prediction, threshold=soft_threshold, strength=5 + ) + actual_hard_prediction = create_hard_prediction_from_soft_prediction( + soft_prediction=soft_prediction, soft_threshold=soft_threshold + ) + assert np.array_equal(actual_hard_prediction, expected_hard_prediction) + # Checking array returned by "create_hard_prediction_from_soft_prediction" for 3-dimensional array + # Specified value of "blur_strength" + soft_prediction = np.random.uniform(0, 1.0, size=(10, 5, 6)) + soft_threshold = 0.5 + blur_strength = 6 + expected_hard_prediction = generate_three_dimensional_hard_prediction( + prediction=soft_prediction, threshold=soft_threshold, strength=blur_strength + ) + actual_hard_prediction = create_hard_prediction_from_soft_prediction( + soft_prediction=soft_prediction, + soft_threshold=soft_threshold, + blur_strength=blur_strength, + ) + assert np.array_equal(actual_hard_prediction, expected_hard_prediction) + # Checking that "ValueError" exception is raised by "create_hard_prediction_from_soft_prediction" when + # 1-dimensional array specified as "soft_prediction" + soft_prediction = np.random.uniform(0, 1.0, size=1) + with pytest.raises(ValueError): + create_hard_prediction_from_soft_prediction(soft_prediction, 0.5) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_get_subcontours(self): + """ + Description: + Check "get_subcontours" function + + Input data: + "Contour" list with coordinates + + Expected results: + Test passes if list of "Contour" coordinates is equal to expected + + Steps + 1. Check list returned by "get_subcontours" function for closed Contour + 2. Check list returned by "get_subcontours" function for open Contour + 3. Check list returned by "get_subcontours" function for Contour with no intersections + """ + # Checking list returned by "get_subcontours" for closed Contour + contour = [ + (0.2, 0.1), # first rectangle + (0.2, 0.2), + (0.2, 0.3), + (0.3, 0.3), + (0.3, 0.2), + (0.3, 0.1), + (0.2, 0.1), + (0.3, 0.1), # second rectangle + (0.3, 0.2), + (0.3, 0.3), + (0.4, 0.3), + (0.4, 0.2), + (0.4, 0.1), + (0.3, 0.1), + (0.2, 0.1), + ] + assert get_subcontours(contour) == [ + [(0.3, 0.1), (0.3, 0.2), (0.3, 0.3), (0.4, 0.3), (0.4, 0.2), (0.4, 0.1)], + [(0.2, 0.1), (0.2, 0.2), (0.2, 0.3)], + ] + + # Checking "get_subcontours" for open Contour + contour = [ + (0.4, 0.4), # first rectangle + (0.4, 0.5), + (0.5, 0.5), + (0.5, 0.4), + (0.4, 0.4), + (0.5, 0.4), # second rectangle + (0.5, 0.5), + (0.6, 0.5), + (0.6, 0.4), + ] + assert get_subcontours(contour) == [[(0.4, 0.4), (0.5, 0.4), (0.5, 0.5), (0.6, 0.5), (0.6, 0.4)]] + # Checking "get_subcontours" for Contour with no intersections + contour = [(0.1, 0.2), (0.1, 0.2), (0.1, 0.2), (0.1, 0.2)] + assert get_subcontours(contour) == [] + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_create_annotation_from_segmentation_map(self): + """ + Description: + Check "create_annotation_from_segmentation_map" function + + Input data: + "hard_prediction" array, "soft_prediction" array, "label_map" dictionary + + Expected results: + Test passes if "Annotations" list returned by "create_annotation_from_segmentation_map" function is + equal to expected + + Steps + 1. Check "Annotations" list returned by "create_annotation_from_segmentation_map" function for 2-dimensional + prediction arrays + 2. Check "Annotations" list returned by "create_annotation_from_segmentation_map" function for 3-dimensional + prediction arrays + 3. Check "Annotations" list returned by "create_annotation_from_segmentation_map" function for prediction arrays + with hole in segmentation mask + """ + + def check_annotation( + annotation: Annotation, + expected_points: list, + expected_label: str, + expected_probability: float, + ): + assert isinstance(annotation.shape, Polygon) + assert annotation.shape.points == expected_points + annotation_labels = annotation._Annotation__labels # type: ignore[attr-defined] + assert len(annotation_labels) == 1 + assert annotation_labels[0].label == expected_label + assert round(annotation_labels[0].probability, 5) == expected_probability + + # Checking list returned by "create_annotation_from_segmentation_map" for 2-dimensional arrays + soft_prediction = np.array( + [ + (0.0, 0.1, 0.8, 0.3, 0.2), + (0.1, 0.6, 0.7, 0.6, 0.2), + (0.2, 0.9, 0.8, 0.8, 0.1), + (0.2, 0.6, 0.9, 0.7, 0.1), + (0.0, 0.1, 0.2, 0.0, 0.1), + ] + ) + hard_prediction = np.array( + [ + (False, False, True, False, False), + (False, True, True, True, False), + (False, True, True, True, False), + (False, True, True, True, False), + (False, False, False, False, False), + ] + ) + labels = { + False: "false_label", + True: "true_label", + 2: "label_2", + } + annotations = create_annotation_from_segmentation_map( + hard_prediction=hard_prediction, + soft_prediction=soft_prediction, + label_map=labels, + ) + assert len(annotations) == 1 # 1 subcontour is created + check_annotation( + annotation=annotations[0], + expected_points=[ + Point(0.5, 0.0), + Point(0.25, 0.25), + Point(0.25, 0.5), + Point(0.25, 0.75), + Point(0.5, 0.75), + Point(0.75, 0.75), + Point(0.75, 0.5), + Point(0.75, 0.25), + ], + expected_label="true_label", + expected_probability=0.7375, + ) + # Checking list returned by "create_annotation_from_segmentation_map" for 3-dimensional arrays + soft_prediction = np.array( + [ + ([0.8, 0.1, 0.2], [0.9, 0.1, 0.2], [0.3, 0.2, 0.8], [0.1, 0.2, 0.8]), + ([0.1, 0.8, 0.3], [0.0, 0.8, 0.2], [0.2, 0.1, 0.9], [0.0, 0.2, 0.8]), + ([0.1, 0.7, 0.3], [0.3, 0.8, 0.2], [0.1, 0.2, 0.8], [0.4, 0.3, 0.7]), + ([0.0, 1.0, 0.0], [0.1, 0.9, 0.1], [0.1, 0.1, 0.9], [0.2, 0.2, 0.8]), + ] + ) + hard_prediction = np.array([(0, 0, 2, 2), (1, 1, 2, 2), (1, 1, 2, 2), (1, 1, 2, 2)]) + labels = {0: "false_label", 1: "class_1", 2: "class_2"} + annotations = create_annotation_from_segmentation_map( + hard_prediction=hard_prediction, + soft_prediction=soft_prediction, + label_map=labels, + ) + assert len(annotations) == 2 # 2 subcontours are created + check_annotation( + annotation=annotations[0], + expected_points=[ + Point(0.0, 0.3333333333333333), + Point(0.0, 0.6666666666666666), + Point(0.0, 1.0), + Point(0.3333333333333333, 1.0), + Point(0.3333333333333333, 0.6666666666666666), + Point(0.3333333333333333, 0.3333333333333333), + ], + expected_label="class_1", + expected_probability=0.83333, + ) + check_annotation( + annotation=annotations[1], + expected_points=[ + Point(0.6666666666666666, 0.0), + Point(0.6666666666666666, 0.3333333333333333), + Point(0.6666666666666666, 0.6666666666666666), + Point(0.6666666666666666, 1.0), + Point(1.0, 1.0), + Point(1.0, 0.6666666666666666), + Point(1.0, 0.3333333333333333), + Point(1.0, 0.0), + ], + expected_label="class_2", + expected_probability=0.8125, + ) + # Checking list returned by "create_annotation_from_segmentation_map" for prediction arrays with hole in + # segmentation mask + soft_prediction = np.array( + [ + (0.9, 0.85, 0.9, 1.0, 0.85, 0.9, 0.95, 1.0), + (0.95, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.85), + (0.9, 0.0, 1.0, 0.9, 0.85, 0.9, 0.0, 0.9), + (0.85, 0.0, 0.8, 0.0, 0.0, 0.95, 0.0, 0.9), + (0.9, 0.0, 0.85, 0.0, 0.0, 1.0, 0.0, 0.9), + (0.85, 0.0, 1.0, 0.9, 0.85, 0.9, 0.0, 0.85), + (0.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9), + (0.9, 0.95, 1.0, 0.9, 0.95, 0.9, 0.95, 0.95), + ] + ) + hard_prediction = np.array( + [ + (True, True, True, True, True, True, True, True), + (True, False, False, False, False, False, False, True), + (True, False, True, True, True, True, False, True), + (True, False, True, False, False, True, False, True), + (True, False, True, False, False, True, False, True), + (True, False, True, True, True, True, False, True), + (True, False, False, False, False, False, False, True), + (True, True, True, True, True, True, True, True), + ] + ) + labels = { + False: "false_label", + True: "true_label", + 2: "label_2", + } + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "The geometry of the segmentation map") + annotations = create_annotation_from_segmentation_map( + hard_prediction=hard_prediction, + soft_prediction=soft_prediction, + label_map=labels, + ) + assert len(annotations) == 2 # 2 subcontours are created + check_annotation( + annotation=annotations[0], + expected_points=[ + Point(0.2857142857142857, 0.2857142857142857), + Point(0.2857142857142857, 0.42857142857142855), + Point(0.2857142857142857, 0.5714285714285714), + Point(0.2857142857142857, 0.7142857142857143), + Point(0.42857142857142855, 0.7142857142857143), + Point(0.5714285714285714, 0.7142857142857143), + Point(0.7142857142857143, 0.7142857142857143), + Point(0.7142857142857143, 0.5714285714285714), + Point(0.7142857142857143, 0.42857142857142855), + Point(0.7142857142857143, 0.2857142857142857), + Point(0.5714285714285714, 0.2857142857142857), + Point(0.42857142857142855, 0.2857142857142857), + ], + expected_label="true_label", + expected_probability=0.90833, + ) + check_annotation( + annotation=annotations[1], + expected_points=[ + Point(0.0, 0.0), + Point(0.0, 0.14285714285714285), + Point(0.0, 0.2857142857142857), + Point(0.0, 0.42857142857142855), + Point(0.0, 0.5714285714285714), + Point(0.0, 0.7142857142857143), + Point(0.0, 0.8571428571428571), + Point(0.0, 1.0), + Point(0.14285714285714285, 1.0), + Point(0.2857142857142857, 1.0), + Point(0.42857142857142855, 1.0), + Point(0.5714285714285714, 1.0), + Point(0.7142857142857143, 1.0), + Point(0.8571428571428571, 1.0), + Point(1.0, 1.0), + Point(1.0, 0.8571428571428571), + Point(1.0, 0.7142857142857143), + Point(1.0, 0.5714285714285714), + Point(1.0, 0.42857142857142855), + Point(1.0, 0.2857142857142857), + Point(1.0, 0.14285714285714285), + Point(1.0, 0.0), + Point(0.8571428571428571, 0.0), + Point(0.7142857142857143, 0.0), + Point(0.5714285714285714, 0.0), + Point(0.42857142857142855, 0.0), + Point(0.2857142857142857, 0.0), + Point(0.14285714285714285, 0.0), + ], + expected_label="true_label", + expected_probability=0.91071, + ) diff --git a/tests/unit/api/utils/test_shape_drawer.py b/tests/unit/api/utils/test_shape_drawer.py new file mode 100644 index 00000000000..b3d96594389 --- /dev/null +++ b/tests/unit/api/utils/test_shape_drawer.py @@ -0,0 +1,1315 @@ +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import datetime +from typing import List + +import cv2 +import numpy as np +import pytest + +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.color import Color +from otx.api.entities.coordinate import Coordinate +from otx.api.entities.id import ID +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.utils.shape_drawer import DrawerEntity, Helpers, ShapeDrawer +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + +RANDOM_IMAGE = (np.random.randint(low=0, high=255, size=(1024, 1280, 3))).astype("uint8") + + +class CommonMethods: + @staticmethod + def labels() -> List[LabelEntity]: + creation_date = datetime.datetime(year=2021, month=12, day=9) + detection_label = LabelEntity( + name="Label for Detection", + domain=Domain.DETECTION, + color=Color(red=100, green=200, blue=150), + creation_date=creation_date, + id=ID("detection_label"), + ) + segmentation_label = LabelEntity( + name="Label for Segmentation", + domain=Domain.DETECTION, + color=Color(red=50, green=80, blue=200), + creation_date=creation_date, + is_empty=True, + id=ID("segmentation_label"), + ) + return [detection_label, segmentation_label] + + @staticmethod + def scored_labels() -> List[ScoredLabel]: + creation_date = datetime.datetime(year=2021, month=11, day=20) + classification_label = LabelEntity( + name="Label for Classification", + domain=Domain.CLASSIFICATION, + color=Color(red=200, green=170, blue=90), + creation_date=creation_date, + id=ID("classification_label"), + ) + anomaly_detection_label = LabelEntity( + name="Label for Anomaly Detection", + domain=Domain.ANOMALY_DETECTION, + color=Color(red=100, green=200, blue=190), + creation_date=creation_date, + is_empty=True, + id=ID("anomaly_detection_label"), + ) + return [ScoredLabel(classification_label), ScoredLabel(anomaly_detection_label)] + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestDrawerEntity: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_drawer_entity(self): + """ + Description: + Check DrawerEntity class + + Input data: + DrawerEntity object + + Expected results: + Test passes if DrawerEntity object "draw" method raises "NotImplementedError" exception + """ + entity = Rectangle.generate_full_box() + labels = CommonMethods.scored_labels() + with pytest.raises(NotImplementedError): + DrawerEntity().draw(RANDOM_IMAGE, entity, labels) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestHelpers: + @staticmethod + def generate_expected_image_with_text( + raw_image, + text: str, + initial_text_color: tuple, + processed_text_color: tuple, + helpers: Helpers, + expected_width: int, + expected_height: int, + expected_baseline: int, + text_scale: float, + thickness: float, + ) -> np.array: + processed_image = raw_image.copy() + helpers.draw_transparent_rectangle( + img=processed_image, + x1=int(helpers.cursor_pos.x), + y1=int(helpers.cursor_pos.y), + x2=int(helpers.cursor_pos.x + expected_width), + y2=int(helpers.cursor_pos.y + expected_height), + color=( + int(initial_text_color[0]), + int(initial_text_color[1]), + int(initial_text_color[2]), + ), + alpha=helpers.alpha_labels, + ) + processed_image = cv2.putText( + img=processed_image, + text=text, + org=( + helpers.cursor_pos.x + helpers.content_padding, + helpers.cursor_pos.y + expected_height - helpers.content_padding - expected_baseline, + ), + fontFace=cv2.FONT_HERSHEY_SIMPLEX, + fontScale=text_scale, + color=processed_text_color, + thickness=thickness, + lineType=cv2.LINE_AA, + ) + return processed_image + + def generate_image_for_labels( + self, + expected_image: np.array, + labels: list, + show_labels: bool, + show_confidence: bool, + helpers: Helpers, + ) -> np.array: + expected_width = 0 + expected_height = 0 + expected_text_scale = helpers.generate_text_scale(expected_image) + expected_thickness = int(expected_text_scale / 2) + for label in labels: + label_text = helpers.generate_text_for_label(label, show_labels, show_confidence) + label_color = label.color.bgr_tuple + label_size = cv2.getTextSize( + label_text, + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=expected_text_scale, + thickness=expected_thickness, + ) + label_baseline = label_size[1] + label_text_width = label_size[0][0] + label_text_height = label_size[0][1] + label_width = label_text_width + 2 * helpers.content_padding + label_height = label_text_height + label_baseline + 2 * helpers.content_padding + expected_image = self.generate_expected_image_with_text( + expected_image, + label_text, + label_color, + (255, 255, 255), # white color + helpers, + label_width, + label_height, + label_baseline, + expected_text_scale, + expected_thickness, + ) + label_content_width = label_width + helpers.content_margin + helpers.cursor_pos.x += label_content_width + helpers.line_height = label_height + expected_width += label_content_width + expected_height = label_height + return expected_image, expected_width, expected_height + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_helpers_initialization(self): + """ + Description: + Check Helpers class object initialization + + Input data: + Helpers class object + + Expected results: + Test passes if attributes of initialized Helpers class object are equal to expected + """ + helpers = Helpers() + assert helpers.alpha_shape == 100 / 256 + assert helpers.alpha_labels == 153 / 256 + assert helpers.assumed_image_width_for_text_scale == 1280 + assert helpers.top_margin == 0.07 + assert helpers.content_padding == 3 + assert helpers.top_left_box_thickness == 1 + assert helpers.content_margin == 2 + assert helpers.label_offset_box_shape == 0 + assert helpers.black == (0, 0, 0) + assert helpers.white == (255, 255, 255) + assert helpers.yellow == (255, 255, 0) + assert helpers.cursor_pos == Coordinate(0, 0) + assert helpers.line_height == 0 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_helpers_draw_transparent_rectangle(self): + """ + Description: + Check Helpers class "draw_transparent_rectangle" method + + Input data: + Helpers class object + + Expected results: + Test passes if image array returned by "draw_transparent_rectangle" method is equal to expected + + Steps + 1. Check array returned by "draw_transparent_rectangle" method for rectangle inscribed to image + 2. Check array returned by "draw_transparent_rectangle" method for rectangle equal to image shape + 3. Check array returned by "draw_transparent_rectangle" method for rectangle of out-of-bounds image + """ + + def generate_image_with_rectangle( + raw_image: np.ndarray, + start_x: int, + start_y: int, + end_x: int, + end_y: int, + new_color: tuple, + new_alpha: float, + ) -> np.ndarray: + new_image = raw_image.copy() + cropped_rectangle = new_image[start_y:end_y, start_x:end_x] + cropped_rectangle[:] = (new_alpha * np.array(new_color))[np.newaxis, np.newaxis] + ( + 1 - new_alpha + ) * cropped_rectangle + return new_image + + helpers = Helpers() + # Checking array returned by "draw_transparent_rectangle" method for rectangle inscribed to image + image = RANDOM_IMAGE.copy() + x1, y1, x2, y2 = 200, 100, 1100, 700 + color = (100, 50, 200) + alpha = 0.8 + expected_image = generate_image_with_rectangle(image, x1, y1, x2 + 1, y2 + 1, color, alpha) + helpers.draw_transparent_rectangle(image, x1, y1, x2, y2, color, alpha) + assert np.array_equal(image, expected_image) + # Checking array returned by "draw_transparent_rectangle" method for rectangle equal to image shape + image = RANDOM_IMAGE.copy() + x1, y1, x2, y2 = 0, 0, 1280, 1024 + color = (200, 80, 160) + alpha = 0.4 + expected_image = generate_image_with_rectangle(image, x1, y1, x2 - 1, y2 - 1, color, alpha) + helpers.draw_transparent_rectangle(image, x1, y1, x2, y2, color, alpha) + assert np.array_equal(image, expected_image) + # Checking array returned by "draw_transparent_rectangle" method for rectangle of out-of-bounds image + image = RANDOM_IMAGE.copy() + x2, y2 = 1300, 1100 + color = (70, 90, 20) + alpha = 0.1 + expected_image = generate_image_with_rectangle( + image, x1, y1, image.shape[1] - 1, image.shape[0] - 1, color, alpha + ) + helpers.draw_transparent_rectangle(image, x1, y1, x2, y2, color, alpha) + assert np.array_equal(image, expected_image) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_helpers_generate_text_scale(self): + """ + Description: + Check Helpers class "generate_text_scale" method + + Input data: + Helpers class object + + Expected results: + Test passes if value returned by "generate_text_scale" method is equal to expected + + Steps + 1. Check value returned by "generate_text_scale" method for image width less than + "assumed_image_width_for_text_scale" attribute of Helpers object + 2. Check value returned by "generate_text_scale" method for image width equal to + "assumed_image_width_for_text_scale" attribute of Helpers object + 3. Check value returned by "generate_text_scale" method for image width more than + "assumed_image_width_for_text_scale" attribute of Helpers object + """ + helpers = Helpers() + # Checking value returned by "generate_text_scale" for image width less than assumed_image_width_for_text_scale + image = np.random.uniform(low=0.0, high=255.0, size=(16, 10, 3)) + assert helpers.generate_text_scale(image) == 0 + image = np.random.uniform(low=0.0, high=255.0, size=(16, 1279, 3)) + assert helpers.generate_text_scale(image) == 1 + # Checking value returned by "generate_text_scale" for image width less than assumed_image_width_for_text_scale + image = np.random.uniform(low=0.0, high=255.0, size=(16, 1280, 3)) + assert helpers.generate_text_scale(image) == 1 + # Checking value returned by "generate_text_scale" for image width more than assumed_image_width_for_text_scale + image = np.random.uniform(low=0.0, high=255.0, size=(16, 1281, 3)) + assert helpers.generate_text_scale(image) == 1 + image = np.random.uniform(low=0.0, high=255.0, size=(16, 2561, 3)) + assert helpers.generate_text_scale(image) == 2 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_helpers_generate_text_for_label(self): + """ + Description: + Check Helpers class "generate_text_for_label" method + + Input data: + Helpers class object + + Expected results: + Test passes if value returned by "generate_text_for_label" method is equal to expected + + Steps + 1. Check value returned by "generate_text_for_label" for LabelEntity-type object specified as "label" parameter + 2. Check value returned by "generate_text_for_label" for ScoredLabel-type object specified as "label" parameter + """ + helpers = Helpers() + # Checking value returned by "generate_text_for_label" for LabelEntity object specified as "label" parameter + labels = CommonMethods.labels() + assert helpers.generate_text_for_label(labels[0], True, True) == "Label for Detection" + assert helpers.generate_text_for_label(labels[0], True, False) == "Label for Detection" + assert helpers.generate_text_for_label(labels[1], False, True) == "" + assert helpers.generate_text_for_label(labels[1], False, False) == "" + # Checking value returned by "generate_text_for_label" for ScoredLabel object specified as "label" parameter + scored_labels = CommonMethods.scored_labels() + assert helpers.generate_text_for_label(scored_labels[0], True, True) == "Label for Classification 0%" + assert helpers.generate_text_for_label(scored_labels[0], True, False) == "Label for Classification" + assert helpers.generate_text_for_label(scored_labels[1], False, True) == "0%" + assert helpers.generate_text_for_label(scored_labels[1], False, False) == "" + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_helpers_generate_draw_command_for_text(self): + """ + Description: + Check Helpers class "generate_draw_command_for_text" method + + Input data: + Helpers class object + + Expected results: + Test passes if tuple returned by "generate_draw_command_for_text" method is equal to expected + """ + text = "Text to add" + text_scale = 1.1 + thickness = 2 + expected_label_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, fontScale=text_scale, thickness=thickness) + expected_baseline = expected_label_size[1] + expected_text_width = expected_label_size[0][0] + expected_text_height = expected_label_size[0][1] + expected_width = expected_text_width + 2 * 3 # text_width + 2*padding + expected_content_width = expected_width + 2 # expected_width + margin + expected_height = expected_text_height + expected_baseline + 2 * 3 # text_height + baseline + 2*padding + for text_color, expected_text_color in [ + ((150, 40, 100), (255, 255, 255)), # black text + ((240, 250, 255), (0, 0, 0)), # white text + ]: + helpers = Helpers() + draw_command = helpers.generate_draw_command_for_text(text, text_scale, thickness, text_color) + assert draw_command[1] == expected_content_width + assert draw_command[2] == expected_height + image = RANDOM_IMAGE.copy() + expected_image = self.generate_expected_image_with_text( + raw_image=image, + text=text, + initial_text_color=text_color, + processed_text_color=expected_text_color, + helpers=helpers, + expected_width=expected_width, + expected_height=expected_height, + expected_baseline=expected_baseline, + text_scale=text_scale, + thickness=thickness, + ) + actual_image = draw_command[0](image) + assert np.array_equal(actual_image, expected_image) + assert helpers.cursor_pos.x == expected_content_width + assert helpers.line_height == expected_height + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_helpers_generate_draw_command_for_label(self): + """ + Description: + Check Helpers class "generate_draw_command_for_labels" method + + Input data: + Helpers class object + + Expected results: + Test passes if tuple returned by "generate_draw_command_for_labels" method is equal to expected + """ + helpers = Helpers() + expected_helpers = Helpers() + image = RANDOM_IMAGE.copy() + expected_image = image.copy() + labels = CommonMethods.labels() + CommonMethods.scored_labels() + for show_labels in [True, False]: + for show_confidence in [True, False]: + (expected_image, expected_width, expected_height,) = self.generate_image_for_labels( + expected_image=expected_image, + labels=labels, + show_labels=show_labels, + show_confidence=show_confidence, + helpers=expected_helpers, + ) + draw_command = helpers.generate_draw_command_for_labels( + labels=labels, + image=image, + show_labels=show_labels, + show_confidence=show_confidence, + ) + assert draw_command[1] == expected_width + assert draw_command[2] == expected_height + actual_image = draw_command[0](image) + assert np.array_equal(actual_image, expected_image) + assert helpers.cursor_pos.x == expected_helpers.cursor_pos.x + assert helpers.cursor_pos.y == expected_helpers.cursor_pos.y + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_helpers_draw_flagpole(self): + """ + Description: + Check Helpers class "draw_flagpole" method + + Input data: + Helpers class object + + Expected results: + Test passes if array returned by "draw_flagpole" method is equal to expected + + Steps + 1. Check array returned by "draw_flagpole" for "coordinates" parameter that fits image borders + 2. Check array returned by "draw_flagpole" for "coordinates" parameter that matches image borders + 3. Check array returned by "draw_flagpole" for "coordinates" parameter that out of image borders + """ + helpers = Helpers() + # Checking array returned by "draw_flagpole" for "coordinates" parameter that fits image bounds + image = RANDOM_IMAGE.copy() + expected_image = image.copy() + start_point = Coordinate(1.0, 1.0) + end_point = Coordinate(1279, 1023) + actual_image = helpers.draw_flagpole(image, start_point, end_point) + expected_image = cv2.line(expected_image, (1, 1), (1279, 1023), color=[0, 0, 0], thickness=2) + assert np.array_equal(actual_image, expected_image) + # Checking array returned by "draw_flagpole" for "coordinates" parameter that match image borders + start_point = Coordinate(0.0, 1024.0) + end_point = Coordinate(1280.0, 0.0) + actual_image = helpers.draw_flagpole(image, start_point, end_point) + expected_image = cv2.line(expected_image, (0, 1024), (1280, 0), color=[0, 0, 0], thickness=2) + assert np.array_equal(actual_image, expected_image) + # Checking array returned by "draw_flagpole" for "coordinates" parameter that out of image borders + start_point = Coordinate(0.0, 0.0) + end_point = Coordinate(1281, 1025) + actual_image = helpers.draw_flagpole(image, start_point, end_point) + expected_image = cv2.line(expected_image, (0, 0), (1281, 1025), color=[0, 0, 0], thickness=2) + assert np.array_equal(actual_image, expected_image) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_helpers_newline(self): + """ + Description: + Check Helpers class "newline" method + + Input data: + Helpers class object + + Expected results: + Test passes if "cursor_pos.x" and "cursor_pos.y" attributes of Helpers object returned after "newline" method + are equal to expected + """ + helpers = Helpers() + helpers.newline() + assert helpers.cursor_pos.x == 0 # resets to 0 + assert helpers.cursor_pos.y == 2 # pos.y + content_margin(equal to 2) + helpers.cursor_pos.x = 10 + helpers.newline() + assert helpers.cursor_pos.x == 0 + assert helpers.cursor_pos.y == 4 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_helpers_set_cursor_pos(self): + """ + Description: + Check Helpers class "set_cursor_pos" method + + Input data: + Helpers class object + + Expected results: + Test passes if "cursor_pos.x" and "cursor_pos.y" attributes of Helpers class object returned after + "set_cursor_pos" method are equal to expected + + Steps + 1. Check cursor_pos.x and cursor_pos.y attributes after "set_cursor_pos" with specified "cursor_pos" parameter + 2. Check cursor_pos.x and cursor_pos.y attributes after "set_cursor_pos" with not specified "cursor_pos" + parameter + """ + helpers = Helpers() + # Checking cursor_pos.x and cursor_pos.y after "set_cursor_pos" method with specified "cursor_pos" parameter + new_position = Coordinate(60.0, 200.0) + helpers.set_cursor_pos(new_position) + assert helpers.cursor_pos.x is new_position.x + assert helpers.cursor_pos.y is new_position.y + # Checking cursor_pos.x and cursor_pos.y after "set_cursor_pos" method with non-specified "cursor_pos" parameter + helpers.set_cursor_pos() + assert helpers.cursor_pos.x == 0 + assert helpers.cursor_pos.y == 0 + + +class ShapeDrawerParams: + @staticmethod + def full_rectangle_labels() -> List[LabelEntity]: + rectangle_label = LabelEntity( + name="Full-Rectangle Annotation Label", + domain=Domain.DETECTION, + color=Color(100, 200, 60), + creation_date=datetime.datetime(year=2021, month=12, day=16), + id=ID("full_rectangle_label_1"), + ) + other_rectangle_label = LabelEntity( + name="other Full-Rectangle Annotation Label", + domain=Domain.SEGMENTATION, + color=Color(80, 160, 200), + creation_date=datetime.datetime(year=2021, month=12, day=15), + id=ID("full_rectangle_label_2"), + ) + return [rectangle_label, other_rectangle_label] + + def full_rectangle_scored_labels(self) -> List[ScoredLabel]: + labels = self.full_rectangle_labels() + return [ScoredLabel(labels[0]), ScoredLabel(labels[1])] + + def full_rectangle_annotation(self) -> Annotation: + return Annotation( + shape=Rectangle(x1=0, y1=0, x2=1, y2=1), + labels=self.full_rectangle_scored_labels(), + id=ID("full_rectangle_annotation"), + ) + + @staticmethod + def rectangle_labels() -> List[LabelEntity]: + rectangle_label = LabelEntity( + name="Rectangle Annotation Label", + domain=Domain.DETECTION, + color=Color(100, 200, 60), + creation_date=datetime.datetime(year=2021, month=12, day=16), + id=ID("rectangle_label_1"), + ) + other_rectangle_label = LabelEntity( + name="other Rectangle Annotation Label", + domain=Domain.SEGMENTATION, + color=Color(80, 160, 200), + creation_date=datetime.datetime(year=2021, month=12, day=15), + id=ID("rectangle_label_2"), + ) + return [rectangle_label, other_rectangle_label] + + def rectangle_scored_labels(self) -> List[ScoredLabel]: + labels = self.rectangle_labels() + return [ScoredLabel(labels[0]), ScoredLabel(labels[1])] + + def rectangle_annotation(self) -> Annotation: + return Annotation( + shape=Rectangle(x1=0.1, y1=0.4, x2=0.4, y2=0.9), + labels=self.rectangle_scored_labels(), + id=ID("rectangle_annotation"), + ) + + @staticmethod + def polygon_labels() -> List[LabelEntity]: + polygon_label = LabelEntity( + name="Polygon Annotation Label", + domain=Domain.DETECTION, + color=Color(200, 200, 100), + creation_date=datetime.datetime(year=2021, month=12, day=16), + id=ID("polygon_label_1"), + ) + other_polygon_label = LabelEntity( + name="other Polygon Annotation Label", + domain=Domain.SEGMENTATION, + color=Color(100, 100, 150), + creation_date=datetime.datetime(year=2021, month=12, day=15), + id=ID("polygon_label_2"), + ) + return [polygon_label, other_polygon_label] + + def polygon_scored_labels(self) -> List[ScoredLabel]: + labels = self.polygon_labels() + return [ScoredLabel(labels[0]), ScoredLabel(labels[1])] + + def polygon_annotation(self) -> Annotation: + return Annotation( + shape=Polygon( + [ + Point(0.3, 0.4), + Point(0.3, 0.7), + Point(0.5, 0.75), + Point(0.8, 0.7), + Point(0.8, 0.4), + ] + ), + labels=self.polygon_scored_labels(), + id=ID("polygon_annotation"), + ) + + @staticmethod + def ellipse_labels() -> List[LabelEntity]: + ellipse_label = LabelEntity( + name="Ellipse Annotation Label", + domain=Domain.DETECTION, + color=Color(100, 100, 200), + creation_date=datetime.datetime(year=2021, month=12, day=16), + id=ID("ellipse_label_1"), + ) + other_ellipse_label = LabelEntity( + name="other Ellipse Annotation Label", + domain=Domain.SEGMENTATION, + color=Color(200, 80, 150), + creation_date=datetime.datetime(year=2021, month=12, day=15), + id=ID("ellipse_label_2"), + ) + return [ellipse_label, other_ellipse_label] + + def ellipse_scored_labels(self) -> List[ScoredLabel]: + labels = self.ellipse_labels() + return [ScoredLabel(labels[0]), ScoredLabel(labels[1])] + + def ellipse_annotation(self) -> Annotation: + return Annotation( + shape=Ellipse(x1=0.5, y1=0.0, x2=1.0, y2=0.5), + labels=self.ellipse_scored_labels(), + id=ID("ellipse_annotation"), + ) + + def annotation_scene(self) -> AnnotationSceneEntity: + return AnnotationSceneEntity( + annotations=[ + self.full_rectangle_annotation(), + self.rectangle_annotation(), + self.polygon_annotation(), + self.ellipse_annotation(), + ], + kind=AnnotationSceneKind.ANNOTATION, + creation_date=datetime.datetime(year=2021, month=12, day=16), + id=ID("annotation_scene"), + ) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestShapeDrawer: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_shape_drawer_initialization(self): + """ + Description: + Check ShapeDrawer class object initialization + + Input data: + ShapeDrawer class object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if attributes of initialized ShapeDrawer class object are equal to expected + + Steps + 1. Check attributes of ShapeDrawer object initialized with "show_count" parameter is "False" and "is_one_label" + is "True" + 2. Check attributes of ShapeDrawer object initialized with "show_count" parameter is "True" and "is_one_label" + is "True" + 3. Check attributes of ShapeDrawer object initialized with "show_count" parameter is "False" and "is_one_label" + is "False" + """ + for show_count, is_one_label, show_labels in [ + (False, True, False), + (True, True, True), + (False, False, True), + ]: + shape_drawer = ShapeDrawer(show_count=show_count, is_one_label=is_one_label) + assert shape_drawer.show_labels == show_labels + assert shape_drawer.show_confidence + assert shape_drawer.show_count == show_count + assert shape_drawer.is_one_label == is_one_label + for drawer in shape_drawer.shape_drawers: + assert drawer.show_labels == show_labels + assert drawer.show_confidence + assert shape_drawer.top_left_drawer.show_labels + assert shape_drawer.top_left_drawer.show_confidence + assert shape_drawer.top_left_drawer.is_one_label == is_one_label + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_shape_drawer_draw(self): + """ + Description: + Check ShapeDrawer class "draw" method + + Input data: + ShapeDrawer class object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if image array returned by "draw" method is equal to expected + + Steps + 1. Check array returned by "draw" method for "show_count" parameter is "True" and "is_one_label" is "True + 2. Check array returned by "draw" method for "show_count" parameter is "False" and "is_one_label" is "False" + 3. Check array returned by "draw" method for "show_count" parameter is "True" and "is_one_label" is "False" + 4. Check array returned by "draw" method for "show_count" parameter is "False" and "is_one_label" is "True" + """ + annotation_scene = ShapeDrawerParams().annotation_scene() + annotations = annotation_scene.annotations + full_rectangle_annotation = annotations[0] + rectangle_annotation = annotations[1] + polygon_annotation = annotations[2] + ellipse_annotation = annotations[3] + + for show_count, is_one_label in [ + (True, True), + (False, False), + (True, False), + (False, True), + ]: + image = RANDOM_IMAGE.copy() + expected_image = image.copy() + shape_drawer = ShapeDrawer(show_count, is_one_label) + if not shape_drawer.is_one_label: + expected_image = shape_drawer.top_left_drawer.draw(expected_image, full_rectangle_annotation, labels=[]) + expected_image = shape_drawer.shape_drawers[0].draw( + expected_image, + rectangle_annotation.shape, + rectangle_annotation.get_labels(), + ) + expected_image = shape_drawer.shape_drawers[1].draw( + expected_image, + polygon_annotation.shape, + polygon_annotation.get_labels(), + ) + expected_image = shape_drawer.shape_drawers[2].draw( + expected_image, + ellipse_annotation.shape, + ellipse_annotation.get_labels(), + ) + if is_one_label: + expected_image = shape_drawer.top_left_drawer.draw_labels(expected_image, annotation_scene.get_labels()) + if show_count: + expected_image = shape_drawer.top_left_drawer.draw_annotation_count(expected_image, 3) + shape_drawer.top_left_drawer.set_cursor_pos() + actual_image = shape_drawer.draw(image, annotation_scene, []) + assert np.array_equal(actual_image, expected_image) + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestTopLeftDrawer: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_top_left_drawer_initialization(self): + """ + Description: + Check TopLeftDrawer subclass object initialization + + Input data: + TopLeftDrawer subclass object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if attributes of initialized TopLeftDrawer subclass object are equal to expected + + Steps + 1. Check attributes of TopLeftDrawer object initialized for ShapeDrawer with "show_count" parameter is "False" + and "is_one_label" is "True" + 2. Check attributes of TopLeftDrawer object initialized for ShapeDrawer with "show_count" parameter is "True" + and "is_one_label" is "True" + 3. Check attributes of TopLeftDrawer object initialized for ShapeDrawer with "show_count" parameter is "False" + and "is_one_label" is "False" + """ + for show_count, is_one_label in [(False, True), (True, True), (False, False)]: + shape_drawer = ShapeDrawer(show_count=show_count, is_one_label=is_one_label) + assert shape_drawer.top_left_drawer.show_labels + assert shape_drawer.top_left_drawer.show_confidence + assert shape_drawer.top_left_drawer.is_one_label == is_one_label + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_top_left_drawer_draw_labels(self): + """ + Description: + Check TopLeftDrawer subclass "draw_labels" method + + Input data: + TopLeftDrawer subclass object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if image array returned by "draw_labels" method is equal to expected + + Steps + 1. Check array returned by "draw_labels" method for ShapeDrawer with "show_count" parameter is "False" and + "is_one_label" is "True" + 2. Check array returned by "draw_labels" method for ShapeDrawer with "show_count" parameter is "True" and + "is_one_label" is "True" + 3. Check array returned by "draw_labels" method for ShapeDrawer with "show_count" parameter is "False" and + "is_one_label" is "False" + """ + labels = ShapeDrawerParams.rectangle_labels() + ShapeDrawerParams().polygon_scored_labels() + for show_count, is_one_label in [(False, True), (True, True), (False, False)]: + image = RANDOM_IMAGE.copy() + expected_image = image.copy() + shape_drawer = ShapeDrawer(show_count=show_count, is_one_label=is_one_label) + expected_shape_drawer = ShapeDrawer(show_count=show_count, is_one_label=is_one_label) + show_confidence = ( + shape_drawer.top_left_drawer.show_confidence if not shape_drawer.top_left_drawer.is_one_label else False + ) + expected_image = TestHelpers().generate_image_for_labels( + expected_image, + labels, + expected_shape_drawer.top_left_drawer.show_labels, + show_confidence, + expected_shape_drawer.top_left_drawer, + )[0] + expected_shape_drawer.top_left_drawer.newline() + actual_image = shape_drawer.top_left_drawer.draw_labels(image, labels) + assert np.array_equal(actual_image, expected_image) + assert shape_drawer.top_left_drawer.cursor_pos == expected_shape_drawer.top_left_drawer.cursor_pos + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_top_left_drawer_draw(self): + """ + Description: + Check TopLeftDrawer subclass "draw" method + + Input data: + TopLeftDrawer subclass object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if image array returned by "draw" method is equal to expected + """ + annotation = ShapeDrawerParams().annotation_scene() + image = RANDOM_IMAGE.copy() + expected_image = image.copy() + shape_drawer = ShapeDrawer(True, True) + expected_shape_drawer = ShapeDrawer(True, True) + draw_command = expected_shape_drawer.top_left_drawer.generate_draw_command_for_labels( + annotation.get_labels(), expected_image, True, True + )[0] + expected_image = draw_command(expected_image) + actual_image = shape_drawer.top_left_drawer.draw(image, annotation, []) + assert np.array_equal(actual_image, expected_image) + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_top_left_drawer_draw_annotation_count(self): + """ + Description: + Check TopLeftDrawer subclass "draw_annotation_count" method + + Input data: + TopLeftDrawer subclass object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if image array returned by "draw_annotation_count" method is equal to expected + """ + image = RANDOM_IMAGE.copy() + expected_image = image.copy() + shape_drawer = ShapeDrawer(True, True) + expected_shape_drawer = ShapeDrawer(True, True) + draw_command = expected_shape_drawer.top_left_drawer.generate_draw_command_for_text( + "Count: 4", 1.0, 1, (255, 255, 0) + )[0] + expected_image = draw_command(expected_image) + expected_shape_drawer.top_left_drawer.newline() + actual_image = shape_drawer.top_left_drawer.draw_annotation_count(image, 4) + assert np.array_equal(actual_image, expected_image) + assert shape_drawer.top_left_drawer.cursor_pos == expected_shape_drawer.top_left_drawer.cursor_pos + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestRectangleDrawer: + @staticmethod + def draw_rectangle_labels( + image: np.array, + labels: list, + shape_drawer: ShapeDrawer, + rectangle: Rectangle, + cursor_position: Coordinate, + ) -> np.ndarray: + + image_copy = image.copy() + rectangle_drawer = shape_drawer.shape_drawers[0] + base_color = labels[0].color.bgr_tuple + x1, y1 = int(rectangle.x1 * image_copy.shape[1]), int(rectangle.y1 * image_copy.shape[0]) + x2, y2 = int(rectangle.x2 * image_copy.shape[1]), int(rectangle.y2 * image_copy.shape[0]) + # Drawing rectangle + image_copy = rectangle_drawer.draw_transparent_rectangle( + image_copy, x1, y1, x2, y2, base_color, rectangle_drawer.alpha_shape + ) + # Drawing rectangle frame + image_copy = cv2.rectangle(img=image_copy, pt1=(x1, y1), pt2=(x2, y2), color=base_color, thickness=2) + # Generating draw command to add labels to image + draw_command, _, _ = rectangle_drawer.generate_draw_command_for_labels( + labels, + image_copy, + rectangle_drawer.show_labels, + rectangle_drawer.show_confidence, + ) + rectangle_drawer.set_cursor_pos(cursor_position) + image_copy = draw_command(image_copy) + return image_copy + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_drawer_initialization(self): + """ + Description: + Check RectangleDrawer subclass object initialization + + Input data: + RectangleDrawer subclass object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if attributes of initialized RectangleDrawer subclass object are equal to expected + """ + for show_count, is_one_label, show_labels in [ + (False, True, False), + (True, True, True), + (False, False, True), + ]: + shape_drawer = ShapeDrawer(show_count=show_count, is_one_label=is_one_label) + assert shape_drawer.shape_drawers[0].show_labels == show_labels + assert shape_drawer.shape_drawers[0].show_confidence + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_drawer_draw(self): + """ + Description: + Check RectangleDrawer subclass "draw" method + + Input data: + RectangleDrawer subclass object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if image array returned by "draw" method is equal to expected + + Steps + 1. Check array returned by "draw" method without changing labels positions + 2. Check array returned by "draw" method with putting labels to the bottom of drawn rectangle + 3. Check array returned by "draw" method with shifting labels to the left of drawn rectangle + """ + labels = ShapeDrawerParams().rectangle_scored_labels() + for rectangle, expected_cursor_position in [ + ( # without changing labels positions + Rectangle(0.1, 0.3, 0.8, 0.5), + Coordinate(128, 271), + ), + ( # with putting labels to the bottom of drawn rectangle + Rectangle(0.1, 0.1, 0.9, 0.9), + Coordinate(128, 102), + ), + ( # with shifting labels to the left of drawn rectangle + Rectangle(0.6, 0.7, 0.9, 0.9), + Coordinate(61, 680), + ), + ]: + image = RANDOM_IMAGE.copy() + shape_drawer = ShapeDrawer(True, False) + expected_shape_drawer = ShapeDrawer(True, False) + expected_image = self.draw_rectangle_labels( + image, + labels, + expected_shape_drawer, + rectangle, + expected_cursor_position, + ) + actual_image = shape_drawer.shape_drawers[0].draw(image, rectangle, labels) + assert np.array_equal(actual_image, expected_image) + assert shape_drawer.top_left_drawer.cursor_pos == expected_shape_drawer.top_left_drawer.cursor_pos + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestEllipseDrawer: + @staticmethod + def draw_ellipse_labels( + image: np.array, + labels: list, + shape_drawer: ShapeDrawer, + ellipse: Ellipse, + cursor_position: Coordinate, + flagpole_start: Coordinate, + flagpole_end: Coordinate, + ) -> np.ndarray: + image_copy = image.copy() + ellipse_shape_drawer = shape_drawer.shape_drawers[2] + base_color = labels[0].color.bgr_tuple + if ellipse.width > ellipse.height: + axes = ( + int(ellipse.major_axis * image_copy.shape[1]), + int(ellipse.minor_axis * image_copy.shape[0]), + ) + else: + axes = ( + int(ellipse.major_axis * image_copy.shape[0]), + int(ellipse.minor_axis * image_copy.shape[1]), + ) + center = ( + int(ellipse.x_center * image_copy.shape[1]), + int(ellipse.y_center * image_copy.shape[0]), + ) + # Drawing Ellipse on image + overlay = cv2.ellipse( + img=image_copy.copy(), + center=center, + axes=axes, + angle=0, + startAngle=0, + endAngle=360, + color=base_color, + thickness=cv2.FILLED, + ) + result_without_border = cv2.addWeighted( + overlay, + ellipse_shape_drawer.alpha_shape, + image_copy, + 1 - ellipse_shape_drawer.alpha_shape, + 0, + ) + # Drawing Ellipse borders on image + result_with_border = cv2.ellipse( + img=result_without_border, + center=center, + axes=axes, + angle=0, + startAngle=0, + endAngle=360, + color=base_color, + lineType=cv2.LINE_AA, + ) + # Generating draw command to add labels to image + draw_command = ellipse_shape_drawer.generate_draw_command_for_labels( + labels, + image_copy, + ellipse_shape_drawer.show_labels, + ellipse_shape_drawer.show_confidence, + )[0] + # Getting top-left corner of box around Ellipse + ellipse_shape_drawer.set_cursor_pos(cursor_position) + image_copy = draw_command(result_with_border) + image_copy = ellipse_shape_drawer.draw_flagpole(image_copy, flagpole_start, flagpole_end) + return image_copy + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_ellipse_drawer_initialization(self): + """ + Description: + Check EllipseDrawer subclass object initialization + + Input data: + EllipseDrawer subclass object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if attributes of initialized EllipseDrawer subclass object are equal to expected + """ + for show_count, is_one_label, show_labels in [ + (False, True, False), + (True, True, True), + (False, False, True), + ]: + shape_drawer = ShapeDrawer(show_count=show_count, is_one_label=is_one_label) + assert shape_drawer.shape_drawers[2].show_labels == show_labels + assert shape_drawer.shape_drawers[2].show_confidence + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_ellipse_drawer_draw(self): + """ + Description: + Check EllipseDrawer subclass "draw" method + + Input data: + EllipseDrawer subclass object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if array returned by "draw" method is equal to expected + + Steps + 1. Check array returned by "draw" method without changing labels positions + 2. Check array returned by "draw" method with putting labels to the bottom + 3. Check array returned by "draw" method with shifting labels to the left + """ + labels = ShapeDrawerParams().ellipse_scored_labels() + for (ellipse, expected_cursor_position, flagpole_start, flagpole_end,) in [ + ( # without changing labels positions + Ellipse(0.1, 0.3, 0.8, 0.5), + Coordinate(128.0, 271.2), + Coordinate(129.0, 307.2), + Coordinate(129, 409), + ), + ( # with putting labels to the bottom + Ellipse(0.1, 0.1, 0.8, 0.8), + Coordinate(128.0, 921.6), + Coordinate(129.0, 921.6), + Coordinate(129, 460), + ), + ( # with shifting labels to the left + Ellipse(0.6, 0.7, 0.9, 0.9), + Coordinate(299, 680.8), + Coordinate(769.0, 716.8), + Coordinate(769, 819), + ), + ]: + image = RANDOM_IMAGE.copy() + shape_drawer = ShapeDrawer(True, False) + expected_shape_drawer = ShapeDrawer(True, False) + expected_image = self.draw_ellipse_labels( + image, + labels, + expected_shape_drawer, + ellipse, + expected_cursor_position, + flagpole_start, + flagpole_end, + ) + actual_image = shape_drawer.shape_drawers[2].draw(image, ellipse, labels) + assert np.array_equal(actual_image, expected_image) + assert shape_drawer.top_left_drawer.cursor_pos == expected_shape_drawer.top_left_drawer.cursor_pos + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestPolygonDrawer: + @staticmethod + def draw_polygon_labels( + image: np.array, + labels: list, + shape_drawer: ShapeDrawer, + polygon: Polygon, + cursor_position: Coordinate, + flagpole_start: Coordinate, + flagpole_end: Coordinate, + ) -> np.ndarray: + image_copy = image.copy() + polygon_drawer = shape_drawer.shape_drawers[1] + base_color = labels[0].color.bgr_tuple + # Draw Polygon on the image + alpha = polygon_drawer.alpha_shape + contours = np.array( + [[point.x * image_copy.shape[1], point.y * image_copy.shape[0]] for point in polygon.points], + dtype=np.int32, + ) + overlay = cv2.drawContours( + image=image_copy.copy(), + contours=[contours], + contourIdx=-1, + color=base_color, + thickness=cv2.FILLED, + ) + result_without_border = cv2.addWeighted(overlay, alpha, image_copy, 1 - alpha, 0) + result_with_border = cv2.drawContours( + image=result_without_border, + contours=[contours], + contourIdx=-1, + color=base_color, + thickness=2, + lineType=cv2.LINE_AA, + ) + # Generating draw command to add labels to image + draw_command = polygon_drawer.generate_draw_command_for_labels( + labels, + image_copy, + polygon_drawer.show_labels, + polygon_drawer.show_confidence, + )[0] + # Getting top-left corner of box around Polygon + polygon_drawer.set_cursor_pos(cursor_position) + image_copy = draw_command(result_with_border) + image_copy = polygon_drawer.draw_flagpole(image_copy, flagpole_start, flagpole_end) + return image_copy + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_polygon_drawer_initialization(self): + """ + Description: + Check PolygonDrawer subclass object initialization + + Input data: + PolygonDrawer subclass object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if attributes of initialized PolygonDrawer subclass object are equal to expected + """ + for show_count, is_one_label, show_labels in [ + (False, True, False), + (True, True, True), + (False, False, True), + ]: + shape_drawer = ShapeDrawer(show_count=show_count, is_one_label=is_one_label) + assert shape_drawer.shape_drawers[1].show_labels == show_labels + assert shape_drawer.shape_drawers[1].show_confidence + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_polygon_drawer_draw(self): + """ + Description: + Check PolygonDrawer subclass "draw" method + + Input data: + PolygonDrawer subclass object with "show_count" and "is_one_label" parameters + + Expected results: + Test passes if image array returned by "draw" method is equal to expected + + Steps + 1. Check array returned by "draw" method without changing labels positions + 2. Check array returned by "draw" method with putting labels to the bottom + 3. Check array returned by "draw" method with shifting labels to the left + """ + labels = ShapeDrawerParams().polygon_scored_labels() + polygon_no_change_labels_position = Polygon( + [ + Point(0.2, 0.2), + Point(0.2, 0.6), + Point(0.4, 0.7), + Point(0.6, 0.6), + Point(0.6, 0.2), + ] + ) + polygon_put_labels_to_bottom = Polygon( + [ + Point(0.2, 0.1), + Point(0.2, 0.6), + Point(0.4, 0.7), + Point(0.6, 0.6), + Point(0.6, 0.1), + ] + ) + polygon_shift_labels_to_left = Polygon( + [ + Point(0.4, 0.2), + Point(0.4, 0.5), + Point(0.6, 0.6), + Point(0.9, 0.5), + Point(0.9, 0.2), + ] + ) + + for (polygon, expected_cursor_position, flagpole_start, flagpole_end,) in [ + ( # without changing labels position + polygon_no_change_labels_position, + Coordinate(251, 168), + Coordinate(257, 204), + Coordinate(257, 204), + ), + ( # with putting labels to the bottom + polygon_put_labels_to_bottom, + Coordinate(251, 716), + Coordinate(257, 716), + Coordinate(257, 102), + ), + ( # with shifting labels to the left + polygon_shift_labels_to_left, + Coordinate(251, 168), + Coordinate(513, 204), + Coordinate(513, 204), + ), + ]: + image = RANDOM_IMAGE.copy() + shape_drawer = ShapeDrawer(True, False) + expected_shape_drawer = ShapeDrawer(True, False) + expected_image = self.draw_polygon_labels( + image, + labels, + expected_shape_drawer, + polygon, + expected_cursor_position, + flagpole_start, + flagpole_end, + ) + actual_image = shape_drawer.shape_drawers[1].draw(image, polygon, labels) + assert np.array_equal(actual_image, expected_image) + assert shape_drawer.top_left_drawer.cursor_pos == expected_shape_drawer.top_left_drawer.cursor_pos diff --git a/tests/unit/api/utils/test_shape_factory.py b/tests/unit/api/utils/test_shape_factory.py new file mode 100644 index 00000000000..cb5b30f6be3 --- /dev/null +++ b/tests/unit/api/utils/test_shape_factory.py @@ -0,0 +1,174 @@ +"""This UnitTest tests ShapeFactory functionality""" + +# Copyright (C) 2021-2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import pytest + +from otx.api.entities.shapes.ellipse import Ellipse +from otx.api.entities.shapes.polygon import Point, Polygon +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.utils.shape_factory import ShapeFactory +from tests.unit.api.constants.components import OtxSdkComponent +from tests.unit.api.constants.requirements import Requirements + + +@pytest.mark.components(OtxSdkComponent.OTX_API) +class TestShapeFactory: + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_rectangle_shape_conversion(self): + """ + Description: + Checks that conversions from Rectangle to other shapes works correctly + + Input data: + A rectangle at [0.25, 0.1, 0.5, 0.3] + + Expected results: + The test passes if the rectangle can be converted to Ellipse and Polygon + + Steps + 1. Create rectangle and get coordinates + 2. Convert to Ellipse + 3. Convert to Polygon + 4. Convert to Rectangle + """ + rectangle = Rectangle(x1=0.25, y1=0.1, x2=0.5, y2=0.3) + rectangle_coords = (rectangle.x1, rectangle.y1, rectangle.x2, rectangle.y2) + + ellipse = ShapeFactory.shape_as_ellipse(rectangle) + assert isinstance(ellipse, Ellipse) + assert (ellipse.x1, ellipse.y1, ellipse.x2, ellipse.y2) == rectangle_coords + + polygon = ShapeFactory.shape_as_polygon(rectangle) + assert isinstance(polygon, Polygon) + assert ( + polygon.min_x, + polygon.min_y, + polygon.max_x, + polygon.max_y, + ) == rectangle_coords + + rectangle2 = ShapeFactory.shape_as_rectangle(rectangle) + assert isinstance(rectangle2, Rectangle) + assert rectangle == rectangle2 + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_ellipse_shape_conversion(self): + """ + Description: + Checks that conversions from Ellipse to other shapes works correctly + + Input data: + A rectangle at [0.1, 0.1, 0.5, 0.2] + + Expected results: + The test passes if the Ellipse can be converted to Rectangle and Polygon + + Steps + 1. Create Ellipse and get coordinates + 2. Convert to Ellipse + 3. Convert to Polygon + 4. Convert to Rectangle + """ + ellipse = Ellipse(x1=0.1, y1=0.1, x2=0.5, y2=0.2) + ellipse_coords = (ellipse.x1, ellipse.y1, ellipse.x2, ellipse.y2) + + ellipse2 = ShapeFactory.shape_as_ellipse(ellipse) + assert isinstance(ellipse, Ellipse) + assert ellipse == ellipse2 + + polygon = ShapeFactory.shape_as_polygon(ellipse) + assert isinstance(polygon, Polygon) + assert polygon.min_x == pytest.approx(ellipse.x1, 0.1) + assert polygon.min_y == pytest.approx(ellipse.y1, 0.1) + assert polygon.max_x == pytest.approx(ellipse.x2, 0.1) + assert polygon.max_y == pytest.approx(ellipse.y2, 0.1) + + rectangle = ShapeFactory.shape_as_rectangle(ellipse) + assert isinstance(rectangle, Rectangle) + assert ( + rectangle.x1, + rectangle.y1, + rectangle.x2, + rectangle.y2, + ) == ellipse_coords + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_polygon_shape_conversion(self): + """ + Description: + Checks that conversions from Polygon to other shapes works correctly + + Input data: + A Polygon at [[0.01, 0.2], [0.35, 0.2], [0.35, 0.4]] + + Expected results: + The test passes if the Polygon can be converted to Rectangle and Ellipse + + Steps + 1. Create rectangle and get coordinates + 2. Convert to Ellipse + 3. Convert to Polygon + 4. Convert to Rectangle + """ + point1 = Point(x=0.01, y=0.2) + point2 = Point(x=0.35, y=0.2) + point3 = Point(x=0.35, y=0.4) + polygon = Polygon(points=[point1, point2, point3]) + polygon_coords = (polygon.min_x, polygon.min_y, polygon.max_x, polygon.max_y) + + ellipse = ShapeFactory.shape_as_ellipse(polygon) + assert isinstance(ellipse, Ellipse) + assert (ellipse.x1, ellipse.y1, ellipse.x2, ellipse.y2) == polygon_coords + + polygon2 = ShapeFactory.shape_as_polygon(polygon) + assert isinstance(polygon2, Polygon) + assert polygon == polygon2 + + rectangle = ShapeFactory.shape_as_rectangle(polygon) + assert isinstance(rectangle, Rectangle) + assert ( + rectangle.x1, + rectangle.y1, + rectangle.x2, + rectangle.y2, + ) == polygon_coords + + @pytest.mark.priority_medium + @pytest.mark.unit + @pytest.mark.reqids(Requirements.REQ_1) + def test_produces_valid_crop(self): + """ + Description: + Checks that shape_produces_valid_crop returns the correct values and + does not raise errors + + Input data: + A valid Rectangle at [0, 0.4, 1, 0.5] + A Polygon that has an invalid bounding box + + Expected results: + The test passes if the call with the Rectangle returns True and + the one with the polygon returns False + + Steps + 1. Check Valid Rectangle + 2. Check invalid Polygon + """ + rectangle = Rectangle(x1=0, y1=0.4, x2=1, y2=0.5) + assert ShapeFactory.shape_produces_valid_crop(rectangle, 100, 100) + + point1 = Point(x=0.01, y=0.1) + point2 = Point(x=0.35, y=0.1) + point3 = Point(x=0.35, y=0.1) + polygon = Polygon(points=[point1, point2, point3]) + assert not ShapeFactory.shape_produces_valid_crop(polygon, 100, 250) diff --git a/tests/unit/cli/__init__.py b/tests/unit/cli/__init__.py deleted file mode 100644 index 6a16273c024..00000000000 --- a/tests/unit/cli/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/cli/builder/test_cli_builder.py b/tests/unit/cli/builder/test_cli_builder.py new file mode 100644 index 00000000000..edf248e62bc --- /dev/null +++ b/tests/unit/cli/builder/test_cli_builder.py @@ -0,0 +1,297 @@ +"""Unit-Test Case for otx.cli.builder.builder.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from pathlib import Path + +import mmcv +import pytest +from mmcv.utils import Registry +from torch import nn + +from otx.algorithms.common.adapters.mmcv.utils.config_utils import OTXConfig +from otx.cli.builder.builder import ( + Builder, + get_backbone_out_channels, + update_backbone_args, + update_channels, +) +from otx.cli.utils.importing import get_otx_root_path +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestOTXCLIBuilder: + """Check Builder's function is working well. + + 1. Check "Builder.build_backbone_config" function that generate backbone configuration file is working well + + 1. Generate backbone config file (mmcls.MMOVBackbone) + 2. Raise ValueError with wrong output_path + + 2. Check "Builder.merge_backbone" function that update model config with new backbone is working well + + 1. Update model config with mmcls.ResNet backbone (default model.backbone: otx.OTXEfficientNet) + 2. Raise ValueError with wrong model_config_path + 3. Raise ValueError with wrong backbone_config_path + 4. Update model config without backbone's out_indices + 5. Update model config with backbone's pretrained path + """ + + @pytest.fixture(autouse=True) + def setup(self, tmp_dir_path: str) -> None: + self.otx_builder = Builder() + self.otx_root = get_otx_root_path() + self.tmp_dir_path = tmp_dir_path if isinstance(tmp_dir_path, Path) else Path(tmp_dir_path) + + @e2e_pytest_unit + @pytest.mark.parametrize("backbone_type", ["mmcls.MMOVBackbone"]) + def test_builder_build_backbone_config_generate_backbone(self, backbone_type: str) -> None: + """Generate backbone config file (mmcls.MMOVBackbone).""" + tmp_backbone_path = self.tmp_dir_path / "backbone.yaml" + self.otx_builder.build_backbone_config(backbone_type, tmp_backbone_path) + assert tmp_backbone_path.exists() + backbone_config = mmcv.load(str(tmp_backbone_path)) + assert backbone_config["backbone"]["type"] == backbone_type + + @e2e_pytest_unit + @pytest.mark.parametrize("backbone_type", ["mmcls.MMOVBackbone"]) + def test_builder_build_backbone_config_abnormal_output_path(self, backbone_type: str) -> None: + """Raise ValueError with wrong output_path.""" + tmp_backbone_path = self.tmp_dir_path / "wrong.path" + with pytest.raises(ValueError): + self.otx_builder.build_backbone_config(backbone_type, tmp_backbone_path) + + @e2e_pytest_unit + def test_builder_merge_backbone_abnormal_model_path(self) -> None: + """Raise ValueError with wrong model_config_path.""" + workspace_path = self.tmp_dir_path / "test_builder_merge_backbone" + tmp_backbone_path = workspace_path / "backbone.yaml" + with pytest.raises(ValueError): + self.otx_builder.merge_backbone("unexpected", tmp_backbone_path) + + @e2e_pytest_unit + def test_builder_merge_backbone_abnormal_backbone_path(self) -> None: + """Raise ValueError with wrong backbone_config_path.""" + workspace_path = self.tmp_dir_path / "test_builder_merge_backbone" + tmp_model_path = workspace_path / "model.py" + with pytest.raises(ValueError): + self.otx_builder.merge_backbone(tmp_model_path, "unexpected") + + @e2e_pytest_unit + def test_builder_merge_backbone(self, mocker) -> None: + """Update model config without backbone's out_indices.""" + mocker.patch("otx.cli.builder.builder.Path.exists", return_value=True) + mock_backbone_config = { + "backbone": {"type": "torchvision.resnet18", "use_out_indices": True, "out_indices": [0, 1, 2]} + } + mock_mmcv_load = mocker.patch("otx.cli.builder.builder.mmcv.load", return_value=mock_backbone_config) + mock_model_config = mocker.MagicMock() + mock_model_config.model.backbone = {"out_indices"} + mock_config_from_file = mocker.patch( + "otx.cli.builder.builder.OTXConfig.fromfile", return_value=mock_model_config + ) + mock_get_backbone_registry = mocker.patch( + "otx.cli.builder.builder.get_backbone_registry", return_value=(None, ["otx"]) + ) + mock_backbone = mocker.MagicMock() + mock_build_from_cfg = mocker.patch("otx.cli.builder.builder.build_from_cfg", return_value=mock_backbone) + mock_get_backbone_out_channels = mocker.patch( + "otx.cli.builder.builder.get_backbone_out_channels", return_value=[0, 1, 2] + ) + mock_update_channels = mocker.patch("otx.cli.builder.builder.update_channels", return_value=None) + + Builder().merge_backbone("fake_model_path", "fake_backbone_path") + + mock_mmcv_load.assert_called_once() + mock_config_from_file.assert_called_once() + mock_get_backbone_registry.assert_called_once() + mock_build_from_cfg.assert_called_once() + mock_get_backbone_out_channels.assert_called_once() + mock_update_channels.assert_called_once() + + +class MockBackbone(nn.Module): + def __init__(self) -> None: + super(MockBackbone, self).__init__() + + def forward(self, x): + return x + + +class MockRegistry(Registry): + def __init__(self, name: str, parent: Registry = None, scope: str = None) -> None: + super(MockRegistry, self).__init__(name=name, parent=parent, scope=scope) + + +class TestOTXBuilderUtils: + """Check util function in builder.py is working well. + + 1. Check "get_backbone_out_channels" function that checking backbone's output channels is working well. + + 1. Check default input_size = 64 + 2. Check backbone input_size change to 128 + + 2. Check "update_backbone_args" function is working. + + 1. Update backbone args (Case without Required Args) + 2. Update required Args in Backbone (Check Missing Args) + 3. Update Args with out_indices + 4. Update backbone using the backbone name from the backbone list (Check updating with options) + 5. Update backbone using the backbone name from the backbone list (Check updating without options) + 6. Raise ValueError with unexpected backbone + + 3. Check "update_channels" function is working. + + 1. Remove model.neck.in_channels (GlobalAveragePooling) + 2. Update model.neck.in_channels + 3. Update model.decode_head.in_channels & in_index (segmentation case) + 4. Update model.head.in_channels + 5. Raise NotImplementedError with unexpected model key + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.registry = MockRegistry(name="test") + self.backbone = MockBackbone + + @e2e_pytest_unit + def test_get_backbone_out_channels(self) -> None: + """Check "get_backbone_out_channels" function that checking backbone's output channels is working well. + + 1. Check default input_size = 64 + 2. Check backbone input_size change to 128 + """ + + backbone = MockBackbone() + expected_result = [64, 64] + out_channels = get_backbone_out_channels(backbone) + assert out_channels == expected_result + + backbone.input_size = 128 + expected_result = [128, 128] + out_channels = get_backbone_out_channels(backbone) + assert out_channels == expected_result + + @e2e_pytest_unit + def test_update_backbone_args_without_required_args(self) -> None: + """Update backbone args (Case without Required Args).""" + + def mock_init(self, a=1, b=2): + super(MockBackbone, self).__init__() + + self.backbone.__init__ = mock_init + self.registry.register_module(module=self.backbone) + + backbone_config = {"type": "MockBackbone"} + inputs = {"backbone_config": backbone_config, "registry": self.registry, "backend": "mmseg"} + results = update_backbone_args(**inputs) + expected_results = [] + assert results == expected_results + + @e2e_pytest_unit + def test_update_backbone_args_required_args(self) -> None: + """Update required Args in Backbone (Check Missing Args).""" + + def mock_init(self, depth, a=1, b=2): + super(MockBackbone, self).__init__() + + self.backbone.__init__ = mock_init + self.registry.register_module(module=self.backbone, force=True) + backbone_config = {"type": "MockBackbone"} + inputs = {"backbone_config": backbone_config, "registry": self.registry, "backend": "mmseg"} + results = update_backbone_args(**inputs) + expected_results = ["depth"] + assert results == expected_results + + @e2e_pytest_unit + def test_update_backbone_args_with_out_indices(self) -> None: + """Update Args with out_indices.""" + backbone_config = {"type": "MockBackbone", "out_indices": (0, 1, 2, 3)} + self.registry.register_module(module=self.backbone, force=True) + inputs = {"backbone_config": backbone_config, "registry": self.registry, "backend": "mmseg"} + results = update_backbone_args(**inputs) + expected_results = ["depth"] + assert results == expected_results + assert "use_out_indices" in backbone_config + + @e2e_pytest_unit + def test_update_backbone_args_with_option(self) -> None: + """Update backbone using the backbone name from the backbone list (Check updating with options).""" + child_registry = MockRegistry(name="mmseg", parent=self.registry, scope="mmseg") + backbone_config = {"type": "mmseg.ResNet"} + child_registry.register_module(name="ResNet", module=self.backbone, force=True) + inputs = {"backbone_config": backbone_config, "registry": self.registry, "backend": "mmseg"} + results = update_backbone_args(**inputs) + expected_results = [] + assert results == expected_results + expected_depth = 18 + assert "depth" in backbone_config + assert backbone_config["depth"] == expected_depth + + @e2e_pytest_unit + def test_update_backbone_args_without_options(self) -> None: + """Update backbone using the backbone name from the backbone list (Check updating without options).""" + + def mock_init(self, extra): + super(MockBackbone, self).__init__() + + backbone_config = {"type": "mmseg.HRNet"} + self.backbone.__init__ = mock_init + child_registry = MockRegistry(name="mmseg", parent=self.registry, scope="mmseg") + child_registry.register_module(name="HRNet", module=self.backbone, force=True) + inputs = {"backbone_config": backbone_config, "registry": self.registry, "backend": "mmseg"} + results = update_backbone_args(**inputs) + expected_results = ["extra"] + assert results == expected_results + expected_extra_value = "!!!!!!!!!!!INPUT_HERE!!!!!!!!!!!" + assert "extra" in backbone_config + assert backbone_config["extra"] == expected_extra_value + + @e2e_pytest_unit + def test_update_backbone_args_abnormal_backbone_type(self) -> None: + """Raise ValueError with unexpected backbone.""" + backbone_config = {"type": "unexpected"} + inputs = {"backbone_config": backbone_config, "registry": self.registry, "backend": "mmseg"} + with pytest.raises(ValueError): + update_backbone_args(**inputs) + + @e2e_pytest_unit + def test_update_channels_neck(self) -> None: + """Remove model.neck.in_channels.""" + cfg_dict = {"model": {"neck": {"type": "GlobalAveragePooling", "in_channels": 100}}} + model_config = OTXConfig(cfg_dict=cfg_dict) + update_channels(model_config, -1) + assert "in_channels" not in model_config.model.neck + + cfg_dict = {"model": {"neck": {"type": "TestNeck", "in_channels": 100}}} + model_config = OTXConfig(cfg_dict=cfg_dict) + update_channels(model_config, -1) + assert model_config.model.neck.in_channels == -1 + + @e2e_pytest_unit + def test_update_channels_decode_head(self) -> None: + """Update model.decode_head.in_channels & in_index (segmentation case).""" + cfg_dict = {"model": {"decode_head": {"in_channels": (0, 1, 2), "in_index": (0, 1, 2)}}} + out_channels = (10, 20, 30, 40) + model_config = OTXConfig(cfg_dict=cfg_dict) + update_channels(model_config, out_channels) + assert model_config.model.decode_head.in_index == list(range(len(out_channels))) + assert model_config.model.decode_head.in_channels == out_channels + + @e2e_pytest_unit + def test_update_channels_head(self) -> None: + """Update model.head.in_channels.""" + out_channels = (10, 20, 30, 40) + cfg_dict = {"model": {"head": {"in_channels": (0, 1, 2)}}} + model_config = OTXConfig(cfg_dict=cfg_dict) + update_channels(model_config, out_channels) + assert model_config.model.head.in_channels == out_channels + + @e2e_pytest_unit + def test_update_channels_abnormal_inputs(self) -> None: + """Raise NotImplementedError with unexpected model key.""" + out_channels = (10, 20, 30, 40) + cfg_dict = {"model": {"unexpected": {"in_channels": (0, 1, 2)}}} + model_config = OTXConfig(cfg_dict=cfg_dict) + with pytest.raises(NotImplementedError): + update_channels(model_config, out_channels) diff --git a/tests/unit/cli/conftest.py b/tests/unit/cli/conftest.py new file mode 100644 index 00000000000..ddf78627279 --- /dev/null +++ b/tests/unit/cli/conftest.py @@ -0,0 +1,9 @@ +from tempfile import TemporaryDirectory + +import pytest + + +@pytest.fixture +def tmp_dir(): + with TemporaryDirectory() as tmp_dir: + yield tmp_dir diff --git a/tests/unit/cli/manager/test_config_manager.py b/tests/unit/cli/manager/test_config_manager.py new file mode 100644 index 00000000000..eefb8e6e3a6 --- /dev/null +++ b/tests/unit/cli/manager/test_config_manager.py @@ -0,0 +1,677 @@ +import argparse +import os +import tempfile +from pathlib import Path +import os +import shutil + +import pytest +from omegaconf import DictConfig, OmegaConf + +from otx.cli.manager.config_manager import ( + DEFAULT_MODEL_TEMPLATE_ID, + ConfigManager, + set_workspace, +) +from otx.cli.registry import Registry +from otx.cli.utils.errors import ( + CliException, + ConfigValueError, + FileNotExistError, + NotSupportedError, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def test_set_workspace(): + task = "CLASSIFICATION" + root = "/home/user/data" + name = "otx-workspace" + + expected_path = f"{root}/{name}-{task}" + assert set_workspace(task, root, name) == expected_path + + expected_path = f"./{name}-{task}" + assert set_workspace(task, name=name) == expected_path + + +@pytest.fixture +def config_manager(mocker): + args = mocker.MagicMock() + args.template = "." + args.config_path = "path/to/config.yaml" + args.workspace_path = "path/to/workspace" + args.mode = "train" + args.task_type = "classification" + args.train_type = "incremental" + return ConfigManager(args) + + +class TestConfigManager: + def get_default_template(self, otx_root, task_type): + otx_registry = Registry(otx_root).filter(task_type=task_type) + return otx_registry.get(DEFAULT_MODEL_TEMPLATE_ID[task_type.upper()]) + + @e2e_pytest_unit + def test_check_workspace(self, mocker, config_manager): + mock_exists = mocker.patch("otx.cli.manager.config_manager.Path.exists") + # Define the return value of the `os.path.exists` function + mock_exists.return_value = True + # Call the function and check the returned value + assert config_manager.check_workspace() + mock_exists.return_value = False + assert not config_manager.check_workspace() + + @e2e_pytest_unit + def test_get_arg_data_yaml(self, mocker): + # Call the function and check the returned value + args = mocker.MagicMock() + args.template = "." + args.train_data_roots = "path/to/data/train" + args.train_ann_files = None + args.val_data_roots = "path/to/data/val" + args.val_ann_files = None + args.test_data_roots = "path/to/data/test" + args.test_ann_files = None + args.unlabeled_data_roots = None + args.unlabeled_file_list = None + args.mode = "train" + config_manager = ConfigManager(args) + assert config_manager._get_arg_data_yaml() == { + "data": { + "train": {"ann-files": None, "data-roots": "path/to/data/train"}, + "val": {"ann-files": None, "data-roots": "path/to/data/val"}, + "test": {"ann-files": None, "data-roots": None}, + "unlabeled": {"file-list": None, "data-roots": None}, + } + } + config_manager.mode = "test" + assert config_manager._get_arg_data_yaml() == { + "data": { + "train": {"ann-files": None, "data-roots": None}, + "val": {"ann-files": None, "data-roots": None}, + "test": {"ann-files": None, "data-roots": "path/to/data/test"}, + "unlabeled": {"file-list": None, "data-roots": None}, + } + } + + args.unlabeled_data_roots = "path/to/data/unlabeled" + config_manager = ConfigManager(args) + assert config_manager._get_arg_data_yaml() == { + "data": { + "train": {"ann-files": None, "data-roots": "path/to/data/train"}, + "val": {"ann-files": None, "data-roots": "path/to/data/val"}, + "test": {"ann-files": None, "data-roots": None}, + "unlabeled": {"file-list": None, "data-roots": "path/to/data/unlabeled"}, + } + } + + @e2e_pytest_unit + def test_create_empty_data_cfg(self, config_manager): + # Call the function and check the returned value + assert config_manager._create_empty_data_cfg() == { + "data": { + "train": {"ann-files": None, "data-roots": None}, + "val": {"ann-files": None, "data-roots": None}, + "test": {"ann-files": None, "data-roots": None}, + "unlabeled": {"file-list": None, "data-roots": None}, + } + } + + @e2e_pytest_unit + def test_export_data_cfg(self, mocker, config_manager): + # Mock data + data_cfg = { + "data": { + "train": {"ann-files": "path/to/train/ann", "data-roots": "path/to/train/images"}, + "val": {"ann-files": "path/to/val/ann", "data-roots": "path/to/val/images"}, + "test": {"ann-files": "path/to/test/ann", "data-roots": "path/to/test/images"}, + "unlabeled": {"file-list": "path/to/unlabeled/files", "data-roots": "path/to/unlabeled/images"}, + } + } + + # Create temporary file + with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file: + output_path = temp_file.name + + # Mock write_text function + mock_write_text = mocker.patch("pathlib.Path.write_text") + + # Test the function + config_manager._export_data_cfg(data_cfg, output_path) + + # Assertions + mock_write_text.assert_called_once_with(OmegaConf.to_yaml(data_cfg), encoding="utf-8") + + @e2e_pytest_unit + def test_build_workspace(self, mocker): + # Setup + task_type = "CLASSIFICATION" + train_type = "Semisupervised" + workspace_path = "./otx-workspace" + args = mocker.Mock() + args.autosplit = None + args.workspace = workspace_path + config_manager = ConfigManager(args) + template = self.get_default_template(config_manager.otx_root, task_type) + config_manager.template = template + config_manager.train_type = train_type + config_manager.task_type = task_type + + pathlib_mkdir_mock = mocker.patch("pathlib.Path.mkdir") + pathlib_exists_mock = mocker.patch("pathlib.Path.exists", return_value=True) + + set_workspace_mock = mocker.patch("otx.cli.manager.config_manager.set_workspace", return_value=workspace_path) + + parse_model_template_mock = mocker.patch( + "otx.cli.manager.config_manager.parse_model_template", return_value=template + ) + + gen_params_dict_from_args_mock = mocker.patch( + "otx.cli.manager.config_manager.gen_params_dict_from_args", return_value={} + ) + omageconf_load_mock = mocker.patch("otx.cli.manager.config_manager.OmegaConf.load", return_value=template) + omageconf_merge_mock = mocker.patch("otx.cli.manager.config_manager.OmegaConf.merge", return_value=template) + path_write_text_mock = mocker.patch("otx.cli.manager.config_manager.Path.write_text") + + config_manager_check_workspace_mock = mocker.patch( + "otx.cli.manager.config_manager.ConfigManager.check_workspace", return_value=False + ) + + config_manager_copy_config_files_mock = mocker.patch( + "otx.cli.manager.config_manager.ConfigManager._copy_config_files" + ) + shutil_copyfile_mock = mocker.patch("shutil.copyfile") + + # Run + config_manager.build_workspace() + + # Check + set_workspace_mock.assert_called_once_with(task=task_type) + parse_model_template_mock.assert_called_once_with(str(config_manager.workspace_root / "template.yaml")) + + # Calls + pathlib_mkdir_mock.assert_called() + pathlib_exists_mock.assert_called() + + gen_params_dict_from_args_mock.assert_called_once_with(args) + omageconf_load_mock.assert_called() + omageconf_merge_mock.assert_called() + path_write_text_mock.assert_called() + + config_manager_check_workspace_mock.assert_called() + config_manager_copy_config_files_mock.assert_called() + + shutil_copyfile_mock.assert_called() + + @e2e_pytest_unit + def test_update_data_config(self, config_manager, tmp_dir_path): + data_yaml = { + "data": { + "train": {"data-roots": "/path/to/train/data", "ann-files": None}, + "val": {"data-roots": "/path/to/val/data", "ann-files": None}, + "test": {"data-roots": "/path/to/test/data", "ann-files": None}, + "unlabeled": {"data-roots": "/path/to/unlabeled/data", "file-list": "/path/to/unlabeled/filelist"}, + } + } + data_yaml_path = tmp_dir_path / "data.yaml" + OmegaConf.save(data_yaml, str(data_yaml_path)) + + config_manager.update_data_config(OmegaConf.load(str(data_yaml_path))) + assert config_manager.data_config == { + "train_subset": {"data_roots": "/path/to/train/data", "ann_files": None}, + "val_subset": { + "data_roots": "/path/to/val/data", + "ann_files": None, + }, + "test_subset": { + "data_roots": "/path/to/test/data", + "ann_files": None, + }, + "unlabeled_subset": { + "data_roots": "/path/to/unlabeled/data", + "file_list": "/path/to/unlabeled/filelist", + }, + } + + data_yaml["data"]["train"]["data-roots"] = "/path/to/train2/data" + data_yaml_path = tmp_dir_path / "data.yaml" + OmegaConf.save(data_yaml, str(data_yaml_path)) + + config_manager.update_data_config(OmegaConf.load(str(data_yaml_path))) + assert config_manager.data_config == { + "train_subset": {"data_roots": "/path/to/train2/data", "ann_files": None}, + "val_subset": {"data_roots": "/path/to/val/data", "ann_files": None}, + "test_subset": {"data_roots": "/path/to/test/data", "ann_files": None}, + "unlabeled_subset": { + "data_roots": "/path/to/unlabeled/data", + "file_list": "/path/to/unlabeled/filelist", + }, + } + + data_yaml_path = tmp_dir_path / "data.yaml" + data_yaml["data"].pop("unlabeled") + OmegaConf.save(data_yaml, str(data_yaml_path)) + + config_manager.update_data_config(OmegaConf.load(str(data_yaml_path))) + assert config_manager.data_config == { + "train_subset": {"data_roots": "/path/to/train2/data", "ann_files": None}, + "val_subset": {"data_roots": "/path/to/val/data", "ann_files": None}, + "test_subset": {"data_roots": "/path/to/test/data", "ann_files": None}, + "unlabeled_subset": { + "data_roots": "/path/to/unlabeled/data", + "file_list": "/path/to/unlabeled/filelist", + }, + } + + @e2e_pytest_unit + def test_get_hyparams_config(self, mocker): + mock_hyper_parameters = { + "learning_rate": { + "type": "FLOAT", + "default_value": "0.01", + "max_value": "1.0", + "min_value": "0.0", + "affects_outcome_of": ["TRAINING"], + }, + "batch_size": { + "type": "INTEGER", + "default_value": "16", + "max_value": "128", + "min_value": "1", + "affects_outcome_of": ["TRAINING", "TESTING"], + }, + } + mock_template = DictConfig({"hyper_parameters": DictConfig({"data": mock_hyper_parameters})}) + + parser = argparse.ArgumentParser() + parser.add_argument("--template") + parser.add_argument( + "--learning_rate", + dest="params.learning_rate", + ) + parser.add_argument( + "--batch_size", + dest="params.batch_size", + ) + mock_input = ["--learning_rate", "0.5", "--batch_size", "8"] + mock_args = parser.parse_args(mock_input) + expected_hyper_parameters = { + "learning_rate": { + "type": "FLOAT", + "default_value": "0.01", + "max_value": "1.0", + "min_value": "0.0", + "affects_outcome_of": ["TRAINING"], + "value": "0.5", + }, + "batch_size": { + "type": "INTEGER", + "default_value": "16", + "max_value": "128", + "min_value": "1", + "affects_outcome_of": ["TRAINING", "TESTING"], + "value": "8", + }, + } + mock_create = mocker.patch("otx.cli.manager.config_manager.create", return_value=expected_hyper_parameters) + + config_manager = ConfigManager(mock_args) + config_manager.template = mock_template + config_manager.get_hyparams_config() + mock_create.assert_called_once_with(expected_hyper_parameters) + + @e2e_pytest_unit + def test_data_config_file_path(self, mocker, tmp_dir_path): + parser = argparse.ArgumentParser() + parser.add_argument("--template") + parser.add_argument("--data") + args = parser.parse_args([]) + config_manager = ConfigManager(args) + + # set up test workspace + workspace_root = tmp_dir_path / "test_data_config" + config_manager.workspace_root = workspace_root + # workspace_root.mkdir(exist_ok=True, parents=True) + assert config_manager.data_config_file_path == workspace_root / "data.yaml" + + # expected file path + mock_exists = mocker.patch("otx.cli.manager.config_manager.Path.exists", return_value=False) + expected_file_path = tmp_dir_path / "data.yaml" + args = parser.parse_args(["--data", str(expected_file_path)]) + config_manager.args = args + with pytest.raises(FileNotExistError): + config_manager.data_config_file_path + + mock_exists.return_value = True + assert config_manager.data_config_file_path == expected_file_path + + @e2e_pytest_unit + def test_configure_template(self, mocker): + # Given + mock_args = mocker.MagicMock() + mock_args.train_data_roots = ["/path/to/train/data"] + mock_args.template = None + mock_workspace_root = mocker.MagicMock() + mock_workspace_root.exists.return_value = True + + mock_template = DictConfig({"name": "template_name", "task_type": "CLASSIFICATION"}) + mock_check_workspace = mocker.patch( + "otx.cli.manager.config_manager.ConfigManager.check_workspace", return_value=True + ) + mocker.patch("otx.cli.manager.config_manager.ConfigManager._get_template", return_value=mock_template) + mocker.patch("otx.cli.manager.config_manager.ConfigManager._get_train_type", return_value="Incremental") + mock_parse_model_template = mocker.patch( + "otx.cli.manager.config_manager.parse_model_template", return_value=mock_template + ) + + config_manager = ConfigManager(args=mock_args, workspace_root=mock_workspace_root) + + # When + config_manager.configure_template() + + # Then + assert config_manager.task_type == "CLASSIFICATION" + assert config_manager.model == "template_name" + assert config_manager.train_type == "Incremental" + + config_manager.mode = "build" + mocker.patch("otx.cli.manager.config_manager.ConfigManager._check_rebuild", return_value=True) + config_manager.configure_template() + assert config_manager.rebuild + assert config_manager.task_type == "CLASSIFICATION" + assert config_manager.model == "template_name" + assert config_manager.train_type == "Incremental" + + mock_check_workspace.return_value = False + mocker.patch("pathlib.Path.exists", return_value=True) + config_manager.template = "test/template" + config_manager.configure_template() + mock_parse_model_template.assert_called_with("test/template") + + config_manager.template = None + config_manager.task_type = None + mock_check_workspace = mocker.patch( + "otx.cli.manager.config_manager.ConfigManager.auto_task_detection", return_value="CLASSIFICATION" + ) + config_manager.configure_template() + assert config_manager.task_type == "CLASSIFICATION" + assert config_manager.model == "template_name" + assert config_manager.train_type == "Incremental" + + mocker.patch("otx.cli.manager.config_manager.ConfigManager.check_workspace", return_value=False) + mocker.patch("otx.cli.manager.config_manager.hasattr", return_value=False) + empty_args = mocker.MagicMock() + empty_args.template = None + config_manager = ConfigManager(args=empty_args, mode="train") + config_manager.template = None + config_manager.task_type = None + with pytest.raises(ConfigValueError, match="Can't find the argument 'train_data_roots'"): + config_manager.configure_template() + + config_manager = ConfigManager(args=empty_args, mode="eval") + with pytest.raises(ConfigValueError, match="No appropriate template or task-type was found."): + config_manager.configure_template() + + @e2e_pytest_unit + def test__check_rebuild(self, mocker): + mock_template = mocker.MagicMock() + mock_template.task_type = "CLASSIFICATION" + + mock_args = mocker.MagicMock() + mock_args.mode = "build" + mock_args.task = "DETECTION" + mock_args.template = mock_template + + config_manager = ConfigManager(mock_args) + with pytest.raises(NotSupportedError): + config_manager._check_rebuild() + + config_manager.template.task_type = "DETECTION" + config_manager.args.model = None + config_manager.args.train_type = "" + assert not config_manager._check_rebuild() + + config_manager.args.model = "SSD" + config_manager.template.name = "MobileNetV2-ATSS" + config_manager.args.train_type = "Semisupervised" + assert config_manager._check_rebuild() + + @e2e_pytest_unit + @pytest.mark.parametrize("mode", ["build", "train", "eval"]) + @pytest.mark.parametrize("task_type", ["", "VISUAL_PROMPTING"]) + def test_configure_data_config(self, mocker, mode: str, task_type: str): + data_yaml = { + "data": { + "train": {"ann-files": None, "data-roots": "train/data/roots"}, + "val": {"ann-files": None, "data-roots": None}, + "test": {"ann-files": None, "data-roots": None}, + "unlabeled": {"file-list": None, "data-roots": None}, + } + } + mock_configure_dataset = mocker.patch( + "otx.cli.manager.config_manager.configure_dataset", return_value=data_yaml + ) + mock_auto_split = mocker.patch("otx.cli.manager.config_manager.ConfigManager.auto_split_data", return_value={}) + mock_get_data_yaml = mocker.patch( + "otx.cli.manager.config_manager.ConfigManager._get_arg_data_yaml", return_value=data_yaml + ) + mock_save_data = mocker.patch("otx.cli.manager.config_manager.ConfigManager._save_data") + mock_export_data_cfg = mocker.patch("otx.cli.manager.config_manager.ConfigManager._export_data_cfg") + mock_update_data_config = mocker.patch("otx.cli.manager.config_manager.ConfigManager.update_data_config") + + mock_args = mocker.MagicMock() + mock_args.mode = mode + + config_manager = ConfigManager(mock_args) + config_manager.train_type = "Incremental" + config_manager.task_type = task_type + config_manager.mode = mode + + config_manager.configure_data_config(update_data_yaml=True) + + mock_configure_dataset.assert_called_once() + mock_export_data_cfg.assert_called_once() + mock_update_data_config.assert_called_once_with(data_yaml) + if mode in ("train", "build"): + mock_auto_split.assert_called_once() + mock_get_data_yaml.assert_called_once() + mock_save_data.assert_called_once() + else: + # mode == "eval" + mock_auto_split.assert_not_called() + mock_get_data_yaml.assert_not_called() + mock_save_data.assert_not_called() + + if task_type == "VISUAL_PROMPTING" and mode == "train": + assert data_yaml.get("options", False) + + @e2e_pytest_unit + def test__get_train_type_incremental(self, mocker): + """General usage""" + mock_args = mocker.MagicMock() + config_manager = ConfigManager(args=mock_args) + config_manager.mode = "build" + config_manager.args.train_type = "Incremental" + assert config_manager._get_train_type() == "Incremental" + # test train_type unlabeled root is None + # train data root ordinary dataset folder + config_manager.args.train_type = None + config_manager.args.unlabeled_data_roots = None + config_manager.args.train_data_roots = "tests/assets/classification_dataset" + config_manager.args.val_data_roots = "tests/assets/classification_dataset" + assert config_manager._get_train_type(ignore_args=False) == "Incremental" + + mock_template = mocker.MagicMock() + mock_template.hyper_parameters.parameter_overrides = { + "algo_backend": {"train_type": {"default_value": "Incremental"}} + } + config_manager.template = mock_template + assert config_manager._get_train_type(ignore_args=True) == "Incremental" + + config_manager.template.hyper_parameters.parameter_overrides = {} + assert config_manager._get_train_type(ignore_args=True) == "Incremental" + # train_data_roots isn't exist + config_manager.args.train_type = None + config_manager.args.train_data_roots = "non_exist_dir" + with pytest.raises(ValueError): + config_manager._get_train_type(ignore_args=False) + + # test val_data_roots is None, train-data-roots contains full dataset format + config_manager.args.val_data_roots = None + config_manager.args.train_data_roots = "tests/assets/classification_dataset" + # auto-split + assert config_manager._get_train_type(ignore_args=False) == "Incremental" + + @e2e_pytest_unit + def test__get_train_type_semisuprvised(self, mocker): + """Auto train type detection""" + mock_args = mocker.MagicMock() + config_manager = ConfigManager(args=mock_args) + config_manager.args.train_data_roots = "tests/assets/classification_dataset" + config_manager.args.train_type = "Semisupervised" + assert config_manager._get_train_type() == "Semisupervised" + # test train_type unlabeled root is not None + config_manager.args.train_type = None + config_manager.args.unlabeled_data_roots = "tests/assets/unlabeled_dataset/a" + assert config_manager._get_train_type(ignore_args=False) == "Semisupervised" + # test train_type unlabeled root is not exist + config_manager.args.unlabeled_data_roots = "non_exist_dir" + with pytest.raises(ValueError): + config_manager._get_train_type(ignore_args=False) + tempdir = tempfile.mkdtemp() + # unlabeled root is empty + config_manager.args.unlabeled_data_roots = str(tempdir) + with pytest.raises(ValueError): + config_manager._get_train_type(ignore_args=False) + Path(f"{tempdir}/file.jpg").touch() + # number of images in unlabeled root is unsufficient + assert config_manager._get_train_type(ignore_args=False) == "Incremental" + Path(f"{tempdir}/file1.jpg").touch() + Path(f"{tempdir}/file2.jpg").touch() + assert config_manager._get_train_type(ignore_args=False) == "Semisupervised" + + @e2e_pytest_unit + def test__get_train_type_selfsupervised(self, mocker): + """Auto train type detection""" + mock_args = mocker.MagicMock() + config_manager = ConfigManager(args=mock_args) + config_manager.args.train_type = "Selfsupervised" + assert config_manager._get_train_type() == "Selfsupervised" + config_manager.args.train_type = None + config_manager.args.unlabeled_data_roots = None + # test folder with only images + config_manager.args.train_data_roots = "tests/assets/unlabeled_dataset/a" + config_manager.args.val_data_roots = None + assert config_manager._get_train_type(ignore_args=False) == "Selfsupervised" + # test val_data_roots is not None + config_manager.args.val_data_roots = "tests/assets/unlabeled_dataset" + assert config_manager._get_train_type(ignore_args=False) == "Selfsupervised" + + @e2e_pytest_unit + def test_auto_task_detection(self, mocker): + mock_args = mocker.MagicMock() + config_manager = ConfigManager(args=mock_args) + with pytest.raises(CliException): + config_manager.auto_task_detection("") + + mock_get_data_format = mocker.patch( + "otx.cli.manager.config_manager.DatasetManager.get_data_format", return_value="Unexpected" + ) + with pytest.raises(ConfigValueError): + config_manager.auto_task_detection("data/roots") + + mock_get_data_format.return_value = "coco" + assert config_manager.auto_task_detection("data/roots") == "DETECTION" + + @e2e_pytest_unit + def test_auto_split_data(self, mocker): + mock_get_data_format = mocker.patch( + "otx.cli.manager.config_manager.DatasetManager.get_data_format", return_value="coco" + ) + mock_import_dataset = mocker.patch( + "otx.cli.manager.config_manager.DatasetManager.import_dataset", return_value=None + ) + mock_get_train_dataset = mocker.patch( + "otx.cli.manager.config_manager.DatasetManager.get_train_dataset", return_value="train_dataset" + ) + mock_get_val_dataset = mocker.patch( + "otx.cli.manager.config_manager.DatasetManager.get_val_dataset", return_value="val_dataset" + ) + mock_auto_split = mocker.patch( + "otx.cli.manager.config_manager.DatasetManager.auto_split", + return_value={"train": "auto_train", "val": "auto_val"}, + ) + + mock_args = mocker.MagicMock() + config_manager = ConfigManager(args=mock_args) + assert config_manager.auto_split_data("test_data_root", task="DETECTION") == { + "train": "train_dataset", + "val": "val_dataset", + } + + mock_get_val_dataset.return_value = None + assert config_manager.auto_split_data("test_data_root", task="DETECTION") == { + "train": "auto_train", + "val": "auto_val", + } + + mock_get_data_format.return_value = "Unexpected" + assert config_manager.auto_split_data("test_data_root", task="DETECTION") is None + mock_get_data_format.assert_called() + mock_import_dataset.assert_called() + mock_get_train_dataset.assert_called() + mock_get_val_dataset.assert_called() + mock_auto_split.assert_called() + + @e2e_pytest_unit + def test_get_dataset_config(self, mocker): + mock_args = mocker.MagicMock() + config_manager = ConfigManager(args=mock_args) + config_manager.task_type = "DETECTION" + config_manager.data_config = { + "train_subset": {"data_roots": "train_path"}, + "val_subset": {"data_roots": "val_path"}, + "test_subset": {"data_roots": "test_path"}, + } + dataset_config = config_manager.get_dataset_config(["train", "val", "test"]) + assert dataset_config["task_type"] == "DETECTION" + assert "train_data_roots" in dataset_config + assert "val_data_roots" in dataset_config + assert "test_data_roots" in dataset_config + + +class TestConfigManagerEncryptionKey: + encryption_key = "dummy_key" + + @pytest.fixture(scope="function") + def fxt_with_args(self, config_manager): + config_manager.args.encryption_key = self.encryption_key + return config_manager + + @pytest.fixture + def fxt_with_envs(self, config_manager, mocker): + config_manager.args.encryption_key = None + k = mocker.patch.dict(os.environ, {"ENCRYPTION_KEY": self.encryption_key}) + yield config_manager + + @pytest.fixture + def fxt_with_both(self, fxt_with_args, mocker): + k = mocker.patch.dict(os.environ, {"ENCRYPTION_KEY": self.encryption_key}) + yield fxt_with_args + + @e2e_pytest_unit + @pytest.mark.parametrize( + "testcase, expected", + [("fxt_with_args", True), ("fxt_with_envs", True), ("config_manager", False)], + ) + def test_encryption_key(self, testcase, expected, request): + config_manager = request.getfixturevalue(testcase) + + actual = config_manager.encryption_key == self.encryption_key + assert actual == expected + + @e2e_pytest_unit + def test_encryption_key_error_raise(self, fxt_with_both): + with pytest.raises(ValueError): + assert fxt_with_both.encryption_key == self.encryption_key diff --git a/tests/unit/cli/registry/test_cli_registry.py b/tests/unit/cli/registry/test_cli_registry.py new file mode 100644 index 00000000000..1ca0ab1ebc5 --- /dev/null +++ b/tests/unit/cli/registry/test_cli_registry.py @@ -0,0 +1,108 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import pytest + +from otx.cli.registry import Registry, find_and_parse_model_template +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestRegistry: + @pytest.fixture + def mock_templates(self, mocker): + template1 = mocker.MagicMock(framework="OTX001", task_type="Classification", model_template_id="001") + template2 = mocker.MagicMock(framework="OTX002", task_type="Segmentation", model_template_id="002") + template3 = mocker.MagicMock(framework="OTX001", task_type="Detection", model_template_id="003") + return [template1, template2, template3] + + @e2e_pytest_unit + def test_init(self, mocker, mock_templates): + mock_glob = mocker.patch("glob.glob") + mock_glob.return_value = ["./template.yaml"] + mock_abspath = mocker.patch("os.path.abspath") + mock_abspath.return_value = "./template.yaml" + mock_parse_model_template = mocker.patch("otx.cli.registry.registry.parse_model_template") + mock_parse_model_template.return_value = mock_templates[0] + + r = Registry(templates_dir=".") + assert r.templates == [mock_templates[0]] + + r = Registry(templates_dir=".", experimental=True) + assert r.templates == [mock_templates[0]] * 2 + + r = Registry(templates=mock_templates) + assert r.templates == mock_templates + + @e2e_pytest_unit + def test_init_empty_raise_RuntimeError(self): + with pytest.raises(RuntimeError): + Registry() + + @e2e_pytest_unit + def test_filter(self, mock_templates): + """Check TestRegistry.filter function working well. + + + 1. Create an instance of Registry with the mock templates + 2. Test filtering by framework + 3. Test filtering by task_type + 4. Test filtering by both framework and task_type + """ + registry = Registry(templates=mock_templates) + + filtered_registry = registry.filter(framework="OTX001") + assert len(filtered_registry.templates) == 2 + assert filtered_registry.templates[0].task_type == "Classification" + assert filtered_registry.templates[1].task_type == "Detection" + filtered_registry = registry.filter(framework="OTX002") + assert len(filtered_registry.templates) == 1 + assert filtered_registry.templates[0].task_type == "Segmentation" + + filtered_registry = registry.filter(task_type="Detection") + assert len(filtered_registry.templates) == 1 + assert filtered_registry.templates[0].task_type == "Detection" + + filtered_registry = registry.filter(framework="OTX002", task_type="Segmentation") + assert len(filtered_registry.templates) == 1 + assert filtered_registry.templates[0].task_type == "Segmentation" + + @e2e_pytest_unit + def test_get(self, mock_templates): + registry = Registry(templates=mock_templates) + template = registry.get("003") + assert template.task_type == "Detection" + + @e2e_pytest_unit + def test_get_unexpected(self, mock_templates): + registry = Registry(templates=mock_templates) + with pytest.raises(ValueError): + registry.get("004") + + @e2e_pytest_unit + def test_get_backbones(self, mocker, mock_templates): + mock_get_backbone_list = mocker.patch("otx.cli.registry.registry.get_backbone_list") + mock_get_backbone_list.return_value = ["resnet50", "resnet101"] + r = Registry(templates=mock_templates) + assert r.get_backbones(["torch", "tensorflow"]) == { + "torch": ["resnet50", "resnet101"], + "tensorflow": ["resnet50", "resnet101"], + } + + +@e2e_pytest_unit +def test_find_and_parse_model_template(mocker): + mock_template = mocker.MagicMock(framework="test", task_type="test", model_template_id="001") + mock_parse_model_template = mocker.patch("otx.cli.registry.registry.parse_model_template") + mock_parse_model_template.return_value = mock_template + + assert find_and_parse_model_template("001") == mock_template + + +@e2e_pytest_unit +def test_find_and_parse_model_template_exists_true(mocker): + mock_template = mocker.MagicMock(framework="test", task_type="test", model_template_id="001") + mock_parse_model_template = mocker.patch("otx.cli.registry.registry.parse_model_template") + mock_parse_model_template.return_value = mock_template + mock_exists = mocker.patch("os.path.exists") + mock_exists.return_value = True + assert find_and_parse_model_template("001") == mock_template diff --git a/tests/unit/cli/test_cli.py b/tests/unit/cli/test_cli.py deleted file mode 100644 index 0eea9f1bc57..00000000000 --- a/tests/unit/cli/test_cli.py +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations - -import sys - -import pytest -import yaml -from otx.cli import OTXCLI, main - - -class TestOTXCLI: - def test_init(self, mocker) -> None: - # Test that main function runs with errors -> return 2 - argv = ["otx"] - with mocker.patch.object(sys, "argv", argv) and pytest.raises(SystemExit, match="2"): - OTXCLI() - - argv = ["otx", "-h"] - with mocker.patch.object(sys, "argv", argv) and pytest.raises(SystemExit, match="0"): - OTXCLI() - - def test_main(self, mocker) -> None: - argv = ["otx"] - with mocker.patch.object(sys, "argv", argv) and pytest.raises(SystemExit, match="2"): - main() - - argv = ["otx", "-h"] - with mocker.patch.object(sys, "argv", argv) and pytest.raises(SystemExit, match="0"): - main() - - @pytest.fixture() - def fxt_train_help_command(self, monkeypatch) -> None: - argv = ["otx", "train", "-h"] - monkeypatch.setattr("sys.argv", argv) - - def test_train_help_command(self, fxt_train_help_command) -> None: - # Test that main function runs with help -> return 0 - with pytest.raises(SystemExit, match="0"): - OTXCLI() - - def test_init_parser(self, mocker) -> None: - mocker.patch("otx.cli.cli.OTXCLI.__init__", return_value=None) - cli = OTXCLI() - parser = cli.init_parser() - assert parser.__class__.__name__ == "ArgumentParser" - argument_list = [action.dest for action in parser._actions] - expected_argument = ["help", "version"] - assert argument_list == expected_argument - - def test_subcommand_parser(self, mocker) -> None: - mocker.patch("otx.cli.cli.OTXCLI.__init__", return_value=None) - cli = OTXCLI() - parser, _ = cli.engine_subcommand_parser(subcommand="train") - assert parser.__class__.__name__ == "ArgumentParser" - argument_list = [action.dest for action in parser._actions] - expected_argument = [ - "help", - "verbose", - "config", - "print_config", - "data_root", - "task", - "seed", - "callback_monitor", - ] - for args in expected_argument: - assert args in argument_list - - def test_add_subcommands(self, mocker) -> None: - mocker.patch("otx.cli.cli.OTXCLI.__init__", return_value=None) - cli = OTXCLI() - cli.parser = cli.init_parser() - cli._subcommand_method_arguments = {} - cli.add_subcommands() - assert cli._subcommand_method_arguments.keys() == cli.engine_subcommands().keys() - - @pytest.fixture() - def fxt_train_command(self, monkeypatch, tmpdir) -> list[str]: - argv = [ - "otx", - "train", - "--config", - "src/otx/recipe/detection/atss_mobilenetv2.yaml", - "--data_root", - "tests/assets/car_tree_bug", - "--model.num_classes", - "3", - "--work_dir", - str(tmpdir), - ] - monkeypatch.setattr("sys.argv", argv) - return argv - - def test_instantiate_classes(self, fxt_train_command, mocker) -> None: - mock_run = mocker.patch("otx.cli.OTXCLI.run") - cli = OTXCLI() - assert mock_run.call_count == 1 - cli.instantiate_classes() - - from otx.core.model.entity.base import OTXModel - - assert isinstance(cli.model, OTXModel) - - from otx.core.data.module import OTXDataModule - - assert isinstance(cli.datamodule, OTXDataModule) - - from otx.engine import Engine - - assert isinstance(cli.engine, Engine) - - assert cli.datamodule == cli.engine.datamodule - assert cli.model == cli.engine.model - - @pytest.fixture() - def fxt_print_config_scheduler_override_command(self, monkeypatch) -> None: - argv = [ - "otx", - "train", - "--config", - "src/otx/recipe/detection/atss_mobilenetv2.yaml", - "--data_root", - "tests/assets/car_tree_bug", - "--scheduler.monitor", - "val/test_f1", - "--print_config", - ] - monkeypatch.setattr("sys.argv", argv) - - def test_print_config_scheduler_override_command(self, fxt_print_config_scheduler_override_command, capfd) -> None: - # Test that main function runs with help -> return 0 - with pytest.raises(SystemExit, match="0"): - OTXCLI() - out, _ = capfd.readouterr() - result_config = yaml.safe_load(out) - expected_str = """ - scheduler: - - class_path: otx.algo.schedulers.LinearWarmupScheduler - init_args: - num_warmup_steps: 3 - interval: step - - class_path: lightning.pytorch.cli.ReduceLROnPlateau - init_args: - monitor: val/test_f1 - mode: max - factor: 0.1 - patience: 4 - threshold: 0.0001 - threshold_mode: rel - cooldown: 0 - min_lr: 0.0 - eps: 1.0e-08 - verbose: false - """ - expected_config = yaml.safe_load(expected_str) - assert expected_config["scheduler"] == result_config["scheduler"] - - @pytest.fixture() - def fxt_metric_override_command(self, monkeypatch) -> None: - argv = [ - "otx", - "train", - "--config", - "src/otx/recipe/detection/atss_mobilenetv2.yaml", - "--data_root", - "tests/assets/car_tree_bug", - "--metric", - "otx.core.metrics.fmeasure.FMeasure", - "--print_config", - ] - monkeypatch.setattr("sys.argv", argv) - - def test_print_metric_override_command(self, fxt_metric_override_command, capfd) -> None: - # Test that main function runs with help -> return 0 - with pytest.raises(SystemExit, match="0"): - OTXCLI() - out, _ = capfd.readouterr() - result_config = yaml.safe_load(out) - expected_str = """ - metric: - - class_path: otx.core.metrics.fmeasure.FMeasure - """ - expected_config = yaml.safe_load(expected_str) - assert expected_config["metric"][0]["class_path"] == result_config["metric"]["class_path"] diff --git a/tests/unit/cli/test_install.py b/tests/unit/cli/test_install.py deleted file mode 100644 index a55c587a8a9..00000000000 --- a/tests/unit/cli/test_install.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import pytest -from _pytest.monkeypatch import MonkeyPatch -from jsonargparse import ArgumentParser -from otx.cli.install import add_install_parser, otx_install -from pkg_resources import Requirement -from pytest_mock.plugin import MockerFixture - - -class TestInstall: - @pytest.fixture(autouse=True) - def setup(self, mocker: MockerFixture) -> None: - requirements_dict = { - "base": [Requirement.parse("torch==2.0.0"), Requirement.parse("pytorchcv")], - "openvino": [Requirement.parse("openvino")], - "mmlab": [Requirement.parse("mmpretrain")], - "anomaly": [Requirement.parse("anomalib")], - "api": [Requirement.parse("test1")], - } - mocker.patch("otx.cli.install.get_requirements", return_value=requirements_dict) - - def test_add_install_parser(self) -> None: - parser = ArgumentParser() - parser_subcommands = parser.add_subcommands() - add_install_parser(parser_subcommands) - assert parser_subcommands.choices.get("install") is not None - install_parser = parser_subcommands.choices.get("install") - argument_list = [action.dest for action in install_parser._actions] - expected_argument = ["help", "option", "verbose", "do_not_install_torch"] - assert argument_list == expected_argument - - def test_install_extra(self, mocker: MockerFixture) -> None: - mock_create_command = mocker.patch("pip._internal.commands.create_command") - mock_create_command.return_value.main.return_value = 0 - status_code = otx_install(option="dev") - assert status_code == mock_create_command.return_value.main.return_value - argument_call_list = mock_create_command.return_value.main.call_args_list[-1][0][-1] - assert "pytorchcv" in argument_call_list - assert "openvino" not in argument_call_list - assert "anomalib" not in argument_call_list - - def test_install_full(self, mocker: MockerFixture, monkeypatch: MonkeyPatch) -> None: - mock_create_command = mocker.patch("pip._internal.commands.create_command") - mock_create_command.return_value.main.return_value = 0 - mock_mim_installation = mocker.patch("otx.cli.install.mim_installation") - mock_mim_installation.return_value = 0 - - status_code = otx_install("full") - assert status_code == mock_create_command.return_value.main.return_value - mock_create_command.assert_called_once_with("install") - argument_call_list = mock_create_command.return_value.main.call_args_list[-1][0][-1] - assert "openvino" in argument_call_list - assert "pytorchcv" in argument_call_list - assert "anomalib" in argument_call_list - assert "mmpretrain" not in argument_call_list - mm_argument_call_list = mock_mim_installation.call_args_list[-1][0][-1] - assert "mmpretrain" in mm_argument_call_list diff --git a/tests/unit/cli/tools/test_build.py b/tests/unit/cli/tools/test_build.py new file mode 100644 index 00000000000..7d43a77f54a --- /dev/null +++ b/tests/unit/cli/tools/test_build.py @@ -0,0 +1,103 @@ +import argparse + +import pytest + +from otx.cli.tools import build as target_package +from otx.cli.tools.build import get_args, main +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_get_args(mocker): + mock_options = { + "--train-data-roots": "train/data/root", + "--val-data-roots": "val/data/root", + "--test-data-roots": "test/data/root", + "--unlabeled-data-roots": "unlabeled/data/root", + "--unlabeled-file-list": "unlabeled/file/list", + "--task": "detection", + "--train-type": "Semisupervised", + "--workspace": "work/dir/path", + "--model": "SSD", + "--backbone": "torchvision.resnet18", + } + mock_command = ["otx"] + for key, value in mock_options.items(): + mock_command.extend([key, value]) + + mocker.patch("sys.argv", mock_command) + mocker.patch.object(target_package, "get_parser_and_hprams_data", return_value=[argparse.ArgumentParser(), {}, []]) + + parsed_args = get_args() + + assert parsed_args.train_data_roots == "train/data/root" + assert parsed_args.val_data_roots == "val/data/root" + assert parsed_args.test_data_roots == "test/data/root" + assert parsed_args.unlabeled_data_roots == "unlabeled/data/root" + assert parsed_args.unlabeled_file_list == "unlabeled/file/list" + assert parsed_args.workspace == "work/dir/path" + assert parsed_args.task == "detection" + assert parsed_args.train_type == "Semisupervised" + assert parsed_args.model == "SSD" + assert parsed_args.backbone == "torchvision.resnet18" + + +@pytest.fixture +def mock_args(mocker, tmp_path): + mock_args = mocker.MagicMock() + mock_args.train_data_roots = None + mock_args.val_data_roots = None + mock_args.test_data_roots = None + mock_args.unlabeled_data_roots = None + mock_args.unlabeled_file_list = None + mock_args.task = "" + mock_args.train_type = "incremental" + mock_args.workspace = tmp_path / "work_dir" + mock_args.model = "" + mock_args.backbone = "torchvision.resnet18" + + def mock_contains(self, val): + return val in self.__dict__ + + mock_args.__contains__ = mock_contains + mock_get_args = mocker.patch("otx.cli.tools.build.get_args") + mock_get_args.return_value = mock_args + + return mock_args + + +@pytest.fixture +def mock_config_manager(mocker): + mock_config_manager = mocker.patch.object(target_package, "ConfigManager") + mock_template = mocker.MagicMock() + mock_template.name = "fake_name" + mock_config_manager.return_value.mode = "build" + mock_config_manager.return_value.template = mock_template + mock_config_manager.return_value.check_workspace.return_value = True + mock_config_manager.return_value.get_dataset_config.return_value = {} + mock_config_manager.return_value.get_hyparams_config.return_value = {} + + return mock_config_manager + + +@e2e_pytest_unit +def test_main(mocker, mock_config_manager, mock_args): + # Mock argparse namespace + mock_builder = mocker.patch("otx.cli.builder.Builder") + + # Call main function + result = main() + + # Check return value + assert result == {"retcode": 0, "task_type": ""} + + # Check ConfigManager constructor call + mock_config_manager.assert_called_once() + + # Check ConfigManager method calls + mock_config_manager.return_value.configure_template.assert_called_once_with(model="") + mock_config_manager.return_value.build_workspace.assert_called_once() + mock_config_manager.return_value.configure_data_config.assert_called_once_with() + + # Check Builder constructor call + mock_builder.assert_called_once_with() diff --git a/tests/unit/cli/tools/test_cli.py b/tests/unit/cli/tools/test_cli.py new file mode 100644 index 00000000000..d66346abb8e --- /dev/null +++ b/tests/unit/cli/tools/test_cli.py @@ -0,0 +1,44 @@ +"""Unit tests for OTX Cli""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import sys + +import pytest + +from otx.cli.tools import cli +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestCli: + @e2e_pytest_unit + def test_cli(self): + backup_argv = sys.argv + # invalid inputs + sys.argv = ["otx"] + with pytest.raises(SystemExit) as e: + cli.main() + assert e.type == SystemExit, f"{e}" + sys.argv = ["otx", None] + with pytest.raises(SystemExit) as e: + cli.main() + assert e.type == SystemExit, f"{e}" + sys.argv = ["otx", 0] + with pytest.raises(SystemExit) as e: + cli.main() + assert e.type == SystemExit, f"{e}" + sys.argv = ["otx", ""] + with pytest.raises(SystemExit) as e: + cli.main() + assert e.type == SystemExit, f"{e}" + # not a string value + sys.argv = ["otx", -1] + with pytest.raises(Exception) as e: + cli.main() + assert e.type == TypeError, f"{e}" + sys.argv = ["otx", b"\x00"] + with pytest.raises(Exception) as e: + cli.main() + assert e.type == TypeError, f"{e}" + sys.argv = backup_argv diff --git a/tests/unit/cli/tools/test_deploy.py b/tests/unit/cli/tools/test_deploy.py new file mode 100644 index 00000000000..323294fd043 --- /dev/null +++ b/tests/unit/cli/tools/test_deploy.py @@ -0,0 +1,103 @@ +import argparse +from pathlib import Path + +import pytest + +from otx.cli.tools import deploy as target_package +from otx.cli.tools.deploy import get_args, main +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_get_args(mocker): + mocker.patch("sys.argv", ["otx", "--load-weights", "load_weights", "--output", "output"]) + mocker.patch.object( + target_package, "get_parser_and_hprams_data", return_value=[argparse.ArgumentParser(), "fake", "fake"] + ) + + parsed_args = get_args() + + assert parsed_args.load_weights == "load_weights" + assert parsed_args.output == "output" + + +@pytest.fixture +def mock_args(mocker, tmp_dir): + mock_args = mocker.MagicMock() + mock_args.load_weights = "fake.bin" + mock_args.output = tmp_dir + + def mock_contains(self, val): + return val in self.__dict__ + + mock_args.__contains__ = mock_contains + mock_get_args = mocker.patch("otx.cli.tools.deploy.get_args") + mock_get_args.return_value = mock_args + + return mock_args + + +@pytest.fixture +def mock_task(mocker): + mock_task_class = mocker.MagicMock() + mock_task = mocker.MagicMock() + mock_task_class.return_value = mock_task + mocker.patch.object(target_package, "get_impl_class", return_value=mock_task_class) + + return mock_task + + +@pytest.fixture +def mock_config_manager(mocker): + mock_config_manager = mocker.patch.object(target_package, "ConfigManager") + mock_template = mocker.MagicMock() + mock_template.name = "fake_name" + mock_config_manager.return_value.template = mock_template + + return mock_config_manager + + +@e2e_pytest_unit +def test_main(mocker, mock_args, mock_task, mock_config_manager, tmp_dir): + # prepare + mocker.patch.object(target_package, "TaskEnvironment") + mocker.patch.object(target_package, "create") + mocker.patch.object(target_package, "read_model") + mocker.patch.object(target_package, "read_label_schema") + mock_deployed_model = mocker.MagicMock() + mock_deployed_model.exportable_code = b"exportable_code" + mocker.patch.object(target_package, "ModelEntity", return_value=mock_deployed_model) + + # run + ret = main() + + # check + assert ret["retcode"] == 0 + assert ret["template"] == "fake_name" + mock_task.deploy.assert_called_once_with(mock_deployed_model) + with (Path(tmp_dir) / "openvino.zip").open() as f: + val = f.readline() + assert val == "exportable_code" + + +@e2e_pytest_unit +def test_main_wrong_workspace(mock_args, mock_config_manager): + mock_args.load_weights = "" + mock_config_manager.check_workspace.return_value = True + + with pytest.raises(RuntimeError): + main() + + +@e2e_pytest_unit +@pytest.mark.parametrize("load_weights", ["fake.jpg", "fake.png"]) +def test_main_wrong_laod_weight(mocker, load_weights): + # prepare + mock_agrs = mocker.MagicMock() + mock_agrs.load_weights = load_weights + mocker.patch.object(target_package, "get_args", return_value=mock_agrs) + mocker.patch.object(target_package, "ConfigManager") + + # run + with pytest.raises(RuntimeError): + main() diff --git a/tests/unit/cli/tools/test_eval.py b/tests/unit/cli/tools/test_eval.py new file mode 100644 index 00000000000..ecbd4297ec2 --- /dev/null +++ b/tests/unit/cli/tools/test_eval.py @@ -0,0 +1,142 @@ +import argparse + +import pytest + +from otx.cli.tools import eval as target_package +from otx.cli.tools.eval import get_args, main +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_get_args(mocker): + mock_options = { + "--test-data-roots": "test/data/root", + "--load-weights": "weight/path", + "--output": "save/path", + "--workspace": "work/dir/path", + } + mock_command = ["otx"] + for key, value in mock_options.items(): + mock_command.extend([key, value]) + + mocker.patch("sys.argv", mock_command) + mocker.patch.object( + target_package, "get_parser_and_hprams_data", return_value=[argparse.ArgumentParser(), {"param": "test"}, []] + ) + mocker.patch.object(target_package, "add_hyper_parameters_sub_parser", return_value=argparse.ArgumentParser()) + + parsed_args, _ = get_args() + + assert parsed_args.test_data_roots == "test/data/root" + assert parsed_args.load_weights == "weight/path" + assert parsed_args.workspace == "work/dir/path" + + +@pytest.fixture +def mock_args(mocker, tmp_path): + mock_args = mocker.MagicMock() + mock_args.test_data_roots = "fake_test_data_root" + mock_args.load_weights = "fake_load_weights.xml" + mock_args.workspace = tmp_path / "work_dir" + + def mock_contains(self, val): + return val in self.__dict__ + + mock_args.__contains__ = mock_contains + mock_get_args = mocker.patch("otx.cli.tools.eval.get_args") + mock_get_args.return_value = [mock_args, []] + + return mock_args + + +@pytest.fixture +def mock_config_manager(mocker): + mock_config_manager = mocker.patch.object(target_package, "ConfigManager") + mock_template = mocker.MagicMock() + mock_template.name = "fake_name" + mock_config_manager.return_value.template = mock_template + mock_config_manager.return_value.check_workspace.return_value = True + mock_config_manager.return_value.get_dataset_config.return_value = {} + mock_config_manager.return_value.get_hyparams_config.return_value = {} + + return mock_config_manager + + +@pytest.fixture +def mock_dataset_adapter(mocker): + mock_dataset_adapter = mocker.patch("otx.cli.tools.eval.get_dataset_adapter") + mock_dataset = mocker.MagicMock() + mock_label_schema = mocker.MagicMock() + mock_dataset_adapter.return_value.get_otx_dataset.return_value = mock_dataset + mock_dataset_adapter.return_value.get_label_schema.return_value = mock_label_schema + + return mock_dataset_adapter + + +@pytest.fixture +def mock_task(mocker): + mock_task_class = mocker.MagicMock() + mock_task = mocker.MagicMock() + mock_task_class.return_value = mock_task + mocker.patch.object(target_package, "get_impl_class", return_value=mock_task_class) + + return mock_task + + +@e2e_pytest_unit +def test_main( + mocker, + mock_args, + mock_config_manager, + mock_dataset_adapter, +): + + mocker.patch.object( + target_package, + "read_model", + return_value=mocker.MagicMock(), + ) + + mocker.patch.object( + target_package, + "get_impl_class", + return_value=mocker.MagicMock(), + ) + + mocker.patch.object( + target_package, + "get_dataset_adapter", + return_value=mock_dataset_adapter, + ) + + mocker.patch.object( + target_package, + "ResultSetEntity", + return_value=mocker.MagicMock(), + ) + + mocker.patch.object( + target_package, + "InferenceParameters", + return_value=mocker.MagicMock(), + ) + + mocker.patch.object( + target_package, + "Subset", + return_value=mocker.MagicMock(), + ) + + mocker.patch.object( + target_package, + "TaskEnvironment", + return_value=mocker.MagicMock(), + ) + mocker.patch("json.dump") + mocker.patch("builtins.open") + + mock_get_args = mocker.patch("otx.cli.tools.eval.get_args") + mock_get_args.return_value = [mock_args, []] + + ret = main() + assert ret["retcode"] == 0 diff --git a/tests/unit/cli/tools/test_export.py b/tests/unit/cli/tools/test_export.py new file mode 100644 index 00000000000..035bfcfbe97 --- /dev/null +++ b/tests/unit/cli/tools/test_export.py @@ -0,0 +1,98 @@ +import argparse +from pathlib import Path + +import pytest + +from otx.cli.tools import export as target_package +from otx.cli.tools.export import get_args, main +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_get_args(mocker): + mocker.patch("sys.argv", ["otx", "--load-weights", "load_weights", "--output", "output"]) + mocker.patch.object( + target_package, + "get_parser_and_hprams_data", + return_value=[ + argparse.ArgumentParser(), + {"result_based_confidence": False, "confidence_threshold": 0.35}, + [ + "params", + "--postprocessing.result_based_confidence", + "false", + "--postprocessing.confidence_threshold", + "0.95", + ], + ], + ) + + parsed_args, override_param = get_args() + + assert parsed_args.load_weights == "load_weights" + assert parsed_args.output == "output" + assert override_param == [ + "params.postprocessing.result_based_confidence", + "params.postprocessing.confidence_threshold", + ] + + +@pytest.fixture +def mock_args(mocker, tmp_dir): + mock_args = mocker.MagicMock() + mock_args.load_weights = "fake.bin" + mock_args.output = tmp_dir + mock_args.export_type = "openvino" + + def mock_contains(self, val): + return val in self.__dict__ + + mock_args.__contains__ = mock_contains + mock_get_args = mocker.patch("otx.cli.tools.export.get_args") + mock_get_args.return_value = (mock_args, []) + + return mock_args + + +@pytest.fixture +def mock_task(mocker): + mock_task_class = mocker.MagicMock() + mock_task = mocker.MagicMock() + mock_task_class.return_value = mock_task + mocker.patch.object(target_package, "get_impl_class", return_value=mock_task_class) + + return mock_task + + +@pytest.fixture +def mock_config_manager(mocker): + mock_config_manager = mocker.patch.object(target_package, "ConfigManager") + mock_template = mocker.MagicMock() + mock_template.name = "fake_name" + mock_config_manager.return_value.template = mock_template + + return mock_config_manager + + +@e2e_pytest_unit +def test_main(mocker, mock_args, mock_task, mock_config_manager, tmp_dir): + mocker.patch.object(target_package, "is_checkpoint_nncf", return_value=True) + mocker.patch.object(target_package, "TaskEnvironment") + mocker.patch.object(target_package, "read_label_schema") + mocker.patch.object(target_package, "read_binary") + + def mock_export_side_effect(export_type, output_model, precision, dump_features): + output_model.set_data("fake.xml", b"fake") + + mock_task.export.side_effect = mock_export_side_effect + tmp_dir = Path(tmp_dir) + + # run + ret = main() + + # check + assert ret["retcode"] == 0 + assert ret["template"] == "fake_name" + mock_task.export.assert_called_once() + with (tmp_dir / "fake.xml").open() as f: + assert f.readline() == "fake" diff --git a/tests/unit/cli/tools/test_find.py b/tests/unit/cli/tools/test_find.py new file mode 100644 index 00000000000..7a0e77d2bc2 --- /dev/null +++ b/tests/unit/cli/tools/test_find.py @@ -0,0 +1,72 @@ +import argparse +from unittest.mock import patch + +from otx.cli.tools import find as target_package +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def create_mock_args(task=None, template=None, backbone=None): + mock_args = argparse.Namespace() + mock_args.task = task + mock_args.template = template + mock_args.backbone = backbone + return mock_args + + +@e2e_pytest_unit +def test_generate_backbone_rows(): + backbone_meta = { + "mock_backbone": { + "required": ["first", "second"], + "options": {"first": ["option1", "option2"], "second": ["option1", "option2"]}, + "available": True, + } + } + rows = target_package.generate_backbone_rows(1, "mock_backbone", backbone_meta["mock_backbone"]) + + assert rows == [ + ["1", "mock_backbone", "first", "option1, option2"], + ["", "", "second", "option1, option2"], + ] + + +@e2e_pytest_unit +def test_main(): + mock_args = create_mock_args() + with patch("argparse.ArgumentParser.parse_args", return_value=mock_args): + result = target_package.main() + + assert result == {"retcode": 0, "task_type": None} + + +@e2e_pytest_unit +def test_main_with_template(): + mock_args = create_mock_args(template=True) + with patch("argparse.ArgumentParser.parse_args", return_value=mock_args): + result = target_package.main() + + assert result == {"retcode": 0, "task_type": None} + + +@e2e_pytest_unit +def test_main_with_task(): + mock_args = create_mock_args(task="CLASSIFICATION") + with patch("argparse.ArgumentParser.parse_args", return_value=mock_args): + with patch.object(target_package.Registry, "filter") as mock_filter: + with patch("prettytable.PrettyTable.add_row"): + result = target_package.main() + + assert result == {"retcode": 0, "task_type": "CLASSIFICATION"} + mock_filter.assert_called_once_with(task_type="CLASSIFICATION") + + +@e2e_pytest_unit +def test_main_with_backbone(): + mock_args = create_mock_args(backbone=["backbone1"]) + with patch("argparse.ArgumentParser.parse_args", return_value=mock_args): + with patch.object(target_package.Registry, "get_backbones") as mock_get_backbones: + with patch("prettytable.PrettyTable.add_rows"): + result = target_package.main() + + assert result == {"retcode": 0, "task_type": None} + mock_get_backbones.assert_called_once_with(["backbone1"]) diff --git a/tests/unit/cli/tools/test_optimize.py b/tests/unit/cli/tools/test_optimize.py new file mode 100644 index 00000000000..93ad23bb179 --- /dev/null +++ b/tests/unit/cli/tools/test_optimize.py @@ -0,0 +1,134 @@ +import argparse + +import pytest + +from otx.cli.tools import optimize as target_package +from otx.cli.tools.optimize import get_args, main +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_get_args(mocker): + mock_options = { + "--train-data-roots": "train_data_roots_path", + "--val-data-roots": "val_data_roots_path", + "--load-weights": "load_weights_path", + "--output": "output", + "--workspace": "work_dir_path", + } + mock_command = ["otx"] + for key, value in mock_options.items(): + mock_command.extend([key, value]) + + mocker.patch("sys.argv", mock_command) + mocker.patch.object( + target_package, "get_parser_and_hprams_data", return_value=[argparse.ArgumentParser(), {"param": "test"}, []] + ) + mocker.patch.object(target_package, "add_hyper_parameters_sub_parser", return_value=argparse.ArgumentParser()) + + parsed_args, _ = get_args() + + assert parsed_args.train_data_roots == "train_data_roots_path" + assert parsed_args.val_data_roots == "val_data_roots_path" + assert parsed_args.load_weights == "load_weights_path" + assert parsed_args.output == "output" + assert parsed_args.workspace == "work_dir_path" + + +@pytest.fixture +def mock_args(mocker, tmp_path): + mock_args = mocker.MagicMock() + mock_args.train_data_roots = "fake_train_data_roots_path" + mock_args.val_data_roots = "fake_val_data_roots_path" + mock_args.load_weights = "fake_load_weights_path" + mock_args.output = tmp_path / "save/model" + mock_args.workspace = tmp_path / "work_dir_path" + + def mock_contains(self, val): + return val in self.__dict__ + + mock_args.__contains__ = mock_contains + mock_get_args = mocker.patch("otx.cli.tools.optimize.get_args") + mock_get_args.return_value = [mock_args, []] + + return mock_args + + +@pytest.fixture +def mock_config_manager(mocker): + mock_config_manager = mocker.patch.object(target_package, "ConfigManager") + mock_template = mocker.MagicMock() + mock_template.name = "fake_template_name" + mock_config_manager.return_value.template = mock_template + mock_config_manager.return_value.check_workspace.return_value = True + mock_config_manager.return_value.get_dataset_config.return_value = {} + mock_config_manager.return_value.get_hyparams_config.return_value = {} + + return mock_config_manager + + +@pytest.fixture +def mock_dataset_adapter(mocker): + mock_dataset_adapter = mocker.patch("otx.cli.tools.optimize.get_dataset_adapter") + mock_dataset = mocker.MagicMock() + mock_label_schema = mocker.MagicMock() + mock_dataset_adapter.return_value.get_otx_dataset.return_value = mock_dataset + mock_dataset_adapter.return_value.get_label_schema.return_value = mock_label_schema + + return mock_dataset_adapter + + +@pytest.fixture +def mock_task_class(mocker): + return mocker.patch.object(target_package, "get_impl_class") + + +@pytest.fixture +def mock_task(mocker, mock_task_class, mock_dataset_adapter): + mock_task_class.return_value.return_value = mocker.MagicMock() + mocker.patch.object(target_package, "get_dataset_adapter", return_value=mock_dataset_adapter) + + +@e2e_pytest_unit +def test_main( + mocker, + mock_args, + mock_config_manager, + mock_dataset_adapter, + mock_task, +): + mocker.patch.object(target_package, "read_model", return_value=mocker.MagicMock()) + + mocker.patch("otx.cli.tools.optimize.save_model_data") + + mocker.patch.object( + target_package, + "ResultSetEntity", + return_value=mocker.MagicMock(), + ) + + mocker.patch.object( + target_package, + "InferenceParameters", + return_value=mocker.MagicMock(), + ) + + mocker.patch.object( + target_package, + "Subset", + return_value=mocker.MagicMock(), + ) + + mocker.patch.object( + target_package, + "TaskEnvironment", + return_value=mocker.MagicMock(), + ) + mocker.patch("json.dump") + mocker.patch("builtins.open") + + mock_get_args = mocker.patch("otx.cli.tools.optimize.get_args") + mock_get_args.return_value = [mock_args, []] + + ret = main() + assert ret["retcode"] == 0 diff --git a/tests/unit/cli/tools/test_train.py b/tests/unit/cli/tools/test_train.py new file mode 100644 index 00000000000..ca322511bde --- /dev/null +++ b/tests/unit/cli/tools/test_train.py @@ -0,0 +1,134 @@ +import argparse + +import pytest + +from otx.cli.tools import train as target_package +from otx.cli.tools.train import get_args, main +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_get_args(mocker): + mock_options = { + "--train-data-roots": "train/data/root", + "--val-data-roots": "val/data/root", + "--unlabeled-data-roots": "unlabeled/data/root", + "--unlabeled-file-list": "unlabeled/file/list", + "--load-weights": "weight/path", + "--resume-from": "resume/path", + "--output": "save/path", + "--workspace": "work/dir/path", + "--hpo-time-ratio": "2", + "--gpus": "0,1", + "--rdzv-endpoint": "localhost:1", + "--base-rank": "1", + "--world-size": "1", + "--data": "data/yaml", + } + mock_command = ["otx"] + for key, value in mock_options.items(): + mock_command.extend([key, value]) + + mocker.patch("sys.argv", mock_command) + mocker.patch.object( + target_package, "get_parser_and_hprams_data", return_value=[argparse.ArgumentParser(), {"param": "test"}, []] + ) + mocker.patch.object(target_package, "add_hyper_parameters_sub_parser", return_value=argparse.ArgumentParser()) + + parsed_args, _ = get_args() + + assert parsed_args.train_data_roots == "train/data/root" + assert parsed_args.val_data_roots == "val/data/root" + assert parsed_args.unlabeled_data_roots == "unlabeled/data/root" + assert parsed_args.unlabeled_file_list == "unlabeled/file/list" + assert parsed_args.load_weights == "weight/path" + assert parsed_args.resume_from == "resume/path" + assert parsed_args.output == "save/path" + assert parsed_args.workspace == "work/dir/path" + assert parsed_args.hpo_time_ratio == 2.0 + assert parsed_args.gpus == "0,1" + assert parsed_args.rdzv_endpoint == "localhost:1" + assert parsed_args.base_rank == 1 + assert parsed_args.world_size == 1 + assert parsed_args.data == "data/yaml" + + +@pytest.fixture +def mock_args(mocker, tmp_path): + mock_args = mocker.MagicMock() + mock_args.train_data_roots = "fake_train_data_root" + mock_args.val_data_roots = "fake_val_data_root" + mock_args.load_weights = "fake_load_weights" + mock_args.resume_from = None + mock_args.output = tmp_path / "models" + mock_args.workspace = tmp_path / "work_dir" + mock_args.enable_hpo = False + mock_args.hpo_time_ratio = 4 + mock_args.gpus = None + mock_args.rdzv_endpoint = "localhost:0" + mock_args.base_rank = 0 + mock_args.world_size = 0 + mock_args.data = None + mock_args.unlabeled_data_roots = None + mock_args.unlabeled_file_list = None + + def mock_contains(self, val): + return val in self.__dict__ + + mock_args.__contains__ = mock_contains + mock_get_args = mocker.patch("otx.cli.tools.train.get_args") + mock_get_args.return_value = [mock_args, []] + + return mock_args + + +@pytest.fixture +def mock_config_manager(mocker): + mock_config_manager = mocker.patch.object(target_package, "ConfigManager") + mock_template = mocker.MagicMock() + mock_template.name = "fake_name" + mock_config_manager.return_value.template = mock_template + mock_config_manager.return_value.check_workspace.return_value = True + mock_config_manager.return_value.get_dataset_config.return_value = {} + mock_config_manager.return_value.get_hyparams_config.return_value = {} + + return mock_config_manager + + +@pytest.fixture +def mock_dataset_adapter(mocker): + mock_dataset_adapter = mocker.patch("otx.cli.tools.train.get_dataset_adapter") + mock_dataset = mocker.MagicMock() + mock_label_schema = mocker.MagicMock() + mock_dataset_adapter.return_value.get_otx_dataset.return_value = mock_dataset + mock_dataset_adapter.return_value.get_label_schema.return_value = mock_label_schema + + return mock_dataset_adapter + + +@pytest.fixture +def mock_task(mocker): + mock_task_class = mocker.MagicMock() + mock_task = mocker.MagicMock() + mock_task_class.return_value = mock_task + mocker.patch.object(target_package, "get_impl_class", return_value=mock_task_class) + + return mock_task + + +@e2e_pytest_unit +def test_main(mocker, mock_args, mock_config_manager, mock_dataset_adapter, mock_task): + mocker.patch.object(target_package, "read_label_schema") + mocker.patch.object(target_package, "read_binary") + mocker.patch("otx.cli.tools.train.Path.symlink_to") + mocker.patch("otx.cli.tools.train.get_otx_report") + mocker.patch.object( + target_package, + "run_hpo", + return_value=mocker.MagicMock(), + ) + mocker.patch.object(target_package, "save_model_data") + + ret = main() + + assert ret["retcode"] == 0 diff --git a/tests/unit/cli/utils/__init__.py b/tests/unit/cli/utils/__init__.py deleted file mode 100644 index 6a16273c024..00000000000 --- a/tests/unit/cli/utils/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/cli/utils/test_config.py b/tests/unit/cli/utils/test_config.py new file mode 100644 index 00000000000..e6b158d0daf --- /dev/null +++ b/tests/unit/cli/utils/test_config.py @@ -0,0 +1,85 @@ +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest +import yaml + +from otx.cli.utils.config import configure_dataset, override_parameters +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@pytest.fixture +def mock_parameters(): + return {"a": {"a.a": {"value": 1, "default_value": 2}}} + + +@e2e_pytest_unit +def test_override_mock_parameters(mock_parameters): + overrides = {"a": {"a.a": {"value": 3, "default_value": 4}}} + override_parameters(overrides, mock_parameters) + + assert mock_parameters["a"]["a.a"]["value"] == 3 + assert mock_parameters["a"]["a.a"]["default_value"] == 4 + + +@e2e_pytest_unit +def test_override_mock_parameters_not_allowed_key(mock_parameters): + overrides = {"a": {"a.a": {"fake": 3, "default_value": 4}}} + + with pytest.raises(ValueError): + override_parameters(overrides, mock_parameters) + + +@e2e_pytest_unit +def test_override_mock_parameters_non_exist_key(mock_parameters): + overrides = {"a": {"a.b": {"value": 3, "default_value": 4}}} + + with pytest.raises(ValueError): + override_parameters(overrides, mock_parameters) + + +@e2e_pytest_unit +def test_configure_dataset(mocker): + # prepare + def mock_contain(self, key): + return key in self.__dict__ + + mock_args = mocker.MagicMock() + mock_args.__contains__ = mock_contain + mock_args.train_ann_files = "train_ann_files" + mock_args.train_data_roots = "train_data_roots" + mock_args.val_ann_files = "val_ann_files" + mock_args.val_data_roots = "val_data_roots" + mock_args.unlabeled_file_list = "unlabeled_file_list" + mock_args.unlabeled_data_roots = "unlabeled_data_roots" + mock_args.test_ann_files = "test_ann_files" + mock_args.test_data_roots = "test_data_roots" + mock_args.data = None + # run + data_config = configure_dataset(mock_args) + + # check + assert data_config["data"]["train"]["ann-files"] == str(Path(mock_args.train_ann_files).absolute()) + assert data_config["data"]["train"]["data-roots"] == str(Path(mock_args.train_data_roots).absolute()) + assert data_config["data"]["val"]["ann-files"] == str(Path(mock_args.val_ann_files).absolute()) + assert data_config["data"]["val"]["data-roots"] == str(Path(mock_args.val_data_roots).absolute()) + assert data_config["data"]["unlabeled"]["file-list"] == str(Path(mock_args.unlabeled_file_list).absolute()) + assert data_config["data"]["unlabeled"]["data-roots"] == str(Path(mock_args.unlabeled_data_roots).absolute()) + assert data_config["data"]["test"]["ann-files"] == str(Path(mock_args.test_ann_files).absolute()) + assert data_config["data"]["test"]["data-roots"] == str(Path(mock_args.test_data_roots).absolute()) + + +@e2e_pytest_unit +def test_configure_dataset_with_data_args(mocker): + mock_args = mocker.MagicMock() + + with TemporaryDirectory() as tmp_dir: + data_yaml_path = Path(tmp_dir) / "data.yaml" + mock_data = {"data": {"train": {"ann-files": "a", "data-roots": "b"}}} + with open(data_yaml_path, "w") as f: + yaml.dump(mock_data, f) + + data_config = configure_dataset(mock_args, str(data_yaml_path)) + + assert data_config["data"]["train"]["ann-files"] == mock_data["data"]["train"]["ann-files"] + assert data_config["data"]["train"]["data-roots"] == mock_data["data"]["train"]["data-roots"] diff --git a/tests/unit/cli/utils/test_experiment.py b/tests/unit/cli/utils/test_experiment.py new file mode 100644 index 00000000000..714931be130 --- /dev/null +++ b/tests/unit/cli/utils/test_experiment.py @@ -0,0 +1,306 @@ +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from otx.cli.utils import experiment as target_file +from otx.cli.utils.experiment import ResourceTracker, _check_resource, CpuUsageRecorder, GpuUsageRecorder, GIB +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestResourceTracker: + @pytest.fixture(autouse=True) + def _set_up(self, mocker): + self.mock_mp = mocker.patch.object(target_file, "mp") + + self.mock_proc = mocker.MagicMock() + self.mock_mp.Process.return_value = self.mock_proc + + self.mock_queue = mocker.MagicMock() + self.mock_mp.Queue.return_value = self.mock_queue + + @e2e_pytest_unit + @pytest.mark.parametrize("resource_type", ("cpu", "gpu", "all", "cpu,gpu")) + @pytest.mark.parametrize("gpu_ids", (None, "0", "0,3")) + @pytest.mark.parametrize("output_path", ("fake", Path("fake"))) + def test_init(self, output_path, resource_type, gpu_ids): + ResourceTracker(output_path, resource_type, gpu_ids) + + @e2e_pytest_unit + @pytest.mark.parametrize("resource_type", ("cpu", "gpu", "all", "cpu,gpu")) + @pytest.mark.parametrize("gpu_ids", (None, "0", "0,3")) + def test_start(self, resource_type, gpu_ids): + # prepare + if resource_type == "all": + expected_resource_type = target_file.AVAILABLE_RESOURCE_TYPE + else: + expected_resource_type = [val for val in resource_type.split(",")] + + expected_gpu_ids = None + if gpu_ids is not None: + expected_gpu_ids = [int(idx) for idx in gpu_ids.split(",")] + expected_gpu_ids[0] = 0 + + # run + resource_tracker = ResourceTracker("fake_output", resource_type, gpu_ids) + resource_tracker.start() + + self.mock_proc.start.assert_called_once() # check that a process to track resource usages starts + # check proper resource type and gpu_ids vaues are passed to a process to run + assert self.mock_mp.Process.call_args.kwargs["args"][1] == expected_resource_type + assert self.mock_mp.Process.call_args.kwargs["args"][2] == expected_gpu_ids + + @e2e_pytest_unit + def test_start_multiple_times(self): + resource_tracker = ResourceTracker("fake_output") + + # run multiple times + resource_tracker.start() + resource_tracker.start() + + self.mock_proc.start.assert_called_once() # check that a process starts once + + @e2e_pytest_unit + def test_stop(self): + output_path = Path("fake") + + resource_tracker = ResourceTracker(output_path) + resource_tracker.start() + resource_tracker.stop() + + # check that code to terminate a process is executed properly + self.mock_queue.put.assert_called_once_with(output_path) + self.mock_proc.join.assert_called() + self.mock_proc.close.assert_called() + + @e2e_pytest_unit + def test_stop_not_exit_normally(self): + output_path = Path("fake") + self.mock_proc.exitcode = None + + resource_tracker = ResourceTracker(output_path) + resource_tracker.start() + resource_tracker.stop() + + # check that code to terminate a process is executed properly + self.mock_queue.put.assert_called_once_with(output_path) + self.mock_proc.join.assert_called() + # check that code to terminate a process forcibly if process doesn't exit normally + self.mock_proc.terminate.assert_called() + self.mock_proc.close.assert_called() + + @e2e_pytest_unit + def test_stop_before_start(self): + resource_tracker = ResourceTracker("fake") + resource_tracker.stop() + + # check that code to make a process done isn't called + self.mock_queue.put.assert_not_called() + self.mock_proc.join.assert_not_called() + self.mock_proc.close.assert_not_called() + + +class MockQueue: + def __init__(self, output_path: str): + self.output_path = output_path + + def empty(self): + return False + + def get(self): + return self.output_path + + +@pytest.mark.parametrize("resource_types", (["cpu"], ["gpu"], ["cpu", "gpu"])) +@e2e_pytest_unit +def test_check_resource(mocker, resource_types, tmp_path): + # prepare + gpu_ids = [0, 1] + output_file = f"{tmp_path}/fake.yaml" + mock_queue = MockQueue(output_file) + + mock_cpu_recorder = mocker.MagicMock() + mocker.patch.object(target_file, "CpuUsageRecorder", return_value=mock_cpu_recorder) + mock_gpu_recorder = mocker.MagicMock() + mock_gpu_recorder_cls = mocker.patch.object(target_file, "GpuUsageRecorder", return_value=mock_gpu_recorder) + + mocker.patch.object(target_file, "yaml") + mocker.patch.object(target_file, "time") + + # run + _check_resource(mock_queue, resource_types, gpu_ids) + + # check the recorders record properly + if "cpu" in resource_types: + mock_cpu_recorder.record.assert_called_once() + if "gpu" in resource_types: + mock_gpu_recorder.record.assert_called_once() + mock_gpu_recorder_cls.assert_called_once_with(gpu_ids) + + assert Path(output_file).exists() # check a file is saved well + + +def test_check_resource_wrong_resource_type(mocker, tmp_path): + # prepare + resource_types = ["wrong"] + output_file = f"{tmp_path}/fake.yaml" + mock_queue = MockQueue(output_file) + + mocker.patch.object(target_file, "CpuUsageRecorder") + mocker.patch.object(target_file, "GpuUsageRecorder") + mocker.patch.object(target_file, "yaml") + mocker.patch.object(target_file, "time") + + # check that ValueError is raised. + with pytest.raises(ValueError): + _check_resource(mock_queue, resource_types) + + +class TestCpuUsageRecorder: + @pytest.fixture(autouse=True) + def _set_up(self, mocker): + self.mock_psutil = mocker.patch.object(target_file, "psutil") + self.mock_virtual_memory = mocker.MagicMock() + self.mock_psutil.virtual_memory.return_value = self.mock_virtual_memory + self.set_mem_usage(0) + self.set_cpu_util(0) + + def set_mem_usage(self, mem_usage: int): + self.mock_virtual_memory.total = mem_usage + self.mock_virtual_memory.available = 0 + + def set_cpu_util(self, cpu_util: int): + self.mock_psutil.cpu_percent.return_value = cpu_util + + @e2e_pytest_unit + def test_init(self): + CpuUsageRecorder() + + @e2e_pytest_unit + def test_record_report(self): + cpu_usage_recorder = CpuUsageRecorder() + + # record cpu usage + cpu_usage_recorder.record() + self.set_mem_usage(4 * GIB) + self.set_cpu_util(40) + cpu_usage_recorder.record() + self.set_mem_usage(6 * GIB) + self.set_cpu_util(60) + cpu_usage_recorder.record() + report = cpu_usage_recorder.report() + + # check right values are returned when calling report + assert float(report["max_memory_usage"].split()[0]) == pytest.approx(6) + assert float(report["avg_util"].split()[0]) == pytest.approx(50) + + @e2e_pytest_unit + def test_report_wo_record(self): + cpu_usage_recorder = CpuUsageRecorder() + report = cpu_usage_recorder.report() + + assert report == {} # if report is called without calling record, empty dict should be returned + + +class TestGpuUsageRecorder: + @pytest.fixture(autouse=True) + def _set_up(self, mocker): + self.mock_pynvml = mocker.patch.object(target_file, "pynvml") + self.mock_pynvml.nvmlDeviceGetCount.return_value = 8 + self.mock_nvmlDeviceGetHandleByIndex = mocker.MagicMock(side_effect=lambda val: val) + self.mock_pynvml.nvmlDeviceGetHandleByIndex = self.mock_nvmlDeviceGetHandleByIndex + + self.gpu_usage = {} + self.mock_pynvml.nvmlDeviceGetMemoryInfo.side_effect = self.mock_nvmlDeviceGetMemoryInfo + self.mock_pynvml.nvmlDeviceGetUtilizationRates.side_effect = self.mock_nvmlDeviceGetUtilizationRates + + self.mock_os = mocker.patch.object(target_file, "os") + self.mock_os.environ = {} + + def mock_nvmlDeviceGetMemoryInfo(self, gpu_idx: int): + gpu_mem = MagicMock() + gpu_mem.used = self.gpu_usage.get(gpu_idx, {}).get("mem", 0) + return gpu_mem + + def mock_nvmlDeviceGetUtilizationRates(self, gpu_idx: int): + gpu_util = MagicMock() + gpu_util.gpu = self.gpu_usage.get(gpu_idx, {}).get("util", 0) + return gpu_util + + def set_mem_usage(self, gpu_idx: int, mem_usage: int): + if gpu_idx in self.gpu_usage: + self.gpu_usage[gpu_idx]["mem"] = mem_usage + else: + self.gpu_usage[gpu_idx] = {"mem": mem_usage} + + def set_gpu_util(self, gpu_idx: int, gpu_util: int): + if gpu_idx in self.gpu_usage: + self.gpu_usage[gpu_idx]["util"] = gpu_util + else: + self.gpu_usage[gpu_idx] = {"util": gpu_util} + + @e2e_pytest_unit + @pytest.mark.parametrize("gpu_to_track", ([0], [0, 4])) + def test_init(self, mocker, gpu_to_track): + mocker.patch.object(GpuUsageRecorder, "_get_gpu_to_track", return_value=gpu_to_track) + + GpuUsageRecorder() + + self.mock_pynvml.nvmlInit.assert_called_once() # check nvml is initialized + # check proper gpu handler is gotten + for i, gpu_idx in enumerate(gpu_to_track): + self.mock_nvmlDeviceGetHandleByIndex.call_args_list[i].args == (gpu_idx,) + + @e2e_pytest_unit + @pytest.mark.parametrize("gpu_ids", ([0], [1, 2, 5])) + def test_get_gpu_to_track_no_cuda_env_var(self, gpu_ids): + gpu_usage_recorder = GpuUsageRecorder() + + assert gpu_usage_recorder._get_gpu_to_track(gpu_ids) == gpu_ids # check right gpu indices are returned + + @e2e_pytest_unit + @pytest.mark.parametrize("gpu_ids", ([0], [1, 2, 5])) + def test_get_gpu_to_track_cuda_env_var(self, gpu_ids): + cuda_visible_devices = [1, 2, 5, 7, 9, 10] + self.mock_os.environ = {"CUDA_VISIBLE_DEVICES": ",".join(list(map(str, cuda_visible_devices)))} + gpu_to_track = [cuda_visible_devices[i] for i in gpu_ids] + + gpu_usage_recorder = GpuUsageRecorder() + + assert gpu_usage_recorder._get_gpu_to_track(gpu_ids) == gpu_to_track # check right gpu indices are returned + + @e2e_pytest_unit + def test_record_report(self): + gpu_ids = [0, 1] + gpu_usage_recorder = GpuUsageRecorder(gpu_ids) + + # first record + self.set_mem_usage(0, 4 * GIB) + self.set_mem_usage(1, 6 * GIB) + self.set_gpu_util(0, 40) + self.set_gpu_util(1, 60) + gpu_usage_recorder.record() + + # second record + self.set_mem_usage(0, 6 * GIB) + self.set_mem_usage(1, 8 * GIB) + self.set_gpu_util(0, 60) + self.set_gpu_util(1, 80) + gpu_usage_recorder.record() + + report = gpu_usage_recorder.report() + + # check right values are returned + assert float(report["gpu_0"]["avg_util"].split()[0]) == pytest.approx(50) + assert float(report["gpu_0"]["max_mem"].split()[0]) == pytest.approx(6) + assert float(report["gpu_1"]["avg_util"].split()[0]) == pytest.approx(70) + assert float(report["gpu_1"]["max_mem"].split()[0]) == pytest.approx(8) + assert float(report["total_avg_util"].split()[0]) == pytest.approx(60) + assert float(report["total_max_mem"].split()[0]) == pytest.approx(8) + + @e2e_pytest_unit + def test_report_wo_record(self): + gpu_usage_recorder = GpuUsageRecorder() + report = gpu_usage_recorder.report() + + assert report == {} # if report is called without calling record, empty dict should be returned diff --git a/tests/unit/cli/utils/test_help_formatter.py b/tests/unit/cli/utils/test_help_formatter.py deleted file mode 100644 index 61a05a6e27b..00000000000 --- a/tests/unit/cli/utils/test_help_formatter.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Tests for Custom Help Formatter.""" - -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import sys -from unittest.mock import patch - -import pytest -from jsonargparse import ArgumentParser -from otx.cli.utils.help_formatter import ( - CustomHelpFormatter, - get_cli_usage_docstring, - get_verbosity_subcommand, - render_guide, -) - - -def test_get_verbosity_subcommand() -> None: - """Test if the verbosity level and subcommand are correctly parsed.""" - argv = ["otx", "train", "-h"] - with patch.object(sys, "argv", argv): - result = get_verbosity_subcommand() - assert result["subcommand"] == "train" - assert result["verbosity"] == 0 - - argv = ["otx", "train", "-h", "-v"] - with patch.object(sys, "argv", argv): - result = get_verbosity_subcommand() - assert result["subcommand"] == "train" - assert result["verbosity"] == 1 - - argv = ["otx", "train", "-h", "-vv"] - with patch.object(sys, "argv", argv): - result = get_verbosity_subcommand() - assert result["subcommand"] == "train" - assert result["verbosity"] == 2 - - argv = ["otx", "-h"] - with patch.object(sys, "argv", argv): - result = get_verbosity_subcommand() - assert result["subcommand"] is None - assert result["verbosity"] == 2 - - -def test_get_cli_usage_docstring() -> None: - """Test if the CLI usage docstring is correctly parsed.""" - assert get_cli_usage_docstring(None) is None - - class Component: - """. - - CLI Usage: - 1. First Step. - 2. Second Step. - - - """ - - assert get_cli_usage_docstring(Component) == "1. First Step.\n2. Second Step." - - class Component2: - """. - - CLI Usage-Test: - test: test. - - - """ - - assert get_cli_usage_docstring(Component2) is None - - -def test_render_guide() -> None: - """Test if the guide is correctly rendered.""" - subcommand = "train" - contents = render_guide(subcommand) - assert len(contents) == 2 - assert contents[0].__class__.__name__ == "Markdown" - assert "# OpenVINO™ Training Extensions CLI Guide" in contents[0].markup - assert contents[1].__class__.__name__ == "Panel" - assert "otx train" in contents[1].renderable.markup - assert render_guide(None) == [] - - -class TestCustomHelpFormatter: - """Test Custom Help Formatter.""" - - @pytest.fixture() - def fxt_parser(self) -> ArgumentParser: - """Mock ArgumentParser.""" - parser = ArgumentParser(env_prefix="otx", formatter_class=CustomHelpFormatter) - parser.formatter_class.subcommand = "train" - parser.add_argument( - "-t", - "--test", - action="count", - help="add_usage test.", - ) - parser.add_argument( - "--model", - action="count", - help="never_skip test.", - ) - return parser - - def test_verbose_0(self, capfd: "pytest.CaptureFixture", fxt_parser: ArgumentParser) -> None: - """Test verbose level 0.""" - argv = ["otx", "train", "-h"] - assert fxt_parser.formatter_class == CustomHelpFormatter - fxt_parser.formatter_class.verbosity_level = 0 - with pytest.raises(SystemExit, match="0"): - fxt_parser.parse_args(argv) - out, _ = capfd.readouterr() - assert "Quick-Start" in out - assert "Arguments" not in out - - def test_verbose_1(self, capfd: "pytest.CaptureFixture", fxt_parser: ArgumentParser) -> None: - """Test verbose level 1.""" - argv = ["otx", "train", "-h", "-v"] - assert fxt_parser.formatter_class == CustomHelpFormatter - fxt_parser.formatter_class.verbosity_level = 1 - with pytest.raises(SystemExit, match="0"): - fxt_parser.parse_args(argv) - out, _ = capfd.readouterr() - assert "Quick-Start" in out - assert "Arguments" in out - - def test_verbose_2(self, capfd: "pytest.CaptureFixture", fxt_parser: ArgumentParser) -> None: - """Test verbose level 2.""" - argv = ["otx", "train", "-h", "-vv"] - assert fxt_parser.formatter_class == CustomHelpFormatter - fxt_parser.formatter_class.verbosity_level = 2 - with pytest.raises(SystemExit, match="0"): - fxt_parser.parse_args(argv) - out, _ = capfd.readouterr() - assert "Quick-Start" not in out - assert "Arguments" in out diff --git a/tests/unit/cli/utils/test_hpo.py b/tests/unit/cli/utils/test_hpo.py new file mode 100644 index 00000000000..f01a048a195 --- /dev/null +++ b/tests/unit/cli/utils/test_hpo.py @@ -0,0 +1,836 @@ +import json +import yaml +from copy import deepcopy +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import List +from unittest.mock import MagicMock + +import pytest + +import otx +from otx.api.configuration.helper import create as create_conf_hp +from otx.api.entities.model import ModelEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.subset import Subset +from otx.api.entities.task_environment import TaskEnvironment +from otx.cli.registry import find_and_parse_model_template +from otx.cli.utils import hpo +from otx.cli.utils.hpo import ( + HpoCallback, + HpoDataset, + HpoRunner, + TaskEnvironmentManager, + TaskManager, + Trainer, + get_best_hpo_weight, + run_hpo, + run_trial, +) +from otx.hpo.hpo_base import TrialStatus +from tests.test_suite.e2e_test_system import e2e_pytest_unit + +CLASSIFCATION_TASK = {TaskType.CLASSIFICATION} +DETECTION_TASK = {TaskType.DETECTION, TaskType.INSTANCE_SEGMENTATION, TaskType.ROTATED_DETECTION} +SEGMENTATION_TASK = {TaskType.SEGMENTATION} +ANOMALY_TASK = {TaskType.ANOMALY_CLASSIFICATION, TaskType.ANOMALY_DETECTION, TaskType.ANOMALY_SEGMENTATION} +MMCV_TASK = CLASSIFCATION_TASK | DETECTION_TASK | SEGMENTATION_TASK +ALL_TASK = MMCV_TASK | ANOMALY_TASK +OTX_ROOT_PATH = Path(otx.__file__).parent + + +class TestTaskManager: + @e2e_pytest_unit + @pytest.mark.parametrize("task", MMCV_TASK) + def test_is_mmcv_framework_task(self, task: TaskType): + task_manager = TaskManager(task) + assert task_manager.is_mmcv_framework_task() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", ANOMALY_TASK) + def test_is_not_mmcv_framework_task(self, task: TaskType): + task_manager = TaskManager(task) + assert not task_manager.is_mmcv_framework_task() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", CLASSIFCATION_TASK) + def test_is_cls_framework_task(self, task: TaskType): + task_manager = TaskManager(task) + assert task_manager.is_cls_framework_task() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", ALL_TASK - CLASSIFCATION_TASK) + def test_is_not_cls_framework_task(self, task: TaskType): + task_manager = TaskManager(task) + assert not task_manager.is_cls_framework_task() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", DETECTION_TASK) + def test_is_det_framework_task(self, task: TaskType): + task_manager = TaskManager(task) + assert task_manager.is_det_framework_task() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", ALL_TASK - DETECTION_TASK) + def test_is_not_det_framework_task(self, task: TaskType): + task_manager = TaskManager(task) + assert not task_manager.is_det_framework_task() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", SEGMENTATION_TASK) + def test_is_seg_framework_task(self, task: TaskType): + task_manager = TaskManager(task) + assert task_manager.is_seg_framework_task() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", ALL_TASK - SEGMENTATION_TASK) + def test_is_not_seg_framework_task(self, task: TaskType): + task_manager = TaskManager(task) + assert not task_manager.is_seg_framework_task() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", ANOMALY_TASK) + def test_is_anomaly_framework_task(self, task: TaskType): + task_manager = TaskManager(task) + assert task_manager.is_anomaly_framework_task() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", ALL_TASK - ANOMALY_TASK) + def test_is_not_anomaly_framework_task(self, task: TaskType): + task_manager = TaskManager(task) + assert not task_manager.is_anomaly_framework_task() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", MMCV_TASK) + def test_get_mmcv_batch_size_name(self, task: TaskType): + task_manager = TaskManager(task) + assert task_manager.get_batch_size_name() == "learning_parameters.batch_size" + + @e2e_pytest_unit + @pytest.mark.parametrize("task", ANOMALY_TASK) + def test_get_anomaly_batch_size_name(self, task: TaskType): + task_manager = TaskManager(task) + assert task_manager.get_batch_size_name() == "learning_parameters.train_batch_size" + + @e2e_pytest_unit + def test_get_unknown_task_batch_size_name(self, mocker): + mock_func1 = mocker.patch.object(TaskManager, "is_mmcv_framework_task") + mock_func1.return_value = False + mock_func2 = mocker.patch.object(TaskManager, "is_anomaly_framework_task") + mock_func2.return_value = False + + task_manager = TaskManager(mocker.MagicMock()) + + with pytest.raises(RuntimeError): + task_manager.get_batch_size_name() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", MMCV_TASK) + def test_get_mmcv_epoch_name(self, task: TaskType): + task_manager = TaskManager(task) + assert task_manager.get_epoch_name() == "num_iters" + + @e2e_pytest_unit + @pytest.mark.parametrize("task", ANOMALY_TASK) + def test_get_anomaly_epoch_name(self, task: TaskType): + task_manager = TaskManager(task) + assert task_manager.get_epoch_name() == "max_epochs" + + @e2e_pytest_unit + def test_get_unknown_task_epoch_name(self, mocker): + mock_func1 = mocker.patch.object(TaskManager, "is_mmcv_framework_task") + mock_func1.return_value = False + mock_func2 = mocker.patch.object(TaskManager, "is_anomaly_framework_task") + mock_func2.return_value = False + + task_manager = TaskManager(mocker.MagicMock()) + + with pytest.raises(RuntimeError): + task_manager.get_epoch_name() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", MMCV_TASK) + def test_copy_weight(self, task: TaskType): + task_manager = TaskManager(task) + fake_model_weight = Path("temp_epoch_3.pth") + with TemporaryDirectory() as src_dir, TemporaryDirectory() as det_dir: + weight_in_src = src_dir / fake_model_weight + weight_in_det = det_dir / fake_model_weight + weight_in_src.write_text("fake") + task_manager.copy_weight(src_dir, det_dir) + + assert weight_in_det.exists() + + @e2e_pytest_unit + @pytest.mark.parametrize("task", MMCV_TASK) + def test_get_latest_weight(self, task: TaskType): + task_manager = TaskManager(task) + + with TemporaryDirectory() as work_dir: + for i in range(1, 10): + (work_dir / Path(f"epoch_{i}.pth")).write_text("fake") + + latest_model_weight = work_dir / Path("epoch_10.pth") + latest_model_weight.write_text("fake") + assert task_manager.get_latest_weight(work_dir) == str(latest_model_weight) + + +def get_template_path(template_dir: str) -> Path: + task_config_dir = OTX_ROOT_PATH / "algorithms" / template_dir + return list(task_config_dir.glob("**/template.yaml"))[0] + + +def make_task_env(template_path: str) -> TaskEnvironment: + template = find_and_parse_model_template(template_path) + return TaskEnvironment(template, None, create_conf_hp(template.hyper_parameters.data), MagicMock()) + + +@pytest.fixture(scope="module") +def cls_template_path() -> str: + return str(get_template_path("classification/configs")) + + +@pytest.fixture(scope="module") +def det_template_path() -> str: + return str(get_template_path("detection/configs/detection")) + + +@pytest.fixture(scope="module") +def seg_template_path() -> str: + return str(get_template_path("segmentation/configs")) + + +@pytest.fixture(scope="module") +def anomaly_template_path() -> str: + return str(OTX_ROOT_PATH / "algorithms/anomaly/configs/classification/stfpm/template.yaml") + + +@pytest.fixture(scope="module") +def cls_task_env(cls_template_path): + return make_task_env(cls_template_path) + + +@pytest.fixture(scope="module") +def det_task_env(det_template_path) -> TaskEnvironment: + return make_task_env(det_template_path) + + +@pytest.fixture(scope="module") +def seg_task_env(seg_template_path) -> TaskEnvironment: + return make_task_env(seg_template_path) + + +@pytest.fixture(scope="module") +def anomaly_task_env(anomaly_template_path) -> TaskEnvironment: + return make_task_env(anomaly_template_path) + + +@pytest.fixture +def mmcv_task_env(cls_task_env, det_task_env, seg_task_env) -> List[TaskEnvironment]: + return [cls_task_env, det_task_env, seg_task_env] + + +@pytest.fixture +def all_task_env(cls_task_env, det_task_env, seg_task_env, anomaly_task_env) -> List[TaskEnvironment]: + return [cls_task_env, det_task_env, seg_task_env, anomaly_task_env] + + +@pytest.fixture +def mock_environment(): + MockTaskEnv = MagicMock(spec=TaskEnvironment) + return MockTaskEnv() + + +@pytest.fixture(scope="module") +def action_template_path() -> str: + return str(get_template_path("action")) + + +@pytest.fixture(scope="module") +def action_task_env(action_template_path) -> TaskEnvironment: + return make_task_env(action_template_path) + + +class TestTaskEnvironmentManager: + @pytest.fixture(autouse=True) + def _make_mock_task_env(self, mock_environment): + self._mock_environment = mock_environment + + @e2e_pytest_unit + def test_init(self, all_task_env): + for task_env in all_task_env: + TaskEnvironmentManager(task_env) + + @e2e_pytest_unit + def test_get_task(self, cls_task_env, det_task_env, seg_task_env): + task_env = TaskEnvironmentManager(cls_task_env) + assert task_env.get_task() == TaskType.CLASSIFICATION + + task_env = TaskEnvironmentManager(det_task_env) + assert task_env.get_task() == TaskType.DETECTION + + task_env = TaskEnvironmentManager(seg_task_env) + assert task_env.get_task() == TaskType.SEGMENTATION + + @e2e_pytest_unit + def test_get_model_template( + self, cls_task_env, det_task_env, seg_task_env, cls_template_path, det_template_path, seg_template_path + ): + task_env = TaskEnvironmentManager(cls_task_env) + assert task_env.get_model_template() == find_and_parse_model_template(cls_template_path) + + task_env = TaskEnvironmentManager(det_task_env) + assert task_env.get_model_template() == find_and_parse_model_template(det_template_path) + + task_env = TaskEnvironmentManager(seg_task_env) + assert task_env.get_model_template() == find_and_parse_model_template(seg_template_path) + + @e2e_pytest_unit + def test_get_model_template_path( + self, cls_task_env, det_task_env, seg_task_env, cls_template_path, det_template_path, seg_template_path + ): + task_env = TaskEnvironmentManager(cls_task_env) + assert task_env.get_model_template_path() == cls_template_path + + task_env = TaskEnvironmentManager(det_task_env) + assert task_env.get_model_template_path() == det_template_path + + task_env = TaskEnvironmentManager(seg_task_env) + assert task_env.get_model_template_path() == seg_template_path + + @e2e_pytest_unit + def test_set_hyper_parameter_using_str_key(self): + task_env = TaskEnvironmentManager(self._mock_environment) + hyper_parameter = {"a.b.c.d": 1, "e.f.g.h": 2} + + task_env.set_hyper_parameter_using_str_key(hyper_parameter) + + env_hp = self._mock_environment.get_hyper_parameters() + + assert env_hp.a.b.c.d == hyper_parameter["a.b.c.d"] + assert env_hp.e.f.g.h == hyper_parameter["e.f.g.h"] + + @e2e_pytest_unit + def test_get_dict_type_hyper_parameter(self): + learning_parameters = self._mock_environment.get_hyper_parameters().learning_parameters + learning_parameters.parameters = ["a", "b"] + learning_parameters.a = 1 + learning_parameters.b = 2 + + task_env = TaskEnvironmentManager(self._mock_environment) + dict_hp = task_env.get_dict_type_hyper_parameter() + + assert dict_hp["learning_parameters.a"] == 1 + assert dict_hp["learning_parameters.b"] == 2 + + @e2e_pytest_unit + @pytest.mark.parametrize("task", ALL_TASK) + def test_get_max_epoch(self, task): + max_epoch = 10 + self._mock_environment.model_template.task_type = task + learning_parameters = self._mock_environment.get_hyper_parameters().learning_parameters + setattr(learning_parameters, TaskManager(task).get_epoch_name(), max_epoch) + + task_env = TaskEnvironmentManager(self._mock_environment) + + assert task_env.get_max_epoch() == max_epoch + + @e2e_pytest_unit + def test_save_mmcv_initial_weight(self, mmcv_task_env): + for task_env in mmcv_task_env: + task_env.model = None + task_env = TaskEnvironmentManager(task_env) + assert not task_env.save_initial_weight("fake_path") + + @e2e_pytest_unit + def test_save_anomaly_initial_weight(self, mocker, anomaly_task_env): + def mock_save_model_data(model, save_path: str): + (Path(save_path) / "weights.pth").write_text("fake") + + mocker.patch.object(TaskEnvironmentManager, "get_train_task") + mocker.patch("otx.cli.utils.hpo.save_model_data", mock_save_model_data) + with TemporaryDirectory() as tmp_dir: + anomaly_task_env.model = None + task_env = TaskEnvironmentManager(anomaly_task_env) + save_path = Path(tmp_dir) / "init.pth" + assert task_env.save_initial_weight(str(save_path)) + assert save_path.exists() + + @e2e_pytest_unit + def test_loaded_inital_weight(self, mocker, all_task_env): + def mock_save_model_data(model, save_path: str): + (Path(save_path) / "weights.pth").write_text("fake") + + mocker.patch.object(TaskEnvironmentManager, "get_train_task") + mocker.patch("otx.cli.utils.hpo.save_model_data", mock_save_model_data) + with TemporaryDirectory() as tmp_dir: + for task_env in all_task_env: + task_env.model = mocker.MagicMock() + task_env = TaskEnvironmentManager(task_env) + save_path = Path(tmp_dir) / "init.pth" + assert task_env.save_initial_weight(str(save_path)) + assert save_path.exists() + + @e2e_pytest_unit + def test_get_train_task(self, mocker, all_task_env): + mock_func = mocker.patch("otx.cli.utils.hpo.get_impl_class") + + for task_env in all_task_env: + mock_class = mocker.MagicMock() + mock_func.return_vlaue = mock_class + task_env = TaskEnvironmentManager(task_env) + task_env.get_train_task() + + mock_class.assert_not_called() + + @e2e_pytest_unit + def test_get_mmcv_batch_size_name(self, mmcv_task_env): + for task_env in mmcv_task_env: + task_env = TaskEnvironmentManager(task_env) + assert task_env.get_batch_size_name() == "learning_parameters.batch_size" + + @e2e_pytest_unit + def test_get_anomaly_batch_size_name(self, anomaly_task_env): + task_env = TaskEnvironmentManager(anomaly_task_env) + assert task_env.get_batch_size_name() == "learning_parameters.train_batch_size" + + @e2e_pytest_unit + def test_load_model_weight(self, mocker, all_task_env): + mock_func = mocker.patch("otx.cli.utils.hpo.read_model") + + for task_env in all_task_env: + mock_class = mocker.MagicMock() + mock_func.return_value = mock_class + task_manager = TaskEnvironmentManager(task_env) + task_manager.load_model_weight("fake", mocker.MagicMock()) + assert task_env.model == mock_class + + @e2e_pytest_unit + def test_resume_model_weight(self, mocker, all_task_env): + mock_func = mocker.patch("otx.cli.utils.hpo.read_model") + + for task_env in all_task_env: + mock_class = mocker.MagicMock() + mock_func.return_value = mock_class + task_manager = TaskEnvironmentManager(task_env) + task_manager.resume_model_weight("fake", mocker.MagicMock()) + assert task_env.model == mock_class + assert mock_class.model_adapters["resume"] + + @e2e_pytest_unit + def test_get_new_model_entity(self, all_task_env): + for task_env in all_task_env: + task_manager = TaskEnvironmentManager(task_env) + model_entity = task_manager.get_new_model_entity() + assert isinstance(model_entity, ModelEntity) + + @e2e_pytest_unit + def test_set_epoch(self, all_task_env): + epoch = 123 + for task_env in all_task_env: + task_manager = TaskEnvironmentManager(task_env) + task_manager.set_epoch(epoch) + assert task_manager.get_max_epoch() == epoch + + +class TestHpoRunner: + @e2e_pytest_unit + def test_init(self, all_task_env): + for task_env in all_task_env: + HpoRunner(task_env, 100, 10, "fake_path") + + @e2e_pytest_unit + @pytest.mark.parametrize("train_dataset_size,val_dataset_size", [(0, 10), (10, 0), (-1, -1)]) + def test_init_wrong_dataset_size(self, cls_task_env, train_dataset_size, val_dataset_size): + with pytest.raises(ValueError): + HpoRunner(cls_task_env, train_dataset_size, val_dataset_size, "fake_path", 4) + + @e2e_pytest_unit + @pytest.mark.parametrize("hpo_time_ratio", [-3, 0]) + def test_init_wrong_hpo_time_ratio(self, cls_task_env, hpo_time_ratio): + with pytest.raises(ValueError): + HpoRunner(cls_task_env, 100, 10, "fake_path", hpo_time_ratio) + + @e2e_pytest_unit + @pytest.mark.parametrize("diff_from_min_bs", [0, 1]) + def test_init_fix_batch_size(self, cls_task_env, diff_from_min_bs): + task_env = TaskEnvironmentManager(cls_task_env) + with (Path(task_env.get_model_template_path()).parent / "hpo_config.yaml").open() as f: + hpo_config = yaml.safe_load(f) + batch_size_name = task_env.get_batch_size_name() + min_bs = hpo_config["hp_space"][batch_size_name]["range"][0] + train_dataset_size = min_bs + diff_from_min_bs + + hpo_runner = HpoRunner(cls_task_env, train_dataset_size, 10, "fake_path") + assert batch_size_name in hpo_runner._fixed_hp + + @e2e_pytest_unit + def test_run_hpo(self, mocker, cls_task_env): + cls_task_env.model = None + hpo_runner = HpoRunner(cls_task_env, 100, 10, "fake_path") + mock_run_hpo_loop = mocker.patch("otx.cli.utils.hpo.run_hpo_loop") + mock_hb = mocker.patch("otx.cli.utils.hpo.HyperBand") + + hpo_runner.run_hpo(mocker.MagicMock(), {"fake", "fake"}) + + mock_run_hpo_loop.assert_called() # call hpo_loop to run HPO + mock_hb.assert_called() # make hyperband + + @e2e_pytest_unit + def test_run_hpo_w_dataset_smaller_than_batch(self, mocker, cls_task_env): + cls_task_env.model = None + hpo_runner = HpoRunner(cls_task_env, 2, 10, "fake_path") + mock_run_hpo_loop = mocker.patch("otx.cli.utils.hpo.run_hpo_loop") + mock_hb = mocker.patch("otx.cli.utils.hpo.HyperBand") + + hpo_runner.run_hpo(mocker.MagicMock(), {"fake", "fake"}) + + mock_run_hpo_loop.assert_called() # call hpo_loop to run HPO + mock_hb.assert_called() # make hyperband + + +class TestTrainer: + @pytest.fixture(autouse=True) + def setup(self, tmp_dir): + self.weight_format = "epoch_{}.pth" + self.hpo_workdir = Path(tmp_dir) / "hpo_dir" + + @pytest.fixture + def tmp_dir(self): + with TemporaryDirectory() as tmp_dir: + yield tmp_dir + + @e2e_pytest_unit + def test_init(self, mocker, cls_template_path): + Trainer( + hp_config={"configuration": {"iterations": 10}}, + report_func=mocker.stub(), + model_template=find_and_parse_model_template(cls_template_path), + data_roots={"fake": "fake"}, + task_type=TaskType.CLASSIFICATION, + hpo_workdir="fake", + initial_weight_name="fake", + metric="fake", + ) + + @pytest.fixture + def mock_task(self, mocker, tmp_dir): + fake_project_path = Path(tmp_dir) / "fake_proejct" + fake_project_path.mkdir(parents=True) + for i in range(1, 5): + (fake_project_path / self.weight_format.format(i)).write_text("fake") + + mock_get_train_task = mocker.patch.object(TaskEnvironmentManager, "get_train_task") + mock_task = mocker.MagicMock() + mock_task.project_path = str(fake_project_path) + mock_get_train_task.return_value = mock_task + + return mock_task + + @e2e_pytest_unit + def test_run(self, mocker, cls_template_path, mock_task, tmp_dir): + # prepare + trial_id = "1" + mock_report_func = mocker.MagicMock() + + mocker.patch("otx.cli.utils.hpo.get_dataset_adapter") + mocker.patch("otx.cli.utils.hpo.HpoDataset") + + # run + trainer = Trainer( + hp_config={"configuration": {"iterations": 10}, "id": trial_id}, + report_func=mock_report_func, + model_template=find_and_parse_model_template(cls_template_path), + data_roots=mocker.MagicMock(), + task_type=TaskType.CLASSIFICATION, + hpo_workdir=self.hpo_workdir, + initial_weight_name="fake", + metric="fake", + ) + trainer.run() + + # check + mock_report_func.assert_called_once_with(0, 0, done=True) # finilize report + assert self.hpo_workdir.exists() # make a directory to copy weight + for i in range(1, 5): # check model weights are copied + assert (self.hpo_workdir / "weight" / trial_id / self.weight_format.format(i)).exists() + + mock_task.train.assert_called() # check task.train() is called + + @e2e_pytest_unit + def test_run_trial_already_done(self, mocker, cls_template_path, mock_task, tmp_dir): + """Test a case where trial to run already training given epoch.""" + # prepare + trial_id = "1" + epoch_to_run = 10 + weight_dir = self.hpo_workdir / "weight" / trial_id + # prepare a weight trained more than given epoch + weight_dir.mkdir(parents=True) + (weight_dir / self.weight_format.format(epoch_to_run + 1)).touch() + mock_report_func = mocker.MagicMock() + + mocker.patch("otx.cli.utils.hpo.get_dataset_adapter") + mocker.patch("otx.cli.utils.hpo.HpoDataset") + + # run + trainer = Trainer( + hp_config={"configuration": {"iterations": epoch_to_run}, "id": trial_id}, + report_func=mock_report_func, + model_template=find_and_parse_model_template(cls_template_path), + data_roots=mocker.MagicMock(), + task_type=TaskType.CLASSIFICATION, + hpo_workdir=self.hpo_workdir, + initial_weight_name="fake", + metric="fake", + ) + trainer.run() + + # check + mock_report_func.assert_called_once_with(0, 0, done=True) # finilize report + mock_task.train.assert_not_called() # check task.train() is called + + @e2e_pytest_unit + def test_delete_unused_model_weight(self, mocker, cls_template_path): + # prepare + trial0_weight_dir = self.hpo_workdir / "weight" / "0" + mocker.patch( + "otx.cli.utils.hpo.TaskManager.get_latest_weight", return_value=str(trial0_weight_dir / "latest.pth") + ) + mocker.patch("otx.cli.utils.hpo.get_best_hpo_weight", return_value=str(trial0_weight_dir / "best.pth")) + + self.hpo_workdir.mkdir() + (self.hpo_workdir / "0.json").touch() + for i in range(2): + weight_dir = self.hpo_workdir / "weight" / str(i) + weight_dir.mkdir(parents=True) + (weight_dir / "latest.pth").touch() + (weight_dir / "best.pth").touch() + (weight_dir / "unused.pth").touch() + + # run + trainer = Trainer( + hp_config={"configuration": {"iterations": 10}, "id": "1"}, + report_func=mocker.MagicMock(), + model_template=find_and_parse_model_template(cls_template_path), + data_roots=mocker.MagicMock(), + task_type=TaskType.CLASSIFICATION, + hpo_workdir=self.hpo_workdir, + initial_weight_name="fake", + metric="fake", + ) + trainer._delete_unused_model_weight() + + assert sorted([f.name for f in (self.hpo_workdir / "weight" / "0").iterdir()]) == sorted( + ["latest.pth", "best.pth"] + ) + assert sorted([f.name for f in (self.hpo_workdir / "weight" / "1").iterdir()]) == sorted( + ["latest.pth", "best.pth", "unused.pth"] + ) + + +class TestHpoCallback: + @e2e_pytest_unit + def test_init(self, mocker): + HpoCallback(mocker.MagicMock(), "fake", 3, mocker.MagicMock()) + + @e2e_pytest_unit + @pytest.mark.parametrize("max_epoch", [-3, 0]) + def test_init_wrong_max_epoch(self, mocker, max_epoch): + with pytest.raises(ValueError): + HpoCallback(mocker.MagicMock(), "fake", max_epoch, mocker.MagicMock()) + + @e2e_pytest_unit + def test_call(self, mocker): + mock_report_func = mocker.MagicMock() + + hpo_call_back = HpoCallback(report_func=mock_report_func, metric="fake", max_epoch=50, task=mocker.MagicMock()) + hpo_call_back(progress=20, score=100) + + mock_report_func.assert_called_once_with(progress=10, score=100) + + @e2e_pytest_unit + def test_call_and_get_stop_flag(self, mocker): + mock_report_func = mocker.MagicMock() + mock_report_func.return_value = TrialStatus.STOP + mock_task = mocker.MagicMock() + + hpo_call_back = HpoCallback(report_func=mock_report_func, metric="fake", max_epoch=50, task=mock_task) + hpo_call_back(progress=20, score=100) + + mock_task.cancel_training.assert_called_once_with() + + @e2e_pytest_unit + def test_not_copy_report_func(self, mocker): + mock_report_func = mocker.MagicMock() + + hpo_call_back = HpoCallback(report_func=mock_report_func, metric="fake", max_epoch=50, task=mocker.MagicMock()) + new_hpo_call_back = deepcopy(hpo_call_back) + new_hpo_call_back(progress=20, score=100) + + mock_report_func.assert_called_once() + + +class TestHpoDataset: + @e2e_pytest_unit + def test_init(self, mocker): + hpo_dataset = HpoDataset(fullset=mocker.MagicMock(), config={"train_environment": {"subset_ratio": 0.5}}) + assert hpo_dataset.subset_ratio == 0.5 + + @e2e_pytest_unit + @pytest.mark.parametrize("subset_ratio", [0.1, 0.5, 1]) + def test_get_subset(self, mocker, subset_ratio): + mock_fullset = mocker.MagicMock() + mock_fullset.get_subset.return_value = [i for i in range(10)] + config = {"train_environment": {"subset_ratio": subset_ratio}} + + hpo_dataset = HpoDataset(fullset=mock_fullset, config=config) + hpo_sub_dataset = hpo_dataset.get_subset(Subset.TRAINING) + + num_hpo_sub_dataset = len(hpo_sub_dataset) + assert num_hpo_sub_dataset == round(10 * subset_ratio) + + for i in range(num_hpo_sub_dataset): + hpo_sub_dataset[i] + + @e2e_pytest_unit + def test_len_before_get_subset(self): + hpo_dataset = HpoDataset(fullset=range(10), config={"train_environment": {"subset_ratio": 0.5}}) + assert len(hpo_dataset) == 10 + + @e2e_pytest_unit + def test_getitem_before_get_subset(self): + hpo_dataset = HpoDataset(fullset=range(10), config={"train_environment": {"subset_ratio": 0.5}}) + + for _ in hpo_dataset: + pass + + +@e2e_pytest_unit +def test_run_hpo(mocker, mock_environment): + with TemporaryDirectory() as tmp_dir: + # prepare + output = Path(tmp_dir) / "fake" + mock_get_best_hpo_weight = mocker.patch("otx.cli.utils.hpo.get_best_hpo_weight") + mock_get_best_hpo_weight.return_value = "mock_best_weight_path" + hpo_weight_dir = output / "hpo" / "weight" + for i in range(3): + trial_weight_dir = hpo_weight_dir / str(i) + trial_weight_dir.mkdir(parents=True) + (trial_weight_dir / "fake.pth").touch() + + def mock_run_hpo(*args, **kwargs): + return {"config": {"a.b": 1, "c.d": 2}, "id": "1"} + + mock_hpo_runner_instance = mocker.MagicMock() + mock_hpo_runner_instance.run_hpo.side_effect = mock_run_hpo + mock_hpo_runner_class = mocker.patch("otx.cli.utils.hpo.HpoRunner") + mock_hpo_runner_class.return_value = mock_hpo_runner_instance + + def mock_read_model(args1, path, arg2): + return path + + mocker.patch("otx.cli.utils.hpo.read_model", mock_read_model) + + hpo_time_ratio = "4" + mock_environment.model_template.task_type = TaskType.CLASSIFICATION + + # run + environment = run_hpo(hpo_time_ratio, output, mock_environment, mocker.MagicMock(), mocker.MagicMock()) + + # check + mock_hpo_runner_instance.run_hpo.assert_called() # Check that HpoRunner.run_hpo is called + env_hp = environment.get_hyper_parameters() # Check that best HP is applied well. + assert env_hp.a.b == 1 + assert env_hp.c.d == 2 + assert environment.model == "mock_best_weight_path" # check that best model weight is used + assert not list(hpo_weight_dir.rglob("*.pth")) # check unused weight is removed + + +@e2e_pytest_unit +def test_run_hpo_not_supported_task(mocker, action_task_env): + mock_hpo_runner_instance = mocker.MagicMock() + mock_hpo_runner_class = mocker.patch("otx.cli.utils.hpo.HpoRunner") + mock_hpo_runner_class.return_value = mock_hpo_runner_instance + hpo_time_ratio = "4" + output = "fake" + + run_hpo(hpo_time_ratio, output, action_task_env, mocker.MagicMock(), mocker.MagicMock()) + mock_hpo_runner_instance.run_hpo.assert_not_called() + + +@e2e_pytest_unit +def test_run_hpo_with_torchrun(mocker, mock_environment): + # prepare + output = "fake" + mock_hpo_runner_instance = mocker.MagicMock() + mock_hpo_runner_class = mocker.patch("otx.cli.utils.hpo.HpoRunner") + mock_hpo_runner_class.return_value = mock_hpo_runner_instance + hpo_time_ratio = "4" + mock_environment.model_template.task_type = TaskType.CLASSIFICATION + mock_os = mocker.patch.object(hpo, "os", return_value={"TORCHELASTIC_RUN_ID": "1234"}) + mock_os.environ = {"TORCHELASTIC_RUN_ID": "1234"} + + # run + run_hpo(hpo_time_ratio, output, mock_environment, mocker.MagicMock(), mocker.MagicMock()) + mock_hpo_runner_instance.run_hpo.assert_not_called() + + +@e2e_pytest_unit +def test_get_best_hpo_weight(): + with TemporaryDirectory() as tmp_dir: + # prepare + hpo_dir = Path(tmp_dir) / "hpo" + weight_path = hpo_dir / "weight" + weight_path.mkdir(parents=True) + + score = {"score": {str(i): i for i in range(1, 11)}} + bracket_0_dir = hpo_dir / "0" + bracket_0_dir.mkdir(parents=True) + for trial_num in range(2): + with (bracket_0_dir / f"{trial_num}.json").open("w") as f: + json.dump(score, f) + trial_weight_path = weight_path / str(trial_num) + trial_weight_path.mkdir(parents=True) + for i in range(1, 11): + (trial_weight_path / f"epoch_{i}.pth").write_text("fake") + + assert get_best_hpo_weight(hpo_dir, "1") == str(weight_path / "1" / "epoch_10.pth") + + +@e2e_pytest_unit +def test_get_best_hpo_weight_not_exist(): + with TemporaryDirectory() as tmp_dir: + # prepare + hpo_dir = Path(tmp_dir) / "hpo" + weight_path = hpo_dir / "weight" + weight_path.mkdir(parents=True) + + score = {"score": {str(i): i for i in range(1, 11)}} + bracket_0_dir = hpo_dir / "0" + bracket_0_dir.mkdir(parents=True) + for trial_num in range(1): + with (bracket_0_dir / f"{trial_num}.json").open("w") as f: + json.dump(score, f) + trial_weight_path = weight_path / str(trial_num) + trial_weight_path.mkdir(parents=True) + for i in range(1, 11): + (trial_weight_path / f"epoch_{i}.pth").write_text("fake") + + assert get_best_hpo_weight(hpo_dir, "1") is None + + +@e2e_pytest_unit +def test_run_trial(mocker): + mock_run = mocker.patch.object(Trainer, "run") + run_trial( + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + ) + + mock_run.assert_called() diff --git a/tests/unit/cli/utils/test_importing.py b/tests/unit/cli/utils/test_importing.py new file mode 100644 index 00000000000..fb1b8ccefca --- /dev/null +++ b/tests/unit/cli/utils/test_importing.py @@ -0,0 +1,60 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import importlib +import inspect + +import pytest +from mmcv.utils import Registry + +from otx.cli.utils.importing import ( + get_backbone_list, + get_backbone_registry, + get_impl_class, + get_otx_root_path, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_get_impl_class(): + impl_path = "otx.algorithms.common.adapters.mmcv.models.OTXMobileNetV3" + task_impl_class = get_impl_class(impl_path) + assert task_impl_class.__name__ == "OTXMobileNetV3" + + +@e2e_pytest_unit +@pytest.mark.parametrize("backend", ["otx", "pytorchcv"]) +def test_get_backbone_list(backend): + available_backbones = get_backbone_list(backend) + assert isinstance(available_backbones, dict) + + +@e2e_pytest_unit +def test_get_backbone_list_for_unsupported_backend(): + backend = "invalid" + with pytest.raises(ValueError): + get_backbone_list(backend) + + +@e2e_pytest_unit +def test_get_backbone_registry(): + backend = "otx" + mm_registry, custom_imports = get_backbone_registry(backend) + assert custom_imports == ["otx.algorithms.common.adapters.mmcv.models"] + assert isinstance(mm_registry, Registry) + + +@e2e_pytest_unit +def test_get_backbone_registry_for_unsupported_backend(): + backend = "invalid" + with pytest.raises(ValueError): + get_backbone_registry(backend) + + +@e2e_pytest_unit +def test_get_otx_root_path(mocker): + mocker.patch.object(importlib, "import_module", return_value=mocker.MagicMock()) + mocker.patch.object(inspect, "getfile", return_value="src/otx/__init__.py") + otx_root_path = get_otx_root_path() + assert otx_root_path == "src/otx" diff --git a/tests/unit/cli/utils/test_installation.py b/tests/unit/cli/utils/test_installation.py deleted file mode 100644 index ecdc1552279..00000000000 --- a/tests/unit/cli/utils/test_installation.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import os -import tempfile -from pathlib import Path - -import pkg_resources -import pytest -from otx.cli.utils.installation import ( - add_hardware_suffix_to_torch, - get_cuda_suffix, - get_cuda_version, - get_hardware_suffix, - get_mmcv_install_args, - get_module_version, - get_requirements, - get_torch_install_args, - mim_installation, - parse_requirements, - update_cuda_version_with_available_torch_cuda_build, -) -from pkg_resources import Requirement -from pytest_mock import MockerFixture - - -@pytest.fixture() -def requirements_file() -> Path: - """Create a temporary requirements file with some example requirements.""" - requirements = ["numpy==1.19.5", "opencv-python-headless>=4.5.1.48"] - with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: - f.write("\n".join(requirements)) - return Path(f.name) - - -def test_get_requirements(mocker: MockerFixture) -> None: - """Test that get_requirements returns the expected dictionary of requirements.""" - requirements = get_requirements("otx") - assert isinstance(requirements, dict) - assert len(requirements) > 0 - for reqs in requirements.values(): - assert isinstance(reqs, list) - for req in reqs: - assert isinstance(req, Requirement) - mocker.patch("otx.cli.utils.installation.requires", return_value=None) - assert get_requirements() == {} - - -def test_parse_requirements() -> None: - """Test that parse_requirements returns the expected tuple of requirements.""" - requirements = [ - Requirement.parse("torch==2.0.0"), - Requirement.parse("mmengine==2.0.0"), - Requirement.parse("mmpretrain==0.12.0"), - Requirement.parse("onnx>=1.8.1"), - ] - torch_req, mm_reqs, other_reqs = parse_requirements(requirements) - assert isinstance(torch_req, str) - assert isinstance(mm_reqs, list) - assert isinstance(other_reqs, list) - assert torch_req == "torch==2.0.0" - assert "mmengine==2.0.0" in mm_reqs - assert "mmpretrain==0.12.0" in mm_reqs - assert other_reqs == ["onnx>=1.8.1"] - - requirements = [ - Requirement.parse("torch<=2.0.1, >=1.8.1"), - ] - torch_req, mm_reqs, other_reqs = parse_requirements(requirements) - assert torch_req == "torch<=2.0.1,>=1.8.1" - assert mm_reqs == [] - assert other_reqs == [] - - requirements = [ - Requirement.parse("onnx>=1.8.1"), - ] - with pytest.raises(ValueError, match="Could not find torch requirement."): - parse_requirements(requirements) - - -def test_get_cuda_version_with_version_file(mocker: MockerFixture, tmp_path: Path) -> None: - """Test that get_cuda_version returns the expected CUDA version when version file exists.""" - tmp_path = tmp_path / "cuda" - tmp_path.mkdir() - mocker.patch.dict(os.environ, {"CUDA_HOME": str(tmp_path)}) - version_file = tmp_path / "version.json" - version_file.write_text('{"cuda": {"version": "11.2.0"}}') - assert get_cuda_version() == "11.2" - - -def test_get_cuda_version_with_nvcc(mocker: MockerFixture) -> None: - """Test that get_cuda_version returns the expected CUDA version when nvcc is available.""" - mock_run = mocker.patch("otx.cli.utils.installation.Path.exists", return_value=False) - mock_run = mocker.patch("subprocess.run") - mock_run.return_value.stdout = "Build cuda_11.2.r11.2/compiler.00000_0" - assert get_cuda_version() == "11.2" - - mock_run = mocker.patch("subprocess.run") - mock_run.side_effect = FileNotFoundError - assert get_cuda_version() is None - - -def test_update_cuda_version_with_available_torch_cuda_build() -> None: - """Test that update_cuda_version_with_available_torch_cuda_build returns the expected CUDA version.""" - assert update_cuda_version_with_available_torch_cuda_build("11.1", "1.13.0") == "11.6" - assert update_cuda_version_with_available_torch_cuda_build("11.7", "1.13.0") == "11.7" - assert update_cuda_version_with_available_torch_cuda_build("11.8", "1.13.0") == "11.7" - assert update_cuda_version_with_available_torch_cuda_build("12.1", "2.0.1") == "11.8" - - -def test_get_cuda_suffix() -> None: - assert get_cuda_suffix(cuda_version="11.2") == "cu112" - assert get_cuda_suffix(cuda_version="11.8") == "cu118" - - -def test_get_hardware_suffix(mocker: MockerFixture) -> None: - mocker.patch("otx.cli.utils.installation.get_cuda_version", return_value="11.2") - assert get_hardware_suffix() == "cu112" - - mocker.patch("otx.cli.utils.installation.get_cuda_version", return_value="12.1") - assert get_hardware_suffix(with_available_torch_build=True, torch_version="2.0.1") == "cu118" - - with pytest.raises(ValueError, match="``torch_version`` must be provided"): - get_hardware_suffix(with_available_torch_build=True) - - mocker.patch("otx.cli.utils.installation.get_cuda_version", return_value=None) - assert get_hardware_suffix() == "cpu" - - -def test_add_hardware_suffix_to_torch(mocker: MockerFixture) -> None: - """Test that add_hardware_suffix_to_torch returns the expected updated requirement.""" - mocker.patch("otx.cli.utils.installation.get_hardware_suffix", return_value="cu121") - requirement = Requirement.parse("torch>=1.13.0, <=2.0.1") - updated_requirement = add_hardware_suffix_to_torch(requirement) - assert "torch" in updated_requirement - assert ">=1.13.0+cu121" in updated_requirement - assert "<=2.0.1+cu121" in updated_requirement - - requirement = Requirement.parse("torch==2.0.1") - mocker.patch("otx.cli.utils.installation.get_hardware_suffix", return_value="cu118") - updated_requirement = add_hardware_suffix_to_torch(requirement, with_available_torch_build=True) - assert updated_requirement == "torch==2.0.1+cu118" - - requirement = Requirement.parse("torch==2.0.1") - updated_requirement = add_hardware_suffix_to_torch(requirement, hardware_suffix="cu111") - assert updated_requirement == "torch==2.0.1+cu111" - - requirement = Requirement.parse("torch>=1.13.0, <=2.0.1, !=1.14.0") - with pytest.raises(ValueError, match="Requirement version can be a single value or a range."): - add_hardware_suffix_to_torch(requirement) - - -def test_get_torch_install_args(mocker: MockerFixture) -> None: - """Test that get_torch_install_args returns the expected install arguments.""" - requirement = Requirement.parse("torch>=1.13.0") - mocker.patch("otx.cli.utils.installation.platform.system", return_value="Linux") - mocker.patch("otx.cli.utils.installation.get_hardware_suffix", return_value="cpu") - install_args = get_torch_install_args(requirement) - expected_args = [ - "--extra-index-url", - "https://download.pytorch.org/whl/cpu", - "torch>=1.13.0+cpu", - "torchvision>=0.14.0+cpu", - ] - for arg in expected_args: - assert arg in install_args - - requirement = Requirement.parse("torch>=1.13.0,<=2.0.1") - mocker.patch("otx.cli.utils.installation.get_hardware_suffix", return_value="cu111") - install_args = get_torch_install_args(requirement) - expected_args = [ - "--extra-index-url", - "https://download.pytorch.org/whl/cu111", - ] - for arg in expected_args: - assert arg in install_args - - requirement = Requirement.parse("torch==2.0.1") - expected_args = [ - "--extra-index-url", - "https://download.pytorch.org/whl/cu111", - "torch==2.0.1+cu111", - "torchvision==0.15.2+cu111", - ] - install_args = get_torch_install_args(requirement) - for arg in expected_args: - assert arg in install_args - - install_args = get_torch_install_args("torch") - assert install_args == ["torch"] - - mocker.patch("otx.cli.utils.installation.platform.system", return_value="Darwin") - requirement = Requirement.parse("torch==2.0.1") - install_args = get_torch_install_args(requirement) - assert install_args == ["torch==2.0.1"] - - mocker.patch("otx.cli.utils.installation.platform.system", return_value="Unknown") - with pytest.raises(RuntimeError, match="Unsupported OS: Unknown"): - get_torch_install_args(requirement) - - -def test_get_mmcv_install_args(mocker: MockerFixture) -> None: - """Test that get_mmcv_install_args returns the expected install arguments.""" - torch_requirement = Requirement.parse("torch>=1.13.0") - mocker.patch("otx.cli.utils.installation.platform.system", return_value="Linux") - mocker.patch("otx.cli.utils.installation.get_hardware_suffix", return_value="cpu") - install_args = get_mmcv_install_args(torch_requirement=torch_requirement, mmcv_requirements=["mmengine==2.0.0"]) - expected_args = [ - "--find-links", - "https://download.openmmlab.com/mmcv/dist/cpu/torch1.13.0/index.html", - "mmengine==2.0.0", - ] - assert install_args == expected_args - - install_args = get_mmcv_install_args(torch_requirement="torch==2.0.1", mmcv_requirements=["mmengine==2.0.0"]) - expected_args = [ - "--find-links", - "https://download.openmmlab.com/mmcv/dist/cpu/torch2.0.0/index.html", - "mmengine==2.0.0", - ] - assert install_args == expected_args - - mocker.patch("otx.cli.utils.installation.platform.system", return_value="Unknown") - with pytest.raises(RuntimeError, match="Unsupported OS: Unknown"): - get_mmcv_install_args(torch_requirement=torch_requirement, mmcv_requirements=["mmengine==2.0.0"]) - - -def test_mim_installation(mocker: MockerFixture) -> None: - mocker.patch("otx.cli.utils.installation.find_spec", return_value=True) - # https://github.com/Madoshakalaka/pipenv-setup/issues/101 - os.environ["SETUPTOOLS_USE_DISTUTILS"] = "stdlib" - mock_mim_install = mocker.patch("mim.install") - mim_installation(["mmengine==2.0.0"]) - mock_mim_install.assert_called_once_with(["mmengine==2.0.0"]) - - mocker.patch("otx.cli.utils.installation.find_spec", return_value=False) - with pytest.raises(ModuleNotFoundError, match="The mmX library installation requires mim."): - mim_installation(["mmengine==2.0.0"]) - - -def test_get_module_version(mocker: MockerFixture) -> None: - mock_get_distribution = mocker.patch("otx.cli.utils.installation.pkg_resources.get_distribution") - mock_get_distribution.return_value.version = "1.0.0" - version = get_module_version("test") - assert version == "1.0.0" - mock_get_distribution.assert_called_once_with("test") - - mock_get_distribution.side_effect = pkg_resources.DistributionNotFound - version = get_module_version("test") - assert version is None diff --git a/tests/unit/cli/utils/test_io.py b/tests/unit/cli/utils/test_io.py new file mode 100644 index 00000000000..cd0ad9a0aff --- /dev/null +++ b/tests/unit/cli/utils/test_io.py @@ -0,0 +1,284 @@ +import json +import shutil +import struct +from os import path as osp +from pathlib import Path + +import cv2 +import numpy as np +import pytest + +from otx.api.entities.model import ModelOptimizationType +from otx.api.usecases.adapters.model_adapter import ModelAdapter +from otx.cli.utils import io as target_package +from otx.cli.utils.io import ( + get_explain_dataset_from_filelist, + get_image_files, + read_binary, + read_label_schema, + read_model, + save_model_data, + save_saliency_output, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + +IMG_DATA_FORMATS = ( + ".jpg", + ".JPG", + ".jpeg", + ".JPEG", + ".gif", + ".GIF", + ".bmp", + ".BMP", + ".tif", + ".TIF", + ".tiff", + ".TIFF", + ".png", + ".PNG", +) + + +@e2e_pytest_unit +def test_save_model_data(mocker, tmp_dir): + mock_model = mocker.MagicMock() + model_adapter = ModelAdapter(b"fake") + file_name = "model.pth" + mock_model.model_adapters = {file_name: model_adapter} + + save_model_data(mock_model, tmp_dir) + + with open(osp.join(tmp_dir, file_name), "rb") as f: + assert f.readline() == b"fake" + + +@e2e_pytest_unit +def test_read_binary(tmp_dir): + file_path = osp.join(tmp_dir, "test.txt") + with open(file_path, "w") as f: + f.write("fake") + + assert read_binary(file_path) == b"fake" + + +@pytest.fixture +def model_adapter_keys(): + return ( + "confidence_threshold", + "metadata", + "config.json", + ) + + +@e2e_pytest_unit +def test_read_xml_bin_model(mocker, model_adapter_keys, tmp_dir): + # prepare + tmp_dir = Path(tmp_dir) + for key in model_adapter_keys: + (tmp_dir / key).write_text(key) + + xml_model_path = tmp_dir / "model.xml" + xml_model_path.write_text("xml_model") + bin_model_path = tmp_dir / "model.bin" + bin_model_path.write_text("bin_model") + + # run + model = read_model(mocker.MagicMock(), str(xml_model_path), mocker.MagicMock()) + + # check + model_adapters = model.model_adapters + assert model_adapters["openvino.xml"].data == b"xml_model" + assert model_adapters["openvino.bin"].data == b"bin_model" + for key in model_adapter_keys: + assert model_adapters[key].data == bytes(key, "utf-8") + + +@e2e_pytest_unit +def test_read_pth_model(mocker, tmp_dir): + # prepare + tmp_dir = Path(tmp_dir) + model_path = tmp_dir / "model.pth" + model_path.write_text("model") + mock_is_checkpoint_nncf = mocker.patch.object(target_package, "is_checkpoint_nncf") + mock_is_checkpoint_nncf.return_value = True + + for i in range(1, 5): + (tmp_dir / f"aux_model_{i}.pth").write_text(f"aux_{i}") + + # run + model = read_model(mocker.MagicMock(), str(model_path), mocker.MagicMock()) + + # check + model_adapters = model.model_adapters + assert model_adapters["weights.pth"].data == b"model" + for i in range(1, 5): + model_adapters[f"aux_model_{i}.pth"].data == bytes(f"aux_model_{i}.pth", "utf-8") + assert model.optimization_type == ModelOptimizationType.NNCF + + +@pytest.fixture +def mock_zip_file(model_adapter_keys, tmp_dir) -> str: + """Mock zip file. + + zip file structure is as below. + model/model.xml + model/model.bin + model/config.json + model_adapter_keys => should have float value + """ + tmp_dir = Path(tmp_dir) + model_zip_dir = tmp_dir / "model_zip" + model_zip_dir.mkdir() + model_dir_in_zip = model_zip_dir / "model" + model_dir_in_zip.mkdir() + (model_dir_in_zip / "model.xml").write_text("xml_model") + (model_dir_in_zip / "model.bin").write_text("bin_model") + with (model_dir_in_zip / "config.json").open("w") as f: + model_config = {"model_parameters": {}} + for key in model_adapter_keys: + if key != "metadata": + model_config["model_parameters"][key] = 0.1234 + model_config["model_parameters"]["labels"] = {"fake": "fake"} + json.dump(model_config, f) + + shutil.make_archive(tmp_dir / "model", "zip", model_zip_dir) + + return tmp_dir / "model.zip" + + +@e2e_pytest_unit +def test_read_zip_model(mocker, model_adapter_keys, mock_zip_file): + # run + model = read_model(mocker.MagicMock(), str(mock_zip_file), mocker.MagicMock()) + + # check + model_adapters = model.model_adapters + assert model_adapters["openvino.xml"].data == b"xml_model" + assert model_adapters["openvino.bin"].data == b"bin_model" + for key in model_adapter_keys: + if key != "metadata": + assert model_adapters[key].data == struct.pack("f", 0.1234) + + +@e2e_pytest_unit +def test_read_unknown_model(mocker): + with pytest.raises(ValueError): + read_model(mocker.MagicMock(), "fake.unknown", mocker.MagicMock()) + + +@pytest.fixture +def mock_lebel_schema_mapper_instance(mocker): + mock_lebel_schema_mapper_class = mocker.patch.object(target_package, "LabelSchemaMapper") + mock_lebel_schema_mapper_instance = mocker.MagicMock() + mock_lebel_schema_mapper_class.return_value = mock_lebel_schema_mapper_instance + return mock_lebel_schema_mapper_instance + + +@e2e_pytest_unit +@pytest.mark.parametrize("extension", [".xml", ".bin", ".pth"]) +def test_read_label_schema_with_model_file(mock_lebel_schema_mapper_instance, extension, tmp_dir): + # prepare + tmp_dir = Path(tmp_dir) + + model_path = tmp_dir / f"model.{extension}" + model_path.write_text("fake") + fake_label_schema = {"fake": "fake"} + with (tmp_dir / "label_schema.json").open("w") as f: + json.dump(fake_label_schema, f) + + # run + read_label_schema(str(model_path)) + + # check + mock_lebel_schema_mapper_instance.backward.assert_called_once_with(fake_label_schema) + + +@e2e_pytest_unit +def test_read_label_schema_with_model_zipfile(mock_lebel_schema_mapper_instance, mock_zip_file): + # run + read_label_schema(str(mock_zip_file)) + + # check + mock_lebel_schema_mapper_instance.backward.assert_called_once_with({"fake": "fake"}) + + +@e2e_pytest_unit +@pytest.mark.parametrize("img_data_format", IMG_DATA_FORMATS) +def test_get_single_image_files(img_data_format): + img_files = get_image_files(f"fake_path/img{img_data_format}") + + assert img_files[0] == ("./", f"fake_path/img{img_data_format}") + + +@e2e_pytest_unit +def test_get_image_files_in_dir(tmp_dir): + # prepare + tmp_dir = Path(tmp_dir) + sub_dir_1 = tmp_dir / "sub_dir_1" + sub_dir_1_1 = sub_dir_1 / "sub_dir_1_1" + sub_dir_2 = tmp_dir / "sub_dir_2" + sub_dir_1_1.mkdir(parents=True) + sub_dir_2.mkdir() + (sub_dir_1_1 / "fake.jpg").write_text("fake") + (sub_dir_2 / "fake.jpg").write_text("fake") + + # run + img_files = get_image_files(str(tmp_dir)) + + # check + for img_file in img_files: + assert img_file in ((str(sub_dir_1_1), "fake.jpg"), (str(sub_dir_2), "fake.jpg")) + + +@e2e_pytest_unit +def test_get_image_files_empty_dir(tmp_dir): + assert get_image_files(tmp_dir) is None + + +@e2e_pytest_unit +@pytest.mark.parametrize( + "process_saliency_maps", + [True, False], + ids=["w_post_processing", "wo_post_processing"], +) +def test_save_saliency_output(tmp_dir, process_saliency_maps): + # prepare + img = np.array([[100 for _ in range(3)] for _ in range(3)]) + saliency_map = np.zeros([3, 3], dtype=np.uint8) + weight = 0.3 + + # run + save_saliency_output(process_saliency_maps, img, saliency_map, tmp_dir, "fake", weight=weight) + + # check + if process_saliency_maps: + saliency_map_file = Path(tmp_dir) / "fake_saliency_map.png" + else: + saliency_map_file = Path(tmp_dir) / "fake_saliency_map.tiff" + assert saliency_map_file.exists() + saved_saliency = cv2.imread(str(saliency_map_file)) + assert (saved_saliency == saliency_map).all() + + if process_saliency_maps: + overlay_img = Path(tmp_dir) / "fake_overlay_img.png" + assert overlay_img.exists() + saved_overlay = cv2.imread(str(overlay_img)) + assert (saved_overlay == np.array([[100 * weight for _ in range(3)] for _ in range(3)])).all() + + +@e2e_pytest_unit +def test_get_explain_dataset_from_filelist(tmp_dir): + # prepare + tmp_dir = Path(tmp_dir) + fake_img = np.array([[[i for _ in range(3)] for _ in range(3)] for i in range(1, 4)]) + cv2.imwrite(str(tmp_dir / "fake1.jpg"), fake_img) + cv2.imwrite(str(tmp_dir / "fake2.jpg"), fake_img) + image_files = [(tmp_dir, "fake1.jpg"), (tmp_dir, "fake2.jpg")] + + # run + dataset_entity = get_explain_dataset_from_filelist(image_files) + + # check + assert (dataset_entity[0].media.numpy == fake_img).all() + assert (dataset_entity[1].media.numpy == fake_img).all() diff --git a/tests/unit/cli/utils/test_jsonargparse.py b/tests/unit/cli/utils/test_jsonargparse.py deleted file mode 100644 index 2137d76526e..00000000000 --- a/tests/unit/cli/utils/test_jsonargparse.py +++ /dev/null @@ -1,248 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import pytest -from jsonargparse import Namespace -from otx.cli.utils.jsonargparse import ( - flatten_dict, - get_configuration, - get_short_docstring, - list_override, - patch_update_configs, -) - - -@pytest.fixture() -def fxt_configs() -> Namespace: - return Namespace( - data=Namespace(batch_size=32, num_workers=4), - callbacks=[ - Namespace( - class_path="otx.algo.callbacks.iteration_timer.IterationTimer", - init_args=Namespace(prog_bar=True), - ), - Namespace( - class_path="lightning.pytorch.callbacks.EarlyStopping", - init_args=Namespace(patience=10), - ), - Namespace( - class_path="lightning.pytorch.callbacks.RichModelSummary", - init_args=Namespace(max_depth=1), - ), - ], - logger=[ - Namespace( - class_path="lightning.pytorch.loggers.csv_logs.CSVLogger", - init_args=Namespace(name="csv/"), - ), - Namespace( - class_path="lightning.pytorch.loggers.tensorboard.TensorBoardLogger", - init_args=Namespace(name="tensorboard/"), - ), - ], - ) - - -def test_list_override(fxt_configs) -> None: - with patch_update_configs(): - list_override(fxt_configs, "callbacks", []) - assert fxt_configs.callbacks[0].init_args.prog_bar - assert fxt_configs.callbacks[1].init_args.patience == 10 - assert fxt_configs.callbacks[2].init_args.max_depth == 1 - - # Wrong Config overriding - wrong_override = [ - { - "init_args": {"patience": 3}, - }, - ] - with pytest.raises(ValueError, match="class_path is required in the override list."): - list_override(fxt_configs, "callbacks", wrong_override) - - callbacks_override = [ - { - "class_path": "lightning.pytorch.callbacks.EarlyStopping", - "init_args": {"patience": 3}, - }, - ] - list_override(fxt_configs, "callbacks", callbacks_override) - assert fxt_configs.callbacks[1].init_args.patience == 3 - - logger_override = [ - { - "class_path": "lightning.pytorch.loggers.tensorboard.TensorBoardLogger", - "init_args": {"name": "workspace/"}, - }, - ] - list_override(fxt_configs, "logger", logger_override) - assert fxt_configs.logger[1].init_args.name == "workspace/" - - new_callbacks_override = [ - { - "class_path": "lightning.pytorch.callbacks.NewCallBack", - "init_args": {"patience": 100}, - }, - ] - list_override(fxt_configs, "callbacks", new_callbacks_override) - assert len(fxt_configs.callbacks) == 4 - assert fxt_configs.callbacks[3].class_path == "lightning.pytorch.callbacks.NewCallBack" - assert fxt_configs.callbacks[3].init_args.patience == 100 - - -def test_update(fxt_configs) -> None: - with patch_update_configs(): - # Test KeyError - with pytest.raises(KeyError): - fxt_configs.update(value=8, key=None) - - # Test updating a single key - updated_configs = fxt_configs.update(8, "data.batch_size") - assert updated_configs.data.batch_size == 8 - - updated_configs = fxt_configs.update("8", "data.batch_size") - assert updated_configs.data.batch_size == 8 - - updated_configs = fxt_configs.update(None, "data.num_workers") - assert "num_workers" not in updated_configs.data - - # Test updating multiple values using a namespace - new_values = Namespace( - callbacks=[ - Namespace( - class_path="new_callback", - init_args=Namespace(prog_bar=False), - ), - ], - logger=[ - Namespace( - class_path="new_logger", - init_args=Namespace(name="new_name"), - ), - ], - ) - updated_configs = fxt_configs.update(new_values) - assert updated_configs.callbacks[0].class_path == "new_callback" - assert updated_configs.callbacks[0].init_args.prog_bar is False - assert updated_configs.logger[0].class_path == "new_logger" - assert updated_configs.logger[0].init_args.name == "new_name" - - # Test updating multiple values using a dictionary - new_values_dict = { - "callbacks": [ - { - "class_path": "updated_callback", - "init_args": {"prog_bar": True}, - }, - ], - "logger": [ - { - "class_path": "updated_logger", - "init_args": {"name": "updated_name"}, - }, - ], - } - updated_configs = fxt_configs.update(new_values_dict) - assert updated_configs.callbacks[0].class_path == "updated_callback" - assert updated_configs.callbacks[0].init_args.prog_bar is True - assert updated_configs.logger[0].class_path == "updated_logger" - assert updated_configs.logger[0].init_args.name == "updated_name" - - -def test_get_short_docstring() -> None: - class Component: - """This is a component.""" - - def method(self) -> None: - """This is a method.""" - - class WithoutDocstring: - pass - - assert get_short_docstring(Component) == "This is a component." - assert get_short_docstring(Component.method) == "This is a method." - assert get_short_docstring(WithoutDocstring) == "" - - -def test_flatten_dict() -> None: - # Test case 1: Flattening an empty dictionary - config: dict = {} - flattened = flatten_dict(config) - assert flattened == {} - - # Test case 2: Flattening a dictionary with nested keys - config_1 = { - "a": { - "b": { - "c": 1, - "d": 2, - }, - "e": 3, - }, - "f": 4, - } - flattened = flatten_dict(config_1) - expected_1 = { - "a.b.c": 1, - "a.b.d": 2, - "a.e": 3, - "f": 4, - } - assert flattened == expected_1 - - # Test case 3: Flattening a dictionary with custom separator - config_2 = { - "a": { - "b": { - "c": 1, - "d": 2, - }, - "e": 3, - }, - "f": 4, - } - flattened = flatten_dict(config_2, sep="-") - expected_2 = { - "a-b-c": 1, - "a-b-d": 2, - "a-e": 3, - "f": 4, - } - assert flattened == expected_2 - - # Test case 4: Flattening a dictionary with non-string keys - config_3 = { - 1: { - 2: { - 3: "value", - }, - }, - } - flattened = flatten_dict(config_3) - expected_3 = { - "1.2.3": "value", - } - assert flattened == expected_3 - - -def test_get_configuration(tmp_path): - # Create a temporary configuration file - config_file = tmp_path / "config.yaml" - config_file.write_text( - """ - data: - task: SEMANTIC_SEGMENTATION - callback_monitor: test/f1 - """, - ) - - # Call the get_configuration function - config = get_configuration(config_file) - assert "config" in config - assert config["config"] == [config_file] - assert "engine" in config - assert "data" in config - assert config["data"]["task"] == "SEMANTIC_SEGMENTATION" - - cli_args = ["verbose", "data_root", "task", "seed", "callback_monitor", "resume", "disable_infer_num_classes"] - for arg in cli_args: - assert arg not in config diff --git a/tests/unit/cli/utils/test_multi_gpu.py b/tests/unit/cli/utils/test_multi_gpu.py new file mode 100644 index 00000000000..1edc7f40a91 --- /dev/null +++ b/tests/unit/cli/utils/test_multi_gpu.py @@ -0,0 +1,441 @@ +import datetime +import os +import socket +from contextlib import closing +from copy import deepcopy + +import pytest + +from otx.cli.utils import multi_gpu +from otx.cli.utils.multi_gpu import ( + MultiGPUManager, + _get_free_port, + get_gpu_ids, + is_multigpu_child_process, + set_arguments_to_argv, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + +NUM_AVAILABLE_GPU = 4 + + +@pytest.fixture(autouse=True) +def mocking_torch_device_count(mocker): + mock_torch = mocker.patch.object(multi_gpu, "torch") + mock_torch.cuda.device_count.return_value = NUM_AVAILABLE_GPU + + +@e2e_pytest_unit +def test_get_free_port(): + free_port = _get_free_port() + + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + sock.bind(("", free_port)) + + +@e2e_pytest_unit +def test_get_gpu_ids(): + gpus = [] + for i in range(0, NUM_AVAILABLE_GPU, 2): + gpus.append(i) + + expected_result = deepcopy(gpus) + gpus.append(NUM_AVAILABLE_GPU + 2) + + assert get_gpu_ids(",".join([str(val) for val in gpus])) == expected_result + + +@e2e_pytest_unit +def test_get_gpu_ids_with_wrong_args(): + with pytest.raises(ValueError): + get_gpu_ids("abcd") + + +@pytest.fixture +def mock_argv_without_params(mocker): + mock_sys = mocker.patch("otx.cli.utils.multi_gpu.sys") + mock_sys.argv = ["--a_key", "a_val", "--b_key"] + return mock_sys.argv + + +@pytest.fixture +def mock_argv_with_params(mock_argv_without_params): + mock_argv_without_params.extend(["params", "--c_key", "c_val", "--d_key"]) + return mock_argv_without_params + + +@e2e_pytest_unit +def test_set_arguments_to_argv_key_exist(mock_argv_without_params): + """Test a case where key already exists and value exists.""" + other_val = "other_val" + set_arguments_to_argv("--a_key", other_val) + + assert mock_argv_without_params[1] == other_val + + +@e2e_pytest_unit +def test_set_arguments_to_argv_keys_exist(mock_argv_without_params): + """Test a case where key already exists and value exists.""" + other_val = "other_val" + set_arguments_to_argv(["--a_key", "-a"], other_val) + + assert mock_argv_without_params[1] == other_val + + +@e2e_pytest_unit +def test_set_arguments_to_argv_key_exist_none_val(mock_argv_without_params): + """Test a case where key already exists in argv and value doesn't exists.""" + expected_result = deepcopy(mock_argv_without_params) + set_arguments_to_argv("--a_key") + + assert mock_argv_without_params == expected_result + + +@e2e_pytest_unit +def test_set_arguments_to_argv_key(mock_argv_with_params): + """Test a case where key to set doesn't exists in argv and order of key is before params and vlaue exists.""" + set_arguments_to_argv("--other_key", "other_val") + + param_idx = mock_argv_with_params.index("params") + new_key_idx = mock_argv_with_params.index("--other_key") + + assert new_key_idx < param_idx + assert mock_argv_with_params[new_key_idx + 1] == "other_val" + + +@e2e_pytest_unit +def test_set_arguments_to_argv_key_none_val(mock_argv_with_params): + """Test a case where key to set doesn't exists in argv and order of key is before params and vlaue doesn't exist.""" + set_arguments_to_argv("--other_key") + + param_idx = mock_argv_with_params.index("params") + new_key_idx = mock_argv_with_params.index("--other_key") + + assert new_key_idx < param_idx + assert "--other_key" in mock_argv_with_params + + +@e2e_pytest_unit +def test_set_arguments_to_argv_key_after_param(mock_argv_with_params): + """Test a case where key to set doesn't exists in argv and order of key is after params and vlaue exists.""" + set_arguments_to_argv("--other_key", "other_val", True) + + param_idx = mock_argv_with_params.index("params") + new_key_idx = mock_argv_with_params.index("--other_key") + + assert new_key_idx > param_idx + assert mock_argv_with_params[new_key_idx + 1] == "other_val" + + +@e2e_pytest_unit +def test_set_arguments_to_argv_key_after_param_non_val(mock_argv_with_params): + """Test a case where key to set doesn't exists in argv and order of key is after params and vlaue doesn't exist.""" + set_arguments_to_argv("--other_key", after_params=True) + + param_idx = mock_argv_with_params.index("params") + new_key_idx = mock_argv_with_params.index("--other_key") + + assert new_key_idx > param_idx + assert "--other_key" in mock_argv_with_params + + +@e2e_pytest_unit +def test_is_multigpu_child_process(mocker): + mocker.patch.object(multi_gpu.dist, "is_initialized", return_value=True) + os.environ["LOCAL_RANK"] = "1" + assert is_multigpu_child_process() + + +@e2e_pytest_unit +def test_is_multigpu_child_process_no_initialized(mocker): + mocker.patch.object(multi_gpu.dist, "is_initialized", return_value=False) + os.environ["LOCAL_RANK"] = "1" + assert not is_multigpu_child_process() + + +@e2e_pytest_unit +def test_is_multigpu_child_process_rank0(mocker): + mocker.patch.object(multi_gpu.dist, "is_initialized", return_value=True) + os.environ["LOCAL_RANK"] = "0" + assert not is_multigpu_child_process() + + +class TestMultiGPUManager: + @pytest.fixture(autouse=True) + def _set_up(self, mocker): + self.mock_singal = mocker.patch.object(multi_gpu, "signal") + self.mock_thread = mocker.patch.object(multi_gpu.threading, "Thread") + self.mock_train_func = mocker.MagicMock() + self.mock_mp = mocker.patch.object(multi_gpu, "mp") + self.mock_process = mocker.MagicMock() + self.mock_mp.get_context.return_value.Process = self.mock_process + self.num_gpu = NUM_AVAILABLE_GPU + self.mock_os = mocker.patch.object(multi_gpu, "os") + self.mock_os.environ = {} + self.mock_os.getpid.return_value = os.getpid() + + self.multigpu_manager = MultiGPUManager(self.mock_train_func, ",".join([str(i) for i in range(self.num_gpu)])) + + @pytest.fixture + def process_arr(self, mocker): + """List consists of normal process excpet last one. Last element is a process which exit abnormally.""" + normal_process = mocker.MagicMock() + normal_process.is_alive.return_value = True + wrong_process = mocker.MagicMock() + wrong_process.is_alive.return_value = False + wrong_process.exitcode = 1 + process_arr = [] + for _ in range(self.num_gpu - 2): + process_arr.append(deepcopy(normal_process)) + process_arr.append(wrong_process) + + return process_arr + + @e2e_pytest_unit + def test_init(self, mocker): + elapsed_second = 180 + start_time = datetime.datetime.now() - datetime.timedelta(seconds=elapsed_second) + MultiGPUManager(mocker.MagicMock(), "0,1", "localhost:0", start_time=start_time) + + # check torch.dist.init_process_group timeout value is adapted if elapsed time is bigger than criteria. + assert int(self.mock_os.environ.get("TORCH_DIST_TIMEOUT", 60)) >= int(elapsed_second * 1.5) + + @e2e_pytest_unit + @pytest.mark.parametrize("num_gpu", [4, 10]) + def test_is_available(self, mocker, num_gpu): + multigpu_manager = MultiGPUManager( + mocker.MagicMock(), ",".join([str(i) for i in range(num_gpu)]), "localhost:0" + ) + + assert multigpu_manager.is_available() + + @e2e_pytest_unit + def test_is_unavailable(self, mocker): + mock_torch = mocker.patch.object(multi_gpu, "torch") + mock_torch.cuda.device_count.return_value = 0 + multigpu_manager = MultiGPUManager(mocker.MagicMock(), ",".join([str(i) for i in range(4)]), "localhost:0") + + assert not multigpu_manager.is_available() + + @e2e_pytest_unit + def test_is_unavailable_by_torchrun(self, mocker): + self.mock_os.environ = {"TORCHELASTIC_RUN_ID": "1234"} + multigpu_manager = MultiGPUManager(mocker.MagicMock(), ",".join([str(i) for i in range(4)]), "localhost:0") + + assert not multigpu_manager.is_available() + + @e2e_pytest_unit + def test_setup_multi_gpu_train(self, mocker): + # prepare + mock_initialize_multigpu_train = mocker.patch.object(MultiGPUManager, "initialize_multigpu_train") + mock_hyper_parameters = mocker.MagicMock() + mock_hyper_parameters.learning_parameters.learning_rate = 0.01 + mock_hyper_parameters.learning_parameters.batch_size = 8 + mock_sys = mocker.patch.object(multi_gpu, "sys") + mock_sys.argv = [] + fake_output_path = "fake" + + # run + self.multigpu_manager.setup_multi_gpu_train(fake_output_path, mock_hyper_parameters) + + # check spwaning child process + assert self.mock_process.call_count == self.num_gpu - 1 + assert self.mock_process.return_value.start.call_count == self.num_gpu - 1 + assert self.mock_process.call_args.kwargs["target"] == MultiGPUManager.run_child_process + assert self.mock_process.call_args.kwargs["args"][0] == self.mock_train_func # train_func + assert self.mock_process.call_args.kwargs["args"][1] == fake_output_path # output_path + assert self.mock_process.call_args.kwargs["args"][-1] == self.num_gpu # num_gpu + + # check initialize multigpu trian + mock_initialize_multigpu_train.assert_called_once() + assert mock_initialize_multigpu_train.call_args.args[-1] == self.num_gpu # world_size + assert mock_initialize_multigpu_train.call_args.args[-2] == list(range(self.num_gpu)) # gpu_ids + + # check that making a thread to check child process is alive + self.mock_thread.assert_called_once_with(target=self.multigpu_manager._check_child_processes_alive, daemon=True) + self.mock_thread.return_value.start.assert_called_once() + + # check that register signal callback + assert self.mock_singal.signal.call_count == 2 + mock_singal_args = self.mock_singal.signal.call_args_list + assert mock_singal_args[0][0][0] in (self.mock_singal.SIGINT, self.mock_singal.SIGTERM) + assert mock_singal_args[1][0][0] in (self.mock_singal.SIGINT, self.mock_singal.SIGTERM) + assert mock_singal_args[0][0][1] == self.multigpu_manager._terminate_signal_handler + assert mock_singal_args[1][0][1] == self.multigpu_manager._terminate_signal_handler + + # check that optimized hyper parameters are in sys.argv to pass them to child process + assert "--learning_parameters.learning_rate" in mock_sys.argv + assert mock_sys.argv[mock_sys.argv.index("--learning_parameters.learning_rate") + 1] == "0.01" + assert "--learning_parameters.batch_size" in mock_sys.argv + assert mock_sys.argv[mock_sys.argv.index("--learning_parameters.batch_size") + 1] == "8" + + @e2e_pytest_unit + def test_check_child_processes_alive(self, mocker, process_arr): + # prepare + mock_kill = mocker.patch.object(multi_gpu.os, "kill") + mocker.patch.object(multi_gpu.time, "sleep") + mocker.patch.object(MultiGPUManager, "initialize_multigpu_train") + self.mock_process.side_effect = process_arr + + # run + self.multigpu_manager.setup_multi_gpu_train("fake") + self.multigpu_manager._check_child_processes_alive() + + # check + for p in process_arr[: self.num_gpu - 2]: + p.kill.assert_called_once() + self.mock_os.kill.assert_called_once_with(os.getpid(), self.mock_singal.SIGKILL) + + @e2e_pytest_unit + def test_terminate_signal_handler(self, mocker, process_arr): + # prepare + mock_exit = mocker.patch.object(multi_gpu.sys, "exit") + mocker.patch.object(MultiGPUManager, "initialize_multigpu_train") + self.mock_process.side_effect = process_arr + + # run + self.multigpu_manager.setup_multi_gpu_train("fake") + self.multigpu_manager._terminate_signal_handler(2, mocker.MagicMock()) + + # check + for p in process_arr[: self.num_gpu - 2]: + p.kill.assert_called_once() + mock_exit.assert_called_once() + + @e2e_pytest_unit + def test_terminate_signal_handler_not_main_thread(self, mocker, process_arr): + # prepare + def raise_error(*args, **kwargs): + raise RuntimeError + + mock_exit = mocker.patch.object(multi_gpu.sys, "exit") + mock_exit.side_effect = raise_error + mocker.patch.object(MultiGPUManager, "initialize_multigpu_train") + mocker.patch.object(multi_gpu.os, "getpid").return_value = os.getpid() + 1 + self.mock_process.side_effect = process_arr + + # run + self.multigpu_manager.setup_multi_gpu_train("fake") + with pytest.raises(RuntimeError): + self.multigpu_manager._terminate_signal_handler(2, mocker.MagicMock()) + + # check + for p in process_arr[: self.num_gpu - 2]: + p.kill.assert_not_called() + + @e2e_pytest_unit + def test_finalize(self, mocker, process_arr): + # prepare + mocker.patch.object(MultiGPUManager, "initialize_multigpu_train") + self.mock_process.side_effect = process_arr + + # run + self.multigpu_manager.setup_multi_gpu_train("fake") + self.multigpu_manager.finalize() + + # check + for p in process_arr: + p.join.assert_called_once() + + @e2e_pytest_unit + def test_finalize_still_running_child_process(self, mocker, process_arr): + # prepare + mocker.patch.object(MultiGPUManager, "initialize_multigpu_train") + self.mock_process.side_effect = process_arr + for p in process_arr: + p.exitcode = None + p.join.return_value = None + + # run + self.multigpu_manager.setup_multi_gpu_train("fake") + self.multigpu_manager.finalize() + + # check + for p in process_arr: + p.join.assert_called_once() + p.kill.assert_called_once() + + @e2e_pytest_unit + def test_finalize_before_spawn(self, mocker, process_arr): + # prepare + mocker.patch.object(MultiGPUManager, "initialize_multigpu_train") + self.mock_process.side_effect = process_arr + + # run + self.multigpu_manager.setup_multi_gpu_train("fake") + self.multigpu_manager.finalize() + + @e2e_pytest_unit + def test_initialize_multigpu_train(self, mocker): + # prepare + mocker.patch.object(multi_gpu.dist, "get_world_size", return_value=2) + mocker.patch.object(multi_gpu.dist, "get_rank", return_value=0) + + # run + MultiGPUManager.initialize_multigpu_train( + rdzv_endpoint="localhost:1234", + rank=0, + local_rank=0, + gpu_ids=[0, 1], + world_size=2, + ) + + # check + assert self.mock_os.environ["MASTER_ADDR"] == "localhost" + assert self.mock_os.environ["MASTER_PORT"] == "1234" + assert self.mock_os.environ["LOCAL_WORLD_SIZE"] == "2" + assert self.mock_os.environ["WORLD_SIZE"] == "2" + assert self.mock_os.environ["LOCAL_RANK"] == "0" + assert self.mock_os.environ["RANK"] == "0" + + @e2e_pytest_unit + def test_run_child_process(self, mocker): + # prepare + mock_set_start_method = mocker.patch.object(multi_gpu.mp, "set_start_method") + mock_sys = mocker.patch.object(multi_gpu, "sys") + mock_sys.argv = ["--gpus", "0,1"] + output_path = "mock_output_path" + rdzv_endpoint = "localhost:1234" + mock_initialize_multigpu_train = mocker.patch.object(MultiGPUManager, "initialize_multigpu_train") + mock_threading = mocker.patch.object(multi_gpu, "threading") + mock_train_func = mocker.MagicMock() + + # run + MultiGPUManager.run_child_process( + train_func=mock_train_func, + output_path=output_path, + rdzv_endpoint=rdzv_endpoint, + rank=0, + local_rank=0, + gpu_ids=[0, 1], + world_size=4, + ) + + # check + assert mock_set_start_method.call_args.kwargs["method"] is None + assert "--gpus" not in mock_sys.argv + for output_arg_key in ["-o", "--output", False]: + if output_arg_key in mock_sys.argv: + break + assert output_arg_key is not False, "There arn't both '-o' and '--output'." + assert mock_sys.argv[mock_sys.argv.index(output_arg_key) + 1] == output_path + assert "--rdzv-endpoint" in mock_sys.argv + assert mock_sys.argv[mock_sys.argv.index("--rdzv-endpoint") + 1] == rdzv_endpoint + mock_initialize_multigpu_train.assert_called_once() + mock_threading.Thread.assert_called_once_with(target=MultiGPUManager.check_parent_processes_alive, daemon=True) + mock_threading.Thread.call_args.return_value.start.assert_called_once + mock_train_func.assert_called_once() + + @e2e_pytest_unit + def test_check_parent_processes_alive(self, mocker): + # prepare + mocker.patch.object(multi_gpu, "time") + mock_cur_process = mocker.MagicMock() + mocker.patch.object(multi_gpu.psutil, "Process", return_value=mock_cur_process) + mock_cur_process.parent.return_value.is_running.return_value = False + + # run + MultiGPUManager.check_parent_processes_alive() + + # check + mock_cur_process.kill.assert_called_once() diff --git a/tests/unit/cli/utils/test_nncf.py b/tests/unit/cli/utils/test_nncf.py new file mode 100644 index 00000000000..7aa23ef8bc5 --- /dev/null +++ b/tests/unit/cli/utils/test_nncf.py @@ -0,0 +1,46 @@ +from os import path as osp +from tempfile import TemporaryDirectory + +import pytest + +from otx.cli.utils.nncf import get_number_of_fakequantizers_in_xml, is_checkpoint_nncf +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_is_checkpoint_nncf_meta_exist(mocker): + mock_state = {"meta": {"nncf_enable_compression": "fake"}} + mocker.patch("torch.load").return_value = mock_state + + assert is_checkpoint_nncf("fake") + + # state["meta"]["nncf_enable_compression"] + # state["nncf_enable_compression"] + + +@e2e_pytest_unit +def test_is_checkpoint_nncf_metainfo_exist(mocker): + mock_state = {"nncf_metainfo": "fake"} + mocker.patch("torch.load").return_value = mock_state + + assert is_checkpoint_nncf("fake") + + +@e2e_pytest_unit +def test_is_not_checkpoint_nncf_meta_not_exist(mocker): + mock_state = {"fake": "fake"} + mocker.patch("torch.load").return_value = mock_state + + assert not is_checkpoint_nncf("fake") + + +@e2e_pytest_unit +@pytest.mark.parametrize("number_of_fakequantizers_in_xml", [0, 10, 100]) +def test_get_number_of_fakequantizers_in_xml(number_of_fakequantizers_in_xml): + with TemporaryDirectory() as tmp_dir: + path_to_xml = osp.join(tmp_dir, "fake.xml") + with open(path_to_xml, "w") as f: + for _ in range(number_of_fakequantizers_in_xml): + f.write('type="FakeQuantize"\n') + + assert get_number_of_fakequantizers_in_xml(path_to_xml) == number_of_fakequantizers_in_xml diff --git a/tests/unit/cli/utils/test_parser.py b/tests/unit/cli/utils/test_parser.py new file mode 100644 index 00000000000..0ee8df9f581 --- /dev/null +++ b/tests/unit/cli/utils/test_parser.py @@ -0,0 +1,253 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from argparse import ArgumentParser, ArgumentTypeError +from pathlib import Path + +import pytest + +from otx.cli.utils import parser as target_package +from otx.cli.utils.parser import ( + MemSizeAction, + add_hyper_parameters_sub_parser, + gen_param_help, + gen_params_dict_from_args, + get_parser_and_hprams_data, + str2bool, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + +FAKE_HYPER_PARAMETERS = { + "a": { + "a": "fake", + "b": { + "default_value": "default_value", + "type": "SELECTABLE", + "affects_outcome_of": "TRAINING", + "header": "header", + }, + "visible_in_ui": True, + }, + "b": { + "a": { + "default_value": "default_value", + "type": "BOOLEAN", + "affects_outcome_of": "TRAINING", + "header": "header", + }, + "visible_in_ui": True, + }, + "c": { + "a": { + "a": { + "default_value": "default_value", + "type": "INTEGER", + "affects_outcome_of": "TRAINING", + "header": "header", + }, + "visible_in_ui": True, + }, + "visible_in_ui": True, + }, + "d": { + "a": {"default_value": "default_value", "type": "FLOAT", "affects_outcome_of": "TRAINING", "header": "header"}, + "visible_in_ui": True, + }, +} + + +@e2e_pytest_unit +def test_gen_param_help(): + param_help = gen_param_help(FAKE_HYPER_PARAMETERS) + + hp_type_map = { + "a.b": str, + "b.a": bool, + "c.a.a": int, + "d.a": float, + } + for key, val in hp_type_map.items(): + assert param_help[key]["default"] == "default_value" + assert param_help[key]["affects_outcome_of"] == "TRAINING" + assert "help" in param_help[key] + assert param_help[key]["type"] == val + + +@pytest.fixture +def mock_args(mocker): + mock_args = mocker.Mock() + setattr(mock_args, "params.a.a", 1) + setattr(mock_args, "params.a.b", 2.1) + setattr(mock_args, "params.a.c", True) + setattr(mock_args, "params.b", "fake") + setattr(mock_args, "params.c", 10) + setattr(mock_args, "params.d", None) + + return mock_args + + +@e2e_pytest_unit +def test_gen_params_dict_from_args(mock_args): + param_dict = gen_params_dict_from_args( + mock_args, ["params.a.a", "params.a.b", "params.a.c", "params.b", "params.c"] + ) + + assert param_dict["a"]["a"]["value"] == 1 + assert param_dict["a"]["b"]["value"] == 2.1 + assert param_dict["a"]["c"]["value"] is True + assert param_dict["b"]["value"] == "fake" + assert param_dict["c"]["value"] == 10 + assert "d" not in param_dict + + +@e2e_pytest_unit +def test_gen_params_dict_from_args_with_type_hint(mock_args): + type_hint = { + "a.a": {"type": str}, + "a.b": {"type": int}, + "a.c": {"type": bool}, + "b": {"type": str}, + "c": {"type": str}, + } + + param_dict = gen_params_dict_from_args( + mock_args, ["params.a.a", "params.a.b", "params.a.c", "params.b", "params.c"], type_hint + ) + + assert param_dict["a"]["a"]["value"] == "1" + assert param_dict["a"]["b"]["value"] == 2 + assert param_dict["a"]["c"]["value"] is True + assert param_dict["b"]["value"] == "fake" + assert param_dict["c"]["value"] == "10" + + +@e2e_pytest_unit +@pytest.mark.parametrize("val", [True, False]) +def test_str2bool_with_bool_input(val): + assert str2bool(val) is val + + +@e2e_pytest_unit +@pytest.mark.parametrize("val", ["true", "1"]) +def test_str2bool_with_bool_string_true(val): + assert str2bool(val) is True + + +@e2e_pytest_unit +@pytest.mark.parametrize("val", ["false", "0"]) +def test_str2bool_with_bool_string_false(val): + assert str2bool(val) is False + + +@e2e_pytest_unit +@pytest.mark.parametrize("val", [1, 1.2, "abc"]) +def test_str2bool_with_bool_wrong_input(val): + with pytest.raises(ArgumentTypeError): + assert str2bool(val) + + +@e2e_pytest_unit +def test_add_hyper_parameters_sub_parser(mocker): + # prepare + mock_parser = mocker.MagicMock() + mock_subparser = mocker.MagicMock() + mock_parser.add_subparsers.return_value.add_parser.return_value = mock_subparser + + # run + parser = add_hyper_parameters_sub_parser(mock_parser, FAKE_HYPER_PARAMETERS, return_sub_parser=True) + + # check + hp_type_map = { + "a.b": str, + "b.a": bool, + "c.a.a": int, + "d.a": float, + } + assert parser is not None + add_args_call_args = mock_subparser.add_argument.call_args_list + for i, key in enumerate(hp_type_map): + assert add_args_call_args[i][0][0] == f"--{key}" + assert add_args_call_args[i][1]["default"] == "default_value" + assert "help" in add_args_call_args[i][1] + assert add_args_call_args[i][1]["dest"] == f"params.{key}" + + hp_type = hp_type_map[key] + if hp_type is not bool: + assert add_args_call_args[i][1]["type"] == hp_type + else: + assert add_args_call_args[i][1]["type"] == str2bool + + +@e2e_pytest_unit +def test_get_parser_and_hprams_data_with_fake_template(mocker, tmp_dir): + # prepare + tmp_dir = Path(tmp_dir) + fake_template_file = tmp_dir / "template.yaml" + fake_template_file.write_text("fake") + mock_argv = ["otx train", str(fake_template_file), "params", "--left-args"] + mocker.patch("sys.argv", mock_argv) + mock_template = mocker.patch.object(target_package, "find_and_parse_model_template") + + # run + parser, hyper_parameters, params = get_parser_and_hprams_data() + + # check + mock_template.assert_called_once() + assert hyper_parameters == {} + assert params == ["params", "--left-args"] + assert isinstance(parser, ArgumentParser) + + +@e2e_pytest_unit +def test_get_parser_and_hprams_data(mocker): + # prepare + mock_argv = ["otx train", "params", "--left-args"] + mocker.patch("sys.argv", mock_argv) + + # run + parser, hyper_parameters, params = get_parser_and_hprams_data() + + # check + assert hyper_parameters == {} + assert params == ["params", "--left-args"] + assert isinstance(parser, ArgumentParser) + + +@pytest.fixture +def fxt_argparse(): + parser = ArgumentParser() + parser.add_argument( + "--mem-cache-size", + dest="params.algo_backend.mem_cache_size", + action=MemSizeAction, + type=str, + required=False, + default=0, + ) + return parser + + +@pytest.mark.parametrize( + "mem_size_arg,expected", + [ + ("1561", 1561), + ("121k", 121 * (2**10)), + ("121kb", 121 * (10**3)), + ("121kib", 121 * (2**10)), + ("121m", 121 * (2**20)), + ("121mb", 121 * (10**6)), + ("121mib", 121 * (2**20)), + ("121g", 121 * (2**30)), + ("121gb", 121 * (10**9)), + ("121gib", 121 * (2**30)), + ("121as", None), + ("121dddd", None), + ], +) +def test_mem_size_parsing(fxt_argparse, mem_size_arg, expected): + try: + args = fxt_argparse.parse_args(["--mem-cache-size", mem_size_arg]) + assert getattr(args, "params.algo_backend.mem_cache_size") == expected + except ValueError: + assert expected is None diff --git a/tests/unit/cli/utils/test_report.py b/tests/unit/cli/utils/test_report.py new file mode 100644 index 00000000000..90b9a40a722 --- /dev/null +++ b/tests/unit/cli/utils/test_report.py @@ -0,0 +1,86 @@ +from pathlib import Path +from pprint import pformat +from otx.algorithms.common.utils.utils import is_xpu_available + +from otx.api.entities.model_template import ModelTemplate +from otx.cli.utils.report import ( + data_config_to_str, + env_info_to_str, + get_otx_report, + sub_title_to_str, + task_config_to_str, + template_info_to_str, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockModelTemplate(ModelTemplate): + """Mock class for ModelTemplate.""" + + def __init__(self): + self.test1 = "abc" + self.test2 = "cba" + + +@e2e_pytest_unit +def test_sub_title_to_str(): + expected = "-" * 60 + "\n\n" + "test" + "\n\n" + "-" * 60 + "\n" + result = sub_title_to_str("test") + assert expected == result + + +@e2e_pytest_unit +def test_env_info_to_str(mocker): + expected = "\tOTX: 1.2\n" + mocker.patch("mmcv.utils.env.collect_env", return_value={"OTX": "1.2"}) + result = env_info_to_str() + if is_xpu_available(): + assert expected in result + else: + assert expected == result + + +@e2e_pytest_unit +def test_template_info_to_str(): + mock_template = MockModelTemplate() + expected = f"\ttest1: {pformat('abc')}\n" + f"\ttest2: {pformat('cba')}\n" + result = template_info_to_str(mock_template) + assert expected == result + + +@e2e_pytest_unit +def test_data_config_to_str(): + data_config = { + "train_subset": {"data-roots": "aaa"}, + "val_subset": {"data-roots": "aaa"}, + } + expected = "train_subset:\n\tdata-roots: aaa\nval_subset:\n\tdata-roots: aaa\n" + result = data_config_to_str(data_config) + assert expected == result + + +@e2e_pytest_unit +def test_task_config_to_str(): + task_config = {"a": "b", "b": "c"} + expected = "a: 'b'\nb: 'c'\n" + result = task_config_to_str(task_config) + assert expected == result + + +@e2e_pytest_unit +def test_get_otx_report(tmp_dir): + report_path = Path(tmp_dir) / "report.log" + model_template = MockModelTemplate() + task_config = {"a": "b", "b": "c"} + data_config = { + "train_subset": {"data-roots": "aaa"}, + "val_subset": {"data-roots": "aaa"}, + } + get_otx_report( + model_template=model_template, + task_config=task_config, + data_config=data_config, + results={"time": "0:01"}, + output_path=str(report_path), + ) + assert report_path.exists() diff --git a/tests/unit/cli/utils/test_telemetry.py b/tests/unit/cli/utils/test_telemetry.py new file mode 100644 index 00000000000..5318083e492 --- /dev/null +++ b/tests/unit/cli/utils/test_telemetry.py @@ -0,0 +1,133 @@ +from unittest.mock import MagicMock, patch + +import openvino_telemetry as ovtm +import pytest + +from otx import __version__ +from otx.cli.utils.telemetry import ( + close_telemetry_session, + init_telemetry_session, + send_cmd_results, + send_version, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestTelemetry: + # FIXME: is there a way to get private constants for the testing? + __TM_CATEGORY_OTX = "otx" + __TM_MEASUREMENT_ID = "UA-17808594-29" + __TM_ACTION_VERSION = "version" + __TM_ACTION_CMD_SUCCESS = "success" + __TM_ACTION_CMD_FAILURE = "failure" + __TM_ACTION_CMD_EXCEPTION = "exception" + __TM_ACTION_ERROR = "error" + + @e2e_pytest_unit + @patch("otx.cli.utils.telemetry.tm") + @patch("otx.cli.utils.telemetry.send_version") + def test_init_telemetry_session( + self, + mock_send_version, + mock_ovtm, + ): + mock_telemetry = MagicMock() + mock_telemetry.start_session = MagicMock() + mock_ovtm.Telemetry = MagicMock(return_value=mock_telemetry) + + init_telemetry_session() + + mock_ovtm.Telemetry.assert_called_once_with( + app_name=self.__TM_CATEGORY_OTX, app_version=str(__version__), tid=self.__TM_MEASUREMENT_ID + ) + mock_telemetry.start_session.assert_called_once_with(self.__TM_CATEGORY_OTX) + mock_send_version.assert_called_once_with(mock_telemetry) + + @e2e_pytest_unit + def test_close_telemetry_session(self): + mock_ovtm_instance = MagicMock(spec=ovtm.Telemetry) + + close_telemetry_session(mock_ovtm_instance) + + mock_ovtm_instance.end_session.assert_called_once_with(self.__TM_CATEGORY_OTX) + mock_ovtm_instance.force_shutdown.assert_called_once_with(1.0) + + with pytest.raises(RuntimeError): + close_telemetry_session(0) + + @e2e_pytest_unit + @patch("otx.cli.utils.telemetry.__send_event") + def test_send_version(self, mock_send_event): + mock_ovtm_instance = MagicMock(spec=ovtm.Telemetry) + send_version(mock_ovtm_instance) + mock_send_event.assert_called_once_with(mock_ovtm_instance, self.__TM_ACTION_VERSION, str(__version__)) + + with pytest.raises(RuntimeError): + send_version(None) + + @e2e_pytest_unit + @patch("otx.cli.utils.telemetry.__send_event") + @patch("otx.cli.utils.telemetry.__send_error") + def test_send_cmd_results( + self, + mock_send_error, + mock_send_event, + ): + with pytest.raises(RuntimeError): + send_cmd_results(None, "something", None) + + mock_ovtm_instance = MagicMock(spec=ovtm.Telemetry) + + # with invalid results arg + results = None + + with pytest.raises(RuntimeError): + send_cmd_results(mock_ovtm_instance, "something", results) + + mock_send_error.reset_mock() + mock_send_event.reset_mock() + + # with empty dict + results = {} + send_cmd_results(mock_ovtm_instance, "something", results) + mock_send_error.assert_called_once() + mock_send_event.assert_not_called() + + mock_send_error.reset_mock() + mock_send_event.reset_mock() + + # with failure retcode + results = {"retcode": 1, "some": "results"} + cmd = "cmd" + send_cmd_results(mock_ovtm_instance, cmd, results) + mock_send_error.assert_not_called() + mock_send_event.assert_called_once_with( + mock_ovtm_instance, self.__TM_ACTION_CMD_FAILURE, dict(cmd=cmd, **results) + ) + + mock_send_error.reset_mock() + mock_send_event.reset_mock() + + # with success retcode + results = {"retcode": 0, "some": "results"} + cmd = "cmd" + send_cmd_results(mock_ovtm_instance, cmd, results) + mock_send_error.assert_not_called() + mock_send_event.assert_called_once_with( + mock_ovtm_instance, self.__TM_ACTION_CMD_SUCCESS, dict(cmd=cmd, **results) + ) + + mock_send_error.reset_mock() + mock_send_event.reset_mock() + + # with exception retcode + results = {"retcode": -1, "some": "results"} + cmd = "cmd" + send_cmd_results(mock_ovtm_instance, cmd, results) + mock_send_error.assert_not_called() + mock_send_event.assert_called_once_with( + mock_ovtm_instance, self.__TM_ACTION_CMD_EXCEPTION, dict(cmd=cmd, **results) + ) + + mock_send_error.reset_mock() + mock_send_event.reset_mock() diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 00000000000..e524f6b03a0 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,15 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import psutil +import pytest + + +@pytest.fixture(autouse=True) +def hello(): + yield + + mem_info = psutil.virtual_memory() + total_g = mem_info.total / 1024**3 + available_g = mem_info.available / 1024**3 + print(f"===== memory usage: {available_g}/{total_g} =====") diff --git a/tests/unit/core/__init__.py b/tests/unit/core/__init__.py index 6a16273c024..9c68be83ef0 100644 --- a/tests/unit/core/__init__.py +++ b/tests/unit/core/__init__.py @@ -1,2 +1,3 @@ # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/core/config/test_resolver.py b/tests/unit/core/config/test_resolver.py deleted file mode 100644 index 51ecd411ee6..00000000000 --- a/tests/unit/core/config/test_resolver.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import torch -from omegaconf import OmegaConf - - -class TestResolver: - def test_as_int_tuple(self) -> None: - cfg_str = """ - mem_cache_img_max_size: ${as_int_tuple:1333,800} - """ - cfg = OmegaConf.create(cfg_str) - assert isinstance(cfg.mem_cache_img_max_size, tuple) - assert cfg.mem_cache_img_max_size == (1333, 800) - - def test_as_torch_dtype(self) -> None: - cfg_str = """ - uint8: ${as_torch_dtype:torch.uint8} - int64: ${as_torch_dtype:torch.int64} - float32: ${as_torch_dtype:torch.float32} - """ - cfg = OmegaConf.create(cfg_str) - - assert cfg.uint8 == torch.uint8 - assert cfg.int64 == torch.int64 - assert cfg.float32 == torch.float32 diff --git a/tests/unit/core/conftest.py b/tests/unit/core/conftest.py deleted file mode 100644 index 5ef50e9eff9..00000000000 --- a/tests/unit/core/conftest.py +++ /dev/null @@ -1,277 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations - -import numpy as np -import pytest -import torch -from datumaro import Label -from datumaro.components.annotation import AnnotationType, LabelCategories -from datumaro.components.dataset import Dataset, DatasetItem -from datumaro.components.media import Image -from otx.core.config import register_configs -from otx.core.data.dataset.base import LabelInfo -from otx.core.data.dataset.classification import HLabelInfo -from otx.core.data.entity.base import ImageInfo, Points -from otx.core.data.entity.visual_prompting import ( - VisualPromptingBatchDataEntity, - VisualPromptingBatchPredEntity, - VisualPromptingDataEntity, - ZeroShotVisualPromptingBatchDataEntity, - ZeroShotVisualPromptingBatchPredEntity, - ZeroShotVisualPromptingDataEntity, -) -from torchvision import tv_tensors - - -@pytest.fixture(scope="session", autouse=True) -def fxt_register_configs() -> None: - register_configs() - - -@pytest.fixture(scope="session", autouse=True) -def fxt_multiclass_labelinfo() -> LabelInfo: - label_names = ["class1", "class2", "class3"] - return LabelInfo( - label_names=label_names, - label_groups=[ - label_names, - ["class2", "class3"], - ], - ) - - -@pytest.fixture(scope="session", autouse=True) -def fxt_multilabel_labelinfo() -> LabelInfo: - label_names = ["class1", "class2", "class3"] - return LabelInfo( - label_names=label_names, - label_groups=[ - [label_names[0]], - [label_names[1]], - [label_names[2]], - ], - ) - - -@pytest.fixture() -def fxt_hlabel_dataset_subset() -> Dataset: - return Dataset.from_iterable( - [ - DatasetItem( - id=0, - subset="train", - media=Image.from_numpy(np.zeros((3, 10, 10))), - annotations=[ - Label( - label=2, - id=0, - group=1, - ), - ], - ), - DatasetItem( - id=1, - subset="train", - media=Image.from_numpy(np.zeros((3, 10, 10))), - annotations=[ - Label( - label=4, - id=0, - group=2, - ), - ], - ), - ], - categories={ - AnnotationType.label: LabelCategories( - items=[ - LabelCategories.Category(name="Heart", parent=""), - LabelCategories.Category(name="Spade", parent=""), - LabelCategories.Category(name="Heart_Queen", parent="Heart"), - LabelCategories.Category(name="Heart_King", parent="Heart"), - LabelCategories.Category(name="Spade_A", parent="Spade"), - LabelCategories.Category(name="Spade_King", parent="Spade"), - LabelCategories.Category(name="Black_Joker", parent=""), - LabelCategories.Category(name="Red_Joker", parent=""), - LabelCategories.Category(name="Extra_Joker", parent=""), - ], - label_groups=[ - LabelCategories.LabelGroup(name="Card", labels=["Heart", "Spade"]), - LabelCategories.LabelGroup(name="Heart Group", labels=["Heart_Queen", "Heart_King"]), - LabelCategories.LabelGroup(name="Spade Group", labels=["Spade_Queen", "Spade_King"]), - ], - ), - }, - ).get_subset("train") - - -@pytest.fixture() -def fxt_hlabel_multilabel_info() -> HLabelInfo: - return HLabelInfo( - label_names=[ - "Heart", - "Spade", - "Heart_Queen", - "Heart_King", - "Spade_A", - "Spade_King", - "Black_Joker", - "Red_Joker", - "Extra_Joker", - ], - label_groups=[ - ["Heart", "Spade"], - ["Heart_Queen", "Heart_King"], - ["Spade_A", "Spade_King"], - ["Black_Joker"], - ["Red_Joker"], - ["Extra_Joker"], - ], - num_multiclass_heads=3, - num_multilabel_classes=3, - head_idx_to_logits_range={"0": (0, 2), "1": (2, 4), "2": (4, 6)}, - num_single_label_classes=3, - empty_multiclass_head_indices=[], - class_to_group_idx={ - "Heart": (0, 0), - "Spade": (0, 1), - "Heart_Queen": (1, 0), - "Heart_King": (1, 1), - "Spade_A": (2, 0), - "Spade_King": (2, 1), - "Black_Joker": (3, 0), - "Red_Joker": (3, 1), - "Extra_Joker": (3, 2), - }, - all_groups=[ - ["Heart", "Spade"], - ["Heart_Queen", "Heart_King"], - ["Spade_A", "Spade_King"], - ["Black_Joker"], - ["Red_Joker"], - ["Extra_Joker"], - ], - label_to_idx={ - "Heart": 0, - "Spade": 1, - "Heart_Queen": 2, - "Heart_King": 3, - "Spade_A": 4, - "Spade_King": 5, - "Black_Joker": 6, - "Red_Joker": 7, - "Extra_Joker": 8, - }, - label_tree_edges=[ - ["Heart_Queen", "Heart"], - ["Heart_King", "Heart"], - ["Spade_A", "Spade"], - ["Spade_King", "Spade"], - ], - ) - - -@pytest.fixture(scope="session") -def fxt_vpm_data_entity() -> ( - tuple[VisualPromptingDataEntity, VisualPromptingBatchDataEntity, VisualPromptingBatchPredEntity] -): - img_size = (1024, 1024) - fake_image = tv_tensors.Image(torch.rand(img_size)) - fake_image_info = ImageInfo(img_idx=0, img_shape=img_size, ori_shape=img_size) - fake_bboxes = tv_tensors.BoundingBoxes( - [[0, 0, 1, 1]], - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_size, - dtype=torch.float32, - ) - fake_points = Points([[2, 2]], canvas_size=img_size, dtype=torch.float32) - fake_masks = tv_tensors.Mask(torch.rand(img_size)) - fake_labels = {"bboxes": torch.as_tensor([1], dtype=torch.int64)} - fake_polygons = [None] - # define data entity - single_data_entity = VisualPromptingDataEntity( - image=fake_image, - img_info=fake_image_info, - masks=fake_masks, - labels=fake_labels, - polygons=fake_polygons, - bboxes=fake_bboxes, - points=fake_points, - ) - batch_data_entity = VisualPromptingBatchDataEntity( - batch_size=1, - images=[fake_image], - imgs_info=[fake_image_info], - masks=[fake_masks], - labels=[fake_labels], - polygons=[fake_polygons], - bboxes=[fake_bboxes], - points=[fake_points], - ) - batch_pred_data_entity = VisualPromptingBatchPredEntity( - batch_size=1, - images=[fake_image], - imgs_info=[fake_image_info], - masks=[fake_masks], - labels=[fake_labels], - polygons=[fake_polygons], - bboxes=[fake_bboxes], - points=[fake_points], - scores=[], - ) - - return single_data_entity, batch_data_entity, batch_pred_data_entity - - -@pytest.fixture(scope="session") -def fxt_zero_shot_vpm_data_entity() -> ( - tuple[ - ZeroShotVisualPromptingDataEntity, - ZeroShotVisualPromptingBatchDataEntity, - ZeroShotVisualPromptingBatchPredEntity, - ] -): - img_size = (1024, 1024) - fake_image = tv_tensors.Image(torch.rand(img_size)) - fake_image_info = ImageInfo(img_idx=0, img_shape=img_size, ori_shape=img_size) - fake_bboxes = tv_tensors.BoundingBoxes( - [[0, 0, 1, 1]], - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=img_size, - dtype=torch.float32, - ) - fake_points = Points([[2, 2]], canvas_size=img_size, dtype=torch.float32) - fake_masks = tv_tensors.Mask(torch.rand(img_size)) - fake_labels = torch.as_tensor([1], dtype=torch.int64) - fake_polygons = [None] - # define data entity - single_data_entity = ZeroShotVisualPromptingDataEntity( - image=fake_image, - img_info=fake_image_info, - masks=fake_masks, - labels=fake_labels, - polygons=fake_polygons, - prompts=[fake_bboxes, fake_points], - ) - batch_data_entity = ZeroShotVisualPromptingBatchDataEntity( - batch_size=1, - images=[fake_image], - imgs_info=[fake_image_info], - masks=[fake_masks], - labels=[fake_labels], - polygons=[fake_polygons], - prompts=[[fake_bboxes, fake_points]], - ) - batch_pred_data_entity = ZeroShotVisualPromptingBatchPredEntity( - batch_size=1, - images=[fake_image], - imgs_info=[fake_image_info], - masks=[fake_masks], - labels=[fake_labels], - polygons=[fake_polygons], - prompts=[[fake_bboxes, fake_points]], - scores=[], - ) - - return single_data_entity, batch_data_entity, batch_pred_data_entity diff --git a/tests/unit/core/data/__init__.py b/tests/unit/core/data/__init__.py index 6a16273c024..9c68be83ef0 100644 --- a/tests/unit/core/data/__init__.py +++ b/tests/unit/core/data/__init__.py @@ -1,2 +1,3 @@ # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/core/data/adapter/test_action_adapter.py b/tests/unit/core/data/adapter/test_action_adapter.py new file mode 100644 index 00000000000..5ef71af9e53 --- /dev/null +++ b/tests/unit/core/data/adapter/test_action_adapter.py @@ -0,0 +1,88 @@ +"""Unit-Test case for otx.core.data.adapter.action_dataset_adapter.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.subset import Subset +from otx.core.data.adapter.action_dataset_adapter import ( + ActionClassificationDatasetAdapter, + ActionDetectionDatasetAdapter, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.data.test_helpers import ( + TASK_NAME_TO_DATA_ROOT, + TASK_NAME_TO_TASK_TYPE, +) + + +class TestOTXActionClassificationDatasetAdapter: + def setup_method(self): + self.root_path = os.getcwd() + task = "action_classification" + + self.task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + self.train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + self.val_data_roots: str = os.path.join(self.root_path, data_root_dict["val"]) + self.test_data_roots: str = os.path.join(self.root_path, data_root_dict["test"]) + + self.train_dataset_adapter = ActionClassificationDatasetAdapter( + task_type=self.task_type, + train_data_roots=self.train_data_roots, + val_data_roots=self.val_data_roots, + ) + + self.test_dataset_adapter = ActionClassificationDatasetAdapter( + task_type=self.task_type, + test_data_roots=self.test_data_roots, + ) + + @e2e_pytest_unit + def test_init(self): + assert Subset.TRAINING in self.train_dataset_adapter.dataset + assert Subset.VALIDATION in self.train_dataset_adapter.dataset + assert Subset.TESTING in self.test_dataset_adapter.dataset + + @e2e_pytest_unit + def test_get_otx_dataset(self): + assert isinstance(self.train_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(self.test_dataset_adapter.get_otx_dataset(), DatasetEntity) + + +class TestOTXActionDetectionDatasetAdapter: + def setup_method(self): + self.root_path = os.getcwd() + task = "action_detection" + + self.task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + self.train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + self.val_data_roots: str = os.path.join(self.root_path, data_root_dict["val"]) + self.test_data_roots: str = os.path.join(self.root_path, data_root_dict["test"]) + + self.train_dataset_adapter = ActionDetectionDatasetAdapter( + task_type=self.task_type, + train_data_roots=self.train_data_roots, + val_data_roots=self.val_data_roots, + ) + + self.test_dataset_adapter = ActionDetectionDatasetAdapter( + task_type=self.task_type, + test_data_roots=self.test_data_roots, + ) + + @e2e_pytest_unit + def test_init(self): + assert Subset.TRAINING in self.train_dataset_adapter.dataset + assert Subset.VALIDATION in self.train_dataset_adapter.dataset + assert Subset.TESTING in self.test_dataset_adapter.dataset + + @e2e_pytest_unit + def test_get_otx_dataset(self): + assert isinstance(self.train_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(self.test_dataset_adapter.get_otx_dataset(), DatasetEntity) diff --git a/tests/unit/core/data/adapter/test_anomaly_adapter.py b/tests/unit/core/data/adapter/test_anomaly_adapter.py new file mode 100644 index 00000000000..20f185fdd87 --- /dev/null +++ b/tests/unit/core/data/adapter/test_anomaly_adapter.py @@ -0,0 +1,124 @@ +"""Unit-Test case for otx.core.data.adapter.anomaly_dataset_adapter.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.subset import Subset +from otx.core.data.adapter.anomaly_dataset_adapter import ( + AnomalyClassificationDatasetAdapter, + AnomalyDetectionDatasetAdapter, + AnomalySegmentationDatasetAdapter, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.data.test_helpers import ( + TASK_NAME_TO_DATA_ROOT, + TASK_NAME_TO_TASK_TYPE, +) + + +class TestOTXAnomalyClassificationDatasetAdapter: + def setup_method(self): + self.root_path = os.getcwd() + task = "anomaly_classification" + + self.task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + self.train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + self.val_data_roots: str = os.path.join(self.root_path, data_root_dict["val"]) + self.test_data_roots: str = os.path.join(self.root_path, data_root_dict["test"]) + + self.train_dataset_adapter = AnomalyClassificationDatasetAdapter( + task_type=self.task_type, + train_data_roots=self.train_data_roots, + val_data_roots=self.val_data_roots, + ) + + self.test_dataset_adapter = AnomalyClassificationDatasetAdapter( + task_type=self.task_type, + test_data_roots=self.test_data_roots, + ) + + @e2e_pytest_unit + def test_init(self): + assert Subset.TRAINING in self.train_dataset_adapter.dataset + assert Subset.VALIDATION in self.train_dataset_adapter.dataset + assert Subset.TESTING in self.test_dataset_adapter.dataset + + @e2e_pytest_unit + def test_get_otx_dataset(self): + assert isinstance(self.train_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(self.test_dataset_adapter.get_otx_dataset(), DatasetEntity) + + +class TestOTXAnomalyDetectionDatasetAdapter: + def setup_method(self): + self.root_path = os.getcwd() + task = "anomaly_detection" + + self.task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + self.train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + self.val_data_roots: str = os.path.join(self.root_path, data_root_dict["val"]) + self.test_data_roots: str = os.path.join(self.root_path, data_root_dict["test"]) + + self.train_dataset_adapter = AnomalyDetectionDatasetAdapter( + task_type=self.task_type, + train_data_roots=self.train_data_roots, + val_data_roots=self.val_data_roots, + ) + + self.test_dataset_adapter = AnomalyDetectionDatasetAdapter( + task_type=self.task_type, + test_data_roots=self.test_data_roots, + ) + + @e2e_pytest_unit + def test_init(self): + assert Subset.TRAINING in self.train_dataset_adapter.dataset + assert Subset.VALIDATION in self.train_dataset_adapter.dataset + assert Subset.TESTING in self.test_dataset_adapter.dataset + + @e2e_pytest_unit + def test_get_otx_dataset(self): + assert isinstance(self.train_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(self.test_dataset_adapter.get_otx_dataset(), DatasetEntity) + + +class TestOTXAnomalySegmentationDatasetAdapter: + def setup_method(self): + self.root_path = os.getcwd() + task = "anomaly_segmentation" + + self.task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + self.train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + self.val_data_roots: str = os.path.join(self.root_path, data_root_dict["val"]) + self.test_data_roots: str = os.path.join(self.root_path, data_root_dict["test"]) + + self.train_dataset_adapter = AnomalySegmentationDatasetAdapter( + task_type=self.task_type, + train_data_roots=self.train_data_roots, + val_data_roots=self.val_data_roots, + ) + + self.test_dataset_adapter = AnomalySegmentationDatasetAdapter( + task_type=self.task_type, + test_data_roots=self.test_data_roots, + ) + + @e2e_pytest_unit + def test_init(self): + assert Subset.TRAINING in self.train_dataset_adapter.dataset + assert Subset.VALIDATION in self.train_dataset_adapter.dataset + assert Subset.TESTING in self.test_dataset_adapter.dataset + + @e2e_pytest_unit + def test_get_otx_dataset(self): + assert isinstance(self.train_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(self.test_dataset_adapter.get_otx_dataset(), DatasetEntity) diff --git a/tests/unit/core/data/adapter/test_classification_adapter.py b/tests/unit/core/data/adapter/test_classification_adapter.py new file mode 100644 index 00000000000..95ec47f5e2a --- /dev/null +++ b/tests/unit/core/data/adapter/test_classification_adapter.py @@ -0,0 +1,173 @@ +"""Unit-Test case for otx.core.data.adapter.classification_dataset_adapter.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os +from typing import Optional + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.subset import Subset +from otx.core.data.adapter.classification_dataset_adapter import ( + ClassificationDatasetAdapter, + SelfSLClassificationDatasetAdapter, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.data.test_helpers import ( + TASK_NAME_TO_DATA_ROOT, + TASK_NAME_TO_TASK_TYPE, +) + + +class TestOTXClassificationDatasetAdapter: + def setup_method(self): + self.root_path = os.getcwd() + task = "classification" + + self.task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + self.train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + self.val_data_roots: str = os.path.join(self.root_path, data_root_dict["val"]) + self.test_data_roots: str = os.path.join(self.root_path, data_root_dict["test"]) + self.unlabeled_data_roots: Optional[str] = None + if "unlabeled" in data_root_dict: + self.unlabeled_data_roots = os.path.join(self.root_path, data_root_dict["unlabeled"]) + + self.train_dataset_adapter = ClassificationDatasetAdapter( + task_type=self.task_type, + train_data_roots=self.train_data_roots, + val_data_roots=self.val_data_roots, + unlabeled_data_roots=self.unlabeled_data_roots, + ) + + self.test_dataset_adapter = ClassificationDatasetAdapter( + task_type=self.task_type, + test_data_roots=self.test_data_roots, + ) + + @e2e_pytest_unit + def test_init(self): + assert Subset.TRAINING in self.train_dataset_adapter.dataset + assert Subset.VALIDATION in self.train_dataset_adapter.dataset + if self.unlabeled_data_roots is not None: + assert Subset.UNLABELED in self.train_dataset_adapter.dataset + + assert Subset.TESTING in self.test_dataset_adapter.dataset + + @e2e_pytest_unit + def test_get_otx_dataset(self): + assert isinstance(self.train_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(self.test_dataset_adapter.get_otx_dataset(), DatasetEntity) + + @e2e_pytest_unit + def test_get_label_schema(self): + _ = self.train_dataset_adapter.get_otx_dataset() + assert isinstance(self.train_dataset_adapter.get_label_schema(), LabelSchemaEntity) + + _ = self.test_dataset_adapter.get_otx_dataset() + assert isinstance(self.test_dataset_adapter.get_label_schema(), LabelSchemaEntity) + + @e2e_pytest_unit + def test_multilabel(self): + train_data_roots = os.path.join(self.root_path, "tests/assets/datumaro_multilabel") + val_data_roots = os.path.join(self.root_path, "tests/assets/datumaro_multilabel") + test_data_roots = os.path.join(self.root_path, "tests/assets/datumaro_multilabel") + + multilabel_train_dataset_adapter = ClassificationDatasetAdapter( + task_type=self.task_type, + train_data_roots=train_data_roots, + val_data_roots=val_data_roots, + ) + + assert Subset.TRAINING in multilabel_train_dataset_adapter.dataset + assert Subset.VALIDATION in multilabel_train_dataset_adapter.dataset + + assert isinstance(multilabel_train_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(multilabel_train_dataset_adapter.get_label_schema(), LabelSchemaEntity) + + multilabel_test_dataset_adapter = ClassificationDatasetAdapter( + task_type=self.task_type, test_data_roots=test_data_roots + ) + + assert Subset.TESTING in multilabel_test_dataset_adapter.dataset + assert isinstance(multilabel_test_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(multilabel_test_dataset_adapter.get_label_schema(), LabelSchemaEntity) + + @e2e_pytest_unit + def test_hierarchical_label(self): + train_data_roots = os.path.join(self.root_path, "tests/assets/datumaro_h-label") + val_data_roots = os.path.join(self.root_path, "tests/assets/datumaro_h-label") + test_data_roots = os.path.join(self.root_path, "tests/assets/datumaro_h-label") + + hlabel_train_dataset_adapter = ClassificationDatasetAdapter( + task_type=self.task_type, + train_data_roots=train_data_roots, + val_data_roots=val_data_roots, + ) + + assert Subset.TRAINING in hlabel_train_dataset_adapter.dataset + assert Subset.VALIDATION in hlabel_train_dataset_adapter.dataset + + assert isinstance(hlabel_train_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(hlabel_train_dataset_adapter.get_label_schema(), LabelSchemaEntity) + + label_tree = hlabel_train_dataset_adapter.get_label_schema().label_tree + assert label_tree.num_labels == len(hlabel_train_dataset_adapter.category_items) + for label in label_tree.get_labels_in_topological_order(): + parent = label_tree.get_parent(label) + parent_name = "" if parent is None else parent.name + assert [i.parent for i in hlabel_train_dataset_adapter.category_items if i.name == label.name][ + 0 + ] == parent_name + + hlabel_test_dataset_adapter = ClassificationDatasetAdapter( + task_type=self.task_type, test_data_roots=test_data_roots + ) + + assert Subset.TESTING in hlabel_test_dataset_adapter.dataset + assert isinstance(hlabel_test_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(hlabel_test_dataset_adapter.get_label_schema(), LabelSchemaEntity) + + label_tree = hlabel_test_dataset_adapter.get_label_schema().label_tree + assert label_tree.num_labels == len(hlabel_test_dataset_adapter.category_items) + for label in label_tree.get_labels_in_topological_order(): + parent = label_tree.get_parent(label) + parent_name = "" if parent is None else parent.name + assert [i.parent for i in hlabel_test_dataset_adapter.category_items if i.name == label.name][ + 0 + ] == parent_name + + +class TestSelfSLClassificationDatasetAdapter: + def setup_method(self): + self.root_path = os.getcwd() + task = "classification" + + self.task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + self.data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + self.train_data_roots: str = os.path.join(self.root_path, self.data_root_dict["train"]) + self.train_data_roots_images: str = os.path.join(self.root_path, self.data_root_dict["train"], "0") + + self.train_dataset_adapter_imagenet = SelfSLClassificationDatasetAdapter( + task_type=self.task_type, + train_data_roots=self.train_data_roots, + ) + + self.train_dataset_adapter_images_only = SelfSLClassificationDatasetAdapter( + task_type=self.task_type, + train_data_roots=self.train_data_roots_images, + ) + + @e2e_pytest_unit + def test_get_otx_dataset(self): + dataset_imagenet = self.train_dataset_adapter_imagenet.get_otx_dataset() + assert isinstance(dataset_imagenet, DatasetEntity) + assert len(self.train_dataset_adapter_imagenet.get_label_schema().get_labels(False)) == 2 + dataset_only_images = self.train_dataset_adapter_images_only.get_otx_dataset() + assert isinstance(dataset_only_images, DatasetEntity) + lables = self.train_dataset_adapter_images_only.get_label_schema().get_labels(False) + assert len(lables) == 1 + assert lables[0].name == "fake_label" diff --git a/tests/unit/core/data/adapter/test_detection_adapter.py b/tests/unit/core/data/adapter/test_detection_adapter.py new file mode 100644 index 00000000000..7222d3c7c82 --- /dev/null +++ b/tests/unit/core/data/adapter/test_detection_adapter.py @@ -0,0 +1,173 @@ +"""Unit-Test case for otx.core.data.adapter.detection_dataset_adapter.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os + +from otx.api.entities.annotation import NullAnnotationSceneEntity +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.label_schema import LabelSchemaEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.subset import Subset +from otx.core.data.adapter.detection_dataset_adapter import DetectionDatasetAdapter +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.data.test_helpers import ( + TASK_NAME_TO_DATA_ROOT, + TASK_NAME_TO_TASK_TYPE, +) + + +class TestOTXDetectionDatasetAdapter: + def setup_method(self): + self.root_path = os.getcwd() + + @e2e_pytest_unit + def test_detection(self): + task = "detection" + + task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + val_data_roots: str = os.path.join(self.root_path, data_root_dict["val"]) + test_data_roots: str = os.path.join(self.root_path, data_root_dict["test"]) + + det_train_dataset_adapter = DetectionDatasetAdapter( + task_type=task_type, + train_data_roots=train_data_roots, + val_data_roots=val_data_roots, + ) + + assert Subset.TRAINING in det_train_dataset_adapter.dataset + assert Subset.VALIDATION in det_train_dataset_adapter.dataset + + det_train_dataset = det_train_dataset_adapter.get_otx_dataset() + det_train_label_schema = det_train_dataset_adapter.get_label_schema() + assert isinstance(det_train_dataset, DatasetEntity) + assert isinstance(det_train_label_schema, LabelSchemaEntity) + + # In the test data, there is a empty_label image. + # So, has_empty_label should be True + has_empty_label = False + for train_data in det_train_dataset: + if isinstance(train_data.annotation_scene, NullAnnotationSceneEntity): + has_empty_label = True + assert has_empty_label is True + + det_test_dataset_adapter = DetectionDatasetAdapter( + task_type=task_type, + test_data_roots=test_data_roots, + ) + + assert Subset.TESTING in det_test_dataset_adapter.dataset + assert isinstance(det_test_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(det_test_dataset_adapter.get_label_schema(), LabelSchemaEntity) + + @e2e_pytest_unit + def test_get_subset_data(self): + class MockDatumDataset: + def __init__(self, subsets): + self._subsets = subsets + self.eager = None + + def subsets(self): + return self._subsets + + class MockDataset: + def __init__(self, string): + self.string = string + + def as_dataset(self): + return self.string + + task = "detection" + + task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + val_data_roots: str = os.path.join(self.root_path, data_root_dict["val"]) + + det_train_dataset_adapter = DetectionDatasetAdapter( + task_type=task_type, + train_data_roots=train_data_roots, + val_data_roots=val_data_roots, + ) + + dataset = MockDatumDataset( + { + "train": MockDataset("train"), + "val": MockDataset("val"), + "test": MockDataset("test"), + } + ) + assert det_train_dataset_adapter._get_subset_data("val", dataset) == "val" + + dataset = MockDatumDataset( + { + "train": MockDataset("train"), + "validation": MockDataset("validation"), + "test": MockDataset("test"), + } + ) + assert det_train_dataset_adapter._get_subset_data("val", dataset) == "validation" + + dataset = MockDatumDataset( + { + "train": MockDataset("train"), + "trainval": MockDataset("trainval"), + "val": MockDataset("val"), + } + ) + assert det_train_dataset_adapter._get_subset_data("val", dataset) == "val" + + dataset = MockDatumDataset( + { + "train2017": MockDataset("train2017"), + "val2017": MockDataset("val2017"), + "test2017": MockDataset("test2017"), + } + ) + assert det_train_dataset_adapter._get_subset_data("val", dataset) == "val2017" + + @e2e_pytest_unit + def test_instance_segmentation(self): + task = "instance_segmentation" + + task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + val_data_roots: str = os.path.join(self.root_path, data_root_dict["val"]) + test_data_roots: str = os.path.join(self.root_path, data_root_dict["test"]) + + instance_seg_train_dataset_adapter = DetectionDatasetAdapter( + task_type=task_type, + train_data_roots=train_data_roots, + val_data_roots=val_data_roots, + ) + + assert Subset.TRAINING in instance_seg_train_dataset_adapter.dataset + assert Subset.VALIDATION in instance_seg_train_dataset_adapter.dataset + + instance_seg_otx_train_data = instance_seg_train_dataset_adapter.get_otx_dataset() + instance_seg_otx_train_label_schema = instance_seg_train_dataset_adapter.get_label_schema() + assert isinstance(instance_seg_otx_train_data, DatasetEntity) + assert isinstance(instance_seg_otx_train_label_schema, LabelSchemaEntity) + + # In the test data, there is a empty_label image. + # So, has_empty_label should be True + has_empty_label = False + for train_data in instance_seg_otx_train_data: + if isinstance(train_data.annotation_scene, NullAnnotationSceneEntity): + has_empty_label = True + assert has_empty_label is True + + instance_seg_test_dataset_adapter = DetectionDatasetAdapter( + task_type=task_type, + test_data_roots=test_data_roots, + ) + + assert Subset.TESTING in instance_seg_test_dataset_adapter.dataset + assert isinstance(instance_seg_test_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(instance_seg_test_dataset_adapter.get_label_schema(), LabelSchemaEntity) diff --git a/tests/unit/core/data/adapter/test_init.py b/tests/unit/core/data/adapter/test_init.py new file mode 100644 index 00000000000..9ea204cc6c7 --- /dev/null +++ b/tests/unit/core/data/adapter/test_init.py @@ -0,0 +1,153 @@ +import os + +import pytest + +from otx.algorithms.common.configs.training_base import TrainType +from otx.api.entities.subset import Subset +from otx.core.data.adapter import get_dataset_adapter +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.data.test_helpers import ( + TASK_NAME_TO_DATA_ROOT, + TASK_NAME_TO_TASK_TYPE, +) + +from pathlib import Path +import shutil + + +@e2e_pytest_unit +@pytest.mark.parametrize("task_name", TASK_NAME_TO_TASK_TYPE.keys()) +@pytest.mark.parametrize("train_type", [TrainType.Incremental.value]) +def test_get_dataset_adapter_incremental(task_name, train_type): + root_path = os.getcwd() + task_type = TASK_NAME_TO_TASK_TYPE[task_name] + data_root = TASK_NAME_TO_DATA_ROOT[task_name] + if str(task_type).upper() == "VISUAL_PROMPTING": + data_root = data_root.get("coco") + + get_dataset_adapter( + task_type=task_type, + train_type=train_type, + train_data_roots=os.path.join(root_path, data_root["train"]), + val_data_roots=os.path.join(root_path, data_root["val"]), + ) + + get_dataset_adapter( + task_type=task_type, + train_type=train_type, + test_data_roots=os.path.join(root_path, data_root["test"]), + ) + + +@e2e_pytest_unit +@pytest.mark.parametrize("task_name", ["classification"]) +@pytest.mark.parametrize("train_type", [TrainType.Selfsupervised.value]) +def test_get_dataset_adapter_selfsl_classification(task_name, train_type): + root_path = os.getcwd() + task_type = TASK_NAME_TO_TASK_TYPE[task_name] + data_root = TASK_NAME_TO_DATA_ROOT[task_name] + + get_dataset_adapter( + task_type=task_type, + train_type=train_type, + train_data_roots=os.path.join(root_path, data_root["train"]), + ) + + get_dataset_adapter( + task_type=task_type, + train_type=train_type, + test_data_roots=os.path.join(root_path, data_root["test"]), + ) + + +@e2e_pytest_unit +@pytest.mark.parametrize("task_name", ["segmentation"]) +@pytest.mark.parametrize("train_type", [TrainType.Selfsupervised.value]) +def test_get_dataset_adapter_selfsl_segmentation(task_name, train_type): + root_path = os.getcwd() + task_type = TASK_NAME_TO_TASK_TYPE[task_name] + data_root = TASK_NAME_TO_DATA_ROOT[task_name] + + with pytest.raises(ValueError, match=r"pseudo_mask_dir must be set."): + get_dataset_adapter( + task_type=task_type, + train_type=train_type, + train_data_roots=os.path.join(root_path, data_root["train"]), + ) + + get_dataset_adapter( + task_type=task_type, + train_type=train_type, + test_data_roots=os.path.join(root_path, data_root["test"]), + ) + + tmp_supcon_mask_dir = Path("/tmp/selfsl_supcon_unit_test") + get_dataset_adapter( + task_type=task_type, + train_type=train_type, + train_data_roots=os.path.join(root_path, data_root["train"]), + pseudo_mask_dir=tmp_supcon_mask_dir, + ) + shutil.rmtree(str(tmp_supcon_mask_dir)) + + +# TODO: direct annotation function is only supported in COCO format for now. +@e2e_pytest_unit +@pytest.mark.parametrize("task_name", ["detection"]) +@pytest.mark.parametrize("train_type", [TrainType.Incremental.value]) +def test_direct_annotation(task_name, train_type): + root_path = os.getcwd() + task_type = TASK_NAME_TO_TASK_TYPE[task_name] + data_root = TASK_NAME_TO_DATA_ROOT[task_name] + + t_adapter = get_dataset_adapter( + task_type=task_type, + train_type=train_type, + train_data_roots=os.path.join(root_path, data_root["train"]), + train_ann_files="tests/assets/car_tree_bug/annotations/instances_train_5_imgs.json", + val_data_roots=os.path.join(root_path, data_root["val"]), + ) + assert t_adapter.dataset[Subset.TRAINING].get_subset("train_5_imgs").get_annotated_items() == 5 + + v_adapter = get_dataset_adapter( + task_type=task_type, + train_type=train_type, + train_data_roots=os.path.join(root_path, data_root["train"]), + val_data_roots=os.path.join(root_path, data_root["val"]), + val_ann_files="tests/assets/car_tree_bug/annotations/instances_val_1_imgs.json", + ) + assert v_adapter.dataset[Subset.VALIDATION].get_subset("val_1_imgs").get_annotated_items() == 1 + + tv_adapter = get_dataset_adapter( + task_type=task_type, + train_type=train_type, + train_data_roots=os.path.join(root_path, data_root["train"]), + train_ann_files="tests/assets/car_tree_bug/annotations/instances_train_5_imgs.json", + val_data_roots=os.path.join(root_path, data_root["val"]), + val_ann_files="tests/assets/car_tree_bug/annotations/instances_val_1_imgs.json", + ) + assert tv_adapter.dataset[Subset.TRAINING].get_subset("train_5_imgs").get_annotated_items() == 5 + assert tv_adapter.dataset[Subset.VALIDATION].get_subset("val_1_imgs").get_annotated_items() == 1 + + +@e2e_pytest_unit +@pytest.mark.parametrize("task_name", ["classification", "detection", "segmentation"]) +@pytest.mark.parametrize("train_type", [TrainType.Incremental.value]) +def test_unlabeled_file_list(task_name, train_type): + root_path = os.getcwd() + task_type = TASK_NAME_TO_TASK_TYPE[task_name] + data_root = TASK_NAME_TO_DATA_ROOT[task_name] + + unlabeled_data_roots = "tests/assets/unlabeled_dataset" + unlabeled_file_list = "tests/assets/unlabeled_dataset/unlabeled_file_list.txt" + + adapter = get_dataset_adapter( + task_type=task_type, + train_type=train_type, + train_data_roots=os.path.join(root_path, data_root["train"]), + val_data_roots=os.path.join(root_path, data_root["val"]), + unlabeled_data_roots=unlabeled_data_roots, + unlabeled_file_list=unlabeled_file_list, + ) + + assert len(adapter.dataset[Subset.UNLABELED]) == 8 diff --git a/tests/unit/core/data/adapter/test_segmentation_adapter.py b/tests/unit/core/data/adapter/test_segmentation_adapter.py new file mode 100644 index 00000000000..4436ba77c14 --- /dev/null +++ b/tests/unit/core/data/adapter/test_segmentation_adapter.py @@ -0,0 +1,157 @@ +"""Unit-Test case for otx.core.data.adapter.segmentation_dataset_adapter.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import os +import shutil +from pathlib import Path +from typing import Optional + +import numpy as np +import pytest + +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.model_template import TaskType +from otx.api.entities.subset import Subset +from otx.core.data.adapter.segmentation_dataset_adapter import ( + SegmentationDatasetAdapter, + SelfSLSegmentationDatasetAdapter, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.data.test_helpers import ( + TASK_NAME_TO_DATA_ROOT, + TASK_NAME_TO_TASK_TYPE, +) + + +class TestOTXSegmentationDatasetAdapter: + def setup_method(self): + self.root_path = os.getcwd() + task = "segmentation" + + self.task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + + self.train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + self.val_data_roots: str = os.path.join(self.root_path, data_root_dict["val"]) + self.test_data_roots: str = os.path.join(self.root_path, data_root_dict["test"]) + self.unlabeled_data_roots: Optional[str] = None + if "unlabeled" in data_root_dict: + self.unlabeled_data_roots = os.path.join(self.root_path, data_root_dict["unlabeled"]) + + self.train_dataset_adapter = SegmentationDatasetAdapter( + task_type=self.task_type, + train_data_roots=self.train_data_roots, + val_data_roots=self.val_data_roots, + unlabeled_data_roots=self.unlabeled_data_roots, + ) + + self.test_dataset_adapter = SegmentationDatasetAdapter( + task_type=self.task_type, + test_data_roots=self.test_data_roots, + ) + + @e2e_pytest_unit + def test_init(self): + assert Subset.TRAINING in self.train_dataset_adapter.dataset + assert Subset.VALIDATION in self.train_dataset_adapter.dataset + if self.unlabeled_data_roots is not None: + assert Subset.UNLABELED in self.train_dataset_adapter.dataset + + assert Subset.TESTING in self.test_dataset_adapter.dataset + + @e2e_pytest_unit + def test_get_otx_dataset(self): + assert isinstance(self.train_dataset_adapter.get_otx_dataset(), DatasetEntity) + assert isinstance(self.test_dataset_adapter.get_otx_dataset(), DatasetEntity) + + +class TestSelfSLSegmentationDatasetAdapter: + def setup_class(self) -> None: + self.root_path = os.getcwd() + task = "segmentation" + + self.task_type: TaskType = TASK_NAME_TO_TASK_TYPE[task] + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[task] + self.train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"], "images") + + self.pseudo_mask_dir = Path(os.path.abspath(self.train_data_roots.replace("images", "detcon_mask"))) + + def teardown_class(self) -> None: + shutil.rmtree(self.pseudo_mask_dir, ignore_errors=True) + + @e2e_pytest_unit + def test_import_dataset_create_all_masks(self, mocker): + """Test _import_datasets when creating all masks. + + This test is for when all masks are not created and it is required to create masks. + """ + shutil.rmtree(self.pseudo_mask_dir, ignore_errors=True) + spy_create_pseudo_masks = mocker.spy(SelfSLSegmentationDatasetAdapter, "create_pseudo_masks") + + dataset_adapter = SelfSLSegmentationDatasetAdapter( + task_type=self.task_type, train_data_roots=self.train_data_roots, pseudo_mask_dir=self.pseudo_mask_dir + ) + + spy_create_pseudo_masks.assert_called() + assert spy_create_pseudo_masks.call_count == len(dataset_adapter.dataset[Subset.TRAINING]) + + @e2e_pytest_unit + @pytest.mark.parametrize("idx_remove", [1, 2, 3]) + def test_import_dataset_create_some_uncreated_masks(self, mocker, idx_remove: int): + """Test _import_datasets when there are both uncreated and created masks. + + This test is for when there are both created and uncreated masks + and it is required to either create or just load masks. + In this test, remove a mask created before and check if `create_pseudo_masks` is called once. + """ + shutil.rmtree(self.pseudo_mask_dir, ignore_errors=True) + dataset_adapter = SelfSLSegmentationDatasetAdapter( + task_type=self.task_type, train_data_roots=self.train_data_roots, pseudo_mask_dir=self.pseudo_mask_dir + ) + assert os.path.isdir(self.pseudo_mask_dir) + assert len(os.listdir(self.pseudo_mask_dir)) == 4 + + # remove a mask + os.remove(os.path.join(self.pseudo_mask_dir, f"000{idx_remove}.png")) + spy_create_pseudo_masks = mocker.spy(SelfSLSegmentationDatasetAdapter, "create_pseudo_masks") + + _ = dataset_adapter._import_datasets( + train_data_roots=self.train_data_roots, pseudo_mask_dir=self.pseudo_mask_dir + ) + + spy_create_pseudo_masks.assert_called() + assert spy_create_pseudo_masks.call_count == 1 + + @e2e_pytest_unit + def test_import_dataset_just_load_masks(self, mocker): + """Test _import_datasets when just loading all masks.""" + spy_create_pseudo_masks = mocker.spy(SelfSLSegmentationDatasetAdapter, "create_pseudo_masks") + + _ = SelfSLSegmentationDatasetAdapter( + task_type=self.task_type, train_data_roots=self.train_data_roots, pseudo_mask_dir=self.pseudo_mask_dir + ) + + spy_create_pseudo_masks.assert_not_called() + + @e2e_pytest_unit + @pytest.mark.xfail + def test_get_otx_dataset_without_skipping_background(self): + """Test get_otx_dataset without skipping background. + + TODO (sungchul): don't skip background class in get_otx_dataset + """ + assert 0 + + @e2e_pytest_unit + def test_create_pseudo_masks(self, mocker): + """Test create_pseudo_masks.""" + mocker.patch("otx.core.data.adapter.segmentation_dataset_adapter.os.makedirs") + mocker.patch("otx.core.data.adapter.segmentation_dataset_adapter.cv2.imwrite") + dataset_adapter = SelfSLSegmentationDatasetAdapter( + task_type=self.task_type, train_data_roots=self.train_data_roots, pseudo_mask_dir=self.pseudo_mask_dir + ) + + pseudo_mask = dataset_adapter.create_pseudo_masks(img=np.ones((2, 2)), pseudo_mask_path="") + + assert type(pseudo_mask) == np.ndarray diff --git a/tests/unit/core/data/adapter/test_visual_prompting_adapter.py b/tests/unit/core/data/adapter/test_visual_prompting_adapter.py new file mode 100644 index 00000000000..767b7e57e44 --- /dev/null +++ b/tests/unit/core/data/adapter/test_visual_prompting_adapter.py @@ -0,0 +1,62 @@ +"""Unit-Test case for otx.core.data.adapter.visual_prompting_dataset_adapter.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +from typing import Union + +import numpy as np +import pytest + +from otx.api.entities.image import Image +from otx.api.entities.model_template import TaskType +from otx.api.entities.shapes.polygon import Polygon +from otx.core.data.adapter.base_dataset_adapter import BaseDatasetAdapter +from otx.core.data.adapter.visual_prompting_dataset_adapter import ( + VisualPromptingDatasetAdapter, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.data.test_helpers import ( + TASK_NAME_TO_DATA_ROOT, + TASK_NAME_TO_TASK_TYPE, +) + + +class TestVisualPromptingDatasetAdapter: + def setup_method(self): + self.root_path: str = os.getcwd() + self.task: str = "visual_prompting" + self.task_type: TaskType = TASK_NAME_TO_TASK_TYPE[self.task] + + @e2e_pytest_unit + @pytest.mark.parametrize( + "data_format, use_mask, expected_shape", + [ + ("coco", True, Image), + ("coco", False, Polygon), + ("voc", True, Image), + ("voc", False, Polygon), + ("common_semantic_segmentation", True, Image), + ("common_semantic_segmentation", False, Polygon), + ], + ) + def test_get_otx_dataset(self, data_format: str, use_mask: bool, expected_shape: Union[Image, Polygon]) -> None: + """Test get_otx_dataset.""" + data_root_dict: dict = TASK_NAME_TO_DATA_ROOT[self.task][data_format] + train_data_roots: str = os.path.join(self.root_path, data_root_dict["train"]) + dataset_adapter: VisualPromptingDatasetAdapter = VisualPromptingDatasetAdapter( + task_type=self.task_type, + train_data_roots=train_data_roots, + use_mask=use_mask, + ) + + results = dataset_adapter.get_otx_dataset() + + assert len(results) > 0 + for result in results: + assert isinstance(result.media, Image) + assert isinstance(result.media.numpy, np.ndarray) + for annotation in result.annotation_scene.annotations: + assert isinstance(annotation.shape, expected_shape) diff --git a/tests/unit/core/data/conftest.py b/tests/unit/core/data/conftest.py deleted file mode 100644 index f9105cefb6d..00000000000 --- a/tests/unit/core/data/conftest.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations - -from typing import TYPE_CHECKING -from unittest.mock import MagicMock - -import cv2 -import numpy as np -import pytest -from datumaro.components.annotation import Bbox, Label, Mask -from datumaro.components.dataset import DatasetSubset -from datumaro.components.dataset_base import DatasetItem -from datumaro.components.media import Image -from otx.core.data.dataset.classification import ( - MulticlassClsDataEntity, - OTXMulticlassClsDataset, -) -from otx.core.data.dataset.detection import ( - DetDataEntity, - OTXDetectionDataset, -) -from otx.core.data.dataset.segmentation import ( - OTXSegmentationDataset, - SegDataEntity, -) -from otx.core.data.mem_cache import MemCacheHandlerSingleton - -if TYPE_CHECKING: - from otx.core.data.dataset.base import OTXDataset, T_OTXDataEntity - from otx.core.data.mem_cache import MemCacheHandlerBase - from pytest_mock import MockerFixture - - -@pytest.fixture() -def fxt_mem_cache_handler(monkeypatch) -> MemCacheHandlerBase: - monkeypatch.setattr(MemCacheHandlerSingleton, "check_system_memory", lambda *_: True) - handler = MemCacheHandlerSingleton.create(mode="singleprocessing", mem_size=1024 * 1024) - yield handler - MemCacheHandlerSingleton.delete() - - -@pytest.fixture(params=["bytes", "numpy"]) -def fxt_dm_item(request) -> DatasetItem: - np_img = np.zeros(shape=(10, 10, 3), dtype=np.uint8) - np_img[:, :, 0] = 0 # Set 0 for B channel - np_img[:, :, 1] = 1 # Set 1 for G channel - np_img[:, :, 2] = 2 # Set 2 for R channel - - if request.param == "bytes": - _, np_bytes = cv2.imencode(".png", np_img) - media = Image.from_bytes(np_bytes.tobytes()) - elif request.param == "numpy": - media = Image.from_numpy(np_img) - else: - raise ValueError(request.param) - - return DatasetItem( - id="item", - subset="train", - media=media, - annotations=[ - Label(label=0), - Bbox(x=0, y=0, w=1, h=1, label=0), - Mask(label=0, image=np.zeros(shape=(10, 10), dtype=np.uint8)), - ], - ) - - -@pytest.fixture() -def fxt_mock_dm_subset(mocker: MockerFixture, fxt_dm_item: DatasetItem) -> MagicMock: - mock_dm_subset = mocker.MagicMock(spec=DatasetSubset) - mock_dm_subset.name = fxt_dm_item.subset - mock_dm_subset.get.return_value = fxt_dm_item - mock_dm_subset.__iter__.return_value = iter([fxt_dm_item]) - return mock_dm_subset - - -@pytest.fixture( - params=[ - (OTXMulticlassClsDataset, MulticlassClsDataEntity), - (OTXDetectionDataset, DetDataEntity), - (OTXSegmentationDataset, SegDataEntity), - ], - ids=["multi_class_cls", "detection", "semantic_seg"], -) -def fxt_dataset_and_data_entity_cls( - request: pytest.FixtureRequest, -) -> tuple[OTXDataset, T_OTXDataEntity]: - return request.param diff --git a/tests/unit/core/data/dataset/__init__.py b/tests/unit/core/data/dataset/__init__.py deleted file mode 100644 index 4fa2be96bd2..00000000000 --- a/tests/unit/core/data/dataset/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of datasets.""" diff --git a/tests/unit/core/data/dataset/test_classification.py b/tests/unit/core/data/dataset/test_classification.py deleted file mode 100644 index 8bef7ffa4e2..00000000000 --- a/tests/unit/core/data/dataset/test_classification.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -"""Unit tests of classification datasets.""" - -from unittest.mock import MagicMock - -from otx.core.data.dataset.classification import OTXHlabelClsDataset - - -class TestOTXHlabelClsDataset: - def test_add_ancestors(self, fxt_hlabel_dataset_subset): - original_anns = fxt_hlabel_dataset_subset.get(id=0, subset="train").annotations - assert len(original_anns) == 1 - - hlabel_dataset = OTXHlabelClsDataset( - dm_subset=fxt_hlabel_dataset_subset, - transforms=MagicMock(), - ) - # Added the ancestor - adjusted_anns = hlabel_dataset.dm_subset.get(id=0, subset="train").annotations - assert len(adjusted_anns) == 2 diff --git a/tests/unit/core/data/dataset/test_visual_prompting.py b/tests/unit/core/data/dataset/test_visual_prompting.py deleted file mode 100644 index 51795c0f861..00000000000 --- a/tests/unit/core/data/dataset/test_visual_prompting.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of visual prompting datasets.""" - -from __future__ import annotations - -import numpy as np -import pytest -from datumaro import Dataset as DmDataset -from otx.core.data.dataset.visual_prompting import OTXVisualPromptingDataset, OTXZeroShotVisualPromptingDataset -from otx.core.data.entity.base import ImageInfo, Points -from torch import Tensor -from torchvision.transforms.v2 import Identity, Transform -from torchvision.tv_tensors import BoundingBoxes, Image, Mask - - -class TestOTXVisualPromptingDataset: - @pytest.fixture() - def fxt_dm_dataset(self) -> DmDataset: - return DmDataset.import_from("tests/assets/car_tree_bug", format="coco_instances") - - @pytest.fixture() - def fxt_tvt_transforms(self, mocker) -> Identity: - return Identity() - - @pytest.mark.parametrize("subset", ["train", "val"]) - @pytest.mark.parametrize("use_bbox", [True, False]) - @pytest.mark.parametrize("use_point", [True, False]) - def test_get_item_impl_subset( - self, - fxt_dm_dataset, - fxt_tvt_transforms: Transform, - subset: str, - use_bbox: bool, - use_point: bool, - ) -> None: - dataset = OTXVisualPromptingDataset( - fxt_dm_dataset.subsets()[subset], - fxt_tvt_transforms, - use_bbox=use_bbox, - use_point=use_point, - ) - - if not use_bbox and not use_point: - assert dataset.prob == 1.0 - - entity = dataset._get_item_impl(0) - - assert hasattr(entity, "image") - assert isinstance(entity.image, np.ndarray) - assert hasattr(entity, "img_info") - assert isinstance(entity.img_info, ImageInfo) - assert hasattr(entity, "masks") - assert isinstance(entity.masks, Mask) - assert hasattr(entity, "labels") - assert isinstance(entity.labels, dict) - assert hasattr(entity, "polygons") - assert isinstance(entity.polygons, list) - assert hasattr(entity, "bboxes") - assert hasattr(entity, "points") - - if not use_point: - assert isinstance(entity.bboxes, BoundingBoxes) - - if not use_bbox and use_point: - assert isinstance(entity.points, Points) - - -class TestOTXZeroShotVisualPromptingDataset: - @pytest.fixture() - def fxt_dm_dataset(self) -> DmDataset: - return DmDataset.import_from("tests/assets/car_tree_bug", format="coco_instances") - - @pytest.fixture() - def fxt_tvt_transforms(self, mocker) -> Identity: - return Identity() - - @pytest.mark.parametrize("use_bbox", [True, False]) - @pytest.mark.parametrize("use_point", [True, False]) - def test_get_item_impl_subset( - self, - fxt_dm_dataset, - fxt_tvt_transforms: Transform, - use_bbox: bool, - use_point: bool, - ) -> None: - dataset = OTXZeroShotVisualPromptingDataset( - fxt_dm_dataset.subsets()["train"], - fxt_tvt_transforms, - use_bbox=use_bbox, - use_point=use_point, - ) - - if not use_bbox and not use_point: - assert dataset.prob == 1.0 - - entity = dataset._get_item_impl(0) - - assert hasattr(entity, "image") - assert isinstance(entity.image, Image) - assert hasattr(entity, "img_info") - assert isinstance(entity.img_info, ImageInfo) - assert hasattr(entity, "masks") - assert isinstance(entity.masks, Mask) - assert hasattr(entity, "labels") - assert isinstance(entity.labels, Tensor) - assert hasattr(entity, "polygons") - assert isinstance(entity.polygons, list) - assert hasattr(entity, "prompts") - - if not use_point: - assert all([isinstance(p, BoundingBoxes) for p in entity.prompts]) # noqa: C419 - - if not use_bbox and use_point: - assert all([isinstance(p, Points) for p in entity.prompts]) # noqa: C419 diff --git a/tests/unit/core/data/entity/__init__.py b/tests/unit/core/data/entity/__init__.py deleted file mode 100644 index 4e265190959..00000000000 --- a/tests/unit/core/data/entity/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of data entities.""" diff --git a/tests/unit/core/data/entity/conftest.py b/tests/unit/core/data/entity/conftest.py deleted file mode 100644 index 00e31e2cd7e..00000000000 --- a/tests/unit/core/data/entity/conftest.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Fixtures for unit tests of data entities.""" - -import numpy as np -import pytest -import torch -from datumaro import Polygon -from otx.core.data.entity.base import ImageInfo, OTXDataEntity, Points -from otx.core.data.entity.visual_prompting import VisualPromptingDataEntity -from torch import LongTensor -from torchvision import tv_tensors - - -@pytest.fixture() -def fxt_numpy_data_entity() -> OTXDataEntity: - return OTXDataEntity( - np.ndarray((10, 10, 3)), - ImageInfo(img_idx=0, img_shape=(10, 10), ori_shape=(10, 10)), - ) - - -@pytest.fixture() -def fxt_torchvision_data_entity() -> OTXDataEntity: - return OTXDataEntity( - tv_tensors.Image(torch.randn(3, 10, 10)), - ImageInfo(img_idx=0, img_shape=(10, 10), ori_shape=(10, 10)), - ) - - -@pytest.fixture() -def fxt_visual_prompting_data_entity() -> VisualPromptingDataEntity: - return VisualPromptingDataEntity( - image=tv_tensors.Image(torch.randn(3, 10, 10)), - img_info=ImageInfo(img_idx=0, img_shape=(10, 10), ori_shape=(10, 10)), - masks=tv_tensors.Mask(torch.ones(10, 10)), - labels=[LongTensor([1]), LongTensor([2])], - polygons=[Polygon(points=[1, 1, 2, 2, 3, 3, 4, 4])], - bboxes=tv_tensors.BoundingBoxes(data=torch.Tensor([0, 0, 5, 5]), format="xyxy", canvas_size=(10, 10)), - points=Points(data=torch.Tensor([7, 7]), canvas_size=(10, 10)), - ) diff --git a/tests/unit/core/data/entity/test_base.py b/tests/unit/core/data/entity/test_base.py deleted file mode 100644 index 7515327135d..00000000000 --- a/tests/unit/core/data/entity/test_base.py +++ /dev/null @@ -1,159 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of base data entity.""" - - -import pytest -import torch -import torchvision.transforms.v2 as tvt -import torchvision.transforms.v2.functional as F # noqa: N812 -from otx.core.data.entity.base import ImageType, OTXBatchDataEntity, OTXDataEntity, Points -from otx.core.data.entity.visual_prompting import VisualPromptingDataEntity - - -class TestOTXDataEntity: - def test_image_type( - self, - fxt_numpy_data_entity, - fxt_torchvision_data_entity, - ) -> None: - assert fxt_numpy_data_entity.image_type == ImageType.NUMPY - assert fxt_torchvision_data_entity.image_type == ImageType.TV_IMAGE - - -class TestOTXBatchDataEntity: - def test_collate_fn(self, mocker, fxt_torchvision_data_entity) -> None: - mocker.patch.object(OTXDataEntity, "task", return_value="detection") - mocker.patch.object(OTXBatchDataEntity, "task", return_value="detection") - data_entities = [ - fxt_torchvision_data_entity, - fxt_torchvision_data_entity, - fxt_torchvision_data_entity, - ] - - data_batch = OTXBatchDataEntity.collate_fn(data_entities) - assert len(data_batch.imgs_info) == len(data_batch.images) - - -class TestImageInfo: - @pytest.fixture(autouse=True) - def fix_seed(self) -> None: - torch.manual_seed(3003) - - @pytest.fixture() - def fxt_resize(self) -> tvt.Resize: - return tvt.Resize(size=(2, 5)) - - @pytest.fixture() - def fxt_random_resize(self) -> tvt.RandomResize: - return tvt.RandomResize(min_size=2, max_size=5) - - @pytest.mark.parametrize("fxt_transform", ["fxt_resize", "fxt_random_resize"]) - def test_resize( - self, - fxt_torchvision_data_entity: OTXDataEntity, - fxt_transform: str, - request: pytest.FixtureRequest, - ) -> None: - transform = request.getfixturevalue(fxt_transform) - transformed = transform(fxt_torchvision_data_entity) - - assert transformed.image.shape[1:] == transformed.img_info.img_shape - assert fxt_torchvision_data_entity.image.shape[1:] == transformed.img_info.ori_shape - - scale_factor = ( - transformed.image.shape[1] / fxt_torchvision_data_entity.image.shape[1], - transformed.image.shape[2] / fxt_torchvision_data_entity.image.shape[2], - ) - assert scale_factor == transformed.img_info.scale_factor - - @pytest.fixture() - def fxt_random_crop(self) -> tvt.RandomCrop: - return tvt.RandomCrop(size=(2, 5)) - - @pytest.fixture() - def fxt_random_resized_crop(self) -> tvt.RandomResizedCrop: - return tvt.RandomResizedCrop(size=(2, 5)) - - @pytest.fixture() - def fxt_center_crop(self) -> tvt.CenterCrop: - return tvt.CenterCrop(size=(2, 5)) - - @pytest.mark.parametrize( - "fxt_transform", - ["fxt_random_crop", "fxt_random_resized_crop", "fxt_center_crop"], - ) - def test_crop( - self, - fxt_torchvision_data_entity: OTXDataEntity, - fxt_transform: str, - request: pytest.FixtureRequest, - ) -> None: - transform = request.getfixturevalue(fxt_transform) - transformed = transform(fxt_torchvision_data_entity) - - assert transformed.image.shape[1:] == transformed.img_info.img_shape - assert fxt_torchvision_data_entity.image.shape[1:] == transformed.img_info.ori_shape - assert transformed.img_info.scale_factor is None - - def test_pad( - self, - fxt_torchvision_data_entity: OTXDataEntity, - ) -> None: - transform = tvt.Pad(padding=(1, 2, 3, 4)) - transformed = transform(fxt_torchvision_data_entity) - - assert transformed.image.shape[1:] != transformed.img_info.img_shape - assert transformed.image.shape[1:] == transformed.img_info.pad_shape - assert fxt_torchvision_data_entity.image.shape[1:] == transformed.img_info.ori_shape - assert transformed.img_info.padding == (1, 2, 3, 4) - - def test_normalize( - self, - fxt_torchvision_data_entity: OTXDataEntity, - ) -> None: - mean = (100, 101, 102) - std = (1, 2, 3) - transform = tvt.Normalize(mean=mean, std=std) - transformed = transform(fxt_torchvision_data_entity) - - assert transformed.img_info.normalized - assert transformed.img_info.norm_mean == mean - assert transformed.img_info.norm_std == std - - @pytest.mark.skipif(not torch.cuda.is_available(), reason="Test only if CUDA is available.") - def test_to_cuda( - self, - fxt_torchvision_data_entity: OTXDataEntity, - ) -> None: - cuda_img_info = fxt_torchvision_data_entity.img_info.to(device="cuda") - # Do not lose its meta info although calling `Tensor.to(device="cuda")` - assert fxt_torchvision_data_entity.img_info.img_shape == cuda_img_info.img_shape - - -class TestPoints: - def test_resize(self, fxt_visual_prompting_data_entity: VisualPromptingDataEntity) -> None: - transform = tvt.Resize(size=(3, 5)) - results = transform(fxt_visual_prompting_data_entity) - - assert isinstance(results.points, Points) - assert results.points.canvas_size == tuple(transform.size) - assert results.points.canvas_size == results.img_info.img_shape - - assert str(results.points) == "Points([3.5000, 2.1000], canvas_size=(3, 5))" - - def test_pad(self, fxt_visual_prompting_data_entity: VisualPromptingDataEntity) -> None: - transform = tvt.Pad(padding=(1, 2, 3, 4)) - results = transform(fxt_visual_prompting_data_entity) - - assert results.points.canvas_size == results.image[1].shape - assert torch.all( - results.points == fxt_visual_prompting_data_entity.points + torch.tensor(transform.padding[:2]), - ) - - assert str(results.points) == "Points([8., 9.], canvas_size=(16, 14))" - - def test_get_size(self, fxt_visual_prompting_data_entity: VisualPromptingDataEntity) -> None: - results = F.get_size(fxt_visual_prompting_data_entity.points) - - assert results == [10, 10] diff --git a/tests/unit/core/data/entity/test_detection.py b/tests/unit/core/data/entity/test_detection.py deleted file mode 100644 index 28dcfc7cdf9..00000000000 --- a/tests/unit/core/data/entity/test_detection.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of detection data entity.""" - -import torch -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.detection import DetBatchDataEntity, DetDataEntity -from otx.core.types.task import OTXTaskType -from torch import LongTensor -from torchvision import tv_tensors - - -class TestDetDataEntity: - def test_task(self) -> None: - data_entity = DetDataEntity( - tv_tensors.Image(torch.randn(3, 224, 224)), - ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - tv_tensors.BoundingBoxes(data=torch.Tensor([0, 0, 50, 50]), format="xywh", canvas_size=(224, 224)), - LongTensor([1]), - ) - assert data_entity.task == OTXTaskType.DETECTION - - -class TestDetBatchDataEntity: - def test_collate_fn(self) -> None: - data_entities = [ - DetDataEntity( - tv_tensors.Image(torch.randn(3, 224, 224)), - ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - tv_tensors.BoundingBoxes( - data=torch.Tensor([0, 0, 50, 50]), - format="xywh", - canvas_size=(224, 224), - ), - LongTensor([1]), - ), - DetDataEntity( - tv_tensors.Image(torch.randn(3, 224, 224)), - ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - tv_tensors.BoundingBoxes( - data=torch.Tensor([0, 0, 50, 50]), - format="xywh", - canvas_size=(224, 224), - ), - LongTensor([1]), - ), - DetDataEntity( - tv_tensors.Image(torch.randn(3, 224, 224)), - ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - tv_tensors.BoundingBoxes( - data=torch.Tensor([0, 0, 50, 50]), - format="xywh", - canvas_size=(224, 224), - ), - LongTensor([1]), - ), - ] - - data_batch = DetBatchDataEntity.collate_fn(data_entities) - assert len(data_batch.imgs_info) == len(data_batch.images) - assert data_batch.task == OTXTaskType.DETECTION diff --git a/tests/unit/core/data/entity/test_visual_prompting.py b/tests/unit/core/data/entity/test_visual_prompting.py deleted file mode 100644 index 94ece5a86a6..00000000000 --- a/tests/unit/core/data/entity/test_visual_prompting.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of visual prompting data entity.""" - - -import torch -from datumaro import Polygon -from otx.core.data.entity.base import ImageInfo, Points -from otx.core.data.entity.visual_prompting import ( - VisualPromptingBatchDataEntity, - VisualPromptingDataEntity, - ZeroShotVisualPromptingBatchDataEntity, - ZeroShotVisualPromptingDataEntity, -) -from otx.core.types.task import OTXTaskType -from torch import LongTensor -from torchvision import tv_tensors - - -class TestVisualPromptingDataEntity: - def test_task(self) -> None: - data_entity = VisualPromptingDataEntity( - image=tv_tensors.Image(torch.randn(3, 224, 224)), - img_info=ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - masks=tv_tensors.Mask(torch.randint(low=0, high=1, size=(224, 224))), - labels=[LongTensor([1]), LongTensor([2])], - polygons=[Polygon(points=[1, 1, 2, 2, 3, 3, 4, 4])], - bboxes=tv_tensors.BoundingBoxes(data=torch.Tensor([0, 0, 50, 50]), format="xywh", canvas_size=(224, 224)), - points=Points(data=torch.Tensor([100, 100]), canvas_size=(224, 224)), - ) - assert data_entity.task == OTXTaskType.VISUAL_PROMPTING - - -class TestVisualPromptingBatchDataEntity: - def test_collate_fn(self) -> None: - data_entities = [ - VisualPromptingDataEntity( - image=tv_tensors.Image(torch.randn(3, 224, 224)), - img_info=ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - masks=tv_tensors.Mask(torch.randint(low=0, high=1, size=(224, 224))), - labels=[LongTensor([1]), LongTensor([2])], - polygons=[Polygon(points=[1, 1, 2, 2, 3, 3, 4, 4])], - bboxes=tv_tensors.BoundingBoxes( - data=torch.Tensor([0, 0, 50, 50]), - format="xywh", - canvas_size=(224, 224), - ), - points=Points(data=torch.Tensor([100, 100]), canvas_size=(224, 224)), - ), - VisualPromptingDataEntity( - image=tv_tensors.Image(torch.randn(3, 224, 224)), - img_info=ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - masks=tv_tensors.Mask(torch.randint(low=0, high=1, size=(224, 224))), - labels=[LongTensor([1]), LongTensor([2])], - polygons=[Polygon(points=[1, 1, 2, 2, 3, 3, 4, 4])], - bboxes=tv_tensors.BoundingBoxes( - data=torch.Tensor([0, 0, 50, 50]), - format="xywh", - canvas_size=(224, 224), - ), - points=Points(data=torch.Tensor([100, 100]), canvas_size=(224, 224)), - ), - VisualPromptingDataEntity( - image=tv_tensors.Image(torch.randn(3, 224, 224)), - img_info=ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - masks=tv_tensors.Mask(torch.randint(low=0, high=1, size=(224, 224))), - labels=[LongTensor([1]), LongTensor([2])], - polygons=[Polygon(points=[1, 1, 2, 2, 3, 3, 4, 4])], - bboxes=tv_tensors.BoundingBoxes( - data=torch.Tensor([0, 0, 50, 50]), - format="xywh", - canvas_size=(224, 224), - ), - points=Points(data=torch.Tensor([100, 100]), canvas_size=(224, 224)), - ), - ] - - data_batch = VisualPromptingBatchDataEntity.collate_fn(data_entities) - assert len(data_batch.imgs_info) == len(data_batch.images) - assert data_batch.task == OTXTaskType.VISUAL_PROMPTING - - -class TestZeroShotVisualPromptingDataEntity: - def test_task(self) -> None: - data_entity = ZeroShotVisualPromptingDataEntity( - image=tv_tensors.Image(torch.randn(3, 224, 224)), - img_info=ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - masks=tv_tensors.Mask(torch.randint(low=0, high=1, size=(224, 224))), - labels=[LongTensor([1]), LongTensor([2])], - polygons=[Polygon(points=[1, 1, 2, 2, 3, 3, 4, 4])], - prompts=[ - tv_tensors.BoundingBoxes(data=torch.Tensor([0, 0, 50, 50]), format="xywh", canvas_size=(224, 224)), - Points(data=torch.Tensor([100, 100]), canvas_size=(224, 224)), - ], - ) - assert data_entity.task == OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING - - -class TestZeroShotVisualPromptingBatchDataEntity: - def test_collate_fn(self) -> None: - data_entities = [ - ZeroShotVisualPromptingDataEntity( - image=tv_tensors.Image(torch.randn(3, 224, 224)), - img_info=ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - masks=tv_tensors.Mask(torch.randint(low=0, high=1, size=(224, 224))), - labels=[LongTensor([1]), LongTensor([2])], - polygons=[Polygon(points=[1, 1, 2, 2, 3, 3, 4, 4])], - prompts=[ - tv_tensors.BoundingBoxes(data=torch.Tensor([0, 0, 50, 50]), format="xywh", canvas_size=(224, 224)), - Points(data=torch.Tensor([100, 100]), canvas_size=(224, 224)), - ], - ), - ZeroShotVisualPromptingDataEntity( - image=tv_tensors.Image(torch.randn(3, 224, 224)), - img_info=ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - masks=tv_tensors.Mask(torch.randint(low=0, high=1, size=(224, 224))), - labels=[LongTensor([1]), LongTensor([2])], - polygons=[Polygon(points=[1, 1, 2, 2, 3, 3, 4, 4])], - prompts=[ - tv_tensors.BoundingBoxes(data=torch.Tensor([0, 0, 50, 50]), format="xywh", canvas_size=(224, 224)), - Points(data=torch.Tensor([100, 100]), canvas_size=(224, 224)), - ], - ), - ZeroShotVisualPromptingDataEntity( - image=tv_tensors.Image(torch.randn(3, 224, 224)), - img_info=ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - masks=tv_tensors.Mask(torch.randint(low=0, high=1, size=(224, 224))), - labels=[LongTensor([1]), LongTensor([2])], - polygons=[Polygon(points=[1, 1, 2, 2, 3, 3, 4, 4])], - prompts=[ - tv_tensors.BoundingBoxes(data=torch.Tensor([0, 0, 50, 50]), format="xywh", canvas_size=(224, 224)), - Points(data=torch.Tensor([100, 100]), canvas_size=(224, 224)), - ], - ), - ] - - data_batch = ZeroShotVisualPromptingBatchDataEntity.collate_fn(data_entities) - assert len(data_batch.imgs_info) == len(data_batch.images) - assert data_batch.task == OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING diff --git a/tests/unit/core/data/manager/test_dataset_manager.py b/tests/unit/core/data/manager/test_dataset_manager.py new file mode 100644 index 00000000000..41a84740d95 --- /dev/null +++ b/tests/unit/core/data/manager/test_dataset_manager.py @@ -0,0 +1,118 @@ +"""Unit-Test case for otx.core.data.manager.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +import shutil +from typing import List +from tempfile import TemporaryDirectory + +import pytest + +from datumaro.components.dataset import DatasetSubset +from otx.cli.manager.config_manager import TASK_TYPE_TO_SUPPORTED_FORMAT +from otx.core.data.manager.dataset_manager import DatasetManager +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.data.test_helpers import ( + generate_datumaro_dataset, + generate_datumaro_dataset_item, +) + +AVAILABLE_TASKS = ["classification", "detection", "segmentation"] +AVAILABLE_SUBSETS = ["train", "val"] +AVAILABLE_DATA_ROOTS = [ + "tests/assets/classification_dataset", + "tests/assets/car_tree_bug", + "tests/assets/cityscapes_dataset/dataset", + "tests/assets/anomaly/hazelnut", + "tests/assets/cvat_dataset/action_classification/train", +] + +DATA_ROOTS2FORMAT = { + "tests/assets/classification_dataset": "imagenet", + "tests/assets/car_tree_bug": "coco", + "tests/assets/cityscapes_dataset/dataset": "cityscapes", +} + + +class TestOTXDatasetManager: + def setup_method(self) -> None: + self.dataset = {} + for subset in AVAILABLE_SUBSETS: + self.dataset[subset] = {} + for task in AVAILABLE_TASKS: + self.dataset[subset][task] = generate_datumaro_dataset(subsets=[subset], task=task) + + @e2e_pytest_unit + @pytest.mark.parametrize("task", AVAILABLE_TASKS) + @pytest.mark.parametrize("subset", AVAILABLE_SUBSETS) + def test_get_train_dataset(self, task: List[str], subset: List[str]): + if subset == "val": + with pytest.raises(ValueError, match="Can't find training data."): + DatasetManager.get_train_dataset(self.dataset[subset][task]) + else: + train_dataset = DatasetManager.get_train_dataset(self.dataset[subset][task]) + assert isinstance(train_dataset, DatasetSubset) + + @e2e_pytest_unit + @pytest.mark.parametrize("task", AVAILABLE_TASKS) + @pytest.mark.parametrize("subset", AVAILABLE_SUBSETS) + def test_get_val_dataset(self, task: List[str], subset: List[str]): + if subset == "train": + assert DatasetManager.get_val_dataset(self.dataset[subset][task]) is None + else: + val_dataset = DatasetManager.get_val_dataset(self.dataset[subset][task]) + assert isinstance(val_dataset, DatasetSubset) + + @e2e_pytest_unit + @pytest.mark.parametrize("data_root", AVAILABLE_DATA_ROOTS) + def test_get_data_format(self, data_root: str): + assert isinstance(DatasetManager.get_data_format(data_root), str) + + @e2e_pytest_unit + @pytest.mark.parametrize("task", AVAILABLE_TASKS) + @pytest.mark.parametrize("subset", AVAILABLE_SUBSETS) + def test_get_image_path(self, task, subset): + random_data = DatasetManager.get_image_path( + generate_datumaro_dataset_item(item_id="0", subset=subset, task=task) + ) + assert random_data is None + + with TemporaryDirectory() as temp_dir: + random_data = DatasetManager.get_image_path( + generate_datumaro_dataset_item(item_id="0", subset=subset, task=task, temp_dir=temp_dir) + ) + assert random_data is not None + + @e2e_pytest_unit + @pytest.mark.parametrize("task", AVAILABLE_TASKS) + @pytest.mark.parametrize("subset", AVAILABLE_SUBSETS) + def test_export_dataset(self, task, subset, tmp_dir_path): + data_format = TASK_TYPE_TO_SUPPORTED_FORMAT[task.upper()][0] + DatasetManager.export_dataset(self.dataset[subset][task], tmp_dir_path, data_format, save_media=False) + shutil.rmtree(tmp_dir_path) + + @e2e_pytest_unit + @pytest.mark.parametrize("data_root", AVAILABLE_DATA_ROOTS[:3]) + def test_import_dataset(self, data_root): + data_format = DATA_ROOTS2FORMAT[data_root] + assert DatasetManager.import_dataset(data_root, data_format=data_format) is not None + + # TODO: Currently, direct annotation only supports COCO format + @e2e_pytest_unit + @pytest.mark.parametrize("data_root", [AVAILABLE_DATA_ROOTS[1]]) + def test_import_dataset_with_direct_annotation(self, data_root): + data_format = DATA_ROOTS2FORMAT[data_root] + assert DatasetManager.import_dataset(data_root, data_format=data_format) is not None + + ann_files = "tests/assets/car_tree_bug/annotations/instances_train_5_imgs.json" + train_dataset = DatasetManager.import_dataset(ann_files, data_format=data_format, subset="train") + assert train_dataset.get_subset("train_5_imgs").get_annotated_items() == 5 + + @e2e_pytest_unit + @pytest.mark.parametrize("task", AVAILABLE_TASKS) + @pytest.mark.parametrize("subset", AVAILABLE_SUBSETS) + def test_auto_split(self, task, subset): + dataset = DatasetManager.auto_split( + task=task, dataset=self.dataset[subset][task], split_ratio=[("train", 0.8), ("val", 0.2)] + ) + assert dataset is not None diff --git a/tests/unit/core/data/test_caching.py b/tests/unit/core/data/test_caching.py new file mode 100644 index 00000000000..bf6ad96f54d --- /dev/null +++ b/tests/unit/core/data/test_caching.py @@ -0,0 +1,91 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import string + +import numpy as np +import pytest +import psutil + +from otx.core.data.caching import MemCacheHandlerSingleton + + +@pytest.fixture +def fxt_data_list(): + np.random.seed(3003) + + num_data = 10 + h = w = key_len = 16 + + data_list = [] + for _ in range(num_data): + data = np.random.randint(0, 256, size=[h, w, 3], dtype=np.uint8) + key = "".join( + [string.ascii_lowercase[i] for i in np.random.randint(0, len(string.ascii_lowercase), size=[key_len])] + ) + meta = { + "key": key, + } + data_list += [(key, data, meta)] + + return data_list + + +def get_data_list_size(data_list): + size = 0 + for _, data, _ in data_list: + size += data.size + return size + + +class TestMemCacheHandler: + @pytest.mark.parametrize("mode", ["singleprocessing", "multiprocessing"]) + def test_cpu_limits(self, mode): + memory_info = psutil.virtual_memory() + total_mem_size_GiB = int(memory_info.total / (1024**3)) + mem_size = total_mem_size_GiB - (MemCacheHandlerSingleton.CPU_MEM_LIMITS_GIB - 5) + MemCacheHandlerSingleton.create(mode, mem_size * (1024**3)) + assert MemCacheHandlerSingleton.instance.mem_size == 0 + + @pytest.mark.parametrize("mode", ["singleprocessing", "multiprocessing"]) + def test_fully_caching(self, mode, fxt_data_list): + mem_size = get_data_list_size(fxt_data_list) + MemCacheHandlerSingleton.create(mode, mem_size) + handler = MemCacheHandlerSingleton.get() + + for key, data, meta in fxt_data_list: + assert handler.put(key, data, meta) > 0 + + for key, data, meta in fxt_data_list: + get_data, get_meta = handler.get(key) + + assert np.array_equal(get_data, data) + assert get_meta == meta + + # Fully cached + assert len(handler) == len(fxt_data_list) + + @pytest.mark.parametrize("mode", ["singleprocessing", "multiprocessing"]) + def test_unfully_caching(self, mode, fxt_data_list): + mem_size = get_data_list_size(fxt_data_list) // 2 + MemCacheHandlerSingleton.create(mode, mem_size) + handler = MemCacheHandlerSingleton.get() + + for idx, (key, data, meta) in enumerate(fxt_data_list): + if idx < len(fxt_data_list) // 2: + assert handler.put(key, data, meta) > 0 + else: + assert handler.put(key, data, meta) is None + + for idx, (key, data, meta) in enumerate(fxt_data_list): + get_data, get_meta = handler.get(key) + + if idx < len(fxt_data_list) // 2: + assert np.array_equal(get_data, data) + assert get_meta == meta + else: + assert get_data is None + + # Unfully (half) cached + assert len(handler) == len(fxt_data_list) // 2 diff --git a/tests/unit/core/data/test_dataset.py b/tests/unit/core/data/test_dataset.py deleted file mode 100644 index 034205c9d6c..00000000000 --- a/tests/unit/core/data/test_dataset.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from unittest.mock import MagicMock - -import pytest - - -class TestDataset: - def test_get_item( - self, - mocker, - fxt_dataset_and_data_entity_cls, - fxt_mock_dm_subset: MagicMock, - ) -> None: - dataset_cls, data_entity_cls = fxt_dataset_and_data_entity_cls - dataset = dataset_cls( - dm_subset=fxt_mock_dm_subset, - transforms=lambda x: x, - mem_cache_img_max_size=None, - max_refetch=3, - ) - item = dataset[0] - - assert isinstance(item, data_entity_cls) - fxt_mock_dm_subset.get.assert_called_once() - - mocker.patch.object(dataset, "_get_item_impl", return_value=None) - with pytest.raises(RuntimeError): - dataset[0] - - def test_sample_another_idx( - self, - fxt_dataset_and_data_entity_cls, - fxt_mock_dm_subset, - ) -> None: - dataset_cls, dataset_entity_cls = fxt_dataset_and_data_entity_cls - dataset = dataset_cls( - dm_subset=fxt_mock_dm_subset, - transforms=lambda x: x, - mem_cache_img_max_size=None, - ) - assert dataset._sample_another_idx() < len(dataset) - - @pytest.mark.parametrize("mem_cache_img_max_size", [(3, 5), (5, 3)]) - def test_mem_cache_resize( - self, - mem_cache_img_max_size, - fxt_mem_cache_handler, - fxt_dataset_and_data_entity_cls, - fxt_mock_dm_subset: MagicMock, - fxt_dm_item, - ) -> None: - dataset_cls, data_entity_cls = fxt_dataset_and_data_entity_cls - - dataset = dataset_cls( - dm_subset=fxt_mock_dm_subset, - transforms=lambda x: x, - mem_cache_handler=fxt_mem_cache_handler, - mem_cache_img_max_size=mem_cache_img_max_size, - ) - - item = dataset[0] # Put in the cache - - # The returned image should be resized because it was resized before caching - h_expected = w_expected = min(mem_cache_img_max_size) - assert item.image.shape[:2] == (h_expected, w_expected) - assert item.img_info.img_shape == (h_expected, w_expected) - - item = dataset[0] # Take from the cache - - assert item.image.shape[:2] == (h_expected, w_expected) - assert item.img_info.img_shape == (h_expected, w_expected) diff --git a/tests/unit/core/data/test_factory.py b/tests/unit/core/data/test_factory.py deleted file mode 100644 index 328ad03d173..00000000000 --- a/tests/unit/core/data/test_factory.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Test Factory classes for dataset and transforms.""" - -import pytest -from otx.core.config.data import DataModuleConfig, SubsetConfig, TileConfig, VisualPromptingConfig -from otx.core.data.dataset.classification import OTXMulticlassClsDataset -from otx.core.data.dataset.detection import OTXDetectionDataset -from otx.core.data.dataset.segmentation import OTXSegmentationDataset -from otx.core.data.dataset.visual_prompting import OTXVisualPromptingDataset, OTXZeroShotVisualPromptingDataset -from otx.core.data.factory import OTXDatasetFactory, TransformLibFactory -from otx.core.data.transform_libs.mmcv import MMCVTransformLib -from otx.core.data.transform_libs.mmdet import MMDetTransformLib -from otx.core.data.transform_libs.mmpretrain import MMPretrainTransformLib -from otx.core.data.transform_libs.mmseg import MMSegTransformLib -from otx.core.data.transform_libs.torchvision import TorchVisionTransformLib -from otx.core.types.task import OTXTaskType -from otx.core.types.transformer_libs import TransformLibType - - -class TestTransformLibFactory: - @pytest.mark.parametrize( - ("lib_type", "lib"), - [ - (TransformLibType.TORCHVISION, TorchVisionTransformLib), - (TransformLibType.MMCV, MMCVTransformLib), - (TransformLibType.MMPRETRAIN, MMPretrainTransformLib), - (TransformLibType.MMDET, MMDetTransformLib), - (TransformLibType.MMSEG, MMSegTransformLib), - ], - ) - def test_generate(self, lib_type, lib, mocker) -> None: - mock_generate = mocker.patch.object(lib, "generate") - config = mocker.MagicMock(spec=SubsetConfig) - config.transform_lib_type = lib_type - _ = TransformLibFactory.generate(config) - mock_generate.assert_called_once_with(config) - - -class TestOTXDatasetFactory: - @pytest.mark.parametrize( - ("task_type", "dataset_cls"), - [ - (OTXTaskType.MULTI_CLASS_CLS, OTXMulticlassClsDataset), - (OTXTaskType.DETECTION, OTXDetectionDataset), - (OTXTaskType.SEMANTIC_SEGMENTATION, OTXSegmentationDataset), - (OTXTaskType.VISUAL_PROMPTING, OTXVisualPromptingDataset), - (OTXTaskType.ZERO_SHOT_VISUAL_PROMPTING, OTXZeroShotVisualPromptingDataset), - ], - ) - def test_create(self, fxt_mock_dm_subset, fxt_mem_cache_handler, task_type, dataset_cls, mocker) -> None: - mocker.patch.object(TransformLibFactory, "generate", return_value=None) - cfg_subset = mocker.MagicMock(spec=SubsetConfig) - cfg_data_module = mocker.MagicMock(spec=DataModuleConfig) - cfg_data_module.tile_config = mocker.MagicMock(spec=TileConfig) - cfg_data_module.tile_config.enable_tiler = False - cfg_data_module.vpm_config = mocker.MagicMock(spec=VisualPromptingConfig) - cfg_data_module.vpm_config.use_bbox = False - cfg_data_module.vpm_config.use_point = False - assert isinstance( - OTXDatasetFactory.create( - task=task_type, - dm_subset=fxt_mock_dm_subset, - mem_cache_handler=fxt_mem_cache_handler, - cfg_subset=cfg_subset, - cfg_data_module=cfg_data_module, - ), - dataset_cls, - ) diff --git a/tests/unit/core/data/test_helpers.py b/tests/unit/core/data/test_helpers.py new file mode 100644 index 00000000000..6bc973c4159 --- /dev/null +++ b/tests/unit/core/data/test_helpers.py @@ -0,0 +1,175 @@ +"""Test Helpers for otx.core.data.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from typing import List, Optional +import os + +import cv2 +import numpy as np +from datumaro.components.annotation import Label, Bbox, Mask +from datumaro.components.dataset import Dataset +from datumaro.components.dataset_base import DatasetItem +from datumaro.components.media import ImageFromFile, ImageFromNumpy + + +from otx.api.entities.model_template import TaskType + +TASK_NAME_TO_TASK_TYPE = { + "classification": TaskType.CLASSIFICATION, + "detection": TaskType.DETECTION, + "rotated_detection": TaskType.ROTATED_DETECTION, + "instance_segmentation": TaskType.INSTANCE_SEGMENTATION, + "segmentation": TaskType.SEGMENTATION, + "anomaly_classification": TaskType.ANOMALY_CLASSIFICATION, + "anomaly_detection": TaskType.ANOMALY_DETECTION, + "anomaly_segmentation": TaskType.ANOMALY_SEGMENTATION, + "action_classification": TaskType.ACTION_CLASSIFICATION, + "action_detection": TaskType.ACTION_DETECTION, + "visual_prompting": TaskType.VISUAL_PROMPTING, +} + +TASK_NAME_TO_DATA_ROOT = { + "classification": { + "train": "tests/assets/classification_dataset", + "val": "tests/assets/classification_dataset", + "test": "tests/assets/classification_dataset", + "unlabeled": "tests/assets/classification_dataset", + }, + "detection": { + "train": "tests/assets/car_tree_bug", + "val": "tests/assets/car_tree_bug", + "test": "tests/assets/car_tree_bug", + "unlabeled": "tests/assets/car_tree_bug", + }, + "rotated_detection": { + "train": "tests/assets/car_tree_bug", + "val": "tests/assets/car_tree_bug", + "test": "tests/assets/car_tree_bug", + "unlabeled": "tests/assets/car_tree_bug", + }, + "instance_segmentation": { + "train": "tests/assets/car_tree_bug", + "val": "tests/assets/car_tree_bug", + "test": "tests/assets/car_tree_bug", + }, + "segmentation": { + "train": "tests/assets/common_semantic_segmentation_dataset/train", + "val": "tests/assets/common_semantic_segmentation_dataset/val", + "test": "tests/assets/common_semantic_segmentation_dataset/val", + "unlabeled": "tests/assets/common_semantic_segmentation_dataset/val", + }, + "anomaly_classification": { + "train": "tests/assets/anomaly/hazelnut", + "val": "tests/assets/anomaly/hazelnut", + "test": "tests/assets/anomaly/hazelnut", + }, + "anomaly_detection": { + "train": "tests/assets/anomaly/hazelnut", + "val": "tests/assets/anomaly/hazelnut", + "test": "tests/assets/anomaly/hazelnut", + }, + "anomaly_segmentation": { + "train": "tests/assets/anomaly/hazelnut", + "val": "tests/assets/anomaly/hazelnut", + "test": "tests/assets/anomaly/hazelnut", + }, + "action_classification": { + "train": "tests/assets/cvat_dataset/action_classification/train", + "val": "tests/assets/cvat_dataset/action_classification/train", + "test": "tests/assets/cvat_dataset/action_classification/train", + }, + "action_detection": { + "train": "tests/assets/cvat_dataset/action_detection/train", + "val": "tests/assets/cvat_dataset/action_detection/train", + "test": "tests/assets/cvat_dataset/action_detection/train", + }, + "visual_prompting": { + "coco": { + "train": "tests/assets/car_tree_bug", + "val": "tests/assets/car_tree_bug", + "test": "tests/assets/car_tree_bug", + }, + "voc": { + "train": "tests/assets/voc_dataset/voc_dataset1", + "val": "tests/assets/voc_dataset/voc_dataset1", + "test": "tests/assets/voc_dataset/voc_dataset1", + }, + "common_semantic_segmentation": { + "train": "tests/assets/common_semantic_segmentation_dataset/train", + "val": "tests/assets/common_semantic_segmentation_dataset/val", + "test": "tests/assets/common_semantic_segmentation_dataset/val", + }, + }, +} + + +def generate_datumaro_dataset_item( + item_id: str, + subset: str, + task: str, + image_shape: np.array = np.array((5, 5, 3)), + mask_shape: np.array = np.array((5, 5)), + temp_dir: Optional[str] = None, +) -> DatasetItem: + """Generate Datumaro DatasetItem. + + Args: + item_id (str): The ID of dataset item + subset (str): subset of item, e.g. "train" or "val" + task (str): task type, e.g. "classification" + image_shape (np.array): the shape of image. + image_shape (np.array): the shape of mask. + temp_dir (str): directory to save image data + + Returns: + DatasetItem: Datumaro DatasetItem + """ + ann_task_dict = { + "classification": Label(label=0), + "detection": Bbox(1, 2, 3, 4, label=0), + "segmentation": Mask(np.zeros(mask_shape)), + } + + if temp_dir: + path = os.path.join(temp_dir, "image.png") + cv2.imwrite(path, np.ones(image_shape)) + return DatasetItem(id=item_id, subset=subset, media=ImageFromFile(path), annotations=[ann_task_dict[task]]) + + return DatasetItem( + id=item_id, subset=subset, media=ImageFromNumpy(np.ones(image_shape)), annotations=[ann_task_dict[task]] + ) + + +def generate_datumaro_dataset( + subsets: List[str], + task: str, + num_data: int = 1, + image_shape: np.array = np.array((5, 5, 3)), + mask_shape: np.array = np.array((5, 5)), +) -> Dataset: + """Generate Datumaro Dataset. + + Args: + subsets (List): the list of subset, e.g. ["train", "val"] + task (str): task name, e.g. "classification", "segmentation", .. + num_data (int): the number of dataset to make. + image_shape (np.array): the shape of image. + mask_shape (np.array): the shape of mask. + + Returns: + dm.Dataset: Datumaro Dataset + """ + dataset_items: DatasetItem = [] + for subset in subsets: + for idx in range(num_data): + dataset_items.append( + generate_datumaro_dataset_item( + item_id=f"{subset}/image{idx}", + subset=subset, + task=task, + image_shape=image_shape, + mask_shape=mask_shape, + ) + ) + return Dataset.from_iterable(dataset_items, categories=["cat", "dog"]) diff --git a/tests/unit/core/data/test_mem_cache.py b/tests/unit/core/data/test_mem_cache.py deleted file mode 100644 index 9172978bfe0..00000000000 --- a/tests/unit/core/data/test_mem_cache.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -from __future__ import annotations - -import string - -import numpy as np -import psutil -import pytest -from otx.core.data.mem_cache import ( - MemCacheHandlerSingleton, - parse_mem_cache_size_to_int, -) - - -@pytest.fixture() -def fxt_data_list() -> list: - bg = np.random.MT19937(seed=3003) - rg = np.random.Generator(bg) - num_data = 10 - h = w = key_len = 16 - - data_list = [] - for _ in range(num_data): - data = rg.integers(0, 256, size=[h, w, 3], dtype=np.uint8) - key = "".join( - [ - string.ascii_lowercase[i] - for i in rg.integers( - 0, - len(string.ascii_lowercase), - size=[key_len], - ) - ], - ) - meta = { - "key": key, - } - data_list += [(key, data, meta)] - - return data_list - - -def get_data_list_size(data_list: list) -> int: - size = 0 - for _, data, _ in data_list: - size += data.size - return size - - -class TestMemCacheHandler: - @pytest.mark.parametrize("mode", ["singleprocessing", "multiprocessing"]) - def test_cpu_limits(self, mode) -> None: - memory_info = psutil.virtual_memory() - total_mem_size_in_giga_bytes = int(memory_info.total / (1024**3)) - mem_size = total_mem_size_in_giga_bytes - (MemCacheHandlerSingleton.CPU_MEM_LIMITS_GIB - 5) - handler = MemCacheHandlerSingleton.create(mode, mem_size * (1024**3)) - assert handler.mem_size == 0 - - @pytest.mark.parametrize("mode", ["singleprocessing", "multiprocessing"]) - def test_fully_caching(self, mode, fxt_data_list, monkeypatch) -> None: - mem_size = get_data_list_size(fxt_data_list) - monkeypatch.setattr(MemCacheHandlerSingleton, "check_system_memory", lambda *_: True) - handler = MemCacheHandlerSingleton.create(mode, mem_size) - - for key, data, meta in fxt_data_list: - assert handler.put(key, data, meta) > 0 - - for key, data, meta in fxt_data_list: - get_data, get_meta = handler.get(key) - - assert np.array_equal(get_data, data) - assert get_meta == meta - - # Fully cached - assert len(handler) == len(fxt_data_list) - - @pytest.mark.parametrize("mode", ["singleprocessing", "multiprocessing"]) - def test_unfully_caching(self, mode, fxt_data_list, monkeypatch) -> None: - mem_size = get_data_list_size(fxt_data_list) // 2 - monkeypatch.setattr(MemCacheHandlerSingleton, "check_system_memory", lambda *_: True) - handler = MemCacheHandlerSingleton.create(mode, mem_size) - - for idx, (key, data, meta) in enumerate(fxt_data_list): - if idx < len(fxt_data_list) // 2: - assert handler.put(key, data, meta) > 0 - else: - assert handler.put(key, data, meta) is None - - for idx, (key, data, meta) in enumerate(fxt_data_list): - get_data, get_meta = handler.get(key) - - if idx < len(fxt_data_list) // 2: - assert np.array_equal(get_data, data) - assert get_meta == meta - else: - assert get_data is None - - # Unfully (half) cached - assert len(handler) == len(fxt_data_list) // 2 - - -@pytest.mark.parametrize( - ("mem_size_arg", "expected"), - [ - ("1561", 1561), - ("121k", 121 * (2**10)), - ("121kb", 121 * (10**3)), - ("121kib", 121 * (2**10)), - ("121m", 121 * (2**20)), - ("121mb", 121 * (10**6)), - ("121mib", 121 * (2**20)), - ("121g", 121 * (2**30)), - ("121gb", 121 * (10**9)), - ("121gib", 121 * (2**30)), - ("121as", None), - ("121dddd", None), - ], -) -def test_parse_mem_size_str(mem_size_arg, expected) -> None: - try: - assert parse_mem_cache_size_to_int(mem_size_arg) == expected - except ValueError: - assert expected is None diff --git a/tests/unit/core/data/test_module.py b/tests/unit/core/data/test_module.py deleted file mode 100644 index 9746e79789f..00000000000 --- a/tests/unit/core/data/test_module.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest -from datumaro.components.dataset import Dataset as DmDataset -from importlib_resources import files -from lightning.pytorch.loggers import CSVLogger -from omegaconf import DictConfig, OmegaConf -from otx.core.config.data import ( - DataModuleConfig, - SubsetConfig, - TileConfig, -) -from otx.core.data.module import ( - OTXDataModule, - OTXTaskType, -) - - -def mock_data_filtering(dataset: DmDataset, data_format: str, unannotated_items_ratio: float) -> DmDataset: - del data_format - del unannotated_items_ratio - return dataset - - -class TestModule: - @pytest.fixture() - def fxt_config(self) -> DataModuleConfig: - mock = MagicMock(spec=DataModuleConfig) - mock.data_format = "coco_instances" - mock.data_root = "." - mock.mem_cache_size = "1GB" - mock.train_subset = MagicMock(spec=SubsetConfig) - mock.train_subset.num_workers = 0 - mock.val_subset = MagicMock(spec=SubsetConfig) - mock.val_subset.num_workers = 0 - mock.test_subset = MagicMock(spec=SubsetConfig) - mock.test_subset.num_workers = 0 - mock.tile_config = MagicMock(spec=TileConfig) - mock.tile_config.enable_tiler = False - - return mock - - @patch("otx.core.data.module.OTXDatasetFactory") - @patch("otx.core.data.module.DmDataset.import_from") - @pytest.mark.parametrize( - "task", - [ - OTXTaskType.MULTI_CLASS_CLS, - OTXTaskType.MULTI_LABEL_CLS, - OTXTaskType.H_LABEL_CLS, - OTXTaskType.DETECTION, - OTXTaskType.SEMANTIC_SEGMENTATION, - ], - ) - def test_init( - self, - mock_dm_dataset, - mock_otx_dataset_factory, - task, - fxt_config, - mocker, - ) -> None: - # Our query for subset name for train, val, test - fxt_config.train_subset.subset_name = "train_1" - fxt_config.val_subset.subset_name = "val_1" - fxt_config.test_subset.subset_name = "test_1" - - # Dataset will have "train_0", "train_1", "val_0", ..., "test_1" subsets - mock_dm_subsets = {f"{name}_{idx}": MagicMock() for name in ["train", "val", "test"] for idx in range(2)} - mock_dm_dataset.return_value.subsets.return_value = mock_dm_subsets - - mocker.patch("otx.core.data.module.pre_filtering", side_effect=mock_data_filtering) - - OTXDataModule(task=task, config=fxt_config) - - assert mock_otx_dataset_factory.create.call_count == 3 - - @pytest.fixture() - def fxt_real_tv_cls_config(self) -> DictConfig: - cfg_path = files("otx") / "recipe" / "_base_" / "data" / "torchvision_base.yaml" - cfg = OmegaConf.load(cfg_path) - cfg = cfg.config - cfg.data_root = "." - cfg.train_subset.subset_name = "train" - cfg.train_subset.num_workers = 0 - cfg.val_subset.subset_name = "val" - cfg.val_subset.num_workers = 0 - cfg.test_subset.subset_name = "test" - cfg.test_subset.num_workers = 0 - cfg.mem_cache_size = "1GB" - cfg.tile_config = {} - cfg.tile_config.enable_tiler = False - cfg.auto_num_workers = False - cfg.device = "auto" - return cfg - - @patch("otx.core.data.module.OTXDatasetFactory") - @patch("otx.core.data.module.DmDataset.import_from") - def test_hparams_initial_is_loggable( - self, - mock_dm_dataset, - mock_otx_dataset_factory, - fxt_real_tv_cls_config, - tmpdir, - mocker, - ) -> None: - # Dataset will have "train", "val", and "test" subsets - mock_dm_subsets = {name: MagicMock() for name in ["train", "val", "test"]} - mock_dm_dataset.return_value.subsets.return_value = mock_dm_subsets - - mocker.patch("otx.core.data.module.pre_filtering", side_effect=mock_data_filtering) - - module = OTXDataModule(task=OTXTaskType.MULTI_CLASS_CLS, config=fxt_real_tv_cls_config) - logger = CSVLogger(tmpdir) - logger.log_hyperparams(module.hparams_initial) - logger.save() - - hparams_path = Path(logger.log_dir) / "hparams.yaml" - assert hparams_path.exists() diff --git a/tests/unit/core/data/test_pre_filtering.py b/tests/unit/core/data/test_pre_filtering.py deleted file mode 100644 index f3f9ccf2d18..00000000000 --- a/tests/unit/core/data/test_pre_filtering.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import pytest -from datumaro.components.annotation import Label -from datumaro.components.dataset import Dataset as DmDataset -from datumaro.components.dataset_base import DatasetItem -from otx.core.data.pre_filtering import pre_filtering - - -@pytest.fixture() -def fxt_dm_dataset_with_unannotated() -> DmDataset: - dataset_items = [ - DatasetItem( - id=f"item00{i}_non_empty", - subset="train", - media=None, - annotations=[ - Label(label=3 % i), - ], - ) - for i in range(1, 81) - ] - dataset_items.extend( - [ - DatasetItem( - id=f"item00{i}_empty", - subset="train", - media=None, - annotations=[], - ) - for i in range(20) - ], - ) - return DmDataset.from_iterable(dataset_items, categories=["0", "1", "2"]) - - -@pytest.mark.parametrize("unannotated_items_ratio", [0.0, 0.1, 0.5, 1.0]) -def test_pre_filtering(fxt_dm_dataset_with_unannotated: DmDataset, unannotated_items_ratio: float) -> None: - """Test function for pre_filtering. - - Args: - fxt_dm_dataset_with_unannotated (DmDataset): The dataset to be filtered. - unannotated_items_ratio (float): The ratio of unannotated background items to be added. - - Returns: - None - """ - empty_items = [ - item for item in fxt_dm_dataset_with_unannotated if item.subset == "train" and len(item.annotations) == 0 - ] - assert len(fxt_dm_dataset_with_unannotated) == 100 - assert len(empty_items) == 20 - - filtered_dataset = pre_filtering( - dataset=fxt_dm_dataset_with_unannotated, - data_format="datumaro", - unannotated_items_ratio=unannotated_items_ratio, - ) - assert len(filtered_dataset) == 80 + int(len(empty_items) * unannotated_items_ratio) diff --git a/tests/unit/core/data/test_storage_caching.py b/tests/unit/core/data/test_storage_caching.py new file mode 100644 index 00000000000..10aa1d9df15 --- /dev/null +++ b/tests/unit/core/data/test_storage_caching.py @@ -0,0 +1,132 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from copy import deepcopy +import os +import stat +import tempfile +import time + +import numpy as np +import pytest +from datumaro.components.annotation import Label, Polygon, Bbox, Mask +from datumaro.components.dataset import Dataset +from datumaro.components.dataset_base import DatasetItem +from datumaro.components.media import Image + +from otx.core.data.caching.storage_cache import init_arrow_cache + + +@pytest.fixture +def fxt_datumaro_dataset(): + items = [] + for i in range(64): + media = Image.from_numpy(data=np.random.randint(0, 255, (5, 5, 3)), ext=".png") + + items.append( + DatasetItem( + id=i, + subset="test", + media=media, + annotations=[ + # annotations used in OTX + Label(np.random.randint(0, 3)), + Bbox(*np.random.randint(0, 5, 4)), + Polygon(Bbox(*np.random.randint(0, 5, 4)).as_polygon()), + Mask(np.random.randint(0, 2, (5, 5))), + ], + ) + ) + + source_dataset = Dataset.from_iterable( + items, + categories=["label"], + media_type=Image, + ) + + return source_dataset + + +def compare_dataset(source_dataset, target_dataset, compare_media=True): + properties = ["id", "subset", "annotations", "attributes"] + if compare_media: + properties.append("media") + for item_s, item_t in zip(source_dataset, target_dataset): + for property in properties: + assert getattr(item_s, property) == getattr(item_t, property) + + +class TestStorageCache: + @pytest.mark.parametrize( + ["scheme", "compare_media"], + [ + pytest.param( + "NONE", + True, + id="test_none_scheme", + ), + pytest.param( + "AS-IS", + True, + id="test_as_is_scheme", + ), + pytest.param( + "PNG", + True, + id="test_png_scheme", + ), + pytest.param( + "TIFF", + True, + id="test_tiff_scheme", + ), + pytest.param( + "JPEG/95", + False, + id="test_jpeg_95_scheme", + ), + pytest.param( + "JPEG/75", + False, + id="test_jpeg_75_scheme", + ), + ], + ) + def test_is_identical(self, scheme, fxt_datumaro_dataset, compare_media): + with tempfile.TemporaryDirectory() as tempdir: + source_dataset = fxt_datumaro_dataset + cached_dataset = init_arrow_cache(source_dataset, scheme=scheme, cache_dir=tempdir) + compare_dataset(source_dataset, cached_dataset, compare_media) + + def test_cache_hit(self, fxt_datumaro_dataset): + with tempfile.TemporaryDirectory() as tempdir: + cached_dataset = init_arrow_cache(deepcopy(fxt_datumaro_dataset), scheme="AS-IS", cache_dir=tempdir) + + mapping = {} + for file in os.listdir(cached_dataset.data_path): + mapping[file] = os.stat(os.path.join(cached_dataset.data_path, file))[stat.ST_MTIME] + + cached_dataset = init_arrow_cache(deepcopy(fxt_datumaro_dataset), scheme="AS-IS", cache_dir=tempdir) + + for file in os.listdir(cached_dataset.data_path): + assert mapping[file] == os.stat(os.path.join(cached_dataset.data_path, file))[stat.ST_MTIME] + + def test_no_cache_hit(self, fxt_datumaro_dataset): + with tempfile.TemporaryDirectory() as tempdir: + cached_dataset = init_arrow_cache(deepcopy(fxt_datumaro_dataset), scheme="AS-IS", cache_dir=tempdir) + + mapping = {} + for file in os.listdir(cached_dataset.data_path): + mapping[file] = os.stat(os.path.join(cached_dataset.data_path, file))[stat.ST_MTIME] + + # sleep 1 second to invalidate cache + time.sleep(1) + + cached_dataset = init_arrow_cache( + deepcopy(fxt_datumaro_dataset), scheme="AS-IS", cache_dir=tempdir, force=True + ) + + for file in os.listdir(cached_dataset.data_path): + assert mapping[file] != os.stat(os.path.join(cached_dataset.data_path, file))[stat.ST_MTIME] diff --git a/tests/unit/core/data/test_transform_libs.py b/tests/unit/core/data/test_transform_libs.py deleted file mode 100644 index 0d9aac017e3..00000000000 --- a/tests/unit/core/data/test_transform_libs.py +++ /dev/null @@ -1,213 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -from __future__ import annotations - -from typing import Any - -import pytest -import torch -from lightning.pytorch.cli import instantiate_class -from omegaconf import OmegaConf -from otx.core.config.data import SubsetConfig -from otx.core.data.entity.base import Points -from otx.core.data.transform_libs.torchvision import ( - PadtoSquare, - PerturbBoundingBoxes, - ResizetoLongestEdge, - TorchVisionTransformLib, -) -from otx.core.types.image import ImageColorChannel -from torchvision import tv_tensors -from torchvision.transforms import v2 - - -class TestPerturbBoundingBoxes: - def test_transform(self) -> None: - transform = PerturbBoundingBoxes(offset=20) - inputs = tv_tensors.BoundingBoxes( - [ - [100, 100, 200, 200], # normal bbox - [0, 0, 299, 299], # can be out of size - [0, 0, 100, 100], # can be out of size - [100, 100, 299, 299], # can be out of size - ], - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=(300, 300), - dtype=torch.float32, - ) - - results = transform._transform(inputs, None) - - assert isinstance(results, tv_tensors.BoundingBoxes) - assert results.shape == inputs.shape - assert torch.all(results[:, :2] >= 0.0) - assert torch.all(results[:, 2:] <= 299.0) - - -class TestPadtoSquare: - def test_transform(self) -> None: - transform = PadtoSquare() - - # height > width - inpt = tv_tensors.Image(torch.ones((1, 5, 3))) - results = transform(inpt, transform._get_params([inpt])) - - assert isinstance(results, tuple) - assert results[0].shape == torch.Size((1, 5, 5)) - assert results[0][:, :, -2:].sum() == 0 - - # height < width - inpt = tv_tensors.Image(torch.ones((1, 3, 5))) - results = transform(inpt, transform._get_params([inpt])) - - assert isinstance(results, tuple) - assert results[0].shape == torch.Size((1, 5, 5)) - assert results[0][:, -2:, :].sum() == 0 - - # skip other formats - inpt = tv_tensors.BoundingBoxes( - [[1, 1, 3, 3]], - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=(5, 3), - dtype=torch.float32, - ) - results = transform(inpt.clone(), transform._get_params([inpt])) - - assert torch.all(results[0] == inpt) - - inpt = Points( - [[1, 1], [3, 3]], - canvas_size=(5, 3), - dtype=torch.float32, - ) - results = transform(inpt.clone(), transform._get_params([inpt])) - - assert torch.all(results[0] == inpt) - - -class TestResizetoLongestEdge: - def test_transform(self) -> None: - transform = ResizetoLongestEdge(size=10) - - # height > width - inpt = tv_tensors.Image(torch.ones((1, 5, 3))) - results = transform(inpt, transform._get_params([inpt])) - - assert isinstance(results, tuple) - assert results[0].shape == torch.Size((1, 10, 6)) - assert results[1]["target_size"] == (10, 6) - - # height < width - inpt = tv_tensors.Image(torch.ones((1, 3, 5))) - results = transform(inpt, transform._get_params([inpt])) - - assert isinstance(results, tuple) - assert results[0].shape == torch.Size((1, 6, 10)) - assert results[1]["target_size"] == (6, 10) - - # square - inpt = tv_tensors.Image(torch.ones((1, 5, 5))) - results = transform(inpt, transform._get_params([inpt])) - - assert isinstance(results, tuple) - assert results[0].shape == torch.Size((1, 10, 10)) - assert results[1]["target_size"] == (10, 10) - - -class TestTorchVisionTransformLib: - @pytest.fixture(params=["from_dict", "from_list", "from_compose"]) - def fxt_config(self, request) -> list[dict[str, Any]]: - if request.param == "from_compose": - return v2.Compose( - [ - v2.RandomResizedCrop(size=(224, 224), antialias=True), - v2.RandomHorizontalFlip(p=0.5), - v2.ToDtype(torch.float32, scale=True), - v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), - ], - ) - prefix = "torchvision.transforms.v2" - cfg = f""" - transforms: - - class_path: {prefix}.RandomResizedCrop - init_args: - size: [224, 224] - antialias: True - - class_path: {prefix}.RandomHorizontalFlip - init_args: - p: 0.5 - - class_path: {prefix}.ToDtype - init_args: - dtype: ${{as_torch_dtype:torch.float32}} - scale: True - - class_path: {prefix}.Normalize - init_args: - mean: [0.485, 0.456, 0.406] - std: [0.229, 0.224, 0.225] - """ - created = OmegaConf.create(cfg) - if request.param == "from_obj": - return SubsetConfig( - batch_size=1, - subset_name="dummy", - transforms=[instantiate_class(args=(), init=transform) for transform in created.transforms], - ) - return created - - def test_transform( - self, - fxt_config, - fxt_dataset_and_data_entity_cls, - fxt_mock_dm_subset, - ) -> None: - transform = TorchVisionTransformLib.generate(fxt_config) - assert isinstance(transform, v2.Compose) - - dataset_cls, data_entity_cls = fxt_dataset_and_data_entity_cls - dataset = dataset_cls( - dm_subset=fxt_mock_dm_subset, - transforms=transform, - mem_cache_img_max_size=None, - ) - - item = dataset[0] - assert isinstance(item, data_entity_cls) - - @pytest.fixture(params=["RGB", "BGR"]) - def fxt_image_color_channel(self, request) -> ImageColorChannel: - return ImageColorChannel(request.param) - - def test_image_info( - self, - fxt_config, - fxt_dataset_and_data_entity_cls, - fxt_mock_dm_subset, - fxt_image_color_channel, - ) -> None: - transform = TorchVisionTransformLib.generate(fxt_config) - assert isinstance(transform, v2.Compose) - - dataset_cls, data_entity_cls = fxt_dataset_and_data_entity_cls - dataset = dataset_cls( - dm_subset=fxt_mock_dm_subset, - transforms=transform, - mem_cache_img_max_size=None, - image_color_channel=fxt_image_color_channel, - ) - - item = dataset[0] - assert item.img_info.img_shape == item.image.shape[1:] - - if fxt_image_color_channel == ImageColorChannel.RGB: - r_pixel = 255.0 * (0.229 * item.image[0, 0, 0] + 0.485) - g_pixel = 255.0 * (0.224 * item.image[1, 0, 0] + 0.456) - b_pixel = 255.0 * (0.225 * item.image[2, 0, 0] + 0.406) - else: - b_pixel = 255.0 * (0.229 * item.image[0, 0, 0] + 0.485) - g_pixel = 255.0 * (0.224 * item.image[1, 0, 0] + 0.456) - r_pixel = 255.0 * (0.225 * item.image[2, 0, 0] + 0.406) - - assert torch.allclose(r_pixel, torch.tensor(2.0)) - assert torch.allclose(g_pixel, torch.tensor(1.0)) - assert torch.allclose(b_pixel, torch.tensor(0.0)) diff --git a/tests/unit/core/data/transform_libs/__init__.py b/tests/unit/core/data/transform_libs/__init__.py deleted file mode 100644 index 2e36e6836df..00000000000 --- a/tests/unit/core/data/transform_libs/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of data transforms.""" diff --git a/tests/unit/core/data/transform_libs/test_mmcv.py b/tests/unit/core/data/transform_libs/test_mmcv.py deleted file mode 100644 index 06ab7e79724..00000000000 --- a/tests/unit/core/data/transform_libs/test_mmcv.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of mmcv data transform.""" - -import numpy as np -import pytest -from mmcv.transforms.builder import TRANSFORMS -from otx.core.config.data import SubsetConfig -from otx.core.data.entity.base import ImageInfo, OTXDataEntity -from otx.core.data.transform_libs.mmcv import LoadImageFromFile, MMCVTransformLib -from otx.core.types.transformer_libs import TransformLibType - - -class TestLoadImageFromFile: - def test_transform(self) -> None: - transform = LoadImageFromFile() - data_entity = OTXDataEntity( - np.ndarray((224, 224, 3)), - ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - ) - out = transform.transform(data_entity) - assert out["img_shape"] == (224, 224) - assert out["ori_shape"] == (224, 224) - - -class TestMMCVTransformLib: - def test_get_builder(self) -> None: - assert MMCVTransformLib.get_builder() == TRANSFORMS - - def test_generate(self, mocker) -> None: - def mock_convert_func(cfg: dict) -> dict: - return cfg - - config = SubsetConfig( - batch_size=64, - subset_name="train", - transform_lib_type=TransformLibType.MMCV, - transforms=[{"type": "LoadImageFromFile"}, {"type": "Normalize", "mean": [0, 0, 0], "std": [1, 1, 1]}], - num_workers=2, - ) - - mocker.patch("otx.core.data.transform_libs.mmcv.convert_conf_to_mmconfig_dict", side_effect=mock_convert_func) - transforms = MMCVTransformLib.generate(config) - assert len(transforms) == 2 - assert np.all(transforms[1].mean == np.array([0.0, 0.0, 0.0])) - - config.transforms.pop(0) - with pytest.raises(RuntimeError): - transforms = MMCVTransformLib.generate(config) diff --git a/tests/unit/core/data/transform_libs/test_mmdet.py b/tests/unit/core/data/transform_libs/test_mmdet.py deleted file mode 100644 index 0815a0ef976..00000000000 --- a/tests/unit/core/data/transform_libs/test_mmdet.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of mmdet transforms.""" - -from __future__ import annotations - -import numpy as np -import pytest -import torch -from otx.core.config.data import SubsetConfig -from otx.core.data.entity.base import ImageInfo -from otx.core.data.entity.detection import DetDataEntity -from otx.core.data.entity.visual_prompting import VisualPromptingDataEntity -from otx.core.data.transform_libs.mmcv import LoadImageFromFile -from otx.core.data.transform_libs.mmdet import LoadAnnotations, MMDetTransformLib, PackDetInputs, PerturbBoundingBoxes -from otx.core.types.transformer_libs import TransformLibType -from torch import LongTensor -from torchvision import tv_tensors - - -class TestLoadAnnotations: - def test_transform(self) -> None: - data_entity = DetDataEntity( - tv_tensors.Image(torch.randn(3, 224, 224)), - ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - tv_tensors.BoundingBoxes(data=torch.Tensor([0, 0, 50, 50]), format="xywh", canvas_size=(224, 224)), - LongTensor([1]), - ) - - data_entity = LoadImageFromFile().transform(data_entity) - transform = LoadAnnotations() - results = transform.transform(data_entity) - assert np.all(results["gt_bboxes"] == np.array([[0.0, 0.0, 50.0, 50.0]])) - assert results["gt_bboxes_labels"] == np.array([1]) - assert results["gt_ignore_flags"] == np.array([False]) - - -class TestPackDetInputs: - @pytest.mark.parametrize( - ("data_entity", "with_point", "expected"), - [ - ( - DetDataEntity( - image=np.ndarray((224, 224, 3)), - img_info=ImageInfo(img_idx=0, img_shape=(224, 224), ori_shape=(224, 224)), - bboxes=tv_tensors.BoundingBoxes( - data=torch.Tensor([0, 0, 50, 50]), - format="xywh", - canvas_size=(224, 224), - ), - labels=LongTensor([1]), - ), - False, - torch.Size([3, 224, 224]), - ), - ( - VisualPromptingDataEntity( - image=np.ndarray((1024, 1024, 3)), - img_info=ImageInfo(img_idx=0, img_shape=(1024, 1024), ori_shape=(1024, 1024)), - bboxes=tv_tensors.BoundingBoxes( - data=torch.Tensor([[0, 0, 50, 50]]), - format=tv_tensors.BoundingBoxFormat.XYXY, - canvas_size=(1024, 1024), - ), - points=None, # TODO(sungchul): add point prompts in mmx # noqa: TD003 - masks=None, - labels=LongTensor([1]), - polygons=None, - ), - False, # TODO(sungchul): add point prompts in mmx # noqa: TD003 - torch.Size([3, 1024, 1024]), - ), - ], - ) - def test_transform( - self, - data_entity: DetDataEntity | VisualPromptingDataEntity, - with_point: bool, - expected: torch.Size, - ) -> None: - transform = PackDetInputs() - data_entity = LoadAnnotations(with_point=with_point).transform(LoadImageFromFile().transform(data_entity)) - - results = transform.transform(data_entity) - - assert results.image.shape == expected - - -class TestPerturbBoundingBoxes: - def test_transform(self) -> None: - transform = PerturbBoundingBoxes(offset=20) - inputs = { - "img_shape": (300, 300), - "gt_bboxes": np.array( - [ - [100, 100, 200, 200], # normal bbox - [0, 0, 299, 299], # can be out of size - [0, 0, 100, 100], # can be out of size - [100, 100, 299, 299], # can be out of size - ], - ), - } - - results = transform.transform(inputs) - - assert isinstance(results, dict) - assert results["gt_bboxes"].shape == inputs["gt_bboxes"].shape - assert np.all(results["gt_bboxes"][1] == inputs["gt_bboxes"][1]) # check if clipped - assert np.all(results["gt_bboxes"][2][:2] == inputs["gt_bboxes"][2][:2]) - assert np.all(results["gt_bboxes"][3][2:] == inputs["gt_bboxes"][3][2:]) - - -class TestMMDetTransformLib: - def test_generate(self, mocker) -> None: - def mock_convert_func(cfg: dict) -> dict: - return cfg - - mocker.patch("otx.core.data.transform_libs.mmcv.convert_conf_to_mmconfig_dict", side_effect=mock_convert_func) - config = SubsetConfig( - batch_size=64, - subset_name="train", - transform_lib_type=TransformLibType.MMCV, - transforms=[ - {"type": "LoadImageFromFile"}, - {"type": "LoadAnnotations"}, - {"type": "Normalize", "mean": [0, 0, 0], "std": [1, 1, 1]}, - {"type": "PackDetInputs"}, - ], - num_workers=2, - ) - - transforms = MMDetTransformLib.generate(config) - assert len(transforms) == 4 diff --git a/tests/unit/core/data/transform_libs/test_mmseg.py b/tests/unit/core/data/transform_libs/test_mmseg.py deleted file mode 100644 index df420569fa3..00000000000 --- a/tests/unit/core/data/transform_libs/test_mmseg.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Helper to support MMPretrain data transform functions.""" -from __future__ import annotations - -import pytest -from otx.core.data.entity.segmentation import SegDataEntity -from otx.core.data.transform_libs.mmcv import LoadImageFromFile -from otx.core.data.transform_libs.mmseg import LoadAnnotations, PackSegInputs - - -class TestLoadAnnotations: - def test_transform(self, fxt_seg_data_entity) -> None: - seg_data_entity: SegDataEntity = fxt_seg_data_entity[0] - with pytest.raises(RuntimeError): - new_results = LoadAnnotations().transform({}) - results = {"__otx__": seg_data_entity} - new_results = LoadAnnotations().transform(results) - assert isinstance(new_results, dict) - assert "seg_fields" in new_results - assert "gt_seg_map" in new_results["seg_fields"] - assert new_results["gt_seg_map"].shape == seg_data_entity.img_info.img_shape - - -class TestPackSegInputs: - def test_transform(self, fxt_seg_data_entity) -> None: - instance: SegDataEntity = fxt_seg_data_entity[0] - - transforms = [ - LoadImageFromFile(), - LoadAnnotations(), - PackSegInputs(), - ] - - for transform in transforms: - instance = transform.transform(instance) - - assert isinstance(instance, SegDataEntity) diff --git a/tests/unit/core/metrics/__init__.py b/tests/unit/core/metrics/__init__.py deleted file mode 100644 index 6a16273c024..00000000000 --- a/tests/unit/core/metrics/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/core/metrics/test_accuracy.py b/tests/unit/core/metrics/test_accuracy.py deleted file mode 100644 index 47c72b7f8b8..00000000000 --- a/tests/unit/core/metrics/test_accuracy.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Test of Module for OTX custom metrices.""" - -import pytest -import torch -from otx.core.data.dataset.base import LabelInfo -from otx.core.data.dataset.classification import HLabelInfo -from otx.core.metrics.accuracy import ( - HlabelAccuracy, - MixedHLabelAccuracy, - MulticlassAccuracywithLabelGroup, - MultilabelAccuracywithLabelGroup, -) - - -class TestAccuracy: - def test_multiclass_accuracy(self, fxt_multiclass_labelinfo: LabelInfo) -> None: - """Check whether accuracy is same with OTX1.x version.""" - preds = [ - torch.Tensor([0]), - torch.Tensor([0]), - torch.Tensor([1]), - torch.Tensor([1]), - torch.Tensor([2]), - torch.Tensor([2]), - ] - targets = [ - torch.Tensor([0]), - torch.Tensor([0]), - torch.Tensor([1]), - torch.Tensor([1]), - torch.Tensor([1]), - torch.Tensor([2]), - ] - metric = MulticlassAccuracywithLabelGroup(average="MICRO") - metric.label_info = fxt_multiclass_labelinfo - metric.update(preds, targets) - result = metric.compute() - acc = result["accuracy"] - assert round(acc.item(), 3) == 0.800 - - metric = MulticlassAccuracywithLabelGroup(average="MACRO") - metric.label_info = fxt_multiclass_labelinfo - metric.update(preds, targets) - result = metric.compute() - acc = result["accuracy"] - assert round(acc.item(), 3) == 0.792 - - def test_multilabel_accuracy(self, fxt_multilabel_labelinfo: LabelInfo) -> None: - """Check whether accuracy is same with OTX1.x version.""" - preds = [ - torch.Tensor([0.2, 0.8, 0.9]), - torch.Tensor([0.8, 0.7, 0.7]), - ] - targets = [ - torch.Tensor([0, 1, 1]), - torch.Tensor([0, 1, 0]), - ] - metric = MultilabelAccuracywithLabelGroup(average="MICRO") - metric.label_info = fxt_multilabel_labelinfo - metric.update(preds, targets) - result = metric.compute() - acc = result["accuracy"] - assert round(acc.item(), 3) == 0.667 - - def test_hlabel_accuracy(self, fxt_hlabel_multilabel_info: HLabelInfo) -> None: - """Check whether accuracy is same with OTX1.x version.""" - preds = [ - torch.Tensor([1, -1, 0, 0.2, 0.8, 0.9]), - torch.Tensor([1, 0, 0, 0.8, 0.7, 0.7]), - ] - targets = [ - torch.Tensor([1, -1, 0, 0, 1, 1]), - torch.Tensor([0, 0, 1, 0, 1, 0]), - ] - - metric = HlabelAccuracy(average="MICRO") - metric.label_info = fxt_hlabel_multilabel_info - metric.update(preds, targets) - result = metric.compute() - acc = result["accuracy"] - assert round(acc.item(), 3) == 0.636 - - -class TestMixedHLabelAccuracy: - @pytest.fixture() - def hlabel_accuracy(self) -> MixedHLabelAccuracy: - # You may need to adjust the parameters based on your actual use case - return MixedHLabelAccuracy( - num_multiclass_heads=2, - num_multilabel_classes=3, - head_logits_info={"head1": (0, 5), "head2": (5, 10)}, - threshold_multilabel=0.5, - ) - - def test_update_and_compute(self, hlabel_accuracy) -> None: - preds = torch.rand((10, 5)) - target = torch.randint(0, 2, (10, 5)) # Replace the dimensions with actual dimensions - - hlabel_accuracy.update(preds, target) - result = hlabel_accuracy.compute() - - assert isinstance(result, torch.Tensor) - - def test_multilabel_only(self) -> None: - # Test when only multilabel heads are present (should raise an exception) - with pytest.raises(ValueError, match="The number of multiclass heads should be larger than 0"): - MixedHLabelAccuracy( - num_multiclass_heads=0, - num_multilabel_classes=3, - head_logits_info={"head1": (0, 5), "head2": (5, 10)}, - threshold_multilabel=0.5, - ) diff --git a/tests/unit/core/metrics/test_fmeasure.py b/tests/unit/core/metrics/test_fmeasure.py deleted file mode 100644 index 0f364fa1654..00000000000 --- a/tests/unit/core/metrics/test_fmeasure.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Test of Module for OTX custom metrices.""" - -from __future__ import annotations - -import pytest -import torch -from otx.core.metrics.fmeasure import FMeasure - - -class TestFMeasure: - @pytest.fixture() - def fxt_preds(self) -> list[dict[str, torch.Tensor]]: - return [ - { - "boxes": torch.Tensor([[0.7, 0.6, 0.9, 0.6], [0.2, 0.5, 0.8, 0.6]]), - "labels": torch.IntTensor([0, 0]), - "scores": torch.Tensor([0.9, 0.8]), - }, - { - "boxes": torch.Tensor([[0.3, 0.4, 0.6, 0.6], [0.3, 0.3, 0.4, 0.5]]), - "labels": torch.IntTensor([0, 0]), - "scores": torch.Tensor([0.9, 0.8]), - }, - ] - - @pytest.fixture() - def fxt_targets(self) -> list[dict[str, torch.Tensor]]: - return [ - { - "boxes": torch.Tensor([[0.8, 0.6, 0.9, 0.7], [0.3, 0.5, 0.8, 0.7]]), - "labels": torch.IntTensor([0, 0]), - }, - { - "boxes": torch.Tensor([[0.4, 0.4, 0.6, 0.6], [0.3, 0.3, 0.4, 0.4]]), - "labels": torch.IntTensor([0, 0]), - }, - ] - - def test_fmeasure(self, fxt_preds, fxt_targets) -> None: - """Check whether f1 score is same with OTX1.x version.""" - metric = FMeasure(num_classes=1) - metric.update(fxt_preds, fxt_targets) - result = metric.compute() - assert result["f1-score"] == 0.5 - - def test_fmeasure_with_fixed_threshold(self, fxt_preds, fxt_targets) -> None: - """Check fmeasure can compute f1 score given confidence threshold.""" - metric = FMeasure(num_classes=1) - - metric.best_confidence_threshold = 0.85 - metric.update(fxt_preds, fxt_targets) - result = metric.compute() - assert result["f1-score"] == 0.3333333432674408 diff --git a/tests/unit/core/model/__init__.py b/tests/unit/core/model/__init__.py deleted file mode 100644 index e769b3dadd2..00000000000 --- a/tests/unit/core/model/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of OTX model.""" diff --git a/tests/unit/core/model/entity/__init__.py b/tests/unit/core/model/entity/__init__.py deleted file mode 100644 index 908e78c0d62..00000000000 --- a/tests/unit/core/model/entity/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of OTX model entity.""" diff --git a/tests/unit/core/model/entity/test_base.py b/tests/unit/core/model/entity/test_base.py deleted file mode 100644 index 9a96a36f45a..00000000000 --- a/tests/unit/core/model/entity/test_base.py +++ /dev/null @@ -1,73 +0,0 @@ -import numpy as np -import pytest -import torch -from openvino.model_api.models.utils import ClassificationResult -from otx.core.data.entity.base import OTXBatchDataEntity -from otx.core.model.entity.base import OTXModel, OVModel - - -class MockNNModule(torch.nn.Module): - def __init__(self, num_classes): - super().__init__() - self.backbone = torch.nn.Linear(3, 1024) - self.head = torch.nn.Linear(3, num_classes) - - -class TestOTXModel: - def test_smart_weight_loading(self, mocker) -> None: - mocker.patch.object(OTXModel, "_create_model", return_value=MockNNModule(2)) - prev_model = OTXModel(num_classes=2) - - mocker.patch.object(OTXModel, "_create_model", return_value=MockNNModule(3)) - current_model = OTXModel(num_classes=3) - current_model.classification_layers = ["model.head.weight", "model.head.bias"] - current_model.classification_layers = { - "model.head.weight": {"stride": 1, "num_extra_classes": 0}, - "model.head.bias": {"stride": 1, "num_extra_classes": 0}, - } - - prev_classes = ["car", "truck"] - current_classes = ["car", "bus", "truck"] - indices = torch.Tensor([0, 2]).to(torch.int32) - - current_model.register_load_state_dict_pre_hook(current_classes, prev_classes) - current_model.load_state_dict(prev_model.state_dict()) - - assert torch.all( - current_model.state_dict()["model.backbone.weight"] == prev_model.state_dict()["model.backbone.weight"], - ) - assert torch.all( - current_model.state_dict()["model.backbone.bias"] == prev_model.state_dict()["model.backbone.bias"], - ) - assert torch.all( - current_model.state_dict()["model.head.weight"].index_select(0, indices) - == prev_model.state_dict()["model.head.weight"], - ) - assert torch.all( - current_model.state_dict()["model.head.bias"].index_select(0, indices) - == prev_model.state_dict()["model.head.bias"], - ) - - -class TestOVModel: - @pytest.fixture() - def input_batch(self) -> OTXBatchDataEntity: - image = [torch.rand(3, 10, 10) for _ in range(3)] - return OTXBatchDataEntity(3, image, []) - - @pytest.fixture() - def model(self) -> OVModel: - return OVModel(num_classes=2, model_name="efficientnet-b0-pytorch", model_type="Classification") - - def test_customize_inputs(self, model, input_batch) -> None: - inputs = model._customize_inputs(input_batch) - assert isinstance(inputs, dict) - assert "inputs" in inputs - assert inputs["inputs"][1].shape == np.transpose(input_batch.images[1].numpy(), (1, 2, 0)).shape - - def test_forward(self, model, input_batch) -> None: - model._customize_outputs = lambda x, _: x - outputs = model.forward(input_batch) - assert isinstance(outputs, list) - assert len(outputs) == 3 - assert isinstance(outputs[2], ClassificationResult) diff --git a/tests/unit/core/model/entity/test_segmentation.py b/tests/unit/core/model/entity/test_segmentation.py deleted file mode 100644 index 288e09e4014..00000000000 --- a/tests/unit/core/model/entity/test_segmentation.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Unit tests for segmentation model entity.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest -import torch -from importlib_resources import files -from omegaconf import OmegaConf -from otx.core.model.entity.segmentation import MMSegCompatibleModel - -if TYPE_CHECKING: - from omegaconf.dictconfig import DictConfig - - -class TestOTXSegmentationModel: - @pytest.fixture() - def config(self) -> DictConfig: - cfg_path = files("otx") / "algo" / "segmentation" / "mmconfigs" / "segnext_t.yaml" - return OmegaConf.load(cfg_path) - - @pytest.fixture() - def model(self, config) -> MMSegCompatibleModel: - return MMSegCompatibleModel(num_classes=1, config=config) - - def test_create_model(self, model) -> None: - mmseg_model = model._create_model() - assert mmseg_model is not None - assert isinstance(mmseg_model, torch.nn.Module) - - def test_customize_inputs(self, model, fxt_seg_data_entity) -> None: - output_data = model._customize_inputs(fxt_seg_data_entity[2]) - assert output_data is not None - assert output_data["data_samples"][-1].metainfo["pad_shape"] == output_data["inputs"].shape[-2:] - assert ( - output_data["data_samples"][-1].metainfo["pad_shape"] - == output_data["data_samples"][-1].gt_sem_seg.data.shape[-2:] - ) - - def test_customize_outputs(self, model, fxt_seg_data_entity) -> None: - from mmengine.structures import PixelData - from mmseg.structures import SegDataSample - from otx.core.data.entity.base import OTXBatchLossEntity - from otx.core.data.entity.segmentation import SegBatchPredEntity - - data_sample = SegDataSample() - pred_segm_map = PixelData() - pred_segm_map.data = torch.randint(0, 2, (1, 4, 4)) - data_sample.pred_sem_seg = pred_segm_map - - output_loss = {"loss_segm": torch.rand(1, requires_grad=True), "acc": torch.rand(1), "some": "some"} - out = model._customize_outputs(output_loss, fxt_seg_data_entity[2]) - assert isinstance(out, OTXBatchLossEntity) - - model.training = False - out = model._customize_outputs([data_sample], fxt_seg_data_entity[2]) - assert isinstance(out, SegBatchPredEntity) diff --git a/tests/unit/core/model/entity/test_visual_prompting.py b/tests/unit/core/model/entity/test_visual_prompting.py deleted file mode 100644 index 7373b31fb11..00000000000 --- a/tests/unit/core/model/entity/test_visual_prompting.py +++ /dev/null @@ -1,620 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Unit tests for visual prompting model entity.""" - -from __future__ import annotations - -from pathlib import Path -from unittest.mock import Mock - -import numpy as np -import pytest -import torch -from otx.core.data.entity.visual_prompting import VisualPromptingBatchPredEntity -from otx.core.exporter.visual_prompting import OTXVisualPromptingModelExporter -from otx.core.model.entity.visual_prompting import ( - OTXVisualPromptingModel, - OVVisualPromptingModel, - OVZeroShotVisualPromptingModel, -) -from torchvision import tv_tensors - - -class TestOTXVisualPromptingModel: - @pytest.fixture() - def otx_visual_prompting_model(self, mocker) -> OTXVisualPromptingModel: - mocker.patch.object(OTXVisualPromptingModel, "_create_model") - return OTXVisualPromptingModel(num_classes=1) - - def test_exporter(self, otx_visual_prompting_model) -> None: - """Test _exporter.""" - assert isinstance(otx_visual_prompting_model._exporter, OTXVisualPromptingModelExporter) - - def test_export_parameters(self, otx_visual_prompting_model) -> None: - """Test _export_parameters.""" - otx_visual_prompting_model.model.image_size = 1024 - - export_parameters = otx_visual_prompting_model._export_parameters - - assert export_parameters["input_size"] == (1, 3, 1024, 1024) - assert export_parameters["resize_mode"] == "fit_to_window" - assert export_parameters["mean"] == (123.675, 116.28, 103.53) - assert export_parameters["std"] == (58.395, 57.12, 57.375) - - def test_optimization_config(self, otx_visual_prompting_model) -> None: - """Test _optimization_config.""" - optimization_config = otx_visual_prompting_model._optimization_config - - assert optimization_config == { - "model_type": "transformer", - "advanced_parameters": { - "activations_range_estimator_params": { - "min": { - "statistics_type": "QUANTILE", - "aggregator_type": "MIN", - "quantile_outlier_prob": "1e-4", - }, - "max": { - "statistics_type": "QUANTILE", - "aggregator_type": "MAX", - "quantile_outlier_prob": "1e-4", - }, - }, - }, - } - - -class TestOVVisualPromptingModel: - @pytest.fixture() - def set_ov_visual_prompting_model(self, mocker): - def ov_visual_prompting_model(for_create_model: bool = False) -> OVVisualPromptingModel: - if for_create_model: - mocker.patch("openvino.model_api.adapters.create_core") - mocker.patch("openvino.model_api.adapters.get_user_config") - mocker.patch("openvino.model_api.adapters.OpenvinoAdapter") - mocker.patch("openvino.model_api.models.Model.create_model") - else: - mocker.patch.object( - OVVisualPromptingModel, - "_create_model", - return_value={"image_encoder": Mock(), "decoder": Mock()}, - ) - return OVVisualPromptingModel(num_classes=0, model_name="exported_model_decoder.xml") - - return ov_visual_prompting_model - - def test_create_model(self, set_ov_visual_prompting_model) -> None: - """Test _create_model.""" - ov_visual_prompting_model = set_ov_visual_prompting_model(for_create_model=True) - ov_models = ov_visual_prompting_model._create_model() - - assert isinstance(ov_models, dict) - assert "image_encoder" in ov_models - assert "decoder" in ov_models - - def test_forward(self, mocker, set_ov_visual_prompting_model, fxt_vpm_data_entity) -> None: - """Test forward.""" - ov_visual_prompting_model = set_ov_visual_prompting_model() - mocker.patch.object( - ov_visual_prompting_model.model["image_encoder"], - "preprocess", - return_value=(np.zeros((1, 3, 1024, 1024)), {}), - ) - mocker.patch.object( - ov_visual_prompting_model.model["image_encoder"], - "infer_sync", - return_value={"image_embeddings": np.random.random((1, 256, 64, 64))}, - ) - mocker.patch.object( - ov_visual_prompting_model.model["decoder"], - "preprocess", - return_value=[ - { - "point_coords": np.array([1, 1]).reshape(-1, 1, 2), - "point_labels": np.array([1], dtype=np.float32).reshape(-1, 1), - "mask_input": np.zeros((1, 1, 256, 256), dtype=np.float32), - "has_mask_input": np.zeros((1, 1), dtype=np.float32), - "orig_size": np.array([1024, 1024], dtype=np.int64).reshape(-1, 2), - "label": 1, - }, - ], - ) - mocker.patch.object( - ov_visual_prompting_model.model["decoder"], - "infer_sync", - return_value={ - "iou_predictions": 0.0, - "upscaled_masks": np.zeros((1, 1, 1024, 1024), dtype=np.float32), - }, - ) - mocker.patch.object( - ov_visual_prompting_model.model["decoder"], - "postprocess", - return_value={ - "hard_prediction": np.zeros((1, 1, 1024, 1024), dtype=np.float32), - "soft_prediction": np.zeros((1, 1, 1024, 1024), dtype=np.float32), - "scores": np.zeros((1, 1), dtype=np.float32), - }, - ) - - results = ov_visual_prompting_model(fxt_vpm_data_entity[1]) - - assert isinstance(results, VisualPromptingBatchPredEntity) - assert isinstance(results.images, list) - assert isinstance(results.images[0], tv_tensors.Image) - assert isinstance(results.masks, list) - assert isinstance(results.masks[0], tv_tensors.Mask) - - def test_optimize(self, tmpdir, mocker, set_ov_visual_prompting_model) -> None: - """Test optimize.""" - mocker.patch("openvino.Core.read_model") - mocker.patch("openvino.save_model") - mocker.patch("nncf.quantize") - - ov_visual_prompting_model = set_ov_visual_prompting_model() - fake_data_module = Mock() - - results = ov_visual_prompting_model.optimize(tmpdir, fake_data_module) - - assert "image_encoder" in results - assert "decoder" in results - - -class TestOVZeroShotVisualPromptingModel: - @pytest.fixture() - def ov_zero_shot_visual_prompting_model(self, mocker) -> OVZeroShotVisualPromptingModel: - mocker.patch.object( - OVZeroShotVisualPromptingModel, - "_create_model", - return_value={"image_encoder": Mock(), "decoder": Mock()}, - ) - mocker.patch.object(OVZeroShotVisualPromptingModel, "initialize_reference_info") - return OVZeroShotVisualPromptingModel(num_classes=0, model_name="exported_model_decoder.xml") - - def test_learn(self, mocker, ov_zero_shot_visual_prompting_model, fxt_zero_shot_vpm_data_entity) -> None: - """Test learn.""" - ov_zero_shot_visual_prompting_model.reference_feats = np.zeros((0, 1, 256), dtype=np.float32) - ov_zero_shot_visual_prompting_model.used_indices = np.array([], dtype=np.int64) - ov_zero_shot_visual_prompting_model.model["decoder"].mask_threshold = 0.0 - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["image_encoder"], - "preprocess", - return_value=(np.zeros((1, 3, 1024, 1024)), {"original_shape": (1024, 1024)}), - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["image_encoder"], - "infer_sync", - return_value={"image_embeddings": np.random.random((1, 256, 64, 64))}, - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["decoder"], - "preprocess", - return_value=[ - { - "point_coords": np.array([1, 1]).reshape(-1, 1, 2), - "point_labels": np.array([1], dtype=np.float32).reshape(-1, 1), - "mask_input": np.zeros((1, 1, 256, 256), dtype=np.float32), - "has_mask_input": np.zeros((1, 1), dtype=np.float32), - "orig_size": np.array([1024, 1024], dtype=np.int64).reshape(-1, 2), - "label": np.array([1], dtype=np.int64), - }, - ], - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["decoder"], - "infer_sync", - return_value={ - "iou_predictions": np.array([[0.1, 0.3, 0.5, 0.7]]), - "upscaled_masks": np.random.randn(1, 4, 1024, 1024), # noqa: NPY002 - "low_res_masks": np.zeros((1, 4, 64, 64), dtype=np.float32), - }, - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["decoder"], - "postprocess", - return_value={ - "hard_prediction": np.zeros((1, 1, 1024, 1024), dtype=np.float32), - "soft_prediction": np.zeros((1, 1, 1024, 1024), dtype=np.float32), - "scores": np.zeros((1, 1), dtype=np.float32), - }, - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model, - "_generate_masked_features", - return_value=np.random.rand(1, 256), # noqa: NPY002 - ) - reference_info, ref_masks = ov_zero_shot_visual_prompting_model.learn( - inputs=fxt_zero_shot_vpm_data_entity[1], - reset_feat=True, - ) - - assert reference_info["reference_feats"].shape == torch.Size((2, 1, 256)) - assert 1 in reference_info["used_indices"] - assert ref_masks[0].shape == torch.Size((2, 1024, 1024)) - - def test_infer(self, mocker, ov_zero_shot_visual_prompting_model, fxt_zero_shot_vpm_data_entity) -> None: - """Test infer.""" - ov_zero_shot_visual_prompting_model.model["decoder"].mask_threshold = 0.0 - ov_zero_shot_visual_prompting_model.model["decoder"].output_blob_name = "upscaled_masks" - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["image_encoder"], - "preprocess", - return_value=(np.zeros((1, 3, 1024, 1024)), {"original_shape": (1024, 1024)}), - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["image_encoder"], - "infer_sync", - return_value={"image_embeddings": np.random.random((1, 256, 64, 64))}, - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["decoder"], - "preprocess", - return_value=[ - { - "point_coords": np.array([1, 1]).reshape(-1, 1, 2), - "point_labels": np.array([1], dtype=np.float32).reshape(-1, 1), - "mask_input": np.zeros((1, 1, 256, 256), dtype=np.float32), - "has_mask_input": np.zeros((1, 1), dtype=np.float32), - "orig_size": np.array([1024, 1024], dtype=np.int64).reshape(-1, 2), - "label": np.array([1], dtype=np.int64), - }, - ], - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["decoder"], - "infer_sync", - return_value={ - "iou_predictions": np.array([[0.1, 0.3, 0.5, 0.7]]), - "upscaled_masks": np.random.randn(1, 4, 1024, 1024), # noqa: NPY002 - "low_res_masks": np.zeros((1, 4, 64, 64), dtype=np.float32), - }, - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["decoder"], - "postprocess", - return_value={ - "hard_prediction": np.zeros((1, 1, 1024, 1024), dtype=np.float32), - "soft_prediction": np.zeros((1, 1, 1024, 1024), dtype=np.float32), - "scores": np.zeros((1, 1), dtype=np.float32), - }, - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model.model["decoder"], - "apply_coords", - return_value=np.array([[1, 1], [2, 2]]), - ) - mocker.patch.object( - ov_zero_shot_visual_prompting_model, - "_get_prompt_candidates", - return_value=({1: np.array([[1, 1, 0.5]])}, {1: np.array([[2, 2]])}), - ) - - reference_feats = torch.rand(2, 1, 256) - used_indices = np.array([1]) - - results = ov_zero_shot_visual_prompting_model.infer( - inputs=fxt_zero_shot_vpm_data_entity[1], - reference_feats=reference_feats, - used_indices=used_indices, - ) - - for predicted_masks, used_points in results: - for label, predicted_mask in predicted_masks.items(): - for pm, _ in zip(predicted_mask, used_points[label]): - assert pm.shape == (1024, 1024) - - def test_gather_prompts_with_labels(self, ov_zero_shot_visual_prompting_model) -> None: - """Test _gather_prompts_with_labels.""" - batch_prompts = [ - [ - {"bboxes": "bboxes", "label": 1}, - {"points": "points", "label": 2}, - ], - ] - - processed_prompts = ov_zero_shot_visual_prompting_model._gather_prompts_with_labels(batch_prompts) - - for prompts in processed_prompts: - for label, prompt in prompts.items(): - if label == 1: - assert "bboxes" in prompt[0] - else: - assert "points" in prompt[0] - assert prompt[0]["label"] == label - - def test_initialize_reference_info(self, ov_zero_shot_visual_prompting_model) -> None: - """Test initialize_reference_info.""" - ov_zero_shot_visual_prompting_model.reference_feats = np.zeros((0, 1, 256), dtype=np.float32) - ov_zero_shot_visual_prompting_model.used_indices = np.array([], dtype=np.int64) - - assert ov_zero_shot_visual_prompting_model.reference_feats.shape == (0, 1, 256) - assert ov_zero_shot_visual_prompting_model.used_indices.shape == (0,) - - @pytest.mark.parametrize("new_largest_label", [0, 3]) - def test_expand_reference_info(self, ov_zero_shot_visual_prompting_model, new_largest_label: int) -> None: - """Test expand_reference_info.""" - ov_zero_shot_visual_prompting_model.reference_feats = np.zeros((0, 1, 256)) - - ov_zero_shot_visual_prompting_model.expand_reference_info( - new_largest_label=new_largest_label, - ) - - assert len(ov_zero_shot_visual_prompting_model.reference_feats) == new_largest_label + 1 - - def test_generate_masked_features(self, ov_zero_shot_visual_prompting_model) -> None: - """Test _generate_masked_features.""" - feats = np.random.random((8, 8, 1)) - masks = np.zeros((16, 16), dtype=np.float32) - masks[4:12, 4:12] = 1.0 - - masked_feat = ov_zero_shot_visual_prompting_model._generate_masked_features( - feats=feats, - masks=masks, - threshold_mask=0.3, - image_size=16, - ) - - assert masked_feat.shape == (1, 1) - - def test_pad_to_square(self, ov_zero_shot_visual_prompting_model) -> None: - """Test _pad_to_square.""" - result = ov_zero_shot_visual_prompting_model._pad_to_square(x=np.ones((8, 8)), image_size=16) - - assert result[:8, :8].sum() == 8**2 - assert result[:8, 8:].sum() == 0 - assert result[8:, :8].sum() == 0 - assert result[8:, 8:].sum() == 0 - - def test_find_latest_reference_info(self, mocker, ov_zero_shot_visual_prompting_model) -> None: - """Test _find_latest_reference_info.""" - mocker.patch( - "otx.core.model.entity.visual_prompting.os.path.isdir", - return_value=True, - ) - - # there are some saved reference info - mocker.patch( - "otx.core.model.entity.visual_prompting.os.listdir", - return_value=["1", "2"], - ) - results = ov_zero_shot_visual_prompting_model._find_latest_reference_info(Path()) - assert results == "2" - - # there are no saved reference info - mocker.patch( - "otx.core.model.entity.visual_prompting.os.listdir", - return_value=[], - ) - results = ov_zero_shot_visual_prompting_model._find_latest_reference_info(Path()) - assert results is None - - def test_load_latest_reference_info(self, mocker, ov_zero_shot_visual_prompting_model) -> None: - """Test load_latest_reference_info.""" - ov_zero_shot_visual_prompting_model.model["decoder"].embed_dim = 256 - - # get previously saved reference info - mocker.patch.object(ov_zero_shot_visual_prompting_model, "_find_latest_reference_info", return_value="1") - mocker.patch( - "otx.core.model.entity.visual_prompting.pickle.load", - return_value={"reference_feats": np.zeros((1, 1, 256)), "used_indices": np.array([0])}, - ) - mocker.patch("otx.core.model.entity.visual_prompting.Path.open", return_value="Mocked data") - - ov_zero_shot_visual_prompting_model.load_latest_reference_info() - assert ov_zero_shot_visual_prompting_model.reference_feats.shape == (1, 1, 256) - assert ov_zero_shot_visual_prompting_model.used_indices.shape == (1,) - - # no saved reference info - mocker.patch.object(ov_zero_shot_visual_prompting_model, "_find_latest_reference_info", return_value=None) - - ov_zero_shot_visual_prompting_model.reference_feats = np.zeros((0, 1, 256), dtype=np.float32) - ov_zero_shot_visual_prompting_model.used_indices = np.array([], dtype=np.int64) - ov_zero_shot_visual_prompting_model.load_latest_reference_info() - - assert ov_zero_shot_visual_prompting_model.reference_feats.shape == (0, 1, 256) - assert ov_zero_shot_visual_prompting_model.used_indices.shape == (0,) - - @pytest.mark.parametrize( - "result_point_selection", - [np.array([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]]), np.array([[-1, -1, -1]])], - ) - def test_get_prompt_candidates( - self, - mocker, - ov_zero_shot_visual_prompting_model, - result_point_selection: np.ndarray, - ) -> None: - """Test get_prompt_candidates.""" - mocker.patch.object( - ov_zero_shot_visual_prompting_model, - "_point_selection", - return_value=(result_point_selection, torch.zeros(1, 2)), - ) - image_embeddings = np.ones((1, 4, 4, 4)) - reference_feats = np.random.random((1, 1, 4)) - used_indices = np.array([0]) - original_shape = np.array([3, 3], dtype=np.int64) - - total_points_scores, total_bg_coords = ov_zero_shot_visual_prompting_model._get_prompt_candidates( - image_embeddings=image_embeddings, - reference_feats=reference_feats, - used_indices=used_indices, - original_shape=original_shape, - ) - - assert total_points_scores[0].shape[0] == len(result_point_selection) - assert total_bg_coords[0].shape[0] == 1 - - @pytest.mark.parametrize( - ("mask_sim", "expected"), - [ - ( - np.arange(0.1, 1.0, 0.1).reshape(3, 3), - np.array([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]]), - ), - (np.zeros((3, 3)), None), - ], - ) - def test_point_selection( - self, - ov_zero_shot_visual_prompting_model, - mask_sim: np.ndarray, - expected: np.ndarray, - ) -> None: - """Test _point_selection.""" - points_scores, bg_coords = ov_zero_shot_visual_prompting_model._point_selection( - mask_sim=mask_sim, - original_shape=np.array([3, 3]), - threshold=np.array([[0.5]]), - num_bg_points=1, - ) - - if points_scores is not None: - assert np.allclose(points_scores, expected) - - def test_resize_to_original_shape(self, ov_zero_shot_visual_prompting_model) -> None: - """Test _resize_to_original_shape.""" - masks = np.random.random((8, 8)) - image_size = 6 - original_shape = np.array([8, 10], dtype=np.int64) - - resized_masks = ov_zero_shot_visual_prompting_model._resize_to_original_shape(masks, image_size, original_shape) - - assert isinstance(resized_masks, np.ndarray) - assert resized_masks.shape == (8, 10) - - def test_get_prepadded_size(self, ov_zero_shot_visual_prompting_model) -> None: - """Test _get_prepadded_size.""" - original_shape = np.array([8, 10], dtype=np.int64) - image_size = 6 - - prepadded_size = ov_zero_shot_visual_prompting_model._get_prepadded_size(original_shape, image_size) - - assert isinstance(prepadded_size, np.ndarray) - assert prepadded_size.dtype == np.int64 - assert prepadded_size.shape == (2,) - assert np.all(prepadded_size == np.array([5, 6], dtype=np.int64)) - - def test_inspect_overlapping_areas(self, mocker, ov_zero_shot_visual_prompting_model) -> None: - """Test _inspect_overlapping_areas.""" - predicted_masks = { - 0: [ - np.array( - [ - [1, 1, 0, 0, 0, 0], - [1, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - ], - ), - np.array( - [ - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 0], - [0, 0, 1, 1, 1, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - ], - ), - np.array( - [ - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 1, 1, 1, 0, 0], - [0, 1, 1, 1, 0, 0], - ], - ), - ], - 1: [ - np.array( - [ - [0, 0, 0, 1, 1, 0], - [0, 0, 0, 1, 1, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - ], - ), - np.array( - [ - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1], - [0, 0, 0, 0, 1, 1], - ], - ), - np.array( - [ - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 1, 1, 0, 0, 0], - [0, 1, 1, 0, 0, 0], - ], - ), - np.array( - [ - [1, 1, 0, 0, 0, 0], - [1, 1, 0, 0, 0, 0], - [1, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - ], - ), - ], - } - used_points = { - 0: [ - np.array([0, 0, 0.5]), # to be removed - np.array([2, 2, 0.5]), - np.array([1, 4, 0.5]), - ], - 1: [ - np.array([3, 0, 0.5]), - np.array([4, 4, 0.5]), - np.array([1, 4, 0.3]), # to be removed - np.array([0, 0, 0.7]), - ], - } - - ov_zero_shot_visual_prompting_model._inspect_overlapping_areas(predicted_masks, used_points, threshold_iou=0.5) - - assert len(predicted_masks[0]) == 2 - assert len(predicted_masks[1]) == 3 - assert all(np.array([2, 2, 0.5]) == used_points[0][0]) - assert all(np.array([0, 0, 0.7]) == used_points[1][2]) - - @pytest.mark.parametrize( - ("largest", "expected_scores", "expected_ind"), - [ - (True, np.array([[3, 2], [6, 5], [9, 8]]), np.array([[2, 1], [2, 1], [2, 1]])), - (False, np.array([[1, 2], [4, 5], [7, 8]]), np.array([[0, 1], [0, 1], [0, 1]])), - ], - ) - def test_topk_numpy( - self, - ov_zero_shot_visual_prompting_model, - largest: bool, - expected_scores: np.ndarray, - expected_ind: np.ndarray, - ) -> None: - """Test _topk_numpy.""" - x = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - k = 2 - axis = -1 - - scores, ind = ov_zero_shot_visual_prompting_model._topk_numpy(x, k, axis, largest) - - np.testing.assert_array_equal(scores, expected_scores) - np.testing.assert_array_equal(ind, expected_ind) diff --git a/tests/unit/core/model/module/__init__.py b/tests/unit/core/model/module/__init__.py deleted file mode 100644 index 8a33fb30887..00000000000 --- a/tests/unit/core/model/module/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests of OTX model module.""" diff --git a/tests/unit/core/model/module/test_base.py b/tests/unit/core/model/module/test_base.py deleted file mode 100644 index 9e294ddadb7..00000000000 --- a/tests/unit/core/model/module/test_base.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Unit tests for base model module.""" - -from __future__ import annotations - -from unittest.mock import MagicMock, create_autospec - -import pytest -from lightning.pytorch.cli import ReduceLROnPlateau -from lightning.pytorch.trainer import Trainer -from otx.algo.schedulers.warmup_schedulers import LinearWarmupScheduler -from otx.core.model.entity.base import OTXModel -from otx.core.model.module.base import OTXLitModule -from torch.optim import Optimizer - - -class TestOTXLitModule: - @pytest.fixture() - def mock_otx_model(self) -> OTXModel: - return create_autospec(OTXModel) - - @pytest.fixture() - def mock_optimizer(self) -> Optimizer: - optimizer = MagicMock(spec=Optimizer) - optimizer.step = MagicMock() - optimizer.keywords = {"lr": 0.01} - optimizer.param_groups = MagicMock() - - def optimizer_factory(*args, **kargs) -> Optimizer: # noqa: ARG001 - return optimizer - - return optimizer_factory - - @pytest.fixture() - def mock_scheduler(self) -> list[LinearWarmupScheduler | ReduceLROnPlateau]: - scheduler_object_1 = MagicMock() - warmup_scheduler = MagicMock(spec=LinearWarmupScheduler) - warmup_scheduler.num_warmup_steps = 10 - warmup_scheduler.interval = "step" - scheduler_object_1.return_value = warmup_scheduler - - scheduler_object_2 = MagicMock() - lr_scheduler = MagicMock(spec=ReduceLROnPlateau) - lr_scheduler.monitor = "val/loss" - scheduler_object_2.return_value = lr_scheduler - - return [scheduler_object_1, scheduler_object_2] - - def test_configure_optimizers(self, mock_otx_model, mock_optimizer, mock_scheduler) -> None: - module = OTXLitModule( - otx_model=mock_otx_model, - torch_compile=False, - optimizer=mock_optimizer, - scheduler=mock_scheduler, - metric=MagicMock(), - ) - - module.trainer = MagicMock(spec=Trainer) - module.trainer.check_val_every_n_epoch = 2 - - optimizers, lr_schedulers = module.configure_optimizers() - assert isinstance(optimizers[0], Optimizer) - assert isinstance(lr_schedulers[0]["scheduler"], LinearWarmupScheduler) - assert lr_schedulers[0]["scheduler"].num_warmup_steps == 10 - assert lr_schedulers[0]["interval"] == "step" - - assert "scheduler" in lr_schedulers[1] - assert "monitor" in lr_schedulers[1] diff --git a/tests/unit/core/model/module/test_detection.py b/tests/unit/core/model/module/test_detection.py deleted file mode 100644 index f694a2864f5..00000000000 --- a/tests/unit/core/model/module/test_detection.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Unit tests for detection model module.""" - -from __future__ import annotations - -from functools import partial -from unittest.mock import create_autospec - -import pytest -from lightning.pytorch.cli import ReduceLROnPlateau -from otx.algo.schedulers.warmup_schedulers import LinearWarmupScheduler -from otx.core.metrics.fmeasure import FMeasure -from otx.core.model.entity.detection import OTXDetectionModel -from otx.core.model.module.base import OTXLitModule -from otx.core.model.module.detection import OTXDetectionLitModule -from torch.optim import Optimizer - - -class TestOTXLitModule: - @pytest.fixture() - def mock_otx_model(self) -> OTXDetectionModel: - return create_autospec(OTXDetectionModel) - - @pytest.fixture() - def mock_optimizer(self) -> Optimizer: - return create_autospec(Optimizer) - - @pytest.fixture() - def mock_scheduler(self) -> list[LinearWarmupScheduler | ReduceLROnPlateau]: - return create_autospec([LinearWarmupScheduler, ReduceLROnPlateau]) - - def test_configure_metric_with_v1_ckpt( - self, - mock_otx_model, - mock_optimizer, - mock_scheduler, - mocker, - ) -> None: - mock_otx_model.test_meta_info = {} - module = OTXDetectionLitModule( - otx_model=mock_otx_model, - torch_compile=False, - optimizer=mock_optimizer, - scheduler=mock_scheduler, - metric=partial(FMeasure), - ) - - mock_v1_ckpt = { - "confidence_threshold": 0.35, - "state_dict": {}, - } - - mocker.patch.object(OTXLitModule, "load_state_dict", return_value=None) - module.load_state_dict(mock_v1_ckpt) - - assert module.test_meta_info["best_confidence_threshold"] == 0.35 - assert module.model.test_meta_info["best_confidence_threshold"] == 0.35 - - module.configure_metric() - assert module.metric.best_confidence_threshold == 0.35 - - def test_configure_metric_with_v2_ckpt( - self, - mock_otx_model, - mock_optimizer, - mock_scheduler, - mocker, - ) -> None: - mock_otx_model.test_meta_info = {} - module = OTXDetectionLitModule( - otx_model=mock_otx_model, - torch_compile=False, - optimizer=mock_optimizer, - scheduler=mock_scheduler, - metric=partial(FMeasure), - ) - - mock_v2_ckpt = { - "hyper_parameters": {"confidence_threshold": 0.35}, - "state_dict": {}, - } - - mocker.patch.object(OTXLitModule, "load_state_dict", return_value=None) - module.load_state_dict(mock_v2_ckpt) - - assert module.test_meta_info["best_confidence_threshold"] == 0.35 - assert module.model.test_meta_info["best_confidence_threshold"] == 0.35 - - module.configure_metric() - assert module.metric.best_confidence_threshold == 0.35 diff --git a/tests/unit/core/model/module/test_segmentation.py b/tests/unit/core/model/module/test_segmentation.py deleted file mode 100644 index edb40c697a4..00000000000 --- a/tests/unit/core/model/module/test_segmentation.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -"""Unit tests for segmentation model module.""" - -from __future__ import annotations - -from unittest.mock import MagicMock - -import pytest -import torch -from otx.core.data.entity.segmentation import SegBatchPredEntity -from otx.core.model.entity.segmentation import MMSegCompatibleModel -from otx.core.model.module.segmentation import OTXSegmentationLitModule -from torchmetrics.metric import Metric - - -class MockMetric(torch.nn.Module): - def update(*args, **kwargs) -> None: - pass - - -class MockModel(torch.nn.Module): - def __init__(self, input_dict): - self.input_dict = input_dict - - def __call__(self, *args, **kwargs) -> SegBatchPredEntity: - return SegBatchPredEntity(**self.input_dict, scores=[]) - - -class TestOTXSegmentationModule: - @pytest.fixture() - def fxt_model_ckpt(self) -> dict[str, torch.Tensor]: - return { - "model.model.backbone.1.weight": torch.randn(3, 10), - "model.model.backbone.1.bias": torch.randn(3, 10), - "model.model.head.1.weight": torch.randn(10, 2), - "model.model.head.1.bias": torch.randn(10, 2), - } - - @pytest.fixture() - def model(self, mocker, fxt_seg_data_entity) -> OTXSegmentationLitModule: - # define otx model - otx_model = mocker.MagicMock(spec=MMSegCompatibleModel) - otx_model.num_classes = 2 - # define lightning model - model = OTXSegmentationLitModule(otx_model, MagicMock, MagicMock, False) - model.model.return_value = fxt_seg_data_entity[1] - model.metric = mocker.MagicMock(spec=Metric) - - return model - - def test_validation_step(self, mocker, model, fxt_seg_data_entity) -> None: - mocker_update_loss = mocker.patch.object(model, "_convert_pred_entity_to_compute_metric") - model.validation_step(fxt_seg_data_entity[2], 0) - mocker_update_loss.assert_called_once() - - def test_test_metric(self, mocker, model, fxt_seg_data_entity) -> None: - mocker_update_loss = mocker.patch.object(model, "_convert_pred_entity_to_compute_metric") - model.test_step(fxt_seg_data_entity[2], 0) - mocker_update_loss.assert_called_once() - - def test_convert_pred_entity_to_compute_metric(self, model, fxt_seg_data_entity) -> None: - pred_entity = fxt_seg_data_entity[2] - out = model._convert_pred_entity_to_compute_metric(pred_entity, fxt_seg_data_entity[2]) - assert isinstance(out, list) - assert "preds" in out[-1] - assert "target" in out[-1] - assert out[-1]["preds"].sum() == out[-1]["target"].sum() diff --git a/tests/unit/core/ov/__init__.py b/tests/unit/core/ov/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/unit/core/ov/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/core/ov/graph/parsers/test_ov_graph_cls_parser.py b/tests/unit/core/ov/graph/parsers/test_ov_graph_cls_parser.py new file mode 100644 index 00000000000..37e0519a318 --- /dev/null +++ b/tests/unit/core/ov/graph/parsers/test_ov_graph_cls_parser.py @@ -0,0 +1,30 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.core.ov.graph import Graph +from otx.core.ov.graph.parsers.cls import cls_base_parser +from otx.core.ov.utils import load_ov_model +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def get_graph(name="mobilenet-v2-pytorch"): + model = load_ov_model(f"omz://{name}") + return Graph.from_ov(model) + + +@e2e_pytest_unit +def test_cls_base_parser(): + graph = get_graph() + assert { + "inputs": ["data"], + "outputs": ["/features/features#18/features#18#2/Clip"], + } == cls_base_parser(graph, "backbone") + assert { + "inputs": ["/GlobalAveragePool"], + "outputs": ["/Flatten"], + } == cls_base_parser(graph, "neck") + assert { + "inputs": ["/classifier/classifier#1/Gemm/WithoutBiases"], + "outputs": ["prob/sink_port_0"], + } == cls_base_parser(graph, "head") diff --git a/tests/unit/core/ov/graph/parsers/test_ov_graph_parser.py b/tests/unit/core/ov/graph/parsers/test_ov_graph_parser.py new file mode 100644 index 00000000000..8d26f8479cb --- /dev/null +++ b/tests/unit/core/ov/graph/parsers/test_ov_graph_parser.py @@ -0,0 +1,29 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.core.ov.graph import Graph +from otx.core.ov.graph.parsers.parser import parameter_parser, result_parser +from otx.core.ov.utils import load_ov_model +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def get_graph(name="mobilenet-v2-pytorch"): + model = load_ov_model(f"omz://{name}") + return Graph.from_ov(model) + + +@e2e_pytest_unit +def test_result_parser(): + graph = get_graph() + names = result_parser(graph) + names_ = [i.name for i in graph.get_nodes_by_types(["Result"])] + assert set(names) == set(names_) + + +@e2e_pytest_unit +def test_parameter_parser(): + graph = get_graph() + names = parameter_parser(graph) + names_ = [i.name for i in graph.get_nodes_by_types(["Parameter"])] + assert set(names) == set(names_) diff --git a/tests/unit/core/ov/graph/test_ov_graph_grapy.py b/tests/unit/core/ov/graph/test_ov_graph_grapy.py new file mode 100644 index 00000000000..c00743df343 --- /dev/null +++ b/tests/unit/core/ov/graph/test_ov_graph_grapy.py @@ -0,0 +1,183 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from copy import deepcopy +from random import shuffle + +import numpy as np +import openvino.runtime as ov +import pytest + +from otx.core.ov.graph.graph import Graph, SortedDict +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSortedDict: + @e2e_pytest_unit + def test(self): + instance = SortedDict("key") + orders = list("abcdefghijklmnopqrstuvwxyz") + cands = list("abcdefghijklmnopqrstuvwxyz") + shuffle(cands) + + for cand in cands: + instance[cand] = {"edge": {"key": ord(cand)}} + + idx = 0 + for key in instance: + assert key == orders[idx] + idx += 1 + + idx = len(orders) - 1 + for key in reversed(instance): + assert key == orders[idx] + idx -= 1 + + repr(instance.keys()) + idx = 0 + for key in instance.keys(): + assert key == orders[idx] + idx += 1 + + idx = len(orders) - 1 + for key in reversed(instance.keys()): + assert key == orders[idx] + idx -= 1 + + repr(instance.values()) + idx = 0 + for value in instance.values(): + assert value["edge"]["key"] == ord(orders[idx]) + idx += 1 + + idx = len(orders) - 1 + for value in reversed(instance.values()): + assert value["edge"]["key"] == ord(orders[idx]) + idx -= 1 + + repr(instance.values()) + idx = 0 + for key, value in instance.items(): + assert key == orders[idx] + assert value["edge"]["key"] == ord(orders[idx]) + idx += 1 + + idx = len(orders) - 1 + for key, value in reversed(instance.items()): + assert key == orders[idx] + assert value["edge"]["key"] == ord(orders[idx]) + idx -= 1 + + instance2 = deepcopy(instance) + idx = 0 + for key, value in instance2.items(): + assert key == orders[idx] + assert value["edge"]["key"] == ord(orders[idx]) + idx += 1 + + instance.pop("i") + assert "i" not in instance + assert len(instance) == len(orders) - 1 + + instance.clear() + assert len(instance) == 0 + + +class TestGraph: + @pytest.fixture(autouse=True) + def setup(self) -> None: + param = ov.opset10.parameter([1, 3, 64, 64], ov.Type.f32, name="in") + constant = ov.opset10.constant(np.array([103.0, 116.0, 123.0]).reshape(1, 3, 1, 1), ov.Type.f32) + node = ov.opset10.subtract(param, constant, "numpy") + constant = ov.opset10.constant(np.random.normal(size=(32, 3, 3, 3)), ov.Type.f32) + node = ov.opset10.convolution(node, constant, [2, 2], [1, 1], [1, 1], [1, 1], "explicit") + constant = ov.opset10.constant(np.random.normal(size=(1, 32, 1, 1)), ov.Type.f32) + node = ov.opset10.add(node, constant, "numpy") + node = ov.opset10.clamp(node, 0, 6) + result = ov.opset10.result(node, name="out") + ov_model = ov.Model([result], [param], "model") + + self.graph = Graph.from_ov(ov_model) + assert isinstance(self.graph, Graph) + + @e2e_pytest_unit + def test_get_edge_data(self): + nodes = [node for node in self.graph] + assert self.graph.get_edge_data(nodes[0], nodes[-1]) is None + assert self.graph.get_edge_data(nodes[0], nodes[2]) + + @e2e_pytest_unit + def test_remove_node(self): + node = self.graph.get_nodes_by_types(["Subtract"])[0] + predecessor = list(self.graph.predecessors(node))[0] + successor = list(self.graph.successors(node))[0] + self.graph.remove_node(node, keep_connect=True) + assert self.graph.get_edge_data(predecessor, successor) + + node = self.graph.get_nodes_by_types(["Convolution"])[0] + predecessor = list(self.graph.predecessors(node))[0] + successor = list(self.graph.successors(node))[0] + self.graph.remove_node(node, keep_connect=False) + assert self.graph.get_edge_data(predecessor, successor) is None + + @e2e_pytest_unit + def test_replace_node(self): + node = self.graph.get_nodes_by_types(["Subtract"])[0] + new_node = deepcopy(node) + predecessors = list(self.graph.predecessors(node)) + successors = list(self.graph.successors(node)) + self.graph.replace_node(node, new_node) + + assert node not in self.graph + assert new_node in self.graph + assert predecessors == list(self.graph.predecessors(new_node)) + assert successors == list(self.graph.successors(new_node)) + + @e2e_pytest_unit + def test_add_edge(self): + node = self.graph.get_nodes_by_types(["Subtract"])[0] + new_node = deepcopy(node) + predecessors = list(self.graph.predecessors(node)) + successors = list(self.graph.successors(node)) + self.graph.remove_node(node) + + for predecessor in predecessors: + assert self.graph.get_edge_data(predecessor, new_node) is None + self.graph.add_edge(predecessor, new_node) + assert self.graph.get_edge_data(predecessor, new_node) + + for successor in successors: + assert self.graph.get_edge_data(new_node, successor) is None + self.graph.add_edge(new_node, successor) + assert self.graph.get_edge_data(new_node, successor) + + assert new_node in self.graph + + @e2e_pytest_unit + def test_get_nodes_by_type_pattern(self): + node = self.graph.get_nodes_by_types(["Subtract"])[0] + founds = self.graph.get_nodes_by_type_pattern(["Subtract", "Clamp"], node) + for found in founds: + start, end = found + assert start == node + assert start.type == "Subtract" + assert end.type == "Clamp" + + @e2e_pytest_unit + def test_remove_normalize_nodes(self): + self.graph.remove_normalize_nodes() + assert len(self.graph._normalize_nodes) == 0 + + @e2e_pytest_unit + def test_topological_sort(self): + assert len(list(self.graph.topological_sort())) == len(self.graph) + + @e2e_pytest_unit + def test_clean_up(self): + nodes = self.graph.get_nodes_by_types(["Subtract"]) + self.graph.remove_node(nodes[0]) + n_nodes = len(self.graph) + self.graph.clean_up() + + assert n_nodes > len(self.graph) diff --git a/tests/unit/core/ov/graph/test_ov_graph_utils.py b/tests/unit/core/ov/graph/test_ov_graph_utils.py new file mode 100644 index 00000000000..9e3a865dfc4 --- /dev/null +++ b/tests/unit/core/ov/graph/test_ov_graph_utils.py @@ -0,0 +1,59 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +from otx.core.ov.graph.graph import Graph +from otx.core.ov.graph.utils import ( + get_constant_input_nodes, + handle_paired_batchnorm, + handle_reshape, +) +from otx.core.ov.utils import load_ov_model +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def get_graph(name="mobilenet-v2-pytorch"): + model = load_ov_model(f"omz://{name}") + return Graph.from_ov(model) + + +@e2e_pytest_unit +def test_get_constant_input_nodes(): + graph = get_graph() + node = graph.get_nodes_by_types(["Add"])[0] + nodes = get_constant_input_nodes(graph, node) + assert all([node.type == "Constant" for node in nodes]) + + +@e2e_pytest_unit +def test_handle_merging_into_batchnorm(): + # TODO: + pass + # graph = get_graph() + # n_nodes = len(graph) + # handle_merging_into_batchnorm(graph) + # + # assert graph.get_nodes_by_types(["BatchNormInference"]) + # assert n_nodes >= len(graph) + + +@e2e_pytest_unit +@pytest.mark.skip(reason="Updated models are not compatible with the paired batchnorm converter") +def test_handle_paired_batchnorm(): + graph = get_graph() + handle_paired_batchnorm(graph) + n_nodes = len(graph) + assert graph.get_nodes_by_types(["BatchNormInference"]) + + graph = get_graph() + handle_paired_batchnorm(graph, replace=True) + assert graph.get_nodes_by_types(["BatchNormInference"]) + + assert n_nodes > len(graph) + + +@e2e_pytest_unit +def test_handle_reshape(): + graph = get_graph("dla-34") + handle_reshape(graph) diff --git a/tests/unit/core/ov/models/__init__.py b/tests/unit/core/ov/models/__init__.py new file mode 100644 index 00000000000..2faffbe2b1f --- /dev/null +++ b/tests/unit/core/ov/models/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/tests/unit/core/ov/models/mmcls/__init__.py b/tests/unit/core/ov/models/mmcls/__init__.py new file mode 100644 index 00000000000..2faffbe2b1f --- /dev/null +++ b/tests/unit/core/ov/models/mmcls/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/tests/unit/core/ov/models/mmcls/backbones/test_ov_mmcls_mmov_backbone.py b/tests/unit/core/ov/models/mmcls/backbones/test_ov_mmcls_mmov_backbone.py new file mode 100644 index 00000000000..87ed5aae9de --- /dev/null +++ b/tests/unit/core/ov/models/mmcls/backbones/test_ov_mmcls_mmov_backbone.py @@ -0,0 +1,43 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.backbones.mmov_backbone import ( + MMOVBackbone, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.ov.models.mmcls.test_helpers import create_ov_model + + +class TestMMOVBackbone: + @pytest.fixture(autouse=True) + def setup(self): + ov_model = create_ov_model() + self.model = MMOVBackbone( + model_path_or_model=ov_model, + remove_normalize=True, + merge_bn=True, + paired_bn=True, + ) + + @e2e_pytest_unit + def test_init_weights(self): + self.model.init_weights() + + @e2e_pytest_unit + def test_forward(self): + assert self.model.inputs == self.model._inputs + assert self.model.outputs == self.model._outputs + assert self.model.features == self.model._feature_dict + assert self.model.input_shapes == self.model._input_shapes + assert self.model.output_shapes == self.model._output_shapes + + data = {} + for key, shape in self.model.input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data[key] = torch.randn(shape) + self.model.train() + self.model(list(data.values()), torch.tensor([0])) diff --git a/tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_cls_head.py b/tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_cls_head.py new file mode 100644 index 00000000000..ea25d9794ea --- /dev/null +++ b/tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_cls_head.py @@ -0,0 +1,31 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.heads.cls_head import ClsHead +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestClsHead: + @pytest.fixture(autouse=True) + def setup(self): + self.model = ClsHead( + do_squeeze=True, + ) + + @e2e_pytest_unit + def test_forward_train(self): + cls_score = torch.randn(10, 100) + gt_label = torch.ones(10, dtype=torch.int64) + output = self.model.forward_train(cls_score, gt_label) + assert "loss" in output + + @e2e_pytest_unit + def test_simple_test(self): + cls_score = torch.randn(10, 100) + outputs = self.model.simple_test(cls_score) + assert isinstance(outputs, list) + assert len(outputs) == 10 diff --git a/tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_conv_head.py b/tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_conv_head.py new file mode 100644 index 00000000000..a58d46f2d01 --- /dev/null +++ b/tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_conv_head.py @@ -0,0 +1,40 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.heads.conv_head import ( + ConvClsHead, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestConvClsHead: + @pytest.fixture(autouse=True) + def setup(self): + self.model = ConvClsHead( + num_classes=10, + in_channels=3, + ) + + @e2e_pytest_unit + def test_simple_test(self): + input = torch.randn(10, 3, 1, 1) + outputs = self.model.simple_test((input,), softmax=True) + assert isinstance(outputs, list) + assert len(outputs) == 10 + for output in outputs: + assert np.isclose(output.sum(), np.array(1.0)) + + outputs = self.model.simple_test((input,), post_process=False) + assert isinstance(outputs, torch.Tensor) + + @e2e_pytest_unit + def test_forward_train(self): + input = torch.randn(10, 3, 1, 1) + gt_label = torch.ones(10, dtype=torch.int64) + output = self.model.forward_train((input,), gt_label) + assert "loss" in output diff --git a/tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_mmcv_cls_head.py b/tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_mmcv_cls_head.py new file mode 100644 index 00000000000..c12e05642b6 --- /dev/null +++ b/tests/unit/core/ov/models/mmcls/heads/test_ov_mmcls_mmcv_cls_head.py @@ -0,0 +1,42 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.algorithms.classification.adapters.mmcls.models.heads.mmov_cls_head import ( + MMOVClsHead, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.ov.models.mmcls.test_helpers import create_ov_model + + +class TestMMOVClsHead: + @pytest.fixture(autouse=True) + def setup(self): + ov_model = create_ov_model() + self.model = MMOVClsHead( + model_path_or_model=ov_model, + ) + + @e2e_pytest_unit + def test_forward_train(self): + data = {} + for key, shape in self.model.model.input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data[key] = torch.randn(shape) + + output = self.model.forward_train(list(data.values()), torch.tensor([0])) + assert "loss" in output + + @e2e_pytest_unit + def test_simple_test(self): + data = {} + for key, shape in self.model.model.input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data[key] = torch.randn(shape) + + outputs = self.model.simple_test(list(data.values())) + assert isinstance(outputs, list) + assert len(outputs) == 1 diff --git a/tests/unit/core/ov/models/mmcls/necks/test_ov_mmcls_mmov_neck.py b/tests/unit/core/ov/models/mmcls/necks/test_ov_mmcls_mmov_neck.py new file mode 100644 index 00000000000..d6255891516 --- /dev/null +++ b/tests/unit/core/ov/models/mmcls/necks/test_ov_mmcls_mmov_neck.py @@ -0,0 +1,22 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.algorithms.classification.adapters.mmcls.models.necks import MMOVNeck +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from tests.unit.core.ov.models.mmcls.test_helpers import create_ov_model + + +class TestMMOVNeck: + @pytest.fixture(autouse=True) + def setup(self): + ov_model = create_ov_model() + self.model = MMOVNeck( + model_path_or_model=ov_model, + ) + + @e2e_pytest_unit + def test_parser(self): + pass diff --git a/tests/unit/core/ov/models/mmcls/test_helpers.py b/tests/unit/core/ov/models/mmcls/test_helpers.py new file mode 100644 index 00000000000..94a175143ce --- /dev/null +++ b/tests/unit/core/ov/models/mmcls/test_helpers.py @@ -0,0 +1,25 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import numpy as np +import openvino.runtime as ov + + +def create_ov_model(): + param = ov.opset10.parameter([1, 3, 32, 32], ov.Type.f32, name="in") + constant = ov.opset10.constant(np.array([103.0, 116.0, 123.0]).reshape(1, 3, 1, 1), ov.Type.f32) + node = ov.opset10.subtract(param, constant, "numpy") + constant = ov.opset10.constant(np.random.normal(size=(16, 3, 3, 3)), ov.Type.f32) + node = ov.opset10.convolution(node, constant, [2, 2], [1, 1], [1, 1], [1, 1], "explicit") + constant = ov.opset10.constant(np.random.normal(size=(1, 16, 1, 1)), ov.Type.f32) + node = ov.opset10.add(node, constant, "numpy") + node = ov.opset10.clamp(node, 0, 6) + node = ov.opset10.reduce_mean(node, [2, 3], False) + node = ov.opset10.reshape(node, [-1, 16], False) + constant = ov.opset10.constant(np.random.normal(size=(16, 10)), ov.Type.f32) + node = ov.opset10.matmul(node, constant, False, False) + result = ov.opset10.result(node, name="out") + ov_model = ov.Model([result], [param], "model") + return ov_model diff --git a/tests/unit/core/ov/models/test_ov_models_ov_model.py b/tests/unit/core/ov/models/test_ov_models_ov_model.py new file mode 100644 index 00000000000..f3bf7fe78ca --- /dev/null +++ b/tests/unit/core/ov/models/test_ov_models_ov_model.py @@ -0,0 +1,45 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import openvino.runtime as ov +import torch + +from otx.core.ov.models.ov_model import OVModel +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestOVModel: + @e2e_pytest_unit + def test(self): + + param = ov.opset10.parameter([1, 3, 64, 64], ov.Type.f32, name="in") + constant = ov.opset10.constant(np.array([103.0, 116.0, 123.0]).reshape(1, 3, 1, 1), ov.Type.f32) + node = ov.opset10.subtract(param, constant, "numpy") + constant = ov.opset10.constant(np.random.normal(size=(32, 3, 3, 3)), ov.Type.f32) + node = ov.opset10.convolution(node, constant, [2, 2], [1, 1], [1, 1], [1, 1], "explicit") + constant = ov.opset10.constant(np.random.normal(size=(1, 32, 1, 1)), ov.Type.f32) + node = ov.opset10.add(node, constant, "numpy") + node = ov.opset10.clamp(node, 0, 6) + result = ov.opset10.result(node, name="out") + ov_model = ov.Model([result], [param], "model") + + model = OVModel( + model_path_or_model=ov_model, + remove_normalize=True, + init_weight=True, + merge_bn=True, + paired_bn=True, + ) + assert model.inputs == model._inputs + assert model.outputs == model._outputs + assert model.features == model._feature_dict + assert model.input_shapes == model._input_shapes + assert model.output_shapes == model._output_shapes + + data = {} + for key, shape in model.input_shapes.items(): + shape = [1 if i == -1 else i for i in shape] + data[key] = torch.randn(shape) + model(**data) diff --git a/tests/unit/core/ov/ops/test_ov_ops_activations.py b/tests/unit/core/ov/ops/test_ov_ops_activations.py new file mode 100644 index 00000000000..a2c6e8f08f7 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_activations.py @@ -0,0 +1,263 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import math + +import pytest +import torch +from torch.nn import functional as F + +from otx.core.ov.ops.activations import ( + ClampV0, + EluV0, + ExpV0, + GeluV7, + HardSigmoidV0, + HSigmoidV5, + HSwishV4, + MishV4, + PReluV0, + ReluV0, + SeluV0, + SigmoidV0, + SoftMaxV0, + SoftMaxV1, + SwishV4, + TanhV0, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSoftMaxV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = SoftMaxV0("dummy", shape=self.shape, axis=-1) + output = op(self.input) + assert torch.equal(output, F.softmax(self.input, dim=op.attrs.axis)) + assert torch.equal(output >= 0, output <= 1) + assert torch.allclose(output.sum(-1), torch.tensor(1.0)) + + +class TestSoftMaxV1: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = SoftMaxV1("dummy", shape=self.shape, axis=-1) + output = op(self.input) + assert torch.equal(output, F.softmax(self.input, dim=op.attrs.axis)) + assert torch.equal(output >= 0, output <= 1) + assert torch.allclose(output.sum(-1), torch.tensor(1.0)) + + +class TestReluV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = ReluV0("dummy", shape=self.shape) + output = op(self.input) + assert torch.equal(output, F.relu(self.input)) + assert all((output >= 0).flatten()) + + +class TestSwishV4: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = SwishV4("dummy", shape=self.shape) + output = op(self.input, 1.0) + assert torch.equal(output, self.input * torch.sigmoid(self.input * 1.0)) + + +class TestSigmoidV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = SigmoidV0("dummy", shape=self.shape) + output = op(self.input) + assert torch.equal(output, torch.sigmoid(self.input)) + assert torch.equal(output >= 0, output <= 1) + + +class TestClampV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = ClampV0("dummy", shape=self.shape, min=-0.05, max=0.05) + output = op(self.input) + assert torch.equal(output, self.input.clamp(min=op.attrs.min, max=op.attrs.max)) + assert torch.equal(output >= op.attrs.min, output <= op.attrs.max) + + +class TestPReluV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = PReluV0("dummy", shape=self.shape) + output = op(self.input, torch.tensor(0.1)) + assert torch.equal(output, F.prelu(self.input, torch.tensor(0.1))) + + +class TestTanhV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = TanhV0("dummy", shape=self.shape) + output = op(self.input) + assert torch.equal(output, F.tanh(self.input)) + + +class TestEluV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = EluV0("dummy", shape=self.shape, alpha=0.1) + output = op(self.input) + assert torch.equal(output, F.elu(self.input, alpha=op.attrs.alpha)) + + +class TestSeluV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = SeluV0("dummy", shape=self.shape) + output = op(self.input, 0.1, 0.1) + assert torch.equal(output, 0.1 * F.elu(self.input, alpha=0.1)) + + +class TestMishV4: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = MishV4("dummy", shape=self.shape) + output = op(self.input) + # assert torch.equal(output, F.mish(self.input)) + assert torch.equal(output, self.input * F.tanh(F.softplus(self.input))) + + +class TestHSwishV4: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = HSwishV4("dummy", shape=self.shape) + output = op(self.input) + assert torch.equal(output, F.hardswish(self.input)) + + +class TestHSigmoidV5: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = HSigmoidV5("dummy", shape=self.shape) + output = op(self.input) + assert torch.equal(output, F.hardsigmoid(self.input)) + + +class TestExpV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = ExpV0("dummy", shape=self.shape) + output = op(self.input) + assert torch.equal(output, torch.exp(self.input)) + + +class TestHardSigmoidV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = HardSigmoidV0("dummy", shape=self.shape) + output = op(self.input, 0.1, 0.1) + assert torch.equal( + output, + torch.maximum( + torch.zeros_like(self.input), + torch.minimum(torch.ones_like(self.input), self.input * 0.1 + 0.1), + ), + ) + + +class TestGeluV7: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + with pytest.raises(ValueError): + GeluV7("dummy", shape=self.shape, approximation_mode="dummy") + + op = GeluV7("dummy", shape=self.shape) + output = op(self.input) + assert torch.equal(output, F.gelu(self.input)) + + op = GeluV7("dummy", shape=self.shape, approximation_mode="tanh") + output = op(self.input) + assert torch.equal( + output, + self.input + * 0.5 + * (1 + F.tanh(torch.sqrt(2 / torch.tensor(math.pi)) * (self.input + 0.044715 * self.input**3))), + ) diff --git a/tests/unit/core/ov/ops/test_ov_ops_arithmetics.py b/tests/unit/core/ov/ops/test_ov_ops_arithmetics.py new file mode 100644 index 00000000000..e477286114b --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_arithmetics.py @@ -0,0 +1,158 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.core.ov.ops.arithmetics import AddV1, DivideV1, MultiplyV1, SubtractV1, TanV0 +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMultiplyV1: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + + @e2e_pytest_unit + def test_forward(self): + op = MultiplyV1("dummy", shape=self.shape, auto_broadcast="none") + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape) + output = op(input_1, input_2) + assert torch.equal(output, input_1 * input_2) + + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape[1:]) + with pytest.raises(AssertionError): + output = op(input_1, input_2) + + op = MultiplyV1("dummy", shape=self.shape, auto_broadcast="numpy") + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape) + output = op(input_1, input_2) + assert torch.equal(output, input_1 * input_2) + + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape[1:]) + output = op(input_1, input_2) + assert torch.equal(output, input_1 * input_2) + + op = MultiplyV1("dummy", shape=self.shape, auto_broadcast="dummy") + with pytest.raises(NotImplementedError): + output = op(input_1, input_2) + + +class TestDivideV1: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + + @e2e_pytest_unit + def test_forward(self): + op = DivideV1("dummy", shape=self.shape, auto_broadcast="none") + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape) + output = op(input_1, input_2) + assert torch.equal(output, input_1 / input_2) + + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape[1:]) + with pytest.raises(AssertionError): + output = op(input_1, input_2) + + op = DivideV1("dummy", shape=self.shape, auto_broadcast="numpy") + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape) + output = op(input_1, input_2) + assert torch.equal(output, input_1 / input_2) + + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape[1:]) + output = op(input_1, input_2) + assert torch.equal(output, input_1 / input_2) + + op = DivideV1("dummy", shape=self.shape, auto_broadcast="dummy") + with pytest.raises(NotImplementedError): + output = op(input_1, input_2) + + +class TestAddV1: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + + @e2e_pytest_unit + def test_forward(self): + op = AddV1("dummy", shape=self.shape, auto_broadcast="none") + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape) + output = op(input_1, input_2) + assert torch.equal(output, input_1 + input_2) + + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape[1:]) + with pytest.raises(AssertionError): + output = op(input_1, input_2) + + op = AddV1("dummy", shape=self.shape, auto_broadcast="numpy") + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape) + output = op(input_1, input_2) + assert torch.equal(output, input_1 + input_2) + + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape[1:]) + output = op(input_1, input_2) + assert torch.equal(output, input_1 + input_2) + + op = AddV1("dummy", shape=self.shape, auto_broadcast="dummy") + with pytest.raises(NotImplementedError): + output = op(input_1, input_2) + + +class TestSubtractV1: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + + @e2e_pytest_unit + def test_forward(self): + op = SubtractV1("dummy", shape=self.shape, auto_broadcast="none") + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape) + output = op(input_1, input_2) + assert torch.equal(output, input_1 - input_2) + + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape[1:]) + with pytest.raises(AssertionError): + output = op(input_1, input_2) + + op = SubtractV1("dummy", shape=self.shape, auto_broadcast="numpy") + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape) + output = op(input_1, input_2) + assert torch.equal(output, input_1 - input_2) + + input_1 = torch.randn(self.shape) + input_2 = torch.randn(self.shape[1:]) + output = op(input_1, input_2) + assert torch.equal(output, input_1 - input_2) + + op = SubtractV1("dummy", shape=self.shape, auto_broadcast="dummy") + with pytest.raises(NotImplementedError): + output = op(input_1, input_2) + + +class TestTanV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = TanV0("dummy", shape=self.shape) + output = op(self.input) + assert torch.equal(output, torch.tan(self.input)) diff --git a/tests/unit/core/ov/ops/test_ov_ops_builder.py b/tests/unit/core/ov/ops/test_ov_ops_builder.py new file mode 100644 index 00000000000..eb6e6bc7bfb --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_builder.py @@ -0,0 +1,54 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +from dataclasses import dataclass + +import pytest + +from otx.core.ov.ops.builder import OperationRegistry +from otx.core.ov.ops.op import Attribute, Operation +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestOperationRegistry: + @e2e_pytest_unit + def test(self): + registry = OperationRegistry("dummy", add_name_as_attr=True) + OperationRegistry.REGISTERED_NAME_ATTR + + @dataclass + class DummyAttributeV1(Attribute): + pass + + class DummyV1(Operation[DummyAttributeV1]): + TYPE = "dummy" + VERSION = "opset1" + ATTRIBUTE_FACTORY = DummyAttributeV1 + + registry.register()(DummyV1) + assert getattr(DummyV1, OperationRegistry.REGISTERED_NAME_ATTR) == "DummyV1" + + with pytest.raises(KeyError): + registry.register("another_dummy")(DummyV1) + + @dataclass + class DummyAttributeV2(Attribute): + pass + + class DummyV2(Operation[DummyAttributeV2]): + TYPE = "dummy" + VERSION = "opset2" + ATTRIBUTE_FACTORY = DummyAttributeV2 + + registry.register()(DummyV2) + assert getattr(DummyV2, OperationRegistry.REGISTERED_NAME_ATTR) == "DummyV2" + + assert DummyV1 == registry.get_by_name("DummyV1") + assert DummyV1 == registry.get_by_type_version("dummy", "opset1") + assert DummyV2 == registry.get_by_type_version("dummy", "opset2") + + with pytest.raises(KeyError): + registry.get_by_type_version("dummy", "opset3") + with pytest.raises(KeyError): + registry.get_by_type_version("invalid", "opset1") diff --git a/tests/unit/core/ov/ops/test_ov_ops_convolutions.py b/tests/unit/core/ov/ops/test_ov_ops_convolutions.py new file mode 100644 index 00000000000..ec0a6c41500 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_convolutions.py @@ -0,0 +1,126 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch +from torch.nn import functional as F + +from otx.core.ov.ops.convolutions import ConvolutionV1, GroupConvolutionV1 +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestConvolutionV1: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 3, 256, 256) + self.input = torch.randn(self.shape) + self.weight = torch.randn((128, 3, 3, 3)) + + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + ConvolutionV1( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + dilations=[1, 1], + auto_pad="error", + ) + + @e2e_pytest_unit + def test_forward(self): + + op = ConvolutionV1( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + dilations=[1, 1], + auto_pad="valid", + ) + + with pytest.raises(NotImplementedError): + op(self.input, torch.randn((1, 1, 1, 1, 1, 1))) + + assert torch.equal( + op(self.input, self.weight), + F.conv2d(self.input, self.weight, None, op.attrs.strides, 0, op.attrs.dilations), + ) + + op = ConvolutionV1( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[1, 1], + pads_end=[1, 1], + dilations=[1, 1], + auto_pad="explicit", + ) + assert torch.equal( + op(self.input, self.weight), + F.conv2d(self.input, self.weight, None, op.attrs.strides, 1, op.attrs.dilations), + ) + + +class TestGroupConvolutionV1: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 3, 256, 256) + self.input = torch.randn(self.shape) + self.weight = torch.randn((3, 128, 1, 3, 3)) + + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + GroupConvolutionV1( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + dilations=[1, 1], + auto_pad="error", + ) + + @e2e_pytest_unit + def test_forward(self): + + op = GroupConvolutionV1( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + dilations=[1, 1], + auto_pad="valid", + ) + + with pytest.raises(NotImplementedError): + op(self.input, torch.randn((1, 1, 1, 1, 1, 1, 1))) + + n_groups = self.weight.shape[0] + weight = self.weight.view(-1, *self.weight.shape[2:]) + assert torch.equal( + op(self.input, self.weight), + F.conv2d(self.input, weight, None, op.attrs.strides, 0, op.attrs.dilations, n_groups), + ) + + op = GroupConvolutionV1( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[1, 1], + pads_end=[1, 1], + dilations=[1, 1], + auto_pad="explicit", + ) + n_groups = self.weight.shape[0] + weight = self.weight.view(-1, *self.weight.shape[2:]) + assert torch.equal( + op(self.input, self.weight), + F.conv2d(self.input, weight, None, op.attrs.strides, 1, op.attrs.dilations, n_groups), + ) diff --git a/tests/unit/core/ov/ops/test_ov_ops_generation.py b/tests/unit/core/ov/ops/test_ov_ops_generation.py new file mode 100644 index 00000000000..c8efe5d31de --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_generation.py @@ -0,0 +1,30 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.core.ov.ops.generation import RangeV4 +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestRangeV4: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 3, 256, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = RangeV4("dummy", shape=self.shape, output_type="i32") + assert torch.equal( + op(torch.tensor(0), torch.tensor(10), torch.tensor(1)), + torch.arange(0, 10, 1), + ) + + op = RangeV4("dummy", shape=self.shape, output_type="f32") + assert torch.equal( + op(torch.tensor(0), torch.tensor(10), torch.tensor(1)), + torch.arange(0, 10, 1, dtype=torch.float32), + ) diff --git a/tests/unit/core/ov/ops/test_ov_ops_image_processings.py b/tests/unit/core/ov/ops/test_ov_ops_image_processings.py new file mode 100644 index 00000000000..1c86c54f691 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_image_processings.py @@ -0,0 +1,98 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch +from torch.nn import functional as F + +from otx.core.ov.ops.image_processings import InterpolateV4 +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestInterpolateV4: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 3, 64, 64) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + + with pytest.raises(ValueError): + op = InterpolateV4( + "dummy", + shape=self.shape, + mode="error", + shape_calculation_mode="sizes", + ) + + with pytest.raises(ValueError): + op = InterpolateV4( + "dummy", + shape=self.shape, + mode="nearest", + shape_calculation_mode="error", + ) + + with pytest.raises(ValueError): + op = InterpolateV4( + "dummy", + shape=self.shape, + mode="nearest", + shape_calculation_mode="sizes", + coordinate_transformation_mode="error", + ) + + with pytest.raises(ValueError): + op = InterpolateV4( + "dummy", + shape=self.shape, + mode="nearest", + shape_calculation_mode="sizes", + nearest_mode="error", + ) + + op = InterpolateV4( + "dummy", + shape=self.shape, + mode="nearest", + shape_calculation_mode="sizes", + ) + assert torch.equal( + op(self.input, torch.tensor((4, 3, 128, 128)), torch.tensor((1, 1, 2, 2))), + F.interpolate(self.input, (128, 128), mode="nearest"), + ) + + op = InterpolateV4( + "dummy", + shape=self.shape, + mode="nearest", + shape_calculation_mode="scales", + ) + assert torch.equal( + op(self.input, torch.tensor((4, 3, 128, 128)), torch.tensor((1, 1, 2, 2))), + F.interpolate(self.input, scale_factor=(2, 2), mode="nearest"), + ) + + op = InterpolateV4( + "dummy", + shape=self.shape, + mode="linear", + shape_calculation_mode="sizes", + ) + assert torch.equal( + op(self.input, torch.tensor((4, 3, 128, 128)), torch.tensor((1, 1, 2, 2))), + F.interpolate(self.input, (128, 128), mode="bilinear", align_corners=False), + ) + + op = InterpolateV4( + "dummy", + shape=self.shape, + mode="cubic", + shape_calculation_mode="sizes", + ) + assert torch.equal( + op(self.input, torch.tensor((4, 3, 128, 128)), torch.tensor((1, 1, 2, 2))), + F.interpolate(self.input, (128, 128), mode="bicubic", align_corners=False), + ) diff --git a/tests/unit/core/ov/ops/test_ov_ops_infrastructures.py b/tests/unit/core/ov/ops/test_ov_ops_infrastructures.py new file mode 100644 index 00000000000..8eb09f1d0ef --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_infrastructures.py @@ -0,0 +1,109 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import numpy as np +import openvino.runtime as ov +import pytest +import torch + +from otx.core.ov.ops.infrastructures import ConstantV0, ParameterV0, ResultV0 +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestParameterV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 3, 256, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + ParameterV0("dummy", shape=(self.shape,), element_type="error") + + @e2e_pytest_unit + def test_forward(self): + op = ParameterV0("dummy", shape=(self.shape,)) + assert torch.equal(self.input, op(self.input)) + + op = ParameterV0("dummy", shape=(self.shape,), permute=(0, 2, 3, 1)) + assert torch.equal(self.input.permute(0, 2, 3, 1), op(self.input)) + + op = ParameterV0("dummy", shape=(self.shape,), verify_shape=True) + assert torch.equal(self.input, op(self.input)) + + op = ParameterV0("dummy", shape=((-1, 3, -1, -1),), verify_shape=True) + assert torch.equal(self.input, op(self.input)) + + with pytest.raises(AssertionError): + op(self.input.permute(0, 2, 3, 1)) + + @e2e_pytest_unit + def test_from_ov(self): + op_ov = ov.opset10.parameter([-1, 256, 256, 3], ov.Type.f32) + op_ov.set_layout(ov.Layout("NHWC")) + op = ParameterV0.from_ov(op_ov) + + assert isinstance(op, ParameterV0) + assert op.attrs.element_type == "f32" + assert op.attrs.layout == ("N", "C", "H", "W") + assert op.attrs.permute == (0, 2, 3, 1) + assert op.attrs.shape == ((-1, 3, 256, 256),) + + +class TestResultV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 3, 256, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = ResultV0("dummy", shape=(self.shape,)) + assert torch.equal(self.input, op(self.input)) + + +class TestConstantV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (512, 256) + self.data = torch.randn(self.shape) + + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(KeyError): + ConstantV0("dummy", shape=(self.shape,)) + + @e2e_pytest_unit + def test_forward(self): + op = ConstantV0("dummy", data=self.data, shape=(self.shape,), is_parameter=False) + assert isinstance(op.data, torch.Tensor) + assert torch.equal(self.data, op()) + + op = ConstantV0("dummy", data=self.data, shape=(self.shape,), is_parameter=True) + assert isinstance(op.data, torch.nn.parameter.Parameter) + assert torch.equal(self.data, op()) + + @e2e_pytest_unit + def test_from_ov(self): + op_ov = ov.opset10.constant(self.data.numpy().astype(np.uint64), ov.Type.u64) + op = ConstantV0.from_ov(op_ov) + assert isinstance(op, ConstantV0) + assert op.attrs.shape == self.shape + assert not op.attrs.is_parameter + assert isinstance(op.data, torch.Tensor) + + op_ov = ov.opset10.constant(self.data.numpy(), ov.Type.f32) + op = ConstantV0.from_ov(op_ov) + assert isinstance(op, ConstantV0) + assert op.attrs.shape == self.shape + assert not op.attrs.is_parameter + assert isinstance(op.data, torch.Tensor) + + op_ov = ov.opset10.constant(self.data.numpy(), ov.Type.f32) + data = ov.opset10.parameter([4, 512], ov.Type.f32) + ov.opset10.matmul(data, op_ov, False, False) + op = ConstantV0.from_ov(op_ov) + assert isinstance(op, ConstantV0) + assert op.attrs.shape == self.shape diff --git a/tests/unit/core/ov/ops/test_ov_ops_matmuls.py b/tests/unit/core/ov/ops/test_ov_ops_matmuls.py new file mode 100644 index 00000000000..e795aea7536 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_matmuls.py @@ -0,0 +1,54 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.core.ov.ops.matmuls import EinsumV7, MatMulV0 +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMatMulV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (10, 3, 5) + self.input_1 = torch.randn(10, 3, 4) + self.input_2 = torch.randn(10, 4, 5) + + @e2e_pytest_unit + def test_forward(self): + op = MatMulV0("dummy", shape=(self.shape,), transpose_a=False, transpose_b=False) + output = op(self.input_1, self.input_2) + assert output.shape == self.shape + assert torch.equal(output, torch.matmul(self.input_1, self.input_2)) + + op = MatMulV0("dummy", shape=(self.shape,), transpose_a=True, transpose_b=False) + output = op(self.input_1.permute(0, 2, 1), self.input_2) + assert output.shape == self.shape + assert torch.equal(output, torch.matmul(self.input_1, self.input_2)) + + op = MatMulV0("dummy", shape=(self.shape,), transpose_a=False, transpose_b=True) + output = op(self.input_1, self.input_2.permute(0, 2, 1)) + assert output.shape == self.shape + assert torch.equal(output, torch.matmul(self.input_1, self.input_2)) + + op = MatMulV0("dummy", shape=(self.shape,), transpose_a=True, transpose_b=True) + output = op(self.input_1.permute(0, 2, 1), self.input_2.permute(0, 2, 1)) + assert output.shape == self.shape + assert torch.equal(output, torch.matmul(self.input_1, self.input_2)) + + +class TestEinsumV7: + @e2e_pytest_unit + def test_forward(self): + input = torch.randn(4, 4) + op = EinsumV7("dummy", shape=(1,), equation="ii") + output = op(input) + assert torch.equal(output, torch.einsum("ii", input)) + + input_1 = torch.randn(3, 2, 5) + input_2 = torch.randn(3, 5, 4) + op = EinsumV7("dummy", shape=(1,), equation="bij,bjk->bik") + output = op(input_1, input_2) + assert torch.equal(output, torch.einsum("bij,bjk->bik", input_1, input_2)) diff --git a/tests/unit/core/ov/ops/test_ov_ops_module.py b/tests/unit/core/ov/ops/test_ov_ops_module.py new file mode 100644 index 00000000000..56e3942ada8 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_module.py @@ -0,0 +1,48 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.core.ov.ops.builder import OPS +from otx.core.ov.ops.modules.op_module import OperationModule +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestOperationModule: + @e2e_pytest_unit + def test(sefl): + + constant_cls = OPS.get_by_name("ConstantV0") + constant = constant_cls( + "weight", + data=torch.tensor([1.5]), + shape=(1,), + is_parameter=True, + ) + + multiply_cls = OPS.get_by_name("MultiplyV1") + multiply = multiply_cls("multiply", shape=(1,)) + + op = OperationModule(multiply, [None, constant]) + assert op.type == multiply.type + assert op.version == multiply.version + assert op.name == multiply.name + assert op.shape == multiply.shape + assert op.attrs == multiply.attrs + assert torch.equal(op(torch.tensor([1.0])), torch.tensor([1.5])) + assert torch.equal(op(input_0=torch.tensor([1.0])), torch.tensor([1.5])) + with pytest.raises(ValueError): + op(input_1=torch.tensor([1.0])) + + op = OperationModule(multiply, {"input_0": None, "input_1": constant}) + assert op.type == multiply.type + assert op.version == multiply.version + assert op.name == multiply.name + assert op.shape == multiply.shape + assert op.attrs == multiply.attrs + assert torch.equal(op(torch.tensor([1.0])), torch.tensor([1.5])) + assert torch.equal(op(input_0=torch.tensor([1.0])), torch.tensor([1.5])) + with pytest.raises(ValueError): + op(input_1=torch.tensor([1.0])) diff --git a/tests/unit/core/ov/ops/test_ov_ops_movements.py b/tests/unit/core/ov/ops/test_ov_ops_movements.py new file mode 100644 index 00000000000..ec5b0faeb04 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_movements.py @@ -0,0 +1,295 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.core.ov.ops.movements import ( + BroadcastV3, + ConcatV0, + GatherV0, + GatherV1, + PadV1, + ScatterNDUpdateV3, + ScatterUpdateV3, + ShuffleChannelsV0, + SplitV1, + StridedSliceV1, + TileV0, + TransposeV1, + VariadicSplitV1, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestPadV1: + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + PadV1("dummy", shape=(1,), pad_mode="error") + + @e2e_pytest_unit + def test_get_torch_pad_mode(self): + assert "constant" == PadV1.get_torch_pad_mode("constant") + assert "replicate" == PadV1.get_torch_pad_mode("edge") + assert "reflect" == PadV1.get_torch_pad_mode("reflect") + with pytest.raises(NotImplementedError): + PadV1.get_torch_pad_mode("symmetric") + with pytest.raises(NotImplementedError): + PadV1.get_torch_pad_mode("error") + + @e2e_pytest_unit + def test_get_torch_pad_dim(self): + assert [10, 10, 10, 10] == PadV1.get_torch_pad_dim([10, 10], [10, 10]) + assert [9, 4, 8, 3, 7, 2, 6, 1] == PadV1.get_torch_pad_dim(range(6, 10), range(0, 5)) + + @e2e_pytest_unit + def test_forward(self): + for mode in ("constant", "edge", "reflect"): + op = PadV1("dummy", shape=(1,), pad_mode=mode) + input = torch.empty(3, 3, 4, 2) + output = op(input, [1, 1], [1, 1]) + assert output.shape == (3, 3, 6, 4) + + output = op(input, [2, 2, 1], [1, 2, 1]) + assert output.shape == (3, 6, 8, 4) + + +class TestConcatV0: + @e2e_pytest_unit + def test_forward(self): + op = ConcatV0("dummy", shape=(1,), axis=1) + input_1 = torch.rand(4, 4, 32, 32) + input_2 = torch.rand(4, 8, 32, 32) + output = op(input_1, input_2) + ref = torch.cat((input_1, input_2), 1) + assert torch.equal(output, ref) + + +class TestTransposeV1: + @e2e_pytest_unit + def test_forward(self): + op = TransposeV1("dummy", shape=(1,)) + input = torch.randn(1, 2, 3, 4, 5) + + assert op(input, torch.tensor([])).shape == (5, 4, 3, 2, 1) + assert op(input, torch.tensor([0, 2, 1, 4, 3])).shape == (1, 3, 2, 5, 4) + + +class TestGatherV0: + @e2e_pytest_unit + def test_forward(self): + op = GatherV0("dummy", shape=(1,), batch_dims=0) + output = op(torch.tensor([1, 2, 3, 4, 5]), torch.tensor([0, 0, 4]), torch.tensor(0)) + ref = torch.tensor([1, 1, 5]) + assert torch.equal(output, ref) + + op = GatherV0("dummy", shape=(1,), batch_dims=0) + output = op(torch.tensor([1, 2, 3, 4, 5]), torch.tensor(4), torch.tensor(0)) + ref = torch.tensor(5) + assert torch.equal(output, ref) + + op = GatherV0("dummy", shape=(1,), batch_dims=-1) + output = op( + torch.tensor([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]), + torch.tensor([[0, 0, 4], [4, 0, 0]]), + torch.tensor(1), + ) + ref = torch.tensor([[1, 1, 5], [10, 6, 6]]) + assert torch.equal(output, ref) + + op = GatherV0("dummy", shape=(1,), batch_dims=1) + output = op( + torch.tensor([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]), + torch.tensor([[0, 0, 4], [4, 0, 0]]), + torch.tensor(1), + ) + ref = torch.tensor([[1, 1, 5], [10, 6, 6]]) + assert torch.equal(output, ref) + + op = GatherV0("dummy", shape=(1,), batch_dims=2) + output = op( + torch.tensor( + [ + [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]], + [[11, 12, 13, 14, 15], [16, 17, 18, 19, 20]], + ] + ), + torch.tensor([[[0, 0, 4], [4, 0, 0]], [[1, 2, 4], [4, 3, 2]]]), + torch.tensor(2), + ) + ref = torch.tensor([[[1, 1, 5], [10, 6, 6]], [[12, 13, 15], [20, 19, 18]]]) + assert torch.equal(output, ref) + + op = GatherV0("dummy", shape=(1,), batch_dims=1) + output = op( + torch.tensor( + [ + [ + [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16], + [17, 18, 19, 20], + ] + ], + [ + [ + [21, 22, 23, 24], + [25, 26, 27, 28], + [29, 30, 31, 32], + [33, 34, 35, 36], + [37, 38, 39, 40], + ] + ], + ] + ), + torch.tensor([[1, 2, 4], [4, 3, 2]]), + torch.tensor(2), + ) + ref = torch.tensor( + [ + [[[5, 6, 7, 8], [9, 10, 11, 12], [17, 18, 19, 20]]], + [[[37, 38, 39, 40], [33, 34, 35, 36], [29, 30, 31, 32]]], + ] + ) + assert torch.equal(output, ref) + + +class TestGatherV1: + @e2e_pytest_unit + def test_forward(self): + op = GatherV1("dummy", shape=(1,)) + input = torch.tensor([[1, 2], [3, 4]]) + output = op(input, torch.tensor([[0, 0], [1, 0]]), 1) + ref = torch.gather(input, 1, torch.tensor([[0, 0], [1, 0]])) + assert torch.equal(output, ref) + + +class TestStridedSliceV1: + @e2e_pytest_unit + def test_forward(self): + op = StridedSliceV1( + "dummy", + shape=(1,), + begin_mask=[0, 1, 1], + end_mask=[1, 1, 0], + new_axis_mask=[0, 0, 0], + shrink_axis_mask=[0, 0, 0], + ) + output = op( + torch.randn(2, 3, 4), + torch.tensor([1, 0, 0]), + torch.tensor([0, 0, 2]), + torch.tensor([1, 1, 1]), + ) + assert output.shape == (1, 3, 2) + + op = StridedSliceV1( + "dummy", + shape=(1,), + begin_mask=[0, 1, 1], + end_mask=[1, 1, 0], + new_axis_mask=[1, 0, 0], + shrink_axis_mask=[0, 0, 0], + ) + output = op( + torch.randn(2, 3, 4), + torch.tensor([0, 0, 0]), + torch.tensor([1, 0, 4]), + torch.tensor([1, 1, 1]), + ) + assert output.shape == (1, 2, 3, 4) + + +class TestSplitV1: + @e2e_pytest_unit + def test_forward(self): + op = SplitV1("dummy", shape=(1,), num_splits=2) + input = torch.randn(4, 8, 32) + outputs = op(input, 1) + refs = torch.split(input, 4, 1) + for output, ref in zip(outputs, refs): + assert torch.equal(output, ref) + + +class TestVariadicSplitV1: + @e2e_pytest_unit + def test_forward(self): + op = VariadicSplitV1("dummy", shape=(1,)) + outputs = op(torch.randn(6, 12, 10, 24), 0, torch.tensor([1, 2, 3])) + ref_shapes = ((1, 12, 10, 24), (2, 12, 10, 24), (3, 12, 10, 24)) + for output, ref_shape in zip(outputs, ref_shapes): + assert output.shape == ref_shape + + outputs = op(torch.randn(6, 12, 10, 24), 0, torch.tensor([-1, 2])) + ref_shapes = ((4, 12, 10, 24), (2, 12, 10, 24)) + for output, ref_shape in zip(outputs, ref_shapes): + assert output.shape == ref_shape + + +class TestShuffleChannelsV0: + @e2e_pytest_unit + def test_forward(self): + op = ShuffleChannelsV0("dummy", shape=(1,)) + input = torch.randn(4, 8, 32, 32) + output = op(input) + assert torch.equal(output, input) + + op = ShuffleChannelsV0("dummy", shape=(1,), group=2) + input = torch.randn(4, 8, 32, 32) + output = op(input) + assert not torch.equal(output, input) + + +class TestBroadcastV3: + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + BroadcastV3("dummy", shape=(1,), mode="error") + + @e2e_pytest_unit + def test_forward(self): + op = BroadcastV3("dummy", shape=(1,), mode="numpy") + output = op(torch.randn(16, 1, 1), torch.tensor([1, 16, 50, 50])) + assert output.shape == (1, 16, 50, 50) + + op = BroadcastV3("dummy", shape=(1,), mode="explicit") + output = op(torch.randn(16), torch.tensor([1, 16, 50, 50]), torch.tensor([1])) + assert output.shape == (1, 16, 50, 50) + + +class TestScatterNDUpdateV3: + @e2e_pytest_unit + def test_forward(self): + ScatterNDUpdateV3("dummy", shape=(1,)) + # TODO + pass + + +class TestScatterUpdateV3: + @e2e_pytest_unit + def test_forward(self): + op = ScatterUpdateV3("dummy", shape=(1,)) + updates = torch.arange(1, 11).reshape((2, 5)) + input = torch.zeros(3, 5, dtype=updates.dtype) + indices = torch.tensor([[0, 1, 2, 0]]) + + output = op(input, indices, updates, torch.tensor(0)) + ref = torch.tensor([[1, 0, 0, 4, 0], [0, 2, 0, 0, 0], [0, 0, 3, 0, 0]]) + assert torch.equal(output, ref) + + +class TestTileV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 3, 256, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_forward(self): + op = TileV0("dummy", shape=(self.shape,)) + output = op(self.input, torch.tensor([2, 2, 2, 2])) + assert torch.equal(output, torch.tile(self.input, [2, 2, 2, 2])) diff --git a/tests/unit/core/ov/ops/test_ov_ops_normalizations.py b/tests/unit/core/ov/ops/test_ov_ops_normalizations.py new file mode 100644 index 00000000000..268b12aa7c9 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_normalizations.py @@ -0,0 +1,149 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch +from torch.nn import functional as F + +from otx.core.ov.ops.normalizations import ( + MVNV6, + BatchNormalizationV0, + LocalResponseNormalizationV0, + NormalizeL2V0, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestBatchNormalizationV0: + @e2e_pytest_unit + def test_forward(self): + op = BatchNormalizationV0("dummy", shape=(1,), epsilon=1e-05, max_init_iter=2) + op.eval() + input = torch.randn(8, 128, 256, 256) + gamma = torch.randn(128) + beta = torch.randn(128) + mean = torch.zeros(128) + variance = torch.ones(128) + + output = op(input, gamma, beta, mean, variance) + ref = F.batch_norm(input, mean, variance, gamma, beta, eps=op.attrs.epsilon) + assert torch.equal(output, ref) + + op.train() + outputs = [] + for _ in range(op.attrs.max_init_iter + 1): + outputs.append(op(input, gamma, beta, mean, variance)) + assert torch.equal(outputs[0], outputs[1]) + assert not torch.equal(outputs[1], outputs[2]) + + +class TestLocalResponseNormalizationV0: + @e2e_pytest_unit + def test_forward(self): + op = LocalResponseNormalizationV0( + "dummy", + shape=(1,), + alpha=0.0001, + beta=0.75, + bias=1.0, + size=2, + ) + input = torch.randn(6, 12, 10, 24) + output = op(input, torch.tensor([1])) + ref = F.local_response_norm(input, size=2, alpha=0.0001, beta=0.75, k=1.0) + assert torch.equal(output, ref) + + +class TestNormalizeL2V0: + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + NormalizeL2V0("dummy", shape=(1,), eps=0.01, eps_mode="error") + + @e2e_pytest_unit + def test_forward(self): + op = NormalizeL2V0("dummy", shape=(1,), eps=0.01, eps_mode="add") + input = torch.rand(6, 12, 10, 24) + + output = op(input, torch.tensor([])) + ref = input / (input + op.attrs.eps).sqrt() + assert torch.equal(output, ref) + + output = op(input, torch.tensor([1])) + ref = input / (input.pow(2).sum(1, keepdim=True) + op.attrs.eps).sqrt() + assert torch.equal(output, ref) + + output = op(input, torch.tensor([1, 2, 3])) + ref = input / (input.pow(2).sum([1, 2, 3], keepdim=True) + op.attrs.eps).sqrt() + assert torch.equal(output, ref) + + output = op(input, torch.tensor([-2, -3])) + ref = input / (input.pow(2).sum([-2, -3], keepdim=True) + op.attrs.eps).sqrt() + assert torch.equal(output, ref) + + op = NormalizeL2V0("dummy", shape=(1,), eps=0.01, eps_mode="max") + input = torch.rand(6, 12, 10, 24) + + output = op(input, torch.tensor([])) + ref = input / torch.clamp(input, max=op.attrs.eps).sqrt() + assert torch.equal(output, ref) + + output = op(input, torch.tensor([1])) + ref = input / torch.clamp(input.pow(2).sum(1, keepdim=True), max=op.attrs.eps).sqrt() + assert torch.equal(output, ref) + + output = op(input, torch.tensor([1, 2, 3])) + ref = input / torch.clamp(input.pow(2).sum([1, 2, 3], keepdim=True), max=op.attrs.eps).sqrt() + assert torch.equal(output, ref) + + output = op(input, torch.tensor([-2, -3])) + ref = input / torch.clamp(input.pow(2).sum([-2, -3], keepdim=True), max=op.attrs.eps).sqrt() + assert torch.equal(output, ref) + + +class TestMVNV6: + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + MVNV6("dummy", shape=(1,), normalize_variance=True, eps=0.01, eps_mode="error") + + @e2e_pytest_unit + def test_forward(self): + op = MVNV6( + "dummy", + shape=(1,), + normalize_variance=False, + eps=0.01, + eps_mode="INSIDE_SQRT", + ) + input = torch.randn(6, 12, 10, 24) + output = op(input, torch.tensor([1, 2])) + ref = input - input.mean([1, 2], keepdim=True) + assert torch.equal(output, ref) + + op = MVNV6( + "dummy", + shape=(1,), + normalize_variance=True, + eps=0.01, + eps_mode="INSIDE_SQRT", + ) + input = torch.randn(6, 12, 10, 24) + output = op(input, torch.tensor([1, 2])) + ref = input - input.mean([1, 2], keepdim=True) + ref = ref / torch.sqrt(torch.square(ref).mean([1, 2], keepdim=True) + 0.01) + assert torch.equal(output, ref) + + op = MVNV6( + "dummy", + shape=(1,), + normalize_variance=True, + eps=0.01, + eps_mode="OUTSIDE_SQRT", + ) + input = torch.randn(6, 12, 10, 24) + output = op(input, torch.tensor([1, 2])) + ref = input - input.mean([1, 2], keepdim=True) + ref = ref / (torch.sqrt(torch.square(ref).mean([1, 2], keepdim=True)) + 0.01) + assert torch.equal(output, ref) diff --git a/tests/unit/core/ov/ops/test_ov_ops_object_detections.py b/tests/unit/core/ov/ops/test_ov_ops_object_detections.py new file mode 100644 index 00000000000..76315d67d61 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_object_detections.py @@ -0,0 +1,133 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest + +from otx.core.ov.ops.object_detections import ( + DetectionOutputV0, + PriorBoxClusteredV0, + PriorBoxV0, + ProposalV4, + RegionYoloV0, + ROIPoolingV0, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestProposalV4: + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + ProposalV4( + "dummy", + shape=(1,), + base_size=1, + pre_nms_topn=1, + post_nms_topn=1, + nms_thresh=0.1, + feat_stride=1, + min_size=1, + ratio=[1.0], + scale=[1.0], + framework="error", + ) + + @e2e_pytest_unit + def test_forward(self): + op = ProposalV4( + "dummy", + shape=(1,), + base_size=1, + pre_nms_topn=1, + post_nms_topn=1, + nms_thresh=0.1, + feat_stride=1, + min_size=1, + ratio=[1.0], + scale=[1.0], + ) + with pytest.raises(NotImplementedError): + op("dummy", "dummy", "dummy") + + +class TestROIPoolingV0: + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + ROIPoolingV0( + "dummy", + shape=(1,), + pooled_h=1, + pooled_w=1, + spatial_scale=1.0, + method="error", + ) + + @e2e_pytest_unit + def test_forward(self): + op = ROIPoolingV0( + "dummy", + shape=(1,), + pooled_h=1, + pooled_w=1, + spatial_scale=1.0, + ) + with pytest.raises(NotImplementedError): + op("dummy", "dummy") + + +class TestDetectionOutputV0: + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + DetectionOutputV0( + "dummy", + shape=(1,), + keep_top_k=[1], + nms_threshold=0.1, + code_type="error", + ) + + @e2e_pytest_unit + def test_forward(self): + op = DetectionOutputV0( + "dummy", + shape=(1,), + keep_top_k=[1], + nms_threshold=0.1, + ) + with pytest.raises(NotImplementedError): + op("dummy", "dummy", "dummy") + + +class TestRegionYoloV0: + @e2e_pytest_unit + def test_forward(self): + op = RegionYoloV0( + "dummy", + shape=(1,), + axis=1, + coords=1, + classes=1, + end_axis=1, + num=1, + ) + with pytest.raises(NotImplementedError): + op("dummy") + + +class TestPriorBoxV0: + @e2e_pytest_unit + def test_forward(self): + op = PriorBoxV0("dummy", shape=(1,), offset=0.1) + with pytest.raises(NotImplementedError): + op("dummy", "dummy") + + +class TestPriorBoxClusteredV0: + @e2e_pytest_unit + def test_forward(self): + op = PriorBoxClusteredV0("dummy", shape=(1,), offset=0.1) + with pytest.raises(NotImplementedError): + op("dummy", "dummy") diff --git a/tests/unit/core/ov/ops/test_ov_ops_op.py b/tests/unit/core/ov/ops/test_ov_ops_op.py new file mode 100644 index 00000000000..d4e9557503c --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_op.py @@ -0,0 +1,24 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import openvino.runtime as ov + +from otx.core.ov.ops.arithmetics import MultiplyV1 +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestOperation: + @e2e_pytest_unit + def test(self): + data = ov.opset10.parameter([3, 1, 2], ov.Type.f32) + mul_constant = ov.opset10.constant([1.5], ov.Type.f32) + mul = ov.opset10.multiply(data, mul_constant) + op = MultiplyV1.from_ov(mul) + assert isinstance(op, MultiplyV1) + assert op.type == MultiplyV1.TYPE + assert op.version == MultiplyV1.VERSION + assert op.name == op._name + assert op.attrs == op._attrs + assert op.shape == op.attrs.shape + assert isinstance(repr(op), str) diff --git a/tests/unit/core/ov/ops/test_ov_ops_poolings.py b/tests/unit/core/ov/ops/test_ov_ops_poolings.py new file mode 100644 index 00000000000..192b6218cc5 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_poolings.py @@ -0,0 +1,147 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch +from torch.nn import functional as F + +from otx.core.ov.ops.poolings import AvgPoolV1, MaxPoolV0 +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestMaxPoolV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 3, 256, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + MaxPoolV0( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + kernel=[8, 8], + rounding_type="error", + ) + + with pytest.raises(ValueError): + MaxPoolV0( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + kernel=[8, 8], + auto_pad="error", + ) + + with pytest.raises(ValueError): + MaxPoolV0( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + kernel=[8, 8], + index_element_type="error", + ) + + with pytest.raises(NotImplementedError): + MaxPoolV0( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + kernel=[8, 8], + axis=1, + ) + + @e2e_pytest_unit + def test_forward(self): + op = MaxPoolV0( + "dummy", + shape=self.shape, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + kernel=[8, 8], + ) + + with pytest.raises(NotImplementedError): + op(torch.randn(1, 1, 1, 1, 1, 1)) + + output = op(self.input) + ref = F.max_pool2d( + self.input, + op.attrs.kernel, + op.attrs.strides, + dilation=op.attrs.dilations, + ceil_mode=op.attrs.rounding_type == "ceil", + return_indices=True, + ) + for i, j in zip(output, ref): + assert torch.equal(i, j) + + +class TestAvgPoolV1: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 3, 256, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + AvgPoolV1( + "dummy", + shape=self.shape, + exclude_pad=False, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + kernel=[8, 8], + rounding_type="error", + ) + + with pytest.raises(ValueError): + AvgPoolV1( + "dummy", + shape=self.shape, + exclude_pad=False, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + kernel=[8, 8], + auto_pad="error", + ) + + @e2e_pytest_unit + def test_forward(self): + op = AvgPoolV1( + "dummy", + shape=self.shape, + exclude_pad=False, + strides=[1, 1], + pads_begin=[0, 0], + pads_end=[0, 0], + kernel=[8, 8], + ) + + with pytest.raises(NotImplementedError): + op(torch.randn(1, 1, 1, 1, 1, 1)) + + output = op(self.input) + ref = F.avg_pool2d( + self.input, + op.attrs.kernel, + op.attrs.strides, + ceil_mode=op.attrs.rounding_type == "ceil", + count_include_pad=not op.attrs.exclude_pad, + ) + assert torch.equal(output, ref) diff --git a/tests/unit/core/ov/ops/test_ov_ops_reductions.py b/tests/unit/core/ov/ops/test_ov_ops_reductions.py new file mode 100644 index 00000000000..34419a695e0 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_reductions.py @@ -0,0 +1,132 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import torch + +from otx.core.ov.ops.reductions import ( + ReduceMeanV1, + ReduceMinV1, + ReduceProdV1, + ReduceSumV1, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestReduceMeanV1: + @e2e_pytest_unit + def test_forward(self): + op = ReduceMeanV1("dummy", shape=(1,), keep_dims=False) + input = torch.randn(6, 12, 10, 24) + + output = op(input, []) + assert torch.equal(output, input) + + output = op(input, torch.tensor([1, 2])) + assert output.shape == (6, 24) + assert torch.equal(output, torch.mean(input, dim=(1, 2))) + + output = op(input, torch.tensor([-1])) + assert output.shape == (6, 12, 10) + assert torch.equal(output, torch.mean(input, dim=-1)) + + op = ReduceMeanV1("dummy", shape=(1,), keep_dims=True) + + output = op(input, []) + assert torch.equal(output, input) + + output = op(input, torch.tensor([1, 2])) + assert output.shape == (6, 1, 1, 24) + assert torch.equal(output, torch.mean(input, dim=(1, 2), keepdim=True)) + + output = op(input, torch.tensor([-1])) + assert output.shape == (6, 12, 10, 1) + assert torch.equal(output, torch.mean(input, dim=-1, keepdim=True)) + + +class TestReduceProdV1: + @e2e_pytest_unit + def test_forward(self): + op = ReduceProdV1("dummy", shape=(1,), keep_dims=False) + input = torch.randn(6, 12, 10, 24) + + output = op(input, []) + assert torch.equal(output, input) + + output = op(input, torch.tensor([1, 2])) + assert output.shape == (6, 24) + + output = op(input, torch.tensor([-1])) + assert output.shape == (6, 12, 10) + + op = ReduceProdV1("dummy", shape=(1,), keep_dims=True) + input = torch.randn(6, 12, 10, 24) + + output = op(input, []) + assert torch.equal(output, input) + + output = op(input, torch.tensor([1, 2])) + assert output.shape == (6, 1, 1, 24) + + output = op(input, torch.tensor([-1])) + assert output.shape == (6, 12, 10, 1) + + +class TestReduceMinV1: + @e2e_pytest_unit + def test_forward(self): + op = ReduceMinV1("dummy", shape=(1,), keep_dims=False) + input = torch.randn(6, 12, 10, 24) + + output = op(input, []) + assert torch.equal(output, input) + + output = op(input, torch.tensor([1, 2])) + assert output.shape == (6, 24) + + output = op(input, torch.tensor([-1])) + assert output.shape == (6, 12, 10) + + op = ReduceMinV1("dummy", shape=(1,), keep_dims=True) + input = torch.randn(6, 12, 10, 24) + + output = op(input, []) + assert torch.equal(output, input) + + output = op(input, torch.tensor([1, 2])) + assert output.shape == (6, 1, 1, 24) + + output = op(input, torch.tensor([-1])) + assert output.shape == (6, 12, 10, 1) + + +class TestReduceSumV1: + @e2e_pytest_unit + def test_forward(self): + op = ReduceSumV1("dummy", shape=(1,), keep_dims=False) + input = torch.randn(6, 12, 10, 24) + + output = op(input, []) + assert torch.equal(output, input) + + output = op(input, torch.tensor([1, 2])) + assert output.shape == (6, 24) + assert torch.equal(output, torch.sum(input, dim=(1, 2))) + + output = op(input, torch.tensor([-1])) + assert output.shape == (6, 12, 10) + assert torch.equal(output, torch.sum(input, dim=-1)) + + op = ReduceSumV1("dummy", shape=(1,), keep_dims=True) + input = torch.randn(6, 12, 10, 24) + + output = op(input, []) + assert torch.equal(output, input) + + output = op(input, torch.tensor([1, 2])) + assert output.shape == (6, 1, 1, 24) + assert torch.equal(output, torch.sum(input, dim=(1, 2), keepdim=True)) + + output = op(input, torch.tensor([-1])) + assert output.shape == (6, 12, 10, 1) + assert torch.equal(output, torch.sum(input, dim=-1, keepdim=True)) diff --git a/tests/unit/core/ov/ops/test_ov_ops_shape_manipulations.py b/tests/unit/core/ov/ops/test_ov_ops_shape_manipulations.py new file mode 100644 index 00000000000..ec449751074 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_shape_manipulations.py @@ -0,0 +1,103 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.core.ov.ops.shape_manipulations import ( + ReshapeV1, + ShapeOfV0, + ShapeOfV3, + SqueezeV0, + UnsqueezeV0, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestSqueezeV0: + @e2e_pytest_unit + def test_forward(self): + op = SqueezeV0("dummy", shape=(1,)) + input = torch.randn(1, 1, 5, 1, 1) + + output = op(input) + assert output.shape == (5,) + + output = op(input, torch.tensor(0)) + assert output.shape == (1, 5, 1, 1) + + output = op(input, torch.tensor([0, 1])) + assert output.shape == (5, 1, 1) + + output = op(input, torch.tensor([-2])) + assert output.shape == (1, 1, 5, 1) + + +class TestUnsqueezeV0: + @e2e_pytest_unit + def test_forward(self): + op = UnsqueezeV0("dummy", shape=(1,)) + input = torch.randn(2, 3) + + output = op(input, torch.tensor(2)) + assert output.shape == (2, 3, 1) + + output = op(input, torch.tensor([0, 1])) + assert output.shape == (1, 2, 1, 3) + + output = op(input, torch.tensor([-1])) + assert output.shape == (2, 3, 1) + + output = op(input, torch.tensor([-1, -2])) + assert output.shape == (1, 2, 1, 3) + + +class TestReshapeV1: + @e2e_pytest_unit + def test_forward(self): + op = ReshapeV1("dummy", shape=(1,), special_zero=False) + input = torch.randn(2, 5, 5, 0) + output = op(input, torch.tensor((0, 4))) + assert output.shape == (0, 4) + + op = ReshapeV1("dummy", shape=(1,), special_zero=True) + input = torch.randn(2, 5, 5, 24) + output = op(input, torch.tensor((0, -1, 4))) + assert output.shape == (2, 150, 4) + + op = ReshapeV1("dummy", shape=(1,), special_zero=True) + input = torch.randn(2, 2, 3) + output = op(input, torch.tensor((0, 0, 1, -1))) + assert output.shape == (2, 2, 1, 3) + + op = ReshapeV1("dummy", shape=(1,), special_zero=True) + input = torch.randn(3, 1, 1) + output = op(input, torch.tensor((0, -1))) + assert output.shape == (3, 1) + + +class TestShapeOfV0: + @e2e_pytest_unit + def test_forward(self): + op = ShapeOfV0("dummy", shape=(1,)) + assert torch.equal(op(torch.randn(5, 3)), torch.tensor([5, 3])) + + +class TestShapeOfV3: + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + ShapeOfV3("dummy", shape=(1,), output_type="error") + + @e2e_pytest_unit + def test_forward(self): + op = ShapeOfV3("dummy", shape=(1,), output_type="i32") + output = op(torch.randn(5, 3)) + assert torch.equal(output, torch.tensor([5, 3])) + assert output.dtype == torch.int32 + + op = ShapeOfV3("dummy", shape=(1,), output_type="i64") + output = op(torch.randn(5, 3)) + assert torch.equal(output, torch.tensor([5, 3])) + assert output.dtype == torch.int64 diff --git a/tests/unit/core/ov/ops/test_ov_ops_sorting_maximization.py b/tests/unit/core/ov/ops/test_ov_ops_sorting_maximization.py new file mode 100644 index 00000000000..5f0d0a65b56 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_sorting_maximization.py @@ -0,0 +1,47 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest + +from otx.core.ov.ops.sorting_maximization import ( + NonMaxSuppressionV5, + NonMaxSuppressionV9, + TopKV3, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestTopKV3: + @e2e_pytest_unit + def test_invalid_attr(self): + with pytest.raises(ValueError): + TopKV3("dummy", shape=(1,), axis=1, mode="error", sort="value", index_element_type="i32") + + with pytest.raises(ValueError): + TopKV3("dummy", shape=(1,), axis=1, mode="max", sort="error", index_element_type="i32") + + with pytest.raises(ValueError): + TopKV3("dummy", shape=(1,), axis=1, mode="max", sort="value", index_element_type="f32") + + @e2e_pytest_unit + def test_forward(self): + op = TopKV3("dummy", shape=(1,), axis=1, mode="max", sort="value") + with pytest.raises(NotImplementedError): + op("dummy", "dummy") + + +class TestNonMaxSuppressionV5: + @e2e_pytest_unit + def test_forward(self): + op = NonMaxSuppressionV5("dummy", shape=(1,)) + with pytest.raises(NotImplementedError): + op("dummy", "dummy", "dummy") + + +class TestNonMaxSuppressionV9: + @e2e_pytest_unit + def test_forward(self): + op = NonMaxSuppressionV9("dummy", shape=(1,)) + with pytest.raises(NotImplementedError): + op("dummy", "dummy", "dummy") diff --git a/tests/unit/core/ov/ops/test_ov_ops_type_conversions.py b/tests/unit/core/ov/ops/test_ov_ops_type_conversions.py new file mode 100644 index 00000000000..9df3ef3de88 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_type_conversions.py @@ -0,0 +1,55 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import pytest +import torch + +from otx.core.ov.ops.type_conversions import ConvertV0 +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestConvertV0: + @pytest.fixture(autouse=True) + def setup(self): + self.shape = (4, 3, 256, 256) + self.input = torch.randn(self.shape) + + @e2e_pytest_unit + def test_convert_ov_type(self): + with pytest.raises(NotImplementedError): + ConvertV0.convert_ov_type("error") + + assert torch.uint8 == ConvertV0.convert_ov_type("u1") + assert torch.uint8 == ConvertV0.convert_ov_type("u4") + assert torch.uint8 == ConvertV0.convert_ov_type("u8") + assert torch.int32 == ConvertV0.convert_ov_type("u32") + assert torch.int64 == ConvertV0.convert_ov_type("u64") + assert torch.int8 == ConvertV0.convert_ov_type("i4") + assert torch.int8 == ConvertV0.convert_ov_type("i8") + assert torch.int16 == ConvertV0.convert_ov_type("i16") + assert torch.int32 == ConvertV0.convert_ov_type("i32") + assert torch.int64 == ConvertV0.convert_ov_type("i64") + assert torch.float16 == ConvertV0.convert_ov_type("f16") + assert torch.float32 == ConvertV0.convert_ov_type("f32") + assert torch.bool == ConvertV0.convert_ov_type("boolean") + + @e2e_pytest_unit + def test_convert_torch_type(self): + with pytest.raises(NotImplementedError): + ConvertV0.convert_torch_type("error") + + assert "u8" == ConvertV0.convert_torch_type(torch.uint8) + assert "i8" == ConvertV0.convert_torch_type(torch.int8) + assert "i16" == ConvertV0.convert_torch_type(torch.int16) + assert "i32" == ConvertV0.convert_torch_type(torch.int32) + assert "i64" == ConvertV0.convert_torch_type(torch.int64) + assert "f16" == ConvertV0.convert_torch_type(torch.float16) + assert "f32" == ConvertV0.convert_torch_type(torch.float32) + assert "boolean" == ConvertV0.convert_torch_type(torch.bool) + + @e2e_pytest_unit + def test_forward(self): + op = ConvertV0("dummy", shape=(1,), destination_type="f16") + output = op(torch.randn(5, dtype=torch.float32)) + assert output.dtype == torch.float16 diff --git a/tests/unit/core/ov/ops/test_ov_ops_utils.py b/tests/unit/core/ov/ops/test_ov_ops_utils.py new file mode 100644 index 00000000000..2c3c9f2d068 --- /dev/null +++ b/tests/unit/core/ov/ops/test_ov_ops_utils.py @@ -0,0 +1,37 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-Lggggcense-Identifier: Apache-2.0 +# + +import openvino.runtime as ov +import pytest +import torch + +from otx.core.ov.ops.movements import get_torch_padding +from otx.core.ov.ops.utils import get_dynamic_shape +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_get_dynamic_shape(): + op_ov = ov.opset10.parameter([-1, 256, 256, 3], ov.Type.f32) + output = get_dynamic_shape(op_ov) + assert output == [-1, 256, 256, 3] + + +@e2e_pytest_unit +def test_get_torch_padding(): + input = torch.randn(4, 3, 64, 64) + + output = get_torch_padding([5, 5], [5, 5], "valid", input.shape[2:], [3, 3], [1, 1]) + assert output == 0 + + output = get_torch_padding([5, 5], [5, 5], "same_upper", input.shape[2:], [3, 3], [1, 1]) + output = output(input) + assert output.shape == (4, 3, 66, 66) + + output = get_torch_padding([5, 5], [5, 5], "explicit", input.shape[2:], [3, 3], [1, 1]) + output = output(input) + assert output.shape == (4, 3, 74, 74) + + with pytest.raises(NotImplementedError): + get_torch_padding([5, 5], [5, 5], "error", input.shape[2:], [3, 3], [1, 1]) diff --git a/tests/unit/core/ov/test_ov_omz_wrapper.py b/tests/unit/core/ov/test_ov_omz_wrapper.py new file mode 100644 index 00000000000..a04c9edce74 --- /dev/null +++ b/tests/unit/core/ov/test_ov_omz_wrapper.py @@ -0,0 +1,42 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import tempfile + +from openvino.model_zoo._configuration import Model + +from otx.core.ov.omz_wrapper import ( + download_model, + get_model_configuration, + get_omz_model, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_get_model_configuration(): + assert get_model_configuration("aa") is None + model = get_model_configuration("mobilenet-v2-pytorch") + assert isinstance(model, Model) + assert getattr(model, "subdirectory") is not None + assert getattr(model, "subdirectory_ori") is not None + + +@e2e_pytest_unit +def test_download_model(): + model = get_model_configuration("mobilenet-v2-pytorch") + with tempfile.TemporaryDirectory() as tempdir: + download_model(model, download_dir=tempdir) + assert str(model.subdirectory) in os.listdir(tempdir) + download_model(model, download_dir=tempdir) + + +@e2e_pytest_unit +def test_get_omz_model(): + with tempfile.TemporaryDirectory() as tempdir: + model = get_omz_model("mobilenet-v2-pytorch", download_dir=tempdir, output_dir=tempdir) + assert model is not None + assert "model_path" in model and os.path.exists(model["model_path"]) + assert "weight_path" in model and os.path.exists(model["weight_path"]) diff --git a/tests/unit/core/ov/test_ov_registry.py b/tests/unit/core/ov/test_ov_registry.py new file mode 100644 index 00000000000..25c5e32be7d --- /dev/null +++ b/tests/unit/core/ov/test_ov_registry.py @@ -0,0 +1,33 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from otx.core.ov.registry import Registry +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class TestRegistry: + @e2e_pytest_unit + def test_register(self): + registry = Registry("dummy") + + @registry.register() + def dummy1(): + pass + + assert "dummy1" in registry.registry_dict.keys() + assert dummy1 in registry + assert dummy1 == registry.get("dummy1") + + @registry.register("dummy_name") + def dummy2(): + pass + + assert "dummy_name" in registry.registry_dict.keys() + assert dummy2 in registry + assert dummy2 == registry.get("dummy_name") + + with pytest.raises(KeyError): + registry.get("error") diff --git a/tests/unit/core/ov/test_ov_utils.py b/tests/unit/core/ov/test_ov_utils.py new file mode 100644 index 00000000000..3057de461db --- /dev/null +++ b/tests/unit/core/ov/test_ov_utils.py @@ -0,0 +1,78 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +import tempfile + +import openvino.runtime as ov +import pytest + +from otx.core.ov.omz_wrapper import get_omz_model +from otx.core.ov.ops.infrastructures import ParameterV0 +from otx.core.ov.ops.modules.op_module import convert_op_to_torch_module +from otx.core.ov.ops.utils import convert_op_to_torch +from otx.core.ov.utils import ( + get_op_name, + load_ov_model, + normalize_name, + unnormalize_name, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +def test_load_ov_model(): + with tempfile.TemporaryDirectory() as tempdir: + model = get_omz_model("mobilenet-v2-pytorch", download_dir=tempdir, output_dir=tempdir) + model = load_ov_model(**model) + + model = load_ov_model("omz://mobilenet-v2-pytorch") + assert not model.inputs[0].get_partial_shape().is_dynamic + + model = load_ov_model("omz://mobilenet-v2-pytorch", convert_dynamic=True) + assert model.inputs[0].get_partial_shape().is_dynamic + + +@e2e_pytest_unit +def test_normalize_name(): + assert "dummy" == normalize_name("dummy") + assert "dummy#dummy" == normalize_name("dummy.dummy") + assert "dummy###dummy" == normalize_name("dummy...dummy") + assert "dummy#dummy#dummy" == normalize_name("dummy.dummy.dummy") + + +@e2e_pytest_unit +def test_unnormalize_name(): + assert "dummy" == unnormalize_name("dummy") + assert "dummy.dummy" == unnormalize_name("dummy#dummy") + assert "dummy...dummy" == unnormalize_name("dummy###dummy") + assert "dummy.dummy.dummy" == unnormalize_name("dummy#dummy#dummy") + + +@e2e_pytest_unit +def test_get_op_name(): + assert "dummy" == get_op_name(ov.opset10.parameter([3, 1, 2], ov.Type.f32, name="dummy")) + assert "dummy#dummy" == get_op_name(ov.opset10.parameter([3, 1, 2], ov.Type.f32, name="dummy.dummy")) + + +@e2e_pytest_unit +def test_convert_op_to_torch(): + dummy = ov.opset10.parameter([3, 1, 2], ov.Type.f32) + assert isinstance(convert_op_to_torch(dummy), ParameterV0) + + dummy = ov.opset10.depth_to_space(dummy, mode="blocks_first") + with pytest.raises(KeyError): + convert_op_to_torch(dummy) + + +@e2e_pytest_unit +def test_convert_op_to_torch_module(): + data = ov.opset10.parameter([3, 1, 2], ov.Type.f32) + mul_constant = ov.opset10.constant([1.5], ov.Type.f32, name="weight") + mul = ov.opset10.multiply(data, mul_constant) + + module = convert_op_to_torch_module(mul) + should_none, node = list(module._dependent_ops.values()) + assert should_none is None + assert node is not None and node.name == "weight" diff --git a/tests/unit/core/test_core_patcher.py b/tests/unit/core/test_core_patcher.py new file mode 100644 index 00000000000..c815806e548 --- /dev/null +++ b/tests/unit/core/test_core_patcher.py @@ -0,0 +1,361 @@ +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from otx.core.patcher import Patcher +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +class MockClass: + def method(self, *args, **kwargs): + pass + + @staticmethod + def static_method(*args, **kwargs): + pass + + @classmethod + def class_method(cls, *args, **kwargs): + pass + + +def mock_function(*args, **kwargs): + pass + + +class Counter: + def __init__(self): + self._ctr = 0 + + def inc(self): + self._ctr += 1 + + def __eq__(self, other): + return self._ctr == other + + +class TestPatcher: + @e2e_pytest_unit + def test_patch(self): + def dummy_wrapper(ctx, fn, *args, **kwargs): + kwargs.get("ctr").inc() + return fn(*args, **kwargs) + + def test_instance(): + patcher = Patcher() + mock_class = MockClass() + + ctr = Counter() + patcher.patch(mock_class.method, dummy_wrapper) + patcher.patch(mock_class.method, dummy_wrapper) + assert len(patcher._patched) == 1 + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.method(ctr=ctr) + assert ctr == 1 + patcher.unpatch(mock_class.method) + assert len(patcher._patched) == 0 + + ctr = Counter() + patcher.patch(mock_class.method, dummy_wrapper) + patcher.patch(mock_class.method, dummy_wrapper, force=False) + assert len(patcher._patched) == 1 + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.method(ctr=ctr) + assert ctr == 2 + patcher.unpatch(mock_class.method, 1) + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.method(ctr=ctr) + assert ctr == 3 + + ctr = Counter() + patcher.patch(mock_class.method, dummy_wrapper, force=False) + patcher.patch(mock_class.method, dummy_wrapper, force=False) + patcher.patch(mock_class.method, dummy_wrapper, force=False) + patcher.patch(mock_class.method, dummy_wrapper, force=False) + assert len(list(patcher._patched.values())[-1]) == 5 + mock_class.method(ctr=ctr) + assert ctr == 5 + patcher.unpatch(mock_class.method, 3) + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.method(ctr=ctr) + assert ctr == 7 + + ctr = Counter() + patcher.patch(mock_class.static_method, dummy_wrapper) + patcher.patch(mock_class.static_method, dummy_wrapper) + assert len(patcher._patched) == 2 + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.static_method(ctr=ctr) + assert ctr == 1 + patcher.unpatch(mock_class.static_method) + assert len(patcher._patched) == 1 + + ctr = Counter() + patcher.patch(mock_class.static_method, dummy_wrapper) + patcher.patch(mock_class.static_method, dummy_wrapper, force=False) + assert len(patcher._patched) == 2 + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.static_method(ctr=ctr) + assert ctr == 2 + patcher.unpatch(mock_class.static_method, 1) + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.static_method(ctr=ctr) + assert ctr == 3 + + ctr = Counter() + patcher.patch(mock_class.static_method, dummy_wrapper, force=False) + patcher.patch(mock_class.static_method, dummy_wrapper, force=False) + patcher.patch(mock_class.static_method, dummy_wrapper, force=False) + patcher.patch(mock_class.static_method, dummy_wrapper, force=False) + assert len(list(patcher._patched.values())[-1]) == 5 + mock_class.static_method(ctr=ctr) + assert ctr == 5 + patcher.unpatch(mock_class.static_method, 3) + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.static_method(ctr=ctr) + assert ctr == 7 + + ctr = Counter() + patcher.patch(mock_class.class_method, dummy_wrapper) + patcher.patch(mock_class.class_method, dummy_wrapper) + assert len(patcher._patched) == 3 + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.class_method(ctr=ctr) + assert ctr == 1 + patcher.unpatch(mock_class.class_method) + assert len(patcher._patched) == 2 + + ctr = Counter() + patcher.patch(mock_class.class_method, dummy_wrapper) + patcher.patch(mock_class.class_method, dummy_wrapper, force=False) + assert len(patcher._patched) == 3 + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.class_method(ctr=ctr) + assert ctr == 2 + patcher.unpatch(mock_class.class_method, 1) + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.class_method(ctr=ctr) + assert ctr == 3 + + ctr = Counter() + patcher.patch(mock_class.class_method, dummy_wrapper, force=False) + patcher.patch(mock_class.class_method, dummy_wrapper, force=False) + patcher.patch(mock_class.class_method, dummy_wrapper, force=False) + patcher.patch(mock_class.class_method, dummy_wrapper, force=False) + assert len(list(patcher._patched.values())[-1]) == 5 + mock_class.class_method(ctr=ctr) + assert ctr == 5 + patcher.unpatch(mock_class.class_method, 3) + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.class_method(ctr=ctr) + assert ctr == 7 + + patcher.unpatch() + assert len(patcher._patched) == 0 + + def test_class(): + patcher = Patcher() + mock_class = MockClass() + + ctr = Counter() + patcher.patch(MockClass.method, dummy_wrapper) + patcher.patch(MockClass.method, dummy_wrapper) + assert len(patcher._patched) == 1 + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.method(ctr=ctr) + assert ctr == 1 + patcher.unpatch(MockClass.method) + assert len(patcher._patched) == 0 + + ctr = Counter() + patcher.patch(MockClass.method, dummy_wrapper) + patcher.patch(MockClass.method, dummy_wrapper, force=False) + assert len(patcher._patched) == 1 + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.method(ctr=ctr) + assert ctr == 2 + patcher.unpatch(MockClass.method, 1) + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.method(ctr=ctr) + assert ctr == 3 + + ctr = Counter() + patcher.patch(MockClass.method, dummy_wrapper, force=False) + patcher.patch(MockClass.method, dummy_wrapper, force=False) + patcher.patch(MockClass.method, dummy_wrapper, force=False) + patcher.patch(MockClass.method, dummy_wrapper, force=False) + assert len(list(patcher._patched.values())[-1]) == 5 + mock_class.method(ctr=ctr) + assert ctr == 5 + patcher.unpatch(MockClass.method, 3) + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.method(ctr=ctr) + assert ctr == 7 + + ctr = Counter() + patcher.patch(MockClass.static_method, dummy_wrapper) + patcher.patch(MockClass.static_method, dummy_wrapper) + assert len(patcher._patched) == 2 + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.static_method(ctr=ctr) + assert ctr == 1 + patcher.unpatch(MockClass.static_method) + assert len(patcher._patched) == 1 + + ctr = Counter() + patcher.patch(MockClass.static_method, dummy_wrapper) + patcher.patch(MockClass.static_method, dummy_wrapper, force=False) + assert len(patcher._patched) == 2 + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.static_method(ctr=ctr) + assert ctr == 2 + patcher.unpatch(MockClass.static_method, 1) + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.static_method(ctr=ctr) + assert ctr == 3 + + ctr = Counter() + patcher.patch(MockClass.static_method, dummy_wrapper, force=False) + patcher.patch(MockClass.static_method, dummy_wrapper, force=False) + patcher.patch(MockClass.static_method, dummy_wrapper, force=False) + patcher.patch(MockClass.static_method, dummy_wrapper, force=False) + assert len(list(patcher._patched.values())[-1]) == 5 + mock_class.static_method(ctr=ctr) + assert ctr == 5 + patcher.unpatch(MockClass.static_method, 3) + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.static_method(ctr=ctr) + assert ctr == 7 + + ctr = Counter() + patcher.patch(MockClass.class_method, dummy_wrapper) + patcher.patch(MockClass.class_method, dummy_wrapper) + assert len(patcher._patched) == 3 + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.class_method(ctr=ctr) + assert ctr == 1 + patcher.unpatch(MockClass.class_method) + assert len(patcher._patched) == 2 + + ctr = Counter() + patcher.patch(MockClass.class_method, dummy_wrapper) + patcher.patch(MockClass.class_method, dummy_wrapper, force=False) + assert len(patcher._patched) == 3 + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.class_method(ctr=ctr) + assert ctr == 2 + patcher.unpatch(MockClass.class_method, 1) + assert len(list(patcher._patched.values())[-1]) == 1 + mock_class.class_method(ctr=ctr) + assert ctr == 3 + + ctr = Counter() + patcher.patch(MockClass.class_method, dummy_wrapper, force=False) + patcher.patch(MockClass.class_method, dummy_wrapper, force=False) + patcher.patch(MockClass.class_method, dummy_wrapper, force=False) + patcher.patch(MockClass.class_method, dummy_wrapper, force=False) + assert len(list(patcher._patched.values())[-1]) == 5 + mock_class.class_method(ctr=ctr) + assert ctr == 5 + patcher.unpatch(MockClass.class_method, 3) + assert len(list(patcher._patched.values())[-1]) == 2 + mock_class.class_method(ctr=ctr) + assert ctr == 7 + + patcher.unpatch() + assert len(patcher._patched) == 0 + + def test_module(): + patcher = Patcher() + + ctr = Counter() + patcher.patch( + "tests.unit.core.test_core_patcher.mock_function", + dummy_wrapper, + ) + patcher.patch( + "tests.unit.core.test_core_patcher.mock_function", + dummy_wrapper, + ) + assert len(patcher._patched) == 1 + assert len(list(patcher._patched.values())[-1]) == 1 + from tests.unit.core.test_core_patcher import mock_function + + mock_function(ctr=ctr) + assert ctr == 1 + patcher.unpatch("tests.unit.core.test_core_patcher.mock_function") + assert len(patcher._patched) == 0 + + ctr = Counter() + patcher.patch( + "tests.unit.core.test_core_patcher.mock_function", + dummy_wrapper, + ) + patcher.patch( + "tests.unit.core.test_core_patcher.mock_function", + dummy_wrapper, + force=False, + ) + assert len(list(patcher._patched.values())[-1]) == 2 + from tests.unit.core.test_core_patcher import mock_function + + mock_function(ctr=ctr) + assert ctr == 2 + patcher.unpatch( + "tests.unit.core.test_core_patcher.mock_function", + 1, + ) + assert len(list(patcher._patched.values())[-1]) == 1 + from tests.unit.core.test_core_patcher import mock_function + + mock_function(ctr=ctr) + assert ctr == 3 + + ctr = Counter() + patcher.patch( + "tests.unit.core.test_core_patcher.mock_function", + dummy_wrapper, + force=False, + ) + patcher.patch( + "tests.unit.core.test_core_patcher.mock_function", + dummy_wrapper, + force=False, + ) + patcher.patch( + "tests.unit.core.test_core_patcher.mock_function", + dummy_wrapper, + force=False, + ) + patcher.patch( + "tests.unit.core.test_core_patcher.mock_function", + dummy_wrapper, + force=False, + ) + assert len(list(patcher._patched.values())[-1]) == 5 + from tests.unit.core.test_core_patcher import mock_function + + mock_function(ctr=ctr) + assert ctr == 5 + patcher.unpatch( + "tests.unit.core.test_core_patcher.mock_function", + 3, + ) + assert len(list(patcher._patched.values())[-1]) == 2 + from tests.unit.core.test_core_patcher import mock_function + + mock_function(ctr=ctr) + assert ctr == 7 + + patcher.unpatch() + assert len(patcher._patched) == 0 + + test_instance() + test_class() + test_module() + + @e2e_pytest_unit + def test_import_obj(self): + patcher = Patcher() + assert (Patcher, "patch") == patcher.import_obj("otx.core.patcher.Patcher.patch") + assert (Patcher, "patch") == patcher.import_obj(Patcher.patch) diff --git a/tests/unit/core/utils/__init__.py b/tests/unit/core/utils/__init__.py deleted file mode 100644 index 3c95eeaf8ed..00000000000 --- a/tests/unit/core/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -"""Unit tests for OTX core utils.""" diff --git a/tests/unit/core/utils/test_mask_utils.py b/tests/unit/core/utils/test_mask_utils.py deleted file mode 100644 index 5f32eca07a9..00000000000 --- a/tests/unit/core/utils/test_mask_utils.py +++ /dev/null @@ -1,20 +0,0 @@ -import numpy as np -import torch -from otx.core.utils.mask_util import encode_rle -from pycocotools import mask as mask_utils - - -def test_encode_rle(num_test_cases=30): - """Test encode_rle function. - - Args: - num_test_cases (int, optional): number of test cases. Defaults to 30. - """ - for _ in range(num_test_cases): - h, w = torch.randint(low=1, high=800, size=(2,)) - mask = torch.randint(low=0, high=2, size=(h, w)).bool() - torch_rle = encode_rle(mask) - torch_rle = mask_utils.frPyObjects(torch_rle, *torch_rle["size"]) - np_rle = mask_utils.encode(np.asfortranarray(mask.numpy())) - assert torch_rle["counts"] == np_rle["counts"], f"Expected {np_rle['counts']} but got {torch_rle['counts']}" - assert torch_rle["size"] == np_rle["size"], f"Expected {np_rle['size']} but got {torch_rle['size']}" diff --git a/tests/unit/core/utils/test_tile.py b/tests/unit/core/utils/test_tile.py deleted file mode 100644 index a32a13cf91f..00000000000 --- a/tests/unit/core/utils/test_tile.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from __future__ import annotations - -from unittest.mock import MagicMock - -import numpy as np -from datumaro import Image -from datumaro.plugins.tiling.util import xywh_to_x1y1x2y2 -from openvino.model_api.models import Model -from openvino.model_api.tilers import Tiler -from otx.core.data.dataset.tile import OTXTileTransform - - -def test_tile_transform_consistency(mocker): - # Test that the tiler and tile transform are consistent - rng = np.random.default_rng() - rnd_tile_size = rng.integers(low=100, high=500) - rnd_tile_overlap = rng.random() - image_size = rng.integers(low=1000, high=5000) - np_image = np.zeros((image_size, image_size, 3), dtype=np.uint8) - dm_image = Image.from_numpy(np_image) - - mock_model = MagicMock(spec=Model) - mocker.patch("openvino.model_api.tilers.tiler.Tiler.__init__", return_value=None) - mocker.patch.multiple(Tiler, __abstractmethods__=set()) - - tiler = Tiler(model=mock_model) - tiler.tile_size = rnd_tile_size - tiler.tiles_overlap = rnd_tile_overlap - - mocker.patch("otx.core.data.dataset.tile.OTXTileTransform.__init__", return_value=None) - tile_transform = OTXTileTransform() - tile_transform._tile_size = (rnd_tile_size, rnd_tile_size) - tile_transform._overlap = (rnd_tile_overlap, rnd_tile_overlap) - - dm_rois = [xywh_to_x1y1x2y2(*roi) for roi in tile_transform._extract_rois(dm_image)] - # 0 index in tiler is the full image so we skip it - assert np.allclose(dm_rois, tiler._tile(np_image)[1:]) diff --git a/tests/unit/core/utils/test_utils.py b/tests/unit/core/utils/test_utils.py deleted file mode 100644 index 917c2e421ed..00000000000 --- a/tests/unit/core/utils/test_utils.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest -from otx.core.utils import utils as target_file -from otx.core.utils.utils import get_adaptive_num_workers - - -@pytest.mark.parametrize("num_dataloader", [1, 2, 4]) -def test_get_adaptive_num_workers(mocker, num_dataloader): - num_gpu = 5 - mock_torch = mocker.patch.object(target_file, "torch") - mock_torch.cuda.device_count.return_value = num_gpu - - num_cpu = 20 - mocker.patch.object(target_file, "cpu_count", return_value=num_cpu) - - assert get_adaptive_num_workers(num_dataloader) == num_cpu // (num_gpu * num_dataloader) - - -def test_get_adaptive_num_workers_no_gpu(mocker): - num_gpu = 0 - mock_torch = mocker.patch.object(target_file, "torch") - mock_torch.cuda.device_count.return_value = num_gpu - - num_cpu = 20 - mocker.patch.object(target_file, "cpu_count", return_value=num_cpu) - - assert get_adaptive_num_workers() is None diff --git a/tests/unit/engine/__init__.py b/tests/unit/engine/__init__.py deleted file mode 100644 index 916f3a44b27..00000000000 --- a/tests/unit/engine/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/engine/utils/__init__.py b/tests/unit/engine/utils/__init__.py deleted file mode 100644 index 916f3a44b27..00000000000 --- a/tests/unit/engine/utils/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/engine/utils/test_api.py b/tests/unit/engine/utils/test_api.py deleted file mode 100644 index e136058d463..00000000000 --- a/tests/unit/engine/utils/test_api.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import pytest -from otx.core.types.task import OTXTaskType -from otx.engine.utils.api import RECIPE_PATH, list_models - - -def test_list_models() -> None: - models = list_models() - assert len(models) >= 32 - - -@pytest.mark.parametrize("task", [task.value for task in OTXTaskType]) -def test_list_models_per_task(task: str) -> None: - task_dir = task - if task_dir.endswith("CLS"): - task_dir = "classification/" + task_dir - elif task_dir.startswith("ACTION"): - task_dir = "action/" + task_dir - target_dir = RECIPE_PATH / task_dir.lower() - target_recipes = [str(recipe.stem) for recipe in target_dir.glob("**/*.yaml")] - - models = list_models(task=task) - assert sorted(models) == sorted(target_recipes) - - -def test_list_models_pattern() -> None: - models = list_models(pattern="efficient") - - target = [ - "efficientnet_v2_light", - "efficientnet_b0_light", - "maskrcnn_efficientnetb2b", - "maskrcnn_efficientnetb2b_tile", - "otx_efficientnet_v2", - "otx_efficientnet_b0", - "tv_efficientnet_b0", - "tv_efficientnet_b1", - "tv_efficientnet_b3", - "tv_efficientnet_b4", - "tv_efficientnet_v2_l", - ] - assert sorted(models) == sorted(target) - - -def test_list_models_print_table(capfd: pytest.CaptureFixture) -> None: - list_models(pattern="otx_efficient", print_table=True) - - out, _ = capfd.readouterr() - assert "Task" in out - assert "Model Name" in out - assert "Recipe Path" in out - assert "otx_efficientnet_b0" in out - assert "otx_efficientnet_v2" in out diff --git a/tests/unit/engine/utils/test_auto_configurator.py b/tests/unit/engine/utils/test_auto_configurator.py deleted file mode 100644 index 028ea79177d..00000000000 --- a/tests/unit/engine/utils/test_auto_configurator.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - - -from pathlib import Path - -import pytest -from otx.core.data.dataset.base import LabelInfo -from otx.core.data.module import OTXDataModule -from otx.core.model.entity.base import OTXModel -from otx.core.types.task import OTXTaskType -from otx.engine.utils.auto_configurator import ( - DEFAULT_CONFIG_PER_TASK, - AutoConfigurator, - configure_task, -) - - -@pytest.fixture() -def fxt_data_root_per_task_type() -> dict: - return { - "tests/assets/classification_dataset": OTXTaskType.MULTI_CLASS_CLS, - "tests/assets/multilabel_classification": OTXTaskType.MULTI_LABEL_CLS, - "tests/assets/car_tree_bug": OTXTaskType.DETECTION, - "tests/assets/common_semantic_segmentation_dataset": OTXTaskType.SEMANTIC_SEGMENTATION, - "tests/assets/action_detection_dataset": OTXTaskType.ACTION_DETECTION, - } - - -def test_configure_task_with_supported_data_format(fxt_data_root_per_task_type: dict) -> None: - # Test the configure_task function with a supported data format - for data_root in fxt_data_root_per_task_type: - task = configure_task(data_root) - assert task is not None - assert isinstance(task, OTXTaskType) - assert task == fxt_data_root_per_task_type[data_root] - - -def test_configure_task_with_unsupported_data_format(tmp_path: Path) -> None: - # Create a temporary directory for testing - data_root = tmp_path / "data" - data_root.mkdir() - - # Test the configure_task function with an unsupported data format - with pytest.raises(ValueError, match="Can't find proper task."): - configure_task(data_root) - - -class TestAutoConfigurator: - def test_check_task(self) -> None: - # None inputs - auto_configurator = AutoConfigurator(data_root=None, task=None) - with pytest.raises(RuntimeError): - _ = auto_configurator.task - - # data_root is None & task is not None - auto_configurator = AutoConfigurator(data_root=None, task="MULTI_CLASS_CLS") - assert auto_configurator.task == "MULTI_CLASS_CLS" - - # data_root is not None & task is None - data_root = "tests/assets/classification_dataset" - auto_configurator = AutoConfigurator(data_root=data_root) - assert auto_configurator.task == "MULTI_CLASS_CLS" - - def test_load_default_config(self) -> None: - # Test the load_default_config function - data_root = "tests/assets/classification_dataset" - task = OTXTaskType.MULTI_CLASS_CLS - auto_configurator = AutoConfigurator(data_root=data_root, task=task) - - # Default Config - default_config = auto_configurator._load_default_config() - target_config = DEFAULT_CONFIG_PER_TASK[task].resolve() - assert isinstance(default_config, dict) - assert len(default_config) > 0 - assert "config" in default_config - assert len(default_config["config"]) > 0 - assert default_config["config"][0] == target_config - - # OTX-Mobilenet-v2 - # new_config - model_name = "otx_mobilenet_v3_large" - new_config = auto_configurator._load_default_config(model_name=model_name) - new_path = str(target_config).split("/") - new_path[-1] = f"{model_name}.yaml" - new_target_config = Path("/".join(new_path)) - assert isinstance(new_config, dict) - assert len(new_config) > 0 - assert "config" in new_config - assert len(new_config["config"]) > 0 - assert new_config["config"][0] == new_target_config - - def test_get_datamodule(self) -> None: - data_root = None - task = OTXTaskType.DETECTION - auto_configurator = AutoConfigurator(data_root=data_root, task=task) - - # data_root is None - assert auto_configurator.get_datamodule() is None - - data_root = "tests/assets/car_tree_bug" - auto_configurator = AutoConfigurator(data_root=data_root, task=task) - - datamodule = auto_configurator.get_datamodule() - assert isinstance(datamodule, OTXDataModule) - assert datamodule.task == task - - def test_get_model(self) -> None: - task = OTXTaskType.DETECTION - auto_configurator = AutoConfigurator(task=task) - - # Default Model - model = auto_configurator.get_model() - assert isinstance(model, OTXModel) - assert model.num_classes == 1000 - - # With label_info - label_names = ["class1", "class2", "class3"] - label_info = LabelInfo(label_names=label_names, label_groups=[label_names]) - model = auto_configurator.get_model(label_info=label_info) - assert isinstance(model, OTXModel) - assert model.num_classes == 3 - - def test_get_optimizer(self) -> None: - task = OTXTaskType.SEMANTIC_SEGMENTATION - auto_configurator = AutoConfigurator(task=task) - optimizer = auto_configurator.get_optimizer() - if isinstance(optimizer, list): - for opt in optimizer: - assert callable(opt) - else: - assert callable(optimizer) - - def test_get_scheduler(self) -> None: - task = OTXTaskType.INSTANCE_SEGMENTATION - auto_configurator = AutoConfigurator(task=task) - scheduler = auto_configurator.get_scheduler() - if isinstance(scheduler, list): - for sch in scheduler: - assert callable(sch) - else: - assert callable(scheduler) diff --git a/tests/unit/hpo/__init__.py b/tests/unit/hpo/__init__.py deleted file mode 100644 index 916f3a44b27..00000000000 --- a/tests/unit/hpo/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/hpo/test_hpo_base.py b/tests/unit/hpo/test_hpo_base.py index 14b574b1d93..38639e9b97d 100644 --- a/tests/unit/hpo/test_hpo_base.py +++ b/tests/unit/hpo/test_hpo_base.py @@ -1,107 +1,130 @@ import json -from pathlib import Path +from os import path as osp import pytest + from otx.hpo.hpo_base import Trial +from tests.test_suite.e2e_test_system import e2e_pytest_component -@pytest.fixture() +@pytest.fixture def good_trial_args(): return {"trial_id": "name", "configuration": {"hp1": 1, "hp2": 1.2}, "train_environment": {"subset_ratio": 0.5}} -@pytest.fixture() +@pytest.fixture def trial(good_trial_args): return Trial(**good_trial_args) -def register_scores_to_trial(trial, scores=list(range(100))): # noqa: B006, B008 - base_resource = max(trial.score.keys()) if len(trial.score) != 0 else 0 +def register_scores_to_trial(trial, scores=[val for val in range(100)]): + if len(trial.score) != 0: + base_resource = max(trial.score.keys()) + else: + base_resource = 0 for idx, score in enumerate(scores): trial.register_score(score, base_resource + idx + 1) class TestTrial: + @e2e_pytest_component def test_init(self, good_trial_args): Trial(**good_trial_args) + @e2e_pytest_component def test_set_iteration(self, trial): trial.iteration = 10 config = trial.get_train_configuration() assert config["configuration"]["iterations"] == 10 + @e2e_pytest_component @pytest.mark.parametrize("iter_val", [-10, 0]) def test_set_negative_iteration(self, trial, iter_val): - with pytest.raises(ValueError, match="should be positive"): + with pytest.raises(ValueError): trial.iteration = iter_val + @e2e_pytest_component def test_get_train_configuration(self, good_trial_args): trial = Trial(**good_trial_args) train_config = trial.get_train_configuration() assert train_config["configuration"] == good_trial_args["configuration"] assert train_config["train_environment"] == good_trial_args["train_environment"] + @e2e_pytest_component def test_get_train_configuration_without_train_env(self, good_trial_args): del good_trial_args["train_environment"] trial = Trial(**good_trial_args) train_config = trial.get_train_configuration() assert train_config["train_environment"] is None + @e2e_pytest_component @pytest.mark.parametrize("score", [-10, 12.5]) def test_register_score(self, trial, score): for resource in [1, 4.3, 10]: trial.register_score(score, resource) + @e2e_pytest_component @pytest.mark.parametrize("resource", [-10, 0]) def test_register_score_not_postive_resource(self, trial, resource): score = 10 - with pytest.raises(ValueError, match="should be positive"): + with pytest.raises(ValueError): trial.register_score(score, resource) + @e2e_pytest_component @pytest.mark.parametrize("mode", ["min", "max"]) @pytest.mark.parametrize("resource_limit", [None, 10, 20]) def test_get_best_score(self, trial, mode, resource_limit): - scores = list(range(100)) + scores = [val for val in range(100)] register_scores_to_trial(trial, scores) if resource_limit is not None: scores = {i + 1: score for i, score in enumerate(scores)} scores = [val for key, val in scores.items() if key <= resource_limit] - expected_score = min(scores) if mode == "min" else max(scores) + if mode == "min": + expected_score = min(scores) + else: + expected_score = max(scores) assert expected_score == trial.get_best_score(mode, resource_limit) + @e2e_pytest_component def test_get_best_score_empty_score(self, trial): assert trial.get_best_score() is None + @e2e_pytest_component def test_get_best_score_no_trial_to_meet_condition(self, trial): - scores = list(range(100)) + scores = [val for val in range(100)] register_scores_to_trial(trial, scores) assert trial.get_best_score(resource_limit=0.5) is None + @e2e_pytest_component def test_get_best_score_with_empty_scores(self, trial): assert trial.get_best_score() is None + @e2e_pytest_component def test_get_best_score_with_wrong_mode_value(self, trial): register_scores_to_trial(trial) - with pytest.raises(ValueError, match="mode should be"): + with pytest.raises(ValueError): trial.get_best_score(mode="wrong") + @e2e_pytest_component @pytest.mark.parametrize("resource", [12, 42.12]) def test_get_progress(self, trial, resource): trial.register_score(100, resource) assert trial.get_progress() == resource + @e2e_pytest_component def test_get_progress_not_trained_at_all(self, trial): assert trial.get_progress() == 0 - def test_save_results(self, trial, tmp_path: Path): + @e2e_pytest_component + def test_save_results(self, trial, tmp_path): register_scores_to_trial(trial) - save_path = tmp_path / "test" + save_path = osp.join(tmp_path, "test") trial.save_results(save_path) - with save_path.open() as f: + with open(save_path, "r") as f: result = json.load(f) assert result["id"] == "name" @@ -111,26 +134,31 @@ def test_save_results(self, trial, tmp_path: Path): for key, val in result["score"].items(): assert int(key) - 1 == val + @e2e_pytest_component def test_finalize(self, trial): trial.iteration = 10 trial.register_score(10, 5) trial.finalize() assert trial.is_done() + @e2e_pytest_component def test_finalize_without_registered_score(self, trial): trial.iteration = 10 - with pytest.raises(RuntimeError, match="didn't report any score but tries to be done"): + with pytest.raises(RuntimeError): trial.finalize() + @e2e_pytest_component def test_is_not_done(self, trial): trial.iteration = 10 assert not trial.is_done() + @e2e_pytest_component def test_is_done(self, trial): trial.iteration = 10 trial.register_score(10, 10) assert trial.is_done() + @e2e_pytest_component def test_is_done_iteration_not_set_yet(self, trial): - with pytest.raises(ValueError, match="iteration isn't set yet"): + with pytest.raises(ValueError): trial.is_done() diff --git a/tests/unit/hpo/test_hyperband.py b/tests/unit/hpo/test_hyperband.py index 00dad36d58c..649a488d281 100644 --- a/tests/unit/hpo/test_hyperband.py +++ b/tests/unit/hpo/test_hyperband.py @@ -2,48 +2,52 @@ # SPDX-License-Identifier: Apache-2.0 # -from __future__ import annotations - import copy import json import math from math import ceil -from pathlib import Path +from os import path as osp from tempfile import TemporaryDirectory +from typing import Union import pytest + from otx.hpo import hyperband from otx.hpo.hpo_base import TrialStatus from otx.hpo.hyperband import AshaTrial, Bracket, HyperBand, Rung +from tests.test_suite.e2e_test_system import e2e_pytest_component -@pytest.fixture() +@pytest.fixture def good_trial_args(): return {"trial_id": "name", "configuration": {"hp1": 1, "hp2": 1.2}, "train_environment": {"subset_ratio": 0.5}} -@pytest.fixture() +@pytest.fixture def trial(good_trial_args): return AshaTrial(**good_trial_args) -@pytest.fixture() +@pytest.fixture def good_rung_args(): return {"resource": 10, "num_required_trial": 16, "reduction_factor": 2, "rung_idx": 0} -def register_scores_to_trial(trial, scores=list(range(100))): # noqa: B006, B008 - base_resource = max(trial.score.keys()) if len(trial.score) != 0 else 0 +def register_scores_to_trial(trial, scores=[val for val in range(100)]): + if len(trial.score) != 0: + base_resource = max(trial.score.keys()) + else: + base_resource = 0 for idx, score in enumerate(scores): trial.register_score(score, base_resource + idx + 1) -@pytest.fixture() +@pytest.fixture def rung(good_rung_args): return Rung(**good_rung_args) -@pytest.fixture() +@pytest.fixture def good_bracket_args(): hp_configs = [AshaTrial(i, {"hp1": 1, "hp2": 1.2}) for i in range(100)] return { @@ -57,18 +61,18 @@ def good_bracket_args(): } -@pytest.fixture() +@pytest.fixture def bracket(good_bracket_args): return Bracket(**good_bracket_args) -@pytest.fixture() +@pytest.fixture def good_hyperband_args(): with TemporaryDirectory() as tmp_dir: yield { "search_space": { - "hp1": {"type": "uniform", "max": 100, "min": 10}, - "hp2": {"type": "qloguniform", "max": 1000, "min": 100, "step": 2, "log_base": 10}, + "hp1": {"param_type": "uniform", "max": 100, "min": 10}, + "hp2": {"param_type": "qloguniform", "max": 1000, "min": 100, "step": 2, "log_base": 10}, }, "save_path": tmp_dir, "mode": "max", @@ -76,6 +80,7 @@ def good_hyperband_args(): "num_full_iterations": 64, "non_pure_train_ratio": 0.2, "full_dataset_size": 100, + "metric": "mAP", "maximum_resource": 64, "minimum_resource": 1, "reduction_factor": 4, @@ -85,49 +90,56 @@ def good_hyperband_args(): } -@pytest.fixture() +@pytest.fixture def hyper_band(good_hyperband_args): return HyperBand(**good_hyperband_args) +@e2e_pytest_component @pytest.mark.parametrize("reduction_factor", [4, 100, 4000]) def test_check_reduction_factor_value(reduction_factor): hyperband._check_reduction_factor_value(reduction_factor) +@e2e_pytest_component @pytest.mark.parametrize("reduction_factor", [-10, 1]) def test_check_reduction_factor_lesser_value(reduction_factor): - with pytest.raises(ValueError, match="should be greater"): + with pytest.raises(ValueError): hyperband._check_reduction_factor_value(reduction_factor) class TestAshaTrial: + @e2e_pytest_component @pytest.mark.parametrize("rung_val", [0, 10]) def teste_set_rung(self, trial, rung_val): trial.rung = rung_val + @e2e_pytest_component @pytest.mark.parametrize("rung_val", [-10, -3]) def test_set_negative_rung(self, trial, rung_val): - with pytest.raises(ValueError, match="should be positive"): + with pytest.raises(ValueError): trial.rung = rung_val + @e2e_pytest_component @pytest.mark.parametrize("bracket_val", [0, 10]) def teste_set_bracket(self, trial, bracket_val): trial.bracket = bracket_val + @e2e_pytest_component @pytest.mark.parametrize("bracket_val", [-10, -3]) def test_set_negative_bracket(self, trial, bracket_val): - with pytest.raises(ValueError, match="should be positive"): + with pytest.raises(ValueError): trial.bracket = bracket_val - def test_save_results(self, trial, tmp_path: Path): + @e2e_pytest_component + def test_save_results(self, trial, tmp_path): rung_idx = 3 trial.rung = rung_idx register_scores_to_trial(trial) - save_path = tmp_path / "test" + save_path = osp.join(tmp_path, "test") trial.save_results(save_path) - with save_path.open() as f: + with open(save_path, "r") as f: result = json.load(f) assert result["id"] == "name" assert result["configuration"]["hp1"] == 1 @@ -139,37 +151,43 @@ def test_save_results(self, trial, tmp_path: Path): class TestRung: + @e2e_pytest_component def test_init(self, good_rung_args): Rung(**good_rung_args) + @e2e_pytest_component @pytest.mark.parametrize("resource", [-10, 0]) def test_init_resource_nenative(self, good_rung_args, resource): wrong_trial_args = good_rung_args wrong_trial_args["resource"] = resource - with pytest.raises(ValueError, match="should be positive"): + with pytest.raises(ValueError): Rung(**wrong_trial_args) + @e2e_pytest_component @pytest.mark.parametrize("num_required_trial", [-10, 0]) def test_init_num_required_trial(self, good_rung_args, num_required_trial): wrong_trial_args = good_rung_args wrong_trial_args["num_required_trial"] = num_required_trial - with pytest.raises(ValueError, match="should be positive"): + with pytest.raises(ValueError): Rung(**wrong_trial_args) + @e2e_pytest_component @pytest.mark.parametrize("reduction_factor", [-10, 0, 1]) def test_init_wrong_reduction_factor(self, good_rung_args, reduction_factor): wrong_trial_args = good_rung_args wrong_trial_args["reduction_factor"] = reduction_factor - with pytest.raises(ValueError, match="reduction_factor should be"): + with pytest.raises(ValueError): Rung(**wrong_trial_args) + @e2e_pytest_component @pytest.mark.parametrize("rung_idx", [-10, -3]) def test_init_wrong_rung_idx(self, good_rung_args, rung_idx): wrong_trial_args = good_rung_args wrong_trial_args["rung_idx"] = rung_idx - with pytest.raises(ValueError, match="should be positive"): + with pytest.raises(ValueError): Rung(**wrong_trial_args) + @e2e_pytest_component def test_add_new_trial(self, rung, good_trial_args): for _ in range(rung.num_required_trial): trial = AshaTrial(**good_trial_args) @@ -178,12 +196,14 @@ def test_add_new_trial(self, rung, good_trial_args): assert trial.iteration == rung.resource assert trial.status == TrialStatus.READY + @e2e_pytest_component def test_add_too_many_trials(self, rung, good_trial_args): - for _ in range(rung.num_required_trial): - rung.add_new_trial(AshaTrial(**good_trial_args)) - with pytest.raises(RuntimeError, match="already sufficient trials"): - rung.add_new_trial(AshaTrial(**good_trial_args)) + with pytest.raises(RuntimeError): + for _ in range(rung.num_required_trial + 1): + trial = AshaTrial(**good_trial_args) + rung.add_new_trial(trial) + @e2e_pytest_component @pytest.mark.parametrize("mode", ["max", "min"]) def test_get_best_trial(self, rung, good_trial_args, mode): for score in range(rung.num_required_trial): @@ -198,6 +218,7 @@ def test_get_best_trial(self, rung, good_trial_args, mode): else: assert best_trial.get_best_score(mode) == 0 + @e2e_pytest_component def test_get_best_trial_with_not_started_trial(self, rung, good_trial_args): for score in range(rung.num_required_trial - 1): trial = AshaTrial(**good_trial_args) @@ -210,6 +231,7 @@ def test_get_best_trial_with_not_started_trial(self, rung, good_trial_args): assert best_trial.get_best_score() == rung.num_required_trial - 2 + @e2e_pytest_component def test_get_best_trial_when_best_trial_is_undone(self, rung, good_trial_args): for _ in range(rung.num_required_trial - 1): trial = AshaTrial(**good_trial_args) @@ -223,14 +245,17 @@ def test_get_best_trial_when_best_trial_is_undone(self, rung, good_trial_args): assert best_trial.get_best_score() == 100 + @e2e_pytest_component def test_get_best_trial_with_no_trial(self, rung): best_trial = rung.get_best_trial() assert best_trial is None + @e2e_pytest_component def test_get_best_trial_wrong_mode_val(self, rung): - with pytest.raises(ValueError, match="mode should be"): + with pytest.raises(ValueError): rung.get_best_trial("wrong") + @e2e_pytest_component def test_need_more_trials(self, rung, good_trial_args): for _ in range(rung.num_required_trial): trial = AshaTrial(**good_trial_args) @@ -239,12 +264,14 @@ def test_need_more_trials(self, rung, good_trial_args): assert not rung.need_more_trials() + @e2e_pytest_component def test_get_num_trials(self, rung, good_trial_args): for idx in range(rung.num_required_trial): trial = AshaTrial(**good_trial_args) rung.add_new_trial(trial) assert rung.get_num_trials() == idx + 1 + @e2e_pytest_component def test_need_more_trails(self, rung, good_trial_args): for i in range(1, rung.num_required_trial + 1): trial = AshaTrial(**good_trial_args) @@ -254,25 +281,27 @@ def test_need_more_trails(self, rung, good_trial_args): else: assert not rung.need_more_trials() + @e2e_pytest_component def test_is_done(self, rung, good_trial_args): - for _ in range(rung.num_required_trial - 1): + for i in range(rung.num_required_trial - 1): trial = AshaTrial(**good_trial_args) - register_scores_to_trial(trial, list(range(rung.resource))) + register_scores_to_trial(trial, [val for val in range(rung.resource)]) rung.add_new_trial(trial) assert not rung.is_done() trial = AshaTrial(**good_trial_args) - register_scores_to_trial(trial, list(range(rung.resource - 1))) + register_scores_to_trial(trial, [val for val in range(rung.resource - 1)]) rung.add_new_trial(trial) assert not rung.is_done() trial.register_score(100, rung.resource + 1) assert rung.is_done() + @e2e_pytest_component def test_get_trial_to_promote_not_asha(self, rung, good_trial_args): maximum_score = 9999999 - for _ in range(rung.num_required_trial - 1): + for i in range(rung.num_required_trial - 1): trial = AshaTrial(**good_trial_args) - register_scores_to_trial(trial, list(range(rung.resource))) + register_scores_to_trial(trial, [val for val in range(rung.resource)]) rung.add_new_trial(trial) assert rung.get_trial_to_promote() is None @@ -292,12 +321,13 @@ def test_get_trial_to_promote_not_asha(self, rung, good_trial_args): best_trial.rung += 1 assert rung.get_trial_to_promote(False) is None + @e2e_pytest_component def test_get_trial_to_promote_asha(self, rung, good_trial_args): num_promoteable = rung._num_required_trial // rung._reduction_factor - for _ in range(num_promoteable // rung._reduction_factor): + for i in range(num_promoteable // rung._reduction_factor): for _ in range(rung._reduction_factor): trial = AshaTrial(**good_trial_args) - register_scores_to_trial(trial, list(range(rung.resource))) + register_scores_to_trial(trial, [val for val in range(rung.resource)]) rung.add_new_trial(trial) assert rung.get_trial_to_promote(True) is not None @@ -305,14 +335,15 @@ def test_get_trial_to_promote_asha(self, rung, good_trial_args): best_trial.rung += 1 assert rung.get_trial_to_promote(True) is None + @e2e_pytest_component def test_get_trial_to_promote_not_running(self, rung, good_trial_args): - for _ in range(rung.num_required_trial): + for i in range(rung.num_required_trial): trial = AshaTrial(**good_trial_args) rung.add_new_trial(trial) - for _ in range(rung.num_required_trial): + for i in range(rung.num_required_trial): trial = rung.get_next_trial() - register_scores_to_trial(trial, list(range(rung.resource))) + register_scores_to_trial(trial, [val for val in range(rung.resource)]) trial.status = TrialStatus.RUNNING promoted_trial = rung.get_trial_to_promote() @@ -322,6 +353,7 @@ def test_get_trial_to_promote_not_running(self, rung, good_trial_args): promoted_trial = rung.get_trial_to_promote() assert promoted_trial.status != TrialStatus.RUNNING + @e2e_pytest_component def test_get_next_trial(self, rung, good_trial_args): trial = AshaTrial(**good_trial_args) rung.add_new_trial(trial) @@ -335,64 +367,73 @@ def test_get_next_trial(self, rung, good_trial_args): assert new_trial is None # finished trial isn't provided - register_scores_to_trial(trial, list(range(trial.iteration))) + register_scores_to_trial(trial, [i for i in range(trial.iteration)]) trial.status = TrialStatus.STOP new_trial = rung.get_next_trial() assert new_trial is None + @e2e_pytest_component def test_get_next_trial_stopped_in_progress(self, rung, trial): rung.add_new_trial(trial) - register_scores_to_trial(trial, list(range(trial.iteration - 1))) + register_scores_to_trial(trial, [i for i in range(trial.iteration - 1)]) undone_trial = rung.get_next_trial() assert trial == undone_trial class TestBracket: + @e2e_pytest_component def test_init(self, good_bracket_args): Bracket(**good_bracket_args) + @e2e_pytest_component def test_init_minimum_is_negative(self, good_bracket_args): wrong_args = good_bracket_args wrong_args["minimum_resource"] = -1 - with pytest.raises(ValueError, match="should be positive"): + with pytest.raises(ValueError): Bracket(**wrong_args) + @e2e_pytest_component @pytest.mark.parametrize("reduction_factor", [-10, 0, 1]) def test_init_wrong_reduction_factor(self, good_bracket_args, reduction_factor): wrong_args = good_bracket_args wrong_args["reduction_factor"] = reduction_factor - with pytest.raises(ValueError, match="reduction_factor should be"): + with pytest.raises(ValueError): Bracket(**wrong_args) + @e2e_pytest_component def test_init_wrong_mode_val(self, good_bracket_args): wrong_args = good_bracket_args wrong_args["mode"] = "wrong" - with pytest.raises(ValueError, match="mode should be"): + with pytest.raises(ValueError): Bracket(**wrong_args) + @e2e_pytest_component def test_init_minimum_val_is_bigger_than_maximum_val(self, good_bracket_args): wrong_args = good_bracket_args wrong_args["minimum_resource"] = 100 wrong_args["maximum_resource"] = 10 - with pytest.raises(ValueError, match="should be bigger"): + with pytest.raises(ValueError): Bracket(**wrong_args) + @e2e_pytest_component def test_init_empty_hyper_parameter_configurations(self, good_bracket_args): wrong_args = good_bracket_args wrong_args["hyper_parameter_configurations"] = [] - with pytest.raises(ValueError, match="not enough"): + with pytest.raises(ValueError): Bracket(**wrong_args) + @e2e_pytest_component def test_max_rung(self, good_bracket_args): bracket = Bracket(**good_bracket_args) expected_val = math.ceil( math.log( good_bracket_args["maximum_resource"] / good_bracket_args["minimum_resource"], good_bracket_args["reduction_factor"], - ), + ) ) assert bracket.max_rung == expected_val + @e2e_pytest_component def test_calcuate_max_rung_idx(self): minimum_resource = 1 maximum_resource = 100 @@ -401,14 +442,15 @@ def test_calcuate_max_rung_idx(self): expected_val = math.ceil(math.log(maximum_resource / minimum_resource, reduction_factor)) assert Bracket.calcuate_max_rung_idx(minimum_resource, maximum_resource, reduction_factor) == expected_val + @e2e_pytest_component @pytest.mark.parametrize( - ("minimum_resource", "maximum_resource", "reduction_factor"), - [(-1, 100, 3), (1, -3, 3), (1, 100, -2), (10, 3, 3)], + "minimum_resource,maximum_resource,reduction_factor", [(-1, 100, 3), (1, -3, 3), (1, 100, -2), (10, 3, 3)] ) def test_calcuate_max_rung_with_wrong_input(self, minimum_resource, maximum_resource, reduction_factor): - with pytest.raises(ValueError): # noqa: PT011 + with pytest.raises(ValueError): Bracket.calcuate_max_rung_idx(minimum_resource, maximum_resource, reduction_factor) + @e2e_pytest_component def test_release_new_trial(self, bracket): while True: new_trial = bracket.get_next_trial() @@ -418,6 +460,7 @@ def test_release_new_trial(self, bracket): assert new_trial.bracket == bracket.id assert new_trial.rung == 0 + @e2e_pytest_component def test_promote_trial_if_available_asha(self, good_bracket_args): reduction_factor = good_bracket_args["reduction_factor"] bracket = Bracket(**good_bracket_args) @@ -429,6 +472,7 @@ def test_promote_trial_if_available_asha(self, good_bracket_args): trial = bracket.get_next_trial() assert trial.rung == 1 + @e2e_pytest_component def test_promote_trial_if_available_sha(self, good_bracket_args): good_bracket_args["asynchronous_sha"] = False bracket = Bracket(**good_bracket_args) @@ -449,6 +493,7 @@ def test_promote_trial_if_available_sha(self, good_bracket_args): trial = bracket.get_next_trial() assert trial.rung == 1 + @e2e_pytest_component def test_get_next_trial(self, bracket): while not bracket.is_done(): trial = bracket.get_next_trial() @@ -460,6 +505,7 @@ def test_get_next_trial(self, bracket): assert bracket.is_done() + @e2e_pytest_component def test_get_next_trial_if_trial_is_always_running(self, bracket): trial_arr = [] while True: @@ -476,6 +522,7 @@ def test_get_next_trial_if_trial_is_always_running(self, bracket): trial = bracket.get_next_trial() assert trial is None + @e2e_pytest_component def test_is_done(self, bracket): while True: trial = bracket.get_next_trial() @@ -486,6 +533,7 @@ def test_is_done(self, bracket): assert bracket.is_done() + @e2e_pytest_component @pytest.mark.parametrize("num", [1, 5, 15]) def test_num_trial_is_not_enough(self, good_bracket_args, num): wrong_bracket_args = good_bracket_args @@ -493,16 +541,16 @@ def test_num_trial_is_not_enough(self, good_bracket_args, num): :num ] - with pytest.raises(ValueError, match="not enough"): + with pytest.raises(ValueError): Bracket(**wrong_bracket_args) + @e2e_pytest_component def test_get_best_trial(self, bracket): expected_score = 999999 trial = bracket.get_next_trial() expected_trial_id = trial.id register_scores_to_trial( - trial, - [expected_score for _ in range(bracket._rungs[trial.rung].resource - trial.get_progress())], + trial, [expected_score for _ in range(bracket._rungs[trial.rung].resource - trial.get_progress())] ) while True: trial = bracket.get_next_trial() @@ -510,23 +558,25 @@ def test_get_best_trial(self, bracket): break register_scores_to_trial( - trial, - [score for score in range(bracket._rungs[trial.rung].resource - trial.get_progress())], # noqa: C416 + trial, [score for score in range(bracket._rungs[trial.rung].resource - trial.get_progress())] ) trial = bracket.get_best_trial() assert trial.get_best_score(bracket._mode) == expected_score assert trial.id == expected_trial_id + @e2e_pytest_component def test_get_best_trial_given_absent_trial(self, bracket): assert bracket.get_best_trial() is None + @e2e_pytest_component def test_get_best_trial_with_one_unfinished_trial(self, bracket): trial = bracket.get_next_trial() register_scores_to_trial(trial, [1]) best_trial = bracket.get_best_trial() assert trial == best_trial - def test_save_results(self, good_bracket_args, tmp_path: Path): + @e2e_pytest_component + def test_save_results(self, good_bracket_args, tmp_path): trial_num = len(good_bracket_args["hyper_parameter_configurations"]) bracket = Bracket(**good_bracket_args) while True: @@ -535,13 +585,12 @@ def test_save_results(self, good_bracket_args, tmp_path: Path): break register_scores_to_trial( - trial, - [score for score in range(bracket._rungs[trial.rung].resource - trial.get_progress())], # noqa: C416 + trial, [score for score in range(bracket._rungs[trial.rung].resource - trial.get_progress())] ) bracket.save_results(tmp_path) - with (tmp_path / "rung_status.json").open() as f: + with open(osp.join(tmp_path, "rung_status.json"), "r") as f: result = json.load(f) assert result["minimum_resource"] == good_bracket_args["minimum_resource"] @@ -554,8 +603,9 @@ def test_save_results(self, good_bracket_args, tmp_path: Path): for rung_status in result["rung_status"]: assert rung_status["num_trial"] == rung_status["num_required_trial"] for i in range(trial_num): - assert (tmp_path / f"{i}.json").exists() + assert osp.exists(osp.join(tmp_path, f"{i}.json")) + @e2e_pytest_component def test_print_result(self, bracket): while True: trial = bracket.get_next_trial() @@ -563,15 +613,16 @@ def test_print_result(self, bracket): break register_scores_to_trial( - trial, - [score for score in range(bracket._rungs[trial.rung].resource - trial.get_progress())], # noqa: C416 + trial, [score for score in range(bracket._rungs[trial.rung].resource - trial.get_progress())] ) bracket.print_result() + @e2e_pytest_component def test_print_result_without_train(self, bracket): bracket.print_result() + @e2e_pytest_component def test_report_trial_exit_abnormally(self, bracket): trial = bracket.get_next_trial() trial.register_score(score=0, resource=trial.iteration - 0.1) @@ -580,35 +631,42 @@ def test_report_trial_exit_abnormally(self, bracket): class TestHyperBand: + @e2e_pytest_component def test_init(self, good_hyperband_args): HyperBand(**good_hyperband_args) + @e2e_pytest_component @pytest.mark.parametrize("minimum_resource", [-10, 0]) def test_init_not_postive_maximum_resource(self, good_hyperband_args, minimum_resource): wrong_arg = good_hyperband_args wrong_arg["minimum_resource"] = minimum_resource - with pytest.raises(ValueError): # noqa: PT011 + with pytest.raises(ValueError): HyperBand(**wrong_arg) + @e2e_pytest_component @pytest.mark.parametrize("reduction_factor", [-10, 0, 1]) def test_init_wrong_reduction_factor(self, good_hyperband_args, reduction_factor): wrong_arg = good_hyperband_args wrong_arg["reduction_factor"] = reduction_factor - with pytest.raises(ValueError): # noqa: PT011 + with pytest.raises(ValueError): HyperBand(**wrong_arg) + @e2e_pytest_component def test_init_maximum_is_same_with_minimum(self, good_hyperband_args): good_hyperband_args["maximum_resource"] = good_hyperband_args["minimum_resource"] HyperBand(**good_hyperband_args) + @e2e_pytest_component def test_init_no_minimum_resource(self, good_hyperband_args): del good_hyperband_args["minimum_resource"] HyperBand(**good_hyperband_args) + @e2e_pytest_component def test_init_no_maximum_resource(self, good_hyperband_args): del good_hyperband_args["maximum_resource"] HyperBand(**good_hyperband_args) + @e2e_pytest_component @pytest.mark.parametrize("num", [1, 10]) def test_make_new_hyper_parameter_configs(self, good_hyperband_args, num): hb = HyperBand(**good_hyperband_args) @@ -623,6 +681,7 @@ def test_make_new_hyper_parameter_configs(self, good_hyperband_args, num): assert 100 <= trial.configuration["hp2"] <= 1000 assert trial.configuration["hp2"] % 2 == 0 + @e2e_pytest_component def test_get_next_sample(self, hyper_band): while True: trial = hyper_band.get_next_sample() @@ -633,6 +692,7 @@ def test_get_next_sample(self, hyper_band): assert hyper_band.is_done() + @e2e_pytest_component def test_get_next_sample_without_train(self, hyper_band): while True: trial = hyper_band.get_next_sample() @@ -642,6 +702,7 @@ def test_get_next_sample_without_train(self, hyper_band): assert not hyper_band.is_done() + @e2e_pytest_component def test_report_score(self, hyper_band): trial = hyper_band.get_next_sample() score = 100 @@ -649,16 +710,18 @@ def test_report_score(self, hyper_band): hyper_band.report_score(score, resource, trial.id) assert trial.score[resource] == score + @e2e_pytest_component def test_report_score_trial_done(self, hyper_band): trial = hyper_band.get_next_sample() hyper_band.report_score(100, 0.1, trial.id) hyper_band.report_score(0, 0, trial.id, done=True) assert trial.is_done() + @e2e_pytest_component def test_get_best_config(self, hyper_band): max_score = 9999999 trial = hyper_band.get_next_sample() - expected_configuration = {"id": trial.id, "configuration": trial.configuration} + expected_configuration = {"id": trial.id, "config": trial.configuration} hyper_band.report_score(score=max_score, resource=trial.iteration, trial_id=trial.id, done=False) hyper_band.report_score(score=max_score, resource=trial.iteration, trial_id=trial.id, done=True) while True: @@ -672,15 +735,18 @@ def test_get_best_config(self, hyper_band): assert best_config == expected_configuration + @e2e_pytest_component def test_get_best_config_before_train(self, hyper_band): best_config = hyper_band.get_best_config() assert best_config is None + @e2e_pytest_component def test_train_option_exists(self, hyper_band): trial = hyper_band.get_next_sample() train_config = trial.get_train_configuration() assert "subset_ratio" in train_config["train_environment"] + @e2e_pytest_component def test_prior_hyper_parameters(self, good_hyperband_args): prior1 = {"hp1": 1, "hp2": 2} prior2 = {"hp1": 100, "hp2": 200} @@ -697,6 +763,7 @@ def test_prior_hyper_parameters(self, good_hyperband_args): assert first_trial.configuration == prior1 assert second_trial.configuration == prior2 + @e2e_pytest_component @pytest.mark.parametrize("num_prior_param", [10, 100, 1000]) def test_many_prior_hyper_parameters(self, good_hyperband_args, num_prior_param): prior_hyper_parameters = [] @@ -723,6 +790,7 @@ def test_many_prior_hyper_parameters(self, good_hyperband_args, num_prior_param) assert i == num_prior_param or hyper_band.is_done() + @e2e_pytest_component def test_auto_config_decrease(self, good_hyperband_args): full_train_resource = good_hyperband_args["maximum_resource"] expected_time_ratio = 4 @@ -744,6 +812,7 @@ def test_auto_config_decrease(self, good_hyperband_args): maximum_resource = full_train_resource * expected_time_ratio * hyperband.acceptable_additional_time_ratio assert maximum_resource >= total_resource >= maximum_resource * 0.8 + @e2e_pytest_component def test_auto_config_increase(self, good_hyperband_args): full_train_resource = good_hyperband_args["maximum_resource"] expected_time_ratio = 100 @@ -765,6 +834,7 @@ def test_auto_config_increase(self, good_hyperband_args): maximum_resource = full_train_resource * expected_time_ratio * hyperband.acceptable_additional_time_ratio assert maximum_resource >= total_resource >= maximum_resource * 0.8 + @e2e_pytest_component def test_asynchronous_bracket(self, hyper_band): bracket_id_arr = [] while True: @@ -778,6 +848,7 @@ def test_asynchronous_bracket(self, hyper_band): assert len(bracket_id_arr) > 1 + @e2e_pytest_component def test_synchronous_bracket(self, good_hyperband_args): good_hyperband_args["asynchronous_bracket"] = False hyper_band = HyperBand(**good_hyperband_args) @@ -793,6 +864,7 @@ def test_synchronous_bracket(self, good_hyperband_args): assert len(bracket_id_arr) == 1 + @e2e_pytest_component def test_print_result(self, hyper_band): while not hyper_band.is_done(): trial = hyper_band.get_next_sample() @@ -804,15 +876,18 @@ def test_print_result(self, hyper_band): hyper_band.print_result() + @e2e_pytest_component def test_print_result_without_train(self, hyper_band): hyper_band.print_result() + @e2e_pytest_component def test_report_trial_exit_abnormally(self, hyper_band): trial = hyper_band.get_next_sample() hyper_band.report_score(score=50, resource=trial.iteration - 0.1, trial_id=trial.id, done=False) new_trial = hyper_band.get_next_sample() assert trial.id == new_trial.id + @e2e_pytest_component def test_absence_minimum_resource(self, good_hyperband_args): del good_hyperband_args["minimum_resource"] hyper_band = HyperBand(**good_hyperband_args) @@ -828,25 +903,29 @@ def test_absence_minimum_resource(self, good_hyperband_args): hyper_band.report_score(score=1, resource=trial.iteration, trial_id=trial.id) s_max = math.floor( - math.log(hyper_band.maximum_resource / first_validation, good_hyperband_args["reduction_factor"]), + math.log(hyper_band.maximum_resource / first_validation, good_hyperband_args["reduction_factor"]) ) expected_min = hyper_band.maximum_resource * (good_hyperband_args["reduction_factor"] ** -s_max) assert min(iter_set) == expected_min + @e2e_pytest_component @pytest.mark.parametrize("num_trial_to_estimate", [10, 30, 100]) def test_without_maximum_resource(self, good_hyperband_args, num_trial_to_estimate): del good_hyperband_args["maximum_resource"] max_validation = 120 hyper_band = HyperBand(**good_hyperband_args) - trials_to_estimate = [hyper_band.get_next_sample() for _ in range(num_trial_to_estimate)] + trials_to_estimate = [] + for _ in range(num_trial_to_estimate): + trials_to_estimate.append(hyper_band.get_next_sample()) + for trial in reversed(trials_to_estimate[1:]): assert trial.iteration == good_hyperband_args["minimum_resource"] - for i in range(1, trial.iteration + 1): - if hyper_band.report_score(score=1, resource=i, trial_id=trial.id) == TrialStatus.STOP: + for iter in range(1, trial.iteration + 1): + if hyper_band.report_score(score=1, resource=iter, trial_id=trial.id) == TrialStatus.STOP: break - assert i == good_hyperband_args["minimum_resource"] + assert iter == good_hyperband_args["minimum_resource"] first_trial = trials_to_estimate[0] hyper_band.report_score(score=1, resource=max_validation, trial_id=first_trial.id) @@ -855,6 +934,7 @@ def test_without_maximum_resource(self, good_hyperband_args, num_trial_to_estima assert hyper_band.maximum_resource == max_validation assert first_trial.estimating_max_resource + @e2e_pytest_component @pytest.mark.parametrize("num_trial_to_estimate", [10, 30, 100]) def test_auto_config_decrease_without_maximum_resource(self, good_hyperband_args, num_trial_to_estimate): """ @@ -877,8 +957,8 @@ def test_auto_config_decrease_without_maximum_resource(self, good_hyperband_args for trial in reversed(trials_to_estimate[1:]): assert trial.iteration == good_hyperband_args["minimum_resource"] - for i in range(1, trial.iteration + 1): - if hyperband.report_score(score=1, resource=i, trial_id=trial.id) == TrialStatus.STOP: + for iter in range(1, trial.iteration + 1): + if hyperband.report_score(score=1, resource=iter, trial_id=trial.id) == TrialStatus.STOP: break first_trial = trials_to_estimate[0] @@ -908,6 +988,7 @@ def test_auto_config_decrease_without_maximum_resource(self, good_hyperband_args assert maximum_resource >= total_resource >= maximum_resource * 0.8 + @e2e_pytest_component @pytest.mark.parametrize("num_trial_to_estimate", [10, 30, 100]) def test_auto_config_increase_without_maximum_resource(self, good_hyperband_args, num_trial_to_estimate): del good_hyperband_args["maximum_resource"] @@ -923,8 +1004,8 @@ def test_auto_config_increase_without_maximum_resource(self, good_hyperband_args for trial in reversed(trials_to_estimate[1:]): assert trial.iteration == good_hyperband_args["minimum_resource"] - for i in range(1, trial.iteration + 1): - if hyperband.report_score(score=1, resource=i, trial_id=trial.id) == TrialStatus.STOP: + for iter in range(1, trial.iteration + 1): + if hyperband.report_score(score=1, resource=iter, trial_id=trial.id) == TrialStatus.STOP: break first_trial = trials_to_estimate[0] @@ -955,6 +1036,7 @@ def test_auto_config_increase_without_maximum_resource(self, good_hyperband_args ) assert maximum_resource >= total_resource >= maximum_resource * 0.8 + @e2e_pytest_component @pytest.mark.parametrize("num_trial_to_estimate", [10, 30, 100]) def test_without_minimum_maximum_resource(self, good_hyperband_args, num_trial_to_estimate): del good_hyperband_args["minimum_resource"] @@ -970,8 +1052,8 @@ def test_without_minimum_maximum_resource(self, good_hyperband_args, num_trial_t trials_to_estimate.append(trial) for trial in trials_to_estimate[1:]: - for i in range(validation_interval, trial.iteration + 1, validation_interval): - if hyper_band.report_score(score=1, resource=i, trial_id=trial.id) == TrialStatus.STOP: + for iter in range(validation_interval, trial.iteration + 1, validation_interval): + if hyper_band.report_score(score=1, resource=iter, trial_id=trial.id) == TrialStatus.STOP: break hyper_band.report_score(score=1, resource=max_validation, trial_id=first_trial.id) @@ -984,7 +1066,7 @@ def test_without_minimum_maximum_resource(self, good_hyperband_args, num_trial_t hyper_band.report_score(score=1, resource=trial.iteration, trial_id=trial.id) s_max = math.floor( - math.log(hyper_band.maximum_resource / validation_interval, good_hyperband_args["reduction_factor"]), + math.log(hyper_band.maximum_resource / validation_interval, good_hyperband_args["reduction_factor"]) ) expected_min = hyper_band.maximum_resource * (good_hyperband_args["reduction_factor"] ** -s_max) @@ -993,6 +1075,7 @@ def test_without_minimum_maximum_resource(self, good_hyperband_args, num_trial_t assert min(iter_set) == expected_min assert hyper_band.maximum_resource == max_validation + @e2e_pytest_component @pytest.mark.parametrize("expected_time_ratio", [3, 4, 5, 6]) def test_hyperband_without_minimum_resource(self, good_hyperband_args, expected_time_ratio): """ @@ -1006,15 +1089,16 @@ def test_hyperband_without_minimum_resource(self, good_hyperband_args, expected_ val_interval = 3 trial = hyper_band.get_next_sample() - for i in range(val_interval, trial.iteration + 1, val_interval): - score = i + 1 - trial_status = hyper_band.report_score(score, i, trial.id) + for iter in range(val_interval, trial.iteration + 1, val_interval): + score = iter + 1 + trial_status = hyper_band.report_score(score, iter, trial.id) if trial_status == TrialStatus.STOP: break - hyper_band.report_score(score, i, trial.id, True) + hyper_band.report_score(score, iter, trial.id, True) assert trial.get_progress() < trial.iteration + val_interval + @e2e_pytest_component def test_get_done_progress(self, hyper_band: HyperBand): while not hyper_band.is_done(): trial = hyper_band.get_next_sample() @@ -1026,6 +1110,7 @@ def test_get_done_progress(self, hyper_band: HyperBand): assert hyper_band.get_progress() == 1 + @e2e_pytest_component @pytest.mark.parametrize("expected_time_ratio", [3, 4, 5, 6]) def test_get_progress_with_expected_time_ratio(self, good_hyperband_args, expected_time_ratio): good_hyperband_args["expected_time_ratio"] = expected_time_ratio @@ -1045,6 +1130,7 @@ def test_get_progress_with_expected_time_ratio(self, good_hyperband_args, expect assert math.isclose(hyper_band.get_progress(), trial.get_progress() / expected_total_resource) + @e2e_pytest_component def test_get_progress_with_out_expected_time_ratio(self, good_hyperband_args): hyper_band = HyperBand(**good_hyperband_args) full_asha_resource = _get_full_asha_resource( @@ -1061,11 +1147,9 @@ def test_get_progress_with_out_expected_time_ratio(self, good_hyperband_args): def _get_full_asha_resource( - maximum_resource: float | int, - minimum_resource: float | int, - reduction_factor: int, -) -> int | float: - total_resource: int | float = 0 + maximum_resource: Union[float, int], minimum_resource: Union[float, int], reduction_factor: int +) -> Union[int, float]: + total_resource: Union[int, float] = 0 s_max = math.floor(math.log(maximum_resource / minimum_resource, reduction_factor)) for idx in range(s_max + 1): num_max_rung_trials = math.floor((s_max + 1) / (idx + 1)) @@ -1075,11 +1159,11 @@ def _get_full_asha_resource( def _calculate_bracket_resource( - maximum_resource: float | int, - reduction_factor: float | int, + maximum_resource: Union[float, int], + reduction_factor: Union[float, int], num_max_rung_trials: int, bracket_index: int, -) -> int | float: +) -> Union[int, float]: """Calculate how much resource is needed for the bracket given that resume is available.""" num_trial = num_max_rung_trials * (reduction_factor**bracket_index) minimum_resource = maximum_resource * (reduction_factor**-bracket_index) diff --git a/tests/unit/hpo/test_resource_manager.py b/tests/unit/hpo/test_resource_manager.py index f1688931750..05df5cfe39e 100644 --- a/tests/unit/hpo/test_resource_manager.py +++ b/tests/unit/hpo/test_resource_manager.py @@ -1,32 +1,40 @@ import pytest + +from otx.hpo import resource_manager as target_file from otx.hpo.resource_manager import ( CPUResourceManager, GPUResourceManager, + XPUResourceManager, _remove_none_from_dict, + _cvt_comma_delimited_str_to_list, get_resource_manager, ) +from tests.test_suite.e2e_test_system import e2e_pytest_component -@pytest.fixture() +@pytest.fixture def cpu_resource_manager(): return CPUResourceManager(num_parallel_trial=4) -@pytest.fixture() +@pytest.fixture def gpu_resource_manager(): - return GPUResourceManager(num_gpu_for_single_trial=1, available_gpu="0,1,2,3") + return GPUResourceManager(num_devices_per_trial=1, available_devices="0,1,2,3") class TestCPUResourceManager: + @e2e_pytest_component @pytest.mark.parametrize("num_parallel_trial", [1, 5, 10]) def test_init(self, num_parallel_trial): CPUResourceManager(num_parallel_trial) + @e2e_pytest_component @pytest.mark.parametrize("num_parallel_trial", [-1, 0]) def test_init_with_not_positive_num_parallel_trial(self, num_parallel_trial): - with pytest.raises(ValueError): # noqa: PT011 + with pytest.raises(ValueError): CPUResourceManager(num_parallel_trial) + @e2e_pytest_component def test_reserve_resource(self, cpu_resource_manager): num_parallel_trial = cpu_resource_manager._num_parallel_trial @@ -36,18 +44,22 @@ def test_reserve_resource(self, cpu_resource_manager): for i in range(10): assert cpu_resource_manager.reserve_resource(i) is None + @e2e_pytest_component def test_reserve_resource_reserved_already(self, cpu_resource_manager): cpu_resource_manager.reserve_resource(0) with pytest.raises(RuntimeError): cpu_resource_manager.reserve_resource(0) + @e2e_pytest_component def test_release_resource(self, cpu_resource_manager): cpu_resource_manager.reserve_resource(1) cpu_resource_manager.release_resource(1) + @e2e_pytest_component def test_release_unreserved_resource(self, cpu_resource_manager): cpu_resource_manager.release_resource(1) + @e2e_pytest_component def test_have_available_resource(self, cpu_resource_manager): num_parallel_trial = cpu_resource_manager._num_parallel_trial @@ -59,102 +71,157 @@ def test_have_available_resource(self, cpu_resource_manager): class TestGPUResourceManager: - @pytest.fixture(autouse=True) - def setupt_test(self, mocker): - mock_torch_cuda = mocker.patch("otx.hpo.resource_manager.torch.cuda") - mock_torch_cuda.is_available.return_value = True - mock_torch_cuda.device_count.return_value = 4 - + @e2e_pytest_component def test_init(self): - GPUResourceManager(num_gpu_for_single_trial=1, available_gpu="0,1,2") + GPUResourceManager(num_devices_per_trial=1, available_devices="0,1,2") - @pytest.mark.parametrize("num_gpu_for_single_trial", [-1, 0]) - def test_init_not_positive_num_gpu(self, num_gpu_for_single_trial): - with pytest.raises(ValueError): # noqa: PT011 - GPUResourceManager(num_gpu_for_single_trial=num_gpu_for_single_trial) + @e2e_pytest_component + @pytest.mark.parametrize("num_devices_per_trial", [-1, 0]) + def test_init_not_positive_num_gpu(self, num_devices_per_trial): + with pytest.raises(ValueError): + GPUResourceManager(num_devices_per_trial=num_devices_per_trial) - @pytest.mark.parametrize("available_gpu", [",", "a,b", "0,a", ""]) - def test_init_wrong_available_gpu_value(self, available_gpu): - with pytest.raises(ValueError): # noqa: PT011 - GPUResourceManager(available_gpu=available_gpu) + @e2e_pytest_component + @pytest.mark.parametrize("available_devices", [",", "a,b", "0,a", ""]) + def test_init_wrong_available_devices_value(self, available_devices): + with pytest.raises(ValueError): + GPUResourceManager(available_devices=available_devices) + @e2e_pytest_component def test_reserve_resource(self): - num_gpu_for_single_trial = 2 + num_devices_per_trial = 2 + num_gpus = 8 + max_parallel = num_gpus // num_devices_per_trial gpu_resource_manager = GPUResourceManager( - num_gpu_for_single_trial=num_gpu_for_single_trial, - available_gpu=",".join([str(val) for val in range(8)]), + num_devices_per_trial=num_devices_per_trial, + available_devices=",".join([str(val) for val in range(num_gpus)]), ) - num_gpus = len(gpu_resource_manager._available_gpu) - max_parallel = num_gpus // num_gpu_for_single_trial + num_gpus = len(gpu_resource_manager._available_devices) for i in range(max_parallel): env = gpu_resource_manager.reserve_resource(i) assert env is not None assert "CUDA_VISIBLE_DEVICES" in env - assert len(env["CUDA_VISIBLE_DEVICES"].split(",")) == num_gpu_for_single_trial + assert len(env["CUDA_VISIBLE_DEVICES"].split(",")) == num_devices_per_trial for i in range(max_parallel, max_parallel + 10): assert gpu_resource_manager.reserve_resource(i) is None + @e2e_pytest_component def test_reserve_resource_reserved_already(self, gpu_resource_manager): gpu_resource_manager.reserve_resource(0) with pytest.raises(RuntimeError): gpu_resource_manager.reserve_resource(0) + @e2e_pytest_component def test_release_resource(self, gpu_resource_manager): gpu_resource_manager.reserve_resource(1) gpu_resource_manager.release_resource(1) + @e2e_pytest_component def test_release_unreserved_resource(self, gpu_resource_manager): gpu_resource_manager.release_resource(1) + @e2e_pytest_component def test_have_available_resource(self): - num_gpu_for_single_trial = 2 + num_devices_per_trial = 2 + num_gpus = 8 + max_parallel = num_gpus // num_devices_per_trial gpu_resource_manager = GPUResourceManager( - num_gpu_for_single_trial=num_gpu_for_single_trial, - available_gpu=",".join([str(val) for val in range(8)]), + num_devices_per_trial=num_devices_per_trial, + available_devices=",".join([str(val) for val in range(num_gpus)]), ) - num_gpus = len(gpu_resource_manager._available_gpu) - max_parallel = num_gpus // num_gpu_for_single_trial + num_gpus = len(gpu_resource_manager._available_devices) for i in range(max_parallel): assert gpu_resource_manager.have_available_resource() gpu_resource_manager.reserve_resource(i) - for _i in range(max_parallel, max_parallel + 10): + for i in range(max_parallel, max_parallel + 10): assert not gpu_resource_manager.have_available_resource() +class TestXPUResourceManager: + @e2e_pytest_component + @pytest.fixture(autouse=True) + def setup(self, mocker): + self.mock_os = mocker.patch.object(target_file, "os") + self.mock_torch = mocker.patch.object(target_file, "torch") + + def test_init_env_var_exist(self): + self.mock_os.getenv.return_value = "level_zero:1,2" + resource_manager = XPUResourceManager(num_devices_per_trial=1) + for i in range(2): + resource_manager.reserve_resource(i) + assert resource_manager.reserve_resource(3) is None + + def test_init_no_env_var(self): + self.mock_torch.xpu.device_count.return_value = 4 + resource_manager = XPUResourceManager(num_devices_per_trial=1) + for i in range(4): + resource_manager.reserve_resource(i) + assert resource_manager.reserve_resource(3) is None + + def test_reserve_resource(self): + self.mock_torch.xpu.device_count.return_value = 4 + resource_manager = XPUResourceManager(num_devices_per_trial=1) + + for i in range(4): + env = resource_manager.reserve_resource(i) + assert env is not None + assert "ONEAPI_DEVICE_SELECTOR" in env + assert env["ONEAPI_DEVICE_SELECTOR"] == f"level_zero:{i}" + + for i in range(4, 10): + assert resource_manager.reserve_resource(i) is None + + +@e2e_pytest_component def test_get_resource_manager_cpu(): manager = get_resource_manager(resource_type="cpu", num_parallel_trial=4) assert isinstance(manager, CPUResourceManager) +@e2e_pytest_component def test_get_resource_manager_gpu(mocker): - mocker.patch("otx.hpo.resource_manager.torch.cuda.is_available", return_value=True) - num_gpu_for_single_trial = 1 - available_gpu = "0,1,2,3" + mock_torch = mocker.patch.object(target_file, "torch") + mock_torch.cuda.is_available.return_value = True + num_devices_per_trial = 1 + available_devices = "0,1,2,3" manager = get_resource_manager( - resource_type="gpu", - num_gpu_for_single_trial=num_gpu_for_single_trial, - available_gpu=available_gpu, + resource_type="gpu", num_devices_per_trial=num_devices_per_trial, available_devices=available_devices ) assert isinstance(manager, GPUResourceManager) +@e2e_pytest_component def test_get_resource_manager_wrong_resource_type(): - with pytest.raises(ValueError, match="Available resource type"): + with pytest.raises(ValueError): get_resource_manager("wrong") +@e2e_pytest_component def test_get_resource_manager_gpu_without_available_gpu(mocker): - mocker.patch("otx.hpo.resource_manager.torch.cuda.is_available", return_value=False) + mock_is_available = mocker.patch("otx.hpo.resource_manager.torch.cuda.is_available") + mock_is_available.return_value = False manager = get_resource_manager("gpu") assert isinstance(manager, CPUResourceManager) +@e2e_pytest_component def test_remove_none_from_dict(): some_dict = {"a": 1, "b": None} ret = _remove_none_from_dict(some_dict) assert ret == {"a": 1} + + +@e2e_pytest_component +def test_cvt_comma_delimited_str_to_list(): + assert _cvt_comma_delimited_str_to_list("1,3,5") == [1, 3, 5] + + +@e2e_pytest_component +def test_cvt_comma_delimited_str_to_list_wrong_format(): + with pytest.raises(ValueError): + _cvt_comma_delimited_str_to_list("a,3,5") diff --git a/tests/unit/hpo/test_search_space.py b/tests/unit/hpo/test_search_space.py index d72b76c1eba..206f1d60678 100644 --- a/tests/unit/hpo/test_search_space.py +++ b/tests/unit/hpo/test_search_space.py @@ -2,7 +2,9 @@ import math import pytest + from otx.hpo.search_space import SearchSpace, SingleSearchSpace +from tests.test_suite.e2e_test_system import e2e_pytest_component ALL_TYPE = ["uniform", "loguniform", "quniform", "qloguniform", "choice"] NOT_CATEGORICAL_TYPE = ["uniform", "loguniform", "quniform", "qloguniform"] @@ -86,18 +88,17 @@ def get_wrong_arg(original_arg, attr_names, values, errors): def make_arg_minmax_wrong(arg): args = [] # value is None - args.extend([get_wrong_arg(arg, attr_name, None, (ValueError, TypeError)) for attr_name in ["min", "max"]]) + for attr_name in ["min", "max"]: + args.append(get_wrong_arg(arg, attr_name, None, (ValueError, TypeError))) # min is greater or same than max - args.extend( - [get_wrong_arg(arg, ["min", "max"], val, ValueError) for val in [(3, 1), (5.5, 2.3), (1, 1), (2.0, 2.0)]], - ) + for val in [(3, 1), (5.5, 2.3), (1, 1), (2.0, 2.0)]: + args.append(get_wrong_arg(arg, ["min", "max"], val, ValueError)) # value is minus although using log scale if "log_base" in arg: - args.extend( - [get_wrong_arg(arg, ["min", "max"], val, ValueError) for val in [(-20, -12), (-12.124, 10), (0, 3)]], - ) + for val in [(-20, -12), (-12.124, 10), (0, 3)]: + args.append(get_wrong_arg(arg, ["min", "max"], val, ValueError)) return args @@ -115,8 +116,13 @@ def make_arg_step_wrong(arg): def make_arg_logbase_wrong(arg): + args = [] + # too small value - return [get_wrong_arg(arg, "log_base", val, ValueError) for val in [1, 0, -1]] + for val in [1, 0, -1]: + args.append(get_wrong_arg(arg, "log_base", val, ValueError)) + + return args def make_arg_choicelist_wrong(arg): @@ -124,27 +130,36 @@ def make_arg_choicelist_wrong(arg): # vlaue is None args.append(get_wrong_arg(arg, "choice_list", None, (TypeError, ValueError))) - # few elements - args.extend([get_wrong_arg(arg, "choice_list", val, ValueError) for val in [[], [1]]]) + + for val in [[], [1]]: + args.append(get_wrong_arg(arg, "choice_list", val, ValueError)) return args def make_arg_type_wrong(arg): - return [get_wrong_arg(arg, "type", val, ValueError) for val in ["wrong_type", 12, 1.24, [1, 2]]] + args = [] + + # wrong type + for val in ["wrong_type", 12, 1.24, [1, 2]]: + args.append(get_wrong_arg(arg, "type", val, ValueError)) + + return args class TestSingleSearchSpace: - @pytest.mark.parametrize("hp_type", ALL_TYPE) - def test_init_with_good_input(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", ALL_TYPE) + def test_init_with_good_input(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: SingleSearchSpace(**arg) - @pytest.mark.parametrize("hp_type", NOT_CATEGORICAL_TYPE) - def test_init_wrong_minmax_arg(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", NOT_CATEGORICAL_TYPE) + def test_init_wrong_minmax_arg(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: wrong_args = make_arg_minmax_wrong(arg) for wrong_arg in wrong_args: @@ -153,9 +168,10 @@ def test_init_wrong_minmax_arg(self, hp_type): with pytest.raises(errors): SingleSearchSpace(**wrong_arg) - @pytest.mark.parametrize("hp_type", USE_QUANTIZED_STEP_TYPE) - def test_init_wrong_step_arg(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", USE_QUANTIZED_STEP_TYPE) + def test_init_wrong_step_arg(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: wrong_args = make_arg_step_wrong(arg) for wrong_arg in wrong_args: @@ -165,9 +181,10 @@ def test_init_wrong_step_arg(self, hp_type): with pytest.raises(errors): SingleSearchSpace(**wrong_arg) - @pytest.mark.parametrize("hp_type", USE_LOG_SCALE_TYPE) - def test_init_wrong_log_base_arg(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", USE_LOG_SCALE_TYPE) + def test_init_wrong_log_base_arg(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: wrong_args = make_arg_logbase_wrong(arg) for wrong_arg in wrong_args: @@ -177,6 +194,7 @@ def test_init_wrong_log_base_arg(self, hp_type): with pytest.raises(errors): SingleSearchSpace(**wrong_arg) + @e2e_pytest_component def test_init_wrong_choice_list_arg(self): args = MAKE_GOOD_ARGS["choice"]() for arg in args: @@ -188,9 +206,10 @@ def test_init_wrong_choice_list_arg(self): with pytest.raises(errors): SingleSearchSpace(**wrong_arg) + @e2e_pytest_component def test_init_with_wrong_type(self): - for hp_type in ALL_TYPE: - args = MAKE_GOOD_ARGS[hp_type]() + for type in ALL_TYPE: + args = MAKE_GOOD_ARGS[type]() for arg in args: wrong_args = make_arg_type_wrong(arg) for wrong_arg in wrong_args: @@ -199,19 +218,21 @@ def test_init_with_wrong_type(self): with pytest.raises(errors): SingleSearchSpace(**wrong_arg) + @e2e_pytest_component def test_set_value_normally(self): args = [] - for hp_type in ALL_TYPE: - args.extend(MAKE_GOOD_ARGS[hp_type]()) + for type in ALL_TYPE: + args.extend(MAKE_GOOD_ARGS[type]()) cur_arg = args.pop(0) for new_arg in args: sss = SingleSearchSpace(**cur_arg) sss.set_value(**new_arg) cur_arg = new_arg - @pytest.mark.parametrize("hp_type", NOT_CATEGORICAL_TYPE) - def test_set_value_wrong_minmax_arg(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", NOT_CATEGORICAL_TYPE) + def test_set_value_wrong_minmax_arg(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: sss = SingleSearchSpace(**arg) wrong_args = make_arg_minmax_wrong(arg) @@ -224,9 +245,10 @@ def test_set_value_wrong_minmax_arg(self, hp_type): with pytest.raises(errors): sss.set_value(**wrong_arg) - @pytest.mark.parametrize("hp_type", USE_QUANTIZED_STEP_TYPE) - def test_set_value_wrong_step_arg(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", USE_QUANTIZED_STEP_TYPE) + def test_set_value_wrong_step_arg(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: sss = SingleSearchSpace(**arg) wrong_args = make_arg_step_wrong(arg) @@ -239,9 +261,10 @@ def test_set_value_wrong_step_arg(self, hp_type): with pytest.raises(errors): sss.set_value(**wrong_arg) - @pytest.mark.parametrize("hp_type", USE_LOG_SCALE_TYPE) - def test_set_value_wrong_log_base_arg(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", USE_LOG_SCALE_TYPE) + def test_set_value_wrong_log_base_arg(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: sss = SingleSearchSpace(**arg) wrong_args = make_arg_logbase_wrong(arg) @@ -254,6 +277,7 @@ def test_set_value_wrong_log_base_arg(self, hp_type): with pytest.raises(errors): sss.set_value(**wrong_arg) + @e2e_pytest_component def test_set_value_wrong_choice_list_arg(self): args = MAKE_GOOD_ARGS["choice"]() for arg in args: @@ -268,9 +292,10 @@ def test_set_value_wrong_choice_list_arg(self): with pytest.raises(errors): sss.set_value(**wrong_arg) + @e2e_pytest_component def test_set_value_with_wrong_type(self): - for hp_type in ALL_TYPE: - args = MAKE_GOOD_ARGS[hp_type]() + for type in ALL_TYPE: + args = MAKE_GOOD_ARGS[type]() for arg in args: sss = SingleSearchSpace(**arg) wrong_args = make_arg_type_wrong(arg) @@ -280,6 +305,7 @@ def test_set_value_with_wrong_type(self): with pytest.raises(errors): sss.set_value(**wrong_arg) + @e2e_pytest_component def test_align_min_max_to_choice_list_if_categorical(self): args = MAKE_GOOD_ARGS["choice"]() for arg in args: @@ -290,62 +316,67 @@ def test_align_min_max_to_choice_list_if_categorical(self): assert sss.min == 0 assert sss.max == len(arg["choice_list"]) - 1 - @pytest.mark.parametrize("hp_type", ALL_TYPE) - def test_is_categorical(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", ALL_TYPE) + def test_is_categorical(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: sss = SingleSearchSpace(**arg) - if hp_type in NOT_CATEGORICAL_TYPE: + if type in NOT_CATEGORICAL_TYPE: assert not sss.is_categorical() else: assert sss.is_categorical() - @pytest.mark.parametrize("hp_type", ALL_TYPE) - def test_use_quantized_step(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", ALL_TYPE) + def test_use_quantized_step(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: sss = SingleSearchSpace(**arg) - if hp_type in USE_QUANTIZED_STEP_TYPE: + if type in USE_QUANTIZED_STEP_TYPE: assert sss.use_quantized_step() else: assert not sss.use_quantized_step() - @pytest.mark.parametrize("hp_type", ALL_TYPE) - def test_use_log_scale(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", ALL_TYPE) + def test_use_log_scale(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: sss = SingleSearchSpace(**arg) - if hp_type in USE_LOG_SCALE_TYPE: + if type in USE_LOG_SCALE_TYPE: assert sss.use_log_scale() else: assert not sss.use_log_scale() - @pytest.mark.parametrize("hp_type", ALL_TYPE) - def test_lower_space_upper_space(self, hp_type): - args = MAKE_GOOD_ARGS[hp_type]() + @e2e_pytest_component + @pytest.mark.parametrize("type", ALL_TYPE) + def test_lower_space_upper_space(self, type): + args = MAKE_GOOD_ARGS[type]() for arg in args: sss = SingleSearchSpace(**arg) - if hp_type == "choice": + if type == "choice": assert sss.lower_space() == 0 assert sss.upper_space() == len(sss.choice_list) - 1 else: - min_val = arg["min"] - max_val = arg["max"] - if hp_type in USE_LOG_SCALE_TYPE: + min = arg["min"] + max = arg["max"] + if type in USE_LOG_SCALE_TYPE: log_base = arg["log_base"] - assert sss.lower_space() == math.log(min_val, log_base) - assert sss.upper_space() == math.log(max_val, log_base) + assert sss.lower_space() == math.log(min, log_base) + assert sss.upper_space() == math.log(max, log_base) else: - assert sss.lower_space() == min_val - assert sss.upper_space() == max_val + assert sss.lower_space() == min + assert sss.upper_space() == max - @pytest.mark.parametrize("hp_type", ALL_TYPE) + @e2e_pytest_component + @pytest.mark.parametrize("type", ALL_TYPE) @pytest.mark.parametrize("number", [2.3, 15]) - def test_space_to_real(self, hp_type, number): - args = MAKE_GOOD_ARGS[hp_type]() + def test_space_to_real(self, type, number): + args = MAKE_GOOD_ARGS[type]() for arg in args: sss = SingleSearchSpace(**arg) - if hp_type == "choice": + if type == "choice": choice_list = arg["choice_list"] ret = sss.space_to_real(number) expected_ret = min(max(int(number), 0), len(choice_list) - 1) @@ -354,22 +385,23 @@ def test_space_to_real(self, hp_type, number): ret = sss.space_to_real(number) expected_ret = number - if hp_type in USE_LOG_SCALE_TYPE: + if type in USE_LOG_SCALE_TYPE: log_base = arg["log_base"] expected_ret = log_base**expected_ret - if hp_type in USE_QUANTIZED_STEP_TYPE: + if type in USE_QUANTIZED_STEP_TYPE: step = arg["step"] gap = sss.min % step expected_ret = round((expected_ret - gap) / step) * step + gap assert ret == expected_ret - @pytest.mark.parametrize("hp_type", ALL_TYPE) + @e2e_pytest_component + @pytest.mark.parametrize("type", ALL_TYPE) @pytest.mark.parametrize("number", [10, 512.3]) - def test_real_to_space(self, hp_type, number): - args = MAKE_GOOD_ARGS[hp_type]() + def test_real_to_space(self, type, number): + args = MAKE_GOOD_ARGS[type]() for arg in args: sss = SingleSearchSpace(**arg) - if hp_type in USE_LOG_SCALE_TYPE: + if type in USE_LOG_SCALE_TYPE: log_base = arg["log_base"] assert sss.real_to_space(number) == math.log(number, log_base) else: @@ -378,66 +410,108 @@ def test_real_to_space(self, hp_type, number): class TestSearchSpace: @staticmethod - def get_search_space_depending_on_type(types) -> dict: + def get_search_space_depending_on_type(types, range_format=False): if not isinstance(types, (list, tuple)): types = [types] search_space = {} if "uniform" in types: - TestSearchSpace.add_uniform_search_space(search_space) + TestSearchSpace.add_uniform_search_space(search_space, range_format) if "quniform" in types: - TestSearchSpace.add_quniform_search_space(search_space) + TestSearchSpace.add_quniform_search_space(search_space, range_format) if "loguniform" in types: - TestSearchSpace.add_loguniform_search_space(search_space) + TestSearchSpace.add_loguniform_search_space(search_space, range_format) if "qloguniform" in types: - TestSearchSpace.add_qloguniform_search_space(search_space) + TestSearchSpace.add_qloguniform_search_space(search_space, range_format) if "choice" in types: TestSearchSpace.add_choice_search_space(search_space) return search_space @staticmethod - def add_uniform_search_space(search_space) -> None: - search_space["uniform_search_space"] = {"type": "uniform"} - search_space["uniform_search_space"].update({"min": 1, "max": 10}) + def add_uniform_search_space(search_space, range_format=False): + search_space["uniform_search_space"] = {"param_type": "uniform"} + if range_format: + search_space["uniform_search_space"]["range"] = [1, 10] + else: + search_space["uniform_search_space"].update({"min": 1, "max": 10}) @staticmethod - def add_quniform_search_space(search_space) -> None: - search_space["quniform_search_space"] = {"type": "quniform"} - search_space["quniform_search_space"].update({"min": 1, "max": 10, "step": 3}) + def add_quniform_search_space(search_space, range_format=False): + search_space["quniform_search_space"] = {"param_type": "quniform"} + if range_format: + search_space["quniform_search_space"]["range"] = [1, 10, 3] + else: + search_space["quniform_search_space"].update({"min": 1, "max": 10, "step": 3}) @staticmethod - def add_loguniform_search_space(search_space) -> None: - search_space["loguniform_search_space"] = {"type": "loguniform"} - search_space["loguniform_search_space"].update({"min": 1, "max": 10, "log_base": 2}) + def add_loguniform_search_space(search_space, range_format=False): + search_space["loguniform_search_space"] = {"param_type": "loguniform"} + if range_format: + search_space["loguniform_search_space"]["range"] = [1, 10, 2] + else: + search_space["loguniform_search_space"].update({"min": 1, "max": 10, "log_base": 2}) @staticmethod - def add_qloguniform_search_space(search_space) -> None: - search_space["qloguniform_search_space"] = {"type": "qloguniform"} - search_space["qloguniform_search_space"].update({"min": 1, "max": 10, "step": 3, "log_base": 2}) + def add_qloguniform_search_space(search_space, range_format=False): + search_space["qloguniform_search_space"] = {"param_type": "qloguniform"} + if range_format: + search_space["qloguniform_search_space"]["range"] = [1, 10, 3, 2] + else: + search_space["qloguniform_search_space"].update({"min": 1, "max": 10, "step": 3, "log_base": 2}) @staticmethod - def add_choice_search_space(search_space) -> None: + def add_choice_search_space(search_space): search_space["choice_search_space"] = { - "type": "choice", + "param_type": "choice", "choice_list": ["somevalue1", "somevalue2", "somevalue3"], } - @pytest.fixture() + @pytest.fixture def search_space_with_all_types(self): return SearchSpace(self.get_search_space_depending_on_type(ALL_TYPE)) + @e2e_pytest_component + def test_init_with_range_format_argument(self): + search_space = self.get_search_space_depending_on_type(ALL_TYPE, True) + ss = SearchSpace(search_space) + assert ss is not None + + @e2e_pytest_component + @pytest.mark.parametrize("type", NOT_CATEGORICAL_TYPE) + def test_init_with_insufficient_range_arguments(self, type): + search_space = self.get_search_space_depending_on_type(type, True) + if type in USE_LOG_SCALE_TYPE: + num_to_delete = 2 + else: + num_to_delete = 1 + search_space[f"{type}_search_space"]["range"] = search_space[f"{type}_search_space"]["range"][:-num_to_delete] + with pytest.raises(ValueError): + SearchSpace(search_space) + + @e2e_pytest_component + def test_init_both_format_exists(self): + search_space = self.get_search_space_depending_on_type(ALL_TYPE) + range_format = self.get_search_space_depending_on_type(ALL_TYPE, True) + for key, val in search_space.items(): + val.update(range_format[key]) + SearchSpace(search_space) + + @e2e_pytest_component def test_get_item_available(self, search_space_with_all_types): - for each_type in ALL_TYPE: - search_space_with_all_types[f"{each_type}_search_space"] + for type in ALL_TYPE: + search_space_with_all_types[f"{type}_search_space"] + @e2e_pytest_component def test_iteratble(self, search_space_with_all_types): - for _ in search_space_with_all_types: + for val in search_space_with_all_types: pass + @e2e_pytest_component def test_len_is_available(self, search_space_with_all_types): assert len(search_space_with_all_types) == 5 + @e2e_pytest_component @pytest.mark.parametrize("choice_exist", [True, False]) def test_has_categorical_param(self, search_space_with_all_types, choice_exist): if choice_exist: @@ -448,6 +522,7 @@ def test_has_categorical_param(self, search_space_with_all_types, choice_exist): assert ss.has_categorical_param() == choice_exist + @e2e_pytest_component def test_get_real_config_with_proper_argument(self, search_space_with_all_types): # search space configuration step = 3 @@ -455,7 +530,7 @@ def test_get_real_config_with_proper_argument(self, search_space_with_all_types) min_val = 1 requested_val = 3.2 - config = {f"{each_type}_search_space": requested_val for each_type in ALL_TYPE} + config = {f"{type}_search_space": requested_val for type in ALL_TYPE} real_space = search_space_with_all_types.get_real_config(config) for key, val in real_space.items(): @@ -472,18 +547,20 @@ def test_get_real_config_with_proper_argument(self, search_space_with_all_types) assert val == rescaled_requested_val + @e2e_pytest_component @pytest.mark.parametrize("wrong_name", ["wrong_name", 1, 3.2]) def test_get_real_config_with_wrong_name_config(self, search_space_with_all_types, wrong_name): config = {wrong_name: 3.2} with pytest.raises(KeyError): search_space_with_all_types.get_real_config(config) + @e2e_pytest_component def test_get_space_config_with_proper_argument(self, search_space_with_all_types): # search space configuration log_base = 2 requested_val = 10 - config = {f"{each_type}_search_space": requested_val for each_type in ALL_TYPE} + config = {f"{type}_search_space": requested_val for type in ALL_TYPE} real_space = search_space_with_all_types.get_space_config(config) for key, val in real_space.items(): @@ -493,12 +570,14 @@ def test_get_space_config_with_proper_argument(self, search_space_with_all_types assert val == rescaled_requested_val + @e2e_pytest_component @pytest.mark.parametrize("wrong_name", ["wrong_name", 1, 3.2]) def test_get_space_config_with_wrong_name_config(self, search_space_with_all_types, wrong_name): config = {wrong_name: 3.2} with pytest.raises(KeyError): search_space_with_all_types.get_space_config(config) + @e2e_pytest_component def test_get_bayeopt_search_space(self, search_space_with_all_types): bayes_opt_format = search_space_with_all_types.get_bayeopt_search_space() @@ -507,12 +586,14 @@ def test_get_bayeopt_search_space(self, search_space_with_all_types): min_val, max_val = val assert min_val < max_val + @e2e_pytest_component def test_convert_from_zero_one_scale_to_real_space_with_good_args(self, search_space_with_all_types): config = {} for key in search_space_with_all_types: config[key] = 0.5 search_space_with_all_types.convert_from_zero_one_scale_to_real_space(config) + @e2e_pytest_component @pytest.mark.parametrize("config", ["wrong_value", [1, 3, 4], (1, 2)]) def test_convert_from_zero_one_scale_to_real_space_with_bad_arg_type(self, search_space_with_all_types, config): with pytest.raises(AttributeError): diff --git a/tests/unit/mpa/__init__.py b/tests/unit/mpa/__init__.py new file mode 100644 index 00000000000..1e19f1159d9 --- /dev/null +++ b/tests/unit/mpa/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/unit/mpa/deploy/__init__.py b/tests/unit/mpa/deploy/__init__.py new file mode 100644 index 00000000000..be388f4bebe --- /dev/null +++ b/tests/unit/mpa/deploy/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/mpa/test_augments.py b/tests/unit/mpa/test_augments.py new file mode 100644 index 00000000000..d7c93288745 --- /dev/null +++ b/tests/unit/mpa/test_augments.py @@ -0,0 +1,83 @@ +# Copyright (C) 2022 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +from copy import deepcopy +from typing import Any, List, Union + +import cv2 +import numpy as np +import pytest +from PIL import Image + +from otx.algorithms.common.adapters.mmcv.pipelines.transforms.augments import ( + Augments, + CythonAugments, +) + + +@pytest.fixture +def images() -> List[Image.Image]: + n_seed = 3003 + n_imgs = 4 + n_shapes = 4 + img_size = 50 + size = [img_size, img_size, 3] + + np.random.seed(n_seed) + + imgs = [] + for _ in range(n_imgs): + img = np.full(size, 0, dtype=np.uint8) + for _ in range(n_shapes): + position = np.random.randint(0, 50, size=[2]).tolist() + color = np.random.randint(0, 256, size=[3]).tolist() + marker_type = np.random.randint(0, 7) + img = cv2.drawMarker(img, position, color, marker_type, thickness=5) + imgs += [Image.fromarray(img)] + + return imgs + + +EXACT_EQUAL_TESTS = [ + ("autocontrast", []), + ("equalize", []), + ("solarize", [64, 128, 196]), + ("posterize", [1, 4, 7]), +] + + +@pytest.mark.parametrize("func,params", EXACT_EQUAL_TESTS) +def test_exact_equal(images: List[Image.Image], func: str, params: List[Any]): + for img in images: + for param in params: + grt = getattr(Augments, func)(deepcopy(img), param) + tst = getattr(CythonAugments, func)(deepcopy(img), param) + + assert np.array_equal(np.asarray(grt), np.asarray(tst)) + + +APPROX_EQUAL_TESTS = [ + ("color", [0.1, 0.5, 0.9], 1), + ("contrast", [0.1, 0.5, 0.9], 1), + ("brightness", [0.1, 0.5, 0.9], 1), + ("sharpness", [0.25, 0.75, 1.25, 1.75], 1), + ("rotate", [-35, -15, 15, 35], 1), + ("shear_x", [-0.8, -0.3, -0.3, 0.8], 1), + ("shear_y", [-0.8, -0.3, -0.3, 0.8], 1), + ("translate_x_rel", [-0.8, -0.3, -0.3, 0.8], 1), + ("translate_y_rel", [-0.8, -0.3, -0.3, 0.8], 1), +] + + +@pytest.mark.parametrize("func,params,tol", APPROX_EQUAL_TESTS) +def test_approx_equal(images: List[Image.Image], func: str, params: List[Any], tol: Union[float, int]): + for img in images: + for param in params: + grt = getattr(Augments, func)(deepcopy(img), param) + tst = getattr(CythonAugments, func)(deepcopy(img), param) + grt = np.array(grt).astype(np.float32) + tst = np.array(tst).astype(np.float32) + med = np.median(grt - tst) + assert med <= tol diff --git a/tests/unit/utils/test_signal.py b/tests/unit/utils/test_signal.py deleted file mode 100644 index 918d66d4497..00000000000 --- a/tests/unit/utils/test_signal.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import annotations - -import signal -from contextlib import contextmanager -from copy import copy - -from otx.utils import signal as target_file -from otx.utils.signal import append_main_proc_signal_handler, append_signal_handler - - -@contextmanager -def register_signal_temporally(sig_num: signal.Signals): - old_sig_handler = signal.getsignal(sig_num) - ori_handler_arr = copy(target_file._SIGNAL_HANDLERS) - yield - signal.signal(sig_num, old_sig_handler) - target_file._SIGNAL_HANDLERS = ori_handler_arr - - -def test_append_signal_handler(mocker): - with register_signal_temporally(signal.SIGTERM): - # prepare - mocker.patch("signal.raise_signal") - spy_signal = mocker.spy(target_file.signal, "signal") - sig_hand_1 = mocker.MagicMock() - sig_hand_2 = mocker.MagicMock() - - # run - append_signal_handler(signal.SIGTERM, sig_hand_1) - append_signal_handler(signal.SIGTERM, sig_hand_2) - - old_sig_handler = signal.getsignal(signal.SIGTERM) - old_sig_handler(signal.SIGTERM, mocker.MagicMock()) - - # check - sig_hand_1.assert_called_once() - sig_hand_2.assert_called_once() - assert spy_signal.call_args == ((signal.SIGTERM, signal.SIG_DFL),) - - -def test_append_main_proc_signal_handler(mocker): - with register_signal_temporally(signal.SIGTERM): - # prepare - mocker.patch("os.getpid", return_value=1) - mocker.patch("signal.raise_signal") - spy_signal = mocker.spy(target_file.signal, "signal") - sig_hand_1 = mocker.MagicMock() - sig_hand_2 = mocker.MagicMock() - - # run - append_main_proc_signal_handler(signal.SIGTERM, sig_hand_1) - append_main_proc_signal_handler(signal.SIGTERM, sig_hand_2) - - mocker.patch("os.getpid", return_value=2) - old_sig_handler = signal.getsignal(signal.SIGTERM) - old_sig_handler(signal.SIGTERM, mocker.MagicMock()) - - # check - sig_hand_1.assert_not_called() - sig_hand_2.assert_not_called() - assert spy_signal.call_args == ((signal.SIGTERM, signal.SIG_DFL),) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py deleted file mode 100644 index 10a6a939257..00000000000 --- a/tests/unit/utils/test_utils.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest -from otx.utils.utils import ( - find_file_recursively, - get_decimal_point, - get_using_dot_delimited_key, - remove_matched_files, - set_using_dot_delimited_key, -) - - -@pytest.fixture() -def fake_obj(mocker): - target = mocker.MagicMock() - target.a.b.c = {"d": mocker.MagicMock()} - target.a.b.c["d"].e = [0, 1, 2] - return target - - -def test_get_using_dot_delimited_key(fake_obj): - assert get_using_dot_delimited_key("a.b.c.d.e.2", fake_obj) == 2 - - -def test_set_using_dot_delimited_key(fake_obj): - expected_val = 2 - set_using_dot_delimited_key("a.b.c.d.e.0", expected_val, fake_obj) - assert fake_obj.a.b.c["d"].e[0] == expected_val - - -@pytest.mark.parametrize(("val", "decimal_point"), [(0.001, 3), (-0.0001, 4), (1, 0), (100, 0), (-2, 0)]) -def test_get_decimal_point(val, decimal_point): - assert get_decimal_point(val) == decimal_point - - -def test_find_file_recursively(tmp_path): - file_name = "some_file.txt" - target = tmp_path / "foo" / "bar" / file_name - target.parent.mkdir(parents=True) - target.touch() - - assert find_file_recursively(tmp_path, file_name) == target - - -def test_find_file_recursively_multiple_files_exist(tmp_path): - file_name = "some_file.txt" - - target1 = tmp_path / "foo" / file_name - target1.parent.mkdir(parents=True) - target1.touch() - - target2 = tmp_path / "foo" / "bar" / file_name - target2.parent.mkdir(parents=True) - target2.touch() - - assert find_file_recursively(tmp_path, file_name) in [target1, target2] - - -def test_find_file_recursively_not_exist(tmp_path): - file_name = "some_file.txt" - assert find_file_recursively(tmp_path, file_name) is None - - -def make_dir_and_file(dir_path: Path, file_path: str | Path) -> Path: - file = dir_path / file_path - file.parent.mkdir(parents=True, exist_ok=True) - file.touch() - - return file - - -@pytest.fixture() -def temporary_dir_w_some_txt(tmp_path): - some_txt = ["a/b/c/d.txt", "1/2/3/4.txt", "e.txt", "f/g.txt", "5/6/7.txt"] - for file_path in some_txt: - make_dir_and_file(tmp_path, file_path) - return tmp_path - - -def test_remove_matched_files(temporary_dir_w_some_txt): - file_path_to_leave = "foo/bar/file_to_leave.txt" - file_to_leave = make_dir_and_file(temporary_dir_w_some_txt, file_path_to_leave) - - remove_matched_files(temporary_dir_w_some_txt, "*.txt", file_to_leave) - - assert file_to_leave.exists() - assert len(list(temporary_dir_w_some_txt.rglob("*.txt"))) == 1 - - -def test_remove_matched_files_remove_all(temporary_dir_w_some_txt): - remove_matched_files(temporary_dir_w_some_txt, "*.txt") - - assert len(list(temporary_dir_w_some_txt.rglob("*.txt"))) == 0 - - -def test_remove_matched_files_no_file_to_remove(temporary_dir_w_some_txt): - remove_matched_files(temporary_dir_w_some_txt, "*.log") - - assert len(list(temporary_dir_w_some_txt.rglob("*.txt"))) == 5 diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000000..b828cf78f2e --- /dev/null +++ b/tools/README.md @@ -0,0 +1,138 @@ +# Experiment helper + +experiment.py is a powerful tool designed to streamline and automate the process of conducting experiments using OTX. +It simplifies the execution of multiple test cases, automatically parses output values, +and organizes results efficiently. +The primary goal is to reduce the manual effort required in running experiments and enhance overall productivity. + +## Key features + +### Automated Experiment Execution + +- Given multiple variables, it automatically generates all combinations and runs the experiments. +- Proper model files are selected automatically when the "otx eval" or "otx optimize" command is executed, based on the preceding command. + +### Fault Tolerance + +- Subsequent jobs are executed independently, irrespective of whether the previous job raised an error. +- All failed commands are printed and saved in a file after the entire experiment is finished. + +### Automated Experiment Execution + +- All possible values from a single workspace are organized and saved in a file. +- Experiment results are aggregated after the completion of all commands. + +## How to Use + +### Feature 1 : run experiments & aggregate results + +Arguments + +- -f / --file : Path to the YAML file describing the experiment setup. After all runs, results are aggregated and saved. +- -d / --dryrun : Preview the experiment list before execution. Use with '-f / --file' argument. + +Both single experiment and multiple experiments are supported. +Here is the example. + +Single Experiment Recipe YAML File: + + output_path: research_framework_demo/det_model_test + constants: # value in constant can't have other constant or variable. + det_model_dir: otx/src/otx/algorithms/detection/configs/detection + dataset_path: dataset + variables: + model: + - cspdarknet_yolox + - mobilenetv2_atss + dataset: + - diopsis/12 + repeat: 2 + command: + - otx train ${det_model_dir}/${model}/template.yaml + --train-data-roots ${dataset_path}/${dataset} + --val-data-roots ${dataset_path}/${dataset} + --track-resource-usage + params + --learning_parameters.num_iters 20 + - otx eval + --test-data-roots ${dataset_path}/${dataset} + - otx export + - otx eval + --test-data-roots ${dataset_path}/${dataset} + +Multiple Experiment Recipe YAML File: + + output_path: research_framework_demo/cls_det_model_test + constants: + dataset_path: some_dataset_path + experiments: + - name det + constants: + model_dir: otx/src/otx/algorithms/detection/configs/detection + variables: + model: + - cspdarknet_yolox + - mobilenetv2_atss + dataset: diopsis/12 + repeat: 2 + command: + - otx train ${model_dir}/${model}/template.yaml ... + - otx eval ... + - name: cls + constants: + model_dir: otx/src/otx/algorithms/classification/configs + dataset_path: other_dataset_path + variables: + model: + - efficientnet_b0_cls_incr + - deit_tiny + dataset: cifar10_300 + repeat: 2 + command: + - otx train ${model_dir}/${model}/template.yaml + --train-data-roots ${dataset_path}/${dataset} ... + - otx eval ... + +Arguments for recipe + +- output_path (optional) : Output path where all experiment outputs are saved. Default is "./experiment\_{executed_time}" +- constant (optional) : + It's similar as constant or variable in programming languages. + You can use it to replace duplicated string by using ${constant_name} in variables or commands. +- variables (optional) : + It can be used in a similar way to "constant". But it's different in that "otx experiment" makes all combinations and summarize experiment results based on variables. + For example, if two models and two dataset are given as variable, then total 4 cases will be run as experiment. Also key of each varaible will be row headers of experiment result table. +- repeat (optional) : Number of times to run experiments. Repeated experiments have different random seeds in "otx train" command. +- command (required) : Specifies the commands to run. Supports both single commands and lists of commands. +- experiments (optional) : + To perform multiple experiments, the user is required to define a list of experiments in this section. + Each element in the list can contain all the keys mentioned above, excluding output_path. + The output path for each experiment is automatically set to output_path/name. + Values outside the experiment element, except for output_path and constants, will be disregarded. + If `constants` exsist at both upper most level and experiment element, + they're merged while experiment element takes precedence. + - name (required) : Specifies the unique name for the experiment. This name will be utilized as the directory name where the output of the experiment is stored. + +Upon completion of each experiment, the results are organized within the own workspace. +Following the conclusion of all experiments, all experiment results are aggregated in two distinct formats: +"all experiments result" and "experiment summary" within the specified output_path. +If the repeat parameter is set to a value greater than 1, the results of repeated experiments are averaged in the summary format. + +All TensorBoard log files are automatically copied to the output_path/tensorboard directory. +If you want to run tensorboard with all experiments result, you just need to use it as a tensorboard argument. +If there are failed cases, variables and error logs are both printed and saved as a file after the execution of all commands. + +Note that all commands within each case are executed within the same workspace, +obviating the need to set a template path from the second command. +When the "otx eval" or "otx optimize" command is executed, the model file (model weight or exported model, etc.) +is automatically selected based on the preceding command. +The output file of "otx eval" is then stored at "workspace_path/outputs/XXXX\_{train, export, optimize, etc.}/" +under the name "performance.json". + +### Feature 2 : organize experiment result from single workspace + +Arguments + +- -p / --path : Path to the workspace. Experiment results in the workspace are organized and saved. + +This feature parses all possible values from a single workspace and saves them as a file. diff --git a/tools/experiment.py b/tools/experiment.py new file mode 100644 index 00000000000..6a79aae7537 --- /dev/null +++ b/tools/experiment.py @@ -0,0 +1,1060 @@ +"""OTX experiment helper.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import argparse +import csv +import dataclasses +import gc +import json +import os +import re +import shutil +import statistics +import sys +from abc import ABC, abstractmethod +from copy import copy, deepcopy +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from itertools import product +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union + +import yaml +from otx.cli.tools.cli import main as otx_cli +from rich.console import Console +from rich.table import Table + +rich_console = Console() + + +def get_parser() -> argparse.ArgumentParser: + """Parses command line arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument("-f", "--file", type=str, help="Experiment recipe file.") + parser.add_argument("-p", "--parse", type=str, help="Workspace path to parse.") + parser.add_argument("-d", "--dryrun", action="store_true", help="Print experiment commands without execution.") + return parser + + +def parse_time_delta_fmt(time_str: str, format: str) -> timedelta: + """Convert datetime to timedelta. + + Args: + time_str (str): datetime format string. + format (str): datetime format. + + Returns: + timedelta: timedelta converted from datetime. + """ + return datetime.strptime(time_str, format) - datetime(1900, 1, 1) + + +def find_latest_file(root_dir: Union[Path, str], file_name: str, recursive: bool = False) -> Union[None, Path]: + """Find a latest file of matched files. + + Args: + root_dir (Union[Path, str]): Root directory for searching. + file_name (str): File name to search. It can constain shell style wild card. + recursive (bool): If it's true, find a file from not only root_dir but also child directories. + + Returns: + Union[None, Path]: Latest file path. If file can't be found, return None. + """ + root_dir = Path(root_dir) + train_record_files = sorted( + (root_dir).rglob(file_name) if recursive else (root_dir).glob(file_name), + reverse=True, + key=lambda x: x.stat().st_mtime + ) + if not train_record_files: + return None + return train_record_files[0] + + +def cvt_number_to_str(target: Dict): + """Convert int or float in dict to string. + + Args: + target (Dict): Dictionary object to change int or float to string in. + """ + result = copy(target) + + for key, val in result.items(): + if isinstance(val, (int, float)): + result[key] = str(val) + elif isinstance(val, list): + for i in range(len(val)): + if isinstance(val[i], (int, float)): + val[i] = str(val[i]) + + return result + + +class EvalResult: + """Class to save otx eval output. + + Current OTX eval output has different metrics depending on a task. + To deal with it, this class can save dynamic metric name. + Each metric can be set or gotten by both dict-like(ins["metric"]) or class-like(ins.metric) way. + "add" (only with a class having same metrics) and "true devide" are supported. + """ + + def __getitem__(self, key): + """Support dict-like way to get attribute.""" + return getattr(self, key) + + def __setitem__(self, key, value): + """Support dict-like way to set attribute.""" + setattr(self, key, value) + + def __add__(self, obj: "EvalResult"): + """Add with a class having same metrics.""" + new_obj = deepcopy(self) + new_obj_metrics = vars(new_obj).keys() + + if new_obj_metrics != vars(obj).keys(): + raise KeyError( + "Two objects have different metrics. " + f"Left operand : {','.join(new_obj_metrics)} / Right operand : {','.join(vars(obj).keys())}" + ) + + for attr in new_obj_metrics: + new_obj[attr] += obj[attr] + return new_obj + + def __truediv__(self, divisor: Union[int, float]): + """Divide each metric in the class.""" + new_obj = deepcopy(self) + + for attr in vars(new_obj).keys(): + new_obj[attr] /= divisor + + return new_obj + + +@dataclass +class ExperimentResult: + """Dataclass to manage experiment result. + + It serves not only storing values but also various features. + For example, it can be added by other ExperimentResult instance and also divided by integer. + It can provide dictionary format result and also can parse a dictionary with same format as itself. + """ + + val_score: Union[float, None] = None + train_eval_result: Union[EvalResult, None] = None + train_e2e_time: Union[timedelta, None] = None + avg_iter_time: Union[float, None] = None + std_iter_time: Union[float, None] = None + avg_data_time: Union[float, None] = None + std_data_time: Union[float, None] = None + export_eval_result: Union[EvalResult, None] = None + max_cpu_mem: Union[float, None] = None + avg_cpu_util: Union[float, None] = None + max_gpu_mem: Union[float, None] = None + avg_gpu_util: Union[float, None] = None + optimize_eval_result: Union[EvalResult, None] = None + epoch: Union[int, None] = None + + def get_formatted_result(self) -> Dict: + """Return dictionary format result.""" + result = dataclasses.asdict(self) + formatted_result = {} + + for key, val in result.items(): + if val is None: + continue + elif key in ["max_cpu_mem", "max_gpu_mem"]: + formatted_result[f"{key}(GiB)"] = round(val, 2) + elif key in ["avg_cpu_util", "avg_gpu_util"]: + formatted_result[f"{key}(%)"] = round(val, 2) + elif key == "train_e2e_time": + formatted_result[key] = str(self.train_e2e_time).split(".")[0] + elif isinstance(val, EvalResult): + task = key.split('_')[0] + for metric, score in vars(val).items(): + formatted_result[f"{metric}({task})"] = round(score, 4) + elif isinstance(val, float): + formatted_result[key] = round(val, 4) + else: + formatted_result[key] = val + + return formatted_result + + def __add__(self, obj: "ExperimentResult"): + """Add with same class. If None exists, it's skipped.""" + new_obj = deepcopy(self) + + for attr in dataclasses.fields(self): + self._add_if_not_none(new_obj, obj, attr.name) + + return new_obj + + @staticmethod + def _add_if_not_none(dst_obj: "ExperimentResult", src_obj: "ExperimentResult", attr: str): + dst_obj_val = getattr(dst_obj, attr) + src_obj_val = getattr(src_obj, attr) + if dst_obj_val is not None and src_obj_val is not None: + setattr(dst_obj, attr, dst_obj_val + src_obj_val) + else: + setattr(dst_obj, attr, None) + + def __truediv__(self, divisor: Union[int, float]): + """Divide with same class. If None exists, it's skipped.""" + new_obj = deepcopy(self) + + for attr in dataclasses.fields(self): + self._divide_if_not_none(new_obj, attr.name, divisor) + + return new_obj + + @staticmethod + def _divide_if_not_none(obj: "ExperimentResult", attr: str, divisor: Union[int, float]): + obj_val = getattr(obj, attr) + if obj_val is not None: + setattr(obj, attr, obj_val / divisor) + + def parse_formatted_dict(self, formatted_dict: Dict): + """Parse a dictionary with same format.""" + max_mem_pat = re.compile(r"max_.*_mem") + cpu_util_pat = re.compile(r"avg.*_util") + eval_result_pat = re.compile(r"(.*)\((.*)\)") + + for key, val in formatted_dict.items(): + max_mem_name = max_mem_pat.search(key) + cpu_util_name = cpu_util_pat.search(key) + eval_result_name = eval_result_pat.search(key) + + if max_mem_name is not None: + max_mem_name = max_mem_name.group(0) + setattr(self, max_mem_name, val) + elif cpu_util_name is not None: + cpu_util_name = cpu_util_name.group(0) + setattr(self, cpu_util_name, val) + elif eval_result_name is not None: + metric = eval_result_name.group(1) + task = eval_result_name.group(2) + eval_result = getattr(self, f"{task}_eval_result") + if eval_result is None: + eval_result = EvalResult() + eval_result[metric] = val + setattr(self, f"{task}_eval_result", eval_result) + else: + eval_result[metric] = val + elif key == "train_e2e_time": + setattr(self, key, parse_time_delta_fmt(val, "%H:%M:%S")) + else: + setattr(self, key, val) + + +class BaseExpParser(ABC): + """Base class for an experiment parser. + + Args: + workspace (Path): Workspace to parse. + """ + + def __init__(self, workspace: Path): + self._workspace = workspace + self._exp_result = ExperimentResult() + self._iter_time_arr = [] + self._data_time_arr = [] + + @abstractmethod + def parse_exp_log(self): + """Abstract method to parse experiment log.""" + raise NotImplementedError + + def get_exp_result(self): + """Get experiment result.""" + self._calculate_avg_std_per_iter() + + return self._exp_result.get_formatted_result() + + def _calculate_avg_std_per_iter(self): + if self._iter_time_arr: + self._exp_result.avg_iter_time = statistics.mean(self._iter_time_arr) + self._exp_result.std_iter_time = ( + statistics.stdev(self._iter_time_arr) if len(self._iter_time_arr) > 1 else 0 + ) + + if self._data_time_arr: + self._exp_result.avg_data_time = statistics.mean(self._data_time_arr) + self._exp_result.std_data_time = ( + statistics.stdev(self._data_time_arr) if len(self._data_time_arr) > 1 else 0 + ) + + def _parse_eval_output(self, file_path: Path): + for task in ["train", "export", "optimize"]: + if task in str(file_path.parent.name): + break + else: + print(f"Can not parse eval output in {file_path.parent.name}") + return + + with file_path.open("r") as f: + eval_output: Dict = json.load(f) + + eval_result = EvalResult() + for metric, score in eval_output.items(): + eval_result[metric] = score + + setattr(self._exp_result, f"{task}_eval_result", eval_result) + + def _parse_resource_usage(self, file_path: Path): + with file_path.open("r") as f: + resource_usage = yaml.safe_load(f) + + if "cpu" in resource_usage: + self._exp_result.max_cpu_mem = float(resource_usage["cpu"]["max_memory_usage"].split()[0]) + self._exp_result.avg_cpu_util = float(resource_usage["cpu"]["avg_util"].split()[0]) + + if "gpu" in resource_usage: + self._exp_result.max_gpu_mem = float(resource_usage["gpu"]["total_max_mem"].split()[0]) + self._exp_result.avg_gpu_util = float(resource_usage["gpu"]["total_avg_util"].split()[0]) + + def _parse_cli_report(self, file_path: Path, save_val_score=True): + with file_path.open("r") as f: + lines = f.readlines() + + val_score_pattern = re.compile(r"score:.*Performance\(score: ([-+]?\d+(\.\d*)?|\.\d+)") + e2e_time_pattern = re.compile(r"time elapsed: '(\d+:\d+:\d+(\.\d*)?)'") + for line in lines: + if save_val_score: + val_score = val_score_pattern.search(line) + if val_score is not None: + self._exp_result.val_score = float(val_score.group(1)) + + e2e_time = e2e_time_pattern.search(line) + if e2e_time is not None: + self._exp_result.train_e2e_time = parse_time_delta_fmt(e2e_time.group(1), "%H:%M:%S.%f") + + +class MMCVExpParser(BaseExpParser): + """MMCV experiment parser class.""" + + def parse_exp_log(self): + """Parse experiment log.""" + for task_dir in (self._workspace / "outputs").iterdir(): + if task_dir.is_symlink(): # prevent duplicated parse + continue + + if "train" in str(task_dir.name): + # test score + eval_files = list(task_dir.glob("performance.json")) + if eval_files: + self._parse_eval_output(eval_files[0]) + + # iter, data time, epoch + train_record_file = find_latest_file(task_dir / "logs", "*.log.json") + if train_record_file is not None: + self._parse_train_record(train_record_file) + + # train e2e time & val score + cli_report_files = list(task_dir.glob("cli_report.log")) + if cli_report_files: + self._parse_cli_report(cli_report_files[0]) + + # get resource info + resource_file = task_dir / "resource_usage.yaml" + if resource_file.exists(): + self._parse_resource_usage(resource_file) + + elif "export" in str(task_dir) or "optimize" in str(task_dir): + eval_files = list(task_dir.glob("performance.json")) + if eval_files: + self._parse_eval_output(eval_files[0]) + + def _parse_train_record(self, file_path: Path): + with file_path.open("r") as f: + lines = f.readlines() + + last_epoch = 0 + iter_time = [] + data_time = [] + for line in lines: + iter_history = json.loads(line) + if iter_history.get("mode") == "train": + if iter_history["epoch"] > last_epoch: + last_epoch = iter_history["epoch"] + if last_epoch <= 2: # if epoch >= 2, first epcoh is excluded from the calcuation + iter_time = [] + data_time = [] + iter_time.append(iter_history["time"]) + data_time.append(iter_history["data_time"]) + + self._iter_time_arr.extend(iter_time) + self._data_time_arr.extend(data_time) + self._exp_result.epoch = last_epoch + + +class AnomalibExpParser(BaseExpParser): + """Anomalib experiment parser class.""" + + def parse_exp_log(self): + """Parse experiment log.""" + for task_dir in (self._workspace / "outputs").iterdir(): + if task_dir.is_symlink(): + continue + + if "train" in str(task_dir.name): + # test score + eval_files = list(task_dir.glob("performance.json")) + if eval_files: + self._parse_eval_output(eval_files[0]) + + # val score and train e2e time + cli_report_files = list(task_dir.glob("cli_report.log")) + if cli_report_files: + self._parse_cli_report(cli_report_files[0]) + + # iter, data time, epoch + train_record_file = find_latest_file(task_dir / "logs", "metrics.csv", recursive=True) + if train_record_file is not None: + self._parse_train_record(train_record_file) + + # get resource info + resource_file = task_dir / "resource_usage.yaml" + if resource_file.exists(): + self._parse_resource_usage(resource_file) + + elif "export" in str(task_dir) or "optimize" in str(task_dir): + eval_files = list(task_dir.glob("performance.json")) + if eval_files: + self._parse_eval_output(eval_files[0]) + + def _parse_train_record(self, file_path: Path): + with file_path.open("r") as f: + rows = list(csv.reader(f)) + + iter_idx = rows[0].index("train/iter_time") + data_idx = rows[0].index("train/data_time") + epoch_idx = rows[0].index("epoch") + + last_epoch = 0 + iter_time = [] + data_time = [] + # breakpoint() + for row in rows[1:]: + if row[epoch_idx] and last_epoch < int(row[epoch_idx]) + 1: + last_epoch = int(row[epoch_idx]) + 1 + if last_epoch <= 2: # if epoch >= 2, first epcoh is excluded from the calcuation + iter_time = [] + data_time = [] + if row[iter_idx]: + iter_time.append(float(row[iter_idx])) + if row[data_idx]: + data_time.append(float(row[data_idx])) + + self._iter_time_arr.extend(iter_time) + self._data_time_arr.extend(data_time) + self._exp_result.epoch = last_epoch + +def get_exp_parser(workspace: Path) -> Union[BaseExpParser, None]: + """Get experiment parser depending on framework. + + Args: + workspace (Path): Workspace to parse. + + Returns: + Union[BaseExpParser, None]: Experiment parser. If template file doesn't exist in the workspace, return None. + """ + template_file = workspace / "template.yaml" + if not template_file.exists(): + return None + + with template_file.open("r") as f: + template = yaml.safe_load(f) + + if "anomaly" in template["task_type"].lower(): + return AnomalibExpParser(workspace) + return MMCVExpParser(workspace) + + +def organize_exp_result(workspace: Union[str, Path], exp_meta: Optional[Dict[str, str]] = None): + """Organize experiment result and save it as a file named exp_result.yaml. + + Args: + workspace (Union[str, Path]): Workspace to organize an expeirment result. + exp_meta (Dict[str, str], optional): + Experiment meta information. If it exists, it's saved together. Defaults to None. + """ + if isinstance(workspace, str): + workspace = Path(workspace) + + exp_parser = get_exp_parser(workspace) + if exp_parser is None: + print(f"Unable to find which task \"{workspace}\" is. Parsing experiment result is skipped.") + return + exp_parser.parse_exp_log() + + exp_result = exp_parser.get_exp_result() + + if not exp_result: + print(f"There is no experiment result in {workspace}") + + with (workspace / "exp_result.yaml").open("w") as f: + yaml.dump({"meta": exp_meta, "exp_result": exp_result}, f, default_flow_style=False) + + +def write_csv(output_path: Union[str, Path], header: List[str], rows: List[Dict[str, Any]]): + """Write csv file based on header and rows. + + Args: + output_path (Union[str, Path]): Where file is saved. + header (List[str]): List of header. + rows (List[Dict[str, Any]]): Each row of csv. + """ + if isinstance(output_path, str): + output_path = Path(output_path) + + with output_path.open("w") as f: + writer = csv.DictWriter(f, fieldnames=header) + writer.writeheader() + writer.writerows(rows) + + +def print_table(headers: List[str], rows: List[Dict[str, Any]], table_title: str = "Table"): + """Print a table to console. + + Args: + headers (List[str]): List of headers. + rows (List[Dict[str, Any]]): Rows of table. + table_title (str, optional): Table title. Defaults to "Table". + """ + table = Table(title=table_title) + for header in headers: + table.add_column(header, justify="center", no_wrap=True) + for row in rows: + table_row = [] + for header in headers: + val = row.get(header) + table_row.append(str(val)) + + table.add_row(*table_row) + + rich_console.print(table, justify="center", crop=False) + + +def aggregate_all_exp_result(exp_dir: Union[str, Path]): + """Aggregate all experiment results and save it and it's summary as a file. + + Args: + exp_dir (Union[str, Path]): Experiment directory. + """ + if isinstance(exp_dir, str): + exp_dir = Path(exp_dir) + + tensorboard_dir = exp_dir / "tensorboard" + tensorboard_dir.mkdir(exist_ok=True) + + meta_header: Union[List[str], None] = None + metric_header = set() + all_exp_result: List[Dict[str, str]] = [] + exp_result_aggregation = {} + for each_exp in exp_dir.iterdir(): + # parse single experiment + exp_result_file = each_exp / "exp_result.yaml" + if not exp_result_file.exists(): + continue + + with exp_result_file.open("r") as f: + exp_yaml_result: Dict[str, Dict] = yaml.safe_load(f) + + each_exp_result = copy(exp_yaml_result["meta"]) + each_exp_result.update(exp_yaml_result["exp_result"]) + all_exp_result.append(each_exp_result) + + if meta_header is None: + meta_header = list(exp_yaml_result["meta"].keys()) + + metric_header = metric_header | set(exp_yaml_result["exp_result"].keys()) + + exp_meta = copy(exp_yaml_result["meta"]) + exp_meta.pop("repeat") + + exp_result = ExperimentResult() + exp_result.parse_formatted_dict(exp_yaml_result["exp_result"]) + + # Sum experiments with same variables. + exp_name = json.dumps(exp_meta, sort_keys=True).encode() # get unique hash based on variable + if exp_name in exp_result_aggregation: + exp_result_aggregation[exp_name]["result"] += exp_result + exp_result_aggregation[exp_name]["num"] += 1 + else: + exp_result_aggregation[exp_name] = {"result": exp_result, "num": 1, "meta": exp_meta} + + # copy tensorboard log into tensorboard dir + exp_tb_dir = list(each_exp.rglob("tf_logs")) + if exp_tb_dir: + shutil.copytree(exp_tb_dir[0], tensorboard_dir / each_exp.name, dirs_exist_ok=True) + + if not all_exp_result: + print("There aren't any experiment results.") + return + + # print and save the experiment aggregation + headers = sorted(meta_header) + sorted(metric_header) + write_csv(exp_dir / "all_exp_result.csv", headers, all_exp_result) + + for key in ["repeat", "std_iter_time", "std_data_time"]: # average of std is distorted value + if key in headers: + headers.remove(key) + + rows = [] + for val in exp_result_aggregation.values(): + exp_result = val["result"] / val["num"] + exp_result.std_iter_time = None + exp_result.std_data_time = None + each_exp_result = copy(val["meta"]) + + each_exp_result.update(exp_result.get_formatted_result()) + rows.append(each_exp_result) + write_csv(exp_dir / "exp_summary.csv", headers, rows) + + +@dataclass +class Command: + """Command dataclass.""" + + command: List[str] + variable: Dict[str, str] = field(default_factory=dict) + + +class ExpInfo: + """Class to store experiment information. + + It does additional things to provide complete experiment information. + For example, it replaces constants or varilabes if necessary, + and then it makes all possibles commands based on variables. + + Args: + command (Union[str, List[str]]): All commands to exeucte. + output_path (Path): Output path to save experiment result. + name (str, optional): Experiment name. Defaults to "". + constants (Dict[str, str], optional): + Constants. If there are constants in variables or commands, + they are replaced based on this value. Defaults to None. + variables (Dict[str, str], optional): + Variables. If there are variables in command, they're replaced based on this value. Defaults to None. + repeat (int, optional): How many times to repeat experiments. Defaults to 1. + """ + + def __init__( + self, + command: Union[str, List[str]], + output_path: Path, + name: str = "", + constants: Optional[Dict[str, str]] = None, + variables: Optional[Dict[str, str]] = None, + repeat: int = 1, + ): + self._raw_command = command + self._commands: Optional[List[Command]] = None + self.output_path = output_path + self.name = name + self._constants = constants if constants is not None else {} + if variables is None: + variables = {} + self._raw_variables = cvt_number_to_str(variables) + self._variables: Optional[Dict[str, str]] = None + self.repeat = repeat + self._replace_pat = re.compile(r"\$\{(\w+)\}") + + @property + def constants(self) -> Dict[str, str]: + """Constants in recipe file.""" + return self._constants + + @property + def variables(self) -> Dict[str, Union[str, List[str]]]: + """Variables in recipe file. If it contains constants, they're replaced by real value.""" + if self._variables is None: + self._variables = self._replace_var_in_target(self.constants, self._raw_variables) + return self._variables + + @property + def commands(self) -> List[Command]: + """List of commands from experiment recipe. + + It counts all available cases and makes Command instance per each case. + + Returns: + List[Command]: List of Command instances. + """ + if self._commands is None: + command = self._raw_command + if isinstance(command, str): + command = [command] + command = self._replace_var_in_target(self.constants, command) + var_combinations = self._product_all_cases(self.variables, command) + if not var_combinations: + self._commands = [Command(command=command)] + else: + command_arr = [] + for var_combination in var_combinations: + command_arr.append(Command(self._replace_var_in_target(var_combination, command), var_combination)) + self._commands = command_arr + return self._commands + + def _product_all_cases( + self, + variable: Dict[str, Union[str, List[str]]], + target_str: Union[str, List[str]], + ) -> List[Dict[str, str]]: + if isinstance(target_str, str): + target_str = [target_str] + found_keys = set() + for each_str in target_str: + found_keys.update([x for x in set(self._replace_pat.findall(each_str)) if x in variable]) + if not found_keys: + return [] + + found_keys = sorted(found_keys) + values_of_found_key = [] + for key in found_keys: + if isinstance(variable[key], list): + values_of_found_key.append(variable[key]) + else: + values_of_found_key.append([variable[key]]) + + all_cases = [] + for value_of_key_found in product(*values_of_found_key): + all_cases.append(dict(zip(found_keys, value_of_key_found))) + + return all_cases + + def _replace_var_in_target( + self, + variable: Dict[str, str], + target: Union[str, List, Dict], + ) -> Union[str, List, Dict]: + if isinstance(target, str): + for key, val in variable.items(): + target = target.replace(f"${{{key}}}", val) + elif isinstance(target, list): + target = target.copy() + for i in range(len(target)): + target[i] = self._replace_var_in_target(variable, target[i]) + elif isinstance(target, dict): + target = target.copy() + for key in target.keys(): + target[key] = self._replace_var_in_target(variable, target[key]) + else: + raise TypeError(f"{type(target)} isn't supported type. target should has str, list or dict type.") + + return target + + + +def parse_exp_recipe(recipe_file: Union[str, Path]) -> Tuple[List[ExpInfo], Path]: + """Parse an experiment recipe and return list of expeirment information and output path. + + Args: + recipe_file (Union[str, Path]): Recipe file to parse. + + Raises: + RuntimeError: If recipe file doesn't exist, error is raised. + + Returns: + Tuple[List[ExpInfo], Path]: List of expeirment information and output path. + """ + if not os.path.exists(recipe_file): + raise RuntimeError(f"{recipe_file} doesn't exist.") + + with open(recipe_file, "r") as f: + exp_recipe: Dict = yaml.safe_load(f) + + ori_constants: Dict[str, str] = cvt_number_to_str(exp_recipe.get("constants", {})) + output_path = Path(exp_recipe.get("output_path", f"experiment_{datetime.now().strftime('%Y%m%d_%H%M%S')}")) + exp_info_list = [] + + if "experiments" not in exp_recipe: + exp_recipe["experiments"] = [exp_recipe] + exp_recipe["experiments"][0]["name"] = "" + exp_recipe["experiments"][0].pop("constants") + + for exp in exp_recipe["experiments"]: + constants = copy(ori_constants) + if "constants" in exp: + constants.update(exp["constants"]) + + exp_info_list.append( + ExpInfo( + exp["command"], + output_path / exp["name"], + exp["name"], + constants, + exp.get("variables"), + exp.get("repeat", 1) + ) + ) + + return exp_info_list, output_path + + +@dataclass +class CommandFailInfo: + """Dataclass to store command fail information.""" + + exception: Exception + variable: Dict[str, str] + command: str + + def get_formatted_result(self) -> Dict: + """Return dictionary format result.""" + result = dataclasses.asdict(self) + result["exception"] = str(result["exception"]) + return result + + +def log_exp_failed_cases( + failed_cases: Union[List[CommandFailInfo], Dict[str, List[CommandFailInfo]]], + output_path: Path, +): + """Print experiments failed cases to console and save them in each experiment directory as a file. + + Args: + failed_cases (Union[List[CommandFailInfo], Dict[str, List[CommandFailInfo]]]): + List of CommandFailInfo or Dictionary having experiment name as key and CommandFailInfo object as value. + output_path (Path): Directory where experiment direcory exists. + """ + if isinstance(failed_cases, list): + failed_cases = {"" : failed_cases} + + for exp_name, failed_cases in failed_cases.items(): + rich_console.rule(f"[bold red]{exp_name} failed cases ") + + for each_fail_case in failed_cases: + rich_console.print(f"Case : {each_fail_case.variable}", crop=False) + rich_console.print(f"command : {each_fail_case.command}", crop=False) + rich_console.print("Error log:", str(each_fail_case.exception), crop=False) + rich_console.print() + + with (output_path / exp_name / "failed_cases.yaml").open("w") as f: + yaml.safe_dump([fail_case.get_formatted_result() for fail_case in failed_cases], f) + + +class OtxCommandRunner: + """Class to run list of otx commands who have same varaibles. + + It provides convenient features not just runs commands. + Same workspae made at first otx command is used for all commands. + Therefore, the template don't have to be set after first command. + And when executing OTX eval, which model to evaluate is decided depending on previous command. + + Args: + command_ins (Command): Command instance to run. + repeat_idx (int): repeat index. + """ + + OUTPUT_FILE_NAME: Dict[str, List[str]] = { + "export": ["openvino.bin"], + "optimize": ["weights.pth", "openvino.bin"] + } + + def __init__(self, command_ins: Command, workspace: Path, repeat_idx: int): + self._command_ins = command_ins + self._repeat_idx = repeat_idx + self._command_var = copy(command_ins.variable) + self._workspace = workspace + self._command_var["repeat"] = str(repeat_idx) + self._fail_logs: List[CommandFailInfo] = [] + self._previous_cmd_entry: List[str] = [] + + @property + def fail_logs(self) -> List[CommandFailInfo]: + """Information of all failed cases.""" + return self._fail_logs + + def run_command_list(self, dryrun: bool = False): + """Run all commands and organize experiment results.""" + for command in self._command_ins.command: + command = command.split() + if not self._prepare_run_command(command) and not dryrun: + print(f"otx {command[1]} is skipped.") + continue + + if not dryrun: + self._run_otx_command(command) + else: + print(" ".join(command)) + + self._previous_cmd_entry.append(command[1]) + + gc.collect() + + if not dryrun: + organize_exp_result(self._workspace, self._command_var) + + def _prepare_run_command(self, command: List[str]) -> bool: + self.set_arguments_to_cmd(command, "--workspace", str(self._workspace)) + cmd_entry = command[1] + previous_cmd = None + for previous_cmd in reversed(self._previous_cmd_entry): + if previous_cmd != "eval": + break + + if cmd_entry == "train": + self.set_arguments_to_cmd(command, "--seed", str(self._repeat_idx)) + elif cmd_entry == "eval": + if previous_cmd in ["export", "optimize"]: + file_path = self._find_model_path(previous_cmd) + if file_path is None: + return False + self.set_arguments_to_cmd(command, "--load-weights", str(file_path)) + output_path = str(file_path.parents[1]) + else: + output_path = str(self._workspace / "outputs" / "latest_trained_model") + self.set_arguments_to_cmd(command, "--output", output_path) + elif cmd_entry == "optimize": + if previous_cmd == "export": # execute PTQ. If not, execute QAT + file_path = self._find_model_path(previous_cmd) + if file_path is None: + return False + self.set_arguments_to_cmd(command, "--load-weights", str(file_path)) + + return True + + def _run_otx_command(self, command: List[str]): + sys.argv = copy(command) + try: + otx_cli() + except Exception as e: + self._fail_logs.append(CommandFailInfo(variable=self._command_var, exception=e, command=" ".join(command))) + + def _find_model_path(self, cmd_entry: str): + output_dir = find_latest_file(self._workspace / "outputs", f"*{cmd_entry}") + if output_dir is None: + print(f"There is no {cmd_entry} output directory.") + return None + for file_name in self.OUTPUT_FILE_NAME[cmd_entry]: + file_path = list(output_dir.rglob(file_name)) + if file_path: + return file_path[0] + + print(f"{', '.join(self.OUTPUT_FILE_NAME[cmd_entry])} can't be found.") + return None + + @staticmethod + def set_arguments_to_cmd(command: List[str], key: str, value: Optional[str] = None, before_params: bool = True): + """Add arguments at proper position in command. + + Args: + command (List[str]): list includng a otx command entry and arguments. + key (str): arguement key. + value (str or None): argument value. + before_params (bool): whether argument should be after `param` or not. + """ + if key in command: + if value is not None: + command[command.index(key) + 1] = value + return + + if before_params and "params" in command: + index = command.index("params") + else: + index = len(command) + + if value is not None: + command.insert(index, value) + command.insert(index, key) + + +def run_experiment(exp_info: ExpInfo, dryrun: bool = False) -> List[CommandFailInfo]: + """Run single expeirment. + + Args: + exp_info (ExpInfo): ExpInfo having expreiment information to conduct. + dryrun (bool, optional): Whether to only print experiment commands. Defaults to False. + + Returns: + List[CommandFailInfo]: List of failed command information. + """ + failed_cases: List[CommandFailInfo] = [] + + for command_ins in exp_info.commands: + for repeat_idx in range(exp_info.repeat): + otx_cmd_runner = OtxCommandRunner( + command_ins, + exp_info.output_path + / "_".join(list(command_ins.variable.values()) + ["repeat", str(repeat_idx)]).replace("/", "_"), + repeat_idx + ) + otx_cmd_runner.run_command_list(dryrun) + failed_cases.extend(otx_cmd_runner.fail_logs) + + if not dryrun: + aggregate_all_exp_result(exp_info.output_path) + + return failed_cases + + +def print_experiments_summary(output_path: Path): + """Print experiment summary to console and save it as a file. + + Args: + output_path (Path): Output path where experiment summary file is saved. + """ + rich_console.rule("[bold green]Experiment summary") + + for summary_file in output_path.rglob("exp_summary.csv"): + exp_name = summary_file.parent.name + if not summary_file.exists(): + print(f"{exp_name} doesn't have exp_summary.csv file. Skipped.") + continue + + with summary_file.open() as f: + exp_summary_csv = csv.reader(f) + + headers = next(exp_summary_csv) + rows = [] + for row in exp_summary_csv: + rows.append(dict((header, val) for header, val in zip(headers, row))) + + print_table(headers, rows, f"{exp_name}") + + +def run_experiment_recipe(recipe_file: Union[str, Path], dryrun: bool = False): + """Run experiments based on the recipe. + + Args: + recipe_file (Union[str, Path]): Recipe file to run. + dryrun (bool, optional): Whether to only print experiment commands. Defaults to False. + """ + total_failed_cases: Dict[str, List[CommandFailInfo]] = {} + exp_info_list, output_path = parse_exp_recipe(recipe_file) + for exp_info in exp_info_list: + failed_cases = run_experiment(exp_info, dryrun) + total_failed_cases[exp_info.name] = failed_cases + + if dryrun: + return + + for failed_cases in total_failed_cases.values(): + if failed_cases: + log_exp_failed_cases(total_failed_cases, output_path) + break + + print_experiments_summary(output_path) + + +def main(): + """Main function to decide which function to execute.""" + parser = get_parser() + args = parser.parse_args() + + if args.file is not None and args.parse is not None: + print("Please give either --file or --parse argument.") + elif args.file is not None: + run_experiment_recipe(args.file, args.dryrun) + elif args.parse is not None: + organize_exp_result(args.parse) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/tox.ini b/tox.ini index 2fb0c99e73c..fe780508865 100644 --- a/tox.ini +++ b/tox.ini @@ -8,84 +8,138 @@ addopts = --csv=.tox/tests-{env:TOXENV_TASK}-{env:TOXENV_PYVER}.csv [testenv] setenv = TOX_WORK_DIR={toxworkdir} -task = - all: "all" - action: "action" - classification: "classification" - detection: "detection" - rotated_detection: "rotated_detection" - instance_segmentation: "instance_segmentation" - semantic_segmentation: "semantic_segmentation" - visual_prompting: "visual_prompting" - anomaly: "anomaly" passenv = ftp_proxy HTTP_PROXY HTTPS_PROXY CUDA_VISIBLE_DEVICES CI_DATA_ROOT + REG_RESULTS_ROOT +test_dir = + all: cli + ano: cli/anomaly + cls: cli/classification + det: cli/detection + iseg,iseg_t: cli/instance_segmentation + seg: cli/semantic_segmentation + act: cli/action + visprompt: cli/visual_prompting +deps = + py38-pt1: torch @ https://download.pytorch.org/whl/cu117/torch-1.13.1%2Bcu117-cp38-cp38-linux_x86_64.whl + py38-pt1: torchvision @ https://download.pytorch.org/whl/cu117/torchvision-0.14.1%2Bcu117-cp38-cp38-linux_x86_64.whl + {all,act,cls,det,seg,iseg,iseg_t}-py38-pt1: mmcv-full @ https://download.openmmlab.com/mmcv/dist/cu117/torch1.13.0/mmcv_full-1.7.0-cp38-cp38-manylinux1_x86_64.whl + py39-pt1: torch @ https://download.pytorch.org/whl/cu117/torch-1.13.1%2Bcu117-cp39-cp39-linux_x86_64.whl + py39-pt1: torchvision @ https://download.pytorch.org/whl/cu117/torchvision-0.14.1%2Bcu117-cp39-cp39-linux_x86_64.whl + {all,act,cls,det,seg,iseg,iseg_t}-py39-pt1: mmcv-full @ https://download.openmmlab.com/mmcv/dist/cu117/torch1.13.0/mmcv_full-1.7.0-cp39-cp39-manylinux1_x86_64.whl + py310-pt1: torch @ https://download.pytorch.org/whl/cu117/torch-1.13.1%2Bcu117-cp310-cp310-linux_x86_64.whl + py310-pt1: torchvision @ https://download.pytorch.org/whl/cu117/torchvision-0.14.1%2Bcu117-cp310-cp310-linux_x86_64.whl + {all,act,cls,det,seg,iseg,iseg_t}-py310-pt1: mmcv-full @ https://download.openmmlab.com/mmcv/dist/cu117/torch1.13.0/mmcv_full-1.7.0-cp310-cp310-manylinux1_x86_64.whl + + py38-pt2: torch @ https://download.pytorch.org/whl/cu117/torch-2.0.1%2Bcu117-cp38-cp38-linux_x86_64.whl + py38-pt2: torchvision @ https://download.pytorch.org/whl/cu117/torchvision-0.15.2%2Bcu117-cp38-cp38-linux_x86_64.whl + py39-pt2: torch @ https://download.pytorch.org/whl/cu117/torch-2.0.1%2Bcu117-cp39-cp39-linux_x86_64.whl + py39-pt2: torchvision @ https://download.pytorch.org/whl/cu117/torchvision-0.15.2%2Bcu117-cp39-cp39-linux_x86_64.whl + py310-pt2: torch @ https://download.pytorch.org/whl/cu117/torch-2.0.1%2Bcu117-cp310-cp310-linux_x86_64.whl + py310-pt2: torchvision @ https://download.pytorch.org/whl/cu117/torchvision-0.15.2%2Bcu117-cp310-cp310-linux_x86_64.whl +extras = + all: full + ano: anomaly + act: action + det,iseg,iseg_t: detection + cls: classification + seg: segmentation + visprompt: visual_prompting -[testenv:pre-commit] +[testenv:pre-commit-all-{py38,py39,py310}-{pt1,pt2}] deps = - pre-commit==2.20.0 + {[testenv]deps} + -r{toxinidir}/requirements/dev.txt skip_install = true commands = pre-commit run --all-files -[testenv:unit-test-{py310, py311}] +[testenv:tests-{all,ano,cls,det,iseg,iseg_t,seg,act,visprompt}-{py38,py39,py310}-{pt1,pt2}] deps = - .[dev] -commands_pre = - ; [TODO]: Needs to be fixed so that this is not duplicated for each test run - otx install -v + {[testenv]deps} + -r{toxinidir}/requirements/dev.txt +passenv = + {[testenv]passenv} commands = - ; Run Unit-Test with coverage report. - pytest tests/unit \ - --cov=otx \ - --cov-report=xml:{toxworkdir}/coverage_{envname}.xml \ - --cov-report=term-missing \ - --cov-fail-under=0 \ - {posargs} + python -m pytest -ra --showlocals --csv={toxworkdir}/{envname}.csv {posargs:tests/integration/{[testenv]test_dir}} -[testenv:integration-test-{all, action, classification, detection, rotated_detection, instance_segmentation, semantic_segmentation, visual_prompting, anomaly}] -setenv = - CUBLAS_WORKSPACE_CONFIG=:4096:8 +[testenv:unittest-all-{py38,py39,py310}-{pt1,pt2}] deps = - .[dev] -commands_pre = - ; [TODO]: Needs to be fixed so that this is not duplicated for each test run - otx install -v + {[testenv]deps} + -r{toxinidir}/requirements/dev.txt +use_develop = true +extras = full commands = - python -m pytest tests/integration -ra --showlocals --csv={toxworkdir}/{envname}.csv --task {[testenv]task} --open-subprocess {posargs} + coverage erase + coverage run -m pytest -ra --showlocals --csv={toxworkdir}/{envname}.csv {posargs:tests/unit} + coverage report -m --fail-under=0 + coverage xml -o {toxworkdir}/coverage.xml + [testenv:perf-benchmark] deps = - .[dev] -commands_pre = - ; [TODO]: Needs to be fixed so that this is not duplicated for each test run - otx install -v + {[testenv:tests-all-py310-pt1]deps} +extras = full +passenv = + {[testenv]passenv} + MLFLOW_TRACKING_SERVER_URI + BENCHMARK_RESULTS_CLEAR + GH_CTX_REF_NAME + GH_CTX_SHA +commands = + python -m pytest -ra --showlocals --csv={toxworkdir}/{envname}.csv {posargs:tests/perf} + + +[testenv:fuzzing] +deps = + {[testenv:tests-all-py310-pt1]deps} + atheris +extras = full commands = - pytest -ra --showlocals --csv={toxworkdir}/{envname}.csv {posargs:tests/perf} + coverage erase + - coverage run tests/fuzzing/cli_fuzzing.py {posargs:-dict=tests/fuzzing/assets/cli/operations.dict -artifact_prefix={toxworkdir}/ -print_final_stats=1 -atheris_runs=500000} + coverage report --precision=2 + ; coverage html -d {toxworkdir}/htmlcov [testenv:build-doc] deps = - {[testenv:unit-test-py310]deps} - .[docs] -commands_pre = - ; [TODO]: Needs to be fixed so that this is not duplicated for each test run - otx install -v + {[testenv:tests-all-py310-pt1]deps} + -r{toxinidir}/requirements/docs.txt change_dir = {toxinidir}/docs allowlist_externals = make +extras = full commands = + make clean make html + +[testenv:package-py{38,39,310}-{pt1,pt2}] +deps = + {[testenv]deps} + build==0.10.0 + -r{toxinidir}/requirements/dev.txt +skip_install = true +allowlist_externals = + rm + find +commands = + rm -rf ./dist + python -m build --sdist + find ./dist -type f -name *.tar.gz -exec pip install {}[full] \; + pytest {posargs:tests/unit tests/integration/cli} + + [testenv:trivy-scan] deps = - {[testenv:unit-test-py310]deps} + {[testenv:tests-all-py310-pt1]deps} passenv = {[testenv]passenv} TRIVY_DOWNLOAD_URL

          BfcTblI6Or`SLa>|_tpp`6!p3x+Ue3V4`4Gtm(z+AA8SgsGtW`Y;J12++*kj&Li4Ae`d_ zYV;O1eFsn*JDvXbwm^Y$Vr-s)70$2&E-z|F51@O#Cr&TXDB7O3#Fwo3Q1p6bMaeR1 zUvezl5&z`q`|QX4(7?TiE=^Gs+|}#@R=LuW(hSk*-v+h7XyDyjXY=;n(}u;0a}p;@ zA5I3!G$}9oCai`@?h;e=_VZeetHW2;wyS2lK-b8L{kNb3b-YCPwk-RE9_%}oprylg zRR)dFiW*yu4FmGAEcOGsTrP6XV$9A)}BNvVm&K z7Qgvo6%pPbq_-M~szI(pZ5~A>k+(QJ{4lBs`g^dfmC@z`^yDnwjX)2_LTu8(VjeYG zaU}5$V^S19)f?(jF&}8NrHzKghu(p%X7UR|fi#gvW-jWN!2PCE*PmG*Zk+`$`UUuH}3 zzWmX=8$Mw$=6 ziqgKG@r+U&7SZp5EpA+IIXpa^`Te8vop1~NJ0QEkR@PTO-a!Mde07E+pG1~i%rAY} zbhac9g0HHl^%w_46vDOR4RV{7RSxC431nUg0D2hserBZ;nk&$*zxIXo6w=x@t@k~5 z22evWb9ZSSe*s(X8iKxlejw&53YzQ6SROQ#C%TMyxwd~4`k(|W%gNAv>hO!j^Mjw2 zhHZ(7Y}aJ`4D))^N3F<=EJ*0%;nRhpZBsFMK9QoN_r#%DZI{tug_dy*8{Xj2^cjv&Y?kVgh!2@i*s18glPZ`}5Uw6lmNHklkVXOXoU8D1Mx!TntaK_b#At~1VnUio>6lo`bh8D47Hs*h1O57~lAynL(B(vz z?t#nVVvOV7am>2jy8Ay>=WEfV^o4}sIzs%))qaWU5zg8l=7C^GXTNy_USOGBZ_*k&$g@maNM?r?*+bB;vSy&*qE>nf*qqUZhIk4aObuS2}^yQ_qz zlO5gH0l!Xs>k5BX1Y_^U%*SI_PLepNzo7tHJ-tg34syH-k_6n6@N@9*P>Wx_D> zK2M6vy}9;v_-n3jF?lfsQ>%zd+X^u+5e>=@57R8!61x)d+$$BfzZ7DoJ8lRWt5oM3 z1tC!*!{w-u#^@x*S(~tp(HZkarN?eyRL9lc`ho=m<|ef5vj>Ln#k=HGiQ0bAOIv`a z@Y4sPO5J5rBCoPh*0BK7`-A7{=>3+v~3@$-OBr z41)Kq8t+dc2TdX*KXhiBwKzxE#D9{DN+ZFQGlx5KZGzKWo>$?K28P=s!i_z9i+N8o z0KyM3lE)&>gVL@tLeJ)Fliaj%{rJLuhpm|-($kQsUQSVN>S)~GbBc%$Eh+HSyUt%# z{2gRb-a|ochma(-s)JlC{}N=4>VNlD#8jRSth5+2Qh>u`@?^NRbtDdK4thWM%g)y` z&m3rBX#5(T?6jkT;bdNN|9E7S_~xC7?jbb8M2BfH&e<$8ANNM_QVQjoAN(lBA~yje zf{OPq8&?XKHQEF@Bwi9et9o4-&zt#Z#_n-RtonG*+Ki)-C4U25;Vac`XLC^hOd-5d zV;U#s_$;nm_crEE&XG})RbxUL=UL^lF3klz`m6qT>9-NlrK}iBTBXfG!3s#q4aEu| zC-`y_IEq}5v>u0ZqYBeVm6~$Kfm85XwzF!Mned?qFjKk=f(~vfmG?o33NYVjlFHXo zEmyK|BAi@sSH=w)`f``{1{%`sbn8z1OY`(;O=h4W%?FSHLw)?fiF81Da2-=?zQhf~ z5Uq!49oR>Q0n*VE`|Un!Y-lY%`2MAf+OvL^_p)x7(dzXIXpf#mxjR#{FmlhxlN(O= z6Q_i00~wD42o-_(DcQp@D%IRjg#tFan>m)QaUej~N#j;BFgju@Q;{*8hngG8xRhv;Crc=8`sVmNOCl)IccjZHy_KXMwd=tOg}B3l3vEuJ4u_ z0ZR7Pv52_THQn?~x{{;$Mt?pnw&*#Tp8(6+PPWVg&59<~ewUUN=8PuUVa!vq9KBdI zfp8y(hxMJ+JRaM1xpDDjc=}Z7vo4v!n%N#no|>&fQAdhF`tWAqh6Zo$J`J>>O{LaM zD3>3s(@)6ox(%=uGa9=8*MfMQtHg(CxBdk0M55+FK zr`DFO@V8A}WH?&xTK$re_NzeAPl#ZL6=}(D7craL4SLt!@YV8356n)&0hR;>S6cy=eu%q-(y8g0nQ;_90|XItvH8qkWG zDk@3y-|N5s;Qz7*usMbqyl$d@K5u)Yu^EOmdpK&I=(MwUcB0#KP1RM-POZ|-6BG;g zcB&Q5bbX7z4&CNkD~Aq36dmCm!@B;m@b``e2YSPwvx(r2{cYrcnjfDxeTuMXAt^&D zhVdLhvNn%sXoFwy{Yztd9|>bwzGc$7b>QINKb8a6N05f6L|tLTGGEXbOt+2aJLV&= zSwa}74}aEok7m6rxAW$^9%tVR4CanG*Kc`J(2jLl94}^pw0#R->ZVvvm=SH~)8d~M zKaM%iQn>!@n|c8J6tN1^d06*Y@zPUQe_^p{m|lyMO~$iReZ)iVRQKAn+nW2&#wq=B zLPWJ~fPGlar#4%276n7)X2E+EK7*kRhWbx;sft2#lgiLlOD}i&rc-e`tY$XkBMsSj z6qAZ@e?Por<&p-T=vf+Te-p8LbF&D`oY3}|%K*y2xgsM?Z*J9q@ z_BN^nWsq(lemk)GXe2p=lDdJhj4z0PG5leMl8d>V0xyLofxH_1I%7)Dz8b^t z*;qfLO}A`=At%75KJY`ptZ9)^pY~nQKQY8!;9>9*z_mxSMy9ju7$mDdMimKw@i<}5S3A+(O)B4o|bP!D^~QqghY_$YN-pvE-p$sy0? zoTqoyNYGm>EEo3G=#Xr1EVkzd(#My%Fg%lH1vr57lVmD9Ft({pRhZH zn~W(g(~|RHIimv}t;?7#Txuv_Vi_LtRddUkXmY)}vHI2BilBNUeqE^%DpE|R>Z1r# z{S`DueT=1tv5E&nZPgD)+gVieZ*mP8q;_7H9(X6J3!~kGK#m&26ljs>Xsvuz&ibLC z%y+km!>~}yfr-WKcy?O%u}{C;3lvkOBYS;aJ#&}c7-jml%i?$VU7Gx50$y0bHXTLN zB!ESVkZW}<8yIWXfS_C1%x?6cPIv#!D1&;tbddty_DcSj4D(1IY9lMrc@x-EgBMBl!$^syvzX<@T1 zdFKsU_0cv{44+Nnv*AE1nASZG(?IW1NJ3PF+8R|sYncMWm45Kh1scSru)+_`3_GvCZE>5kk$dYGBZM01 zeEM2gznqGfiqEb!3S7*FCLbk{xx|q=LoV#b1?C;jAs@lB07ozQ0v+v5CJrbx9*3TJ zRlXJ)s+r81OH-XjT z5gsK8TZ9L9LF}&j?c&E9Lo`QJhijLHYqul@qUBB3Y+R+vND|Q}8Mx|Ul910#3_#}DD`(Xj zE$)&=TAi+Kw20wA`{m>{Yv`UH=<#x3E~9hD6}(&zTyuiuGNC_miL>~kVERuIhh_Og zI?2m-TMn&;tLN9Fu2|ru5YDQ_GV&?X5>X$lj!2RcnwgjQ!Ew^>S>|73tQ5H_1<{4E zNO*9C1k*Q4!6(#L||?zGD5yj+i6l-HH&JT<(PNK0JO z9lc^1SK1q}pgS7kMdD&Y)ez-#=&H2Qg~H(S1^r@0vq8iK-dvhK>5gHM^L(}IcBVTb zAP?j$t9?B)Umy;mOD?cJU1|Z9R4&r`Wl5AV)DRAlP5L^vwlZfG;#g~R)#gz|_Mm7F z%(@Z6ikI;&02j9K`g(ddtaW*RP{o7oHoBm$8d`~gzUJ=(KcT)4nY zl~uzf#D7}j!?nEPr zt*bNC#)KoO>kErNWKguXo(bR;zWY>u4*4ZDO2*51X|{N0N16%!+8juIEY1p=LwF-* zWz2}toRBUgjot0RC^yUeMn&ui|3qP3d~ByT6dFL{v*=JiqW8jrEMd=TgqJWVb;{wI zm$>yj7sGv&n3DD4zC6=0@d?!J;bKPW^j$%-F;75=FHJI?&J%gzYBxSTz;&JSyG<#S zt5FXO-BKP_EeZTV_&Fpv1+s1z%^`qlf47``JOKrBYj3)Bk~W8abnF^@N*oY4|KBsd zgG1~45b-H2$Ni9hX?7nG(7P45*#948%kY2hWB=z!`OlHEt3*UPbEzT!XJ+4B<(hQ- z-anf|6Y%R_8a?Mf((X8m3WycinWR@}kX+K-3v8y2I5+Z!W$^2V2 zL?EUD_*zP5kX8)Xjx!2adRY5p@tVKTiq*KLRdw?;@3uxP37oG(NO{3J=e*XL>n|JK zvj?3EIp2PyoJ75>AD`O$h~14^?|8tl7|(yiKTDiQzYUX~>2Qc{eAZaMoTxwWRiZF3 z_$h;>F5a!cde0yc-& z^Jx`w)=twfc&WQ4WOkTT@U1Ov@D%aq9>0mqS?aB| zaxmFPOgaW2n3C=wWGr9~50GG|_KP?Z&z)zzT9`3kI`{^2@u^N+d{vs8u~fCFx#k%H z8>_@mP)e@QD5$RSV*WxGR)L1Qz5${eeHOVcY6j&M4zHlK70S>?asBKF-337wt1 zWTcb3MRQhnLu#S7em*UM1e1P*v_UGP$Ocjx9&Z^HZ=frar1m8vUnOk-6kPQKUZ-Tv zX_b~kvz$$kT>(IKG+=CPCQ)8s->&6*!55$-x$ks>j`NhN@v^Sm6+nn0MGLMt)IYg_ z-Qkz|U6Z4EwVj+Zo@T00WC(4mfDjgFw8;m(Z zV7xJT7(xH#*CPY@^-iXLINm@CrK;UHO+fe?k`243C@;?#w}d$k#|b2r+3tQDvX@eGseHx9q> zzA=RJ;91D=%v3PU}f{yn|7FDdk2nuR^S z8<{RGPASuz%%km`c6GVgKcORjr8Y;a^-Dj3R)*DSpL9@svqG@6${)b-SJHv{eVYbo z;!ob@eYF&JK^99w0F_f&eotripYih3_c8WN(Wk;skm{=04H>Q zxn8R2JGc1l@fHV|_(M%SvfBU^d=FeQNDU}$r=zTDV*gRAio{;vIKdaSS!_rF;$WM% z+MyhaEVjm-*;A%?E>oO1oQXrC@+vzrld*v|hu5~%eE+e^Hh>}2!@PO zS~A9H#*t@rO$S^evL&{`51izXmwY^)0j$t7O|58|@F-OJ9Hb?7#K$G^J?moog}e=~ zGDjYsM#mVh5J@m+Wxzf^8<6J*xf<29G^KLS7Q8|V&{EcMYZQ{2j{68!Yx(Xby4}CN z{@P3U3BCM+k&^T)cAyH$$4ym)I&<_%^axy?JoEOSz^YM`3Di&TV171h_7nNZxd*{v zCxL_+3xnHK0}-&$=*d<9Z`0=r533j(b`QtO*X>~L+ATI?U^CLvr*CF>6yC&!X1`o* ztKf=YfcK3Q=u+py=-UnFRkR|Lk}UhNW;}zZmQR+; zqp|>T4?WjY?N**6*8T4By834^72!>%xy0PSTV29r4^<^Bm%M1v<9r&*zcg(Vi8Hh5 zVL^{pJydE0wbSBgYJAF*q=K(~OhT7QFONsgnZ6TDqV@Y};Y#ZAlv z>icononLASn!A3}6q1NVGgBW6^`U$w>Db7Zty3U9h(M>Hg_-7&v|D|Ck zFBv3V=W1I79ZJ13SAvD8hCl7o+@HB(^6byz;bW;~MqQNfyv4XWe^-Rz+b;YI76JMd z$W*G(9tsCnxDB&5po#+T@>*_WH^@d^IVPu6F?&*t7W;t)J@wOa{h}>88mM;Oz8L)D zp>aW33&I`22#S`BeTUP-CD_>^lHK&WbpO5)&qcDHT*K9bwxn59ZoPS1EZS7ZTZdiu z{oUG-Ir+~YI2~83x#==0xo)R#OXgQs-==U@PEsaTCXNlRQk2qlUl>#eu$6^Igsax) z`8O`3+r(~&d|`R4#FX&YovtjqM>mJmwxux*`qTRy+Pr_pOgl6LHt;vA3fd{FVYWU31pW*-MjEk z&9;G$#f*R&i+H*T4j<)7!e21sutudn)%6~9a4BP!J;(zm?|-_a2h8b@^jdi3S(+oP zn3>gW&CdB73^AY-dcz#^a^x>Nt0)BJ*EQE^EYzz(?yfZdsBy1dIWpSvUm8zR(h+EO z=+k-hvq63eeI7u)IFdJM6EdN5WPDN>3H~$wCFu)L&O7%PxJ|y@+AX*SP)lEk zBr1|fH+h~Ztmhsp+QgufD|$;eWU@)R%VeAH)}!sE8#p?$7NmL0l9PB-dO*lhA;#2C zDz=({=4-TQEkwhn#?2&2l?9ySCaJy@r!8KttW=jmgMzPmcZ0GfkVYhvEpyTHvRqaD zL3j^urrwdI%!?T$y%Ymo4qbgb(vk^@QhhyY16~ZTyQXplW8UsB`_sEDcivevgzWP#lb?kf6G)C`=?|N2W zzno#0yDi2^Ge2NDd+~qfE&5;jAB<-YOBrOZ+~pzOi8jh56q4_|@0NCXv~r zPeCemvUNj&Jd9q1dZb!sX5LHmaKbkHt|Ih1*f&^SwA;kDRLK*`=z{xT!hEPl*Y>Tv z)##s(t|98=1_;pRA_^OHytnymVE)$3YgfO$V*6{xF2zRklPBH`D~&LD)$0EZZ%Aat zrPLi+7)_!&vMcSbQ3G@$nZjc(56s!R)@u8_RFy|(ql~v3e2ztP3#&MPv5CGTqYrMM z+;LUsbfc(0zuPN!qEZtmrT-y)BrlryRL#9sM(}QRxmVAwO0A-I8dRXYlz7UQ%xSnn ztEtHP4Qr48hU~ZcLhs#;_6rW#vq-}RM# zr4c+^3U52-%TLGm=u}7!IpYbME?xo?aeQC&B(grQ@7mc)`k!A`w0>(5nnUxypL$T) zk?@-Ct*4}FFE;3gg4T7Dr@S_uMR;XF`Gkgq#U~k{c#%%g672c;!o6obHg5RDU!^Vj z6UJ*GZY`i~DPqCX1HQH;oYqd3QQ1O2Z;HF3aSN`jN>;Gd0 zckA`>UNn6n7QS0P{P?p|@W9c*@#ES97U+{O4ztdHd++n#_UQ^vS4Q4s8KF-Iy|Yp^ z^!%KhjFDGR$YHP9nRY+xjt?b@Xfz?@}Sd8(7I^!qd?xNMT zSbB`nmChms5i$O+uvp>}=A29h&F`i&b((z)9*XFnQ<~q%Du08ycCE=S&yh89y)ur< z0Y^qWU;YR`Sr#a5K(xJNIH_#YAIXtBF1wBQ7!y(Hsh0}2L}qX2m~@4I20)K9AY$5g zjQ^UEP-S8V0f7#h7&r~NUy2V0Ga7bDU2l1eX4|XX35Ix4L>H`uXSnq&Fc;yCCiY?t zTzpDDTBe?c*BF3}DN^{GE&kcLq^DoddOUCMtJZu=hA6$`dd<%de(+oD6kn#7F^qNn z^(gEPsb>O-))8H9sCP-9RND}<6%SWwb|KWR3VwB6b&-0TQh=VKh{SD$3Sa#N4stIy zx6A(GIZKT>=l5_jpvJtcz!q=}RhMf<*2l)&*9vBqL)7oEg*_K*ySRZ^+ivr{k6N2E z`1Fz8q;{-n^Xq5m_jiJE->;)?Oczq!aoM$`yBUBP?c0mOY_9kxYU14;M{B%qji<+8 zpKL{_H$GKa%i*QV^YyPD-UhAw=r;?KJ#9$=PQ3cS$7YtB!=Vde3^XJ7;(tx##6SUi2y55S3K0&w*nN2D;cs zNXYyHOz&)s&BP7DX_M4h?r-(Tc$c(Io=f^tK7hzeCMQn;7|;6gAK(cofD3U_xm(?a ze(u&?YmYWo?WYgIbd*glFFtG!@Vatq^z7J^XV+WXA}U<uYHjck_Y)8 z$uBvwrQStQKE(5g4(L5F94V=^GGX%(V>Ai==}@Z2I5Gs-xfelNZjOmNZ~(uqC5g!R zqe%nhB!AZZzYZl@YGA!M5Sz?S;AhAp@fC96_`W_?R$R^Pqwo+&qip*=@6Q0-O@Y{e zh=#-@4kfKQJ!g%apONw->s|YQdq@qOFJh~=1m)pfIx?>{$Tt$)UY&s`{hxp#O|~?i z0Ft*OU)U+`#ul_!1ePH3)xzrnWcq|7CpFs6q-y!`Qi!tj0I%1u=f(kK*M#)sX!~IY z7U~@rINRjb@~ZL@=e(<(8gEOQEPBqC7~NZ+)#Bp+PLHiy$LSW;hBVivjUZ9R`a@qi~_MB{;Ej`2^cmHroM`eu9+F>e*d`Et!#Z4-dtV1sCu6U}HO@K7K{GT* z`BGG3y2i>1Jcuyib@@?VN{w18V%n(t9M#dvd{BrF6`&HUzSj}>5Z&V@{!0K8zxh@v zm<%M-sesW9nft-8)7R%aO@Jw4Zz^T^hVI~EHE|OElwL>>jiqGvO?`)%nTnKSj1UI%-s&IALe89dTmAU(}lxA z)V3Mvkk)>3zp+{Q0%rpStcXM956JJ)6@l{=%8etFR8Z#k584i}7yKzUW3mtL#X9k< zSrY8FIqbu480P#k$c%6rrDaYx~_)k49or5L1m%qinhO1OZCDY2AYrJ39-^lDqa-GzgUx`Ko=Sbq zD?|jULP9%1wtF*RfNsi$ihz&~pta3@dUz5csOZc{>-qzt~8fa%|t&0)6PbnX_G% zshTqT58#@t;ibiP@-sh)+xJbBblBQ{^@AP*btD3b-YnO$D-XefaM-NFpXlg#%ya48 z|Bt=%{Aas=-@n#qYg19PRn!V<#isVA2tkb6B(Y)@trj&ZV((bBMz%ul-^?HVH1yynGQFxmZ`+BB zq5?ZN=&%>v$4C7EOq4DzU)nF8a+%ch$a=cBV;n>t74u;46+awz|KFvzZ(QoMNwGuy z{VWf8Hgc73+h#p9=rxTk`}^#DS1GIC*O?$E=T{A`G=AwYf@bhsOkcv89*STu^rosU zJG;EOKyJs@pMsvuy0S%}R~47jo}7B|{iu7}D%7c#04dS*OIwii39B-B4Zi_$*TNg& zrN?dZs91d{K?BbwwLPqdA7xQaA#;?9s2v$mu8P7j&9>a^&KTdBS3FvH@_Rt?-jznR zHLJ|`^$mg-4vuI{RlK945HP#DO`!Ai+Ipj5cUO!LT_sO%ZN0+LHIv10Kc(CAqkqiO zR)B?sKA507*^EV|O#QrKz@vtIlKO7PJY+0rTx22)hqX@~y>hS`#pf^Ap*G{%43WB5 z+j)kxBEoizNB5mceQj(G@xq~vz@MLVh(;CtUK&fCTy+_siSpRG0L%BQHhHm|014O0aH!i(4l zyfFB5uAD4wgQhmkd8}&R7N00<5&#*6D3b%YgR~oz9#4s`D+DLWV#Bb>(1sF6*T$~N zU(d6z zIFEEXRkI&&I|tkoT2ZLde@OX6UqjO9Qhra3KU#q(wNi-F! zz7XR!w&iZzEj*UizDCdFW4dX7w0*jIj@8j_a6Cbu`ZnR=&W9am&`7_HVu&OQqPNw( zMY`=tm(!^IabppdAoX+Xe=}+9VJ7rxbJtZ2=p~+Ia$~pm8~$GL_Du`ZLU8JEy;ou-V7lCJ~V0*0?EAE7<0Gb8O*p96MllUc;ddF zX=5D4dp^uy`=sOMZJ@g2C~kgXNk}-v$P-D$MGe{OOu(H|ksyWiim()9?-5Bb(@dQ_ zyUUmM#Llk#AVBLD(XC5f|@(wTdz56^wxn)lZy3Y4ab>-t#NA$00`Prw>Yl-*D zMk}i$j=E`PIVBSNM4{jGl=9y9#BR16odd1dRyi}|C)096IJ%Y*77RBo(DiuK?#YMR zQ3msD%g!{-xH#^dZuM5?n;?@tldjmW9H_(ZIQOMuRtaaF&Gm;RHej~V4^WG&tNokZ zPkfSDuMyo`4k!iqxwXuLZ)M(u_56#X_DH?XH4oK@F)dM-Vb9^xEEoLB9uOQjO!h}k?o-!IM# z+SB|#wukW`+*pBt9xzO%_i0}(3FPYNkTueT>oyll-@V4p*4=2RE_A}?kdHC@BAy$9 z@^9~L?qG&7&gT8{%AgSi%=@-X$w_aTBZF73b8!MXD}Q3HJ6p!;Pt;Jpb%DV#Wo zZ!wGofBv=B*H+d?{jHvR;b*{%f7=<~AR}e*zcz7CHIra8xw;2ha4_nQVSg7?Sqij> zww{qn?V@?=7ouu+A?9ASr4ITw56m@4@ z6m8g-&TLLbv<>8JW1e2jv|SV6Q6YMa3eJ6T*+BDoU{E>d79XH5jy40F`D@l~Aalcf7m3Ovf%@1%Iw;)K<6Ob1BCJ0&HQuRfhPgWN_uf+^G86$a|`& zgIaq^l%<0m=gJuI76aFDn+>Bm3I##O~;Zc6iQbrxhE8ZXh<XY&!>VRs-=1yw(l)&^N`84^-kvrV>kly0Ahr2p{2kT3@}lMewy;HTP-` zU+}f2=W*vt>2Q=|r2m$BUDeX0%#u+Hi>!GlG%Vg*HW#ae96^d$(s#n>h=5z%pf@Gg zN8SUiuM4C!HN`!u{k)g@D*)vHB6OxB*)4{2J0Ug?OGVUEB)M+mrLurv)BA^SEiTXMxb z`66`a*Spr@HegI5=M>oH%gK<#Zq0EjGse<;v$5X4*iKxrso6LSW|*>}-InQSa7)4L z!@OuHm=tmg^CIy9$T)0M44;!ANH{#i4P_wo%?jnV4j7-K>K{Jqk3M)$!R?g!J9f4` zS*5*(S(cH_`1yLZlxto|$!sLs`F6?1f) z3wQHl(<@l%$6D#x7fRE-*aPu>OQflV))`+zwKJ9*k}O6cV*Q;u+X0gkWa0o?>p>sN zXSpIU=L!blsJe2d6r-Xref>RB(_T8r&ZtkO ztSiOVgV!PZ`hz*O8gGSAod_>BrzU{!N@PhrL@ zzG_TiDC#c1WZ1P=#rI4PajU9ow6g$hZa=y zi!mWpFPg6~Z7cr)#+BI;x&reI27c5*jFT^yFp{@K`{wZ=u z36YjeTL0YZ$W9>Y{n&KNRj`*f5951cgofym?c_X%nH-cf9maV&11U_9DrJgKHTYH; zywe?*X~TGyJqDVF{|H()fy&6*Z;Ks6iQFA>_aa$9VNwQi`i2}0HI30Ys#-TEEgJ8} zLCx#CtzHD0Z)T>&JP__y^`d-KmC1`Uu=Q3%L#MPZ(t7&DNzeKNAkv$ymaf(R<92OZ zVZHBThVf)iE^D|NtVVzECPd&$x3`GSwWTeIaY8K3>tvuE43mYY4#8xK=0WRx3fZLQrn-G}wy5bJW+P&Et@p3|s z+6IS8HpV^tX>cMt3H$b$z5X+7H4oi0a%a$H;*935F+S0p9y||b&ajH)V9%T|lo~Xr zoU;98aX^(RRm*pr*eAAFqN$fKPm}@3rVu1=f|b7= z0!?I918?b1nMm8BGPGDe0&3Zz6@U7z8KTGg8y+hLZ)BLuDmaxoT6Q@>q?XQD#w?Bb znw!UYLc#G5Kv$c+Cq<}t0bw@Oww&K&iT9U@BG$T~KhND(J^q$apxkWEF0IJ7CMD7T ze+iOc(siWwE8Q~C82-@#$$>z z>ASp@Mq=vu*Pq3kZGPYH*79H0ZO)UG2ZX)Tk6);)bhn7hf_Pu&)HyBQse{d03~SZ& zU7aQKRxrlfY2Qw&Y44n{Q?8eiCrVn|*V1cq;eQ)6|8|cV#vSnRqXS6Hv(XI<+`=u- zu3XKull^1-3A`JLiVCss=pO1eeHj!4oW4i6<%8J5;G#bv9FC!0RBt4*ZhAY{;4u|1 zSExa-KfFu_w^o5kZ#w4{#uH@>eqKY<&&>21Vo$Dr{`o)CbrN)jLVEQQdjZPjV7fa7 z_$)a0+e&GbIr>o4YF59yGp9G*WJa&3kaJme6={1eb^l06LV_5-Xu0M#pD-1*TD-Dm znCuW@LFAJO|6-BmJk`+_#-x0iH0t!a9n5JhFgZt=r;R!E=B$b*%XV$dXF{jMm)|3e zHCi1*IYtG2gFe*HYTfcTWn__bAcC{Frk zLA_bQbHV6XRuk^ps;Af_h^er?q?%u?dL$`mGF6HcaxYVO%=k8x{IuhT$|s&&yvWfb z)T-E7$n5LopCye;qHa!~5uZ+v{J@x*X2JknnpHi&)I}S}{(?+1b>ZD#=kAUE)3$#% zFQlX<&3ZoBrZ5{P98~mpo#_T&rnC&aeIo1lGtBgcncQ}4b?f7 zX^dLb0Q7Z-P`sb>#7Y*adVAxd$IqzJPUNLOkv#P`a#{H*dimms4lvyrSGX_ZkpcR}j z+s9(23A02^m>O`E{jV>@jWK_z<4Ih6&j=g zNg9V_g`SspOw>l#P`6(g7JXa-_0maxZ~O~1I;-6ycl(=g=ACz4hS+jv4Fyp8?4^1>Ft|gXI1MAs&`)WN0bZ1v7)84=IsLd+UW1D@MW0KSA=)3%l8& zE4VJ+-+KO8t-*K12Tg0+$9}g-$1F_(`=TYd(y72ulFnBNz85)XE2-iYP$D7D<|+D{ zOT}{V`=EEP@&_+S^Yl*#-iJW%Bzw;t_g1i&luDVHj&JNKi*$4K-uDG71DwA{9DA-- zR#QBRaEWAp!`Y@V@i!ycn7Op=*e1LCo3MKV8$3^v#M!>dCV#_Bw=b?AG8{TdTB%Ja z`QRR_0&=^q+oNY(cR-sl@0HldRkGJ9_y8*!OQv`qKsplGp;hY^oO=R#F5cU0iH#h2 zjx)ShTk!kkZncq%>>R2*n6Z?9-2R7ZWXNo%Y1G_Sn$m&3Y%pK<62~4OfNwa;&P?)? z_;UR?f<7A!7R~U=om8{+H}F4=?d*Hho~W^Elh27%nm}T@nzkkPF*c7OuR2r!WepKf zsYY#%u`uJtN+4$tkbZ9{zeNRH4Cx(s4+<4d)FnYpji$o@xgzx{+q-q72B*Bs5DHv+ z_E=v-UwBdb<_o7ym(|QXy38*W>64_E?V;muN^@pY29ba_tL@jUIpWk%#HI7K+UiFs zL&|_S{~5RSRx{ay7Ybb4z@@Mq*NB} zx_L-W%GCl=GQ-7$nMV>p)riV5O*y7;j8am02DM=NaJT1-lz(ucStK~8EF%>@~!g*nZ*6XM(IIWauTlSvL!>u%?Y%*Hw#-@we2hR6NYc|D5LU z7X55-WI>VK-nZiX{ymB&kxv&%sYKO7^Ek#!Pl!jdRnx1@ZjIOy(zPxb^hsS}%lI%n z{!Iw8c8wwPLz2V$T$I^D;y1hqwFaVT_WMU!Kq`_`n>}o=FG@O9>@vm9RNcK2 zLrH`Mm4w(7{$rB@#N}9#e=s}6)Wib(8}RbXSr`_G1qAow0UmNJ3$0n=N9rYh2P`XFlpvGY5_`D zY1VQ(O-zGG1PfwY<}&3AYL;cz!r4)0SUf#I~H1Hw154=SPf(|LzO1%wg)-LarYO7z&>I}QAg|4#E5s3;B;zPxb`#qCu@TnMR2p4- zwV*Zcf6~rt+IRX*;Y4Xj7v?QBGe9GIBj#Xb%jvx(3R4f$6HEp?sO5bc>kJ z-8jZz63k?Lm#&1K+E6TnwK4S$T`6z01$pDlDwhFT1x&MVwN82ZqfY&us`cfGSd&v` zfIA}8zU>bjeGRnsRbcH^~?E`bWG+2kDK_6SNR#@d8FCTUn8R72!7wUn_C+EOhX z*e%O6)PER$?~PpX!kW&4nL}fnKAH!@fN4H(fqH;wx-;_yTRLTn_!~&?B^*y&+V5*C z+VvD9ae+R+VNLa#GgWeK-&MTCJS^?e;I;kD=gzux4V5@t$c&R`avIh=LAXe%cy=w;%a`YL%f)2-ttaY|Q0J{-=^A+-TiJxe>t>Y||~l5~VV1`ux*;6VrBb-d3`ChfSb++Dx1BRVMW=K=H|Cw1A~kktVKow0%j7x zrC}%h^SmM5VL5 zaVJ0{p{+Ig9Ms8~%FX+7x18d}msWcSx*}L(t_~@W(UYOoTS?M3Ex_v(5w~&faGn&s z>sPxKldCpRSm~Fo=g?|FCyT3FHkqrbHAM`@vY!zhnt=G?<9he6M^>er4co!R3+>s; z+qUg<1*N%jBW^T8*RIyEuB2@i*nestr)Ex7FD-ih{jO73RC-Dt+ova__<&2uiVNDhB?#bqc02tFz9CTghx*A!vvn zz4taxr}~p)jOMmYS59Gx*JfN`$3s!>FU|_feN`E$XnE8b;i*dSqJjP>&7y8qMEHR> zRdYQ|Fk7**q&moQ(W?3~!H)Fce$+(k^!oJG^SVxn`W9qmL~!P{ z9rbkXU-q>)-YnL#b?%T9NHu#I?U{8E-Y0`{@`^yfF`mE6gQzg(dy;hFNTkF;xWhS3 zrJt4)J#<9lQzsDC8ny!3Z%T%2+Gj*ya(1XiQ!#(=j|N)RsiC}~t*iC$?yNOMA&qD; z{`Z~Wl^fw`TSR<9<>=K)a|zh0riH}(GNMXZqmF<4AD*QwB&2;Qo&bu4J^5Qq2{^_5 z6yYgWOH#M#kWbC&1#g6s9B(muP~C>_me&Guifx6_L#wE~_f5lDTl3R6@dt{Ca>~0_ z=T}YV$^(EbHBe&a}0bR9K&P>|$#uUWpMF$$mx+$Zx)UrfwOe zbIC|tO&79_XMOG|pTGZ&j9^;RjHAe*@DRa`7rH|p+@OVzT1~43@8)jmB2GALLeSgs zblw}@?V*1~qCe$Y0INH{7OiAV03r8KlNyb5`h5M;=%+X=@`s_*qQ0tc)?kv26l%w_ z*kZ*)S#-9+$=+FDjnI5vQzds&4RdcaZ>jqQ@w}yEHNj?oM3Uv+dY~fGNL1zI3o&l4 zjC6HOaf{6tB=kAz^$O&IQTw3;9c8K6j+56?7y)|pqIzw8w_zsnl?!*Qr!wg=MH9`0 zG94(nw=t1g;=*x0Bs`I?i1wijyD3#_+BE=tc*9_or~G1v;KBk$H>VU&8^8HcRKQ4% zr=Ca$jp9D6VdmQx*=0B|l0eL3J;!00c4;yvwQd{x>F_$+ktuY4B zEGmUnJ6VjtK#ZDvJS%;~XJxPZInc_U)FKxcqE*6f?ed=O4SIdV~b}!>Pqa zIVht7dD`61{n^S`ohw|IMEEK~aZrE?c>Ca7^nv)R$2`L_=R#aibw=-oNXhZy1qV-G z=7%PB45@tvPPvWeS^660PQQa!`D1Lb-cFwMkGY+kyUcFdVj}sQZs4xX6#*P@sf1Nn z4fV0ZmI}OCY9Ju-RhqWSa=X$8``fVf{u#DZFD^IRojIZ5_Psz5+FSClfulIk{3vScTs%8v3!;psFLt>M_gc5$~rEpTzy@lmo zFJM9UT3WSnbi zT4xHkIq0_)RXtWIsaLBhy323nC>TP6;*JA`D`1yZeTMLQKjtW@JAMQpF;o$9F&&#B z@6d9{+OUOQxVA8$&O@r0SrvY9r(2;WGIqPzPO27xy|WI&{%z(PMB6Mq2AO?3zWj!H zEj-=1I*br}lC`n1px5nNBnjs_fsaRS-?k8syIl2hL=n4-S3AeSgZHzqk+d#T*&;3n zKMF=m?~Wzqov=cw8rdefoaz6P>QhgQMpD>EbIGKPX~s6no7g z^CqU$Ex7n_rL#CZO@aX{Ui|bG`SPVuu*8H3I+CL1Ad&m|MZ;H!q0s2;y4$Y1-S$$S z#DC~(2eb1vaWD_ESqqQVdG*?UhNG8X23y*47q%LTrMAB@CwSOOhXHlyx4rACB8G~!I1~iRaI7dzIg8g0kjV47>}R*7B1zjn0ThBRTn00 zZ73ATQM2KpftXhC$`Z3_x9j+NXrK$&cjTq9JmS~)+_pA|>4up3V0PLEf={3ADPQ1} zUtBu`cT8b3F_e!Q;o%-E+mJL50?v-`3#zx zwQ60d7G-{9MHbL(b~}47tbnE%L_W@9mv4^3?Q?St{z2FsHOU24wEE!+t~f$2j`THy zDVNrY|Jxv2w@a&F@Y`pbCXG-j|xyZ7mR=L5@*(!Hc_7||?#zM>%fLeJC z>Lf{LhYqVtoqNk&OF9z;&=_g&rX(aK37>)Msl3a@DY&VcWob^~o7f;GLoty`)Q5#1 zw{yL^DVgvN7EJb63L_eh-YUW;z_eTnPLg&Jt6}xW#rL6E#()ukeO#N9KHG2_ zOryvE(IIuvwnibz@QTN5jbasZuJ`m+gy9$qnm%7}-Xgm3Ls2Z)u$r_8!{;qptZ!^~ z=9}BTI6;lgOKF|$`;}ZXNs#mk-{7-ztXrWu2E(EvFoH{UVTPlwBekzy?71LqJla1X zPf^SBHJm@_B>daEG7`&e?hTF17mcT5-hB`z^$zxm;>xK)9dl=j zxlN}=jqx+5`+;{+;UG5IVvd>;LbrLS6*l{3BR=7}2h;Y+Dj|)wJeu zzesM_5HS4G^AV z7XW76X5z}j9chUOX{cka%g_S2W^LpyGny7|{~`Wvf$O{qM`o)=x4ye@KSeppZoKX@ z(r5fcehjhmTHQ_qfExQ3?Qq)z zf?0O#bsSc*Y0hiA1iTdKFdL-|5q&3`$4+0MvJiwsc}z_6%=OuDKKi^v69=h>{Mh>z zZ!GC9aNBRvj4l%C@PfW)xQBy#6Ykfdh|&C8V*{`45DEoZhJcbtm=~3%o7Fk4V$Aon zXi!)T48t2*#ukpVxr{xB)r_JC^1Ao~fO@NJdF^J2X-t_ZuHY?{*%oH8J&~?#SBM+u zfq)Ir*oNHn;2AWA-E&|HCqEv181saFGWmEjoqiBxaBA#DW0)m=Mn9Zbk7xKUyV+d% zqMoQv+qh}gT?|=hqk63{{~~9g^kz@BWlyUVH@+p}?loc5RI$znOMN?;VhamigVZsS z$)vTGJX8rXL6896`;_zk#x)#e@nQbM1p8dL8YHy4$W+|Swnv@89Jg;fBojHb?VXZ< zPBoy*h`WNHdIw>;P|sYhAMg*n%lpl_CP^xx?Jh%|NOdaTWF_2ufH$Jy?FY88MZn4d z=4pt|oSys${OtF5QBmZQ>jqCERFz3W`TN;C%rKd3#9Oi!PUss+c?cL&;bG6X!*f?^PeHz)o3DoGH zh}o@qOh~?{Q-c>V@Jx4%EQ<$LdzztezV;c8I2aei>k3ws{lrV_i_8E?Zp(PB5crHi ztkP-TjPB~L-n?K#`?%7++89#Jv$BN<$V?% ziS0%1q!%tvst-aGm2NsafRDsqMU49CLmxs`%W`(ZZu5`14!~S`TN=oLpky@J#ogZR@|KD=NZZu|2=zKl!Whqq$eyqqPT#%F~y6KFb8& z`Y$|YVt$r%eHUHRI`7-Zp8J`GgxkD8lfQGm^-cLPn0;u@BgyvWE3HDZ-(=oejU}RT z`g@b>c$5_)@W!WRqUN5ZDL?{E>wilMDr|mw0oJ&;iR0lg2{hzy*lcO5`3r~nHc{Sq&4VO2Bf#6 znK~0S)%9EwX}Qf;={lDKKJhcmLhf7Y25*0ZS_S5+a1fH-2Sp_o3`2(D#ZYkGXTwHXpQFEV!g+TK-W+EuiD5KzXaA59Bv0UTy!{_5K)2Q!`iSyFT_@^{xK=B3gI#u)++J?8{{Zab7U({Ph>gDVE(1q&x=Eh2xhmoi zx$(43M4yLIUiW16*T!H-OsSghgr?i!!vcs<)12;jcPC?+TRN(|)=7{Qr|$DA_QgZJj`S*(EWk2Uex z2~~Vx@UP{<&rUwiu;$@L(hnzHCG>Q<-R;Mx9XO8wN-t&zWA$EzGG0ZQ5K@;LgsEj@ z6hM=8RPQB7*GUhneoz0rr0W#++bY3SEY?SuoZ3tq41Y0<9j*KGBR$s0JH9pknM5Cv zY~7%M9u%o}GWuB)Z~JDbYJsD5H3Zd}7{2@3v0ru+$9^~0t%G5?&3~HO+_m1!xIdsB z#@*qFSv69%O);VYns?+r!aWpjzz$%WnUWtr+0j8{_L7Y<6A@tRGO>Q1ZBgq}OgLa< z748oV!$M@EE4eig_dX4fVEbn;n@Q4EtDld*SL?Ibv0L34FU{2slil75YLNRFYy^@P zA`tFPkE}Oh3DkSLjfLl&M{qPIKK{G`1sHGD;W}@~(%~$>g?=bfwij^dNM!xsqGL1P zjK&Go!EG<~{gPcr{0+PXtNRrq;joO+XVWmfp}7fTbBT)bj1fALqQy0i*`-y=R_CM7+&a-%h(~Ra`lCEMn)#eFgn*2+N2?Y+xBa_qHY+qTU^WTopK*dy%oJ#<$s`Ri;IV8%jlBZrSP#rm>tHf7sGI3-_Sr3bkZQxr*eN1 z_rei>Ah6jbC3k9-);_PTkTrY_G$rJlu9y-&#FGen-bzR0hePC6QkMi9#w9~KzZj9c z--|iHJ_d+{EaEIneOhR!BJib5)jF?Dye{%Nns3HpZup+989D`D7g*r(Qql#rH+xABH4UlU4Z5nKq9|5?Q z1ggNKEe2+TiZYH6E>Oj+sye*;zf@r#B;WO!9H?oStonP20!9wVGAjYpfFdv9$nyz9 zS|IoxIT6@D^tiUYrEjDI5j@O=(+Z$9ABG=W?a`uX8NB143 zRosN0GvA#k;y3A%H=o;pxfiv;em*I4&(vGj(|;QD|J~4lw>P9j(nSb%JBp|FGmxvB zn$tORtP(;Ys_>TsB8@5P(nbw9q_FjoOD&vcDesRlmCTg{hw>#yo|2retcuc`QN>N(C}0E`M*@ugS!#dgC8gUrJ6}~SP0kG zcP`;9iVhW}BCKG(gl;Du$cRvVTkl$I`^~5MxZ2BfD|fD-UISQGFgxP)ddb1SV#pGa zooyEB_=A##kUgAI|3(|<$ih0vO51Ea3cI=ND^EwKPYK4`Cx09HVE~W#0}fO@a`G+` zO35(NfCt#7J+h2=CCm)G4g8+AA*=4)t)Z*;BvF`WOcRg?Zu1eT*%9^|*aQku=?p5@ z?RdTUmx_{0p7iN_N$rOs(0km>|IS~Z2T)yk%_(HS>NW&dS~I<2v}!Zgy^3xi^MAh| zcS|sv`@;$X5=sqhtZAs@7rqc zkFY63E*0A53^k0-yI7Rn1aQ|#Cu_db!A#BK&D%ac0DXWSvbNdxW39#Y&Pb0Q65?fd z3Bui3z(ykrd6#DFIAGd~D#S@+FVV%p^mSCb0P=3y>Dn|M5h%$X{L1FR*L0Jl`dVJ^ z4i|H1CbO_S`xTpp+xqX`Y!!oddQW$E%QTzf_-J=a#&&q--oJ$E8(r8_GD3LQ?1D8!zi^1?h(_MGIRSFA;H!^=ATrS z>9R{BtC3aizRe?bMmmh-)n&U3N@7purwHbJ*)uhs&-cR$wu55A8n;P;mTd&DZZ?(6 z5NF|EeoBh3-o1$A$4W2iKZz6Nzr-Cn7!+SC90lZ(STxCP&L-UQw>RV+-`sBM<|LGf zrk#_GBLnNuGQEa26YBL!l z4ifNRHv;Kv8c)p<5Pf%vll+mS@HaQ-c!dA%J%G;4w|T*O`;}`l_@eP>N^&O_x&KZ4 zavnkmjSXj;tzb*ZPN>OXB?i*}otE%HeRLb;Zs;_>kgjPJp4WX2u%>hH$BM7aR^Kw^ z+9s=gyfO)wRq0ZuYqOH~`{=kaNVO0-&g!TRi2~}$nJaGD+MsKro>p~0JLlO|f`;3l zekYiTlaMke1j5ESh`=#oVLk~b=m(cJwd&s=C)&k1ybCs0vLbtwy2wJLjPeBZLF@r{ z2~UGzRIqw!!KJF*hLJi+rm=>dn_eArRmcnp^slGmr9guRtUA~isMnf(3_jJGR!+lt zv)c2vDpDzd{^;#`Y##hoR1AC zVl9YdJ@VuRzdn-AnByGe&XCVf^4x^_7)$+1PGkXK=^B%-kK6H|Js@H7*iSq2ZZtxr z}apRi?T-doo#rUW3cB~k^pNu!KdB60I=P`-Cg zxpOfnF6)cj>&>Eo5qpJED;;r*!(C+a%0bG z$jea+&zrd^EoiGI8!)>KGrk}t{o(hbb0A!b0FS#cTKW#CGV)>XetOhJby9zKPP)Q> z_C-s#ewOk_BsD;a^hZhN(u4|B;aT9$8xli-?I`?a@JTtLCq7X$T!V(&J<~a+=@J&?cqM&Vmo!@mOG*81MV*$Y5f4m-~pFiG3^=m%X-Dq_r(@K}Gh=2ol zo$9j*c+A%6BNK!`ff2HLNS-y=}Kl8VE%025=K$EFi;4b7*@eKuZ98 z-_t2b-}+ugPMXCl@Xd$lHyahLB5zsWLi42jsSY99ef*tNV%Ut6gvdrybEHy;ar7pQ z)j)YH4*5WZ1pe&{*(Tpa#A%bUeft?-;SPqzij~|h?PXHaAp?!vCxvt&R>bFdL?aSu zw>k|n1;zP3-2l3Lw2*V1ZMGA%h9;tYs$u-x*<{%CyW9s~t7+XltVHY_WTc z+|h=2S!wRGV@~1S_KjIa-Dg0cBf75t6(da4X~P;INdRz!A01<6j1=`rR92~X!c^_Y z)aMVh;GUm^g=@K+2SK9vg;k?5=KFnTb<3q<%n!Y49DhuFC@=*Y;c1ys!`w`;-%7{4x z+TB<2sLAVUi}%ajwg^wf{*DkRFIVyH+)95>f%E_gVAr2qDU=-p#{{d#j~IyUMPfiv zD8op*6FYDIa$m#WH}#yQ>I5p$^^~m5;H-W25pJDYkio~n?B2^IB<4G7^T#_NEuyc( z%FrENed|oCWa41eAe>NgZOb+g|0+?D)ioN}u^8HlWIG+!%-vf~%vWG%)h-{!gR{CY z|Lj>VHxh5)7AOy~P6t-jC9GnHy9#y7cRUK%g^?-JH!T^=Nq^V6HXObxJJRQi;FMo` z^Z8U?k?GZr0Z>N+s%$qNbfa#Ufv@c1$-954WOnm**I^zjw<-9d45^Qkl{hUoM|r}? z)!cW)2P`lBU|CL+KTx zi0M`c?eXCfoa(HN$Bu!w^BG@v7FYouejENzy7$HG6#=a3qg0%Cn`3ZgxU8Kzv20J9 zT;06%W+oZTU}KT@MOp0$IW=RM0Pm9CO1->Fv2sG4W%CwemkB-C8;@@@JD>+U$CH`iUxQV*=5+5XXG)~I}i_ht40LA69l<(YvUhkEU__^QuzihBG`ilpA zuWqxxRsNT%^!YDY#0apH@e6f6+c#PcD`&d@lgjd+GWVY{`M(-)nc6TGOdsfeB2P8= z|C>4&S!a>dIyf6tmr<>^yx8LaA@Tm3bm?C6c%9VpCHGc!NYqF|MTzMN-$Jws{sP#f zf}LA;q8Z!+j6C7B-=$0QyRkx2n3CWNahAN#`0?;MN#px(A8se>u34EQyr(g>=b|a1 zQI(3xry7l)k(-^j_I{@z6ZW2`4lDG)c;&I^i>Osj~^XL-5( zFdMd6v|D1fflAzT)|k%;BbnBa=#^+J<-JFAQ^qe})&=%HV4gLQs9taR`bH=~++$(g zYfuSl#t58ZsT#sd?52JSjX43-II$Eg^r-#Jj(gWCHbv;l8n)tjf$HNhea?KR4~k(x zG{22Q2=4E;PvtPvS|$*_mURE~RfeQUzW)6mG*H?<;LAPxVu;_b;>oRSiVbHm)u=7z z3ekT>*p4lAX_w$HA%giB$+uZ`%PwfDJp?5UF>WMFYzY}vCiL`S)JR>*Bp1wi)>M!S(s2S=z; zxFFh``bLM6j~QF}vWr~HtflKP=d!x0sHmj5u+J6|^ zQj~p#=MsI0PdC+K-Xj)qZP9YV)H@V2(TC;o&}E7Fm%f{!UlP9>T@ip}{Pge>f%M~i z!`f26;w63|BoDhlnr`+pGk=8q-L7V75q%;?Mw1?Tq2siUZ9tDWNVKgq&yY?EJUi>P z&pf(>%Qf~xn2C;J>rWDCjks}Ly_#8UcMpIrOp3HxGqIQ_$$}f9^?udmwgKjtNE21? z6(wAILttyNV08ESOabR=w=(GhFoPD!f$B!z`fEH4>%h@m=vF{`QX1 z0`7=~pQ3T5-8FdKg5DH(Zd}uW;{O#aJvgB~CB3u}f2L8YJDYO0wSDPAS6$Hk+&TO0E zhPAFne;R&gV=va!s%1;Bmq-(vh}na0SJWo=!DDAlD>Tdyjfw)ybt`cSV)C0Wv-Zrq z)$Y>UFL|@~q++Kus@0%fP(@ju7vc8IDN}2?5GN8Tv``jPlR=HoeQ-c{=$Tk;o;ku~ zNW86Cw2v8_@GdnK7Tl7ReM0-y^Ki&OIFrj0Ka1Rd(!^?)vs)Yi9y%dkrR62as7C5+!x7Xs}HvOGx*%+u23vgyc(k|wLAe5TC8ek#MY-?|z#mDPJh{(+WQGThbn#_# ze|Je@s>NiEo0DW1|2``7dzn?QSEo;&2BXw5^45s^^eI~js1?1{=Zmz%z8UMvl^JBpSMz2o9vn$N161#JliJ?kb_WK_K#MZSz}D-kUMEZQHLNOmS=#X(Tb{ z4Oq#0$@5P)UD(c4X;H2WZh|nX=q~tD;qR5IuVjD_bj9 zQ!kyWFbc!Vo7wW65ue9BDH^^!T)nxF~XhS%;+E z-|azhA4}qcz+t`bVN8BdG+|{w7GUK(w_LV(ZT2~o(6qmH5$vJ`(HznDdhf%dokq|| z{s{YJ%ms#@do8t63|qFsVxm_zu9jBuwB8MPfFM*yqf~ZB)JsS7orkJ!UP+rMrj_{( zl%&Z8LNmtuE(q`p6=Nz+*{cK^(UvY@5}m9L|4?j`a^w&LDaaQ|zanWYR@j2^13=CUIJGlxY*f#F9`Rklq~ZJN0;hpIO|NgEjEMv{2**H)W8ulDW>vH zSk~yYtm*VD5+aje=gcdYDu#*SZ`~jmEWe_d2HT-IdATE$nQz9pi{Qn4pLDR;O;LhA z`xzAiTa|yj@-1&N*Rb6KGf~c*!3}d;#hVJYC1kv3_!09#^uQm^h#>o;!Y0pN2Zbnq z-W4AAy%BfH_8;Jj>lNc7v6Qz5cDk|@$v#&Yi4M7EX0-mr?uD_LVsl*gdv7NyII38y>DoDy+#ivNvr?h}7L1%-w2F&5kW}s{-Wd)A zL~C`p6nmAm`~%qI&!BFrxy|+~OY%zb|7pa8aRrzctUbeJiTJ&*HjO{0wCt6X(wp2c>9QaX z5+5`SY36$SV;gn-dz)aRj}O>bO#?-!XP{{utAVbPDDE~HA8vlOwig}AfK!6;CeaJL z!&%JDB9GR3yEq?Gtc-;+lHZ9Ekd~(#ufGMECEaN!uT;$;-GHPQ$#q2wNp zp%~G|+CLiJei*XVmK&E{B|&Oe=~?3`K6!2QKZ1ee0ZXuwmDdCR`X6O}4lksdg-#>;=;Z-tXE>c; z!YZ2~Ko1prRHJ{YK0`AL;_xXWB7xqvc;0JNGFOj<+7{O&bTL>S7&g3lSiR0>8D=MW zIum)qr-V>=zispL*@qbFk_u;7LT{>YeRgzo#T&Z@QVXoU7iAudV(+~(-cYDhUO69f zTkAG&b_^G|BizI~TaEJ2Xplo2o;%2J34x@@t}8mHtrV};{nfC_a$ZL1_O7PHn9dP#{p*FJCSdnjMog`=uc+SizMvuQl{>}X&R zqleyWE=X>6-Vo2l`S`c!-=@feds;VnNY*+dAV+D|#)zE>vWVDh8M$lH3ji^yogW=W z-f6X=(MW!XPnsd*Z|K5j+XW|B_+HrI;uRyI5HU*t_T=k7fPrjCdeCyj=`H`Mh;LT< zK&lG^p#%JH{BI5ZFEIezI^nEqN$qO3>5r^mWY~zaAonpxwXfP#XCp3eHh5%yfA#}~ z`M~`RXFccnwlVV2yA}LB6Cicv8U;E36J&GE*G!iy@-QKzMnfpKshRJdyA8wgR0x(1 zcrtZ6V_=TGt{H{JS~=(boo@`i?gRfI4N{*o2e;(U2dy-qmG>n5bQ0Yi6ELA0b-Ooe z{_Gg+um;)-YVh^kI^j;K6ENJ=85YrA`IY4p)Ds8|w(ezfpriBHjytdW=)uDXl3nue zEcqZ}Fgb&;$((R6YaMex2e>(ziM^{mUc#|bFY!m#u3am~m~+@log|-vwl0Kj0>ECv z^}cn}WU^Fv{Kcq*>7lEm`-VW#r&Ci>NlMN7UcCuCu#GJ4d$ZsaTRW+yzTgp1_teS# zQ?_(m@ltdXQ;BmR5Er}1OyYI)5c&p=Z_h4oWevd6DU5<)TsXImarugvcuS)*u*a4 zs4;gk@G|5qikleMfw>dNrk}0-RNVP8^Ni$9VQ5?eL(XmLOgPmD*!^H1^|rvm4zXmf zQFZ5%m^b{PQX+15LptJDkM$(P%dB2#1ceN@QCecJRgJm4tW836=W5Sb!LRugXIv(q5((x)X(Og=v?EviJ+^Q2Gi(-M246OtOZpO!G0 zPTY3+%9?mFyrwx71BAGT_|DW!ASXjETN?K5!pYkyrzRoJhD#w)uvxsA=Dz=P^}Q zGkQJh*SvM3?1)*)9vG1x#s;rM6~iX$^^%ut_Z94=Tgr50Vze3bKe#Q)%O&n(Um2n$ za*lJF(IK{CNiJS?fiw?tNhcIj9!Kb)Dx0Z)x=#bEQS31PK`F-NC-K#*-6AnxHAc^k z>XDis1^yIzVVWJJ8-#6dV0BYZVU9-1jYu`zxz}^+t3E~t>1eAW(a)CGE#4Xx>Oso2XWG94&9bT}(OmL)zync9-NPk)wI=yQk>r{42u(l*d+% zzasr$tvtn^oBkg}n&~WRRVytCyTPoh5cr~*zpg-!x8p~C+9jBZwLyAASF9Q^5M6@x zgPjH`;7pZ*tE5E_C|h-!bla4eCljSF6YLd|Xcz&%Ykh20e+#SDCO6`Jdvtz>`k8tr zf|n3-$iMIAeaK-@L>Q`c>6relMRM*uGMFv#<{=#xcMeyKGwa=rX|H_z%`w^jR^!+W zUGudjKe}KzA)ah+K<{r}O{5*YbWK$&_R@Xrt&uby70UGJ3vhJ7VeX>wMmsm}DmDE} zT2X(6uiBZ2NuI)seoh8C3IHDnXHVhxL{^7r+8#z9m za?1QnIiF%rBHyV1%YpKY&!l2>dXDlx0I3jYNXh-vhIn3xC%S;ha6HvCY|I6V47A}1q zw^S`N|E)mupd_#so2IaF8a$7O<`*Rc0Bp| zdkc`K{?;2g%Z=8zGYtciRpW>ru>i~`*O#N}5Ka9OXNqu90BK^aCE@u4=x3Vq*+~7+ z&z%a_;bQc&YV&4y>@`pn-OG|Xp`v6H3j5a9KfnkejRF^plH)=_Z(Qg~+PTU=;p_!a zUaYx#6ssgqO%dKvbXgwMT!lY7wgc&R8W2-ghaieuiCVvs8;F*FNs-5sc`U*K*6ET3 zQi!|_a^?$HBat@){{Rj~RWfr1zaZsPXQtorD4$KUF-y^VS6%nnzr~K2pyA4t3;iQ0 zVMQ(OL41;ZqWw|8c%hLBy`XJn&pKzcX=PSz@Epq!4t!9U;H!pLRG1%F!J81ORg_Fs zi*@OVR&%Yb;dVl;uz=^*ca~qny@z$9HWDpc;~+aldt_>#pLm|b1X{N47e(9w%W$H8 zrQq%Cu?=-;5iLsGyjzAyTsNLr=W>Q$Ypj4IRF<%+5!HPFYIu9=xC`SCS_cqjiVABJ zg>Tb1cp)x_GO4zw9tt+aJ!+8{Qd^re>n0hCF)V{a7*bo#8Csxjg^P+vsUC;#x8*?N z9KnMff*2?AN|9GE#4tHqdJYOQ?7j?g@OI03=-wnc!STY+G2>=Pf{tixoKNAgF*f{y4D9Ya9~tA;xWB`xo1PFaJRS3 z%(U|FrC0N&baP3OcU*tTe1d=^$D_f8uZ+H6I`Rd++XwQABtq5Vn-y%do_jCZ>&anc z*;Z#NhkUF0%D{=ekAf6k%WjYLH@FZ7@PxbkmGm{!*?Mcm5F2DPk`{1B`w-OFxw}5Cto(v4l|9AJ@FQ;U6 zQEoD&9D?(_UB8a##B$u#4=$-mga@3G`j&=eCI)u_m`!xK-D@GQj3AetPe6uTtmDL^ zWTN)xc}%RHEU9-jFPrEfb}WC+yxlJUVhPdxqmjUoy^kNi1OM(}@!;;t)Zc(By(Oeq zJN_B2$<}jH9nLM77`U7T!6!CyZlzApYr#KQ*u8mqOrUtNt+96X_`-D&kcmNa>J71B z$eZJeUO=r@6a95|3vdj+l@lpW6`0uJE?=gWys}JKpYClCa^%Rj)WjuH`MmxN=L%+6 z-}KTRZIRG-Lo{ieSW35~r0G&Tss)6ICL$Dva!{vqk12ZHIiVNdO*YOy)AAO?8gRSU z{Y1=WBu;ujpmMckTDmXsK$QjTK$4`q(8~>~0b5`WIdz&|HgCKUX3cIEggv?0Ah*t< zjNrPr6)GNorVr9@dZQPQ6<~neIvo>c`Tk&DTc_hXB|G2x8LuNF$rtsPjwz&}xw8Zh zFX5!t@|i97xkwAa(gWU;DY6e6OK!lCuKOiGwMssEyi3l6gWYOHSYl_`dqdX2Cj$SM<)>Ct6>R&w_#vWta^0EQI zUp#Z+Zg1v-Q8%QOS;mVyd`<(k&*3>2^E-fx4^H^hi#YG zE0nmrrYVhlJuNaP0v)$QG;aNX)XLWy>>2(8V8XthFflqKiHt6s<&#Uenq_?+=!WzE zN?-pgef?i!;9lR7#iizgW$-zj5DTS)O22 z`X*+kl`1=~3c}J~cyQMs6X+8bI?CQA^wcp@;ISkNJfb5W^P&MLe;gvAUr z}X&9It0Shgb~;QcVTTPxG3mNBQ}P`+L9d3A?5; zQPHo9=gWXDz|@2)t=SS~K7Z@D=gQqyb^g3wrFud=rlnH%$#w9>=0QiP-DThI3n)7! zZJTWRpYsg$zF3b<#6x<3qeY9?_Py@_%!Vdnb+0DaVSEQ}Z7J(5fIw87bMu_yW_6|) z(teb9T*=~XcNEd$eZb=fJ)rPgcrw?75M4zP77BGna!UL8(o^TfZ!~(n%CNQ=u%YOMmo=f1IK!-AFkLju19@N`gw#|a?205819nI3?Pea_Fi4y^)nLZAFbd5_HC+kU+650le4SN-$$ z0zbA2I|XVJq+H`t1AgqQ90=W4@5kTFm7EVYP%j(iRsUXo&W3av=+7xHtS(GX%#T;x zxJN?kBxAYK0$4GJeM8s$pbL=n2sDTg(0wHZ1fGo%JG#4Hk`*M4ydVPuq4IN}y3yotD=n`P@UQ!lOvcK(hr zceT3U5XVD*+KV9=^`IS%b9~Lr#V%e3h4%FasLUAjEkW1cB+fmAl8Bg3;&yS%hJ8G( z&(qxAw8_928C>X~0&{Zy1K-vjFhye281m~yi4xKn8RB;LwrL| zqMv2i{wG%a;*99SHB1w${266>fh$8+@!O|Lys8xSt?NdNwLxBo$yE;IAH#%wx>IWi z@dPa`>k~_U56K(yoX)t$zQ#U++e=$%dYI>i4S?-;@&71ZQ??0({etkS3U-VEUSUec zw^EfnTYQ-Fn}$edDtO*2==MNyE?+2c@eglO^plb}CDLgLQr4W_O)6h5-v=5Fxv_k@ zYIn7@ii_GoQ&s5IRR?BjR|~AD0-fAibN?``Ms!TSr=@yUK@I#&xN(GM zDD4O93LXhrP}9R<$FiUlcyCEEEZFMy*-dDvoK1er)P?|Y2yCZDv6Y0NLBpy-VS?id z%G$qOkrD7mVI{vLGaepWT*pPNIE)dkWPw7Kq3&RKhVR+>WQWa*ar>ws`ifebzm#~2 zx0a&x-sXk2wUz4mMV2Eo+nc$u9T39=>Bm_Y9&*F>Kg#)n+K@ITl8UI?THS7Vn4kWQ z25*}vSnxT~fiB$ESodiP=H*J0`#=vk(={cNB8?UFZnEZG6Wr0JxPM^b`QXOP=Au+1Q)hJ#;)OO5Zco=%S&KRKXXg=`wtV2YV zJ@||Cdpwwj6cAJlsay-3rRTl$p9?FwwrwKUG`;OmGq-0~=1$=~hqFNvdaG56uD`Lz zqBSQcqZK>G2l+%_TPEcn)Ve+X8W{i^;xP_~5_5E_Xzq;Eu3DtcFeeK;2`;;HiWyW! zIWD?(9+)Y+lNhS(5Papa?scG*WhyoQocadwHvSC7`t~ESW{EIcRo>m|F5Dss^|VCX zu8Ma&H3bx>9xmjb^rtKP>G_PGz=`hWfxIljQ+=!Tl_>tVuygG^$Dz${!A4)ATKa+5 z@is$WX%)M~3X3Fppu1zIbRAg8G?)dK5iIF!qnyT2Us^G`9=p*`oecHvzn89^%WS0o zu2nKcd?Po*3($-5|H$&80J%YeEoh|RL&{GzUf=^v^5qq4+7x@F3RKrk3$zg>m34hI zQx4c_WwX=#L0?EOd{=#Yyu@Y=I+CreVsMJ}YtZ!aI9>xko1swV*Xb{~G*NRa}`rNq-Yq{q@y48pU5R zd;chRjLo~bS`(|t`AFy&bLeGI-PS^eG&%dq4ew4*rO0%+HP6 zMmJS^W~H$}PK7;Id`@OJWeeJ#4KbKNPGp>FF7Kk?I|HQi2zC$Be0d!J1u3@?2{z#_$j{Eydquv{exhr)LPJN=& ztFqwlPG0nkUfC@n;3sXdSaYYMwJXK|%#^iiZHw}D`=-q6AzaaNj`Ul4a8)m=l-Bed z%+ey7Y~WUlK~KkaHG}=BQ^)p~ecx%5B=btAb@{y_Ikb>pde%$1y_=oMVYXO|#Tod! z99j*E#WMEeTiPbP-f3~JRXDv2H{qWu|7js=u5Ti9B+umX!K{peuBVWgS`F`Am7`nN zb&!8%2({jIsz~U$M0%jsD*)9?)xZ~=^~N!Ku?Rx_=}*;p?hBD+9VrJ62O@`=WR;~7 zFFysPED-!Jqf|6-M@KHeCAt+2)&@maKcW7eFez`VyX;Jup12XeDW>rbLMlc1Xhy4U z+CCv)mZJ@YbYDap+Y8hZi~F;IeTX}HNvw};=6U%8>`bi&&+;=cxYr_)Y2j2ew?ntN zIrjMp!b4S8@cu-Hho|dA0N_{s3|>Z1tv!Az;Uxq)@}rK8Jcfq)ON_!dAFs(?5N*D? zMIeb{!FD9;f#(S9S<#^hOsgMQ{E2N{Yc+)0mfG`x60+zybJM-}(%`(3`Ac9KE2F7< zY;)KaJehF&po3tm(sRcZ$hj>IPpJujKUc0n#>FGun@e9JzJWRpv94-BLypHe=P~3+ zL(q#1YH8^bt;+WrzbnR7ti2T#)I~cNE_Mj-HLzI%?EZ)_EJuT*O5<^;_{P^mpIkNpFkCkIKW71(XN&avAZw>zc$3R%{%O4+t zmo*IR2E6|Pehb-WO|?AHp3xMlxb*s$*MCYR4a9Gm76IuU-_WJ|e6cDZC)KVQf+FX7 z{k``hlWpBhR^EeLY$7ueg;)*S&{U%<`c0sd!#%ow5h5 zW9OP&Nh8Rz-P}yqBcu{y3;A*}2<1Km<36bLJeMID;K(6Q6mP)h4HwcS`eHMO0nohi zQpp#B8SgsA-&IGJK+?k%yOFRO6^6~nS2v~S7+u$*2}q9T>W5@pQ=o&ylWV{Y)l6%6 z{d=*Iemq!_`xmGSRFNR4(-iRL-~v@*7jEM>uaJR~ZBJ2~Mw-1g+9B`P zHoPsOp^igPonP>q-r*zk7yGwU=mAnBB!j{8zl_2kR=SgC+X|%kY7+3wL73r`4aJ~B z=H}hUuHrDtgE_C>8K*wdwex1YThA6H{cJ+*tvIoLm^W@K{9l$YBq>n~AcMo_qUh=G zFMQl8F;`EbBDP%)b=OH&3n0|ZGRpw#drrL(rd-HX z@R_x%-IoCKsKTg~Y`_Cf=+o;X&*X!vib?YH@od=(_oml2uDvcZ80Fx_HZXMp%iY;u z2!0~nQLOJXo;1x7PTw}u+Ln0~lxj@3=`PBFRIyfMYOmV+778-0YWCXXb>vL8>_o$(L)z@%aavtLU+e+!79ps6h`D%T-%q4JYs(sV<(XZ;iX%iQ0%IqyNmAiV=GUW~4 zu(}Ua;*F9}V9R#5RJ2#JW7Bp4Jn5!}lbNK35k=K9(pWLNQC0?beV*c#9Sz{ra}(3n zkd7LKR3O{WaN4{FwXFiBTTVcEcON^&R^0=U=(?5qeQ91Ph){;5i?$BMGJlyx z*jlsQ7E>gei7XXe3plL?K5x3zY!=3wTp=T+*|U*pIzeX@fKMb5P(5;}co%BUW5feHRD7(huuDDq;JoZ> zNJ22-!g)weyxY7W?&TJCX-Xk9%tTo5?iPJ-}``BF0N`mVNH7O?_;xCFrz8UW2=&+ zBHyu7_$ynhn(7n^ci^Cp!DT^aX>TeWQ&T z1mI#Q4+th28duULrNzkaui`pBdB6>_czUIj6-)AMQ&|%{`<^v%_ToDE=odaRS8)%ApLLBu`?y-s2;*JUfitPTlJ7Q?JJW;v={PB|AW2^t^FJIaZHn9!(T#5#?(A#d+SZ)`YU3M4|iVGns3&qYfW4 zz%f&(Vll}}`fGV7!mb}-@bgEttUq}Z?jYlw1I7L*6OY2Q%0%DovAA5Yfu*U7gZW2< ze^higdPJkFS3dL$h*lS!gOZq@yi#CX5+x<-5~1xvDwDjV?*-aTJSx&W)WJRy{JEJJ zE`7&OzuWz++v#X8H*&W9SHTvEmW1ZUxsWR1q#&D!F1L-chLkHP-+1e$sI^npAIcyOysTVEM_InB>apy; zQE@BPahl22`jyOnhC<-^*@2lx@kG1RsgC<2;z09<92J;TE_Zet_RSapXzO0p=kxZ7 z3n?Lvkt8F6xS8wLY&dia0Jh1!`WC`!zO;agmhmQO&0}imiegLhWnM$f(QqID=mf^Z=)Zd^1E#yYMBOL6(e#^;YqTgr<@jc^Ti+WpEp2&69rh%hIOv@uOq=`PH3a~xzv1WOy zxh1*l9mGeLO7VHARh{L?q7=hMa%8MrzVd9Z$Y$OqA%i!i-!Ng^lRLoOBR1qi_axRoPi#0!Tf@-Wjgxa#3t) zl@yTj+*5OSGG0AlY1@!X16EH%LpaA9A5l;rHxs?n-2}7`IwkvOSq;2WP3!NH*Z;RN2H6f1XJ+?F5_g$Oa%jv!#j-$?`)RG zB^P6a?ae$lg>^1S%lWKEnLMz}${t00>_aMUflwmw8DXg&Tbhsl-b{IMqAY7TdRsALh15c%SbUhH^A=Cu+2$^Q7umU}QOz07U6-@sZQ zOf4hf9BeVf8ig3H>uark{mP1YhvTRw=~=lB&*e!@-WM4@q$^1iOS0z@K4CCwX2}~B zVxtI5O2V<3^~Vcbs@C(0_pjOX*Eq@MxsRB6&#EUFAd7b-{V!jmLe&-HFt5zrTH-mK zo*q;Fxc-$?6cVd)B3clvQ4g#`<8F+nBsa8)mb;xx8a#^)La+adE?Bh>tao` zW!FToo$O`1@2R^71jqa54O7@Q+QD`g9Qa2OwsVYfFAb`?Kd@#~gI29&Ym_4jpmF9O zt6U|`rN|DAOrw$nq1bd^KilkKJs|WpRTxt!WA2ivp+j+9@VYejteWB|5W5Sxez@Wo z$czIHHp3q_5507GeWafi{-^0FL&uw`iE2gtLCq?|~fWI&vp! zG{(J-@WobomJ`;l6xV^}&#xi0xdXh<5y~oCMbLj{@Jvk<8fME*vFL>2&)lf~6-Ti( z@jc={s=n}d(D2sk+EXuuJl^aINhYFD>Y?3APWjVu6a3$qE*+~AT3}f7%%6BJFz4F? znS!lt-d+Jx^2LnHf(ksVAfoBZZwwh*yukNrr~8;;HCZl89KZSlvSslPaM}Am zprUhi{Vi$ol%kOPclmOHW;hp;n7LTpIH}C?c6$7_Z@4Wys9tm-@T(=|r1CFVyYJ-I zGeJu1k2Jc-$`#|0j#fp6`{}K%K57ZB(FN242#*X{XgeJ%!O(@5?fkuWK-~NXSRG^d zafw|;%0B7?LU+H^^6XaDyxpsu*cJ3_autEVzo~>z8Tp(s)Hx&+{R3RNlHh&!r1B~F z4qWlz?63$(2zBVTk?OE%OVs|N&kwj34m^9tppTv-2X8~G7KZN}xD4%1 zg8u;y4wCpv-nd1VByN^H5v;jIqr(3Ih}H4TJ0}>ZgW3}mL8KX zd`5N$+{|V9awWlQbNsxNovg`;_ANo`#6Q3!dyMF^4sX?ndiB>M(2IE_pylCBX#GEc z9ZcnEH3N;n7aAZ|HF--Vpnwi%)qUJ0u_n~{b=PEeFjc^jTrCYp(O zG6zC}&Z-bC;jMhe*^!%W;xzrLhsU%*+o4D-(r}i!7@2aj>Qr~$$jk<$3e#)$IQ8$$ zI%)L4psAv@M{C5`Bqr35^2lrRtsJW0A0VSM;c?jg-}o;kw+l^JaXCc?HKP_JyM&M$ z_~MB+gU$k9InZPDMkc1$qqO0Bu*^(sE3oYPJVE1Y2TH0@UBKR&PqG2+Iu8#B#9y+v zHSL#OW}hUXk8Pp0A}He+J{MGMN#GfnMqx5XaT+2u)a7>lh zrrEPMdd$O|(V)*+E0y{*yPB?|giCR%ma8DII18UPB+Rh9o<+h-_d<`A#X)+-X=6%8 ztlTvL{E1<8<3S~>wNz|3C_Ln)GHuL`BhfQvEB{8if@#iIn~7ceBhv`4v&Bh2DqCe- zaWw}TlflGd_kLXEyrFgNWl~PKV-fJlKDS-ZCfb#Z4gWL*($^SN}I2?^VJQGXI|PL6}J$6>N=R5 zG(ANHP3*F-=FslIvwwllX`upaj?-UmJMlC#D{3QKnR-D5dv@fT7;(1TyBG=OJ;ouM-^EIalUkn!nqDrZ2mR&`n9XYg5}J{QWb@7I_Uzznr0liGvF z&A_Wesd9%D%Qrsr-0&OJ#v7fJxQC6B3ryk_l0--n2MB!YfK>Po5Mu2j=LnBtg@*fH zZem>C(IS9wi?>Jt!$xO*8;kZU5ZfKu@_co*DYmynivzH(|Mv~8F zeRHbItPR`#QN3||YZ;+@e;Y-lcCIddBi_+wX!aH)+_HJ&(+~kPxc~F`kJs;lKVcC( zF<3(%M@mg1{hC&h>m~wwfBTu>PSfJgy<5gHaCMD1)0rOyCM0*+Wc29C^J2El#OUVHFT7kL!#m7hQVpu9UL*$vOB zpvcw>4us@<4J&7R#@=U(tT}y!H$mvWhsbu3x1{n5;UIj{v%asZjuX!+uNd32FnytG zM;AUbese3V_^&5(9);{CSv@n$c@Xie)e`>qcd4XqFjC1Q|hYiJyTI|S(t?rB^b8fhfB1`iGaf`tz5H16&mAV6>n?u1~05CRDr z2n4=UrTGcwT9(=)LNa5Ay*vC`9XiE{G^ z3V}eN$DHEQV!~1aA|TMMH7nfJRu5W(- zg$oUZ^FOYCfc+0#WGGzdn3x!tIDg?nL-$2B3^GhCW+7~Hd0iY!k4G%RVYn0uNrjF5 zc&sAOpFk_mS$s-1&^G(&U(o(W_J0r9>;G5C{u9{$folywh=GQBc^G5>S-`K~Tt%Vy z|9|`c7!T?t5kbLg*lE+xin`~%`d9gS5Apeg!s{G(Q!lD`$1l~f1jCQb&33Z2vpTk* z9i7^h4)swi^Lghml^Gk#de>RdeEEt}`MY^wf`3uBJ5l33Xc&Z*U{}bsMxjNVrX-UC&VX}RJ|aqXh7^*Lsh6&tgYGJ1#jya#Wx_kq0@pSe(dBo0O0 zmY&`u^O4D4zj^yU zmntHp+Y@^dvMzwY^PiQJ2`+oY8CgGC48to94Y0t5qvq3-zwmhnyY!VR^*eI-0@Hxo ztUvVMymBh#O&Les?KV8+83!yXG#-Hr)49&Vn07%U8gaK31dO#1|EE&}Yzx8yTm+20 z5Wm@qDKzUfhkA-3>H@w%GvF>pDFs%7eNneft6=NjJ&znA&umc7Jc_#c%YbC5dJRUu zki319MMzufqJHbo6?L-P1Fk!=^mmbI$uZHfdn{)s63q zwVgm;>6$LJi@Sh#8HhLtckUh+#RzClzImlL3<(e!#f33Y7(9LJb`{9J7O+&nFa=UX zW43?@t#9TSF;Z1juuTz?ijcs=);EzKtCtaxD=CpQ1y=8kPN(EXa(v6W#>GF*I4~<5 zh;x+2#+*G5Q=JmSHk9^4XePVZSq?EVXUOKOSv^83+po|cV1#K9YRDejg?74f(v7f! zXI37y7zzO}>t6i?&p93qACDl>50qW{X=<@XfLWNKTLv{er4S>*L=PDIWA5s@ZkQug zxIEujn!l6*|5;8-Ja0jVIWNZ-;RvWjyz90<*o3;4poIxp6tIvQ2~VYsaoj15&C*oW zsVrXzEeliQCV^+M?2KWl2DexlwZ#-cJHZY5gP zv*fD^c*!)yFtCW@nc1QK-NNwJz`dLrU&eJ?qtnS#_OU<&aW#=-Nys=kL zj=5|QB6K7Hex9G4r$Dtf)yHWaAD!!4mgd}5E#qHT5ViG5JF$3a#4W*aq@Bz%X$mkI zxQ4iBD7LuJBz3ihK(zp+!G#wav}p-IvuQXWEN9`#6CC8razrVscL5%)TR`l@G`!9{ z)BJ5RT=;fgquaZxOkP7c4AOU{9eLa}V90r&Pxk4^!-rHT`TE^Z@vMT#KrF6ycJ$j z#f%~a93xt)>784Z(WCm#Q?_C-gTBO+$%ffE(m7jL_3&Z2t`u9%b(o&bZV`mF=1!n3 zI_>q64BWUT#CpRof)QM6?^Cfp3O~nOSGU6#U?1jb%@6%h?SH^J*Y!crucSmWhvFXn zH57rpfL&qchoxKQkl`cq^|D^8j}_?{r-?50@=kgIx^08AXPGR`b0Ndn+u8v+d&}u% zUPgVfE-&{2COLl2a%DCbjYorl_i(t2P`{4IX4x-{(I**oIBH`_ zY8Gj%(qi?F8U5+(B%V&KKO!zhYe#M9+B_gpS-aoL=TZfw#cSi#*4BpiZTV;1dHFY~ zj~AmE4n5@os}Zo7>Ko!JA}%Zv)3NyCCc>{36%Yl$2lojF+k> zdSH}r?8`EdMbNQ|3{{9$+T7rJG3IjWVLRWlQ0TSzp?SGo^mpAxaj^ot1rIIs$UVK# z0xK&dL>1vkfd=b8K-Apq*L7?y8soqPu!G7G#oSv<5!FNX<0a@0=+)#rPhYL>rz?2C zG84VzVrbqR>5OsUz^zA}y59SNaDP3CAnN*HsC=KF{{VoDn1`AB{&ddAHG2Ro-#!4o zb{x%ezu_uE0R9~tZ~SZ!*xA{&n$6nps&2S29!tx~24omD%=HsU#W2jH`=a2kiqxmC z(&RPwB5OxKHAcNm%T7UjF1JI0BKDc5--x`*_(wD;!Afqf4qQ>j_qkk<5xB0p9Nt3C zMQI<3E1E^A_*@0(2(AL`H1SdZbXE>8K49)1q$`J)5b*SNZdPlHiE*JM5;HGV7t$xU z9%WdHlQdegLEu86vQClcj~*e6cQS{AKj>v1xLZ#HDc5Q&CR+K*lkhgkg0JoiEM?z( zwVrAj-05vn-jOsWKFpIS0>2&P5Aonj7M;|_07H*VL`-ns2_7Q$BUXZP=Chjg5p$!t znjAeT5Px+NqnU zJDPa(TX4~|Zhey3)nB~Z!i(WusZE)PcC!!^F?`Q>jW7KOqUbKdd!`C(_upRX=td`t zpr65cTIFLiw+HL$Qsv*XS#xhS=cDYWpx_WuZCM!d-$`)+z`Vt(nwa-nXvDzA^R=3N zlWBfc}2RDUn3B85X`3YLZotY-siIk>l=eSeNE z&q&fWefI$9y?-0uL)m@1b$0wSZ3ShhA3OkNJ(dgRNp&FO~mk{Eib+M%N<*d{K`1=v750N{JwupO+qWF_*@buDaX#X)%am21v2Myz4j^{=7I zieXw&=JW6=knY6wTjTU|opjB;4C~E?g{Iz2f1K85it6r0If&=E!gPgFu)?S6LiRMK z_)g6Y7)&lD{AUX+nl(;!nqwtCrSS%yl2*u?+NIomHdlb@eq3K#sfEx|Nrd?A0l^Pg z-iwA23UyZCV^Intf=0?mKMXKQ)GlSCk)fU0zoPfw1QpM9r<2R;U{iJ0ej+e-Vb%4C zIiuDYS6+B$`!!=i=he^JL`lPhYpP)(bFHM&2SE2%Vdtfy3?IB7#zIY2KC?)=BvoWu zMU8-?V`u3U>~&v&o*0AUMTD+}h+D1H7dC)$Vbz2+y}JZj+-6$cVW&zNZj_*7`~gkH zVIA{2wz|+Cr?mReV>8)cnvDam9B=Sfbr)KOFY@Jkj4+3eyz*qTJCU})8ylXQbD}ET zZHQ6lPxG{b!+ZG3O}j=1VR5MQ0*#!-IvPj8yVgg4esXmf{X8OxTo#REYj1j?ERO#^ z?Q0(OPuJ{16CQa|f5~S2BbV<`c5#;vmveSR-D4TeOMYR&v}Rd1}kE!UGW-R5)O1Z#9!r zDh=Il^vSVUBn*oNVwA&M42oL zlX`TvlsYj4P4n9?om8#~VtW82864gBqK;8HwI9wXyIyG9IVWWL2z83$nJqd}d3y*J zA-F^T9hD9C@}_D??{iy`EsGLe;2aeDlN^|eA)2_yah0*zmzXEa6dPh0({zIiP;%fPL|_r!JEaCwf{h#w zWhqJy1SF_unkXe^UM&WtddWpjaX6OD*&`uFW}Drx!0d=f>XaG1l2jTY(8)~`*2{a&h0XmF8)0&z952%%4o}m& zVdNv5D45O7D?QyYUP%%;Aq|K46CQbLf*%HNItE0k*L5lR0`zNASwgiOW33LBfjnak z=TqQ}xrn#!;stn~$@IvlVFFv89fumL@fVk6nCrQzb>HzJir6%n3EBa$YRSZbC=7?k zg3_v2g<;j(>$~OO+M1;zS%pb54Hw_(@9Ye3>Uh$iP1gp67pzYQSD7f1jta+yZx}AE z3E$8LcT4(wmlF&$fO6BxpAQk8#V+7K0Knp(!CYlEDs-U2X3c~oHJ%75?{YnefCDMo z6tij0e(a1UfcuMw3iczKvXf$O0GBDD&~zy^!%fK)O#xb?ra0)NrOSbONCyYVRJKd8 zEr;(<5A}+*OGg-zWdvHeh6k+-c`m2dVx(cnTz(S7B$YsZ@t!bVe9I5F7QX^M!cWXM z@A<|75@faADfFi11cRc{t5iAR>6pT#5WQ4DS$R@NRa2XM&H5wmNxz!gs2f?@9-*D) zgFXGHsz>>F@j<-a4}ji z71<6pB_|C@7)Jz)gY`QBNo(0?n?ohw6s~u3`{WZ#Z#Go2aPltYn%TO%&S-Npr9$3` zjXV!{gzm+y+fCHxU0ym)7IkB?S>b<{Ii1=aUxc(%FCB(=60Y*tyu`axE-E8Xk>8rO>-!Ur={GHu;-9cxk^rB7El^?FqbU;=5_$PSoKXN zuWv~w<~aUE8P;h2q!kH8Skqnr%r0dd@Oz=kkLXD5k!v}zQ)7@2#Wp){L!->Lis_=8 zD4!GL1w!D%U}T)q$6y}rI`jqI4eT;ZQCF+j(N6wDbH!5}Xa~9+o;mO0<=X+Q>i5`y zsi4E%=VW7nA#Bzs;omAH91oMF`zr(g~w65&~cJMuOL!r&yNM{egS<7-2`oqa^{va;(nifeS7|_5_T0 zKai!s1qYgamy&p%*QQ!-L4%l(yjCHuZ_FN{Bzj9FTQHS=BgIRO8rLcUa=R37!B$W! znw7$xzMTyW>!ZjV65jEH?mC{IRWm7kd*O>CUas$&uS|dJe4Z5*4<2~${_%C3vp=7m z@#{uvaq!o+wd4(@dU}Ml^trl@Qk9BNR1o1p!E0e&DUIwVQ{b^7tGDfqCT(mQ2|V~& z$+g>^OST}~%)cVZU72H4o?)!2|UQ>dk+EV(y zy3#cfqP6mTy<9B0ANHEsAUYYHl}HUllMiXU+kuH82baBnMX%bU|5oIJe@jI^ z(ENbS-EdNe=%{v{=QDTS0zaFGDR*I2`(8UNp?9)};F2Rbw{EYs8Ccd&m@lNS!-tSju=u@TUFjPOmwKIB1-`iOH!Gh3LC90MYLIEuKeQy$vn zeybLihx2(v9!6?oE_ezuXoCiQW`?34AaC|B_usuw*Zkb&Gg@0~KiKqbKK;e<{jo(> z(_XyLTARvL@xe;sfiQBb@WckFys&fs7hKCL}>j^qv?44V{&Q;c5cT$(&<((E?e_Xp- z;!VC?MuUrtadeIDJ0|IH85v>Id-9?@v#1vdH5^7f>7Tecx#>iC@e(oA6D`(__4JW}xDQ*y^ z@Vt)7zFqK#JE6rBa4-IluUGOxXWjSUHBA6s#CzV|@p@zA%WUo88Mhr}3?VL@47SCtQdu;h@qCwJ^J%>U)y zmTX8A=xg_8`2i47J}7&^xG4KmSo^e6Z-|je_MBni0q|2m`?N+%e|DB*+-~+%_Kna3 zfO$$AMa>>+*~o6)9s1LHr~GRg&g(_&Lmf%1noA*HureY@LE>nw{2p8N9wdwCmT!*> z)JKszCOh1~p2Ke->d$7^f_MS^80*(Nh`DyDBr;MB%K&iEI7z061eH$@&ogJ zI-Ztx*t4|row=ScOMVgJ`pxtKFu#$%YcN0f`!p!T)#>>)73cMGuE}O<7O^vw-#F;L zHK&k;)>q?JMJ&`??`G<`)F?5bqtL!ngR;w*!9YJP0{3v1{|I=6Paa}z~ z)PB$TF8>~D>>fOx?7I|M=U3fX8eb`;T9UNZc<)34>=y94xC5Ih}S>>5@6R7(#I!PVf9LTF6kiLcU5@g>2d6?-~o->1!;Q zrFfxM>?O0mSN#3=YXvj?*Fi>fos}3+mzdE_M($IbKRDj9cLXVSPPV5?y$LtOP9cAD zTZXnwX{WSJz}i1ZyFO&*Z{h8f9Y%T2cu#v!BHyF_w~#v%!Ao$EhQS~9$FDO`LFt0c zzv$JzN3F=wg&J1-k1jaVXsQrNHgq;gLFz(g>~;Tz-2K0)k24>>A5cKk8@L*Lr(B1< zYy2;!W}(((d>67WQt<%zTmSTVNH_}p$+%4CzCk;SQN5JiA^YI}=oP&uCknX_56HO3 zlD%F6{i=w`S!i44ii?GAK)<*5;s59yIByZdV634FL^t$`b^0!rw*D_o1k;?_ z$H;!QSPc33RQsN%d9)(aAj8tfiG*uhpecuSk&?C2CAJ0*2 zjA$P0y%7BN`I?UN0RS}a;#!5Qn9lys=c*{iyTycjwOs7|heU)PKGY?0wIs-7$MOLn z@}Dcu@BDZaGBflqitWgfaEzdzRdYnb*Q!gtgEnon9_t)q>~t`{{OO05es z{$o1b-6tr;o|&i#*?QS~2eUpdB{0Y)$W|Pd`D@sJu6prD;8wr4*?R>FVIZLa?=m~j z9D}n{8yrfzbUF2twBa4!TvShdB|zt@j?6u7JVx2h1+oG4wHF-Xu*$inI^8v6&l;u|*Rj3h%BfpaHv^Y>-NF$Jb<1#Yeog=MD6+038CJs_+% z$J@?Pe%a2xze=@R*oR?C(oS);dmKM3-Ls_Zdz`z59s#J&H*3i!d#s@8Q(T@8}t^+&yYK;aEnJR8>5SIRao1E3v@GxJk)T6>=EpD2L(2r?>;N>xfe?D{U zHpG3Ce*vm4tUIoO_d`F4uXo3da;mh@Vhn^CJmWkHo(8H{YwWYHMPGZ(RX8D=TZTE3 zl^CO%w^ruJ`wHmKH(OM~lBzQ{jrJ~gO8Eg;M&u(s63S2R>w#41eJ57JJTsUMB{_S$ zWlm7M*dJd>=x5}0@J3|PSXlsQqf-D##IV3_rY6H9Jw4&^9mkM`OAoa`f081{C>zd5=-vtw?r z4=5$9scomoq-v=0<*}GgoB+kp3J3=v9g+}#)^gGlEKih_Bxy+*s3Q3}%se+B`>&4A zROk)oB$ODL_O=_R{5S_Rq5DKS*_lbT8^?qJ`#q;@VGL|lJjuHFS4adY6n30 zJn^bTsqkyqlx`dopZjOfdFffbZ4_yVrQd_Cq>_b-Rj?#Hxva&tT;nbj&OHw8eLI#? z=7l^D0S1AiVckd-7Dk&0S2y@&eJVYzJZ(~_;6RViW2Wnr546$ImozT-<2yp5iNsi* zHItpj^i<}G#Ftl}uCnce>DU)*yjgyecAmUue7sals2^CnsP4)+Evs%+s-FB7|>4)y!8G6{V2>NG^N*dxZ66AjNZS~D#ZA3h? zLo@oljDLEZfsw=bx@5H1WvPxH?FWpRy3+V3&kV60^?s!$6HR|5dkTTE=%d`>=0r-u z18Ik>aK#b$gh=k9#7Cd=XGV;TKbI4TO+%K~Lr`(=IFEe7b>C3xaE4|UsuW)yg$e`{o zjDHpj&{%R=9G;)6)8<$?LjS#+GKJ4!rS9FGG*>BgOXmx(TE_9%ukPM!6EP!=@p$%& z3R`QXYTDE1cw|@8QADS4%DGT6oYV`Mtcuj^X@s4t5z(UAn5KcQOI>c;VH{HPv!kUp zeH+bPLRvNcb-C>1&*;6StHa~1I}B9|H(K_FwbjOT2#{XftYD*NI@OBWPJY5wY zhQGX#Jut_*(j$pnFzLEh(q?q@SpUWMgN{@_-FBJ9*@5lqixq_&QTTgk=WBLMfVNYX zyl#%&I|h*tUaF*y!o<$HK`bAi^@Ed?HO-h zgmdQT6i-4qy57Zeu==X?cQ5pDVmJKoc4W&PwT@k~$?#P9*`F6{3Veh94hT> zsL_Mrs_CgVQd!-W{9c&I5sGx$^^m6Alz)o8uw}E;*dm#uc%J9%=r4X*|Fl$6M4Xw> z!nb1lRIgp_%|(T-$T$5=n79~C^&FyjDfFrUH-Tca<*MQw!G|tbkzH8cSTROl_cH~~MriNPOpzW=Yhuj zE(Lxisbe0@Hy$g7n%n9kMOh~{UN}f$JaNuLEtOTqI9tC)m8)U1IVE9bcPGc%{l#7O z)il-vnbsK0w&wBnhJ(n#=q`ghi4vbR_3mQ)4g9I1SzqmR>fSJMV zT%x5G3H&q+%1jR?_b_zUuMq?uYo@aJPXjJw!>ymN>-s zmU{z75kVnGZ&_mbRZ~%*m{j3hO!3bK%)UEne=#TvX7u|M61t)zz_ccJ6F@uht`oUH zuJ!$;^pWt|flhnYLh|gDQ*EWH#}`mi+y4DAB@O%*p0t&e3?uAGfS(6@8Km#}aq>y? znE5!TXJv&$rp2V4Q<29dIc0vAU{&(*{8!4K$ENi(5-Gj)X7sY-wt~MeR1~J#(_kP= za^}!61hj}`*+|k$1Lbx-7Z^1PNF{X81!$Uv%O_Qe;|70K z9ie?Xr){WU1#FSY5koks`#+6Fp^oiP6v;sfMvMl11%? zX`b(8(3o=Vk!{Qg$LMSwW3{?DzdQ8BtSapGn6$R}3@;GG zcOIGArR=c!`-m!>bNE^C&Oo>Lks3#@2P%%$hG?Ja7NW(=GsKxfBx8=k;n4pZo(e z8(1p%lJ*i+t7vm~2g*mk7TkkX@bj2E+Y|r%f(CLQnb!C_#)~nw#yxo>GaCTo_#FOP zniq-}Q9>d0tI`hgXXS^A?Irk88x|}Xind?&JJeM3+yZQC);YFck`6`HuD^F2#SI$h z`a#=}oG7fR^XVKG>e-UWIQz5q*R>TY+A)9h*J(cG{v0o45B3ffB0~jfB)6#QnxOHV{# zHe7Q^m8S@jcU`7uC{oXn3idDIR_96`2Mj2b9FtT)tbUgRy-SZQ;c-Je!G7T+oJ z(rK)H)Trp`FvSc?9kQ4ba7--PJjuRZwk(=v_?Sjmep}RnUQHOE z#)B`Bo)l)i?Mz4dv?%4G*5O4F1{My3SasD~tv>VA?1t|`{Idb1SeRtm#bEkv%RX$4 zM^ikk!5gQV@GPgB@Ki?zG@A9f*V)n1ha*3r2tz8a#ZcwYatv-IwlFcq?V8=^4--X4Bllhk(l;n)l}D z1+`egA%sN4+(}%fH-%fsHO~wlJ)Dk8d3>)snbzaAAK@>+K}`ipby`Q;^COS^Iej)g zjMZ?b=I!wwVT%DBv9%-@ceig>>}*v?7#E=@O9xt#`(~Y&HKur9tY<IhW7^}>I+XrE*pn!<{#nU6KTikBwd zEz(DIez#qSGKGo1I;<83>nDD3D$+HpqCA4OK3l9}h&t5-`Ib^d@L7jz53$Tw07fV> zOR--&0CtcnB^*diSg13ZuF#NloWen=Bp_C~R!JKqR54bv>rOwYuc@Xfh%_w#}LE=2Z%OSXff9^C0X{0zeIJY%?dpZL3t0$zn8S85hFl z*lCWVWof^)c>zo5*jnD_67V$ztR--lwfNestSyM;H$bVtcI6Ze5rJuDZhaPc)Z)ED$fwuPXh9 zrM6Wly1De9j{?bQC3th8nliIn83(VPS@_gFF;3=}q4Te+3w@U-->PhB;&bxLS0FSh zPFY}a==!yK;8eh;C^jQg?t{Y0hLZrFOzTLCtkMY=8kxmK$>JGf}uE@%~^&lIbX>N7tkIwdAE?BeVTa;mg=%v^gqMd(ZHO zMdH&ke0{xm66;&zc(Ep^B#Qcl_x667+JhUND>c2{+DGYBc7Yj=R=sw{b`VF|Zl^Od zh%=dx=+x)AO1z{`A2yVEqA>SpqcjDSg*vq)U8H$9x|8?KM(Ctft4}vS9yPH)fAU^a zh*D;O|NFQK2B{chS%cjoAna~5sPM;;!3ixlyX3m7%vZ+NNx!#7uel~4*_P0|OU^@H ziuaN^8Jrc{kchN{2Q++h`_MRoouBI=@$fjY?ptFx@HVQ`tCU<)w%tXRih~QO4_ZYq z<6Az6_gh}ivo$*07Ja;z=7Dq!SP&I=f&tvL?n*zbRgz+w_Zt~Mgfjl}xXBqqMwvw5 z`218ZKW)=WYKn`vJxJJ`#Zsm++|y0cWPusnY0btFf8o^?pz z@(l~J?{chSbM7Z)70Pzr^Y#2E?hjpHIWt6o?gwKkAna52!J)RD_O_J1xrxx1DDHdZ z$J{%iRJ=#f4}%w9c}O__6zVB#KA$*3rNBJb&v4ZAGuvpU`8=+n8dZ;}x8~8`eB9sj zDL_?RM$I{A-reBJDTyDM*Is4gy_nlLCTIbaT;4ek)$9tJDxZ^9Px-W+kL(Sdf!;4L z7teykTXBCB$1kxXK54x=k-u}E7Y(kOsTycSe5sz6qH%X8Y!ZuonZUq1lkCr~*l=uW z4-!tGJ(YR?@&Q0?LI61{iy8j@tKNZ%UnbYZ?+m>zv`yN1T*`Ob!JEO80b+~)Rp#3- zaP3yDry}M%$c-(}E6qEbK&YJW{&rBL-ngis{`0|uX`t*FC)AEGb~t+>M|~)oK+C??eI*j=(D& zzxAc=qrvxrh)r>F^6k@DQDFJ5rqz+^8&kQ@%rs7?Q*LI#4$&5>(_+995PP%2hnx3v z&%2&#`bxNLJB$DiezU!DuHf;jJ6U`tgr=qu<)rF~djw<45T~ga`&G6TutemfN_SB; zZScI*(6v=FHUi<4Lz{?T)tpA8UOtk}-zJ-m6zDy=L4DYQd-T4JWLx<7XYCBC`1Bv! zVf?EJ(F;@}O(^pJ>Wdfu`r++-Y~zp>i0p-F+9B-VIZdi)_%Rv{Du?!0IDoT(k(mUS zXHqj9CQeVBCgq(i=csVQQvZadnSYw4W%tmyMNw{duVx~0Z>c(Xtym)7uY!s@#~_sZ zd-p9A#*&y><(n)VZYW-Jdnk{?3RJNJuVRurQ$j`Q4RZIYD7N++vwjJQ4Y^TZm@Yg! z*LI3s1kkE+6=0Q?P@Kx4Bhnxs!n+>sb$5lz=^kA^e|m$+jIj`xlRkq$p^c=1jlpHx zGC#GmZ-J_xd@7hL#W8hH-ak$jd-;addJ*Cr#d>m4BP|0`7qi0fibQX<-Y9qWIo8>G zQDkE1lt!JipBd?)4Hb5xkG8z9OY_j~8&L~wc&o?%jC<#8n&n6>7b;xkK1D@93`!*% zQW~K!^(bp0q*@Qf((&HO z7j`rEuYwpz?FIA9v#->9dk{5PSwlzGx}$f9o95bvK7qA_n8|StN9XCiBapcxstgn^Py1BR4?gE>NXwaJGDmprE+bk+XTXp45C+&UrNYUan`4iCzAW=BPQ>=7KA*ni(< zNW$*AX^O`gv&V)-!yIP^SK(Ns{Y}=XfBYNY0ArrB>osmovXS~WU<;NBv$sR)RQ{Qf z=1nuRI=kPeGjUkdeRWqOHuRFH!e|vpuKo@xgfO7B&4PWj3{pnXLBge*QfNRh`;FHfJuZiPAWwAflY(-;sRsKVbZ&d;96CcXNvFJm9rX9xwgaEY{u(h`1c=Jk0<8hu%Yo36yrN9%fcel(ze719C9{>viO?s5Ry?}Cltu*LD@VMrg$wuGLdS_E^eyp|Qo@%)`TI<@-qbGju*$bMBI%5% zysX)Kc&9;cMZ7DyNY3inmaXldmNR@;8ivH7G3CQaXTM3n25H3u9Wz@ZmQCyf=@ZGD zj%7bNo$sIR&24q$K~#GY3W1BYta=9TNK1;mjug2LPxy8_JI>h>#Kgy!C~IH*)V^xa zwbg#7UHbBjda78+vz$TyqRwFdyDCNExTqK^KcRvr+eT;5Zi&0jX8kL|5NEfm5nNl_ zIvLlXhWBI!(Ok8mYU>Ffc4KS9urG%38xH(htIf39u^QkZ`8kh~@@}9Z82^xB}I@-l>#(SO1GAAeT4h;}Wyj^wIk6T$i`+r`u+W+^0qG z*Z>zS-Eza~5%ObV!1kSBog*2Zu5LTNng;;W1u)<3NtzGC{7DX?$^4+=j^}6fkmTm( zpd#7Xo=?49)JNfNl<;|(qc5cB%`ztyoUH(QB$h2E&!ht&-P(jnt8DbG>TuGt2)CH9$tmL(j78hKX>Pm zGl;b+;zFBJ8ETv>@E7W@upvylu5!Hwvwg(TLzJA5DC(?2N?$C@URLRfO>@+h;qH`e z1wQ$Za09ICJ^JTM(UT7C2vh|DuUW|QKPBd$ycM?! zFvOe>8K!v|DBSfpiNeE-KBftFRtOpHCvYPQnO7VO8Zv8+wS-uLdYmCz-^zo@^x0@T zV(4?OCL_|NV0c1ZUup@~%LsMqca5j}e=k4NA#11@MQyou*#vPz)TKqC&V`W2xDVkqKe#L16TL%D#WVYa1x1y$L`q}B$^V}BUfVlaV;kHiwcTk#X!a&0Y zhufX@FeMr}^upN-b$)GiJa6h-g@m+fBgcB1H>}hIYmKRM0=S2)`p*m!qAl*8XLVRAi!Vbatg&y3RpxFEJs+B zV`vSMpyy#JJ!Yuf(RSj)c(YUZ^3z-?e^)DDzTrgAfo=|orU*A#re24Yz7bV~&Z@?$ zVm-+e)yEbtuXB8wa{!j4sF~2{LzKSkO0{m)lJG8TIzFgx1v7al@h`AjCu?2T(LsNv z`f{@$CFG0OE5Ca#8EX+>Kv|aTuSxroHB7%`GDn+X@gn8Rdz?>oEN5ed8F`oA;bmDY z^6&Lp!q55r)4SX}-%1U$u;6?NSQ8O(8j;wbhlXp&8T;DhNbJ-$nkITWy`*2@>Rv3- z8v$}@X0NH-wyr*V1^I2rS)^by;&epe0BKR|?aRvI_)h?vd_+X(Pr11+PR zXt1ej9l}_;^fjS*5G8{aMH+|z*Sbof2!a5b`l~E$2*B*hBfPM`rO!5lPFd_Nh(z0| z1pHZejLb%Qo+AsW4KfW%K_47wXyBIcRHS18-*99l(6L~ldP-g1j?U3TYe7Kp=< zb!6G4CT%f3Z9jU}Dw-gX^#YXI21jPzX^}l8R_;$JXvBXQUDXq{FgJFlx=SDKe>=PE zPxY zoeH z(2vWN(#{R)pRaTzXp>H!?Wu5*jU;3#2Mi}@;x+roPZJpSHchDJMo&ml~(docegDy4f%H`D^rnJ0?RBnVU4d+#~@&m8^Y--7a5wc`FpdSEmEvkGY(3`eai*IWzMyE3S`Zy~5Dq>!$M@1y7<)z7_fZ(~=5U#U5gX7KCrH{CU zXXQFpmYYbLvx8Mv!a3YRJD0&c-j<&UufHr(n+qow+>I|Dh0D#<3PcV%QmTmS#XCKD zI-#9H2~|Rj?CUrt{(9P?De+moP0r8(qd=_sK(9$P1XYj*HyP>pkbyIEa;#S{R78plr18sbq>gbavgGqq?F8ZOm z@jpD|ZnX3G{ufX9dR~%?mD}IwGOz2-078k%et5DXqT?nizn&V?Q2?~ zq>HfIqK$%~`ib!d+VUqjUz|2*2B`6ZU(HKAQkQQ&|IyW8{hNOv@S<`Lk6xH8T=cvC zucb7QtC_p>wME7z=8t2R5t{+}o4&0);QYOZYSyJr61TVPbHeu=KGUD(|E~@#_lHHQ zTglP9>6*Q@nPBeYx!zx{5@c~rI4bky`>(z5tuH)R+Zt~l|LXijM+wP-e|30lOTK-4 z`p-+il{k^mkWdQsJ0ChN1@v_l_-y!_Qd`^DpIslU^)TVQEEKH6{OP z&h*R@?l{3+GF+w2XUB$d9Gghg_~~(oZ!L9%5~(L#$Vv#ja>wZwn!FPWP_y#8tx6%v zfvtXNQnbm)p)hga!jXitC@9*s&>+nt*D#Mpt21(u zOooulcMdE1=}vZ&VFxIX5iv}`3o-eU!zvPKG2FyN;Kll^axZ>~Y;~=1wW1Xqhq2ji zi)gZvJ>&iy?9Qe9?5+~0*=Rz*-cQ(yWKG#oiF`3u5}RGIPbj3VVqi?XTCslHaXX-l zvLeo~Sb^*eG>?e36R$wiNO0`cMYl@B^<_s7i4?1>)6&M)k}g3n3o$}*J3dE_lIxyid; z#ms4m#Xq2Jv`Z}&R#)0Hv?>!)2aR1VwCOu7{Po<7*at|3$Rxr=3n4o|TRu)i*o!2n1xyoP2X{#X-tUG|hlph@I?kls~ zP7RWVQ#D!B;FeW&SYo(1>h5G@qGU@zR*pD!baGnMDBJ)cClkAzoYE^*CFQ_HMqFJF zHC1S2Qq3YU6qOzS{t8lP1(8?Y6G7fM)H{-XmRdOCeD-uy{ah((TooIt+oi8-v#{-t za1d8d=vvU3s~MG81RtySje|cno6HE?J}#>1OhV@b9bgK^p(MjCv`eKB&Wqp84s+Qv zYb%YrNJoXzT-dQoQ(GEZ-9gjKxvSLHGC>X6J&2kZuo^oFEXx-cfjko0=cG!Ds6Mx; ztBSxxP%)*_#c4=dN|38g8^OA92nVbcUE<>A5V@UFBENhz{zpY2Ux{| zi1IgtQHqtCwp}OkTq^}nXKGNsq^K-|^}TO4&rN79wHK3Qa#a$MY^K!5yP)$g51z>H zGeJ|_m0GasRr`6qg}aop^(?nW69*dFuP?G4U$L1*RX)UQ*634P^k;t+&*UQ&!gzHAHr8@U|*8z8*^_H7{)fPIo0%BLY=Jn&P+WK4R z7hdRNrh>vT_5FCo$mUTC@HZ{TV@j#}ucM!=mmEn{1vU=sb0<922{>Aa#Vg;75L?T? zrQGrqFH+-?!*z`CGssaZB*+Vx0=!Y?Fxb=mGsgz_ig9e6I*}=qHYWoGG)h_Nb@@~h zo3t?@A!}nVPxv8ss}4z90RGM3dhO{^*&j>DmMpw~fa*U^paE?&9}Hl-TLqvf5o0N2 zWi(cKJ**N@^7YLN)4UC*;)GEN+vl?C89PH6d~%kzg^sk}Lf)}KJuA?wB-Ynwvfotd zn>MUW#ILXk<2i;8E4Z!eK(W0#A{v$O&JQQ0b`Zkpi*?~&cBxLygN{#t#^~yVk?^mQ z!%{;yetrVfZ{=;R8_(GTmo_$FUCzmoY;Bb;TPeWb6|RBQa(a!h z3-)W_0oq(^;YCN1S6#;}b5sKSQ#KP$EDe7U5mK<1(0kXqO3<$ehF8;HJQ-k1^`oHB`=k39#sdLD_GUL&*I2FI8 z>JkgicbwSF&D^9=k813Aow=+U*FHbgDZ`-NAmB?JS{n>wT9=N%B=halOgz}Zmt z;!)sQ0WXimH?`EZr@53<(y)>#eIN9ig)zv82L~qhvMDD?b?gC(mq@ueRDWK>n!vr4 zLB|Qf%WG8eOtsPJJk@L7EKNCA-L||~Ld}*$q#BDCsNLvT}VXF17fuixw|4i{XAqR(vadSk!NE+_={G z;yI*0g9E=T9XDjDyYV-=tKa(M>GdZyI%V)$w&Lq-!%)A{ZpP%7}lq`E+P%T4TC{@^4-eim8z z`na(cg*flp;bm>bWHoWRs^QFH<7qTI-2PTlP1R8|l9Fz_Z&fioHIhK>vxLRsIyc=# zkk}H%GeR%O)wb1|@whU=Se4ZM(J+Kgh3eY~Qj@v}3bKkm>Hkq^AYUa1JrfyI(jv7M zHUnj>y?L1-YO^plhhVm?y7}abB$3iT4lU4B2(Cay95Y=sbqQSuI(Et{4K+1*270Pq zE5t)6s!Vm3RA0nxYaw2X>Rc@&8jhwXgn>f`9k45URpn|3OR;=u%hmqd=yi<)=tShF z>J_@&D0U}nT=C_5>M|jR@yQE0=?F(EVitdy_Tqw9e;)8PE71A4VuJ!+zVgk`0dcMW zQGtf!BHMjNHkrCM4~Bm}o(<5V=}UWD=SQW~X;2$=5P zUdu6ws|;5zFRtQG5#O4mvGy+I=J@F;xNc~*z6Aa{8YHZEJ|qk;vD*m`m!dXbd0nJyD z)GS=WZ>IW+;6*%|rBPuZ3|$NmhCG-@*&pco5?)4p4B^AyiKS*wRm@$C?*a{`%k;rA zXv^OtmJp+2NHwD{lX-j$Rc%)93T%YB=K)gN9hyTAO`Z|ruWoA=k51`Y#$DVjbxv`a z0lK|8_Te!}?KyzVCA`aKad#3|QwVujSjL^gCLWfX;)3T0sI`!Bwqw@}xz}R<`i0); za=7X_UtYg?Vt6OV}(LEa#6ePqg<7+}zia zg1)pZ370f%$1xYE^Wu~>kuO_%QIAFid+6m6KI6qU0z86WN8Vx)2awjIs=69}wFpJY$e!1Yu*GkVSa zp=z`V3#RMkJF16sStQb3vO+coMF~TCl!ZV+ooj|ns`41+IXrp?^cr8wJyrwCTLfD} zd?vl|nvVF-V4@LgLx_rgWLomEoS>*dJ&8$^W1W3M`hLe!?ho=0RRJUFw=vP#W)C4M z&dVG%EKC&Up_(^BBs1LJ?AnfG=;fcG9uL|^bXvMAq1>(KX2-c0%jXjH}XD||yOugxq} zBm?r210bw*Cj~X*{eJ6ba`4ePP|Z4K6kQE@-?i8%qZf`Kt&R|B1m;R#NTk;~?rsVl zzShs}I(ZrcoZd)!|MY#$;xT1C@Y3wh9tI?yL*xW8##||c-|bqjKrs_?gQzunuiyoc z1mhD4UVumYF2U5t`&zY>)-W`RSJAQWi?B5wj{}aa79B>5c(o$TXm>G?B$3BVLy{6?%$)I19Q)N+51S3-V|C16e{E z8g7GB&6^#h;)$dIpK^o7>uCVQ+t&OJaS+TK(Ec{*Wo_}U(vFY2I ze`|<=)zC{QSMy*8&NB!dl`vSEaiRILos0Sio;()fA~scr@lmy0BA1}x6Wdol?@Gey z_%97@RLgy7JljXc1FG&NBn85{I1BWr+0k$AWBqe@m_gr8x;Z+6HjOtm4kgk(W}R5R zuQ(>FanF%`>yJ3mGOllj?}%Lxt)~`BE6LG-Hu_w1qRlVQr*w$YSxom@)EYOU)W>|{ zKNDxa@BeaJ=(_cW+(g9wB4jm~mN1W*GJl%!`^Eg9YSSRUp_+g*50eHpidE5o@D~4F zB+vQ4Xe^aW`z3CknmTT>bcs1@b{AeSeX;on#xPU8vc1G<1sI&XF?@1`AIiQQ{R7xKmDd{ea?`xY4JCTiKy~(goT`w|g8tLj{qM2&a93?4 z3{|{8V=Q>&Rlk8=C&n|&5H>R0{@+P&w)*V|U}l+a8zmdCGMS_qytAI-#FdhrMO*Fe zfl;iS>*ct)>N)!~wjDXijmUQ3a^v<8+4 z4G+-xn?s>FoHD8s;a9|bEbneAEnay<0v(WiNFfEmi<*+Q@DjAvd0_Y>Ccmo53!?rG z#}}EWLjtO37R`<+von9Am2-nXR5>(~GVHxzK96RW~Hwia;WBH8Ge<~m` z`aSjCg}wH!ur@~td4h0V3Clr|AEt+z6ljEBc?P@qQ<_!L@L-dectvE@He$QVl%;`k zA*8AfnhZ%+97=@h%|NVj7hs&Iv@j6lf&i4bycFOmF8X+D@1nqXY7oj@##q&W+sRlp zN0_d96k{0iM#4FLsUxQer{(VLJX>Lt&%!0~Nmp2;6K++3K`wrgKWCJ+3L*XKx?EN)!OCQh=*Rh#AG`U^NWqfx#EM;pHB3k z>VV#m73%md`B(FtO8F86>Zn3zZmr6uw6YfDH(ga6C#KBCQ5zM7QQS|=9PjpB=4J(G zzKO%VTUIXXtT~Niy?O1#8xkvVp^QL|7W2w+7MxeVQ1B=h zupwrtg3Cp|Wo@bK9g`Qd7Z;9Nb^2e}-#4Bs^{!AnGpiQGC`8PIVDy5Ao*j{O7h=b<@q_sGmDbF5N_Ak>h_^95L^G=h6i~y<|#}Gf?es zKb@c|8Cy3FnrovXqfg@-c&Fb&JVRNIfen>YJ(VNd9UTIo9S;VCk@bGLIiu&73!~wX z|Bhlogys?F*@n021KRy{F86cY6sw|Ln30(m?fzrnrlF z&9ymg(&c$e)0vA3d3~6{t(!&0_?}QlACy8Oz6UiP;#uXj|HEHq(U(n3Iqvnm5CP53 z!KOQgjX_-(2%mjgatT9oJDG5izS+`bhi$wKM5G7DhVmQcsl_#rj@=+BpC$+HZ7?p4 zuXMP2->62>1(Yjrm}vjNq4iQL6y_xn1%>|^=@wx`-z}G1a?)bYQa!|INVwU#$JO46 zAY#sUG$s)CvBzNyS&WxG=f4ok6>V`M7cW~q0kHd5MsyH$wux32_qv%CU#bE7t9>xK zmB;YjmF7Si4NFxOZ7yzD!+E>f=ogF$-b|M%CMK_ZeP&MsX{6(|JyN5q2d&9-GUTF~ z8@A_jJ1dAjfKA{*##+B4}C{6KGnL^RCvzK5e3&VS>KAJmGpG(>c!U5ynj+VO_OS#aVcb z+xqfm`hA`FDVY9*1cPl{#mujiLI|H&2ajB@lNJSKQgL=s zYJ&;!Fx^lcaY;BKw<1>g#Kf!a?v+>a8bfu&Zpta7Teo{L29_zcdX8O2{-ANy)iP|` z_O<;On#fpJ+QmoJ$4NS24fNYeWV(#SC|Jz@yFN?*8tnY@%0c${e^X(@zV{IGO<*&_ z%^y@>cwuJy-3%~63OB)65Z;ySp?c-+l*iAWdpk)v1&(%xe#^%{Kzt~`DFfNTSQGMC z_wg>xc^ZJaYPD7adr7#*+in}M62({rf~efisrnq+sVD=qE#|ve%*)H6+%kBvR{Ooj2zj0=`?08Dj+kcL>TNO@ zWRSM~)~#7W!+$a09aN}3K4IUPda5CXrv_h~0G_SadukYxLV~tun4SyltE7ylV$Nhx zfB#CqvTBckPsCH2Q_o|1BiE4eb!6OUh_%=eXB#K>NowT`5=2Tmxf)PG6Tw`e9+OeR zb&aNp_jQy-jMlI&Ctw3UZOGmZv?I5p{mOJ>@1qrmTbT&UmDx5>lV~N|azw-zmr@o2 zyg;wX)(i96I#{dP@aUyqg7_;}uH+pYEOj=96!IM(cFNMCF3PY=u=C(4 z$*fAOf_fKa-_>^UI^K(2G5;Ihzi zi`UC{o`GPpS<%ck*ikd$j%ZV4*O+9A1(8?CSixd7tC-hxHRnV@u*}0%U@R z=%60J=#%!aY%Q?DkR}rJl@tbfCv=jbfg3TxT3)&0Q!bXy0zop78qpdC-~d&RLX@>R zWtzE5;BmMm2Ar^TBbXIx%nplG(IW%`{z@P@4AYF?>vajRf zFf^O9aQwIfq>RMeC36*k>QuF`8j^#23AVn_|1LgRMnc)@3VjHf!NGl=9Xa zWY)5e83IqyNJ{>D;glhfDo*FwG$u?%Q|o|H3Nzo^J3un`SK#-zi}{EXYh&T)2AJ{` z^k>!j%y2WFKwl@?R!jD$^JEJ%H|Pn@eeXp^U&sYr_r z<_2@JajabHTL2n-giTs*>7Yrt9E;?;>a_DLY5Hlwo=`q5N$G9}^kL;y8-KxC4*p>x z5C_wP?LUBnE|V{V*yDT-(_5%boIFmqtnYxHQQ_Tw9K)|_L zeMwP6OhchJ61|vwWB+io%48Z-F3zr*IosI9lY>>yc${r_{4YncGO)#ES86HT(==*W z3EkA13m^#h1Hw`vTq#vO9Mv4Q`@@||GI(vvoLld_5%;~-Kga!i_GVGu1Q{0A?+0kX z@8Qm0TeI=dC1LXcqRGE!;nk9AwBLx@KUB6&_%0Ql&rFlQkpRtDx*rFx*wR7aY7DvY z?V0&bE^f^J;{hef%P4|m<@p7^Q(MM6k0A}-+^Yh>$u_{nPf+b&FU4Xei5Qy{evizQi z)6aLrtz{YJ6lwQZdOIE)!uZ>~eYIt7Tw1?b)2_?5w<%fYx1os-M^c2EDNZd)@$Z)E z_jve6ivr!BWx*;bnOwSxqclA`-=GiD?&4{Bz^h>>dC6aD)&9^i>&~IZ!py6CDP1jG zmOGJ*wswP*cRgFnUJxxpWQl1ZS>AnMZ=({JVv!@_4pnxfzKv5|lQ}+UvEiGb&I@k9 zx0iMjZWVV(dDV9b=f(2)fFGNRHZzCJsRUzLy83C60N) zcRwp2pIiR_!dflEPVvQ$p*qyO6N>lnmA(_U z^O2p_AG-F`LxIBi1jhW~cJdkn$w=s%V-pyIo}n zS!P9;duS`hTnD8wpdQOw6*^L(EZQ! zd=D$+xtgrTEelN3nl^SVE*1KrwP?a48i*!lJnWO@+1kmNFaBTBza~%LNL1P;)nV>| zhbkpM9@jXT6xy0R{{}zeskKbOFWN}B5dztU;CB2}GWv}k<7W?3a9?YKG?LcK`ZlE2 z7h5kqTMUGILyJ1+X;BI?4qe&%dK0syfedkll&SPH&T+-R2FY3ky(5sf!t)43-PmBx zN29$v#QVYl@gV~>5?393jstzlhXf_nGqrn$OWdT1VE|Q?_Q{_xSo{UeOQSN#8(v~w zuY&%_4u@e%tE#RP_wn_4`on4S22Ul!8rBJdpd-gwvy3t6YJOwXBuUpcw10x~(0|0$ z5q8ksJ$xMo!R;mFBHUE;>O*4YvXXhhec7@)OtH&j{%YxiH$b1s@V~H(DCm$Z0yAk1 zj{HDvXQoCQoB0HCVQ#{la2^k1|1$BLGJG`bl7Q!_9CN7(lz)86Mb%C2ZAcdN4nC~V z!ZOAcvkywo4Kw73m`bk)@U4NO>}jBDSd5uL(J%*r05{xxp;X`3uC!Q*F`=owvc46j z3r*$@*wY1+S~QjCjXJoGAXz3M0_Y2Nxl>#TlDJDOjZ6pI$k~`TPN7%4NI#jQbAaK6 zmft;SH*hS>jJ_k(k@}hCc2DA5b4>2W?4jSQ9q#*it{@EKslXIX#aAs)>0eJEhTUoV zwWZf+=kWati&q`8_U84E;7A^7zBbop@i_YQOtJytK*?Y%84v`2Yo}CSZU6A#w#1c- zN_miU7-}nQ!5di)nKWY4*O_|5aVsF-_6QOyCEJ}#O09>;8eW8VpONRd%Tvm6iJ7r}NZZPPfh+c}iv2moa z9yxLD20KAKmKu1!9GAwi)hBu_RqfPF-B)3N6GXWh$SeImBaJOP>J={}NEQR>X6R8Z$!YFC=0|D6-t$c@edh(k(w(Q_gblw#g-(lf4otfsw}#L^4ageg>lv;j15C4wX6I{m+ED{wMurIcnzP_YX~%L!Ha6tT9QW@N&+!h4#!K3k z*uXn)=Ne?bVz}}QF{{&FYz*<$SHer<*0T2UZ&FW)nEn(uN{vQIKF%H}N736u@-4|V zxDc;O!I#&DhI#Mj3dcr1EHdP3BzH1Jx=ZM{QS(FnIM5h#3B`pi4t4FL1v93DsF5tQ z>Cabg%iHBCb;Zg&jJ@7Y7Ko*HJT82$HsjE>aTb+^hG8*ddZ-$cv{PXe7H?bY{6?Wr z4m&@3$0H>1113K-nz%8A-Fs*oB~sxWRJBpi=)Lpk`pR3HW@*Zb5X6{b%l_FajrBa_ z#;6b5_lNoR$DUObWvF=fv&LuMU${emp8KnfJL>?w`otPW(V3 zP$6xB;n(hRfQ8>x`knuquPA-)2gV?$|-MG)CmYF{U{5}(xjc~SD z4Z#;;_ftpE8a1AQ`4MtS5Vj6q z7*=t!_XDmMv!^jYkBeGTq0y-h81Nl$65!%_R5qI@*X)@Fp1bWke z^B@ZFgwDNcsEm(c=>fjEjVN{DPqLpE_T3RR)&M94%)~VhhA8T)`XzP=8jUMhR{d(( z-s4B~(w!3j(f>pXQw*=Xul;cO{=%kms^9a)6E-}CC%dl?f&~q!*Q({x#kax{y(@RzLHrHIIte0AsjlR z(Waq;E-morH~6;|;RuH^rhc_fSQ>RT<*GL4V1Tq^p>vKD2*u0sSjq*gnJjy<0LGYI zFiqg?9R{#l&pT6d_FBEe7F?2vFC@!mcO2x?ToFIv;!qxr$s|`#QZpEw&5z7ynNcti zafo#i(b7zW7eBi}kiX$D!{hZ_z|d4)Zx@P)J~qha&X$;u2b(bX$OciDYS25795RcQ z;w)t|s=a8k#{}04+8I6=$4b(|u4TziGPNgL0GJm+%U;znEwYy~7Pkkh=j(bns%@#i z56fIg#c7q!_xslwngs=e76pzu8wrWZc#9j>RV_3kb`Zm4bmJ?`w@Bg~9#$po8vn`{ zhe+|PgmlG5LtgB!oVG5DiL_h9M%!&~A)fJL^*+j+O2YV_&#_gY=>fGS%7abn-B9)D z|45X^qLU1=9+VV0f6xU{QCmYu&R5V>Q*t-Vr zQj(QA#C}akuZDJWaT12M@ZhLV!3SqUpehy|FDWZ})H6&4FDSx{{mcnm*S&=!0(ym# z!|(XZe=lNqSMwI-z#XIMxr|F-t4WHT@Xpgb8XcVnuY`6QCq82j^T7uD&%$mBsz>%i zrN?KzG-9X9uZbLa-Iw^_R>TQd=1ndYa3jvW*ljvor2)r!FO&xoJv|DD=WX~=`~gGC zFoa$DBxznicKi8{qtl8(oMy?V7jA?dS9YPzsLbR2Okg0^B=Z&nQ6$S62mPY@VJ|iD z@A`%hT$KXWNzm@WaKu3>Uat8_aFfGAfrR|AF@*(LbNp!NcGOa5>=@zqtb_KoG`pl^ zv!8hpUe8c1vctTXbC|+*+sL05=(@o>lo^=;Ye`;g*1?*boI9(~_cN@2fc)|fqqsoa zGR`FblI$o!0p*y8RhIHRw47h3CdOER+X`&ycpb@hBfcDk^iOdH1SONQ`-Qb(nDtXi zoI!?FVeL_WQ+=Lh4pqH#Y8W~nmX?LQPEq$A@oQ_3XAkv;Wttq*g649%hxmfGH6OIs z@O^7#(yOw}28YRoV4A&g7?cdXVCm0$O$9a-i} z813?c^JEv{5zj%Y&iZ*zW*D$AI5!2)!)2Tsg1Nn~Pq+ZYd5F4#@|cK}fH~wyUDoV@ zz3Syz;3%{nC_ZBJ5pgixx^(r}gFm}PqvrScky3Lb#FqISG>Sb(4ZIvz zM~}JIms}u_Lff~baAtS(=<_knnzU~>e^n9S@Z@t{WJ}459h@N<8w%Pn3E2q_1m=CX z%e68m^WFZb^Y`e=mGyrBGRzNHGB!eEaH_`6NpYEUZl(EZ{wjPMX^k>;-^A+r;_adu z8@8n^vn7unnbMCy8jJd)1;T@IlH*n?p&!2SW!g^Ul6QLd@v5(NhBIBRnY? zpoyN>o#N*Pn}XGN{je?g8&3K^u&GoFr>mYP#R=Kp&nli`Q_4*VCG)9B zNuVd_O5oP$Bm(H#YGVlO&@RE3E82KIqLF^FCga<=eyp<(kRZFGoT{18kSm>?Ncih| zzp^nQSHYJtoMtC9uX4)`K~>zaDVymxxw4Q>4=$ybVt@$TvJzRpL&#Y-l$vvITOb0l^E!(N%9Ly&UFcIyNky=NZy*G+c}}NV z4NG~oNg3#+PT{AXgViSyVi9yjVT>C&P9@S#yR}>>_$=D~82*DO22xxR^GZ$|B_MNr zcD=LpcG^Cv(v>wk%6q(q&1^)M z(X$ul;-5W!j8RZqX|UvEC1iij=gHppJSs(--nQuZ@m=4fh+Tu6)Z#!nfsR<`pIy7lWBRKbX9zK&-EAu(8?LDBSL0Mm(Z!(Tx7SfEQ}Z z=vN*-;?g%Ih6^%s!_=4Rl{74-VyD&jXkj(fgB!Xp;(zZP#koRSDToujSl4l_6?tFY zl{i=}J?qBxEN|Lr^>!Y5r*lw80Q>QYw3B;Ig{Ecl4!IL&Gf7NcIQVC`2dTm~tq_4r zaa)-j7aJouqHbih=d)X8f_c>}4lh(55^I6R&=wvFTRx{dX{eVn2(BZW$DuWQlaCV|rogHX`BFj$K4Dz^=>L;)m2BxYQ+$?4RL=@Nq)FHxqdvTzxZM+R%*=?XVoLj6lycf zaejYgNxubdg7GlwX`HEfUv*oy34ch|{V*l~qWPGh8DXR*?F7hSN_)YnQnCI=dAfgKo*beQl&_N zkv5LT-c_)ruD40F*Zy!*j=(?T7MMEMFU$pk>mcQ&NYH z=xo|EqDZ>>H<+@Z`HCNRWw>pqUun@5>wKpT4SUl`V`w#G;g;PMtzTl_iRq4*tS;^_ z5Z%Gb?l7VjHdFeGfVY2x=1NHEzlPv<>#wDRU6;Z>XA z=&%apFmF8iYdE3t2TmpTu((8|aOwdN{}0BLt*o4dpvruB@5z2z4gzw&~f5KHtU|u5oQ{|iJx2^d*y~aFTLd>Q8@8Boc z!h{?2(|-8d`zbjV0uT-mc>^5Gm|O38q4EnCtE}hGmCkrnCJxzm$<2l&FA^FJ^RE8v zM?*)RbF_Z|P(ix!^^-b+J6o7@A85fYN~_rRXkf=KdzkWMaDZ~Bq^_=yy$cfj2jC7U z|C_LW11nJeaMXzwrQP&&T)C#*)VHOWS{@>jnP%}Op2mUgXpBChao)l8L>D!26Cp!Bx!k!9J z{}Y1sWh3HU{gKF?aWQ14t;89pA_+sq$@a+hBTYHA;yuP zdV0%Ue*Ob2v`w)z2D_gDip4B5v)02*nC(VX#9|s7c{V=}y-c&IWvAD4K*NRP(H}a8 zc0y+fGG4;TFhzmP=BCych5<@#J|rJ5p}dN3&~-nnmuhgHP%xjR1n?4H4F$Z8_W7VL zKAs)>y1ur;tUZ}rCp-A=v*^NWxAtl?<-8w*hx zho)*u-a-i5`%ygCvr}_CY{p|5mI8Tl9uIdY% z-tR*o()0Pn`BezYB1O2V9>RRK(eiF39TQZ#Tg|}JgZ~_3OAjLn}w+la5*((IU84wji^72 z4EfGMsn>MauxP?Rm*3L7mRZOz_5Dn6#Sm#uj9A4QhX7LWA#*Q#|j&Bh*ct9~7IBqQuY%IK*xa zLdz4Xb0k@Jya*G2&~o}EYfcTWX|UdNq+E**?*TwJ(EdSW270pomkDN44fO0_%fihK0CWU4(!k}p)A0FD_$;0Cd*f?*hF-|H(IVr zzGdU?q$p3)&>ENk#@{rSy06OQVj<`g#>{+`KpW2T;iRqDW>S{qQ^^n`@x$*Z&7038 z%S<`v+OzyNY)PLYyHy4=N!G@7;FCC-exLcS=&L=r*DxKz{^q0H@Q!1qn1L#4bHRK>88HLdypo&_Lcb_x})4F=D^5@Msut!SjHw{V}qx)9&e$FmCaMPE(Y($!~3sYuwaxhS!iPAuUx; z6j=TWMHHacD?8En2jKk&_=U2mh<7}<%BYd9k-Zl4 zcNI@Tab879tp6SERKZ0)r|kKu5iRN+6AFG*mM$RIQrVs)psRGVApb9*m72Tlw(5J) zyNgc6f?vl~>Sb%}g2pp%%u-IC!=1}(l^or7x#&M_y(!9AvO6iLtV!DqGAnQ*JeqF% z(0$BkHK<=(Qzmu(S4qR(QD&1={2u6QIoX*}T+whCXOL_%9p6+Ts|OmxulGC+xg|U2 z9cV9*ZfINSGNh=#CZv2{z1qxhAD+A#y?aggHK0hO6?0nqxAsF~Mk(04#SPPPt!Ss4 z&>Y3ao8~vpghOVi0DlUX>ZBsjNe9h!FvXE)xfH4$j-yj1n1ow_YF~|POKN*NuG!pvX^Q`rL z9TpXxSTPuuKh;ddwDB%4C3@M%<%`qINkzdy)I8OLn*-R%?M;F7*3> zH1>Zsb&R6AMmv|Kk+sf-}Abwg~>qh531P!$tu-r zZ4HxBw{*c@uy*>)ptuLw|M@GJs#-IhPKgIsW2pCV4Kn=CH3im!z;2WY!AR7vh1J1p z-aX8SN*2a@#OBhrvV{Q7g27jRFL006T}!^z;{!}KyGo1izRK*3V@T>xcL5hfkQOudaoo?~&fTyQ##mQ~{SBnV~(Zq`>HO!)($#WDtMFVJknEPzld#=u zf|H-M`%R<&0JVrGp*0PJ+0mFqI)3(s>FeURF;a~lvhLNwsn!k|&>qlj87G;M^ z^0_M8m%R#NJU6&N?Ee6Mb2U2K=Ng*ILDe4AwSvEgq_0-y_3F3`c6p*E^ZT7?prvEr zZ;iTbqeW*dx6(6P{v)B6vS%oRnnzxVn~v*Gq1N;SC~8Sv7|ODt?R1XI_&s}FF^uy+ zCrrTqyST!C)pmBhhRPm4$ll=8UbMNvD(ESjQJm6+|MdQgEBgEBtsNQ0xm>4ZuxucF zt6Z*AkyxIrwtCcjgwTP+jBMAg#V8BT6mbY&vvGzojBhI7pwrR~gM516(jpn5!~z4q zyk5@#mC&y`CTjBpL`sj4yWiWy_%nV++NcyKH7Mx4s%?|&IqGXDFD&Vj~{DC5j@Vh4X&xY7z4v46rmT8w{Zo8vh+}S=qAxoG@`qLF3h|s?`ljxPUs5d1FDX z*ZG*%7@IZRo?J2X>S?BS;Ta9?5J7ABp?-%2bKppPOA=Q*NYa03>!(!@oZ=k=sSmqMa;m%JY{f{qS zk;~yBQ}uDn&y!_@wQJNQ+a>P(TdZGi2++Sz=V6#xv!FV9-#%~m5Grk-OYo7oF~g7a z`al~~D&%%prN^szBnU~hVNO&IONlugP?aeXx~(lYrOs%{yYrn#EWQDwb01;LOu+N1 zwSAb7;BC}o$g~P%);;C@3EJ9bQHGMQ8SYqGRFzPDC382_WT;sM2<(WYlfZ2Xe@IKW751Webnswwax=kH!<{JfGVjKb)-y?T6nA`|X8zJ1zj8^VW`3}Uvd-;Es z=VVFV^TKQ6mURcIy{W{2*K55XBNZAG?Po~Rw(OVy;!Tlx0|9~5-rMR-$}#3K zH`ygY&A662;(^Aayb)$Isri=2AtT((v`ix;IG~0O+^-UK;L^TRL$x0>c2K!_LLd>w zyo*z2peJAt|J87C(VLY-5>_O9uq+@sX_DjN`epI-iFL{QIwxTFu+Z5k6MA>|W5aHv zmCNgyze>Z&Ye5w+&}hA_MXYV zdq3axto2z~0^{PLb3G&y2{zm^+>|dgaB-Il#AmTya8Dp#7k(~O~%^|&0BpQ zbYEQ(6p20dCepFITd@2Sj(F}J>#=ku4U5G`o2d2sGt%$2rr>Lmm9KpC{li+8y_U9k z9h{WlvqZ$d2y69#hKAQUDT#m5h+q`4?YJp-$L>Ae0GKi^b>M4?i`kqSgZxywQv%=e zeGoNeVFC&c;^>|dwf+6h{z23*R0Ig@b8~$GX=TLj8`5hX>%%aL;vPIQYdPo8I>;8W zpR^X?WY!&y9c{OD#Hg6}N)Ra!k9{m8Qd!HRR8??IRlgj}5^;g=`;VjXYcn^xuVGi1Hose8SDup8S-;+@==*Gx*lf-%GoWtZ|vHoEZE$2}t%3XiJEQKe41KbeiJ4l0?=l;X(4GhMalzRWT?4CZ(XF4KFoB zc2{ffa4_QvP8**E`z<1%Mhq{hAfV2YbvjtG@bA!utD8oewfS!7O=s^KL#nqGaC36` zk-*n7`7A|j#mUmanX{#ew$fdF7;UfGC4#F@zXz~`O0nYEEJ9&~A*VXxvo}U%?!ZRB+K2fI@K1PZuM_?qc8Cp0A*U&(AG$Bo!i7`-)v&YAACLhEvujhyPe5S`EYIqUFpgtH{UD7vRbHX_3a zg-Y0H#5=_WeN!c+MWpZeHq33e;mGcBG+GQ3rNsjdrJX9b9I%A@+~B9D^}FzA>(@m* zU22C2owaNa;1^k zVA^m}qZf8cicGwddfc{AN@?Epf#cRrF1vV)=o-1T*a4H$uk=i183_Yj%^5Sp3saPi zMj9*pu`xm8+fH3yo&Z(y1NKeGQ%WXJyOj4@{eOq0t+suYqAs0ZJk&XuG8(?4RAFO? z8_~F`SXldd=gMgB4ca=PzY%WhzkejzZGESVdXdGV&s*@DzQUeh_ebJ^+YM;B|LJwKA9WT9M<=&Plp5!e>w!7h5)U0S+$Lci-rc?gX381$iMPhozHzhyrvMLQH&_(>f23UkHCQ zWWLe*ful=(zcdm;Y+za$=?h^YkX$}#F{Rc49>)fMh1X0~7w3;C!2!peN$ zKm5rwWL!RL5&Q(oj^Te)O~qn5AroL<5==yC@YK}MXt<@AWE%xUqz?V0?+BmMT>{VTEmN}v9>w?!2d7sg%u|1+`G{|Ae;xBl<@s(<0Pn=8bZ z_WzWj|Bb!DKIRSM^WL~7716=>TuZ`EIIK%y?IN|mO4w7IZfy1#Of#j$X;oRh;=O#u z4@vvF&SQm$OH_SW!QOQibqvJWy8R*XWS!1?7&sHA=wWxw0htFH?0T8bWlpQj6xZcJ zmC%a7#u=%)BWNX;1}i}nTed#Y(W<{#6jGhx!65m2x5tb&_M*%4_Qo!Q6l8T?sO(?|^Db@B;-(T3IXjDo1@3MNIgGnS0;Ry;~Fjj&xxjByE zKLXP5Y+`p?8`gIt9o4Au@sHfw{va5e|0nSWF0tL=c^NjAE|Qlfx&{y)q+OijxBHMK z8L4^)#jLgGHAsp;2CgjbvD?z$>C>tvZP_X^W)$a^0NHoEX5wrr)ZQlizqH`-H5+wd1`V?s33h(N2k(hM*PjM^$@$sLgOO=j zmIuASA3r*+p&x(6mcOn(YVwS8R3+0Uv3Mb&RwE!=i7V%;AMMg1$t$yPF7|MGwy!{> z5Cp>0*_kxp0iNjWs{Qa2k5pfr+$On2;`0TtS)kD8*0wvn^uWD)HPEp<(>!P6=-Y1C zZBjDDLUs}sfLLre_?bsPPN{lw=RUgV9!J0MKUmR@Y%U4SIdp3ZRl#TE&pC@X)`fV@ zaQ#8)Vvb2A+S5W1VZn(hZUS|2Op(k`XP)A3lekzr(u$c$;r3qNkk7xKW%|cf743$BJ`aKn$D_g0etbBvkzU_5(ELDE&5M(#;3FiX zI;B_?`+O%qGj!LJbkYLCLUGGZa?PZ%>3&YnLcVl#TVD#wW$$iH1GF(?K*VncrH#Zz z)348+WQ~Lo1w9``!qV=;K^iVT*P{P9fi*yZCD zP+h=)wRY1R9^aNV7b9-j;*rn>$9>{pv;vl4JqWSGymnmKT??12Zmaj*KQ2ip4_^9b zbS7m#vJa4aZfh80CP|o$T$YLI2(003x=O)Km|2sdukERw9Fc~+94CRUt378iX>sjr z;GJLXq+_vbs5j>*$w$;oyvufWdj=IWX-UQEkTnhF=u%yWmxQ`9jurTt&ktHL)UGiy z>WhR|*5zuXLJdMG)Az9>^mU>bNq#0RDA%^LDdslnr<3B3uvSHDKEXEKX-bac&^fy~ zgVF`^9nCrkdFymDIN-GMZ$BF(#AO3zh4Q%zK9Q`eMENczk9ZFA&TGU1^3eort_TV? zW?#!w#*B#SXrdbB79IIk9US8It$GDySA$Qph|Xkm@Mqj8QN$;dyP##;5-tXU|zEzP=$KL_4}Z7CQhwnQnBDa8aWErg!=eQliiqJ^|* zZWAejz}qKTjh#Fdcy9|19I$Y$bV3+3P%#k=j&zv37Dpbd3SaN9DeLwuEt^Khgx-=M zsQ!w-AI;pSX7V~!K$K60lw}21BZMgOqkEYvAO+RN;y`;c6!Q7g#j(v~j>yi3Sn9|6 zXz>-%GCElQJwnaEWsBM^NMjISWaAj^;mT=d==RZF))c9Gk*FJFq$<2c4oM#Q8lXKrcm&PmuUlQJUcQ$t-H$R*RT=6{$~`S=bZbaO zkx;|vQIjRRNY&gOt_$j``IZ2*(lAXEm{&M8;?E)bwLCPBjSb49cGS`KlXMAaspH?- zHnE`k+{$G&`4ob@GYuF882w|?+M&6j5r4^>Jee-z4nVabn`r?myMjDT%229GvGm0X z{3s6*kVWz!o6ox)E$CEZmJ~XX;w5uR~I_enflg z+XOyNZ%ZVjKT!;YkgH8{d8#{g9gQmK5%?|jz8lt z^+|&L7MnYf55Du3#`LOz_nUmRqn@=T`iZ&$oXlibzI|vXSY_M9L1-Xcif^eEN}C`{ zE^6l6_A~J#U#qa7d$!dZ${>Ko*_1;nYKt)BdvAjytJ(t>{A%L!Enl`56$D_<{80 zoJzFzv~{Yj#>c&`H9*MI$-xL~^~F!%qccG~>7bO)yh#D2QVJvBMQQ$_T?B7(6bizg zwLmZ8*TLTT$&1h_Wclyyeu&i<%T!)ysAHoo^9DB)RKhHVXZ_qE#8~}te7Lr78uDn= zwTv1RihM4++jR=mf`%$9d=X}Ze~1;M-NYM>%Ghc@3vYgqL~_ZI{Y_Adc}l9fOQe!@ zm*$Q!IbE$!d~f%hT40kCUg-t>!+tJ>=`sEE;%*1Cp9QLZrOLj1myQ$g39}SdfZT?B zCw5hA-|(An+(kUTO2w=^h+q9v`*1g25JuED@`wG?1KWT7ZS@DLNDa=o5-E%6W+n1S z6fe9xZwt;LdzE((95i0LNB3&;1e5TjHs)}DhL8D@;QXK9N*%8fUtIw))eI|FAAUTw zI=~p0J1$!5|9;^57lEn2rja}gewWKe(2g|2P#b8f4gl>=L4e7$LSM+YKGynukxTOi ze?V~|&lq9%kY5fI%ayuU9;{tJV6BrBbgUww64lJ4$!3j zIIP-hH(AbTvRocXm+O#SN4GC?;^6Rxf%Fz8bAa7kCXoTFIa7989Af&%Pa*C?1{oKh1BGBGy5->|}GS z1!mA4Lucw*ja9C&1h!gx%T)t~)77hRo_$zacBkhmnVa}PFF*P)qX8w%!2d;19jy{J z!ouzcW{0lFQk)7`|Kp8k-JKXzO3N}JJ~tPXY{_lFLH4kMuq}K8#Uh8FGaQ_lm*&5y zIwteZGEib}?Pd&!6zH_K+yf-aD|t!pR<0}Ptp6tMMmriv$dcoKa%|=4W!d6&pSbo2aS+0w)ckB8X zdH8~d>^WW1n5Nc>SyT%`jj2B?1>&*R1rO0BB@lrabUltuT?m@p=2%_O*ZX*dpO;*k zlCU=56?op+TQ3rzn<^O;1-t1CP7&l~yx$-d!G5!nynAfbQx*wmA<=dzKw51@S9B4( zt0iL-$r|^tt^usEF5gu3u@B~L8H3!Na97&0c*CMqCc9X~y28Jm$u?{w`1foHO~2Tf zzvt~P$lpH_GRG!9DkxA98smIO7-gh$$kFTiW7tlAO6nO48-{i%B2<}q!DC%EHq8?k zk~E(NYuRVi+YP5>^c#|Vvhdzhhi8Cknj3qaOt=gJm+RO@!w;#k-;S~GbQwl)uRpI~ zfE}B&y2XI8pR*LiP}CkI2W&B?Upgeoe;4fap$~&JAzp#cZp_`*Ur0=uw|P>Bz%sLE z+>)x$=T?+D@kGk~_VXRu@&^V&Y}3Hn1>HPmhwZz%Gx20|{I1t>5nc<#lwD z*T}T?O}Le0fZ7xRUkrlnx7It9r18}SzY*H22O{MvorTzFXep(y7SQqthxBll%rs)M2eoMme& zN0;~O4i`YpP+v3)px|Xv!ER+cE9TAm4FzsMbC&ZZ^Vn{5)>0`2(<1$g*l6j{wJjHP zBver+mK!huG~`a|G0}{97ZaXf}nKI%nMz(y(pXSEBdcPGmUs74V4 z4<&jV_Mnu|8p71LRi~zZTL*CguR=jD@bkMpq`V;>6o-G z)-(NxZ-Q)oZfE}EZ3a8yEpSdKBWXx&mz*+P+Fcsq6>gnA1qF@ICwt%@Lva|)NqA&& z^(@T+W9$ht-)>Z;O#{SHva0l-(h!t)|G_#HDJ<^v_}X%#OSA>m!J`nSJ5~-EiTy|G z`ujr;Xq8e+Y6Q~d2a<M272=l<=Mh*+J3HeDk~9q!4e2OQ&akZa$aHj)+B2zLj){@gc!1 zt7BLVf!8L9lac*4KME7dT9G{Y5-Tk?wUN=3#s-r-xAv_I6Y6ZVXm{GG;=sDOs}ERH z4f_k>;s3KMyY!^mrxoH(xgAzm?b;roAIF(g#$2>p?8F!EN#_q-N%*a4eZl+*+2gC8 z8MN9!BE13&LJUZB=`_t+tUp)%@#_d&V;Rf9c#yfy{t#i;@P0OFb3+ofYxYiOyOT3H@e~o}Ujui>-Bz@lWP$)C~C5 zjaA58^a6eUi8-%KF2Vvz{A4VMl;gfYZ%Yef3Pxz)DL02Ejd~Y-X1(7W_edzoaNIYRfa}zixe@( zX`YJ>Vw{M@<>-niYfC;GA1Hfi;UpV_$Wr2;LZ8s3%6?l@4?kkDN5?c36?r!kshmkS zLA;P9qXMw>Vv9I($F%>Zs^_24abB%SvPfEgf30-+)sRkHcDiSIVUFutl>RxPgXAF6l%Y}>6`A*d!w0pB= z0VIz=(5c504~Y*i0bVyuv}??3zSd|BVb~&h2s5^;sy}~qV_}M1N%^JzO*Yh)fnp{P zBet#eMs^E*z`X?!24PmDz`E7{?|$_EeGiQI)s9i#kX8S491C2+@IL;7m1eT??@Pvi z1*7x+V6?ZZ3B#*|^=4U;uPw3(TkWzW!@OwYZ;hUdCJBCZ9Vr^KA1^J~y(DUEmC~-s z<)B5EWToo3!8*PPKPIgm{G;m1Q@;*U%p+YWz(uc+;2w{`nv>77Pe_cVqrE+vq^yV`Zm}x3rAn>Q1NTJ~^EWD#Zx#ZNf?jfkj}(xy`bF z0*$VSiQ5+zr}|M=itT62Io#*&WI^U7?K14Nzvm7ki<^DT8pjUa%!qxg{`V|cjI#>2 zfi!o-eQ|-Kxv9ZF=(CS7*o&x1Tv&c-$b80^f4o+WP7cw!U{;M4kp|{h3weHV5TvM2 zWR`BWTNj*86)|55?8)71VCZ#;cZ)_pO0XOr>1kKzy>u%Gg(E}ipYUB$4l=$69BzqU zT1{uCsw7@fO#^l8+%^3|y3(9_0ZSR?TD}{JkGsIRGF?^2nnz1wP!4S_*wXP51+p@(vW5+|>Q{s_( zKIt7Z9?+15A-$I`dI;A+`vG@PxhXyDEN*H5Llq-u2b_+#vig(cTytIG`+lNoj0Glj{V{>eK#^d!*Qp{9=IBasqCLXwYEMfQ^5w%;ML*h?S;T;RrvG5A zy>RGpKQdE?m)dcrlI=f%$3VHZgdplR!Kww#PXzVIjGj{=n0^1QnRHq%v$t(B zwbNt(gA=D+G^1gxD_+fldZs8y;Z~VXV{oI8aG=g@oon8>-Djk?%eE#R5s#^5J5|g0 z=rfoiDEA+GW2d~;+*_e1*hiMn6WANu^C}~E`ZInhZkrs73;Xc>>NcE5ab|g#H8(yy zg*m6RWDFZnN<%P>%M*=hJ?s;MORX>}ov-*Z zHK(+s1&c<>1^&g#alZ{ZYLC)3mZD|#aKcvas@IvOR_G@FD}=zi9aTLpB{ss@@QhE7~v>Nbi4F zXgBxpRxEM3A+5)J}7By{an-2<=jt>shLzBUrvGU6~+uWQ@dLr+5;vRcL4MZ~_eRR;Fi zCnVQs#mC!p!=nUuz0dqw3oft&kg$cf^*3uwFd`d`vv4n$7w7?h?`4d_H`Oq zctccVlrge3nIRfc|DW6Xzd@lOyI<54>%6t8tBj&tN-Vllm%Kjde5-X5-ef9 zUM$~%Zv7gy5_JkzDbV*Gs6IAa_{sOvaJ)6beuNGO6*0v*FOPmj?}cdk;Zd5pjrrtI z*?djl9?~=8E)&y}G5fD?5D$+_u?yx09--tOQh(9SwmTxL#3U_7!oI?q3s-D*pcUBi zbp(d-W?3%nHKm0j2d($~NU60P9v0n00Fr4ytu=Bz{b*gk3 z`srF96@dOBjS#-I4&gmy-7@$!1n#hW4gQiL=IB63H}u9PYj|6NWXESetZw_u7pIr< zzP?5%0X+AnilW!_P4(Xa7B;B|zn6w8YUPNkhb5GVv08A)ZLV^zcu%Kbwj|@9efF+QvIZpp*hBmlvt<>@suDjMo@Ad-PG*u` zlL>q)nH0Kw)Eh(hYz<|x*3`1e>TY+>F&eL{XmyYhUEIoh6mz?2NYttHS8M0_gjdX; zO*5kL^hqJ#wdNJYXv^_QOzrvNDF5mX^*bP?zQ4CmfH-BdU&W#}ENu@a(pWvd1kI=L zuk0yyjAlQ-v)PH@%q!cR5lf-sS$*f)Zw~ueQSny#HM@W?3?K4sd(bkW-nZ&!H@-B0tO+Mu?1H_s04L-&{#u`be>w@G`LG!#sFto{Cx*X=j+O8v-_~OmF!(2 zS?MY5np7=Zv^k{tZNF*h$*##KjOk47!;Rre$4b;6sTSZ~H)+)D!+%-m+^sLI%yc7u z7g8ISVk$iR>Hm-ImH)fZ((4t?uM6b=621bL`ogUCz6N}`#emiDFg8PT{f(>q*Q3Z2 z-#(Pjyb`UcA_Sv^;yGF|2s3Au5$`r{W9_z->fyWFM<6wcdy1X{Vt091EdrjhFq+f| zyd}{Ard+z&kGcQclrc7A>pX;KaFMZ))2)z!Yx%vz(M*lOgz zt>H+t!8W?}xRqKeix=wu#9Ro$(Dvd445VmeoYuBlms};LV=3izeJ#o3MJ=@$IbUrd zAI_p{_`B=Dt;KoYmBUi{Y~dzOK0Y7l_iplkaoT8A1ul2J42X;MP5#`fp72%U6!+QT zfrk|8#qjSJYd;6)u-R0vZA`7?HlBx173!5OCqGIpNzfBqjRBdblt-R-( z;5=?swyXe8LtRr7uSpkN6?czg?mzkdQnI*5#-}Z(CP#l3I2RVvAww}a)yGuzt9x`z zt6tE$<_q}>8Q^!z+@@e7vA&X>6mo4uP|@mo^Q-j!ayi70ORwd-%K>V0Yz0Xart&Ss zGxp~baqpPCVR|JgNR^ZWz)!QxdfLKupO2~uge#PU9kAP{bd$ep8bt8AM0?d*^y0U_klg-{vDZf z^U#XtB!qk98;7m;zq-HQL)4vwlGjCu|Er$WAoYuDzsr&r)POYZ9VJOE}+}@UqE0bT9y1(Y0Z2@qiJ+X@Gg19id+<;yKf1 ziHUWdSE%SP*!&1xN*&?v#xe7w&O;Y=3CLy$;99DjSc2+l|bC-_NTMmy;0sDv(UH1L&$6tH+A*rpX~eak7iJ_zfE* zIj$RkU@*h1tL0oo)q7U^U#9juk8c#}xi=1xzFlG(m_Up!GDINR*$st^oD5kJ%j?mf zJ)%$xm-XH@ed{#ezuTrYQxQ?J{H zr+W;aC+$Fm*GRu`Nj<}!`i^Bv+Q9- z(ESkmOuTy^UP@#AznGhfwn}41mm+6&I;k4|0=DQiO_N(>k`LD;j}APZYm{)g(La55 zC!-^^i>UuS?@;Sc9HqLAkcAc`jg`iKu*N()WEMXQ&a%xT0oz`Mopt+}S#!)2HTmz} zk)w^VGR!%3QzPXeF?>I*4!M>q#+yCGeggn@KnJH!mxvsR_bPV7-dn}wxQ;5kq+_D# zDi+IKKak{cc`NPw!jeo-{?#)KDAOq|it%ZMk&h-N)b|Ytp6XvNR#sd-&deKj71hSu z;bxTZI6T*XTaG{Uip{xQBTRx)nN{@Nk`&#!J~oCfhI~y8v#i~|o(?6!y7_`xnO!Z- z6LU&V>$U6u1a5^%p#%_i$psCJ8E=6{=@DLO<-tA*4_*HNH@XqKk7SWb?|sG@yiQLI zh67z5&76B*1%gbTo3mSrdmG+^*c|d}7W>H5@Woy#Q|lXHtGG%SP$giAYQj^3rg;W; z!F@Sys ztPEa5gJxCPfo#hxJ`1|9oCW#9SHk!`Yf9h%<4jx_aH3x9Wa1fSshwQF=mh*YXx-^KFso@wx+0IFTqiWO#KhQB6Vbd!GcGACrI&{TP`C2y0h zC3QV&8G*b^kb`->14gBLg%m;j?fC1D0@}fi9)mdqQKpgd2sN0z$qp`%~ zjr20>`0!6Jj(M`p6ZlHYdDB>@H2sg8dE<{BAtd?cjhfv`)M2Y9;K9=>eGRTW?NlGp zgg3tD7H_{D*V*}x5hBfE)slZdF3jmrM99UY6K4J@bjZa_9cN#K z0Ggu;jo;YbHp%)Jvt{F@h6?cs`?_NP#?zwG_5E28KvTcH=%4?y^lIsZ`L66&d>Utj zj3f!u{z03V1@lZwI@Nk)*N`ZLx9PvaB0lUun+f=z(k3?)Pxtj~7K4 z0Y{ffc)X9X92_gNw3_H`jsR;jd>d#e!iTutH?|NaNmMu;H>Jat<{UjdcIb}nzFU`i zuwN(adY*mEAtbCpbQLb7?y=i-u98|=UWCvuQ?ym-4#FnBUU2(N0-gTEFc^88hwudM3UkpAf+H~ZcYgZ;76YIL&uel~i`8wY!4K~Pito#vf)ozJ->jAv2o1y;(M`AN zr?&A?$-}v&g)_9vI~xqdFla%VhCk|jP{Jh7KDlRwDUhA=#b!hCNrVD@AoZ|8RXPtb zYvh<@SwaeW*i}N0hGRFmq@()ERU+$94Ev>5nM<`s7CJNekPWF#Dg3y}t2eqTO%as} zxs4uQXWxIC3*)Vpu^>#-_2he+QFCHa_nKWZgZJMpOj|a$jG^5Fq?h!Bd$cmH!57XH zlL!r#ktC9Tt1%lB%ss9v$KysS_8oVCc8LU?!4&Mo zcgKORF%GuZkx?;qzbbO3#OET_4&jBMpsP1g=zkRE_wQNbod{?nzbGloBsn2%1WE_z zVlviSZP$+U%fIwI%rd$op~=dX$>nhwEPq|$+Pk2coKRY2*)!%A>~1&fY1%&(t8zeV z;(Dg{v-9c2H+2)CkFFI#i)W;>@YMwnlT-zTIw#wUkX+HKxpF(>QAs&y8El_e2=Qx0 z(6!F>?n25O8{;T33u-*ba1e$C?Pjo!*ajl_^R{RyWsE+oc^?Nn?tPuv>UfNhy+Wu> zWlZxVhAsPK@welM( zs$XmYELL9gHq&U;wi?PW8hI67cJ^qzE~j0UqzwFUJLL7drLNs>y~i2D^QeTTfrgh~ zh@jRiOFMh(rupotYRFzxRd}cWf(59r5o~yw=NnxXDJvQ8N#e3rJH!p`Ii2czOrSpo=ouy9 zs>N;0a;m=^of3)Nm1S;;7{J`|w6mXE=6QWgbPS6t0h|#O_zW>BsuhrFqh_Wf3>VP@RXR{ez85>>|VOp~7w_Cc`Xv?cVU!sv~%t?l|Tkrx?*#2>eQ#-y$ zL67|QkXf#k>iD9YT)8XdPcY(({nh1i$18f>WH7+vi}Q=$RK_mTVH)@3uSPK#u4+B? zg56eX#1nh5m5(3sys)S2rFo8nFM6k|zP2@v5a;!Mw;UN%T3O&D@_L>D4tZbt7*K^p zfl0bsUxCcs+RFn|MHe=2M|<;g!>j z+4>P+II9Q`k9zqOYjoAfa@mwzAP+-kcnCdKgF|1r%99YAkig%I+SiP@Q+#oDB#$-t z$#N`Ni`0&Jt4WeSfJYxlSSzzU+z&PmRQ{~Q{i5Gmmk=>PPIEzSC9XVq=$zk^GTbK`nVj3|5B`FL5m+qdocc2vPd*6OhR4aK3;Dr)%&gg-Xxn%27dz;>u32c4tV z%FPKrXuK9c7)3-InNxRa?O{SlGy8kLniZb6KhdK#7Rw373Z$<484Jg1<)jx8P1ya% zYH4lu%VhUz@?fuD5{2Y=q96xk8*Cg9j}3+Lro&>rZxC^L`@eOE-ho}pHlNuf;F8?T zko{^~AN1zsOJtn5&jDR0C$}_h>H->tbN5*?j=ntbUn zk>XmGMez0>6XNLredkgO6xtBzVJUS3YM4u2Py0eLKH>Z3%xH=K{kl85k8&^3k6vGE zx?MS`8w<3Ach}${Ow{@xUDt~oSk`O3`BTB&*S>f@_Kzpl#tsK;T*!p>j<|5f7Ey>2IemJl&#uea0oo0DmUebtOuG|F++r3S1F?DSp z@E>xni;|jFr!MvF#=Eqfhrd}smDU*+jXC6IQU5}U2Ot41cW33~FfU%YvQYB-K*x9a z1llgHyiGGvMjq;<7?0qOxh@UMB3`HjXR_@o;)B}!kr#;R6CdSRSNVd{2z zYGW6Ut#9#d#O2U=6G1Ut@ojYP#In_qPgDN~%xP=bsj=d9HJUqE4kF3t(ehWa8n5BV zbjPo~OXM)=1|be!YQ3H4hba#Ef2*$M|Csk$q2trk(6r)A$mVAuO*$-C7GSfKXCvI1 z2_Z!aOnZqa=v|`CZkIf#<_jok+-meQ))f*B1g-vxRg%tp)t!oKb_W+r#ZR#vzelaP zOIpN+j|oc|xJ8(iyT zOTw56ebm~}_d`sgP&@GUd(1B~*D%bWZ>m_WuS|{gf4fXcFU`_+Lan}B|5wfWiofO4 zp>NAiiPxGLal(oJY6V?AU;cTjptnlkT>hh$^edIJ7M3IeyN;Td>09iwL->Sp3LFm{{IJ zdxxIfarDYSvI<};N4O0*$CO+D9jFz5v1O4l6T%>-bp@PxdMsWXtF-;W&%~lSzO73{ zqNKi9e#oOSY!sziU6)u-x@t{1zs7ApJKu2@)Q27Wgy)H(3feFQd?Tm1^HWvO|iq1 zzzUEF5Ak%+iT;i+_xPe@NBl4SjT}J#crnK8?b6&{JoEishnp`Ws!;4L*gx} zh%|X_nBwz2I6aLWY$3L*%H{%K1miuYY|a>l8ffvi&Hh=;j+;}dNy-*`NN6 zrn^)2NFrtb?o*J-QbnnvSj6vEO^1Z8D3njP3!0R13Ki=!4`@FQ0wK6Qmcv%Z*z?Vv zPPdD-QfPBCioVFM-%wf6pj5{qzVEEn(z^b^cV^r5HZO_g&v%U@clXj(ds#Pm+s7s- zaFD*#^&a=NJbZWSqrJ-BH)ALcMlr2 zgid(Pb+}(S*SA``+p<{}P^5G#&^I@2eAGhAhKQ8|XW9ZmIPIj_d~{WLaK*YpOmr1~RfJ{&mJ9W^R0U2BH5Q3iUuvxU#?YzYoiNp&@R~K64zwQh zLD%khJ{&03?7~($*J6e&H5m$kJ9ZTtnlB5#VI{TDt6ajkfz(ASxw|y>jjMA3eYI(W z)<~5iD-AkZZH=b@9C(hbF^gSxB~4{^C6!6MeWGdJABC)(!GLYQM_x=s+0!~zJ9q_F zDspo1;&;H?zr?l9KwyE{3dB_8r=<`bBF7>uBSmABlEE{i(9?Kl!u!;q){5C(N_=8X znkHaIW8%`WSxvkenJj?br2ezV)hbXz)S)X%2OyE&xC#C{FyYV=RQ?cA&DK8EFNK;g zSP1*E_GTNlG|nx&@$4iD{!L@}Sxrr&BNY+3R9&xawxL(PKR55g zPqjBX_Av&zBMJk14vX88r0fap90R4hGLe)WWXr|Ug|e4~2ZI&S56E!ZzgS*N^NW#R zs-WYLR~a=Q)3QvoI~;Ips_j)NcE}hWd3VZ-2!p;yy|}G+xj4-(B>Q~AY2Tgw_Vtmu zxA_S5uk4lgL0BX|Tk_RxK9UB7)m`}-47-Ad$BXO7Ki@io7zk{r+nJX*H?XHj6`stW zR~GS#EGcMzhEM5xmb=PZ?q=BV$SyN~L8(^sO&)iTytbf}RGY}?qpPR9ypNNg3uWgW zu{*p(veob*QDJWulLrz_$D{gC7#qjn8gjXW>2$Czk!}p@Ulv#>rwNzYzW*TwiHX=Z zr2M7d3n>cNLt4Bktc<$kU$<=68?5V4AU?95AiK|{P#~1OJ%7uG%8~0{xB4nU{c=;) z-g<1uO7Z-oA^X(JzKy&e5PBZwYIeLNF1d$ugHdDyWxBe0C4m&jg( z`jZk@*^&dLa_5qo@EqAT$Ur3;%f>kp(i1l)K?cX_q-4rl*WXxIl|y_zDcr-|N(^=w z-S{pT(s=~}9&@G?{;}A0w@_I|rw?Yfq@Ib`OQLwsb(iF~(CQ|PyVD6+L zYZt{RYY%jnD`M-bJ;JrIr7iZapM7CgY1Wua`$g=aux=$2>y=)PP)5sL$ZgPEe#S^i zeXbiLHY0?Rb(`SnwcY3D z?z8EVHa+tN9!UrTw5hBCgI%4SV?WaoNBg%a4@dx;PTn1XTT!_Q+3Bv^9x`~z)+tz| zY<#?F(#VG10+&4_aLDN|Qn}yc5H#nqE74Djg33p8lvI(WrjhSV^Jv*E6a0kC=1jO{ zZJT&h1f{>4n9ne9N_BFJ?^4?Lfgp$4bA1z*+moi|f)J{@4pu`Ddy@FgpW1I8I@4y< z-Lx365WfiuWG5we>v9#{%k5hE`ALe|qD*;49H1%&*-5iOY4$|qSk;tf#(%0jTGr#k z%CUd{UdG>?=Gb6>kI7q^Z_@0Y6>0{Bh)j56Kaoz;f}7{l{OG{wcvM=;g7iDKsvJ+j z$L=azWUSXGmd2L6vcpsROeLV6wRM#|j;y`R*Wk#nJfh5dF#O=% z&sB);y8hJK)Vw-`$UvKFpGv#sieIPn?6z!Y_ES!-LQ~}RqQP$jeBm)G1_fJUq6cQo zm0d))7A}@Ac_%Sktr}$rIKDWC!zc&c=;^VhSy(G13cenr2$$UPHZs}bxf&e+O^u$H z$ijAL4)lwZEs_m%#1qQx{oYm7x|G|jGC`H6sFaDR)FD&c%7EH5WJRU=AvHMI)f26u za$Mr;wLj#Egv>GdRcD6!v)LqpyV1SS60L` zpj4-2ZWWU$dz#HS=M8)E#8HhMZUsYhV=bmB%yYXhdC16q4SXe*Pnv2$?06Wckva3K z0BfI|hab5n!S(?lqhKJ~vX4%T9&Yiul|&c0W4Fol(VgoNkJ;3%4q1A4vFtRJwLT}! z%?S{m;v67p+N%LfLz&4UUGhrxkf&DFo|$c1ygitP`e#q5jHd}xw}LeFS{P%Xg*79C07<-=!#(*h+X=DVTa@h}4Y zdRWzb=VLwbF^!r9`(7(JbuJ^?0Gklgp(FWNFgve}<$_N7XY@nOwr@z@HZ^?z&xV2D zxk>rQ_K}LR6)kK=!VRLZczpK>g^oLQJ3LIO*~vePdf1mei1K>_Z!j zXCGkWn_*}N8y9?5xq~h+nJPjuepF|0W&%z+*_{?FM@K(esJ5D+t1`nzYAY)k=X#R1 zg_v6nRtyuEFt{yEd!!d>?Q~$+2-7jL_2-!tX@N5diXBYce&vQqeV@>-Gu2HEKpbZBd8dri{9kmCt@%}4Cp)6{u%WS-7iJO1t9aZ&KG z`z4(ecC&#lub8lVC9b_1qNxgs|;PPz5tx?DVa1!Xbm6oVFqYHiK)^0xg{)<|csmBUwWz4shP_+V z&&pgGj^i>VD&~~O)&#ml>jXt%v)Zt@jl*puZ;IQX#GZ zqPF6N&T;3|xG5%#I|{H*(|)oT9IfK5@RTCv*jvreQ^z&WdEZ;*E~Q=!+Zo73?4m#hi zP9~hmX(jU8|3TbaN3|7vZKJ^*ihF5+0>O$)(U#&Kg1bX-w^FQ72pYV>T|%I^x463% zFHUjS()P>m{l53Eb=SJ@y?@?6PO@g!oH>(o&P?{~{p{!Y)h3d$gEl#Cazi_h>8nm9pJ~k!spW20MP5p(+ha zHiF|ru0^Z%E5$GP z_sXo9ovyenAq2yF2`hnn$+(hrG*?CfoSIH|IHg>yGh!@QWZ$Dcth9&Pcwz_rC>%-Z zFWG*Pt^Y+~*1)7Z+I^s{xi!-O(3cHe<`FC~>i6)jKFu7e)-i6h4A@iX(AzCS^Yrr* zF7mR+yzrr|+V%Eao@pI59O^Ejd7S@a27Lb0{T(=AZl{jw>Jd8CVi#ml1kjx>PmQNY zo4^@Gm%H>k1Mi#v6a@rx`Dv?>p-99e-xF>_&k{;UK?N!fF~vh#`Hv}}WWm3}c;SM) zhKb{6;1M9!83OHXeq={Lp!bg-1V=q$i8}@56+|htgl`SR+YzVG(bNah|=XL z~Iu{Vcb<5Ve3a1gQDC-Gj^SS0Y1k_P}gnE8aaV~g5XBv zHaK)Gcv32(#O~j2`>)Ayk+dnit&H(L-pHS9G*zaYot*o;xmR4eP77G_XD}vqb#z4V3Th`y1GLePALM-EOjpAb9TMo6JQN zy1&rWK>s7Yr_;f7dD->P5+>;kqT7Zg#r?p{USf$MF^B~bCEtc72+h!%b$QW1h|B9l zwK68aAqQPeC+sTKpKNqd|CXPk)>yhM#|Rx+tPD)sOnT5F8A;Mu*Gw(jJ<&ZOQ#x&Z zwDOnbeJ zYe`!S#f6F&!}u9RFDS8w$18p79*6?DYn5>o1Ls`g+ZuBna1&r;gT#!2*2aj@ zklx=sTy~X~q|@}>6STnFC~ExDS&{hksD!eNIs$r^t^v-b^hC^8xhl1sRLz6|Y(-H^ zJJFjE>v4~t#xKH;CxaWZ6ODd>a9Ol=BQ4|{TxQ3)BH&*(+FPUBS^>IqIq9X@3#;?( zmR>YUSz!md30B%nI)=W@przzG^uhI-Zf~{}2~Q_VuIzcpPiwy(eu4YCvD`eFzoGMo zX?wYp!xzE@EKL6uz`Jg{aS!SJSh}dN%J>_FW-_(c`k#DR|Ebr%I`ogwX4?>kdSJ^4UGwK13m~QL3SK$(u`5j zj@dHfzP@T~$jCs2ON%IT64`s%)zF>2;`C(!{#<#zcb>F?b1~k~YWg#63-)mPQ0GPb z-0R-JIxd@}-uw#Wrg3!L1U0n!YoVIoHehauK*TVcao?@P@O`e3uPmb?5}b+_LECj( z_9vgly*l`ihGI9Jx!plk7FVC+0@(=Q|gesK-C+Ui9#VOMK5K+0C(I^k$>b#_P#$BghHsv+0S>T zs3>(=WH+oC_>! zy0Xh=olge&RtSm&wSNF=6)0EuCyzM43Wk8VatWVONy&}Csh7ysiL3S8eQ${1(3!$V ztK4vSb`ueZvH!(Li}g%GIU)f|`fZ$53I$f*PS~W1YW5I@AH_M)Q1lfQL7MjHONIS) zxIR0@9z7AK&v0RH%kCyBV?S)|hftMC5rRVm$TI^y8(K3R-~h9MHYe%c8arF%S!fXMg6IIV{KD5n+Bmzs@IkNflm+BWywo z95G?aDgnsg2Quh{vww8Ns40Pt6Xuo5y)sar{NRRLBnQhyD4K%wv{P$ivoC zLRYJpt{trhuA@vzjy&&?h}*{0FFnA;46q&#_z}rUuSxlOb9ZiKU~X8ArfiszWCMFVY};k%_8YD&d?r%vC~bc+{ku~5!9UTh6c-} z5S{1JIpsy=pHj_HrnD;TLs{8Dz2rsJ$VAIwv0fK_sP%gJ3jHViSh94+;3H+vpZ+MY zu?h=Y80P>)?4!2kncmB;nG|&ja#>t;zVTfCw~%-GuChV^=e2djA@yjPVLP6}hT!-c zLp|1YWy<}(ycd|b1}*~POy1rF5FC=NGcpq=ze(3i8{jtOkT7!Y^6#ZEzC?;qv(AdD zyM4T5vsAmT;ccAKsapKBQz4fen{L$VL%Wf%&-2d8OJg}VU}lp(BhEb-XWU&a|K0Z< zMUdk{!gSP&m{*cygJE)0v}+%vW=>9@G>MMCkVZayY&utXG_&7h%wts*De$LNX}#zE=jal6^r;~ z$vxZh{#V)De7}i$te-F91v~6OH@bP)^B+Lan{x8-Idgr2!gBwj_n^ui?+0V=PZCB+ zRRNYy%r&T7Pc5BWk93$H?DFb8JX@Pr&!E@u0nuN2u{%GPyd3T^Z6hTwaHgNA9vi&2 zen4Zxd)i>2FZFH3s4)d#+DD?^-nKx}=SC5sUWUKS2SD>4yN(C*! z#Fw^OQSW>w&_0%zP;Zk;VGZ`AyXx>2Gsb+h8@(Q;^lQeK-_wuIvtC$^qR4M`uZk;T zg_03l(ZiXo+15HyWm8hIWHrl|S@MzYY|oHRX4Nv&m#ywbLi{w9gpX_c)VELz6ccY- zutjXPe$LuTlpED%Kt}8G1Jj;Tv+q%|@Sr#;z_l#Suky|NdTfpdfIbYA!JU9vHBmY; zr1TRANN^JD)2B|~;kJOCsAcH{*=zE&P71f;MQGO(-}69~CN$Rtyb*hR7|c)WH$^TL zd&jRcbjunpQPI(E#>!yc0jIbz>Jp%0-de)G{0VvR*$h$K2tYWa@<>aX5_cRlceR@qQ-6>QlSPGx`NxeJS3Jwk0cmt-yQkO*TZCoNl8w{Wk#lm8(S4>gf}{=}Kt zE*J#TowmifQVR>qkdd9Z&}<^`Y}2{sy>g1LG#f|{1!==iR&>TV78}2qd?* z?+4u8>nwkrS=a02iVKH08GfOt5JPgK*jzJyM@7*8zuC(5k#3U)_ zrXHQQS7kYeUT-oF1pnrfUR?T~5muJKuL;j#4rjN!ya5db+=k6X* zu|4b>l^Q2(c7G5ZZgA+LV`Kyv%z#y6fJmh+NY z7V%D$%PBz$J?Od2ty2iA_=n~|`P_H!IG=I^l_}Kp*-H$l`3Ik`b##e ze1eTyBURFHWH1y~Q|<7FpTJacXiOe#Cg(T#cGU5?pm5_aW1uEBN*^X$lN zAdFgYY<_V59&eJcME9=mV!hC~B}&I_ENEs>oTS;YJD)&M7Fj=<1%aj;OH+I2RN}^g zMzmL41j!W)(z(&LN@a&lI^-NoM~^hf`f&-GS#Qe0@@ZIt9GRfrSNWHv4e9U|} z$QLoBv8RvtR?2j77BfPy@0g?@+_cO#iv-|>I$fW&rAG87UyF>9TXOYJ8739mWW~J( zXtcCZ`W+t!m_BNUmd-mTnmL7LJVN5?82^%s^wYhIj z+pHi9uCG|WmskRd>2l4ZfWSFBU`bNI2c|%uIejrO?axD?34Sw>+nUgmi`?Wq@iww- z`DL8jfPZz1_;YwOF-&qnd-O55wj$Jdo{UFS#9_Vw8-s{>jXaa+ksJ>^_OH3}ER zIoK8C6mbC&-?+y28gmDB`gwg*`kiP^)ErFWpt|qYup}*&2APqC1uP6G37hvxtn&KY zVr5vw(kdV7iGEYqXgzBl`3@91#w3&9tzbEWBV#Z=^rjdQk%G{6piucy4EfN?4bF%s zdn66b(2?dB0Wa+^D8MsZaa>^;tpqz1P+F2mv$75+V$N2Eqfwr2TPzJyqPZoZE!3DW z9)W#7%u%S4xzY;1w8%NI0`Q1{$I0nfyXU%)-Yg)n3ZNz=uc z^LDJFf@9%tfkJ>YpghNQ2?JEkOO}}8#;S;>G)#l2xyb>oNAAzEIo};c)?^9q$*NT- zg=phzcu{#7=Y_UXDj>>2lOikgx(pW^tqg1Bz3lsmGk7D9%iUnMyOR*#I<`3Sh{hmP zaDbCjipU}X(JL}tY^%90#nnf1Dj_i`S$B{ZWuu8W4 zOP8=xRwB0`N{@-uu(4d^A7HW$6I+cH-KYweT$SXz%LcD0TPX&0B0 z?i=#$YkZ{BL<|BnG12jWL`olXB zR2i}3Lg^7dVU)0|WJL=7va3x?@q-?cy0e+^F;xf#SI|9KR^;EyPz~QVnMIvV?BO9rt{G)eC0SA&632N?DEnlW?SY4<6Xw-o6w9lg~ZyYv3+<{mo?*8oa{b?G2x+0X-pcfTQN1c%s$ zFTy?4<{=eLd(4=pN2%*qNT(iojmhs>RRwYyik+3PaVIhY{_B!SVLCDNYy$F`>J3Rc zysc3BnM^$Ea1ONMS<@wItVpRJl$cqsOlSTclNbRe7HnWH%6i+BJ9Ib4>&~!f_p;K! zAJyg~yjFbOw9dqhy-tBVYkfp*FOYvuudCStI=77nEz=%$*k3AY>rTg_Nz`cI=iWGJ z%@)_TNZZX6wRt*`Jzivne~SJQm(bW?V>ipQwkGevhra?vrF`X=tNdyFHbJ03ABtn5 zr??!N;VoCT&Qeh$d(M`=Jb=K=RrsZ|CrCOu6V)?V{Xu3%NA0|W)P-|#BezQGs79~c z#Y!n3{EL4uzhYkMke)GyvXh8>c&C5`mH-i9b>~bkO(7C-NLEwY1|$?lS8wx6%5Mz` zF_EZ-u(CF(>=1`{Fqz#Klpl`u;5IGiwTus<;+RpeO2`|Yg&fbJv61((chWc8%K3_z z^~;c5WN}tKR!Iw(whx40zTjU0S6dh}`+(??eq1bo8BimxAPLd`qM>U6g70 z_ntj7!Yy;FCaUvqf0ZqFVvlTz!+d$?v_s$Mdb7V>?oU>yN$rbzziHfBd7C2X)Szfg zqde$)fbx0w`>KCmWfvTZexm4 zm7QZGLPcb$mg^1D@AA>EZp-%~2%@k?eFh;Vuf3sJ46gMKrJ$4{rf*J!iz|gfG0^1; z%v+DssLfoKDN&?n205}5fm_V`xwV=O`P@adf3@sYOZuH z<7b+l6lObA3;zHXUzJN-Wh?IO&OxNqB`kpJvT1)-mY|6t(XO>J3b{s*p zn35Z;(t}2*0r2F@14_S$ah^VP279h6J?^Rr-ysj^fDGlxuA9}UsX8n4&^-bPdDShl zgd;{7ekGb@rTZlBeD90>g_hy3PpAA?bpjN8)|yRVZnlky*6*@tC~w`mBa@@n=4oWQ z8;Y3?jWvo{LW{dNdGA_eU9@=FUf1Dg=5xf1!j25u>BjEW9lIFOC zvvtR@MVwqF0)OyMXN|u<_k|wtlo?o&0(U?lu`D4FAu;Cp5&|(RboA^6KygRLHJ6!o3j60r;Pe8kzfVf}0YWC<0NHAi64R?f8NVtDGt4kJYbc#*mr%N_6i&g^H~$K8WrjzSL4P|(4l^B{Iq4I zn__`6fxfBFmRAwbX{YF-P?-z^9^Tnzx6vbgmmSqCd)cS$Fj;CAgZ;5J&ex4XHVCf+ ztwa$5iw>*AH_Mc3EUv8JA$9T)2by~pe29Np!h7y`!_S~_aZ*D0-XOhrttkC5(k)ZZii%ortF7XK(lS%2vh+PwGD& z2ZEE072?}O#ic5v6l@GBVHp?;MZX#NdGCwYH6g~I{An;onvG(<@I?Fk3ev(_%nvO+ zpx^>$z0@9}sT}w*K_HqfdtIh*@qi>~QljJmi1fYyZ_p(h8!|6%StypsQ>NnmUii}V z2u@+;LuLA&M+Elx!6nAPG!+#?cAow}^mm zKdBeKMA@M~3c8(b4DIx22hr61Rp0T}MXr;?$4stBLB|F?-h}res z;E^&+mV`ov0B(2ar(4Qj>4zV#nL3{epfgU>dDgWT%E{eQ79$E)De@EM3I%kPxKqU8 z$7O?dre%+rbAeqm+Oh_q_5lu3s}xqy&f^pn12M1*?A_vKgx=Q$N>+TUW!dRCk`OSQ z&#lJT$*~>`%;!6FTpH>}vpiy_WJm^W#pmBpV1;U;B6lm5FN&l%ZMq8hg)J{QAC7Vx zTOkt(P$?%$ktmcyaraXch=$kcO=-i@n!^=`?v3zNuLzMR6**(2$Od#d+-{QFDe}}O zM^eC*(n4TCh?<5Gvg4{e;aF%`Sk;icu}z8J(*NAStkIVh1bl;fGlxBNbB}6-=9E2o zhGi&KRFg@JYF-U8LgJbkZYs3|ygQ-QbBKZ!+Pvas^StcB#`qf3Jslcvik_0@j?k2B0_UThAkblT>F}8&&Ew` zTk0sE1f|-$nm&94tqH(%&LqmS))^B?6q>)bfwLJ{asdyI#e@fjH)C;2r9I7*r9U34 zrq-C`sl)cyZMna%NpUtr-(I;i#lAikL&U{6Y&3kwF&gKhb!m!NBzBsY6fQPAph9KT z;p0vy-c&YG`OUNjMHfZ;j3lVL8LSLw)brNTxw(QXXxwZCcFXm!eVraR728eRt` zox2H%RVL~l!I)m$)-MM`AT8F$&3{&gNI#FM+f7(&xp2!~zWuW)<@8=ofL?8i_gRA; z>3h6h`};^Qzi;$kTXKp>k0X-9xavfbG!%NI&5FK0f~iID8sEF5GpbAt9|pX-O~mv& zXEpAx=O?7Ep`1HN8j6d3wnDRE-KoBX2y%HtZ+My-3HwUpi$v0K`FppNHpAGpCyuYn z<)z|tc*oz_?XF-HKK{_O13i=8d-35W4OAxmkor8lqNAUF*J3{IOO@8|@;TlYZh1n#0}KeUd%>!F6Wg?t2Ow)1 zK^OB9n-SZ_RvS0CERlBS+-a|n`}g$0BDy>a;FR_6acu~(ebXYW=&K1jmwd5WsplPk z$rFfRDcullBVxT3Z@QpOTj^s6L!w*?2TT-26L4TP7j@%**Lz<`gbJ91Ym$Jsh5nvyOb*yJ*PhU_?t zP86r&?)BSTE6wXc2)ypYV}%+Y1e@;kb9Df?8!1ue3=W z;d~CtAIQ)2kx1!b@nuxr$#a6yj4WrDP$@2}5i7rdefYEld^WwAdo{mRhq-yIL(dwo z7>mBqxYp7ufD>I-yNi) zHW7d6D)IUnrDE)Z$Zn3lw)B{Q72lm)Dt%K!})WsBQNzb7Ncm7Gg9Ru&4fca=g2 z@aP#Bq|1hVSD9)02DRZdp{n?bRi)5%8c7?o)v!g)J)m!q?~PrT-|@xO(E1_0Of`ii zg|(IINvO+^*WS-IB|Dc$sWy=tPrTG{-58RRs)i_|={HIjiIO$3l?TXi*m96Yo81u2 zh2xL`lvpTz$`WiJbix+WkS?drS9?#}ja+DqRYx9lRfxz_#~Ecpo+~eENW~W(B}_Zs z8z*l&8MxHY8c+zcXC3=3w<4~iWV>IhQp`)G@%OcW(Z->~nbLHMmevH0VUxt~APj35 z&&@MYCZ2ERkQruDmiM0tR-}&JJPxh&mC9sCXf(~mO;e-;UTMT1>A1*NR2^)o+VfD; z96>LM+|+}N)6@J-ezd;G_5U7nM*`WB^e9=8bYPY+p7KK$aG~u@$B(ue!|RH6SI5V} z{_C_$w7#eeWrdY&fZ&lSX;PIz)Vpd`&$1Gu#qv_QAz|wDI$MwU27y!)*P&}9odY4s+^(y{V)OFuw&Q35XDc33!1{rid1HRQd63{b@GngOs=?(^_`M-bzK63P9 z{lVh!`OC|J*6uI?e9-&ln8lcd=J3uMM%a0eL_Xgh&e5t?faIjZB%k8KOnqVb)>wnm zjpTFX4sJ_bb?>VX3pKCmfd_idF7h459`MG7Kz*5d5iia{KFW_&J@bP(qpo)`Bp$4zCOibyV`BRNG&=<`Azkk@LMzh67u8lJPlT&~GB zJChT)3cVBX*(;qsDTV{t7p5xBc+s?T6c>9cwGrtz_ zzqgtM1E-d{DV(17j19s)N~fO>{Wxzc8Sho;&^Obc+g~sL1$U-uvKSEAuXd7+bAO|2 zH29u5`GswH9u0FDr26QJ?u!(@eyau;bJj9;uObJF6YWW=ZLjsDqGULB$sV=j`~^h@p* zUEvLtuuL38@@Xe;>GA-9lEGpj{*QlWu<6`TVp1F#S($8eF@yV`uNL&v5oaW|K955u8#|+P zAD!XS-~!xjXpLY+tcNYnD)GF+-T_P4b3Vh&6V5jgM`{G30^)j$_fiO=GsZK$X~oPw zd#QT)-4%eI=)qlH6C`1Ig6Inx2&A1rutQLgExj1aL13Wp6-B0+Fng^bX-xS zGYX9qFHo=I>IcD#-YG8Sb3LXw)W$=05W5vMSgKzVM{$QpPqlR=n?o z(Jeg2L~5o?uU0}^OI%=`$W!Ko2l|Xe7}#s%J1?`gUe;P43N^m ziP~4DrM}JKWB?EIrmCBqX97X%OlNu8!3NkH1!n|#%nk*0%Lg1-5#mu-HXl? zXbl6suat>Sg!p2|29UknMujf@X6kklM=~?*y06(9aM8TzKMYoLN&6d+c}!N6BhHs- z#=LOn)e<(dZ>!wuxbJc;7>U$b$xiL`hNa5uuQshaJ-H^p`#}x1qXjN|2Fz|+T-lnW zxM$Fz&c}t$<$%5&QiFO&Yz1B`Zk!UYxf0NwlDbHhvY*?ZvNuX3wBtPEW$EE>xc8HR zl8p@+czUjO0W^X}s_@}yRES-AKu#!&%{VSJ5ZO601HseD~-moaShIo%C%SjI>kJ^0m;uFwdQWt?*Y{kh<>lZ019 zgWZaddO^DoV6Y**X}Vfne_(;N3id^0yVa^I?H;?hChvP#QMJXAle$!Q!S`&wcZ_1m z=6ek7C68>q&kEunmltceVZKx}4z@|@8i|N7DO`*dUzCwE3cu_1x87UHN(DPnYl4c{+NdP)$5GVfX`v`i zh*8ZyRP)D;yJpXztQRi1(*FP`c1-LV-Rdf#0ITU~JTI>Q1oN=4{+o^I|LHqma3J5f zd%?oz@6>;Jp`-$S4p=AREHgcZ{HM%@^SLV_JEa>TZI#4wQ)w0?Sh)>lzoN33jtc{^ z=rSu^;xYo`T^apO2Z-#8XVtvg%Z$F;bAGk8zT8fc#cZZmDSwp;CUUP+p>`G2i;%n3 z!*3No(q$TsE{^*dsYPazSXVjT0z1&@m(-*aKnej8jE999wYA$c2eNc^ z*d9L^=8`K{DjQ}{<`t1`$=IDF^ZA<@1?eBHHddh?DYEn)5F?vVOFzDwvOHd2vH@tN z6c^j1M${@*vew%JeKp9oH`$5qVM8X>igK*hkbp~9u_lqa%aySP!FFq9?V%%XH%QY< zdl*TxGDfVDxLJXr@Y2hT;31R>CmF~C2kzoy;7CELs6mI7!-Tm?QsZywHA}|2e~!EM zfiMaL{-ly(a>lRKE)=QDrvC#dc^Jl2N{xjoiOjobJaS}^zOxB|0DkrNaertT>h-Ct zlUSnS^s{gDfE`iu@Xke-l87Ut7Nmz*9KTtt>R`NMG?|nhO-z@5K(4$?2|@VSFk*R4 zmJyuu(gb*r!dDYR5n?GE*wf2jh>oX}W|Sf@N_0{{DStePrGPNA zj`81p1E=$3=ueL~^-5MS1M&l{#@5#i3t3-(1_{%KuLl)OkuJozzJ6(!z&a)+8VFt_ zB5Ba5Xv_@ek}+$Bx`>&6#oF4Zi@HGZbm}Q*eu_;mqr~j<;JR8Il*nh5=BpM}OI=!YXj^iO z^Iz+6ZCIU$EalGd!8>#WJO>3~dKLfullPI5yl0DeDQvxDbCzJZ*kMU$k47kyqL^V8 zP{ArWi-TTlDNkGY<*f=9da4%s^Hfl?iVDEUNX0CB3LWZSJ|E#N%nXA9{3$ltTPA>& zquG84ErUtVZf#Yv~9j$7=ZB+@U^C9Fb2Up1*o)9rb;=dg6a?>Ffz2< z?5G(Mw$B*P^@6)0AIu{o1xXnTqKg}m3doml%3IG*b_=YYgvT#Vdm3_$_siPu75bJl zo)Zy8H_gQ91&r-)#GxlaqbC`9mwy+&PY>=($Yq<#_d_tbm1nl{Zg7YG`w9JCD6wS9}r1%vV~t3w^eTJ1_G1x%@?zZzs}Yj)Pi{^X{0g4!__L|HyBd~H$4r|11Ren zqvt$%AwcZuUDpjblD9M|C-$xrk}~Q(R-B8sSD!*BRBa2QS~IFh+BJA_ITex%d5MAJ zsLI$|B3=XHSYixg|BjTiEtaSKEu?Yk{<-x`EB~t76W6S@aCvY2bRX@z_nRWWic{CZ zxTkzhwD>ep&HC5E{d%>|cpAw-sMD~0N4mLyjLi9_CWI(i=Oa5|f9+*E%WqqPd~ zYZB10KLS&0JzF+0(-29M06*WV#uT9T-sDf`F?ejy%EBD%&sxX|7-b)0 zzP+ioHe85|+cV&mnW3;P8V4`LkAKNEK(4Cv61bX)dEMj2N44j*GW}ZK$a$rN7vQ2M8vTLZeKE;+lt;V5q z#uvi&W9+2exS7^Hdy))_J;?eJA*VMaIOe~uBySJd!&)!9cCjqgZ5cA{K6s_8c^3|3 zmydou_lCz`6S257dR@^S9D6s;o>qGgO%2nLq`w0$`|QMX8ZXQ4yP*wj=z9euW(#K2evCts-Do7^Ti|x5Qjmge3QPd0{m9lMk;JW5Bjc_PwOONq4s(WwB_Xf z(_)e4;>4I=*?Y8eT2>0d%xfgKOEkrJ@(Z#`rgFa?UXK|^?eG#!68_y4TX1hjw~(Px zI6s>IL4inVp|<(4hYM)ivija7&C;`4IX$^U7p5}gb&wrugI)Gn#k*Ue+&nN`UE23UjK;?XWZ4=D8i16^ zQQZ&5@rUOvxpQKZy>z`VHsur#6swA??=-1_d)!nMM4j>`!>L6r%g9xKGwRC-T!0iy zSnhPdNvmI@C1!j3W8#bBDxeU=85@}{DrY2RrbFB0* ziiJ_ms?Bb`jjs3yu#Nnjv1=!zQdgi77Pm2X*!P^Q;ZAO;D6JzxrMb=7IP_y$hSYeR zC!p4}3`02X6ZeiE|tkz33pJ=X9$27wW}B?*9PNIHR#wfuqWY zD?k-HP1NfKB!l#sGvtX%3;*vly#MLDy zdH0GwZcpeaWN1t=Gj*Td@1AWRc^AIuX8~(QDgGgS^#dpk=}&%o0KeZ9c~9zKRKd7! z$Wu}RksUIdimCzVlJjSV^5v#+(PUzI^qYR}m1fPL?~p>UOC1+8%`~j^7)>{`{3Zdd zB|cn-oaYn2o#C)wqj{=aX7wUX@-o8d`N)!>R`dA=HLhlHnVwQq2W$dyzvv1RgT!&J z^9dI}FZE>^K&c#1VRA3y?VdNkS@sp?1VRqx{{ea`lsC?v$;UYaX4%{}B>e-Zs1aTW z@lF@<(;(ue722(=$E7~7*95dmKcDyi2Pi}JUlQA!`+`^JFX@S);$3`p=3j6o1sN?1 z=2t?ZH~vV;M(f@{y$FB%zL6awY7o~yfLU@#q&#(GhnygPFov!kBZ1+NOo@&hxy1$H;2CwsppQ9`; z-zx&3TRU`N>iSa~#tFVDc*m~I?!2jRC&oKy&vo#4a_~Tp_%qV5r}U>vCwmLkspe%q zIz4M!OuqPg3}|2bOjIrbKX2&5QTG-t?MqW?Et8v&x^FURhU(-7@TH4=L*6AGs3y{a>&F zvB;i;&9`)AbMApwmycaML%G<}hDXrDMiN~AQgt)AKPXLV`4%qwmrNrib0288G;1Fm z3l245Pa??-A5*t*asqnY@%=02o4?*7$u~5qitnG(EJWW>k!}A2ysfZ2H;;Ymd~a^_ zJn!Ol25K@O3$abkY6$}8E!j>Q+u6gPj9+~tiM;7}46z?Yp{VCQHWH}`L;878?5i8! z%>S}gZ^&a4eS_3nk0XaZFm3fm;Plc>N#|Mor?1NW?r*$Tf+z*3CR9Rh_(RA&oL4!U zzXQB|CR0G^?Jv>a{`m>?`13Ep?mxg5`V^x_-uKR|)L$v;5%hwo47ub*D5 z|KBQfyLu?{7ta2>+W7yp!|3|o&HlSgje&ceuZQaHzp9P@yPD%%H1_*1y@h{(bMx2# z0Oayr-ISb_byiK^H*qf9{@^eFpP%j+vyn){6*7dZw*K4wU!yf4)QY7Q`ehlwgfAxC zBuTWjmx{3&>`Y>Q{GYpc;9Y}uA1;h-PI3qhS*}kCQ332bdyAwZD64xlW-uXQ$aXh; zC(?Y>9pTNFIr-0m(O`N0x9bDR9{?@y z3D~Q&@cF674K-w1)M+T+OCb+S8`?tlqe4rdBi-k}7&7-@rp30S=ib3`|N9I;uDb?W zdG7Xl=^Nta#0N|W(>)>T6567lZ+ia$`2J5b4)pp}_keP>t*YkFjXxhTATsy(s8(vy z3_rgAA}B|_PN+pE@&y-tm^N(yTQw80)6Z+Hx$ozcs8NJ%cKZDdZA<)%{{3DH>n)Fn zvKRE5q-Ztjc~WbmmUVs9j8r&ZXbaie+4+k4x0xp?50?tV z)g_f5YFiS~a1pLrZ1*mYq=4JfaEYAd)jk5U4PB9ijmQz|NhwIqO*v@_Lq}dab}~^Md;Jb%Lxr&;zF)04s(%p3TTm8q&ySchoLR11JWHi=ES35`aaue zXsNAyiIE%3MN0wwYoGDsboSd4lbVOFO2+z_C)Qhr3VvWSO$jZfwY+Qd_jH`oLxR{L z7=egEL>RO}_@LPo22Bv*?=!q;cThyQ-lkXE4A`Wfkyc+{V=;EiGj@iOMj;Y zNm4(==$;C*yz1#{;Sld)gmCx#Y8KevWoP15x8bBIel3U{;}xb?()#=TSfzWHXK-*O zrXdZrN2_ssL*^_C^xU|Y3pZnIa>&C_V14M4O1nvC@k;-cMV5+EeDY;wZH#^pEK2)s zTv?y1C)w1rDuvAV&xRPnkFTGM>{+I!te8;K4(_(2g6U)VnKJi)p_P21 z*PGgHR-lNfi{zg%j!fTku8>4qoL5EYwY;7=#;gn>P76tw@-4B|}Vpu3E6yEv7 z{}O#$cz2y;CLzsT$%EDAQNLB4A_FgTVlk0bpQq;j?DJ3RvziY{d*<(ZzDB=TK$^Y#8ENwMTYHqlR%2T8_ZrBL zd8^LkK4p)Wm>2M%LSb7XvaQ_$^j#?DISJC6R2FxgSNVCGs=9+^I0swe7)wC~+*Y+N zS_CM=V=+Z-8EIyoxnosbg{eTrKdz&UH=#IP5$_?%)M?oKTUQ=`Ai~(J9+yLNG>s2b{sF2fC%=^0?{PzeR!#Eht%|gl=r7jQ2rPSNZ8}*PnLT+x##CCm(gd$=W&MR^^8rBZ z1k8Rve)>HHq^PGD>6d{mWdjKsN~!M zo$cZkUWClSbOy`chlE0>bV^_&)`uKRCC&c;mGRiJ8F~}#nO>TI7oe}(O4Cx*%;F=i zD`)8W@5G;gLOiN+eWlV=f>8I10FBytrO2Y130WBx%f|Wu{yh$gt>jfYhmH>x2Kysh z@DF+@(}DY;Qpd5$@|&{4a^?B*yN`bze2Fyj-Y&rJ44Y%^D4V^mK~4=dqUS9-TgD0s z57HlHHi*n%vj3mkg#T&#@c&nRO9-tdGvq=c^a>!(bSCUaIpUwIl%X9bM&AK@8fQBO zi;BNs|2^nY@jkx(kLH%Vu_3q_Q>Vl-p|C7-C3T#OyX;fHx!LkIRE@FpXq;~m?9H87(K z>*BX7V(O?qM=@sSRy2wosehoH475}Aq=LldIxbo{#`luK)A{krS2}^x*E7KfX!JU? za=^8Ne#x*S?Ro?wa=AX4mpeZ+XZs!!cUj2?rnwPDaE2DM1Lg@b^)c|^!w9-TkT|9j z_LD1QBrP!=q$q8jm!wzlMNg7j^13Q!$M9#eCov8qu(Z3d&-tf~yjQx;Lg{ZI_nzkH z?wmmU3AoaDv@W|BV<~c_$EqQoG8St$^$w9x>NyoX(t~BgB<>8Ic8u)&b}29>W;9Nw zR)v?iHwg#?XH1x3+lc#iQHaF|)aCsYNJ(0a&|Vw)ZnON;BMW13-gu?LB4P+C^E2Dk z4U)#1pwzXt7a+HHCoICbr9!tLPv|FsHruH`ANO_nBtZ>(=j%?y0Z0gOYt`S%o_GtP z-F=IWhT7CbUyTce?;?U=j{mE@uZoJR+170!xCSS<1c#s@1c%`6&@@SKcMAk3kU(&E zx5gnrkl+@ikw!xZPLsynL+;AI|Gn?IW88gS?>=MPhkmG9)oaa~bJkiV-~6Tx3O^AX zuYO1VK|$o!J*z0@clamxbs-_S+z7#sjTf=B-(Cx7mVhVK#J`nU_O7B0H5r(R zFIH4pRuDigD!p|wGG+vVH82QWz^tk4VUEc({$=$24oVHi1rE@`lg77hG!{v_YF}Qo z;Ie!i%~rs)-!4qG?s{W+&4Ak#JG9))kYn{uy({VDNr&lIz9tC{EBA|H)@P2fr47}0 zED?9gjbb8l(XZ#t_noT&oLDw=d}?9FU7$W;!%@m$$V|R__`RHx^N9R2pP`%h=b2uE z?{S9dHol6UOgs}NH5B}=(V&GG}Z&%(*(rOiGR&z)!gF~_0Cb^TV8hD3ID$xeH z%;)9?#i;f-)beSF#pd~hKWxXEm}|Ut`ZlqAQC%saWXkenYwfXV16x^#1h!K=b+zQo zssh@R75i3}i2Jb@+gQGkIS*tAK!j*6pe(gg8R$1`OZDB+7R!(p`BZ46A!EJ!11KzJ zmiT3){+vtdi=Xyrg4gn@E==;D+rgt6K+e$^$sKM-r-wV9|Iap z$H`#P;?w?HjX=cW8nr>*D2C{QiX>t!rV^<)5ou-fP%Sv&PTC+9Hy8Ra!cdgi^mTkT~FA4XmY$WUtf-+u%b3)p|pTxof^v*a?c!Xi&Yf`3Cy7N``Dtg zld0@nI1Hm$M47R8k(*dWsy5r-n)FNm20hy5u{J}kXfb~tJ=@hdf+;ngGevGay&%U~ z&J$JXbrB%(#y@j74V(xU*ux%@(PQXpo!t@;e|z)ydoao}^483*+sTGCfnUvVv#5b9EUvbGS9$?5Rf&zJHl3C^zStaWD!_OX}O1nB-JuZWj%V!?A2^lA<0#6?p)3ubtMlaO@$*N9< z7{7LtPW;F_m`2EI`v^SR$UC>Zg$En^I5IV{@T1eSi%*x?rlK^VOInJMoBt|E+FDd+ zTTHqtV6Aq3zo6ar#NLl;$M#A?K8P4hrc<=NaSxhBrfk2Y_zs-U;X^6!rT&E4*_s0k zPVxWk5#0anSnmI2o$q1n;abx?L##s2(~+QKM6GNs`PG+be`EVz^?ai;U~yHNsHJC` zK0mvz9e<8%-z&fLxcg?FGBGv+GE-zH=98^cU&g=KSJ6*`$PV;;ZyNEGNmM^;7ZTYV z%&Rs;4^A9hhKqiItJ;LXgd*J~*!ohdp%Cm*yjVBWJMDd$me)}mckHQ0;7ZF%FPPVf z5}y4?Puh&`;PrwhNqPshW@@kF-)`z*YQ@#2lLpxF@v6ErBMMFv22XxCq|fME$tdk7v$vxSCjWhdozs28x>n+j@H2=bD7p9EBV@iC}3sf*UMr*wO!v}QC9X+*&CQejqb>(QPfF1Zm!F zl{_;;nvnbTgN>3V`UG9+;jPdE15q6>N`3CNq0+I{+K24gb`GG{q_w06F%G$c$*_SCN zy~<&Z;F;l&nFg>Ub9`}W0E0wKRjS3ei|+UUbj zpIW(0J0otBmKmFN$98M0wGWhK{WNA-1P;Qg&P$qeWl=WKTP9>axYTo(=(0&e5?rP( z^jwK<=QZ-I+_%N{Q=$8@C{&~n;pZ&}MH3?{{G`K&FPOOp>kC^WP297EjqFx4T=z3m zN8>Wgo|Jh1z^iJ4I{B27Q6_nrDPTYQV2M-rS-0=bsLCoZVk&XRN!=}Q6`Pr$$=Gs^ zIf;+Ks8Y1aI1-{Bj)?#!lfD}FJ^sH{=r7$1qx*Xi~YG| zjLVv*Qbcvu3#)V+!fbaeKD0SO&s$ih${Hi*iASEngfy-P9a1%keq^ud4+Yw7_m3AL zK5GSd=p}Wex2}6lwOc%}u8A}nBqid9FujRycjk%qhGq@$b8fi7L?~Plk&CHze7GCO z(1-8*ZM=N1j{W+JX`1wt1`TUgej-ocTV^m4cvkJmo8s-;S;WdwmOWoS21vsf(M-}m zPYmWBrDUTCw*5q&m6OROay*@5T*y3zgs4e!``*Ymo(VsjFScmRwvLf$wAJKd*=t7* zGfE8@v%V5A{kA!~P?%si#*K4B0zf%kb+msvOy_hGwcdE)$hgAP#Js zdW~jI{CTw0J8S;6SqB%{uQot|HrOY!sTd-J8l$iCGlOU4s z2~Fbz^gYHDoooP|2opWnn{`_^XybJvp9uHWo&)jrP@-&vLf;S@2o2t%y4!1GJxF>{ zwbE2Jrp)#$6UgNyp=fINQ8|nuu>Y*e*b6%kr=e~21Z7oUsNLHDLd0P)G$3eSCW+z%O1z26}7#mehv@r$Z% zG&8K)I7s_(${!(<5MAqkVvwjt+Ka!vTUa#MA6b9EcW2 z9tmY3hLHolc=H}5lP$_d>U21D`=%)qkrC}{;S?`;?wBLRGVRdEr}aAC2|xz7;09=M zYZaf9T%Y^!SEQ#hM*Gbewk@HlMV*kOzQlM7*g)uq>nJ|FZ7_`h z7f)}o&+(Io!1p&!@1UpLcZO1r?~+1jdk^{!WyK5zX1}&h7d2$>+n54d*qnVQC58{3 z7>|c93VU1qM)VSo!`oh4czlLd;f|TCK-!-W$EG}o!0>h-)yMHf6P&*Us5&9m8omQU z!S77fm~gpB<>}cgA9*c`N}4g;WJ0B$?$Uh8S)#YEpyyS7h8D$*DNk2(F+PL_IzBYj z*|97CiBF>PNS;b2^VB%!Q)8~4B|Z34*(+){LFX++nrS69B=M259ip9xbS~f~rGo3~+a)OJ2}$rYQ1rtA5br9{ zExc0K2+vJPP?{W|!q|$Ce;ItxLl}ZIR?+aSrdDsIB1I{p?4j06TCP=yM;{G1R0*NR zdHVX1_n<{~RH~3@4Nf+z9zdty8hFi`YyRwdf($F3Wq~6>YgF#t5yzqrXx2mFm<$ma za51WE!qrE3h?9SaP?1){iF(0*CY+<lyZrs?RkPwygm`8`*gjTBvy zH3}gw3k{z|D5t2SQ_*b$+SRVWzuxw#$1~{~oCc6zk3_6r(?54_}G%L)zpFSRVv9&|BK`v9Qf&%8qq;>qtEkmEGH6%o^?KtvhE;HPt%fI(cizou~#O6iTV& z+(+!&jNIs=O?J|~1dndy2$TFWJ9YaF3e4@8>-4y?23@6<>?3ave-WRbSqZ%WHqS~K z{#_XDe`0-YR+lXW*T(mUdDjt&zd=P-wZ#sIphd>nKn5dJ&AU+G0CTEglGXP^@(nyP zQp`i|1@za3CUMOes)zJck#(olr0&z@0EeoN0on2v>4}Qq&Bk(K<*?_U=h`9{*{gaP z%oY8b=nAc{d6m71Iooe%sSa|b$Mbo`aMc+fMXz;-(tDgN27(RoEp#F7MH;8;T0JbQiV)WM)t%O z_hF`KOf-CWfNdgVxR}4VE*~$Z1F5=EO?QUV*S312CswD~A;sdBDMy*lE!4bmUWQ?= z@@X}0E@>34pm;Sqw-Nn4*YN1UxKunfi1$KAbl~IBV zw1zwgb@k!ip=NyErzpeaqlxaCl8uw@8gJosw88^Nt?-0Gr+Kr_7L)opB1Xn%Nb#9p zSB}mXH3p)OM3$oUHmEL2V%HKDxQMV}+arHR8KF^#|!+Vg&mUB26@ylRUfeqQbL zcNQ>ZmJE6uvy&iP-8zEy6ss8N9nP99pVx0Fy9);UKG$}L(3pS3d)uk>qX&(25;Z&S zH^`K6)C-XqLSVR{^r}N6R{lw9dGPfojYSF0;Dl3zWx%v_@$e8>%eI98DJ?{ooE+>b zT85%VYcDSZ!DMoFGzkAtU?# z-Kx1P-j8!c9HFmTL6g?J(|edZ7t0Z{F?H;itvj5LI0Thr;QgPtU&P+&G3`NUt51Nh zVOd1eD?(&0-AS#raSiLW#^G9xkN&i$cZ>C>FQcO4C`fG>Pwrq8jC3-C0QzRo18=TnUB z9uzz;M+U1TE+7^3IIF~9fZnlnk-C$X#yXM9Gw}7Cl`3;powo|twX>9e9|CW?yiQ#7 z_YH~bP>qCq-M=SY61!7_6P-!=+(-J|n0G?1lo_SSguHkr4zK9XdsS{?6L^;XWRw0+ zoPGa0naH-fh&}Pe^pDBDTqaa1HAe;KsUHMEYjtV}2agYD*CmZz&b&5*zRaN4_)Z=< zNB#zhlbn>$VNGG5iWh_48uWyG4yp_q$#&f0chPh7>?!`tJE!=#0(s+|`+g5e#MIJ0 z8YZ-98E1twb%|QXX~*IOG7Bq(kGja z(+J;8Q}?!xqFVOdc4pe^gkYXVzbGZl2ra(Ab9si1+(w9!#p@7vsSb6g?}6HR9%`5& zw{4L!OLkf{u45iZISxD}Ay5qg2g<5;7p-0An{j?=0vP9cX$1aq(zBnoC|1SGSObir zhpL0@C?z5~7bP$SWgrx6x-BU)QIcQ-9?WzNm4PmyorVkulL)0h+>Wxg_fgYj)?U&< z<(bK!sVOSuh(PW5iQ+N9%!q~0O7&LfkS$sZRVavEvhh%4428PzvdB&R;Q$93}aWE2@~q%=_oOdJQRm=shNqJ;k(`1h2uvNQo3X@y<#nluTQ zemb~5pSCH)nqHK;o|P3%lS$?ReR=fX3#h92q+?}oN)uC}T+BWm2))+94p z5bLh!;D#%clJ5w`T=yT9GbD1k3Yak2&W;#O$=pc=as%ng<$}WLdPHg}$xc^g?g|%NcvU|1#GmPE3v?$^E)5QKiLPTfFszNc$@T97ZZjr6##3rl-1 zo>yWgQ?>9W7q(ZsZb^SIkg`MGdLS^w~(?FV@o#d{K!g;RGfhy`jxDZ$X{ z-1UZRmN=%eVN}{%MGvsH`=X4#!XiF-;j^)?-|VvNZwbLUnu)vA*@bbyi^#8nYG72T z_1mKw5G^{&=wQ~wOUw@Mk-5o@sCgaf!F#vOGR}oN&bcCLyD7oEHp9$aar}ahl~lZs z_+%a{52>twvGO$QY^j%Pv2mHygW8JFs>MmnypOqj0-mX#y?+hM({RXqM|qULC0)(s z=o=;;?OFNaQ$wg{t|ZnrywVhDFpoc(CYds!Sg!2Q^FT2d-x+yR?7?8@NOGrdAluq; zG^Ch1u^yQ6B(YccwA11cH|j>=i`d%MF#tcD&}jPcgZiYq;5A&3 zi&S^0!(%Oa$}*I&pxTd87;0L?aClz6`6{)<7wa0|&{c0k6#Clmr`@LDJ}o-4E@_@1 zbb#f-DW5)8&~v%=5s+(V6U|b!Le+N=%X&I?$~fV+DgxysEMZpxrMUYnAC0=a_v}zr zc{%uzcAQF(OxRqK&~!E&S7?WJ>7 z(^qlakX$kAD)oSikY?pbh09uw_x&P7kZWTFu9=(J${^winhEitt^30gE)=A+|8SjP zt)o}z(r$wBr=c%YTJ9*fUEtHvuE=Wq^r&H!7XP>sx6ITo@8gES!%mBgEOGc2?S#gR zxRafACF--(9Y{v8=IV{1h}*_j_=YP>Lr~?A+lp|H_&2QzjY-K%4XW1gLgJUT#}aeyN((>^U}$GUD96+>>^c0 zOj3cRB9!vKBd`2VyJkRFOX~iL(FgaoPDSPVPb%h)EvHxp%sR`sFxk2# zj#$QhP!=WOxVvWjZI+nQsMzZ~zg2&`DyHw&i|SPY_Iy~MCnvc1|W@f1vxcd)>{AU-&FR47qrrpmgL|H7)D+SCo<7v|mfrvcLO^}<; zpfHqwIn9?Aie?E%Aru-jFOEOfnnszKo&$)iieW1@=U0~|M>8%JX|@KBe?wD@=a;$l6b9(W*KR@^yYa!x=c#|2S)?VSt;Jl zAf5il8gj61*xevrI6gF?5Y6Xoj|tt*{R zbR=Nst;4a;mHhI_7wLSH38GIl&)cX^C4+9l*%m>PKV zgr*6z0%2j&cx+MnLOvy=C_?_3f%{F6Tiy7ZDOWZUygOL5ehBn=O}~55BDDSi0lLt} zq_!(>U$73o^R1j zYI&2%i|YH{7GgmQeLmf{x1SBCz@IS>rc}Tx%WGF|HC&Frn*m+DG8;x_?r`(speN8r zd&1lwyY@+WH}W|5%Moad&jioTdWe? z^G_I43Y(7#(qFwCEMI8%D4=}hEz^^X*!6mw=39=>2Bx1M+yQXD>Bj}1`mfKr%X#*5 z(4P8vDEoRkJ^s#WoP7+I?wxymX?8N@W`yh{7Nd{MlxB`w0T9ZLFFSKSIZYI>vY@xUjZ894e5{vY#Oy>qE3&8zIMy7~=L#E-io2@3Z~L3BM7=csX5 z-0B$2a6)vaj#ASLQy}n$u0HI^qiGCLKpmtOxnLfEZ3V+O$EZWio;>=z>)@cx zKNrwehvF{suHyeuVt7VHNc!y=rv&9SMfo|UW`}58~efkDtn@ApGCA! z9lct|2--yX4N3yqcn0;RU4=9aU0}Y}vCq4Q-L(nAJKO0G{srt(R zSa~4Z0~^n85OBLSd8(S-d(uCWfPyx+y8dwdOXA+ie2d#72--aXl>9NI2QE6KayZlz z_kr%ERGsK=Q0*T{o zQy*Yozf|(SiZ*&=F|=kxi&pzf5|D6U5;Kn!n77rw&Z=L=TY8kf^dmua)DGe&?<7SH&>MNB{&KLwmMe*kR#v<=3i|U)Oh7l%G(>1Z+j2Bt zgTgCYHN%$0jRidE{?WD+K)%auBvFSIocINm^Z2ejyXQHM#ZD{<1yx0qdb z_kTJDukD$ji*WoY;TKf;r#$5&c@;cX;z^KqcKt_2r9eD?X>P&`O#1V` z%F?muZMyP)BzKg@*W$MH*P?$KsY(6i8Gyb2^4adyA)$Bc?GyiSJ`|w#FBir9%SHd` z7)k_qk{ax+?Op$ei;AiOzj^?3I{=6UHN76uq$KQ6De%}*E z55VpJ>5M=I+W*x;UXz+wW^z=;;8 zJC11%%l)X`wAJ~z_jlbvC+@)5_+xX4jU|g)pznVT!=Gs5nT#|c$(EO-2(MF-kQP5b zL;n?wJAXf0sedS5D=zdmh)D1^h~e*!--$qpj?8lf&-AJtGiE)8Pco(U$E-Jbu2uX{#WFMKm zRkn)2NYy@h!p>C^w|dzBAf0}XFzvqU&zoY$Sn}bW%xlo(8`{oqe_TPnTapN(?saUV zketnUdjIOy>~VnMD@lK$E1UIwv~`TMO8Yj=Z~F|V{zJEFnx8#2io(tP5*9ndivKNj T)xV>(`v3L+mhKDl_qYEABF})+ diff --git a/tests/assets/multilabel_classification/images/test/Slide6.jpg b/tests/assets/multilabel_classification/images/test/Slide6.jpg deleted file mode 100644 index ac3f4bc73616ee2ce06a2da075e33422352c3433..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75048 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 diff --git a/tests/assets/multilabel_classification/images/train/Slide1.jpg b/tests/assets/multilabel_classification/images/train/Slide1.jpg deleted file mode 100644 index 4963189aa22bc92a97c34e9b26b8b9d048fe1948..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37943 zcmeFZXH-*L_ct1PM}dSUq7;$Pq=Y6#dJm97i$SH60MZl$EFe-0HFN?ZkRFOu1r-%B z^e7NOih2+c1w<4CMJ#VT=l{I#xcAe&_tP17JUgrmviC|dXV$F0IoJL@^LqiX-^Ib% z0l>ut0B~_0fZy`~TYw-xzW_g7w zkb8^|z{8~s;FjRxk>L9M1aOE$Cm+|}2jG7WE^Z!PK7Ii~Az=~DgwFi{ZY~}kZeAWf zK3-nV>`R>M0A2|`NhLjNekoXhfO0HQKMh|esAALF3xbb-QZ)#SyDB6sEh8%@e?;x5 zI#>f@Xk=_+dfe8|-oeqy*#&|0^7irdLj?tggocG0C)k(rf!9amIb zQd(ACQAwyLlE@S)jm~Im@94aJr>pz!kqRYKy}lG zD`8ZTXsl^}U7-3mdKy(wklN@7i7%f0xgR}0!Xfa@Q^dxeba)rlWkFDaPclouIZ4K4qwSwZjAw7yS!}>#Ry2KZ;@sgKhcnsiX%)h_^yI^^0nT^t z3x-J$VsfWm3v`-TJ9g{~hD#94+kKuWoFi{q&{L2;t)a2Giv$(RQqq|>!xgx1odo|*+|0!=&|BIWEhzDV~ujB@tMSMY^n90{?$5`PM?oC z!JJ`HCteKx0Qs5OxQl#nx>IrN)KF=HyiFD;dSr(DmvR0w@-2YQ&!Vl+(E9-ZDT~BsFT_b+H=?t?6bq@n!_JDuG z0Oc)?-{g-V{;Kb{?{EJ;T`*n8-uU?rV3G9V@s>EcDEd9JNa@=II5)iVByVE& z928kV^I{ie=;n}pEv=4YU2LbG9tz@rTnl&Z$8Wwdrc*F?s zffrf=ai3vP+k``oW{EK%RdNKvl9RVy{v=y^!s&~J9^ zt;N+h8Sl!}H<=0_Ebtyfq)jpZP3Ex@aD2;-6LD!`R~2{ft+08WquIV-+$n3~2$OU9 zXc>5`WyjvLB(mX@FL4AU9{Dg7chA7}?bJ;z_-KYK_1^uv z=N|aG;&d_e9Q^)WWS>BfFH;HRQX8jCw6QlHF^mrl-(1YPv-qTQkxXA9 z<^gcLbgj?)x;3i0mJ*ngQ*w=b{-LPPA&GOh_M$k_SJNG++4wE^#Nz9fD?87B1MDr( zmsRba-+fm1z8<$`{;3|P4)`~W#+$$^gXG5Ffakft0URAdwcY0^k8(}>Z@?*o-+=eG zNjnOOjx6qK^~K!hHOXbbad$TfmH@C>`T%UX22+C8Qi9O80S4B}H`7*}tXMMgwu!L2<2O8U@u^e3S9K; znX&VgruV3>tv?3(si60g2n_G1MAZ#3gvZ_9;aK8>)+S} z=E4`tf#?P?rYyO)J_jE_AzI|)(Ue#$#3dn|aipQ(3nb;6I1JE8HG7aUc}*Sz zS9gmHAVEGLVzA9lB_BNbIr7TOR096+vICF(nXu4^IYuc$BqWwiTB`dct^N^1+G$oAgj5g)u<@ zm5=jCvvvvxN_a4NJ@u8rk8j|jcoe~FZorR|sh7i9jsyd%Ap-}j`8WZPWR9>V9mR3c zgH-vek0Z}a=1i3KXTc}n<8d5;GGRkylplMl*TP~H)NA33IjM8j(=`}C{p$cd2k;D$ zBT%}WG(-nXLU3!raF^Te2DyZcROi+ysn1+2h5$aW zSC1E-tL4Vug^ye5>w~W$pCAMo-OB2VHDCZ$ScjYKMAvh&ml2VWutKv3DF#s{^>13L zz{2AXiZ98Ca8U@c6nPtYj35<;Be}}KYC3hmSxur&A-ur|Ix=|ino+h38|r(G>2tZ$ zM>W8lokqL9TrCDu?Ov?9TWpRn_T>5MpmQ`%33G0DsJ%lOmq~lM~K1UXjlIW`I7ly|3#xU_8qC zgJJh(mi9CW$+QTfdue8WF}2IPKs~ntS3ctjPn*d}LA(chh}iezCDe+wy?K+UNtTn% zIHwGu22Fx}&OwG}7d^5;a z^xi|=a^o}%K^pn?PUB|~8JsMWpLSsh0aagts-@|X*zwko%38MzSoCrw!82U#O}lfk zGr?^P5j5Z;L^=D2Qbuu&ObW6Jf=QG4Gqa?9`D+?uJ$VpEUCMIY3Z>jxx`Pb5O_<=Gt|Bw%n$RV=K{ zPmzN10J|w?AyQBlOsI7}PotZ^l3?JJ7~J>-*8o-_`wA%6mtwM;P30hAzz;z<4CQ)Y zH01fE9(4;bpF%>Ne}N?7gQcv|Wo69io%ug;Va%iI=V6~ZIF9+d6Gy5!x!3Aq&r+Dt zuHTnDAk05Y5M~bq{2PY1Bkng~$z%FA;BQYnb<58Z{OLdFor}iBd!6s_lOT-lI^1J* zy47+9>hdcMYhl4{>QxQhj)e|T?Cv6AvWv-|hA+f)87h%#%Zy6k?D8gV`FLpW7drHa zYX6`!QltcPrq=^?Bg{cgiyyly zxRZ&io^X2YbIXwMT%7v$A};7uCi+m1B)ke|s=)tewGlS?XIj9UYR zodTE3A#*N~;mL$br)?Ua39=0z#0f`mkzq|>NG1dqKrMz9v*3Z@;HHUkNZ!Paq2O~R zi{NXpI05mJ%+h4|c#QJl#VV#-AFg=~7Y@x|m{DXm$n zOkQ_wD-VJ8<3)H@+zx;D%8~_1d8vu*uV%+!ZSBnp@rNK;oZp;d_Gx0`8ulwrU(i~} zn8b557OLOgZiPS^3crOX;H>t!+^DC>GK!DXw|I&roDfO<4ag`UaUzH4pOV|dvJK7O zPAmSEP2fa=9EAX{zWJG7u>QqF~Qs~c)b(h>iP9t1G z=2>c)lsvvNlH@e%C6aErS}s|FCmrFhmi2;MoUA!JS1!pus>7#vh7n~9xf%!6@%GWz z*?0z=%Q)HNbO7>!TOQQPH2e#sxc-8W(0znlh^z?%Iaz`)u3l_J58hU#3h^QK5h|1D z$$TSRcRVrIH`(n35vy>fw()uHa5SPM-tDDT@j<$tZ#i(zTFHXmN61N630bbXP2KE$ z2kH0Jn63}ciP8b5jM<;o6cMUMq_YKSBc+jC&J_LrRnKpxG+PA(%0|G2Rg$BRrh{Dk zkY9V!`G8_wyrom>_SsIipFNg%&d`0+PXu7fm?^#I{rNfzjP3{xOlBLS zO?Ih&CvWh>kQ$Pi zwd-R(=kPfv^of?dL`_Fv!;=}FIm(u%_Rg`WM>t!i&JXs%PneT%b{E`J95X|jVILq| zFqEb8xO~+NL_$cD0iUgty@TN13Qz+`6}@VgqFL`YK1~qEJn}r+GQo+f=5g0=0!KeO z2>=6$l6#2s<+LWp#k$g1c)A<@4#I8-lq_F*n!En2m_ZUlQd@}6Y?M)#XCh_oW1qKJ zVnRFc_8_Jv+R!Lyrd1^v33WOIAzcMuACe#>Vy`dFTb1A^DuwE8z#Vn%wx7Cz&Hh=p zTpa85ohZZe;i&8H9ZoC2&+-#h^`3?=BsR2))xDFY$Qvh6^q6{F%=4#3IflIcKMp9) ztSzGDThMR7k^dx%s|!Du{>Cb3^|%f5g?|H1@rQ=S0?=>3;Ekr=fEi0pD)_eplKYg| zabo5FWQMWv&T6sV-p3>H9RH(;uqY{Zsl~M^@6W;(X`bekrVl%e4H;B;rRIJi zckHNs^`x>6HU!!M5h_{8ZxgL^E!Np z@*W{I+b9W-xK`~T|5dIw1NX_d8r0?Hlq8I)Nz;^>oau`($;tz%g7n4P`L<U3SP3TR*HTiUu$3O+)9 zTq$t$1g^6w&CYsdx@Ga+j6?)h&K7cI-Nyn^pWisc+ioFerd-~q$fxDGP4U3u4;Fpec z`ENkfl`n`XYm^hBbn#hzcs*|2t0dMjp{(UeDt3j zHB`U0*u*PGw-NPR(t#IdvmbjE)gr|CJam0?9pQpM8TS1e_DoJtvfnKpUP~<$q$++=xt-Ii`BaZI*mf^)3 zI!{85|G|Ba!(=+GW_>~|4NV&qeL78pdRMG6Gv1ILmX4`=_B)^}7D8S)#D zbWaWhf9{+A39#ff9U`%)g!jE0We@^`sN(7HC*Jm>WAj-@=_@E1nIp=*pi%6d*1M&L zk~^p}@uowv->%#Lhwf&z{sRNmr?cnt#Hghj0Npm3X8{7+0Z|iRubu>JjUg6UInLb}7_7h>WC1 z+9*%H?}E?|x=C@9+rx7QK&SwpgwI~fsFL9#_2P6~G65l{R^2M!VRME9xKK%9F?2t0 z+a1ltYQ6jtUFKnPBnJ$PXMrK3W70>CDig5=?mQ3x6DR@CAZc3prs8fX$6aV3V~@IE zW-9F{L_)PV_}UqEmdliZv`dGfh%NkVpUYcQm6vLmJP9TmATKNGNfMLO?{~mHm}8e( z4>Fn#hF4bEDk(gV{nYo%MX~fBWAa=&o&TOk=7~)h+e4G z{MY#ZE7GwGFXR6M)#3hz>InaV>dHoAU+hMmqB`>-#Na}Dr?8GGA-5wp!EiTuS_nxh z9=Ui##Y>eJQQ6CYL7A@0NI!yuU0;*QZ9XeiQj!UM@?H_7zVho`FP|!4PHg_Ri?Pz+ z{ib{l(Dzh^#{zp{nPt2`cjSE%y)SK!##BRl-B@f(G;%AgmJgt80H+Ii?9=WEPd?qH zbd<^!(Pf8&-}bJ%J8(Df)4dptE+L&UedGiESiXzG9tH%cZ^yQdj(m9rO{J)Oxy()5 zC6pkgJ2cQ4h&}#rCQah!>iDD%AGtE{GWNC3N5bMlG25`mgfgn(GGfy=WK;)Ej1!<> zULg6XYj|D3b4-!kLbl0qz`rJB-JIwdcCw`rM=@>|%eeeOuUKn+~4h~MzN5`xpi<8pVg zvnmAlYCmk$LI1DU&*Y3-W{vS?9mkf9W%FXI%{7UzJ27PPUcQwjib z(wcupbAk9xEKjL^P*dDE)<;?>ZMu9f;q@*)UsJ6*kN)7D-4erShgb0`@s5&2<_yMC#QGn1ND=dovG3Z_S9CtCY2uVEtUEA*nJ-|1moQKBB3RU2pySeI#jjM zNZn)|>ORPxrC3K(Y&&wN_Abd8ZJ_)j_ZB&aLV~;Nk+(zWhCBx3XIhWGTbi1we zVi3u-f|{78c_u7Z@7I4L+t@?2H>v(`dq`op*Lr9L<>|PkaYl3!0m8joey?e>|4u}= znfHUKPMT^!gtUVu8Siej=7qfcNq4}IZE?Ljk9Zau@R#2KFi^fG;BG{mujrV9!v3TN zLD9C4^LK!QvW1O~pcZfAJHx5+^=?>b_~=Txxv?RS73t!Aud)L|FJbv7=k9`=#5MBJ z*8>7xH7W;|aKQoUQ7`=Y+dhIsK6@)Hc$uCg7#_7+m}u5GJf)AfwF`H@O4ejrHS;-z z9EdPZDZ5c@6YQ>w$w)j$#?!NB84gi-Dq*O#2Qq^P7IjKatli-hqn7@{`=5BcRd5|;wY~N5 zi|cgDH+;!X*aQS-pHN-*Z-8+B(BS(^n*-w-{DE;V{ef|38l$B-DM@AXpOoYc2hHm| z+A{ra_!?V;aKMnKcnys^iU@8}0`?v=M3+57#ib(`T|%$H&owAL)ijJd?viZgf3bWd ziznHAx$L+4JtNli)hS66YB>PW>S|;20%5%6*>QiHs>#hMb$P&FMEgWlJM0?PH1auZ zW~5cuT%~Bj=f~dN_n?>E-_x%V3mER*pFCN<@y;L8G$hc3D819w+@(?$cMK(3O&E80 zm9WDwn%QK!-p|~HMuzIYee8lfPXpbknyXps9x+y{CIY$CJ*5(pqbtllJ75~QR@J$Z zqVLALh3}ax=dQ0Xon>|QD&7GL)x55k8h;-@MH09t_;*Anh6M!GWagTg5m3;ucx4E0(wU` ztEqB%(psRx)difc(KDG#qBXx<$PwEL>+#C;y{X?Va;GW0bo6BF_(>>`8sSy>CQtjT zcKiVBYB*^9*+nT-PHfu~ezUX<_<~TyorHCjdpix^<@aY`<=VCMBh7^ipcttL{{DT2 zM-H%$`xS1CO@}^Ih+lb#ai5~ctfWY$rY^+$wLLDf`J|OrvW@;yOB$VlpEP7-7$Goq@a`Gg?? z2O`BaTvbC4BO{j^AdUOfs+Of*(UrEO%4tiNHBMA~r8wW{bWt6)mD^o&-{*B8A(>s8 zy&B<56mn|6KM~DfDQ(vUf2E9-7CIBDDj z$&PGG0!?1jKlC~N+?BuEHcoX@Lgtz09*R1hkb)HabE+fjN$%Tr%fdfp^5uWc9)^E{ zpN)U&l-|ssZ|%SA{=`OqI-%$mnRhJV0ZgA*@41GaIJNT~)^`k*kBGo28dXP>hVR2V z*nQe2`bcTVjxppSg%5KX;b~y$J4CytI$dmkkE}6&)1_mu$l%&hLk$Oee_e$y)0O-#B3Z$hHC{2kNdJ-;u^8`K~DSGk3B!2B{cM??FtB3 zEF3hrSV!=^CJA+`tT!njySFaZwK#b_)woU7OLq@?bxR#oyyz$tsdVVVlr`2gGM=~G zCWVlL;SFu7-CE8%v5;gt+X-6FWa(rFI-PP4os5bxs zlz4WZW1%)+kBXggx)y}}-uV`=h7M}7nvSZa$cFdpsihEAu30~C3U`$MAP4Y1*kD|P zE-0CP;Np z%V=?k`cB*Z?DEq+yGdo4@rp(5T=CK|_mu-Gjii37-4w+6CjVRKtiw{| zJ935xlb74Iq679MnWjG%4CC^Ka+|uRn7opHnPOMpkiA`xq!!99Oh-`~L#hTd;%|3K zelAbEsc4~DIe<7>dhFOQmd4zpCnHC>>2vSpt7Wuf6ZN12O7YW8_OTH^8xHn*27BBU z3qLiYy=C}hqO^Ax35yUn)_mE=H)&-&qF?av6&CN$^F-ALt0_D0pcPw}3NudJQ>iOnr6jk8+3yYeBavyP7Df$Lx%8^8rwwlA0z zyaX>UuVm`kF&w4?!BQ?T3tsD5jWDE5Wl0ky{mL4=j zxh@ii!Y$>QicP1PZZig&)LGIpX4+csV(2IoiHl`&r%4C4W#saVaCS9&kdLc(1?f)C zz;eK%Q_0BTJkJrkg~>QYu=M~cXw#%l`6IGq0=!LatNAjaX=fYEcueRs6jGzwK4O^) zFYP;&oYBvB>Y@bGJAB@9l5G%9S{SROzd8;A-&W0l+f3|-I!RYclkSz`6yO7;4JY|E z^gHcZMX%x39!M+vaGf~ucWdRF@8_HQUWc8K{E~Zx{igr|`-ZQ+9X3(IiG37a|7&8O zS2`x2PTjo(Su5aRukF5<4NN{A%l4pD#d8uyLh(#Q{9Ea2BvcJsaMN|U82uy5Q7>YY zX&M5uUOGH>gj3hb(^IErfe1%TYSDY+g9`m^U7VLCcPzJ+U+hi9$m1E!8X>WMn(-U( zCZ>lHy1%v;c;7qb-Xrqf=^h=c?mDKDCpPuoyC!r0(0r@z;C?aV5vGHV0W>04-jw@o zYRR^BmlrJ7L&jt|OM)(Q&DK!)iAMJQuXMhJZTefyin}TzE@2!Vx~lYbh^CIrID6!FBOVV#mukZNAY&2<4PS_ zZUXWFbTNl;uoy(CmrU#JLrA?)I9uX&EL2^EYD+mS1vh_~%{Sja6@C^oWIjjHvYU zH_;kX$y(OJ!Aohmh%f!#_;gWz+Ih3Y&Jx#Zx`n1wb=->v#;kJ;?$Kx8 z-+<6UCo4JSJH)PttNXuDJHUVVv);hlVv@Xf6ypo3x0};+qNu}dxHk)v&X+Hs zr_=qpu2T71Fob@Tj1HtDVq7%}he?8sMhc-P`q$HxE|-QT6%X#q_ef^l&Lb*|S#xL4 zD8AH4vrpu6(0Ed$wN~^JT=?d)hG}Kv`6I}gZd!1RyGF|GS?LH+^W`A_$xg=`+4NzS z5-ujdFLdkRs?mdi#QI33-*mhHmu0DX#e1Z!|e;#>2>G$sV1d4TU+yKsmCY1`+*ZvVNH(Nr~Z1 zhZ)*sb`3bs@F-Wjc%$h2`djmn(vx%7cF$B_!Sd$`SPuDzNU+Vtd}Z9^-Y#>3kb>-U(g!h5&AO z&!0@>B|2daI@roi>VHLZpGpqLbEKj$K;W+*dI@$aIM1^tnX6 zX*&GpLv>DRh@Jlp5CQJHF7NU4HmAgR46S~bm2gEk?B(ZUPNz?s{RY_em<%An7W*jb z^|%$&tN(^}zCSdy+>he4^vPsD{2=$YvA7c<(*M_{(uJH~z2zsqgK0c(p#9}3h;Vvq z?IQE;crg^%;VTdg39ND@Xz{e;;<`DVPo5ax@T$=Om!q+s%ej*?^^YohFt0G%jFdd} zAosrR7?*g~iFMfxWE)f#;;VCN&+p(0Xn7ATR zcn)#RUB+>`-2wB)P=QRBxA~O+<@JS25nAu2OQhDIRu}te_bofC&TB$ryYJ$-EIWbJ zuU84z&7U6un2EAgtrUcl4^<$JoVX8sGM({F$@Y+;C^29c>w8Fot#B3_ERi|*uE{W) z^1xOC)(Qb$=i#2>WCpQLSzSUi(}sZ&HmDo& za*9Cy>fXXwgdH;VL8djyLcQZfv3WYmRi9 zZM2HY+>O?^m7A{v23R*;%ATr-rX3WLWuYbJ&~i!@KuOWi7P{sEb$?lj*#Xt{w-?8M z1B5^gig^b0tu_(WQ;mjAV$WvMEt=yS`wY09zNh<3qwnai1ts`f#yvoX zKf<-(cVh+LAyQ2n)jm7W6KrgyRm(qCdo|cip`K!nk>o%n_4(QRWphpyW_?X+z+`lJ zt$Wp7Y?&WwxGE_#8gNxp1~fMHz$@-St8V_WlIMCmw<@++e$yKqB)#3RR9@yl!-HIi zxf`;YX?>6I(h(u}QhKqpoD6+uq>+8`PaWO;IN@lW<_nYe*mn0_ zwJOz8=~bY0Tadwb_QiMGv>Z79@{Q}=#)L0P&tRds6D{Zx)@l;**;huhP$WAhyajz< z$CP8Qc)v0L5-go9`;6|$k(zB_QQ-wcu6XBrkPn(ga<8g(vS=fSc>9dF(-!Zy(37TA z9{1ijn5KA1(ycYva_;yUPN$e&59VS&lhX(2Tqh}qs#@+ehE&!aR`9LqNab|sRWe$H zeJ3`s0g%ufz9I8|@|Ga2-Cm`}V&9XGU)R1Ww)aJ!Qm!Uqqgq-VyEQ zkPNj_d2?L79?z*#^YN#l)k%~xl4b!>x&EaKRV{T{l_f_RcU0dUvf_oOT3~fAP(uqp zo&dx*)~OEXM$YsraQZ(_0oG+O5iO6X33rk>~NOei$V({+j3dT za-lfJS7_|V2=c;E>i0I!Pi>{W1W=(traH1fM24^N;50+QY$ySmIlst8fUyvtwWHvzc@g{jr)j1LPkVi49sCI%BxI{MbnPL``qBk;}MJ(GWV!(|mA zS+vjomRgUccglq%u(IRn!myq&f3EwyrrQk^x1xP#OPv$IR}g*MM2XB;4nXA^F@~Q{<0$23j0l zzk`2p#>^)qY4NAp96%`+a{Y&Lx3}9x9xCu|mB~7Ezy)P3=;dF8Z~; zkasPMr_=wg-V^e3x#np9yHeJv@u>0il1|glhCk;Ow$~~@y7J&_I+aIS0U(sYCLVrx zvB$)*WHZFVaWLL9o=}z>+GL0RDjVMqj*E3ap{+;A>Y8k{S5hmkK1Y=%p66MXjkhu% zLlICpxHnslmL*JViJ>O9t&nBO*&S*zA%GfS!TYjCUDJ7N40YogNeP8nygFIQk`~Fv zbZj}}w3P0%X-rv03tD}-1U}Is#%b5;nDg#>#A&L;h$+-+1Lw}00ZKeEV$y1lmy3#f zOvcZ&R5q_I%>D-C+^XkvMP2zRaU>@5-mUc0n_gc$_M$lvw`%?4@bCRNb%rX(*8~0y zL-TK^{_TI9dP73XeNHJ!`OyFN`l*in6-3}7fW%VqBtb|Mp;vuN*mBfFkmWe8?1}d# z>o8jWMi=GhM|C$XN1UFAh6Se6M}$AtMa-aj0%y~G7}D`b%mfbB+a)Wo?afST10|es z7vPvhNlZp|tb>MQPP8pdipAY>jtDtVc}L(igM9F8&D?iY4BXsa@wJh!DPg44W8D;wze_vap%rPO)~Q1Y8bFPf zNf$v!dKI3d0~=#JQfjiu7o@cv`b{UUXx&VHdHa(>LS*CD%+ovZX#T9ly?LR)jzG7Q zEzpFG#niKlPEV0<)VdpNmcOhTze&yNR|-@{*j0wdTG2DF)qdHeYx_D0ZFjR0#f_nsUmZ(eC!D=LK#a^ zy@I898k9K5IyO7dq*;cq4x&`7?q-1wIHuW;yd7h-5(*u#Augps^@~oVJ^^5#cW90% z)XVg#MlrB3HF&XP0N!|5=f@^^P-GkU(Z(KL!pA(dXM_c7e+uvV;Kfwa7$~&Bm>0O{ zNGlP6J2oAoghWs}u!1V#>}C^KR1T@6D2W}>J_1k&c9B>*s{lQy@{Q?SI9Ek2yLAH! zd1T9RwzqaQz90R^+4_k5&+Gp@E8hXp+_p>P+(OsOtyC4tMO-k|hCO>UJBH*>j|mC$KtZ z5>MiN_H%A_td`Bq!LL*v z%7)F6vagU9N!?LJu`5Ulns_ecnpZJq?`1>Wu#Ci6t;7U|^v+{UnfG?nyVMqLO8`hJ z5mG1S*emDz)hyoo_2d45MAWmQD>`Dim$76c`cglCvNw!8$|74L5>C!EEv{|(Xf~h? zM>EoBADibAyv2Gc%?Hx`^=gUqrA0lldmv&l_DzpVek{v;Zs8;CW)=^qzTEc6frg@= ztEV*~_AGQB7t;ueTa%uU`@p#^~a(?UK&qWOJ!7QPKbQ8R!@IopTl&V*JGz#7>$%U3aAq1wRZTy}V^Ksr;7 z5}G^4v*m>0S}qymmjC!b?8=bu_=)w0zp{(E5dP14AMR?(|BpI*s$B~CSE;W|zsSR0 z`S0IXp7Z<$D0N18d*Dnzm9^j8{e1I3RmvfWl@DgfyfaVEH9g*+>}e#ww38LqZraZt zTz4}Pn7A93Cv*_ZE0_XX=b5eM+#Vo|?CWpXwHrZ{9$G;(!ahGDt(M9=9w!70yrer+ z#121qRsg@(K%C9j)wtAp>dD&!ZacK6mFxZ|{cyQ+qT}o7Gu4}zbRJv_;moh~XmZ7z zhBcP&7*_E~ZxkeQuak>?sexkj1|)G?jyRXqs!L=$BqJZr5-l|2%mUyC0`3-IkJ>%{ z_4Ezd?t3g~;%9YWeMF3y;^e6(%dGT6uPFyjeghKjo(5ac7kMo*6elgNv+OBL3Qb@( zf3lC8c1b>%G8tNMR`F>+Z*tf{%|oZ#E<_mv91UL$U`-k~fO7*sbA7q+KJw3{@(L_) zu(#^U6hEAW?7Syl0lpew|k|^X82ld-YBs^p5H!{2AfV^9OS9h*4#q zuDQ3_3zMT)5@Z#{d?&t?O=-V9WBGoutzFB&;(lr>l{*K=95`L-8)r5dVtkHjGm}<& zcc+dM6A~tmok7k{*WX-@#GNY^&bjQ};bfZa!B6s$W!k(`!yDk?V>wDK_b`C@Y2wph zmRnzpEUh&*NVr;R%~^H74dQ2`qdU+gJp?I2_{RwPOKRaa}Bf-?1If*-th)-wv z?XNyZF&6-Kk9E7Sbb&c0w6n01Nj+|;9w8uI2YbJP5FUC;eK(J{bk>Y5T4jGyulgKg z^JLiyRL{3bwW^E1NMa2^CWhhd3QNua7~L2theyIb6#%S~6n=tuLG@@Iu~JIaVdI{m zUz&j0t)>R?`pP5SDrC#xkU&DqSqrceH76AAuC9BBYRo=d{acuB4brk@}K&Jo*&n zxaTpTJX2qnEZONsFO|>S{EJ2>PWl)=6GD|g4Oynng4V@z|Kf4pY5dxJ0ez>*oHG;$ zE$YUb7KHi3_d??T!bYPkCtr11a!!sN7!~h$c007?!NdSA<_2xYdR=Vk{`@p)@6?;> zX}}Ho<%zzYKkpv9)Slnb-}UkpXXfN3dQbSDnNtw8Y(Llt-uGu|Rm~)9c6dUbUOMCf+z$OgD60hvs*l_PlwyVxEP@%v^7;xE8-(TFP`>c}$lE?4PaHc-Zw4s=lHr=iqZ}HwEc>rEy z_E2o#;O&wd#>Ob(qL`3Q$w*Y{&-eKr?T={Ux*FlzqPdEgRrq!0VB73X9p9mWTL+qT2ec8fPs9PAQE!4YJ zTs$6r>!o4nG}jTCm|cI31BSS6V60?MGW+>g?abvO>0NtExCkz%bl4OvcO0$~`i5-$ ztzYAOfxvlJJSix)Kte`o&+Glm;DC-G5HNN(uh^TIrJVsPc^mJ`ZfO%u1;!)lfwxUh z82b5wR7TD>ey#dSPJz2DuVR$%=eq^VUkh|{`AT(P;PHGktLioLwn6g)lqV-4&*|wK z=J}mMzC7m*^uX-(@RJ9{&P5o_6ebJX<}+{4Ue%i*zNkIN$cb^wdPO?sTu`DKV!>y_ z{RZ!T6!NN6bu%}-gaDI#LpFo(upm0HG*AjsDkC6OXE`zWjC)+c%2r&YU}1d>PhiPy2Mx(Ybz*#Eh^c~CaED%XF^UvQ zR2RdGU!BG1at}XPA{-pX9>gPMfIO39(@G zF4q8Hqk&F>r( zv|hE@>16Kn_)kUqZ%Uox!1Df_t}C_&%&_juPaq5>`>)w$75J_1mtEeKU%k)&+~Wn? zdNF{5D!4!7fP#Mtk(~PhoViA1Bo=SH6Gk*9Qa&cub7GA5HPd{* zy*C|L?aBKB#&j%pCGeX_xXV)D!DGCcsrFVM;!LTg%DRrIL=pX%q{bfX8;!%Kwxg;W z`P!dqYN*LbL2eDAv}`L6R)=zBZlGe)o=>;c*nF@$;c_oR&rA3Pp~-C{LFS?n7_t}X zuT`<(9cF)FGkMZm>BomKPIaRrYLt=0GCE96gQ|uJ5D41oozC?Vug@e3=M{k+W{*!T zuC>+T18o#;0o6|W_?Z55mINt@1wC<}5W`^e+tII&d+Bw)+MHCx8s(z zyDdut6^?tY^;lYoa6PJvnu&N=tF&%Vyy zbDw?f>o~1iJUV9_lBD#$*vN^Ln|o=gR{WZ!h2JQ1dNS2Czxe$;&CHR}>Zy%f*TJP_ zty4V08STqV!dth8I-SQF%k@=2EcY<>y)~_nX@VI{iLhXumCdRmb z+RN@{C*AS)N=!&0)kaH3N?57&FIE$C6!)U?26XiM&Ls}bM}+wpqVoKpd1W%_yVQ)t zUp*Q>?4Vu|1y}g`+BmOhc_Z^do3DIAXWWUCK$!&VrgsQxWt?d4sLk@bRI-@>u#h;p zo271ONC+~G+ymfqT`MjtaRNY6fHMNjd+cUSAYf^x$CYDP{_4?^%hx>N*ZNZfH$i_> zePHZP!%%B0RogErsjmhIjQq!F8Ya8GsViAF%yqHfmggzJ_;f}?aZrE>v zkOh7-Ff=%$eR@aO)}@`LC6A%`ZfsaXEvX89P+w7)#L$%9ai3h&n2AWb!6vmY!(iAH zE{fH^dTBPKMK7qPvNXs=xmW)XltjJOU|;NS)A6)ZLQTgJdOUJ>XD4#E&bB>;3MFS2 z$WVsMyfQNvUq*6g z1rA&mbyrk0{o{`?3xg+eiUrBbkgSZv7Sc4=eY3O6g&<}8*4p7h0-LRBA=9^%8sJBL z@VocB((Ssk;Ti&IwDO*l#i_fpsHLx2z^2p9hxw0d94rp2`cAElOD$raRBnl>u-|8PSS>c^uWwwa*Ju+lQzWDBcPqu z5#EqJ{~NH6lX6}Ofy_=fa^K7j;jaT~Q{4MUon!^I;ZU9H*>f8K?sFj4IP|JQY=f|} zN#pr;<%h+L0{Bn-*0Bf7Gtz|xS=tdk%7~Z zZJV?63MN?|?rbdX#4y>i-Ex8E)e%)`JQqmZLo zY{`t#?-^yVW5C|rg-si)vh`zdf}F%ch@-hlx$X#oJ@if?fe-^(3AFzea`$BygWMz? zzQlW|PX3Heizp%Do@W0T7(a?ca_O2A!x3!$KT-1IU9*p##~G;!9hwpQf3(p&{dHZR zDr?YK49NUz{-_JXBL3NUjDI)Yn`-L*hbNprqHlX!F>$og$+ifDgApW&tB9)$tjQdD z6M0n`FQ`Mti|yB5KBf*rGge`VcxcJe^gva|0K{k=hEfGnUB^fPSJ=#q)-5=8r8-7J=qrvItxBN|-q(_rs4?%{Xp46ZL|8HBV11A9Ih| zlVveyhUw6sL+y35V?I~yrt4}#B;eHS{I2z``hZUENED(L{*ez-9m>Jj-sF{7&*#+Q4xPkO z0&F&wz7&wzx3#f8b>X2MkgjS08O_mKJk82JY3V{~sd>zIyk@@rx`?u*DogtnW%HiS zl3Cjt5q~(|D6F$DVBkO^_mQV;^_A9+=vDQr#+SoahB{l-j0I+Ufe&PXlwUj@?Kfu*7|JPSeEYIVYfXW`JkG+m4$> z`fAwK8TU$6?xh}mJj$EGxC6c!vBm<8TNm4s{5+NNBG00tOK3^Ic|t60#8y|$c2d){ zBP#&>UbPhg&C-x?^WTPx>MiS?Ms4PdR@k>4HNSggsxYj!NQ2kk6liEsN#9<!WCNMPCF!R+)=u!`318}HKF2~?pWGo zW0J|z5?I_&LZoYUln6KCx(=+*^8gbGMMO<-67Qfj4S0!D+Nyg+WF!&2y|^cmFoK^lby$(1ioMKUhg%U85r>~;)a?_vR!Pk$QYv)P&NLqNo6N& zDPK``VHCOSm;a=)TdfMk=#Qo20lxvo-hVVhf%AKVI1?fGOWh(~ihd=ZO(XXKSL5b*QQO0?iN)z;x0E{#Ehw@RQ2B`m^8Z zJtRoA_2Tid%J6cJJ=lFu&#SSq3GlNf2V#1LBt-7F)8a4U1O%507}B`krDYY4o<=&- ztebn68$j2y>C20!*hBTt>Bo3vi=DshbWC28fg?AIE@J_r%#o6O@DB(l?m0=&z3nk# zf0}a_a*V)7DID~~|46|JG23M*Mx|Jt9m32d`-RjaJz1X&S9^VY(C#ktv7)lhzS{EamB6yP9nSF(+0Y?ti3n|K#bcHit=g1X1B+#gfW$S?CH z&;yI+PzVew*QRATMI1#>p1M5?aO&ACPC1Zf4Bjj!AhJ3`yDaa#X1W-YUkRA};yvi) z-S7ECJ9*k>>(C|BZv9c+J4gtNak|~PiJW^L!z%~vcNM(uSrnxqVc>+@d=`1v$Hl++w>O+NAstZd0}61zCE zp!k+4&VuLm>f26eVgqh$g3YO0SYerzoNV6{2LI&|-a4{dbH@G<0>r^tyWx0BFeh#A zk35zEOZTf~9*#F3H0%FvrRXpYTa79YC7YmtcD^#!6$719nF2x6V(;cnSaq=3Utmr7JAR zNUji$0EG_EQO*ht8h@5ucjq&(Oew z2u!O=pU>mwL`^7r1aW0t!9u44JZLi+^6V%z60k>QY@RW?hRvq)Dj-s{pKGVgCfQO% z3w?!-k9SwuY=ooFs+=*RTVfr9fn!0R2;-G$B)lHqT;3+Y+t4QqAH9zY%)$+)zJI*L z$oA~l4rUL`G#hJp1;+kc)Xf}&$KL#4>?7Srt`uFzVBC(9jLb1X{NTY&&R-^8)T?cF zzlSY~j#QU5v)TGt3e;_5&b|T56?=ow(#Q(3;=DKi+CqIDq|DS*2xL-hB^Xtz~jMDYdHpj z!UD6ow#B()0-7d?n5RooE0K3ywoTQpXWKGicvVz9<|`v(Sbo_GT6)jLoc8anqy=SF zZ44!a9)pY9J`kQEX>n@hbl;P$nAtus?Q9XDcpgdwXA2yxGTI0=^A@$(aL2ZdQF@3C zXFcbc&lnBy@5keGF}RDM3XQ~SBn?E=wk57_m3+HTuc-pOM9FodUfQpI$>1mzlc-u0 z%Cg!BygzFlta_BvvZtF>08m7Snm0T=e5~AeH=(-3B*=NbHRK*k$XRt`r4*T#U`p1A z;6kGQ&8e-Z2hsd`^v7ENHtPaZc^2#qj@wu-Y60tu_6NnG0i8#0ws|Exrhtm~`|mq= ze@;hk*Ju6)i(YmVNf-Jw`%{AgOd1}M(J zm?5E@zCbJC;$`G;FlTMR1U0WvDAD4akkTCXDa=w>5k2~`+M9m3kc^M525P*rI8}4I zGA-`coeX`COTPUZ@T6S3Vo|cZu~=qHQfSd203=qzT6%wd2?$fnyD^F^4RasoX!^HF`;=#&svu)G)d&0v| z!4X?ZLWm67?u9@e+AMe&lP=Alh_BB?2Qa2)oybZGnAYnR^2x+qj6AMFYAhyVZwvaN z+jeVJ6WBrLsPrQv4Ze|vwqpI{w)FIRJk3VgFJR}4uw5jhRV_Bqy2Bfp7M5=@nx>Nd z7V401ju$JW>b*h)Ksvd~nx;cmRVF08*y2SjP+Xf-0b3H2Nk9&{)QrjP+1R<9IOOu+ zb2|nAtTQ@#z7Srp5NO)bKOK#*wuezH7T>X+eOx&L{_LS2Z#XJbxx}@4l9~<2XR+sx zm90z?RUiuUiaC~>ALiV9|=e1xXPJ@fyOsXo)1b>-ecf|N@3Ex=cFL#D9HDO zw0fpliV->LMGf{;3-$(G@iu0esv|w$Y=<$qjj30B1RRm<$_FxIPM`w&cuFB$bl~khhn6K92l_AmkR$ zOiGO3RV_TD2%0H5NF-`&cn@?*A>~xFa^#XJ#K<_i2NkXEQr_;`;Ir zhtQImKXdg!|Fp?(0(GD44chx1l-Gx~s(w`{7Ug4M}=Tw~PjD-<5;O zstfXsEXtpxM^*K8kwMFibNm&i@AUqvkm#?If98G~-bh}n0St1yvu>J5C?c*IJv*5K zxrknad&Z~_D$G$6t8bV$A>ypcPE{OLB-|MgA`c;kI)%FJclOQhvhq4BimZsrRMrPF zq&*N*+GR3ULiVi-(-z*32{|ynI@DS%?H#oXA$cXUTSVHI&i!G!5;R^RX|G2w-AXW> zL<*^Q(cDgf*7|KgRLw1ptk$_8KC~g6MLM$9cAvB56XJh0zYWaZfmS_@iqkW>`jz63 zW~z~$5?^Vg1s<5lA0}a*BG4d;7r&WM$LT|}ZCan1^;K+Xe=ozyRycv^Wc-+O5=c0SlYAMwurglbK?KB_uh9;+BID1B z^AlMdA)9N32Il~^@s|Wk%3Ses5a|Q0pw@$az2vZNA0JqlapjO}^)Y8(a?){SJsb@h` zBe>vhlJZM0w1LM`=k}?!joMR$bewo6Z6%wt%HaY}ivw36lBz*~eepm|^VFM#&~PNN z7&aOCA%E)0TlZUw-UI8(=CSq+EygEgL)&V>6SNj8BdjfUv$cEK>z(195jL;7%UNvJ z(;pP@lx$n3c0u6mdJGPHFE~ozlNF;6kH>iL?d&}^ZxPdL040G`SX!*})!WBC5tR*@9XD(Q$^;el}guh>gS zP~3q*n8j%;+h(n<8&iGw9sx!!N4?=Dm-#n9+#+HD2LC3A)`vQExRP!2omDMHIZ*|D zQ;9FQ3&OI=W#Uiab3X#QY%36H(~BwTIp)S>_}i;ilfW7XTnAABA@DRK3OvXILjiVK zrOY@T9&05#)0mQHdS)OQBlTcOK5eQxXW(VAUP=fjKsYpzEf`1GqgFsyAa?eLz_YX*F&C?12X8*9+j!>1AyJ zBGs!Z*hweF$b446jNY65NHKj;x0SxeO=NR6mY>4;x_P(i5zj8>60W2uHTgQABQj9|kgB0VqbM+!L{DFfP(`EsQODWcmWm7E~r)D05Gc1x9yGz|r_7_4= zjI0Ly-_c)`pH5pk6;wH=g5SeX{rUSJe$aRNj4m)Epp3u8gS~8L~6oNoBHx9zPxY@#+<~+|@OP zb4t%^3!i^6duFWR?)b0yjDE3Yctxpj?|&KL?N-k)68`CQeaipK2%p~@uzE#vXq-5{ zyUyr-S%sU*aij?5j2T=!E3;p7o#RM~90H}VN6*ftN2cYRBZ(gg|N5Zs3O)%c_b-!^ zJx(5wSjhF+n=flkc4;%)M|4J%UEx{VJ$v-RF+-Lbe}GoJ(O^V>3D8R~Nv~hwCsQl{ zYA3O5QIFSktt(TVl{PgvQJXXb6O(8dJ?EQE%H*hyuOu@9B54G!c($v;Q6ye;Hwfqp zFg1$z=95i;nhxEw(kww(@(H4&{>ncqa2~ZW?E3XowoI(;O2fueDFBHuHnLA~ZwttJ zIQm(9jPITp42F2;3v|yPi#Dm%ASS6NgXW}@X0geeW*){&zZSWKJsy{y(cjObP{9ZNCPn@{Oi zs2Md69>3`_e;Ed!n4xtah%^O68G}H|0HPcx2r@~q(mCGB&x78n#fKapaq%^S5POf7 zoxR-|Fj?-HZ_BO=)cy^yjtCt9zq>4-qL)y)Bl80`wOAflrR(Yq}yws8ZF=| zmFA-;TW1_EL_w`nEm=DZN%12Kot3d`k!Kz~5`;lsPbG^V2I%_Jq!-H13zgJrKlNnz z4WKd~3yyXn(5sFo*L0a7|HR;{sFU0dcxNB4Z zc4<{1sVS)M5|iGwK4VsA zQZuzseqtvtFiIfrNo+Fxwe~UgR6Aw;pro$xlU4)`Nx-(K<+qAa9S|?)Q zaQJ}K`#>QE6ORziJclb_hBlGhvM_Q}}rg!=({E4-_fdWr1EABB2#z{He zsou0I)q6Yb(Wv>6`ffacl+B+lrQhmPfNU+$1p#88kR-^H2up)Gb;|{vED0XU78fL7 za#jvPWU>_swe%L_4{7fiy#tfyP$_s&1eELSDk(cMU1$b2c z<`Lg=n*>CykMLoRVx>S9uzt`3+QoiBiHH}|y)AF6+H#-yR%a6_z?b|ozTe|jBPgbL zdoDpQC30)$qH_BrTg1$zl9)Uelooyi<}$tvBIEOP;1rp_p(WNQX%fTVX257l{G0b zNxzZ}lz(5H#mp$SB9EgOpOddmrpnd7K8uzE&dOml_&g@<_d5oN+J4NfA2J%hc6I6(1gu8);4LLx|E3!Fon+qk)jh!MN)9T_K2Zj=LhXK{M^dw)P&o z##N*bA8RvcJw=nZK9JhLG-w@K-q%q{aQV=FH_(rJ=qgHS)|6fBVrtY_A#R-mNDOzmB6#P~D zG$=TWuSZaj|BPFS`240M*(W@vCU=Q|rG+GM_{u;-(S=pad)stNPZOQ&6o`CJ>sJt> zl*J?hzY;VA^$!ycdcds)^+z;%oWE!FuR*bJNg|>40Rb$rsh;qPCoSClX=g?TDr`Sz z;U7SeKDoEthzBZ48?<*n@Wa}Hm|H0F=+iGK$mD0ZqChLl4TuKv#kpKj&}Q^@WH2{I z3xCO0!YH3y%cgv|}x_RnT6Uha_zyQ{-(UdQ%JQhe&+b<&4eXF1Ido7+i+Tpm5rJ zB);N+EKS?oXnOMub?P$+sd0_U9dS34qG(=j;@JP3{jPG_7XXkL+@Z=Q0UtftfuOp< zcFtg5-b~mQEj`S3#doJ#+YC6em!ZdE9G_FHt`s=Fj*0|>Qtn^b1)>#QTg}+5a10qO z*uJbIaCvfw-gEk%$DwfBo;y+CkWL~BdRO;I;Ih?zn!>a4^=iDcEi^{9)E6-ZHKDVY zhyZ+|Jo3+cvr)Ab_Zaa9Obt$T?-Od%S3O}&)by|&HZI{mz2L5XlNCAu^S2dS`I;No z<1|Avcz^082tn;^Ef(9IS!&PK52(;~VVXXc3$@4;O7;sMJuBsolg-CMTZ#nG3%e1$ zTir!0xjO=QxSMaaxdxd@tw_FgI z3OYL5OmpY}&a0lEv$_GN6+j!VH#x(DX$*j7ZtiK-J42T!i-h1KI6gtG)ROhlkeV1n zjj!`K2rx;erQ zw2xU>gW{Q<2igcSR#ROkdd)0{*xNfB;h9rIuzCS=x}8Z;zbYq-{Pngi1{ihb}ZXA-Ofn?^`0WrHZ$+f3o?&)CHza*5%CDHcw{ zX_@$f=wBH|&FV-Xb74ytM+<3W+r(NqzYlqKq{Or4KmvZ*S^Ksn!s`Ab3#=B+FYhjb z1JufiU3@w1DG}klPO>ewnbDqocND*yF6%TcQ#a$&y6T!m(&mzOqqt%;@yFvZT+i6* zX7xH%MKp)XE=O$R8uB&TuSAD(y7bY>_NPKpx;CrIw;tH!I=BMy&Rg8I$u(tEVo=gf zd0dOlw16yh%Pa1yU|)}T4VvUeF@MWEc9^$+cBP(!!Hyk~!!TRy`u(KmicmXIm}_Q^ zGl+#(Ll6rz!`Jidi~X$0w$f&BxjDXgOXG)ffqSERy1m7?$uZVmk1=O8)bww`pU1Uu zCjK7Q@JmoKK3>pNse8SC@#B_f|4u0DpBnNZeIKybQ(a6t`^CR0oo%2%`8s`BA5#Q7 zZ`w5%&a!HQKUT4XN(#&Xu*;UJv0ZMyW4DQ?`oK0kM~Z{p-w145Z2aNjS? z?4gS+fwE1Q3R%Flu>(?|nnW~ig_t2YojaNkHm5B(7q_xU^La4V5AQDap^Deihutcj z(&U{;lxL7yQ4Rt|P{j2TN5YEOTdbIkB5ao2zx_#6w4SGhHI`|*@n~vcZn-`1WO(ef zP!YAlPc;A+N1#Sd62ti88()=Ud$lx5d|<(=C{l<7sTV}-ub< zpDb6Wj4T@Rb#aQ(>UwabO<32T8ldOJk3*MYe*FQhOZ-4$SuY=BkA})BkxXg>E4?Of z!RDWc(y^cl_72nSl$|t4-;IW@2GYG$@RiioJI+c}y5Sqko)tqqlf|Y?zUx$*c3&E) zjqUY(S0s9+f~nl9<^}zErLfQ%iPu%NhI3N~tSPyM@7+();iExRLqQz$q~-MbD@{sP ziS(i)5UxL}+~LWrE+YS!z9dFXwI%6pX^)4ko$a_z$Mvgnft8CrcdcNdai z{Gy9UXHiHG7@79X(LBq5E3o&-)KTU|E?MAG<%?D^FCh`|0~hj>){*J*l9g;1=!`!` zn3XEZr$}^A5ykD;=yMu3qo&n9(!AyCQJFXR2GvNrM-=K>=p z6Df+P`npIx+_(1^aHC-#)uxtWdR&M!^oMm#Gef8Z>ZYwiO&y%qrWL#`eAVvpiwSM$ zx8|>u=tn6Nxd&LVg`v&(jMmSPwfXqMq7!TT7Kz+5)Iyu`GYV}>0$9^kr9vuRFb6xP zigG1YLe-OW-HYs#>lV{TjLw%WiVx=}o3?8$F(kfPvi;RPDy;Ux+*X%H^7i$Yv zKp7lx)D_;|i;O*q7b!}+kaSh0DTW0aHP!^56%$BA;cMeaEW1zb2=AtjCvc|hF=0v- zz0yS%egkT87Z4ZH>HT|GS~@8_jEQbaSxz1!WYc~g&3-p|BW@ChY;aD_nAErmU7Yu3 zI?%*v!JbSqzVp$k3Ewz7MH4kX37wNl*j^$uNc&CI6^h?MARtYdsDZSTudA^zu&3Br zj1zy&KwrDNJQdmxmKDsH&H#p|WCB)S8dWMnzoXya;vXF zj9TsO;Z1j);o`SZJL!Zg=G$IO12El1h`Aj(b>P7T`R$?>KiQ+; z#>TtgM615uOP&>G&!vIGAH@-aBd4J^(U?2g%tnssyu#@E+E~vwFui$qoJOOt=}Gn$v>ND zS3VKnx?NHJRDQkZ-^*Zfu%lbX71Uoz4j1l57wjlRo8QIs~%1x85E8&I{uNN?_ zDQ_pjSpB~%X;u9B^(SqxRX=DHx$<4T5&S?)@^I`J$A9pm9rW-TX5T}=GJ=x+cZtK^P?Rvat(g7tRMn0)a%dLmK$H^7JWrZI<; zEVz(a06z3cB-d`P9b4A?VfTaaPXNOG&G3ljzl(57|0~O3GhF1`1=+hRsuWE#4~@qP z8iJpWWV0@!mgkWOXGf=pnV}zBbdL$f`@c~K$p2k@Q}V+fW%fUEegi<5S5UH$Hc^~l ze7^AE?ho5hU!8(oX`#v~Z??b(y>va?QIJ$L5Tjp?L4@HCqkyj%1yDjuo_7}!7$a0| zrz?C{@KK#IbHmS8$*`9aYdPdu$Jt7ywg34W;P&T@&2EZ@ql>7&^1S=ATJ9NK7^}U% ztK8YIp-1Ver=7z8@#96<(ytse@ZuN2pCKbQR9S!0|2X^mt9-bxEENbb!pDl^lVvv) zmPdy$8;#Z#!`Ez#JSNyUVnyR-)1S))IAEpt8HXd+v!ECDh<+& zVRx6ZM!wY}WxWQqPelCFisAQVTjO%3R%d_61n<~Ss1OuM5(J6>gwUe(+p07QWCY!*Q_MqyROj4zyPgZea zp#yWhhfZ>Y+d@u|g4V~s>&in&cjUBkvK-Bt)5(U93KWCW%Gy1CZI`i*3m4l%KV-XQ zJ^T&0UAENfY0Ji9I9TBP@fQ7$p=lwX0{axMIOWX=Z9)whI(Edgce}f93HqmE?+1g= zf6-x(Zj>7NJ$B$4#47!I;T>j2X%o(-2KLu6hxbr~H}^R5P}bRWKYa@j%@p~rN%!mB zU#mGN;gtDUhieS|22==RJSHd(rPaP=q?O}Z;73racdEiUYVT}W#~OJe4qxLPRDAo_ z%M$cRn)q)T{DUvkZO6x)frInAYmoRfqQuovuYSrF!_W(vx7eD=Ip4{??_UT+9O>lU z;l5E~f~@KDLor^|I#*8AAl+N1K&?>jg7P4J&>Yjuk& z=PaFYN&b)fERdx^z5i7x?R8oyUn&GX?46BNXnL?zgco#gCpp~h*C&jr4!@KYVfDrA z_2pu`#h)=~K(gl1UxK@c+18}LURKV5Oag8^m)3ufZf6ux`4F@&V3@fryvkTnUKn1i z_CUn~2LHB6;(H036?~$duS0RIcFJEvbJjTqEm=UzsXsBzk22?f;MQn{YY+T0%|dPR zCCs8dP-`7!loq=;Z0+yG9W!+byFeL@E-^K7v^;1hqTNZ z>P@6O%ptG6U##=Mw~MI0GQU^|jo8*hx?P>LckU=C%KN@rq&e1U0&}ONEegr#$70eBRryG?q=T56^-66@Bgh#78i};R))F zA@x52%aWf?PQmqyxoxcIBW7r+W*%|edeCbqYttH8#0JlAz!1h`=_tGPpv=SdVZQ-0 zFaFFc>xGmAtxvx&nX@;>!ygKH5`39)hs&Qle_=$AR&G*9R@>>7E>2Us3k#~szUsmi4H9{)#s4*Ww7jK} j|IOvUHTVw$|6$-i4E%?I|1j_$2L8jq|6dqT{=N7=L7S9~ diff --git a/tests/assets/multilabel_classification/images/train/Slide10.jpg b/tests/assets/multilabel_classification/images/train/Slide10.jpg deleted file mode 100644 index c64bd55f666c83e02dfa9adab1ac69a7128ed427..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57545 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

          1s=i3ZI&Aj7`C;6yr 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�cwHlP`{C0{iz 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 diff --git a/tests/assets/hlabel_classification/images/val/20.jpg b/tests/assets/hlabel_classification/images/val/20.jpg deleted file mode 100644 index 7764a1776264b1b10a19fcf7e5f1b3a1afa84fd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157413 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ǁnm00mo(ufEza zI0s-gVEk$r*-rpKi`W9f+P(W&0kFtp&_UPqN zy(SE+Y#~}2H;+M_&Bvfd?Z;;);Y&Gy;yeNd!98xTI?TG2G6Vkb`0^y#5-`iO?aBWw z9x0f}$Cor8R%jgw0Kx%`$E+8LpXf9^$MoA3#K}PNtM~p`sO_xDr&QSvjG(s+==yk+zu!W~JU`B0b-!>nXYn^ze6GAO?ke@zD z*GpdV+XKrr*5*TJX&==mqcRJ2pS%0M;r2tc7V!(0JV5 z3YP;weGNv)*bkQRO^iik?s~x^ejulghOh4ucopkd0Biz$j@^dvM8`Y8V}BjonL9H0 zW#@H+H9=zAeg{wdni51;Ze;|5eSfkZgN`2kI!nU}=t?{7$=Ni~mCH~j!F#sI7j&txDHQ+#3d_@W*MIkLySgDf3jy~kd^5eMbyTD|2596I+fRrAz}knCqc%TzBldRz z>_lgGvQExLxW#4E@YkN5RC89Zaq5vjuBAZkhmZH>nwml}fBY^~QU;jX;(=n*w3J{-k2OlX$oGgx&dX3~5ltBC2 zOtg}`z`X%bchdUXS7pWjG#>d?t(Uu_r-9=IG~)3avm$`Y}lO*J>hnjSSh==YAv7T!b^g6jn6h;W<=0@wdsOu?0aw zG%t4Z9@PNo2KaYCEiH9SxbBmz$$S!7NUmG11;k?wAoGOLqe{l3R=_PL0&X#z>qM1O zvcZJ3;M;<186EKShZSFYIpRZ+vc!b;UG-UM0hRP zHkkJia9b(Si_E<%dKu+Yt4$?*ZHCi+UrrFrdo{H}dgu={1FYn6d-W@*U0rawau@3t zilZ7pZ6kLr45}p2-Cl>~@`rXC%_r2iWK?Pec&s}A5hsdjowbiT{CUl9P=_C@QM|@j-TKkFP6JSWZ0GI(V_Cm~* z7x2)(B2d;omJ@Jc63j3?wHsAqSA9b737+JBe9Fz@{`DZ@1Ob)Gg53dry2a|>89%dj zajU=X*TssD*@~_k(H?}R9l&ot`j7!|JycrIa#tUDzv7qFk^!}EI&plztUaMD_(X0T z=pj?eC5^uJUyg{7wi;|d*gYtD3@ZEuuRs(GFewn~=-0rDkJxklZ84b*$cY6&fKew7 zHZxT)Z!anY>VEcH#08Zd_wN9H{6f}*qw}8iKjjE0Nh_U>qIx}tPr>&_52s(35OHcbTUc4&MxWa^}|jvx$W^k1_K1_ z2Ga+EHOjAzX`5&N&=j53k;dGd7$CS40Tku^Z@vQx)!z!lUn0!66C!>-IvC_%g=ZEm9xPAG!(?mYR}fIno^3f*kMhy@Ldt0U8oGLX zVoqgUFg1PZyidf&lF`p{&)%dZT7|TX!m)+!hZ{Wi<&0hHOxJMm7a5b^JL$Awz@4pf z!(ULyLcv+)#KMq&?ge5wd-(o6__K2q9l-!DEFs9GSTdaF8x7n!%5rt;_by0oT-u_# z>M+rN+sM7!VgZ)#GdwG}%Opkb)ZCFqK$y%VBkMdv4~a)XxU9>KEP-dABvd2*yVn1l zi&R+FGQjPw>K*NHso6y!D47JK%rD=b5kr2z!v=F4uZJ@%3n zOf@u$EN|9~H_-}TJfxT!F^Gu1iZ{upiXG|Kkgp=k^P_qhnY7&2Eg<^KGFv#^HYJ}z ztJ~$Yx&kOva#VOZ-Kcdbbu%$mSwrC-U;HrgVKRJOgH)s8KWngV0(c}6L*rSvoAIi)P;lv0T8qvVf%GXjs_dtw{CX5fL1mH!+^#}NP)bU@WY^ zn2s`z77P;Ka^fFEaPuT;h?k3F%!`@KvUxBXq7G8$XpCNAvS=Z$qniZ+oKhT{_IhvX z^QIQ`NR8nfu3g-DMqrt+Y7rPh9hNU_pY(Coh10U}&K%fWi2*0}K@8cCV21l=H{RKR zdvoV`#h7f)Sl}*}-1-2O9ugg(e6C?T1dY+Z=>m&uor*x0GmPjKNZyaTa!@Wpuh_B) zZNQC2;eB(Eb`!vb-Wyt68o0sG0RK-orW3Av50rJUZ@x7OvX33kSv8eLg1 zA-&6pDvFfpitAU65w*5A`Uc}5{OA(2Hh9)3X%ul?varr3J^7x5=jZS^yq&}2k0P4= z?9{a2e8=+}YuXdoi^vPf7XFO_cjWP&&mOqw?A>9KqP|Edh+0$OG6Xxb(}zCmpOJV~ z^6DCeRHMxp?`eINYwXy`k`=R78J#x!4mYvSYM5vuXH8MNWBh{d$jF1ujL)jjcF8Jm zPSj@JviV151X`()T>YLgHwjAJ935{hKpB}r`QQN=^x7NB_*dwQhA!IT-}q;7T5iw1 z3x<9?e=cTNmCU8~#9eB(*d|fTBkNu5SgO5T=e04El*R?0t1(DziBW_DwZLG^sf4SF z0mhHtqXIhseNy0RW?X`rA24@K@>{RlqmnjJ0ZTvO^e`;9%$i0i zoKy1(x;AFbAW~$qNU|1nC)g37Gl)1bfIL~;oEPWk4uB;&B zR%0;*?i*2-KraR`d@VU8kUj7W&QlTh1$=+2r^(*QEVKQw3ONxqIq~W;@6!F?Nxp+7 z2Yu%FMjKw1CCbn&`$1}`Zl`dMUB%aH9=hx5q{gnuSlyZ!_arK^KSPG|W( z5o^m6o_hG$kZM0 zVk*zTLgDuLWfAXf6_^DntaZAL2wlKaig_-$Uw|5P{r8Ry3& zvx4R;vR(Nf(VZw-|@x7bz-eBRq& zWf)Xk*Zln#1s@S~jY~zH?G0j+3>otI5iIfy;&DA{H17|jbhEZy2!3g*%{8U*10T#{ zc}ap5^rSVV@j5yfAMpHN^oT9VpBww&YDYKl0r{_BC-uW*vE4sTWJ`zBwuC4pmUvqe zi^ekLJnGVMiNlZudNy$K2jGTo z287#(XNXx(qbZ429gjLqjGnc_g;UPkix+%$la4j)bSrXFe=S&0qcK$Q$#KKI@THh? z9A0SOMSqxCNbU61JCoT7B=$zHFIeB}b%RZ-BVs?(!E)IVQR1pIoW$%<>-^HN0U1$B zN7~r3k~LL5`WY9reT#^8j{&NJ#xQKr|0zm&rXE||De{~@{COtYm{2f8PofEJ6zB2I zZ6^l)EJsxJRW*3_Q_uMJXW^nfGKZqSnV`Q15X*&Dx+oys_s-cPhBV34{6jO`CB7)J zp!DByzPA~9eknvH&qc(x?8~%Qj-OrB_}dvFc&oz}wuEPQK>3HR67G^@r`xk~)zCIK z-QH_U_#gXjNS*t0%wJd@N=;$<(9_SPuroY!`Yb)N3%qGFO9qi+zJ%z}8J~U~{tKwf z40qj(5xKt6iuP{^HXZ6>#u`VHv*qZUAtt$FyvQUY2vp}4JX@E{{oX?0D^Ncd2xH5o zXHt!is$nLRq=d5x2@4n$^5vp!v*}6L#hn^&Lay8tCz05{3A=?=(4)F&9gU$8wxuGG z8F=Tp02|H;DKTtiRAE}Hqo3jf-_h_VV!p6$y?A5AmvHdI$F)0r3&XRM8zOjFy+K=? zdWZ#8n}u<(EZ3T`gsl&D<}-HXikI9|$#umwE<_^oTE{p=X|JI zdfHb58P~AGs(7(<#fUL~1IS{^b|iHg8y2YQwg+LCt#=BBzEayhe|!@;Aol1rv#WIh z->9JU&<-C23mWBfco#SOiZk2F7#jKo%4LY`s0l6trz8m_3TH7zb2h4p8xs^}u%RKd z;4uBivpKxt&t(T5>Wn(Dui!^mMGTAUyH)$1Ih-mKFXS|G9)Ytwd)ycD2>0dU13DAu zEJ50MZRLDc+e=%o=2o1_1&t@KS(iEAosrhyF)JHbGVKb43q?VDnELx9)$T~oRwxUVbjhlGjWgb1A5JR|KZ6n7u1Di}rUR*SQf=+lS7$sWU=I|)dkOKw$>r`tXIp{)kd!OXGD z&vFc7gj_*Ou(=ur8KPxE;t$wtZI^ehn{q~D;}>2`!-v|d#WX6ACY9N@PKocd9qE;= zuaC#kLJTzpV5n<$<_Egwc)^u|-Fk>CJ5-RJ<)&)R!N5>8GOqzkA7vQB6UQ-|#fkAT z52C&M$Q{)@G9LT&*=oeYx-0&^<7V}QNa__zQY{Cuc0rwB1hxV!V4P%S;hH8?t+36~*Az2#L zh{~^t`LVqWFOc)&4>$@eEx624><=N4En-Nf7vEa!`DYmmMz|clorXl*9LFu(6zhxIhhY`N1`zbyDC92FN3ISHe-3?QVw{Svz=W;P@IfvSm!d2Ir zwCjdu>p4SBD3$Yg)Jf$v7~kF&AQd6csRBbcFhLzWuk2lZ=qb5uAW*Y8FE0*I+_El8 zt^=EoiyFXvZ5bvj@Iob_=-#UtTQ8Kt1>ZaurH5bPd)5rDzW6NmhdKJYDmvHg>I0~) z`WNa4OCS^)xnN;^eg>g+VL{R5b(;HCPc;Qa zr`PoCV{Me5qApsRbKul%h{Jrf6=8NpOr4Tgjp zQ{wFo{P|y6Ly6|MnbJ8W!j#e+Ji1U`p=W2yxf6EaT_|2Rrc4R#__bFaxt}Cv86i5E zT>Y_N$i=IezVqgjFDbyfyw`4^Ms4VM(dyB+490gK%3eQuwl#kE+LvAXeYj zQvK|i7MGqwdnHXVqgav^brb!hXMli`L>;R(R^7X|7!h>B zVX_x+Vcp5rB7tk4LUaX|f%mfpGVa~i4<7l2u?m3zFwyg|K=qog4 zexdH?U2_EFTfe`G_|dqRwwn8WZV=c<>&_@6kGQRP|HiFIm7}=p+cyto{HD?leNdra z<$aZRfbDVj-pHHXVmCH*BG!Po*3#%0^whM&FHpZY@qLy$Qqb?GME{Yu-&FH~?fUnt zha+c5SdtkJ?GEpo7`(^LKhQCu7%Z4Q{1#@wy4cK?t^b6TThguH*OuS4 zJ#XnJol}!?lR59v9uCrL)V=+(qMcExxg)fyyI(e6_~_$En&=I6B_JBl$d)T-c}_Rq zCVujaI*{!9OG;V(74tZeIc#EkF6b?@#*%FpToj27Zq(;Ih07SIiY20XuT5*_>#^78 za4;n@Q-<7|o4uy^@toa#XfraW3&rCpyTq!eENaKaxTF(8frpl1bf}p6M5N7CzmWMb z6$$lQ;MKN0Ir7^b2*W(eMcWl$iRBItixo92A*G~!S!iB8LsfE3UNf3i!5wVVw49#t zCfUbD)Sz`Uva#XwVBJ+jo`f*!{2VcW`%E8SNn{4Do0py#pWm{Fzfh(_9jXSu6@4c^ zX%CmZz<|$8$Mv*EE7WBBcf#F%ww2JT=7-NdOPI+USHQfI(G8DHER?LI7i1>fWA5*9 z(`|I!Jck-z5r~CYx?`zABl{H|Xm5^X;bE$;E+jI&2usdnuM3&1vpI=Zxk%Yo++5GLjB4Lq0`~cf(f0`^&M1g{P;i? ziseS0+wui4X>+Hen&UE@b#NbnORvGf?vn{a&%I!>)x&&n*@$Se^Tb3$wy?z_eQ7rc z=s}JQw@$;yFfv!W4f-ra{*V0?dWIU}ZR*ZuS`-k|(|QBg(fkPUEe9BMo<=39CFQ-M z!EB@@0bMS1{_|bbXK(S3RoFM|5bNI9SCLnzAaBw=2S}ugz@jZi*%q8{ z-4jA&M1Ws}d($=cgRNlZ83y5Ex`Gzy8jI%wV;4yZ45vXUn8DhIkzYi7=3MIWy}k4b z&fj7J;8BT^*_5BB3HfY3y-b--jrF03nM`Uo``tf=F;TZ-|ll z>vFth`Mic8YjdavhbE;Yb&lR^)0{xR`4-do&~);_U33?^Cukbs#WDK`t)*H@ZRr8V z&IshH7A}f4KBRDs;!>{46@OW4gF)ZWhuk%9SdyB8jA<}&p)#lNN}iuux%8;KUy5jJ zHn$7dXWzPP9(z4r{H;0#W6tLnbnuywSF^EMmmt?{0H-MwoAvc6B!(_=7f+=%RuxIS zSLW&5mFc8`f^sZVKt4aIbAJ_Jv}(Lk$-JR(BZa16-nU;=%&=FCb<6dJ(R&LXSB*=t z{UtgXktl?mkXxJ5Erlzm-b4|+vf{#YBcpx3$Yj}Fv|!(QqbiFkB=W@GYbtS*=1>ug zmMmzGJ|MpR?9vI9;iN#LEJyT`l17+G>(2Q+7eP>2!D;3P_CN-w+nz4J{ z-3bk3)Z36`cJ?*3^X5LVF7n0H^HmG*4n62A_XSL4<*AIIf}3O5*{g9`DF2t)k?W{S zkOB|Is6oa+%E>pLSqSkVQSpnaNmh+Ip@c;{#!E*%P)?I#u&H>H3);5o4Q9E^Ucx)6 z6jO370GuWhfGKmYKd(AOk&5#7rut?Bwn`R4W9an=@Ju7{B<*;)gHne8+ci(Hb+1Gf zHi~*#JfH%CS4(PWCKg{4KBXvy6)y7E>ontJ1Q$8MN@h!0pZXB=HM_5}6YkI%id|B- z6w

        1s=i3ZI&Aj7`C;6yr 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�cwHlP`{C0{iz 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 diff --git a/tests/assets/hlabel_classification/images/train/20.jpg b/tests/assets/hlabel_classification/images/train/20.jpg deleted file mode 100644 index 7764a1776264b1b10a19fcf7e5f1b3a1afa84fd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157413 zcmeFa2UJsAw>BI^Kq;a^6p$vp2ucY8f`}M;CkYThq(dmu5l|5kkVx+Y1nDFYigZK~ zl-?mky3(5^Yt8n|x#yZYIUYRz20E{* zsG^K!3bQPLZA_Bd0iXmXZqSP<|eCiiDK( z)M-*OveT!5-cNw@pwkz~E?yOpC#Tafqu_9%7xj-yKf`&qq#mr>yUHbI?)v;J<)zCE zj7;1-ynOru;t+|Ol2XzN_n?YO$||aQFgQZrzz}I+X=QC=Ylm`k_we+3}bsAZX7K>4F&_m_P{>d39yHM&yjlqWyd z3VqN-u(!UT{yo*pUe)EoQKXhl9WORn?i_>Ukom$f($h_C-`rWwo*d#?WvC@V7e3NNsRDpwJ7KlSWr^4&c8Aj|MYOTRx2w z{&$mR4mO!m(b^K{=koAa@t0S)86#5z^4R8<;Ff*7#)(75=K|8@=9aYQJKgzVuf*oD zR+SjipDjaAem?a8TlX8q)9XOlDfu#<{oVfN!T%&3;0<4&yk0G60a#c>%Rt_iw|>Yk z11ne*7*G*P66RkCtJ<>nn)r0JS&ES@TWxx1?D^fMW6)>SRqw54NOvz%;K#CH%=a&f z4y{@DhSsMorGB(D>OB#j5qVa0w6L&8Pe)MBmpECKEf*5j$>tBbB`Sc&$^rH@lSj{kz9`+c0c2y=w^R=wBuKUB)cfnUgQ^S(x|9fKa7OG9D}+JuEWdd0T^u|zif4fcOI*#GTd|33~g3JpHFcQIY; zOrADuM+!9R#0IN7v`@P>kb;v494sq8Q^W3F~}&Lq4SISO>Co|ou?mn&}tpMx6sRS z;|g6K2-lFv$=aK7Y5HXv&!7L*?j#QW`gumY#UM4N)XLf^Yws-wn|JUg-U~x)Jms8X zDG;u;w}UrROEP)K!F}r%CyV>f9PI|WfWsA@YeUQDiq$V9wILKwllou9sKR0A;-er}VsK&~N+ylsAM=9V0@ zf7LMvDI03~iG4fFDU_O-G$5eKPB6!SL;0ROMZDaLjf2IorpT}knzCb%M>V|~Ems*C zT(-kUK;=O^`G^2mt}C;EsbV%AO0P}wcl(nY>TGdI4Te3V3@X)Y>Pf8=u>xEJ$+52Ob7Y^1un`O0R>G2 z^9F9SEQy{8-N;fKh%i+h{*Z_?{I1Cm_EM||u?a7_+0&KzlP}vluSHmEXu5%~Nr^pm zvv_f(>S7avOWUen_l#i~%-3*yhmFh8LUl(cXmYSAs$x><8U;8R3KmU{c_$#EgOG>P z!$JZ75Bj_P&4Yi14hU?=-3Xt~WWE-_7V&TiW+yKofBL!9X}F?s;UsFYDZYrDg%|YT zAb07=FV0$D;R^R*MCXA5HHC$NXj89hZ`EGxH)337k?2sA#)3_c6}^rDhKye4&R^M> zkTQdDle|HUjIzyphy0i_BjFb*6U~c~#~{jF`Y`Hz4z?sssFN|rSA!WTE+?&`+|9z5 zzMG>ynf$(QtNnH5sYX>fLoL%PZ(9EA zq|b!yzL?si**|Iiw$ZacaH!hR_1u7vbUF0gWQm}K+Pm?Lv7_~f*txt-{DO*#t8yEv zJBeWveyG^eFl#{YR?1^*Km}UyRlO6t*RTJVI}<@YmFjwl?^-gsTWoXVZx?jX(lsff zDFOOtUE8MHWZD`Jbw}D0y0s;w<_;IT3xgOh9(Am+C=wawpX8sLB(Hp(>9G2U%Y(Jt zBYiu*1LOMh8XC=4pmFN24*36`@Bcsfs@RKJ=ao4lq1^N&b@m=C->-$<*!@(Lw_T9m z&RhO?y*I7qYY*W@$M$*%rzGdrHvV%oiz4B<-1z-P`)mk8!Z-0<{g{K>ChUtux4m%l zG+WJBn1b=L`1j73Dj>vt_OI~a|Isl@E!-V#P9HW+)HRh~$3sW2P7Pa<3i#E^3R0Gf zzw690YPn`l-@zX2y$U9BIa3<6-4a6pfJjn9g&f|zmkF!+R;BUbbY(1`MC27*0_R?E6;s0qcv=VAs zwNfFqFJP3&H0&8x-K-Fvkx}Sb@V&DV5P0NY8D#%G*I#>6^M9S+YGj||SC}L?37&FD zG}>+iMq~U{DL}V^Jp#9*XLwn{b-1!a^u@Ou^C(GcU+;TSDHrzGP@_SURJ-W&qDmdX z7ODDWwr zewdHchqy0TzCKEFl^Hw#G){3x!`4gfvPihgnBV@uoj;;ghauEm^Ae4~r~dCmj^cV5 zaj)WilO(I`=`Nv(@B7L7g{tPT@`r-F*;bO0w?wD7xq+N3nCveD9b3D~47wTBX0xAJ zn|g+ua79hwd0mx!F&o{tVzkoS?Gd#U2yhq@W3|vy85bn?ENw=U@I8p;vs(9>|G|=7 zqlVk(E}_QtQXVv4p_}&qlr6Bo>G&_ZolC0PtjquNT@bgg)N};i=v|3gF<03054Tt6 z$J35#8uC58){V~R3SIWt+()e9Ff*x5u0fR{hP|1t;n@Zr zKSsxg6=XsNrOnLRDzy&8dhW0eBpbzFp8V$R`ao4~Z81AG&!q_PUqppy=C{viWJbN}Ts5 zdeOzxOvKmvEYcVF_%HnspR3%hqqy(XZ-~n?KnBmx|Gpp*u&3=$918A>QT^N(fGKP*&~(M(ZeMljI!3Vo0CEx zY?>LwzKLUhF3|!%r%XYsN zYnxv;bouMXr(gfT(%mIw{j3-|2Kg85_RU7PdS}!m5fE*U^pG(tLpDdlxx0jb`kK83 zvtv+H<}qjzJ4Q9EP;(;>Ph9n^eeWQ*F79^#*N^$+Q)c?KpKZHWt|@VEYP2n*=on;S zI>%-+cen*C4vnl#K8L5-LkN8E#tL9A?3dOQJGl^vndV-Tt7TGkx+9R*jVwFkJI?L~R()0ud` zWt?A_p()93+|f#M+Ruk<#~`rYvfVLA^k?gKwAF_UEt8C#w*Gx-YM8H1hA1)%uNmHa&pD z((3oOD)*OY>b8ey*HKT2EIq!;UN`3Ri?U1=oqHY}gFe(vX&iOb9)nVUtJveUmD{#E z0y_F~Z;SlqB2IJ-Gwd%WaGW1bQQ|MA`a7_>J5m}Jpze^5wXYWy*1_XLnH-?8aMvJLt~ z9Q^#PJ86S8!GC^Uxy+}dUsqDpd{F#L3+?3IUay-AKDu2ub*B&wgHL3BO^x(B(YT59 zMc(YXlM`x5!s&o^ZXyy_xAT0E$DnB_pq$RD;{`rQzv*B0r%UA-*bOGMWmM=l)bzW* z1xx^YBtE+uUX>|i;eQM=0i=%3UdH}bvR~>m%MAt$=uB71pH>vHt61*$r}YRF`>N0m zf2t9+{ok#s=1;3a^=p}JK(|Ea@pNw%)rh|>Da(Xb$HM<%+Ab5o37V{PE=Hxd(F9iJ z0WCGtgO_zvTBcM>LVewG+kg>=Ta8LhjkaV|&<2*L6T6Q=9J#xFv{XyH*=Z*LlIt?Y zNRRk(z=51#TblxON$AT#=En-^d9^Y_>0D1H^p&B?ZBB z6Gj5Sv7>+En8F*B{mC*puJ87`UsiMkXC4M({kkhpc&uf5ZV|uZ=ynWp1n@$t(Wzm+ zJ)=S?&?9HzkLhmH6N3+`{ib2SOlgiMX2slI9R@|!1w(9)4s+yAa7yVg_|E|~U;#tv z04BbM+W9?L1a{SbD~eo+2ldqVgI}5o?|AoWSfTnZdO+(Os(Zǁnm00mo(ufEza zI0s-gVEk$r*-rpKi`W9f+P(W&0kFtp&_UPqN zy(SE+Y#~}2H;+M_&Bvfd?Z;;);Y&Gy;yeNd!98xTI?TG2G6Vkb`0^y#5-`iO?aBWw z9x0f}$Cor8R%jgw0Kx%`$E+8LpXf9^$MoA3#K}PNtM~p`sO_xDr&QSvjG(s+==yk+zu!W~JU`B0b-!>nXYn^ze6GAO?ke@zD z*GpdV+XKrr*5*TJX&==mqcRJ2pS%0M;r2tc7V!(0JV5 z3YP;weGNv)*bkQRO^iik?s~x^ejulghOh4ucopkd0Biz$j@^dvM8`Y8V}BjonL9H0 zW#@H+H9=zAeg{wdni51;Ze;|5eSfkZgN`2kI!nU}=t?{7$=Ni~mCH~j!F#sI7j&txDHQ+#3d_@W*MIkLySgDf3jy~kd^5eMbyTD|2596I+fRrAz}knCqc%TzBldRz z>_lgGvQExLxW#4E@YkN5RC89Zaq5vjuBAZkhmZH>nwml}fBY^~QU;jX;(=n*w3J{-k2OlX$oGgx&dX3~5ltBC2 zOtg}`z`X%bchdUXS7pWjG#>d?t(Uu_r-9=IG~)3avm$`Y}lO*J>hnjSSh==YAv7T!b^g6jn6h;W<=0@wdsOu?0aw zG%t4Z9@PNo2KaYCEiH9SxbBmz$$S!7NUmG11;k?wAoGOLqe{l3R=_PL0&X#z>qM1O zvcZJ3;M;<186EKShZSFYIpRZ+vc!b;UG-UM0hRP zHkkJia9b(Si_E<%dKu+Yt4$?*ZHCi+UrrFrdo{H}dgu={1FYn6d-W@*U0rawau@3t zilZ7pZ6kLr45}p2-Cl>~@`rXC%_r2iWK?Pec&s}A5hsdjowbiT{CUl9P=_C@QM|@j-TKkFP6JSWZ0GI(V_Cm~* z7x2)(B2d;omJ@Jc63j3?wHsAqSA9b737+JBe9Fz@{`DZ@1Ob)Gg53dry2a|>89%dj zajU=X*TssD*@~_k(H?}R9l&ot`j7!|JycrIa#tUDzv7qFk^!}EI&plztUaMD_(X0T z=pj?eC5^uJUyg{7wi;|d*gYtD3@ZEuuRs(GFewn~=-0rDkJxklZ84b*$cY6&fKew7 zHZxT)Z!anY>VEcH#08Zd_wN9H{6f}*qw}8iKjjE0Nh_U>qIx}tPr>&_52s(35OHcbTUc4&MxWa^}|jvx$W^k1_K1_ z2Ga+EHOjAzX`5&N&=j53k;dGd7$CS40Tku^Z@vQx)!z!lUn0!66C!>-IvC_%g=ZEm9xPAG!(?mYR}fIno^3f*kMhy@Ldt0U8oGLX zVoqgUFg1PZyidf&lF`p{&)%dZT7|TX!m)+!hZ{Wi<&0hHOxJMm7a5b^JL$Awz@4pf z!(ULyLcv+)#KMq&?ge5wd-(o6__K2q9l-!DEFs9GSTdaF8x7n!%5rt;_by0oT-u_# z>M+rN+sM7!VgZ)#GdwG}%Opkb)ZCFqK$y%VBkMdv4~a)XxU9>KEP-dABvd2*yVn1l zi&R+FGQjPw>K*NHso6y!D47JK%rD=b5kr2z!v=F4uZJ@%3n zOf@u$EN|9~H_-}TJfxT!F^Gu1iZ{upiXG|Kkgp=k^P_qhnY7&2Eg<^KGFv#^HYJ}z ztJ~$Yx&kOva#VOZ-Kcdbbu%$mSwrC-U;HrgVKRJOgH)s8KWngV0(c}6L*rSvoAIi)P;lv0T8qvVf%GXjs_dtw{CX5fL1mH!+^#}NP)bU@WY^ zn2s`z77P;Ka^fFEaPuT;h?k3F%!`@KvUxBXq7G8$XpCNAvS=Z$qniZ+oKhT{_IhvX z^QIQ`NR8nfu3g-DMqrt+Y7rPh9hNU_pY(Coh10U}&K%fWi2*0}K@8cCV21l=H{RKR zdvoV`#h7f)Sl}*}-1-2O9ugg(e6C?T1dY+Z=>m&uor*x0GmPjKNZyaTa!@Wpuh_B) zZNQC2;eB(Eb`!vb-Wyt68o0sG0RK-orW3Av50rJUZ@x7OvX33kSv8eLg1 zA-&6pDvFfpitAU65w*5A`Uc}5{OA(2Hh9)3X%ul?varr3J^7x5=jZS^yq&}2k0P4= z?9{a2e8=+}YuXdoi^vPf7XFO_cjWP&&mOqw?A>9KqP|Edh+0$OG6Xxb(}zCmpOJV~ z^6DCeRHMxp?`eINYwXy`k`=R78J#x!4mYvSYM5vuXH8MNWBh{d$jF1ujL)jjcF8Jm zPSj@JviV151X`()T>YLgHwjAJ935{hKpB}r`QQN=^x7NB_*dwQhA!IT-}q;7T5iw1 z3x<9?e=cTNmCU8~#9eB(*d|fTBkNu5SgO5T=e04El*R?0t1(DziBW_DwZLG^sf4SF z0mhHtqXIhseNy0RW?X`rA24@K@>{RlqmnjJ0ZTvO^e`;9%$i0i zoKy1(x;AFbAW~$qNU|1nC)g37Gl)1bfIL~;oEPWk4uB;&B zR%0;*?i*2-KraR`d@VU8kUj7W&QlTh1$=+2r^(*QEVKQw3ONxqIq~W;@6!F?Nxp+7 z2Yu%FMjKw1CCbn&`$1}`Zl`dMUB%aH9=hx5q{gnuSlyZ!_arK^KSPG|W( z5o^m6o_hG$kZM0 zVk*zTLgDuLWfAXf6_^DntaZAL2wlKaig_-$Uw|5P{r8Ry3& zvx4R;vR(Nf(VZw-|@x7bz-eBRq& zWf)Xk*Zln#1s@S~jY~zH?G0j+3>otI5iIfy;&DA{H17|jbhEZy2!3g*%{8U*10T#{ zc}ap5^rSVV@j5yfAMpHN^oT9VpBww&YDYKl0r{_BC-uW*vE4sTWJ`zBwuC4pmUvqe zi^ekLJnGVMiNlZudNy$K2jGTo z287#(XNXx(qbZ429gjLqjGnc_g;UPkix+%$la4j)bSrXFe=S&0qcK$Q$#KKI@THh? z9A0SOMSqxCNbU61JCoT7B=$zHFIeB}b%RZ-BVs?(!E)IVQR1pIoW$%<>-^HN0U1$B zN7~r3k~LL5`WY9reT#^8j{&NJ#xQKr|0zm&rXE||De{~@{COtYm{2f8PofEJ6zB2I zZ6^l)EJsxJRW*3_Q_uMJXW^nfGKZqSnV`Q15X*&Dx+oys_s-cPhBV34{6jO`CB7)J zp!DByzPA~9eknvH&qc(x?8~%Qj-OrB_}dvFc&oz}wuEPQK>3HR67G^@r`xk~)zCIK z-QH_U_#gXjNS*t0%wJd@N=;$<(9_SPuroY!`Yb)N3%qGFO9qi+zJ%z}8J~U~{tKwf z40qj(5xKt6iuP{^HXZ6>#u`VHv*qZUAtt$FyvQUY2vp}4JX@E{{oX?0D^Ncd2xH5o zXHt!is$nLRq=d5x2@4n$^5vp!v*}6L#hn^&Lay8tCz05{3A=?=(4)F&9gU$8wxuGG z8F=Tp02|H;DKTtiRAE}Hqo3jf-_h_VV!p6$y?A5AmvHdI$F)0r3&XRM8zOjFy+K=? zdWZ#8n}u<(EZ3T`gsl&D<}-HXikI9|$#umwE<_^oTE{p=X|JI zdfHb58P~AGs(7(<#fUL~1IS{^b|iHg8y2YQwg+LCt#=BBzEayhe|!@;Aol1rv#WIh z->9JU&<-C23mWBfco#SOiZk2F7#jKo%4LY`s0l6trz8m_3TH7zb2h4p8xs^}u%RKd z;4uBivpKxt&t(T5>Wn(Dui!^mMGTAUyH)$1Ih-mKFXS|G9)Ytwd)ycD2>0dU13DAu zEJ50MZRLDc+e=%o=2o1_1&t@KS(iEAosrhyF)JHbGVKb43q?VDnELx9)$T~oRwxUVbjhlGjWgb1A5JR|KZ6n7u1Di}rUR*SQf=+lS7$sWU=I|)dkOKw$>r`tXIp{)kd!OXGD z&vFc7gj_*Ou(=ur8KPxE;t$wtZI^ehn{q~D;}>2`!-v|d#WX6ACY9N@PKocd9qE;= zuaC#kLJTzpV5n<$<_Egwc)^u|-Fk>CJ5-RJ<)&)R!N5>8GOqzkA7vQB6UQ-|#fkAT z52C&M$Q{)@G9LT&*=oeYx-0&^<7V}QNa__zQY{Cuc0rwB1hxV!V4P%S;hH8?t+36~*Az2#L zh{~^t`LVqWFOc)&4>$@eEx624><=N4En-Nf7vEa!`DYmmMz|clorXl*9LFu(6zhxIhhY`N1`zbyDC92FN3ISHe-3?QVw{Svz=W;P@IfvSm!d2Ir zwCjdu>p4SBD3$Yg)Jf$v7~kF&AQd6csRBbcFhLzWuk2lZ=qb5uAW*Y8FE0*I+_El8 zt^=EoiyFXvZ5bvj@Iob_=-#UtTQ8Kt1>ZaurH5bPd)5rDzW6NmhdKJYDmvHg>I0~) z`WNa4OCS^)xnN;^eg>g+VL{R5b(;HCPc;Qa zr`PoCV{Me5qApsRbKul%h{Jrf6=8NpOr4Tgjp zQ{wFo{P|y6Ly6|MnbJ8W!j#e+Ji1U`p=W2yxf6EaT_|2Rrc4R#__bFaxt}Cv86i5E zT>Y_N$i=IezVqgjFDbyfyw`4^Ms4VM(dyB+490gK%3eQuwl#kE+LvAXeYj zQvK|i7MGqwdnHXVqgav^brb!hXMli`L>;R(R^7X|7!h>B zVX_x+Vcp5rB7tk4LUaX|f%mfpGVa~i4<7l2u?m3zFwyg|K=qog4 zexdH?U2_EFTfe`G_|dqRwwn8WZV=c<>&_@6kGQRP|HiFIm7}=p+cyto{HD?leNdra z<$aZRfbDVj-pHHXVmCH*BG!Po*3#%0^whM&FHpZY@qLy$Qqb?GME{Yu-&FH~?fUnt zha+c5SdtkJ?GEpo7`(^LKhQCu7%Z4Q{1#@wy4cK?t^b6TThguH*OuS4 zJ#XnJol}!?lR59v9uCrL)V=+(qMcExxg)fyyI(e6_~_$En&=I6B_JBl$d)T-c}_Rq zCVujaI*{!9OG;V(74tZeIc#EkF6b?@#*%FpToj27Zq(;Ih07SIiY20XuT5*_>#^78 za4;n@Q-<7|o4uy^@toa#XfraW3&rCpyTq!eENaKaxTF(8frpl1bf}p6M5N7CzmWMb z6$$lQ;MKN0Ir7^b2*W(eMcWl$iRBItixo92A*G~!S!iB8LsfE3UNf3i!5wVVw49#t zCfUbD)Sz`Uva#XwVBJ+jo`f*!{2VcW`%E8SNn{4Do0py#pWm{Fzfh(_9jXSu6@4c^ zX%CmZz<|$8$Mv*EE7WBBcf#F%ww2JT=7-NdOPI+USHQfI(G8DHER?LI7i1>fWA5*9 z(`|I!Jck-z5r~CYx?`zABl{H|Xm5^X;bE$;E+jI&2usdnuM3&1vpI=Zxk%Yo++5GLjB4Lq0`~cf(f0`^&M1g{P;i? ziseS0+wui4X>+Hen&UE@b#NbnORvGf?vn{a&%I!>)x&&n*@$Se^Tb3$wy?z_eQ7rc z=s}JQw@$;yFfv!W4f-ra{*V0?dWIU}ZR*ZuS`-k|(|QBg(fkPUEe9BMo<=39CFQ-M z!EB@@0bMS1{_|bbXK(S3RoFM|5bNI9SCLnzAaBw=2S}ugz@jZi*%q8{ z-4jA&M1Ws}d($=cgRNlZ83y5Ex`Gzy8jI%wV;4yZ45vXUn8DhIkzYi7=3MIWy}k4b z&fj7J;8BT^*_5BB3HfY3y-b--jrF03nM`Uo``tf=F;TZ-|ll z>vFth`Mic8YjdavhbE;Yb&lR^)0{xR`4-do&~);_U33?^Cukbs#WDK`t)*H@ZRr8V z&IshH7A}f4KBRDs;!>{46@OW4gF)ZWhuk%9SdyB8jA<}&p)#lNN}iuux%8;KUy5jJ zHn$7dXWzPP9(z4r{H;0#W6tLnbnuywSF^EMmmt?{0H-MwoAvc6B!(_=7f+=%RuxIS zSLW&5mFc8`f^sZVKt4aIbAJ_Jv}(Lk$-JR(BZa16-nU;=%&=FCb<6dJ(R&LXSB*=t z{UtgXktl?mkXxJ5Erlzm-b4|+vf{#YBcpx3$Yj}Fv|!(QqbiFkB=W@GYbtS*=1>ug zmMmzGJ|MpR?9vI9;iN#LEJyT`l17+G>(2Q+7eP>2!D;3P_CN-w+nz4J{ z-3bk3)Z36`cJ?*3^X5LVF7n0H^HmG*4n62A_XSL4<*AIIf}3O5*{g9`DF2t)k?W{S zkOB|Is6oa+%E>pLSqSkVQSpnaNmh+Ip@c;{#!E*%P)?I#u&H>H3);5o4Q9E^Ucx)6 z6jO370GuWhfGKmYKd(AOk&5#7rut?Bwn`R4W9an=@Ju7{B<*;)gHne8+ci(Hb+1Gf zHi~*#JfH%CS4(PWCKg{4KBXvy6)y7E>ontJ1Q$8MN@h!0pZXB=HM_5}6YkI%id|B- z6w

      1s=i3ZI&Aj7`C;6yr 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�cwHlP`{C0{iz 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 diff --git a/tests/assets/hlabel_classification/images/test/20.jpg b/tests/assets/hlabel_classification/images/test/20.jpg deleted file mode 100644 index 7764a1776264b1b10a19fcf7e5f1b3a1afa84fd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157413 zcmeFa2UJsAw>BI^Kq;a^6p$vp2ucY8f`}M;CkYThq(dmu5l|5kkVx+Y1nDFYigZK~ zl-?mky3(5^Yt8n|x#yZYIUYRz20E{* zsG^K!3bQPLZA_Bd0iXmXZqSP<|eCiiDK( z)M-*OveT!5-cNw@pwkz~E?yOpC#Tafqu_9%7xj-yKf`&qq#mr>yUHbI?)v;J<)zCE zj7;1-ynOru;t+|Ol2XzN_n?YO$||aQFgQZrzz}I+X=QC=Ylm`k_we+3}bsAZX7K>4F&_m_P{>d39yHM&yjlqWyd z3VqN-u(!UT{yo*pUe)EoQKXhl9WORn?i_>Ukom$f($h_C-`rWwo*d#?WvC@V7e3NNsRDpwJ7KlSWr^4&c8Aj|MYOTRx2w z{&$mR4mO!m(b^K{=koAa@t0S)86#5z^4R8<;Ff*7#)(75=K|8@=9aYQJKgzVuf*oD zR+SjipDjaAem?a8TlX8q)9XOlDfu#<{oVfN!T%&3;0<4&yk0G60a#c>%Rt_iw|>Yk z11ne*7*G*P66RkCtJ<>nn)r0JS&ES@TWxx1?D^fMW6)>SRqw54NOvz%;K#CH%=a&f z4y{@DhSsMorGB(D>OB#j5qVa0w6L&8Pe)MBmpECKEf*5j$>tBbB`Sc&$^rH@lSj{kz9`+c0c2y=w^R=wBuKUB)cfnUgQ^S(x|9fKa7OG9D}+JuEWdd0T^u|zif4fcOI*#GTd|33~g3JpHFcQIY; zOrADuM+!9R#0IN7v`@P>kb;v494sq8Q^W3F~}&Lq4SISO>Co|ou?mn&}tpMx6sRS z;|g6K2-lFv$=aK7Y5HXv&!7L*?j#QW`gumY#UM4N)XLf^Yws-wn|JUg-U~x)Jms8X zDG;u;w}UrROEP)K!F}r%CyV>f9PI|WfWsA@YeUQDiq$V9wILKwllou9sKR0A;-er}VsK&~N+ylsAM=9V0@ zf7LMvDI03~iG4fFDU_O-G$5eKPB6!SL;0ROMZDaLjf2IorpT}knzCb%M>V|~Ems*C zT(-kUK;=O^`G^2mt}C;EsbV%AO0P}wcl(nY>TGdI4Te3V3@X)Y>Pf8=u>xEJ$+52Ob7Y^1un`O0R>G2 z^9F9SEQy{8-N;fKh%i+h{*Z_?{I1Cm_EM||u?a7_+0&KzlP}vluSHmEXu5%~Nr^pm zvv_f(>S7avOWUen_l#i~%-3*yhmFh8LUl(cXmYSAs$x><8U;8R3KmU{c_$#EgOG>P z!$JZ75Bj_P&4Yi14hU?=-3Xt~WWE-_7V&TiW+yKofBL!9X}F?s;UsFYDZYrDg%|YT zAb07=FV0$D;R^R*MCXA5HHC$NXj89hZ`EGxH)337k?2sA#)3_c6}^rDhKye4&R^M> zkTQdDle|HUjIzyphy0i_BjFb*6U~c~#~{jF`Y`Hz4z?sssFN|rSA!WTE+?&`+|9z5 zzMG>ynf$(QtNnH5sYX>fLoL%PZ(9EA zq|b!yzL?si**|Iiw$ZacaH!hR_1u7vbUF0gWQm}K+Pm?Lv7_~f*txt-{DO*#t8yEv zJBeWveyG^eFl#{YR?1^*Km}UyRlO6t*RTJVI}<@YmFjwl?^-gsTWoXVZx?jX(lsff zDFOOtUE8MHWZD`Jbw}D0y0s;w<_;IT3xgOh9(Am+C=wawpX8sLB(Hp(>9G2U%Y(Jt zBYiu*1LOMh8XC=4pmFN24*36`@Bcsfs@RKJ=ao4lq1^N&b@m=C->-$<*!@(Lw_T9m z&RhO?y*I7qYY*W@$M$*%rzGdrHvV%oiz4B<-1z-P`)mk8!Z-0<{g{K>ChUtux4m%l zG+WJBn1b=L`1j73Dj>vt_OI~a|Isl@E!-V#P9HW+)HRh~$3sW2P7Pa<3i#E^3R0Gf zzw690YPn`l-@zX2y$U9BIa3<6-4a6pfJjn9g&f|zmkF!+R;BUbbY(1`MC27*0_R?E6;s0qcv=VAs zwNfFqFJP3&H0&8x-K-Fvkx}Sb@V&DV5P0NY8D#%G*I#>6^M9S+YGj||SC}L?37&FD zG}>+iMq~U{DL}V^Jp#9*XLwn{b-1!a^u@Ou^C(GcU+;TSDHrzGP@_SURJ-W&qDmdX z7ODDWwr zewdHchqy0TzCKEFl^Hw#G){3x!`4gfvPihgnBV@uoj;;ghauEm^Ae4~r~dCmj^cV5 zaj)WilO(I`=`Nv(@B7L7g{tPT@`r-F*;bO0w?wD7xq+N3nCveD9b3D~47wTBX0xAJ zn|g+ua79hwd0mx!F&o{tVzkoS?Gd#U2yhq@W3|vy85bn?ENw=U@I8p;vs(9>|G|=7 zqlVk(E}_QtQXVv4p_}&qlr6Bo>G&_ZolC0PtjquNT@bgg)N};i=v|3gF<03054Tt6 z$J35#8uC58){V~R3SIWt+()e9Ff*x5u0fR{hP|1t;n@Zr zKSsxg6=XsNrOnLRDzy&8dhW0eBpbzFp8V$R`ao4~Z81AG&!q_PUqppy=C{viWJbN}Ts5 zdeOzxOvKmvEYcVF_%HnspR3%hqqy(XZ-~n?KnBmx|Gpp*u&3=$918A>QT^N(fGKP*&~(M(ZeMljI!3Vo0CEx zY?>LwzKLUhF3|!%r%XYsN zYnxv;bouMXr(gfT(%mIw{j3-|2Kg85_RU7PdS}!m5fE*U^pG(tLpDdlxx0jb`kK83 zvtv+H<}qjzJ4Q9EP;(;>Ph9n^eeWQ*F79^#*N^$+Q)c?KpKZHWt|@VEYP2n*=on;S zI>%-+cen*C4vnl#K8L5-LkN8E#tL9A?3dOQJGl^vndV-Tt7TGkx+9R*jVwFkJI?L~R()0ud` zWt?A_p()93+|f#M+Ruk<#~`rYvfVLA^k?gKwAF_UEt8C#w*Gx-YM8H1hA1)%uNmHa&pD z((3oOD)*OY>b8ey*HKT2EIq!;UN`3Ri?U1=oqHY}gFe(vX&iOb9)nVUtJveUmD{#E z0y_F~Z;SlqB2IJ-Gwd%WaGW1bQQ|MA`a7_>J5m}Jpze^5wXYWy*1_XLnH-?8aMvJLt~ z9Q^#PJ86S8!GC^Uxy+}dUsqDpd{F#L3+?3IUay-AKDu2ub*B&wgHL3BO^x(B(YT59 zMc(YXlM`x5!s&o^ZXyy_xAT0E$DnB_pq$RD;{`rQzv*B0r%UA-*bOGMWmM=l)bzW* z1xx^YBtE+uUX>|i;eQM=0i=%3UdH}bvR~>m%MAt$=uB71pH>vHt61*$r}YRF`>N0m zf2t9+{ok#s=1;3a^=p}JK(|Ea@pNw%)rh|>Da(Xb$HM<%+Ab5o37V{PE=Hxd(F9iJ z0WCGtgO_zvTBcM>LVewG+kg>=Ta8LhjkaV|&<2*L6T6Q=9J#xFv{XyH*=Z*LlIt?Y zNRRk(z=51#TblxON$AT#=En-^d9^Y_>0D1H^p&B?ZBB z6Gj5Sv7>+En8F*B{mC*puJ87`UsiMkXC4M({kkhpc&uf5ZV|uZ=ynWp1n@$t(Wzm+ zJ)=S?&?9HzkLhmH6N3+`{ib2SOlgiMX2slI9R@|!1w(9)4s+yAa7yVg_|E|~U;#tv z04BbM+W9?L1a{SbD~eo+2ldqVgI}5o?|AoWSfTnZdO+(Os(Zǁnm00mo(ufEza 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

  3. yF_s#qVM?C7~dXx|SmXq@mrv#}ZE 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 diff --git a/for_developers/images/product_design/core_design_drawing.drawio b/for_developers/images/product_design/core_design_drawing.drawio deleted file mode 100644 index 1732277c1c9..00000000000 --- a/for_developers/images/product_design/core_design_drawing.drawio +++ /dev/null @@ -1,1381 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/for_developers/images/product_design/reuse_model.png b/for_developers/images/product_design/reuse_model.png deleted file mode 100644 index 343fe953139942735727f5d56853a8d2deb183b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270631 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